Android-setContentView与findViewById源码解析

原创-转载请注明出处。

当我们给Activity设置布局时,都是直接调用setContentView来完成的,但具体Android是怎么把布局加载到window,又是怎么通过findViewById获取view对象的,我们可能并没有太关心,下面就结合源码来分析下这个过程。

Android setContentView

打开Activity的源码发现,setContentView有三个重载方法,

  1. public void setContentView(int layoutResID);
  2. public void setContentView(View view);
  3. public void setContentView(View view, ViewGroup.LayoutParams params)
    我们就来看下最常用的第一个方法:
    public void setContentView(int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

这个方法调用了,Window类中的setContentView()方法,其他方法也是调用了Window类中的setContentView(),但是Window是一个抽象类,在Activity的attach方法中被初始化,其实是一个PhoneWindow实例,所以这个setContentView方法在PhoneWindow中实现。

    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);
        }
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }

首先判断mContentParent是否为空,如果为空的话则调用installDecor()方法,其次判断是否设置了FEATURE_CONTENT_TRANSITIONS属性,如果没有的话则移除所有view(从这里我们可以得出setContentView可以调用多次,反正会removeAllViews),然后调用LayoutInflater.inflate(),将我们设置的布局文件添加到mContentParent中。接着获取了一个Callback对象,那这个是在Activity的attach方法中设置的一个回调

mWindow.setCallback(this);  

所以可以得出在Activity中一定有一个onContentChanged回调,我们来看下这个回调

public void onContentChanged() {}    

额,空空如也。但是我们可以在自己的Activity中重写这个回调,用于在setContentView之后做一些事情,比如findViewById,但貌似实际场景也不需要。。。
好了,现在我们回到上面提到的installDecor()方法,好长,我们捡重要的看吧。

private void installDecor() {
             //初始化decorView
            if (mDecor == null) {
                mDecor = generateDecor();
                mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
                mDecor.setIsRootNamespace(true);
                if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                    mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
                }
            }
            //初始化mContentParent
            if (mContentParent == null) {
                mContentParent = generateLayout(mDecor);
                  ......
                  //设置一堆属性值
            }
    }    

看下PhoneWindow中的generateDecor()方法

 protected DecorView generateDecor() {
           return new DecorView(getContext(), -1);
     }      

只是单纯的new了一个DecorView实例。这个DecorView是什么鬼。其实它是PhoneWindow的一个内部类,是整个window界面最顶层的view。包含ActionBar,内容块等。好了,现在我们缕一下Window,PhoneWindow,decorView的关系

1.Window类是一个抽象类,提供了绘制窗口的一组通用API。可以将之理解为一个载体,各种View在这个载体上显示。
2.PhoneWindow是Window的一个子类,是Window的具体实现,包含一个内部类DecorView,PhoneWindow是将decorView进行了一定包装,并提供一些方法用于操作窗口。
3。DecorView继承自FrameLayout,是窗口的根view。

好了,接着看mContentParent的初始化,generateLayout(mDecor).这里传入了上一部初始化好的DecorView. 又是一个长方法,我们还是挑出重要的部分。

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

        TypedArray a = getWindowStyle();

        //......
        //根据定义的style设置一些值,比如是否显示ActionBar,

        // Inflate the window decor.

        int layoutResource;
        int features = getLocalFeatures();
        //......
        //根据设定好的features值选择不同的窗口修饰布局文件,
        //得到layoutResource值,系统定义了不同的layout,比如  
        //R.layout.screen_custom_title,R.layout.screen_simple

        //把选中的窗口修饰布局文件添加到DecorView对象里,并且指定contentParent值
        View in = mLayoutInflater.inflate(layoutResource, null);
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        mContentRoot = (ViewGroup) in;

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }

        //......
        //继续一堆属性设置,返回contentParent
        return contentParent;
    }    

根据不同的features值,设定layoutResource,最终添加到decorView中,所以我们通过在xml中设置的theme,还有在代码中设置的requestWindowFeature,都是用来设置features值,这也是为什么requestWindowFeature方法必须在setContentView之前的原因。
这样看来,如果我们设置我们的Theme为NoTitleBar,最终layoutResource的值为R.layout.screen_simple

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

来看下去处标题栏后的视图树

setContentView

所以installDecor主要是初始化了PhoneWindow中的DecorView.和contentParent,之后在setContentView()中通过mLayoutInflater.inflate(layoutResID, mContentParent);将layoutResId,add到初始化好的contentParent中。
大家是否好奇状态栏怎么被加载进DecorView的,我们来看下DecorView中的updateColorViewInt方法

    private View updateColorViewInt(View view, int sysUiVis, int systemUiHideFlag,
                int translucentFlag, int color, int height, int verticalGravity,
                String transitionName, int id, boolean hiddenByWindowFlag) {
              ......
            if (view == null) {
                if (show) {
                    view = new View(mContext);
                    view.setBackgroundColor(color);
                    view.setTransitionName(transitionName);
                    view.setId(id);
                    addView(view, new LayoutParams(LayoutParams.MATCH_PARENT, height,
                            Gravity.START | verticalGravity));
                }
            } else {
               ......
                   }
            return view;
        }

可以看到直接new了一个view,这个view就是状态栏,然后将状态栏添加到了DecorView,其实这个状态栏只是一个单纯的占位view。被updateColorViews方法调用,比如当我们调用setStatusBarColor时就是调用了updateColorViews这个方法。这里先不做过多介绍。

findViewById

那么将layout添加进decorView中后,我们是怎么通过findViewById找到View的呢?
看下Activity的findViewById方法

        /**
         * Finds a view that was identified by the id attribute from the XML that
         * was processed in {@link #onCreate}.
         *
         * @return The view if found or null otherwise.
         */
        public View findViewById(int id) {
            return getWindow().findViewById(id);
        }       

又是到了window中,看下window中的方法

    public View findViewById(int id) {
        return getDecorView().findViewById(id);
    }  

是调用了getDecorView的findViewById,也就是调用了view的findViewById,我们来看下view类中

    public final View findViewById(int id) {
        if (id < 0) {
            return null;
        }
        return findViewTraversal(id);
    }
    
    protected View findViewTraversal(int id) {
        if (id == mID) {
            return this;
        }
        return null;
    }  

到这我们就疑惑了,直接判断了id是否为view的id,是的话就返回。怎么也应该有一个循环或者递归查找啊,什么都没有。
这时我们来看下,mID是怎么初始化的

    ....
    case com.android.internal.R.styleable.View_id:
                    mID = a.getResourceId(attr, NO_ID);
                    break;
   ...  

喔,这个id就是我们在xml中设置的id。那会不会在ViewGroup中进行查找的呢?来看下

    protected View findViewTraversal(int id) {
        if (id == mID) {
            return this;
        }

        final View[] where = mChildren;
        final int len = mChildrenCount;

        for (int i = 0; i < len; i++) {
            View v = where[i];

            if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
                v = v.findViewById(id);

                if (v != null) {
                    return v;
                }
            }
        }

        return null;
    }  

果然, ViewGroup重写了View的findViewTraversal()方法,遍历了自己的child的findViewById方法,如果找到了返回View自身。
ok,到现在我们就理解了view是怎么findViewById的了。

总结

上面我们介绍了,Activity setContentView和findViewById的流程,是不是又多了一层理解呢,喜欢的话就点个赞吧~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容