Activity当中的View是如何呈现在手机上的?含源码分析

学习任何一门开发语言的经典入门课就是“Hello World”,Android虽然是以java为基础,但是也不能仅仅是在控制栏输出"Hello World"这么简单就行了,我们总得在手机上跑起来,让界面展示"Hello World"才行,那么我们要怎样做呢?很简单,新建项目这些就不用说了,新建一个布局,添加一个android:text = "Hello World" 的TextView,通过Activity在onCreate方法中setContentView(R.layout.xxx)即可。虽然想要展示一个界面这么简单,但是背后的原理和流程也是需要知道的。

该篇文章源码基于 android-28

先放一张网上关于Activity视图层级的图,看完这篇文章再回过头来看这张图:
Activity视图层级

通过setContentView,我们就给Activity设置了布局,布局中写的任何View都能展示在手机上,那我们就从这个方法开始跟踪:

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

该方法通过getWindow()的返回值进行setContentView:

//Activity.java
public Window getWindow() {
    return mWindow;
}

返回的是一个Window对象:

/**
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */
public abstract class Window {
	//。。。。省略类中的代码,主要是看注释
}

Window是一个抽象类,英文中注释的意思是存在唯一的一个实现类是 android.view.PhoneWindow ,我们接着看PhoneWindow的setContentView方法:

//PhoneWindow.java
//把mContentParent的声明放过来方便源码阅读
ViewGroup mContentParent;

@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();
    } 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 肯定是null,就会走第一个if判断执行 installDecor() 方法:

//PhoneWindow.java
//声明放过来方便源码阅读
private DecorView mDecor; //继承自FrameLayout

private void installDecor() {
    mForceDecorInstall = false;
    if (mDecor == null) {
        mDecor = generateDecor(-1);
        //。。。省略代码
    } else {
        mDecor.setWindow(this);
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);
		//。。。省略代码,省略的代码是设置图标、背景、title以及Acitivty动画的,对我们该篇文章的分析没有用
    }
}

有两行代码关系到我们的流程,一个是 mDecor = generateDecor(-1); ,另一个是 mContentParent = generateLayout(mDecor); ,我们先看 generateDecor(-1) 方法,(返回值通过上面的声明我们可以知道是一个DecorView,它是一个继承自FrameLayout的ViewGroup):

// PhoneWindow.java

protected DecorView generateDecor(int featureId) {
    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();
    }
    return new DecorView(context, featureId, this, getAttributes());
}

这个方法最主要就是new了一个DecorView返回过去,把这个返回过去的DecorView又传到了generateLayout(mDecor) 方法:

// PhoneWindow.java

protected ViewGroup generateLayout(DecorView decor) {
    TypedArray a = getWindowStyle();
	//。。。省略部分代码
	
    if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
        requestFeature(FEATURE_NO_TITLE);
    } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
        // Don't allow an action bar if there is no title.
        requestFeature(FEATURE_ACTION_BAR);
    }
    //。。。省略部分代码
    
    int layoutResource;//注意这个layoutResource
    int features = getLocalFeatures();

    if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
        layoutResource = R.layout.screen_swipe_dismiss;
        setCloseOnSwipeEnabled(true);
    } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
        if (mIsFloating) {
            TypedValue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                    R.attr.dialogTitleIconsDecorLayout, res, true);
            layoutResource = res.resourceId;
        } else {
            layoutResource = R.layout.screen_title_icons;
        }
        // XXX Remove this once action bar supports these features.
        removeFeature(FEATURE_ACTION_BAR);
    } //。。。省略大量else if代码
    else{//直接到最后的else分支
		layoutResource = R.layout.screen_simple;
	}

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

	//注意这个方法
	ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
	//。。。省略部分代码

	return contentParent;

我们在这段长代码中可以看到很多熟悉的东西,比如第一行的 TypedArray a = getWindowStyle(); ,是不是想起了我们自定义View的自定义属性?再比如后面获取属性的if else 分支 R.styleable.Window_windowNoTitleR.styleable.Window_windowActionBar ,是否想到了我们在清单文件中给Application、Activity节点添加的style中写的属性?最后到了一个关键的局部变量 layoutResource,通过各种style属性的判断给它赋值了各种layout文件,通过查看这些布局文件你会发现一个特点,它们都有一个id为 @android:id/content 的FrameLayout,记住这一点,特别是这个id: @android:id/content
layoutResource 被赋值之后调用了DecorView的 onResourcesLoaded 方法,传进去了两个参数:mLayoutInflaterlayoutResource ,第一个参数我们都知道是LayoutInflater,用于inflate布局的,我们看这个方法:

//DecorView.java

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
    //。。。省略部分代码
    
    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 {

        // Put it below the color views.
        addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }
    mContentRoot = (ViewGroup) root;
    initializeElevation();
}

该方法会inflate一个view,这个view就是我们在PhoneWindow中经过大量判断(是否有title、actionBar)后拿到的布局文件id,返回的root被直接addView到DecorView自身了(前面可以知道它是一个ViewGroup)。

看到这里我们可能会有疑惑了,现在我知道activity下面有PhoneWindow,PhoneWindow下有DecorView了,那我在Activity中setContentView传入的布局文件呢?我们回过头看 installDecor 方法中,mContentParent = generateLayout(mDecor); 的返回值赋值给了 mContentParent (也记住这一点),而这个返回值在 generateLayout 方法中是通过 findViewById得到的:

//将这个常量的声明放到这里,方便阅读
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

我们可以看到,这个常量就是R.id.content,而那个 layoutResource 的每一个赋值的布局文件中都有定义了这个id的FrameLayout,最后我们再回到最初的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();
    } 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 就是那个id为 @android:id/content 的FrameLayout,而 mLayoutInflater.inflate(layoutResID, mContentParent) 这一行代码就把我们在Activity中setContentView的布局文件inflate到了这个FrameLayout中了。现在再去看那张Activity层级视图,是否清晰一些了呢?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值