背景

之前在项目的开发中,同事遇到了一个问题

在某个版本后交给市场部门同事的 apk 文件,市场的同事反馈,线上的用户新装后的上报的渠道全是官方渠道

PS. 项目中使用美团的 Walle 进行多渠道打包

问题排查

组里的另一个同事开始排查问题,先是排查了交给市场同事的签渠道的工具是不是出了问题。于是先自己用该工具对 apk 进行渠道签入进行检查,发现确实不行。于是手动写入渠道,Debug 发现通过 WalleChannelReader.getChannel(this.getApplicationContext())方法获取到的值一直为空

后来,组内的同事来找我讨论这个问题,同事猜测是否跟之前引入了某个依赖库有关系,而按照我的理解,美团的渠道获取是通过获取到 .apk 文件然后从该文件中的某个分区里获取写入的渠道的,所以应该跟这个问题不会有关系。

我开始看项目中的代码,看似没有问题

1
2
3
4
5
6
7
8
#XXXXApplication.java
private String getChannel() {
String channel = WalleChannelReader.getChannel(getApplicationContext(), "xxxx");
if (TextUtils.isEmpty(channel)) {
channel = "xxxx";
}
return channel;
}

实在没辙了,只好通过 git commit 对比在出现问题前的代码,看是否在做某个需求时不小心改动了此处的代码。

果不其然,发现是另一个同事在做另一个需求的时候,不小心将 getChannel() 的调用放到了 Application#attachBaseContext() 方法中。

而通过查看walle 的源码,发现 WalleChannelReader.getChannel(Context context) 中需要通过 applicationContext 获取到 .apk 文件的地址(path) 才能获取到写在分区内的渠道。

walle 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//这里的 context 即 WalleChannelReader.getChannel(Context context) 中传入的 context,所以这里的 context.getApplicationInfo() 为 null
@Nullable
private static String getApkPath(@NonNull final Context context) {
String apkPath = null;
try {
final ApplicationInfo applicationInfo = context.getApplicationInfo();
if (applicationInfo == null) {
return null;
}
apkPath = applicationInfo.sourceDir;
} catch (Throwable e) {
}
return apkPath;
}

于是,bug 解决了,将上报渠道的代码重新恢复到 onCreate() 后即可

但,仅仅是修复了 bug ,但问题的背后,还有很多不了解的地方。

思考🤔

于是我开始思考

为何在 attachBaseContext(base: Context?) 方法中获取不到 applicationContext

首先,看一下 application 的生命周期,他是如何被创建出来的,这就要从 App 启动流程说起,但这是个比较复杂的过程,但对于这个问题,我们查看源码时只关注跟 context 相关的代码,忽略其他代码,避免在其中浪费太多时间和消耗无谓的精力

首先看一下 getApplicationContext() 方法的实现,可以看到

ContextWrapper.java

1
2
3
4
@Override
public Context getApplicationContext() {
return mBase.getApplicationContext();
}

继续跟踪,看一下 mBase.getApplicationContext() 返回的是什么

Context.java 中可见,这是一个抽象(abstract)类,其 Context getApplicaitonContext() 是个抽象(abstract)方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Return the context of the single, global Application object of the
* current process. This generally should only be used if you need a
* Context whose lifecycle is separate from the current context, that is
* tied to the lifetime of the process rather than the current component.
*
* <p>Consider for example how this interacts with
* {@link #registerReceiver(BroadcastReceiver, IntentFilter)}:
* <ul>
* <li> <p>If used from an Activity context, the receiver is being registered
* within that activity. This means that you are expected to unregister
* before the activity is done being destroyed; in fact if you do not do
* so, the framework will clean up your leaked registration as it removes
* the activity and log an error. Thus, if you use the Activity context
* to register a receiver that is static (global to the process, not
* associated with an Activity instance) then that registration will be
* removed on you at whatever point the activity you used is destroyed.
* <li> <p>If used from the Context returned here, the receiver is being
* registered with the global state associated with your application. Thus
* it will never be unregistered for you. This is necessary if the receiver
* is associated with static data, not a particular component. However
* using the ApplicationContext elsewhere can easily lead to serious leaks
* if you forget to unregister, unbind, etc.
* </ul>
*/
public abstract Context getApplicationContext();

简单解释一下这段注释

返回当前进程的全局唯一的 Application 单例对象

通常来说,仅当需要一个生命周期和当前上下文分开的 Context 时才使用此方法,该 Context 的生命周期和进程的生命周期相关,而和当前的组件无关

思考一下例如如何与 registerReceiver(BroadcastReceiver, IntentFilter) 交互

如果使用一个 Activity Context ,接收器会在 Activity 内被注册,这意味着你需要在 Activity 销毁之前取消注册,否则 framework 会在 activity 移除时清理掉你泄漏的注册并记录下错误。因此,如果你使用 Activity Context 注册一个静态的接收器(进程内全局的,与该 Activity 实例无关) 那么无论你使用的 Activity 什么时候被销毁,这个注册都会被移除

如果使用这里返回的 Context ,则会向你的应用程序相关的全局状态注册接收器,因此他永远不会为你注销。接收器与静态数据而不是特定的组件相关联,这是有必要的。

但是如果你忘记注销,取消绑定等,则在其他地方使用 Application Context 可能容易导致严重的泄漏

那我们就接着看这个方法的实现,正是在 ContextWrapper.java 中,也就是说,ContextWrapper 继承自 Context,并实现了该方法

那我们看一下这个 mBase 是什么

1
2
3
4
5
6
7
8
9
10
11
Context mBase;
public ContextWrapper(Context base){
mBase = base;
}
protected void attachBaseContext(Context base){
if(mBase == null){
throw new IllegalStateException("Base context already set");
}
mBase = base;
}

先不管 mBase 是在什么时候被赋值的,先去看 mBase 的 getApplicationContext() 方法到底做了些什么。
由断点处的信息可见,mBase 即 ContextImpl 的实例,我们也知道 Context 只有一个实现类,即 ContextImpl,找到 ContextImpl.java 类。找到 getApplicaitonContext() 方法的实现如下

1
2
3
4
5
@Override
public Context getApplicationContext() {
return (mPackageInfo != null) ?
mPackageInfo.getApplication() : mMainThread.getApplication();
}

这样一来,我们就知道 getApplicaitonContext() 方法返回的要么是mPackageInfo.getApplication() 要么是 mMainThread.getApplication()

那么在 attachBaseContext() 方法时候调用 getApplicaitonContext() 方法到底是返回哪个呢,这取决于 mPackageInfo 是否为空。

那继续看 mPackageInfo 是在什么时候被赋值的。

1
2
3
4
5
6
7
8
9
10
11
private ContextImpl(@Nullable ContextImpl container, @NonNull ActivityThread mainThread,
@NonNull LoadedApk packageInfo, @Nullable String splitName,
@Nullable IBinder activityToken, @Nullable UserHandle user, int flags,
@Nullable ClassLoader classLoader, @Nullable String overrideOpPackageName) {
//忽略对我们来说暂时不重要的其他代码
...
mPackageInfo = packageInfo;
//忽略对我们来说暂时不重要的其他代码
...
}

mPackageInfo 是在 ContextImpl 的构造方法中赋值的,那也就是说非特殊情况下,mPackageInfo 是不会为 null 的。

那我们接着看 mPackageInfo.getApplication() 的返回值。

1
2
3
4
5
6
#LoadedApk.java
private Application mApplication;
Application getApplication() {
return mApplication;
}

LoadedApk 实例中的一个私有变量,继续看是什么时候赋值的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@UnsupportedAppUsage
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
if (mApplication != null) {
return mApplication;
}
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "makeApplication");
Application app = null;
String appClass = mApplicationInfo.className;
if (forceDefaultAppClass || (appClass == null)) {
appClass = "android.app.Application";
}
try {
java.lang.ClassLoader cl = getClassLoader();
if (!mPackageName.equals("android")) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
"initializeJavaContextClassLoader");
initializeJavaContextClassLoader();
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
}
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
//①
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
appContext.setOuterContext(app);
} catch (Exception e) {
if (!mActivityThread.mInstrumentation.onException(app, e)) {
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
throw new RuntimeException(
"Unable to instantiate application " + appClass
+ ": " + e.toString(), e);
}
}
mActivityThread.mAllApplications.add(app);
//②
mApplication = app;
//忽略暂时不关心的代码
...
return app;
}

我们重点看一下

1
2
app = mActivityThread.mInstrumentation.newApplication(cl, appClass, appContext);

跟踪一下 newApplication() 这个方法

1
2
3
4
5
6
7
8
public Application newApplication(ClassLoader cl, String className, Context context)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
Application app = getFactory(context.getPackageName())
.instantiateApplication(cl, className);
app.attach(context);
return app;
}

可见,在这里调用了 Application 的 attach(Context context) 方法

Application 这个类中的 attach 方法中调用了 attachBaseContext(Context context) 方法,而这个,就是我们在自定义的 XXApplication 中 Override 的 attachBaseContext(Context context) ,而根据上面的分析,我们知道此时还未给 ②mApplication 赋值,所以我们在 attachBaseContext(Context context) 方法中调用 getApplicaitonContext() 就为 null

到此,真相大白