background
As an important branch of Android client technology, componentization has been actively explored and practiced by the industry in recent years. The various Android development teams within Meituan are also trying and practicing different componentization solutions, and there are a lot of high-quality outputs on the componentized communication framework. Recently, our team made a component-based transformation of two Android apps, Meituan Retail Cashier and Meituan Light Cashier. This paper mainly introduces our componentization scheme, hoping to inspire students engaged in Android componentization development.
Why componentize
Why do so many teams have componentized practices in recent years? What benefits does componentization bring to our projects and code? We believe that componentization offers two major benefits:
Improve component reusability
Some people might think that improving reusability is simply a matter of making the code that needs to be reused into an Android Module, packaging the AAR and uploading the code repository so that these features can be easily introduced and used. However, we think that this is not enough. Whether the AAR library of the upload warehouse can be easily reused needs to be constrained by component-based rules, so as to improve the convenience of reuse.
Reduce coupling between components
We need to use componentized rules to break the code into modules with high cohesion and low coupling. Modules cannot be called directly between modules, which requires the support of the componentized communication framework. Reduced coupling between components has two immediate benefits: first, the code is more maintainable; Second, it reduces the Bug rate of modules.
The state before componentization
Our goal is to carry out component-based reconstruction of the team’s two apps (Meituan Retail Cashier and Meituan Light Cashier), so here is a brief introduction to the architecture of these two apps. Generally speaking, the architecture of the two applications is similar. The main engineering Module relies on the Business Module, which is a collection of various Business functions, and the Business Module relies on the Service Module. Service Module depends on Platform Module. Both Service Module and Platform Module provide services to the upper layer. The difference is that Platform Module provides more basic services. It mainly includes some Utils and interface widgets, while the Service Module provides various functional services, such as KNB, location Service, network interface invocation, etc. In this case, the Business Module becomes bloated and complicated. Various Business modules call each other with strong coupling, and it is easy to change the Business code at the same time. Even if you change a small piece of Business code, you may have to modify many related places, which is not conducive to maintenance at the code level. And changes to one business can easily cause bugs in other businesses.
Componentization scheme research
In order to find the most suitable componentization solution for our business format and architecture, we investigated some open source componentization solutions in the industry and other teams within the company, and concluded here.
Open source componentization scheme research
We investigated some of the leading open source componentization solutions in the industry.
- CC
Known as the industry’s first support for incremental componentization of Android componentization open source framework. Regardless of page jump or call between components, CC unified component call method is used to complete.
- DDComponentForAndroid
The resulting scheme adopts the route + interface sinking method, all interfaces are sunk into the base, the interface is implemented in the component and the code is added in IApplicationLike to register the Router.
- ModularizationArchitecture
Component calls need to specify synchronous or asynchronous implementation. When calling components, we get RouterResponse as the return value. When calling components, we use RouterResponse.getData() to get the result.
- ARouter
The routing engine launched by Ali is a routing framework, not a complete componentized solution, but a communication engine for componentized architecture.
- Gather the Router
The routing engine of Jumei also has the componentization practice scheme of Jumei on this basis. The basic idea is to realize componentization by the way of routing + interface sinking.
Research the componentization scheme of other teams in Meituan
- Meituan cash register ComponentCenter
The componentized scheme of Meituan cashier supports interface call and message bus. The way of interface call needs to build CCPData, then call ComponentCenter.call, and finally process in the unified Callback. Message bus way also need to build CCPData, last call ComponentCenter. Adds. The business components of Meituan Cashier are all packaged as AArs and uploaded to the warehouse. The components are interdependent, so MainApp needs to carefully exclude some repeated dependencies when referencing these components. In our componentization scheme, we take an ingenious approach to this problem.
- Meituan App ServiceLoader
The componentization scheme of Meituan App adopts the form of ServiceLoader, which is a typical way of interface calling component communication. Define a service with an annotation, get a List of interfaces when getting the service, determine whether the List is empty, if not, get one of the interface calls.
- WMRouter
An Android routing framework developed by Meituan Takeout team is based on componentized design ideas. It provides routing and ServiceLoader functions. WMRouter: The Android Open Source Routing Framework for Meituan Takeout. WMRouter provides two infrastructure frameworks for componentization: routing and component indirect interface invocation. The support and documentation are also sufficient to consider as the infrastructure for componentization for our team.
Componentization scheme
Componentized infrastructure
During the initial research work, we found that WMRouter of the delivery team was a good choice. First, WMRouter provides communication between routing and ServiceLoader components. Second, WMRouter has a clean architecture, scalability, and documentation and support. So we decided to use WMRouter as one of the componentized infrastructure frameworks. However, there are two problems with using WMRouter directly:
- Our project is already using a routing framework. If we use WMRouter, we need to change the routing framework we used before to WMRouter.
- WMRouter did not have a message bus framework, nor did any of the other projects we investigated have a message bus framework suitable for our project, so we needed to develop a message bus framework that could meet our needs, as described in more detail in this section below.
Componentized hierarchical structure
After referring to different componentization schemes, we adopted the following hierarchical structure:
- App shell project: Responsible for managing various business components and packaging APK, without specific business functions.
- Business Component layer: Independent business components are formed based on different businesses, each of which contains an Export Module and an Implement Module.
- Function component layer: Provides basic function services for the upper layer, such as login, print, and log services.
- Component infrastructure: Includes WMRouter, which provides page routing services and ServiceLoader interface invocation services, as well as the component message bus framework, modular- Event, described below.
The overall architecture is shown in the figure below:
Business Component separation
When we investigated other componentization solutions, we found that many component solutions split a business Module into an independent business component, i.e., a separate Module. In our scenario, each business component is split into an Export Module and an Implement Module. Why do this?
- Avoid circular dependencies
With one Module per business component, If Module A needs to call the interface provided by Module B, Then Module A needs to rely on Module. Also, if Module B needs to call Module A’s interface, then Module B needs to rely on Module A. This creates a circular dependency, which is not allowed.
Some readers may say that this is easy to solve: You can put the interfaces that Module A and Module B depend on in another Module, and then have both Module A and Module B depend on this Module. This is indeed a solution, and some project teams are using this approach of sinking the interface.
But we want a component’s interface to be provided by the component itself, rather than in a more submerged interface, so we split each business component into an Export Module and an Implement Module. In this case, if Module A needs to call the interface provided by Module B, and Module B needs to call the interface of Module A, Module A needs to rely on Module B Export, and Module B needs to rely on Module A Export.
- Business components are completely equal
In a componentized scenario that uses a single Module scenario, the business components are not exactly equal, and some of the dependent components are lower down the hierarchy. However, with the Export Module+Implement Module solution, all business components are completely equal at the hierarchy.
- The functional division is clearer
Each business component is partitioned into an Export Module+Implement Module pattern, which makes the functional partitioning of each Module clearer. Export Module mainly defines the components to be exposed, including:
- Exposed interfaces that are called by WMRouter’s ServiceLoader.
- Exposed events that are subscribed to and distributed using the message bus framework Modular – Event.
- The Router Path of the component, although the project before componentization also used the Router framework, but all Router paths are defined in a public Class of sinking modules. The problem is that any module that adds/removes pages, or changes routes, needs to modify the public Class. Imagine if, after componentization, a component added a page and had to add a route to an external Java file, which would be unacceptable and inconsistent with the goal of componentization cohesion. Therefore, we put the Router Path of each component in the Export Module of the component, so that it can be exposed to other components and each component can manage its own Router Path without the dilemma of all components modifying a Java file.
An Implement Module is the part of a component implementation that contains:
- Page-related activities, fragments, and routes defined with WMRouter’s annotations.
- Implementation of exposed interfaces in Export Module.
- Other business logic.
Modular message bus framework Modular – Event
In the componentized infrastructure framework mentioned earlier, we used the takeaway team’s WMRouter to implement page routing and component indirection calls, but there was no message bus infrastructure, so we developed our own componentized message bus framework, Modular -Event.
Why do you need a message bus framework
Previously, we developed a LiveData-based message bus framework, LiveDataBus, and published an article on the Meituantech blog about the framework: Evolution of the Android Message Bus: Replacing RxBus and EventBus with LiveDataBus. There is always a lot of debate about the use of message buses. Some people find the message bus useful, others find it easy to abuse.
Why do you need a message bus approach when you already have a framework for component indirect invocation by ServiceLoader? There are two main reasons:
- Further decoupling
The ServiceLoader framework based on interface invocation does achieve decoupling, but message buses can achieve more complete decoupling. The way an interface is invoked the caller needs to depend on the interface and know which component implements the interface. The message-bus sender only needs to send a message and doesn’t care if someone subscribes to the message, so the sender doesn’t need to know anything about other components and has less coupling to them.
- Many-to-many communication
The interface-based approach can only make one-to-one calls, while the message bus-based approach can provide many-to-many communication.
Advantages and disadvantages of message bus
In general, the greatest advantage of a message bus is decoupling, so it is well suited to componentization scenarios that require complete decoupling between components. However, the main reason why message buses are criticized by many people is that message buses can be easily abused. The message bus is prone to abuse in several scenarios:
- Messages are hard to trace
Sometimes in the process of reading code, we find a place to subscribe to a message and want to see who sent the message, so we can only “trace back” by looking up the message. As a result, the process of reading the code and sorting out the logic is not coherent, and there is a sense of fragmentation.
- Messages are sent randomly without any constraint
Message buses generally have no constraints on sending messages. Neither EventBus, RxBus, nor LiveDataBus checks the message when it is sent, nor does it constrain the send call. This irregularity can even have disastrous consequences at a given moment. For example, if a subscriber subscribed to a message called login_SUCCESS, the sending message was written by a casual programmer. Instead of defining the message as a global variable, a temporary String variable was defined to send the message. Unfortunately, he spelled the message name login_success instead of login_seccess. In this case, the subscriber will never receive a successful login message, and the error will be difficult to detect.
Design goals for the componentized message bus
- Messages are defined by the component itself
When we used the message bus, we liked to define all messages in a common Java file. Componentization, however, would modify the Java file whenever a component’s message changes. So we want the component itself to define and maintain the message definition file.
- Messages with the same name that distinguish different component definitions
If messages are defined and maintained by components, it is possible that different components define messages with the same name, and the message bus framework needs to be able to distinguish such messages.
- Address the message bus shortcomings mentioned earlier
Solve the problem of message bus message tracing and message sending without constraint.
Message bus based on LiveData
A previous blog post, Evolution of the Android Message Bus: Replacing RxBus and EventBus with LiveDataBus, explained in detail how to build a message bus based on LiveData. The componentized message bus framework Modular – Event is also built on LiveData. Building a message bus using LiveData has many advantages:
- Building a message bus with LiveData provides lifecycle awareness, eliminates the need for users to invoke de-registration, is more convenient than using EventBus and RxBus, and has no risk of memory leaks.
- Using the normal message bus, if the Activity is stopped during a callback, an action such as a pop-up Dialog will crash. Building a message bus with LiveData has no such risk.
Implementation of the component message bus Modulan-Event
Resolve a problem where different components define a message with the same name
In fact, this problem is relatively easy to solve, the implementation of the way is to use a two-level HashMap to solve. The first-level HashMap is built with ModuleName as the Key and the second-level HashMap as the Value. The second-level HashMap takes the message name EventName as the Key and LiveData as the Value. The component name ModuleName is used in the first-level HashMap, and EventName is used in the second-level HashName. The whole structure is shown in the figure below:
Constraints on the message bus
We expect the message bus framework to have the following constraints:
- You can only subscribe to and send messages that are predefined in the component. In other words, consumers cannot send and subscribe to temporary messages.
- The type of message needs to be specified at definition time.
- You need to specify which component belongs to when defining a message.
How are these constraints implemented
- Use annotations on the message definition file to define the type of message and the Module to which the message belongs.
- Define annotation handlers that collect information about messages at compile time.
- The interface is used to constrain message sending and subscription when the compiler generates a call based on the message’s information.
- The runtime builds a LiveData storage structure based on a two-level HashMap.
- The runtime uses interface+ dynamic proxy to implement real message subscription and delivery.
The whole process is shown in the figure below:
The structure of the message bus modular- Event
- Modular -event-base: defines Anotation and other basic types
- Modular -event-core: modular-event core implementation
- Modulan-event-compiler: Annotation processor
- Modular – event – the plugin: Gradle plugin
Anotation
- @moduleEvents: Message definition
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface ModuleEvents {
String module(a) default "";
}
Copy the code
- @eventType: Message type
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface EventType {
Class value(a);
}
Copy the code
The message definition
Annotate a Java class that defines a message with @ModuleEvents. If @ModuleEvents specifies a property module, the value of that module is the module to which the message belongs. If no property module is specified, The package name of the package in which the Java class defines the message is used as the Module to which the message belongs.
All messages defined in this message definition Java class are of type public Static Final String. @EventType supports Java native or custom types. If the message type is not specified by @EventType, the default message type is Object. Here is an example of a message definition:
// You can specify the module. Otherwise, use the package name as the module name
@ModuleEvents(a)public class DemoEvents {
// If no message type is specified, the default message type is Object
public static final String EVENT1 = "event1";
// Specify the message type as a custom Bean
@EventType(TestEventBean.class)
public static final String EVENT2 = "event2";
// Specify the message type as Java native
@EventType(String.class)
public static final String EVENT3 = "event3";
}
Copy the code
Automatic interface generation
We handle these annotations in modular -event-Compiler. A Java class that defines a message generates an interface named EventsDefineOf+ The name of the message definition class, such as DemoEvents, The automatically generated interface is EventsDefineOfDemoEvents. Each message defined in the message definition class is converted to a method in the interface. Consumers can use the message bus only through these automatically generated interfaces. We implemented the constraints on the message bus in this clever way. The message definition example demoEvents.java generates an interface class that looks like this:
package com.sankuai.erp.modularevent.generated.com.meituan.jeremy.module_b_export;
public interface EventsDefineOfDemoEvents extends com.sankuai.erp.modularevent.base.IEventsDefine {
com.sankuai.erp.modularevent.Observable<java.lang.Object> EVENT1();
com.sankuai.erp.modularevent.Observable<com.meituan.jeremy.module_b_export.TestEventBean> EVENT2(
);
com.sankuai.erp.modularevent.Observable<java.lang.String> EVENT3();
}
Copy the code
As for the automatic generation of interface class, we use Square/Javapoet to achieve, there are many articles about Javapoet on the Internet, here will not be too tired to describe.
Use dynamic proxies for runtime invocation
With an interface automatically generated, it is equivalent to having a shell. However, all the logic below the shell is realized through dynamic proxy. Briefly introduce the proxy mode and dynamic proxy:
- Proxy mode: an object is given a proxy object and the proxy object controls access to the original object. That is, the client does not directly control the original object, but indirectly controls the original object through the proxy object.
- Dynamic proxy: Proxy classes are generated at run time. That is, there is no actual class file after Java is compiled, but rather a class bytecode that is dynamically generated at run time and loaded into the JVM.
Implement the lookup logic in the dynamic proxy’s InvocationHandler:
- ModuleName is obtained based on the typename of the interface.
- The methodName of the called method is the message name.
- Find the corresponding LiveData based on ModuleName and message name.
- Complete the process of subscribing to subsequent messages or sending messages.
The subscription and delivery of messages can be encoded as chain calls:
- Subscribe to news
ModularEventBus
.get()
.of(EventsDefineOfModuleBEvents.class)
.EVENT1()
.observe(this.new Observer<TestEventBean>() {
@Override
public void onChanged(@Nullable TestEventBean testEventBean) {
Toast.makeText(MainActivity.this."MainActivity receives custom message:"+ testEventBean.getMsg(), Toast.LENGTH_SHORT).show(); }});Copy the code
- Send a message
ModularEventBus
.get()
.of(EventsDefineOfModuleBEvents.class)
.EVENT1()
.setValue(new TestEventBean("aa"));
Copy the code
Subscribe and send patterns
- Patterns for subscribing to messages
- Observe: Lifecycle aware, automatically unsubscribe onDestroy.
- ObserveSticky: Lifecycle aware, automatically unsubscribe when onDestroy, Sticky mode.
- ObserveForever: Manual unsubscription is required.
- ObserveStickyForever: Need to manually unsubscribe, Sticky mode.
- Mode for sending messages
- SetValue: main thread call.
- PostValue: background thread call.
Componentized summary
This paper introduces the componentized practice of Android team of Meituanindustry cashier r&d group, as well as the principle and use of the industry’s first strongly constrained component message bus Modular Event. Our team has been exploring component-based transformation for a long time, and we encountered many difficulties in the initial implementation of some schemes. We also studied many open source componentization schemes, as well as those of other teams within the company (Meituan App, Meituan takeaway, Meituan cashier, etc.), learnt and borrowed many excellent design ideas, and of course stepped on many holes. We have come to realize that every componentization solution has its own application scenarios, and that our componentization architecture choices should be more business-oriented, not just technology-oriented.
Future work prospect
Our componentized transformation work is far from finished, and we may continue in-depth research in the following directions in the future:
- Component management: After component transformation, each component is an independent project, and components will be developed iteratively. How to version management of these components?
- Component reuse: Now it seems easy to reuse these components simply by importing their libraries, but if a new project comes along and the requirements change, how can we maximize the reuse of these components?
- CI Integration: How to better integrate with CI.
- Integration to scaffolding: Integration to scaffolding allows new projects to be developed in a componentized mode from the start.
The resources
- Evolution of Android message Bus: Replace RxBus and EventBus with LiveDataBus
- WMRouter: Meituan Takeaway Android open source routing framework
- Evolution practice of Meituan Waimai’s Android platform architecture
Author’s brief introduction
Hai Liang, a senior engineer of Meituan, joined Meituan in 2017. Currently, he is mainly responsible for the business and module development of Meituan Light Cashier, Meituan Retail cashier and other apps.
recruitment
Meituan catering ecology is looking for senior/senior engineers and technical experts on Android, based in Beijing and Chengdu. Interested students are welcome to send their resumes to [email protected].