Android AOP之字節碼插樁

背景

隨着流量紅利時代過去,精細化運營時代的開始,網易樂得開始構建自己的大數據平臺。其中,客戶端數據採集是第一步。傳統收集數據的方式是埋點,這種方式依賴開發,採集時效慢,數據採集代碼與業務代碼不解藕。

爲了實現非侵入的,全量的數據採集,AOP成了關鍵,數據收集SDK探索和實現了一種Android上AOP的方式。

目錄

  • 1.2 Android AOP方式概述
    1.3 Android AOP方式對比選擇
  •  
  • 4.1 Android打包流程說明

    4.3 hook dx.jar獲得插樁入口
  • 五、bytecode manipulation


    5.3 bytecode manipulation實踐

一、Android AOP

1.1 什麼是AOP

面向切向編程(Aspect Oriented Programming),相對於面向對象編程(ObjectOriented Programming)而言。

OOP的精髓是把功能或問題模塊化,每個模塊處理自己的家務事。但在現實世界中,並不是所有問題都能完美得劃分到模塊中,有些功能是橫跨並嵌入衆多模塊裏的,比如下圖所示的例子。

圖1-1 AOP概念說明示例

上圖是一個APP模塊結構示例,按照照OOP的思想劃分爲“視圖交互”,“業務邏輯”,“網絡”等三個模塊,而現在假設想要對所有模塊的每個方法耗時(性能監控模塊)進行統計。這個性能監控模塊的功能就是需要橫跨並嵌入衆多模塊裏的,這就是典型的AOP的應用場景。

AOP的目標是把這些橫跨並嵌入衆多模塊裏的功能(如監控每個方法的性能) 集中起來,放到一個統一的地方來控制和管理。如果說,OOP如果是把問題劃分到單個模塊的話,那麼AOP就是把涉及到衆多模塊的某一類問題進行統一管理。

我們在開發無埋點數據收集是同樣也遇到了很多需要橫跨並嵌入衆多模塊裏的場景,這些場景將在第二章(AOP應用情景)進行介紹。下面我們調研下Android AOP的實現方式。

1.2 Android AOP方式概述

AOP從實現原理上可以分爲運行時AOP和編譯時AOP,對於Android來講運行時AOP的實現主要是hook某些關鍵方法,編譯時AOP主要是在Apk打包過程中對class文件的字節碼進行掃描更改。Android主流的aop 框架有:

  • Dexposed,Xposed等(運行時)
  • aspactJ(編譯時)

除此之外,還有一些非框架的但是能幫助我們實現 AOP的工具類庫:

  • java的動態代理機制(對java接口有效)
  • ASM,javassit等字節碼操作類庫
  • (偏方)DexMaker:Dalvik 虛擬機上,在編譯期或者運行時生成代碼的 Java API。
  • (偏方)ASMDEX(一個類似 ASM 的字節碼操作庫,運行在Android平臺,操作Dex字節碼)

1.3 Android AOP方式對比選擇

Dexposed,Xposed的缺陷很明顯,xposed需要root權限,Dexposed只對部分系統版本有效。

與之相比aspactJ沒有這些缺點,但是aspactJ作爲一個AOP的框架來講對於我們來講太重了,不僅方法數大增,而且還有一堆aspactJ的依賴要引入項目中(這些代碼定義了aspactJ框架諸如切點等概念)。更重要的是我們的目標僅僅是按照一些簡單的切點(用戶點擊等)收集數據,而不是將整個項目開發從OOP過渡到AOP。

AspactJ對於我們想要實現的數據收集需求太重了,但是這種編譯期操作class文件字節碼實現AOP的方式對我們來說是合適的。

因此我們實現Android上AOP的方式確定爲:

  • 採用編譯時的字節碼操作的做法
  • 自己hook Android編譯打包流程並藉助ASM庫對項目字節碼文件進行統一掃描,過濾以及修改。

在具體講解實現技術之前,先看一下無埋點數據收集需求遇到的三個需要AOP的場景。

二、AOP應用情景

下面舉出數據收集SDK通過修改字節碼進行AOP的三個應用情景,其中情景一和二的字節碼修改是方法級別的,情景三的字節碼修改是指令級別的。

2.1 Fragment生命週期

說明

收集頁面數據時發現有些fragment是希望當作頁面來看待,並且計算pv的(如首頁用fragmen實現的tab)。而fragment的頁面顯示/隱藏事件需要根據:

onResume()
onPause()
onHiddenChanged(boolean hidden)
setUserVisibleHint(boolean isVisibleToUser)

這四個方法綜合得出。

也就是說當項目中任一一個Fragment發生如上狀態變化,我們都要拿到這個時機,並上報相關頁面事件,也就是對Fragment的這幾個方法進行AOP。

做法是:

  • 對項目中所有代碼進行掃描,篩選出所有Fragment的子類
  • 對這些篩選出來的類的的onResumed,onPaused,onHiddenChanged,setFragmentUserVisibleHint這幾個方法的字節碼進行修改,添加上類似回調的邏輯
  • 這樣在項目中任何一個Fragment的這些回調觸發的時候我們都可以得到通知,也即對Fragment的這幾個切點進行了AOP。

示例

假設我們有一個Fragment1(空類,內部什麼代碼也沒有)

public class Fragment1 extends Fragment {}

經過掃描修改字節碼後變爲:

public class Fragment1 extends Fragment {

    @TransformedDCSDK
    public void onResume() {
        super.onResume();
        Monitor.onFragmentResumed(this);
    }

    @TransformedDCSDK
    public void onPause() {
        super.onPause();
        Monitor.onFragmentPaused(this);
    }

    @TransformedDCSDK
    public void onHiddenChanged(boolean var1) {
        super.onHiddenChanged(var1);
        Monitor.onFragmentHiddenChanged(this, var1);
    }

    @TransformedDCSDK
    public void setUserVisibleHint(boolean var1) {
        super.setUserVisibleHint(var1);
        Monitor.setFragmentUserVisibleHint(this, var1);
    }
}

注:

  1. Monitor.onFragmentResumed等函數用於上報頁面事件
  2. @TransformedDCSDK 註解標記方法被數據收集SDK進行了字節碼修改

2.2 用戶點擊事件

說明

點擊事件是分析用戶行爲的一個重要事件,Android中的點擊事件回調大多是View.OnClickListener的onClick方法(當然還有一部分是DialogInterface.OnClickListener或者重寫OnTouchEvent自己封裝的點擊)。

也就是說當項目中任一一個控件被點擊(觸發了OnClickListener),我們都要拿到這個時機,並上報點擊事件。也就是對View.OnClickListener的onClick方法進行AOP。做法是:

  • 對項目中所有代碼進行掃描,篩選出所有實現View.OnClickListener接口的類(匿名or不匿名)
  • 對onClick方法的字節碼進行修改,添加回調。
  • 達到的效果就是當APP中任何一個View被點擊時,我們都可以在捕捉到這個時機,並且上報相關點擊事件。

示例

假設有個實現接口的類

public class MyOnClickListener implements OnClickListener {
    public void onClick(View v) {
        //此處代表點擊發生時的業務邏輯
    }
}

經過掃描修改字節碼後變爲:

public class MyOnClickListener implements OnClickListener {
    @TransformedDCSDK
    public void onClick(View v) {
        if (!Monitor.onViewClick(v)) {
           //此處代表點擊發生時的業務邏輯
        }
    }
}

注:

  1. Monitor.onViewClick函數裏面包含上報點擊事件的邏輯
  2. 可以通過Monitor.onViewClick的返回值控制原有業務邏輯是否執行,基本都是執行的,只有在特殊模式下(圈選)數據收集SDK纔會忽略原有邏輯

2.3 彈窗事件

說明

彈窗顯示/關閉事件,當然彈窗的實現可以是Dialog,PopupWindow,View甚至Activity,這裏僅以Dialog爲例。

當項目中任意一個地方彈出/關閉Dialog,我們都要拿到這個時機,即對Dialog.show/dismiss/hide這幾個方法進行AOP。做法是:

  • 對項目中所有代碼進行掃描,篩選出所有字節碼指令中有調用Dialog.show/dismiss/hide的地方
  • 字節碼指令替換,替換成一段回調邏輯。
  • 這樣APP中所有Dialog的顯示/關閉時,我們都可以在這時進行一些收集數據的操作。

示例

假設項目中有一個代碼(例如方法)塊如下,其中某處調用了dialog.show()

某個方法 {
    //其他代碼
    dialog.show()
    //其他代碼
}

經過掃描修改字節碼後變爲

某個方法 {
    //其他代碼
    Monitor.showDialog(dialog)
    //其他代碼
}

注:Monitor.showDialog除了調用dialog.show()還進行一些數據收集邏輯

三、AOP實現概述

第二章 (AOP應用情景)簡單地列舉了AOP在三種應用情景中達到的效果,下面介紹AOP的實現,實現的大致流程如下圖所示:

圖3-1 Android AOP實現流程

關鍵有以下幾點:

A、字節碼插樁入口(圖3-1 中1,3兩個環節)。

我們知道Android程序從Java源代碼到可執行的Apk包,中間有(但不止有)兩個環節:

  • javac:將源文件編譯成class格式的文件
  • dex:將class格式的文件彙總到dex格式的文件中

我們要想對字節碼進行修改,只需要在javac之後,dex之前對class文件進行字節碼掃描,並按照一定規則進行過濾及修改就可以了,這樣修改過後的字節碼就會在後續的dex打包環節被打到apk中,這就是我們的插樁入口(更具體的後面還會詳述)。

B、bytecode manipulate(上圖3-1 中第二個環節),這個環節主要做:

  1. 字節碼掃描,並按照一定規則進行過濾出哪些類的class文件需要進行字節碼修改
  2. 對篩選出來的類進行字節碼修改操作

最後B步驟修改過字節碼的class文件,將連同資源文件,一起打入Apk中,得到最終可以在Android平臺可以運行的APP。

下面分別就插樁入口和ASM字節碼操作兩個方面進行詳述。

四、插樁入口

如 第三章(AOP實現概述)所述,我們在Android 打包流程的javac之後,dex之前獲得字節碼插樁入口。

4.1 Android打包流程說明

完整的Android 打包流程如下圖所示:

圖4-1 Android打包流程

說明:

  • 圖4-1中“dex”節點,表示將class文件打包到dex文件的過程,其輸入包括1.項目java源文件經過javac後生成的class文件以及2.第三方依賴的class文件兩種,這些class文件都是我們進行字節碼掃描以及修改的目標。

  • 具體來說,進行圖4-1中dex任務是一個叫dx.jar的jar包,存在於Android SDK的sdk/build-tools/22.0.1/lib/dx.jar目錄中,通過類似 :

    java dx.jar com.android.dx.command.Main --dex --num-threads=4 —-output output.jar input.jar

    的命令,進行將class文件打包爲dex文件的步驟。

  • 從上面的演示命令可以看出,dex任務是啓動一個java進程,執行dx.jar中com.android.dx.command.Main類(當然對於multidex的項目入口可能不是這個類,這個再說)的main()方法進行dex任務,具體完成class到dex轉化的是這個方法:

private static boolean processClass(String name,byte[] bytes) {
      //內容省略
}

方法processClass的第二個參數是一個byte[],這就是class文件的二進制數據(class文件是一種緊湊的8位字節的二進制流文件, 各個數據項按順序緊密的從前向後排列, 相鄰的項[包括字節碼指令]之間沒有間隙),我們就是通過對這個二進制數據進行掃描,按照一定規則過濾以及字節碼修改達到第二部分所描述的AOP情景。

4.2 插樁入口

那麼我們怎麼獲得插樁入口呢?

入口一:transform api

對於Android Gradle Plugin 版本在1.5.0及以上的情況,Google官方提供了transformapi用作字節碼插樁的入口。此處的Android Gradle Plugin 版本指的是build.gradle dependencies的如下配置:

compile 'com.android.tools.build:gradle:1.5.0'

此處1.5.0即爲Android Build Gradle Plugin 版本。

關於transform api如何使用就不詳細介紹了,

  1. 可自行查看 API

  2. 參考熱修復項目Nuwa的 gradle插樁插件 (使用transfrom api實現)

入口二:hook dx.jar

那麼對於Android Build Gradle Plugin 版本在1.5.0以下的情況呢?

下面我們介紹一種不依賴transform api而獲得插樁入口的方法,暫且稱爲 hook dx.jar吧。

提示:具體使用可以考慮綜合這兩種方式,首先檢查build環境是否支持transform api(反射檢查類com.android.build.gradle.BaseExtension是否有registerTransform這個方法即可)然後決定使用哪種方式的插樁入口。

4.3 hook dx.jar獲得插樁入口

hook dx.jar 即是在圖4-1中的dex步驟進行hook,具體來講就是hook 4.1節介紹的dx.jar中com.android.dx.command.Main.processClass方法,將這個方法的字節碼更改爲:

private static boolean processClass(String name,byte[] bytes) {

  bytes=掃描並修改(bytes);// Hook點

  //原有邏輯省略

}

注:這種方式獲得插樁入口也可參見博客 《APM之原理篇》

如何在一個標準的java進程(記得麼?dex任務是啓動一個java進程,執行dx.jar中com.android.dx.command.Main類的main()方法進行dex任務)中對特定方法進行字節碼插樁?

這就需要運用Java1.5引入的Instrumentation機制。

java Instrumentation

java Instrumentation指的是可以用獨立於應用程序之外的代理(agent)程序來監測和協助運行在JVM上的應用程序。這種監測和協助包括但不限於獲取JVM運行時狀態,替換和修改類定義等。

Instrumentation 的最大作用就是類定義的動態改變和操作。

Java Instrumentation兩種使用方式:

  • 方式一(java 1.5+):

    開發者可以在一個普通 Java 程序(帶有 main 函數的 Java 類)運行時,通過 – javaagent 參數指定一個 特定的 jar 文件(agent.jar) (包含 Instrumentation 代理)來啓動 Instrumentation 的代理程序。例如:

    java -javaagent agent.jar  dex.jar  com.android.dx.command.Main  --dex …........

    如此,則在目標main函數執行之前,執行agent jar包指定類的 premain方法 :

    premain(String args, Instrumentation inst)
  • 方式二(java 1.6+):

    VirtualMachine.loadAgent(agent.jar)
    VirtualMachine vm = VirtualMachine.attach(pid);
    vm.loadAgent(jarFilePath, args);

    此時,將執行agent jar包指定類的 agentmain方法:

    agentmain(String args, Instrumentation inst)

說明:

  • 關於上述代碼中出現的agent.jar?

    這裏的agent就是一個包含一些指定信息的jar包,就像OSGI的插件jar包一樣,在jar包的META-INF/MANIFEST.MF中添加如下信息:

    Manifest-Version: 1.0
    Agent-Class: XXXXX
    Premain-Class: XXXXX
    Can-Redefine-Classes: true
    Can-Retransform-Classes: true

    這個jar包就成了agent jar包,其中Agent-Class指向具有agentmain(String args, Instrumentation inst)方法的類,Premain-Class指向具有premain(String args, Instrumentation inst)的類。

  • 關於premain(String args, Instrumentation inst)?

    第二個參數,Instumentation 類有個方法

    addTransformer(ClassFileTransformer transformer,boolean canRetransform)

    而一旦爲Instrumentation inst添加了ClassFileTransformer:

    ClassFileTransformer c=new ClassFileTransformer()
    inst.addTransformer(c,true);

    那麼以後這個jvm進程中再有 任何類的加載定義 ,都會出發此ClassFileTransformer的transform方法

    byte[] transform(  ClassLoader loader,String className,Class classBeingRedefined,ProtectionDomain protectionDomain,byte[] classfileBuffer)throwsIllegalClassFormatException;

    其中,參數byte[] classfileBuffer是類的class文件數據,對它進行修改就可以達到在一個標準的java進程中對特定方法進行字節碼插樁的目的。

hook dx.jar獲得插樁入口的完整流程

完整流程如下圖所示:

圖4-2 hook dx.jar流程圖

注:apply plugin: 'bytecodeplugin'中的bytecodeplugin是我們用於字節碼插樁的gradle插件

A. 通過任意方式(as界面內點擊/命令gradle build等)都會啓動圖4-2所描述的build流程。

B. 通過Java Instrumentation機制,爲獲得插樁入口,對於apk build過程進行了兩處插樁(即hook),圖4-2中標紅部分:

  • 在build進程,對ProcessBuilder.start()方法進行插樁

    ProcessBuilder類是J2SE 1.5在java.lang中新添加的一個新類,此類用於創建操作系統進程,它提供一種啓動和管理進程的方法,start方法就是開始創建一個進程,對它進行插樁,使得通過下面方式啓動dx.jar進程執行dex任務時:

    java  dex.jar  com.android.dx.command.Main  --dex …........

    增加參數 -javaagent agent.jar ,使得dex進程也可以使用Java Instrumentation機制進行字節碼插樁

  • 在dex進程

    對我們的目標方法com.android.dx.command.Main.processClasses進行字節碼插入,從而實現打入apk的每一個項目中的類都按照我們制定的規則進行過濾及字節碼修改。

C. 圖4-2左側build進程使用Instrumentation的方式時之前敘述過的VirtualMachine.loadAgent方式(方式二),dex進程中的方式則是-javaagent agent.jar方式(方式一)。

由此,我們獲得了進行字節碼插樁的入口,下面我們就使用ASM庫的API,對項目中的每一個類進行掃描,過濾,及字節碼修改。

五、bytecode manipulation

在這一部分我們以第二部分描述的情景二的應用場景爲例,對View.OnClickListener的onClick方法進行字節碼修改。在實踐bytecode manipulation時需要一些關於字節碼以及ASM的基礎知識需要了解。因此本部分組織結構如下:

  • 首先介紹一下我們用來操縱字節碼的類庫ASM
  • 然後介紹一些關於字節碼的基本知識
  • 最後實踐對View.OnClickListener的onClick方法進行bytecode manipulation

5.1 ASM庫簡要介紹

簡介

ASM是一個java字節碼操縱框架,它能被用來動態生成類或者增強既有類的功能。ASM 可以直接產生二進制 class 文件,也可以在類被加載入 Java 虛擬機之前動態改變類行爲。類似功能的工具庫還有javassist,BCEL等。

那麼爲什麼選擇ASM呢?

ASM與同類工具庫(這裏以javassist爲例)相比:

A. 較難使用,API非常底層,貼近字節碼層面,需要字節碼知識及虛擬機相關知識

B. ASM更快更高效,Javassist實現機制中包括了反射,所以更慢。下表是使用不同工具庫生成同一個類的耗時比較

Framework First time Later times
Javassist 257 5.2
BCEL 473 5.5
ASM 62.4 1.1

C. ASM庫更加強大靈活,比如可以感知細到字節碼指令層次(第二部分情景三中的場景)

總結起來,ASM雖然不太容易使用,但是功能強大效率高值得挑戰。

關於ASM庫的使用可以 參考手冊 ,下面對其API進行簡要介紹:

ASM API簡介

ASM(core api) 按照 visitor模式 按照 class文件結構 依次訪問class文件的每一部分,有如下幾個重要的visitor。

ClassVisitor

按照class文件格式,按次序訪問類文件每一部分,如下:

public abstract class ClassVisitor {
public ClassVisitor(int api);
public ClassVisitor(int api, ClassVisitor cv);
public void visit(int version, int access, String name,
String signature, String superName, String[] interfaces); public void visitSource(String source, String debug);
public void visitOuterClass(String owner, String name, String desc); AnnotationVisitor visitAnnotation(String desc, boolean visible); public void visitAttribute(Attribute attr);
public void visitInnerClass(String name, String outerName,
String innerName, int access);
public FieldVisitor visitField(int access, String name, String desc,
String signature, Object value);
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions); void visitEnd();
}

與之對應的class文件格式爲:

圖5-1 class文件格式

重點看ClassVisitor的如下幾個方法:

  • visit:按照圖5-1中描述的 class文件格式 ,讀出“class類名”(this_class的指向),“父類名”(super_class的指向),“實現的接口(數組)”(interfaces的指向)等信息
  • visitField:訪問字段,即訪問圖5-1 class文件格式 中的“field_info”,訪問字斷的邏輯委託給另外一種visitor(FieldVisitor)
  • visitField:訪問方法,即訪問圖5-1 class文件格式 中的“method_info”,訪問方法的邏輯委託給另外一種visitor(MethodVisitor)

其他方法可參考前面推薦的ASM手冊,下面介紹一下負責訪問方法的MethodVisitor。

MethodVisitor

按以下次序訪問一個方法:

visitAnnotationDefault?
( visitAnnotation | visitParameterAnnotation | visitAttribute )* 
  ( visitCode
    ( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn | visitLocalVariable | visitLineNumber )*
  visitMaxs )? 
visitEnd

注:上述出現的“*”表示出現“0+”次,“?”表示出現“0/1”次。 含義可類比正則式元字符。

下面說明幾個比較關鍵的visit方法:

  • visitCode():開始訪問方法體內的代碼
  • visitTryCatchBlock:訪問方法的try catch block
  • visitLocalVariable:指令,訪問局部變量表裏面的某個局部變量(關於局部變量表後面會有介紹)
  • visitXxxInsn:指令,表示class文件方法體裏面的字節碼指令(如:IADD,ICONST_0,ARETURN等等字節碼指令),完整的字節碼指令表可參考 維基百科
  • visitLabel(Label label):如果方法體中有跳轉指令,字節碼指令中會出現label,所謂label可以近似看成行號的標記(並不是),指示跳轉指令將要跳轉到哪裏
  • visitFrame:記錄當前棧幀(棧幀結構將在後面有介紹)狀態,用於Class文件加載時的校驗
  • visitMaxs:指定當前方法的棧幀中,局部變量表和操作數棧的大小。(java棧大小是javac之後就確定了的)

簡單介紹了asm庫後,由於使用ASM還需要對字節碼有一定的瞭解,故在實踐之前再介紹一些關於字節碼的基礎知識:

5.2 字節碼基礎

概念

關於字節碼,有以下概念定義比較重要:

  • 全限定名(Internal names):
    全限定名即爲全類名中的“.”,換爲“/”,舉例:
    類android.widget.AdapterView.OnItemClickListener的全限定名爲:
    android/widget/AdapterView$OnItemClickListener
  • 描述符(descriptors):
    1.類型描述符,如下圖所示:

圖5-2 java類型描述符

如圖5-2所示,在class文件中類型 boolean用“Z”描述,數組用“[”描述(多維數組可疊加),那麼我們最常見的自定義引用類型呢?“L全限定名;”.例如:

Android中的android.view.View類,描述符爲“Landroid/view/View;”

2.方法描述符的組織結構爲:

(參數類型描述符)返回值描述符

其中無返回值void用“V”代替,舉例:

方法boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id)     的描述符如下:
(Landroid/widget/ExpandableListView;Landroid/view/View;IJ)Z

執行引擎

jvm執行引擎用於執行字節碼,如下圖

圖5-3 字節碼執行引擎棧幀結構

如圖5-3所示,縱向來看有三個線程,其中每一個線程內部都有一個棧結構(即通常所說的“堆棧”中的虛擬機棧),棧中的每一個元素(一幀)稱爲一個棧幀(stack frame)。棧幀與我們寫的方法一一對應,每個方法的調用/return對應線程中的一個棧幀的入棧/出棧。

方法體中各種字節碼指令的執行都在棧幀中完成,下面介紹下棧幀中兩個比較重要的部分:

  • 局部變量表:

    故名思義,存儲當前方法中的局部變量,包括方法的入參。值得注意的是局部變量表的第一個槽位存放的是this。還拿方法onGroupClick舉例:

    boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id)

    剛進入此方法時,局部變量表的槽位狀態如下:

Slot Number value
0 this
1 ExpandableListView parent
2 View v
3 int groupPosition
4 long id
  • 操作數棧:

    字節碼指令執行的工作臺。下面用指令iadd(int類型加)執行時操作數棧的變化進行舉例:

圖5-4 執行iadd指令時操作數棧的狀態變化

例如,方法體中有語句如下:

1+1
  • 在執行iadd之前需要先壓兩個“1”到操作數棧(因爲iadd指令需要兩個操作數,執行後產生一個操作數)
  • 從常量池中(“1”爲int常量)經過兩個iconst_1後操作數棧的狀態如圖5-4中所示“操作數棧狀態1”
  • 執行iadd,將兩個“1”彈出,交給ALU相加,把結果“2”入棧,操作數棧的狀態如圖5-4中所示“操作數棧狀態2”

5.3 bytecode manipulation實踐

我們來實踐第二部分情景二描述的AOP,即修改所有View.OnClickListener的OnClick方法的字節碼。流程如下圖所示:

圖5-5 AOP 控件點擊實現流程

對上圖中三個步驟的詳細說明:

步驟一:

ASM的ClassVisitor對所有類的class文件進行掃描,在visit方法中得到當前類實現了哪些接口,判斷這些接口中是否包含全限定名爲“android/view/View$OnClickListener”的接口。如果有,證明當前類是View.OnClickListener,進行步驟二,否則終止掃描;

步驟二:

ClassVisitor每掃描到一個方法時,在visitMethod中進行如下判定:

  1. 此方法的名字是否爲"onClick"
  2. 此方法的描述符是否爲"(Landroid/view/View;)V"

如果全部判定通過,則證明本次掃描到的方法是View.OnClickListener的onClick方法,然後將

將掃描邏輯交給MethodVisitor,進行字節碼的修改(步驟三)。

步驟三:修改onClick方法的字節碼

假設待修改的onClick方法如下:

public void onClick(View v) {
        System.out.println("test");//代表方法中原有的代碼(邏輯)
}

修改之後需要變成:

public void onClick(View v) {
        if(!Monitor.onViewClick(v)) {
            System.out.println("test");//代表方法中原有的代碼(邏輯)
        }
    }

即:

進入方法之後先執行Monitor.onViewClick(v)(裏面是數據收集邏輯),然後根據返回值決定是執行原有onClick方法內的邏輯,還是說直接返回。下面是修改之後onClick方法的字節碼:

public onClick(Landroid/view/View;)V
    ALOAD 1//插入的字節碼,將index爲1的局部變量(入參v)壓入操作數棧
    INVOKESTATIC com/netease/lede/bytecode/monitor/Monitor.onViewClick (Landroid/view/View;)Z//插入的字節碼,調用方法Monitor.onViewClick(v),將返回值(true/false)壓入操作數棧
    IFEQ L0//插入的字節碼,如果操作數棧棧頂爲0(if條件爲false),則跳轉到lable L0,執行原有邏輯
    RETURN//插入的字節碼,上條指令判斷不滿足(即操作數棧棧頂爲1(true)),直接返回
   L0
    LINENUMBER 11 L0
   FRAME SAME
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "test"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 12 L1
    RETURN
   L2
    LOCALVARIABLE this Lcom/netease/caipiao/datacollection/bytecode/ViewOnclickListener; L0 L2 0
    LOCALVARIABLE v Landroid/view/View; L0 L2 1
    MAXSTACK = 2//操作數棧最大爲2
    MAXLOCALS = 2//局部變量表最大爲2

如上圖所示,插入的字節碼主要是 前面四行(圖中已經用註釋的形式做了標記) ,圖中的字節碼指令可以參照下表:

字節碼指令 說明 指令入參
ALOAD 將引用類型的對象從局部變量表load到操作數棧 局部變量表index
INVOKESTATIC 調用類方法(即靜態方法) 1.類全限定名 2.方法描述符
INVOKEVIRTUAL 調用對象方法 1.類全限定名 2.方法描述符
IFEQ 檢查操作數棧棧定位置是否爲0 跳轉Lable(棧頂爲0時跳轉)
RETURN 無返回值返回(操作數棧無彈棧操作)  
IRETURN 返回int值(操作數棧將棧頂int值彈棧)  
GETSTATIC 獲取類字段(靜態成員變量) 1.類全限定名,2.字段類型描述符
LDC 從常量池取int,float,String等常量到操作數棧頂 常量值
MAXSTACK 操作數棧最大容量(javac編譯時確定)  
MAXLOCALS 局部變量表最大容量(javac編譯時確定)

具體插入的代碼是字節碼代碼的前四行,邏輯比較簡單:

  1. 進入方法之後先執行Monitor.onViewClick(v)
    ALOAD 1:將index爲1的局部變量(入參v)壓入操作數棧
    INVOKESTATIC com/netease/lede/bytecode/monitor/Monitor.onViewClick (Landroid/view/View;)Z:
    調用方法Monitor.onViewClick(v)(消耗ALOAD 1壓入的操作數),並將返回值(true/false)壓入操作數棧
  2. 根據返回值決定跳轉
    IFEQ L0:
    如果操作數棧棧頂爲0(if條件爲false),則跳轉到lable L0,執行原有邏輯
    RETURN:上條指令判斷不滿足(即操作數棧棧頂爲1(true)),直接返回

注:值得注意的是MAXSTACK,MAXLOCALS 兩個值在javac生成的class文件就已經固定,即,棧內存大小已經確定(有別於堆內存可以在運行時動態申請/釋放)。

如此,經過上述三個步驟,我們完成了第二部分情景二描述的AOP實踐。

六、總結

文章寫的比較長,下面對主要的幾點進行總結:

首先介紹了AOP的概念,已及在Android平臺的主流框架,面對無埋點數據收集的需求,這些現有的都不太合適因此需要自己動手實現,

然後,簡單列舉了無埋點數據收集SDK中需要AOP的應用情景

最後介紹了實現的技術細節,主要有兩點:

  1. 通過hook dx.jar的方式獲得插樁入口(可以和transfrom api配合使用)
  2. 使用ASM庫修改字節碼,此部分簡要介紹了關於字節碼的一些基本概念以及執行引擎,最後以View.OnClickListener爲例進行了實踐。

 

來自:http://www.jianshu.com/p/c202853059b4

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章