从业多年的Android开发,竟拿不到 Application Context?

news2024/11/25 22:40:03

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");
    }
}
  1. 上文提到 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

the end

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


为了彻底搞的这块知识点,我对Framework 进行了一段时间的深入研究,并根据自己所学所理解整理了一些学习笔记,并更具不同小知识点进行了归类:

《Android Framework学习手册》:https://0a.fit/acnLL

  1. 开机Init 进程
  2. 开机启动 Zygote 进程
  3. 开机启动 SystemServer 进程
  4. Binder 驱动
  5. AMS 的启动过程
  6. PMS 的启动过程
  7. Launcher 的启动过程
  8. Android 四大组件
  9. Android 系统服务 - Input 事件的分发过程
  10. Android 底层渲染 - 屏幕刷新机制源码分析
  11. Android 源码分析实战

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/116108.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

国考省考行测:细节理解,对错判断,要素查找,问什么,找什么,对比分析

国考省考行测:细节理解,对错判断,要素查找,问什么,找什么,对比分析 2022找工作是学历、能力和运气的超强结合体! 公务员特招重点就是专业技能,附带行测和申论,而常规国考省考最重要…

头戴式耳机适合运动吗、五款最适合运动的耳机分享

戴着耳机锻炼,听着动感的音乐,会让你心潮澎湃,瞬间感觉自己力大无穷。那什么样的耳机更适合在健身房锻炼时戴呢?首先稳固性和舒适度一定要比较好,毕竟在运动的过程中老是感觉到不适或者掉落,那真的是很令人…

旋转的扰动、导数和积分

The plus operator 设M\mathcal{M}M表示一个n维的流型,因为流型局部同胚与Rn\mathbb{R}^nRn,所以我们可以通过定义符号⊞\boxplus⊞和⊟\boxminus⊟建立一个流型M\mathcal{M}M的局部邻域和其正切空间的双射。 ⊞:MRn→M;⊟:MMn→Rn\boxplus:\mathcal{M}…

为什么说用了MES系统,但生产数字化管理只做到20%?

随着越来越多的制造企业开始体验和建设MES系统,MES系统能够实现生产执行过程的有效数字化管理已经成为共识。回过头来看,是不是只有生产进度才是工厂需要管理的?当你想详细了解当前存在的问题,进展如何,哪些月度重点任…

JAVA零基础小白学习教程之day09-内部类权限final静态

day09-JAVAOOP 课程目标 1. 【理解】什么是内部类 2. 【掌握】匿名内部类 3. 【掌握】引用数据类型作为方法的参数 4. 【理解】final关键字的使用 5. 【理解】包的定义及使用 6. 【理解】权限修饰符 7. 【掌握】static关键字的使用B友https://www.bilibili.com/video/BV1QG4y…

职场员工有没有潜力,看这一个能力就够了

在我组织的研讨会上,讲故事的课程通常以一个思考练习开始。 我会让参与者闭上眼睛回忆《小红帽》的故事,尤其考虑其中的情节、起伏和结尾。这个练习有时会引发一阵笑声,人们会好奇这与课程有什么相关性,或者干脆把《小红帽》与《…

给Debezium connector 发送信号

1. 概述 Debezium 信号机制提供了一种方法来修改连接器的行为,或触发一次性操作,例如启动表的 临时快照。要触发连接器执行指定操作,可以发出特殊的SQL 命令以将信号消息添加到专门的信号表,也称为信号数据集合。在源数据库上创建…

哪些进销存软件既好用又免费?

中小企业刚起步都会面临着资金紧缺、人力资源不足等诸多管理问题,对于大部分预算不是很多的中小企业或者尚未尝试过进销存软件的企业,都会选择免费进销存软件来进行企业管理或体验进销存软件的功效。 进销存软件的开发需要强大的技术支持,数…

B站技术选型与架构

目录前言B站前端之路B站Golang技术栈分析bilibili技术总监毛剑简介前言 了解了一下B站的技术发展历程:最开始是用PHP语言开发的,后来B站的中台逐步被Node占领,而后台技术为了更高的并发、更稳健,以及为了大数据分析,逐…

RK3568平台开发系列讲解(Linux系统篇)共享内存

🚀返回专栏总目录 文章目录 一、共享内存底层实现二、共享内存API三、共享内存案例沉淀、分享、成长,让自己和他人都能有所收获!😄 📢共享内存技术是功能最强、应用最广的进程间通信技术。其原理是多个进程共享相同的物理内存区,一个进程对该内存区的任意修改,可被其…

不懂Nacos没关系,可以看看它是怎么运用代理模式的

背景 看Nacos的源代码时,发现其中有对代理模式的运用,而且用得还不错,可以作为一个典型案例来聊聊,方便大家以更真实的案例来体验一下代理模式的运用。如果你对Nacos不了解,也并不影响对本篇文章的阅读和学习。 本文…

前端三小时用html和js写一个贪吃蛇游戏,非常简单带讲解,代码可直接用,功能完整

目录 游戏主体部分--地狱模式 游戏主页入口 预览图 游戏入口代码 1.html 2.css 3.js 注册页面代码 游戏实现很简单,只写游戏主体的话只要三小时就够了。 话不多说,我们直接来看效果预览。 转成gif图之后有点卡,但是游戏效果并不卡&…

php宝塔搭建部署实战PESCMSTEAM团队任务管理系统源码

大家好啊,我是测评君,欢迎来到web测评。 本期给大家带来一套php开发的PESCMSTEAM团队任务管理系统源码,感兴趣的朋友可以自行下载学习。 技术架构 PHP7.2 nginx mysql5.7 JS CSS HTMLcnetos7以上 宝塔面板 文字搭建教程 下载源码&am…

k8s lifecycle——poststart和prestop

1、lifecycle的声明 lifecycle:postStart:exec:command: ["/bin/sh", "-c", "sleep 100"]preStop:exec:command: ["/bin/sh", "-c", "sleep 100"]2、poststart 容器创建后立即执行,主要用于资源部署、…

JPEG编码原理及简易编码器实现

简介 以学习为目的编写的简易jpeg编码器,以看得懂为目标,代码尽可能清晰简洁,不对内存、性能做看不懂的优化,也不实现jpeg更多高级特性。 这篇文章是我从自己的开源工程中整理来的 本文对应的工程为https://gitee.com/dma/learn…

【OpenFOAM】-olaFlow-算例4- irreg45degTank

算例路径: olaFlow\tutorials\irreg45degTank 算例描述: 不规则波浪模拟 学习目标: 不规则波浪模拟:olaFlow中单向不规则波采用线性波浪叠加法生成,基本原理如图2所受,需要提供对应波谱的周期、波高和相位的…

生产制造业管理系统对企业究竟有哪些作用?

对于生产制造企业来说,除了涉及到产品的生产制造和原料采购,还需要管理销售、库存、财务等方方面面,生产制造业管理系统的使用,尤为重要。正因如此,借助生产制造业管理系统来完善生产管理流程、提升生产管理水平&#…

LVGL学习笔记4 - 主题Themes

目录 1. 获取主题句柄 2. 设置基础主题 3. 设置主题的回调函数 4. 使能主题 5. 实例 5.1 定义一个全局Style变量 5.2 显示默认主题风格的矩形 5.3 初始化新主题的样式 5.4 初始化新主题 5.5 回调函数的实现 5.6 设置新主题 5.7 显示 主题是风格的集合。对应的变量结构…

设计模式--reactor 模式

说明 本文基于 tomcat 8.5.x 编写。author blog.jellyfishmix.com / JellyfishMIX - githubLICENSE GPL-2.0 介绍 reactor 模式通常应用于网络 IO 场景,高性能的中间件 redis, netty 都在使用。 背景 原始的网络 IO 模型 最原始的网络 IO 模型,服务…

Java学习笔记【8】异常

⛵ ⛵ ⛵ ⛵ ⛵ 🚀 🚀 🚀 🚀 🚀   大家好🤝,我是 👉老孙👈,未来学习路上多多关照 🤝 一个喜欢用 ✍️ 博客记录人生的程序猿 🙉&…