parent
7428e29dfc
commit
cc01b41a9b
@ -0,0 +1,166 @@ |
|||||||
|
--- |
||||||
|
埋点之代理 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) |
After Width: | Height: | Size: 135 KiB |
Loading…
Reference in new issue