背景

最近在对一些大厂App进行研究学习,在对某音App进行研究时,发现其在线程方面做了一些优化作业,而且其处理的问题也是之前我在做线上卡顿优化时遇到的,因而对其具体完成计划做了深入剖析。本文是对其相关源码的研究加上个人了解的一个小结。

问题

创立线程卡顿

在Java中,真实的内核线程被创立是在履行 start函数的时分, nativeCreate的具体流程能够参阅我之前的一篇剖析文章 Android虚拟机线程发动进程解析 。这儿假定你已经了解了,咱们能够能够知道 start()函数底层涉及到一系列的操作,包含 栈内存空间分配、内核线程创立 等操作,这些操作在某些情况下或许呈现长耗时现象,比方因为linux体系中,一切体系线程的创立在内核层是由一个专门的线程排队完成,那么是否或许因为行列较长一起内核调度呈现问题而呈现长耗时问题? 具体的原因因为没有在线下复现过此类问题,因而只能斗胆猜测,不过在线上确实收集到一些case, 以下是线上收集到一个堵塞现场样本:

扒一扒抖音是如何做线程优化的

那么是不是不要直接在主线程创立其他线程,而是直接运用线程池调度使命就没有问题? 让咱们看下 ThreadPoolExecutor.execute(Runnable command)的源码完成

扒一扒抖音是如何做线程优化的

从文档中能够知道,execute函数的履行在很多情况下会创立(JavaThread)线程,而且盯梢其内部完成后能够发现创立Java线程目标后,也会当即在当时线程履行start函数。

扒一扒抖音是如何做线程优化的

来看一下线上收集到的一个在主线程运用线程池调度使命仍旧发生卡顿的现场。

扒一扒抖音是如何做线程优化的

线程数过多的问题

在ART虚拟机中,每创立一个线程都需求为其分配独立的Java栈空间,当Java层未显现设置栈空间巨细时,native层会在FixStackSize函数会分配默许的栈空间巨细.

扒一扒抖音是如何做线程优化的

从这个完成中,能够看出每个线程至少会占用1M的虚拟内存巨细,而在32位体系上,因为每个进程可分配的用户用户空间虚拟内存巨细只有3G,假如一个运用的线程数过多,而当进程虚拟内存空间缺乏时,创立线程的动作就或许导致OOM问题.

扒一扒抖音是如何做线程优化的

另一个问题是某些厂商的运用所能创立的线程数比较原生Android体系有更严格的约束,比方某些华为的机型约束了每个进程所能创立的线程数为500, 因而即使是64位机型,线程数不做控制也或许呈现因为线程数过多导致的OOM问题。

优化思路

线程收敛

首先在一个Android App中存在以下几种情况会运用到线程

  • 经过 Thread类 直接创立运用线程
  • 经过 ThreadPoolExecutor 运用线程
  • 经过 ThreadTimer 运用线程
  • 经过 AsyncTask 运用线程
  • 经过 HandlerThread 运用线程

线程收敛的大致思路是, 咱们会预先创立上述几个类的完成类,并在自己的完成类中做修正, 之后经过编译期的字节码修正,将App中上述运用线程的当地都替换为咱们的完成类。

运用以上线程相关类一般有几种方法:

  1. 直接经过 new 原生类 创立相关实例
  2. 承继原生类,之后在代码中 运用 new 指令创立自己的承继类实例

因而这儿的替换包含:

  • 修正类的承继联系,比方 将一切 承继 Thread类的当地,替换为 咱们完成 的 PThread
  • 修正上述几品种直接创立实例的当地,比方将代码中存在 new ThreadPoolExecutor(..) 调用的当地替换为 咱们完成的 PThreadPoolExecutor

经过字码码修正,将代码中一切运用线程的当地替换为咱们的完成类后,就能够在咱们的完成类做一些线程收敛的操作。

Thread类 线程收敛

在Java虚拟机中,每个Java Thread 都对应一个内核线程,而且线程的创立实际上是在调用 start()函数才开端创立的,那么咱们其实能够修正start()函数的完成,将其使命调度到指定的一个线程池做履行, 示例代码如下

class ThreadProxy : Thread() {
    override fun start() {
        SuperThreadPoolExecutor.execute({
            this@ThreadProxy.run()
        }, priority = priority)
    }
}

线程池 线程收敛

因为每个ThreadPoolExecutor实例内部都有独立的线程缓存池,不同ThreadPoolExecutor实例之间的缓存互不搅扰,在一个大型App中或许存在非常多的线程池,一切的线程池加起来导致运用的最低线程数不容小视。

另外也因为线程池是独立的,线程的创立和收回也都是独立的,不能从整个App的使命视点来调度。举个比方: 比方A线程池因为闲暇正在开释某个线程,一起B线程池确或许正因为可作业线程数缺乏正在创立线程,假如能够把一切的线程池合并成 一个共同的大线程池,就能够避免相似的场景。

中心的完成思路为:

  1. 首先将一切直接承继 ThreadPoolExecutor的类替换为 承继 ThreadPoolExecutorProxy,以及代码中一切new ThreadPoolExecutor(..)类 替换为 new ThreadPoolExecutorProxy(…)
  2. ThreadPoolExecutorProxy 持有一个 大线程池实例 BigThreadPool ,该线程池实例为运用中一切线程池共用,因而其间心线程数能够依据运用当时实际情况做调整,比方假如你的运用当时线程数平均是200,你能够将BigThreadPool 中心线程设置为150后,再调查其调度情况。
  3. 在 ThreadPoolExecutorProxy 的 addWorker 函数中,将使命调度到 BigThreadPool中履行

扒一扒抖音是如何做线程优化的

AsyncTask 线程收敛

关于AsyncTask也能够用同样的方法完成,在execute1函数中调度到一个共同的线程池履行


public abstract class AsyncTaskProxy<Params,Progress,Result> extends AsyncTask<Params,Progress,Result>{
    private static final Executor THREAD_POOL_EXECUTOR = new PThreadPoolExecutor(0,20,
            3, TimeUnit.MILLISECONDS,
            new SynchronousQueue<>(),new DefaultThreadFactory("PThreadAsyncTask"));
    public static void execute(Runnable runnable){
        THREAD_POOL_EXECUTOR.execute(runnable);
    }
    /**
     * TODO 运用插桩 将一切 execute 函数调用替换为 execute1
     * @param params  The parameters of the task.
     * @return This instance of AsyncTask.
     */
    public AsyncTask<Params, Progress, Result> execute1(Params... params) {
        return executeOnExecutor(THREAD_POOL_EXECUTOR,params);
    }
}

Timer类

Timer类一般项目中运用的当地并不多,而且因为Timer一般对使命间隔准确性有比较高的要求,假如收敛到线程池履行,假如某些Timer类履行的task比较耗时,或许会影响原事务,因而暂不做收敛。

卡顿优化

针对在主线程履行线程创立或许会呈现的堵塞问题,能够判别下当时线程,假如是主线程则调度到一个专门负责创立线程的线程进行作业。

    private val asyncExecuteHandler  by lazy {
        val worker = HandlerThread("asyncExecuteWorker")
        worker.start()
        return@lazy Handler(worker.looper)
    }
    fun execute(runnable: Runnable, priority: Int) {
        if (Looper.getMainLooper().thread == Thread.currentThread() && asyncExecute
        ){
            //异步履行
            asyncExecuteHandler.post {
                mExecutor.execute(runnable,priority)
            }
        }else{
            mExecutor.execute(runnable, priority)
        }
    }

32位体系线程栈空间优化

在问题剖析中的环节中,咱们已经知道 每个线程至少需求占用 1M的虚拟内存,而32位运用的虚拟内存空间又有限,假如期望在线程这儿挤出一点虚拟内存空间来,能够参阅微信的一个计划, 其利用PLT hook需改了创立线程时的栈空间巨细。

而在另一篇 /post/720930… 技术文章中,也介绍了另一个取巧的计划 :在Java层直接装备一个 负值,然后起到一样的作用

扒一扒抖音是如何做线程优化的

OOM了? 我还能再抢救下!

针对在创立线程时因为内存空间缺乏或线程数约束抛出的OOM问题,能够做一些兜底处理, 比方将使命调度到一个预先创立的线程池进行排队处理, 而这个线程池中心线程和最大线程是共同的 因而不会呈现创立线程的动作,也就不会呈现OOM反常了。

扒一扒抖音是如何做线程优化的

另外因为一个运用或许会存在非常多的线程池,每个线程池都会设置一些中心线程数,要知道默许情况下中心线程是不会被收回的,即使一向处于闲暇状态,该特性是由线程池的 allowCoreThreadTimeOut控制。

扒一扒抖音是如何做线程优化的

该参数值可经过 allowCoreThreadTimeOut(value) 函数修正

扒一扒抖音是如何做线程优化的

从具体完成中能够看出,当value值和当时值不同 且 value 为true时 会触发 interruptIdleWorkers()函数, 在该函数中,会对闲暇Worker 调用 interrupt来中止对应线程

扒一扒抖音是如何做线程优化的

因而当创立线程呈现OOM时,能够测验经过调用线程池的 allowCoreThreadTimeOut 来触发 interruptIdleWorkers 完成闲暇线程的收回。 具体完成代码如下:

扒一扒抖音是如何做线程优化的

因而咱们能够在每个线程池创立后,将这些线程池用弱引用行列保存起来,当线程start 或许某个线程池execute 呈现OOM反常时,经过这种方法来完成线程收回。

线程定位

线程定位 首要是指在进行问题剖析时,期望直接从线程名中定位到创立该线程的事务,关于此类优化的文章网上已经介绍的比较多了,基本完成是经过ASM 修正调用函数,将当时类的类名或类名+函数名作为兜底线程名设置。这儿就不具体介绍了,感兴趣的能够看 booster 中的完成

扒一扒抖音是如何做线程优化的

字节码修正工具

前文讲了一些优化方法,其间涉及到一个必要的操作是进行字节码修正,这些需求能够归纳为如下

  • 替换类的承继联系,比方将 一切承继于 java.lang.Thread的类,替换为咱们自己完成的 ProxyThread
  • 替换 new 指令的实例类型,比方将代码中 一切 new Thread(..) 的调用替换为 new ProxyThread(…)

针对这些通用的修正,没必要每次遇到相似需求时都 进行插件的独自开发,因而我将这种修正才能集成到开源库 LanceX插件中:github.com/Knight-ZXW/… ,咱们能够经过以下 注解便利完成上述功用。

替换 new 指令

@Weaver
@Group("threadOptimize")
public class ThreadOptimize {
    @ReplaceNewInvoke(beforeType = "java.lang.Thread",
    afterType = "com.knightboost.lancetx.ProxyThread")
    public static void replaceNewThread(){
    }
}

这儿的 beforeType表明原类型,afterType 表明替换后的类型,运用该插件在项目编译后,项目中的如下源码

扒一扒抖音是如何做线程优化的

会被主动替换为

扒一扒抖音是如何做线程优化的

替换类的承继联系

@Weaver
@Group("threadOptimize")
public class ThreadOptimize {
    @ChangeClassExtends(
            beforeExtends = "java.lang.Thread",
            afterExtends = "com.knightboost.lancetx.ProxyThread"
    )
    public void changeExtendThread(){};
}

这儿的beforeExtends表明 原承继父类,afterExtends表明修正后的承继父类,在项目编译后,如下源码

扒一扒抖音是如何做线程优化的

会被主动替换为

扒一扒抖音是如何做线程优化的

总结

本文首要介绍了有关线程的几个方面的优化

  • 主线程创立线程耗时优化
  • 线程数收敛优化
  • 线程默许虚拟空间优化
  • OOM优化

这些不同的优化手法需求依据项目的实际情况进行选择,比方主线程创立线程优化的完成方面比较简单、影响面也比较低,能够优先实施。 而线程数收敛需求涉及到字节码插桩、各种目标代理 复杂度会高一些,能够依据当时项目的实际线程数情况再考虑是否需求优化。

线程OOM问题首要呈现在低端设备 或一些特定厂商的机型上,或许关于某些大厂的用户基数来说有必定的收益,假如你的App日活并没有那么大,这个优化的优先级也是较低的。

功能优化专栏历史文章:

文章 地址
抖音消息调度优化发动速度计划的实践 /post/721766…
扒一扒抖音是怎么做线程优化的 /post/721244…
监控Android Looper Message调度的另一种姿势 /post/713974…
Android 高版别采集体系CPU运用率的方法 /post/713503…
Android 平台下的 Method Trace 完成及运用 /post/710713…
Android 怎么处理运用SharedPreferences 造成的卡顿、ANR问题 /post/705476…
根据JVMTI 完成功能监控 /post/694278…

参阅资料

1.某音App

2.内核线程创立流程

3./post/720930… 虚拟内存优化: 线程 + 多进程优化

4.github.com/didi/booste…