我们不得不看看 AppCompatActivity 的 setContentView() 是怎么实现的。
public void setContentView(@LayoutRes int layoutResID) { this.getDelegate().setContentView(layoutResID); } @NonNull public AppCompatDelegate getDelegate() { if (this.mDelegate == null) { this.mDelegate = AppCompatDelegate.create(this, this); } return this.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); } }关于实现类 AppCompatDelegateImpl 的 setContentView() 方法这里就不做过多分析了,感兴趣的可以直接移步掘金上的 View.getContext() 里的小秘密 进行查阅。
不过这里还是要结合小缘的回答,简单总结一下:之所以能得到上面的结论是因为我们在 AppCompatActivity 里面的 layout.xml 文件里面使用原生控件,比如 TextView、ImageView 等等,当在 LayoutInflater 中把 XML 解析成 View 的时候,最终会经过 AppCompatViewInflater 的 createView() 方法,这个方法会把这些原生的控件都变成 AppCompatXXX 一类。包含了哪些 View 呢?
RatingBar
CheckedTextView
MultiAutoCompleteTextView
TextView
ImageButton
SeekBar
Spinner
RadioButton
ImageView
AutoCompleteTextView
CheckBox
EditText
Button
那么重点肯定就是在 AppCompat 这些开头的控件了,随便打开一个源码吧,比如 AppCompatTextView。
public AppCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(TintContextWrapper.wrap(context), attrs, defStyleAttr); this.mBackgroundTintHelper = new AppCompatBackgroundHelper(this); this.mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr); this.mTextHelper = new AppCompatTextHelper(this); this.mTextHelper.loadFromAttributes(attrs, defStyleAttr); this.mTextHelper.applyCompoundDrawablesTints(); }可以看到,关键是 super(TintContextWrapper.wrap(context), attrs, defStyleAttr); 这行代码。我们点进去看看这个 wrap() 做了什么。
public static Context wrap(@NonNull Context context) { if (shouldWrap(context)) { Object var1 = CACHE_LOCK; synchronized(CACHE_LOCK) { if (sCache == null) { sCache = new ArrayList(); } else { int i; WeakReference ref; for(i = sCache.size() - 1; i >= 0; --i) { ref = (WeakReference)sCache.get(i); if (ref == null || ref.get() == null) { sCache.remove(i); } } for(i = sCache.size() - 1; i >= 0; --i) { ref = (WeakReference)sCache.get(i); TintContextWrapper wrapper = ref != null ? (TintContextWrapper)ref.get() : null; if (wrapper != null && wrapper.getBaseContext() == context) { return wrapper; } } } TintContextWrapper wrapper = new TintContextWrapper(context); sCache.add(new WeakReference(wrapper)); return wrapper; } } else { return context; } }可以看到当,shouldWrap() 这个方法返回为 true 的时候,就会采用了 TintContextWrapper 这个对象来包裹了我们的 Context。来看看什么情况才能满足这个条件。
private static boolean shouldWrap(@NonNull Context context) { if (!(context instanceof TintContextWrapper) && !(context.getResources() instanceof TintResources) && !(context.getResources() instanceof VectorEnabledTintResources)) { return VERSION.SDK_INT < 21 || VectorEnabledTintResources.shouldBeUsed(); } else { return false; } }很明显了吧?如果是 5.0 以前,并且没有包装的话,就会直接返回 true;所以也就得出了上面的结论:当运行在 5.0 系统版本以下的手机,并且 Activity 是继承自 AppCompatActivity 的,那么View 的 getConext() 方法,返回的就不是 Activity 而是 TintContextWrapper。
还有其它情况么?上面讲述了两种非 Activity 的情况:
直接构造 View 的时候传入的不是 Activity;
使用 AppCompatActivity 并且运行在 5.0 以下的手机上,XML 里面的 View 的 getContext() 方法返回的是 TintContextWrapper。
那不禁让人想想,还有其他情况么?有。