聊聊setContentView

前言

setContentView應(yīng)該是我們剛開始使用Android 就使用的Api了 來看一下setContentView具體實(shí)現(xiàn)

先看一下setContentView時(shí)序圖

時(shí)序圖.png

解釋一下幾個(gè)類的作用

  • AppCompatDelegateImpl

    AppCompatActivity的代理實(shí)現(xiàn)類,AppCompatActivity的具體實(shí)現(xiàn)會(huì)交由它實(shí)現(xiàn)

  • LayoutInflater

    我用google翻譯了一下 布局充氣機(jī)?? 感覺有點(diǎn)gaygay的 這個(gè)類的作用就是解析xml 遍歷創(chuàng)建view

  • Factory2

    這個(gè)接口只有一個(gè)方法onCreateView 顧名思義 就是創(chuàng)建view AppCompatDelegateImpl就繼承了這個(gè)接口 我們可以實(shí)現(xiàn)這個(gè)接口來創(chuàng)建我們需要的view 比如AppCompatDelegateImpl就會(huì)將所有的TextView轉(zhuǎn)換為AppCompatTextView 一會(huì)可以看一下代碼

接下來上一下源碼?? 全都以AppCompatActivity為例哦

onCreate

 @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        final AppCompatDelegate delegate = getDelegate();
        delegate.installViewFactory();
        delegate.onCreate(savedInstanceState);
        super.onCreate(savedInstanceState);
    }

我們看到 AppCompatActivity的操作都是交由代理類來實(shí)現(xiàn)
重點(diǎn)看一下installViewFactory()

 @Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory2(layoutInflater, this);//1
        } else {
            if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
                Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                        + " so we can not install AppCompat's");//2
            }
        }
    }

我們看注釋1的地方 發(fā)現(xiàn)在OnCreate方法中 會(huì)默認(rèn)設(shè)置一個(gè)Factory2對(duì)象 所以我們需要在Activity.OnCreate之前設(shè)置Factory2對(duì)象 否則就會(huì)出現(xiàn)注釋2的報(bào)錯(cuò)

setContentView

今天的重頭戲 我們看一下上面的時(shí)序圖 大致的流程其實(shí)就是解析xml 然后反射生成view 具體根據(jù)時(shí)序圖 我們來看一下源碼分析

我們看到 setContentView 完全都是交由delegate實(shí)現(xiàn)

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

    //delegate
    @Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        //通過LayoutInflater和resId 創(chuàng)建View
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mAppCompatWindowCallback.getWrapped().onContentChanged();
    }

之前的時(shí)序圖有說明 delegate會(huì)通過LayoutInflater創(chuàng)建View

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                  + Integer.toHexString(resource) + ")");
        }

        View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
        if (view != null) {
            return view;
        }
        //生成xml解析器
        XmlResourceParser parser = res.getLayout(resource);
        try {
            //1. 通過反射生成view
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

上面這段代碼會(huì)將xml文件進(jìn)行解析 然后通過inflate方法創(chuàng)建view 并返回 我們看一下下面的部分精簡(jiǎn)代碼

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            .......
            try {
                    //將parser前進(jìn)到第一個(gè)START_TAG
                advanceToRootNode(parser);
                final String name = parser.getName();

                //如果是merger標(biāo)簽
                if (TAG_MERGE.equals(name)) {
                    ......
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    //1.根據(jù)tag生成view Tag就是我們寫在xml的帶包名的標(biāo)簽 比如TextView
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        //設(shè)置LayoutParams
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }
                    
                    // Inflate all children under temp against its context.
                    // 遞歸實(shí)例化子View 這里也會(huì)根據(jù)include等標(biāo)簽 調(diào)用不同方法 大家可以自己看一下
                    rInflateChildren(parser, temp, attrs, true);

                    //setContentView的話 會(huì)將View 添加到android.R.id.Content中
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                   if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }
            } 
                ......
            return result;
        }
    }

上面的代碼我稍微精簡(jiǎn)了一下 流程主要分為3步

  1. 前進(jìn)到第一個(gè)START_TAG 解析xml 生成View,但是ViewGroup都有子View
  2. 遞歸生成所有子View
  3. 因?yàn)槭?code>setContentView 所以attachToRoot時(shí)鐘為tree 將View 添加到android.R.id.content中

我們關(guān)注的重點(diǎn)主要還是createViewFromTag 看下面的代碼 發(fā)現(xiàn)createViewFromTag是交由Factory2實(shí)現(xiàn)

         View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        ......

        try {
            //這里會(huì)交由Factory2實(shí)現(xiàn) 如果Factory沒有處理這個(gè)Tag 那么會(huì)交由系統(tǒng)實(shí)現(xiàn) 就是下面的onCreateView和createView
            View view = tryCreateView(parent, name, context, attrs);

            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    //比如TextView等不需要包名
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(context, parent, name, attrs);
                    } else {
                        view = createView(context, name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        } 
        .......
    }

我們重點(diǎn)還是關(guān)注tryCreateView,onCreateView等方法大家可以自己看一下 就是反射生成view

tryCreateView會(huì)通過Factory2接口實(shí)現(xiàn) 還記得我們之前說 AppDelegateImpl繼承了Factory2這就是AppCompatActivity對(duì)一些Tag進(jìn)行了攔截創(chuàng)建 我們也可以自己實(shí)現(xiàn)Factory2來進(jìn)行攔截 實(shí)現(xiàn)一些像換膚的功能 大家可以看一下我之前寫的文章手?jǐn)]動(dòng)態(tài)換膚框架(一)
感覺有收獲的同學(xué)點(diǎn)點(diǎn)贊吶??

扯遠(yuǎn)了 我們看一下tryCreateView方法

    public final View tryCreateView(@Nullable View parent, @NonNull String name,
        @NonNull Context context,
        @NonNull AttributeSet attrs) {
        if (name.equals(TAG_1995)) {
              // 這里好像致敬了JAVA誕生
            // Let's party like it's 1995!
            return new BlinkLayout(context, attrs);
        }

        View view;
        //這里就是我們可以做的Hook點(diǎn) 我們以AppCompatActivity為例 看一下AppCompatActivity的實(shí)現(xiàn)
        if (mFactory2 != null) {
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }

        if (view == null && mPrivateFactory != null) {
            view = mPrivateFactory.onCreateView(parent, name, context, attrs);
        }

        return view;
    }

上面方法我們發(fā)現(xiàn) 我們?nèi)绻際ook系統(tǒng)的setContentView方法的話 可以通過Factory2來實(shí)現(xiàn) 我們以AppCompatActivity為例 看一下AppCompatActivity Factory的實(shí)現(xiàn)

我們上面說過 AppCompatActivity的實(shí)現(xiàn)都交由AppCompatDelegate實(shí)現(xiàn) 具體實(shí)現(xiàn)類為AppCompatDelegateImpl

AppCompatDelegateImpl繼承了Factory2接口 所以我們看一下AppCompatDelegateImplonCreateView偽代碼

    @Override
    public View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs) {
        ......
        mAppCompatViewInflater = new AppCompatViewInflater();

        return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
                IS_PRE_LOLLIPOP,true, VectorEnabledTintResources.shouldBeUsed());
    }

感覺有點(diǎn)繞 但其實(shí)邏輯又非常清楚?? 符合單一職責(zé) 創(chuàng)建View都是通過LayoutInflate來實(shí)現(xiàn)

    final View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs, boolean inheritContext,
            boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
        ......
        switch (name) {
            case "TextView":
                view = createTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "ImageView":
                view = createImageView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "Button":
                view = createButton(context, attrs);
                verifyNotNull(view, name);
                break;
            case "EditText":
                view = createEditText(context, attrs);
                verifyNotNull(view, name);
                break;
            ......
            }

        if (view == null && originalContext != context) {
            // If the original context does not equal our themed context, then we need to manually
            // inflate it using the name so that android:theme takes effect.
            //注釋1
            view = createViewFromTag(context, name, attrs);
        }

        if (view != null) {
            // If we have created a view, check its android:onClick
            //檢查onClick 如果存在 就調(diào)用view.setonClickListener
            checkOnClickListener(view, attrs);
        }

        return view;
    }

看到AppCompatViewInflater對(duì)TextView等做了兼容處理 重點(diǎn)看一下注釋1的地方 里面通過反射獲取View 但是眾所周知 反射是一個(gè)比較耗時(shí)的操作 所以我在布局優(yōu)化的文章中寫過 可以通過一些X2C等框架 來解決反射問題 但是可能會(huì)有一些兼容問題 需要處理一下

    private View createViewFromTag(Context context, String name, AttributeSet attrs) {
        ......
            if (-1 == name.indexOf('.')) {
                for (int i = 0; i < sClassPrefixList.length; i++) {
                    final View view = createViewByPrefix(context, name, sClassPrefixList[i]);
                    if (view != null) {
                        return view;
                    }
                }
                return null;
            } else {
                return createViewByPrefix(context, name, null);
            }
        } 
        ......
    }
    
    private View createViewByPrefix(Context context, String name, String prefix)
            throws ClassNotFoundException, InflateException {
         //先從緩存中取 避免每次都反射獲取
        Constructor<? extends View> constructor = sConstructorMap.get(name);

        try {
            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                //反射生成
                Class<? extends View> clazz = Class.forName(
                        prefix != null ? (prefix + name) : name,
                        false,
                        context.getClassLoader()).asSubclass(View.class);

                constructor = clazz.getConstructor(sConstructorSignature);
                sConstructorMap.put(name, constructor);
            }
            constructor.setAccessible(true);
            return constructor.newInstance(mConstructorArgs);
        } catch (Exception e) {
            // We do not want to catch these, lets return null and let the actual LayoutInflater
            // try
            return null;
        }
    }

至此View已經(jīng)通過反射生成了 再看一次時(shí)序圖 來回顧一下整體的流程


時(shí)序圖.png

總結(jié)

在學(xué)習(xí)setContentView的過程中 可以參考上面的那個(gè)時(shí)序圖來分析 我們需要了解其中的幾個(gè)類的職責(zé)是什么 分析清楚之后其實(shí)邏輯也就相當(dāng)清楚了

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末镜廉,一起剝皮案震驚了整個(gè)濱河市欠拾,隨后出現(xiàn)的幾起案子饲宿,更是在濱河造成了極大的恐慌,老刑警劉巖裙顽,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異杨箭,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)锅移,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來彬檀,“玉大人帆啃,你說我怎么就攤上這事∏系郏” “怎么了?”我有些...
    開封第一講書人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵诽偷,是天一觀的道長(zhǎng)坤学。 經(jīng)常有香客問我,道長(zhǎng)报慕,這世上最難降的妖魔是什么深浮? 我笑而不...
    開封第一講書人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮眠冈,結(jié)果婚禮上飞苇,老公的妹妹穿的比我還像新娘。我一直安慰自己蜗顽,他們只是感情好布卡,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著雇盖,像睡著了一般忿等。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上崔挖,一...
    開封第一講書人閱讀 52,262評(píng)論 1 308
  • 那天贸街,我揣著相機(jī)與錄音,去河邊找鬼狸相。 笑死薛匪,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的脓鹃。 我是一名探鬼主播逸尖,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼将谊!你這毒婦竟也來了冷溶?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤尊浓,失蹤者是張志新(化名)和其女友劉穎逞频,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體栋齿,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡苗胀,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年襟诸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片基协。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡歌亲,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出澜驮,到底是詐尸還是另有隱情陷揪,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布杂穷,位于F島的核電站悍缠,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏耐量。R本人自食惡果不足惜飞蚓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望廊蜒。 院中可真熱鬧趴拧,春花似錦、人聲如沸山叮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽聘芜。三九已至兄渺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間汰现,已是汗流浹背挂谍。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瞎饲,地道東北人口叙。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像嗅战,于是被迫代替她去往敵國(guó)和親妄田。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359