Android全埋點解決方案讀書筆記(全)與最佳方案總結

原創不易,轉載請著名出處,謝謝

一. 全埋點概述

事件類型 事件定義
AppStart 應用程序啟動,包含冷啟動/熱啟動
AppEnd 應用程序退出,包含正常退出,home按下,程序強殺/崩潰
AppViewScreen 頁面瀏覽,包含切換Activity/Fragment
AppClick 控件點擊

1. Android View 類型

序號 控件名 監聽方法
1 Button,CheckedTextView,TextView,ImageButton,ImageView View.OnClickListener
2 SeekBar SeekBar.OnSeekBarChangeListener
3 TabHost TabHost.OnTabChangeListener
4 RatingBar RatingBar.OnRatingBarChangeListener
5 CheckBox,SwitchCompat,RadioButton,ToggleButton,RadioGroup CompoundButton.OnCheckChangeListener
6 Spinner AdapterView.OnItemSelectListener
7 MenuItem 重寫 Activity的 onOptionItemSelect,onContextItemSelect
8 ListView,GridView AdapterView.OnItemSelectChangeListener
9 ExpandableListView ExpandableListView.OnChildClickListener,ExpandableListView.OnGroupClickListener
10 Dialog DialogInterface.OnClickListener,DialogInterface.OnMultiChoiceClickListener

2. View 綁定listener方式

序號 監聽方法
1 代碼方式 - 直接 setOnClickListener 監聽
2 xml - 中 android:onClick 綁定方法,在方法中監聽
3 butterKnife - 注解方法 @OnClick(xxx) ,在方法中監聽
4 lambda 方式 - setOnClickListener(v -> xxx)【aspectj不支持】
5 dataBinding - android:onclick ="xx:xxx" ,在指定的xxx方法中監聽

二. AppViewScreen 全埋點方案

源碼:https://github.com/wangzhzh/AutoTrackAppViewScreen

1. Application.ActivityLifecycleCallbacks

  1. 通過此registerActivityLifecycleCallbacks里面監聽到onActivityResume上報AppViewScreen數據。
  2. 上報數據有event,deviceId,properties,time。
  3. 上報數據properties包含有appName,model,os-version,app-version,maunfacturer,width,height,os,lib-version,lib,activity。

2. 權限問題 READ_CONTACTS

6.0之后執行運行時權限回調onRequestPermissionResult 之后會再次執行onResume導致頁面重復上報。

  • 解決措施
    制作ignore忽略類,在onRequestPermissionResult 回調中加入addIgnoreActivity,在onStop中 移除 removeIgnoreActivity,不上報的類也可以添加進去,在上報之前判斷未忽略才執行上報。

3. 頁面名稱采集

采集按照如下優先級:

  1. activity.getTitle
  2. sdkInt>=11 ,直接獲取getToolbarTitle{activity.getActionBar.getTitle/appCompatAct.getActionBar.gettitle}
  3. activity.packageManager.activityInfo.loadLabel

三. AppStart,AppEnd 全埋點方案

源碼:https://github.com/wangzhzh/AutoTrackAppStartAppEnd

1. 原理

  • AppStart : Application.registerActivityLifecycleCallbacks方法onActivityStarted中,執行上報,并通過ContentProvider+SQLite存儲標記(作用是解決跨進程數據共享問題,通過ContentObserver監聽新進頁面,標記變化,如果在30s內,就取消上個頁面退出倒計時,如果超30s,就執行AppEnd上報)

  • AppEnd : sdk初始化的時候創建定時器,Application.registerActivityLifecycleCallbacks方法onActivityStop時,開啟定時器。30s后無新頁面進入,執行上報,程序奔潰,強殺退出,下次進入頁面需要補上報 AppEnd

2. 缺點

因為程序奔潰,強殺,后面需要補上報 AppEnd,如果用戶后面不在使用程序,或卸載程序,會導致 AppEnd 丟失

四. AppClick 全埋點方案 - 1:代理 View.OnClickenerListener

源碼:https://github.com/wangzhzh/AutoTrackAppClick1

1. 原理

在Application.registerActivityLifecycleCallbacks方法onActivityResume中,通過activity.getwindow.getDecorView獲取到其rootView,然后遞歸遍歷所有子控件,并對所有子控件的點擊事件設置代理攔截wrapperOnClickListener,其中有無點擊事件,通過反射View里面的mOnClickListener屬性判斷。

注意:

  1. 根據層級關系,DecorView是最頂層,子控件包含MenuItem及R.layout.content容器,所以為了能夠監聽到MenuItem,取最頂層DecorView,不要取R.layout.content作為rootView,(與此同時,獲取text需要加MenuItem類型的判斷)
  2. 為解決頁面中動態添加控件問題,所以引入ViewTreeObserver.OnGlobalLayoutListener,所以此時邏輯變更了,在registerActivityLifecycleCallbacks方法onActivityCreate中創建OnGlobalLayoutListener監聽器及監聽器中遍歷綁定所有控件,在onActivityResume添加監聽,在onActivityStop中移除監聽

2. 上傳字段

  • element_type: view.getclass.getCanonicalName
  • element_id: 獲取view的id
  • element_content: 獲取view的text
  • activity: 包名+類名(通過context獲取包名,如果是contextWrapper類型,需要遞歸獲取getBaseContext,直至找到activity返回包名)

3. 拓展

控件名 content獲取 監聽方法(反射+代理)
Button,CheckedTextView,TextView getText View.OnClickListener
ImageButton,ImageView getContentDescription View.OnClickListener
CheckBox,SwitchCompat,RadioButton,ToggleButton getText CompoundButton.OnCheckChangeListener
RadioGroup 獲取選中的控件,在getText CompoundButton.OnCheckChangeListener
RatingBar getRating RatingBar.OnRatingBarChangeListener
SeekBar getPrgress SeekBar.OnSeekBarChangeListener
TabHost 遍歷子控件,拼接文本 TabHost.OnTabChangeListener
Spinner 遍歷子控件,拼接文本 AdapterView.OnItemSelectChangeListener
MenuItem getMenuText 重寫 Activity的 onOptionItemSelect,onContextItemSelect
ListView,GridView getPosition AdapterView.OnItemSelectChangeListener
ExpandableListView Group position: child position ExpandableListView.OnChildClickListener,ExpandableListView.OnGroupClickListener
Dialog getText 獲取到rootView之后,在遍歷所有子控件,show添加/dismiss移除OnGlobalListener監聽,點擊代理 DialogInterface.OnClickListener,DialogInterface.OnMultiChoiceClickListener

五. AppClick 全埋點方案 - 2:代理 Window.CallBack

源碼:https://github.com/wangzhzh/AutoTrackAppClick2

1. 原理

Application.registerActivityLifecycleCallbacks方法onActivityCreate中,通過activity.getWindow.getcallBack,然后設置代理wrapperWindowCallback,通過這個代理類的dispatchTouchEvent,確定點擊的位置,然后從控件列表集合中找到具體的控件,插入埋點代碼。

判斷控件是否是集合中的哪個控件,需要滿足的條件:

  1. view.visible==view.visible
  2. view.isClickable==true
  3. MotionEvent的x,y坐標必須處于view內部

2. 拓展

控件名 判斷規則(默認滿足上面1,2,3條件)
RatingBar 4.view是ratingBar類型
SeekBar 4.view是SeekBar類型
Spinner 采用代理方式處理,代理 dapterView.OnItemSelectChangeListener
ListView,GridView 采用代理方式處理,代理 ExpandableListView.OnChildClickListener,ExpandableListView.OnGroupClickListener

六. AppClick 全埋點方案 - 3:代理 View.AccessibilityDelegate

源碼:https://github.com/wangzhzh/AutoTrackAppClick3

1. 原理

在Application.registerActivityLifecycleCallbacks方法onActivityResume中,通過activity.getwindow.getDecorView獲取到其rootView,然后遞歸遍歷所有子控件,并對所有子控件設置代理攔截mAccessibilityEvent,埋點代碼就在其回調方法中處理。

2. 拓展

ratingBar/SeekBar/Spinner/ListView,GradView/ExpandableListView 均與之前《第四章View.OnClickenerListener》反射+動態代理方案一致

3. 缺點

  1. 使用反射,效率低,有版本兼容問題
  2. 需要開啟輔助功能,部分Android Rom機型上可能會失效

七. AppClick 全埋點方案 - 4:透明層

源碼:https://github.com/wangzhzh/AutoTrackAppClick4

1. 原理

在activity的最上層添加一個透明的View,然后重寫透明view的onTouchEvent,從里面取出xy位置,判斷控件集合的具體控件,然后使用wrapperOnClickListener代理其mOnclickListener對象,并在代理類中實現埋點上報。

透明層條件:

  1. width/height需是layout.MATCH_PARENT
  2. 設置透明層在最上層,view.setElevation(xxx,999f)
  3. decorView.addView(xxx)

判斷控件是否在控件集合中,與之前《第六章View.AccessibilityDelegate》的尋找方法一致

2. 拓展

與《第五章 Window.CallBack》方案一致

七. AppClick 全埋點方案 - 5:Aspectj

源碼:https://github.com/wangzhzh/AutoTrackAppClick5

1. Aspectj

AOP 面向切面編程,可實現的有日志埋點,性能監控,動態權限控制,代碼調試
Aspectj 使用ajc編譯器,在編譯期把代碼插入目標程序中
Aspectj簡單使用:Aspectj簡單使用

使用AspectJ的2種方式:

  1. 簡單的配置Aspectj:https://github.com/wangzhzh/AutoTrackAspectJProject1
  2. 自定義Gradle Plugin:https://github.com/wangzhzh/AutoTrackAspectJProject2

2. 擴展View屬性

通過給控件setTag(int,object)的方式支持拓展,后續從view中取出這個值使用,但是為了保證tag的key不重復,需要在xml中定義資源id,使用時就使用它即可

3. 無法采集情況

無法采集的情況 解決思路 aspectj代碼
butterknife的onClick注解綁定的事件 新增對onClick有參數情況的切入點,無參數暫不考慮 @After("execution(@butterKnife.onclick **(android.view.View))")
xml android:onclick屬性綁定的事件 新增一個注解,然后加在此xml指定的方法上 @After("execution(@xxx **(android.view.View))")
MenuItem的點擊事件 新增2個menuItem監聽的2方法 @After("execution(@android.app.Activity.onOptionItemSelected(android.view.MenuItem))") @After("execution(@android.app.Activity.onContextItemSelected(android.view.MenuItem))")
設置onclickListener使用了lambda語法 aspectj暫不支持lambda語法,所以無法解決

4. 拓展

控件名 aspectj代碼
AlertDialog @After("execution(@android.content.dialogInterface.onClickListener.onClick(android.content.dialogInterface,int))") @After("execution(@android.content.dialogInterface.onMultiChoiceClickistener.onClick(android.content.dialogInterface,int,Boolean))")
CheckBox,SwitchCompat,RadioButton,ToggleButton,RadioGroup @After("execution(@android.widget.CompoundButton.OnCheckChangeListener.onCheckChanged(android.widget.CompoundButton,Boolean))")
RatingBar @After("execution(@android.widget.RatingBar.OnRatingBarChangeListener.onRatingChanged(android.widget.RatingBar,float,Boolean))")
SeekBar @After("execution(@android.widget.SeekBar.OnSeekBarChangeListener.onStopTrackingTouch(android.widget.RatingBar,float,Boolean))")
Spinner @After("execution(@android.widget.AdapterView.OnItemSelectListener.onItemSelected(android.widget.AdapterView,android.view.View,int,long))")
TabHost @After("execution(@android.widget.TabHost.OnTabChangeListener.onTabChanged(String))")
ListView,GridView @After("execution(@android.widget.AdapterView.OnItemSelectChangeListener.onItemClick(android.widget.AdapterView,android.view.View,int,long))")
ExpandableListView @After("execution(@android.widget.ExpandableListView.OnChildClickListener.onChildClick(android.widget.ExpandableListView,android.view.View,int,long))") @After("execution(@android.widget.ExpandableListView.OnGroupClickListener.onGroupClick(android.widget.ExpandableListView,android.view.View,int,long))")

5. 缺點

  1. 無法織入第三方庫
  2. 無法兼容Lambda語法
  3. 有兼容性問題,D8、Gradle4.X

七. AppClick 全埋點方案 - 6:ASM

1. ASM

Android gradle 1.5.0之后,提供了transfrom API ,允許第三方插件形式,在安卓打包過程中操作.class文件,遍歷類,jar包等,在此過程中可再使用字節碼操作工具ASM去操作,去訪問具體的類,從類中讀取類名,方法,屬性等,然后通過字節碼指令去修改原有的類(例如:訪問到onClick方法,并在方法結束之前加一段埋點上報代碼),然后在將修改好的類,繼續執行打包task,后續apk中就有了此上報邏輯。

涉及到的2個技術點:

2. 無法采集情況

無法采集的情況 解決思路 ASM代碼
xml android:onclick屬性綁定的事件 新增一個注解,然后加在此xml指定的方法上,繼續visitorAnnotation中找到此注解,設置標識,并在此方法結束之后插入埋點代碼 isFlag=true&&desc=='(Landroid/view/View;)V'

4. 拓展

所有的操作都是在方法訪問器,結束方法中判斷是否達到條件,滿足則加入埋點字節碼

控件名 ASM判斷代碼
AlertDialog mInterface.conteins('android/content/DialogInterfaceOnclickListener')&&nameDesc=='onClick(Landroid/content/DialogInterface;I)V' mInterface.conteins('android/content/DialogInterfaceOnMultichoiceclickListener')&&nameDesc=='onClick(Landroid/content/DialogInterface;IZ)V'
MenuItem nameDesc=='onContextItemSelected(Landroid/view/MenuItem;Z)V' 或 nameDesc=='onOptionsItemSelected(Landroid/view/MenuItem;Z)V'
CheckBox,SwitchCompat,RadioButton,ToggleButton,RadioGroup mInterface.conteins('android/widget/CompoundButton$OnCheckChangeListener')&&nameDesc=='onCheckChanged(Landroid/content/CompoundButton;Z)V'
RatingBar mInterface.conteins('android/widget/RatingBar$OnRatingBarChangeListener')&&nameDesc=='onRatingChanged(Landroid/content/RatingBar;FZ)V'
SeekBar mInterface.conteins('android/widget/SeekBar$OnSeekBarChangeListener')&&nameDesc=='onStopTrackingTouch(Landroid/content/SeekBar;)V'
Spinner mInterface.conteins('android/widget/AdapterView$OnItemSelectListener')&&nameDesc=='onItemSelected(Landroid/content/AdapterView;Landroid/view/View;IJ)V
TabHost mInterface.conteins('android/widget/TabHost$OnTabChangeListener')&&nameDesc=='onTabChanged(Ljava/lang/String;)V
ListView,GridView mInterface.conteins('android/widget/AdapterView$OnItemClickListener')&&nameDesc=='onItemClick(Landroid/content/AdapterView;Landroid/view/View;IJ)V
ExpandableListView mInterface.conteins('android/widget/ExpandableListViewOnChildClickListener')&&nameDesc=='onChildClick(Landroid/content/ExpandableListView;Landroid/view/View;IIJ)Z) mInterface.conteins('android/widget/ExpandableListViewOnGroupClickListener')&&nameDesc=='onGroupClick(Landroid/content/ExpandableListView;Landroid/view/View;IJ)Z

七. AppClick 全埋點方案 - 7:Javassist

1. javassist

與ASM類似,為字節碼操作工具。那么處理流程也是通過transfrom遍歷文件找到指定類,然后通過 javassist處理指定文件,實現代碼注入。

2. 拓展

所有的操作都是在獲取到所有接口數組,遍歷方法,斷是否達到條件,滿足則通過method.insertAfter加入埋點字節碼

控件名 javassist判斷代碼(nameDesc=method.name+emthod.getSignature))
xml android:onclick屬性綁定的事件 新增一個注解,然后加在此xml指定的方法上。annotation== xxx && 'currentMethod.getSignature=='(Landroid/view/View;)V''
AlertDialog mInterface.conteins('android/content/DialogInterfaceOnclickListener')&&nameDesc=='onClick(Landroid/content/DialogInterface;I)V' mInterface.conteins('android/content/DialogInterfaceOnMultichoiceclickListener')&&nameDesc=='onClick(Landroid/content/DialogInterface;IZ)V'
MenuItem nameDesc=='onContextItemSelected(Landroid/view/MenuItem;Z)V' 或 nameDesc=='onOptionsItemSelected(Landroid/view/MenuItem;Z)V'
CheckBox,SwitchCompat,RadioButton,ToggleButton,RadioGroup mInterface.conteins('android/widget/CompoundButton$OnCheckChangeListener')&&nameDesc=='onCheckChanged(Landroid/content/CompoundButton;Z)V'
RatingBar mInterface.conteins('android/widget/RatingBar$OnRatingBarChangeListener')&&nameDesc=='onRatingChanged(Landroid/content/RatingBar;FZ)V'
SeekBar mInterface.conteins('android/widget/SeekBar$OnSeekBarChangeListener')&&nameDesc=='onStopTrackingTouch(Landroid/content/SeekBar;)V'
Spinner mInterface.conteins('android/widget/AdapterView$OnItemSelectChangeListener')&&nameDesc=='onItemSelected(Landroid/content/AdapterView;Landroid/view/View;IJ)V
TabHost mInterface.conteins('android/widget/TabHost$OnTabChangeListener')&&nameDesc=='onTabChanged(Ljava/lang/String;)V
ListView,GridView mInterface.conteins('android/widget/AdapterView$OnItemClickListener')&&nameDesc=='onItemClick(Landroid/content/AdapterView;Landroid/view/View;IJ)V
ExpandableListView mInterface.conteins('android/widget/ExpandableListViewOnChildClickListener')&&nameDesc=='onChildClick(Landroid/content/ExpandableListView;Landroid/view/View;IIJ)Z) mInterface.conteins('android/widget/ExpandableListViewOnGroupClickListener')&&nameDesc=='onGroupClick(Landroid/content/ExpandableListView;Landroid/view/View;IJ)Z

八. AppClick 全埋點方案 - 8:AST

源碼:https://github.com/wangzhzh/AutoTrackAppClick8

1. APT

  • APT
    APT 簡單api
    實例:https://github.com/wangzhzh/AutoTrackAPTProject
    實質就是對頁面某個控件添加注解,然后此注解生成器會編譯時生成添加了注解的類的輔助埋點上報類,在registerActivityLifecycleCallbacks的創建方法會找到此注解類,然后執行上報邏輯

2. AST

抽象語法樹,用樹的形式表示源代碼,源代碼每個元素映射到一個節點或子樹。

編譯器對代碼的處理流程是:JavaTxt->詞語法分析->生成AST->語義分析->編譯字節碼,通過操作AST,達到修改源代碼目的。

具體流程:

  1. 注解處理器的process方法
  2. element=roundEnvironment.getRootElements
  3. tree=trees.getTree(element)
  4. 自定義一個TreeTranslator,執行tree.accept(this)
  5. 在TreeTranslator的visitMethodDef找到指定方法,通過AST框架插入埋點代碼

3. 無法采集情況

無法采集的情況 解決思路 AST代碼
butterknife的onClick注解綁定的事件 AST遍歷注解時判斷@OnClick,且方法是onClick,無返回void,參數1個 jcMethodDecl.getName==onClick&&jcMethodDecl.getParameters==void&&jcMethodDecl.getParameters.size==1
xml android:onclick屬性綁定的事件 新增一個注解,然后加在此xml指定的方法上 jcMethodDecl.getName==onClick&&jcMethodDecl.getParameters==void&&jcMethodDecl.getParameters.size==1
設置onclickListener使用了lambda語法 AST暫不支持lambda語法,所以無法解決

4. 拓展

主要根據返回值,方法名,方法參數個數及類型判斷,故封裝一個公用類統一判斷

控件名 AST代碼
AlertDialog 'onclick,void,Collections.singletonList(View),After' 'onclick,void,Arrays.asList(dialogInterface,int),After' 'onclick,void,Arrays.asList(dialogInterface,int,boolean),After'
MenuItem 'onOptionsItemSelected,boolean,Collections.singletonList(MenuItem),After' 'onContextItemSelected,boolean,Collections.singletonList(MenuItem),After'
CheckBox,SwitchCompat,RadioButton,ToggleButton,RadioGroup 'onCheckedChanged.void,Arrays.asList(CompoundButton,boolean),After'
RatingBar 'onRatingChanged,vpid,Arrays.asList(RatingBar,boolean),After'
SeekBar 'onStopTrackingTouch,void,Collections.singletonList(SeekBar),After'
Spinner 'onItemSelected,void,Arrays.asList(AdapterView<?>,View,int,long),After'
TabHost 'onTabChanged,void,Collections.singletonList(String),After
ListView,GridView 'onItemClick,void,Arrays.asList(AdapterView<?>,View,int,long),After'
ExpandableListView 'onGroupClick,boolean,Arrays.asList(ExpandableListView,View,int,long),Before' 'onChildClick,boolean,Arrays.asList(ExpandableListView,View,int,int,long),Before'

5. 缺點

  1. com.sun.tools.javac.tree APi語法晦澀,理解難度大
  2. APT無法掃描其他Module
  3. 不支持lambda語法
  4. 有返回值的方法,很難把埋點代碼插入方法之后

最佳方案總結

由于本人曾參與公司埋點SDK的研發,所以對其有一套自己的理解和感悟,總結了一種最佳的方案,其方案如下

1. 上報事件的方案選擇

  • 點擊事件,上報方案選擇
    使用asm的方案是最好,最簡單的,不會影響運行時的時間,直接執行點擊攔截上報

  • 頁面進入/離開事件,上報方案選擇
    如果是activity的頁面進入離開,直接通過Application.registerActivityLifecycleCallbacks可以直接監聽上報。

    如果希望fragment/dialog/dialogFragment/popupwindow也可以上報,可以制作他們的基類,并在基類的進入離開,加入埋點上報代碼,制作transfrom 插件,通過asm方式去替換父類,注意:(此處不僅僅是通過類訪問器找到父類,簡單的替換父類,還需要導入常量池修改庫替換常量池里面的父類,達到構造方法也同步修改,否則修改失效)

  • 冷熱啟動事件,上報方案選擇
    通過Application.registerActivityLifecycleCallbacks統計activity的有無的個數,統計冷熱啟動狀態,執行上報

  • 前臺,后臺事件,上報方案選擇
    可使用此書中的方案,開啟倒計時30s,或者直接根據registerActivityLifecycleCallbacks統計當前activity的狀態判斷也可,根據業務而定

  • 曝光/業務/xxx上報
    直接在上報sdk中提供上報方法即可

2. 處理流程

  • 構造上報對象:sdk需創建線程池,所有的上報都應該在線程池中執行

  • 加入消息隊列:創建handler線程,使用此線程隊列保證消息的次序,待上報在線程池構造成功具體的上報對象,統一封裝成消息,發送到消息隊列

  • 存入數據庫:消息隊列取出消息,執行插入數據庫操作,并加入判斷100條,執行上報,或者3分鐘上報數據庫的上報數據

  • 執行上報:從數據庫取出消息,執行okHttp的上報,并且處理上報成功刪除數據庫數據,及重試機制

3. transfrom 編譯優化

  • 開啟增量編譯,處理好增量編譯
  • 創建線程池,在線程池中執行遍歷文件,jar,提高同步編譯速度
  • 設置debug不開啟編譯,release開啟編譯,類似的動態配置開關,解決不需要此編譯項不讓其拉低apk編譯速度
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容