ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android 14 Framework 自定义系统服务:从 AIDL 到 SystemServer 注册全流程

Android 14 Framework 自定义系统服务:从 AIDL 到 SystemServer 注册全流程 1. 从“能用”到“可扩展”为什么要往 Framework 里塞一个自定义服务做 Android 系统定制的人早晚会碰到一个尴尬场景应用层要读一个系统级的状态或者要下发一个只有系统进程才能安全触达的指令。常规做法有两条路——写一个系统应用常驻后台或者开一个 ContentProvider 做 IPC 中转。这两条路都能跑通但都有先天缺陷系统应用会被杀、会被停用ContentProvider 接口一旦多了就变成大杂烩权限模型也得自己造轮子。而系统原生服务走的是另一条路。ActivityManager、WindowManager、AlarmManager 这些服务都是开机时由 SystemServer 启动并注册进 ServiceManager应用侧通过Context.getSystemService()拿到一个 Binder 代理直接跨进程调用。这条路之所以稳是因为它享受了三重保障服务生命周期由系统进程托管权限由 Framework 的 SELinux 和权限检查统一把关接口由 AIDL 类型系统约束。把自定义服务注册进去就是让业务代码继承这套成熟的系统能力而不是在应用层重新发明一套。这个需求在 Android 14API 34上尤其典型。Android 14 强化了前台服务类型限制、强制 64 位、加强了动态代码加载校验应用层搞“后台常驻 反射调用系统能力”的路子越来越难走。反过来看在 Framework 层做一个正经的系统服务反而更符合系统当前的设计取向。这篇文章我会完整走一遍流程从定义 AIDL 接口、实现服务类到修改 SystemServer、注册进 ServiceManager再到应用层通过Context.getSystemService()调用的全过程连带我在实际编译和调试中踩过的坑。先说清楚适用范围。整个流程需要 AOSP 源码环境和编译产物适合系统 ROM 开发、企业定制设备、车机或者平板方案商的相关同学。纯应用开发者不需要碰这个但了解一下系统服务的注册机制对你理解getSystemService内部到底发生了什么也有帮助。2. 动手前的全局设计系统服务的完整链路2.1 一个服务从开机到被应用拿到经历了什么要改 Framework首先得知道系统服务的“一生”是怎么走的。从开机到应用调用一条完整链路是这样的SystemServer.main()启动进入run()方法依次启动各种服务。服务创建后调用ServiceManager.addService()把 Binder 对象以字符串名称注册到全局 Binder 上下文管理者中。应用进程调用Context.getSystemService()其实是通过SystemServiceRegistry的静态注册表查询拿到一个工厂类。工厂类内部通过ServiceManager.getService()按名称获取 Binder 代理再通过XXXManager.Stub.asInterface()转成 Manager 接口。应用调用 Manager 方法Binder 代理把调用打包发送到系统进程服务端执行对应逻辑返回结果。这条链路里ServiceManager负责“名字到 Binder”的映射SystemServiceRegistry负责“服务名称到 Manager 工厂”的映射。两个映射都要注册缺一个应用层就拿不到服务。理解了这条链路你就知道 Android 14 Framework 里的自定义服务需要动哪些文件了新增 AIDL 接口定义描述跨进程调用契约。新增 Stub/Proxy 实现类即服务端 Binder 实体。新增 Manager 类可选但推荐封装 Binder 调用细节。修改SystemServer启动并注册服务。修改SystemServiceRegistry注册服务获取入口。修改Context或ContextImpl中的服务名称常量按需。修改 SELinux 策略按需如果涉及特权操作。从这你能看出来这不像写一个普通应用它牵涉到 Binder、AIDL、系统启动流程、应用进程的 ServiceManager 通信每一环都有讲究。2.2 方案取舍为什么不直接写一个系统应用在决定“注册系统服务”之前我建议你先想想有没有更轻的方案。很多人一上来就要改 Framework结果改完发现只是要做一个后台任务完全用不着。方案优点缺点适用场景系统应用常驻开发快不碰系统源码易被强停权限受限升级要整包轻量业务、UI 为主的系统组件应用层 ContentProvider跨进程方便应用内实现权限难控Provider 容易被杀不适合高频调用数据共享插件间通信系统服务生命周期稳定权限系统原生支持可被系统统一管控必须改 Framework 源码编译调试周期长系统级能力、硬件控制、全局策略下发以我自己的经验判断标准就三条是否需要系统级权限、是否需要常驻存活、是否需要被多个应用同时高频访问。三条中两条以上命中就值得上系统服务。2.3 Android 14 的特别之处这些改动和旧版本有什么不同Android 14 和 Android 13 及更早版本相比做系统服务的整体框架没有剧变但有几处细节你要注意SystemServiceRegistry的实现仍然在ContextImpl中但部分服务改用了ServiceCompat等新封装注册方式本身稳定。Android 14 对 Binder 调用的权限校验更严onTransact()中要处理的Parcel数据校验要多看一眼Binder.getCallingUid()会成为你常用的调试武器。编译方面Android 14 默认使用 Soong 构建系统Ninja Blueprint如果你新增了 AIDL 文件必须正确配置Android.bp中的aidl相关属性否则会碰到编译失败。如果目标设备启用了动态 SystemServer即apex化的系统服务服务注册路径可能有差异。一般定制 ROM 都是传统SystemServer路径但需要确认你的产品基线。所以Android 14 依然是用“AIDL ServiceManager SystemServiceRegistry”的老配方只是构建环境和权限策略更新了。下面进入正题把每一步拆开讲。3. 核心实现自定义服务的 AIDL 接口与 Binder 服务端3.1 定义 AIDL 接口跨进程通信的“合同”AIDLAndroid Interface Definition Language的作用是把接口定义翻译成 Binder 通信所需的 Java 类。它的核心价值是你只需要关心接口签名不用手工处理 Parcel 的读写和事务码分发。我以一个“设备信息上报服务”为例。假设产品需要收集设备运行时长、电池温度、某硬件状态并向应用层开放查询接口。首先在frameworks/base/core/java/android/os/目录下新建文件IDeviceInfoService.aidlpackage android.os; interface IDeviceInfoService { long getDeviceUptimeMillis(); int getBatteryTemperature(); String getHardwareStatus(int hardwareId); }这里有几个细节值得注意包名必须是android.os这样生成的服务类就会进入android.os包和系统其他服务保持一致。方法参数和返回值只支持 AIDL 支持的类型如果需要复杂数据要定义Parcelable或使用Bundle传输。默认情况下 AIDL 是同步调用如果方法耗时建议增加oneway标记或者把耗时操作放在服务端线程池里执行。写完 AIDL 后要把它添加进frameworks/base/Android.bp或frameworks/base/core/api/current.txt的相关依赖中。这一步容易被忽略——AIDL 文件不会被自动纳入编译除非你在 Android.bp 里声明了它所在的目录或直接将接口添加到core模块的资源列表。在实际项目中我通常把 AIDL 文件放在frameworks/base/core/java/android/os/然后确认core模块的srcs里已经包含了core/java/**/*.aidl的 glob 模式。大多数 AOSP 分支默认就是包含的所以风险不大但你要知道自己依赖的是什么。3.2 实现服务端 Binder 类写一个 StubAIDL 编译后会生成IDeviceInfoService.Stub抽象类它本身就是一个 Binder。我们的服务类继承IDeviceInfoService.Stub即可。创建服务实现类DeviceInfoService.java放在frameworks/base/services/core/java/com/android/server/下package com.android.server; import android.content.Context; import android.os.Binder; import android.os.IDeviceInfoService; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.util.Slog; public class DeviceInfoService extends IDeviceInfoService.Stub { private static final String TAG DeviceInfoService; private final Context mContext; public DeviceInfoService(Context context) { mContext context; } Override public long getDeviceUptimeMillis() { return SystemClock.uptimeMillis(); } Override public int getBatteryTemperature() { // 这里可以做权限校验比如只允许系统应用调用 enforceSystemCaller(); return mContext.getResources().getInteger( com.android.internal.R.integer.config_defaultBatteryTemperature); } Override public String getHardwareStatus(int hardwareId) { // 模拟硬件状态读取 return hardware_ hardwareId _online; } private void enforceSystemCaller() { int callingUid Binder.getCallingUid(); if (callingUid ! Process.SYSTEM_UID mContext.checkCallingOrSelfPermission( android.Manifest.permission.MANAGE_DEVICE_POLICY_BASE) ! PackageManager.PERMISSION_GRANTED) { throw new SecurityException(Not allowed to read battery temperature); } } }关于enforceSystemCaller这段说明一下系统服务暴露给应用层大概率需要做权限控制。最常用的方式有两种Binder.getCallingUid()配合Process.SYSTEM_UID判断调用者身份。Context.checkCallingOrSelfPermission()检查调用者的权限。安全上要记住一点所有暴露给应用的方法都要考虑和验证调用者身份。不要觉得“这是内部接口不会有人调”只要应用能通过getSystemService拿到 Binder就可以无视 Java 层的包名限制直接发起 Binder 调用。Android 14 对 Binder 调用方的权限校验越做越细服务端多一道检查不是坏事。3.3 在 SystemServer 中启动服务选对启动阶段打开frameworks/base/services/java/com/android/server/SystemServer.java找到startOtherServices()方法在合适的位置插入服务启动代码。我的习惯是在startBootstrapServices()或startCoreServices()完成后、startOtherServices()的中后段加入自定义服务。原因是太早启动服务依赖的 Context 可能还没准备好太晚启动部分应用已经启动可能首次调用时拿不到服务。以下是示例代码private void startDeviceInfoService() { try { Slog.i(TAG, Starting DeviceInfoService); DeviceInfoService service new DeviceInfoService(mSystemContext); ServiceManager.addService(Context.DEVICE_INFO_SERVICE, service); } catch (Throwable e) { Slog.e(TAG, Failure starting DeviceInfoService, e); throw e; } }然后在startOtherServices()里调用startDeviceInfoService();注意ServiceManager.addService()的第二个参数是IBinder类型IDeviceInfoService.Stub本身继承自Binder所以直接传service没有问题。服务启动阶段的选择直接决定了服务可用性的时序。如果某些系统服务初始化过程会反向依赖你的服务你需要把启动顺序往前调反过来如果你的服务依赖 PMS 或 AMS 完成初始化就往后放。我遇到过服务启动后 PMS 还没就绪导致checkCallingOrSelfPermission走空指针的情况最后靠延迟注册或者把依赖解耦才解决。3.4 在 SystemServiceRegistry 注册让 getSystemService 认你光有 ServiceManager 的注册还不够。应用调用Context.getSystemService()时走的是ContextImpl内部的SystemServiceRegistry所以必须在这里注册对应关系。打开frameworks/base/core/java/android/app/SystemServiceRegistry.java在静态注册区加入registerService(Context.DEVICE_INFO_SERVICE, IDeviceInfoService.class, new ServiceFetcherIDeviceInfoService() { Override public IDeviceInfoService createService(ContextImpl ctx) { IBinder b ServiceManager.getService(Context.DEVICE_INFO_SERVICE); return IDeviceInfoService.Stub.asInterface(b); } });这里有一个关键点Context.DEVICE_INFO_SERVICE是服务字符串名称需要先在Context.java或ContextImpl.java中定义为常量例如public static final String DEVICE_INFO_SERVICE device_info;SystemServiceRegistry的泛型类型就是应用直接拿到的对象类型。如果你不想让应用直接操作 AIDL 接口而是想提供一个更高层的 Manager可以注册DeviceInfoManager内部封装 Binder 调用逻辑。对于大多数场景我建议做一层 Manager 封装原因后面单独讲。有时候你会看到有人在这里直接返回new DeviceInfoManager(ctx)这也是正确写法前提是 Manager 内部不要持有 Activity 等强引用。ContextImpl 传入 Manager 时最好使用 ApplicationContext避免内存泄漏。4. Manager 层封装让调用方更舒服4.1 为什么要加一层 Manager直接暴露 AIDL 接口给应用在技术上是可行的但如果服务接口后面增加内部逻辑比如缓存、回调注册、权限预处理应用侧就得跟着改。加一层 Manager 的好处非常明显隐藏 Binder 调用细节应用层只需要面向业务方法。在 Manager 内做参数校验、默认值处理避免每个调用方重复判断。可以持有 Context方便后续做权限校验、资源获取。服务端接口变更时Manager 的适配可以向上兼容。Android 系统本身就是这么做的ActivityManager封装了IActivityManagerAlarmManager封装了IAlarmManager。你完全可以照抄这个模式。4.2 编写 DeviceInfoManager放在frameworks/base/core/java/android/os/下package android.os; import android.annotation.SystemService; import android.content.Context; SystemService(Context.DEVICE_INFO_SERVICE) public class DeviceInfoManager { private final IDeviceInfoService mService; public DeviceInfoManager(Context context) { IBinder b ServiceManager.getService(Context.DEVICE_INFO_SERVICE); mService IDeviceInfoService.Stub.asInterface(b); } public long getDeviceUptimeMillis() { try { return mService.getDeviceUptimeMillis(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } public String getHardwareStatus(int hardwareId) { try { return mService.getHardwareStatus(hardwareId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } }特别注意RemoteException的处理方式。Binder 调用系统服务时如果系统进程意外崩溃RemoteException会被抛出。在系统服务的场景下这个异常其实意味着“系统进程不可用”正确的做法是e.rethrowFromSystemServer()它会把异常包装成运行时异常。不要吞掉异常否则调用方会拿到一个默认值掩盖系统级故障。SystemService注解主要给 IDE 和文档工具提供元数据不影响运行逻辑但写上更规范。4.3 Manager 与 AIDL 接口的权限边界划分划分原则是这样的AIDL 接口层只负责跨进程数据交换不写业务逻辑不判断业务场景。Manager 层负责面向应用的接口形态可以做一些轻量预处理。真正的权限检查放在服务端 Binder 方法里因为 Binder 调用是可以被任何有 Binder 权限的应用伪造的Manager 层的检查不能替代服务端检查。我见过不少团队把权限判断写在 Manager 里然后在 AIDL 服务端什么都不做结果被有心人绕过 Manager 直接调 Binder权限形同虚设。5. 应用侧调用getSystemService 到底返回了什么5.1 应用如何拿到你的服务写一个测试应用在MainActivity里DeviceInfoManager manager (DeviceInfoManager) getSystemService(Context.DEVICE_INFO_SERVICE); if (manager ! null) { long uptime manager.getDeviceUptimeMillis(); String status manager.getHardwareStatus(1); Log.d(TestApp, uptime uptime , status status); }因为SystemServiceRegistry注册的 fetcher 返回的是IDeviceInfoService或DeviceInfoManager所以getSystemService返回值取决于你注册的类型。如果你注册的是DeviceInfoManager.class应用侧就能直接强转成DeviceInfoManager。这里有个常被忽略的坑应用侧要访问android.os.DeviceInfoManager必须保证该类被加入frameworks/base/core/api/current.txt的公共 API 列表中否则应用编译时可能找不到这个类或者即使找到了系统运行时也不一定会导出给应用。AOSP 对系统 API 的管理非常严格新增公共 API 需要更新 API 文件。如果你是内部定制设备不给第三方应用使用可以采用更省事的方案不导出 API而是通过系统的SystemApi或隐藏 API 白名单方式暴露给特定应用。这种方案免去了维护current.txt的麻烦但应用编译时要额外导入系统framework.jar或使用反射。5.2 应用无法拿到服务的几个排查方向如果应用侧getSystemService()返回null排查方向依次是SystemServiceRegistry里是否注册了对应的服务名检查注册代码是否真的被编译进framework.jar。Context.java/ContextImpl.java中的字符串常量是否和ServiceManager.addService()时使用的一致。你的服务在 SystemServer 启动过程中是否抛异常了查看logcat里的SystemServer或你的自定义 TAG。是否是因为公共 API 未导出导致应用编译期找不到类。编译期报错通常是cannot find symbol运行期报错通常是ClassNotFoundException或NoSuchMethodError。应用的 targetSdkVersion 或系统版本是否触发了隐藏 API 限制如果是考虑用SystemApi 白名单方式。我排查这类问题时第一步永远是抓开机日志看DeviceInfoService有没有启动成功。如果SystemServer启动过程中抛了异常整个服务都不会注册应用侧当然拿不到。6. Android.bp 与编译配置最容易翻车的一环6.1 新增文件要配哪些构建属性新增了 Java 文件和 AIDL 文件之后构建系统不会自动把它们编进产物。你需要在对应模块的Android.bp中确认或补充配置。以frameworks/base/services为例services/core的Android.bp通常会包含类似java_library { name: services.core, srcs: [java/**/*.java], aidl: [java/**/*.aidl], // 如果 AIDL 文件也放在这个模块 ... }如果你的 AIDL 文件放在frameworks/base/core/java/那么它由framework模块编译你需要在frameworks/base/Android.bp的framework模块配置里确认srcs包含 AIDL 文件所在的路径。这里要把话说透AOSP 里 Java 文件可以被多个模块引用但你新增的类如果不在任何模块的srcs范围内编译时它就是不存在。:tofu似的构建系统不会报错只是生成物里没有它。我踩过一个很典型的坑在frameworks/base/services/core/java/com/android/server/下新增了DeviceInfoService.java但因为services.core模块的srcs用了java/**/*.java的 glob新文件被自动纳入看起来没问题。可当我修改SystemServer.java时SystemServer属于另一个模块结果编译期出现cannot find symbol: DeviceInfoService。原因是services模块和services.core模块的依赖关系没有配置好。解决方案是检查SystemServer所在模块的static_libs或libs是否依赖了services.core。一般来说不需要你额外改动因为 AOSP 默认依赖已经包含但如果你的源码分支被裁剪过就要仔细查。6.2 AIDL 文件的编译配置坑如果 AIDL 文件没有被编译报错信息通常是cannot find symbol: IDeviceInfoService。处理方式确认 AIDL 文件的包名声明和文件路径一致。AIDL 要求package android.os的文件必须放在.../android/os/目录下。确认 AIDL 文件所在目录属于某个模块的aidl属性范围。确认没有同名 AIDL 冲突。在大型源码树里多个模块可能都有同名文件AIDL 编译会出现重复定义错误。还有一个少见但真实存在的情况如果你修改了 AIDL 接口的签名其他模块还在引用旧接口编译会出现Stub类方法未实现的报错。解决思路是找出引用方同步更新调用代码。改动 AIDL 前最好先全局搜一下引用点别等编译器帮你找。6.3 实测过一次的编译报错out/soong/build.ninja 找不到很多人在源码里改了东西之后直接跑m或make结果遇到类似这种报错failed: out/soong/build.ninja cd $(dirname out/host/linux-x86/bin/soong_ui) ...这个报错本身不是代码逻辑问题而是 Soong 构建系统在重新生成build.ninja时出了问题常见诱因包括添加了新文件模块但没有在Android.bp中声明或者Android.bp语法错误。AIDL 文件的 import 路径写错Soong 在解析依赖时失败。源码目录权限不对或者磁盘空间不足导致生成文件失败。我的处理顺序是先跑一次rm -rf out/soong注意这不是万能药但能清除 Soong 的缓存状态再执行source build/envsetup.sh lunch 你的目标最后重新编译。如果还不行仔细看报错完整日志定位到具体是哪个Android.bp文件报错。这里还要提醒一句不要在源码树的根目录直接全局搜索Soong日志的片段先看完整报错。Soong 的报错信息往往长且绕但只要找到Error关键字后面的第一行就能定位到具体文件。7. SELinux 权限与开机验证最后两步硬仗7.1 服务注册是否需要放行 SELinux如果你的自定义服务只是普通 Java 服务注册进ServiceManager本身一般不需要额外策略因为SystemServer进程拥有足够的 SELinux 域权限。但是如果你的服务要访问特定设备节点、读取某个 sysfs 属性、或者和底层 HAL 通信很可能需要新增 SEPolicy。例如要访问某个自定义的/sys/devices/platform/xxx/status节点你可能需要在system/sepolicy/private/system_server.te中添加allow system_server sysfs_xxx:file read;或者在system/sepolicy/private/service_contexts中注册你的服务上下文device_info u:object_r:system_server_service:s0服务上下文这块对不熟悉 SELinux 的人来说最容易卡住。如果注册时没有对应的 service_contexts 条目系统在ServiceManager.addService()时可能会因为无法获取正确的 SELinux 标签而失败现象是服务没注册成功但 logcat 里可能只显示一条很隐蔽的错误。我的做法每次新增系统服务都顺手在service_contexts中加上对应的上下文同时检查system_server.te里的权限。对于一些只给系统应用用的敏感服务还可以配合neverallow规则进一步收紧。7.2 编译完成后如何验证服务已经注册成功编译刷机后验证服务是否注册成功最直接的办法是在 adb shell 里执行adb shell service list | grep device_info如果能看到类似输出127 device_info: [android.os.IDeviceInfoService]说明服务已经成功注册到 ServiceManager。此时再用测试应用调用基本就能跑通。如果service list里没有你的服务优先怀疑SystemServer启动阶段抛了异常。连上 logcat 抓一次启动日志adb logcat -s SystemServer DeviceInfoService看DeviceInfoService是否有异常堆栈。还有一种情况是服务注册成功但应用侧拿不到这时候检查SystemServiceRegistry是否把服务名拼错。我个人还习惯写一个内置验证脚本放在测试 ROM 的/data/local/tmp/下用service call直接调 Binder 事务码验证服务端是否活着adb shell service call device_info 1 s16 service call的用法稍微有点绕事务码要跟 AIDL 生成的方法顺序对应不过用来快速确认服务没崩溃很好使。8. 常见问题速查这些坑我至少各踩过一次现象原因解决方案getSystemService()返回 nullSystemServiceRegistry 未注册或服务字符串不一致检查注册代码和常量定义服务未出现在service listSystemServer 启动异常、ServiceManager.addService 失败抓开机日志定位启动异常应用编译时找不到DeviceInfoManager系统 API 未导出或未在 current.txt 中注册更新 API 文件或用隐藏 API 白名单调用服务时抛SecurityException权限校验失败常见于 SELinux 或自定义权限未配好调整 SEPolicy检查服务端权限判断逻辑修改 AIDL 后出现 Stub 实现类报错其他模块还在依赖旧接口全局搜索接口名同步更新实现编译报错 out/soong/build.ninja 缺失Android.bp 语法错误、依赖缺失或磁盘不足修复 bp 配置必要时清理 out 重新生成服务注册成功但跨进程传输自定义对象失败Parcelable 类未正确实现CREATOR或writeToParcel检查 Parcelable 实现确保在 AIDL 中声明 import每一个现象我都遇到过其中“服务没出现在 service list”最花时间。后来我养成习惯在所有服务启动路径的关键节点都加Slog.i包括 addService 之前和之后。这样开机日志里能清楚看到每个服务的注册状态定位问题能快一半。9. 从单服务到多服务这套模式的扩展性以上流程跑通一次后后面再添加新的系统服务就可以照葫芦画瓢了。我把一套完整流程总结为六步在android.os下定义 AIDL 接口。在com.android.server下实现 Stub 服务类。在 SystemServer 的适当位置启动并注册服务。在 SystemServiceRegistry 注册服务名与 Manager/Fetcher 的映射。在 Context/ContextImpl 中新增服务名称常量。刷机后通过service list验证注册结果。如果你要加多个服务我的建议是不要把它们全部塞进同一个 SystemServer 方法里而是按业务域拆分或者做一个聚合XXXService内部维护多个子服务。这样既能减少 SystemServer 启动逻辑的膨胀也便于单独控制某个子服务的生命周期。另外如果是团队协作开发建议把“服务名常量”集中放在一个文件里比如专门的ServiceNames.java。不然每个人都各起各的字符串等联调时发现 A 写的“abc”和 B 写的“abc_service”是同一个意思就得花不少时间去核对。10. 最终的一点个人体会把这个自定义服务链路走通之后我对 Android 系统的“服务化”理解深了不少。过去我总把getSystemService当成一个黑盒现在知道它背后是 ServiceManager、SystemServiceRegistry、Binder、SELinux 的组合拳。平时我们随手调用的getSystemService(Activity.ACTIVITY_SERVICE)其实就是从 ServiceManager 里拿一个 Binder 代理再转化成 Manager这个复杂度系统已经帮我们屏蔽了。从实际项目角度看把一个自定义服务注册进 Framework 并不复杂真正耗时的是理解权限模型和构建系统。我建议第一次实践时不要贪多先用一个最简单的服务跑通全链路再逐步增加复杂逻辑。手机一次刷机周期本身就长如果在权限和 SEPolicy 上卡住会很消耗精力。最后再分享一个小技巧修改 Framework 之后如果只有你的服务相关代码变动可以尝试局部编译验证比如m services或m framework不一定每次都要出完整刷机包。这样能大幅缩短迭代时间。当然有些改动必须整包验证但先做局部编译检查语法和依赖总比完整构建到一半才发现问题要强得多。
返回列表