美文网首页
Gadle插件实现代码插桩与构件时依赖

Gadle插件实现代码插桩与构件时依赖

作者: lotty_wh | 来源:发表于2020-03-11 23:24 被阅读0次

    目标

    在安卓开发中,传统的SDK集成方式,需要将原始的文件添加到项目中、或者在项目的构建文件(build.gradle)中明确添加对应的依赖,然后在适当的地方调取初始化等方法。这些方式至少需要修改构建文件、在项目原始java代码中编写调用等。但是,对于某些SDK而言,本身只需要进行一次初始化操作,这时候,有没有一种方式,让SDK自己来调用初始化方法,并且以最简洁的方式让用户去集成呢?
    目前市面上其实已经有很多的SDK采用了类似这种方式,对原始项目的侵入非常少。而且基本思路都是从gradle插件出发。只需要一行apply plugin代码,就可以达到目的。
    为了实现无侵入式的SDK集成,我们可以利用aop的方式实现在构件时动态修改安卓字节码,同时支持第三方库的构件时依赖。

    字节码注入

    入口

    想要不修改原始的java代码,并且实现SDK的代码的集成,实现初始化init方法的调用,就不得不在编译或者打包的过程中干预原始代码的编译结果。安卓APK文件的编译过程。从宏观上是:
    .java -->.class -->.dex -->.apk
    可以看到除我们可以修改.class、.dex文件生成过程来实现代码的注入。本文主要研究如何修改字节码文件.class

    工具

    目前主流的字节码注入工具主要是asm以及javassist,从使用者的反馈中可以看到asm的运行效率会比javassist高很多,但是asm的编码难度比较高。在进一步研究了asm之后,发现协助asm字节码生成工具AS插件ASMified,可以快速的生成ASM字节码代码。

    特点

    asm主要采用了访问者模式进行字节码文件的扫描,包括注释、成员变量、方法、内部类等。工具会提供加载类文件各种属性时的入口回调

    grdle插件

    插件开发有多种方式,可以查阅gradle官网api说明得知。本文选择了作为独立插件的开发方式,在本地搭建了maven的nexus服务,方便测试开发,当然也可以直接使用本地的.m2仓库

    引入的插件

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

    一个是groovy语法插件、一个是仓库插件

    必须的依赖

    implementation gradleApi()
    implementation 'com.android.tools.build:gradle:3.5.0'
    

    需要添加gradle的依赖,而且在实际开发过程中发现,gradle:3.5.0里面自依赖了asm框架,所以就不需要再次去添加asm的依赖了。这也方便了开发者排除重复依赖的问题

    目录结构

    aop-plugin(插件名称)
    ----src
        ----main
            ----groovy
                ----包名
                    ----.groovy文件(插件逻辑)
            ----java
                ----包名
                    ----.java文件(asm逻辑)
            ----resources
                ----META-INF
                    ----gradle-plugins
                        ----aop-plugin.properties
    ----build.gradle
    

    其中groovy目录是插件代码的目录,文件以.groovy结尾。java目录是asm注入代码的目录。resoures目录是固定结构。需要注意的是在aop-pligin.properties文件中必须指明插件实现类:

    implementation-class=com.lotty520.plugin.AopPlugin
    

    插件实现

    构建

    构建脚本build.gradle定义了构建的插件依赖,决定了构建插件的groovy属性

    apply plugin: 'maven'
    apply plugin: 'groovy'
    
    compileGroovy {
        sourceCompatibility = 1.7
        targetCompatibility = 1.7
        options.encoding = "UTF-8"
    }
    
    dependencies {
        implementation gradleApi()
        implementation 'com.android.tools.build:gradle:3.5.0'
    }
    
    repositories {
        jcenter()
    }
    
    uploadArchives {
        repositories {
            mavenDeployer {
                pom.groupId = 'com.lotty520.plugin'
                pom.artifactId = 'aop-plugin'
                pom.version = '1.50-SNAPSHOT'
                repository(url: 'http://localhost:8081/repository/maven-snapshots/')        }
        }
    }
    
    

    plugin

    gradle插件api的顶级接口,自定义插件时,需要实现该接口,该接口只有一个apply方法

    public interface Plugin<T> {
        void apply(T var1);
    }
    
    

    插件开始加载时的入口,我们可以在这里进行构建环境数据的获取或者修改

    Trasform

    安卓系统5.0新增的api。主要用来控制java->class转换操作的中间处理。主要api有

    public abstract String getName();
    
    public abstract Set<ContentType> getInputTypes();
        
    public abstract Set<? super Scope> getScopes();
    
    public abstract boolean isIncremental();
    
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
            this.transform(transformInvocation.getContext(), transformInvocation.getInputs(), transformInvocation.getReferencedInputs(), transformInvocation.getOutputProvider(), transformInvocation.isIncremental());
        }
    
    

    其中isIncremental是标记是否开启增量。getInputTypes()用来标记检测的文件类型,主要是class文件和资源文件。getScopes()用来检测文件目录范围。transform是核心的方法,主要用来实现文件的遍历和字节码注入。

    代码实现

    检测jar和目录的方式实现可以参照以下代码:改代码对扫描的目录和jarz中的class文件进行注入操作。利用了ASM的ClassVisitor、ClassReader、ClassWritor等实现字节码文件的读取、注入、回写等操作。

    package com.lotty520.plugin
    
    import com.android.build.api.transform.*
    import com.android.build.gradle.AppExtension
    import com.android.build.gradle.AppPlugin
    import com.android.build.gradle.LibraryPlugin
    import com.android.build.gradle.internal.pipeline.TransformManager
    import org.apache.commons.io.FileUtils
    import org.apache.commons.io.IOUtils
    import org.gradle.api.Plugin
    import org.gradle.api.Project
    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
    
    import static org.objectweb.asm.ClassReader.EXPAND_FRAMES
    
    class AopPlugin extends Transform implements Plugin<Project> {
    
        @Override
        void apply(Project project) {
            println("==================")
            println("      Gradle插件1.50         ")
            println("==================")
            if (!project.android) {
                throw new IllegalStateException('Must apply \'com.android.application\' or \'com.android.library\' first!')
            }
            def isApp = project.plugins.withType(AppPlugin)
            def isLib = project.plugins.withType(LibraryPlugin)
            if (isApp) {
                println("build is app.")
            } else {
                println("build is lib.")
            }
            if (!isApp && !isLib) {
                throw new IllegalStateException("'android' or 'android-library' plugin required.")
            }
            def list = project.getConfigurations().toList().iterator()
            while (list.hasNext()) {
                def config = list.next().getName()
                if ("implementation".equals(config)) {
                    println("----Configuration----" + config)
                    project.getDependencies().add(config, "com.squareup.retrofit2:retrofit:2.5.0")
                }
            }
            project.extensions.getByType(AppExtension).registerTransform(this)
    
        }
    
        @Override
        String getName() {
            return "AopPlugin"
        }
    
        @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 {
            println("==================")
            println("       transform begin.        ")
            println("==================")
            Collection<TransformInput> inputs = transformInvocation.inputs
            TransformOutputProvider outputProvider = transformInvocation.outputProvider
            //删除之前的输出
            if (outputProvider != null)
                outputProvider.deleteAll()
            //遍历inputs
            inputs.each { TransformInput input ->
                //遍历directoryInputs
                input.directoryInputs.each { DirectoryInput directoryInput ->
                    handleDirectoryInput(directoryInput, outputProvider)
                }
    
                //遍历jarInputs
                input.jarInputs.each { JarInput jarInput ->
                    handleJarInputs(jarInput, outputProvider)
                }
            }
        }
    
        /**
         * 处理文件目录下的class文件
         */
        private static void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
            //是否是目录
            if (directoryInput.file.isDirectory()) {
                //列出目录所有文件(包含子文件夹,子文件夹内文件)
                directoryInput.file.eachFileRecurse { File file ->
                    def name = file.name
                    if (name.contains("View")) {
                        println("目录文件::::" + name)
                    }
                    if (checkClassFile(name)) {
                        ClassReader classReader = new ClassReader(file.bytes)
                        ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                        ClassVisitor cv = new ClassVisitorImpl(classWriter)
                        classReader.accept(cv, 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(directoryInput.name,
                    directoryInput.contentTypes, directoryInput.scopes,
                    Format.DIRECTORY)
            FileUtils.copyDirectory(directoryInput.file, dest)
        }
    
        /**
         * 处理Jar中的class文件
         */
        private static void handleJarInputs(JarInput jarInput, TransformOutputProvider outputProvider) {
            if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
                println("JAR文件::::" + jarInput.file.getAbsolutePath())
                // 创建新的jar文件
                File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")
                //避免上次的缓存被重复插入
                if (tmpFile.exists()) {
                    tmpFile.delete()
                }
                JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
                //用于保存
                JarFile originJar = new JarFile(jarInput.file)
                Enumeration enumeration = originJar.entries()
                while (enumeration.hasMoreElements()) {
                    JarEntry nextEntry = (JarEntry) enumeration.nextElement()
                    //名称
                    String entryName = nextEntry.getName()
                    //创建zipEntry对象
                    ZipEntry zipEntry = new ZipEntry(entryName)
                    //获取当前entry的输入流
                    InputStream inputStream = originJar.getInputStream(nextEntry)
                    //将当前entry添加到临时文件中
                    jarOutputStream.putNextEntry(zipEntry)
                    //插桩class
                    if (checkClassFile(entryName)) {
                        //class文件处理
                        if (entryName.contains("View")) {
                            println("class文件::::" + entryName)
                        }
                        ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
                        ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                        ClassVisitor cv = new ClassVisitorImpl(classWriter)
                        //原始的CR接收CW的访问,开始重排字节码
                        classReader.accept(cv, EXPAND_FRAMES)
                        //回写
                        byte[] code = classWriter.toByteArray()
                        jarOutputStream.write(code)
    
                    } else {
                        // 直接拷贝,不插桩
                        jarOutputStream.write(IOUtils.toByteArray(inputStream))
                    }
                    jarOutputStream.closeEntry()
                }
                //结束
                jarOutputStream.close()
                originJar.close()
                //重命名输出文件,因为可能同名,会覆盖
                def jarName = jarInput.name
                if (jarName.endsWith(".jar")) {
                    jarName = jarName.substring(0, jarName.length() - 4)
                }
                def fileName = "aop-plugin" + jarName
                //替换输出
                def dest = outputProvider.getContentLocation(fileName, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                FileUtils.copyFile(tmpFile, dest)
                tmpFile.delete()
            }
        }
    
        /**
         * 检查class文件是否需要处理,只处理需要的class文件
         * @param fileName 文件名
         * @return 是否需要处理
         */
        private static boolean checkClassFile(String name) {
            return (name.endsWith(".class") && !name.startsWith("R\$")
                    && !"R.class".equals(name) && !"BuildConfig.class".equals(name) && !"AsmByteCode.class".equals(name))
        }
    }
    
    

    注入部分代码实现

    ClassVisitor

    对类的访问,可以用来决定哪些类需要被注入、添加类属性、添加实例方法、类方法等。具体可以参考ASM官方文档,查看调用方式。以下代码是在MainActivy中添加了成员变量cls(String)、begin(long)、end(long)。ClassVisitor会在扫描字节码文件时,严格的按照调用顺序,调用相应的visit(仅调用一次)、visitMethod(多次)、visitEnd(一次)。当然还有访问注解、访问属性等visitXXX方法。这里不列举。

    package com.lotty520.plugin;
    
    import org.objectweb.asm.ClassVisitor;
    import org.objectweb.asm.FieldVisitor;
    import org.objectweb.asm.MethodVisitor;
    import org.objectweb.asm.Opcodes;
    
    public class ClassVisitorImpl extends ClassVisitor {
    
        private static final String INJEXT_CLS = "MainActivity";
        private static final String INJEXT_METHOD = "printEnter";
    
        private static final String CLS_VIEW = "printEnter";
        private static final String METHOD_VIEW = "performClick";
    
    
        private String currentCls;
    
        public ClassVisitorImpl(ClassVisitor cv) {
            super(Opcodes.ASM5, cv);
        }
    
        public ClassVisitorImpl(int api) {
            super(api);
        }
    
        public ClassVisitorImpl(int api, ClassVisitor cv) {
            super(api, cv);
        }
    
        @Override
        public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            super.visit(version, access, name, signature, superName, interfaces);
            currentCls = name;
        }
    
        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
            if (INJEXT_METHOD.equals(name)) {
                return new AdviceAdapterImpl(Opcodes.ASM5, mv, access, name, desc);
            }
            return mv;
        }
    
        @Override
        public void visitEnd() {
            if (currentCls != null && currentCls.contains(INJEXT_CLS)) {
                FieldVisitor clsname = cv.visitField(Opcodes.ACC_PRIVATE, "clsname", "Ljava/lang/String;", null, null);
                if (clsname != null) {
                    clsname.visitEnd();
                }
                FieldVisitor begin = cv.visitField(Opcodes.ACC_PRIVATE, "begin", "J", null, null);
                if (begin != null) {
                    begin.visitEnd();
                }
                FieldVisitor end = cv.visitField(Opcodes.ACC_PRIVATE, "end", "J", null, null);
                if (end != null) {
                    end.visitEnd();
                }
    
            }
            cv.visitEnd();
        }
    }
    
    

    MethodVisitor

    对方法的访问,实现字节码注入的核心实现。可以实现在调用对应方法之前、之后进行字节码注入,从而实现对应的需求。针对上面注入的成员变量,在方法调用前后,我们打印一下这些成员变量的值。主要实现功能为:

    public void enter() {
            clsname = getClass().getPackage().getName();
            begin = System.currentTimeMillis();
        }
    
    public void exit() {
            end = System.currentTimeMillis();
            Log.e("wh", "cls::" + clsname + "-->method cost:::" + (end - begin));
        }
        
    

    为了将java转换成asm字节码,需要借助ASM ByteCode Outline插件来进行打码转换,转换后,我们的实现逻辑为:

    package com.lotty520.plugin;
    
    import org.objectweb.asm.MethodVisitor;
    import org.objectweb.asm.commons.AdviceAdapter;
    
    public class AdviceAdapterImpl extends AdviceAdapter {
    
        private static final String OWNER = "com/xapplication/MainActivity";
    
        protected AdviceAdapterImpl(int api, MethodVisitor mv, int access, String name, String desc) {
            super(api, mv, access, name, desc);
        }
    
        @Override
        protected void onMethodEnter() {
            mv.visitLdcInsn("wh");
            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
            mv.visitInsn(DUP);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            mv.visitLdcInsn("method::enter");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
            mv.visitInsn(POP);
    
            mv.visitVarInsn(ALOAD, 0);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getPackage", "()Ljava/lang/Package;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Package", "getName", "()Ljava/lang/String;", false);
            mv.visitFieldInsn(PUTFIELD, OWNER, "clsname", "Ljava/lang/String;");
    
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitFieldInsn(PUTFIELD, OWNER, "begin", "J");
        }
    
        @Override
        protected void onMethodExit(int opcode) {
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitFieldInsn(PUTFIELD, OWNER, "end", "J");
    
            mv.visitLdcInsn("wh");
            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
            mv.visitInsn(DUP);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            mv.visitLdcInsn("pkg::");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitFieldInsn(GETFIELD, OWNER, "clsname", "Ljava/lang/String;");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitLdcInsn("-->method cost:::");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitFieldInsn(GETFIELD, OWNER, "end", "J");
            mv.visitVarInsn(ALOAD, 0);
            mv.visitFieldInsn(GETFIELD, OWNER, "begin", "J");
            mv.visitInsn(LSUB);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
            mv.visitInsn(POP);
    
            mv.visitLdcInsn("wh");
            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
            mv.visitInsn(DUP);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            mv.visitLdcInsn("method::exit");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
            mv.visitInsn(POP);
        }
    }
    
    

    打包集成

    打包插件

    直接运行aop:uploadArchives就可以编译插件并上传到maven仓库中

    集成插件

    在项目的根目录的build.gradle中添加插件依赖版本

    // Top-level build file where you can add configuration options common to all sub-projects/modules.
    buildscript {
        repositories {
            google()
            jcenter()
            maven {
                url = 'http://localhost:8081/repository/maven-snapshots/'
            }
        }
        dependencies {
            classpath 'com.android.tools.build:gradle:3.5.0'
            // NOTE: Do not place your application dependencies here; they belong
            // in the individual module build.gradle files
            classpath "cn.tongdun.plugin:aop-plugin:1.49-SNAPSHOT"
    
        }
    }
    allprojects {
        repositories {
            google()
            jcenter()
            maven {
                url = 'http://localhost:8081/repository/maven-snapshots/'
            }
        }
    }
    
    task clean(type: Delete) {
        delete rootProject.buildDir
    }
    
    

    在项目的主module的build.gradle中引入插件
    apply plugin: 'aop-plugin'
    到此,就可以运行APP查看结果了。

    相关连接

    字节码指令解释

    ASM字节码官网

    相关文章

      网友评论

          本文标题:Gadle插件实现代码插桩与构件时依赖

          本文链接:https://www.haomeiwen.com/subject/wngkjhtx.html