Android常见内存泄漏

Android常见内存泄漏

标签(空格分隔): 性能优化


性能优化是一个大的范畴,如果有人问你在Android中如何做性能优化的,也许都不知道从哪开始说起。

首先要明白的是,为什么我们的App需要优化,最显而易见的时刻:用户say,什么狗屎,刷这么久都没反应,取关卸载算了。

这跟什么有关,我们先苍白的反驳下,尼玛用户设备老旧网又烂,关我屁事,根本不用优化。可是,老板拍板了,施压给CTO,然后CTO又来找你:Y的今天必须给我想办法优化了,不然不准回家—引用自Stay

优化之前,首先我们需要了解一下跟进程相关的一些基本内容。

  • app进程为毛容易出现ooM.

    这个是因为Android系统对dalvik的vm heapsize作了硬性限制,当java进程申请的java空间超过阈值时,就会抛出OOM异常(这个阈值可以是64M、32M、16M等,视机型而定),可以通过adb shell getprop | grep dalvik.vm.heapgrowthlimit查看此值。

    也就是说,程序发生OMM并不表示RAM不足,而是因为程序申请的java heap对象超过了dalvik vm heapgrowthlimit。也就是说,在RAM充足的情况下,也可能发生OOM。

    这样的设计似乎有些不合理,但是Google为什么这样做呢?这样设计的目的是为了让Android系统能同时让比较多的进程常驻内存,这样程序启动时就不用每次都重新加载到内存,能够给用户更快的响应。迫使每个应用程序使用较小的内存,移动设备非常有限的RAM就能使比较多的app常驻其中。

  • 内存泄漏
    内存泄漏跟内存溢出不是一个概念,内存泄漏:简单说来就是内存不在GC的掌控之中,内存泄漏的结果会造成内存溢出。

首先了解一下内存的集中策略:

  1. 静态的:内存在程序编译的时候就已经分配好,这块内存在程序整个运行过程中一直都存在,主要存放静态数据、全局的static数据和一些常量。

  2. 栈式的:在执行函数时,函数一些内部变量的存储都可以放在栈上创建,函数执行结束,这些存储单元自动释放。栈内存包括分配的运算,速度很快,因为内置在处理器里面。当然容量有限。

  3. 堆式的:也叫动态内存分配。有时候用malloc或者new来申请分配一个内存,在c/c++自己掌控内存,java没有什么方法解决。成员变量全部存储在堆中(包括基本数据类型引用和引用的对象实体)–因为他们属于类,类对象最终是要被new出来的;局部变量的基本数据类型和引用存储于栈当中引用的的对象实体存储在堆中—因为他们属于方法中的变量,随着方法结束生命周期

区别
1. 堆式不连续的内存,堆空间比较大灵活。栈式一块连续的内存区域,大小是操作系统决定的。
2. 堆内存管理很麻烦,频繁的new/remove会造成大量的内存碎片,这样就会慢慢导致效率低下。对于栈内存的话,先进后出,进出完全不会产生碎片,运行效率高且稳定;

我们所讨论的内存泄漏,主要讨论堆内存,他存放的就是引用指向的对象实体。

  • StrongReference强引用:回收时机,从不回收,使用:对象一般保存生命周期jvm停止的时候才会停止。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
  • SoftReference 软银用:回收时机;当内存不足的时候,使用:软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。;生命周期:内存不足时终止。软引用可用来实现内存敏感的高速缓存
  • WeakReference弱引用:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。,生命周期:GC后终止。
    弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。构建非敏感数据的缓存
  • PhatomReference虚引用:回收时机:在垃圾回收的时候,生命周期:GC后终止,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收
    建议
    开发时,为了防止内存溢出,处理一些占用内存大且生命周期长的对象的时候,尽量使用软引用和弱引用
/*******************下面看一些常见的内存泄漏例子******************/
  • -
public class CommUtil{
    private static instance;
    private Context context;

    private CommUtil(Context context){
        this.context = context;
    }

    public static CommUtils getInstance(Context context){
        if(instance == null){
            instance = new CommUtil(context);
        }
        return instance;
    }

}

public class Main extends Activity{

    @Override
    public void onCreate(Bundle bundle){
     CommUtil util = CommUtil.getInstance(this);
    }

}


/**
分析:当MainActivity将要回收时,由于CommonUtil持有MainActivity的强引用,此时jvm根本不能回收(也就是内存泄露了),当MainActivity重走生命周期时,会重新创建一个MainActivity,此时堆内存中就会存在两个activity。

解决方案:能用Application的context就用Application的,因为CommUtil跟Application生命周期同生死
*/
  • 非静态内部类引起内存泄露
public class Main extends Activity{
    private int a = 1;
    public void onCreate(Bundle bundle){
        loadData();
    }
}

 public void loadData(){
        new Thread(new Runnable() {
            @Override
            public void run() {
            while(true){
                try {
                int b=a;
                Thread.sleep(1000);
                } catch (InterruptedException e) {
                e.printStackTrace();
                }
            }
            }
        }).start();
       }

   /**
    分析:如果是现在这种情况,直接finish(),你以为Main释放了,其实没什么卵用。
    因为匿名内部类持有Main的强引用,jvm同样不能回收Main,造成内存泄漏。
    方案:将非静态内部类修改为静态内部类。(静态内部类不会隐士持有外部类的引用,但此时静态内部类不能直接引用外部类的成员变量和函数)
   */

    private static class MyHandler extends Handler{

        private WeakReference<MainActivity> mainActivity;//设置弱引用保存,当内存一发生GC的时候就会回收。

        public MyHandler(MainActivity mainActivity) {
            this.mainActivity = new WeakReference<MainActivity>(mainActivity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            MainActivity main =  mainActivity.get();
            if(main==null||main.isFinishing()){
                return;
            }
            switch (msg.what){
                case 0:
                    //加载数据
                    int b = main.a;
                    break;

            }
        }
    };
  • 不需要用的监听未移除会发生内存泄露
//add监听,放到集合里面
        tv.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
            @Override
            public void onWindowFocusChanged(boolean b) {
                //监听view的加载,view加载出来的时候,计算他的宽高等。

                //计算完后,一定要移除这个监听
                tv.getViewTreeObserver().removeOnWindowFocusChangeListener(this);
            }
        });

看一下源码实现:

    public void addOnWindowFocusChangeListener(OnWindowFocusChangeListener listener) {
        checkIsAlive();

        if (mOnWindowFocusListeners == null) {
            mOnWindowFocusListeners
                    = new CopyOnWriteArrayList<OnWindowFocusChangeListener>();
        }

        mOnWindowFocusListeners.add(listener);
    }

此处可知道,其实listeners 被放在了CopyOnWriteArrayList中,所以如果不移除的话,CopyOnWriteArrayList一直会持有监听引用,造成内存泄漏
  • 资源未关闭引起的内存泄露情况
    比如:BroadCastReceiver、Cursor、Bitmap、IO流、自定义属性attribute attr.recycle()回收。
    当不需要使用的时候,要记得及时释放资源。否则就会内存泄露。
  • 无限循环动画
    没有在onDestroy中停止动画,否则Activity就会变成泄露对象。
    比如:轮播图效果。

以上就是我总结的常见的一些内存泄漏的情况。

知道了会存在内存泄漏,那我们怎么样去寻找当前项目中的可能存在的内存泄漏?

  • 确定是否存在内存泄漏
    • Android Monitors工具的内存分析
      最直观的看内存增长情况,知道该动作是否发生内存泄露。重复执行某一操作,然后主动GC(initiate GC)后,观察此时内存的大小,如果有明显增长,肯定有内存泄漏的情况。
    • 使用MAT内存分析工具
      MAT分析heap的总内存占用大小来初步判断是否存在泄露。
      我们反复执行某一个操作并同时执行GC排除可以回收掉的内存,注意观察histogram的object size值,正常情况下object Size值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况。反之如果代码中存在没有释放对象引用的情况,随着操作次数的增多Object Size的值会越来越大。那么这里就已经初步判断这个操作导致了内存泄露的情况。
  • 找怀疑对象(哪些对象属于泄露的)
    MAT对比操作前后的hprof来定位内存泄露是泄露了什么数据对象。(这样做可以排除一些对象,不用后面去查看所有被引用的对象是否是嫌疑)
    快速定位到操作前后所持有的对象哪些是增加了(GC后还是比之前多出来的对象就可能是泄露对象嫌疑犯)
    技巧:Histogram中还可以对对象进行Group,比如选择Group By Package更方便查看自己Package中的对象信息。
  • MAT分析hprof来定位内存泄露的原因所在。(哪个对象持有了上面怀疑出来的发生泄露的对象)
    1)Dump出内存泄露“当时”的内存镜像hprof,分析怀疑泄露的类;
    2)把上面2得出的这些嫌疑犯一个一个排查个遍。步骤:
    (1)进入Histogram,过滤出某一个嫌疑对象类
    (2)然后分析持有此类对象引用的外部对象(在该类上面点击右键List Objects—>with incoming references)
    (3)再过滤掉一些弱引用、软引用、虚引用,因为它们迟早可以被GC干掉不属于内存泄漏(在类上面点击右键Merge Shortest Paths to GC Roots—>exclude all phantom/weak/soft etc.references)
    (4)逐个分析每个对象的GC路径是否正常
    此时就要进入代码分析此时这个对象的引用持有是否合理,这就要考经验和体力了!

另外还有LeakCanary,Lint分析工具都是非常好用的工具,对我们写出高质量的代码,减少内存泄漏都非常有用。
性能优化方面,请参考 stay写的:那些Android上的性能优化

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值