您的位置:首页 > 移动开发 > Android开发

Android中View的相关知识(4)

2017-06-01 11:15 281 查看

Android中View的相关知识(4)

@(Android)

我们在了解了有关Window的窗口系统机制以后,继续往下走,深入Activity,了解Activity中布局的加载方式:

起始于setContentView

一般情况下,在Activity中加载布局大家都知道,在onCreate();方法中使用setContentView来加载,但是仅仅凭借setContentView();这样一行的代码,就将一个xml文件布局加载到手机屏幕了么,我们今天就研究研究setContentView,看看这一行代码背后的故事。

我们首先找到Activity的源码,(源码的结构很长,这里就不贴图了)直接进入
setContentView
的方法。

我们来看这个方法,Activity的源码中提供了3个重载的
setContentView
的方法:

public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
public void setContentView(View view) {
getWindow().setContentView(view);
initWindowDecorActionBar();
}
public void setContentView(View view, ViewGroup.LayoutParams params) {
getWindow().setContentView(view, params);
initWindowDecorActionBar();
}


可以看见他们都先是调用了
getWindow()
setContentView()
方法,然后调用
initWindowDecorActionBar()
;ok,知道这个分析就有思路了,我们兵分两路,一路进入
getWindow().setContentView();
一路进入
initWindowDecorActionBar();
(如果看过我的Android中View的相关知识(1),那里有个手机屏幕层次图,你就会发现
DecorView
是由两部分组成的:一部分是
TitileView
(这部分由
initWindowDecorActionBar();
实现),一部分是
ContentView
(这部分自然而然由
getWindow().setContentView();
实现),所以说,一切是那么的顺其自然~。)

首先分析getWindow().setContentView();这一路。

这三个重载方法估计差不了多少,我们抓住第一个主要分析,首先它是获取一个
Window
对象,然后利用这个
Window
对象调用
setContentView(layoutResID);


找的
getWindow()
方法:

public Window getWindow() {
return mWindow;
}
//Activity中申明的Window
private Window mWindow;


可以看出,他是获取一个Window,将Activity中申明的mWindow返回。所以调用的是Window的setContentView()方法。之前我们已经分析过窗口管理系统的流程,具体可以看Android中View的相关知识(2)所以分析就简单而就了。

Window
类是个抽象类,具体实现是
PhoneWindow
类。

//Window抽象类
public abstract class Window {
...
//省略其中的代码

public abstract void setContentView(int layoutResID);
...
}


好,进入具体实现类
PhoneWindow
setContectView(int layoutResID);
方法

public class PhoneWindow extends Window implements MenuBuilder.Callback {
...
//省略了其他代码

public void setContentView(int layoutResID) {
/*
* private ViewGroup mContentParent:该变量即为Activity的根布局文件,这是mDecor自身或mDecor的子类
* installDecor()方法用于加载mDecor,后面详说
* FEATURE_CONTENT_TRANSITIONS:窗口内容发生变化时是否需要使用TransitionManager进行过渡的标识
*/
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);
}
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
...
}


这里我光是列出了
setContentView()
方法的详细代码便于我们进行分析。

可以看出,首先是判断
mContentParent
是否为
null
(即是否第一次调用该方法);如果为
null
则调用
installDecor();
方法,否则继续判断是否需要使用
TransitionManager
进行过渡,如果不需要则直接将窗口中的所有
view
移除。如果
mContentParent
不为空且需要使用
TransitionManager
进行过渡,则过渡,否则使用
LayoutInflater.inflate()
方法,将布局加载(这种加载的具体流程我们将在接下来的文章中进行分析);然后,看源码往下走,新建了个
Callback
对象,通过
getCallback()
方法拿到
Callback
对象,这个
Callback
对象其实就是
Activity
,我们在后面会进行解读。

这个
Callback
明显就是一个回调,当
PhoneWindow
接到系统分发给它的各种事件时(触摸事件、IO流、菜单事件)时,可以回调相应的
Activity
进行处理。
Callback
回调的方法很多,我们只关心其
onContentChanged();
方法。看字头疼,其实我写的也麻烦,画个图放松下~



好了,setContentView();这个几行代码的信息很多,总结下:

1. 
mContentParent
肯定是个
ViewGroup
,用于包裹整个的布局,而
installDecor()
必然就是初始化这个
mContentParent;


2. 
hasFeature(FEATURE_CONTENT_TRANSITIONS)
是用于判断是否需要使用
TransitionManager
进行过渡,也是个问题,这是个什么东东?

3. 
Callback
对象是个回调接口,也要具体分析分析。

1.installDecor();

这个方法是在
PhoneWindow
类中,我们进入此方法:

private void installDecor() {
// 如果mDecor为空,则生成一个Decor,并设置其属性
if (mDecor == null) {
mDecor = generateDecor();
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
// 设置mDecor为整个Activity窗口的根节点,从此处可以看出窗口根节点为一个DecorView
mDecor.setIsRootNamespace(true);
...
//省略了一些代码
...
// 如果mContentParent为空,则生成一个Decor,并设置其属性
// 后面会详说generateLayout(DecorView decor)方法
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
mDecor.makeOptionalFitsSystemWindows();
/*
* DecorContentParent位于com.android.internal.widget中,是一个接口
* 由应用程序窗口的顶层Decor实现,该类主要为mDecor提供了许多title/window decor features
*/
final DecorContentParent decorContentParent = (DecorContentParent) mDecor
.findViewById(R.id.decor_content_parent);

if (decorContentParent != null) {

/*
* decorContentParent非空时做的工作:
* 1. 将decorContentParent赋值给mDecorContentParent
* 2. 设置窗口回调函数
* 3.设置窗口的title、icon、logo等属性值
* 代码以省略只将主要功能逻辑进行了简单介绍
*
*/
} else {
/*
* decorContentParent为空时根据窗口是否为一个包含Title的窗口决定是否显示title
* 如果窗口包含特征FEATURE_NO_TITLE,则隐藏窗口的title view 否则设置窗口的title
*/
}
if (mDecor.getBackground() == null
&& mBackgroundFallbackResource != 0) {
mDecor.setBackgroundFallback(mBackgroundFallbackResource);
}
if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) {
...
/*
*设置TransitionManager的相关工作。
*/
...
}
}
}


源码注释的很详细,一些代码细节已经省略,大致可以从以下流程走:



这里面的代码较长,可以看出是各种的初始化,不仅初始化了
mContentParent
,而且在这之前还通过调用
generateDecor();
初始化了一个
mDecor
,设置其焦点的获取方式为,当其子孙都不需要时,自己才获取。设置
mDecor
为整个
Activity
窗口的根节点,
mDecor
DecorView
对象,即手机屏幕层次图中的顶层
View
,这个
mDecor
实际上为
FrameLayout
的子类。然后通过
generateLayout(mDecor);
mDecor
作为参数传入,获取到了我们的
mContentParent
,然后就是通过
findViewById
进行各种控件的获取,最后是各种属性的设置。

代码的流程走完,我们抓住这两个可疑的方法.generateDecor();和generateLayout();方法行分析。不管是哪个方法,肯定总要实现findViewById方法。

a.generateDecor();

看起来这是个初始化mDecor的代码,到底具体做了什么呢,我们进去看看:

protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}


What! 一行代码,666,继续进去DecorVIew类,看看:

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
...
//省略代码
public DecorView(Context context, int featureId) {
super(context);
mFeatureId = featureId;
...
//省略代码
...

}
...
}


看上去generateDecor();只是初始化了一个FrameLayout对象,并没有在其内部压入布局文件,那么那么generateLayout(mDecor);中一定设置了layout文件,(聪明的童鞋,肯定鄙视,这一看名字都像这么回事。- -!)

b.generateLayout();

废话不多说了,上代码~

//返回当前Activity的内容区域视图,即我们的布局文件显示区域mContentParent
protected ViewGroup generateLayout(DecorView decor) {
//从当前Window的Theme中获取一组属性值,赋给a
TypedArray a = getWindowStyle();
/*
* 此处有段代码未贴出,功能为:
* 1. 根据Activity的Theme特征,为当前窗口选择布局文件的修饰feature
* 2. Inflate the window decor
*/
...
/*
* 此处代码功能为:
* 1. 通过对features和mIsFloating的判断,对layoutResource进行赋值
* 2.创建了WindowManager对象
* 3.通过 WindowManager.LayoutParams params = getAttributes();
* 获得各种属性值。
*/
// System.out.println("Features: 0x" + Integer.toHexString(features));
if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
com.android.internal.R.attr.dialogTitleIconsDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = com.android.internal.R.layout.screen_title_icons;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
// System.out.println("Title Icons!");
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {
// Special case for a window with only a progress bar (and title).
// XXX Need to have a no-title version of embedded windows.
layoutResource = com.android.internal.R.layout.screen_progress;
// System.out.println("Progress!");
} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
// Special case for a window with a custom title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
com.android.internal.R.attr.dialogCustomTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = com.android.internal.R.layout.screen_custom_title;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
// If no other features and not embedded, only need a title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
com.android.internal.R.attr.dialogTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
if ((features & (1 << FEATURE_ACTION_BAR_OVERLAY)) != 0) {
layoutResource = com.android.internal.R.layout.screen_action_bar_overlay;
} else {
layoutResource = com.android.internal.R.layout.screen_action_bar;
}
} else {
layoutResource = com.android.internal.R.layout.screen_title;
}
// System.out.println("Title!");
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = com.android.internal.R.layout.screen_simple_overlay_action_mode;
} else {
// Embedded, so no decoration is needed.
layoutResource = com.android.internal.R.layout.screen_simple;
// System.out.println("Simple!");
}
...
int layoutResource;
int features = getLocalFeatures();
...
/*
* 此处代码的功能为:
* 1. getLocalFeatures()返回一个用于描述当前Window特征的整数值
* 2. layoutResource为根据features所指代的窗口特征值而为当前窗口选定的资源文件id
* 3. 系统包含多个布局资源文件,位于frameworks/base/core/res/layout/
* 4. 主要有:R.layout.dialog_titile_icons、R.layout.screen_title_icons
*     R.layout.screen_progress、R.layout.dialog_custom_title
*     R.layout.dialog_title
*     R.layout.screen_title    最常用的Activity窗口修饰布局文件
*     R.layout.screen_simple   全屏的Activity窗口布局文件
*/

//startChanging()方法内容:mChanging = true;
mDecor.startChanging();
//将layoutResource资源文件包含的View树添加到decor中
//width和height均为MATCH_PARENT
//并为mContentRoot和contentParent赋值
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 ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {
ProgressBar progress = getCircularProgressBar(false);
if (progress != null) {
progress.setIndeterminate(true);
}
}
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
registerSwipeCallbacks();
}
//后面包含一段只能应用于顶层窗口的一些Remaining steps
//主要用于设置一些title和background属性
return contentParent;
}


要不是省略了好多代码,代码更多。。。首先,
getWindowStyle()
在当前的
Window
theme
中获取我们的
Window
中定义的属性,然后设置
Window
的各种属性,接下来通过对
features
mIsFloating
的判断,对
layoutResource
进行赋值,以及创建
WindowManager
,获得
WindowManger.LayoutParams
;接着在得到
layoutResource
后,通过
LayoutInflater
把布局转化成
view
,然后通过
addView();
方法(是个熟悉的方法)将此View加入到我们的
decor
,即传入的
mDecor
中。通过
mDecor.findViewById
传入
R.id.content
(相信这个id大家或多或少都听说过),好了,可以看到我们的
mDecor
是一个
FrameLayout
,然后会根据
theme
去选择系统中的布局文件,将布局文件通过
inflate
转化为
view
,加入到
mDecor
中;这些布局文件中都包含一个
id
content
FrameLayout
,将其引用返回给
mContentParent
。有了
mContentParent
,然后返回到PhoneWindow.setContentView();方法中(其实generateLayout()方法还有很多细节,这里都省略了,抓主要线路,得到contentParent后,回到PhoneWindow),通过
mLayoutInflater.inflate(layoutResID, mContentParent);
把我们写的布局文件通过
inflater
加入到
mContentParent
中。这样布局就加载进来了。



2.hasFeature(FEATURE_CONTENT_TRANSITIONS)

废话不说,进入Window类~上代码~

public boolean hasFeature(int feature) {
return (getFeatures() & (1 << feature)) != 0;
}


又是传说中的一行代码~ 大值可以断定这是个判断是否有属性值的方法,继续深入进入getFeatures()方法:

protected final int getFeatures(){
return mFeatures;
}


-!依旧一行代码,我们继续找找mFeatures;

在Window类中找的这样代码

public Window(Context context) {
mContext = context;
mFeatures = mLocalFeatures = getDefaultFeatures(context);
}


接着深入,进入getDefaultFeatures();

public static int getDefaultFeatures(Context context) {
int features = 0;
final Resources res = context.getResources();
if (res.getBoolean(com.android.internal.R.bool.config_defaultWindowFeatureOptionsPanel)) {
features |= 1 << FEATURE_OPTIONS_PANEL;
}
if (res.getBoolean(com.android.internal.R.bool.config_defaultWindowFeatureContextMenu)) {
features |= 1 << FEATURE_CONTEXT_MENU;
}
return features;
}


代码很简单,首先通过getResources();获得Window的Resources对象,如果res.getBoolean()能获得,就对features进行相应的设置,最后返回features;(features的值代表着Window的各种属性比如FEATURE_NO_TITLE )根据设置的features,如果需要TransManager,就进行TransManager,~

3.Callback对象

光听这名字,闭着眼也能知道这个是回调接口。

public interface Callback {
...
//这里面是各种回调方法,

...
}


这里是各种回调方法,每次contentView发生变化时,就调用onContentChanged();方法。

最后我们总结下setContentView的工作流程

setContentView方法的具体实现是在PhoneWindow类中,主要通过如下几个步骤完成xml布局资源文件或View的加载。

第一步:若是首次使用setContentView方法,则先创建一个DecorView对象mDecor,该对象是整个Activity窗口的根视图;然后根据程序中选择的Activity的Theme/Style等属性值为窗口添加布局属性和相应的修饰文件,并通过findViewById方法获取对应的根布局文件添加到mDecor中,创建mContentParent,获得id为content的FrameLayout,将其返回给mContentParent,也就是说,第一次使用该方法时会将Activity显示区域进行初始化;若不是第一次使用该方法,则之前已完成初始化过程并获得了mDecor和mContentParent对象,则只需要将之前添加到mContentParent区域的Views移除,空出该区域重新进行布局即可,简而言之,就是对mContentParent区域进行刷新;

第二步:通过inflate(加载xml文件)或addView(加载View)方法将Activity的布局文件添加到mContentParent区域;

第三步当setContentView设置显示OK以后,回调Activity的onContentChanged方法,通知Activity布局文件已经成功加载完成,接下来我们便可以使用findViewById方法获取布局文件中含有id属性的view对象了;

注意:使用setContentView(View view)方法设置Activity的布局时,系统会默认将该view的width和height值均设为MATCH_PARENT,而不是使用view自己的属性值,所以如果想通过一个View对象设置布局,又想使用自己设置的参数值时,需要使用setContentView(View view, LayoutParams params)方法 。

老规矩,画个图总结下:~



浅谈布局文件优化技巧

1.从上面的分析可知,在加载xml布局文件时,系统是通过递归的方式从根节点到叶子节点一步一步对控件的属性进行解析的,所以xml文件的层次越深,效率越低,如果嵌套过多,还有可能导致栈溢出,所以在书写布局文件时,应尽量对布局文件进行优化,通过使用相对布局等方式减少不必要的嵌套层次。

2.在源码中,可以看到对merge标签进行处理的过程。在某些场合下,merge标签的使用也可以有效减少布局文件的嵌套层次。如某些比较复杂的布局文件,需要将布局文件拆分开来,分为一个根布局文件和若干个子布局文件,这时可能子布局文件的根节点在添加到根布局文件中时并没有太多意义,只会增加根布局文件的嵌套层次,这种情况下,在子布局文件处使用merge标签就可以去掉无谓的嵌套层次。不过merge标签的使用也是有限制的,首先merge标签只能用于一个xml文件的根节点;其次,使用inflate方法来加载一个根节点为merge标签的布局文件时,需要为该文件指定一个ViewGroup对象作为其父元素,同时需要设置attachToRoot属性为true,否则会抛出异常;

3.利用include标签增加布局文件的重用性和可读性;

参考文章:

1 Android 从setContentView谈Activity界面的加载过程

2 Android 源码解析 之 setContentView

这两篇文章写得真心好,学习了好多东西~。后面的总结就是直接的照搬。。。

好了,content的流程分析完了,接下来我们继续分析Title,看看其是如何创建的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android view