[Video transcript] Vehicle Android application development and analysis - AIDL practice and packaging (Part 2)

This video address: https://www.bilibili.com/video/BV1zh4y1x7KE/

The previous video explained the simple use of AIDL and 5 problems that may be encountered when using AIDL. In this video, we will continue to explain the remaining 5 problems.

"1. AIDL Advanced"

Question 6: The "server" initiates a callback to the "client"

In the examples in the previous section, we all introduced how the "client" sends requests to the "server". In actual development, there will also be situations where the "server" needs to actively initiate a request to the "client". At this time, we need to save a Binder instance of the "client" on the "server". When needed, the "server" can make requests to the "client" through this Binder.

The specific operations are as follows:

1) Create a new ICalculatorListener.aidl file and define the required methods.

// ICalculatorListener.aidl
package com.wj.sdk.listener;

interface ICalculatorListener {

   void callback(String result);

}

2) Define the corresponding registration and deregistration methods in ICalculator.aidl.

package com.wj.sdk;

// 引入这个listener
import com.wj.sdk.listener.ICalculatorListener;

interface ICalculator {
    ...
    oneway void registerListener(ICalculatorListener listener);
    oneway void unregisterListener(ICalculatorListener listener);
}

Since a "server" may connect to multiple "clients" at the same time, for the Binder instance registered by the "client", we need to use a List collection to save it. If you use or save the Binder instance of the " ArrayListclient CopyOnWriteArrayList" , the saved Binder needs to be cleared when the connection between the "client" and the "server" is disconnected. If you call a Binder that has been disconnected, it will be thrown DeadObjectException.

If you need to monitor whether the "client" disconnects on the "server", you can use linkToDeath, as shown below:

@Override
public void registerListener(final ICalculatorListener listener) throws RemoteException {
    final Binder binder = listener.asBinder();
    Binder.DeathRecipient deathRecipient = new DeathRecipient() {
        @Override
        public void binderDied() {
        // 从集合中移除存在的Binder实例。
        }
    };
    binder.linkToDeath(deathRecipient, 0);
}

However, here we recommend using RemoteCallbackListthe Binder instance to save the "client".

Issue 7: Avoid DeadObjectException

RemoteCallbackListIs a class that manages a set of registered IInterface callbacks and automatically cleans them from the list when their process disappears. RemoteCallbackList is usually used to execute callbacks from Service to its client to achieve cross-process communication.

RemoteCallbackList has the following advantages:

  1. It identifies each registered interface based on the underlying unique Binder by calling the IInterface.asBinder() method.
  2. It attaches an IBinder.DeathRecipient to each registered interface so that if the process in which the interface is located dies, it can be cleared from the list.
  3. It locks the underlying interface list to cope with concurrent calls from multiple threads, and provides a thread-safe way to traverse snapshots of the list without holding a lock.

To use this class, create an instance and call its register(E) and unregister(E) methods to register and unregister the service as a client. To call back to a registered client, use the beginBroadcast(), getBroadcastItem(int), and finishBroadcast() methods.

Here are some RemoteCallbackListcode examples used:

    private RemoteCallbackList<ICalculatorListener> mCallbackList = new RemoteCallbackList<>();

    @Override
    public void registerListener(final ICalculatorListener listener) throws RemoteException {
        Log.i(TAG, "registerListener: " + Thread.currentThread().getName());
        mCallbackList.register(listener);
    }

    @Override
    public void unregisterListener(final ICalculatorListener listener) throws RemoteException {
        Log.i(TAG, "unregisterListener: " + Thread.currentThread().getName());
        mCallbackList.unregister(listener);
    }

Then we can RemoteCallbackListinitiate a request to the client through the "client" Binder saved in.

// 向客户端发送消息
private synchronized void notifyToClient() {
    Log.i(TAG, "notifyToClient");
    int n = mCallbackList.beginBroadcast();
    for (int i = 0; i < n; i++) {
        try {
            mCallbackList.getBroadcastItem(i).callback(i + "--");
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
    mCallbackList.finishBroadcast();
}

Question 8: There are multiple Binders on the server side

In the above example, we only defined one Binder instance, ICalculator. When the "client" needs to perform various business interactions with the "server", it is necessary to implement multiple different Binder instances on the "server". This is We can introduce the BinderPool mechanism to optimize this scenario.

BinderPoolIt is a mechanism for managing and distributing Binder. It allows different modules to communicate with Binder through a unified Service. The client connects to the server through a Binder, and then obtains the corresponding Binder according to different business needs. instance to achieve cross-process communication. This can reduce the number of connections between the client and the server and improve performance and stability.

The specific usage of BinderPool is as follows:

1) Define an AIDL interface to describe the functions of BinderPool.

Includes a queryBinder method to return different Binder instances according to different types.

package com.wj.sdk;

interface ICalculator {
  ...
  Binder queryBinder(int type);
}

2) Implement this AIDL interface and return the corresponding Binder instance according to the code in the queryBinder method.

These Binder instances are generally implementation classes of other AIDL interfaces. In order to avoid creating a Binder instance for every request, we can cache these created Binder instances in the list and take them out directly when using them.

private final SparseArray<IBinder> mCache = new SparseArray<>();

@Override
public IBinder queryBinder(final int type) throws RemoteException {
    IBinder binder = mCache.get(type);
    if (binder != null) {
        return binder;
    }

    switch (type) {
        case 1:
            binder = new MyHavc();
            break;
        case 2:
            binder = new MyVehicle();
            break;
    }
    mCache.put(type, binder);
    return binder;
}

3) Create a Service class, inherit from Service, override the onBind method, and return the BinderPool instance implemented in the previous step.

@Override
public IBinder onBind(Intent intent) {
    if (mCalculatorBinder == null) {
        mCalculatorBinder = new CalculatorBinder(this);
    }
    return mCalculatorBinder;
}

4) "Client", first bind to the Service through the bindService method and obtain the instance, then call the method to obtain the required Binder instance, and then call its method to implement the function.BinderPool queryBinder

// 其它方法省略

public static final int TYPE_HAVC = 1;
public static final int TYPE_VEHICLE = 2;

// 问题7 - Binder连接池
private void callBinderPool() {
    try {
        IBinder binder = mCalculator.queryBinder(TYPE_HAVC);
        IHvac hvac = IHvac.Stub.asInterface(binder);
        // Hvac 提供的aidl接口
        hvac.basicTypes(1, 2, true, 3.0f, 4.0, "5");

        binder = mCalculator.queryBinder(TYPE_VEHICLE);
        IVehicle vehicle = IVehicle.Stub.asInterface(binder);
        // Vehicle 提供的aidl接口
        vehicle.basicTypes(1, 2, true, 3.0f, 4.0, "5");
    } catch (RemoteException exception) {
        Log.i(TAG, "callBinderPool: " + exception);
    }
}

Question 9: AIDL permission control

  • Control the binding permissions of the "client"

When exposing the AIDL interface to the outside world, we do not want all "clients" to be able to connect to the Service. Then we can customize permissions and restrict applications with specified permissions to bind to the "server".

1) In the "server" AndroidManifest.xml, customize a permission

In the Service manifest file, add an android:permission attribute and specify a custom permission name. In this way, only clients with this permission can bind to this Service. For example, you can write:

<permission
    android:name="com.example.permission.BIND_MY_SERVICE"
    android:protectionLevel="signature" />

Among them, protectionLevel has the following types:

  1. normal : Default value, indicating low-risk permissions. The system will automatically grant the requested application without user consent.
  2. dangerous : Indicates high-risk permissions involving user private data or device control. The system will display it to the user and confirm whether to grant the requested application.
  3. signature : Indicates that the system will grant permissions only if the requesting application and the application declaring the permission are signed with the same certificate.
  4. signatureOrSystem : Indicates a permission that the system will grant only if the requesting app and the app claiming the permission are signed with the same certificate, or the requesting app is located in a private folder of the system image.

This parameter is deprecated in API level 23, it is recommended to use signature.

2) Specify the required permissions in the Service tag of "Server" AndroidManifest.xml

<service android:name=".MyService"
         android:permission="com.example.permission.BIND_MY_SERVICE">
    ...
</service>

At this time, the "client" must declare com.example.permission.BIND_MY_SERVICEpermissions whether it is startService or bindService.

3) Finally, add a label to the "client" manifest file to declare the use of this permission.

<uses-permission android:name="com.example.permission.BIND_MY_SERVICE" />
  • Control the usage rights of the "client" AIDL interface

In addition to controlling the permissions to connect to the Service, most of the time we also need to control the request permissions of the aidl interface to prevent the "client" from freely accessing some dangerous aidl interfaces 1) In the "server" AndroidManifest.xml, customize the interface permissions

<permission android:name="com.example.aidl.ServerService2"
    android:protectionLevel="signature" />

2) Define a new AIDL interface

interface ICalculator {
 
  oneway void optionPermission1(int i);
 
 }

3) Register permissions in the "Client" list and call the remote interface

<uses-permission android:name="com.example.aidl.ServerService2"/>
@RequiresPermission(PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
private void callPermission() {
    try {
        if (checkPermission()) {
            Log.i(TAG, "callPermission: 有权限");
            mCalculator.optionPermission(1);
        } else {
            Log.i(TAG, "callPermission: 没有权限");
        }
    } catch (RemoteException exception) {
        Log.i(TAG, "callPermission: " + exception);
    }
}

/**
 * 检查应用自身是否有权限
 * @return true 有权限,false 没有权限
 */
private boolean checkPermission() {
    return checkSelfPermission(PERMISSION_CAR_CONTROL_AUDIO_VOLUME) == PackageManager.PERMISSION_GRANTED;
}

public static final String PERMISSION_CAR_CONTROL_AUDIO_VOLUME = "car.permission.CAR_CONTROL_AUDIO_VOLUME";

4) Implement this interface on the "server" and check whether the caller has obtained the corresponding permissions

@Override
public void optionPermission(final int i) throws RemoteException {
    // 在oneway 接口中Binder.getCallingPid() 始终为 0
    Log.i(TAG, "optionPermission: calling pid " + Binder.getCallingPid() + "; calling uid" + Binder.getCallingUid());

    // 方法一:检查权限,如果没有权限,抛出SecurityException
    mContext.enforceCallingPermission("car.permission.CAR_CONTROL_AUDIO_VOLUME", "没有权限");

    // 方法二:检查权限,如果没有权限,返回false
    boolean checked = mContext.checkCallingPermission("car.permission.CAR_CONTROL_AUDIO_VOLUME") == PackageManager.PERMISSION_GRANTED;
    Log.e(TAG, "optionPermission: " + checked);
}

Binder.getCallingPid()and Binder.getCallingUid()are used to obtain information about the caller (that is, the process that sent the Binder request). The difference is that:

  • Binder.getCallingPid()The method returns the caller's process ID, which is an int type value that can be used to distinguish different processes. This method exists since API 1 and can be used on any version of Android.
  • Binder.getCallingUid()The method returns the caller's user ID, which is an int type value that can be used to distinguish different users or applications. This method exists since API 1 and can be used on any version of Android.

Both methods can only be called in the Binder method, otherwise the ID of the current process or user will be returned. They can be used to check whether the caller has certain permissions, or to perform some security verification.

checkCallingPermission()Both and enforceCallingPermission()can be used for permission checking, the difference is that

  • int checkCallingPermission(String permission): Check whether the caller has the specified permission. If there is no caller or the caller is not IPC, -1 is returned, and 0 is returned if the IPC caller has the specified permissions.
  • void enforceCallingPermission: Check whether the caller has the specified permission. If there is no caller or the caller is not IPC, a SecurityException is thrown.

In addition to the above methods, there are also the following more commonly used methods for checking AIDL interfaces.

  • int checkPermission(String permission, int pid, int uid): Check whether the specified process and user ID have the specified permissions.
  • int checkCallingOrSelfPermission(String permission): Check whether the caller or itself has the specified permission. If there is no caller, it is equivalent to checkSelfPermission. Use this method with caution as it may grant access to protected resources to a malicious application that lacks permissions.
  • int checkSelfPermission(String permission): Check whether you have the specified permission. This is a dynamic check method at runtime and is usually used to request dangerous permissions.
  • void enforcePermission(String permission, int pid, int uid, @Nullable String message): Check whether the specified process and user ID have the specified permissions, if not, throw a SecurityException exception.
  • void enforceCallingOrSelfPermission(String permission, @Nullable String message): Check whether the caller or itself has the specified permission. If not, throw a SecurityException. If there is no caller, this is equivalent to enforcePermission. Use this method with caution as it may grant access to protected resources to a malicious application that lacks permissions.

Question 10: Encapsulating AIDL SDK

When the "server" provides business capabilities to the outside world, it is impossible to require each caller to write AIDL and implement the binding logic of the Service. Therefore, we must encapsulate AIDL into an SDK for external use. The following principles generally need to be followed when packaging the SDK:

  • Simplify the calling cost of "client"
  • Hide the Service reconnection mechanism so that the caller does not need to care about the specific implementation of Service reconnection
  • Reduce the number of unnecessary communications between the "client" and the "server" and improve performance
  • Verify permissions as needed

Based on the above principles, the following implementation is encapsulated.

  • SdkBase

    SdkBase is an abstract class. Its function is to allow subclasses to more conveniently connect to the server. It implements the Service reconnection mechanism internally. And expose connect(), disconnect(), isConnected() and other methods to the outside world. It is a reusable template class .

  • SDKAppGlobal

    Use reflection to obtain the APP Context class. In this way, we can initialize Sdk anywhere without being restricted by Context. Can be reused

  • SdkManagerBase

    SdkManagerBase is an abstract class. In this example, the subclasses of SdkManagerBase include AudioSdkManager, InfoSdkManager, etc.

There is too much code in the implementation part, please read github to see the specific implementation.

When using it, you need to inherit SdkBase. The implementation of this example is Sdk.

  • Sdk

    Sdk inherits from SdkBase and is a management class used to provide a unified entrance to the "client". Shows how to use SdkBase.

        /**
         * Sdk 是一个管理类,用于管理服务端的各种功能,包括音频、信息等。
         *
         * @author linxu_link
         * @version 1.0
         */
        public class Sdk extends SdkBase<ISdk> {

            public static final String PERMISSION_AUDIO = "com.wj.standardsdk.permission.AUDIO";

            private static final String SERVICE_PACKAGE = "com.wj.standardserver";
            private static final String SERVICE_CLASS = "com.wj.standardserver.StandardService";
            private static final String SERVICE_ACTION = "android.intent.action.STANDARD_SERVICE";

            public static final int SERVICE_AUDIO = 0x1001;
            public static final int SERVICE_INFO = 0x1002;

            private static final long SERVICE_BIND_RETRY_INTERVAL_MS = 500;
            private static final long SERVICE_BIND_MAX_RETRY = 100;

            /**
             * 创建一个 Manager 对象
             * <p>
             * 是否需要设定为单例,由开发者自行决定。
             *
             * @param context  上下文
             * @param handler  用于处理服务端回调的 Handler
             * @param listener 用于监听服务端生命周期的 Listener
             * @return SdkASyncManager
             */
            public static Sdk get(Context context, Handler handler, SdkServiceLifecycleListener<Sdk> listener) {
                return new Sdk(context, handler, listener);
            }

            public static Sdk get() {
                return new Sdk(null, null, null);
            }

            public static Sdk get(Context context) {
                return new Sdk(context, null, null);
            }

            public static Sdk get(Handler handler) {
                return new Sdk(null, handler, null);
            }

            public static Sdk get(SdkServiceLifecycleListener<Sdk> listener) {
                return new Sdk(null, null, listener);
            }

            public Sdk(@Nullable final Context context, @Nullable final Handler handler, @Nullable final SdkServiceLifecycleListener<Sdk> listener) {
                super(context, handler, listener);
            }

            @Override
            protected String getServicePackage() {
                return SERVICE_PACKAGE;
            }

            @Override
            protected String getServiceClassName() {
                return SERVICE_CLASS;
            }

            @Override
            protected String getServiceAction() {
                return SERVICE_ACTION;
            }

            @Override
            protected ISdk asInterface(final IBinder binder) {
                return ISdk.Stub.asInterface(binder);
            }

            @Override
            protected boolean needStartService() {
                return false;
            }

            @Override
            protected String getLogTag() {
                return TAG;
            }

            @Override
            protected long getConnectionRetryCount() {
                return SERVICE_BIND_MAX_RETRY;
            }

            @Override
            protected long getConnectionRetryInterval() {
                return SERVICE_BIND_RETRY_INTERVAL_MS;
            }

            public static final String TAG = "CAR.SERVICE";

            public <T extends SdkManagerBase> T getService(@NonNull Class<T> serviceClass) {
                Log.i(TAG, "getService: "+serviceClass.getSimpleName());
                SdkManagerBase manager;
                // 涉及 managerMap 的操作,需要加锁
                synchronized (getLock()) {
                    HashMap<Integer, SdkManagerBase> managerMap = getManagerCache();
                    if (mService == null) {
                        Log.w(TAG, "getService not working while car service not ready");
                        return null;
                    }
                    int serviceType = getSystemServiceType(serviceClass);
                    manager = managerMap.get(serviceType);
                    if (manager == null) {
                        try {
                            IBinder binder = mService.getService(serviceType);
                            if (binder == null) {
                                Log.w(TAG, "getService could not get binder for service:" + serviceType);
                                return null;
                            }
                            manager = createCarManagerLocked(serviceType, binder);
                            if (manager == null) {
                                Log.w(TAG, "getService could not create manager for service:" + serviceType);
                                return null;
                            }
                            managerMap.put(serviceType, manager);
                        } catch (RemoteException e) {
                            handleRemoteExceptionFromService(e);
                        }
                    }
                }
                return (T) manager;
            }

            private int getSystemServiceType(@NonNull Class<?> serviceClass) {
                switch (serviceClass.getSimpleName()) {
                    case "AudioManager":
                        return SERVICE_AUDIO;
                    case "InfoManager":
                        return SERVICE_INFO;
                    default:
                        return -1;
                }
            }

            @Nullable
            private SdkManagerBase createCarManagerLocked(int serviceType, IBinder binder) {
                SdkManagerBase manager = null;
                switch (serviceType) {
                    case SERVICE_AUDIO:
                        manager = new AudioManager(this, binder);
                        break;
                    case SERVICE_INFO:
                        manager = new InfoManager(this, binder);
                        break;
                    default:
                        // Experimental or non-existing
                        break;
                }
                return manager;
            }
        }
  • AudioManager

    Inherited from SdkManagerBase. Shows how to use SdkManagerBase.

        /**
         * 一个使用示例:音频管理类
         * @author linxu_link
         * @version 1.0
         */
        public class AudioManager extends SdkManagerBase {

            private final IAudio mService;
            private final CopyOnWriteArrayList<AudioCallback> mCallbacks;

            public AudioManager(SdkBase sdk, IBinder binder) {
                super(sdk);
                mService = IAudio.Stub.asInterface(binder);
                mCallbacks = new CopyOnWriteArrayList<>();
            }

            private final IAudioCallback.Stub mCallbackImpl = new IAudioCallback.Stub() {
                @Override
                public void onAudioData(byte[] data, int length) throws RemoteException {
                    for (AudioCallback callback : mCallbacks) {
                        callback.onAudioData(data, length);
                    }
                }
            };

            // 提示需要权限
            @RequiresPermission(Sdk.PERMISSION_AUDIO)
            public void play() {
                try {
                    mService.play();
                } catch (RemoteException e) {
                    Log.e(TAG, "play: " + e);
                    handleRemoteExceptionFromService(e);
                }
            }

            public long getDuration() {
                try {
                    return mService.getDuration();
                } catch (RemoteException e) {
                    return handleRemoteExceptionFromService(e, 0);
                }
            }


            public void registerAudioCallback(AudioCallback callback) {
                Objects.requireNonNull(callback);
                if (mCallbacks.isEmpty()) {
                    registerCallback();
                }
                mCallbacks.add(callback);
            }

            public void unregisterAudioCallback(AudioCallback callback) {
                Objects.requireNonNull(callback);
                if (mCallbacks.remove(callback) && mCallbacks.isEmpty()) {
                    unregisterCallback();
                }
            }

            /************* 内部方法 *************/
            /**
             * 向服务端注册回调
             */
            private void registerCallback() {
                try {
                    mService.registerAuidoCallback(mCallbackImpl);
                } catch (RemoteException e) {
                    Log.e(TAG, "registerAudioCallback: " + e);
                    handleRemoteExceptionFromService(e);
                }
            }

            /**
             * 取消注册回调
             */
            private void unregisterCallback() {
                try {
                    mService.unregisterAudioCallback(mCallbackImpl);
                } catch (RemoteException e) {
                    Log.e(TAG, "unregisterAudioCallback: " + e);
                    handleRemoteExceptionFromService(e);
                }
            }

            @Override
            protected void onDisconnected() {

            }

            public abstract static class AudioCallback {

                public void onAudioData(byte[] data, int length) {

                }

            }

        }
  • AudioDataLoader

    Simulates the encapsulation of the Model layer in the MVVM architecture to show how the "client" re-encapsulates the SDK.

/**
 * 用于加载音频数据的DataLoader.
 * <p>
 * 在MVVM架构中属于 Model 层的组成部分之一.
 *
 * @author linxu_link
 * @version 1.0
 */
public class AudioDataLoader {

    private Sdk mSdk;
    private AudioManager mAudioManager;
    // 同步锁。将异步的Service的连接,改为同步的。
    private CountDownLatch mAudioManagerReady;

    public AudioDataLoader() {
        mAudioManagerReady = new CountDownLatch(1);
        mSdk = Sdk.get(new SdkBase.SdkServiceLifecycleListener<Sdk>() {
            @Override
            public void onLifecycleChanged(@NonNull final Sdk sdk, final boolean ready) {
                if (ready) {
                    mAudioManager = sdk.getService(AudioManager.class);
                    mAudioManager.registerAudioCallback(mAudioCallback);
                    mAudioManagerReady.countDown();
                } else {
                    if (mAudioManagerReady.getCount() <= 0) {
                        mAudioManagerReady = new CountDownLatch(1);
                    }
                    mAudioManager = null;
                    // 重新连接
                    sdk.connect();
                }
            }
        });
    }

    private final AudioManager.AudioCallback mAudioCallback = new AudioManager.AudioCallback() {
        @Override
        public void onAudioData(final byte[] data, final int length) {

        }
    };

    public void play() {
        // 实际应该放入线程池中执行
        new Thread(() -> {
            try {
                mAudioManagerReady.await();
            } catch (InterruptedException e) {
                return;
            }
            mAudioManager.play();
            Log.i("TAG", "play 执行完毕");
        }).start();
    }

    private MutableLiveData<Long> mDurationData;

    public LiveData<Long> getDuration() {
        // 实际应该放入线程池中执行
        new Thread(() -> {
            try {
                mAudioManagerReady.await();
            } catch (InterruptedException e) {
                getDurationData().postValue(0L);
            }
            getDurationData().postValue(mAudioManager.getDuration());
        }).start();
        return getDurationData();
    }

    public void release() {
        mAudioManager.unregisterAudioCallback(mAudioCallback);
        mSdk.disconnect();
        mSdk = null;
        mAudioManager = null;
    }

    private MutableLiveData<Long> getDurationData() {
        if (mDurationData == null) {
            mDurationData = new MutableLiveData<>();
        }
        return mDurationData;
    }

}

"2. Summary"

In this video, we introduce the most commonly used cross-process communication method in vehicle Android development - AIDL. Of course, there are other more commonly ContentProviderused methods. Generally speaking, AIDL has the following advantages and disadvantages:

advantage:

  • Can realize cross-process communication, allowing different applications to share data and functions
  • Can handle multi-threaded concurrent requests to improve efficiency and performance
  • Transfer instances can be customized, providing high flexibility

shortcoming:

  • The usage process is complicated and requires the creation of multiple files and classes.
  • There are restrictions on transmitting data. Only data types supported by AIDL can be used.
  • There is overhead in transmitting data and requires serialization and deserialization operations.

Through these five recent videos, we have basically introduced all the basic technical requirements for vehicle application development. Most of the time, automotive applications are developing system applications, so starting from the next video, we will introduce the principles of common system applications.

Okay, that’s all for this video. The text content of this video is published on my personal WeChat public account - "Car Android" and my personal blog. The PPT files and source code used in the video are published on my Github [https://github.com/linxu-link /CarAndroidCourse], you can find the corresponding address in the introduction of this video.

Thank you for watching, see you in the next video, bye.

Guess you like

Origin blog.csdn.net/linkwj/article/details/131493476