Android 主线程与子线程关系详解

Android 主线程与子线程关系详解

主线程与屏幕渲染

当用户启动一个的应用时,Android 会创建新的 Linux 进程以及执行线程。这个主线程也称为界面线程(UI 线程),负责屏幕上发生的一切活动。

Android 中,主线程的设计非常简单:它的唯一工作就是从线程安全工作队列中,获取任务(工作块)并执行,直到应用被终止。

主线程执行的这些任务来源有以下几个:与生命周期信息、用户事件(例如输入)或来自其他应用和进程的事件相关的回调。当然,我们在应用开发中也可以不使用框架而自行实现任务队列。

应用执行的任何代码块几乎都与事件回调(例如输入、布局扩充或绘制)相关联。当某个操作触发事件时,发生了事件的子线程会将事件从子线程推送到主线程的消息队列中。然后,主线程可以为事件提供服务。

当正在执行动画或进行屏幕更新时,系统会每隔 16ms 左右尝试执行一个任务(负责绘制屏幕),从而以每秒 60 帧的流畅速度进行渲染。要使系统达到此目标,界面/视图层次结构必须在主线程上更新。但是,如果主线程的消息队列中的任务太多或太长,导致主线程无法足够快地完成更新。如果主线程无法在 16ms 内执行完任务,则用户可能会察觉到卡顿、延迟或界面对输入无响应。 如果主线程阻塞大约 5 秒,系统会显示“应用无响应”(ANR) 对话框,允许用户直接关闭应用。

为了避免主线程因为执行大量耗时任务,而造成卡顿等问题,我们应该将大量或耗时的任务从主线程中移到子线程进行处理,使其不影响流畅渲染和快速响应用户输入。

子线程和 UI 对象之间的引用关系

根据 Android 的设计,Android 中 UI 对象不是线程安全的。无论是创建、使用还是销毁 UI 对象,应用都应在主线程上进行。如果尝试在主线程以外的其他线程中修改甚至引用 UI 对象,则可能导致异常、无提示故障、崩溃以及其他未定义的异常行为。

所以,我们必须在主线程中,进行 UI 对象的操作。上文中说,我们要在子线程中执行耗时任务,但是如果我们进行的耗时任务和 UI 对象有关怎么办呢?比如,我们常见的数据请求操作,先请求数据,然后更新 UI 对象,请求数据通常是耗时操作,如果在主线程操作,必然会导致主线程阻塞,引起卡顿。那么,我们如何用子线程来解决问题呢?

在讨论之前,我们先讨论子线程对 UI 对象的引用的两种类型:显式引用和隐式引用。

显示引用

非主线程上的许多任务的最终目标是更新 UI 对象。但是,如果其中一个线程访问视图层次结构中的某个对象,则可能导致应用不稳定:如果工作线程更改该对象的属性,与此同时有任何其他线程正在引用该对象,则结果无法确定。

例如,假设某个应用在工作线程上直接引用了 UI 对象。工作线程上的该对象可能包含对 View 的引用;但在工作完成之前,View 已从视图层次结构中移除。当这两个操作同时发生时,该引用会将 View 对象保留在内存中,并对其设置属性。但是,这时用户从不会看到此对象,而且应用会在对象引用消失后删除该对象。

再举一个例子,假设 View 对象包含对其所属的 Activity 的引用。如果该 Activity 被销毁,但仍有直接或间接引用它的线程处理任务,则垃圾回收器会等到该处理任务执行完毕再收集该 Activity。

如果在线程处理工作的执行过程中发生某个 Activity 生命周期事件(例如屏幕旋转),这种情况可能会导致问题。在执行中的工作完成之前,系统将无法执行垃圾回收。因此,等到可以进行垃圾回收时,内存中可能有两个 Activity 对象。

在这类情况下,我们一定不要在应用的线程处理工作任务中包含对界面对象的显式引用。避免此类引用有助于防止这些类型的内存泄漏,同时避开线程处理争用。

在任何情况下,应用都只应在主线程上更新界面对象。那子线程和主线程应该如何配合一起工作呢?

我们可以由主线程,转到子线程中,使用子线程来处理耗时任务(比如该例中的数据请求操作),当任务处理完成后,回到主线程中,执行 UI 更新。这样,工作任务在子线程中得到处理,而最终的 UI 视图更新,又转回到主线程统一处理,避免了子线程显示引用 UI 对象造成的各种问题。

隐式引用

在 Java 中内部类(包括匿名内部类和命名内部类),它们的对象都会隐式持有外部类对象的引用,这样就会造成外部类无法垃圾回收的问题。

举一个 Android 中常见的例子:

    public class MainActivity extends Activity {
      // ...
      public class MyAsyncTask extends AsyncTask<Void, Void, String>   {
        @Override protected String doInBackground(Void... params) {...}
        @Override protected void onPostExecute(String result) {...}
      }
    }

示例代码中,将线程处理对象 MyAsyncTask 声明为某个 Activity 的非静态内部类(或 Kotlin 中的内部类)。此声明会导致内部类 MyAsyncTask 持有 Activity 实例的隐式引用。因此,在线程处理工作完成之前,该对象一直包含对相应 Activity 的引用,导致所引用 Activity 的销毁出现延迟,造成内存泄漏问题。

如何解决呢?

其实也简单,只需要将内部类 MyAsyncTask 定义为静态类,即可移除隐式引用。

将 AsyncTask 对象声明为静态嵌套类(或在 Kotlin 中移除内部限定符)。这样做可以消除隐式引用问题,因为静态嵌套类与内部类有所不同:内部类的实例要求对外部类的实例进行实例化,并且可直接访问封装实例的方法和字段。相反,静态嵌套类不需要引用封装类的实例,因此它不包含对外部类成员的引用。

    public class MainActivity extends Activity {
      // ...
      static public class MyAsyncTask extends AsyncTask<Void, Void, String>   {
        @Override protected String doInBackground(Void... params) {...}
        @Override protected void onPostExecute(String result) {...}
      }
    }

线程和应用 Activity 生命周期

应用生命周期会影响子线程在 APP 中的工作方式。我们可能需要确定,子线程在 Activity 销毁后是否应该保留,并且还应注意线程优先级与 Activity 是在前台运行还是在后台运行之间的关系。

保留线程

当 UI 界面销毁后,我们需要确认是否应该继续保留子线程,以继续执行原有任务。

下面举例说明。

假设某个 Activity 生成了一组线程处理工作任务,然后在工作线程能执行工作任务之前被销毁,应用应如何处理正在执行的工作任务呢?

如果工作任务将要更新的是已经销毁了的界面,则该工作不必再继续了。例如,如果该工作任务是从数据库加载用户信息,然后更新视图,则不再需要该线程。

相比之下,工作任务处理的是与更新界面不完全相关的数据操作,并且该数据之后可能需要使用。在这种情况下,我们应该保留该线程。例如,数据包可能正在等待下载图片,将其缓存到磁盘并更新关联的 View 对象。虽然该对象已不存在,但是下载和缓存该图片可能仍然有用,以防用户返回到已销毁的 Activity。

线程优先级

在应用中创建和管理线程时,请务必设置线程的优先级,以便使线程获得正确的优先级。如果子线程的优先级设置得过高,就可能会干扰界主线程和渲染线程,从而导致应用掉帧,产生卡顿。如果子线程优先级设置得过低,可能会导致异步任务(例如图片加载)达不到所需的速度。

系统的线程调度程序会优先考虑优先级较高的线程,在这些优先级与最终将所有工作都完成的需求之间做出权衡。一般来说,前台组约占设备总执行时间的 95%,而后台组约占 5%。

每次创建线程时,都应调用 setThreadPriority() 来设置线程的优先级。

关于线程优先级的设置,请参考《Android 中设置线程优先级的正确方式(2种方法)》

另外,通常我们需要设置线程池中,线程的优先级,如何做呢?请参考:《Android 在线程池中实现线程优先级的代码实现》

系统还会使用 Process 类为每个线程分配系统自己的优先级值。

默认情况下,系统会将线程的优先级设置为与生成它的线程具有相同的优先级和组成员资格,也就是默认情况下,子线程优先级会继承创建它的父线程优先级。但是,我们应该使用 setThreadPriority() 明确调整线程优先级。

Process 类提供了一组线程优先级的常量,以帮助简化优先级值的分配。例如,THREAD_PRIORITY_DEFAULT 代表线程的默认值。如果线程执行的工作不太紧急,应用应将线程的优先级设为 THREAD_PRIORITY_BACKGROUND。

我们也可以使用 THREAD_PRIORITY_LESS_FAVORABLE 和 THREAD_PRIORITY_MORE_FAVORABLE 常量作为增量器来设置相对优先级。

异步线程的创建和使用方法

Android 提供了相同的 Java 类和基元来方便线程处理,例如 Thread、Runnable 和 Executors 类。并且,为了帮助减轻与开发适用于 Android 的线程处理,Android 提供了一组可协助开发的辅助程序,例如 AsyncTaskLoader 和 AsyncTask。每个辅助类都有一组特定的性能细微差别,专用于解决一小部分特定的线程处理问题。将错误的类用在错误的场合可能会导致性能问题。

关于 Android 中使用线程的方法,可以参考前文:《Android 异步任务的6种实现方式详解》

我们应该创建多少线程呢?

上面已经提到了,使用子线程,可以避免主线程由于耗时任务产生的卡顿问题。那么,是不是线程创建的越多越好呢?

尽管在技术层面上,我们可以在代码中创建数百个线程,但这样做会导致性能问题。我们的应用与后台服务、渲染程序、音频引擎、网络等共享有限的 CPU 资源。CPU 实际上只能并行处理少量线程(这个与设备的内核数量相关);一旦超限便会遇到优先级和调度问题。因此,务必要根据工作负载需求创建合适数量的线程。另外,同时要控制子线程的优先级。

子线程和主线程争抢 CPU 资源:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tl4v5SpC-1608193450595)(evernotecid://6FE75482-54A0-433A-9625-A01F7FEE92EC/appyinxiangcom/9896050/ENResource/p2888)]

实际上,要创建多少个线程取决于很多变量,但是可以选择一个值(例如首先选择 4 个),并使用 Systrace 进行测试,这个策略跟任何其他策略一样可靠。我们可以采用试错法得出最少要将线程数减至多少才不至于遇到问题。

在决定创建多少个线程时,还需要考虑到线程不是免费的,它们会占用内存。Android 中的线程,最终是通过 native 层创建的,使用 FixStackSize 设置线程栈大小,默认情况下,线程栈所需内存总大小 = 1M + 8k + 8k,即为 1040k。设备上安装的众多应用会使这一数字迅速累加,特别是在调用堆栈显著扩大的情况下。

其实,通常情况下我们应该复用线程来进行资源优化,我们可以重复使用现有线程池,这样可以减少内存和处理资源争用,从而帮助提高性能。


**PS:更多精彩内容,请查看 --> 《Android 开发》
**PS:更多精彩内容,请查看 --> 《Android 开发》
**PS:更多精彩内容,请查看 --> 《Android 开发》

猜你喜欢

转载自blog.csdn.net/u011578734/article/details/111318450
今日推荐