“This article has participated in the good article call order activity, click to see: back end, big front end double track submission, 20,000 yuan prize pool for you to challenge!”

background

In bytedance developer ecosystem projects, there is a lot of IPC communication. To avoid having to write AIDL every time you write an IPC call, the complexity of corrections and the speed of compilation is a burden for users. In order to make it as easy for developers to use IPC as to call local objects, an AIDL-based IPC communication framework was developed.

features

  • Replace the AIDL interface with a normal Java interface
  • Generate an IPC implementation of the remote service interface like Retrofit
  • You can extend CallAdapter yourself to support Rxjava in a retrofit-like manner
  • Supported Call Adapters: Call
  • Support the remote service Callback mechanism IPC Callback
  • All AIDL data types are supported
  • Support AIDL for all data orientation tags (default: in) : in, Out, inOUT
  • Support the AIDL Oneway keyword
  • Support for in-process calls when demoted to local calls
  • When IPC Callback is supported, a Callback notification is made to the void {methodName}() method annotated with @connecterror if a Binder dies
  • Automatic registration and deregistration of IPC Callback objects is supported

BdpIPC supports all AIDL data types

  • All primitive types in the Java language (e.g. Int, long, char, Boolean, etc.)
  • String
  • CharSequence
  • Parcelable
  • List (All elements in the List must be supported data types in this List)
  • Map (All elements in the Map must be supported data types in this list)
  • JSONObject and JSONArrayType are also supported
  • You don’t need to implement Parcelable serialization for types that are already supported, but you do need to implement Parcelable serialization for those that are not explicitly supported
  • If there are additional requirements, you can modify the source code to expand

use

In the client App, create the BdpIPC object through the Builder, and generate an IPC implementation of the remote interface of IRemoteService through the create() method

.packageName(*REMOTE_SERVICE_PKG*) .action(*REMOTE_SERVICE_ACTION*) //.usedProvider(getPackageName() + "." + RemoteProvider.URI_SUFFIX) // Specify the callback executor by yourself / / addCallAdapterFactory (OriginalCallAdapterFactory. Create (callbackExecutor)) / / Basic MainThread callback / / Settings specified Callback < T > () in the thread. AddCallAdapterFactory (OriginalCallAdapterFactory. * the create * (new Executor () {@ Override public void execute(Runnable command) { BdpThreadUtil.*runOnWorkThread*(command); }})) // Set the thread where the request is sent. Dispatcher(new IDispatcher() {@override public void enqueue(Runnable Task) { BdpThreadUtil.*runOnWorkThread*(task); } }) .addInterceptor(new CacheInterceptor()) // Basic //.addCallAdapterFactory(OriginalCallAdapterFactory.create()) .build();Copy the code

Now all the methods in the mRemoteService object are IPC methods and can be called directly

int pid = mRemoteService.getPid(); MRemoteService. BasicTypes (1, 2 l, true, 3.0 f, 4.0 d, "STR");Copy the code

Use the advanced

Call Adapters

In the client App, you can copy and modify the remote service interface, wrapping the return value of the method

@RemoteInterface

public interface IRemoteService {

         

    Call<Integer> getPid();



    Call<Void> basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, 

        double aDouble, String aString);

}
Copy the code

Register the corresponding Call Adapter Factory in bdPIPC. Builder, and the rest of the steps are basically the same as Retrofit.

new BdpIPC.Builder(this)

        ...

        .addCallAdapterFactory(OriginalCallAdapterFactory.create()) // Basic

        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())  // RxJava2 

        .build();
Copy the code

Call the IPC

Call<Integer> call = mRemoteTask.remoteCalculate(10, 20); // Synchronous Request // int result = call.execute(); // Log.d("IPCDemoActivity", "remoteCalculate() result:" + result); // Async Request call.enqueue(new Callback<Integer>() { @Override public void onResponse(Call<Integer> call, Integer response) { Toast.makeText(IPCDemoActivity.this, "remoteCalculate() onResponse: " + response, Toast.LENGTH_SHORT).show(); } @Override public void onFailure(Call<Integer> call, Throwable t) { Toast.makeText(IPCDemoActivity.this, "remoteCalculate() failure: " + t.getMessage(), Toast.LENGTH_SHORT).show(); }});Copy the code

Handle remote service interface callbacks using @callback

Modify the remote service callback interface IRemoteCallback with the @remoteInterface annotation

@RemoteInterface
public interface IRemoteCallback {



    void onValueChange(int value);

}
Copy the code

The Callback parameter is decorated with the @callback annotation in the remote method

void registerCallback(@Callback IRemoteCallback callback);
Copy the code

Callback objects are registered internally through dynamic proxies, and automatic deregistration is supported.

Specifies the data orientation TAG

You can specify @in, @Out, or @inout annotations for remote method arguments, which mark the flow of data through the underlying Binder, as used In AIDL

void directionalMethod(@In KeyEvent event, @Out int[] arr, @Inout Rect rect);
Copy the code
Note that all non-primitive types must specify the data orientation tag: '@in', '@out', or '@inout', to mark the direction of the data. Primitive types default to '@in' and cannot specify other values. Parcelable arguments that use '@out' or '@inout' must implement the 'SuperParcelable' interface, otherwise you must manually add this method 'public void readFromParcel(Parcel in)'.Copy the code

Use the @oneway annotation

You can use the @oneway annotation on remote methods, which will modify the behavior of remote method calls. When used, the remote method call is not blocked; it simply sends data and returns it immediately, as in AIDL

@OneWay

void onewayMethod(String msg);
Copy the code

Use the @Connecterror annotation

You can use the @Connecterror annotation on a method of the remote Callback object, which will make the remote method listen

Callbacks for connections broken due to Binder’s death

Using the @ RemoteInterface (target = IpcServiceInterfaceImpl. Class)

You can use this to achieve automatic registration on the Server side.

The idea is to generate an instance of the object by reflecting the name of the class (reflecting on the first call)

Here are some examples:

Specify the thread of execution (Callback)<T>– Callback after execution and Disptacher- thread that sent the request)

Currently, you cannot specify the thread in which the remote implementation class resides. Binder thread is the default

mBdpIPC = new BdpIPC.Builder(this) .packageName(REMOTE_SERVICE_PKG) .action(REMOTE_SERVICE_ACTION) //.usedProvider(getPackageName() + "." + RemoteProvider.URI_SUFFIX) // Specify the callback executor by yourself / / addCallAdapterFactory (OriginalCallAdapterFactory. Create (callbackExecutor)) / / Basic MainThread callback / / Settings specified Callback < T > () in the thread. AddCallAdapterFactory (OriginalCallAdapterFactory. Create (new Executor () {@ Override public void execute(Runnable command) { BdpThreadUtil.runOnWorkThread(command); }})) // Set the thread where the request is sent. Dispatcher(new IDispatcher() {@override public void enqueue(Runnable Task) { BdpThreadUtil.runOnWorkThread(task); } }) .addInterceptor(new CacheInterceptor()) // Basic //.addCallAdapterFactory(OriginalCallAdapterFactory.create()) .build();Copy the code

performance

Because of the use of dynamic proxies, overall, the performance penalty is still around 20% compared to native calls. However, in contrast, the cost of using Parcelable is reduced, and the transfer of data using Parcelable is generally less costly than other third-party frameworks (using JSON strings and converting to objects), and the scalability and complexity of the code is much lower

If 1000 simple IPC communications are repeated 3 times 1000 IPC, the average value is taken

Online use

BdpIPC is currently in steady use in developer Ecology projects. If there is a scenario requirements of the business, welcome to discuss.

Design principle

  • Dynamic proxies cause the final implementation of a normal interface to follow a binder’s invocation
  • References to Okhttp’s Intercept mode allow for rapid scaling of interception, such as adding in-memory caching, or other uniform processing
  • Reference to Retrofit’s CallAdapter makes it possible to support the Call mode, even Rxjava
  • – phantom references (virtual references) are referenced so that object reclamation across processes can be supported synchronously (manually, of course)
  • This framework is not a complete re-creation, absorbing the design approach of many open source projects, thanks to open source

Flow chart of the call

The whole process

1. Initialization process I. The local object obtains the remote Binder object (ITransfer) in Service mode and registers the Callback Binder (ICallback) object ii with the remote Binder object (ITransfer). The remote Service registers the ServiceImplObject(the remote Service interface implementation object) using BdpIPCBinderImpl(the remote object that holds an Invoker object internally).Iii. The registered ServiceImplObject turns into MethodInvoker (Key based on class name and method name) and is stored in the remote Invoker object (automatic registration is added later, no manual registration is required).

  1. The local object calls the method localCallback(Callback Callback). Note: this method is user-owned, not in the library
  2. The method after the hook is called by the local remote proxy object (dynamic proxy). ,BdpIPC registers the callback object (resolved to MethodInvoker object) with the local Invoker object and stores it inside the Invoker
  3. Hook method is parsed into Request object and wrapped into RemoteCall object for Request processing. A Request is sent with binder

5. Remote Invoker object discovery is a method with callback interface parameters. When the method is implemented, the dynamic proxy object implements the implementation of the interface object, and BdpCallbackGc is used for GC listening

6. The remote object’s interface is called and a Callback request is sent using the previously registered Callback BInder (locally registered to the remote Callback BInder (ICallback)). Wrap the Request as the previous Request logic and send the Request with the binder inside RemoteCall7. The CallbackBinder (ICallback) object generated by the local process receives the Response Callback (Request Request), and uses the local Invoker to process the Request. Find a MethodInvoker with the corresponding key in the container that was previously registered with the local Invoker, and perform the callback request handling 8. The local process fires the callback callback. 9. If the remote BdpCallbackGc is listening to an object that needs to be Gc, call the previously registered Callback BInder (locally registered to the remote Callback BInder (ICallback)) and send a Gc process. The local Invoker receives a MethodInvoker object that needs to be processed by GC to prevent the local Invoker object from holding too many temporary callback objects

Important Class Description

BdpCallbackGc The remote process listens for temporary callback objects, and once there is a GC, it triggers a message to the local process to explicitly manipulate the Calllback response objects in the Invoker

BdpIPC does the generation of local remote proxy objects, the acquisition of remote Binder objects and the registration of local callback objects

Invoker the actual method implementor, both local and remote

At remote: Internally holds the method responder for each remote service interface implementation object (Remote service interface implementation object parsing MethodInvoker)

Local: internally holds the method responder of the callback object (the MethodInvoker generated by the callback object resolution)

The true implementor of the proxy object generated by RemoteCall BdpIPC, the internally constructed Request object uses ICallback to make the Request

ITransfer The Binder object that executes the request

ICallback remote obtains the binder object from the local process that was registered when the local process was initialized, and uses the binder object to perform the Callback

Overall UML diagram