当前位置:首页 > 谈天说地

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

34资源网2022-09-19247

背景

在我们日常开发中,多线程管理一直是非常头疼的问题之一,尤其在历史性长,结构复杂的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线程监控线程统一的资料请关注萬仟网其它相关文章!

看完文章,还可以扫描下面的二维码下载快手极速版领4元红包

快手极速版二维码

快手极速版新人见面礼

除了扫码领红包之外,大家还可以在快手极速版做签到,看视频,做任务,参与抽奖,邀请好友赚钱)。

邀请两个好友奖最高196元,如下图所示:

快手极速版邀请好友奖励

扫描二维码推送至手机访问。

版权声明:本文由34楼发布,如需转载请注明出处。

本文链接:https://www.34l.com/post/22280.html

分享给朋友:

相关文章

​京东钢镚怎么使用?教你用京东钢镚的支付的方法教程

​京东钢镚怎么使用?教你用京东钢镚的支付的方法教程

京东钢镚怎么使用?很多朋友看到自己的京东钱包里面有京东钢镚,不知道京东钢镚怎么使用,在使用京东钢镚之前,一定要要先了解清楚京东钢镚使用条件有哪些,下面开淘小编来介绍一下京东钢镚怎么使用?如何用你的京东钢镚支付购买商品的方法教程分享。…

好想你花开富贵大礼包多少钱一提?1236g优惠价只需要43.90元

好想你花开富贵大礼包多少钱一提?1236g优惠价只需要43.90元

好想你花开富贵大礼包多少钱一提?好想你,花开富贵礼盒1236g零食大礼包,原价58.90元,领取15元的优惠券减掉之后只需要43.90元,这个价格在近30日内属于最低价,喜欢这款商品的朋友不要错过哦。…

调整心态温暖哲理经典语录,看这些语录能够调整心态

调整心态温暖哲理经典语录,看这些语录能够调整心态

取得成功是他人不成功时仍在坚持不懈。若自身不作出一点外貌,别人想拉你一把都不知道你的手在哪儿。直至你不会再找我聊,直至你找不着我,直至最终,你一直在某一瞬间猛地想起我。但是,那个时候,被你弄丢的我就确实早已没有了,也再不想要你再找回家了。理…

联想乐pad平板电脑有哪些版本(平板电脑排行榜性价比)

联想乐pad平板电脑有哪些版本(平板电脑排行榜性价比)

对于安卓平板电脑行业而言,创新相对来说比较困难。一方面,安卓系统的功能早已经被各方挖掘殆尽;另一方面,从整个平板电脑行业大环境来看,iPad阵营坚固的护城河也影响着安卓阵营的创新欲望。再加上时下热衷于投身平板电脑行业的品牌本来就不像以往那么…

融资丨「大湾生物」完成近千万美元A轮融资,比邻星创投及高瓴创投共同领投

融资丨「大湾生物」完成近千万美元A轮融资,比邻星创投及高瓴创投共同领投

创业邦获悉,近日,大湾生物有限公司(以下简称:大湾生物)宣布完成近千万美元A轮融资,由比邻星创投与高瓴创投共同领投,阿隆资本跟投以及阿里巴巴香港创业者基金等现有投资者追加投资。本轮融资将加快大湾生物全球创新的三大人工智能平台,分别是智能化细…

一场关于元宇宙公司之死的剧本杀

一场关于元宇宙公司之死的剧本杀

编者按:本文来自脑极体,创业邦经授权发布。 2021年,被称作元宇宙元年。这种结合了区块链、虚拟现实、增强现实多种技术的概念,据称能够提供社交、娱乐、电商多种功能。美国彭博社称,元宇宙的市场规模将在2024年将达到8000亿美元。而就在20…