6、Android系统服务开发:AIDL接口定义、Service实现、权限管理

系统服务开发,说白了就是让你的App能跟系统底层“对话”。我刚开始接触这块时,总觉得AIDL这玩意儿很玄乎。后来做车载项目,需要在应用层控制底层的背光亮度,才真正体会到它的价值。

这一章,我们就来聊聊怎么定义AIDL接口、实现Service,以及如何做好权限管理。嗯,都是实战中绕不开的硬骨头。

6.1 AIDL接口定义——跨进程通信的“契约”

AIDL(Android Interface Definition Language)是Android跨进程通信的接口定义语言。你可以把它想象成一份合同——服务端和客户端都得按这个来。

为什么需要AIDL?

同一个App里,我们直接用Binder就行。但跨App通信时,AIDL就成了标准。我见过不少新手直接传复杂对象,结果崩溃了——因为没实现Parcelable接口。

核心要点:AIDL支持的数据类型包括基本类型、String、CharSequence、List、Map,以及实现了Parcelable接口的自定义对象。

来看一个实际例子。假设我们要做一个“系统亮度服务”,先定义AIDL接口:

// ILightService.aidl
package com.example.systemservice;

interface ILightService {
    // 设置亮度值,范围0-255
    void setBrightness(int value);
    
    // 获取当前亮度
    int getBrightness();
    
    // 注册回调,亮度变化时通知客户端
    void registerCallback(ILightCallback callback);
    void unregisterCallback(ILightCallback callback);
}

这里有个细节——回调接口也得用AIDL定义。为什么呢?因为回调也是跨进程的。

// ILightCallback.aidl
package com.example.systemservice;

interface ILightCallback {
    void onBrightnessChanged(int newValue);
}

我个人习惯把AIDL文件放在 src/main/aidl/ 目录下,包名跟Java代码保持一致。这样编译时能自动生成Stub和Proxy类,省心不少。

小技巧:如果AIDL里用了自定义对象,记得在同级目录下定义该对象的Parcelable声明文件。比如 LightInfo.aidl 里写 parcelable LightInfo;

6.2 Service实现——让接口“活”起来

AIDL接口定义好了,接下来就是实现它。Service是Android系统服务的载体,我们得在Service里把接口方法真正跑起来。

实现步骤:

  1. 创建一个Service子类
  2. 在Service内部实现AIDL接口的Stub
  3. onBind() 方法中返回Stub实例
  4. 在AndroidManifest.xml中注册Service

来看代码:

public class LightService extends Service {
    private static final String TAG = "LightService";
    private int mBrightness = 128; // 默认亮度
    private RemoteCallbackList<ILightCallback> mCallbacks = 
            new RemoteCallbackList<>();

    private final ILightService.Stub mBinder = new ILightService.Stub() {
        @Override
        public void setBrightness(int value) throws RemoteException {
            // 检查权限
            if (checkCallingPermission("com.example.permission.SET_BRIGHTNESS") 
                    != PackageManager.PERMISSION_GRANTED) {
                throw new SecurityException("没有设置亮度的权限");
            }
            
            // 这里可以写底层驱动操作
            mBrightness = value;
            Log.d(TAG, "亮度设置为: " + value);
            
            // 通知所有注册的客户端
            int count = mCallbacks.beginBroadcast();
            for (int i = 0; i < count; i++) {
                try {
                    mCallbacks.getBroadcastItem(i).onBrightnessChanged(value);
                } catch (RemoteException e) {
                    Log.e(TAG, "回调失败", e);
                }
            }
            mCallbacks.finishBroadcast();
        }

        @Override
        public int getBrightness() throws RemoteException {
            return mBrightness;
        }

        @Override
        public void registerCallback(ILightCallback callback) 
                throws RemoteException {
            mCallbacks.register(callback);
        }

        @Override
        public void unregisterCallback(ILightCallback callback) 
                throws RemoteException {
            mCallbacks.unregister(callback);
        }
    };

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}

这里有个坑——RemoteCallbackList 是专门为跨进程回调设计的。我曾经直接用 ArrayList 存回调,结果客户端进程挂了,服务端还傻傻地调用,直接抛异常。用 RemoteCallbackList 会自动处理这些情况。

注意:beginBroadcast()finishBroadcast() 之间不要做耗时操作,否则会阻塞其他回调的发送。

别忘了在AndroidManifest.xml里注册:

<service
    android:name=".LightService"
    android:enabled="true"
    android:exported="true"
    android:process=":system_service">
</service>

把Service放在独立进程里,这是系统服务的常见做法。你想想看,如果服务挂了,至少不影响主App的运行。

6.3 权限管理——别让谁都来调你的服务

系统服务暴露给外部App,权限管理就是第一道防线。Android提供了两种方式:

  • 声明式权限:在AndroidManifest中定义,客户端需要声明才能绑定
  • 运行时检查:在Service方法中通过 checkCallingPermission() 检查

定义权限:

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

<permission
    android:name="com.example.permission.GET_BRIGHTNESS"
    android:protectionLevel="normal" />

protectionLevel 的选择很关键。我做过一个项目,把系统服务的权限设成了 normal,结果随便一个第三方App都能调用。后来改成了 signature,只有同签名的App才能访问,安全多了。

在Service中检查权限:

@Override
public void setBrightness(int value) throws RemoteException {
    // 检查调用者是否有权限
    if (checkCallingPermission("com.example.permission.SET_BRIGHTNESS") 
            != PackageManager.PERMISSION_GRANTED) {
        throw new SecurityException("权限不足");
    }
    // ... 执行操作
}

这里有个细节——checkCallingPermission() 检查的是调用者进程的权限,不是Service自身的权限。嗯,这个很容易搞混。

权限检查的最佳实践:
  • 写操作用 signature 级别权限
  • 读操作用 normal 级别权限
  • 敏感操作一定要在方法内部二次检查
  • 不要完全依赖AndroidManifest的权限声明

6.4 知识体系总览

说了这么多,我们来画一张图,把整个系统服务开发的脉络理清楚:

Android系统服务开发知识体系 AIDL接口定义 跨进程通信契约 Service实现 接口落地执行 权限管理 安全访问控制 数据类型支持 Parcelable对象 回调接口定义 Stub实现 onBind返回 RemoteCallbackList 声明式权限 运行时检查 protectionLevel 核心流程:定义AIDL → 实现Service → 注册权限 → 客户端绑定调用 三者缺一不可,共同构成完整的系统服务

6.5 客户端如何调用

服务端写好了,客户端怎么用?其实很简单:

// 绑定服务
Intent intent = new Intent();
intent.setComponent(new ComponentName(
    "com.example.systemservice",
    "com.example.systemservice.LightService"
));
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);

// 连接回调
private ServiceConnection mConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        mLightService = ILightService.Stub.asInterface(service);
        try {
            mLightService.setBrightness(200);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        mLightService = null;
    }
};

记得在AndroidManifest中声明权限:

<uses-permission android:name="com.example.permission.SET_BRIGHTNESS" />
避坑指南:我曾经在 onServiceConnected 里直接做UI操作,结果ANR了。跨进程调用是异步的,但回调是在Binder线程池里执行的,不能直接更新UI。记得用Handler切回主线程。

好了,系统服务开发的核心就这些。AIDL定义好接口,Service实现逻辑,权限管理守住大门。这三板斧用熟了,大部分系统服务场景都能搞定。


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