Can Android cross process component communication be written like this?

Posted by phpcharan on Fri, 24 Dec 2021 03:56:35 +0100

Can Android cross process component communication be written like this?

As an Android Developer, you should have some component knowledge. The main feature of componentization is to peel off dependencies, but how to solve the communication problem when there is no direct dependency between components.

We usually use this C/S architecture similar to Binder communication. A ServiceManager service manager acts as a bridge to provide service registration and service query. Business communication is the following Trilogy: defining service interfaces, publishing services and using services.

This architecture is simple and intuitive to run between single processes. What we need to do is sink the service interface into public dependency, instantiate the object of the service interface in component A, and then publish the service through ServiceManager, and component B obtains and uses the service through ServiceManager. But to cross process, it's a little troublesome.

In the scenario of maintaining this architecture, we can write AIDL and let the ServiceManager support the management of Binder type services. But in fact, friends who have written AIDL may know that the experience is not good. The following steps are required:

  1. Define AIDL interface file (no IDE automatic prompt support)
  2. Compile and generate Stub class (pre compile and generate once)
  3. Inherit the Stub class and implement the interface defined in AIDL (you must inherit the Stub type. Unlike the implementation class of ordinary services, you only need to implement the corresponding interface, and you can inherit other classes for code reuse)
  4. RemoteException must be caught for remote calls through IInterface (either catch each call, or write a wrapper class wrapper to eat the exception and return to the default value)

If the interface changes are involved, you have to compile it twice, change the AIDL file and the Stub class implementation file. If the packaging class is used in step 4, you have to change the packaging class file

The templating based on AIDL is too serious, which has greatly affected our development experience. Therefore, the SimpleService project was launched. It is expected to dynamically generate these template codes through the popular APT (annotation processing tool) technology. Let's just focus on the interface and implementation, so as to make the writing cross process communication as simple and elegant as ordinary component communication.

After several weeks of efforts, a usable version has been realized: github transfer

How to use it? See the following:

1, The publishing and use process of common component services is as follows:

// 1. The public dependency module declares the service interface
interface IMusic {
  fun play(name: String)
}

// 2. Music component module to realize service interface
class MusicService: IMusic {
  override fun play(name: String) {
    Logging.d("MusicService", "play $name")
  }
}

// 3. Music component module, publishing service
SimpleService.publishService(IMusic::class.java, MusicService())

// 4. Other component modules to obtain and use services
val music = SimpleService.getService(IMusic::class.java)
music.play("Thousands of miles away")

2, The main play of the cross process component service publishing process is as follows:

Before using the cross process service related interfaces, each process needs to initialize the remote service and provide context. Generally, it can be initialized in the onCreate method of application:

class App : Application() {

    override fun onCreate() {
        super.onCreate()
        // [change 1] initialization is required before using the cross process service
        SimpleService.initRemoteService(this)
    }
}

Specific release process:

// 1. The public dependency module declares the service interface
// [change 2] add the 'RemoteServive' annotation to the cross process service interface, and simpleservice will automatically generate AIDL types
@RemoteService
interface IMusic {
  fun play(name: String)
}

// 2. Music component module to realize service interface
class MusicService: IMusic {
  override fun play(name: String) {
    Logging.d("MusicService", "play $name")
  }
}

// 3. Music component module, publishing remote services
// [change 3] change the publishing service to the call of publishRemoteService method
SimpleService.publishRemoteService(IMusic::class.java, MusicService())

// 4. Other component modules (cross process), obtain and use remote services
// [change 4] the remote service interface is changed to getRemoteServiceWait. Here, it is obtained by synchronous waiting for 5s.
// It also provides callbacks and asynchronously obtaining of coroutines, which are bindRemoteService and getRemoteService respectively
val music = SimpleService.getRemoteServiceWait(IMusic::class.java, 5000)
music.play("Thousands of miles away")

The above four simple changes have completed the release and use of cross process services. There is only one incremental change, that is, initializing remote services, and the rest are the same as writing local services!

3, Cross process component service publishing supports the separable type parameter

// Declare music information type
// Using the 'ParcelableAidl' annotation to modify the Pacelable type, SimpleService will automatically generate an AIDL declaration
@ParcelableAidl
@Parcelize
class MusicInfo(var name: String, var author: String): Parcelable

// Enhance the service interface so that the play interface can receive richer information for more accurate music matching
@RemoteService
interface IMusic {
  fun play(info: MusicInfo)
}

4, It also supports the modifiers of AIDL, such as oneway, in, out and inout

@RemoteService
interface IMusic {
  // play does not need to return a value and can be declared as oneway
  @OneWay
  fun play(info: MusicInfo)
  // Read the original audio data, and the parameter data is declared as out
  fun getRawData(info: MusicInfo, @Out data: ByteArray)
}

5, Callbacks are also supported

// Declare playback progress callback interface
@RemoteService
interface OnPlayProgressUpdate {
  fun onProgress(progress: Int)
}

@RemoteService
interface IMusic {
  // Add playback progress callback to play parameter
  @OneWay
  fun play(info: MusicInfo, onProgress: OnPlayProgressUpdate)
  fun getRawData(info: MusicInfo, @Out data: ByteArray)
}

Above, there is basically no big difference between the use of cross process services and inter process services. All you need to do is add a few annotations.

6, Before starting

Of course, we need to make some basic project configurations before use, as follows:

  1. In the project root directory of build Add maven repository of jitpack and plug-in dependency in gradle:
buildscript {
    // Unified version number of simple service Library
    ext.simple_service_version = "1.0.5"
  
    repositories {
        maven { url 'https://jitpack.io' }
    }
  
    dependencies {
      	// Declare the gradle plug-in dependency of simple service
        classpath("com.github.xiangning17.simpleservice:plugin:$simple_service_version")
    }
}

allprojects {
    repositories {
        maven { url 'https://jitpack.io' }
    }
}
  1. In the base module of remote service interface declaration, build In gradle:
// Enable the simple service plug-in
apply plugin: 'simple-service'

dependencies {
    // Add library dependencies and expose them to upper level components
    api "com.github.xiangning17.simpleservice:core:$simple_service_version"
  
    // Declaration annotation processor, which is used to parse RemoteService and other annotations and generate relevant agent types
    // If you need to handle the annotated kotlin class, you need to use kapt
    annotationProcessor "com.github.xiangning17.simpleservice:annotation-processor:$simple_service_version"
}
  1. In the build. Of the app module In gradle:
// The app module enables the simple service plug-in, which is used to set the process of RemoteServiceBridg
apply plugin: 'simple-service'

// Specify the process in which the RemoteServiceBridge remote service bridge is located through the following configuration
// RemoteServiceBridge is an external window of RemoteServiceManager remote service manager. It is an Android Service.
// When other processes want to connect to the RemoteServiceManager, they can obtain the singleton RemoteServiceManager object of the service process by binding the RemoteServiceBridge service,
// Then other processes can query, obtain, publish and listen to remote services.

// This configuration item can be omitted, and the default is applicationId, that is, the main process.
// However, even if this configuration is omitted, the step of enabling the 'simple service' plug-in in the app module cannot be omitted,
// Otherwise, the process in which the RemoteServiceBridge service is located cannot be successfully set
simpleService {
    bridgeServiceProcess = "me.xiangning.simpleservice"
}

7, Advanced usage

In addition to the above basic capabilities, SimpleService also provides the following features.

Before you begin, define a few concepts:

  • Remote service: refers to the interface modified by @ RemoteService
  • Remote service implementation: refers to the implementation class of "remote service". For example, MusicService is the remote service implementation of IMusic
  • Remote service Binder: refers to the Binder type automatically generated for 'remote service', which is used for cross process Binder service delivery
  • Remote service proxy: refers to the object actually obtained by the remote service user. It is a proxy implementation of the remote service Binder

1. Remote service republishing capability

When obtaining remote services through SimpleService, we actually get an agent type, that is, remote service agent, which is automatically generated according to the interface processed by @ RemoteService annotation. It implements the annotated interface and proxies the remote service Binder. Because the object returned to the service consumer is a proxy object, we can update the binding point of the specific remote service. This feature can be used for republishing remote services. As follows:

// Component A, publish IMusic as MusicService
SimpleService.publishRemoteService(IMusic::class.java, MusicService())

// Component B, get and use IMusic
val music = SimpleService.getRemoteServiceWait(IMusic::class.java, 5000)
music.play("Thousands of miles away")

// Component A, republish IMusic to MusicServiceNew
SimpleService.publishRemoteService(IMusic::class.java, MusicServiceNew())

// Component B can directly use the MusicServiceNew service without obtaining the IMusic again, and the direction of the remote service is automatically updated by SimpleService
music.play("Beautiful new world")

This feature can also perform disconnection reconnection when component A wants to actively update the service implementation and restart the publishing service after the process of component A exits abnormally.

Even if you want to implement this service update for local services, you can publish it through remote services.

2. Custom exception handling during remote service invocation

Since the remote service agent we returned to the user represents the access to the remote service Binder, it must deal with the call exceptions such as RemoteException. The current method is to let the upper layer implement the exception handling during the specific call through the IMethodErrorHandler interface, which is similar to the dynamic agent. Now, by default, the DefaultValueMethodErrorHandler provided by default is used for the processing of all remote service agents. The processor catches all exceptions and returns the default return value. If you want to customize the exception, for example, you can't eat some exceptions on demand, you need to throw them out again, or you need to do specific processing for some special types of exceptions, you can set the error handler in the following ways:

// Component B, the user registers the calling error handler of IMusic remote service (global effect in the same process)
SimpleService.registerMethodErrorHandler(IMusic::class.java, errorHandler))

3. Get the remote service Binder of a remote service agent

On the service user side, we sometimes want to get the remote service proxy of the remote service proxy object proxy, such as some callback parameters of an interface. When we want to use RemoteCallbackList for management (refer to RemoteServiceManagerImpl of the project source code), we need IInterface type objects, which can be done in the following ways:

// Component B, get service
val music = SimpleService.getRemoteServiceWait(IMusic::class.java, 5000)

// Component A wants to get the remote service Binder of the remote service agent object at A later time`
val musicBinder: IMusicBinder = SimpleService.getServiceRemoteInterface(IMusic::class.java, music)

ending

The above is the simple scheme to realize cross process component communication introduced this time. Have you got it!

Welcome to experience, welcome to ask questions, welcome to github and give me some praise!

github transfer

Topics: Android