• Android热修复2(ASM技术的运用)


    我们借由上一篇文件Android热修复1的项目引出下面这个问题。

    如果MainActivity类中只引用了:Utils类。当打包dex时,MainActivity与Utils都在classes.dex中,则加载时MainActivity类会被标记为CLASS_ISPREVERIFIED
    如果使用补丁包中的Utils类取代出现bug的Utils,则会导致MainActivity与其引用的Utils不在同一个Dex,但MainActivity已经被打上标记,此时出现冲突。导致校验失败!会报java.lang. IllegalAccessError: class ref in pre-verified class resolved to unexpected implementation错误。
    当然,这个错误是只在DVM虚拟机会出现的,如果不需要兼容Android5以下的机型,那么大可不必费神处理这个bug;若是我们要兼容呢?这就需要使用字节码插桩技术来进行热修复处理。
    那么,我们该如何阻止类被打上CLASS_ISPREVERIFIED标记呢?我们可以单独写一个类AntilazvLoad类,将该类打包成单独的hack.dex,这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的AntilazvLoad类,防止了类被打 上CLASS ISPREVERIFIED的标志,只要没被打上这个标志的类都可以使用进行Android热修复1中提供的方案进行打补丁操作。

    接下来,我们就进入这篇博客的主题——字节码插桩。

    • 定义
      所谓的字节码插桩就是在java字节码文件中的某些位置插入或修改代码。其实就像是我们使用java语法在java文件中写代码一样,我们使用class相关的语法在class文件中写代码。为什么可以这样做呢?因为class文件本就是java文件编译而来的,而jvm执行的是class文件只要我们在jvm执行class文件之前更改class文件那么同样能达到和更改Java文件一样的效果,具体可以看下下面这幅Android打包流程图。

    • Android打包流程
      在这里插入图片描述
      通过上图可知,我们只要在图中蓝色方框处拦截(即生成.dex文件之前),就可以拿到当前应用程序中所有的.class文件,然后借助一些库,就可以遍历这些.class文件中的所有方法,再根据一定的条件找到需要的目标方法,最后进行修改并保存,就可以插入我们自己想要的代码,堪称Android里面的黑科技。
      Google从Android Gradle1.5.0开始,提供了Transform API。通过该API,允许第三方以插件(Plugin)的形式,在Android应用程序打包成.dex文件之前的编译过程中操作.class文件。我们只要实现一套Transform,去遍历所有.class文件的所有方法,然后进行修改,最后再对原文件进行替换,即可达到插入代码的目的。

    明白了原理之后,接下来我们就使用ASM技术来仿造美团的Robust框架实现热修复功能。

    ASM

    ASM是一个功能比较齐全的java字节码操作与分析框架。通过使用ASM框架,我们可以动态生成类或者增强既有类的功能。ASM可以直接生成二进制.class文件,也可以在类被加载入jvm之前动态改变现有类的行为。java的二进制被存储在严格格式定义的.class文件里面,这些字节码文件拥有足够的元数据信息用来表示类中的所有元素,包括类名称、方法、属性以及Java字节码指令。ASM从字节码文件中读入这些信息后,能够改变类行为、分析类的信息,甚至能够根据具体的要求生成新的类。
    下面介绍一下ASM框架中几个核心的相关类。

    • ClassReader
      该类主要用来解析编译过的.class字节码文件。
    • ClassWriter
      该类主要用来重新构建编译后的类,比如修改类名、属性以及方法,甚至可以生成新的类字节码文件。
    • ClassVisitor
      主要负责“拜访”类成员信息。其中包括标记在类上的注解、类的构造方法、类的字段、类的方法、静态代码块等。
    • AdviceAdapter
      实现了MethodVisitor接口,主要负责“拜访”方法的信息,用来进行具体的方法字节码操作。

    我们重点了解ClassVisitor这个类,它会按照一定的标准次序来遍历一个类中的所有成员,在该类中,我们可以根据实际的需求进行条件判断,只要满足我们特定条件的类,我们才会去修改它的特定方法。

    import org.objectweb.asm.ClassVisitor
    import org.objectweb.asm.MethodVisitor
    import org.objectweb.asm.Opcodes
    
    class BrettAnalyticsClassVisitor extends ClassVisitor {
    
        BrettAnalyticsClassVisitor(final ClassVisitor classVisitor) {
            super(Opcodes.ASM5, classVisitor)
        }
    
        //可以拿到类的详细信息,然后对满足条件的类进行过滤
        @Override
        void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            super.visit(version, access, name, signature, superName, interfaces)
        }
    
        //访问内部类信息
        @Override
        void visitInnerClass(String name, String outerName, String innerName, int access) {
            super.visitInnerClass(name, outerName, innerName, access)
        }
    
        //拿到需要修改的方法,然后通过AdviceAdapter进行修改操作
        @Override
        MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions)
            methodVisitor = new BrettAnalyticsDefaultMethodVisitor(methodVisitor, access, name, desc)
            return methodVisitor
        }
    
        //遍历类中成员信息结束
        @Override
        void visitEnd() {
            super.visitEnd()
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    在上面的visitMethod中,可以对满足特定条件的方法进行修改。修改方法需要用到可以“拜访”方法所有信息的MethodVisitor。

    
    import org.objectweb.asm.AnnotationVisitor
    import org.objectweb.asm.Attribute
    import org.objectweb.asm.Label
    import org.objectweb.asm.MethodVisitor
    import org.objectweb.asm.commons.AdviceAdapter
    
    class BrettAnalyticsDefaultMethodVisitor extends AdviceAdapter{
        BrettAnalyticsDefaultMethodVisitor(MethodVisitor mv, int access, String name, String desc) {
            super(Opcodes.ASM6, mv, access, name, desc)
        }
    
        /**
         * 表示 ASM 开始扫描这个方法
         */
        @Override
        void visitCode() {
            super.visitCode()
        }
    
        @Override
        void visitMethodInsn(int opcode, String owner, String name, String desc) {
            super.visitMethodInsn(opcode, owner, name, desc)
        }
    
        @Override
        void visitAttribute(Attribute attribute) {
            super.visitAttribute(attribute)
        }
    
        /**
         * 表示方法输出完毕
         */
        @Override
        void visitEnd() {
            super.visitEnd()
        }
    
        @Override
        void visitFieldInsn(int opcode, String owner, String name, String desc) {
            super.visitFieldInsn(opcode, owner, name, desc)
        }
    
        @Override
        void visitIincInsn(int var, int increment) {
            super.visitIincInsn(var, increment)
        }
    
        @Override
        void visitIntInsn(int i, int i1) {
            super.visitIntInsn(i, i1)
        }
    
        /**
         * 该方法是 visitEnd 之前调用的方法,可以反复调用。用以确定类方法在执行时候的堆栈大小。
         * @param maxStack
         * @param maxLocals
         */
        @Override
        void visitMaxs(int maxStack, int maxLocals) {
            super.visitMaxs(maxStack, maxLocals)
        }
    
        @Override
        void visitVarInsn(int opcode, int var) {
            super.visitVarInsn(opcode, var)
        }
    
        @Override
        void visitJumpInsn(int opcode, Label label) {
            super.visitJumpInsn(opcode, label)
        }
    
        @Override
        void visitLookupSwitchInsn(Label label, int[] ints, Label[] labels) {
            super.visitLookupSwitchInsn(label, ints, labels)
        }
    
        @Override
        void visitMultiANewArrayInsn(String s, int i) {
            super.visitMultiANewArrayInsn(s, i)
        }
    
        @Override
        void visitTableSwitchInsn(int i, int i1, Label label, Label[] labels) {
            super.visitTableSwitchInsn(i, i1, label, labels)
        }
    
        @Override
        void visitTryCatchBlock(Label label, Label label1, Label label2, String s) {
            super.visitTryCatchBlock(label, label1, label2, s)
        }
    
        @Override
        void visitTypeInsn(int opcode, String s) {
            super.visitTypeInsn(opcode, s)
        }
    
        @Override
        void visitLocalVariable(String s, String s1, String s2, Label label, Label label1, int i) {
            super.visitLocalVariable(s, s1, s2, label, label1, i)
        }
    
        @Override
        void visitInsn(int opcode) {
            super.visitInsn(opcode)
        }
    
        //可以在这里通过注解的方式操作字节码
        @Override
        AnnotationVisitor visitAnnotation(String s, boolean b) {
            return super.visitAnnotation(s, b)
        }
    
        //进入方法时可以插入字节码
        @Override
        protected void onMethodEnter() {
            super.onMethodEnter()
        }
    
        //退出方法前可以插入字节码
        @Override
        protected void onMethodExit(int opcode) {
            super.onMethodExit(opcode)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126

    下面我们重点介绍ClassVisitor中的visit方法和visitMethod方法。

    • visit方法
      该方法是扫描类第一个会调用的方法。
      方法的完整定义如下:
    void visit(int version, int access, String name, String signature, String superName, String[] interfaces);
    
    • 1
    1. version
      表示JDK的版本,比如51,代表JDK版本1.7
    JDK版本int数值
    J2SE 852
    J2SE 751
    J2SE 650
    1. access
      类的修饰符。修饰符在ASM中是以“ACC_”开头的常量。可以作用到类级别上的修饰符主要有下面这些。
    修饰符含义
    ACC_PUBLICpublic
    ACC_PRIVATEprivate
    ACC_PROTECTEDprotected
    ACC_FINALfinal
    ACC_SUPERextends
    ACC_INTERFACE接口
    ACC_ABSTRACT抽象类
    ACC_ANNOTATION注解类型
    ACC_ENUM枚举类型
    ACC_DEPRECATED标记了@Deprecated注解的类
    ACC_SYNTHETICjavac生成
    1. name
      代表类的名称。我们通常会使用完整的包名+类名来表示类,比如:a.b.c.MyClass,但是在字节码中是以路径的形式表示,即:a/b/c/MyClass。值得注意的是,虽然这种方法是路径表示法但是不需要写明类的”.class“扩展名。
    2. signature
      表示泛型的信息,如果类并未定义任何泛型,则该参数为空。
    3. superName
      表示当前类所继承的父类。由于java的类是单根结构,即所有类都继承自Object,因此可以简单地理解为任何类都会具有一个父类。虽然在编写程序时我们没有去写extends明确继承的父类,但是JDK在编译时总会为我们加上“extends Object”。
    4. interfaces
      表示类所实现的接口列表。在java中,一个类时可以实现多个不同的接口,因此该参数是一个数组类型。
    • visitMethod方法
      该方法是当扫描器扫描到类的方法时进行调用。
      方法的完整定义如下:
    MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions)
    
    • 1

    各参数解释如下:

    1. access
      表示方法的修饰符。
    修饰符含义
    ACC_PUBLICpublic
    ACC_PRIVATEprivate
    ACC_PROTECTEDprotected
    ACC_FINALfinal
    ACC_SUPERextends
    ACC_INTERFACE接口
    ACC_ABSTRACT抽象类
    ACC_ANNOTATION注解类型
    ACC_ENUM枚举类型
    ACC_DEPRECATED标记了@Deprecated注解的类
    ACC_SYNTHETICjavac生成
    1. name
      表示方法名。

    2. desc
      表示方法签名,方法签名的格式如下:“(参数列表)返回值类型”。在ASM中不同的类型对应不同的代码,详细的对应关系如下表:

    代码类型
    “I”int
    “B”byte
    “C”char
    “D”double
    “F”float
    “J”long
    “S”short
    “Z”boolean
    “V”void
    “[…;”数组
    “[[…;”二维数组
    “[[[…;”三维数组

    下面我们举几个例子:

    参数列表方法参数
    String[][Ljava/lang/String;
    String[][][[Ljava/lang/String;
    int,String,String[]ILjava/lang/String;[Ljava/lang/String;
    int,boolean,long,String[],doubleIZJ[Ljava/lang/String;D
    Class,String,Object…paramTypeLjava/lang/Class;Ljava/lang/String;[Ljava/lang/Object;
    int[][I
    1. signature
      表示泛型相关的信息。

    2. exceptions
      表示将会抛出的异常,如果方法不会抛出异常,该参数为空。

    知道了大概需要用到的技术以及相关的API之后,我们现在就来解决文章开篇时提到的问题。
    首先,我们在原有工程中多创建一个依赖库,用于存放AntilazvLoad类。
    在这里插入图片描述
    然后将该文件编译成class文件,最后使用dx命令,打包成dex文件。接着我们将dex文件放到assets目录下。
    在这里插入图片描述
    接下来,我们要修改下BrettFix类的installPatch方法,让程序先去读取assets目录下的dex文件。为什么要先去读取assets目录下的dex文件呢?别忘了我们之前是没有AntilazvLoad类的,这样我们通过asm去获取该类是会报“找不到该类”的错误的。

     public static void installPatch(Application application, File patch) {
            //新增部分
            File hackDex = initHack(application);
            //===============
            List<File> patchs = new ArrayList<>();
            patchs.add(hackDex);
            if (patch.exists()) {
                Log.e(TAG,"path is "+patch.getAbsolutePath()+patch.canRead());
                patchs.add(patch);
            }
            //....
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    private static File initHack(Context context) {
            File hackFile = new File(context.getExternalFilesDir(""), "hack.dex");
            FileOutputStream fos = null;
            InputStream is = null;
            try {
                fos = new FileOutputStream(hackFile);
                is = context.getAssets().open("hack.dex");
                int len;
                byte[] buffer = new byte[2048];
                while ((len = is.read(buffer)) != -1) {
                    fos.write(buffer, 0, len);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (fos != null) {
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (is != null) {
                    try {
                        is.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
    
            }
            return hackFile;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    最后,我们在app模块build.gradle中通过groovy语言实现一段脚本。其实,groovy语言并不神秘,把它当成java语言就是了,甚至我们可以用java语言的语法来写groovy。唯一的缺点是,个人感觉build.gradle文件提示功能不太好,有时候不知道一个类到底有哪些方法。

    import java.util.jar.JarEntry
    import java.util.jar.JarFile
    import java.util.jar.JarOutputStream
    
    import org.apache.commons.compress.utils.IOUtils
    import org.objectweb.asm.ClassReader
    import org.objectweb.asm.ClassVisitor
    import org.objectweb.asm.ClassWriter
    import org.objectweb.asm.MethodVisitor
    import org.objectweb.asm.Opcodes
    import org.objectweb.asm.Type
    
    
    apply plugin: 'com.android.application'
    apply plugin: 'kotlin-android'
    apply plugin: 'kotlin-kapt'
    
    android {
        compileSdkVersion 31
        buildToolsVersion "28.0.3"
    
        defaultConfig {
            applicationId "com.brett.myapplication"
            minSdkVersion 16
            targetSdkVersion 31
            versionCode 1
            versionName "1.0"
            multiDexEnabled true
            testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        }
    
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            }
        }
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
        lintOptions {
            abortOnError false
    
        }
        buildFeatures {
            dataBinding true
        }
        viewBinding {
            enabled = true
        }
    }
    
    dependencies {
    
        implementation 'androidx.appcompat:appcompat:1.4.2'
        implementation 'com.google.android.material:material:1.6.1'
        implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
        implementation project(':lib')
        implementation project(':hack')
        implementation 'androidx.multidex:multidex:2.0.0'
    }
    
    //gradle执行会解析build.gradle文件,afterEvaluate表示在解析完成之后再执行我们的代码
    afterEvaluate({
        android.getApplicationVariants().all {
            variant ->
                //获得: debug/release
                String variantName = variant.name
                //首字母大写 Debug/Release
                String capitalizeName = variantName.capitalize()
    
                //这就是打包时,把jar和class打包成dex的任务
                Task dexTask =
                        project.getTasks().findByName("transformClassesWithDexBuilderFor" + capitalizeName);
    
                //在他打包之前执行插桩
                dexTask.doFirst {
                    //任务的输入,dex打包任务要输入什么? 自然是所有的class与jar包了!
                    FileCollection files = dexTask.getInputs().getFiles()
    
                    for (File file : files) {
                        //.jar ->解压-》插桩->压缩回去替换掉插桩前的class
                        // .class -> 插桩
                        String filePath = file.getAbsolutePath();
                        //依赖的库会以jar包形式传过来,对依赖库也执行插桩
                        if (filePath.endsWith(".jar")) {
                            processJar(file);
    
                        } else if (filePath.endsWith(".class")) {
                            //主要是我们自己写的app模块中的代码
                            processClass(variant.getDirName(), file);
                        }
                    }
                }
        }
    })
    
    static boolean isAndroidClass(String filePath) {
        return filePath.startsWith("android") ||
                filePath.startsWith("androidx")
    }
    
    static byte[] referHackWhenInit(InputStream inputStream) {
        // class的解析器
        ClassReader cr = new ClassReader(inputStream)
        // class的输出器
        ClassWriter cw = new ClassWriter(cr, 0)
        // class访问者,相当于回调,解析器解析的结果,回调给访问者
        ClassVisitor cv = new ClassVisitor(Opcodes.ASM5, cw) {
    
            //要在构造方法里插桩 init
            @Override
            public MethodVisitor visitMethod(int access, final String name, String desc,
                                             String signature, String[] exceptions) {
    
                MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
                mv = new MethodVisitor(Opcodes.ASM5, mv) {
                    @Override
                    void visitInsn(int opcode) {
                        //在构造方法中插入AntilazyLoad引用
                        if ("".equals(name) && opcode == Opcodes.RETURN) {
                            //引用类型
                            //基本数据类型 : I J Z
                            super.visitLdcInsn(Type.getType("Lcom/brett/hack/AntilazyLoad;"))
                        }
                        super.visitInsn(opcode)
                    }
                }
                return mv
            }
    
        }
        //启动分析
        cr.accept(cv, 0)
        return cw.toByteArray()
    }
    
    static void processClass(String dirName, File file) {
        String filePath = file.getAbsolutePath()
        //注意这里的filePath包含了目录+包名+类名,所以去掉目录
        String className = filePath.split(dirName)[1].substring(1);
        //application或者android support我们不管
        if (className.startsWith("com\\brett\\myapplication\\MyApplication") || isAndroidClass(className)) {
            return
        }
    
        try {
            FileInputStream is = new FileInputStream(filePath)
            byte[] byteCode = referHackWhenInit(is)
            is.close()
    
            FileOutputStream os = new FileOutputStream(filePath)
            os.write(byteCode)
            os.close()
        } catch (Exception e) {
            e.printStackTrace()
        }
    }
    
    static void processJar(File file) {
        try {
            File bakJar = new File(file.getParent(), file.getName() + ".bak");
            JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(bakJar))
    
            JarFile jarFile = new JarFile(file)
            Enumeration<JarEntry> entries = jarFile.entries()
            while (entries.hasMoreElements()) {
                JarEntry jarEntry = entries.nextElement()
    
                //读取jar包中的一个文件:class
                jarOutputStream.putNextEntry(new JarEntry(jarEntry.getName()))
                InputStream is = jarFile.getInputStream(jarEntry)
    
                String className = jarEntry.getName()
                if (className.endsWith(".class") && !className.startsWith
                        ("com/brett/myapplication/MyApplication")
                        && !isAndroidClass(className) && !className.startsWith("com/brett" +
                        "/hack")) {
                    byte[] byteCode = referHackWhenInit(is)
                    jarOutputStream.write(byteCode)
                } else {
                    //输出到临时文件
                    jarOutputStream.write(IOUtils.toByteArray(is))
                }
                jarOutputStream.closeEntry()
            }
            jarOutputStream.close()
            jarFile.close()
            file.delete()
            bakJar.renameTo(file)
        } catch (IOException e) {
            e.printStackTrace()
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195

    这样,我们就能在transformClassesWithDexBuilderFor任务执行之前插入我们需要的方法,但是使用gradle脚本来实现需要兼容各种版本的差异,甚至有些版本是没有transformClassesWithDexBuilderFor任务的。因此,我们最好通过Transform来实现相关的功能。

    Gradle Transform

    Gradle Transform是android官方提供给开发者在项目构建阶段(即由.class文件到.dex文件转换期间)用来修改.class文件的一套标准API。目前比较经典的应用是字节码插桩、代码注入等。
    先了解一下Transform的两个基础概念:

    • TransformInput
      TransformInput是指这些输入文件的一个抽象。它主要包括两个部分:DirectoryInput集合和JarInput集合。

    DirectoryInput集合是指以源码方式参与项目编译的所有目录结构及其目录下的源码文件。
    JarInput集合是指以jar包方式参与项目编译的所有本地jar包和远程jar包

    • TransformOutputProvider
      Transform的输出,通过它可以获取输出路径等信息

    Transform是一个抽象类,主要有以下几个方法:

    1. getName
      代表该Transform对应Task的名称,它会出现在app/build/intermediates/t
      ransforms目录下。
    2. getInputTypes
      它是指定Transform要处理的数据类型。目前主要支持两种数据类型:CLASSES和RESOUCES。

    CLASSES表示要处理编译后的字节码,可能是jar包也可能是目录。
    RESOUCES表示要处理标准的java资源。

    1. isIncremental
      该Transform是否支持增量构建
    2. getScopes
      指定Transform的作用域。常见的有下面7种:

    PROJECT:只处理当前项目
    SUB_PROJECTS:只处理子项目
    PROJECT_LOCAL_DEPS:只处理当前项目的本地依赖,例如jar, aar
    EXTERNAL_LIBRARIES:只处理外部的依赖库
    SUB_PROJECTS_LOCAL_DEPS:只处理子项目的本地依赖,例如jar, aar
    PROVIDED_ONLY:只处理本地或远程以provided形式引入的依赖库
    TESTED_CODE:测试代码
    SCOPE_FULL_PROJECT:包括了PROJECT、SUB_PROJECT、EXTERNAL_LIBRARIES,也是最常用的一个

    实例如下

    详细步骤:

    1. 新建一个Java Library module。名称叫plugin。
    2. 清空plugin/build.gradle文件中的内容,然后修改成如下内容
    apply plugin: 'groovy'//gradle会根据插件的名称,找到这个插件并且调用插件里面的apply方法
    apply plugin:  'maven'
    
    repositories {
        maven { url 'https://maven.aliyun.com/repository/google' }
        jcenter()
    }
    
    sourceSets {
        main {
            groovy {
                srcDir 'src/main/groovy'
            }
        }
    }
    
    dependencies {
        implementation gradleApi() //buildSrc目录下可以不引用gradleApi
        implementation localGroovy()
        implementation 'com.android.tools.build:gradle:3.4.3'
    }
    uploadArchives {
        repositories {
            mavenDeployer {
             	//提交到远程服务器:
                // repository(url: "http://www.xxx.com/repos") {
                //    authentication(userName: "admin", password: "admin")
                // }
                //本地的Maven地址
                repository(url: uri('../repo'))
                pom.groupId = 'com.brett.test'
                pom.artifactId = 'plugin'
                pom.version = '1.0.0'
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    1. 新建groovy目录
      在plugin/src/main目录下新建groovy目录。这是因为我们的插件是用groovy语言开发的,所以需要放到groovy目录下。然后在groovy目录下新建一个package,用来存放后面新建的Transform类文件,如下所示:

    在这里插入图片描述
    然后再gradle中执行upload任务会生成repo文件夹:
    在这里插入图片描述

    1. 创建Transform类
    package com.brett.plugin
    
    import com.android.build.api.transform.Context
    import com.android.build.api.transform.DirectoryInput
    import com.android.build.api.transform.JarInput
    import com.android.build.api.transform.QualifiedContent
    import com.android.build.api.transform.Transform
    import com.android.build.api.transform.TransformException
    import com.android.build.api.transform.TransformInput
    import com.android.build.api.transform.TransformInvocation
    import com.android.build.api.transform.TransformOutputProvider
    import com.android.build.gradle.internal.pipeline.TransformManager
    import com.android.build.api.transform.Format
    import com.android.utils.FileUtils
    import org.apache.commons.codec.digest.DigestUtils
    import org.gradle.api.Project
    
    class BrettTransform extends Transform{
        private Project project;
    
        public BrettTransform(Project project){
            this.project = project
        }
    
    
        @Override
        String getName() {
            return "BrettTransform"
        }
    
        /**
         * 需要处理的数据类型,有两种枚举类型
         * CLASSES 代表处理的 java 的 class 文件,RESOURCES 代表要处理 java 的资源
         * @return
         */
        @Override
        Set<QualifiedContent.ContentType> getInputTypes() {
            return TransformManager.CONTENT_CLASS
        }
    
        /**
         * 指 Transform 要操作内容的范围,官方文档 Scope 有 7 种类型:
         * 1. EXTERNAL_LIBRARIES        只有外部库
         * 2. PROJECT                   只有项目内容
         * 3. PROJECT_LOCAL_DEPS        只有项目的本地依赖(本地jar)
         * 4. PROVIDED_ONLY             只提供本地或远程依赖项
         * 5. SUB_PROJECTS              只有子项目。
         * 6. SUB_PROJECTS_LOCAL_DEPS   只有子项目的本地依赖项(本地jar)。
         * 7. TESTED_CODE               由当前变量(包括依赖项)测试的代码
         * @return
         */
        @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 {
            _transform(transformInvocation.context,transformInvocation.inputs,transformInvocation.outputProvider,transformInvocation.incremental)
        }
    
        void _transform(Context context, Collection<TransformInput> inputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
            if (!incremental) {
                outputProvider.deleteAll()
            }
            println("BrettTransform")
    
            /**Transform 的 inputs 有两种类型,一种是目录,一种是 jar 包,要分开遍历 */
            inputs.each { TransformInput input ->
                /**遍历目录*/
                input.directoryInputs.each { DirectoryInput directoryInput ->
                    /**当前这个 Transform 输出目录*/
                    File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
    
                    //。。。我们可以在这里使用asm,修改class文件然后替换原来的文件,进而实现热修复的功能
    
                    //将input的目录复制到output指定目录下
                    FileUtils.copyDirectory(directoryInput.file, dest)
                }
    
                /**遍历 jar*/
                input.jarInputs.each { JarInput jarInput ->
                    String destName = jarInput.file.name
    
                    /**截取文件路径的 md5 值重命名输出文件,因为可能同名,会覆盖*/
                    def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath).substring(0, 8)
                    /** 获取 jar 名字*/
                    if (destName.endsWith(".jar")) {
                        destName = destName.substring(0, destName.length() - 4)
                    }
    
                    /** 获得输出文件*/
                    File dest = outputProvider.getContentLocation(destName + "_" + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)
    
                    //。。。我们可以在这里使用asm,修改class文件然后替换原来的文件,进而实现热修复的功能
                    FileUtils.copyFile(jarInput.file, dest)
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105

    我们在Transform的函数中,先是打印一个提示消息,然后分别遍历目录和jar包。在实际的处理过程中,我们仅仅是把所有的输入文件拷贝到目标目录下,然后什么都没有做,相当于插件什么都没有处理。即使我们什么都没有做,也需要把所有的输入文件拷贝到目标目录下,否则下一个Task就没有TransformInput了。如果我们空实现了transform方法,最后会导致打包的apk缺少class文件。

    5.新建plugin
    在上面我们新建的plugin包下面新建BrettPlugin插件,并且注册新建的Transform类。

    package com.brett.plugin
    
    import com.android.build.gradle.AppExtension
    import org.gradle.api.Plugin
    import org.gradle.api.Project
    
    
    public class BrettPlugin implements Plugin<Project>{
    
        @Override
        void apply(Project project) {
            AppExtension appExtension = project.extensions.findByType(AppExtension.class)
            appExtension.registerTransform(new BrettTransform(project))
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    1. 创建properties文件
      在src/main目录下新建resources/META-INF/gradle-plugins,然后在此目录下新建一个文件:com.brett.plugin.properties,其中文件名com.brett.plugin就是用来指定我们插件名称的,也就是apply plugin:'x.y.z’中的x.y.z。该文件里面的内容为:
    implementation-class=com.brett.plugin.BrettPlugin
    
    • 1
    1. 在app/build.gradle文件中声明使用该插件
    apply plugin: 'com.android.application'
    apply plugin: 'com.brett.plugin'
    
    android {
        compileSdkVersion 30
        buildToolsVersion "30.0.3"
    
        defaultConfig {
            applicationId "com.brett.myapplication"
            minSdkVersion 16
            targetSdkVersion 30
            versionCode 1
            versionName "1.0"
    
            testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        }
    
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            }
        }
    }
    
    dependencies {
        implementation fileTree(dir: "libs", include: ["*.jar"])
        implementation 'androidx.appcompat:appcompat:1.0.0'
        implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
        testImplementation 'junit:junit:4.12'
        androidTestImplementation 'androidx.test.ext:junit:1.1.3'
        androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
        lintChecks project(path:':lintcheck')
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    1. 在项目级build.gradle中引用该插件
    // Top-level build file where you can add configuration options common to all sub-projects/modules.
    buildscript {
        ext {
            kotlin_version = '1.3.72'
        }
        repositories {
            maven { url 'https://maven.aliyun.com/repository/google' }
            maven {
                url "http://maven.aliyun.com/nexus/content/repositories/releases"
            }
            google()
            jcenter()
            maven {
                url uri('repo')
            }
        }
        dependencies {
            classpath "com.android.tools.build:gradle:4.0.1"
            classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
            classpath 'com.brett.test:plugin:1.0.0'
           // classpath project(":plugin")//如果是使用buildSrc实现,可以不用引用
    
            // NOTE: Do not place your application dependencies here; they belong
            // in the individual module build.gradle files
        }
    }
    
    allprojects {
        repositories {
            maven { url 'https://maven.aliyun.com/repository/google' }
            maven {
                url "http://maven.aliyun.com/nexus/content/repositories/releases"
            }
            google()
            jcenter()
        }
    }
    
    task clean(type: Delete) {
        delete rootProject.buildDir
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    最后执行aeesmbleDebug任务,这样我们就自定义了一个简单的gradle插件了。
    在这里插入图片描述
    注意:每当我们修改gradle插件的时候最好将引用grald插件的地方给注释掉,这样gradle同步的时候就不会报错。

    参考资料

    https://www.jianshu.com/p/d14f24c4a807

  • 相关阅读:
    C++的Odyssey之旅——STL
    网页JS自动化脚本(七)使用在线jQuery来操作元素
    SpringAOP的实现机制(底层原理)、应用场景等详解
    Golang基础教程
    基于 MediaPipe 的图像去背
    Vue使用总结-包括Vue2和Vue3
    失效的访问控制及漏洞复现
    计网个人作业02
    爬虫基础 - 爬虫学的好,牢饭吃得饱系列
    序列化-序列化的嵌套
  • 原文地址:https://blog.csdn.net/qq_36828822/article/details/126130446