「通关Android」我们熟悉的陌生人-进程和线程

今天文章的题目叫做「我们熟悉的陌生人」,为何这么一说?

因为关于进程和线程,几乎每个开发者每天都会碰到并且在运用它们,Android应用程序是以事件驱动机制为主的单进程应用,一个进程在整个系统中就是一个程序,系统会分配给他们进程号,进程名称、进程占用的内存资源等等。同样在进程中,做企业级开发的应用几乎不会存在只有一个主线程的页面,基本都是多线程异步构建的,特别是Android这种不能在主线程阻塞的系统,超过系统规定的阻塞时间,那么该应用就会报ANR错误。这些就是我们觉得熟悉的由来。

但是陌生又谈何说起呢,因为在多线程开发过程中,很多开发者并没有掌握的很好,多线程可能会提高CPU占用率,内存资源进一步扩展,没有合理的分配多线程资源,可能就会引起应用的性能问题。一旦出现性能问题,我们就需要去分析发生性能问题的本质原因是什么,这个排查过程是需要耗费比较大的时间去分析的,有时候分析出来很可能就是应用在前期开发规划时没有妥善处理多线程问题,所以说,我们对于如何合理分配多线程资源又是陌生的。

那么话不多说,今天主要介绍的主题是「进程和线程」。

关于定义

进程(Process)是程序的一个运行实例,而线程(Thread)则是CPU调度的基本单位。现在大部分操作系统都是支持多任务运行,这一特性让用户感到计算机好像可以同时处理很多事情。其实在只有一个CPU核心情况下,这种同时处理是一种假象,这是因为操作系统采用的分时的方法,为正在运行的多个任务分配合理、单独的CPU时间片来实现的。只不过这个时间运行时间是以微秒算,并且不断进行时间片切换,最终运行起来,让用户以为是多任务实时运行,这也是因为机器运算速度够快而已。

那么在Android系统中,我们所熟知的四大组件并不是程序的全部,应用在启动后会创建一个ActivityThread的主线程和两个Binder线程,这也是系统分配给该应用的默认资源。

关于Handler、MessageQueue、Looper

在Android系统里,这几个名词相信对于每个开发者都不陌生,但是就是“深恶痛绝”,因为感觉很熟悉,但就是说不明白,今天我以简单化的语句把它给勾兑明白。看张图:

循环示意图

先说这三者的关系:Loope不断获取MessageQueue中的一个Message,然后由Handler来处理。

其实就是这么简单,Looper类似有个死循环机制,如果MessageQueue有数据过来,就从中读取,如果MessageQueue没有,Looper自身就挂起,这个挂起不会阻塞应用主线程,然后把读取的数据交给Handler来处理,这样就完成了线程之间的切换和消息传递。

一般这三者的应用场景常见于后台多IO计算或网络请求这种比较耗时类计算,两个线程直接做通信,一个线程做数据准备,应用主线程做相关UI刷新。至于这三者怎么联系起来,可以具体去上网搜索相关资料,不要有太多分析源码的资料。

还有一点在面试中会经常被碰到一个问题,那就是Handler Post Delayed的问题,回答这个问题关键点是:因为MessageQueque读取时,如果碰到有Delayed Time,那么线程会执行Sleep Time操作,直到时间超过了,则恢复线程操作,继续进行分发。

关于UI 主线程 - ActivityThread

Android系统的源码中,有这么一个文件类,名称叫做「ActivityThread」,它就是应用的主线程,Looper启动是通过prepareMainLooper方法,同样有个叫做sMainThreadHandler,这个就是与Looper相绑定的Handler对象。

在ActivityThread里,提供了一个“事件管家”,以处理主线程的各种消息,也就是说我们经常要UI线程中刷新最新的UI界面,基本都是需要在ActivityThread提供的sMainThreadHandler来处理。还有一点就是,事件管家机制其实本质就是创建循环处理消息的环境,因为消息才是推动整个系统动起来的基础。

具体过程如下:

  • loop函数。不断地从消息队列中获取需要处理的事件。
  • 消息队列。如果消息队列为空,它会进入睡眠以让出CPU资源,如果消息队列不为空,则提供消息事件。
  • Handler。被分发到消息事件,进行相应的处理

从源码角度来看的时序图如下:

时序图

关于Thread类

在Java中有关线程的定义是体现在Thread类上,这个类的属性和方法仅用于完成「线程管理」这个任务。这边我们需要重点关注的是线程中几个状态的切换。

线程状态

  • New。线程已经创建,但还没Start
  • Runnable。线程处于可运行的状态,一切就绪
  • Running。线程正在运行中
  • Blocked。线程处于阻塞状态,比如等待某个锁的释放
  • Waiting。线程处于等待状态
  • Dead。线程停止运行

Thread 休眠、等待、唤醒是多线程编程非常重要的环节,从逻辑上来讲,线程无非就是运行和不运行的两种可能,只有把握住这一点就能理清,分析下什么条件下会出现不运行的状态,诸如休眠、等待等等。然后再结合Java中的wait()、notify()、notifyAll()、interrupt()、join()和sleep(),那就会清晰很多。

小结

经过以上的三个定义,相信大家对于进程和线程理解会更透彻点,至于在以后的项目开发中,对于多线程资源的分配问题,可以先参考业内最佳实践做法,一般都会有个线程池的来管理线程,避免线程的乱创建和可以复用已有的线程资源,特别是在多线程通信时,碰到问题不妨多打印LOG日志,把当前线程需要竞争资源的状态给打印出来,这样更有利于分析和写出合理的线程分配代码。

,