android 如何分析应用的内存(十八)终章——使用Perfetto查看内存与调用栈之间的泄露

android 如何分析应用的内存(十八)

在前面两篇文章中,先是介绍了如何用AS查看Android的堆内存,然后介绍了使用MAT查看
Android的堆内存。AS能够满足基本的内存分析需求,但是无法进行多个堆的综合比较,因此引入了MAT工具。它可以很好的在两个堆之间进行比较。两个工具已经能解决95%的内存问题了。

但是在一些极端情况下,如多线程带来的内存泄漏,上面两个工具可能就不太好定位问题,即泄漏点的调用栈和调用线程了。

对于Android来讲,怎样才能定位这种多线程调用带来的内存呢?下面是一些经验之谈:

  1. 如果能够添加代码,对于不同的线程,在泄露的对象上,添加一个字段,用于表示线程的id。此方法比较简单,就不再赘述
  2. 如果不能添加代码,那么就需要同时录制java的调用栈和java的堆。根据在同时间段,进行逻辑比较,得出是哪一个调用栈导致的内存泄露。如,通过比较哪个调用栈调用的次数最多,哪个调用栈分配的内存最多。甚至需要在一段时间间隔之间做差分,来获得泄露的对象和调用栈之间的关系。这些方法往往需要一定的经验进行逻辑处理。

本文将围绕不能添加代码的情况,进行分析这种极端情况。

Android Studio虽然提供了java调用栈的录制和java 堆的转储。但是他们无法同时使用,导致在时间轴上面的对比无法完成。但是Perfetto提供了类似的功能。

接下来将以Perfetto为工具,首先介绍同时录制java的调用栈和java堆,在逻辑上进行比较,得出泄漏点的调用栈。然后在一定时间间隔上,对java调用栈和java堆进行差分比较,得出泄漏点的调用栈。

Perfetto同时录制堆栈和heap dump

在android 如何分析应用的内存(十三)——perfetto一文中我们介绍了Perfetto的使用方法。接下来我们将使用常规模式,来同时录制java heap和java callstack。

adb shell perfetto \-c - --txt \-o /data/misc/perfetto-traces/trace \
<<EOFbuffers: {size_kb: 63488fill_policy: DISCARD
}
buffers: {size_kb: 2048fill_policy: DISCARD
}
data_sources: {config {name: "android.packages_list"target_buffer: 1}
}
data_sources: {config {name: "android.heapprofd"target_buffer: 0heapprofd_config {sampling_interval_bytes: 4096process_cmdline: "com.example.test_malloc"shmem_size_bytes: 8388608block_client: true## 只录制com.android.art的堆heaps: "com.android.art"}}
}
## 增加了第二个数据源
data_sources: {## 数据源配置config {## 名字必须为"android.java_hprof"name: "android.java_hprof"## 指定目标buffer,关于目标buffer的含义见android 如何分析应用的内存(十三)target_buffer: 0## java_hprof的配置java_hprof_config {## dump的进程名为:"com.example.test_malloc"process_cmdline: "com.example.test_malloc"}}
}
## 时间修改为60s
duration_ms: 60000EOF

在上面的命令中,我们新增了一个data_source,并且将其指定为录制java heap。同时还有另外一个data_source即android.heapprofd。它会录制指定进程的堆内存,因为我们暂时不需要native堆,所以在heaps中设置了“com.android.art”

关于Perfetto配置文件的说明见:android 如何分析应用的内存(十三)——perfetto

分析结果

输入上面的命令,然后操作APP,60s之后,会在/data/misc/perfetto-traces/trace中形成结果文件,将其pull出来,用https://ui.perfetto.dev/打开即可。如下图
在这里插入图片描述

图中:

  • 标记1:到GC root的这条路的retained size大小。
  • 标记2:到GC root的这条路径的Retained set。

注意:某个对象的Retained size可以理解为:回收这个对象之后,会回收Retained size这么多内存。某个对象的Retained set可以理解为:回收这个对象之后,被它引用且能被回收的对象的集合。某个路径上的Retained size,即为这个路径上的对象的Retained size之和。某个路径上的Retained set,即为这个路径上的对象的Retained set之和

Retained size的计算见:<android 如何分析应用的内存(十六)——使用AS查看Android堆>

注意注意:在上面的操作中,我故意放置了一个小小的漏洞。仔细观察图中,两个菱形的位置,一个在开头,一个在结尾。为了进行泄露点的逻辑分析dump heap和callstack,这两个棱形应该越靠近越好,因此,对上面的配置,调整如下:

adb shell perfetto \-c - --txt \-o /data/misc/perfetto-traces/trace \
<<EOFbuffers: {## 将buffer增大1000倍,否则出现Perfetto ui解析出错size_kb: 63488000fill_policy: DISCARD
}
buffers: {size_kb: 2048fill_policy: DISCARD
}
data_sources: {config {name: "android.packages_list"target_buffer: 1}
}
data_sources: {config {name: "android.heapprofd"target_buffer: 0heapprofd_config {sampling_interval_bytes: 4096process_cmdline: "com.example.test_malloc"shmem_size_bytes: 8388608heaps: "com.android.art"continuous_dump_config {## 10s之后,才开始第一次dumpdump_phase_ms: 10000## 每隔2s,dump一次dump_interval_ms: 10000}}}
}data_sources: {config {name: "android.java_hprof"target_buffer: 0java_hprof_config {process_cmdline: "com.example.test_malloc"continuous_dump_config {## 10s后,才开始第一次dumpdump_phase_ms: 10000## 每隔2s,dump一次dump_interval_ms: 10000}}}
}
## 总时间变成 30s
duration_ms: 30000EOF

注意:这里dump heap时,需要先启动APP,再运行Perfetto。

得到的结果如下图:
在这里插入图片描述

调整时间轴,将两个重合的棱形,放大一点如下图:
在这里插入图片描述

图中,点击第二个棱形图标,出现从GC root的火焰图。

晴天霹雳!!!发现Perfetto在我的Pixel 3上面拉取出来的java heap dump并没有正确的计算引用链。导致我的火焰图,没有正确的反应内存泄露。经过深入分析,发现问题出现在Classloader和它加载的对象之间的引用链没有正确处理,导致了一些从GC root可达的对象,变成了不可达,即已经是泄露的对象,变成了没有泄露。

我们的目标是为了同时采集callstack和heap dump进行逻辑分析。因此我们可以忽略这种影响,直接操作数据库即可。

Heap dump的数据库表

java heap dump只会涉及到3张表:

  • heap_graph_reference:存储引用
  • heap_graph_object:存储对象
  • heap_graph_class:存储类

为了能够直观的展示这些表的结构。下面使用工具,将trace文件的数据库导出来,然后使用数据库UI工具进行查看

导出数据库

使用如下的命令进行数据库的导出。

./trace_processor /Users/biaowan/Documents/trace_single_conti -e  ~/Documents/trace_to_sqlite.db

本次实验,使用了DBeaver的社区版,进行数据库的查看。打开导出的数据库:trace_to_sqlite.db.如下图:

在这里插入图片描述

表说明

在真正使用之前,需要对表的各个列做一个说明:

heap_graph_reference

  • id 本引用唯一的id
  • type 本表名字,即heap_graph_reference
  • reference_set_id 引用对象集ID,如果这个引用是在某个对象中,那么在heap_graph_object中的reference_set_id和此值相等
  • owned_id 被引用的对象的id,即heap_graph_object的id
  • owner_id 使用这个引用的对象的id
  • field_name 这个引用的字段名
  • field_type_name 这个引用的字段的类型名
  • deobfuscated_field_name 反混淆之后的字段名

heap_graph_object

  • id 本对象的id
  • type 本表的名字
  • upid pid
  • graph_sample_ts 采样时间,即dump这个对象的时间
  • self_size 自身大小
  • native_size native大小
  • reference_set_id 本对象引用的其他对象的应用集id
  • reachable 从根对象是否可达,如果可达,则不可回收,否则,可回收(有bug)
  • type_id 本对象对应的class的id
  • root_type 如果不为空,则说明是根对象

heap_graph_class

  • id 本class的id
  • type 本表名字
  • name 本class的名字
  • deobfuscated_name 本class反混淆之后的名字
  • location 本class在什么地方
  • classloader_id classloader的id,这个id即为heap_graph_object的id
  • superclass_id 父类id,对应于本表的id
  • kind 类型

有了这些表的使用说明之后,我们就可以根据自己的需要使用SQL查询语句查看堆中的对象。

接下来,我们先简单的查看,在第二个heap dump中,哪些对象占据的内存空间最大。

查看第二个堆中的内存占用情况

注意:为什么要使用第二个堆?因为在我们的采集数据中,是从Perfetto开始运行的第十秒开始采集,然后再过十秒再次采集。而第一次采集的数据,因为callstack和heap时间间隔较大,所以不采用它。

选取正确的时间戳

使用SQL语句如下:

select * from heap_graph_object group by graph_sample_ts;

在这里插入图片描述

从图中,我们可以看到有三个时间段。分别对应于火焰图中的三个菱形。其余的菱形为callstack

我们要分析的就是第二个采样时间即:398831516584184

将对应时间戳的堆中对象,按照类进行分类,并统计其大小,倒序输出

select class.id,sum(object.self_size) as totalSize,class.name from heap_graph_class as class inner join heap_graph_object as objecton class.id=object.type_id where object.graph_sample_ts=398831516584184group by class.idorder by totalSize desc;

结果如下
在这里插入图片描述

图中可以看到最大的对象类型是int[],其次为String。

注意:本次查询,即包括了能被回收的对象,也包括了不能回收的对象。因为Perfetto本身的错误,不能通过reachable字段来判断是否可回收。事实上,可以自己写一个脚本,递归处理classloader的引用关系,然后修改数据库中的reachable字段。不过我们的任务是为了找到泄漏对象和调用栈的关系。泄露对象其实已经明确了。即可以通过前面两篇的文章,来确定泄露的对象。见:

  • android 如何分析应用的内存(十七)——使用MAT查看Android堆:http://t.csdn.cn/c3BfM
  • android 如何分析应用的内存(十六)——使用AS查看Android堆:http://t.csdn.cn/xYGoA

如果仅仅是上面的查询结果,并不能简单的归因于内存泄露对象为int[],好在我们有AS和MAT工具可以辅助归因。随着Perfetto工具的完善(修复reachable字段的值),仅用Perfetto也可以很好的找到内存泄露点。

又因为前两篇文章和本篇文章,都是使用的一个测试APP,所以,我们已经将内存泄漏点归因于int[]了。接下来就是将这个内存泄漏点与调用栈联系起来

将泄露对象与调用栈简单的联系起来

上面小节说明了泄露对象为int[],如果同一时间在调用栈中某个调用点执行的次数最多,或者在该调用点分配的对象最大,即可简单的将其进行逻辑联系起来。认为是该调用点导致的内存泄露。

我们点击离第二个堆最近的,棱形图标,如下图:
在这里插入图片描述

因为int[]占用了大量的空间,所以我们选择调用栈的total allocation size.如下
在这里插入图片描述

从图中,我们可以看到,doText()这个调用点,几乎占据了99%的分配大小。毫无疑问,int[]的泄露是这个doText()调用点导致的。但是这里面有两个doText(),分别占据约60%和约40%,可以肯定这两个调用点,都导致了内存的泄露。

然后查看其火焰图,可以找到整个调用栈和调用线程。

思考:一切看起来很简单,对吗?是否思考过一个问题——他们的时间点真的对的上吗?或者他们的时间点真的合理吗?

事实上:java的heap dump的数据,是从程序开始运行到dump点的堆中数据。而heapprofd中的数据(即这里的java调用栈)为Perfetto开始运行,到录制点之间的数据。画个图如下
在这里插入图片描述

解决办法:可能读者会想到将Perfetto在app启动之前启动,这样他们开始计算的时间点都是从app启动的时候开始了。然而,根据实测,要抓取java heap,必须先app启动,所以此种方法不可取。真正要解决这种问题,我们只需要再次进行一次录制,然后分别对callstack和heap进行差分比较即可。

用差分比较解决解决剩下难以定位的问题

用差分比较,可以排除,上述时间起始点不同步带来的干扰,同时还能排除,线程过多调用栈过杂带来的干扰。接下来看看使用步骤

重新录制更长时间段的内存数据和调用栈

将上面的配置文件的总时长更改为60s。然后重新录制,得如下图所示的情况

在这里插入图片描述

如上图我们采样了大约6组数据,现在我们选择第40s和第50s的两组进行差分比较分析。当然也可以选择其他组进行比较。

差分分析两个堆中查看增加内容最多的数据

  1. 先找出两者之间的时间。如下
select * from heap_graph_object group by graph_sample_ts;

在这里插入图片描述

根据结果我们选择上图的两个时间,分别为:462051768285433和462061780815388

  1. 将两个时间上的堆做减法,留下40s到50s中增加的对象。
    为了对两个表有一个感性的认知,可以执行下面的指令,进行查看
select class.id,object.graph_sample_ts,sum(object.self_size) as totalSize,class.name from heap_graph_class as class inner join heap_graph_object as objecton class.id=object.type_id where object.graph_sample_ts=462051768285433 or object.graph_sample_ts=462061780815388group by class.idorder by totalSize desc;

在这里插入图片描述

从图中可以看到,不同时间段上对象的大小之和。

接下来,将50s和40s之间的堆,进行相减,如下:

select t2.totalSize - COALESCE(t1.totalSize,0) as diff,t2.name
from (select class.id,object.graph_sample_ts,sum(object.self_size) as totalSize,class.name from heap_graph_class as class inner join heap_graph_object as objecton class.id=object.type_id where object.graph_sample_ts=462061780815388group by class.id,object.graph_sample_tsorder by totalSize desc
) as t2 left join (select class.id,object.graph_sample_ts,sum(object.self_size) as totalSize,class.name from heap_graph_class as class inner join heap_graph_object as objecton class.id=object.type_id where object.graph_sample_ts=462051768285433group by class.id,object.graph_sample_tsorder by totalSize desc
) as t1 on t2.name = t1.name
order by diff desc

在这里插入图片描述

为了更好的计算它们所占的百分比,我在这里求总和之后,直接写入插入语句中,如下:

select (t2.totalSize - COALESCE(t1.totalSize,0))/2119766.0 as percentage,t2.totalSize - COALESCE(t1.totalSize,0) as diff,t2.name
from (select class.id,object.graph_sample_ts,sum(object.self_size) as totalSize,class.name from heap_graph_class as class inner join heap_graph_object as objecton class.id=object.type_id where object.graph_sample_ts=462061780815388group by class.id,object.graph_sample_tsorder by totalSize desc
) as t2 left join (select class.id,object.graph_sample_ts,sum(object.self_size) as totalSize,class.name from heap_graph_class as class inner join heap_graph_object as objecton class.id=object.type_id where object.graph_sample_ts=462051768285433group by class.id,object.graph_sample_tsorder by totalSize desc
) as t1 on t2.name = t1.name
order by percentage desc

如下图
在这里插入图片描述

从图中我们看到了97%的对象为int[],接下来只要比较40s到50s之间,什么样的调用点被调用的次数最多,或者该调用点分配的内存最多,那么这个调用点大概率就是产生这97%的int[]的地方

差分分析同时间段的调用栈

在介绍如何查看调用栈的差分之前,我们需要知道跟调用栈相关的数据库中的表,分别有下面三张表:

  • heap_profile_allocation:存储分配
  • stack_profile_frame:存储栈帧名
  • stack_profile_callsite:存储调用点

当然除了上面三个表以外,还有其他的表,但是对于我们的分析关系不大,故不在啰嗦,查看所有表的信息可参阅:https://perfetto.dev/docs/analysis/sql-tables

调用栈表说明

heap_profile_allocation

  • id 唯一id
  • type 本表名
  • ts 采样时间
  • upid pid
  • heap_name 堆名字
  • callsite_id 调用点id,即stack_profile_callsite的di
  • count 分配的次数,正数就是该调用点的分配次数,负数就是该调用点的释放次数
  • size 分配的大小,同样有正负之分,正数表示分配大小,负数表示释放大小

stack_profile_frame

  • id 唯一id
  • type 本表名
  • name 函数名
  • mapping 该函数映射到哪一个库,如so,.dex 即stack_profile_mapping的id
  • rel_pc 相对于映射库的pc值
  • symbol_set_id 该函数名对应的符号表的id,即stack_profile_symbol的id
  • deobfuscated_name 反混淆之后的名字

stack_profile_callsite

  • id 唯一id
  • type 本表名
  • depth 到调用栈顶部的距离,多一个函数,则深度加一
  • parent_id 本调用点的父函数的调用点id。即stack_profile_callsite的id
  • frame_id 帧id,即stack_profile_frame的id

查看各个采样时间

使用如下的命令查看。

select *,sum(count) as totalCount ,sum (size) as totalSize 
from heap_profile_allocation group by ts;

在这里插入图片描述

从图中可以看到,整个数据被分成了6个时间段,刚好对应于火焰图的六菱形。在火焰图中,每个棱形表示的是从开始抓取到棱形位置对应时间的所有分配。

然而数据库中的每个时间点,表示的是从上一个时间点到本次抓取的所有数据,因此查看40s到50s之间的数据,只需要看第50s的数据即可。也就是倒数第二行。

查看40s和50s的调用详细情况

为了便于理解,下面的语句,将40s和50s的两个堆都列出来分析。如下

select * 
from (select * from heap_profile_allocation where ts = 462061299971174 order by count desc
) as t1
union all 
select * 
from (select * from heap_profile_allocation where ts = 462071449972185 order by count desc
) as t2

在这里插入图片描述

上图列出了30-40区间的情况,以及40-50区间的情况,稍微计算下各个调用点分配的内存占比,可以知道:701调用点的内存分配占据了40s到50s所有分配的82% 因此我们可以大胆的下结论——40s到50s之间的内存泄露由701分配点导致。

查看701分配点的调用栈

使用下面的递归语句,查看整个701的调用栈,如下

WITH RECURSIVE RecursiveCTE AS (SELECT id, parent_id,frame_idFROM stack_profile_callsiteWHERE id = 701UNION ALLSELECT origin.id, origin.parent_id,origin.frame_idFROM stack_profile_callsite originJOIN RecursiveCTE r ON r.parent_id = origin.id
)
SELECT result.id,result.parent_id,frame.name 
FROM RecursiveCTE as result inner join stack_profile_frame as frame on frame.id=result.frame_id 
order by result.id desc;

如下图
在这里插入图片描述

直接观察即可看到40s至50s之间泄露对象int[]由上图的调用栈产生泄露。

注意,使用同样的分析方法,查看调用点518,依然会得出相同的结论,不过他们发生在30s至40s之间。同样的操作步骤,不再继续举例

至此,使用perfetto进行内存分析,已经介绍完毕。

内存方法大总结

万事大吉,关于Android的内存分析已经介绍完毕。现在对前面的所有文章进行一个总结:

native篇

  1. 第零个工具xdd:只能查看任意内存
  2. 第一个工具gdb:它可以查看:寄存器,和任意位置的内存,分析coredump,能查看栈情况,不能查看堆情况
  3. 第二个工具lldb:它可以查看:寄存器,和任意位置的内存,分析coredump,能查看栈情况,不能查看堆情况
  4. 第三个工具自定义malloc:只能查看堆情况,且查看的范围较小,几乎只有自己编译的代码
  5. 第四个工具malloc hook:能查看所有的堆分配情况
  6. 第五个工具malloc统计和libmemunreachable:可以查看所有堆分配情况
  7. 第六个工具malloc debug和libc回调:能查看所有堆分配情况
  8. 第七个工具ASan/HWASan:只能查看linux的堆分配情况,无法查找android的分配情况,列在此处只是为了知识的完整性
  9. 第八个工具perfetto:只能查看堆内存分配情况

java篇

  1. 第零个工具jdb:查看堆帧,本地变量,锁,对象
  2. 第一个工具java debugger for vscode:查看堆栈,本地变量,对象
  3. 第二个工具Android studio:查看堆,对象引用,Retained size,调用栈
  4. 第三个工具MAT:查看堆,对象引用,Retained size ,还能进行堆间差分分析
  5. 第四个工具Perfetto:查看堆,对象引用,Retained size,调用栈,还能在堆和调用栈之间进行差分分析

本系列完。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/34019.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ArcGIS Pro基础:【按顺序编号】工具实现属性字段的编号自动赋值

本次介绍一个字段的自动排序编号赋值工具&#xff0c;基于arcgis 的字段计算器工具也可以实现类似功能&#xff0c;但是需要自己写一段代码实现&#xff0c; 相对而言不是很方便。 如下所示&#xff0c;该工具就是【编辑】下的【属性】下的【按顺序编号】工具。 其操作方法是…

FreeRTOS(二值信号量)

资料来源于硬件家园&#xff1a;资料汇总 - FreeRTOS实时操作系统课程(多任务管理) 目录 一、信号量的概念 1、信号量的基本概念 2、信号量的分类 二、二值信号量的定义与应用 1、二值信号量的定义 2、二值信号量的应用 三、二值信号量的运作机制 1、FreeRTOS任务间二值…

Spring Web

◆ Spring整合web环境 - Javaweb三大组件及环境特点 - Spring整合web环境的思路及实现 把ApplicationContext放在ServleContent域【listen组件中】中 ContextLoaderListener &#xff1a;部分代码写死了 /*** 配置通用的Spring容器的创建&#xff0c;只需要创建一次就可以*/…

SQL server 与 MySQL count函数、以及sum、avg 是否包含 为null的值

sql server 与 mysql count 作用一样。 count 计算指定字段出现的个数&#xff0c; 不是计算 null的值 获取表的条数 count(n) n:常数 count(1),count&#xff08;0&#xff09;等 count(*) count(字段) 其中字段为null 不会统计在内。 avg(字段)、sum(字段) 跟count(字段)…

科技资讯|苹果手机版Vision Pro头显专利曝光,内嵌苹果手机使用

根据美国商标和专利局&#xff08;USPTO&#xff09;公示的清单&#xff0c;苹果公司近日获得了一项头显相关的技术专利&#xff0c;展示了一款亲民款 Vision Pro 头显&#xff0c;可以将 iPhone 放置在头显内部充当屏幕。 根据patentlyapple 媒体报道&#xff0c;这是苹果公司…

案例12 Spring MVC入门案例

网页输入http://localhost:8080/hello&#xff0c;浏览器展示“Hello Spring MVC”。 1. 创建项目 选择Maven快速构建web项目&#xff0c;项目名称为case12-springmvc01。 2.配置Maven依赖 <?xml version"1.0" encoding"UTF-8"?><project xm…

【计算机网络】TCP协议超详细讲解

文章目录 1. TCP简介2. TCP和UDP的区别3. TCP的报文格式4. 确认应答机制5. 超时重传6. 三次握手7. 为什么两次握手不行?8. 四次挥手9. 滑动窗口10. 流量控制11. 拥塞控制12. 延时应答13. 捎带应答14. 面向字节流15. TCP的连接异常处理 1. TCP简介 TCP协议广泛应用于可靠性要求…

IP 协议的相关特性和数据链路层相关知识总结

目录 IP 协议的相关特性 一、IP协议的特性 二、 IP协议数据报格式 三、 IP协议的主要功能 1. 地址管理 动态分配 IP地址 NAT机制 NAT背景下的通信 IPV6 2. 路由控制​​​​​​​ 3.IP报文的分片与重组 数据链路层相关知识 1、以太网协议&#xff08;Ethernet&#xff09; 2.M…

OpenCV实例(八)车牌字符识别技术(一)模式识别

车牌字符识别技术&#xff08;一&#xff09;模式识别 1.模式识别流程2. 模式识别方式 影响并导致汽车牌照内字符出现缺损、污染、模糊等情况的常见因素有照相机的性能、采集车辆图像时光照的差异、汽车牌照的清洁度等。为了提高汽车牌照字符识别的准确率&#xff0c;本节将把英…

【C语言】自定义实现strlen函数的3种方法

大家好&#xff0c;我是苏貝&#xff0c;本篇博客带大家了解C语言中自定义实现strlen函数的3种方法&#xff0c;如果大家觉得我写的不错的话&#xff0c;可以给我一个赞&#x1f44d;吗&#xff0c;感谢❤️ 在自定义实现strlen函数之前&#xff0c;我们简单的介绍一下strlen函…

“冰箭卫士·IP发布会”首次亮相第14届海峡两岸(厦门)文博会

2023年8月6日,“冰箭卫士IP发布会”首次亮相海峡两岸文博会思明馆。此次发布会由厦门市文化创意产业协会、厦门理工&#xff08;集美区&#xff09;政产学研基地主办&#xff0c;厦门市文化创意产业协会IP设计研究院、厦门一笔之上文化发展有限公司、冰箭应急安全科技研究院承办…

T113-S3-调试debug串口修改

目录 前言 一、原理图示意 二、设备树文件配置 三、系统配置文件修改 四、调试问题 总结 前言 在嵌入式系统开发过程中&#xff0c;Debug串口是一个不可或缺的工具&#xff0c;用于输出调试信息、观察系统运行状态以及进行错误排查。T113-S3开发板作为一款功能强大的嵌入式…

20230811导出Redmi Note12Pro 5G手机的录音机APP的录音

20230811导出Redmi Note12Pro 5G手机的录音机APP的录音 2023/8/11 10:54 redmi note12 pro 录音文件 位置 貌似必须导出录音&#xff0c;录音的源文件不知道存储到哪里了&#xff01; 参考资料&#xff1a; https://jingyan.baidu.com/article/b87fe19e9aa79b1319356842.html 红…

Ubuntu18.04搭配无人机仿真环境(ROS,PX4,gazebo,Mavros,QGC安装教程)

Ubuntu18.04搭配无人机仿真环境 ROS环境配置版本安装 gazebo安装Mavrosa安装PX4源码下载和编译运行仿真地面站安装 ROS环境配置 我个人使用了代理环境进行下载。Linux没有代理的可以使用国内源。 清华大学源 sudo sh -c ‘. /etc/lsb-release && echo “deb http://m…

简单入门seleniumUI自动化测试

目录 一、selenium的介绍 二、selenium的原理 三、selenium的八种元素定位的方法 1、ID定位&#xff1a; 2 、name定位&#xff1a; 3、class定位&#xff1a; 4、tag定位&#xff1a; 5、link_text定位&#xff1a; 6、partial_link_text定位&#xff1a; 7、css定位…

vue table动态合并, 自定义合并,参照合并,组合合并

<template><div><el-table:data"tableData":span-method"objectSpanMethod"border:header-cell-style"{ textAlign: center }"><el-table-column prop"area" label"区域" align"center">…

【uniapp】一文读懂app端安装包升级

一、前言 首先&#xff0c;在app端开发上线的过程中&#xff0c;会面临一个问题&#xff0c;就是关于app端的版本升级的问题。如果不做相关处理来引导用户的话&#xff0c;那么app就会出现版本没有更新出现的各种问题&#xff0c;我们常见的有在线升级和去指定地址下载安装两种…

【CSS】CSS 布局——弹性盒子

Flexbox 是一种强大的布局系统&#xff0c;旨在更轻松地使用 CSS 创建复杂的布局。 它特别适用于构建响应式设计和在容器内分配空间&#xff0c;即使项目的大小是未知的或动态的。Flexbox 通常用于将元素排列成一行或一列&#xff0c;并提供一组属性来控制 flex 容器内的项目行…

LabVIEW开发图像采集和基于颜色的隔离

LabVIEW开发图像采集和基于颜色的隔离 在当今的工业和工厂中&#xff0c;准确性和精度是决定特定行业生产力的两个重要关键点。为了优化生产力&#xff0c;各行各业正在从手动操作转向自动操作和控制。机器人技术在工业过程中的出现为人类提供了机械辅助。机器视觉在工业机器人…

Java GUI,mybatis实现资产管理系统

Java GUI——资产管理系统 前言&#xff1a;为了做java课设&#xff0c;学了一手Java GUI。感觉蛮有意思的&#xff0c;写写文章&#xff0c;做个视频记录一下。欢迎大家友善指出我的不足 资产管理系统录制视频&#xff0c;从头敲到尾 模块划分 资产信息管理 资产信息查询 …