You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
5.7 KiB
5.7 KiB
埋点之代理 View.OnClickListener
目录
- 思维导图
- 原理
- 实现
- 优缺点
- 参考
思维导图
原理
在注册的 Application.ActivityLifecycleCallbacks 的 onActivityResumed(Activity activity) 回调方法中,通过 activity.getWindow().getDecorView 获取 DecorView,然后遍历 DecorView,判断当前 View 是否设置了 onClickListener,如果已设置并且不是我们的 OnClickListenerWrapper,就用自定义的 OnClickListenerWrapper 替代当前 View.onClickListener。WrapperOnClickListener 的 onClick 里会先调用原有的 OnClickListener 处理逻辑,然后再调用埋点代码,从而达到自动埋点的效果。
之所以选择 DecorView 而不是 ContentView(Activity#setContentView),主要是考虑到了 MenuItem 的点击事件。
但是,这样并不能拦截到动态添加的 View,所以还需要引入 ViewTreeObserver.OnGlobalLayoutListener 来监听视图树变化,而且要在 Activity#onStop 中 removeOnGlobalLayoutListener。
实现
-
首先创建代理类
public class ClickListenerWrapper implements View.OnClickListener { private static final String TAG = "HookedClickListenerProx"; private View.OnClickListener origin; public ClickListenerWrapper(View.OnClickListener origin) { this.origin = origin; } @Override public void onClick(View v) { if (origin != null) { Log.i(TAG, "onClick: 前"); origin.onClick(v); Log.i(TAG, "onClick: 后"); } } }
-
代理原 OnClickListener 事件
public class HookOnClickHelper { private static final String TAG = "HookOnClickHelper"; public static void hook(View view) throws Exception { //第一步:反射得到 ListenerInfo 对象 Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo"); getListenerInfo.setAccessible(true); Object listenerInfo = getListenerInfo.invoke(view); //第二步:得到原始的 OnClickListener 事件方法 Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo"); Field mOnClickListener = listenerInfoClz.getDeclaredField("mOnClickListener"); mOnClickListener.setAccessible(true); View.OnClickListener originOnClickListener = (View.OnClickListener) mOnClickListener.get(listenerInfo); if (originOnClickListener == null || originOnClickListener instanceof ClickListenerWrapper) { //如果没有设置点击事件或者已经代理过了,则跳过 return; } //第三步:替换 ClickListenerWrapper proxy = new ClickListenerWrapper(originOnClickListener); mOnClickListener.set(listenerInfo, proxy); } }
-
在 Activity 完全显示的时候遍历 View 动态代理 OnClickListener
public class MyApplication extends Application { private static final String TAG = "MyApplication"; @Override public void onCreate() { super.onCreate(); this.registerActivityLifecycleCallbacks(new MyActivityLifecycleCallback()); } static class MyActivityLifecycleCallback implements ActivityLifecycleCallbacks { private View mDecorView; private ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener=new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { setAllViewsProxy((ViewGroup) mDecorView); } }; @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { } @Override public void onActivityStarted(Activity activity) { } @Override public void onActivityResumed(Activity activity) { mDecorView = activity.getWindow().getDecorView(); setAllViewsProxy((ViewGroup) mDecorView); } @Override public void onActivityPaused(Activity activity) { } @Override public void onActivityStopped(Activity activity) { mDecorView.getViewTreeObserver().removeGlobalOnLayoutListener(mOnGlobalLayoutListener); } @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { } @Override public void onActivityDestroyed(Activity activity) { } private void setAllViewsProxy(ViewGroup viewGroup) { int childCount = viewGroup.getChildCount(); for (int i = 0; i < childCount; i++) { View view = viewGroup.getChildAt(i); if (view instanceof ViewGroup) { setAllViewsProxy((ViewGroup) view); } else { try { if (view.hasOnClickListeners()) { HookOnClickHelper.hook(view); } } catch (Exception e) { e.printStackTrace(); } } } } } }
优缺点
优点:无
缺点:
- 使用反射,对 APP 整体性能有影响,也可能会引入兼容性问题
- 无法采集游离于 Activity 之上的 View 的点击,比如 Dialog、PopupWindow等等
- OnGlobalLayoutListener API 16+
参考
《神策数据 Android 全埋点技术白皮书》