Android性能优化系列:CPU收敛优化(线程优化)

因为 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 {
	@Override
	public 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 {
	@Override
	public boolean isManifestParsingEnabled() {
		return false;
	}
	
	@Override
	public 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() {
	@Override
	public 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(); // 列表停止滑动时恢复图片加载
			}
		}
	}
});
  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android系统性能优化对于解决卡顿、提升稳定性和优化续航方面起着重要的作用。 首先,在解决卡顿问题上,开发人员需要关注应用程序的UI线程。为了确保应用程序的流畅运行,可以采用以下优化措施:优化布局文件,减少层级嵌套;使用异步加载图片,避免在主线程中进行网络请求等耗时操作;合理利用缓存机制,避免重复加载数据。此外,还可以针对卡顿问题进行性能分析,通过工具查找耗时操作,并进行相应的优化。 其次,在提高系统稳定性方面,开发人员需要考虑异常崩溃的处理和内存管理。异常崩溃处理可通过捕获并记录崩溃异常来及时解决问题和改进代码。内存管理方面,应避免内存泄漏和过度分配内存,使用系统提供的工具来进行内存管理和优化。 最后,在续航优化上,需要考虑电源管理和资源使用的合理分配。通过使用省电模式、灵活控制后台任务和限制应用程序在后台运行等方式,最大程度地延长设备的电池寿命。另外,合理管理资源,避免过度使用CPU、网络和图形渲染等资源,有助于降低能耗并优化系统续航。 总之,Android系统性能优化是一个综合性的工作,需要开发人员关注卡顿问题、提升稳定性和优化续航方面的问题。通过合理使用工具和采取相应的优化措施,可以实现系统性能的有效提升。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值