Android内存回收机制——Activity被系统回收了?

日常开发中经常会出现这样的情景,测试跟开发唇枪舌战“为什么从其他app返回,页面内容要重新加载呢,这肯定是个bug”,“Activity被系统回收了,不是bug”;“为什么切换到后台,这个app心跳不在线了呢,这肯定也是bug”,“这是后台进程被系统回收了,算什么bug”。内存回收是在Android开发中经常接触到的概念,但是你真的了解内存回收吗?这篇我们就来简单探讨下Android的内存回收机制。

Android内存回收机制

Addroid系统在设计时处于用户体验和性能优化的角度,设计了LMK机制:Low Memory Killer简称LMK,用于处理内存回收调度。

简单来说就是,当app切换到后台时,为了能在用户再次打开app时,及时进行响应加快app打开时间,Android系统对于切换到后台的app,并不会立即杀死回收,而是在保证系统资源足够的情况下,尽可能的提升app进程在系统内存中的留存时间,只有当系统内存资源不足时,按照一定的优先级将app进程回收,这套机制就是LMK。

Android进程分类

Android系统根据app当前状态和组件运行声明周期,按照进程重要程度,将app进程进行了分类。

在Android 5.0之前,系统将app进程分为了5类,分别是:

  • 前台进程——IMPORTANCE_FOREGROUND
    • 简单来说:当前用户正在操作的进程。
    • 比如一下任意一个,都会被认为是前台进程:
      • 它正在用户的互动屏幕上运行一个 Activity(其 onResume() 方法已被调用)。
      • 它有一个 BroadcastReceiver 目前正在运行(其 BroadcastReceiver.onReceive() 方法正在执行)。
      • 它有一个 Service 目前正在执行其某个回调(Service.onCreate()Service.onStart()Service.onDestroy())中的代码。
  • 可见进程——IMPORTANCE_VISIBLE
    • 正在进行用户当前知晓的任务,也就是说能够被用户感知到的进程。
    • 比如:
      • 它正在运行的 Activity 在屏幕上对用户可见,但不在前台(其 onPause() 方法已被调用)。举例来说,如果前台 Activity 显示为一个对话框,而这个对话框允许在其后面看到上一个 Activity,则可能会出现这种情况。
      • 它有一个 Service 正在通过 Service.startForeground()(要求系统将该服务视为用户知晓或基本上对用户可见的服务)作为前台服务运行。
      • 系统正在使用其托管的服务实现用户知晓的特定功能,例如动态壁纸、输入法服务等。
  • 服务进程——IMPORTANCE_SERVICE
    • 含一个已使用 startService() 方法启动的 Service
    • 注意:
      • 这里说的服务进程是指Service是单独的进程,此进程中没有启动过Activity。
      • 并且在30分钟内活跃过的服务进程。
  • 后台进程——IMPORTANCE_BACKGROUND
    • app切换到后台,app通常包含用户当前不可见的一个或多个 Activity 实例(onStop() 方法已被调用并返回)。
  • 空白进程——IMPORTANCE_EMPTY

当然在5.0之后的Android系统中,对进程分类再次进行的细化,比如:

  • Android 5.0,新增了
      • IMPORTANCE_PERCEPTIBLE
      • IMPORTANCE_CANT_SAVE_STATE
      • IMPORTANCE_CACHED
        • 等同于IMPORTANCE_BACKGROUND
    • Android 11.0,新增到了9种
      • IMPORTANCE_TOP_SLEEPING
        • 前台app,但是系统进入的休眠,此app进程。
      • IMPORTANCE_FOREGROUND_SERVICE
        • Service.startForeground()前台服务这种情况进行了拆分,单列。

这里就不详细介绍了,可以通过源码:android.app.ActivityManager.RunningAppProcessInfo查看详细的定义和注释介绍。

ADJ

刚开始提到了当系统内存资源不足时,系统会按照一定的优先级将app进程回收,这个优先级就是adj。

App进程启动时,Android系统会为其分配一个单独的adj,adj的取值会随着进程的状态和其组件的生命周期动态变化。

查看系统adj

Android维护了一套adj值与可用内存的对应关系,他们是一一对应的,当可用内存达到阈值时,会将对应adj的进行进行回收,释放资源。

minfree和adj文件是描述系统可用内存与对应进程优先级的关系,分成多个等级两个文件值是一一对应的。

aosp:/sys/module/lowmemorykiller/parameters # cat minfree
18432,23040,27648,32256,36864,46080
aosp:/sys/module/lowmemorykiller/parameters # cat adj
0,100,200,300,900,906

通过查看sys/module/lowmemorykiller/parameters/下的minfreeadj文件,可用获得内存阈值与adj对应关系。以上面数据为例:

注意:menfree值单位是page, 1page = 4K。

当系统可用内存小于46080 * 4 / 1024 = 180(M)时,系统会将adj大于906的进程回收,从而释放内存,同理,当系统可用内存小于18432 * 4 / 1024 = 72 M时,系统会将adj大于0的进程回收。

应用进程adj

App进程启动时,Android系统会为其分配一个单独的adj,进程adj的大小也可以通过adb命令进行查看。

// 查看应用进程号
aosp:/ # ps |grep com.zhong.event
u0_a45    2104  1060  1153372 92444          0 c7f29c02 S com.zhong.event
// 查看进程adj值
aosp:/ # cat /proc/2104/oom_score_adj
// com.zhong.event为前台应用,adb = 0
0

进程adj值,可以通过查看文件/proc/进程号/oom_score_adj获得。

我们按Home键,将应用切换到后台,再次查看其adj值:

aosp:/ # cat /proc/2104/oom_score_adj
700

手动kill掉进程后再次查看:

aosp:/ # cat /proc/2104/oom_score_adj
sh: cat: /proc/2104/oom_score_adj: No such file or directory
1|aosp:/ #

可以发现,进程的adj值是动态变化的,且只有当应用进程存活时,系统才会为期分配adj。

注意:adj取值根据系统版本不同,硬件配置不同,会有差异,但是关系是一样的。

adj级别

adj值是动态变化的,系统会根据当前app状态及四大组件生命周期变化等因素,动态改变adj值,常见级别如下:

以下类型的adj取值,同样会因为不同Android版本不同,硬件配置不同,甚至不同厂家等原因会有所差异。

ADJ级别 取值 含义
NATIVE_ADJ -1000 native进程
SYSTEM_ADJ -900 仅指system_server进程
PERSISTENT_PROC_ADJ -800 系统persistent进程
PERSISTENT_SERVICE_ADJ -700 关联着系统或persistent进程
FOREGROUND_APP_ADJ 0 前台进程,用户目前执行操作所需的进程。
VISIBLE_APP_ADJ 100 可见进程,正在进行用户当前知晓的任务,
PERCEPTIBLE_APP_ADJ 200 可感知进程,ForegroundService
BACKUP_APP_ADJ 300 备份进程
HEAVY_WEIGHT_APP_ADJ 400 重量级进程
SERVICE_ADJ 500 服务进程,纯Service进程
HOME_APP_ADJ 600 Home进程,launcher app
PREVIOUS_APP_ADJ 700 上一个进程
SERVICE_B_ADJ 800 B List中的Service,资源不足时Service进程,降级
CACHED_APP_MIN_ADJ 900 不可见进程的adj最小值,activity finish
CACHED_APP_MAX_ADJ 906 不可见进程的adj最大值

我们重点关注下表格中加粗部分就可以了,可以看到这其中好多类型与我们上文介绍的Android进程分类有很明显的对应关系。

adj < 0

这些进程基本都是系统处理的进程,不用太多关注。唯一需要注意的是PERSISTENT_SERVICE_ADJ这种情况,当app的AndroidManifest.xml中添加了android:persistent="true"属性,并且app必须是系统内置app(必须内置/system/app下),满足这个必要条件的基础上,系统在开机时会自动创建app进程,并且进程的优先级是PERSISTENT_SERVICE_ADJ(-800)。当手动杀死进程时,系统还会自动重启进程,这在开发系统内置应用时,处理进程保活会很有效。

FOREGROUND_APP_ADJ, adj = 0

对应进程类型的前台进程,当app满足一下任意条件时,就属于adj = 0的情况:

  • 它正在用户的互动屏幕上运行一个 Activity(其 onResume() 方法已被调用)。
  • 它有一个 BroadcastReceiver 目前正在运行(其 BroadcastReceiver.onReceive() 方法正在执行)。
  • 它有一个 Service 目前正在执行其某个回调(Service.onCreate()Service.onStart()Service.onDestroy())中的代码。

VISIBLE_APP_ADJ, adj = 100

对应可见进程,但是不包含Service.startForeground()前台服务情况:

  • 它正在运行的 Activity 在屏幕上对用户可见,但不在前台(其 onPause() 方法已被调用)。举例来说,如果前台 Activity 显示为一个对话框,而这个对话框允许在其后面看到上一个 Activity,则可能会出现这种情况。

PERCEPTIBLE_APP_ADJ, adj = 200

使用Service.startForeground()启动前台服务进程。

SERVICE_ADJ, adj = 500

对应服务进程:

  • 这里说的服务进程是指Service是单独的进程,此进程中没有启动过Activity。
  • 并且在30分钟内活跃过的服务进程。

HOME_APP_ADJ, adj = 600

Home进程,对应launcher属性的进程,类型为ACTIVITY_TYPE_HOME的应用。

PREVIOUS_APP_ADJ, adj = 700

用户使用的上一个进程(有activity没有finish),通常是用户两个app间切换时,被切换到后台的应用。

这里“上一个”的概念,不单只上一个使用app,如:用户从A应用切换到B应用,又从B应用切换到C应用,这时A应用可能是PREVIOUS_APP_ADJadj = 700。

CACHED_APP_MIN_ADJ, adj = 900

对应后台进程,app切换到后台:

  • app通常包含用户当前不可见的一个或多个 Activity 实例(onStop() 方法已被调用并返回)。
  • 通常是所有Activity都被finish。

其他的adj类型,大都是一些进程的特殊状态,不再详述了,这里在提一下SERVICE_B_ADJ

SERVICE_B_ADJ, adj = 800

SERVICE_B_ADJ类型,可以理解成是SERVICE_ADJ状态的降级,当出现一些特殊情况,如资源不足Service进程可能会被降级成SERVICE_B_ADJ

除此之外,对应同级别,同样adj值的进程,系统会优先回收占用内存高的app进程。

引用部分官方文档内容:

低内存终止守护进程

很多时候,kswapd 不能为系统释放足够的内存。在这种情况下,系统会使用 onTrimMemory() 通知应用内存不足,应该减少其分配量。如果这还不够,内核会开始终止进程以释放内存。它会使用低内存终止守护进程 (LMK) 来执行此操作。

LMK 使用一个名为 oom_adj_score 的“内存不足”分值来确定正在运行的进程的优先级,以此决定要终止的进程。最高得分的进程最先被终止。后台应用最先被终止,系统进程最后被终止。下表列出了从高到低的 LMK 评分类别。评分最高的类别,即第一行中的项目将最先被终止:

Android 进程,高分在上

以下是上表中各种类别的说明:

  • 后台应用:之前运行过且当前不处于活动状态的应用。LMK 将首先从具有最高 oom_adj_score 的应用开始终止后台应用。
  • 上一个应用:最近用过的后台应用。上一个应用比后台应用具有更高的优先级(得分更低),因为相比某个后台应用,用户更有可能切换到上一个应用。
  • 主屏幕应用:这是启动器应用。终止该应用会使壁纸消失。
  • 服务:服务由应用启动,可能包括同步或上传到云端。
  • 可觉察的应用:用户可通过某种方式察觉到的非前台应用,例如运行一个显示小界面的搜索进程或听音乐。
  • 前台应用:当前正在使用的应用。终止前台应用看起来就像是应用崩溃了,可能会向用户提示设备出了问题。
  • 持久性(服务):这些是设备的核心服务,例如电话和 WLAN。
  • 系统:系统进程。这些进程被终止后,手机可能看起来即将重新启动。
  • 原生:系统使用的极低级别的进程(例如,kswapd)。

设备制造商可以更改 LMK 的行为。

更多关于ADJ详细的介绍和相关介绍,可以参考大佬的文章:《解读Android进程优先级ADJ算法》,讲解的特别详细了,膜拜大佬。。

从进程回收来看服务保活

了解了Android进程回收的机制和原理,对后台服务的保活也有一定的启发,简单来说就是尽可能的提高服务进程的adj级别:

  • 特殊情况下可以通过android:persistent="true"属性将进程提升至PERSISTENT_SERVICE_ADJ(-800)级别,近乎无敌。当然了条件也是非常苛刻的:app必须内置为系统应用。
  • 使用前台服务Service.startForeground(),将进程提升至PERCEPTIBLE_APP_ADJ(200)
  • UI进程与服务进程分离,当app切换到后台,未分离的Service进程(app进程)降级为CACHED_APP_MIN_ADJ(900),而进程分离的Service进程,始终保持为SERVICE_ADJ(500)
  • 内存优化,对应同级别,同样adj值的进程,系统会优先回收占用内存高的app进程。

小结

回到最开始的问题:Activity被系统回收了?只可能是因为整个app进程被系统回收吗?其实并不是,我们知道Android系统为每个app进程分配一个内存上限,当app使用内存,超过此上限时,就会发生OOM异常。针对此场景Android系统也制定了一些对Activity栈进行回收的机制,简单来说:

对于单栈(TaskRecord)应用,在前台的时候,所有界面都不会被回收,只有多栈情况下,系统才会回收不可见栈的Activity

本文转自 https://juejin.cn/post/7070764959898009607,如有侵权,请联系删除。

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

推荐阅读更多精彩内容