Android性能优化—卡顿分析与布局优化

一、什么是卡顿?或者说我们怎么感知APP卡顿?

这里面涉及到android UI渲染机制,我们先了解一下android UI是怎么渲染的,android的View到底是如何一步一步显示到屏幕上的?

android系统渲染页面流程:

1)通过 LayoutInflater 将 View 组件解析成 View 对象,对象中封装了组件位置 、显示图片等信息,加载到内存中;

2)CPU 将 View 对象进行计算处理,最终得到该组件对应的多维向量图形 ( 使用向量表示的图形 ) ;

3)GPU 接收上述多维向量图形,GPU 将该向量图进行栅格化,将向量图转为位图 ( 矢量图转为像素图 ) ,计算出对应屏幕上每个像素点显示的值,将图像数据写入到 Back Buffer;

4)Android系统每隔大概16.6ms发出VSYNC信号,触发对UI进行下一帧的渲染,显示屏会使用Frame Buffer跟Back buffer进行交互,拿到最新的一帧数据的渲染到屏幕上。

这个渲染过程如果每次渲染都成功,就能够达到一个流畅的画面,如果16.6ms内CPU和GPU无法处理完一帧画面,就会导致Frame Buffer没能交换,导致上一帧被重复显示,即丢了一帧,当丢帧频率越高时,用户越能感觉画面卡顿。

这里的16.6ms刷新一帧是由人眼对于每秒60帧的刷新频率感觉是很流畅的,计算出来即一帧16.6ms,为了能够实现60fps,这意味着程序的大多数操作都必须在16ms内完成。 

二、Systrace&CPU Profiler卡顿分析:

Systrace是Android平台提供的一款工具,用于记录短期内的设备活动。该工具会生成一份报告,其中汇总了Android 内核中的数据,例如 CPU 调度程序、磁盘活动和应用线程。Systrace主要用来分析绘制性能方面的问题。在发生卡顿时,通过这份报告可以知道当前整个系统所处的状态,从而帮助开发者更直观的分析系统瓶颈,改进性能。

也可以使用上一节说的CPU Profiler进行卡顿分析,CPU Profiler不仅能分析出代码卡顿时间,还能精准的定位到代码内容。连接:http://t.csdn.cn/WGzhA

三、App层面监控卡顿:

目前业界两种主流有效的app监控方式如下:

1)利用UI线程的Looper打印的日志匹配;

2)使用Choreographer.FrameCallback。

1、Looper日志检测卡顿

Android主线程更新UI。如果界面1秒钟刷新少于60次,即FPS小于60,用户就会产生卡顿感觉。简单来说,Android使用消息机制进行UI更新,UI线程有个Looper,在其loop方法中会不断取出message,调用其绑定的Handler在UI线程执行。如果在handler的dispatchMesaage方法里有耗时操作,就会发生卡顿。

public static void loop() {//......for (;;) {//......Printer logging = me.mLogging;if (logging != null) {logging.println(">>>>> Dispatching to " + msg.target + " " +msg.callback + ": " + msg.what);}msg.target.dispatchMessage(msg);if (logging != null) {logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);}//......}
}

只要检测 msg.target.dispatchMessage(msg) 的执行时间,就能检测到部分UI线程是否有耗时的操作。注意到系统源码的这行代码的执行前后,有两个logging.println函数,如果设置了logging,会分别打印出>>>>> Dispatching to和<<<<< Finished to 这样的日志,这样我们就可以通过两次log的时间差值,来计算dispatchMessage的执行时间,从而设置阈值判断是否发生了卡顿。这里我们可以自定义LogMonitor,并设置到Looper中,替换系统的LogMonitor来实现,这种方式也是 BlockCanary 的实现原理。

系统Looper和Printer 接口:

public final class Looper {private Printer mLogging;public void setMessageLogging(@Nullable Printer printer) {mLogging = printer;}
}public interface Printer {void println(String x);
}

自定义BlockCanary:

public class BlockCanary {public static void install() {LogMonitor logMonitor = new LogMonitor();Looper.getMainLooper().setMessageLogging(logMonitor);}
}

自定义LogMonitor:

public class LogMonitor implements Printer {private StackSampler mStackSampler;private boolean mPrintingStarted = false;private long mStartTimestamp;// 卡顿阈值private long mBlockThresholdMillis = 3000;//采样频率private long mSampleInterval = 1000;private Handler mLogHandler;public LogMonitor() {mStackSampler = new StackSampler(mSampleInterval);HandlerThread handlerThread = new HandlerThread("block-canary-io");handlerThread.start();mLogHandler = new Handler(handlerThread.getLooper());}@Overridepublic void println(String x) {//从if到else会执行 dispatchMessage,如果执行耗时超过阈值,输出卡顿信息if (!mPrintingStarted) {//记录开始时间mStartTimestamp = System.currentTimeMillis();mPrintingStarted = true;mStackSampler.startDump();} else {final long endTime = System.currentTimeMillis();mPrintingStarted = false;//出现卡顿if (isBlock(endTime)) {notifyBlockEvent(endTime);}mStackSampler.stopDump();}}private void notifyBlockEvent(final long endTime) {mLogHandler.post(new Runnable() {@Overridepublic void run() {//获得卡顿时 主线程堆栈List<String> stacks = mStackSampler.getStacks(mStartTimestamp, endTime);for (String stack : stacks) {Log.e("block-canary", stack);}}});}private boolean isBlock(long endTime) {return endTime - mStartTimestamp > mBlockThresholdMillis;}
}

自定义StackSampler:

public class StackSampler {public static final String SEPARATOR = "\r\n";public static final SimpleDateFormat TIME_FORMATTER =new SimpleDateFormat("MM-dd HH:mm:ss.SSS");private Handler mHandler;private Map<Long, String> mStackMap = new LinkedHashMap<>();private int mMaxCount = 100;private long mSampleInterval;//是否需要采样protected AtomicBoolean mShouldSample = new AtomicBoolean(false);public StackSampler(long sampleInterval) {mSampleInterval = sampleInterval;HandlerThread handlerThread = new HandlerThread("block-canary-sampler");handlerThread.start();mHandler = new Handler(handlerThread.getLooper());}/*** 开始采样 执行堆栈*/public void startDump() {//避免重复开始if (mShouldSample.get()) {return;}mShouldSample.set(true);mHandler.removeCallbacks(mRunnable);mHandler.postDelayed(mRunnable, mSampleInterval);}public void stopDump() {if (!mShouldSample.get()) {return;}mShouldSample.set(false);mHandler.removeCallbacks(mRunnable);}public List<String> getStacks(long startTime, long endTime) {ArrayList<String> result = new ArrayList<>();synchronized (mStackMap) {for (Long entryTime : mStackMap.keySet()) {if (startTime < entryTime && entryTime < endTime) {result.add(TIME_FORMATTER.format(entryTime)+ SEPARATOR+ SEPARATOR+ mStackMap.get(entryTime));}}}return result;}private Runnable mRunnable = new Runnable() {@Overridepublic void run() {StringBuilder sb = new StringBuilder();StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();for (StackTraceElement s : stackTrace) {sb.append(s.toString()).append("\n");}synchronized (mStackMap) {//最多保存100条堆栈信息if (mStackMap.size() == mMaxCount) {mStackMap.remove(mStackMap.keySet().iterator().next());}mStackMap.put(System.currentTimeMillis(), sb.toString());}if (mShouldSample.get()) {mHandler.postDelayed(mRunnable, mSampleInterval);}}};
}

2、Choreographer.FrameCallback检测卡顿

Android系统每隔16ms发出VSYNC信号,来通知界面进行重绘、渲染,每一次同步的周期约为16.6ms,代表一帧的刷新频率。通过Choreographer类设置它的FrameCallback函数,当每一帧被渲染时会触发回调FrameCallback.doFrame (long frameTimeNanos) 函数。frameTimeNanos是底层VSYNC信号到达的时间戳 。

public class ChoreographerHelper {public static void start() {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {long lastFrameTimeNanos = 0;@Overridepublic void doFrame(long frameTimeNanos) {//上次回调时间if (lastFrameTimeNanos == 0) {lastFrameTimeNanos = frameTimeNanos;Choreographer.getInstance().postFrameCallback(this);return;}long diff = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000;if (diff > 16.6f) {//掉帧数int droppedCount = (int) (diff / 16.6);}lastFrameTimeNanos = frameTimeNanos;Choreographer.getInstance().postFrameCallback(this);}});}}
}

通过 ChoreographerHelper 可以实时计算帧率和掉帧数,实时监测App页面的帧率数据,发现帧率过低,还可以自动保存现场堆栈信息。

Looper比较适合在发布前进行测试或者小范围灰度测试然后定位问题,ChoreographerHelper适合监控线上环境的 app 的掉帧情况来计算 app 在某些场景的流畅度然后有针对性的做性能优化。

四、布局优化 

1、Layout Inspector层级优化 

measure、layout、draw这三个过程都包含自顶向下的View Tree遍历耗时,如果视图层级太深自然需要更多的时间来完成整个绘测过程,从而造成启动速度慢、卡顿等问题。而onDraw在频繁刷新时可能多次出发,因此onDraw更不能做耗时操作,同时需要注意内存抖动。

使用Layout Inspector来检查应用的视图层次结构,

选择需要查看的进程与Activity,在id为content之下的就是我们写在XML中的布局。 

排查是否存在Layout的多层嵌套,我们应该尽量减少其层级,也可以使用 ConstraintLayout 约束布局使得布局尽量扁平化,移除非必需的UI组件。 

2、使用merge标签: 

当我们有一些布局元素需要被多处使用时,这时候我们会考虑将其抽取成一个单独的布局文件。在需要使用的地方通过 include 加载。 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#000000"android:orientation="vertical"><!-- include layout_merge布局 --><include layout="@layout/layout_merge" />
</LinearLayout>
<!-- layout_merge -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="wrap_content"android:layout_height="wrap_content"android:orientation="vertical"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="#ffffff"android:text="测试merge" />
</LinearLayout>

这时候我们的主布局文件是垂直的LinearLayout,include的 "layout_merge" 也是垂直的LinearLayout,这时候include的布局中使用的LinearLayout就没意义了,使用的话反而减慢你的UI表现。这时可以使用merge标签优化。 

<!-- layout_merge -->
<merge xmlns:android="http://schemas.android.com/apk/res/android"><TextViewandroid:background="#ffffff"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="测试merge" />
</merge>

修改为merge后,通过LayoutInspector能够发现,include的布局中TextView直接被加入到父布局中。 

3、使用ViewStub 标签:

当我们布局中存在一个View/ViewGroup,在某个特定时刻才需要他的展示时,可能会把这个元素在xml中定义为invisible或者gone,在需要显示时再设置为visible可见。比如在登陆时,如果密码错误在密码输入框上显示提示。

1)invisible

view设置为invisible时,view在layout布局文件中会占用位置,但是view为不可见,该view还是会创建对象,会被初始化,会占用资源。 

2)gone

view设置gone时,view在layout布局文件中不占用位置,但是该view还是会创建对象,会被初始化,会占用资源。 

如果view不一定会显示,此时可以使用 ViewStub 来包裹此View 以避免不需要显示view但是又需要加载view消耗资源。viewstub是一个轻量级的view,它不可见,不用占用资源,只有设置viewstub为visible或者调用其inflater()方法时,其对应的布局文件才会被初始化。 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#000000"android:orientation="vertical"><ViewStubandroid:id="@+id/viewStub"android:layout_width="600dp"android:layout_height="500dp"android:inflatedId="@+id/textView"android:layout="@layout/layout_viewstub" />
</LinearLayout>
<!-- layout_viewstub -->
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="wrap_content"android:layout_height="wrap_content"android:background="#ffffff"android:text="测试viewStub" />

加载viewStub后,可以通过 inflatedId 找到layout_viewstub 中的根View。

五、过度渲染: 

过度绘制是指系统在渲染单个帧的过程中多次在屏幕上绘制某一个像素。例如,如果我们有若干界面卡片堆叠在一起,每张卡片都会遮盖其下面一张卡片的部分内容。但是,系统仍然需要绘制堆叠中的卡片被遮盖的部分。 

1、GPU 过度绘制检查

手机开发者选项中能够显示过度渲染检查功能,通过对界面进行彩色编码来帮我们识别过度绘制。开启步骤如下:

1. 进入开发者选项 (Developer Options)。

2. 找到调试 GPU 过度绘制(Debug GPU overdraw)。

3. 在弹出的对话框中,选择显示过度绘制区域(Show overdraw areas)。 

Android 将按如下方式为界面元素着色,以确定过度绘制的次数: 

1. 真彩色:没有过度绘制
2. 蓝色:过度绘制 1 次
3. 绿色:过度绘制 2 次
4. 粉色:过度绘制 3 次
5. 红色:过度绘制 4 次或更多次 

有些过度绘制是不可避免的。在优化应用的界面时,应尝试达到大部分显示真彩色或仅有 1 次过度绘制(蓝色)的视觉效果。 

2、解决过度绘制问题:

可以采取以下几种策略来减少甚至消除过度绘制:

1. 移除布局中不需要的背景:

默认情况下,布局没有背景,这表示布局本身不会直接渲染任何内容。但是,当布局具有背景时,其有可能会导致过度绘制。移除不必要的背景可以快速提高渲染性能。不必要的背景可能永远不可见,因为它会被应用在该视图上绘制的任何其他内容完全覆盖。例如,当系统在父视图上绘制子视图时,可能会完全覆盖父视图的背景。

2.使视图层次结构扁平化:

可以通过优化视图层次结构来减少重叠界面对象的数量,从而提高性能。

3.降低透明度:

对于不透明的 view ,只需要渲染一次即可把它显示出来。但是如果这个 view 设置了 alpha 值,则至少需要渲染两次。这是因为使用了 alpha 的 view 需要先知道混合 view 的下一层元素是什么,然后再结合上层的 view 进行Blend混色处理。透明动画、淡入淡出和阴影等效果都涉及到某种透明度,这就会造成了过度绘制。可以通过减少要渲染的透明对象的数量,来改善这些情况下的过度绘制。例如,如需获得灰色文本,可以在 TextView 中绘制黑色文本,再为其设置半透明的透明度值。但是,简单地通过用灰色绘制文本也能获得同样的效果,而且能够大幅提升性能。

六、布局加载优化: 

1、异步加载 

LayoutInflater加载xml布局的过程会在主线程使用IO读取XML布局文件进行XML解析,再根据解析结果利用反射创建布局中的View/ViewGroup对象。这个过程随着布局的复杂度上升,耗时自然也会随之增大。Android为我们提供了 Asynclayoutinflater 把耗时的加载操作在异步线程中完成,最后把加载结果再回调给主线程。 

2、添加依赖: 

dependencies {implementation "androidx.asynclayoutinflater:asynclayoutinflater:1.0.0"
}

3、使用AsyncLayoutInflater

new AsyncLayoutInflater(this).inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() {@Overridepublic void onInflateFinished(@NonNull View view, int resid, @Nullable ViewGroup parent) {setContentView(view);//......}});

1. 使用异步 inflate,那么需要这个 layout 的 parent 的 generateLayoutParams 函数是线程安全的;

2. 所有构建的 View 中必须不能创建 Handler 或者是调用 Looper.myLooper;(因为是在异步线程中加载的,异步线程默认没有调用 Looper.prepare );

3. AsyncLayoutInflater 不支持设置 LayoutInflater.Factory 或者 LayoutInflater.Factory2;

4. 不支持加载包含 Fragment 的 layout;

5. 如果 AsyncLayoutInflater 失败,那么会自动回退到UI线程来加载布局。

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

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

相关文章

短视频矩阵营销系统技术开发者开发笔记分享

一、开发短视频seo抖音矩阵系统需要遵循以下步骤&#xff1a; 1. 确定系统需求&#xff1a;根据客户的需求&#xff0c;确定系统的功能和特点&#xff0c;例如用户注册登录、视频上传、视频浏览、评论点赞等。 2. 设计系统架构&#xff1a;根据系统需求&#xff0c;设计系统的…

[STL]详解list模拟实现

[STL]list模拟实现 文章目录 [STL]list模拟实现1. 整体结构总览2. 成员变量解析3. 默认成员函数构造函数1迭代器区间构造函数拷贝构造函数赋值运算符重载析构函数 4. 迭代器及相关函数迭代器整体结构总览迭代器的模拟实现begin函数和end函数begin函数和end函数const版本 5. 数据…

FreeIPA Server/Client不同版本组合,对podman rootless container的支持

FreeIPA Server/Client不同版本组合&#xff0c;对podman rootless container的支持 根据实验&#xff0c; CentOS 7.9 yum仓库自带的FreeIPA Server 4.6.8&#xff0c; ipa client版本支持CentOS 7.9 yum仓库自带的FreeIPA Client 4.6.8不支持subids&#xff0c;podman调用…

机器学习-Basic Concept

机器学习(Basic Concept) videopptblog Where does the error come from? 在前面我们讨论误差的时候&#xff0c;我们提到了Average Error On Testing Data是最重要的 A more complex model does not lead to better performance on test data Bias And Variance Bias(偏差) …

re学习(26)攻防世界-re-BABYRE(IDA无法分析出函数-代码混淆)

题目链接&#xff1a;https://adworld.xctf.org.cn/challenges/list elf是一种对可执行文件&#xff0c;目标文件和库使用的文件格式&#xff0c;跟window下的PE文件格式类似。载入IDA后如果需要对此文件进行远程调试&#xff0c;需要用linux系统&#xff0c;比如说Ubuntu&…

【机器学习】西瓜书学习心得及课后习题参考答案—第3章线性模型

过了一遍第三章&#xff0c;大致理解了内容&#xff0c;认识了线性回归模型&#xff0c;对数几率回归模型&#xff0c;线性判别分析方法&#xff0c;以及多分类学习&#xff0c;其中有很多数学推理过程以参考他人现有思想为主&#xff0c;没有亲手去推。 术语学习 线性模型 l…

排序八卦炉之冒泡、快排

文章目录 1.冒泡排序1.1代码实现1.2复杂度 2.快速排序2.1人物及思想介绍【源于百度】2.2hoare【霍尔】版本1.初识代码2.代码分析3.思其因果 3.相关博客 1.冒泡排序 1.1代码实现 //插入排序 O(N)~O(N^2) //冒泡排序 O(N)~O(N^2) //当数据有序 二者均为O(N) //当数据接近有序或…

【多模态】ALIGN——使用噪声文本数据进行视觉语言感知预训练

ALIGN: A Large-scale ImaGe and Noisy-text embedding 目录 &#x1f36d;&#x1f36d;1.网络介绍 &#x1f36d;&#x1f36d;2.大规模噪声图像文本数据集 &#x1f438;&#x1f438;2.1图像过滤器 &#x1f438;&#x1f438;2.2文本过滤器 &#x1f36d;&#x1f3…

Bean的实例化方法

目录 1.工厂模式通常有三种形态&#xff1a; 2.简单工厂 2.1 静态工厂 2.1通过factory-bean实例化 2.3通过FactoryBean接口实例化 3.测试 关于容器的使用 3.1获得spring文件方式 3.2getBean方式 4.关闭容器 1.工厂模式通常有三种&#xff1a; 第一种&#xff1a;简单工…

利用鸿鹄快速构建公司IT设备管理方案

需求描述 相信应该有一部分朋友跟我们一样&#xff0c;公司内部有很多各种各样的系统&#xff0c;比如资产管理、CRM、issue管理等等。这篇文章介绍下&#xff0c;鸿鹄是如何让我们的资产系统&#xff0c;按照我们的需求展示数据的。 我们的资产管理系统&#xff0c;是使用开源…

Go语音介绍

Go语言介绍 Go 即Golang&#xff0c;是Google公司2009年11月正式对外公开的一门编程语言。 Go是静态强类型语言&#xff0c;是区别于解析型语言的编译型语言。 解析型语言——源代码是先翻译为中间代码&#xff0c;然后由解析器对代码进行解释执行。 编译型语言——源代码编…

Vue3描述列表(Descriptions)

&#x1f601; 整体功能效果与 ant design vue 保持高度一致 &#x1f601; 包含两种组件&#xff1a;Descriptions 和 DescriptionsItem&#xff08;必须搭配使用&#xff01;&#xff09; 效果如下图&#xff1a;在线预览 APIs Descriptions 参数说明类型默认值必传title…

删除注释(力扣)

删除注释 题目 给一个 C 程序&#xff0c;删除程序中的注释。这个程序source是一个数组&#xff0c;其中source[i]表示第 i 行源码。 这表示每行源码由 ‘\n’ 分隔。 在 C 中有两种注释风格&#xff0c;行内注释和块注释。 字符串// 表示行注释&#xff0c;表示//和其右侧…

冒泡排序【Java算法】

文章目录 1. 概念2. 思路3. 代码实现 1. 概念 比较前后相邻的两个数据&#xff0c;如果前面数据大于后面的数据&#xff0c;就将这两个数据互换。这样对数组的第0个数据到第 N - 1 个数据进行一次遍历后&#xff0c;最大的一个数据就 “沉” 到数组的第 N - 1 个位置。 N N - …

知识区博主转型——兼做知识区和改造区博主!!!!!

想脱单的进来&#xff0c;一起交流如何能脱单&#xff01;&#xff01;&#xff01; 为什么——我太羡慕有对象的人了哭死&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 你是不是很羡慕别人怎么都有女朋友 别人家的女朋友怎么都那么好&#xff…

FPGA学习—通过数码管实现电子秒表模拟

文章目录 一、数码管简介二、项目分析三、项目源码及分析四、实现效果五、总结 一、数码管简介 请参阅博主以前写过的一篇电子时钟模拟&#xff0c;在此不再赘述。 https://blog.csdn.net/qq_54347584/article/details/130402287 二、项目分析 项目说明&#xff1a;本次项目…

RISCV 5 RISC-V调用规则

RISCV 5 RISC-V调用规则 1 Register Convention1.1 Integer Register Convention1.2 Floating-point Register Convention 2. Procedure Calling Convention2.1 Integer Calling Convention2.2 Hardware Floating-point Calling Convention2.3 ILP32E Calling Convention2.4 Na…

大数据课程F4——HIve的其他操作

文章作者邮箱&#xff1a;yugongshiyesina.cn 地址&#xff1a;广东惠州 ▲ 本章节目的 ⚪ 掌握HIve的join&#xff1b; ⚪ 掌握HIve的查询和排序 ⚪ 掌握HIve的beeline ⚪ 掌握HIve的文件格式 ⚪ 掌握HIve的基本架构 ⚪ 掌握HIve的优化&#xff1b; 一、jo…

想了解好用的翻译pdf的软件吗?

在全球化的时代背景下&#xff0c;跨国贸易越来越普遍&#xff0c;跨语言沟通也越来越频繁。小黄是一家跨国公司的员工&#xff0c;他梦想能在全球各地拓展自己的业务&#xff0c;奈何遇到了一个巨大的挑战&#xff1a;跨语言沟通。在这其中&#xff0c;pdf文件是他经常接收到的…

linux基本功系列之cd命令实战

文章目录 前言一. cd命令的介绍二. 语法格式及常用选项三. 参考案例总结 前言 居然发现了落下了CD命令&#xff0c;也不算落下把&#xff0c;主要是cd命令内容太少&#xff0c;撑不起一篇文章&#xff0c;今天也写一写&#xff0c;就当记个笔记吧 &#x1f3e0;个人主页&#…