Building Modular and Multi-Process Android Apps: A Practical Guide

Building Modular and Multi-Process Android Apps: A Practical Guide

Related Articles:

Incredible! 74 Complete APP Source Codes!

Android Architecture Analysis: What Happens from APP Launch to Main Page Display?

Two Latest Android Open Source Libraries from Alibaba: A Boon for Android Developers

Source: http://blog.spinytech.com/2016/12/28/android_modularization/

Regarding the issue of modularization, I believe every developer has thought seriously about it. As the project develops, the business continues to grow, and more and more business modules are added, the coupling between various modules becomes increasingly severe. At the same time, some projects (like our company) also involve the separate packaging and promotion of sub-applications, as well as the release of shadow applications, etc., making it urgent to readjust the architecture. Today, let’s talk about modularization, and this article is also my understanding of project architecture over the past few years.

The Initial Ultra-Small Project

When we first started working on Android projects, most people did not consider the project architecture. Let’s take a look at a diagram.

Building Modular and Multi-Process Android Apps: A Practical Guide

A small project developed in 2012

This sub-package structure looks familiar, with various components all packed into one package, completely lacking a hierarchical structure, and business, interface, and logic are all coupled together. This was a small project I developed when I first started learning Android at the end of 2012. Six months later, a colleague joined, and we developed together, leading to daily disputes over who modified whose code to no avail.

Architecture Improvement: Small Projects

Later, as we developed the App, the number of people increased, so we couldn’t do it the old way; we had to refactor. So I extracted the common code to create an SDK base library, encapsulated individual functions into Library packages, and divided different businesses into different modules using a sub-package structure, allowing each team member to develop their own module. At first, it was still easy and pleasant, with parallel development and a harmonious scene, as shown in the following figure.

Building Modular and Multi-Process Android Apps: A Practical GuideArchitecture after refactoring

Building Modular and Multi-Process Android Apps: A Practical Guide

Building Modular and Multi-Process Android Apps: A Practical GuideAfter making a few minor changes

As we can see, with the increase of business in several versions, the coupling between various business modules became increasingly severe, leading to code that was difficult to maintain and update, let alone write test code. Although we later introduced a unified broadcasting system, which improved the problem of mutual references between modules to some extent, the limitations and coupling were still high, making it impossible to cure this issue. By the end, this architecture had poor extensibility and maintainability, and it was also difficult to test, so it was ultimately abandoned by the historical process.

Medium and Small Projects: Routing Architecture

Building Modular and Multi-Process Android Apps: A Practical Guide

Implementation Principle

Let’s first look at the routing architecture diagram

Building Modular and Multi-Process Android Apps: A Practical GuideRouting Architecture

As seen in the figure, we created a Router in the basic Common library, with n modules Module in between. These Module are actually the modules in Android Studio, and all these Module are Android Library Modules, with the top Module Main being a Runnable Android Application Module.

All these Module reference the Common library, while the Main Module also references modules A, B, and N. After this processing, the mutual calls between all Module disappear, reducing coupling, and all communications are handled by the Router for dispatching, while the registration work is initialized by the Main Module. This architectural idea is actually very similar to the Binder’s concept, adopting a C/S pattern, isolating modules, and passing data through a shared area. Modules only expose open Actions, so it also embodies the interface-oriented programming philosophy.

The red rectangles in the figure represent Actions, Action is the specific execution class, and its internal invoke method contains the specific execution logic. If concurrent operations are involved, locks can be added in the invoke method, or the synchronized keyword can be applied directly to the invoke method.

The yellow rectangles represent Providers, Provider, each Provider contains one or more Action, and its internal data structure uses a HashMap to store Actions. The time complexity for querying a HashMap is O(1), meeting our requirements for calling speed, and since we register uniformly, there are no concurrency issues during writing, and any concurrency issues during reading are handled by the Action’s invoke. In each Module, there will be one or more Providers (if there are no Provider, then this Module cannot provide services to other Module).

The blue rectangles in the diagram represent Routers, Router, each Router contains multiple Provider, and its internal data structure also uses a HashMap to store Provider, and the principle is the same as that of Provider. The reason for using HashMap twice is twofold: first, it makes it less likely to cause Action name collisions; second, during registration, only registering Provider reduces registration code and improves readability. And since the query time complexity of HashMap is O(1), two lookups won’t waste too much time. When the corresponding Action cannot be found, the Router will generate an ErrorAction, informing the caller that there is no corresponding Action, and the caller can decide how to handle it next.

Request Flow

The specific flow of calling through the Router is as follows:

Building Modular and Multi-Process Android Apps: A Practical GuideRouter Sequence Diagram

  1. Any code creates a RouterRequest, containing Provider and Action information, and requests the Router.

  2. Router receives the request, looks up the corresponding Provider in its internal HashMap using the Provider information from RouterRequest.

  3. Provider receives the request and finds the corresponding Action information in its internal HashMap.

  4. Action calls the invoke method.

  5. Returns the ActionResult generated by the invoke method.

  6. Wraps the Result into a RouterResponse and returns it to the caller.

Reduced Coupling

All mutual dependencies between Module have vanished, allowing us to cancel any Module references in the main app without affecting the overall App’s compilation and operation.

Building Modular and Multi-Process Android Apps: A Practical GuideCanceling dependency on Module N

As shown in the figure, we canceled the dependency on Module N, and the overall application can still run stably. If there is a call to Module N, it will return a Not Found prompt. In actual development, specific handling can be done according to requirements.

Enhanced Testability

Since each Module does not depend on other Module, we can focus on developing our module during the development process and can create a test App for white-box testing.

Building Modular and Multi-Process Android Apps: A Practical GuideTesting Module A

Enhanced Reusability

Regarding reusability, the industry I work in is investment promotion, which requires developing many shadow apps around the main business to expand coverage (similar to 58->58 rental, 58 recruitment, Meituan->Meituan takeaway, etc.). At this point, the reusability of this architecture becomes apparent; we can decompose the business and write a wrapper App to generate an independent shadow App. This shadow App can reference whichever Module it needs, allowing for rapid development, and if there are changes in the Module business later, there is no need to modify all the code, reducing code duplication. For example, we once separated the IM module and the investment consulting module, wrote some interfaces and styles, and generated the “Investment Broker” App.

Support for Parallel Development

The entire architecture is quite similar to the Git Branch concept, based on the main line, with branches developed separately, and finally merged back into the main line. Here, the thought process is similar to branches, but in actual development, each module can be a branch or a repository. Each module needs its own version control to facilitate issue management and tracing. The main project can reference each module directly, export AAR references, or upload to JCenter Maven, etc. However, the thought process remains unified: inherit common -> independent development -> merge back into the main line.

Multi-Process Considerations: Medium Projects

As the project expands, the App’s memory consumption during operation also increases, and sometimes online bugs can lead to overall crashes. To ensure a good user experience and reduce consumption of system resources, we began to consider re-architecting the program using multiple processes, loading on demand, and releasing promptly to achieve optimization.

Advantages of Multi-Process

The advantages and usage scenarios of multi-process have been introduced in the previous article “Android Multi-Process Usage Scenarios”, and the main advantages are as follows:

  • Improving the stability of each process, preventing a single process crash from affecting the entire program.

  • More controllable memory, allowing manual process release to achieve memory optimization.

  • Based on independent JVMs, each module can be fully decoupled.

  • Retaining only daemon processes will make the application last longer and less likely to be reclaimed.

Potential Issues

However, enabling multiple processes means the Router system becomes invalid. Router is a JVM-level singleton and does not support cross-process access. This means that all your Provider and Action in the background process are registered to the background Router. When you call from the foreground process, you cannot access the Action of other processes.

Solution

Actually, the solution is not complicated. The original routing system can continue to be used; we can imagine the entire architecture as the Internet. Now multiple processes have multiple routers, and we just need to connect multiple routers together, so the entire routing system can still operate normally. Therefore, we refer to the original Router as the local router LocalRouter, and now we need to provide an IPS and DNS provider, so we create a process whose function is to register routes, connect routers, and forward messages, which we call the wide-area router WideRouter.

Let’s take a look at the routing connection architecture diagram

Building Modular and Multi-Process Android Apps: A Practical GuideRouting Connection Architecture

As shown in the figure, in the vertical direction, each column represents a process, separated by dashed lines, including Process WideRouter, Process Main, Process A, …, Process N. The light yellow represents WideRouter, and the dark yellow represents the service guardian of WideRouter. The light blue represents each process’s LocalRouter, and the dark blue represents the service guardian of each LocalRouter. WideRouter binds to each process’s LocalRouter guardian service through AIDL, and each LocalRouter is also bound to the guardian service of WideRouter through AIDL, achieving the goal of all routers being interconnected.

Event Dispatching

Building Modular and Multi-Process Android Apps: A Practical Guide

URL: xxxDomain/xxxProvider/xxxAction?data1=xxx&data2=xxx

This is very similar to an HTTP request. The benefit of doing this is that it allows subsequent WebView to conveniently call local Action.

JSON:

    { 
        domain: xxx, 
        provider: xxx, 
        action: xxx, 
        data{ 
            data1: xxx, 
            data2: xxx 
        } 
    } 

JSON format is simple and clear and can serve as an interface return value sent from the server to the client.

Next, let’s discuss how an event is transmitted during a cross-process request:

Building Modular and Multi-Process Android Apps: A Practical GuideEvent Transmission Diagram

From the diagram, we can clearly see that we mainly divide the event dispatching and transmission into two main parts.

  • The first part is to determine whether the target Action is an asynchronous program across processes.

  • The second part is to execute the target Action across processes.

Building Modular and Multi-Process Android Apps: A Practical Guide

As for local event dispatching, it still follows the previous Router pattern; from Step 17 to Step 21, they are all the same as the single-process synchronous Router dispatch mechanism without any changes.

Multi-Process Application Logic Dispatching

In a multi-process environment, each time a new process is started, a new Application is created, so we need to separate the application logic of each process and choose different application logic for processing based on the Process Name.

The actual Application startup process is as follows:

Building Modular and Multi-Process Android Apps: A Practical GuideMulti-Process Application Startup Process

Building Modular and Multi-Process Android Apps: A Practical Guide

Conclusion

Building Modular and Multi-Process Android Apps: A Practical Guide

This article’s project address: ModularizationArchitecture, welcome everyone to star, fork, and provide suggestions.

Or directly introduce it into the project:

compile 'com.spinytech.ma:macore:0.2.0'

Did you gain something from this article? Please share it with more people

Java and Android Architecture

Welcome to follow us and discuss technology together. Scan and press the QR code below to quickly follow us. Or search for the WeChat public account: JANiubility.

Building Modular and Multi-Process Android Apps: A Practical Guide

Public Account:JANiubility

Building Modular and Multi-Process Android Apps: A Practical Guide

Leave a Comment