持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情
1 示例代码
继承Activity
和AppCompatActivity
,setContentView()
的流程会有区别,本文后续源码分析会对比它们的不同之处。
public class ContentViewActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_content_view);
}
}
public class ContentViewActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_content_view);
}
}
2 结构框架
此图列出了Activity
和AppCompatActivity
结构框架,可以先对比一下。
3 流程分析
3.1 Actiivty的setContentView()
//首先通过Activity的启动流程
ActivityThread.performLaunchActivity()
->Activity.attach();
->mWindow = new PhoneWindow();
->mInstrumentation.callActivityOnCreate();
//启动了一个Activity,得到了一个PhoneWindow对象
PhoneWindow.setContentView() //目的:创建一个DecorView,拿到Content
->installDecor();
->mDecor = generateDecor(-1);//创建DecorView
->new DecorView();
->mContentParent = generateLayout(mDecor);//生成一个Content
->设置styleable中的各种属性
->根据不同的属性特征选择layout
->layoutResource = R.layout.screen_simple;
//R.layout.screen_simple添加到DecorView中
->mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
->ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
//R.layout.activity_content_view渲染到mContentParent
->mLayoutInflater.inflate(layoutResID, mContentParent);
//...跟下边的AppCompatActiivty流程一样
3.2 AppCompatActiivty的setContentView()
setContentView->AppCompatDelegate.setContentView()
//获取一个DecorView
->ensureSubDecor();
->mSubDecor = createSubDecor();
->设置一些窗口属性
->ensureWindow();
->attachToWindow(((Activity) mHost).getWindow());//从Activity拿到PhoneWindow
->mWindow.getDecorView();//走PhoneWindow的getDecorView()
->installDecor();
->mDecor = generateDecor(-1);//创建DecorView
->new DecorView();
->mContentParent = generateLayout(mDecor);//生成一个Content
->设置styleable中的各种属性
->根据不同的属性特征选择layout
->layoutResource = R.layout.screen_simple;
//R.layout.screen_simple添加到DecorView中
->mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
->ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
->final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
R.id.action_bar_activity_content);
->final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
//把windowContentView(也就是R.layout.screen_simple中的content)的view添加到R.id.action_bar_activity_content中
->contentView.addView(windowContentView.getChildAt(0));
->windowContentView.setId(View.NO_ID);//将原始的content的id设为NO_ID
->contentView.setId(android.R.id.content);//action_bar_activity_content改为content
->mWindow.setContentView(subDecor);
//获取Content
->ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
//创建布局R.layout.activity_content_view
LayoutInflater.from(mContext).inflate(resId, contentParent);
->XmlResourceParser parser = res.getLayout(resource);
->inflate(parser, root, attachToRoot)
->merge布局
->rInflate(parser, root, inflaterContext, attrs, false);
->非merge布局
//通过反射创建View
->final View temp = createViewFromTag(root, name, inflaterContext, attrs);
->if (-1 == name.indexOf('.'))//SDK提供的View 如LinearLayout,不带包名,但是在onCreateView中会自动给加上包名
->view = onCreateView(context, parent, name, attrs);
->PhoneLayoutInflater.onCreateView(name, attrs);
->View view = createView(name, prefix, attrs);
->else//第三方的(非SDK)提供的View 如androidx.constraintlayout.widget.ConstraintLayout,带包名
->view = createView(context, name, null, attrs);
//通过反射创建View
->clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);
->constructor = clazz.getConstructor(mConstructorSignature);
->final View view = constructor.newInstance(args);
//递归创建子View
->rInflateChildren(parser, temp, attrs, true);
->rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
->final View view = createViewFromTag(parent, name, context, attrs);
->rInflateChildren(parser, view, attrs, true);
4 常见问题
4.1 Window有几类?哪些地方会创建Window?
3类
- PhoneWindow:
Activity
、Dialog
- Window:
Toast
- WindowManager:
PopupWindow
4.2 为什么requestWindowFeature()要在setContentView之前调用?
Activity.requestWindowFeature
方法里调用的是PhoneWindow.requestFeature(int featureId)
:
@Override
public boolean requestFeature(int featureId) {
if (mContentParentExplicitlySet) {
throw new AndroidRuntimeException("requestFeature() must be called before adding content");
}
//...省略代码
}
该方法里边首先会判断mContentParentExplicitlySet
这个变量,为true的时候就会抛出异常;而mContentParentExplicitlySet
在PhoneWindow.setContentView(int layoutResID)
中会被置为true:
@Override
public void setContentView(int layoutResID) {
//...省略代码
mContentParentExplicitlySet = true;
}
一旦setContentView
被调用,requestFeature
就会报错,所以必须先调用requestFeature
。
这样设计的原因是在PhoneWindow.setContentView
中生成mContentParentgenerateLayout(mDecor)()
的时候,需要通过各种窗口特征属性去设置布局样式。如果已经调用了setContentView
,窗口的各种属性已经设置好了,再去单独调Activity
的requestWindowFeature
方法,就没有意义了。
需要注意的一点,在Activity
和AppCompatActivity
中调用的方法不一样:
requestWindowFeature(Window.FEATURE_NO_TITLE);//Activity中调用
supportRequestWindowFeature(Window.FEATURE_NO_TITLE);//AppCompatActivity中调用
4.3 LayoutInflater的inflate方法的几个参数的作用是什么?
/**
* @param resource 要加载的xml布局文件id
* @param root 想要附着的父view
* @param attachToRoot 是否要附着到父view上
*/
inflate(int resource, ViewGroup root, boolean attachToRoot)
使用和影响:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="300dp"
android:background="@android:color/holo_blue_light"
android:gravity="center">
<TextView
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@android:color/holo_orange_light" />
</LinearLayout>
LinearLayout llRoot = findViewById(R.id.ll_root);
//方式1 成功 正常的将R.layout.layout_content_view_inflate添加到llRoot中去,
View inflateView = LayoutInflater.from(this).inflate(R.layout.layout_content_view_inflate, llRoot, true);
//方式2 报错,一个View只能有一个parent
View inflateView = LayoutInflater.from(this).inflate(R.layout.layout_content_view_inflate, llRoot, true);
llRoot.addView(inflateView);//上一行代码已经执行addView,这个时候如果重复调用addView,则会报错:The specified child already has a parent.
//方式3 成功 这样做是为了要布局layout_content_view_inflate的根节点的属性(宽高)有效,
//但是又不想让其处于某一容器中,然后在后边手动再将其添加到某一个容器中
View inflateView = LayoutInflater.from(this).inflate(R.layout.layout_content_view_inflate, llRoot, false);
llRoot.addView(inflateView);
//方式4 root=null 这个时候不管第三个参数attachToRoot是true还是false,显示效果都一样。
//因为layout_content_view_inflate的根节点的属性无效,只是包裹子View,但是子View(TextView)有效,因为子View是处于容器下的。
View inflateView = LayoutInflater.from(this).inflate(R.layout.layout_content_view_inflate, null, false);
llRoot.addView(inflateView);
方式1和方式3显示效果:
方式4显示效果
4.4 merge、include、ViewStub的作用和区别。
merge:
作用:merge 能被其他layout用包含进去,并不再另外生成ViewGroup容器。也就是说会减少一层layout到达优化layout的目的。
使用方法:用标签引入或者在代码中使用LayoutInflate.inflate()
注意:
1、merge标签必须用在根布局
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 子布局用merge会报错,merge标签必须用在根布局 -->
<!-- <merge>-->
<!-- -->
<!-- </merge>-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="200dp"
android:layout_height="20dp"
android:layout_marginVertical="5dp"
android:background="@android:color/holo_orange_light" />
<TextView
android:layout_width="200dp"
android:layout_height="20dp"
android:layout_marginVertical="5dp"
android:background="@android:color/holo_orange_light" />
<TextView
android:layout_width="200dp"
android:layout_height="20dp"
android:layout_marginVertical="5dp"
android:background="@android:color/holo_orange_light" />
</LinearLayout>
</merge>
2、因为merge标签并不是View
,所以在通过LayoutInflate.inflate()
方法渲染的时候,第二个参数必须指定一个父容器,且第三个参数必须为true,也就是必须为merge下的视图指定一个父亲节点。
View mergeView = LayoutInflater.from(this).inflate(R.layout.layout_content_view_merge, llRoot, true);
3、由于merge不是View
所以对merge标签设置的所有属性都是无效的。
include:
作用: include可以重复使用同一段xml文件,提高代码的重用性。也可以用来拆分xml布局,使得结果更加清晰。
注意:
1、include不能作为根元素,必须在ViewGroup
中使用。
2、可能会出现的findViewById(int)
空指针问题。
如果在include布局文件的根布局中设置了id=@+id/ll_include_root,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ll_include_root"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="include中的内容" />
</LinearLayout>
然后引用的时候也设置了id=@+id/ll_include,如下所示:
<include
android:id="@+id/ll_include"
layout="@layout/layout_content_view_include" />
那么在使用ll_include_root去findViewById
的时候就会报空指针,找不到根View
。
LinearLayout llInclude = findViewById(R.id.ll_include_root);
TextView tvContent = llInclude.findViewById(R.id.tv_content);
tvContent.setText("X");
原因是LayoutInflater
中rInflate
调用的parseInclude
方法中,如果使用include标签时设置了id,这个id就会覆盖layout根view中设置的id,从而找不到这个id。
final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
if (id != View.NO_ID) {
view.setId(id);
}
ViewStub:
作用:ViewStub
是一个轻量级的View
,它一个看不见的,不占布局位置,占用资源非常小的控件,具备懒加载能力,只有在显示的时候才会初始化。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ll_view_stub_root"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ViewStub中的内容" />
</LinearLayout>
<ViewStub
android:id="@+id/view_stub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout="@layout/layout_content_view_viewstub" />
ViewStub viewStub = findViewById(R.id.view_stub);
//使用inflate和setVisibility都可以
viewStub.inflate();
// viewStub.setVisibility(View.VISIBLE);
4.5 为什么在xml布局中使用SDK的View可以不带包名,使用第三方或者自定义的View需要带上包名?
源码中,在LayoutInflater
的createViewFromTag
中,首先根据标签中是否带.
去判断是不是SDK的view:
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
//...
if (-1 == name.indexOf('.')) {
//不带. 是SDK中的View
view = onCreateView(context, parent, name, attrs);
} else {
//第三方或者自定义View
view = createView(context, name, null, attrs);
}
//...
}
如果是SDK中的View
,然后调用onCreateView
方法,在onCreateView
方法中调用createView
会自动加上包名前缀。
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
关注木水小站 (zhangmushui.cn)和微信公众号【木水Code】,及时获取更多最新技术干货。