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 的 App:Query App;另一个是供 ContentProvider 的App :Provider App。

Query App 与 Provider App 在同一个进程 ,通过 android:process=“XXX” 指定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();

}

可以看到有两个来源:

mPackageInfo:即 LoadedApk,一般情况下都是经过该实例获取的 ApplicationmMainThread:当 ActivityThread 在 attach 的时候就已经初始化了mInitialApplication,不太可能为 null ,这里不展开。

所以问题应该就是 LoadedApk 中持有的 Application 为空导致的。

而 LoadedApk 持有的 Application 实例是在 makeApplication() 里创建和赋值的,所以需要进一步分析一下 makeApplication() 的调用源头。

经过搜索发现在 ActivityThread 中存在如下几个关键调用地方:

handleBindApplication(): 进程冷启动的时候创建 Application 实例,即本案例中的 Query App 的 ApplicationperformLaunchActivity(): 启动 Activity 的时候handleCreateService(): 启动 Service 的时候handleReceiver(): 收到广播的时候

四大组件除了 ContentProvider 都会执行 makeApplication()(这里也可以理解,其他几个都是需要启动是时候才创建实例,ContentProvider 实例的创建是进程启动中)。

// 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;

}

试试吧

经过了如上的代码分析,不禁产生了如下猜想:

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");

}

}

上文提到 Service、Activity、Receiver 三大组件启动的时候有机会调用 makeApplication(),那么我在 Provider App 里启动一个Service ,是不是就没有问题了? 答案是肯定的,如下的 Log 可以看到两个 App 共用一个进程,手动启动 Service 之后 Application 实例才可以拿到。 Demo 信息补充如下:

Query App,包名 为 com.zxg.testcodeProvider 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() 是 android.app.ContextImpl@869d7cf

2022-04-01 15:14:41.127 18687-18687/com.zxg.testcode E/QueryProvider: getContext() is android.app.ContextImpl@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.ProviderApplication@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.ContextImpl@869d7cf

// 这时候 query() 里也拿到了 Application

2022-04-01 15:14:49.564 18687-18687/com.zxg.testcode E/QueryProvider: context is com.zxg.queryproviderdemo.ProviderApplication@472f1c7

the end

如果提供 ContentProvider 的 App 进程是共用的,需要注意其生命周期回调的时候有可能拿不到 Application 实例这个坑。当然这种情况比较罕见,如果遇到了可以考虑下 Context 实例能不能满足你的需求,并辅以必要的 Null 检查。

写在最后

在技术领域内,没有任何一门课程可以让你学完后一劳永逸,再好的课程也只能是“师傅领进门,修行靠个人”。“学无止境”这句话,在任何技术领域,都不只是良好的习惯,更是程序员和工程师们不被时代淘汰、获得更好机会和发展的必要前提。

如果你觉得自己学习效率低,缺乏正确的指导,可以扫码,加入我们资源丰富,学习氛围浓厚的技术圈一起学习交流吧!

加入我们吧!群内有许多来自一线的技术大牛,也有在小厂或外包公司奋斗的码农,我们致力打造一个平等,高质量的Android交流圈子,不一定能短期就让每个人的技术突飞猛进,但从长远来说,眼光,格局,长远发展的方向才是最重要的。

35岁中年危机大多是因为被短期的利益牵着走,过早压榨掉了价值,如果能一开始就树立一个正确的长远的职业规划。35岁后的你只会比周围的人更值钱。

好文推荐

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。