App处于前台,Activity就不会被回收了?

昨天在康 KunMinX 大佬的:《重学安卓:Activity 生命周期的 3 个辟谣》,在加餐处看到这段:

转换后的理解:单进程场景,Activity被回收只可能是因为进程被系统回收了

感觉不太对?因为在很久以前,遇到过这样一个场景:

App打开多个Activity,然后手机晾一边,过一段时间后(屏幕常亮),点击回退,之前的Activity空白,然后重新加载了。

App在前台,不在栈顶的Activity却被干掉,但进程还健在,如果真是这样,就和上面的理解有些出入了。

立马写个代码验证下,大概流程如下:

写个父类Activity,生命周期回调加日志打印,接着打开一个Activity,包含一个按钮,点击后依次打开多个Activity,最后一个加个按钮,点一下就申请一个大一点的ByteArray来模拟内存分配,看内存不足时是否会回收Activity。

测试结果如下:

App宁愿OOM,也不愿意回收Activity,鬼使神差地加上 android:largeHeap="true" ,结果一样。

em...难道是我记错了???

等等!!!我好像混淆了两个东西:系统可用内存不足应用可用内存不足

0x1、系统可用内存不足

LMK机制

Android系统中,进程的生命周期由系统控制,处于体验和性能考虑,在APP中点击Home键或Back回退操作,并不会真的杀掉APP,进程依旧存在于内存中,这样下次启动此APP时就能更加快速。随着系统运行时间增长,打开APP越来越多,内存中的进程随着增多,系统的可用内存会越来越少。咋办,总不能让用户自己去杀进程吧,所以系统内置一套 回收机制,当系统可用内存达到一个 阈值,系统会根据 进程优先级 来杀掉一部分进程,释放内存供后续启动APP使用。

Android的这套回收机制,是基于Linux内核的OOM规则改进而来的,叫 Low Memory Killer,简称 LMK

阈值 & 杀谁

通过下述两个文件配合完成,不同手机数值可能不同,以我的老爷机 魅蓝E2 为例 (Android 11的Mix2S一直说没权限打开此文件):

# /sys/module/lowmemorykiller/parameters/minfree
# 单位:Page页,1Page = 4KB
18432,23040,27648,46080,66560,97280

# /sys/module/lowmemorykiller/parameters/adj
0,58,117,176,529,1000

Android系统会为每个进程维护一个 adj(优先级)

  • Android 6及以前称为:oom_adj,值范围:[-17,16],LMK要换算*1000/17
  • Android 7后称为:oom_score_adj,值范围:[-1000,1000]

然后,上面两个文件的值,其实是以一一对应的,比如:

66560  * 4 / 1024 = 260MB → 当系统可用内存减少到260MB时,会杀掉adj值大于529的进程;
18432 * 4 / 1024 = 72MB → 当系统可用内存减少到72MB,杀掉ajd值大于0的进程;

adj怎么看

直接通过命令行查看:

可以看到,adj是动态变化的,当App状态及四大组件生命周期发生改变时,都会改变它的值。常见ADJ级别如下:

  • NATIVE_ADJ → -1000,init进程fork出来的native进程,不受system管控
  • SYSTEM_ADJ → -900,system_server进程
  • PERSISTENT_PROC_ADJ → -800,系统persistent进程,一般不会被杀,杀了或者Carsh系统也会重新拉起
  • PERSISTENT_SERVICE_ADJ → -700,关联着系统或persistent进程
  • FOREGROUND_APP_ADJ → 0,前台进程
  • VISIBLE_APP_ADJ → 100,可见进程
  • PERCEPTIBLE_APP_ADJ → 200,可感知进程,比如后台音乐播放
  • BACKUP_APP_ADJ → 300,执行bindBackupAgent()过程的备份进程
  • HEAVY_WEIGHT_APP_ADJ → 400,重量级进程,system/rootdir/init.rc文件中设置
  • SERVICE_ADJ → 500,服务进程
  • HOME_APP_ADJ → 600,Home进程,类型为ACTIVITY_TYPE_HOME的应用,如Launcher
  • PREVIOUS_APP_ADJ → 700,用户上一个使用的App进程
  • SERVICE_B_ADJ → 800,B List中的Service
  • CACHED_APP_MIN_ADJ → 900,不可见进程 的adj最小值
  • CACHED_APP_MAX_ADJ → 906,不可见进程的adj最大值
  • UNKNOWN_ADJ → 1001,一般指将要会缓存进程,无法获取确定值

关于ADJ计算的详细算法分析可见Gityuan大佬的:《解读Android进程优先级ADJ算法》,干货多多,顺带从总结处捞一波进程保活伎俩:

  • UI进程与Service进程分离,包含Activity的Service进程,一进后台ADJ>=900,随时可能被系统回收,分离的话ADJ=500,被杀的可能性降低,尤其是系统允许自启动的服务进程,必须做UI分离,避免消耗较大内存;
  • 真正需要用户可感知的应用,调用startForegroundService()启用前台服务,ADJ=200;
  • 进程中的Service工作完,务必主动调用stopService或stopSelf来停止服务,避免占用内存,浪费系统资源;
  • 不要长时间绑定其他进程的service或者provider,每次使用完成后应立刻释放,避免其他进程常驻于内存;
  • APP应该实现接口onTrimMemory()和onLowMemory(),根据TrimLevel适当地将非必须内存在回调方法中加以释放,当系统内存紧张时会回调该接口,减少系统卡顿与杀进程频次;
  • 更应在优化内存上下功夫,相同ADJ级别,系统会优先杀内存占用的进程;

:能否把自己的App的ADJ值设置为-1000,让其杀不死? :不可以,要有root权限才能修改adj,而且改了重启手机还是恢复的。

扯得有点远了,回到问题上:

系统内存不足时,会在内核层直接查杀进程,不会在Framework层还跟你叨逼叨看回收哪个Activity。

所以在系统这个层面,单进程场景,Activity被回收只可能是因为进程被系统回收了,这句话是没毛病的,但在应用层面就不一定了。


0x2、应用可用内存不足

APP进程(虚拟机)的内存分配实际上是对 堆的分配和释放,为了整个系统的内存控制需要,会为每个应用程序设置一个 堆的限制阈值,如果应用使用内存接近阈值还尝试分配内存,就很容易引起OOM。

当然,不会那么蠢,还要开发仔自己在APP里回收内存,虚拟机自带 GC,这里就不向去卷具体的回收算法了

假设应用内存不足真的会回收Activity,那该怎么设计?一种解法如下:

应用启动时,开一个子线程,定时轮询当前可用内存是否超过阈值,超过的话干掉Activity

那就来跟下Android是不是也是这样设计的?

Activity回收机制

跟下应用启动入口:ActivityThreadmain()

跟下 attach()

这里就非常像,run()中计算:已用内存 > 3/4最大内存,就执行 releaseSomeActivities(),跟下:

=

所以 getService() 是获取了 IActivityTaskManager.aidl接口,具体的实现类是 ActivityTaskManangerService

继续往下跟: RootActivityContainerreleaseSomeActivitiesLocked()

跟下:WindowProcessControllergetReleaseSomeActivitiesTasks()

然后再往下走就是释放Activity的代码了:ActivityStackreleaseSomeActivitiesLocked()

具体咋释放,就不往下跟了哈,接着跟下是怎么监控的~

内存监控机制

跟回:BinderInternal.addGcWatcher()

这里可能看得你有点迷,但是当你理解了就会觉得很妙了:

虚拟机GC会干掉 WeakReference 的对象,在释放内存前,会调用对象的 finalize(),而这里有创建了一个新的 WeakReference 实例。下次GC,又会走一遍这里的代码,啧啧啧,相比起轮询高效多了

到此,应用内存不足回收Activity的流程就大概缕清了,接着可以写个代码验证下是否真的这样。

Demo验证

先试下两个Task的:

模拟内存分配的页面,然后一直点~

宁愿OOM,也不回收,试试三个~

好家伙,onDestory()了,此时按Back回退这些页面,发现走了onCreate(),即回收了,接着试试四个的情况:

可以,每次只回收一个Task,到此验证完毕了~

0x3、结论

  • 系统内存不足时,直接在内核层查杀(回收)进程,并不会考虑回收哪个Activity;
  • 进程内存不足时,如果此进程 Activity Task数 >= 3 且 使用内存超过3/4,会对 不可见 Task进行回收,每次回收 1个 Task,回收时机为每次gc;

参考文献

作者:coder_pig
链接:https://juejin.cn/post/7063068797304832030

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容