1. Modularization, componentization and plug-inization When a project develops to a certain extent, with the increase of personnel, the code becomes more and more bloated, and then it must be split into modules. In my opinion, modularization is a guiding concept, and its core idea is to divide and conquer and reduce coupling. There are currently two ways to implement it in Android projects, which are also two major schools of thought: one is componentization and the other is plug-inization. When it comes to the difference between componentization and plug-inization, there is a very vivid picture: The above picture looks clear, but it is easy to cause some misunderstandings. There are several small problems that may not be clearly explained in the picture: Is componentization a whole? Can it still exist without the head and arms? In the left picture, it seems that componentization is an organic whole that requires all organs to be healthy. In fact, one of the goals of componentization is to reduce the dependency between the whole (app) and the organs (components). The app can exist and run normally without any organ. Can the head and arms exist independently? The left picture does not explain it clearly, but the answer should be yes. Each organ (component) can survive independently after supplementing some basic functions. This is the second goal of componentization: components can operate independently. Can both componentization and plug-inization be represented by the right picture? If the answers to the above two questions are both YES, the answer to this question is also YES. Each component can be regarded as a separate whole, which can be integrated with other components (including the main project) as needed to form an app. Can the little robot in the right picture be added and modified dynamically? If both componentization and plug-inization are represented by the right picture, then the answer to this question is different. For componentization, the answer to this question is partially yes, that is, it can be added and modified dynamically at compile time, but it cannot be done at runtime. For plug-inization, the answer to this question is very straightforward, that is, it is completely yes, whether it is at compile time or runtime! This article mainly focuses on the implementation ideas of componentization. The technical details of plug-inization are not discussed. We just summarize a conclusion from the above questions and answers: the biggest difference between componentization and plug-inization (probably the only difference) is that componentization does not have the function of dynamically adding and modifying components at runtime, but plug-inization can. Putting aside the "moral" criticism of plug-inization, I think plug-inization is indeed a blessing for an Android developer, which will give us great flexibility. However, there is no completely suitable and fully compatible plug-in solution (RePlugin's hunger marketing is very good, but the effect has not yet been seen), especially for a mature product with hundreds of thousands of codes, it is very dangerous to apply any plug-in solution. So we decided to start with componentization, and refactor the code in the spirit of making the most thorough componentization solution. The following is the result of recent thinking, and everyone is welcome to put forward suggestions and opinions. 2. How to implement componentization To achieve componentization, no matter what technical path is adopted, the following issues need to be considered:
2.1 Code Decoupling Android studio can provide good support for splitting huge codes. Using the multiple module function in IDE, we can easily split the code. Here we distinguish between two modules:
For convenience, we call library as dependency library and component as component. The componentization we are talking about is mainly for this type of component. The module responsible for assembling these components to form a complete app is generally called the main project, main module or host. For convenience, we also call it the main project. After a simple thought, we may be able to split the code into the following structure: Componentization and simple decomposition This kind of split is relatively easy to do. From the diagram, we can see that reading, sharing, etc. have been split into components and rely on a common dependency library (only one is drawn for simplicity), and then these components are referenced by the main project. There is no direct connection between reading, sharing, etc. components, so we can think that the components have been decoupled. However, there are several problems with this diagram that need to be pointed out:
We will solve these problems one by one later. First, let's look at the effect of code decoupling. Directly referencing and using the classes in the above will definitely not work. Therefore, we believe that the primary goal of code decoupling is to completely isolate components. Not only can we not directly use the classes in other components, but we may not even understand the implementation details. Only this level of decoupling is what we need. 2.2 Individual Debugging of Components In fact, single debugging is relatively simple. You only need to switch apply plugin: 'com.android.library' to apply plugin: 'com.android.application'. However, we also need to modify the AndroidManifest file because a single debugging requires an entry activity. We can set a variable isRunAlone to mark whether separate debugging is needed at present. According to the value of isRunAlone, we can use different gradle plug-ins and AndroidManifest files, and even add Java files such as Application, so as to perform initialization operations. To avoid duplication of resource names between different components, add resourcePrefix "xxx_" to the build.gradle of each component to fix the resource prefix of each component. The following is an example of the build.gradle of the reading component:
With this additional code, we built a test host for the component so that the component code can run in it, so we can further optimize our framework diagram above. Supports separate debugging of components 2.3 Component Data Transfer As mentioned above, the main project and components, or components and components cannot directly use mutual references of classes to interact with each other. So how do we achieve this isolation? Here we use the interface + implementation structure. Each component declares the services it provides. These services are abstract classes or interfaces. The components are responsible for implementing these services and registering them in a unified router. If you want to use the function of a component, you only need to request the implementation of this service from the router. We don't care about the specific implementation details at all, as long as we can return the results we need. This is very similar to the C/S architecture of Binder. Because data transfer between our components is based on interface programming, the interface and implementation are completely separated, so the components can be decoupled, and we can dynamically manage components such as replacement and deletion. There are a few small issues that need to be clarified: How do components expose the services they provide? For simplicity, we have created a componentservice dependency library in the project, which defines the services and some public models provided by each component. All component services are integrated together to make the operation easier in the early stage of splitting, and they need to be generated in an automated way later. This dependency library needs to strictly follow the open-closed principle to avoid version compatibility and other issues. The specific implementation of the service is registered in the Router by the component to which it belongs. So when is it registered? This involves the life cycle of component loading, etc., which we will introduce later. A small mistake that is easy to make is to pass data through persistence methods, such as file, sharedpreference, etc. This needs to be avoided. The following is the architecture diagram after adding the data transmission function: Data transfer between components 2.4 UI jump between components It can be said that UI jump is also a special service provided by the component, which can be attributed to the above data transmission. However, we usually handle UI jump separately, usually through the short chain method to jump to the specific Activity. Each component can register the schme and host of the short chain it can handle, and define the format of the transmitted data. Then register it in the unified UIRouter, which is responsible for distributing routes through the matching relationship between schme and host. The specific implementation of the UI jump part is to add annotations to each Activity, and then form specific logic codes through apt. This is also the mainstream implementation method of UI routing in Android. 2.5 Component Lifecycle Since we want to manage components dynamically, we add several life cycle states to each component: loading, unloading, and dimensionality reduction. To this end, we add an ApplicationLike class to each component, which defines two life cycle functions: onCreate and onStop. Loading: As mentioned above, each component is responsible for registering its own service implementation into the Router, and its specific implementation code is written in the onCreate method. Then the main project calling this onCreate method is called component loading, because once the onCreate method is executed, the component registers its own service into the Router, and other components can directly use this service. Uninstall: Uninstall is basically the same as loading, except that the onStop method of ApplicationLike is called, in which each component unregisters its service implementation from the Router. However, this usage scenario may be relatively rare and is generally suitable for some components that are only used once. Dimensionality reduction: Dimensionality reduction is used in rare scenarios. For example, if a component has a problem, we want to change the component from local implementation to a wap page. Dimensionality reduction generally requires background configuration to take effect. You can check the online configuration in onCreate. If dimensionality reduction is required, jump all UI to the configured wap page. A small detail is that the main project is responsible for loading components. Since the main project and components are isolated, how does the main project call the lifecycle method of the component ApplicationLike? Currently, we use a compile-time bytecode insertion method to scan all ApplicationLike classes (which have a common parent class), and then use javassisit to insert the code that calls ApplicationLike.onCreate in the onCreate of the main project. Let's optimize the componentized architecture diagram: Component lifecycle 2.6 Integrated debugging The fact that each component has been debugged individually does not mean that there are no problems when integrated together. Therefore, in the later stage of development, we need to integrate several components into one app for verification. Since the above mechanism ensures the isolation between components, we can arbitrarily select several components to participate in the integration. This on-demand loading mechanism can ensure great flexibility in integration debugging and can greatly speed up the compilation speed. Our approach is that after each component is developed, a relevant AAR is published to a public repository, usually a local Maven repository. Then the main project can configure the components to be integrated through parameters. So we slightly change the connection line between the component and the main project, and the final componentized architecture diagram is as follows: Final structure diagram 2.7 Code Isolation Now, looking back at the three questions we raised when we first split the components, we have found solutions, but there is still a hidden danger that has not been solved, that is, can we use compile project (xxx: reader.aar) to introduce components? Although we used the interface + implementation architecture in the data transmission chapter, components must be programmed for the interface, but once we introduce reader.aar, we can directly use the implementation class in it, so our specification for interface programming becomes a dead letter. A thousand-mile dam is destroyed by an ant hole. As long as there is code (whether intentional or unintentional) that does this, our previous work will be in vain. We hope to only import aar when assembleDebug or assembleRelease, and in the development stage, all components are invisible, so as to fundamentally eliminate the problem of referencing implementation classes. We hand this problem over to gradle to solve it. We create a gradle plug-in, and then apply this plug-in to each component. The configuration code of the plug-in is also relatively simple:
3. Component-based decomposition steps and dynamic requirements 3.1 Split Principles Component-based decomposition is a huge project, especially when it comes to decomposing a large project with hundreds of thousands of lines of code. There are many things to consider. For this reason, I think it can be divided into three steps: From product demand to development stage to operation stage, functions with clear boundaries began to be split, such as reading module, live broadcast module, etc. These began to be split out in batches During the split, the modules that cause the component to rely on the main project are further split out, such as the account system, etc. The final main project is a Host, which contains small functional modules (such as startup images) and the splicing logic between components. 3.2 Dynamic Requirements for Componentization At the beginning, we mentioned that the ideal code organization form is plug-in, which will have complete runtime dynamics. In the process of migrating to plug-in, we can achieve the following centralized method to improve compilation speed and dynamic update. For fast compilation, incremental compilation at the component level is used. Before extracting components, you can use code-level incremental compilation tools such as freeline (but databinding support is poor), fastdex, etc. In terms of dynamic updates, major functional improvements such as adding new components are not supported for the time being. You can temporarily use tools such as method-level hot fixes or function-level Tinker, but the access cost of Tinker is relatively high. IV. Conclusion This article is a summary of some ideas I have in the design of the componentization of "Get App". At the beginning of the design, I referred to the existing componentization and plug-in solutions. Standing on the shoulders of giants, I added some of my own ideas, mainly in terms of componentization life cycle and complete code isolation. Especially for the isolation of *** code, there must be not only normative constraints (for interface programming), but also mechanisms to ensure that developers do not make mistakes. I think only by doing this can it be considered a thorough componentization solution. |
<<: Android advanced obfuscation and code protection technology
>>: Code to handle iOS horizontal and vertical screen rotation
When you first enter operations , your leader ass...
Recently, the State Administration of Radio, Film...
Smartphone sales in the first quarter were dismal...
In the "Fortune World's Top 500 Companie...
There are only a handful of companies that know h...
Video account batch diversion, screen domination ...
Big news coming The most comprehensive APP promot...
As an account operator, I pray for the birth of a...
There has always been a saying circulating in the...
Boss, I want to buy this mobile phone. Is QQ inst...
The 2024 China Guizhou International Energy Indus...
How can we miss the icy cold desserts in the hot ...
[[146603]] Let's take a look at the 14 top de...
Recently, some netizens took photos of long white...
Course Contents: 1. Long Frame vs Short Frame.mp4...