来聊聊Activity的显示原理

5,135

我们本文要回答的问题:

  • setContentView原理是什么?
  • Activity在onResume之后才会显示的原因是什么?
  • ViewRoot是干嘛的,是ViewTree的rootview吗?

一、整体流程图

先上份地图:

整体流程图

大概说一下流程(本文最后也会再重复一次):

  1. 创建PhoneWindow对象,往PhoneWindow对象里面添加了一个DecorView
  2. DecorView绑定一个ViewRootImpl,由这个ViewRootImpl负责View的绘制和刷新
  3. ViewRootImpl通过IWindowSessionWMS发起Binder调用,而WMS也会通过IWindow向应用端发起调用
  4. ViewRootImpl会在WMS里面注册一个窗口,然后由WMS统一的管理所有窗口的大小,位置和层级
  5. 在第一次绘制时,ViewRootImpl还会向WMS申请一块SurfaceWMSSurface申请),有了Surface之后,应用端就可以进行绘制了
  6. 应用端绘制完之后,SurfaceFlinger就会按照WMS里面提供的层级等信息进行合成,最终显示

为了让大家在跟踪源码的时候不会迷路,先跟大家科普路下路上的这几个“门卫大哥”:

  • PhoneWindowWindow在手机端的唯一实现类,WMS管理的就是一个个Window,而不是View
  • DecorView:顶层的View,我们平时setContentView设置的View对应的就是蓝色的ContentView
  • ViewRootImpl:这个哥们是ViewWMS通信的桥梁,每次View想跟WMS通信要通过它;每次WMS想让View更新也要通过它;一个DecorView对应一个ViewRootImpl
  • SurfaceFlinger:负责Surface的合成,一块Surface就是一块画布,应用端其实都是在Surface上绘制图形
  • WindowManagerService:我们口中经常说的WMS,主要负责管理窗口,,并不负责view的绘制。以下是WMS的主要作用:

对了,我采用的源码是Android 28的。

下面正式开始~ Action, go ~

二、源码分析

2.1 setContentView

// 类:---> View.java
public void setContentView(int layoutResId){
    getWindow().setContentView(layoutResId)
;
    ...
}

//类:---->PhoneWindow.java
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
            installDecor();
        }
    ...
    
     mLayoutInflater.inflate(layoutResID, mContentParent);
    ...   
}

我们可以看到,通过mLayoutInflater.inflate(layoutResID, mContentParent)进行加载布局,这里有两个参数,一个是layoutId,另外一个是mContentParent,这个mContentParent又是什么?

可以看到mContentParent是通过installDecor()初始化的,继续跟吧。

2.2 installDecor

---> 类:PhoneWindow.java
 private void installDecor() {
     if (mDecor == null) {
     //1、new 一个 DecorView
            mDecor = generateDecor(-1);
     .... 
     }
      if (mContentParent == null) {
      //2、inflate 布局
            mContentParent = generateLayout(mDecor);
          ....
      }
 }
 
//1、new 一个 DecorView
 protected DecorView generateDecor(int featureId) {
     ....
     return new DecorView(context, featureId, this, getAttributes());
 }
 //2、inflate 布局
 protected ViewGroup generateLayout(DecorView decor) {
     ....
     // Inflate the window decor.
     //3、inflate 布局,并添加到decorView中
     mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
    //4、找到contentView,返回
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    ...
     return contentParent;
 }
 
 ///----->DecorView.java
 void onResourcesLoaded(LayoutInflater inflater, int layoutResource){
      //3、
      final View root = inflater.inflate(layoutResource, null);
      ...
      // Put it below the color views.
      addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
 }
 

我们理一下流程,以上代码主要做了:

  • 创建一个DecorView,这个DecorView本质是一个FrameLayout
  • inflate一个布局root
  • 把root 添加到decorView里面
  • 通过findViewById找到contentParent,其实就是id为R.id.content的View

提个小问题提提神,大家知道为什么在实现沉浸式布局的时候,通过getWindow().requestFeature(Window.FEATURE_NO_TITLE);等配置为什么要在setContentView()之前吗?看看installDecor()方法你就知道了~

到这一步,整个界面就加载完成了吗?too naive,这一步只是初始化了View Tree,整个界面还没有显示出来,界面真正要显示出来还要看onResume()这个生命周期里做了啥。继续跟吧~

2.3 handleResumeActivity(IBinder token)

---->类:ActivityThread.java
@Override
    public void handleResumeActivity(IBinder token,..){
         // TODO Push resumeArgs into the activity for consideration
        //1、
        final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
        final Activity a = r.activity;
        
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    //2、
                    wm.addView(decor, l);
                } else {
                ...
            }
        }
            
        if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) { 
                    ...
          if (r.activity.mVisibleFromClient) {
                //3、
                r.activity.makeVisible();
            }
        }
                
    }

整个流程分为三步:

  • 回调ActivityonResume()方法
  • 调用WindowManageraddView()方法
  • 设置Activityvisible

我们接下来要重点看的是addView()这个方法。为什么呢?因为第一步就是回调,第三步是让它可见,那就说明触发界面绘制的流程是在第二步。所以,我们接下来要跟踪第二步。

2.4 wm.addView(decor, l)

---->WindowManagerImpl.java
    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }
----> WindowManagerGlobal.java
    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
                 ....
                 ViewRootImpl root;
                  synchronized (mLock) {
                  root = new ViewRootImpl(view.getContext(), display);
                  ...
                  // do this last because it fires off messages to start doing things
                  //注释大意:最后才用调用这个方法,因为它会发送消息开始干活
                  try {
                root.setView(view, wparams, panelParentView);
            }
            ....
                      
                  }
            }

这个方法就做了两个操作:

  • 创建了ViewRootImpl对象(所以,ViewRootImplWindow的关系是一对一,第二次说明了)
  • 调用ViewRootImpl的setView方法

2.5 viewRootImpl.setView

 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();
          try{
              ...
              res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);
             ...
          }
          ...
        }
            
        }

setContentView这个方法,重点就做了这么几件事:

  • mView赋值
  • 调用requestlayout
  • 调用windowSessionaddToDisplay

第一个分叉路口来了,我们要分开看requestlayout()方法和addToDisplay()方法。

分岔路 2.5.1 requestlayout()

----> ViewRootImpl.java

 @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
        //检查是否是主线程。下次面试官问你ui不能在子线程更新
        //的异常在哪里抛出的,勇敢的告诉面试官,在ViewRootImpl
        //的requestLayout()
            checkThread();
            mLayoutRequested = true;
            //1
            scheduleTraversals();
        }
    }
   //1
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
           ...
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            ....
        }
    }    

在这一步里面会post一个callback,这个callback在下一次Vsync信号到来的时候会被调用(关于下一次Vsync信号来了怎么回调到Java层,请看这篇文章)。

Anyway,当Vsync信号到来后,最终会执行mTraversalRunnablerun方法,我们具体来看一下:

----> ViewRootImpl.java
 final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    
    
void doTraversal() {
...
    performTraversals();
...
} 

private void performTraversals() {
    ...
    relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
    ...
    //绘制三部曲:measure、layout、draw
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
     performLayout(lp, mWidth, mHeight);
    ...
     performDraw();
     ...
}

measurelayoutdraw这几个方法大家都很熟悉了,所以我们重点看relayoutWindow这个方法做了什么事情?为什么它是放在measurelayoutdraw之前的?

private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
            boolean insetsPending) throws RemoteException {
        //看到RemoteException,掐指一算,估摸有ipc通信
            
        ...
        int relayoutResult = mWindowSession.relayout(..., mSurface);  
        ...    
    }

其实这一步的作用就一个————向WMS申请一块Surface

这里不是有一个Surface了吗?不就是那个mSurface??(黑人问号脸❓)

其实,此时这个mSurface是一块空的Surface,它只是被创建了出来,但是还没有分配内存空间,还是空白的。

    public final Surface mSurface = new Surface();

应用端需要调用WMSrelayoutWindowSurface进行赋值,关于Surface的传输和赋值过程我就不分析了,大家可以看文章

Surface赋值初始化成功后,客户端就可以进行绘制了。当绘制完成后,会调用unlockAndPostCanvas()来通知SurfaceFlinger进行合成。

现在,requestLayout()这个方法就算看完了,接下来去另一个分岔路口。

分岔路 2.5.2 windowSession.addToDisplay()

这个WindowSession是一个AIDL,它的作用是用来给应用和WMS通信的,它真正的实现类是Session,具体找寻的流程如下:

 ---> Session.java
@Override
    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,...) {
    //这里要注意这个IWindow,IWindow是WMS向应用端发起Binder调用的
    //Session把它传给WMS是为了以后WMS可以主动向应用端发起调用
        return mService.addWindow(this, window, seq, attrs, ...);
    }

我们可以看到,最终调用的是WMSaddView方法,调用这个方法的作用是向WMS注册窗口对象,然后就会WMS统一去协调这些Window的层级、大小和位置。

WMS来说,它并不关心我们应用本身的Window对象或者View对象,对它来说,它一个重要的功能就是给应用端的Window分配Surface,并且,控制这些Surface的显示顺序,位置和尺寸。

当应用端在Surface绘制完了之后,SurfaceFlinger就会按照这些图像数据,按照WMS提供的尺寸、层级和位置等等进行合成,最后写到屏幕缓冲区里面,绘制出来。

2.6 小结

handleResumeActivity()这个方法整体流程大概可以概括为以上几步。 整个绘制流程比较长,等源码看的差不多了,再考虑写一个系列吧~

三、总结

到这里其实整个流程就算完成了,来总结一下:

  1. 创建了PhoneWindow对象,往PhoneWindow对象里面添加了一个DecorView
  2. DecorView绑定一个ViewRootImpl,由这个ViewRootImpl负责View的绘制和刷新
  3. ViewRootImpl通过IWindowSessionWMS发起Binder调用,而WMS也会通过IWindow向应用端发起调用
  4. ViewRootImpl会在WMS里面注册一个窗口,然后由WMS统一的管理所有窗口的大小,位置和层级
  5. 在第一次绘制时,ViewRootImpl还会向WMS申请一块Surface,有了Surface之后,应用端就可以进行绘制了
  6. 绘制完之后,SurfaceFlinger就会按照WMS里面提供的层级等信息进行合成,最终显示

四、问题解答

  • setContentView的原理是什么?

setContentView()的原理是主要是:(1)创建DecorViewViewRootImpl,并将它们两者进行绑定(2)通过inflate方法创建ViewTree,此时还没有显示

  • ActivityonResume之后才会显示的原因是什么?

在onResume方法中才会调用ViewRootImplperformTraversal()方法进行界面绘制以及makeVisible方法进行界面显示;

  • ViewRoot是干嘛的,是ViewTree的RootView吗?

ViewRoot,或者说它的实现类ViewRootImpl,跟View没有任何关系。ViewRoot只是ViewTree的管理者,而不是ViewTree的根节点。真正的根节点是DecorView

五、参考文档


渣渣程序员一枚,才疏学浅,错漏之处请见谅~