* This article has been published exclusively by guolin_blog, an official wechat account

CC: Component Caller, an Android component-based development framework, github address: github.com/luckybilly/… This article explains how the framework works. If you just want to know how to use it, you can go to the README documentation on Github

preface

First of all, componentization in this article is not the same concept as plugins in the industry (Atlas, RePlugin, etc.)



[Photo from Internet]

Component development: It is to divide an app into multiple modules, and each Module is a component (it can also be a basic library for components to rely on). During the development process, we can debug some components separately. Components do not need to depend on each other, but can call each other. For final release, all components are lib dependent by the main app project and packaged into an APK.

Plug-in development: and modular development not slightly, pluggable development will the entire app split into many modules, these modules include a host and multiple plug-ins, each module is an apk (componentization of each module is a lib), finally package will host the apk and plugin apk (or other format) or joint packaged separately.

This paper will mainly introduce the following aspects:

1. Why is componentization needed?

Ii. Function introduction of CC

Iii. Technical points of CC

Detailed analysis of CC execution process

Five, the use of the introduction

Six, how high is the cost of componentized transformation of old projects?

1. Why is componentization needed?

There are many reasons to use componentalization, such as business isolation, and the efficiency of development and debugging by running the app separately. I will add that componentalization makes it easy to implement some component-level AOP, such as:

  • Easily realize page data (network request, I/O, database query, etc.) preloading function
  • This time consuming logic is executed asynchronously while the page jumps when the component is invoked
  • After the page jumps and initialization is complete, the pre-loaded data is displayed
  • Verify login status during component function invocation
  • With the interceptor mechanism, you can dynamically add different intermediate processing logic to component function calls

Ii. Function introduction of CC

  1. Support component calls to each other (not just Activity jumps, but calls/callbacks to any instruction)
  2. Support component calls associated with Activity and Fragment life cycles
  3. Support inter-app cross-process component invocation (component can be run as app separately during development/debugging)
  • This is useful for running components independently. For example, if a function of a component requires a user’s login information, the login page of the login component is displayed if the user has logged in, and the current user information is obtained if the user has logged in. In this case, you can directly use the login component in the main APP and the login state of the user in the main APP. When the component runs independently as the APP, it does not need to rely on the login component and can always maintain the independent running state for development.
  1. Support switch and permission setting of inter-APP call (meet security requirements of different levels, open by default and do not need permission)
  2. Supports synchronous or asynchronous invocation
  3. Support synchronous/asynchronous implementation of components
  4. The invocation is not limited by the implementation (for example, the synchronous implementation of another component can be invoked asynchronously. Note: Do not use time-consuming operation at the same time in the main thread.
  5. Support for adding custom interceptors (in the order they were added)
  6. Support timeout setting
  7. Manual cancellation
  8. Automatic component registration (IComponent) at compile time, no manual maintenance of the component registry (implemented using ASM to modify bytecode)
  9. Support for dynamic/unregistered components (IDynamicComponent)
  10. Supports the transfer of non-basic types of objects such as fragments between components (supported when components are in the same APP, but not supported for the transfer of non-basic types of objects across apps)
  11. Solved the crash caused by incorrect posture as much as possible:

    • The crash of component invocation, callback and component implementation is all caught inside the framework
    • CCResult objects returned synchronously or called asynchronously must not be null to avoid null Pointers and can be degraded based on CCResult results.

Demo effect

Component A is packaged in the main app, and component B is A separate running component app. The following figure illustrates the effect of calling both in the main app, with the result shown below in Json format. Demo download address) :

Iii. Technical points of CC

The main problems to be solved in realizing the CC component development framework are as follows:

  • How do components automatically register?
  • How to make synchronous/asynchronous component calls compatible?
  • How to implement components synchronously/asynchronously?
  • How do I call components across apps?
  • How can components switch between Application and Library more easily?
  • How do I implement startActivityForResult?
  • How do I prevent illegal external calls?
  • How does it relate to the life cycle of activities and fragments

3.1 How do I Automatically Register components?

In order to reduce the maintenance cost, we want to achieve the effect that when we need to add a component to the app, we just need to add a dependency on this module in gradle (usually a Maven dependency or a project dependency).

The original idea was to use annotationProcessor to dynamically generate component map code through compile-time annotations. This doesn’t work because compile-time annotations only work at source compile time and can’t be scanned for aar annotations (project dependencies, Maven dependencies), which means that each module must generate its own code at compile time. Then we need to find a way to find these classes scattered in the AAR category for centralized registration.

ARouter’s solution:

  • Each module to generate your own Java classes, these classes of package names are ‘com. Alibaba. Android. Arouter. Routes’
  • The mapping table is then registered at runtime by reading all classes under this package in each dex file through reflection, as shown in the classutils.java source code

    The runtime iterates through each entry by reading all dex files to find all class names in the specified package, and then retrieves class objects by reflection. That doesn’t seem very efficient.

The ActivityRouter solution (demo has 2 components named ‘app’ and ‘SDK ‘) is:

  • There is one in the main App Module@Modules({"app ", "sdk "})A RouterInit class is generated based on annotations that mark how many components are currently in the app
  • Generate routerMApping_app.map that calls the same package in the Init method of the RouterInit class
  • Each module of the generated classes (RouterMapping_app. Java and RouterMapping_sdk. Java) in the com. Making. Mzule. Activityrouter. The router package (in different aar, but the same package name)
  • The map() method of the RouterMapping_sdk class generates calls to phones.map (…) based on all route annotations in the current Module that are scanned. Method to register the route code
  • Routerinit.init () is triggered by all apis of the Routers to register all routes in the mapping table

    This approach combines all module routing map classes with a RouterInit class. It is more efficient to run than scanning all dex files, but requires an additional component name list annotation in the main project code: @modules ({"app ", "SDK "})Copy the code

    Is there a better way?

Transform API: Can scan all classes currently packaged into APK at compile time (before dex/ ProGuard), including: Classes in Java files compiled in the current Module, classes in AIDL files compiled, classes in JAR packages, classes in AAR packages, classes in project dependencies, classes in Maven dependencies.

ASM: can read analysis bytecode, can modify bytecode

You can make a gradle plug-in that automatically scans all component classes (IComponent interface implementation classes) at compile time and then modifies the bytecode. The generated code calls the constructors of all scanned component classes to register them in a ComponentManager class. Generate a mapping table between component names and component objects.

This gradle plugin named AutoRegister is now open source and upgrades to automatically scan any specified interface implementation class (or a subclass of class) at compile time and automatically register the specified method of the specified class. Gradle app/build.gradle app/build.gradle app/build.gradle

3.2 How to Invoke Components synchronously or asynchronously?

By implementing the Java. Util. Concurrent. Callable interface synchronization results returned to compatible with synchronous/asynchronous invocation:

  • Call directly when called synchronouslyCCResult result = Callable.call()To get the return result
  • Asynchronous call, put it in the thread pool, after completion of execution to invoke a callback object returned results: IComponentCallback. OnResult (cc, the result)

ExecutorService.submit(callable)Copy the code

3.3 How to Implement components in synchronous/asynchronous mode?

The onCall method of a component may need to be implemented asynchronously and not return results synchronously, but it may need to return results synchronously, which is a paradox. The wait-notify mechanism of Object is used here. When the component needs to return the result asynchronously, it blocks inside the CC framework. When the result returns, it interrupts the blocking through notify and returns the result to the caller

Note that when implementing a component, you must ensure that the component will always call back the result, i.e. : You need to make sure that the result is called back on every logical branch that terminates the calling process (including if-else/try-catch/ activity.Finish ()-back key – return button, etc.), otherwise the caller will block waiting for the result until it times out. It is similar to sending a network request to the server and the server must return the result of the request, otherwise the request will time out.

3.4 How do I call components across APPS?

Why do you need to make component calls across your app?

  1. The process of componentized transformation of existing projects is certainly not accomplished overnight, but gradually separated from the main project one by one, which involves the communication between the main project and components. If you can’t make component calls across apps, you need to package development with the main project, losing one of the big advantages of component-based development: component compilation and running improves development & testing efficiency.
  2. When independent components need to call the functions of other components, there is no need to compile and package other components together. Components in the main app can be called, and the state of single Module compilation and operation can always be maintained for development.

Currently, common componentized frameworks adopt cross-APP communication solutions as follows:

  • URLScheme(e.g. ActivityRouter, Ali ARouter, etc.)

    • Advantages:

      • There’s built-in support for calling from the WebView
      • No need to register with each other (no need to know the process name of the app to be called, etc.)
    • Disadvantages:

      • You can only send information to a component in one direction, for starting an Activity and sending instructions, not for getting data (for example, getting the current user login information for a user component)
      • You need an extra relay for the Activity to process the URLScheme and then forward it
      • If multiple apps using the same URLScheme are installed on the device, a selection box will pop up (this problem occurs when multiple components are installed on the device as apps)
      • Permissions cannot be set, switch cannot be set, any APP can be called, there are security risks
  • AIDL (such as: ModularizationArchitecture)

    • Advantages:

      • Objects of type Parcelable can be passed
      • High efficiency
      • You can set the switch for cross-app calls
    • Disadvantages:

      • You need to know in advance which process the component is in before invoking it; otherwise, ServiceConnection cannot be established
      • When components are packaged into the main app as a standalone app and as a lib, the process name is different and the maintenance cost is high

      When I designed this feature, my starting point was: as the basic library of the componential development framework, I wanted to make cross-process invocation as consistent as possible as the function called within the process, and it was as easy as possible for developers using this framework to switch between app mode and Lib mode, in addition, I needed to avoid compromising product security. Therefore, the implementation of inter-component communication should meet the following conditions:

  • Each app can be called by other apps
  • The app can set whether to provide support for cross-process component calls externally
  • After the request for a component call is sent, it automatically detects whether there is an APP on the current device that supports the call
  • Supports timeout and cancellation

Based on these requirements, I chose BroadcastReceiver + Service + LocalSocket as the final solution:

* If a component, Component1, which does not exist in the current APP, is initiated in appA, then a LocalServerSocket is established and broadcast to other apps installed on the device that also use this framework. Meanwhile, if this component is supported in an appB, According to the information brought in the broadcast, the connection is established with LocalServerSocket, and the component Component1 is called in appB, and the result is sent to appA through LocalSocket. BroadcastReceiver is one of the four components of Android. It can set the receiving permissions and avoid malicious calls from outside. And you can set the switch, after receiving this broadcast to decide whether to respond (pretend not to receive…) . The LocalSocket link is set up to continue sending timeout and cancellation instructions to the component invocation request. *

When implemented this way, there are three problems:

  • As the broadcast receiver is defined in the basic library and exists in all apps, when the user synchronously calls components across apps in the main thread, the caller’s main thread is blocked, and the broadcast receiver also runs in the main thread that requires it, so the broadcast receiver cannot run until timeout, and the component invocation fails.

    • Running the broadcast receiver in a child process solves the problem
  • The invoked APP is not started or the process is manually terminated, and the broadcast cannot be received. Procedure

    • This problem is not well solved at the moment, but considering that componentized development only needs to use cross-process communication during development, developers can solve the problem by manually granting self-start permission to the corresponding APP in system Settings
  • When called across processes, only basic data types can be passed and Java objects such as fragments cannot be retrieved

    • This problem does not exist when calling from within the app, where maps are passed back and forth and any data type can be passed. However, since inter-process communication is sent back and forth through strings, non-basic data types are not supported for now, so Serializable can be considered in the future

3.5 How can components switch between Application and Library more easily?

Apply plugin: ‘com.android. Application ‘or apply plugin: ‘com.android. Library and sourceSets toggle. In order to avoid having too much duplicate code in build.gradle for each module, I made a wrapper that defaults to Library mode and provides two ways to switch to Application mode: Add ext.runAsApp =true to module build.gradle or module_name=true to project root local.properties

Using this wrapper requires only one line of code:

// Replace the original apply plugin: 'com.android.application' or apply plugin: 'com.android.library' // with the following line apply from: 'https://raw.githubusercontent.com/luckybilly/CC/master/cc-settings.gradle'Copy the code

Cc -settings.gradle source portal

3.6 How do I Implement startActivityForResult?

StartActivityForResult is also designed for page value transfer. In the CC componentization framework, page value transfer does not need to use startActivityForResult at all. Handle it directly as a component of the asynchronous implementation (call cc.sendccresult (callId, ccResult) where setResult used to be, and note that pressing the back key and the return button also calls back the result).

If there is a large amount of startActivityForResult code in the original project and the transformation cost is high, you can use the following method to keep the original onActivityResult(…). And setResult related code in the activity:

  • Where startActivityForResult was called, CC is used to pass the current context to the component

    CC.obtainBuilder("demo.ComponentA ")
     .setContext(context)
     .addParams("requestCode ", requestCode)
     .build()
     .callAsync();Copy the code
  • Start the Activity with startActivityForResult in the component’s onCall(CC) method

    @Override public boolean onCall(CC cc) { Context context = cc.getContext(); Object code = cc.getParams().get("requestCode "); Intent intent = new Intent(context, ActivityA.class); if (! (context instanceof Activity) {// The caller does not set the context or app component jump, The context for the application intent. AddFlags (intent. FLAG_ACTIVITY_NEW_TASK); } if (context instanceof Activity && code ! = null && code instanceof Integer) { ((Activity)context).startActivityForResult(intent, (Integer)code); } else { context.startActivity(intent); } CC.sendCCResult(cc.getCallId(), CCResult.success()); return false; }Copy the code

3.7 How do I Prevent Illegal External Calls?

To meet different requirements, two security levels can be set:

  • Permission verification (Set the permission for the broadcast of interprocess communication, which can generally be set to signature-level permission verification). The procedure is as follows:

    • Create a New Module
    • Add base library dependencies to build.gradle in this module, for example:The compile 'com. Billy. Android: cc: 0.3.0'
    • In the module of SRC/main/AndroidManifest. Set the permissions on the XML and the level of permissions, reference component_protect_demo
    • Every other module depends on this module in addition, or defines a global cc-settings.gradle, see cc-settings-demo-b.gradle
  • A switch setting for whether or not an external call responds (this is easier to use)

    • Called in application.oncreate ()CC.enableRemoteCC(false)You can turn off responding to external calls

To facilitate developer access, support for external component calls is turned on by default and no permission verification is required. Before the app is officially released, it is recommended to call cc.enablerEmotecc (false) to close components that respond to external calls to the app.

3.8 How to Associate activities and Fragments with their life cycles

Background: When using asynchronous callbacks, callback objects usually use anonymous inner classes and hold references to external class objects, which can easily cause memory leaks. Such memory leaks are common in various asynchronous callbacks. Such as Handler.post(runnable), Retrofit’s Call.enqueue(callback), etc.

To avoid memory leaks and the ability to cancel unnecessary tasks after a page exits, CC has added life-cycle associations that automatically cancel all unfinished component calls within a page when the onDestroy method is called

  • Activity lifecycle association

    You can use API Level 14 (Android 4.0) and above to register the global Activity lifecycle callback listener, find all associated cc objects in the 'onActivityDestroyed' method, and automatically call the cancel function:Copy the code
    application.registerActivityLifecycleCallbacks(lifecycleCallback);Copy the code
  • Android. Support. The v4. App. Fragments of life cycle

    The support library supports life cycle listening for fragments from [25.1.0][support25] :Copy the code
    FragmentManager.registerFragmentLifecycleCallbacks(callback)Copy the code
    You can cancel outstanding CC calls in its' onFragmentDestroyed 'methodCopy the code
  • Andorid.app. Fragment Lifecycle association (not supported yet)

Detailed analysis of CC execution process

Communication between components adopts the way of component bus. All component objects are registered in the component management class (ComponentMananger) of the basic library. ComponentMananger finds component objects and calls them by searching the mapping table.

When ComponentMananger receives a component invocation request, it looks up whether the component manifest in the current app contains the component that needs to be invoked

  • There are: processes for executing intra-app CC calls:

  • None: Executes the flow of CC calls between apps

    ! [App component calls between bus] (HTTP: / / https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2017/12/9/1603a35e9b27c92e~tplv-t2oaga2asx- image.image)Copy the code

4.1 Synchronous/asynchronous implementation of components and synchronous/asynchronous invocation principle of components

  • When the component is implemented, when the related function called by the component is finished, the result of the call is sent to the framework via cc.sendCcresult (callId, ccResult)
  • The return value of the IComponent implementation class (component entry class)onCall(CC) method represents whether the callback is asynchronous or not:

    • True: call cc. sendCCResult(callId, ccResult) asynchronously
    • False: cc. sendCCResult(callId, ccResult) will be called synchronously. This means that the onCall method will be called to send the results to the framework before the onCall method completes execution
  • When icomponent.onCall (cc) returns false, the CCResult is fetched directly and returned to the caller
  • When icomponent.oncall (cc) returns true, wait() is blocked until CCResult is obtained, and notify() is used to abort the block and continue running, returning CCResult to the caller
  • ComponentManager invoking component, create an implementation of a Java. Util. Concurrent. Callable interface ChainProcessor class is responsible for the specific component of the call

    • When called synchronously, execute directlyChainProcessor.call()To call the component and return the CCResult directly to the caller
    • When called asynchronously, willChainProcessorPut into the thread pool to execute, passIComponentCallback.onResult(cc, ccResult)CCResult is called back to the caller

The execution process is as follows:

4.2 Custom Interceptor (ICCInterceptor) Implementation principle

  • All interceptors are stored sequentially in the call Chain
  • There are 1 interceptors for the CC framework before the custom interceptor:

    • ValidateInterceptor
  • After the custom interceptor there are two interceptors of the CC framework itself:

    • LocalCCInterceptor(orRemoteCCInterceptor)
    • Wait4ResultInterceptor
  • The Chain class is responsible for executing all interceptors in turninterceptor.intercept(chain)
  • The interceptorintercept(chain)Method by callingChain.proceed()Method to obtain CCResult

4.3 App CC Call process

This process is executed when the component to be called is inside the current APP. The complete flow chart is as follows:

The main functions of CC are provided by icCInterceptors, which form a Chain of calls that are initiated and executed by the ChainProcessor. The ChainProcessor object is created in ComponentManager. Therefore, the ChainProcessor can be viewed as a whole. Once created by ComponentManager, the Component’s onCall method is called and the result of component execution is returned to the caller. Wait4ResultInterceptor Within the ChainProcessor The execution of the ChainProcessor can be interrupted by a timeout or cancel event.

4.4 CC Call process between Apps

When the component to be called cannot be found in the current APP, execute this process. The complete flow chart is as follows:

Five, the use of the introduction

CC integration is very simple and can be completed in just four steps:

  1. Add automatic registration plug-in

    Buildscript {dependencies {classpath 'com. Billy. Android: autoregister: 1.0.4'}}Copy the code
  2. Use the apply cc-settings.gradle file instead of ‘app plugin… ‘

    
    apply from: 'https://raw.githubusercontent.com/luckybilly/CC/master/cc-settings.gradle'Copy the code
  3. Implement the IComponent interface to create a component class

    Public class ComponentA implements IComponent {@override public String getName() {// ComponentA implements IComponent;  // CC.obtainBuilder("demo.ComponentA ").build().callAsync() return "demo.ComponentA "; } @Override public boolean onCall(CC cc) { Context context = cc.getContext(); Intent intent = new Intent(context, ActivityComponentA.class); if (! (context instanceof Activity) {// The caller does not set the context or app component jump, The context for the application intent. AddFlags (intent. FLAG_ACTIVITY_NEW_TASK); } context.startActivity(intent); // Send the result of the component call (return message) cc.sendCcresult (cc.getcallid (), ccresult.success ()); return false; }}Copy the code
  4. Call the component using cc.obtainBuilder (” Component_name “).build().call()

    CCResult result = cc.obtainBuilder ("demo.ComponentA ").build().call(); CCResult result = cc.obtainBuilder ("demo.ComponentA ").build(). Cc.obtainbuilder ("demo.ComponentA ").build().callasync (); cc.obtainBuilder ("demo.ComponentA ").build().callasync (); Cc.obtainbuilder (" demo.ponenta ").build().callAsync(new IComponentCallback(){... }); // call asynchronously, In the main thread execute callback CC. ObtainBuilder (" demo.Com ponentA "). The build () callAsyncCallbackOnMainThread (new IComponentCallback () {... });Copy the code

    For more usage, see README on Github

PS: It tastes even better with another of my libraries (PreLoader) : AOP implementations preload the data required for a page before opening it, and this preloading is implemented entirely inside the component, uncoupled from the outside.

Six, how high is the cost of componentized transformation of old projects?

Some students really want to try component-based development, but only stay at the stage of understanding, because they are worried about the amount of transformation in the old project is too large, and dare not make a big change.

CC framework itself is designed under the requirement of componentization transformation of old projects. Considering some pain points in the process of componentization, CC supports progressive componentization transformation at the beginning of design

conclusion

This paper introduces the main functions, technical scheme and execution process of android componentized development framework CC in detail, and gives a simple example of how to use it. If you are interested, you can clone source code from GitHub for specific analysis. If you have better ideas and solutions, you are also welcome to contribute code to further improve CC.

series

CC framework practice (1) : Implement the function of entering the target interface after successful login

CC framework practice (2) : Componentization of Fragments and Views

CC Framework Practice (3): Make jsBridge more elegant

Thank you

ActivityRouter

ARouter

ModularizationArchitecture

Android Architecture Thinking (modular, multi-process)

Open source best practice: ARouter, the page routing framework for the Android platform

DDComponentForAndroid

Router