前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >我要抄袭字节的Bytex了 | Transform 进阶教程

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

作者头像
逮虾户
发布2021-03-03 16:17:22
1.4K0
发布2021-03-03 16:17:22
举报
文章被收录于专栏:逮虾户逮虾户

我是小偷

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>


    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的拓扑排序的类。

代码语言:javascript
复制
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(),方法直接返回当前有向无环图的拓扑排序了。

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 我是小偷
  • 我想要啥
  • 开工了
    • 巧用AutoService
      • 如何实现一个子插件呢
      • 有向无环图
      • TODO
      • 总结
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档