线程优化是Android性能优化中一个非常重要的部分,作为进程中逻辑处理调度的基本单位,如果使用不当,非常容易造成系统资源的浪费,从而导致应用性能出问题。在日常开发中,最常出现的问题主要有两个方面,一是线程启动过多造成CPU和内存资源浪费,并且应用耗电过大;二是线程作为GCRoots,如果使用不当,容易直接或间接造成Activity无法销毁,导致内存泄漏。

本篇博文主要以这两点为基础,结合日常开发中遇到的场景,整理和分析下相应的优化策略。


一、无需使用线程的场景

虽然程序无时无刻不是运行在线程中,不过为了防止阻塞主线程(比如Android的UI线程),获得更加流畅的用户体验,像网络请求、数据库操作、IO操作、密集计算等,往往都需要开启一个异步工作线程,这是众所周知的道理。不过,往往因此导致了线程的滥用,下面我们来看一些无需使用线程却误用的场景。

1、延时任务

由于某些操作或者逻辑需要延时处理,比如延时2s设置一个按钮消失。大家都知道不能阻塞主线程,否则会导致页面无响应,然后就习惯性地开个线程。

new Thread() {public void run() {try {Thread.sleep(3000);} catch (Exception e) {}runOnUiThread(new Runnable() {@Overridepublic void run() {...}});}
}.start();

当然,从功能运行和代码逻辑的角度上来看,这并没有问题。但是,如果这种操作过于频繁,或者sleep过长,很容易导致同时开启的线程过多。而且runOnUiThread里面基本都是视图相关的操作,而线程作为GCRoots之一,存活状态下关联的对象都是无法被回收的。所以像View、Activity等一系列对象销毁后内存都无法被回收导致内存泄漏,除非等到线程销毁后的下一次GC才能被回收掉。

那么,除了开线程sleep之外,还有什么方式执行延时任务呢?

推荐做法:利用主线程Handler消息机制的postDelay。如果延时前和延时后的执行环境都是在主线程中,那么利用利用Handler消息机制的delay功能,能够非常方便作延时处理。以上的代码,可以修改成:

mHandler.postDelayed(new Runnable() {@Overridepublic void run() {...}
}, 3000);

而如果你正好有一个任意的View可以使用,连mHandler都不需要创建了:

mView.postDelayed(new Runnable() {@Overridepublic void run() {...}
}, 3000);

这两种方式原理其实是一模一样的,都是利用主线程的消息机制。注意,这两种方式的Runnable执行都是在主线程中的

不过,需要小心的是,这是解决了乱开线程的问题,而没有解决内存泄漏的问题。如果仅仅是这样处理,内存泄漏的问题还在,具体请查阅Handler导致的内存泄漏。所以,我们还需要做相应延时任务移除的处理:

mView.removeCallbacks(Runnable action)
mHandler.removeCallbacks(Runnable action)

这样一来,既解决了乱开线程的问题,也解决了内存泄漏的问题(当然,严格地讲如果Runnable刚好正在执行,依然会有短暂的内存泄漏)。

2、计时任务

谈到计时,最容易想到的就是Timer类了,使用其schedule方法可以非常简单地发布一个计时任务。其内部原理会启动一个TimerImpl线程,通过wait与noitfy机制实现了计时功能。不过,如果要即时更新UI,比如按秒显示一个倒计时数据,需要通过主线程的Handler发送一个Message来处理,大概代码如下:

Handler handler = new Handler() {@Overridepublic void handleMessage(Message msg) {... // handle timer callback};
};
Timer timer = new Timer();
TimerTask task = new TimerTask() {@Overridepublic void run() {handler.sendEmptyMessage(...);}
};timer.schedule(task, 0, 1000);

如果需要在TimerTask中执行耗时操作,比如密集型计算,IO操作等,使用Timer确实是无比合适的。但是如果仅仅是为了计时更新UI,可以使用Android官方提供的CountDownTimer类,其原理同样是利用Handler消息机制的Delay功能。

推荐做法:

new CountDownTimer(30000, 1000) {public void onTick(long millisUntilFinished) {mTextField.setText("seconds remaining: " + millisUntilFinished / 1000); }public void onFinish() {mTextField.setText("done!");}}.start();

CountDownTimer虽然简单实用,但是自身还是有缺陷的,观察仔细的童鞋会发现在最后一次onTick到倒计时结束的时候,最后一次计数会显得非常地长,所以这里建议手动矫正最后一次计时。

除了使用CountDownTimer的方式外,还可以使用消息机制的递归调用,比如:

Handler handler = new Handler() {@Overridepublic void handleMessage(Message msg) {// handle timer callback...sendEmptyMessageDelayed(MSG, 1000);};
};
handler.sendEmptyMessageDelayed(MSG, 1000);

很明显,以上两种方式API比使用Timer开线程来说,除了性能更好,代码也更加简洁。但是无论是使用哪一种,切记不能忘记在destroy的时候调用cancel方法,不然内存又会出现泄漏了。


二、AsyncTask异步线程的注意点

AsyncTask是Android中老生常谈的一个线程调用工具,为了不落窠臼,本文就不去讲解AsyncTask的具体用法,而是针对性地研究下使用AsyncTask容易忽略的几个点。

首先,关于其cancel(boolean mayInterruptIfRunning)方法,我们通常使用这个方法来取消当前Task任务以希望能够释放Activity等。如果mayInterruptIfRunning为true,会调用线程的interrupt()方法。而interrupt()只是改变中断状态,无法中断正在运行的线程。也就是说即使调用的cancel(true),当前Task任务也会执行完,只是不再调用onPostExecute而是调用onCancelled方法。

一般情况下内部类或者匿名AsyncTask回持有Activity或者View的引用,这容易造成内存泄漏,所以建议在使用AsyncTask的时候尽量使用静态内部类。

其次,AsyncTask具有完整一套生命周期PENDING->RUNNING->FINISHED,如果已经执行过了,便无法再次执行任务。

public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,Params... params) {if (mStatus != Status.PENDING) {switch (mStatus) {case RUNNING:throw new IllegalStateException("Cannot execute task:"+ " the task is already running.");case FINISHED:throw new IllegalStateException("Cannot execute task:"+ " the task has already been executed "+ "(a task can be executed only once)");}}...
}

最重要的,AsyncTask的默认execute实现是同时只存在一个后台线程,如果同时调用多个AsyncTask,任务会加入到队列中,等待前一个执行完才执行下一个。有趣的是,Android API 4之前就是这种单独线程队列执行,之后改成了多线并发执行,而在Android API 11又改回了单独线程队列执行,主要还是基于性能考虑。不过,如果希望使用多线程并发,可以使用executeOnExecutor方法以及内部提供的THREAD_POOL_EXECUTOR线程池执行并发任务。

AsyncTask内部提供的THREAD_POOL_EXECUTOR每个版本的代码都略有不用,有时候我们可能需要针对各版本做兼容处理,下面稍微整理了下(4.0及以后)关键的改动点:

Android API 15-18:

  • 线程池核心线程数:5
  • 线程池最大线程数:128
  • 线程池缓存队列大小:10
  • 线程池维护线程空闲时间:1

Android API 19-23:

  • 线程池核心线程数:CPU_COUNT + 1
  • 线程池最大线程数:CPU_COUNT * 2 + 1
  • 线程池缓存队列大小:128
  • 线程池维护线程空闲时间:1

Android API 24-25:

  • 线程池核心线程数:Math.max(2, Math.min(CPU_COUNT - 1, 4))
  • 线程池最大线程数:CPU_COUNT * 2 + 1
  • 线程池缓存队列大小:128
  • 线程池维护线程空闲时间:30
  • 核心线程如果超出空闲时间也会被回收

可以看出,在API 19及之后,线程池最大线程数量由128调整为CPU核数x2+1,主要是防止同时开启的线程数量膨胀影响性能,也就是意味着AsyncTask的THREAD_POOL_EXECUTOR不适合用来执行高并发的任务,而是更加适合用来执行CPU密集型运算。

另外,如果只是希望执行一些异步操作,而不需要回调到主线程,可以使用AsyncTask.execute(Runnable runnable) 静态方法,非常便捷。


三、HandlerThread的使用场景

以笔者几年的Android项目经验而言,实际里HandlerThread的使用场景并不多,而使用的地方是否合理也都是值得商榷的。

从一定角度上来说,HandlerThread是对AsyncTask的一种补充。

AsyncTask的使用场景是: 主线程->异步线程->主线程,而HandlerThread的使用场景则是:主线程->异步线程->主线程->异步线程。简而言之,AsyncTask表示一次异步任务,执行完成就结束了。而HandlerThread表示多次异步任务,可以由主线程多次启动。HandlerThread继承于Thread类,只需要创建和启动一个线程,就能完成多个异步耗时任务,相比于多个AsyncTask多个线程而言,性能的提高是很大的。

总结一下,HandlerThread适用于持续性异步任务或者不连续多次异步任务。如果非持续性或者非多次异步操作,直接使用AsyncTask就可以,使用HandlerThread就纯属杀鸡用牛刀了。

另外,HandlerThread在使用结束后,需要中断looper销毁线程:

@Override
protected void onDestroy(){super.onDestroy();mHandlerThread.quit();
}

四、线程池的设计

涉及到多线程的场景,我们会很容易想到使用线程池。在Android项目管理中,我们往往会遇到很多new Thread这种随意创建线程的不合理代码,非常容易出现性能问题,合理的解决方案就是设计一套公用的线程调度机制来取代直接创建线程的方式。然而,很多时候一个公用的线程池调度往往很难处理项目中各种复杂的行为,这时候针对不同行为模式使用不同的线程调度策略就很有必要了。

1、IO行为模式

常见的IO操作包括文件操作、网络操作、数据库操作等,这一类操作都有一个特性,就是堵塞时间长且CPU利用率低,所以最好设计成无上限且重复空闲线程的线程池。比如说:

Executors.newCachedThreadPool()

2、密集计算行为模式

密集型计算包括图片处理、大数据计算、时间复杂度高的算法等行为,这一类行为对CPU的利用率较高,所以我们可以充分利用多核CPU的特性来提升性能,固定大小为CPU核数的线程池就比较适用了。比如说:

Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()).

本博客不定期持续更新,欢迎关注和交流:

http://blog.csdn.net/megatronkings

Android应用性能优化系列逻辑篇——线程相关性能优化相关推荐

  1. Android性能优化系列总篇

    目前性能优化专题已完成以下部分: 性能优化总纲--性能问题及性能调优方式 性能优化第四篇--移动网络优化 性能优化第三篇--Java(Android)代码优化 性能优化第二篇--布局优化 性能优化第一 ...

  2. Android App 性能优化系列结语篇

    Android App 性能优化系列结语篇 原文出处:http://gold.xitu.io/post/581f4ad667f3560058a33057 关于Android App的优化, 从第一篇的 ...

  3. 性能优化系列第一篇——数据库性能优化

    本文章转载的Trinea大神的文章,文章原地址 http://www.trinea.cn/android/database-performance/ 性能优化之数据库优化 本文为性能优化的第一篇--数 ...

  4. Android应用性能优化系列视图篇——隐藏在资源图片中的内存杀手

    图片加载性能优化永远是Android领域中一个无法绕过的话题,经过数年的发展,涌现了很多成熟的图片加载开源库,比如Fresco.Picasso.UIL等等,使得图片加载不再是一个头疼的问题,并且大幅降 ...

  5. Android编译优化系列-kapt篇

    一.背景 本文是编译优化系列文章之 kapt 优化篇,后续还会有 build cache, kotlin, dex 优化等文章,敬请期待.本文由Client Infra->Build Infra ...

  6. 性能优化系列(三)内存性能优化

    文章首发「Android波斯湾」公众号,更新地址:https://github.com/jeanboydev/Android-ReadTheFuckingSourceCode Android 为每个应 ...

  7. 【MySQL性能优化系列】百万数据limit分页优化

    背景 众所周知,在使用limit分页过程中,随着前端传过来的PageSize越来越大,查询速度会越来越慢.那有什么优化的办法呢? 本文将通过百万数据表进行演示和优化, 欲知详情,请看下文分解. lim ...

  8. JVM篇·线程安全与优化

    线程安全与优化 本文为<深入理解Java虚拟机_JVM高级特性与最佳实践·周志明>学习笔记 文章目录 线程安全与优化 线程安全 共享数据分类 1. 不可变 2. 绝对线程安全 3. 相对线 ...

  9. 百度App网络深度优化系列(一):DNS优化

    一.前言 网络优化是客户端几大技术方向中公认的一个深度领域,所以百度App给大家带来网络深度优化系列文章,其中包含系列<一>DNS优化,系列<二>连接优化,系列<三> ...

最新文章

  1. 平面设计中的网格系统pdf_全面掌握版式设计中的网格系统
  2. 如何促使团队紧密协作
  3. 初探Git git基本用法
  4. linux按键驱动中的结构体,linux 驱动之input子系统(gpio-keys)实现
  5. n个人选k个c语言_leetcode之第k个缺失的正整数
  6. [Java核心技术(卷I)] - Java中的参数能做什么和不能做什么
  7. 为什么要将html页面和样式表分离,0031 如何使用css文件对网页内容和样式进行分离...
  8. singer页左侧滚动的时候右侧跟随高亮显示
  9. BZOJ 4285 使者 (CDQ分治+dfs序)
  10. 量子艺术、魔法成像、水生政治、性方程式……这些AI“衍生”科目都是啥?...
  11. python_统计数组中指定范围的数据占的比例
  12. CAD填充技巧:填充图案
  13. NCA(Neighborhood Components Analysis)
  14. CentOS7下安装和开启远程连接reids
  15. 操作系统中的虚拟内存详解
  16. Armadillo | 复数小记
  17. 推荐 | 一些奇特的人工智能App
  18. Docker pull 失败,更换国内源daocloud
  19. MD5 SHA1 加密
  20. 马云宣布传承计划,回归教育

热门文章

  1. 明确市场定位让软文营销从针对性出发
  2. 献给阿尔吉侬的花束(广搜)
  3. 建立一个程序员相亲圈子可好
  4. Windows Jpype安装
  5. 脾和胃各自的功能是什么?
  6. 有道云笔记迁移到自建服务器Joplin
  7. tabindex属性_tabindex(HTML属性)
  8. 给定三角形ABC和一点P(x, y),判断P是否在三角形内
  9. redis雪崩、穿透、击穿
  10. LInux上搭建GitLab详细步骤