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

2023-10-31

前言

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

原因

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

Java的GC可分为

引用计数算法

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

image

可达性分析算法

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

image

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

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

image

内存泄漏的例子

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();
  }

总结

看了那么多是不是感觉其实内存泄漏的原理很简单,变来变去其实只是形式变了,换汤不换药。但是在编码中不注意还是可能会出现这些问题。还有很多示例我没有举出来,但是不表示就没有其他情况了,这需要大家编程的时候自己耐心去发觉,有一些View或者其他对象默认持有Activity对象的时候,如果不小心被其他生命周期更长的对象引用了,依然会导致内存泄漏。并且只要有一个强引用应用了Activity即使你对其他很多地方做了防泄漏处理结果还是一样的—Activity泄漏(这就很扎心了),所以这就是一个细节与编程规范的问题,但是可有利用一些工具来找出我们粗心写下的代码,比如leakcanary内存泄漏分析工具。找出来利用自己对原理的理解解决这些问题是很简单的。

了解原理之后就去写代码吧 ?

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android内存泄漏原因及解决办法 的相关文章

  • STM32F103 GPIO输出模式2MHz,10MHz,50MHz波形对比

    在STM32F103中GPIO的输出模式有三种速度配置 如图所示 经过测试发现 这三种速度的输出模式与IO的翻转频率没有关系 它们影响的可能是驱动能力 下面是在IO极限频率 18MHz 下 三种模式的波形
  • bootstrap实现轮播图

    div class carousel slide style width 400px margin 30px auto ol class carousel indicators li class active li li li ol div

随机推荐

  • 数据库类型区分

    数据库类型主要可分为 网状数据库 Network Database 关系数据库 Relational Database 树状数据库 Hierarchical Database 面向对象数据库 Object oriented Database
  • qemu 对 ARMv8的支持

    qemu 可以支持 ARMv8 且支持的 boot 包括 1 从 ATF启动 2 从 UEFI启动 3 从 u boot启动 4 从 linux 启动 这里打算用以下平台 qemu system aarch64 M virt cpu qem
  • buckboost变换器简介

    1 buckboost拓扑 Vo Vin D 1 D 极性相反 可以升压也可以降压
  • latex 大于小于大于等于小于等于

    转载于https www cnblogs com kjkj p 10505832 html 发现大部分人只回答大于等于号 小于等于号的写法 而没有说大于 小于号的分别写法 大于号 textgreater 小于号 textless 下面的后面
  • android 动画开发

    动画的分类 Android通过Animations为android UI提供了一系列的动画效果 可以进行旋转 缩放 淡入淡出等 这些效果可以应用在绝大多数的空间中 Android中的动画一般为两类 1 Frame by frame Anim
  • 笔试题(十二):走梅花桩

    Redraiment是走梅花桩的高手 Redraiment可以选择任意一个起点 从前到后 但只能从低处往高处的桩子走 他希望走的步数最多 你能替Redraiment研究他最多走的步数吗 数据范围 每组数据长度满足 1 n 200 数据大小满
  • 利用Charles打断点修改返回数据

    1 选择你需要修改数据的接口 2 选中后 proxy gt breakPoint Setting 在出来的弹窗中找到你标记的接口 双击 选择对应的方法 get post 然后将Query里面的删掉 写上 确保一下request和respon
  • python学习之基于Python的人脸识别技术学习

    摘要 面部识别技术的应用越来越广泛 它广泛应用于安全系统 人机交互 社交媒体 医疗保健等领域 本文介绍了基于Python的人脸识别技术 包括人脸检测 人脸特征提取和人脸识别三个部分 我们使用OpenCV和Dlib库来实现这些功能 并使用Py
  • Python实现最小顶点覆盖算法

    Python实现最小顶点覆盖算法 最小顶点覆盖问题是图论中的重要问题 其目标是找到至少数量的顶点 使得每一条边至少有一个端点被这些顶点所覆盖 该问题在实际中有诸多应用 例如网络流分析和计算机视觉等领域 本文将介绍如何使用Python实现最小
  • 10.Docker(一)-----安装、初步使用、镜像、Dockerfile常用指令、通过Dockerfile文件封装nginx镜像并优化

    安装 初步使用 镜像 Dockerfile常用指令 通过Dockerfile文件封装nginx镜像并优化 一 简介 二 安装 1 安装配置 仓库配有的安装包 安装 桥接 2 导入马里奥游戏 浏览器访问172 25 15 1 3 删除 4 导
  • 删除链表中重复的元素

    题目描述 给定一个排序链表 删除所有含有重复数字的节点 只保留原始链表中 没有重复出现 的数字 样例 输入 1 gt 2 gt 3 gt 3 gt 4 gt 4 gt 5 输出 1 gt 2 gt 5 分析 从头结点开始遍历该链表 如果当前
  • 【华为OD统一考试A卷

    华为OD统一考试A卷 B卷 新题库说明 2023年5月份 华为官方已经将的 2022 0223Q 1 2 3 4 统一修改为OD统一考试 A卷 和OD统一考试 B卷 你收到的链接上面会标注A卷还是B卷 请注意 根据反馈 目前大部分收到的都是
  • 逆向工具(IDA、pyinstxtractor+uncompyle6、jadx等持续更新)

    IDA Pro IDA Pro Interactive Disassembler Professional 交互式反汇编器专业版 CTF RE PWN必备 打开一个可执行文件前 应先用file命令或者DIE等工具 确定是32位还是64位 然
  • 使用armDebian基本的操作方式

    1 查看进程id pgrep ttnode 2 杀死进程 kill 1308 3 查看硬盘的分区 sudo fdisk l 3 卸载文件卷 umount dev sda1 4 格式化 mkfs vfat dev sda1 5 重启 rebo
  • C++:类和对象(中)---默认成员函数---运算符重载---const的含义

    文章目录 默认成员函数 构造函数 析构函数 拷贝构造函数 运算符重载 赋值运算符重载 const的含义 取地址及const取地址操作符重载 默认成员函数 首先要理解什么是默认成员函数 类在什么都不写的时 编译器会生成六个默认成员函数 用户没
  • 阿里微服务架构Spring Cloud Alibaba Nacos实战

    Spring Cloud Alibaba Nacos 1 常用特性 2 注册中心 配置中心对比 3 生态图 4 安装 5 名词解析 6 代码实战 注册中心 配置中心 nacos官方文档 https nacos io zh cn docs q
  • multipart/form-data格式接口调用工具类,实现文件上传

    1 添加依赖
  • Access violation at address 00000000. Read of address 00000000.的解决办法

    Access violation at address 00000000 Read of address 00000000 原理 解决办法 在使用spacesniffer查看C盘空间的时候报错 原理 这个问题是关于Access Violat
  • ZBrush中自动保存在哪里

    在使用 ZBrush 执行任何会话期间 您都可以设置将文件自动保存 并可以修改保存时间间隔 文件保存位置等设置 发生系统错误后要重新启动ZBrush时 可以从临时文件夹或指定的文件夹中恢复备份文件 如果您选择不恢复备份文件 退出应用程序后文
  • Android内存泄漏原因及解决办法

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