自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Android埋點(diǎn)技術(shù)分析

移動(dòng)開(kāi)發(fā) Android
埋點(diǎn),是對(duì)網(wǎng)站、App或者后臺(tái)等應(yīng)用程序進(jìn)行數(shù)據(jù)采集的一種方法。通過(guò)埋點(diǎn),可以收集用戶在應(yīng)用中的產(chǎn)生行為,進(jìn)而用于分析和優(yōu)化產(chǎn)品后續(xù)的體驗(yàn),也可以為產(chǎn)品的運(yùn)營(yíng)提供數(shù)據(jù)支撐,其中常見(jiàn)的指標(biāo)有PV、UV、頁(yè)面時(shí)長(zhǎng)和按鈕的點(diǎn)擊等。

一、埋點(diǎn),是對(duì)網(wǎng)站、App或者后臺(tái)等應(yīng)用程序進(jìn)行數(shù)據(jù)采集的一種方法。通過(guò)埋點(diǎn),可以收集用戶在應(yīng)用中的產(chǎn)生行為,進(jìn)而用于分析和優(yōu)化產(chǎn)品后續(xù)的體驗(yàn),也可以為產(chǎn)品的運(yùn)營(yíng)提供數(shù)據(jù)支撐,其中常見(jiàn)的指標(biāo)有PV、UV、頁(yè)面時(shí)長(zhǎng)和按鈕的點(diǎn)擊等。

Android埋點(diǎn)技術(shù)分析

采集行為數(shù)據(jù)時(shí),通常需要在Web頁(yè)面/App里面添加一些代碼,當(dāng)用戶的行為達(dá)到某種條件時(shí),就會(huì)向服務(wù)器上報(bào)用戶的行為。其實(shí)添加這些代碼的過(guò)程就可以叫做“埋點(diǎn)”,在很久以前就已經(jīng)出現(xiàn)了這種技術(shù)。隨著技術(shù)的發(fā)展和大家對(duì)數(shù)據(jù)采集要求的不斷提高,我認(rèn)為埋點(diǎn)的技術(shù)方案走過(guò)了下面幾個(gè)階段:

代碼埋點(diǎn):代碼埋點(diǎn)是指開(kāi)發(fā)人員按照產(chǎn)品/運(yùn)營(yíng)的需求,在Web頁(yè)面/App的源碼里面添加行為上報(bào)的代碼,當(dāng)用戶的行為滿足某一個(gè)條件時(shí),這些代碼就會(huì)被執(zhí)行,向服務(wù)器上報(bào)行為數(shù)據(jù)。這種方案是最基礎(chǔ)的方案,每次增加或者修改數(shù)據(jù)上報(bào)的條件,都需要開(kāi)發(fā)人員的參與,并且只能在下一個(gè)版本上線后才能看到效果。很多公司都提供了這類數(shù)據(jù)上報(bào)的SDK,將行為上報(bào)的后臺(tái)服務(wù)器接口封裝成了簡(jiǎn)單的客戶端SDK接口。開(kāi)發(fā)者可以通過(guò)嵌入這類SDK,在埋點(diǎn)的地方調(diào)用少量的代碼就可以上報(bào)行為數(shù)據(jù)。

全埋點(diǎn):全埋點(diǎn)指的是將Web頁(yè)面/App內(nèi)產(chǎn)生的所有的、滿足某個(gè)條件的行為,全部上報(bào)到后臺(tái)服務(wù)器。例如把App中所有的按鈕點(diǎn)擊都進(jìn)行上報(bào),然后由產(chǎn)品/運(yùn)營(yíng)去后臺(tái)篩選所需要的行為數(shù)據(jù)。這種方案的優(yōu)點(diǎn)非常明顯,就是可以不用在新增/修改行為上報(bào)條件時(shí),再找開(kāi)發(fā)人員去修改埋點(diǎn)的代碼。然而它的缺點(diǎn)也和優(yōu)點(diǎn)一樣明顯,那就是上報(bào)的數(shù)據(jù)量比代碼埋點(diǎn)大很多,里面可能很多是沒(méi)有價(jià)值的數(shù)據(jù)。此外,這種方案更傾向于獨(dú)立去看待用戶的行為,而沒(méi)有關(guān)注行為的上下文,給數(shù)據(jù)分析帶來(lái)了一些難度。很多公司也提供了這類功能的SDK,通過(guò)靜態(tài)或者動(dòng)態(tài)的方式,“Hook”了原有的App代碼,從而實(shí)現(xiàn)了行為的監(jiān)測(cè),在數(shù)據(jù)上報(bào)時(shí)通常是采用累積多條再上報(bào)的方案來(lái)合并請(qǐng)求。

hook直譯是鉤子的意思,以前學(xué)信息安全的時(shí)候在windows上聽(tīng)到過(guò),大體意思是通過(guò)某種手段去改變系統(tǒng)API的一個(gè)行為,繞過(guò)系統(tǒng)的某個(gè)方法,或者改變系統(tǒng)的工作流程。在這里其實(shí)是指把本來(lái)要執(zhí)行某個(gè)方法的對(duì)象替換成另一個(gè),一般用的是反射或者代理,需要找到hook的代碼位置,甚至還可以在編譯階段實(shí)現(xiàn)替換。

可視化埋點(diǎn): 可視化埋點(diǎn)是指產(chǎn)品/運(yùn)營(yíng)在Web頁(yè)面/App的界面上進(jìn)行圈選,配置需要監(jiān)測(cè)界面上哪一個(gè)元素,然后保存這個(gè)配置,當(dāng)App啟動(dòng)時(shí)會(huì)從后臺(tái)服務(wù)器獲得產(chǎn)品/運(yùn)營(yíng)預(yù)先圈選好的配置,然后根據(jù)這份配置監(jiān)測(cè)App界面上的元素,當(dāng)某一個(gè)元素滿足條件時(shí),就會(huì)上報(bào)行為數(shù)據(jù)到后臺(tái)服務(wù)器。有了全埋點(diǎn)技術(shù)方案,從體驗(yàn)優(yōu)化的角度很容易想到按需埋點(diǎn),可視化埋點(diǎn)就是一種按需配置埋點(diǎn)的方案?,F(xiàn)在也有一些公司提供了這類SDK,圈選監(jiān)測(cè)元素時(shí),一般都是提供一個(gè)Web管理界面,手機(jī)在安裝并初始化了SDK之后,可以和管理界面了連接,讓用戶在Web管理界面上配置需要監(jiān)測(cè)的元素。

業(yè)界有多家SDK都支持上面介紹的3種埋點(diǎn)方案中的一種或者全部,例如Mixpanel、Sensorsdata、TalkingData、GrowingIO、諸葛IO、Heap Analytics、MTA、Umeng Analytics、百度,只是大家對(duì)后兩種埋點(diǎn)的稱呼不完全相同,有的叫無(wú)埋點(diǎn)或者codeless埋點(diǎn)。由于 Mixpanel (支持代碼埋點(diǎn)、可視化埋點(diǎn))和 Sensorsdata (全部支持)都開(kāi)源了自己的全部SDK,技術(shù)方案也比較類似,下面以它們的Android SDK為例,簡(jiǎn)單分析一下3種埋點(diǎn)方案的技術(shù)實(shí)現(xiàn)。關(guān)于JS的SDK技術(shù)實(shí)現(xiàn),可以看下我的另一篇博客-JS埋點(diǎn)SDK技術(shù)分析。

二、代碼埋點(diǎn)

包含Mixpanel SDK在內(nèi)的大部分SDK,都會(huì)把這種埋點(diǎn)方案封裝成一個(gè)比較簡(jiǎn)單的接口,在這里是 track(String eventName, JSONObject properties) ,開(kāi)發(fā)者在調(diào)用這個(gè)接口時(shí),可以把一個(gè)事件名稱和事件的屬性傳入,然后就可以上報(bào)到后臺(tái)了。

在實(shí)現(xiàn)上,Mixpanel SDK默認(rèn)采用一條HandlerThread線程來(lái)處理事件,當(dāng)開(kāi)發(fā)者調(diào)用 track(String eventName, JSONObject properties) 方法時(shí), 主線程切換到HandlerThread 當(dāng)中,并先將事件存入數(shù)據(jù)庫(kù)。然后看SDK中是否累計(jì)到了40個(gè)事件,如果累計(jì)到40個(gè)事件的話,就合并它們上報(bào)到后臺(tái)。

當(dāng)開(kāi)發(fā)者設(shè)置為debug模式,或者手動(dòng)調(diào)用 flush 接口時(shí),可以立即上報(bào)累計(jì)的所有事件,不過(guò)由于只有一條線程,所以如果在flush的時(shí)候,前面的事件還沒(méi)有處理完成,SDK會(huì)間隔1分鐘再次去處理后面的這些事件。

開(kāi)發(fā)者可以設(shè)置累計(jì)上報(bào)的事件數(shù)量閾值、事件阻塞時(shí)再次嘗試上報(bào)的時(shí)間間隔等。這種方案比較基礎(chǔ),相信大部分開(kāi)發(fā)者都接觸過(guò),不需要過(guò)多分析。

三、全埋點(diǎn)

3.1 AOP基礎(chǔ)

Mixpanel現(xiàn)在的Android SDK沒(méi)有提供這個(gè)功能,但是神策Android SDK提供了,實(shí)現(xiàn)方式是依賴AOP。那么什么是AOP?

在軟件業(yè),AOP為Aspect Oriented Programming的縮寫(xiě),意為:面向切面編程,通過(guò)預(yù)編譯方式和運(yùn)行期動(dòng)態(tài)代理實(shí)現(xiàn)程序功能的統(tǒng)一維護(hù)的一種技術(shù)。AOP是OOP的延續(xù),是軟件開(kāi)發(fā)中的一個(gè)熱點(diǎn),也是Spring框架中的一個(gè)重要內(nèi)容,是函數(shù)式編程的一種衍生范型。利用AOP可以對(duì)業(yè)務(wù)邏輯的各個(gè)部分進(jìn)行隔離,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時(shí)提高了開(kāi)發(fā)的效率。(from baidu baike)

簡(jiǎn)而言之,AOP是可以通過(guò)預(yù)編譯方式和運(yùn)行期動(dòng)態(tài)代理實(shí)現(xiàn)在不修改源代碼的情況下給程序動(dòng)態(tài)統(tǒng)一添加功能的一種技術(shù)。

Sensors Analytics AndroidSDK全埋點(diǎn)的實(shí)現(xiàn)就是通過(guò)在代碼編譯階段,找到源代碼中需要上報(bào)事件的位置,插入SDK的事件上報(bào)代碼。它用到的框架是 AspectJ 。

說(shuō)到這里,必須簡(jiǎn)單了解一下AspectJ以及它里面的一些概念.它是AOP的領(lǐng)跑者,在很多地方我們可以看到它的身影,例如JakeWartson大神貢獻(xiàn)的一個(gè)注解日志和性能調(diào)優(yōu)框架 Hugo ,在Spring框架里面也大量應(yīng)用到AspectJ。我理解AspectJ里面的主要幾個(gè)概念有:

  • JPoint: 代碼切點(diǎn)(就是我們要插入代碼的地方)
  • Aspect: 代碼切點(diǎn)的描述
  • Pointcut: 描述切點(diǎn)具體是什么樣的點(diǎn),如函數(shù)被調(diào)用的地方( Call(MethodSignature) )、函數(shù)執(zhí)行的內(nèi)部( execution(MethodSignature) )
  • Advice: 描述在切點(diǎn)的什么位置插入代碼,如在Pointcut前面( @Before )還是后面( @After ),還是環(huán)繞整個(gè)Pointcut( @Around )

由此可見(jiàn),在實(shí)現(xiàn)AOP功能時(shí),需要做下面幾件事:

  • 定義一個(gè)Aspect,這個(gè)Aspect里面必須有Pointcut和Advice兩個(gè)屬性
  • 編寫(xiě)在匹配到符合Pointcut和Advice描述的代碼時(shí),需要注入的代碼
  • 在代碼編譯時(shí),通過(guò)特殊的java編譯器(Aspect的ajc編譯器),找到符合我們定義的Aspect的代碼,將需要注入的代碼插入到Advice指定的位置。

如果你對(duì)AspectJ有了解的話,已經(jīng)可以猜到SDK內(nèi)部是怎么實(shí)現(xiàn)全埋點(diǎn)的了;如果沒(méi)有接觸,我覺(jué)得也不用急于全面地去學(xué)習(xí)AspectJ,因?yàn)镾DK內(nèi)部只用到了AspectJ當(dāng)中的一小部分功能而已,可以直接看下面的分析。

3.2 全埋點(diǎn)-技術(shù)實(shí)現(xiàn)

神策SDK里面是如何監(jiān)測(cè)View點(diǎn)擊事件呢?我把SDK代碼簡(jiǎn)化一下進(jìn)行分析,有下面幾個(gè)步驟:

3.2.1 定義一個(gè)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的代碼定義了: 在執(zhí)行android.view.View.OnClickListener.onClick(android.view.View)方法原有的實(shí)現(xiàn)后面,需要插入 AopUtil.sendTrackEventToSDK(joinPoint, "onViewOnClick"); 這段代碼。

AopUtil.sendTrackEventToSDK(joinPoint, "onViewOnClick"); 這段代碼做的事情就是點(diǎn)擊事件的上報(bào)。因?yàn)樯癫逽DK將全埋點(diǎn)功能和主SDK包分離成了兩個(gè)jar包,所以通過(guò)AopUtil工具去調(diào)用真正的事件上報(bào)代碼,這里不細(xì)述其實(shí)現(xiàn),下面直接看這段代碼背后真正的點(diǎn)擊上報(bào)實(shí)現(xiàn)。

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

可以看到AOP實(shí)現(xiàn)的點(diǎn)擊監(jiān)測(cè),***也走 track 方法進(jìn)行上報(bào)了。

3.2.2 使用ajc編譯器向源代碼中插入Aspect代碼

采用AspectJ框架編寫(xiě)的代碼,想要注入原來(lái)的工程的代碼,需要在 /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中,把上面這段腳本編寫(xiě)成了一個(gè) gradle插件 ,開(kāi)發(fā)者只需要在 app/build.gradle 引用這個(gè)插件即可。

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

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

完成上面兩步,就可以實(shí)現(xiàn)在 android.view.View.OnClickListener.onClick(android.view.View) 方法中插入我們的數(shù)據(jù)上報(bào)代碼了。我們?cè)赿emo代碼中加一個(gè)Button,并給它set一個(gè)OnClickListener,編譯一下代碼,查看 /build/intermediates/classes/debug/ 里面class文件,經(jīng)過(guò)ajc編譯之后,原始代碼中插入了Aspect的代碼,并調(diào)用了 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代碼,這是一種靜態(tài)的方式。靜態(tài)的全埋點(diǎn)方案,本質(zhì)上是對(duì)字節(jié)碼進(jìn)行修改,插入事件上報(bào)的代碼。

修改字節(jié)碼,除了這種方案之外,還有Android Gradle插件提供的trasform api(1.5.0版本以上)、ASM、Javassist。在網(wǎng)易樂(lè)得的埋點(diǎn)方案,Nuwa熱修復(fù)項(xiàng)目都可以見(jiàn)到這些技術(shù)的實(shí)踐。

3.3 AspectJ相關(guān)資料

  • 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
  • 滬江開(kāi)源了一個(gè)叫做AspectJX的插件,擴(kuò)展了AspectJ,除了對(duì)src代碼進(jìn)行AOP,還支持kotlin、工程中引用的jar和aar進(jìn)行AOP: https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx
  • 關(guān)于 Spring AOP (AspectJ) 你該知曉的一切: http://blog.csdn.net/javazejian/article/details/56267036

3.4 其他思路

上面介紹的是以AspectJ為代表的 “靜態(tài)Hook” 實(shí)現(xiàn)方案,有沒(méi)有其他辦法可以不修改源代碼,只是在App運(yùn)行的時(shí)候去 “動(dòng)態(tài)Hook” 點(diǎn)擊行為的處理呢?答案是肯定的,在Java的世界,還有反射大法啊,下面看一下怎么實(shí)現(xiàn)點(diǎn)擊事件的替換吧。

在 android.view.View.java 的源碼( API>=14 )中,有這么幾個(gè)關(guān)鍵的方法: 

  1. // getListenerInfo方法:返回所有的監(jiān)聽(tīng)器信息mListenerInfo 
  2. ListenerInfogetListenerInfo(){ 
  3.     if (mListenerInfo != null) { 
  4.         return mListenerInfo; 
  5.     } 
  6.     mListenerInfo = new ListenerInfo(); 
  7.     return mListenerInfo; 
  8.  
  9. // 監(jiān)聽(tīng)器信息 
  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. // 我們非常熟悉的方法,內(nèi)部其實(shí)是把mListenerInfo的mOnClickListener設(shè)成了我們創(chuàng)建的OnclickListner對(duì)象 
  30. public void setOnClickListener(@Nullable OnClickListener l){ 
  31.     if (!isClickable()) { 
  32.         setClickable(true); 
  33.     } 
  34.     getListenerInfo().mOnClickListener = l; 
  35.  
  36. /** 
  37. * 判斷這個(gè)View是否設(shè)置了點(diǎn)擊監(jiān)聽(tīng)器 
  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); 

通過(guò)上面幾個(gè)方法可以看到,點(diǎn)擊監(jiān)聽(tīng)器其實(shí)被保存在了 mListenerInfo.mOnClickListener 里面。那么實(shí)現(xiàn) Hook點(diǎn)擊監(jiān)聽(tīng)器 時(shí),只要將這個(gè) mOnClickListener 替換成我們包裝的 點(diǎn)擊監(jiān)聽(tīng)器代理對(duì)象 就行了。簡(jiǎn)單看一下實(shí)現(xiàn)思路:

1. 創(chuàng)建點(diǎn)擊監(jiān)聽(tīng)器的代理類 

  1. // 點(diǎn)擊監(jiān)聽(tīng)器的代理類,具有上報(bào)點(diǎn)擊行為的功能 
  2. class OnClickListenerWrapperimplements View.OnClickListener{ 
  3.     // 原始的點(diǎn)擊監(jiān)聽(tīng)器對(duì)象 
  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.         // 讓原來(lái)的點(diǎn)擊監(jiān)聽(tīng)器正常工作 
  13.         if(onClickListener != null){ 
  14.             onClickListener.onClick(view); 
  15.         } 
  16.         // 點(diǎn)擊事件上報(bào),可以獲取被點(diǎn)擊view的一些屬性 
  17.         track(APP_CLICK_EVENT_NAME, getSomeProperties(view)); 
  18.     } 

2. 反射獲取一個(gè)View的mListenerInfo.mOnClickListener,替換成代理的點(diǎn)擊監(jiān)聽(tīng)器 

  1. // 對(duì)一個(gè)View的點(diǎn)擊監(jiān)聽(tīng)器進(jìn)行hook 
  2. public void hookView(View view) { 
  3.     // 1. 反射調(diào)用View的getListenerInfo方法(API>=14),獲得mListenerInfo對(duì)象 
  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對(duì)象 
  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. 創(chuàng)建代理的點(diǎn)擊監(jiān)聽(tīng)器對(duì)象 
  20.     View.OnClickListener mOnClickListenerWrapper = new OnClickListenerWrapper(mOnClickListener); 
  21.      
  22.     // 4. 把mListenerInfo的mOnClickListener設(shè)成新的onClickListenerWrapper 
  23.     onClickListenerField.set(mListenerInfo, mOnClickListenerWrapper); 
  24.     // 用這個(gè)似乎也可以:view.setOnClickListener(mOnClickListenerWrapper); 

注意,如果是 API<14 的話,mOnClickListener直接是直接以一個(gè)Field保存在View對(duì)象中的,沒(méi)有ListenerInfo,因此反射的次數(shù)要更少一些。

3. 對(duì)App中所有的View進(jìn)行Hook

我們?cè)诜治龅氖侨顸c(diǎn),那么怎樣把App里面所有的View點(diǎn)擊都Hook到呢?有兩種方式:

***種:當(dāng)Activity創(chuàng)建完成后,開(kāi)始從Activity的DecorView開(kāi)始自頂向下深度遍歷ViewTree,遍歷到一個(gè)View的時(shí)候,對(duì)它進(jìn)行hookView操作。這種方式有點(diǎn)暴力,由于這里面遍歷ViewTree的時(shí)候用到了大量反射,性能會(huì)有影響。

第二種:比***種方式稍微優(yōu)秀一些,來(lái)源是一個(gè)Github上的開(kāi)源庫(kù) AndroidTracker (Kotlin實(shí)現(xiàn))。他的處理方式是當(dāng)Activity創(chuàng)建完成后,在DecorView中添加一個(gè)透明的View作為子View,在這個(gè)子View的onTouchEvent方法中,根據(jù)觸摸坐標(biāo)找到屏幕中包含了這個(gè)坐標(biāo)的View,再對(duì)這些View嘗試進(jìn)行hookView操作。 這種方式比較取巧,首先是拿到了手指按下的位置,根據(jù)這個(gè)位置來(lái)找需要被Hook的View,避免了在遍歷ViewTree的同時(shí)對(duì)View進(jìn)行反射。具體實(shí)現(xiàn)是在遍歷ViewTree中的每個(gè)View時(shí),判斷這個(gè)View的坐標(biāo)是否包含手指按下的坐標(biāo),以及View是否Visible,如果滿足這兩個(gè)條件,就把這個(gè)View保存到一個(gè)ArrayList hitViews。然后再遍歷這個(gè)ArrayList里面的View,如果一個(gè)View#hasOnClickListeners返回true,那么才對(duì)他進(jìn)行hookView操作。

整體來(lái)看,動(dòng)態(tài)Hook的思路用到了反射,難免對(duì)程序性能產(chǎn)生影響,如果要采用這種方式實(shí)現(xiàn)全埋點(diǎn)方案,還需要好好評(píng)估。

四、可視化埋點(diǎn)

4.1 可視化埋點(diǎn)-技術(shù)實(shí)現(xiàn)

可視化埋點(diǎn),需要經(jīng)過(guò)兩個(gè)步驟,可以由非技術(shù)人員操作完成。***步,使用嵌入了Mixpanel/SensorsSDK的App連接后臺(tái),當(dāng)手機(jī)App與后臺(tái)同步時(shí),后臺(tái)管理界面上會(huì)顯示和手機(jī)App一樣的界面,用戶可以在管理界面上用鼠標(biāo)選擇需要監(jiān)測(cè)的元素,設(shè)置事件名稱,需要監(jiān)測(cè)的元素屬性等(據(jù)說(shuō)有些SDK的圈選操作是在手機(jī)上進(jìn)行的,不管是什么方式本質(zhì)上是一樣的,需要保存一份配置到后臺(tái))。第二步,嵌入了SDK的App啟動(dòng)時(shí),會(huì)從服務(wù)器獲取到一份配置,再根據(jù)這份配置去檢測(cè)App中的界面及其元素,滿足配置的條件時(shí)向服務(wù)器上報(bào)事件。下面以Mixpanel、SensorsdataSDK為例,簡(jiǎn)單分析一下技術(shù)方案的實(shí)現(xiàn)。

4.1.1 圈選需要監(jiān)測(cè)的元素,保存配置

1.創(chuàng)建WebSocket連接后臺(tái)

采用WebSocket連接是因?yàn)橐屖謾C(jī)和后臺(tái)長(zhǎng)時(shí)間保持連接,是一個(gè)持續(xù)的雙向通信。連接到后臺(tái)時(shí),把手機(jī)的設(shè)備信息發(fā)送到后臺(tái)。

2.把App界面截圖發(fā)送到后臺(tái)

創(chuàng)建Socket連接后,在主線程中,對(duì)App中啟動(dòng)的Activity進(jìn)行掃描,找到界面的RootView(其實(shí)是DecorView)。在查找RootView的同時(shí),會(huì)對(duì)RootView進(jìn)行截圖,截圖時(shí)采用反射調(diào)用View類 createSnapshot 方法。

截圖之后,SDK內(nèi)部會(huì)判斷圖片的hash值,如果圖片發(fā)生了變化,會(huì)采用遞歸的方法深度遍歷Activity的ViewTree,遍歷同時(shí)讀取View的屬性(id、top、left、width、height、class名稱、layoutRules等等)。

***,將上面收集到數(shù)據(jù)發(fā)送到連接的后臺(tái),由后臺(tái)解析之后,把App界面展示在Web頁(yè)面。用戶可以在這個(gè)Web頁(yè)面圈選需要監(jiān)測(cè)的元素,設(shè)置這個(gè)元素的時(shí)間名稱(event_type和event_name),并保存這個(gè)配置。

4.1.2 獲取配置,監(jiān)測(cè)元素的行為,上報(bào)事件

1.獲取配置

SDK啟動(dòng)時(shí),會(huì)從服務(wù)器拉取一份JSON格式的配置,保存到sharedPreference里,同時(shí)SDK會(huì)掃描 android.R 文件里面的資源id和資源的name并保存起來(lái)。

SDK得到配置之后,解析成JSON對(duì)象,讀取 event_bindings 字段,再進(jìn)一步讀取 events 字段,這個(gè)字段下面包含了一個(gè)數(shù)組,數(shù)組的每個(gè)元素都描述了一類事件,并包含了這類事件需要監(jiān)測(cè)的元素所在的Activity和元素的路徑。這份配置基本上是這樣的一個(gè)結(jié)構(gòu): 

  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會(huì)把根據(jù)每個(gè)event信息,生成一個(gè) ViewVisitor 。 ViewVisitor 的作用就是把 path 數(shù)組里面指向的所有View元素都找到,并且根據(jù)event_type,給這個(gè)View元素設(shè)置相應(yīng)的行為監(jiān)測(cè)器,當(dāng)這個(gè)View發(fā)生指定行為時(shí),監(jiān)測(cè)器就會(huì)監(jiān)測(cè)到,并上報(bào)行為。

生成ViewVisitor之后,SDK內(nèi)部是以 Map 結(jié)構(gòu)保存它們的,這也比較容易理解。

2.監(jiān)測(cè)元素,上報(bào)事件

ViewVisitor 是怎么監(jiān)測(cè)元素的產(chǎn)生的行為呢?答案就是 View.AccessibilityDelegate 。

在Android SDK里面,AccessibilityService)為我們提供了一系列的事件回調(diào),幫助我們指示一些用戶界面的狀態(tài)變化。我們可以派生輔助功能類,進(jìn)而對(duì)不同的AccessibilityEvent進(jìn)行處理,我們看下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. ... 

以點(diǎn)擊事件 TYPE_VIEW_CLICKED 為例 ,當(dāng)Activity界面的RootView開(kāi)始繪制的時(shí)候(ViewTreeObserver.OnGlobalLayoutListener的onGlobalLayout回調(diào)時(shí)),ViewVisitor也會(huì)開(kāi)始尋找指定的View,并給這個(gè)View設(shè)置新的AccessibilityDelegate。簡(jiǎn)單看一下這個(gè)新的View.AccessibilityDelegate是怎么寫(xiě)的: 

  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); // 事件上報(bào) 
  17.                 } 
  18.  
  19.                 if (null != mRealDelegate) { 
  20.                     mRealDelegate.sendAccessibilityEvent(host, eventType); 
  21.                 } 
  22.             } 
  23.  
  24.             private View.AccessibilityDelegate mRealDelegate; 
  25.         } 
  26.         ... 

可以看到在SDK的 TrackingAccessibilityDelegate#sendAccessibilityEvent 方法里面,發(fā)出了事件上報(bào)。

那么View在點(diǎn)擊方法的內(nèi)部實(shí)現(xiàn)里有調(diào)用 sendAccessibilityEvent 方法嗎?通過(guò)View處理點(diǎn)擊事件時(shí)調(diào)用的 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; 

由此可以見(jiàn)在RootView開(kāi)始繪制的時(shí)候,給View注冊(cè)AccessibilityDelegate可以監(jiān)測(cè)到它的點(diǎn)擊事件。

4.2 可視化埋點(diǎn)的難點(diǎn)和問(wèn)題

上面簡(jiǎn)單分析了Mixpanel和SensorsSDK可視化埋點(diǎn)的基本實(shí)現(xiàn),里面還有一個(gè)難點(diǎn)需要仔細(xì)琢磨,那就是 如何唯一標(biāo)識(shí)App中的一個(gè)View? 需要記錄View的哪些信息,如何生成View的唯一ID,保證在不同手機(jī)上這些ID是固定的,而且保證App每次啟動(dòng),ID也不會(huì)變化,同時(shí)ID也要能應(yīng)對(duì)一定程度的界面調(diào)整。

另外在網(wǎng)上看到有網(wǎng)友提出,setAccessibilityDelegate來(lái)監(jiān)測(cè)View的點(diǎn)擊對(duì)大多數(shù)廠商的機(jī)型和版本都是可以的,但是有部分機(jī)型是無(wú)法成功捕獲監(jiān)控到點(diǎn)擊事件。從View的標(biāo)識(shí)生成,以及監(jiān)測(cè)原理來(lái)講,這個(gè)方案的穩(wěn)定性存在一些疑問(wèn)。

4.3 參考資料

  • sensorsdata git,包含了Android、iOS、js、JAVA等多個(gè)版本的SDK: https://github.com/sensorsdata
  • Mixpanel git,包含了Android、iOS、js、JAVA等多個(gè)版本的SDK: https://github.com/mixpanel
  • 網(wǎng)易移動(dòng)端數(shù)據(jù)收集和分析博客: http://www.jianshu.com/c/ee326e36f556

五、總結(jié)

***簡(jiǎn)單總結(jié)一下幾種方案的優(yōu)缺點(diǎn)和使用場(chǎng)景,在實(shí)際應(yīng)用中多種方式配合使用,平衡效率和可靠性,適合自己的業(yè)務(wù)才是***的。 

Android埋點(diǎn)技術(shù)分析

責(zé)任編輯:未麗燕 來(lái)源: Uncle Chen
相關(guān)推薦

2016-12-12 13:42:54

數(shù)據(jù)分析大數(shù)據(jù)埋點(diǎn)

2021-06-17 13:35:23

數(shù)據(jù)埋點(diǎn)分析客戶端

2018-11-14 11:26:49

神策數(shù)據(jù)

2023-12-13 18:46:50

FlutterAOP業(yè)務(wù)層

2024-08-27 08:27:19

2023-04-19 09:05:44

2021-02-19 07:59:21

數(shù)據(jù)埋點(diǎn)數(shù)據(jù)分析大數(shù)據(jù)

2023-01-10 09:08:53

埋點(diǎn)數(shù)據(jù)數(shù)據(jù)處理

2021-08-10 13:50:24

iOS

2024-07-18 08:33:19

2021-08-31 19:14:38

技術(shù)埋點(diǎn)運(yùn)營(yíng)

2022-10-14 08:47:42

埋點(diǎn)統(tǒng)計(jì)優(yōu)化

2022-11-01 18:21:14

數(shù)據(jù)埋點(diǎn)SDK

2024-03-06 19:57:56

探索商家可視化

2024-08-29 14:44:01

質(zhì)檢埋點(diǎn)

2020-04-29 16:24:55

開(kāi)發(fā)iOS技術(shù)

2023-09-05 07:28:02

Java自動(dòng)埋點(diǎn)

2017-04-11 15:34:41

機(jī)票前臺(tái)埋點(diǎn)

2025-01-22 14:00:12

2009-10-29 17:17:01

接入層技術(shù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)