Android开发之UI线程和非UI线程

这里又是老生畅谈的话了,前边已经有多篇文章针对线程进行探究解释,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 called
             if (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讲义》——李刚
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值