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.
166 lines
5.7 KiB
166 lines
5.7 KiB
6 years ago
|
---
|
||
|
埋点之代理 View.OnClickListener
|
||
|
---
|
||
|
|
||
|
#### 目录
|
||
|
|
||
|
1. 思维导图
|
||
|
2. 原理
|
||
|
3. 实现
|
||
|
4. 优缺点
|
||
|
5. 参考
|
||
|
|
||
|
#### 思维导图
|
||
|
|
||
|
#### 原理
|
||
|
|
||
|
在注册的 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。
|
||
|
|
||
|
#### 实现
|
||
|
|
||
|
1. 首先创建代理类
|
||
|
|
||
|
```java
|
||
|
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: 后");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
2. 代理原 OnClickListener 事件
|
||
|
|
||
|
```java
|
||
|
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);
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
3. 在 Activity 完全显示的时候遍历 View 动态代理 OnClickListener
|
||
|
|
||
|
```java
|
||
|
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();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
#### 优缺点
|
||
|
|
||
|
优点:无
|
||
|
|
||
|
缺点:
|
||
|
|
||
|
1. 使用反射,对 APP 整体性能有影响,也可能会引入兼容性问题
|
||
|
2. 无法采集游离于 Activity 之上的 View 的点击,比如 Dialog、PopupWindow等等
|
||
|
3. OnGlobalLayoutListener API 16+
|
||
|
|
||
|
#### 参考
|
||
|
|
||
|
《神策数据 Android 全埋点技术白皮书》
|
||
|
|
||
|
[Android Hook 机制之简单实战](https://www.jianshu.com/p/c431ad21f071)
|