简介:面向 Android 开发者的谷歌输入法 PinyinIME 注释源码包,覆盖拼音识别、词组预测、自动纠错、手势输入等核心模块,可帮助理解输入法内部工作原理与 Android 输入事件处理机制,适合希望自研或优化输入法的中高级 Android 开发者。资源共 320 个文件,压缩包约 2.82MB,以 java、cpp/h、xml、aidl 源码及配置文件为主,并包含 so 动态库、class 编译产物与 png 图片素材,层次清晰地覆盖底层调用、UI 交互和资源设计。已有 761 人学习/下载。通过逐段注释可重点学习 InputMethodManager 事件流转、拼音到汉字的转换算法、词库加载与更新、自定义词组存储、JNI 层调用、异步任务处理及应用生命周期注册等知识点。对梳理输入法产品设计、开展自定义输入法开发或排查 IME 接入问题,均有直接参考价值。
1. 带注释的 PinyinIME 源码包,是理解 Android 输入法最值得啃的第一份代码
花两年啃过好几份开源输入法之后,我的结论是:谷歌的 PinyinIME 是 Android 输入法领域最适合精读的源码,没有之一。这套源码在 AOSP 里存在了十多年,结构清晰、分层干净,既包含了InputMethodService这个系统组件的完整生命周期实现,也覆盖了从按键事件、音节切分、候选词排序到软键盘绘制的全部链路。市面上能找到的带注释版本,往往在关键算法和系统回调处做了中文标记,这对于第一次进入输入法开发领域的工程师来说,价值远高于一份英文原版。
这份注释版源码包能解决三类人的问题:刚开始接触 Android 系统开发的人,需要一份“能看懂”的输入法框架样例做主线;做定制 ROM 或企业键盘方案的人,需要快速定位InputMethodService各回调的触发时机;以及纯粹想搞明白“拼音到候选词这条链路到底走了几步”的源码阅读者。文章中我会结合实际源码路径、关键方法和调试命令,把这套东西一次讲透。
2. PinyinIME 源码包的整体结构与 Android 输入法框架的对应关系
2.1 源码包里的核心目录与文件职责
解压这份注释版源码包之后,第一件事不是打开 README,而是先把目录结构和 AOSP 里的原始路径对照起来看。PinyinIME 在 AOSP 中的路径是packages/inputmethods/PinyinIME,整个工程由 Java 层、C++ 层(JNI)、资源层三块拼成。带注释的版本一般会保留这个结构,注释集中在 Java 层,因为那里是业务逻辑密度最高的地方。
PinyinIME/ ├── AndroidManifest.xml ├── jni/ │ ├── src/ │ │ ├── dictbuilder.cpp │ │ ├── dictdef.h │ │ ├── engine.cpp │ │ ├── engine.h │ │ ├── lexparser.cpp │ │ ├── matcher_b.cpp │ │ ├── matcher_u.cpp │ │ ├── search.cpp │ │ └── spelling_trie.cpp │ └── Android.mk ├── res/ │ ├── layout/ │ ├── values/ │ ├── xml/ │ └── raw/ # 词库文件(dict_pinyin.dat 等) ├── src/com/android/inputmethod/pinyin/ │ ├── PinyinIME.java │ ├── PinyinInputMethodService.java │ ├── InputModeSwitcher.java │ ├── PhoneticPinyinDecoder.java │ ├── CandidateView.java │ ├── SoftKeyboardView.java │ └── ... └── tests/注意com.android.inputmethod.pinyin包名,这是在 Android 8.0 之前一直沿用的包名,后来的 Gboard 是独立于 AOSP 的闭源方案。注释版通常会在PinyinInputMethodService.java的类声明旁边标注类似“输入法服务入口,继承 InputMethodService,所有系统回调都从这里的 onXxx 方法进入”这样的说明——这类注释直接把源码阅读者带到了正确的出发点,不用自己去 AOSP 源码里翻来翻去。
2.2InputMethodService的五个回调与键盘显示的关系
配置文件里的android:imeSubtype和android:isDefault等标签决定了系统如何识别这个输入法,但真正决定输入法“什么时候显示、什么时候隐藏”的是InputMethodService的生命周期回调。注释版源码在方法签名上方通常都会补一段时序说明,把它和SoftKeyboardView的显示逻辑串起来。
@Override public void onInitializeInterface() { // 注释:界面创建时调用,APP 进程启动后第一次绑定 IME 时触发 // 主要用于初始化与输入框配置无关的一次性数据,比如候选词视图的尺寸 } @Override public View onCreateInputView() { // 注释:系统在软键盘需要展示时调用,返回给系统真正用于显示的 KeyboardView // 返回值会被系统 WindowManager 添加进输入法窗口,所以这里必须创建 SoftKeyboardView // 并关联 InputModeSwitcher 以处理中英文模式切换 if (mKeyboardView == null) { mKeyboardView = (SoftKeyboardView) getLayoutInflater().inflate( R.layout.keyboard_view, null); mKeyboardView.setService(this); mKeyboardView.setInputModeSwitcher(mInputModeSwitcher); mKeyboardView.setGestureDetector(mGestureDetector); } return mKeyboardView; } @Override public void onStartInputView(EditorInfo info, boolean restarting) { // 注释:输入框获得焦点后、软键盘展示之前调用 // 此时可以根据 EditorInfo.inputType 切换键盘模式,比如数字键盘、英文键盘、拼音键盘 mInputModeSwitcher.switchModeForEditorInfo(info); mSoftKeyboardView.invalidateAllKeys(); }用代码块的注释做线索再去看InputModeSwitcher,会发现整个模式切换的核心逻辑就浓缩在一个类里:switchModeForEditorInfo根据inputType和imeOptions判定应该展示全键盘还是九宫格,switchToMode修改当前的mInputMode状态,然后驱动SoftKeyboardView重绘。这个流程在系统输入法开发中具有通用性,任何自定义 IME 都绕不开这三步:识别输入框类型、切换内部状态、刷新键盘视图。
2.3 源码包中 Java 层与 JNI 层的边界划分
注释过的源码包里,最容易被误解的是PhoneticPinyinDecoder这个类的定位。它表面上看是一个 Java 类,但里面所有核心方法都是 native 声明,真正的实现全部在jni/src/下的 C/C++ 代码里。以search为例:
public class PhoneticPinyinDecoder { // 注释:以下 native 方法对应 jni/src/search.cpp 中的实现 // 每个 native 方法都对应一个 Java 层的调用入口,参数和返回值的设计 // 就是为了让 C++ 层不依赖 Android 框架的任何类 public static native long openDecoder(); public static native void closeDecoder(long decoder); public static native int search(long decoder, String spelling); public static native int getCandidate(long decoder, int index); public static native int[] getSplStart(long decoder); // ... }这份注释点明了为什么search返回的是 int 而不是直接返回候选词列表——C++ 层把候选词存放于内部缓冲区,Java 层按需通过getCandidate拉取,减少 JNI 的跨层数据拷贝。实际阅读源码时,我建议用readelf -s libjni_pinyinime.so | grep java结合javah生成的头文件来对照 native 方法签名,能快速确认JNIEXPORT和Java_com_android_inputmethod_pinyin_...的映射关系,这个技巧在阅读任何带 JNI 层的源码包时都适用。
3. 拼音输入核心链路拆解:从按键事件到候选词上屏
3.1 按键事件在SoftKeyboardView中的分派路径
注释过的源码包里,SoftKeyboardView.java是界面层里注释密度最高的文件之一,因为按键事件的接收、触摸处理、手势识别、按键重绘全都挤在这个类里。阅读时应该沿着“事件从哪来、到哪去”这条主线走:onTouchEvent是入口,GestureDetector在这里被用来区分点击、滑动和长按,而真正处理按键逻辑的方法是onKeyEvent。
@Override public boolean onTouchEvent(MotionEvent event) { // 注释:触摸事件的统一入口 // 单击按键 -> onKeyEvent;滑动 -> 手势识别; // 滑动结束后抬手 -> 恢复键盘为未按下状态 mGestureDetector.onTouchEvent(event); return true; } // 来自 GestureDetector 的 onDown / onSingleTapUp 会触发此方法 public boolean onKeyEvent(int keyCode, KeyEvent event) { // 注释:keyCode 是 Android 标准的键盘码,比如 KEYCODE_A = 29 // 这里把 KeyEvent 交给 Service 层处理,Service 层再决定是上屏字符还是进入拼音搜索流程 return mService.onKeyEvent(keyCode, event); }真正让人困惑的是KeyEvent怎么变成拼音串。PinyinIMEService内部维护了一个ComposingView和当前输入法的状态机;当按键是字母时,onKeyEvent会调用mComposingView的addSpelling方法,把字母追加进当前音节串;当按键是数字时,如果处于“拼音搜索”状态,则直接选中对应位置的候选词。注释版的核心价值就在这里:源码仓库里很多逻辑没有明确的状态标注,注释版的InputModeSwitcher里往往会补一份状态枚举说明,比如STATE_INPUT(输入拼音)、STATE_COMPOSING(正在组合)、STATE_SELECT_CANDIDATE(候选选择中),这几个状态直接决定了按键的分流方向。
3.2PhoneticPinyinDecoder.search()背后的音节切分与候选排序
这一节是注释版源码包最“值钱”的部分,也是面试输入法方向时的高频考察点。所有中文拼音输入法背后都逃不开两个核心算法问题:音节切分和候选排序。PinyinIME 的 C++ 层把这两件事合并在search()的实现里,而非注释版的阅读难度恰恰卡在这一层——search.cpp和spelling_trie.cpp涉及大量位运算和内存池操作。
// 来自 jni/src/search.cpp,注释版会在关键步骤旁补中文说明 int search(const char* spelling, int spellingLen) { // 注释:search 分为三个阶段 // 阶段1:构建拼音串对应的音节图(spl_start) // 阶段2:在词典 trie 树里逐字查找,收集候选 // 阶段3:对候选按词频和上下文(历史选择)重新排序 SpellingTrie* trie = SpellingTrie::getInstance(); LmScore* lmScore = LmScore::getInstance(); // 使用 double-array trie 结构做前缀匹配 // 这里的 key 是拼音串的 hash 值,value 是词在词典文件中的偏移 int ret = trie->getAllWordIds(spelling, spellingLen, &mWordIds); // 注释:核心排序依据 LmScore 的语言模型权重 // LmScore 来自词频统计文件,raw 目录下 dict_pinyin.dat 就是它的实例 // 所以修改 dict_pinyin.dat 就能改变候选排序结果,这是输入法定制的最常用手段 for (int i = 0; i < ret; i++) { mCandResults[i].score = lmScore->getScore(mWordIds[i]); } // 按分数降序排列 qsort(mCandResults, ret, sizeof(CandResult), comp); return ret; }字节占比上,词典文件dict_pinyin.dat大概是整个 APK 体积的 60% 以上,这份注释版源码包同样会包含原版词库。词库格式的解析逻辑在dictbuilder.cpp和lexparser.cpp里,注释版一般会标注“此处词条格式为:拼音 ID + 词频 + 词条长度”这样的关键信息。这个细节对做定制输入法的人尤其有用,因为号段词库的压缩策略、spelling_trie的构建方式、以及二进制词条的内存分布,都是商业输入法不会公开的部分,而PinyinIME把这套东西原原本本摆在了你面前。
3.3 候选词视图的回收机制与滚动边界处理
CandidateView是源码包里另一个典型到几乎可以当教程用的文件。它继承自View,内部采用一个横向滚动的候选词列表,最大的难点在于:候选词数量可能超过屏幕宽度,需要在onDraw里动态计算可见范围,并对候选词进行“虚拟化”渲染——只画当前可见的那几个词,而不是把所有候选都创建成TextView。
@Override protected void onDraw(Canvas canvas) { // 注释:虚拟化绘制的核心 // mStartPos 和 mEndPos 是当前可见的候选词区间 // 超出这个区间的内容不需要绘制,所以候选词再多也不会卡顿 for (int i = mStartPos; i < mEndPos; i++) { // 计算每个词的绘制位置 int x = mPaddingLeft + i * mItemWidth; // 命中高亮:当前选中的候选词用不同的背景色绘制 if (i == mSelectedIndex) { canvas.drawRoundRect(...); } // 绘制词条文本——这里的文本来自 PhoneticPinyinDecoder.getCandidate(i) canvas.drawText(mCandidates[i].mWord, x, mBaseLine, mWordPaint); } }这种虚拟化绘制在源码阅读中有一个参考意义——如果你需要在公司项目里做一个流畅的横向候选栏,CandidateView的onDraw实现比 RecyclerView 更适合直接迁移,因为输入法候选栏的 UI 层级要尽可能扁平,避免在系统窗口里创造过多的视图层级导致输入延迟。注释版源码里经常会补一条说明:“候选词视图不要用 ListView,因为输入法窗口的绘制频率极高,View 层级越深,性能损耗越大”。
3.4 中英文混输模式与数字键盘切换的判据
源码包里的InputModeSwitcher.java藏着所有输入模式切换的规则,注释版里最容易被忽略的恰恰是一个很小的枚举数组mInputMode。它的值在整个运行期间会随着EditorInfo.inputType往返切换。如果你想在定制键盘时支持“中文键盘上直接打英文缩写”这种需求,必须理解这里的优先级判断逻辑。
public boolean switchModeForEditorInfo(EditorInfo editorInfo) { // 注释:核心逻辑——根据输入框的 type 决定键盘模式 // 判断顺序是:TYPE_MASK_CLASS -> TYPE_MASK_VARIATION -> TYPE_MASK_FLAGS // 比如 CLASS_NUMBER 直接进入数字键盘,CLASS_TEXT 再细分是 email 还是 URI int inputType = editorInfo.inputType; if ((inputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) { mInputMode = IS_NUMBER_KEYBOARD; } else if ((inputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) { int variation = inputType & EditorInfo.TYPE_MASK_VARIATION; if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) { mInputMode = IS_EMAIL_KEYBOARD; } else { mInputMode = IS_PINYIN_KEYBOARD; } } return true; }阅读注释版时,把InputModeSwitcher的注释对照res/xml/input_methods.xml里的subtype声明一起看,能理解系统层的 IME 配置与 Java 层运行时状态的对应关系。这个文件也回答了“为什么输入法切换面板里显示的中文键盘和英文键盘其实是同一个 IME”的问题——因为 subtype 的language标签决定了它在系统设置页面的展示名称,但实际代码路径仍然在同一个PinyinInputMethodService里。
4. 把注释版源码包导入 Android Studio:编译、调试与验证
4.1 源码包的工程化改造:从 AOSP 结构到 Gradle 工程
直接从压缩包解压出来的 PinyinIME 是 AOSP 结构,没有build.gradle,不能直接用 Android Studio 打开编译。这里需要做一步工程化改造:把src/下的 Java 代码迁到一个新的模块里,jni/目录使用 CMake 或 ndk-build 重新挂接。我一般这样操作:
# 1. 在项目根目录创建 CMakeLists.txt,把 jni 下的源文件全部加入 # 2. 在 app/build.gradle 里配置 externalNativeBuildandroid { defaultConfig { externalNativeBuild { cmake { cppFlags "-O2 -fvisibility=hidden" arguments "-DANDROID_TOOLCHAIN=clang" // 注释:PinyinIME 的 jni 是纯 C++ 实现,不需要链接额外的第三方库 } } } externalNativeBuild { cmake { path "src/main/cpp/CMakeLists.txt" } } }关键点在于src/main/cpp/CMakeLists.txt里要正确设置 include 路径,把jni/src下所有头文件目录加进去,避免 speling_trie 和 dic_parser 之间互相找不到头文件。注释版源码包里的注释通常会在类头标注“这个文件依赖 dictdef.h 中的宏定义”,方便你判断编译顺序。
编译过程中最常出现的三类问题:
JNIEXPORT函数签名与 Java native 方法不一致——解决方法是删除build目录后重新生成javah头文件- 缺少
android/log.h——在 CMake 里find_library(log-lib log)并链接 - 资源文件找不到——
dict_pinyin.dat必须拷贝到res/raw/目录,并在build.gradle里确认没有被打包压缩
4.2 使用adb shell ime系列命令验证输入法可用性
编译成功后,安装 APK 并验证输入法是否被系统正确识别,最快的路径是adb shell ime系列命令。这套命令是输入法开发过程中使用频率最高的调试工具,比反复去系统设置里点按要高效得多。
# 列出系统当前所有可用的输入法,确认你的 IME 已被识别 adb shell ime list -s # 预期输出中会出现你的包名,例如: # com.android.inputmethod.pinyin/.PinyinIME # 启用你的输入法 adb shell ime enable com.android.inputmethod.pinyin/.PinyinIME # 直接设置为默认输入法——这步在模拟器或专用测试机上非常省事 adb shell ime set com.android.inputmethod.pinyin/.PinyinIME # 打开一个输入框界面,例如 Settings 的搜索框 adb shell am start -a android.settings.INPUT_METHOD_SETTINGS验证成功标志是弹出的键盘和res/layout/keyboard_view.xml中定义的键位布局一致。如果键盘没有弹出来,优先去adb logcat里过滤InputMethodService关键字,绝大多数问题出在onCreateInputView返回了 null,或者AndroidManifest.xml里的BIND_INPUT_METHOD权限缺失:
<!-- 注释:输入法 Service 必须声明 BIND_INPUT_METHOD 权限, 否则系统不会把它识别为可用的输入法 --> <uses-permission android:name="android.permission.BIND_INPUT_METHOD" />4.3 用logcat + systrace定位候选栏刷新卡顿
输入法工程的性能瓶颈和普通 App 有很大不同。普通 App 的卡顿可以被用户感知但不会阻塞系统输入通道,而输入法一旦在键盘弹出、字符上屏、候选刷新这三个环节产生掉帧,用户会立刻在打字时感受到“字跟不上手”。注释版里经常会在CandidateView.onDraw注释里提到“不要在这里做日志输出”,这是因为onDraw绘制频率极高,任何额外的 Java 层调用都会拖慢绘制。
如果需要精确定位候选栏刷新耗时,我建议在search的 JNI 调用两端打时间戳:
long start = System.nanoTime(); int result = PhoneticPinyinDecoder.search(...); long cost = System.nanoTime() - start; Log.d("PinyinPerf", "search cost " + cost + " ns");正常情况下一次search操作在-O2优化下应该控制在 5ms 以内。如果超过 10ms,大概率是dict_pinyin.dat文件过大导致 trie 树访问时 CPU cache 命中率下降。这时候可以尝试用注释版源码里的dictbuilder工具重建一个体积更小的词库,去掉少见词和噪声词,优先保证高频词的命中速度。
systrace抓取输入法窗口的绘制链路:
# 抓取 5 秒的 systrace 输出,过滤输入法进程 python systrace.py -t 5 --app=com.android.inputmethod.pinyin -o trace.html结合CandidateView.onDraw和SoftKeyboardView.onDraw的时间线,能清楚看到一次按键按下后,键盘重绘、候选刷新、字符上屏这三个阶段的 GPU 渲染耗时。如果CandidateView的 doFrame 时间反复超过 16ms,就考虑把候选词的绘制方式改成Bitmap缓存——把固定词频区域的文本预渲染成位图,滑动时直接canvas.drawBitmap。
5. 注释版源码进阶阅读:词库替换、自定义键位与扩展思路
5.1 修改dict_pinyin.dat重建词库的完整步骤
这份注释版源码包最有实用价值的扩展方向就是替换词典。企业定制输入法、行业专用键盘、甚至某些考勤机里的输入法模块,核心需求往往就是“让某些词排在前面”。PinyinIME 的词库构建工具也在源码包里,jni/dictbuilder/目录下的dictbuilder.cpp就是干这个的。
使用步骤分为三步:准备词表、编译工具、生成字典。
# 1. 准备一个 UTF-8 编码的词表文件,格式为: # 词条<TAB>拼音<TAB>词频 # 示例: # 你好 ni hao 200 # 您好 nin hao 150 # 云原生 yun yuan sheng 80 # 2. 编译 dictbuilder(在 jni/dictbuilder 目录下执行) g++ -o dictbuilder dictbuilder.cpp ../src/lexparser.cpp ../src/spelling_trie.cpp \ -I../src -O2 # 3. 生成字典文件 ./dictbuilder wordlist.txt dict_pinyin.dat生成的dict_pinyin.dat直接替换res/raw/下的文件,重新编译 APK 即可。注意词频数值直接影响候选排序——PinyinIME 的LmScore内部会对词频取对数后再加权,所以词频差一个数量级对排序名次的影响远非线性。如果你希望某个词绝对排在第一位,把它设为 9999 这样的极大值通常有效。
5.2 自定义软键盘布局:九宫格、全键盘与符号面板的重绘逻辑
注释版源码包里res/xml/和res/layout/下定义了键盘的默认布局。SoftKeyboardView本身是一个完全自绘的 View,不依赖 Android 系统的KeyboardView组件。也就是说,键位分布、按键尺寸、行间距全部由res/values/arrays.xml里的keyboard.xml数据驱动。
<!-- 注释:全键盘模式的键位定义,每行是一个 String-array 第一列是键显示字符,第二列是 KeyEvent 的 keyCode --> <array name="keyboard_qwerty"> <item>q,31</item> <item>w,32</item> <item>e,33</item> <item>r,34</item> <item>t,35</item> <item>y,36</item> <item>u,37</item> <item>i,38</item> <item>o,39</item> <item>p,40</item> </array>修改布局时需要注意一个约束:每个按键的keyCode必须是标准的 Android key event code,物理键盘输入的代码路径与软键盘点击的代码路径最终都会汇聚到onKeyEvent(int keyCode, KeyEvent event),只是软键盘的KeyEvent是从MotionEvent转换而来的。这就是为什么在PinyinIME里,硬键盘输入和中英文软键盘可以共享同一套业务逻辑。
5.3 从 PinyinIME 到 Gboard 式体验的改造路线
不少做输入法定制的人会问:PinyinIME这套框架能做到 Gboard 那样的滑行输入和语音按键吗?路线其实很清晰:滑行输入需要在SoftKeyboardView.onTouchEvent里实现轨迹采集,然后通过GestureDetector识别路径特征,映射到对应的拼音串——和PinyinIME的现有按键逻辑完全解耦;语音按键则需要在键盘上预留一个功能键位,按下时启动SpeechRecognizer或调用系统RecognizerIntent。
建议的改造顺序是:先替换词库、再修改键位布局、然后增加功能键的响应逻辑,最后才是动手做滑行识别。PinyinIME的注释版源码包给你留下的最大财富不是它能直接变成 Gboard,而是它把输入法的最核心路径——事件输入、拼音解析、候选排序、视图刷新——以最朴素的方式展现了出来,理解了这条路径,任何上层交互的创新都能找到合适的挂载点。
6. 读注释版源码包的三个验证技巧:git diff、动态调试与资源对比
6.1 用git diff反查注释位置,验证你读到的注释是否合理
拿到一份注释过的源码包,第一步不是读代码,而是验证注释质量。把源码包解压后初始化为一个 git 仓库,然后用原始 AOSP 的同路径文件做一次 diff:
# 初始化 git 仓库 cd PinyinIME && git init && git add . && git commit -m "annotated version" # 用 AOSP 原始文件替换某文件后 diff 出注释内容 git diff src/com/android/inputmethod/pinyin/PinyinIME.java如果diff结果里注释行占到了 40% 以上,说明这份注释版确实用心;如果注释密度低于 20%,那和原版差异不大,参考价值有限。这个方法也适合内部团队做源码评审——用统一的基准版本做 diff,能快速定位所有被改动过的位置,避免注释版里藏着某些非预期修改。
6.2 动态调试:断点加在PhoneticPinyinDecoder的入口处观察输入
Android Studio 直接调试输入法进程会有一个坑:输入法运行在系统服务的InputMethodService上下文里,进程名是包名,但调试器的 attach 时机要选在键盘首次弹出前后。我的经验是先在onStartInputView里打日志确认进程已经拉起,然后再挂断点。
// 断点位置示例 public static native int search(long decoder, String spelling);在这个 native 方法上打断点,每敲一个字母都会命中一次,可以观察spelling参数从n变成ni再变成nih的完整变化过程,同时用调试器的Evaluate Expression查看返回的候选词数量。这个操作能彻底理解一件事:输入法不是每敲完一个字就查询一次,而是每次按键都触发一次新的search调用,返回一组全新的候选序列。
6.3 资源对比法:通过解压原始 APK 比对字节差异
最后一个验证技巧是资源级对比。先编译原始 AOSP 的 PinyinIME 得到一个 APK,再把注释版源码包编译出的 APK 解压,对比两者的res/raw/dict_pinyin.dat是否一致:
# 对比两个 APK 中的词库文件 MD5 md5sum original_apk/res/raw/dict_pinyin.dat annotated_apk/res/raw/dict_pinyin.dat如果 MD5 不一致,说明注释版源码包里的词库可能被替换过——这会直接影响候选排序行为,对于需要复现原始体验的人来说是一个关键差异点。同理,res/values/strings.xml里的app_name、pref_key之类的资源也可以逐项 diff。这一招同样适用于验证市面上其他“注释版/汉化版”源码包的可信度。
本文还有配套的精品资源,点击获取