Android 5.0~6.0系统,由于硬件加速引起的内存泄漏问题
实例
从 hprof 文件可以看到:这些 bitmap 除了一个 JNI Global 的引用之外,已经没有其他的引用了,而正是由于这个 GC root 引用,导致这些 bitmap 无法被及时回收。
原因
......
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 什么时候会释放呢?
......
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()。这样来看,似乎逻辑上并没有什么问题,但是我们不妨接着往下看:
......
public void destroyDisplayListData() {
if (!mValid) return;
nSetDisplayListData(mNativeRenderNode, 0);
mValid = false;
}
......
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() 方法。
......
// 更新mStagingDisplayListData
void RenderNode::setStagingDisplayList(DisplayListData* data) {
// 注意这里将mNeedsDisplayListDataSync置为true
mNeedsDisplayListDataSync = true;
delete mStagingDisplayListData;
mStagingDisplayListData = data;
}
setStagingDisplayList() 方法的逻辑也相对比较简单,但是问题恰恰就是因为这个逻辑太过于简单了:我们发现 setStagingDisplayList() 方法仅仅只是清除了 mStagingDisplayListData,然而这个只是 staging 状态的缓存,对于已经绘制过的 View 来说,真正保存数据的是 mDisplayListData,而 mDisplayListData 并没有被清除。那么 mDisplayListData 什么时候会被清除呢?
android_view_ThreadedRenderer.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);
.....
}
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链接:
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 将不会被绘制。
@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 不会被绘制。
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();
}
}
}