前言:這是我第一次寫源碼解析類的文章,閱讀源碼真的能學到不少,驚訝大牛設計思路的同時,感覺到更多的是自己好多知識都不清楚。由于水平有限,文中難免有些錯誤,歡迎指正互相學習~
Window概述
Window,正如它的直譯,表示一個窗口。以前我們常說,Activity是直接可與用戶交互的UI界面,而這些交互界面都要依附在Window窗口下才能工作、顯示!從某種程度甚至可以這么說,Android中的視圖(Activity,Dialog,PopupWindow......)都是依附于Window來呈現的,所有的Event事件也都是從Window層下發的。關于Window,Activity,View的關系,這里可以先給出一張粗糙的二維關系圖:
從面向對象的角度來說,Window是一個抽象的概念,它對應著一個頂級view,還有一個ViewRootImpl,通過這個實現類中的ViewRootImpl,我們可以操作具體的View,并向它們下發事件。這從抽象Window的實現類PhoneWindow中可以找到如下源碼:
@Override
public void injectInputEvent(InputEvent event) {
getViewRootImpl().dispatchInputEvent(event);
}
private ViewRootImpl getViewRootImpl() {
if (mDecor != null) {
ViewRootImpl viewRootImpl = mDecor.getViewRootImpl();
if (viewRootImpl != null) {
return viewRootImpl;
}
}
throw new IllegalStateException("view not added");
}
關于view事件的dispatch(分發)、onIntercept(攔截)、onTouch(消耗)就不在這宣兵奪主詳細講了。
除此之外,Window內部還向我們提供了一個方便各種狀態下回調的CallBack接口,主要回調方法如下:
/**
* API from a Window back to its caller. This allows the client to
* intercept key dispatching, panels and menus, etc.
*/
public interface Callback {
public boolean dispatchKeyEvent(KeyEvent event);
public boolean dispatchTouchEvent(MotionEvent event);
public boolean onMenuItemSelected(int featureId, MenuItem item);
public void onContentChanged();
public void onWindowFocusChanged(boolean hasFocus);
public void onAttachedToWindow();
public void onDetachedFromWindow();
}
很熟悉把~原來我們在Activiy中的各種回調方法都是這其中來的,這些方法回調的周期相信大家也都知道把,這里只說一下onContentChanged()
,還記得在Activiy的creat方法中,我們必須給界面通過setContenViewt設置布局,而當布局設置完成后,Window便會回調此方法。至于到底什么是ContentView?后面會分析。
而外部訪問Window則必須通過WindowManager的實現類WindowMangerImpl,可以發現其中向外部提供了三個增加、更新、刪除View的方法:
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
@Override
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.updateViewLayout(view, params);
}
@Override
public void removeView(View view) {
mGlobal.removeView(view, false);
}
通過方法名我們可以很清楚地辨別這三個方法的作用,但是mGlobal又是什么呢?其實這只是一個工作業務的橋接類,將addView的工作通過WindowManagerGlobal全部橋接給了ViewRootImpl來實現了。這也就與上文中說的``Window是一個抽象的概念,它對應著一個頂級view,還有一個ViewRootImpl,通過這個實現類中的ViewRootImpl,我們可以操作具體的View,向它們下發事件`對應起來了,我們主要分析addView栗子:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
}
synchronized (mLock) {
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
synchronized (mLock) {
final int index = findViewLocked(view, false);
if (index >= 0) {
removeViewLocked(index, true);
}
}
throw e;
}
}
這里很清晰地看到new了一個ViewRootImpl實例,隨后將Window中的view,root(ViewRootImpl),wparams(布局參數)存入了相應列表中,之后又通過root.setView(view, wparams, panelParentView);
將事情全部移交給了ViewRootImpl來做。setView方法比較復雜,大致思路是先通過requestLayout
刷新當前布局,隨后通過IPC機制,遠程調用WindowManagerService完成View的set。給出一小部分源碼:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
mView = view;
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
} catch (RemoteException e) {
mAdded = false;
mView = null;
mAttachInfo.mRootView = null;
mInputChannel = null;
mFallbackEventHandler.setView(null);
unscheduleTraversals();
setAccessibilityFocus(null, null);
throw new RuntimeException("Adding window failed", e);
} finally {
if (restore) {
attrs.restore();
}
}
}
最后在這里簡單提及下DecorView(PhoneWindow中的內部類),具體的會在下文Activity中講更好理解,這里只是說明下Window中有這么一個頂級view。
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
可以看出DecorView本質還是一個FrameLayout,這里面就負責我們的各種UI顯示,各種事件消耗、分發。通過Activity,我們就可以將自己的UI布局載入Window中的DecorView!
這里總結一下Window中的知識點:
- 1.一個Window可以抽象理解為一個View和一個ViewRootImpl的組合。
- 2.Window內部有一個十分豐富的CallBack接口,可以滿足我們大部分的回調需求。
- 3.通過WindowManager的實現類WindowManagerImpl,管理修改我們的Window,本質上是將這些操作橋接給了ViewRootImpl。
- 4.Window中的UI承載體--DecorView。
Activity概述
關于Activity的啟動流程和Thread這里不會講述(其實是我也弄不清楚,_~),主要講Activity是如何和Window建立起聯系的。
當新建一個Activity時,通常ide工具都會自動幫我setContentView,之前一直以為這是將我們的布局文件通過ID直接設置給Activity,見多了也就以為這是一種定理了。其實看源碼,很清楚地可以發現它本質還是獲取的Window:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
先不急著看Window中的setContentView方法,我們先看Window是如何獲取的。
public Window getWindow() {
return mWindow;
}
對比源碼發現,這個mWindow在L、M版本上初始化的方式還不太一樣,如圖:
L版本是通過一個策略類PolicyManager,使用反射機制獲取IPolicy來實例化mWindow,而M版本直接在attach方法中,
mWindow = new PhoneWindow(this);
直接實例化。不知道這其中是不是考慮到了性能的優化。
通過mWindow.setCallback(this);
,便在Activity綁定了Window中的回調接口Callback。
接下來我們看PhoneWindow中的setContentView具體邏輯:
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
當我們初次設置contentView時,會執行installDecor(),否則只是清除所有子view,接著inflate我們的布局進decorView的內容區域,最后回調onContentChanged(),通知Activity,DecorView裝載完畢了!
再追蹤installDecor()方法前,我們先了解一下DecorView的具體結構:
不難看出,一個DecorView可以非為兩部分,第一部分就是上放的titleBar(標題欄),第二部分就是contentParent(內容區),我們的布局便是裝載進contentParent區域。
接著我們看installDecor()方法:
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
}
.................
.................
.................
}
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
很好理解,如果我們的DecorView不存在,則為我們生成一個,接著在生成一個內容區域以供裝載布局。
我們主要看generateLayout()的源碼:
protected ViewGroup generateLayout(DecorView decor) {
//省略ViewGroup參數、樣式設置部分//
mDecor.startChanging();
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
if (getContainer() == null) {
final Drawable background;
if (mBackgroundResource != 0) {
background = getContext().getDrawable(mBackgroundResource);
} else {
background = mBackgroundDrawable;
}
mDecor.setWindowBackground(background);
final Drawable frame;
if (mFrameResource != 0) {
frame = getContext().getDrawable(mFrameResource);
} else {
frame = null;
}
mDecor.setWindowFrame(frame);
mDecor.setElevation(mElevation);
mDecor.setClipToOutline(mClipToOutline);
if (mTitle != null) {
setTitle(mTitle);
}
if (mTitleColor == 0) {
mTitleColor = mTextColor;
}
setTitleColor(mTitleColor);
}
mDecor.finishChanging();
return contentParent;
}
發現contentParent內容區也是通過ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
找到我們傳入布局文件的ID,為我們生成一個ViewGroup,在對其做一系列UI優化后,就可以正常使用了。
當然,我們并不一定每次都必須通過布局文件的ID來setContentView,完全可以自己動態地設置內容,栗:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//setContentView(R.layout.activity_main);
LinearLayout linearLayout = new LinearLayout(this);
linearLayout.setOrientation(LinearLayout.VERTICAL);
for (int i= 0; i < 5; i++) {
TextView textView = new TextView(this);
textView.setTextSize(12);
textView.setText(i+"");
linearLayout.addView(textView);
}
setContentView(linearLayout);
}
不過要想讓用戶正常看到這些布局,還需要等待Activity的onResume生命周期中執行如下方法:
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
我想這也是初識Android時,會說必須經過onResume生命周期后,Activity才能"獲取焦點"的原因吧!
其實從另一個常用的方法,我們也能清楚地理解到Activity,Window,DecorView的關系。
findViewById :
@Nullable
public View findViewById(@IdRes int id) {
return getWindow().findViewById(id);
}
@Nullable
public View findViewById(@IdRes int id) {
return getDecorView().findViewById(id);
}