Android 服务为啥保不活
转发请注明出处:https://www.jianshu.com/p/bf0c82be9b25
请尊重原创作者
有一段时间都在说服务怎么保活,怎么才能不被杀死,各种花里胡哨的保活方案抄来抄去,到头来 能打的 一个都没有。
【硬核场景】
既然这么多可操作性低的“保命方案”,那么需要知道些什么知识点才能脱颖而出,好吧我们提供不了保活的黑科技,如果对方按套路出牌的话,至少会让你解释下为什么保不活。
【GC】
就是因为GC机制,所以我们的服务才保不活,但是这个过程我们不可控,很难让运行中的应用感知什么时候GC触发了,从而做点什么来补救。所以才有了 轮询机制 来唤醒,一段时间检查下服务有没有在跑,没有就再扶起来继续送。
通常情况下,触发GC的条件有两个:
1.当应用程序空闲时,即没有应用线程在运行时,GC会被调用(随心随意收垃圾)
2.Java堆内存不足时,GC会被调用(被强制叫去收垃圾)
D/dalvikvm: GC_CONCURRENT freed 2012K, 63% free 3213K/9291K, external 4501K/5161K, paused 2ms+2ms
各参数对照含义
D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_stats>, <Pause_time>
相信大家都很熟悉这log,但对它表达的意义估计就看不太懂,为了方便解释,把各参数对照在上面列出,简单说个参数,其它大家再去详细了解GC的参数解读吧。
GC Reason 就是指引起GC原因,有以下几种:
- GC_CONCURRENT:当堆开始填充时,并发GC可以释放内存。
- GC_FOR_MALLOC:当堆内存已满时,app尝试分配内存而引起的GC,系统必须停止app并回收内存。
- GC_HPROF_DUMP_HEAP:当你请求创建 HPROF 文件来分析堆内存时出现- 的GC。
- GC_EXPLICIT:显示的GC,例如调用System.gc()(应该避免调用显示的GC,信任GC会在需要时运行)。
- GC_EXTERNAL_ALLOC:仅适用于 API 级别小于等于10 ,用于外部分配内存的GC。
回到正题,GC为什么导致服务保不活?那是因为在当各种情况下触发了垃圾回收,它就会去找能宰的都宰掉,从而挤出内存给那些优先给用户接触到的应用或服务。所以才会有 Notification的前台服务 这样的做法让服务不轻易被杀死,尽可能把服务提升靠近用户,但是其核心思想都是为了改变 oom_adj 的等级,等级越低越重要越不容易被杀死。在解释改变等级之前,有个等级的概念需要先了解的。
【进程等级】
还是大家熟知的知识点,进程等级可以划分为这五大类:
1.前台进程
2.可见进程
3.服务进程
4.后台进程
5.空进程
他们在不同场景下,会对应到某一个值,具体查看方法可以用 adb 的命令查看。但这还只是等级概念,能大概地估算被杀死的可能性。而最终决定会不会被杀的是一个叫做 阀值 概念的值,这就涉及到 linux 的知识点,我们只需要知道 linux 会通过某个计算方式得出一个叫做 oom_score 的值,而这个就是和GC有直接关系了,因为根据不同情况,GC回收的程度不一样,那么每个等级都有对应阀值,只要没超过这个阀值的进程,就不会被回收。
比如说,这次GC只宰到 “后台进程” 的最低阀值,那么 “空进程”、“后台进程” 都会被宰,而 [服务进程]、[可见进程]、[前台进程] 这些进程安然无事,但某一时机这个阀值被系统调得更严格,需要更多内存了,调到 “服务进程” 的最低阀值,那么按照这个道理, “空进程”、“后台进程” 、“服务进程” 都会被宰掉。好,那么我们平时怎么知道自己的服务在什么等级呢?
【oom_adj】
adj级别 | 值 | 解释 |
---|---|---|
UNKNOWN_ADJ | 16 | 预留的最低级别,一般对于缓存的进程才有可能设置成这个级别 |
CACHED_APP_MAX_ADJ | 15 | 缓存进程,空进程,在内存不足的情况下就会优先被kill |
CACHED_APP_MIN_ADJ | 9 | 缓存进程,也就是空进程 |
SERVICE_B_ADJ | 8 | 不活跃的进程 |
PREVIOUS_APP_ADJ | 7 | 切换进程 |
HOME_APP_ADJ | 6 | 与Home交互的进程 |
SERVICE_ADJ | 5 | 有Service的进程 |
HEAVY_WEIGHT_APP_ADJ | 4 | 高权重进程 |
BACKUP_APP_ADJ | 3 | 正在备份的进程 |
PERCEPTIBLE_APP_ADJ | 2 | 可感知的进程,比如那种播放音乐 |
VISIBLE_APP_ADJ | 1 | 可见进程 |
FOREGROUND_APP_ADJ | 0 | 前台进程 |
PERSISTENT_SERVICE_ADJ | -11 | 重要进程 |
PERSISTENT_PROC_ADJ | -12 | 核心进程 |
SYSTEM_ADJ | -16 | 系统进程 |
NATIVE_ADJ | -17 | 系统起的Native进程 |
在终端输入命令(过程就大概是 先连接设备;进入shell;获得进程pid;查看值)
adb shell
cat /proc/{pid}/oom_adj
查看oom_adj的终端命令
如图所示了,某一个进程等级在 -13 那么对照到上面的表,可以知道起码是 系统进程 级别,那么普通的GC基本是宰不到它的。所以保活的核心,就是为了提升这个等级,类似的思想就有在AndroidManifest.xml的服务增加 优先级priority ,就尽量在这个阀值的范围往上靠,希望别宰到我。
【不过】
也不是没有办法保活,你需要知道一下这几个属性的作用,但有个前提咯!要提升到系统级别,唯一有效而且安全的做法,就是有 系统签名 。类似的,每个品牌每个主板每个型号都有会它的签名文件,就如平时我们自己生成的那种 jks、keystore 文件(只不过它们由 platform.x509.pem、platform.pk8 这些来生成),然后下面这些属性就能起作用
android:sharedUserId="android.uid.system"
这个属性使用应该会比较少,官方对它的解释:与其他应用程序共享的Linux用户ID的名称。默认情况下,Android为每个应用程序分配了自己唯一的用户ID。但是,如果将该属性设置为两个或多个应用程序相同的值,它们将共享相同的ID——前提是它们的证书集相同。具有相同用户ID的应用程序可以访问彼此的数据,如果需要,还可以在相同的进程中运行。
换言之,你得有系统签名才能和他们平起平坐。
persistent=true
这个在保活文章出现的频率就比较高了,但是却不怎么起作用,再来看看官方解释:应用程序是否应该一直运行—如果应该,则为“true”;如果不应该,则为“false”。默认值为“false”。应用程序通常不应设置此标志;持久性模式仅适用于某些系统应用程序。
通常情况下,我们的包都会安装到 data/data/ 目录下,而 persistent 需要 system/app 配套使用才会起效果的,也就是说应用本身就已经是系统应用,那么这个属性才生效,才会在系统启动的时候拉起有该属性的应用,并且被杀死后能够重启应用。
那我们怎么验证自己的属性有没有生效呢?
adb shell dumpsys meminfo
专业素养,打码了
看一下自己的应用包名有没有出现在 Persistent 列表就行了,按照网上那些保活方式,通常只会出现在 A Services 、Visible、Foreground 这些列表内。
【总结】
大家对服务为啥保不活,有了个整体概念,那么概括成一句话应该就是:GC可能会杀死等级 oom_adj 不太重要的服务,而我们所做的一切都是为了提升这个等级的值,让它不那么轻易被杀死。