In this morning’s group discussion on AIDL, a question occurred to me: when a client passes a non-primitive parameter object to a server through AIDL, and then changes the parameter on the client, will the server change as well?

Binder is a cross-process communication that synchronizes memory changes to the called party as they are called. Binder is not called when changing the properties of a data class. That’s what I thought at first, but when I dug deeper, I found something a little different.

The official documentation

When you see non-primitive parameters being passed, you should immediately think of the three tags in, out, and inout. Check the official documentation and the description is very simple

All non-primitive parameters require direction markers indicating where the data is going. Such tags can be in, out, or inout (see examples below). The default primitive is in, not any other direction.

There is only one useful clue to this question: the direction of the data.

So let’s take a bold guess. Does this data trend refer to the data flow within this parameter? If so, the data trend should look like this:

  • in: Parameters can flow from client to server
  • out: Parameters can flow from server to client
  • inout: Parameters can flow between the client and server

Passing parameters from client to server is easier to understand, but what exactly does it mean to pass parameters from server to client?

Write a Demo to verify:

The Demo code

JavaBean

public class Person implements Parcelable {
  private String name;
	private String age;

  public Person(a) {}public Person(String name, String age) {
    this.name = name;
		this.age = age;
  }

  protected Person(Parcel in) { name = in.readString(); age = in.readString(); }}Copy the code

AIDL:

interface IYaYaInterface {
  void setPersonIn(in Person person);
  void setPersonOut(out Person person);
  void setPersonInOut(inout Person person);
  void changePerson(a);
  void personChanged(a);
  Person getPerson(a);
}
Copy the code

Service:

class MyYaYa extends IYaYaInterface.Stub {

  @Override
  public void setPersonIn(Person person) throws RemoteException {
    Log.d(TAG, "setPersonIn1: " + person);
    mPerson = person;
    mPerson.setPrice("666666");
    Log.d(TAG, "setPersonIn2: " + person);
  }

  @Override
  public void setPersonOut(Person person) throws RemoteException {
    Log.d(TAG, "setPersonOut1: " + person);
    mPerson = person;
    mPerson.setPrice("666666");
    Log.d(TAG, "setPersonOut2: " + person);
  }

  @Override
  public void setPersonInOut(Person person) throws RemoteException {
    Log.d(TAG, "setPersonInOut1: " + person);
    mPerson = person;
    mPerson.setPrice("666666");
    Log.d(TAG, "setPersonInOut2: " + person);
  }

  @Override
  public void changePerson(a) throws RemoteException {
    mPerson.setName("CCCCCCC");
    Log.d(TAG, "changePerson: " + mPerson);
  }

  @Override
  public void personChanged(a) throws RemoteException {
    Log.d(TAG, "personChanged: " + mPerson);
  }

  @Override
  public Person getPerson(a) throws RemoteException {
    returnmPerson; }}Copy the code

Client:

findViewById(R.id.in).setOnClickListener(v -> {
  try {
    Person in = new Person("In"."1");
    Log.d(TAG, "in1: " + in);
    mService.setPersonIn(in);
    Log.d(TAG, "in2: " + in);
  } catch(RemoteException e) { e.printStackTrace(); }}); findViewById(R.id.out).setOnClickListener(v -> {try {
    Person out = new Person("Out"."1");
    Log.d(TAG, "out1: " + out);
    mService.setPersonOut(out);
    Log.d(TAG, "out2: " + out);
  } catch(RemoteException e) { e.printStackTrace(); }}); findViewById(R.id.inout).setOnClickListener(v -> {try {
    Person inOut = new Person("InOut"."1");
    Log.d(TAG, "inOut1: " + inOut);
    mService.setPersonInOut(inOut);
    Log.d(TAG, "inOut2: " + inOut);
  } catch(RemoteException e) { e.printStackTrace(); }});Copy the code

As you can see from the code, we define three methods that use in, out, and inout decorations, and then call these three methods on the client side. The server side modifies the properties of the object as soon as it receives the object. Let’s take a look at the Log running, and see where the data is going.

The Log to verify

The server Log

D/AIDLDebug: setPersonIn1: Person{name='In', age='1'}
D/AIDLDebug: setPersonIn2: Person{name='In', age='666666'}
D/AIDLDebug: setPersonOut1: Person{name='null', age='null'}
D/AIDLDebug: setPersonOut2: Person{name='null', age='666666'}
D/AIDLDebug: setPersonInOut1: Person{name='InOut', age='1'}
D/AIDLDebug: setPersonInOut2: Person{name='InOut', age='666666'}
Copy the code

As you can see, both in and Inout servers receive complete attributes, while out servers receive no attributes at all.

This verifies part of our hypothesis: In and Inout can pass attributes of a parameter from the client to the server, while out cannot pass attributes of a parameter from the client to the server.

Then verify that the data flows from the server to the client. We modify the properties of the parameter in the server to see if the object properties of the client follow the change:

The client

D/AIDLDebug: in1: Person{name='In', age='1'}
D/AIDLDebug: in2: Person{name='In', age='1'}
D/AIDLDebug: out1: Person{name='Out', age='1'}
D/AIDLDebug: out2: Person{name='null', age='666666'}
D/AIDLDebug: inOut1: Person{name='InOut', age='1'}
D/AIDLDebug: inOut2: Person{name='InOut', age='666666'}
Copy the code

Log confirms our suspicions that the object properties of IN have not changed at all, while both out and Inout have synchronized server changes.

At this point, our conjecture has been verified, and the final question remains: inout inout life cycle, that is, outside the scope of this function, does synchronization still work? Modify our code:

Validation lifecycle

Code changes

The client

findViewById(R.id.in).setOnClickListener(v -> {
  try {
    Person in = new Person("In"."1");
    Log.d(TAG, "in1: " + in);
    mService.setPersonIn(in);
    Log.d(TAG, "in2: " + in);
    in.setPrice("3");
    mService.personChanged();
  } catch(RemoteException e) { e.printStackTrace(); }}); findViewById(R.id.out).setOnClickListener(v -> {try {
    Person out = new Person("Out"."1");
    Log.d(TAG, "out1: " + out);
    mService.setPersonOut(out);
    mService.changePerson();
    Log.d(TAG, "out2: " + out);
  } catch(RemoteException e) { e.printStackTrace(); }});Copy the code

There are two changes:

  1. inThe modifier in thesetPersonIn()After that, the client modifies the object parameters itself and then callspersonChanged()Print objects on the server side
  2. outThe modifier in thesetPersonOut()After that, callchangePerson()Change the object properties on the server side and print the object on the client side

The Log to verify

The service side

D/AIDLDebug: setPersonIn1: Person{name='In', age='1'}
D/AIDLDebug: setPersonIn2: Person{name='In', age='666666'}
D/AIDLDebug: personChanged: Person{name='In', age='666666'}
D/AIDLDebug: setPersonOut1: Person{name='null', age='null'}
D/AIDLDebug: setPersonOut2: Person{name='null', age='666666'}
D/AIDLDebug: changePerson: Person{name='CCCCCCC', age='666666'}
Copy the code

The client

D/AIDLDebug: in1: Person{name='In', age='1'}
D/AIDLDebug: in2: Person{name='In', age='1'}
D/AIDLDebug: out1: Person{name='Out', age='1'}
D/AIDLDebug: out2: Person{name='null', age='666666'}
Copy the code

As you can see, the flow of data is not always maintained and will fail after leaving the scope of the corresponding function.

conclusion

Direction markers specify the flow of data within a parameter in cross-process communication:

  • in: Indicates that internal parameter data can only be transferred from the client to the server
  • out: Indicates that internal parameter data can only be transferred from the server to the client
  • inout: Indicates that internal data of a parameter can flow between the client and server

Moreover, the flow property is only valid in the scope of the function being modified, once out of the scope, the flow property is invalid.

Source:

To verify our conclusion with the source code, take a look at the generated AIDL Java file:

@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_setPersonIn:
    {
      data.enforceInterface(descriptor);
      // You can see that _arg0 is the argument passed in by the clientCom.yaya. server. Person _arg0;if ((0! =data.readInt())) { _arg0 = com.yaya. The service side. Person. The CREATOR. CreateFromParcel (data); }else {
        _arg0 = null;
      }
      // Pass the parameters to the server here
      this.setPersonIn(_arg0);
      reply.writeNoException();
      return true;
    }
    caseTRANSACTION_setPersonOut: { data.enforceInterface(descriptor); Com.yaya. server. Person _arg0;// Out does not take data from the client at all, but new a new object
      _arg0 = newCom.yaya. server. Person();// The new object is then passed to the server
      this.setPersonOut(_arg0);
      reply.writeNoException();
      // This is where the server object is rewritten to serialization, which can be sent back to the client. If a parameter is changed in the setPersonOut method, it is passed to the client here
      if((_arg0! =null)) {
        reply.writeInt(1);
        _arg0.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
      }
      else {
        reply.writeInt(0);
      }
      return true;
    }
    case TRANSACTION_setPersonInOut:
    {
      data.enforceInterface(descriptor);
      // Inout gets the parameters passed by the client hereCom.yaya. server. Person _arg0;if ((0! =data.readInt())) { _arg0 = com.yaya. The service side. Person. The CREATOR. CreateFromParcel (data); }else {
        _arg0 = null;
      }
      // Pass it here to the server
      this.setPersonInOut(_arg0);
      reply.writeNoException();
      // Write back to the client here
      if((_arg0! =null)) {
        reply.writeInt(1);
        _arg0.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
      }
      else {
        reply.writeInt(0);
      }
      return true;
    }
    case TRANSACTION_changePerson:
    {
      data.enforceInterface(descriptor);
      this.changePerson();
      reply.writeNoException();
      return true;
    }
    case TRANSACTION_personChanged:
    {
      data.enforceInterface(descriptor);
      this.personChanged();
      reply.writeNoException();
      return true;
    }
    caseTRANSACTION_getPerson: { data.enforceInterface(descriptor); Com.yaya. server. Person _result =this.getPerson();
      reply.writeNoException();
      if((_result! =null)) {
        reply.writeInt(1);
        _result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
      }
      else {
        reply.writeInt(0);
      }
      return true;
    }
    default:
    {
      return super.onTransact(code, data, reply, flags); }}}Copy the code

Looking at the comments in the source code, we can verify our conjecture at the code level and make it clear why it flows only within the scope of the function.

Note:

If you try it on Android 11, you’ll probably find that the Service can’t be bind at all:

ActivityManager: Unable to start service Intent { act=com.yaya.server cmp=com.yaya.server/.YaYaService } U=0: not found
Copy the code

This is because Android 11 has updated its privacy policy so that packages are not visible to each other by default.