Android开发——Android多进程以及使用场景介绍

0.  前言

在Android中,默认情况下,同一应用的所有组件均运行在同一进程中,且大多数应用都不会改变这一点。不过,单进程开发并不是Android应用的全部,今天我们就来说说Android中的多进程开发以及多进程的使用场景。

1.  进程

我们都知道Android系统是基于Linux改造而来的,进程系统也是一脉相承,进程其实就是程序的具体实现。当程序第一次启动,Android会启动一个Linux进程(具体由Zygote fork出来)以及一个主线程,默认的情况下,所有组件都将运行在该进程内。同一个应用由系统分配一个独立的Linux账户,该应用产生的所有进程,都会是这同一个Linux账户。

2.  使用多进程

在开发中,我们通常会使用修改清单文件的android:process来达到多进程的目的。如果android:process的value值以冒号开头的话,那么该进程就是私有进程,如果是以其他字符开头,那么就是公有进程,这样拥有相同 ShareUID 的不同应用可以跑在同一进程里。至于创建进程的具体源码分析,网上有一篇很详细的文章,在这就不重复造*了,有需要的朋友可以前往理解Android进程创建流程

还有一种方法开启进程,是通过JNI利用C/C++,调用fork()方法来生成子进程,一般开发者会利用这种方法来做一些daemon进程,来实现防杀保活等效果。

3.  进程优先级

Android利用重要性层次结构,就是将最重要的保留,杀掉不重要的进程。

Android将重要性层次结构分为5个层级,具体可以查看Android开发——Android进程保活招式大全中1.1部分的内容,这里就不赘述了。

根据进程中当前活动组件的重要程度,Android 会将进程评定为它可能达到的*别。例如,如果某进程托管着服务和可见 Activity,则会将此进程评定为可见进程,而不是服务进程。

此外,一个进程的级别可能会因其他进程对它的依赖而有所提高。例如进程 A 中的内容提供程序为进程 B 中的客户端提供服务,或者进程 A 中的服务绑定到进程 B 中的组件,则进程 A 始终被视为至少与进程 B 同样重要。

由于运行服务的进程其级别高于托管后台 Activity 的进程,因此启动长时间运行操作的 Activity 最好为该操作启动服务,而不是简单地创建工作线程。例如,正在将图片上传到网站的 Activity 应该启动服务来执行上传,这样一来,即使用户退出 Activity仍可在后台继续执行上传操作。使用服务可以保证无论 Activity 发生什么情况该操作至少具备“服务进程”优先级。同理,广播接收器也应使用服务,而不是简单地将耗时冗长的操作放入线程中。

4.  Low Memory Killer

Android的Low Memory Killer基于Linux的OOM机制,Low Memory Killer会根据进程的adj级别以及所占的内存,来决定是否杀掉该进程,adj越大,占用内存越多,进程越容易被杀掉。

关于adj的分级,我们可以参考ProcessList.java,这里的常量定义了ADJ的分级。

UNKNOWN_ADJ = 16
//级别最低级的进程,通常是被缓存的进程,但是系统也不清楚缓存的内容 CACHED_APP_MAX_ADJ = 15
//这是一个只托管不可见的活动的进程,因此可以在没有任何中断的情况下被杀死 CACHED_APP_MIN_ADJ = 9
//缓存进程,没有英文解释 SERVICE_B_ADJ = 8
//不活跃的服务,不像adj=5的服务那么活跃
//在root以后,有的系统优化大师会把所有服务都调成adj=8来达到内存优化的目的
//因为当所有人adj都比较高时,这样才能保证名正言顺的杀进程 PREVIOUS_APP_ADJ = 7
//被切换的进程,一般是用户前一个使用的进程。两个应用来回切换,那么前一个应用一般adj设置为7 HOME_APP_ADJ = 6
//与主应用程序有交互的进程 SERVICE_ADJ = 5
//活跃的服务进程 HEAVY_WEIGHT_APP_ADJ = 4
//高权重进程 BACKUP_APP_ADJ = 3
//正在备份的进程 PERCEPTIBLE_APP_ADJ = 2
//可感知进程,通常是前台Service进程 VISIBLE_APP_ADJ = 1
//可见进程 FOREGROUND_APP_ADJ = 0
//前台进程

剩下的就是adj值为负数的进程,基本上都是系统集成,不在本文的讨论范围内。负数进程是不会被lmk杀掉的。

5.  如何查看进程优先级和设备的内存临界值

首先通过 adb shell ps 指令查找对应进程的pid。

然后通过 adb shell cat /proc/${pid}/oom_adj(设备需要root)返回对应进程的adj值。

我们可以通过adb shell cat 查看下面两个文件,cat之前请先用chmod赋予权限,adj 代表的是oom_score_adj的值,对应的minfree则代表内存临界值。

/sys/module/lowmemorykiller/parameters/adj

/sys/module/lowmemorykiller/parameters/minfree

比如我的测试机小米4C测试机对应的值就是:

adj: 0,58,117,176,529,1000

这个值其实是oom_score_adj的值,用这个值*17再除1000四舍五入取整数,就是对应的adj的值,例如第二个值58即为 58*17/1000 = 1,对应的adj也就是1,1000默认就是15。所以这6个值对应的adj是0,1,2,3,9,15。

minfree: 18432,23040,27648,32256,56250,81250

这个值是页值,一页等于4KB,换算成MB大概是72,90,108,126,220,318

当可用内存小于318MB的时候,系统开始杀adj=15的进程,以此类推。

6.  什么情况需要使用多进程

举个例子,现在要做一款音乐播放器,现在有以下几种方案:

A. 在Activity中直接播放音乐。

B. 启动后台Service,播放音乐。

C. 启动前台Service,播放音乐。

D. 在新的进程中,启动后台Service,播放音乐。

E. 在新的进程中,启动前台Service,播放音乐。

A方案

我们的播放器是直接在activity中启动的。首先这么做肯定是不对的,我们需要在后台播放音乐,所以当activity退出后就播不了了,之所以给出这个例子是为了控制变量作对比。

音乐播放器无非是打开app,选歌,播放,退到桌面,切其他应用。我们选取了三个场景,打开、按home、按back退回桌面。让我们看一下A的相对应的oom_adj、oom_score、oom_score_adj的值。

Android开发——Android多进程以及使用场景介绍

Android开发——Android多进程以及使用场景介绍

Android开发——Android多进程以及使用场景介绍

从上述三个场景的结果来看,当我们应用在前台的时候,无论adj还是score还是score_adj,他们的值都非常的小,基本不会被LMK所杀掉,但是当我们按了Home之后,进程的adj就会急剧增大,变为7,相应的score和score_adj也会增大。在上篇文章中我们得知,adj=7即为被切换的进程,两个进程来回切换,上一个进程就会被设为7。当我们按Back键的时候,adj就会被设为9,也就是缓存进程,优先级比较低,有很大的几率被杀掉。

B方案

B直接启动一个后台service并播放音乐,让我们来看下B的对应的打开、按下Home切换、按下Back退出相应的adj、score、score_adj的值。

Android开发——Android多进程以及使用场景介绍

Android开发——Android多进程以及使用场景介绍

Android开发——Android多进程以及使用场景介绍

三种状态的adj、score_adj的值和A都是一样的,只有score有一点出入,其实分析源码得知,LMK杀进程的时候,score的影响其实并不大,所以我们暂时忽略它。所以adj和score_adj的值都相同却内存不足的情况下,这两个应用谁占得内存更大,谁就会被杀掉。不过鉴于A实在activity中播放音乐,所以B还是比A略好的方案。

这里有朋友肯定要问了,为什么切到后台后,adj的值是7而不是5,后台不是还有service在跑吗?

我们通过查看源码可以找出来,当切换Home的时候,会调用ActivityStack.java的finishCurrentActivityLocked函数,然后调用到了ActivityManagerService.java的computeOomAdjLocked函数,在这里对进程的ADJ值进行重新计算。当进程为PreviousProcess情况,则ADJ=7。具体的计算流程请看computeOomAdjLocked计算流程

if (app == mPreviousProcess && app.activities.size() > 0) {
if (adj > ProcessList.PREVIOUS_APP_ADJ) {
adj = ProcessList.PREVIOUS_APP_ADJ;
schedGroup = Process.THREAD_GROUP_BG_NONINTERACTIVE;
app.cached = false;
app.adjType = "previous";
}
if (procState > ActivityManager.PROCESS_STATE_LAST_ACTIVITY) {
procState = ActivityManager.PROCESS_STATE_LAST_ACTIVITY;
}
}

C方案

C的话是启动一个前台Service来播放音乐。让我们来看一下对应的值。

Android开发——Android多进程以及使用场景介绍

Android开发——Android多进程以及使用场景介绍

Android开发——Android多进程以及使用场景介绍

在前台的时候,和AB是一样的,adj都是0,当切到后台,或者back结束时,C对应的adj都是2,也就是可感知进程。adj=2可以说是很高优先级了。一般adj<5的应用不会被杀掉。因此总的来说,C方案相对于B来说更稳定,用户体验更好。不过有一点不足是必须启动一个前台service。不过现在大部分的音乐类软件都会提供一个前台service,也就不是什么缺点了。


D方案

D把应用进行了拆分,把用于播放音乐的service放到了新的进程内,让我们看一下对应的值。

Android开发——Android多进程以及使用场景介绍

Android开发——Android多进程以及使用场景介绍

Android开发——Android多进程以及使用场景介绍

上面三张图对应的是D应用主进程的ADJ相关值,我们可以看出来,跟A类似,adj都是0,7,9。由于少了service部分,内存使用变少,最后计算出的oom_score_adj也更低了,意味着主进程部分也更不容易被杀死。下面我们看下拆分出的service的相关值。

Android开发——Android多进程以及使用场景介绍

因为是service进程,所以不受打开,关闭,切换所影响,我们可以看到service的adj值一直会是5,也就是活跃的服务进程,相比于B来说,优先级高了不少。不过对于C来说,其实这个方案反倒不如C的adj=2的前台进程更稳定。但是D可以自主释放主进程,使D实际所占用的内存很小,从而不容易被杀掉。C、D各有利弊。

E方案

E也是使用了多进程,并且在新进程中,使用了前台service,先来看下对应的值。

Android开发——Android多进程以及使用场景介绍

Android开发——Android多进程以及使用场景介绍

Android开发——Android多进程以及使用场景介绍

这个不多解释,和ABD基本差不多,都是0,7,9。我们看下拆分出来的进程的值。

Android开发——Android多进程以及使用场景介绍

我们可以看到,这个进程的值是2,像C方案,非常小,非常稳定,而且我们还可以在系统进入后台后,手动杀掉主进程,使整个应用的内存消耗降到最低。内存低,优先级又高,E获得了今天的“最稳定方案奖”。

7.  使用多进程的其他场景补充

多进程还有一种非常有用的场景,就是多模块应用。比如我做的应用大而全,里面肯定会有很多模块,假如有地图模块、大图浏览、自定义WebView等等(这些都是吃内存大户),一个成熟的应用一定是多模块化的。首先多进程开发能为应用解决了OOM问题,因为Android对内存的限制是针对于进程的,所以,当我们需要加载大图之类的操作,可以在新的进程中去执行,避免主进程OOM。而且假如图片浏览进程打开了一个过大的图片,java heap 申请内存失败,该进程崩溃并不影响我主进程的使用。

上一篇:基于UML的需求分析和系统设计


下一篇:C#与Java的比较