RecyclerView导致内存泄漏问题分析

注意:千万不要在ListView中添加包含RecyclerView的HeaderView,否则会泄漏整个Activity,当然ListView的adpter中最好也不要使用RecyclerView,原因文末给出。如果要用最好是在RecyclerView中嵌套RecyclerView

笔者写了一个demo来演示此BUG,目前demo还有些欠妥,笔者会继续跟进,也欢迎各路大神共同关注。
直达链接

问题是这样的,笔者开发的一款APP某一个版本后后台的OOM Crash一下变得多起来。由于应用使用了LargerHeap以及泄漏与一定的业务逻辑相关,还没有发展到大规模爆发的地步。但是看着后台不断增多的OOM,迫于压力修改优化了一版代码。

  1. 修改Handler为静态并使用WeakReference引用对应的Activity。
  2. 检查是否有静态变量引用背景,参考google官方文档(需要翻墙),但是即便是这个原因泄漏毕竟是有限的,因为静态变量的引用是可以被修改的。
  3. 检查所有的Cursor是否正确关闭
  4. 检查是否有重写finalize方法,没有使用,依赖的第三方库有,比如作者使用的fresco,而且作者的OOM大部分还是发生在fresco代码内部的decode image。然后去Fresco的issue中查找,当然也找到很多OOM相关的问题,不过大部分是是否使用ListView或者RecyclerView代码ScrollView。然后检查代码,是否有相关问题。

根据上面4点(不仅限于此4点)优化代码后,发布一个版本,问题依旧。
其中Crash的log也一直误导方向,下面是log

java.lang.OutOfMemoryError: Failed to allocate a 1000012 byte allocation with 288864 free bytes and 282KB until OOM
    at dalvik.system.VMRuntime.newNonMovableArray(Native Method)
    at android.graphics.Bitmap.nativeCreate(Native Method)
    at android.graphics.Bitmap.createBitmap(Bitmap.java:812)
    at android.graphics.Bitmap.createBitmap(Bitmap.java:789)
    at android.graphics.Bitmap.createBitmap(Bitmap.java:756)
    at com.facebook.imagepipeline.memory.BitmapPool.alloc(BitmapPool.java:55)
    at com.facebook.imagepipeline.memory.BitmapPool.alloc(BitmapPool.java:30)
    at com.facebook.imagepipeline.memory.BasePool.get(BasePool.java:259)
    at com.facebook.imagepipeline.platform.ArtDecoder.decodeStaticImageFromStream(ArtDecoder.java:137)
    at com.facebook.imagepipeline.platform.ArtDecoder.decodeFromEncodedImage(ArtDecoder.java:81)
    at com.facebook.imagepipeline.decoder.DefaultImageDecoder.decodeStaticImage(DefaultImageDecoder.java:158)
    at com.facebook.imagepipeline.decoder.DefaultImageDecoder$1.decode(DefaultImageDecoder.java:71)
    at com.facebook.imagepipeline.decoder.DefaultImageDecoder.decode(DefaultImageDecoder.java:123)
    at com.facebook.imagepipeline.producers.DecodeProducer$ProgressiveDecoder.doDecode(DecodeProducer.java:239)
    at com.facebook.imagepipeline.producers.DecodeProducer$ProgressiveDecoder.access$200(DecodeProducer.java:111)
    at com.facebook.imagepipeline.producers.DecodeProducer$ProgressiveDecoder$1.run(DecodeProducer.java:144)
    at com.facebook.imagepipeline.producers.JobScheduler.doJob(JobScheduler.java:207)
    at com.facebook.imagepipeline.producers.JobScheduler.access$000(JobScheduler.java:27)
    at com.facebook.imagepipeline.producers.JobScheduler$1.run(JobScheduler.java:78)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
    at com.facebook.imagepipeline.core.PriorityThreadFactory$1.run(PriorityThreadFactory.java:43)
    at java.lang.Thread.run(Thread.java:818)

从log看怀疑

  • Bitmap没有recycle,但是笔者的app的minSdkVersion是16,都是java内存,所以跟recycle也没有关系。
  • 再就是看log,发生OOM的时候是解析一张1M的图片,就猜测是否Fresco的缓存导致的,仍然没有头绪。

下面将带你一步一步发现内存泄漏元凶

step1: 使用AndroidStudio的monitors可以查看当前进程的内存状况,于是打开app,各种界面打开关闭、打开关闭,查看内存变化,果然发现一段时候后内存再也不下降了。

内存图

step2: Dump heap,如图所示,记得把AndroidStudio的内存调大,否则dump很容易失败
dump按钮

dump出来的数据后会看到如下界面
dump后数据

泄漏分析

点击Analyzer Tasks上的绿色小箭头,就会列出当前泄漏的activity,虽然我们知道哪个Activity泄漏了,但是怎么找真正泄漏的代码呢,当然查看代码是可以的,不过如果遇到笔者这样的问题再牛逼的人估计也很难通过看代码找到原因。

step3 使用MemoryAnalyzer,俗称MAT下载地址,MAT是不能直接打开AndroidStudio的hprof文件的,需要使用sdk\platform-tools\hprof-conv工具转换一下,笔者windows的语法:

hprof-conv.exe xxx_2017.03.03_15.43.hprof convert_xxx.hprof

xxx_2017.03.03_15.43.hprof文件在项目captures目录下

使用MAT打开convert_xxx.hprof文件,打开后如下图

图片名称

正常情况MAT已经列出了可能导致泄漏的对象,但是此次讨论的泄漏因为已经通过Analyzer Tasks找到了具体的泄漏源。在Overview中打开Histogram,搜索之前Analyzer Tasks中泄漏的LeakMemoryActivity,可以看出已经有3个Activity被泄漏了。右击列出所有对象,选择其中一个右击选择Path GC Roots->exculde all phantom/weak/soft etc references
这里写图片描述

到此就可以看到最终是有谁泄漏的
这里写图片描述

表示第一次看到这个泄漏源的时候是这样子的
这里写图片描述
然后没办法硬着头皮往下看

  1. 可以看到LeakMemoryActivity是被MyRecyclerView引用(这里MyRecyclerView继承自RecyclerView,所以必然会引用activity)
  2. MyRecyclerView被GapWorker的mRecyclerViews引用(mRecyclerViews是一个List容器)
  3. 到这里看不明白了,不过可以肯定是跟ThreadLocal有关系

其实还是有突破口的,目前还能理清楚的是可以查看GapWorker的源码,至于怎么查看GapWorker的源码通过AndroidStudio就可以,还不需要下载全部的源码,到这也不需要借助sourceinsight或者vim之类的。

打开GapWorker源码发现有如下代码

    static final ThreadLocal<GapWorker> sGapWorker = new ThreadLocal<>();

    ArrayList<RecyclerView> mRecyclerViews = new ArrayList<>();

果不其然跟MAT列出来的很像,而且这里有一个静态的ThreadLocal对象,并且我们知道静态属性生命周期约等于虚拟机的生命周期。而且这个ThreadLocal的泛型是GapWorker,必然会有地方通过sGapWorker的set方法放一下GapWorker对象到sGapWorker 里面,而且GapWorker又有一个mRecyclerViews的List,也就是发生错误的时候mRecyclerViews没有把里面的RecyclerView移除。
到这里自然想到什么时候会添加和移除,于是找到GapWorker里的另外2个方法,但是有个弊端就是源码无法通过IDE的方法调用找到调用的地方。

    public void add(RecyclerView recyclerView) {
        if (RecyclerView.DEBUG && mRecyclerViews.contains(recyclerView)) {
            throw new IllegalStateException("RecyclerView already present in worker list!");
        }
        mRecyclerViews.add(recyclerView);
    }

    public void remove(RecyclerView recyclerView) {
        boolean removeSuccess = mRecyclerViews.remove(recyclerView);
        if (RecyclerView.DEBUG && !removeSuccess) {
            throw new IllegalStateException("RecyclerView removal failed!");
        }
    }

虽然无法通过IDE查到调用,但是可以猜到肯定是跟RecyclerView有关系,直接打开RecyclerView源码,笔者直接通过ctr+f搜索关键词GapWorker,发现在onAttachedToWindow方法中有相关逻辑

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        mLayoutOrScrollCounter = 0;
        mIsAttached = true;
        mFirstLayoutComplete = mFirstLayoutComplete && !isLayoutRequested();
        if (mLayout != null) {
            mLayout.dispatchAttachedToWindow(this);
        }
        mPostedAnimatorRunner = false;

        if (ALLOW_THREAD_GAP_WORK) {
            // Register with gap worker
            mGapWorker = GapWorker.sGapWorker.get();
            if (mGapWorker == null) {
                mGapWorker = new GapWorker();

                // break 60 fps assumption if data from display appears valid
                // NOTE: we only do this query once, statically, because it's very expensive (> 1ms)
                Display display = ViewCompat.getDisplay(this);
                float refreshRate = 60.0f;
                if (!isInEditMode() && display != null) {
                    float displayRefreshRate = display.getRefreshRate();
                    if (displayRefreshRate >= 30.0f) {
                        refreshRate = displayRefreshRate;
                    }
                }
                mGapWorker.mFrameIntervalNs = (long) (1000000000 / refreshRate);
                GapWorker.sGapWorker.set(mGapWorker);
            }
            mGapWorker.add(this);//笔者注,哈哈就是在这里添加的
        }
    }

发现在onAttachedToWindow的会有创建GapWorker的逻辑,并且在onAttachedToWindow方法中把当前RecyclerView的引用通过mGapWorker.add(this)添加到了容器中。既然是在onAttachedToWindow中添加进去的,必然就会有remove。那就看与onAttachedToWindow相对应的onDetachedFromWindow方法:

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (mItemAnimator != null) {
            mItemAnimator.endAnimations();
        }
        stopScroll();
        mIsAttached = false;
        if (mLayout != null) {
            mLayout.dispatchDetachedFromWindow(this, mRecycler);
        }
        mPendingAccessibilityImportanceChange.clear();
        removeCallbacks(mItemAnimatorRunner);
        mViewInfoStore.onDetach();

        if (ALLOW_THREAD_GAP_WORK) {
            // Unregister with gap worker
            mGapWorker.remove(this);
            mGapWorker = null;
        }
    }

果然看到有相应的remove操作,这里有一个ALLOW_THREAD_GAP_WORK判断,查看这个值是什么,看如下代码,发现只有5.0以上的手机才会启用GapWorker,到这里也验证了笔者之前疑惑的的Crash日志只有5.0以上的手机。

/**
 * On L+, with RenderThread, the UI thread has idle time after it has passed a frame off to
 * RenderThread but before the next frame begins. We schedule prefetch work in this window.
 */
private static final boolean ALLOW_THREAD_GAP_WORK = Build.VERSION.SDK_INT >= 21;

到这里基本可以想到应该是RecyclerView的onDetachedFromWindow方法没有被调用导致的,于是笔者就在自定义的RecyclerView中添加Log,发现果不其然,RecyclerView的onDetachedFromWindow没有被调用。由于笔者当时是在ListView中添加了一个包含横向RecyclerView的header。

于是笔者开始搜索有没有相关问题,于是收到下面的文章(需要翻墙),强烈建议打开查看。
因为截至到2015年也有人反馈在5.0以上的手机仍然有存在ListView的Adpter中Item还存在此问题,虽然笔者拿自己的5.1手机测试此种情况不会导致onDetachedFromWindow不被调用。

至此,由于Google的RecyclerView导致的内存泄漏问题分析结束了。

结论:作为Android开发者,一定要注意RecyclerView的使用,千万不要跟ListView一起使用,慎用!自定义View的时候设计到RecyclerView也要注意是否调用RecyclerView的onDetachedFromWindow方法

DEMO直达链接


目前笔者测试的此问题再华为P9 7.0已经修复,不会导致泄漏,不知Google是否在7.0已经修复此问题,希望有知道的同学留言告知相关文章

  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值