View繪製流程源碼解析

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

流程圖如下:

在這裏插入圖片描述

調用棧如下:

在這裏插入圖片描述

上面的只是創建了我們的 PhoneWindowDecorView 並建立加載了我們的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);
	...
}

經過上面的流程,DecorViewViewRootImpl 就建立關聯了。

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)ViewRootImplDecorView 建立關聯

  • ViewRootImplDecorView 關聯前會調用 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,也就不會有線程檢查了。

發佈了199 篇原創文章 · 獲贊 7 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章