

View#getContext() 不要随便当成 Activity!
source link: https://mp.weixin.qq.com/s?__biz=MzAxMTI4MTkwNQ%3D%3D&%3Bmid=2650833856&%3Bidx=1&%3Bsn=ed3d7713bb0a8ad30663e8c151761db8
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文作者
作者: 彭旭锐
链接:
https://www.jianshu.com/p/7d4bc7083a2f
本文由作者授权发布。
稳住,今天周末,来波小复习吧。
0
前言最近,在玩安卓上看到 每日一问:View#getContext() 一定会返回 Activity 对象么?直觉是:View 是由 Activity 管理的,那么 View#getContext() 一定是 Activity 了,事实真的如此吗?
其实这个问题主要还是考察应试者对于源码(包括:Context类型、LayoutInflater 布局解析、View 体系等)的熟悉度,在这篇文章里,我将跟你一起探讨。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。
目录
1
问题分析1.1 Context 有哪些?
首先,我们回顾一下 Context 以及它的子类,在之前的这篇文章里,我们曾经讨论过:《 Android | 一个进程有多少个 Context 对象(答对的不多) 》。
简单来说:Context 使用了装饰模式,除了 ContextImpl 外,其他 Context 都是 ContextWrapper 的子类。
我们熟悉的 Activity & Service & Application,都是 ContextWrapper 的子类。调用getBaseContext(),可以获得被代理的基础对象:
ContextWrapper.java
Context mBase; public ContextWrapper(Context base) { mBase = base; } public Context getBaseContext() { return mBase; }
需要注意的是, Activity 也是可以作为被代理的对象的 ,类似这样:
Activity activity = ...; Context wrapper = new ContextThemeWrapper(activity, themeResId); wrapper.startActivity(...); // OK wrapper instanceOf Activity // false
这个时候,代理对象wrapper可以使用 Activity 的能力,可以用它 startActivity(),也可以初始化 View,然而它却不是 Activity。看到这里,我们似乎找到了问题的一点苗头了: getContext() 可能返回 Activity 的包装类,而不是 Activity。
1.2 问题延伸
网上讨论得比较多的,主要还是View#getContext()的返回值,在这篇文章里,我们将延伸一下,以下几种情况我都会归纳,以便帮助你建立更为清晰全面的认识:
-
View#getContext()
-
Fragment#getContext()
-
Window#getContext()
-
Dialog#getContext()
2
View#getContext() 的返回值我们来看View#getContext()的源码,可以看到,View#getContext()返回值是在构造函数中设置的,源码里未发现其它赋值语句。
所以,这个问题的关键是看:实例化 View 时传入构造器的 Context 对象。
View.java
@hide protected Context mContext; public final Context getContext() { return mContext; } public View(Context context) { mContext = context; ... } ...
在使用 View 的过程用,有两种方式可以实例化 View :
方法1:代码调用,类似这样:new TextView(Context)
很明显,只要你传入什么对象,将来你调用 getContext(),得到的就是同一个对象。回顾 第 1 节 的讨论,你可以传入 Activity,也可以传入包装类。诶,那可以传入 Service、Application、ContextImpl 吗?
还真的可以,只是你要保证 getContext() 后的行为正确,一般不会这么做。
new TextView(Activity) new TextView(ContextWrapper) new TextView(Service) 一般不会这么做 new TextView(Application) 一般不会这么做 new TextView(ContextImpl) 一般不会这么做
方法2:布局文件,类似这样:<TextView ...>
这种方式其实是利用了 LayoutInflater 布局解析的能力,在之前的这篇文章里,我们曾经讨论过:《Android | 带你探究 LayoutInflater 布局解析原理 https://www.jianshu.com/p/a4dd4892c84e 》,如果你对 LayoutInflater 布局解析的流程还不熟悉,可以先复习下,相同的地方不再重复提。
在这里,我们只关注使用反射实例化 View 的地方:
可以看到,实例化 View 的地方使用了反射,而Constructor#newInstance(...)的首个参数即为将来 getContext() 返回的对象。
那么,mConstructorArgs[0]到底是什么对象呢,是 Activity 吗?我们逆着源码找找看:
LayoutInflater.java
public final View createView(@NonNull Context viewContext, @NonNull String name, @Nullable String prefix, @Nullable AttributeSet attrs){ ... 疑问:viewContext 到底是什么呢? mConstructorArgs[0] = viewContext; final View view = constructor.newInstance(mConstructorArgs); ... } createViewFromTag() -> createView()(已简化) View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) { 1. 应用 ContextThemeWrapper 以支持 android:theme if (!ignoreThemeAttr) { final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME); final int themeResId = ta.getResourceId(0, 0); if (themeResId != 0) { 1.1 注意:这里使用了包装类 context = new ContextThemeWrapper(context, themeResId); } ta.recycle(); } 2. 先使用 Factory2 / Factory 实例化 View,相当于拦截 3. 使用 mPrivateFactory 实例化 View,相当于拦截 4. 调用自身逻辑 if (view == null) { view = createView(name, null, attrs); } return view; } // inflate() -> createViewFromTag() public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { ... 注意:使用了 mContext final Context inflaterContext = mContext; ... final View temp = createViewFromTag(root, name, inflaterContext, attrs); ... } protected LayoutInflater(Context context) { mContext = context; initPrecompiledViews(); }
AppCompatViewInflater.java
2. 先使用 Factory2 / Factory 实例化 View,相当于拦截 final View createView(...) { final Context originalContext = context; 2.1 应用 ContextThemeWrapper 以支持 android:theme / app:theme if (readAndroidTheme || readAppTheme) { context = themifyContext(context, attrs, readAndroidTheme, readAppTheme); } if (wrapContext) { 2.2 应用 ContextThemeWrapper 以支持矢量图 tint context = TintContextWrapper.wrap(context); } View view = null; switch (name) { case "TextView": 2.3 实例化 AppCompatTextView view = createTextView(context, attrs); break; ... default: view = createView(context, name, attrs); } return view; } -> 2.1 应用 ContextThemeWrapper 以支持 android:theme(已简化) private static Context themifyContext(Context context, AttributeSet attrs, boolean useAndroidTheme, boolean useAppTheme) { // 事实上,分支 1.1 已经处理了,这里是兼容 Android 5.0 以前。 return new ContextThemeWrapper(context, themeId); } -> 2.2 应用 ContextThemeWrapper 以支持矢量图 android:tint(已简化) public static Context wrap(@NonNull final Context context) { return new TintContextWrapper(context); }
AppCompatTextView.java
-> 2.3 实例化 AppCompatTextView public AppCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(TintContextWrapper.wrap(context), attrs, defStyleAttr); }
以上代码已经十分简化了,当然你也可以选择直接看结论:
小结:
-
分支 1.1:应用 ContextThemeWrapper 以支持android:theme,此时 View#getContext() 返回这个包装类;
-
分支 2.1:应用 ContextThemeWrapper 以支持android:theme(事实上,分支 1.1 已经处理了,这里是兼容 Android 5.0 前),同样也是返回包装类;
-
分支 2.2:应用 ContextThemeWrapper 以支持矢量图android:tint,这是为了兼容 Android 5.0 以前不支持 tint,同样也是返回包装类;
-
分支 2.3:实例化 AppCompatTextView,同样也是返回包装类;
-
分支 4:返回的是 LayoutInflater#mContext,这个是LayoutInflater.from(Context)传入的参数。在 《Android | 带你探究 LayoutInflater 布局解析原理》里,我们讨论过:在 Activity / Fragment / View / Dialog 中,获取LayoutInflater#getContext(),返回的就是 Activity。
第 2 节讨论完后,下面这几节就容易多了。
3
Dialog & Window 的 getContext() 的返回值直接看源码:
Window.java
private final Context mContext; public final Context getContext() { return mContext; } public Window(Context context) { mContext = context; mFeatures = mLocalFeatures = getDefaultFeatures(context); }
Activity.java
final void attach(Context context, ActivityThread aThread,...){ ... 注意:mContext 为 Activity 本身 mWindow = new PhoneWindow(this, window, activityConfigCallback); ... }
Dialog.java
public Dialog(@NonNull Context context, @StyleRes int themeResId) { this(context, themeResId, true); } Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) { if (createContextThemeWrapper) { if (themeResId == Resources.ID_NULL) { final TypedValue outValue = new TypedValue(); context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true); themeResId = outValue.resourceId; } 包装为 ContextThemeWrapper mContext = new ContextThemeWrapper(context, themeResId); } else { mContext = context; } ... final Window w = new PhoneWindow(mContext); ... }
小结:
-
Dialog#getContext() 返回 ContextThemeWrapper;
-
在 Activity 中,Window#getContext() 返回 Activity;在 Dialog中,Window#getContext() 返回 ContextThemeWrapper;
4
Fragment#getContext() 的返回值直接看源码:
Fragment.java
FragmentHostCallback mHost; public Context getContext() { return mHost == null ? null : mHost.getContext(); }
FragmentHostCallback.java
Context getContext() { return mContext; } FragmentHostCallback(FragmentActivity activity) { this(activity, activity /*context*/, activity.mHandler, 0 /*windowAnimations*/); } FragmentHostCallback(Activity activity, Context context, Handler handler, int windowAnimations) { mActivity = activity; mContext = Preconditions.checkNotNull(context, "context == null"); mHandler = Preconditions.checkNotNull(handler, "handler == null"); mWindowAnimations = windowAnimations; }
FragmentActivity.java
final FragmentController mFragments = FragmentController.createController(new HostCallbacks()); class HostCallbacks extends FragmentHostCallback<FragmentActivity> { public HostCallbacks() { super(FragmentActivity.this /*fragmentActivity*/); } ... }
小结:
-
Fragment#getContext() 返回 Activity;
5
从 View#getContext() 获得 Activity 对象在很多场景中,经常需要通过 View 来获得 Activity 对象,经过前面几节内容的讨论,我们已经知道View#getContext()的返回值总共有以下五种情况:
Activity ContextWrapper Service 一般不会 Application 一般不会 // 预加载的时候可能会 ContextImpl 一般不会
那么,要获得 Activity 则只要不断得获取 Context 的被代理对象(基础对象),就可以获得 Activity;当然了,下面 Service & Application & ContextImpl几种情况是返回空的,所以我们用@Nullable修饰。
递归写法: @Nullable private static Activity findActivity(Context context) { if (context instanceof Activity) { return (Activity) context; } else if (context instanceof ContextWrapper) { return findActivity(((ContextWrapper) context).getBaseContext()); } else { return null; } } 迭代写法: @Nullable public static Activity findActivity(Context context){ Context cur = context; while (true){ if (cur instanceof Activity){ return (Activity) cur; } if (cur instanceof ContextWrapper){ ContextWrapper cw = (ContextWrapper) cur; cur = cw.getBaseContext(); }else{ return null; } } }
总结
应该对Context类型、LayoutInflater 布局解析、View 体系等源码有一定熟悉度,不仅仅能够解答本文问题,更多有意思/深度的问题也能迎刃而解。
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读 :
官方也无力回天?“SharedPreferences 存在什么问题?”
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!
Recommend
-
45
不要把认错当成一种PR!
-
38
-
40
从普通到卓越,要经历执行力,创造力,责任心的洗礼。
-
35
如果你对问题的背景不太熟悉,不如复习一下上一篇,入口》.初级版本这是玩家的抽象基础类,这个设计很好,把一些玩家共有的特性抽象出来//玩家的基础抽象类abstractclassPlayer{//玩家的级别publicintLevel{get;set;}//其他属性代码省略一万字}这是新加需求:10级...
-
38
如果你对问题的背景不太熟悉,不如复习一下上一篇,入口》. 初级版本 这是玩家的抽象基础类,这个设计...
-
6
“千万!不要!随便尝试网红产品!!” 哈哈哈哈哈小丑竟是我自己… 网红产品 时间:2020-12-28 11:52 | 阅读: 1948...
-
9
前言:有不少动效设计的初学者,他们刚开始做UI的动效设计基本都是很生硬,没有一种自然的感觉。他们做的动效往往以装饰性为主,仅仅如此的话,可能会损害产品的可用性。今天给大家介绍12项动效设计原理,这些都适合用于UX/UI设计项目中,是非常有...
-
11
在两人或者多人的对话中,“非主动的请求”是指对方没有直接或者间接要求我提出建议。我不会在这种情况之下提出建议。原因有两个,分别针对男人和女人,或者说拥有这两种典型性格的人。 如果对方是男人 一个社会默认的对男人的期待是,要有荣誉...
-
7
欧雷说:「类似地,不要随便向别人寻求或提供“人生建议”,可以讲故事,但别直接给建议,因为你基本是没有资格的!...」 欧雷 发表于 2022-03-14 13:18 类似地,不要...
-
10
Voyager Digital坠落史:不要随便借钱给「熟人」 • 17 小时前...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK