您的位置:首页 > 移动开发 > Android开发

进程和线程(Processes and Threads)——翻译自developer.android.com

2016-04-19 18:59 597 查看


线程和进程Processes and Threads

当你的app的组件启动了,并且app中没有其他的组件的时候,Android系统会自动为为这个应用启动一个linux进程,其中有一个执行的线程。默认时,一个app的所有的组件运行在同一个进程的同一个线程,也就是主线程。如果一个组件启动的时候,app已经有一个进程了,(因为可能这个应用中其他的组件已经运行了),那么这个组件就在这个进程中被运行,而且使用同一个线程。但是你可以你的应用中的不同的组件在不同的进程中运行,而且可以给一个进程添加新的线程。

下面的教程讲述Android应用中进程和线程是如何工作的。

进程

默认而言,同一个应用中的组件运行在同一个进程中,应用不应该改变这种设定。但是如果你需要控制某一个组件对应一个线程,那么你可以在manifest文件中进行设定。

manifest文件给四大组件元素——<activity>,<service>,<receriver>和<provider>都提供了一个android:process的属性,来指定组件可以在哪一个进程中运行。你可以设定来使得所有的组件都在各自的进程中运行,也可以让其中几个共享一个进程。你也可以通过设定android:process属性来使得不同应用中的组件在同一个进程中运行,这样就可以实现不同的应用使用同一个Linux用户ID,并且使用同一个证书签名。

<application>元素也支持android:process属性,可以为其中的元素都设定一个默认的属性。

当内存不足的时候,或者用户启动了更加迫近使用的程序的时候,Android可能会在一些时刻关闭一些进程。而在被关闭进程中的组件,会按照顺序被销毁。当这个进程里面的组件又有任务的时候,这个进程就会被重新创建。

Android系统会通过衡量进程相对于用户的权重,来决定销毁哪些的进程。比如销毁一个含有一个很久都没有显示过的activity的进程,要比销毁一个正在显示的activity所在的进程好。进程是否被销毁取决于在进程中运行的组件的状态。用来判别是否结束线程规则将在下文讨论。

进程的声明周期 Process lifecycle

Android系统会尽可能长时间地维护一个进程,但是终究会为了更新更重要的进程来将旧的进程结束,以回收空间。系统根据进程中拥有的组件和组件的状态来给进程设立优先级等级,每一个进程都在优先级等级当中。最低重要性的进程会最先被淘汰,然后是次不重要的,以此类推,直到系统回收了足够的所需空间。

在重要性等级中有五个层次。下面按照重要性的顺序来列举这些等级。(最前面的是最重要的,最不能被杀死的)

1.前台进程

  这是个用户正在使用的进程。如果下面的情况成立就认为一个进程是前台进程。

  - 其中包含一个正在和用户交互的activity(activity的onResume方法被调用过)。

  -包含一个与正在和用户交互的activity绑定的service。

  - 包含了一个运行在前台的service,也就是service调用过startForeground

  - 包含一个正在执行这些声明周期回调方法的service:onCreate,onStart,或者onDestroy。

  - 包含了一个正在执行onReceive方法的BroadcastRecevier。

通常来讲,只有一些的前台进程才会一直都存在。他们只是最后被杀死,如果内存很低他们就不能都运行。通常在这些时候设备已经进入了内存分页状态,所以需要保证杀掉一些进程的时候用户交互的界面得以保留。

2.可视进程

这是一种虽然已经不包含任前台的组件,但是仍然影响用户用户的屏幕显示的进程。如果下面任意的情况都属于可视进程。

  - 包含一个不在前台了的activity,但是仍然可以被用户看到(调用了onPause方法)。比如一个前台的activity开启了一个对话框,与此同时之前的那个activity仍然在后面可见。

  - 包含一个绑定了可见或者前台activity的service。

可见进程的重要性很高,除非为了保护前台进程,否则不会回收可视进程。

3.服务进程

包含一个使用startService启动的服务,并且服务不在上面两种的级别里面。尽管服务的进程的功用用户不能直接可见,但是他们通常都和用户关心的业务相关。比如说后台播放音乐以及从网络下载数据。所以系统会尽量保证其运行,除非内存空间很低,为了保护前两者等级。

4.后台进程

后台进程是包含现在用户不可见的的activity的进程(调用过activity的onStop方法)。它们对用户体验没有直接影响,系统可以在任何时候杀掉他们,给前台,可视,或者服务进程腾出空间。总是有很多的后台进程,所以系统会维护一个LRU(多久没用了的列表least recent used)来使得最近刚刚用过的那些activity最后被杀死。如果一个activity正确地实现了他的生命周期方法,保存了当前的状态,那么杀死他不会对用户体验造成可以看见的影响。因为当用户从新进入这些activity的时候,activity恢复了它的所有的可见状态。关于存储和恢复状态,可以参见Activity文档。

5.空进程

这是不包含任何的activity和组建的进程。保留这种进程唯一目的是用作缓存,来提升下此次其中组件启动需要运行的时间。系统通过杀掉这种进程,来平衡整个系统的里的,进程缓存和底层的内核的缓存。

Android系统按照他们包含组件的最高等级的情况来判断优先级。例如如果一个进程包含了一个service和一个可视的activity,那么就被判定为可视进程而非服务进程。

另外,一个进程可能因为被其他的进程所依赖而提升等级。一个被依赖进程的等级一定会比依赖它的进程等级高。例如A进程中的content provider正在为进程B的客户端服务,或者进程A中的服务被B中的组件绑定了,那么A的重要性就会比B高。(至少相同)。因为一个运行了service的进程优先级高于后台activity的优先级,所以与其启动了一个进行长时间运行操作的activity(通过建立一个线程)不如启动一个service更好。特别是这个操作要比activity的生存更长时间的时候。例如如果一个activity通过启动一个service来用网路上传图片,那么即使用户离开了这个activity,图片仍然在被上传。使用service可以保证操作至少拥有service级别的优先级,无论activity发生了什么。这也是为什么broadcast应该使用service,而不是仅仅把费时操作放入一个线程中。

线程 Thread

当启动一个应用的时候,为其创建一个执行线程,叫主线程。这个线程十分重要,因为它肩负着派发事件到合适的用户交互的的组件上去的任务,包括绘制事件。她也是你的应用和Android UI工具箱(android.widget和android.view包中的组件)中的组件进行交互的线程。这样而言,主线程有时候也叫作UI线程。(用户交互线程)

系统不会为每一个组件创建分别的线程。 在同一个进程中的组件都是有UI线程实例化的,并且系统对每一个组件的调用都被派发到这个线程中。因此,响应系统调用的方法,比如说onKeyDown方法,用来汇报用户动作的,或者声明周期调用,都是在进程的UI线程中调用的。

例如,当用户用户点了一下屏幕上的按钮,你的应用的UI线程就会把这个触控事件派发给控件,反过来它会设定成按下的状态,并且发送一个使之无效的请求给事件队列(因为已经响应过了)。UI线程会把请求出列,并且向控件发出重绘自己的通知。

如果你的app要在响应用户交互中运行高负荷,那么如果你没有合适地构造你的应用,单线程模型就会性能很差。特别是如果什么事情都交给UI线程去做的话,网络访问或者数据库查询,就会使得整个UI都阻塞。当线程被阻塞的时候,就无法再派发事件,包括绘制事件。从用户的角度来说,就是应用卡了。更差劲的是如果UI线程阻塞了大概5秒以上,就会弹出一个恼人的应用没有响应ANR对话框。用户会为此不开心,从而关掉甚至卸载你的应用。

另外,Android UI工具箱是线程不安全的。所以你不可以在你的工作线程中(也就是非UI线程)操作篡改UI——你只能也必须在UI线程中操作UI。如此仅有两个Android单线程模型的规则:

1.不要阻塞UI线程。

2.不要在UI线程以外访问访问UI工具箱。

工作线程 (后台线程) Worker threads

基于上面讨论的单线程模型的特性,保持你的应用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。例子中是在工作线程中进行了UI操作,而不是在UI线程中。这会导致一个未定义的和出乎意料的结果,这会很难也很费时间来追踪这种错误。

为了修整这种错误,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线程中发过来的消息。可能最好的解决办法通过扩展AsynTask类,它简化了工作线程需要跟UI线程交互需要做的工作。

使用异步任务  Using AsyncTask

AsyncTask可以让你在你的UI上进行异步的工作。它将阻塞的进程放在工作线程,并在UI线程中发布工作进展的情况。不需要你自己处理thread和handler。

你需要继承AsyncTask并且实现doInBackgound()的回调方法,这个方法会在后台的线程池里面运行。如果要一边更新你的UI,你需要实现onPostExcecute方法,它会发送doInBackground()的结果,并且在UI线程中运行,所以你可以安全地更新你的UI。你可以在UI线程中调用execute()方法来启动这个task。

我们依然用上面例子演示:

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() 系统调用这个方法来进行UI线程的UI操作,它接收从doInBackground中传来的结果*/
    protected void onPostExecute(Bitmap result) {
        mImageView.setImageBitmap(result);
    }
}

现在UI是安全的情况下,代码也更简单了,因为它将应该在UI线程和工作线程中进行的业务分离了。

你可以阅读AsyncTask的文档来全面的了解怎样使用这个类,下面是一个快速一览:

- 你可以使用泛型来制定传入的参数类型,过程变量,和任务的结果返回类型。

- doInBackground会在工作线程自动执行。

- onPreExcecute(),onPostExecute()和onProgressUpdate()都是在UI线程中执行的。

-在doInBackground()中返回的参数被传入到onPoastExecute()中。

- 你可以在doInBackground中的任何时候调用publishProgress()来在UI线程上执行onProgressUpdate()。

- 你可以在任何时候,任何线程中来取消任务。

警告:当你使用工作线程的时候可能会遇到一个问题。就是因为运行时适配变化导致的activity的重启(比如说当用户改变了屏幕的方向),这可能会销毁掉你的工作线程。想知道怎样在这种重启中继续保持你的任务,和怎样在activity销毁的时候合适地取消任务,请参见Shelves 例子的源代码。

线程安全的方法 Thread-safe methods

有时候你写的方法可能会被很多的线程调用,所以你的方法需要写的线程安全。

对于远程调用的方法而言,确实是这样,比如说bound service中的方法。当调用运行在同一个进程中的IBinder中的方法时,方法会在调用者的线程中执行。当这个调用在另一个进程的时候,方法会在IBinder所在的进程中维护的线程池中选择一个,在其中来执行方法(排除进程中的UI线程)。例如一个service的onBind方法可能被service进程中的UI线程调用,onBind返回对象(比如说实现了RPC方法的子类)中的方法就会从线程池中的一个线程中调用。因为一个service可能有很多个客户端,多个在线程池中的方法都可以同时调用同一个IBinder中的方法。因此IBinder一定要构造地线程安全。

类似地,一个content provider可能同时接收其他进程中的数据请求。尽管ContentResolver和ContentProvider隐藏了跨进程的通信是怎样管理的,ContentProvider中的响应请求的方法-query(),insert(),delete(),update(),getType-都是在content provider所在的进程的线程池的线程中调用的。因为这些方法可能被任意数量的线程同时调用,所以它们也一定要被定义为线程安全。

跨进程通信 Interprocess Communication

Android提供了一种跨进程通信机制(IPC),它使用远程调用来实现(RPCs)。其中的方法从一个activity或者其他的应用组件中发起调用,在远程(其他进程)中执行,并且可以返回给调用者结果。这就需要把调用的方法和其数据分解为一个操作系统可以理解的层面上,把它们从本地进程和地址空间传送到远程的进程和地址空间,在那里重新组装以后再重新激活。返回值同样的方式传回。Android系统提供了所有实现IPC传送的代码,所以你需要关注的就是定义和实现RPC编程接口。

要实现IPC,你的应用需要使用bindServie来绑定(bind)一个服务。更多的信息可以参见教程的Service文档。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: