For more mobile technology articles, please pay attention to this collection: Zhihu Mobile Platform column
background
Zhihu Android client used the most common single-project MVC architecture at the earliest. All the business logic was placed in the main project Module, and the network layer and some common codes were extracted into one Module respectively. Now it seems that the business lines, product functions and R&D team at that time are not as big and rich as they are now. Problems encountered can be solved by communication within the group at any time. Therefore, in the first few years of its steady development, Zhihu did not encounter any major problems.
Later, the company grew faster and split into several independent business units, each with its own Android development team, each with its own development, testing and deployment requirements. As the business grows, the problems caused by early code coupling become apparent; There are more and more developers, and the single-project architecture is becoming less and less able to collaborate with people. Considering the possibility of multiple applications in the future, we started a componentized reconstruction of the project. Today we will share some of our practices in componentization in this article.
Componentization practice
We use a multi-project multi-warehouse scheme, that is, each component has its own independent warehouse, which can be run independently of the main project; The main project relies on the components through the AAR and is gradually dismantled into a shell state without the business logic code. After more than a year of continuous iteration, it now looks like this:
It consists of four levels: Main project: Contains no business code except for some global configuration and the main Activity. Business components: The top level of the business, each representing a complete line of business, independent of each other. Base components: The underlying business services that support the operation of the upper-layer business components. Base SDK: Completely business-independent base code. Each level of responsibility is clear and independent, which can be easily disassembled and combined; Because all have their own version, the business line can be independently issued version, upgrade, roll back at any time.
Basic decoupling scheme
The first step of componentalization is to decouple the components to be removed. Decoupling methods are often described as follows:
(1) Common code processing: The basic business logic is separated into basic components, which are a group of codes with complete logic and are used to complete a specific function without business logic. The amount of independent SDK code is too small to be separated into separate codes and resources. We put them into a specially established Common component. And strictly limit the growth of common components. As componentization progresses, the common should become smaller rather than larger. Happen to be using some of the code and resources together fragments, they were usually only reuse because by developers to search and use directly, most of the time A resource has been A business declares the prefix, but since there is no isolation, still will inevitably be others to reuse in the business of B, if A business to make some changes at this time, B’s business will be affected — in this case we allow direct replication
(2) Initialization: Some components have the need to initialize services when the application starts, and many services have dependencies. Initially, we added an init() method for each component, but it did not solve the dependency order problem, so each component needs to add the initialization code in order in the APP project to run properly. This makes it difficult for people unfamiliar with the whole component business to build a component app that can run independently. Therefore, we developed a set of multithreaded initialization framework, each component just need to create several start Task classes, and declare dependencies in the Task:
This eliminates the problem of components piling up initialization code in the main project, simplifying the code and speeding up startup.
(3) Routing: Url is used to jump between interfaces, not only realizing decoupling, but also unifying the page opening mode of each end. We have implemented a set of flexible and small routing framework ZRouter, which supports multi-component, route interception, AB Test, parameter regular matching, degrade strategy, arbitrary parameter transfer and custom jump, etc. It can customize each stage of routing, fully meeting our business needs.
(4) Interface: In addition to the jump between pages, there will inevitably be some calls between different businesses. In order to avoid the direct communication of components, interface-dependent methods are usually used. We implemented an Interface Provider to support Interface communication, which dynamically registers an Interface at runtime, as well as ServiceLoader support. As long as one component exposes the communication interface, the consumer can make calls directly using the interface.
Dynamically registered interface
Provider. The register (AbcInterface. Class, new AbcInterfaceImpl ())Copy the code
Get the instance and call
Provider.get(AbcInterface.class).doSomething()Copy the code
(5) EventBus: Needless to say, abuse is a problem, but there are some scenarios where using events is the most convenient and simple way
(6) Component API modules: Where to put the interfaces and events mentioned above and some of the models used across components? If we were to sink these classes directly into a common component, which would be frequently updated and inconvenient to develop due to frequent business updates, it would not be feasible to use the common component, so we adopted another approach — component API: Add an API module for each component with external exposure requirements. The API module only contains interfaces and events for the exposed Model and component communication. Components that need to reference these classes simply rely on the API.
A typical component engineering structure looks like this:
- Template: component code, which contains all the business code for this component
- Template-api: The Interface module of a component that is used to communicate with other components and contains only models, interfaces, and events without any business or logical code
- App module: Used to run the app independently. It directly depends on the component module. As long as some simple configuration is added, the component can run independently
Semi-automatic component separation
With decoupling method, and the rest is to take action to break up the component, the component is a very headache problem, it is considered a man’s care and patience, because it could not accurately know what code to be dismantled, also cannot depend on intuitive knowledge, mobile becomes very difficult and error-prone, once cannot one-time split, everywhere is a compiler error, Can only rely on human flesh bit by bit to move. To do a good job, he must sharpen his tools. To solve this problem, we developed an auxiliary tool called RefactorMan: It can recursively parse out all source code references and references in the project, and will automatically analyze all unreasonable dependencies according to the preset rules. After the developer solves the unreasonable dependencies according to the prompts, the component can be moved out by one key, greatly reducing the workload of disassembly components. We have taken some detours in the initial stage of componentization. The original source code of eight component projects has to be moved repeatedly for several times before we get the optimal solution. With RefactorMan, we can face the repeated split and combination of components without fear: The ability to analyze and move resources gives you the added ability to clean up unwanted resources
Compile the complete package jointly
Running component app alone cannot completely cover all cases, especially in QA testing, we still need to compile the complete main project package, so we need a solution to directly compile the complete package: At first, our implementation method is only for components, which is relatively simple: First we dynamically introduce the component module in setting.gradle:
def allComponents = ["base", "account" ... "template" ...] ForEach ({name -> if (shouldUseSource(name)) {include ":${name}" project(":${name}").projectDir = getComponentDir(name); }})Copy the code
Gradle/app/build.gradle and exclude all indirectly dependent components to prevent modules and AArs that depend on one component at the same time:
allComponents.forEach({ name ->
if (shouldUseSource(name)) {
implementation(project(":${name}")) { exclude group: COMPONENT_GROUP }
} else {
implementation("${COMPONENT_GROUP}:${name}:${versions[name]}") { exclude group: COMPONENT_GROUP }
}
})Copy the code
Since the groups of all components are the same, this is not a problem, but later on some of the basic SDKS also required a common source dependency scheme, so we changed it to use gradle dependency substitution directly. Setting. Gradle:
/ /... Ignore reading configuration code... configs.forEach { artifact, prj -> include ":${prj.name}" project(":${prj.name}").projectDir = new File(prj.dir) } gradle.allprojects { project -> if (project == project.rootProject) { return } project.configurations.all { resolutionStrategy.dependencySubstitution { Substitute module(artifact) with project(":${prj.name}")}}}}Copy the code
Build. gradle dependencies are written exactly like normal projects. General state of the main project:
Source code after the template component after the main project:
Small tip: Project. Idea/Vc.xml defines the Git repository associated with the current project. You can modify vC.xml to associate the component directory with the Git configuration of the main project at the same time of co-compilation.
Components that contain sublines of business
Most of our current component, a component of a warehouse, for general component, there is no problem, but for some lines of business, the relatively large size itself, contains several child business, such as university of zhihu, e-books, live DengZi business and private class, these independent child business itself a function, But the underlying code for the entire line of business is shared, and the large line of business also has pages that summarize all the sub-lines, and their relationship looks like this:
If these businesses are to be separated into independent components, and then separated from the common part to become a basic component of the business line, there will be a big problem: As several lines of business belong to the same main line of business, the linkage of these components often occurs when activities or new features are created. Base needs to be updated first and then other lines of business need to be updated. When submitting Mr, multiple warehouses need to be mentioned at the same time, resulting in frequent chain updates. Without disassembly, the code of the line of business itself is already huge, even if the app is compiled separately, it will be slow. Moreover, over time, the code boundary of each line of business will gradually deteriorate like the main project before componentization, and the coupling will become more and more serious.
So now the requirement looks like this: External retention has only one component: When there is a linkage requirement, the components are still only updated once and the sub-businesses are still independent and isolated from each other, they can run independently and we tried to use sourceSets to put different business code in different folders, but the problem with sourceSets is, It does not restrict sourceSets from referring to each other. The Base module can even refer directly to the top layer of code. Although this can be checked at compile time, there is always a sense of hindsight, and using sourceSets to make each module run on its own can be troublesome to configure. The Android Studio Module naturally has the advantage of isolation. So our solution is to use multi-module structures in component projects:
The sublines of business are split into different modules in the same project: they depend on the base together, and the lines of business are independent of each other, and these sublines of business are grouped together in a main Module, as shown in the picture above
Gradlew :main:uploadArchives./gradlew :main:uploadArchives can only be used to publish the main Module code. Other modules cannot be published. So we need to merge all the code into main when we publish it. The only way to do this is to add sourceSet, and once you use sourceSet, the code is no longer isolated. So we used a dynamic strategy of using sourceSet dependencies at compile time and Module dependencies at other times to have the best of both worlds.
In other words: on the surface, this looks like a normal multi-module project, but in reality, their relationship is dynamic: seven kids write code, and they compile it as a monster:
Assemble, install, upload, and project dependencies are only used for assemble, install, and upload. Example code:
boolean useSource = gradle.startParameter.taskNames.any { it.contains("assemble") || it.contains("install") || it.contains("upload")) } subProject.forEach { subProject -> if (useSource) { android.sourceSets.main { java.srcDirs += file(".. /${subProject}/src/main/java") res.srcDirs += file(".. /${subProject}/src/main/res") } } else { dependencies { implementation project(":$subProject") } } }Copy the code
Other resources such as Resources, Assets, AIdl, RenderScript, JNI, jniLibs, Shaders, and AAR and JAR files are multi-files and can be added using a similar approach to the above.
But the manifest is different. There is only one Androidmanifest.xml in a module, so there needs to be a way to merge the sub-business manifests. We implement the manifest merger using the ManifestMerger provided by the official ManifestMerger. The specific code of the merger is not expanded here. Interested students can read the source code for themselves.
Enclosing a using method, the main module can refer to the child module as follows:
dependencies {
using "base"
using "sub1"
using "sub2"
using 'sub3'
using 'sub4'
}Copy the code
Because each sub-business component is independent, it can still be configured, compiled and run independently, and the amount of code for each business is greatly reduced relative to the entire line of business, resulting in faster compilation times.
conclusion
In the last two years, many companies have started the componentization of App. The basic ideas of componentization are the same, but there is no universal solution. In the process of componentization, each company will adjust the solution according to its own situation, and the one suitable for its own development is the best. Some problems that seem insignificant at the initial stage of componentation may gradually appear in the later stage, so it is necessary to adjust the plan in time. Componentization of Zhihu is also gradually improved in constant changes, and it will definitely be continuously optimized with changes of business and code in the future. This will be a continuous process, and we will continue to share some problems and solutions encountered in componentization in the future. The above is a part of our practice in the process of componentization. Due to my limited level, if there are mistakes and omissions, please correct them. In addition, zhihu mobile platform team is also recruiting, welcome to join us and do some cool things together! The specific recruitment information app.mokahr.com/apply/zhihu here…
About the author
Pan Zhihui, who joined Zhihu in 2016, is now the head of The Android infrastructure team of Zhihu. He has rich experience in Android engineering and componentization, and has designed and led the Android componentization division of Zhihu.