Android 进程和线程详解
当启动一个应用程序组件时,如果该应用没有正在运行的其它程序组件,那么Android系统将为这个应用创建一个新进程(包含一个线程)用于运行应用。缺省情况下,一个应用的所有组件(Activity,Service等)运行在同一个进程和线程中(称为“主”线程)。如果在启动一个应用程序组件时,这个应用已经有进程在运行(因为有应用的其它组件存在),那么这个应用程序组件将使用同一进程和线程运行。当然你可以使用不同进程来运行不同的组件,或者在进程中创建新的线程。
进程
缺省情况,应用的所有组件都运行在同一个进程,而且应用不应该改变这个传统。然而,如果你发现你需要控制某个组件运行在那个进程中,你可以通过应用程序清单来配置。
在应用程序清单文件中,每个类型的应用程序组件-<activity>,<service>,<receiver>和<provider>都支持 android:process 属性,这个属性用来指明该程序组件运行的进程。你可以为应用程序组件设置这个属性以使每个组件运行在不同的进程中或者某几个组件使用同一进程。你也可以通过设置android:process 使得不同应用中的组件运行在同一个进程中-前提是这些应用使用同一个Linux用户名并且使用同一个证书签名。
<Application>元素也支持 android: process 属性,用来为应用程序的所有组件设置缺省的进程。
Android系统中系统资源过低而且有需要为用户立即提供服务的进程需要启动时可能会终止某些进程的运行。运行在这些被终止的进程中的程序组件将逐个被销毁。此后如果还有工作需要这些应用程序组件时将启动新的进程。
系统中决定哪些进程可以杀死时,系统将权衡这些进程对用户的重要性。比如,对于那些运行不可见的Activity的进程比运行屏幕上可见的Activity的进程更容易被杀死。
进程生命周期
Android系统会尽可能长的保持应用程序进程的运行,但总会有需要清除旧的进程来释放资源以满足新或是重要的进程的运行。为了决定哪些进程可以杀死,哪些进程需要保留,系统根据运行在其中的应用程序组件和这些组件的状态,将这些进程分配到“重要性层次表”中。具有最低重要性的进程首先被杀死,次重要性的进程为其次等等直到系统恢复所需的资源。
“重要性层次表”可以分为五个层次,下面列表给出了不同类型的进程的重要性等级(最重要的排在前面):
1.前台进程
这种进程是当前用户所需要的。一个进程被认为是前台进程需满足下面条件之一:
- · 本进程中有Activity是当前和用户有交互的Activity(该Activity的onResume()已调用)。
- · 本进程中有Service和当前用户有交互Activity的绑定。
- · 本进程中有在前台运行的Service—该Service调用过startForeground()。
- · 本进程中有Service正在执行某个生命周期回调函数(onCreate(),onStart()或onDestroy())。
- · 本进程中的某个BroadcastReceiver正在执行onReceive()方法。
2.可见进程
这种进程虽然不含有任何在前台运行的组件,但会影响当前显示给用户屏幕上的内容,一个进程中满足下面两个条件之一时被认为是个可见进程:
- · 本进程含有一个虽然不在前台但却部分可见的Activity(该Activity的onPause()被调用)。可能发生的情形是前台Activity显示一对话框,此时之前的Activity变为部分可见。
- · 本进程含有绑定到可见Activity的Service。
3. 服务进程
该进程运行了某个使用startService()启动的Service,但不属于以上两种情况。尽管此服务进程不直接和用户可以看到的任何部分有关联,但它会运行一些用户关心的事情(比如在后台播放音乐或者通过网络下载文件)。因此Android系统会尽量让它们运行直到系统资源低到无法满足前台和可见进程的运行。
4.后台进程
该进程运行一些目前用户不可见的Activity(该Activity的onStop()已被调用),该进程对用户体验无直接的影响,系统中资源低时为保证前台,可见或服务进程运行时可以随时杀死该进程。通常系统中有很多进程在后台运行,这些进程保存在LRU(最近使用过)列表中以保证用户最后看到的进程最后被杀死。如果一个Activity正确实现了它的生命周期函数,并保存了它的状态。杀死运行该Activity的进程对用户来说在视觉上不会有什么影响,这是因为之后用户回到该Activity时,该Activity能够正确恢复之前屏幕上的状态。
5.空进程
该进程不运行任何活动的应用程序组件。保持这种进程运行的唯一原因是由于缓存,以缩短下次运行某个程序组件时的启动时间。系统会为了进程缓存和内核缓存之间的平衡经常会清除空进程。
Android系统会根据进程中当前活动的程序组件的重要性,近可能高的给该进程评级。比如,如果一个进程中同时有一个Service和一个可见的Activity在运行,该进程将被定级为可见进程而不是服务进程(可见进程的优先级高于服务进程)。
此外,一个进程的级别可能有对其有依赖的其它进程提升—一个给其它进程提供服务的进程的级别不会低于它所服务的进程的级别。比如,进程A中的Content Provider 给进程B中某客户端提供数据服务或者进程A中某个服务被进程B某组件所绑定。那么进程A重要性程度不会低于进程B。
由于运行Service的进程的级别高于运行后台Activity的进程的级别,一个需要较长时间运行操作的Activity 启动能够完成该操作的Service可能也能很好的完成任务而无需简单创建一个新工作线程—尤其是该操作运行时间比该Activity还要长。比如,如果一个Activity需要完成向服务器上传图片任务时应该使用一个服务来完成上载任务,这些即使用户离开该Activity,Service依然可以在后台完成上载任务。使用Service可以保证某个操作至少具有“服务进程”的优先级而无需关心该Activity发生了什么变化。这也是一个Broadcast Receiver应该使用一个Service而非一线程来完成某个耗时的任务。
线程
Android系统启动某个应用后,将会创建一个线程来运行该应用,这个线程成为“主”线程。主线程非常重要,这是因为它要负责消息的分发,给界面上相应的UI组件分发事件,包括绘图事件。这也是应用可以和UI组件(为android.widget和android.view中定义的组件)发生直接交互的线程。因此主线程也通常称为用户界面线程(UI线程)。
Android系统不会主动为应用程序的组件创建额外的线程。运用在同一进程中所有程序组件都在UI线程中初始化,并使用UI线程来分发对这些程序组件的系统调用。由此可见,响应系统回调函数(比如onKeyDown() 响应用户按键或者某个生命周期回调函数)的方法总是使用UI线程来运行。
比如,当用户触摸屏幕上某个按钮时,你的应用中的UI线程将把这个触摸事件发送到对应的UI小组件,然后该UI小组件设置其按下的状态并给事件队列发送一个刷新的请求,之后UI线程处理事件队列并通知该UI小组件重新绘制自身。
当你的应用中响应用户事件时需要完成一些费事的工作时,这种单线程工作模式可能会导致非常差的用户响应性能。尤其是如果所有的工作都在UI线程中完成,比如访问网络,数据库查询等费时的工作将会阻塞UI线程。当UI线程被阻塞时,就无法分发事件,包括绘图事件。此时从用户的角度来看,该应用看起来不再有响应。更为糟糕的是,如果UI线程阻塞超过几秒钟(目前为五秒),系统将给用户显示著名的“应用程序无响应”(ANR)对话框。用户可能会选择退出你的应用,更为甚者如果他们感觉很不满意也会选择卸载你的应用。
此外,Android的UI组件包不是“线程安全”的,因此你不能走工作线程中调用UI组件的方法,所有有关UI的操作必须在UI线程中完成,因此下面为使用UI单线程工作线程的两个规则:
1. 永远不要阻塞UI线程。
2. 不要在非UI线程中操作UI组件。
工作线程
由于Android使用单线程工作模式,因此不阻塞UI线程对于应用程序的响应性能至关重要。如果在你的应用中包含一些不是一瞬间就能完成的操作的话,你应用使用额外的线程(工作线程或是后台线程)来执行这些操作。
比如下面示例,在用户点击某个按钮后,就启动一个新线程来下载某个图像然后在ImageView中显示:
public void onClick(View v) { new Thread(new Runnable() { public void run() { Bitmap b = loadImageFromNetwork("http://example.com/image.png"); mImageView.setImageBitmap(b); } private Bitmap loadImageFromNetwork(String string) { // TODO Auto-generated method stub return null; } }).start(); }
乍一看,这段代码应该很好的完成工作,因为它创建了一个新线程来完成网络操作。然而它违法了上面说的第二个规则:不要在非UI线程中操作UI组件。在这段代码中的工作线程中而不是在UI线程中,直接修改ImageView,这将导致一些不可以预见的后果,常常导致发现此类错误捕捉异常困难和费时。
为了更正此类错误,Android提供了多种方法使得在非UI线程中访问UI组件,下面给出了其中的几种方法:
- · Activity.runOnUiThread (Runnable) 方法
- · View.post (Runnable) 方法
- · View.postDelayed (Runnable) 方法
比如,使用View.post(Runnable)修改上面的代码:
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(); }
这样的实现是符合“线程安全”原则的:在额外的线程中完成网络操作并且在UI线程中完成对ImageView的操作。
然而,随着操作复杂性的增加,上述代码可能会变得非常复杂导致维护困难。为了解决工作线程中处理此类复杂操作,你可能会考虑在工作线程中使用Handler类来处理由UI线程发送过来的消息。但可能使用AsyncTask是此类问题的最好解决方案,它很好的简化了工作线程需要和UI组件发生交互的问题。
使用AsyncTask
AsyncTask允许你完成一些和用户界面相关的异步工作。它在一个工作线程中完成一些阻塞工作任务然后在任务完成后通知UI线程,这些都不需要你自己来管理工作线程。
你必须从AsyncTask派生一个子类并实现doInBackground()回调函数来使用AsyncTask,AsyncTask将使用后台进程池来执行异步任务。为了能够更新用户界面,你必须实现onPostExecute()方法,该方法将传递doInBackground()的返回结果,并且运行在UI线程中。然后你可以在UI线程中调用execute() 方法来执行该任务。
比如,使用AsyncTask来完成之前的例子:
public void onClick(View v) { new DownloadImageTask().execute("http://example.com/image.png"); } private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> { /** The system calls this to perform work in a worker thread and * delivers it the parameters given to AsyncTask.execute() */ protected Bitmap doInBackground(String... urls) { return loadImageFromNetwork(urls[0]); } /** The system calls this to perform work in the UI thread and delivers * the result from doInBackground() */ protected void onPostExecute(Bitmap result) { mImageView.setImageBitmap(result); } }
现在UI是安全的而且代码变的更简单,因为它把在工作线程中的工作和在UI线程的工作很好的分隔开。
你应该参考AsyncTask的详细文档以便更好的理解它的工作原理,这里给出它的基本步骤:
- · 你可以使用generics为Task指定参数类型,返回值类型等
- · 方法doInBackground()将自动在一个工作线程中执行。
- · 方法onPreExecute(),onPostExecute()和onProgressUpdate都在UI线程中调用。
- · 方法doInBackground()的返回值将传递给onPostExecute()方法。
- · 你可以在doInBackground()中任意调用publishProgress()方法,该方法将会调用UI线程中的onProgressUpdate()方法,你可以用它来报告任务完成的进度。
- · 你可以在任意线程中任意时刻终止任务的执行。
要注意的是,由于系统配置的变化(比如屏幕的方向转动)你的工作线程可能会碰到意外的重新启动,这种情况下,你的工作线程可能被销毁,你可以参考Android开发包中Shelves示例来处理线程重新的问题。
编写“线程安全”方法
在某些情况下,你编写的方法可能会被多个线程调用,此时你实现方法时要保证它是“线程安全”的。
“线程安全”是可以被远程调用方法实现的基本规则—比如支持“绑定”的Service中的方法。当在实现IBinder接口同一进程中调用IBinder对象的方法时,该方法运行在调用者运行的同一线程中。然而,如果调用来自不同进程,系统将使用和实现IBinder接口的进程关联的线程池中的某个线程(非该进程中的UI线程)来执行IBinder的方法。比如,一个Service的onBind()方法会在某个Service进程的UI线程中调用,而由onBind()返回的对象(比如实现远程调用RPC方法的子类)的方法会在线程池的某个线程中执行。由于Service可能服务于多个客户端,那么可能有线程池中的多个线程同时执行IBinder对象的某个方法,因此IBinder对象的方法必须保证是线程安全的。
同样的,一个Content Provider可以接受来自其它多个进程的数据请求。尽管ContentResolver和ContentProvider类隐藏了处理这些数据请求时进程间通信的详细机制,这些请求方法有query(), insert (), delete (), update () 及getType() 等。这些方法会在Content Provider的进程的线程池的某个线程中执行。由于这些方法同时有不定数量的线程同时调用,因此这些方法也必须是线程安全的。
进程间通信
Android系统支持使用远程调用(RPC)来实现进程间通信(IPC)的机制。此时在一个Activity或其它程序组件调用某个方法,而该方法的实现执行是在另外的进程中(远程进程)。远程调用可能给调用者返回结果。这就要求将方法调用和相关数据分离到某个层次,以便能让操作系统理解,能从本地进程传送数据到远程进程地址空间,在远程能够重新构造数据以执行方法,返回数据也能够反向返回。Android支持能够完成这些进程间通信事务的所有代码,从而使你可以只关注于定义和实现远程调用的接口。
为了使用进程间通信(IPC),你的应用需要使用bindService()绑定到某个Service。