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线程监控线程统一的资料请关注我们其它相关文章!

时间: 2022-09-15

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

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

Android实现多线程断点续传

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

Android&nbsp;线程优化知识点学习

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

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

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

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 在子线程中更新UI的几种方法示例

本文介绍了Android 在子线程中更新UI的几种方法示例,分享给大家,具体如下: 方式一:Handler和Message ① 实例化一个Handler并重写handlerMessage()方法 private Handler handler = newHandler() { public void handleMessage(Message msg) { // 处理消息 super.handleMessage(msg); switch (msg.what) { case 1: button1.

Android 开发中线程的分析

Android 开发中线程的分析 今天早上把公司给的任务做完了之后,突然就有点无聊,于是,把以前学的那些东西翻了翻,博客看了看,就看到一个关于线程的博客,有了很大的争议,我也差点误解了(感觉高大上~~~).整体代码差不多是这样: package sw.angel.thread; import android.app.Activity; import android.os.Bundle; import android.os.Handler; import android.util.Log; pub

Android 中不用线程如何实现倒计时

需求: 有多个组件可以开启倒计时,正常情况下默认倒计时时间终了后更新UI,另,用户可以取消指定倒计时. 这里使用CountDownTimer进行倒计时,其中回调函数onFinish是在倒计时终了时回调,onTick是在倒计时开始时回调,用户可以使用CountDownTimer对象的cancel方法取消倒计时. 这样做的好处:不需要使用繁琐的线程去控制倒计时,更方便的进行UI更新. 上代码: MainActivity package test.demo.countdowntest; import

Android编程自定义线程池与用法示例

本文实例讲述了Android编程自定义线程池与用法.分享给大家供大家参考,具体如下: 一.概述: 1.因为线程池是固定不变的,所以使用了单例模式 2.定义了两个线程池,长的与短的,分别用于不同的地方.因为使用了单例模式,所以定义两个. 3.定义了两个方法,执行的与取消的 二.代码: /** * @描述 线程管理池 * @项目名称 App_Shop * @包名 com.android.shop.manager * @类名 ThreadManager * @author chenlin * @dat

Android如何调整线程调用栈大小

在常规的Android开发过程中,随着业务逻辑越来越复杂,调用栈可能会越来越深,难免会遇到调用栈越界的情况,这种情况下,就需要调整线程栈的大小. 当然,主要还是增大线程栈大小,尤其是存在jni调用的情况下,C++层的栈开销有时候是非常恐怖的,比如说递归调用. 这就需要分三种情况,主线程,自定义线程池,AsyncTask. 主线程的线程栈是没有办法进行修改的,这个没办法处理. 针对线程池的情况,需要在创建线程的时候,调用构造函数 public Thread(@RecentlyNullable Th

详解Android中用于线程处理的AsyncTask类的用法及源码

为什么要用AsyncTask 我们写App都有一个原则,主线程不能够运行需要占用大量CPU时间片的任务,如大量复杂的浮点运算,较大的磁盘IO操作,网络socket等,这些都会导致我们的主线程对用户的响应变得迟钝,甚至ANR,这些会使应用的用户体验变差,但是有时又的确需要执行这些耗时的任务,那么我们通常可以使用AsyncTask或者new Thread 来处理,这样把任务放入工作线程中执行,不会占用主线程的时间片,所以主线程会及时响应用户的操作,如果使用new Thread来执行任务,那么如果需要

详解Android进程和线程

写在前面的话 一个Android应用就是一个Linux进程,每个应用在各自的进程中运行,互不干扰,比较安全. 一个应用对应一个主线程,就是通常所说的UI线程,android遵守的就是单线程模型,所以说Ui操作不是线程安全的并且这些操作必须在UI线程中执行. 本文是对官方文档的翻译,原文链接:https://developer.android.com/guide/components/processes-and-threads.html 概述 当某个应用组件启动且该应用没有运行其他任何组件时,An

完全解析Android多线程中线程池ThreadPool的原理和使用

前言对于多线程,大家应该很熟悉.但是,大家了解线程池吗?今天,我将带大家全部学习关于线程池的所有知识. 目录 1. 简介 2. 工作原理 2.1 核心参数线程池中有6个核心参数,具体如下 上述6个参数的配置 决定了 线程池的功能,具体设置时机 = 创建 线程池类对象时 传入 ThreadPoolExecutor类 = 线程池的真正实现类 开发者可根据不同需求 配置核心参数,从而实现自定义线程池 // 创建线程池对象如下 // 通过 构造方法 配置核心参数 Executor executor =

Android App在线程中创建handler的方法讲解

相关概念 1.Handler:可以看做是一个工具类,用来向消息队列中插入消息的; 2.Thread:所有与Handler相关的功能都是与Thread密不可分的,Handler会与创建时所在的线程绑定; 3.Message:消息; 4.MessageQueue:消息队列,对消息进行管理,实现了一个Message链表; 5.Looper:消息循环,从MessageQueue中取出Message进行处理: 6.HandlerThread:继承Thread,实例化时自动创建Looper对象,实现一个消息

深入Android Handler与线程间通信ITC的详解

在<Android Handler之消息循环的深入解析>中谈到了Handler是用于操作线程内部的消息队列,所以Handler可以用来线程间通信ITC,这种方式更加安全和高效,可以大大减少同步的烦恼,甚至都可以不用syncrhonized.线程间通讯ITC正常情况下函数调用栈都会生存在同一个线程内,想要把执行逻辑交换到其他线程可以新建一个Thread,然后start().另外一种方法就是用ITC,也即用消息队列来实现,线程需要把执行逻辑交到其他线程时就向另外的线程的消息队列发送一个消息,发送消