您好,Android 线程的合理使用对于保障应用的流畅性和性能至关重要。本页从以下几个方面讨论线程的使用:使用界面线程(即主线程);应用生命周期与线程优先级之间的关系;以及平台为帮助管理线程复杂性所提供的方法。对于每个方面,本页都介绍了潜在的陷阱和相应的规避策略。
当用户启动您的应用时,Android 会创建新的 Linux 进程以及执行线程。这个主线程也称为界面线程,负责屏幕上发生的一切活动。了解其工作原理有助于您通过设计让应用利用主线程实现最佳性能。
内部原理:主线程的设计非常简单:它的唯一工作就是从线程安全工作队列获取工作块并执行,直到应用被终止。框架会从多个位置生成部分工作块。这些位置包括与生命周期信息、用户事件(例如输入)或来自其他应用和进程的事件相关的回调。此外,应用也可以不使用框架而自行对块进行明确排队。
在应用中采用线程处理的最大好处是可以将大量或冗长的任务从主线程中移除,使其不影响流畅渲染和快速响应用户输入。
根据设计,Android 视图对象不是线程安全的。无论是创建、使用还是销毁界面对象,应用都应在主线程上完成。如果您尝试在主线程以外的其他线程中修改甚至引用界面对象,可能导致异常、无提示故障、崩溃以及其他未定义的异常行为。
在许多情况下,非主线程上的任务的最终目标是更新界面对象。然而,如果其中一个线程访问视图层次结构中的某个对象,可能会导致应用不稳定:如果工作器线程更改该对象的属性,与此同时有任何其他线程正在引用该对象,结果将无法确定。
例如,假设某个应用在工作器线程上直接引用了界面对象。工作线程上的该对象可能包含对 的引用;但在工作完成之前, 已从视图层次结构中移除。当这两个操作同时发生时,该引用会将 对象保留在内存中,并对其设置属性。但是,用户绝不会看到此对象,而且应用会在对象引用消失后删除该对象。
再举一个例子,假设 对象包含对其所属 activity 的引用。如果该 activity 被销毁,但仍有直接或间接引用它的工作块在接受线程处理,那么垃圾回收器会等到该工作块执行完毕后再收集该 activity。如果在线程处理工作的过程中发生 activity 生命周期事件(例如屏幕旋转),那么上述情况可能会导致问题。在接受线程处理的工作完成之前,系统将无法执行垃圾回收。因此,等到可以进行垃圾回收时,内存中可能有两个 对象。
在这种情况下,我们建议您不要让应用在接受线程处理的工作任务中包含对界面对象的显式引用。避免此类引用有助于防止这些类型的内存泄漏,同时避免出现线程处理争用。在任何情况下,应用都只应在主线程上更新界面对象。这意味着您应制定允许多个线程将工作传回主线程的协商政策,让最顶层的 activity 或 fragment 负责更新实际界面对象。
隐式引用是一个常见的代码设计缺陷。以下代码段演示了接受线程处理的对象的问题:
```java
MyAsyncTask
```
此问题的直接解决方法是将过载的类实例定义为静态类,或在其自己的文件中定义,从而移除隐式引用。
```java
```
线程和应用 activity 生命周期:应用生命周期会影响线程处理在应用中的工作方式。您可能需要确定线程在 activity 销毁后应不应该保留。您还应注意线程优先级与 activity 是在前台运行还是在后台运行之间的关系。
保留线程:线程会在生成这些线程的 activity 的生命周期过后继续保留。无论是否发生 activity 创建或销毁事件,线程都会继续不间断地执行,但在没有其他处于活跃状态的应用组件时,线程会与应用进程一起终止。在某些情况下,这种持久性是可取的。
在 Android 开发中,如果一个 Activity 生成了一组需要线程处理的工作块,但在工作器线程可以执行相应工作块之前被销毁,该如何处理正在执行的工作块?一种可能的解决方案是:如果工作块将要更新的界面不再存在,那么该工作就不必再继续。例如,如果该工作是从数据库加载用户信息并更新视图,那么便不再需要该线程。
相比之下,工作数据包可能具有某种不完全与界面相关的优势。在这种情况下,应该保留该线程。例如,数据包可能正在等待下载图片,将其缓存到磁盘并更新关联的对象。虽然该对象已不存在,但是下载和缓存该图片可能仍然有用,以防用户返回到已销毁的 Activity。
此外,Android 提供了 ViewModelLiveDataViewModel 类来帮助管理这种情况。ViewModel 可以用于存储与生命周期相关的数据和状态,以及处理与 UI 无关的任务,如网络请求和后台任务。这样可以确保在 Activity 被销毁后,相关的任务仍然可以在子线程中安全地执行。
在处理多线程时,还需要注意线程优先级的问题。根据进程和应用的生命周期阶段,应用线程的优先级会有所不同。因此,在创建和管理线程时,应设置适当的线程优先级,以确保合适的线程能够适时获得合适的优先级。系统会使用 ThreadPriority 枚举为每个线程分配系统自己的优先级值。默认情况下,系统会为线程和生成它的线程设置相同的优先级和组成员资格。但是,应用可以通过明确调整线程优先级来更改这些设置。
对于将 Kotlin 作为主要语言的开发者,建议使用协程来处理多线程。协程具有编写没有回调的异步代码以及用于限定范围、取消和错误处理的结构化并发的优点。同时,可以使用 HandlerThread 类来创建处理程序线程,这个线程实际上是一个长时间运行的线程,会从队列中抓取工作并对其进行操作。
在获取相机预览帧时,常见问题包括:
1. 回调在界面线程上被调用,处理大型像素矩阵的任务会干扰渲染和事件处理工作。为解决这个问题,可以将命令委托给处理程序线程上的工作块,并设置优先级。
2. 当使用创建线程时,需要根据线程正在执行的工作类型设置其优先级。CPU只能并行处理少量线程,设置优先级有助于系统了解调度工作的正确方法。
为了简化这个过程,可以使用HandlerThread类。这个类可以帮助管理线程的创建、设置优先级以及管理工作在这些线程之间的分布情况。它还可以根据工作负载生成最佳数量的线程。
在构造对象时,需要设置最小和最大线程数。随着工作负载的增减,HandlerThread会启动或销毁更多线程以适应工作负载。此外,它还会考虑初始化的最小和最大线程计数,并根据待处理工作量决定在任何特定时间应保留多少线程。
实际操作过程中,可以根据工作负载需求创建合适数量的线程。通常选择一个值(例如首先选择4个),并使用Systrace进行测试。如果遇到问题,可以逐步减少线程数以找到合适的平衡点。
需要注意的是,线程不是免费的,它们会占用内存。每个线程至少需要占用64 K内存。设备上安装的众多应用会使这一数字迅速累加,特别是在调用堆栈显著扩大的情况下。因此,在决定创建多少个线程时,还需要考虑到内存使用情况。
许多系统进程和第三方库经常会启动自己的线程池。如果您的应用可以重复使用现有线程池,可以减少内存和处理资源争用,从而帮助提高性能。