0%

InspectUI Code Review

前言

一个类似 Android Studio Layout Inspector 的工具,可以非常方便的在手机上查看当前页面的 UI 信息,包括布局层级、View 树中各个 View 的属性。

UInspector

A UI inspector to traverse a view hierarchy on Android

Preview

带着问题去看源码

  • 怎么初始化的?
  • 为什么需要一个 Notification?怎么实现的?
  • 怎么拦截事件的?
  • 如何找到对应的 View 的?View 属性?
  • 如何将拦截的事件再次分发出去的?
  • 设计模式相关

初始化

Uinspector-optional-autoinstall

1
2
3
4
5
6
7
8
9
 @RestrictTo(RestrictTo.Scope.LIBRARY)
class UInspectorInstaller : ContentProvider() {

override fun onCreate(): Boolean {
UInspector.create(requireNotNull(context))
return true
}
//...
}
1
2
3
4
5
6
7
8
9
10
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.pitaya.mobile.uinspector.optional.autoinstall">

<application>
<provider
android:name="com.pitaya.mobile.uinspector.optional.UInspectorInstaller"
android:authorities="${applicationId}.uinspector.optional.installer"
android:exported="false" />
</application>
</manifest>

类似 LeakCanary, 使用 ContentProvider 的 Context 进行初始化。

缺点:不灵活,但是作为 debug 工具,完全可以忽略。

优点:无侵入式的依赖

Uinspector.create

1
2
3
4
5
6
7
8
fun create(context: Context) {
if (init.compareAndSet(false, true)) {
application = context.applicationContext as Application
installPlugins
lifecycle.register(application)
stop()
}
}

Notification

前台 Service

  • UInspectorNotificationService
    • setContentIntent
    • onStartCommand
    • UInspector.changeStateInner(pendingState)
    • 前后台

如何拦截事件

  • UInspector

    • changePanelState
    • UInspectorPanel
  • UInspectorDialogFragment

    • 拦截事件
    • UInspectorMask
  • UInspectorMask

    • dispatchKeyEvent 拦截返回键
    • onTouchEvent 拦截 down 事件
      • isSingleTap = true 时 dispatchCancelEvent
  • 如何找到 DecorView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
val global by lazy(LazyThreadSafetyMode.NONE) {
Class.forName("android.view.WindowManagerGlobal")
}

val windowManagerGlobal: Any by lazy(LazyThreadSafetyMode.NONE) {
global.getDeclaredMethod("getInstance").invoke(null)
}

val getWindowViews by lazy(LazyThreadSafetyMode.NONE) {
val f = global.getDeclaredField("mViews")
f.isAccessible = true
f
}

@Suppress("KDocUnresolvedReference")
fun findAllDecorViews(): List<View>? {
try {
@Suppress("UNCHECKED_CAST")
return getWindowViews.get(windowManagerGlobal) as List<View>
} catch (e: Throwable) {
Log.e(LibName, e.toString())
return null
}
}

如何找到对应的 View 的?View 属性?

  • 如何找到被点击的 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

protected val firstTouchTarget: Field by lazy(LazyThreadSafetyMode.NONE) {
val f = ViewGroup::class.java.getDeclaredField("mFirstTouchTarget")
f.isAccessible = true
f
}

protected val touchTargetChild: Field by lazy(LazyThreadSafetyMode.NONE) {
val cls = Class.forName("android.view.ViewGroup\$TouchTarget")
val f = cls.getDeclaredField("child")
f.isAccessible = true
f
}

/**
* 1. Try to get the [view]'s 'mFirstTouchTarget' field
* 2. If fail, use [findTouchTargetByEvent] instead
*/
protected open fun findFirstTouchTarget(view: View?, touchEvent: MotionEvent): View? {
if (view is ViewGroup) {
return try {
val touchTarget = firstTouchTarget.get(view)
if (touchTarget != null) {
touchTargetChild.get(touchTarget) as? View
} else {
findTouchTargetByEvent(view, touchEvent)
}
} catch (e: Throwable) {
Log.e(LibName, e.toString())
findTouchTargetByEvent(view, touchEvent)
}
}
return null
}
  • 面板
    -UInspectorPopupPanelContainerImpl
加个鸡腿呗.