美文网首页
自定义Android Plugin

自定义Android Plugin

作者: 被虐的小鸡 | 来源:发表于2020-10-22 15:51 被阅读0次

    前言

    最近有些朋友一直在问ARouter是如何实现的,于是我又把ARouter的源码搂
    (lou)了一遍。首先抛开注解处理器和拦截器,还有它内部的一些服务,诸如
    AutoWired,build,navigation之类的逻辑。他在编译期间通过gradle-plugin来对class字节码做了一些操作,那么gradle-plugin是什么?

    其实每一个Android开发都接触过他,不管是新建一个application module还是library module的时候在头部都有一个apply plugin。其实他是studio帮我们提供的一个gradle插件。

    打包apk流程

    接下来我们先考虑一下这个插件是什么时候起作用的?他又做了哪些事情?这个就需要我们对打包流程有一定的了解了。


    打包流程.png

    gradle-plugin中的Transform在字节码打包成dex文件的过程中会被执行。application的插件AppPlugin,它主要就干了检测Gradle版本,初始化插件初始化器和profiler,校验path和module的错误等等。附上一部分源码,感兴趣可以自己去看看。

    private void basePluginApply(@NonNull Project project) {
            // We run by default in headless mode, so the JVM doesn't steal focus.
            System.setProperty("java.awt.headless", "true");
    
            this.project = project;
            this.projectOptions = new ProjectOptions(project);
            checkGradleVersion(project, getLogger(), projectOptions);
            DependencyResolutionChecks.registerDependencyCheck(project, projectOptions);
    
            project.getPluginManager().apply(AndroidBasePlugin.class);
    
            checkPathForErrors();
            checkModulesForErrors();
    
            PluginInitializer.initialize(project);
            ProfilerInitializer.init(project, projectOptions);
            threadRecorder = ThreadRecorder.get();
    
            // initialize our workers using the project's options.
            Workers.INSTANCE.initFromProject(
                    projectOptions,
                    // possibly, in the future, consider using a pool with a dedicated size
                    // using the gradle parallelism settings.
                    ForkJoinPool.commonPool());
    
            ProcessProfileWriter.getProject(project.getPath())
                    .setAndroidPluginVersion(Version.ANDROID_GRADLE_PLUGIN_VERSION)
                    .setAndroidPlugin(getAnalyticsPluginType())
                    .setPluginGeneration(GradleBuildProject.PluginGeneration.FIRST)
                    .setOptions(AnalyticsUtil.toProto(projectOptions));
    
            if (!projectOptions.get(BooleanOption.ENABLE_NEW_DSL_AND_API)) {
    
                threadRecorder.record(
                        ExecutionType.BASE_PLUGIN_PROJECT_CONFIGURE,
                        project.getPath(),
                        null,
                        this::configureProject);
    
                threadRecorder.record(
                        ExecutionType.BASE_PLUGIN_PROJECT_BASE_EXTENSION_CREATION,
                        project.getPath(),
                        null,
                        this::configureExtension);
    
                threadRecorder.record(
                        ExecutionType.BASE_PLUGIN_PROJECT_TASKS_CREATION,
                        project.getPath(),
                        null,
                        this::createTasks);
            } else {
                // Apply the Java plugin
                project.getPlugins().apply(JavaBasePlugin.class);
    
                // create the delegate
                ProjectWrapper projectWrapper = new ProjectWrapper(project);
                PluginDelegate<E> delegate =
                        new PluginDelegate<>(
                                project.getPath(),
                                project.getObjects(),
                                project.getExtensions(),
                                project.getConfigurations(),
                                projectWrapper,
                                projectWrapper,
                                project.getLogger(),
                                projectOptions,
                                getTypedDelegate());
    
                delegate.prepareForEvaluation();
    
                // after evaluate callbacks
                project.afterEvaluate(
                        CrashReporting.afterEvaluate(
                                p -> {
                                    threadRecorder.record(
                                            ExecutionType.BASE_PLUGIN_CREATE_ANDROID_TASKS,
                                            p.getPath(),
                                            null,
                                            delegate::afterEvaluate);
                                }));
            }
    }
    

    自定义一个Android Plugin

    如果我们可以插手编译的过程,我们是不是可以动态的添加一些代码到源码里面?
    接下来我们动态的往Activity的字节码中的onCreate方法中插入一条日志和一行埋点的代码,定义Plugin需要完成以下几步

    • 新建一个module
      1.将module中除了libs,src,build.gradle文件之外的删除
      2.将src中的java改成groovy,并且删除所有的java文件
    • 创建一个xxPlugin.groovy文件
    • 在main文件夹下创建resources文件夹
    • 在resources文件夹下创建META-INF文件夹
    • 在META-INF文件夹下创建gradle-plugins文件夹
    • 在gradle-plugins文件夹下创建xxx.properties文件,这里的xxx是插件在使用时候的名字
    • 在xxx.properties中将上面编写的plugin.groovy的全路径配置进去
    # 等号后面的是我的插件的路径
    implementation-class=com.domain.android.testplugin.TestPlugin
    
    • build.gradle文件的修改,将文件中的内容全部删除
    apply plugin: 'groovy'
    apply plugin: 'maven'
    dependencies {
        implementation gradleApi() //gradle sdk
        implementation localGroovy() //groovy sdk
    
        implementation 'com.android.tools.build:gradle:3.4.1' //最好和自己主项目的版本一致
    
        implementation 'org.javassist:javassist:3.24.0-GA'
    }
    
    repositories {
        jcenter()
    }
    
    //打包
    uploadArchives {
        repositories.mavenDeployer {
            //本地仓库路径,以放到项目根目录下的 repo 的文件夹为例
            repository(url: uri('../repo'))
    
            //在依赖的时候会用到groupId,artifactId和version
            //groupId ,自行定义,组织名或公司名
            pom.groupId = 'com.domain'
    
            //artifactId,自行定义,项目名或模块名
            pom.artifactId = 'testplugin.android'
    
            //插件版本号
            pom.version = '1.0.0'
        }
    }
    
    插件的目录结构.png

    plugin使用

    • plugin module编译通过后,点击uploadArchives进行打包。


      打包plugin.png
    • 在项目的build.gradle中添加如下代码
    buildscript {
        repositories {
            //由于plugin打包到了本地,所以要加本地的maven地址
            maven {
                url uri('repo')
            }
            google()
            jcenter()
    
    
        }
        dependencies {
            classpath "com.android.tools.build:gradle:3.4.1"
            
            //com.domain为plugin的groupId,testplugin.android为artifactId,1.0.0为version。
            //这些都是在插件的module中的build.gradle中配置的
            classpath "com.domain:testplugin.android:1.0.0"
            // NOTE: Do not place your application dependencies here; they belong
            // in the individual module build.gradle files
        }
    }
    
    • 在需要使用的module中配置apply plugin,例如app module的build.gradle中
    apply plugin: 'com.android.application'
    //testplugin为插件module中testplugin.properties文件的文件名
    apply plugin: 'testplugin'
    

    到此为止整个插件就配置好了。每次使用只需要uploadArchives 插件模块打包,然后Rebuild Project就可以了。

    编写Plugin中的逻辑,无痕埋点

    当我们编译的时候会调用到transform,相当于一个Task,如果我们自定义的化会优先执行我们的,然后才会执行默认的。当然我们也可以定义多个transform。

    transform.png
    • plugin文件代码
    package com.domain.android.testplugin
    
    import com.android.build.gradle.AppExtension
    import org.gradle.api.Plugin
    import org.gradle.api.Project
    
    public class TestPlugin implements Plugin<Project>{
    
        @Override
        void apply(Project project) {
    
            def transform = new TestPluginTransform(project)
    
            def extension = project.extensions.findByType(AppExtension.class)
    
    
            extension.registerTransform(transform)
    
    
        }
    }
    
    • transform代码,最关键的代码就在transform方法中,需要对jar包和class文件分别处理,在下面的代码中我们只对class文件进行插入
    package com.domain.android.testplugin
    
    import com.android.build.api.transform.*
    import com.android.build.gradle.internal.pipeline.TransformManager
    import org.apache.commons.codec.digest.DigestUtils
    import org.apache.commons.io.FileUtils
    import org.gradle.api.Project
    
    class TestPluginTransform extends Transform{
    
        Project project
    
        TestPluginTransform(Project project){
            this.project=project
        }
    
        @Override
        String getName() {
            return "TestPluginTransform"
        }
    
         //字节码
        @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(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
            super.transform(context, inputs, referencedInputs, outputProvider, isIncremental)
    
            println("transform old")
        }
    
        @Override
        void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
            super.transform(transformInvocation)
    
            println("transform")
    
            def addCode='android.util.Log.i("plugin","plugin info");'
    
    
            def outputProvider=transformInvocation.outputProvider
    
            transformInvocation.inputs.each { TransformInput it ->
                // scan all jars
                it.jarInputs.each { JarInput jarInput ->
                    String destName = jarInput.name
                    // rename jar files
                    def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath)
                    if (destName.endsWith(".jar")) {
                        destName = destName.substring(0, destName.length() - 4)
                    }
                    // input file
                    File src = jarInput.file
                    // output file
                    File dest = outputProvider.getContentLocation(destName + "_" + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)
    
                    FileUtils.copyFile(src, dest)
    
                }
                println("transform jars end")
                // scan class files
                it.directoryInputs.each { DirectoryInput directoryInput ->
                    File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                    println("transform Directory add before")
    //                AddCodeUtils.addCode(directoryInput.file.absolutePath, project, addCode)
                    String root = directoryInput.file.absolutePath
                    if (!root.endsWith(File.separator))
                        root += File.separator
                    directoryInput.file.eachFileRecurse { File f->
                        def path=f.getAbsolutePath().replace(root,"")
    
                        if (f.isFile()&& path.startsWith("com/domain/android/test/")) {
                            println(path)
                            def bytes = Util.scanClass(f, path)
                            def outputStream = new FileOutputStream(f.path)
                            outputStream.write(bytes)
                            outputStream.close()
                        }
                    }
    
                    // copy to dest
                    FileUtils.copyDirectory(directoryInput.file, dest)
                }
            }
        }
    }
    
    • Util代码,主要用于扫描class文件,并且插入代码
    package com.domain.android.testplugin
    
    
    import org.objectweb.asm.*
    
    class Util{
    
        public static byte[] scanClass(File file,String path){
            def inputStream = new FileInputStream(file)
            ClassReader cr = new ClassReader(inputStream)
            ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS)
            ScanClassVisitor cv = new ScanClassVisitor(Opcodes.ASM5, cw,path)
            cr.accept(cv, ClassReader.EXPAND_FRAMES)
    
            def byteArray = cw.toByteArray()
            inputStream.close()
            return byteArray
        }
    
    
        static class ScanClassVisitor extends ClassVisitor{
            private String superName
            String path
            boolean isHasOnCreate=false
            ScanClassVisitor(int api, ClassVisitor cv,String path) {
                super(api, cv)
                this.path=path
    
    
            }
    
            @Override
            void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
                this.superName=superName
                super.visit(version, access, name, signature, superName, interfaces)
            }
    
            @Override
            MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
                MethodVisitor mv=super.visitMethod(access, name, desc, signature, exceptions)
                println("method name"+name+"...desc="+desc)
    
                if (isActivity(superName)&&name=="onCreate"&&desc=="(Landroid/os/Bundle;)V"){
                    println("superName"+superName)
                    mv=new InsertCodeClassVisitor(Opcodes.ASM5,mv,path)
                    isHasOnCreate=true
                }
    
                return mv
            }
             //扫描完之后如果Activity文件中没有onCreate方法需要我们手动添加onCreate
            @Override
            void visitEnd() {
                if (isActivity(superName)&&!isHasOnCreate){
                    println("activity end")
                    MethodVisitor methodVisitor = visitMethod(Opcodes.ACC_PUBLIC, "onCreate", "(Landroid/os/Bundle;)V", null, null)
                    methodVisitor.visitVarInsn(Opcodes.ALOAD,0)
                    methodVisitor.visitVarInsn(Opcodes.ALOAD,1)
                    methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL,
                    superName,"onCreate","(Landroid/os/Bundle;)V",false)
    
                    methodVisitor.visitInsn(Opcodes.RETURN)
                    methodVisitor.visitMaxs(2, 2)
                    isHasOnCreate=true
                    methodVisitor.visitEnd()
                }
                super.visitEnd()
            }
    
            private static boolean isActivity(String name){
                return name=="androidx/appcompat/app/AppCompatActivity" || name=="com/domain/android/test/MainActivity"
            }
        }
    
        static class InsertCodeClassVisitor extends MethodVisitor{
    
            String path
    
            InsertCodeClassVisitor(int api, MethodVisitor mv,String path) {
                super(api, mv)
                this.path=path
            }
    
            @Override
            void visitInsn(int opcode) {
                if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
                    //添加Log代码
                    mv.visitLdcInsn("TAG")
                    mv.visitLdcInsn(path)
                    mv.visitMethodInsn(Opcodes.INVOKESTATIC
                    ,"android/util/log"
                    ,"i"
                    ,"(Ljava/lang/String;Ljava/lang/String;)I"
                    ,false)
    
                    //添加埋点代码
                    mv.visitVarInsn(Opcodes.ALOAD,0)
                    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL
                            ,"java/lang/Object","getClass","()Ljava/lang/Class;",false)
    
                    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL
                    ,"java/lang/Class","getSimpleName","()Ljava/lang/String;",false)
                    mv.visitMethodInsn(Opcodes.INVOKESTATIC
                            ,"com/domain/android/test/trace/Trace"
                            ,"traceInPage"
                            ,"(Ljava/lang/String;)V"
                            ,false)                
                }
                super.visitInsn(opcode)
            }
        }
    }
    

    动态插入代码就完成了,但是可能很多同学不知道字节码如何去编写,那么可以在android studio中安装一个bytecode插件。
    拿出来一段代码,举个例子

    MethodVisitor methodVisitor = visitMethod(Opcodes.ACC_PUBLIC, "onCreate", "(Landroid/os/Bundle;)V", null, null)
    methodVisitor.visitVarInsn(Opcodes.ALOAD,0)
    methodVisitor.visitVarInsn(Opcodes.ALOAD,1)
    methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL,
    superName,"onCreate","(Landroid/os/Bundle;)V",false)
    methodVisitor.visitInsn(Opcodes.RETURN)
    methodVisitor.visitMaxs(2, 2)
    

    使用ASM的bytecode插件显示的class代码,大家对比一下其实是一摸一样的

      // access flags 0x4
      protected onCreate(Landroid/os/Bundle;)V
       L0
        LINENUMBER 8 L0
        ALOAD 0
        ALOAD 1
        INVOKESPECIAL com/domain/android/test/MainActivity.onCreate (Landroid/os/Bundle;)V
       L1
        LINENUMBER 9 L1
        RETURN
       L2
        LOCALVARIABLE this Lcom/domain/android/test/AActivity; L0 L2 0
        LOCALVARIABLE savedInstanceState Landroid/os/Bundle; L0 L2 1
        MAXSTACK = 2
        MAXLOCALS = 2
    

    app模块的代码

    • Trace,埋点类
    package com.domain.android.test.trace;
    
    import android.util.Log;
    
    public class Trace {
    
        public static void traceInPage(String className){
            Log.e("a","traceInPage");
        }
    }
    
    • MainActivity
    package com.domain.android.test;
    
    import android.os.Bundle;
    import android.os.PersistableBundle;
    
    import androidx.annotation.Nullable;
    import androidx.appcompat.app.AppCompatActivity;
    
    public class MainActivity extends AppCompatActivity {
    
        public static void a(){
            System.out.println("a");
        }
    
        public static int i=10;
    
        @Override
        public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
            super.onCreate(savedInstanceState, persistentState);
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
        }
    }
    
    • AActivity,没有onCreate方法
    package com.domain.android.test;
    
    public class AActivity extends MainActivity{
    
    }
    

    编译之后的字节码

    • AActivity.class


      AActivity.class.png
    • MainActivity.class


      MainActivity.class.png

    相关文章

      网友评论

          本文标题:自定义Android Plugin

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