UI的绘制流程以及原理

  • 作者:鱼八(网易1902期)
  • 公众号:鱼八说
  • 日期:2019-06-23
  • 类型:Android
  • 说明:本文源于 鱼八 原创,如需转载请带上链接或注明出处!

一、View是如何被添加到屏幕窗口上的?

总体上来说:

  • 1.创建顶层的布局容器DecorView。
  • 2.在顶层布局中加载基础布局的ViewGroup。
  • 3.将ContentView添加到基础布局中的FrameLayout中

源码分析:

开发中我们都知道,setContentView方法是将我们编写的xml界面加载到我们的手机屏幕窗口上的。于是,我们就从这里进入一个新的世界。

第一步 找到setContentView的实现

我们从Activity(该Activity继承Activity),进入到setContentView()方法中

/**
* Set the activity content from a layout resource. The resource will be
* inflated, adding all top-level views to the activity.
*
* @param layoutResID Resource ID to be inflated.
*
* @see #setContentView(android.view.View)
* @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
*/
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}

我们可以看到这里getWindow()调用了setContentView方法,那么这个getWindow()什么呢?
我们进入到getWindow()中,可以看到返回的是一个Window对象。

/**
* Retrieve the current {@link android.view.Window} for the activity.
* This can be used to directly access parts of the Window API that
* are not available through Activity/Screen.
*
* @return Window The current window, or null if the activity is not
* visual.
*/
public Window getWindow() {
return mWindow;
}

我们再进入到Window的源码中查看。

/**
* Abstract base class for a top-level window look and behavior policy. An
* instance of this class should be used as the top-level view added to the
* window manager. It provides standard UI policies such as a background, title
* area, default key processing, etc.
*
* <p>The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
*/
public abstract class Window {...}

可以看到Window只是一个抽象类,从注释中了解到,PhoneWindow是他的唯一实现类。我们再进入到PhoneWindow中查看setContentView具体的实现。到这里我们就知道了setContentView的具体实现,实际上是Window类的实现类PhoneWindow所实现的。

我们进入到 setContentView方法中,浏览一遍发现,这个方法主要的过程在 installDecor()方法和mLayoutInflater.inflate(layoutResID, mContentParent);。接下来我们主要研究installDecor() 方法。直接理解它是加载Decor的一个方法。通过它,我们可以去了解加载Decor的整个过程。

@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
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 {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
...
}

第二步 了解installDecor()的加载过程

进入installDecor() 方法中

private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
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) {
mContentParent = generateLayout(mDecor);

// Set up decor part of UI to ignore fitsSystemWindows if appropriate.
mDecor.makeOptionalFitsSystemWindows();
...
}
...

通过阅读源码,在PhoneWindow的installDecor() 方法中,我们可以知道这个方法主要做了以下:

  • 1 .generateDecor() 获取到DecorView
  • 2 .generateLayout(mDecor) 将layoutResources(基础布局)加载到DecorView

详细流程如下:

1. 通过generateDecor() 方法获取到DecorView(顶层布局)

进入到generateDecor() 方法返回的是一个DecorView的对象,
进入到这个方法中我们可以看到

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

我们在去看看DecorView到底是什么?

/** @hide */
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks
{...}

通过源码可以看到,它实际上是一个继承自FrameLayout 的一个容器
这就是说明generateDecor() 方法实际上是让我们获取到了一个DecorView的容器。我们接着继续往下看。

2.generateLayout(mDecor)方法将layoutResources(基础布局)加载到DecorView

我们在得到DecorView容器以后,在进入到generateLayout()方法 中,

protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme. 设置系统主题的不同样式和特性
TypedArray a = getWindowStyle();
...
if (mIsFloating) {
...
setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
}
...
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
...
requestFeature(FEATURE_NO_TITLE);
}
...
// Inflate the window decor. 解析decor
int layoutResource;
int features = getLocalFeatures();
...
if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;
} else {
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;
// System.out.println("Simple!");
}

...
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
...
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
...
return contentParent;
}

通过阅读源码 可以看到它主要是做了这三件事。

  • 1 . 通过requestFeature()setFlags()等方法设置系统主题的不同样式和特性。
  • 2 . 解析窗口view
    • 1 . 根据具体的fetures的不同获取到系统相应的 layoutResources。
    • 2 . 获取完layoutResource 后,通过mDecor.onResourcesLoaded()方法去解析这个 layoutResources 。将解析获得的view加载到DecorView上。
  • 3 . 根据layoutResources (R.layout.screen_simple)上的布局ID:ID_ANDROID_CONTENT 获取到相应的 contentParent(实际上是一个R.layout.screen_simple 上的一个FrameLayout布局)并返回。

第一件事

这里是对系统主题特性的一些设置我们这里不做研究了。

第二件事

  • 1 . 先通过fetures获取相应的 layoutResources
    这里我们假定根据某些特性条件下获取到的是 `layoutResource = R.layout.screen_simple;`          
  • 2 . 执行onResourcesLoaded()方法,解析 layoutResources,加载到 DecorView上
    > 先查看源码 `onResourcesLoaded()源码`
    void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
    ...
    final View root = inflater.inflate(layoutResource, null);
    ...
    // Put it below the color views.
    addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }
    }

在onResourcesLoaded 方法中,他通过解析 layoutResource 创建了一个root对象。 该root对象例如为: R.layout.screen_simple(系统的xml)。通过addView方法将 root 对象加载到DecorView上。

第三件事

在onResourcesLoaded方法执行完毕后,回到 generateLayout()方法中接下来我们会获取到ViewGroup的对象 contentParent。那么这个contentParent是什么?

ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}

首先通过查看ID_ANDROID_CONTENT 的注释, 我们可以看到这是系统主布局(我们这里假定获取的是R.layout.screen_simple)一定所具有的ID 。com.android.internal.R.id.content

/**
* The ID that the main layout in the XML layout file should have.
*/
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

我们查看screen_simple.xml,发现获取到这个contentParent ,实际上是 screen_simple.xml 上的FrameLayout 。

	<!--screen_simple.xml-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

获取contentParent 并返回后,generateLayout()方法也就执行完毕。我们回到installDecor()方法中。
就此installDecor()的主要方法执行完毕。 以上我们可以理解是在这个过程是获取到DecorView,然后将根据不同特性获取到的系统的layoutResources加载到DecorView上。最后我们再根据ID_ANDROID_CONTENT 从layoutResources获取到contentParent 。它实际上是一个FrameLayout。
View的加载显示如下:

第三步 加载setContentView中设置的布局

1.将setContentView中设置的布局加载到contentParent上

installDecor()方法执行完毕后,返回到setContentView中,查看源码

@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
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 {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
...
}

最后通过:
mLayoutInflater.inflate(layoutResID, mContentParent);
将在activity上设置的布局 layoutResID 加载到 contentParent上 也就是 FrameLayout上。至此,我们的整个View就加载并显示到屏幕上了。

总结

基本三个步骤

  • 1.创建顶层的布局容器DecorView。
  • 2.在顶层布局中加载基础布局的ViewGroup
  • 3.将ContentView添加到基础布局中的FrameLayout中

view是如何被添加到屏幕窗口上的?
1.首先系统会创建一个顶层布局容器DecorView。DecorView 是一个ViewGroup容器, 继承FrameLayout,是PhoneWindow对象持有的一个实例,他是所有应用程序的顶层View,在系统内部进行初始化。当DecorView 初始化完成后,系统会根据应用程序的顶层特性会加载一个基础容器,例如no_actionBar等,不同的主题加载的基础容器也不一样,但是无论什么样的的基础容器一定会有一个 android.R.di.content 的FrameLayout。开发者通过setContView设置的xml ,通过解析后就加载到了这个FrameLayout中。

二、View的绘制流程

总体上来说

  • 1 . 绘制入口
  • 2 . 绘制的类及方法
  • 3 . 绘制的三大步骤

源码分析:

第一步 绘制入口

1.找到 wm.addView

绘制的入口是在ActivityThread在这个类中找到handleMessage()方法。这个方法是主线程里面有一个处理主线程消息的对象,叫 H 。是 handler的子类。

public void handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY: {
...
handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
...
} break;
}
...

我们看到第一个case 是 LAUNCH_ACTIVITY 表示的是启动一个activity,其中有一个handleLaunchActivity()方法,点进去

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
...
Activity a = performLaunchActivity(r, customIntent);

if (a != null) {
handleResumeActivity(r.token, false, r.isForward,
!r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);
...
}

可以看到 performLaunchActivity(),这个方法主要是启动一个Activity。返回到handleLaunchActivity()方法中接着往下看,我们可以看到 handleResumeActivity()

final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
ActivityClientRecord r = mActivities.get(token);
...
// TODO Push resumeArgs into the activity for consideration
r = performResumeActivity(token, clearHide, reason);
...
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;
wm.addView(decor, l);
}
...
}
}
...

我们点进去,会发现一个performResumeActivity()方法,这个方法被调用时,就会回调Activity生命周期中的OnResume()方法。
接着往下翻 我们看到这样一个判断。

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;
wm.addView(decor, l);
}
...
}
}

通过对源码的阅读,我们发现这个方法 在判空和其他条件满足下,通过方法WindowManger.LayoutsParams l = r.window.getAttributes();获取到窗口的布局属性对象,然后执行了 wm.addView(),到这我们也就找到了入口。

2.wm对象是什么?

我们先看一下 wm 是什么,通过阅读它是一个 ViewManger对象,而ViewManger是一个接口

public interface ViewManager
{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}

那么接下来去找它的实现类,我们点击getWindowManager()

/** Retrieve the window manager for showing custom windows. */
public WindowManager getWindowManager() {
return mWindowManager;
}

发现她返回的是一个WindowManager对象。我们搜索 mWindowManager

mWindowManager = mWindow.getWindowManager();

发现mWindowManager是通过mWindow.getWindowManager() 获取的。从前面我们知道mWindow的唯一实现类是PhoneWindow。所以我们在PhoneWindow中去找getWindowManager()方法。

final WindowManager wm = getWindowManager();
if (wm == null) {
return;
}

找到后我们点击getWindowManager() 方法

/**
* Return the window manager allowing this Window to display its own
* windows.
*
* @return WindowManager The ViewManager.
*/
public WindowManager getWindowManager() {
return mWindowManager;
}

发现它又返回的是一个WindowManager对象,然后我们去搜索 mWindowManager 这个对象。

/**
* Set the window manager for use by this Window to, for example,
* display panels. This is <em>not</em> used for displaying the
* Window itself -- that must be done by the client.
*
* @param wm The window manager for adding new windows.
*/
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
...
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}

我们看到 mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);所以我们得出结果。
wm对象是什么? 她是通过createLocalWindowManager()方法进行进行实例化返回的一个WindowManagerImpl类型的对象。

3.绘制的入口addView方法

现在我们知道了wm 是什么? 我们可以理解为 他是通过createLocalWindowManager()进行实例化的一个对象。createLocalWindowManager()返回的对象是WindowManagerImpl类型的。
在之前我们看到wm调用了addView()方法,现在我们进入到WindowManagerImpl中。去寻找wm调用的addView()方法。

public final class WindowManagerImpl implements WindowManager {
...
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
}
...

在这个方法中 我们看到了
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
mGlobal 调用了addView()方法,点击mGlobal这个对象。

private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

发现mGlobal 是 WindowManagerGlobal 的一个对象,我们再进入到WindowManagerGlobal 中去寻找addView()方法

第二步 绘制的类以及方法

1.了解setView方法

接着我们去WindowManagerGlobal中去搜索查看addView()方法。
addView()方法中 我们看到这里声明了一个ViewRootImpl

public final class WindowManagerGlobal {		...
private final ArrayList<View> mViews = new ArrayList<View>();
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
private final ArrayList<WindowManager.LayoutParams> mParams =
new ArrayList<WindowManager.LayoutParams>();
...

public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
ViewRootImpl root;
View panelParentView = null;
...
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);
...
}

往下看,我们可以看到通过ViewRootImpl的构造方法,将root 进行了实例化。
root = new ViewRootImpl(view.getContext(), display);
接着给View 设置了LayoutParams,然后将view ,root 等 添加到了对应的集合中
最后通过 root.setView()将view,wparams,panelParentView 进行关联起来。
root.setView(view, wparams, panelParentView);
进入到setView()中我们主要关心requestLayout()方法

 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
...
requestLayout();
...
}
2.requestLayout()后的流程

点击进入到requestLayout()方法中

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

我们看到两个方法。

  • 1 .checkThread()方法主要是检测当前线程是否为主线程。
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
  • 2 .scheduleTraversals() 点击进入到该方法中
void scheduleTraversals() {
if (!mTraversalScheduled) {
...
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...
}
}

看到mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); 方法,可以看到执行了mTraversalRunnable,进入到这个runable中

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

这个runable方法里只是执行了一个 doTraversal()

void doTraversal() {
...
performTraversals();
...
}

在这个方法中 我们看到他只是执行了一个performTraversals();。

第三步 performTraversals()中的View绘制三大流程

我们进入到performTraversals()中,可以发现View的绘制三大流程

  • 1 . performMeasure()
  • 2 . performLayout()
  • 3 . performDraw()
private void performTraversals() 
{
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, mWidth, mHeight);
...
performDraw();
}

至此我们找到了类的绘制入口以及绘制的类和方法。

总结

基本三个步骤以及涉及的类和方法

    1. 绘制入口
      ActivityThread.handleResumeActivity()
      —> WindowManagerImpl.addView(decorView,layoutParams)
      —> WindowManagerGlobal.addView()
    1. 绘制的类及方法
      ViewRootlmpl.setView(decorView,layoutParams,parentView)
      —> ViewRootlmpl.requestLayout()
      —> scheduleTraversals()
      —> doTraversals()
      —> performTraversals()
    1. 绘制的三大步骤
      —> ViewRootlmpl.performMesure()
      —> ViewRootlmpl.performLayout()
      —> ViewRootlmpl.performDraw()

View的绘制流程?
1.当Activity创建之后,在ActivityThread.handleResumeActivity()中 会通过 wm.addView方法,这个wm的实现类是WindowManagerImpl,他调用的就是WindowManagerImpl.addView方法,第一个参数是顶层布局,第二个参数是布局属性。在这个addView中会调用WindowManagerGlobal.addView()方法,在其中会创建出ViewRootlmpl的对象root,并且调用了setView方法,将decorView,布局属性做一个关联。关联成功后,开始绘制。在setView中调用了 requestLayout()方法,然后调用了 scheduleTraversals(),doTraversal(),最后调用了performTraversals,在这个方法中调用了View的三大绘制流程测量performMesure(),布局performLayout(),绘制performDraw()。