About componentization
As the project structure becomes larger and larger, the boundary between modules gradually becomes unclear, the code maintenance becomes more and more difficult, and even the compilation speed becomes the bottleneck affecting the development efficiency.
Component-based splitting is a common solution. On the one hand, it solves the coupling relationship between modules and sinks common modules. On the other hand, it isolates the code and resource files of each module, so that modules can be compiled on demand and tested separately.
However, the following problems are becoming more and more prominent. The refinement and separation of modules inevitably increases the communication cost between modules. On both sides of communication is a C/S architecture. If the server and client belong to the same process, we call it local service. If they belong to different processes, we call it remote service. Note that a Service is not limited to the Android Service component, but is a capability that can provide functionality or data externally.
If you’re already in ARouter, declare that the service class inherits the IProvider+Router annotation to register the service.
However, cross-process communication is more complicated. In Android, IPC communication is implemented with Binder, which limits the data format involved in communication, namely the basic data type or the type of Parcelable interface.
The advantage of multi-process is that it can take up more system resources, and the independent core process can prevent the whole APP from crashing and becoming unavailable due to the exception of non-core business.
The cross-process communication Service scenario is complex. It needs to ensure the reliability of the server and support callback, and Service is usually the first choice.
IPC communication based on Service
Let’s recall how services are used for cross-process communication.
- Declare the AIDL interface that provides the service.
- Create the Service and return the Binder object that implements the Stub interface in the onBind method.
- The Client uses an Intent bindService and passes in a ServiceConnection object. The Binder object is called at the onServiceConnected callback.
Essentially, Binder objects (or rather proxy objects) are passed between processes, with the Service as a carrier.
In the context of componentized business, the number of communication interfaces between modules may be very large, so there will be many problems according to this scheme.
- You need to write the AIDL file and the Service class.
- BindService is an asynchronous operation that requires a write callback and is not consistent with local service invocation.
- There is no unified Binder manager, how to deal with Binder Die, how to implement Binder cache, etc.
In this way, we can sum up the characteristics of a good componentized communication framework or the demands to be achieved.
The core appeal of componentized cross-process communication
- Is it possible to declare a remote service interface in the same way as a normal interface class without writing an AIDL file? Could you not write Service, because the essence of IPC communication is simply passing binders?
- We want to invoke a remote service as if it were a local service, avoiding callback hell, where the retrieval of a remote service is a blocking call.
- How to manage remote services provided by various processes to ensure high availability.
Andromeda is our main topic today. I hope you can read it patiently.
Andromeda
Andromeda is an open source componentized IPC communication solution from IQiyi that addresses problems 2 and 3 above without the need to write a Service, but still requires an AIDL file.
The earlier open source Hermes framework does this by replacing aiDL-generated static proxies with dynamic proxies + reflection, but does not support oneway, in, out, inout and other modifiers.
Later, IQiyi also open source InterStellar, realizing that there is no need to write AIDL files. When using cross-process interface, it declares @oneway/ @IN annotations to complete the addition of IPC modifiers. This is a complete implementation of remote calls as simple as local calls. But somehow Andromeda wasn’t incorporated into a single project, and the engineering code went unmaintained for a long time.
Andromeda also has some features:
- Added an EventBus for cross-process communication, called cross-process EventBus.
- In order to enhance the stability of the process, the priority of the background Service process is finally improved by pre-inserting the staked Service for each process and binding the staked Service with the UI component (Activity/Fragment/View) when obtaining remote Service.
- Support IPCCallback.
- Support for configuring processes that Binder Dispatcher belongs to.
Andromeda making address
Let’s look at simple uses first
// The first argument to register the local service is the interface class that will be used as the key, and the second argument is the interface implementation class. Andromeda.registerLocalService(ICheckApple.class, new CheckApple()); / / using the local service ICheckApple checkApple = Andromeda. GetLocalService (ICheckApple. Class); -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- / / registered remote service The second parameter for IBinder type, Will Andromeda transmission between the process in the future. RegisterRemoteService (IBuyApple. Class, BuyAppleImpl getInstance ()); Andromeda.with(this).getremoteservice (ibuyapple.class); andromeda.with (this).getremoteservice (ibuyapple.class);Copy the code
The overall API design is clear and all are completed synchronously. See the project example for detailed use. The focus of this paper is to analyze the internal principle.
Although it is source code analysis, BUT I am not ready to post too much source code, so the reading experience is not good; I will try to restrain, really have a need to small partners please check the source code, my goal is to explain the core idea clearly.
The architecture analysis
To clarify a few concepts, both the event bus and service distribution require a hub for transit storage, which is called the Dispatcher in the Andromeda framework.
Dispatcher
It is an AIDL interface. Each process needs to first get DispatcherProxy when registering service, and then transmit the process service Binder to DispatcherProxy for storage. When other processes need to use this service, they also need to obtain a DispatcherProxy first. It then reads the cache Binder in DispatcherProxy and stores a cache in its own process so that the process does not need to make IPC calls the next time it retrives the same service.
Let’s take a look at what the Dispatcher provides.
# IDispatcher.aidlBinderBean getTargetBinder(String serviceCanonicalName); BinderBean getTargetBinder(String serviceCanonicalName); IBinder fetchTargetBinder(String URI); Void registerRemoteTransfer(int PID,IBinder remoteTransferBinder); / / register/registered the remote service void registerRemoteService (String serviceCanonicalName, String processName, IBinder Binder); void unregisterRemoteService(String serviceCanonicalName); Void publish(in Event event);
}
Copy the code
The Dispatcher process can be either the main process or a user – defined process. Once the Center, as the core of componentized communication, is disabled, the previous registration service will be unavailable. Therefore, it needs to be placed in the process with the longest application life cycle. Usually, this process is the main process, but for music player-related apps, it may be an independent player process. So the framework provides us with a configuration item that explicitly declares the process where the Dispatcher is located.
Build. Gradle adds a declaration to the main project
dispatcher{
process ":downloader"
}
Copy the code
The Dispatcher architecture diagram
RemoteTransfer
As mentioned above, each process itself needs to manage (cache) binders acquired from the Dispatcher to prevent repeated IPC requests; In addition, due to the requirements of the event bus, each process needs to register its process component manager with the Dispatcher process, so that Dispatcher can send the event to each process after the event pubish, and the process manager is RemoteTransfer.
IRemoteTransfer is an AIDL interface, and RemoteTransfer is its implementation class. RemoteTransfer also implements IRemoteServiceTransfer interface.
Here’s a class diagram to help you figure things out:
#IRemoteTransfer.aidlVoid registerDispatcher(IBinder dispatcherBinder); oneway void unregisterRemoteService(String serviceCanonicalName); oneway void notify(in Event event);
}
#IRemoteServiceTransfer.javaBinderBean getRemoteServiceBean(String serviceCanonicalName); Void registerStubService(String serviceCanonicalName, IBinder stubBinder); void unregisterStubService(String serviceCanonicalName); }Copy the code
Two issues need to be noted:
The caller of the Dispatcher method is in the Dispatcher, so the remote proxy of the Dispatcher is passed back to the current process, and then the remote service registration can be done through the DispatcherProxy.
BinderBean BinderBean BinderBean BinderBean BinderBean BinderBean BinderBean BinderBean BinderBean BinderBean BinderBean
Now that the body logic is clear, let’s start analyzing the functionality.
- The Dispatcher proxy is obtained synchronously through the ContentProvider, which belongs to the Dispatcher process and is threaded into the manifeset file.
- When getting the remote service, pass the Activity or Fragment of the current process and bind the StubService, which belongs to the process of the remote service.
This is the core principle of the Entire Andromeda project. If you don’t understand it, it doesn’t matter. Below we will analyze the implementation process in detail with the combination of sequence diagrams and diagrams.
The local service
The local service has nothing to say. Internally, it maintains a Map relational table that records the names and implementation classes of the registered service.
# LocalServiceHub
public class LocalServiceHub implements ILocalServiceHub {
private Map<String, Object> serviceMap = new ConcurrentHashMap<>();
@Override
public Object getLocalService(String module) {
returnserviceMap.get(module); } @Override public void registerService(String module, Object serviceImpl) { serviceMap.put(module, serviceImpl); } @Override public void unregisterService(String module) { serviceMap.remove(module); }}Copy the code
The remote service
Remote service is the core of the framework, the operation of remote service is two, one is to register remote service, the other is to obtain remote service.
Let’s first look at the registration of the service. The sequence diagram is as follows ↓
- Client pass
<T extends IBinder> registerRemoteService(String serviceCanonicalName, T stubBinder)
Register the remote services that this process can provide. StubBinder is the service implementation class. - Call RemoteTransfer’s registerStubService method.
- RegisterStubService initializes DispatcherProxy internally. If it is empty, go to 3.1.
- 3.1-3.2 To achieve synchronous registration of services, it is essentially synchronous acquisition of DispatcherProxy, which is an IPC communication. Andromeda’s solution is to plug a ContentProvider into the Dispatcher process. Then a Cursor containing the DispatcherProxy is returned to the client process, and the client parses the Cursor to get the DispatcherProxy.
- RemoteTransfer asks RemoteServiceTransfer to help complete the actual registration.
- RemoteServiceTransfer makes an IPC communication with the Binder to the Dispatcher process via the DispatcherProxy obtained in Step 3.
- The Dispatcher process asks the ServiceDispatcher class to register the service, which stores the Binder in a Map.
The blue nodes represent the current process that registers the service, namely the Server process, and the red nodes represent the Dispatcher process.
The whole process focuses on the third step, let’s focus on the analysis:
# RemoteTransfer
private void initDispatchProxyLocked() {
if(null == dispatcherProxy) {bind IBinder dispatcherBinder = getIBinderFromProvider();if(null ! After = dispatcherBinder) {/ / remove asInterface create remote agent object dispatcherProxy = IDispatcher. Stub. AsInterface (dispatcherBinder); registerCurrentTransfer(); }}... } private voidregisterCurrentTransfer() {/ / registered himself the Process with the Dispatcher RemoteTransfer Binder dispatcherProxy. RegisterRemoteTransfer (android. OS. Process. MyPid (), this.asBinder()); . } private IBindergetIBinderFromProvider() { Cursor cursor = null; Through contentprovider try {/ / get cursor cursor = context. GetContentResolver () query (getDispatcherProviderUri (), DispatcherProvider.PROJECTION_MAIN, null, null, null);if (cursor == null) {
return null;
}
returnDispatcherCursor.stripBinder(cursor); } finally { IOUtils.closeQuietly(cursor); }}Copy the code
Let’s look at the DispatcherProvider
public class DispatcherProvider extends ContentProvider { ... @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {// Encapsulate Binder to CURSOR returnsreturnDispatcherCursor.generateCursor(Dispatcher.getInstance().asBinder()); }}Copy the code
Next, we will look at the service acquisition. Similarly, we will first look at the sequence diagram ↓
1. Andromeda portal Access remote services through getRemoteService.
2-4. Related to the priority of the promotion process, we will not discuss it for the time being.
5. Request RemoteTransfer for the wrapper bean of the remote service.
6-7. RemoteTransfer request RemoteServiceTransfer helps to find the target Binder in the cache of the process first, if found directly return.
7.2. Call getAndSaveIBinder if the cache is not hit. Binder is cached in steps 6-7, as the method name indicates.
8. RemoteServiceTransfer initiates IPC communication through DispatcherProxy and requests remote service Binder.
9-10. Dispatcher asks ServiceDispatcher to help find the service registry in the process.
11. Go back to client process and cache Binder.
Return Binder to the caller.
Similarly, the blue nodes represent the process that obtains the service, namely the Client process, and the red nodes represent the Dispatcher process.
At this point, the analysis of the remote service registration and acquisition process is complete.
Process priority
As mentioned above, the framework does something to increase the priority of the process when retrieving remote services. Usually, the remote Server (Client) is in the foreground process, while the Server process is in the background after registration. To improve the stability of the Server, ensure that the priorities of processes on the Server are close to those of clients; otherwise, processes on the Server may be reclaimed by LMK (Low Memory Killer).
So how do you prioritize server-side processes? The approach here is to bind a pre-staked Service on the Server side with the UI component (Activity/Fragment/View) in the foreground.
The entire process is finally implemented using AMS’s updateOomAdjLocked method.
Returning to the Andromeda implementation, the pre-poked Service looks like this:
public class CommuStubService extends Service {
public CommuStubService() {}
@Override
public IBinder onBind(Intent intent) {
return new ICommuStub.Stub() {
@Override
public void commu(Bundle args) throws RemoteException {
//donothing now } }; } @override public int onStartCommand(Intent Intent, int flags, int startId) {// Override public int onStartCommand(Intent Intent, int flags, int startId)return Service.START_STICKY;
}
public static class CommuStubService0 extends CommuStubService {}
public static class CommuStubService1 extends CommuStubService {}
public static class CommuStubService2 extends CommuStubService {}
...
public static class CommuStubService14 extends CommuStubService {}
}
Copy the code
You can see that the framework presets 15 services for use by processes, that is, a maximum of 15 processes, which is sufficient in most scenarios. In addition, a mapping table of process and Service names is maintained, otherwise how to know which Service should be bind, this mapping is also done at compile time.
The bind process for this service occurs when the remote service is fetched in the previous section, as shown below:
The module in the figure is divided into three parts according to the process:
- Blue indicates that the Client process initiates a remote service request.
- The light grey indicates the Server process that pre-registers the service with the Dispatcher.
- Purple represents the Dispatcher process, which internally caches Binder objects for each process’s services.
We focus on the ConnectionManager part of the blue module. In fact, when Client requests remote service to Dispatcher, it will immediately bind StubService of the stubbed process of the remote service through ConnectionManager. This increases the priority of the process in which the Server resides.
Now that bind is done, when do YOU unbind? Obviously when the UI component is destroyed, since it is no longer in the foreground, the process priority needs to be lowered.
This requires listening for the lifecycle of the UI component and unbind when onDestroy occurs.
This is what RemoteManager does in the figure, which internally maintains the life cycle of the foreground component. Andromeda provides several with methods to get the corresponding RemoteManager:
public static IRemoteManager with(android.app.Fragment fragment) {returngetRetriever().get(fragment); } public static IRemoteManager with(Fragment fragment) {returngetRetriever().get(fragment); } public static IRemoteManager with(FragmentActivity fragmentActivity) {returngetRetriever().get(fragmentActivity); } public static IRemoteManager with(Activity activity) {returngetRetriever().get(activity); } public static IRemoteManager with(Context context) {returngetRetriever().get(context); } public static IRemoteManager with(View view) {returngetRetriever().get(view); }Copy the code
Borrowed from Glide, these methods were eventually converted into two categories:
- A UI component with a life cycle, ultimately an Activity or Fragment.
- ApplicationContext.
In the first case, the framework adds an invisible RemoteManagerFragment to the current Activity or Fragment to listen for its life cycle.
Do not unbind for scenarios where ApplicationContext is used to obtain remote services.
In fact with the Jetpack lifecycle components can also be convenient to monitor the Activity/fragments of life cycle, but this has a premise, that is the Activity must inherit the android. Support. The v4. App. FragmentActvity, And Fragment must inherit the android. Support. The v4. App. Fragments, and v4 library version must be greater than or equal to 26.1.0, since this version to support the Lifecycle.
Event bus
On top of the above communication framework, implementing an event bus is as easy as reverse engineering.
Let’s look at usage
Andromeda. Subscribe (EventConstants.APPLE_EVENT, mainActivity.this); Bundle = new Bundle(); bundle.putString("Result"."gave u five apples!");
Andromeda.publish(new Event(EventConstants.APPLE_EVENT, bundle));
Copy the code
Here Event is the carrier of Event delivery.
public class Event implements Parcelable { private String name; private Bundle data; . }Copy the code
As for the principle, recall that we registered the RemoteTransfer Binder of this process with the Dispatcher while registering the remote service.
When we subscribe to an Event, we only store the Event name and listener in the RemoteTransfer of this process. When another process issues an Event, it will send the Event object to Dispatcher through an IPC call. After the Dispatcher receives the Event, Callback information will be successively sent to the registered RemoteTransfer, that is to say, multiple IPC calls may be made in this step, and efficiency needs to be diss.
After the event arrives at the subscriber process, all listeners with that name are extracted and sent to the listener.
Note: The listener here is usually an Activity, but RemoteTransfer is obviously part of the process life cycle, so a weak reference is needed to save the listener.
Insert the pile
Pile insertion was repeatedly mentioned in the above analysis principle process, and several points were summarized:
- Insert the DispatcherProvider and DispatcherService that belong to the Dispatcher process into the manifest (StubServiceGenerator).
- Insert the preset StubService for each process into the MANIFEST (StubServiceGenerator).
- Insert the StubServiceMatcher class map into the StubServiceMatchInjector table.
For the manifest operation, the framework provides a number of tools and methods, such as the process to get all declarations, worth studying; Javasisst is used for class operations, which was also introduced in the previous AOP article for interested students to check out.
While reading the source code, I found two noteworthy issues:
First, DispatcherProvider forgery DispatcherCursor inherits MatrixCursor, which is usually used to return several fixed known records without the need to query such scenarios from the database.
If the bundle contains parcelable objects, you need to manually set the setClassLoader.
#DispatcherCursor
public static IBinder stripBinder(Cursor cursor) {
if (null == cursor) {
returnnull; } Bundle bundle = cursor.getExtras(); / / removed from the cursor bundles need to set up this bundle. SetClassLoader (BinderWrapper. Class. GetClassLoader ()); BinderWrapper BinderWrapper = bundle.getParcelable(KEY_Binder_WRAPPER);returnnull ! = BinderWrapper ? BinderWrapper.getBinder() : null; }Copy the code
By default, the ClassLoader used for bundle transmission is BootClassLoader, which can only load system classes. In this project, the class needs to be loaded by PathClassLoader. Therefore, an additional call to the bundle’s setClassLoader method is required to set up the class loader, as described in the bundle.setclassloader () method resolution.
disadvantages
- Services require manual registration, which is difficult timing. It is best to provide a switch for automatic registration of the service, the upper layer does not need to worry about the registration of the service.
- Sending an event requires multiple IPC calls, which is inefficient and has room for optimization.
- You still need to write the AIDL file.
At this point, the core principles of Andromeda have been analyzed. There are some issues that need to be addressed, but it has provided us with a number of good ideas to solve the problem, whether it is to continue to optimize or simplify localization is a good choice.