Android 布局加载源代码分析
在我们Activity中,我们要加载页面布局文件,通过setContentView()
方法就能将我们用XML编写的布局文件载入。今天我们通过源代码来对这个过程进行分析。
看一段简单代码:
public class Main2Activity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main2); }}
我们进入setContentView()
方法,可以看到:
/** * Set the activity content from a layout resource. The resource will be * inflated, adding all top-level views to the activity. * * @param layoutResID Resource ID to be inflated. * * @see #setContentView(android.view.View) * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams) */public void setContentView(@LayoutRes int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar();}
只有两行,由于涉及到layoutResID
,我们继续去看getWindow()
,发现很简单的一个函数:
/** * Retrieve the current {@link android.view.Window} for the activity. * This can be used to directly access parts of the Window API that * are not available through Activity/Screen. * * @return Window The current window, or null if the activity is not * visual. */ public Window getWindow() { return mWindow; }
其实也就是返回了一个Window
对象,那么Window
类又是一个什么类呢?
我们去了解下Window
类:
/** * Abstract base class for a top-level window look and behavior policy. An * instance of this class should be used as the top-level view added to the * window manager. It provides standard UI policies such as a background, title * area, default key processing, etc. * * The only existing implementation of this abstract class is * android.view.PhoneWindow, which you should instantiate when needing a * Window. */
public abstract class Window { /** Flag for the "options panel" feature. This is enabled by default. */ public static final int FEATURE_OPTIONS_PANEL = 0; /** Flag for the "no title" feature, turning off the title at the top * of the screen. */ public static final int FEATURE_NO_TITLE = 1; ……}
截取了部分代码,我们可以看到其实Window
是一个抽象类,接着我们去看下关于这个类的描述,其中有一句话特别重要,就是这个Window
类只有唯一的一个实现类PhoneWindow
,好了,对于:
getWindow().setContentView(layoutResID);
我们就直接去PhoneWindow
中的setContentView()
方法一探究竟。
@Overridepublic 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) { 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(); } mContentParentExplicitlySet = true;}
一看这个函数还是不少,没关系,我们慢慢看。
首先我们看mContentParent
是什么?
// This is the view in which the window contents are placed. It is either// mDecor itself, or a child of mDecor where the contents go.ViewGroup mContentParent;
看下对这个字段的注释,注释上说这是放置窗口内容的视图,它要么是mDecor
本身,要么是是内容所去mDecor
的孩子。其实说一千道一万,只是说了mContentParent
是我们放置内容的父布局。那么我们的mContentParent
是什么时候初始化的呢?带着这个问题,我们去寻找,最后我们可以发现:
private void installDecor() { mContentParent = generateLayout(mDecor);}
可以看到它的初始化是在installDecor()
方法中,而我们的installDecor()
又是在当mContentParent == null
时才会被调用,所以这里的mContentParent
是空的。
既然是空的,那我们接下来就是要去installDecor()
这个方法。
方法很长,首先来了解下mDecor
。其实就是DecorView
,而DecorView
是FrameLayout
的一个子类。
函数有点长,看重点:
private void installDecor() { mForceDecorInstall = false; if (mDecor == null) { 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) { mContentParent = generateLayout(mDecor); ...}
省略了部分代码,在mContentParent == null
之前都是初始化eDecor
,由于刚刚我们说过mContentParent
就是空,所以接下来,我们要去看看这个generateLayout(mDecor)
方法。
走了好半天,终于走到了关键部分了,真的是不容易。走过去以后,发现这个函数不是一般的长,怎么办?
看重点!!!
我们往下略着看,看到有个地方有点意思:
final Context context = getContext();final int targetSdk = context.getApplicationInfo().targetSdkVersion;final boolean targetPreHoneycomb = targetSdk < android.os.Build.VERSION_CODES.HONEYCOMB;final boolean targetPreIcs = targetSdk < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH;final boolean targetPreL = targetSdk < android.os.Build.VERSION_CODES.LOLLIPOP;final boolean targetHcNeedsOptions = context.getResources().getBoolean( R.bool.target_honeycomb_needs_options_menu);final boolean noActionBar = !hasFeature(FEATURE_ACTION_BAR) || hasFeature(FEATURE_NO_TITLE);if (targetPreHoneycomb || (targetPreIcs && targetHcNeedsOptions && noActionBar)) { setNeedsMenuKey(WindowManager.LayoutParams.NEEDS_MENU_SET_TRUE);} else { setNeedsMenuKey(WindowManager.LayoutParams.NEEDS_MENU_SET_FALSE);}
看到没,这里就是控制版本在11之前和11之后是否显示菜单按钮的控制部分。看,读源码的过程还会有意外的收获。
嗯,我们接着往下看。
发现了一段注释
// Inflate the window decor.
嗯,看到这句我们就知道重点要来了。
看完了一大推的if-else语句之后,终于看到了一句:
// Embedded, so no decoration is needed.layoutResource = R.layout.screen_simple;// System.out.println("Simple!");
对呀,我们没有设置任何关于Feature的属性,所以也很好理解为啥一下子就来到了这里。
既然有布局可以看,那么我们就看看上面的布局是个什么。
<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>
很简单有木有?
往下看,其实我们可以发现,mDecor
是在
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
将上面的xml
布局inflate
出来,然后添加到我们的mDecor
上的。
然后通过mDecor
的findViewById()
,找到上面xml
中的FrameLayout
,然后在最后返回。
终于把这个过程屡清楚了。其实就是inflate
一个xml
,然后将里面的FrameLayout
返回出来。
所以我们的mContentParent
,最后的引用就是刚刚那个FrameLayout
。
一个installDecor()
找的我们真是肝肠寸断。
最终我们还是回到了这里:
@Overridepublic 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) { 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(); } mContentParentExplicitlySet = true;}
然后就会走mContentParent.removeAllViews()
,再接着就是mLayoutInflater.inflate(layoutResID, mContentParent)
,进去看看吧,这个方法不止一点重要。
进去后发现这TM不就是用了XmlPullParser
来解析xml的么?
我们看到其中一部分源码:
if (TAG_MERGE.equals(name)) { if (root == null || !attachToRoot) { throw new InflateException(" can be used only with a valid " + "ViewGroup root and attachToRoot=true"); } rInflate(parser, root, inflaterContext, attrs, false);}
这里就是如果merge
标签没有parent
,就直接抛异常,然后调用:
rInflate(parser, root, inflaterContext, attrs, false);
看看这个函数的实现:
/** * Recursive method used to descend down the xml hierarchy and instantiate * views, instantiate their children, and then call onFinishInflate(). * * Note: Default visibility so the BridgeInflater can * override it. */
void rInflate(XmlPullParser parser, View parent, Context context, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException { final int depth = parser.getDepth(); int type; while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { if (type != XmlPullParser.START_TAG) { continue; } final String name = parser.getName(); if (TAG_REQUEST_FOCUS.equals(name)) { parseRequestFocus(parser, parent); } else if (TAG_TAG.equals(name)) { parseViewTag(parser, parent, attrs); } else if (TAG_INCLUDE.equals(name)) { if (parser.getDepth() == 0) { throw new InflateException(" cannot be the root element"); } parseInclude(parser, context, parent, attrs); } else if (TAG_MERGE.equals(name)) { throw new InflateException(" 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); } } if (finishInflate) { parent.onFinishInflate(); }}
不短,但也不长,代码很简单,看看就懂了,我们只需要关注一句:
rInflateChildren(parser, view, attrs, true);
返回到函数调用的地方,我们在看看else里面,直接通过
// Temp is the root view that was found in the xmlfinal View temp = createViewFromTag(root, name, inflaterContext, attrs);
获取到了根节点,最后的最后也走到了:
// Inflate all children under temp against its context.rInflateChildren(parser, temp, attrs, true);
看函数意思,其实我们很容易理解,这个函数就是去inflate子布局的。
进入里面一看,我擦,最后又去了上面的rInflate
,不用说咯,这就是递归,而且还是以深度优先的递归,厉害了。
最后我们的mContentParent
上就有了布局啦,真的不容易啊,给自己一杯奶茶,庆祝下吧。
更多相关文章
- Unity-Android通信:AndroidJava 使用Unity c#编写Android程序调用
- Android例子—设置Activity全屏的三种方法
- Android stuio在MainActivity中运行java的main方法
- Android UI布局
- android中的提示信息显示方法(toast应用)
- adb devices 找不到设备的解决方法
- android 按home键返回到桌面后,再按桌面应用图标又重新打开该应用
- android获取各种系统路径的方法