Android内存泄漏完整解决方案

1、概述

Android开发中经常出现内存泄漏问题,本文将从发现问题,确定问题,分析问题,解决问题四个方面解决内存泄漏问题。

内存泄漏:

java是有垃圾回收机制的,JVM会派出一些回收线程不定时地回收那些不再需要的内存空间,回收的不是对象本身,而是对象占据的内存空间。

java凭引用来和对象进行关联,通过引用来操作对象。如果一个对象没有与任何引用关联,那么这个对象也就不太可能被使用到了,回收器便是把无任何引用的对象作为目标,回收了它们占据的内存空间。

判断对象有无引用:

这里写图片描述
上图中“GC Roots“为垃圾回收器对象,垃圾回收检查的起点。“箭头“代表对象之间的引用关系。“圆“代表内存中对象。

系统设置了一系列的“GC Roots“对象作为索引起点,如果一个对象与起点对象之间均为可达路径,那么这个不可达的对象就会成为回收对象。

内存泄漏产生原因:

虽然垃圾回收器会帮我们回收大部分无用的内存空间,但是对于还保存着引用,但逻辑上已经不再用到的对象,垃圾回收器就不会再回收他们。这些对象积累在内存中,直到程序结束,所以导致内存泄漏。

这里写图片描述

上图中E对F保持引用,但F已经不再用到了,垃圾回收时,F不能被回收,导致内存泄漏。

2、发现问题

(1)连续打开应用或界面后,界面卡顿,动画不流畅;
(2)操作过程中,Logcat频繁输出GC日志。

通过上述两个方面可初步发现内存泄漏问题。

3、确定问题

(1)反复操作观察内存变化

频繁打印GC日志,说明系统频繁触发GC来释放内存,内存泄漏常见变现为程序使用时间越长,内存占用越多。那我们通过反复操作应用,比如反复点开/关闭页面,观察内存变化状况是否一点点上涨,可以粗略地判断是否有内存泄漏。

通过DDMS的heap工具查看应用内存的使用情况

这里写图片描述

  1. 选择Tools -> Android -> Android Device Monitor;
  2. 选择需要测试进程;
  3. 点击update heap按钮,DDMS通知应用收集信息;
  4. 点击Cause GC,load内存信息;
  5. 连续打开应用,或反复打开关闭页面,观察各对象的Total Size变化,验证是否有内存泄漏;

通过Android Monitor查看

这里写图片描述

通过Android Monitor我们可以实时看到APP在运行时的Memory、CPU、GPU和Network的消耗情况。

4、分析问题

(1)DDMS Dump HPROF file

1、导出.prof文件
通过3中的DDMS操作步骤找到需要调试的APP进程,点击“update heap“ 按钮,可以先主动发出一次GC,待内存占用数据稍微稳定下来后,点击“DDMS Dump HPROF file“导出.prof文件。

2、分析.prof文件

这里写图片描述

Android studio能直接打开.prof文件。打开Analyzer Tasks面板,点击run分析。

分析完成后, 如果有内存泄漏在Analysis Results面板中会出现Leaked Activities目录,里面就是泄漏的Activity对象。找出这个Activity分析泄漏原因。

(2)分析内存泄漏原因工具:MAT(Memory Analyzer Tool)

打开MAT工具,如果没有,请下载

MAT是用来分析Java程序的hprof文件,与Android导出的hprof有格式区别,因此需要先进行转换。

在Android sdk的platform-tools有hprof-conv工具进行格式转化:

hprof-conv 源文件 输出文件
hprof-conv old.hprof new.hprof

格式转化后,用MAT工具打开new.hprof文件,

正常打开后如下图所示:

这里写图片描述

Leak Suspects视图展示了app内存占用的比例,浅色是空闲的内存,其他是内存占用的空间。每块内存对应的问题也都列在下面。点开每个Problem Suspect下的details,可以看到有哪些类的实例占用了内存和占用大小等信息

我们就可以查看当前内存中存在的对象了,由于内存泄漏一般发生在Activity中,因此只需要查找Activity即可。

点击下图中标记的QQL图标 输入 select * from instanceof android.app.Activity 查找Activity的相关信息,点击红色叹号执行:

这里写图片描述

如上图所示, 其中内存中还存在 6个MainActivity实例,但是我们是想要全部退出的,这表明出现了内存泄漏。

其中有 Shallow size 和 Retained Size两个属性。

Shallow Size
对象自身占用的内存大小,不包括它引用的对象。针对非数组类型的对象,它的大小就是对象与它所有的成员变量大小的总和。
当然这里面还会包括一些java语言特性的数据存储单元。针对数组类型的对象,它的大小是数组元素对象的大小总和。

Retained Size
Retained Size=当前对象大小+当前对象可直接或间接引用到的对象的大小总和。(间接引用的含义:A->B->C, C就是间接引用)
不过,释放的时候还要排除被GC Roots直接或间接引用的对象。他们暂时不会被被当做Garbage。

右击一个MainActivity,选择 with all references :
这里写图片描述

打开下图:

这里写图片描述

this0引用这个Activity,this0表示内部类,也就是一个内部类引用了Activity 而 this$0又被 target引用 target是一个线程,原因找到了,内存泄漏的原因就是 Activity被内部类引用而内部类又被线程使用因此无法释放,查看此类代码。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(6*1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        new Thread(runnable).start();
    }
}

在MainActivity中存在Runnable 内部类对象,然后又被线程使用,而线程要执行6秒,因此 MainActivity对象被引用无法释放,导致了内存溢出。
要解决这种的内存溢出,要及时在Activity退出时结束线程,或者良好的控制线程执行的时间即可。

这样我们就找出了这个程序中的内存溢出。

(3)LeakCanary工具内存检测

LeakCanary是square公司推出的一款简单粗暴的检测内存泄漏的工具。

LeakCanary会检测应用的内存回收情况,如果发现有垃圾对象没有被回收,就会去分析当前的内存快照,也就是上边MAT用到的.hprof文件,找到对象的引用链,并显示在页面上。这款插件的好处就是,可以在手机端直接查看内存泄露的地方,可以辅助我们检测内存泄露。

详细LeakCanary,请查看这里,以后有时间我会再来单独分析这块。

5、内存泄漏的情景和解决办法

(1)使用关于application的context来替代和activity相关的context

这是一个很隐晦的内存泄漏的情况。有一种简单的方法来避免context相关的内存泄漏。最显著地一个是避免context逃出他自己的范围之外。使用Application context。这个context的生存周期和你的应用的生存周期一样长,而不是取决于activity的生存周期。如果你想保持一个长期生存的对象,并且这个对象需要一个context,记得使用application对象。你可以通过调用 Context.getApplicationContext() or Activity.getApplication()来获得。

(2)集合中对象没清理造成的内存泄漏

我们通常把一些对象的引用加入到了集合中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。

(3)构造Adapter时,没有使用缓存的convertView

以构造ListView的BaseAdapter为例,在BaseAdapter中提供了方法:
public View getView(int position, ViewconvertView, ViewGroup parent)
来向ListView提供每一个item所需要的view对象。初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的 view对象,同时ListView会将这些view对象缓存起来。当向上滚动ListView时,原先位于最上面的list item的view对象会被回收,然后被用来构造新出现的最下面的list item。这个构造过程就是由getView()方法完成的,getView()的第二个形参View convertView就是被缓存起来的list item的view对象(初始化时缓存中没有view对象则convertView是null)。由此可以看出,如果我们不去使用 convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费资源也浪费时间,也会使得内存占用越来越大。

错误代码:

public View getView(int position, ViewconvertView, ViewGroup parent) { 

    View view = new Xxx(...); 
    ... ... 

    return view;  
} 

修正代码:

public View getView(int position, ViewconvertView, ViewGroup parent) { 
    View view = null; 
    if (convertView != null) { 
        view = convertView; 
        populate(view, getItem(position)); 
        ... 
    } else { 
        view = new Xxx(...); 
    ... 
    } 
    return view; 
} 

(4)资源对象没关闭造成的内存泄漏

资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于 java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。因为有些资源性对象,比如 SQLiteCursor(在析构函数finalize(),如果我们没有关闭它,它自己会调close()关闭),如果我们没有关闭它,系统在回收它时也会关闭它,但是这样的效率太低了。因此对于资源性对象在不使用的时候,应该调用它的close()函数,将其关闭掉,然后才置为null.在我们的程序退出时一定要确保我们的资源性对象已经关闭。

程序中经常会进行查询数据库的操作,但是经常会有使用完毕Cursor后没有关闭的情况。如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险。

(5)注册没取消造成的内存泄漏

一些Android程序可能引用我们的Anroid程序的对象(比如注册机制)。即使我们的Android程序已经结束了,但是别的引用程序仍然还有对我们的Android程序的某个对象的引用,泄漏的内存依然不能被垃圾回收。调用registerReceiver后未调用unregisterReceiver。

(6)static变量引起的内存泄漏
因为static变量的生命周期是在类加载时开始类卸载时结束,也就是说static变量是在程序进程死亡时才释放,如果在static变量中引用了Activity 那么这个Activity由于被引用,便会随static变量的生命周期一样,一直无法被释放,造成内存泄漏。

解决办法:
在Activity被静态变量引用时,使用 getApplicationContext 因为Application生命周期从程序开始到结束,和static变量的一样。

(7)线程造成的内存泄漏

类似于上述例子中的情况,线程执行时间很长,及时Activity跳出还会执行,因为线程或者Runnable是Acticvity内部类,因此握有Activity的实例(因为创建内部类必须依靠外部类),因此造成Activity无法释放。
AsyncTask有线程池,问题更严重。

解决办法:
1.合理安排线程执行的时间,控制线程在Activity结束前结束。
2.将内部类改为静态内部类,并使用弱引用WeakReference来保存Activity实例因为弱引用只要GC发现了就会回收它 ,因此可尽快回收

(8)BitMap占用过多内存
bitmap的解析需要占用内存,但是内存只提供8M的空间给BitMap,如果图片过多,并且没有及时 recycle bitmap 那么就会造成内存溢出。

解决办法:
及时recycle 压缩图片之后加载图片

(9)Handler的使用造成的内存泄漏

由于在Handler的使用中,handler会发送message对象到 MessageQueue中 然后 Looper会轮询MessageQueue 然后取出Message执行,但是如果一个Message长时间没被取出执行,那么由于 Message中有 Handler的引用,而 Handler 一般来说也是内部类对象,Message引用 Handler ,Handler引用 Activity 这样 使得 Activity无法回收。

解决办法:
依旧使用 静态内部类+弱引用的方式可解决

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 黑客帝国 设计师:上身试试 返回首页