基于https://www.wanandroid.com每日一问的笔记,做一些整理,方便自己进行查看和记忆。
不一定是
那么,在什么场景下不是呢:
- 除了自己手动传不是Activity的Context进去之外,还有一种情况,就是:当使用AppCompatActivity 时。
- 我们都知道,在这个Activity里的原生控件(如TextView, ImageView等等),当在LayoutInflater中把xml解析成View的时候,最终会经过AppCompatViewInflater的createView方法: 把这些原生控件都变成AppCompatXXX一类的!比如TextView的话,就会变成AppCompatTextView, ImageView会变成AppCompatImageView 。
当然了,这些AppCompat开头的,都是继承于被转换的那个对象的。
那重点就在这些AppCompat开头的控件了,随便打开一个他们源码,比如AppCompatImageView
- 打开之后会看到: 当它们调用父类的构造方法时,调用了TintContextWrappe
- 看这个方法的名字, wrap很明显就是包装的意思嘛,点进去wrap方法看,还会看到首先调用了shouldWrap方法: 检查一下这个context应不应该被包装。
- 如果方法返回true, 会创建一个TintContextWrapper对象(把Context传进去),然后返回,那么,这时候,当我们调用这个View的getContext方法,自然就不是Activity了,而是它传进去的TintContextWrapper。
那么,究竟什么情况下,shouldWrap方法会返回true呢(Context会被包装), 点开看下源码:
- 如果它已经被包装过了,那么就不需要继续包装,即返回false了。
- 如果没有被包装过,并且Build.VERSION.SDK_INT<21(也就是5.0之前的版本),就会返回true。
得出结论:
- 当运行在5.0系统版本以下的手机,并且Activity是继承自AppCompatActivity的,那么View的getConext方法,返回的就不是Activity而是TintContextWrapper.
- 首先,显而易见这个问题有不少陷阱,比如这个
View
是我们自己构造出来的,那肯定它的getContext()
返回的是我们构造它的时候传入的 Context 类型。 - 但是
View.getContext()
它也可能返回的是TintContextWrapper
- 直接继承 Activity 的 Activity 构造出来的
View.getContext()
返回的是当前 Activity。但是:当 View 的 Activity 是继承自 AppCompatActivity,并且在 5.0 以下版本的手机上,View.getContext() 得到的并非是 Activity,而是 TintContextWrapper。imageActivity.setContentView()
看看
Activity.setContentView()
方法。不过是直接调用 Window 的实现类 PhoneWindow 的 setContentView() 方法1234public void setContentView(@LayoutRes int layoutResID) {getWindow().setContentView(layoutResID);initWindowDecorActionBar();}看看 PhoneWindow 的 setContentView() 是怎样的
12345678910111213141516171819202122232425public void setContentView(int layoutResID) {// 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)) {final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,getContext());transitionTo(newScene);} else {mLayoutInflater.inflate(layoutResID, mContentParent);}mContentParent.requestApplyInsets();final Callback cb = getCallback();if (cb != null && !isDestroyed()) {cb.onContentChanged();}mContentParentExplicitlySet = true;}假如我们没有
FEATURE_CONTENT_TRANSITIONS
标记的话,我们直接通过mLayoutInflater.inflate()
加载出来。这个如果有 mLayoutInflater 的是在PhoneWindow 的构造方法中被初始化的。而 PhoneWindow 的初始化是在 Activity的attach() 方法中:1234567891011121314151617final 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, ActivityConfigCallback activityConfigCallback) {attachBaseContext(context);mFragments.attachHost(null /*parent*/);mWindow = new PhoneWindow(this, window, activityConfigCallback);mWindow.setWindowControllerCallback(this);mWindow.setCallback(this);mWindow.setOnWindowDismissedCallback(this);mWindow.getLayoutInflater().setPrivateFactory(this);// 此处省略部分代码...}所以 PhoneWindow 的 Context 实际上就是 Activity 本身
回到我们前面分析的 PhoneWindow 的 setContentView() 方法,如果有 FEATURE_CONTENT_TRANSITIONS 标记,我们直接调用了一个 transitionTo() 方法:
123456789101112131415161718192021222324252627private void transitionTo(Scene scene) {if (mContentScene == null) {scene.enter();} else {mTransitionManager.transitionTo(scene);}mContentScene = scene;}public void enter() {// Apply layout change, if anyif (mLayoutId > 0 || mLayout != null) {// empty out parent container before adding to itgetSceneRoot().removeAllViews();if (mLayoutId > 0) {LayoutInflater.from(mContext).inflate(mLayoutId, mSceneRoot);} else {mSceneRoot.addView(mLayout);}}// Notify next scene that it is entering. Subclasses may override to configure scene.if (mEnterAction != null) {mEnterAction.run();}setCurrentScene(mSceneRoot, this);}还是通过这个 mContext 的 LayoutInflater 去 inflate 的布局。这个 mContext 初始化的地方是:
12345678910111213141516public static Scene getSceneForLayout(ViewGroup sceneRoot, int layoutId, Context context) {SparseArray<Scene> scenes = (SparseArray<Scene>) sceneRoot.getTag(com.android.internal.R.id.scene_layoutid_cache);if (scenes == null) {scenes = new SparseArray<Scene>();sceneRoot.setTagInternal(com.android.internal.R.id.scene_layoutid_cache, scenes);}Scene scene = scenes.get(layoutId);if (scene != null) {return scene;} else {scene = new Scene(sceneRoot, layoutId, context);scenes.put(layoutId, scene);return scene;}}即 Context 来源于我们外面传入的 getContext(),这个 getContext() 返回的就是初始化的 Context 也就是 Activity 本身。
AppCompatActivity.setContentView()
- AppCompatActivity 的 setContentView() 实现。这个 mDelegate 实际上是一个代理类,由 AppCompatDelegate 根据不同的 SDK 版本生成不同的实际执行类,就是代理类的兼容模式:123456789101112131415161718192021222324252627282930313233343536373839public void setContentView(@LayoutRes int layoutResID) {this.getDelegate().setContentView(layoutResID);}public AppCompatDelegate getDelegate() {if (this.mDelegate == null) {this.mDelegate = AppCompatDelegate.create(this, this);}return this.mDelegate;}/*** 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);}}
简单总结
- 之所以能得到上面的结论,是因为我们在 AppCompatActivity 里面的 layout.xml 文件里面使用原生控件,比如 TextView、ImageView 等等,当在 LayoutInflater 中把 XML 解析成 View 的时候,最终会经过 AppCompatViewInflater 的 createView() 方法,这个方法会把这些原生的控件都变成 AppCompatXXX 一类。
- 包含了:
- RatingBar
- CheckedTextView
- MultiAutoCompleteTextView
- TextView
- ImageButton
- SeekBar
- Spinner
- RadioButton
- ImageView
- AutoCompleteTextView
- CheckBox
- EditText
- Button
那么重点肯定就是在 AppCompat这些开头的控件了,随便打开一个源码.可以看到,关键是
super(TintContextWrapper.wrap(context), attrs, defStyleAttr);
这行代码。shouldWrap() 这个方法返回为 true 的时候,就会采用了 TintContextWrapper 这个对象来包裹了我们的 Context。如果是 5.0 以前,并且没有包装的话,就会直接返回 true。12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849public 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();}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;}}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 系统版本以下的手机,并且 Activity 是继承自 AppCompatActivity 的,那么View 的 getConext() 方法,返回的就不是 Activity 而是 TintContextWrapper。
其它情况么
- 上面讲述了两种非 Activity 的情况:
- 直接构造 View 的时候传入的不是 Activity;
- 使用 AppCompatActivity 并且运行在 5.0 以下的手机上,XML 里面的 View 的 getContext() 方法返回的是 TintContextWrapper。
- 实际上,View.getContext() 和 inflate 这个 View 的 LayoutInflater 息息相关,比如 Activity 的 setContentView() 里面的 LayoutInflater 就是它本身,所以该 layoutRes 里面的 View.getContext() 返回的就是 Activity。但在使用 AppCompatActivity 的时候,值得关注的是, layoutRes 里面的原生 View 会被自动转换为 AppCompatXXX,而这个转换在 5.0 以下的手机系统中,会把 Context 转换为其包装类 TintThemeWrapper,所以在这样的情况下的 View.getContext() 返回是 TintThemeWrapper。