关于 Android 线程优化你应该了解的知识点

·  阅读 1760
关于 Android 线程优化你应该了解的知识点

本文已收录到 GitHub/Android-Notes 中,这里有 Android 进阶成长知识体系笔记,有志同道合的朋友,关注公众号 [小尘Android专栏] 跟我一起成长。

前言

在实际项目开发中会频繁的用到线程,线程使用起来是很简单,但是滥用线程会带来性能问题, 比如启动一个线程至少 占用16kb的内存、线程过多会导致cpu的频繁切换而cpu切换成本是很高的、消耗大量用户电量等问题, 所以应该让app的线程数保持在合理水平,这是性能优化中很重要的一部分。本文对线程优化方面的知识做了一个全面总结,主要内容如下:

image.png

一、线程调度原理解析

线程调度的原理

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

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

  • 在任意时刻,只有一个线程占用 CPU,处于运行状态
  • 多线程并发运行,实际上说的是多个线程轮流获取 CPU 的使用权
  • JVM 负责线程调度,按照特定机制为多个线程分配 CPU 使用权

线程调度模型

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

  • 分时调度模型:让所有线程轮流获取 CPU 的使用权,而且均分每个线程占用 CPU 的时间片,这种方式非常公平
  • 抢占式调度模型:JVM 使用的是抢占式调度模型,让优先级高的线程优先获取到 CPU 的使用权,如果在可运行池当中的线程优先级都一样,那就随机选取一个

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
 * &lt;var&gt;tid&lt;/var&gt; 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?

  • 第一种就是那些 手动设置了优先级比较低的线程
  • 第二种就是 不在前台运行的那些应用程序的线程

线程调度小结

  • 线程过多会导致 CPU 频繁切换,降低线程运行效率。 在前面讲解启动优化的时候有强调要充足的利用线程比如异步启动任务,但是线程也不能无限制的使用
  • 正确认识任务重要性决定哪种优先级。 一般情况下线程工作量和优先级是成反比,比如线程的工作量越大,所做的工作没那么重要,那这个线程的优先级应该越低
  • 线程的优先级具有继承性。 比如在 A 线程创建了 B 线程,在我们没有指定线程优先级的情况下,B 线程的优先级是和 A 一样的。所以我们在 UI 线程中创建线程,线程的优先级是和 UI 线程一样的,这就会导致 UI 线程抢占 CPU 时间片的概率会变少

二、Android 异步方式汇总

Thread

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

  • 不易复用,频繁创建及销毁开销大
  • 复杂场景不易使用

HandlerThread

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

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();
复制代码

三、Android线程优化实战

线程使用准则

  • 严禁使用直接new Thread()的方式
  • 提供基础线程池供各个业务线使用: 避免各个业务线各自维护一套线程池,导致线程数过多
  • 根据任务类型选择合适的异步方式: 比如优先级低且长时间执行可以使用Handler Thread,再比如:有一个任务需要定时执行,使用线程池更适合
  • 创建线程必须命名: 方便定位线程归属于哪一个业务方,在线程运行期可以使用Thread.currentThread().setName修改名字
  • 关键异步任务监控: 异步不等于不耗时,如果一个任务在主线程需要耗费500ms,那么它在异步任务中至少需要500ms,因为异步任务中优先级较低,耗费时间很可能会高于500ms,所以这里可以使用AOP的方式来做监控,并且结合所在的业务场景,根据监控结果来适时的做一些相对应的调整
  • 重视优先级设置: 使用Process.setThreadPriority();设置,并且可以设置多次

线程池优化实战

接下来针对线程池的使用来做一个简单的实践,还是打开我们之前的项目,这里说一下每次实践的代码都是基于第一篇启动优化的那个案例上写的。

首先新建一个包async,然后在包中创建一个类ThreadPoolUtils,这里我们创建可重用且固定线程数的线程池,核心数为5,并且对外暴露一个get方法,然后我们可以在任何地方都能获取到这个全局的线程池:

public class ThreadPoolUtils {

    //创建定长线程池,核心数为5
    private static ExecutorService mService = Executors.newFixedThreadPool(5, new ThreadFactory() {
        @Override
        public Thread newThread(Runnable runnable) {
            Thread thread = new Thread(runnable,"ThreadPoolUtils");//设置线程名
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); //设置线程优先级
            return thread;
        }
    });

    //获取全局的线程池
    public static ExecutorService getService(){
        return mService;
    }

}
复制代码

然后使用的时候就可以在你需要的地方直接调用了,并且你在使用的时候还可以修改线程的优先级以及线程名称:

        //使用全局统一的线程池
        ThreadPoolUtils.getService().execute(new Runnable() {
            @Override
            public void run() {
                Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT); //修改线程优先级
                String oldName = Thread.currentThread().getName();
                Thread.currentThread().setName("Jarchie"); //修改线程名称
                Log.i("MainActivity","");
                Thread.currentThread().setName(oldName); //将原有名称改回去
            }
        });
复制代码

四、定位线程创建者

如何确定线程创建者

当你的项目做的越来越大的时候一般情况下线程都会变的非常多,最好是能够对整体的线程数进行收敛,那么问题来了,如何知道某个线程是在哪里创建的呢?不仅仅是你自己的项目源码,你依赖的第三方库、aar中都有线程的创建,如果单靠人眼review代码的方式,工作量很大而且你还不一定能找的全。并且你这次优化完了线程数,你还要考虑其他人新加的线程是否合理,所以就需要能够建立一套很好的监控预防手段。然后针对这些情况来做一个解决方案的总结分析,主要思想就是以下两点:

  • 创建线程的位置获取堆栈
  • 所有的异步方式,都会走到new Thread

解决方案:

  • 特别适合Hook手段
  • 找Hook点:构造函数或者特定方法
  • Thread的构造函数

可以在构造函数中加上自己的逻辑,获取当前的调用栈信息,拿到调用栈信息之后,就可以分析看出某个线程是否使用的是统一的线程池,也可以知道某个线程具体属于哪个业务方。

Epic实战

Epic简介

  • Epic是一个虚拟机层面、以Java Method为粒度的运行时Hook框架
  • 支持Android4.0-10.0(我的手机上程序出现了闪退,后来查找原因发现这个库开源版本一些高版本手机好像不支持)

Epic使用

  • implementation 'me.weishu:epic:0.6.0'
  • 继承XC_MethodHook,实现相应逻辑
  • 注入Hook:DexposedBridge.findAndHookMethod

代码中使用

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //Hook Thread类的构造函数,两个参数:需要Hook的类,MethodHook的回调
        DexposedBridge.hookAllConstructors(Thread.class, new XC_MethodHook() {
            //afterHookedMethod是Hook此方法之后给我们的回调
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                super.afterHookedMethod(param); //Hook完成之后会回调到这里
                //实现自己的逻辑,param.thisObject可以拿到线程对象
                Thread thread = (Thread) param.thisObject;
                //Log.getStackTraceString打印当前的调用栈信息
                Log.i(thread.getName() + "stack", Log.getStackTraceString(new Throwable()));
            }
        });
    }
复制代码

如果你的手机支持的话,这个时候运行程序应该就可以看到线程打印出来的堆栈信息了

五、优雅实现线程收敛

线程收敛常规方案

  • 根据线程创建堆栈考量合理性,使用统一线程库
  • 各业务线需要移除自己的线程库使用统一的线程库

基础库如何使用线程

  • 直接依赖线程库
  • 缺点:线程库更新可能会导致基础库也跟着更新

基础库优雅使用线程

  • 基础库内部暴露API:setExecutor
  • 初始化的时候注入统一的线程库

举个栗子: 比如这里有一个日志工具类,我们将它作为应用的日志基础库,假设它内部有一些异步操作,原始的情况下是它自己内部实现的,然后现在在它内部对外暴露一个API,如果外部注入了一个ExecutorService,那么我们就使用外部注入的这个,如果外部没有注入,那就使用它默认的,代码如下所示:

public class LogUtils {
    private static ExecutorService mExecutorService;
 
    public static void setExecutor(ExecutorService executorService){
        mExecutorService = executorService;
    }
 
    public static final String TAG = "Jarchie";
 
    public static void i(String msg){
        if(Utils.isMainProcess(BaseApp.getApplication())){
            Log.i(TAG,msg);
        }
        // 异步操作
        if(mExecutorService != null){
            mExecutorService.execute(() -> {
                ...
            });
        }else {
            //使用原有的
            ...
        }
    }
}
复制代码

统一线程库

  • 区分任务类型:IO密集型、CPU密集型
  • IO密集型任务不消耗CPU,核心池可以很大(网络请求、IO读写等)
  • CPU密集型任务:核心池大小和CPU核心数相关(如果并发数超过核心数会导致CPU频繁切换,降低执行效率)

举个栗子:根据上面的说明,可以做如下的设置:

    //获取CPU的核心数
    private int CPUCOUNT = Runtime.getRuntime().availableProcessors();
 
    //cpu线程池,核心数大小需要和cpu核心数相关联,这里简单的将它们保持一致了
    private ThreadPoolExecutor cpuExecutor = new ThreadPoolExecutor(CPUCOUNT, CPUCOUNT,
            30, TimeUnit.SECONDS, new LinkedBlockingDeque<>(), sThreadFactory);
 
    //IO线程池,核心数64,这个数量可以针对自身项目再确定
    private ThreadPoolExecutor iOExecutor = new ThreadPoolExecutor(64, 64,
            30, TimeUnit.SECONDS, new LinkedBlockingDeque<>(), sThreadFactory);
 
    //这里面使用了一个count作为标记
    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);
        public Thread newThread(Runnable runnable) {
            return new Thread(runnable, "ThreadPoolUtils #" + mCount.getAndIncrement());
        }
    };
复制代码

然后在你实际项目中需要区分具体的任务类型,针对性的选择相应的线程池进行使用。 以上就是对于Android线程优化方面的总结了,今天的内容还好不算多,觉得有用的朋友可以看看。

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:  点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。微信搜索公众号 [小尘Android专栏]  ,第一时间阅读更多干货知识!

分类:
Android
收藏成功!
已添加到「」, 点击更改