Android每日一问笔记-View中的getContext一定返回的是Activity对象吗?

基于https://www.wanandroid.com每日一问的笔记,做一些整理,方便自己进行查看和记忆。

原文链接:https://www.wanandroid.com/wenda/show/8626
以及
nanchen的文章


不一定是

那么,在什么场景下不是呢:

  • 除了自己手动传不是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。
    image

Activity.setContentView()

  • 看看Activity.setContentView()方法。不过是直接调用 Window 的实现类 PhoneWindow 的 setContentView() 方法
public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}
  • 看看 PhoneWindow 的 setContentView() 是怎样的
@Override
public 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() 方法中:
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, 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() 方法:
private void transitionTo(Scene scene) {
    if (mContentScene == null) {
        scene.enter();
    } else {
        mTransitionManager.transitionTo(scene);
    }
    mContentScene = scene;
}


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);
        }
    }
    // 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 初始化的地方是:
public 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 版本生成不同的实际执行类,就是代理类的兼容模式:
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;
}



/**
 * 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。
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();
}

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。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值