外掛式換膚框架搭建 setContentView原始碼閱讀

紅橙Darren發表於2019-02-28

1. 概述


  內涵段子架構第一階段已經更新完了,後面我們主要是以google原始碼為主,今天我帶大家來看一下setContentView的原始碼,請先看一下如果繼承自Activity去列印一個TextView與繼承自AppCompatActivity去列印一個TextView分別是這樣的:

繼承自Activity:  
android.widget.TextView{ac5cd17 V.ED..... ......ID 0,0-0,0 #7f0b002c app:id/text_view}

繼承自AppCompatActivity:  
android.support.v7.widget.AppCompatTextView{392562b V.ED..... ......ID 0,0-0,0 #7f0b0055 app:id/text_view}
複製程式碼

   誰能告訴我這到底是怎麼啦?我佈局裡面明明是TextView為什麼繼承自AppCompatActivity就變成了AppCompatTextView,那麼接下來我們就來看一下原始碼到底是怎麼把我的TextView給拐走的。

所有分享大綱:2017Android進階之路與你同行

視訊講解地址:http://pan.baidu.com/s/1pLNiBWZ

2. Activity的setContentView原始碼閱讀


2.1 很多人都問過我怎麼看原始碼,我只想說怎麼看?當然是坐著點進去看啊!

    public void setContentView(@LayoutRes int layoutResID) {
        // 獲取Window 呼叫window的setContentView方法,發現是抽象類,所以需要找具體的實現類PhoneWindow
        getWindow().setContentView(layoutResID);
    }

    // PhoneWindow 中的 setContentView方法
    @Override
    public void setContentView(int layoutResID) {
        // 如果mContentParent 等於空,呼叫installDecor();
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        // 把我們自己的佈局layoutId加入到mContentParent,我們set進來的佈局原來是放在這裡面的Soga
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
複製程式碼

2.2 installDecor(),這個之前已經帶大家看過一遍了,不過沒辦法再進來看看吧:
  

    // This is the top-level view of the window, containing the window decor.
    // 看到這解釋木有?
    private DecorView mDecor;

    private void installDecor() {    
        if (mDecor == null) {
           // 先去建立一個  DecorView 
           mDecor = generateDecor(-1);
        }
        // ......
        // 省略調一些程式碼,看著暈,不過這也太省了。
        if (mContentParent == null) {
           mContentParent = generateLayout(mDecor);
        }
    }
    
    // generateDecor 方法
    protected DecorView generateDecor(int featureId) {
        // 就是new一個DecorView ,DecorView extends FrameLayout 不同版本的原始碼有稍微的區別,
        // 低版本DecorView 是PhoneWindow的內部類,高版本是一個單獨的類,不過這不影響。
        return new DecorView(context, featureId, this, getAttributes());
    }

    protected ViewGroup generateLayout(DecorView decor) {
        // Inflate the window decor.
        // 我看你到底怎麼啦
        int layoutResource;
        // 都是一些判斷,發現 layoutResource = 系統的一個資原始檔,
        if(){}else if(){}else if(){
            // Embedded, so no decoration is needed.
            layoutResource = R.layout.screen_simple;
            // System.out.println("Simple!");
        }
        
        mDecor.startChanging();
        // 把佈局解析載入到  DecorView 而載入的佈局是一個系統提供的佈局,不同版本不一樣
        // 某些原始碼是 addView() 其實是一樣的
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

        //  ID_ANDROID_CONTENT 是 android.R.id.content,這個View是從DecorView裡面去找的,
        //  也就是    從系統的layoutResource裡面找一個id是android.R.id.content的一個FrameLayout
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        
        // 返回
        return contentParent;
    }
複製程式碼

  其實看原始碼一定要帶著最初的出發點來看,要不然裡面太多了根本找不到方向,如果帶著思想來看那麼就算跑偏了也可以從新再回來,我目前就是想弄清楚我們的 setContentView() 系統到底把我們的佈局加到哪裡去了。我先用文字總結一下,然後去畫一張圖:

  • Activity裡面設定setContentView(),我們的佈局顯示主要是通過PhoneWindow,PhoneWindow獲取例項化一個DecorView。
  • 例項化DecorView,然後做一系列的判斷然後去解析系統的資源layoutId檔案,至於解析哪一個資原始檔會做判斷比如有沒有頭部等等,把它解析載入到DecorView,資源layout裡面有一個View的id是android.R.id.content。
  • 我們自己通過setContentView設定的佈局id其實是解析到mParentContent裡面的,也就是那個id叫做android.R.id.content的FarmeLayout,好了就這麼多了。
外掛式換膚框架搭建   setContentView原始碼閱讀

3. AppCompatActivity的setContentView

    @Override
    public void setContentView(@LayoutRes int layoutResID) {
        // 跟我在網上看的完全不一樣
        getDelegate().setContentView(layoutResID);
    }
   

    public AppCompatDelegate getDelegate() {
        if (mDelegate == null) {
            mDelegate = AppCompatDelegate.create(this, this);
        }
        return mDelegate;
    }
    
    // window 還是那個window ,留意一下就行 , 不同的版本返回 AppCompatDelegateImpl,但是都是相互繼承
    // 最終繼承都是繼承  AppCompatDelegateImplV9 有的版本V7有的V9 好麻煩 嗨!
    private static AppCompatDelegate create(Context context, Window window,
            AppCompatCallback callback) {
        final int sdk = Build.VERSION.SDK_INT;
        if (BuildCompat.isAtLeastN()) {
            return new AppCompatDelegateImplN(context, window, callback);
        } else if (sdk >= 23) {
            return new AppCompatDelegateImplV23(context, window, callback);
        } else if (sdk >= 14) {
            return new AppCompatDelegateImplV14(context, window, callback);
        } else if (sdk >= 11) {
            return new AppCompatDelegateImplV11(context, window, callback);
        } else {
            return new AppCompatDelegateImplV9(context, window, callback);
        }
    }

    // 下面其實就沒啥好看的了,一個一個點進去,仔細看看就好了。與Activity沒啥區別了
    @Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mOriginalWindowCallback.onContentChanged();
    }

    private void ensureSubDecor() {
        mSubDecor = createSubDecor();
    }
複製程式碼

4. AppCompatViewInflater原始碼分析


  看到這裡還是不知道為什麼我的TextView變成了AppCompatTextView,找啊找啊就找了這麼個方法:

    @Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            // 把LayoutInflater 的 Factory設定為了this,也就說待會建立View就會走自己的onCreateView方法
            // 如果看不懂還需要看一下 LayoutInflater 的原始碼,我們的LayoutInflater.from(mContext)其實是一個單例
            // 如果設定了Factory那麼每次建立View都會先執行Factory的onCreateView方法
            LayoutInflaterCompat.setFactory(layoutInflater, this);
        } else {
            if (!(LayoutInflaterCompat.getFactory(layoutInflater)
                    instanceof AppCompatDelegateImplV7)) {
                Log.i(TAG, "The Activity`s LayoutInflater already has a Factory installed"
                        + " so we can not install AppCompat`s");
            }
        }
    }

    @Override
    public View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs) {
        // 看一下是不是 5.0 ,5.0 都自帶什麼效果我就不說了
        final boolean isPre21 = Build.VERSION.SDK_INT < 21;

        if (mAppCompatViewInflater == null) {
            mAppCompatViewInflater = new AppCompatViewInflater();
        }

        // We only want the View to inherit its context if we`re running pre-v21
        final boolean inheritContext = isPre21 && shouldInheritContext((ViewParent) parent);
        // 通過 AppCompatViewInflater 去建立View
        return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
                isPre21, /* Only read android:theme pre-L (L+ handles this anyway) */
                true, /* Read read app:theme as a fallback at all times for legacy reasons */
                VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
        );
    }
複製程式碼

  其實在講資料庫優化我們已經看過一次AppCompatViewInflater的原始碼了,建立View都是用的反射,只不過做了快取和優化而已,我們寫程式碼其實可以仿照原始碼來,給我們很好的思路。

public final View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs, boolean inheritContext,
            boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {

        View view = null;
        // 果真找到你了,哈哈 ,做了替換
        // We need to `inject` our tint aware Views in place of the standard framework versions
        switch (name) {
            case "TextView":
                view = new AppCompatTextView(context, attrs);
                break;
            case "ImageView":
                view = new AppCompatImageView(context, attrs);
                break;
            case "Button":
                view = new AppCompatButton(context, attrs);
                break;
            case "EditText":
                view = new AppCompatEditText(context, attrs);
                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.
            view = createViewFromTag(context, name, attrs);
        }
        return view;
    }

private View createView(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 = context.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
                // 利用反射建立一個建構函式
                constructor = clazz.getConstructor(sConstructorSignature);
                sConstructorMap.put(name, constructor);
            }
            constructor.setAccessible(true);
            // 利用反射建立View的例項
            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;
        }
}
複製程式碼

5. LayoutInflater原始碼分析


  LayoutInflater的原始碼我們分三個步驟去看相對來說會更加的系統:
  4. 1 如何獲取LayoutInflater?
  4. 2 如何使用LayoutInflater?
  4. 3 佈局的View是如何被例項化的?

  先來看看我們平時都是怎麼去獲取LayoutInflater的,這個我們其實並不陌生LayoutInflater.from(context):

    /**
    * Obtains the LayoutInflater from the given context.
    */
    // 是一個靜態的方法
    public static LayoutInflater from(Context context) {
        // 通過context獲取系統的服務
        LayoutInflater LayoutInflater =
                // context.getSystemService()是一個抽象類,所以我們必須找到實現類ContextImpl
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
    }

   
    // ContextImpl 裡面的實現方法
    @Override
    public Object getSystemService(String name) {
        return SystemServiceRegistry.getSystemService(this, name);
    }

    /**
     * Gets a system service from a given context.
     */
    // SystemServiceRegistry 裡面的getSystemService方法
    public static Object getSystemService(ContextImpl ctx, String name) {
        ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
        return fetcher != null ? fetcher.getService(ctx) : null;
    }
    
    // 這是一個靜態的HashMap集合
    private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
            new HashMap<String, ServiceFetcher<?>>();
    
    // 靜態的程式碼塊中
    static{
         // 註冊LayoutInflater服務
         registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
                new CachedServiceFetcher<LayoutInflater>() {
            @Override
            public LayoutInflater createService(ContextImpl ctx) {
                return new PhoneLayoutInflater(ctx.getOuterContext());
            }});
         }
         // 註冊很多的其他服務......
    }
複製程式碼

  接下來大致的整理一下獲取LayoutInflater的思路,通過Context的實現類ContextImpl獲取的,最終是通過SystemServiceRegistry.getSystemService()方法,而SYSTEM_SERVICE_FETCHERS是一個靜態的HashMap,初始化是在靜態程式碼塊中通過registerService註冊了很多服務。所以到目前為止我們有兩個思想對於我們後面外掛化的皮膚框架有很大的關係,第一LayoutInflater其實是一個系統的服務,第二每次通過LayoutInflater.form(context)是一個靜態的單例類無論在哪裡獲取都是同一個物件。接下來我們來看一下載入佈局的三種方式:

1.View.inflate(context,layoutId,parent);
2.LayoutInflater.from(context).inflate(layoutId,parent);
3.LayoutInflater.from(context).inflate(layoutId,parent,attachToRoot);
複製程式碼

1.View.inflate(context,layoutId,parent);

    // 其實就是呼叫的  LayoutInflater.from(context).inflate(layoutId,parent);
    public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) {
        LayoutInflater factory = LayoutInflater.from(context);
        return factory.inflate(resource, root);
    }
複製程式碼

2.LayoutInflater.from(context).inflate(layoutId,parent);

    // 其實就是呼叫的 LayoutInflater.from(context).inflate(layoutId,parent,attachToRoot);
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
         return inflate(resource, root, root != null);
    }
複製程式碼

3.LayoutInflater.from(context).inflate(layoutId,parent,attachToRoot); 其實最終都是呼叫的該方法,我們關鍵是要弄清楚這個引數的概念,尤其是attachToRoot:

    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) + ")");
        }
        // 獲取一個 XmlResourceParser 解析器,這個應該並不陌生,就是待會需要去解析我們的layoutId.xml檔案
        // 這個到後面的外掛化架構再去詳細講解
        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        ......
        //儲存傳進來的這個view
        View result = root;

        try {
            // Look for the root node.
            int type;
            //在這裡找到root標籤
            while ((type = parser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) {
                // Empty
            }

            if (type != XmlPullParser.START_TAG) {
                throw new InflateException(parser.getPositionDescription()
                        + ": No start tag found!");
            }
            //獲取這個root標籤的名字
            final String name = parser.getName();
             ......

            //判斷是否merge標籤
            if (TAG_MERGE.equals(name)) {
                if (root == null || !attachToRoot) {
                    throw new InflateException("<merge /> can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }
                //這裡直接載入頁面,忽略merge標籤,直接傳root進rInflate進行載入子view
                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                //通過標籤來獲取view
                //先獲取載入資原始檔中的根view
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                //佈局引數          
                ViewGroup.LayoutParams params = null;

                //關鍵程式碼A
                if (root != null) {
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        //temp設定佈局引數
                        temp.setLayoutParams(params);
                    }
                }
                  ......
                //關鍵程式碼B
                //在這裡,先獲取到了temp,再把temp當做root傳進去rInflateChildren
                //進行載入temp後面的子view
                rInflateChildren(parser, temp, attrs, true);
                  ......

                 //關鍵程式碼C
                if (root != null && attachToRoot) {
                    //把view新增到root中並設定佈局引數
                    root.addView(temp, params);
                }

                //關鍵程式碼D
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }

        } catch (XmlPullParserException e) {
            ......
        } catch (Exception e) {
            ......
        } finally {
            ......
        }

        return result;
       }
    }
    // 建立View
    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        // ......
        try {
            // 建立我們的View
            View view;
            if (mFactory2 != null) {
                // 先通過mFactory2 建立,其實在 AppCompatActivity裡面會走這個方法,也就會去替換某些控制元件
                // 所以我們就 看到了上面的內容
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {
                // 走mFactory 
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }
            // ......省略
            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    // 判斷是不是自定義View,自定義View在佈局檔案中com.hc.BannerView是個全類名,
                    // 而系統的View在佈局檔案中不是全類名 TextView
                    if (-1 == name.indexOf(`.`)) {
                        view = onCreateView(parent, name, attrs);
                    } else {
                        view = createView(name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        } catch (InflateException e) {
            // ........
        }
    }
    // 建立View
    public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        // 做一些反射的效能優化

        try {
            // 先從快取中拿,這是沒拿到的情況
            if (constructor == null) {
                // Class not found in the cache, see if it`s real, and try to add it
                // 載入 clazz
                clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
                
                // 建立View的建構函式
                constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
                // 加入快取集合集合
                sConstructorMap.put(name, constructor);
            } else {
                
            }
            // 通過反射建立View
            final View view = constructor.newInstance(args);
            return view;

        } catch (NoSuchMethodException e) {
            // ......省略部分程式碼
        }
    }
複製程式碼

  這裡有兩個思想比較重要第一個View的建立是通過當前View的全類名反射例項化的View,第二個View的建立首先會走mFactory2,然後會走mFactory,只要不為空先會去執行Factory的onCreateView方法,最後才會走系統的LayoutInflater裡面的createView()方法,所以我們完全可以自己去例項化View,這對於我們的外掛化換膚很有幫助。
  基於外掛式換膚框架搭建 – 資源載入原始碼分析外掛式換膚框架搭建 – setContentView原始碼閱讀這兩篇文章我們完全可以自己動手搭建一套換膚框架了,我們下期再見。
  

  所有分享大綱:2017Android進階之路與你同行

  視訊講解地址:http://pan.baidu.com/s/1pLNiBWZ

相關文章