一拱撵、埋點(diǎn)方案總結(jié)
AppEnd 全埋點(diǎn)方案
- AppClick全埋點(diǎn)方案1: 代理View.OnclickListener
- AppClick全埋點(diǎn)方案2: 代理Window.Callback
- AppClick全埋點(diǎn)方案3: 代理View.AccessibilityDelegate
- AppClick全埋點(diǎn)方案4: 透明層
- AppClick全埋點(diǎn)方案5: AspectJ
- AppClick全埋點(diǎn)方案6: ASM
- AppClick全埋點(diǎn)方案7: JavaSsist
- AppClick全埋點(diǎn)方案8: AST
二振定、埋點(diǎn)事件簡介
- AppStart 事件
是指app啟動(dòng)谢澈,同時(shí)包括冷啟動(dòng)和熱啟動(dòng)拨扶,熱啟動(dòng)是指應(yīng)用程序從后臺恢復(fù)逗余。 - AppEnd 事件
是指app退出,包括正常退出影钉、home退到后臺画髓、被強(qiáng)殺、崩潰等場景平委。 - AppViewScreen 事件
是指App頁面瀏覽奈虾,切換Activity或者Fragment - AppClick 事件
是指App的點(diǎn)擊事件,所有的view的點(diǎn)擊事件
三、AppClick事件的全埋點(diǎn)整體解決思路
就是要自動(dòng)找到 那個(gè)被點(diǎn)擊事件的控制處理邏輯(后文統(tǒng)稱原處理邏輯)肉微,利用一定的技術(shù)處理匾鸥,來對原處理邏輯進(jìn)行 "攔截" ,或者在原處理邏輯執(zhí)行前面或執(zhí)行后面 "插入" 相應(yīng)的埋點(diǎn)代碼碉纳,從而達(dá)到自動(dòng)埋點(diǎn)的效果勿负。
在編譯器對Java代碼的處理流程中,可以采用不同的埋點(diǎn)方案劳曹。
? ? ? ? ? ? ? ? ??APT? ? ? ? ? ? ??????AspectJ? ? ? ? ? ? ? ? ASM
JavaCode ----------.java ----------- .class ----------- .dex
? ? ? ? ? ? ? ? ?? AST? ? ? ? ? ? ? ? ??? ? ? ? ? ? ? ? ? ? ?????? Javassit
四奴愉、全埋點(diǎn)綜合方案考慮因素
- 效率
- 靜態(tài)代理
通過Gradle Plugin 在應(yīng)用程序編譯期間 “插入”代碼或者修改代碼(.class)。比如AspectJ铁孵、ASM锭硼、JavaSsist、AST等均屬于這種方式库菲。 - 動(dòng)態(tài)代理
在代碼運(yùn)行的時(shí)候(Runtime)去進(jìn)行代理账忘。例如:View.OnClickListener志膀、Window.Callback熙宇、View.AccessbilityDelegate等方案均屬于這種方式。
- 靜態(tài)代理
靜態(tài)代理明顯優(yōu)于動(dòng)態(tài)代理溉浙,因?yàn)殪o態(tài)代理是在程序編譯階段處理的烫止,不會對應(yīng)用程序的整體性能有太大影響,而動(dòng)態(tài)代理是在程序運(yùn)行階段發(fā)生的戳稽,所以對程序性能會有一定的影響馆蠕。
兼容性
Android生態(tài)系統(tǒng)一直在飛速發(fā)展,有不同的開發(fā)語言(Java惊奇、Kotlin互躬、Flutter),不同的Java版本(Java7颂郎、Java8)吼渡、混合開發(fā)、不同的Gradle版本乓序,以及Lambda寺酪、D8、Instant Run替劈、DataBinding寄雀、Fragemnt等,都會給兼容性帶來影響陨献。擴(kuò)展性
隨著業(yè)務(wù)快速發(fā)展盒犹,數(shù)據(jù)分析不斷提高,我們自動(dòng)采集要求越來越高等。
五急膀、埋點(diǎn)實(shí)現(xiàn)思路
- AppViewScreen 事件
ActivityLifecycleCallbacks是Application的一個(gè)內(nèi)部接口膜蛔,是從API14(Android 4.0)開始提供的。它提供了生命周期的監(jiān)聽脖阵。
public interface ActivityLifecycleCallbacks {
/**
* Called as the first step of the Activity being created. This is always called before
* {@link Activity#onCreate}.
*/
default void onActivityPreCreated(@NonNull Activity activity,
@Nullable Bundle savedInstanceState) {
}
/**
* Called when the Activity calls {@link Activity#onCreate super.onCreate()}.
*/
void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState);
/**
* Called as the last step of the Activity being created. This is always called after
* {@link Activity#onCreate}.
*/
default void onActivityPostCreated(@NonNull Activity activity,
@Nullable Bundle savedInstanceState) {
}
/**
* Called as the first step of the Activity being started. This is always called before
* {@link Activity#onStart}.
*/
default void onActivityPreStarted(@NonNull Activity activity) {
}
/**
* Called when the Activity calls {@link Activity#onStart super.onStart()}.
*/
void onActivityStarted(@NonNull Activity activity);
/**
* Called as the last step of the Activity being started. This is always called after
* {@link Activity#onStart}.
*/
default void onActivityPostStarted(@NonNull Activity activity) {
}
/**
* Called as the first step of the Activity being resumed. This is always called before
* {@link Activity#onResume}.
*/
default void onActivityPreResumed(@NonNull Activity activity) {
}
/**
* Called when the Activity calls {@link Activity#onResume super.onResume()}.
*/
void onActivityResumed(@NonNull Activity activity);
/**
* Called as the last step of the Activity being resumed. This is always called after
* {@link Activity#onResume} and {@link Activity#onPostResume}.
*/
default void onActivityPostResumed(@NonNull Activity activity) {
}
/**
* Called as the first step of the Activity being paused. This is always called before
* {@link Activity#onPause}.
*/
default void onActivityPrePaused(@NonNull Activity activity) {
}
/**
* Called when the Activity calls {@link Activity#onPause super.onPause()}.
*/
void onActivityPaused(@NonNull Activity activity);
/**
* Called as the last step of the Activity being paused. This is always called after
* {@link Activity#onPause}.
*/
default void onActivityPostPaused(@NonNull Activity activity) {
}
/**
* Called as the first step of the Activity being stopped. This is always called before
* {@link Activity#onStop}.
*/
default void onActivityPreStopped(@NonNull Activity activity) {
}
/**
* Called when the Activity calls {@link Activity#onStop super.onStop()}.
*/
void onActivityStopped(@NonNull Activity activity);
/**
* Called as the last step of the Activity being stopped. This is always called after
* {@link Activity#onStop}.
*/
default void onActivityPostStopped(@NonNull Activity activity) {
}
/**
* Called as the first step of the Activity saving its instance state. This is always
* called before {@link Activity#onSaveInstanceState}.
*/
default void onActivityPreSaveInstanceState(@NonNull Activity activity,
@NonNull Bundle outState) {
}
/**
* Called when the Activity calls
* {@link Activity#onSaveInstanceState super.onSaveInstanceState()}.
*/
void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState);
/**
* Called as the last step of the Activity saving its instance state. This is always
* called after{@link Activity#onSaveInstanceState}.
*/
default void onActivityPostSaveInstanceState(@NonNull Activity activity,
@NonNull Bundle outState) {
}
/**
* Called as the first step of the Activity being destroyed. This is always called before
* {@link Activity#onDestroy}.
*/
default void onActivityPreDestroyed(@NonNull Activity activity) {
}
/**
* Called when the Activity calls {@link Activity#onDestroy super.onDestroy()}.
*/
void onActivityDestroyed(@NonNull Activity activity);
/**
* Called as the last step of the Activity being destroyed. This is always called after
* {@link Activity#onDestroy}.
*/
default void onActivityPostDestroyed(@NonNull Activity activity) {
}
}
所以皂股,我們可以直接在onResume里面做一個(gè)頁面信息的統(tǒng)計(jì)。
AppStart 命黔、 AppEnd 埋點(diǎn)方案
最好的方案還是使用ActivityLifecycleCallbacks呜呐,監(jiān)聽onResume表示AppStart。但是由于應(yīng)用程序會有多個(gè)進(jìn)程悍募,會導(dǎo)致無法判斷當(dāng)前進(jìn)程是出于前臺還是后臺蘑辑。我們可以采用ContentProvider+SharedPreferences的方案來解決跨進(jìn)程數(shù)據(jù)共享問題。
然后當(dāng)應(yīng)用程序被強(qiáng)殺坠宴、崩潰的時(shí)候洋魂,我們該如何判斷呢?對于一個(gè)應(yīng)用程序喜鼓,如果一個(gè)頁面退出30s內(nèi)副砍,沒有其他頁面顯示出來,我們就認(rèn)為應(yīng)用程序處于后臺了庄岖,也就是發(fā)生了AppEnd事件豁翎。-
AppClick 事件
1.代理View.OnClickListener
監(jiān)聽OnResume生命周期,我們會寫一個(gè)WrapperOnClickListener類隅忿,來代理點(diǎn)擊事件心剥,以及在后面插入對應(yīng)的統(tǒng)計(jì)代碼。我們可以通過activity.getWindow().getDecorView()來獲取根布局背桐,然后通過遍歷根布局來獲取當(dāng)前設(shè)置了點(diǎn)擊事件的OnclickListener對象优烧,這里通過反射view的getListenerInfo方法拿到ListenerInfo的mOnclickListener字段。反射效率比較低链峭,然后高版本的兼容性也會有些問題畦娄。然后把OnclickListener對象交給WrapperOnclickListener觸發(fā)。
基本上實(shí)現(xiàn)了埋點(diǎn)熏版,但是在onResume后動(dòng)態(tài)addview纷责,無法注入埋點(diǎn)代碼『扯蹋可以采用ViewTreeObserver.OnGlobalLayoutListener來解決這個(gè)問題再膳。在onGlobalLayout回調(diào)中重新調(diào)用上面的步驟,也就是重新遍歷布局曲横,找到有點(diǎn)擊事件的view喂柒,把點(diǎn)擊事件交給WrapperOnClickListener處理不瓶,并插入對應(yīng)的統(tǒng)計(jì)代碼。
缺點(diǎn):- 由于使用反射灾杰,效率比較低蚊丐,對App的整體性能有一定的影響,也可能會引入兼容性問題艳吠;
- Application.ActivityLifecycleCallbacks 要求是API 14+麦备;
- View.hasOnClickListeners()要求API 15+;
- removeOnGlobalLayoutListener要求API 16+昭娩;
- 無法采集Activity之上的View的點(diǎn)擊凛篙,比如Dialog,PopupWindow等栏渺。
2.Window.Callback
Window.callback是Window類的一個(gè)內(nèi)部類呛梆。該接口包含了一系列類似于dispatchXXX和onXXX是接口。當(dāng)用戶點(diǎn)擊某個(gè)控件時(shí)磕诊,就會回調(diào)Window.Callback中的dispatchTouchEvent(MotionEvent event)方法填物。
原理概述
在Application中初始化埋點(diǎn)sdk,然后注冊監(jiān)聽Application.ActivityLifecycleCallbacks的onCreate霎终,獲取當(dāng)前的Activity對象滞磺,通過activity拿到當(dāng)前的window,activity.getWindow(),再通過window.getCallback()可以拿到Window.callback對象神僵。然后使用自定義的WrapperWindowCallbcak代理這個(gè)Window.Callback對象雁刷。WrapperWindowCallbcak里面主要是重寫了dispatchTouchEvent(MotionEvent event)方法,通過MotionEvent參數(shù)(點(diǎn)擊的坐標(biāo))找到被點(diǎn)擊的那個(gè)view保礼,并插入埋點(diǎn)代碼,最后在調(diào)用原有的dispatchTouchEvent(MotionEvent event)方法责语,即達(dá)到“插入”埋點(diǎn)代碼的效果炮障。
缺點(diǎn):
- 由于使用反射,效率比較低坤候,對App的整體性能有一定的影響胁赢,也可能會引入兼容性問題;
- Application.ActivityLifecycleCallbacks 要求是API 14+白筹;
- View.hasOnClickListeners()要求API 15+智末;
- removeOnGlobalLayoutListener要求API 16+;
- 無法采集Dialog徒河、Popupwindow等游離于Activity之外的控件的點(diǎn)擊事件
3.Accesibility
輔助功能系馆,Android系統(tǒng)通過輔助功能幫助一些功能損失的人更好的使用APP。我們知道顽照,點(diǎn)擊事件是會調(diào)用performClik()的由蘑,里面調(diào)用了mOnclickListener.onClick之后闽寡,還會調(diào)用到sendAccessibilityEvent(AccessbilityEvent.TYPE_VIEW_CLICKED),它里面是調(diào)用了mAccessbilityDelegate對象的sendAccessibilityEvent方法,并傳入View對象和mAccessbilityDelegate.TYPE_VIEW_CLICKED參數(shù)尼酿。
原理概述
首先還是通過Application來監(jiān)聽activity的onResume方法爷狈,拿到DecordView,然后遍歷所有view裳擎,設(shè)置自定義的SensorsDataAccessbilityDelegate代理當(dāng)前View.sendAccessbiityEvent方法涎永。在布局改變的時(shí)候做上面相同的操作(監(jiān)聽ViewTreeObserve)。在自定義SensorsDataAccessbilityDelegate中會調(diào)用原有的sendAccessibilityEvent方法鹿响,并判斷是否是AccessbilityEvent.TYPE_VIEW_CLICKED類型土辩,如果是,說明有點(diǎn)擊事件抢野,就做對應(yīng)的代碼插入拷淘。
缺點(diǎn)
- 由于使用反射,效率比較低指孤,對App的整體性能有一定的影響启涯,也可能會引入兼容性問題;
- Application.ActivityLifecycleCallbacks 要求是API 14+恃轩;
- View.hasOnClickListeners()要求API 15+结洼;
- removeOnGlobalLayoutListener要求API 16+;
- 無法采集Dialog叉跛、Popupwindow等游離于Activity之外的控件的點(diǎn)擊事件
- 輔助功能需要用戶手動(dòng)開啟松忍,在部分android Rom上輔助功能可能會失效。
4.透明層
原理概述
由于Android的事件分發(fā)都是會經(jīng)過onTouchEvent方法筷厘。我們可以獲取到當(dāng)前的Activity鸣峭,在布局的最上層添加一個(gè)自定義透明的view。重寫view的onTouchEvent方法酥艳,獲取當(dāng)前點(diǎn)擊的坐標(biāo)摊溶,從RootView中找到點(diǎn)擊的view,然后交給自定義的WrapperOnClickLitener處理充石。
- Application.ActivityLifecycleCallbacks 要求是API 14+莫换;
- View.hasOnClickListeners()要求API 15+;
- removeOnGlobalLayoutListener要求API 16+骤铃;
- 無法采集Dialog拉岁、Popupwindow等游離于Activity之外的控件的點(diǎn)擊事件
- 每次點(diǎn)擊都需要遍歷RootView,效率比較低惰爬。
5.AspectJ
AOP喊暖,面向切面編程,AspectJ實(shí)際上是其中的一種补鼻。對于ApsectJ不了解的可以自行了解哄啄。也需要使用到 Gradle plugin 不了解可以自行學(xué)習(xí)一下雅任。
原理概述
我們可以把AspectJ的處理腳本放到我們自定義的插件里面,然后編寫相應(yīng)的切面類咨跌,再定義合適的PointCut用來匹配我們的織入目標(biāo)的方法(listener對象的相應(yīng)回調(diào)方法)沪么,比如Android.view.View.OnClickListener的onClick方法,就可以在編譯期間插入埋點(diǎn)代碼锌半,從而達(dá)到自動(dòng)埋點(diǎn)即全埋點(diǎn)的效果禽车。
缺點(diǎn)
- 無法織入第三方庫
- 由于定義的切點(diǎn)依賴編程語言,目前該方案我無法兼容Lambda語法
- 會有一些兼容性方面的問題刊殉,比如:D8殉摔、Gradle4.x等。
6.ASM
ASM可以在.class 文件打包成.dex文件之前修改.class文件记焊。
Gradle Transform 可以在編譯的時(shí)候遍歷所有.class文件逸月,并可以轉(zhuǎn)換成所有需要的.class輸出。
原理概述
定義一個(gè)Gradle Plugin遍膜,然后注冊一個(gè)Transform對象碗硬。在transform方法里面,可以分別遍歷目錄和jar包瓢颅,然后我們就可以遍歷當(dāng)前應(yīng)用程序所有的.class文件恩尾。然后再利用ASM框架的API,去加載相應(yīng)的.class文件挽懦、解析.class文件翰意,然后可以找到符合條件的.class文件和相關(guān)方法,最后去修改相應(yīng)的方法以動(dòng)態(tài)插入埋點(diǎn)字節(jié)碼信柿,從而達(dá)到自動(dòng)埋點(diǎn)的效果冀偶。
缺點(diǎn):目前來看ASM是最完美的方法,沒有什么缺點(diǎn)角塑。
7.Javassist
java字節(jié)碼以二進(jìn)制的形式存儲在.class文件中蔫磨,每一個(gè).class文件包含一個(gè)java類和接口。Javassist框架就是一個(gè)用來處理java字節(jié)碼的類庫圃伶。它可以在一個(gè)已編譯好的類中添加新的方法,或者修改已有的方法蒲列,并且不需要對字節(jié)碼方面有深入的了解窒朋。
javassist可以繞過編譯,直接操作字節(jié)碼蝗岖,從而實(shí)現(xiàn)代碼的注入侥猩。所以,使用javassist框架的最佳時(shí)機(jī)就是構(gòu)建工具Gradle將源文件編譯成.class文件之后抵赢,在將.class打包成.dex文件之前欺劳。
原理概述
跟上面ASM的原理一樣唧取,只是把ASM換成了javassist。
8.AST
APT是Annotation Processing Tool 的縮寫划提,即注解處理器枫弟,是一種處理注解的工具。確切來說鹏往,它是javac的一個(gè)工具淡诗,用來在編譯時(shí)掃描和處理注解。注解處理器以java代碼作為輸入伊履,以生成.java文件作為輸出韩容。簡單來說,就是在編譯期間通過注解生成.java文件唐瀑。
AST群凶,是Abstract Syntax Tree的縮寫,即“抽象語法樹”哄辣,是編輯器對代碼的第一步加工之后的結(jié)果请梢,是一個(gè)樹形式表示的源代碼。源代碼的每個(gè)元素映射到一個(gè)節(jié)點(diǎn)或者子樹柔滔。
java的編譯分為三個(gè)階段:
第一階段:所有的源文件都會被解析成語法樹溢陪。
第二階段:調(diào)用注解解析器,即APT模塊睛廊。如果注解解析器產(chǎn)生了新的源文件形真,新的源文件也要參與編譯。
第三個(gè)階段:語法樹會被分析并轉(zhuǎn)化為類文件超全。
原理概述
JavaTXT-->詞語法分析-->生成AST-->編譯字節(jié)碼咆霜、
通過操作AST,可以達(dá)到修改源代碼的功能嘶朱。
在自定義的注解解析器的process方法里蛾坯,通過roundEnvironment.getRootElements()方法可以拿到所有的Element對象,通過tree.getTree(element)方法可以拿到對應(yīng)的抽象語法樹(AST)疏遏,然后我們自定義一個(gè)TreeTranslator脉课,在visitMethodDef里面即可對方法進(jìn)行判斷。如果是目標(biāo)處理方法财异,則通過AST框架的相關(guān)API即可插入埋點(diǎn)代碼倘零,從而實(shí)現(xiàn)全埋點(diǎn)效果。
缺點(diǎn)
- com.sun.tools.javac.tree相關(guān)語法晦澀戳寸,理解難度大呈驶,要求有一定的編譯原理基礎(chǔ)
- APT無法掃描其他的module,導(dǎo)致AST無法處理其他module
- 不支持Lambda語法
- 帶有返回值的方法疫鹊,很難把埋點(diǎn)代碼插入到方法之后
本文參考資料《Android 全埋點(diǎn)解決方案》