网站功能建设特点,上海站优云网络科技有限公司,什么是网络营销的市场细分,wordpress 后台分页按钮背景
最近在对一些大厂App进行研究学习#xff0c;在对某音App进行研究时#xff0c;发现其在线程方面做了一些优化工作#xff0c;并且其解决的问题也是之前我在做线上卡顿优化时遇到的#xff0c;因此对其具体实现方案做了深入分析。本文是对其相关源码的研究加上个人理…背景
最近在对一些大厂App进行研究学习在对某音App进行研究时发现其在线程方面做了一些优化工作并且其解决的问题也是之前我在做线上卡顿优化时遇到的因此对其具体实现方案做了深入分析。本文是对其相关源码的研究加上个人理解的一个小结。
问题
创建线程卡顿
我们可以可以知道 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中上述使用线程的地方都替换为我们的实现类。
使用以上线程相关类一般有几种方式
直接通过 new 原生类 创建相关实例继承原生类之后在代码中 使用 new 指令创建自己的继承类实例
因此这里的替换包括
修改类的继承关系比如 将所有 继承 Thread类的地方替换为 我们实现 的 PThread修改上述几种类直接创建实例的地方比如将代码中存在 new ThreadPoolExecutor(…) 调用的地方替换为 我们实现的 PThreadPoolExecutor
通过字码码修改将代码中所有使用线程的地方替换为我们的实现类后就可以在我们的实现类做一些线程收敛的操作。
Thread类 线程收敛
在Java虚拟机中每个Java Thread 都对应一个内核线程并且线程的创建实际上是在调用 start()函数才开始创建的那么我们其实可以修改start()函数的实现将其任务调度到指定的一个线程池做执行, 示例代码如下
class ThreadProxy : Thread() {override fun start() {SuperThreadPoolExecutor.execute({thisThreadProxy.run()}, priority priority)}
}线程池 线程收敛
由于每个ThreadPoolExecutor实例内部都有独立的线程缓存池不同ThreadPoolExecutor实例之间的缓存互不干扰在一个大型App中可能存在非常多的线程池所有的线程池加起来导致应用的最低线程数不容小视。
另外也因为线程池是独立的线程的创建和回收也都是独立的不能从整个App的任务角度来调度。举个例子: 比如A线程池因为空闲正在释放某个线程同时B线程池确可能正因为可工作线程数不足正在创建线程如果可以把所有的线程池合并成 一个统一的大线程池就可以避免类似的场景。
核心的实现思路为:
首先将所有直接继承 ThreadPoolExecutor的类替换为 继承 ThreadPoolExecutorProxy以及代码中所有new ThreadPoolExecutor(…)类 替换为 new ThreadPoolExecutorProxy(…)ThreadPoolExecutorProxy 持有一个 大线程池实例 BigThreadPool 该线程池实例为应用中所有线程池共用因此其核心线程数可以根据应用当前实际情况做调整比如如果你的应用当前线程数平均是200你可以将BigThreadPool 核心线程设置为150后再观察其调度情况。在 ThreadPoolExecutorProxy 的 addWorker 函数中将任务调度到 BigThreadPool中执行 AsyncTask 线程收敛
对于AsyncTask也可以用同样的方式实现在execute1函数中调度到一个统一的线程池执行
public abstract class AsyncTaskProxyParams,Progress,Result extends AsyncTaskParams,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 AsyncTaskParams, 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()returnlazy 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需改了创建线程时的栈空间大小。
在Java层直接配置一个 负值从而起到一样的效果 OOM了? 我还能再抢救下
针对在创建线程时由于内存空间不足或线程数限制抛出的OOM问题可以做一些兜底处理, 比如将任务调度到一个预先创建的线程池进行排队处理, 而这个线程池核心线程和最大线程是一致的 因此不会出现创建线程的动作也就不会出现OOM异常了。 另外由于一个应用可能会存在非常多的线程池每个线程池都会设置一些核心线程数要知道默认情况下核心线程是不会被回收的即使一直处于空闲状态该特性是由线程池的 allowCoreThreadTimeOut控制。 该参数值可通过 allowCoreThreadTimeOut(value) 函数修改 从具体实现中可以看出当value值和当前值不同 且 value 为true时 会触发 interruptIdleWorkers()函数, 在该函数中会对空闲Worker 调用 interrupt来中断对应线程 因此当创建线程出现OOM时可以尝试通过调用线程池的 allowCoreThreadTimeOut 来触发 interruptIdleWorkers 实现空闲线程的回收。 具体实现代码如下: 因此我们可以在每个线程池创建后将这些线程池用弱引用队列保存起来当线程start 或者某个线程池execute 出现OOM异常时通过这种方式来实现线程回收。
线程定位
线程定位 主要是指在进行问题分析时希望直接从线程名中定位到创建该线程的业务关于此类优化的文章网上已经介绍的比较多了基本实现是通过ASM 修改调用函数将当前类的类名或类名函数名作为兜底线程名设置。 字节码修改工具
前文讲了一些优化方式其中涉及到一个必要的操作是进行字节码修改这些需求可以概括为如下
替换类的继承关系比如将 所有继承于 java.lang.Thread的类替换为我们自己实现的 ProxyThread替换 new 指令的实例类型比如将代码中 所有 new Thread(…) 的调用替换为 new ProxyThread(…)
针对这些通用的修改没必要每次遇到类似需求时都 进行插件的单独开发因此我将这种修改能力集成到 LanceX插件中我们可以通过以下 注解方便实现上述功能。
替换 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日活并没有那么大这个优化的优先级也是较低的。 其实不管你是在做项目中还是面试中都会发现有一些性能优化的相关问题出现我们一般采用的方法是发现问题→定位问题→解决问题但有时可能有些问题的出现第一时间想不起来解决方法或是面试时答不上来这也就证明了你对这一块掌握的不是很熟练。为了帮助到大家快速熟练掌握性能优化的知识点整理了《Android 性能优化》的核心笔记大家可以参考https://qr18.cn/FVlo89
Android 性能优化核心笔记
包含内容有启动优化、内存优化、启动优化速度、卡顿优化、布局优化、崩溃优化、应用启动全流程源码深度解析……等内容