Modified code during compile time of Gradle plugin actual combat

background

We often use third-party libraries in our work. It is inevitable that there will be various problems in these libraries. There is no way to issue issues to the authors of the open source libraries. However, this efficiency is very low and the authors may not modify them, so only Change it yourself.

solution

  • We can modify the project chone, but this efficiency is very low, and unpredictable problems may also be encountered, which greatly increases the development cost.
  • Develop the Gradle plug-in and use Javassit to dynamically modify the existing methods in the class. This method is very efficient and does not affect the source code.

Javassit

javassist is an open source library for dynamically modifying java bytecode. It can add/modify methods, insert code, etc. in the compiled class file, and does not require in-depth understanding of bytecode. At the same time, you can also generate a new class object through a completely manual method. The specific usage of javassit can be viewed: Tutorial-1 , Tutorial-2 , Tutorial-3

Gradle

The Android Studio project is built using Gradle. The build tool Gradle can be seen as a script containing a series of tasks. After these tasks are executed in sequence, the project is packaged successfully. The essence of a custom Gradle plug-in is to extract and encapsulate logically independent code so that we can more efficiently reuse functions through plug-in dependence. The gradle plugins under Android are divided into two categories:

  • Script plug-in: Same as ordinary gradle script writing form, you can write it directly in the build.gradle file, or you can create a new gradle script file yourself
  • Object plug-in: It is referenced by the full path class name or id of the plug-in. It mainly has three writing forms, as shown below:
    1) Directly write under the current build script.
    2) Write in the buildSrc directory.
    3) Written in a completely independent project.

buildSrc

We use the buildSrc default plugin directory. The code in this directory will be automatically compiled and packaged during build, and then it will be added to the classpath in buildScript, so it can be directly used by other modules without any additional configuration. Gradle script reference. Note: In Gradle 6.0 or above, do not configure in the settings.gradle file, otherwise it will generate:'buildSrc' cannot be used as a project name as it is a reserved name exception

  • The execution timing of buildSrc is not only earlier than any project (build.gradle), but also earlier than settings.gradle.
  • If':buildSrc' is configured in settings.gradle, the buildSrc directory will be treated as a sub-project, because it will be executed twice. So the configuration of':buildSrc' should be deleted in settings.gradle.

step

1. Create a new module in the project root directory and name it buildSrc with a fixed name.
2. Delete an empty main directory under the src directory, and create a groovy directory and a resources directory under the main directory.
3. Delete all the configuration of build.gradles and add the following configuration

apply plugin: 'groovy'

dependencies {
    
    
    implementation gradleApi()
    implementation localGroovy()
    implementation "com.android.tools.build:gradle:4.1.0"
    implementation "commons-io:commons-io:2.4"
    implementation "org.javassist:javassist:3.25.0-GA"
}
repositories {
    
    
    google()
    jcenter()
}

4. Create your own package name in the groovy directory com.gh.gamecenter.plugin, and then create two groovy classes, inheriting Plugin and Transform respectively , the code is as follows:

class GhTransform extends Transform {
    
    
  ......
}
class GhPlugin implements Plugin<Project> {
    
    
    void apply(Project project) {
    
    
        project.android.registerTransform(new GhTransform(project))
    }
}

5. Create a META-INF.gradle-plugins directory under the resources directory, and then create a com.gh.gamecenter.plugin.propertiesfile in it. The content before properties is the name of the plug-in. Add the following content to the file:

implementation-class=com.gh.gamecenter.plugin.GhPlugin

6. Finally, we introduce our plugin in build.gradle, so that we are basically configured.

apply plugin: "com.gh.gamecenter.plugin"
android {
......
}

The above is the basic configuration of the custom plug-in, the next step is to implement the plug-in function

Transform

Google officially provides Transfrom API after Android Gradle V1.5.0, allowing third-party Plugins to manipulate .class files during the compilation process before packaging them into .dex files. All we need to do is to implement Transform to traverse the .class files to get For all methods, replace the original file after the modification is completed.

In the background of the article, we mentioned that the purpose of developing a plug-in is to modify the method of a class in a third-party open source library. Specifically, we need to add a piece of our own code before this method to implement interception according to requirements. My approach is: in the project Write a static method, and then call our static method before this method, so that the interception function can be realized. The specific code is as follows:

class GhTransform extends Transform {
    
    

    private ClassPool classPool = ClassPool.getDefault()
    Project project

    GhTransform(Project project) {
    
    
        this.project = project
    }

    @Override
    String getName() {
    
    
        return "GhTransform"
    }
    /**
      * 需要处理的数据类型,目前 ContentType有六种枚举类型,通常我们使用比较频繁的有前两种:
      * 1、CONTENT_CLASS:表示需要处理 java 的 class 文件。
      * 2、CONTENT_JARS:表示需要处理 java 的 class 与 资源文件。
      * 3、CONTENT_RESOURCES:表示需要处理 java 的资源文件。
      * 4、CONTENT_NATIVE_LIBS:表示需要处理 native 库的代码。
      * 5、CONTENT_DEX:表示需要处理 DEX 文件。
      * 6、CONTENT_DEX_WITH_RESOURCES:表示需要处理 DEX 与 java 的资源文件。 
     */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
    
    
        return TransformManager.CONTENT_CLASS
    }

    /**
      * Transform 要操作的内容范围,目前 Scope 有五种基本类型:
      * 1、PROJECT:只有项目内容
      * 2、SUB_PROJECTS:只有子项目
      * 3、EXTERNAL_LIBRARIES:只有外部库
      * 4、TESTED_CODE:由当前变体(包括依赖项)所测试的代码
      * 5、PROVIDED_ONLY:只提供本地或远程依赖项
      * SCOPE_FULL_PROJECT 是一个Scope集合,包含Scope.PROJECT,Scope.SUB_PROJECTS,Scope.EXTERNAL_LIBRARIES 这三项,即当前Transform的作用域包括当前项目、子项目以及外部的依赖库
     */
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
    
    
        //通常我们使用 SCOPE_FULL_PROJECT
        return TransformManager.SCOPE_FULL_PROJECT
    }

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

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
    
    
        super.transform(transformInvocation)
        //添加加android.jar目录
        classPool.appendClassPath(project.android.bootClasspath[0].toString())
        def outputProvider = transformInvocation.outputProvider
        // 删除之前的输出
        if (outputProvider != null) {
    
    
            outputProvider.deleteAll()
        }
        transformInvocation.inputs.each {
    
     input ->
            input.directoryInputs.each {
    
     dirInput ->
                handleDirectory(dirInput.file)

                def dest = outputProvider.getContentLocation(dirInput.name, dirInput.contentTypes, dirInput.scopes, Format.DIRECTORY)
                // 将input的目录复制到output指定目录
                FileUtils.copyDirectory(dirInput.file, dest)
            }

        }
        transformInvocation.inputs.each {
    
     input ->
            input.jarInputs.each {
    
     jarInput ->
                if (jarInput.file.exists()) {
    
    
                    def srcFile = handleJar(jarInput.file)

                    //必须给jar重新命名,否则会冲突
                    def jarName = jarInput.name
                    def md5 = DigestUtils.md5Hex(jarInput.file.absolutePath)
                    if (jarName.endsWith(".jar")) {
    
    
                        jarName = jarName.substring(0, jarName.length() - 4)
                    }
                    def dest = outputProvider.getContentLocation(md5 + jarName, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                    FileUtils.copyFile(srcFile, dest)
                }
            }
        }

    }

    void handleDirectory(File dir) {
    
    
        //将类路径添加到classPool中
        classPool.insertClassPath(dir.absolutePath)
        if (dir.isDirectory()) {
    
    
            dir.eachFileRecurse {
    
     file ->
                def filePath = file.absolutePath
                classPool.insertClassPath(filePath)
                if (shouldModify(filePath)) {
    
    
                    def inputStream = new FileInputStream(file)
                    CtClass ctClass = modifyClass(inputStream)
                    ctClass.writeFile()
                    //调用detach方法释放内存
                    ctClass.detach()
                }
            }
        }
    }

    /**
     * 主要步骤:
     * 1.遍历所有jar文件
     * 2.解压jar然后遍历所有的class
     * 3.读取class的输入流并使用javassit修改,然后保存到新的jar文件中
     */
    File handleJar(File jarFile) {
    
    
        classPool.appendClassPath(jarFile.absolutePath)
        def inputJarFile = new JarFile(jarFile)
        def entries = inputJarFile.entries()
        //创建一个新的文件
        def outputJarFile = new File(jarFile.parentFile, "temp_" + jarFile.name)
        if (outputJarFile.exists()) outputJarFile.delete()
        def jarOutputStream = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(outputJarFile)))
        while (entries.hasMoreElements()) {
    
    
            def jarInputEntry = entries.nextElement()
            def jarInputEntryName = jarInputEntry.name

            def outputJarEntry = new JarEntry(jarInputEntryName)
            jarOutputStream.putNextEntry(outputJarEntry)

            def inputStream = inputJarFile.getInputStream(jarInputEntry)
            if (!shouldModify(jarInputEntryName)) {
    
    
                jarOutputStream.write(IOUtils.toByteArray(inputStream))
                inputStream.close()
                continue
            }

            def ctClass = modifyClass(inputStream)
            def byteCode = ctClass.toBytecode()
            ctClass.detach()
            inputStream.close()
            jarOutputStream.write(byteCode)
            jarOutputStream.flush()
        }
        inputJarFile.close()
        jarOutputStream.closeEntry()
        jarOutputStream.flush()
        jarOutputStream.close()
        return outputJarFile
    }

    static boolean shouldModify(String filePath) {
    
    
        return filePath.endsWith(".class") &&
                !filePath.contains("R.class") &&
                !filePath.contains('$') &&
                !filePath.contains('R$') &&
                !filePath.contains("BuildConfig.class") &&
                filePath.contains("ExoSourceManager")
    }

    CtClass modifyClass(InputStream is) {
    
    
        def classFile = new ClassFile(new DataInputStream(new BufferedInputStream(is)))
        def ctClass = classPool.get(classFile.name)
        //判断是否需要解冻
        if (ctClass.isFrozen()) {
    
    
            ctClass.defrost()
        }

        def method = ctClass.getDeclaredMethod("release")
        //必须使用全类名,否则编译会找不到类
        def body = '''
            int size = com.gh.gamecenter.video.detail.CustomManager.getVideoManagerSize();
            if (size > 1) {
                android.util.Log.e(\"gh_tag\",\"拦截成功\");
                return;
            }
        '''
        method.insertBefore(body)
        return ctClass

    }
}

So far we have completed the development of plug-in, then we must verify that the code was inserted successfully, we can use the built-in Android Studio Build / Analyze APK tool, choose to compile the apk file
Insert picture description here
to find the modified class, right click Show Bytecodeto view bytecode, Then search for our modified method:
Insert picture description here
the red box above is the added code, I inserted a line of log in the code, and run the code to check logcat printing:
Insert picture description here
Finally, we completed the development of the plug-in and verified its effectiveness, although it seems a little bit Trouble, but if we encounter similar problems in the future, we can also solve the problem based on the above code.

Some issues that need attention

  • Be careful not to confuse the open source library and the insert code written by yourself
  • The AGP version of build.gradle in buildSrc must be the same as in the app module
  • Use the full path for the class referenced by the inserted code
  • Insert the class used in the code need to add the classpath to the classPool, otherwise it will not compile
  • Do not configure buildSrc in settings.gradle
  • Regardless of whether we modify the jar, we must copy it to the target path

Guess you like

Origin blog.csdn.net/ZYJWR/article/details/113091371