I am a thief

Demo project or the previous Github address, posted at the front of the project, interested big guy please click a star bar.

ByteX is a bytecode plug-in platform based on the Gradle Transform API and ASM (think of it as a socket with an infinite number of plugs, perhaps?). .

At present, several bytecode plug-ins are integrated, and each plug-in is completely independent. It can not only exist independently from the host ByteX, but also automatically integrate into the host and other plug-ins into a separate Transform. The code is completely decoupled from plug-in to plug-in, and from host to plug-in (kind of like componentalization), which makes ByteX extensible in code and makes the development of new plug-ins much easier and more efficient.

Bytex is basically an outlet, and I’ve been dying to have one of these for the last year. I will figure out how to elegantly combine multiple plug-ins in the simplest way to form my own shrimp-catcher X. But I’m not a big fan of byteX, mainly because I’m not familiar with its API, so I’ll just try it on my own.

Anyway, just do it. There’s no reason why I can’t write if you’re good. Go shrimp!! By the way, let’s have a look at my New Year’s zero gravity trial.

What I want

Before I do something, I have to decide what I want and what features I definitely need.

  1. I want the power of that socket
  2. But it would be nice if individual plug-ins could also be used
  3. Adjust it based on the current BaseTransform
  4. I’m starting to need Task dependencies

To start the

For specific implementation, I refer to part of the idea of Booster in didi. Booster is very well written, and AutoService is used to combine multiple plug-ins together.

But when you make it dynamic, you lose the ability to actively set the order, and when you have dependencies between plug-ins, you need something else to solve the problem.

Depend on the order of this part of the code, I copied the written down my bosses BRouter CachingDirectedGraphWalker, who is under the reference the Gradle compile time depends on the order.

Use opportunely AutoService

The Service Provider Interface (SPI) allows the caller to formulate the Interface specification and provide it to the external for implementation. The caller selects the required external implementation during invocation. In terms of users, SPI is used by framework extenders.

If you’ve written AbstractProcessor, we’ll put an AutoService annotation on it. An AutoService is the simplest Service Provider Interface (SPI).

The SPI mechanism helps you decouple your project somewhat, because you’re programming based on interfaces rather than on specific implementation classes. For example, what do you do when two businesses AB are interdependent? If I were you, I would have abstracted the logic of ab calling each other once, and then put the implementation class in the module of AB, because the interface is directly used, so that the circular dependency between the two modules can be solved.

One of SPI’s most important capabilities is to collect annotations defined in a project and then load the implementation classes for those annotation definitions through the ServiceLoader. Since AutoService is based on a meta-info file, the performance of the file is relatively poor due to the IO operation, but in Plugin, the 100ms time can be completely ignored.

That is, I define the abstract interface in the Base library, and then I can use the ServiceLoader mechanism on the composite plug-in to call all the implementation classes at once.

interface PluginProvider {

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


    fun dependOn(a): List<String>
}
Copy the code

The above is the abstract interface I defined. The first method is to get the currently defined plug-in, and the second method is to get the pre-plug-in that the current plug-in depends on (in preparation for the subsequent topology sorting).

class MultiPlugin : Plugin<Project> {

    override fun apply(project: Project) {
        // Shrimp version byteX Beta
        val providers = ServiceLoader.load(PluginProvider::class.java).toList()
        providers.forEach {
          // Register the child plug-in with the merged plug-in
            project.plugins.apply(it.getPlugin())
        }
    }
}
Copy the code

Above is the beta VERSION I wrote earlier, and this is the easiest way to use ServiceLoader.

How do you implement a child plug-in

We have defined the interface above, let you see how I write a child plug-in.

@AutoService(value = [PluginProvider::class])
class AutoTrackPluginProvider : PluginProvider {

    override fun getPlugin(a): Class<out Plugin<Project>> {
        return AutoTrackPlugin::class.java
    }

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

In order to ensure that there is no circular dependency between child plug-ins, the dependency is declared in the form of className. For example, the current plug-in relies on ThreadHookProvider. The getPlugin method returns a subclass of Plugin, so the current Plugin can stand alone.

This way, for my personal use, the plugins are separate and can stand alone, so they can be used directly.

Directed acyclic graph

As you write more plugins, there will be dependencies between several plugins, with the first task being performed before the next. The Transform is also a Task by nature, so its dependencies can be tricky.

The relationship between them is likely to look something like this Graph, which we call a Graph.

In computer science, a graph is a collection of vertices that are paired (joined) by a series of edges. Vertices are represented by circles, and edges are the lines between those circles. Vertices are connected by edges.

And in our shrimp door X, the normal will appear is a one-way map (DAG), delimit the key point, when the interview can brag. There are several scenarios where this is used. The first is that when gradle is compiled, it needs to know the order in which modules are executed because of their dependencies. The second is that it can be applied to boot optimization because there are many initialization dependencies. For example, starUp in the Jetpack component is sorted based on topology. WorkManger also uses this.

I made a mistake, I think I misled you a little bit, but I’ve reworked the topological sort algorithm. The following content can not be referenced, this time I refer to the topology sorting algorithm in AnchorTask of Xugong.

This time to catch the shrimp households also want to use X to the extent that the technology stack, here I refer to our bosses use within BRouter CachingDirectedGraphWalker, this is gradle within the source code to solve the Task of topological sort of class, bosses are calculated based on Tarjan ring to rely on, I haven’t finished studying how it works in Gradle yet.

class MultiPlugin : Plugin<Project> {

    override fun apply(project: Project) {
        // Shrimp version 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()) } } } }Copy the code

Since I am using Node instead of the original Plugin, I need to retrieve the PluginProvider object after finishing the topology sorting and make a code call.

TODO

AGP(Android Gradle Plugin) after each iteration of the large version of the API will actually change, if there is a unified convergence, in fact, is very good. So I’m going to think about converging some of the base APIS into my baseTransform.

conclusion

In fact, the purpose of writing this is to do a bit of the essence of technical reserves, I used to do a part of the startup optimization on the basis of the big guy, I am actually a little curious about topology sorting, but I have not played. Secondly, I also planned to break the SDK into multiple plugins in the project later, but the plugins would be scattered and the calls would be redundant. So this is an experimental toy.