前面兩篇文章介紹了 Gradle自定義插件以及擴展配置的用法。 今天我們來看一下一個具體的應用場景,動態編譯。我們將嘗試在編譯期間修改class文件。
初識Transform
Android Gradle 工具在 1.5.0 版本後提供了 Transfrom API, 允許第三方 Plugin 在打包 dex 文件之前的編譯過程中 操作 .class 文件。目前 jarMerge、proguard、multi-dex、Instant-Run 都已經換成 Transform 實現。
如下圖:
具體怎麼操作呢?我們在自定義的Gradle插件中先創建一個自己的Transform。 重寫的transform方法處就是處理class文件的時機。
public class MyTransform extends Transform { Project project MyTransform(Project project) { this.project = project } @Override void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException { //todo } @Override String getName() { return "MyTransform" } @Override Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS } @Override Set<QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT } @Override boolean isIncremental() { return false } }
當然我們希望做點什麼,例如修改我們類中的某個方法,或加入一行log。(尤其當你需要修改一些你沒有源碼修改權限的第三方Jar包時) 我們需要藉助另一個工具,javassist。
javassist
先配置一下插件的gradle
dependencies { compile gradleApi() compile localGroovy() compile 'com.android.tools.build:gradle:3.2.1' compile 'com.android.tools.build:transform-api:1.5.0' compile 'javassist:javassist:3.12.1.GA' compile 'commons-io:commons-io:2.5' }
然後創建一個插入的代碼類MyInjects,該類的作用就是往MainActivity中插入一句log-->"Hello world!"
public class MyInjects { private static ClassPool pool = ClassPool.getDefault() private static String injectStr = "System.out.println(\"Hello world!\" ); " public static void injectDir(String path) { pool.appendClassPath(path) File dir = new File(path) if (dir.isDirectory()) { dir.eachFileRecurse { File file -> String filePath = file.absolutePath if (filePath.endsWith("MainActivity.class")) { String className = "com.example.huhu.kotlindemo.ac.MainActivity" CtClass c = pool.getCtClass(className) if (c.isFrozen()) { c.defrost() } CtConstructor[] cts = c.getDeclaredConstructors() if (cts == null || cts.length == 0) { CtConstructor constructor = new CtConstructor(new CtClass[0], c) constructor.insertBeforeBody(injectStr) c.addConstructor(constructor) } else { cts[0].insertBeforeBody(injectStr) } c.writeFile(path) c.detach() } } } } }
接着在Transform中調用,transform的inputs包含了兩個部分 jar 包和目錄:
@Override void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException { inputs.each { TransformInput input -> input.directoryInputs.each { DirectoryInput directoryInput -> MyInjects.injectDir(directoryInput.file.absolutePath) def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY) FileUtils.copyDirectory(directoryInput.file, dest) } input.jarInputs.each { JarInput jarInput -> def jarName = jarInput.name def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath()) if (jarName.endsWith(".jar")) { jarName = jarName.substring(0, jarName.length() - 4) } def dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR) FileUtils.copyFile(jarInput.file, dest) } } }
最後,我們看一下,在Gradle Plugin中如何註冊Transform:
class DemoPlugin implements Plugin<Project> { @Override void apply(Project project) { def android = project.extensions.getByType(AppExtension) android.registerTransform(new MyTransform(project)) } }
然後,我們運行App以後,就可以發現:
I/System.out: Hello world!
這只是一種簡單的用法,大家可以查閱javassist更多的用法去做更多事情。