Android | View & Fragment & Window 的 getContext() 一定返回 Activity 嗎?

前言

  • 最近,在玩安卓上看到 每日一問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()的返回值,在這篇文章里,我們將延伸一下,以下幾種情況我都會歸納,以便幫助你建立更為清晰全面的認識:

    1. View#getContext()
    1. Fragment#getContext()
    1. Window#getContext()
    1. 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 布局解析原理》,如果你對 LayoutInflater 布局解析的流程還不熟悉,可以先復習下,相同的地方不再重復提。在這里,我們只關注使用反射實例化 View 的地方:

createViewFromTag(...) 示意圖

可以看到,實例化 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;
        }
    }
}

6. 總結

  • 應試建議
    • 遇到此問題,答案應為:可能是Application、Service、ContextImpl、ContextWrapper、Activity的任何一個;
    • 應該對Context類型、LayoutInflater 布局解析、View 體系等源碼有一定熟悉度,不僅僅能夠解答本文問題,更多有意思/深度的問題也能迎刃而解。

推薦閱讀

感謝喜歡!你的點贊是對我最大的鼓勵!歡迎關注彭旭銳的GitHub!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內容