天天看點

View.getContext() 裡的小秘密

一、引言

關于這篇文章内容适合哪些同學,可以先提幾個問題請大家考慮下。

1、如果通過一個 ImageView 類型的 ​

​view.getContext()​

​ 來擷取到的 context 是什麼類型?

2、Activity 中調用 ​

​setContentView()​

​​ 與 ​

​addContentView()​

​ 方法有什麼差別?

3、AppCompatActivity 相對于 Activity 的 ​

​setContentView()​

​ 方法會有什麼差別麼?

4、Android 是如何從 XML 裡讀取并建構視圖的(或者說是如何建立一個 View 的)?

5、support-v7 包裡如何針對不同版本 API 做到相容和擴充的?

6、AppCompatActivity 裡如何将一些基礎類型控件替換為 AppCompatXXX 控件?具體哪些控件會被替換?什麼時候替換?

如果對上述的問題有疑惑或者有不确定的,都可以在下文裡找到答案。我們會從項目調試時發現的問題入手,逐漸分析找到原因,是以本文可能會有些長,着急的小夥伴可以按照标題找到自己關心的内容。

另外本文所貼的源碼版本為:android-25 、support-v7-25.4.0

為了簡化閱讀,本文中“不相關”的代碼會有些省略,是以有需要的小夥伴可以依照本文給的線索,自行檢視所有源碼。

1.1 View.getContext()

Context context = imageView.getContext();
 if (context instanceof Activity) {
     Activity activity = (Activity)context;
    // ...
 }

複制代碼      

從上面的代碼舉例中可以看到,從 ​

​imageView​

​​ 控件裡擷取到 ​

​context​

​​ ,轉化為 ​

​Activity​

​​ 來繼續操作。這個 ​

​imageView​

​​ 是來自 XML 布局中的一個控件,但在實際項目運作時有的手機并未走到轉換類型的 ​

​if​

​​ 分支裡去,表明這個 ​

​context​

​​ 并非 ​

​Activity​

​ 類型。這個就很奇怪了,為什麼呢?

/**
 * Simple constructor to use when creating a view from code.
 *
 * @param context The Context the view is running in, through which it can
 *        access the current theme, resources, etc.
 */
public View(Context context) {
    mContext = context;
     //...省略
}

@ViewDebug.CapturedViewProperty
public final Context getContext() {
    return mContext;
}
複制代碼      

我們點進去看下 ​

​View.getContext()​

​​ 方法,傳回 ​

​mContext​

​​ 成員變量,而且 ​

​mContext​

​​ 指派隻有在構造函數裡。依據印象,這個 ​

​imageView​

​​ 是寫在 XML 中的,在 ​

​setContextView(R.layout.xxx)​

​​ 時候,實際調用的應該就是 ​

​PhoneWindow​

​​ 裡的 ​

​setContextView()​

​​ 方法,那建構使用的 ​

​context​

​​ 應該就是 ​

​Activity​

​ 類型啊?

這時候我又回去仔細 Debug 了一回,發現出現問題的都是在 5.0 以下的手機裡。是以上面的印象是有問題的,在 5.0 以下,這個 ​

​imageView.getContext()​

​​ 擷取到的 ​

​context​

​​ 類型不是我一開始以為的 ​

​Activity​

​​ 類型,而是 ​

​TintContextWrapper​

1.2 Context 類型

這個 TintContextWrapper 是什麼 Wrapper ?我印象中 Context 的繼承關系中沒有這個啊。 關于 Context 類型 ​​www.jianshu.com/p/94e0f9ab3…​​ 的講解,不清楚的小夥伴可以自行搜尋下,這裡就不展開了,網上能講清楚的也不少,這裡貼個圖看下。

View.getContext() 裡的小秘密

确實也沒有這個 TintContextWrapper 這個類型,從名字看應該也是個 Wrapper 類型的 Context ,還和 Tint 有關系。那剩下的線索還有這個 ​

​imageView​

​​ ,再 Debug 一次,發現這個 ​

​imageView​

​ 的類型也不是原先在 XML 中定義的 ImageView 類型,而是 AppCompatImageView 類型。

猛然醒悟,控件所在的 Activity 是繼承自 AppCompatActivity ,這個 ​

​context​

​ 類型的變化一定是和 v7 包裡的 AppCompatActivity 有關系。之前所謂的印象已經出了兩次錯誤,何不讀源碼解惑?

注意:下面的文章并不是完全依照查問題時的順序來的,而是閱讀完相關源碼後,整理出來的相關知識點。已經清楚的小夥伴可以挑着閱讀。

二、Activity 中 setContentView() 與 addContentView() 的差別

如果多次調用 setContentView() ,則之後每次都會清空 ​

​mContentParent​

​​ 容器。然後組裝資源 ​

​layoutResID​

​ 。

如果多次調用 addContentView() ,則之後每次都會将 View 添加到 ​

​mContentParent​

​ 容器中。最後産生 View 的疊加效果。

這個 ​

​mContentParent​

​ 存在于 PhoneWindow 中。

// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
ViewGroup mContentParent;
複制代碼      

三、AppCompatActivity 和 Activity 的 setContentView() 方法的差別?

setContentView() 方法有兩類,其中一類的必要參數是 XML 布局 id ,另一類的必要參數是 View 類型。

setContentView(@LayoutRes int layoutResID)

setContentView(View view)

這裡我們以參數為 View 類型的代碼讨論。

3.1 Activity

3.1.1 Activity.setContentView()

// Activity代碼
public void setContentView(View view) {
    getWindow().setContentView(view);
    initWindowDecorActionBar();
}

public Window getWindow() {
    return mWindow;
}
複制代碼      

Activity 中 ​

​setContentView()​

​​ 代碼,擷取 ​

​window​

​​ 來 ​

​setContentView()​

​ 。

// Window代碼
public abstract void setContentView(View view);
複制代碼      

而這個 ​

​window​

​​ 其實就是 ​

​PhoneWindow​

​ ,看下面的代碼。

// Activity代碼
final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor,
        Window window) {
    //...省略
    
    mWindow = new PhoneWindow(this, window);
    
    //...省略
}
複制代碼      

3.1.2 PhoneWindow.setContentView()

@Override
public void setContentView(View view) {
    setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}

@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
    // 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)) {
        view.setLayoutParams(params);
        final Scene newScene = new Scene(mContentParent, view);
        transitionTo(newScene);
    } else {
        mContentParent.addView(view, params);
    }
    //...省略
}
複制代碼      

代碼第12行,確定 ​

​mContentParent​

​ 已經初始化過。

第14行,如果沒有 ​

​FEATURE_CONTENT_TRANSITIONS​

​​ ,先清空 ​

​mContentParent​

​ 裡内容。

第22行, ​

​mContentParent​

​​ 将 ​

​view​

​ 當子孩子添加進來。

第17行,如果有 ​

​FEATURE_CONTENT_TRANSITIONS​

​​ ,調用 ​

​transitionTo(newScene)​

​ 。這部分不展開了,最終也是調用以下代碼,邏輯步驟都是一樣的。

//Scene 代碼
//mSceneRoot 就是剛才的 mContentParent
//mLayout 就是 setContentView 方法傳進來的 view

public Scene(ViewGroup sceneRoot, View layout) {
    mSceneRoot = sceneRoot;
    mLayout = layout;
}

public void enter() {
    // Apply layout change, if any
    if (mLayoutId > 0 || mLayout != null) {
        // empty out parent container before adding to it
        getSceneRoot().removeAllViews();

        if (mLayoutId > 0) {
            LayoutInflater.from(mContext).inflate(mLayoutId, mSceneRoot);
        } else {
            mSceneRoot.addView(mLayout);
        }
    }
    //...省略
}
複制代碼      

3.2 AppCompatActivity

可以看到 Activity 中 setContentView() 流程還是比較簡單的,基本上就是調用了PhoneWindow 裡的相應方法。下面我們來看看 AppCompatActivity 中有什麼特别的。

3.2.1 AppCompatActivity.setContentView() 方法

// AppCompatActivity
@Override
public void setContentView(@LayoutRes int layoutResID) {
    getDelegate().setContentView(layoutResID);
}
 
 /**
 * @return The {@link AppCompatDelegate} being used by this Activity.
 */
@NonNull
public AppCompatDelegate getDelegate() {
    if (mDelegate == null) {
        mDelegate = AppCompatDelegate.create(this, this);
    }
    return mDelegate;
}
複制代碼      

​mDelegate​

​ 是一個代理類,由 AppCompatDelegate 根據不同的 SDK 版本生成不同的實際執行類,就是個代理的相容模式。看下面的代碼:

/**
 * Create a {@link android.support.v7.app.AppCompatDelegate} to use with {@code activity}.
 *
 * @param callback An optional callback for AppCompat specific events
 */
public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
    return create(activity, activity.getWindow(), callback);
}

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);
    }
}
複制代碼      

我們可以看到最基礎的就是 AppCompatDelegateImplV9 這個版本,其他的實作類最終都是繼承自這個 AppCompatDelegateImplV9 類的。我們後面要檢視的方法都在 AppCompatDelegateImplV9 這個類實作裡。

是以我們在 AppCompatActivity 中調用 ​

​setContentView()​

​ 方法,實際最終實作都是 AppCompatDelegateImplV9 裡。

3.2.2 AppCompatDelegateImplV9.setContentView() 方法。

// 代理類的具體實作類 AppCompatDelegateImplV9 中 setContentView() 方法
@Override
public void setContentView(View v, ViewGroup.LayoutParams lp) {
    ensureSubDecor();
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    contentParent.addView(v, lp);
    mOriginalWindowCallback.onContentChanged();
}
複制代碼      

從代碼第 5 - 7 行,從 ​

​mSubDector​

​​(類型 ViewGroup )中取出個 ​

​android.R.id.content​

​​ 辨別的 ​

​contentParent​

​​ ,然後重新添加 ​

​view​

​ 。第 8 行回調通知。

那第 4 行代碼從名字上可以看出是確定這個 ​

​mSubDector​

​ 初始化的方法。我們進去看下:

private void ensureSubDecor() {
    if (!mSubDecorInstalled) {
        mSubDecor = createSubDecor();
        
        //...省略...
    }
}
複制代碼      
private ViewGroup createSubDecor() {
     //...省略... 這部分主要針對 AppCompat 樣式檢查和适配

    // Now let's make sure that the Window has installed its decor by retrieving it
    mWindow.getDecorView();

    final LayoutInflater inflater = LayoutInflater.from(mContext);
    ViewGroup subDecor = null;
 
    //...省略... 這部分主要針對不同的樣式設定來初始化不同的 subDecor(inflater 不同的布局 xml )
 
    if (subDecor == null) {
        throw new IllegalArgumentException(
                "AppCompat does not support the current theme features: { "
                        + "windowActionBar: " + mHasActionBar
                        + ", windowActionBarOverlay: "+ mOverlayActionBar
                        + ", android:windowIsFloating: " + mIsFloating
                        + ", windowActionModeOverlay: " + mOverlayActionMode
                        + ", windowNoTitle: " + mWindowNoTitle
                        + " }");
    }

    //...省略...
    
    // Make the decor optionally fit system windows, like the window's decor
    ViewUtils.makeOptionalFitsSystemWindows(subDecor);

    final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
            R.id.action_bar_activity_content);

    final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
    if (windowContentView != null) {
        // There might be Views already added to the Window's content view so we need to
        // migrate them to our content view
        while (windowContentView.getChildCount() > 0) {
            final View child = windowContentView.getChildAt(0);
            windowContentView.removeViewAt(0);
            contentView.addView(child);
        }

        // Change our content FrameLayout to use the android.R.id.content id.
        // Useful for fragments.
        windowContentView.setId(View.NO_ID);
        contentView.setId(android.R.id.content);

        // The decorContent may have a foreground drawable set (windowContentOverlay).
        // Remove this as we handle it ourselves
        if (windowContentView instanceof FrameLayout) {
            ((FrameLayout) windowContentView).setForeground(null);
        }
    }

    // Now set the Window's content view with the decor
    mWindow.setContentView(subDecor);

    //...省略...

    return subDecor;
}
複制代碼      

下面我們重點看一下代碼 28 - 31 行,從 ​

​subDecor​

​​ 中取出了 ​

​R.id.action_bar_activity_content​

​​ 标示的 FrameLayout ,從 ​

​window​

​​ 中取出我們熟悉的 ​

​android.R.id.content​

​​ 标示 ​

​view​

​​ 。這個 ​

​view​

​​ 呢其實就是 PhoneWindow 中 DecorView 裡的 ​

​contentView​

​ 了。

代碼 35 - 38 行,就是将 ​

​window​

​​ 裡取出的 ​

​windowContentView​

​ 裡已有的 childview 依次挪到這個 ​

​subDector​

​​ 取出的 ​

​contentView​

​​ 中去,并清空這個 ​

​windowContentView​

​ 。這裡就達到狸貓換太子的第一步。

代碼 43 - 44 行,接下來将原來 ​

​window​

​​ 裡的 ​

​windowContentView​

​​ 的 id( android.R.id.content )替換給我們 ​

​subDecor​

​​ 裡的 ​

​contentView​

​。

代碼 54 行,狸貓換太子的最後一步,将狸貓 ​

​subDecor​

​​ 設定給 ​

​mWindow​

​ 。

分析完上述代碼,我們再回過來看一下 setContentView() 方法的代碼第 4 行,就不難了解為什麼可以通過 ​

​android.R.id.content​

​ 來取到 “根 View ” 了。

@Override
public void setContentView(View v, ViewGroup.LayoutParams lp) {
    ensureSubDecor();
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    contentParent.addView(v, lp);
    mOriginalWindowCallback.onContentChanged();
}
複制代碼      

四、如何從 XML 裡讀取并建構一個 View?

剛才我們讨論了一類參數為 View 的 setContentView() 方法,現在我們來看下另一個參數為布局 id 的 setContentView() 方法。

4.1 LayoutInflater.inflate() 方法

當我們在 Activity 的 onCreate() 方法裡調用 setContentView(R.layout.xxx) 來設定一個頁面時,最終都會走到類似如下的方法:

​LayoutInflater.from(mContext).inflate(resId, contentParent);​

是以下面我們來看下怎麼 inflate 一個頁面出來。

// LayoutInflater 代碼
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
    return inflate(resource, root, root != null);
}

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) + ")");
    }

    final XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}
複制代碼      

看代碼第 13 行,通過 XML 解析器 XmlResourceParser 來解析我們傳進來的布局檔案的。下面我們貼下第 14 行代碼方法的詳細。

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

        final Context inflaterContext = mContext;
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context) mConstructorArgs[0];
        mConstructorArgs[0] = inflaterContext;
        View result = root;

        try {
            // Look for the root node.
            int type;
            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!");
            }

            final String name = parser.getName();
            
            if (DEBUG) {
                System.out.println("**************************");
                System.out.println("Creating root view: "
                        + name);
                System.out.println("**************************");
            }

            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");
                }

                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // Temp is the root view that was found in the xml
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;

                if (root != null) {
                    if (DEBUG) {
                        System.out.println("Creating params from root: " +
                                root);
                    }
                    // Create layout params that match root, if supplied
                    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);
                    }
                }

                if (DEBUG) {
                    System.out.println("-----> start inflating children");
                }

                // Inflate all children under temp against its context.
                rInflateChildren(parser, temp, attrs, true);

                if (DEBUG) {
                    System.out.println("-----> done inflating children");
                }

                // We are supposed to attach all the views we found (int temp)
                // to root. Do that now.
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }

                // Decide whether to return the root that was passed in or the
                // top view found in xml.
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }

        } catch (XmlPullParserException e) {
            final InflateException ie = new InflateException(e.getMessage(), e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        } catch (Exception e) {
            final InflateException ie = new InflateException(parser.getPositionDescription()
                    + ": " + e.getMessage(), e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        } finally {
            // Don't retain static reference on context.
            mConstructorArgs[0] = lastContext;
            mConstructorArgs[1] = null;

            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }

        return result;
    }
}
複制代碼      

可以看到上面的代碼不是特别多,主要就是根據一個個 XML 中的标簽( </> 封裝的内容),用 ​

​parser​

​ 來解析并做相應處理。

代碼第 74 行将 view 添加到 ​

​root​

​​ 中去。而這個 ​

​root​

​​ 就是一開始傳下來的 ​

​contentParent​

​(類型 ViewGroup )。

那就有疑問了,讀取到标簽,知道是什麼标簽了,比如是個 TextView ,那在什麼地方建立一個 View 呢?

代碼第 41 - 42 行,調用 ​

​createViewFromTag()​

​ 方法來建立 View 的。

// Temp is the root view that was found in the xml

final View temp = createViewFromTag(root, name, inflaterContext, attrs);

4.2 createViewFromTag() 方法

我們簡化掉一部分代碼。

// LayoutInflater 代碼
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) {
//...省略...        
try {
        View view;
        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);
        }

        if (view == null) {
            final Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = context;
            try {
                if (-1 == name.indexOf('.')) {
                    view = onCreateView(parent, name, attrs);
                } else {
                    view = createView(name, null, attrs);
                }
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }

        return view;
    } catch 
//...省略捕獲異常...
}
複制代碼      

其中 ​

​Factory​

​​ 、 ​

​Factory2​

​​ 都是接口,都提供了 ​

​onCreateView()​

​​ 方法,其中 ​

​Factory2​

​​ 繼承自 ​

​Factory​

​ ,擴充了個字段。

public interface Factory {
        /**
         * Hook you can supply that is called when inflating from a LayoutInflater.
         * You can use this to customize the tag names available in your XML
         * layout files.
         * 
         * <p>
         * Note that it is good practice to prefix these custom names with your
         * package (i.e., com.coolcompany.apps) to avoid conflicts with system
         * names.
         * 
         * @param name Tag name to be inflated.
         * @param context The context the view is being created in.
         * @param attrs Inflation attributes as specified in XML file.
         * 
         * @return View Newly created view. Return null for the default
         *         behavior.
         */
        public View onCreateView(String name, Context context, AttributeSet attrs);
    }

    public interface Factory2 extends Factory {
        /**
         * Version of {@link #onCreateView(String, Context, AttributeSet)}
         * that also supplies the parent that the view created view will be
         * placed in.
         *
         * @param parent The parent that the created view will be placed
         * in; <em>note that this may be null</em>.
         * @param name Tag name to be inflated.
         * @param context The context the view is being created in.
         * @param attrs Inflation attributes as specified in XML file.
         *
         * @return View Newly created view. Return null for the default
         *         behavior.
         */
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
    }
複制代碼      

如果所有 factory 都為空或者 factory 建構的 view 為空,則最終調用 ​

​CreareView()​

​ 方法了,關于此方法代碼就不貼了,就是通過控件名字( XML 中标簽名)反射生成個對象,貼一段注釋就明白了。

Low-level function for instantiating a view by name. This attempts to instantiate a view class of the given name found in this LayoutInflater's ClassLoader.

最後的疑問就是這個 Factory(或 Factory2 )接口類型的成員變量什麼時候會指派了?請往下看。

4.3 Activity 中 Factory 指派

我們先看看 Activity 是實作了 LayoutInflater.Factory2 接口的。

public class Activity extends ContextThemeWrapper
        implements LayoutInflater.Factory2,
        Window.Callback, KeyEvent.Callback,
        OnCreateContextMenuListener, ComponentCallbacks2,
        Window.OnWindowDismissedCallback, WindowControllerCallback {
        //...省略
        
   /**
     * Standard implementation of
     * {@link android.view.LayoutInflater.Factory#onCreateView} used when
     * inflating with the LayoutInflater returned by {@link #getSystemService}.
     * This implementation does nothing and is for
     * pre-{@link android.os.Build.VERSION_CODES#HONEYCOMB} apps.  Newer apps
     * should use {@link #onCreateView(View, String, Context, AttributeSet)}.
     *
     * @see android.view.LayoutInflater#createView
     * @see android.view.Window#getLayoutInflater
     */
        @Nullable
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        return null;
    }

    /**
     * Standard implementation of
     * {@link android.view.LayoutInflater.Factory2#onCreateView(View, String, Context, AttributeSet)}
     * used when inflating with the LayoutInflater returned by {@link #getSystemService}.
     * This implementation handles <fragment> tags to embed fragments inside
     * of the activity.
     *
     * @see android.view.LayoutInflater#createView
     * @see android.view.Window#getLayoutInflater
     */
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        if (!"fragment".equals(name)) {
            return onCreateView(name, context, attrs);
        }

        return mFragments.onCreateView(parent, name, context, attrs);
    }

}
複制代碼      

這裡我們有了一個額外的收獲,就是這個 ​

​“fragment”​

​​。如果我們的 XML 中用 fragment 标簽來嵌入一個 Fragment ,在解析 XML 時候,會在 Activity 中調用 ​

​mFragments​

​​ 的 ​

​onCreateView()​

​​ 方法來傳回一個 View ,最後加入到 ​

​contentParent​

​ 中。

4.3.1 Activity 與 LayoutInflater 關聯

// Activity 代碼
final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor,
        Window window) {
    //...省略
   
    mWindow = new PhoneWindow(this, window);
    mWindow.setWindowControllerCallback(this);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    mWindow.getLayoutInflater().setPrivateFactory(this);
複制代碼      

還是這個 ​

​attach()​

​​ 方法( Internal API ),在代碼第 15 行調用了 PhoneWindow 的 ​

​getLayoutInflater()​

​​ 方法,設定了 ​

​privateFactory​

​ 。

public PhoneWindow(Context context) {
    super(context);
    mLayoutInflater = LayoutInflater.from(context);
}

/**
 * Return a LayoutInflater instance that can be used to inflate XML view layout
 * resources for use in this Window.
 *
 * @return LayoutInflater The shared LayoutInflater.
 */
@Override
public LayoutInflater getLayoutInflater() {
    return mLayoutInflater;
}
複制代碼      

代碼已經說明了一切,注釋也很清楚了。

4.4 AppCompatActivity 中 Factory 指派

請往下看

五、AppCompatActivity

我們之前的内容都是一些準備知識,我們最初的問題是 ImageView 裡 getContext() 的類型為什麼在 5.0 以下會是 TintContextWrapper ?什麼時候以及是替換掉的?還沒有解答,下面會陸續給出答案。小夥伴們堅持下!

5.1 AppCompatActivity.onCreate() 方法分析

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    final AppCompatDelegate delegate = getDelegate();
    delegate.installViewFactory();
    delegate.onCreate(savedInstanceState);
    if (delegate.applyDayNight() && mThemeId != 0) {
        // If DayNight has been applied, we need to re-apply the theme for
        // the changes to take effect. On API 23+, we should bypass
        // setTheme(), which will no-op if the theme ID is identical to the
        // current theme ID.
        if (Build.VERSION.SDK_INT >= 23) {
            onApplyThemeResource(getTheme(), mThemeId, false);
        } else {
            setTheme(mThemeId);
        }
    }
    super.onCreate(savedInstanceState);
}
複制代碼      

怎麼樣第 3 行代碼是不是很熟悉,代理加相容模式,這個 AppCompatDelegate 具體實作類我們再看一遍。

// AppCompatActivity 代碼,代碼 8 行的 this 就是這個 Activity 本身。
/**
 * @return The {@link AppCompatDelegate} being used by this Activity.
 */
@NonNull
public AppCompatDelegate getDelegate() {
    if (mDelegate == null) {
        mDelegate = AppCompatDelegate.create(this, this);
    }
    return mDelegate;
}

// AppCompatDelegate代碼    
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);
    }
}
複制代碼      

AppCompatActivity.onCreate() 代碼裡,第 4 行 ​

​delegate.installViewFactory()​

​ 。具體的實作是在 AppCompatDelegateImplV9 裡。看如下代碼:

@Override
public void installViewFactory() {
    LayoutInflater layoutInflater = LayoutInflater.from(mContext);
    if (layoutInflater.getFactory() == null) {
        LayoutInflaterCompat.setFactory(layoutInflater, this);
    } else {
        if (!(LayoutInflaterCompat.getFactory(layoutInflater)
                instanceof AppCompatDelegateImplV9)) {
            Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                    + " so we can not install AppCompat's");
        }
    }
}
複制代碼      

代碼第 3 - 5 行,如果 ​

​layoutInflater​

​​ 的​

​factory​

​​為空,則将自身設定給​

​layoutInflater​

​,達到設定 factory 的效果( 4.3 章節問題解決),也達到了自定義 contentView 的效果。

對比下之前的 setContentView(View view) 代碼,有差別就是在下面的第 6 行。

@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();
}
複制代碼      

還不明白 AppCompatActivity 如何自定義 contentView 的小夥伴,可以回去看看第四章,看看 4.2 createViewFromTag() 方法 章節。對 ​

​contentParent​

​ 有疑問的看看第三章。

聯系下我們最初的問題,在這裡傳給 LayoutInflater 的 ​

​mContext​

​​ 已經替換TintContextWrapper 了麼?當然不是,從 AppCompatActivity.onCreate() 方法裡一路傳下來的 ​

​context​

​ 都是 AppCompatActivity 自身。我們還得往下看。

5.2 AppCompatDelegateImplV9.onCreateView() 方法分析

從 5.1 的代碼我們已經可以看到在 AppCompatActivity 中通過 AppCompatDelegateImplV9 将自己與 LayoutInflater 的 setFactory 系列方法關聯。具體實作 Factory 接口方法也自然在 AppCompatDelegateImplV9 中了。

這裡我們先将 support-v4 包裡 LayoutInflaterFactory 接口等同與 LayoutInflater 的 Factory2 接口,具體如何等效我們後面第 6 章節會講述。
class AppCompatDelegateImplV9 extends AppCompatDelegateImplBase
        implements MenuBuilder.Callback, LayoutInflaterFactory {
        
 //...省略...
 
 /**
 * From {@link android.support.v4.view.LayoutInflaterFactory}
 */
@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    // First let the Activity's Factory try and inflate the view
    final View view = callActivityOnCreateView(parent, name, context, attrs);
    if (view != null) {
        return view;
    }

    // If the Factory didn't handle it, let our createView() method try
    return createView(parent, name, context, attrs);
}
  
//...省略...
    
@Override
public View createView(View parent, final String name, @NonNull Context context,
        @NonNull AttributeSet attrs) {
    if (mAppCompatViewInflater == null) {
        mAppCompatViewInflater = new AppCompatViewInflater();
    }

    //...省略...

    return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
            IS_PRE_LOLLIPOP, /* 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 */
    );
}
 
//...省略...
}
複制代碼      

從上面的代碼可以看到,​

​LayoutInflate​

​​ 裡 Factory2 接口 ​

​onCreateView()​

​​ 方法的實作,是在 AppCompatDelegateImplV9 ( AppCompatActivity 中代理實作類)中并且使用的是 ​

​AppCompatViewInflater​

​ 。忘記了可以回去看看第四章。

我們再進去看看這個 AppCompatViewInflater 的 ​

​createView()​

​ 是做了什麼事情。

5.3 AppCompatViewInflater

“duang duang duang”!

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

    // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
    // by using the parent's context
    if (inheritContext && parent != null) {
        context = parent.getContext();
    }
    if (readAndroidTheme || readAppTheme) {
        // We then apply the theme on the context, if specified
        context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
    }
    if (wrapContext) {
        context = TintContextWrapper.wrap(context);
    }

    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;
        case "Spinner":
            view = new AppCompatSpinner(context, attrs);
            break;
        case "ImageButton":
            view = new AppCompatImageButton(context, attrs);
            break;
        case "CheckBox":
            view = new AppCompatCheckBox(context, attrs);
            break;
        case "RadioButton":
            view = new AppCompatRadioButton(context, attrs);
            break;
        case "CheckedTextView":
            view = new AppCompatCheckedTextView(context, attrs);
            break;
        case "AutoCompleteTextView":
            view = new AppCompatAutoCompleteTextView(context, attrs);
            break;
        case "MultiAutoCompleteTextView":
            view = new AppCompatMultiAutoCompleteTextView(context, attrs);
            break;
        case "RatingBar":
            view = new AppCompatRatingBar(context, attrs);
            break;
        case "SeekBar":
            view = new AppCompatSeekBar(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);
    }

    if (view != null) {
        // If we have created a view, check it's android:onClick
        checkOnClickListener(view, attrs);
    }

    return view;
}
複制代碼      

代碼 15 - 17 行,如果 ​

​wrapContext​

​ 為 true ,将 ​

​context​

​ 用 ​

​TintContextWrapper​

​ 包了一次。我們終于第一次看到這個 ​

​TintContextWrapper​

​ 了!!!下面我們再詳細看。

代碼 23 - 61 行,将一些常見的基礎 View 轉變為 AppCompatXXX 了。終于知道在 AppCompatActivity 中哪些基礎控件會被替換了,具體參見上面的 case 。

代碼 23 - 61 行,将一些常見的基礎 View 轉變為 AppCompatXXX 了。終于知道在 AppCompatActivity 中哪些基礎控件會被替換了,具體參見上面的 case 。

這裡我們隻看下 AppCompatImageView 的構造函數(其他類似),也将 ​

​context​

​​ 用 ​

​TintContextWrapper​

​包下。

public AppCompatImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public AppCompatImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(TintContextWrapper.wrap(context), attrs, defStyleAttr);
       //...省略...
    }
複制代碼      

5.4 TintContextWrapper

代碼直接告訴我們 SDK 版本低于 21 ( android 5.0 ),将 Context 包裝成 TintContextWrapper 類型。 這就是為什麼 XML 中的 ImageView 擷取到的 Context 可能是 TintContextWrapper 類型了。

public static Context wrap(@NonNull final Context context) {
    if (shouldWrap(context)) {
        synchronized (CACHE_LOCK) {
            //...省略...
            
            // If we reach here then the cache didn't have a hit, so create a new instance
            // and add it to the cache
            final TintContextWrapper wrapper = new TintContextWrapper(context);
            
            //...省略...
            
            return wrapper;
        }
    }
    return context;
}

private static boolean shouldWrap(@NonNull final Context context) {
    if (context instanceof TintContextWrapper
            || context.getResources() instanceof TintResources
            || context.getResources() instanceof VectorEnabledTintResources) {
        // If the Context already has a TintResources[Experimental] impl, no need to wrap again
        // If the Context is already a TintContextWrapper, no need to wrap again
        return false;
    }
    return Build.VERSION.SDK_INT < 21 || VectorEnabledTintResources.shouldBeUsed();
}
複制代碼      

5.5 VectorEnabledTintResources.shouldBeUsed()

無論是在 5.2 章節裡 ​

​mAppCompatViewInflater.createView()​

​​ 方法裡還是 ​

​TintContextWrapper.shouldWrap()​

​ 方法裡都有這句 VectorEnabledTintResources.shouldBeUsed() 。我們繼續看下代碼:

@RestrictTo(LIBRARY_GROUP)
public class VectorEnabledTintResources extends Resources {

    public static boolean shouldBeUsed() {
        return AppCompatDelegate.isCompatVectorFromResourcesEnabled()
                && Build.VERSION.SDK_INT <= MAX_SDK_WHERE_REQUIRED;
    }

    /**
     * The maximum API level where this class is needed.
     */
    public static final int MAX_SDK_WHERE_REQUIRED = 20;
    
    //...省略...
}
複制代碼      
//AppCompatDelegate代碼
 //...省略...

 private static boolean sCompatVectorFromResourcesEnabled = false;
 
 //...省略...
 
 /**
 * Sets whether vector drawables on older platforms (< API 21) can be used within
 * {@link android.graphics.drawable.DrawableContainer} resources.
 *
 * <p>When enabled, AppCompat can intercept some drawable inflation from the framework, which
 * enables implicit inflation of vector drawables within
 * {@link android.graphics.drawable.DrawableContainer} resources. You can then use those
 * drawables in places such as {@code android:src} on {@link android.widget.ImageView},
 * or {@code android:drawableLeft} on {@link android.widget.TextView}. Example usage:</p>
 *
 * <pre>
 * <selector xmlns:android="...">
 *     <item android:state_checked="true"
 *           android:drawable="@drawable/vector_checked_icon" />
 *     <item android:drawable="@drawable/vector_icon" />
 * </selector>
 *
 * <TextView
 *         ...
 *         android:drawableLeft="@drawable/vector_state_list_icon" />
 * </pre>
 *
 * <p>This feature defaults to disabled, since enabling it can cause issues with memory usage,
 * and problems updating {@link Configuration} instances. If you update the configuration
 * manually, then you probably do not want to enable this. You have been warned.</p>
 *
 * <p>Even with this disabled, you can still use vector resources through
 * {@link android.support.v7.widget.AppCompatImageView#setImageResource(int)} and it's
 * {@code app:srcCompat} attribute. They can also be used in anything which AppCompat inflates
 * for you, such as menu resources.</p>
 *
 * <p>Please note: this only takes effect in Activities created after this call.</p>
 */
public static void setCompatVectorFromResourcesEnabled(boolean enabled) {
    sCompatVectorFromResourcesEnabled = enabled;
}

/**
 * Returns whether vector drawables on older platforms (< API 21) can be accessed from within
 * resources.
 *
 * @see #setCompatVectorFromResourcesEnabled(boolean)
 */
public static boolean isCompatVectorFromResourcesEnabled() {
    return sCompatVectorFromResourcesEnabled;
}
複制代碼      

那什麼時候 ​

​VectorEnabledTintResources.shouldBeUsed()​

​​ 傳回 true ?當版本低于 5.0 且調用 ​

​AppCompatDelegate.setCompatVectorFromResourcesEnabled​

​ 設定為 true (注意是靜态方法)。

這個 ​

​VectorEnabledTintResources.shouldBeUsed()​

​ 方法其實是判斷當系統在 5.0 以下時,是否要支援矢量圖資源,預設 false 。對這塊有疑惑的同學,可以搜尋相關的矢量圖使用方法,相容低版本政策,這裡就不展開了。

5.6 我們小結下

1、在 AppCompatActivity 中,​

​onCreate()​

​ 方法裡先建立了自己的代理實作類,該類實作了 LayoutInflater.Fatory2 接口(其實是 support-v4 包裡的 LayoutInflaterFactory 接口)。

2、再調用 ​

​installViewFactory()​

​ 方法,将代理實作類和 LayoutInflater 裡的 ​

​factory​

​ 成員變量綁定。

3、當我們自己調用 ​

​setContentView(R.layout.xxx)​

​ 方法後,解析 XML 時會調用到 LayoutInflater 裡的 ​

​inflate()​

​ 方法,再接着是 ​

​createViewFromTag()​

​ 方法。

4、​

​createViewFromTag()​

​ 方法裡如果有 factory 系列的本地變量,就先調用這些接口的 ​

​onCreateView()​

​ 方法。在 AppCompatActivity 中 ​

​onCreateView()​

​ 是在 AppCompatDelegateImplV9 裡。

5、AppCompatDelegateImplV9 裡用 ​

​AppCompatViewInflater​

​ 來生成 View。是以有了替換基礎控件的内容,有了 5.0 以下系統将 ​

​Context​

​ 包裝成​

​TintContextWrapper​

​ ,建構 AppCompatxxx 控件時,傳入的 ​

​context​

​ 被替換成了 ​

​TintContextWrapper​

​ 類型。

六、V4包的LayoutInflater接口如何等效LayoutInflter的Factory2接口?

@Override
public void installViewFactory() {
    LayoutInflater layoutInflater = LayoutInflater.from(mContext);
    if (layoutInflater.getFactory() == null) {
        LayoutInflaterCompat.setFactory(layoutInflater, this);
    } else {
     //...省略...
    }
       
}
複制代碼      

最後的疑問了:代碼第 5 行,如何将 ​

​layoutInflater​

​ 接受的Factory(Factory2)類型變為接受 this(實作了 android.support.v4.view.LayoutInflaterFactory 接口)??

先看下 v4 包裡關于 LayoutInflaterFactory 的注釋,可以明白其意圖。如何實作這樣的目的,我們往下看 6.1 章。

/**
 * Used with {@code LayoutInflaterCompat.setFactory()}. Offers the same API as
 * {@code LayoutInflater.Factory2}.
 */
public interface LayoutInflaterFactory {

/**
 * Hook you can supply that is called when inflating from a LayoutInflater.
 * You can use this to customize the tag names available in your XML
 * layout files.
 *
 * @param parent The parent that the created view will be placed
 * in; <em>note that this may be null</em>.
 * @param name Tag name to be inflated.
 * @param context The context the view is being created in.
 * @param attrs Inflation attributes as specified in XML file.
 *
 * @return View Newly created view. Return null for the default
 *         behavior.
 */
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);

}
複制代碼      

6.1 LayoutInflaterCompat

我們回到 android.support.v4.view.LayoutInflaterCompat 裡看做了什麼。

// 代碼android.support.v4.view.LayoutInflaterCompat

/**
 * Attach a custom Factory interface for creating views while using
 * this LayoutInflater. This must not be null, and can only be set once;
 * after setting, you can not change the factory.
 *
 * @see LayoutInflater#setFactory(android.view.LayoutInflater.Factory)
 */
public static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {
    IMPL.setFactory(inflater, factory);
}
     
static final LayoutInflaterCompatImpl IMPL;

static {
    final int version = Build.VERSION.SDK_INT;
    if (version >= 21) {
        IMPL = new LayoutInflaterCompatImplV21();
    } else if (version >= 11) {
        IMPL = new LayoutInflaterCompatImplV11();
    } else {
        IMPL = new LayoutInflaterCompatImplBase();
    }
}
複制代碼      

又是我們熟悉的代理模式,實作類 ​

​IMP​

​ 又是一個相容模式。

我們看一個最簡單的 ​

​LayoutInflaterCompatBase​

​ 的代碼實作就明白了。

//代碼LayoutInflaterCompat

interface LayoutInflaterCompatImpl {
    public void setFactory(LayoutInflater layoutInflater, LayoutInflaterFactory factory);
    public LayoutInflaterFactory getFactory(LayoutInflater layoutInflater);
}

static class LayoutInflaterCompatImplBase implements LayoutInflaterCompatImpl {
    @Override
    public void setFactory(LayoutInflater layoutInflater, LayoutInflaterFactory factory) {
        LayoutInflaterCompatBase.setFactory(layoutInflater, factory);
    }

    @Override
    public LayoutInflaterFactory getFactory(LayoutInflater layoutInflater) {
        return LayoutInflaterCompatBase.getFactory(layoutInflater);
    }
}

複制代碼      

6.2 LayoutInflaterCompatBase

class LayoutInflaterCompatBase {

static class FactoryWrapper implements LayoutInflater.Factory {

    final LayoutInflaterFactory mDelegateFactory;

    FactoryWrapper(LayoutInflaterFactory delegateFactory) {
        mDelegateFactory = delegateFactory;
    }

    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        return mDelegateFactory.onCreateView(null, name, context, attrs);
    }

    public String toString() {
        return getClass().getName() + "{" + mDelegateFactory + "}";
    }
}

static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {
    inflater.setFactory(factory != null ? new FactoryWrapper(factory) : null);
}
    
//...省略...
}
複制代碼      

代碼第 22 行,将 v4 包裡的 LayoutInflaterFactory 包裝成 FactoryWrapper 類型,再調用 LayoutInflater 的 ​

​setFactory()​

​ 方法。

代碼 13 行,運用代理模式。FactoryWrapper 實作了 LayoutInflater 的 Factory 接口,在具體的 ​

​onCreateView()​

​ 方法實作中替換為代理類來實作。

代碼第 7 行,FactoryWrapper 的構造函數入參就是個代理類,類型正是 v4 包裡的 LayoutInflaterFactory 接口。

6.3 小結一下:

1、在 ​

​LayoutInflaterCompat.setFactory(layoutInflater, this);​

​ 裡,通過一系列的代理相容模式,将 LayoutInflater 的 ​

​setFactory()​

​ 系列方法接收的參數,變化為 v4 包裡的 LayoutInflaterFactory 接口類型參數。

七、解決辦法

@Nullable
private Activity getActivity(@NonNull View view) {
    if (null != view) {
        Context context = view.getContext();
        while (context instanceof ContextWrapper) {
            if (context instanceof Activity) {
                return (Activity) context;
            }
            context = ((ContextWrapper) context).getBaseContext();
        }
    }

    return null;
}
複制代碼