android 如何分析应用的内存(十六)——使用AS查看Android堆

news2025/3/1 18:24:30

android 如何分析应用的内存(十六)——使用AS查看Android堆

在前面,先介绍了如何使用jdb和VS code查看应用栈相关内容。
本文将介绍,如何查看堆中的内容。大概有:

  1. 堆中的对象,有哪些
  2. 堆中的对象,由谁分配
  3. 堆中的对象,引用关系是怎么

堆中的对象有哪些,以及他们的引用关系——使用堆转储

要查看当前堆中的对象,需要使用工具将堆数据dump出来。

接下来,我们使用Android studio自带的memory profiler进行操作。

第一步:打开Android profiler
在Android Studio中,可以按照如下的步骤,打开memory profiler
在这里插入图片描述

在上图中,出现了两个选择,分别解释如下:

  • profile xxx with low overhead:性能分析器只会启用cpu性能分析器和
    内存分析器,在内存分析器中,只有Record native allocations为启用
    状态(即,录制native分配的功能为启用状态)
  • profile xxx with complete data:启用所有的分析器,包括cpu分析器,
    内存分析器,功耗分析器。

注意:除了上图通过图标启动以外,还可以通过菜单启动:Run->Profile.然后
选择要进行性能分析的模块。

注意:在性能不是很好的电脑上,可以单独运行Android profiler。如下:
android studio安装目录/bin/profiler.xx(mac,linux为profiler.sh, windows为profiler.exe)

启动之后如下图
在这里插入图片描述

注意:一个session就表示一次性能分析,因此可以在Android Profiler中新建session,然后选择不同的应用进程。如下图:
在这里插入图片描述

第二步:打开Memory profiler

在上图中,点击memory的任何区域,打开memory profiler。下图展示了各个区域的具体意义。

在这里插入图片描述

如上图,各个类型解释如下,也可以参考,本系列的第一篇文章:[android 如何分析应用的内存 (一)——内存总览]http://t.csdn.cn/HN1Ma

  • Java:java或kotlin分配的对象的内存
  • Native:c或者c++分配的内存
  • Graphics:图形缓冲区队列为了向屏幕显示像素使用的内存,如GL表面,GL纹理。他们不是GPU专用内存,而是与cpu共用的内存
  • Stack:java栈和native栈的内存,这个跟运行线程的多少有关
  • Code:应用用于处理代码和资源的内存,如dex字节码,so库,字体等
  • Others:无法确定分类的内存
  • Allocated:应用分配的对象个数。在上图中是:N/A。即无法统计,如果能够统计,则会显示一跟虚线,Y轴则对应于图像的右侧,表示多少个对象数。

第三步:使用capture heap dump

为了查看堆中有多少对象,使用capture heap dump捕获当前的堆。如下图:
在这里插入图片描述

从上图可以看到,总共有727个类,所有对象按照类名,列在了列表中

对于上图的几个标记,分别解释如下:

  • 标记1:选择不同的堆进行查看。有如下的堆可以查看

    • image heap:镜像堆,包含启动期间预加载的类。此处的分配不会移动或消失。
    • zygote heap:zygote堆,继承于zygote进程,包含系统资源,类库。
    • app heap:应用的分配的主堆
    • JNI heap:jni引用的堆

    注意:如何理解这四个堆,请见本文后面部分:如何理解Image heap,zygote heap,app heap,JNI heap

  • 标记2:选择不同的排序方法,有如下几种:

    • arrange by class:按照类名排序
    • arrange by package:按照报名排序
    • arrange by callstack:按照调用栈排序。

    注意:按照调用栈排序,在capture heap dump中不支持此功能,欲支持此功能需要使用:Record java/kotlin allocations。见后文:堆中的对象由谁分配

  • 标记3:对类进行条件过滤,有如下几种:

    • show all classes:显示所有的类
    • show activity/fragments classes:显示可能的Activity和fragment的泄露

    注意:此处使用了”可能“两个字。事实上,memory profiler显示的泄露,不一定是真正的泄露

    • show project classes:显示本project中的类。
  • 标记4:Allocations表示分配的次数,如第一排表示MaterialTextView分配了1次,即分配了一个对象

  • 标记5:Native size表示该对象native的大小。尽管只有java代码或者kotlin代码,在某些情况下,依然会存在native的大小,因为java可以使用jni操作native的内存。上图第一行,表示native大小为0

  • 标记6:shallow size表示本对象自身所具有的大小,有时又称为flat size.它不包含内部引用对象的大小。

  • 标记7:Retained size表示本对象自身大小加上内部引用对象的大小。可以直接理解为:若该对象被回收,heap将会释放的大小,上图第一行表示:若MaterialTextView被回收,将会释放10381个字节

注意注意:如果A对象引用了E,C对象,而B对象也引用了E,C对象。那么A对象的retained size会包含E和C吗?B对象的retained size会包含E和C吗?关于retained size的计算,请见后文:如何计算 Shallow Size和retained size

  • 标记8:搜索框,后面两个复选框表示是否大小写敏感,是否使用正则。

第四步:查看各个对象的引用关系

为了举例说明引用关系,现在写一个测试用的链表。如下

//首先定义一个测试类
public class WanbiaoTest{
    public String value = "wanbiao_test";
    public WanbiaoTest next ;
}
//链表的头,用字母o表示
private WanbiaoTest o ;
//构建测试链表
public void do(){
    for(int i=0;i<10;++i){
        if( o == null){
            o = new WanbiaoTest();
        }else{
            WanbiaoTest p = o;
            while(p.next != null){
                p = p.next;
            }
            p.next = new WanbiaoTest();
        }
    }
}

把上述代码运行之后,按照第一,二步dump出heap如下图。然后按照图中步骤进行查看引用
在这里插入图片描述

在上图中。

  1. 通过键入类名,快速定位到要查找的类。
  2. 然后点击类名之后,出现对象列表。
  3. 在对象列表中,我们看到各个不同的对象。因为WanbiaoTest对象按照链表组织。所以Depth一列,将会出现不同的深度。
  4. 任意选择一个对象,右侧出现该对象的详细信息。
  5. 点击references栏,则查看它的引用情况。

从图中可以看到:选中的对象一直通过next被引用。最顶层的WanbiaoTest被o引用着,它存在于MainActivity中。而MainActivity这个对象存在于ActivityThread$ActivityClient对象中。
整个引用链如下图
在这里插入图片描述

如果要查看某个具体的对象,还可以右键点击对象,然后选择:go to instance

注意:除了查看”到最近GC root的引用链“以外,还可以查看所有的引用,即去掉:show nearest GC root only,即可查看该对象被引用的所有对象

上面只是展示了,如何查看对象之间的引用关系。那么该如何去确定内存是否泄露了呢?为了回答这个问题,我们还需要先学习如何查看这些对象是由谁分配的。

堆中的对象由谁分配——使用Record java/kotlin allocations

要想知道对象由谁分配,即知道分配该对象的调用栈,因此可以按照如下的步骤,录制应用的调用栈信息。如下:

第一步:录制调用栈信息
如下图开始录制
在这里插入图片描述

如下图结束录制
在这里插入图片描述

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

详细信息见图中标注,未标注地方,前文已经介绍过。

在这里还需要注意一点:在结束录制按钮边上有一个下拉单选框,当前为Full。可选的选项有:

  • Full:录制所有的对象分配,这会导致app性能大幅下降
  • Sample:以特定的采样间隔(采样率)来采样内存分配。关于采样间隔和采样率的具体细节描述见:[android 如何分析应用的内存(十三)——perfetto]http://t.csdn.cn/laqYB中:heapprofd为什么性能好

第二步:查看对象调用栈

选中待查看的类,然后出现对象列表,然后选中某个对象。如下图
在这里插入图片描述

至此,可以查看某个对象的调用栈信息了。

除了前面介绍的表格查看以外,还可以通过火焰图来查看。选择table边上的Visualization切换到火焰图模式下。如下图
在这里插入图片描述

上图火焰图中,函数跨度越大,则表示选择对应的值越大(即Allocation count,Allocation Size,Totaol Remainning Size,Total Remaining Count之一)

自此,介绍了工具如何查看堆中对象,以及对象的引用关系,还有对象的调用栈信息。

接下来使用两个小小的例子,作为实战

综合运用上面的工具——实战1,Activity泄露

在这个例子里面,我们手动造就了一个Activity的泄露。我们考虑如下的场景:

  1. 我们有一个设备管理器叫做DeviceManager.它是一个单例的对象。
  2. 该设备管理器,有两个接口,分别用于注册和销毁设备状态的监听器。如下
public class DeviceManager {
    //单例对象
    private DeviceManager() {
    }
    private static class DeviceManagerHolder {
        private static final DeviceManager INSTANCE = new DeviceManager();
    }

    public static final DeviceManager getInstance() {
        return DeviceManagerHolder.INSTANCE;
    }


   //定义监听器接口
    public interface DeviceChangedListener{
        void onChanged(int oldStatus,int newStatus);
    }

    private ArrayList<DeviceChangedListener> listeners = new ArrayList<>();

    public void addListener(DeviceChangedListener listener){
        if(!listeners.contains(listener)){
            listeners.add(listener);
        }
    }

    public void removeListener(DeviceChangedListener listener){
        listeners.remove(listener);
    }

}
  1. 现在有我们的业务对象Class Task,需要做如下的操作。在Task创建时,向DeviceManager注册一个监听。在Task销毁时,从DeviceManager注销掉监听。代码如下:
//Task自我监听,Device的状态改变 
//看上去这是一个较好的封装
public class Task implements DeviceManager.DeviceChangedListener {
    private Runnable mTaskRunnable;
    public Task(Runnable task){
        mTaskRunnable = task;
        DeviceManager.getInstance().addListener(this);
        task.run();//运行其他业务
    }
    @Override
    protected void finalize(){
      //回收的时候,注销掉监听器
        DeviceManager.getInstance().removeListener(this);
    }
    @Override
    public void onChanged(int oldStatus, int newStatus) {
        Log.i("Task","oldStatus = "+oldStatus+"newStatus = "+newStatus);
    }
}
  1. 接下来在Activity的onCreate中创建任务,并开始执行。代码如下:
@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        //执行业务
        class TaskRunable implements Runnable{
            private Context mContext;
            public TaskRunable(Context context){
                mContext = context;
            }

            @Override
            public void run() {
                // do something
            }
        }

        Task t = new Task(new TaskRunable(this));
    }
  1. 接下来我们模拟用户的操作。

我们将横竖屏颠倒几次。然后强制调用GC。如下图
在这里插入图片描述

然后使用capture heap dump查看是否有内存泄露。如下图
在这里插入图片描述

从上图我们可以看到,memory profiler已经提示了Activity的泄露。

思考:为什么memory profiler能够检查到Activity的泄露。
回答:因为当Activity destroy之后,如果还能从GC root可达,则表示泄露

在此处,我们并不知道是何处的代码导致的泄露,为了找到泄露,我们按照如下图中的步骤进行操作。
在这里插入图片描述
从上图我们可以看到如下的调用链
在这里插入图片描述

从中我们找到了Activity的泄露原因,TaskRunnable持有了其强引用。那么我们将其改为弱引用,是不是就会好了呢?如下:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        //执行业务
        class TaskRunable implements Runnable{
        //将强引用改为弱引用
          private WeakReference<Context> mContext;
          public TaskRunable(Context context){
              mContext = new WeakReference<>(context);
          }

          @Override
          public void run() {
              // do something
          }
      }
    }

改成上面的代码之后,再次使用Memory profiler进行heap dump。你会看到如下结果
在这里插入图片描述

非常滴不幸!!!依然没有解决泄露。这种问题出现在什么地方?按照上面查看引用的步骤,得到如下的图
在这里插入图片描述

从图中可以看到,TaskRunable对象内部有一个this$0的引用指向了MainActivity.

哦!!,原来如此,这个this$0是内部类对外部对象的一个引用,因此为了解决这个问题,将
class TaskRunable移动到MainActivity之外。

再次进行heap dump,这次,终于没有Activity的泄露了!!!

综合运用上面的工具——实战2,对象泄露

上面实战1,真的就没有泄露吗?假若TaskRunnable含有一个大对象呢?它将会表现如何?

为了模拟TaskRunnable含有一个大对象,我们在内部增加一个整数数组,如下:

class TaskRunable implements Runnable{
    private WeakReference<Context> mContext;

    //1024*4=4096byte 等于4KB.模拟一个大对象
    private int[] values = new int[1024];
    public TaskRunable(Context context){
        mContext = new WeakReference<>(context);
    }

    @Override
    public void run() {
        // do something
    }
}

在经过一系列的内存测试之后,发现java内存一直在增大,并且GC也无法回收(蓝色部分),如下图:
在这里插入图片描述

注意:关于Android 应用如何测试其内存,后续有时间再写,本系列专注于如何分析内存。不过好在内存测试比较容易。事实上,可以按照:http://t.csdn.cn/ovFmO。写一个实时监控脚本即可。当然还可以搭配procstats服务,该服务将在后续文章中介绍。

为了找到这个内存问题,我们将使用heap dump,看看内部的细节。如下图:
在这里插入图片描述

上图可以看到,并没有提示有什么对象泄露了。那么怎么查找这种泄露问题呢?我们观察到java内存在持续增大,我们怀疑,是heap中某个对象分配太多次,而没有被释放。为此,我们点击Allocations(即分配次数)进行倒序查看
在这里插入图片描述

从上图可以看到,allocations最多的分别是:int[],WeakReference,FinalizerReference,TaskRunable,Task这几个类。
再从Shallow size可以看到,最高的为int[]. 几乎可以肯定导致内存泄露的对象即为int[].按照上面的学习,我们来看看其引用链。如下:
在这里插入图片描述

从图上可以看到,导致泄露是因为注册到DeviceManager的对象没有及时的注销掉。

为了解决这个问题,DeviceManager应该在合适的时候,移除其中不再使用的监听器。从Task的finalize函数可以知道,当类不再使用的时候,应该由GC移除监听器。因此,将DeviceManager的listener设计成弱引用。代码过于简单,不再附上代码。

改成弱引用之后,不再出现对象的泄露。

注意:上面的代码,依然不能当做工程实践。因为当Task不再使用,而GC还未回收时,若DeviceManager里面确实有状态发生变化,将会通知到Task,此时若Task有相应的处理逻辑,可能会导致问题。故,此处应该谨慎处理。不过上面的例子仅仅是为了说明内存工具的使用。

上面的两个例子,太过于清晰明了了。他们仅仅是为了说明memory profiler的使用。事实上,真正的内存关系可能比上面复杂得多。不过介于篇幅不再展开,若有机会后续补充。

在结束本文之前,似乎还有两个小问题需要解答:第一个,Android的Image heap,zygote heap,app heap以及JNI heap都是什么。第二个:retained size如何计算

如何理解Image heap,zygote heap,app heap,JNI heap

为了说明这四个堆,我们从启动开始说起,然后简单概括如下

  1. Android系统启动的时候,会创建一个进程,叫做zygote进程。zygote进程作为第一次启动,会加载很多很多的资源,包括一些系统资源。然后再运行。

  2. 当Android要启动另外一个进程时,并不会再次像zygote一样,从头再来。而是直接fork zygote进程。然后复用zygote进程中的部分资源,其中zygote的一个特殊堆,就会被复用。这个堆就是第一步中,加载系统资源的堆。这个堆的名字就叫zygote heap。如果应用需要修改这个堆中内容,此时应用才会新建一个堆,然后拷贝zygote heap中的内容到新堆中,然后再修改,即:写时复制

  3. 当Android的虚拟机启动之后,需要去加载经过优化的字节码。将这些经过优化的字节码被映射到一个特殊的堆中,方便以后直接使用,这个堆就叫做Image heap

  4. 当Android应用起来之后,需要分配对象,那么就从app heap中分配对象,这个堆就是我们应用的主堆

  5. 当Android应用在使用过程中,使用了JNI 引用,则会将这些JNI 引用单独放在一个堆中,这个堆就是JNI heap

如何计算 Shallow Size和retained size

shallow size:即对象本身的大小,而不用计算它内部引用对象的大小。如下:

class A{
  int a;
  A aInner;
 }

那么A对象的shallow size = 4(a为int占四个字节)+4(aInner为引用占四个字节)+8(为Object对象中的字段)= 16字节

注意:有时候,为了保证4字节对齐,当不满4字节的倍数时,也会变成4字节的倍数。为什么要四字节对齐?这源于内存总线为了提高内存访问效率。此处不表,可自行百度

retained size:对象本身的大小,加上它内部引用对象的大小。但一个对象被多个对象引用怎么办呢?如下图
在这里插入图片描述

要解决这个问题,我们需要知道一下dominator tree(支配树)。

它的定义如下:在有向图中,如果从源点到 B 点,无论如何都要经过 A 点,则 A 是 B 的支配点,称 A 支配 B。而距离 B 最近的支配点,称之为直接支配点。如上图。B是D,G的直接支配点。

根据上面的关系,可以得到如下的支配树图形(右图)
在这里插入图片描述

对上图说明:

  1. D直接支配:E,F
  2. B直接支配:D,G,H

那么reatined size就是根据这个支配树来计算。
E retained size = E shallow size
F retained size = F shallow size
H retained size = H shallow size
G retained size = G shallow size
D retained size= E shallow size+ F retained size + D shallow size
B retained size = D retained size + H retained size + G retained size + B shallow size

至此,本文完。

下一篇文章,依然是java的堆内存,将使用另外的两个工具,分别是perfetto和mat进行堆内存分析。敬请期待。

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

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

相关文章

“Can‘t open perl script configure : No such file or directory”的解决办法

编译OpenSSL的时候执行到 perl configure 时提示找不到configure&#xff0c; 然后在网上搜了搜&#xff0c;大家给的解决办法一般都是说设置环境变量或者指定configure路径再执行&#xff1b;我试了都不行&#xff0c; 最后我把perl卸了重装就正常了&#xff1b; 然后我换了…

QEMU源码全解析32 —— Machine(2)

接前一篇文章&#xff1a;QEMU源码全解析31 —— Machine&#xff08;1&#xff09; 本文内容参考&#xff1a; 《趣谈Linux操作系统》 —— 刘超&#xff0c;极客时间 《QEMU/KVM》源码解析与应用 —— 李强&#xff0c;机械工业出版社 特此致谢&#xff01; 上一篇文章给m…

【力扣每日一题】2023.8.11 矩阵对角线元素的和

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 题目给我们一个矩阵&#xff0c;让我们把矩阵对角线上的元素都加起来返回。 那么矩阵的对角线是有两条的&#xff0c;一条是从左上到右下…

Maven安装与配置教程

目录 一、前言 1.什么是Maven 2.为什么要使用Maven 二、Maven安装与配置 1.官网下载 2.Maven配置 3.修改Maven仓库下载镜像及修改仓库位置 3.1.修改仓库下载镜像地址 3.2.修改默认Maven的仓库位置 三、eclipse配置Maven 四、eclipse部署Maven项目 注意事项&#xff…

Python非线性全局优化

文章目录 全局优化函数简介详解性能测试 全局优化函数简介 scipy的optimize模块非常强大&#xff0c;也是我个人使用最多的scipy模块&#xff0c;这里面封装的都是成熟且高效的算法&#xff0c;久经考验。对于参加数学竞赛的同学来说&#xff0c;辛辛苦苦撸出来的遗传算法、模…

Eudic欧路词典 for Mac v4.4.5增强版

欧路词典 (Eudic)是一个功能强大的英语学习工具&#xff0c;它包含了丰富的英语词汇、短语和例句&#xff0c;并提供了发音、例句朗读、单词笔记等功能。 多语种支持&#xff1a;欧路词典支持多种语言&#xff0c;包括英语、中文、日语、法语等等&#xff0c;用户可以方便地进…

Kubernetes 调度 约束

调度约束 Kubernetes 是通过 List-Watch 的机制进行每个组件的协作&#xff0c;保持数据同步的&#xff0c;每个组件之间的设计实现了解耦。 用户是通过 kubectl 根据配置文件&#xff0c;向 APIServer 发送命令&#xff0c;在 Node 节点上面建立 Pod 和 Container。 APIServer…

python——案例14:斐波那契数列

兔子生殖为例子而引入&#xff0c;故又称“兔子数列”&#xff0c; 其数值为&#xff1a;1、1、2、3、5、8、13、21、34……在数学上&#xff0c; 这一数列以如下递推的方法定义&#xff1a; F(0)1&#xff0c;F(1)1, F(n)F(n - 1)F(n - 2)&#xff08;n ≥ 2&#xff0c;n ∈ …

液体神经网络:LNN是个啥概念?

一、说明 在在人工智能领域&#xff0c;神经网络已被证明是解决复杂问题的非常强大的工具。多年来&#xff0c;研究人员不断寻求创新方法来提高其性能并扩展其能力。其中一种方法是液体神经网络&#xff08;LNN&#xff09;的概念&#xff0c;这是一个利用动态计算功能的迷人框…

Simpack助力中国铁路创新发展

中国铁路尤其是高铁的迅速发展是中国装备制造业走向世界一张名片&#xff0c;不仅为人们出行提供了便利&#xff0c;也为中国经济的快速增长提供了有力的支撑。同时&#xff0c;高速铁路的发展给产品研发带来了新的课题和挑战。尤其在动力学领域&#xff0c;各部件或子系统之间…

DP(区间DP)

石子合并 设有 N 堆石子排成一排&#xff0c;其编号为 1,2,3,…,N。 每堆石子有一定的质量&#xff0c;可以用一个整数来描述&#xff0c;现在要将这 N 堆石子合并成为一堆。 每次只能合并相邻的两堆&#xff0c;合并的代价为这两堆石子的质量之和&#xff0c;合并后与这两堆…

远程通信-RPC

项目场景&#xff1a; 在分布式微服务架构中&#xff0c;远程通信是最基本的需求。 常见的远程通信方式&#xff0c;有基于 REST 架构的 HTTP协议、RPC 框架。 下面&#xff0c;从三个维度了解一下 RPC。 1、什么是远程调用 2、什么是 RPC 3、RPC 的运用场景和优 什么是远程调用…

树莓派第一次开机

文章目录 基于树莓派的OpenEuler基础实验一一、树莓派介绍树莓派较普通电脑的优势1、廉价便携可折腾2、树莓派运行开源的Linux操作系统3、编程好平台4、开源大社区5、引脚可编程6、便携随身带7、灵活可扩展 二、openEuler embedded介绍三、树莓派开机指南1. 硬件准备2. 软件准备…

ROS入门-使用常用的ROS命令行工具:操作节点、话题、服务、消息和参数

目录 使用常用的ROS命令行工具&#xff1a;操作节点、话题、服务、消息和参数 1. rosnode&#xff1a;操作节点 2. rostopic&#xff1a;操作话题 3. rosservice&#xff1a;操作服务 4. rosmsg&#xff1a;操作msg消息 5. rossrv&#xff1a;操作srv消息 6. rosparam&am…

MySQL 存储过程、函数、触发器、事件

​ 目录 存储过程 创建存储过程 调用存储过程 查看存储过程 删除存储过程 进阶 变量 if条件判断 传递参数 case结构 while循环 repeat结构 loop语句 leave语句 游标/光标 存储函数 触发器 创建触发器 删除触发器 查看触发器 事件 查看事件调度器是否开启…

eNSP:ebgp和bgp的基础运用

实验要求&#xff1a; 拓扑图&#xff1a; 命令操作&#xff1a; r1: <Huawei>sys [Huawei]sys r1 [r1]int g 0/0/1 [r1-GigabitEthernet0/0/1]ip add 12.1.1.1 24 [r1-GigabitEthernet0/0/1]int lo0 [r1-LoopBack0]ip add 1.1.1.1 24[r2]ospf 1 router-id 2.2.2.2 [r2…

肉豆蔻酰五肽-8——祛眼袋和黑眼圈

肉豆蔻酰五肽-8 简介 眼袋和黑眼圈形成的原因&#xff1a; 1. 随着年龄的增大眼部皮肤会失去弹性, 眼部肌肉同时也会松弛, 从而在眼脸形成皱褶。衬垫在眼眶的脂肪从眼腔转移出并在眼脸聚集。袋状眼脸医学上称为皮肤松垂, 通常可以通过眼脸成形术得到改善。 2. 眼袋形成另外一…

【2023年11月第四版教材】《第2章-信息技术发展(合集篇)》

《第2章-信息技术发展&#xff08;第一部分&#xff09;》 章节说明1 计算机软硬件2 计算机网络2.1 网络的作用范围2.2 OSI模型2.3 广域网协议2.4 网络协议2.5 TCP/IP2.6 软件定义网络&#xff08;SDN&#xff09;2.7 第五代移动通信技术 3 存储和数据库3.1 存储系统架构3.2 存…

能化校对软件:提高招标文件质量的创新解决方案

智能化校对软件是一种创新的解决方案&#xff0c;可以进一步提高招标文件的质量和准确性。 以下是一些智能化校对软件的创新功能和优势&#xff1a; 1.自然语言处理(NLP)技术&#xff1a;智能化校对软件利用NLP技术来理解和分析文本&#xff0c;识别和纠正更复杂的语法和语义错…

Linux系统性能调优及调试课:Linux Kernel Printk

🚀返回专栏总目录 文章目录 0、printk 说明1、printk 日志等级设置2、屏蔽等级日志控制机制3、printk打印常用方式4、printk打印格式0、printk 说明 在开发Linux device Driver或者跟踪调试内核行为的时候经常要通过Log API来trace整个过程,Kernel API printk()是整个Kern…