使用ASM完成編譯時插樁

ASM,是一個跟AspectJ功能相似比AspectJ更強大的編譯時插樁框架。功能雖強大,不過用起來比AspectJ麻煩很多。java

其實這個框架在Java中用的不少,對於Android開發者來講若是以前沒有開發過Java就有點陌生了android

官網 asm.ow2.io/web

  • ASM是一個通用的Java字節碼操做和分析框架,能夠用它來動態的生成類後者加強現有類的功能。
  • ASM能夠直接產生二進制的class文件,也能夠在類被加載到Java虛擬機以前動態改變類的行爲。
  • Java Class的類文件的信息存儲在.class文件中,ASM能夠讀取.class文件中的類信息,改變類行爲,分析類信息,甚至生成新的類。

Andorid java文件打包流程:apache

.java文件->.class文件->.dex文件。想要編譯時插樁通常有兩種方式api

  • 更改java文件:APT,AndroidAnnotation 都是這個層面的dagger,butterknife等框架就是這個層面的應用。
  • 更改class文件:AspectJ,ASM,javassisit等,功能更增強大

下面練習一個小例子,使用ASM來統計Application中onCreate執行的時間。緩存

咱們須要兩大步來完成:bash

第一步拿到全部的.class文件,第二步交給ASM動態插入代碼。服務器

第一步找到class文件

如何能拿到呢?Google官方在Adnroid Gradle1.5.0版本提供了Transform API,容許第三方Plugin在打包dex文件以前的編譯過程當中操做.class文件。因此咱們就可使用Transform,拿到全部的.class文件。app

想要使用Transform API,這時候就得自定義一個Gradle的插件了框架

  • 新建一個項目,而後建一個新的module來寫插件代碼
  • 由於gradle是用groovy寫的,因此須要在main文件夾下在新建一個入口文件夾groovy。
  • 告訴gradle哪一個是咱們自定義的插件,在main目錄下新建resources目錄,而後在resources目錄裏面再新建META-INF目錄,再在META-INF裏面新建gradle-plugins目錄。最後在gradle-plugins目錄裏面新建properties文件
  • properties文件的名字能夠隨便取,後面用到的時候就用這個取好的名字。在properties文件中指明咱們自定義的插件的類implementation-class=com.hsm.asmplugin.AsmPlugin

最後的目錄結構是這樣的

下面去到當前module下面的build.gradle添加相關的依賴

apply plugin: 'groovy'
apply plugin: 'maven'

repositories {
    mavenCentral()
    jcenter()
}
dependencies {
    //gradle sdk
    implementation gradleApi() //groovy sdk implementation localGroovy() implementation 'com.android.tools.build:gradle:3.5.0' //ASM相關依賴 implementation 'org.ow2.asm:asm:7.1' implementation 'org.ow2.asm:asm-commons:7.1' } //uploadArchives是將已經自定義好了插件打包到本地Maven庫裏面去了, // 你也能夠選擇打包到遠程服務器中。其中, // group和version是咱們以後配置插件地址時要用到的。 group='com.chs.asm.plugin'
version='1.0'
uploadArchives {
    repositories {
        mavenDeployer {
            //本地的Maven地址:當前工程下
            repository(url: uri('./my-plugin'))

            //提交到遠程服務器:
            // repository(url: "http://www.xxx.com/repos") {
            // authentication(userName: "admin", password: "admin")
            // }
        }
    }
}
複製代碼

由於打包的時候須要用到maven,因此添加maven相關的依賴,uploadArchives是將已經自定義好了插件打包到本地Maven庫裏面去,也能夠打包到遠程服務器。group和version是使用的時候須要的組名和版本信息。

配置完成了,下面開始寫代碼,在groovy文件夾下寫咱們本身的插件繼承Plugin

package com.hsm.asmplugin

import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.jetbrains.annotations.NotNull
public class AsmPlugin implements Plugin<Project> {

    @Override
    public void apply(@NotNull Project project) {
        def android = project.extensions.getByType(AppExtension)
        println '----------- 開始註冊 >>>>> -----------'
        AsmTransform transform = new AsmTransform()
        android.registerTransform(transform)
    }
}
複製代碼

獲取project中的AppExtension類型extension,而後註冊咱們本身定義的Transform。

啥是AppExtension,咱們app的gradle中最上面都有這個插件apply plugin: 'com.android.application若是依賴了這個插件,AppExtension就存在。

下面來看AsmTransform

package com.hsm.asmplugin

import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import com.chs.asm.LogVisitor
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter

import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry

public class AsmTransform extends Transform {

    // 設置咱們自定義的Transform對應的Task名稱 
    // 編譯的時候能夠在控制檯看到 好比:Task :app:transformClassesWithAsmTransformForDebug
    @Override
    public String getName() {
        return "AsmTransform"
    }
    // 指定輸入的類型,經過這裏的設定,能夠指定咱們要處理的文件類型
    // 這樣確保其餘類型的文件不會傳入
    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }
    // 指定Transform的做用範圍
    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    public boolean isIncremental() {
        return false
    }

    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        long startTime = System.currentTimeMillis()
        println '----------- startTime <' + startTime + '> -----------'
        //拿到全部的class文件
        Collection<TransformInput> inputs = transformInvocation.inputs;
        TransformOutputProvider outputProvider = transformInvocation.outputProvider;
        if (outputProvider != null) {
            outputProvider.deleteAll()
        }
        //遍歷inputs Transform的inputs有兩種類型,一種是目錄,一種是jar包,要分開遍歷
        inputs.each { TransformInput input ->
            //遍歷directoryInputs(文件夾中的class文件) directoryInputs表明着以源碼方式參與項目編譯的全部目錄結構及其目錄下的源碼文件
            // 好比咱們手寫的類以及R.class、BuildConfig.class以及R$XXX.class等
            input.directoryInputs.each { DirectoryInput directoryInput ->
                //文件夾中的class文件
                handDirectoryInput(directoryInput, outputProvider)
            }
            //遍歷jar包中的class文件 jarInputs表明以jar包方式參與項目編譯的全部本地jar包或遠程jar包
            input.jarInputs.each { JarInput jarInput ->
                //處理jar包中的class文件
                handJarInput(jarInput, outputProvider)
            }
        }
    }

    //遍歷directoryInputs 獲得對應的class 交給ASM處理
    private static void handDirectoryInput(DirectoryInput input, TransformOutputProvider outputProvider) {
        //是不是文件夾
        if (input.file.isDirectory()) {
            //列出目錄全部文件(包含子文件夾,子文件夾內文件)
            input.file.eachFileRecurse { File file ->
                String name = file.name
                //須要插樁class 根據本身的需求來------------- 這裏判斷是不是咱們本身寫的Application
                if ("MyApp.class".equals(name)) {
                    ClassReader classReader = new ClassReader(file.bytes)
                    //傳入COMPUTE_MAXS ASM會自動計算本地變量表和操做數棧
                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                    //建立類訪問器 並交給它去處理
                    ClassVisitor classVisitor = new LogVisitor(classWriter)
                    classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
                    byte[] code = classWriter.toByteArray()
                    FileOutputStream fos = new FileOutputStream(file.parentFile.absolutePath + File.separator + name)
                    fos.write(code)
                    fos.close()
                }
            }
        }
        //處理完輸入文件後把輸出傳給下一個文件
        def dest = outputProvider.getContentLocation(input.name, input.contentTypes, input.scopes, Format.DIRECTORY)
        FileUtils.copyDirectory(input.file, dest)
    }
    //遍歷jarInputs 獲得對應的class 交給ASM處理
    private static void handJarInput(JarInput jarInput, TransformOutputProvider outputProvider) {
        if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
            //重名名輸出文件,由於可能同名,會覆蓋
            def jarName = jarInput.name
            def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
            if (jarName.endsWith(".jar")) {
                jarName = jarName.substring(0, jarName.length() - 4)
            }
            JarFile jarFile = new JarFile(jarInput.file)
            Enumeration enumeration = jarFile.entries()
            File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")
            //避免上次的緩存被重複插入
            if (tmpFile.exists()) {
                tmpFile.delete()
            }
            JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
            //用於保存
            while (enumeration.hasMoreElements()) {
                JarEntry jarEntry = (JarEntry) enumeration.nextElement()
                String entryName = jarEntry.getName()
                ZipEntry zipEntry = new ZipEntry(entryName)
                InputStream inputStream = jarFile.getInputStream(jarEntry)
                //須要插樁class 根據本身的需求來-------------
                if ("androidx/fragment/app/FragmentActivity.class".equals(entryName)) {
                    //class文件處理
                    println '----------- jar class <' + entryName + '> -----------'
                    jarOutputStream.putNextEntry(zipEntry)
                    ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                    //建立類訪問器 並交給它去處理
                    ClassVisitor cv = new LogVisitor(classWriter)
                    classReader.accept(cv, ClassReader.EXPAND_FRAMES)
                    byte[] code = classWriter.toByteArray()
                    jarOutputStream.write(code)
                } else {
                    jarOutputStream.putNextEntry(zipEntry)
                    jarOutputStream.write(IOUtils.toByteArray(inputStream))
                }
                jarOutputStream.closeEntry()
            }
            //結束
            jarOutputStream.close()
            jarFile.close()
            //獲取output目錄
            def dest = outputProvider.getContentLocation(jarName + md5Name,
                    jarInput.contentTypes, jarInput.scopes, Format.JAR)
            FileUtils.copyFile(tmpFile, dest)
            tmpFile.delete()
        }
    }
}
複製代碼

上面類上的註釋很清楚啦,Transform的inputs有兩種類型,一種是源碼目錄,一種是jar包,分別遍歷這兩個,找到咱們須要處理的class類型。好比上面的代碼中源碼部遍歷篩選的是咱們本身的Application->MyApp.class。jar包部分處理全部的FragmentActivity。這些均可以根據本身的需求來篩選。

而後經過ClassReader讀取,經過ClassWriter交給咱們自定義的類訪問器LogVisitor來處理

ASM核心類

  • ClassReader 用來解析編譯過的字節碼文件
  • ClassWriter 用來從新構建編譯後的類,好比修改類名,屬性,方法或者生成新類的字節碼文件
  • ClassVisitor 用來訪問類成員信息,包括標記在類上的註解,類的構造方法,類的字段,方法,靜態代碼塊
  • MethodVisitor 用來訪問方法的信息,用來進行具體的方法字節碼操做。
  • AdviceAdapter 用來訪問方法的信息,用來進行具體的方法字節碼操做。是MethodVisitor的加強實現

第二步動態插入代碼

第一步中經過Transform,遍歷全部的class文件,篩選出咱們想要處理的class,而後交給了類訪問器來處理,下面就來看怎麼處理

package com.chs.asm;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class LogVisitor extends ClassVisitor {
    private String mClassName;
    public LogVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM5, classVisitor);
    }
    
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        System.out.println("LogVisitor : visit -----> started:" + name);
        this.mClassName = name;
        super.visit(version, access, name, signature, superName, interfaces);
    }
    //定義一個方法, 返回的MethodVisitor用於生成方法相關的信息
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        if ("com/hsm/asmtext/MyApp".equals(this.mClassName)) {
            if ("onCreate".equals(name)) {
                //處理onCreate
                System.out.println("LogVisitor : visitMethod method ----> " + name);
                return new OnCreateVisitor(mv);
            }
        }
        return mv;
    }
    //訪問結束
    @Override
    public void visitEnd() {
        System.out.println("LogVisitor : visit -----> end");
        super.visitEnd();
    }
}
複製代碼

在visitMethod方法中篩選出咱們想要操做的方法。好比這裏操做onCreate方法。篩選出來以後交給自定義的方法訪問者OnCreateVisitor來處理

怎麼處理呢?假如咱們想要在Application的onCreate方法執行前插入一行記錄時間的代碼,在onCreate以後在插入一行代碼以下

public class MyApp extends Application {

    @Override
    public void onCreate() {
        long startTime = System.currentTimeMillis();
        super.onCreate();
        ...一堆操做..
        long interval = System.currentTimeMillis()-startTime;
    }
}
複製代碼

那麼使用ASM插入的方式以下

package com.chs.asm;

import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;


public class OnCreateVisitor extends MethodVisitor {

    public OnCreateVisitor(MethodVisitor methodVisitor) {
        super(Opcodes.ASM5, methodVisitor);
    }
    //開始訪問方法
    @Override
    public void visitCode() {
        super.visitCode();

        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        mv.visitVarInsn(Opcodes.LSTORE, 1);
    }

    @Override
    public void visitInsn(int opcode) {
        //判斷內部操做指令
        //當前指令是RETURN,表示方法內部的代碼已經執行完
        if (opcode == Opcodes.RETURN) {
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(Opcodes.LLOAD, 1);
            mv.visitInsn(Opcodes.LSUB);
            mv.visitVarInsn(Opcodes.LSTORE, 3);
            Label l3 = new Label();
            mv.visitLabel(l3);
            mv.visitLineNumber(20, l3);
            mv.visitLdcInsn("TAG");
            mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
            mv.visitInsn(Opcodes.DUP);
            mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            mv.visitLdcInsn("interval:");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitVarInsn(Opcodes.LLOAD, 3);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
            mv.visitInsn(Opcodes.POP);
        }
        super.visitInsn(opcode);
    }

    @Override
    public void visitEnd() {
        super.visitEnd();
        //訪問結束
    }
}
複製代碼

其實到這裏就完事了,以後就是打包發佈到maven而後供咱們的主工程使用了,不過上面的代碼是個什麼鬼,啥意思啊,咱們該怎麼寫出來啊。

想要弄懂上面的代碼,須要對java字節碼和JVM的指令有必定的瞭解,上面就是組裝一個方法的代碼,須要用到包名啊,方法簽名等。

若是咱們不瞭解JVM指令能夠寫出上面的代碼嗎?固然能夠,有牛逼的前輩早已經寫出了插件來生成這樣的代碼,上面的代碼就是生成出來的。

打開AndroidStudio的安裝插件的界面,搜索ASM Bytecode Outline這個插件安裝。

怎麼使用呢

先寫一個空的Application以下

public class MyApp extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
    }
}
複製代碼

而後鼠標右擊,選擇Show Bytecode Outline,就能看到這幾行代碼的字節碼了。

而後在裏面加上咱們要插入的代碼,在執行一樣的操做

public class MyApp extends Application {

    @Override
    public void onCreate() {
        long startTime = System.currentTimeMillis();
        super.onCreate();
        long interval = System.currentTimeMillis()-startTime;
        Log.i("TAG","interval:"+interval);
    }
}
複製代碼

又能看到當前幾行代碼的字節碼。而後牛逼的功能又來了,點擊ASMified這個tab,裏面有個show differences

就能看到先後兩次操做生成的指令的區別在哪裏了。這樣就能很清晰的知道該怎麼寫了以下。

OK,下面開始發佈上傳,以前build.gradle中已經配置好了maven的本地倉庫地址了。下面直接使用AndroidSrudio的快捷鍵上傳

打開最右邊的Gradle面板,而後點擊uploadArchives上傳,

OK以後就能夠在對應目錄看到咱們上傳的jar包了。本項目配置的倉庫在當前目錄下的my-plugin文件夾,最後生成目錄以下

在當前工程目錄引入本地maven倉庫地址和咱們本身寫的插件,插件的包名和版本號就是以前插件build.gradle中配置的

buildscript {
    repositories {
        google()
        jcenter()
        maven {
            //本地倉庫地址
            url uri('D:/androiddemo/5/ASMText/asmplugin/my-plugin') } } dependencies {
        classpath 'com.android.tools.build:gradle:3.5.0'
        //本身寫的插件
        classpath 'com.chs.asm.plugin:asmplugin:1.0'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        maven {
            //本地倉庫地址
            url uri('D:/androiddemo/5/ASMText/asmplugin/my-plugin') } } } 複製代碼

而後去app中build.gradle中引入插件

apply plugin: 'com.chs.asm-plugin'
複製代碼

OK大工完成,在onCreate中添加個耗時代碼用來測試,運行app

public class MyApp extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}
複製代碼

運行結果以下

2019-09-26 11:09:28.838 28745-28745/com.hsm.asmtext I/TAG: interval:1002
複製代碼

到這裏ASM的簡單用法就算入門了,想要自如的操控咱們的代碼,還須要繼續系統的學習一下gradle和ASM的知識。多多練習多熟悉。

參考博文

在AndroidStudio中自定義Gradle插件

【Android】函數插樁(Gradle + ASM)

Android ASM自動埋點方案實踐

相關文章
相關標籤/搜索