阅读 521

Android Gradle 自定义plugin

摘要

插件很重要,比如咱们

apply plugin: 'com.android.application'// 这样我们才可以在build.gradle中使用 android的api
apply plugin: 'kotlin-android-extensions'
复制代码

都是系统的plugin。自定义plugin对于android开发来说很重要,可以做很多内容 ,比如 多渠道打包,修改资源,压缩图片,修改字节码,切面编程aop思想等等,简单学习一下 制定自己的 plugin

开始自定义

一般有三种

  1. 直接写在app的 build.gradle 中,这个一般没人用
  2. 把插件代码放在buildSrc中 也很少这样使用,我们做的例子用的是这个
  3. 发布到本地或者远程仓库供其他项目使用

第一种

直接在我们app的build.gradle中最外层,加入下面的类继承自Plugin,并且引用我们的plugin

// 引用咱们 自己的
apply plugin: DemoPlugin
// 编写plugin类
class DemoPlugin implements Plugin<Project>{

    @Override
    void apply(Project project) {
        println "DemoPlugin-----"
    }
}
复制代码

然后我们执行 ./gradlew 就会打印 "DemoPlugin-----" 我们的代码了

第二种

创建文件夹

插件可以使用Groovy、Koglin和java语言我们在主工程目录下创建 buildSrc/src/main/java/com.nzy。然后创建一个class

public class DemoPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        System.out.println("DemoPlugin-----");
    }
}
复制代码

创建配置文件

在src文件夹中创建 resources,在 resources 创建META-INF目录,在META-INF创建gradle-plugins目录,之后再里面创建一个com.nzy.plugin.properties 配置文件,名字就是我们引用的时候需要写的。

// 把咱们的类指定在这里
implementation-class=com.nzy.DemoPlugin
复制代码

在buildSrc中创建build.gradle,加入以下代码,主要是咱们的plugin需要android的一下api

apply plugin:'java'

dependencies {
    implementation 'com.android.tools.build:gradle:4.1.1'
    implementation 'com.android.tools.build:gradle-api:4.1.1'
    // ASM 相关
    implementation 'org.ow2.asm:asm:7.1'
    implementation 'org.ow2.asm:asm-util:7.1'
    implementation 'org.ow2.asm:asm-commons:7.1'
}

repositories {
    google()
    jcenter()
}
复制代码

使用

在app的build.gradle中加入 apply plugin: 'com.nzy.plugin',这里的名字就是咱们配置文件的名字。 之后执行 ./gradlew 就会打印 "DemoPlugin-----" 我们的代码了

第三种 这里就不讲了,需要maven仓库

我们利用ASM字节码插庄实现一个方法耗时的功能

需要了解 Transform 以及 ASM字节码的知识,Gradle Transform 是 Android 官方提供的在这一阶段用来修改 .class 文件的一套标准 API。 这一应用现在主要集中在字节码查找、代码注入等

3.1 写咱们自己的plugin

把咱们自己的 TimeTransform 注册到android中

public class TimePlugin implements Plugin<Project> {

    @Override
    public void apply(Project project) {
        System.out.println("-------NzyPlugin------");
        // 注册 Transform, AppExtension 依赖 gradle,所以该模块需要导入 gradle
        AppExtension appExtension = project.getExtensions().getByType(AppExtension.class);
        // 打印每个方法的时间
        appExtension.registerTransform(new TimeTransform(project));
    }
}
复制代码

3.2 自定义 TimeTransform

遍历每个class 和jar 找到如果有 DebugLog的注解的就开始添加

public class TimeTransform extends Transform {

    private Map<String, File> modifyMap = new HashMap<>();
    private static final String NAME = "TimeTransform";
    private static final String TAG = "TimeTransform:";
    private final Logger mLogger;
    public Project mProject;

    public TimeTransform(Project project) {
        mProject = project;
        mLogger = mProject.getLogger();

    }

    @Override
    public String getName() {
        return NAME;
    }

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }

    /**
     * 指Transform要操作内容的范围,官方文档Scope有7种类型:
     * <p>
     * EXTERNAL_LIBRARIES        只有外部库
     * PROJECT                   只有项目内容
     * PROJECT_LOCAL_DEPS        只有项目的本地依赖(本地jar)
     * PROVIDED_ONLY             只提供本地或远程依赖项
     * SUB_PROJECTS              只有子项目。
     * SUB_PROJECTS_LOCAL_DEPS   只有子项目的本地依赖项(本地jar)。
     * TESTED_CODE               由当前变量(包括依赖项)测试的代码
     * SCOPE_FULL_PROJECT        整个项目
     */
    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }

    @Override
    public boolean isIncremental() {
        return false;
    }

    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {

        super.transform(transformInvocation);
        // inputs中是传过来的输入流,其中有两种格式,一种是jar包格式一种是目录格式。
        Collection<TransformInput> inputs = transformInvocation.getInputs();

        // 获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做不然编译会报错
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();

        if(outputProvider!=null){
            outputProvider.deleteAll();
        }



        // 遍历
        for (TransformInput input : inputs) {
            // 处理jar
            for (JarInput jarInput : input.getJarInputs()) {

                try {
                    transformJar(transformInvocation, jarInput);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }


            // 处理目录里面的Class
            for (DirectoryInput directoryInput : input.getDirectoryInputs()) {

                transformDirectory(transformInvocation, directoryInput);
            }
        }


    }

    private void transformJar(TransformInvocation invocation, JarInput input) throws Exception {
            File tempDir = invocation.getContext().getTemporaryDir();
            String destName = input.getFile().getName();
            String hexName = DigestUtils.md5Hex(input.getFile().getAbsolutePath()).substring(0, 8);
            if (destName.endsWith(".jar")) {
                destName = destName.substring(0, destName.length() - 4);
            }
            // 获取输出路径
            File dest = invocation.getOutputProvider()
                    .getContentLocation(destName + "_" + hexName, input.getContentTypes(), input.getScopes(), Format.JAR);

            JarFile originJar = new JarFile(input.getFile());
            File outputJar = new File(tempDir, "temp_"+input.getFile().getName());
            JarOutputStream output = new JarOutputStream(new FileOutputStream(outputJar));

            // 遍历原jar文件寻找class文件
            Enumeration<JarEntry> enumeration = originJar.entries();
            while (enumeration.hasMoreElements()) {
                JarEntry originEntry = enumeration.nextElement();
                InputStream inputStream = originJar.getInputStream(originEntry);

                String entryName = originEntry.getName();
                if (entryName.endsWith(".class")) {
                    JarEntry destEntry = new JarEntry(entryName);
                    output.putNextEntry(destEntry);

                    byte[] sourceBytes = IOUtils.toByteArray(inputStream);
                    // 修改class文件内容
                    byte[] modifiedBytes = referHackClass(sourceBytes);
                    if (modifiedBytes == null) {
                        modifiedBytes = sourceBytes;
                    }
                    output.write(modifiedBytes);
                    output.closeEntry();
                }
            }
            output.close();
            originJar.close();

            // 复制修改后jar到输出路径
            FileUtils.copyFile(outputJar, dest);
        }





    private void transformDirectory(TransformInvocation invocation, DirectoryInput input) throws IOException {
        File tempDir = invocation.getContext().getTemporaryDir();
        // 获取输出路径
        File dest = invocation.getOutputProvider()
                .getContentLocation(input.getName(), input.getContentTypes(), input.getScopes(), Format.DIRECTORY);
        File dir = input.getFile();
        if (dir != null && dir.exists()) {
            traverseDirectory(tempDir, dir);
            FileUtils.copyDirectory(input.getFile(), dest);
            for (Map.Entry<String, File> entry : modifyMap.entrySet()) {
                File target = new File(dest.getAbsolutePath() + entry.getKey());
                if (target.exists()) {
                    target.delete();
                }
                FileUtils.copyFile(entry.getValue(), target);
                entry.getValue().delete();

                mLogger.log(LogLevel.ERROR,target.getAbsolutePath()+"-----");
            }
        }
    }

    private void traverseDirectory(File tempDir, File dir) throws IOException {
        for (File file : Objects.requireNonNull(dir.listFiles())) {
            if (file.isDirectory()) {
                traverseDirectory(tempDir, file);
            } else if (file.getAbsolutePath().endsWith(".class")) {
                String className = path2ClassName(file.getAbsolutePath()
                        .replace(dir.getAbsolutePath() + File.separator, ""));
                byte[] sourceBytes = IOUtils.toByteArray(new FileInputStream(file));
                byte[] modifiedBytes = referHackClass(sourceBytes);
                File modified = new File(tempDir, className.replace(".", "") + ".class");

                if (modified.exists()) {
                    modified.delete();
                }
                modified.createNewFile();
                new FileOutputStream(modified).write(modifiedBytes);
                String key = file.getAbsolutePath().replace(dir.getAbsolutePath(), "");
                modifyMap.put(key, modified);

                mLogger.log(LogLevel.ERROR,key+"----"+file.getAbsolutePath());
            }
        }
    }

    private byte[] referHackClass(byte[] inputStream) {
        ClassReader classReader = new ClassReader(inputStream);
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        ClassVisitor cv = new AutoClassVisitor(Opcodes.ASM6, classWriter);

        classReader.accept(cv, ClassReader.EXPAND_FRAMES);
        return classWriter.toByteArray();
    }

    static String path2ClassName(String pathName) {
        return pathName.replace(File.separator, ".").replace(".class", "");
    }

}

复制代码

代码地址

文章分类
Android
文章标签