Android 应用卸载监控的实现方法

不再主业折腾 Android 了,这段时间会陆续把之前折腾过的东西记录一下,免得日后忘记。

对于产品经理和运营来说,知道哪个用户,什么时候,为什么会卸载你的 app 可能是很重要的事。虽然我觉得挺扯蛋——看看国内那些应用在卸载时弹出的页面,可选原因 12345,列的比用户想的都全面周到——这分明就是在说,我也知道自己干了很多坏事让你们不爽,可我干的坏事是如此之多以至于我实在弄不清你们的底线在哪里。

好吧,在我朝可以有理想,但是不能太理想主义。所以本文只从技术角度讨论一些实现细节。

首先来看这篇文章,基本勾勒出了实现卸载监控所需的技术框架。这篇文章有以下几个关键字:

  • JNI
    Android 应用被卸载之前,其运行实例就会被停止掉。因此必须要有代码能够脱离应用的生命周期管理。这个入口就是 JNI。进入 c 的世界,想象空间完全不是 Android API 可以比拟。
  • fork
    对于卸载监控这个需求来说,JNI 是入口,为的就是进入 fork 这一环。fork 本身是比 Java 更为历史悠久的系统调用,在这里负责生成监控进程,是整个逻辑中非常关键的一环。至少要把 fork 示例的执行逻辑搞懂(系统补习的话当然是建议读 APUE 相关章节,后面 exec 也会用到)。
  • inotify
    Linux 特有的文件监控 api,IBM 这篇介绍的很好。
  • UserSerial
    这个是 Android 4.2+ 引入的安全机制,原文说的很清楚,这里不再赘述。
但是这篇文章的实现方案两个明显不足:
  • fork 出来的进程仍旧是一个带着 dalvik 的庞大进程(内存 20MiB+)
  • 通过进程父子关系较容易找到卸载监控进程
所以,针对上述不足,可以继续改良实现方案:
  • exec
    将 inotify 部分的逻辑抽离到独立的可执行文件中,fork 出来的监控进程,可以通过 exec 这个独立的可执行文件来将自己变成一个非常轻量级的监控进程(内存占用只有 KiB 级别),同时也可以通过参数将伪装自己的进程名字。其实 fork + exec 的组合在传统的 unix 多进程编程中非常常见。
  • 两次 fork
    为了断绝父子进程关系,可以通过两次 fork 的技巧。主进程 A 通过 JNI 调用,进行第一次 fork,其父进程 A 继续运行,子进程 B 调用 exec 成为监控进程。监控进程 B 再进行一次 fork,父进程 B 直接退出,子进程 C 执行 inotify 逻辑监控 app 目录。由于进程 C 的父进程 B 直接退出,进程 C 将直接成为 init 的子进程,进程 C 和原始进程 A 之间看不到明显的关联关系。
上述技术环节打通之后,剩下的就是细节的打磨了。下面这些问题都比较简单,或是影响较小。考虑到我厂在黑科技上的投入,恕不巨细无遗了。

  • inotify 是面向 inode 的。当可执行文件在运行中被删除,inode 还在不在?会触发哪些 inotify 事件?文件夹呢?所以监控哪个目录,选择哪个 inotify 事件其实还是挺有讲究的事情。这些问题追到底其实很考较对 Linux 文件子系统的理解。
  • 如何将运行时参数(比如用户 id)传递给监控进程?监控进程既然已经跟主进程没有任何关系,那么在监控进程孤悬海外期间,主进程的用户 id 变化了怎么办?
  • 覆盖安装、升级时候的误判怎么办?360 某应用的方案竟然是在反馈页面中直接加了个『我在升级/重装』的选项,挺有意思。
  • 应该在主进程的哪个时间点启动监控进程?在 thread 中执行了 fork 会怎么样?fork 过的进程中线程还都在吗?fork 执行时父进程是个 Android 进程,而 Android 进程都是从 zygote 进程 fork 出来的。这样的进程直接 fork 会有问题吗?zygote 都做了些什么事让 fork 可能存在风险?
  • 如何避免多个监控进程同时存在?
  • 默认 ndk-build 编译出来的可执行文件 stdin/stdout/stderr 都被重定向到了 /dev/null,非常不利于调试,怎么办?

评论