15、系统服务定制:添加自定义系统服务、系统服务注册与权限管理、跨进程通信(AIDL)

说实话,系统服务是Android系统的灵魂。没有服务,应用就是个空壳子。在车载场景下,我们经常需要定制一些系统级服务——比如车辆状态监控、空调控制、座椅调节等等。这些服务需要常驻后台,还要能被多个应用同时调用。

今天我们就来聊聊,怎么在Android Automotive里添加一个自定义系统服务。我会把注册流程、权限管理、还有AIDL跨进程通信都串起来讲。嗯,这部分内容我踩过不少坑,希望能帮你少走弯路。

15.1 系统服务架构概览

先看一张图,理解系统服务在整个Android系统中的位置。

应用层 (App Layer) 车载应用(导航、音乐、车辆设置等) Framework 层 ServiceManager (服务注册中心) 自定义系统服务 (VehicleService) AIDL 接口定义 · Binder 通信 系统服务层 (System Services) 系统服务注册 · 权限校验 · 生命周期管理 SystemServer 进程 HAL / 内核层

从这张图可以看到,应用层通过AIDL接口调用Framework层的系统服务,而系统服务最终会跟HAL层打交道。我个人的习惯是,先把这张图印在脑子里,再动手写代码。

15.2 定义AIDL接口

跨进程通信的核心就是AIDL。说白了,AIDL就是定义了一个契约——服务端和客户端都按这个契约来通信。

我们先定义一个车辆状态服务的AIDL接口:

// IVehicleService.aidl
package com.example.vehicle;

interface IVehicleService {
    // 获取车辆速度
    int getVehicleSpeed();
    
    // 设置空调温度
    void setAcTemperature(int temperature);
    
    // 注册车辆状态监听
    void registerCallback(IVehicleCallback callback);
    
    // 注销监听
    void unregisterCallback(IVehicleCallback callback);
}

再定义一个回调接口,用于服务端主动推送数据给客户端:

// IVehicleCallback.aidl
package com.example.vehicle;

oneway interface IVehicleCallback {
    void onSpeedChanged(int speed);
    void onAcTemperatureChanged(int temperature);
}
小提示:oneway关键字表示异步调用,客户端不会阻塞。适合回调场景,比如车辆速度变化通知。

15.3 实现系统服务端

服务端实现,说白了就是写一个继承自Binder的类,然后把AIDL接口里的方法都实现一遍。

// VehicleService.java
public class VehicleService extends IVehicleService.Stub {
    private static final String TAG = "VehicleService";
    private int mSpeed = 0;
    private int mAcTemp = 24;
    private RemoteCallbackList<IVehicleCallback> mCallbacks = 
        new RemoteCallbackList<>();

    @Override
    public int getVehicleSpeed() {
        return mSpeed;
    }

    @Override
    public void setAcTemperature(int temperature) {
        mAcTemp = temperature;
        // 通知所有注册的客户端
        int count = mCallbacks.beginBroadcast();
        for (int i = 0; i < count; i++) {
            try {
                mCallbacks.getBroadcastItem(i).onAcTemperatureChanged(mAcTemp);
            } catch (RemoteException e) {
                Log.e(TAG, "Callback failed", e);
            }
        }
        mCallbacks.finishBroadcast();
    }

    @Override
    public void registerCallback(IVehicleCallback callback) {
        mCallbacks.register(callback);
    }

    @Override
    public void unregisterCallback(IVehicleCallback callback) {
        mCallbacks.unregister(callback);
    }
}
重点:RemoteCallbackList是系统提供的工具类,专门用来管理跨进程的回调。它内部做了Binder死亡通知处理,客户端挂了会自动移除。我曾经见过有人自己用ArrayList管理回调,结果客户端崩溃后服务端还一直尝试调用,导致各种异常。

15.4 注册系统服务到ServiceManager

服务写好了,怎么让应用能找到它?答案是注册到ServiceManager。这个过程在SystemServer里完成。

// SystemServer.java (部分代码)
private void startVehicleService() {
    try {
        VehicleService service = new VehicleService();
        ServiceManager.addService("vehicle", service);
        Log.i(TAG, "VehicleService registered successfully");
    } catch (Throwable e) {
        Log.e(TAG, "Failed to register VehicleService", e);
    }
}

嗯,这里要注意一个细节:服务名称"vehicle"是全局唯一的。我建议命名时加上公司前缀,比如"com.example.vehicle",避免跟其他厂商的服务冲突。

15.5 权限管理

系统服务不能谁都能调,对吧?我们需要做权限控制。Android提供了两种方式:

方式 说明 适用场景
AndroidManifest权限 在服务端声明权限,客户端需要申请 普通系统服务
签名权限 只有相同签名的应用才能调用 核心系统服务
运行时权限校验 在服务方法中动态检查 需要细粒度控制的场景

我推荐的做法是:在服务端每个方法里都做权限校验。这样最安全。

// 在VehicleService中添加权限校验
private void checkPermission() {
    int pid = Binder.getCallingPid();
    int uid = Binder.getCallingUid();
    
    // 检查调用者是否有权限
    if (mContext.checkPermission(
            "com.example.vehicle.ACCESS_VEHICLE_DATA", 
            pid, uid) != PackageManager.PERMISSION_GRANTED) {
        throw new SecurityException("No permission to access vehicle data");
    }
}

@Override
public int getVehicleSpeed() {
    checkPermission();  // 先校验权限
    return mSpeed;
}
注意:千万不要在服务端用Binder.getCallingUid() == 0来判断是否是系统进程。这种做法在Android 10之后已经不可靠了。老老实实用权限机制。

15.6 客户端调用

客户端怎么用?其实很简单,通过ServiceManager拿到服务代理,然后调用方法就行。

// 客户端代码
public class VehicleClient {
    private IVehicleService mService;
    
    public void connect() {
        // 获取服务代理
        IBinder binder = ServiceManager.getService("vehicle");
        if (binder != null) {
            mService = IVehicleService.Stub.asInterface(binder);
        }
    }
    
    public int getSpeed() {
        if (mService != null) {
            try {
                return mService.getVehicleSpeed();
            } catch (RemoteException e) {
                Log.e(TAG, "Remote call failed", e);
            }
        }
        return -1;
    }
}

你想想看,客户端拿到的其实是一个Binder代理对象,真正的服务在SystemServer进程里。每次调用都是一次跨进程通信,所以要注意性能问题。

15.7 避坑指南

这部分我踩过的坑比较多,挑几个典型的说说:

  • Binder线程池耗尽:系统默认的Binder线程池只有16个线程。如果你的服务被大量并发调用,线程池会耗尽,导致其他服务也受影响。解决方案是:耗时操作一定要异步处理。
  • 回调泄漏:客户端注册了回调但忘记注销,服务端会一直持有Binder引用。我曾经排查过一个内存泄漏问题,查了两天才发现是回调没注销。用RemoteCallbackList可以自动处理,但客户端最好还是在onDestroy里主动注销。
  • 权限声明位置:自定义权限要在frameworks/base/core/res/AndroidManifest.xml里声明,而不是在你的服务模块里。否则系统不认这个权限。
  • AIDL接口变更:AIDL接口一旦发布,尽量不要修改方法签名。如果非要改,记得保持向后兼容。我见过有人删了一个方法,结果所有客户端都崩溃了。

15.8 调试技巧

调试系统服务,最常用的命令就是dumpsys。你可以在服务端实现dump方法:

@Override
protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
    writer.println("VehicleService State:");
    writer.println("  Speed: " + mSpeed);
    writer.println("  AC Temp: " + mAcTemp);
    writer.println("  Callback count: " + mCallbacks.getRegisteredCallbackCount());
}

然后通过adb命令查看:

adb shell dumpsys vehicle

这个命令在调试时特别有用。我每次写完服务,第一件事就是验证dumpsys能不能正常输出。

15.9 总结

自定义系统服务其实不复杂,核心就三步:定义AIDL接口、实现服务端、注册到ServiceManager。但细节决定成败——权限管理、回调处理、线程安全,这些才是真正考验功底的地方。

嗯,如果你在车载项目里需要添加系统服务,建议先从简单的开始,比如只暴露几个get/set方法。等跑通了整个流程,再逐步增加复杂功能。这样出问题了也好定位。

最后说一句:系统服务是常驻进程的,一旦崩溃会影响整个系统。所以代码质量一定要过硬,该加的try-catch一个都不能少。


公众号:蓝海资料掘金营,微信deep3321