霍雅
把IKF 130.76 MiB 的耳机 App 精简到 43.24 MiB:Android APK 逆向、修复与真机验证实录
把 130.76 MiB 的耳机 App 精简到 43.24 MiB:Android APK 逆向、修复与真机验证实录
一个耳机控制 App,为什么要带登录、商城、内容推荐、白噪音和个人中心?如果我只想查看电量、切换降噪、调整耳机设置,能把这些附加业务都剥离吗?
这次项目,就是从一份已经编译、带加固的 iKF 安卓安装包出发,保留原有设备控制体验,裁剪其他业务,并在真实的 iKF-King S+ 上排查问题。它不是重新写一个“蓝牙连接演示”,而是尽量复用原 App 的设备模型、界面、控制分发和相关协议实现。
过程并不顺利:安装失败、申请权限的流程失效、系统已连接但 App 不识别耳机、三个 ANC 按钮消失、设置页为空、返听和煲机闪退、开屏耗时过长,都是实际遇到过的问题。
本文记录的不只是最终数字,也包括那些“删得太快,后来又补回来”的修改,以及每项测试究竟证明了什么。
一、先看结果:包小了,设备功能也要留下
截至 2026 年 9 月 30 日,文件和真机记录如下。全文统一使用 MiB,1 MiB = 1,048,576 字节,避免把十进制 MB 和二进制 MiB 混在一起。
| 项目 | 实际结果 |
|---|---|
| 原始 APK | 137,113,491 字节,130.76 MiB |
| 最终 APK | 45,335,359 字节,43.24 MiB |
| 体积变化 | 减少约 87.53 MiB,降幅约 66.94% |
| 原生库架构 | 仅保留 arm64-v8a |
| 真机与耳机 | MEIZU 20 Pro,Android API 36;iKF-King S+ |
| 设备主界面 | 连接状态、电量、ANC ON/通透/正常重新显示 |
| 耳机操控 | 7 个设置入口恢复 |
| 最后一轮回归 | 20 个阶段,页面与交互检查通过,期间未记录到应用崩溃 |
| 进程冷启动中位数 | 对照包 4,952 ms,最终包 1,535 ms,约缩短 69.0% |
这里有两个重要的口径:体积对比用的是最初原包与最终包;启动对比用的是功能和布局修复前的中间包与最终包。两者的基线不是同一份 APK。
先把修改前后的界面并列放出来。左图是需求中提供的原版设备页对照,右图是最终包的真机截图:耳机图、电量和操作卡片仍然保留原有风格,而内容、白噪音、我的等底部导航已经退出主流程。
| 修改前:原版设备页对照 | 修改后:最终精简版真机页面 |
|---|---|
![]() | ![]() |
图 1:原版与最终精简版对照。原图用于确认目标界面,并非本次重新安装原包后的截图;右图来自 2026 年 9 月 30 日的最终包回归。两图拍摄时间与手机状态不同,只比较页面结构和保留的功能,不比较电量等动态数值。
二、需求最容易被误解的地方:保留“设备功能”,不只是“设备页面”
最初的目标可以概括为:删除手机号和微信登录,移除内容、白噪音、我的等非设备业务,去掉皮肤包、商城入口和产品介绍素材,只交付 arm64 安卓包。
但真正的验收标准是:保留原版耳机控制体验。
这意味着“设备”并不是一个孤立的 Fragment。它至少依赖下面这些链路:
运行时权限 → 扫描/已配对设备 → 型号识别 → 建立控制连接
→ 加载该型号的功能面板 → 显示电量和状态
→ ANC/EQ/设置分发 → 子页面与设备反馈耳机返听、煲机、设备名称、固件升级、耳机定时关机,虽然会打开新的页面,也属于这条设备功能链。相反,“私人闹钟”是手机本地闹钟,最后根据需求明确移除。
白噪音也要按用途区分:独立的白噪音内容页被裁剪,不代表设备煲机用到的噪声音源也一并清空。 相同的素材、工具类或播放器可能被多个模块共享。
所以我后来把裁剪原则改为:不是按页面名称删除,而是按设备功能的实际依赖关系保留。先让该型号的控制链完整运行,再收缩其外部依赖。
三、第一步不是改代码,而是给 APK 做体积体检
APK 是 ZIP 容器。拿到文件后,我先统计每个条目的路径、未压缩大小和 ZIP 中的压缩大小,再按目录和 ABI 聚合。
这里必须区分:ZipInfo.file_size 表示解压后的大小,ZipInfo.compress_size 才接近这个条目对安装包体积的贡献。直接统计反编译目录,很容易得到与 APK 文件大小不一致的结果。
原始包与最终包的主要目录数据如下,表中使用 ZIP 条目的压缩大小之和:
| 目录 | 原始包 | 最终包 | 主要处理 |
|---|---|---|---|
res/ | 56.55 MiB | 19.16 MiB | 去除无关资源负载,扫描动画无损重编码,保留设备布局和必要图像 |
assets/ | 39.22 MiB | 12.03 MiB | 清理皮肤及无关业务素材,保留设备动画、型号数据和功能面板 |
lib/ | 23.72 MiB | 2.43 MiB | 去除多余 ABI 和不再使用的 SDK 原生库 |
这三行不是完整 APK 的精确拆账:DEX、清单、资源表、ZIP 元数据、对齐填充和签名等还会占用空间。更重要的是,脱离原加固启动链后,恢复出的业务 DEX 会改变代码部分的占比,各类负载并非只减不增。
图 2:从实际 APK 重新统计的体积变化。整包看文件字节数,目录看 ZIP 条目压缩大小,两种口径分别标注。
assets/ 里究竟有什么?
它并不全是商城图片。最终保留下来的大文件仍包含设备动画包,例如 assets/animfile/t1.zip、mini.zip、findair.zip、findpro.zip,以及 EQ 展示图片和型号功能数据。
这解释了“为什么删完无关页面还剩几十 MiB”:设备界面本身也有大图、逐帧动画和多型号资产。继续缩小,需要在“只支持一个型号”与“保留原版多型号设备体系”之间再做选择。
当前结果保留了较完整的设备基础设施,并没有把包压成一个只识别蓝牙名称的极简壳。
四、加固让逆向难度上了一个台阶
直接查看原始 classes.dex,看到的主要是加固启动相关内容,而不是完整业务实现。包内还存在 libjiagu 相关文件,运行时能看到 StubApp.interface11 等桥接痕迹。
因此这次不是“反编译一次,然后删几个 Java 文件”这么简单。主要工具分工是:
| 技术或工具 | 实际用途 |
|---|---|
Python、zipfile、哈希计算 | APK 清点、DEX 提取、ZIP 增量替换和验证 |
| ADB、进程内存映射 | 在调试环境定位运行时展开后的 DEX |
| Apktool、Smali | 读布局和清单、追踪实际调用、修改可重新组装的字节码 |
| Pillow | 扫描动画重编码及逐像素验证 |
zipalign、apksigner | 对齐、重新签名、检查可安装包 |
Logcat、UIAutomator、am start -W | 崩溃定位、真机回归和启动耗时测量 |
1. 从运行时恢复业务 DEX
在可读取进程内存的调试环境里,先根据进程映射定位候选区域,再导出对应片段,搜索 DEX 魔数。此次内存片段中的四个 DEX 起点为:
0x0000000
0x055e000
0x0bf0000
0x1267000提取脚本利用 DEX 头部偏移 0x20 的 file_size,按小端读取长度,再切出完整字节范围。下面是根据实际脚本整理的核心逻辑,增加了边界检查以说明验证思路:
import struct
from pathlib import Path
src = Path(r"F:\ikf\device-only\memory_scan\rwxp_25m.bin").read_bytes()
out = Path(r"F:\ikf\device-only\unprotected-dex")
out.mkdir(exist_ok=True)
# 这些偏移只对应本次已导出的内存片段。
offsets = [0, 0x55e000, 0xbf0000, 0x1267000]
for index, offset in enumerate(offsets, 1):
assert src[offset:offset + 4] == b"dex\n"
size = struct.unpack_from("<I", src, offset + 0x20)[0]
assert size >= 0x70 and offset + size <= len(src)
name = "classes.dex" if index == 1 else f"classes{index}.dex"
(out / name).write_bytes(src[offset:offset + size])这些地址和偏移是样本相关的数据,不是通用解包常量。魔数和长度检查也只是第一层,后续还要验证 DEX 解析、类定义以及实际装载结果。
兼容重组阶段,将恢复出的四份业务 DEX 放回包中,并在兼容阶段保留原包 DEX 作为 classes5.dex。这一步还没有“自动解决所有加固影响”。
2. DEX 恢复了,部分生命周期方法却仍是 native
真正棘手的是:一些 Activity 的 onCreate(Bundle) 仍声明为 native。业务类已经出现,原加固桥接链被移除后,方法实现却没有接上。
于是点击页面时出现了类似这样的错误:
java.lang.UnsatisfiedLinkError:
No implementation found for ...V2EarphoneListActivity.onCreate(Bundle)这类问题与“忘了声明 Activity”不同,更不是捕获异常就算修好了。生命周期方法承担布局、状态、监听器和 Presenter 初始化,空实现只会把明显闪退变成空白页或没有功能的界面。
我按页面残存的字段、Binding、监听器、Lambda 和初始化方法,逐项重建实际初始化顺序,而不是照着一个统一模板给所有页面塞 return-void。
五、删页面的时候,要同时看入口、绑定和资源编号
裁剪工作主要落在三层:
- 导航和组件层:主入口回到设备主界面;清理非设备页的导航、清单组件和触发路径。
- 业务字节码层:移除无关页面及专属回调,修正仍然触发它们的调用。
- 资源与依赖层:去掉独占素材和 SDK 负载,保留设备链仍在使用的部分。
一个很典型的问题是 ViewBinding。它可能在 bind() 中按固定资源 ID 寻找布局节点,甚至字段是必需的。把 XML 节点物理删除后,主页面还没显示出来,就可能因为绑定失败而崩溃。
所以某些裁剪先保留绑定占位,设置 GONE,断开监听器和业务逻辑,再移除独占负载。资源表中留有少量占位,不等于相关业务还可访问;反过来,仅仅把按钮藏起来,也不代表代码依赖已经清干净。
另外,Smali 和编译资源表里大量存在固定的 0x7f... 资源 ID。完全重新编译资源可能改变编号或触发残留 XML 引用问题。因此后期更多采用:保留已验证的编译资源骨架,按 ZIP 条目增量替换 DEX 和资源负载。
这种方式需要额外核对引用和可达性,避免随意删除条目。我就误判过 pic_wecome_bg.png:名字像欢迎页素材,实际设备添加流程也用到它,后来必须补回。
六、图片和原生库:删错了,设备页也会坏
扫描动画不是只有几张图
设备扫描动画共有 75 帧。逐帧 PNG 加起来明显占空间,单纯改 ZIP 压缩参数,收益有限。
这次对它们做的是无损 WebP 重编码,并用解码后的 RGBA 像素逐帧比较。判断标准不是“肉眼看起来一样”,而是像素字节完全一致。
from PIL import Image
with Image.open(source) as image:
rgba = image.convert("RGBA")
original_pixels = rgba.tobytes()
rgba.save(target, format="WEBP", lossless=True, method=6, exact=True)
with Image.open(target) as image:
assert image.convert("RGBA").tobytes() == original_pixels重新封装时,为了沿用已有资源表和路径映射,部分原 .png 条目名保持不变,负载改为 WebP。这个处理建立在当前解码链与真机扫描界面检查之上;文件扩展名不再等于实际媒体编码,排查时应看文件头而不是后缀。
Fresco 不是只服务内容页
早期清理曾把图片相关原生库视为无关媒体依赖。后来设备图片和动画路径证明,Fresco、WebP、GIF 解码能力也被设备页共享,相关库必须恢复。
最终 arm64-v8a 下保留 7 个原生库:
libc++_shared.so
libjl_ota_auth.so
libgifimage.so
libimagepipeline.so
libstatic-webp.so
libwebp.so
libwebpimage.so它们分别支撑 C++ 运行时、杰理 OTA 相关能力和图片解码链。包中其他 ABI、地图和位置 SDK 等不再需要的原生负载被裁剪。库名像“媒体”,不代表它只属于媒体页面。
七、“删除 Chrome 内核”这件事,要先证明内核存在
用户最初希望检查 Chrome 内核是否可以移除。这是合理的体积排查方向,但“用了网页”与“APK 自带完整浏览器内核”是两件事。
本次检查了原包原生库、相关资源和业务入口,原包没有显示出独立 libchromium 或 libxwalkcore 这类大体积内核负载。搜索到的 X5.png 等资源名,也不足以证明存在腾讯 X5 内核——资源名称可能只是混淆后的名字。
最终保留的 7 个原生库里,同样没有独立浏览器内核。此次体积下降应归因于真实删掉的资源、ABI 和 SDK 负载,以及动画重编码等工作,而不是写成“删掉 Chrome,省了几十兆”。
网页商城或介绍入口可以退出业务流程;系统提供的 WebView 与 APK 自带浏览器库仍要分别判断。没有对应文件和调用证据,就不把它作为减包成果。
八、权限修复:Manifest 有声明,不代表真的申请过
真机最初出现的是顶部提示“请先允许 APP 获取位置权限”,但系统没有弹出申请窗口。点击开始配对,只短暂显示添加页,随后闪退。
这里存在两个不同问题:权限申请链未完成,以及配对相关 Activity 初始化失效。仅修改提示文案,或者把红色横幅隐藏,两个问题都还在。
权限部分沿着原工具库的过渡 Activity 和回调映射排查,补回 UtilsTransActivity 相关初始化及权限回调,使申请结果重新回到业务层。设备添加页则另外修复生命周期和监听器初始化。
权限判定还必须结合系统版本和 target SDK:面向 Android 12 及以上时,扫描、与已配对设备通信分别涉及 BLUETOOTH_SCAN、BLUETOOTH_CONNECT,而且需要运行时显式申请;较旧系统的扫描链还涉及位置权限。具体规则以 Android 蓝牙权限文档 为准。
这次没有把“位置权限”三个字机械替换为“附近设备”,也没有仅靠调整 Manifest 假装修复完成。验收点是:权限请求能发出,结果能回传,拒绝或授权后的配对流程仍保持正确状态。
九、系统蓝牙已连接,为何 App 仍不认识耳机?
音频连接与控制连接是不同状态
手机系统里的“已连接”主要说明系统蓝牙音频链路的状态。App 还要完成设备识别、型号匹配、控制通道和功能配置装载,才会显示相应的设备控制面板。
因此,显示一个“连接成功”标签,不足以证明已经恢复了 ANC、EQ 和固件操作。此前只有基础连接、却没有原版控制内容的版本,确实偏离了需求。
修复型号目录和冷启动加载顺序
排查时发现,本地设备目录过旧,只覆盖 3 组、3 个型号。后续替换为包含 5 组、189 个型号的目录,并补上 iKF-King S+ 的识别信息,其 VID 为 16162,即 0x3F22。
另一个问题在加载顺序:如果设备 Fragment 开始恢复保存的耳机时,型号目录尚未加载,就会出现系统已连接、App 却没有正确匹配设备的情况。因此在设备页使用目录前,先完成 EarphoneListManager.loadListData,并避免旧缓存继续覆盖新的打包目录。
注意:189 个型号是目录覆盖数量,不是 189 款耳机完成真机验证。此次物理设备测试围绕 King S+ 展开。
三个 ANC 按钮来自型号功能配置
King S+ 面板不是写死的三个按钮。它来自功能配置,由视图模型和命令分发链生成。最终包补入 assets/ikf_king_s_plus_panel.json,在识别到对应 VID 时加载。
配置中 ancDeepMode 的外层 cmdid 是 9,三个子模式为:
| 显示名称 | 子模式 cmdid | 深度选择 |
|---|---|---|
| ANC ON | 1 | 轻度 0、均匀 1、重度 2 |
| 通透 | 3 | 无深度选项 |
| 正常 | 2 | 无深度选项 |
这里的 cmdid 是配置和内部命令分发使用的标识,不是可以直接抄成 GATT 写入字节的完整协议报文。真正的下发还依赖原控制实现的封装和连接状态。
此外,设备状态分支曾抑制控制列表显示,导致“有连接,有电量,却没有 ANC 和操控卡片”。修复时同时处理了面板装载与可见性,而不是只往布局里加三个外观按钮。
下面这组对照展示修复前的真实中间状态:左图已经显示连接和电量,但电量下方直接进入操控卡片,缺少三个 ANC 模式;右图加载 King S+ 面板后,三种模式及降噪深度选项恢复。
| 修复前:中间版本缺少 ANC 控制 | 修复后:最终版恢复模式及深度选项 |
|---|---|
![]() | ![]() |
图 3:ANC 修复前后对照。左图来自调试中间版本,并非原版 App;两图滚动位置不同,只比较相关控件是否存在。右图选中 ANC ON,并显示轻度、均匀、重度。最后一轮还切换了通透和正常;界面选中状态不是声学降噪测试结果。
十、把“耳机操控”从空页面恢复成真实功能入口
操控页原本还依赖自身的 native 生命周期。修复 BTNSettingActivity 初始化后,King S+ 的配置项才真正进入页面。
最终恢复的 7 个入口及分发标识如下:
| 入口 | cmdid | 处理要点 |
|---|---|---|
| 寻找耳机 | 3 | 保留寻机音源、播放器与页面退出释放逻辑 |
| 游戏模式 | 14 | 保留型号配置中的控制项 |
| 定时关机 | 32 | 保留耳机功能中的时间选择,不与手机私人闹钟混淆 |
| 音量调节 | 15 | 恢复对应页面;当前路径包含系统媒体音量调节 |
| 提示音音量 | 41 | 按面板配置恢复设置入口 |
| 固件升级 | 13 | 保留 OTA 页面和相关 Presenter、原生依赖 |
| 更多设置 | -1 | 保留恢复默认、清除配对、恢复出厂等子项 |
寻找耳机页面保留了本地 MediaPlayer 循环播放及停止、释放逻辑,音量路径包含 AudioManager。这两点要如实描述:系统媒体音量不是“已经验证芯片内部增益控制”的证据,本地寻机声音也不是所有机型的独立寻机指令都已验证的证据。
| 修复前:中间版本操控页为空白 | 修复后:最终版显示 7 个功能入口 |
|---|---|
![]() | ![]() |
图 4:耳机操控页修复前后对照。左图只显示标题,没有功能内容;右图恢复型号面板中的设置入口。最后回归打开了相关页面和弹窗,但没有执行恢复出厂、清除配对等动作。
返听:生命周期里藏着完整初始化流程
BackListenActivity 要恢复的不只是布局。它还包括输入来源、声道、输出设备、耳返音量、输出延迟等默认值和监听器。
实际重建顺序包括:调用基类 onCreate,创建 ActivityEarphonebackBinding,设置内容视图,初始化输出声道和图标,接回输入源与立体声/单声道按钮,再设置输出选择以及两个 SeekBar 的回调。
缺少其中一段,就可能出现“页面开了,按钮没反应”或字段访问异常。最后真机验证了页面完整打开、控件可见和返回正常;麦克风采集与实际返听音频效果需要另行测试。
图 5:耳机返听配置页恢复。截图和导航回归证明初始化链已接上,不代表已完成音频延迟及音质测量。
煲机:保留设备功能,不恢复独立内容业务
煲机页、方案页、方案设置页分别修复了对应 Activity 的初始化,保留原有计划与进度界面,以及设备功能所需的音频和配置依赖。
图 6:原有煲机计划页。页面文案和方案时长存在原版遗留的显示不一致,本次没有把它改写成新的产品逻辑。煲机播放未在最后一轮回归启动。
这里的工作是恢复软件功能链,不是论证“煲机提升音质”。截图中原有宣传用语只反映原 App 的界面内容。
EQ 与 OTA:保留入口,还要保留其共享依赖
设备顶部的“音效”属于耳机控制,因此保留官方和自定义 EQ 页面,而不是与底部“内容”一起删除。
图 7:设备音效页显示原有 EQ 预设。最后一轮检查了页面和预设显示,没有进行 EQ 声学测量。
OTA 页恢复初始化时,还遇到一个隐蔽依赖:布局使用了已裁剪短信 SDK 里的圆形图片控件。为一个图片节点重新引入整个短信 SDK 很不划算,因此保留所需类名和三个构造入口,使用精简的 ImageView 兼容实现满足布局加载。
这属于局部兼容处理,不等于完整复刻该第三方控件的所有视觉属性。OTA 仍保留相关 Presenter 和杰理原生认证库;后续也应继续检验升级、重连和失败恢复链。
图 8:2026 年 9 月 30 日真机查询到当前版本 v4.16.6、页面显示可用版本 v4.16.9。本轮未开始固件传输,版本信息只是当时观察结果。
十一、私人闹钟:一个残留入口暴露出整条依赖链
最后真机测试时,点击“私人闹钟”再次闪退。日志给出的不是 native 方法错误,而是:
android.content.ActivityNotFoundException
com.ikf.earphone.v2ui.activity.alarmclock.AlarmCLockListActivity对应 Activity 已被前期精简删除,入口还留着。这是一个典型的“删了目标,却没删触发它的路径”的问题。
需求最终明确为:移除私人闹钟,保留耳机功能。 因此这次没有恢复整个手机本地闹钟模块,而是把相关依赖一并收尾:
DeviceStatusFragment的闹钟项设置GONE,移除点击监听器,并处理初始化和两个刷新分支,避免状态更新后又显示。- 从
V2MainActivity.onNewIntent去掉私人闹钟任务跳转。 - 从
BluetoothDisplayService的TIME_TICK路径去掉AlarmClockUtils调用,防止入口消失后后台每分钟仍触发旧逻辑。 - 清理后台设置页到闹钟详情页的路由。
- 删除私人闹钟的 Activity、模型、接收器和回调,共 40 个 Smali 类。
- 删除 19 个相关资源负载,从
assets/litepal.xml去掉AlarmClockBean映射。 - 保留设备侧仍复用的选择器;没有为了清理一个模型而随意提高数据库版本。
这轮删除了约 239,612 字节的 Smali 源文本和 152,980 字节的资源原始负载,但这些不是 APK 的直接减小量。组装、压缩和签名之后,整包从 45,500,962 字节变为 45,335,359 字节,实际减少 165,603 字节。
这次最有价值的经验是:删除功能必须覆盖入口、刷新逻辑、后台事件、模型映射和专属资源;同时还要保护共享组件。
十二、开屏慢:安装包变小,不等于首屏会快
页面和设备功能接上后,开屏仍然明显偏慢。把原因全归到“联网”或“图片多”,会错过真正的布局开销。
排查把注意力放到主界面初始化和复杂布局测量链,包括 ConstraintLayout.onMeasure、约束求解,以及滚动容器、ViewPager 与设备卡片组合后的重复测量成本。
本次对包内 ConstraintLayout.mOptimizationLevel 的四个构造默认值做了调整:
原值:0x107
最终保留值:0x17f中途试过 0x1ff,启动更快,但部分卡片消失。这不是成功优化,而是布局语义被破坏。实验涉及的 0x80 GRAPH_WRAP 位在此版本和页面组合下带来了显示问题,因此回退,再验证保留值对应的完整界面。
这些位掩码与具体库实现有关,不应当作所有 Android App 的通用加速参数。修改的验收标准必须同时包含耗时和布局完整性。
用相同方法重复测量,而不是只看一次感觉
2026 年 9 月 30 日,对修复前中间包和最终包各做 5 次进程冷启动:每次先 am force-stop,等待 700 ms,再执行 am start -W,保留 App 数据,读取 TotalTime。
| 次数 | 修复前中间包 | 最终包 |
|---|---|---|
| 1 | 3,488 ms | 1,377 ms |
| 2 | 3,657 ms | 1,536 ms |
| 3 | 4,952 ms | 1,535 ms |
| 4 | 5,101 ms | 1,451 ms |
| 5 | 5,062 ms | 1,921 ms |
| 中位数 | 4,952 ms | 1,535 ms |
图 9:两组 5 次进程冷启动的实际样本与中位数。保留全部数据点,避免用最快一次代表整体表现。
按中位数计算,耗时缩短约 69.0%。这组对照还包含其他功能与初始化修复,不是只改变一个布局位的隔离实验,因此该比例描述的是两份安装包的整体差异,而非 0x17f 的独立贡献。
测试也不是重启手机后的存储冷缓存实验,没有测量整个连接恢复到可交互状态的所有阶段。手机后台活动、缓存和系统调度仍可能影响结果,所以这里保留原始样本,而不是包装成一个确定的“加速倍数”。
十三、修改后的 APK 如何重组和交付
这次没有完整源码工程,构建采取“已验证资源骨架 + 修订 DEX + ZIP 增量替换”的方式。
原始 APK 留档并校验哈希
→ 运行时恢复业务 DEX
→ 反汇编,追踪调用,修改 Smali
→ 组装 DEX
→ 合并保留资源、型号面板和原生库
→ 去掉旧签名数据
→ ZIP 校验及对齐
→ 新签名
→ 再验证、真机安装与回归这里还有一个容易弄错的 Apktool 参数:-na/--no-apk 表示不把构建文件重新封成 APK,不是“禁用 aapt”。 它适合本次只获取构建产物、随后自定义封包的阶段。参数定义见 Apktool CLI 文档。
用户最初说“不用签名”,但进入真机安装阶段,仍需要一份通过安装校验的 APK。修改原包后,旧签名不再对应新内容,这次使用新的签名完成测试交付;这与保留原厂签名是两回事。对齐必须在 apksigner 签名前完成,签名后再改包会破坏签名校验,见 Android apksigner 文档。
最终包的实际验证命令包括:
& 'F:\ikf\device-only\tools\build-tools-r37\android-37.0\zipalign.exe' -c -p 4 'F:\ikf\device-only\MODIFIED_FILE.apk'
& 'F:\ikf\device-only\tools\build-tools-r37\android-37.0\apksigner.bat' verify 'F:\ikf\device-only\MODIFIED_FILE.apk'两条命令均无标准输出,退出状态为 0。这里记录的是项目实际采用的 -p 4 检查;Android 当前文档针对含 .so 的 APK 给出 -P 16 的页对齐指导,旧 -p 已标记弃用。本次没有将现有验证扩展成“所有 16 KiB 页面设备都已兼容”的结论,见 zipalign 文档。
早期安装器曾显示错误码 -2。单靠这一张提示图,还缺少定位 ZIP、解析、签名或打包具体问题的证据。后续以 CRC、对齐、签名以及 ADB 返回 Success 分别确认,而不是根据提示文案猜一个唯一原因。
十四、测试过程:模拟器用于定位,真机用于设备回归
1. 模拟器阶段
模拟器适合观察加固展开、DEX、资源、Activity 初始化和基础崩溃。耳机的真实蓝牙控制不应只靠模拟器验收,因此后来切换到无线 ADB 真机测试。
2. 真机调试本身也会制造干扰
无线调试端口在过程中多次变化,过期连接和自动发现记录曾导致连接握手停滞。更新手机首页的调试端口、清理旧连接,以及在该调试环境关闭 ADB 的 mDNS 自动连接后,继续测试。
锁屏或其他 App 占据前台,也会造成 UIAutomator 看不到目标页面。遇到空树时,先确认手机状态和顶层 Activity,再判断是否真的崩溃,避免把调试环境问题算在 APK 头上。
文章中的通用命令使用占位端点,不记录无线配对码:
$Serial = '手机IP:当前调试端口'
& 'F:\tool\Tools\AndroidTools\adb\adb.exe' connect $Serial
& 'F:\tool\Tools\AndroidTools\adb\adb.exe' -s $Serial install --no-incremental -r 'F:\ikf\device-only\MODIFIED_FILE.apk'
& 'F:\tool\Tools\AndroidTools\adb\adb.exe' -s $Serial logcat -b crash -d3. 回归不只是“点一遍看看”
真机回归脚本读取 UIAutomator XML,按文字和控件边界定位目标,再检查顶层 Activity、必需文本、应消失的入口、崩溃缓冲区,并在每一步保存截图与结构化记录。
最后一轮覆盖 20 个阶段,包含重复返回和恢复主界面的检查,不是 20 项独立硬件功能认证:
- 主界面,以及 ANC ON、通透、正常三个选中状态。
- 耳机操控、寻找耳机、音量、提示音、定时关机和更多设置。
- OTA、耳机返听、煲机、煲机方案、煲机设置。
- 改名弹窗、私人闹钟消失、回到设备页、EQ 页,再恢复设备页。
记录中的最后结果为:
REGRESSION_PASS=True
AUDIO_NOT_STARTED=True
FIRMWARE_NOT_FLASHED=True
RESET_NOT_TRIGGERED=True独立启动和型号列表烟雾测试还记录了:
START_EXIT=0 START_STATUS=ok
SAVED_DEVICE_CONNECTED=True BATTERY_TEXT=90%
NINE_DOT_LIST_OPEN=True RETURNED_TO_DEVICE=True
APP_CRASH_LOG_EMPTY=True4. 已验证与待验证,要分开写
| 项目 | 最后一轮实际覆盖 |
|---|---|
| 安装、冷启动、保存设备识别 | 已验证 |
| 九点型号列表打开并返回 | 已验证 |
| ANC 三种模式界面切换 | 已验证界面选中与深度控件显示 |
| 控制页、返听、煲机、EQ、OTA 页面 | 已验证导航、可见内容及期间无崩溃 |
| 私人闹钟入口和专属负载移除 | 已验证 |
| ANC 实际声学效果、EQ 输出变化 | 留待声学或设备侧反馈测试 |
| 麦克风返听、寻机声、长时间煲机播放 | 本轮未启动音频,留待专项测试 |
| 更改定时关机、实际音量参数 | 本轮检查界面,未更改对应数值 |
| 固件传输、恢复出厂、清除配对 | 本轮未执行,留待专项测试 |
“没有崩溃”和“页面内容恢复”是必要的回归结果,但它们与“每条设备指令都已闭环验证”之间还有距离。写技术复盘时,这条边界比一个漂亮的通过率更重要。
十五、版本、补丁和回滚:让每个结果对应一份确定的文件
反复修改二进制时,最怕测试的是 A 包、交付的却是 B 包。此次保留原包、阶段基线、最终包,以及补丁和验证记录,并用 SHA-256 关联测试输入。
主要交付记录位于:
| 文件 | 绝对路径 |
|---|---|
| 原包留档 | F:\ikf\device-only\ORIGINAL_FILE.apk |
| 最终安装包 | F:\ikf\device-only\MODIFIED_FILE.apk |
| 修改差异 | F:\ikf\device-only\DIFF_FILE.diff |
| 验证记录 | F:\ikf\device-only\VERIFICATION.txt |
| 可执行回滚脚本 | F:\ikf\device-only\ROLLBACK.sh |
| 真机阶段记录 | F:\ikf\device-only\phone_regression_0930.json |
| 启动原始数据 | F:\ikf\device-only\startup_measurement_0930.json |
原始包与留档副本一致,哈希为:
ACDEF3FF400EFFE7DBC18EE4A874DE8FBFF091958700C73775375EC6BFDA76ED最终 APK 哈希为:
985849B27B0D9BAA747A88F06549C2355148A6EA7B98202CE33CB430F6BFAC4F最新修改分支记录为 device-only-arm64/king-s-plus-features-startup-private-alarm-removal。针对私人闹钟收尾,使用下面两份输入做结构检查:
python 'F:\ikf\device-only\verify_feature_fix.py' 'F:\ikf\device-only\MODIFIED_FILE_BEFORE_ALARM_REMOVAL.apk'
python 'F:\ikf\device-only\verify_feature_fix.py' 'F:\ikf\device-only\MODIFIED_FILE.apk' modified两次命令的退出状态均为 0。第一份是移除私人闹钟前的基线,第二份是最终包。字面输出中的关键变化如下:
# BASELINE:输入 MODIFIED_FILE_BEFORE_ALARM_REMOVAL.apk
KING_PANEL_PRESENT=True ANC_MODES=[('ANC ON', 1), ('通透', 3), ('正常', 2)] SETTINGS=7
DEX_KING_PANEL_REF=True ABIS=['arm64-v8a']
PRIVATE_ALARM_CLASS=True ALARM_MAPPING=True PRIVATE_ALARM_LAYOUTS=4
# MODIFIED:输入 MODIFIED_FILE.apk,mode=modified
KING_PANEL_PRESENT=True ANC_MODES=[('ANC ON', 1), ('通透', 3), ('正常', 2)] SETTINGS=7
DEX_KING_PANEL_REF=True ABIS=['arm64-v8a']
PRIVATE_ALARM_CLASS=False ALARM_MAPPING=False PRIVATE_ALARM_LAYOUTS=0回滚测试在独立副本 F:\ikf\device-only\ROLLBACK_ALARM_TEST.apk 上执行,命令为:
& 'C:\Program Files\Git\bin\bash.exe' 'F:\ikf\device-only\ROLLBACK.sh' 'F:\ikf\device-only\ROLLBACK_ALARM_TEST.apk'该副本事先由最终包复制。回滚脚本恢复的是最新这次私人闹钟修改前的基线,不是最初 130.76 MiB 的原包。实际结果为:
RESTORED_SHA256=9143ae29b64d5ac4843da9c61a4ab6124ff7d0dcb301f559a9a8c9e424b87444
RESTORED_MATCH=True退出状态为 0。随后结构验证确认私人闹钟类、模型映射和四个对应布局恢复,King S+ 面板仍在。回滚副本没有安装到真机,最终 APK 继续保持修改后的状态,真机也重新安装了最终包。原来用于调试的屏幕超时设置恢复为 120,000 ms。
十六、这次逆向真正难的,不是“怎么删文件”
回头看,困难主要集中在五件事:
第一,加固把可见代码与真正运行代码分开。 运行时恢复 DEX 只是开始,native 生命周期还要按页面重建。
第二,UI、业务和后台服务之间存在隐式依赖。 一个私人闹钟不仅有页面,还有主界面刷新、Intent 路由、每分钟广播和 ORM 映射。
第三,设备能力由型号配置驱动。 名称匹配成功之后,还要加载正确面板,保留相应命令分发;一个通用连接页替代不了原版控制体验。
第四,资源和第三方库经常跨业务共享。 欢迎图可能服务配对页,图片库可能服务设备卡片,短信 SDK 的控件类可能被 OTA 布局引用。
第五,性能优化也可能制造功能退化。 0x1ff 的实验就是例子:更快却丢卡片,应该回退,不该拿耗时截图宣布完成。
如果继续迭代,我会优先补齐设备侧闭环验证,再考虑只保留 King S+ 的型号资产、剥离其他设备动画、收缩残留资源表和用法,以及拆分启动阶段耗时。当前主界面仍有“提示与使用手册”条目,它没有被本次 20 阶段回归列为专项验收项;这也不应被写成“所有非核心入口都已彻底清零”。
这些是后续方向,不是已经完成的成果。当前 43.24 MiB 是一份保留原设备体系、经过真机页面回归的折中结果,不是理论极限。
结语
这次项目最初看起来只是“删登录、删商城、删几个页面”,实际却变成了一次涵盖运行时 DEX、Smali、资源表、权限、BLE 型号识别、功能配置、native 生命周期、布局性能和真机回归的完整工程。
最终把安装包从 130.76 MiB 缩到 43.24 MiB,恢复 King S+ 的原版设备界面和核心控制入口,移除了私人闹钟,并让两份对照包的进程冷启动中位数从 4,952 ms 降到 1,535 ms。
比这些数字更值得记住的是:二进制精简不是把文件删到最少,而是弄清“谁依赖谁”,保住目标功能的完整调用链,并让每一次修改都有可重复的验证和回滚记录。
图片说明:本文含 1 张用户提供的原版对照截图、9 张项目真机调试截图和 2 张数据图表,共 12 张图片。其中最终版本截图来自 2026 年 9 月 30 日的回归;中间版本截图用于展示故障状态,两类基线分别标注。图表由实际 APK 与启动数据生成。所有图片均使用 HTTPS 外链,Markdown 内不再嵌入 Base64 数据。
