6 个 App 还是 1 个大而全?我把一套安全工具拆了又拆,聊聊安卓项目的模块化取舍
PhoneGuard 开源系列收官篇(仓库:github.com/hnpdsyn/PhoneGuard1)。前五篇讲了 ADB 取证、图标伪装、指数退避、SIM 哨兵这些单点功能,这篇不聊功能,聊架构——为什么这套东西最终是 6 个独立 App,而不是 1 个"安全大师"。
从一个反直觉的决定说起
市面上同类产品的标准做法是做"全家桶":一个 App 塞进应用锁、防盗、日志,首页九宫格,开屏广告,会员弹窗。
我做 PhoneGuard 时反着来:一个功能一个 App。应用锁是 AppLock,SIM 防护是 SimGuard,ADB 防护是 AdbGuard——互相不依赖,装哪个用哪个。
三个理由,都很实际:
1. 权限最小化是安全工具的底线
一个隐私保护工具自己先要一堆权限,这本身就是讽刺。拆开之后,用户装 SimGuard 只需要短信和开机广播权限;装 AppLock 只需要无障碍服务。不想要的功能,权限一点不多给——这比任何隐私政策声明都有说服力。
2. 崩溃隔离
安全类 App 大量依赖系统回调:无障碍服务、设备管理器、SIM 广播。这些回调的触发时机不可控,谁知道哪个 OEM 会给你来点惊喜。拆成 6 个进程,一个模块被系统杀掉或者抛异常,其余五个照常工作。安防系统的第一原则:单点失效不能全链报废。
3. 按需安装本身就是卖点
有人只想锁微信,有人只想防换卡。独立 App 让用户各取所需,也让每个 App 的界面简洁到没有第二屏。
拆分实际操作:比想象中枯燥,比想象中多坑
起点是一个 5000+ 行的单工程。拆分的核心工作不是"剪切粘贴",而是依赖清算——每个模块到底依赖什么,画清楚才能下刀。
拆完的目录结构,六个工程长一个样:
plaintext
1
2
3
4
5
6
7
8
9
AppLock/
├── app/src/main/java/com/phonGuard/applock/
│ ├── core/ # ConfigManager、日志、工具类
│ ├── service/ # 无障碍服务等系统回调
│ ├── ui/ # Compose 界面
│ └── App.kt
├── app/build.gradle.kts
└── build_apk.bat # 一键编译脚本
几个踩过的坑,说出来省得你们再踩:
坑一:包名残留
拆分最阴险的 bug 不在编译期。代码里 import com.phonGuard.xxx.R 这种残留引用,如果只拆了文件没改包名,AS 会给你标红,好发现;但如果某处用的是全限定名字符串(比如 ComponentName(context, "com.phonGuard.old/.Service")),编译能过,运行时静默失效。拆完必须全局 grep 旧包名,一个都不能留。
坑二:Compose 版本锁死
拆分后统一了技术栈:Kotlin 1.9 + Compose 1.5.4 + Material 3。锁版本是因为某次随手升级后 HorizontalPager 签名变了,六个工程一起编译失败。依赖升级要一次一个工程试,别全家桶式同步升级——讽刺的是,这句话本来是我不推荐全家桶的理由。
坑三:共享代码怎么办
这是模块化最核心的取舍。core 包里的 ConfigManager、日志工具,六个工程各有一份几乎相同的拷贝。"正确"做法是抽 common 模块做依赖引用,但我没这么做——开源项目要照顾用 Android Studio 点开就编译的小白,复合构建和本地 Maven 对他们是天书。六份拷贝的维护成本,我认了。这是工程判断,不是偷懒。
迭代怎么不失控:以短信告警为例
独立工程最怕"改一个功能要改六个地方"。上周给五件套统一加入侵短信告警(换卡/USB/暴力破解触发时自动向紧急号码发短信),正好是一次实战检验。
做法是定义一个统一模式:每个工程的 core/ 下放一个同名 SmsAlertHelper object——
kotlin
1
2
3
4
5
6
7
8
9
10
11
object SmsAlertHelper {
private var lastSmsAt = 0L
private const val SMS_INTERVAL_MS = 10 * 60 * 1000L // 10分钟节流
fun sendIfNeeded(context: Context, phone: String, body: String, …) {
if (phone.isBlank()) return
// SEND_SMS 运行时权限检查,未授权静默跳过
// 节流判断 → 子线程发送 → 结果回调写日志
}
}
包名不同、代码几乎相同,然后每个工程的触发点(SIM 变更回调、USB 连接回调、密码失败回调)各加一行调用。
有人会问:这不就是复制粘贴吗?是的。但换来的好处是:SimGuard 的短信逻辑改坏了,绝不会影响 AppLock 的。五个工程可以各自独立升版——这次就是 SimGuard v1.1、AdbGuard v1.2、LoginGuard v1.2 各发各的。对独立开源项目来说,"每次发布只动一个包"比 DRY 重要得多。
如果重来一次?
诚实说,有两个地方我会改:
common 模块还是该抽,只是要用最保守的方式(源码级 Gradle include,不搞发布),提前半小时配置换后期少改五份
SecurityLog 该最先拆——它是唯一被其他模块"弱依赖"的(都往本地写日志),先拆它能倒逼日志接口先定型,后面拆分会顺很多
但"6 个独立 App"这个大方向不会变。安全工具的模块化不只是代码组织问题,是信任问题:用户能看到每个 App 只要它该要的权限,这比什么都值钱。
全系列导航
表格
篇 主题
1 六件套总览:为什么拆 6 个 App
2 ADB 取证:USB 调试入侵自动拍照
3 图标伪装 + 假崩溃:应用锁的"装死"艺术
4 设备管理器 + 指数退避:暴力破解处置引擎
5 SIM 换卡哨兵:ICCID 绑定的坑
6 本篇:模块化架构复盘
前五篇在我主页可以翻到。
开源仓库(马上 100 Star 了,差你一个 ⭐):
https://github.com/hnpdsyn/PhoneGuard1
Releases 直接下载全部 APK,每个工程都能用 build_apk.bat 一键编译。二次开发欢迎,issue 随时提。
这个系列暂时告一段落。下一阶段计划把换卡短EE信追踪(SIM 被换后新卡号码自动发到紧急联系人)的进阶玩法整理成篇——想看的评论区扣个 1。


