埋点实战 - 动态代理实现点击事件埋点
1. 埋点方式
- 静态代理
- 通过编译期“织入”代码,或者修改代码(通常为修改字节码.class)。比如 AspectJ(AOP)、ASM、JavaSsist等均为此方式
- 动态代理
- 运行时进行代理。例如代理:View.OnClickListener、Window.Callback、View.AccesbilityDelegate等均为此方式。虽然叫做动态代理,但不是真的Proxy代理,而是动态地进行代码增强。
静态代理在性能方面明显优于动态代理,静态代理在编译器织入代码,或者修改代码,对运行时的程序不会带来太大的性能影响,而动态代理在程序运行阶段发生,可能需要反射,对程序性能有一定的影响。
2. 动态代理埋点思路
一般思路有两种:
- 代理 View.OnClickListener
- 监听触摸事件
- 监听辅助功能回调
2.1 代理 View.OnClickListener
我们不希望在原有代码上做修改,我们就需要做功能增强,功能增强除了可以通过代理设计模式,还可以通过装饰器模式。我们的任务:
- 拿到 Activity 当前所有的 View
- 判断 View 是否设置了 View.OnClickListener
- 反射将其替换为代理类(装饰类)进行功能增强
除了在 OnResume 生命周期来到的时候进行上述遍历替换行为,在 OnResume 后动态 addView 的时候也要进行上述替换行为。对于后来动态添加View的情况,我们可以采用 ViewTreeObserer.OnGlobalLayoutListener 来监听View树的动态变化回调,从而重新进行上述遍历替换行为。
缺点:
- 除了反射性能问题、遍历耗时问题,还有兼容性问题
- Application.ActivityLifecycleCallbacks 要求 API 14+
也可以通过hook Instrumentation来做生命周期监听
- View.hasOnClickListeners() 要求 API 15+
- removeOnGlobalLayoutListener 要求 API 16+
- 无法采集其他Window的点击事件,例如 Dialog、PopupWindow等
2.2 监听触摸事件
我们可以通过代理 Window.Callback 增强其 dispatchTouchEvent 方法,或者通过增加一个顶级透明ViewGroup,增强其 dispatchTouchEvent 方法,目的都是为了获取到用户点击的位置,然后通过遍历View,找到点击的View是哪个。
2.3 辅助功能点击回调
View的点击事件 performClick() 内部会调用 sendAccessibilityEvent(AccessbilityEvent.TYPE_VIEW_CLICKED),里面调用了 mAccessbilityDelegate 对象的 sendAccessibilityEvent 方法,并传入了 View 对象。我们可以通过装饰器模式来增强 AccessibilityDelegate 对象。这里就不需要反射了,View本身就提供了 getter setter 该对象的方法。
缺点:
- 兼容问题:API:29+
- 只能在点击事件完成之后进行捕获。
优点:
- 更少的反射
3. 简单实现
Activity结构如下,Activity的布局为LinearLayout为父布局,里面只放了一个Button按钮:
public class MainActivity extends AppCompatActivity {
Button btn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn = findViewById(R.id.btn_click);
btn.setOnClickListener(v->{
Toast.makeText(this, "hello", Toast.LENGTH_SHORT).show();
Log.e(TAG,"btn clicked");
});
}
}
Application 基础结构:
- 重写Application,在其中监听Activity的onResume生命周期
- 遍历所有View
- 点击事件埋点
public class MyApplication extends Application {
private static final String TAG = "MyApplicationTAG";
@Override
public void onCreate() {
super.onCreate();
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
//...
@Override
public void onActivityResumed(@NonNull Activity activity) {
//发起点击事件监听(埋点)
listenToClick(activity);
}
});
}
private void listenToClick(@NonNull Activity activity) {
//1. 拿到window
Window window = activity.getWindow();//PhoneWindow
//2.拿到根View
View decor = window.getDecorView();//根View(除了ViewRootImpl)
//3.层序遍历 空间复杂度为最大宽度
Queue<View> queue = new LinkedList<>();
queue.add(decor);
while(!queue.isEmpty()){
int n = queue.size();
for(int i = 0; i< n;i++){
View poll = queue.poll();
//埋点
if (poll instanceof ViewGroup) {
for (int j = 0; j < ((ViewGroup) poll).getChildCount(); j++) {
View child = ((ViewGroup) poll).getChildAt(j);
queue.add(child);
}
}
}
}
}
}
搭建上述结构的时候,可能会有一些问题:
- 为什么不用反射获取 ViewGroup 中的 mChildren?
答:Android对反射Field增加了限制,一般反射只能反射到ViewGroup的 public 对象。但对方法的反射则暂时没用这样的限制。
3.1 代理 OnClickListener
基础结构中的 【埋点】 部分,实现为 insertWrapperOnClickListener(View view)
//...
View poll = queue.poll();
//代理 OnCLickLisenter
insertWrapperOnClickListener(poll);
if(poll instanceof ViewGroup){...}
//...
首先需要实现一个装饰类WrapperOnClickListener
来做功能增强:
//Wrapper包装类,功能增强
private static class WrapperOnClickListener implements View.OnClickListener {
//装饰对象
View.OnClickListener upStream;
public WrapperOnClickListener(View.OnClickListener upStream) {
this.upStream = upStream;
}
//功能增强
@Override
public void onClick(View v) {
//pre
Log.e(TAG, "before click");
//打印view信息(可以用于存储埋点)
Log.e(TAG,"view info :"+createViewInfo(v));
//perform click
upStream.onClick(v);
//after
Log.e(TAG, "after click");
}
//打印控件的唯一标识信息
//parent1[index]#id/parent2[index]#id/view[index]#id
private String createViewInfo(View v) {
Deque<String> path = new LinkedList<>();
View cur = v;
StringBuilder info;
while ((cur)!=null){
info = new StringBuilder();
info.append(cur.getClass().getSimpleName());
info.append("[");
if (cur.getParent()!=null && cur.getParent() instanceof ViewGroup){
info.append(((ViewGroup) cur.getParent()).indexOfChild((View)cur));
}else{
info.append(0);
}
info.append("]");
if (((View) cur).getId()!=-1) {
//如果有id
info.append("#");
info.append(((View) cur).getId());
}
path.add(info.toString());
if (!(cur.getParent() instanceof View))break;
cur = (View)cur.getParent();
}
//拼接
StringBuilder result = new StringBuilder();
result.append("path: ");
while (!path.isEmpty()){
result.append(path.pollLast());
if (!path.isEmpty()){
result.append("/");
}
}
return result.toString();
}
}
有了功能增强的装饰类,接下来就是注入了,我们通过反射拿到 View的ListenerInfo的mOnClickListener对象,将其替换为我们的装饰类:
private void insertWrapperOnClickListener(View view) {
if (view.hasOnClickListeners()) {
//如果有主动设置点击监听器
//反射拿到原来的mOnClickListener
Class<?> v = View.class;
try {
//注意这里要用 getDeclaredMethod 而不是 getMethod
Method getListenerInfo = v.getDeclaredMethod("getListenerInfo");
getListenerInfo.setAccessible(true);
Object li = getListenerInfo.invoke(view);
//下列注释代码运行失败,虽然反射实例性能更快,但是android有反射限制,只能通过反射方法来获取 ListenerInfo 对象
// Field mListenerInfo = v.getDeclaredField("mListenerInfo");
//mListenerInfo.setAccessible(true);
// Object li = mListenerInfo.get(v);
if (li != null) {
//拿到li中的mOnClickListener
Class<?> liClass = li.getClass();
//这是个public的域,是允许直接反射获取的
Field clickListenerField = liClass.getDeclaredField("mOnClickListener");
clickListenerField.setAccessible(true);
View.OnClickListener o = (View.OnClickListener) clickListenerField.get(li);
//之前已经判空过了
//注入装饰类
Log.e(TAG, "get one click listener : " + o);
clickListenerField.set(li, new WrapperOnClickListener(o));
o = (View.OnClickListener) clickListenerField.get(li);
Log.e(TAG, "get one click listener : " + o);
}
} catch (IllegalAccessException | NoSuchFieldException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
Log.e(TAG, "error : " + e);
}
}
}
运行结果:
2023-03-18 16:39:59.611 21646-21646/? E/MyApplicationTAG: get one click listener : MainActivity$$ExternalSyntheticLambda0@fea8936
2023-03-18 16:39:59.611 21646-21646/? E/MyApplicationTAG: get one click listener : MyApplication$WrapperOnClickListener@26f1037
//点击按钮之后:
2023-03-18 16:40:05.041 21646-21646/? E/MyApplicationTAG: before click
2023-03-18 16:40:05.041 21646-21646/? E/MyApplicationTAG: view info :path: DecorView[0]/LinearLayout[0]/FrameLayout[1]/ActionBarOverlayLayout[0]#2131230845/ContentFrameLayout[0]#16908290/LinearLayout[0]/MaterialButton[0]#2131230808
2023-03-18 16:40:05.055 21646-21646/? E/MyApplicationTAG: btn clicked
2023-03-18 16:40:05.055 21646-21646/? E/MyApplicationTAG: after click
可以看到代理成功。在替换 OnClickListener 前,点击监听器为匿名内部类,替换完成后,点击监听器变为了我们的装饰器类。点击按钮之后,除了执行按钮本身监听器内容之外,还执行了我们增强的功能。也就是埋点的功能。
3.2 辅助功能回调
基础结构中的 【埋点】 部分,实现为替换 AccessibilityDelegate:
private void insertWrapperAccessibilityDelegate(View view){
//插入装饰类
if (view.getAccessibilityDelegate()==null){
view.setAccessibilityDelegate(new WrapperAccessibilityDelegate(null));
}else{
view.setAccessibilityDelegate(new WrapperAccessibilityDelegate(view.getAccessibilityDelegate()));
}
}
//包装类
private static class WrapperAccessibilityDelegate extends View.AccessibilityDelegate{
//包装对象
View.AccessibilityDelegate upstream;
public WrapperAccessibilityDelegate(View.AccessibilityDelegate upstrea) {
this.upstream = upstrea;
}
//功能增强
@Override
public void sendAccessibilityEvent(View host, int eventType) {
//pre 监听事件
Log.e(TAG,"begin sendAccessibilityEvent");
Log.e(TAG,"view info :"+createViewInfo(host));
//调用装饰对象的方法
if (upstream!=null)upstream.sendAccessibilityEvent(host,eventType);
else super.sendAccessibilityEvent(host,eventType);
//post 监听事件
Log.e(TAG,"end sendAccessibilityEvent");
}
//打印控件的唯一标识信息
//parent1[index]#id/parent2[index]#id/view[index]#id
private String createViewInfo(View v) {
Deque<String> path = new LinkedList<>();
View cur = v;
StringBuilder info;
while ((cur)!=null){
info = new StringBuilder();
info.append(cur.getClass().getSimpleName());
info.append("[");
if (cur.getParent()!=null && cur.getParent() instanceof ViewGroup){
info.append(((ViewGroup) cur.getParent()).indexOfChild((View)cur));
}else{
info.append(0);
}
info.append("]");
if (((View) cur).getId()!=-1) {
//如果有id
info.append("#");
info.append(((View) cur).getId());
}
path.add(info.toString());
if (!(cur.getParent() instanceof View))break;
cur = (View)cur.getParent();
}
//拼接
StringBuilder result = new StringBuilder();
result.append("path: ");
while (!path.isEmpty()){
result.append(path.pollLast());
if (!path.isEmpty()){
result.append("/");
}
}
return result.toString();
}
}
运行效果:
2023-03-18 16:42:32.510 21856-21856/? E/MyApplicationTAG: btn clicked
2023-03-18 16:42:32.510 21856-21856/? E/MyApplicationTAG: begin sendAccessibilityEvent
2023-03-18 16:42:32.511 21856-21856/? E/MyApplicationTAG: view info :path: DecorView[0]/LinearLayout[0]/FrameLayout[1]/ActionBarOverlayLayout[0]#2131230845/ContentFrameLayout[0]#16908290/LinearLayout[0]/MaterialButton[0]#2131230808
2023-03-18 16:42:32.511 21856-21856/? E/MyApplicationTAG: end sendAccessibilityEvent
可以看到代理成功。按钮执行完点击事件后,回调到了 mAccessibilityDeletage的sendAccessibilityEvent方法。
4. 相关源码
4.1 无法直接反射 Field 获取 mListenerInfo 变量
在View.java中,这个变量定义如下,这是一个 private 修饰的变量
//View.java
ListenerInfo mListenerInfo;
而 Android 的 Class.java 的反射方法被修改了,只能获取 public 修饰的变量:
//Class.java in Android
// Android-changed: Removed SecurityException.
public native Field getDeclaredField(String name) throws NoSuchFieldException;
但是可以通过反射 getListenerInfo() 方法来获得 mListenerInfo 变量
public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException {
return getMethod(name, parameterTypes, false);
}
可以直接通过反射 Field 来获取 mOnClickListener,因为它的访问权限是 public
//View.java
static class ListenerInfo {
//未来可能会变成 private
public OnClickListener mOnClickListener;
}
4.2 View的点击事件会回调到辅助功能
View如果消费了点击事件,最后回来到 performClick() 方法:
public boolean performClick() {
notifyAutofillManagerOnClick();
//如果点击监听器有设置的话,回调监听器
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
//返回值表示是否被点击监听器处理
result = true;
} else {
//没有点击监听器可以处理
result = false;
}
//回调到辅助功能
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
//如果有 mAccessibilityDelegate的话,会回调其 sendAccessibilityEvent 方法
public void sendAccessibilityEvent(int eventType) {
if (mAccessibilityDelegate != null) {
mAccessibilityDelegate.sendAccessibilityEvent(this, eventType);
} else {
sendAccessibilityEventInternal(eventType);
}
}