Abstract

Plug-ins are important, like us

Application plugin: 'kotlin-android-extensions' // We can use the Android API in build.gradle to apply plugin: 'kotlin-android-extensions'Copy the code

Are plugins to the system. Custom plugin for Android development is very important, you can do a lot of content, such as multi-channel packaging, modify resources, compress images, modify bytecode, aspect programming AOP ideas and so on, simple learning to develop their own plugin

Start customizing

There are generally three kinds

  1. Write it directly in your app’s build.gradle
  2. Putting plug-in code in buildSrc is also rarely used, as is the example we did
  3. Publish to local or remote repositories for use by other projects

The first kind of

Add the following classes directly to the outermost layer of our app build.gradle that inherit from Plugin and reference our Plugin

// Use our own apply plugin: Class DemoPlugin implements plugin <Project>{@override void apply(Project Project) {println "DemoPlugin-----" } }Copy the code

Then we execute./gradlew to print “DemoPlugin—–” to our code

The second,

Creating a folder

Plug-ins can use Groovy, Koglin and Java language we in the main project directory to create buildSrc/SRC/main/Java/com. Nzy. Then create a class

public class DemoPlugin implements Plugin<Project> { @Override public void apply(Project project) { System.out.println("DemoPlugin-----"); }}Copy the code

Creating a Configuration File

Create resources in SRC folder, create meta-inf directory in the resources, create gradle in meta-inf – plugins directory, then create a com inside. Nzy. Plugin. The properties configuration file, The name is what we need to write when we quote it.

// specify our class here implementation-class= com.nzy.demopluginCopy the code

Build. Gradle in buildSrc. Add the following code to build

Apply the plugin: 'Java' dependencies {implementation 'com. Android. View the build: gradle: 4.4.1' implementation 'com. Android. Tools. Build: gradle - API: 4.4.1' / / ASM related implementation 'org. Ow2. ASM: ASM: 7.1' implementation 'org.ow2.asm: ASM: Commons :7.1'} Repositories {Google () jCenter ()}Copy the code

use

Add the apply plugin: ‘com.nzy.plugin’ to our build.gradle app. The name here is the name of our configuration file. Executing./gradlew will print “DemoPlugin—–” to our code

The third, which I won’t cover here, requires a Maven repository

We use ASM bytecode interpolation to implement a method time consuming function

Gradle Transform is a standard Android API for modifying.class files at this stage. The application now focuses on bytecode lookup, code injection, and so on

3.1 Write our own plugin

Register our own TimeTransform with Android

public class TimePlugin implements Plugin<Project> { @Override public void apply(Project project) { System.out.println("-------NzyPlugin------"); // Register Transform AppExtension to gradle. Gradle AppExtension AppExtension = project.getextensions ().getByType(AppExtension.class); / / print each method's time appExtension registerTransform (new TimeTransform (project)); }}Copy the code

3.2 Customizing TimeTransform

Iterate through each class and JAR to find if there are DebugLog annotations and start adding them

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", "");
    }

}

Copy the code

The code address