返回顶部
首页 > 资讯 > 移动开发 >如何正确使用Android线程详解
  • 760
分享到

如何正确使用Android线程详解

android线程Android 2022-06-06 07:06:52 760人浏览 独家记忆
摘要

前言 对于移动开发者来说,“将耗时的任务放到子线程去执行,以保证UI线程的流畅性”是线程编程的第一金科玉律,但这条铁则往往也是UI线程不怎么流畅的主因。我们在督促自己更多的使用

前言

对于移动开发者来说,“将耗时的任务放到子线程去执行,以保证UI线程的流畅性”是线程编程的第一金科玉律,但这条铁则往往也是UI线程不怎么流畅的主因。我们在督促自己更多的使用线程的同时,还需要时刻提醒自己怎么避免线程失控。

多线程编程之所以复杂原因之一在于其并行的特性,人脑的工作方式更符合单线程串行的特点。一个接着一个的处理任务是大脑最舒服的状态,频繁的在任务之间切换会产生“头痛”这类系统异常。人脑的多任务和计算机的多任务性能差异太大导致我们在设计并行的业务逻辑之时,很容易犯错。

另一个复杂点在于线程所带来的副作用,这些副作用包括但不限于:多线程数据安全,死,内存消耗,对象的生命周期管理,UI的卡顿等。每一个新开的线程就像扔进湖面的石子,在你忽视的远处产生涟漪。

把抽象的东西具像化是我们认知世界的主要方式。线程作为操作系统世界的“公民”之一,是如何被调度获取到CPU和内存资源的,又怎么样去和其他“公民”互通有无进而实现效益最大化?把这些实体和行为具像到大脑,像操作系统一样开“上帝视角”,才能正确掌控线程这头强大的野兽。

进程优先级(Process Priority)

线程寄宿在进程当中,线程的生命周期直接被进程所影响,而进程的存活又和其优先级直接相关。在处理进程优先级的时候,大部分人靠直觉都能知道前台进程(Foreground Process)优先级要高于后台进程(Background Process)。但这种粗糙的划分无法满足操作系统高精度调度的需求。无论Android还是iOS,系统对于Foreground,Background进程有进一步的细化。

Foreground Process

Foreground一般意味着用户双眼可见,可见却不一定是

active
。在Android的世界里,一个Activity处于前台之时,如果能采集用户的
input
事件,就可以判定为
active
,如果中途弹出一个
Dialog
Dialog
变成新的
active
实体,直接面对用户的操作。被部分遮挡的
activity
尽管依然可见,但状态却变为
inactive
。不能正确的区分
visible
active
是很多初级程序员会犯的错误。

Background Process

后台进程同样有更细的划分。所谓的Background可以理解为不可见(invisible)。对于不可见的任务,Android也有重要性的区分。重要的后台任务定义为

Service
,如果一个进程包含
Service
(称为Service Process),那么在“重要性”上就会被系统区别对待,其优先级自然会高于不包含
Service
的进程(称为Background Process),最后还剩一类空进程(Empty Process)。Empty Process初看有些费解,一个Process如果什么都不做,还有什么存在的必要。其实Empty Process并不Empty,还存在不少的内存占用。

ioS的世界里,

Memory
被分为
Clean Memory
Dirty Memory
Clean Memory
是App启动被加载到内存之后原始占用的那一部分内存,一般包括初始的
stack, heap, text, data
segment,Dirty Memory
是由于用户操作所改变的那部分内存,也就是App的状态值。系统在出现Low Memory Warning的时候会首先清掉Dirty Memory,对于用户来说,操作的进度就全部丢失了,即使再次点击App图标,也是一切从头开始。但由于
Clean Memory
没有被清除,避免了从磁盘重新读取app数据的io损耗,启动会变快。这也是为什么很多人会感觉手机重启后,app打开的速度都比较慢。

同理Android世界当中的Empty Process还保存有App相关的

Clean Memory
,这部分Memory对于提升App的启动速度大有帮助。显而易见Empty Process的优先级是最低的。

综上所述,我们可以把Android世界的Process按优先级分为如下几类:

进程的优先级从高到低依次分为五类,越往下,在内存紧张的时候越有可能被系统杀掉。简而言之,越是容易被用户感知到的进程,其优先级必定更高。

线程调度(Thread Scheduling)

Android系统基于精简过后的linux内核,其线程的调度受时间片轮转和优先级控制等诸多因素影响。不少初学者会认为某个线程分配到的time slice多少是按照其优先级与其它线程优先级对比所决定的,这并不完全正确。

Linux系统的调度器在分配

time slice
的时候,采用的
CFS(completely fair scheduler)
策略。这种策略不但会参考单个线程的优先级,还会追踪每个线程已经获取到的
time slice
数量,如果高优先级的线程已经执行了很长时间,但低优先级的线程一直在等待,后续系统会保证低优先级的线程也能获取更多的CPU时间。显然使用这种调度策略的话,优先级高的线程并不一定能在争取
time slice
上有绝对的优势,所以Android系统在线程调度上使用了
cgroups
的概念,
cgroups
能更好的凸显某些线程的重要性,使得优先级更高的线程明确的获取到更多的
time slice

Android将线程分为多个

group
,其中两类
group
尤其重要。一类是
default group
,UI线程属于这一类。另一类是
background group
,工作线程应该归属到这一类
。background group
当中所有的线程加起来总共也只能分配到5~10%的
time slice
,剩下的全部分配给
default group
,这样设计显然能保证UI线程绘制UI的流畅性。

有不少人吐槽Android系统之所以不如iOS流畅,是因为UI线程的优先级和普通工作线程一致导致的。这其实是个误会,Android的设计者实际上提供了

background group
的概念来降低工作线程的CPU资源消耗,只不过与iOS不同的是,Android开发者需要显式的将工作线程归于
background group


new Thread(new Runnable() {
   @Override
   public void run() {
     Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
   }
}).start();

所以在我们决定新启一个线程执行任务的时候,首先要问自己这个任务在完成时间上是否重要到要和UI线程争夺CPU资源。如果不是,降低线程优先级将其归于

background group
,如果是,则需要进一步的profile看这个线程是否造成UI线程的卡顿。

虽说Android系统在任务调度上是以线程为基础单位,设置单个

thread
的优先级也可以改变其所属的
control groups
,从而影响
CPU time slice
的分配。但进程的属性变化也会影响到线程的调度,当一个App进入后台的时候,该App所属的整个进程都将进入
background group
,以确保处于
foreground
,用户可见的新进程能获取到尽可能多的CPU资源。用adb可以查看不同进程的当前调度策略。


$ adb shell ps -P

当你的App重新被用户切换到前台的时候,进程当中所属的线程又会回归的原来的

group
。在这些用户频繁切换的过程当中,
thread
的优先级并不会发生变化,但系统在
time slice
的分配上却在不停的调整。

是否真的需要新线程?

开线程并不是提升App性能,解决UI卡顿的万金油。每一个新启的线程会消耗至少64KB的内存,系统在不同的线程之间

switch context
也会带来额外的开销。如果随意开启新线程,随着业务的膨胀,很容易在App运行的某个时间点发现几十个线程同时在运行。后果是原本想解决UI流畅性,却反而导致了偶现的不可控的卡顿。

移动端App新启线程一般都是为了保证UI的流畅性,增加App用户操作的响应度。但是否需要将任务放入工作线程需要先了解任务的瓶颈在哪,是i/o,gpu还是cpu?UI出现卡顿并不一定是UI线程出现了费时的计算,有可能是其它原因,比如layout层级太深。

尽量重用已有的工作线程(使用线程池)可以避免出现大量同时活跃的线程,比如对Http请求设置最大并发数。或者将任务放入某个串行的队列(HandlerThread)按顺序执行,工作线程任务队列适合处理大量耗时较短的任务,避免出现单个任务阻塞整个队列的情况。

用什么姿势开线程?

new Thread()

这是Android系统里开线程最简单的方式,也只能应用于最简单的场景,简单的好处却伴随不少的隐患。


new Thread(new Runnable() {
  @Override
  public void run() {
  }
}).start();

这种方式仅仅是起动了一个新的线程,没有任务的概念,不能做状态的管理。start之后,run当中的代码就一定会执行到底,无法中途取消。

Runnable作为匿名内部类还持有了外部类的引用,在线程退出之前,该引用会一直存在,阻碍外部类对象被GC回收,在一段时间内造成内存泄漏。

没有线程切换的接口,要传递处理结果到UI线程的话,需要写额外的线程切换代码。

如果从UI线程启动,则该线程优先级默认为

Default
,归于
default cgroup
,会平等的和UI线程争夺CPU资源。这一点尤其需要注意,在对UI性能要求高的场景下要记得


Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

虽说处于

background group
的线程总共只能争取到5~10%的CPU资源,但这对绝大部分的后台任务处理都绰绰有余了,1ms和10ms对用户来说,都是快到无法感知,所以我们一般都偏向于在
background group
当中执行工作线程任务。

AsyncTask

一个典型的AsyncTask实现如下:


public class MyAsyncTask extends AsyncTask {
   @Override
   protected Object doInBackground(Object[] params) {
      return null;
   }
   @Override
   protected void onPreExecute() {
     super.onPreExecute();
   }
   @Override
   protected void onPostExecute(Object o) {
     super.onPostExecute(o);
   }
}

和使用

Thread()
不同的是,多了几处api回调来严格规范工作线程与UI线程之间的交互。我们大部分的业务场景几乎都符合这种规范,比如去磁盘读取图片,缩放处理需要在工作线程执行,最后绘制到
ImageView
控件需要切换到UI线程。

AsyncTask的几处回调都给了我们机会去中断任务,在任务状态的管理上较之

Thread()
方式更为灵活。值得注意的是AsyncTask的
cancel()
方法并不会终止任务的执行,开发者需要自己去检查
cancel
的状态值来决定是否中止任务。

AsyncTask也有隐式的持有外部类对象引用的问题,需要特别注意防止出现意外的内存泄漏。

AsyncTask由于在不同的系统版本上串行与并行的执行行为不一致,被不少开发者所诟病,这确实是硬伤,绝大部分的多线程场景都需要明确任务是串行还是并行。

线程优先级为

background
,对UI线程的执行影响极小。

HandlerThread

在需要对多任务做更精细控制,线程切换更频繁的场景之下,

Thread()
AsyncTask
都会显得力不从心。HandlerThread却能胜任这些需求甚至更多。

HandlerThread将

Handler
Thread
Looper
MessageQueue
几个概念相结合。Handler是线程对外的接口,所有新的
message
或者
runnable
都通过
handler post
到工作线程。
Looper
MessageQueue
取到新的任务就切换到工作线程去执行。不同的
post
方法可以让我们对任务做精细的控制,什么时候执行,执行的顺序都可以控制。HandlerThread最大的优势在于引入
MessageQueue
概念,可以进行多任务队列管理。

HandlerThread背后只有一个线程,所以任务是串行执行的。串行相对于并行来说更安全,各任务之间不会存在多线程安全问题。

HandlerThread所产生的线程会一直存活,Looper会在该线程中持续的检查

MessageQueue
。这一点和
Thread(),AsyncTask
都不同,thread实例的重用可以避免线程相关的对象的频繁重建和销毁。

HandlerThread
较之
Thread(),AsyncTask
需要写更多的代码,但在实用性,灵活度,安全性上都有更好的表现。

ThreadPoolExecutor

Thread(),AsyncTask
适合处理单个任务的场景,HandlerThread适合串行处理多任务的场景。当需要并行的处理多任务之时,ThreadPoolExecutor是更好的选择。


public static Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);

线程池可以避免线程的频繁创建和销毁,显然性能更好,但线程池并发的特性往往也是疑难杂症的源头,是代码降级和失控的开始。多线程并行导致的bug往往是偶现的,不方便调试,一旦出现就会耗掉大量的开发精力。

ThreadPool较之HandlerThread在处理多任务上有更高的灵活性,但也带来了更大的复杂度和不确定性。

IntentService

不得不说Android在API设计上粒度很细,同一样工作可以通过各种不同的类来完成。

IntentService
又是另一种开工作线程的方式,从名字就可以看出这个工作线程会带有
service
的属性。和
AsyncTask
不同,没有和UI线程的交互,也不像HandlerThread的工作线程会一直存活。IntentService背后其实也有一个HandlerThread来串行的处理
Message Queue
,从IntentService的
onCreate
方法可以看出:


@Override
public void onCreate() {
  // TODO: It would be nice to have an option to hold a partial wakelock
  // during processing, and to have a static startService(Context, Intent)
  // method that would launch the service & hand off a wakelock.
  super.onCreate();
  HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
  thread.start();
  mServiceLooper = thread.getLooper();
  mServiceHandler = new ServiceHandler(mServiceLooper);
}

只不过在所有的

Message
处理完毕之后,工作线程会自动结束。所以可以把
IntentService
看做是
Service
HandlerThread
的结合体,适合需要在工作线程处理UI无关任务的场景。

总结

Android开线程的方式虽然五花八门,但归根到底最后还是映射到linux下的pthread,业务的设计还是脱不了和线程相关的基础概念范畴:线程的执行顺序,调度策略,生命周期,串行还是并行,同步还是异步等等。摸清楚各类API下线程的行为特点,在设计具体业务的线程模型的时候自然轻车熟路了,线程模型的设计要有整个app视角的广度,切忌各业务模块各玩各的。以上就是本文的全部内容,希望对大家开发Android能有所帮助,如果有疑问欢迎大家留言讨论。

您可能感兴趣的文章:全面总结Android中线程的异步处理方式Android 中三种启用线程的方法总结


--结束END--

本文标题: 如何正确使用Android线程详解

本文链接: https://lsjlt.com/news/24313.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 如何正确使用Android线程详解
    前言 对于移动开发者来说,“将耗时的任务放到子线程去执行,以保证UI线程的流畅性”是线程编程的第一金科玉律,但这条铁则往往也是UI线程不怎么流畅的主因。我们在督促自己更多的使用...
    99+
    2022-06-06
    android线程 Android
  • 详解Redis单线程的正确理解
    很多同学对Redis的单线程和I/O多路复用技术并不是很了解,所以我用简单易懂的语言让大家了解下Redis单线程和I/O多路复用技术的原理,对学好和运用好Redis打下基础。 一、R...
    99+
    2024-04-02
  • 如何正确使用注解
    本篇内容介绍了“如何正确使用注解”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!日志脱敏场景简介在日志里我们的日志一般打印的是 model 的...
    99+
    2023-06-16
  • 如何正确区分Python线程
    这篇文章给大家介绍如何正确区分Python线程,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。在Python语言中Python线程可以从这里开始与主线程对GIL的竞争,在t_bootstrap中,申请完了GIL,也就是说...
    99+
    2023-06-17
  • 一篇文章带你了解如何正确使用java线程池
    目录1、线程是不是越多越好?2、如何正确使用多线程?3、Java线程池的工作原理4、掌握JUC线程池API总结1、线程是不是越多越好? 在学习多线程之前,读者可能会有疑问?如果单线程...
    99+
    2024-04-02
  • GO使用Mutex确保并发程序正确性详解
    目录1. 简介2. 基本使用2.1 基本定义2.2 使用方式2.3 使用例子2.3.1 未使用mutex同步代码示例2.3.2 使用mutex解决上述问题3. 使用注意事项3.1 L...
    99+
    2023-03-03
    GO Mutex并发正确性 Mutex确保并发正确性
  • Python线程池的正确使用方法
    目录Python线程池的正确使用1、为什么要使用线程池呢?2、线程池怎么用呢?3、如何非阻塞的获取线程执行的结果4、线程池的运行策略Python线程池的正确使用 1、为什么要使用线程...
    99+
    2024-04-02
  • 如何正确的使用@SpringBootApplication注解
    如何正确的使用@SpringBootApplication注解?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。对SpringBoot工程的自动配置很感兴趣,于是学习其源码并整...
    99+
    2023-06-14
  • 如何正确使用MVCC
    这篇文章主要讲解了“如何正确使用MVCC”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“如何正确使用MVCC”吧! 简单理解版以下先引用我之前写过的...
    99+
    2024-04-02
  • 如何正确使用@property
    本篇内容主要讲解“如何正确使用@property”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“如何正确使用@property”吧!他们是这样说的:class People: ...
    99+
    2023-06-15
  • 如何正确使用WideCharToMultiByte
    要正确使用WideCharToMultiByte函数,需要按照以下步骤操作:1. 确定要转换的宽字符编码方式。WideCharToM...
    99+
    2023-09-26
    使用
  • 一文了解Java 线程池的正确使用姿势
    目录概述线程池介绍线程池创建ThreadPoolExecutor创建Executors创建newFixedThreadPoolnewCachedThreadPoolnewSingle...
    99+
    2022-11-13
    Java 线程池使用 Java 线程池
  • springboot中redis正确的使用详解
    redis实现了对数据的缓存,在项目里一些字典数据,会话数据,临时性数据都会向redis来存储,而在springboot里对redis也有支持,一般来说多个线程共同使用一个redis...
    99+
    2024-04-02
  • SpringBoot中@Import注解如何正确使用
    目录简介一、功能简介二、示例1.引入普通类2.引入ImportSelector的实现类(1)静态import场景(注入已知的类)(2)动态import场景(注入指定条件的类...
    99+
    2024-04-02
  • python如何正确使用yield
    目录生成器nextsendthrowclose使用场景大集合的生成简化代码结构协程与并发总结生成器 如果在一个方法内,包含了 yield 关键字,那么这个函数就是一个「生成器」。 生成器其实就是一个特殊的迭代器,它...
    99+
    2022-06-02
    python yield 使用yield
  • 如何正确的使用Gradle
    本篇文章为大家展示了如何正确的使用Gradle,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。一、Gradle相比Maven的优势配置简洁Maven是用pom.xml管理,引入一个jar包至少5行代码...
    99+
    2023-06-06
  • 如何正确的使用javascript
    本篇文章为大家展示了如何正确的使用javascript,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。使用javascript的方法:可以用script标签引入<script type...
    99+
    2023-06-14
  • 如何正确的使用springcloud
    这期内容当中小编将会给大家带来有关如何正确的使用springcloud,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。一、微服务简介 Ⅰ、我对微服务的理解微服务是软件开发的一种架构方式,由单一的应用小程序构...
    99+
    2023-06-14
  • 如何正确的使用Puppet
    今天就跟大家聊聊有关如何正确的使用Puppet,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。1. 概述 puppet是一个开源的软件自动化配置和部署工具,它使用简单且功能强大,正得到...
    99+
    2023-06-12
  • 如何正确的使用pytest
    本篇文章为大家展示了如何正确的使用pytest,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。1、安装pytest,打开dos窗口输入:pip install pytest2、通过pycharm工具下...
    99+
    2023-06-07
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作