其实之所以要说这个话题有几个原因:
-
理解xml等控件是咋被显示的原理,通常大家写代码都是直接在onCreate里setContentView就完事,没怎么关注其实现原理。
-
前面分析《Android触摸屏事件派发机制详解与源码分析三(Activity篇)》时提到了一些关于布局嵌套的问题,当时没有深入解释。
所以接下来主要分析的就是View或者ViewGroup对象是如何添加至应用程序界面(窗口)显示的。我们准备从Activity的setContentView方法开始来说(因为默认Activity中放入我们的xml或者Java控件是通过setContentView方法来操作的,当调运了setContentView所有的控件就得到了显示)。
【工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重分享成果】
2 Android5.1.1(API 22)从Activity的setContentView方法说起
2-1 Activity的setContentView方法解析
Activity的源码中提供了三个重载的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方法,然后调运Activity的initWindowDecorActionBar方法,关于initWindowDecorActionBar方法后面准备写一篇关于Android ActionBar原理解析的文章,所以暂时跳过不解释。
2-2 关于窗口Window类的一些关系
在开始分析Activity组合对象Window的setContentView方法之前请先明确如下关系(前面分析《Android触摸屏事件派发机制详解与源码分析三(Activity篇)》时也有说过)。
看见上面图没?Activity中有一个成员为Window,其实例化对象为PhoneWindow,PhoneWindow为抽象Window类的实现类。
这里先简要说明下这些类的职责:
-
Window是一个抽象类,提供了绘制窗口的一组通用API。
-
PhoneWindow是Window的具体继承实现类。而且该类内部包含了一个DecorView对象,该DectorView对象是所有应用窗口(Activity界面)的根View。
-
DecorView是PhoneWindow的内部类,是FrameLayout的子类,是对FrameLayout进行功能的修饰(所以叫DecorXXX),是所有应用窗口的根View 。
依据面向对象从抽象到具体我们可以类比上面关系就像如下:
Window是一块电子屏,PhoneWindow是一块手机电子屏,DecorView就是电子屏要显示的内容,Activity就是手机电子屏安装位置。
2-2 窗口PhoneWindow类的setContentView方法
我们可以看见Window类的setContentView方法都是抽象的。所以我们直接先看PhoneWindow类的setContentView(int layoutResID)方法源码,如下:
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);
}
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
可以看见,第五行首先判断mContentParent是否为null,也就是第一次调运);如果是第一次调用,则调用installDecor()方法,否则判断是否设置FEATURE_CONTENT_TRANSITIONS Window属性(默认false),如果没有就移除该mContentParent内所有的所有子View;接着16行mLayoutInflater.inflate(layoutResID, mContentParent);
将我们的资源文件通过LayoutInflater对象转换为View树,并且添加至mContentParent视图中(其中mLayoutInflater是在PhoneWindow的构造函数中得到实例对象的LayoutInflater.from(context);
)。
再来看下PhoneWindow类的setContentView(View view)方法和setContentView(View view, ViewGroup.LayoutParams params)方法源码,如下:
@Override
public void setContentView(View view) {
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
// 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)) {
view.setLayoutParams(params);
final Scene newScene = new Scene(mContentParent, view);
transitionTo(newScene);
} else {
mContentParent.addView(view, params);
}
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
看见没有,我们其实只用分析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而已。
所以说在我们的应用程序里可以多次调用setContentView()来显示界面,因为会removeAllViews。
2-3 窗口PhoneWindow类的installDecor方法
回过头,我们继续看上面PhoneWindow类setContentView方法的第6行installDecor();代码,在PhoneWindow中查看installDecor源码如下:
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
}
if (mContentParent == null) {
//根据窗口的风格修饰,选择对应的修饰布局文件,并且将id为content的FrameLayout赋值给mContentParent
mContentParent = generateLayout(mDecor);
//…
//初始化一堆属性值
}
}
我勒个去!又是一个死长的方法,抓重点分析吧。第2到9行可以看出,首先判断mDecor对象是否为空,如果为空则调用generateDecor()创建一个DecorView(该类是
FrameLayout子类,即一个ViewGroup视图),然后设置一些属性,我们看下PhoneWindow的generateDecor方法,如下:
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
可以看见generateDecor方法仅仅是new一个DecorView的实例。
回到installDecor方法继续往下看,第10行开始到方法结束都需要一个if (mContentParent == null)
判断为真才会执行,当mContentParent对象不为空则调用generateLayout()方法去创建mContentParent对象。所以我们看下generateLayout方法源码,如下:
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
TypedArray a = getWindowStyle();
//…
//依据主题style设置一堆值进行设置
// Inflate the window decor.
int layoutResource;
int features = getLocalFeatures();
//…
//根据设定好的features值选择不同的窗口修饰布局文件,得到layoutResource值
//把选中的窗口修饰布局文件添加到DecorView对象里,并且指定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”);
}
//…
//继续一堆属性设置,完事返回contentParent
return contentParent;
}
可以看见上面方法主要作用就是根据窗口的风格修饰类型为该窗口选择不同的窗口根布局文件。mDecor做为根视图将该窗口根布局添加进去,然后获取id为content的FrameLayout返回给mContentParent对象。所以installDecor方法实质就是产生mDecor和mContentParent对象。
在这里顺带提一下:还记得我们平时写应用Activity时设置的theme或者feature吗(全屏啥的,NoTitle等)?我们一般是不是通过XML的android:theme属性或者java的requestFeature()方法来设置的呢?譬如:
通过java文件设置:
requestWindowFeature(Window.FEATURE_NO_TITLE);
通过xml文件设置:
android:theme=“@android:style/Theme.NoTitleBar”
对的,其实我们平时requestWindowFeature()设置的值就是在这里通过getLocalFeature()获取的;而android:theme属性也是通过这里的getWindowStyle()获取的。
所以这下你应该就明白在java文件设置Activity的属性时必须在setContentView方法之前调用requestFeature()方法的原因了吧。
我们继续关注一下generateLayout方法的layoutResource变量赋值情况。因为它最终通过View in = mLayoutInflater.inflate(layoutResource, null);
和decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
将in添加到PhoneWindow的mDecor对象。为例验证这一段代码分析我们用一个实例来进行说明,如下是一个简单的App主要代码:
AndroidManifest.xml文件
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android=“http://schemas.android.com/apk/res/android”
package=“com.yanbober.myapplication” >
<application
…
//看重点,我们将主题设置为NoTitleBar
android:theme=“@android:style/Theme.Black.NoTitleBar” >
…
主界面布局文件:
<RelativeLayout xmlns:android=“http://schemas.android.com/apk/res/android”
android:layout_width=“match_parent”
android:layout_height=“match_parent”>
<TextView android:text=“@string/hello_world”
android:layout_width=“wrap_content”
android:layout_height=“wrap_content” />
APP运行界面:
看见没有,上面我们将主题设置为NoTitleBar,所以在generateLayout方法中的layoutResource变量值为R.layout.screen_simple
,所以我们看下系统这个screen_simple.xml布局文件,如下:
<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” />
布局中,一般会包含ActionBar,Title,和一个id为content的FrameLayout,这个布局是NoTitle的。
再来看下上面这个App的hierarchyviewer图谱,如下:
看见了吧,通过这个App的hierarchyviewer和系统screen_simple.xml文件比较就验证了上面我们分析的结论,不再做过多解释。
然后回过头可以看见上面PhoneWindow类的setContentView方法最后通过调运mLayoutInflater.inflate(layoutResID, mContentParent);
或者mContentParent.addView(view, params);
语句将我们的xml或者java View插入到了mContentParent(id为content的FrameLayout对象)ViewGroup中。最后setContentView还会调用一个Callback接口的成员函数onContentChanged来通知对应的Activity组件视图内容发生了变化。
2-4 Window类内部接口Callback的onContentChanged方法
上面刚刚说了PhoneWindow类的setContentView方法中最后调运了onContentChanged方法。我们这里看下setContentView这段代码,如下:
public void setContentView(int layoutResID) {
…
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
看着没有,首先通过getCallback获取对象cb(回调接口),PhoneWindow没有重写Window的这个方法,所以到抽象类Window中可以看到:
/**
- Return the current Callback interface for this window.
*/
public final Callback getCallback() {
return mCallback;
}
这个mCallback在哪赋值的呢,继续看Window类发现有一个方法,如下:
public void setCallback(Callback callback) {
mCallback = callback;
}
Window中的mCallback是通过这个方法赋值的,那就回想一下,Window又是Activity的组合成员,那就是Activity一定调运这个方法了,回到Activity发现在Activity的attach方法中进行了设置,如下:
final void attach(Context context, ActivityThread aThread,
…
mWindow.setCallback(this);
…
}
也就是说Activity类实现了Window的Callback接口。那就是看下Activity实现的onContentChanged方法。如下:
public void onContentChanged() {
}
咦?onContentChanged是个空方法。那就说明当Activity的布局改动时,即setContentView()或者addContentView()方法执行完毕时就会调用该方法。
所以当我们写App时,Activity的各种View的findViewById()方法等都可以放到该方法中,系统会帮忙回调。
2-5 setContentView源码分析总结
可以看出来setContentView整个过程主要是如何把Activity的布局文件或者java的View添加至窗口里,上面的过程可以重点概括为:
-
创建一个DecorView的对象mDecor,该mDecor对象将作为整个应用窗口的根视图。
-
依据Feature等style theme创建不同的窗口修饰布局文件,并且通过findViewById获取Activity布局文件该存放的地方(窗口修饰布局文件中id为content的FrameLayout)。
-
将Activity的布局文件添加至id为content的FrameLayout内。
至此整个setContentView的主要流程就分析完毕。你可能这时会疑惑,这么设置完一堆View关系后系统是怎么知道该显示了呢?下面我们就初探一下关于Activity的setContentView在onCreate中如何显示的(声明一下,这里有些会暂时直接给出结论,该系列文章后面会详细分析的)。
2-6 setContentView完以后Activity显示界面初探
这一小部分已经不属于sentContentView的分析范畴了,只是简单说明setContentView之后怎么被显示出来的(注意:Activity调运setContentView方法自身不会显示布局的)。
记得前面有一篇文章《Android异步消息处理机制详解及源码分析》的3-1-2小节说过,一个Activity的开始实际是ActivityThread的main方法(至于为什么后面会写文章分析,这里站在应用层角度先有这个概念就行)。
那在这一篇我们再直接说一个知识点(至于为什么后面会写文章分析,这里站在应用层角度先有这个概念就行)。
当启动Activity调运完ActivityThread的main方法之后,接着调用ActivityThread类performLaunchActivity来创建要启动的Activity组件,在创建Activity组件的过程中,还会为该Activity组件创建窗口对象和视图对象;接着Activity组件创建完成之后,通过调用ActivityThread类的handleResumeActivity将它激活。
所以我们先看下handleResumeActivity方法一个重点,如下:
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume) {
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
…
// TODO Push resumeArgs into the activity for consideration
ActivityClientRecord r = performResumeActivity(token, clearHide);
if (r != null) {
…
// If the window hasn’t yet been added to the window manager,
// and this guy didn’t finish itself or start another activity,
// then go ahead and add the window.
…
// If the window has already been added, but during resume
// we started another activity, then don’t yet make the
// window visible.
…
// The window is now visible if it has been added, we are not
// simply finishing, and we are not starting another activity.
if (!r.activity.mFinished && willBeVisible
&& r.activity.mDecor != null && !r.hideForNow) {
…
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
}
…
} else {
// If an exception was thrown when trying to resume, then
// just end this activity.
…
}
}
看见r.activity.makeVisible();
语句没?调用Activity的makeVisible方法显示我们上面通过setContentView创建的mDecor视图族。所以我们看下Activity的makeVisible方法,如下:
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
看见没有,通过DecorView(FrameLayout,也即View)的setVisibility方法将View设置为VISIBLE,至此显示出来。
到此setContentView的完整流程分析完毕。
【工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重分享成果】
3 Android5.1.1(API 22)看看LayoutInflater机制原理
上面在分析setContentView过程中可以看见,在PhoneWindow的setContentView中调运了mLayoutInflater.inflate(layoutResID, mContentParent);
,在PhoneWindow的generateLayout中调运了View in = mLayoutInflater.inflate(layoutResource, null);
,当时我们没有详细分析,只是告诉通过xml得到View对象。现在我们就来分析分析这一问题。
3-1 通过实例引出问题
在开始之前我们先来做一个测试,我们平时最常见的就是ListView的Adapter中使用LayoutInflater加载xml的item布局文件,所以咱们就以ListView为例,如下:
省略掉Activity代码等,首先给出Activity的布局文件,如下:
<LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”
android:layout_width=“match_parent”
android:layout_height=“match_parent”
android:orientation=“vertical”>
<ListView
android:id=“@+id/listview”
android:dividerHeight=“5dp”
android:layout_width=“match_parent”
android:layout_height=“match_parent”>
给出两种不同的ListView的item布局文件。
textview_layout.xml文件:
<?xml version="1.0" encoding="utf-8"?><TextView xmlns:android=“http://schemas.android.com/apk/res/android”
android:layout_width=“100dp”
android:layout_height=“40dp”
android:text=“Text Test”
android:background=“#ffa0a00c”/>
textview_layout_parent.xml文件:
<?xml version="1.0" encoding="utf-8"?><LinearLayout
android:layout_height=“wrap_content”
android:layout_width=“wrap_content”
xmlns:android=“http://schemas.android.com/apk/res/android”>
<TextView xmlns:android=“http://schemas.android.com/apk/res/android”
android:layout_width=“100dp”
android:layout_height=“40dp”
android:text=“Text Test”
android:background=“#ffa0a00c”/>
ListView的自定义Adapter文件:
public class InflateAdapter extends BaseAdapter {
private LayoutInflater mInflater = null;
public InflateAdapter(Context context) {
mInflater = LayoutInflater.from(context);
}
@Override
public int getCount() {
return 8;
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
//说明:这里是测试inflate方法参数代码,不再考虑性能优化等TAG处理
return getXmlToView(convertView, position, parent);
}
private View getXmlToView(View convertView, int position, ViewGroup parent) {
View[] viewList = {
mInflater.inflate(R.layout.textview_layout, null),
// mInflater.inflate(R.layout.textview_layout, parent),
mInflater.inflate(R.layout.textview_layout, parent, false),
// mInflater.inflate(R.layout.textview_layout, parent, true),
mInflater.inflate(R.layout.textview_layout, null, true),
mInflater.inflate(R.layout.textview_layout, null, false),
mInflater.inflate(R.layout.textview_layout_parent, null),
// mInflater.inflate(R.layout.textview_layout_parent, parent),
mInflater.inflate(R.layout.textview_layout_parent, parent, false),
// mInflater.inflate(R.layout.textview_layout_parent, parent, true),
mInflater.inflate(R.layout.textview_layout_parent, null, true),
mInflater.inflate(R.layout.textview_layout_parent, null, false),
};
convertView = viewList[position];
return convertView;
}
}
当前代码运行结果:
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
最后看一下学习需要的所有知识点的思维导图。在刚刚那份学习笔记里包含了下面知识点所有内容!文章里已经展示了部分!如果你正愁这块不知道如何学习或者想提升学习这块知识的学习效率,那么这份学习笔记绝对是你的秘密武器!
一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算
12154917214)]
[外链图片转存中…(img-2JkDd585-1712154917215)]
[外链图片转存中…(img-I2QHXqyg-1712154917215)]
[外链图片转存中…(img-ZpK09vx8-1712154917215)]
[外链图片转存中…(img-UMrrXCI4-1712154917216)]
[外链图片转存中…(img-6r1XpPKp-1712154917216)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-XqvDmJjE-1712154917216)]
最后看一下学习需要的所有知识点的思维导图。在刚刚那份学习笔记里包含了下面知识点所有内容!文章里已经展示了部分!如果你正愁这块不知道如何学习或者想提升学习这块知识的学习效率,那么这份学习笔记绝对是你的秘密武器!
[外链图片转存中…(img-y3bmZwHd-1712154917217)]
一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算