天天看点

activity ontouchevent没有被调用_View.getContext() 一定返回 Activity 对象么?

1 背景

很多时候,我们在代码中可能有如下写法:

(Activity)(view.getContext()),直接强转为Activity后调用其中方法。

又或者担心强转错误,干脆:

Context context = view.getContext():if(context instanceof Activity){// do something}
           

然后else就不管了,反正线上不会崩溃,测试还正常,其实都埋下了风险的种子。

一般我们被问到这样的问题,通常来说,答案都是否定的,但我们一定得知道其中的原因,不然回答肯定与否又有什么意义呢。

首先,显而易见这个问题有不少陷阱,比如这个 View 是我们自己构造出来的,那肯定它的 getContext() 返回的是我们构造它的时候传入的 Context 类型。

2 它也可能返回的是 TintContextWrapper

那,如果是 XML 里面的 View 呢,会怎样?

可能不少人也知道了另外一个结论:

直接继承 Activity 的 Activity 构造出来的 View.getContext() 返回的是当前 Activity。

但是:当 View 的 Activity 是继承自 AppCompatActivity,并且在 5.0 以下版本的手机上,View.getContext() 得到的并非是 Activity,而是 TintContextWrapper。

不太熟悉 

Context

 的继承关系的小伙伴可能也会很奇怪,正常来说,自己所知悉的 

Context

 继承关系图是这样的。

activity ontouchevent没有被调用_View.getContext() 一定返回 Activity 对象么?

Activity.setContentView()

我们可以先看看 

Activity.setContentView()

 方法:

不过是直接调用 

Window

 的实现类 

PhoneWindow

 的 

setContentView()

 方法。看看 

PhoneWindow

 的 

setContentView()

 是怎样的。

假如我们没有 

FEATURE_CONTENT_TRANSITIONS

 标记的话,我们直接通过 

mLayoutInflater.inflate()

 加载出来。这个如果有 

mLayoutInflater

 的是在

PhoneWindow

 的构造方法中被初始化的。而 

PhoneWindow

 的初始化是在 

Activity

 的 

attach()

 方法中:

所以 

PhoneWindow

 的 

Context

 实际上就是 

Activity

 本身。

在回到我们前面分析的 

PhoneWindow

 的 

setContentView()

 方法,如果有 

FEATURE_CONTENT_TRANSITIONS

 标记,我们直接调用了一个 

transitionTo()

 方法:

我们在看看 

scene.enter()

 方法。

基本逻辑没必要详解了吧?

还是通过这个 mContext 的 LayoutInflater 去 inflate 的布局。这个 mContext 初始化的地方是:

public static Scene getSceneForLayout(ViewGroup sceneRoot, int layoutId, Context context) {
    SparseArray scenes = (SparseArray) sceneRoot.getTag(
            com.android.internal.R.id.scene_layoutid_cache);if (scenes == null) {
        scenes = new SparseArray();
        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 版本生成不同的实际执行类,就是代理类的兼容模式:

关于实现类 

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。

可以看到,关键是 super(TintContextWrapper.wrap(context), attrs, defStyleAttr);这行代码。我们点进去看看这个 wrap() 做了什么。

可以看到当,shouldWrap() 这个方法返回为 true 的时候,就会采用 TintContextWrapper 这个对象来包裹了我们的 Context。我们看看什么情况才能满足这个条件。

很明显了吧?如果是 5.0 以前,并且没有包装的话,就会直接返回 true;所以也就得出了上面的结论:

当运行在 5.0 系统版本以下的手机,并且 Activity 是继承自 AppCompatActivity 的,那么View 的 getConext() 方法,返回的就不是 Activity 而是 TintContextWrapper。

3 还有其它情况么?

上面讲述了两种非 Activity 的情况:

  1. 直接构造 View 的时候传入的不是 Activity;
  2. 使用 AppCompatActivity 并且运行在 5.0 以下的手机上,XML 里面的 View 的 getContext() 方法返回的是 TintContextWrapper。

那不禁让人想想,还有其他情况么?有。

我们直接从我前两天线上灰测包出现的一个 bug 说起。

先说说 bug 背景,灰测包是 9.5.0,而线上包是 9.4.0,在灰测包上发生崩溃的代码是三个月前编写的代码,也就是说这可能是 8.43.0 或者 9.0.0 加入的代码,在线上稳定运行了 4 个版本以上没有做过任何修改。但在 9.5.0 灰测的时候,这里却出现了必现崩溃。

单看崩溃日志应该非常好改吧,出现了一个强转错误,原来是在我编写的 ProductReceiveCouponItem 类的 57 行调用项目中的通用对话框 CommonDialog 直接崩溃了。

翻看 CommonDialog 的相关代码发现,原来是之前的同学在使用传入的 Context的时候没有做类型验证,直接强转为了 Activity。

而我的代码通过 View.getContext() 传入的 Context 类型是 ContextThemeWrapper。

看到了日志改起来就非常简单了,第一种方案是直接在 CommonDialog 强转前做一下类型判断。第二种方案是直接在我这里的代码中通过判断 binding.root.context 的类型,然后取出里面的 Activity。

虽然 bug 非常好解决,但作为一名 Android 程序员,绝对不可以满足于仅仅解决 bug 上,任何事情都事出有因,这里为什么数月没有更改的代码,在 9.4.0 上没有问题,在 9.5.0 上就成了必现崩溃呢?

切换代码分支到 9.4.0,debug 发现,这里的 binding.root.context 返回的确实就是 Activity,而在 9.5.0 上 binding.root.context 确实就返回的是 ContextThemeWrapper,检查后确定代码没有任何改动。

分析出现 ContextThemeWrapper 的原因

看到 ContextThemeWrapper,不由得想起了这个类使用的地方之一:Dialog,熟悉 Dialog 的童鞋一定都知道,我们在构造 Dialog 的时候,会把 Context 直接变成 ContextThemeWrapper。

oh,在第三个构造方法中,我们通过构造的时候传入的 createContextThemeWrapper 总是 true,所以它一定可以进到这个 if 语句里面去,把 mContext 强行指向了 Context 的包装类 ContextThemeWrapper。所以这里会不会是由于这个原因呢?

我们在看看我们的代码,我这个 ProductReceiveCouponItem 实际上是一个 RecyclerView 的 Item,而这个相应的 RecyclerView 是显示在 DialogFragment 上的。

熟悉 DialogFragment 的小伙伴可能知道,DialogFragment 实际上也是一个 Fragment。

而 DialogFragment 里面,其实是有一个 Dialog 的变量 mDialog 的,这个 Dialog 会在 onStart() 后通过 show() 展示出来。

在我们使用 DialogFragment 的时候,一定都会重写 onCreatView() 对吧,有一个 LayoutInflater 参数,返回值是一个 View,我们不禁想知道这个 LayoutInflater 是从哪儿来的? 

onGetLayoutInflater(),我们看看。

我们是以一个 Dialog 的形式展示,所以不会进入其中的 if 条件。所以我们直接通过了 onCreateDialog() 构造了一个 Dialog。

如果这个 Dialog 不为空的话,那么我们的 LayoutInflater 就会直接通过 Dialog 的 Context 构造出来。我们来看看 onCreateDialog() 方法。

很简单,直接 new 了一个 Dialog,Dialog 这样的构造方法上面也说了,直接会把 mContext 指向一个 Context 的包装类 ContextThemeWrapper。

至此我们能做大概猜想了,DialogFragment 负责 inflate 出布局的 LayoutInflater是由 ContextThemeWrapper 构造出来的,所以我们暂且在这里说一个结论:

DialogFragment onCreatView() 里面这个 layout 文件里面的 View.getContext() 返回应该是 `ContextThemeWrapper。

但是!!!我们出问题的是 Item,Item 是通过 RecyclerView 的 Adapter 的 ViewHolder 显示出来的,而非 DialogFragent 里面 Dialog 的 setContentView() 的 XML 解析方法。看起来,我们分析了那么多,并没有找到问题的症结所在。

所以我们得看看我们的 Adapter 是怎么写的,直接打开我们的 MultiTypeAdapter 的 onCreateViewHolder() 方法。

oh,在这里我们的 LayoutInflater.from() 接受的参数是 parent.getContext()。parent 是什么?就是我们的 RecyclerView,我们的 RecyclerView 是从哪儿来的?

通过 DialogFragment 的 LayoutInflater 给 inflate 出来的。

所以 parent.getContext() 返回是什么?在这里,一定是 ContextThemeWrapper。

也就是说,我们的 ViewHolder 的 rootView 也就是通过 ContextThemeWrapper 构造的 LayoutInflater 给 inflate 出来的了。

所以我们的 ProductReceiveCouponItem 这个 Item 里面的 binding.root.context 返回值,自然也就是 ContextThemeWrapper 而不是 Activity 了。

自然而然,在 CommonDialog 里面直接强转为 Activity 一定会出错。

那为什么在 9.4.0 上没有出现这个问题呢?

我们看看 9.4.0 上 MultiTypeAdapter 的 onCreateViewHolder() 方法:

咦,看起来似乎不一样,这里直接传入的是 mInflater,我们看看这个 mInflater 是在哪儿被初始化的。

oh,在 9.4.0 的分支上,我们的 ViewHolder 的 LayoutInflater 的 Context,是从外面传进来的。我们看看我们 DialogFragment 中对 RecyclerView 的处理。

是吧,在 9.4.0 的时候,MultiTypeAdapter 的 ViewHolder 会使用我们外接传入的 Context,这个 Context 是 Activity,所以我们的Item 的 binding.root.context 返回为 Activity。

而在 9.5.0 的时候,同事重构了 MultiTypeAdapter,而让其 ViewHolder 的 LayoutInflater 直接取的 parent.getContext(),这里的情况即 ContextThemeWrapper,所以出现了几个月没动的代码,在新版本上灰测却崩溃了。

4 总结

写了这么多,还是做一些总结。首先对题目做个答案: View.getContext() 的返回不一定是 Activity。

实际上,View.getContext() 和 inflate 这个 View 的 LayoutInflater 息息相关,比如 Activity 的 setContentView() 里面的 LayoutInflater 就是它本身,所以该 layoutRes 里面的 View.getContext() 返回的就是 Activity。

但在使用 AppCompatActivity 的时候,值得关注的是, layoutRes 里面的原生 View 会被自动转换为 AppCompatXXX,而这个转换在 5.0 以下的手机系统中,会把 Context 转换为其包装类 TintThemeWrapper,所以在这样的情况下的 View.getContext() 返回是 TintThemeWrapper。

最后,从一个奇怪的 bug 中,给大家分享了一个简单的原因探索分析,也进一步验证了上面的结论。任何 bug 的出现,总是有它的原因,作为 Android 开发,我们不仅要处理掉 bug,更要关注到它的更深层次的原因,这样才能在代码层面就发现其它的潜在问题,以免带来更多不必要的麻烦。

本文就一个简单的示例进行了此次试探的讲解,但个人技术能力有限,唯恐出现纰漏,还望有心人士指出。

文章部分来源于:View.getContext() 里的小秘密,链接:https://juejin.im/post/5a54ad16f265da3e347b211