TypechoJoeTheme

霍雅的博客

登录
用户名
密码
/
注册
用户名
邮箱

霍雅

追求源于热爱,极致源于梦想!
网站页面
文章目录

把IKF 130.76 MiB 的耳机 App 精简到 43.24 MiB:Android APK 逆向、修复与真机验证实录

2026-09-30
/
0 评论
/
14 阅读
/
正在检测是否收录...
09/30

把 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 混在一起。

项目实际结果
原始 APK137,113,491 字节,130.76 MiB
最终 APK45,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 MiB19.16 MiB去除无关资源负载,扫描动画无损重编码,保留设备布局和必要图像
assets/39.22 MiB12.03 MiB清理皮肤及无关业务素材,保留设备动画、型号数据和功能面板
lib/23.72 MiB2.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。

五、删页面的时候,要同时看入口、绑定和资源编号

裁剪工作主要落在三层:

  1. 导航和组件层:主入口回到设备主界面;清理非设备页的导航、清单组件和触发路径。
  2. 业务字节码层:移除无关页面及专属回调,修正仍然触发它们的调用。
  3. 资源与依赖层:去掉独占素材和 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 ON1轻度 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。

次数修复前中间包最终包
13,488 ms1,377 ms
23,657 ms1,536 ms
34,952 ms1,535 ms
45,101 ms1,451 ms
55,062 ms1,921 ms
中位数4,952 ms1,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 -d

3. 回归不只是“点一遍看看”

真机回归脚本读取 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=True

4. 已验证与待验证,要分开写

项目最后一轮实际覆盖
安装、冷启动、保存设备识别已验证
九点型号列表打开并返回已验证
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 数据。

朗读
赞(0)
版权属于:

霍雅的博客

本文链接:

https://huoya.work/bk/index.php/archives/574/(转载时请注明本文出处及文章链接)

评论 (0)

人生倒计时

今日已经过去小时
这周已经过去天
本月已经过去天
今年已经过去个月