前言
- 最近,在玩安卓上看到 每日一問(wèn):View#getContext() 一定會(huì)返回 Activity 對(duì)象么? 直覺(jué)是:View 是由 Activity 管理的,那么 View#getContext() 一定是 Activity 了,事實(shí)真的如此嗎?
- 其實(shí)這個(gè)問(wèn)題主要還是考察應(yīng)試者對(duì)于源碼(包括:Context類型、LayoutInflater 布局解析、View 體系等)的熟悉度,在這篇文章里,我將跟你一起探討。如果能幫上忙,請(qǐng)務(wù)必點(diǎn)贊加關(guān)注,這真的對(duì)我非常重要。
相關(guān)文章
- 《Android | 一個(gè)進(jìn)程有多少個(gè) Context 對(duì)象(答對(duì)的不多)》
- 《Android | 帶你探究 LayoutInflater 布局解析原理》
- 《Android | View & Fragment & Window 的 getContext() 一定返回 Activity 嗎?》
- 《Android | 說(shuō)說(shuō)從 android:text 到 TextView 的過(guò)程》
目錄
1. 問(wèn)題分析
1.1 Context 有哪些?
首先,我們回顧一下 Context 以及它的子類,在之前的這篇文章里,我們?cè)?jīng)討論過(guò):《Android | 一個(gè)進(jìn)程有多少個(gè) Context 對(duì)象(答對(duì)的不多)》。簡(jiǎn)單來(lái)說(shuō):Context 使用了裝飾模式,除了 ContextImpl 外,其他 Context 都是 ContextWrapper 的子類。
我們熟悉的 Activity & Service & Application,都是 ContextWrapper 的子類。調(diào)用getBaseContext()
,可以獲得被代理的基礎(chǔ)對(duì)象:
ContextWrapper.java
Context mBase;
public ContextWrapper(Context base) {
mBase = base;
}
public Context getBaseContext() {
return mBase;
}
需要注意的是,Activity 也是可以作為被代理的對(duì)象的,類似這樣:
Activity activity = ...;
Context wrapper = new ContextThemeWrapper(activity, themeResId);
wrapper.startActivity(...); // OK
wrapper instanceOf Activity // false
這個(gè)時(shí)候,代理對(duì)象wrapper
可以使用 Activity 的能力,可以用它 startActivity()
,也可以初始化 View,然而它卻不是 Activity。看到這里,我們似乎找到了問(wèn)題的一點(diǎn)苗頭了:getContext() 可能返回 Activity 的包裝類,而不是 Activity。
1.2 問(wèn)題延伸
網(wǎng)上討論得比較多的,主要還是View#getContext()
的返回值,在這篇文章里,我們將延伸一下,以下幾種情況我都會(huì)歸納,以便幫助你建立更為清晰全面的認(rèn)識(shí):
- View#getContext()
- Fragment#getContext()
- Window#getContext()
- Dialog#getContext()
2. View#getContext() 的返回值
我們來(lái)看View#getContext()
的源碼,可以看到,View#getContext()
返回值是在構(gòu)造函數(shù)中設(shè)置的,源碼里未發(fā)現(xiàn)其它賦值語(yǔ)句。所以,這個(gè)問(wèn)題的關(guān)鍵是看:實(shí)例化 View 時(shí)傳入構(gòu)造器的 Context 對(duì)象。
View.java
@hide
protected Context mContext;
public final Context getContext() {
return mContext;
}
public View(Context context) {
mContext = context;
...
}
...
在使用 View 的過(guò)程用,有兩種方式可以實(shí)例化 View :
- 方法1:代碼調(diào)用,類似這樣:
new TextView(Context)
很明顯,只要你傳入什么對(duì)象,將來(lái)你調(diào)用 getContext(),得到的就是同一個(gè)對(duì)象。回顧 第 1 節(jié) 的討論,你可以傳入 Activity,也可以傳入包裝類。誒,那可以傳入 Service、Application、ContextImpl 嗎?還真的可以,只是你要保證 getContext() 后的行為正確,一般不會(huì)這么做。
new TextView(Activity)
new TextView(ContextWrapper)
new TextView(Service) 一般不會(huì)這么做
new TextView(Application) 一般不會(huì)這么做
new TextView(ContextImpl) 一般不會(huì)這么做
- 方法2:布局文件,類似這樣:
<TextView ...>
這種方式其實(shí)是利用了 LayoutInflater 布局解析的能力,在之前的這篇文章里,我們?cè)?jīng)討論過(guò):《Android | 帶你探究 LayoutInflater 布局解析原理》,如果你對(duì) LayoutInflater 布局解析的流程還不熟悉,可以先復(fù)習(xí)下,相同的地方不再重復(fù)提。在這里,我們只關(guān)注使用反射實(shí)例化 View 的地方:
可以看到,實(shí)例化 View 的地方使用了反射,而Constructor#newInstance(...)
的首個(gè)參數(shù)即為將來(lái) getContext() 返回的對(duì)象。那么,mConstructorArgs[0]
到底是什么對(duì)象呢,是 Activity 嗎?我們逆著源碼找找看:
LayoutInflater.java
public final View createView(@NonNull Context viewContext, @NonNull String name,
@Nullable String prefix, @Nullable AttributeSet attrs){
...
疑問(wèn):viewContext 到底是什么呢?
mConstructorArgs[0] = viewContext;
final View view = constructor.newInstance(mConstructorArgs);
...
}
createViewFromTag() -> createView()(已簡(jiǎn)化)
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) {
1. 應(yīng)用 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 實(shí)例化 View,相當(dāng)于攔截
3. 使用 mPrivateFactory 實(shí)例化 View,相當(dāng)于攔截
4. 調(diào)用自身邏輯
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 實(shí)例化 View,相當(dāng)于攔截
final View createView(...) {
final Context originalContext = context;
2.1 應(yīng)用 ContextThemeWrapper 以支持 android:theme / app:theme
if (readAndroidTheme || readAppTheme) {
context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
}
if (wrapContext) {
2.2 應(yīng)用 ContextThemeWrapper 以支持矢量圖 tint
context = TintContextWrapper.wrap(context);
}
View view = null;
switch (name) {
case "TextView":
2.3 實(shí)例化 AppCompatTextView
view = createTextView(context, attrs);
break;
...
default:
view = createView(context, name, attrs);
}
return view;
}
-> 2.1 應(yīng)用 ContextThemeWrapper 以支持 android:theme(已簡(jiǎn)化)
private static Context themifyContext(Context context, AttributeSet attrs, boolean useAndroidTheme, boolean useAppTheme) {
// 事實(shí)上,分支 1.1 已經(jīng)處理了,這里是兼容 Android 5.0 以前。
return new ContextThemeWrapper(context, themeId);
}
-> 2.2 應(yīng)用 ContextThemeWrapper 以支持矢量圖 android:tint(已簡(jiǎn)化)
public static Context wrap(@NonNull final Context context) {
return new TintContextWrapper(context);
}
AppCompatTextView.java
-> 2.3 實(shí)例化 AppCompatTextView
public AppCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(TintContextWrapper.wrap(context), attrs, defStyleAttr);
}
以上代碼已經(jīng)十分簡(jiǎn)化了,當(dāng)然你也可以選擇直接看結(jié)論:
小結(jié):
- 分支 1.1:應(yīng)用 ContextThemeWrapper 以支持
android:theme
,此時(shí) View#getContext() 返回這個(gè)包裝類; - 分支 2.1:應(yīng)用 ContextThemeWrapper 以支持
android:theme
(事實(shí)上,分支 1.1 已經(jīng)處理了,這里是兼容 Android 5.0 前),同樣也是返回包裝類; - 分支 2.2:應(yīng)用 ContextThemeWrapper 以支持矢量圖
android:tint
,這是為了兼容 Android 5.0 以前不支持 tint,同樣也是返回包裝類; - 分支 2.3:實(shí)例化 AppCompatTextView,同樣也是返回包裝類;
- 分支 4:返回的是 LayoutInflater#mContext,這個(gè)是
LayoutInflater.from(Context)
傳入的參數(shù)。在 《Android | 帶你探究 LayoutInflater 布局解析原理》里,我們討論過(guò):在 Activity / Fragment / View / Dialog 中,獲取LayoutInflater#getContext()
,返回的就是 Activity。
第 2 節(jié)討論完后,下面這幾節(jié)就容易多了。
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);
...
}
小結(jié):
- 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*/);
}
...
}
小結(jié):
- Fragment#getContext() 返回 Activity;
5. 從 View#getContext() 獲得 Activity 對(duì)象
在很多場(chǎng)景中,經(jīng)常需要通過(guò) View 來(lái)獲得 Activity 對(duì)象,經(jīng)過(guò)前面幾節(jié)內(nèi)容的討論,我們已經(jīng)知道View#getContext()
的返回值總共有以下五種情況:
Activity
ContextWrapper
Service 一般不會(huì)
Application 一般不會(huì)
ContextImpl 一般不會(huì)
那么,要獲得 Activity 則只要不斷得獲取 Context 的被代理對(duì)象(基礎(chǔ)對(duì)象),就可以獲得 Activity;當(dāng)然了,下面 Service & Application & ContextImpl幾種情況是返回空的,所以我們用@Nullable
修飾。
遞歸寫(xiě)法:
@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;
}
}
迭代寫(xiě)法:
@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;
}
}
}
6. 總結(jié)
- 應(yīng)試建議
- 遇到此問(wèn)題,答案應(yīng)為:可能是Application、Service、ContextImpl、ContextWrapper、Activity的任何一個(gè);
- 應(yīng)該對(duì)Context類型、LayoutInflater 布局解析、View 體系等源碼有一定熟悉度,不僅僅能夠解答本文問(wèn)題,更多有意思/深度的問(wèn)題也能迎刃而解。
推薦閱讀
- 密碼學(xué) | Base64是加密算法嗎?
- 算法面試題 | 回溯算法解題框架
- 算法面試題 | 鏈表問(wèn)題總結(jié)
- Java | 帶你理解 ServiceLoader 的原理與設(shè)計(jì)思想
- Android | 面試必問(wèn)的 Handler,你確定不看看?
- Android | 帶你理解 NativeAllocationRegistry 的原理與設(shè)計(jì)思想
- 計(jì)算機(jī)組成原理 | Unicode 和 UTF-8是什么關(guān)系?
- 計(jì)算機(jī)組成原理 | 為什么浮點(diǎn)數(shù)運(yùn)算不精確?(阿里筆試)
- 計(jì)算機(jī)網(wǎng)絡(luò) | 圖解 DNS & HTTPDNS 原理