UI绘制流程-从setContentView说起

平时我们在写 Android UI 时,我们都会通过 setContentVoew 方法,将我们的布局资源交给 Activity,让它去进行关联 window,解析 布局,渲染 视图 ,最终在屏幕上展现出了我们需要的效果。上一节我们分析了 Activity 的启动流程,知道了新 Activity 最终都会通过 AMSClientLifecycleManager 的相互调度依次执行 attachperformCreateperformStartperformResume 的四个方法,下面我们就按照这个流程来聊聊布局的 绑定解析渲染 的流程。

附:Activity 的启动流程

Window的创建

Activity 的创建同时伴随着 Window 的创建,并且为其二者建立相应的联系。在 AndroidPhoneWindowWindow 的唯一实现类,系统实例化的Window对象实际是它。在其之上会创建 DecorView,它是窗口的视图的具体载体,也是我们平时说的根布局。通常我们通过 setContentView 方法设置我们自己的视图布局,它会被添加到 DecorView 中的一个叫做 ContentParent 的子View 中。

这里我们直接来看看 Window 的实例化过程。

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);
		// 代码 1
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
    
    	
        if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
            mWindow.setSoftInputMode(info.softInputMode);
        }
        if (info.uiOptions != 0) {
            mWindow.setUiOptions(info.uiOptions);
        }
    
    	// 代码 2
        mUiThread = Thread.currentThread();

        mMainThread = aThread;
        mInstrumentation = instr;
        mToken = token;
        mAssistToken = assistToken;
        mIdent = ident;
        mApplication = application;
        mIntent = intent;
        mReferrer = referrer;
        mComponent = intent.getComponent();
        mActivityInfo = info;
        mTitle = title;
        mParent = parent;
        mEmbeddedID = id;
        mLastNonConfigurationInstances = lastNonConfigurationInstances;
        if (voiceInteractor != null) {
            if (lastNonConfigurationInstances != null) {
                mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor;
            } else {
                mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this,
                        Looper.myLooper());
            }
        }

    	// 代码 3
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        if (mParent != null) {
            mWindow.setContainer(mParent.getWindow());
        }
        mWindowManager = mWindow.getWindowManager();
        mCurrentConfig = config;

        mWindow.setColorMode(info.colorMode);

        setAutofillOptions(application.getAutofillOptions());
        setContentCaptureOptions(application.getContentCaptureOptions());
    }

上述代码是 Activity 的 attach 方法,它是 Activity 被实例化后,第一个执行的方法(源码 ActivityThread#performLaunchActivity 方法中),可以看到这段代码主要完成了以下工作:

  1. mWindow 的创建,并且和它建立相应的联系,各种 callback 设置方法。
  2. 给 Activity 设置初始化参数,比如 Intent、UIThread、Application等。
  3. 给 mWindow 设置一个 WindowManager,在 Android中基本上所有的 View 都是通过 Window 来呈现的,比如 Activity、Dialog、Toast等.
  4. https://download.seafile.com/d/6e5297246c/files/?p=%2Fpro%2Fseafile-pro-server_7.0.9_x86-64.tar.gz&dl=1
  5. https://download.seafile.com/d/6e5297246c/files/?p=%2Fpro%2Fseafile-pro-server_7.0.9_x86-64.tar.gz.md5.txt&dl=1

AppCompatActivity 的委托类

在我们第一步追踪 setContentView 中的时候,我们发现了这样的一段代码

代码 片段1

  @Override
  public void setContentView(@LayoutRes int layoutResID) {
    getDelegate().setContentView(layoutResID);
  }

他是通过新建了一个 AppCompatDelegate 这个类来间接调用 setContentView 的方法,继续追下去我们发现 AppCompatDelegate 只有一个实现类 AppCompatDelegateImpl(这里说明下,笔者探究的源码是 androidX的版本,如果是 support 包下的,AppCompatDelegate 的实现类有多个,会根据 Build.VERSION 来判断具体实例化哪个实现类)。
我们来看看他的具体实现。

代码 片段2

@Override
public void setContentView(View v, ViewGroup.LayoutParams lp) {
  // SubDecorView
  ensureSubDecor();
  // ContentView 通过 mSubDecor 找到
  ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
  contentParent.removeAllViews();
  // 装载我们定义的布局
  contentParent.addView(v, lp);
  mOriginalWindowCallback.onContentChanged();
}

继续看 ensureSubDecor() 方法

private void ensureSubDecor() {
    if (!mSubDecorInstalled) {
        // 创建 DecorView
        mSubDecor = createSubDecor();
        ...
    }
}

继续看 createSubDecor() 方法

代码 片段3

private ViewGroup createSubDecor() {的
    ...
    // 这一段是 根据获取当前Activity的style并设置相关的风格的代码

    // Now let's make sure that the Window has installed its decor by retrieving it
    mWindow.getDecorView();

    final LayoutInflater inflater = LayoutInflater.from(mContext);
    ViewGroup subDecor = null;

    // 根据相关的样式加载不同的布局
    if (!mWindowNoTitle) {
        if (mIsFloating) {
            subDecor = (ViewGroup) inflater.inflate(
                    R.layout.abc_dialog_title_material, null);
            ...
        } else if (mHasActionBar) {
            ...
            subDecor = (ViewGroup) LayoutInflater.from(themedContext)
                    .inflate(R.layout.abc_screen_toolbar, null);
            ...
        }
    } else {
        if (mOverlayActionMode) {
            subDecor = (ViewGroup) inflater.inflate(
                    R.layout.abc_screen_simple_overlay_action_mode, null);
        } else {
            subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
        }
        ...
        // 这一堆不看 适配 inset 相关
    }
    ...
    
    // Now set the Window's content view with the decor
    mWindow.setContentView(subDecor);
    ...
    return subDecor;
}

从以上代码可以看到 AppCompatDelegate 根据各种主题样式与场景创建了一个 subDecor 的View,subDecor 中包含了我们的 contentView,最后将其和Window做了绑定。但是从命名上我们似乎知道,这个并不是真正的DecorView。为了一探究竟,下面我们继续跟进Window中源码。

Window中的流程

我们继续跟进,发现Window是个抽象类,我们知道Android中Window有且只有一个子类,那就是PhoneWindow,直接找到相关实现,看代码:

@Override
public void setContentView(View view) {
    setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}

这里设置了 subDecor 的LayoutParams。继续跟进:

@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
    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 {
        // 将 `subDecor` 添加进 mContentParent
        mContentParent.addView(view, params);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}

在上一个流程中,我们猜测 subDecor 并不是 DecorView,在这里我们发现有一个 installDecor() 的方法,所以这里面是不是就是初始化了我们的 DecorView呢?

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

进入之后发现了两处神奇的代码,里面有两个变量,我们看看这两个变量都是啥。

// 这是窗口的顶层布局
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;

// 这是放置窗口内容的视图。它要么是mDecor本身,要么是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.

ViewGroup mContentParent;

好,我们在这里确实找到了 DecorView,继续看看 DecorView 是怎么被创建的。

protected DecorView generateDecor(int featureId) {
    // System process doesn't have application context and in that case we need to directly use
    // the context we have. Otherwise we want the application context, so we don't cling to the
    // activity.
    Context context;
    if (mUseDecorContext) {
        Context applicationContext = getContext().getApplicationContext();
        if (applicationContext == null) {
            context = getContext();
        } else {
            context = new DecorContext(applicationContext, getContext());
            if (mTheme != -1) {
                context.setTheme(mTheme);
            }
        }
    } else {
        context = getContext();
    }
    // 创建了 DecorView 并且将 this  window传入进行绑定
    return new DecorView(context, featureId, this, getAttributes());
}

这里我们看到 DecorView 确实被创建出来了。然后我们看看 mContentParent。

protected ViewGroup generateLayout(DecorView decor) {
    // Apply data from current theme.

    TypedArray a = getWindowStyle();
    ... 根据style进行一系列赋值操作
    // Inflate the window decor.

    int layoutResource;
    int features = getLocalFeatures();
    ... 给 layoutResource 进行赋值操作

    mDecor.startChanging();
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
   
    ...

    return contentParent;
}

我们看到 上面有这样的一段代码 mDecor.onResourcesLoaded(mLayoutInflater, layoutResource); 这是做什么的呢?继续跟进:

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
    ...
    // inflate 了这个资源文件
    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 {

        // 将这个布局文件 add进DecorView 中.
        addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }
    mContentRoot = (ViewGroup) root;
    initializeElevation();
}

我们发现,这里 inflate 了这个资源文件,并且将它添加进了DecorView中,在上上面的代码片段中,我们发现了 contentParent 是通过 findViewById 出来的,进去可以看到

public <T extends View> T findViewById(@IdRes int id) {
    return getDecorView().findViewById(id);
}

它是通过DecorView找出来的,现在让我们来梳理下Window的setContentView的方法都干了什么。

  1. 初始化了DecorView
  2. 根据style的不同设置了不同的feature
  3. 根据feature的不同得到了不同的layoutResource
  4. 用layoutResource inflate出了不同布局的View,并且被添加到了DecorView中
  5. 通过DecorView的findViewById的方法找到了contentView
  6. contentView将setContentView中View添加了进去

结合AppCompatActivity中setContentView的流程,整体流程就变成了

  1. 根据不同的主题等,初始化SubDecorView
  2. 初始化了DecorView
  3. 根据style的不同设置了不同的feature
  4. 根据feature的不同得到了不同的layoutResource
  5. 用layoutResource inflate出了不同布局的View,并且被添加到了DecorView中
  6. 通过DecorView的findViewById的方法找到了contentView
  7. contentView将SubDecorView添加了进去
  8. SubDecorView中的contentView将setContentView中View添加了进去