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

 

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() 代碼,獲取 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>
 * &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;
}
複製代碼

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章