Android 热修复使用Gradle Plugin1.5改造Nuwa插件
随着谷歌的Gradle插件版本的不断升级,Gradle插件现在最新的已经到了2.1.0-beta1,对应的依赖为com.android.tools.build:gradle:2.0.0-beta6,而Nuwa当时出来的时候,Gradle插件还只是1.2.3版本,对应的依赖为com.android.tools.build:gradle:1.2.3,当时的Nuwa是根据有无preDex这个Task进行hook做不同的逻辑处理,而随着Gradle插件版本的不断增加,谷歌增加了一个新的接口可以用于处理我们的字节码注入的需求。这个接口最早出现在1.5.0-beta1中,官方的描述如下,不想看英文的直接略过看翻译。
Starting with 1.5.0-beta1, the Gradle plugin includes a Transform API allowing 3rd party plugins to manipulate compiled class files before they are converted to dex files.
(The API existed in 1.4.0-beta2 but it's been completely revamped in 1.5.0-beta1)
The goal of this API is to simplify injecting custom class manipulations without having to deal with tasks, and to offer more flexibility on what is manipulated. The internal code processing (jacoco, progard, multi-dex) have all moved to this new mechanism already in 1.5.0-beta1.
Note: this applies only to the javac/dx code path. Jack does not use this API at the moment.
The API doc is here.
To insert a transform into a build, you simply create a new class implementing one of the Transform interfaces, and register it with android.registerTransform(theTransform) or android.registerTransform(theTransform, dependencies).
Important notes:
The Dex class is gone. You cannot access it anymore through the variant API (the getter is still there for now but will throw an exception)
Transform can only be registered globally which applies them to all the variants. We'll improve this shortly.
There's no way to control ordering of the transforms.
We're looking for feedback on the API. Please file bugs or email us on our adt-dev mailing list.
从1.5开始,gradle插件包含了一个叫Transform的API,这个API允许第三方插件在class文件转为为dex文件前操作编译好的class文件,这个API的目标就是简化class文件的自定义的操作而不用对Task进行处理,并且可以更加灵活地进行操作。我们如何注入一个Transform呢,很简单,实现Transform抽象类中的方法,使用下面的两个方法之一进行注入即可。
android.registerTransform(theTransform)
android.registerTransform(theTransform, dependencies)
那么我们就可以在这个函数中操作之前1.2.3版本中的Nuwa Gradle做的一切事情。在这之前,你最好通读下面三篇文章
如何使用Android Studio开发Gradle插件 Android 热修复Nuwa的原理及Gradle插件源码解析 深入理解Android之Gradle现在,新建一个gradle插件项目,如何新建请阅读上面的第一篇文章,这个插件项目中有两个module,一个为app,用于测试插件,一个是插件module,姑且叫hotpatch,用于编写插件。
将你的gradle plugin版本切到1.5
classpath 'com.android.tools.build:gradle:1.5.0'
然后将gralde wrapper版本改为2.10
distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
现在编译运行一下项目下的app module,看下gradle控制台输出的是什么。
可以看到,的确没有preDex这个Task,反倒是多了很多transform开头的Task,那么这些Task是怎么来的呢。在gradle plugin的源码中有一个叫TransformManager的类,这个类管理着所有的Transform的子类,里面有一个方法叫getTaskNamePrefix,在这个方法中就是获得Task的前缀,以transform开头,之后拼接ContentType,这个ContentType代表着这个Transform的输入文件的类型,类型主要有两种,一种是Classes,另一种是Resources,ContentType之间使用And连接,拼接完成后加上With,之后紧跟的就是这个Transform的Name,name在getName()方法中重写返回即可。代码如下:
@NonNull
private static String getTaskNamePrefix(@NonNull Transform transform) {
StringBuilder sb = new StringBuilder(100);
sb.append("transform");
Iterator iterator = transform.getInputTypes().iterator();
// there's always at least one
sb.append(capitalize(iterator.next().name().toLowerCase(Locale.getDefault())));
while (iterator.hasNext()) {
sb.append("And").append(capitalize(
iterator.next().name().toLowerCase(Locale.getDefault())));
}
sb.append("With").append(capitalize(transform.getName())).append("For");
return sb.toString();
}
ContentType是一个接口,有一个默认的枚举类的实现类,里面定义了两种文件,一种是class文件,另一种就是资源文件。
interface ContentType {
/**
* Content type name, readable by humans.
* @return the string content type name
*/
String name();
/**
* A unique value for a content type.
*/
int getValue();
}
/**
* The type of of the content.
*/
enum DefaultContentType implements ContentType {
/**
* The content is compiled Java code. This can be in a Jar file or in a folder. If
* in a folder, it is expected to in sub-folders matching package names.
*/
CLASSES(0x01),
/**
* The content is standard Java resources.
*/
RESOURCES(0x02);
private final int value;
DefaultContentType(int value) {
this.value = value;
}
@Override
public int getValue() {
return value;
}
}
说到ContentType,顺便把另一个枚举类带掉,叫Scope,翻译过来就是作用域,关于详细的内容,请看下面的注释。
enum Scope {
/** Only the project content */
PROJECT(0x01),
/** Only the project's local dependencies (local jars) */
PROJECT_LOCAL_DEPS(0x02),
/** Only the sub-projects. */
SUB_PROJECTS(0x04),
/** Only the sub-projects's local dependencies (local jars). */
SUB_PROJECTS_LOCAL_DEPS(0x08),
/** Only the external libraries */
EXTERNAL_LIBRARIES(0x10),
/** Code that is being tested by the current variant, including dependencies */
TESTED_CODE(0x20),
/** Local or remote dependencies that are provided-only */
PROVIDED_ONLY(0x40);
private final int value;
Scope(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
ContentType和Scope,一起组成输出产物的目录结构。可以看到下图transforms下有很多莫名其妙的目录,比如1000,1f,main,3,等等,这些目录可不是随机产生的,而是根据上面的两个值产生的。
举个例子,上面的文件夹中有个proguard的目录,这个目录是ProGuardTransform产生的,在源码中可以找到其实现了getName方法,返回了proguard。这个getName()方法返回的值就创建了proguard这个目录。
public String getName() {
return "proguard";
}
然后再看这个Transform的输入文件类型
public Set getInputTypes() {
return TransformManager.CONTENT_JARS;
}
TransformManager.CONTENT_JARS是什么鬼呢,跟进去一目了然
public static final Set CONTENT_JARS = ImmutableSet.of(CLASSES, RESOURCES);
因此Proguard这个Transform有两种输入文件,一种是class文件(含jar),另一种是资源文件,这个Task是做混淆用的,class文件就是ProGuardTransform依赖的上一个Transform的输出产物,而资源文件可以是混淆时使用的配置文件。
因此根据上