从setContentView方法源码出发,弄懂Activity的视图是怎么附属在Window上的

文章目录

  • 前言
  • setContentView
    • installDecor
      • generateDecor
      • generateLayout
        • onResourcesLoaded
    • 添加布局文件到mContentParent
    • onContentChanged
  • DecorView正式添加到Window中
    • handleResumeActivity
    • makeVisible
  • 总结

前言

最近正在看Android开发艺术探索,看到了Window的创建过程中的Activity的创建过程,讲到Activity的视图是怎么附属在Window上的时候,书中分析了setContentView方法,但是说得比较简略,一些方法也跟以前有了些改变,所以我就边看书、边自己找源码来啃。本文主要记录了我在查看源码的时候的一些理解以及书中的一些内容,因为是初学者,所以里面可能会有一些理解错误,欢迎大家指出。

setContentView

由于Activity的视图由setContentView方法提供,所以我们只需查看该方法即可。

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

其中getWindow方法返回的是一个PhoneWindow实例

public Window getWindow() {
    return mWindow;
}
mWindow = new PhoneWindow(this, window, activityConfigCallback);

所以现在变成看PhoneWindow的setContentView方法

	@Override
    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();		//生成DecorView和mContentParent,主要分析该方法
        } 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);
            //将布局文件转化为View,mContentParent作为其父布局
			//也就是将Activity的视图添加到DecorView的mContentParent中
        }
        mContentParent.requestApplyInsets();
        
        final Callback cb = getCallback();	//获得当前Window的Callback接口实例
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();	//回调Activity的onContentChanged()方法,通知Activity视图已经改变
        }
        mContentParentExplicitlySet = true;
    }

因为是要明白Activity的视图是怎么附属在Window上的,所以上面的代码主要分析installDecor方法mLayoutInflater.inflate(layoutResID, mContentParent);以及最后的onContentChanged回调。

installDecor

要分析该方法,首先要知道mContentParent是什么,我们先看看官方的解释

    // 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;

翻译过来就是:这是放置窗口内容的视图。它既可以是mDecor本身,也可以是包含内容的mDecor的子项。
这里又涉及到了mDecor,那么我们继续看看mDecor是什么

    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;

可以看到mDecor是一个DecorView(DecorView是一个FrameLayout,它是Activity中的顶级View。一般来说,它包含标题栏和内容栏,内容栏就是setContentView方法传入的布局)

如果mContentParent为空的时候,调用installDecor方法,这个方法比较长,我们看一些关键代码

private void installDecor() {
	  if (mDecor == null) {
            mDecor = generateDecor(-1);		//创建DecorView,之后会分析该方法
			
			//省略一些mDecor的设置
        } else {
            mDecor.setWindow(this);		//如果已经有了DecorView,则直接在设置窗口方法中传入PhoneWindow
        }

	if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);	//生成mContentParent,之后会分析该方法

			//以下省略一大堆代码
			//这堆代码主要处理mDecor和窗口属性,跟mContentParent的初始化没有多大关系
     }   
}

generateDecor

我们先分析generateDecor方法,顾名思义,该方法是用于创建DecorView(注意:调用该方法后,虽然创建了DecorView,但是此时的DecorView还是一个空白的FrameLayout)

protected DecorView generateDecor(int featureId) {
	// 注意:系统进程没有应用程序上下文,在这种情况下,我们需要直接使用我们拥有的上下文。
	//否则我们需要应用程序上下文,所以我们不依赖于活动。
	Context context;
	//先判断是否使用decor context(只有主活动窗口使用decor context,所有其他窗口使用给予它们的context)
	if (mUseDecorContext) {
		Context applicationContext = getContext().getApplicationContext();	
		//获取应用程序上下文
		if (applicationContext == null) {
			context = getContext();		//没有应用程序上下文的情况下,我们需要直接使用我们拥有的上下文。
		} else {
			context = new DecorContext(applicationContext, getContext().getResources());
			//获得decor view的context
			if (mTheme != -1) {
				context.setTheme(mTheme);
			}
		}
	} else {
		context = getContext();		//其他窗口使用它们自己的context
	}
	//总的来说,上面的代码就是为了获取context。只是根据是否使用decor context,获取的方法有所不同

	return new DecorView(context, featureId, this, getAttributes());
	//四个参数分别是Context context, int featureId, PhoneWindow window,WindowManager.LayoutParams params
	//作用是创建DecorView并返回
}

generateLayout

为了初始化DecorView的结构,PhoneWindow还需要通过generateLayout方法来加载具体的布局文件到DecorView中。
所以现在来分析generateLayout方法,由于该方法有大量代码,这里只选取一些相关的代码

protected ViewGroup generateLayout(DecorView decor) {
	TypedArray a = getWindowStyle();	//getWindowStyle()是Window的方法,返回该窗口的style属性
	
	//这里有一大段代码,都是根据上面获取到的window的style属性(a变量)进行相关设置
	//例如判断是否是dialog样式、扩展屏幕功能、设置TypedValue的属性、设置相关颜色等等

	// Inflate the window decor.
	int layoutResource;
	int features = getLocalFeatures();	
	//getLocalFeatures()是Window中的方法,用于获取当前Window正在实现的功能

	//这里一大段代码是通过判断features,来决定layoutResource的值
	
	mDecor.startChanging();  	//开始改变DecorView
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);	//在这里加载布局,下面会重点看下这方法

	ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
	//这里的id就是R.id.content

	//一堆其他操作

	mDecor.finishChanging();	//停止改变DecorView,停止后调用drawableChanged方法更新DeoorView

	return contentParent;
}

onResourcesLoaded

再看一下generateLayout方法中的onResourcesLoaded方法,里面有DecorView加载布局的具体过程

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {

	//省略部分其他代码

	mDecorCaptionView = createDecorCaptionView(inflater);	
	//创建DecorCaptionView(装饰标题视图)
	final View root = inflater.inflate(layoutResource, null);	
	//加载传入的layoutResource,成为根视图

	//判断DecorCaptionView是否为空
	if (mDecorCaptionView != null) {
		if (mDecorCaptionView.getParent() == null) {
			addView(mDecorCaptionView,
			new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));	
			//如果DecorCaptionView没有父布局,就添加DecorCaptionView到DecorView的最后一项
		}
		mDecorCaptionView.addView(root,
			new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
		//添加root到DecorCaptionView的最后一项
	} else {
		// Put it below the color views.
		addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
		//添加root到DecorView的第一项
	}
	mContentRoot = (ViewGroup) root;	
	//将root视图作为DecorView的mContentRoot(一个ViewGroup)

}

上面又有一个新的视图DecorCaptionView,我们看看这又是什么。
下面是DecorView中对于mDecorCaptionView的解释

    // This is the caption view for the window, containing the caption and window control
    // buttons. The visibility of this decor depends on the workspace and the window type.
    // If the window type does not require such a view, this member might be null.
    DecorCaptionView mDecorCaptionView;

翻译过来就是:这是窗口的标题视图,包含标题和窗口控制按钮。 这种装饰的可见性取决于工作空间和窗口类型。如果窗口类型不需要这样的视图,则此成员可能为null。

也就是说当不需要这种视图的时候,mDecorCaptionView为null,也就知道为什么上面会有对mDecorCaptionView的判空操作了。

添加布局文件到mContentParent

上面一层接着一层地分析源码的方法,其实就是为了探究初始化mContentParent,而从上面可以知道,mContentParent是在generateLayout方法中被初始化的,所以其他的就先不再分析了。

接着我们看看setContentView方法中的mLayoutInflater.inflate(layoutResID, mContentParent);

这句话比较重要,通过这条语句,Activity中的视图就与DecorView关联起来,因为Acitvity的布局文件已经添加到DecorView中了。
由此可以理解Activity的setContentView这个方法的来历了。这个方法为什么不叫setView呢?明明是给Activity设置视图的啊!但是从这里可以看出,它确实不适合叫setView,因为Activity的布局文件只是添加到了DecorView的mContentParent(内容部分)中,所以叫setContentView更为准确。

onContentChanged

这个过程比较简单,就是获得当前Window的Callback接口实例后,执行该接口的onContentChanged()方法。因为Activity实现了Window的Callback接口,所以当Activity的布局文件添加到了DecorView的mContentParent后,就会回调该接口。

查看Activity的源码发现,Acitvity的onContentChanged方法是一个空实现。

    public void onContentChanged() {
    }

我们可以在子Activity中重写这个回调方法。在Activity的布局文件添加到了DecorView的mContentParent后做相应的处理。

DecorView正式添加到Window中

setContentView方法完成后,DecorView已经被创建并初始化完毕,Activity的布局文件也已经添加到了DecorView的mContentParent中,但是此时DecorView还没有被WindowManager正式添加到Window中。

虽然说早在Acitvity的attach方法中Window已经被创建,但这个时候由于DecorView并没有被WindowManager识别,所以这时候Window无法提供具体功能,因为它还无法接受外界的输入信息。

handleResumeActivity

那么何时DecorView才正式添加到Window中呢?
先看下ActivityThread的handleResumeActivity方法中,这里只列出部分相关代码

 final void handleResumeActivity(IBinder token,
	boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {

	ActivityClientRecord r = mActivities.get(token);

	//此处省略代码

	// TODO Push resumeArgs into the activity for consideration
	r = performResumeActivity(token, clearHide, reason);	
	//这个方法主要负责调用Activity的onResume()方法

	if (r != null) {

		//此处省略代码

		// 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();	//调用Activity的makeVisible方法	
			}
		}
	}
}

在该方法中,首先会调用Activity的onResume()方法,接着在后面会调用Activity的makeVisible方法。正是在makeVisible方法中,DecorView才真正地完成了添加和显示这两个过程。

makeVisible

    void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());	
            //添加DecorView到Window
            
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);		//显示DecorView
    }

调用该方法后,Activity的视图才能被用户看到

总结

分析了这么多源码,这里回到主题,总结一下Activity的视图是怎么附属在Window上的:

  1. 在PhoneWindow的setContentView方法初始化DecorView和mContentParent
  2. 将Activity的视图(Activity的布局文件)添加到DecorView的mContentParent中
  3. 回调Activity的onContentChanged方法,通知Activity视图已经发生变化
  4. DecorView正式添加到Window中(通过DecorView将Activity的视图和window联系起来)
  5. 设置DecorView可见后,Activity的视图就可以被用户所看到

参考资料:
《Android开发艺术探索》

你可能感兴趣的:(从setContentView方法源码出发,弄懂Activity的视图是怎么附属在Window上的)