【Android View源码分析(一)】setContentView加载视图机制深度分析
2017-08-14 23:42
615 查看
【大圣代的技术专栏 http://blog.csdn.net/qq_23191031 转载烦请注明出处,尊重他人劳动成功就是对您自己的尊重】
Ps:不喜欢看文字的可以直接到文字尾,看图说话。
从上面的文章中我们知道
# 2-1 Activity源码中的setContentView
经过阅读Android的源码发现,系统为我们提供了三个
那么
通过源码我们可以看到
而在
查看
一言不可就上图:
源码中的第一步就是验证
如果在初始化
再来看下PhoneWindow类的setContentView(View view)方法和setContentView(View view, ViewGroup.LayoutParams params)方法源码,如下:
看见没有,我们其实只用分析setContentView(View view, ViewGroup.LayoutParams params)方法即可,如果你在Activity中调运setContentView(View view)方法,实质也是调运setContentView(View view, ViewGroup.LayoutParams params),只是LayoutParams设置为了MATCH_PARENT而已。
所以直接分析setContentView(View view, ViewGroup.LayoutParams params)方法就行,可以看见该方法与setContentView(int layoutResID)类似,只是少了LayoutInflater将xml文件解析装换为View而已,这里直接使用View的addView方法追加道了当前mContentParent而已。
我在源码中发现了一个很重要的东西,请看第2677行!!!,这就在最根本上解释了:为什么要在
言归正传,
ps:怎么又一大堆,看来7.1.1的源码和5.1.1的差异真是不小啊。啥,Androdi5.1.1里面的长啥样?
不看不知道,一看吓一跳。看见没有,一共两行。这里就不展开讨论了…..
从整体角度来讲这个方法就是根据用户设置的风格、标签为窗口选择不同的主布局文件,DecorView做为根视图将该窗口根布局添加进去,然后获取id为content的FrameLayout返回给mContentParent对象。所以installDecor方法实质就是产生mDecor和mContentParent对象。 哎!我怎么没看见DecorView添加布局的代码呢?别急下边就告诉你怎么回事。
在进入这个方法时,系统就会调用
我们顺藤摸瓜找到属性位置 源码地址
所以这里就是解析我们为Activit设置theme的地方,至于theme一般可以在AndroidManifest.xml文件中设置。
接下来就到关键的部分了,2494-2510行:通过对features和mIsFloating的判断,获取不同的主布局文件为layoutResource进行赋值,值可以为R.layout.screen_custom_title;R.layout.screen_action_bar;等等。
经过上面的源码我们可以看到设置features,除了theme中设置的,我们还可以在代码中进行:
其实我们平时requestWindowFeature()设置的features值就是在这里通过getLocalFeature()获取的;而android:theme属性也是通过这里的getWindowStyle()获取的。两方式具体流程不同,但是效果是一样的。
所以这下你应该就明白在java文件设置Activity的属性时必须在setContentView方法之前调用requestFeature()方法的原因了吧。
我靠,我还是没看见DecorView添加布局的代码啊 ,这就来:
源码 2554行,进行了如下操作:
看名字是在进行资源文件的加载,具体是怎么操作的呢:
在源码1824行,系统将 layoutResource 所代表的主布局文件。添加到 DecorView 中,而在源码中第 2556行我们可以看到,系统又在DecorView中需找一个
而
随手贴几个布局文件加以证明:
R.layout.screen_simple:
R.layout.screen_simple_overlay_action_mode
同样在
最后
由此就组成了我们在《【Android 控件架构】详解Android控件架构与常用坐标系》一篇中提到的视图框架(图中contentView就是源码中的contentParent)
1. 在线源码地址
1. Android应用setContentView与LayoutInflater加载解析机制源码分析
2. Android 源码解析 之 setContentView
3. Android UI 窗口体系 —— 源码阅读
Ps:不喜欢看文字的可以直接到文字尾,看图说话。
1, 前言
在前面《【Android 控件架构】详解Android控件架构与常用坐标系》的文章中我们提到了setContentView()方法,当时只是匆匆带过,并没有阐明具体流程。而这篇文章就是从Activity中的
setContentView()方法出发结合上篇的视图框架,详细分析
setContentView()的工作原理。还是贴一张图复习一下吧。
从上面的文章中我们知道
setContentView()方法是用来设置ContentView布局地,当系统调用了
setContentView()方法所有的控件就得到了显示,但是你有想过Android系统是如何让xml文件加载到界面并显示出来的呢?
setContentView()中具体是如何实现的呢?就让我们在这些疑问来进入下面的探讨吧。
2 从setContentView说起(基于Api 25 Android 7.1.1)
本来是想基于Api 26来看的,可是后来才想起来 Android 8.0的源码还没发布。。。# 2-1 Activity源码中的setContentView
经过阅读Android的源码发现,系统为我们提供了三个
setContentView()的重载方法,他们都调用了
getWindow()中的
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()方法有事做什么的呢,咱们继续往下看。
2-2 关于窗口Window类的一些关系
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. */ // 如果返回为null表示,则表示当前Activity不在窗口上 public Window getWindow() { return mWindow; } ... mWindow = new PhoneWindow(this, window);
通过源码我们可以看到
getWindow()方法返回的就是PhoneWindow的实例对象(PhoneWindow是抽象类Window的唯一实现类 PhoneWindow在线源码地址)
public class PhoneWindow extends Window implements MenuBuilder.Callback { private final static String TAG = "PhoneWindow"; ... // This is the top-level view of the window, containing the window decor. private DecorView mDecor; // 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. private ViewGroup mContentParent; private ViewGroup mContentRoot; ... }
而在
PhoneWindow中我们看到了作为成员变量的
mDecor,(在Android 7.1.1中DecorView已经不再是PhoneWindow的内部类了,而且包都换了,有图有真相)。
查看
DecorView之后发现
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks,看见没有,
DecorView才是
Activity的根布局(root view),他继承了
FrameLayout负责
Activity视图的加载,而
DecorView本身则是由
PhoneWindow加载的。
PhoneWindow是如何加载
DecorView的呢,咱们带着问题继续往下看
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks { private static final String TAG = "DecorView"; private static final boolean DEBUG_MEASURE = false; private static final boolean SWEEP_OPEN_MENU = false; // The height of a window which has focus in DIP. private final static int DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP = 20; // The height of a window which has not in DIP. private final static int DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP = 5; .... }
一言不可就上图:
2-3 PhoneWindow中的setContentView方法
在Window类中
setContentView方法是抽象的,所以我们直接去看
PhonWindow类中关于
setContentView方法的实现过程
@Override 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) { //创建DecorView,并添加到mContentParent上 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 { //将要加载的资源添加到mContentParent上 mLayoutInflater.inflate(layoutResID, mContentParent); } mContentParent.requestApplyInsets(); final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { //回调通知表示完成界面加载 cb.onContentChanged(); } }
源码中的第一步就是验证
mContentParent是否为
null,如果为null则表示程序是第一次运行,执行
installDecor。如果不为null则会判断当前是否设置了FEATURE_CONTENT_TRANSITIONS(这个属性表示内容加载时需不需要过场动画,默认为false)。如果没有使用过场动画则移除
mContentParent中的所有view(所以说
setContentView方法可以多次调用,因为他会移除掉所有的控件);
如果在初始化
mContentParent之后,用户设置了启用转场动画则使用
Scene开启过度,否则
mLayoutInflater.inflate(layoutResID, mContentParent);将我们的资源文件通过
LayoutInflater对象转化为控件树添加到
mContentParent中。
再来看下PhoneWindow类的setContentView(View view)方法和setContentView(View view, ViewGroup.LayoutParams params)方法源码,如下:
422 @Override 423 public void setContentView(View view) { 424 setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); 425 }
@Override 428 public void setContentView(View view, ViewGroup.LayoutParams params) { 429 // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window 430 // decor, when theme attributes and the like are crystalized. Do not check the feature 431 // before this happens. 432 if (mContentParent == null) { 433 installDecor(); 434 } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { 435 mContentParent.removeAllViews(); 436 } 437 438 if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { 439 view.setLayoutParams(params); 440 final Scene newScene = new Scene(mContentParent, view); 441 transitionTo(newScene); 442 } else { 443 mContentParent.addView(view, params); 444 } 445 mContentParent.requestApplyInsets(); 446 final Callback cb = getCallback(); 447 if (cb != null && !isDestroyed()) { 448 cb.onContentChanged(); 449 } 450 mContentParentExplicitlySet = true; 451 }
看见没有,我们其实只用分析setContentView(View view, ViewGroup.LayoutParams params)方法即可,如果你在Activity中调运setContentView(View view)方法,实质也是调运setContentView(View view, ViewGroup.LayoutParams params),只是LayoutParams设置为了MATCH_PARENT而已。
所以直接分析setContentView(View view, ViewGroup.LayoutParams params)方法就行,可以看见该方法与setContentView(int layoutResID)类似,只是少了LayoutInflater将xml文件解析装换为View而已,这里直接使用View的addView方法追加道了当前mContentParent而已。
2-4 installDecor()方法 源码分析
2614 private void installDecor() { 2615 mForceDecorInstall = false; 2616 if (mDecor == null) { 2617 mDecor = generateDecor(-1); 2618 mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); 2619 mDecor.setIsRootNamespace(true); 2620 if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) { 2621 mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); 2622 } 2623 } else { 2624 mDecor.setWindow(this); 2625 } 2626 if (mContentParent == null) { //根据窗口的风格修饰,选择对应的修饰布局文件,并且将id为content的FrameLayout赋值给mContentParent 2627 mContentParent = generateLayout(mDecor); //...... 2674 } else { 2675 mTitleView = (TextView) findViewById(R.id.title); 2676 if (mTitleView != null) { //根据FEATURE_NO_TITLE隐藏,或者设置mTitleView的值 2677 if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) { 2678 final View titleContainer = findViewById(R.id.title_container); 2679 if (titleContainer != null) { 2680 titleContainer.setVisibility(View.GONE); 2681 } else { 2682 mTitleView.setVisibility(View.GONE); 2683 } 2684 mContentParent.setForeground(null); 2685 } else { 2686 mTitleView.setText(mTitle); 2687 } 2688 } 2689 }
我在源码中发现了一个很重要的东西,请看第2677行!!!,这就在最根本上解释了:为什么要在
setContentView()方法之前设置
requestWindowFeature(Window.FEATURE_NO_TITLE)才能不显示TitleActionBar部分,达到全屏的效果。
言归正传,
installDecor()方法一进来就判断
mDcor是否为空,为空怎么办创建一个喽,咦
generateDecor(-1)传一个 -1 是什么鬼???代码规范呢!Google也可以这么写代码么??……咳咳。
2263 protected DecorView generateDecor(int featureId) { //...... 2281 return new DecorView(context, featureId, this, getAttributes()); 2282 }
ps:怎么又一大堆,看来7.1.1的源码和5.1.1的差异真是不小啊。啥,Androdi5.1.1里面的长啥样?
protected DecorView generateDecor() { return new DecorView(getContext(), -1); }
不看不知道,一看吓一跳。看见没有,一共两行。这里就不展开讨论了…..
2-5 generateLayout()方法 源码分析
在源码 2626行,我们看到当mContentParent == null的时候使用
generateLayout(mDecor)方法创建一个
mContentParent出来。
generateLayout(mDecor)看名字好像倒是像用来设置layout的。
2284 protected ViewGroup generateLayout(DecorView decor) { 2285 // Apply data from current theme. //首先通过WindowStyle中设置的各种属性,对Window进行requestFeature或者setFlags 2287 TypedArray a = getWindowStyle(); 2288 //... 2299 mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false); 2300 int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR) 2301 & (~getForcedWindowFlags()); 2302 if (mIsFloating) { 2303 setLayout(WRAP_CONTENT, WRAP_CONTENT); 2304 setFlags(0, flagsToUpdate); 2305 } else { 2306 setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate); 2307 } 2309 if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) { 2310 requestFeature(FEATURE_NO_TITLE); 2311 } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) { 2312 // Don't allow an action bar if there is no title. 2313 requestFeature(FEATURE_ACTION_BAR); 2314 } //.... //...根据当前sdk的版本确定是否需要menukey 2413 WindowManager.LayoutParams params = getAttributes(); 2491 // Inflate the window decor. 2492 2493 int layoutResource; 2494 int features = getLocalFeatures(); //...... //根据设定好的features值选择不同的窗口修饰布局文件,得到layoutResource值 //把选中的窗口修饰布局文件添加到DecorView对象里,并且指定contentParent值 2495 // System.out.println("Features: 0x" + Integer.toHexString(features)); 2496 if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { 2497 layoutResource = R.layout.screen_swipe_dismiss; 2498 } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) { 2499 if (mIsFloating) { 2500 TypedValue res = new TypedValue(); 2501 getContext().getTheme().resolveAttribute( 2502 R.attr.dialogTitleIconsDecorLayout, res, true); 2503 layoutResource = res.resourceId; 2504 } else { 2505 layoutResource = R.layout.screen_title_icons; 2506 } 2507 // XXX Remove this once action bar supports these features. 2508 removeFeature(FEATURE_ACTION_BAR); 2509 // System.out.println("Title Icons!"); 2510 } else if { //...... 2552 2553 mDecor.startChanging(); //通知 开始改变 2554 mDecor.onResourcesLoaded(mLayoutInflater, layoutResource); 2555 2556 ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); //...... 2604 mDecor.finishChanging();//通知 改变完成 2605 2606 return contentParent; 2607 } }
从整体角度来讲这个方法就是根据用户设置的风格、标签为窗口选择不同的主布局文件,DecorView做为根视图将该窗口根布局添加进去,然后获取id为content的FrameLayout返回给mContentParent对象。所以installDecor方法实质就是产生mDecor和mContentParent对象。 哎!我怎么没看见DecorView添加布局的代码呢?别急下边就告诉你怎么回事。
在进入这个方法时,系统就会调用
getWindowStyle()在当前的Window的theme中获取我们的Window属性,对我们的Window设置各种requestFeature,setFlags等等。
getWindowStyle()为抽象类
Window提供的方法,具体源码如下:
665 public final TypedArray getWindowStyle() { 666 synchronized (this) { 667 if (mWindowStyle == null) { 668 mWindowStyle = mContext.obtainStyledAttributes( 669 com.android.internal.R.styleable.Window); 670 } 671 return mWindowStyle; 672 } 673 }
我们顺藤摸瓜找到属性位置 源码地址
<!-- The set of attributes that describe a Windows's theme. --> <declare-styleable name="Window"> <attr name="windowBackground" /> <attr name="windowContentOverlay" /> <attr name="windowFrame" /> <attr name="windowNoTitle" /> <attr name="windowFullscreen" /> <attr name="windowOverscan" /> <attr name="windowIsFloating" /> <attr name="windowIsTranslucent" /> <attr name="windowShowWallpaper" /> <attr name="windowAnimationStyle" /> <attr name="windowSoftInputMode" /> <attr name="windowDisablePreview" /> <attr name="windowNoDisplay" /> <attr name="textColor" /> <attr name="backgroundDimEnabled" /> <attr name="backgroundDimAmount" />
所以这里就是解析我们为Activit设置theme的地方,至于theme一般可以在AndroidManifest.xml文件中设置。
接下来就到关键的部分了,2494-2510行:通过对features和mIsFloating的判断,获取不同的主布局文件为layoutResource进行赋值,值可以为R.layout.screen_custom_title;R.layout.screen_action_bar;等等。
经过上面的源码我们可以看到设置features,除了theme中设置的,我们还可以在代码中进行:
//通过java文件设置: requestWindowFeature(Window.FEATURE_NO_TITLE); //通过xml文件设置: android:theme="@android:style/Theme.NoTitleBar"
其实我们平时requestWindowFeature()设置的features值就是在这里通过getLocalFeature()获取的;而android:theme属性也是通过这里的getWindowStyle()获取的。两方式具体流程不同,但是效果是一样的。
所以这下你应该就明白在java文件设置Activity的属性时必须在setContentView方法之前调用requestFeature()方法的原因了吧。
我靠,我还是没看见DecorView添加布局的代码啊 ,这就来:
源码 2554行,进行了如下操作:
2554 mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
看名字是在进行资源文件的加载,具体是怎么操作的呢:
1801 void onResourcesLoaded(LayoutInflater inflater, int layoutResource) { //...... 1813 final View root = inflater.inflate(layoutResource, null); //...... 1824 addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); //...... 1826 mContentRoot = (ViewGroup) root; //...... 1828 }
在源码1824行,系统将 layoutResource 所代表的主布局文件。添加到 DecorView 中,而在源码中第 2556行我们可以看到,系统又在DecorView中需找一个
ID_ANDROID_CONTENT布局赋值给
contentParent。
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
而
ID_ANDROID_CONTENT又是个什么东西呢?我在
Windows抽象类中找到了它的源码。注释说的很明确,每一个主布局都拥有id为
content的控件。通过
mContentRoot = (ViewGroup) root;我们可以清楚的知道,
layoutResource既为整个窗口的根布局。
/** * The ID that the main layout in the XML layout file should have. */ public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
随手贴几个布局文件加以证明:
R.layout.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>
R.layout.screen_simple_overlay_action_mode
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true"> <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" /> <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>
同样在
Windows抽象类中找到了
findViewByID方法的源码,
findViewByID的作用就是将在
DecoreView中需找
id为
content的
FragmentLayout赋值给
contentParent
1252 /** 1253 * Finds a view that was identified by the id attribute from the XML that 1254 * was processed in {@link android.app.Activity#onCreate}. This will 1255 * implicitly call {@link #getDecorView} for you, with all of the 1256 * associated side-effects. 1257 * 1258 * @return The view if found or null otherwise. 1259 */ 1260 @Nullable 1261 public View findViewById(@IdRes int id) { 1262 return getDecorView().findViewById(id); 1263 }
最后
generateLayout()的最后系统还会调用
Callback接口的成员函数
onContentChanged来通知对应的Activity组件视图内容发生了变化。至此Android
setContentView()方法分析完成。
3,总结
图片被缩小了不清楚,不要紧。请右键 - 在新标签中打开图片。由此就组成了我们在《【Android 控件架构】详解Android控件架构与常用坐标系》一篇中提到的视图框架(图中contentView就是源码中的contentParent)
4,参考:
如果说我比别人看得更远些,那是因为我站在了巨人的肩上1. 在线源码地址
1. Android应用setContentView与LayoutInflater加载解析机制源码分析
2. Android 源码解析 之 setContentView
3. Android UI 窗口体系 —— 源码阅读
相关文章推荐
- Android应用setContentView与LayoutInflater加载解析机制源码分析
- android应用程序窗口框架学习(2)-view绘制流程源代码解析-setContentView与LayoutInflater加载解析机制源码分析
- Android源码梳理(一):setContentView(...)与LayoutInflater的加载机制分析
- Android setContentView与LayoutInflater加载解析机制源码分析
- Android应用setContentView与LayoutInflater加载解析机制源码分析
- Android应用setContentView与LayoutInflater加载解析机制源码分析
- Android应用setContentView与LayoutInflater加载解析机制源码分析
- Android应用setContentView与LayoutInflater加载解析机制源码分析
- Android应用setContentView与LayoutInflater加载解析机制源码分析(超级棒!)
- Android应用setContentView与LayoutInflater加载解析机制源码分析(转载)
- [置顶] android源码分析——由SetContentView串起来的布局加载机制
- Android应用setContentView与LayoutInflater加载解析机制源码分析
- Android应用setContentView与LayoutInflater加载解析机制源码分析
- Android应用setContentView与LayoutInflater加载解析机制源码分析
- 源码分析 setContentView() 布局加载机制
- Android布局文件的加载过程分析:Activity.setContentView()源码分析
- Activity加载view6.0源码分析---setContentView
- Android窗口机制(二)Window,PhoneWindow,DecorView,setContentView源码理解
- android中布局和View创建的源码分析---setContentView
- 从setContentView方法分析Android加载布局流程