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来更新视图,具体的逻辑大家可以自己跟踪下,边边角角的逻辑,其实不需要了解太详情,真要使用到的时候再详细的看。


更多相关文章

  1. Android上超级好用的前端调试方法(adb reverse)
  2. Android(安卓)入门第十讲02-广播(广播概述,使用方法(系统广播,自定义
  3. Android(java方法)上实现mp4的分割和拼接 (二)
  4. Android开发指南-用户界面-事件处理
  5. Android(安卓)Studio 基础 之 获取 Wifi 的状态,设置 Wifi 的开关
  6. Android开发中立即停止AsyncTask和Thread的一些办法
  7. Cocos2d-x Lua实现从Android回调到Lua的方法
  8. Android(安卓)副屏density设置,解决副屏view拉伸现象
  9. Android(安卓)APP首次登录和之后自动登录流程

随机推荐

  1. application全局变量
  2. Android的animation
  3. android 渐变的半圆进度条
  4. 在Eclipse 中关联android sdk 源代码
  5. Android的Location功能代码
  6. Android(安卓)Dialog定义没有标题的注意
  7. 关于android studio 的FAILURE: Build fa
  8. 获取android系统信息或应用程序信息
  9. 7 android 星级滑块
  10. **android studio 编写淘宝界面## 标题**