Android埋點探索--ASM字節碼插樁

爲何進行全埋點?

以往手動形式埋點

以往的埋點方式都是人爲進行定義名稱和選擇性埋點,版本迭代屢次後形成埋點數量持續增長。java

  • 在各個代碼塊進行基本相同的代碼調用,侵入性高,若是後期進行更換SDK,有可能會進行大量改動
  • 手動進行埋點可能致使認爲疏忽形成的埋點丟失
  • 只能根據埋點進行用戶行爲回溯,有些細節和流程沒法銜接上,沒法還原用戶使用場景
  • 每一個版本迭代都須要PM,RD進行埋點梳理,時間進行消耗

全埋點

  • 沒法在每一個按鈕,頁面加載調用代碼,只須要在應用初始化加載便可
  • 用戶行爲觸發自動上報,無需PM思考應該在哪一個頁面進行埋點
  • 可配置化,能夠選擇過濾上報頁面,事件,或者特定頁面增長屬性上報
  • 版本迭代不須要從新進行埋點

如何進行?

  • 頁面操做:Application.ActivityLifecycleCallbacks接口
public interface ActivityLifecycleCallbacks {
  
  void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState);
  
  void onActivityStarted(@NonNull Activity activity);
  
  void onActivityResumed(@NonNull Activity activity);
  
  void onActivityPaused(@NonNull Activity activity);
  
  void onActivityStopped(@NonNull Activity activity);
  
  void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState);
  
  void onActivityDestroyed(@NonNull Activity activity);

}
複製代碼

應用啓動結束:AppStart,AppEnd

在ActivityLifecycleCallbacks接口中監聽start和pause,並使用SP和ContentProvider進行輔助記錄應用的開啓時間和pause時間,若是用戶App在後臺被強殺或者手動退出,那麼下次從新使用APP的時候會進行檢測Sp中的時間和當前的時間,而後進行對比,判斷用戶是否爲從新啓動APP,仍是僅僅切換到後臺再切換回來。android

注意⚠️:start中進行檢測,pause中進行時間數據更新。編程

應用點擊控件

方案1:hook控件的點擊事件接口進行代理

總體思路:根據ActivityLifecycleCallbacks接口監聽回調,在onActivityResume回調中拿到當前的Activity,而後利用DecorView遞歸遍歷全部子view進行代理onClickListener方法。同時在Activity啓動的時候進行ViewTree的observer,ViewTree改動的時候(好比設置了view的不可見不可點擊等)從新進行一遍hook。json

hook:利用反射獲取到View已經設置的onClickListener對象、區別view的對象類型(button,textView.....)進而設置不一樣的listener。app

缺點:基本每一個View或者Viewgroup都會有本身的點擊事件,而且點擊事件接口都爲class內部的藉口,沒有頂層的接口進行兼容檢測,因此須要作大量的wrapperListener,工做繁瑣重複。此外,每建立一個頁面就要進行一次Hook,性能不高,效率低。框架

方案2:利用Window點擊的回調

每次點擊的事件分發函數——dispatchTouchEvent(MotionEvent event),進行hook,利用當前activity的RootView的信息再結合event的信息進行埋點。maven

具體:判斷點擊的座標是否位於view(利用rootView循環判斷)之中、該view是否處於可見狀態;ide

缺點:每次點擊都要去遍歷一次rootView,而且逐個判斷,效率低下。函數

方案3:AOP(Aspect Oriented Programming)

面向切面編程。使用AspectJ,性能

思路:在程序編譯期間,在相應的onClick方法調用前或後插入埋點代碼。

方案4:字節碼插樁

字節碼函數插樁目前有如下兩種框架

ASM

思路:應用程序打包成APK以前會先編譯成.class文件,而後打包成dex,最後組成apk。因此在打包成dex文件和編譯成.class文件之間進行源文件的替換就行。

缺點:目前沒什麼缺點

Javassist

與ASM思路一致,可是和ASM對比,效率不夠高。

ASM框架進行字節碼函數插樁

通過上述方案的對比,最終採用ASM進行字節碼插樁。主要是對代碼的侵入低,可定製化配置(過濾採集頁面,過濾時長,配置頁面映射等)。

下圖箭頭指向處就是進行函數插樁的位置。

代碼侵入性低

方案實現是在代碼文件編譯成class文件以後進行方法的插入,無需在編寫階段進行。

  • 使用android提供的Transform API獲取project的文件
  • 檢測到文件後綴爲class的時候進行文件修改
    • ASM框架相應API進行字節碼讀取和分析和插入
    • 先拿到類的詳細信息(類名,修飾符,繼承的父類,實現的接口等信息)
    • 接着掃描到該類的方法,進行判斷插入咱們預設的埋點代碼
    • 而後覆蓋原來的class文件
  • 接着gradle繼續編譯生成dex

效率

比java中使用反射快,在ASM的官網中也有介紹。ASM的設計和實現是儘量的小和儘量快,因此它很是適合在動態系統中使用(但固然也能夠以靜態方式使用,例如在編譯器中使用)。

更多關於框架ASM的遠離和具體使用在這裏就不贅述了。

如何使用?

在project的build.gradle添加:

buildscript {
    
    repositories {
        google()
        jcenter()
        maven {
            url uri('repo')
        }
        
    }
    dependencies {
        classpath 'com.cage:autotrack.android:1.0.0'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}
複製代碼

在APP模塊中:

apply plugin: 'com.cage.plugin'

dependencies{
 implementation project(':cgtrack_support')
}

複製代碼

初始化:

//Application中初始化
//kotlin
TrackApi.init(this)

//java
TrackApi.INSTANCE.init(this);
//配置
ConfigOptions.INSTANCE.addTrackInfoCallBack(new TrackInfoCallback() {
                @Override
                public void trackInfo(String eventName, JSONObject json) {
                   //這裏進行埋點事件上報
                   //固然回調的類型也能夠從JSONObjetc變爲String
                }
            });
複製代碼

接入APP後

在APP中進行點擊瀏覽頁面,相應的事件進行觸發:

頁面點擊的時候觸發:

頁面退出的時候觸發:

進入頁面的時候觸發:

後續維護與迭代升級

目前已經覆蓋了View,Dialog,CompoundButton,AdapterView,BottomNavigationView。

後續若是缺乏相應的控件,那麼能夠根據相應的控件進行添加對應的字節碼描述便可:

例如在APP中的底部控件爲Google的design控件,添加:

SDK_API_CLASS = "com/cage/cgtrack/TrackUtils"

//普通設置點擊事件
if(mInterfaces.contains('android/support/design/widget/BottomNavigationView$OnNavigationItemSelectedListener') && nameDesc == 'onNavigationItemSelected(Landroid/view/MenuItem;)Z') {
    //插入變量
    methodVisitor.visitVarInsn(ALOAD, 1)
    //插入方法
    methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/view/MenuItem;)V", false)
}

//使用Lambda形式設置
MethodCell onNavigationItemSelected = new MethodCell(
                'onNavigationItemSelected',
                '(Landroid/view/ MenuItem;)Z',
                'Landroid/support/design/widget/BottomNavigationView$OnNavigationItemSelectedListener',
                'trackViewOnClick',
                '(Landroid/view/MenuItem;)V',
                1, 1,
                [Opcodes.ALOAD])
        LAMBDA_METHODS.put(onNavigationItemSelected.parent + onNavigationItemSelected.name + onNavigationItemSelected.desc, onNavigationItemSelected)

複製代碼

上述步驟的意思:

先判斷該類中實現的接口是否包含OnNavigationItemSelectedListener接口,接着判斷實現該接口的方法是否是onNavigationItemSelected,若是符合,那麼表明這個類包含該接口並實現了方法,能夠進行埋點代碼的插入。

相關文章
相關標籤/搜索