打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
Android开发常见内存泄漏和相应的对策(二)
原创 looshen09 印象Android 2018-08-05
三、定位分析内存问题
1、log分析
在Android系统中,GC有以下三种类型:
①kGcCauseForAlloc:在分配内存时发现内存不够的情况下引起的GC,这种情况下的GC会Stop World。Stop World是由于并发GC时,其他线程都会停止,直到GC完成。
②kGcCauseBackground:当内存达到一定的阈值时触发GC,这个时候是一个后台GC,不会引起Stop World。
③kGcCauseExplicit:显式调用时进行的GC,如果ART打开这个选项,在system.gc时会进行GC。
常见的虚拟机打印日志:
D/dalvikvm( 7030):  GC_CONCURRENT free 1049k, 60% free 2341k/9351k, external 3502k/6261k , paused 3md 3ms
GC_CONCURRENT是当前GC时的类型,在Android的虚拟机中GC日志有以下几种类型:
A、GC_CONCURRENT:当应用进程中的Heap内存占用上涨时,避免因Heap内存满了而触发GC。
B、GC_FOR_MALLOC:这是由于Concurrent GC没有及时执行完,而应用又需要分配更多的内存,这时不得不停下来进行Malloc GC。
C、GC_EXTERNAL_ALLOC:这是为external分配的内存执行的GC。
D、GC_HPROF_DUMP_HEAP:创建一个HPROF profile的时候执行。
E、GC_EXPLICIT:显式地调用了System.gc()。
一般来说,可以信任系统的GC机制,尽量不去显式调用System.gc(),减少不必要的系统开销,影响应用的流畅度。
对上面日志的解析:
free 1049K表明在这次GC中回收了多少内存。
60% free 3571K/9991K是Heap的一些统计数据,表明这次回收后60%的Heap可用,存活的对象大小为2341KB,Heap大小是9351KB。
External 3502K/6261K是Native Memory的数据。存放位图数据(Bitmap Pixel Data)或者堆以外内存(NIO Direct Buffer)之类的。第一个数字表明Native Memory中已分配了多少内存,第二个值有点类似一个浮动的阈值,表明分配内存达到这个值,系统就会触发一次GC进行内存回收。
Paused 3ms 3ms表明GC暂停的时间。从这里可以看到,越大的Heap Size在GC时会导致暂停的时间越长。如果是Concurrent GC,会看到两个时间:一个开始,一个结束,且时间很短,但如果是其他类型的GC,很可能只会看到一个时间,且这个时间是相对比较长的。
在Dalvik虚拟机下,GC的操作都是并发的,也就意味着每次触发GC都会导致其他线程暂停工作(包括UI线程)。而在ART模式下,在GC时,不像Dalvik仅有一种回收算法,ART在不同的情况下会选择不同的算法,比如Alloc内存不够时会采用非并发GC,但在Alloc后,发现内存达到一定的阈值时又会触发并发GC。所以在ART模式下,并不是所有的GC都是非并发的。
总体来看,在GC方面,与Dalvik相比,ART更为高效,不仅仅是GC的效率,大大地缩短了Pause时间,而且在内存分配上对大内存分配单独的区域,还能有算法在后台做内存整理,减少内存碎片。因此在ART虚拟机下,可以避免较多的类似GC导致的卡顿问题。
开发者定位问题需要在日志分析中花费大量时间,可能GC影响是性能,可能是OOM,可能是某个进程对内存的占用率高等,需要具体分析才能知道什么问题。同时也需要了解Android内存管理机制,了解Java对象的生命周期、内存分配、内存回收机制等能帮助我们更快高效分析和解决问题。
2、工具使用
内存问题往往需要借助工具分析,还有了解Android内存使用现状,通过现状去分析哪些数据类型有问题,各种类型的分布情况如何,以及在发现问题后如何发现哪些具体对象导致的。下面介绍一些普遍用到的工具:
⑴Memory Monitor
Memory Monitor是一款使用非常简单的图形化工具,可以很好地监控系统或者应用的内存使用情况,主要有以下几个功能:
①显示可用和已用内存,并且以时间为维度实时反应内存分配和回收情况。
②快速判断应用程序的运行缓慢是否由于过度的内存回收导致
③快速判断应用是否是由于内存不足导致程序崩溃
通过观察时间维度实时反应内存分配和回收情况,可以快速发现内存抖动、大内存分配,甚至由于GC导致卡顿。接下来介绍如何使用Memory Monitor的。
在使用Memory Monitor前,需要确认设备是否打开了开发者模式,并且打开了USB调试模式,确定后,可以根据如下步骤使用Memory Monitor:
在Android Studio上运行需要监控的应用。
从Android Studio菜单栏中选择Tools--->Android--->Memory Monitor。或者单击Android Studio应用程序面板右下角的Android图标,直接运行Memory Monitor,当然这个跟Android Studio版本不一样可能地方不太一样,如下图红色③圈住的部分,红色①是选择设备,红色②是选择哪个应用。
一旦Memory Monitor开始运行,图形就开始显示当前内存使用情况,如下图:
在内存显示区域可以看到有深蓝和浅色两种的区域,其中深蓝表示当前应用使用的内存大小,浅色为可用的未分配内存大小。
Memory Monitor几个使用介绍,如下图:
上图红框部分的按钮分别是:启动与关闭Memory监测按钮、手动触发GC按钮、dump java heap 按钮,点击后会生成hprof文件、start(stop) allocation tracking按钮先点击一次,然后会看到Memory Recorder开始转动,然后自己开始在APP上面做相应的操作。在合适的时间再点一次,结束记录。
⑵Heap Viewer
Heap Viewer的主要功能是查看不同数据类型在内存中的使用情况,可以看到当前进程中的Heap Size的情况,分别有哪些类型的数据,以及各种类型数据占比情况。通过分析这些数据找到大的内存对象,再进一步分析这些大对象,进而通过优化减少内存开销,也可以通过数据的变化发现内存泄漏。Heap Viewer的使用介绍如下:
Heap Viewer在Android Studio的Android Device Monitor工具中,Android Device Monitor可以从快捷工具栏上打开,或者选择Tools--->Android--->Android Device Monitor命令。
进入Android Device Monitor面板后,在进程列表中选择需要查看的应用进程,单击Update Heap按钮,在右边的Heap Viewer开始更新数据,右边面板中的数据会在每次GC时改变,包括应用自动触发或者在面板上手动触发。如下图,按照红圆圈框中的数字步骤操作就可以看到内存的具体数据,每次GC数据都不太一样。
针对上图,分步解析如下:
说明:
Heap Size
堆栈分配给App的内存大小
Allocated
已分配使用的内存大小
Free
空闲的内存大小
%Used
Allocated/Heap Size,使用率
Objects
对象数量
说明:
free
空闲的对象
data object
数据对象,类类型对象,最主要的观察对象
class object
类类型的引用对象
1-byte array(byte[],boolean[])
一个字节的数组对象
2-byte array(short[],char[])
两个字节的数组对象
4-byte array(long[],double[])
4个字节的数组对象
non-Java object
非Java对象
对以上出现的列说明:
列名
意义
Count
数量
Total Size
总共占用的内存大小
Smallest
将对象占用内存的大小从小往大排,排在第一个的对象占用内存大小
Largest
将对象占用内存的大小从小往大排,排在最后一个的对象占用的内存大小
Median
将对象占用内存的大小从小往大排,拍在中间的对象占用的内存大小
Average
平均值
自动或者手动GC下,然后观察data object一栏的total size(也可以观察Heap Size/Allocated内存的情况,如下图),看看内存是不是会回到一个稳定值,多次操作后,只要内存是稳定在某个值,那么说明没有内存溢出的,如果发现内存在每次GC后,都在增长,不管是慢增长还是快速增长,都说明有内存泄漏的可能性。
⑶Allocation Tracker
Allocation Tracker的主要功能如下:
①在一段时间内以对象类型为纬度,跟踪在此时间内的内存分配和释放情况。
②寻找代码中内存使用不合理的地方。
Allocation Tracker是分析较短一段时间内的内存使用情况,在使用Allocation Tracker前,可以先用Memory Monitor或者Heap Viewer找到内存异常的场景,然后使用Allocation Tracker分析这个场景的内存使用情况。Allocation Tracker的使用介绍:
Allocation Tracker在Android Studio和Eclipse上都支持,在Android Studio上使用Allocation Tracker界面更加清晰和有条理,但两个IDE上使用Allocation Tracker的功能都是相同的。运行应用后,切换到Android选项卡(和Memory Monitor是同一个视图)。
单击启动追踪按钮(Start Allocation Tracking)
操作应用,怀疑有内存泄漏或者内存变化较大的操作。
单击结束追踪按钮,与启动追踪按钮时同一个位置,如下图:
4.自动生成一个alloc结尾的文件,这个文件记录了这次追踪到的所有内存数据,并且在Android Studio中自动打开一个数据面板,显示当前生成alloc文件的内存数据,如下图
针对上图作如下说明:
A、有两个选项,分别是Group by Method(用方法来分类我们的内存分配)和Group by Allocator(用内存分配器来分类我们的内存分配),不同的选项,在D区显示的信息会不同,默认会以Group by Method来组织。
B、Jump To Source按钮。如果我们想看内存分配的实际在源码中发生的地方,可以选择需要跳转的对象,点击该按钮就能发现我们的源码,但是前提是你有源码
C、统计图标按钮。该按钮比较酷炫,如果点击该按钮,会弹出一个新窗口,里面是一个酷炫的统计图标,有柱状图和轮胎图两种图形可供选择,默认是轮胎图,其中分配比例可以选择分配次数和占用内存大小,默认是大小Size.
⑷MAT
对于大型Java应用程序来说,再精细的测试也难以堵住所有漏洞,即便在测试阶段进行了大量卓有成效的工作,很多问题还是会在生产环境下暴露出来,并且很难在测试环境中重现。在没有发现或者不知道哪有内存泄漏的情况下,可以使用前面提到的几种内存分析工具去分析。通常情况下,可以使用Heap View粗略查看堆得使用情况,又或者使用Allocation Tracker跟踪内存分配情况,当发现内存持续上涨并没有释放时,说明有内存泄漏的可能性,这时再深入分析这个场景的内存情况。Android虚拟机能够记录下问题发生时,系统的部分运行状态和内存使用情况,并将其存储在堆转储(Heap Dump)文件中,而这个文件为开发者分析和诊断问题提供了重要依据。
抓取这个疑似有内存问题的使用场景的Heap信息,然后进行分析,目前来看Memory Analyzer Tool(MAT)是一个快速、功能丰富的Java Heap分析工具,通过分析Java进程的内存快照HPROF文件,从众多的对象中分析,快速计算出在内存中对象的占用大小,查看哪些对象不能被垃圾收集器回收,并可以通过视图直观地查看可能造成这种结果的对象。MAT工具可以帮助开发者定位导致内存泄漏的对象,以及发现大的内存对象,然后解决内存泄漏并通过优化内存对象,达到减少内存消耗的目的。
①获取MAT插件或者工具,常在Eclipse中用MAT插件,可以在线安装MAT,其地址为:http://download.eclipse.org/mat/1.3/update-site/。在AndroidStudio并没有集成MAT工具,需要下载MAT独立客户端,下载地址为:https://eclipse.org/mat/download.php。
②获取HPROF文件。Eclipse和Android Studio差不多,Eclipse还简单些,这里介绍Android Studio获取。从Android Studio进入Android Device Monitor(DDMS),选择需要分析的应用进程,单机Update Heap按钮,对应用进程怀疑有内存问题的操作,也可以整体操作一段时间,结束操作后,多进行几次GC,最后单击Dump HPROF File按钮,保存HPROF文件。因为Android Studio保存的是Android Dalvik格式.hprof文件,所以需要转换成J2SE HPROF格式才能被MAT识别和分析。Android SDK自带一个转换工具hprof-conv,转换语句如下:
./hprof-conv path/file.hprof exitPath/heap-converted.hprof
其中path为转换前的文件路径,exitPath为转换后文件的路径。
在Android Studio1.2以上版本中获取HPROF文件和转换有更快捷的方式是在Memory Monitor工具中,单击Dump Java Heap按钮,在左侧的Capture栏中的Heap Snapshot列表中看到Dump下来的HPROF文件,右击文件,在弹出的菜单中选择Export to .hprof选项,既可以转换成标准的HPROF文件,再使用MAT打开。
③MAT视图。用MAT打开HPROF文件后,可看到MAT的分析内存视图,在MAT窗口上,OverView是一个总体概览,显示总体的内存消耗情况和疑似问题。分析内存常用的是Histogram和Dominator Tree两个视图,这两个视图的区别是统计的维度不一样,但使用Dominator Tree可以方便地查看其引用关系。
Histogram,列出内存中的所有实例类型对象、对象的个数以及大小,并支持正则表达式查找。
Dominator Tree,列出最大的对象及其依赖存活的Object。分析流程和Histogram大同小异,但Dominator Tree能更方便地查看出引用关系。
Top Consumers,通过图形列出最大的Object。
Leak Suspects,通过MAT自动分析泄漏的原因和泄漏的一份总体报告。Leak Suspects列出了工具怀疑的内存泄漏点,以及泄漏的内存大小,在后面有问题列表和所有对象,单击对应的<Details>可以看到更深入的分析情况。
3、经验分析
我们需要了解Android的几个内存优化问题:
①系统在内存收回(GC)时对性能会造成什么影响?
②在有一定剩余内存的情况下,有时还会导致OOM的产生?
③发生OOM是概率性的,并且每次在执行不同的代码时产生OOM?
④内存占用高对应用有什么影响?
上面问题可能许多开发者都有接触,解决的方法也有所不同,但应该形成自己的经验处理相应的问题。遇到问题时,应当注意现象、复现步骤、运行环境等,通过工具检查、log分析、代码检查、监控、再优化等操作,把问题逐步细化,定位在可控范围,然后按照固定条件和变化某个条件进行排除,最终找出原因并择方案解决掉。
四、解决和规避
1、解决
上面11种情况进行解决
⑴、单例
对于应用开发,我们应当修改这句:
mSingleInstance = new SingleInstance(context)
改为:mSingleInstance = new SingleInstance(context.getApplicationContext());
不管外面传入什么Context,最终都会使用Applicaton的Context,而我们单例的生命周期和应用的一样长,这样就防止了内存泄漏。
对于系统而言,需要清楚其单例的作用范围和生命周期,尽量在生命周期内和作用范围内有效,不使用的时候注意销毁。
⑵、非静态内部类创建静态实例造成的内存泄漏
解决办法:将该内部类拿出来封装成一个单例,如果使用到了Context,则使用applicationContext。
⑶、Handler造成的内存泄漏
解决办法:首先把Handler类定义成静态的,然后显示的持有外部类的引用,但是这个持有不能是强引用,而是使用弱引用,这样当回收时就可以释放外部类的引用了,代码如下:
package com.example.testhandler;
import java.lang.ref.WeakReference;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
public class HandlerTest extends Activity {
private Handler mHandler = new MyHandler(this);
private static class MyHandler extends Handler {
private WeakReference<Context> mReference = null;
public MyHandler(Context context) {
mReference = new WeakReference<Context>(context);
}
@Override
public void handleMessage(android.os.Message msg) {
Context context = mReference.get();
if(context != null) {
// TODO
}
};
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
}
这样一改之后就可以避免activity的内存泄漏了,但是当还有消息队列里面还有消息或者正在处理最后一个消息时,虽然activity不会内存泄漏了,但是这些剩余的消息或者正在处理的消息会造成一些内存泄漏,所以最好的做法是在onStop方法或者onDestroy方法里把这些消息移除掉.使用mHandler.removeCallbacksAndMessages(null);是移除消息队列中所有消息和所有的Runnable。当然也可以使用mHandler.removeCallbacks();或mHandler.removeMessages();来移除指定的Runnable和Message。
⑷、线程造成的内存泄漏
解决办法:
将线程的内部类,改为静态内部类。
在线程内部采用弱引用保存Context引用。
package com.example.testthread;
import java.lang.ref.WeakReference;
import android.app.Activity;
import android.os.Bundle;
public class ThreadTest extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
protected void onResume() {
super.onResume();
new TestThread1(this).start();
}
private void dosomething() {
}
class TestThread1 extends Thread {
private WeakReference<ThreadTest> mReference = null;
public TestThread1(ThreadTest context) {
mReference = new WeakReference<ThreadTest>(context);
}
@Override
public void run() {
super.run();
if(mReference.get()!=null) {
mReference.get().dosomething();
}
}
}
}
⑸、AsyncTask
解决方式:采用弱引用的方式,将线程与Activity进行解耦,在Activity退出时取消异步任务。
如下代码:
package com.example.testasynctask;
import java.lang.ref.WeakReference;
import android.app.Activity;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
public class AsyncTaskTest extends Activity {
private static TestAsync mTestAsync = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
mTestAsync = new TestAsync(this);
mTestAsync.execute();
}
static class TestAsync extends AsyncTask<Void , Integer , String> {
private WeakReference<Context> mReference = null;
public TestAsync(Context context) {
mReference = new WeakReference<Context>(context.getApplicationContext());
}
@Override
protected String doInBackground(Void... arg0) {
if(mReference!=null && mReference.get()!=null) {
// TODO
}
return null;
}
}
@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
if(mTestAsync!=null) {
mTestAsync.cancel(true);
mTestAsync = null;
}
}
}
⑹、资源未关闭造成的内存泄漏
在开发过程中注意资源的使用,使用完成注意释放或者关闭、注销等操作,特别是一些比较重要的IO流、Cursor等。
⑺、未取消注册或回调导致内存泄露
解决:
针对广播,我们应道保持良好的习惯使用如下配对:
registerReceiver(mReceiver, new IntentFilter());
unregisterReceiver(mReceiver);
⑻、Timer和TimerTask导致内存泄露
解决:
因此当我们Activity销毁的时候要立即cancel掉Timer和TimerTask,以避免发生内存泄漏。如下代码:
package com.example.testtimer;
import java.util.Timer;
import java.util.TimerTask;
import android.app.Activity;
import android.os.Bundle;
public class TimerTest extends Activity {
private Timer mTimer;
private TimerTask mTimerTask;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mTimer = new Timer();
mTimerTask = new TimerTask() {
@Override
public void run() {
TimerTest.this.runOnUiThread(new Runnable() {
@Override
public void run() {
// TODO
}
});
}
};
mTimer.schedule(mTimerTask, 3000, 3000);
}
@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
if (mTimer != null) {
mTimer.cancel();
mTimer.purge();
mTimer = null;
}
if (mTimerTask != null) {
mTimerTask.cancel();
mTimerTask = null;
}
}
}
⑼、集合中的对象未清理造成内存泄露
解决:
在Activity退出后,应当清掉集合的数据并设置为空,这样垃圾回收器才有可能对其回收,如下代码:
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
if(map!=null) {
map.clear();
map = null;
}
}
⑽、属性动画造成内存泄露
解决:
因此同样要在Activity销毁的时候cancel掉属性动画,避免发生内存泄漏。
@Override
protected void onDestroy() {
super.onDestroy();
mAnimator.cancel();
}
⑾、WebView造成内存泄露
最终的解决方案是:在销毁WebView之前需要先将WebView从父容器中移除,然后在销毁WebView。
@Override
protected void onDestroy() {
super.onDestroy();
//先从父控件中移除WebView
mWebViewContainer.removeView(mWebView);
mWebView.stopLoading();
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.clearHistory();
mWebView.removeAllViews();
mWebView.destroy();
}
2、规避
对于部分暴露出来的内存问题,一时半会还没有办法解决的,需要找到方法尽量规避,但规避不是办法,还需要找到真正原因进行分析和解决掉。
3、优化
内存优化是一项长期而且比较艰难的工作,需要持续关系内存问题,不断走读代码和现象分析,造成可能的原因进行分析和寻找解决办法。
五、编程习惯
编程是一项不简单的工作,牺牲很多宝贵的时间和脑力还不一定能够做好,编程也是一项危险的工作,长期的坚持会先内伤,第一伤及身体,因为长时间的保持一个 动作,加上坐姿问题,午休随意摆放,可能坑到腰或者脖子;第二是编程需要不断自身学习和持续积累的过程,在某些领域可能是专家,但在不擅长的领域是什么靠 自己了。下面几点建议:
1、养成良好的编程习惯
编程是在不断地学习和强化的过程,把抽象的东西通过一定的思维展现出来,对自身编程应当坚持养成良好的习惯。健康方面注意长时间坐姿和午休睡姿的合理,眼睛的防护和适当参加锻炼;编码方面应当形成自己的习惯,学习高效的工具使用,代码风格、注释、文档记录等应当长期坚持。
2、不断积累和丰富经验
在现有的认识和条件下,不断积累和丰富自己的经验,一个开始就想写出很高质量的代码是很少人能做到的,大部分都是在工作奋斗中长期积累,遇到各种问题然后解决或者协同解决才能提升的,编程应当以工作中解决问题为出发点,同时提升自身能力为突破点。
3、交流
大 部分软件是一个团队协同才能完成的,编程过程中多多少少都遇到过问题,在某些时间内交流借鉴还是比较重要的,一方面能够分享自身掌握的经验,同时能获取别 人的经验,另一方面增强了交流互相了解,很多小问题都是在交流中解决掉,防止低级错误多次在不同的人犯。软件中的新功能或者遇到棘手问题时,交流可能更高 效,无论是同事之间还是网络之间,描述清楚问题很有必要,否则交流会遇到很大问题,所以需要交流提升自己的学习总结和锻炼口才与胆量。
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
android 中如何分析内存泄漏
Android应用程序的内存分析
Android内存分析和调优(中)
内存分析工具 MAT 的使用
Android内存泄漏分析及调试
Android 分析内存的使用情况
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服