文章目錄
1 子線程能更新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,可以更新
2 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怎麼關聯建立起來說起。
2.1 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又是由誰繪製的?接下來繼續分析。
2.2 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
就建立關聯了。
2.3 scheduleTraversals
當 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()
後依次調用 scheduleTraversals()
-> doTraversals()
-> 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的測量、佈局、繪製流程,依次調用 performMeasure()
-> performLayout()
-> performDraw()
。
其中 performMeasure()
在首次創建 ViewRootImpl
的時候被調用了兩次,第一次是爲了確定Window窗口的大小執行,第二次纔是真正的View樹的測量流程。
而 performDraw()
是在第二次執行 scheduleTraversals()
才調用。
總的來說,scheduleTraversals()
會調用兩次,第一次會走兩遍 performMeasure()
、一遍 performLayout()
,第二次才執行 performDraw()
。
2.4 View關聯的整體流程圖和調用棧步驟
整體流程圖:
調用棧如下:
-
ActivityThread
調用performLaunchActivity()
,首先會創建Activity,Activity.attach()
創建出PhoneWindow,performLaunchActivity()
繼續執行Activity.onCreate()
,開始調用Activity.setContentView()
-
Activity.setContentView()
實際上是調用PhoneWindow.setContentView()
,在這裏會創建DecorView
,然後加載我們的xml佈局 -
ActivityThread
調用performResumeActivity()
會調用Activity.onResume()
,performResuleActivity()
繼續執行WindowManager.addView(DecorView)
,實際是調用WindowManagerGlobal.addView(DecorView)
創建ViewRootImpl
, 並且ViewRootImpl.setView(DecorView)
將ViewRootImpl
和DecorView
建立關聯 -
在
ViewRootImpl
和DecorView
關聯前會調用requestLayout()
,執行performTraversals()
開始進行View的繪製流程
3 問題解答
搞清楚了View的整體繪製流程,也要解決在一開始我們提出來的問題。
3.1 爲什麼我們認爲更新UI要在主線程?
ActivityThread
調用 handleResumeActivity()
會觸發Activity調用 onResume()
,此時是處在主線程,而當 WindowManager.addView()
創建 ViewRootImpl
的時候成員變量 mThread
就是主線程了。
3.2 爲什麼在子線程可以更新UI?
我們按一開始的在子線程更新UI的操作一個個的解答。
- TextView的寬高設置爲
wrap_content
,應用啓動時開啓子線程更新TextView,可以更新
解答:
從View的繪製流程我們知道,ViewRootImpl
的創建是在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,也就不會有線程檢查了。