若是你对本文感兴趣,也许你对个人公众号也会有兴趣,可扫下方二维码或搜索公众微信号:mxszgg html
![]()
若是读者对模块化开发的服务调用具备必定的认识能够跳过下面一小节。java
模块化开发如今对于 Android 开发者来讲应该是一个耳熟能详的名词了,如今应该有许多应用的开发迭代都使用了模块化开发,模块化开发的意义是在于将 App 的业务细分红 N 个模块,利于开发人员的协做开发。模块化开发当中有一个须要解决的问题就是模块之间的服务调用——由于各个模块是以 library 形式存在,彼此之间不相互依赖,故使彼此之间实际上并不知道对方的存在,那么当 A 模块想要知道 B 模块中的某个信息,须要调用 B 中的某个方法时该怎么办呢?例如开发人员当前正在 main 模块开发,当前的一个 TextView 须要展现电影信息,可是很明显电影信息这块属于 movie 模块而并非 main 模块,那么此时该如何解决呢?机智的 Android 开发人员建立了基础模块 service 并让全部的业务模块依赖 service 模块,service 模块的职责也很简单,只须要提供接口声明,具体的实现就交给具体的业务模块本身去实现了。例如 service 模块中提供一个 MovieService 类:android
public interface MovieService {
String movieName();
}
复制代码
那么在 movie 模块中就能够建立一个 MovieServiceImpl 类去实现 MovieService 接口了——git
public class MovieServiceImpl implements MovieService {
@Override public String movieName() {
return "一出好戏";
}
}
复制代码
而对于 main 模块来讲,它应该调用 MovieService 实现类的 movieName()
方法就行了,可是事实上 main 模块又不可能知道 MovieService 的具体实现类是什么,因此看起来彷佛问题又卡住了...github
实际上问题在于如何获取到接口实现类的路径,例如 renxuelong/ComponentDemo 中所提到的,反射调用全部模块的 application 的某个方法,在该方法中将接口与实现类映射起来,该方法的弊端很明显,开发者须要显示填写全部模块 application 的彻底限定名,这在开发中应当是尽可能避免的。api
流行的解决方案就是 ARouter 的实现方式了——使用 APT—— build 时扫描全部的被 @Route
注解所修饰的类,判断该类是否实现了某个接口,若是是的话则建立相应的 xxx$$app
类,读者能够下载 ARouter 的 demo 在 build 以后找到 ARouter$$Providers$$app
类 ——bash
如上图所示,左侧是接口的彻底限定名,右侧是具体的实现类,这样就将接口与实现类一一映射起来了,相比于上面所提到的方法,开发者并不须要手动地去填写类的彻底限定名,由于在实际开发中类的路径是极可能被改变的,这种撰写类的彻底限定名的操做应该避免由开发者去作,而应该去交给构建工具去完成。微信
实际上笔者本文所想要阐述的方案与 APT 的原理是同样的,经过扫描指定注解所修饰的类获取到全部的 service 接口的实现类,并用 Map 将其维护起来。app
结合官方文档文档上来讲,Transform 是一个类,构建工具中自带诸如 ProGuardTransform
、DexTransform
等 Transform,一系列的 Transform 类将全部的 .class 文件转换为 .dex 文件,而官方容许开发者建立自定义的 Transform 来操做转换成 .dex 文件以前的全部 .class 文件,这意味着开发者能够对app 中全部的 .class 文件进行操做。开发者能够在插件中经过 android.registerTransform(theTransform)
或者 android.registerTransform(theTransform, dependencies)
来注册一个 Transform。maven
前面提到,Transform 其实是一系列的操做,因此开发者应该很容易理解,前一个 Transform 的输出理应会是下一个 Transform 的输入——
关于理解本文所须要的 Transform 知识先说到这,其余涉及的知识点会在后文的实操中提到。若是各位读者对 Transform 想要深一步了解,更多 Transform 使用姿式可参考官方文档。
javassist 是一个字节码工具,简单来讲能够利用它来增删改 .class 文件的代码,毕竟在构建时期的 .java 文件都编译成了 .class 文件了。
在动手写代码前应该思考一下须要建立几个 lib 工程,对于模块化开发中的各个 module 来讲,它们总共须要两个类,一个是注解,若是当前 module 有接口服务须要实现,那么得用这个注解来标记实现类;另外一个就是 Map,须要经过它来获取其余 module 的实现类。固然,除了建立前面所提到的这个 lib 工程之外,还须要建立一个 plugin 供 app 模块使用。
新建一个 java 模块取名为 hunter,并建立 HunterRegistry 类和 Impl 注解以下:
public final class HunterRegistry {
private static Map<Class<?>, Object> services;
private HunterRegistry() {
}
@SuppressWarnings("unchecked") public static <T> T get(Class<T> key) {
return (T) services.get(key);
}
}
复制代码
public @interface Impl {
Class<?> service();
}
复制代码
对于 main 模块来讲,若是它想要获取 movie 模块的电影信息,它仅需调用 HunterRegistry.get(MovieService.class).movieName()
便可得到 MovieService 实现类的具体方法实现,HunterRegistry 类看起来有些匪夷所思,services
对象甚至都没有初始化,因此调用 get()
方法必定会报错,从现有代码看起来确实是这样可是实际上在 Transform 中获取到全部的接口-实现类的映射关系以后将会经过 javassist 插入静态代码初始化 services
对象并向 services
对象中 put 键值对,最终生成 .class 文件相似以下:
public final class HunterRegistry {
private static Map<Class<?>, Object> services = new HashMap();
static {
services.put(MovieService.class, new MovieServiceImpl());
}
private HunterRegistry() {
}
@SuppressWarnings("unchecked") public static <T> T get(Class<T> key) {
return (T) services.get(key);
}
}
复制代码
而对于 movie 模块来讲,它须要建立 MovieService 的具体实现类,并用 @Impl
注解标记以便 Transform 能够找到它与接口的映射关系,例如:
@Impl(service = MovieService.class)
public class MovieServiceImpl implements MovieService {
@Override public String movieName() {
return "一出好戏";
}
}
复制代码
接下来就是建立 gradle plugin 了:
建立 plugin 的基本过程本文就不说起了,若是读者不太清楚的话,能够参考笔者以前写的写给 Android 开发者的 Gradle 系列(三)撰写 plugin。
建立一个 plugin 类,plugin 的内容很简单:
class HunterPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.plugins.withId('com.android.application') {
project.android.registerTransform(new HunterTransform())
}
}
}
复制代码
因此能够看得出来全部的重点就是在这个 HunterTransform 身上了——
class HunterTransform extends Transform {
private static final String CLASS_REGISTRY = 'com.joker.hunter.HunterRegistry'
private static final String CLASS_REGISTRY_PATH = 'com/joker/hunter/HunterRegistry.class'
private static final String ANNOTATION_IMPL = 'com.joker.hunter.Impl'
private static final Logger LOG = Logging.getLogger(HunterTransform.class)
@Override
String getName() {
return "hunterService"
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return Collections.singleton(QualifiedContent.Scope.SUB_PROJECTS)
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(TransformInvocation transformInvocation)
throws TransformException, InterruptedException, IOException {
// 1
transformInvocation.outputProvider.deleteAll()
def pool = ClassPool.getDefault()
JarInput registryJarInput
def impls = []
// 2
transformInvocation.inputs.each { input ->
input.jarInputs.each { JarInput jarInput ->
pool.appendClassPath(jarInput.file.absolutePath)
if (new JarFile(jarInput.file).getEntry(CLASS_REGISTRY_PATH) != null) {
registryJarInput = jarInput
LOG.info("registryJarInput.file.path is ${registryJarInput.file.absolutePath}")
} else {
def jarFile = new JarFile(jarInput.file)
jarFile.entries().grep { entry -> entry.name.endsWith(".class") }.each { entry ->
InputStream stream = jarFile.getInputStream(entry)
if (stream != null) {
CtClass ctClass = pool.makeClass(stream)
if (ctClass.hasAnnotation(ANNOTATION_IMPL)) {
impls.add(ctClass)
}
ctClass.detach()
}
}
FileUtils.copyFile(jarInput.file,
transformInvocation.outputProvider.getContentLocation(jarInput.name,
jarInput.contentTypes, jarInput.scopes, Format.JAR))
LOG.info("jarInput.file.path is $jarInput.file.absolutePath")
}
}
}
if (registryJarInput == null) {
return
}
// 3
def stringBuilder = new StringBuilder()
stringBuilder.append('{\n')
stringBuilder.append('services = new java.util.HashMap();')
impls.each { CtClass ctClass ->
ClassFile classFile = ctClass.getClassFile()
AnnotationsAttribute attr = (AnnotationsAttribute) classFile.getAttribute(
AnnotationsAttribute.invisibleTag)
Annotation annotation = attr.getAnnotation(ANNOTATION_IMPL)
def value = annotation.getMemberValue('service')
stringBuilder.append('services.put(')
.append(value)
.append(', new ')
.append(ctClass.name)
.append('());\n')
}
stringBuilder.append('}\n')
LOG.info(stringBuilder.toString())
def registryClz = pool.get(CLASS_REGISTRY)
registryClz.makeClassInitializer().setBody(stringBuilder.toString())
// 4
def outDir = transformInvocation.outputProvider.getContentLocation(registryJarInput.name,
registryJarInput.contentTypes, registryJarInput.scopes, Format.JAR)
copyJar(registryJarInput.file, outDir, CLASS_REGISTRY_PATH, registryClz.toBytecode())
}
private void copyJar(File srcFile, File outDir, String fileName, byte[] bytes) {
outDir.getParentFile().mkdirs()
def jarOutputStream = new JarOutputStream(new FileOutputStream(outDir))
def buffer = new byte[1024]
int read = 0
def jarFile = new JarFile(srcFile)
jarFile.entries().each { JarEntry jarEntry ->
if (jarEntry.name == fileName) {
jarOutputStream.putNextEntry(new JarEntry(fileName))
jarOutputStream.write(bytes)
} else {
jarOutputStream.putNextEntry(jarEntry)
def inputStream = jarFile.getInputStream(jarEntry)
while ((read = inputStream.read(buffer)) != -1) {
jarOutputStream.write(buffer, 0, read)
}
}
}
jarOutputStream.close()
}
}
复制代码
这里简单提一下前三个方法,首先是 getInputTypes()
,它表示输入该 Transform 的文件类型是什么,从 QualifiedContent.ContentType 的实现类中能够看到仍是有不少种输入文件类型的,然并卵,前文提到,官方只容许开发者对 .class 文件操做,固然,这里咱们也只须要对 .class 文件操做就行了,因此这里得填 TransformManager.CONTENT_CLASS
;接着是 getScopes()
方法,它表示开发者须要从哪些地方获取这些输入文件,而 QualifiedContent.Scope.SUB_PROJECTS
就是表明各个 module,由于咱们也只须要获取各个 module 的 .class 文件就行了;最后是 isIncremental()
方法,它表明当前 Transform 是否支持增量编译,为了使得本文所谈到的内容更简单一些,笔者选择了 return false
表明当前 Transform 不支持增量编译,各位读者后期能够参考官方文档优化这个 Transform 使其支持增量编译。接下来就是核心的 transform()
方法了——为了方便解释代码,笔者将 transform()
方法分红了4个部分,首先第1部分为了不上一次构建对本次构建的影响,须要调用 transformInvocation.outputProvider.deleteAll()
删除上一次构建的产物,以及一些初始化的操做;第2部分就是对 Transform 输入产物的操做了,也就是全部的 .class 文件,input 除了 jarInputs 以外还有 dirInputs,可是对于输入范围为 QualifiedContent.Scope.SUB_PROJECTS
的 Transform 来讲输入类型只有 jarInputs,而这里的 jarInputs.file 其实是当前项目中全部 module:
在这一步中,咱们要区分出两类 jar,一类是包含 HunterRegistry.class 的 jar 包,经过 new JarFile(jarInput.file).getEntry(CLASS_REGISTRY_PATH) != null
便可判断当前 jar 包是否包含 HunterRegistry.class 也就是上面截图的 hunter.jar;而另外一类就是 module 的 jar 包,经过 groovy 的 api 筛选出 jar 包中全部的 .class 文件,再依靠 javassist 提供的 api 判断当前 .class 是不是被 @Impl
注解所修饰的,若是是的话就将它添加到 impls 里面,前文提到前一个 Transform 的输出会是下一个 Transform 的输入,因此须要经过 transformInvocation.outputProvider.getContentLocation(jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
获取该 jar 包应该移动到的路径下,由于它还要做为下一个 Transform 的输入;第3步就是利用 impls 获取具体实现类,利用 javassist api 获取 @Impl
注解中的 service 方法的返回值,也就是接口类,再将它们拼接成字符串,最终再经过 registryClz.makeClassInitializer().setBody(stringBuilder.toString())
便可将这段字符串注入到 HunterRegistry.class 文件中了;第4步就是将上一步获取到的新 HunterRegistry.class 文件的字节码替换掉原先的字节码并最后打入指定的路径下就行了。
经过 jadx 工具打开 debug.apk 再找到 HunterRegistry.class 文件,字节码以下:
能够看到 MovieService 和它的实现类 MovieServiceImpl 被 put 进了 services 当中。运行 debug.apk 跳转到 main 模块下 HomeActivity 就能够看到屏幕上的输出值了:
不管是 APT 方案仍是 Transform 方案,它们所解决模块化开发中的服务调用核心思想都是在于找到接口与实现类的映射关系,只要解决了映射关系,问题也就迎刃而解了。若是是暂不了解 Transform 的读者,笔者认为在了解完本文的知识后,能够更深一步的去了解 Transform,例如优化 HunterTransform
,使其支持增量编译;例如尝试改变输入范围后,输入的文件会有什么不同?
当输入范围为
QualifiedContent.Scope.PROJECT
时输入的文件中将会有 directoryInput 类型,其文件夹路径实际上就是../app/build/intermediates/classes/debug
,实际上里面就是 app 模块的全部 .class 文件:而当输入范围为 ![]()
QualifiedContent.Scope.EXTERNAL_LIBRARIES
时输入的 jar 包所有都是第三方库:![]()
因此若是将插件传到 maven,以第三方形式以来进工程的话,那么输入范围就不能仅仅是上文提到的
QualifiedContent.Scope.SUB_PROJECTS
了,由于插件的 jar 包将会找不到。
最后是项目地址:jokermonn/transformSample