Android-进程和线程

当一个应用(application)组件首次启动时,Android系统会为这个应用启动一个新的进程(process),并为之开启一个单独的线程(thread);如果一个应用组件启动时它所再的应用已经创建了一个进程,则它直接运行在这个进程中。默认情况下,一个应用所以的组件(Activity/Service/BroadCastReceiver/ContentProvider)都运行在一个同一个进程的同一个线程中(main thread)。当然,也可以指定你的应用组件运行在单独的进程中,你也可以在你的进程中创建多个线程。

进程


默认情况下,应用程序的所有组件都运行在相同的进程中,通常情况下,我们也不需要去改变它们。当然,如你需要,你可以去通过修改 manifest 文件的配置,为你的应用组件指定特定的进程

 manifest 文件中四大组件节点<activity><service><receiver>, 和 <provider>—都支持 android:process 这个属性来指定当前组件特有的进程。于是可能应用组件之间可能就会出现如下关系:

  • 几个应用横向组件运行在单独的进程中
  • 同一个应用的不同组件运行在相同的进程中(共享进程)
  • 不同应用的组件运行在同一个进程中。(当然这个需要两个应用共享 Linux user id 并且提供相同的签名认证)

在某些情况下 Android 会关闭的进程,例如,在 low memory 情况下,其他优先级更高的应用进程(例如电话)起来需要更多内存服务用户时,系统会杀掉那些不重要的进程,同时,依附在这个进程中的应用组件将会被销毁释放。当然,用户再次唤醒它的时候进程会重新启动。

Android 决定关掉哪个进程,时通过权衡进程对用户的重要性来判断的,如何权衡进程对用户的重要性?


进程的生命周期

Android 系统会尽可能的让应用进程存活的越久,但是当系统内存有限,最终不得不杀掉旧进程释放内存来给那些新的对用户更重要的进程。系统如何决定“谁去谁留”?系统会根据运行在进程中的应用组件及应用组件的状态来划分重要性的等级。一共划分了五个等级:

  • 前台进程(Foreground process)

 当前正在被用户请求服务的进程。满足以下条件:

*包含一个 Activity 正在和用户交互(在 Resume 状态下)

*包含一个绑定到 Activity 的 Service ,这个Activity 正在和用户交互。

*包含一个前台Service(Service 调用了startForeground()方法)

*包含一个正在执行 onReceive() 方法的 BroadcastReceiver

总之,在给定的时间内,前台进程不多,它们优先级最高,是最后被列为被杀对象的。

  • 可见进程(Visible process)

一个进程不包含前台组件(Foreground components),但包含能被用户看的见的组件(可见不可交互)。如下几种条件:

*包含一个 Activity,这个 Activity 不坑交互但是可见(处于 onPause()状态),例如 Activity 打开一个半透明的 Dialog

*包含一个 Service 这个 Service 绑定到一个可见但不可交互的 Activity 。

  • Service 进程

一个进程包含一个 Service ,例如调用了 startService ()方法的,但是不包含上面两种更高优先级的组件。例如在后台播放的音乐,或在后台下载数据的 service。

  • 后台进程

一个进程包含 Activity ,但是 Activity 不再对用户可见了。例如一个应用,这个应用没有上面提到的几种组件,这个应用被按了home 键,Activity 处于 onStop()状态。

  • 空进程

一个进程不存在活动的应用组件。系统为什么会有这样的进程?主要是为了缓存,以便下次启动一个应用的时候直接使用它,从而提高启动速度。系统进程会杀掉一些这样的进程以保存进程资源缓存和 Linuxe 内涵缓存资源的平衡。

因为一个运行着 service 的进程优先级比一个运行着后台 activity 的优先级高,这就是为什么要使用一个 service 来处理一个耗时的操作会比简单开启一个工作线程来处理要好的原因。也是为啥在广播中开启线程来处理操作比不上启动一个 service 来操作更保险的原因。

线程(Threads)


当一个应用启动,系统就为这个应用启动了一个线程,这个线程叫做“main” 。这个线程非常重要,因为它负责着整个应用的事件分发及组件的绘制。和 UI 组件交互的操作都是在这个线程中完成的,因此这个线程有叫做 UI 线程(UI Thread)。

系统不会为每个组件创建独立的线程。所以同一个进程里面的所有组件都运行在 UI 线程中。 系统对这些组件的调用、分发都是从 UI 线程开始的。所以,UI 组件的事件回调 (例如 onKeyDown()等事件)都是在 UI 线程中发生的。

例如,当用户点击一个 button ,点击事件从 UI 线程分发(dispatch)到这个 button,button 接收到这会开始设置他的 pressed state ,然后发送一个刷新请求到事件列表,UI 线程从事件列表中取出并通知 button 重会。

由于 UI 的单线程模式,你不能再 UI 线程中处理密集耗时的工作,这将会导致 UI 阻塞,影响用户体验,甚至导致 “application not response”(ANR).对于 UI 线程,要遵循下面简单的两条规则:

1  避免 UI 线程阻塞。

2 避免从其他线程访问 UI 组件。

  • 工作线程(Worker thread)

由于上面我们讨论的 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();
}


首先,我们看到,开启工作线程,下载是没问题的,满足上面说的第一条原则,但是再看代码 


mImageView.setImageBitmap(b);


这里,在工作线程中对 ImageView 进程操作,这里违背来第二条原则,这会引起意想不到的错误或异常。我们可通过以下几种方式来修正这个问题:

例如使用View.post(Runnable)来修改这个问题。


Activity.runOnUiThread(Runnable)
View.post(Runnable)
View.postDelayed(Runnable, long)
For example, you can fix the above code by using the View.post(Runnable) method:

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 的刷新彼此分离。然而,这样会使代码变的很复杂,不好维护。对于工作线程和 UI 交互比较复杂的可以考虑使用 handler 来处理。或许  AsyncTask 也是极其不错的方案。

  • 使用 AsyncTask

AsyncTask 实现 UI 的异步交互。AsyncTask 内部存在一组工作线程(线程池管理),用于处理耗时(阻塞)操作,并且会将操作的结果发布到 UI 线程中进行处理。对与线程的管理及和 UI 交互的细节不用你自己去处理,使用简单方便。

实现 AsyncTask 必须实现 doInBackGround()  方法和 onPostExecute()方法。在doInBackground()方法中处理阻塞操作,在onPostExecute()方法中处理 UI 刷新。我们需要在 UI 线程中调用 AsyncTask 的 execute()方法来开始工作。

例如:


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 阻塞线程安全的解决方案的代码变的简单了。更多关于 AsyncTask 的使用细节,点击  AsyncTask  。

  • 线程安全方法(Thread-safe methods)

在以下解决方案中,你的方法实现可能会被多个线程调用,所以这些方法必须是线程安全的。

这里重要提及的事关于被远程调用的方法,例如,一个 bound service。通过 IBinder 实现的方法调用来自运行这个 IBinder 的进程,方法执行发生着调用者的线程。然而,当方法调用来自另一个进程时,情况就不一样了。方法的执行将发生在一个线程中,这个线程来自一个线程池,这个线程池由运行者这个Binder服务的进程维持。简单的说就是当 boud service (通过 IBinder 通信) 提供的事一个本地服务,服务方法的执行方式在本进程中,并且能能够明确的知道调用发生在哪个线程里。如果 bound service(通过 IBinder 通信)是一个远程服务,那么如果调用来自另一个进程则调用发生在一个不确定的线程中(启用线程池来处理远程请求),所以这种情况下必须保证服务方法是线程安全的。图解:

Android-进程和线程

类似的,像 contentProvider 处理来自另一个进程的请求,也是一种远程服务调用。其实 ContentResolver 和 ContentProvider 已经帮我们封装了进程之间的交互细节。contentProvider 不可能只服务于一个客户端进程,甚至可能同时服务于多个客户端的请求。所以contentProvider 的 insert()、delete()、update()和 getType()等这样的方法都是在一个线程池的线程中执行的。所以这些方法的实现也要确保是线程安全的。

上一篇:HTTP请求流程(一)----流程简介


下一篇:Unix下shell对文件随机读写