我是小偷
Demo项目还是之前的Github地址,贴在项目的最前面,有兴趣的大佬求求你点个star吧。
ByteX是一个基于gradle transform api和ASM的字节码插件平台(或许,你可以把它当成一个有无限个插头的插座?)。
目前集成了若干个字节码插件,每个插件完全独立,既可以脱离ByteX这个宿主而独立存在,又可以自动集成到宿主和其它插件一起整合为一个单独的Transform。插件和插件之间,宿主和插件之间的代码是完全解耦的(有点像组件化),这使得ByteX在代码上拥有很好的可拓展性,新插件的开发将会变得更加简单高效。
bytex说白了就是一个插座,我去年就非常想自己也能有一个这玩意(偷鸡)。我会盘算一下如何用最简单的方式将多个插件优雅的组合到一起,形成自己的逮虾户x。但是我不是特别喜欢byteX,主要是因为我对他的api不熟悉啊,我还是自己折腾一下练练手好了。
总之说干就干,没理由你们行我就写不出来对吧。小菜虾加油!!!顺便让各位见识下我过年拼的零重力审判。
我想要啥
做事情之前一定要想好我想要什么,哪些是我一定需要的功能。
- 我想要那个插座的功能
- 但是单独的插件如果也能使用那么就更棒了
- 在当前的BaseTransform的基础上调整一下
- 我开始需要Task依赖了
开工了
读书人的事怎么能叫偷,具体的实现我参考了下滴滴的booster的一部分思路,booster还是写的非常不错的,其中将多个插件组合到一起就是用的AutoService
。
但是动态化之后,你就缺失了能主动设置顺序的能力,这个时候如果插件之间有依赖关系,那么就需要另外一个东西来解决这个问题。
依赖顺序执行的这部分代码,我抄袭了下我大佬写的BRouter内的CachingDirectedGraphWalker
,大佬则是参考了下Gradle编译时的依赖排序。
巧用AutoService
SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。
如果你写过AbstractProcessor
,我们会在上面打一个AutoService
的注解。AutoService
其实就是最简单粗暴的SPI(Service Provider Interface)
。
而SPI
机制可以帮助大家对于项目进行一定的解耦,因为是基于接口进行编程,而不是关心具体的实现类。举个例子如果AB两个业务之间相互依赖的情况下,你们一般会咋做呢?如果是我,肯定就会自己将其中ab互相调用的逻辑进行一次接口抽象,然后将实现类放在AB的模块内,因为直接使用的就是其接口,也就可以将两个模块间的循环依赖关系给解决了。
而SPI最重要的一个能力就是把项目内定义的注解收集起来,然后通过ServiceLoader
把这些注解定义的实现类加载出来。因为AutoService是基于META-INFO
格式的文件的,而文件因为有个IO操作,所以相对来说性能较差,但是如果是在Plugin中,这100ms左右的时间是完全可以被忽略的。
也就是说我只要在Base基础库定义好抽象接口,之后就可以在组合插件上,通过ServiceLoader
机制,把所有的实现类都调用一次,就能完成这个简单的功能。
interface PluginProvider {
fun getPlugin(): Class<out Plugin>
fun dependOn(): List
}
复制代码
上面就是我定义的抽象接口,第一个方法就是获取当前定义好的插件,而第二个就是获取到当前插件所依赖的前置插件(为了后面的拓扑排序做准备)。
代码语言:javascript复制class MultiPlugin : Plugin<Project> {
override fun apply(project: Project) {
// 菜虾版本byteX beta版本
val providers = ServiceLoader.load(PluginProvider::class.java).toList()
providers.forEach {
// 将子插件注册到合并插件上
project.plugins.apply(it.getPlugin())
}
}
}
复制代码
上面是我之前写的beta版本,这个就是ServiceLoader的最简单的使用了。
如何实现一个子插件呢
上面我们已经定义好了接口,让各位看看我怎么写一个子插件的。
代码语言:javascript复制@AutoService(value = [PluginProvider::class])
class AutoTrackPluginProvider : PluginProvider {
override fun getPlugin(): Class<out Plugin> {
return AutoTrackPlugin::class.java
}
override fun dependOn(): List {
return arrayListOf().apply {
add("com.kronos.plugin.thread.ThreadHookProvider")
}
}
}
复制代码
这里为了保证子插件之间没有形成循环依赖,所以是通过className的形式,声明依赖关系的。举个例子,当前插件就要依赖于ThreadHookProvider
。而getPlugin方法因为返回的就是一个Plugin的子类,所以也就是当前的plugin可以单独存在。
这样我从我个人使用上来说,因为插件是分离而且都能单独存在的,所以就可以直接使用了。
有向无环图
你写的plugin多了,其中就会出现好几个插件之间也存在了依赖关系,前置任务要先被执行,然后才能执行后面的任务。而Transform的本质其实也是一个Task,所以它的依赖关系就会非常难搞。
他们之间的关系很有可能就和下面这张图一样,这种数据结构我们叫他Graph。
在计算机科学中,一个图就是一些顶点的集合,这些顶点通过一系列边结对(连接)。顶点用圆圈表示,边就是这些圆圈之间的连线。顶点之间通过边连接。
而在我们的逮虾户X中,正常会出现的就是一个单向图(DAG),划重点,面试的时候可以吹牛的。一般有几种场景会使用到这个东西。第一就是gradle编译的时候,因为Module之间有依赖关系,所以就需要搞清楚他们的先后执行顺序,第二就是可以被应用到启动优化中,因为初始化的时候也会有很多这种初始化依赖关系的,举个例子jetpack组件中的starup就是基于拓扑排序的。还有就是workManger
也有对这个的使用。
而这次逮虾户X也要使用到这个技术栈了,这里我参考了我们大佬在BRouter内使用的CachingDirectedGraphWalker
,这个就是gradle源代码内解决Task的拓扑排序的类。
fun analyze(): Set {
val modules = ConcurrentHashMap()
val walker = CachingDirectedGraphWalker(false, object : DirectedGraph {
override fun getNodeValues(node: ModuleNode, values: MutableCollection<in ModuleNode>, connectedNodes: MutableCollection<in ModuleNode>) {
values.add(node)
node.taskDependencies.forEach { name ->
modules[name]?.let {
connectedNodes = ModuleNode(it.moduleName, it.taskDependencies)
} ?: if (!allowMiss) error("Task(${name}) that $node dependsOn does not exists.")
}
}
})
libs.parallelStream().forEach {
val nodes = arrayListOf()
modules.put(it.moduleName, it)?.let {
error("Duplicated module: ${it.moduleName}")
}
nodes = ModuleNode(it.moduleName, it.taskDependencies)
synchronized(walker) {
walker.add(nodes)
}
}
return walker.findValues()
}
复制代码
这部分就是我拿来计算其中的拓扑排序相关的。CachingDirectedGraphWalker
其中定义的DirectedGraph
接口呢,是让我们可以自己定义当前Node
节点的连通关系的,通过这个connectedNodes
添加当前节点的连通,我们就可以在最后的findValues(),方法直接返回当前有向无环图的拓扑排序了。
class MultiPlugin : Plugin<Project> {
override fun apply(project: Project) {
// 菜虾版本byteX beta版本
val providers = ServiceLoader.load(PluginProvider::class.java).toList()
val graph = mutableListOf()
val map = hashMapOf()
providers.forEach {
val list = it.dependOn()
val className = it.javaClass.name
val meta = ModuleNode(className, list)
graph.add(meta)
map[className] = it
}
Log.info("after sort:$graph")
val analyzer = Analyzer(graph, true)
val graphNodes = analyzer.analyze()
Log.info("graphNode:$graphNodes")
graphNodes.forEach {
map[it.moduleName]?.apply {
project.plugins.apply(getPlugin())
}
}
}
}
复制代码
这个地方由于我使用的是Node,而不是原始的Plugin,所以我在完成拓扑排序之后要重新获取到PluginProvider
对象,进行一次代码调用操作。
TODO
AGP(Android Gradle Plugin)每个大版本迭代之后其实对于api都会出现变更,如果能有一个统一的收敛,其实也是非常不错的。所以后面我会考虑下对一部分基础api进行收敛,放到自己的baseTransform中去。
总结
其实写这个的目的就是做一点存粹的技术储备,我以前在大佬的基础上做过一部分启动优化相关的,我对拓扑排序其实就有点好奇,奈何自己没玩过。其次我后面也打算在项目内将SDK打散成多个Plugin,但是Plugin一多就会很零散,调用会显得很多余。所以就有了这么个实验性的玩具了。