关于 Android 进程和线程,你必须了解的东西

前言

按照操作系统中的描述。线程是 CPU 调度的最小单元,同时线程也是一种有限的资源。而进程一般指一个执行单元,在 PC 和移动设备上指一个程序或者一个应用。一个进程可以包含多个线程。对于 Android 来说,它是一种基于 Linux 内核的移动操作系统,它的进程和线程有着其特有的性质。我们这篇文章就来聊聊关于 Android 中的进程和线程,我们需要了解的知识。

进程

当一个程序第一次启动的时候,Android 会启动一个 Linux 进程和一个主线程。默认情况下,同一应用的所有组件均在相同的进程中运行,且大多数应用都不会改变这一点。如果我们发现需要控制某个组件所属的进程,则可在清单文件中执行此操作。

组件运行在哪个进程中,是在 AndroidManifest 文件中进行设置的,<activity>、<service>、<receiver> 和 <provider> 均支持 android:process 属性,此属性可以指定该组件应在哪个进程运行。我们可以设置此属性,使每个组件均在各自的进程中运行,或者使一些组件共享一个进程,而其他组件则不共享。此外,<application> 元素还支持 android:process 属性,用来设置适用于所有组件的默认值。

进程的优先级

Android 系统将尽量长时间地保持应用进程,但为了新建进程或运行更重要的进程,最终需要移除旧进程来回收内存。为了确定保留或终止哪些进程,系统会根据进程中正在运行的组件以及这些组件的状态,将每个进程放入 “重要性层次结构” 中。必要时,系统会首先消除重要性最低的进程,然后是重要性相对较高的进程,以此类推,以回收进程。

重要性层次结构一共有 5 级

1、前台进程 — Foreground process

用户当前操作所必需的进程。如果一个进程满足以下任一条件,即是为前台进程:

  • 托管用户正在交互的 Activity(已调用 Activity 的 onResume() 方法)
  • 托管某个 Service,后者绑定到用户正在交互的 Activity
  • 托管正在 “前台” 运行的 Service(服务已调用 startForeground())
  • 托管正执行一个生命周期回调的 Service(onCreate()、onStart() 或 onDestroy())
  • 托管正执行其 onReceive() 方法的 BroadcastRecevier

通常,在任意给定时间前台进程都为数不多。只有在内存不足以支撑他们同时运行这一万不得已的情况下,系统才会终止它们。此时,设备往往已达到内存分页状态,因此需要终止一些前台进程来确保用户界面正常响应。

2、可见进程 — Visible process

  没有任何前台组件、但仍会影响用户在屏幕上所见内存的进程

  • 托管不在前台、但仍对用户可见的 Activity(已调用其 onPause() 方法)
  • 托管绑定到可见(或前台)Activity 的 Service

可见进程被视为极其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程。

3、服务进程 — Service process

正在运行已使用 startService() 方法启动的服务且不属于和上述两个更高类别进程的进程。尽管服务进程与用户所见内容没有直接关联,但它们通常在「执行一些用户关心的操作」(例如,在后台播放音乐或从网络下载数据)。因此,除非内存不足以维护所有前台进程和可见进程同时运行,否则会让服务进程保持运行状态。

4、后台进程 — Background process

包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。这些进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。

5、空进程 — Empty process

不含任意活动应用组件的进程。保留这种进程的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。

比较常见的使用场景

由于运行服务的进程级别高于托管后台 Activity 的进程,因此启动长时间运行操作的 Activity 最好为此操作启动服务,而不是简单地创建工作线程,当操作有可能比 Activity 更加持久时尤要如此。

例如,正在将图片上传到网站的 Activity 应该启动服务来执行上传,这样一来,即使用户退出 Activity,仍可在后台继续执行上传操作。使用服务可以保证,无论 Activity 发生什么情况,该操作至少具备 “服务进程” 优先级。同理,广播接收器也应使用服务,而不是简单地将耗时冗长的操作放入线程中。

线程

线程在 Android 中是一个很重要的概念,从用途上来说,线程分为主线程和子线程,主线程的作用是「运行四大组件以及处理它们和用户的交互」,而子线程的作用则是「执行耗时任务,比如网络请求、I/O 操作等」,由于 Android 的特性,如果在主线程中执行耗时操作那么就会导致程序无法及时地响应。因此耗时操作必须放在子线程中执行。

Android 中的线程形态

除了 Thread 本身以外,在 Android 中可以扮演线程角色的还有很多,比如 AsyncTask 和 IntentService,同时 HandlerThread 也是一种特殊的线程。尽管 AsyncTask、IntentService 以及 HandlerThread 的「表现形式」都有别于传统的线程,但是它们的本质仍然是传统的线程。对于 AsyncTask 来说,它的底层用到了线程池,对于 IntentService 和 HandlerThread 来说,他们的底层则直接使用了线程。

不同形式的线程虽然都是线程,但是它们仍然有不同的特性和使用场景。AsyncTask 封装了线程池和 Handler,它主要是为了方便开发者在子线程中更新 UI。HandlerThread 是一种具有消息循环的线程,在它的内部可以使用 Handler。IntentService 内部采用 HandlerThread 来执行任务,当任务执行完毕后 IntentService 会自动退出。

从任务执行的角度来看,IntentService 的作用很像一个后台线程,但是 IntentService 是一种服务,它不容易被系统杀死从而可以尽量保证任务的执行,而如果是一个后台线程的话,由于这个时候进程中没有活动的四大组件,那么这个进程的优先级就会非常低,会很容易被系统杀死,这就是 IntentService 的优点。

主线程的一些事

从 Android 3.0 开始,系统要求网络访问必须在子线程中进行,否则网络访问将会失败并抛出 NetworkOnMainThreadException 这个异常,这样做是为了避免主线程由于被耗时操作阻塞从而出现 ANR 现象。

而 Android 规定访问 UI 只能在主线程中进行,如果在子线程中访问 UI,那么程序就回抛出异常。ViewRootImpl 对 UI 操作做了验证,这个验证工作是由 ViewRootImpl 的 checkThread() 方法来完成的。

void checkThread(){
      if(mThread != Thread.currentThread()){
          throw new CalledFromWrongThreadException(
                 "Only the original thread that created a view hierarch can
                  ouch its views.");
      }
}

Android 系统为什么不允许在子线程中访问 UI 呢?这是因为 Android 的 UI 控件不是线程安全的,如果在多线程中并发访问可能会导致 UI 控件处于不可预期的状态,那为什么系统不对 UI 控件的访问加上锁机制呢?

缺点有两个

  • 加上锁机制会让 UI 访问的逻辑变得复杂

  • 锁机制会降低 UI 访问的效率

鉴于这两个缺点,最简单且高效的方法就是采用单线程模型来处理 UI 操作,对于开发者来说也不是很麻烦,只是需要通过 Handler 切换一下 UI 的访问执行线程即可。

参考资料

    原文作者:developerHaoz
    原文地址: https://www.jianshu.com/p/49b6addc0a40
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞