View.getContext() 裡的小祕密

梅花發表於2018-01-10

一、引言

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

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… 的講解,不清楚的小夥伴可以自行搜尋下,這裡就不展開了,網上能講清楚的也不少,這裡貼個圖看下。

cmd-markdown-logo

確實也沒有這個 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() 程式碼,獲取 windowsetContentView()

// 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行, mContentParentview 當子孩子新增進來。

第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 
//...省略捕獲異常...
}
複製程式碼

其中 FactoryFactory2 都是介面,都提供了 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 中呼叫 mFragmentsonCreateView() 方法來返回一個 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 行,如果 layoutInflaterfactory為空,則將自身設定給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 ,將 contextTintContextWrapper 包了一次。我們終於第一次看到這個 TintContextWrapper 了!!!下面我們再詳細看。

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

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

這裡我們只看下 AppCompatImageView 的建構函式(其他類似),也將 contextTintContextWrapper包下。

   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>
 * &lt;selector xmlns:android=&quot;...&quot;&gt;
 *     &lt;item android:state_checked=&quot;true&quot;
 *           android:drawable=&quot;@drawable/vector_checked_icon&quot; /&gt;
 *     &lt;item android:drawable=&quot;@drawable/vector_icon&quot; /&gt;
 * &lt;/selector&gt;
 *
 * &lt;TextView
 *         ...
 *         android:drawableLeft=&quot;@drawable/vector_state_list_icon&quot; /&gt;
 * </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 介面型別引數。

2、傳入的 this 就是 AppCompatDelegateImplV9 本身。所以 Factory 系列介面的 onCreateView() 方法實現,就落到了 AppCompatDelegateImplV9 裡的方法裡。

七、解決辦法

1、問題 View.getContext() 如何強制轉為 Activity ?

下面給個常用思路作為參考:

@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;
}
複製程式碼

八、其他

首先這篇文章貼的原始碼有點多,分析的內容也只是原始碼中的一部分。再加上這麼長的內容,並沒有一個很好的敘述順序將這些內容有趣味性地串起來。所以堅持下來的小夥伴們,給你們點個贊!

本文起因來自一個需要修復的專案 bug ,後來在團隊內的技術交流會中分享後,重新完善寫下來的。文字對比現場講解還是少了些互動交流,所以有寫得疏漏地方和錯誤地方,請大家不吝指教。謝謝了!

歡迎留言或者發郵件給我:fanzhu@imdada.cn

相關文章