对于UI加载过程我们会有很多的疑问
1.setContentView到底做了什么,为什么我们调用了之后就能显示相应的布局
2.PhoneWindow是什么,和window有什么关系
3.DecorView是什么,和我们的布局又有什么样的关系
4.RequestFeature为什么要在setContentView之前调用
好了,开始我们的摸索。
DecorView的创建过程
我们先从继承Activity进行探究
1.进入setContentView
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
通过attach方法可知getWindow得到的是PhoneWindow对象(具体可以查看ActivityThread类performLaunchActivity方法),PhoneWindow继承Window
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();//生成DecorView
} 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();
}
mContentParentExplicitlySet = true;
}
这一段代码最重要的是installDecor方法和inflate方法,那我们先进入第一个方法
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);//生成DecorView
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
//下面的就是设置title、转场
}
}
DecorView继承于FrameLayout,下面进入generateLayout方法
protected ViewGroup generateLayout(DecorView decor) {
......//设置Flag和requestFeature
WindowManager.LayoutParams params = getAttributes();
......
mDecor.startChanging();
//根据layoutResource来生成DecorView
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//contentParent为内容布局
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
......
return contentParent;
}
通过这段代码我们也就知道了为什么RequestFeature要在setContentView之前调用。为了更好的理解,我们可以研究一下布局文件,以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">
<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>
这个布局生成的View就是DecorView,contentParent也就是FrameLayout,其实就是我们平时写的布局。
ok,回到PhoneWindow->setContentView的代码,接下来进入inflate方法,在这里面我们传入了我们写的布局和contentParent,可想而知inflate方法做的就是将我们xml写的布局解析出来依次添加到contentParent中
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
next
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
View result = root;
try {
//观察root节点
int type;
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
if (TAG_MERGE.equals(name)) {//标签是否为merge
//当root不为空则使用merge才有效
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
rInflateChildren(parser, temp, attrs, true);
if (root != null && attachToRoot) {
root.addView(temp, params);
}
}
}
return result;
}
}
进入rInflateChildren方法
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
final String name = parser.getName();
if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {//include不能是根元素
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);//如果深度不为0则进行解析include标签
} else if (TAG_MERGE.equals(name)) {//merge标签必须是根元素
throw new InflateException("<merge /> must be the root element");
} else {
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);//递归调用
viewGroup.addView(view, params);//将解析完的View添加到contentParent中
}
}
if (finishInflate) {//当inflate完成则调用ViewGroup的onFinishInflate方法
parent.onFinishInflate();
}
}
不断的递归调用将View添加到上面定义的contentParent中,OK,这样就将试图显示出来了
上面我们是用Activity的源码来分析的,现在做兼容都使用了AppCompatActivity了,那它里面是怎么调用的呢?
我们进入AppCompatActivity的setContentView方法
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
这里调用了一个代理对象的setContentView方法,进入
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
final int sdk = Build.VERSION.SDK_INT;
if (BuildCompat.isAtLeastN()) {
return new AppCompatDelegateImplN(context, window, callback);
} else if (sdk >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else if (sdk >= 14) {
return new AppCompatDelegateImplV14(context, window, callback);
} else if (sdk >= 11) {
return new AppCompatDelegateImplV11(context, window, callback);
} else {
return new AppCompatDelegateImplV9(context, window, callback);
}
}
擦,原来Google做了这么多的兼容,进入setContentView方法是进入的AppCompatDelegateImplV9类里面的
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
这代码和研究Activity的setContentView方法非常的相似,也是通过DecorView得到contentParent然后将自己写的布局添加到contentParent中,那ensureSubDecor里面是做了什么操作呢?
private ViewGroup createSubDecor() {
//这里得到的是AppCompat的主题资源,所以要在继承AppCompatActivity的类使用AppCompat的主题
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
......//设置title和Feature
// 开始初始化PhoneWindow的DecorView
mWindow.getDecorView();
subDecor = (ViewGroup) LayoutInflater.from(themedContext)
.inflate(R.layout.abc_screen_toolbar, null);
mDecorContentParent = (DecorContentParent) subDecor
.findViewById(R.id.decor_content_parent);
mDecorContentParent.setWindowCallback(getWindowCallback());
//为布局生成View并赋值给subDecorView
//生成subDecor中的contentView
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
R.id.action_bar_activity_content);
//得到PhoneWindow中contentParent
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
if (windowContentView != null) {
//将DecorView中的contentView设置为NO_ID,并将subDecorView的contentView设置为android.R.id.content
windowContentView.setId(View.NO_ID);
contentView.setId(android.R.id.content);
}
// Now set the Window's content view with the decor
mWindow.setContentView(subDecor);
return subDecor;
}
这里需要注意的是在AppCompatActivity里面,它做了一个替换,将DecorView的contentView设置为NO_ID,然后将subDecorView的contentView设置为android.R.id.content。只所以可以这样是因为在之前调用了mWindow.getDecorView()方法对DecorView进行了初始化操作,里面的布局就不多说和上面的布局类似,所以说我们要多多使用AppCompatActivity,毕竟Google做了这么多兼容
总结:每一个Activity都有一个关联的Window对象,用来描述应用程序窗口。每一个窗口内部又包含了一个DecorView对象,Decorview对象用来描述窗口的视图--xml布局
最后我们附上一张AppCompatActivity的视图结构
image可以看到继承AppCompatActivity会将TextView自动转化为AppCompatTextView
DecorView添加到Window过程
首先我们直接进入ActivityThread的handleResumeActivity方法
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
ActivityClientRecord r = mActivities.get(token);
......
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);
}
}
......
}
进入WindowManagerImpl类addView方法(decor代表DecorView)
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
root = new ViewRootImpl(view.getContext(), display);//生成ViewRootImpl对象
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);
}
}
}
WindowManagerGlobal用来提供window显示和操作的全局方法,接下来进入ViewRootImpl的setView方法
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
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();(1)
......
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);(2)
}
......
view.assignParent(this);(1)
......
}
}
这里我们只需要看三个方法,一个是requestLayout()方法,然后一步一步调用了doTraversal方法进而调用了performTraversal方法
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
另一个则是addToDisplay方法,该过程使用了aidl的方式将视图显示到了window上
最后一个则是view.assignParent(this)方法,虽然它调用的是DecorView的方法,最后也是调用的View里面的assignParent方法
void assignParent(ViewParent parent) {
if (mParent == null) {
mParent = parent;
} else if (parent == null) {
mParent = null;
} else {
throw new RuntimeException("view " + this + " being added, but"
+ " it already has a parent");
}
}
这个方法是将ViewRootImpl赋值给了mParent,这就是我们在代码中调用requestLayout方法最后会调用ViewRootImpl的原因,看到这里有一种豁然开朗的感觉
根据上面绘制DecorView添加到窗口的流程图
image至此,就完了。如果不知道怎么使用的话我们可以去看snackBar的源码,这个控件更能教会我们怎么去使用android.R.id.content
private static ViewGroup findSuitableParent(View view) {
ViewGroup fallback = null;
do {
if (view instanceof CoordinatorLayout) {
return (ViewGroup) view;
} else if (view instanceof FrameLayout) {
if (view.getId() == android.R.id.content) {
return (ViewGroup) view;
} else {
fallback = (ViewGroup) view;
}
}
if (view != null) {
final ViewParent parent = view.getParent();
view = parent instanceof View ? (View) parent : null;
}
} while (view != null);
return fallback;
}
不断的去getPartent直到当前View是contentView,使用这个特性我们可以去做非常炫酷的弹框效果应用在所有的界面。
每天的进步一点点,将来收获多一点
共勉~
网友评论