View绘制流程源码解析

子线程能更新 UI 吗

在平常开发中,我们都说 UI 必须要在主线程更新,否则就会抛出异常。那么,真的是这样吗?我们写一个测试用例:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />
</FrameLayout>

class MainActivity : AppCompatActivity() {

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

        thread {
            text_view.text = Thread.currentThread().name
        }
    }
}

运行结果:
Thread-2

TextView 在子线程更新了?不是说 UI 只能在主线程更新吗?我们在来修改上面的例子:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/text_view"
        android:layout_width="100dp" // 修改为具体的数值
        android:layout_height="100dp" // 修改为具体的数值
        android:text="Hello World!" />
</FrameLayout>

class MainActivity : AppCompatActivity() {

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

		// 我们设置点击的时候更新UI
        text_view.setOnClickListener {
            thread {
                text_view.text = Thread.currentThread().name
            }    
        }
    }
}

运行结果:
每次点击TextView都更新了

TextView 还是可以在子线程更新 UI。我们再改一下用例:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/text_view"
        android:layout_width="wrap_content" // 修改回来
        android:layout_height="wrap_content" // 修改回来
        android:text="Hello World!" />
</FrameLayout>

class MainActivity : AppCompatActivity() {

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

        text_view.setOnClickListener {
            thread {
                text_view.text = Thread.currentThread().name
            }
        }
    }
}

运行结果:
点击后崩溃,log提示:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

再改一下:

class MainActivity : AppCompatActivity() {

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

        text_view.setOnClickListener {
        	it.requestLayout() // 添加这一行代码
            thread {
                text_view.text = Thread.currentThread().name
            }
        }
    }
}

运行结果:
每次点击TextView都更新了

我们小结一下刚才的操作:

  • TextView 的宽高设置为 wrap_content,应用启动时开启子线程更新 TextView,可以更新

  • TextView 的宽高设置为具体 dp 值,TextView 点击的时候在子线程更新 TextView,可以更新

  • TextView 的宽高设置为 wrap_content,TextView 点击的时候在子线程更新 TextView,崩溃

  • TextView 的宽高设置为 wrap_content,TextView 点击的时候先调用 view.requestLayout() 后再在子线程更新 TextView,可以更新

View 的绘制流程

我们先看一下子线程更新 UI 会崩溃的 log:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
    at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7547)
    at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1208)
    at android.view.View.requestLayout(View.java:22159)
    at android.view.View.requestLayout(View.java:22159)
    at android.view.View.requestLayout(View.java:22159)
    at android.view.View.requestLayout(View.java:22159)
    at android.view.View.requestLayout(View.java:22159)
    at android.view.View.requestLayout(View.java:22159)
    at android.view.View.requestLayout(View.java:22159)
    at android.widget.TextView.checkForRelayout(TextView.java:8556)
    at android.widget.TextView.setText(TextView.java:5419)
    at android.widget.TextView.setText(TextView.java:5272)
    at android.widget.TextView.setText(TextView.java:5229)
    at com.example.kotlin.MainActivity$onCreate$1$1.invoke(MainActivity.kt:17)
    at com.example.kotlin.MainActivity$onCreate$1$1.invoke(MainActivity.kt:9)
    at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)

根据上面的 log 分析,程序崩溃是因为触发了 ViewRootImpl#checkThread(),进去看下 ViewRootImpl#checkThread()

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

上面的 log 还反映一个流程:View 并不是直接在自己内部去更新的,而是一层层通过 requestLayout() 从顶层 ViewGroup 到 View 更新下来的。

哪个是我们顶层的 View?是 setContentView() 里面的 ViewGroup?

上面两个问题先放着,我们要从 View 怎么关联建立起来说起。

setContentView

我们写的布局要在 Activity 展示,是通过 setContentView() 建立起来的:

Activity.java

public void setContentView(@LayoutRes int layoutResID) {
	// mWindow = getWindow()
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

这里的 getWindow() 在 Android 目前只有一个具体实现类 PhoneWindow

PhoneWindow 是什么时候被创建的?这需要到 ActivityThread 创建 Activity 查看:

ActivityThread.java

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
	...
	Activity activity = null;
	try {
		// 创建 Activity
		activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
	} catch (Exception e) {
		...
	}
	...
	try {
		if (activity != null) {
			...
			activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window, r.configCallback);
		}
	} catch (Exception e) {
		...
	}
	...
}

Activity.java

final void attach(...) {
	...
	// 创建了 PhoneWindow
	mWindow = new PhoneWindow(this, window, activityConfigCallback);
	...
}

所以直接进到 PhoneWindow#setContentView()

PhoneWindow.java

@Override
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        installDecor(); // 初始化 DecorView
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }

	if (...) {
		...
	} else {
		// 最终加载我们的布局
		// mContentParent 是 R.layout.screen_simple.xml 获取的 FrameLayout
		mLayoutInflater.inflate(layoutResID, mContentParent);
	}
	...
}

private void installDecor() {
   mForceDecorInstall = false;
   // 判断到 DecorView==null
   if (mDecor == null) {
       mDecor = generateDecor(-1);
       ...
   } else {
       mDecor.setWindow(this);
   }
   // 上面只是创建了 DecorView,但是我们的布局还没有加载进来
   if (mContentParent == null) {
   	   // 拿到加载我们布局的 FrameLayout
       mContentParent = generateLayout(mDecor);
	   ...	
   }
}

protected DecorView generateDecor(int featureId) {
    ...
    // 创建了一个 DecorView,this 是 Window 对象,Window 和 Decor 关联
    return new DecorView(context, featureId, this, getAttributes());
}

protected ViewGroup generateLayout(DecorView decor) {
	...
	int layoutResource;
	int features = getLocalFeatures();
	// 根据 features 赋值 layoutResource
    if (...) {} else if (...) {}
	...
    } else {
        // Embedded, so no decoration is needed.
        // 加载这个布局
        // <LinearLayout>
		// 	<ViewStub id=action_mode_bar_stub> // actionBar 标题栏
		//  <FrameLayout id=@android:id/content/> // 内容添加我们的xml布局
		//</LinearLayout>
        layoutResource = R.layout.screen_simple; 
        // System.out.println("Simple!");
    }
	
	// 将 R.layout.screen_simple.xml 添加到 DecorView 里面
	mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
	
	// 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;
	 ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
     if (contentParent == null) {
         throw new RuntimeException("Window couldn't find content container view");
     }
     ...
     // 返回填充我们布局的FrameLayout
     return contentParent;
}

总结一下 setContentView() 做了什么事情:

  • 获取到 PhoneWindow,调用 PhoneWindow#setContentView()

  • PhoneWindow#setContentView() 创建 DecorView 和 PhoneWindow 关联

  • 加载顶层布局 R.layout.screen_simple 添加到 DecorView,返回填充我们 xml 布局的 mContentParent,它是一个 FrameLayout

  • LayoutInflater 加载我们的布局到 mContentParent

流程图如下:

在这里插入图片描述

调用栈如下:
在这里插入图片描述
上面的只是创建了我们的 PhoneWindow、DecorView 并建立加载了我们的xml布局,但是分析到这里还不能解决我们的问题,具体的控件测量、布局、绘制流程都还没走。

而且又有一个问题了:既然我们 View 或 ViewGroup 测量布局是一层层往下绘制的,那么 DecorView 作为顶层 View 又是由谁绘制的?接下来继续分析。

ViewRootImpl

DecorView 它也是 View,要测量布局也要有一个 parent,那就是 ViewRootImpl。

DecorView 是什么时候将 ViewRootImpl 设置为它的 parent?ViewRootImpl 又是什么时候被创建的?

还是从刚才的崩溃日志查看:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
    at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7547)
    at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1208)
    at android.view.View.requestLayout(View.java:22159)
    at android.view.View.requestLayout(View.java:22159)
    at android.view.View.requestLayout(View.java:22159)
    at android.view.View.requestLayout(View.java:22159)
    at android.view.View.requestLayout(View.java:22159)
    at android.view.View.requestLayout(View.java:22159)
    at android.view.View.requestLayout(View.java:22159)
    at android.widget.TextView.checkForRelayout(TextView.java:8556)
    at android.widget.TextView.setText(TextView.java:5419)
    at android.widget.TextView.setText(TextView.java:5272)
    at android.widget.TextView.setText(TextView.java:5229)
    at com.example.kotlin.MainActivity$onCreate$1$1.invoke(MainActivity.kt:17)
    at com.example.kotlin.MainActivity$onCreate$1$1.invoke(MainActivity.kt:9)
    at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)

TextView#setText() 调用后会往上调用 parent.requestLayout(),最终到 ViewRootImpl。

看下 ViewRootImpl#requestLayout()

ViewRootImpl.java

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

void checkThread() {
	// 检查 View 是否可以更新并不是检查是否在主线程,而是检查 mThread 是否是当前线程
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

public ViewRootImpl(Context context, Display display) {
	...
	// mThread 是在 ViewRootImpl 被创建的时候初始化的
	mThread = Thread.currentThread();
	...
}

现在需要先分析 ViewRootImpl 是什么时候被创建出来的。这需要退回去 ActivityThread:

ActivityThread.java

public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {
	...
	// Activity 调用 onResume()
	final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
	...
	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;
        ...
        if (a.mVisibleFromClient) {
            if (!a.mWindowAdded) {
                 a.mWindowAdded = true;
                 // 创建 ViewRootImpl,ViewRootImpl 和 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);
             }
         }
	}
}           

WindowManager 哪里获取的?

Activity.java

final void attach(...) {
	...
	mWindow = new PhoneWindow(this, window, activityConfigCallback);
	...
	mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    ...
	mWindowManager = mWindow.getWindowManager();
	...
}

Window.java

public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
	...
	mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}

WindowManagerImpl.java

public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
    return new WindowManagerImpl(mContext, parentWindow);
}

从上面可以看到,WindowManager 接口是 WindowManagerImpl 实现。

WindowManagerImpl.java

private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

@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;
	...
	// ViewRootImpl 被创建
	root = new ViewRootImpl(view.getContext(), display);

	// wpParams 布局参数宽高都是 ViewGroup.MATCH_PARENT
	// 所以 ViewRootImpl 的 DecorView 根布局的宽高会和 Window 的布局参数统一
    view.setLayoutParams(wparams);

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

上面就是 ViewRootImpl 被实例化的过程。

这里可以缕清一个问题:为什么我们认为更新 UI 要在主线程?

答:ActivityThread#handleResumeActivity() 会触发 Activity#onResume(),此时是处在主线程,而当 WindowManager#addView() 创建 ViewRootImpl 的时候成员变量 mThread 就是主线程了。

总结一下 ViewRootImpl 被创建的步骤:

  • ActivityThread#handleResumeActivity() 触发 Activity#onResume(),然后 WindowManager#addView() 添加 DecorView

  • WindowManager 接口实现类是 WindowManagerImpl,调用 WindowManagerImpl#addView(),实际是委托给 WindowManagerGlobal#addView()

  • WindowManagerGlobal#addView() 创建 ViewRootImpl

DecorView 是怎么将 ViewRootImpl 设置为 parent?

WindowManagerGlobal.java

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
	...
	root = new ViewRootImpl(view.getContext(), display);
	...
	try {
		// view 是参数传递过来的 DecorView
		// 在这句代码关联 DecorView 和 ViewRootImpl
		root.setView(view, wparams, panelParentView);
	} catch (RuntimeException e) {
		...
	}
}

ViewRootImpl.java

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
	// 将 DecorView 存储到 ViewRootImpl 的成员变量中
	mView = view; 
	...
	// 第一次创建的时候 ViewRootImpl 开启绘制流程
	requestLayout();
	...
	// ViewRootImpl 设置为 DecorView 的 parent
	view.assignParent(this);
	...
}

经过上面的流程,DecorView 和 ViewRootImpl 就建立关联了。

scheduleTraversals

当 ViewRootImpl 创建出来的时候,会主动调用 ViewRootImpl#requestLayout()

ViewRootImpl.java

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

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        // 主要是这一句代码
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

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

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

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

        performTraversals();

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

ViewRootImpl#requestLayout() 后依次调用 ViewRootImpl#scheduleTraversals() -> ViewRootImpl#doTraversals() -> ViewRootImpl#performTraversals()。继续往下看:

private void performTraversals() {
	final View host = mView;
	...
	// Window 的宽高大小
	WindowManager.LayoutParams lp = mWindowAttributes;

	int desiredWindowWidth;
	int desiredWindowHeight;
	...
	Rect frame = mWinFrame;
	if (mFirst) {
		...
		if (shouldUseDisplaySize(lp)) {
			Point size = new Point();
			mDisplay.getRealSize(size);
			desiredWindowWidth = size.x;
			desiredWindowHeight = size.y;
		} else {
			// 根据设备宽高获取大小
			desiredWindowWidth = mWinFrame.width();
			desiredWindowHeight = mWinFrame.height();
		}

		// 一个 ViewRootImpl 对应一个 mAttachInfo
		// mAttachInfo 也是在 ViewRootImpl 创建的时候实例化的
        mAttachInfo.mUse32BitDrawingCache = true;
        mAttachInfo.mHasWindowFocus = false;
        mAttachInfo.mWindowVisibility = viewVisibility;
        mAttachInfo.mRecomputeGlobalAttributes = false;
        mLastConfigurationFromResources.setTo(config);
        mLastSystemUiVisibility = mAttachInfo.mSystemUiVisibility;
        // Set the layout direction if it has not been set before (inherit is the default)
        if (mViewLayoutDirectionInitial == View.LAYOUT_DIRECTION_INHERIT) {
            host.setLayoutDirection(config.getLayoutDirection());
        }
        host.dispatchAttachedToWindow(mAttachInfo, 0);
        // View.getViewTreeObserver().addOnWindowAttachListener()
        mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true); 
        // host 是 DecorView,这个方法给 DecorView 添加 statusBar 和 navigationBar
        dispatchApplyInsets(host); 
	}
	...
	// 执行 view.post(Runnable) 存储到队列的 Runnable
	// view.post() 的时候可能 mAttachInfo.mHandler 还没有被创建
	// 因为 ViewRootImpl 是在 Activity.onResume() 时才创建的
	// 而 view.post() 可能会在 Activity.onCreate() 执行,会将 Runnable 暂存队列
	getRunQueue().executeActions(mAttachInfo.mHandler);
	
	boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
	if (layoutRequested) {
		...
		// 开始测量流程
		// 需要注意的是,这一次测量是为了确定 Window 窗口的尺寸
		// 后面还会有一次测量
		windowSizeMayChange |= measureHierarchy(host, lp, res,
			desiredWindowWidth, desiredWindowHeight);
	}
	...
	// 重置标志位避免重绘
	if (layoutRequested) {
		mLayoutRequested = false;
	}
	...
	// mFirst 还是 true
	if (mFirst || windowShouldResize || insetsChanged ||
			viewVisibitilyChanged || params != null || mForceNextWindowRelayout) {
		...
		// 通知 WindowManager 系统服务大小宽高发生更改
		relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
		...
	}
	...
	if (!mStopped || mReportNextDraw) {
		boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
			(relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0
		);
		if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
				|| mHeight != host.getMeasuredHeight() || contentInsetsChanged || updatedConfiguration) {
			int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
            int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
            // 重新再测量一遍,这里才是真正测量 View 的大小
            // 开始测量流程
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            ...
		}
	}
	...
	final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
	if (didLayout) {
		// 开始布局流程
		performLayout(lp, mWidth, mHeight);
		...
	}
	...
	if (triggerGlobalLayoutListener) {
		mAttachInfo.mRecomputeGlobalAttributes = false;
		// 上面已经完成了布局流程,ViewTreeObserver 就可以回调到测量大小
		// View.getViewTreeObserver().addOnGlobalLayoutListener()
		mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
	}
	...
	boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
	if (!cancelDraw && !newSurface) {
		...
		// 开始绘制流程
		performDraw();
	} else {
		if (isViewVisible) {
			// Try again
			// 这里要注意的是,重新调用总调度 View 绘制流程方法不会去测量和布局
			// 因为标志位 mLayoutRequested 已经更改,会直接进入绘制流程		
			scheduleTraversals();
		}
		...
	}
	...
}

privaete boolean measureHierarchy(...) {
	int childWidthMeasureSpec;
	int childHeightMeasureSpec;
	
	boolean goodMeasure = false;
	...
	if (!goodMeasure) {
		// 这里的 MeasureSpec 肯定是具体的值,因为 desiredWindowXXX 是设备宽高
		childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
		childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
		// 开启绘制
		performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
		...
	}
}

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
	if (mView == null) {
		reutrn;
	}
	Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
	try {
		// DecorView.measure()
		mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
	} finally {
		Trace.traceEnd(Trace.TRACE_TAG_VIEW);
	}
}

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
	mLayoutRequested = false; // 重置标志位
	...
	final View host = mView;
	...
	// DecorView.layout()
	host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
	...
}            

可以分析到,在 ViewRootImpl#scheduleTraversals() 依次完成了 View 的测量、布局、绘制流程,依次调用 ViewRootImpl#performMeasure() -> ViewRootImpl#performLayout() -> ViewRootImpl#performDraw()

其中 ViewRootImpl#performMeasure() 在首次创建 ViewRootImpl 的时候被调用了两次,第一次是为了确定 Window 窗口的大小执行,第二次才是真正的 View 树的测量流程。

ViewRootImpl#performDraw() 是在第二次执行 ViewRootImpl#scheduleTraversals() 才调用。

总的来说,ViewRootImpl#scheduleTraversals() 会调用两次,第一次会走两遍 ViewRootImpl#performMeasure()、一遍 ViewRootImpl#performLayout(),第二次才执行 ViewRootImpl#performDraw()

View 关联的整体流程图和调用栈步骤

整体流程图:

在这里插入图片描述

调用栈如下:

在这里插入图片描述

  • ActivityThread#performLaunchActivity(),首先会创建 Activity,Activity#attach() 创建出 PhoneWindow,ActivityThread#performLaunchActivity() 继续执行 Activity#onCreate(),开始调用 Activity#setContentView()

  • Activity#setContentView() 实际上是调用 PhoneWindow#setContentView(),在这里会创建 DecorView,然后加载我们的xml布局

  • ActivityThread#performResumeActivity() 会调用 Activity#onResume()ActivityThread#performResuleActivity() 继续执行 WindowManager#addView(DecorView),实际是调用 WindowManagerGlobal#addView(DecorView) 创建 ViewRootImpl, 并且 ViewRootImpl#setView(DecorView) 将 ViewRootImpl 和 DecorView 建立关联

  • 在 ViewRootImpl 和 DecorView 关联前会调用 ViewRootImpl#requestLayout(),执行 ViewRootImpl#performTraversals() 开始进行 View 的绘制流程

问题解答

搞清楚了 View 的整体绘制流程,也要解决在一开始我们提出来的问题。

为什么我们认为更新 UI 要在主线程?

ActivityThread#handleResumeActivity() 会触发 Activity#onResume(),此时是处在主线程,而当 WindowManager#addView() 创建 ViewRootImpl 的时候成员变量 mThread 就是主线程了。

为什么在子线程可以更新 UI?

我们按一开始的在子线程更新 UI 的操作一个个的解答。

  • TextView 的宽高设置为 wrap_content,应用启动时开启子线程更新 TextView,可以更新

解答:

从 View 的绘制流程我们知道,ViewRootImpl 的创建是在 Activity 执行了 Activity#onResume() 之后,ViewRootImpl 在创建的时候会主动调用 ViewRootImpl#requestLayout(),在这个方法会调用 ViewRootImpl#checkThread() 检查是否处在当前线程。因为 ViewRootImpl 都还没创建,所以不会触发线程检查。

  • TextView 的宽高设置为具体 dp 值,TextView 点击的时候在子线程更新 TextView,可以更新

解答:

我们可以先看下 TextView#setText() 的源码:

TextView.java

public void setText(...) {
	...
	if (mLayout != null) {
		checkForRelayout();
	}
	...
}

private void checkForRelayout() {
	// 首先检查一下 TextView 的宽是否为固定 dp 或者 match_parent,不是则往下走
    if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
            || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
            && (mHint == null || mHintLayout != null)
            && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
		...
        if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
        	// 再检查一下如果高度是固定 dp,那么就只会调用 invalidate()
            if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
                    && mLayoutParams.height != LayoutParams.MATCH_PARENT) {
                autoSizeText();
                invalidate();
                return;
            }
            
            // 如果检查到 TextView 的宽高并没有改变,同样的也调用 invalidate()
            if (mLayout.getHeight() == oldht
                    && (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
                autoSizeText();
                invalidate();
                return;
            }
        }
	
		...
    } else {
       ...
    }
}

当我们设置 TextView 的宽高为具体 dp 时,更新 UI 只会触发到 invalidate()。这个方法具体在 ViewGroup 执行:

public final void invalidateChild(View child, final Rect dirty) {
	final AttachInfo attachInfo = mAttachInfo;
	// 检查到开启了硬件加速,使用硬件加速更新UI
	if (attachInfo != null && attachInfo.mHardwareAccelerated) {
		onDescendantInvalidated(child, child);
		return;
	}
	...
}

public void onDescendantInvalidated(@NonNull View child, @NonNull View target) {
	...
	// 会不断往上传递到 ViewRootImpl
	if (mParent != null) {
		mParent.onDescendantInvalidated(this, target);
	}
}

ViewRootImpl.java

@Override
public void onDescendantInvalidated(@NonNull View child, @NonNull View descendant) {
    if ((descendant.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {
        mIsAnimating = true;
    }
    invalidate();
}

void invalidate() {
    mDirty.set(0, 0, mWidth, mHeight);
    if (!mWillDrawSoon) {
    	// 在这里触发了 View 绘制流程
        scheduleTraversals();
    }
}

这次操作能在子线程更新 UI,主要的原因是开启了硬件加速,最终触发到 ViewRootImpl#scheduleTraversals() 执行 View 绘制流程,跳过了 ViewRootImpl#checkThread() 检查线程。

所以,如果我们关闭硬件加速,那么也将会崩溃。

  • TextView 的宽高设置为 wrap_content,TextView 点击的时候在子线程更新 TextView,崩溃

解答:

可以肯定 ViewRootImpl 已经成功创建出来,所以当点击 TextView 尝试在子线程更新 UI 时,TextView#setText() 会调用 parent.requestLayout() 不断往上调用直到 ViewRootImpl#requestLayout(),因为 ViewRootImpl.mThread 是在主线程,而 Thread.currentThread() 是在子线程,所以会抛出异常崩溃

  • TextView 的宽高设置为 wrap_content,TextView 点击的时候先调用 view.requestLayout() 后再在子线程更新 TextView,可以更新

解答:

或许你的想法是这样的:

同样的会触发 parent.requestLayout() 往上调用直到 ViewRootImpl#requestLayout(),我们可以看下 ViewRootImpl 的这个方法:

ViewRootImpl.java

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

有一个标志位 mHandlingLayoutInLayoutRequest,我们每次点击主动触发一次 ViewRootImpl#requestLayout() 改变了标志位的值,所以在子线程调用 TextView#setText() 触发 parent.requestLayout() 时,ViewRootImpl.requestLayout() 标志位判断为 mHandlingLayoutInLayoutRequest=true,跳过了检查。

但实际上我们每次点击 TextView 并没有执行到 ViewRootImpl#requestLayout(),因为 TextView#requestLayout() 做了优化:

public void requestLayout() {
	...
	mPrivateFlags |= PFLAG_FORCE_LAYOUT;
	mPrivateFlags |= PFLAG_INVALIDATED;

	// mPrivateFlags 修改了标志位,mParent.isLayoutRequested()=true
	// PFALG_FORCE_LAYOUT 会在 layout() 阶段才清除
	if (mParent != null && !mParent.isLayoutRequested()) {
		mParent.requestLayout();
	}
}

public boolean isLayoutRequested() {
	return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
}

因为 TextView#requestLayout() 时修改了标志位 mPrivateFlags,所以根本没有调用到 mParent.requestLayout() 将 View 的绘制往上传递到 ViewRootImpl,也就不会有线程检查了。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值