一、背景
在日常开发APP的进程中,不免需求运用第二方库和第三方库来帮助开发者快速完成一些功用,进步开发效率。可是,这些库也或许会给线程带来一定的压力,首要表现在以下几个方面:
- 线程数量增多:一些库或许会在后台发动一些线程来履行使命,这样会添加体系中线程的数量,然后导致体系资源的糟蹋。
- 线程竞赛:一些库或许会在同一时间发动多个线程来履行使命,这样会导致线程之间的竞赛,然后影响程序的履行效率。
- 线程堵塞:一些库或许会在履行使命时堵塞主线程,然后导致程序的卡顿和响应速度变慢。
二、全体思路
为了解决运用第二方库和第三方库署理的线程问题,我挑选用下面的思路来进行线程优化:
- 线程检测,评估优化空间。
- 线程计算,搜集优化规模。
- 线程和线程池优化,线程数收敛。
- 线程栈裁剪,削减线程内存。
三、详细计划
1. 线程检测
最常见的几种获取线程信息的办法如下
为了有完好的线程计算,而且能实时了解运转进程中线程数的改动,那咱们就挑选了读取伪文件体系里边线程信息的办法。
/**
* 获取一切线程信息
*/
private fun getThreadInfoList(): List<ThreadInfo>? {
//获取伪文件一切的线程信息文件
val file = File("/proc/self/task")
...
//遍历task文件目录下
for (threadDir in listFile) {
//读取每个目录下的status文件获取单个线程信息
val statusFile = File(threadDir, "status")
if (statusFile.exists()) {
val threadInfo = ThreadInfo()
try {
BufferedReader(InputStreamReader(FileInputStream(statusFile))).use { reader ->
var line: String
hitFlag = 0
while (reader.readLine().also { line = it } != null) {
if (hitFlag > 2) {
break
}
//解析线程名
if (line.startsWith("Name")) {
val name =
line.substring("Name".length + 1).trim { it <= ' ' }
threadInfo.name = name
hitFlag++
continue
}
//解析线程Pid
if (line.startsWith("Pid")) {
val pid =
line.substring("Pid".length + 1).trim { it <= ' ' }
threadInfo.id = pid
hitFlag++
continue
}
//解析线程状态
if (line.startsWith("State")) {
...
threadInfo.status = state
hitFlag++
}
}
}
} catch (e: Exception) {
Log.e(LOG_TAG, e.toString())
}
threadInfoList.add(threadInfo)
}
}
return threadInfoList
}
最终只需求在APP发动后就敞开轮询使命:1,获取伪文件。2,写入数据库。3,更新视图展示。
计算了运转时创立的线程、可用的线程、正在运转的线程。
抱负的状况便是可用的线程数应该和正在运转的线程数尽量挨近,实践发现差异巨大,所以优化的空间仍是蛮值得期待的。
2. 线程计算
-
了解创立线程和线程池的字节码
-
怎么扫描到创立的线程和线程池
经过插桩的办法,来查找创立线程池和线程的类名,并把这些类名一致输出到一份txt文档。插桩的框架,我挑选的是ASM,由于运用ASM进行插桩具有高效性、灵敏性、易用性、兼容性和社区活跃等长处,是一种比较优异的字节码操作框架,关于进步运用程序的功能和可保护性具有重要意义。
那么经过ASM是怎么扫描到的呢?
要扫描到创立线程池的类名,你需求运用ASM的访问者模式(Visitor Pattern)来遍历字节码中的办法和指令。在遍历进程中,当遇到创立线程的指令(如:new java/util/concurrent/ThreadPoolExecutor)时,就能够获取到创立线程的类名。
import org.objectweb.asm.*;
public class ThreadPoolDetectorClassVisitor extends ClassVisitor {
public ThreadPoolDetectorClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
return new ThreadPoolDetectorMethodVisitor(api, mv);
}
class ThreadPoolDetectorMethodVisitor extends MethodVisitor {
public ThreadPoolDetectorMethodVisitor(int api, MethodVisitor methodVisitor) {
super(api, methodVisitor);
methodVisitor);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf)) {
if (opcode == Opcodes.INVOKESTATIC && owner.startsWith("java/util/concurrent/Executors")) {
System.out.println("Detected creation of new ThreadPool!");
}
super.visitMethodInsn(opcode, owner, name, desc desc, itf);
}
}
}
-
计算和分类扫描到的创立线程和线程池的类名
- 扫描到的成果
- 成果进行分类
- 成果的用途
- 了解项目现状。
- 对后续优化能够设置白名单。
- 能够对线上设置的线程进行降级处理。
3. 线程和线程池优化
3.1 线程优化
- 关于APP事务层和自研SDK,咱们查看是否真的需求直接new thread,能否用线程池替代,假如有必要创立单个线程,那咱们创立的时分有必要加上线程名,便利排查线程问题。
- 关于三方SDK,那就能够经过插桩来重命名(名称有必要少于16个字符),便利赶快知道该线程是来自哪个SDK。
3.2 线程池优化
- 关于APP事务层,咱们需求供给常用线程池,例如I/O、CPU、Single、Cache等等线程池,防止开发各自创立重复的线程池。
- 关于自研SDK,咱们尽量让架构组的开发同学供给能够设置自界说线程池的才能,便利咱们署理到咱们APP事务层的线程池。
- 关于三方SDK,首先了解有没有供给设置咱们自界说线程池的接口,有的话,那就直接设置咱们APP事务层的线程池。假如没有这种才能,那咱们就进行插桩来进行线程池收敛。在进行三方SDK插桩署理的时分,需求留意三点:
- 设置白名单,进行逐步署理。
- 针对不同的SDK,要区分是本地使命仍是网络使命,这样能明确是署理到I/O线程池仍是CPU线程池。
- 设置降级开关,便利线上有问题时,及时对单个SDK进行降级处理。
3.2.1 职业计划
(1)反射收敛,可是运用反射来收敛线程池的确有一些潜在的坏处:
- 功能开支:反射在履行时需求进行一系列的查看和解析,这会比直接的Java办法办法调用带来更大的功能开支。
- 安全问题:反射能够访问一切的字段和办法,包含私有有的和受保护的,这或许会损坏目标的封装性,导致安全问题。
- 代码复杂性:运用反射的代码一般比直接的Java代码更复杂,更难了解和保护。
因而,尽管反射是一种强壮的工具,但在运用时需求谨慎,尽量防止不必要的运用。
(2)署理收敛,可是运用署理规划模式来收敛线程池也有一些潜在的坏处:
- 添加复杂性:署理办法会引进额定的类和目标,这会添加体系的复杂性。关于简略的问题,运用署理或许会显得过于复杂。
- 代码可读性:由于署理办法涉及到额定的抽象层,这或许会对代码的可读性产生一定的影响。
- 调试困难:由于署理模式的存在,错误或许会被掩盖或许难以定位,这或许会使得调试变得愈加困难。
因而,尽管署理模式是一种强壮的规划模式,但在运用时也需求考虑到这些潜在的问题。
(3)协程收敛,可是运用协程收敛线程池也有一些局限性和潜在的坏处:
- 需求依靠Kotlin协程库:运用Kotlin协程需求依靠Kotlin协程库,假如运用程序中没有运用Kotlin言语,那么需求额定引进Kotlin库,添加了运用程序的体积。
- 协程的履行时间不能过长:Kotlin协程的履行时间不能过长,不然会影响其他协程的履行。因而,在运用Kotlin协程进行线程收敛时,需求合理控制协程的履行时间。
- 或许会导致内存走漏:假如协程没有正确地撤销,或许会导致内存走漏。因而,在运用Kotlin协程时,需求留意正确地撤销协程。
因而,尽管Kotlin协程能够经过运用协程调度器来完成线程收敛,可是也存在一些坏处,需求开发者依据详细状况来挑选是否运用。
(4)插桩收敛,尽管插桩也有一些不足之处:
- 或许影响程序行为:假如插桩代码改动了程序的状态或许影响了线程的线程的调度,那么它或许会改动程序的行为。
- 或许引进错误:假如插桩代码桩代码本身存在错误,那么它或许会引进新的错误到程序中。
可是这些缺陷在线程池收敛的时分仍是可控的,比较于上面的反射收敛、署理收敛和协程收敛来说,还有许多长处:
- 直接性:插桩直接在代码中刺进额定的逻辑,不需求经过署理或反射射间接地操作目标,这使得插桩更直接,更易于了解和控制。
- 灵敏活性:插桩能够在任何方位刺进代码,,这供给了很大的灵敏性。而署理和反射一般只能操作公开的接口和办法。
- 无需修正原始代码:插桩一般常不需求常不需求修正原始的线程池代码,这使得它能够在不影响原始代码的状况下搜集信息。
- 颗粒度控制:能够对某个办法或某段代码进行线程收敛,而不是整个运用程序。
综上所述,我就挑选了愈加通用、灵敏、精确的办法来收敛二方和三方的线程池—插桩署理。
3.2.2 代码规划图
3.2.3 代码流程图
暂时无法在飞书文档外展示此内容
3.2.4 代码施行
- 创立NewThreadTrackerPlugin,在插件里首要是获取到需求进行署理的线程池白名单以及注册ThreadTrackerTransform。
class NewThreadTrackerPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
System.out.println("ThreadTracker:start ThreadTrackerPlugin")
project.getRootProject().getSubprojects().each { subProject ->
PluginUtils.addProjectName(subProject.name)
PluginUtils.projectPathList.add(subProject.projectDir.toString())
}
org.gradle.api.plugins.ExtraPropertiesExtension ext = project.getRootProject().getExtensions().getExtraProperties()
//经过装备来设置是否需求输出一切创立线程池的txt文件,文件名为"thread_tracker_XXX.txt"
if (ext.has("scanProject")) {
boolean scan = ext.get("scanProject")
PluginUtils.setScanProject(scan)
System.out.println("ThreadTracker:需求扫描项目吗?" + scan)
}
//经过装备来获取需求进行插桩署理的白名单
if(ext.has("whiteList")){
List<String> list = ext.get("whiteList")
PluginUtils.addWhiteList(list)
}else {
System.out.println("ThreadTracker:请创立thread_tracker.gradle文件,设置whiteList白名单")
}
//注册ThreadTrackerTransform。
//Gradle Transform 是 Android 官方供给给开发者在项目构建阶段,即由 .class 到 .dex 转化期间修正 .class 文件的一套 API。现在比较经典的运用是字节码插桩、代码注入技能。
AppExtension appExtension = (AppExtension) project.getProperties().get("android")
appExtension.registerTransform(new ThreadTrackerTransform(), Collections.EMPTY_LIST)
}
}
- 创立 ThreadTrackerTransform,重写ThreadTrackerTransform的transform办法,在该办法里边来遍历文件目录下和Jar包中的class文件,并让ClassReader接受的是咱们自界说的ThreadTrackerClassVisitor。
/**
* transform 办法来处理中间转化进程,首要逻辑在该办法中完成。咱们能够在 transform 办法中,完成对字节码的修正、处理等操作。
* @param transformInvocation
*/
@Override
void transform(@NonNull TransformInvocation transformInvocation) {
...
//关于一个.class文件进行Class Transformation操作,全体思路是这样的:
// ClassReader --> ClassVisitor(1) --> ... --> ClassVisitor(N) --> ClassWriter
ClassReader classReader = new ClassReader(file.bytes)
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new ThreadTrackerClassVisitor(classWriter, null)
classReader.accept(cv, EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
FileOutputStream fos = new FileOutputStream(
file.parentFile.absolutePath + File.separator + name)
fos.write(code)
fos.close()
...
}
- 创立ThreadTrackerClassVisitor,重写visitMethod来回来自界说的MethodVisitor,经过这个目标来访问办法的详细信息。
在visitMethod办法办法中,咱们能够刺进自己的代码,以修正或替换原有的办法声明声明。例如,咱们能够改动办法的访问权限、改动办法的参数、改动办法的回来值,甚至能够彻底替换原有的办法声明。
@Override
public MethodVisitor visitMethod(int access0, String name0, String desc0, String signature0, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access0, name0, desc0, signature0, exceptions);
if (filterClass(className)) {
return mv;
}
return new ProxyThreadPoolMethodVisitor(ASM6, mv, className);
}
/**
*。 过滤掉不需求插桩的类,比方这个插桩代码模块、自界说的线程池等等
**/
private boolean filterClass(String className) {
return className.contains("com/lalamove/threadtracker/") || className.contains("com/lalamove/plugins/thread") || className.contains("com/tencent/tinker/loader") || className.contains("com/lalamove/huolala/client/asm/HllPrivacyManager");
}
- 创立ProxyThreadPoolMethodVisitor,并重写它的visitMethodInsn办法来实在插桩自己的线程池。
在visitMethodInsn办法中,咱们能够刺进自己的代码,以修正或替换原有的办法调用。
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
//假如装备中是需求扫描App,则把创立线程池的类名全部都写在"thread_tracker_XXX.txt"里边,供开发者计算、分类、设置白名单和降级处理
if (PluginUtils.getScanProject()) {
if (owner.equals(O_ThreadPoolExecutor) && name.equalsIgnoreCase("<init>")) {
PluginUtils.writeClassNameToFile("创立ThreadPoolExecutor的类:" + className);
}
}
//假如装备中是需求插桩署理线程池,则把原本的类 "java/util/concurrent/ThreadPoolExecutor"换成了咱们自界说的类"com/lalamove/threadtracker/proxy/BaseProxyThreadPoolExecutor"
//mClassProxy只是一个总开关,是否敞开署理;详细某个类是否需求署理,在创立线程池的详细当地会依据类名来判断
if (mClassProxy) {
if (owner.equals(O_ThreadPoolExecutor) && name.equalsIgnoreCase("<init>")) {
if ("(IIJLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/BlockingQueue;)V".equalsIgnoreCase(descriptor)) {
mv.visitLdcInsn(className);
mv.visitMethodInsn(opcode, O_BaseProxyThreadPoolExecutor, name, "(IIJLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/BlockingQueue;Ljava/lang/String;)V", false);
} else if ("(IIJLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/BlockingQueue;Ljava/util/concurrent/ThreadFactory;)V".equalsIgnoreCase(descriptor)) {
mv.visitLdcInsn(className);
mv.visitMethodInsn(opcode, O_BaseProxyThreadPoolExecutor, name, "(IIJLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/BlockingQueue;Ljava/util/concurrent/ThreadFactory;Ljava/lang/String;)V", false);
} else if ("(IIJLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/BlockingQueue;Ljava/util/concurrent/RejectedExecutionHandler;)V".equalsIgnoreCase(descriptor)) {
mv.visitLdcInsn(className);
mv.visitMethodInsn(opcode, O_BaseProxyThreadPoolExecutor, name, "(IIJLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/BlockingQueue;Ljava/util/concurrent/RejectedExecutionHandler;Ljava/lang/String;)V", false);
} else if ("(IIJLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/BlockingQueue;Ljava/util/concurrent/ThreadFactory;Ljava/util/concurrent/RejectedExecutionHandler;)V".equalsIgnoreCase(descriptor)) {
mv.visitLdcInsn(className);
mv.visitMethodInsn(opcode, O_BaseProxyThreadPoolExecutor, name, "(IIJLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/BlockingQueue;Ljava/util/concurrent/ThreadFactory;Ljava/util/concurrent/RejectedExecutionHandler;Ljava/lang/String;)V", false);
} else {
mv.visitMethodInsn(opcode, O_BaseProxyThreadPoolExecutor, name, descriptor, false);
}
return;
}
}
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
上述运用到的一些常量界说如下,也引进到了咱们自己自界说的线程池。
class ClassConstant {
//Java里边创立线程池的类名
static final String O_ThreadPoolExecutor = "java/util/concurrent/ThreadPoolExecutor";
//自界说创立线程池的类名
static final String O_BaseProxyThreadPoolExecutor = "com/lalamove/threadtracker/proxy/BaseProxyThreadPoolExecutor";
}
- 创立BaseProxyThreadPoolExecutor,重写了创立线程池的一切构造办法,也经过传入的类名判断了该类里边的线程池是否需求署理,以及署理的是的CPU密集型线程池仍是IO密集型线程池。
package com.lalamove.threadtracker.proxy
import android.util.Log
import com.lalamove.threadtracker.TrackerUtils
import java.util.concurrent.*
/**
* ThreadPoolExecutor署理类
*/
open class BaseProxyThreadPoolExecutor : ThreadPoolExecutor {
var mProxy = true
//App层自界说的IO线程池
private var threadPoolExecutor: ThreadPoolExecutor =
TrackerUtils.getProxyNetThreadPool()
constructor(
corePoolSize: Int,
maximumPoolSize: Int,
keepAliveTime: Long,
unit: TimeUnit?,
workQueue: BlockingQueue<Runnable>?,
className: String?,
) : super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue) {
init(corePoolSize,
maximumPoolSize,
keepAliveTime, className)
}
private fun init(
corePoolSize: Int,
maximumPoolSize: Int,
keepAliveTime: Long,
className: String?,
) {
//判断className下创立的线程池是否要被插桩署理
if (className != null) {
mProxy = TrackerUtils.isProxy(className)
}
//单线程暂不署理
if (corePoolSize == 1 || (corePoolSize == 0 && maximumPoolSize == 1)) {
mProxy = false
}
if (!mProxy) {
return
}
//设置中心线程超时允许销毁
if (keepAliveTime <= 0) {
setKeepAliveTime(10L, TimeUnit.MILLISECONDS)
}
allowCoreThreadTimeOut(true)
//设置className的线程池被署理为CPU线程池
if (className != null && TrackerUtils.proxyCpuClass(className)) {
threadPoolExecutor = TrackerUtils.getProxyCpuThreadPool()
}
}
...
override fun submit(task: Runnable): Future<*> {
return if (mProxy) threadPoolExecutor.submit(task) else super.submit(task)
}
override fun execute(command: Runnable) {
if (mProxy) threadPoolExecutor.execute(command) else super.execute(command)
}
//留意:不能关闭,不然影响其他被署理的线程池
override fun shutdown() {
if (!mProxy) {
super.shutdown()
}
}
//留意:不能关闭,不然影响其他被署理的线程池
override fun shutdownNow(): MutableList<Runnable> {
val list = if (mProxy) mutableListOf<Runnable>() else super.shutdownNow()
return list
}
}
3.2.5 施行署理
- 在工程最外层创立thread_tracker.gradle,里边能够设置需求署理的线程池白名单。
- 经过打印日志就能看出白名单里边的线程池是否被署理成功。
- 设置降级开关
(1)设置每个SDK里边细分类名对应的code
(2)在装备体系上设置需求关闭SDK,设置上面对应的code码即可。
(3)在APP初始化的时分尽或许早的获取装备体系上的code字符串
(4)在进行署理的时分,会匹配code字符串,来决定详细的线程池是否进行署理。
3.2.6 署理后的收益
- 累计削减了大约40条线程的开支
4. 线程栈裁剪
4.1 裁剪办法
创立线程的时分,线程默许的栈空间巨细为 1M 左右,经过测验大部分状况下线程内履行的逻辑并不需求这么大的空间,因而线程栈空间减小,能够对内存这块有显着的优化。
接下来咱们来看下函数FixStackSize源码,是怎么设置线程栈默许为1M的?
static size_t FixStackSize(size_t stack_size) {
//参数是java层中thread 的stack_size默许0
if (stack_size == 0) {
stack_size = Runtime::Current()->GetDefaultStackSize();
}
// 默许栈巨细是 1M
stack_size += 1 * MB;
//...
if (Runtime::Current()->ExplicitStackOverflowChecks()) {
stack_size += GetStackOverflowReservedBytes(kRuntimeISA);
} else {
8k+8K
stack_size += Thread::kStackOverflowImplicitCheckSize +
GetStackOverflowReservedBytes(kRuntimeISA);
}
//...
return stack_size;
}
发现函数的源码完成便是经过 stack_size += 1 * MB 来设置 stack_size 的: 假如咱们传入的 stack_size 为 0 时,默许巨细便是 1 M ; 假如咱们传入的 stack_size 为 -512KB 时,stack_size 就会变成 512KB(1M – 512KB)。 那咱们是不是只用带有 stack_size 入参的构造函数去创立线程,而且设置 stack_size 为 -512KB 就行了呢? 运用中创立线程的当地太多很难一一修正,前面咱们现已将运用中的线程部分收敛到自界说的线程池中去了,所以只需求修正自界说线程池中创立的线程办法即可。在咱们自界说的 ThreadFactory 中,创立 stack_size 为 – 512 KB 的线程,这么一个简略的操作就能削减线程所占用的虚拟内存。
package com.lalamove.threadtracker.proxy
import java.util.concurrent.ThreadFactory
import java.util.concurrent.atomic.AtomicInteger
open class ProxyThreadFactory : ThreadFactory {
override fun newThread(runnable: Runnable): Thread {
val mAtomicInteger = AtomicInteger(1)
return Thread(null, runnable, "Thread-" + mAtomicInteger.getAndIncrement(), -512 * 1024)
}
}
需求留意是线程栈巨细的设置需求依据详细的运用场景来进行调整。 假如线程栈巨细设置得过小,或许会导致栈溢出等问题; 假如设置得过大,或许会糟蹋过多的内存资源。 因而,在进行线程栈巨细设置时,我这边会设置一个动态的裁剪值,即便有线上问题,咱们也能够进行适当的调整,以确保程序的正常运转。
4.2 裁剪后的收益
- 经过火山引擎的APP功能分析平台比照发现,内存均匀值削减了20M
- 经过Profiler实测,发现和火山引擎检测成果相近
办法 | Total(单位:M) | Java(单位:M) | Native(单位:M) | Graphics(单位:M) | Stack(单位:M) | Code(单位:M) | Others(单位:M) |
---|---|---|---|---|---|---|---|
关闭署理 | 492.4 | 61.1 | 181.6 | 57.9 | 0.2 | 144.7 | 46.9 |
敞开署理 | 464.3 | 58.2 | 158.6 | 64.5 | 0.1 | 139 | 43.8 |
四、收益和踩坑
1. 收益
- 优化之前,线程数为197条;优化之后,线程数为152条;线程数削减了大约40条
- 优化之前,内存运用了470.93M;优化之后,内存运用了450.24M;内存削减了大约20M
- 优化之前,体系CPU运用率为34.83%;优化之后,体系CPU运用率为31.51%;体系CPU运用率下降了3%
- APP运用的流畅性:优化之前,每秒改写23.36帧;优化之后,每秒改写36.3帧;帧率均匀每秒添加了13帧。
综上所述:经过插桩署理线程池进行收敛,能有用削减线程数(削减了40条),然后削减内存的运用(削减了20M)、下降CPU运用率(下降了3%)、使得APP运用的流畅性更高(每秒均匀多改写13帧),符合优化预期。
2. 踩坑
- 网络使命线程和本地使命线程要分隔,防止网络不好的时分网络使命堵塞了本地使命
- 要相互依靠的线程池需求分隔署理或许某些不署理,防止出现由于使命排队和相互依靠导致类似“死锁”现象
- 中心线程数等于1的不要署理,由于不仅优化效果有限,还或许把占用1个线程变成占用多个线程,然后导致部分使命会常驻,占用中心线程