Android内存泄露问题总结

这里笔者主要是把Android内存泄露发生的点拿出来进行了一下总结,方便个人查阅,可能还是不够全,后面看到更多的知识点慢慢再进行补充。

用来总结的内容来自知网论文以及网络资源,图片进行了重绘,引用的文章较多,总结的时候没记到底都引用了哪些文章,在此统一感谢最初发现各个问题及知识点的同学们。

PS:大家看了以后要是对您有那么一丢丢的帮助,请给个赞或者留言呗,兄弟这新开这博客来发一些学习记录,级别当前太低,需要刷刷分啊,andy在这谢谢大家了!


 Android内存问题综述

Android操作系统应用程序一直被诟病的问题有许多,尤其是相对比较复杂,程序较大的程序问题非常明显,例如界面滑动的卡顿、电量的过高消耗、屏幕变成黑屏等等。

以上这些问题现象,其原因多多少少都与内存有一定的关系。内存问题最直观的体现表现为Android应用程序对物理内存的不断占用,并且持续增高。当Android应用程序占用的内存增高到一定的阀值,上述界面卡顿、屏幕黑屏等等现象就会爆发出来。所以Android的内存问题是我们需要关注的绝对重点,也是最好的优化Android应用程序性能的切入点。同时,Android的内存问题也是一个难点,需要掌握的知识面较广,并且现象和成因较多,问题现象的数据化量化标准比较难确定,并且分析用的内存数据也较为复杂,需要具体分类并且分析提取有用的数据。

内存问题主要就是内存溢出OOM(Out Of Memory)还有就是内存泄露两类问题。下面我们就具体说明一下这内存泄露问题,稍后一篇再对OOM进行说明。

 内存泄露

内存泄露指的是内存中某些对象不再被应用程序所使用,但是却由于仍然被引用而导致垃圾收集器不能释放这些无用的对象。如图所示。


内存泄露问题不仅会发生在Dalvik虚拟机的内存管理中,同样通过Linux底层管理的Native层,即通过C/C++语言JNI方式编写的代码运行的层次,也会有内存泄露的风险。Native层通过C/C++语言编写的带代码中生成的内存中的对象使用完忘记释放,Dalvik虚拟机中通过Java语言中生成的对象被生命周期较长的引用一直指向,并且由于代码问题而不断的有冗余重复的对象产生,此时我们的应用程序便发生了内存泄露。

内存泄露大多是由于应用程序的开发人员疏忽而造成的代码问题引发的。短时间内对存在内存泄露问题的应用程序的使用,并不一定会体验到使用中发生的明显的问题现象,小的内存泄露并不会反映在应用程序的界面UI层。具体是否发生了内存泄露问题得通过监控PSS以及Dalvik虚拟机的内存变化来发现。同时定位较复杂,得具体分析内存数据。本文提供的工具将会通过内存数据的对比分析来定位增长点,从而达到定位问题代码的目的。

Android应用程序的内存泄露问题长时间持续发生,最终将有可能导致前文提到的内存溢出OOM问题的发生,问题拖延到产生明显的问题现象发生时才发现就已经很晚了,通常用户反馈到应用程序开发者的内存问题现象基本就是这种应用程序中的问题隐患在应用程序发布后才爆发的体现。此时已经是开发事故了,修改的成本很大,需要重新发布版本,所以内存泄露的问题隐患是我们排查内存问题的重要一环。

具体的Android应用程序内存泄露发生的原因有很多。下文将一一说明并分析泄露原因。


1、Context造成的内存泄露

Context泄露是Android应用程序中最常见的内存泄露问题原因。

这种内存泄露属于Java语言中生成的对象被生命周期较长的引用一直指向,已经无用的Activity对象由于通过Activity Context被传递给生命周期长于它的对象从而Activity对象无法及时被释放。


如图所示,对象A引用了对象B,对象A的生命周期(t1 - t4) 比对象B的生命周期(t2 - t3)要长得多。当B在应用程序逻辑中不会再被使用以后,对象A仍然持有着B的引用。在这种情况下垃圾收集器不能将对象B从内存中释放。假若对象A 还持有着其他对象的引用,那么这些被引用的无用的对象都不会被回收,并占用着内存空间。甚至可能对象B也持有一大堆其他对象的引用。这些被引用的对象由于被B所引用,也不会被垃圾收集器所回收。

Android平台中Context最主要的功能室加载和访问资源,在Android平台中有两种Context,一种是Application Context,一种是Activity Context,通常我们在各种类和方法间传递的是Activity Context。当一旦Context发生内存泄漏问题,Activity也会因为与Context存在的引用关系而无法被垃圾回收器回收释放。

这里我们可以使用Application Context来替代Activity Context。因为Application Context伴随Android应用程序的完整生命周期,与具体的Activity的生命周期无关。Application Context可以通过调用Context.getApplicationContext或者Activity.getApplication的方法来获取。

我们为了避免由于Context造成的相关的内存泄露,需要注意以下几点:

1、不要令生命周期较长的对象引用Activity Context,尽量保证引用Activity的对象要和Activity本身的生命周期是一致的。

2、对于生命周期较长的对象,我们可以使用ApplicationContext来取代ActivityContext。

3、避免使用非静态的内部类,尽量使用静态类,代码中要注意内部类对外部对象引用所导致的内存中的对象生命周期变化,避免由于不同对象的生命周期而引发的内存泄露问题。

2、Handler造成的内存泄露

Handler造成的应用程序内存泄漏问题也就是由于Inner Class(内部类)造成的内存泄漏问题。

我们已如下代码举例说明,先看下面的代码。

public class MainActivity extendsActivity {
      private Handler mHandler = new Handler() {
             @Override
             public voidhandleMessage(Message msg) {
                    // TODO handle message...
             }
      };
 
      @TargetApi(11)
      @Override
      public voidonCreate(Bundle savedInstanceState) {
             super.onCreate(savedInstanceState);
             setContentView(R.layout.activity_main);
             mHandler.sendMessageDelayed(Message.obtain(),60000);
             // just finish thisactivity
             finish();
      }
}

我们使用AndroidLint工具进行分析会报一个内存泄露的警告。警告内容:This Handler class should be static orleaks might occur. Issue: Ensures that Handler classesdo not hold on to a reference to an outer class。

这里我们针对上述代码进行分析,首先我们需要了解如下几个知识点。

1、在Android应用程序启动的时候,会优先创建一个应用程序主线程的Looper对象,Looper具体实现了一个简单的消息队列,用于一个一个的处理里面的Message对象。主线程的Looper对象在整个应用程序的生命周期中一直存活。

2、当在应用程序主线程中初始化Handle的时候,该Handle和Looper的消息队列被相互关联起来。发送到消息队列的Message消息会引用发送该条消息的Handler对象来处理。这样的话,Android系统可以通过Handler#handleMessage(Message)这个方法来分法处理该消息。这个机制即回调机制。

3、在Java语言中,非静态内部类即匿名内部类会引用外部类对象,而静态内部类不会引用外部类对象。

4、如果外部类是Activity,则会引起Activity泄露。

当Activity 执行完后,延时消息会继续存在于主线程消息队列中一分钟的时间,然后处理消息。而这个消息引用了Activity的Handler对象,然后这个Handler又引用了这个Activity。这里说的这些引用对象会保持到该消息被处理完为止,这样就会导致该Activity对象无法被垃圾回收机制回收,从而导致了上面说的内存的Activity泄露。

3、Drawable回调造成的内存泄露

Drawable类型的对象要求它的调用者实现Drawable.Callback这个接口,目的是被Drawable用来处理与动画相关的任务。所以,如果调用了Drawable的View是被Activity移除,但是Drawable仍然在被引用,进而就会导致view也任然在被引用,于是View对象就不能被垃圾回收机制回收。具体的我们可以看下面的代码。

<pre name="code" class="java">public classMyActivity extends Activity {
private static Drawablebackground;
protected voidonCreate(Bundle state){
       super.onCreate(state);
       TextView label = new TextView(this);
       label.setText("Leaks will happen");
       if (background == null) {
         background = getDrawable(R.drawable.large_bitmap);
       }
       label.setBackgroundDrawable(background);
       setContentView(label);
}
}

 

这段程序看起来是非常简单的,但是却存在着很大的内存泄露风险。当移动终端屏幕旋转的时候,即竖屏界面与横屏界面切换的时候,内存就会发生泄露,这个时候GC没法销毁此处的Activity。我们在这段代码中使用了一个static类型的Drawable对象,通常我们这么做是因为我们需要经常调用一个Drawable对象,而其加载又比较耗时,不希望每次加载Activity的时候都去创建这个Drawable对象。此时,我们使用static来修饰这个对象,使其变成静态对象常驻内存中是最快的代码实现方式 ,但是这么做就为内存泄露问题埋下了隐患。

当一个Drawable对象被附加到一个View上的时候,这个View会调用Drawable的回调函数Drawable.setCallback(),这里的回调就意味着此处的Drawable对象background和View类型的lable关联后,Drawable对象保存了View对象的引用信息,即background保存了label的引用,而label又保存了Activity的引用。既然Drawable对象不能被GC销毁,它所直接引用和间接引用的对象都不能被GC销毁,这样系统的内存回收机制GC就没有办法销毁当前的Activity,于是就造成了内存泄露问题的隐患。当Android移动设备屏幕发生旋转的时候,系统内部会销毁当前的Activity,为切换后的界面创建一个新的Activity,此时前面的隐患就会爆发成真正的内存泄露问题。

Java语言的垃圾回收器GC对这种类型的内存泄露问题是无能为力的。我们避免这类内存泄露的主要方法就是避免Activity中任何对象的 生命周期比Activity对象长,避免由于Activity内的对象对 Activity直接或间接的引用导致的Activity不能被内存垃圾回收机制正常销毁回收。

4、数据库没有关闭游标造成的内存泄露

Androidy应用程序中经常要进行查询本地数据库的一些操作,但是代码中经常会发生在使用完Cursor对象后没有对其进行关闭的情况。如果应用程序代码中的查询结果比较小,那么对内存的消耗并不明显,此时不容易被发现已经发生内存泄露,只有在应用程序长时间的大量操作数据库的情况下才会明显发现因为数据库的游标没有被关闭而引发的内存泄露问题,这样的话就会给之后的测试和内存泄露问题暴露出来时的定位排查带来一定的困难和风险[31]

我们可以看一下以下存在由于数据库没有关闭游标而造成内存泄露问题隐患的示例代码。

Cursor cursor = getContentResolver().query(uri ...);
if(cursor.moveToNext()){
…… 
}


       上述这类代码的修改也十分容易,我们只需要时刻谨记对数据库游标进行关闭就可以很好地避免此类问题。存在内存泄露隐患的代码可以如下修正。

Cursor cursor = null;
try{
cursor= getContentResolver().query(uri ...);
if (cursor != null && cursor.moveToNext()){
 ... ... 
}
}finally{
if (cursor != null){
try { 
cursor.close();
             }catch(Exception e){
                    //ignore this
             }
      }
}

       Java语言的机制,可以保证finally必然被调用,这样Cursor对象就会调用close()方法被关闭,而不会再造成内存泄露的问题。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值