Android性能优化之线程监控与线程统一详解

目录
  • 背景
  • 常规解决方案
    • 线程监控
      • 当前线程统计
      • 线程信息具体化
    • 线程统一
      • Thread创建
    • 注意
  • 总结

背景

在我们日常开发中,多线程管理一直是非常头疼的问题之一,尤其在历史性长,结构复杂的app中,线程数会达到好几百个甚至更多,然而过多的线程不仅仅带来了内存上的消耗同时也降低了cpu调度的效率,过多的cpu调度带来的消耗的坏处甚至超过了多线程带来的好处。

在我们日常开发中,通常会遇到以下几个问题

  • 某个场景会创造过多的线程,最终导致oom
  • 线程池过多问题,比如三方库有一套线程池,自己项目也有一套线程池,随着三方/二方业务接入,导致了不相兼容的线程池数越多,降低了全体线程池数的调度效率,比如多个okhttp的调用
  • 历史原因导致,new Thread横行,又或者是各种线程使用不规范,导致工程混乱
  • 即使是空闲时候,依旧有线程在不断Waiting
  • 各种线程死锁问题

最终种种原因导致,我们的项目在上线过程中,会遇到各种线程不明的情况,对排查问题或者解决问题带来极大的考验。

常规解决方案

对于上述问题的解决,许多团队通过codeview去限制代码准入,比如定制Thread的规范,又或者是定义项目统一的线程池,在项目中去使用。这个方案优点就是可操作性强,便于团队去实施,但是这比较依靠review(或者其他代码扫描插件),对于历史项目来说比较容易出现疏漏,而且后期也依旧需要维护,对于大型团队来说,需要兼顾所有人代码,且三方库无法处理。同时Thread的衍生物也有很多,比如Android中的HandlerThread等等,也是线程。

现在比较流行的方案是通过字节码插桩的方式,统一做线程监控亦或进行线程统一,比如监控处理的matrix,还有优化相关的booster等。线程统一这个依靠项目的情况,会有全统一线程池的情况(所以共用一个线程池),也有统一某单一业务的线程池的情况(比如只收口项目okhttp的线程池)下面我们围绕这两个主题,分别进行探讨

线程监控

当前线程统计

对线程的监控,首先我们要统计当前的信息对不对,可以直接通过

Thread.getAllStackTraces()

获取到当前所有thread的信息与堆栈情况,其返回值是一个map对象,

Map<Thread, StackTraceElement[]>

获取结果例子如下

[Thread[Binder:30506_2,5,main], Thread[FinalizerWatchdogDaemon,5,system], Thread[Binder:30506_3,5,main], Thread[Jit thread pool worker thread 0,5,system], Thread[ReferenceQueueDaemon,5,system], Thread[Profile Saver,5,system], Thread[main,5,main], Thread[Binder:30506_1,5,main], Thread[RenderThread,7,main], Thread[pika_thread,5,main], Thread[vivo.PerfThread,5,main], Thread[Signal Catcher,10,system], Thread[FinalizerDaemon,5,system], Thread[HeapTaskDaemon,5,system]]

我们可以看到key是一个thread对象,如果我们要设计一个自己的apm的话可以通过遍历key拿到一个Thread对象,然后再通过该Thread对象拿到自身的信息即可,比如获取thread的名称

Thread.getAllStackTraces().keys.map {
    it.name
}

线程信息具体化

通过上述,我们可以拿到了当前所有的线程信息,但是很遗憾的是,其中有一些线程信息几乎是“不可用”的,比如我们用new Thread构建出来的线程,如果不给它指定的名字的话,默认就会出现类似这种情,比如Thread-1,这种名称的线程对我们来说几乎是没有任何意义的,我们暂且把它称为“匿名线程”,解决匿名线程的手段有很多,之前在学完ASM Tree api,再也不怕hook了这篇我们可以看到,我们可以用asm对调用thread进行插桩,通过改变指令调用函数,把普通的空参数Thread()方法变成带有name的构造方法Thread(String)进行hook处理,把调用者名称的信息放到前置的ldc指令,从而到达一个转化的效果。

转化前Thread构造函数 转化后Thread构造函数
Thread() Thread(String)
Thread(Runnable) Thread(Runnable, String)
Thread(ThreadGroup, Runnable) Thread(ThreadGroup, Runnable, String)
... ...

asm 代码实例如下

method.instructions.insertBefore(
        node,
        new LdcInsnNode(klass.name)
)
def r = node.desc.lastIndexOf(')')
把构造函数描述变成了带有string name的构造函数描述
def desc =
 "${node.desc.substring(0, r)}Ljava/lang/String;${node.desc.substring(r)}"
println(" * ${node.owner}.${node.name}${node.desc} => ${node.owner}.${node.name}$desc: ${klass.name}.${method.name}${method.desc}")
node.desc = desc

当然,Thread还有很多构造函数,我们就不一一举例子去适配,相关的操作也是类似的,涉及到Executors等其他创建线程的方式,我们也可以通过这种指令替换的方式去进行Thread的命名操作。这里就不再赘述,可以参考booster 的做法

线程统一

线程的统一可以依靠项目统一的线程池,但是这个约束不到第三方,我们可以利用ASM等工具进行线程的统一,线程统一包括全模块统一跟单模块统一(特定模块),由于单模块统一涉及具体业务,比如对okhttpclient的调度线程统一,由于不具备通用性,需要根据模块具体实现去统一,我们这里就不讨论了,单模块统一有个好处就是风险低,只影响单一模块的线程调度。我们讨论一下全模块的统一。

在项目中,我们有各种各样的线程调度api,直接new Thread,Executors,ThreadPoolExecutor等等,它们公共点就是都用到了Thread,最终都是靠着Thread去运行,但是想要把它们统一起来,我们要兼顾更上一层的api,那么适配工作量可是不少!!那么我们有没有一种黑科技,能够简单点就把线程统一到一个特定的线程池,作为收口呢?(注意这里讨论的是把全项目的线程统一,包括三方库),为了找到突破点,我们先看一下最基本的Thread是怎么创建出来的

Thread创建

最常用的Thread创建肯定是最简单的,我们举个例子

var thread = Thread{
    Log.i("hello","this is my thread ${Thread.currentThread().name}")
}

那么这段代码它做了什么呢?我们要从字节码的角度去分析,才能找到突破点

    NEW java/lang/Thread
    DUP
    INVOKEDYNAMIC run()Ljava/lang/Runnable; [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
      // arguments:
      ()V,
      // handle kind 0x6 : INVOKESTATIC
      com/example/spider/MainActivity.onCreate$lambda-0()V,
      ()V
    ]
    INVOKESPECIAL java/lang/Thread.<init> (Ljava/lang/Runnable;)V
    ASTORE 2

我们来一一说明下调用的指令:

  • NEW 创建一个java/lang/Thread对象,此时只是引用被创建,所引用的对象还没有创建,并加入操作数栈顶部

2. DUP 将操作数栈顶部的参数复制一份,并加入操作数栈

3.INVOKEDYNAMIC lambad用到的函数调用指令,运行时绑定信息,()Ljava/lang/Runnable,由于入参为null,所以不消耗操作数栈的参数,返回值是Runnable,所以会在操作数栈上新加入一个Runnable对象

4.INVOKESPECIAL 构造函数能调用到的特殊指令,即创建一个对象,(Ljava/lang/Runnable;)V,我们看到入参只有一个Runnable对象,但是实际上调用INVOKESPECIAL的构造函数隐藏了一个条件,就是需要一个被创建对象对应的引用对象,这就是dup存在的原因,因为需要消耗一个Thread引用对象!这点需要注意

5.ASTORE 2,就是把操作数栈顶部的变量放到了局部变量表index为2的地方,这里为什么是2呢,是由当前运行环境决定的,静态方法中index为0的就是参数1,而普通方法index为0的地方却是this指针,这点是需要注意的,除了index = 0 的地方有这个约定,其他index下标其实就是函数环境的决定的。(这也侧面说明,存在AStore,ALoad这些指令的时候,我们很难去做通用性插桩,因为这里依赖了局部变量表的具体实现)

看到这里,我们就能够明白了一个Thread创建的字节码是怎么样的了

那么我们想想看,怎么达到我们统一线程池的目的。看到Thread的创建过程我们就知道,Thread会依赖局部变量表(第5条),所以我们如果直接对Thread进行操作的话,是不行的,因为局部变量表的存储index是依靠当前环境的!其实我们统一线程池,想要统一的也不一定是要统一Thread,而是统一Runnable执行的线程环境对吧!突破点就来了,我们对Runnable进行操作,把其原本依赖执行的Thread变成我们自己线程池的Thread是不是就可以了!

目标明确了,但是我们也需要为此做一些特定的处理,因为这种自定义指令集的处理,用其他ASM工具也是无法生成的,所以我们才具体解释相关的指令集。最终这边的方案就是,进行Thread调用替换,即把new Thread这个指令,替换为我们自己的MyThread的指令进行定制化处理。步骤如下

  • 替换原本的INVOKESPECIAL指令调用为我们自己的MyThread调用,这里给出MyThread实现
class MyThread(private val runnable: Runnable) : Thread(runnable) {
   // 调用到自己的start
   override fun start() {
       Log.i("hello", "MyThread")
       // runnable 在定义的统一线程池执行
       ThreadHelper.runInCustomPool(runnable)
   }
}

  • 原本指令返回的是Thread,由于我们替换为了MyThread,那么原本跟Thread强绑定的NEW指令,DUP指令就也需要变更跟MyThread类型相关的指令,我们这里就不采用替换,采取新加的方式(替换也可以,这里选择方便处理,因为操作数只对栈顶元素生效)

3.到了这一步,还不行,因为我们原本要返回的是Thread对象,现在变成了MyThread对象,所以我们需要一个转化指令CHECKCAST

我们给出具体的ASM代码

class MyThreadHookUtils {
    static THREAD = "java/lang/Thread"
    static void transform(ClassNode klass) {
        // 我们自定义的MyThread类不需要参加转化
        if (klass.name.equals("com/example/spider/MyThread")) {
            return
        }
        klass.methods?.forEach { methodNode ->
            methodNode.instructions.each {
                if (it.opcode == Opcodes.INVOKESPECIAL) {
                    transformInvokeSpecial((MethodInsnNode) it, klass, methodNode)
                }
            }
        }
    }
    private static void transformInvokeSpecial(MethodInsnNode node, ClassNode klass, MethodNode method) {
        // 如果不是构造函数,就直接退出
        if (node.owner != THREAD) {
            return
        }
        println("transformInvokeSpecial")
        transformThreadInvokeSpecial(node, klass, method)
    }
    private static void transformThreadInvokeSpecial(
            MethodInsnNode node,
            ClassNode klass,
            MethodNode method
    ) {
        println("init  ===>  " + node.desc + " " + node.owner)
        if (node.desc.equals("(Ljava/lang/Runnable;)V")) {
            int index = method.instructions.indexOf(node)
            def dyc = method.instructions[index - 1]
            InsnList insertNodes1 = new InsnList()
            TypeInsnNode newInsnNode = new TypeInsnNode(Opcodes.NEW, "com/example/spider/MyThread")
            InsnNode dupNode = new InsnNode(Opcodes.DUP)
            insertNodes1.add(newInsnNode)
            insertNodes1.add(dupNode)
            method.instructions.insertBefore(dyc, insertNodes1)
            MethodInsnNode methodHookNode = new MethodInsnNode(Opcodes.INVOKESPECIAL,
                    "com/example/spider/MyThread",
                    "<init>",
                    "(Ljava/lang/Runnable;)V",
                    false)
            TypeInsnNode typeInsnNode = new TypeInsnNode(Opcodes.CHECKCAST, "java/lang/Thread")
            InsnList insertNodes = new InsnList()
            insertNodes.add(methodHookNode)
            insertNodes.add(typeInsnNode)
            method.instructions.insertBefore(node, insertNodes)
            method.instructions.remove(node)
            println("hook  ===>  " + node.name + " " + node.owner + " " + method.instructions.indexOf(node))
        }
    }
}

这个时候,任何Thread的start方法或者其他方法,都会调用到我们自定义的MyThread类的方法里面,在这里做线程池统一的处理,就非常方便了,因为我们有Runnable对象!同时所以方法我们都可以随意去玩了!

注意

注意的是,这种全局Thread插桩是有风险的,在实际项目中,我们会通过白名单的方式,选择性的去统一部分Thread,因为全局统一容易导致不可预期的问题。同时还有一个非常注意的点,我们可以看到上面关于指令的代码全部是基于index的去定位各种指令集的,NEW -> DUP ->INVOKEDYNAMIC ->INVOKESPECIAL 然而在真实项目中,这个指令集顺序不一定可靠,因为可能会被插入其他指令或者无关指令,所以我们还有一步就是指令顺序的校验,必须是满足NEW -> DUP ->INVOKEDYNAMIC ->INVOKESPECIAL这几个顺序的函数指令集才进行插桩,这部分内容比较简单,就不列举了,比较INSN指令的OpCode即可,校验规则按照项目实际需要。

总结

看到这里,我们对Thread应该有了足够的了解,同时本篇也介绍了ASM相关黑科技操作在Thread类的使用!更多关于Android线程监控线程统一的资料请关注我们其它相关文章!

(0)

相关推荐

  • Android性能优化之plt hook与native线程监控详解

    目录 背景 native 线程创建 PLT PLT Hook xhook bhook plt hook总结 背景 我们在android超级优化-线程监控与线程统一可以知道,我们能够通过asm插桩的方式,进行了线程的监控与线程的统一,通过一系列的黑科技,我们能够将项目中的线程控制在一个非常可观的水平,但是这个只局限在java层线程的控制,如果我们项目中存在着native库,或者存在着很多其他so库,那么native层的线程我们就没办法通过ASM或者其他字节码手段去监控了,但是并不是就没有办法,还有

  • Android Dispatchers.IO线程池深入刨析

    目录 一. Dispatchers.IO 1.Dispatchers.IO 2.DefaultScheduler类 3.LimitingDispatcher类 4.ExperimentalCoroutineDispatcher类 二.CoroutineScheduler类 1.CoroutineScheduler类的继承关系 2.CoroutineScheduler类的全局变量 三.Worker类与WorkerState类 1.WorkerState类 2.Worker类的继承关系与全局变量 3

  • Android 线程优化知识点学习

    目录 前言 一.线程调度原理解析 线程调度的原理 线程调度模型 Android 的线程调度 线程调度小结 二.Android 异步方式汇总 Thread HandlerThread IntentService AsyncTask 线程池 RxJava 三.Android线程优化实战 线程使用准则 线程池优化实战 四.定位线程创建者 如何确定线程创建者 Epic实战 五.优雅实现线程收敛 线程收敛常规方案 基础库如何使用线程 基础库优雅使用线程 前言 在实际项目开发中会频繁的用到线程,线程使用起来

  • Android实现多线程断点续传

    本文实例为大家分享了Android实现多线程断点续传的具体代码,供大家参考,具体内容如下 多线程下载涉及到的知识点: 1.Service的使用:我们在Service中去下载文件:2.Thread的使用:Service本身不支持耗时操作,所以我们要去开启线程:3.Sqlite的使用:使用数据库来存储每个线程下载的文件的进度,和文件的下载情况:4.权限:涉及到文件的读写就要用到权限:5.BroadCastReceiver的使用:通过广播来更新下载进度:6.线程池使用:使用线程池来管理线程,减少资源的

  • android原生实现多线程断点续传功能

    本文实例为大家分享了android实现多线程断点续传功能的具体代码,供大家参考,具体内容如下 需求描述: 输入一个下载地址,和要启动的线程数量,点击下载 利用多线程将文件下载到手机端,支持 断点续传. 在前两章的java 多线程的从基础上进行 效果展示 示例代码: 布局 activity_main.xml <?xml version="1.0" encoding="utf-8"?> <android.support.constraint.Const

  • Android在Sqlite3中的应用及多线程使用数据库的建议

    1.首先先建立一个DatabaseHelper类,继承自SQLiteOpenHelper,用于建立数据库,并可更新数据库例如我新建了两张表 public class DatabaseHelper extends SQLiteOpenHelper { private static DatabaseHelper _databaseHelper; private static final String TAG = DatabaseHelper.class.getSimpleName(); public

  • Android启动优化之延时加载的步骤详解

    前言 在应用启动的时候,为了加快启动速度,往往需要把一些比较重的操作放到子线程中,或者是延时加载.将任务放在子线程中是一个比较简单并且看起来有效的操作,但是呢,也不能太过于依赖子线程,它虽然不会阻塞主线程,但是却会跟主线程抢占CPU,当子线程很多并且任务很重的时候,也还是会拖慢主线程的,不信你可以打出Systrace看一下.延时加载也是一个比较好的策略,但难点就在于延时多久,这个时间并不好掌控. 下面话不多说了,来一起看看详细的介绍吧 IdleHandler 以前一直在想Android为什么不在

  • MySQL性能优化配置参数之thread_cache和table_cache详解

    一.THREAD_CACHE MySQL里面为了提高客户端请求创建连接过程的性能,提供了一个连接池也就是 Thread_Cache池,将空闲的连接线程放在连接池中,而不是立即销毁.这样的好处就是,当又有一个新的请求的时候,mysql不会立即去创建连接 线程,而是先去Thread_Cache中去查找空闲的连接线程,如果存在则直接使用,不存在才创建新的连接线程. 有关Thread_Cache在MySQL有几个重要的参数,简单介绍如下: thread_cache_size Thread_Cache 中

  • Android性能优化之线程监控与线程统一详解

    目录 背景 常规解决方案 线程监控 当前线程统计 线程信息具体化 线程统一 Thread创建 注意 总结 背景 在我们日常开发中,多线程管理一直是非常头疼的问题之一,尤其在历史性长,结构复杂的app中,线程数会达到好几百个甚至更多,然而过多的线程不仅仅带来了内存上的消耗同时也降低了cpu调度的效率,过多的cpu调度带来的消耗的坏处甚至超过了多线程带来的好处. 在我们日常开发中,通常会遇到以下几个问题 某个场景会创造过多的线程,最终导致oom 线程池过多问题,比如三方库有一套线程池,自己项目也有一

  • Android性能优化死锁监控知识点详解

    目录 前言 死锁检测 线程Block状态 获取当前线程所请求的锁 通过锁获取当前持有的线程 线程启动 nativePeer 与 native Thread tid 与java Thread tid dlsym与调用 系统限制 死锁检测所有代码 总结 前言 “死锁”,这个从接触程序开发的时候就会经常听到的词,它其实也可以被称为一种“艺术”,即互斥资源访问循环的艺术,在Android中,如果主线程产生死锁,那么通常会以ANR结束app的生命周期,如果是两个子线程的死锁,那么就会白白浪费cpu的调度资

  • Android性能优化方法

    GPU过度绘制 •打开开发者选型,"调试GPU过度绘制",蓝.绿.粉红.红,过度绘制依次加深  •粉红色尽量优化,界面尽量保持蓝绿颜色  •红色肯定是有问题的,不能忍受 使用HierarchyView分析布局层级 •删除多个全屏背景:应用中不可见的背景,将其删除掉  •优化ImageView:对于先绘制了一个背景,然后在其上绘制了图片的,9-patch格式的背景图中间拉伸部分设置为透明的,Android 2D渲染引擎会优化9-patch图中的透明像素.这个简单的修改可以消除头像上的过度

  • Android性能优化之ANR问题定位分析

    目录 前言 1 ANR原因总结 1.1 KeyDispatchTimeout 1.2 BroadCastTimeout 1.3 ServiceTimeout 1.4 ContentProviderTimeout 2 ANR问题解决 2.1 线下问题解决 2.2 线上问题解决 2.2.1 Bugly 2.2.2 FileObserver 2.2.3 WatchDog 前言 ANR(Application Not Response)应用程序未响应,当主线程被阻塞时,就会弹出如下弹窗 要么关闭当前ap

  • Android性能优化系列篇UI优化

    目录 前言 一.UI优化 1.1 系统做的优化 1.1.1 硬件加速 1.2 优化方案 1.2.1 java代码布局 1.2.2 View重用 1.2.3 异步创建view 1.2.4 xml布局优化 1.2.5 异步布局框架Litho 1.2.6 屏幕适配 1.2.7 Flutter 1.2.8 Jetpack Compose 1.3 工具篇 1.3.1 Choreographer 1.3.2 LayoutInspector/Android Device Monitor 1.3.3 Systr

  • Android性能优化之JVMTI与内存分配

    目录 前言 JVMTI JVMTI 简介: native层开启jvmti 前置准备 复写Agent 开启jvmtiCapabilities 设置jvmtiEventCallbacks 开启监听 java层开启agent 验证分配数据 总结 前言 内存治理一直是每个开发者最关心的问题,我们在日常开发中会遇到各种各样的内存问题,比如OOM,内存泄露,内存抖动等等,这些问题都有以下共性: 难发现,内存问题一般很难发现,业务开发中关系系数更少 治理困难,内存问题治理困难,比如oom,往往堆栈只是压死骆驼

  • Android性能优化以及数据优化方法

    Android性能优化-布局优化 今天,继续Android性能优化 一 编码细节优化. 编码细节,对于程序的运行效率也是有很多的影响的.今天这篇主题由于技术能力有限,所以也不敢在深层去和大家分享.我将这篇主题分为以下几个小节: (1)缓存 (2)数据 (3)延迟加载和优先加载 1> 缓存 在Android中缓存可以用在很多的地方:对象.IO.网络.DB等等..对象缓存能减少内存分配,IO缓存能对磁盘的读写访问,网络缓存能减少对网络的访问,DB缓存能减少对数据库的操作. 缓存针对的场景在Andro

随机推荐