这里又是老生畅谈的话了,前边已经有多篇文章针对线程进行探究解释,Android开发过程中线程的体现更是淋漓尽致。Android开发过程中涉及到的线程从大类上分可以归为两类:UI线程和非UI线程。本篇就根据这两类做一个总结。

谈到线程,首先顺带讲一下Android上进程的相关知识,进程和线程是相辅相成的,前边我也写过一篇针对进程和线程概括性的解释——《什么是进程,什么是线程》,这里就针对Android上面向开发的做一个记录总结。

当某个应用组件启动且该应用没有运行其他任何组件时,Android 系统会使用单个执行线程为应用启动新的 Linux 进程。默认情况下,同一应用的所有组件在相同的进程和线程(称为“主”线程)中运行。 如果某个应用组件启动且该应用已存在进程(因为存在该应用的其他组件),则该组件会在此进程内启动并使用相同的执行线程。 但是,您可以安排应用中的其他组件在单独的进程中运行,并为任何进程创建额外的线程。

进程

默认情况下,同一应用的所有组件均在相同的进程中运行,且大多数应用都不会改变这一点。 但是,如果您发现需要控制某个组件所属的进程,则可在清单文件中执行此操作。

各类组件元素的清单文件条目、、和均支持 android:process 属性,此属性可以指定该组件应在哪个进程运行。此外, 元素还支持 android:process 属性,以设置适用于所有组件的默认值。

如果内存不足,而其他为用户提供更紧急服务的进程又需要内存时,Android 可能会决定在某一时刻关闭某一进程。在被终止进程中运行的应用组件也会随之销毁。 当这些组件需要再次运行时,系统将为它们重启进程。

进程生命周期

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

重要性层次结构一共有 5 级。以下列表按照重要程度列出了各类进程(第一个进程最重要,将是最后一个被终止的进程):

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

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

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

  1. 可见进程
    没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。 如果一个进程满足以下任一条件,即视为可见进程:

    • 托管不在前台、但仍对用户可见的 Activity(已调用其 onPause() 方法)。例如,如果前台 Activity 启动了一个对话框,允许在其后显示上一 Activity,则有可能会发生这种情况。
    • 托管绑定到可见(或前台)Activity 的 Service。

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

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

  2. 后台进程
    包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。这些进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 通常会有很多后台进程在运行,因此它们会保存在 LRU (最近最少使用)列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。如果某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该 Activity 时,Activity 会恢复其所有可见状态。 有关保存和恢复状态的信息,请参阅 Activity文档。

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

主线程(UI线程)

应用启动时,系统会为应用创建一个名为“主线程”的执行线程。 此线程非常重要,因为它负责将事件分派给相应的用户界面小部件,其中包括绘图事件。 此外,它也是应用与 Android UI 工具包组件(来自 android.widget 和 android.view 软件包的组件)进行交互的线程。因此,主线程有时也称为 UI 线程。

系统不会为每个组件实例创建单独的线程。运行于同一进程的所有组件均在 UI 线程中实例化,并且对每个组件的系统调用均由该线程进行分派。 因此,响应系统回调的方法(例如,报告用户操作的 onKeyDown() 或生命周期回调方法)始终在进程的 UI 线程中运行。

主线程是不安全的

android主线程是不安全的,这句话是不是经常听到?上边也说了所有相关的UI更新操作均在主线程(UI线程),为什么都要在主线程中完成呢?这里就是主线程不全性导致的,其实主线程的不安全性就是指的UI刷新界面展示的不安全性。前边一篇文章《Android开发Handler消息机制探究》开篇提到,从主线程中可以创建多个子线程来分配任务,一个activity的所有view都是唯一的,都有唯一的标识,如果在每个子线程中更新view,我们不能预知线程执行结果的先后顺序,也就无法预知什么时候才能更新view,所以造成结果就是view更新时的冲突问题。官方也是为了规避这种多线程执行无序导致冲突的问题,所以从安卓2.0之后规定只能在主线程中更新界面了。

单线程模式下必须遵守两条规则

现在又有一个新问题了,既然都要在主线程中去执行有关界面的更新操作,主线程势必给人感觉比较“重”,在应用执行繁重的任务以响应用户交互时,除非正确实现应用,否则这种单线程模式可能会导致性能低下。 具体地讲,如果 UI 线程需要处理所有任务,则执行耗时很长的操作(例如,网络访问或数据库查询)将会阻塞整个 UI。 一旦线程被阻塞,将无法分派任何事件,包括绘图事件。 从用户的角度来看,应用显示为挂起。 更糟糕的是,如果 UI 线程被阻塞超过几秒钟时间(目前大约是 5 秒钟),用户就会看到一个让人厌烦的“应用无响应”(ANR) 对话框。如果引起用户不满,他们可能就会决定退出并卸载此应用。

因此,您不得通过工作线程操纵 UI,而只能通过 UI 线程操纵用户界面。 因此,Android 的单线程模式必须遵守两条规则:

  1. 不要阻塞UI线程
  2. 不要在UI线程之外访问Android UI 组件(工具包)

工作线程(非UI线程)

根据上述单线程模式,要保证应用UI的响应能力,关键是不能阻塞 UI 线程。 如果执行的操作不能很快完成,则应确保它们在单独的线程(“后台”或“工作”线程)中运行。

如何从非UI线程访问UI线程

以下代码演示了一个点击侦听器从单独的线程下载图像并将其显示在ImageView中:

public void onClick(View v) {new Thread(new Runnable() {public void run() {Bitmap b = loadImageFromNetwork("http://example.com/image.png");mImageView.setImageBitmap(b);}}).start();
}

乍看起来,这段代码似乎运行良好,因为它创建了一个新线程来处理网络操作。 但是,它违反了单线程模式的第二条规则:不要在UI线程之外访问Android UI组件(工具包) ,此示例从工作线程(而不是UI线程)修改了ImageView。 这可能导致出现不明确、不可预见的行为,但要跟踪此行为困难而又费时。

为解决此问题,Android 提供了几种途径来从其他线程访问 UI 线程:

  • 使用Handler实现线程之间的通信
  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • View.postDelayed(Runnable, long)

例如,您可以通过使用 View.post(Runnable) 方法修复上述代码:

public void onClick(View v) {new Thread(new Runnable() {public void run() {final Bitmap bitmap =loadImageFromNetwork("http://example.com/image.png");mImageView.post(new Runnable() {public void run() {mImageView.setImageBitmap(bitmap);}});}}).start();
}

AsyncTask

少量情况下非UI线程访问UI线程可以采用上边的Activity.runOnUiThread(Runnable)View.post(Runnable),多的情况下可以采用Handler+Thread方式,但是这种也不好,代码量太大。好在Android官方给我们封了一个可以异步处理并在UI线程中回调的类——AsyncTask,AsyncTask可以正确,方便地使用UI线程。此类允许您执行后台操作并在UI线程上发布结果,而无需操作线程和/或处理程序。

AsyncTask的设计其实也是围绕一个辅助类Thread和Handler,AsyncTask主要用于短时间内的异步回调操作,如果长时间执行线程,还是强烈建议采用各种API java.util.concurrent包,如ExecutorThreadPoolExecutorFutureTask

AsyncTask的泛型参数

AsyncTask<Params,Progress,Result>是一个抽象类,通常用于被继承.继承AsyncTask需要指定如下三个泛型参数:

  • Params:启动任务执行时输入的参数类型.
  • Progress:后台任务执行中返回进度值的类型.
  • Result:后台任务执行完成后返回结果的类型.

AsyncTask主要方法

AsyncTask主要有如下几个方法:

  • doInBackground:必须重写,异步执行后台线程要完成的任务,耗时操作将在此方法中完成.
  • onPreExecute:执行后台耗时操作前被调用,通常用于进行初始化操作.
  • onPostExecute:当doInBackground方法完成后,系统将自动调用此方法,并将doInBackground方法返回的值传入此方法.通过此方法进行UI的更新.
  • onProgressUpdate:当在doInBackground方法中调用publishProgress方法更新任务执行进度后,将调用此方法.通过此方法我们可以知晓任务的完成进度.

AsyncTask使用遵循规则及缺点

使用AsyncTask时必须遵循如下规则:

  • 必须在UI线程中创建AsyncTask的实例
  • 必须在UI线程中调用AsyncTask的execute()方法
  • 重写的四个方法是系统自动调用的,不应手动调用
  • 每个AsyncTask只能被执行一次,多次调用将会引发异常

AsyncTask使用时虽然很简单,并且块化好管理,但是AsyncTask也有一定的缺点,使用的过程中也要格外在意:

  1. 线程池中已经有128个线程,缓冲队列已满,如果此时向线程提交任务,将会抛出RejectedExecutionException。过多的线程会引起大量消耗系统资源和导致应用FC的风险。
  2. AsyncTask不会随着Activity的销毁而销毁,直到doInBackground()方法执行完毕。如果我们的Activity销毁之前,没有取消 AsyncTask,这有可能让我们的AsyncTask崩溃(crash)。因为它想要处理的view已经不存在了。所以,我们总是必须确保在销毁活动之前取消任务。如果在doInBackgroud里有一个不可中断的操作,比如BitmapFactory.decodeStream(),调用了cancle() 也未必能真正地取消任务。关于这个问题,在4.4后的AsyncTask中,都有判断是取消的方法isCancelled()。
  3. 如果AsyncTask被声明为Activity的非静态的内部类,那么AsyncTask会保留一个对创建了AsyncTask的Activity的引用。如果Activity已经被销毁,AsyncTask的后台线程还在执行,它将继续在内存里保留这个引用,导致Activity无法被回收,引起内存泄露。
  4. 屏幕旋转或Activity在后台被系统杀掉等情况会导致Activity的重新创建,之前运行的AsyncTask会持有一个之前Activity的引用,这个引用已经无效,这时调用onPostExecute()再去更新界面将不再生效。

AsyncTask简单使用

以下是一个AsyncTask创建声明的例子:

 private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {protected Long doInBackground(URL... urls) {int count = urls.length;long totalSize = 0;for (int i = 0; i < count; i++) {totalSize += Downloader.downloadFile(urls[i]);publishProgress((int) ((i / (float) count) * 100));// Escape early if cancel() is calledif (isCancelled()) break;}return totalSize;}protected void onProgressUpdate(Integer... progress) {setProgressPercent(progress[0]);}protected void onPostExecute(Long result) {showDialog("Downloaded " + result + " bytes");}}

声明好后,只需要在主线程中简单执行execute即可:

new DownloadFilesTask().execute(url1, url2, url3);

最后

我是i猩人,总结不易,转载注明出处,喜欢本篇文章的童鞋欢迎点赞、关注哦。

参考

  • https://developer.android.com/guide/components/processes-and-threads.html
  • https://developer.android.com/reference/android/os/AsyncTask.html
  • 《疯狂Android讲义》——李刚

Android开发之UI线程和非UI线程相关推荐

  1. android 组件 线程,Android UI线程和非UI线程

    UI线程及Android的单线程模型原则 当应用启动,系统会创建一个主线程(main thread). 这个主线程负责向UI组件分发事件(包括绘制事件),也是在这个主线程里,你的应用和Android的 ...

  2. SWT的UI线程和非UI线程

    为什么80%的码农都做不了架构师?>>>    要理解UI线程,先要了解一下"消息循环"这个概念.链接是百度百科上的条目,简单地说,操作系统把用户界面上的每个操作 ...

  3. Android开发之无bug滑动删除源码(非第三方库)

    Android开发之无bug滑动删除源码(非第三方库源码请在最后面自行下载) 1.我们先来看下效果图:上边是抽取出来的demo,下边是公司用到的项目 2.我们来看下如何调用(我们这里以listView ...

  4. Android开发之旅:应用程序基础及组件

    --成功属于耐得住寂寞的人,接下来几篇将讲述Android应用程序的原理及术语,可能会比较枯燥.如果能够静下心来看,相信成功将属于你. 引言 为了后面的例子做准备,本篇及接下来几篇将介绍Android ...

  5. Android 开发之旅:深入分析布局文件又是“Hello World!”

    引言 上篇可以说是一个分水岭,它标志着我们从Android应用程序理论进入实践,我们拿起手术刀对默认的"Hello World!"程序进行了3个手术,我们清楚了"Hell ...

  6. android中oncreate方法,android开发之onCreate( )方法详解

    这里我们只关注一句话:This is where you should do all of your normal static set up.其中我们只关注normal static, normal ...

  7. Android开发之MVVM模式实践(六),2021字节跳动春招技术面试题

    以上是我们创建协程的实现方式,我们可以通过指定Dispatchers来决定协程到底在什么线程中工作,而其实Kotlin的协程核心库中也为我们提供封装好了的scope,例如MainScope,源码如下: ...

  8. android开发之onCreate( )方法详解

    android开发之onCreate( )方法详解 onCreate( )方法是android应用程序中最常见的方法之一,那么,我们在使用onCreate()方法的时候应该注意哪些问题呢? 先看看Go ...

  9. Android开发之2048安卓版

    之前是在eclipse上写的,后面换成了android sudio. 2048游戏的UI整体可以采用线性布局,即LinearLayout,其中嵌套一个线性布局和一个GridLayout,内嵌的线性布局 ...

最新文章

  1. 适用于Mac上的SQL Server
  2. Ural_1353. Milliard Vasya's Function(DP)
  3. SUSE Linux 维护笔记一
  4. h5 php js实验总结,H5学习_番外篇_PHP数据库操作
  5. 用python设计简易计算器代码_Python简易计算器制作方法代码详解
  6. 使用Windows中的字体生成点阵字库
  7. 修改Win10 C盘用户文件夹名称
  8. 产品可靠性研究不可不知的知识之MTTF
  9. 二层交换机、三层交换机及四层交换机的区别
  10. java教程——电商秒杀系统介绍
  11. RT-Thread学习笔记【ADC与DAC设备】
  12. Nginx 配置问题 server directive is not allowed here in /etc/nginx/nginx.conf:69
  13. 数据时代大数据管理,主要有哪些策略?
  14. 2020 JAVA eclipse 中文汉化包 安装教程--傻瓜式操作
  15. 白光干涉仪(光学3D表面轮廓仪)与台阶仪的区别
  16. 评高工计算机专业是啥系列,评高级工程师需要职称计算机吗?高级工程师计算机模块是什么意思?...
  17. pandas学习笔记之DateFrame
  18. 用Python读红楼梦之——一、词云绘制
  19. Go 使用IP纯真库获取IP对应的国家、省、市
  20. 和讯博客广告位置改变

热门文章

  1. 到底什么是5G CPE?
  2. SQL如何合并两个表
  3. 渗透测试-安全岗位面试题总结(含答案)
  4. 关于用例 前置条件等的理解(1)
  5. 文件下载时设置文件名以及中文被转换成下划线的解决办法
  6. java多线程并发之旅-34-性能与可伸缩性
  7. 私有云计算机械硬盘还是固态硬盘,硬盘寿命有多久?机械硬盘比固态硬盘更长寿吗?...
  8. 每周论文精读05-A2J:AnchortoJointRegressionNetwork for 3D ArticulatedPoseEstimation from a SingleDepthImage
  9. 打工人最怕听到的词是团建
  10. How to Add a Dotted Underline Beneath HTML Text