ASM字節碼插樁


個人博客

http://www.milovetingting.cn

ASM字節碼插樁

前言

熱修復的多Dex加載方案中,對於5.0以下的系統存在CLASS_ISPREVERIFIED的問題,而解決這個問題的一個方案是:通過ASM插樁,在類的構造方法里引入一個其它dex里的類,從而避免被打上CLASS_ISPREVERIFIED標簽。熱修復可以參考其它資料或者前面寫的一篇文章。本文主要介紹ASM插樁,主要參考 https://juejin.im/post/5c6eaa066fb9a049fc042048

ASM框架

ASM是一個可以分析和操作字節碼的框架,通過它可以動態地修改字節碼內容。使用ASM可以實現無埋點統計、性能監控等。

什么是字節碼插樁

Android編譯過程中,往字節碼插入自定義的字節碼。

插樁時機

Android打包要經過:java文件--class文件--dex文件,通過Gradle提供的Transform API,可以在編譯成dex文件前,得到class文件,然后通過ASM修改字節碼,即字節碼插樁。

實現

下面通過自定義Gradle插件來處理class文件來實現插樁。

自定義Gradle插件

具體自定義Gradle插件的步驟,這里不再詳細介紹,可以參考之前的一篇文章或者自行查閱其它資料。

處理Class

插件分為插件部分(src/main/groovy)、ASM部分(src/main/java)

ASM插樁

ASMPlugin類繼承自Transform並實現Plugin接口,在apply的方法里注冊,transform里回調並處理class。

class ASMPlugin extends Transform implements Plugin<Project> {

    @Override
    void apply(Project project) {
        def android = project.extensions.getByType(AppExtension)
        android.registerTransform(this)
    }

    @Override
    String getName() {
        return "ASMPlugin"
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        //處理class
    }
}

主要的邏輯處理都在transform方法里

@Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        println('--------------------ASMPlugin transform start--------------------')
        def startTime = System.currentTimeMillis()
        Collection<TransformInput> inputs = transformInvocation.inputs
        TransformOutputProvider outputProvider = transformInvocation.outputProvider
        //刪除舊的輸出
        if (outputProvider != null) {
            outputProvider.deleteAll()
        }
        //遍歷inputs
        inputs.each { input ->
            //遍歷directoryInputs
            input.directoryInputs.each {
                directoryInput -> handleDirectoryInput(directoryInput, outputProvider)
            }
            //遍歷jarInputs
            input.jarInputs.each {
                jarInput -> handleJarInput(jarInput, outputProvider)
            }
        }
        def time = (System.currentTimeMillis() - startTime) / 1000
        println('-------------------- ASMPlugin transform end --------------------')
        println("ASMPlugin cost $time s")
    }

在transform里處理class文件和jar文件

    /**
     * 處理目錄下的class文件
     * @param directoryInput
     * @param outputProvider
     */
    static void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
        //是否為目錄
        if (directoryInput.file.isDirectory()) {
            //列出目錄所有文件(包含子文件夾,子文件夾內文件)
            directoryInput.file.eachFileRecurse {
                file ->
                    def name = file.name
                    if (isClassFile(name)) {
                        println("-------------------- handle class file:<$name> --------------------")
                        ClassReader classReader = new ClassReader(file.bytes)
                        ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                        ClassVisitor classVisitor = new ActivityClassVisitor(classWriter)
                        classReader.accept(classVisitor, org.objectweb.asm.ClassReader.EXPAND_FRAMES)
                        byte[] bytes = classWriter.toByteArray()
                        FileOutputStream fileOutputStream = new FileOutputStream(file.parentFile.absolutePath + File.separator + name)
                        fileOutputStream.write(bytes)
                        fileOutputStream.close()
                    }
            }
        }
        def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
        FileUtils.copyDirectory(directoryInput.file, dest)
    }

    /**
     * 處理Jar中的class文件
     * @param jarInput
     * @param outputProvider
     */
    static void handleJarInput(JarInput jarInput, TransformOutputProvider outputProvider) {
        if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
            //重名名輸出文件,因為可能同名,會覆蓋
            def jarName = jarInput.name
            def md5Name = DigestUtils.md5Hex(jarInput.file.absolutePath)
            if (jarName.endsWith(".jar")) {
                jarName = jarName.substring(0, jarName.length() - 4)
            }
            JarFile jarFile = new JarFile(jarInput.file)
            Enumeration enumeration = jarFile.entries()
            File tempFile = new File(jarInput.file.parent + File.separator + "temp.jar")
            //避免上次的緩存被重復插入
            if (tempFile.exists()) {
                tempFile.delete()
            }
            JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tempFile))
            //保存
            while (enumeration.hasMoreElements()) {
                JarEntry jarEntry = enumeration.nextElement()
                String entryName = jarEntry.name
                ZipEntry zipEntry = new ZipEntry(entryName)
                InputStream inputStream = jarFile.getInputStream(zipEntry)
                if (isClassFile(entryName)) {
                    println("-------------------- handle jar file:<$entryName> --------------------")
                    jarOutputStream.putNextEntry(zipEntry)
                    ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                    ClassVisitor classVisitor = new ActivityClassVisitor(classWriter)
                    classReader.accept(classVisitor, org.objectweb.asm.ClassReader.EXPAND_FRAMES)
                    byte[] bytes = classWriter.toByteArray()
                    jarOutputStream.write(bytes)
                } else {
                    jarOutputStream.putNextEntry(zipEntry)
                    jarOutputStream.write(IOUtils.toByteArray(inputStream))
                }
                jarOutputStream.closeEntry()
            }
            jarOutputStream.close()
            jarFile.close()
            def dest = outputProvider.getContentLocation(jarName + "_" + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
            FileUtils.copyFile(tempFile, dest)
            tempFile.delete()
        }
    }

    /**
     * 判斷是否為需要處理class文件
     * @param name
     * @return
     */
    static boolean isClassFile(String name) {
        return (name.endsWith(".class") && !name.startsWith("R\$")
                && "R.class" != name && "BuildConfig.class" != name && name.contains("Activity"))
    }

在handleDirectoryInput和handleJarInput調用了我們自己定義在src/main/java里的ClassVisitor,

class ActivityClassVisitor extends ClassVisitor implements Opcodes {

    private String mClassName;

    private static final String CLASS_NAME_ACTIVITY = "androidx/appcompat/app/AppCompatActivity";

    private static final String METHOD_NAME_ONCREATE = "onCreate";

    private static final String METHOD_NAME_ONDESTROY = "onDestroy";

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

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

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature,
                                     String[] exceptions) {
        MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
        if (CLASS_NAME_ACTIVITY.equals(mClassName)) {
            if (METHOD_NAME_ONCREATE.equals(name)) {
                System.out.println("-------------------- ActivityClassVisitor,visit method:" + name +
                        " --------------------");
                return new ActivityOnCreateMethodVisitor(Opcodes.ASM5, methodVisitor);
            } else if (METHOD_NAME_ONDESTROY.equals(name)) {
                System.out.println("-------------------- ActivityClassVisitor,visit method:" + name +
                        " --------------------");
                return new ActivityOnDestroyMethodVisitor(Opcodes.ASM5, methodVisitor);
            }
        }
        return methodVisitor;
    }
}

這里為簡化操作,只處理了Activity的onCreate和onDestroy方法。在visitMethod方法里又調用了具體的MethodVisitor。如果對字節碼不是特別了解的,可以通過在Android Studio中安裝ASM Bytecode Outline插件來輔助。

具體使用:

安裝完成ASM Bytecode Outline后,重啟Android Studio,然后在相應的Java文件中右鍵,選擇Show Bytecode outline

ASM插樁2

稍待一會后,會生成相應的字節碼,在打開的面板中選擇ASMified標簽

ASM插樁3

public class ActivityOnCreateMethodVisitor extends MethodVisitor {

    public ActivityOnCreateMethodVisitor(int api, MethodVisitor mv) {
        super(api, mv);
    }

    @Override
    public void visitCode() {
         mv.visitLdcInsn("ASMPlugin");
        mv.visitLdcInsn("-------------------- MainActivity onCreate --------------------");
        mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;" +
                "Ljava/lang/String;)I", false);
        mv.visitInsn(POP);

        super.visitCode();
    }

    @Override
    public void visitInsn(int opcode) {
        super.visitInsn(opcode);
    }
}

public class ActivityOnDestroyMethodVisitor extends MethodVisitor {

    public ActivityOnDestroyMethodVisitor(int api, MethodVisitor mv) {
        super(api, mv);
    }

    @Override
    public void visitCode() {
        super.visitCode();

        mv.visitLdcInsn("ASMPlugin");
        mv.visitLdcInsn("-------------------- MainActivity onDestroy --------------------");
        mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;" +
                "Ljava/lang/String;)I", false);
        mv.visitInsn(POP);
    }

    @Override
    public void visitInsn(int opcode) {
        super.visitInsn(opcode);
    }
}

在visitCode和visitInsn方法里執行具體的操作。

在處理Class過程中,可能會出現各種問題,可以通過調試插件來定位問題。可以參考上一篇文章來調試插件。

引用插件

在app模塊引用插件,這里不再詳細介紹,可以參考前面的文章

將應用運行在手機上,打開后,可以看到日志輸出:

02-25 17:29:45.885 31237 31237 I ASMPlugin: -------------------- MainActivity onCreate --------------------
02-25 17:29:50.646 31237 31237 I ASMPlugin: -------------------- MainActivity onDestroy --------------------

結語

這篇文章只是實現了簡單的ASM插樁。可以查閱其它資料,了解更多關於字節碼、ASM相關的內容。

源碼地址:https://github.com/milovetingting/Samples/tree/master/ASM


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM