Android AOP之字節碼插樁


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

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

背景

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

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

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

目錄

1、Android AOP

1.1 什麼是AOP

  面向切向編程(Aspect Oriented Programming),相對於面向對象編程(ObjectOriented Programming)而言。
  OOP的精髓是把功能或問題模塊化,每一個模塊處理本身的家務事。但在現實世界中,並非全部問題都能完美得劃分到模塊中,有些功能是橫跨並嵌入衆多模塊裏的,好比下圖所示的例子。編程

圖1-1 AOP概念說明示例

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

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

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

1.2 Android AOP方式概述

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

  • 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的場景。

2、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()還進行一些數據收集邏輯

3、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字節碼操做兩個方面進行詳述。

4、插樁入口

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

5、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實踐。

6、總結

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

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

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