Android 中的进程和线程
当一个程序组件开始的时候,并且这个程序没有其他组件在运行时,Android系统会为这个程序开启一个新的Linux进程,这个程序拥有一个单线程来执行。默认的来说,相同的程序的所有组件都运行在相同的进程和线程中,这个线程被称为主线程(main thread).如果一个程序组件开启了,但是这个程序的进程已经存在了,(因为程序的某一个组件已经存在在运行了),这个时候,这个程序组件就在那个进程中被开启,并且使用相同的执行线程。然而,你可以安排你程序中的不同的组件运行在分开的进程中,也可以为任何一个进程创建额外的线程。
下面这个文章就是讨论在Android程序中进程和线程是如何工作的?
1.进程
默认的,同一个程序的所有组件都运行在同一个进程中,大多数程序都是这样的。但是,如果你觉得需要控制某一个组件属于哪一个进程的话,你可以在manifest文件中配置它。
对应与每种组件元素-<activity>,<service>,<receiver>,<provider>的manifest条目,支持一个 android:process属性,这个属性可以指定一个组件应该运行在哪一个进程中。通过配置这个属性,你可以指定每一个组件运行在它自己的进程中或者一些组件共享一个进程而另一些组件则不共享;你也可以指定不同程序的组件运行在相同的进程中,这种情况就需要这些程序共享同一个Linux User ID,并且被相同的证书所签名。
<application>元素也支持android:process属性,为所有的组件都设置一个默认的值。
Android系统在某一个时间点上,比如可用内存容量比较低但是对于用户来讲是更加紧急或者急迫的进程需要内存的时候,也许会决定关闭一个进程来回收一些系统资源。当运行程序组件的进程被kill掉以后,程序组件也因此被销毁了。当这些组件再一次有任务要做的时候,运行这些组件的进程会再一次被启动。
当决定哪一个进程将要被kill的时候,Android系统会权衡他们对于用户的相对的重要程度。例如,相对于一个托管着可见的activity的进程来讲,它更加可能kill一个托管(hosting)着在屏幕不可见的activity的进程。因此,是否终止一个进程的决定依赖于运行在这个进程中的组件的状态。下面将讨论用来决定哪一个进程被终止的规则。
2.进程的生命周期
Android系统尽力着去保持一个进程尽可能长的持续运行,但是最终需要移除老进程来回收内存,用于新的或者更加重要的进程。为了决定哪一个进程继续,哪一个进程被kill,Android系统将每一个进程放置于一个"重要性阶层"的逻辑结构中(重要性层级关系),这个关系是基于运行在这个进程的组件以及这些组件的状态来决定的。有最低重要性的那些进程是首先被淘汰的,然后接下来就是那些紧随其后的最低重要性的的,等等。这些都是回收系统资源必要的过程。
2.1 Foreground process
一个服务于用户当前工作的进程。如果下面任何一个条件成立的话,一个进程被认定为是前台进程。
- 它托管着一个用户正在与之交互的activity(这个activity的onResume()方法已经被调用)
- 它托管着一个Service,这个Service被绑定在一个用户正在交互的activity上面
- 它托管着一个运行在前台的Service(这个service已经调用了startForeground()方法)
- 它托管着一个Service,这个Service正在执行某一个Service的生命周期回调,(onCreate(),onStart(), 或者onDestroy())
- 它托管着一个正在执行它的onReceive()方法的BroadcastReceiver
通常来讲,在一个给定的时间点,仅仅存在很少的前台进程。这个前台进程被kill作为最后的手段,如果内存低到这些前台进程不能全部继续运行。通常,在这种情况下,设备已经达到了一种memory paging (不知道什么意思)状态,因此系统会选kill一些前台进程来保持系统是可以响应用户的交互请求的。
2.2 Visible process
一个没有包含任何前台组件,但是仍然用户在屏幕上观察的内容的进程。如果下面任何一个条件成立的话,一个进程被认定为是可见进程。
- 它托管着一个Activity,着Activity没有在前台,但是对于用户仍然是可见的(它的onPause()方法已经被调用),这个也许会发生,例如,如果这个前台Activity开启了一个对话框,这允许前面的Activity在对话框的后面能被看见
- 它托管着一个Service,这个Service被绑定到一个可见的(或者前台)activity
一个可见进程被认为是十分重要的,将不会被kill,除非为了保证所有的前台进程(Foreground Process)继续运行,不得已kill掉可见进程。
2.3 Service process
Service process是这么一个进程,它运行着一个Serice,这个Serice通过startService()方法被开始,并且没有进入上诉两种高级分类(Foreground process & Visible process)的状态.尽管服务进程没有和用户看见的内容直接相关,但是他们通常是在是在做一些用户在意的一些事情的(例如播放音乐在背后,或者通过网络下载数据),因此系统需要保证他们的运行除非没有足够多的内存来维持他们以及所有的foreground process 和 visible process
2.4 Background process
一个托管着一个activity,这个activity当前对于用户是不可见的(这个activity的onStop()方法已经被调用)的进程被称为后台进程。这些进对于用户的体验没有直接的影响,系统可以为了回收内存以用于一个前台、可见或者服务进程,在任何时候kill这些进程。通常有很多后台进程在运行,因此他们被保持在一个LRU列表以确保这个托管用户最近看见的activity的进程最后一个被kill.如果一个activity正确的实现了它的生命周期的方法,保存了它的状态,那么kill它的进程将不会对用户体验产生一个可见的影响。因为当用户导航回到这个activity的时候,这个activity恢复了所有的可见的状态。
2.5 Empty process
一个没有持有任何程序组件的进程。让这种空进程活着的唯一的原因就是缓存的目的 ———— 为了减少这个进程的启动时间当下一次某个组件在需要在这个进程中运行的时候。系统经常kill这些进程为了在进程缓存和底层内核缓存之间平衡整个系统资源。
Android排列一个进程在它能够成为的最高等级(最大化进程重要性等级),根据这个进程中当前活跃的组件的重要程度。例如,如果一个进程托管着一个Service 和一个 可见的activity,这个进程就被排列作为一个可见进程, 而不是 一个服务进程。
另外,一个进程的等级也许会因为其他一些线程依赖于它而增加,也就是说,一个正在服务于另一个进程的进程的的等级绝不会低于它正在服务的进程的等级。例如:如果进程A中的content provider正为进程B中的client提供服务,或者进程A中的一个Service被都绑定到进程B中的一个组件,那么进程A一直会被认为至少和进程B是同等重要的。
因为运行一个Service的进程的重要性等级要比一个运行一个后台activity的进程的重要性等级要高,因此一个准备开启一个长时间运行的操作的activity也许更加适合选择开启一个Service来执行这个操作,而不是简单的仅仅创建一个工作线程————特别是当这个操作很可能比activity存活的时间长,那么就更应该选择开启一个Service而不是一个单独的线程。例如:一个上传图片到指定站点的activity应该开始一个Service来执行上传操作,因为当用户即使离开了当前的上传图片的activity,上传也可以在后台继续执行。使用一个Serice可以保证这个操作至少有一个“服务进程”级别的优先级,而不用去理会activity发生了什么。同样的道理,一个broadcast receiver也应该使用service而不是简单的放耗时的操作在一个单独的线程里面。
3.线程
当一个程序加载的时候,系统为这个程序创建了一个执行线程,被称为"main."。这个线程是非常重要的,因为它负责分发事件给合适的用户交互控件,包括绘画事件。同时,它也是处理程序和Android UI toolkit(组件来自 android.widget and android.view 包)的组件交互的线程。正因为如此,这个主线程也被称为UI线程。
系统不会为每一个组件实例创建一个单独的线程。运行在同一个进程的所有组件都被实例化在UI线程中,对每一个组件的系统调用都从这个线程中分发出来。因此,响应系统回调的方法(例如onKeyDown()或者生命周期回方法例如onStart())一直运行在这个进程的UI线程中。
例如,当用户触摸屏幕上的一个按钮的时候,你的app的UI线程就会分发这个触摸事件给这个控件,这个控件轮流设置它自己的按下的状态,同时发布一个失效请求进入事件队列。UI线程将这个请求出队列,然后通知这个控件重绘。
当你的app执行密集的工作为了响应用户的交互的时候,这个单线程模型可能产生糟糕的表现除非你恰当的实现了你自己的程序。特别的是,如果你的程序的所有操作都放在UI线程中,执行长时间的操作例如网络操作或者数据库查询操作将会阻塞整个UI。当UI线程被阻塞的时候,没有事件被分发,包括绘制事件。从这个用户的角度来看,这个程序好像挂起了。更糟糕的是,如果UI线程被阻塞超过数秒后(一般是大约5秒钟),那么系统就会弹出一个ANR的对话框给用户。用户很可能就会选择退出你的程序,如果他们不爽,那么你的程序就会被卸载了。
另外,Android UI toolkit不是线程安全的,因此,你是不被允许在一个工作线程中操作你的UI的,你必须做所有的UI操作在你的UI线程。因此,有两个规则关于这个Android的单线程模型:
1. 不要阻塞UI线程
2. 不要在UI线程之外去访问Android UI toolkit
4. 工作线程
因为上面描述的单线程模型,所以你的app的UI响应性是非常重要的,因此记住不要阻塞你的UI线程。如果你有操作需要执行,但是这些操作不是瞬间完成的,那么你应该在另外的线程(后台或者工作线程)中执行这些操作。
例如,下面是点击在单独的线程中下载图片的示例。
<span style="font-size:18px;">public void onClick(View v) { new Thread(new Runnable() { public void run() { final Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png"); mImageView.post(new Runnable() { public void run() { mImageView.setImageBitmap(bitmap); } }); } }).start(); }</span>
5.线程安全的方法
在一些情况下,你实现的方法也许会被不止一个线程调用,因此这些方法必须是线程安全的。
这个对于那些也许会被远程调用的方法来说是更加值得注意的例如在绑定service中的一些方法。当调用者调用一个在IBinder中实现的方法的时候,这个调用者所在的进程和IBinder运行的进程是同一个进程的时候,这个方法在调用者的线程中执行。但是,当调用来自于另一个进程的时候,系统会从IBinder所在进程中的由系统维护的线程池中选择一个线程来执行这个方法。(它不会被执行在这个进程的UI线程中)。例如:鉴于一个服务的onBind()方法将被service所在进程的UI线程调用,在onBind()方法返回的对象(例如一个实现RPC方法的子类)中被实现的方法将在线程池的某一个线程中被调用。因为一个service不止一个client,所以不止一个线程池中线程能够在同时访问相同IBinder中的方法。所以,IBinder中的方法必须被实现成为线程安全的。
同样的,一个content provider能够收到来自其他进程的数据请求。尽管ContentProvider 和 ContentResolver类隐藏了管理IPC的细节,但是ContentProvider中响应那些数据请求的方法例如:query(),insert(),delete(),update(),和getType(),这些方法会被ContentProvider所在进程中的线程池中线程调用,不是这个进程的UI线程。因为这些方法也许会在同一个时刻被调用在多个线程,因此这些方法也必须被实现是线程安全的。
6.进程间通讯(Interprocess Communication,IPC)
Android使用远程程序调用(RPCs)建立了一个IPC机制,在这个机制里面,被一个activity或者其他程序组件调用的方法,将被在远端执行(在另一个进程中),但是返回结果是给调用者。这个机制需要拆分方法调用和它的数据到操作系统能够理解的水平,以及将它从本地进程和地址空间传输到远程进程和远程的地址空间,然后再在那里重新组装和重新制定这个调用。返回值然后被传输在相反的反向。Android提供IPC事务的所有代码,所以你只需要关注RPC编程接口的定义和实现即可。
为了执行IPC,你的程序必须绑定到一个service,实用bindService().
7. 总结
本文章主要讲了:
1. Android中的进程和线程的概念
2. Android中的进程的优先级以及Android系统kill进程的机制
3. Android中5种进程,Foreground process,Visible process,Service process,Background process,Empty process的定义
4. Android线程分为UI线程和非UI线程,以及怎么使用这些线程?
5. Android中多线程时的方法的线程安全的问题