1. 前言
动态编译技术在开源框架中的应用非常的广泛,现在市面上的插件化框架,热修复框架几乎都使用了动态编译技术,原理几乎都是在编译期间动态的在class文件中注入代码或者或修改。那就让我们来了解一下这高大上的技术吧。
2. 揭开动态编译的神秘面纱
揭开动态编译的神秘面纱只需三步:
- 自定义Gradle插件
- 介绍Transform API 及 实现步骤
- 实现在编译的过程中操作.class文件,对原有代码或者说逻辑进 行一些处理,修改原class代码和动态生成java类
3. 案例demo
插件目录结构:
3.1 自定义Gradle插件
在Gradle中创建自定义插件,Gradle提供了三种方式:
- 在build.gradle脚本中直接使用
- 在buildSrc中使用
- 在独立Module中使用
具体实现请移步:实现Gradle自定义插件
3.2 利用Google提供的Transform API 在编译的过程中操作.class文件
gradle从1.5开始,gradle插件包含了一个叫Transform的API,这个API允许第三方插件在class文件转为为dex文件前操作编译好的class文件,这个API的目标是简化自定义类操作,而不必处理Task,并且在操作上提供更大的灵活性。并且可以更加灵活地进行操作。
官方文档:http://google.github.io/android-gradle-dsl/javadoc/
官方文档:http://google.github.io/android-gradle-dsl/javadoc/
1) 在我们自定义的gradle插件的build.gradle中引入transform的包,下面会进行代码注入,就一起引入的其他包
apply plugin: 'groovy'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
//build tools
implementation 'com.android.tools.build:gradle:3.1.3'
//gradle sdk
implementation gradleApi()
//groovy sdk
implementation localGroovy()
//transform API 已经移到 gradle-api里面
//implementation 'com.android.tools.build:transform-api:2.0.0-deprecated-use-gradle-api'
//javassist
implementation 'org.javassist:javassist:3.23.1-GA'
//commons-io
implementation 'commons-io:commons-io:2.6'
}
repositories {
google()
jcenter()
mavenCentral()
}
2) 创建一个类继承Transform 并实现其方法:
package com.aoaoyi.plugin.gradle;
import com.android.build.api.transform.*;
import com.android.build.gradle.internal.pipeline.TransformManager;
import org.gradle.api.Project;
import java.io.IOException;
import java.util.Set;
/**
* Created by yuzhenbei on 2018/7/15 08:54
* <p>
* Email aoaoyi.com@gmail.com
*/
public class MyTransform extends Transform {
private Project mProject;
public MyTransform(Project p) {
this.mProject = p;
}
/**
* transform的名称
* transformClassesWithMyClassTransformForDebug 运行时的名字
* transformClassesWith + getName() + For + Debug或Release
*
* @return String
*/
@Override
public String getName() {
return "MyTransform";
}
/**
* 需要处理的数据类型,有两种枚举类型
* CLASSES和RESOURCES,CLASSES代表处理的java的class文件,RESOURCES代表要处理java的资源
*
* @return
*/
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
/**
* 指Transform要操作内容的范围,官方文档Scope有7种类型:
* EXTERNAL_LIBRARIES 只有外部库
* PROJECT 只有项目内容
* PROJECT_LOCAL_DEPS 只有项目的本地依赖(本地jar)
* PROVIDED_ONLY 只提供本地或远程依赖项
* SUB_PROJECTS 只有子项目。
* SUB_PROJECTS_LOCAL_DEPS 只有子项目的本地依赖项(本地jar)。
* TESTED_CODE 由当前变量(包括依赖项)测试的代码
*
* Returns the scope(s) of the Transform. This indicates which scopes the transform consumes.
*/
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
/**
* 指明当前Transform是否支持增量编译
* If it does, then the TransformInput may contain a list of changed/removed/added files, unless
* something else triggers a non incremental run.
*/
@Override
public boolean isIncremental() {
return false;
}
/**
* Transform中的核心方法
* transformInvocation.getInputs() 中是传过来的输入流,其中有两种格式,一种是jar包格式一种是目录格式。
* transformInvocation.getOutputProvider() 获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做不然编译会报错
*
* @param transformInvocation
* @throws TransformException
* @throws InterruptedException
* @throws IOException
*/
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation);
}
}
3) 在我们自定义的gradle插件的apply方法中注册自定义的Transform
package com.aoaoyi.plugin.gradle
import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.gradle.AppExtension
import com.android.build.gradle.AppPlugin
class MyPlugin implements Plugin<Project> {
void apply(Project project) {
project.extensions.create('pluginSrc', MyExtension)
project.task('testPlugin').doLast {
println project.pluginSrc.message
}
//AppExtension就是build.gradle中android{...}这一块
def android = project.extensions.getByType(AppExtension)
//注册一个Transform
//def transform = new MyTransform(project)
android.registerTransform(new MyTransform(project))
}
}
我们知道studio会给我们创建一个BuildConfig的类,但是是否知道这个类是怎么生成的呢?下面我们来模拟一下 在app下的build.gradle下可以创建参数列表,然后将参数生成一个java类,在代码中就可以使用了
/* 在app.gradle里和android{……},dependencies{……}同一级*/
testCreateJavaConfig{
str = "动态生成java类的字符串"
}
然后回到我们的自定义的Plugin中实现代码:
package com.aoaoyi.plugin.gradle
import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.gradle.AppExtension
import com.android.build.gradle.AppPlugin
class MyPlugin implements Plugin {
void apply(Project project) {
project.extensions.create('pluginSrc', MyExtension)
project.task('testPlugin').doLast {
println project.pluginSrc.message
}
//AppExtension就是build.gradle中android{...}这一块
def android = project.extensions.getByType(AppExtension)
//注册一个Transform
//def transform = new MyTransform(project)
android.registerTransform(new MyTransform(project))
/**
* 创建一个Extension,名字叫做testCreateJavaConfig
*
* MyTestClassExtension.groovy
*
* class MyTestClassExtension {
* def str = "默认值"
* }
*/
project.extensions.create("testCreateJavaConfig", MyTestClassExtension)
//生成一个类
if (project.plugins.hasPlugin(AppPlugin)) {
//获取到Extension,Extension就是 build.gradle中的{}闭包
android.applicationVariants.all { variant ->
//获取到scope,作用域
def variantData = variant.variantData
def scope = variantData.scope
//拿到build.gradle中创建的Extension的值
def config = project.extensions.getByName("testCreateJavaConfig")
//创建一个task
def createTaskName = scope.getTaskName("testTask", "myTestPlugin")
def createTask = project.task(createTaskName)
//设置task要执行的任务
createTask.doLast {
//生成java类
createJavaTest(variant, config)
}
//设置task依赖于生成BuildConfig的task,然后在生成BuildConfig后生成我们的类
String generateBuildConfigTaskName = variant.getVariantData().getScope().getGenerateBuildConfigTask().name
def generateBuildConfigTask = project.tasks.getByName(generateBuildConfigTaskName)
if (generateBuildConfigTask) {
createTask.dependsOn generateBuildConfigTask
generateBuildConfigTask.finalizedBy createTask
}
}
}
}
static void createJavaTest(variant, config) {
//要生成的内容
def content = """package com.aoaoyi.hotfix.plugin;
/**
* Created by yuzhenbei on 2018/7/15 22:55
* Email aoaoyi.com@gmail.com
*/
public class MyPluginTestClass {
public static final String str = "${config.str}";
}
"""
//获取到BuildConfig类的路径
File outputDir = variant.getVariantData().getScope().getBuildConfigSourceOutputDir()
def javaFile = new File(outputDir, "MyPluginTestClass.java")
javaFile.write(content, 'UTF-8')
}
}
执行Build APK(s)或者执行run,看一下结果
生成MyPluginTestClass 代码内容如下:
1)下面是MainActivity的代码
package com.aoaoyi.hotfix.ui
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import com.aoaoyi.hotfix.R
import com.aoaoyi.hotfix.jni.Jni
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Example of a call to a native method
sample_text.text = Jni().stringFromJNI()
txtHotFixed.text = "Hi fixed!"
}
}
2)利用Transform插入代码:
/**
* Transform中的核心方法
* transformInvocation.getInputs() 中是传过来的输入流,其中有两种格式,一种是jar包格式一种是目录格式。
* transformInvocation.getOutputProvider() 获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做不然编译会报错
*
* @param transformInvocation
* @throws TransformException
* @throws InterruptedException
* @throws IOException
*/
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
Context context = transformInvocation.getContext()
Collection inputs = transformInvocation.getInputs()
Collection referencedInputs = transformInvocation.getReferencedInputs()
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider()
boolean isIncremental = transformInvocation.isIncremental()
outputProvider.deleteAll()
inputs.each {
TransformInput input ->
input.directoryInputs.each {
DirectoryInput directoryInput ->
//注入代码
MyInjects.inject(directoryInput.file.absolutePath, mProject)
// 获取output目录
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
println("" + directoryInput.file + " transform" + dest);
// 将input的目录复制到output指定目录
FileUtils.copyDirectory(directoryInput.file, dest)
}
input.jarInputs.each {
JarInput jarInput ->
//jar文件一般是第三方依赖库jar文件
// 重命名输出文件(同目录copyFile会冲突)
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)
println("jar " + jarInput.file + " transform " + dest)
//将输入内容复制到输出
FileUtils.copyFile(jarInput.file, dest)
}
}
}
3)利用javassist实现代码的注入,创建MyInjects.groovy类,在这个类中我们传入了两个参数,一个是当前变量的文件夹,一个是当前的工程对象,来看一下代码:
package com.aoaoyi.plugin.gradle
import javassist.ClassPool
import javassist.CtClass
import javassist.CtMethod
import org.gradle.api.Project
class MyInjects {
//初始化类池
private final static ClassPool pool = ClassPool.getDefault()
static void inject(String path,Project project) {
//将当前路径加入类池,不然找不到这个类
pool.appendClassPath(path)
//project.android.bootClasspath 加入android.jar,不然找不到android相关的所有类
pool.appendClassPath(project.android.bootClasspath[0].toString())
//引入android.os.Bundle包,因为onCreate方法参数有Bundle
pool.importPackage("android.os.Bundle")
File dir = new File(path)
if (dir.isDirectory()) {
//遍历文件夹
dir.eachFileRecurse { File file ->
String filePath = file.absolutePath
println("filePath = " + filePath)
if ("MainActivity.class".equals(file.getName())) {
//获取MainActivity.class
CtClass ctClass = pool.getCtClass("com.aoaoyi.hotfix.ui.MainActivity")
println("ctClass = " + ctClass)
//解冻
if (ctClass.isFrozen())
ctClass.defrost()
//获取到OnCreate方法
CtMethod ctMethod = ctClass.getDeclaredMethod("onCreate")
println("方法名 = " + ctMethod)
String insetBeforeStr = """ android.widget.Toast.makeText(this,"我是被插入的Toast代码~!!",android.widget.Toast.LENGTH_SHORT).show();
"""
//在方法开头插入代码
ctMethod.insertBefore(insetBeforeStr)
ctClass.writeFile(path)
ctClass.detach()//释放
}
}
}
}
}
4)运行项目就可以出现Toast,这里的MainActivity是使用Kotlin编写的,我暂时没有找到编译后的class文件,不过这里我反编译生成的APK,使用java2samli插件查看MainActivity,找到onCreate方法可以看到动态插入的Toast代码:
可以看出来,代码已经成功注入了。
4. 总结
本篇是让我们了解plugin、transform和javassist结合使用入门,很多插件化等技术都会用到javassist,需要我们更多的深入了解和探索,无论是自定义gradle还是注入代码这些技术都是通往大牛之路的必备技能,有描述错误的地方欢迎童鞋们指出。
相关:
2. Transform: http://google.github.io/android-gradle-dsl/javadoc/
3. Javassist : http://www.javassist.org/
4. apk反编译及重新打包