全链路无痕埋点作为一个明确的需求,目前已经有较多的实现方案
本人认为比较好的是Hook+Aop方案
hook是利用view
的AccessibilityDelegate
接口做代理, RecyclerView
的mScrollListeners
对象替换等等
Aop可以用AspectJ
实现
本文是提供一个较为轻量级的做法,实现对所有view的touch
事件监控
首先我们来复习下相关知识点:
1. View的层级
首先我们来了解下View
的层级:
引用一张网图:
Window
的根View
是DecorView
,通过iewRoot
管理,此处不作扩展
DecorView
是个ViewGroup
,我们目标是监听DecorView
的touch
事件
那么如何监听:
2. 事件分发
view
和子view
的事件分发可以用伪代码表示:
fun dispatchTouchEvent(ev: MotionEvent) {
var consume = false
if(onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev)
} else {
consume = child.dispatchTouchEvent(ev)
}
return consume
}
image
ViewGroup
中的dispatchTouchEvent
会分发到onInterceptTouchEvent
中, 在onInterceptTouchEvent
中就可以记录日志,并且return super.onInterceptTouchEvent(event)
不会影响子view
的调用
监听方案:
DecorView
我们无法改写,我们可以用一个ProxyView
插入在DecorView
之后,只去监听onInterceptTouchEvent
将第一张图中
Activity->PhoneWindow->DecorView->titleBar+mContentParent
变为
Activity->PhoneWindow->DecorView->ProxyView->titleBar+mContentParent
代理方案:
使用插装思想来插入ProxyView
,利用ActivityLifecycleCallbacks
的onActivityResumed
方法,在每个Activity resume
之后,插入ProxyView
:
首先在入口启动时注册一个ActivityLifecycleCallbacks
:
object HookTrack {
private var activityLifeCycleRegister = false
fun init(application: Application?) {
if (application == null) {
Log.e("e", "Please init with the param \"Application\"/")
throw RuntimeException()
}
if (!activityLifeCycleRegister) {
application.registerActivityLifecycleCallbacks(HookActivityLifecycleCallbacks())
activityLifeCycleRegister = true
}
}
}
HookActivityLifecycleCallbacks的实现:
internal class HookActivityLifecycleCallbacks : ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
override fun onActivityStarted(activity: Activity) {}
override fun onActivityResumed(activity: Activity) {
if (!activityNameSet.contains(activity.javaClass.name)) {
val viewGroup = activity.window.decorView as ViewGroup
if (viewGroup != null) {
val size = viewGroup.childCount
val customFrameLayout = ProxyFrameLayout(activity)
for (i in 0 until size) {
val view = viewGroup.getChildAt(i)
if (view != null) {
viewGroup.removeView(view)
customFrameLayout.addView(view)
}
}
viewGroup.addView(customFrameLayout)
}
activityNameSet.add(activity.javaClass.name)
}
}
override fun onActivityPaused(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {}
companion object {
var activityNameSet: MutableSet<String> = HashSet()
}
}
遍历decorView
对象,将其子view全部放到ProxyFrameLayout
,再将ProxyFrameLayout
加到decorView
,形成代理
此处我们用activityNameSet
防止重复插入ProxyFrameLayout
在用ProxyFrameLayout用
中实现上述用onInterceptTouchEvent用
:
class ProxyFrameLayout(private val resumedActivity: Activity) : FrameLayout(resumedActivity) {
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
//ACTION_DOWN do some thing
}
MotionEvent.ACTION_MOVE -> {
//ACTION_MOVE do some thing
}
MotionEvent.ACTION_UP -> {
//ACTION_UP do some thing
}
}
return super.onInterceptTouchEvent(event)
}
}
现在实现了“根”view的事件监听,我们如何来获知具体是哪个view呢:
可以通过屏幕上的像素点,遍历view,计算当前像素点在哪个view:
private fun findEventSrcView(event: MotionEvent, srcView: View): View? {
if (srcView is ViewGroup) {
val viewGroup = srcView
val size = viewGroup.childCount
for (i in 0 until size) {
val view = viewGroup.getChildAt(i)
if (view !is ProxyFrameLayout && isEventInView(event, view)) {
val tmpRetView = findEventSrcView(event, view)
if (tmpRetView != null) {
return tmpRetView
}
}
}
} else if (isEventInView(event, srcView)) {
return srcView
}
return null
}
/**
* 判断是否在view的rect范围内
* @param event
* @param srcView
* @return
*/
private fun isEventInView(event: MotionEvent, srcView: View): Boolean {
val currentViewRect = Rect()
if (srcView.getGlobalVisibleRect(currentViewRect)) {
val rectF = RectF(currentViewRect)
if (rectF.contains(event.rawX, event.rawY)) {
return true
}
}
return false
}
所以我们的ProxyFrameLayout
中的nInterceptTouchEvent
就变成了:
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
val touchViewDown = findEventSrcView(event, this)
if (touchViewDown != null) {
Log.d(Constants.TAG, "Activity:" + resumedActivity::class.java.name
+ "- ACTION_DOWN:" + touchViewDown::class.java.name)
}
}
MotionEvent.ACTION_MOVE -> {
}
MotionEvent.ACTION_UP -> {
val touchViewUp = findEventSrcView(event, this)
if (touchViewUp != null) {
Log.d(Constants.TAG, "Activity:" + resumedActivity::class.java.name
+ "- ACTION_UP:" + touchViewDown::class.java.name)
}
}
}
return super.onInterceptTouchEvent(event)
}
至此,每个子view的touch事件就监听到了
另外:由于子view重名的会很多,如果直接打印view的名字无法区分
我们需要打印整个view的全链路才有意义:
在此我们遍历view,通过view.parent方法一路寻找到根节点才结束:
fun getAbsolutePath(view: View?): String {
if (view == null) {
return ""
}
if (view.parent == null) {
return "rootView"
}
var path = "";
var temp = view!!
while (temp.parent != null && temp.parent is View) {
var index = 0
try {
index = indexOfChild(temp.parent as ViewGroup, temp)
} catch (e: Exception) {
}
path = "${temp.javaClass.simpleName}[$index]/${path}"
temp = temp.parent as View
}
return path
}
private fun indexOfChild(parent: ViewGroup?, child: View): Int {
if (parent == null) {
return 0
}
val count = parent.childCount
var j = 0
for (i in 0 until count) {
val view = parent.getChildAt(i)
if (child.javaClass.isInstance(view)) {
if (view === child) {
return j
}
j++
}
}
return -1
}
运行后打印的效果:
点击一个button:
robin.scaffold.jet D/Track: Activity:robin.scaffold.jet.MainActivity- ACTION_DOWN:ProxyFrameLayout[0]/LinearLayout[0]/FrameLayout[0]/FitWindowsLinearLayout[0]/ContentFrameLayout[0]/DrawerLayout[0]/CoordinatorLayout[0]/ConstraintLayout[0]/FragmentContainerView[0]/ConstraintLayout[0]/AppCompatButton[0]/
robin.scaffold.jet D/Track: Activity:robin.scaffold.jet.MainActivity- ACTION_UP:ProxyFrameLayout[0]/LinearLayout[0]/FrameLayout[0]/FitWindowsLinearLayout[0]/ContentFrameLayout[0]/DrawerLayout[0]/CoordinatorLayout[0]/ConstraintLayout[0]/FragmentContainerView[0]/ConstraintLayout[0]/AppCompatButton[0]/
点击RecycleView
中的item
:
robin.scaffold.jet D/Track: Activity:robin.scaffold.jet.ui.NavTestActivity- ACTION_DOWN:ProxyFrameLayout[0]/LinearLayout[0]/FrameLayout[0]/ActionBarOverlayLayout[0]/ContentFrameLayout[0]/ConstraintLayout[0]/FragmentContainerView[0]/ConstraintLayout[0]/SwipeRefreshLayout[0]/LinearLayout[0]/RecyclerView[0]/ConstraintLayout[2]/AppCompatTextView[0]/
robin.scaffold.jet D/Track: Activity:robin.scaffold.jet.ui.NavTestActivity- ACTION_UP:ProxyFrameLayout[0]/LinearLayout[0]/FrameLayout[0]/ActionBarOverlayLayout[0]/ContentFrameLayout[0]/ConstraintLayout[0]/FragmentContainerView[0]/ConstraintLayout[0]/SwipeRefreshLayout[0]/LinearLayout[0]/RecyclerView[0]/ConstraintLayout[2]/AppCompatTextView[0]/
ConstraintLayout[2]
代表的是点中第三个item
至此,一个最简单的无痕touch事件监听实现完成
此处是监听了所有view
,当然也可以不通过onInterceptTouchEvent
,而是实现AccessibilityDelegate
,来监听实现了Click
事件的view
读者可以自行实验
网友评论