Android 滑动绘制流程探究 系统是如何提高滑动性能?

Android 滑动绘制流程探究 系统是如何提高滑动性能?


页面在滑动的过程中如果要感觉流程,必须要达到每秒60帧,当然多了也是浪费,因为那样人眼也是无法区分开来的。Android 图形绘制通过VSYNC机制来保证每秒的绘制帧数达到60帧。Android系统每隔16ms发出VSYNC信号,触发GPU对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,为了能够实现60fps,这意味着程序的大多数操作都必须在16ms内完成。那一帧占用内存数呢?当画面的分辨率是1080×1920(目前最主流的分辨率),刷新率要达到60帧/秒时,那么显卡在一秒钟内需要处理的像素量就达到了“1080×1920×60=124416000”。那么一个“像素量”,相当与占用多少内存?我们用位图来代替粗略计算,把分辨率是1080×1920看成一张对应大小的位图,通过位图的大小来大概计算占用的内存大小。 1080×1920×16/8 = 4147200B = 4050KB = 3 MB,每秒钟需要处理3*60 = 180M的内存。这个数据量是相当大的了,所以滑动绘制过程必定采取了相应的策略增加绘制效率。

我们可以首先研究下ScrollView的滑动绘制原理,ScrollView相对于ListView等其他滑动控件功能更加单一,而且非常常用。说到滑动,必须先说明下View的绘制流程,这部分流程是相当的复杂的。分为以下几种情况1、开启了硬件加速 且滑动视图区域硬件加速关闭了但设置了软加速(缓存bitmap)2、开启硬件加速,滑动视图层硬件加速被关闭,软加速也没开启  3、开启了硬件加速 且滑动视图区域硬件加速也开启了。4、整体硬件加速没开启,但滑动视图区域软加速开启了 5、整体硬件加速没开启,软加速也没开启。听起来听绕口的,其实就是根据硬件加速分情况,View的层级上不能单独的开启硬件加速。所以列举出的情况是五种,而不是六种。关于硬件加速各层级开启方式如下:

在Android中,可以四给不同层次上开启硬件加速:
1、应用:

2、Activity

3、Window
getWindow().setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
4、View
view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
在这四个层次中,应用和Activity是可以选择的,Window只能打开,View只能关闭。
在apk的AndroidManifest中,如果指定了minSDKVersion&targetSDKVersion=7,会使得应用无法使用硬件加速进行绘图。

以上部分是从http://blog.csdn.net/oujunli/article/details/8570902 拷贝而来,如果不允许,请给我留言。


开启硬件加速View的绘制流程

大家应该知道绘制指令是从ViewRootImp 的performTraversals分发出来的。关于这部分知识,大家可以在网上找相关资料,在这里我们先不展开说明。为了简化流程,我们假设绘制的过程没有动画,有动画的情况会稍微复杂点。当我们手动调用invalidate或者通过改变控件内容等操作间接触发invalidate最后都会向上递归,最后调用ViewRooImp的invalidate方法,然后触发scheduleTraversals,最后会触发performTraversals。这个方法大概有着一千行的代码。大概说下,会分别触发performMeasure,performLayout,performDraw等方法,分别对应着测量,布局,绘制三部分。我们今天重点讨论绘制部分,事实上在滑动过程中不会重复触发测量和布局的方法,而绘制会不断的调用,多以这部分是性能优化的重点。
  private void performDraw() {


        final boolean fullRedrawNeeded = mFullRedrawNeeded;
        mFullRedrawNeeded = false;
        try {
            draw(fullRedrawNeeded);
        } finally {
        }

}



performDraw调用draw(fullRedrawNeeded),参数意味着是否需要全部绘制,performDraw调用过来的全部为false。


 private void draw(boolean fullRedrawNeeded) {
        if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) {
                // If accessibility focus moved, always invalidate the root.
                boolean invalidateRoot = accessibilityFocusDirty || mInvalidateRootRequested;
                mInvalidateRootRequested = false;


                // Draw with hardware renderer.
                mIsAnimating = false;


                if (mHardwareYOffset != yOffset || mHardwareXOffset != xOffset) {
                    mHardwareYOffset = yOffset;
                    mHardwareXOffset = xOffset;
                    invalidateRoot = true;
                }


                if (invalidateRoot) {
                    mAttachInfo.mHardwareRenderer.invalidateRoot();
                }


                dirty.setEmpty();


                // Stage the content drawn size now. It will be transferred to the renderer
                // shortly before the draw commands get send to the renderer.
                final boolean updated = updateContentDrawBounds();


                if (mReportNextDraw) {
                    // report next draw overrides setStopped()
                    // This value is re-sync'd to the value of mStopped
                    // in the handling of mReportNextDraw post-draw.
                    mAttachInfo.mHardwareRenderer.setStopped(false);
                }


                if (updated) {
                    requestDrawWindow();
                }


                mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this);
            }
    }



 if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled())意味着如果开启了硬件加速,我只是截取了一部分代码,后面逻辑没有截出来,待会讨论。 mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this);使用mHardwareRenderer来构建视图,我们看看里面具体实现:

    void draw(View view, AttachInfo attachInfo, HardwareDrawCallbacks callbacks) {
        attachInfo.mIgnoreDirtyState = true;


        final Choreographer choreographer = attachInfo.mViewRootImpl.mChoreographer;
        choreographer.mFrameInfo.markDrawStart();


        updateRootDisplayList(view, callbacks);


        attachInfo.mIgnoreDirtyState = false;

}



内部调用了updateRootDisplayList 这个方法是关键。

  
 private void updateRootDisplayList(View view, HardwareDrawCallbacks callbacks) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Record View#draw()");
        updateViewTreeDisplayList(view);


        if (mRootNodeNeedsUpdate || !mRootNode.isValid()) {
            DisplayListCanvas canvas = mRootNode.start(mSurfaceWidth, mSurfaceHeight);
            try {
                final int saveCount = canvas.save();
                canvas.translate(mInsetLeft, mInsetTop);
                callbacks.onHardwarePreDraw(canvas);


                canvas.insertReorderBarrier();
                canvas.drawRenderNode(view.updateDisplayListIfDirty());
                canvas.insertInorderBarrier();


                callbacks.onHardwarePostDraw(canvas);
                canvas.restoreToCount(saveCount);
                mRootNodeNeedsUpdate = false;
            } finally {
                mRootNode.end(canvas);
            }
        }
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }



有没有发现硬件加速使用的是DisplayListCanvas ,它是通过DisplayListCanvas canvas = renderNode.start(width, height)来获得,用于硬件加速的画布。上面代码中的updateViewTreeDisplayList(view),会更新RenderNode数据,所以仅仅是透明度这种样式的改变,是不需要重新绘制的。而且RenderNode缓存了整个View树的绘制命令,可以不通过调用View的draw方法,而完整的绘制出整个视图。部分updateViewTreeDisplayList(view)代码如下:

    public RenderNode updateDisplayListIfDirty() {
    
    if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0
                || !renderNode.isValid()
                || (mRecreateDisplayList)) {
            if (renderNode.isValid()
                    && !mRecreateDisplayList) {
                mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                dispatchGetDisplayList();
                return renderNode; // no work needed
            }


            try {
                if (layerType == LAYER_TYPE_SOFTWARE) {
                    buildDrawingCache(true);
                    Bitmap cache = getDrawingCache(true);
                    if (cache != null) {
                        canvas.drawBitmap(cache, 0, 0, mLayerPaint);
                    }
                } else {
                    computeScroll();


                    canvas.translate(-mScrollX, -mScrollY);
                    mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
                    mPrivateFlags &= ~PFLAG_DIRTY_MASK;


                    // Fast path for layouts with no backgrounds
                    if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                        dispatchDraw(canvas);
                        if (mOverlay != null && !mOverlay.isEmpty()) {
                            mOverlay.getOverlayView().draw(canvas);
                        }
                    } else {
                        draw(canvas);
                    }
                }
            } finally {
                renderNode.end(canvas);
                setDisplayListProperties(renderNode);
            }
        } else {
            mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        }
        return renderNode;
    }


大概解释下上述代码,第一个判断语句为了判断当前的RenderNode是否需要更新,如果不需要更新则直接返回使用。第二个判断语句,判断当前的View的layerType看是否开启软加速,软加速其实是将绘制保存在一个bitmap中,gpu渲染bitmap的速度是相当快的,在一定程度上可以提高视图渲染性能,但bitmap缓存却会占用大量的内存空间,所以是否开启软加速要视情况而定。
上面大概的介绍了下硬件加速绘图的整体流程,我这里再总结下。系统发出绘制指令,可以是手动调用invilidate触发,也可能是系统自主触发,最后都会触发ViewRootImpl的performTraversals,然后会调用draw(boolean fullRedrawNeeded) 进行绘图,内部会判断是否是硬件加速,如果是则会创建DisplayListCanvas来更新或者复用RenderNode,最后通过RenderNode将绘图完成。所以如果是滑动,系统不需要遍历整个View树来绘制,而只需要简单更新RenderNode便可以完成整个滑动过程。在这里大家可以发现在硬件加速环境下,确实可以很大程度上提高滑动性能,那非硬件加速环境下呢?


未开启硬件加速View的绘制流程

未开启硬件加速整体的流程和之前大体是相同的,不过调用draw(boolean fullRedrawNeeded)不会再触发mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this),而是调用了drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)看名字也知道,这个绘制软件层的视图。大概流程如下:
  private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {


            Canvas  canvas = mSurface.lockCanvas(dirty);
            mView.draw(canvas);
     
        return true;
    }

mSurface.lockCanvas(dirty);会调用如下代码:
   public Canvas lockCanvas(Rect inOutDirty)
            throws Surface.OutOfResourcesException, IllegalArgumentException {
Canvas mCanvas = new CompatibleCanvas();
            return mCanvas;
        }
    }



从这里就可以看出,软件层的绘制使用的是CompatibleCanvas,并且会触发根视图的draw方法,其实draw方法又会触发dispatchDraw方法,然后触发drawChild方法,完成整个View树的更新。不过其中也是有优化,比如通过canvas的canvas.quickReject快速拒绝,可以拒绝绘制屏幕外的内容。如果开启了LAYER_TYPE_SOFTWARE,软加速,会对一屏的View进行bitmap缓存,所以每次滑动都只会绘制新出现的那个View。


ScrollView滑动流程分析


这部分比较简单,大概说下流程,以拖动滑动为例。触摸事件的消费肯定在public boolean onTouchEvent(MotionEvent ev)方法中,我们找下case MotionEvent.ACTION_MOVE这个条件分支,处理手势移动事件,里面方法比较多,不过会触发overScrollBy方法然后间接触发onOverScrolled方法,onOverScrolled有实现,如下:

protected void onOverScrolled(int scrollX, int scrollY,
            boolean clampedX, boolean clampedY) {
        // Treat animating scrolls differently; see #computeScroll() for why.
        if (!mScroller.isFinished()) {
            final int oldX = mScrollX;
            final int oldY = mScrollY;
            mScrollX = scrollX;
            mScrollY = scrollY;
            invalidateParentIfNeeded();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (clampedY) {
                mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange());
            }
        } else {
            super.scrollTo(scrollX, scrollY);
        }


        awakenScrollBars();
    }



大家看到,如果没有动画则会调用super.scrollTo(scrollX, scrollY);方法实现位置的改变,有动画的情况也是一样的,会不断的触发scrollTo实现动画的效果。scrollTo会调用

postInvalidateOnAnimationScroll来更新视图。

 public void postInvalidateOnAnimation() {
        // We try only with the AttachInfo because there's no point in invalidating
        // if we are not attached to our window
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            attachInfo.mViewRootImpl.dispatchInvalidateOnAnimation(this);
        }
    }



大家看到又将事件传回到ViewRootImp中了,其实所有的刷新事件都是最后由ViewRootImp来消费的。最后也都会触发performTraversals来更新视图,具体的逻辑大家可以自己跟踪下,边边角角的逻辑,其实不需要了解太详情,真要使用到的时候再详细的看。



你可能感兴趣的:(android,ui,性能,硬件加速)