App Components.Process and Threads
当需要启动某个应用的某个组件,并且该应用中没有其他组件在运行的时候,android系统会为这个应用启动一个新的linux进程,里面只含一个执行线程。默认情况下,同应用的所有组件都运行在同一进程及线程下(通常这个线程就叫主线程)。而当需要启动某个应用的组件,并且该应用已经在某个进程里运行了(因为应用里的其他组件之前已经启动了),这个时候系统就直接在那个进程里启动组件并且和之前的组件放到同一执行线程下。不过,你可以为你的应用中的组件安排让它们运行在不同的进程中,也可以给任意进程创建额外的线程。
本篇文档就是讨论android应用中,进程和线程的工作原理。
进程:
默认情况下,同应用的所有组件都运行在同一进程下,并且大多数的应用最好不要去修改这个设定。如果非改不可的话,你可以在Manifest中设定某个组件所属的进程。
在Manifest文件中的每个组件的条目,即<activity><service><receiver><provider>,都支持android:process属性,该属性用来指定组件所属的进程。你可以通过设定该属性来让每个组件运行在各自的进程里或者某几个组件运行在同一个进程而另外几个运行在别的进程,说白了就是自己随意安排。。另外,android:process属性可以实现跨应用的组件运行在同一进程中(有个前提:这几个应用要有同样的证书以及同样的UserID)。
而<application>标签同样支持android:process属性,在这里设定属性的话就会应用到应用的所有组件中。
在某些时候系统可能会要强制停止掉某个进程,比如在系统的可用内存所剩无几的情况下另外某个进程又急需内存来和用户交互,此时可能就会杀掉一部分进程来满足它需要。杀掉进程的同时,进程当中原先在运行的应用组件就都被销毁了。之后如果有用到这些组件的地方到时候再重新启动。
上面提到某些情况下系统会杀进程,那么到底杀哪些进程呢?android系统会权衡不同进程对于用户的重要性,重要性越低当然就越该杀~~。比如,两个进程,其中一个里面运行着一个用户可见的活动,而另一个进程中有一个用户看不见的活动,那显然系统会倾向于杀掉后者。所以,关于进程对于用户的重要性,其实是取决于进程中正在运行的组件的状态(比如上面提到的活动可见与不可见)。下面详细讨论一下这个“被杀优先级”。
进程生命周期:
android系统对于任何进程其实都是想要让它们运行的越久越好,但是资源终归有限,当一个新的更重要的进程需要资源的时候系统就会把老的进程杀掉以留出可用内存供新进程使用。为了更公正地决定谁死谁活的问题,系统把每个进程都放到一个“重要性等级结构”中的对应位置上。当需要恢复资源的时候,重要性最低的进程最先被杀掉,以此类推。
在这个等级结构中有5个级别。下面按照重要性的高低顺序(由高到低)列出这个5种进程:
1. 前台进程
用户正在进行的操作必须依赖的进程。当以下条件之一满足的时候,我们就称某进程为前台进程:
~ 进程中有个活动在运行,用户正在和这个活动交互(也就是活动的onResume()方法已经被调了)
~ 进程中有个服务在运行,这个服务绑定到了用户正在交互的活动上。
~ 进程中有个服务在运行,并且这个服务运行在前台(即服务是通过startForeground()启动起来的)。
~ 进程中有个服务在运行,并且这个服务正在执行它的生命周期回调方法之一(即onCreate()onStartCommand()onDestroy()之一)。
~ 进程中有个广播接收者,并且它正在执行onReceive()方法。
一般来说,在任一时刻只有很少的前台进程存在。只有在实在实在没有内存的时候才会考虑杀掉前台进程,而到了这个地步的时候设备已经达到了一个内存分页状态,为了让UI保持响应状态不得不杀掉一些前台进程。
2. 可见进程
可见进程并不包含在前台运行的组件,但是它仍然可以影响到用户在屏幕上看到的东西。当下面条件之一满足的时候,我们将该进程称为可视进程:
~ 进程中有个活动在运行,活动不在前台但是用户依然可以看到它(即该活动只调用了onPause())。这个情况往往出现在前台活动以一个对话框的形式出现,这时后面的活动就处于可见但不可交互的状态。
~ 进程中有个服务,绑定到了某个可见不可交互的活动(或者前台活动?文档是这么写的)上。
可见进程的重要性很高,仅次于前台进程,所以除非不杀它就不能保证前台进程的运行,其他情况下可见进程一般不会被杀掉。
3. 服务进程
进程中运行着一个服务,服务是通过startService()启动起来的,并且进程既不满足前台进程也不满足可见进程的,就属于服务进程。虽然服务进程并没有直接绑定到任何用户可见的东西,但是它的服务进程的很多操作都是影响到用户的,比如后台放音乐或者后台下载什么的。所以系统仅在内存不足以维持所有的前台和可见进程的时候会考虑杀掉服务进程。
4. 后台进程
后台进程中运行着一个用户不可见的活动(即活动调了onStop()方法)这种进程对用户的体验没什么影响,系统随时会为了给上面三种进程提供资源而杀掉它们。通常来说系统中会有很多后台进程,它们被放在一个LRU列表里,所以用户最近刚看到过的活动所在进程会最后被杀掉。如果活动的生命周期回调实现的很健壮,保存了很多它的状态。那么杀掉活动所在的进程并不会有太大的影响,因为当用户回到这个活动的时候会把原来可视的状态都还原回来(类似IOS的应用重载,很多人都感觉不到..)。
5. 空进程
进程中没有任何应用组件,就是空进程。保留空进程的唯一目的就是为了缓存,即方便用户下一次要在这个进程里启动组件。系统经常会杀掉空进程来平衡进程缓存和底层的内核缓存所占的资源。
进程中如果有多个组件的话,系统会根据最高级的那个状态来给进程设定重要性。比如进程中有一个可见的活动,以及一个启动的服务,那么系统会认为这是一个可见进程,而不是服务进程(感觉是废话)。
另外,进程的重要性等级可能会因为其他进程依赖该进程而提高,也就是说,给别人提供服务的进程的优先级是不可能低于那个被服务的进程的。比如,A进程中有一个内容提供者,B进程中有一个客户端要来访问A进程的内容提供者,此时A进程的优先级即重要性,必须大于等于B进程。又如A进程中有一个服务,而B进程中有个客户端绑定到了A中的服务,此时A的重要性还是大于等于B。
由于内含服务的进程相比内含后台活动的进程,重要性要更高,所以如果活动中需要启动一个长时间执行的任务,最好考虑启动一个服务来执行这个任务,别直接启个新线程就完事。特别是,当你这个长时间任务的“时间”比活动还要长的时候,就更要考虑用服务来做了。比如,某活动需要向一个网站上传图片,那就最好启动一个服务来进行上传的操作,这样在上传的同时用户可以离开这个活动去干点别的事情。而且使用服务的话,不管活动的状态如何,至少能够让你的进程拥有“服务进程”的级别。同样,对于广播接收者来说,耗时的操作也一样要用启动一个服务来执行,而不要用线程。
线程:
当一个应用被启动的时候,系统会为它创建一个执行线程,叫做“主线程”。这个线程至关重要,因为它负责将事件分发给相应的UI控件,包括绘画事件。同时这个线程还是你的应用和androidUI工具包里的控件(就是android.widget和android.view包下面的控件)进行通信的线程。所以有时候主线程也叫UI线程。
系统不会为每个控件的实例创建新线程,所有运行于同一进程内的控件都在UI线程实例化,然后系统对控件的调用会从那个UI线程分发出来。进而,响应系统回调的方法们(像onKeyDown()这种发送用户动作的方法或者其他的一
些生命周期回调方法)都是在进程中的UI线程中运行的。
比如,用户点击了屏幕上的一个按钮,然后你的应用的UI线程就会把这个点击事件分发给对应的控件,然后控件把自己的状态设为被点击时的对应状态,同时把一个invalidate请求发到请求队列上。然后UI线程从队列中取下该请
求,然后通知控件让它重新画一下自己。
当你的应用在和用户互动时要执行重量级的任务,那么这个单线程的模型的表现就会比较糟糕,拖累用户体验,除非你在应用中做出了恰当的处理。具体来说,如果所有的操作都在UI线程中执行,像联网的操作及查询数据库的操作,这些操作会阻塞整个UI。当UI线程被阻塞的时候,什么事件都分发不出去了,包括绘画事件。从用户角度来看,这个应用就是“卡住了”。更糟糕的是,如果阻塞时间超过一定的值(现在大约是5秒)的话系统给用户弹出“应用无响应”(ANR,application not responding)的对话框。此时用户可能就直接选择终止程序然后就顺手不爽地把它卸掉(T_T)。
另外,android的UI工具包是没有考虑线程安全问题的。所以不要在非UI线程中更新UI,也就是只能在UI线程中刷新UI。对于android的这个单线程模型,有两个简单原则需要注意:
1. 不要阻塞UI线程
2. 不要在非UI线程刷新UI。
工作线程:
根据我们上面描述的单线程模型,为了使你应用的UI保持响应,不要阻塞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);
}
}).start();
}
看上去很合理啊貌似,启动新线程来执行网络相关的操作。然而。。注意我们是不允许在非UI线程更新UI的,这里违反了这个原则:它在子线程中试图修改ImageView的内容。这种行为会导致一些不可预知的后果,并且一旦出了问题,很难追根溯源。
为了解决此问题,android提供了几个方法来在非UI线程中更新UI。如下:
~Activity.runOnUiThread(Runnable)
~View.post(Runnable)
~View.postDelayed(Runnable, long)
比如,对于上面的下载图片然后显示的功能,可以这样实现:
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();
}
这种实现方式是线程安全的:网络操作在一个独立线程里进行,而对ImageView的修改在主线程执行。
然而,随着操作复杂度的增长,以这种方法写出的代码会变得越来越复杂,且难以维护。那么如何处理这种主线程和工作线程间更加复杂的交互呢,此时要考虑在工作线程中用到Handler来处理UI线程发来的消息。一般来说最好的解决方法是继承AsyncTask类,这个类简化了工作线程与UI线程间的交互。
使用AsyncTask:
AsyncTask能够让你异步地操作你的UI。它把阻塞式的操作放在工作线程然后把操作的结果发到UI线程中,并且这个线程间的发送过程不需要你管。
要使用它,首先要继承AsyncTask然后实现doInBackground()回调方法,这个方法会在后台的线程池中运行。如果要更新UI呢,就需要实现onPostExecute(),该方法把doInBackground()方法的执行结果发送到UI线程中,然后你就可以安全地根据这个结果来更新你的UI。继承/实现完了以后,最后在主线程中调用execute()方法就ok了。
比如,对于之前的例子,我们可以改用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()系统调用这个方法,并且把用户给到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() 这个方法把doinbackground返回的那个东西更新到UI上。*/
protected void onPostExecute(Bitmap result) {
mImageView.setImageBitmap(result);
}
}
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);
}
}
我们看到这个方法是线程安全的,并且代码也更简单,因为它把任务分解成了两个部分,一部分在工作线程中完成,一部分在主线程中完成。
想要完全了解AsyncTask类的使用方法,就去看reference吧,不过我们这里可以大概地总结一下:
~你可以用泛型来指定参数的类型,以及进度的类型,以及结果的类型。比如上面的例子,参数,就是用户输入的那个字符串,所以参数类型就是String,进度,我们没有用到,所以这里进度类型是void,结果,可以看到就是onPostExecute接收的那个参数同时也是doInBackgound的返回值,是Bitmap类型的。
~doInBackgound()方法会自动在一个工作线程里执行
~onPreExecute(), onPostExecute(), and onProgressUpdate()这三个方法都是在主线程执行的。
~doInBackgound()方法的返回值会作为参数被转送到onPostExecute()方法
~在doInBackgound()方法中,你随时可以调用publishProgress()方法,该方法会触发onProgressUpdate()方法在主线程执行,从而可以更新UI。(这一连串操作也就实现了一边在后台执行任务,一边更新UI,即实现了“更新进度”)。
~你可以在任何时间任何线程中停止任务。
注意:在使用工作线程的过程中你可能会遇到一个小问题,就是由一个runtime configuration change引发的活动重启行为,比如用户横竖屏切换了一下(这是会导致活动被销毁再重启的~),这种重启的行为可能会销毁掉你的工作线程。想知道如何在这种情况下让你的任务“顽强地”存活下来,或者如何在活动销毁的时候采用合适的方法停止你的任务吗?去看我们的sample当中的Shelves应用的代码吧。
线程安全方法:
某些情况下,你实现的某方法可能会在其他线程被调到,所以你实现方法的时候就要考虑到线程安全问题。
对于那些可被远程调用的方法尤为如此,比如bound service中的方法。如果IBinder中实现的某方法是在和IBinder同一个进程中被调用的,那这个方法就会在调用者所属的线程中执行。然而,如果是另外一个进程中发起的调用,那系统会从一个线程池里面选出一个线程来执行这个方法,且这个线程池是和IBinder同属一个进程的(当然系统不会选出UI线程来执行这个方法的)。比如,
某个服务的onBind()方法是在同进程的UI线程中被调用的,而作为onBind()返回值的那个对象(比如,一个提供RPC方法的IBinder的子类),它实现的一些方法,就是在线程池里的某线程中执行的。由于服务可同时拥有多个客户端,那么线程池中可能有多个线程同时调用IBinder的某犯法。那这种情况下,IBinder实现的方法就必须保证线程安全。
类似的,内容提供者也可能收到来自其他进程的请求。尽管ContentResolver和ContentProvider类为我们隐藏了这些进程间通信的细节,但我们仍然要知道,query(),insert(),delete(), update(), and getType()这些用来响应请求的方法,都是在内容提供者同进程下的线程池中的某线程中被调到的,而不是UI线程。由于这些方法也可能被多个线程同时调到,这些方法必须保证线程安全。
进程间通信(IPC):
android通过远程程序调用(Remote Procedure Calls,RPC)来提供进程间的通信,RPC就是指,某应用调用了别的应用的一个方法,然后这个方法是在那个“别的应用”也就是另外一个进程里远程执行的,执行完之后会把结果给“某应用”返回来。这就需要我们要把对方法的调用以及它的数据解析成系统能够理解的东西,让后让系统把它从当地进程传到远端进程(其实就是跨进程传输),传输过程还包含着地址空间的转换,传到另外一个进程之后再把这个调用重新拼装起来。而返回值的传输刚好和调用是反向的。android提供了IPC事务所需的代码,所以你可以只关注于定义并实现RPC接口。
要实现IPC,你的应用必须绑定到某服务,使用bindService()。关于这个更多的信息请看服务的文档。
posted on
浙公网安备 33010602011771号