Android埋点技术分析

移动开发 Android
埋点,是对网站、App或者后台等应用程序进行数据采集的一种方法。通过埋点,可以收集用户在应用中的产生行为,进而用于分析和优化产品后续的体验,也可以为产品的运营提供数据支撑,其中常见的指标有PV、UV、页面时长和按钮的点击等。

一、埋点,是对网站、App或者后台等应用程序进行数据采集的一种方法。通过埋点,可以收集用户在应用中的产生行为,进而用于分析和优化产品后续的体验,也可以为产品的运营提供数据支撑,其中常见的指标有PV、UV、页面时长和按钮的点击等。

Android埋点技术分析

采集行为数据时,通常需要在Web页面/App里面添加一些代码,当用户的行为达到某种条件时,就会向服务器上报用户的行为。其实添加这些代码的过程就可以叫做“埋点”,在很久以前就已经出现了这种技术。随着技术的发展和大家对数据采集要求的不断提高,我认为埋点的技术方案走过了下面几个阶段:

代码埋点:代码埋点是指开发人员按照产品/运营的需求,在Web页面/App的源码里面添加行为上报的代码,当用户的行为满足某一个条件时,这些代码就会被执行,向服务器上报行为数据。这种方案是最基础的方案,每次增加或者修改数据上报的条件,都需要开发人员的参与,并且只能在下一个版本上线后才能看到效果。很多公司都提供了这类数据上报的SDK,将行为上报的后台服务器接口封装成了简单的客户端SDK接口。开发者可以通过嵌入这类SDK,在埋点的地方调用少量的代码就可以上报行为数据。

全埋点:全埋点指的是将Web页面/App内产生的所有的、满足某个条件的行为,全部上报到后台服务器。例如把App中所有的按钮点击都进行上报,然后由产品/运营去后台筛选所需要的行为数据。这种方案的优点非常明显,就是可以不用在新增/修改行为上报条件时,再找开发人员去修改埋点的代码。然而它的缺点也和优点一样明显,那就是上报的数据量比代码埋点大很多,里面可能很多是没有价值的数据。此外,这种方案更倾向于独立去看待用户的行为,而没有关注行为的上下文,给数据分析带来了一些难度。很多公司也提供了这类功能的SDK,通过静态或者动态的方式,“Hook”了原有的App代码,从而实现了行为的监测,在数据上报时通常是采用累积多条再上报的方案来合并请求。

hook直译是钩子的意思,以前学信息安全的时候在windows上听到过,大体意思是通过某种手段去改变系统API的一个行为,绕过系统的某个方法,或者改变系统的工作流程。在这里其实是指把本来要执行某个方法的对象替换成另一个,一般用的是反射或者代理,需要找到hook的代码位置,甚至还可以在编译阶段实现替换。

可视化埋点: 可视化埋点是指产品/运营在Web页面/App的界面上进行圈选,配置需要监测界面上哪一个元素,然后保存这个配置,当App启动时会从后台服务器获得产品/运营预先圈选好的配置,然后根据这份配置监测App界面上的元素,当某一个元素满足条件时,就会上报行为数据到后台服务器。有了全埋点技术方案,从体验优化的角度很容易想到按需埋点,可视化埋点就是一种按需配置埋点的方案。现在也有一些公司提供了这类SDK,圈选监测元素时,一般都是提供一个Web管理界面,手机在安装并初始化了SDK之后,可以和管理界面了连接,让用户在Web管理界面上配置需要监测的元素。

业界有多家SDK都支持上面介绍的3种埋点方案中的一种或者全部,例如Mixpanel、Sensorsdata、TalkingData、GrowingIO、诸葛IO、Heap Analytics、MTA、Umeng Analytics、百度,只是大家对后两种埋点的称呼不完全相同,有的叫无埋点或者codeless埋点。由于 Mixpanel (支持代码埋点、可视化埋点)和 Sensorsdata (全部支持)都开源了自己的全部SDK,技术方案也比较类似,下面以它们的Android SDK为例,简单分析一下3种埋点方案的技术实现。关于JS的SDK技术实现,可以看下我的另一篇博客-JS埋点SDK技术分析。

二、代码埋点

包含Mixpanel SDK在内的大部分SDK,都会把这种埋点方案封装成一个比较简单的接口,在这里是 track(String eventName, JSONObject properties) ,开发者在调用这个接口时,可以把一个事件名称和事件的属性传入,然后就可以上报到后台了。

在实现上,Mixpanel SDK默认采用一条HandlerThread线程来处理事件,当开发者调用 track(String eventName, JSONObject properties) 方法时, 主线程切换到HandlerThread 当中,并先将事件存入数据库。然后看SDK中是否累计到了40个事件,如果累计到40个事件的话,就合并它们上报到后台。

当开发者设置为debug模式,或者手动调用 flush 接口时,可以立即上报累计的所有事件,不过由于只有一条线程,所以如果在flush的时候,前面的事件还没有处理完成,SDK会间隔1分钟再次去处理后面的这些事件。

开发者可以设置累计上报的事件数量阈值、事件阻塞时再次尝试上报的时间间隔等。这种方案比较基础,相信大部分开发者都接触过,不需要过多分析。

三、全埋点

3.1 AOP基础

Mixpanel现在的Android SDK没有提供这个功能,但是神策Android SDK提供了,实现方式是依赖AOP。那么什么是AOP?

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。(from baidu baike)

简而言之,AOP是可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。

Sensors Analytics AndroidSDK全埋点的实现就是通过在代码编译阶段,找到源代码中需要上报事件的位置,插入SDK的事件上报代码。它用到的框架是 AspectJ 。

说到这里,必须简单了解一下AspectJ以及它里面的一些概念.它是AOP的领跑者,在很多地方我们可以看到它的身影,例如JakeWartson大神贡献的一个注解日志和性能调优框架 Hugo ,在Spring框架里面也大量应用到AspectJ。我理解AspectJ里面的主要几个概念有:

  • JPoint: 代码切点(就是我们要插入代码的地方)
  • Aspect: 代码切点的描述
  • Pointcut: 描述切点具体是什么样的点,如函数被调用的地方( Call(MethodSignature) )、函数执行的内部( execution(MethodSignature) )
  • Advice: 描述在切点的什么位置插入代码,如在Pointcut前面( @Before )还是后面( @After ),还是环绕整个Pointcut( @Around )

由此可见,在实现AOP功能时,需要做下面几件事:

  • 定义一个Aspect,这个Aspect里面必须有Pointcut和Advice两个属性
  • 编写在匹配到符合Pointcut和Advice描述的代码时,需要注入的代码
  • 在代码编译时,通过特殊的java编译器(Aspect的ajc编译器),找到符合我们定义的Aspect的代码,将需要注入的代码插入到Advice指定的位置。

如果你对AspectJ有了解的话,已经可以猜到SDK内部是怎么实现全埋点的了;如果没有接触,我觉得也不用急于全面地去学习AspectJ,因为SDK内部只用到了AspectJ当中的一小部分功能而已,可以直接看下面的分析。

3.2 全埋点-技术实现

神策SDK里面是如何监测View点击事件呢?我把SDK代码简化一下进行分析,有下面几个步骤:

3.2.1 定义一个Aspect 

  1. import org.aspectj.lang.JoinPoint; 
  2. import org.aspectj.lang.annotation.After
  3. import org.aspectj.lang.annotation.Aspect; 
  4. import org.aspectj.lang.annotation.Pointcut; 
  5.  
  6. @Aspect 
  7. public class ViewOnClickListenerAspectj{ 
  8.  
  9.     /** 
  10. * android.view.View.OnClickListener.onClick(android.view.View
  11. *@paramjoinPoint JoinPoint 
  12. *@throwsThrowable Exception 
  13. */ 
  14.     @After("execution(* android.view.View.OnClickListener.onClick(android.view.View))"
  15.     public void onViewClickAOP(final JoinPoint joinPoint)throws Throwable { 
  16.         AopUtil.sendTrackEventToSDK(joinPoint, "onViewOnClick"); 
  17.     } 

这段Aspect的代码定义了: 在执行android.view.View.OnClickListener.onClick(android.view.View)方法原有的实现后面,需要插入 AopUtil.sendTrackEventToSDK(joinPoint, "onViewOnClick"); 这段代码。

AopUtil.sendTrackEventToSDK(joinPoint, "onViewOnClick"); 这段代码做的事情就是点击事件的上报。因为神策SDK将全埋点功能和主SDK包分离成了两个jar包,所以通过AopUtil工具去调用真正的事件上报代码,这里不细述其实现,下面直接看这段代码背后真正的点击上报实现。

  1. SensorsDataAPI.sharedInstance().track(AopConstants.APP_CLICK_EVENT_NAME, properties); 

可以看到AOP实现的点击监测,***也走 track 方法进行上报了。

3.2.2 使用ajc编译器向源代码中插入Aspect代码

采用AspectJ框架编写的代码,想要注入原来的工程的代码,需要在 /app/build.gradle 中引用ajc编译器,脚本如下: 

  1. ... 
  2. import org.aspectj.bridge.IMessage 
  3. import org.aspectj.bridge.MessageHandler 
  4. import org.aspectj.tools.ajc.Main 
  5.  
  6. buildscript { 
  7.     repositories { 
  8.         mavenCentral() 
  9.     } 
  10.     dependencies { 
  11.         classpath 'org.aspectj:aspectjtools:1.8.10' 
  12.     } 
  13.  
  14. repositories { 
  15.     mavenCentral() 
  16.  
  17. android { 
  18.     ... 
  19.  
  20. dependencies { 
  21.     ... 
  22.     compile 'org.aspectj:aspectjrt:1.8.10' 
  23.  
  24. final def log = project.logger 
  25. final def variants = project.android.applicationVariants 
  26.  
  27. variants.all { variant -> 
  28.     if (!variant.buildType.isDebuggable()) { 
  29.         log.debug("Skipping non-debuggable build type '${variant.buildType.name}'."
  30.         return
  31.     } 
  32.  
  33.     JavaCompile javaCompile = variant.javaCompile 
  34.     javaCompile.doLast { 
  35.         String[] args = ["-showWeaveInfo"
  36.                      "-1.5"
  37.                      "-inpath", javaCompile.destinationDir.toString(), 
  38.                      "-aspectpath", javaCompile.classpath.asPath, 
  39.                      "-d", javaCompile.destinationDir.toString(), 
  40.                      "-classpath", javaCompile.classpath.asPath, 
  41.                      "-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)] 
  42.         log.debug "ajc args: " + Arrays.toString(args) 
  43.  
  44.         MessageHandler handler = new MessageHandler(true); 
  45.         new Main().run(args, handler); 
  46.         for (IMessage message : handler.getMessages(nulltrue)) { 
  47.            switch (message.getKind()) { 
  48.                 case IMessage.ABORT: 
  49.                 case IMessage.ERROR: 
  50.                 case IMessage.FAIL: 
  51.                     log.error message.message, message.thrown 
  52.                     break; 
  53.                 case IMessage.WARNING: 
  54.                     log.warn message.message, message.thrown 
  55.                     break; 
  56.                 case IMessage.INFO: 
  57.                     log.info message.message, message.thrown 
  58.                     break; 
  59.                 case IMessage.DEBUG: 
  60.                     log.debug message.message, message.thrown 
  61.                     break; 
  62.             } 
  63.         } 
  64.     } 

在SensorsAndroidSDK中,把上面这段脚本编写成了一个 gradle插件 ,开发者只需要在 app/build.gradle 引用这个插件即可。

  1. apply plugin: 'com.sensorsdata.analytics.android' 

3.2.3 完成代码插入,查看插入之后的效果

完成上面两步,就可以实现在 android.view.View.OnClickListener.onClick(android.view.View) 方法中插入我们的数据上报代码了。我们在demo代码中加一个Button,并给它set一个OnClickListener,编译一下代码,查看 /build/intermediates/classes/debug/ 里面class文件,经过ajc编译之后,原始代码中插入了Aspect的代码,并调用了 ViewOnClickListenerAspectj 里面的 onViewClickAOP 方法。 

  1. public class MainActivityextends Activity{ 
  2.     public MainActivity(){ 
  3.     } 
  4.  
  5.     protected void onCreate(Bundle savedInstanceState){ 
  6.         super.onCreate(savedInstanceState); 
  7.         this.setContentView(2130968603); 
  8.         Button btnTst = (Button)this.findViewById(2131427422); 
  9.         btnTst.setOnClickListener(new OnClickListener() { 
  10.             public void onClick(View v){ 
  11.                 JoinPoint var2 = Factory.makeJP(ajc$tjp_0, this, this, v); 
  12.  
  13.                 try { 
  14.                     Log.i("MainActivity""button clicked"); 
  15.                 } catch (Throwable var5) { 
  16.                     ViewOnClickListenerAspectj.aspectOf().onViewClickAOP(var2); 
  17.                     throw var5; 
  18.                 } 
  19.  
  20.                 ViewOnClickListenerAspectj.aspectOf().onViewClickAOP(var2); 
  21.             } 
  22.  
  23.             static { 
  24.                 ajc$preClinit(); 
  25.             } 
  26.         }); 
  27.     } 

AspectJ的基本用法就是这样,SensorsAndroidSDK借助AspectJ插入了Aspect代码,这是一种静态的方式。静态的全埋点方案,本质上是对字节码进行修改,插入事件上报的代码。

修改字节码,除了这种方案之外,还有Android Gradle插件提供的trasform api(1.5.0版本以上)、ASM、Javassist。在网易乐得的埋点方案,Nuwa热修复项目都可以见到这些技术的实践。

3.3 AspectJ相关资料

  • Aspect Oriented Programming in Android: https://fernandocejas.com/2014/08/03/aspect-oriented-programming-in-android/
  • AOP之AspectJ全面剖析in Android: http://www.jianshu.com/p/f90e04bcb326
  • 沪江开源了一个叫做AspectJX的插件,扩展了AspectJ,除了对src代码进行AOP,还支持kotlin、工程中引用的jar和aar进行AOP: https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx
  • 关于 Spring AOP (AspectJ) 你该知晓的一切: http://blog.csdn.net/javazejian/article/details/56267036

3.4 其他思路

上面介绍的是以AspectJ为代表的 “静态Hook” 实现方案,有没有其他办法可以不修改源代码,只是在App运行的时候去 “动态Hook” 点击行为的处理呢?答案是肯定的,在Java的世界,还有反射大法啊,下面看一下怎么实现点击事件的替换吧。

在 android.view.View.java 的源码( API>=14 )中,有这么几个关键的方法: 

  1. // getListenerInfo方法:返回所有的监听器信息mListenerInfo 
  2. ListenerInfogetListenerInfo(){ 
  3.     if (mListenerInfo != null) { 
  4.         return mListenerInfo; 
  5.     } 
  6.     mListenerInfo = new ListenerInfo(); 
  7.     return mListenerInfo; 
  8.  
  9. // 监听器信息 
  10. static class ListenerInfo{ 
  11.     ... // 此处省略各种xxxListener 
  12.     /** 
  13. * Listener used to dispatch click events. 
  14. * This field should be made private, so it is hidden from the SDK. 
  15. * {@hide} 
  16. */ 
  17.     public OnClickListener mOnClickListener; 
  18.  
  19.     /** 
  20. * Listener used to dispatch long click events. 
  21. * This field should be made private, so it is hidden from the SDK. 
  22. * {@hide} 
  23. */ 
  24.     protected OnLongClickListener mOnLongClickListener; 
  25.  
  26.     ... 
  27. ListenerInfo mListenerInfo; 
  28.  
  29. // 我们非常熟悉的方法,内部其实是把mListenerInfo的mOnClickListener设成了我们创建的OnclickListner对象 
  30. public void setOnClickListener(@Nullable OnClickListener l){ 
  31.     if (!isClickable()) { 
  32.         setClickable(true); 
  33.     } 
  34.     getListenerInfo().mOnClickListener = l; 
  35.  
  36. /** 
  37. * 判断这个View是否设置了点击监听器 
  38. Return whether this view has an attached OnClickListener. Returns 
  39. true if there is a listener, false if there is none. 
  40. */ 
  41. public boolean hasOnClickListeners(){ 
  42.     ListenerInfo li = mListenerInfo; 
  43.     return (li != null && li.mOnClickListener != null); 

通过上面几个方法可以看到,点击监听器其实被保存在了 mListenerInfo.mOnClickListener 里面。那么实现 Hook点击监听器 时,只要将这个 mOnClickListener 替换成我们包装的 点击监听器代理对象 就行了。简单看一下实现思路:

1. 创建点击监听器的代理类 

  1. // 点击监听器的代理类,具有上报点击行为的功能 
  2. class OnClickListenerWrapperimplements View.OnClickListener{ 
  3.     // 原始的点击监听器对象 
  4.     private View.OnClickListener onClickListener; 
  5.  
  6.     public OnClickListenerWrapper(View.OnClickListener onClickListener){ 
  7.         this.onClickListener = onClickListener; 
  8.     } 
  9.  
  10.     @Override 
  11.     public void onClick(View view){ 
  12.         // 让原来的点击监听器正常工作 
  13.         if(onClickListener != null){ 
  14.             onClickListener.onClick(view); 
  15.         } 
  16.         // 点击事件上报,可以获取被点击view的一些属性 
  17.         track(APP_CLICK_EVENT_NAME, getSomeProperties(view)); 
  18.     } 

2. 反射获取一个View的mListenerInfo.mOnClickListener,替换成代理的点击监听器 

  1. // 对一个View的点击监听器进行hook 
  2. public void hookView(View view) { 
  3.     // 1. 反射调用View的getListenerInfo方法(API>=14),获得mListenerInfo对象 
  4.     Class viewClazz = Class.forName("android.view.View"); 
  5.     Method getListenerInfoMethod = viewClazz.getDeclaredMethod("getListenerInfo"); 
  6.     if (!getListenerInfoMethod.isAccessible()) { 
  7.         getListenerInfoMethod.setAccessible(true); 
  8.     } 
  9.     Object mListenerInfo = listenerInfoMethod.invoke(view); 
  10.      
  11.     // 2. 然后从mListenerInfo中反射获取mOnClickListener对象 
  12.     Class listenerInfoClazz = Class.forName("android.view.View$ListenerInfo"); 
  13.     Field onClickListenerField = listenerInfoClazz.getDeclaredField("mOnClickListener"); 
  14.     if (!onClickListenerField.isAccessible()) { 
  15.         onClickListenerField.setAccessible(true); 
  16.     } 
  17.     View.OnClickListener mOnClickListener = (View.OnClickListener) onClickListenerField.get(mListenerInfo); 
  18.      
  19.     // 3. 创建代理的点击监听器对象 
  20.     View.OnClickListener mOnClickListenerWrapper = new OnClickListenerWrapper(mOnClickListener); 
  21.      
  22.     // 4. 把mListenerInfo的mOnClickListener设成新的onClickListenerWrapper 
  23.     onClickListenerField.set(mListenerInfo, mOnClickListenerWrapper); 
  24.     // 用这个似乎也可以:view.setOnClickListener(mOnClickListenerWrapper); 

注意,如果是 API<14 的话,mOnClickListener直接是直接以一个Field保存在View对象中的,没有ListenerInfo,因此反射的次数要更少一些。

3. 对App中所有的View进行Hook

我们在分析的是全埋点,那么怎样把App里面所有的View点击都Hook到呢?有两种方式:

***种:当Activity创建完成后,开始从Activity的DecorView开始自顶向下深度遍历ViewTree,遍历到一个View的时候,对它进行hookView操作。这种方式有点暴力,由于这里面遍历ViewTree的时候用到了大量反射,性能会有影响。

第二种:比***种方式稍微优秀一些,来源是一个Github上的开源库 AndroidTracker (Kotlin实现)。他的处理方式是当Activity创建完成后,在DecorView中添加一个透明的View作为子View,在这个子View的onTouchEvent方法中,根据触摸坐标找到屏幕中包含了这个坐标的View,再对这些View尝试进行hookView操作。 这种方式比较取巧,首先是拿到了手指按下的位置,根据这个位置来找需要被Hook的View,避免了在遍历ViewTree的同时对View进行反射。具体实现是在遍历ViewTree中的每个View时,判断这个View的坐标是否包含手指按下的坐标,以及View是否Visible,如果满足这两个条件,就把这个View保存到一个ArrayList hitViews。然后再遍历这个ArrayList里面的View,如果一个View#hasOnClickListeners返回true,那么才对他进行hookView操作。

整体来看,动态Hook的思路用到了反射,难免对程序性能产生影响,如果要采用这种方式实现全埋点方案,还需要好好评估。

四、可视化埋点

4.1 可视化埋点-技术实现

可视化埋点,需要经过两个步骤,可以由非技术人员操作完成。***步,使用嵌入了Mixpanel/SensorsSDK的App连接后台,当手机App与后台同步时,后台管理界面上会显示和手机App一样的界面,用户可以在管理界面上用鼠标选择需要监测的元素,设置事件名称,需要监测的元素属性等(据说有些SDK的圈选操作是在手机上进行的,不管是什么方式本质上是一样的,需要保存一份配置到后台)。第二步,嵌入了SDK的App启动时,会从服务器获取到一份配置,再根据这份配置去检测App中的界面及其元素,满足配置的条件时向服务器上报事件。下面以Mixpanel、SensorsdataSDK为例,简单分析一下技术方案的实现。

4.1.1 圈选需要监测的元素,保存配置

1.创建WebSocket连接后台

采用WebSocket连接是因为要让手机和后台长时间保持连接,是一个持续的双向通信。连接到后台时,把手机的设备信息发送到后台。

2.把App界面截图发送到后台

创建Socket连接后,在主线程中,对App中启动的Activity进行扫描,找到界面的RootView(其实是DecorView)。在查找RootView的同时,会对RootView进行截图,截图时采用反射调用View类 createSnapshot 方法。

截图之后,SDK内部会判断图片的hash值,如果图片发生了变化,会采用递归的方法深度遍历Activity的ViewTree,遍历同时读取View的属性(id、top、left、width、height、class名称、layoutRules等等)。

***,将上面收集到数据发送到连接的后台,由后台解析之后,把App界面展示在Web页面。用户可以在这个Web页面圈选需要监测的元素,设置这个元素的时间名称(event_type和event_name),并保存这个配置。

4.1.2 获取配置,监测元素的行为,上报事件

1.获取配置

SDK启动时,会从服务器拉取一份JSON格式的配置,保存到sharedPreference里,同时SDK会扫描 android.R 文件里面的资源id和资源的name并保存起来。

SDK得到配置之后,解析成JSON对象,读取 event_bindings 字段,再进一步读取 events 字段,这个字段下面包含了一个数组,数组的每个元素都描述了一类事件,并包含了这类事件需要监测的元素所在的Activity和元素的路径。这份配置基本上是这样的一个结构: 

  1. event_bindings: { 
  2.     events:[ 
  3.         { 
  4.             target_activity: "" 
  5.             event_name: "" 
  6.             event_type: "" 
  7.             path: [ 
  8.                 { 
  9.                     prefix: 
  10.                     view_class: 
  11.                     index
  12.                     id: 
  13.                     id_name: 
  14.                 },  
  15.                 ... 
  16.             ] 
  17.         } 
  18.     ] 

收到了这份配置之后,SDK会把根据每个event信息,生成一个 ViewVisitor 。 ViewVisitor 的作用就是把 path 数组里面指向的所有View元素都找到,并且根据event_type,给这个View元素设置相应的行为监测器,当这个View发生指定行为时,监测器就会监测到,并上报行为。

生成ViewVisitor之后,SDK内部是以 Map 结构保存它们的,这也比较容易理解。

2.监测元素,上报事件

ViewVisitor 是怎么监测元素的产生的行为呢?答案就是 View.AccessibilityDelegate 。

在Android SDK里面,AccessibilityService)为我们提供了一系列的事件回调,帮助我们指示一些用户界面的状态变化。我们可以派生辅助功能类,进而对不同的AccessibilityEvent进行处理,我们看下AccessibilityEvent里面有哪些事件类型: 

  1. /** 
  2. * Represents the event of clicking on a {@linkandroid.view.Viewlike 
  3. * {@linkandroid.widget.Button}, {@linkandroid.widget.CompoundButton}, etc. 
  4. */ 
  5. public static final int TYPE_VIEW_CLICKED = 0x00000001; 
  6.  
  7. /** 
  8. * Represents the event of long clicking on a {@linkandroid.view.Viewlike 
  9. * {@linkandroid.widget.Button}, {@linkandroid.widget.CompoundButton}, etc. 
  10. */ 
  11. public static final int TYPE_VIEW_LONG_CLICKED = 0x00000002; 
  12.  
  13. /** 
  14. * Represents the event of selecting an item usually in the context of an 
  15. * {@linkandroid.widget.AdapterView}. 
  16. */ 
  17. public static final int TYPE_VIEW_SELECTED = 0x00000004; 
  18.  
  19. /** 
  20. * Represents the event of setting input focus of a {@linkandroid.view.View}. 
  21. */ 
  22. public static final int TYPE_VIEW_FOCUSED = 0x00000008; 
  23.  
  24. /** 
  25. * Represents the event of changing the text of an {@linkandroid.widget.EditText}. 
  26. */ 
  27. public static final int TYPE_VIEW_TEXT_CHANGED = 0x00000010; 
  28. ... 

以点击事件 TYPE_VIEW_CLICKED 为例 ,当Activity界面的RootView开始绘制的时候(ViewTreeObserver.OnGlobalLayoutListener的onGlobalLayout回调时),ViewVisitor也会开始寻找指定的View,并给这个View设置新的AccessibilityDelegate。简单看一下这个新的View.AccessibilityDelegate是怎么写的: 

  1. private class TrackingAccessibilityDelegateextends View.AccessibilityDelegate{ 
  2. ... 
  3.             public TrackingAccessibilityDelegate(View.AccessibilityDelegate realDelegate){ 
  4.                 mRealDelegate = realDelegate; 
  5.             } 
  6.  
  7.             public View.AccessibilityDelegategetRealDelegate(){ 
  8.                 return mRealDelegate; 
  9.             } 
  10.  
  11.             ... 
  12.              
  13.             @Override 
  14.             public void sendAccessibilityEvent(View host,int eventType){ 
  15.                 if (eventType == mEventType) { 
  16.                     fireEvent(host); // 事件上报 
  17.                 } 
  18.  
  19.                 if (null != mRealDelegate) { 
  20.                     mRealDelegate.sendAccessibilityEvent(host, eventType); 
  21.                 } 
  22.             } 
  23.  
  24.             private View.AccessibilityDelegate mRealDelegate; 
  25.         } 
  26.         ... 

可以看到在SDK的 TrackingAccessibilityDelegate#sendAccessibilityEvent 方法里面,发出了事件上报。

那么View在点击方法的内部实现里有调用 sendAccessibilityEvent 方法吗?通过View处理点击事件时调用的 View.performClick 方法,看一下源码: 

  1. public boolean performClick(){ 
  2.     final boolean result; 
  3.     final ListenerInfo li = mListenerInfo; 
  4.     if (li != null && li.mOnClickListener != null) { 
  5.         playSoundEffect(SoundEffectConstants.CLICK); 
  6.         li.mOnClickListener.onClick(this); 
  7.         result = true
  8.     } else { 
  9.         result = false
  10.     } 
  11.     sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); 
  12.     return result; 
  13. ... 
  14. public void sendAccessibilityEvent(int eventType){ 
  15.     if (mAccessibilityDelegate != null) { 
  16.         mAccessibilityDelegate.sendAccessibilityEvent(this, eventType); 
  17.     } else { 
  18.         sendAccessibilityEventInternal(eventType); 
  19.     } 
  20. ... 
  21. public void setAccessibilityDelegate(@Nullable AccessibilityDelegate delegate){ 
  22.     mAccessibilityDelegate = delegate; 

由此可以见在RootView开始绘制的时候,给View注册AccessibilityDelegate可以监测到它的点击事件。

4.2 可视化埋点的难点和问题

上面简单分析了Mixpanel和SensorsSDK可视化埋点的基本实现,里面还有一个难点需要仔细琢磨,那就是 如何唯一标识App中的一个View? 需要记录View的哪些信息,如何生成View的唯一ID,保证在不同手机上这些ID是固定的,而且保证App每次启动,ID也不会变化,同时ID也要能应对一定程度的界面调整。

另外在网上看到有网友提出,setAccessibilityDelegate来监测View的点击对大多数厂商的机型和版本都是可以的,但是有部分机型是无法成功捕获监控到点击事件。从View的标识生成,以及监测原理来讲,这个方案的稳定性存在一些疑问。

4.3 参考资料

  • sensorsdata git,包含了Android、iOS、js、JAVA等多个版本的SDK: https://github.com/sensorsdata
  • Mixpanel git,包含了Android、iOS、js、JAVA等多个版本的SDK: https://github.com/mixpanel
  • 网易移动端数据收集和分析博客: http://www.jianshu.com/c/ee326e36f556

五、总结

***简单总结一下几种方案的优缺点和使用场景,在实际应用中多种方式配合使用,平衡效率和可靠性,适合自己的业务才是***的。 

Android埋点技术分析

责任编辑:未丽燕 来源: Uncle Chen
相关推荐

2016-12-12 13:42:54

数据分析大数据埋点

2021-06-17 13:35:23

数据埋点分析客户端

2018-11-14 11:26:49

神策数据

2023-12-13 18:46:50

FlutterAOP业务层

2021-02-19 07:59:21

数据埋点数据分析大数据

2023-01-10 09:08:53

埋点数据数据处理

2021-08-10 13:50:24

iOS

2023-04-19 09:05:44

2021-08-31 19:14:38

技术埋点运营

2024-03-06 19:57:56

探索商家可视化

2020-04-29 16:24:55

开发iOS技术

2023-09-05 07:28:02

Java自动埋点

2022-10-14 08:47:42

埋点统计优化

2022-11-01 18:21:14

数据埋点SDK

2017-04-11 15:34:41

机票前台埋点

2016-08-12 00:30:45

互联网数据埋点

2023-02-08 19:37:37

大数据技术

2023-11-21 07:14:43

埋点大数据

2022-08-31 07:54:08

采集sdk埋点数据

2009-10-29 17:17:01

接入层技术
点赞
收藏

51CTO技术栈公众号