setContentView加载解析过程源码分析

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了setContentView加载解析过程源码分析相关的知识,希望对你有一定的参考价值。

Activity的setContentView方法

Activity的源码中提供了三个重载的setContentView方法,如下:
  1. public void setContentView(int layoutResID) {
  2. getWindow().setContentView(layoutResID);
  3. initWindowDecorActionBar();
  4. }
  5. public void setContentView(View view) {
  6. getWindow().setContentView(view);
  7. initWindowDecorActionBar();
  8. }
  9. public void setContentView(View view, ViewGroup.LayoutParams params) {
  10. getWindow().setContentView(view, params);
  11. initWindowDecorActionBar();
  12. }
可以看见他们都先调用了getWindow()对应的setContentView方法,然后调用Activity的initWindowDecorActionBar方法,关于initWindowDecorActionBar方法暂时跳过不去管他(只是装饰ActionBar用的)。

Activity,Window,DectorView间的关系

在开始分析Activity组合对象Window的setContentView方法之前请先明确如下关系:
技术分享
Activity中有一个成员为Window,其实例化对象为PhoneWindow,PhoneWindow为抽象Window类的实现类。

这里先简要说明下这些类的职责:
  • Window是一个抽象类,提供了绘制窗口的一组通用API。
  • PhoneWindow是Window的具体继承实现类。而且该类内部包含了一个DecorView对象,该DectorView对象是所有应用窗口(Activity界面)的最顶层的View(根View),但注意DectorView并不是应用窗口的直接父布局
  • DecorView是PhoneWindow的内部类,是FrameLayout的子类,是对FrameLayout进行功能的修饰(所以叫DecorXXX)。

PhoneWindow的setContentView方法

我们可以看见Window类的setContentView方法都是抽象的。所以我们直接先看PhoneWindow类的setContentView(int layoutResID)方法源码,如下:
  1. public void setContentView(int layoutResID) {
  2. // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
  3. // decor, when theme attributes and the like are crystalized. Do not check the feature before this happens.
  4. if (mContentParent == null) {
  5. installDecor();
  6. } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
  7. mContentParent.removeAllViews();
  8. }
  9. if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
  10. final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, getContext());
  11. transitionTo(newScene);
  12. } else {
  13. mLayoutInflater.inflate(layoutResID, mContentParent);
  14. }
  15. final Callback cb = getCallback();
  16. if (cb != null && !isDestroyed()) {
  17. cb.onContentChanged();
  18. }
  19. }
可以看见,第4行首先判断mContentParent是否为null(第一次调用时才为null),如果是第一次调用,则调用installDecor()方法,否则判断是否设置FEATURE_CONTENT_TRANSITIONS的Window属性(默认false),如果没有就移除该mContentParent内所有的所有子View;
接着13行将我们的资源文件通过LayoutInflater转换为View树,并且添加至父视图mContentParent中。

再来看下PhoneWindow类setContentView另外两个重载方法的源码,如下:
  1. @Override
  2. public void setContentView(View view) {
  3. //从这里就可以解释,为什么调用setContentView(View)时,被添加的View的宽高都是无效的,而是MATCH_PARENT
  4. setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
  5. }
  6. @Override
  7. public void setContentView(View view, ViewGroup.LayoutParams params) {
  8. // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
  9. // decor, when theme attributes and the like are crystalized. Do not check the feature before this happens.
  10. if (mContentParent == null) {
  11. installDecor();
  12. } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
  13. mContentParent.removeAllViews();
  14. }
  15. if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
  16. view.setLayoutParams(params);
  17. final Scene newScene = new Scene(mContentParent, view);
  18. transitionTo(newScene);
  19. } else {
  20. mContentParent.addView(view, params);
  21. }
  22. final Callback cb = getCallback();
  23. if (cb != null && !isDestroyed()) {
  24. cb.onContentChanged();
  25. }
  26. }
可以看到,setContentView(View view)实质也是调用了setContentView(View view, ViewGroup.LayoutParams params),只是LayoutParams设置为了MATCH_PARENT而已。从这里就可以解释,为什么调用setContentView(View)时,被添加的View的宽高都是无效的,而是MATCH_PARENT。

这两个方法与setContentView(int layoutResID)相比的区别仅仅是:一个是通过LayoutInflater将xml文件解析转换为View后添加至父视图中,一个是直接使用父视图的addView方法将View添加到自身中而已。
我们之前已经分析过LayoutInflater的inflate方法,结论如下,在这里setContentView(int layoutResID)中执行的是第三条,也就是:View会添加到mContentParent中,且View的宽高是有效的。
技术分享
同样,如果我们去探究addView(view, params)的源码的话,它的逻辑也是一样的。

由上我们也容易明白,我们可以多次调用setContentView()来显示界面,因为会先removeAllViews。

PhoneWindow的installDecor方法

回过头,我们继续看上面PhoneWindow类的setContentView方法的第5行installDecor(),其源码如下:
  1. private void installDecor() {
  2. if (mDecor == null) {
  3. mDecor = generateDecor();
  4. mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
  5. mDecor.setIsRootNamespace(true);
  6. if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
  7. mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
  8. }
  9. }
  10. if (mContentParent == null) {
  11. //根据窗口的风格修饰,选择对应的修饰布局文件,并且将id为content的FrameLayout赋值给mContentParent
  12. mContentParent = generateLayout(mDecor);
  13. //......
  14. //初始化一堆属性值
  15. }
  16. }
抓重点分析,从第2到9行可以看出,首先判断mDecor对象是否为空(第一次调用时为空),如果为空则调用generateDecor()创建一个DecorView,然后设置一些属性。

我们看下PhoneWindow的generateDecor方法:
  1. protected DecorView generateDecor() {
  2. return new DecorView(getContext(), -1);
  3. }
可以看见generateDecor方法仅仅是new了一个DecorView的实例,但并没有对DecorView做任何处理。

回到installDecor方法继续往下看,当mContentParent对象为空时则调用generateLayout()方法去创建mContentParent对象。
我们看下generateLayout方法的源码:
  1. protected ViewGroup generateLayout(DecorView decor) {
  2. // Apply data from current theme.从xml主题中获取窗口相关的属性
  3. TypedArray a = getWindowStyle();
  4. //......
  5. //获取代码中对主题style的设置
  6. // Inflate the window decor.
  7. int layoutResource;
  8. int features = getLocalFeatures();
  9. //......
  10. //根据设定好的features值选择不同的窗口修饰布局文件,得到layoutResource值
  11. //把选中的窗口修饰布局文件添加到DecorView对象里,并且指定contentParent值
  12. View in = mLayoutInflater.inflate(layoutResource, null);
  13. decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
  14. mContentRoot = (ViewGroup) in;
  15. ViewGroup contentParent = (ViewGroup)findViewById(ID_android_CONTENT);
  16. if (contentParent == null) {
  17. throw new RuntimeException("Window couldn‘t find content container view");
  18. }
  19. //......
  20. //继续一堆属性设置,完事返回contentParent
  21. return contentParent;
  22. }
可以看见上面方法主要作用就是根据窗口的风格修饰类型为该窗口选择不同的窗口根布局文件,也即installDecor方法的实质就是:产生mDecor和mContentParent对象。

在这里顺带提一下:还记得我们平时写应用Activity时设置的theme或者feature吗(全屏、NoTitle等)?我们一般是不是通过XML的android:theme属性或者requestFeature()方法来设置的呢?譬如:
requestWindowFeature(Window.FEATURE_NO_TITLE);
android:theme="@android:style/Theme.NoTitleBar"
其实我们平时requestWindowFeature()设置的值就是在这里通过getLocalFeature()获取的,而android:theme属性是通过这里的getWindowStyle()获取的。    
由于这些逻辑是在调用setContentView方法之后执行的,所以要想在这里能够获取到对Window窗口属性的设置,必须在setContentView方法之前调用requestFeature()方法。

Callback的onContentChanged方法

setContentView方法的最后还会调用一个Callback接口的onContentChanged来通知对应的Activity组件视图内容发生了变化,我们先看下getCallback()方法。PhoneWindow没有重写Window中的这个方法,所以到抽象类Window中可以看到:
  1. /**
  2. * Return the current Callback interface for this window.
  3. */
  4. public final Callback getCallback() {
  5. return mCallback;
  6. }
这个mCallback在哪赋值的呢,继续看Window类发现有一个方法,如下:
  1. public void setCallback(Callback callback) {
  2. mCallback = callback;
  3. }
Window中的mCallback是通过这个方法赋值的,那就回想一下,Window是Activity的组合成员,那就是Activity调用这个方法了,回到Activity发现在Activity的attach方法中进行了设置,如下:
  1. final void attach(Context context, ActivityThread aThread,
  2. ......
  3. mWindow.setCallback(this);
  4. ......
  5. }
也就是说Activity类实现了Window的Callback接口,那就说明当Activity的布局改动时,即setContentView()或addContentView()方法执行完毕时就会调用该方法。
不过Activity的onContentChanged是个空方法,Google文档中的说明如下:
技术分享

总结

可以看出来setContentView整个过程主要是如何把Activity的布局文件或者java的View添加至窗口里,上面的过程可以重点概括为:
  • 创建一个DecorView的对象mDecor,该mDecor对象将作为整个应用窗口的根视图。
  • 依据Feature等style、theme创建不同的窗口修饰布局文件,并且通过findViewById获取Activity布局文件该存放的地方。
  • 将Activity的布局文件添加至id为content的FrameLayout内。
至此整个setContentView的主要流程就分析完毕。

















以上是关于setContentView加载解析过程源码分析的主要内容,如果未能解决你的问题,请参考以下文章

Android setContentView 加载布局源码解析

Android从xml加载到View对象过程解析

android setContentView()源码解析

android setContentView()源码解析

Activity加载view6.0源码分析---setContentView

Android XML布局文件解析过程源码解析