Related to the author: the author: green bamboo blog: http://www.jianshu.com/p/1b1d77f58e84 copyright owned by the author, this article is made by authorization of the author to push.

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:

The picture above looks quite clear, but it can lead to some misunderstandings. There are a few minor problems, which may not be very clear:

  • 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

Androidstudio provides a great way to split large code. Using the Multiple Module feature in IDE, it is easy to split the 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. But there are a few problems with this graph:

  • From the above figure, it seems 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 you debug separately?

  • Can the main project reference components directly? 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.

2-2 Separate debugging of components

It’s actually easier to debug it alone, just switch the Apply plugin: ‘com.android.library’ to the apply plugin: ‘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 the resourcePrefix “xxx_” to each component’s build.gradle 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.

2-3 Data transmission of components

As we mentioned above, data interaction between main projects and components, and between components, cannot be done directly using class references to each other. So how do you do this isolation? Here we use the interface + implementation structure. Each component declares its own services. These services are abstract classes or interfaces. The component implements these services and registers them with a unified Router. To use the functionality of a component, we simply request the Router to implement the Service. We don’t care about the implementation details, as long as it returns the result we need. This is similar to Binder’s C/S architecture.

Because the data transfer between our components is based on interface programming, interface and implementation are completely separated, so components can be decoupled, we can replace, delete and other dynamic management of components. There are a few small things to be clear about:

  • How does a component expose the services it provides? For the sake of simplicity in the project, we specially established a componentService dependency library, which defines the service provided by each component and some common models. The integration of services for all components is intended to make it easier to operate in the initial phase of the split, and will need to be automated later. This dependency library needs to be strictly open and closed to avoid issues such as version compatibility.

  • The implementation of a service is registered with the Router by the component to which it belongs. When was it registered? This involves the loading of components and other life cycles, which we will cover later.

  • An easy mistake to make is to pass data in persistent ways, such as file, sharedpreference, and so on. This should be avoided. Here is the architecture with data transfer:

2-4 UI hops between components

UI jumps are also a special service provided by components and can be attributed to the above data passing. However, we usually handle UI jumps separately, usually through short links to specific activities. Each component can register the schme and host of short chains it can handle, and define the format for transferring data. The UIRouter is then registered to the unified UIRouter, which distributes routes by matching schme and host. The UI jump section is implemented by adding annotations to each Activity and then using APT to form concrete logical code. This is also the current mainstream implementation of UI routing in Android.

2-5 Component life cycle

Since we want to manage components dynamically, we add several life cycle states to each component: load, unload, and dimension reduction. To do this, we add an ApplicationLike class to each component that defines the onCreate and onStop lifecycle functions.

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. 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. 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:

2-6 Integration debugging

Just because each component passes debugging alone does not mean that there are no problems with integration. Therefore, we need to integrate several component machines into an APP for verification in the later development period. 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:

2-7 Code isolation

We have found solutions to the three problems we raised in componentization at the beginning, but there is still a hidden danger, that is, can we use Compile project(XXX :reader.aar) to introduce 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 code if (project.Android instanceof AppExtension) {AssembleTask AssembleTask = getTaskInfo(project.gradle.startParameter.taskNames) if (assembleTask.isAssemble && (assembleTask. Modules. The contains (" all ") | | assembleTask. Modules. The contains (module))) {/ / add the component dependence Project.dependencies. Add ("compile"," XXX :reader-release@aar") // The bytecode insert part is also implemented here}} 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

3-1 Split principle

Componentized unbundling is a huge undertaking, especially when it comes to unbundling hundreds of thousands of lines of code. 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

We started by saying that the ideal code organization is plug-in, and that you have full 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.

[Commercial breaks]

We are in urgent need of Android and iOS heroes to join us. As long as you meet the following points, that is what we need: 1. Passionate about mobile terminal development (Android /ios/ RN), preferably with 2-3 years of APP development experience. 2. Our office is located in a beautifully decorated single-family building along Chang ‘an Avenue in a very beautiful environment. As for the treatment, it is absolutely industry leading, as evidenced by our internal promotion award being the luxurious Apple three-piece set. If you are interested, please send your resume to [email protected].

Recommended reading

Android APP Banner, this one is enough

2

Parallax Animation – Yahoo News Digest loaded

3

Android automatic test tool UiAutomator usage details

If you find this article helpful, please share it with others

Pay attention to Android technology grocery store, there are Android dry goods articles to share every day!

Long click the QR code to follow us