文章目录

  • 线程调度
    • 线程调度的原理
    • 线程调度模型
    • Android 的线程调度
    • 线程调度小结
  • Android 异步方式汇总
    • Thread
    • HandlerThread
    • IntentService
    • AsyncTask
    • 线程池
    • RxJava
  • 线程使用准则
  • CPU 占用率收敛的方式
    • 线程池
    • OkHttp
    • Glide

因为 CPU 的收敛涉及到方方面面,线程、UI渲染等等,该篇文章主要以线程优化的角度分析如何降低 CPU 占用率,合理使用 CPU。

在 Android性能优化系列:启动优化 谈到,为了提高启动速度(一般指冷启动),可以使用线程池、异步任务启动器等方式提高 CPU 的使用率,让影响启动耗时的耗时任务更多的放在子线程执行,不让耗时任务影响主线程,以达到提高启动速度的目的。

需要注意的是,即使有了线程池或异步任务启动器,并不代表就可以无节制的创建线程,耗时任务只不过是从主线程迁移到了子线程执行而已,线程越多,可能同一时间就有很多线程同时抢占 CPU 资源,CPU 占用率也随之提高。在遇到预装或对 app 性能有要求的平台,线程过多带来的 CPU 占用率过高将会导致你的 app 不能如常上线。

所以就需要在一定程度限制线程数量,合理的利用线程,减少 CPU 占用率。那有哪些方式可以减少线程使用呢?在 Android 线程又是怎么工作的?

线程调度

线程调度的原理

在任意时刻,CPU 只能执行一条机器指令,每个线程只有获得了 CPU 的使用权之后才能执行指令,也就是说 在任意时刻,只有一个线程占用 CPU,处于运行状态。而我们平常所说的 多线程并发运行,实际上说的是多个线程轮流获取 CPU 的使用权,然后分别执行各自的任务。其实在可运行池当中有多个处于就绪状态的线程在等待 CPU,而 JVM 负责线程调度,按照特定机制为多个线程分配 CPU 使用权

上面的描述提到了三个主要信息:

  • 在任意时刻,只有一个线程占用 CPU,处于运行状态

  • 多线程并发运行,实际上说的是多个线程轮流获取 CPU 的使用权

  • JVM 负责线程调度,按照特定机制为多个线程分配 CPU 使用权

线程调度模型

线程调度模型可以分为两类,分别是 分时调度模型抢占式调度模型

  • 分时调度模型:让所有线程轮流获取 CPU 的使用权,而且均分每个线程占用 CPU 的时间片,这种方式非常公平

  • 抢占式调度模型:JVM 使用的是抢占式调度模型,让优先级高的线程优先获取到 CPU 的使用权,如果在可运行池当中的线程优先级都一样,那就随机选取一个

所以在 java 中我们按顺序开启线程,其实并不能保证线程就按线程的开启顺序执行,要实现按顺序执行线程的方式,最简单的做法就是修改线程的优先级

Android 的线程调度

Android 的线程调度从两个因素决定,一个是 nice 值(即线程优先级),一个是 cgroup(即线程调度策略)。

对于 nice 值来说,它首先是在 Process 中定义的,值越小,进程优先级越高,默认值是 THREAD_PRIORITY_DEFAULT = 0,主线程的优先级也是这个值。修改 nice 值只需要在对应的线程下设置即可:

public class MyRunnable implements Runnable {@Overridepublic void run() {Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT)}
}// 附上 setThreadPriority() 文档说明
/*** Set the priority of the calling thread, based on Linux priorities.  See* {@link #setThreadPriority(int, int)} for more information.* * @param priority A Linux priority level, from -20 for highest scheduling* priority to 19 for lowest scheduling priority.* * @throws IllegalArgumentException Throws IllegalArgumentException if* <var>tid</var> does not exist.* @throws SecurityException Throws SecurityException if your process does* not have permission to modify the given thread, or to use the given* priority.* * @see #setThreadPriority(int, int)*/
public static final native void setThreadPriority(int priority)throws IllegalArgumentException, SecurityException;

nice 值它还有其他的优先级可选:

public class Process {/*** Standard priority of application threads.* Use with {@link #setThreadPriority(int)} and* {@link #setThreadPriority(int, int)}, <b>not</b> with the normal* {@link java.lang.Thread} class.*/public static final int THREAD_PRIORITY_DEFAULT = 0;/*** Lowest available thread priority.  Only for those who really, really* don't want to run if anything else is happening.* Use with {@link #setThreadPriority(int)} and* {@link #setThreadPriority(int, int)}, <b>not</b> with the normal* {@link java.lang.Thread} class.*/public static final int THREAD_PRIORITY_LOWEST = 19;   /*** Standard priority background threads.  This gives your thread a slightly* lower than normal priority, so that it will have less chance of impacting* the responsiveness of the user interface.* Use with {@link #setThreadPriority(int)} and* {@link #setThreadPriority(int, int)}, <b>not</b> with the normal* {@link java.lang.Thread} class.*/public static final int THREAD_PRIORITY_BACKGROUND = 10;    /*** Standard priority of threads that are currently running a user interface* that the user is interacting with.  Applications can not normally* change to this priority; the system will automatically adjust your* application threads as the user moves through the UI.* Use with {@link #setThreadPriority(int)} and* {@link #setThreadPriority(int, int)}, <b>not</b> with the normal* {@link java.lang.Thread} class.*/public static final int THREAD_PRIORITY_FOREGROUND = -2;/*** Standard priority of system display threads, involved in updating* the user interface.  Applications can not* normally change to this priority.* Use with {@link #setThreadPriority(int)} and* {@link #setThreadPriority(int, int)}, <b>not</b> with the normal* {@link java.lang.Thread} class.*/public static final int THREAD_PRIORITY_DISPLAY = -4;    /*** Standard priority of the most important display threads, for compositing* the screen and retrieving input events.  Applications can not normally* change to this priority.* Use with {@link #setThreadPriority(int)} and* {@link #setThreadPriority(int, int)}, <b>not</b> with the normal* {@link java.lang.Thread} class.*/public static final int THREAD_PRIORITY_URGENT_DISPLAY = -8;/*** Standard priority of video threads.  Applications can not normally* change to this priority.* Use with {@link #setThreadPriority(int)} and* {@link #setThreadPriority(int, int)}, <b>not</b> with the normal* {@link java.lang.Thread} class.*/public static final int THREAD_PRIORITY_VIDEO = -10;/*** Standard priority of audio threads.  Applications can not normally* change to this priority.* Use with {@link #setThreadPriority(int)} and* {@link #setThreadPriority(int, int)}, <b>not</b> with the normal* {@link java.lang.Thread} class.*/public static final int THREAD_PRIORITY_AUDIO = -16;/*** Standard priority of the most important audio threads.* Applications can not normally change to this priority.* Use with {@link #setThreadPriority(int)} and* {@link #setThreadPriority(int, int)}, <b>not</b> with the normal* {@link java.lang.Thread} class.*/public static final int THREAD_PRIORITY_URGENT_AUDIO = -19;/*** Minimum increment to make a priority more favorable.*/public static final int THREAD_PRIORITY_MORE_FAVORABLE = -1;/*** Minimum increment to make a priority less favorable.*/public static final int THREAD_PRIORITY_LESS_FAVORABLE = +1;
}

在实践过程当中,如果只有 nice 值是不足够的。比如有一个 app 它有1个前台线程,而且它还有10个后台线程,虽然后台线程的优先级比较低,但是数量比较多,这10个后台线程对 CPU 的消耗量是可以影响到前台线程的性能的。所以 Android 需要一种机制来处理这种情况,也就是 cgroup。

Android 借鉴了 Linux 的 cgroup 来执行 更严格的前台和后台调度策略,后台优先级的线程会被隐式的移动到后台 group,而其他 group 的线程如果处于工作状态,那么后台这些线程它们将会被限制,只有很小的几率能够利用 CPU。这种分离的调度策略既允许了后台线程来执行一些任务,同时又不会对用户可见的前台线程造成很大的影响,让前台线程有更多的 CPU

或许你会有疑问:哪些线程会被移到后台 group?

  • 第一种就是那些 手动设置了优先级比较低的线程

  • 第二种就是 不在前台运行的那些应用程序的线程

对于我们平常开发中只能设置 nice 值,也就是设置线程的优先级,所以要根据不同的场景设置合适的线程优先级

线程调度小结

  • 线程过多会导致 CPU 频繁切换,降低线程运行效率。在前面讲解启动优化的时候有强调要充足的利用线程比如异步启动任务,但是线程也不能无限制的使用

  • 正确认识任务重要性决定哪种优先级。一般情况下线程工作量和优先级是成反比,比如线程的工作量越大,所做的工作没那么重要,那这个线程的优先级应该越低

  • 线程的优先级具有继承性。比如在 A 线程创建了 B 线程,在我们没有指定线程优先级的情况下,B 线程的优先级是和 A 一样的。所以我们在 UI 线程中创建线程,线程的优先级是和 UI 线程一样的,这就会导致 UI 线程抢占 CPU 时间片的概率会变少

Android 异步方式汇总

Thread

使用 Thread 创建线程是最简单、常见的异步方式,但在实际项目中,它也就只有这个优点了,并不推荐直接使用 Thread 创建线程,主要有以下几点原因:

  • 不易复用,频繁创建及销毁开销大

  • 复杂场景不易使用

HandlerThread

HandlerThread 是 Android 提供的一个自带消息循环的线程,它内部使用 串行的方式执行任务,比较 适合长时间运行,不断从队列中获取任务的场景

IntentService

IntentService 继承了 Android Service 组件,内部创建了 HandlerThread,相比 Service 是在主线程执行,IntentService 是 在子线程异步执行不占用主线程,而且 优先级比较高,不易被系统 kill

AsyncTask

AsyncTask 是 Android 提供的工具类,内部的实现是使用了线程池,它比较大的好处是无需自己处理线程切换,但需要注意 AsyncTask 不同版本执行方式不一致的问题。

线程池

java 提供了线程池,在实际项目中比较推荐使用线程池的方式实现异步任务,它主要有以下优点:

  • 易复用,减少线程频繁创建、销毁的时间

  • 功能强大:定时、任务队列、并发数控制等,java 提供了 Executors 工具类可以很方便的创建一个线程池,也可以自己定制线程池

RxJava

RxJava 由强大的 Scheduler 集合提供,内部实际也是使用的线程池,它封装的非常完善,可以根据任务类型的不同指定使用不同的线程池,比如 IO 密集型的任务可以指定 Schedulers.IO,CPU 密集型任务可以指定 Schedulers.Computation

Single.just(xxx).subscribeOn(Schedulers.IO) // 指定工作线程类型为 IO 密集型.observeOn(AndroidSchedulers.mainThread()) // 指定下游接收所在线程.subscribe();

线程使用准则

  • 严禁直接 new Thread() 直接创建线程

  • 统一线程池。提供基础线程池供各个业务线使用,避免各个业务线各自维护一套线程池,导致线程数过多

  • 根据任务类型选择合适的异步方式。比如优先级低且需要长时间执行选择 HandlerThread

  • 创建线程必须命名。在自己定制创建线程池的情况下,可以在运行时手动更改线程名字 Thread.currentThread().setName(),线程命名主要是方便定位线程归属和问题

  • 关键异步任务监控。因为异步不等于不耗时,只是将耗时任务从主线程迁移到子线程运行

  • 重视优先级设置。通过 Process.setThreadPriority() 设置线程优先级,根据需求场景合理设置线程优先级

CPU 占用率收敛的方式

线程池

在实际的项目开发中,创建线程的方式主要有两种:一种是 Runnable,另一种是 new Thread()。基本上不会直接使用 new Thread() 的方式频繁创建线程,因为线程的创建和销毁都是占用资源的,并且使用这种方式创建的线程不好复用。

线程池 可以解决上面提出的问题,并且 java 为了更方便的使用线程池,也提供了相应的工具类 Executors,根据不同的场景创建不同的线程池。

除了 Executors 提供的几种线程池,我们也可以自定义线程池,定制核心线程数量、最大线程数量、线程回收时间等等。

实际项目中定制的线程池,要怎样才能做到有效的线程收敛,合理的使用 CPU 呢?项目中线程池的使用主要有四个注意事项:

1、项目可以提供管理类提供给其他模块统一的线程池

2、限制核心线程和非核心线程数量:根据 CPU 核心数在不同设备合理创建线程数量

3、设置线程优先级

4、线程回收:线程回收包括核心线程的回收调用 threadPoolExecutor.allowCoreThreadTimeOut(true)

参考 AsyncTask 源码,定制创建线程池代码如下:

private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
// 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
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);
threadPoolExecutor.allowCoreThreadTimeOut(true); // 回收核心线程private static final BlockingQueue<Runnable> sPoolWorkQueue =new LinkedBlockingQueue<Runnable>(128);private static final ThreadFactory sThreadFactory = new ThreadFactory() {private final AtomicInteger mCount = new AtomicInteger(1);public Thread newThread(Runnable r) {Thread thread = new Thread(r, "CustomThread #" + mCount.getAndIncrement());// 设置线程优先级,示例代码仅作为演示,实际项目需根据使用场景设置线程优先级thread.setPriority(Thread.MAX_PRIORITY); return thread;}
};

OkHttp

相信大多数 Android 开发都有使用到 OkHttp,可能再结合 Retrofit 使用,在项目中非常方便就可以实现网络处理。网络请求都会放在子线程执行,同样内置了线程池处理,所以当某个界面或模块网络请求较多的情况下,不可避免的 CPU 占用率也会随之提高。

OkHttp 能处理的位置主要有两点:

1、限制网络请求数量

无论是同步还是异步请求,在执行 HTTP 网络请求前都会被放近队列中,因为实际项目比较多是使用异步请求,所以这里是以异步请求说明。

Dispatcher 维护着三个队列,异步请求会先被放进待执行的队列,然后才会被添加到可执行队列中,最终由线程池启动线程执行 HTTP 请求。具体原理参考文章 OkHttp 源码解析。

其中,Dispatcher 提供了配置异步网络请求的最大数量 maxRequests 属性,默认最大请求数量为64。当超过配置的最大请求数量时,会先执行所有在可执行队列的异步网络请求,然后再继续请求:

private boolean promoteAndExecute() {assert (!Thread.holdsLock(this));List<AsyncCall> executableCalls = new ArrayList<>();boolean isRunning;synchronized (this) {for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {AsyncCall asyncCall = i.next();// 队列中可执行异步网络请求的数量超过配置的最大请求数// 跳出循环先执行已被添加进可执行队列的异步网络请求if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.if (runningCallsForHost(asyncCall) >= maxRequestsPerHost) continue; // Host max capacity.i.remove();executableCalls.add(asyncCall);runningAsyncCalls.add(asyncCall);}isRunning = runningCallsCount() > 0;}for (int i = 0, size = executableCalls.size(); i < size; i++) {AsyncCall asyncCall = executableCalls.get(i);asyncCall.executeOn(executorService());}return isRunning;
}

maxRequests 的配置通过配置管理类 OkHttpClient 设置:

Dispatcher dispatcher = new Dispatcher();
dispatcher.setMaxRequests(32);
new OkHttpClient.Builder().dispatcher(dispatcher);

通过这种方式可以减少在同一时间过多的网络请求同时被执行,也就能一定程度上减少同一时间 CPU 过高的占用率。

2、OkHttpClient 单例共享

OkHttpClient 作为管理配置类,也提供了配置的默认值,比如 connectionPool 连接池、线程池,也就是说,如果多次的创建 OkHttpClient,将会导致连接池和线程池等不能重用,每一个 OkHttpClient 对象都创建了一个新的连接池和线程池,这对资源也是一种浪费。

OkHttp 的官方文档也有提及到,推荐将 OkHttpClient 设置为单例,方便有效的利用资源:


将 OkHttpClient 对象单例共享,有配置修改的地方使用 okHttpClient.newBuilder() 重用连接池和线程池这些资源,减少资源的浪费。

Glide

Glide 作为 Android 优秀的图片加载库,相信项目基本上也会有用到。图片从网络请求到最终加载出来,涉及到网络请求,就不可避免也会有线程处理,当列表快速滑动时,将会有多个线程异步网络请求,并且处理图片加载,CPU 占用率也会有一定幅度的提高。

Glide 降低 CPU 占用率的方式有两种:

1、限制 Glide 异步图片加载开启线程数量

@GlideModule
public class MyGlideModule extends AppGlideModule {@Overridepublic boolean isManifestParsingEnabled() {return false;}@Overridepublic void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {super.applyOptions(context, builder);builder.setSourceExecutor(GlideExecutor.newSourceExecutor(1, // 修改处理图片加载的线程数量"glide-load-source",GlideExecutor.UncaughtThrowableStrategy.DEFAULT));}
}

2、列表滑动暂停加载图片

图片列表滑动时都会异步启动网络请求获取图片,Glide 支持手动暂停图片请求和恢复图片请求,可以在列表滑动监听的位置,当列表滑动时暂停图片请求,列表停止滑动时恢复图片请求:

recyclerView.addOnScrollListener(new RecyclerView.onScrollListener() {@Overridepublic void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {super.onScrollStateChanged(recyclerView, newState);GlideRequests requests = Glide.with(context);if (newState != RecyclerView.SCROLL_STATE_IDLE) {if (!requests.isPaused()) {requests.pauseRequests(); // 列表滑动时暂停图片加载}} else {if (requests.isPaused()) {requests.resumeRequests(); // 列表停止滑动时恢复图片加载}}}
});

Android性能优化系列:CPU收敛优化(线程优化)相关推荐

  1. MySQL优化系列(二)--查找优化(1)(非索引设计)

    MySQL优化系列(二)--查找优化(1)(非索引设计) 接下来这篇是查询优化,用户80%的操作基本都在查询,我们有什么理由不去优化他呢??所以这篇博客将会讲解大量的查询优化(索引以及库表结构优化等高 ...

  2. SSE图像算法优化系列二十二:优化龚元浩博士的曲率滤波算法,达到约1000 MPixels/Sec的单次迭代速度...

      2015年龚博士的曲率滤波算法刚出来的时候,在图像处理界也曾引起不小的轰动,特别是其所说的算法的简洁性,以及算法的效果.执行效率等方面较其他算法均有一定的优势,我在该算法刚出来时也曾经有关注,不过 ...

  3. PLSQL_性能优化系列10_Oracle Array数据组优化

    2014-09-25 Created By BaoXinjian 一.摘要 集合是Oracle开发中经常遇到的情况,Oracle集合分为三种情况:索引表集合(index by table).嵌套表集合 ...

  4. 史上最详细的网站优化系列(一)mysql优化1

    一.mysql优化概述 方针: ① 存储层:数据表"存储引擎"选取.字段类型选取.逆范式(3范式) ② 设计层:索引.分区/分表.存储过程,sql语句的优化 ③ 架构层:分布式部署 ...

  5. 【优化系列】MIPS架构汇编优化资料

    DATE: 2021.8.3 文章目录 前言 1.MIPS介绍 2.MIPS汇编入门基础 3.MIPS汇编优化手册 3.1.中文手册 3.2.英文手册 3.3.Loongson 3A 优化 前言 在学 ...

  6. Android性能优化系列:启动优化

    文章目录 1 应用启动类型 1.1 冷启动 1.2 温启动 1.3 热启动 2 查看启动耗时 2.1 adb命令查看 2.2 Logcat Displayed查看启动耗时 2.3 手动记录启动耗时 2 ...

  7. Android 系统性能优化(30)---Android性能全面分析与优化方案研究

    Android 性能优化 1.结合以下四个部分讲解: 性能问题分类 性能优化原则和方法 借助性能优化工具分析解决问题 性能优化指标 2性能问题分类 1.渲染问题:过度绘制.布局冗杂 2.内存问题:内存 ...

  8. 几乎是史上最全最实用的Android性能全面分析与优化方案研究

    结合以下四个部分讲解: 性能问题分类 性能优化原则和方法 借助性能优化工具分析解决问题 性能优化指标 性能问题分类 1.渲染问题: 过度绘制.布局冗杂 2.内存问题: 内存浪费(内存管理).内存泄漏 ...

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

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

  10. linux tf2 中文,tf2+cnn+中文文本分类优化系列(2)

    1 前言 接着上次的tf2+cnn+中文文本分类优化系列(1),本次进行优化:使用多个卷积核进行特征抽取.之前是使用filter_size=2进行2-gram特征的识别,本次使用filter_size ...

最新文章

  1. 文本比较算法Ⅴ——回顾贴,对前面几篇文章的回顾与质疑
  2. Linux笔记:vim
  3. 【考前必知】软考考前注意事项
  4. dojo 加载自定义module的路径问题
  5. 一些常用的辅助代码 (网络收藏)
  6. 缺少ntstrsafe.lib kndis5mp.lib解决办法
  7. 如何为自己赢得更好的口碑
  8. Python科学计算函数库介绍
  9. 10本编程书籍推荐!带你从入门到精通
  10. 乾颐堂现任明教教主(2014年课程)TCPIP协议详解卷一 第一节课笔记
  11. 文件同步工具BT Sync介绍和使用说明
  12. matlab画分散点图,Matlab怎么绘制离散图? matlab画散点图的教程
  13. 浏览器 实现复制粘贴 上传图片, js实现粘贴图片预览功能
  14. 一站式WPF--依赖属性(DependencyProperty)一
  15. lol人物模型提取(八)
  16. 日行一善的100种方式
  17. 解决Android手机开发者选项经常自动关闭的问题
  18. 微软终于要在2022年让ie浏览器退役了
  19. C++ 栈的括号匹配
  20. C++ qsort的理解

热门文章

  1. 惠普打印机136a 136w 136nw的区别
  2. Umi4各种运行报错排查
  3. 【BFS】献给阿尔吉侬的花束、噩梦
  4. 微服务中的熔断、限流、降级
  5. openJDK 17.0.1 的下载与安装
  6. seo前景如何?就业前景怎么样?
  7. 数据类型说明:int16,int32,int64,WORD
  8. 微信小程序商机_微信小程序提供的创业商机有哪些
  9. 数据结构实验--停车场管理
  10. 阿里巴巴中台战略--数据库分库分表之异构索引表