Android 老手翻车了,竟拿不到 Application Context?| 开发者说·DTalk

cf277bb29e5b51933486250f621de97f.jpeg

本文原作者: Jingle Zhang,原文发布于: TechMerger

Android 开发者们对于 Application 并不陌生。有的时候为避免内存泄漏,常常不直接使用 Context 而是通过其提供的 getApplicationContext() 确保拿到的是 Application 级别的 Context。而本次像通常一样,拿到的 Application 却是 null,到底是发生什么事了?

a9b76021306c72594b8cd28a74bde46a.png

翻车了

先来回顾一下发生问题的代码。为了避免内存泄漏,在对外提供的 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 却没有呢?

1c1a939887838bf3db0ce4ca8313c641.png

发生什么事了

尝试写了 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?

ace9210e2cd9ca12dc7bc6cc8f8a2077.png

不按套路出牌啊

首先回顾下 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;
}

2d3a23bcc1274676bb5dfa838a2588e3.png

试试吧

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

  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() 是 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

be1a1167db510e23ba8acb966e4252ef.png

The End

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


长按右侧二维码

查看更多开发者精彩分享

05d045dfa3823d5cdc062d5686da0a9e.png

"开发者说·DTalk" 面向7a23ab427b0e1843be9f0d9a768c7263.png中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。

aff227c3d2f3e07c9682505d0c873529.gif 点击屏末 | 阅读原文 | 即刻报名参与 "开发者说·DTalk" 


527d1f3581b62efbd119b3a500b6f67b.png

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值