Android 老手翻車了,竟拿不到 Application Context?| 開發者說·DTalk

語言: CN / TW / HK

本文原作者: Jingle Zhang, 原文 釋出於: TechMerger

Android 開發者們對於 Application 並不陌生。有的時候為避免記憶體洩漏,常常不直接使用 Context 而是通過其提供的 getApplicationContext() 確保拿到的是 Application 級別的 Context。而本次像通常一樣,拿到的 Application 卻是 null ,到底是發生什麼事了?

翻車了

先來回顧一下發生問題的程式碼。為了避免記憶體洩漏,在對外提供的 Jar 包裡不假思索地用瞭如下程式碼: 

private DemoManager(Context context){
mContext = context.getApplicationContext();
if(DEBUG){
mContext.getPackageName();
...
}
}

看似很平常的一個寫法,在專案中應用該 Jar 包的時候,卻發生了崩潰:  mContext.getPackageName() 發生了空指標異常。

當看到是此處發生的 crash,屬實有點意外、但也沒時間多想,暫時將程式碼改成了這樣。

private DemoManager(Context context){
mContext = context.getApplicationContext();
if(null == mContext){
mContext = context;
}
if(DEBUG){
mContext.getPackageName();
...
}
}

事後覺得有必要搞清楚,作為一名 Android 老手這著實有點顛覆認知!

Application Context 不應該都是先建立的嘛,為什麼 Context 都有了 Application 卻沒有呢?

發生什麼事了

嘗試寫了 Demo 去復現,但是沒成功。後來發現一般不會發生這樣的問題,本次發生是因為執行的 App 比較特殊。

實際的程式碼在 TelephonyProvider App 裡添加了自定義的 ContentProvider ,並在 query() 裡使用了上述 Jar 包。而 TelephonyProvider App 所依賴的 com.android.phone 系統程序會先啟動,之後 TelephonyProvider 才會被載入到該程序。

令人意想不到的是,對於 TelephonyProvider App 來說 其 Application 一直是 null,並不是它自己的 Application,更不是 Phone Application

所以,Demo 需要採用上述類似的特性才能復現。比如提供 2 個 App,一個是查詢 ContentProvider 的 Query App;另一個是供 ContentProvider 的 Provider App。

  1. Query App 要和 Provider App 在同一個程序,通過 android:process="XXX" 指定

  2. Query App 先啟動,並通過 ContentResolver 呼叫 Provider App 進行 query ( 需要註明: ApplicationContext 為 null 和 Query App 呼叫 query 並無關係 )

起初沒注意到 TelephonyProvider 和 Phone 同進程的特性,所以 DEMO 怎麼也復現不了。接下來我們在 FW 裡深入分析下: 

為什麼共用程序的 Provider App 拿不到 Application?

不按套路出牌啊

首先回顧下 ContentProvider 中 Context 是哪兒來的?

// frameworks/base/core/java/android/app/ActivityThread.java
private ContentProviderHolder installProvider(Context context...) {
ContentProvider localProvider = null;
IContentProvider provider;
if (holder == null || holder.provider == null) {
Context c = null;
ApplicationInfo ai = info.applicationInfo;
if (context.getPackageName().equals(ai.packageName)) {
// 如果 Provider App 是獨立程序,context 採用傳遞過來的 Application 引數
c = context;
} else if (mInitialApplication != null &&
mInitialApplication.getPackageName().equals(ai.packageName)) {
c = mInitialApplication;
} else {
try {
// 反之呼叫 createPackageContext 建立特有的 Context
c = context.createPackageContext(ai.packageName,
Context.CONTEXT_INCLUDE_CODE);
}...
}
...
if (info.splitName != null) {
try {
c = c.createContextForSplit(info.splitName);
} catch (NameNotFoundException e) {
throw new RuntimeException(e);
}
}
if (info.attributionTags != null && info.attributionTags.length > 0) {
final String attributionTag = info.attributionTags[0];
c = c.createAttributionContext(attributionTag);
}


try {
// 這裡的 c 就是傳遞給 ContentProvider 的實際 Context
localProvider.attachInfo(c, info);
...
}
}
...
}

傳遞給 ContentProvider 的 Context 有多種建立方式。如果 Query App 與 Provider App 的 packageName 不相同,這個時候 Provider App 就不能直接使用 Query App 的 Application,要重新建立一個給它,入口在 createPackageContextAsUser 中。

@Override
public Context createPackageContextAsUser(String packageName, int flags, UserHandle user)
throws NameNotFoundException {
// 這裡會呼叫 LoadedApk 建構函式
// LoadedApk 持有 Application 例項預設情況為 null
LoadedApk pi = mMainThread.getPackageInfo(packageName, mResources.getCompatibilityInfo(),
flags | CONTEXT_REGISTER_PACKAGE, user.getIdentifier());
...
}

createPackageContextAsUser() 會建立自己的 LoadedApk 例項,而 LoadedApk 持有的 Application 例項預設情況下是 null。 所以後面如果沒有機會賦值 Application 的話,Provider App 拿到的 Application 永遠為空。

而 Context#getApplicationContext 獲取的 Application 是不是就是它哩?

// frameworks/base/core/java/android/app/ContextImpl.java
public Context getApplicationContext() {
return (mPackageInfo != null) ?
mPackageInfo.getApplication() : mMainThread.getApplication();
}

可以看到有兩個來源: 

  1. mPackageInfo : 即 LoadedApk,一般情況下都是經過該例項獲取的 Application

  2. mMainThread : 當 ActivityThread 在 attach 的時候就已經初始化了 mInitialApplication,不太可能為 null,這裡不展開。

所以問題應該就是 LoadedApk 中持有的 Application 為空導致的。

而 LoadedApk 持有的 Application 例項是在 makeApplication() 裡建立和賦值的,所以需要進一步分析一下 makeApplication() 的呼叫源頭。

經過搜尋發現在 ActivityThread 中存在如下幾個關鍵呼叫地方: 

  • handleBindApplication() : 程序冷啟動的時候建立 Application 例項,即本案例中的 Query App 的 Application

  • performLaunchActivity() : 啟動 Activity 的時候

  • handleCreateService() : 啟動 Service 的時候

  • handleReceiver() : 收到廣播的時候

四大元件除了 ContentProvider 都會執行 makeApplication() (暫時無法知道 Google 為什麼這麼做,可能另有深意)。

// frameworks/base/core/java/android/app/LoadedApk.java
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
...
// 建立Application
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
...
mActivityThread.mAllApplications.add(app);
mApplication = app;
if (instrumentation != null) {
try {
// 呼叫 Application#onCreate()
instrumentation.callApplicationOnCreate(app);
...
}
}
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
return app;
}

試試吧

經過了如上的程式碼分析,不禁產生了如下猜想: 

  1. getApplicationContext 為 null,是不是意味著 Provider app 中的 Application 不會建立了?

    加入如下 Log 復現了一下,發現問題發生的時候確實不會呼叫 Application#onCreate()。

    public class ProviderApplication extends Application {
    @Override
    public void onCreate() {
    super.onCreate();
    android.util.Log.e("ProviderApplication","onCreate");
    }
    }
  2. 上文提到 Service、Activity、Receiver 三大元件啟動的時候有機會呼叫 makeApplication(),那麼我在 Provider App 裡啟動一個Service,是不是就沒有問題了?

    答案是肯定的,如下的 Log 可以看到兩個 App 共用一個程序,手動啟動 Service 之後 Application 例項才可以拿到。

    Demo 資訊補充如下: 

    • Query App,包名為 com.zxg.testcode

    • Provider App,包名:  com.zxg.queryproviderdemo ,啟動的 Service 為 ProviderService ,Application 為 ProviderApplication ,ContentProvider 為  QueryProvider

// 啟動 Query App 第一次查詢
2022-04-01 15:14:41.126 18687-18687/com.zxg.testcode E/QueryProvider: query
// getContext() 是 [email protected]
2022-04-01 15:14:41.127 18687-18687/com.zxg.testcode E/QueryProvider: getContext() is android.app.[email protected]869d7cf
// 而 getApplicationContext() 是 null
2022-04-01 15:14:41.127 18687-18687/com.zxg.testcode E/QueryProvider: context is null


// 手動啟動一個 service,ProviderApplication 建立了並回調了 onCreate
2022-04-01 15:14:46.378 18687-18687/com.zxg.testcode E/ProviderApplication: onCreate
// Service 啟動了並拿到了 Application
2022-04-01 15:14:46.380 18687-18687/com.zxg.testcode E/ProviderService: onStartCommand ApplicationContext is com.zxg.queryproviderdemo.[email protected]472f1c7


// Query App 第二次查詢
2022-04-01 15:14:49.564 18687-18687/com.zxg.testcode E/QueryProvider: query
2022-04-01 15:14:49.564 18687-18687/com.zxg.testcode E/QueryProvider: getContext() is android.app.[email protected]869d7cf
// 這時候 query() 裡也拿到了 Application
2022-04-01 15:14:49.564 18687-18687/com.zxg.testcode E/QueryProvider: context is com.zxg.queryproviderdemo.[email protected]472f1c7

The End

如果提供 ContentProvider 的 App 程序是共用的,需要注意其生命週期回撥的時候有可能拿不到 Application 例項這個坑。當然這種情況比較罕見,如果遇到了可以考慮下 Context 例項能不能滿足您的需求,並輔以必要的 Null 檢查。

長按右側二維碼

檢視更多開發者精彩分享

"開發者說·DTalk" 面向 中國開發者們徵集 Google 移動應用 (apps & games) 相關的產品/技術內容。歡迎大家前來分享您對移動應用的行業洞察或見解、移動開發過程中的心得或新發現、以及應用出海的實戰經驗總結和相關產品的使用反饋等。我們由衷地希望可以給這些出眾的中國開發者們提供更好展現自己、充分發揮自己特長的平臺。我們將通過大家的技術內容著重選出優秀案例進行 谷歌開發技術專家 (GDE) 的推薦。

  點選屏末  |  閱讀原文  | 即刻報名參與  " 開發者說 · DTalk"