hook + aop 簡單實(shí)現(xiàn)
1. View的層級

1639734894(1).jpg

1.jpg
View 所有的Listener都存儲在getListenerInfo()=ListenerInfo
所有hook getListenerInfo()拿到mOnClickListener代理到我們自己的WrapperOnClickListener里
1.hook setOnClickListener
fun hookViewOnClickListener(view: View) {
val listener = getOnClickListener(view)
if(listener !is WrapperViewOnClickListener){
view.setOnClickListener(WrapperViewOnClickListener(listener))
}
}
2.反射拿到getListenerInfo,通過ListenerInfo拿到mOnClickListener
private fun getOnClickListener(view: View): View.OnClickListener? {
val hasOnClick = view.hasOnClickListeners()
if (hasOnClick) {
try {
val viewClazz: Class<*> = View::class.java
val method: Method = viewClazz.getDeclaredMethod("getListenerInfo")
if (!method.isAccessible) {
method.isAccessible = true
}
val listenerInfoObj: Any = method.invoke(view)
val listenerInfoClazz = Class.forName("android.view.View\$ListenerInfo")
val field: Field = listenerInfoClazz.getDeclaredField("mOnClickListener")
if (!field.isAccessible) {
field.isAccessible = true
}
return field.get(listenerInfoObj) as View.OnClickListener
} catch (e: Exception) {
e.printStackTrace()
}
}
return null
}
}
3.WrapperViewOnClickListener
class WrapperViewOnClickListener(val listener: View.OnClickListener?) : View.OnClickListener {
override fun onClick(v: View?) {
Log.e("TAG","自動埋點(diǎn)")
listener?.onClick(v)
val path = ViewPath.getPath(v, StringBuilder())
Log.e("TAG", path)
}
}
獲取當(dāng)前View所在Activity的路徑
注意RecyclerView 子item的位置索引
fun getPath(view: View?, path: StringBuilder): String? {
if (view != null) {
if (path.isEmpty()) path.insert(0, view.javaClass.simpleName)
} else {
return ""
}
if (view.parent is ViewGroup) {
if (view.parent is RecyclerView || view.parent is AdapterView<*>) {
val listView: View = view.parent as View
var index = 0
if (view.parent is RecyclerView) {
index = (view.parent as RecyclerView).getChildAdapterPosition(view)
} else if (view.parent is AdapterView<*>) {
index = (view.parent as AdapterView<*>).getPositionForView(view)
}
path.insert(0, view.parent.javaClass.simpleName + "[" + index + "]|")
} else {
path.insert(0, view.parent.javaClass.simpleName + "[" + (view.parent as ViewGroup).indexOfChild(view) + "]|")
}
}
//遇到PhoneWindow退出遞歸
return if (view.parent is View) {
//不統(tǒng)計(jì)統(tǒng)計(jì)DecorView遇到Decorview退出遞歸
if ((view.parent as View).id === android.R.id.content) {
path.insert(0, view.parent.javaClass.simpleName + "|")
path.toString()
} else {
getPath(view.parent as View, path)
}
} else {
path.toString()
}
}
效果:
TAG: 自動埋點(diǎn)
TAG: DecorView[0]|LinearLayout[1]|FrameLayout[0]|FitWindowsLinearLayout[1]|ContentFrameLayout[0]|LinearLayout[4]|FrameLayout[0]|FrameLayout[0]|FrameLayout[0]|FrameLayout[0]|FrameLayout[0]|FrameLayout[0]|FrameLayout[0]|AppCompatButton
or
ContentFrameLayout|ContentFrameLayout[0]|LinearLayout[4]|AppCompatButton
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<Button
android:id="@+id/get"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="埋點(diǎn)" />
</FrameLayout>
</FrameLayout>
</FrameLayout>
</FrameLayout>
</FrameLayout>
</FrameLayout>
</FrameLayout>
</LinearLayout>