Android 中使用ASM,對Activity生命周期打點統計

介紹ASM

ASM是一款基于java字節碼層面的代碼分析和修改工具。無需提供源代碼即可對應用嵌入所需debug代碼,用于應用API性能分析。ASM可以直接產生二進制class文件,也可以在類被加入JVM之前動態修改類行為。

ASM庫結構

Paste_Image.png
  • Core 為其他包提供基礎的讀、寫、轉化Java字節碼和定義的API,并且可以生成Java字節碼和實現大部分字節碼的轉換
  • Tree提供了Java字節碼在內存中的表現
  • Analysis為存儲在tree包結構中的java方法字節碼提供基本的數據流統計和類型檢查算法
  • Commons提供一些常用的簡化字節碼生成轉化和適配器
  • Util包含一些幫助類和簡單的字節碼修改,有利于在開發或者測試中使用
  • XML提供一個適配器將XML和SAX-comliant轉化成字節碼結構,可以允許使用XSLT去定義字節碼轉化。

class文件結構

ASM 是基于java字節碼層面的代碼分析和修改工具。所以學習ASM之前,還得不下class文件結構,java類型,java方法等知識
Class文件結構如下:

| Header|
| --------- ------ |
| Modifiers, name, super class, interfaces |
| Constant pool: numeric, string and type constants |
| Source file name (optional) |
| Enclosing class reference |
| Annotation* |
| Attribute* |

member attribute
Inner class* Name
Field* Modifiers, name, type
Annotation*
Attribute*
Method* Modifiers, name, return and parameter types
Annotation*
Attribute*
Compiled code

翻譯成中文:

| Header|
| --------- ------ |
| Modifiers, name, super class, interfaces 修飾(public/private等),名稱,父類,實現的接口 |
| Constant pool: numeric, string and type constants 常量池,數字,字符串,類型常量(枚舉類型) |
| Source file name (optional) 原文件名稱,(可選) |
| Enclosing class reference 外部類的引用 |
| Annotation* Class的注解 |
| Attribute* Class屬性 |

member attribute
Inner class* 內部類 Name 名稱
Field* 成員變量 Modifiers, name, type 修飾符,名稱,類型
Annotation* 注解
Attribute* 屬性
Method* 方法 Modifiers, name, return and parameter types Modifiers, name, return and parameter types 修飾符,名稱,返回類型,參數類型
Annotation* 注解
Attribute* 類型
Compiled code 編譯的代碼
  • 每個類、字段、方法和方法代碼的屬性有屬于自己的名稱記錄在類文件格式的JVM規范的部分,這些屬性展示了字節碼多方面的信息,例如源文件名、內部類、簽名、代碼行數、本地變量表和注釋。JVM規范允許定義自定義屬性,這些屬性會被標準的VM(虛擬機)忽略,但是可以包含附件信息。
  • 方法代碼表包含一系列對java虛擬機的指令。有些指令在代碼中使用偏移量,當指令從方法代碼被插入或者移除時,全部偏移量的值可能需要調整。

原java類型與class文件內部類型對應關系

Java type Type descriptor
boolean Z
char C
byte B
short S
int I
float F
long J
double D
Object Ljava/lang/Object;
int[] [I
Object[][] [[Ljava/lang/Object;

原java方法聲明與class文件內部聲明的對應關系

Method declaration in source file Method descriptor
void m(int i, float f) (IF)V
int m(Object o) (Ljava/lang/Object;)I
int[] m(int i, String s) (ILjava/lang/String;)[I
Object m(int[] i) ([I]Ljava/lang/Object;

參數描述在前面,返回值描述在后面

ASM的處理流程,生產者消費者模式

在Core包中邏輯上分為2部分:

  • 字節碼生產者,例如ClassReader
  • 字節碼消費者,例如writers(ClassWriter, FieldWriter, MethodWriter和AnnotationWriter),adapters(ClassAdapter和MethodAdapter)
    下圖是生產者和消費者交互的時序圖:
    官網提供的時序圖:
Paste_Image.png

網友畫的時序圖:

Paste_Image.png

通過時序圖可以看出ASM在處理class文件的整個過程。ASM通過樹這種數據結構來表示復雜的字節碼結構,并利用Push模型來對樹進行遍歷。

  • ASM中提供一個ClassReader類,調用accept方法,接受一個實現了抽象類ClassVisitor的對象實例作為參數,然后依次調用ClassVisitor的各個方法。字節碼空間上的偏移被轉成各種visitXXX方法。使用者只需要在對應的的方法上進行需求操作即可,無需考慮字節偏移。
  • 這個過程中ClassReader可以看作是一個事件生產者,ClassWriter繼承自ClassVisitor抽象類,負責將對象化的class文件內容重構成一個二進制格式的class字節碼文件,ClassWriter可以看作是一個事件的消費者。

示例:攔截Android中 Activity生命周期方法,執行的時長。

首先定義一個ActivityTimeManager記錄方法的使用時長,以onCreate方法為例。

public class ActivityTimeManger {
    public static HashMap<String, Long> startTimeMap = new HashMap<>();
    public static void onCreateStart(Activity activity) {
        startTimeMap.put(activity.toString(), System.currentTimeMillis());
    }
    public static void onCreateEnd(Activity activity) {
        Long startTime = startTimeMap.get(activity.toString());
        if (startTime == null) {
            return;
        }
        long coastTime = System.currentTimeMillis() - startTime;
        System.out.println(activity.toString() + " onCreate coast Time" + coastTime);
        startTimeMap.remove(activity.toString());
… …
    }

在Activity編譯的時候,在onCreate方法中,前后各插入ActivityTimeManger. onCreateStart() 和
ActivityTimeManger. onCreateEnd() 方法
原始的Activity,onCreate方法:

public class TestActivity extends Activity{
    
    public void onCreate() {
        System.out.println("onCreate");
    }
}

使用javap –c 命令 查看class文件的字節碼,如下:

public com.test.aop.main.TestActivity();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method android/app/Activity."<init>":()V
       4: return

  public void onCreate();
    Code:
       0: getstatic     #15                 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #21                 // String onCreate
       5: invokevirtual #22                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

加上ActivityTimeManger. onCreateStart(),ActivityTimeManger. onCreateEnd()之后的源碼如下:

public class TestActivity extends Activity{
    public void onCreate() {
        ActivityTimeManger.onCreateStart(this);
        System.out.println("onCreate");
        ActivityTimeManger.onCreateEnd(this);
    }

使用javap –c 命令 查看class文件的字節碼,如下:

public com.test.aop.main.TestActivity();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method android/app/Activity."<init>":()V
       4: return

  public void onCreate();
    Code:
       0: aload_0
       1: invokestatic  #15                 // Method com/test/aop/tools/ActivityTimeManger.onCreateStart:(Landroid/app/Activity;)V
       4: getstatic     #21                 // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #27                 // String onCreate
       9: invokevirtual #28                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: aload_0
      13: invokestatic  #34                 // Method com/test/aop/tools/ActivityTimeManger.onCreateEnd:(Landroid/app/Activity;)V
      16: return

紅色部分是增加ActivityTimeManger. onCreateStart(),ActivityTimeManger. onCreateEnd()2個方法后,增加的字節碼。
所以我們怎么使用ASM,對class文件進行修改。把紅色部分的字節碼插入到class文件中呢?
先輸入文件。把class文件重命名為.opt文件,修改完后,再重命名回去。

public static void processClass(File file) {
        System.out.println("start process class " + file.getPath());
        File optClass = new File(file.getParent(), file.getName() + ".opt");
        FileInputStream inputStream = null;
        FileOutputStream outputStream = null;
        try {
            inputStream = new FileInputStream(file);
            outputStream = new FileOutputStream(optClass);
            byte[] bytes = referHack(inputStream);
            outputStream.write(bytes);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
        if (file.exists()) {
            file.delete();
        }
        optClass.renameTo(file);
    }

referHack 方法

    private static byte[] referHack(InputStream inputStream) {
        try {
            ClassReader classReader = new ClassReader(inputStream);
            ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
            ClassVisitor changeVisitor = new ChangeVisitor(classWriter);
            classReader.accept(changeVisitor, ClassReader.EXPAND_FRAMES);
            return classWriter.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
        }
        return null;
    }

創建ClassReader,生產者,讀出class字節碼,輸出給ClassWriter消費。
自定義ChangeVisitor 來處理class字節碼。

    public static class ChangeVisitor extends ClassVisitor {
   // 記錄文件名 
    private String owner;
        private ActivityAnnotationVisitor fileAnnotationVisitor = null;
        public ChangeVisitor(ClassVisitor cv) {
            super(Opcodes.ASM5, cv);
        }

        @Override
        public void visit(int version, int access, String name, String signature, String superName,
                String[] interfaces) {
            super.visit(version, access, name, signature, superName, interfaces);
            this.owner = name;
        }

        @Override
// 處理class文件的注解
        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
            System.out.println("visitAnnotation: desc=" + desc + " visible=" + visible);
            AnnotationVisitor annotationVisitor = super.visitAnnotation(desc, visible);
            if (desc != null) {
// 如果注解不是空,傳遞給ActivityAnnotationVisitor處理。
                fileAnnotationVisitor = new ActivityAnnotationVisitor(Opcodes.ASM5, annotationVisitor, desc);
                return FileAnnotationVisitor;
            }
            return annotationVisitor;
        }
        
        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
// 獲取到原始的MethodVisitor
            MethodVisitor mv = this.cv.visitMethod(access, name, desc, signature, exceptions);
// 如果文件的注解不為空,說明文件要進行修改。則創建RedefineAdvice,修改方法
            if (fileAnnotationVisitor!= null) {
                return new RedefineAdvice(mv, access, owner, name, desc);
            }
            return mv;
        }
    }

ChangeVisitor,繼承ClassVisitor,class文件的訪問,可以重寫
visitAnnotation(), 獲取或者修改注解
visitMethod(),獲取或者修改方法
visitField(),獲取或者修改成員變量
這段代碼的邏輯是:先判斷這個class文件是否有注解。如果有注解,則先解析注解。如果注解不為空,則說明有方法需要修改則創建RedefineAdvice,訪問和修改方法。
看下ActivityAnnotationVisitor,對注解的訪問和解析。

public static class ActivityAnnotationVisitor extends AnnotationVisitor {
        public String desc;
        public String name;
        public String value;

        public ActivityAnnotationVisitor(int api, AnnotationVisitor av, String paramDesc) {
            super(api, av);
            this.desc = paramDesc;
        }

        public void visit(String paramName, Object paramValue) {
            this.name = paramName;
            this.value = paramValue.toString();
            System.out.println("visitAnnotation: name=" + name + " value=" + value);
        }

    }

記錄注解的名稱和值,描述。
RedefineAdvice,對方法的修改

public static class RedefineAdvice extends AdviceAdapter {
        String owner = "";
        ActivityAnnotationVisitor activityAnnotationVisitor = null;
        protected RedefineAdvice(MethodVisitor mv, int access, String className, String name, String desc) {
            super(Opcodes.ASM5, mv, access, name, desc);
            owner = className;
        }

        @Override
        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
            System.out.println("visitAnnotation: desc=" + desc + " visible=" + visible);
            AnnotationVisitor annotationVisitor = super.visitAnnotation(desc, visible);
// 先判斷方法上是否有注解,如果有注解,則使用ActivityAnnotationVisitor解析注解
            if (desc != null) {
                activityAnnotationVisitor = new ActivityAnnotationVisitor(Opcodes.ASM5, annotationVisitor, desc);
                return activityAnnotationVisitor;
            }
            return annotationVisitor;
        }

        @Override
// 修改方法入口,在方法執行前,插入字節碼      
protected void onMethodEnter() {
            if (activityAnnotationVisitor == null) {
                return;
            }
            super.onMethodEnter();
//插入字節碼,ALOAD
            mv.visitVarInsn(ALOAD, 0);
//插入字節碼INVOKESTATIC,調用ActivityTimeManger.onCreateStart().
// onCreate使用注解寫入
            mv.visitMethodInsn(INVOKESTATIC, "com/test/aop/tools/ActivityTimeManger",
                    activityAnnotationVisitor.value+"Start",
                    "(Landroid/app/Activity;)V");
        }

//在方法執行結束前,插入字節碼
        @Override
        protected void onMethodExit(int opcode) {
            if (activityAnnotationVisitor == null) {
                return;
            }
            super.onMethodExit(opcode);
//插入字節碼,ALOAD
            mv.visitVarInsn(ALOAD, 0);
//插入字節碼INVOKESTATIC,調用ActivityTimeManger.onCreateEnd().
// onCreate使用注解寫入
            mv.visitMethodInsn(INVOKESTATIC, "com/test/aop/tools/ActivityTimeManger", 
                    activityAnnotationVisitor.value+"End",
                    "(Landroid/app/Activity;)V");
        }
    }

整段代碼邏輯是:
先查找方法上的注解,如果方法上有注解,則獲取注解的value。在方法執行前后,插入字節碼。通過重寫onMethodEnter和onMethodExit方法。
所以在原來的TestActivity上,增加注解class注解和方法注解,然后通過processClass()處理,就能在記錄Activity方法執行的時間。

@FileAnnotation("TestActivity")
public class TestActivity extends Activity{
    
    @ActivityAnnotation("onCreate")
    public void onCreate() {
        System.out.println("onCreate");
    }

編譯后的class文件,反編譯后,結果如下:

@FileAnnotation
public class TestActivity extends Activity {
    @ActivityAnnotation
    public void onCreate() {
        ActivityTimeManger.onCreateStart(this);
        System.out.println("onCreate");
        ActivityTimeManger.onCreateEnd(this);
    }
}

備注:Android App目前大部分都是通過gradle編譯。所以以上字節碼處理代碼,都需要寫在自定義的gradle插件中,自定義一個Transform處理。
關于怎么自定義gradle插件和Transform ,可以百度,或者google。這里就不在寫了。
以上所有代碼鏈接

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

推薦閱讀更多精彩內容