Android源码梳理:setContentView(...)与LayoutInflater的加载机制分析
Posted FightSeeker
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android源码梳理:setContentView(...)与LayoutInflater的加载机制分析相关的知识,希望对你有一定的参考价值。
题外话:写给自己!
从毕业到如今,已经在社会上混了大概有四年了,做android方面的工作也是有四年了。四年的时间说短不短,说长也不长,但是足以改变很多的东西。从最初的写一个简单的布局一个简单的功能模块都需要Google+百度的青涩到现如今能很熟练的完成自己所想的功能,这不能不说是一个进步,有时候感觉这样就够了可以了。但是当静下心来,回头看看,或者上网查查的时候,发现好多知识还是懵懂,或者是说压根就不会。有些时候,觉得自己啥都会了,又有些时候觉得自己啥都不会,模棱两可,但是当你面试的时候,你会发现,你会被那些真正的大牛们虐的体无完肤的,这个时候,你可能会恍然醒悟,原来我还有那么多的知识点不会呢。当你知道的越多的时候,你会发现自己不会的就越多,而恰恰相反的是,当你觉得都会的时候正好反应了你的无知!学无止境,不会不可怕,不会的我们可以学,怕就怕你不会还不学,还当做自己什么都会的样子,这就是白痴。无知是阻碍你的敌人,放下自己学过的会的知识,去沉淀自己,认识到自己的不足,那就接下来完善自己!怎么完善呢?下面就先从源码分析来提升自己的知识点。
1.背景
做Android应用开发,大家都知道,显示一个界面,就是在activity的子类中重写onCreate方法,在里面调用setContentView(…),那么你在启动activity的时候,就你显示一个界面了。因此,接下来,我就通过源码流程来简单的梳理一下setContentView(…)的加载机制。
2.源码梳理
2.1 在Activity源码当中,setContentView有三个重载的方法,如下:
public void setContentView(@LayoutRes 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().addContentView方法,紧接着就是调用了initWindowDecorActionBar()方法用来初始化ActionBar。getWindow返回的是一个Window类,进这个类一探究竟,你发现这是一个抽象类,里面定义了抽象方法和一些接口等,其中就包括了setContentView方法。既然在这里面找不到,那就去它的子类中看看呢。
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor)
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this);
在Activity中的attach方法中,mWindow = new PhoneWindow(this);这句话,就是实例化了PhoneWindow,也就是上面我们提到的Window的实现类,那么接下来就是到PhoneWindow中看一下.
2.2 PhoneWindow的窗口类中setContentView的方法梳理
Window是一个抽象类,里面定义提供了绘制窗口的一组api以及接口等,而PhoneWindow是Window的具体实现类,它里面包含了一个DecorView对象mDeco,继承自FrameLayout,是对其功能的修饰,并且该对象是所有应用窗口的根view。
下面进入到PhoneWindow类的setContentView(…)方法里面一探究竟。
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)
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();
代码不是很长,首先是对mContentParent 的判断,如果为null,则进入到installDecor()方法中,否在判断是否设置了FEATURE_CONTENT_TRANSITIONS Window属性,默认为false,如果没有则移除所有的view;接着往下看,你会看到又是一个if判断,就是判断是否需要场景变化的,这个不做介绍,在else里面,你发现调用了mLayoutInflater.inflate(layoutResID, mContentParent)这个api,就是把我们的资源文件通过LayoutInflate转换成view树,然后在添加到mContentParent里面去。所以通过这里的分析可以看出,当我们多次设置setContentView(…)之后,为什么能正确的显示我们想要的界面,因为mContentParent会removeAllViews啊!
2.3 PhoneWindow的窗口类中installDecor的方法梳理
在我们分析PhoneWindow类的setContentView(…)方法的时候,当mContentParent == null时,会进入到installDecor()方法,那么我们就进去看看里面到底做了一些什么操作呢?
private void installDecor()
//对根View的判断,DecorView的对象mDecor是否为null
if (mDecor == null)
mDecor = generateDecor();
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0)
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
//对mContentParent 的判断
if (mContentParent == null)
//根据窗口的风格修饰,选择对应的修饰布局文件,并且将id为content的FrameLayout赋值给mContentParent
mContentParent = generateLayout(mDecor);
//这里会初始化一大堆的属性值
.........
方法installDecor()里面的代码有点长,但基本的注释已在上面说明,下面就是看一下里面其中的2个比较关键的点:
(1) 对mDecor的初始化
protected DecorView generateDecor()
return new DecorView(getContext(), -1);
很简单的一句话,就是new一个DecorView对象而已。
(2) 对mContentParent 的初始化
这个很重要,因为我们要添加的布局文件就是要放到它里面的。下面看一下对mContentParent 的初始化到底都做了些什么让我们意外的操作呢。
protected ViewGroup generateLayout(DecorView decor)
// Apply data from current theme.
//首先获取android:theme="..."主题形式
TypedArray a = getWindowStyle();
//一大堆属性值的获取,擦擦的
........
// Inflate the window decor.
//根布局文件id
int layoutResource;
//获取动态设置的Window属性值
int features = getLocalFeatures();
//一大串的if...else... 语句判断,归根结底就是根据设定的不同features来选择不同的根布局文件,得到layoutResource的值
.......
//重要的来了,根据上面得到的layoutResource来回去根view:in,并添加到根视图中decor
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
//获取id为com.android.internal.R.id.content的view,也就是我们的mContentParent
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null)
throw new RuntimeException("Window couldn't find content container view");
//然后又是一大堆属性值的设置等
.......
//返回得到的mContentParent
return contentParent;
通过对以上函数方法体的分析看到,里面其实主要做了2件重要的事,一是对Window属性的设置,二是对mContentParent 的初始化。到这里你会不会想得到,为什么我们在动态设置Window属性的时候,必须要写在setContentView(…)方法之前了吧,是因为对Window窗口属性的设置是在setContentView(…)里面执行的,getLocalFeatures()方法是对动态设置Window属性requestWindowFeature(…)的获取,而getWindowStyle()是对静态设置android:theme=”…”的获取,恍然大悟有没有。
2.4 Window类内部接口Callback的回调
在setContentView(…)方法的最后,你会发现一个回调方法
final Callback cb = getCallback();
if (cb != null && !isDestroyed())
cb.onContentChanged();
这是干啥的呢?就是说,当前的窗口的内容已经发生了变化,并且发出通知。因此,我们是不是有理由这样想,把对view 的获取findViewById放在这里面呢,显然是可以的啊。不过现如今各种注解模式的诞生,好像也用不到了,呵呵….
下面看一下onContentChanged()方法
public void onContentChanged();
咦,是一个空函数,注释已经说明了就是当conetnt发生变化的时候回调。
3 LayoutInflater加载机制原理
很常用的一个类,当我们在Adapter的getView(…)方法里面获取一个布局的时候,LayoutInflater.inflater(….)是不是就常常用的到啊。下面开始从从实例的获取到加载布局的机制一一看起。
3.1 实例对象的获取
获取一个实例,我们通常是这样写的,
LayoutInflater.from(context);
在更以前呢是这样写的,
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
两者之间有什么不同呢?下面看一下LayoutInflater.from(context)的方法体:
public static LayoutInflater from(Context context)
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null)
throw new AssertionError("LayoutInflater not found.");
return LayoutInflater;
哎吆歪,没啥啊,就是对后者一个安全封装而已嘛。
3.2 实例获取了,那么接下来就是加载布局了,对View inflater(…)方法族的分析
方法主要有以下几种
(1)public View inflate(@LayoutRes int resource, @Nullable ViewGroup root)(...)
(2)public View inflate(XmlPullParser parser, @Nullable ViewGroup root)(...)
(3)public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)(...)
(4)public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)(...)
上面给出了inflater(…)的四种方法,但归根最后都会到第四个方法里面,去解析布局,加载文件。那我们就去研究看一下这个方法什么怎么执行的。
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
//一个同步锁区域
synchronized (mConstructorArgs)
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
//定义返回的结果,初始化传入root
View result = root;
try
// Look for the root node.
//xml pull解析的标准模式,首先做判断
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT)
// Empty
//如果一开始不是START_TAG,说明xml文件时有问题滴
if (type != XmlPullParser.START_TAG)
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
final String name = parser.getName();
if (DEBUG)
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
//对merge文件的判断
if (TAG_MERGE.equals(name))
//看到这个判断你想到了什么?merge标注的xml文件root不能为null,并且attachToRoot 必须为true(merge标签,并不代表一个具体的view,只是将它包裹起来的view添加到另一个ViewGroup里面,性能优化你懂得)
if (root == null || !attachToRoot)
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
//递归调用infalter(...)方法
rInflate(parser, root, inflaterContext, attrs, false);
else
// Temp is the root view that was found in the xml
//xml文件的root view,暂存为temp
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
//对传入的root做判断,下面这个if语句的作用就是,如果root!=null,attaToRoot=false,则给temp设置params属性
if (root != null)
if (DEBUG)
System.out.println("Creating params from root: " +
root);
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
//对attachToRoot判断
if (!attachToRoot)
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
if (DEBUG)
System.out.println("-----> start inflating children");
// Inflate all children under temp against its context.
//开始解析加载所有的view
rInflateChildren(parser, temp, attrs, true);
if (DEBUG)
System.out.println("-----> done inflating children");
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
//判断是否要把temp添加到root里面去
if (root != null && attachToRoot)
root.addView(temp, params);
// Decide whether to return the root that was passed in or the
// top view found in xml.
//再次确认最后返回的view
if (root == null || !attachToRoot)
result = temp;
catch (XmlPullParserException e)
InflateException ex = new InflateException(e.getMessage());
ex.initCause(e);
throw ex;
catch (Exception e)
InflateException ex = new InflateException(
parser.getPositionDescription()
+ ": " + e.getMessage());
ex.initCause(e);
throw ex;
finally
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
return result;
通过对以上代码的分析,我们是不是发现了一些有用的额知识点呢,现总结如下:
方法(1) : 创建temp的view,它是xml布局的root view,当root == null时 ,直接返回temp,
当root != null时,执行root.addView(temp, params),最后返回root。
方法(3): 创建temp的view,它是xml布局的root view
当 root == null && attachToRoot == false时,直接返回temp;
当 root == null && attachToRoot == true时,直接返回temp;
当 root != null && attachToRoot == false时,执行temp.setLayoutParams(params),然后直接返回temp;
当 root == null && attachToRoot == true时,执行root.addView(temp, params);然后返回root;
3.3 LayoutInflater源码inflate(…)方法中调运的一些非public方法分析
通过上面的分析,我们会发现,在解析的时候调用了这么一个函数rInflate(…),那么久进入这个函数,看下具体做了什么
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException
final int depth = parser.getDepth();
int type;
//XmlPullParser解析器的标准解析模式
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT)
//找到START_TAG节点程序才继续执行这个判断语句之后的逻辑,否则说明xml布局文件有问题
if (type != XmlPullParser.START_TAG)
continue;
//获取name标记
final String name = parser.getName();
//处理TAG_REQUEST_FOCUS的标记
if (TAG_REQUEST_FOCUS.equals(name))
parseRequestFocus(parser, parent);
else if (TAG_TAG.equals(name)) //处理TAG_TAG标记
parseViewTag(parser, parent, attrs);
else if (TAG_INCLUDE.equals(name)) //处理TAG_INCLUDE标记
//判断include是否是根节点
if (parser.getDepth() == 0)
throw new InflateException("<include /> cannot be the root element");
parseInclude(parser, context, parent, attrs);
else if (TAG_MERGE.equals(name)) //处理TAG_MERGE标记,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都解析完成之后,回调onFinishInflate()方法
if (finishInflate)
parent.onFinishInflate();
4.从以上的分析来简单说一下布局上的优化
(1) 因为xml解析是递归滴,因此减少层级结构可以防止栈溢出,建议多用相对布局,Realtivelayout
(2) 使用merge属性,可以有效的将某些符合条件的多余层级干掉。但是merge属性必须作为根节点,必须指定一个ViewGroup作为其父布局,兵器attachToRoot必须为true。
(3) 使用ViewStub属性,轻量级的预加载处理页面,想什么时候加载就什么时候加载。它本身不会占用层级结构,当被加载的时候,它会被指定的层级替代。
(4) 使用include属性,这个并不难优化层级结构,只是布局的重用,看起来更美观一些
(5)当布局设置了权重widget的时候,最好相应的layout_hORW-xxx 设置为0dp,减少系统运算的次数。
5.总结
setContentView整个过程主要是如何把Activity的布局文件或者java的View添加至窗口里,重点概括为:
(1) 创建一个DecorView的对象mDecor,该mDecor对象将作为整个应用窗口的根视图。
(2) 依据Feature等style theme创建不同的窗口修饰布局文件,并且通过findViewById获取Activity布局文件该存放的地方(窗口修饰布局文件中id为content的FrameLayout)。
(3) 将Activity的布局文件添加至id为content的FrameLayout内。
(4) 当setContentView设置显示OK以后会回调Activity的onContentChanged方法。Activity的各种View的findViewById()方法等都可以放到该方法中,系统会帮忙回调。
LayoutInflater的使用中重点关注inflate方法的参数含义:
(1) inflate(xmlId, null); 只创建temp的View,然后直接返回temp。
(2) inflate(xmlId, parent); 创建temp的View,然后执行root.addView(temp, params);最后返回root。
(3) inflate(xmlId, parent, false); 创建temp的View,然后执行temp.setLayoutParams(params);然后再返回temp。
(4) inflate(xmlId, parent, true); 创建temp的View,然后执行root.addView(temp, params);最后返回root。
(5) inflate(xmlId, null, false); 只创建temp的View,然后直接返回temp。
(6) inflate(xmlId, null, true); 只创建temp的View,然后直接返回temp。
当我们自定义View时在构造函数inflate一个xml后可以实现onFinishInflate这个方法一些自定义的逻辑。
以上是关于Android源码梳理:setContentView(...)与LayoutInflater的加载机制分析的主要内容,如果未能解决你的问题,请参考以下文章
Android源码梳理:setContentView(...)与LayoutInflater的加载机制分析