Android各種Context的前世今生

前言

Android開發過程中,Context是繞不開的東西,因此本篇文章將一探究竟。
通過這篇文章,你將了解到:

1、Context衍生的子類
2、Context作用
3、四大組件里的Context
4、Context與Resources
5、不同Context關聯與使用

Context家族

Context是抽象類,來看看常見的子類


image.png

上圖展示了常見的Context子類的繼承關系。
我們平時接觸比較多的是Activity、Application、Service。

Context作用

image.png

根據上圖,我們主要講解Resource和四大組件相關的。

四大組件里的Context

Context 子類

Context里沒有特別需要留意的成員變量,其成員方法也多靠子類實現。

ContextWrapper

成員變量
遍觀ContextWrapper,只有個成員變量:

Context mBase;

成員方法
ContextWrapper重寫Context的方法,內部依靠mBase調用。

image.png

mBase實際上就是ContextImpl類型的,來看看它的內容。

ContextImpl

成員變量

        //ContentResolver
        private final ApplicationContentResolver mContentResolver;
        //通過ResourcesManager管理Resource
        private final @NonNull ResourcesManager mResourcesManager;
        //Resource引用
        private @NonNull Resources mResources;
        //主題資源
        private int mThemeResource = 0;
        //主題
        private Resources.Theme mTheme = null;
        //緩存context.getSystemService 獲取的實例
        final Object[] mServiceCache = SystemServiceRegistry.createServiceCache();
        //省略...

成員方法
ContextImpl成員方法是Context方法具體實現的地方。

image.png

Context/ContextWrapper/ContextImpl 三者關系

image.png

如上圖,ContextWrapper、ContextImpl繼承自Context,ContextWrapper作為ContextImpl 代理。

ContextWrapper和ContextImpl關聯

ContextWrapper和ContextImpl如何關聯以及在什么時機進行關聯的呢?
Application
Application是ContextWrapper子類,先來看看它是如何關聯的。以下部分涉及到Application/Activity啟動流程,有興趣的請移步:Android Activity創建到View的顯示過程

#LoadedApk.java 
#makeApplication(...)
        //創建ContextImpl 實例
        ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
        //創建Application實例,并將mBase指向appContext
        //Application.attach(...)->ContextWrapper.attachBaseContext(...)->mBase=appContext
        app = mActivityThread.mInstrumentation.newApplication(
                cl, appClass, appContext);
        //ContextImpl mOuterContext指向app
        appContext.setOuterContext(app);

在創建Application的時候,會先構造ContextImpl對象,然后構造Application實例,并將Application里的mBase指向ContextImpl對象,最后將ContextImpl mOuterContext指向app。完成了Application和ContextImpl關聯,也即是ContextWrapper和ContextImpl的關聯。
Service
ContextWrapper還有另一個常見的子類:Service。來看看Service如何關聯ContextImpl的。

#ActivityThread.java
    private void handleCreateService(CreateServiceData data) {

        //獲取LoadedApk
        LoadedApk packageInfo = getPackageInfoNoCheck(
                data.info.applicationInfo, data.compatInfo);
        Service service = null;
        try {
            java.lang.ClassLoader cl = packageInfo.getClassLoader();
            //創建service
            service = packageInfo.getAppFactory()
                    .instantiateService(cl, data.info.name, data.intent);
        } catch (Exception e) {
        }

        try {
            //創建ContextImpl
            ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
            //contextImpl 持有該Service
            context.setOuterContext(service);
            Application app = packageInfo.makeApplication(false, mInstrumentation);
            //初始化Service一些成員變量,關聯ContextImpl
            service.attach(context, this, data.info.name, data.token, app,
                    ActivityManager.getService());
            //service onCreate方法,一般會重寫該方法監聽service的創建
            service.onCreate();
        } catch (Exception e) {
        }
    }

接著看看service.attach(...)

    public final void attach(
            Context context,
            ActivityThread thread, String className, IBinder token,
            Application application, Object activityManager) {
        //關聯ContextImpl
        attachBaseContext(context);
        mThread = thread;           // NOTE:  unused - remove?
        mClassName = className;
        mToken = token;
        mApplication = application;
        mActivityManager = (IActivityManager)activityManager;
        mStartCompatibility = getApplicationInfo().targetSdkVersion
                < Build.VERSION_CODES.ECLAIR;
    }

以上,ContextImpl和Service關聯起來了。
Activity
ContextWrapper還有一個子類ContextThemeWrapper。
ContextThemeWrapper顧名思義,和主題相關的。

    {
        //主題資源id
        private int mThemeResource;
        //theme
        private Resources.Theme mTheme;
        private LayoutInflater mInflater;
        private Configuration mOverrideConfiguration;
        private Resources mResources;
    }

而Activity繼承自ContextThemeWrapper,來看看Activity和ContextImpl如何關聯上的。

    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        //省略...
        //創建ContextImp
        ContextImpl appContext = createBaseContextForActivity(r);
        //創建Activity
        Activity activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);
        //關聯ContextImpl和activity
        appContext.setOuterContext(activity);
        //初始化Activity成員變量
        activity.attach(appContext, this, getInstrumentation(), r.token,
                r.ident, app, r.intent, r.activityInfo, title, r.parent,
                r.embeddedID, r.lastNonConfigurationInstances, config,
                r.referrer, r.voiceInteractor, window, r.configCallback,
                r.assistToken);
        //省略
    }

類似的,繼續看activity.attach(...)

image.png

最終也是調用到了ContextWrapper attachBaseContext(...),關聯mBase。
BroadcastReceiver
BroadcastReceiver 并沒有繼承自Context,但可以在onReceive(...)里拿到Context,那么這個Context是怎么來的呢?
image.png

Context作為參數傳進來的,那么就看看onReceive(...)的調用棧。

#ActivityThread.java
    private void handleReceiver(ReceiverData data) {
        //省略...
        Application app;
        BroadcastReceiver receiver;
        ContextImpl context;
        try {
            app = packageInfo.makeApplication(false, mInstrumentation);
            //用的是Application的mBase,也就是ContextImpl
            context = (ContextImpl) app.getBaseContext();
            //構造receiver
            receiver = packageInfo.getAppFactory()
                    .instantiateReceiver(cl, data.info.name, data.intent);
        } catch (Exception e) {}

        try {
            //調用onReceive
            receiver.onReceive(context.getReceiverRestrictedContext(),
                    data.intent);
        } catch (Exception e) {
        } finally {
        }
    }

我們注意到context.getReceiverRestrictedContext():

#ContextImpl.java
    final Context getReceiverRestrictedContext() {
        //ReceiverRestrictedContext 是ContextWrapper的子類
        if (mReceiverRestrictedContext != null) {
            return mReceiverRestrictedContext;
        }
        
        //該Context是關聯Application的,也即是ContextImpl,因此getOuterContext()返回的是
        //Application實例。最后ReceiverRestrictedContext的mBase指向Application實例
        return mReceiverRestrictedContext = new ReceiverRestrictedContext(getOuterContext());
    }

因此我們得出結論是:

onReceive里的Context是ReceiverRestrictedContext類型,繼承自ContextWrapper,其mBase指向Application。
可以看出,mBase不一定是ContextImpl類型,但是最終都會調用到ContextImpl。

ContentProvider
ContentProvider沒有繼承自Context,但是其成員變量mContext是Context類型的,那么這個變量是怎么賦值的呢?
在構造ContextImpl時,會初始化ContentResolver

mContentResolver = new ApplicationContentResolver(this, mainThread);

這個this即是ContextImpl自身,傳進去賦值給了ContentResolver變量:

private final Context mContext;

當使用ContentResolver查詢ContentProvider并且創建ContentProvider的時候,這個mContext就賦值給ContentProvider的mContext。
上面分析了Application和四大組件與Context關系,用圖表示:


image.png

Context與Resources

之前列舉了Context的用處,最常用的莫過于通過Context獲取資源文件(Resources),具體情況是怎么樣的,接下來分析。
Context并沒有Resources類型的成員變量,ContextWrapper也沒有,ContextImpl有成員變量:

private @NonNull Resources mResources;

而Context里有獲取Resources的成員方法:

public abstract Resources getResources();

最終會調用ContextImpl,返回mResources。因此重點是ContextImpl的mResources如何賦值的。
上面提到過,Application/Activity/Service等關聯ContextImpl時,會新構造一個ContextImpl實例,在初始化的時候,會給mResources賦值。而Resources是通過ResourcesManager管理的,最終來看ResourcesManager如何管理Resources的。

ResourcesManager

Resources 和 ResourcesImpl
Resources里有個成員變量:

private ResourcesImpl mResourcesImpl;

顧名思義,Resources具體加載資源是通過ResourcesImpl實現的。
ResourcesImpl里成員變量:

final AssetManager mAssets;

AssetManager負責與底層通信(操作文件)。
ResourcesManager獲取Resources核心代碼:

    Resources getOrCreateResources(@android.annotation.Nullable IBinder activityToken,
                                   @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
        //ResourcesKey 記錄著資源文件的路徑、Configuration等信息,最后用來生成AssetManager
        synchronized (this) {
            //activityToken 不為空,說明是Activity的Resource
            if (activityToken != null) {
                //從ResourcesImpl 的Map里尋找相應的緩存
                ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
                if (resourcesImpl != null) {
                    return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                            resourcesImpl, key.mCompatInfo);
                }

            } else {
                //非Activity的Resource
                ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
                if (resourcesImpl != null) {
                    return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
                }
            }
            //沒找到現成的,生成ResourcesImpl 實例
            //同時創建AssetManager
            ResourcesImpl resourcesImpl = createResourcesImpl(key);
            if (resourcesImpl == null) {
                return null;
            }
            //記錄到Map里
            mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
            final Resources resources;
            if (activityToken != null) {
                //Activity Resource
                resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                        resourcesImpl, key.mCompatInfo);
            } else {
                //非Activity Resource
                resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
            }
            return resources;
        }
    }

總結來說:

1、通過ResourcesKey去檢索之前是否創建過ResourcesImpl,如果沒有則創建新的對象,并記錄到Map里。
2、通過ArrayList遍歷尋找可以復用的Resources對象,判斷的依據是傳入的ResourcesImpl與已有的相等(其中的條件)。如果沒有可以復用的,則創建Resources對象,設置ResourcesImpl,并將Resources對象放入List等待下次復用。
3、可以看出ResourcesManager通過設置緩存來管理Resource。而ResourcesManager是單例,Context通過getResources(...)獲取Resource本質是通過ResourcesManager獲取的。

接下來看看Application/Service/Activity獲取的Resource是同一個Resource嗎?
ActivityThread為Application/Service創建ContextImpl時使用如下方法:

    #ContextImpl.java
    static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo,
                                        String opPackageName) {
        ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0,
                null, opPackageName);
        //從packageInfo 獲取resources
        context.setResources(packageInfo.getResources());
        return context;
    }

packageInfo.getResources():

    #LoadedApk
    public Resources getResources() {
        //LoadedApk是全局的,因此它的mResources也只有一個
        if (mResources == null) {
            mResources = ResourcesManager.getInstance().getResources(null, mResDir,
                    splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
                    Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
                    getClassLoader());
        }
        return mResources;
    }

可以看出,通過createAppContext(xx)創建的ContextImpl共用同一個Resources對象。

而ActivityThread為Activity創建ContextImpl時使用的是:

    #ContextImpl.java
    static ContextImpl createActivityContext(ActivityThread mainThread,
                                             LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId,
                                             Configuration overrideConfiguration) {
        ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityInfo.splitName,
                activityToken, null, 0, classLoader, null);

        final ResourcesManager resourcesManager = ResourcesManager.getInstance();
        //通過resourcesManager 獲取
        context.setResources(resourcesManager.createBaseActivityResources(activityToken,
                packageInfo.getResDir(),
                splitDirs,
                packageInfo.getOverlayDirs(),
                packageInfo.getApplicationInfo().sharedLibraryFiles,
                displayId,
                overrideConfiguration,
                compatInfo,
                classLoader));
        return context;
    }

可以看出,直接使用了ResourcesManager獲取Resource,最終不同的Activity獲取的Resource對象不同,但是共用同一個ResourcesImpl對象。

不同Context關聯與使用

這么多的Context,什么時候該使用哪種Context呢?以下列舉幾個易混的地方。

啟動Activity

    public void startActivity(Intent intent, Bundle options) {
        final int targetSdkVersion = getApplicationInfo().targetSdkVersion;
        //targetSdkVersion 小于7.0 或者大于9.0時
        //通過ContextImpl啟動Activity,如果沒有加入FLAG_ACTIVITY_NEW_TASK,則拋出異常
        if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == 0
                && (targetSdkVersion < Build.VERSION_CODES.N
                || targetSdkVersion >= Build.VERSION_CODES.P)
                && (options == null
                || ActivityOptions.fromBundle(options).getLaunchTaskId() == -1)) {
            throw new AndroidRuntimeException(
                    "Calling startActivity() from outside of an Activity "
                            + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
                            + " Is this really what you want?");
        }
        mMainThread.getInstrumentation().execStartActivity(
                getOuterContext(), mMainThread.getApplicationThread(), null,
                (Activity) null, intent, -1, options);
    }

TargetSdkVersion相關請查看:targetSdkVersion、compileSdkVersion、minSdkVersion作用與區別

非得要用非Activity的Context啟動Activity,只需要加入FLAG_ACTIVITY_NEW_TASK標記即可。建議持有當前棧頂Activity對象引用,通過Activity啟動另一個Activity。

啟動Dialog

非Activity Context啟動Dialog會失敗。原因是:

Activity帶有IBinder appToken,Window關聯WindowManager時會將appToken賦予Window的IBinder mAppToken,在添加Window到WindowManagerService過程中,會將mAppToken賦值給WindowManager.LayoutParams IBinder token。WindowManagerService檢查添加窗口的合法性,發現如果要添加的窗口類型是Dialog,但是有沒有Token,則拋出異常。
Activity啟動Dialog時,Dialog獲取的WindowManager即是Activity的WindowManager,其parentWindow是Activity的Window,而Activity的Window是有token的,最后該token賦值給LayoutParams。

Activity Window的關系有興趣請查看:Android Activity創建到View的顯示過程
因此啟動Dialog需要Activity作為Context傳進去。

View的Context

View的Context mContext變量是在創建View對象時賦值的。我們知道創建View對象有兩種方式:

1、動態創建new View(Context),此時決定于傳入的Context。
2、xml布局,最終是通過LayoutInflater加載的。LayoutInflater from(Context context),該context最終傳給View。

View的Context并沒有明確限制需要使用什么類型。但是如果沒有使用Activity作為Context的話,就無法使用Activity Theme的特性。
主題相關請移步:
全網最深入 Android Style/Theme/Attr/Styleable/TypedArray 清清楚楚明明白白

自從看了這篇文章,媽媽再也不擔心我不會Android 事件分發了
Android 輸入事件一擼到底之View接盤俠(3)

如果您喜歡,請點贊,您的鼓勵是我前進的動力。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容