聊聊setContentView

前言

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

先看一下setContentView時序圖

時序圖.png

解釋一下幾個類的作用

  • AppCompatDelegateImpl

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

  • LayoutInflater

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

  • Factory2

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

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

onCreate

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

我們看到 AppCompatActivity的操作都是交由代理類來實現(xià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方法中 會默認設置一個Factory2對象 所以我們需要在Activity.OnCreate之前設置Factory2對象 否則就會出現(xiàn)注釋2的報錯

setContentView

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

我們看到 setContentView 完全都是交由delegate實現(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();
    }

之前的時序圖有說明 delegate會通過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();
        }
    }

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

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

                //如果是merger標簽
                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的帶包名的標簽 比如TextView
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        //設置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.
                    // 遞歸實例化子View 這里也會根據(jù)include等標簽 調(diào)用不同方法 大家可以自己看一下
                    rInflateChildren(parser, temp, attrs, true);

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

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

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

  1. 前進到第一個START_TAG 解析xml 生成View,但是ViewGroup都有子View
  2. 遞歸生成所有子View
  3. 因為是setContentView 所以attachToRoot時鐘為tree 將View 添加到android.R.id.content中

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

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

        try {
            //這里會交由Factory2實現(xiàn) 如果Factory沒有處理這個Tag 那么會交由系統(tǒng)實現(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;
        } 
        .......
    }

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

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

扯遠了 我們看一下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點 我們以AppCompatActivity為例 看一下AppCompatActivity的實現(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來實現(xiàn) 我們以AppCompatActivity為例 看一下AppCompatActivity Factory的實現(xiàn)

我們上面說過 AppCompatActivity的實現(xiàn)都交由AppCompatDelegate實現(xiàn) 具體實現(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());
    }

感覺有點繞 但其實邏輯又非常清楚?? 符合單一職責 創(chuàng)建View都是通過LayoutInflate來實現(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對TextView等做了兼容處理 重點看一下注釋1的地方 里面通過反射獲取View 但是眾所周知 反射是一個比較耗時的操作 所以我在布局優(yōu)化的文章中寫過 可以通過一些X2C等框架 來解決反射問題 但是可能會有一些兼容問題 需要處理一下

    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)通過反射生成了 再看一次時序圖 來回顧一下整體的流程


時序圖.png

總結

在學習setContentView的過程中 可以參考上面的那個時序圖來分析 我們需要了解其中的幾個類的職責是什么 分析清楚之后其實邏輯也就相當清楚了

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容