一种 Android 应用内全局获取 Context 实例的装置【转】

App 运行的时候,肯定是存在至少一个 Application 实例的。同时,Context 我们再熟悉不过了,写代码的时候经常需要使用到 Context 实例,它一般是通过构造方法传递进来,通过方法的形式参数传递进来,或者是通过 attach 方法传递进我们需要用到的类。Context 实在是太重要了,以至于我经常恨不得着藏着掖着,随身带着,这样需要用到的时候就能立刻掏出来用用。但是换个角度想想,既然 App 运行的时候,Application 实例总是存在的,那么为何不设置一个全局可以访问的静态方法用于获取 Context 实例,这样以来就不需要上面那些繁琐的传递方式。
说到这里,有的人可能说想这不是我们经常干的好事吗,有必要说的这么玄乎?少侠莫急,请听吾辈徐徐道来。

获取Context实例的一般方式

这再简单不过了。

这种方式应该是最常见的获取 Context 实例的方式了,优点就是严格按照代码规范来,不用担心兼容性问题;缺点就是 API 设计严重依赖于 Context 这个 API,如果早期接口设计不严谨,后期代码重构的时候可能很要命。此外还有一个比较有趣的问题,我们经常使用 Activity 或者 Application 类的实例作为 Context 的实例使用,而前者本身又实现了别的接口,比如以下代码。

这段代码是我许久前看过的代码,本身不是什么厉害的东西,不过这段代码段我至今印象深刻。设想,如果 Foo 的接口设计可以不用依赖 Context,那么这里至少可以少一个this不是吗。

获取Context实例的二般方式

现在许多开发者喜欢设计一个全局可以访问的静态方法,这样以来在设计 API 的时候,就不需要依赖 Context 了,代码看起来像是这样的。

这样在整个项目中,都可以通过Foo#getContext()获取 Context 实例了。不过目前看起来好像还有点小缺陷,就是使用前需要调用Foo#setContext(Context)方法进行注册(这里暂不讨论静态 Context 实例带来的问题,这不是本篇幅的关注点)。好吧,以我的聪明才智,很快就想到了优化方案。

不过这样又有带来了另一个问题,一般情况下,我们是把应用的入口程序类FooApplication放在 App 模块下的,这样一来,Library 模块里面代码就访问不到FooApplication#getContext()了。当然把FooApplication下移到基础库里面也是一种办法,不过以我的聪明才智又立刻想到了个好点子。

这样以来,就不用把FooApplication下移到基础库里面,Library 模块里面的代码也可以通过BaseApplication#getContext()访问到 Context 实例了。嗯,这看起来似乎是一种神奇的膜法,因吹斯听。然而,代码写完还没来得及提交,包工头打了个电话来和我说,由于项目接入了第三发 SDK,需要把FooApplication继承SdkApplication。

有没有什么办法能让FooApplication同时继承BaseApplication和SdkApplication啊?(场面一度很尴尬,这里省略一万字。)

以上谈到的,都是以前我们在获取 Context 实例的时候遇到的一些麻烦:

  • 类 API 设计需要依赖 Context(这是一种好习惯,我可没说这不好);
  • 持有静态的 Context 实例容易引发的内存泄露问题;
  • 需要提注册 Context 实例(或者释放);
  • 污染程序的 Application 类;
    那么,有没有一种方式,能够让我们在整个项目中可以全局访问到 Context 实例,不要提前注册,不会污染 Application 类,更加不会引发静态 Context 实例带来的内存泄露呢?

一种全局获取 Context 实例的方式

回到最开始的话,App 运行的时候,肯定存在至少一个 Application 实例。如果我们能够在系统创建这个实例的时候,获取这个实例的应用,是不是就可以全局获取 Context 实例了(因为这个实例是运行时一直存在的,所以也就不用担心静态 Context 实例带来的问题)。那么问题来了,Application 实例是什么时候创建的呢?首先先来看看我们经常用来获取 Base Context 实例的Application#attachBaseContext(Context)方法,它是继承自ContextWrapper#attachBaseContext(Context)的。

是谁调用了这个方法呢?可以很快定位到Application#attach(Context)。

又是谁调用了Application#attach(Context)方法呢?一路下来可以直接定位到Instrumentation#newApplication(Class<?>, Context)方法里(这个方法名很好懂啊,一看就知道是干啥的)。

看来是在这里创建了 App 的入口 Application 类实例的,是不是想办法获取到这个实例的应用就可以了?不,还别高兴太早。我们可以把 Application 实例当做 Context 实例使用,是因为它持有了一个 Context 实例(base),实际上 Application 实例都是通过代理调用这个 base 实例的接口完成相应的 Context 工作的。在上面的代码中,可以看到系统创建了 Application 实例 app 后,通过app.attach(context)把 context 实例设置给了 app。直觉告诉我们,应该进一步关注这个 context 实例是怎么创建的,可以定位到LoadedApk#makeApplication(boolean, Instrumentation)代码段里。

好了,到这里我们定位到了 Application 实例和 Context 实例创建的位置,不过距离我们的目标只成功了一半。因为如果我们要想办法获取这些实例,就得先知道这些实例被保存在什么地方。上面的代码一路逆向追踪过来,好像也没看见实例被保存给成员变量或者静态变量,所以暂时还得继续往上捋。很快就能捋到ActivityThread#performLaunchActivity(ActivityClientRecord, Intent)。

这里是我们启动 Activity 的时候,Activity 实例创建的具体位置,以上代码段还可以看到喜闻乐见的”Unable to start activity” 异常,你们猜猜这个异常是谁抛出来的?这里就不发散了,回到我们的问题来,以上代码段获取了一个 Application 实例,但是并没有保持住,看起来这里的 Application 实例就像是一个临时变量。没办法,再看看其他地方吧。接着找到ActivityThread#handleCreateService(CreateServiceData),不过这里也一样,并没有把获取的 Application 实例保存起来,这样我们就没有办法获取到这个实例了。

我们可以看到,这里创建 Application 实例后,把实例保存在 ActivityThread 的成员变量mInitialApplication中。不过仔细一看,只有当system == true的时候(也就是系统应用)才会走这个逻辑,所以这里的代码也不是我们要找的。不过,这里给我们一个提示,如果能想办法获取到 ActivityThread 实例,或许就能直接拿到我们要的 Application 实例。此外,这里还把 ActivityThread 的实例赋值给一个静态变量sCurrentActivityThread,静态变量正是我们获取系统隐藏 API 实例的切入点,所以如果我们能确定 ActivityThread 的mInitialApplication正是我们要找的 Application 实例的话,那就大功告成了。继续查找到ActivityThread#handleBindApplication(AppBindData),光从名字我们就能猜出这个方法是干什么的,直觉告诉我们离目标不远了~

我们看到这里同样把 Application 实例保存在 ActivityThread 的成员变量mInitialApplication中,紧接着我们看看谁是调用了handleBindApplication方法,很快就能定位到ActivityThread.H#handleMessage(Message)里面。

Bingo!至此一切都清晰了,ActivityThread#mInitialApplication确实就是我们需要找的 Application 实例。整个流程捋顺下来,系统创建 Base Context 实例、Application 实例,以及把 Base Context 实例 attach 到 Application 内部的流程大致可以归纳为以下调用顺序。

ActivityThread#bindApplication (异步) –> ActivityThread#handleBindApplication –> LoadedApk#makeApplication –> Instrumentation#newApplication –> Application#attach –> ContextWrapper#attachBaseContext

源码撸完了,再回到我们一开始的需求来。现在我们要获取 ActivityThread 的静态成员变量 sCurrentActivityThread。阅读源码后我们发现可以通过ActivityThread#currentActivityThread()这个静态方法来获取这个静态对象,然后通过ActivityThread#getApplication()方法就可能直接获取我们需要的 Application 实例了。啊,这用反射搞起来简直再简单不过了!说搞就搞。

这样以来, 无论在项目的什么地方,无论是在 App 模块还是 Library 模块,都可以通过Applications#context()获取 Context 实例,而且不需要做任何初始化工作,也不用担心静态 Context 实例带来的问题,测试代码跑起来没问题,接入项目后也没有发现什么异常,我们简直要上天了。不对,哪里不对。不科学,一般来说不可能这么顺利的,这一定是错觉。果然项目上线没多久后立刻原地爆炸了,在一些机型上,通过Applications#context()获取到的 Context 恒为 null。

通过测试发现,在 4.1.1 系统的机型上,会稳定出现获取结果为 null 的现象,看来是系统源码的实现上有一些出入导致,总之先看看源码吧。

原来是这么一个幺蛾子,在 4.1.1 系统上,ActivityThread 是使用一个 ThreadLocal 实例来存放静态 ActivityThread 实例的。至于 ThreadLocal 是干什么用的这里暂不展开,简单说来,就是系统只有在 UI 线程使用 sThreadLocal 来保存静态 ActivityThread 实例,所以我们只能在 UI 线程通过 sThreadLocal 获取到这个保存的实例,在 Worker 线程 sThreadLocal 会直接返回空。

这样以来解决方案也很明朗,只需要在事先现在 UI 线程触发一次Applications#context()调用保存 Application 实例即可。不过项目的代码一直在变化,我们很难保证不会有谁不小心触发了一次优先的 Worker 线程的调用,那就 GG 了,所以最好在Applications#context()方法里处理,我们只需要确保能在 Worker 线程获得 ActivityThread 实例就 Okay 了。不过一时半会我想不出切确的办法,也找不到适合的切入点,只做了下简单的处理:如果是优先在 Worker 线程调用,就先使用 UI 线程的 Handler 提交一个任务去获取 Context 实例,Worker 线程等待 UI 线程获取完 Context 实例,再接着返回这个实例。

最终完成的代码可以参考 Applications

vip视频

著作信息:本文章出自 Kaede 的博客,原文地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,710评论 4 376
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,839评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,295评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,776评论 0 223
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,198评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,074评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,200评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,986评论 0 214
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,733评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,877评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,348评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,675评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,393评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,209评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,212评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,003评论 2 280

推荐阅读更多精彩内容