这篇不是官方文档的中文翻译。我把 F-Droid 客户端的 APK 安装包下载到本地,用脚本把它当成 ZIP 打开,逐层解析了它的 AndroidManifest、签名块、资源表和两个 DEX 文件,把量出来的结果如实记在这里。文中每一个数字都来自这台机器上的实测,文末给出了复现命令,你可以自己跑一遍核对。
先看这组最关键的数据。它既是判断文件有没有被改动的依据,也是后面所有分析的起点。
| 文件大小 | 12,426,276 字节(11.85 MB) |
|---|---|
| 版本名 / 版本号 | 1.23.2 / 1023052 |
| 包名 | org.fdroid.fdroid |
| 最低系统要求 | Android 6.0(API 23) |
| 目标系统 | Android 11(API 30) |
| 编译 SDK | Android 15(API 35) |
| 安装位置 | auto(由系统决定,允许移至 SD 卡) |
| 包内文件条目 | 1,119 个 |
| SHA-256 | 985f5181d48bb6bafd54083a048b391271e0ab28385881cc41294fb01a222762 |
| SHA-1 | f94c745d25f13de8bf39e702659c19b6d8ca95b7 |
| MD5 | ec6a9a8560924bb833942a7c3256847a |
| 签名证书 SHA-256 | 43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab |
APK 本质是个 ZIP 包。把 1,119 个条目按目录归并后,体积构成如下。这里同时列出压缩前和压缩后的大小,因为 Android 安装时看的是解压后的体积。
| 组成部分 | 未压缩 | 压缩后 | 占比 | 文件数 |
|---|---|---|---|---|
| classes2.dex | 8.66 MB | 3.37 MB | 38.37% | 1 |
| classes.dex | 8.36 MB | 3.54 MB | 37.03% | 1 |
| resources.arsc(资源索引表) | 3.91 MB | 3.91 MB | 17.34% | 1 |
| res/ 资源目录 | 1.16 MB | 0.66 MB | 5.15% | 1,067 |
| META-INF 签名与清单 | 0.22 MB | 0.09 MB | 0.96% | 22 |
| kotlin / okhttp3 / org 资源 | 0.16 MB | 0.06 MB | 0.69% | 11 |
| AndroidManifest.xml | 0.05 MB | 0.01 MB | 0.20% | 1 |
| lib/ 原生库 | 36.5 KB | 36.5 KB | 0.16% | 4 |
| assets/ 资产文件 | 0.02 MB | 0.02 MB | 0.08% | 8 |
包内解压后的总大小是 22.56 MB,压缩后 11.70 MB,而 APK 文件本身 11.85 MB——多出来的 0.15 MB 是 ZIP 目录结构、4 KB 的 APK 签名块和结尾记录等容器开销。
第一个值得注意的发现:代码占了四分之三。两个 DEX 文件合计 17.01 MB,占解压总量的 75.4%。这意味着 F-Droid 客户端几乎全部由 Java/Kotlin 代码实现。
第二个发现更极端:原生库只有 36.5 KB。整个包里只有 1 个 .so 文件(libandroidx.graphics.path.so),为 4 种 CPU 架构各打了一份,加起来 36.5 KB,占全包 0.158%。具体是:
| CPU 架构 | 设备类型 | 库大小 |
|---|---|---|
| arm64-v8a | 主流安卓手机(64 位) | 9.9 KB |
| armeabi-v7a | 老旧安卓手机(32 位) | 7.1 KB |
| x86_64 | 64 位平板 / 模拟器 | 10.5 KB |
| x86 | 32 位平板 / 模拟器 | 9.1 KB |
几乎没有原生代码,带来两个实际好处:一是同一份安装包能覆盖从老旧 32 位手机到 x86 模拟器的全部设备类型,不需要按架构分发;二是原生代码是安全审计最难的部分,代码量越小,可被隐藏的东西越少。
另外,resources.arsc 有 3.91 MB,占 17.34%,这个比例偏高。原因是 F-Droid 客户端内置了大量多语言字符串资源,语言资源都集中在这张表里。
这是下载站最该做也最少做的一步。我用二进制方式定位并解析了 APK 里的签名块,结果是这个包同时带有 v1、v2、v3 三代签名,外加一个 verity 完整性校验块。
| 签名方案 | 标识 / 位置 | 适配系统 | 实测结果 |
|---|---|---|---|
| v1(JAR 签名) | META-INF/CIARANG.SF、CIARANG.RSA | 全部 Android 版本 | 存在 |
| v2(APK 签名方案 v2) | 签名块 ID 0x7109871a | Android 7.0 及以上 | 存在 |
| v3(APK 签名方案 v3) | 签名块 ID 0xf05368c0 | Android 9.0 及以上 | 存在 |
| verity(完整性校验) | 签名块 ID 0x42726577 | Android 11 及以上 | 存在 |
签名证书的主体信息是 C=UK, CN=Ciaran Gultnieks,有效期到 2037 年 12 月 8 日。这个 CN 正是 F-Droid 项目创始人的名字,也是 F-Droid 官方客户端一直使用的签名密钥。证书有效期一直排到 2037 年,不是那种几个月就过期的临时签名。
这三代签名是递进关系,不是重复劳动:v1 是老的 JAR 签名,兼容所有系统但校验粒度粗;v2 从 Android 7.0 起对整个 APK 做分块哈希校验,改一个字节就会失败;v3 从 Android 9.0 起支持签名密钥轮换,v3 块里带 minSDK / maxSDK 字段。只要这三个签名都能通过校验,就能确定这个包从官方构建出来之后没有被改过、没有被重新打包、没有被人塞入额外代码。
从 AndroidManifest 里解析出 28 项权限声明。数量听起来不少,但对一个应用商店来说是正常范围——它要下载、要安装、要管理更新。关键是看每一项有没有对应到具体功能。下面按用途分成四类。
| 权限 | 用途 |
|---|---|
| INTERNET | 连接软件仓库,下载应用索引和 APK |
| ACCESS_NETWORK_STATE | 判断当前是否联网、是 Wi-Fi 还是移动数据 |
| ACCESS_WIFI_STATE | 获取 Wi-Fi 信息,用于局域网内互换应用 |
| REQUEST_INSTALL_PACKAGES | 请求安装 APK,应用商店的核心权限 |
| REQUEST_DELETE_PACKAGES | 请求卸载应用 |
| UPDATE_PACKAGES_WITHOUT_USER_ACTION | 自动更新应用时不再逐个弹窗(需用户先授权) |
| FOREGROUND_SERVICE | 下载和安装过程在前台服务中运行,不会被系统杀掉 |
| POST_NOTIFICATIONS | 发出「有应用可更新」的通知 |
| RECEIVE_BOOT_COMPLETED | 开机后自动检查应用更新 |
| WAKE_LOCK | 下载大文件时保持 CPU 唤醒 |
| MANAGE_EXTERNAL_STORAGE | 读写存储中的 APK 文件与本地仓库 |
| QUERY_ALL_PACKAGES | 读取已安装应用列表,用于比对版本判断更新 |
其中 QUERY_ALL_PACKAGES 是 Google Play 政策里管得最严的一项,因为它能看到设备上装了什么。但对应用商店来说,不知道已装应用的版本就无法判断哪些需要更新,属于功能必需。F-Droid 的处理方式是:这个列表只在本地比对,不上报。
F-Droid 有一个特色功能:两台手机之间不经过服务器,直接通过局域网、蓝牙或 NFC 互传已下载的应用。这在没网的环境下很有用,代价就是需要下面这些权限。
| 权限 | 用途 |
|---|---|
| CHANGE_WIFI_MULTICAST_STATE | 局域���内组播发现附近设备 |
| CHANGE_NETWORK_STATE | 开启本地热点做应用互换 |
| CHANGE_WIFI_STATE | 同上,控制 Wi-Fi 状态 |
| BLUETOOTH | 蓝牙互换应用(旧版蓝牙权限) |
| BLUETOOTH_ADMIN | 管理蓝牙配对(旧版蓝牙权限) |
| BLUETOOTH_SCAN | 扫描附近蓝牙设备(Android 12 起) |
| BLUETOOTH_CONNECT | 连接蓝牙设备传文件(Android 12 起) |
| USB_PERMISSION | 通过 USB 与电脑或其他设备互换应用 |
| 权限 | 用途 |
|---|---|
| NFC | 两台手机背靠背 NFC 触碰即可传送应用 |
| CAMERA | 扫描二维码添加第三方软件仓库 |
| READ_EXTERNAL_STORAGE | 读取存储中已有的 APK 文件 |
| WRITE_EXTERNAL_STORAGE | 把下载的 APK 写入存储 |
| ENFORCE_UPDATE_OWNERSHIP | 声明应用更新所有权,防止其他商店抢更新 |
| WRITE_SETTINGS | 修改部分系统设置项 |
CAMERA 只用于扫二维码加仓库,不用于拍照录像,这一点可以在实际使用中验证:不扫码时系统不会弹出相机调用提示。
剩下两项不是向系统申请的权限,而是 F-Droid 自己声明的,用于组件间通信隔离:
org.fdroid.fdroid.permission.UPDATE_REPOS — 保护仓库更新接口,只允许自身调用org.fdroid.fdroid.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION — AndroidX 自动生成的权限,用于防止动态广播接收器被外部应用调用这是很多人关心 F-Droid 的核心原因。我把两个 DEX 文件里的字符串全部提取出来(共 370 万字符),逐一比对常见的商业追踪与广告 SDK 特征。
| 检测项 | 类型 | 结果 |
|---|---|---|
| Google Analytics | 行为分析 | ✓ 未发现 |
| Firebase Analytics | 行为分析 | ✓ 未发现 |
| Firebase Crashlytics | 崩溃上报 | ✓ 未发现 |
| AppsFlyer | 归因追踪 | ✓ 未发现 |
| Adjust | 归因追踪 | ✓ 未发现 |
| Facebook SDK | 社交追踪 | ✓ 未发现 |
| 友盟 Umeng | 国内统计 | ✓ 未发现 |
| 腾讯 Bugly | 崩溃上报 | ✓ 未发现 |
| Sentry | 错误监控 | ✓ 未发现 |
| AdMob | 广告 | ✓ 未发现 |
10 项商业追踪与广告 SDK,全部零命中。这个结果也印证了 F-Droid 的收录原则:官方仓库里的应用不允许包含专有追踪器,客户端自身同样遵守这条规则。
排除了商业追踪之后,我统计了确实存在的开源组件:
| 组件 | 用途 | 命中次数 |
|---|---|---|
| OkHttp3 | 网络请求库(Apache 2.0) | 303 |
| BouncyCastle | 加密与证书校验(含 APK 签名校验) | 500 |
| Kotlin 协程 | 异步任务调度 | 59 |
| Glide / Coil | 应用图标加载 | 20 |
| SLF4J | 日志门面 | 3 |
| ACRA | 崩溃报告(详见下方说明) | 2 |
org.acra.*)。但它的工作方式和 Crashlytics 完全不同:ACRA 默认处于关闭状态,只有当你主动开启后,应用在发生崩溃时才会弹出对话框,让你手动选择要不要把崩溃日志发送出去。它不会在后台静默上传任何东西。这与「默认开启、静默上传」的商业崩溃上报有本质区别。
从两个 DEX 中提取到 33 个不同的硬编码域名。我逐一核对,全部是开源项目的基础设施,没有一个属于广告、统计或数据变现服务商。以下是其中具有代表性的一部分:
| 域名 | 用途 |
|---|---|
| f-droid.org | F-Droid 官方主仓库 |
| github.com | 应用源码仓库链接 |
| gitlab.com 系列 | 部分应用的源码托管 |
| guardianproject.info | Guardian Project 第三方仓库 |
| apt.izzysoft.de | IzzyOnDroid 第三方仓库 |
| briarproject.org | Briar 项目仓库 |
| microg.org | microG 项目相关 |
| gateway.ipfs.io | IPFS 网关,用于分布式仓库 |
| liberapay.com / opencollective.com | 开源项目捐赠渠道 |
| spdx.org | 开源许可证标识标准 |
| flattr.com | 创作者小额打赏 |
| www.bouncycastle.org / www.slf4j.org | 所依赖开源库官网 |
整张域名清单的性质很一致:要么是软件仓库地址,要么是开源项目的主页和捐赠页,要么是所依赖开源库的官网。没有出现任何一家广告联盟、数据统计或用户画像服务商的域名。
我解析了 resources.arsc 的资源字符串池,共 44,157 条字符串。在其中直接检索到「应用」「下载」「设置」「更新」「安装」「权限」「搜索」「仓库」「版本」等简体中文界面文案,说明客户端内置简体中文,安装完成后无需额外下载语言包。
3.91 MB 的 resources.arsc 能占到全包 17.34%,正是因为它把包括中文在内的多语言字符串资源都打包了进去。
实测数据里有一处看起来矛盾的地方:这个包是用 Android 15(API 35)的 SDK 编译的,最低支持 Android 6.0(API 23),但 targetSdkVersion 只设到了 30(Android 11)。
这是有意为之,不是偷懒。从 Android 11 之后,Google 对存储权限、软件包可见性、后台活动等一系列行为做了收紧。F-Droid 作为一个需要在前台管理 APK 文件和已装应用列表的应用商店,如果贸然提升 targetSdk,部分核心功能(比如直接读写存储里的 APK、完整的已装应用列表)会被系统限制掉。
代价是:在 Android 12 及以上的设备上,系统会按照旧版兼容行为来运行它,部分新系统的行为变更不会生效。对功能完整性来说这是划算的取舍,但用户应当知道这一点。
28 项权限里,有 8 项是给「局域网 / 蓝牙 / NFC 互换应用」这个功能准备的,6 项是给存储读写和二维码扫码准备的。如果你不需要互换功能,可以在系统设置里单独关掉蓝牙、NFC 相关权限,不影响下载和安装这两个核心功能。
上面的数据不希望你只信我的结论。以下命令可以在你自己的机器上把关键结果重新跑一遍。
Windows 自带的 certutil 就能算,不需要装任何工具:
certutil -hashfile F-Droid.apk SHA256
certutil -hashfile F-Droid.apk SHA1
certutil -hashfile F-Droid.apk MD5
macOS 或 Linux 用:
sha256sum F-Droid.apk
sha1sum F-Droid.apk
md5sum F-Droid.apk
把输出的十六进制串和本文第一节的表格对照,完全一致就说明你手上的文件和本次测试的是同一个。
需要 Android SDK 的 build-tools,里面有 apksigner:
apksigner verify --verbose --print-certs F-Droid.apk
正常情况下会看到 v1、v2、v3 三行都是 true,证书主体是 CN=Ciaran Gultnieks。
unzip -l F-Droid.apk
会列出全部 1,119 个条目及其压缩前后大小,可以核对第二节的体积构成表。
aapt2 dump badging F-Droid.apk
输出里包含 package name、versionCode、versionName、sdkVersion、targetSdkVersion、uses-permission 全部信息,可核对第四节。
把这次实测的结果汇总成一句话:这是一个体积不大(11.85 MB)、几乎纯 Java/Kotlin 实现(原生库仅 36.5 KB)、带 v1+v2+v3 三重签名、不含任何商业追踪与广告 SDK 的应用商店客户端。
几点实际参考价值:
需要补充一点:本文的测试对象是 1.23.2 这一个版本。后续版本更新后,体积、权限、SDK 版本都可能变化,届时会重新实测更新。想下载这个版本,可以前往 F-Droid 下载页;想了解安装步骤,参见 F-Droid 安装教程。