Cross-process communication of Service

Imagine such a scenario, we have 2 APPs (or 2 processes, either), one of the APPs needs to provide a Person-related service (the service has an interface named eat), we call it PersonServer; the other APP Need to access the services provided by PersonServer, we call it Client. That is to say, there are now two APPs, one as the server side, providing services, and the other as the client side, using services.

Let's see how to implement it in Android?

PersonServer side implementation

PersonServer is the provider of the service. We first need to create a Service to provide the service, and the service needs to have the ability to communicate across processes so that the Client can call.

Implement AIDL

Cross-process, our first thought is to use AIDL to implement the cross-process capability of the interface.

First we need to define an AIDL interface called IPerson and provide the eat method:

package com.testaidl;
interface IPerson {
    boolean eat(String food);
}

We see that the implementation of IPerson is very simple. We use IDE to compile it, and the compiler will generate a file named IPerson.java, which is the java code corresponding to aidl.

Implement Service

Next we define a Service:

public class PersionService extends Service
{
    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return super.onStartCommand(intent, flags, startId);
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new IPerson.Stub() {
            @Override
            public boolean eat(String food) throws RemoteException {
                Log.d("PersionService", "this is Server");
                Log.d("PersionService", "Persion eat " + food);
                return true;
            }
        };
    }
}

The focus here is on the onBind method, which returns an IBinder object, which is the IPerson.Stub generated class of IPerson.aidl we defined earlier. The IPerson.Stub class has an abstract method eat() that needs to be implemented here, which is The concrete implementation of the actions performed by the remote service provided by this service.

Don't forget to register in AndroidManifest:

 <service android:name=".PersionService">
            <intent-filter>
                <action android:name="com.example.simpledemo.PersionService"/>
            </intent-filter>
        </service>

Client

The server side is ready, so how does the client side access the services provided by PersonServer?

Let's look at the implementation of the client side:

public class MainActivity extends AppCompatActivity {
    private IPerson mService = null;
    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) { //绑定Service成功后执行
            mService = IPerson.Stub.asInterface(iBinder);//获取远程服务的代理对象,一个IBinder对象
            try {
                mService.eat("banana"); //调用远程服务的接口。
            } catch (RemoteException e) {
                e.printStackTrace();
            }

        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {
            mService = null;
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button bindService = findViewById(R.id.bind_service);
        bindService.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //调用bindService发送绑定远程服务的请求
                Intent intent = new Intent();
                intent.setAction("com.example.simpledemo.PersionService");
                intent.setPackage("com.example.simpledemo");
                bindService(intent, mConnection, BIND_AUTO_CREATE);
            }
        });
    }
}

Step analysis:

  • Call bindService to send a request to bind a remote service.
  • The ServiceConnection object executes the callback function onServiceConnected after binding the Service.
  • In onServiceConnected, get the proxy object of the remote service, an IBinder object.
  • Finally, through the remote service proxy object obtained in the previous step, the interface of the remote service is called.

Notice

Don't forget to place the IPerson.aidl file in the same location on the client side, with the same package name and file name .

Service/AIDL remote call procedure analysis

In Serivce's remote communication process, the most critical point is AIDL, which undertakes the task of remote communication access. Of course, its low-level implementation is also achieved through the Binder mechanism , but compared with Binder, AIDL simplifies our use.

IPerson.aidl

Let's first look at IPerson.aidl:

package com.testaidl;
interface IPerson {
    boolean eat(String food);
}

Its implementation is very simple, so how does it realize the ability of remote communication?

We know that after the aidl file is compiled by the compiler, a .java file will be generated, and this java file is the place where the logic code of remote communication implementation is really stored.

IPerson.java:

package com.testaidl;
// Declare any non-default types here with import statements

public interface IPerson extends android.os.IInterface
{
  /** Local-side IPC implementation stub class. */
  public static abstract class Stub extends android.os.Binder implements com.testaidl.IPerson
  {
    private static final java.lang.String DESCRIPTOR = "com.testaidl.IPerson";
    /** Construct the stub at attach it to the interface. */
    public Stub()
    {
      this.attachInterface(this, DESCRIPTOR);
    }
    /**
     * Cast an IBinder object into an com.testaidl.IPerson interface,
     * generating a proxy if needed.
     */
    public static com.testaidl.IPerson asInterface(android.os.IBinder obj)
    {
      if ((obj==null)) {
        return null;
      }
      android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
      if (((iin!=null)&&(iin instanceof com.testaidl.IPerson))) {
        return ((com.testaidl.IPerson)iin);
      }
      return new com.testaidl.IPerson.Stub.Proxy(obj);
    }
    @Override public android.os.IBinder asBinder()
    {
      return this;
    }
    @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
    {
      java.lang.String descriptor = DESCRIPTOR;
      switch (code)
      {
        case INTERFACE_TRANSACTION:
        {
          reply.writeString(descriptor);
          return true;
        }
        case TRANSACTION_eat:
        {
          data.enforceInterface(descriptor);
          java.lang.String _arg0;
          _arg0 = data.readString();
          boolean _result = this.eat(_arg0);
          reply.writeNoException();
          reply.writeInt(((_result)?(1):(0)));
          return true;
        }
        default:
        {
          return super.onTransact(code, data, reply, flags);
        }
      }
    }
    private static class Proxy implements com.testaidl.IPerson
    {
      private android.os.IBinder mRemote;
      Proxy(android.os.IBinder remote)
      {
        mRemote = remote;
      }
      @Override public android.os.IBinder asBinder()
      {
        return mRemote;
      }
      public java.lang.String getInterfaceDescriptor()
      {
        return DESCRIPTOR;
      }
      @Override public boolean eat(java.lang.String food) throws android.os.RemoteException
      {
        android.os.Parcel _data = android.os.Parcel.obtain();
        android.os.Parcel _reply = android.os.Parcel.obtain();
        boolean _result;
        try {
          _data.writeInterfaceToken(DESCRIPTOR);
          _data.writeString(food);
          boolean _status = mRemote.transact(Stub.TRANSACTION_eat, _data, _reply, 0);
          if (!_status && getDefaultImpl() != null) {
            return getDefaultImpl().eat(food);
          }
          _reply.readException();
          _result = (0!=_reply.readInt());
        }
        finally {
          _reply.recycle();
          _data.recycle();
        }
        return _result;
      }
      public static com.testaidl.IPerson sDefaultImpl;
    }
    static final int TRANSACTION_eat = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
    public static boolean setDefaultImpl(com.testaidl.IPerson impl) {
      if (Stub.Proxy.sDefaultImpl == null && impl != null) {
        Stub.Proxy.sDefaultImpl = impl;
        return true;
      }
      return false;
    }
    public static com.testaidl.IPerson getDefaultImpl() {
      return Stub.Proxy.sDefaultImpl;
    }
  }
  public boolean eat(java.lang.String food) throws android.os.RemoteException;
}

When we defined aidl, we only used a few lines of code, and there is still a lot of code in the generated java class.

Let's first look at its class structure:

 Its structure can be divided into the following parts;

  1. Interface IInterface.
  2. Abstract method eat.
  3. Inner class Stub.
  4. Stub's inner class Proxy.

We analyze these components separately.

1. Interface IInterface

Interface android.os.IInterface:

public interface IInterface
{
    /**
     * Retrieve the Binder object associated with this interface.
     * You must use this instead of a plain cast, so that proxy objects
     * can return the correct result.
     */
    public IBinder asBinder();
}

Here is just a method that must be implemented asBinder(), which returns the IBinder object associated with the IInterface interface.

In IPerson.java, the asBinder method is still an abstract method and does not need to be implemented. The main function of IPerson is to provide an abstract class for the interface, and the real implementation is in its subclass Stub.

2. Abstract method eat

This is an abstract method to be implemented, which is ultimately implemented by the caller on the server side. In this example, it is implemented in PersionService:

@Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new IPerson.Stub() {
            @Override
            public boolean eat(String food) throws RemoteException {
                Log.d("PersionService", "this is Server");
                Log.d("PersionService", "Persion eat " + food);
                return true;
            }
        };
    }

3. Inner class Stub

This inner class is the main logic for implementing IPC, and all external use is usually realized by calling it.

The Stub class inherits the Binder class and implements the IPerson interface.

Construction method

The instance object of Stub is created on the PersonServer side, that is, it is instantiated on the Server side.

Its constructor:

private static final java.lang.String DESCRIPTOR = "com.testaidl.IPerson";
    /** Construct the stub at attach it to the interface. */
    public Stub()
    {
      this.attachInterface(this, DESCRIPTOR);
    }

The attachInterface method of the parent class Binder is called directly:

  public void attachInterface(@Nullable IInterface owner, @Nullable String descriptor) {
        mOwner = owner;
        mDescriptor = descriptor;
    }

Here, the current object and a description string are assigned to the corresponding variables for use in future searches.

asInterface method

The asInterface method returns an IPerson object. When the client side obtains the service object, this method is called:

mService = IPerson.Stub.asInterface(iBinder);

The asInterface method is called on the client side, and it returns a proxy object of the PersonServer service on the client side.

Let's look at its logical implementation:

public static com.testaidl.IPerson asInterface(android.os.IBinder obj)
    {
      if ((obj==null)) {
        return null;
      }
      android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
      if (((iin!=null)&&(iin instanceof com.testaidl.IPerson))) {
        return ((com.testaidl.IPerson)iin);
      }
      return new com.testaidl.IPerson.Stub.Proxy(obj);
    }

Logical analysis:

  • Returns null if the parameter obj is null. obj is an IBinder object, and obj is the parameter of the onServiceConnected method, which represents the return parameter after the connection service is successful. If it is the same process, the object is the instance of the IPerson service. If it is cross-process, it is IPerson in the Client. Remote proxy object.
  • If the parameter obj is not empty, try to obtain an instance of the IPerson service object through obj.queryLocalInterface(DESCRIPTOR), and return the instance directly if successful. Not really cross-process here.
  • Finally, create a proxy object for the IPersion service (the proxy object on the client side) IPerson.Stub.Proxy, and then return it as a proxy reference for IPersion when communicating across processes.

Implementation of queryLocalInterface

The queryLocalInterface method is used to query whether there is a corresponding service in the current process. If there is a corresponding service in the current process, it means that the process is called, and it does not involve cross-process, and the corresponding service reference can be returned directly.

public @Nullable IInterface queryLocalInterface(@NonNull String descriptor) {
        if (mDescriptor != null && mDescriptor.equals(descriptor)) {
            return mOwner;
        }
        return null;
    }

mOwner is assigned in the construction method of Stub. If both the PersonServer service provider and the Client service user are in the same process, the mOwner must not be empty.

Next, the cross-process situation is left, let's analyze the inner class of Proxy.

4. Stub's inner class Proxy

The Proxy class is created on the client side as a proxy class on the client side of the remote service side to assist in accessing remote services. The Proxy class implements the IPerson interface.

Proxy constructor

private android.os.IBinder mRemote;
      Proxy(android.os.IBinder remote)
      {
        mRemote = remote;
      }

Saved here, the IBinder object of the remote server passed by the onServiceConnected method after the service connection is successful.

eat method

When the Client calls the eat method of IPerson through the Proxy object, the eat method will use the remote IBinder object saved when the Proxy is created to make a remote call.

static final int TRANSACTION_eat = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
    
      @Override public boolean eat(java.lang.String food) throws android.os.RemoteException
      {
        android.os.Parcel _data = android.os.Parcel.obtain();
        android.os.Parcel _reply = android.os.Parcel.obtain();
        boolean _result;
        try {
          _data.writeInterfaceToken(DESCRIPTOR);
          _data.writeString(food);
          boolean _status = mRemote.transact(Stub.TRANSACTION_eat, _data, _reply, 0);
          if (!_status && getDefaultImpl() != null) {
            return getDefaultImpl().eat(food);
          }
          _reply.readException();
          _result = (0!=_reply.readInt());
        }
        finally {
          _reply.recycle();
          _data.recycle();
        }
        return _result;
      }
      public static com.testaidl.IPerson sDefaultImpl;
    }

The TRANSACTION_eat value will be used when calling across processes, so pay attention here.

Logical analysis:

  • Create a Parcel as the input parameter of the eat remote method.
  • Create a Parcel as the return value of the eat remote method.
  • Call the transact() method of the remote IBinder object (which is actually a proxy object, but we can regard it as a remote IBinder object), and pass the unique identifier, parameters, return value reference of the remote method eat, and whether to synchronize several objects as parameters to the transact() method.
  • After the remote call is completed, obtain the call result, handle errors and other information, and finally take out the return value of the remote method and return directly.

Remote call procedure analysis

After the above analysis, we basically understand the whole process of remote service invocation, but here, there is another point that is implemented inside the system, that is, the processing process of remote invocation of transact and other related logic. We will analyze it in detail next.

boolean _status = mRemote.transact(Stub.TRANSACTION_eat, _data, _reply, 0);

Call the transact method of the remote proxy object, which is implemented in the Binder class. The implementation logic is as follows:

android.os.Binder

public final boolean transact(int code, @NonNull Parcel data, @Nullable Parcel reply,
            int flags) throws RemoteException {
        if (false) Log.v("Binder", "Transact: " + code + " to " + this);

        if (data != null) {
            data.setDataPosition(0);
        }
        boolean r = onTransact(code, data, reply, flags);
        if (reply != null) {
            reply.setDataPosition(0);
        }
        return r;
    }

At this point, the current calling process has been transferred from the Client side to the PersonServer side, and the conversion process is implemented by the bottom layer of Binder. Here, it can be simply considered as a remote call.

The onTransact method is called here for processing:
com.testaidl.IPerson#Stub

 @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
    {
      java.lang.String descriptor = DESCRIPTOR;
      switch (code)
      {
        case INTERFACE_TRANSACTION:
        {
          reply.writeString(descriptor);
          return true;
        }
        case TRANSACTION_eat:
        {
          data.enforceInterface(descriptor);
          java.lang.String _arg0;
          _arg0 = data.readString();
          boolean _result = this.eat(_arg0);
          reply.writeNoException();
          reply.writeInt(((_result)?(1):(0)));
          return true;
        }
        default:
        {
          return super.onTransact(code, data, reply, flags);
        }
      }
    }

Here, the parameters sent by the client are read, and the eat method of the real IPerson is called, and the result is finally returned.

eat implementation code:
com.example.simpledemo.PersionService

public IBinder onBind(Intent intent) {
        return new IPerson.Stub() {
            @Override
            public boolean eat(String food) throws RemoteException {
                Log.d("PersionService", "this is Server");
                Log.d("PersionService", "Persion eat " + food);
                return true;
            }
        };
    }

The real remote interface implementation is the IPerson.Stub object in the PersionService class.

summary

From a practical point of view, this paper analyzes in detail the specific implementation of cross-process invocation of Service, as well as the invocation process and principle analysis.

The calling process can be summarized as:

1.Service providing services across processes needs to be implemented through AIDL (the bottom layer actually uses Binder).
2. The client side binds a remote Service through PersionService.
3. After the binding is successful, the Client obtains the local IBinder proxy object of the remote service through the onServiceConnected method of the ServiceConnection.
4. The client obtains the proxy object of the service interface (IPerson) on the client side through the remote service IBinder proxy object (if it is the same process, it directly returns the IBidner service object).
5. The client calls the eat method of the remote proxy object, and the bottom layer is driven by the Binder and calls the transact method on the server side.
6. The transact method on the server side calls the onTransact method of IPerson.Stub.
7. The onTransact method on the Server side calls the final implementation of the eat method - the IPerson.Stub object created in the PersionService class.

Guess you like

Origin blog.csdn.net/ahou2468/article/details/122578120