Android 5从入门到精通
上QQ阅读APP看书,第一时间看更新

3.6 进程和线程

当一个应用组件启动,并且该应用没有别的正在运行的组件,则Android系统会为这个应用程序创建一个包含单个线程的linux进程。默认情况下,同一个应用程序的所有组件都运行在同一个进程与线程中(叫作“main”主线程)。某个应用组件启动,如果该应用程序的进程已经存在(因为应用程序的其他组件已经在运行了),那么刚刚启动的组件会在已有的进程和线程中启动运行。不过,可以指定组件运行在其他进程中,也可以为任何进程创建其他的线程。

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

3.6.1 进程(Processes)

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

manifest文件中的每种组件元素(<activity>、<service>、<receiver>和<provider>)都支持android:process属性,用于指定组件所属运行的进程。设置此属性即可实现每个组件在各自的进程中运行,或者某几个组件共享一个进程而其它组件不可以参与。设置此属性也可以让来自于不同应用程序的组件运行在同一个进程中,实现多个应用程序共享同一个Linux user ID,相同的签名认证。

<application>元素也支持android:process属性,用于指定所有组件的默认值。

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

在决定关闭哪个进程的时候,Android系统会权衡它们相对用户的重要程度。比如,相对于一个拥有可视activity的进程,更有可能去关闭一个持有一组不再对用户可见的Activity的进程。也就是说,是否终止一个进程,取决于运行在此进程中组件的状态。终止进程的判定规则将在后续内容中讨论。(注:一个进程的关闭级别,按照该进程中最高的级别来定义。如该进程中有activity和service,那么该进程的级别为service。)

Android系统试图尽可能长时间地保持应用程序进程,但为了新建的或者更为重要的进程,总是需要清除掉旧的进程以回收内存。为了决定保留或终止哪个进程,根据进程内运行的组件及这些组件的状态,系统把每个进程都划入一个“importance hierarchy”中。重要性最低的进程首先会被清除,然后是其次低的进程,依此类推,这都是回收系统资源所必需的。

“importance hierarchy”共有5级,以下列表按照重要程度列出了各类进程(第一类进程是最重要的,将最后一个被终止):

(1)前台进程(Foreground process)

用户正在请求的进程。当以下任何一个条件成立时,该进程被认为是前台进程:


●持有一个用户正在与之交互的Activity(Activity对象的onResume()方法已被调用)。

●持有一个服务Service,且该服务被绑定到一个正在与用户交互的Activity上了。

●持有一个服务,且该服务在前台运行,即该服务startForground()调用。

●持有一个服务,且该服务正在执行其生命周期的回调方法(onCreate(),onStart(),onDestroy())。

●持有一个BroadcastReceiver,且其正在执行onRecevie()方法。


通常,在一个给定的时间内,只有很少的前台进程存在。当系统内存较小,以至于他们不能全部继续运行时,他们会依序被清除。通常,这时设备已经到了内存分页状态(memory paging state),清除那些前台进程以确保用户响应。

(2)可视进程(Visible process)

一个可视进程是没有前台组件,但仍会影响用户在屏幕上所见内容的进程。当以下任何一个条件成立时,该进程被认为是可视进程:


●持有一个Activity,且该Activity没有处于前台,但是对于用户而言他仍然可见(onPause()方法被调用)。这是可能发生的,例如,一个前台activity启动了一个对话框,而之前的activity还允许显示在后面。

●持有一个服务Service,且该服务被绑定到一个可视(或一个前台)activity。


一个可视进程是极其重要的,除非无法维持所有前台进程同时运行了,它们是不会被终止的。

(3)服务进程(Service process)

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

(4)后台进程(Background process)

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

(5)空进程(Empty process)

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

依据进程中目前活跃组件的重要程度,Android会给进程评估一个尽可能高的等级。例如,一个进程拥有一个服务和一个用户可见的activity,则此进程会被评定为可视进程,而不是服务进程。

此外,一个进程的等级可能会由于其他进程的依赖而被提高,一个服务于另一个进程的进程永远不能比另一个进程的等级低。比如,进程A中的content provider为进程B中的客户端提供服务,或进程A中的服务被进程B中的组件所调用,则进程A被认为其重要等级不低于进程B。

因为运行服务的进程级别高于后台activity进程的等级,所以,如果activity需要启动一个长时间运行的操作,则为其启动一个服务service会比简单地创建一个工作线程更好些,尤其是在此操作时间比activity本身存在时间还要长久的情况下。比如,一个activity要把图片上传至Web网站,就应该创建一个服务来执行,即使用户离开了此activity,上传还是会在后台继续运行。无论activity发生什么情况,使用服务可以保证操作至少拥有服务进程(service process)的优先级。同理,上一篇中的广播接收器broadcast receiver也是使用服务而非简单的启用一个线程。

3.6.2 线程(Threads)

应用程序启动时,系统会为它创建一个名为“main”的主线程。主线程非常重要,因为它负责分配事件到合适的用户接口,包括绘图事件。它也是应用程序与Android UI组件包(来自android.widget和android.view包)进行交互的线程。因此,主线程有时也被叫作UI线程。

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

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

如果应用程序在与用户交互的同时需要执行繁重密集的任务,单线程模式可能会导致运行性能很低下,除非应用程序的执行时机很合适。如果UI线程需要处理每一件事情,那些耗时很长的操作(诸如访问网络或查询数据库等)将会阻塞整个UI(线程)。一旦线程被阻塞,所有事件都不能被分发,包括屏幕绘图事件。从用户的角度来看,应用程序看上去似乎被挂起了。更糟糕的是,如果UI线程被阻塞超过一定时间(目前设置大约是5秒钟),用户就会被提示“应用程序没有响应”(ANR)的对话框。如果引起用户不满,可能就会决定退出并删除这个应用程序。

此外,Andoid的UI组件包并不是线程安全的。因此不允许从工作线程中操作UI,只能从UI线程中操作用户界面。因此,Andoid的单线程模式必须遵守两个规则:


●不允许阻塞UI线程。

●不允许在UI线程之外访问Andoid的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线程之外访问Android的UI组件包。这个例子在工作线程里而不是UI线程里修改了ImageView,这可能导致不明确、不可预见的后果,要跟踪这种情况也是很困难很耗时的。

为了解决以上问题,Android提供了几种方法,从其他线程中访问UI线程。下面列出了有助于解决问题的几种方法:


●Activity.runOnUiThread(Runnable)

●View.post(Runnable)

●View.postDelayed(Runnable, long)


例如,可以使用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();
    }

现在,这个代码的执行线程是安全的了。网络相关的操作在单独的线程里完成,而ImageView是在UI线程里操纵的。

不过,随着操作变得越来越复杂,这类代码也会变得复杂且难以维护。为了用工作线程完成更加复杂的交互处理,可以考虑在工作线程中用Handler来处理UI线程分发过来的消息。当然,最好的解决方案也许就是继承使用异步任务类AsyncTask,此类简化了一些工作线程和UI交互的操作。

(2)使用异步任务(AsyncTask)

异步任务允许以异步的方式对用户界面进行操作。它先阻塞工作线程,然后在UI线程中显示结果,在此过程中不需要对线程和handler进行人工干预。

要使用异步任务,必须继承AsyncTask类并实现doInBackground()回调方法,该对象将运行于一个后台线程池中。要更新UI时,须实现onPostExecute()方法来分发doInBackground()返回的结果。由于此方法运行在UI线程中,所以就能安全地更新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来指定参数的类型、进度值和任务最终值。

●工作线程中的doInBackground()方法会自动执行。

●onPreExecute()、onPostExecute()和onProgressUpdate()方法都在UI线程中调用。

●doInBackground()的返回值会传给onPostExecute()。

●在doInBackground()内的任何时刻,都可以调用publishProgress()来执行UI线程中的onProgressUpdate()。

●可以在任何时刻、任何线程内取消任务。


提示

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

3.6.3 线程安全方法

在一些情况下,实现的方法可能会被多个线程调用,因此他应该设计线程为安全的。

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

类似的,content provider也能接收来自其他进程的数据请求。尽管ContentResolver类、ContentProvider类隐藏了进程间通讯管理的细节,ContentProvider中响应请求的方法有:query()、insert()、delete()、update()和getType()方法,这些方法都会从ContentProvider所在进程的线程池中被调用,而不是进程的UI线程。由于这些方法可能会从很多线程中同时被调用,所以它们也必须实现为线程安全的。

3.6.4 进程间的通信

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

要执行IPC,应用程序必须用bindService()绑定到服务上。详情请参阅服务Services开发指南。