Android View 的绘制流程

197 阅读8分钟

Android View 的绘制流程

说到Android中Vew的绘制流程,想必每个Android开发的人员都会想到是onMeasure、onLayout、onDraw。事实上这三个方法是正确的但是只知道这三个方法是不够的我们还要知道整体的绘制流程是什么时候开始的怎么触发的。

首先先通过图片总结一下View整体的绘制流程

下面根据上面的流程来分析View绘制流程中的相关操作

其实上面我们能看出View整体的绘制流程开始是由ActivityThread的handleResumeActivity方法触发的,那么先看一下这个方法是怎么执行的,代码如下

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume) {
  ....
     if (r.window == null && !a.mFinished && willBeVisible) {
                r.window = r.activity.getWindow();
                //获取当前的Activity绑定的decorView
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                l.softInputMode |= forwardBit;
                if (a.mVisibleFromClient) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l);
                }

            // If the window has already been added, but during resume
            // we started another activity, then don't yet make the
            // window visible.
            } else if (!willBeVisible) {
                if (localLOGV) Slog.v(
                    TAG, "Launch " + r + " mStartedActivity set");
                r.hideForNow = true;
            }
  ...

从上面的代码中可以看出的是在resumeActivity的时候会获取当前Activity对应的窗体Window,之后通过Window获取已经绑定好的DecorView,同时通过Activity获取对应的WindowManager,使用获取的WindowManger将decorView交给要执行布局绘制的类,这就是在handleResumeActivity中做的事情。接下来我们看wm.addView()这个方法又进行了那些操作,根据源码我们会发现这个addView最终会交由WindowManagerGlobal(以下简称WMG)去执行,那么WMG又是怎么将View交给要执行布局绘制的类的呢,通过源码我们可以知道实际上WMG是通过ViewRootImpl实现View的绘制的,详细代码如下

 public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
   			...
   			ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            // Start watching for system property changes.
            if (mSystemPropertyUpdater == null) {
                mSystemPropertyUpdater = new Runnable() {
                    @Override public void run() {
                        synchronized (mLock) {
                            for (int i = mRoots.size() - 1; i >= 0; --i) {
                                mRoots.get(i).loadSystemProperties();
                            }
                        }
                    }
                };
                SystemProperties.addChangeCallback(mSystemPropertyUpdater);
            }

            int index = findViewLocked(view, false);
            if (index >= 0) {
                if (mDyingViews.contains(view)) {
                    // Don't wait for MSG_DIE to make it's way through root's queue.
                    mRoots.get(index).doDie();
                } else {
                    throw new IllegalStateException("View " + view
                            + " has already been added to the window manager.");
                }
                // The previous removeView() had not completed executing. Now it has.
            }

            // If this is a panel window, then find the window it is being
            // attached to for future reference.
            if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                    wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
                final int count = mViews.size();
                for (int i = 0; i < count; i++) {
                    if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
                        panelParentView = mViews.get(i);
                    }
                }
            }

            root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }

        // do this last because it fires off messages to start doing things
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            synchronized (mLock) {
                final int index = findViewLocked(view, false);
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
            }
            throw e;
        }
 }

从上面的代码可以看出,WMG首先创建了ViewRootImpl对象,之后通过ViewRootImpl中的setView方法将要进行绘制的DecorView交给ViewRootImpl。那么来看一下ViewRootImpl中是怎么操作的吧,先来看一下源码,源码如下

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;
              	....
                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.
                requestLayout();
               ....
    }

这个方法中在进行绘制的流程中只需要关注两行代码一行是赋值,一行是进行方法调用。首先赋值这行代码很清楚的告诉我们是将DecorView赋值给当前ViewRootImpl中的成员变量mView,之后就是调用requestLayout()方法,这个方法通过名字我们可以看出就是请求布局,那么是怎么布局的呢我们跟进去看一下。源码如下

    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }   
    }

其实这个方法很简单只有几行代码,但是这几行代码都是比较重要的其他几行先不关注本篇只关心绘制流程(关于其他的代码行文章末尾会有简单的介绍),绘制流程实际上走的最后一行代码也就是调用了scheduleTraversals()方法,那么这个方法又是怎么实现绘制流程的呢这个问题还需要我们继续看源码。源码如下

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            //交给Choreographer一个线程实现View绘制流程的执行
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

通过上面的源码可以发现,实际上scheduleTraversals()这个方法就是像Choreographer中post一个回调,这个回调是什么呢,这个回调就是一个Runnable,当Choreographer收到底层发过来的信号那么就会调用这个Runnable进行布局的绘制,那么Choreographer是怎么调用的这个Runnable的呢,我们可以根据源码进行查看。源代码如下

public void postCallback(int callbackType, Runnable action, Object token) {
        postCallbackDelayed(callbackType, action, token, 0);
    }
public void postCallbackDelayed(int callbackType,
            Runnable action, Object token, long delayMillis) {
       ......
        postCallbackDelayedInternal(callbackType, action, token, delayMillis);
    }

private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
     	......  		
      mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
			......
    }

    void doFrame(long frameTimeNanos, int frame) {
        ......
        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
            AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

            mFrameInfo.markInputHandlingStart();
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

            mFrameInfo.markAnimationsStart();
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);

            mFrameInfo.markPerformTraversalsStart();
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        } finally {
            AnimationUtils.unlockAnimationClock();
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }

        if (DEBUG_FRAMES) {
            final long endNanos = System.nanoTime();
            Log.d(TAG, "Frame " + frame + ": Finished, took "
                    + (endNanos - startNanos) * 0.000001f + " ms, latency "
                    + (startNanos - frameTimeNanos) * 0.000001f + " ms.");
        }
    }
void doCallbacks(int callbackType, long frameTimeNanos) {
        CallbackRecord callbacks;
        synchronized (mLock) {
            // We use "now" to determine when callbacks become due because it's possible
            // for earlier processing phases in a frame to post callbacks that should run
            // in a following phase, such as an input event that causes an animation to start.
            final long now = System.nanoTime();
            callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
                    now / TimeUtils.NANOS_PER_MS);
            .....
            try {
                Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
                for (CallbackRecord c = callbacks; c != null; c = c.next) {
                    if (DEBUG_FRAMES) {
                        Log.d(TAG, "RunCallback: type=" + callbackType
                                + ", action=" + c.action + ", token=" + c.token
                                + ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime));
                    }
                    c.run(frameTimeNanos);
                }
        		......
    }

上面的这段代码有点长,别急我们慢慢分析,首先先分析最上面的几个post方法,第一个post方法实际上就是ViewRootImpl中调用的post方法,用于将Runnable对象添加给Choreographer的mCallbackQueues这个数组的。那么这个数组是用来干嘛的这个数组就是用来记录当前的Choreographer中有多少回调的,方便以后执行的回调通知。接下来我们会看到doFrame()这个方法这个方法就实际上执行绘制的起始点,实际上Android中我们将View添加到了窗体中并不会立即执行View的绘制,Android会等到底层的信号帧过来才会执行绘制流程,那么这个绘制帧什么时候过来呢,这就需要我们了解60Fps这个概念了,60Fps是指Android系统1s中刷新60次那么一次刷新的时间是16ms,所以绘制帧过来的时间是16ms,每隔16ms底层会发出一个这样的帧那么Choreographer收到这个帧就会调用doFrame这个方法,doFrame这个方法会调用doCallBacks这个方法,doCallBacks中会取出mCallbackQueues中的callbacks执行其run方法,那么这里就将会执行之前的Runnable中的run方法执行doTraversal方法。接下来我们继续分析doTraversal方法,其源代码如下

 void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }

doTraversal这个方法先判断mTraversalScheduled是不是true,如果是的话执行下面的代码,不是的话不执行下面的代码。那么mTraversalScheduled什么时候被设置为true的那么,这个就要看上面scheduleTraversals方法,在这方法中会将mTraversalScheduled设置为true,所以doTraversal方法会继续执行下面的代码,那么下面的代码是什么意思呢,首先将mTraversalScheduled的值修改为false,为了下次继续进行绘制的调用,之后调用Handler获取Looper通过Looper获取MessageQueue移除同步屏障,这个同步屏障是什么时候添加的呢,其实同样也是scheduleTraversals这个方法中添加的,大家可以仔细看看这个方法。那么接下来就会执行performTraversals这个方法了,这个方法实际上就是调用measure、layout、draw的,我们可以跟进源码看一下。源代码如下

 private void performTraversals() {
           					WindowManager.LayoutParams lp = mWindowAttributes;
                    ......
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

                    // Implementation of weights from WindowManager.LayoutParams
                    // We just grow the dimensions as needed and re-measure if
                    // needs be
                    int width = host.getMeasuredWidth();
                    int height = host.getMeasuredHeight();
                    boolean measureAgain = false;

                    if (lp.horizontalWeight > 0.0f) {
                        width += (int) ((mWidth - width) * lp.horizontalWeight);
                        childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }
                    if (lp.verticalWeight > 0.0f) {
                        height += (int) ((mHeight - height) * lp.verticalWeight);
                        childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }

                    if (measureAgain) {
                        if (DEBUG_LAYOUT) Log.v(TAG,
                                "And hey let's measure once more: width=" + width
                                + " height=" + height);
                        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    }

                    layoutRequested = true;
                }
            }
        } 

        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
        boolean triggerGlobalLayoutListener = didLayout
                || mAttachInfo.mRecomputeGlobalAttributes;
        if (didLayout) {
            	performLayout(lp, desiredWindowWidth, desiredWindowHeight);

      				......
            }

            if (DBG) {
                System.out.println("======================================");
                System.out.println("performTraversals -- after setFrame");
                host.debug();
            }
        }

        ......

        boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() ||
                viewVisibility != View.VISIBLE;

        if (!cancelDraw && !newSurface) {
            if (!skipDraw || mReportNextDraw) {
                if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                    for (int i = 0; i < mPendingTransitions.size(); ++i) {
                        mPendingTransitions.get(i).startChangingAnimations();
                    }
                    mPendingTransitions.clear();
                }

                performDraw();
            }
        }......
    }

上面的代码就是performTraversals中的一部分,实际上这个方法中还有很多代码但是我们只需要跟measure、layout、draw相关的部分,所以很的其他代码让我给省略了,想看其他代码的朋友可以自行翻阅ViewRootImpl中的performTraversals,那么来说说上面的代码是干什么用的,首先可以看到performMeasure会执行,这个方法实际上就是执行View的measure方法的并且将测量规格传递给View,让View进行测量,测量结束之后会对Window的LayoutParams进行判断实际判断是不是带有权重的,如果带有权重需要重新进行二次测量;如果没有那么就会向下执行,向下执行会判断是不是需要进行layout如果需要那么就触发performLayout,那么就会触发View的layout方法进行排版,排版之后会依然会判断是不是需要进行绘制如果需要进行绘制会执行performDraw方法,这个方法会让View执行draw进行相关的内容绘制。

结尾小内容

关于上面说的requestLayout中的其他代码,其实主要想说的就是checkThread,这个方法我们跟进源码会发现这个方法中会抛出“Only the original thread that created a view hierarchy can touch its views.”这个异常,这个方法实际上就是校验是不是当前线程更新UI的。那究竟是怎么校验的呢大家可以阅读一下源码。

以上就是整个View的绘制流程的执行过程,本篇文章只是简单的分析了一下View的绘制流程,有什么不对的希望同行们可以多多给予指正。 谢谢!!!