Android开发-API指南-进程与线程

Processes and Threads

英文原文:http://developer.android.com/guide/components/processes-and-threads.html
采集(更新)日期:2014-12-25
搬迁自原博客:http://blog.sina.com.cn/s/blog_48d491300100zjpb.html

如果某个应用程序组件是第一次被启动,且这时应用程序也没有其他组件在运行,则 Android 系统会为应用程序创建一个包含单个运行线程的 Linux 进程。 默认情况下,同一个应用程序的所有组件都运行在一个进程和线程里(叫做主线程“main”)。 如果组件启动时,应用程序已经存在进程了(因为应用程序的其他组件已经在运行了),则此组件会在已有的进程中启动,并使用同一个运行线程。 不过,可以让一个应用程序中的不同组件运行在各自独立的进程中,也可以为任何进程创建更多的线程。

本文讨论进程和线程是如何在 Android 应用程序中发挥作用的。

进程


默认情况下,同一个应用程序中的所有组件都运行于一个进程中,大部分应用程序也不会去改变这种方式。 不过,如果需要指定某个特定组件所属的进程,则可以利用 Manifest 文件来达到目的。

Manifest 文件中的每种组件元素的节点—— <activity><service><receiver><provider> ——都支持android:process属性的定义,用于指定运行组件的进程。 设置此属性即可实现不同组件在各自的进程中运行,或者某几个组件共享一个进程而其他组件运行于自己的进程。 设置此属性还可以让不同应用程序的组件运行于同一个进程中——实现多个应用程序公用同一个 Linux 用户 ID 并拥有同样的身份签名。

<application> 元素也支持 android:process 属性,用于指定应用中全部组件的默认进程。

当内存不足时,其他为用户提供更紧急服务的进程又要请求更多内存, Android 系统可能要决定关闭某个进程。 正在此进程中运行着的应用程序组件也会因此被销毁。 当这些组件需要再次工作时,会为他们重新创建一个进程。

在决定杀死哪个进程的时候, Android 系统会权衡它们相对用户的重要程度。 比如,相对于一个拥有可见 Activity 的进程而言,更有可能去关闭一个 Activity 已经在屏幕上看不见的进程。 也就是说,是否终止一个进程,取决于运行其中的组件状态。 后续章节将会介绍终止进程的判定规则。

进程的生命周期

Android 系统会尽可能持久地保留某个应用程序的进程,但为了新建或者运行更加重要的进程,总是需要清除过时的进程,以便回收内存。 为了确定保留或杀死哪个进程,根据进程内运行的组件及这些组件的状态,系统把每个进程都列入一个“重要级别列表(importance hierarchy)”中。 当必需回收系统资源时,重要程度最低的进程会首先被清除,然后是下一个最低的,依此类推,。

重要级别列表共有5级,以下列表按照重要程度列出了各类进程(第一类进程是最重要的,将排在最后终止):

  1. 前台进程

    用户当前操作所必需的进程。满足以下任一条件时,进程被视作处于前台:

    一般情况下,任何时刻前台进程都是为数不多的。 它们只有在别无他法时——内存不足以维持它们同时运行时——才会被终止。 通常,这时候设备已经到了使用虚拟内存(内存分页)的地步,终止一些前台进程是为了保证用户界面的及时响应。

  2. 可见进程

    不包含前台组件、但仍会影响用户在屏幕上所见内容的进程。满足以下任一条件时,进程被视为是可见进程:

    • 其中运行着不在前台的 Activity 对象,但用户仍然可见到此 Activity( onPause() 方法被调用了)。 比如以下场合就可能发生这种情况:前台 Activity 打开了一个对话框,而之前的 Activity 还同时显示在对话框下面。
    • 其中运行着被可见(或前台) Activity 绑定的 Service

    可见进程被认为是非常重要的进程,除非无法维持全部前台进程的运行,不然它们是不会被终止的。

  3. 服务进程

    此类进程中运行着由 startService() 方法启动的服务,它不会升级为前面的两个重要度级别。 尽管服务进程不直接和用户所见内容关联,但它们通常正在执行一些用户关心的操作(比如在后台播放音乐或从网络下载数据)。 因此,除非内存不足以维持全部前台、可见进程的同时运行,系统会保持服务进程的运行。

  4. 后台进程

    包含当前用户不可见 Activity (Activity对象的 onStop() 方法已被调用)的进程。 这些进程对用户体验没有直接的影响,系统可能在任意时间杀死它们,以回收内存供前台进程、可见进程及服务进程使用。 通常会有很多后台进程在运行,所以它们被保存在一个 LRU(最近最少使用)列表中,以确保最近被用户使用的 Activity 最后一个被杀死。 如果一个 Activity 正确实现了生命周期方法,并保存了当前的状态,则终止此类进程不会对用户体验产生可见的影响。 因为当用户返回时,Activity 会恢复所有可见的状态。 关于保存和恢复状态的详细信息,请参阅 Activity 文档。

  5. 空进程

    不包含任何活动应用组件的进程。 保留这类进程的唯一目的就是用作缓存,以改善下次在此进程中运行组件的启动性能。 为了在进程缓存和内核缓存之间平衡系统整体资源,系统经常会杀死这种进程。

依据进程中当前活跃组件的重要程度, Android 会给进程评定一个尽可能高的级别。 例如:如果某个进程中运行着一个服务和一个用户可见的 Activity,则此进程会被定为可见进程,而不是服务进程。

此外,进程的级别可能会由于其他进程的依赖而被提高——为其他进程提供服务的进程级别永远不会低于使用此服务的进程。 比如:如果A进程中的 Content Provider 为进程B中的客户端提供服务,或进程A中的服务被进程B中的组件所调用,则A进程至少是被视为与进程B同等重要。

因为服务所在进程的级别是高于后台 Activity 所在进程的, 所以,如果某个 Activity 需要启动一个长时间运行的操作,则为其启动一个 服务 会比简单地创建一个工作线程更好些——尤其是在该操作的持续时间比 Activity 本身的生存期还要长的情况下。 比如,某 Activity 要把图片上传至 Web 网站,就应该创建一个服务来执行上传任务。 这样即使用户离开了该 Activity,上传还是会在后台继续运行。 不论 Activity 处于什么状态,使用服务可以保证操作至少拥有“服务进程”的级别。 出于同样的考虑,广播接收器(Broadcast Receiver)也应该使用服务来处理耗时任务的,而不是简单地把任务放入线程中去完成。

线程


应用程序启动时,系统会为它创建一个名为“main”的运行线程。 主线程非常重要,因为它负责把事件分发给相应的用户界面部件(Widget),包括屏幕绘图事件。 它也是应用程序与 Android UI 组件包(来自 android.widget 和 android.view 包)进行交互的线程。 因此,主线程有时也被叫做 UI 线程。

系统并不会为每个组件的实例都创建单独的线程。 运行于同一个进程中的所有组件都是实例化于 UI 线程中的,对每个组件的系统调用也都是由 UI 线程分发的。 因此,响应系统回调的方法(比如报告用户操作的 onKeyDown() 或生命周期回调方法)总是运行在 UI 线程中。

举个例子,当用户触摸屏幕上的按钮时,应用程序的 UI 线程会把触摸事件分发给 Widget, Widget 先把自己置为按下状态,再发送一个显示区域已失效(invalidate)的请求到事件队列中。 UI 线程从队列中取出此请求,并通知 widget 需要刷新(redraw)自己。

除非经过了精心设计,不然假如应用程序既要与用户交互又要运行繁重的任务,那么单线程模式就可能会使得运行性能非常低下。 尤其当 UI 线程要处理所有任务时,那些耗时很长的操作——诸如访问网络或查询数据库等——将会冻结所有用户界面。 一旦 UI 线程被阻塞,所有事件都不能被分发,包括屏幕绘图事件。 在用户看来,这时应用程序就像是被挂起了。 更糟糕的是,如果 UI 线程被阻塞超过一定时间(目前大约是5秒钟),用户就会看到那个臭名昭著的“应用程序停止响应”(ANR)对话框。 如果引起用户不满,他就可能会退出并删除这个应用程序。

此外,Andoid 的 UI 组件包并不是线程安全的。 因此不允许从工作线程中操作 UI——只能从 UI 线程中完成所有的用户界面操作。 这样,Andoid 的单线程模式只有两个规则:

  1. 不要阻塞UI线程。
  2. 不要在 UI 线程之外访问 Andoid 的 UI 组件包。

工作线程

根据以上对单线程模式的描述,要想保证程序界面的响应能力,关键是不能阻塞 UI 线程。 如果操作不能很快完成,应该让它们运行于独立的线程中(“后台”或“工作”线程)。

例如:下面给出了点击侦听器(Listener)的部分代码,其中实现了在独立线程中下载图片并在 ImageView中显示:

publicvoid onClick(View v){
    newThread(newRunnable(){
        publicvoid run(){
            Bitmap b = loadImageFromNetwork("http://example.com/image.png");
            mImageView.setImageBitmap(b);
        }
    }).start();
}

乍看起来,这段代码似乎能运行得很好,因为创建了一个新的线程来处理访问网络的操作。 可是这里违反了单线程模式的第二条规则:不要在 UI 线程之外访问 Andoid 的 UI 组件包 ——以上例子在工作线程里而不是 UI 线程里修改了 ImageView。 这会导致不明确、不可预见的后果,要跟踪这种情况也是很困难、很耗时间的。

为了解决以上问题, Android 为从其他线程中访问 UI 线程提供了几种途径。 下面列出了这几种方式:

比如说,可以使用 View.post(Runnable) 方法来修正上面的代码:

publicvoid onClick(View v){
    newThread(newRunnable(){
        publicvoid run(){
            finalBitmap bitmap = loadImageFromNetwork("http://example.com/image.png");
            mImageView.post(newRunnable(){
                publicvoid run(){
                    mImageView.setImageBitmap(bitmap);
                }
            });
        }
    }).start();
}

现在的代码是线程安全的了:有关网络的操作在单独的线程里完成,而 ImageView 是在 UI 线程里操控的。

不过,随着操作变得越来越复杂,这类代码也会变得很复杂很难维护。 为了能用工作线程完成更加复杂的交互处理,可以考虑在工作线程中使用 Handler 来处理由 UI 线程发送的消息。 不过,最佳方案也许是扩展 AsyncTask 类,它将简化与 UI 交互的工作线程的运行模式。

使用 AsyncTask

AsyncTask 能够实现对用户界面的异步操作。 它先阻塞工作线程,再在 UI 线程中呈现结果,在此过程中不需要对线程和 handler 再进行干预。

要使用异步任务,必须继承 AsyncTask 类并实现 doInBackground() 回调方法,该对象运行于一个后台线程池中。 在更新 UI 时,须实现 onPostExecute() 方法来分发doInBackground()返回的结果,由于此方法运行在 UI 线程中,所以就能安全地更新 UI 了。 然后就可以在 UI 线程中调用 execute() 来运行该任务了。,

例如,可以利用 AsyncTask 来实现上面的那个例子:

publicvoid onClick(View v){
    newDownloadImageTask().execute("http://example.com/image.png");
} privateclassDownloadImageTaskextendsAsyncTask<String,Void,Bitmap>{
    /**系统会在工作线程中执行本方法
      * 并传入由AsyncTask.execute()给出的参数*/
    protectedBitmap doInBackground(String... urls){
        return loadImageFromNetwork(urls[0]);
    }
   
    /** 系统在 UI 线程中执行本方法
      * 并传入由 doInBackground() 返回的结果*/
    protectedvoid onPostExecute(Bitmap result){
        mImageView.setImageBitmap(result);
    }
}

现在 UI 是安全的,代码也得到简化,因为这里把任务拆分为工作线程内完成部分和 UI 线程内完成部分。

要全面理解这个类的使用,须阅读 AsyncTask 的参考文档。以下是关于其工作方式的概述:

警告: 在使用工作线程时,另一个可能会碰到的问题是由 运行时的配置变化 (比如用户改变了屏幕方向)导致的 Activity 意外重启,这可能会销毁该工作线程。 要了解如何在这种情况下维持任务执行、以及如何在 Activity 被销毁时正确地取消任务,请参阅 Shelves例程的源代码。

线程安全的方法

某些时候,方法可能会由多个线程调用,因此这些方法必须以线程安全的方式编写。

对于能被远程调用的方法——比如 Bound 服务中的方法,这是理所当然的。 如果对 IBinder 中的方法的调用发起于 IBinder 所在进程的内部,那么这个方法就是执行在调用者的线程中的。 但是,如果调用发起于其他进程,那么这个方法将运行于线程池中的某个线程中(而不是运行于发起进程的 UI 线程中),该线程池由系统维护且位于该 IBinder 所在的进程中。 比如,即使某个服务的 onBind() 方法是从服务所在进程的 UI 线程中发起调用的, onBind() 返回对象中的方法(比如,实现了 RPC 方法的某个子类)仍会从线程池中的线程发起调用。 因为一个服务可能存在多个客户端,所以同时可以有多个线程池与同一个 IBinder 方法相关联。 因此,IBinder 方法必须以线程安全的方式实现代码。

类似地,Content Provider 也能接收来自其他进程的数据请求。 尽管 ContentResolver类、 ContentProvider 类隐藏了进程间通讯管理的细节,ContentProvider 中响应请求的方法—— query()insert()delete()update()getType() ——都是从 Content Provider 所在进程的线程池中发起调用的,而不是从其进程的 UI 线程中发起的。 因为这些方法可能会从任意数量的线程同时发起调用,它们也必须实现为线程安全的。

进程间通讯


Android 利用远程过程调用(remote procedure call,RPC)提供了一种进程间通信(IPC)机制, 通过这种机制,被 Activity 或其他应用程序组件调用的方法将(在其他进程中)被远程执行,而所有的结果将被返回给调用者。 这就要求把方法调用及其数据拆分至操作系统可以处理的程度,并将其从本地的进程和地址空间传输至远程的进程和地址空间,然后在远程进程中重新组装并执行这个调用。 执行完毕后的返回值将被反向传输回来。 Android 已提供了执行 IPC 事务所需的全部代码,因此只要把精力放在定义和实现 RPC 编程接口上即可。

应用程序要进行 IPC,必须用 bindService() 与某个服务进行绑定。 详情请参阅开发指南中的 服务

上一篇:java.lang.OutOfMemoryError: PermGen space


下一篇:Linux定时任务Crontab执行PHP脚本