Android内存泄漏原因及解决办法

作者: 881ef7b85f62 | 来源:发表于2019-06-12 15:40 被阅读2次

    前言

    面试中最常问的就是:“你了解Android内存泄漏和Android内存溢出的原因吗,请简述一下” ,然后大多数的人都能说出原因及其例子和解决办法,但是实际项目中稍微不注意还是会导致内存泄漏,今天就来梳理一下那些是常见的内存泄漏写法和解决方法。

    原因

    内存泄漏的原理很多人都明白,但是为了加强大家的防止内存泄漏的意识,我再来说一遍。说到内存泄漏的原理就必须要讲一下Java的GC的。Java之所以这么流行不仅仅是他面向对象编程的方式,还有一个重要的原因是因为,它能帮程序员免去释放内存的工作,但Java并没有我们想象的那么智能,它进行内存清理还得依靠固定的判断逻辑。

    Java的GC可分为

    引用计数算法

    给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;在任何时刻计数器的值为0的对象就是不可能再被使用的,也就是可被回收的对象。这个原理容易理解并且效率很高,但是有一个致命的缺陷就是无法解决对象之间互相循环引用的问题。如下图所示

    可达性分析算法

    针对引用计数算法的致命问题,可达性分析算法能够轻松的解决这个问题。可达性算法是通过从GC root往外遍历,如果从root节点无法遍历该节点表明该节点对应的对象处于可回收状态,如下图中obj1、obj2、obj3、obj5都是可以从root节点出发所能到达的节点。反观obj4、obj6、obj7却无法从root到达,即使obj6、obj7互相循环引用但是还是属于可回收的对象最后被jvm清理。

    看了这些知识点,我们再来寻找内存泄漏的原因,Android是基于Java的一门语言,其垃圾回收机制也是基于Jvm建立的,所以说Android的GC也是通过可达性分析算法来判定的。但是如果一个存活时间长的对象持有另一个存活时间短的对象就会导致存活时间短的对象在GC时被认定可达而不能被及时回收也就是我们常说的内存泄漏。Android对每个App内存的使用有着严格的限制,大量的内存泄漏就可能导致OOM,也就是在new对象请求空间时,堆中没有剩余的内存分配所导致的。

    既然知道了原理那么平时什么会出现这种问题和怎么合理的解决这种问题呢。下面来按实例说话。

    内存泄漏的例子

    Handler

    说到Handler这个东西,大家平时肯定没少用这玩意,但是要是用的不好就非常容易出现问题。举个例子

    public Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
          super.handleMessage(msg);
          toast("handlerLeakcanary");
        }
      };
    
    private void handlerLeakcanary(){
        Message message = new Message();
        handler.sendMessageDelayed(message,TIME);
      }
    
    

    老实说写过代码的人肯定很多。其中不乏了解内存泄漏原理的人。但是平时需要多的时候一不小心就可能写下这气人的代码。

    了解Handler机制的人都明白,但message被Handler send出去的时候,会被加入的MessageQueue中,Looper会不停的从MessageQueue中取出Message并分发执行。但是如果Activity 销毁了,Handler发送的message没有执行完毕。那么Handler就不会被回收,但是由于非静态内部类默认持有外部类的引用。Handler可达,并持有Activity实例那么自然jvm就会错误的认为Activity可达不就行GC。这时我们的Activity就泄漏,Activity作为App的一个活动页面其所占有的内存是不容小视的。那么怎么才能合理的解决这个问题呢

    1、使用弱引用

    Java里面的引用分为四种类型强引用、软引用、弱引用、虚引用。如果有不明白的可以先去了解一下4种引用的区别

     public static class MyHandler extends Handler{
        WeakReference<ResolveLeakcanaryActivity> reference;
    
        public MyHandler(WeakReference<ResolveLeakcanaryActivity> activity){
          reference = activity;
        }
    
        @Override
        public void handleMessage(Message msg) {
          super.handleMessage(msg);
          if (reference.get()!=null){
            reference.get().toast("handleMessage");
          }
        }
      }
    
    

    引用了弱引用就不会打扰到Activity的正常回收。但是在使用之前一定要记得判断弱引用中包含对象是否为空,如果为空则表明表明Activity被回收不再继续防止空指针异常

    2、使用Handler.removeMessages();
    知道原因就很好解决问题,Handler所导致的Activity内存泄漏正是因为Handler发送的Message任务没有完成,所以在onDestory中可以将handler中的message都移除掉,没有延时任务要处理,activity的生命周期就不会被延长,则可以正常销毁。

    单例所导致的内存泄漏

    在Android中单例模式中经常会需要Context对象进行初始化,如下简单的一段单例代码示例

    public class MyHelper {
    
      private static MyHelper myHelper;
    
      private Context context;
    
      private MyHelper(Context context){
        this.context = context;
      }
    
      public static synchronized MyHelper getInstance(Context context){
        if (myHelper == null){
          myHelper = new MyHelper(context);
        }
        return myHelper;
      }
    
      public void doSomeThing(){
    
      }
    
    }
    
    

    这样的写法看起来好像没啥问题,但是一旦如下调用就会产生内存溢出

      public void singleInstanceLeakcanary(){
        MyHelper.getInstance(this).doSomeThing();
      }
    
    

    首先单例中有一个static实例,实例持有Activity,但是static变量的生命周期是整个应用的生命周期,肯定是会比单个Activity的生命周期长的,所以,当Activity finish时,activity实例被static变量持有不能释放内存,导致内存泄漏。
    解决办法:
    1.使用getApplicationContext()

      private void singleInstanceResolve() {
        MyHelper.getInstance(getApplicationContext()).doSomeThing();
      }
    
    

    2.改写单例写法,在Application里面进行初始化。

    匿名内部类导致的异常

     /**
       * 匿名内部类泄漏包括Handler、Runnable、TimerTask、AsyncTask等
       */
      public void anonymousClassInstanceLeakcanary(){
        new Thread(new Runnable() {
          @Override
          public void run() {
            try {
              Thread.sleep(TIME);
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
          }
        }).start();
      }
    
    

    这个和Handler内部类导致的异常原理一样就不多说了。改为静态内部类+弱引用方式调用就行了。

    静态变量引用内部类

      private static Object inner;
      public void innearClassLeakcanary(){
    
        class InnearClass{
    
        }
        inner = new InnearClass();
      }
    
    

    因为静态对象引用了方法内部类,方法内部类也是持有Activity实例的,会导致Activity泄漏
    解决方法就是通过在onDestory方法中置空static变量

    网络请求回调接口

        Retrofit retrofit = new Retrofit.Builder()
            .addConverterFactory(GsonConverterFactory.create())
            .baseUrl("http://gank.io/api/data/")
            .build();
        Api mApi = retrofit.create(Api.class);
        Call<AndroidBean> androidBeanCall = mApi.getData(20,1);
        androidBeanCall.enqueue(new Callback<AndroidBean>() {
          @Override
          public void onResponse(Call<AndroidBean> call, Response<AndroidBean> response) {
            toast("requestLeakcanary");
          }
    
          @Override
          public void onFailure(Call<AndroidBean> call, Throwable t) {
    
          }
        });
    
    

    这是一段很普通的请求代码,一般情况下Wifi请求很快就回调回来了,并不会导致什么问题,但是如果是在弱网情况下就会导致接口回来缓慢,这时用户很可能就会退出Activity不在等待,但是这时网络请求还未结束,回调接口为内部类依然会持有Activity的对象,这时Activity就内存泄漏的,并且如果是在Fragment中这样使用不仅会内存泄漏还可能会导致奔溃,之前在公司的时候就是写了一个Fragment,里面包含了四个网络请求,由于平时操作的时候在Wi-Fi情况下测试很难发现在这个问题,后面灰度的时候出现Crash,一查才之后当所附属的Activity已经finish了,但是网络请求未完成,首先是Fragment内存泄漏,然后调用getResource的时候返回为null导致异常。这类异常的原理和非静态内部类相同,所以可以通过static内部类+弱引用进行处理。由于本例是通过Retrofit进行,还可以在onDestory进行call.cancel进行取消任务,也可以避免内存泄漏。

    RxJava异步任务

    RxJava最近很火,用的人也多,经常拿来做网络请求和一些异步任务,但是由于RxJava的consumer或者是Observer是作为一个内部类来请求的时候,内存泄漏问题可能又随之而来

      @SuppressLint("CheckResult")
      public void rxJavaLeakcanary(){
        AppModel.getData()
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(
            new Consumer<Object>() {
              @Override
              public void accept(Object o) throws Exception {
                toast("rxJavaLeakcanary");
              }
            });
      }
    
    

    这个代码很常见,但是consumer这个为内部类,如果异步任务没有完成Activity依然是存在泄漏的风险的。好在RxJava有取消订阅的方法可通过如下方法解决

      @Override
      protected void onDestroy() {
        super.onDestroy();
        if (disposable!=null && !disposable.isDisposed()){
          disposable.dispose();
        }
      }
    
    

    Toast显示

    看到这个可能有些人会惊讶,为啥Toast会导致内存泄漏,首先看一下

    Toast.makeText(this,"toast",Toast.LENGTH_SHORT);
    
    

    这个代码大家都很熟悉吧,但是如果直接这么做就可能会导致内存泄漏
    ,这里传进去了一个Context,而Toast其实是在界面上加了一个布局,Toast里面有一个LinearLayout,这个Context就是作为LinearLayout初始化的参数,它会一直持有Activity,大家都知道Toast显示是有时间限制的,其实也就是一个异步的任务,最后让其消失,但是如果在Toast还在显示Activity就销毁了,由于Toast显示没有结束不会结束生命周期,这个时候Activity就内存泄漏了。
    解决方法就是不要直接使用那个代码,自己封装一个ToastUtil,使用ApplicationContext来调用。或者通过getApplicationContext来调用,还有一种通过toast变量的cancel来取消这个显示

     private void toast(String msg){
        Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
      }
    
    

    总结

    看了那么多是不是感觉其实内存泄漏的原理很简单,变来变去其实只是形式变了,换汤不换药。但是在编码中不注意还是可能会出现这些问题。了解原理之后就去写代码吧 😄


    我花了一年时间整理出一份腾讯T4级别的Android架构师全套学习资料,特别适合有3-5年以上经验的小伙伴深入学习提升。

    主要包括腾讯,以及字节跳动,华为,小米,等一线互联网公司主流架构技术。如果你有需要,尽管拿走好了。至于能学会多少,真的只能看你自己

    全套体系化高级架构视频;七大主流技术模块

    部分展示;java内核视频+源码+笔记

    免费分享

    点击获取资料文档;

    《腾讯T4级别Android架构师技术脑图+全套视频》

    为什么免费分享?

    我不想有很多开发者朋友因为门槛而错过这套高级架构资料,错过提升成为架构师的可能。国内程序员千千万,大多数是温水煮青蛙的现状,靠着天天加班,拿着外人以为还不错的薪资待遇。

    请记住自身技术水平才是我们的核心竞争力,千万别把年轻和能加班当做本钱。

    相关文章

      网友评论

        本文标题:Android内存泄漏原因及解决办法

        本文链接:https://www.haomeiwen.com/subject/gwelfctx.html