Android 5.0~6.0系统,由于硬件加速引起的内存泄漏问题分析

1,322 阅读5分钟

Android 5.0~6.0系统,由于硬件加速引起的内存泄漏问题

实例

图001

从 hprof 文件可以看到:这些 bitmap 除了一个 JNI Global 的引用之外,已经没有其他的引用了,而正是由于这个 GC root 引用,导致这些 bitmap 无法被及时回收。

原因

DisplayListCanvas.cpp

......
void DisplayListCanvas::drawBitmap(const SkBitmap* bitmap, const SkPaint* paint) {
    bitmap = refBitmap(*bitmap);
    paint = refPaint(paint);
    addDrawOp(new (alloc()) DrawBitmapOp(bitmap, paint));
}

硬件加速原理此处不深入讨论,主要是将绘制操作分别保存到 DisplayListData 中,这样如果某个 ChildView 更新了,那么只需更新该 ChildView 对应的DisplayListData 就行,不需要更新整个 ViewTree。例如上面这段__绘制 bitmap __的代码,DisplayListCanvas 会将 bitmap 保存到 DisplayListData 中,换句话说就是 DisplayListData 存在对 bitmap 的引用。那么这个DisplayListData 什么时候会释放呢?

View.java

    ......
    private void cleanupDraw() {
        resetDisplayList();
        if (mAttachInfo != null) {
            mAttachInfo.mViewRootImpl.cancelInvalidate(this);
        }
    }
    
    ......
    @CallSuper
    protected void destroyHardwareResources() {
        resetDisplayList();
    }
    
    ......
    private void resetDisplayList() {
        if (mRenderNode.isValid()) {
            mRenderNode.destroyDisplayListData();
        }
        if (mBackgroundRenderNode != null && mBackgroundRenderNode.isValid()) {
            mBackgroundRenderNode.destroyDisplayListData();
        }
    }

从 View 的源代码可以看出,View 在 detach 或者不可见(GONE或INVISIBLE)的时候,都会调用 resetDisplayList()。这样来看,似乎逻辑上并没有什么问题,但是我们不妨接着往下看:

RenderNode.java

    ......
    public void destroyDisplayListData() {
        if (!mValid) return;
        nSetDisplayListData(mNativeRenderNode, 0);
        mValid = false;
    }

android_view_RenderNode.cpp

......
static void android_view_RenderNode_destroyRenderNode(JNIEnv* env,
        jobject clazz, jlong renderNodePtr) {
    RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr);
    renderNode->decStrong(0);
}

...... 
static void android_view_RenderNode_setDisplayListData(JNIEnv* env,
        jobject clazz, jlong renderNodePtr, jlong newDataPtr) {
    RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr);
    DisplayListData* newData = reinterpret_cast<DisplayListData*>(newDataPtr);
    renderNode->setStagingDisplayList(newData);
}

RenderNode.java 的代码相对比较简单,可以看出 destroyDisplayListData() 方法最后调用的是 setStagingDisplayList() 方法。

RenderNode.cpp

......
// 更新mStagingDisplayListData
void RenderNode::setStagingDisplayList(DisplayListData* data) {
    // 注意这里将mNeedsDisplayListDataSync置为true
    mNeedsDisplayListDataSync = true;
    delete mStagingDisplayListData;
    mStagingDisplayListData = data;
}

setStagingDisplayList() 方法的逻辑也相对比较简单,但是问题恰恰就是因为这个逻辑太过于简单了:我们发现 setStagingDisplayList() 方法仅仅只是清除了 mStagingDisplayListData,然而这个只是 staging 状态的缓存,对于已经绘制过的 View 来说,真正保存数据的是 mDisplayListData,而 mDisplayListData 并没有被清除。那么 mDisplayListData 什么时候会被清除呢?

ViewRootImpl.java

ThreadedRenderer.java

android_view_ThreadedRenderer.cpp)

RenderProxy.cpp

DrawFrameTask.cpp

// 绘制任务
void DrawFrameTask::run() {
    ......
    {
        TreeInfo info(TreeInfo::MODE_FULL, mRenderThread->renderState());
        canUnblockUiThread = syncFrameState(info);
        canDrawThisFrame = info.out.canDrawThisFrame;
    }
    ......
}

bool DrawFrameTask::syncFrameState(TreeInfo& info) {
    ......
    for (size_t i = 0; i < mLayers.size(); i++) {
        mContext->processLayerUpdate(mLayers[i].get());
    }
    mLayers.clear();
    mContext->prepareTree(info, mFrameInfo, mSyncQueued);
    .....
}

RenderNode.cpp


void RenderNode::prepareTree(TreeInfo& info) {
    ......
    prepareTreeImpl(info, functorsNeedLayer);
}

void RenderNode::prepareTreeImpl(TreeInfo& info, bool functorsNeedLayer) {
    ......
    prepareLayer(info, animatorDirtyMask);
    if (info.mode == TreeInfo::MODE_FULL) {
        pushStagingDisplayListChanges(info);
    }
    prepareSubTree(info, childFunctorsNeedLayer, mDisplayListData);
    pushLayerUpdate(info);
    info.damageAccumulator->popTransform();
}

// 1、重新绘制
void RenderNode::pushStagingDisplayListChanges(TreeInfo& info) {
    if (mNeedsDisplayListDataSync) {
        // 注意这里将mNeedsDisplayListDataSync置为false
        mNeedsDisplayListDataSync = false;
        if (mStagingDisplayListData) {
            for (size_t i = 0; i < mStagingDisplayListData->children().size(); i++) {
                mStagingDisplayListData->children()[i]->mRenderNode->incParentRefCount();
            }
        }
        ......
        // 更新DisplayListData
        deleteDisplayListData();
        ......
        mDisplayListData = mStagingDisplayListData;
        mStagingDisplayListData = nullptr;
        ......
    }
}

// 释放资源,清除DisplayList
void RenderNode::deleteDisplayListData() {
    if (mDisplayListData) {
        for (size_t i = 0; i < mDisplayListData->children().size(); i++) {
            // 父节点释放对子节点的引用
            mDisplayListData->children()[i]->mRenderNode->decParentRefCount();
        }
        if (mDisplayListData->functors.size()) {
            Caches::getInstance().unregisterFunctors(mDisplayListData->functors.size());
        }
    }
    // 清除mDisplayListData
    delete mDisplayListData;
    mDisplayListData = nullptr;
}

void RenderNode::decParentRefCount() {
    LOG_ALWAYS_FATAL_IF(!mParentCount, "already 0!");
    mParentCount--;
    if (!mParentCount) {
        destroyHardwareResources();
    }
}

void RenderNode::destroyHardwareResources() {
    if (mLayer) {
        LayerRenderer::destroyLayer(mLayer);
        mLayer = nullptr;
    }
    if (mDisplayListData) {
        for (size_t i = 0; i < mDisplayListData->children().size(); i++) {
            mDisplayListData->children()[i]->mRenderNode->destroyHardwareResources();
        }
        if (mNeedsDisplayListDataSync) {
            deleteDisplayListData();
        }
    }
}

// 2、析构函数
RenderNode::~RenderNode() {
    deleteDisplayListData();
    delete mStagingDisplayListData;
    if (mLayer) {
        ALOGW("Memory Warning: Layer %p missed its detachment, held on to for far too long!", mLayer);
        mLayer->postDecStrong();
        mLayer = nullptr;
    }
}

从代码上看,有三个场景会__清除 mDisplayListData__:

  • 子节点 View __刷新重绘__时,清除旧数据

  • 子节点 RenderNode 执行__析构函数__的时候

  • 父节点清除 mDisplayListData 时,如果子节点的 mParentCount 为 0(即子节点没有关联到任何父节点,这种情况一般出现在 View 被移除、隐藏、超出显示区域的时候),并且 mNeedsDisplayListDataSync 为 true(即子节点数据刷新待重绘)。另外事实上,这种场景是建立在前面两种场景之上的。

问题就出在 setStagingDisplayList() 方法和 decParentRefCount() 方法调用的 先后顺序 上。如果先调用了 decParentRefCount() 方法,而此时 mNeedsDisplayListDataSync 为 false,则不会清除 mDisplayListData,这种情况下就只能指望析构函数了。

总结

出现场景:在某一个View不会再次绘制的情况下,才去释放其对 bitmap 资源的引用。最常见的例如:对于 ViewPager + FrameLayout + ImageView 这样结构的布局,如果我们在 FrameLayout 移出显示范围之后,才去释放 bitmap 资源,那么这个 bitmap 资源将无法及时被回收,只有等到FrameLayout 被复用或者回收的时候,这个bitmap 资源才能被回收。

幸运的是,绝大多数场景下,要么不需要使用 ViewPager,要么 FrameLayout 是复用的,要么及时刷新重绘,因此即使有内存泄漏也只是短暂的(退出页面的时候也可以被回收)。

解决

尽管这个问题不是很严重,但是依然会__占用消耗内存资源__,因此针对该问题出现的场景,有两种方案可以选择:

  • 其一,在父节点重绘(注意这里__特指即释放子节点引用的那次重绘__)之前,及时释放资源,例 ViewPagerCompat.java

  • 其二,复用或者释放相关的 View,尽管这样做可以减少内存泄漏,但是依然还是存在内存泄漏。

另外该问题只出现在 5.0 和 6.0 的系统上,7.0之后的版本Google官方已做修复,详情见:

Free DisplayListData for Views with GONE parents

void RenderNode::setStagingDisplayList(DisplayList* displayList) {
    mNeedsDisplayListSync = true;
    delete mStagingDisplayList;
    mStagingDisplayList = displayList;
    // If mParentCount == 0 we are the sole reference to this RenderNode,
    // so immediately free the old display list
    if (!mParentCount && !mStagingDisplayList) {
        deleteDisplayList();
    }
}

可以看到,关键正是在 setStagingDisplayLis() 方法增加了判断及清除 mDisplayListData 的逻辑。

附其他相关commit链接:

Fix some edge cases

Add a callback for rendernode parentcount=0

Android 5.0以下系统暂未发现该问题,是因为Android 5.0对硬件加速模块做了一次较大的重构,详见链接 Switch DisplayListData to a staging model

但是从log上看,Android 4.4以下也存在类似问题,此处不再讨论,详见链接 Fix hardware layers lifecycle

ChildView 不会被绘制(注意不是内存泄漏-.-)的场景例举

首先,显而易见,当 ChildView __被移除__的时候,这个 ChildView 将不会被绘制。

ViewGroup.java

    @Override
    protected void dispatchDraw(Canvas canvas) {
    ......
        for (int i = 0; i < childrenCount; i++) {
            int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
            final View child = (preorderedList == null)
                    ? children[childIndex] : preorderedList.get(childIndex);
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }
    ......
    }

其次,从 ViewGroup 的 dispatchDraw 方法来看,如果其 ChildView 不可见且不是在执行动画,则该 ChildView 不会被绘制。

View.java

    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    ......
        concatMatrix |= !childHasIdentityMatrix;
        // Sets the flag as early as possible to allow draw() implementations
        // to call invalidate() successfully when doing animations
        mPrivateFlags |= PFLAG_DRAWN;
        if (!concatMatrix &&
                (parentFlags & (ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS |
                        ViewGroup.FLAG_CLIP_CHILDREN)) == ViewGroup.FLAG_CLIP_CHILDREN &&
                canvas.quickReject(mLeft, mTop, mRight, mBottom, Canvas.EdgeType.BW) &&
                (mPrivateFlags & PFLAG_DRAW_ANIMATION) == 0) {
            mPrivateFlags2 |= PFLAG2_VIEW_QUICK_REJECTED;
            return more;
        }
        mPrivateFlags2 &= ~PFLAG2_VIEW_QUICK_REJECTED;
    ......
    }

另外,从 View 的 draw(drawChild 的时候调用)方法可以看出,当 ChildView 超出显示区域 的时候,该 ChildView 也不会被绘制。需要注意的是:当 ViewGroup 设置 clipChildren 为 false 的时候,因为这种场景无法判断这个 ChildView 的显示区域,因此这种情况下,会尝试绘制该 ChildView。

附: ViewPagerCompat:

package com.xosp.hwademo;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
 * @author xuqingqi01@gmail.com
 * 兼容处理 Android5.0-6.0系统,由于硬件加速引起的内存泄漏的问题
 */
public class ViewPagerCompat extends ViewPager {

    private static final String TAG = "ViewPagerCompat";

    private static final boolean TRICK_ENABLED = false;

    private static final int PFLAG2_VIEW_QUICK_REJECTED_COPY = 0x10000000; //View.PFLAG2_VIEW_QUICK_REJECTED

    private static final int LOLLIPOP = 21; // Build.VERSION_CODES.LOLLIPOP

    private static final int MARSHMALLOW = 23; // Build.VERSION_CODES.M

    public ViewPagerCompat(Context context) {
        super(context);
    }

    public ViewPagerCompat(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        final boolean more = super.drawChild(canvas, child, drawingTime);
        if (!TRICK_ENABLED) {
            return more;
        }

        if (Build.VERSION.SDK_INT >= LOLLIPOP && Build.VERSION.SDK_INT <= MARSHMALLOW
                && canvas.isHardwareAccelerated() && child.isHardwareAccelerated()
                && isViewQuickRejected(child)
        ) {
            resetDisplayList(child);
        }

        return more;
    }

    /**
     * check whether the view failed the quickReject() check in draw()
     */
    private static boolean isViewQuickRejected(@NonNull View view) {
        try {
            Field field = View.class.getDeclaredField("mPrivateFlags2");
            field.setAccessible(true);
            int flags = (int) field.get(view);
            return (flags & PFLAG2_VIEW_QUICK_REJECTED_COPY ) == PFLAG2_VIEW_QUICK_REJECTED_COPY;
        } catch (Exception ignore) {
            //ignore.printStackTrace();
        }
        return false;
    }

    /**
     * release display list data
     */
    @SuppressLint("PrivateApi")
    private static void resetDisplayList(@NonNull View view) {
        Log.d(TAG, "resetDisplayList, view=" + view);
        try {
            Method method = View.class.getDeclaredMethod("resetDisplayList");
            method.setAccessible(true);
            method.invoke(view);
        } catch (Exception ignore) {
            //ignore.printStackTrace();
        }
    }

}