Android 进程和线程详解转
我们都知道,在操作系统中进程是OS分配资源的最小单位,而线程是执行任务的最小单位。一个进程可以拥有多个线程执行任务,这些线程可以共享该进程分配到的资源。当我们的app启动运行后,在该app没有其他组件正在运行的前提下,Android系统会启动一个新Linux进程来运行app,这个进程只包含了一个线程在运行。在默认情况下,app的组件都运行在该进程中,最初就包含的这个线程也被称为主线程或者UI线程。如果我们启动该app的时候,系统中已经有一个进程在运行该app的组件,那么该app也会在该进程中运行。当然,我们也可以让app中不同的组件运行在不同的进程中,也可以在任意进程中新开线程执行任务。
进程
前面提到过,在默认情况下同一app的所有组件都是运行在同一进程中的,而且大多数app并不需要去更改这个设定。但如果我们真的需要指定进程来运行特定组件,那么可以在manifest文件中设置。我们在manifest文件中定义了各个组件,例如activity,service,receiver,provider等等,我们可以设置process属性来指定一个新线程来运行该组件。通过设置process属性,我们可以设置每个组件都运行在不同的进程中,也可以指定几个组件运行在同一个进程中。我们甚至可以设置不同来自app的组件运行在同一个进程中(通过指定相同的process属性并共享相同的user ID)。
之前的文章中提到,Android系统启动后会载入通用的framework的代码与资源之后,启动一个Zygote进程。为了启动一个新的程序进程,系统会fork Zygote进程生成一个新的进程,然后在新的进程中加载并运行应用程序的代码。这就使得大多数的RAM pages被用来分配给framework的代码,同时促使RAM资源能够在应用的所有进程之间进行共享。
Android系统在内存不足的情况下会杀死一些进程来满足那些直接和用户交互的进程,在这些被杀死的进程中运行的组件也会被注销掉。当这些组件重新运行时, 才会启动该线程。那么,系统将会如何决定要杀死哪个进程呢?Android系统需要根据进程与用户的相关重要性来判断的。例如,与那些正在显示activity的进程相比,系统更倾向于杀死那些不再显示的activity所在的进程。
进程生命周期
Android系统会尽可能长时间的维持一个进程的运行,但是最终会回收旧进程的内存空间提供给新的进程或是更重要的进程使用。Android系统采用了基于组件运行的进程以及组件状态的“重要性层级”的策略,根据重要性逐层清除进程。
重要性层级从高到低共分为5层:
1、前台进程foreground process
当前正在与用户交互的进程。如果一个进程P满足如下任意一个条件,则进程P被称为前台进程:
- 当前正在与用户交互(调用过resume方法)的activity在进程P中运行
- 某个service与当前正在与用户交互的activity相互绑定,该service运行于进程P中
- 某个service调用了startForeground()方法,该service运行在进程P中
- 某个service正在执行某个生命周期的回调方法,该service运行在进程P中
- 某个broadcastReceiver正在执行它的onReceive函数,该broadcastReceiver运行在进程P中
通常情况下,某时刻系统只会有很少一部分前台进程存在。它们只会在内存非常低的情况下才会被杀死,在这种情况下,设备达到了“memory paging state”状态,只有杀死一些前台进程才能保证系统的快速响应。
2、可见进程Visable process
没有任何前台组件在此进程中运行,但是仍然可以影响到用户所看到的屏幕。如果进程P满足以下任意一个条件,则进程P被称为可见进程:
- 某个activity并不运行于前台,但仍能被用户所见(调用了pause方法),activity运行于进程P中。例如某activity启动了一个dialog,仍然可以看到该activity。
- 如果某个service与visible activity或foreground activity绑定,该service运行于进程P中。
可见进程相对而言比较重要,但在某些情况下为了保证前台进程的运行,系统还是会杀死这些可见进程的。
3、服务进程service process
如果一个service通过startservice方法启动,并且不属于以上两种更高级别的情况,那么运行该service的进程被称为service process。尽管该service并不与任何能被用户看到的组件绑定,但是它们做的工作是用户关心的,例如音乐播放,文件下载等等。
4、后台进程background process
某个当前不可见的activity(回调了onStop方法)运行于该线程。此类线程无法直接影响到用户体验,系统可能随时杀死此类进程回收内存供以上三种进程使用。通常情况下,系统中有多个后台进程在运行,所以它们被存于一个LRU列表中,从而最不常被用户用到的进程会先被杀死。如果一个activity正常回调了它的生命周期的函数并存储了相应的状态数据,那么杀死该后台进程是不会影响到用户体验的。因为当用户试图返回到该activity时,系统会恢复该activity所有的状态。
5、空进程 empty process
该进程中没有运行任何组件,保持此类进程存在的唯一原因就是等待任务。一旦有组件需要运行,则可以缩短进程启动时间。所以系统往往会杀死这些进程用来平衡进程缓存和底层内核缓存之间的系统资源。
Android系统中,一个进程的等级是可以动态提升的,因为其他的进程可能会依赖于该进程。某个进程为其他进程提供服务,那么该进程的等级一定不会低于它所服务的进程。例如,某content provider 运行于进程A中,它为处于进程B中的客户端B提供服务;或者如果处于进程A的service绑定于位于进程B中的组件,那么A进程的重要等级只会高于或等于进程B。
由于一个运行service的进程等级要高于那些运行处于后台activity的进程,如果我们需要有长时间执行的操作,那么从一个activity中启动一个service来完成这些操作就比在activity中新开子线程来完成这些操作效果要好(特别是这些子线程的持续时间要比activity长的情况下)。例如,一个activity想要上传一张图片给服务器,那么应当开启一个service在后台来完成上传操作,即使用户离开了当前activity,这些操作也能够在后台完成。使用service可以保证这些操作至少具有service优先级,无论当前activity的状态是否改变。这也是为什么broadcast receiver应当使用service而不是简单的把耗时操作放在子线程中的原因。
线程
当应用程序启动后,系统将会创建一个主线程来运行应用程序。主线程非常重要,它负责为适当的用户控件分发任务和事件,包括绘制任务等等。同时,主线程也负责UI组件和应用程序的交互,所以我们也称主线程为UI线程。
系统并不会为每个组件单独开启一个线程来运行,所有的组件都会在主线程中初始化并运行运行在同一个进程中,系统通过主线程来调用每个组件。所以,系统回调方法(例如onKeyDown,生命周期回调方法等)通常运行于主线程。
例如,当用户点击屏幕上的按钮,UI线程会将点击事件分发给控件。控件就会设置自身的按下状态,并将重绘请求添加到事件请求队列。UI线程从事件队列中取出该重绘请求后,通知该控件重绘。
当用户和app交互频繁时,单线程的模式可能会导致响应速度慢,用户体验不尽人意。如果在主线程中进行网络或数据库请求等耗时操作,则会导致线程阻塞,主线程将无法调度分发事件和任务。当超过5s的阻塞会使系统弹出ANR窗口。另外,UI控件都不是线程安全的,所以系统规定只能在UI线程中修改控件。我们需要遵循两个规则:
- 不要使UI线程阻塞
- 不要在UI线程之外修改控件
worker线程
上面讨论了只有UI线程工作的情况。为了提高应用程序UI的响应速度,获得更好的用户体验,我们需要把耗时操作放在子线程中来完成。但是我们需要注意的是,不要在子线程中操作UI控件。我们通常使用Android的Handler机制来解决线程间通信的问题,详细请参看之前的文章Android线程间异步通信机制源码分析。同时,Android也提供了async task来完成异步任务。
异步任务ASYNC TASK
async task在子线程中执行耗时任务,然后将结果返回给UI线程,无需自己手动创建handler。关于async task的使用就不在这里介绍了,在使用async task的过程中,我们需要注意的是多线程问题。由于运行配置的问题(例如屏幕横竖方向改变),会导致子线程任务未经过我们允许就重新启动执行。
线程安全方法
多数情况下,我们的方法有可能被多个线程所调用,所以我们必须考虑到线程安全的问题。特别是对于那些可以被远程调用的方法更是如此,例如,绑定service的方法。当我们试图调用在IBinder中实现的方法,如果调用者和IBinder处于同一个进程,那么方法将会在调用者所在线程中执行。如果调用者与IBinder并不处于同一个进程中,那么系统从所维护的线程池中取出一个线程来执行该方法,该线程池与IBinder运行在同一个进程中(并非在UI线程中执行)。举个栗子,尽管service的进程的UI线程将会调用service的onBind方法,然而在onBind方法所返回的IBinder对象中实现的那些方法就会被线程池中线程执行。因为一个service可以由多个客户端访问,线程池中的多个线程可以在同一时刻调用同一方法。所以IBinder对象中实现的方法需要是线程安全的。
类似的,一个ContentProvider可以接收到来自不同进程的数据请求,虽然CP和CR类中隐藏了进程间通信管理的细节,但是CP中对应的查询,删除,修改,插入等请求方法将会被交给CP所在进程的线程池中线程来执行。这些方法可能在同一时刻被多个进程所调用,所以这些方法必须是线程安全的。
进程间通信
Android系统提供了远程调用RPC机制来完成进程通信IPC,通过RPC机制,应用程序中的组件(例如activity)作为调用者在本地调用某个方法,该方法在远程(另外一个进程中)执行,然后将结果返回给调用者。这就需要将所调用方法和它的数据解析为操作系统可以理解的程度,然后从本地进程和地址空间传递给远程的进程和地址空间后,再进行重组和执行。