setContentView()背后的秘密你知道吗?

906 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情


1 示例代码

继承ActivityAppCompatActivitysetContentView()的流程会有区别,本文后续源码分析会对比它们的不同之处。

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 结构框架

此图列出了ActivityAppCompatActivity结构框架,可以先对比一下。

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类

  • PhoneWindowActivityDialog
  • WindowToast
  • WindowManagerPopupWindow

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的时候就会抛出异常;而mContentParentExplicitlySetPhoneWindow.setContentView(int layoutResID)中会被置为true:

@Override
public void setContentView(int layoutResID) {
    //...省略代码
    mContentParentExplicitlySet = true;
}

一旦setContentView被调用,requestFeature就会报错,所以必须先调用requestFeature

这样设计的原因是在PhoneWindow.setContentView中生成mContentParentgenerateLayout(mDecor)()的时候,需要通过各种窗口特征属性去设置布局样式。如果已经调用了setContentView,窗口的各种属性已经设置好了,再去单独调ActivityrequestWindowFeature方法,就没有意义了。

需要注意的一点,在ActivityAppCompatActivity中调用的方法不一样:

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");

原因是LayoutInflaterrInflate调用的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需要带上包名?

源码中,在LayoutInflatercreateViewFromTag中,首先根据标签中是否带.去判断是不是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】,及时获取更多最新技术干货。