1. 同样是预装为什么有的输入法出厂后却消失了做RK3588安卓方案开发的朋友大概率都遇到过这种情况给客户演示样机的时候把设备恢复出厂设置结果预装的第三方输入法没了或者变成了系统默认的AOSP英文键盘。客户一脸疑惑地问你这个输入法不是出厂就有吗你也不好解释到底是哪一步出了问题。这事的根源在于大多数人理解的预装其实是用adb install装进/data分区或者量产工具刷机后走首次开机脚本静默安装。这两种方式都依赖用户数据分区一旦执行恢复出厂设置、清除数据、或者换一个user版本固件重新刷机输入法就跟着没了。真正意义上的内置是把输入法APK直接打进/system或者/vendor镜像让它随着系统分区走恢复出厂设置一百遍它都还在。在RK3588的安卓系统定制里实现这种拆不掉的内置核心就一个字mk。也就是产品级Makefile文件。通过修改device/rockchip/rk3588目录下的.mk文件把输入法声明成系统模块编译时直接打包进镜像。这篇文章我就结合自己的实际调试经历把这个过程里最容易踩的坑一个一个拆开讲。先说清楚一个概念系统内置不是把APK塞进去这么简单它要同时满足三件事。第一APK文件必须物理存在于系统分区也就是/system/app或/system/priv-app下面第二系统输入法服务InputMethodManagerService要能识别到这个输入法这要求APK的AndroidManifest里正确声明了InputMethodService并且有BIND_INPUT_METHOD权限第三输入法要出现在已启用输入法列表里甚至直接成为默认输入法这涉及Settings.Secure里的配置。很多人在第二步就卡住了——APK明明在/system/app里躺着输入法列表里就是不显示后面我会单独讲这个排查过程。2. 动手前先摸清RK3588安卓SDK的目录结构与mk文件分布2.1 找到正确的产品mk文件不同版本的RK3588 SDK目录结构会有些差异但大方向是一致的。你拿到的SDK解压之后源码根目录下会有device目录进入device/rockchip/rk3588/里面通常有rk3588.mk、device.mk、BoardConfig.mk这些文件。在Android 12、Android 13的SDK里rk3588.mk是产品级配置文件lunch选中的产品就是它device.mk是板级通用配置很多公共模块也在这里声明。这里有个很关键的细节你改了mk文件一定要确认lunch选择的变体用的是哪个mk。RK3588 SDK里通常有多个lunch选项比如rk3588-userdebug、rk3588_smarc-userdebug、rk3588_evb-userdebug等等每一个对应不同的产品mk。如果你改的是rk3588.mk编译时却lunch了rk3588_evb-userdebug那改的东西根本不会参与编译。我之前就吃过这个亏把输入法加到A产品的mk里结果编译B产品固件刷机后怎么都找不到还以为是代码问题排查了半天才发现是lunch错了。所以第一步source build/envsetup.sh之后先lunch再确认当前产品mk到底是谁。2.2 认识PRODUCT_PACKAGES与模块定义的关系Android的编译系统里PRODUCT_PACKAGES是一个核心变量它声明了这个产品需要打包哪些模块到系统里。但很多新手会误以为只要在PRODUCT_PACKAGES后面写上APK的文件名就够了其实不是。PRODUCT_PACKAGES里面填的是模块名module name而模块的实际构建规则需要在一个Android.mk或者Android.bp文件里定义。系统编译的时候会根据模块名去查找对应的构建规则找到了才能打包进去找不到就报错或者直接跳过。所以完整的内置流程是先为输入法APK写一个Android.mk模块定义然后在产品mk里把模块名加进PRODUCT_PACKAGES最后编译打包。网上有些教程会直接教你用PRODUCT_COPY_FILES把APK复制到/system/app目录这种办法不是不行但很粗糙它不会帮你处理签名、依赖库、权限、ABI这些后续问题后面刷机了很可能输入法根本跑不起来。正规做法还是走模块化定义。2.3 准备一个干净的输入法APK动手写mk之前先把手头的输入法APK整理好。我用的是百度输入法、讯飞输入法、搜狗输入法的定制版APK都是从原厂或者合作伙伴那里拿到的。需要注意几个点APK必须是release版本不能用debug版本debug版本有时候会因为签名或调试标志导致系统预置后行为异常。APK最好不带多余的ABI目录尤其是那种同时包含armeabi-v7a和arm64-v8a的包体积会很大。RK3588是arm64架构只保留arm64-v8a目录就够了。如果想做极致精简还可以用apktool把无用so拆出来但这个是后话。APK的文件名、包名、Service ID一定要先确认清楚后续写mk和设置默认输入法都要用。可以先把APK用adb装到设备上去设置里打开输入法用adb shell dumpsys input_method拿到完整的输入法ID这个ID的格式通常是包名/服务类名先记下来。3. .mk文件修改指南三种常用内置方式的对比与实操3.1 方式一整机PRODUCT_PACKAGES 预编译模块推荐这是我最推荐的方式也是RK3588 SDK里最规范的做法。具体操作分两步。第一步在源码目录下找到合适的第三方应用目录或者自己新建一个目录。比如在device/rockchip/rk3588/third-party/InputMethod/下面放入输入法APK并新建一个Android.mk文件内容如下LOCAL_PATH : $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE : BaiduIME LOCAL_MODULE_CLASS : APPS LOCAL_MODULE_TAGS : optional LOCAL_BUILT_MODULE_STEM : package.apk LOCAL_MODULE_SUFFIX : $(COMMON_ANDROID_PACKAGE_SUFFIX) LOCAL_SRC_FILES : BaiduIME.apk LOCAL_CERTIFICATE : PRESIGNED # 如果想让输入法成为系统级应用可以打开下面这行 # LOCAL_PRIVILEGED_MODULE : true include $(BUILD_PREBUILT)这里的含义我解释一下。LOCAL_MODULE_CLASS : APPS告诉编译系统这是一个APK模块LOCAL_SRC_FILES写的是相对这个Android.mk所在目录的APK路径LOCAL_CERTIFICATE : PRESIGNED表示使用APK自带的原始签名不需要重新签名这一点对输入法这种第三方应用尤其重要。如果写LOCAL_CERTIFICATE : platform系统会用平台签名重新签名APK很多三方输入法在运行时会校验自身签名一旦被重新签名轻则功能异常重则直接闪退。第二步在设备的产品mk文件里把模块名加进PRODUCT_PACKAGESPRODUCT_PACKAGES \ BaiduIME这里有个细节PRODUCT_PACKAGES不是只能用一次后面跟一个变量名如果你想分多处添加每次都用即可千万不要在某处写成了:那样会把前面所有的模块配置全部覆盖掉。这个坑我见过不止一次。3.2 方式二带so库输入法的内置处理有些输入法APK把so库存放在APK的lib目录中系统在安装时会自动解压到应用私有目录这种情况下不需要额外处理。但也有一些定制输入法比较特殊so库是单独拿出来的需要单独声明、打包进系统。这种情况下Android.mk要写成两个模块一个是APK模块一个是so库模块然后用LOCAL_REQUIRED_MODULES把两者关联起来。so库模块的定义示例include $(CLEAR_VARS) LOCAL_MODULE : libxxx_input LOCAL_MODULE_CLASS : SHARED_LIBRARIES LOCAL_MODULE_SUFFIX : .so LOCAL_SRC_FILES : lib/arm64-v8a/libxxx_input.so LOCAL_MODULE_TARGET_ARCH : arm64 LOCAL_STRIP_MODULE : false include $(BUILD_PREBUILT)然后在APK模块里加一行LOCAL_REQUIRED_MODULES : libxxx_input这样编译出来的镜像里so库会被放到/system/lib64目录APK在运行时就能直接找到它。LOCAL_STRIP_MODULE : false的意思是不要对so库做strip有些情况下so库被strip后会导致符号丢失运行时报undefined symbol这个参数在调试时尤其要注意。3.3 方式三PRODUCT_COPY_FILES粗暴复制不推荐及其原因我看网上很多教程会教你用PRODUCT_COPY_FILES直接复制APK到/system/app写法大概是这样的PRODUCT_COPY_FILES \ device/rockchip/rk3588/third-party/BaiduIME.apk:/system/app/BaiduIME/BaiduIME.apk坦率讲这种方式在早期的Android版本上能用但问题很多。第一APK的权限归属、SELinux上下文、签名验证都可能不对系统在扫描的时候会把它当成一个无效应用直接忽略第二有些定制ROM对这个目录有odex优化机制你直接复制一个裸APK进去系统无法生成对应的odex运行时反而更慢第三没有任何模块化声明的话后续想单独编译这个模块、做增量更新都非常别扭。所以我的建议是除非是临时验证用量产固件不要用PRODUCT_COPY_FILES来预置输入法。4. 踩坑实录输入法不显示、崩溃、被替换的完整排查链路4.1 坑1预置后输入法列表里找不到新输入法这是我自己踩过最深的坑。把输入法APK成功打进/system/app之后开机进入设置语言与输入法列表里死活看不到新输入法。一开始我以为是APK本身有问题反复换版本直到后来用命令排查才找到真正的原因。排查链路是这样的先确认系统里确实有这个包adb shell pm list packages | grep baidu能看到包说明APK已经被系统识别了。接着用命令看看输入法服务能不能发现它adb shell ime list -a -s如果这个命令的输出里没有你的输入法ID说明输入法服务根本没把它当成一个可用的输入法。这时候就要检查APK的AndroidManifest.xml里是否写对了service声明。一个合法的输入法service声明长这样service android:namecom.baidu.input.ImeService android:labelstring/ime_name android:permissionandroid.permission.BIND_INPUT_METHOD intent-filter action android:nameandroid.view.InputMethod / /intent-filter meta-data android:nameandroid.view.im android:resourcexml/method / /service关键点有两个一是service必须声明android:permissionandroid.permission.BIND_INPUT_METHOD二是intent-filter里必须有android:view.InputMethod这个action。缺少任何一个系统输入法服务都不会把它识别为输入法。你看到的那些装了包但列表里没有的情况基本都是这个原因。另外还有一种情况输入法已经在列表里但显示为灰色、不可勾选。这种一般是APK的targetSdkVersion太高或者系统版本对某些权限有特殊要求需要用adb shell dumpsys input_method去看具体禁用原因。4.2 坑2切到输入法就闪退日志指向so库缺失有次我把一个定制输入法内置进去系统能识别设置里也能启用但只要一切换到它键盘弹出来瞬间就闪退。抓logcat看到的是dlopen failed: library libnativecore.so not found这个so库文件用的是方式二里的独立so库方案而且我也加了LOCAL_REQUIRED_MODULES但为什么还是找不到后来检查发现问题出在so库的ABI上。输入法SDK给的是armeabi-v7a的so虽然RK3588是arm64处理器但安卓系统在加载so库时如果应用进程是64位的它只会去/lib64目录找64位so库你给它一个32位so库即使放在/lib64目录里也加载不起来。解决办法有两个方向一是找输入法厂商要arm64-v8a版本的so库二是如果APK本身是32位应用需要在Android.mk里给APK模块加上LOCAL_MULTILIB : 32限制或者通过apk的android:extractNativeLibs配置去处理。总的来说在RK3588这种arm64平台上最好直接从源头要arm64的so32位兼容方案一是性能打折扣二是后续系统升级很容易踩雷。4.3 坑3签名冲突导致内置版本总是无法覆盖/更新这个问题比较隐蔽通常在量产之后做第一次OTA升级时才会暴露。场景是这样第一次内置的输入法用的是PRESIGNED保留原签名但某一天你拿到输入法厂商的新版APK直接替换旧APK重新编译固件结果刷机后系统提示应用未安装或者升级失败。原因很可能是新旧APK的签名发生了变化。输入法厂商给的是不同的签名包或者你自己在某个环节用了platform签名导致新旧版本的签名不一致。安卓系统不允许相同包名但签名不同的应用互相覆盖唯一的办法是卸载旧应用再装新的但内置在/system里的应用根本卸载不掉于是就成了卡死状态。我的经验是内置输入法的签名策略从一开始就要定死。如果输入法厂商能提供固定签名的release版那是最好的始终用PRESIGNED。如果拿不到固定签名包那就索性全部用platform签名并且从第一个版本开始就一直用platform签名不要中间混着来。另外每次编译前都检查一下APK的md5确保放进源码目录的包确实是你要的那个版本。4.4 坑4编译时明明加了PRODUCT_PACKAGES刷机后却没有这个问题听着很蠢但真的会反复出现。有一次我在rk3588.mk里加了PRODUCT_PACKAGES编译时也没报错但刷机后输入法就是不在。排查路径是这样的先确认编译日志里有没有编译到这个模块。grep BaiduIME out/build*.ninja_log如果编译日志里压根没有这个模块说明你加的PRODUCT_PACKAGES所在的mk文件在产品编译过程中压根没被读取。这时候就要检查mk文件的组织关系。有些SDK的产品mk不是简单的一个文件而是通过include的方式层层引用rk3588.mk里可能只include了一个common.mk而common.mk里又有条件分支。我在rk3588的某个SDK版本里就遇到过产品mk通过ifneq ($(TARGET_PRODUCT),rk3588)判断导致某些模块只在特定lunch下生效——你lunch的不是那个产品模块自然就不编译。解决思路是修改之后先grep一下确认grep -n BaiduIME device/rockchip/rk3588/ -r确认你加模块的mk文件确实在这个产品的编译路径里。另外还有一个很常见的低级错误PRODUCT_PACKAGES前面的反斜杠续行符后面带了空格导致变量解析异常。建议写完之后用make PRODUCT-PACKAGES或者直接编译看输出不要跳过验证。5. 验证、收尾与进阶玩法5.1 编译刷机后的验证清单内置输入法不是编译通过、刷机成功就万事大吉我整理了一份验证清单每次改完都照着过一遍能少走很多弯路。验证项命令/操作预期结果包是否在系统分区adb shell pm list packages -f | grep 包名路径显示在/system/app或/system/priv-app下输入法是否被识别adb shell ime list -a -s列表中能看到输入法ID输入法能否启用adb shell ime enable 输入法ID返回enable状态为true切换是否正常设置中手动切换输入法弹出键盘无闪退能正常输入so库是否加载切换输入法后抓logcat搜索dlopen无library not found类报错恢复出厂设置设置中恢复出厂设置重新开机输入法仍然存在睡眠唤醒后再切换多次锁屏、唤醒切换输入法无Anr、无闪退其中恢复出厂设置这一步是必须做的。很多人编译完固件刷机第一次启动看着输入法在就以为搞定了实际上一恢复出厂设置就原形毕露。我在RK3588的Android 12上遇到过一次输入法第一次开机在但只要执行恢复出厂设置系统因为SettingsProvider里的默认输入法配置被重置输入法就掉出启用列表了。这个只有真正执行一次恢复出厂设置才能发现。5.2 进阶把内置输入法设为系统默认输入法很多客户都会提出一个需求开机之后不要是AOSP英文键盘直接用我们内置的中文输入法。这就要在系统层面把输入法ID写进Settings.Secure。临时验证阶段可以用adb命令直接设置adb shell ime enable com.baidu.input/.ImeService adb shell ime set com.baidu.input/.ImeService adb shell settings put secure default_input_method com.baidu.input/.ImeService adb shell settings put secure enabled_input_methods com.baidu.input/.ImeService但量产固件不可能要求每台设备都插adb设置必须做到烧录完就是默认输入法。这里有两条路可以走。一条是针对SDK版本里如果SettingsProvider提供了默认输入法的配置项直接在frameworks/base/packages/SettingsProvider/res/values/defaults.xml里把def_input_method改成你的输入法ID。不过不同Android版本、不同RK SDK对这个键的支持情况不一样需要先确认。我印象中Android 13的某些版本里这个键并不生效最后还得靠第二种方式。另一条更通用预置一个很小的辅助应用给它platform签名在开机广播里写入Settings.Secure。这个应用的逻辑很简单public class BootReceiver extends BroadcastReceiver { Override public void onReceive(Context context, Intent intent) { if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { ContentResolver resolver context.getContentResolver(); Settings.Secure.putString(resolver, Settings.Secure.ENABLED_INPUT_METHODS, com.baidu.input/.ImeService); Settings.Secure.putString(resolver, Settings.Secure.DEFAULT_INPUT_METHOD, com.baidu.input/.ImeService); } } }这个应用因为是platform签名才能写入WRITE_SECURE_SETTINGS权限保护的Settings.Secure。在RK3588的Android 12上我实际验证过是能稳定生效的。但要注意开机广播在Android 14的某些版本里对应用启动限制更严如果做Android 14项目建议改用UserHandle.USER_SYSTEM广播注册或者首次开机时在SystemServer里通过其他地方写入这个要根据具体SDK再调整。5.3 关于后续升级维护的一点忠告输入法这类第三方内置应用后续维护其实比你想的更麻烦。我遇到过的情况是客户在量产后的某一天拿来一个输入法新版本说要修复一个安全漏洞要求尽快出固件升级。如果你是从老版本固件OTA到新版本而新固件里的输入法只是简单替换了/system里的APK那大概率升级不上去。原因就是前面讲的签名和版本覆盖问题。解决思路是在做系统升级的脚本里提前对/system/app下旧的输入法做处理比如先归到/data/app进行数据迁移。但这个操作涉及整个OTA链路复杂度一下子高很多。所以在量产前最好和输入法厂商确认三件事一是提供长期固定签名的release版二是确认输入法是否支持通过系统升级做应用替换很多定制输入法不允许版本降级三是把输入法做成独立分区或者独立模块便于以后单独发OTA包。这些不是技术上的硬门槛但提前沟通能省掉后面大量的返工。我在实际项目中还养成了一个习惯所有和输入法相关的修改不管多小都会单独提交一个commit并且在commit message里写上输入法的版本号、签名md5、以及验证结果。这样即使在量产几个月之后翻出commit历史来还能一眼看明白当时用的是什么版本有没有换过签名避免在升级时自我打架。这个习惯帮我避免过好几次生产事故值得分享给每一位做RK3588安卓定制的工程师。