Android 轻松解决内存泄漏

标签: jianshu | 发表时间:2019-05-02 00:00 | 作者:
出处:http://itindex.net/relian

基础知识

Java 的内存分配简述

image.png
  • 方法区(non-heap):编译时就分配好,在程序整个运行期间都存在。它主要存放静态数据和常量;
  • 栈区:当方法执行时,会在栈区内存中创建方法体内部的局部变量,方法结束后自动释放内存;
  • 堆区(heap):通常用来存放 new 出来的对象。由 GC 负责回收。

Java四种不同的引用类型

  • 强引用(Strong Reference):JVM 宁愿抛出 OOM,也不会让 GC 回收存在强引用的对象。
  • 软引用(Soft Reference) : 一个对象只具有软引用,在内存不足时,这个对象才会被 GC 回收。
  • 弱引用(weak Reference): 在 GC 时,如果一个对象只存在弱引用,那么它将会被回收
  • 虚引用(Phantom Reference):任何时候都可以被 GC 回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为 GC 回收 Object 的标志。

与 Android 中的差异:在 2.3 以后版本中,即使内存够用,Android 系统会优先将 SoftReference 的对象提前回收掉, 其他和 Java 中是一样的。

因此谷歌官方建议用 LruCache(least recentlly use 最少最近使用算法)。会将内存控制在一定的大小内, 超出最大值时会自动回收, 这个最大值开发者自己定。

什么是内存泄漏?

  • 对于 C++ 来说,内存泄漏就是 new 出来的对象没有 delete,俗称野指针;
  • 而对于 java 而言,就是存放在堆上的 Object 无法被 GC 正常回收。

内存泄漏根本原因

长生命周期的对象持有 短生命周期对象**强/软引用**,导致本应该被回收的短生命周期的对象却无法被正常回收。

例如在单例模式中,我们常常在获取单例对象时需要传一个 Context 。单例对象是一个长生命周期的对象(应用程序结束时才终结),而如果我们传递的是某一个 Activity 作为 context,那么这个 Activity 就会因为引用被持有而无法销毁,从而导致内存泄漏。

内存泄漏的危害

  • 运行性能的问题: Android在运行的时候,如果内存泄漏将导致其他组件可用的内存变少,一方面会使得GC的频率加剧,在发生GC的时候,所有进程都必须进行等待,GC的频率越多,从而用户越容易感知到卡顿。另一方面,内存变少,将可能使得系统会额外分配给你一些内存,而影响整个系统的运行状况。
  • 运行崩溃问题: 内存泄露是内存溢出(OOM)的重要原因之一,会导致 Crash。如果应用程序在消耗光了所有的可用堆空间,那么再试图在堆上分配新对象时就会引起 OOM(Out Of Memory Error) 异常,此时应用程序就会崩溃退出。

内存泄漏的典型案例

永远的单例(Singleton)

由于单例模式的静态特性,使得它的生命周期和我们的应用一样长,一不小心让单例无限制的持有 Activity 的强引用就会导致内存泄漏。

解决方案

  • 把传入的 Context 改为同应用生命周期一样长的 Application 中的 Context。
  • 通过重写 Application,提供 getContext 方法,那样就不需要在获取单例时传入 context。
   public class BaseApplication extends Application{
    private static ApplicationContext sContext;
    @Override
    public void onCreate(){
        super.onCreate();
        sContext = getApplicationContext();
    }
    public static Context getApplicationContext(){
        return sContext;
    }
}

Handler引发的内存泄漏

由于 Handler 属于 TLS(Thread Local Storage)变量,导致它的生命周期和 Activity 不一致。因此通过 Handler 来更新 UI 一般很难保证跟 View 或者 Activity 的生命周期一致,故很容易导致无法正确释放。

例如:

   public class HandlerBadActivity extends AppCompatActivity {
    private final Handler handler = new Handler(){//非静态内部类,持有外部类的强引用
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler_bad);
        // 延迟 5min 发送一个消息
        handler.postDelayed(new Runnable() {
        //内部会将该 Runable 封装为一个 Message 对象,同时将 Message.target 赋值为 handler
            @Override
            public void run() {
                //do something
            }
        }, 1000 * 60 * 5);
        this.finish();
    }
}

上面的代码中发送了了一个延时 5 分钟执行的 Message,当该 Activity 退出的时候,延时任务(Message)还在主线程的 MessageQueue 中等待,此时的 Message 持有 Handler 的强引用(创建时通过 Message.target 进行指定),并且由于 Handler 是 HandlerBadActivity 的非静态内部类,所以 Handler 会持有一个指向 HandlerBadActivity 的强引用,所以虽然此时 HandlerBadActivity 调用了 finish 也无法进行内存回收,造成内存泄漏。

解决方法

将 Handler 声明为静态内部类,但是要注意 **如果用到 Context 等外部类的 非static 对象,还是应该使用 ApplicationContext 或者通过弱引用来持有这些外部对象**

   public class HandlerGoodActivity extends AppCompatActivity {
    private static final class MyHandler extends Handler{//声明为静态内部类(避免持有外部类的强引用)
        private final WeakReference<HandlerGoodActivity> mActivity;
        public MyHandler(HandlerGoodActivity activity){
            this.mActivity = new WeakReference<HandlerGoodActivity>(activity);//使用弱引用
        }
        @Override
        public void handleMessage(Message msg) {
            HandlerGoodActivity activity = mActivity.get();
            if (activity == null || activity.isFinishing() || activity.isDestroyed()) {//判断 activity 是否为空,以及是否正在被销毁、或者已经被销毁
              removeCallbacksAndMessages(null);
              return;
            }
            // do something
        }
    }
    private final MyHandler myHandler = new MyHandler(this);
}

慎用 static 成员变量

static 修饰的变量位于内存的方法区,其生命周期与 App 的生命周期一致。 这必然会导致一系列问题,如果你的 app 进程设计上是长驻内存的,那即使 app 切到后台,这部分内存也不会被释放。

解决方法

不要在类初始化时初始化静态成员,也就是可以考虑懒加载。架构设计上要思考是否真的有必要这样做,尽量避免。如果架构需要这么设计,那么此对象的生命周期你有责任管理起来。

当然,Application 的 context 不是万能的,所以也不能随便乱用,对于有些地方则必须使用 Activity 的 Context,对于Application,Service,Activity三者的Context的应用场景如下:

功能 Application Service Activity
Start an Activity NO1 NO1 YES
Show a Dialog NO NO YES
Layout Inflation YES YES YES
Start an Service YES YES YES
Bind an Service YES YES YES
Send a Broadcast YES YES YES
Register BroadcastReceiver YES YES YES
Load Resource Values YES YES YES
  • NO1 表示 Application 和 Service 可以启动一个 Activity,不过 需要创建一个新的 task 任务队列
  • 对于 Dialog 而言,只有在 Activity 中才能创建。

使用系统服务引发的内存泄漏

为了方便我们使用一些常见的系统服务,Activity 做了一些封装。比如说,可以通过 getPackageManager在 Activtiy 中获取 PackageManagerService,但是,里面实际上调用了 Activity 对应的 ContextImpl 中的 getPackageManager 方法

ContextWrapper#getPackageManager

   @Override
public PackageManager getPackageManager() {
    return mBase.getPackageManager();
}

ContextImpl#getPackageManager

   @Override
public PackageManager getPackageManager() {
    if (mPackageManager != null) {
        return mPackageManager;
    }
    IPackageManager pm = ActivityThread.getPackageManager();
    if (pm != null) {
        // Doesn't matter if we make more than one instance.
        return (mPackageManager = new ApplicationPackageManager(this, pm));//创建 ApplicationPackageManager
    }
    return null;
}

ApplicationPackageManager#ApplicationPackageManager

   ApplicationPackageManager(ContextImpl context,
                          IPackageManager pm) {
    mContext = context;//保存 ContextImpl 的强引用
    mPM = pm;
}

private UserManagerService(Context context, PackageManagerService pm,
        Object packagesLock, File dataDir) {
    mContext = context;//持有外部 Context 引用
    mPm = pm;
    //代码省略
}

PackageManagerService#PackageManagerService

   public class PackageManagerService extends IPackageManager.Stub {
    static UserManagerService sUserManager;//持有 UMS 静态引用
    public PackageManagerService(Context context, Installer installer,
        boolean factoryTest, boolean onlyCore) {
          sUserManager = new UserManagerService(context, this, mPackages);//初始化 UMS
        }
}

遇到的内存泄漏问题是因为在 Activity 中调用了 getPackageManger 方法获取 PMS ,该方法调用的是 ContextImpl,此时如果ContextImpl 中 PackageManager 为 null,就会创建一个 PackageManger(ContextImpl 会将自己传递进去,而 ContextImpl 的 mOuterContext 为 Activity),创建 PackageManager 实际上会创建 PackageManagerService(简称 PMS),而 PMS 的构造方法中会创建一个 UserManger(UserManger 初始化之后会持有 ContextImpl 的强引用)。

只要 PMS 的 class 未被销毁,那么就会一直引用着 UserManger ,进而导致其关联到的资源无法正常释放。

解决办法

将getPackageManager()改为 getApplication()#getPackageManager()。这样引用的就是 Application Context,而非 Activity 了。

远离非静态内部类和匿名类

因为使用非静态内部类和匿名类都会默认持有外部类的引用,如果生命周期不一致,就会导致内存泄漏。

   public class NestedClassLeakActivity extends AppCompatActivity {

    class InnerClass {//非静态内部类

    }

    private static InnerClass sInner;//指向非静态内部类的静态引用

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_nested_class);
        if (sInner == null) {
           sInner = new InnerClass();//创建非静态内部类的实例
        }
    }
}

非静态内部类默认会持有外部类的引用,而外部类中又有一个该非静态内部类的静态实例,该静态实例的生命周期和应用的一样长,而静态实例又持有 Activity 的引用,因此导致 Activity 的内存资源不能正常回收。

解决方法

将该内部类设为静态内部类 也可以将该内部类抽取出来封装成一个单例

集合引发的内存泄漏

我们通常会把一些对象的引用加入到集合容器(比如ArrayList)中,当我们不再需要该对象时(通常会调用 remove 方法),并没有把它的引用从集合中清理掉(其中的一种情况就是 remove 方法没有将不再需要的引用赋值为 null),下面以 ArrayList 的 remove 方法为例

   public E remove( int index) {
    // 数组越界检查
    RangeCheck(index);
    modCount++;
    // 取出要删除位置的元素,供返回使用
    E oldValue = (E) elementData[index];
   // 计算数组要复制的数量
    int numMoved = size - index - 1;
   // 数组复制,就是将index之后的元素往前移动一个位置
    if (numMoved > 0)
       System. arraycopy(elementData, index+1, elementData, index,
                      numMoved);
   // 将数组最后一个元素置空(因为删除了一个元素,然后index后面的元素都向前移动了,所以最后一个就没用了),好让gc尽快回收
    elementData[--size ] = null; // Let gc do its work
    return oldValue;
}

WebView 引发的内存泄漏

WebView 解析网页时会申请 Native堆内存用于保存页面元素,当页面较复杂时会有很大的内存占用。如果页面包含图片,内存占用会更严重。并且打开新页面时,为了能快速回退, 之前页面占用的内存也不会释放。有时浏览十几个网页,都会占用几百兆的内存。这样加载网页较多时,会导致系统不堪重负,最终强制关闭应用,也就是出现应用闪退或重启。

由于占用的都是 Native 堆内存,所以 实际占用的内存大小不会显示在常用的 DDMS Heap 工具中( DMS Heap 工具看到的只是Java虚拟机分配的内存,即使Native堆内存已经占用了几百兆,这里显示的还只是几兆或十几兆)。只有使用 adb shell 中的一些命令比如 dumpsys meminfo 包名,或者在程序中使用 Debug.getNativeHeapSize()才能看到 Native 堆内存信息。

据说由于 WebView 的一个 BUG,即使它所在的 Activity(或者Service) 结束也就是 onDestroy() 之后,或者直接调用 WebView.destroy()之后,它所占用这些内存也不会被释放。

解决方法

把使用了 WebView 的 Activity (或者 Service) 放在单独的进程里。

  • 系统在检测到应用占用内存过大有可能被系统干掉
  • 也可以在它所在的 Activity(或者 Service) 结束后,调用 System.exit(0),主动Kill掉进程。由于系统的内存分配是以进程为准的,进程关闭后,系统会自动回收所有内存。

使用 WebView 的页面(Activity),在生命周期结束页面退出(onDestory)的时候,主动调用WebView.onPause()==以及==WebView.destory()以便让系统释放 WebView 相关资源。

其他常见的引起内存泄漏原因

  • Android 3.0 以下,Bitmap 在不使用的时候没有使用 recycle() 释放内存。

  • 非静态内部类的静态实例容易造成内存泄漏:即一个类中如果你不能够控制它其中内部类的生命周期(譬如Activity中的一些特殊Handler等),则尽量使用静态类和弱引用来处理(譬如ViewRoot的实现)。

  • 警惕线程未终止造成的内存泄露;譬如在 Activity 中关联了一个生命周期超过 Activity 的 Thread,在退出 Activity 时切记结束线程。

    一个典型的例子就是 HandlerThread 的 run 方法。该方法在这里是一个死循环,它不会自己结束,线程的生命周期超过了 Activity 生命周期,我们必须手动在 Activity 的销毁方法中中调用 thread.getLooper().quit() 才不会泄露。

  • 对象的注册与反注册没有成对出现造成的内存泄露;譬如注册广播接收器、注册观察者(典型的譬如数据库的监听)等。

  • 创建与关闭没有成对出现造成的泄露;譬如Cursor资源必须手动关闭,WebView必须手动销毁,流等对象必须手动关闭等。

  • 避免代码设计模式的错误造成内存泄露;譬如循环引用,A 持有 B,B 持有 C,C 持有 A,这样的设计谁都得不到释放。

【附】相关架构及资料

加群 Android IOC架构设计领取获取往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术,群内还有技术大牛一起讨论交流解决问题。

Android高级技术大纲

领取方式:

点赞+加群免费获取 Android IOC架构设计

相关 [android 内存泄漏] 推荐:

Android 解析内存泄漏

- - CSDN博客移动开发推荐文章
1、引用没释放造成的内存泄露.        1.1、注册没取消造成的内存泄露.        这种 Android的内存泄露比纯 Java的内存泄露还要严重,因为其他一些Android程序可能引用我们的Anroid程序的对象(比如注册机制). 即使我们的Android程序已经结束了,但是别的引用程序仍然还有对我们的Android程序的某个对象的引用,泄露的内存依然不能被垃圾回收.

Android内存泄漏检测-LeakCanary

- - CSDN博客推荐文章
添加LeakCanary依赖包. 在主模块app下的build.gradle下添加如下依赖:. 添加Application子类. 首先创建一个ExampleApplication,该类继承于Application,在该类的onCreate方法中添加如下代码开启LeakCanary监控:. 在配置文件中注册ExampleApplication.

Android性能优化之内存泄漏

- - CSDN博客推荐文章
  内存泄漏(memory leak)是指由于疏忽或错误造成程序未能释放已经不再使用的内存. 那么在Android中,当一个对象持有Activity的引用,如果该对象不能被系统回收,那么当这个Activity不再使用时,这个Activity也不会被系统回收,那这么以来便出现了内存泄漏的情况. 在应用中内出现一次两次的内存泄漏获取不会出现什么影响,但是在应用长时间使用以后,若是存在大量的Activity无法被GC回收的话,最终会导致OOM的出现.

Android 轻松解决内存泄漏

- - IT瘾-jianshu
方法区(non-heap):编译时就分配好,在程序整个运行期间都存在. 栈区:当方法执行时,会在栈区内存中创建方法体内部的局部变量,方法结束后自动释放内存;. 堆区(heap):通常用来存放 new 出来的对象. Java四种不同的引用类型. 强引用(Strong Reference):JVM 宁愿抛出 OOM,也不会让 GC 回收存在强引用的对象.

Android性能优化之常见的内存泄漏

- - CSDN博客推荐文章
最近腾讯bugly也推出了三篇关于Android内存泄漏调优的文章,有兴趣的可以看看:. 1、 内存泄露从入门到精通三部曲之基础知识篇. 2、 内存泄露从入门到精通三部曲之排查方法篇. 3、 内存泄露从入门到精通三部曲之常见原因与用户实践. 当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏.

Android内存泄漏产生的6大原因

- - IT瘾-geek
1.资源对象没关闭造成的内存泄漏. 资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存. 它们的缓冲不仅存在于 java虚拟机内,还存在于java虚拟机外. 如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏.

Android内存泄漏思考 - 编程学习网

- -
Android内存泄漏是一个经常要遇到的问题,程序在内存泄漏的时候很容易导致OOM的发生. 那么如何查找内存泄漏和避免内存泄漏就是需要知晓的一个问题,首先我们需要知道一些基础知识. 强引用: 强引用是Java中最普通的引用,随意创建一个对象然后在其他的地方引用一下,就是强引用,强引用的对象Java宁愿OOM也不会回收他.

Android 性能篇 -- 带你领略Android内存泄漏的前世今生

- - SegmentFault 最新的文章
内存泄漏是当程序不再使用到的内存时,释放内存失败而产生了无用的内存消耗. 内存泄漏并不是指物理上的内存消失,这里的内存泄漏是指由程序分配的内存但是由于程序逻辑错误而导致程序失去了对该内存的控制,使得内存浪费. Java 程序运行时的内存分配策略有三种,分别是. 静态分配 、 栈式分配 和. 堆式分配 ,对应的三种存储策略使用的内存空间主要分别是.

内存泄漏

- - CSDN博客系统运维推荐文章
程序申请了堆空间,但是“忘记”释放,导致该块区域在程序结束前无法被再次使用导致的. 泄漏时间长了,就会导致用户空间内存不足,严重的导致死机. 如果泄漏比较严重,很容易察觉;但是有些泄漏很缓慢,不容易察觉,但是软件会运行很长时间后,会慢慢导致严重问题,而且当发现症状的时候,基本上已经是比较晚的时候了,想要识别泄漏,还是可以实现的,本篇文章来聊聊内存操作的原理.

java内存泄漏

- - 编程语言 - ITeye博客
不论哪种语言的内存分配方式,都需要返回所分配内存的真实地址,也就是返回一个指针到内存块的首地址. Java中对象是采用new或者反射的方法创建的,这些对象的创建都是在堆(Heap)中分配的,所有对象的回收都是由Java虚拟机通过垃圾回收机制完成的. GC为了能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋值等状况进行监控,Java会使用有向图的方法进行管理内存,实时监控对象是否可以达到,如果不可到达,则就将其回收,这样也可以消除引用循环的问题.