Android性能优化(十一)之正确的异步姿势

1、 前言

在前面的性能优化系列文章中,我曾多次说过:异步不是灵丹妙药,不正确的异步方式不仅不能较好的完成异步任务,反而会加剧卡顿。Android开发中我们使用异步来进行耗时操作,异步离不开一个词:线程。那么问题来了:

  1. Android中线程调度是如何实现的?
  2. 正确的异步姿势是什么呢?
  3. 线程池一定会提升效率吗?

那今天这篇文章我们就来聊聊Android中正确的异步姿势。

2、 Android线程调度

Android的线程调度由两个主要因素来决定如何在整个系统调度线程:nice values和cgroups。

2.1 Nice values

Linux中使用nice value来设定一个进程的优先级,系统任务调度器根据这个值来安排调度。而在Android中nice values被用在线程优先级上,高nice values(低优先级)的线程运行机会少于低nice values(高优先级)的线程。最重要的两个线程优先级是default和background。线程的优先级应该根据线程的工作量谨慎选择,简单来说,线程优先级应该和该线程期望完成的工作量相反。线程做的工作越多,它的优先级应该越小,以便它不会造成系统资源紧张。所以,UI线程(Activity的主线程)通常是default优先级,然而后台线程(AsyncTask的线程)通常是background优先级。

Nice values在理论上很重要,因为他们减少了后台工作线程中断UI的可能性。 但在实践中,只有Nice values并不足够。例如,存在20个后台线程和一个单独的执行UI的前台线程。虽然他们每个的优先级很低,但是合起来这个20个后台线程将影响前台线程的性能,结果就是损害了用户体验。因为在任何时刻几个应用程序可能已经有等待运行的后台线程,Android OS必须以某种方式处理这些问题。

Android中线程优先级

2.2 Cgroups

为了处理这个问题,Android系统使用Linux cgroups(Linux内核的一个功能,用来限制,控制与分离一个进程组群的资源)强制执行更严格的foreground、background调度策略。background优先级的线程被隐式的移动到了background cgroup,当其它组中的线程处于工作状态,它们被限制只有很小的几率(5%到10%)利用CPU。这种分离允许后台线程执行一些任务,但不会对用户可见的前台线程产生较大的影响。

除了自动将低优先级线程分配给background cgroup,Android也将当前不在前台运行的应用程序的线程移动到background cgroup中。将应用程序线程自动分组保证了当前前台线程总是优先的,无论有多少应用程序在后台运行。

总结:

  • 高Nice Value对应较低的线程优先级,意味着更少的执行机会,让步于高优先级的UI线程;
  • Cgroups可以更好的凸显某类线程的优先级,Android中有两类group尤其重要:一类是default group,对应UI线程。另一类是background group,对应工作线程;
  • 进程的属性变化也会影响到线程的调度,当一个App进入后台,该App所属的整个线程都将进入background group,以确保处于foreground、用户可见的进程能获取到尽可能多的CPU资源。

3、 正确的异步姿势

3.1 Thread

new Thread(){
    @Override
    public void run() {
        super.run();
        // NetWork or DataBase Operation
    }
}.start();

这是最简单的创建异步线程的姿势了,但是每当项目中出现这类代码,我都忍不了要把它改掉的冲动。

缺点:

  • 创建及销毁线程消耗性能较大;
  • 缺乏统一的管理;
  • 优先级与UI线程一致,抢占资源处于同一起跑线;
  • 匿名内部类默认持有外部类的引用,有内存泄漏的风险;
  • 需要自己处理线程切换。

备注:此种姿势最好不要使用,特定场景下(例如App启动阶段为避免在主线程创建线程池的资源消耗)使用的话务必加上优先级的设置。

Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

3.2 AysncTask

AsyncTask是Android1.5提供了工具类,它使创建异步任务变得更加简单,同时屏蔽了线程切换。

下面代码是官方文档的示例代码,在doInBackground()方法中处理耗时操作,处理的进度由onProgressUpdate()方法进行回调,耗时操作处理完成之后会调用onPostExecute()方法,在UI线程中执行。

 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");
     }
 }

优点:

  • 创建异步任务变得更加简单,同时屏蔽了线程切换;
  • AsyncTask.java中我们可以看到,异步线程的优先级已经被默认设置成了:THREAD_PRIORITY_BACKGROUND,不会与UI线程抢占资源;

缺点:

  • Api实现版本不一致问题:在Android1.5时AsyncTask的执行是串行的,在Android1.5——3.0之间AsyncTask是并行的,而到了Android3.0之后AsyncTask的执行又回归到了串行。当然目前我们兼容的最低版本一般都会是最低4.0,那么就不需要对其进行过多的自定义适配,但是一定要注意AsyncTask默认是串行的,用于多线程场景下的话需要调用其重载方法executeOnExecutor()传入自定义的线程池,并且自己处理好同步问题;
  • 匿名内部类默认持有外部类的引用,有内存泄漏的风险。

备注:对于AsyncTask正确的使用姿势,就是区分场景调用不同的执行方法;并且避免出现内存泄漏的问题。

3.3 HandlerThread

通过HandlerThread可以创建一个带有looper的线程,引入了Handler、Looper、MessageQueue等概念,可以实现对工作线程的调度。

以下是HandlerThread的使用示例:

HandlerThread handlerThread = new HandlerThread("DataBase Opeartion", Process.THREAD_PRIORITY_BACKGROUND);
handlerThread.start();

Handler handler = new Handler(handlerThread.getLooper()){
    @Override
    public void handleMessage(Message msg) {
        // Do DataBase Opeartion
    }
};

优点:

  • 串行执行,没有并发带来的问题;
  • 不退出的前提下一直存在,避免线程相关的对象频繁重建和销毁造成的资源消耗。

缺点:

  • 串行执行(不同的视角优点也变缺点),并发场景下无能为力;
  • 不指定优先级的情景下默认优先级为THREAD_PRIORITY_DEFAULT,与UI线程同级别。

备注:HandlerThread的正确使用姿势:串行场景,并在构造方法中明确指定优先级。

3.4 IntentService

根据官方文档的描述:IntentService是继承于Service并处理异步请求的一个类,在IntentService内有一个工作线程来处理耗时操作,启动IntentService的方式和启动传统Service一样,同时,当任务执行完后,IntentService会自动停止,而不需要我们去手动控制。另外,可以启动IntentService多次,而每一个耗时操作会以工作队列的方式在IntentService的onHandleIntent回调方法中执行,并且,每次只会执行一个耗时操作,依次执行。

实际上IntentService是Service与HandlerThread的组合,内部的工作线程以及调度机制都依赖于HandlerThread。

    @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);
    }
    
    @Override
    public void onDestroy() {
        mServiceLooper.quit();
    }

优势:

  • 同HandlerThread的优势;
  • 开启服务,进程优先级会提升;
  • 无需手动关闭,执行完之后自动结束。

备注:
有人可能对于Service的理解会有误区,Service并不是执行耗时操作的乐园,在《Android 性能优化(七)之你真的理解 ANR 吗?》中分析过,Service中执行耗时操作会导致ANR。

3.5 ThreadPoolExecutor

线程池:基本思想是一种对象池的思想,开辟一块内存空间,里面存放了众多(存活状态)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。

优势:

  • 线程的创建和销毁由线程池维护,一个线程在完成任务后并不会立即销毁,而是由后续的任务复用这个线程,从而减少线程的创建和销毁,节约系统的开销;
  • 线程池旨在线程的复用,这就可以节约我们用以往的方式创建线程和销毁所消耗的时间,减少线程频繁调度的开销,从而节约系统资源,提高系统吞吐量;
  • 在执行大量异步任务时提高了性能;
  • Java内置的一套ExecutorService线程池相关的api,可以更方便的控制线程的最大并发数、线程的定时任务、单线程的顺序执行等。

备注:回到我们上面提的第三个问题:线程池一定会提升效率吗?

  • 使用线程池需要特别注意同时并发线程数量的控制。因为CPU只能同时执行固定数量的线程数,一旦同时并发的线程数量超过CPU能够同时执行的阈值,CPU就需要花费精力来判断到底哪些线程的优先级比较高,在不同的线程之间进行调度切换。一旦同时并发的线程数量达到一定的量级,CPU在不同线程之间进行调度的时间就可能过长,反而导致性能严重下降;
  • 每开一个新的线程,都会耗费至少64K以上的内存。线程池中存在了过多的并发数量不仅会影响CPU的调度时间而且会减少可用内存;
  • 线程的优先级具有继承性,在某线程中创建的线程会继承此线程的优先级。那么我们在UI线程中创建了线程池,其中的线程优先级是和UI线程优先级一样的;所以仍然可能出现20个同样优先级的线程平等的和UI线程抢占资源。

对于线程池中线程数量的限制,可以参考AsyncTask中的配置,基于7.0源码,不同版本的实现可能有细微差别;

    // We want at least 2 threads and at most 4 threads in the core pool,
    // preferring to have 1 less than the CPU count to avoid saturating
    // the CPU with background work 核心池数量被限定在2到4之间。
    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final int KEEP_ALIVE_SECONDS = 30;
    
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
                sPoolWorkQueue, sThreadFactory);

4、 总结

  • Thread、AsyncTask适合处理单个任务的场景;
  • HandlerThread适合串行处理多任务的场景;
  • IntentService适合处理与UI无关的多任务场景;
  • 当需要并行的处理多任务之时,ThreadPoolExecutor是更好的选择,当然也可以使用AsyncTask传入自定义的线程池;
  • 注意线程优先级的设置;
  • 特别注意对不同场景下异步方式的选择。

参考:
《Java线程池》
《Thread Scheduling in Android》
《java线程池大小为何会大多被设置成CPU核心数+1?》
《Android性能优化典范——The Importance of Thread Priority 》

欢迎关注微信公众号:定期分享Java、Android干货!

欢迎关注
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,113评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,644评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,340评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,449评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,445评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,166评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,442评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,105评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,601评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,066评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,161评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,792评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,351评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,352评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,584评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,618评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,916评论 2 344

推荐阅读更多精彩内容

  • 线程是程序员进阶的一道重要门槛。对于移动开发者来说,“将耗时的任务放到子线程去执行,以保证UI线程的流畅性”是线程...
    vb12阅读 1,437评论 0 2
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,524评论 25 707
  • Android中的线程 线程,在Android中是非常重要的,主线程处理UI界面,子线程处理耗时操作。如果在主线程...
    shenhuniurou阅读 748评论 0 3
  • 系列文章Android面试攻略(1)——Android基础Android面试攻略(2)——异步消息处理机制Andr...
    黎清海阅读 1,337评论 0 10
  • 一看到年龄和妈妈差不多大,生活状态比妈妈好的阿姨,我就想到让自己变的优秀点,也让妈妈以我为豪, 一是学习,一是改变...
    张严阅读 251评论 0 0