前言
不知道大家有沒有想過一個問題谐区,當(dāng)啟動一個Activity的時候湖蜕,相應(yīng)的XML布局文件中的View是如何顯示到屏幕上的?有些同學(xué)會說是通過onMeasure()
宋列、onLayout()
昭抒、onDraw()
這3個方法來完成的,實際上這只是系統(tǒng)暴露給我們使用的最基本的方法,背后的流程要比這個更加復(fù)雜灭返,今天就和大家一起扒一下背后還做了什么事情盗迟。
我們知道Activity執(zhí)行了onCreate()
、onStart()
熙含、 onResume()
3個方法后罚缕,用戶就能看見視圖了。這背后實際上經(jīng)歷了2個過程怎静,其一邮弹,通過ActivityThread
調(diào)度生命周期相關(guān)的方法;其二蚓聘,通過setContentView()
把XML解析成View對象腌乡。這里兵分2路,先來看一下setContentView()
或粮。
setContentView()方法
這里借用這篇文章的圖來表示setContentView
整個流程
https://blog.csdn.net/Rayht/article/details/80782697
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
從這里進去會進去到Activity.setContentView()
导饲,最后會調(diào)用PhoneWindow.setContentView()
##PhoneWindow
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
//1.初始化DecorView
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
//判斷有沒有轉(zhuǎn)場動畫
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//2.解析傳進來的xml布局捞高,生成一個ContentView
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
這里分為2條線氯材,我們先來看一下初始化mDecor。
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
//1.生成DecorView
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
//2.生成mContentParent
mContentParent = generateLayout(mDecor);
}
.......
}
protected DecorView generateDecor(int featureId) {
Context context;
......
//DecorView extends FrameLayout
return new DecorView(context, featureId, this, getAttributes());
}
mDecor就是DecorView對象硝岗,它是PhoneWindow的頂層View氢哮,查看DecorView源碼可以看出DecorView 是一個FrameLayout,但是源碼中并沒有展示出它是一個怎樣的布局型檀,因為它是在注釋2mContentParent = generateLayout(mDecor)
中添加的冗尤,下面就來一起看一下。
##PhoneWindow. generateLayout()
protected ViewGroup generateLayout(DecorView decor) {
// Inflate the window decor.
int layoutResource; //是一個布局文件id
int features = getLocalFeatures();
// 根據(jù)不同的主題將對應(yīng)的布局文件id賦值給layoutResource
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
setCloseOnSwipeEnabled(true);
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
....
}
....
else {
layoutResource = R.layout.screen_simple;
}
mDecor.startChanging();
//通過LayoutInflater加載解析layoutResource布局文件
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
.......
}
這里展示的是generateLayout
的上半部分胀溺,先根據(jù)不同的主題生成不同的布局文件裂七,然后解析layoutResource布局文件。
onResourcesLoaded()
##DecorView. onResourcesLoaded()
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
.....
mDecorCaptionView = createDecorCaptionView(inflater);
//1.通過XmlResourceParser解析XML布局文件仓坞,得到一個View對象
//這里的root就是DecorView中添加的layoutResource
final View root = inflater.inflate(layoutResource, null);
//2.把layoutResource布局文件添加到DecorView中
if (mDecorCaptionView != null) {
if (mDecorCaptionView.getParent() == null) {
addView(mDecorCaptionView,
new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mDecorCaptionView.addView(root,
new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
} else {
// Put it below the color views.
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mContentRoot = (ViewGroup) root;
initializeElevation();
}
現(xiàn)在我們知道了背零,generateLayout
其實是給DecorView添加一個布局文件,下面就來看一下DecorView到底是怎樣的布局无埃?
上面說過會根據(jù)不同的主題選擇不同的layoutResource
徙瓶,這里我們看一下最常用的layoutResource = R.layout.screen_simple
。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<!-- ActionBar -->
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
可以看出這是一個垂直的LinearLayout嫉称,上面是一個ActionBar侦镇,下面是一個id為content
的FrameLayout,先給大家透漏一下织阅,這個FrameLayout就是用于裝載平時我們寫的Activity中的xml布局壳繁。
generateLayout
下半部分
##PhoneWindow. generateLayout()
protected ViewGroup generateLayout(DecorView decor) {
int layoutResource; //是一個布局文件id
......
//往DecorView中添加layoutResource布局文件
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//查找DecorView中id為ID_ANDROID_CONTENT的View
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
.......
return contentParent;
}
這里就更簡單了,通過findViewById
找到之前布局中id為content
的view,最后返回到這個contentParent
就是剛才的FrameLayout闹炉。
我們再回到最開始的setContentView()
方法
##PhoneWindow
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
//1.初始化DecorView
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
//判斷有沒有轉(zhuǎn)場動畫
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//2.解析傳進來的xml布局伍派,生成一個ContentView
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
上面我們講了installDecor()
實際上就是創(chuàng)建并初始化DecorView對象,也就是完成了mContentParent
的初始化剩胁,接下來看一下注釋2诉植,這里是解析Activity中傳進來的布局layoutResID
,其中parent是mContentParent
昵观,也就是之前說過的那個FrameLayout晾腔,這樣就把Activity中的布局添加到DecorView中了。
小結(jié)一下:
setContentView()流程如下:
1啊犬、在PhoneWindow中創(chuàng)建頂層的DecorView灼擂;
2、在DecorView中會根據(jù)主題的不同加載一個不同的布局觉至;
3剔应、把Activity中的布局解析并添加到DecorView的FrameLayout中
這是我畫的一幅圖,按照這個去看源碼會比較清晰语御。
到這里算是把setContentView流程分析完了峻贮,但把我們僅僅是把自己的Layout添加到DecorView中,但如何顯示到屏幕上還沒看呢应闯。
布局文件中的UI是如何顯示的呢纤控?
其實在文章開頭說過了,在ActivityThread的生命周期調(diào)度中完成的碉纺。剛才已經(jīng)看過了onCreate()船万,后面還會調(diào)用onStart()、onResume()骨田,而最終UI的顯示就是在onResume()中完成的耿导,也就是說我們平常接觸最多的measure
、layout
态贤、draw
3個方法都在onResume中完成的舱呻。而onResume()是在ActivityThread的handleResumeActivity()
中調(diào)用的。
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
unscheduleGcIdler();
mSomeActivitiesChanged = true;
// 1.會調(diào)用Activity的onResume
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
final Activity a = r.activity;
final int forwardBit = isForward
? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;
// If the window hasn't yet been added to the window manager,
// and this guy didn't finish itself or start another activity,
// then go ahead and add the window.
boolean willBeVisible = !a.mStartedActivity;
if (!willBeVisible) {
try {
willBeVisible = ActivityTaskManager.getService().willActivityBeVisible(
a.getActivityToken());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
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;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
// Normally the ViewRoot sets up callbacks with the Activity
// in addView->ViewRootImpl#setView. If we are instead reusing
// the decor view we have to notify the view root that the
// callbacks may have changed.
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
//2.WindowManager添加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);
}
}
......
}
這里最關(guān)鍵的地方是注釋1和注釋2抵卫,這里先看一下注釋2狮荔,wm是一個接口ViewManager
對象,而wm是通過Activity的getWindowManager()
獲取的介粘,最后你會發(fā)現(xiàn)wm是在WindowManagerImpl
中初始化的殖氏,
##WindowManagerImpl
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
##WindowManagerGlobal
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
....
ViewRootImpl root;
View panelParentView = null;
//創(chuàng)建ViewRootImpl對象,挺重要的一個類姻采,后面會解釋
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
try {
//1.關(guān)鍵調(diào)用
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
這里最關(guān)鍵的是調(diào)用了setView()
方法雅采,這個方法內(nèi)又調(diào)用了requestLayout()
##ViewRootImpl
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
//檢查是否在UI線程
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//callback與Choreographer交互,會在下一幀被渲染時觸發(fā)
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
這里的 mChoreographer
實際上是Choreographer對象,Choreographer是在屏幕刷新機制中接收顯示系統(tǒng)的VSync信號婚瓜,postFrameCallback設(shè)置自己的callback與Choreographer交互宝鼓,你設(shè)置的callCack會在下一個frame被渲染時觸發(fā)。這里簡單了解下即可巴刻。這里重點關(guān)注下mTraversalRunnable
對象愚铡。
##ViewRootImpl
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
traversal是遍歷的意思,也就是說后面會做遍歷操作胡陪,至于為什么沥寥,接著往下看。
##ViewRootImpl
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
//關(guān)鍵代碼
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
performTraversals()
這段代碼非常長柠座,但是核心的地方就是下面注釋的3處邑雅。
##ViewRootImpl
private void performTraversals() {
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
if (mFirst) {
.....
// host為DecorView
// 調(diào)用DecorVIew 的 dispatchAttachedToWindow,并且把 mAttachInfo 給子view 妈经,view.post就是利用了這個原理
host.dispatchAttachedToWindow(mAttachInfo, 0);
.....
}
// Ask host how big it wants to be
//1.執(zhí)行測量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
......
//2.執(zhí)行布局
performLayout(lp, mWidth, mHeight);
......
//3.執(zhí)行繪制
performDraw();
}
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
//調(diào)用View的measure()
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mLayoutRequested = false;
mScrollMayChange = true;
mInLayout = true;
final View host = mView;
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
//調(diào)用View的layout()
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
}
private void performDraw() {
......
draw(fullRedrawNeeded);
......
}
public void draw(Canvas canvas) {
......
//調(diào)用View的onDraw()
onDraw(canvas);
......
}
performMeasure()
:從根節(jié)點向下遍歷View樹淮野,完成所有ViewGroup和View的測量工作,計算出所有ViewGroup和View顯示出來需要的高度和寬度吹泡;performLayout()
:從根節(jié)點向下遍歷View樹骤星,完成所有ViewGroup和View的布局計算工作,根據(jù)測量出來的寬高及自身屬性荞胡,計算出所有ViewGroup和View顯示在屏幕上的區(qū)域妈踊;performDraw()
:從根節(jié)點向下遍歷View樹,完成所有ViewGroup和View的繪制工作泪漂,根據(jù)布局過程計算出的顯示區(qū)域,將所有View的當(dāng)前需顯示的內(nèi)容畫到屏幕上歪泳。
現(xiàn)在明白了為什么前面那個方法的名字是遍歷了吧萝勤,因為最后是要完成以DecorView為根節(jié)點的view樹的遍歷。
大家對照我畫的這幅圖去看源碼呐伞,會比較好理解敌卓。
關(guān)于View繪制流程真的很長,代碼量也大伶氢,但是我們只需要關(guān)注核心流程就可以了趟径。最后做一個總結(jié):
- 1、在
onCreate
方法中通過setContentView
將XML布局解析成java對象癣防,并添加到PhoneWindow的DecorView中蜗巧; - 2、在
onResume
中將DecorView添加到WindowManagerImpl
中蕾盯,然后通過ViewRootImpl
來執(zhí)行View的繪制流程幕屹; - 3、在
ViewRootImpl
的performTraversals()
方法中分別調(diào)用performMeasure
、performLayout
望拖、performDraw
來完成測量渺尘、布局、繪制说敏; - 4鸥跟、
performMeasure
、performLayout
盔沫、performDraw
這幾個方法最終會調(diào)用measure()
锌雀、layout()
、draw()
來完成最終的繪制迅诬。
參考
Android 自定義View之View的繪制流程(一)
【朝花夕拾】Android自定義View篇之(一)View繪制流程