自定义Android Plugin

前言

最近有些朋友一直在问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 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{

    @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 getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }
    //整个项目的处理
    @Override
    Set getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    // 是否增量编译
    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(Context context, Collection inputs, Collection 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)