0%


最近对现有项目做性能优化时,发现WebView存在内存泄漏的问题,导致外部类Activity无法被释放,由于网络延时、Session、Cookies、内核线程、HTML5调系统音频、视频组件的引用链条无法及时打断,除了及时调它的destroy方法进行清理以外,还需要把webView放到一个新的进程,修改其Activity的进程,通过AIDL与主线程通信,避免内存泄漏的问题,因此需要封装一个独立进程的WebView,在此做一个记录


封装WebView

自定义WebView

初始化WebView默认设置,设置javascript调用接口,回调js的dispatchEvent方法做事件分发,web进程初始化主进程调用接口,加载url时重置touch状态,监听用户的触摸操作,通过是否为用户点击(touch状态)判断当前跳转是否为重定向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
open class BaseWebView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : WebView(context, attrs, defStyleAttr), BaseWebViewClient.WebviewTouch {
private var webViewCallBack: WebViewCallBack? = null
private var mHeaders: HashMap<String, String> ?= null
private var isTouchByUser: Boolean = false
private var redirectIntercept = false // 是否拦截重定向
protected var mContext: Context? = null

init {
init(context)
}
fun registerWebViewCallBack(webViewCallBack: WebViewCallBack) {
this.webViewCallBack = webViewCallBack
webViewClient = BaseWebViewClient(this, webViewCallBack, mHeaders, this, redirectIntercept)
}

fun setHeaders(mHeaders: HashMap<String, String>) {
this.mHeaders = mHeaders
}

fun setRedirectIntercept(intercept: Boolean){
redirectIntercept = intercept
}

protected fun init(context: Context?) {
mContext = context
// 初始化默认设置
WebviewDefaultSetting.getInstance().toSetting(this)
// javascript调用接口
addJavascriptInterface(this, "webview")
// web进程初始化主进程调用接口
CommandDispatcher.getInstance().initAidlConnect(getContext())
}


@JavascriptInterface
fun post(cmd: String, param: String) {
// web进程的主线程执行
Handler().post {
try {
if (webViewCallBack != null) {
CommandDispatcher.getInstance().execBySelf(context, cmd, param, this@BaseWebView)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}

override fun loadUrl(url: String) {
if (mHeaders == null) {
super.loadUrl(url)
} else {
super.loadUrl(url, mHeaders)
}
Log.e(TAG, "load url: $url")
resetAllStateInternal(url)
}

/**
* 处理header数据请求
* @param url String
* @param additionalHttpHeaders Map<String?, String?>
*/
override fun loadUrl(url: String, additionalHttpHeaders: Map<String?, String?>?) {
super.loadUrl(url, additionalHttpHeaders)
Log.e(TAG, "load url: $url")
resetAllStateInternal(url)
}

/**
* 回调js的callback方法
* @param response String?
*/
fun handleCallback(response: String?) {
if (!TextUtils.isEmpty(response)) {
val trigger = "javascript:callback($response)"
evaluateJavascript(trigger, null)
}
}

/**
* 回调js的cmd方法
* @param cmd String
* @param param Any?
*/
fun loadJS(cmd: String, param: Any?) {
val trigger = "javascript:" + cmd + "(" + Gson().toJson(param) + ")"
evaluateJavascript(trigger, null)
}

/**
* 回调js的dispatchEvent方法做事件分发
* @param name String
*/
fun dispatchEvent(name: String) {
val param = HashMap<String,String>(1)
param["name"] = name
loadJS("dispatchEvent", param)
}

private fun resetAllStateInternal(url: String) {
// url为空或者回调js方法直接返回
if (!TextUtils.isEmpty(url) && url.startsWith("javascript:")) {
return
}
resetAllState()
}

// 加载url时重置touch状态
protected fun resetAllState() {
isTouchByUser = false
}

override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> isTouchByUser = true
}
return super.onTouchEvent(event)
}

companion object {
private const val TAG = "BaseWebView"
const val CONTENT_SCHEME = "file:///android_asset/"
}

override fun isTouchByUser(): Boolean {
return isTouchByUser
}
}

封装WebViewClient

回调接口逻辑,url跳转判断,重定向处理,对特殊链接统一处理,刷新处理,拦截指定url处理,判断是否加载完成,封装请求头,ssl错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
class BaseWebViewClient(
val webView: WebView, // 封装的回调
val webViewCallBack: WebViewCallBack, // 封装的回调
val mHeaders: HashMap<String, String>?, // 请求头
val mWebviewTouch: WebviewTouch, // 是否有点击操作
val redirectIntercept: Boolean // 是否拦截重定向
) : WebViewClient() {
var isReady = false // 是否加载完成

interface WebviewTouch {
fun isTouchByUser(): Boolean
}

/**
* url重定向会执行此方法以及点击页面某些链接也会执行此方法
*
* @return true:表示当前url已经加载完成,即使url还会重定向都不会再进行加载 false 表示此url默认由系统处理,该重定向还是重定向,直到加载完成
*/
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
Log.e(TAG, "shouldOverrideUrlLoading url: $url")
// 未发生过点击,当前是重定向且不拦截重定向
if (!mWebviewTouch.isTouchByUser() && !redirectIntercept) {
return super.shouldOverrideUrlLoading(view, url)
}
// 如果链接跟当前链接一样,表示刷新
if (webView.url == url) {
return super.shouldOverrideUrlLoading(view, url)
}
// 特殊链接处理,跳转手机自带应用
if (handleLinked(url)) {
return true
}
// 拦截指定url
if (webViewCallBack.overrideUrlLoading(url)) {
return true
}
// 控制页面中点开新的链接在当前webView中打开
view.loadUrl(url, mHeaders)
return true
}

@RequiresApi(api = Build.VERSION_CODES.N)
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
Log.e(TAG, "shouldOverrideUrlLoading url: " + request.url)
// 当前链接的重定向
if (!mWebviewTouch.isTouchByUser() && !redirectIntercept) {
return super.shouldOverrideUrlLoading(view, request)
}
// 如果链接跟当前链接一样,表示刷新
if (webView.url == request.url.toString()) {
return super.shouldOverrideUrlLoading(view, request)
}
// 需要跳转应用的特殊链接
if (handleLinked(request.url.toString())) {
return true
}
// 拦截指定url
if (webViewCallBack.overrideUrlLoading(request.url.toString())) {
return true
}
// 控制页面中点开新的链接在当前webView中打开
view.loadUrl(request.url.toString(), mHeaders)
return true
}

/**
* 支持电话、短信、邮件、地图跳转,跳转的都是手机系统自带的应用
*/
private fun handleLinked(url: String): Boolean {
if (url.startsWith(WebView.SCHEME_TEL)
|| url.startsWith(SCHEME_SMS)
|| url.startsWith(WebView.SCHEME_MAILTO)
|| url.startsWith(WebView.SCHEME_GEO)
) {
try {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url)
webView.context.startActivity(intent)
} catch (ignored: ActivityNotFoundException) {
ignored.printStackTrace()
}
return true
}
return false
}

override fun onPageFinished(view: WebView?, url: String?) {
Log.e(TAG, "onPageFinished url:$url")
if (url == null) return
if (!TextUtils.isEmpty(url) && url.startsWith(CONTENT_SCHEME)) {
isReady = true
}
webViewCallBack.pageFinished(url)
}

override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
Log.e(TAG, "onPageStarted url: $url")
if (url == null) return
webViewCallBack.pageStarted(url)
}


override fun onScaleChanged(view: WebView?, oldScale: Float, newScale: Float) {
super.onScaleChanged(view, oldScale, newScale)
}

@TargetApi(21)
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
return shouldInterceptRequest(view, request.url.toString())
}

/**
* 默认继续加载
* @param view WebView
* @param url String
* @return WebResourceResponse?
*/
override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? {
return null
}

/**
* webview加载错误处理
* @param view WebView
* @param errorCode Int
* @param description String
* @param failingUrl String
*/
override fun onReceivedError(
view: WebView,
errorCode: Int,
description: String,
failingUrl: String
) {
super.onReceivedError(view, errorCode, description, failingUrl)
Log.e(
TAG,
"webview error$errorCode + $description"
)
webViewCallBack.onError(errorCode,description,failingUrl)
}

/**
* SSL错误处理
* @param view
* @param handler
* @param error
*/
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
var message = webView.context.getString(R.string.ssl_error)
when (error.primaryError) {
SslError.SSL_UNTRUSTED -> message =
webView.context.getString(R.string.ssl_error_not_trust)
SslError.SSL_EXPIRED -> message = webView.context.getString(R.string.ssl_error_expired)
SslError.SSL_IDMISMATCH -> message =
webView.context.getString(R.string.ssl_error_mismatch)
SslError.SSL_NOTYETVALID -> message =
webView.context.getString(R.string.ssl_error_not_valid)
}
message += webView.context.getString(R.string.ssl_error_continue_open)
Log.v(TAG,message)
}

companion object {
private const val TAG = "WebviewClient"
const val SCHEME_SMS = "sms:"
}
}

封装WebChromeClient

处理文件选择和相册选择回调,js提示回调,进度刷新回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
class BaseWebChromeClient(private val progressHandler: Handler) : WebChromeClient() {
private var mFilePathCallback: ValueCallback<Array<Uri>>? = null // 文件选择回调
private var mCameraPhotoPath: String? = null // 相册选择回调
override fun onReceivedTitle(view: WebView, title: String) {
super.onReceivedTitle(view, title)
if (view is ProgressWebView) {
if (!TextUtils.isEmpty(title)) {
val params = ArrayMap<String, String>()
params[WebConstants.COMMAND_UPDATE_TITLE_PARAMS] = title
// 调起刷新标题命令
(view as BaseWebView).post(WebConstants.COMMAND_UPDATE_TITLE, Gson().toJson(params))
}
}
}

// 进度更新回调
override fun onProgressChanged(view: WebView, newProgress: Int) {
var newProgress = newProgress
val message = Message()
if (newProgress == 100) {
message.obj = newProgress
progressHandler.sendMessageDelayed(message, 200)
} else {
if (newProgress < 10) {
newProgress = 10
}
message.obj = newProgress
progressHandler.sendMessage(message)
}
super.onProgressChanged(view, newProgress)
}

// js 提示回调
override fun onJsAlert(view: WebView, url: String, message: String, result: JsResult): Boolean {
AlertDialog.Builder(view.context)
.setTitle(R.string.dialog_alert_title)
.setMessage(message)
.setPositiveButton(
R.string.ok
) { dialoginterface, i -> //按钮事件
Toast.makeText(
view.context,
view.context.getString(R.string.ok) + " clicked.",
Toast.LENGTH_LONG
).show()
}.show()
//result.confirm();// 不加这行代码,会造成Alert劫持:Alert只会弹出一次,并且WebView会卡死
return true
}

//文件选择回调
override fun onShowFileChooser(
webView: WebView,
filePathCallback: ValueCallback<Array<Uri>>,
fileChooserParams: FileChooserParams
): Boolean {
mFilePathCallback?.onReceiveValue(null)
mFilePathCallback = filePathCallback
var takePictureIntent:Intent? = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
if (takePictureIntent?.resolveActivity(webView.context.packageManager) != null) {
// Create the File where the photo should go
var photoFile: File? = null
try {
photoFile = createImageFile()
takePictureIntent.putExtra("PhotoPath", mCameraPhotoPath)
} catch (ex: IOException) {
ex.printStackTrace()
}

// Continue only if the File was successfully created
if (photoFile != null) {
mCameraPhotoPath = "file:" + photoFile.absolutePath
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(photoFile))
} else {
takePictureIntent = null
}
}
if (takePictureIntent != null && mFilePathCallback != null){
(webView as BaseWebView).webViewCallBack?.onShowFileChooser(takePictureIntent, mFilePathCallback!!)
}
return true
}

/**
* More info this method can be found at
* http://developer.android.com/training/camera/photobasics.html
*
* @return
* @throws IOException
*/
@Throws(IOException::class)
private fun createImageFile(): File {
// Create an image file name
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
val imageFileName = "JPEG_" + timeStamp + "_"
val storageDir =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
return File.createTempFile(imageFileName, ".jpg", storageDir)
}
}

封装WebView回调接口

1
2
3
4
5
6
7
8
9
10
11
interface WebViewCallBack {
fun pageStarted(url: String) // 页面开始加载
fun pageFinished(url: String) // 页面加载完成
fun overrideUrlLoading(url: String): Boolean // 拦截url
fun onError(errorCode: Int,description: String,failingUrl:String) // 错误回调
// 执行操作
fun onShowFileChooser(
cameraIntent: Intent,
filePathCallback: ValueCallback<Array<Uri>>
)
}

封装进度条

大部分app展示web页面顶部有个web页的加载进度条提升交互体验

定义进度条操作接口

1
2
3
4
5
6
interface BaseProgressSpec {
fun show()
fun hide()
fun reset()
fun setProgress(newProgress: Int)
}

封装进度条控制逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class IndicatorHandler{
var baseProgressSpec: BaseProgressSpec? = null // 进度条接口

/**
* 单例
*/
companion object{
@Volatile private var instance: IndicatorHandler? = null
fun getInstance() = instance?: synchronized(this){
instance?: IndicatorHandler().also { instance = it }
}
}

/**
* 进度刷新
* @param newProgress Int
*/
fun progress(newProgress: Int) {
when (newProgress) {
0 -> {
reset()
}
in 1..10 -> {
showProgressBar()
}
in 11..94 -> {
setProgressBar(newProgress)
}
else -> {
setProgressBar(newProgress)
finish()
}
}
}


fun reset() {
baseProgressSpec?.reset()
}

fun finish() {
baseProgressSpec?.hide()
}

fun setProgressBar(n: Int) {
baseProgressSpec?.setProgress(n)
}

fun showProgressBar() {
baseProgressSpec?.show()
}

fun inJectProgressView(baseProgressSpec: BaseProgressSpec?):IndicatorHandler {
this.baseProgressSpec = baseProgressSpec
return this
}
}

自定义进度条

自定义FrameLayout布局,实现进度条操作接口,进度小于95时渲染匀速动画,大于等于95时渲染透明加减速动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
class WebProgressBar @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr),BaseProgressSpec {
/**
* 进度条颜色
*/
private var mColor = 0

/**
* 进度条的画笔
*/
private var mPaint: Paint = Paint()

/**
* 进度条动画
*/
private var mAnimator: Animator? = null

/**
* 控件的宽度
*/
private var mTargetWidth = 0

companion object{
/**
* 默认匀速动画最大的时长
*/
const val MAX_UNIFORM_SPEED_DURATION = 5 * 1000

/**
* 默认加速后减速动画最大时长
*/
const val MAX_DECELERATE_SPEED_DURATION = 600

/**
* 结束动画时长 , Fade out 。
*/
const val DO_END_ANIMATION_DURATION = 300

/**
* 当前匀速动画最大的时长
*/
var CURRENT_MAX_UNIFORM_SPEED_DURATION = MAX_UNIFORM_SPEED_DURATION

/**
* 当前加速后减速动画最大时长
*/
var CURRENT_MAX_DECELERATE_SPEED_DURATION = MAX_DECELERATE_SPEED_DURATION

/**
* 标志当前进度条的状态
*/
private var TAG = 0
const val UN_START = 0
const val STARTED = 1
const val FINISH = 2

/**
* 默认的高度
*/
var WEB_PROGRESS_DEFAULT_HEIGHT = 3
}

init {
init(context)
}

private fun init(context: Context) {
mColor = Color.parseColor("#c15d3e")
mPaint.isAntiAlias = true
mPaint.color = mColor
mPaint.isDither = true
mPaint.strokeCap = Paint.Cap.SQUARE
mTargetWidth = context.resources.displayMetrics.widthPixels
WEB_PROGRESS_DEFAULT_HEIGHT = dipToPx(context, 2.5f)
}

private fun setColor(color: Int) {
mColor = color
mPaint.color = color
}

fun setColor(color: String) {
this.setColor(Color.parseColor(color))
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val wMode = MeasureSpec.getMode(widthMeasureSpec)
val hMode = MeasureSpec.getMode(heightMeasureSpec)
var w = MeasureSpec.getSize(widthMeasureSpec)
var h = MeasureSpec.getSize(heightMeasureSpec)
if (wMode == MeasureSpec.AT_MOST) {
w =
if (w <= context.resources.displayMetrics.widthPixels) w else context.resources.displayMetrics.widthPixels
}
if (hMode == MeasureSpec.AT_MOST) {
h = WEB_PROGRESS_DEFAULT_HEIGHT
}
setMeasuredDimension(w, h)
}

private var currentProgress = 0f

override fun onDraw(canvas: Canvas?) {}

override fun dispatchDraw(canvas: Canvas) {
canvas.drawRect(
0f,
0f,
currentProgress / 100 * java.lang.Float.valueOf(this.width.toFloat()),
this.height.toFloat(),
mPaint
)
}

override fun show() {
if (visibility == GONE) {
this.visibility = VISIBLE
currentProgress = 0f
startAnim(false)
}
}

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mTargetWidth = measuredWidth
val screenWidth = context.resources.displayMetrics.widthPixels
if (mTargetWidth >= screenWidth) {
CURRENT_MAX_DECELERATE_SPEED_DURATION = MAX_DECELERATE_SPEED_DURATION
CURRENT_MAX_UNIFORM_SPEED_DURATION = MAX_UNIFORM_SPEED_DURATION
} else {
//取比值
val rate = mTargetWidth / java.lang.Float.valueOf(screenWidth.toFloat())
CURRENT_MAX_UNIFORM_SPEED_DURATION = (MAX_UNIFORM_SPEED_DURATION * rate).toInt()
CURRENT_MAX_DECELERATE_SPEED_DURATION = (MAX_DECELERATE_SPEED_DURATION * rate).toInt()
}
}

fun setProgress(progress: Float) {
if (visibility == GONE) {
visibility = VISIBLE
}
if (progress < 95f) return
if (TAG != FINISH) {
startAnim(true)
}
}

override fun hide() {
TAG = FINISH
}


private var target = 0f


/**
* 开始动画
* @param isFinished Boolean
*/
private fun startAnim(isFinished: Boolean) {
val v: Float = if (isFinished) 100F else 95.toFloat()
if (mAnimator?.isStarted == true) {
mAnimator?.cancel()
}
// 刷新当前进度
currentProgress = if (currentProgress == 0f) 0.00000001f else currentProgress
// 进度为0-94,匀速动画
if (!isFinished) {
val mAnimator = ValueAnimator.ofFloat(currentProgress, v)
val residue = 1f - currentProgress / 100 - 0.05f
// 使用匀速插值器
mAnimator.interpolator = LinearInterpolator()
mAnimator.duration = (residue * CURRENT_MAX_UNIFORM_SPEED_DURATION).toLong()
mAnimator.addUpdateListener(mAnimatorUpdateListener)
mAnimator.start()
this.mAnimator = mAnimator
}
// 进度大于等于95后执行透明减速的动画
else {
var segment95Animator: ValueAnimator? = null
// 使用减速插值器
if (currentProgress < 95f) {
segment95Animator = ValueAnimator.ofFloat(currentProgress, 95f)
val residue = 1f - currentProgress / 100f - 0.05f
segment95Animator.duration =
(residue * CURRENT_MAX_DECELERATE_SPEED_DURATION).toLong()
segment95Animator.interpolator = DecelerateInterpolator()
segment95Animator.addUpdateListener(mAnimatorUpdateListener)
}
// alpha动画
val mObjectAnimator = ObjectAnimator.ofFloat(this, "alpha", 1f, 0f)
mObjectAnimator.duration = DO_END_ANIMATION_DURATION.toLong()
val mValueAnimatorEnd = ValueAnimator.ofFloat(95f, 100f)
mValueAnimatorEnd.duration = DO_END_ANIMATION_DURATION.toLong()
mValueAnimatorEnd.addUpdateListener(mAnimatorUpdateListener)
var mAnimatorSet = AnimatorSet()
mAnimatorSet.playTogether(mObjectAnimator, mValueAnimatorEnd)
if (segment95Animator != null) {
val mAnimatorSet1 = AnimatorSet()
// 执行alpha动画并刷新进度,后执行减速动画
mAnimatorSet1.play(mAnimatorSet).after(segment95Animator)
mAnimatorSet = mAnimatorSet1
}
mAnimatorSet.addListener(mAnimatorListenerAdapter)
mAnimatorSet.start()
mAnimator = mAnimatorSet
}
TAG = STARTED
target = v
}

// 动画刷新回调,刷新进度
private val mAnimatorUpdateListener =
ValueAnimator.AnimatorUpdateListener { animation ->
val t = animation.animatedValue as Float
currentProgress = t
this@WebProgressBar.invalidate()
}

// 监听动画完成
private val mAnimatorListenerAdapter: AnimatorListenerAdapter =
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
doEnd()
}
}

// 释放动画资源
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
/**
* animator cause leak , if not cancel;
*/
if (mAnimator?.isStarted == true) {
mAnimator?.cancel()
mAnimator = null
}
}

// 进度条执行完处理
private fun doEnd() {
if (TAG == FINISH && currentProgress == 100f) {
visibility = GONE
currentProgress = 0f
this.alpha = 1f
}
TAG = UN_START
}

// 状态重置
override fun reset() {
currentProgress = 0f
if (mAnimator?.isStarted == true) mAnimator?.cancel()
}

override fun setProgress(newProgress: Int) {
setProgress(newProgress.toFloat())
}
}

封装带进度条的WebView

设置自定义进度条,添加到WebView,传入主线程handler调用刷新进度条逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class ProgressWebView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : BaseWebView(context,attrs,defStyleAttr) {
private var indicatorHandler: IndicatorHandler? = null
private var progressBar: WebProgressBar = WebProgressBar(context)
// 主线程调刷新进度条逻辑
private val mHandler: Handler = object :Handler(Looper.getMainLooper()){
override fun handleMessage(msg: Message) {
val progress = msg.obj as Int
indicatorHandler?.progress(progress)
}
}

init {
init()
}

private fun init() {
progressBar.layoutParams = LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
progressBar.visibility = GONE
addView(progressBar)
indicatorHandler = IndicatorHandler.getInstance().inJectProgressView(progressBar)
webChromeClient = BaseWebChromeClient(mHandler)
}
}

封装带WebView的Fragment

  • fragment基类,统一设置标题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    open class BaseFragment: Fragment() {
    protected var mContext: Context? = null

    fun setTitle(titleId: Int) {
    activity?.setTitle(titleId)
    }

    fun setTitle(title: CharSequence?) {
    activity?.title = title
    }

    override fun onAttach(context: Context) {
    super.onAttach(context)
    this.mContext = context
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    if (mContext == null){
    mContext = context
    }
    }

    override fun getContext(): Context? {
    return if (super.getContext() == null) mContext else super.getContext()
    }
    }
  • 封装带Webview的fragment基类,处理带header请求,定义页面开始加载,结束加载,指定url拦截,加载错误回调,是否重定向拦截,网页返回按键处理,相册文件选择回调处理,页面销毁时释放WebView

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    abstract class BaseWebviewFragment : BaseFragment(), WebViewCallBack {
    companion object {
    const val INFO_HEADERS = "info_headers"
    const val REDIRECT_INTERCEPT = "redirect_intercept"
    const val REQUEST_CODE = 1
    }

    var webView: BaseWebView? = null
    protected var headers: HashMap<String, String> = HashMap()
    var webUrl: String? = null
    var pageStarted: ((String) -> Unit)? = null
    var pageFinished: ((String) -> Unit)? = null
    var overrideUrlLoading: ((String) -> Boolean)? = null
    var onError: ((Int, String, String) -> Unit)? = null
    var onShowFileChooser: ((Intent, ValueCallback<Array<Uri>>) -> Unit)? = null
    private var redirectIntercept = false
    @LayoutRes
    protected abstract fun getLayoutRes(): Int

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val bundle = arguments
    if (bundle != null) {
    webUrl = bundle.getString(WebConstants.INTENT_TAG_URL)
    if (bundle.containsKey(INFO_HEADERS)) {
    headers =
    bundle.getSerializable(INFO_HEADERS) as HashMap<String, String>
    redirectIntercept = bundle.getBoolean(REDIRECT_INTERCEPT)
    }
    }
    // 注册吐司命令
    CommandsManager.getInstance().registerCommand(ToastCommand())
    }

    override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
    ): View? {
    val view = inflater.inflate(getLayoutRes(), container, false)
    webView = view.findViewById(R.id.web_view)
    webView?.setHeaders(headers)
    webView?.setRedirectIntercept(redirectIntercept)
    return view
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    webView?.registerWebViewCallBack(this)
    loadUrl()
    }

    protected open fun loadUrl() {
    webUrl?.let {
    webView?.loadUrl(it)
    }
    }

    override fun onResume() {
    super.onResume()
    Log.v("BaseWebviewFragment","onResume")
    webView?.dispatchEvent("pageResume")
    webView?.onResume()
    }

    override fun onPause() {
    super.onPause()
    Log.v("BaseWebviewFragment","onPause")
    webView?.dispatchEvent("pagePause")
    webView?.onPause()
    }

    override fun onStop() {
    super.onStop()
    webView?.dispatchEvent("pageStop")
    }

    override fun onDestroyView() {
    super.onDestroyView()
    webView?.dispatchEvent("pageDestroy")
    clearWebView(webView)
    }

    open fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
    return if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_DOWN) {
    onBackHandle()
    } else false
    }


    override fun pageStarted(url: String) {
    this.pageStarted?.invoke(url)
    }

    override fun pageFinished(url: String) {
    this.pageFinished?.invoke(url)
    }

    override fun overrideUrlLoading(url: String): Boolean {
    this.overrideUrlLoading?.let {
    return it.invoke(url)
    }
    return false
    }

    override fun onError(errorCode: Int, description: String, failingUrl: String) {
    onError?.invoke(errorCode, description, failingUrl)
    }

    /**
    * 处理返回
    * @return Boolean
    */
    protected open fun onBackHandle(): Boolean {
    return if (webView != null) {
    if (webView!!.canGoBack()) {
    webView?.goBack()
    true
    } else {
    false
    }
    } else false
    }

    private fun clearWebView(m: WebView?) {
    val m: WebView? = m ?: return
    // 非主线程退出
    if (Looper.myLooper() != Looper.getMainLooper()) return
    // 停止加载处理
    m?.stopLoading()
    if (m?.handler != null) {
    m.handler.removeCallbacksAndMessages(null)
    }
    // 移除webview的所有view
    m?.removeAllViews()
    // 获取父布局移除webview
    val mViewGroup: ViewGroup? = m?.parent as? ViewGroup
    mViewGroup?.removeView(m)
    // 回调置空
    m?.webChromeClient = null
    m?.webViewClient = null
    m?.tag = null
    // 清理历史并销毁
    m?.clearHistory()
    m?.destroy()
    }

    override fun onShowFileChooser(
    cameraIntent: Intent,
    filePathCallback: ValueCallback<Array<Uri>>
    ) {
    if (onShowFileChooser != null) {
    onShowFileChooser!!.invoke(cameraIntent, filePathCallback)
    } else {
    mFilePathCallback = filePathCallback
    //------------------------------------
    //弹出选择框有:相机、相册(Android9.0,Android8.0)
    //如果是小米Android6.0系统上,依然是:相机、相册、文件管理
    //如果安装了其他的相机(百度魔拍)、文件管理程序(ES文件管理器),也有可能会弹出
    val selectionIntent = Intent(Intent.ACTION_PICK, null)
    selectionIntent.type = "image/*"
    //------------------------------------
    val intentArray: Array<Intent?> = arrayOf(cameraIntent)
    val chooserIntent = Intent(Intent.ACTION_CHOOSER)
    chooserIntent.putExtra(Intent.EXTRA_TITLE, getString(R.string.file_chooser))
    chooserIntent.putExtra(Intent.EXTRA_INTENT, selectionIntent)
    chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray)
    startActivityForResult(chooserIntent, REQUEST_CODE)
    }
    }

    private var mFilePathCallback: ValueCallback<Array<Uri>>? = null
    private val mCameraPhotoPath: String? = null

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    when (requestCode) {
    REQUEST_CODE -> {
    var results: Array<Uri>? = null
    if (resultCode == Activity.RESULT_OK) {
    if (data == null) {
    if (mCameraPhotoPath != null) {
    Log.d("AppChooserFragment", mCameraPhotoPath)
    results = arrayOf(Uri.parse(mCameraPhotoPath))
    }
    } else {
    val dataString = data.dataString
    if (dataString != null) {
    results = arrayOf(Uri.parse(dataString))
    }
    }
    }
    mFilePathCallback?.onReceiveValue(results)
    mFilePathCallback = null
    }
    }
    }
    }
  • WebViewFragment实现类,header传参,是否拦截重定向,是否同步header到cookie

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    class WebviewFragment : BaseWebviewFragment() {
    override fun getLayoutRes(): Int {
    return R.layout.fragment_common_webview
    }

    companion object{
    fun newInstance(keyUrl: String, headers: HashMap<String, String> = HashMap(), redirectIntercept: Boolean,isSyncToCookie: Boolean): WebviewFragment? {
    val fragment = WebviewFragment()
    fragment.arguments = getBundle(keyUrl, headers, redirectIntercept)
    if (isSyncToCookie) {
    syncCookie(keyUrl, headers)
    }
    return fragment
    }

    private fun getBundle(url: String, headers: HashMap<String, String>, redirectIntercept:Boolean): Bundle? {
    val bundle = Bundle()
    bundle.putString(WebConstants.INTENT_TAG_URL, url)
    bundle.putSerializable(INFO_HEADERS, headers)
    bundle.putBoolean(REDIRECT_INTERCEPT, redirectIntercept)
    return bundle
    }


    /**
    * cookie同步到WebView
    *
    * @param url WebView要加载的url
    * @return true 同步cookie成功,false同步cookie失败
    * @Author JPH
    */
    private fun syncCookie(url: String?, map: Map<String, String?>): Boolean {
    val cookieManager = CookieManager.getInstance()
    for (key in map.keys) {
    cookieManager.setCookie(url, key + "=" + map[key])
    }
    val newCookie = cookieManager.getCookie(url)
    return !TextUtils.isEmpty(newCookie)
    }
    }

    }

    封装带WebView的Activity

    支持自定义标题布局,设置带WebView的Fragment,按键处理,注册使用网页标题事件,网页Activity关闭事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    open class WebActivity : AppCompatActivity() {
    private var title: String? = null // 标题设置

    protected var webviewFragment: BaseWebviewFragment? = null
    // 标题布局可自定义
    @LayoutRes
    protected fun getLayoutTitle(): Int = R.layout.title_normal

    // 添加标题布局
    val titleView by lazy { LayoutInflater.from(this).inflate(getLayoutTitle(), fl_title) }

    companion object{
    // 启动入口
    fun startCommonWeb(context: Context, title: String?, url: String?,header: HashMap<String, String> = HashMap()) {
    val intent = Intent(context, WebActivity::class.java)
    intent.putExtra(WebConstants.INTENT_TAG_TITLE, title)
    intent.putExtra(WebConstants.INTENT_TAG_URL, url)
    intent.putExtra(WebConstants.INTENT_TAG_HEADERS, header)
    if (context is Service) {
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    }
    context.startActivity(intent)
    }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    CommandsManager.getInstance().registerCommand(titleUpdateCommand)
    setContentView(R.layout.activity_common_web)
    // 注册标题刷新回调
    // 设置标题和url
    title = intent.getStringExtra(WebConstants.INTENT_TAG_TITLE)
    val url = intent.getStringExtra(WebConstants.INTENT_TAG_URL)?:""

    // 如果是默认布局设置标题
    if (getLayoutTitle() == R.layout.title_normal){
    val textView = titleView.findViewById(R.id.tv_title) as TextView
    textView.text = title
    }

    // 填充webviewFragment
    val fm = supportFragmentManager
    val transaction = fm.beginTransaction()

    webviewFragment = null
    val params = intent.extras?.getSerializable(WebConstants.INTENT_TAG_HEADERS) as HashMap<String, String>
    webviewFragment = WebviewFragment.newInstance(
    url,
    params,
    redirectIntercept = false,
    isSyncToCookie = true
    )
    transaction.replace(R.id.web_view_fragment, webviewFragment as Fragment).commit()
    }

    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
    // 调用webviewFragment的按键回调
    if (webviewFragment != null && webviewFragment is BaseWebviewFragment && event != null) {
    val flag: Boolean = webviewFragment!!.onKeyDown(keyCode, event)
    if (flag) {
    return flag
    }
    }
    return super.onKeyDown(keyCode, event)
    }

    /**
    * 标题刷新命令
    */
    private val titleUpdateCommand: Command = object : Command {
    override fun name(): String {
    return WebConstants.COMMAND_UPDATE_TITLE
    }

    override fun exec(context: Context, params: ArrayMap<String, String>, resultBack: ResultBack) {
    if (params.containsKey(WebConstants.COMMAND_UPDATE_TITLE_PARAMS)) {
    if (getLayoutTitle() == R.layout.title_normal){
    val textView = titleView.findViewById(R.id.tv_title) as TextView
    textView.text = params[WebConstants.COMMAND_UPDATE_TITLE_PARAMS]
    }
    }
    }
    }

    /**
    * 默认title布局返回 回调
    * @param view View
    */
    fun back(view: View){
    finish()
    }

    override fun finish() {
    val map = ArrayMap<String, String>().apply {
    this[WebFinishCommand.FINISH_COMMAND_PARAMS] = "true"
    }
    CommandDispatcher.getInstance().execByOther(WebFinishCommand.FINISH_COMMAND, Gson().toJson(map))
    super.finish()
    }

    /**
    * 点击右侧button 回调
    * @param view View
    */
    fun clickRight(view: View){

    }
    }

    分发消息

  • 命令模式封装消息管理器,对当前分发的消息采用当前进程自行处理,或跨进程处理,根据传参判断是否需要回调给h5

1
2
3
4
5
6
7
8
9
10
11
/**
* 命令实现接口
*/
interface Command {


// 命令名称
fun name():String
// 执行命令传参和定义返回接口
fun exec(context: Context, map: ArrayMap<String, String>, resultBack: ResultBack)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* 命令管理器
* @property instance CommandsManager?
* @property commands ArrayMap<String, Command>
*/
class CommandsManager private constructor(){
private var instance: CommandsManager? = null
// 命令缓存
val commands:ArrayMap<String, Command> = ArrayMap()

// 返回单例
companion object{
@Volatile private var instance:CommandsManager? = null
fun getInstance() = instance?: synchronized(this){
instance?:CommandsManager().also { instance = it }
}
}
/**
* 注册web进程处理的命令
* @param command Command
*/
fun registerCommand(command: Command) {
commands[command.name()] = command
}


/**
* 非UI线程执行
*/
fun execCommand(
context: Context,
action: String,
params: ArrayMap<String, String>?,
resultBack: ResultBack
) {
// 命令/传参不为空,执行命令逻辑
if (commands[action] != null && params != null) {
commands[action]?.exec(context, params, resultBack)
}
// 返回错误
else {
val aidlError =
AidlError(WebConstants.NO_METHOD, WebConstants.NO_METHOD_STR)
resultBack.onResult(WebConstants.FAILED, action, aidlError)
}
}


/**
* 判断命令是否存在
* @param action String?
* @return Boolean
*/
fun isCommandExist(action: String?): Boolean {
return commands[action] != null
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
class CommandDispatcher {
/**
* json解析对象
*/
private val gson by lazy { Gson() }

// 跨进程通信接口
protected var iHandleAction: IHandleAction? = null


/**
* 单例返回
*/
companion object {
@Volatile
private var instance: CommandDispatcher? = null

fun getInstance() = instance ?: synchronized(this) {
return instance ?: CommandDispatcher().also { instance = it }
}
}

/**
* 跨进程连接
* @param context Context?
*/
fun initAidlConnect(context: Context) {
// 避免重复初始化
if (iHandleAction != null) {
return
}
// 开启子线程,获取跨进程调用实例
Thread {
iHandleAction = IHandleAction.Stub.asInterface(
ProcessConnector.getInstance(context).getAidlInterface()
)
}.start()
}

/** 其它进程执行命令
* @param cmd String
* @param params String
*/
fun execByOther(cmd: String, params: String) {
Log.i("CommandDispatcher", "${Process.myPid()}进程调跨进程处理,command: $cmd params: $params")
try {
iHandleAction?.handleAction(cmd, params, object : ICallback.Stub() {
override fun onResult(
responseCode: Int,
actionName: String?,
response: String?
) {
// 当前进程回调结果
handleCallback(responseCode, actionName, response)
}
})
} catch (e: Exception) {
Log.e("CommandDispatcher", "Command exec error!!!!", e)
}
}

/**
* 当前进程执行命令
* @param context Context
* @param cmd String
* @param params String
* @param webView WebView
*/
fun execBySelf(context: Context, cmd: String, params: String, webView: WebView? = null) {
Log.i("CommandDispatcher", "${Process.myPid()}进程自己处理,command: $cmd params: $params")
try {
// json传参数据反序列化为map对象
val mapParams = gson.fromJson<ArrayMap<String, String>>(
params,
ArrayMap::class.java
)
// 命令管理器执行该命令逻辑
CommandsManager.getInstance()
.execCommand(context, cmd, mapParams, object : ResultBack {
// 结果回调
override fun onResult(status: Int, action: String, result: Any?) {
handleCallback(status, action, gson.toJson(result), webView)
}
})
} catch (e: Exception) {
Log.e("CommandDispatcher", "Command exec error!!!!", e)
}
}

private fun handleCallback(
responseCode: Int, actionName: String?, response: String?,
webView: WebView? = null
) {
Log.d(
"CommandDispatcher",
String.format(
"Callback result: responseCode= %s, action= %s, result= %s",
responseCode,
actionName,
response
)
)
runOnUiThread(Runnable {
// 返回结果反序列化为json
val params = Gson().fromJson<ArrayMap<String, String>>(response, ArrayMap::class.java)
// 从传参判断是否需要回调给h5
if (webView != null && params[WebConstants.NATIVE2WEB_CALLBACK] != null && !TextUtils.isEmpty(
params[WebConstants.NATIVE2WEB_CALLBACK].toString()
)
) {
// web进程调js
if (webView is BaseWebView) {
webView.handleCallback(response)
}
}
})
}
}
  • 进程连接管理类,绑定另一个服务进程,连接断开时解绑重连,返回跨进程通信接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    class ProcessConnector private constructor(context: Context) {
    private var mContext: Context = context.applicationContext
    // 跨进程通信接口
    private var iHandleAction: IHandleAction? = null
    private var mConnectBinderPoolCountDownLatch: CountDownLatch = CountDownLatch(1) // 个数为1的同步变量

    // 单例对象
    companion object {
    @Volatile
    private var instance: ProcessConnector? = null
    fun getInstance(context: Context) = instance ?: synchronized(this) {
    instance ?: ProcessConnector(context).also { instance = it }
    }
    }

    init {
    connectToMainProcessService()
    }

    // 绑定另一个服务进程
    @Synchronized
    private fun connectToMainProcessService() {
    val targetClass = if(!isMainProcess(mContext)) RemoteBindMainService::class.java else MainBindRemoteService::class.java
    // 绑定主进程服务
    mContext.bindService(Intent(mContext,targetClass), object : ServiceConnection {

    // 服务断开
    override fun onServiceDisconnected(name: ComponentName) {
    Log.v("ServiceConnect","跨进程连接断开")
    }

    // 服务连接
    override fun onServiceConnected(name: ComponentName, service: IBinder) {
    Log.v("ServiceConnect","跨进程连接成功")
    // 绑定断开回调
    iHandleAction = IHandleAction.Stub.asInterface(service)
    try {
    iHandleAction?.asBinder()?.linkToDeath(object : IBinder.DeathRecipient {
    // 解绑重连
    override fun binderDied() {
    iHandleAction?.asBinder()?.unlinkToDeath(this, 0)
    iHandleAction = null
    connectToMainProcessService()
    }
    }, 0)
    } catch (e: RemoteException) {
    e.printStackTrace()
    }
    mConnectBinderPoolCountDownLatch.countDown()
    }
    }, Context.BIND_AUTO_CREATE)

    // 线程同步阻塞
    try {
    mConnectBinderPoolCountDownLatch.await()
    } catch (e: InterruptedException) {
    e.printStackTrace()
    }
    }


    // 返回跨进程调用接口
    fun getAidlInterface(): IBinder? {
    return iHandleAction?.asBinder()
    }

    }

    跨进程通信

    跨进程接口定义

    定义AIDL跨进程调用接口
    1
    2
    3
    interface IHandleAction {
    void handleAction(String actionName, String jsonParams, in ICallback callback);
    }
    定义AIDL跨进程调用回调接口
    1
    2
    3
    interface ICallback {
    void onResult(int responseCode, String actionName, String response);
    }

    跨进程接口实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    class AidlInterface (val context: Context): IHandleAction.Stub() {
    private val gson: Gson by lazy { Gson() }
    override fun handleAction(
    actionName: String?,
    jsonParams: String?,
    callback: ICallback?
    ) {
    Log.v("AidlInterface","${Process.myPid()}进程正在跨进程执行命令$actionName")
    if (actionName != null){
    // 跨进程处理命令
    CommandsManager.getInstance().execCommand(context, actionName,
    gson.fromJson(jsonParams, ArrayMap::class.java) as? ArrayMap<String, String>, object : ResultBack {
    override fun onResult(status: Int, action: String, result: Any?) {
    try {
    // 原进程处理回调
    callback?.onResult(status, actionName, Gson().toJson(result))
    } catch (e: Exception) {
    e.printStackTrace()
    }
    }

    })
    }
    }

    }

    声明主进程服务和Web进程服务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /**
    * 主进程绑定web进程
    */
    class MainBindRemoteService : Service() {
    override fun onBind(intent: Intent?): IBinder? {
    val pid = Process.myPid()
    Log.d(
    "MainBindRemoteService", String.format(
    "web进程: %s",
    "当前进程ID为:$pid---主进程连接web进程成功"
    )
    )
    // web进程操作对象
    return AidlInterface(this)
    }

    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
    * web绑定主进程服务
    */
    class RemoteBindMainService: Service() {
    override fun onBind(intent: Intent?): IBinder? {
    val pid = Process.myPid()
    Log.d(
    "RemoteBindMainService", String.format(
    "主进程: %s",
    "当前进程ID为:$pid----web连接主进程成功"
    )
    )
    // 主进程操作对象
    return AidlInterface(this)
    }
    }
    1
    2
    3
    <service android:name="com.example.weblib.service.MainBindRemoteService"
    android:process=":remoteweb"/>
    <service android:name="com.example.weblib.service.RemoteBindMainService" />
    最后利用ContentProvider初始化封装的SDK,兼容>=Android 9.0 不同进程中使用Webview需要配置不同的缓存目录
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class WebInitializer : ContentProvider() {
    override fun onCreate(): Boolean {
    Log.v("SdkInitializer","WebLib初始化进程:${getProcessName(context!!)}")
    if (context == null) return true
    // >=Android 9.0 在不同进程中使用Webview需要配置缓存目录,配置WebLib进程使用的Webview缓存目录
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
    WebView.setDataDirectorySuffix("remote")
    }
    return true
    }
    ...
    }
    1
    2
    3
    4
    5
    6
    <provider
    android:authorities="${applicationId}.library-installer"
    android:name="com.example.weblib.WebInitializer"
    android:multiprocess="true"
    android:process=":remoteweb"
    android:exported="false"/>


最近在做性能优化的东西,研究了一下相关的内存监测,卡顿监测,以及ANR监测开源框架,对里面的核心原理做了总结,并手写一份简易版以便加深印象,为后续搭建线上日志监控做铺垫,在此做一个记录,线上监控框架可根据业务在此基础上做扩展。收集必要的日志信息,排查问题及时修复BUG,提升性能和稳定性,也是每个Android工程师必不可少的技能。


BlockCanary

  1. 通过采样工具类,在子线程中获取主线程的堆栈信息并保存到Map,暴露一个接口返回采样结果
  2. 通过日志监控类,实现Printer接口,判断是否卡顿(采样开始到采样结束的时间间隔是否超过阈值),如果卡顿调用接口获取返回的采样结果在子线程中日志打印
  3. App出现卡顿,会阻塞主线程的dispatchMessage,主线程Looper的loop方法中有一个Printer在每个Message处理前后被调用,所以设置主线程的MessageLogging为自定义的Printer

    采样工具类

    开启一个采样子线程,设置原子变量记录本次是否采样保证多线程同步,避免重复开始和结束,开始采样post一个采样任务到子线程,存入当前时间戳和对应的主线程堆栈信息到Map中,如果本次仍然需采样继续延迟间隔时间执行采样任务,暴露一个获取主线程堆栈信息的接口方法,返回当前堆栈信息列表
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    public class StackSampler {
    public static final String SEPARATOR = "\r\n";
    public static final SimpleDateFormat TIME_FORMATTER =
    new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
    private Handler mHandler;
    private Map<Long, String> mStackMap = new LinkedHashMap<>(); //保存的主线程堆栈信息
    private int mMaxCount = 100; // 最多保存100条
    private long mSampleInterval; // 采样时间间隔
    // 本次是否采样
    protected AtomicBoolean mShouldSample = new AtomicBoolean(false);

    public StackSampler(long sampleInterval) {
    mSampleInterval = sampleInterval;
    // 开启采样子线程
    HandlerThread handlerThread = new HandlerThread("block-canary-sampler");
    handlerThread.start();
    mHandler = new Handler(handlerThread.getLooper());
    }

    /**
    * 开始采样 执行堆栈
    */
    public void startDump() {
    // 避免重复开始
    if (mShouldSample.get()) {
    return;
    }
    // 设置采样标记
    mShouldSample.set(true);
    // 移除上一个采样任务,在采样间隔时间后执行采样
    mHandler.removeCallbacks(mRunnable);
    mHandler.postDelayed(mRunnable, mSampleInterval);
    }

    public void stopDump() {
    // 避免重复结束
    if (!mShouldSample.get()) {
    return;
    }
    // 设置采样标记
    mShouldSample.set(false);

    mHandler.removeCallbacks(mRunnable);
    }


    public List<String> getStacks(long startTime, long endTime) {
    ArrayList<String> result = new ArrayList<>();
    synchronized (mStackMap) {
    for (Long entryTime : mStackMap.keySet()) {
    // 记录时间大于开始时间小于结束时间就放入返回列表中
    if (startTime < entryTime && entryTime < endTime) {
    result.add(TIME_FORMATTER.format(entryTime)
    + SEPARATOR
    + SEPARATOR
    + mStackMap.get(entryTime));
    }
    }
    }
    return result;
    }

    private Runnable mRunnable = new Runnable() {
    @Override
    public void run() {
    StringBuilder sb = new StringBuilder();
    // 获得主线程堆栈信息并拼接到字符串
    StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
    for (StackTraceElement s : stackTrace) {
    sb.append(s.toString()).append("\n");
    }
    synchronized (mStackMap) {
    //最多保存100条堆栈信息,到了数量上限移除
    if (mStackMap.size() == mMaxCount) {
    mStackMap.remove(mStackMap.keySet().iterator().next());
    }
    // 存入当前时间戳和对应的堆栈信息
    mStackMap.put(System.currentTimeMillis(), sb.toString());
    }
    // 如果本次要采样,设置延迟继续执行此任务
    if (mShouldSample.get()) {
    mHandler.postDelayed(mRunnable, mSampleInterval);
    }
    }
    };

    }

    卡顿监控工具类

    初始化采样工具类,开启打印日志子线程,记录采样的开始时间和结束时间,如果时间大于设定的卡顿阈值则判定为卡顿状态,调用采样工具类获取主线程堆栈信息
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    // 卡顿监控工具类
    public class LogMonitor implements Printer {

    private StackSampler mStackSampler; // 采样工具类
    private boolean mPrintingStarted = false; // 开始打印标记
    private long mStartTimestamp; // 开始时间戳
    private long mBlockThresholdMillis = 3000; // 卡顿阈值
    private long mSampleInterval = 1000; // 采样频率

    private Handler mLogHandler;

    public LogMonitor() {
    // 初始化采样工具类
    mStackSampler = new StackSampler(mSampleInterval);
    // 开启打印子线程
    HandlerThread handlerThread = new HandlerThread("block-canary-io");
    handlerThread.start();
    mLogHandler = new Handler(handlerThread.getLooper());
    }

    @Override
    public void println(String x) {
    //从if到else会执行消息分发,如果执行耗时超过阈值,输出卡顿信息
    if (!mPrintingStarted) {
    //记录开始时间
    mStartTimestamp = System.currentTimeMillis();
    mPrintingStarted = true;
    mStackSampler.startDump();
    } else {
    final long endTime = System.currentTimeMillis();
    mPrintingStarted = false;
    //出现卡顿,通知卡顿事件
    if (isBlock(endTime)) {
    notifyBlockEvent(endTime);
    }
    mStackSampler.stopDump();
    }
    }

    private void notifyBlockEvent(final long endTime) {
    // 获得卡顿时主线程堆栈信息在子线程打印
    mLogHandler.post(new Runnable() {
    @Override
    public void run() {
    List<String> stacks = mStackSampler.getStacks(mStartTimestamp, endTime);
    for (String stack : stacks) {
    Log.e("block-canary", stack);
    }
    }
    });
    }


    private boolean isBlock(long endTime) {
    return endTime - mStartTimestamp > mBlockThresholdMillis;
    }
    }

    卡顿监控入口

    设置主线程的MessageLogging为自定义的Printer
    1
    2
    3
    4
    5
    6
    public class BlockCanary {
    public static void install() {
    LogMonitor logMonitor = new LogMonitor();
    Looper.getMainLooper().setMessageLogging(logMonitor);
    }
    }

LeakCanary

Java中WeakReference和ReferenceQueue联合使用是监控某个对象是否被gc回收的手段,LeakCanary正是利用这个原理实现的。

WeakReference和ReferenceQueue联合使用

创建一个对象,包装到弱引用对象中并关联引用队列,把对象置空,强制GC,取出引用队列的弱引用对象是否与关联时的弱引用对象相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(String[] args) {
// 创建一个引用队列
ReferenceQueue referenceQueue = new ReferenceQueue();
// 创建一个对象
Object obj = new Object();

//把obj放入弱引用对象,并和一个引用队列关联
//当obj被gc回收后,weakReference会被添加到与之关联的referenceQueue
WeakReference weakReference = new WeakReference(obj,referenceQueue);

//把obj置空,让它没有强引用
obj = null;
Runtime.getRuntime().gc(); //强制gc

try{
Thread.sleep(1000);
}catch (Exception e){}

Reference findRef = null;
do{
findRef = referenceQueue.poll();
//如果能找到上面的weakReference对象,说明obj被gc回收了
System.out.println("findRef = " +findRef + "是否等于上面的weakReference = " + (findRef == weakReference));
}while(findRef !=null);// 把所有referenceQueue的weakReference对象找出来
}

封装包含key和name的弱引用类

继承WeakReference弱引用类,方便根据key删除对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class KeyWeakReference<T> extends WeakReference<T> {

private String key;
private String name;

public KeyWeakReference(T referent, String key, String name) {
super(referent);
this.key = key;
this.name = name;
}

public KeyWeakReference(T referent, ReferenceQueue<? super T> q, String key, String name) {
super(referent, q);
this.key = key;
this.name = name;
}

public String getKey() {
return key;
}

public void setKey(String key) {
this.key = key;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public String toString() {
final StringBuffer sb = new StringBuffer("KeyWeakReference{");
sb.append("key='").append(key).append('\'');
sb.append(", name='").append(name).append('\'');
sb.append('}');
return sb.toString();
}
}

监控工具类

创建一个观察对象Map,怀疑对象Map和引用队列,先清理一遍被GC的对象,遍历引用队列的所有弱引用对象,清理观察对象Map和怀疑对象Map中对应的对象,被监控的对象生成UUID作为key,把对象放入弱引用并与引用队列关联,放入到观察对象Map中,5秒后判断该对象是否还存在观察对象Map中,还存在则说明没有被GC,将该对象移动到怀疑对象Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
public class Watcher {
// 观察对象Map
private HashMap<String, KeyWeakReference> watchedReferences = new HashMap<>();
// 怀疑对象Map
private HashMap<String, KeyWeakReference> retainedReferences = new HashMap<>();

//当被监视的对象被gc回收后,对象的弱引用就会被加入到引用队列
private ReferenceQueue queue = new ReferenceQueue();

public Watcher() {
}

/**
* 取出引用队列的所有弱引用对象,清理观察对象Map和怀疑对象Map中对应的对象
*/
private void removeWeaklyReachableReferences() {
KeyWeakReference findRef = null;
do {
// 取出引用队列的弱引用对象
findRef = (KeyWeakReference) queue.poll();
// 不为空说明对象被gc回收了,把对应的弱引用对象从观察对象Map,怀疑对象Map移除
if (findRef != null) {
// 根据key把它从观察对象Map移除
Reference removedRef = watchedReferences.remove(findRef.getKey());
// 如果removedRef为空,有可能被放入到怀疑对象Map了
// 尝试从怀疑对象Map中移除
if (removedRef == null) {
retainedReferences.remove(findRef.getKey());
}
}
} while (findRef != null);// 把referenceQueue的所有弱引用取出来
}

/**
* 根据key把对应的弱引用对象从观察对象Map移动到怀疑对象Map
*
* @param key
*/
private synchronized void moveToRetained(String key) {
System.out.println("加入到怀疑列表...");
// 加入怀疑对象Map前,做一次清理
removeWeaklyReachableReferences();
// 根据key从观察对象Map中去找弱引用对象
KeyWeakReference retainedRef = watchedReferences.remove(key);
// 发现还没有被删除,说明没有被回收
if (retainedRef != null) {
//从观察对象Map中移除,加入到怀疑对象Map
retainedReferences.put(key, retainedRef);
}
}


public void watch(Object watchedReference, String referenceName) {
//1. 先清理下观察对象Map和怀疑对象Map
removeWeaklyReachableReferences();
//2. 被监视的对象生成唯一的uuid作为key
final String key = UUID.randomUUID().toString();
//3. 被监视的对象放入weakReference,并和一个引用队列关联
KeyWeakReference reference = new KeyWeakReference(watchedReference, queue, key, "");
//4. 加入到观察对象Map
watchedReferences.put(key, reference);

//5. 延迟5秒后检查是否还在观察对象Map,如果还在,则加入到怀疑对象Map
Executor executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
Utils.sleep(5000);
moveToRetained(key);
});

}

// 获取泄漏对象Map
public HashMap<String, KeyWeakReference> getRetainedReferences() {
retainedReferences.forEach((key, keyWeakReference) -> {
System.out.println("key: " + key + " , obj: " + keyWeakReference.get() + " , keyWeakReference: " + keyWeakReference);
}
);
return retainedReferences;
}
}

监控泄漏对象

创建对象,调用监控工具类的监控方法,查看怀疑对象Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
// 初始化监控工具类
Watcher watcher = new Watcher();
// 创建对象并开始对象监控
Object obj = new Object();
watcher.watch(obj,"");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 释放对象
obj = null;
// 强制GC
Runtime.getRuntime().gc();
sleep(100);
System.runFinalization();

System.out.println("查看是否在怀疑对象Map:" + watcher.getRetainedReferences().size());
}

ANRWatchDog

FileObserver

监控Android系统的anr日志目录/data/anr/,利用FileObserver监控目录下的文件操作,间接监控ANR问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class ANRFileObserver extends FileObserver {

public ANRFileObserver(String path) {
super(path);
}

public ANRFileObserver(String path, int mask) {
super(path, mask);
}

@Override
public void onEvent(int event, @Nullable String path) {
switch (event)
{
case FileObserver.ACCESS://文件被访问
Log.i("Zero", "ACCESS: " + path);
break;
case FileObserver.ATTRIB://文件属性被修改,如 chmod、chown、touch 等
Log.i("Zero", "ATTRIB: " + path);
break;
case FileObserver.CLOSE_NOWRITE://不可写文件被 close
Log.i("Zero", "CLOSE_NOWRITE: " + path);
break;
case FileObserver.CLOSE_WRITE://可写文件被 close
Log.i("Zero", "CLOSE_WRITE: " + path);
break;
case FileObserver.CREATE://创建新文件
Log.i("Zero", "CREATE: " + path);
break;
case FileObserver.DELETE:// 文件被删除,如 rm
Log.i("Zero", "DELETE: " + path);
break;
case FileObserver.DELETE_SELF:// 自删除,即一个可执行文件在执行时删除自己
Log.i("Zero", "DELETE_SELF: " + path);
break;
case FileObserver.MODIFY://文件被修改
Log.i("Zero", "MODIFY: " + path);
break;
case FileObserver.MOVE_SELF://自移动,即一个可执行文件在执行时移动自己
Log.i("Zero", "MOVE_SELF: " + path);
break;
case FileObserver.MOVED_FROM://文件被移走,如 mv
Log.i("Zero", "MOVED_FROM: " + path);
break;
case FileObserver.MOVED_TO://文件被移来,如 mv、cp
Log.i("Zero", "MOVED_TO: " + path);
break;
case FileObserver.OPEN://文件被 open
Log.i("Zero", "OPEN: " + path);
break;
default:
//CLOSE : 文件被关闭,等同于(IN_CLOSE_WRITE | IN_CLOSE_NOWRITE)
//ALL_EVENTS : 包括上面的所有事件
Log.i("Zero", "DEFAULT(" + event + "): " + path);
break;
}
}
}

线程

开启ANR监控后台线程,检查是否ANR,通过检查标志位和时间差判断规定时间内是否执行完成,如果没执行完成则可能发生ANR卡住了,那么打印主线程堆栈信息并调用回调接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
public class ANRWatchDog extends Thread {

private static final String TAG = "ANR";
private int timeout = 5000; // 超时阈值
private boolean ignoreDebugger = true; // 开启开关

static ANRWatchDog sWatchdog;

private Handler mainHandler = new Handler(Looper.getMainLooper());

private class ANRChecker implements Runnable {

private boolean mCompleted; // 是否完成
private long mStartTime; // 开始时间
private long executeTime = SystemClock.uptimeMillis(); // 执行时间

@Override
public void run() {
synchronized (ANRWatchDog.this) {
mCompleted = true;
executeTime = SystemClock.uptimeMillis();
}
}

void schedule() {
// 设置是否完成标记
mCompleted = false;
// 记录开始时间
mStartTime = SystemClock.uptimeMillis();
// 结束任务,在主线程中重置完成标记,记录结束时间
mainHandler.postAtFrontOfQueue(this);
}

boolean isBlocked() {
return !mCompleted || executeTime - mStartTime >= timeout;
}
}

// anr监听接口
public interface ANRListener {
void onAnrHappened(String stackTraceInfo);
}

private ANRChecker anrChecker = new ANRChecker();

private ANRListener anrListener;


public void addANRListener(ANRListener listener){
this.anrListener = listener;
}

public static ANRWatchDog getInstance(){
if(sWatchdog == null){
sWatchdog = new ANRWatchDog();
}
return sWatchdog;
}

private ANRWatchDog(){
super("ANR-WatchDog-Thread");
}

@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
@Override
public void run() {
// 设置为后台线程
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
while(true){
// 没有被打断停止
while (!isInterrupted()) {
synchronized (this) {
// 检查是否anr
anrChecker.schedule();
long waitTime = timeout;
long start = SystemClock.uptimeMillis();
// 防止假唤醒
while (waitTime > 0) {
try {
// 等待超时时长后检查标志位
wait(waitTime);
} catch (InterruptedException e) {
Log.w(TAG, e.toString());
}
// 发现没有到阈值继续休眠
waitTime = timeout - (SystemClock.uptimeMillis() - start);
}
// 如果没有阻塞跳过此次循环
if (!anrChecker.isBlocked()) {
continue;
}
}
// 如果开关关闭并且在调试跳过此次循环
if (!ignoreDebugger && Debug.isDebuggerConnected()) {
continue;
}
// 打印堆栈信息并回调
String stackTraceInfo = getStackTraceInfo();
if (anrListener != null) {
anrListener.onAnrHappened(stackTraceInfo);
}
}
anrListener = null;
}
}

// 获取主线程堆栈信息返回字符串
private String getStackTraceInfo() {
StringBuilder stringBuilder = new StringBuilder();
for (StackTraceElement stackTraceElement : Looper.getMainLooper().getThread().getStackTrace()) {
stringBuilder
.append(stackTraceElement.toString())
.append("\r\n");
}
return stringBuilder.toString();
}
}


最近公司的项目做性能优化,正在折腾Fragment懒加载的东西,由于项目迁移到了AndroidX,懒加载方案和以前有所不同,不过总体来说代码量少了很多,正好可以对新老版本的懒加载方案做一个对比,下面是关于AndroidX前和AndroidX后的fragment懒加载方案总结


AndroidX之前采用旧懒加载方案

4步优化

  1. View已加载且fragment可见时懒加载
  • 当onViewCreated()方法执行时,表明View已经加载完毕,isViewCreated标记为true,并调lazyLoad()方法
  • 当setUserVisibleHint(boolean isVisibleToUser)执行时,isVisibleToUser为true并调lazyLoad()方法
  • 在lazyLoad()方法双重标记判断,再进行停止一切/加载数据事件分发
  • 定义抽象方法loadData(),子类重写进行加载数据
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    private boolean isViewCreated = false;
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    ...
    isViewCreated = true; // View已加载
    // 此时正好可见
    if (getUserVisibleHint()) {
    setUserVisibleHint(true);
    }

    return rootView;
    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
    super.setUserVisibleHint(isVisibleToUser);
    // View已加载
    if (isViewCreated) {
    // 根据当前可见状态分发事件
    if (isVisibleToUser) {
    dispatchUserVisibleHint(true);
    } else {
    dispatchUserVisibleHint(false);
    }
    }
    }

    private void dispatchUserVisibleHint(boolean visibleState) {
    if (visibleState) {
    // 加载网络数据请求
    onFragmentLoad();
    } else {
    // 停止网络数据请求
    onFragmentLoadStop();
    }
    }

    public void onFragmentLoadStop() {
    E("onFragmentLoadStop");
    }

    public void onFragmentLoad() {
    E("onFragmentLoad");
    }
  1. 记录上一次状态与当前状态比较,保证变化过程,状态未改变不需要分发事件
  • 从可见到不可见,停止一切操作
  • 从不可见到可见,加载数据操作
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    private boolean isVisibleStateUP = false; // 记录上一次可见的状态
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
    super.setUserVisibleHint(isVisibleToUser);

    if (isViewCreated) {
    // 当前可见且上一次不可见
    if (isVisibleToUser && !isVisibleStateUP) {
    dispatchUserVisibleHint(true);
    }
    // 当前不可见且上一次可见
    else if (!isVisibleToUser && isVisibleStateUP){
    dispatchUserVisibleHint(false);
    }

    }
    }

    private void dispatchUserVisibleHint(boolean visibleState) {
    this.isVisibleStateUP = visibleState;
    if (visibleState) {
    // 加载网络数据请求
    onFragmentLoad();
    } else {
    // 停止网络数据请求
    onFragmentLoadStop();
    }
    }
  1. 启动新的Activity没有分发事件
  • 除了onCreate和setUserVisibleHint在onResume/onPause中也要分发事件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Override
    public void onResume() {
    super.onResume();
    // 当前可见且上一次不可见
    if (getUserVisibleHint() && !isVisibleStateUP) {
    dispatchUserVisibleHint(true);
    }
    }

    @Override
    public void onPause() {
    super.onPause();
    // 当前不可见且上一次可见
    if (getUserVisibleHint() && isVisibleStateUP) {
    dispatchUserVisibleHint(false);
    }
    }
  1. 双重嵌套下子Fragment无法接收到事件
    需判断父fragment是否可见,当前fragment可见,父fragment上一次不可见时手动遍历分发每个fragment停止一切操作/加载数据事件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    // 判断父fragment是否可见 
    private boolean isParentInvisible() {
    // 获取父fragment
    Fragment parentFragment = getParentFragment();
    // 父fragment是懒加载fragment的实例
    if (parentFragment instanceof LazyFragment) {
    LazyFragment fragment = (LazyFragment) parentFragment;
    // 父fragment不可见
    return !fragment.isVisibleStateUP;
    }
    return false;
    }

    private void dispatchUserVisibleHint(boolean visibleState) {
    this.isVisibleStateUP = visibleState;
    // 当前fragment可见且父fragment不可见,不分发事件
    if (visibleState && isParentInvisible()) {
    return;
    }

    if (visibleState) {
    onFragmentLoad();
    // 分发子fragment可见事件
    dispatchChildVisibleState(true);

    } else {
    onFragmentLoadStop();
    // 分发子fragment不可见事件
    dispatchChildVisibleState(false);
    }
    }

    protected void dispatchChildVisibleState(boolean state) {
    FragmentManager fragmentManager = getChildFragmentManager();
    List<Fragment> fragments = fragmentManager.getFragments();
    if (fragments != null) {
    // 遍历子Fragment分发事件
    for (Fragment fragment: fragments) {
    // fragment是懒加载实例未被隐藏且可见
    if (fragment instanceof LazyFragment &&
    !fragment.isHidden() &&
    fragment.getUserVisibleHint()) {
    ((LazyFragment5)fragment).dispatchUserVisibleHint(state);
    }
    }
    }
    }

    AndroidX之后采用新懒加载方案

    Google在Androidx在FragmentTransaction中增加了setMaxLifecycle方法控制Fragment 调用的最大的生命周期函数。该方法可以设置活跃状态下Fragment最大状态,如果该Fragment 超过了设置的最大状态,会强制将Fragment降级到正确的状态

    viewPager下设置adapter的behavior

    BEHAVIOR_SET_USER_VISIBLE_HINT:Fragment对用户可见状态发生改变时,setUserVisibleHint方法会被调用。
    BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT:当前选中的Fragment在Lifecycle.State#RESUMED状态,其他不可见的 Fragment限制在Lifecycle.State#STARTED状态
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    open class FragmentLazyPagerAdapter(
    fragmentManager: FragmentManager,
    private val fragments: MutableList<Fragment>,
    private val titles: MutableList<String>
    ) : FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {

    override fun getItem(position: Int) = fragments[position]

    override fun getCount() = fragments.size

    override fun getPageTitle(position: Int) = titles[position]

    }

    封装懒加载fragment

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    abstract class LazyFragment : Fragment() {

    // 是否加载过标记
    private var isLoaded = false

    override fun onResume() {
    super.onResume()
    if (!isLoaded) {
    lazyInit()
    Log.d(TAG, "lazyInit:!!!!!!!")
    isLoaded = true
    }
    }

    override fun onDestroyView() {
    super.onDestroyView()
    isLoaded = false
    }

    abstract fun lazyInit()
    }

    add/show/hide时设置最大生命周期(未使用viewPager)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    // 初始化fragment
    private fun loadFragmentsTransaction(
    @IdRes containerViewId: Int,
    showPosition: Int,
    fragmentManager: FragmentManager,
    vararg fragments: Fragment
    ) {
    if (fragments.isNotEmpty()) {
    fragmentManager.beginTransaction().apply {
    for (index in fragments.indices) {
    val fragment = fragments[index]
    add(containerViewId, fragment, fragment.javaClass.name)
    if (showPosition == index) {
    setMaxLifecycle(fragment, Lifecycle.State.RESUMED)
    } else {
    hide(fragment)
    setMaxLifecycle(fragment, Lifecycle.State.STARTED)
    }
    }

    }.commit()
    } else {
    throw IllegalStateException(
    "fragments must not empty"
    )
    }
    }

    // 展示/隐藏fragment
    private fun showHideFragmentTransaction(fragmentManager: FragmentManager, showFragment: Fragment) {
    fragmentManager.beginTransaction().apply {
    show(showFragment)
    setMaxLifecycle(showFragment, Lifecycle.State.RESUMED)

    //获取其中所有的fragment,其他的fragment进行隐藏
    val fragments = fragmentManager.fragments
    for (fragment in fragments) {
    if (fragment != showFragment) {
    hide(fragment)
    setMaxLifecycle(fragment, Lifecycle.State.STARTED)
    }
    }
    }.commit()
    }

    Fragment嵌套下的问题

    第一次初始化时,同级不可见的Fragment仍然要调生命周期方法,需增加Fragment是否可见的判断
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    abstract class LazyFragment : Fragment() {

    private var isLoaded = false

    override fun onResume() {
    super.onResume()
    //增加Fragment是否可见的判断
    if (!isLoaded && !isHidden) {
    lazyInit()
    Log.d(TAG, "lazyInit:!!!!!!!")
    isLoaded = true
    }
    }

    override fun onDestroyView() {
    super.onDestroyView()
    isLoaded = false
    }

    abstract fun lazyInit()

    }

    ViewPager2懒加载

    最新的ViewPager2默认就实现了懒加载,可以说不用任何处理

但是ViewPager2中的RecyclerView可以缓存Fragment的数量是有限的,会造成Fragment的多次销毁和创建,也可通过setOffscreenPageLimit()方法设置预加载数量,再用AndroidX下的懒加载fragment方式去处理


加密技术这块不仅涉及到很多JAVA基础,加密技术还涉及到很多Android底层知识,JAVA反射,JAVA IO,apk的启动流程,类的加载机制,dex文件的构造,APK打包的过程,而这些东西又正好是面试的常考点,是深入学习Android的必经之路,这次从原理入手,手写一个简单的加固框架,在这里做一个记录,如果有不对的地方欢迎指出和交流。


加固的主要目的是为了防止反编译,代码遭到阅读和窃取甚至重新打包上架的事情发生,那反编译的过程是什么呢?

  1. zip解压apk
  2. dex2jar把class.dex转成jar包
  3. jd-gui看class文件源码

加固的原理

所以加固的关键是对dex文件用加密算法进行加密,防止可执行部分的源码被阅读,此时就需要一个壳程序负责解密原dex文件,然后再合并原dex和壳dex重新签名打包成新的apk,运行时壳程序解密,获得原dex重新手动类加载

apk的打包流程

  1. APT工具处理资源文件(xml资源如布局、AndroidManifest),生成R.java
  2. AIDL工具处理AIDL文件,生成相应的Java文件
  3. Javac工具编译Java,生成Class文件
  4. DX工具将Class文件转换成DEX文件
  5. ApkBuilder工具将资源文件和DEX文件打包成APK
  6. KeyStore签名APK
  7. 正式版APK用ZipAlign工具对齐

实现加固

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
public static void main(String[] args) throws Exception {

byte[] mainDexData; // 存储源apk中的源dex文件
byte[] aarData; // 存储壳中的壳dex文件
byte[] mergeDex; // 存储壳dex 和源dex 的合并的新dex文件

// 删除source/apk/temp目录下所有文件
File tempFileApk = new File("source/apk/temp");
if (tempFileApk.exists()) {
File[]files = tempFileApk.listFiles();
for(File file: files){
if (file.isFile()) {
file.delete();
}
}
}
// 删除source/aar/temp目录下所有文件
File tempFileAar = new File("source/aar/temp");
if (tempFileAar.exists()) {
File[]files = tempFileAar.listFiles();
for(File file: files){
if (file.isFile()) {
file.delete();
}
}
}

//第一步 处理原始apk 加密dex
AES.init(AES.DEFAULT_PWD);
//待加固的apk
File apkFile = new File("source/apk/app-debug.apk");
//创建临时文件夹
File newApkFile = new File(apkFile.getParent() + File.separator + "temp");
if(!newApkFile.exists()) {
newApkFile.mkdirs();
}
//加密apk文件并写入到临时文件夹获取主dex
File mainDexFile = AES.encryptAPKFile(apkFile,newApkFile);
//临时文件夹存在,重命名dex文件
if (newApkFile.isDirectory()) {
File[] listFiles = newApkFile.listFiles();
for (File file : listFiles) {
if (file.isFile()) {
if (file.getName().endsWith(".dex")) {
String name = file.getName();
System.out.println("rename step1:"+name);
int cursor = name.indexOf(".dex");
String newName = file.getParent()+ File.separator + name.substring(0, cursor) + "_" + ".dex";
System.out.println("rename step2:"+newName);
file.renameTo(new File(newName));
}
}
}
}

// 第二步 处理aar 获得壳dex,其实这就是一个解密程序
File aarFile = new File("source/aar/mylibrary-debug.aar");
// jar包转dex文件
File aarDex = Dx.jar2Dex(aarFile);
//读取dex文件为byte数组
aarData = Utils.getBytes(aarDex);
// 创建一个classes.dex文件
File tempMainDex = new File(newApkFile.getPath() + File.separator + "classes.dex");
if (!tempMainDex.exists()) {
tempMainDex.createNewFile();
}
// 写入byte数组到classes.dex文件
FileOutputStream fos = new FileOutputStream(tempMainDex);
byte[] fbytes = Utils.getBytes(aarDex);
fos.write(fbytes);
fos.flush();
fos.close();


/**
* 第三步 打包签名
*/
// 创建未签名apk的文件夹
File unsignedApk = new File("result/apk-unsigned.apk");
unsignedApk.getParentFile().mkdirs();
// 合并壳dex和加密dex,压缩newApkFile中的文件为unsignedApk
Zip.zip(newApkFile, unsignedApk);
// 对unsignedApk文件签名输出签名后的文件apk-signed.apk
File signedApk = new File("result/apk-signed.apk");
Signature.signature(unsignedApk, signedApk);
}
}

Zip压缩工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
public class Zip {

// 解压文件到目标文件夹
public static void unZip(File zip, File dir) {
try {
// 删除已存在的目标文件夹
dir.delete();
// 包装成压缩文件对象
ZipFile zipFile = new ZipFile(zip);
// 获取被压缩的所有文件
Enumeration<? extends ZipEntry> entries = zipFile.entries();
// 遍历被压缩的文件
while (entries.hasMoreElements()) {
ZipEntry zipEntry = entries.nextElement();
String name = zipEntry.getName();
// 如果是META-INF/CERT.RSA,META-INF/CERT.SF,META-INF/MANIFEST.MF文件就跳过
if (name.equals("META-INF/CERT.RSA") || name.equals("META-INF/CERT.SF") || name
.equals("META-INF/MANIFEST.MF")) {
continue;
}
// 如果当前压缩文件不是一个文件夹,就输出到目标文件夹
if (!zipEntry.isDirectory()) {
File file = new File(dir, name);
if (!file.getParentFile().exists()) file.getParentFile().mkdirs();
FileOutputStream fos = new FileOutputStream(file);
InputStream is = zipFile.getInputStream(zipEntry);
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
is.close();
fos.close();
}
}
zipFile.close();
} catch (Exception e) {
e.printStackTrace();
}
}

public static void zip(File dir, File zip) throws Exception {
// 删除已存在的压缩文件
zip.delete();
// 压缩文件并对输出文件做CRC32校验
CheckedOutputStream cos = new CheckedOutputStream(new FileOutputStream(
zip), new CRC32());
ZipOutputStream zos = new ZipOutputStream(cos);
compress(dir, zos, "");
zos.flush();
zos.close();
}

private static void compress(File srcFile, ZipOutputStream zos,
String basePath) throws Exception {
if (srcFile.isDirectory()) {
compressDir(srcFile, zos, basePath);
} else {
compressFile(srcFile, zos, basePath);
}
}

private static void compressDir(File dir, ZipOutputStream zos,
String basePath) throws Exception {
File[] files = dir.listFiles();
// 文件夹为空,构建空目录
if (files.length < 1) {
ZipEntry entry = new ZipEntry(basePath + dir.getName() + "/");
zos.putNextEntry(entry);
zos.closeEntry();
}
// 递归压缩
for (File file : files) {
compress(file, zos, basePath + dir.getName() + "/");
}
}

private static void compressFile(File file, ZipOutputStream zos, String dir)
throws Exception {
// 当前文件路径
String dirName = dir + file.getName();
// 文件新名称拼接
String[] dirNameNew = dirName.split("/");
StringBuffer buffer = new StringBuffer();

if (dirNameNew.length > 1) {
for (int i = 1; i < dirNameNew.length; i++) {
buffer.append("/");
buffer.append(dirNameNew[i]);
}
} else {
buffer.append("/");
}
// 创建压缩文件并写入数据
ZipEntry entry = new ZipEntry(buffer.toString().substring(1));
zos.putNextEntry(entry);
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(
file));
int count;
byte data[] = new byte[1024];
while ((count = bis.read(data, 0, 1024)) != -1) {
zos.write(data, 0, count);
}
bis.close();
zos.closeEntry();
}
}

AES对称加密工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
public class AES {
// 默认密码
public static final String DEFAULT_PWD = "abcdefghijklmnop";
//
private static final String algorithmStr = "AES/ECB/PKCS5Padding";

private static Cipher encryptCipher;
private static Cipher decryptCipher;

public static void init(String password) {
try {
// 创建加密对象,ECB模式,PKCS5Padding填充方式
encryptCipher = Cipher.getInstance(algorithmStr);
// 创建解密对象
decryptCipher = Cipher.getInstance(algorithmStr);
// 获取密码字节数组
byte[] keyStr = password.getBytes();
// 生成加密密钥
SecretKeySpec key = new SecretKeySpec(keyStr, "AES");
// 初始化加密对象
encryptCipher.init(Cipher.ENCRYPT_MODE, key);
// 初始化解密对象
decryptCipher.init(Cipher.DECRYPT_MODE, key);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
}
}

/**
*
* @param srcAPKfile 源文件所在位置
* @param dstApkFile 目标文件
* @return 加密后的新dex 文件
* @throws Exception
*/
public static File encryptAPKFile(File srcAPKfile, File dstApkFile) throws Exception {
if (srcAPKfile == null) {
System.out.println("encryptAPKFile :srcAPKfile null");
return null;
}
// 解压源文件到目标文件夹
Zip.unZip(srcAPKfile, dstApkFile);
// 获得目标文件夹所有的dex
File[] dexFiles = dstApkFile.listFiles(new FilenameFilter() {
@Override
public boolean accept(File file, String s) {
return s.endsWith(".dex");
}
});

File mainDexFile = null;
byte[] mainDexData = null;
// 遍历所有的dex文件,找到并记录主dex文件并获得加密后的字节数组
for (File dexFile: dexFiles) {
// 获取dex的字节数组
byte[] buffer = Utils.getBytes(dexFile);
// 加密后的字节数组
byte[] encryptBytes = AES.encrypt(buffer);

if (dexFile.getName().endsWith("classes.dex")) {
mainDexData = encryptBytes;
mainDexFile = dexFile;
}
//用加密后的字节数组替换原来的数据
FileOutputStream fos = new FileOutputStream(dexFile);
fos.write(encryptBytes);
fos.flush();
fos.close();
}

// 返回主dex文件
return mainDexFile;
}

// 对字节数组加密返回
public static byte[] encrypt(byte[] content) {
try {
byte[] result = encryptCipher.doFinal(content);
return result;
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}

// 字节数组解密返回
public static byte[] decrypt(byte[] content) {
try {
byte[] result = decryptCipher.doFinal(content);
return result;
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}
}

dx转换工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class Dx {

public static File jar2Dex(File aarFile) throws IOException, InterruptedException {
// 创建临时文件夹
File fakeDex = new File(aarFile.getParent() + File.separator + "temp");
// 解压aar到临时文件夹下
Zip.unZip(aarFile, fakeDex);
// 过滤找到classes.jar
File[] files = fakeDex.listFiles(new FilenameFilter() {
@Override
public boolean accept(File file, String s) {
return s.equals("classes.jar");
}
});
// aar文件不存在抛异常
if (files == null || files.length <= 0) {
throw new RuntimeException("the aar is invalidate");
}
// 将classes.jar转classes.dex
File classes_jar = files[0];
// 创建classes.dex文件
File aarDex = new File(classes_jar.getParentFile(), "classes.dex");

//使用android tools里面的dx.bat,调windows下的命令
Dx.dxCommand(aarDex, classes_jar);
return aarDex;
}

public static void dxCommand(File aarDex, File classes_jar) throws IOException, InterruptedException {
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec("cmd.exe /C dx --dex --output=" + aarDex.getAbsolutePath() + " " +
classes_jar.getAbsolutePath());

try {
process.waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
throw e;
}
// 转换失败,输出错误到文件并抛异常
if (process.exitValue() != 0) {
InputStream inputStream = process.getErrorStream();
int len;
byte[] buffer = new byte[2048];
ByteArrayOutputStream bos = new ByteArrayOutputStream();
while((len=inputStream.read(buffer)) != -1){
bos.write(buffer,0,len);
}
System.out.println(new String(bos.toByteArray(),"GBK"));
throw new RuntimeException("dx run failed");
}
process.destroy();
}
}

签名工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;


public class Signature {
public static void signature(File unsignedApk, File signedApk) throws InterruptedException, IOException {
// 执行windows下的签名命令
String cmd[] = {"cmd.exe", "/C ","jarsigner", "-sigalg", "MD5withRSA",
"-digestalg", "SHA1",
"-keystore", "C:/Users/allen/.android/debug.keystore",
"-storepass", "android",
"-keypass", "android",
"-signedjar", signedApk.getAbsolutePath(),
unsignedApk.getAbsolutePath(),
"androiddebugkey"};
Process process = Runtime.getRuntime().exec(cmd);
System.out.println("start sign");
try {
int waitResult = process.waitFor();
System.out.println("waitResult: " + waitResult);
} catch (InterruptedException e) {
e.printStackTrace();
throw e;
}
System.out.println("process.exitValue() " + process.exitValue() );
// 执行失败,输出错误到文件并抛异常
if (process.exitValue() != 0) {
InputStream inputStream = process.getErrorStream();
int len;
byte[] buffer = new byte[2048];
ByteArrayOutputStream bos = new ByteArrayOutputStream();
while((len=inputStream.read(buffer)) != -1){
bos.write(buffer,0,len);
}
System.out.println(new String(bos.toByteArray(),"GBK"));
throw new RuntimeException("sign run failed");
}
System.out.println("finish signed");
process.destroy();
}
}

实现壳程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
public class ShellApplication extends Application {
private static final String TAG = "ShellApplication";

public static String getPassword(){
return "abcdefghijklmnop";
}

@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
// 初始化AES加密
AES.init(getPassword());
// 待解密文件路径data/data/包名/files/fake_apk/
File apkFile = new File(getApplicationInfo().sourceDir);
File unZipFile = getDir("fake_apk", MODE_PRIVATE);
// 待解密的文件目录data/data/包名/files/fake_apk/app
File app = new File(unZipFile, "app");
// 如果不存在待解密文件目录,解压apk
if (!app.exists()) {
Zip.unZip(apkFile, app);
// 过滤不为classes.dex的.dex文件,对读取的字节数组解密写出到文件
File[] files = app.listFiles();
for (File file : files) {
String name = file.getName();
if (name.equals("classes.dex")) {

} else if (name.endsWith(".dex")) {
try {
byte[] bytes = getBytes(file);
FileOutputStream fos = new FileOutputStream(file);
byte[] decrypt = AES.decrypt(bytes);
fos.write(decrypt);
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
// 获取待解密的文件目录.dex文件列表
List list = new ArrayList<>();
Log.d("FAKE", Arrays.toString(app.listFiles()));
for (File file : app.listFiles()) {
if (file.getName().endsWith(".dex")) {
list.add(file);
}
}

Log.d("FAKE", list.toString());
try {
V19.install(getClassLoader(), list, unZipFile);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}

// 反射获取对象某个变量的值
private static Field findField(Object instance, String name) throws NoSuchFieldException {
Class clazz = instance.getClass();

while (clazz != null) {
try {
Field e = clazz.getDeclaredField(name);
if (!e.isAccessible()) {
e.setAccessible(true);
}

return e;
} catch (NoSuchFieldException var4) {
clazz = clazz.getSuperclass();
}
}

throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
}

// 反射获取对象的某个方法
private static Method findMethod(Object instance, String name, Class... parameterTypes)
throws NoSuchMethodException {
Class clazz = instance.getClass();
while (clazz != null) {
try {
Method e = clazz.getDeclaredMethod(name, parameterTypes);
if (!e.isAccessible()) {
e.setAccessible(true);
}

return e;
} catch (NoSuchMethodException var5) {
clazz = clazz.getSuperclass();
}
}
throw new NoSuchMethodException("Method " + name + " with parameters " + Arrays.asList
(parameterTypes) + " not found in " + instance.getClass());
}

// 扩展某个对象的某个变量数组
private static void expandFieldArray(Object instance, String fieldName, Object[]
extraElements) throws NoSuchFieldException, IllegalArgumentException,
IllegalAccessException {
Field jlrField = findField(instance, fieldName);
Object[] original = (Object[]) ((Object[]) jlrField.get(instance));
Object[] combined = (Object[]) ((Object[]) Array.newInstance(original.getClass()
.getComponentType(), original.length + extraElements.length));
System.arraycopy(original, 0, combined, 0, original.length);
System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
jlrField.set(instance, combined);
}

// 动态加载类
private static final class V19 {
private V19() {
}
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory) throws IllegalArgumentException,
IllegalAccessException, NoSuchFieldException, InvocationTargetException,
NoSuchMethodException {
// 获取ClassLoader对象的pathList变量的值
Field pathListField = findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList suppressedExceptions = new ArrayList();
Log.d(TAG, "Build.VERSION.SDK_INT " + Build.VERSION.SDK_INT);
// 根据当前SDK版本动态批量加载类
if (Build.VERSION.SDK_INT >= 23) {
expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList, new
ArrayList(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
} else {
expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new
ArrayList(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
}
// 如果有异常,遍历打印
if (suppressedExceptions.size() > 0) {
Iterator suppressedExceptionsField = suppressedExceptions.iterator();

while (suppressedExceptionsField.hasNext()) {
IOException dexElementsSuppressedExceptions = (IOException)
suppressedExceptionsField.next();
Log.w("MultiDex", "Exception in makeDexElement",
dexElementsSuppressedExceptions);
}
// 获取dexElementsSuppressedExceptions变量的值
Field suppressedExceptionsField1 = findField(loader,
"dexElementsSuppressedExceptions");
IOException[] dexElementsSuppressedExceptions1 = (IOException[]) ((IOException[])
suppressedExceptionsField1.get(loader));
// 如果值为空,赋值为异常数组
if (dexElementsSuppressedExceptions1 == null) {
dexElementsSuppressedExceptions1 = (IOException[]) suppressedExceptions
.toArray(new IOException[suppressedExceptions.size()]);
} else {
// 否则扩展异常数组,合并dexElementsSuppressedExceptions并重新赋值
IOException[] combined = new IOException[suppressedExceptions.size() +
dexElementsSuppressedExceptions1.length];
suppressedExceptions.toArray(combined);
System.arraycopy(dexElementsSuppressedExceptions1, 0, combined,
suppressedExceptions.size(), dexElementsSuppressedExceptions1.length);
dexElementsSuppressedExceptions1 = combined;
}

suppressedExceptionsField1.set(loader, dexElementsSuppressedExceptions1);
}

}

// 调用dexPathList对象的makeDexElements方法
private static Object[] makeDexElements(Object dexPathList,
ArrayList<File> files, File
optimizedDirectory,
ArrayList<IOException> suppressedExceptions) throws
IllegalAccessException, InvocationTargetException, NoSuchMethodException {

Method makeDexElements = findMethod(dexPathList, "makeDexElements", new
Class[]{ArrayList.class, File.class, ArrayList.class});
return ((Object[]) makeDexElements.invoke(dexPathList, new Object[]{files,
optimizedDirectory, suppressedExceptions}));
}
}
// 调用dexPathList对象的makePathElements方法
private static Object[] makePathElements(
Object dexPathList, ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions)
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
// 查找返回类型为List的makePathElements方法
Method makePathElements;
try {
makePathElements = findMethod(dexPathList, "makePathElements", List.class, File.class,
List.class);
} catch (NoSuchMethodException e) {
Log.e(TAG, "NoSuchMethodException: makePathElements(List,File,List) failure");
// 查找返回类型为ArrayList的makePathElements方法
try {
makePathElements = findMethod(dexPathList, "makePathElements", ArrayList.class, File.class, ArrayList.class);
} catch (NoSuchMethodException e1) {
Log.e(TAG, "NoSuchMethodException: makeDexElements(ArrayList,File,ArrayList) failure");
// 调用dexPathList对象的makeDexElements方法
try {
Log.e(TAG, "NoSuchMethodException: try use v19 instead");
return V19.makeDexElements(dexPathList, files, optimizedDirectory, suppressedExceptions);
} catch (NoSuchMethodException e2) {
Log.e(TAG, "NoSuchMethodException: makeDexElements(List,File,List) failure");
throw e2;
}
}
}
return (Object[]) makePathElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions);
}

private byte[] getBytes(File file) throws Exception {
RandomAccessFile r = new RandomAccessFile(file, "r");
byte[] buffer = new byte[(int) r.length()];
r.readFully(buffer);
r.close();
return buffer;
}
}


国外的疫情越来越严重,不排除有卷土重来的可能,今年突如其来的疫情影响着每一个人,就业形势也不容乐观,不管是企业还是个人都要共同面对这次生存考验,今年的毕业生也是非常的不容易,不管外界如何变化,持续强化自己的专业技能永远是立身之本,提升自己的核心竞争力永远是生存之道。最近学了一些自定义gradle插件相关的知识,开发了自动上传fir并发送钉钉消息的gradle插件,提升了个人和团队的工作效率,在此记录并分享。


由于公司当前的开发流程尚未用到Jenkins这类在线的自动化构建平台,开发流程仍然是开发者本地打包再上传fir托管平台并发送上传完成的消息到钉钉群里,再@相关的测试人员提醒可进行测试,这一系列流程化的操作显得过于繁琐。作为Android开发掌握groovy这门语言也是有必要的,毕竟这是Android studio的构建脚本语言且语法与java相似,完全可以利用构建脚本实现打包后自动上传到fir托管平台,再利用自定义钉钉机器人的官方api发送钉钉消息给相关的测试人员,解放双手,自动化这一系列的流程操作来提升工作效率,减少加班的可能,打包/上传/发消息这段时间喝杯咖啡放松一下不是更好吗?我决定把这个插件打包上传到JitPack分享给团队和网络上的其他人,因为帮更多的人提升工作效率确实是一件很cool的事情

实现自定义Gradle插件的三种方式

app的gradle文件中定义

  1. 直接修改app的gradle文件,继承gradle的Plugin类,定义一个自定义插件的实现类,重写apply方法完成插件逻辑
    1
    2
    3
    4
    5
    6
    7
    apply plugin: PluginImpl
    class PluginImpl implements Plugin<Project>{
    @override
    void apply(Project project){
    println("hello Plugin")
    }
    }
    优点:方便调试
    缺点:跟app工程代码混在一起,无法给其它项目使用

    buildSrc目录定义

    在工程目录下,新建buildSrc目录,在该目录下开发
    新建文件夹src/main/groovy/xxx/xxx/xxx,其中xxx/xxx/xxx是包名,在这个目录下自定义gradle插件类PluginImpl
    新建文件夹resource/META-INF/gradle-plugins,在这个目录下新建文件xxx.xxx.xxx.properties,声明插件实现类implementation-class=xxx.xxx.xxx.PluginImpl
    效果图
    优点:方便调试,与app工程分离
    缺点:无法给其它项目使用

新增插件lib定义

新建一个gradle插件的lib库,删除其它的所有文件,保持项目结构与buildSrc目录一样,在这个lib库下开发
优点:与app工程分离,可打包上传maven,在其它项目中引用并分享给其他人
缺点:调试稍微麻烦,需发布到本地仓库
以上三种方式最好的是第三种,与app工程分离,可发布到本地maven仓库下调试,也可以打包插件上传到私有的maven仓库或者公有maven仓库如JitPack分享给其他人

定义插件参数

定义fir配置参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.demo.plugin

import javax.inject.Inject

public class FirExtension {
private String appName //app名称
private String iconPath //图标路径
private String token //fir设置中的上传token

@Inject
public FirExtension() {
}

String getAppName() {
return appName
}

void setAppName(String appName) {
this.appName = appName
}

String getIconPath() {
return iconPath
}

void setIconPath(String iconPath) {
this.iconPath = iconPath
}

String getToken() {
return token
}

void setToken(String token) {
this.token = token
}


@Override
public String toString() {
return "FirExtension{" +
"appName='" + appName + '\'' +
", iconPath='" + iconPath + '\'' +
", token='" + token + '\'' +
'}';
}
}

定义钉钉配置参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.demo.plugin

import javax.inject.Inject

public class DingTalkExtension {
private String webHook // 钉钉自定义机器人的webhook
private String title // 消息标题
private String content // 内容
private boolean isAtAll // 是否@所有人
private List<String> atMobiles // 手机号列表,单独@某些人
@Inject
public DingTalkExtension() {
}

String getWebHook() {
return webHook
}

void setWebHook(String webHook) {
this.webHook = webHook
}

String getTitle() {
return title
}

void setTitle(String title) {
this.title = title
}

String getContent() {
return content
}

void setContent(String content) {
this.content = content
}

boolean getIsAtAll() {
return isAtAll
}

void setIsAtAll(boolean isAtAll) {
this.isAtAll = isAtAll
}

List<String> getAtMobiles() {
return atMobiles
}

void setAtMobiles(List<String> atMobiles) {
this.atMobiles = atMobiles
}


@Override
public String toString() {
return "DingTalkExtension{" +
"webHook='" + webHook + '\'' +
", title='" + title + '\'' +
", content='" + content + '\'' +
", isAtAll=" + isAtAll +
", atMobiles=" + atMobiles +
'}';
}
}

定义插件参数

  • 结合上面两类参数配置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    package com.demo.plugin

    import org.gradle.api.Action
    import org.gradle.api.model.ObjectFactory

    class UploadApkPluginExtension {
    FirExtension firExtension
    DingTalkExtension dingTalkExtension
    public UploadApkPluginExtension(ObjectFactory objectFactory) {
    firExtension = objectFactory.newInstance(FirExtension.class)
    dingTalkExtension = objectFactory.newInstance(DingTalkExtension.class)
    }

    public void fir(Action<FirExtension> action) {
    action.execute(firExtension)
    }

    public void dingTalk(Action<DingTalkExtension> action){
    action.execute(dingTalkExtension)
    }

    }

    插件逻辑

    groovy中我们仍然可以使用okhttp,Gson这类Android中常用的开源框架来实现网络请求,Json序列化/反序列化相关的逻辑,封装网络请求工具类,最后在自定义Plugin中调用这个工具类完成自动化

    引入开源库

    修改插件lib中的gradle文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    apply plugin: 'java-library'
    apply plugin: 'groovy'
    apply plugin: 'maven'
    dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation gradleApi()//gradle sdk
    implementation localGroovy()//groovy sdk
    implementation 'com.squareup.okhttp3:okhttp:4.7.2'
    implementation 'com.google.code.gson:gson:2.8.6'
    }

    repositories {
    mavenCentral()
    jcenter()
    }

    网络请求工具类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    package com.demo.plugin

    import com.google.gson.Gson
    import okhttp3.*

    import java.util.concurrent.TimeUnit

    public class OkHttpUtil{
    OkHttpClient okHttpClient
    Gson gson
    DingTalk dingTalk // 序列化钉钉消息工具类
    public OkHttpUtil(){
    okHttpClient = new OkHttpClient.Builder()
    .connectTimeout(10, TimeUnit.SECONDS)
    .readTimeout(60, TimeUnit.SECONDS).build()
    gson = new Gson()
    dingTalk = new DingTalk()
    }
    // 获取fir上传证书
    BundleApp getCert(String appPackage, String apiTokenFir){

    FormBody.Builder build = new FormBody.Builder()
    build.add("bundle_id", appPackage)
    build.add("api_token", apiTokenFir)
    build.add("type", "android")
    Request request = new Request.Builder().url("http://api.bq04.com/apps").post(build.build()).build()
    Response response = okHttpClient.newCall(request).execute()
    String result = response.body.string()
    return gson.fromJson(result, BundleApp.class)
    }

    // 上传apk到fir
    String uploadApk(String apkPath,String key,String token,String appName,String appVersion,String appBuild,String fileName,String upload_url) {
    RequestBody fileBody = RequestBody.create(MediaType.parse("application/octet-stream"), new File(apkPath))
    MultipartBody body = new MultipartBody.Builder()
    .setType(MediaType.parse("multipart/form-data"))
    .addFormDataPart("key", key)
    .addFormDataPart("token", token)
    .addFormDataPart("x:name", appName)
    .addFormDataPart("x:version", appVersion)
    .addFormDataPart("x:build", appBuild)
    .addFormDataPart("file", fileName, fileBody)
    .build()
    Request requestApk = new Request.Builder().url(upload_url).post(body).build()

    Response responseApk = okHttpClient.newCall(requestApk).execute()
    return responseApk.body.string()
    }

    // 上传icon到fir
    String uploadIcon(String apkIconPath,String keyIcon,String tokenIcon,String upload_urlIcon){
    RequestBody fileBodyIcon = RequestBody.create(MediaType.parse("application/octet-stream"),new File(apkIconPath))
    MultipartBody bodyIcon = new MultipartBody.Builder()
    .setType(MediaType.parse("multipart/form-data"))
    .addFormDataPart("key", keyIcon)
    .addFormDataPart("token", tokenIcon)
    .addFormDataPart("file", "icon.png", fileBodyIcon)
    .build()
    Request requestIcon = new Request.Builder().url(upload_urlIcon).post(bodyIcon).build()
    Response responseIcon = okHttpClient.newCall(requestIcon).execute()
    return responseIcon.body.string()
    }

    ApkInfo getApkUrl(String appPackage, String apiTokenFir) {
    // 获取成功连接
    String queryurl =
    "http://api.bq04.com/apps/latest/$appPackage?api_token=$apiTokenFir&type=android"
    Request requestUrl = new Request.Builder().url(queryurl).get().build()
    Response responseUrl = okHttpClient.newCall(requestUrl).execute()
    String result = responseUrl.body.string()
    return gson.fromJson(result,ApkInfo.class)
    }

    // 发送钉钉链接消息
    void sendDingTalkLink(String text,String title,String url,String webHook){
    RequestBody linkBody = FormBody.create(MediaType.parse("application/json; charset=utf-8")
    , dingTalk.createLinkMsg(text,title,url))
    Request linkDingTalk = new Request.Builder().url(webHook)
    .post(linkBody).build()
    Response responseLink = okHttpClient.newCall(linkDingTalk).execute()
    String result = responseLink.body.string()
    println("已发送钉钉链接:$result")
    }

    // 发送钉钉文本消息
    void sendDingTalkMsg(String text,String webHook,boolean isAtAll,List<String> atMobiles){
    RequestBody textBody = FormBody.create(MediaType.parse("application/json; charset=utf-8")
    , dingTalk.createTextMsg(text,atMobiles,isAtAll))
    Request textDingTalk = new Request.Builder().url(webHook)
    .post(textBody).build()
    Response responseText = okHttpClient.newCall(textDingTalk).execute()
    String result = responseText.body.string()
    println("已发送钉钉消息:$result")
    }
    }

    序列化工具类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    package com.demo.plugin
    import com.google.gson.Gson

    public class DingTalk {
    Gson gson
    public DingTalk(){
    gson = new Gson()
    }
    // 定义钉钉链接消息的bean类
    def class LinkMsg{
    String msgtype
    Link link

    public LinkMsg(String msgtype,Link link){
    this.msgtype = msgtype
    this.link = link
    }
    }

    def class Link{
    String title
    String text
    String messageUrl

    public Link(String title,String text,String messageUrl){
    this.title = title
    this.text = text
    this.messageUrl = messageUrl
    }
    }

    // 定义钉钉文本消息的bean类
    def class TextMsg{
    String msgtype
    Text text
    At at
    public TextMsg(String msgtype,Text text,At at){
    this.msgtype = msgtype
    this.text = text
    this.at = at
    }
    }

    def class At{
    List<String> atMobiles = new ArrayList<>()
    boolean isAtAll = true
    public At(List<String> atMobiles,boolean isAtAll){
    this.atMobiles = atMobiles
    if (!atMobiles.isEmpty()){
    this.isAtAll = false
    }
    else{
    this.isAtAll = isAtAll
    }
    }
    }

    def class Text{
    String content

    public Text(String content){
    this.content = content
    }
    }

    /**
    * 构建一个钉钉链接消息
    * @param text String
    * @param title String
    * @param url String
    * @return String
    */
    String createLinkMsg(String text ,String title,String url) {
    Link link = new Link(title,text,url)
    LinkMsg linkMsg = new LinkMsg("link",link)
    return gson.toJson(linkMsg)
    }

    /**
    * 构建一个钉钉文本消息
    * @param msgtype String
    * @param content String
    * @param text String
    * @return String
    */
    String createTextMsg(String content,List<String> atMobiles,boolean isAtAll){
    Text text = new Text(content)
    At at = new At(atMobiles,isAtAll)
    TextMsg textMsg = new TextMsg("text", text, at )
    return gson.toJson(textMsg)
    }
    }

反序列化bean类定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package com.demo.plugin;

public class ApkInfo {
/**
* name : fir.im
* version : 1.0
* changelog : 更新日志
* versionShort : 1.0.5
* build : 6
* installUrl : http://download.bq04.com/v2/app/install/xxxxxxxxxxxxxxxxxxxx?download_token=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
* install_url : http://download.bq04.com/v2/app/install/xxxxxxxxxxxxxxxx?download_token=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
* update_url : http://fir.im/fir
* binary : {"fsize":6446245}
*/
private String name;
private String version;
private String changelog;
private String versionShort;
private String build;
private String installUrl;
private String install_url;
private String update_url;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getVersion() {
return version;
}

public void setVersion(String version) {
this.version = version;
}

public String getChangelog() {
return changelog;
}

public void setChangelog(String changelog) {
this.changelog = changelog;
}

public String getVersionShort() {
return versionShort;
}

public void setVersionShort(String versionShort) {
this.versionShort = versionShort;
}

public String getBuild() {
return build;
}

public void setBuild(String build) {
this.build = build;
}

public String getInstallUrl() {
return installUrl;
}

public void setInstallUrl(String installUrl) {
this.installUrl = installUrl;
}

public String getInstall_url() {
return install_url;
}

public void setInstall_url(String install_url) {
this.install_url = install_url;
}

public String getUpdate_url() {
return update_url;
}

public void setUpdate_url(String update_url) {
this.update_url = update_url;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
package com.demo.plugin;

public class BundleApp {

/**
* id : 5592ceb6537069f2a8000000
* type : ios
* short : yk37
* cert : {"icon":{"key":"xxxxx","token":"xxxxxx","upload_url":"http://upload.qiniu.com"},"binary":{"key":"xxxxx","token":"xxxxxx","upload_url":"http://upload.qiniu.com"}}
*/


private CertBean cert;

public CertBean getCert() {
return cert;
}

public void setCert(CertBean cert) {
this.cert = cert;
}

public static class CertBean {
/**
* icon : {"key":"xxxxx","token":"xxxxxx","upload_url":"http://upload.qiniu.com"}
* binary : {"key":"xxxxx","token":"xxxxxx","upload_url":"http://upload.qiniu.com"}
*/

private IconBean icon;
private BinaryBean binary;

public IconBean getIcon() {
return icon;
}

public void setIcon(IconBean icon) {
this.icon = icon;
}

public BinaryBean getBinary() {
return binary;
}

public void setBinary(BinaryBean binary) {
this.binary = binary;
}

public static class IconBean {
/**
* key : xxxxx
* token : xxxxxx
* upload_url : http://upload.qiniu.com
*/

private String key;
private String token;
private String upload_url;

public String getKey() {
return key;
}

public void setKey(String key) {
this.key = key;
}

public String getToken() {
return token;
}

public void setToken(String token) {
this.token = token;
}

public String getUpload_url() {
return upload_url;
}

public void setUpload_url(String upload_url) {
this.upload_url = upload_url;
}
}

public static class BinaryBean {
/**
* key : xxxxx
* token : xxxxxx
* upload_url : http://upload.qiniu.com
*/

private String key;
private String token;
private String upload_url;

public String getKey() {
return key;
}

public void setKey(String key) {
this.key = key;
}

public String getToken() {
return token;
}

public void setToken(String token) {
this.token = token;
}

public String getUpload_url() {
return upload_url;
}

public void setUpload_url(String upload_url) {
this.upload_url = upload_url;
}
}
}
}

自定义Plugin

  1. 继承gradle的Plugin类,重写apply方法中实现插件逻辑
  2. 在assemble任务后执行插件相关的逻辑
  • 兼容Debug/Release两种打包方式和各个flavor下的打包任务,在这些任务打包完成后执行插件逻辑,采用”assemble” + variant.name.capitalize() +”Fir”的打包命令灵活配置
  • 如渠道名为googlePlay下打debug/release包则打包任务名为assembleGooglePlayDebug/assembleGooglePlayRelease
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    package com.demo.plugin
    import org.gradle.api.Plugin
    import org.gradle.api.Project
    import org.gradle.api.Task

    class PluginImpl implements Plugin<Project> {
    UploadApkPluginExtension extension

    @Override
    void apply(Project project) {
    extension = project.extensions.create('uploadApk', UploadApkPluginExtension.class, project.getObjects())
    if (project.android.hasProperty("applicationVariants")) {
    project.android.applicationVariants.all { variant ->
    // 定义gradle任务名称
    Task uploadFir = project.task("assemble${variant.name.capitalize()}Fir").doLast {
    println("开始上传Fir")
    def (String appPackage, String apiTokenFir, String apkPath, String fileName, String appName, String appVersion, String appBuild, String apkIconPath) = getParams(project, variant)
    OkHttpUtil okHttpUtil = new OkHttpUtil()
    BundleApp bundleApp = okHttpUtil.getCert(appPackage,apiTokenFir)
    println("获取凭证信息成功")
    BundleApp.CertBean certBean = bundleApp.getCert()

    // 上传apk
    println("上传apk中...")
    String key = certBean.getBinary().getKey()
    String token = certBean.getBinary().getToken()
    String upload_url = certBean.getBinary().getUpload_url()

    String jsonApk = okHttpUtil.uploadApk(apkPath,key,token,appName,appVersion,appBuild,fileName,upload_url)
    println("上传apk文件返回结果:$jsonApk")

    // 上传icon
    println("上传Icon中...")
    String keyIcon = certBean.getIcon().getKey()
    String tokenIcon = certBean.getIcon().getToken()
    String upload_urlIcon = certBean.getIcon().getUpload_url()

    String jsonIcon = okHttpUtil.uploadIcon(apkIconPath,keyIcon,tokenIcon,upload_urlIcon)
    println("上传Icon返回结果:$jsonIcon")

    ApkInfo apkInfo = okHttpUtil.getApkUrl(appPackage,apiTokenFir)
    println("下载链接:${apkInfo.installUrl}")

    def (String content, String title, String webHook, boolean isAtAll,List<String> atMobiles) = getDingTalkParams()
    String dingTalkMsg = "点击跳转gilos下载链接(版本号:$appBuild 版本名称:$appVersion)"
    if (content.length() > 0){
    dingTalkMsg = "${dingTalkMsg},此次更新:$content"
    }

    /**
    * 发送钉钉消息
    */
    okHttpUtil.sendDingTalkLink(dingTalkMsg,title,apkInfo.installUrl,webHook)
    okHttpUtil.sendDingTalkMsg(content,webHook,isAtAll,atMobiles)
    }

    // 在assembleDebug执行后执行
    uploadFir.dependsOn project.tasks["assemble${variant.name.capitalize()}"]
    }
    }
    }

    // 获取钉钉消息配置相关的参数并返回
    private List getDingTalkParams() {
    String webHook = extension.getDingTalkExtension().getWebHook()
    String title = extension.getDingTalkExtension().getTitle()
    String content = extension.getDingTalkExtension().getContent()
    String isAtAll = extension.getDingTalkExtension().getIsAtAll()
    List<String> atMobiles = extension.getDingTalkExtension().getAtMobiles()
    [content, title, webHook, isAtAll, atMobiles]
    }

    // 获取相关gradle配置文件和fir配置相关的参数并返回
    private List getParams(Project project, variant) {
    String appName = extension.getFirExtension().getAppName()
    String appPackage = project.android.defaultConfig.applicationId
    String appVersion = project.android.defaultConfig.versionName
    String appBuild = project.android.defaultConfig.versionCode
    String apkPath = variant.outputs.first().outputFile
    String fileName = apkPath.substring(apkPath.lastIndexOf("\\") + 1, apkPath.length())
    String apkIconPath = project.android.applicationVariants.first().outputs.first().outputFile.parent.split("build")[0] + extension.getFirExtension().getIconPath()
    String apiTokenFir = extension.getFirExtension().getToken()
    // 获取上传凭证
    // println("appName:$appName")
    // println("appPackage:$appPackage")
    // println("appVersion:${appVersion}")
    // println("appBuild:${appBuild}")
    // println("apiTokenFir:${apiTokenFir}")
    // println("apkIconPath:${apkIconPath}")
    println("文件路径:$apkPath")
    println("文件名称:$fileName")
    [appPackage, apiTokenFir, apkPath, fileName, appName, appVersion, appBuild, apkIconPath]
    }
    }

    声明插件

    在lib的配置文件xxx.xxx.xxx.properties(xxx.xxx.xxx是插件定义类所在的包名)中声明插件
    1
    com.demo.plugin.PluginImpl

    获取所有打包任务下的插件执行命令

    variant.name.capitalize()这个参数是打包任务名称首字母大写,如不确定当前的打包任务名称,可以增加app的gradle代码,打印所有的打包任务名称来获取插件的执行命令,同步一下即可输出
    1
    2
    3
    project.android.applicationVariants.all { variant ->
    println("assemble${variant.name.capitalize()}Fir")
    }

    插件上传

    上传到maven仓库

    上传本地仓库实际上就是生成maven工程的本地文件夹再引用,方便调试
    增加插件lib的gradle文件代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    uploadArchives {
    repositories {
    mavenDeployer {
    repository(url: uri('../repo')) // 上传到本地仓库调试
    // 上传到远程仓库
    // repository(url: "xxx.xxx.xxx:xxxx/repo私有仓库maven地址") {
    // authentication(userName: "用户名", password: "密码")
    // }
    pom.groupId = 'com.demo.plugin'//插件lib包名
    pom.artifactId = 'firPlugin'
    pom.version = '0.4' //插件版本号
    }
    }
    }
    执行gradle uploadArchives命令上传

    上传到远程仓库(私有maven/公有maven)

使用插件

  • 修改根目录的gradle文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    // Top-level build file where you can add configuration options common to all sub-projects/modules.
    buildscript {
    ext.kotlin_version = "1.3.72"
    repositories {
    // maven { url './repo' } //本地Maven仓库地址
    // maven { url 'xxx.xxx.xxx:xxxx/repository/release'} // 私有仓库引用
    maven { url 'https://jitpack.io' } //Jitpack仓库引用

    google()
    jcenter()
    }
    dependencies {
    classpath "com.android.tools.build:gradle:4.0.0"
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    classpath 'com.github.jessieeeee:upload-apk-fir-plugin:0.7' //Jitpack插件引用
    // classpath 'com.demo.plugin:firPlugin:0.4' // 本地/私有仓库插件引用
    // NOTE: Do not place your application dependencies here; they belong
    // in the individual module build.gradle files
    }
    }

    allprojects {
    repositories {
    google()
    jcenter()
    }
    }

    task clean(type: Delete) {
    delete rootProject.buildDir
    }
  • 修改app的gradle文件,引入插件并进行相关配置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    apply plugin: 'com.demo.plugin'
    uploadApk {
    fir {
    appName = "你的app名称"
    iconPath = "src/main/res/mipmap-xxxhdpi/ic_launcher.png"
    token = "fir平台的token"
    }
    dingTalk{
    webHook = "钉钉机器人的webhook"
    title = "Android:xxx打包完成"
    content = "带关键字的消息内容" //这个关键字跟自定义钉钉机器人的安全设置有关
    isAtAll = false // 是否at所有人
    atMobiles = ["手机号1","手机号2"] //at某些人
    }

    }
    github完整项目传送门,欢迎star,issue~


随着JetPack的火热,google首推的Kotlin+LiveData+ViewModel+DataBinding的MVVM框架也越来越流行,在MVP的基础上,解决了P层带来的接口地狱和内存泄漏的问题,通信框架也从传统的handler,broadcast,interface到EventBus再到rxBus,最后到LiveDataBus,目前为止LiveDataBus确实是众多通信方案中最优的,在LiveData的加持下,拥有体积小,易封装,易维护且可感知生命周期防止内存泄漏的特点,接下来记录一下LiveDataBus的封装过程


通信框架对比

Handler 高耦合,不利于维护,内存泄漏

BroadCast 性能差,传输数据有限,打乱代码的执行逻辑

Interface 实现复杂,不利于维护

RxBus 基于RxJava,学习成本高且依赖大

EventBus 需解决混淆问题,无法感知生命周期,实现复杂

发布订阅模式和观察者模式区别

  • 观察者模式:观察者和被观察者相互知道对方的存在

  • 发布订阅模式:发布者和订阅者互相不知道对方的存在

什么是LiveData

数据持有类,持有数据并且这个数据可以被观察者监听,它是和LifeCycle绑定的,在生命周期内使用有效,减少内存泄漏和引用问题

LiveData的特点

  1. UI和数据保持一致:LiveData采用观察者模式,在数据变化时得到通知更新UI
  2. 避免内存泄漏:观察者被绑定到组件的生命周期上,组件销毁时,观察者会立刻清理数据
  3. 不会在Activity的stop状态下崩溃:当Activity处于后台,不会收到LiveData的延迟消息
  4. 解决屏幕旋转重启问题:能收到最新的数

LiveDataBus的封装

  1. 通过map维护一个消息事件和MutableLiveData的映射关系,MutableLiveData的类型默认为Object,接收任意类型,实现总线通信
  2. 将LiveDataBus封装为一个单例类
  3. 消息注册时,如果当前map中不存在,则先将消息和对应的MutableLiveData对象放入维护的map中,添加映射关系,返回当前map中缓存的MutableLiveData对象

粘性消息问题解决

具体现象:当前Activity给未启动的Activity发送一个消息,Activity在启动时能收到之前发送的消息

LiveData源码

LifecycleBoundObserver

1
2
3
4
5
6
7
8
9
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
removeObserver(mObserver);
return;
}
activeStateChanged(shouldBeActive());
}

LiveData的version初始化是-1,每次LiveData设置值都会version加1

1
2
3
4
5
6
7
8
private int mVersion = START_VERSION;
@MainThread
protected void setValue(T value) {
assertMainThread("setValue");
mVersion++;
mData = value;
dispatchingValue(null);
}

LifeCircleOwner的状态变化时,会调LiveData.ObserverWrapper的activeStateChanged方法,如果这个时候ObserverWrapper的状态是active,就会调用LiveData的dispatchingValue,继续跟踪considerNotify,如果ObserverWrapper的mLastVersion小于LiveData的mVersion,会调mObserver的onChanged方法。所以LiveDataBus注册一个新的订阅者就会收到消息,即使消息发生在订阅之前。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
void activeStateChanged(boolean newActive) {
if (newActive == mActive) {
return;
}
// immediately set active state, so we'd never dispatch anything to inactive
// owner
mActive = newActive;
boolean wasInactive = LiveData.this.mActiveCount == 0;
LiveData.this.mActiveCount += mActive ? 1 : -1;
if (wasInactive && mActive) {
onActive();
}
if (LiveData.this.mActiveCount == 0 && !mActive) {
onInactive();
}
if (mActive) {
dispatchingValue(this);
}
}

void dispatchingValue(@Nullable ObserverWrapper initiator) {
if (mDispatchingValue) {
mDispatchInvalidated = true;
return;
}
mDispatchingValue = true;
do {
mDispatchInvalidated = false;
if (initiator != null) {
considerNotify(initiator);
initiator = null;
} else {
for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
considerNotify(iterator.next().getValue());
if (mDispatchInvalidated) {
break;
}
}
}
} while (mDispatchInvalidated);
mDispatchingValue = false;
}

private void considerNotify(ObserverWrapper observer) {
if (!observer.mActive) {
return;
}
// Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
//
// we still first check observer.active to keep it as the entrance for events. So even if
// the observer moved to an active state, if we've not received that event, we better not
// notify for a more predictable notification order.
if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
if (observer.mLastVersion >= mVersion) {
return;
}
observer.mLastVersion = mVersion;
//noinspection unchecked
observer.mObserver.onChanged((T) mData);
}

Hook

在事件传递过程中拦截并监控事件的传输,修改事件传递流程
只要调用setValue版本号mVersion就会加1,此时版本号已经不一致导致onChange的调用,触发粘性事件,如果将mObservers.observer.mLastVersion修改为mVersion当前版本,就会在mObservers.observer.onChange调用前,也就是数据变化通知前return结束,这样就不调onChange方法
mObservers是Map对象,Map的item是键值对,observer是键值对的value,反射Map获取到Entry并获取到value也就是observer
继承MutableLiveData,重写observe方法,在注册监听时进行hook逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class LiveDataBus {
private final Map<String, BusMutableLiveData<Object>> bus;

private LiveDataBus() {
bus = new HashMap<>();
}

private static class SingletonHolder {
private static final LiveDataBus DEFAULT_BUS = new LiveDataBus();
}

public static LiveDataBus get() {
return SingletonHolder.DEFAULT_BUS;
}

public <T> MutableLiveData<T> with(String key, Class<T> type) {
if (!bus.containsKey(key)) {
bus.put(key, new BusMutableLiveData<>());
}
return (MutableLiveData<T>) bus.get(key);
}

public MutableLiveData<Object> with(String key) {
return with(key, Object.class);
}



private static class BusMutableLiveData<T> extends MutableLiveData<T> {

// 生命周期感知的注册监听处理,去除粘性事件
@Override
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<T> observer) {
super.observe(owner, observer);
try {
hook(observer);
} catch (Exception e) {
e.printStackTrace();
}
}


// 去除粘性事件
private void hook(@NonNull Observer<T> observer) throws Exception {
//get wrapper's version
Class<LiveData> classLiveData = LiveData.class;
Field fieldObservers = classLiveData.getDeclaredField("mObservers");
fieldObservers.setAccessible(true);
Object objectObservers = fieldObservers.get(this);
Class<?> classObservers = objectObservers.getClass();
Method methodGet = classObservers.getDeclaredMethod("get", Object.class);
methodGet.setAccessible(true);
Object objectWrapperEntry = methodGet.invoke(objectObservers, observer);
Object objectWrapper = null;
if (objectWrapperEntry instanceof Map.Entry) {
objectWrapper = ((Map.Entry) objectWrapperEntry).getValue();
}
if (objectWrapper == null) {
throw new NullPointerException("Wrapper can not be bull!");
}
Class<?> classObserverWrapper = objectWrapper.getClass().getSuperclass();
Field fieldLastVersion = classObserverWrapper.getDeclaredField("mLastVersion");
fieldLastVersion.setAccessible(true);
//get livedata's version
Field fieldVersion = classLiveData.getDeclaredField("mVersion");
fieldVersion.setAccessible(true);
Object objectVersion = fieldVersion.get(this);
//set wrapper's version
fieldLastVersion.set(objectWrapper, objectVersion);
}
}
}

对非生命周期感知的observeForever方法,生成的wrapper不是LifecycleBoundObserver而是AlwaysActiveObserver,没有办法在observeForever调用完后再改AlwaysActiveObserver的version,因为注册监听时直接调了wrapper.activeStateChanged(true)而不是在LifeCircleOwner的状态变化时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  @MainThread
public void observeForever(@NonNull Observer<? super T> observer) {
assertMainThread("observeForever");
AlwaysActiveObserver wrapper = new AlwaysActiveObserver(observer);
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
if (existing != null && existing instanceof LiveData.LifecycleBoundObserver) {
throw new IllegalArgumentException("Cannot add the same observer"
+ " with different lifecycles");
}
if (existing != null) {
return;
}
wrapper.activeStateChanged(true);
}

private class AlwaysActiveObserver extends ObserverWrapper {

AlwaysActiveObserver(Observer<? super T> observer) {
super(observer);
}

@Override
boolean shouldBeActive() {
return true;
}
}

可以用ObserverWrapper,包装真正的回调传给observeForever,回调时检查调用栈,如果回调是observeForever方法,那么就不调真正的回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 包装类包裹真正的Observer,处理非生命周期感知的注册监听
private static class ObserverWrapper<T> implements Observer<T> {

private Observer<T> observer;

public ObserverWrapper(Observer<T> observer) {
this.observer = observer;
}

@Override
public void onChanged(@Nullable T t) {
if (observer != null) {
// 目标方法不调onChanged
if (isCallOnObserve()) {
return;
}
observer.onChanged(t);
}
}

private boolean isCallOnObserve() {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
if (stackTrace != null && stackTrace.length > 0) {
for (StackTraceElement element : stackTrace) {
// 如果当前是LiveData对象且为observeForever方法
if ("android.arch.lifecycle.LiveData".equals(element.getClassName()) &&
"observeForever".equals(element.getMethodName())) {
return true;
}
}
}
return false;
}
}

修改BusMutableLiveData增加对非生命周期感知的注册监听处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
  private static class BusMutableLiveData<T> extends MutableLiveData<T> {

private Map<Observer, Observer> observerMap = new HashMap<>();

// 生命周期感知的注册监听处理,去除粘性事件
@Override
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<T> observer) {
super.observe(owner, observer);
try {
hook(observer);
} catch (Exception e) {
e.printStackTrace();
}
}

// 非生命周期感知的注册监听处理,去除粘性事件
@Override
public void observeForever(@NonNull Observer<T> observer) {
if (!observerMap.containsKey(observer)) {
observerMap.put(observer, new ObserverWrapper(observer));
}
super.observeForever(observerMap.get(observer));
}

// 非生命周期感知取消注册监听
@Override
public void removeObserver(@NonNull Observer<T> observer) {
Observer realObserver = null;
if (observerMap.containsKey(observer)) {
realObserver = observerMap.remove(observer);
} else {
realObserver = observer;
}
super.removeObserver(realObserver);
}

// 去除粘性事件
private void hook(@NonNull Observer<T> observer) throws Exception {
//get wrapper's version
Class<LiveData> classLiveData = LiveData.class;
Field fieldObservers = classLiveData.getDeclaredField("mObservers");
fieldObservers.setAccessible(true);
Object objectObservers = fieldObservers.get(this);
Class<?> classObservers = objectObservers.getClass();
Method methodGet = classObservers.getDeclaredMethod("get", Object.class);
methodGet.setAccessible(true);
Object objectWrapperEntry = methodGet.invoke(objectObservers, observer);
Object objectWrapper = null;
if (objectWrapperEntry instanceof Map.Entry) {
objectWrapper = ((Map.Entry) objectWrapperEntry).getValue();
}
if (objectWrapper == null) {
throw new NullPointerException("Wrapper can not be bull!");
}
Class<?> classObserverWrapper = objectWrapper.getClass().getSuperclass();
Field fieldLastVersion = classObserverWrapper.getDeclaredField("mLastVersion");
fieldLastVersion.setAccessible(true);
//get livedata's version
Field fieldVersion = classLiveData.getDeclaredField("mVersion");
fieldVersion.setAccessible(true);
Object objectVersion = fieldVersion.get(this);
//set wrapper's version
fieldLastVersion.set(objectWrapper, objectVersion);
}
}
}


注解和反射是Android开发的基础,也是项目框架搭建中用到的必不可少的技术,减少重复代码编写,提高开发效率,并且广泛用于知名的开源框架中,有利于我们阅读源码,同时提升自己的架构能力和封装基础库的能力。下面对注解和反射的学习做一个记录。


原注解

元注解是定义注解的注解

@Retention:该注解保留阶段,保留的时长, 源码(RetentionPolicy.SOURCE) < 字节码(RetentionPolicy.CLASS) < 运行时(RetentionPolicy.RUNTIME)

  • 源码级别的注解:应用于APT编译期处理注解生成JAVA代码,生成额外的辅助类,如Dagger2, ButterKnife, EventBus3
  • 字节码级别的注解:应用于字节码插桩,可用于埋点,如ASM,AspectJ
  • 运行时级别的注解:反射获取被注解标记的变量/方法/类的信息

@Target:该注解被使用的位置,字段枚举常量级(ElementType.FIELD),局部变量级(ElementType.LOCAL_VARIABLE),方法级(ElementType.METHOD),方法级(ElementType.PARAMETER),类级接口级(ElementType.TYPE),包级(ElementType.PACKAGE),构造方法(ElementType.CONSTRUCTOR),注解级(ElementType.ANNOTATION_TYPE)

注解+反射实现Intent参数传递

定义注解

用反射获取该变量的信息需保留到运行时阶段且注解应用于类的字段变量之上

1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface AutoInject {
String key();
}

注入逻辑

  1. 获取到该Activity的class对象,获取到该Activity的Intent数据
  2. 没有任何传值时直接返回
  3. 如果有参数传递,获取到该Activity的所有字段变量
  4. 确定每个字段变量的传值key,遍历所有的字段变量,判断该字段变量是否被注解,如果被注解则获取到注解对象,判断注解上的参数传值是否为空,如果为空直接使用被注解的变量名称为key,不为空则使用注解上的参数传值为key
  5. 判断传递的参数中是否有该key的值,如果有获取传入的值,如果字段变量不为数组,这里传入的值为最终结果
  6. 获取被注解的变量类型,如果该变量是数组并且是序列化的类,强转对象数组,并复制一份新的对象数组为最终结果,修改Activity中该变量的访问权限,将结果赋值给该变量
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    public static void injectBundle(Activity activity){
    // 获取class对象
    Class<? extends Activity> cls = activity.getClass();
    Intent intent = activity.getIntent();
    Bundle bundle = intent.getExtras();
    // 如果没有传值返回
    if (bundle == null){
    return;
    }
    // 获取所有的变量
    Field[] fields = cls.getDeclaredFields();
    // 遍历activity的变量
    for(Field field: fields){
    // 判断是否被注解
    if (field.isAnnotationPresent(AutoInject.class)){
    // 获取到注解对象
    AutoInject autoInject = field.getAnnotation(AutoInject.class);
    // 判断注解传值是否为空,如果为空使用当前被注解的变量名称
    String key = TextUtils.isEmpty(autoInject.key()) ? field.getName() : autoInject.key();
    // 如果有该key的传值
    if (bundle.containsKey(key)){
    // 获取传入的值
    Object object = bundle.get(key);
    // 获取被注解的变量类型
    Class<?> componentType = field.getType().getComponentType();
    // 如果当前变量是数组并且是序列化的class
    if (field.getType().isArray() && Parcelable.class.isAssignableFrom(componentType)){
    // 强转对象数组
    Object[] objs = (Object[])object;
    // 复制到新的对象数组
    Object[] objects = Arrays.copyOf(objs, objs.length, (Class<? extends Object[]>) field.getType());
    object = objects;
    }
    // 修改该变量的访问权限
    field.setAccessible(true);
    try {
    // 设置当前activity该变量的值为传值对象
    field.set(activity,object);
    } catch (IllegalAccessException e) {
    e.printStackTrace();
    }
    }
    }
    }
    }

    使用

  • 第一个Activity传递参数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //User是序列化对象
    User user1 = new User("小明",12);
    User user2 = new User("小王",13);
    User[] users = new User[2];
    users[0] = user1;
    users[1] = user2;
    ArrayList<User> userList = new ArrayList<User>();
    userList.add(user1);
    userList.add(user2);
    // 传对象
    intent.putExtra("test1",user1);
    // 传对象数组
    intent.putExtra("test2",users);
    // 传对象列表
    intent.putParcelableArrayListExtra("test3",userList);
  • 第二个Activity声明接收变量添加注解
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 接收对象
    @AutoInject(key = "test1")
    private User value1;
    // 接收对象数组
    @AutoInject(key = "test2")
    private User[] value4;
    // 接收对象列表
    @AutoInject(key = "test3")
    private ArrayList<User> value5;
    // Activity创建时调注入逻辑
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    InjectUtils.injectBundle(this);
    }

注解+反射实现View.OnClick注入逻辑

定义注解

定义注解的注解

声明监听器类型,注入的方法

1
2
3
4
5
6
7
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EventType {

Class listenerType();
String listenerSetter();
}

定义方法注解

用反射获取该变量的信息需保留到运行时阶段且注解应用于方法之上

普通点击监听的类型为View.OnClickListener.class,作用的方法为setOnClickListener

1
2
3
4
5
6
7
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@EventType(listenerType = View.OnClickListener.class, listenerSetter = "setOnClickListener")
public @interface OnClick {
int[] value();

}

长按监听的类型为View.OnLongClickListener.class,作用的方法为setOnLongClickListener

1
2
3
4
5
6
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@EventType(listenerType = View.OnLongClickListener.class, listenerSetter = "setOnLongClickListener")
public @interface OnLongClick {
int[] value();
}

注入逻辑

  1. 获取到该Activity的class对象,获取到当前Activity的所有方法
  2. 遍历所有方法,获取到方法的所有注解
  3. 遍历所有注解,获取到当前注解类型
  4. 如果是EventType目标注解,获取到注解对象,获取到注解上定义的传值,监听的Class类型,注解作用的方法
  5. 获取到方法上注解传入的id
  6. 修改方法的访问权限
  7. 利用Java的代理器生成代理对象,动态代理OnClickListener/OnLongClickListener接口
  8. 自定义InvocationHandler添加在对应的点击事件上注入的逻辑
  9. 获取到View对象,在对应的点击方法上注入代理对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    public class InjectClick {
    public static void injectEvent(Activity activity){
    // 获取到当前的activity的class对象
    Class<? extends Activity> activityClass = activity.getClass();
    // 获取到当前activity的所有方法
    Method[] methods = activityClass.getDeclaredMethods();
    // 遍历所有方法
    for (Method method: methods){
    // 获取到方法的所有注解
    Annotation[] annotations = method.getAnnotations();
    // 遍历所有注解
    for (Annotation annotation: annotations){
    // 获取到注解的类型
    Class<? extends Annotation> annotationType = annotation.annotationType();
    // 如果是EventType的注解
    if (annotationType.isAnnotationPresent(EventType.class)){
    // 获取到注解对象
    EventType eventType = annotationType.getAnnotation(EventType.class);
    // 获取到注解上定义的传值
    Class listenerType = eventType.listenerType();
    String listenerSetter = eventType.listenerSetter();
    try{
    // 获取到注解传入的id值
    Method valueMethod = annotationType.getDeclaredMethod("value");
    int[] viewIds = (int[]) valueMethod.invoke(annotation);
    method.setAccessible(true);
    ListenerInvocationHandler<Activity> handler = new ListenerInvocationHandler(activity, method);
    // OnClickListener/OnLongClickListener的代理对象
    Object listenerProxy = Proxy.newProxyInstance(listenerType.getClassLoader(),
    new Class[]{listenerType}, handler);

    // 遍历传入的id
    for (int viewId : viewIds) {
    // 获得view
    View view = activity.findViewById(viewId);
    // 获得OnClickListener/OnLongClickListener的setOnClickLisnter/setOnLongClickLisnter方法
    Method setter = view.getClass().getMethod(listenerSetter, listenerType);
    // 在View的点击方法上注入代理对象
    setter.invoke(view, listenerProxy);
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }
    }
    }


    /**
    * 兼容自定义view注入,所以是泛型: T = Activity/View
    *
    * @param <T>
    */
    static class ListenerInvocationHandler<T> implements InvocationHandler {

    private Method method;
    private T target;

    public ListenerInvocationHandler(T target, Method method) {
    this.target = target;
    this.method = method;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Log.v("injectClick","注入点击事件逻辑");
    return this.method.invoke(target, args);
    }
    }

使用

声明对应的点击回调,并添加注解,传入被注入点击事件View的Id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@OnClick({R.id.text, R.id.button})
public void click(View view) {
switch (view.getId()) {
case R.id.text:
Log.i("click", "click: 按钮1");
break;
case R.id.button:
Log.i("click", "click: 按钮2");
break;
}
}

@OnLongClick({R.id.text, R.id.button})
public boolean longClick(View view) {
switch (view.getId()) {
case R.id.text:
Log.i("click", "longClick: 按钮1");
break;
case R.id.button:
Log.i("click", "longClick: 按钮2");
break;
}
return false;
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
InjectClick.injectEvent(this);
}


又是很久没有写博客了,疫情结束后经济萧条,很多年轻人受疫情的影响而被迫离职至今未找到工作。今年受疫情影响,招聘需求明显萎缩,离职人员近几个月大幅增加,这应该是最难找工作的一年,相信不管是在职或离职的小伙伴应该都不太轻松。有工作经验的人尚且如此,今年毕业的应届生求职可能会更加艰难。生活不易,但是我们仍要以积极的心态去应对,相信终有一天这一切都会过去,生活又美好如初。受疫情影响今年我也经历了找工作这段痛苦的日子,受老天眷顾找到了一份新的工作,但仍然感觉自己的技术实力还需提高,作为程序员扎实的基础是核心竞争力中不可缺少的一部分,在扎实的基础下进一步扩展深度,学习更多的计算机底层原理,同时扩展广度,学习当下的新技术,只有保证自己的核心竞争力,才能在任何时候面对危机和考验从容应对。代理模式是设计模式中的常考点且很多开源框架都用到了这个模式,有必要学习并加深理解,在此做一个学习记录。


静态代理

外地拼搏的年轻人总是要面对租房的问题,这里以租房为例,理解静态代理模式。首先我们需要一个租赁接口,这个接口中只有一个方法就是租房

1
2
3
public interface Rent {
void rentHouse();
}

有一个年轻人叫小明,在外地拼搏的他需要租房,需要继承租赁这个接口并实现租房的方法

1
2
3
4
5
6
public class XiaoMing implements Rent{
@Override
public void rentHouse() {
Log.v("proxy","小明需要租房");
}
}

有一个中介机构链家可以帮你租房,你将租房需求告诉他们,当你满意成功租房后需要支付给他们中介费

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LianJia implements Rent{

private Rent rent;
public LianJia(Rent rent){
this.rent = rent;
}
@Override
public void rentHouse() {
Log.v("proxy","链家获取你的租房需求");
rent.rentHouse();
Log.v("proxy","链家帮你租房");
}
}

接下来让链家帮小明租房

1
2
3
4
5
6
// 声明一个有租房需求的人小明
Rent xiaoMing = new XiaoMing();
// 声明中介机构链家,接受小明的租房需求
LianJia lianJia = new LianJia(xiaoMing);
// 链家帮小明租房
lianJia.rentHouse();

动态代理

Java中有个代理器可以实现接口对象的代理并生成对应的代理对象,我们利用Proxy.newProxyInstance生成实现了Rent接口的小明并生成代理对象,invoke方法中第一个参数是代理对象,第二个参数是被代理的方法,第三个参数是当前方法传入的参数值

1
2
3
4
5
6
Object o = Proxy.newProxyInstance(SplashActivity.class.getClassLoader(), new Class[]{Rent.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return method.invoke(xiaoMing,args);
}
});

将代理对象转化为Rent接口的实例并调用租房的方法

1
2
3
// 代理生成器的代理对象
Rent xiaoMingProxy = (Rent) o;
xiaoMingProxy.rentHouse();

手写Retrofit框架

Retrofit的核心就是通过动态代理,将注解参数拼接成一个完整的http请求再给网络请求框架去处理

自定义注解

Field

1
2
3
4
5
@Target(ElementType.PARAMETER) // 表单提交,作用在POST请求的参数上
@Retention(RetentionPolicy.RUNTIME) // 运行期保留
public @interface Field {
String value();
}

GET

1
2
3
4
5
@Target(ElementType.METHOD) // 声明GET请求,作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行期保留
public @interface GET {
String value();
}

POST

1
2
3
4
5
@Target(ElementType.METHOD) // 声明POST请求,作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行期保留
public @interface POST {
String value();
}

Query

1
2
3
4
5
@Target(ElementType.PARAMETER) // url上拼接,作用在请求的参数上
@Retention(RetentionPolicy.RUNTIME) // 运行期保留
public @interface Query {
String value();
}

实现Retrofit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class MyRetrofit {
final Map<Method, ServiceMethod> serviceMethodMap = new ConcurrentHashMap<>(); // 缓存调用方法到方法参数解析服务的映射
final Call.Factory callFactory;// 网络请求框架
final HttpUrl baseUrl;// 请求服务器url地址

public MyRetrofit(Call.Factory callFactory, HttpUrl baseUrl) {
this.callFactory = callFactory;
this.baseUrl = baseUrl;
}

// 返回请求接口的代理对象
public <T> T create(final Class<T> service) {
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class[]{service},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//解析方法所有注解信息
ServiceMethod serviceMethod = loadServiceMethod(method);
//传入参数的值并返回拼接好的请求
return serviceMethod.invoke(args);
}
});
}

// 双琐式创建实例
private ServiceMethod loadServiceMethod(Method method){
ServiceMethod serviceMethod = serviceMethodMap.get(method);
if (serviceMethod == null){
synchronized (serviceMethodMap){
// 直接取出对应的方法参数解析服务
serviceMethod = serviceMethodMap.get(method);
// 如果没有缓存就初始化调用,再放入缓存
if (serviceMethod == null){
serviceMethod = new ServiceMethod.Builder(this,method).build();
serviceMethodMap.put(method,serviceMethod);
}
}
}
return serviceMethod;
}

// 接收外部传入的参数并构建实例
public static final class Builder{
private HttpUrl baseUrl;
private Call.Factory callFactory;
public Builder callFactory(Call.Factory callFactory){
this.callFactory = callFactory;
return this;
}

public Builder baseUrl(String url){
this.baseUrl = HttpUrl.parse(url);
return this;
}

public MyRetrofit build(){
if (baseUrl == null){
throw new IllegalStateException("base url required");
}
Call.Factory callFactory = this.callFactory;
if(callFactory == null){
callFactory = new OkHttpClient();
}
return new MyRetrofit(callFactory, baseUrl);
}
}
}

实现方法参数解析服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
public class ServiceMethod {
private final Call.Factory callFactory;// 网络请求框架
private final String url; // 当前的请求路径
private final boolean hasBody; // 是否有请求体
private final ParameterHandler[] parameterHandlers;
private FormBody.Builder formBuild; // 请求体表单
HttpUrl baseUrl;// 请求服务器url
String httpMethod; // 请求方式post/get
HttpUrl.Builder urlBuilder;

public ServiceMethod(Builder builder) {
baseUrl = builder.retrofit.baseUrl;
callFactory = builder.retrofit.callFactory;

httpMethod = builder.httpMethod;
url = builder.url;
hasBody = builder.hasBody;
parameterHandlers = builder.parameterHandlers;

//如果有请求体,创建okhttp请求体对象
if (hasBody) {
formBuild = new FormBody.Builder();
}
}

// 处理传参
public Object invoke(Object[] args) {
// 处理请求的地址与参数
for (int i = 0; i < parameterHandlers.length; i++) {
ParameterHandler handlers = parameterHandlers[i];
//handler内本来就记录了key,现在给到对应的value
handlers.apply(this, args[i].toString());
}

//获取最终请求地址
HttpUrl httpUrl;
if (urlBuilder == null) {
urlBuilder = baseUrl.newBuilder(url);
}
httpUrl = urlBuilder.build();

//请求体
FormBody formBody = null;
if (formBuild != null) {
formBody = formBuild.build();
}

// 最后拼接成功的请求
Request request = new Request.Builder().url(httpUrl).method(httpMethod, formBody).build();
return callFactory.newCall(request);
}

// get请求, 按http的方式处理参数
public void addQueryParameter(String key, String value) {
if (urlBuilder == null) {
urlBuilder = baseUrl.newBuilder(url);
}
urlBuilder.addQueryParameter(key, value);
}

//Post请求, 按http的方式处理参数
public void addFiledParameter(String key, String value) {
formBuild.add(key, value);
}

public static class Builder{
private final MyRetrofit retrofit;
private final Annotation[] methodAnnotations;
private final Annotation[][] parameterAnnotations;
ParameterHandler[] parameterHandlers;
private String httpMethod;
private String url;
private boolean hasBody;

public Builder(MyRetrofit retrofit, Method method) {
this.retrofit = retrofit;
//获取方法的所有注解
methodAnnotations = method.getAnnotations();
//获取方法参数的所有注解
parameterAnnotations = method.getParameterAnnotations();
}

public ServiceMethod build() {

//处理POST与GET
for (Annotation methodAnnotation : methodAnnotations) {
if (methodAnnotation instanceof POST) {
//记录请求方式
this.httpMethod = "POST";
//记录请求url的path
this.url = ((POST) methodAnnotation).value();
// 是否有请求体
this.hasBody = true;
} else if (methodAnnotation instanceof GET) {
this.httpMethod = "GET";
this.url = ((GET) methodAnnotation).value();
this.hasBody = false;
}
}

// 处理方法参数的注解
int length = parameterAnnotations.length;
// 创建请求参数映射数组
parameterHandlers = new ParameterHandler[length];
for (int i = 0; i < length; i++) {
// 一个参数的所有注解
Annotation[] annotations = parameterAnnotations[i];
// 处理每一个注解
for (Annotation annotation : annotations) {
// 如果是Field注解
if (annotation instanceof Field) {
//得到注解上的value也就是请求参数的key
String value = ((Field) annotation).value();
// 传入参数的key
parameterHandlers[i] = new ParameterHandler.FieldParameterHandler(value);
}
// 如果是Query注解
else if (annotation instanceof Query) {
String value = ((Query) annotation).value();
parameterHandlers[i] = new ParameterHandler.QueryParameterHandler(value);

}
}
}

return new ServiceMethod(this);
}
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public abstract class ParameterHandler {
abstract void apply(ServiceMethod serviceMethod, String value);
static class QueryParameterHandler extends ParameterHandler{
String key;
public QueryParameterHandler(String key){
this.key = key;
}

@Override
void apply(ServiceMethod serviceMethod, String value) {
serviceMethod.addQueryParameter(key,value);
}
}

static class FieldParameterHandler extends ParameterHandler{
String key;
public FieldParameterHandler(String key) {
this.key = key;
}

@Override
void apply(ServiceMethod serviceMethod, String value) {
serviceMethod.addFiledParameter(key,value);
}
}
}

定义网络请求Api

1
2
3
4
5
6
7
public interface WeatherApi {
@POST("/v3/weather/weatherInfo")
Call postWeather(@Field("city") String city, @Field("key") String key);

@GET("/v3/weather/weatherInfo")
Call getWeather(@Query("city") String city, @Query("key") String key);
}

自定义Retrofit发起请求

初始化Retrofit

1
2
MyRetrofit myRetrofit = new MyRetrofit.Builder().baseUrl("https://restapi.amap.com").build();
weatherApi = myRetrofit.create(WeatherApi.class);

发起post请求

1
2
3
4
5
6
7
8
9
10
11
12
13
okhttp3.Call getCall = weatherApi.getWeather("110101", "ae6c53e2186f33bbf240a12d80672d1b");
getCall.enqueue(new okhttp3.Callback() {
@Override
public void onFailure(okhttp3.Call call, IOException e) {

}

@Override
public void onResponse(okhttp3.Call call, okhttp3.Response response) throws IOException {
Log.i("onResponse", "onResponse enjoy get: " + response.body().string());
response.close();
}
});

发起get请求

1
2
3
4
5
6
7
8
9
10
11
12
13
okhttp3.Call postCall = weatherApi.postWeather("110101", "ae6c53e2186f33bbf240a12d80672d1b");
postCall.enqueue(new okhttp3.Callback() {
@Override
public void onFailure(okhttp3.Call call, IOException e) {

}

@Override
public void onResponse(okhttp3.Call call, okhttp3.Response response) throws IOException {
Log.i("onResponse", "onResponse enjoy post: " + response.body().string());
response.close();
}
});


又是很久没有写博客了,最近由于武汉疫情,这个春节从初一起就全程宅家,春节假期一延再延,本该上班的我们依然没有正常返工,街上仍然是没有几个人,快递延迟,很多人仍然是宅在家里远程上班。停工不停学,之前的漫画项目主要使用了网易漫画和腾讯漫画爬取的数据,而网易漫画在不久前被bilibili收购了,现在正式改为bilibili漫画,所以之前的爬虫逻辑和接口失效了,正好趁着这个时间把之前的服务端数据爬取接口改一下,这里做一个简单的记录。


爬取漫画列表和漫画详情都没什么问题,跟之前的思路一样,改一下对应的标签重新绑定目标数据,但是在爬去漫画内容的时候,发现漫画图片的链接已经不在html的标签中了,而是直接获取到服务端返回的图片地址后用canvas绘制出来的。如下图所示:
截图

所以只要我们能获取到该页面的网络请求结果,我们就能过滤出图片地址,也就不用去标签中获取目标数据了,接下来我发现在chrome浏览器中元素审查界面的网络拦截器中可以找到漫画内容的图片链接,如下图所示:
截图

所以只要我们目前使用的爬虫框架puppeteer能够拦截到网络请求的结果就可以解决标签中无法爬取到图片地址的问题了。我查了一下puppeteer的官方文档,发现了这些api

开启拦截

page.setRequestInterception(true)

监听服务端返回
page.on('response')

另外还可以监听当前页面的请求

page.on('request')

返回一个自定义的响应

req.respond()

根据当前的场景,我们需要获取服务器返回的数据,并过滤其中的漫画图片地址

https://manga.hdslb.com/bfs/manga/a39f3fd06e540fe14b7e591ced413f372bd9f85f.jpg@660w.jpg?token=3a96fd02961137c00a76145fb381d544&ts=5e3a7d62

https://manga.hdslb.com/bfs/manga/4cd38fde6581e146c249373c9ed120b75047004a.jpg@660w.jpg?token=2e740fd7ccfefec3f1f5d0d27e925e33&ts=5e3a7d62

https://manga.hdslb.com/bfs/manga/18a6e2e4739e7e3eb9888e7220b398fb2d0def9d.jpg@660w.jpg?token=0e8b573c3c40fa3c5554d2df6ec8b2cb&ts=5e3a7d62

https://manga.hdslb.com/bfs/manga/e6cbe3162d3c4b6e1175557a90d4a0e54562032f.jpg@660w.jpg?token=0d1a55fe1d27d3527a9340034cd5a35f&ts=5e3a7d62

https://manga.hdslb.com/bfs/manga/0c649ad9107997801dd4e45179323381b16dc50a.jpg@660w.jpg?token=fbdf6eb7df231dbe345f4411a32d56c6&ts=5e3a7d62

以上的链接地址特征

  1. https://manga.hdslb.com/bfs/manga/开头
  2. 尾部都跟有token和ts的参数,?token=&ts=
  3. @660w.jpg看起来是传入了请求图片的宽度和图片格式
  4. 抛开@后面的尾部参数,请求的图片格式和@后面的尾部参数传入的格式一致

根据特征,抓取漫画图片代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const puppeteer = require('puppeteer')
const browser = await puppeteer.launch({
headless: false,
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
})
const page = await browser.newPage();
await page.setRequestInterception(true);
page.on('response',
function (response){
let url = response.url().toString()
let tokenStart = url.indexOf('?token=')
let tsStart = url.indexOf('&ts=')
// 捕获目标url
if (url.indexOf('manga.hdslb.com/bfs/manga/') !== -1
&& tokenStart !== -1
&& tsStart !== -1){
// 尾部参数请求的格式
let imgParamStart = url.indexOf('@')
if (imgParamStart !== -1){
// 图片本身的格式
let suffix = url.substring(imgParamStart - 3, imgParamStart)
// 请求图片的参数
let imgParam = url.substring(imgParamStart, tokenStart)
let imgWidthEnd = imgParam.indexOf('w')
// 请求图片的宽度
let imgWidth = imgParam.substring(1, imgWidthEnd)
let imgFormatStart = imgParam.indexOf('.')
// 请求图片的格式
let imgFormat = imgParam.substring(imgFormatStart + 1,tokenStart)
// 过滤图片信息
if (suffix === imgFormat){
console.log(response.url())
let data = response.url().toString();
let imgHeight = 1320
resolve({data, imgWidth, imgHeight})
}
}
}
}
)
// 跳转到目标网站
await page.goto(url)


又是很长一段时间没写博客了,最近工作繁忙,加班多,也是无奈啊~但是好习惯还是应该坚持下去的,平时工作钉钉作为主要的沟通工具,发现它除了是个聊天软件以外,还有一个好玩的东西-钉钉机器人,做些自动化推送提醒还是不错的,之前在telegram看到过类似的东西,它有一套专属api,可发送一些自定义的消息,实现一些自动化功能,目前推送提醒的解决方案一般是app推送,短信,企业微信,邮件,为了推送提醒单独开发一个app成本太高,短信现在几乎都是收费的,企业微信注册麻烦,而邮件一般都不会及时去看的。钉钉机器人创建成本低,又是主力聊天工具之一,对于个人或群组推送还是很实用的,随便拉两个人创建一个群就可以添加机器人了,如果只是做个人提醒的话,创建好后可以把这两个人T掉。唯一的限制是1秒最多发送20条~由于个人用iphone手机,本土化做得不好,节假日后补班那几天经常因为忘记定闹钟而睡过头,又不想下载第三方app,准备用家里有个树莓派做个人小型服务器,每天晚上定时跑一个python脚本,提醒我明天是否上班,如果上班提醒我设好闹钟,并请求天气预报,如果明天上班且下雨,提醒我闹钟提前半个小时,下雨早点出门不堵啊。


创建钉钉机器人

只要是个群组即可创建钉钉机器人,先拉两个人组成群组,在群组菜单中选择群组助手,添加机器人选择自定义机器人,创建的时候填写机器人名字,这里需要复制webhook的url链接,安全设置中可勾选自定义关键字,签名,ip地址,这里为了简单选择自定义关键字,设置为“提醒”,只要发送内容中带了关键字“提醒”即可。如果选择签名的话需要参考官方的签名算法,签名需添加到webhook的url上,如果选择ip地址的话,只有该ip地址的服务器才可以调用api发送消息。

钉钉机器人发送消息

发起post请求,参数为json格式,就是机器人发送的内容,请求的地址就是创建机器人的webhook,如果安全设置选择了签名的话要带上签名参数。

1
2
3
4
5
6
7
8
import requests
import json
def messageRobot(msg):
url = '你的webhook'
headers = {
'Content-Type': 'application/json'
}
requests.post(url, data=json.dumps(msg), headers=headers)

获取日期信息

找了一个免费api,http://timor.tech/api/holiday/info/{yyyy-MM-dd},{yyyy-MM-dd}为要查询的日期,请求结果如下:
如果该接口请求失败,code = 1,也需要给自己发送接口请求失败的消息,及时处理
type = 0, 明天是正常的工作日
request url:http://timor.tech/api/holiday/info/2019-10-11

1
2
3
4
5
6
7
8
9
{
"code": 0,
"type": {
"type": 0,
"name": "周五",
"week": 5
},
"holiday": null
}

type = 1, 明天是正常的双休日
request url: http://timor.tech/api/holiday/info/2019-10-13

1
2
3
4
5
6
7
8
9
{
"code": 0,
"type": {
"type": 1,
"name": "周日",
"week": 7
},
"holiday": null
}

type = 2, 明天是法定节假日
request url:http://timor.tech/api/holiday/info/2019-10-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"code": 0,
"type": {
"type": 2,
"name": "国庆节",
"week": 2
},
"holiday": {
"holiday": true,
"name": "国庆节",
"wage": 3,
"date": "2019-10-01"
}
}

type = 3, 明天是补班的特殊日子
request url:http://timor.tech/api/holiday/info/2019-10-12

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"code": 0,
"type": {
"type": 3,
"name": "国庆节后调休",
"week": 6
},
"holiday": {
"holiday": false,
"name": "国庆节后调休",
"after": true,
"wage": 1,
"target": "国庆节",
"date": "2019-10-12"
}
}

获取天气预报

也是找了一个免费Api,http://t.weather.sojson.com/api/weather/city/{citycode},参数是城市编码,以成都为例,请求结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
{
"message": "success感谢又拍云(upyun.com)提供CDN赞助",
"status": 200,
"date": "20200307",
"time": "2020-03-07 22:52:50",
"cityInfo": {
"city": "成都市",
"citykey": "101270101",
"parent": "四川",
"updateTime": "22:30"
},
"data": {
"shidu": "63%",
"pm25": 52.0,
"pm10": 79.0,
"quality": "良",
"wendu": "14",
"ganmao": "极少数敏感人群应减少户外活动",
"forecast": Array[15][
{
"date": "07",
"high": "高温 19℃",
"low": "低温 12℃",
"ymd": "2020-03-07",
"week": "星期六",
"sunrise": "07:25",
"sunset": "19:06",
"aqi": 94,
"fx": "无持续风向",
"fl": "<3级",
"type": "多云",
"notice": "阴晴之间,谨防紫外线侵扰"
},
{
"date": "08",
"high": "高温 18℃",
"low": "低温 11℃",
"ymd": "2020-03-08",
"week": "星期日",
"sunrise": "07:24",
"sunset": "19:07",
"aqi": 57,
"fx": "无持续风向",
"fl": "<3级",
"type": "阴",
"notice": "不要被阴云遮挡住好心情"
},
{
"date": "09",
"high": "高温 17℃",
"low": "低温 8℃",
"ymd": "2020-03-09",
"week": "星期一",
"sunrise": "07:22",
"sunset": "19:08",
"aqi": 52,
"fx": "无持续风向",
"fl": "<3级",
"type": "多云",
"notice": "阴晴之间,谨防紫外线侵扰"
},
{
"date": "10",
"high": "高温 18℃",
"low": "低温 8℃",
"ymd": "2020-03-10",
"week": "星期二",
"sunrise": "07:21",
"sunset": "19:08",
"aqi": 59,
"fx": "无持续风向",
"fl": "<3级",
"type": "多云",
"notice": "阴晴之间,谨防紫外线侵扰"
},
{
"date": "11",
"high": "高温 15℃",
"low": "低温 9℃",
"ymd": "2020-03-11",
"week": "星期三",
"sunrise": "07:20",
"sunset": "19:09",
"aqi": 55,
"fx": "无持续风向",
"fl": "<3级",
"type": "多云",
"notice": "阴晴之间,谨防紫外线侵扰"
},
{
"date": "12",
"high": "高温 17℃",
"low": "低温 11℃",
"ymd": "2020-03-12",
"week": "星期四",
"sunrise": "07:19",
"sunset": "19:10",
"aqi": 62,
"fx": "无持续风向",
"fl": "<3级",
"type": "多云",
"notice": "阴晴之间,谨防紫外线侵扰"
},
{
"date": "13",
"high": "高温 15℃",
"low": "低温 11℃",
"ymd": "2020-03-13",
"week": "星期五",
"sunrise": "07:18",
"sunset": "19:10",
"fx": "无持续风向",
"fl": "<3级",
"type": "多云",
"notice": "阴晴之间,谨防紫外线侵扰"
},
{
"date": "14",
"high": "高温 20℃",
"low": "低温 12℃",
"ymd": "2020-03-14",
"week": "星期六",
"sunrise": "07:16",
"sunset": "19:11",
"fx": "东北风",
"fl": "<3级",
"type": "阴",
"notice": "不要被阴云遮挡住好心情"
},
{
"date": "15",
"high": "高温 15℃",
"low": "低温 11℃",
"ymd": "2020-03-15",
"week": "星期日",
"sunrise": "07:15",
"sunset": "19:12",
"fx": "东北风",
"fl": "<3级",
"type": "阴",
"notice": "不要被阴云遮挡住好心情"
},
{
"date": "16",
"high": "高温 14℃",
"low": "低温 9℃",
"ymd": "2020-03-16",
"week": "星期一",
"sunrise": "07:14",
"sunset": "19:12",
"fx": "东北风",
"fl": "<3级",
"type": "阴",
"notice": "不要被阴云遮挡住好心情"
},
{
"date": "17",
"high": "高温 16℃",
"low": "低温 10℃",
"ymd": "2020-03-17",
"week": "星期二",
"sunrise": "07:13",
"sunset": "19:13",
"fx": "南风",
"fl": "<3级",
"type": "小雨",
"notice": "雨虽小,注意保暖别感冒"
},
{
"date": "18",
"high": "高温 20℃",
"low": "低温 9℃",
"ymd": "2020-03-18",
"week": "星期三",
"sunrise": "07:12",
"sunset": "19:14",
"fx": "南风",
"fl": "<3级",
"type": "多云",
"notice": "阴晴之间,谨防紫外线侵扰"
},
{
"date": "19",
"high": "高温 22℃",
"low": "低温 11℃",
"ymd": "2020-03-19",
"week": "星期四",
"sunrise": "07:10",
"sunset": "19:14",
"fx": "东南风",
"fl": "<3级",
"type": "阴",
"notice": "不要被阴云遮挡住好心情"
},
{
"date": "20",
"high": "高温 23℃",
"low": "低温 12℃",
"ymd": "2020-03-20",
"week": "星期五",
"sunrise": "07:09",
"sunset": "19:15",
"fx": "东北风",
"fl": "<3级",
"type": "多云",
"notice": "阴晴之间,谨防紫外线侵扰"
},
{
"date": "21",
"high": "高温 22℃",
"low": "低温 13℃",
"ymd": "2020-03-21",
"week": "星期六",
"sunrise": "07:08",
"sunset": "19:16",
"fx": "西北风",
"fl": "<3级",
"type": "多云",
"notice": "阴晴之间,谨防紫外线侵扰"
}
],
"yesterday": {
"date": "06",
"high": "高温 16℃",
"low": "低温 10℃",
"ymd": "2020-03-06",
"week": "星期五",
"sunrise": "07:26",
"sunset": "19:06",
"aqi": 79,
"fx": "无持续风向",
"fl": "<3级",
"type": "多云",
"notice": "阴晴之间,谨防紫外线侵扰"
}
}
}

如果请求成功status=200,data的forecast字段是包括今天以及未来14天的天气情况,所以这个字段下的第二个元素就是明天的天气预报,判断该元素下的type字段是否包含“雨”,并返回调用结果,如果接口请求失败也给自己发送一条消息,及时处理

python3实现

每天定时跑脚本,给自己发钉钉消息,并结合明天的天气预报,如果要下雨给自己发送要早起的提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import json
import requests
import datetime
import time
def getWeather():
url = "http://t.weather.sojson.com/api/weather/city/{citycode}"
response = requests.get(url)
logWithTime("getWeather ---> " + response.text)
json_data = json.loads(response.text)
status = json_data.get('status')
if status == 200:
# 明天的天气信息
tomorrow = json_data.get('data').get('forecast')[1]

# 明天的天气
tomorrow_weather = str(tomorrow.get("type"))
if "雨" in tomorrow_weather:
return 1
else:
return 0
else:
return -1

def getDateInfo():
#获得今天的日期
today = datetime.date.today()
#获得明天的日期
tomorrow = today + datetime.timedelta(days=1)
url = "http://timor.tech/api/holiday/info/" + str(tomorrow)
response = requests.get(url)
logWithTime("getDateInfo ---> " + response.text)
json_data = json.loads(response.text)
dayType = json_data.get('type').get('type')
code = json_data.get('code')
holiday = json_data.get('holiday')

if code == 1:
logWithTime("getDateInfoError ---> ")
requestError(True)
else:
if(dayType == 0): #正常上班
messageWorkNormal(getWeather())
elif(dayType == 1 or dayType == 2): #普通周末和节假日
messageHoliday()
elif(dayType == 3): # 补假日
messageWorkAbnormal(today, str(holiday.get('name')),getWeather())



# 休息日
def messageHoliday():
logWithTime("messageHoliday ---> ")
messageRobot({
"msgtype": "text",
"text": {
"content": "【闹钟提醒】明天休息日,不上班哦~"
}
})

# 正常上班
def messageWorkNormal(rain):
if rain == -1:
logWithTime("getWeatherError ---> ")
requestError(False)
else:
str = ""
if rain == 1:
logWithTime("messageWorkNormal rain ---> ")
str = "可能下雨,需提前出门,"
logWithTime("messageWorkNormal ---> ")
messageRobot({
"msgtype": "text",
"text": {
"content": "【闹钟提醒】明天要上班,"+ str +"注意添加闹钟!!!"
}
})


# 接口请求出错处理
def requestError(date):
str = ""
if date:
str = "日期信息"
else:
str = "天气信息"

messageRobot({
"msgtype": "text",
"text": {
"content": "【闹钟提醒】"+str+"接口请求失败,注意添加闹钟!!!"
}
})

# 补假要上班
def messageWorkAbnormal(date, reason, rain):
if rain == -1:
logWithTime("getWeatherError ---> ")
requestError(False)
else:
str = ""
if rain == 1:
logWithTime("messageWorkAbnormal rain ---> ")
str = "可能下雨,需提前出门,"
logWithTime("messageWorkAbnormal ---> ")
messageRobot({
"msgtype": "text",
"text": {
"content": "【闹钟提醒】明天是:{},{} 要上班,".format(date,reason)+ str + "注意添加闹钟!!!"
}
})


# 钉钉机器人发送消息
def messageRobot(msg):
url = 'your webhook'

headers = {
'Content-Type': 'application/json'
}
requests.post(url, data=json.dumps(msg), headers=headers)

# 日志带时间利于排查
def logWithTime(msg):
localtime = time.asctime( time.localtime(time.time()) )
print (localtime + " " + msg)


if __name__ == "__main__":
getDateInfo()

每日晚上11点23在服务端运行这个脚本,请求钉钉机器人接口,给自己发送一个钉钉推送提醒,要定好闹钟

树莓派设置定时任务

将python脚本上传到树莓派,用ftp,ssh,samba都可以,这里就不详细说明了,设置定时任务前需要注意的是校准树莓派的时间,树莓派默认采用欧洲时区,如果树莓派的时间校准过,可略过此步骤。

校准树莓派时间

查看当前树莓派时间date
设置树莓派时区
sudo dpkg-reconfigure tzdata
选择亚洲时区Asia,选择上海时间Shanghai

contab设置定时任务

linux定时任务可利用contab设置,crontab -e,进入文件编辑,选择编辑工具,nano或vim
格式为:Minute Hour Day Month Dayofweek command
Minute 每个小时的第几分钟执行该任务
Hour 每天的第几个小时执行该任务
Day 每月的第几天执行该任务
Month 每年的第几个月执行该任务
DayOfWeek 每周的第几天执行该任务
Command 要执行的命令
设置23点22分执行python3的脚本,并输出日志,利于维护和问题排查
22 23 * * * python3 /home/pi/upload/test123.py >>/home/pi/mylog.log
保存文件,并重启contab服务
sudo service cron restart

ip变化提醒(2020/03/07更新)

家里申请了电信的公网ip,之前买了域名,一直在用端口转发+动态域名服务访问家里的树莓派,最近域名过期了,不打算续了,但希望仍然能在外网访问到家里的树莓派,电信的公网ip一直都是变动的,让电信固定ip需要繁杂的手续且需要企业申请,所以是不可能的了。最后想了一个可行的方法,如果能利用钉钉机器人在ip变化时给自己发送一条消息,告知最新的ip,那么仍然可以访问到家里的树莓派,这个问题就解决了。那如何知道ip变化了呢?还是利用linux的定时任务,每个小时跑一次python脚本,获取当前的外网ip,并将第一次的结果写入本地文件记录下来,每次运行脚本将当前外网ip与上一次记录的外网ip对比,如果不一致则ip变化,给自己发送钉钉消息告知最新的ip并再次写入文件刷新本地记录,如果ip一致则不用发送。

获取本机外网ip

找了一个免费的接口,http://members.3322.org/dyndns/getip,直接返回本机外网ip,如果为空就请求失败了,也给自己发送钉钉消息,及时处理,python实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import json
import requests
import os
import time
# 获取当前ip
def getip():
url = "http://members.3322.org/dyndns/getip"
response = requests.get(url)
logWithTime ("getip --->" + response.text)
myip = response.text.strip()
return myip

# 接口请求出错处理
def requestError():
messageRobot({
"msgtype": "text",
"text": {
"content": "【ip变更提醒】获取ip接口请求失败!!!"
}
})

def writeFile(ip):
fo = open("ip.txt", "w")
fo.write( ip)

def readFile():
fo = open("ip.txt", "r+")
return fo.read()

# 钉钉机器人发送消息
def messageRobot(msg):
url = 'your webhook'

headers = {
'Content-Type': 'application/json'
}
requests.post(url, data=json.dumps(msg), headers=headers)

# 开启ip变更通知任务
def notifyIpTask():
if os.path.exists("ip.txt"):
lastIp = readFile()
else:
lastIp = ""
logWithTime("notifyIpTask ---> notify ip server start")
myIp = getip()
if myIp == "":
logWithTime("notifyIpTask ---> get ip error")
requestError()
elif myIp == lastIp:
logWithTime("notifyIpTask ---> same ip")
else:
logWithTime("notifyIpTask ---> send ip")
writeFile(myIp)
messageRobot({
"msgtype": "text",
"text": {
"content": "【ip变更提醒】当前ip为{}".format(myIp)
}
})

# 日志带时间利于排查
def logWithTime(msg):
localtime = time.asctime( time.localtime(time.time()) )
print (localtime + " " + msg)

if __name__ == "__main__":
notifyIpTask()