我要抄袭字节的Bytex了 | Transform 进阶教程

2023-10-16 17:24:17 浏览数 (2)

我是小偷

Demo项目还是之前的Github地址,贴在项目的最前面,有兴趣的大佬求求你点个star吧。

ByteX是一个基于gradle transform api和ASM的字节码插件平台(或许,你可以把它当成一个有无限个插头的插座?)。

目前集成了若干个字节码插件,每个插件完全独立,既可以脱离ByteX这个宿主而独立存在,又可以自动集成到宿主和其它插件一起整合为一个单独的Transform。插件和插件之间,宿主和插件之间的代码是完全解耦的(有点像组件化),这使得ByteX在代码上拥有很好的可拓展性,新插件的开发将会变得更加简单高效。

bytex说白了就是一个插座,我去年就非常想自己也能有一个这玩意(偷鸡)。我会盘算一下如何用最简单的方式将多个插件优雅的组合到一起,形成自己的逮虾户x。但是我不是特别喜欢byteX,主要是因为我对他的api不熟悉啊,我还是自己折腾一下练练手好了。

总之说干就干,没理由你们行我就写不出来对吧。小菜虾加油!!!顺便让各位见识下我过年拼的零重力审判。

我想要啥

做事情之前一定要想好我想要什么,哪些是我一定需要的功能。

  1. 我想要那个插座的功能
  2. 但是单独的插件如果也能使用那么就更棒了
  3. 在当前的BaseTransform的基础上调整一下
  4. 我开始需要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机制,把所有的实现类都调用一次,就能完成这个简单的功能。

代码语言:javascript复制
interface PluginProvider {

    fun getPlugin(): Class<out Plugin<Project>>


    fun dependOn(): List<String>
}

上面就是我定义的抽象接口,第一个方法就是获取当前定义好的插件,而第二个就是获取到当前插件所依赖的前置插件(为了后面的拓扑排序做准备)。

代码语言: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<Project>> {
        return AutoTrackPlugin::class.java
    }

    override fun dependOn(): List<String> {
        return arrayListOf<String>().apply {
            add("com.kronos.plugin.thread.ThreadHookProvider")
        }
    }

}

这里为了保证子插件之间没有形成循环依赖,所以是通过className的形式,声明依赖关系的。举个例子,当前插件就要依赖于ThreadHookProvider。而getPlugin方法因为返回的就是一个Plugin的子类,所以也就是当前的plugin可以单独存在。

这样我从我个人使用上来说,因为插件是分离而且都能单独存在的,所以就可以直接使用了。

有向无环图

你写的plugin多了,其中就会出现好几个插件之间也存在了依赖关系,前置任务要先被执行,然后才能执行后面的任务。而Transform的本质其实也是一个Task,所以它的依赖关系就会非常难搞。

他们之间的关系很有可能就和下面这张图一样,这种数据结构我们叫他Graph。

在计算机科学中,一个图就是一些顶点的集合,这些顶点通过一系列边结对(连接)。顶点用圆圈表示,边就是这些圆圈之间的连线。顶点之间通过边连接。

而在我们的逮虾户X中,正常会出现的就是一个单向图(DAG),划重点,面试的时候可以吹牛的。一般有几种场景会使用到这个东西。第一就是gradle编译的时候,因为Module之间有依赖关系,所以就需要搞清楚他们的先后执行顺序,第二就是可以被应用到启动优化中,因为初始化的时候也会有很多这种初始化依赖关系的,举个例子jetpack组件中的starup就是基于拓扑排序的。还有就是workManger也有对这个的使用。

我之前写错了,应该是误导了大家了,我这边重新调整了下拓扑排序的算法。以下内容可以不参考了,这次参考了下徐工大佬的AnchorTask内的拓扑排序算法。

而这次逮虾户X也要使用到这个技术栈了,这里我参考了我们大佬在BRouter内使用的CachingDirectedGraphWalker,这个就是gradle源代码内解决Task的拓扑排序的类,大佬是基于Tarjan来计算环状依赖的,我暂时还没研究完gradle内是如何使用的。

代码语言:javascript复制
class MultiPlugin : Plugin<Project> {

    override fun apply(project: Project) {
        // 菜虾版本byteX beta版本
        val providers = ServiceLoader.load(PluginProvider::class.java).toList()
        val graph = mutableListOf<ModuleNode>()
        val map = hashMapOf<String, PluginProvider>()

        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一多就会很零散,调用会显得很多余。所以就有了这么个实验性的玩具了。

0 人点赞