文章目录
子线程能更新 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,也就不会有线程检查了。