Android AOP之字節碼插樁


title: Android AOP之字節碼插樁
author: 陶超
description: 實現數據收集SDK時,為了實現非侵入的,全量的數據采集,采用了AOP的思想,探索和實現了一種Android上AOP的方式。本文基于數據收集SDK的AOP實現總結而成。
categories: Android
date: 2017/02/11
tags:

  • Android AOP
  • 字節碼
  • java
  • bytecode
  • 數據收集

背景

本篇文章基于《網易樂得無埋點數據收集SDK》總結而成,關于網易樂得無埋點數據采集SDK的功能介紹以及技術總結后續會有文章進行闡述,本篇單講SDK中用到的Android端AOP的實現。

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

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

目錄

<h1 id="1">一、Android AOP</h1>

<h2 id="1.1">1.1 什么是AOP</h2>

面向切向編程(Aspect Oriented Programming),相對于面向對象編程(ObjectOriented Programming)而言。
??OOP的精髓是把功能或問題模塊化,每個模塊處理自己的家務事。但在現實世界中,并不是所有問題都能完美得劃分到模塊中,有些功能是橫跨并嵌入眾多模塊里的,比如下圖所示的例子。

圖1-1 AOP概念說明示例

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

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

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

<h2 id="1.2">1.2 Android AOP方式概述</h2>

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字節碼)

<h2 id="1.3">1.3 Android AOP方式對比選擇</h2>

Dexposed,Xposed的缺陷很明顯,xposed需要root權限,Dexposed只對部分系統版本有效。
??與之相比aspactJ沒有這些缺點,但是aspactJ作為一個AOP的框架來講對于我們來講太重了,不僅方法數大增,而且還有一堆aspactJ的依賴要引入項目中(這些代碼定義了aspactJ框架諸如切點等概念)。更重要的是我們的目標僅僅是按照一些簡單的切點(用戶點擊等)收集數據,而不是將整個項目開發從OOP過渡到AOP。
??AspactJ對于我們想要實現的數據收集需求太重了,但是這種編譯期操作class文件字節碼實現AOP的方式對我們來說是合適的。
??因此我們實現Android上AOP的方式確定為:

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

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

<h1 id="2">二、AOP應用情景</h1>

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

<h2 id="2.1">2.1 Fragment生命周期</h2>

說明

收集頁面數據時發現有些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進行了字節碼修改

<h2 id="2.2">2.2 用戶點擊事件</h2>

說明

點擊事件是分析用戶行為的一個重要事件,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才會忽略原有邏輯

<h2 id="2.3">2.3 彈窗事件</h2>

說明

彈窗顯示/關閉事件,當然彈窗的實現可以是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()還進行一些數據收集邏輯

<h1 id="3">三、AOP實現概述</h1>

第二章 (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字節碼操作兩個方面進行詳述。

<h1 id="4">四、插樁入口</h1>

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

<h2 id="4.1">4.1 Android打包流程說明</h2>

完整的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情景。

<h2 id="4.2">4.2 插樁入口</h2>

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

入口一: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這個方法即可)然后決定使用哪種方式的插樁入口。

<h2 id="4.3">4.3 hook dx.jar獲得插樁入口</h2>

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,對項目中的每一個類進行掃描,過濾,及字節碼修改。

<h1 id="5">五、bytecode manipulation</h1>

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

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

<h2 id="5.1">5.1 ASM庫簡要介紹</h2>

簡介

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還需要對字節碼有一定的了解,故在實踐之前再介紹一些關于字節碼的基礎知識:

<h2 id="5.2">5.2 字節碼基礎</h2>

概念

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

  • 全限定名(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”

<h2 id="5.3">5.3 bytecode manipulation實踐</h2>

我們來實踐第二部分情景二描述的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實踐。

<h1 id="6">六、總結</h1>

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

首先介紹了AOP的概念,已及在Android平臺的主流框架,面對無埋點數據收集的需求,這些現有的都不太合適因此需要自己動手實現,
??然后,簡單列舉了無埋點數據收集SDK中需要AOP的應用情景
??最后介紹了實現的技術細節,主要有兩點:

  1. 通過hook dx.jar的方式獲得插樁入口(可以和transfrom api配合使用)
  2. 使用ASM庫修改字節碼,此部分簡要介紹了關于字節碼的一些基本概念以及執行引擎,最后以View.OnClickListener為例進行了實踐。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容