The componentization solution proposed in this paper has been open source, see Android Complete Componentization Solution open source, for detailed source analysis, see the article, see Android Complete Componentization Demo release

(This article is the last article “Android complete componentization Demo release” principle explanation, more theoretical, can be more comprehensive understanding of componentization)

Modularization, componentization and plug-in

At a certain point in the project, as the number of people increases and the code becomes bloated, modular breakdowns become necessary. In my opinion, modularity is a guiding concept, and the core idea is to divide and conquer and reduce coupling. And how to implement in the Android project, there are currently two ways, but also two schools, one is componentization, one is plug-in. When it comes to the difference between componentization and plug-in, there is a good picture:

  • Is componentization a whole? Can you still exist without your head and arms? On the left, it seems that componentization is an organic whole that requires all organs to exist. In fact, one of the goals of componentization is to reduce the dependence between the whole (APP) and the organ (component). App can exist and operate normally without any organ.
  • Can the head and arms stand alone? The picture on the left does not make it clear. In fact, the answer should be yes. Each organ (component) can survive on its own after providing some basic functions. This is the second goal of componentization: components can run separately.
  • Can componentization and pluginization both be represented on the right? If the answer to both questions is YES, the answer to this one is also YES. Each component can be viewed as a separate whole, which can be integrated with other components (including the main project) as needed to complete the formation of an APP
  • Can the small robot in the picture on the right be added and modified dynamically? If both componentization and pluginization were represented on the right, the answer to this question would be different. In terms of componentization, the answer to this question is partially yes, that is, you can add and modify dynamically at compile time, but not at run time. With plug-ins, the answer is simply yes, both at compile time and at run time! This article mainly is the realization of the modular thinking, about plug-in “don’t do discuss technical details, we are summarized from the above questions and answers to one conclusion: componentization and plug-in (should) is the only difference between the biggest difference between is componentized at run time does not have the function that dynamically add and modify components, but the plugin is ok. Aside from criticizing the “morality” of plugins, I think plugins are a boon for Android developers and give us a lot of flexibility. But since there is no perfectly compatible plug-in solution yet (RePlugin’s hunger marketing has done a good job, but has yet to prove effective), applying any plug-in solution is a dangerous undertaking, especially with a mature product running hundreds of thousands of code. So we decided to start from componentization, in line with the idea of doing the most thorough componentization scheme to carry out the code reconstruction, the following is the recent thinking results, welcome everyone to put forward suggestions and opinions.

How to realize componentization

To achieve componentization, regardless of the technical path adopted, the main issues to consider include the following:

  • Code decoupling. How to break down a huge project into organic whole?
  • Components run separately. As mentioned above, each component is a complete whole. How do you make it run and debug separately?
  • Data transfer. Because each component provides services to other components, how does the Host pass data to and from components?
  • The UI jump. UI jump can be considered as a special kind of data transfer. What is the difference in the implementation idea?
  • Component life cycle. The goal is to have components that can be used on demand and dynamically, thus involving the lifecycle of component loading, unloading, and dimension reduction.
  • Integration debugging. How do you compile components on demand during development? Only one or two components may be integrated at a time, which greatly reduces compile time and improves development efficiency.
  • Code isolation. If the interaction between components is still direct reference, then components are not decoupled at all. How can we fundamentally avoid direct reference between components? How do you eliminate coupling at all? Only by doing so can we achieve complete componentization. ###2-1 Code decoupling is a good way to split large code. Androidstudio provides a good support for this. Using the Multiple Module feature in IDE, it is easy to split code initially. Here we distinguish between the two modules,
  • One is the base library, where the code is directly referenced by other components. For example, a web library module can be thought of as a library.
  • The other, we call Component, is a complete functional module. Reading a book or sharing a Module is a Component. For convenience, library is called a dependent library, and Component is called a Component, and the componentization we talk about is mainly for Component. The module responsible for assembling these components to form a complete APP is generally called the main project, main Module or Host, which is also called the main project for convenience. After some simple thinking, we might be able to break the code down into the following structure:

    This kind of splitting is easy to do, as shown in the diagram, reading, sharing, and so on have split components and shared dependencies on a common dependency library (just one is drawn for simplicity), which are then referenced by the main project. Reading, sharing, and so on are not directly related to each other, so we can assume that we have decoupled the components. There are a few problems with this diagram, however: ● From the above diagram, we seem to think that components can only be used if they are integrated into the main project, when in fact we want each component to be a whole, run and debug independently. How do we debug separately? ● Can the main project refer directly to components? Can we refer to components directly using compile Project (: Reader)? If this is the case, then the coupling between the main project and the components has not been removed. Components can be managed dynamically. If we delete the reader component, the main project will not compile. Therefore, it is not possible for the main project to directly reference components, but our reading components will eventually be connected to APK, not only the code will be combined into the claases.dex, but also the resources will be merged into apK resources through meage operation, how to avoid this contradiction? ● Is it true that components do not reference or interact with each other? The reading component also calls the sharing module, which is not shown at all. How do components interact with each other? We will solve these problems one by one. First, we will look at the effect of code decoupling, such as the above direct reference and use of the class will not work. So we think that the primary goal of code decoupling is complete isolation between components, so that not only can we not directly use classes in other components, but we can better not know the implementation details at all. Only this degree of decoupling is needed. Library = ‘com.android.library’; library = ‘com.android.library’; library = ‘com.android.library’ ‘com.android.application’ will do, but we also need to modify the AndroidManifest file because a single debug requires an entry actiiVity. We can set a variable isRunAlone to indicate whether we need to debug separately. Depending on the value of isRunAlone, we can use different Gradle plug-ins and AndroidManifest files, and even add Java files such as Application. So we can do some initialization. To avoid duplicate resource names between components, add a resourcePrefix “xxx_” to build. Gradle for each component to fix the resourcePrefix for each component. Here is an example of build.gradle for a reading component:

if(isRunAlone.toBoolean()){    
apply plugin: 'com.android.application'
}else{  
 apply plugin: 'com.android.library'}... resourcePrefix"readerbook_"
    sourceSets {
        main {
            if (isRunAlone.toBoolean()) {
                manifest.srcFile 'src/main/runalone/AndroidManifest.xml'
                java.srcDirs = ['src/main/java'.'src/main/runalone/java']
                res.srcDirs = ['src/main/res'.'src/main/runalone/res']}else {
                manifest.srcFile 'src/main/AndroidManifest.xml'}}}Copy the code

With this extra code, we set up a test Host for the component to run on, so we can refine our diagram above.

  1. Load: As mentioned above, each component is responsible for registering its service implementation with the Router. The implementation code is written in the onCreate method. The main project calls the onCreate method as a component load, because once the onCreate method is complete, the component registers its service with the Router, and other components can use the service directly.
  2. Unload: Unload is basically the same as load, except that the ApplicationLike onStop method is called, in which each component unregisters its service implementation from the Router. However, this usage scenario may be rare and generally applies to some components that are used only once.
  3. Dimension reduction: Dimension reduction is used in rarer scenarios, such as when a component has a problem and we want to change the component from a native implementation to a WAP page. Dimension reduction generally requires background configuration to take effect. You can check online configuration in onCreate. If dimension reduction is required, redirect all UI to the configured WAP page. One small detail is that the main project is responsible for loading the component. Since the main project is isolated from the component, how does the main project call the lifecycle method of the component ApplicationLike? Currently we use compile-time bytecode insertion. Scan all ApplicationLike classes (which have a common parent) and insert the code calling ApplicationLike.onCreate in the main project onCreate via Javassisit. Let’s optimize the componentized architecture diagram again:

    Each component debugging alone does not mean that there is no problem with integration, so we need to integrate several component machines into an APP for verification in the late development. Because our mechanism above guarantees isolation between components, we can choose as many components as we want to participate in the integration. This on-demand loading mechanism can ensure great flexibility in integration debugging, and can increase compilation speed. After each component is developed, we publish a Relaese AAR to a common repository, typically a local Maven library. The main project then parameters the components to be integrated. So we slightly changed the wiring between the components and the main project, resulting in the final componentized architecture diagram as follows:

    Can we use Compile Project (XXX :reader.aar) to import components? Although we used the interface + implementation architecture in the data transfer section and had to program interfaces between components, once we introduced reader.aar, we could use the implementation classes directly, and our specification for interface programming became a dead letter. If any code (whether intentional or not) does this, the work we’ve done has been wasted. We want to introduce an AAR only at assembleDebug or assembleRelease, and not see any of the components during development, thereby eliminating the problem of referring to implementation classes at all. To solve this problem, we create a Gradle plugin and apply the plugin to each component. The plugin configuration code is relatively simple:

// Add various component dependencies according to the configuration, and automatically generate component loading codeif (project.android instanceof AppExtension) {
            AssembleTask assembleTask = getTaskInfo(project.gradle.startParameter.taskNames)
            if (assembleTask.isAssemble
                    && (assembleTask.modules.contains("all") | | assembleTask. Modules. The contains (module))) {/ / add components depend on the project. The dependencies. The add ("compile"."xxx:reader-release@aar"Private AssembleTask getTaskInfo(List<String> taskNames) {AssembleTask AssembleTask = new AssembleTask();for (String task : taskNames) {
            if (task.toUpperCase().contains("ASSEMBLE")) {
                assembleTask.isAssemble = true;
                String[] strs = task.split(":")
                assembleTask.modules.add(strs.length > 1 ? strs[strs.length - 2] : "all"); }}return assembleTask
    }
Copy the code

Componentized split steps and dynamic requirements

Componentized splitting is a huge project, especially from a large project of hundreds of thousands of lines of code to split out, there are many things to consider. I think it can be divided into three steps:

  • Functions with clear boundaries from product requirements to development and then to operation began to be separated, such as reading module, live broadcast module, etc., which began to be separated in batches
  • In unsplitting, the module that caused the component dependencies of the main project continues to be unbundled, such as the account system
  • The final main project is a Host that contains small functional modules (such as a startup diagram) and the concatenation logic between components

###3-2 Componentized Dynamic requirements At the beginning, we said that the ideal code organization would be plug-in, and that you would have complete runtime dynamics. During the migration to plug-ins, we can improve compilation speed and dynamic updates in the following centralized ways.

  • Incremental compilation at the component level is used for fast compilation. Code-level incremental compilation tools such as Freeline (but poorly supported by Databinding), Fastdex, and so on can be used before components are pulled away
  • For dynamic updates, large functional improvements such as new components are not supported for the time being. You can use temporary method-level hot fixes or functional-level tools like Tinker, which is expensive to access.

Four,

In this paper, the author summarizes some ideas in designing the componentization of “Get APP”. At the beginning of the design, we refer to the existing componentization and plug-in schemes, and add some of my own ideas on the shoulders of giants, mainly in the aspects of componentization life cycle and complete code isolation. The final code isolation, in particular, should not only have a formal constraint (for interface programming), but also a mechanism to ensure that developers do not make mistakes, which I think can only be considered a complete componentization solution.