1. 从零到一:为什么要在系统层加一个AIDL服务
做过Android系统开发的人都有一个共识:Framework层的每一次改动,影响面都远比写一个App大得多。你改一个系统服务的接口,可能牵动十几个模块的编译依赖;你加一个AIDL文件,可能要让整个SDK重新生成。但反过来,一旦你掌握了在Framework里添加系统级AIDL服务的完整流程,你就拥有了从底层向上层输出能力的“钥匙”——App能调用什么、不能调用什么,由你来定。
这个项目的核心目标很明确:在Android Framework中新增一个系统AIDL服务,编译进系统镜像,让上层App通过bindService或者直接通过ServiceManager获取代理来调用。听起来就几步,但真正踩进去,你会发现涉及AIDL接口定义、服务实现、SystemServer注册、SELinux策略配置、权限管控、编译规则适配等一系列环节,任何一个环节出错,要么编译不过,要么运行时服务拿不到,要么App调用直接被权限拒绝。
这篇文章适合谁看?如果你已经写过App层的AIDL,知道Stub和Proxy是怎么回事,但对Framework层的服务注册流程还不熟悉,那这篇就是为你准备的。如果你正在做ROM定制、系统裁剪、或者需要给自家App开放一些系统级能力,这套流程你可以直接抄作业。我会把每一步为什么这么做、不这么做会出什么问题,都讲清楚。
先说一下整体思路。Android的系统服务大体分两类:一类是Java层的SystemService,跑在SystemServer进程里,比如ActivityManagerService、PackageManagerService;另一类是Native层的服务,用C++写的,比如SurfaceFlinger。我们要加的是前者——一个Java层的系统AIDL服务。它需要在SystemServer启动的某个阶段被实例化并注册到ServiceManager,这样其他进程才能通过ServiceManager.getService()拿到它的Binder代理。同时,为了让App能调用,还需要在AndroidManifest.xml里声明权限,在SELinux策略里放开对应的规则,否则App一调用就会被avc denied拦下来。
整个流程我把它拆成六个阶段:定义AIDL接口、实现服务类、注册到SystemServer、配置编译规则、处理SELinux策略、验证调用。下面逐个展开。
2. 核心细节拆解:AIDL接口定义与服务实现
2.1 AIDL文件放哪里,怎么定义
在Framework里加AIDL,位置很关键。通常有两种选择:放在frameworks/base/core/java/android/os/下面,或者放在frameworks/base/core/java/com/android/internal/下面。前者适合对App公开的接口,后者适合系统内部使用。我建议放在frameworks/base/core/java/android/os/下,因为这样编译出来的接口会包含在framework.jar里,App可以直接引用。
假设我们要加一个服务叫IDeviceInfoService,提供两个方法:获取设备序列号和获取系统启动时间。AIDL文件这样写:
// frameworks/base/core/java/android/os/IDeviceInfoService.aidl package android.os; interface IDeviceInfoService { String getDeviceSerial(); long getBootTime(); }这里有个细节要注意:AIDL文件里不能有中文注释,否则在某些Android版本上编译会报编码错误。另外,方法参数如果是自定义类型,必须实现Parcelable,并且要在AIDL文件里显式import。对于基本类型和String,不需要额外处理。
定义好AIDL之后,Android的编译系统会自动生成IDeviceInfoService.java,里面包含Stub和Proxy两个内部类。Stub是服务端要继承的,Proxy是客户端拿到的代理。这个生成过程是在编译时由aidl工具完成的,你不需要手动写。
2.2 服务实现类的编写要点
服务实现类要继承IDeviceInfoService.Stub,然后实现具体方法。这个类通常放在frameworks/base/services/core/java/com/android/server/下面。比如:
// frameworks/base/services/core/java/com/android/server/DeviceInfoService.java package com.android.server; import android.content.Context; import android.os.IDeviceInfoService; import android.os.SystemProperties; public class DeviceInfoService extends IDeviceInfoService.Stub { private final Context mContext; private static final String TAG = "DeviceInfoService"; public DeviceInfoService(Context context) { mContext = context; } @Override public String getDeviceSerial() { return SystemProperties.get("ro.serialno", "unknown"); } @Override public long getBootTime() { return System.currentTimeMillis() - android.os.SystemClock.elapsedRealtime(); } }这里有几个坑要提前说。第一,SystemProperties是隐藏API,在Framework里可以直接用,但如果你在App里想用,需要反射或者系统签名。第二,服务实现类里不要做耗时操作,因为Binder线程池是有限的,阻塞了会影响其他调用。如果确实需要耗时,开子线程或者用HandlerThread。第三,mContext传进来之后,注意不要造成内存泄漏,虽然系统服务生命周期很长,但规范还是要遵守。
2.3 注册到SystemServer的正确姿势
服务写好了,得让它跑起来。SystemServer是Android系统启动时第一个被创建的Java进程,所有系统服务都在这里启动。你需要在SystemServer.java的startOtherServices()方法里找到合适的时机注册你的服务。
具体来说,在frameworks/base/services/java/com/android/server/SystemServer.java里,找到startOtherServices()方法,在合适的阶段加入:
try { Slog.i(TAG, "DeviceInfoService"); ServiceManager.addService(Context.DEVICE_INFO_SERVICE, new DeviceInfoService(context)); } catch (Throwable e) { reportWtf("starting DeviceInfoService", e); }注意Context.DEVICE_INFO_SERVICE这个常量需要你在Context.java里定义,比如:
public static final String DEVICE_INFO_SERVICE = "device_info";这个字符串就是App调用时用的服务名。注册的时机很重要,太早的话context还没准备好,太晚的话其他依赖这个服务的模块可能已经初始化完了。一般来说,放在startOtherServices()的中后段比较安全,具体位置可以根据你的服务依赖关系调整。
还有一个细节:ServiceManager.addService()有多个重载版本,如果你的服务需要允许App在未授权情况下也能拿到Binder(但调用方法时再检查权限),就用普通的addService。如果希望连获取Binder都受权限控制,可以用带allowIsolated参数的版本。大多数情况下,普通版本就够了。
3. 实操过程:编译配置、权限管控与SELinux策略
3.1 编译规则怎么配
AIDL文件和服务实现类写好了,但编译系统不一定认识它们。你需要检查frameworks/base/Android.bp文件,确保AIDL文件被包含在framework-minus-apex或者framework模块的srcs里。通常frameworks/base/core/java/下的AIDL会自动被扫描,但如果你放在自定义目录,就需要手动添加。
对于服务实现类,它属于services模块,在frameworks/base/services/core/java/下,一般会被services.core模块自动包含。但如果你新建了目录,需要在frameworks/base/services/core/Android.bp里添加对应的srcs路径。
编译命令方面,如果你只改了Framework层,可以用:
make framework services -j32如果改了AIDL,建议先make update-api更新API列表,否则编译会报API不匹配的错误。这个坑我踩过好几次:AIDL新增方法后,current.txt没有更新,编译直接失败。make update-api会自动把新接口写入frameworks/base/api/current.txt,然后再编译就顺利了。
3.2 权限声明与App调用方式
App要调用这个服务,有两种方式。第一种是通过ServiceManager.getService()直接拿Binder:
IBinder binder = ServiceManager.getService("device_info"); IDeviceInfoService service = IDeviceInfoService.Stub.asInterface(binder); String serial = service.getDeviceSerial();但这种方式有个问题:ServiceManager是隐藏类,App直接引用编译不过。你需要用反射,或者把App放到系统源码里编译,使用framework.jar作为依赖。第二种方式是通过bindService,但系统服务通常不通过这种方式暴露,所以第一种更常见。
为了让App有权限调用,你需要在frameworks/base/core/res/AndroidManifest.xml里声明权限:
<permission android:name="android.permission.ACCESS_DEVICE_INFO" android:protectionLevel="signature|privileged" />然后在服务方法里检查权限:
@Override public String getDeviceSerial() { mContext.enforceCallingOrSelfPermission( "android.permission.ACCESS_DEVICE_INFO", "getDeviceSerial"); return SystemProperties.get("ro.serialno", "unknown"); }protectionLevel设为signature|privileged意味着只有系统签名或者特权App才能获取这个权限。如果你想让普通App也能用,可以设为normal,但这样安全性就降低了。根据实际需求权衡。
3.3 SELinux策略配置:最容易翻车的地方
SELinux是Android安全模型的核心,也是新手最容易忽略的地方。你的服务注册到ServiceManager之后,App调用时如果SELinux策略没配好,会直接报avc: denied,服务拿不到或者调用被拒。
首先,你需要给服务定义一个SELinux类型。在system/sepolicy/public/service.te里添加:
type device_info_service, service_manager_type;然后在system/sepolicy/private/service_contexts里关联服务名和类型:
device_info u:object_r:device_info_service:s0接下来,允许App域访问这个服务。在system/sepolicy/private/untrusted_app.te里添加:
allow untrusted_app device_info_service:service_manager find;如果App需要调用服务方法,还需要允许Binder调用:
allow untrusted_app device_info_service:binder call;这些策略配好之后,编译sepolicy并刷机验证。如果还是报avc denied,可以用adb shell dmesg | grep avc查看具体拒绝信息,根据提示逐条添加策略。注意不要直接用audit2allow生成宽松策略,那样会引入安全风险,最好手动分析每条规则的必要性。
4. 常见问题与排查技巧实录
4.1 服务注册失败:ServiceManager返回null
这是最常见的问题。App调用ServiceManager.getService("device_info")返回null,说明服务没注册成功。排查步骤:
第一,确认SystemServer里addService的代码确实执行了。可以在addService前后加Slog.i日志,看logcat里有没有输出。第二,确认服务名拼写一致,包括大小写和下划线。第三,确认SystemServer没有因为异常提前退出。如果addService抛异常被reportWtf捕获,服务就不会注册。
还有一种情况是服务注册了,但App进程看不到。这通常是因为App和SystemServer不在同一个SELinux域,或者ServiceManager的访问权限没放开。用adb shell service list | grep device_info可以查看服务是否在列表中。
4.2 编译报错:AIDL文件找不到或API不匹配
AIDL编译报错通常有三种原因。一是AIDL文件路径不对,编译系统没扫描到。检查Android.bp里的srcs是否包含你的AIDL文件所在目录。二是AIDL语法错误,比如缺少package声明、方法参数类型不支持。三是API列表没更新,报API mismatch错误。解决方法就是跑make update-api,然后重新编译。
如果报cannot find symbol,说明生成的Java文件没有被正确引用。检查你的服务实现类是否在正确的模块里,以及模块依赖是否包含了AIDL生成的代码。
4.3 运行时权限拒绝:App调用直接抛SecurityException
App调用服务方法时抛SecurityException,说明权限检查没通过。排查:第一,确认App已经在AndroidManifest.xml里声明了uses-permission。第二,确认权限的protectionLevel和App的签名匹配。第三,确认服务方法里enforceCallingOrSelfPermission的参数和声明的权限名一致。
如果App是系统签名,但权限还是被拒,检查privapp-permissions.xml里是否给App授予了特权权限。这个文件在frameworks/base/data/etc/或者vendor/etc/permissions/下,不同Android版本位置可能不同。
4.4 SELinux拒绝:avc denied日志分析
SELinux拒绝的日志格式是avc: denied { 操作 } for scontext=源域 tcontext=目标域 tclass=类别。你需要根据scontext和tcontext确定是哪个域访问哪个类型被拒,然后添加对应的allow规则。常见的有find、call、transfer等操作。
一个容易忽略的点是:SELinux策略修改后需要重新编译sepolicy并刷入boot镜像,只刷system镜像是不够的。另外,有些设备开启了SELinux enforcing模式,策略不生效会直接拒绝;如果临时调试,可以用adb shell setenforce 0关闭强制模式,但正式版本必须配好策略。
4.5 服务调用超时或阻塞
如果App调用服务方法时卡住,最后报TimeoutException,说明服务端处理太慢或者Binder线程被占满。检查服务方法里是否有耗时操作,比如IO、网络请求、大量计算。如果有,改成异步处理,或者用oneway关键字声明AIDL方法,让调用不阻塞。
另外,Binder线程池默认最多16个线程,如果并发调用量很大,也可能导致阻塞。这种情况下可以考虑用ThreadPoolExecutor自己管理线程,或者优化服务逻辑减少单次调用耗时。
5. 进阶技巧:让服务更稳、更安全、更好用
5.1 服务生命周期管理与异常恢复
系统服务虽然生命周期很长,但也不是永远不会挂。如果服务进程崩溃,SystemServer会重启,但你的服务状态可能会丢失。对于有状态的服务,建议在onBootPhase或者systemReady回调里做状态恢复。另外,可以在ServiceManager.addService之后注册一个DeathRecipient,当Binder断开时做清理或重连。
还有一个技巧:如果你的服务依赖其他系统服务,比如PackageManagerService,要确保依赖的服务已经启动完成。可以在SystemServer里调整注册顺序,或者在服务内部做延迟初始化。
5.2 权限设计的粒度控制
不要把所有方法都放在一个权限下。如果服务提供多个能力,建议按功能拆分权限。比如读取设备信息一个权限,修改设备状态另一个权限。这样App可以按需申请,用户也能更清楚地知道App拿了什么权限。
对于特别敏感的操作,可以用enforceCallingPermission而不是enforceCallingOrSelfPermission,前者不允许系统进程绕过检查,后者允许。根据安全需求选择。
5.3 兼容性处理:不同Android版本的差异
Android 10之后,ServiceManager的API有变化,addService的参数和权限检查逻辑在不同版本上可能有差异。如果你的代码要兼容多个版本,建议用@RequiresApi注解或者版本判断。另外,Android 11引入了AIDL的稳定版本机制,如果你的AIDL要跨版本兼容,需要把接口放到aidl_api目录下并生成稳定版本。
对于SELinux策略,不同版本的策略文件路径和语法也可能不同。建议在system/sepolicy下按版本分目录管理,编译时根据PLATFORM_VERSION选择对应的策略。
5.4 调试技巧:快速定位服务问题
调试系统服务,最有效的工具是logcat和dmesg。logcat看Java层异常和日志,dmesg看SELinux拒绝。另外,adb shell service list可以查看所有已注册的服务,adb shell dumpsys device_info可以查看服务状态(如果你的服务实现了dump方法)。
如果服务注册成功但App调用失败,可以在App端用Binder.getCallingUid()和Binder.getCallingPid()打印调用者信息,确认是不是权限问题。服务端也可以用Binder.getCallingUid()做更细粒度的权限控制。
6. 个人实操体会与后续扩展方向
这套流程我前前后后走了不下十遍,每次都有新坑。最大的体会是:SELinux策略和权限检查是两大拦路虎,编译通过不代表运行通过,一定要在真机或者模拟器上完整验证一遍。另外,make update-api这个步骤千万别忘,否则编译报错会让你怀疑人生。
后续如果想扩展,可以考虑几个方向。一是把服务做成SystemService的子类,享受系统服务的生命周期管理;二是用AIDL的oneway和callback机制实现异步通知;三是把服务接口做成@SystemApi,让SDK自动生成文档和权限声明。这些进阶玩法,等基础流程跑通之后再折腾也不迟。
最后分享一个小技巧:在SystemServer里注册服务时,把服务名和类名用常量管理,不要硬编码字符串。这样以后改名字或者加服务,只需要改一处,不容易漏。这个习惯在大型ROM定制项目里尤其重要。