货拉拉技术

聊聊Android线程优化这件事

一、背景

在日常开发APP的过程中,难免需要使用第二方库和第三方库来帮助开发者快速实现一些功能,提高开发效率。但是,这些库也可能会给线程带来一定的压力,主要表现在以下几个方面:

  • 线程数量增多:一些库可能会在后台启动一些线程来执行任务,这样会增加系统中线程的数量,从而导致系统资源的浪费。

  • 线程竞争:一些库可能会在同一时间启动多个线程来执行任务,这样会导致线程之间的竞争,从而影响程序的执行效率。

  • 线程阻塞:一些库可能会在执行任务时阻塞主线程,从而导致程序的卡顿和响应速度变慢。

二、整体思路

为了解决使用第二方库和第三方库代理的线程问题,我选择用下面的思路来进行线程优化:

  1. 线程检测,评估优化空间。

  2. 线程统计,收集优化范围。

  3. 线程和线程池优化,线程数收敛。

  4. 线程栈裁剪,减少线程内存。

三、具体方案

1. 线程检测

最常见的几种获取线程信息的方式如下

Image

为了有完整的线程统计,而且能实时了解运行过程中线程数的变化,那我们就选择了读取伪文件系统里面线程信息的方式。

  1. /**

  2. * 获取所有线程信息

  3. */

  4. private fun getThreadInfoList():List<ThreadInfo>?{

  5. //获取伪文件所有的线程信息文件

  6. val file =File("/proc/self/task")

  7. ...

  8. //遍历task文件目录下

  9. for(threadDir in listFile){

  10. //读取每个目录下的status文件获取单个线程信息

  11. val statusFile =File(threadDir,"status")

  12. if(statusFile.exists()){

  13. val threadInfo =ThreadInfo()

  14. try{

  15. BufferedReader(InputStreamReader(FileInputStream(statusFile))).use{ reader ->

  16. var line:String

  17. hitFlag =0

  18. while(reader.readLine().also { line = it }!=null){

  19. if(hitFlag >2){

  20. break

  21. }

  22. //解析线程名

  23. if(line.startsWith("Name")){

  24. val name =

  25. line.substring("Name".length +1).trim { it <=' '}

  26. threadInfo.name = name

  27. hitFlag++

  28. continue

  29. }

  30. //解析线程Pid

  31. if(line.startsWith("Pid")){

  32. val pid =

  33. line.substring("Pid".length +1).trim { it <=' '}

  34. threadInfo.id = pid

  35. hitFlag++

  36. continue

  37. }

  38. //解析线程状态

  39. if(line.startsWith("State")){

  40. ...

  41. threadInfo.status = state

  42. hitFlag++

  43. }

  44. }

  45. }

  46. }catch(e:Exception){

  47. Log.e(LOG_TAG, e.toString())

  48. }

  49. threadInfoList.add(threadInfo)

  50. }

  51. }

  52. return threadInfoList

  53. }

最后只需要在APP启动后就开启轮询任务:1,获取伪文件。2,写入数据库。3,更新视图展示。

Image

统计了运行时创建的线程、可用的线程、正在运行的线程。

理想的情况就是可用的线程数应该和正在运行的线程数尽量接近,实际发现差异巨大,所以优化的空间还是蛮值得期待的。

2. 线程统计

Image

  1. ### 了解创建线程和线程池的字节码

Image

  1. ### 如何扫描到创建的线程和线程池

通过插桩的方式,来查找创建线程池和线程的类名,并把这些类名统一输出到一份txt文档。插桩的框架,我选择的是ASM,因为使用ASM进行插桩具有高效性、灵活性、易用性、兼容性和社区活跃等优点,是一种比较优秀的字节码操作框架,对于提高应用程序的性能和可维护性具有重要意义。

那么通过ASM是如何扫描到的呢?

要扫描到创建线程池的类名,你需要使用ASM的访问者模式(Visitor Pattern)来遍历字节码中的方法和指令。在遍历过程中,当遇到创建线程的指令(如:new java/util/concurrent/ThreadPoolExecutor)时,就可以获取到创建线程的类名。

  1. import org.objectweb.asm.*;

  2. publicclassThreadPoolDetectorClassVisitorextendsClassVisitor{

  3. publicThreadPoolDetectorClassVisitor(int api,ClassVisitor classVisitor){

  4. super(api, classVisitor);

  5. }

  6. @Override

  7. publicMethodVisitor visitMethod(int access,String name,String desc,String signature,String[] exceptions){

  8. MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);

  9. returnnewThreadPoolDetectorMethodVisitor(api, mv);

  10. }

  11. classThreadPoolDetectorMethodVisitorextendsMethodVisitor{

  12. publicThreadPoolDetectorMethodVisitor(int api,MethodVisitor methodVisitor){

  13. super(api, methodVisitor);

  14. methodVisitor);

  15. }

  16. @Override

  17. publicvoid visitMethodInsn(int opcode,String owner,String name,String desc,boolean itf)){

  18. if(opcode ==Opcodes.INVOKESTATIC && owner.startsWith("java/util/concurrent/Executors")){

  19. System.out.println("Detected creation of new ThreadPool!");

  20. }

  21. super.visitMethodInsn(opcode, owner, name, desc desc, itf);

  22. }

  23. }

  24. }

  1. ### 统计和分类扫描到的创建线程和线程池的类名

  • 扫描到的结果

Image

  • 结果进行分类

Image

  • 结果的用处

  1. 了解项目现状。

  2. 对后续优化可以设置白名单。

  3. 可以对线上设置的线程进行降级处理。

3. 线程和线程池优化

Image

### 3.1 线程优化

  • 对于APP业务层和自研SDK,我们检查是否真的需要直接new thread,能否用线程池代替,如果必须创建单个线程,那我们创建的时候必须加上线程名,方便排查线程问题。

  • 对于三方SDK,那就可以通过插桩来重命名(名称必须少于16个字符),方便尽快知道该线程是来自哪个SDK。

3.2 线程池优化

  • 对于APP业务层,我们需要提供常用线程池,例如I/O、CPU、Single、Cache等等线程池,避免开发各自创建重复的线程池。

  • 对于自研SDK,我们尽量让架构组的开发同学提供可以设置自定义线程池的能力,方便我们代理到我们APP业务层的线程池。

  • 对于三方SDK,首先了解有没有提供设置我们自定义线程池的接口,有的话,那就直接设置我们APP业务层的线程池。如果没有这种能力,那我们就进行插桩来进行线程池收敛。在进行三方SDK插桩代理的时候,需要注意三点:

  1. 设置白名单,进行逐步代理。

  2. 针对不同的SDK,要区分是本地任务还是网络任务,这样能明确是代理到I/O线程池还是CPU线程池。

  3. 设置降级开关,方便线上有问题时,及时对单个SDK进行降级处理。

    3.2.1 行业方案<

(1)反射收敛,但是使用反射来收敛线程池的确有一些潜在的弊端:

  • 性能开销:反射在执行时需要进行一系列的检查和解析,这会比直接的Java方法方法调用带来更大的性能开销。

  • 安全问题:反射可以访问所有的字段和方法,包括私有有的和受保护的,这可能会破坏对象的封装性,导致安全问题。

  • 代码复杂性:使用反射的代码通常比直接的Java代码更复杂,更难理解和维护。

因此,虽然反射是一种强大的工具,但在使用时需要谨慎,尽量避免不必要的使用。

(2)代理收敛,但是使用代理设计模式来收敛线程池也有一些潜在的弊端:

  • 增加复杂性:代理方式会引入额外的类和对象,这会增加系统的复杂性。对于简单的问题,使用代理可能会显得过于复杂。

  • 代码可读性:由于代理方式涉及到额外的抽象层,这可能会对代码的可读性产生一定的影响。

  • 调试困难:由于代理模式的存在,错误可能会被掩盖或者难以定位,这可能会使得调试变得更加困难。

因此,虽然代理模式是一种强大的设计模式,但在使用时也需要考虑到这些潜在的问题。

(3)协程收敛,但是使用协程收敛线程池也有一些局限性和潜在的弊端:

  • 需要依赖Kotlin协程库:使用Kotlin协程需要依赖Kotlin协程库,如果应用程序中没有使用Kotlin语言,那么需要额外引入Kotlin库,增加了应用程序的体积。

  • 协程的执行时间不能过长:Kotlin协程的执行时间不能过长,否则会影响其他协程的执行。因此,在使用Kotlin协程进行线程收敛时,需要合理控制协程的执行时间。

  • 可能会导致内存泄漏:如果协程没有正确地取消,可能会导致内存泄漏。因此,在使用Kotlin协程时,需要注意正确地取消协程。

因此,虽然Kotlin协程可以通过使用协程调度器来实现线程收敛,但是也存在一些弊端,需要开发者根据具体情况来选择是否使用。

(4)插桩收敛,虽然插桩也有一些不足之处:

  • 可能影响程序行为:如果插桩代码改变了程序的状态或者影响了线程的线程的调度,那么它可能会改变程序的行为。

  • 可能引入错误:如果插桩代码桩代码本身存在错误,那么它可能会引入新的错误到程序中。

但是这些缺点在线程池收敛的时候还是可控的,相比于上面的反射收敛、代理收敛和协程收敛来说,还有许多优点:

  • 直接性:插桩直接在代码中插入额外的逻辑,不需要通过代理或反射射间接地操作对象,这使得插桩更直接,更易于理解和控制。

  • 灵活活性:插桩可以在任何位置插入代码,,这提供了很大的灵活性。而代理和反射通常只能操作公开的接口和方法。

  • 无需修改原始代码:插桩通常常不需要常不需要修改原始的线程池代码,这使得它可以在不影响原始代码的情况下收集信息。

  • 颗粒度控制:可以对某个方法或某段代码进行线程收敛,而不是整个应用程序。

综上所述,我就选择了更加通用、灵活、精确的方式来收敛二方和三方的线程池---插桩代理。

#### 3.2.2 代码设计图

Image

#### 3.2.3 代码流程图

Image

暂时无法在飞书文档外展示此内容

3.2.4 代码实施

  1. 创建NewThreadTrackerPlugin,在插件里主要是获取到需要进行代理的线程池白名单以及注册ThreadTrackerTransform。

  1. classNewThreadTrackerPluginimplementsPlugin<Project>{

  2. @Override

  3. void apply(Project project){

  4. System.out.println("ThreadTracker:start ThreadTrackerPlugin")

  5. project.getRootProject().getSubprojects().each { subProject ->

  6. PluginUtils.addProjectName(subProject.name)

  7. PluginUtils.projectPathList.add(subProject.projectDir.toString())

  8. }

  9. org.gradle.api.plugins.ExtraPropertiesExtension ext = project.getRootProject().getExtensions().getExtraProperties()

  10. //通过配置来设置是否需要输出所有创建线程池的txt文件,文件名为"thread_tracker_XXX.txt"

  11. if(ext.has("scanProject")){

  12. boolean scan = ext.get("scanProject")

  13. PluginUtils.setScanProject(scan)

  14. System.out.println("ThreadTracker:需要扫描项目吗?"+ scan)

  15. }

  16. //通过配置来获取需要进行插桩代理的白名单

  17. if(ext.has("whiteList")){

  18. List<String> list = ext.get("whiteList")

  19. PluginUtils.addWhiteList(list)

  20. }else{

  21. System.out.println("ThreadTracker:请创建thread_tracker.gradle文件,设置whiteList白名单")

  22. }

  23. //注册ThreadTrackerTransform。

  24. //Gradle Transform 是 Android 官方提供给开发者在项目构建阶段,即由 .class 到 .dex 转换期间修改 .class 文件的一套 API。目前比较经典的应用是字节码插桩、代码注入技术。

  25. AppExtension appExtension =(AppExtension) project.getProperties().get("android")

  26. appExtension.registerTransform(newThreadTrackerTransform(),Collections.EMPTY_LIST)

  27. }

  28. }

  1. 创建 ThreadTrackerTransform,重写ThreadTrackerTransform的transform方法,在该方法里面来遍历文件目录下和Jar包中的class文件,并让ClassReader接受的是我们自定义的ThreadTrackerClassVisitor。

  1. /**

  2. * transform 方法来处理中间转换过程,主要逻辑在该方法中实现。我们可以在 transform 方法中,实现对字节码的修改、处理等操作。

  3. * @param transformInvocation

  4. */

  5. @Override

  6. void transform(@NonNullTransformInvocation transformInvocation){

  7. ...

  8. //对于一个.class文件进行Class Transformation操作,整体思路是这样的:

  9. // ClassReader --> ClassVisitor(1) --> ... --> ClassVisitor(N) --> ClassWriter

  10. ClassReader classReader =newClassReader(file.bytes)

  11. ClassWriter classWriter =newClassWriter(classReader,ClassWriter.COMPUTE_MAXS)

  12. ClassVisitor cv =newThreadTrackerClassVisitor(classWriter,null)

  13. classReader.accept(cv, EXPAND_FRAMES)

  14. byte[] code = classWriter.toByteArray()

  15. FileOutputStream fos =newFileOutputStream(

  16. file.parentFile.absolutePath +File.separator + name)

  17. fos.write(code)

  18. fos.close()

  19. ...

  20. }

  1. 创建ThreadTrackerClassVisitor,重写visitMethod来返回自定义的MethodVisitor,通过这个对象来访问方法的详细信息。

在visitMethod方法方法中,我们可以插入自己的代码,以修改或替换原有的方法声明声明。例如,我们可以改变方法的访问权限、改变方法的参数、改变方法的返回值,甚至可以完全替换原有的方法声明。

  1. @Override

  2. publicMethodVisitor visitMethod(int access0,String name0,String desc0,String signature0,String[] exceptions){

  3. MethodVisitor mv = cv.visitMethod(access0, name0, desc0, signature0, exceptions);

  4. if(filterClass(className)){

  5. return mv;

  6. }

  7. returnnewProxyThreadPoolMethodVisitor(ASM6, mv, className);

  8. }

  9. /**

  10. *。 过滤掉不需要插桩的类,比如这个插桩代码模块、自定义的线程池等等

  11. **/

  12. privateboolean filterClass(String className){

  13. 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");

  14. }

  1. 创建ProxyThreadPoolMethodVisitor,并重写它的visitMethodInsn方法来真实插桩自己的线程池。

在visitMethodInsn方法中,我们可以插入自己的代码,以修改或替换原有的方法调用。

  1. @Override

  2. publicvoid visitMethodInsn(int opcode,String owner,String name,String descriptor,boolean isInterface){

  3. //如果配置中是需要扫描App,则把创建线程池的类名全部都写在"thread_tracker_XXX.txt"里面,供开发者统计、分类、设置白名单和降级处理

  4. if(PluginUtils.getScanProject()){

  5. if(owner.equals(O_ThreadPoolExecutor)&& name.equalsIgnoreCase("<init>")){

  6. PluginUtils.writeClassNameToFile("创建ThreadPoolExecutor的类:"+ className);

  7. }

  8. }

  9. //如果配置中是需要插桩代理线程池,则把原本的类 "java/util/concurrent/ThreadPoolExecutor"换成了我们自定义的类"com/lalamove/threadtracker/proxy/BaseProxyThreadPoolExecutor"

  10. //mClassProxy只是一个总开关,是否开启代理;具体某个类是否需要代理,在创建线程池的具体地方会根据类名来判断

  11. if(mClassProxy){

  12. if(owner.equals(O_ThreadPoolExecutor)&& name.equalsIgnoreCase("<init>")){

  13. if("(IIJLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/BlockingQueue;)V".equalsIgnoreCase(descriptor)){

  14. mv.visitLdcInsn(className);

  15. mv.visitMethodInsn(opcode, O_BaseProxyThreadPoolExecutor, name,"(IIJLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/BlockingQueue;Ljava/lang/String;)V",false);

  16. }elseif("(IIJLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/BlockingQueue;Ljava/util/concurrent/ThreadFactory;)V".equalsIgnoreCase(descriptor)){

  17. mv.visitLdcInsn(className);

  18. mv.visitMethodInsn(opcode, O_BaseProxyThreadPoolExecutor, name,"(IIJLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/BlockingQueue;Ljava/util/concurrent/ThreadFactory;Ljava/lang/String;)V",false);

  19. }elseif("(IIJLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/BlockingQueue;Ljava/util/concurrent/RejectedExecutionHandler;)V".equalsIgnoreCase(descriptor)){

  20. mv.visitLdcInsn(className);

  21. mv.visitMethodInsn(opcode, O_BaseProxyThreadPoolExecutor, name,"(IIJLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/BlockingQueue;Ljava/util/concurrent/RejectedExecutionHandler;Ljava/lang/String;)V",false);

  22. }elseif("(IIJLjava/util/concurrent/TimeUnit;Ljava/util/concurrent/BlockingQueue;Ljava/util/concurrent/ThreadFactory;Ljava/util/concurrent/RejectedExecutionHandler;)V".equalsIgnoreCase(descriptor)){

  23. mv.visitLdcInsn(className);

  24. 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);

  25. }else{

  26. mv.visitMethodInsn(opcode, O_BaseProxyThreadPoolExecutor, name, descriptor,false);

  27. }

  28. return;

  29. }

  30. }

  31. super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);

  32. }

上述使用到的一些常量定义如下,也引入到了我们自己自定义的线程池。

  1. classClassConstant{

  2. //Java里面创建线程池的类名

  3. staticfinalString O_ThreadPoolExecutor ="java/util/concurrent/ThreadPoolExecutor";

  4. //自定义创建线程池的类名

  5. staticfinalString O_BaseProxyThreadPoolExecutor ="com/lalamove/threadtracker/proxy/BaseProxyThreadPoolExecutor";

  6. }

  1. 创建BaseProxyThreadPoolExecutor,重写了创建线程池的所有构造方法,也通过传入的类名判断了该类里面的线程池是否需要代理,以及代理的是的CPU密集型线程池还是IO密集型线程池。

  1. package com.lalamove.threadtracker.proxy

  2. import android.util.Log

  3. import com.lalamove.threadtracker.TrackerUtils

  4. import java.util.concurrent.*

  5. /**

  6. * ThreadPoolExecutor代理类

  7. */

  8. open classBaseProxyThreadPoolExecutor:ThreadPoolExecutor{

  9. var mProxy =true

  10. //App层自定义的IO线程池

  11. privatevar threadPoolExecutor:ThreadPoolExecutor=

  12. TrackerUtils.getProxyNetThreadPool()

  13. constructor(

  14. corePoolSize:Int,

  15. maximumPoolSize:Int,

  16. keepAliveTime:Long,

  17. unit:TimeUnit?,

  18. workQueue:BlockingQueue<Runnable>?,

  19. className:String?,

  20. ):super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue){

  21. init(corePoolSize,

  22. maximumPoolSize,

  23. keepAliveTime, className)

  24. }

  25. private fun init(

  26. corePoolSize:Int,

  27. maximumPoolSize:Int,

  28. keepAliveTime:Long,

  29. className:String?,

  30. ){

  31. //判断className下创建的线程池是否要被插桩代理

  32. if(className !=null){

  33. mProxy =TrackerUtils.isProxy(className)

  34. }

  35. //单线程暂不代理

  36. if(corePoolSize ==1||(corePoolSize ==0&& maximumPoolSize ==1)){

  37. mProxy =false

  38. }

  39. if(!mProxy){

  40. return

  41. }

  42. //设置核心线程超时允许销毁

  43. if(keepAliveTime <=0){

  44. setKeepAliveTime(10L,TimeUnit.MILLISECONDS)

  45. }

  46. allowCoreThreadTimeOut(true)

  47. //设置className的线程池被代理为CPU线程池

  48. if(className !=null&&TrackerUtils.proxyCpuClass(className)){

  49. threadPoolExecutor =TrackerUtils.getProxyCpuThreadPool()

  50. }

  51. }

  52. ...

  53. override fun submit(task:Runnable):Future<*>{

  54. returnif(mProxy) threadPoolExecutor.submit(task)elsesuper.submit(task)

  55. }

  56. override fun execute(command:Runnable){

  57. if(mProxy) threadPoolExecutor.execute(command)elsesuper.execute(command)

  58. }

  59. //注意:不能关闭,否则影响其他被代理的线程池

  60. override fun shutdown(){

  61. if(!mProxy){

  62. super.shutdown()

  63. }

  64. }

  65. //注意:不能关闭,否则影响其他被代理的线程池

  66. override fun shutdownNow():MutableList<Runnable>{

  67. val list =if(mProxy) mutableListOf<Runnable>()elsesuper.shutdownNow()

  68. return list

  69. }

  70. }

3.2.5 实施代理

  1. 在工程最外层创建thread_tracker.gradle,里面可以设置需要代理的线程池白名单。

Image

  1. 通过打印日志就能看出白名单里面的线程池是否被代理成功。

Image

  1. 设置降级开关

(1)设置每个SDK里面细分类名对应的code

Image

(2)在配置系统上设置需要关闭SDK,设置上面对应的code码即可。

Image

(3)在APP初始化的时候尽可能早的获取配置系统上的code字符串

Image

(4)在进行代理的时候,会匹配code字符串,来决定具体的线程池是否进行代理。

Image

3.2.6 代理后的收益

  • 累计减少了大约40条线程的开销

Image

Image

## 4. 线程栈裁剪

4.1 裁剪方式

创建线程的时候,线程默认的栈空间大小为 1M 左右,经过测试大部分情况下线程内执行的逻辑并不需要这么大的空间,因此线程栈空间减小,可以对内存这块有明显的优化。

Image接下来我们来看下函数FixStackSize源码,是怎么设置线程栈默认为1M的?

  1. staticsize_tFixStackSize(size_t stack_size){

  2. //参数是java层中thread 的stack_size默认0

  3. if(stack_size ==0){

  4. stack_size =Runtime::Current()->GetDefaultStackSize();

  5. }

  6. // 默认栈大小是 1M

  7. stack_size +=1* MB;

  8. //...

  9. if(Runtime::Current()->ExplicitStackOverflowChecks()){

  10. stack_size +=GetStackOverflowReservedBytes(kRuntimeISA);

  11. }else{

  12. 8k+8K

  13. stack_size +=Thread::kStackOverflowImplicitCheckSize +

  14. GetStackOverflowReservedBytes(kRuntimeISA);

  15. }

  16. //...

  17. return stack_size;

  18. }

发现函数的源码实现就是通过 stacksize += 1 * MB 来设置 stacksize 的: 如果我们传入的 stacksize 为 0 时,默认大小就是 1 M ; 如果我们传入的 stacksize 为 -512KB 时,stacksize 就会变成 512KB(1M - 512KB)。 那我们是不是只用带有 stacksize 入参的构造函数去创建线程,并且设置 stacksize 为 -512KB 就行了呢? 应用中创建线程的地方太多很难一一修改,前面我们已经将应用中的线程部分收敛到自定义的线程池中去了,所以只需要修改自定义线程池中创建的线程方式即可。在我们自定义的 ThreadFactory 中,创建 stacksize 为 - 512 KB 的线程,这么一个简单的操作就能减少线程所占用的虚拟内存。

  1. package com.lalamove.threadtracker.proxy

  2. import java.util.concurrent.ThreadFactory

  3. import java.util.concurrent.atomic.AtomicInteger

  4. open classProxyThreadFactory:ThreadFactory{

  5. override fun newThread(runnable:Runnable):Thread{

  6. val mAtomicInteger =AtomicInteger(1)

  7. returnThread(null, runnable,"Thread-"+ mAtomicInteger.getAndIncrement(),-512*1024)

  8. }

  9. }

需要注意是线程栈大小的设置需要根据具体的应用场景来进行调整。 如果线程栈大小设置得过小,可能会导致栈溢出等问题; 如果设置得过大,可能会浪费过多的内存资源。 因此,在进行线程栈大小设置时,我这边会设置一个动态的裁剪值,即使有线上问题,我们也可以进行适当的调整,以保证程序的正常运行。

### 4.2 裁剪后的收益

  • 通过火山引擎的APP性能分析平台对比发现,内存平均值减少了20M

Image

  • 通过Profiler实测,发现和火山引擎检测结果相近

方式Total(单位:M)Java(单位:M)Native(单位:M)Graphics(单位:M)Stack(单位:M)Code(单位:M)Others(单位:M)
关闭代理492.461.1181.657.90.2144.746.9
开启代理464.358.2158.664.50.113943.8

Image

Image

四、收益和踩坑

## 1. 收益

  • 优化之前,线程数为197条;优化之后,线程数为152条;线程数减少了大约40条

  • 优化之前,内存使用了470.93M;优化之后,内存使用了450.24M;内存减少了大约20M

  • 优化之前,系统CPU使用率为34.83%;优化之后,系统CPU使用率为31.51%;系统CPU使用率降低了3%

Image

  • APP使用的流畅性:优化之前,每秒刷新23.36帧;优化之后,每秒刷新36.3帧;帧率平均每秒增加了13帧。

Image

综上所述:通过插桩代理线程池进行收敛,能有效减少线程数(减少了40条),从而减少内存的使用(减少了20M)、降低CPU使用率(降低了3%)、使得APP使用的流畅性更高(每秒平均多刷新13帧),符合优化预期。

2. 踩坑

  • 网络任务线程和本地任务线程要分开,避免网络不好的时候网络任务堵塞了本地任务

  • 要相互依赖的线程池需要分开代理或者某些不代理,避免出现因为任务排队和互相依赖导致类似“死锁”现象

  • 核心线程数等于1的不要代理,因为不仅优化效果有限,还可能把占用1个线程变成占用多个线程,从而导致部分任务会常驻,占用核心线程