Android常见面试题——内存泄漏缘由及解决办法

前言

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

缘由

内存泄漏的原理不少人都明白,可是为了增强你们的防止内存泄漏的意识,我再来讲一遍。说到内存泄漏的原理就必需要讲一下Java的GC的。Java之因此这么流行不只仅是他面向对象编程的方式,还有一个重要的缘由是由于,它能帮程序员免去释放内存的工做,但Java并无咱们想象的那么智能,它进行内存清理还得依靠固定的判断逻辑。java

Java的GC可分为

引用计数算法android

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

可达性分析算法面试

针对引用计数算法的致命问题,可达性分析算法可以轻松的解决这个问题。可达性算法是经过从GC root往外遍历,若是从root节点没法遍历该节点代表该节点对应的对象处于可回收状态,以下图中obj一、obj二、obj三、obj5都是能够从root节点出发所能到达的节点。反观obj四、obj六、obj7却没法从root到达,即便obj六、obj7互相循环引用可是仍是属于可回收的对象最后被jvm清理。算法

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

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

                                 

内存泄漏的例子

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的一个活动页面其所占有的内存是不容小视的。那么怎么才能合理的解决这个问题呢

一、使用弱引用

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被回收再也不继续防止空指针异常

二、使用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架构师技术脑图+全套视频》

为何免费分享?

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

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

相关文章
相关标签/搜索