Android字節碼的手術刀插樁初體驗

本文有對其他博客的一些借鑑。

我們都知道Dalvik虛擬機運行的是.dex文件。.dex文件又是通過.class文件通過dx工具編譯而來。今天要體驗的就是一個非常有意思的技術,字節碼的插樁。

大部分時候都會用埋點來介紹這個技術。原理就是,通過Transform這個類去獲取項目中的.class文件。然後使用AMS提供的幾個類去解析.class文件。通過對類名,方法名的判斷,篩選出你需要修改的.class文件。然後在需要修改的地方插入你想要的被轉成字節碼的代碼

最複雜的部分是:.class文件有着自己很嚴格的格式,如果我們想注入代碼時,不是直接插入相關的指令即可。我們還需要去找到相應的StackMapFrame,換句話說就是要找到對應的幀棧,因爲我們插入的方法可能和已有的方法中的對象有引用關係,所以需要對幀棧進行計算,最後還要壓縮剩下的幀。不過好在這步AMS已經處理完了,我們只需要進行調用就行。

首先要使用使用Transform就需要使用自定義插件。那麼先去自定義一個插件。新建一個android library。把除了src/main/java和.gradle文件外的其他所有文件都刪除了。

這樣就行了。

然後我們需要用groovy語言去寫插件所以需要一個groovy文件夾。在此之前先去把gradle重新寫一下。把之前的都刪了,然後插入下面的就行,這個寫法基本上是固定了。因爲我們要把插件發佈到本地。

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

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation  gradleApi()
    implementation  localGroovy()

    implementation 'com.android.tools.build:gradle:3.6.1'

    //ASM相關依賴
    implementation 'org.ow2.asm:asm-commons:7.1'
    implementation 'org.ow2.asm:asm:7.1'
}

然後在groovy文件夾下面自己新建一個.groovy文件。用AS編寫groovy文件需要相當注意,因爲這玩意大部分時候都不會報錯。裏面的代碼意思是將自定義的transform註冊到任務裏,而且打印了一句話。

package my.test.lifecycle

import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project

class LifeCyclePlugin implements Plugin<Project>{

    @Override
    void apply(Project project) {
        System.out.println("register_LifeCyclePlugin")
        def android =project.extensions.getByType(AppExtension);
        LifeCycleTransForm lifeCycleTransForm=new LifeCycleTransForm();
        android.registerTransform(lifeCycleTransForm)
    }
}

然後在main文件夾下在新建一個resources文件夾

文件名一定不能錯。在這個文件夾下新建一個.properties文件。my.test.lifecycle前面這段就是你的插件名了。

gradle文件中在寫上group與version,然後直接運行uploadArchives這個任務。你會看到在工程下出現一個新的文件夾asm_lifecycle。

group='my.test.lifecycle'
version='1.0.0'

uploadArchives{
    repositories{
        mavenDeployer {
            //本地的Maven地址設置
            repository(url: uri('../asm_lifecycle'))
        }
    }
}

然後在app的gradle裏把插件給導進來。

apply plugin: 'my.test.lifecycle'
buildscript {
    repositories {
        google()
        jcenter()
        maven { url '../asm_lifecycle' }
        //自定義插件maven地址
    }
    dependencies {
        //加載自定義插件 group + module + version
        classpath 'my.test.lifecycle:my_lifecycle_plugin:1.0.0'
    }
}

這個時候,我們的APP就可以使用自己的插件了。現在開始寫我們的自定義Transform。

package my.test.lifecycle

import asm.test.plugin.LifecycleClassVisitor
import asm.test.plugin.LifecycleMethodVisitor
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import groovy.io.FileType
import org.apache.commons.io.FileUtils
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter


class LifeCycleTransForm extends Transform {

    //自定義的TransForm名稱
    @Override
    String getName() {
        return "LifeCycleTransForm"
    }

    //設置自定義TransForm接收的文件類型
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    //設置自定義TransForm檢索範圍
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.PROJECT_ONLY
    }

    //是否支持增量編譯
    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        //拿到所有的class文件
        Collection<TransformInput> transformInputs = transformInvocation.inputs
        TransformOutputProvider outputProvider = transformInvocation.outputProvider
        if (outputProvider != null) {
            outputProvider.deleteAll()
        }

        transformInputs.each { TransformInput transformInput ->
            // 遍歷directoryInputs(文件夾中的class文件) directoryInputs代表着以源碼方式參與項目編譯的所有目錄結構及其目錄下的源碼文件
            // 比如我們手寫的類以及R.class、BuildConfig.class以及MainActivity.class等
            transformInput.directoryInputs.each { DirectoryInput directoryInput ->
                File dir = directoryInput.file
                if (dir) {
                    dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) { File file ->
                        def name = file.name;
                        if (name.endsWith(".class") && !name.startsWith("R\$")
                                && !"R.class".equals(name) && !"BuildConfig.class".equals(name)) {
                            System.out.println("find class: " + file.name)

                            //對class文件進行讀取與解析
                            ClassReader classReader = new ClassReader(file.bytes)
                            //對class文件的寫入
                            ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                            //訪問class文件相應的內容,解析到某一個結構就會通知到ClassVisitor的相應方法
                            ClassVisitor classVisitor = new LifecycleClassVisitor(classWriter)
                            //依次調用 ClassVisitor接口的各個方法
                            classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
                            //toByteArray方法會將最終修改的字節碼以 byte 數組形式返回。
                            byte[] bytes = classWriter.toByteArray()

                            //通過文件流寫入方式覆蓋掉原先的內容,實現class文件的改寫。
//                            FileOutputStream outputStream = new FileOutputStream( file.parentFile.absolutePath + File.separator + fileName)
                            FileOutputStream outputStream = new FileOutputStream(file.path)
                            outputStream.write(bytes)
                            outputStream.close()

                        }
                    }
                }

                //處理完輸入文件後把輸出傳給下一個文件
                def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes,
                        directoryInput.scopes, Format.DIRECTORY)
                FileUtils.copyDirectory(directoryInput.file, dest)
            }
        }
    }
}

註釋寫的還是很詳細的。

ClassReader是用於解析class文件
ClassWriter是用於寫入你要插入的字節碼,並以流的形式返回
ClassVisitor用於訪問class文件的類,需要自定義去繼承

所以我們新建一個class的訪問類,與一個方法的訪問類

package asm.test.plugin;

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

public class LifecycleClassVisitor extends ClassVisitor {
    private String className;
    private String superName;

    public LifecycleClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }

    /**
     * 
     * @param version JDK版本
     * @param access 類修飾信息 public private
     * @param name 全類名
     * @param signature 泛型信息
     * @param superName 繼承的類名
     * @param interfaces 接口信息
     */
    @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.className = name;
        this.superName = superName;
    }

    /**
     * 
     * 
     * @param access 方法修飾 private public
     * @param name 方法名
     * @param desc 返回類型 int boolean
     * @param signature 泛型信息
     * @param exceptions 異常信息
     * @return
     */
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        System.out.println("ClassVisitor visitMethod name-------" + name + ", superName is " + superName);
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);

        if(superName.equals("android/app/Activity")){
            if(name.startsWith("onCreate")){
                return new LifecycleMethodVisitor(mv,className,name);
            }
        }
        return mv;
    }

    @Override
    public void visitEnd() {
        super.visitEnd();
    }
}

註釋寫的很詳細了,就不在解釋

package asm.test.plugin;


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

public class LifecycleMethodVisitor extends MethodVisitor {
    private String className;
    private String methodName;

    public LifecycleMethodVisitor(MethodVisitor methodVisitor, String className, String methodName) {
        super(Opcodes.ASM5, methodVisitor);
        this.className = className;
        this.methodName = methodName;
    }

    /**
     * 方法執行前
     */
    @Override
    public void visitCode() {
        super.visitCode();
        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("Activity=");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/Object;)Ljava/lang/StringBuilder;", false);
        mv.visitLdcInsn(" method=onCreate");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
        mv.visitInsn(Opcodes.POP);
    }

    /**
     * 方法執行後
     * @param opcode
     */
    @Override
    public void visitInsn(int opcode) {
        super.visitInsn(opcode);
    }
}

看到visitCode是不是感覺很難,沒關係他們提供了一個工具

插件裏安裝這個插件

右邊會多一塊區域

先寫好你想插入的代碼,然後右擊鼠標

然後他就會把相應的字節碼寫法展示給你

拷貝這段就行。

這樣在oncreate方法前插入一個日誌的事兒就完成了。

插樁可以做的事情太多了,各種監控,插件化,或者當做一個過濾器。而且這個技術相當好玩,因爲不會去修改源碼,你就可以實現自己想做的事情。

 

 

 

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