Android 内存优化:什么原因导致内存问题?通过内存工具进行分析;内存抖动和内存泄漏;MAT的使用;Profiler的使用;如何优化?
我们开发一个App程序,如果不了解内存的使用情况,就是将稳定性弃之不管。因为你不知道他在什么时候会发生OOM问题,不知道为什么程序会卡顿,不知道为什么会发生问题。你也没有自信跟别人说,你可以也出一个稳定可靠的App程序,所以这一篇文章,我们来研究一下内存优化。Android进行内存优化是为了提高应用的稳定性、流畅性和存活时间,同时降低应用占用的ROM空间。
目录
一、为什么要进行内存优化呢?
我们开发一个App程序,如果不了解内存的使用情况,就是将稳定性弃之不管。因为你不知道他在什么时候会发生OOM问题,不知道为什么程序会卡顿,不知道为什么会发生问题。你也没有自信跟别人说,你可以也出一个稳定可靠的App程序,所以这一篇文章,我们来研究一下内存优化。
Android进行内存优化是为了提高应用的稳定性、流畅性和存活时间,同时降低应用占用的ROM空间。
二、有哪些问题需要我们进行内存优化
-
防止应用发生OOM(Out of Memory)错误:Android设备对每个应用进程分配的内存是有限的,当应用内存使用超过这个限制时,就会发生OOM错误,导致应用异常退出。还有一种是,当申请的控件在内存中无连续的空间可分配的时候,也会出现问题。
-
避免不合理使用内存导致GC(Garbage Collection)次数增多,导致应用卡顿:在Android系统中,GC会暂停所有线程(包括主线程)来回收内存,如果内存使用不合理,频繁触发GC,就会导致应用卡顿。这个问题用两个名词来说,就是内存抖动和内存泄漏。
只要解决了内存抖动和内存泄漏,那么OOM问题就迎刃而解了。
2.1 内存抖动是什么?
- 内存频繁的申请和回收。
- 申请次数太多引发GC,同时还会引起卡顿,因为当GC发生时,应用的线程会被暂停,等待GC完成后才能继续执行。
- 申请速度过快。
(1)代码举例:内存频繁的申请和回收
public class MemoryChurnView extends View {
private Paint paint;
public MemoryChurnView(Context context) {
super(context);
paint = new Paint();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 这里每次onDraw都会创建一个新的String和Rect对象
// 这些对象在onDraw结束后都会变得不可达
String text = "Some text that changes frequently"; // 实际上这里的text并没有改变,但只是为了示例
Rect bounds = new Rect();
paint.getTextBounds(text, 0, text.length(), bounds);
// 绘制文本(这个操作本身不会导致内存抖动,但上面的对象创建会)
canvas.drawText(text, bounds.left, bounds.bottom, paint);
// 注意:这里的text和bounds在onDraw结束后应该被GC回收
// 但如果onDraw被频繁调用,就会导致频繁的GC,即内存抖动
}
}
如果onDraw中包含了创建大对象或大量对象的代码,那么就会导致内存抖动。出于性能和内存使用的考虑,我们通常不会在onDraw中做这样的事情。而是将创建对象放到外面。
public class MemoryChurnView extends View {
private Paint paint;
public MemoryChurnView(Context context) {
super(context);
paint = new Paint();
}
// 这里每次onDraw都会创建一个新的String和Rect对象
// 这些对象在onDraw结束后都会变得不可达
String text = "Some text that changes frequently"; // 实际上这里的text并没有改变,但只是为了示例
Rect bounds = new Rect();
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.getTextBounds(text, 0, text.length(), bounds);
// 绘制文本(这个操作本身不会导致内存抖动,但上面的对象创建会)
canvas.drawText(text, bounds.left, bounds.bottom, paint);
// 注意:这里的text和bounds在onDraw结束后应该被GC回收
// 但如果onDraw被频繁调用,就会导致频繁的GC,即内存抖动
}
}
(2)代码示例:申请速度过快
public void allocateMemoryTooFast() {
List<byte[]> largeDataList = new ArrayList<>();
// 假设我们有一个很大的数字N
int N = 1000000;
// 在一个循环中快速分配内存
for (int i = 0; i < N; i++) {
// 分配一个相对较大的byte数组(例如1MB)
byte[] largeData = new byte[1024 * 1024]; // 1MB
largeDataList.add(largeData); // 假设我们需要保留这些数据的引用
// 注意:这里的代码在真实应用中可能会导致内存峰值过高和OOM异常
// 因为我们在短时间内分配了大量的内存
}
// 在实际应用中,我们可能需要对largeDataList进行处理
// 或者在分配过程中添加一些逻辑来限制内存的使用
}
申请内存过快的问题通常发生在短时间内分配了大量内存的情况下。这可能会导致内存峰值过高,甚至触发OOM(Out Of Memory)异常。
2.2 内存泄漏是什么?
- 一个长生命周期的对象持有一个短生命周期对象的强引用。
public class MainActivity extends AppCompatActivity {
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 处理消息
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 假设在某个地方启动了一个线程,该线程使用handler发送消息
new Thread(new Runnable() {
@Override
public void run() {
Message message = handler.obtainMessage();
// 设置消息内容等
handler.sendMessage(message);
}
}).start();
}
@Override
protected void onDestroy() {
super.onDestroy();
// 这里没有取消handler的消息队列或处理handler的引用,可能导致内存泄漏
}
}
Handler被定义为一个匿名内部类的实例,并持有MainActivity的隐式引用(因为Handler中的handleMessage方法需要访问MainActivity的成员)。当MainActivity被销毁时,如果Handler仍然有未处理的消息或Runnable任务在消息队列中,那么这些任务会持有MainActivity的引用,从而阻止垃圾回收器回收MainActivity的内存,导致内存泄漏。
解决方法:将Handler定义为一个静态内部类,并通过弱引用持有外部类的引用。这样,当外部类(如MainActivity)被销毁时,垃圾回收器可以回收它,而不会因为Handler的引用而阻止。
public class MainActivity extends AppCompatActivity {
private static class MyHandler extends Handler {
private final WeakReference<MainActivity> activityWeakReference;
MyHandler(MainActivity activity) {
activityWeakReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = activityWeakReference.get();
if (activity != null) {
// 处理消息,注意这里要判断activity是否为null
}
}
}
private final MyHandler handler = new MyHandler(this);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 启动线程并使用handler发送消息
new Thread(new Runnable() {
@Override
public void run() {
Message message = handler.obtainMessage();
// 设置消息内容等
handler.sendMessage(message);
}
}).start();
}
@Override
protected void onDestroy() {
super.onDestroy();
// 不需要显式取消handler,因为handler是静态内部类,不会持有外部类的强引用
}
}
如果Handler不是静态内部类,且你确实需要在非静态内部类中使用它,那么你应该在onDestroy方法中显式地移除所有回调和取消所有未完成的任务。然而,这种方法通常不如使用静态内部类+弱引用来得安全和优雅。
好的,到这里,大概问题我们都知道了,那么内存抖动和内存泄漏,很多时候,你可能自己都不知道,因为:
- 有可能你写的代码有问题,但是你没有看出来。
- 有可能是第三方框架,或者使用系统的代码有问题,你不知道。
那么怎么办呢?这个时候我们就需要借助工具来将其检测出来。
三、内存抖动工具使用
3.1 内存抖动工具介绍:Profiler
Android Profiler是Android Studio中的一个集成工具,它允许开发者在运行的应用上执行实时性能分析,是评估代码性能的重要工具。
这个章节,我们主要使用Memory Profiler。
Memory Profiler:帮助开发人员理解应用程序的内存使用情况,包括内存分配和回收,通过可视化图表
3.2 我们先来看看如何使用呢
(1)在Android Studio的主界面上,找到并点击“View”菜单,选择“Tool Windows”下的“Profiler”
(2)我们运行一个程序看看,直接点击运行。
我们就可以在Profiler里面看到信息。
我们主要使用Memory Profiler,点击它
3.3 正常情况的内存
(1)正常的情况
刚启动的时候,内存有波动是正常的,启动后,我们可以看到也是一条直线,这就是正常状态。
我们可以点击一下Record,开始记录,看看申请的内存变量怎么样。
点击排序,可以看看哪个对象最多。
3.4 异常情况的内存
我们看看一个异常代码,比如在自定义view里面不断创建Paint。
public class MemoryJitterView extends View {
private static final String TAG = "MemoryJitterView";
private int width;
private int height;
public MemoryJitterView(Context context) {
super(context);
}
public MemoryJitterView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MemoryJitterView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
this.width = w;
this.height = h;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 这里故意创建新的Paint对象来模拟内存抖动
// 在实际应用中,应该避免这样做,而是应该重用Paint对象
Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setStrokeWidth(10);
// 画一条对角线来演示
canvas.drawLine(0, 0, width, height, paint);
// 为了更频繁地触发onDraw,可以调用invalidate()来请求重绘
// 注意:这样做会导致CPU和GPU过度使用,并且在实际应用中是不推荐的
// 这里仅用于演示内存抖动
postInvalidateOnAnimation(); // 在下一帧动画时间请求重绘
Log.d(TAG, "onDraw: ");
}
}
然后在MainActivity里面使用它
Handler().postDelayed(object :Runnable{
override fun run() {
// 设置自定义View为Activity的内容视图
setContentView(MemoryJitterView(this@MainActivity))
setContentView(MemoryJitterView(this@MainActivity))
setContentView(MemoryJitterView(this@MainActivity))
setContentView(MemoryJitterView(this@MainActivity))
setContentView(MemoryJitterView(this@MainActivity))
setContentView(MemoryJitterView(this@MainActivity))
}
},10000)
接下来我们运行程序。
(1)一开始的时候,91.9MB。
随着运行,占用不断的增大,线条也呈斜线递增。
这个时候,我们就点击进行记录分析。
记录到一定量以后点击停止,就可以开始分析。
点击一下排序,看看哪些对象产生的最多。
比如我们点击Paint。
具体问题位置,代码也刚好是41行,就全部出来。我们就要可以开始优化了。
四、内存泄漏的工具使用
LeakCanary是一个用于检测Android和Java应用中的内存泄漏的开源工具。但LeakCanary只能检测Activity、Fragment、ViewModel等,比较局限。
想要使用LeakCanary也可以看看这篇文章:https://blog.csdn.net/qq_40853919/article/details/140700972
下面我们来介绍一种MAT的内存分析工具,可以到官网进行下载。
4.1 如何使用
假设一个这样的场景:
- A:在MainActivity1界面。
- B:跳转到MainActivity2界面,然后再返回。
我想看看MainActivity2关掉以后,有没有出现内存泄漏的情况。所以呢我需要记录MainActivity1的堆栈信息,然后执行完B后跳回来的时候,再记录一次MainActivity1的堆栈信息,然后进行对比,如果有多出来的地方,那么就是内存泄漏了。
(1)记录堆栈
点击保存。
就会得到一个这样的文件。
如果直接放过去MemoryAnalyzer软件里面是会报错的。我们需要转一下格式,需要使用的Android SDK转换工具,hprof-conv命令
进行类型转换。
这样,我们就得到了两个转换后的文件,直接放到MemoryAnalyzer里面。
接下来使用工具,找到两个文件,直接打开他们。
到这里,我们已经可以看到内存情况。
除了柱状图,我们还有其他的数据显示方式。
如下是直方图的方式。
如果要进行对比,那么我们只需要点击这个就可以实现堆栈对比。对比两个文件,看看有没有内存泄漏。
如果是+,那么就是增加了多少,也就是有多少没有释放。
如果是+0,那么就是没有增加,也就是都释放了。
好了,MAT就介绍到这里。
五、总结
可以看到,其实大部分的原因,都是因为写代码的水平问题,写代码不规范,导致的一些内存抖动,或者内存泄漏。所以我们需要去提升自己的编码能力,去注意这些细节。
更多推荐
所有评论(0)