Android View绘制流程

前言

不知道大家有没有想过一个问题,当启动一个Activity的时候,相应的XML布局文件中的View是如何显示到屏幕上的?有些同学会说是通过onMeasure()onLayout()onDraw()这3个方法来完成的,实际上这只是系统暴露给我们使用的最基本的方法,背后的流程要比这个更加复杂,今天就和大家一起扒一下背后还做了什么事情。

我们知道Activity执行了onCreate()onStart()onResume()3个方法后,用户就能看见视图了。这背后实际上经历了2个过程,其一,通过ActivityThread调度生命周期相关的方法;其二,通过setContentView()把XML解析成View对象。这里兵分2路,先来看一下setContentView()

setContentView()方法

这里借用这篇文章的图来表示setContentView整个流程
https://blog.csdn.net/Rayht/article/details/80782697

setContentView.png

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
}

从这里进去会进去到Activity.setContentView(),最后会调用PhoneWindow.setContentView()

##PhoneWindow
@Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            //1.初始化DecorView
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        //判断有没有转场动画
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            //2.解析传进来的xml布局,生成一个ContentView
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

这里分为2条线,我们先来看一下初始化mDecor。

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            //1.生成DecorView
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            //2.生成mContentParent
            mContentParent = generateLayout(mDecor);
    }
    .......
}

protected DecorView generateDecor(int featureId) {
        Context context;
        ......
        //DecorView extends FrameLayout
        return new DecorView(context, featureId, this, getAttributes());
    }

mDecor就是DecorView对象,它是PhoneWindow的顶层View,查看DecorView源码可以看出DecorView 是一个FrameLayout,但是源码中并没有展示出它是一个怎样的布局,因为它是在注释2mContentParent = generateLayout(mDecor)中添加的,下面就来一起看一下。

##PhoneWindow. generateLayout()
protected ViewGroup generateLayout(DecorView decor) {
          // Inflate the window decor.

        int layoutResource; //是一个布局文件id
        int features = getLocalFeatures();
        // 根据不同的主题将对应的布局文件id赋值给layoutResource
        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            layoutResource = R.layout.screen_swipe_dismiss;
            setCloseOnSwipeEnabled(true);
        } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
            ....
            }
            ....          
         else {
            layoutResource = R.layout.screen_simple;
        }

        mDecor.startChanging();
        //通过LayoutInflater加载解析layoutResource布局文件
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

        .......
}

这里展示的是generateLayout的上半部分,先根据不同的主题生成不同的布局文件,然后解析layoutResource布局文件。

onResourcesLoaded()

##DecorView. onResourcesLoaded()
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
        .....

        mDecorCaptionView = createDecorCaptionView(inflater);
        //1.通过XmlResourceParser解析XML布局文件,得到一个View对象
        //这里的root就是DecorView中添加的layoutResource 
        final View root = inflater.inflate(layoutResource, null);
        //2.把layoutResource布局文件添加到DecorView中
        if (mDecorCaptionView != null) {
            if (mDecorCaptionView.getParent() == null) {
                addView(mDecorCaptionView,
                        new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
            }
            mDecorCaptionView.addView(root,
                    new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
        } else {

            // Put it below the color views.
            addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
        mContentRoot = (ViewGroup) root;
        initializeElevation();
    }

现在我们知道了,generateLayout其实是给DecorView添加一个布局文件,下面就来看一下DecorView到底是怎样的布局?

上面说过会根据不同的主题选择不同的layoutResource,这里我们看一下最常用的layoutResource = R.layout.screen_simple


    
    
    

可以看出这是一个垂直的LinearLayout,上面是一个ActionBar,下面是一个id为content的FrameLayout,先给大家透漏一下,这个FrameLayout就是用于装载平时我们写的Activity中的xml布局。

generateLayout下半部分

##PhoneWindow. generateLayout()
protected ViewGroup generateLayout(DecorView decor) {
        int layoutResource; //是一个布局文件id
        ......
        //往DecorView中添加layoutResource布局文件
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
        //查找DecorView中id为ID_ANDROID_CONTENT的View
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }
        .......
      return contentParent;
}

这里就更简单了,通过findViewById找到之前布局中id为content的view,最后返回到这个contentParent就是刚才的FrameLayout。

我们再回到最开始的setContentView()方法

##PhoneWindow
@Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            //1.初始化DecorView
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        //判断有没有转场动画
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            //2.解析传进来的xml布局,生成一个ContentView
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

上面我们讲了installDecor()实际上就是创建并初始化DecorView对象,也就是完成了mContentParent的初始化,接下来看一下注释2,这里是解析Activity中传进来的布局layoutResID,其中parent是mContentParent,也就是之前说过的那个FrameLayout,这样就把Activity中的布局添加到DecorView中了。

小结一下:

setContentView()流程如下:
1、在PhoneWindow中创建顶层的DecorView;
2、在DecorView中会根据主题的不同加载一个不同的布局;
3、把Activity中的布局解析并添加到DecorView的FrameLayout中

这是我画的一幅图,按照这个去看源码会比较清晰。

setContentView().png

到这里算是把setContentView流程分析完了,但把我们仅仅是把自己的Layout添加到DecorView中,但如何显示到屏幕上还没看呢。

布局文件中的UI是如何显示的呢?

其实在文章开头说过了,在ActivityThread的生命周期调度中完成的。刚才已经看过了onCreate(),后面还会调用onStart()、onResume(),而最终UI的显示就是在onResume()中完成的,也就是说我们平常接触最多的measurelayoutdraw3个方法都在onResume中完成的。而onResume()是在ActivityThread的handleResumeActivity()中调用的。

 public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {
    unscheduleGcIdler();
        mSomeActivitiesChanged = true;

        // 1.会调用Activity的onResume
        final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
        
        final Activity a = r.activity;

      
        final int forwardBit = isForward
                ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;

        // If the window hasn't yet been added to the window manager,
        // and this guy didn't finish itself or start another activity,
        // then go ahead and add the window.
        boolean willBeVisible = !a.mStartedActivity;
        if (!willBeVisible) {
            try {
                willBeVisible = ActivityTaskManager.getService().willActivityBeVisible(
                        a.getActivityToken());
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            //获取WindowManager对象
            ViewManager wm = a.getWindowManager();
            //初始化窗口布局属性
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            if (r.mPreserveWindow) {
                a.mWindowAdded = true;
                r.mPreserveWindow = false;
                // Normally the ViewRoot sets up callbacks with the Activity
                // in addView->ViewRootImpl#setView. If we are instead reusing
                // the decor view we have to notify the view root that the
                // callbacks may have changed.
                ViewRootImpl impl = decor.getViewRootImpl();
                if (impl != null) {
                    impl.notifyChildRebuilt();
                }
            }
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                   //2.WindowManager添加DecorView
                    wm.addView(decor, l);
                } else {
                    // The activity will get a callback for this {@link LayoutParams} change
                    // earlier. However, at that time the decor will not be set (this is set
                    // in this method), so no action will be taken. This call ensures the
                    // callback occurs with the decor set.
                    a.onWindowAttributesChanged(l);
                }
            }
            ......
}

这里最关键的地方是注释1和注释2,这里先看一下注释2,wm是一个接口ViewManager对象,而wm是通过Activity的getWindowManager()获取的,最后你会发现wm是在WindowManagerImpl中初始化的,

##WindowManagerImpl
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }
##WindowManagerGlobal
public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
    ....
     ViewRootImpl root;
     View panelParentView = null;
      //创建ViewRootImpl对象,挺重要的一个类,后面会解释
     root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

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

      
            try {
                //1.关键调用
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
}

这里最关键的是调用了setView()方法,这个方法内又调用了requestLayout()

##ViewRootImpl
public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            //检查是否在UI线程
            checkThread();
            mLayoutRequested = true;
            
            scheduleTraversals();
        }
    }

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            //callback与Choreographer交互,会在下一帧被渲染时触发
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

这里的 mChoreographer实际上是Choreographer对象,Choreographer是在屏幕刷新机制中接收显示系统的VSync信号,postFrameCallback设置自己的callback与Choreographer交互,你设置的callCack会在下一个frame被渲染时触发。这里简单了解下即可。这里重点关注下mTraversalRunnable对象。

##ViewRootImpl
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }

traversal是遍历的意思,也就是说后面会做遍历操作,至于为什么,接着往下看。

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

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }
            //关键代码
            performTraversals();

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

performTraversals()
这段代码非常长,但是核心的地方就是下面注释的3处。

##ViewRootImpl
private void performTraversals() {
     int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
     int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);      
     ......

if (mFirst) { 
        ..... 
        // host为DecorView 
        // 调用DecorVIew 的 dispatchAttachedToWindow,并且把 mAttachInfo 给子view ,view.post就是利用了这个原理
        host.dispatchAttachedToWindow(mAttachInfo, 0); 
        ..... 
    }  
     // Ask host how big it wants to be
     //1.执行测量 
     performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
     ......
     //2.执行布局
     performLayout(lp, mWidth, mHeight);
    ......
     //3.执行绘制
     performDraw();
}
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
              //调用View的measure()
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        mLayoutRequested = false;
        mScrollMayChange = true;
        mInLayout = true;

        final View host = mView;

        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
       //调用View的layout()
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
}

private void performDraw() {
   ......
   draw(fullRedrawNeeded);
   ......
}

 public void draw(Canvas canvas) {
      ......
      //调用View的onDraw()
      onDraw(canvas);
      ......
}
  • performMeasure():从根节点向下遍历View树,完成所有ViewGroup和View的测量工作,计算出所有ViewGroup和View显示出来需要的高度和宽度;

  • performLayout():从根节点向下遍历View树,完成所有ViewGroup和View的布局计算工作,根据测量出来的宽高及自身属性,计算出所有ViewGroup和View显示在屏幕上的区域;

  • performDraw():从根节点向下遍历View树,完成所有ViewGroup和View的绘制工作,根据布局过程计算出的显示区域,将所有View的当前需显示的内容画到屏幕上。

现在明白了为什么前面那个方法的名字是遍历了吧,因为最后是要完成以DecorView为根节点的view树的遍历。

大家对照我画的这幅图去看源码,会比较好理解。


View绘制流程.png

关于View绘制流程真的很长,代码量也大,但是我们只需要关注核心流程就可以了。最后做一个总结:

  • 1、在onCreate 方法中通过setContentView将XML布局解析成java对象,并添加到PhoneWindow的DecorView中;
  • 2、在onResume中将DecorView添加到WindowManagerImpl中,然后通过ViewRootImpl来执行View的绘制流程;
  • 3、在ViewRootImplperformTraversals()方法中分别调用performMeasureperformLayoutperformDraw来完成测量、布局、绘制;
  • 4、performMeasureperformLayoutperformDraw这几个方法最终会调用measure()layout()draw()来完成最终的绘制。

参考

Android 自定义View之View的绘制流程(一)
【朝花夕拾】Android自定义View篇之(一)View绘制流程

你可能感兴趣的:(Android View绘制流程)