分享

Android开发常见内存泄漏和相应的对策(二)

 宇宙之彬 2021-07-23

原创 looshen09 印象Android 2018-08-05

三、定位分析内存问题

1log分析

Android系统中,GC有以下三种类型:

kGcCauseForAlloc:在分配内存时发现内存不够的情况下引起的GC,这种情况下的GCStop WorldStop 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日志有以下几种类型:

AGC_CONCURRENT:当应用进程中的Heap内存占用上涨时,避免因Heap内存满了而触发GC

BGC_FOR_MALLOC:这是由于Concurrent GC没有及时执行完,而应用又需要分配更多的内存,这时不得不停下来进行Malloc GC

CGC_EXTERNAL_ALLOC:这是为external分配的内存执行的GC

DGC_HPROF_DUMP_HEAP:创建一个HPROF profile的时候执行。

EGC_EXPLICIT:显式地调用了System.gc()

一般来说,可以信任系统的GC机制,尽量不去显式调用System.gc(),减少不必要的系统开销,影响应用的流畅度。

对上面日志的解析:

free 1049K表明在这次GC中回收了多少内存。

60% free 3571K/9991KHeap的一些统计数据,表明这次回收后60%Heap可用,存活的对象大小为2341KBHeap大小是9351KB

External 3502K/6261KNative Memory的数据。存放位图数据(Bitmap Pixel Data)或者堆以外内存(NIO Direct Buffer)之类的。第一个数字表明Native Memory中已分配了多少内存,第二个值有点类似一个浮动的阈值,表明分配内存达到这个值,系统就会触发一次GC进行内存回收。

Paused 3ms 3ms表明GC暂停的时间。从这里可以看到,越大的Heap SizeGC时会导致暂停的时间越长。如果是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

  1. Android Studio上运行需要监控的应用。

  2. Android Studio菜单栏中选择Tools--->Android--->Memory Monitor。或者单击Android Studio应用程序面板右下角的Android图标,直接运行Memory Monitor,当然这个跟Android Studio版本不一样可能地方不太一样,如下图红色圈住的部分,红色是选择设备,红色是选择哪个应用。

图片

  1. 一旦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 ViewerAndroid StudioAndroid 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 TrackerAndroid StudioEclipse上都支持,在Android Studio上使用Allocation Tracker界面更加清晰和有条理,但两个IDE上使用Allocation Tracker的功能都是相同的。运行应用后,切换到Android选项卡(Memory Monitor是同一个视图)

  1. 单击启动追踪按钮(Start Allocation Tracking)

  2. 操作应用,怀疑有内存泄漏或者内存变化较大的操作。

  3. 单击结束追踪按钮,与启动追踪按钮时同一个位置,如下图:

图片

图片

4.自动生成一个alloc结尾的文件,这个文件记录了这次追踪到的所有内存数据,并且在Android Studio中自动打开一个数据面板,显示当前生成alloc文件的内存数据,如下图

图片

针对上图作如下说明:

A、有两个选项,分别是Group by Method(用方法来分类我们的内存分配)Group by Allocator(用内存分配器来分类我们的内存分配),不同的选项,在D区显示的信息会不同,默认会以Group by Method来组织。

BJump 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./mat/1.3/update-site/。在AndroidStudio并没有集成MAT工具,需要下载MAT独立客户端,下载地址为:https:///mat/download.php

获取HPROF文件。EclipseAndroid 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是一个总体概览,显示总体的内存消耗情况和疑似问题。分析内存常用的是HistogramDominator Tree两个视图,这两个视图的区别是统计的维度不一样,但使用Dominator Tree可以方便地查看其引用关系。

  1. Histogram,列出内存中的所有实例类型对象、对象的个数以及大小,并支持正则表达式查找。

  2. Dominator Tree,列出最大的对象及其依赖存活的Object。分析流程和Histogram大同小异,但Dominator Tree能更方便地查看出引用关系。

  3. Top Consumers,通过图形列出最大的Object

  4. 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,最终都会使用ApplicatonContext,而我们单例的生命周期和应用的一样长,这样就防止了内存泄漏。

对于系统而言,需要清楚其单例的作用范围和生命周期,尽量在生命周期内和作用范围内有效,不使用的时候注意销毁。

、非静态内部类创建静态实例造成的内存泄漏

解决办法:将该内部类拿出来封装成一个单例,如果使用到了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();来移除指定的RunnableMessage

、线程造成的内存泄漏

解决办法:

将线程的内部类,改为静态内部类。

在线程内部采用弱引用保存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);

TimerTimerTask导致内存泄露

解决:

因此当我们Activity销毁的时候要立即cancelTimerTimerTask,以避免发生内存泄漏。如下代码:

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交流

大 部分软件是一个团队协同才能完成的,编程过程中多多少少都遇到过问题,在某些时间内交流借鉴还是比较重要的,一方面能够分享自身掌握的经验,同时能获取别 人的经验,另一方面增强了交流互相了解,很多小问题都是在交流中解决掉,防止低级错误多次在不同的人犯。软件中的新功能或者遇到棘手问题时,交流可能更高 效,无论是同事之间还是网络之间,描述清楚问题很有必要,否则交流会遇到很大问题,所以需要交流提升自己的学习总结和锻炼口才与胆量。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多