堅(jiān)持原創(chuàng)日更,短平快的 Android 進(jìn)階系列,敬請(qǐng)直接在微信公眾號(hào)搜索:nanchen,直接關(guān)注并設(shè)為星標(biāo),精彩不容錯(cuò)過(guò)。
一般我們被問(wèn)到這樣的問(wèn)題,通常來(lái)說(shuō),答案都是否定的,但一定得知道其中的原因,不然回答肯定與否又有什么意義呢。
首先,顯而易見這個(gè)問(wèn)題有不少陷阱,比如這個(gè) View 是自己構(gòu)造出來(lái)的,那肯定它的 getContext()
返回的是構(gòu)造它的時(shí)候傳入的 Context
類型。
它也可能返回的是 TintContextWrapper
那,如果是 XML 里面的 View 呢,會(huì)怎樣?可能不少人也知道了另外一個(gè)結(jié)論:直接繼承 Activity 的 Activity 構(gòu)造出來(lái)的 View.getContext() 返回的是當(dāng)前 Activity。但是:當(dāng) View 的 Activity 是繼承自 AppCompatActivity,并且在 5.0 以下版本的手機(jī)上,View.getContext() 得到的并非是 Activity,而是 TintContextWrapper。
不太熟悉 Context
的繼承關(guān)系的小伙伴可能也會(huì)很奇怪,正常來(lái)說(shuō),自己所知悉的 Context
繼承關(guān)系圖是這樣的。
Activity.setContentView()
我們可以先看看 Activity.setContentView()
方法:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
不過(guò)是直接調(diào)用 Window
的實(shí)現(xiàn)類 PhoneWindow
的 setContentView()
方法??纯?PhoneWindow
的 setContentView()
是怎樣的。
@Override
public void setContentView(int layoutResID) {
// 省略部分代碼...
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
// 省略部分代碼...
}
假如沒有 FEATURE_CONTENT_TRANSITIONS
標(biāo)記的話,就直接通過(guò) mLayoutInflater.inflate()
加載出來(lái)。這個(gè)如果有 mLayoutInflater
的是在PhoneWindow
的構(gòu)造方法中被初始化的。而 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
實(shí)際上就是 Activity
本身。
在回到我們前面分析的 PhoneWindow
的 setContentView()
方法,如果有 FEATURE_CONTENT_TRANSITIONS
標(biāo)記,直接調(diào)用了一個(gè) transitionTo()
方法:
private void transitionTo(Scene scene) {
if (mContentScene == null) {
scene.enter();
} else {
mTransitionManager.transitionTo(scene);
}
mContentScene = scene;
}
在看看 scene.enter()
方法。
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);
}
}
// 省略部分代碼...
}
基本邏輯沒必要詳解了吧?還是通過(guò)這個(gè) mContext
的 LayoutInflater
去 inflate
的布局。這個(gè) mContext
初始化的地方是:
public static Scene getSceneForLayout(ViewGroup sceneRoot, int layoutId, Context context) {
// 省略部分代碼...
if (scene != null) {
return scene;
} else {
scene = new Scene(sceneRoot, layoutId, context); // 初始化關(guān)鍵代碼
scenes.put(layoutId, scene);
return scene;
}
}
即 Context
來(lái)源于外面?zhèn)魅氲?getContext()
,這個(gè) getContext()
返回的就是初始化的 Context
也就是 Activity
本身。
AppCompatActivity.setContentView()
我們不得不看看 AppCompatActivity
的 setContentView()
是怎么實(shí)現(xiàn)的。
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;
}
這個(gè) mDelegate
實(shí)際上是一個(gè)代理類,由 AppCompatDelegate
根據(jù)不同的 SDK 版本生成不同的實(shí)際執(zhí)行類,就是代理類的兼容模式:
/**
* 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);
}
}
關(guān)于實(shí)現(xiàn)類 AppCompatDelegateImpl
的 setContentView()
方法這里就不做過(guò)多分析了,感興趣的可以直接移步掘金上的 View.getContext() 里的小秘密 進(jìn)行查閱。
不過(guò)這里還是要結(jié)合小緣的回答,簡(jiǎn)單總結(jié)一下:之所以能得到上面的結(jié)論是因?yàn)槲覀冊(cè)?AppCompatActivity
里面的 layout.xml
文件里面使用原生控件,比如 TextView
、ImageView
等等,當(dāng)在 LayoutInflater
中把 XML 解析成 View
的時(shí)候,最終會(huì)經(jīng)過(guò) AppCompatViewInflater
的 createView()
方法,這個(gè)方法會(huì)把這些原生的控件都變成 AppCompatXXX
一類。包含了哪些 View 呢?
- RatingBar
- CheckedTextView
- MultiAutoCompleteTextView
- TextView
- ImageButton
- SeekBar
- Spinner
- RadioButton
- ImageView
- AutoCompleteTextView
- CheckBox
- EditText
- Button
那么重點(diǎn)肯定就是在 AppCompat
這些開頭的控件了,隨便打開一個(gè)源碼吧,比如 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();
}
可以看到,關(guān)鍵是 super(TintContextWrapper.wrap(context), attrs, defStyleAttr);
這行代碼。我們點(diǎn)進(jìn)去看看這個(gè) wrap()
做了什么。
public static Context wrap(@NonNull Context context) {
if (shouldWrap(context)) {
// 省略關(guān)鍵代碼...
TintContextWrapper wrapper = new TintContextWrapper(context);
sCache.add(new WeakReference(wrapper));
return wrapper;
} else {
return context;
}
}
可以看到當(dāng),shouldWrap()
這個(gè)方法返回為 true 的時(shí)候,就會(huì)采用了 TintContextWrapper
這個(gè)對(duì)象來(lái)包裹了我們的 Context
。來(lái)看看什么情況才能滿足這個(gè)條件。
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 以前,并且沒有包裝的話,就會(huì)直接返回 true;所以也就得出了上面的結(jié)論:當(dāng)運(yùn)行在 5.0 系統(tǒng)版本以下的手機(jī),并且 Activity
是繼承自 AppCompatActivity
的,那么View
的 getConext()
方法,返回的就不是 Activity
而是 TintContextWrapper
。
還有其它情況么?
上面講述了兩種非 Activity
的情況:
- 直接構(gòu)造
View
的時(shí)候傳入的不是Activity
; - 使用
AppCompatActivity
并且運(yùn)行在 5.0 以下的手機(jī)上,XML 里面的View
的getContext()
方法返回的是TintContextWrapper
。
那不禁讓人想想,還有其他情況么?有。
我們直接從我前兩天線上灰測(cè)包出現(xiàn)的一個(gè) bug 說(shuō)起。先說(shuō)說(shuō) bug 背景,灰測(cè)包是 9.5.0,而線上包是 9.4.0,在灰測(cè)包上發(fā)生崩潰的代碼是三個(gè)月前編寫的代碼,也就是說(shuō)這可能是 8.43.0 或者 9.0.0 加入的代碼,在線上穩(wěn)定運(yùn)行了 4 個(gè)版本以上沒有做過(guò)任何修改。但在 9.5.0 灰測(cè)的時(shí)候,這里卻出現(xiàn)了必現(xiàn)崩潰。
Fatal Exception: java.lang.ClassCastException: android.view.ContextThemeWrapper cannot be cast to android.app.Activity
at com.codoon.common.dialog.CommonDialog.openProgressDialog + 145(CommonDialog.java:145)
at com.codoon.common.dialog.CommonDialog.openProgressDialog + 122(CommonDialog.java:122)
at com.codoon.common.dialog.CommonDialog.openProgressDialog + 116(CommonDialog.java:116)
at com.codoon.find.product.item.detail.i$a.onClick + 57(ProductReceiveCouponItem.kt:57)
at android.view.View.performClick + 6266(View.java:6266)
at android.view.View$PerformClick.run + 24730(View.java:24730)
at android.os.Handler.handleCallback + 789(Handler.java:789)
at android.os.Handler.dispatchMessage + 98(Handler.java:98)
at android.os.Looper.loop + 171(Looper.java:171)
at android.app.ActivityThread.main + 6699(ActivityThread.java:6699)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run + 246(Zygote.java:246)
at com.android.internal.os.ZygoteInit.main + 783(ZygoteInit.java:783)
單看崩潰日志應(yīng)該非常好改吧,出現(xiàn)了一個(gè)強(qiáng)轉(zhuǎn)錯(cuò)誤,原來(lái)是在我編寫的 ProductReceiveCouponItem
類的 57 行調(diào)用項(xiàng)目中的通用對(duì)話框 CommonDialog
直接崩潰了。翻看 CommonDialog
的相關(guān)代碼發(fā)現(xiàn),原來(lái)是之前的同學(xué)在使用傳入的 Context
的時(shí)候沒有做類型驗(yàn)證,直接強(qiáng)轉(zhuǎn)為了 Activity
。
// 得到等待對(duì)話框
public void openProgressDialog(String message, OnDismissListener listener, OnCancelListener mOnCancelistener) {
if (waitingDialog != null) {
waitingDialog.dismiss();
waitingDialog = null;
}
if (mContext == null) {
return;
}
if (((Activity) mContext).isFinishing()) {
return;
}
waitingDialog = createLoadingDialog(mContext, message);
waitingDialog.setCanceledOnTouchOutside(false);
waitingDialog.setOnCancelListener(mOnCancelistener);
waitingDialog.setCancelable(mCancel);
waitingDialog.setOnDismissListener(listener);
waitingDialog.show();
}
而我的代碼通過(guò) View.getContext()
傳入的 Context
類型是 ContextThemeWrapper
。
// 領(lǐng)取優(yōu)惠券
val dialog = CommonDialog(binding.root.context)
dialog.openProgressDialog("領(lǐng)取中...") // 第 57 行出問(wèn)題的代碼
ProductService.INSTANCE.receiveGoodsCoupon(data.class_id)
.compose(RetrofitUtil.schedulersAndGetData())
.subscribeNet(true) {
// 邏輯處理相關(guān)代碼
}
看到了日志改起來(lái)就非常簡(jiǎn)單了,第一種方案是直接在 CommonDialog
強(qiáng)轉(zhuǎn)前做一下類型判斷。第二種方案是直接在我這里的代碼中通過(guò)判斷 binding.root.context
的類型,然后取出里面的 Activity
。
雖然 bug 非常好解決,但作為一名 Android 程序員,絕對(duì)不可以滿足于僅僅解決 bug 上,任何事情都事出有因,這里為什么數(shù)月沒有更改的代碼,在 9.4.0 上沒有問(wèn)題,在 9.5.0 上就成了必現(xiàn)崩潰呢?
切換代碼分支到 9.4.0,debug 發(fā)現(xiàn),這里的 binding.root.context
返回的確實(shí)就是 Activity
,而在 9.5.0 上 binding.root.context
確實(shí)就返回的是 ContextThemeWrapper
,檢查后確定代碼沒有任何改動(dòng)。
分析出現(xiàn) ContextThemeWrapper 的原因
看到 ContextThemeWrapper
,不由得想起了這個(gè)類使用的地方之一:Dialog
,熟悉 Dialog
的童鞋一定都知道,我們?cè)跇?gòu)造 Dialog
的時(shí)候,會(huì)把 Context
直接變成 ContextThemeWrapper
。
public Dialog(@NonNull Context context) {
this(context, 0, true);
}
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 == ResourceId.ID_NULL) {
final TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
themeResId = outValue.resourceId;
}
mContext = new ContextThemeWrapper(context, themeResId);
} else {
mContext = context;
}
// 省略部分代碼...
}
oh,在第三個(gè)構(gòu)造方法中,通過(guò)構(gòu)造的時(shí)候傳入的 createContextThemeWrapper
總是 true
,所以它一定可以進(jìn)到這個(gè) if
語(yǔ)句里面去,把 mContext
強(qiáng)行指向了 Context
的包裝類 ContextThemeWrapper
。所以這里會(huì)不會(huì)是由于這個(gè)原因呢?
我們?cè)倏纯次覀兊拇a,我這個(gè) ProductReceiveCouponItem
實(shí)際上是一個(gè) RecyclerView
的 Item,而這個(gè)相應(yīng)的 RecyclerView
是顯示在 DialogFragment
上的。熟悉 DialogFragment
的小伙伴可能知道,DialogFragment
實(shí)際上也是一個(gè) Fragment
。而 DialogFragment
里面,其實(shí)是有一個(gè) Dialog
的變量 mDialog
的,這個(gè) Dialog
會(huì)在 onStart()
后通過(guò) show()
展示出來(lái)。
在我們使用 DialogFragment
的時(shí)候,一定都會(huì)重寫 onCreatView()
對(duì)吧,有一個(gè) LayoutInflater
參數(shù),返回值是一個(gè) View
,我們不禁想知道這個(gè) LayoutInflater
是從哪兒來(lái)的? onGetLayoutInflater()
,我們看看。
@Override
public LayoutInflater onGetLayoutInflater(Bundle savedInstanceState) {
if (!mShowsDialog) {
return super.onGetLayoutInflater(savedInstanceState);
}
mDialog = onCreateDialog(savedInstanceState);
if (mDialog != null) {
setupDialog(mDialog, mStyle);
return (LayoutInflater) mDialog.getContext().getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
}
return (LayoutInflater) mHost.getContext().getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
}
我們是以一個(gè) Dialog
的形式展示,所以不會(huì)進(jìn)入其中的 if
條件。所以我們直接通過(guò)了 onCreateDialog()
構(gòu)造了一個(gè) Dialog
。如果這個(gè) Dialog
不為空的話,那么我們的 LayoutInflater
就會(huì)直接通過(guò) Dialog
的 Context
構(gòu)造出來(lái)。我們來(lái)看看 onCreateDialog()
方法。
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new Dialog(getActivity(), getTheme());
}
很簡(jiǎn)單,直接 new
了一個(gè) Dialog
,Dialog
這樣的構(gòu)造方法上面也說(shuō)了,直接會(huì)把 mContext
指向一個(gè) Context
的包裝類 ContextThemeWrapper
。
至此我們能做大概猜想了,DialogFragment
負(fù)責(zé) inflate
出布局的 LayoutInflater
是由 ContextThemeWrapper
構(gòu)造出來(lái)的,所以我們暫且在這里說(shuō)一個(gè)結(jié)論:DialogFragment onCreatView() 里面這個(gè) layout 文件里面的 View.getContext() 返回應(yīng)該是 `ContextThemeWrapper。
但是?。。∥覀兂鰡?wèn)題的是 Item,Item 是通過(guò) RecyclerView
的 Adapter
的 ViewHolder
顯示出來(lái)的,而非 DialogFragent
里面 Dialog
的 setContentView()
的 XML 解析方法??雌饋?lái),分析了那么多,并沒有找到問(wèn)題的癥結(jié)所在。所以得看看我們的 Adapter
是怎么寫的,直接打開我們的 MultiTypeAdapter
的 onCreateViewHolder()
方法。
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (typeMap.get(viewType, TYPE_DEFAULT) == TYPE_ONE) {
return holders.get(viewType).createHolder(parent);
}
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), viewType, parent, false);
return new ItemViewHolder(binding);
}
oh,在這里我們的 LayoutInflater.from()
接受的參數(shù)是 parent.getContext()
。parent
是什么?就是我們的 RecyclerView
,這個(gè) RecyclerView
是從哪兒來(lái)的?通過(guò) DialogFragment
的 LayoutInflater
給 inflate
出來(lái)的。所以 parent.getContext()
返回是什么?在這里,一定是 ContextThemeWrapper
。
也就是說(shuō),我們的 ViewHolder
的 rootView
也就是通過(guò) ContextThemeWrapper
構(gòu)造的 LayoutInflater
給 inflate
出來(lái)的了。所以我們的 ProductReceiveCouponItem
這個(gè) Item 里面的 binding.root.context
返回值,自然也就是 ContextThemeWrapper
而不是 Activity
了。自然而然,在 CommonDialog
里面直接強(qiáng)轉(zhuǎn)為 Activity
一定會(huì)出錯(cuò)。
那為什么在 9.4.0 上沒有出現(xiàn)這個(gè)問(wèn)題呢?我們看看 9.4.0 上 MultiTypeAdapter
的 onCreateViewHolder()
方法:
@Override
public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewDataBinding binding = DataBindingUtil.inflate(mInflater, viewType, parent, false);
return new ItemViewHolder(binding);
}
咦,看起來(lái)似乎不一樣,這里直接傳入的是 mInflater
,我們看看這個(gè) mInflater
是在哪兒被初始化的。
public MultiTypeAdapter(Context context) {
mInflater = LayoutInflater.from(context);
}
oh,在 9.4.0 的分支上,我們的 ViewHolder
的 LayoutInflater
的 Context
,是從外面?zhèn)鬟M(jìn)來(lái)的。再看看我們 DialogFragment
中對(duì) RecyclerView
的處理。
val rvAdapter = MultiTypeAdapter(context)
binding.recyclerView.run {
layoutManager = LinearLayoutManager(context)
val itemDecoration = DividerItemDecoration(context, DividerItemDecoration.VERTICAL_LIST)
itemDecoration.setDividerDrawable(R.drawable.list_divider_10_white.toDrawable())
addItemDecoration(itemDecoration)
adapter = rvAdapter
}
是吧,在 9.4.0 的時(shí)候,MultiTypeAdapter
的 ViewHolder
會(huì)使用外界傳入的 Context
,這個(gè) Context
是 Activity
,所以我們的Item 的 binding.root.context
返回為 Activity
。而在 9.5.0 的時(shí)候,同事重構(gòu)了 MultiTypeAdapter
,而讓其 ViewHolder
的 LayoutInflater
直接取的 parent.getContext()
,這里的情況即 ContextThemeWrapper
,所以出現(xiàn)了幾個(gè)月沒動(dòng)的代碼,在新版本上灰測(cè)卻崩潰了。
總結(jié)
寫了這么多,還是做一些總結(jié)。首先對(duì)題目做個(gè)答案: View.getContext() 的返回不一定是 Activity。
實(shí)際上,View.getContext()
和 inflate
這個(gè) View
的 LayoutInflater
息息相關(guān),比如 Activity
的 setContentView()
里面的 LayoutInflater
就是它本身,所以該 layoutRes
里面的 View.getContext()
返回的就是 Activity
。但在使用 AppCompatActivity
的時(shí)候,值得關(guān)注的是, layoutRes
里面的原生 View
會(huì)被自動(dòng)轉(zhuǎn)換為 AppCompatXXX
,而這個(gè)轉(zhuǎn)換在 5.0 以下的手機(jī)系統(tǒng)中,會(huì)把 Context
轉(zhuǎn)換為其包裝類 TintThemeWrapper
,所以在這樣的情況下的 View.getContext()
返回是 TintThemeWrapper
。
最后,從一個(gè)奇怪的 bug 中,給大家分享了一個(gè)簡(jiǎn)單的原因探索分析,也進(jìn)一步驗(yàn)證了上面的結(jié)論。任何 bug 的出現(xiàn),總是有它的原因,作為 Android 開發(fā),我們不僅要處理掉 bug,更要關(guān)注到它的更深層次的原因,這樣才能在代碼層面就發(fā)現(xiàn)其它的潛在問(wèn)題,以免帶來(lái)更多不必要的麻煩。本文就一個(gè)簡(jiǎn)單的示例進(jìn)行了此次試探的講解,但個(gè)人技術(shù)能力有限,唯恐出現(xiàn)紕漏,還望有心人士指出。
文章部分來(lái)源于:View.getContext() 里的小秘密