天天看點

Android Context的設計思想和源碼分析

原文位址:https://duanqz.github.io/2017-12-25-Android-Context

做了好些年Android,終于可以聊一聊既熟悉又陌生的Context了,每個剛入門的Android開發人員都會接觸到它;然而要讀懂Context的設計哲學,卻又要經過好多輪的認知更新。很多時候,大家是感覺不到Context的存在的,筆者最開始“被迫”使用Context,是在自定義控件的時候,布局中有一個按鈕,點選一次就發送一次廣播,其代碼片段如下所示:

public class CustomLayout extends LinearLayout implements View.OnClickListener {
    private Context mContext;
    private Button mBtnBroadcast;

    // 1. 強制傳入Context
    public CustomLayout(Context context) {
        super(context);
        LayoutInflater.from(context).inflate(R.layout.custom_layout, this);
        mContext = context;
        mBtnBroadcast = (Button) findViewById(R.id.btn_broadcast);
        mBtnBroadcast.setOnClickListener(this);
    }

    public void onClick(View v) {
        ...
        // 2. 通過Context發送廣播
        mContext.sendBroadcast(intent);
    }
}
           

之是以說“被迫”使用Context,是因為:1. 構造函數就強制要傳入Context,否則會導緻編譯報錯;2. 在點選按鈕發送廣播時,又必須使用Context,于是乎,又“被迫”設計一個對象屬性mContext來儲存外部傳入的Context。各種疑問湧上心頭:

  • 好不容易想實作控件的代碼解耦,為什麼要把Context傳來傳去呢?
  • 為什麼不能像在Activity中一樣,直接調用sendBroadcast()就可以了呢?
  • 通過Context可以調用到很多Android接口,譬如getString(), getColor(), startActivity()等等,它到底是何方神聖呢?

本文會結合Context的設計思想和源碼分析來進行解構。

1. Context的設計思想

1.1 面向應用程式的設計

Android有意淡化程序的概念,在開發一個Android應用程式時,通常都不需要關心目标對象運作在哪個程序,隻需要表明意圖(Intent),譬如撥打電話、檢視圖檔、打開連結等;也不需要關心系統接口是在哪個程序實作的,隻需要通過Context發起調用。對于一個Android應用程式而言,Context就像運作環境一樣,無處不在。有了Context,一個Linux程序就搖身一變,成為了Android程序,變成了Android世界的公民,享有Android提供的各種服務。那麼,一個Android應用程式需要一些什麼服務呢?

  • 擷取應用資源,譬如:drawable、string、assets
  • 操作四大元件,譬如:啟動界面、發送廣播、綁定服務、打開資料庫
  • 操作檔案目錄,譬如:擷取/data/分區的資料目錄、擷取sdcard目錄
  • 檢查授予權限,譬如:應用向外提供服務時,可以判定申請者是否具備通路權限
  • 擷取其他服務,有一些服務有專門的提供者,譬如:包管理服務、Activity管理服務、視窗管理服務

在應用程式中,随處都可通路這些服務,這些服務的通路入口就是Context。Android對Context類的注解是:

Interface to global information about an application environment. This is an abstract class whose implementation is provided by the Android system. It allows access to application-specific resources and classes, as well as up-calls for application-level operations such as launching activities, broadcasting and receiving intents, etc.

其意為:Context是一個抽象類,提供接口,用于通路應用程式運作所需服務,譬如啟動Activity、發送廣播、接受Intent等。Android是建構在Linux之上的,然而對于Android的應用開發者而言,已經不需要關心Linux程序的概念了,正是因為有了Context,為應用程式的運作提供了一個Android環境,開發者隻需要關心Context提供了哪些接口。

1.2 關于Decorator設計模式

在類的世界裡面,要為一個類增加新的功能,最直接的方式就是繼承,子類可以基于父類進行擴充。然而,當要增加的功能次元有很多,并且功能互相疊加的時候,要擴充的子類會變得非常多。

舉個例子,基類是衣服,需求是分别生産防水、透氣和速幹的三個不同功能的衣服,便會擴充出三個子類:防水衣服、透氣衣服和速幹衣服。如果又有新的需求,即防水又速幹,便會擴充出一個新的子類:防水速幹衣服。然後,又有新的需求,即保暖又速幹,這可能會擴充出兩個子類:保暖衣服和保暖速幹衣服。長此以往,市場需求不斷變化,要擴充的類就會越來越多。

在GOF設計模式裡面,把繼承看成靜态的類擴充,擴充功能的增多會導緻子類膨脹。為了有效緩解這種情況,便産生了動态的類擴充:修飾器模式(Decorator Pattern),先上UML圖:

Android Context的設計思想和源碼分析

Decorator就是所謂的修飾器,包裝了(Wrapper)一個Component類型對象,修飾器可以在已有Component的基礎上,增加新的屬性(addedState)和行為(addedBehavior),進而形成不同的ConcreteDecorator。Decorator通過包裝的手段,在外圍擴充了Component的功能。 說到這裡,讀者們一定心生詫異,實作不同擴充功能的ConcreteDecorator,還是得繼承實作多個不同子類啊!确實如此,擴充不同次元的功能需要實作不同的子類,但要實作這些功能的組合,卻不需要新的子類了,因為一個修飾器可以修飾另外一個修飾器,通過修飾器的疊加便可實作功能的組合。

還是上面的例子,基類是衣服,有三個修飾器:防水、透氣和速幹,在這三個修飾器的包裝下,便可生成三種不同的衣服:防水衣服、透氣衣服和速幹衣服。如果又有新需求,即防水又速幹,隻需要在防水衣服上再疊加一個速幹修飾器,便生成了防水速幹衣服。然後,又有新需求,即保暖又速幹,這時,隻需要增加一個修飾器:保暖,将這個修飾器疊加到速幹衣服上,便可生成保暖速幹衣服。這樣一來,便能有效緩解類的膨脹。

了解Decorator模式,有助于大家了解Context類簇的設計,前文說過Context是一個抽象類,圍繞Context還有很多實作類,這些類的結構設計就是Decorator模式。

1.3 Context類簇的設計

先奉上Context類簇的類圖如下:

Android Context的設計思想和源碼分析

一個典型的Decorator模式,基類Context定義了各種接口,ContextImpl負責實作接口的具體功能。對外提供使用時,ContextImpl需要被包裝(Wrapper)一下,這就有了ContextWrapper這個修飾器。修飾器一般隻是一個傳遞者,修飾器所有的方法實作都是調用具體的實作類ContextImpl,是以修飾器ContextWrapper需要持有一個ContextImpl的引用。

修飾器存在的價值是為了擴充類的功能,Context已經提供了豐富的系統功能,但仍不能滿足最終應用程式程式設計的需要,是以Android又擴充了一些修飾器,包括Application、Activity和Service。虎軀一震,這些東西竟然就是Context,原來Context真的是無處不在啊!在Activity中調用startActivity啟動另外的界面,原來就是通過父類Context發起的調用!

Application擴充了應用程式的生命周期,Activity擴充了界面顯示的生命周期,Service擴充了背景服務的生命周期,它們在父類Context的基礎上進行了不同次元的擴充,同時也仍可以将它們作為Context使用,這可以解釋很多Applicaiton、Activity和Service的使用方式,但很多問題也随之而來:

  • 為什麼四大元件的另外兩個BroadcastReceiver和ContentProvider不是Context的子類呢?
  • 為什麼Application、Activity和Service不直接繼承ContextImpl呢,不是更直接嗎?所謂的Decorator模式,也沒看到有多大實際用處啊?

看一下ContentProvider的構造函數和BroadcastReceiver.onReceive()函數:

// ContentProvider
public ContentProvider(
        Context context,
        String readPermission,
        String writePermission,
        PathPermission[] pathPermissions) {
    mContext = context;
    mReadPermission = readPermission;
    mWritePermission = writePermission;
    mPathPermissions = pathPermissions;
}

// BroadcastReceiver
public abstract void onReceive(Context context, Intent intent);
           

ContentProvider和BroadcastReceiver都需要把Context作為參數傳入,雖然它們不繼承于Context,但它們都依賴于Context,換個角度看:它們就是修飾器,包裝了Context。因為這兩個元件在使用上與Activity和Service存在較大的差別,是以它們的實作方式存在較大差異。

往深一步了解,Decorator模式的優勢也展現出來了,譬如Application、Activity和Service都可以作為BroadcastReceiver的載體,隻需要通過它們各自的Context去注冊廣播接收器就可以了,将BroadcastReceiver修飾在它們之上,就形成了新的功能擴充,而不是去擴充一個可以接收廣播的Applicaiton、Activity或Service類。

題外話,Decorator模式在Android中随處可見,除了Context類簇,還有Window類簇。

至此,我們已經領會了Context的設計思想,Context無處不在,它是應用程序與系統對話的一個接口:從使用的角度,更是可以将Context了解為應用程序的Android運作環境,想要什麼資源,都可以向Context索取;從實作的角度,Context類簇利用Decorator設計模式,Android最核心的四大元件都可以了解為“修飾器”,它們從不同的功能次元擴充了Context的功能。

2. Context的源碼分析

Context本身作為一個最高層的抽象類,僅僅是定義接口,方法的實作都在ContextImpl中。因為Context是為應用程式設計的,筆者試圖通過兩條主線來滲透Context的各項知識點:

  • 第一條主線:應用程式Application的Context建構過程
  • 第二條主線:應用界面Activity的Context建構過程

2.1 Application的Context的建構過程

在應用程序與系統程序之間的通信一文中,介紹過應用程序啟動時,需要和系統程序進行通信:

  • 當應用程序在初始化自己的主線程ActivityThread時,便會發起跨程序調用IActivityManager.attachApplication(),告訴系統程序(SystemServer):我已經在Linux的世界誕生了,現在需要增加Android的屬性(應用的包資訊、四大元件資訊、Android程序名等),才能成為一個真正的Android程序。
  • 系統程序在進行包解析時,就擷取了所有應用程式的靜态資訊。在AMS中執行一個應用程序的attachApplication()時,便會将這些資訊的資料結構準備好,發起跨程序調用IApplicationThread.bindApplication(),傳送應用程序啟動所必需的資訊。

經過以上的互動,應用程序就進入ActivityThread.handleBindApplication(),開始建構自己所需的Android環境了:

Android Context的設計思想和源碼分析

從時序圖的第一個函數開始分析:

// ActivityThread.handleBindApplication()
private void handleBindApplication(AppBindData data) {
    ...
    Application app = data.info.makeApplication(data.restrictedBackupMode, null);
    mInitialApplication = app;
    ...
    try {
        mInstrumentation.callApplicationOnCreate(app);
    } catch (Exception e) {...}
}
           

在ActivityThread.handleBindApplication()完成大量變量的初始化後,便開始建立一個Application類型的對象了,有了這個對象後便開始調用其onCreate()方法,就進入到了大家非常熟悉的一個系統回調函數Application.onCreate()。該函數片段的關鍵點是調用LoadedApk.makeApplication()建立Application對象,data.info是之前調用ActivityThread.getPackageInfoNoCheck()生成的LoadedApk對象,表示一個已經加載解析過的APK檔案。

// LoadedApk.makeApplication()
public Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) {
    ...
    if (forceDefaultAppClass || (appClass == null)) {
        appClass = "android.app.Application";
    }
    try {
        java.lang.ClassLoader cl = getClassLoader();
        ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
        app = mActivityThread.mInstrumentation.newApplication(
                cl, appClass, appContext);
        appContext.setOuterContext(app);
    } catch (Exception e) {...}
    ...
}
           

該函數片段中,建立了一個ClassLoader對象和ContextImpl對象,連同将要建構的Application類名appClass,一起作為參數傳送給Instrumentation.newApplication()方法,可想而知,最終的Application對象是反射建構的。

// ContextImpl.createAppContext()
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
    if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
    return new ContextImpl(null, mainThread,
            packageInfo, null, null, 0, null, null, Display.INVALID_DISPLAY);
}

// ContextImpl.constructor()
private ContextImpl(ContextImpl container, ActivityThread mainThread,
        LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
        Display display, Configuration overrideConfiguration, int createDisplayWithId) {
    mOuterContext = this; // 外圍包裝器,暫時用
    ...
    mMainThread = mainThread; // 主線程
    mActivityToken = activityToken; // 關聯到系統程序的ActivityRecord
    mFlags = flags;
    ...
    mPackageInfo = packageInfo; // LoadedApk對象
    mResourcesManager = ResourcesManager.getInstance();
    ...
    Resources resources = packageInfo.getResources(mainThread);
    ...
    mResources = resources; // 通過各種計算得到的資源
    ...
    mContentResolver = new ApplicationContentResolver(this, mainThread, user); // 通路ContentProvider的接口
}
           

ContextImpl有三種不同的類型:

  • SystemContext:系統程序SystemServer的Context
  • AppContext:應用程序的Context
  • ActivityContext:Activity的Context,隻有ActivityContext跟界面顯示相關,需要傳入activityToken和有效的DisplayId

該函數片段是建立一個AppContext,要初始化的屬性其實不多,需要特别注意的是:Context中會初始化一個ContentResovler對象,是以,可以通過Context操作資料庫。Context建立完畢後,會作為參數傳遞給Instrumentation對象去建構一個Application對象:

// Instrumentation.newApplication()
static public Application newApplication(Class<?> clazz, Context context)
        throws InstantiationException, IllegalAccessException, ClassNotFoundException {
    Application app = (Application)clazz.newInstance();
    app.attach(context);
    return app;
}

// Application.attach()
final void attach(Context context) {
    attachBaseContext(context);
    mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;
}

// ContextWrapper.attachBaseContext()
protected void attachBaseContext(Context base) {
    if (mBase != null) {
        throw new IllegalStateException("Base context already set");
    }
    mBase = base;
}
           

Application對象是通過反射建構的,如果應用程式沒有繼承實作Application,則預設使用android.app.Application這個包名進行反射。建構Application對象完成後,便會調用其attach()函數綁定一個Context,這一綁定就相當于在ContextWrapper中關聯了一個ContextImpl,這一層Decorator的修飾包裝關系這就麼套上了。

回顧一下時序圖,ActivityThread中發起Application對象的建立操作,然後建立一個真實的ContextImpl對象(AppContext),最後将AppContext包裝進Application對象中,才完成整個的修飾動作,在這之後,Application便可作為一個真正的Context使用,可以回調其生命周期的onCreate()方法了。

2.2 Activity的Context建構過程

在Activity的啟動過程一文中,介紹過一個Activity是如何從無到有,再到顯示狀态的,這個過程極其複雜。本節将聚焦在Activity的Context建構時機,當要啟動一個Activity時,ActivityThread.performLaunchActivity()會被調用,從這以後便會開始建構Activity對象,時序圖如下:

Android Context的設計思想和源碼分析
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ...
    try {
        java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
        // 1. 反射建構Activity對象
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);
        StrictMode.incrementExpectedActivityCount(activity.getClass());
        r.intent.setExtrasClassLoader(cl);
        r.intent.prepareToEnterProcess();
        if (r.state != null) {
            r.state.setClassLoader(cl);
        }
    } catch (Exception e) {...}

    try {
        // 2. 擷取已有的Application對象
        Application app = r.packageInfo.makeApplication(false, mInstrumentation);
        if (activity != null) {
            // 3. 建立Activity的Context
            Context appContext = createBaseContextForActivity(r, activity);
            CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
            Configuration config = new Configuration(mCompatConfiguration);
            ...
            // 4. 将Context包裝進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);
            ...
            // 5. 調用Activity.onCreate()
            if (r.isPersistable()) {
                    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
                } else {
                    mInstrumentation.callActivityOnCreate(activity, r.state);
            }
        }
    } catch (Exception e) {...}
    ...
}
           

該函數片段完成了從一個Activity對象建構到Activity.onCreate()函數被回調的過程:

  1. 調用Instrumentation.newActivity()函數,傳入這裡通過ClassLoader和Activity的類名,反射建構一個Activity對象。
  2. 擷取已有的Application。LoadedApk.makeApplication()這個函數在前文分析過,當已有建立了一個Application時,會直接傳回。
  3. 調用ActivityThread.createBaseContextForActivity()函數,該函數内部會繼續調用ContextImpl.createActivityContext(),建立一個ActivityContext。此處不再展開分析這兩個函數,ContextImpl對象的初始化過程與上節中一緻,請讀者自行參考。
  4. 以上過程都可以了解為在準備參數,真正将Context包裝進Activity是調用Activity.attach()函數完成的,這個函數我們在分析Activity與Window的關系時,還會重點介紹。此處隻需要了解其中一行,就是調用ContextWrapper.attachBaseContext()函數,将之前建立的ContextImpl對象包裝到ContextWrapper中:
    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) {
        attachBaseContext(context);
        ...
    }
               
  5. Activity的Context建構完成後,便可以回調大家熟悉的Activity.onCreate()函數了。

可見,Activity和Application的Context建構過程極為相似,這也是符合邏輯的,因為它們本質上就是Context,隻不過功能次元不同。其實Service的Context建構過程也很相似。無非都是建構ContextImpl對象,建構Decorator對象(Application、Activity和Service),再将Decorator對象包裝到ContextImpl對象之上。

3. Context的注意事項

3.1 不同類型Context的差別

從前文中可以看出,Android中有好幾種不同的Context,在一個Activity中,就可以擷取到以下幾種Context:

  • getApplication()
  • getApplicationContext()
  • getBaseContext()
  • Activity.this

它們都什麼差別呢?分别在什麼時候使用呢?可以在Activity中通過以下代碼,将這個Context分别列印出來:

// 在Activity.onCreate()中插入以下代碼:
Log.i(TAG, "Application: " + getApplication());
Log.i(TAG, "ApplicationContext: " + getApplicationContext());
Log.i(TAG, "Activity: " + this);
Log.i(TAG, "ActivityContext:" + this);
Log.i(TAG, "Application BaseContext: " + getApplication().getBaseContext());
Log.i(TAG, "Activity BaseContext: " + getBaseContext());

// 得到的運作結果:
I MainActivity: Application: com.duanqz.github.DemoApp@cf8644e
I MainActivity: ApplicationContext: com.duanqz.github.DemoApp@cf8644e
I MainActivity: Activity: com.duanqz.github.MainActivity@bbcadec
I MainActivity: Activity Context: com.duanqz.github.MainActivity@bbcadec
I MainActivity: Application BaseContext: android.app.ContextImpl@6a6a96f
I MainActivity: Activity BaseContext: android.app.ContextImpl@770267
           

可以看到,有以下幾點不同:

  • getApplication()和getApplicationContext()傳回的是同一個對象[email protected],雖然同一塊記憶體區域,但對象的類型不同:前者是Application,後者是Context。Java是強類型的語言,Application到Context相當于向上轉型,會丢失掉一些接口的通路入口。
  • 同理,Activity和Activity Context也是同一個對象,不同的類型。
  • Application和Activity的Base Context都是ContextImpl對象,正是這個Context真正的實作類,被外圍的修飾器包裝了一下,才形成不同功能的類。

3.2 Context導緻的記憶體洩露問題

Context經常會被作為參數傳遞,很容易導緻記憶體洩露。以下代碼片段是一個很常見的Activity洩露問題:

public class MainActivity extends AppCompatActivity {
    protected void onCreate(Bundle savedInstanceState) {
       ... // Other codes
       Singleton.get(this);
    }
}

public class Singleton {
    private static Singleton sMe;
    private Singleton(Context context) {
        // Do something with context
    }

    public static synchronized Singleton get(Context context) {
        if (sMe == null) {
            sMe = new Singleton(context);
        }
        return sMe;
    }
}
           

本例中,有一個需要Context才能初始化的單例,在Activity中使用了這個單例,并且傳入的Context是Activity對象。這種使用方式很可能導緻Activity的洩露,因為MainActivity對象在應用程序的生命周期中可能會存在多個(譬如:多次進入/退出MainActivity界面、橫豎屏切換都可能導緻Activity對象的建立和銷毀),但單例卻是存在于整個應用程序的生命周期的,Activity作為Context傳送給單例,會導緻Activity銷毀後,其對象不能被垃圾回收,這樣一來Activity對象就洩露了。

往深一點說:單例的實作都包含一個靜态變量,而在Java的垃圾回收機制中,靜态變量是GC ROOT,某對象隻要存在到達GC ROOT的路徑,就不會被回收。其實,所謂的記憶體洩露,都是生命周期短的對象沒有被正确的回收,之是以沒有被回收,是因為它們處在到GC ROOT的路徑上,像靜态變量、類加載器等都是GC ROOT,在使用過程如果關聯到了生命周期短的對象,而且沒有及時解除關聯,就會産生記憶體洩露。

寫過Android代碼的朋友都知道,在單例中使用Context是一種剛需,那怎樣才能解決記憶體洩露的問題呢?其實,隻要傳入一個生命周期長的Context就可以,自然就想到了與應用程式生命周期一緻的ApplicationContext:

public class MainActivity extends AppCompatActivity {
    protected void onCreate(Bundle savedInstanceState) {
       ... // Other codes
       Singleton.get(getApplicationContext());
    }
}
           

如此一來,就将單例與Application綁定,解決了Activity洩露的問題。這個案例提醒大家ApplicationContext和ActivityContext的使用場景是不同的:

  • 使用ApplicationContext的場景:Context的生命周期超出Activity或Service的生命周期時,譬如工具類
  • 使用ActivityContext的場景:Context的生命周期小于Activity,譬如初始化Activity的子控件、彈出對話框

4. 總結

本文從設計、源碼和使用三個方面剖析了Android Context這一重要的概念,它是應用程式通路Android資源的接口,它是應用程序的運作環境,它是四大元件的基礎,它是開發者既熟悉的陌生人。Context采用Decorator模式這一頂層設計,其對象建構/銷毀都和四大元件緊密相關,稍有使用不當,便會導緻記憶體洩露。

相信各位開發者在讀完本文後,會對Context有一個更加深刻的認識。

原文位址:https://duanqz.github.io/2017-12-25-Android-Context