【小木箱成长营】内存优化系列文章:
内存优化 · 工具论 · 常见的 Android 内存优化工具和框架
内存优化 · 方法论 · 揭开内存优化神秘面纱
内存优化 · 实战论 · 内存优化实践与应用
Tips: 关注微信公众号小木箱成长营,回复"内存优化"可免费获得内存优化思维导图
Hello,我是小木箱,欢迎来到小木箱成长营系列教程,今天将分享内存优化 · 基础论 · 初识 Android 内存优化。
本次分享主要分为五个部分内容,第一部分内容是 5W2H 分析内存优化,第二部分内容是内存管理机制,第三部分内容是内存优化 SOP,第四部分内容是 内存优化指导原则, 最后一部分内容是总结与展望。
如果学完小木箱内存优化的基础论、工具论、方法论和实战论,那么任何人做内存优化都可以拿到结果。
首先我们说说我们的第一部分内容,5W2H 分析内存优化,5W2H 分析内存优化提出了 7 个高价值问题
Android 内存优化是指优化 Android 应用程序的内存使用,以减少可用内存的消耗,提高应用程序的性能和可靠性。Android 内存优化可以通过减少内存使用量,减少对资源的消耗,以及提高内存利用率来实现。
安卓系统对每个应用程序都有一定的内存限制,当应用程序的内存超过了上限,就会出现 OOM (Out of Memory),也就是 App 的异常退出。
因此,要改善系统的运行效率、改善用户体验、降低系统资源占用、延长电池寿命、降低系统故障的危险。
Android 通过内存优化,可以减少系统内存使用,让系统更加流畅,运行更快,减少系统 Crash,提升用户体验。
关于应用内存分析,需要重点关注四个阶段
Android 给每个应用进程分配的内存都是非常有限的,那么,为什么不能把图片下载下来都放到磁盘中呢?
因为放在内存中,展示会更“快”,快的原因两点:
那么,问题来了,什么是解码呢?
Android 系统要在屏幕上展示图片的时候只默认“像素缓冲”,而这也是大多数操作系统的特征。jpg,png 等图片格式,是把“像素缓冲”使用不同的手段压缩后的结果。
不同格式的图片,在设备上展示,必须经过一次解码,执行速度会受图片压缩比、尺寸等因素影响。
对于 Android 内存优化可以细分为 RAM 和 ROM 两个维度:
主要是降低运行时内存,RAM 优化目的有以下三个:
减少程序占用的 ROM,并进行 APK 精简。其目标是减少应用程序的占用,防止由于 ROM 空间限制而导致程序的安装失败。
手机不使用 PC 的 DDR 内存,采用的是 LP DDR RAM,也就是“低功率的两倍数据率存储器”。其计算规则如下所示:
LP DDR 系列的带宽=时钟频率 ✖️ 内存总线位数/8
LP DDR4=1600MHZ✖️64/8✖️ 双倍速率=26GB/s。
那么内存占用是否越少越好?
如果当系统内存充足的时候,那么小木箱建议你多用一些内存获得更好的性能。
如果系统内存不足的时候,那么小木箱建议你可以做到“用时分配,及时释放”。
做好内存优化将带来以下三点好处:
第一点好处是减少 OOM,提高应用稳定性。
第二点好处是减少卡顿,提高应用流畅度。
第三点好处是减少内存占用,提高应用后台运行时的存活率。
那么,内存痛点定位主要是有哪几类呢?内存痛点问题通常来说,可以细分为如下三类:
第一,内存抖动。
第二,内存泄漏。
第三,内存溢出。
下面,小木箱带大家来了解下内存抖动、内存泄漏和内存溢出。
内存波动图形呈锯齿状、GC 导致卡顿。内存抖动在 Dalvik 虚拟机上更明显,因为 ART 虚拟机内存管理、回收策略做了优化,所以内存分配、GC 效率提升了 5~10 倍,内存抖动发生概率小。
当内存频繁分配和回收导致内存不稳定,出现内存抖动,内存抖动通常表现为频繁 GC、内存曲线呈锯齿状。
并且,内存抖动的危害严重,会导致页面卡顿,甚至 OOM。
那么,为什么内存抖动会导致 OOM?
主要原因有如下两点:
第一,频繁创建对象,导致内存不足及不连续碎片;
public class MainActivity extends AppCompatActivity {
private Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button) findViewById(R.id.button);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
for (int i = 0; i < 100000; i++) {
// 频繁创建大量的对象
byte[] data = new byte[1024 * 1024];
}
}
});
}
}
在这段代码中,每次点击按钮时都会创建 100,000 个大约为 1MB 的数组,如果内存不够用,则可能导致 OOM 错误。请注意,实际应用中应避免这种不负责任的内存使用行为。
第二,不连续的内存片无法被分配,导致 OOM;
public class MainActivity extends AppCompatActivity {
private Button mButton;
private ArrayList mDataList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button) findViewById(R.id.button);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mDataList = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
// 频繁创建大量的对象
byte[] data = new byte[1024 * 1024];
mDataList.add(data);
}
}
});
}
}
在这段代码中,每次点击按钮时都会创建大量的 1MB 大小的数组,并将它们添加到 mDataList 中。由于内存是不连续的,因此在较大的数组中分配这些不连续的内存片可能导致 OOM 错误。请注意,实际应用中应避免这种不负责任的内存使用行为。
这里假设有这样一个场景:点击按钮使用 Handler 发送空消息,Handler 的 handleMessage 方法接收到消息后会导致内存抖动
for 循环创建 100 个容量为 10w+的 string[]数组在 30ms 后继续发送空消息。使用 MemoryProfiler 结合代码可找到内存抖动出现的地方。查看循环或频繁调用的地方即可。
public class MainActivity extends AppCompatActivity {
private Button mButton;
private Handler mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button) findViewById(R.id.button);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mHandler.sendEmptyMessage(0);
}
});
mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
for (int i = 0; i < 100; i++) {
String[] arr = new String[100000];
}
mHandler.sendEmptyMessageDelayed(0, 30);
}
};
}
}
请注意,这个代码中的消息循环可能会导致内存泄漏,因此您需要在适当的时候删除消息。
下面列举一些导致内存抖动的常见案例,如下所示:
public class Main {
public static void main(String[] args) {
// 使用加号拼接字符串
String str = "";
long startTime = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
str = str + "hello";
}
System.out.println("使用加号拼接字符串的内存使用量:" + (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / (1024 * 1024) + " MB");
System.out.println("使用加号拼接字符串的时间:" + (System.currentTimeMillis() - startTime) + " ms");
// 使用StringBuilder
StringBuilder sb = new StringBuilder(5);
startTime = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
sb.append("hello");
}
System.out.println("使用StringBuilder的内存使用量:" + (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / (1024 * 1024) + " MB");
System.out.println("使用StringBuilder的时间:" + (System.currentTimeMillis() - startTime) + " ms");
}
}
输出结果:
使用加号拼接字符串的内存使用量:75 MB
使用加号拼接字符串的时间:4561 ms
使用 StringBuilder 的内存使用量:77 MB
使用 StringBuilder 的时间:4 ms
使用全局缓存池,避免频繁申请和释放的对象。
public class ObjectPool {
private static ObjectPool instance = null;
private HashMap pool = new HashMap<>();
private ObjectPool() {}
public static ObjectPool getInstance() {
if (instance == null) {
instance = new ObjectPool();
}
return instance;
}
public void addObject(String key, Object object) {
pool.put(key, object);
}
public Object getObject(String key) {
return pool.get(key);
}
public void removeObject(String key) {
pool.remove(key);
}
}
该代码使用单例模式创建了一个 ObjectPool 类,并实现了添加、获取和删除对象的方法。
当应用程序需要使用某个对象时,可以通过调用 ObjectPool.getInstance().getObject(key) 方法从缓存池中获取该对象。
当不再需要该对象时,可以调用 removeObject(key) 方法将其从缓存池中删除。
但使用后,手动释放对象池中的对象(removeObject 这个 key)。
public class CustomView extends View {
private Paint paint;
private Rect rect;
public CustomView(Context context) {
super(context);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 重复创建对象,导致内存抖动
paint = new Paint();
rect = new Rect();
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.FILL);
rect.set(0, 0, getWidth(), getHeight());
canvas.drawRect(rect, paint);
}
}
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 重复创建对象,导致内存抖动
setContentView(new CustomView(this));
}
}
上面的代码中,在CustomView的onDraw方法和MainActivity的onCreate方法中,每次都重新创建了Paint和Rect对象,这会导致内存波动,因为系统并不能回收之前创建的对象。
为了避免这种情况,我们可以将Paint和Rect对象声明为类变量,并在构造方法中初始化,以保证只创建一次:
public class CustomView extends View {
private Paint paint;
private Rect rect;
public CustomView(Context context) {
super(context);
// 初始化对象
paint = new Paint();
rect = new Rect();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.FILL);
rect.set(0, 0, getWidth(), getHeight());
canvas.drawRect(rect, paint);
}
}
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new CustomView(this));
}
}
每次创建局部变量时,内存都会分配给它,但在循环结束后,它们不会被立即回收。这将导致内存的不断增加,最终导致内存抖动。
//----------------------------错误示例---------------------------
for(int i=0;i< 100000;i++){
Bitmap bitmap=BitmapFactory.decodeResource(getResources(),R.drawable.large_image);
}
//----------------------------正确示例---------------------------
Bitmap bitmap;
for(int i=0;i< 100000;i++){
bitmap=BitmapFactory.decodeResource(getResources(),R.drawable.large_image);
bitmap.recycle();
}
在这个例子中,每次循环都会创建一个 Bitmap 对象,并将其赋值给局部变量 bitmap。但是,循环结束后, Bitmap 对象不会被立即回收,因此内存不断增加。
使用 SparseArray 类族、ArrayMap 来替代 HashMap。
public class Main {
public static void main(String[] args) {
int N = 100000;
// Create a SparseArray
SparseArray sparseArray = new SparseArray<>();
for (int i = 0; i < N; i++) {
sparseArray.put(i, i);
}
System.out.println("SparseArray size: " + sparseArray.size());
System.gc();
long memorySparseArray = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
// Create an ArrayMap
ArrayMap arrayMap = new ArrayMap<>();
for (int i = 0; i < N; i++) {
arrayMap.put(i, i);
}
System.out.println("ArrayMap size: " + arrayMap.size());
System.gc();
long memoryArrayMap = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
// Create a HashMap
HashMap hashMap = new HashMap<>();
for (int i = 0; i < N; i++) {
hashMap.put(i, i);
}
System.out.println("HashMap size: " + hashMap.size());
System.gc();
long memoryHashMap = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
System.out.println("Memory usage:");
System.out.println("SparseArray: " + memorySparseArray / 1024.0 + " KB");
System.out.println("ArrayMap: " + memoryArrayMap / 1024.0 + " KB");
System.out.println("HashMap: " + memoryHashMap / 1024.0 + " KB");
}
}
Android 系统虚拟机的垃圾回收是通过虚拟机 GC 机制来实现的。GC 会选择一些还存活的对象作为内存遍历的根节点 GC Roots,通过对 GC Roots 的可达性来判断是否需要回收。
内存泄漏是在当前应用周期内不再使用的对象被 GC Roots 引用,导致不能回收,使实际可使用内存变小。
对象被持有导致无法释放或不能按照对象正常的生命周期进行释放,内存泄漏导致可用内存减少和频繁 GC,从而导致内存溢出,App 卡顿。
public class MainActivity extends AppCompatActivity {
private List bitmaps = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 不断加载图片并加入到List中
while (true) {
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.large_image);
bitmaps.add(bitmap);
}
}
}
在上面的代码中,每次加载图片并加入到List中都不会释放内存,因为List引用了这些图片,导致图片无法释放,最终造成内存溢出。为了避免内存溢出,你可以考虑使用低内存占用的图片格式,或者在不需要使用图片时主动调用recycle方法释放图片的内存。
OOM,OOM 时会导致程序异常。Android 设备出厂以后,java 虚拟机对单个应用的最大内存分配就确定下来了,超出值就会 OOM。
单个应用可用的最大内存对应于 /system/build.prop 文件中的 dalvik.vm.heap growth limit。
此外,除了因内存泄漏累积到一定程度导致 OOM 的情况以外,也有一次性申请很多内存,比如说一次创建大的数组或者是载入大的文件如图片的时候会导致 OOM。而且,实际情况下很多 OOM 就是因图片处理不当而产生的。
public class MainActivity extends AppCompatActivity {
private ImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView = findViewById(R.id.image_view);
// 试图创建大的数组
int[] largeArray = new int[Integer.MAX_VALUE];
// 或者试图载入大的图片
Bitmap largeBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.large_image);
imageView.setImageBitmap(largeBitmap);
}
}
ART 和 Dalvik 虚拟机使用分页和内存映射来管理内存。ART 和 Dalvik 虚拟机有什么区别呢?
Dalvik 是 Android 系统首次推出的虚拟机,它是一个字节码解释器,把 Java 字节码转换为机器码执行。由于它的设计历史和硬件限制,它的性能较差,但是可以很好地支持多个 Android 设备。
而 ART 则是 Android 4.4(KitKat)发布后推出的一种新的 Java 虚拟机,它把 Java 字节码编译成机器码,在安装应用时一次性编译,因此不需要在运行时解释字节码,提高了性能。ART 的编译技术带来了更快的应用启动速度和更低的内存消耗。
因此,ART 相比 Dalvik,在性能和稳定性方面有了很大的提升,但是由于 ART 把字节码编译成机器码,因此空间占用更大,对于一些低内存的设备来说可能不太适用。
说到这两种虚拟机我们不得不提到 LMK(Low Memory killer)
LMK(Low Memory Killer)是 Android 系统内存管理机制中的一部分,LMK 是用来在内存不足时释放系统中不必要的进程,以保证系统的正常运行。
LMK 机制的底层原理是利用内核 OOM(Out-of-Memory)机制来管理内存。当系统内存不足时,内核会根据各进程的优先级将内存分配给重要的进程,同时会结束一些不重要的进程,以避免系统崩溃。
LMK 机制的使用场景包括:
在系统内存紧张的情况下,LMK 机制可以通过结束不重要的进程来释放内存,以保证系统的正常运行。但是,如果不当使用,它也可能导致应用程序的不稳定。因此,开发者需要合理设计应用程序,避免内存泄露。
下面先从 Java 的内存分配开始说起。
Java 的内存分配区域分为如下五部分:
3.4 Java 内存回收算法
标记清除算法是最早的内存回收算法,其工作原理是标记出不再使用的对象并将其回收。
实现比较简单。
复制算法是一种将内存分为两个区域的算法,其中一个区域用于存储活动对象,另一个区域用于存储不再使用的对象。
实现简单,运行高效,每次仅需遍历标记一半的内存区域。
会浪费一半的空间,代价大。
标记整理算法是标记清除算法和复制算法的结合,其工作原理是先标记出不再使用的对象,再整理内存使得活动对象的内存分配连续
分代回收算法是一种将内存分为几个代的算法,并对每个代进行不同的回收策略
主流的虚拟机一般用的比较多的是分代收集算法。
在 Java 中,两种常用的内存回收算法分别是新生代回收算法和老年代回收算法。
新生代回收算法推荐场景:
老年代回收算法推荐场景:
请注意,这是基于 Java 的默认内存回收算法(即垃圾回收器)的推荐使用场景。您可以通过配置 JVM 参数来更改这些默认设置,以适应您的特定需求。
Android 中的内存是弹性分配的,分配值与最大值受具体设备影响。
对于 OOM 场景其实可以细分为如下两种:
因此,在解决内存不足的问题时,需要首先判断是可用(被分配的)内存不足还是内存真正不足,并根据相应情况采取适当的措施。
如果是可用(被分配的)内存不足,可以通过调整程序的内存配置或者关闭其他应用程序来解决问题。
如果是内存真正不足,则需要通过升级内存或者更换计算机等方式来解决问题。
JVM 场景的引用类型有四种,分别是强引用、软引用、软引用和虚引用
强引用、软引用、软引用和虚引用的本质区别可以参考如下表:
引用类型GC 回收时间用途生存时间强引用永不对象的一般状态JVM 停止运行时软引用内存不足时对象缓存内存不足时终止弱引用GC对象缓存GC 后终止虚引用未知未知未知
强引用是 Java 中最常见的引用类型,当对象具有强引用时,它永远不会被垃圾回收。只有在程序结束或者手动将对象设置为 null 时,才会释放强引用。
public class StrongReferenceExample {
public static void main(String[] args) {
ArrayList data = new ArrayList<>();
data.add("Hello");
data.add("World");
// 创建强引用
ArrayList strongReference = data;
System.out.println("Data before garbage collection: " + strongReference);
// 断开 data 引用,使其可以被回收
data = null;
System.gc();
System.out.println("Data after garbage collection: " + strongReference);
}
}
输出结果:
Data before garbage collection: [Hello, World]
Data after garbage collection: [Hello, World]
在代码中,我们创建了一个 ArrayList 对象 data,并通过赋值语句将它的引用赋给了变量 strongReference,此时,strongReference 和 data 将指向同一个对象。
在之后的代码中,我们断开了 data 的引用,让其变成可回收对象,但因为 strongReference 仍然保持着对该对象的强引用,所以该对象在 GC 后仍然不会被回收。
一种用于追踪对象的引用,不会对对象的生命周期造成影响。在内存管理方面,弱引用不被认为是对象的“有效引用”。
因此,如果一个对象只被弱引用指向,那么在垃圾回收的时候,这个对象可能会被回收掉。
弱引用常被用来在内存敏感的应用中实现对象缓存。在这种情况下,弱引用可以让缓存的对象在内存不足时被回收,从而避免内存泄漏。
public class WeakReferenceExample {
public static void main(String[] args) {
String data = new String("Hello");
// 创建弱引用
WeakReference weakReference = new WeakReference<>(data);
System.out.println("Data before garbage collection: " + weakReference.get());
// 断开 data 引用,使其可以被回收
data = null;
System.gc();
System.out.println("Data after garbage collection: " + weakReference.get());
}
}
输出结果:
Data before garbage collection: Hello
Data after garbage collection: null
在代码中,我们创建了一个字符串对象 data,并通过创建 WeakReference 对象并将 data 作为参数来创建弱引用。
在之后的代码中,我们断开了 data 的引用,让其变成可回收对象,但因为 weakReference 仅持有对该对象的弱引用,所以当 JVM 进行 GC 时该对象可能会被回收。
可以通过 weakReference.get 方法来检查对象是否被回收。
如果对象已被回收,则 weakReference.get() 返回 null。
软引用是比强引用更容易被回收的引用类型。当 Java 堆内存不足时,软引用可能会被回收,以腾出内存空间。如果内存充足,则软引用可以继续存在。
public class SoftReferenceExample {
public static void main(String[] args) {
Object referent = new Object();
SoftReference
输出结果:
java.lang.Object@2f92e0f4
这段代码创建了一个 Object 的实例,并使用它作为 SoftReference 的引用对象。
然后,它将该实例设置为 null,并试图强制进行垃圾回收。如果内存不足,软引用会被回收,并且可以从 softReference 获取的对象将为 null。
虚引用是 Java 中最弱的引用类型,对于虚引用,对象只存在于垃圾回收的最后阶段,在这个阶段,对象将被回收,而无论内存是否充足。虚引用主要用于监测对象被回收的状态,而不是用于缓存对象。
public class PhantomReferenceExample {
public static void main(String[] args) {
Object referent = new Object();
ReferenceQueue
输出结果:
false
这段代码创建了一个 Object 的实例,并使用它作为 PhantomReference 的引用对象。
然后,它将该实例设置为 null,并试图强制进行垃圾回收。如果垃圾回收发生,虚引用会被加入引用队列,从而可以从引用队列中获取。
如果发现 APP 在内存方面可能存在很大的问题,第一方面的原因是线上的 OOM 率比较高。
第二方面的原因是经常会看到在 Android Studio 的 Profiler 工具中内存的抖动比较频繁。
这是一个初步的现状,然后在知道了初步的现状之后,进行了问题的确认,经过一系列的调研以及深入研究,最终发现项目中存在以下几点大问题,比如说:内存抖动、内存溢出、内存泄漏,还有 Bitmap 粗犷使用。
如果想解决内存抖动,Memory Profiler 会呈现了锯齿张图形,然后我们分析到具体代码存在的问题(频繁被调用的方法中出现了日志字符串的拼接),就能解决内存泄漏或内存溢出。
为了不增加业务工作量,使用一些工具类或 ARTHook 大图检测方案,没有任何的侵入性。同时,将技术进行团队分享,团队的工作效率上会有本质提升。
对内存优化工具如 Profiler Memory、MAT 的使用,可以针对一系列不同问题的情况,写一系列解决方案文档,整个团队成员的内存优化意识会更强。
做内存优化首先应该学习 Google 内存方面的文档,如 Memory Profiler、MAT 等工具的使用,当在工程遇到内存问题,才能对问题进行排查定位。而不是一开始并没有分析项目代码导致内存高占用问题,就依据自己看的几篇企业博客,不管业务背景,瞎猫碰耗子做内存优化。
如果不结合业务背景,直接对 APP 运行阶段进行内存上报然后内存消耗进行内存监控,那么内存监控一旦不到位,比如存在使用多个图片库,因为图片库内存缓存不公用的,应用内存占用效率不会有质的飞跃。因此技术优化必须结合业务。
在做内存优化的过程中,Android 业务端除了要做优化工作,Android 业务端还得负责数据采集上报,数据上报到 APM 后台后,无论是 Bug 追踪人员或者 Crash 追踪人员,对问题"回码定位"都提供好的依据。
大图片检测方案,大家可能想到去是继承 ImageView,然后重写 ImageView 的 onDraw 方法实现。但是,在推广的过程中,因为耦合度过高,业务同学很难认可,ImageView 之前写一次,为什么要重复造轮子呢? 替换成本非常高。所以我们可以考虑使用类似 ARTHook 这样的 Hook 方案。
内存优化、启动优化、卡顿优化、包体积优化是 Android 性能优化四驾马车,而内存优化又是四驾马车最难驾驭的一驾,如果你掌握了这项基础技能,那么你将超过绝对多数的 Android 开发
内存优化 · 基础论 · 初识 Android 内存优化我们讲解了五部分内容,第一部分内容是 5W2H 分析内存优化,第二部分内容是内存管理机制,第三部分内容是内存优化 SOP,第四部分内容是内存优化指导原则,最后一部分内容是总结与展望。
下一节,小木箱将带大家深入学习内存优化 · 工具论 · 常见的内存优化工具和框架。
我是小木箱,如果大家对我的文章感兴趣,那么欢迎关注小木箱的公众号小木箱成长营。小木箱成长营,一个专注移动端分享的互联网成长社区。
参考资料
页面更新:2024-03-01
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号