看這一篇,你也能夠自如的掌握字節碼插樁

本文簡介

使用GradlePlugin、Transform和ASM實現字節碼插樁,GradlePlugin相關代碼所有由Kotlin編寫,因此不熟悉groovy的小夥伴也能夠無障礙閱讀。html

什麼是字節碼插樁

要了解字節碼插樁,首先要了解AOP(Aspect Oriented Programming)思想,對比來講,OOP(面向對象編程)主要針對的是業務處理過程的實體極其屬性和行爲進行抽象封裝,爲的是有更清晰高效的邏輯單元劃分。AOP(面向切面編程)則是針對業務處理過程當中的切面進行提取,它所面對的是處理過程當中的某個步驟或階段,以得到邏輯過程當中各部分之間低耦合的隔離效果。java

字節碼插入位置

Android開發者應該會熟悉這個圖片(來自於Android Developer):android

image.png

在Compilers中具體爲:git

image.png

字節碼插樁就是在.class文件轉爲.dex以前,修改.class文件從而達到修改代碼的目的。github

業務場景

字節碼插樁能夠用於日誌記錄、性能統計、異常處理、修改三方庫代碼等。web

須要掌握的知識

在Android項目中使用字節碼插樁,你要掌握編程

  • GradlePlugin相關知識
  • 瞭解Transform API
  • 瞭解字ASM,Javassist或者其餘相關插樁框架

建立你的GradlePlugin

GradlePlugin相關知識也能夠直接參考官網文檔api

建立的三種方式

  • 直接在相應model的build.gradle裏面寫相應的plugin代碼(方式直接,可是做用範圍只有單個model)

image.png

  • 在buildSrc裏面寫plugin代碼(做用範圍是整個工程)

image.png

image.png

  • 建立plugin模塊,寫資源文件xxx.properties文件肯定plugin名字以及文件對應地址(相比前兩個實現方式較爲複雜,可是能夠上傳到公共倉庫多項目複用)

image.png

在properties文件中標出模塊文件位置:markdown

properties文件 image.png網絡

引入模塊的名字:properties文件的文件名(例子中爲com.shawn.addlogformethodplugin)

build.gradle image.png

引入maven庫方便本地調試。

使用plugin

  • 在第一種方式中plugin爲默認包名,能夠直接引用

image.png

  • 第二種方式中根據本身定義的包名引入

image.png

  • 第三種方式首先在項目根目錄下引入倉庫,再引用

根目錄下build.gradle image.png

model裏面build.gradle image.png

瞭解Transform

什麼是Transform

從1.5.0-beta1開始,Gradle插件包含一個Transform API,容許第三方插件在將已編譯的類文件轉換爲dex文件以前對其進行操做。 (該API已存在於1.4.0-beta2中,但已在1.5.0-beta1中進行了完全的改進

Transform的工做原理

相似於OkHttp的攔截器,採用責任鏈模式。每個Transform都是一個Task,編譯中間產物在多個Transform鏈上傳遞。用戶自定一的Transform會在鏈頭第一個執行,因爲每個Transform的處理源都是由前一個Transform提供的,因此若是用戶自定義的Transform沒有把相應的文件搬運到指定輸出位置,那麼下一個Transform就沒法正常工做。

image.png

Transform相關方法

getName()

/** * Returns the unique name of the transform. * * <p>This is associated with the type of work that the transform does. It does not have to be * unique per variant. */
    @NonNull
    public abstract String getName();
複製代碼

表示當前Transform名稱,這個名稱會被用來建立目錄,它會出如今app/build/intermediates/transforms目錄下。

getInputTypes()

/** * Returns the type(s) of data that is consumed by the Transform. This may be more than * one type. * * <strong>This must be of type {@link QualifiedContent.DefaultContentType}</strong> */
    @NonNull
    public abstract Set<ContentType> getInputTypes();
複製代碼

須要處理的數據類型,用於肯定咱們須要對哪些類型的結果進行修改,好比class,資源文件等。

image.png

getScopes()

/** * Returns the scope(s) of the Transform. This indicates which scopes the transform consumes. */
    @NonNull
    public abstract Set<? super Scope> getScopes();
複製代碼

表示Transform要操做的內容範圍

image.png

這些Scope能夠自由組合使用(好比SCOPE_FULL_PROJECT包含了Scope.PROJECT,Scope.SUB_PROJECTS,Scope.EXTERNAL_LIBRARIES)

isIncremental()

/** * Returns whether the Transform can perform incremental work. * * <p>If it does, then the TransformInput may contain a list of changed/removed/added files, unless * something else triggers a non incremental run. */
    public abstract boolean isIncremental();
複製代碼

是否支持增量更新:若是返回true,則TransformInput會包含一份修改的文件列表,若是返回false,則進行全量編譯,刪除上一次輸出的內容。

相關狀態說明:

  • NOTCHANGED: 不須要作任何處理。

  • REMOVED: 須要移除文件。

  • ADDED、CHANGED: 須要正常修改文件。

transform()

/** * Executes the Transform. * * <p>The inputs are packaged as an instance of {@link TransformInvocation} * <ul> * <li>The <var>inputs</var> collection of {@link TransformInput}. These are the inputs * that are consumed by this Transform. A transformed version of these inputs must * be written into the output. What is received is controlled through * {@link #getInputTypes()}, and {@link #getScopes()}.</li> * <li>The <var>referencedInputs</var> collection of {@link TransformInput}. This is * for reference only and should be not be transformed. What is received is controlled * through {@link #getReferencedScopes()}.</li> * </ul> * * A transform that does not want to consume anything but instead just wants to see the content * of some inputs should return an empty set in {@link #getScopes()}, and what it wants to * see in {@link #getReferencedScopes()}. * * <p>Even though a transform's {@link Transform#isIncremental()} returns true, this method may * be receive <code>false</code> in <var>isIncremental</var>. This can be due to * <ul> * <li>a change in secondary files ({@link #getSecondaryFiles()}, * {@link #getSecondaryFileOutputs()}, {@link #getSecondaryDirectoryOutputs()})</li> * <li>a change to a non file input ({@link #getParameterInputs()})</li> * <li>an unexpected change to the output files/directories. This should not happen unless * tasks are improperly configured and clobber each other's output.</li> * <li>a file deletion that the transform mechanism could not match to a previous input. * This should not happen in most case, except in some cases where dependencies have * changed.</li> * </ul> * In such an event, when <var>isIncremental</var> is false, the inputs will not have any * incremental change information: * <ul> * <li>{@link JarInput#getStatus()} will return {@link Status#NOTCHANGED} even though * the file may be added/changed.</li> * <li>{@link DirectoryInput#getChangedFiles()} will return an empty map even though * some files may be added/changed.</li> * </ul> * * @param transformInvocation the invocation object containing the transform inputs. * @throws IOException if an IO error occurs. * @throws InterruptedException * @throws TransformException Generic exception encapsulating the cause. */
    public void transform(@NonNull TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        // Just delegate to old method, for code that uses the old API.
        //noinspection deprecation
        transform(transformInvocation.getContext(), transformInvocation.getInputs(),
                transformInvocation.getReferencedInputs(),
                transformInvocation.getOutputProvider(),
                transformInvocation.isIncremental());
    }

複製代碼

在這裏能夠獲取到getInputs(),若是消費了getInputs()的話,則transform後必須輸出給下一級,否則下一級讀取不到編譯中間件。是否增量編譯要以isIncremental爲準。

override fun transform(transformInvocation: TransformInvocation?) {
        val inputs = transformInvocation?.inputs
        val out = transformInvocation?.outputProvider

        inputs?.forEach { transformInput ->

            //項目目錄
            transformInput.directoryInputs.forEach { directoryInput ->
                if (directoryInput.file.isDirectory) {
                    FileUtils.getAllFiles(directoryInput.file).forEach {
                        val file = it
                        val name = file.name
                        if (name.endsWith(".class") && name != "R.class" && !name.startsWith("R\$") && name != "BuildConfig.class") {
                            val classPath = file.absolutePath
                            val cr = ClassReader(file.readBytes())
                            val cw = ClassWriter(cr, ClassWriter.COMPUTE_MAXS)
                            val visitor = AddLogClassVisitor(cw)
                            cr.accept(visitor, ClassReader.EXPAND_FRAMES)

                            val byte = cw.toByteArray();
                            val fos = FileOutputStream(classPath)
                            fos.write(byte)
                            fos.close()
                        }
                    }
                }

                val dest = out?.getContentLocation(
                    directoryInput.name,
                    directoryInput.contentTypes,
                    directoryInput.scopes,
                    Format.DIRECTORY
                )

                FileUtils.copyDirectoryToDirectory(directoryInput.file, dest)

            }


            //jar包
            transformInput.jarInputs.forEach {
                val dest = out?.getContentLocation(
                    it.name,
                    it.contentTypes,
                    it.scopes,
                    Format.JAR
                )

                FileUtils.copyFile(it.file, dest)
            }

        }

    }
複製代碼

ASM實踐方案

字節碼相關知識請自行查看,本文不作過多講解。

ASM核心API

ASM Core API能夠類比解析XML文件中的SAX方式,不須要把這個類的整個結構讀取進來,就能夠用流式的方法來處理字節碼文件。好處是很是節約內存,可是編程難度較大。然而出於性能考慮,通常狀況下編程都使用Core API。在Core API中有如下幾個關鍵類:

  • ClassReader:用於讀取已經編譯好的.class文件。
  • ClassWriter:用於從新構建編譯後的類,生成新的類的字節碼文件。
  • 各類Visitor類:CoreAPI根據字節碼從上到下依次處理,對於字節碼文件中不一樣的區域有不一樣的Visitor,好比用於訪問方法的MethodVisitor、用於訪問類變量的FieldVisitor、用於訪問註解的AnnotationVisitor等。爲了實現AOP,重點要使用的是MethodVisitor。

情景目標

要給MainActivity.kt中全部的方法添加一個自定義方法的調用:

  • 首先在transform()方法中讀取想要修改的class文件。建立ClassVisitor類而且傳入ClassWriter用於字節碼定位修改操做,把ClassVisitor傳入ClassReader用於從新構建新的字節碼文件。
val classPath = file.absolutePath
    val cr = ClassReader(file.readBytes())
    val cw = ClassWriter(cr, ClassWriter.COMPUTE_MAXS)
    val visitor = AddLogClassVisitor(cw)
    cr.accept(visitor, ClassReader.EXPAND_FRAMES)

    val byte = cw.toByteArray()
    val fos = FileOutputStream(classPath)
    fos.write(byte)
    fos.close()
複製代碼
  • 編寫自定義Visitor類繼承於ClassVisitor,實現方法定位。
  1. visit()方法能夠拿到類的相關信息:
public void visit( final int version, final int access, final String name, final String signature, final String superName, final String[] interfaces) {
    if (cv != null) {
      cv.visit(version, access, name, signature, superName, interfaces);
    }
  }
複製代碼
  1. visitMethod()方法能夠遍歷全部方法信息,用於定位須要修改的方法。
public MethodVisitor visitMethod( final int access, final String name, final String descriptor, final String signature, final String[] exceptions) {
    if (cv != null) {
      return cv.visitMethod(access, name, descriptor, signature, exceptions);
    }
    return null;
  }
複製代碼

在這裏找到經過名字判判定位目標方法:

AddLogClassVisitor.kt

class AddLogClassVisitor(classVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM7, classVisitor) {
    private var className: String? = null
    override fun visit( version: Int, access: Int, name: String?, signature: String?, superName: String?, interfaces: Array<out String>? ) {
        super.visit(version, access, name, signature, superName, interfaces)
        className = name
        println("className = $className")
    }

    override fun visitMethod( access: Int, name: String?, descriptor: String?, signature: String?, exceptions: Array<out String>? ): MethodVisitor {
        val methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)
        println("method = $name")

        if (className.equals("com/shawn/krouter/MainActivity") && name != "<init>") {
            return if (name == "onCreate") {
                AddLogInitMethodVisitor(methodVisitor)
            } else {
                AddLogMethodVisitor(methodVisitor)
            }
        }

        return methodVisitor
    }
}
複製代碼
  • 編寫自定義Visitor類繼承於MethodVisitor,進行字節碼修改。
  1. 在visitCode()方法中的字節碼會插入到方法執行以前。
/** Starts the visit of the method's code, if any (i.e. non abstract method). */
  public void visitCode() {
    if (mv != null) {
      mv.visitCode();
    }
  }
複製代碼
  1. 在visitInsn(opcode: Int)方法中當opcode == ARETURN || opcode == RETURN的時候,插入的字節碼會插入到方法後面。
/** * Visits a zero operand instruction. * * @param opcode the opcode of the instruction to be visited. This opcode is either NOP, * ACONST_NULL, ICONST_M1, ICONST_0, ICONST_1, ICONST_2, ICONST_3, ICONST_4, ICONST_5, * LCONST_0, LCONST_1, FCONST_0, FCONST_1, FCONST_2, DCONST_0, DCONST_1, IALOAD, LALOAD, * FALOAD, DALOAD, AALOAD, BALOAD, CALOAD, SALOAD, IASTORE, LASTORE, FASTORE, DASTORE, * AASTORE, BASTORE, CASTORE, SASTORE, POP, POP2, DUP, DUP_X1, DUP_X2, DUP2, DUP2_X1, DUP2_X2, * SWAP, IADD, LADD, FADD, DADD, ISUB, LSUB, FSUB, DSUB, IMUL, LMUL, FMUL, DMUL, IDIV, LDIV, * FDIV, DDIV, IREM, LREM, FREM, DREM, INEG, LNEG, FNEG, DNEG, ISHL, LSHL, ISHR, LSHR, IUSHR, * LUSHR, IAND, LAND, IOR, LOR, IXOR, LXOR, I2L, I2F, I2D, L2I, L2F, L2D, F2I, F2L, F2D, D2I, * D2L, D2F, I2B, I2C, I2S, LCMP, FCMPL, FCMPG, DCMPL, DCMPG, IRETURN, LRETURN, FRETURN, * DRETURN, ARETURN, RETURN, ARRAYLENGTH, ATHROW, MONITORENTER, or MONITOREXIT. */
  public void visitInsn(final int opcode) {
    if (mv != null) {
      mv.visitInsn(opcode);
    }
  }
複製代碼

咱們在這裏插入須要的字節碼:

AddLogMethodVisitor.kt

class AddLogMethodVisitor(methodVisitor: MethodVisitor) :
    MethodVisitor(Opcodes.ASM7, methodVisitor) {
    override fun visitCode() {
        super.visitCode()
        //方法執行前插入
        mv.visitLdcInsn("AddLogMethodPlugin")
        mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder")
        mv.visitInsn(Opcodes.DUP)
        mv.visitMethodInsn(
            Opcodes.INVOKESPECIAL,
            "java/lang/StringBuilder",
            "<init>",
            "()V",
            false
        )
        mv.visitLdcInsn("into ")
        mv.visitMethodInsn(
            Opcodes.INVOKEVIRTUAL,
            "java/lang/StringBuilder",
            "append",
            "(Ljava/lang/String;)Ljava/lang/StringBuilder;",
            false
        )
        mv.visitTypeInsn(Opcodes.NEW, "java/lang/Exception")
        mv.visitInsn(Opcodes.DUP)
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Exception", "<init>", "()V", false)
        mv.visitMethodInsn(
            Opcodes.INVOKEVIRTUAL,
            "java/lang/Exception",
            "getStackTrace",
            "()[Ljava/lang/StackTraceElement;",
            false
        )
        mv.visitInsn(Opcodes.ICONST_0)
        mv.visitInsn(Opcodes.AALOAD)
        mv.visitMethodInsn(
            Opcodes.INVOKEVIRTUAL,
            "java/lang/StackTraceElement",
            "getMethodName",
            "()Ljava/lang/String;",
            false
        )
        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,
            "com/shawn/krouter/uitl/UtilsKt",
            "MyLogD",
            "(Ljava/lang/String;Ljava/lang/String;)V",
            false
        )
    }


    override fun visitInsn(opcode: Int) {
        //方法後插入

        super.visitInsn(opcode)
    }
}
複製代碼
  • 把plugin編譯出來,而後引入相應model中,即可以實現需求。

查看class文件,能夠看到插入字節碼成功 image.png

log輸出 image.png

輕鬆的生成插入代碼

在不少網絡文章中都會推薦使用ASM Bytecode Outline插件來查看字節碼,可是這個插件目前只能夠安裝在Intellij IDEA,Android studio沒法安裝,這邊找到ASM Bytecode Viewer這個插件也能夠達到一樣的效果,可讓咱們更加方便的插入字節碼。

  • 首先編寫Test.java類,編譯稱.class,而後用ASM Bytecode Viewer插件來查看相關ASM代碼。

Test.java

package com.shawn.krouter;

import android.util.Log;

import com.shawn.krouter.uitl.UtilsKt;

public class Test {
    String name = "testFun";
    public void TestFun(){
        Log.d("suihw","lalala");
    }
}
複製代碼

經過插件查看ASM相關代碼:

package asm.com.shawn.krouter;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.ConstantDynamic;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.TypePath;

public class TestDump implements Opcodes {

    public static byte[] dump() throws Exception {

        ClassWriter classWriter = new ClassWriter(0);
        FieldVisitor fieldVisitor;
        MethodVisitor methodVisitor;
        AnnotationVisitor annotationVisitor0;

        classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "com/shawn/krouter/Test", null, "java/lang/Object", null);

        classWriter.visitSource("Test.java", null);

        {
            methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            methodVisitor.visitCode();
            Label label0 = new Label();
            methodVisitor.visitLabel(label0);
            methodVisitor.visitLineNumber(5, label0);
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            methodVisitor.visitInsn(RETURN);
            Label label1 = new Label();
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLocalVariable("this", "Lcom/shawn/krouter/Test;", null, label0, label1, 0);
            methodVisitor.visitMaxs(1, 1);
            methodVisitor.visitEnd();
        }
        {
            methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "TestFun", "()V", null, null);
            methodVisitor.visitCode();
            Label label0 = new Label();
            methodVisitor.visitLabel(label0);
            methodVisitor.visitLineNumber(7, label0);
            methodVisitor.visitLdcInsn("suihw");
            methodVisitor.visitLdcInsn("lalala");
            methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
            methodVisitor.visitInsn(POP);
            Label label1 = new Label();
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLineNumber(8, label1);
            methodVisitor.visitInsn(RETURN);
            Label label2 = new Label();
            methodVisitor.visitLabel(label2);
            methodVisitor.visitLocalVariable("this", "Lcom/shawn/krouter/Test;", null, label0, label2, 0);
            methodVisitor.visitMaxs(2, 1);
            methodVisitor.visitEnd();
        }
        classWriter.visitEnd();

        return classWriter.toByteArray();
    }
}
複製代碼
  • 修改Test.java類,把想要插入的相關java代碼寫好,而後在編譯成.class文件:

修改後的Test.java

package com.shawn.krouter;

import android.util.Log;

import com.shawn.krouter.uitl.UtilsKt;

public class Test {
    String name = "testFun";
    public void TestFun(){
        UtilsKt.MyLogD("suihw","into " + new Exception().getStackTrace()[0].getMethodName());
        Log.d("suihw","lalala");
    }
}
複製代碼

經過插件查看ASM相關代碼:

package asm.com.shawn.krouter;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.ConstantDynamic;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.TypePath;

public class TestDump implements Opcodes {

    public static byte[] dump() throws Exception {

        ClassWriter classWriter = new ClassWriter(0);
        FieldVisitor fieldVisitor;
        MethodVisitor methodVisitor;
        AnnotationVisitor annotationVisitor0;

        classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "com/shawn/krouter/Test", null, "java/lang/Object", null);

        classWriter.visitSource("Test.java", null);

        {
            methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            methodVisitor.visitCode();
            Label label0 = new Label();
            methodVisitor.visitLabel(label0);
            methodVisitor.visitLineNumber(7, label0);
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            methodVisitor.visitInsn(RETURN);
            Label label1 = new Label();
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLocalVariable("this", "Lcom/shawn/krouter/Test;", null, label0, label1, 0);
            methodVisitor.visitMaxs(1, 1);
            methodVisitor.visitEnd();
        }
        {
            methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "TestFun", "()V", null, null);
            methodVisitor.visitCode();
            Label label0 = new Label();
            methodVisitor.visitLabel(label0);
            methodVisitor.visitLineNumber(9, label0);
            methodVisitor.visitLdcInsn("suihw");
            methodVisitor.visitTypeInsn(NEW, "java/lang/StringBuilder");
            methodVisitor.visitInsn(DUP);
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            methodVisitor.visitLdcInsn("into ");
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getSimpleName", "()Ljava/lang/String;", false);
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            methodVisitor.visitMethodInsn(INVOKESTATIC, "com/shawn/krouter/uitl/UtilsKt", "MyLogD", "(Ljava/lang/String;Ljava/lang/String;)V", false);
            Label label1 = new Label();
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLineNumber(10, label1);
            methodVisitor.visitLdcInsn("suihw");
            methodVisitor.visitLdcInsn("lalala");
            methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
            methodVisitor.visitInsn(POP);
            Label label2 = new Label();
            methodVisitor.visitLabel(label2);
            methodVisitor.visitLineNumber(11, label2);
            methodVisitor.visitInsn(RETURN);
            Label label3 = new Label();
            methodVisitor.visitLabel(label3);
            methodVisitor.visitLocalVariable("this", "Lcom/shawn/krouter/Test;", null, label0, label3, 0);
            methodVisitor.visitMaxs(3, 1);
            methodVisitor.visitEnd();
        }
        classWriter.visitEnd();

        return classWriter.toByteArray();
    }
}

複製代碼
  • 找到異同處,即是咱們須要的相關字節碼,複製插入,即可以實現。

image.png

彩蛋

buildSrc統一版本控制

官方介紹

buildSrc模塊會在整個項目編譯最前執行,比項目根目錄下build.gradle執行還要早,因此能夠用來作統一版本控制,還會有代碼提示,十分方便實用。

項目結構

image.png

build.gradle.kts

image.png

Dependencies.kt

image.png

在相關模塊中引入:

image.png

Talk is cheap. Show me the code.

相關文章
相關標籤/搜索