Android重学系列(一):setContentView

1,750 阅读9分钟

作为应用层开发的同学,对Android UI的显示绘制和事件响应应该不陌生,本文将在Android 13源码的基础上,来介绍setContentView的流程逻辑

前言

下面是我们业务开发同学经常接触到的模板代码,在Activity的onCreate方法中调用setContentView将XML编写好的activity_main.xml布局绑定到界面上

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

之所以说是模板代码,是因为大家都这么写,那可以不这么写吗?结论肯定是可以的

比如我们完全可以先不设置ContentView,等待业务网络请求回来后,再setContentView显示也是可以的,说到这其实还可以引申出布局优化,页面启动优化,如我们大型App的首页,任务繁重,UI的渲染肯定是极其耗时的,这个时候就可以先让setContentView显示一个较为简单的Layout,待各个业务初始化成功后,再一步一步显示出真实的UI出来,这也就是所谓的懒加载,是一个对用户友好,对我们KPI也友好的方式,不过这个太依赖编程者的功力和对业务的熟练程度。

到这里,应用层开发基本上就结束了,剩下要做的就是findViewById,和业务处理数据回显的操作。那当我们调用这个api背后到底发生了什么呢? 搜索Android源码,发现有两处地方调用了setContentView

image.png

AppCompatActivity继承自Activity,重写了setContentView,两者本质上都会调用到window的setContentView,区别在于AppCompatActivity将我们创建的布局进行了再一层的包装处理,下面我们分开探索其中的逻辑

Activity setContentView

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

这里通过getWindow获取到了一个Window对象,官方对Window的解释是:

顶层窗口和行为的抽象,当前接口的实例应用作添加到窗口管理器的顶级视图,它提供了标准的 UI 策略,例如背景、标题区域、默认键处理等。这个抽象类的唯一现有实现是 android.view.PhoneWindow

由此可知最终会调用PhoneWindow的setContentView方法,这里还有一个initWindowDecorActionBar的逻辑,主要是用来处理一些关于ActionBar的逻辑

 public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            installDecor();//当ContentParent为null时,去初始化DecorView
        } 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);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

这里先判断了mContentParent是否为null(后面我都会以ContentParent命名来介绍),是就调用installDecor去重新创建,否则就将我们传入的layoutResID对应的布局填入 mContentParent中。 这里还判断了hasFeature()来检测标志位,主要是作用于Activity的过渡动画,我们可以在Activity中通过requestWindowFeature等方式设置,如Activiy A跳转到Activity B,我们就可以getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS)来开启动画

回到mContentParent 那这个mContentParent是什么呢?又是怎么创建的?我们接着往下看installDecor()

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            ...
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
            ...
}

上面可以总结成2步:

第一步首先创建DecorView,第二步创建ContentParent,并将DecorView传入,最终ContentParent会被add 到DecorView中; 我们还是来看下DecorView的创建,发现其实很简单,就是直接new了一个

protected DecorView generateDecor(int featureId) {  
    ...       
    return new DecorView(context, featureId, this, getAttributes());   
}

然后我们再看看看DecorView的定义

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
}

发现DecorView其实就是一个FrameLayout,并且创建的时候将PhoneWindow传入了,由此就将DecorView和Window进行了联系,如根据Window的一些属性去设置View等

接下来我们再看ContentParent的创建

protected ViewGroup generateLayout(DecorView decor) {
        int layoutResource;
        int features = getLocalFeatures();
         if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
            layoutResource = R.layout.screen_title_icons;
        } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
                && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
            ...
        } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
            ...
        } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
            ...
        } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
            layoutResource = R.layout.screen_simple_overlay_action_mode;
        } else {
            layoutResource = R.layout.screen_simple;
        }
        mDecor.startChanging();
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        return contentParent;
}

这里会根据我们设置features的不同,加载不同的xml布局文件,如我们在Activity中通过requestWindowFeature等方式设置,就可改变getLocalFeatures获取的值,接着调用了onResourcesLoaded(),将上面加载的xml文件add到DecorView中

如果我们不对Features进行设置,则会走默认的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" />
</LinearLayout>

这里就有我们很熟悉的一个逻辑,即@android:id/content,开发中我们可能会经常看到findViewById(android.R.id.content)这样的代码,这里找到的就是ContentParent中的FrameLayout

然后我们再来看看onResourcesLoaded

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
        ...
        mDecorCaptionView = createDecorCaptionView(inflater);
        final View root = inflater.inflate(layoutResource, null);
        if (mDecorCaptionView != null) {
            if (mDecorCaptionView.getParent() == null) {
                addView(mDecorCaptionView,
                        new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
            }
            mDecorCaptionView.addView(root,
                    new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
        } else {
           addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
        mContentRoot = (ViewGroup) root;
        initializeElevation();
}

这里最终会根据mDecorCaptionView的创建结果,去决定是将上一步传入的xml文件id填充到哪,mDecorCaptionView的创建依赖于之前的fetures,如浮窗、dialog等,如果不是这些类型,则直接将我们的布局add到DecorView中

至此,经过了DecorView的创建,和ContentParent的创建,我们自己书写的布局就被添加到顶层View中了

image.png

此时我们已经得到了完整的DecorView,其中就包含了我们的布局,到这我们只是走完了创建这个步骤,那这个顶层View是如何绑定到Window中的呢,这得回归到ActivityThread中的handleResumeActivity方法

public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
            boolean isForward, String reason) {
        ...
        boolean willBeVisible = !a.mStartedActivity;
        if (!willBeVisible) {
            willBeVisible = ActivityClient.getInstance().willActivityBeVisible(
                    a.getActivityToken());
        }
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            if (r.mPreserveWindow) {
               ...
            }
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l);
                } else {
                    ...
                }
            }
           ...
        } else if (!willBeVisible) {
            if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
            r.hideForNow = true;
        }
    if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
        if (r.activity.mVisibleFromClient) {
                //使View可见
                r.activity.makeVisible();
            }
    }
}

这里会先判断window的属性,如之前是否add过,是否有经历onStart方法,是否已经被Finished,最终会调用ViewManager.addView,将DecorView填充到Window中,这里的ViewManager实例就是Activity中的mWindowManager对象,实现类是WindowManagerImpl,然后最终会调用到这里的addView方法

这里有一个逻辑是在调用WindowManagerImpl.addView之前,先设置View不可见,添加过后,再设置为可见

public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyTokens(params);
        mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
                mContext.getUserId());
    }

WindowManagerImpl中的addView最终会调用到WindowManagerGlobal中的addView

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow, int userId) {
    
            ...
            if (windowlessSession == null) {
                root = new ViewRootImpl(view.getContext(), display);
            } else {
                root = new ViewRootImpl(view.getContext(), display,
                        windowlessSession);
            }

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);

            try {
                root.setView(view, wparams, panelParentView, userId);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        
    }

上面根据条件最终创建了ViewRootImpl,然后将DecorView设置进去, ViewRootImpl是管理DecorView和WMS的桥梁,用来收发一些WMS的通知,每次addView()添加窗口时,都会创建一个新的ViewRootImpl


public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
            int userId) {
     ...
     requestLayout();
     try {
            ...
            res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), userId,
                            mInsetsController.getRequestedVisibilities(), inputChannel, mTempInsets,
                            mTempControls);
        } catch (RemoteException e) {
            。。。
        }
         switch (res) {
                        case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
                        case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
                            throw new WindowManager.BadTokenException(
                                    "Unable to add window -- token " + attrs.token
                                    + " is not valid; is your activity running?");
                        case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
                            throw new WindowManager.BadTokenException(
                                    "Unable to add window -- token " + attrs.token
                                    + " is not for an application");
                        case WindowManagerGlobal.ADD_APP_EXITING:
                            throw new WindowManager.BadTokenException(
                                    "Unable to add window -- app for token " + attrs.token
                                    + " is exiting");

                        。。。
                    }


}

可以看到,这里通过和WMS远程调用,mWindowSession.addToDisplayAsUser最终会调用到WindowManagerService的addWindow 才最终将PhoneWindow显示在屏幕上,同时可以取得返回值,后面根据这些返回值,去抛出了一系列异常,相信其中的一些Exception我们并不陌生,如Unable to add window -- token

这里值得注意的是在调用WMS方法之前,调用了requestLayout,由此展开了View的measure、layout、draw三件套方法流程,完成了视图最终在屏幕上的显示

到这里,我们明白了setContentView后,我们的布局是怎么加载到DecorView中,同时DecorView又是怎么和Window进行绑定,然后Window又是何时以怎样的方式将DecorView呈现给用户的。

AppcompatActivity setContentView

接下来我们再回到前面说的AppcompatActivity的setContentView(),看源码得知AppcompatActivity将操作给了代理类AppCompatDelegateImpl,我们找到AppCompatDelegateImpl的setContentView方法

public void setContentView(int resId) {
    ensureSubDecor();
    ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mAppCompatWindowCallback.getWrapped().onContentChanged();
}

与之前Activity不同的是,这里通过findViewById的方式直接找到了ContentParent,并且将当前我们自己写的布局add进去了.知道了之前的流程,我们知道会先创建DecorView,接着创建ContentParent,那这里是怎么创建的呢,我们接着看ensureSubDecor()

private void ensureSubDecor() {
    if (!mSubDecorInstalled) {
        mSubDecor = createSubDecor();
        mSubDecorInstalled = true;
     }
}

然后看createSubDecor()

private ViewGroup createSubDecor() {
    ...
    mWindow.getDecorView();
    ViewGroup subDecor = null;

    if (!mWindowNoTitle) {
        if (mIsFloating) {
            。。。
            subDecor = (ViewGroup) inflater.inflate(
                        R.layout.abc_dialog_title_material, null);
        } else {
            。。。
            subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
        }
    }
    if (subDecor == null) {
            throw new IllegalArgumentException(
                    "AppCompat does not support the current theme features: { "
                            + "windowActionBar: " + mHasActionBar
                            + ", windowActionBarOverlay: "+ mOverlayActionBar
                            + ", android:windowIsFloating: " + mIsFloating
                            + ", windowActionModeOverlay: " + mOverlayActionMode
                            + ", windowNoTitle: " + mWindowNoTitle
                            + " }");
    }
    final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
        if (windowContentView != null) {
            while (windowContentView.getChildCount() > 0) {
                final View child = windowContentView.getChildAt(0);
                windowContentView.removeViewAt(0);
                contentView.addView(child);
            }
            windowContentView.setId(View.NO_ID);
            contentView.setId(android.R.id.content);
            if (windowContentView instanceof FrameLayout) {
                ((FrameLayout) windowContentView).setForeground(null);
            }
        }
        mWindow.setContentView(subDecor);
}

可以发现,这里默认调了1次mWindow.getDecorView() 我们来看看getDecorView的实现

 public final @NonNull View getDecorView() {
        if (mDecor == null || mForceDecorInstall) {
            installDecor();
        }
        return mDecor;
    }

看到这里就明白了,getDecorView也是会间接的去调用了installDecor,去完成DecorView的创建和ContentParent的创建工作,那我们AppcompatActivity相比于Activity有其它不同吗?答案是有的,继续从createSubDecor往下看,发现会根据window的一些属性,例如是否悬浮弹窗、是否有标题主题等等,去加载不同的xml布局得到subDecorView,到这里其实从变量命名也可以看出,subDecor最终也会是DecorView的一个子View,假设我们设置的属性命中了abc_screen_toolbar.xml,布局如下

<androidx.appcompat.widget.ActionBarOverlayLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/decor_content_parent"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

//源码这里是include的,这里为了直观,直接copy进来
<androidx.appcompat.widget.ContentFrameLayout
        android:id="@id/action_bar_activity_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:foregroundGravity="fill_horizontal|top"
        android:foreground="?android:attr/windowContentOverlay" />

    <androidx.appcompat.widget.ActionBarContainer
        android:id="@+id/action_bar_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        style="?attr/actionBarStyle"
        android:touchscreenBlocksFocus="true"
        android:keyboardNavigationCluster="true"
        android:gravity="top">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/action_bar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:navigationContentDescription="@string/abc_action_bar_up_description"
            style="?attr/toolbarStyle"/>

        <androidx.appcompat.widget.ActionBarContextView
            android:id="@+id/action_context_bar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:visibility="gone"
            android:theme="?attr/actionBarTheme"
            style="?attr/actionModeStyle"/>

    </androidx.appcompat.widget.ActionBarContainer>

</androidx.appcompat.widget.ActionBarOverlayLayout>

这个布局默认给我们加了Toolbar相关的属性,是我们通过Theme设置或者requestFeture来设置的, 注意其中的id=action_bar_activity_content的ContentFrameLayout,后面会有使用到,那这个subDecor又是怎样和DecorView关联起来的呢?接着往下看

ViewGroup subDecor = null;

    if (!mWindowNoTitle) {
        if (mIsFloating) {
            。。。
            subDecor = (ViewGroup) inflater.inflate(
                        R.layout.abc_dialog_title_material, null);
        } else {
            。。。
            subDecor = (ViewGroup) LayoutInflater.from(themedContext)
                        .inflate(R.layout.abc_screen_toolbar, null);
        }
    } else {
        ...
    }
    if (subDecor == null) {
            throw new IllegalArgumentException(
                    "AppCompat does not support the current theme features: { "
                            + "windowActionBar: " + mHasActionBar
                            + ", windowActionBarOverlay: "+ mOverlayActionBar
                            + ", android:windowIsFloating: " + mIsFloating
                            + ", windowActionModeOverlay: " + mOverlayActionMode
                            + ", windowNoTitle: " + mWindowNoTitle
                            + " }");
    }

这里有去判断subDecor的生成情况,如果是null则会抛出一个异常,这个异常我们应该似曾相识,比如我们项目里面的Activiy继承自AppcompatActivity,但是又没有使用对应的Appcompat下的主题,就会报这个错误

我们接着来看DecorViewsubDecor关联的点

final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
                R.id.action_bar_activity_content);

        final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
        if (windowContentView != null) {
            while (windowContentView.getChildCount() > 0) {
                final View child = windowContentView.getChildAt(0);
                windowContentView.removeViewAt(0);
                contentView.addView(child);
            }
            windowContentView.setId(View.NO_ID);
            contentView.setId(android.R.id.content);
            if (windowContentView instanceof FrameLayout) {
                ((FrameLayout) windowContentView).setForeground(null);
            }
        }

        mWindow.setContentView(subDecor);

这里发现会先找出一个ContentFrameLayout,就是我们上面提到的这个id为action_bar_activity_content的布局,然后又将DecorView中将ContentParent找了出来,然后看下面代码

if (windowContentView != null) {
            while (windowContentView.getChildCount() > 0) {
                final View child = windowContentView.getChildAt(0);
                windowContentView.removeViewAt(0);
                contentView.addView(child);
            }
            windowContentView.setId(View.NO_ID);
            contentView.setId(android.R.id.content);
            if (windowContentView instanceof FrameLayout) {
                ((FrameLayout) windowContentView).setForeground(null);
            }
        }

这段代码主要做的事是,将ContentParent中的子View都迁移到我们上面从subDecor中找出的ContentFrameLayout中,然后将当前ContentFrameLayout设置为android.R.id.content新的id,之前的ContentParent设置id为NO_ID,然后最后调用mWindow.setContentView(subDecor);,将subDecor add到之前的ContentParent。秒呀,来了个乾坤大挪移,完成了View的包装和替换,最终我们的Activity的页面视图结构会变成这样

image.png

至此,我们的setContentView的流程分析就结束了,从源码中我们可以学习很多知识点,如代理模式,如AppCompatActivity中的兼容替换逻辑,如View的容器化模式等等,理解这些,都可以让我们的编程效率得到提升