目录
写在前面
各位小伙伴们早上好,端午节即将到来,提前恭祝大家“端午安康”!
在上一篇中我们说到了Android平台卡顿优化的相关知识,还没了解的可以先去了解一波哦——《你想要知道的android卡顿优化》,
今天咱们继续Android性能优化专题的分析,来到了Android线程优化。
一、Android线程调度原理解析
1.1、线程调度原理
- 在任意时刻,CPU只能执行一条机器指令,每个线程只有获取到CPU的使用权之后才可以执行指令,也就是说,在任意时刻,只有一个线程占用CPU,处于运行状态
- 多线程并发:实际上是指多个线程轮流获取CPU的使用权,分别执行各自的任务
- JVM负责线程调度:在可运行池中实际是有多个处于就绪状态的线程在等待CPU,JVM按照特定机制分配CPU使用权
1.2、线程调度模型
- 分时调度模型:所有线程轮流获取CPU使用权,平均分配每个线程占用CPU的时间片
- 抢占式调度模型(JVM采用):优先让可运行池中优先级较高的线程占用CPU,如果优先级都一样则随机选取一个让其占用CPU。这里需要注意,由于JVM的线程调度并不是分时调度,因此同时启用多个线程之后并不能保证多个线程轮流获取到均等的时间片,所以如果程序希望干预线程的调度过程,最简单的方式就是给每个线程设定优先级
1.3、Android线程调度
①、nice值
- Process中定义
- 值越小,优先级越高
- 默认是THREAD_PRIORITY_DEFAULT,值为0
下面是android.os.Process类中定义的各个优先级:
②、cgroup
对于Android来说,只有nice值实际上并不能满足所有场景,比如某个应用有一个前台的UI线程,同时它还有10个后台线程,虽然后台线程的优先级比较低,但是数量较多,合起来这些后台线程对CPU的消耗也会影响到前台线程的性能,所以对于Android来说又引入了另外一套机制来处理这种特殊的情况——cgroup。
- 更严格的群组调度策略:后台优先级的线程会被隐式的移到后台group,当其他组的线程处于工作状态,后台group的线程会被限制,只有很小的几率能利用CPU,这种分离的策略,既允许了后台线程能执行一些任务,同时也不会对用户可见的前台线程造成很大的影响
- 保证前台线程可以获取到更多的CPU
- 可能会被移到后台group的线程:①、手动设置了优先级较低的线程;②、不在前台运行的应用程序的线程
需要注意的问题
- 线程过多会导致CPU频繁切换,降低线程运行效率:异步不能无限制的使用
- 正确认识任务重要性然后决定使用哪种优先级:一般情况下线程的优先级是和它所承担的工作量成反比,即:工作量越大优先级越低,CPU空闲阶段,线程的优先级对执行效率的影响并不明显,但是如果CPU处于忙碌阶段,线程频繁调度会对CPU产生较大影响
- 优先级具有继承性:举个栗子:在线程A中创建了线程B,如果没有指定线程B的优先级,则B的优先级会默认继承A的优先级,如果在UI线程中直接创建了一个子线程,实际上它俩的优先级是一样的,如果UI线程去抢占CPU的时间片概率会变小
二、Android异步方式
①、Thread:最简单、常见的异步方式
- 不易复用,频繁创建及销毁开销大
- 复杂场景不易使用
②、Handler Thread:自带消息循环的线程
- 串行执行
- 长时间运行,不断从队列中获取任务
③、Intent Service:继承自Service在内部创建Handler Thread
- 异步,不占用UI线程
- 优先级较高,不会轻易被系统Kill掉
④、AsyncTask:Android提供的异步工具类,内部实现是基于线程池
- 无需自己处理线程切换
- 需注意版本不一致问题(早期版本api不一致,由于现在适配版本普遍提高了,所以这个问题可以忽略)
⑤、线程池:Java提供的线程池
- 易复用,减少频繁创建、销毁的时间
- 功能强大:定时机制、任务队列、并发数控制等等
⑥、RxJava:由强大的Scheduler集合提供(这里只看线程调度功能)
- 不同类型的区分:IO、Computation
总结:
- 推荐程度:由后向前依次降低
- 正确场景选择正确的方式
三、Android线程优化实战
3.1、线程使用准则
- 严禁使用直接new Thread()的方式
- 提供基础线程池供各个业务线使用:避免各个业务线各自维护一套线程池,导致线程数过多
- 根据任务类型选择合适的异步方式:比如:优先级低且长时间执行可以使用Handler Thread,再比如:有一个任务需要定时执行,使用线程池更适合
- 创建线程必须命名:方便定位线程归属于哪一个业务方,在线程运行期可以使用Thread.currentThread().setName修改名字
- 关键异步任务监控:异步不等于不耗时,如果一个任务在主线程需要耗费500ms,那么它在异步任务中至少需要500ms,因为异步任务中优先级较低,耗费时间很可能会高于500ms,所以这里可以使用AOP的方式来做监控,并且结合所在的业务场景,根据监控结果来适时的做一些相对应的调整
- 重视优先级设置:使用Process.setThreadPriority();设置,并且可以设置多次
3.2、线程池优化实战
接下来针对线程池的使用来做一个简单的实践,还是打开我们之前的项目,这里说一下每次实践的代码都是基于第一篇启动优化的那个案例上写的。
首先新建一个包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); //将原有名称改回去
}
});
四、定位线程创建者
4.1、如何确定线程创建者
当你的项目做的越来越大的时候一般情况下线程都会变的非常多,最好是能够对整体的线程数进行收敛,那么问题来了,如何知道某个线程是在哪里创建的呢?不仅仅是你自己的项目源码,你依赖的第三方库、aar中都有线程的创建,如果单靠人眼review代码的方式,工作量很大而且你还不一定能找的全。并且你这次优化完了线程数,你还要考虑其他人新加的线程是否合理,所以就需要能够建立一套很好的监控预防手段。然后针对这些情况来做一个解决方案的总结分析,主要思想就是以下两点:
- 创建线程的位置获取堆栈
- 所有的异步方式,都会走到new Thread
解决方案:
- 特别适合Hook手段
- 找Hook点:构造函数或者特定方法
- Thread的构造函数
可以在构造函数中加上自己的逻辑,获取当前的调用栈信息,拿到调用栈信息之后,就可以分析看出某个线程是否使用的是统一的线程池,也可以知道某个线程具体属于哪个业务方。
4.2、Epic实战
Epic简介
- Epic是一个虚拟机层面、以Java Method为粒度的运行时Hook框架
- 支持Android4.0-10.0(我的手机上程序出现了闪退,后来查找原因发现这个库开源版本一些高版本手机好像不支持)
- https://github.com/tiann/epic
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()));
}
});
}
如果你的手机支持的话,这个时候运行程序应该就可以看到线程打印出来的堆栈信息了,我的手机不支持,所以就随便扒了一张图给大家了:
五、优雅实现线程收敛
5.1、线程收敛常规方案
- 根据线程创建堆栈考量合理性,使用统一线程库
- 各业务线需要移除自己的线程库使用统一的线程库
5.2、基础库如何使用线程
- 直接依赖线程库
- 缺点:线程库更新可能会导致基础库也跟着更新
5.3、基础库优雅使用线程
- 基础库内部暴露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线程优化方面的总结了,今天的内容还好不算多,觉得有用的朋友可以看看。
OK,废话就不多说了,今天就先到这里吧,各位下期再会!