当前位置: 首页 > news >正文

Android方法耗时监控插件开发

需求:自定义一个Gradle插件,这个Gradle插件可以统计方法的耗时,并当方法耗时超过阈值时,可以通过打印Log日志在控制台,然后可以通过Log定位到耗时方法的位置,帮助我们找出耗时方法和当前线程名,并且可以过滤指定包名的类进行插桩和不插桩。
技术栈:ASM + Transform Action

ASM

ASM是一个通用的Java字节码操作和分析框架,它提供了一个简单易用的字节码操作方法,可以直接以二进制的形式修改现有类或动态生成类。简单地说,ASM就是一个字节码操作框架,通过ASM,我们可以凭空生成一个类或者修改现有的类,Asm相比其他的字节码操作框架如Javasist、AspectJ等的优点就是体积小、性能好、效率高。但它的缺点就是学习成本高,不过现在已经有Android Studio插件ASM Bytecode Outline可以替我们自动的生成Asm代码。
ASM中有两类api,一种是基于树模型的tree api,一种是基于访问者模式的visitor api,这里主要使用的visitor api,在visitor api中有三个主要的类用于读取、访问和生成class字节码:

  • classVisitor:它是用于访问class字节码,它里面有很多visitXX方法,每调用一个visitXX方法,就表示你在访问class文件的某个结构,如Method、Field、Annotation等
  • classReader:用于读取以字节数组形式给出的class字节码,它有一个accept方法,用于接收一个classVisitor实例,accept方法内部会调用ClassVisitor的visitXX方法来访问已读取的class文件
  • classWriter:它继承自ClassVisitor,可以以二进制形式生成class字节码,它有一个toByteArray方法,可以把已生成的二进制形式的class字节码转换成字节数组形式返回

classVisitor,classReader,classWriter这三个之间是可以组合使用的。

需要在build.gradle中引入ASM

dependencies {implementation 'com.android.tools.build:gradle:8.6.1'//核心api,提供visitor apiimplementation 'org.ow2.asm:asm:9.6'//可选,提供了一些基于核心api的预定义类转换器implementation 'org.ow2.asm:asm-commons:9.6'//可选,提供了一些基于核心api的工具类implementation 'org.ow2.asm:asm-util:9.6'
}

读取、访问一个类

ClassVisitor的主要结构如下:

public abstract class ClassVisitor {//ASM的版本, 版本数值定义在Opcodes接口中,最低为ASM4,目前最新为ASM7protected final int api;//委托的ClassVisitor,可传空protected ClassVisitor cv;public ClassVisitor(final int api) {this(api, null);}public ClassVisitor(final int api, final ClassVisitor cv) {//...this.api = api;this.cv = cv;}//表示开始访问这个类public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {if (cv != null) {cv.visit(version, access, name, signature, superName, interfaces);}}//表示访问这个类的源文件名(如果有的话)public void visitSource(String source, String debug) {if (cv != null) {cv.visitSource(source, debug);}}//表示访问这个类的外部类(如果有的话)public void visitOuterClass(String owner, String name, String desc) {if (cv != null) {cv.visitOuterClass(owner, name, desc);}}//表示访问这个类的注解(如果有的话)public AnnotationVisitor visitAnnotation(String desc, boolean visible) {if (cv != null) {return cv.visitAnnotation(desc, visible);}return null;}//表示访问这个类的内部类(如果有的话)public void visitInnerClass(String name, String outerName,String innerName, int access) {if (cv != null) {cv.visitInnerClass(name, outerName, innerName, access);}}//表示访问这个类的字段(如果有的话)public FieldVisitor visitField(int access, String name, String desc,String signature, Object value) {if (cv != null) {return cv.visitField(access, name, desc, signature, value);}return null;}//表示访问这个类的方法(如果有的话)public MethodVisitor visitMethod(int access, String name, String desc,String signature, String[] exceptions) {if (cv != null) {return cv.visitMethod(access, name, desc, signature, exceptions);}return null;}//表示结束对这个类的访问public void visitEnd() {if (cv != null) {cv.visitEnd();}}//...省略了一些其他visitXX方法
}

可以发现,ClassVisitor的所有visitXX方法都把逻辑委托给另外一个ClassVisitor的visitorXX方法。
我们知道了ClassVisitor中方法的作用后,我们自定义一个类,

class PrintClassVisitor : ClassVisitor(ASM9),Opcodes{override fun visit(version: Int,access: Int,name: String?,signature: String?,superName: String?,interfaces: Array<out String>?) {println("$name extends $superName {")}override fun visitSource(source: String?, debug: String?) {println("source name = $source")}override fun visitOuterClass(owner: String?, name: String?, descriptor: String?) {println("Outer Class = $name")}override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? {println(" annotation = $descriptor")return null}override fun visitInnerClass(name: String?,outerName: String?,innerName: String?,access: Int) {println(" inner class = $name")}override fun visitField(access: Int,name: String?,descriptor: String?,signature: String?,value: Any?): FieldVisitor? {println("field = $name")return null}override fun visitMethod(access: Int,name: String?,descriptor: String?,signature: String?,exceptions: Array<out String>?): MethodVisitor? {println("method = $name")return null}override fun visitEnd() {println("}")}}

在每个visitXX方法把类的相关信息打印出来,最后使用ClassReader读取OuterClass的class字节码,在accept方法中传入classVisitor实例,完成对OuterClass的访问

fun main(){//创建ClassVisitor实例val printClassVisitor: PrintClassVisitor = PrintClassVisitor()//从构造传入OuterClass的全权限定名,ClassReader会读取OuterClass字节码为字节数组val classReader:ClassReader = ClassReader(OuterClass::class.java.name)//在ClassReader的accept传入ClassVisitor实例,开启访问,第二个参数表示访问模式classReader.accept(printClassVisitor,0)
}

输出结果
在这里插入图片描述

生成一个类

ClassWriter可以凭空生成一个类,举个例子
定义一个接口

interface Author {val name: Stringget() = "YiRan"fun getAge():Int
}

使用ClassWriter生成Author接口的代码

val classWriter = ClassWriter(0)//生成类的头部classWriter.visit(V1_7, ACC_PUBLIC+ ACC_ABSTRACT+ ACC_INTERFACE,"com/example/gradlestudy/Author",null,"java/lang/Object",null)//生成文件名classWriter.visitSource("Author.java",null)//生成名为Name,值为YiRan的字段val fileVisitor = classWriter.visitField(ACC_PUBLIC+ ACC_FINAL+ ACC_STATIC,"NAME","LJava/lang/String",null,"YiRan")fileVisitor.visitEnd()//生成名为getAge,返回值为int方法val methodVisitor = classWriter.visitMethod(ACC_PUBLIC+ ACC_ABSTRACT,"getAge","()I",null,null)methodVisitor.visitEnd()//生成类完毕classWriter.visitEnd()val bytes = classWriter.toByteArray()// 将字节码写入文件val outputPath = "E:\\GradleStudy\\app\\src\\main\\java\\com\\example\\gradlestudy\\Author.class"FileOutputStream(outputPath).use { fos ->fos.write(bytes)}

输出相应的class文件
在这里插入图片描述
可以发现使用ClassWriter生成一个简单的接口代码量就很多,如果这是一个类,并且类中的方法有方法体,那么会更复杂,因此可以使用ASM Bytecode Outline插件来完成这繁琐的过程,,然后在你想要查看的Asm代码的类右键 -> Show Bytecode outline,就会在侧边窗口中显示这个类的字节码(Bytecode)和Asm代码(ASMified),点击ASMified栏目就会显示这个类的Asm码,例如下图就是Person接口的通过插件生成的Asm代码:
在这里插入图片描述

转换一个类

ClassReader可以用来读取一个类,ClassVisitor可以用来访问一个类,ClassWirter可以生成一个类,所以当把它们三个组合在一起时,我们可以把class字节码通过ClassReader读取,把读取到的class字节码通过扩展的ClassVisitor转换,转换后,再通过ClassWirter重新生成这个类,就可以达到转换一个类的目的。

class RemoveAnnotationClassVisitor(visitor: ClassVisitor) : ClassVisitor(ASM9,visitor),Opcodes {override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? {//返回nullreturn null;}}
@Deprecated
public class OuterClass {private int mData = 1;public OuterClass(int data){mData = data;}public int getmData(){return mData;}class InnerClass {}}
fun main(){val classReader = ClassReader(OuterClass::class.java.name)val classWriter = ClassWriter(0)val removeAnnotationClassVisitor = RemoveAnnotationClassVisitor(classWriter)classReader.accept(removeAnnotationClassVisitor,0)val bytes = classWriter.toByteArray()val outputPath = "E:\\GradleStudy\\app\\src\\main\\java\\com\\example\\gradlestudy\\Outer.class"FileOutputStream(outputPath).use { fos ->fos.write(bytes)}
}

RemoveAnnotationClassVisitor代理了ClassWriter,RemoveAnnotationClassVisitor把OuterClass转换完成后就交给了ClassWriter,最终我们可以通过ClassWriter的toByteArray方法返回转换后的OuterClass类的字节数组,最后输出对应的类到指定目录。
可以看到转换后的类把前面的OuterClass类的注解移除掉了
在这里插入图片描述

通过想象把ClassVisitor串成一条转换链,把ClassReader为头,ClassWriter为尾,中间是一系列的ClassVisitor,ClassReader把读取到的class字节码经过一系列的ClassVisitor转换后到达ClassWriter,最终被ClassWriter生成新的class。

Transform Action

Transform API是AGP1.5就引入的特性,主要用于在Android构建过程中,在Class转Dex的过程中修改Class字节码。利用Transform API,我们可以拿到所有参与构建的class文件,然后使用ASM或其他字节码插桩工具进行修改,插入自定义逻辑。
但Transform已经被标记为废弃,在AGP9.0中移除
在这里插入图片描述

Transform Action介绍

Transform API是由Android Gradle插件提供,而Transform Action则是由Gradle提供,由Gradle提供统一的Transform API。
Gradle官方提供了详细的文档文档链接跳转,具体可参考文档

AsmClassVisitorFactory介绍

直接使用Transform Action和Transform API一样,需要手动处理增量编译的逻辑。AGP做了一层封装,提供了AsmClassVisitorFactory来方便我们使用Transform Action进行ASM操作。并且使用AsmClassVisitorFactory能够提升性能和减少代码量。

代码编写

实现AsmClassVisitorFactory

abstract class TimeCostTransform: AsmClassVisitorFactory<InstrumentationParameters.None> {companion object{const val TAG = "TimeCostTransform"}override fun createClassVisitor(classContext: ClassContext, nextClassVisitor: ClassVisitor): ClassVisitor {return TimeCostClassVisitor(nextClassVisitor,classContext.currentClassData.className,TimeCostPlugin.includePackages,TimeCostPlugin.excludePackages)}//对所有class文件都进行插桩处理override fun isInstrumentable(classData: ClassData): Boolean {return true}//自动增量编译,就是第一次会全部编译,后面只有更改了才编译,可通过观察打印信息
}
  • AsmClassVisitorFactory即创建ClassVisitor对象的工厂。此接口的实现必须是一个抽象类
  • createClassVisitor返回我们自定义的classVisitor,在自定义visitor处理完成后,需要传内容给下一个visitor,因此放其在构造函数中传入
  • isInstrumentable用于控制我们自定义Visitor是否需要处理这个类,通过这个方法可以过滤我们不需要的类,从而提高编译速度

自定义ClassVisitor

class TimeCostClassVisitor(nextVisitor: ClassVisitor,private val className: String,private val includePackages: Set<String>,private val excludePackages: Set<String>) : ClassVisitor(Opcodes.ASM9, nextVisitor) {companion object{const val TAG = "TimeCostClassVisitor"}private var startTime:Int = 0private var endTime:Int = 0private var costTime:Int = 0private var thisMethodStack:Int = 0override fun visitMethod(access: Int,name: String?,descriptor: String?,signature: String?,exceptions: Array<out String>?): MethodVisitor {if(!shouldInstrumentClass()){return super.visitMethod(access, name, descriptor, signature, exceptions)}val methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)val newMethodVisitor =object : AdviceAdapter(Opcodes.ASM9, methodVisitor, access, name, descriptor) {@Overrideoverride fun onMethodEnter() {//println("$TAG onMethodEnter-----> name $name className $className")// 方法开始if (isNeedVisiMethod(name)) {//long startTime = System.currentTimeMillis()mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);startTime = newLocal(Type.LONG_TYPE)mv.visitVarInsn(Opcodes.LSTORE,startTime)}super.onMethodEnter();}@Overrideoverride fun onMethodExit(opcode: Int) {//println("$TAG onMethodExit-----> name $name className $className opcode $opcode")// 方法结束if (isNeedVisiMethod(name)) {//long endTime = System.currentTimeMillis();//表示调用静态方法System.currentTimeMillis() 的返回值(一个 long 类型的时间戳)会被压入操作数栈顶mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);//创建了一个新的局部变量槽,用于存储 long 类型的值,newLocal 方法会返回这个局部变量的索引(index),并将其赋值给 endTimeendTime = newLocal(Type.LONG_TYPE)//System.currentTimeMillis() 的返回值从操作数栈被存储到局部变量 endTimemv.visitVarInsn(Opcodes.LSTORE, endTime)//long costTime = endTime - startTime;//从局部变量表中加载 endTime 的值到操作数栈mv.visitVarInsn(LLOAD,endTime)//从局部变量表中加载 startTime 的值到操作数栈mv.visitVarInsn(LLOAD,startTime)//操作数栈顶的两个 long 值会被弹出并相减(栈顶的值减去次栈顶的值),结果再被压入操作数栈mv.visitInsn(LSUB)costTime = newLocal(Type.LONG_TYPE)//将操作数栈顶的 long 值存储到局部变量表中mv.visitVarInsn(LSTORE,costTime)//判断costTime是否大于阈值mv.visitVarInsn(LLOAD,costTime)mv.visitLdcInsn((TimeCostPlugin.mThreshold).toLong())mv.visitInsn(LCMP)//if costTime <= sThreshold,就跳到end标记处,否则继续往下执行val end:Label = Label()mv.visitJumpInsn(IFLE,end)//StackTraceElement thisMethodStack = (new Exception()).getStackTrace()[0]mv.visitTypeInsn(Opcodes.NEW, "java/lang/Exception")mv.visitInsn(Opcodes.DUP)mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Exception", "<init>", "()V", false);mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Exception", "getStackTrace", "()[Ljava/lang/StackTraceElement;", false);mv.visitInsn(Opcodes.ICONST_0);mv.visitInsn(Opcodes.AALOAD);thisMethodStack = newLocal(Type.getType(StackTraceElement::class.java))mv.visitVarInsn(ASTORE, thisMethodStack);/*** Log.e("TimeCost", String.format(*                             "===> %s.%s(%s:%s)方法耗时 %d ms",*                             thisMethodStack.getClassName(), //类的全限定名称*                             thisMethodStack.getMethodName(),//方法名*                             thisMethodStack.getFileName(),  //类文件名称*                             thisMethodStack.getLineNumber(),//行号*                             costTime                        //方法耗时*                     )*             );*/mv.visitLdcInsn("MethodTimeCost")mv.visitLdcInsn("===> %s.%s(%s:%s)\u65b9\u6cd5\u8017\u65f6 %d ms,线程名称:%s")mv.visitLdcInsn(6)mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object")mv.visitInsn(Opcodes.DUP)mv.visitInsn(Opcodes.ICONST_0)mv.visitVarInsn(ALOAD,thisMethodStack)mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StackTraceElement", "getClassName", "()Ljava/lang/String;", false);mv.visitInsn(Opcodes.AASTORE)mv.visitInsn(Opcodes.DUP)mv.visitInsn(Opcodes.ICONST_1)mv.visitVarInsn(ALOAD, thisMethodStack);mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StackTraceElement", "getMethodName", "()Ljava/lang/String;", false);mv.visitInsn(Opcodes.AASTORE)mv.visitInsn(Opcodes.DUP)mv.visitInsn(Opcodes.ICONST_2)mv.visitVarInsn(ALOAD,thisMethodStack)mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StackTraceElement", "getFileName", "()Ljava/lang/String;", false);mv.visitInsn(AASTORE);mv.visitInsn(DUP);mv.visitInsn(ICONST_3);mv.visitVarInsn(ALOAD, thisMethodStack)mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StackTraceElement", "getLineNumber", "()I", false);mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);mv.visitInsn(AASTORE);mv.visitInsn(DUP);mv.visitInsn(ICONST_4)mv.visitVarInsn(Opcodes.LLOAD,costTime)mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false)mv.visitInsn(Opcodes.AASTORE)mv.visitInsn(DUP)mv.visitInsn(ICONST_5)//获取当前线程名称mv.visitMethodInsn(INVOKESTATIC,"java/lang/Thread", "currentThread", "()Ljava/lang/Thread;", false)mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Thread", "getName", "()Ljava/lang/String;", false)mv.visitInsn(Opcodes.AASTORE)mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/String", "format", "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;", false);mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);mv.visitInsn(Opcodes.POP);//end标记处,即方法的末尾mv.visitLabel(end)}super.onMethodExit(opcode);}}return newMethodVisitor}//排除特定方法名 <clinit>类的静态初始化方法 <init>类的实例初始化方法private fun isNeedVisiMethod(name: String?):Boolean {return  name != "<clinit>"  && name != "<init>"}private fun shouldInstrumentClass(): Boolean {// 排除指定的包for (excludePackage in excludePackages) {if (className.startsWith(excludePackage)) {return false}}// 包含指定的包if (includePackages.isNotEmpty()) {for (includePackage in includePackages) {if (className.startsWith(includePackage)) {return true}}return false}// 默认插桩return true}
}

主要通过ASM字节码插桩,在方法的前后插入如下代码,通过计算两者的时间差得到方法耗时

相关文章:

Android方法耗时监控插件开发

需求&#xff1a;自定义一个Gradle插件&#xff0c;这个Gradle插件可以统计方法的耗时&#xff0c;并当方法耗时超过阈值时&#xff0c;可以通过打印Log日志在控制台&#xff0c;然后可以通过Log定位到耗时方法的位置&#xff0c;帮助我们找出耗时方法和当前线程名&#xff0c;…...

TWAS / FUSION

FUSION 是一套用于执行转录组范围和调控组范围关联研究&#xff08;TWAS 和 RWAS&#xff09;的工具。它通过构建功能/分子表型的遗传成分的预测模型&#xff0c;并使用 GWAS 汇总统计数据预测和测试该成分与疾病的关联&#xff0c;目标是识别 GWAS 表型与仅在参考数据中测量的…...

C++中的static_cast:类型转换的安全卫士

C中的static_cast&#xff1a;类型转换的安全卫士 在C编程中&#xff0c;类型转换是不可避免的操作&#xff0c;而static_cast作为C四大强制类型转换运算符之一&#xff0c;是最常用且相对安全的一种转换方式。今天我们就来深入探讨一下这个重要的类型转换工具。 一、static_…...

uniapp-商城-51-后台 商家信息(logo处理)

前面对页面基本进行了梳理和说明&#xff0c;特别是对验证规则进行了阐述&#xff0c;并对自定义规则的兼容性进行了特别补充&#xff0c;应该说是干货满满。不知道有没有小伙伴已经消化了。 下面我们继续前进&#xff0c;说说页面上的logo上传组件&#xff0c;主要就是uni-fil…...

04 mysql 修改端口和重置root密码

当我们过了一段时间&#xff0c;忘了自己当初创建的数据库密码和端口&#xff0c;或者端口被占用了&#xff0c;要怎么处理呢 首先&#xff0c;我们先停止mysql。 一、修改端口 打开my.ini文件&#xff0c;搜索port&#xff0c;默认是3306&#xff0c;根据你的需要修改为其他…...

多线程 2 - 死锁问题

死锁 死锁&#xff0c;是多线程代码中的一类经典问题。加锁能够解决线程安全问题&#xff0c;但如果加锁方式不当&#xff0c;就很可能产生死锁。 出现死锁的三种场景 1、一个线程一把锁 就像上篇文章讲过的&#xff0c;如果对同一个线程上了两把锁&#xff0c;而且上的锁是…...

网络原理(Java)

注&#xff1a;此博文为本人学习过程中的笔记 在网络初始中谈到TCP/IP五层模型&#xff0c;接下来我们将介绍这里面涉及到的网络协议。 应用层是程序员接触最多的层次&#xff0c;程序员写的代码只要涉及到网络通信都可以视为是应用层的一部分。应用层里的东西和程序员直接相…...

HDFS 常用基础命令详解——快速上手分布式文件系统

简介&#xff1a; 本文面向刚接触 Hadoop HDFS&#xff08;Hadoop 分布式文件系统&#xff09;的读者&#xff0c;结合 CSDN 博客风格&#xff0c;系统梳理最常用的 HDFS 客户端命令&#xff0c;并配以示例和注意事项&#xff0c;帮助你在开发和运维中快速掌握 HDFS 的文件管理…...

Unity Shaders and Effets Cookbook

目录 作者简介 审稿人简介 前言 我是偏偏 Unity Shaders and Effets Cookbook 第一章&#xff1a;Diffuse Shading - 漫反射着色器 第二章&#xff1a;Using Textures for Effects - 着色器纹理特效的应用 第三章&#xff1a;Making Your Game Shine with Specular - 镜…...

Markdown—LaTeX 数学公式

目录 一、字母1. 希腊大写字母2. 希腊小写字母3. 花体字母 二、上标和下标1. 上标2. 下标3. 其他 三、括号四、数学符号1. 基本数学符号1&#xff09;运算符2&#xff09;常见函数3&#xff09;分式、根号、累加/乘4&#xff09;极限5&#xff09;积分 2. 三角函数与几何符号1&…...

AI 驱动的开发工具

&#x1f527; 主流 AI 前端开发工具 1. GitHub Copilot 由 GitHub 与 OpenAI 联合开发&#xff0c;集成在 Visual Studio Code、JetBrains 等主流 IDE 中&#xff0c;提供智能代码补全、函数生成等功能&#xff0c;极大地提高了开发效率。 (CSDN博客) 2. Cursor 一款 AI 驱…...

【入门】数字走向I

描述 输入整数N&#xff0c;输出相应方阵。 输入描述 一个整数N。&#xff08; 0 < n < 10 ) 输出描述 一个方阵&#xff0c;每个数字的场宽为3。 #include <bits/stdc.h> using namespace std; int main() {int n;cin>>n;for(int i1;i<n*n;i){cout…...

Kubernetes生产实战(十三):灰度发布与蓝绿发布实战指南

在微服务架构中&#xff0c;如何安全高效地发布新版本是每个团队必须掌握的技能。本文将深入讲解Kubernetes中两种主流发布策略的落地实践&#xff0c;附带生产环境真实案例。 一、金丝雀发布&#xff08;灰度发布&#xff09;&#xff1a;渐进式验证新版本 核心思想&#xf…...

数孪实战笔记(1)数字孪生的含义、应用及技术体系

一、含义 数字孪生&#xff08;Digital Twin&#xff09;是一种通过数字化模型在虚拟世界中实时映射和模拟物理实体、系统或过程的技术。它的核心目的是通过对现实对象的建模、感知、分析和预测&#xff0c;实现对物理世界的全面感知、智能控制和优化决策。数字孪生 实体对象 …...

深入浅出之STL源码分析5_类模版实例化与特化

在 C 中&#xff0c;​​类模板的实例化&#xff08;Instantiation&#xff09;和特化&#xff08;Specialization&#xff09;​​ 是模板编程的核心概念&#xff0c;而 ​​显式实例化&#xff08;Explicit Instantiation&#xff09;和隐式实例化&#xff08;Implicit Insta…...

JDBC演进之路:从基础操作到高效连接池

文章目录 一、JDBC 1.0&#xff1a;手动管理的起点1.1 核心特点1.2 代码示例&#xff1a;1.3 痛点分析 二、JDBC 2.0&#xff1a;配置化的升级2.1 核心改进2.2 代码示例2.3 优势与不足 三、JDBC 3.0&#xff1a;连接池的革命3.1 核心改进3.2 代码示例3.3 核心优势 四、版本对比…...

远程调试---在电脑上devtools调试运行在手机上的应用

1、启动项目–以vite项目为例:先ipconfig查看ip地址 ,然后在vite中配置host为ip地址 2、手机上查看项目:保证手机和电脑在同一局域网, 在手机浏览器打开我们vite启动的项目地址, 3、使用chii进行远程调试 (1) 安装 npm install chii -g (2)启动 chii start -p 8080 (3)在…...

街景主观感知全流程(自建数据集+两两对比程序+Trueskill计算评分代码+训练模型+大规模预测)27

目录 0、Emeditor软件1、Place Pluse 2.0数据集2、街景主观感知大框架2.1 街景主观感知&#xff1a;自建数据集2.2 街景主观感知&#xff1a;两两对比程序2.3 街景主观感知&#xff1a;Trueskill评分2.4 街景主观感知&#xff1a;训练模型&#xff0c;Resnet或EfficientNet或V…...

进阶二:基于HC-SR04和LCD1602的超声波测距

一、实验目的 掌握HC-SR04超声波测距模块的工作原理和使用方法。学会使用LCD1602液晶显示屏显示测量数据。熟悉89C51单片机与外设的接口电路设计和编程方法。二、实验原理 1. HC-SR04超声波测距模块原理 HC-SR04超声波测距模块可提供2cm - 400cm的非接触式距离感测功能,测距精…...

单因子实验 方差分析

本文是实验设计与分析&#xff08;第6版&#xff0c;Montgomery著傅珏生译)第3章单因子实验 方差分析python解决方案。本文尽量避免重复书中的理论&#xff0c;着于提供python解决方案&#xff0c;并与原书的运算结果进行对比。您可以从 下载实验设计与分析&#xff08;第6版&a…...

《Python星球日记》 第53天:卷积神经网络(CNN)入门

名人说&#xff1a;路漫漫其修远兮&#xff0c;吾将上下而求索。—— 屈原《离骚》 创作者&#xff1a;Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 目录 一、图像表示与通道概念1. 数字图像的本质2. RGB颜色模型3. 图像预处理 二、卷积…...

基于人工智能的个性化 MySQL 学习路径推荐研究

基于人工智能的个性化 MySQL 学习路径推荐研究 摘要: 随着信息技术的飞速发展,数据库在各行业应用广泛,MySQL 作为主流数据库之一,学习需求庞大。然而,不同学习者在知识水平、学习进度和目标上存在差异,传统统一的学习路径难以满足个性化需求。本研究通过运用人工智能技…...

阿里云OSS-服务端加签直传说明/示例(SpringBoot)

目录 概述 OSS文件上传方式 1. OSS控制台上传 2. 客户端直传 3. 后端上传 4. 加签直传 服务端加签方式 1. 服务端生成PostObject所需的签名和Post Policy 2.服务端生成STS临时访问凭证 3. 服务端生成PutObject所需的签名URL 实现1&#xff1a;生成PostObject所需的签…...

《向上生长》读书笔记day5

哎&#xff0c;好像有点坚持不下去了&#xff0c;有点松懈了 不咋想继续写读书笔记&#x1f602;&#xff0c;不过我不可能这么轻易放弃的&#xff0c;起码要做完这一本书&#xff0c;话不多说&#xff0c;开始进入的读书&#x1f4d2;笔记 今天读了两个章节&#xff0c;穷人翻…...

优选算法——队列+BFS

目录 1. N叉树的层序遍历 2. 二叉树的锯齿层序遍历 3. 二叉树最大宽度 4. 在每个树行中找最大值 1. N叉树的层序遍历 题目链接&#xff1a;429. N 叉树的层序遍历 - 力扣&#xff08;LeetCode&#xff09; 题目展示&#xff1a; 题目分析&#xff1a; 层序遍历即可~仅…...

Java MCP 实战 --> AI玩转贪吃蛇

MCP 实战 --> AI玩转贪吃蛇 MCP 更加便捷的扩展了 LLM 的能力&#xff0c;使得 AI 发展更加迅猛。本篇主要为了学习MCP的应用&#xff0c;实现了让AI去玩贪吃蛇&#xff0c;使用 Java 实现了 MCP Server 和 MCP Client 的编码。其他文章如下&#xff1a; thinking 基础版…...

Day20打卡-奇异值SVD分解

今天学习非特征筛选的方法&#xff1a; 知识点回顾&#xff1a; 线性代数概念回顾&#xff08;可不掌握&#xff09;奇异值推导&#xff08;可不掌握&#xff09;奇异值的应用 特征降维&#xff1a;对高维数据减小计算量、可视化数据重构&#xff1a;比如重构信号、重构图像&am…...

【RT-Thread Studio】nor flash配置Fal分区

前置条件&#xff1a;【RT-Thread Studio】W25Q128配置 添加 FAL软件包 配置SFUD驱动程序&#xff0c;使用FAL的设备为W25Q128 将fal_cfg.h和fal_flash_sfud_port.c提取出来&#xff0c;放到自己创建的fal_porting目录。 修改 fal_flash_sfud_port.c struct fal_flash_dev n…...

在资源受限设备上实现手势识别:基于包络EMG数据和实时测试的Tiny-ML方法

英文标题&#xff1a;Enabling Gesture on a Resource-Constrained Device: A Tiny-ML Approach with Envelope EMG Data and Real-Time Testing 中文标题&#xff1a;在资源受限设备上实现手势识别&#xff1a;基于包络EMG数据和实时测试的Tiny-ML方法 作者信息 Mohsin Ali S…...

动态规划:最长递增子序列

给定一个数组&#xff0c;求最长递增子序列的长度,就是要求我们求出一个序列中最长的上升子序列的长度&#xff0c;最长上升子序列的定义就是从原序列中按照孙旭去除一些数字&#xff0c;这些数字是逐渐增大的。 *定义dp[i]表示以第i个元素结尾的最长上升子序列的长度。 *初始…...

贪心算法专题(Part2)

目录 1. 最优除法 2. 加油站 3. 坏了的计算器 4. 可被三整除的最大和 5. 单调递增的数字 6. 合并区间 7. 无重叠区间 8. 用最少数量的箭引爆气球 1. 最优除法 题目链接&#xff1a;553. 最优除法 - 力扣&#xff08;LeetCode&#xff09; 题目展示&#xff1a; 题目分…...

4.9/Q1,GBD数据库最新文章解读

文章题目&#xff1a;The burden of diseases attributable to high body mass index in Asia from 1990 - 2019: results from the global burden of disease study 2019 DOI&#xff1a;10.1080/07853890.2025.2483977 中文标题&#xff1a;1990 年至 2019 年亚洲高体重指数导…...

API 网关核心功能解析:负载均衡、容灾、削峰降级原理与实战摘要

在微服务架构中&#xff0c;API 网关作为流量入口枢纽&#xff0c;通过负载均衡、容灾、削峰降级等核心功能保障系统稳定性与高可用性。本文结合 Spring Cloud Gateway 实战代码、原理剖析及行业最佳实践&#xff0c;深度解析网关核心能力&#xff0c;并对比当前前沿技术方案&a…...

Spring之AOP

什么是AOP AOP:Aspect 0riented Programming(面向切面编程、面向方面编程)&#xff0c;可简单理解为就是面向特定方法编程。 场景:案例中部分业务方法运行较慢&#xff0c;定位执行耗时较长的接口&#xff0c;此时需要统计每一个业务方法的 执行耗时。 优势: 1.减少重复代…...

TransmittableThreadLocal:穿透线程边界的上下文传递艺术

文章目录 前言一、如何线程上下文传递1.1 ThreadLocal单线程1.2 InheritableThreadLocal的继承困境1.3 TTL的时空折叠术 二、TTL核心设计解析2.1 时空快照机制2.2 装饰器模式2.3 采用自动清理机制 三、设计思想启示四、实践启示录结语 前言 在并发编程领域&#xff0c;线程上下…...

基于STM32的甲醛检测

一、制作目标 以正点原子的miniSTM32F103RCT6开发板为主控&#xff0c;使用甲醛传感器检测环境空气中的甲醛含量&#xff08;以mg/m^3为单位&#xff09;、C02含量&#xff08;以ppm为单位&#xff09;和总有机挥发物含量TVOC&#xff08;以mg/m^3为单位&#xff09;在OLED显示…...

人形机器人:主控芯片

目前人形机器人领域的主控芯片因厂商和应用场景不同而有所差异&#xff0c;以下是一些主要人形机器人及其可能使用的主控芯片概况&#xff0c;基于公开信息和行业趋势。由于具体型号常为商业机密&#xff0c;部分信息为推测&#xff1a; 主要人形机器人及其主控芯片 特斯拉&am…...

Web自动化测试入门详解

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 一、目的 web自动化测试作为软件自动化测试领域中绕不过去的一个“香饽饽”&#xff0c;通常都会作为广大测试从业者的首选学习对象&#xff0c;相较于C/S架…...

数据结构:树(树的定义和基本术语)

非空树&#xff1a;有且仅有一个根节点 空树&#xff1a;节点数为0的树 在非空树中根节点没有前驱&#xff0c;叶子结点&#xff08;终端结点&#xff09;没有后继&#xff0c;分支结点&#xff08;非终端结点&#xff09;前驱和后继都有&#xff0c;前驱有且仅有一个。 下图…...

用jsp简单实现C语言标准化测试系统

C语言标准化测试系统 在Web编程技术的学习过程中&#xff0c;我们小组为了深入理解相关技术原理&#xff0c;提升实践能力&#xff0c;开发了一个基于动态Web工程框架的C语言标准化考试系统。现在&#xff0c;就来和大家分享一下我们的项目经历。 一、实验目的剖析 这个项目…...

牛客周赛round91

C 若序列为1 4 5 7 9 1 2 3&#xff0c;1 9一定大于1 1或1 4...所以只需要记录当前数之前数字的最大值&#xff0c;然后遍历取max即可&#xff0c;所以对于上面的序列有效的比较为1 9&#xff0c;2 9&#xff0c;3 9取max 代码 //求大于当前数的最大值&#xff0c;然后…...

java-代理

1.什么是java代理模式&#xff1f; 给目标对象提供一个代理对象&#xff0c;并且由代理对象控制对目标对象的引用 我们可以这样理解 我们是用户&#xff0c;代理类是支付宝&#xff0c;我们想用支付宝的转账功能&#xff0c;但是支付宝本身没有转账功能&#xff0c; 又恰好…...

【数据结构与算法】图的基本概念与遍历

目录 一、图的基本概念 1.1 图的基本组成 1.2 图的分类 1.3 顶点的度数 1.4 路径与回路 1.5 子图与特殊图 二. 图的存储结构 2.1 邻接矩阵 2.2 邻接表 三、深度优先遍历 3.1 原理 3.2 实现步骤 3.3 代码实现 四、广度优先遍历 4.1 原理 4.2 实现步骤 4.3 代码…...

《AI大模型应知应会100篇》第54篇:国产大模型API对比与使用指南

第54篇&#xff1a;国产大模型API对比与使用指南 ——从百度文心到通义千问&#xff0c;一文看懂国内AI平台选型 &#x1f4cc; 摘要 随着中国人工智能产业的快速发展&#xff0c;越来越多的国产大模型平台开始崭露头角。本文将系统梳理当前主流国产大模型 API&#xff08;如…...

论文分享➲ arXiv2025 | TTRL: Test-Time Reinforcement Learning

TTRL: Test-Time Reinforcement Learning TTRL&#xff1a;测试时强化学习 https://github.com/PRIME-RL/TTRL &#x1f4d6;导读&#xff1a;本篇博客有&#x1f9a5;精读版、&#x1f407;速读版及&#x1f914;思考三部分&#xff1b;精读版是全文的翻译&#xff0c;篇幅较…...

LeetCode 热题 100 24. 两两交换链表中的节点

LeetCode 热题 100 | 24. 两两交换链表中的节点 大家好&#xff0c;今天我们来解决一道经典的链表问题——两两交换链表中的节点。这道题在 LeetCode 上被标记为中等难度&#xff0c;要求两两交换链表中的相邻节点&#xff0c;并返回交换后链表的头节点。 问题描述 给你一个链…...

好用的播放器推荐

以下是一些好用的播放器推荐&#xff0c;按照不同平台和使用场景分类&#xff1a; 电脑端 VLC Media Player 特点&#xff1a;开源、跨平台&#xff0c;支持几乎所有的音视频格式&#xff0c;无需额外安装解码器。具备强大的功能&#xff0c;如播放列表管理、视频和音频滤镜、…...

C语言_函数hook方案

背景 单体测试中测试一个函数时,该函数调用的其他函数,需要按照测试case,依赖其他函数进行调用参数检查,返回特定值。但是其他函数,不容易做到参数检查和返回特定值,这时需要将其他函数进行hook,hook函数用户自己实现,比较容易实现参数检查和返回值特定值。 本文主要…...

翻转数位题目解释和代码

这段代码的功能是计算一个32位整数中&#xff0c;经过至多一次位翻转&#xff08;0变1或1变0&#xff09;后能得到的连续1的最大长度。例如&#xff0c;输入1775&#xff08;二进制11011101111&#xff09;&#xff0c;翻转中间的0后变为11011111111&#xff0c;连续1的最大长度…...

问题及解决01-面板无法随着窗口的放大而放大

在MATLAB的App Designer中&#xff0c;默认情况下&#xff0c;组件的位置是固定的&#xff0c;不会随着父容器的大小变化而改变。问题图如下图所示。 解决&#xff1a; 为了让Panel面板能够随着UIFigure父容器一起缩放&#xff0c;需要使用布局管理器&#xff0c;我利用 MATLA…...