为什么 AndroidTestCase.getContext().getApplicationContext() 返回 null?
2012 年 2 月 13 日更新:接受了一个答案,解释说这种行为是一个错误,并指出它似乎在模拟器上消失得比 v 1.6 更好,这使得它对我们大多数人来说不是问题。解决方法只是循环/睡眠,直到 getContext().getApplicationContext() 返回非空。 结束更新
根据 android.app.Application javadoc,我定义了一个单例(称为数据库),我的所有活动都可以访问该单例以获取状态和持久数据,并且 Database.getDatabase(Context) 通过 Context.getApplicationContext() 获取应用程序上下文。当活动将自身传递给 getDatabase(Context) 时,此设置可以正常工作,但是当我从 AndroidTestCase 运行单元测试时,getApplicationContext() 调用通常返回 null,尽管测试时间越长,返回非 null 的频率就越高价值。
以下代码在 AndroidTestCase 中重现 null ——演示中不需要单例。
首先,为了记录应用程序实例化消息,在测试中的应用程序中,我定义了 MyApp 并将其添加到清单中。
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
Log.i("MYAPP", "this=" + this);
Log.i("MYAPP", "getAppCtx()=" + getApplicationContext());
}
}
接下来,我定义了一个测试用例来报告 AndroidTestCase.getContext() 4 次,中间有一些睡眠和 getSharedPreferences() 调用:
public class DatabaseTest extends AndroidTestCase {
public void test_exploreContext() {
exploreContexts("XPLORE1");
getContext().getSharedPreferences("foo", Context.MODE_PRIVATE);
exploreContexts("XPLORE2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
exploreContexts("XPLORE3");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
exploreContexts("XPLORE4");
}
public void exploreContexts(String tag) {
Context testContext = getContext();
Log.i(tag, "testCtx=" + testContext +
" pkg=" + testContext.getApplicationInfo().packageName);
Log.i(tag, "testContext.getAppCtx()=" + testContext.getApplicationContext());
try {
Context appContext = testContext.createPackageContext("com.foo.android", 0);
ApplicationInfo appInfo = appContext.getApplicationInfo();
Log.i(tag, "appContext=" + appContext +
" pkg=" + appContext.getApplicationInfo().packageName);
Log.i(tag, "appContext.getAppCtx()=" + appContext.getApplicationContext());
} catch (NameNotFoundException e) {
Log.i(tag, "Can't get app context.");
}
}
}
这是生成的 logCat 的一部分(通过 Eclipse 在 SDK11 WinXP 上运行 1.6 模拟器):
INFO/TestRunner(465): started: test_exploreContext(test.foo.android.DatabaseTest)
INFO/XPLORE1(465): testCtx=android.app.ApplicationContext@43757368 pkg=com.foo.android
INFO/XPLORE1(465): testContext.getAppCtx()=null
INFO/XPLORE1(465): appContext=android.app.ApplicationContext@437801e8 pkg=com.foo.android
INFO/XPLORE1(465): appContext.getAppCtx()=null
INFO/XPLORE2(465): testCtx=android.app.ApplicationContext@43757368 pkg=com.foo.android
INFO/XPLORE2(465): testContext.getAppCtx()=null
INFO/XPLORE2(465): appContext=android.app.ApplicationContext@43782820 pkg=com.foo.android
INFO/XPLORE2(465): appContext.getAppCtx()=null
INFO/MYAPP(465): this=com.foo.android.MyApplication@43783830
INFO/MYAPP(465): getAppCtx()=com.foo.android.MyApplication@43783830
INFO/XPLORE3(465): testCtx=android.app.ApplicationContext@43757368 pkg=com.foo.android
INFO/XPLORE3(465): testContext.getAppCtx()=com.foo.android.MyApplication@43783830
INFO/XPLORE3(465): appContext=android.app.ApplicationContext@43784768 pkg=com.foo.android
INFO/XPLORE3(465): appContext.getAppCtx()=com.foo.android.MyApplication@43783830
INFO/XPLORE4(465): testCtx=android.app.ApplicationContext@43757368 pkg=com.foo.android
INFO/XPLORE4(465): testContext.getAppCtx()=com.foo.android.MyApplication@43783830
INFO/XPLORE4(465): appContext=android.app.ApplicationContext@43785778 pkg=com.foo.android
INFO/XPLORE4(465): appContext.getAppCtx()=com.foo.android.MyApplication@43783830
INFO/TestRunner(465): finished: test_exploreContext(test.foo.android.DatabaseTest)
请注意getApplicationContext() 暂时返回 null,然后开始返回 MyApp 的实例。我无法在该测试的不同运行中获得完全相同的结果(这就是我最终进行 4 次迭代、睡眠以及调用 getSharedPreferences() 来尝试使应用程序存在的原因)。
上面的 LogCat 消息块似乎最相关,但是该单个测试的单次运行的整个 LogCat 很有趣。 Android启动了4个AndroidRuntime;上面的块来自第四块。有趣的是,第三个运行时显示的消息表明它在进程 ID 447 中实例化了 MyApp 的不同实例:
INFO/TestRunner(447): started: test_exploreContext(test.foo.android.DatabaseTest)
INFO/MYAPP(447): this=com.foo.android.MyApplication@437809b0
INFO/MYAPP(447): getAppCtx()=com.foo.android.MyApplication@437809b0
INFO/TestRunner(447): finished: test_exploreContext(test.foo.android.DatabaseTest)
我假设 TestRunner(447) 消息来自报告进程 465 中其子级的父测试线程。不过,问题是:为什么 Android 让 AndroidTestCase 在其上下文正确连接到应用程序实例之前运行?
解决方法:如果我调用 getContext().getSharedPreferences("anyname", Context.MODE_PRIVATE).edit().clear(),我的一项测试似乎在大多数情况下都可以避免空值。 commit(); 首先,所以我就这么做。
顺便说一句:如果答案是“这是一个 Android 错误,为什么不将其归档;哎呀,为什么不修复它呢?”那么我愿意两者都做。我还没有迈出成为错误报告者或贡献者的一步——也许这是一个好时机。
UPDATE 2/13/2012: Accepted an answer, explained that this behavior is a bug, and noted that it appears to have disappeared on emulators better than v 1.6, which makes it a non-issue for most of us. The workaround is simply to loop/sleep until getContext().getApplicationContext() returns non-null.
END UPDATE
As per android.app.Application javadoc, I defined a singleton (called Database) that all of my activities access for state and persistent data, and Database.getDatabase(Context) gets the application context via Context.getApplicationContext(). This setup works as advertised when activities pass themselves to getDatabase(Context), but when I run a unit test from an AndroidTestCase, the getApplicationContext() call often returns null, though the longer the test, the more frequently it returns a non-null value.
The following code reproduces the null within an AndroidTestCase -- the singleton isn't necessary for the demonstration.
First, to log app-instantiation messages, in the app-under-test I defined MyApp and added it to the manifest.
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
Log.i("MYAPP", "this=" + this);
Log.i("MYAPP", "getAppCtx()=" + getApplicationContext());
}
}
Next, I defined a test case to report on AndroidTestCase.getContext() 4 times, separated by some sleeps and a getSharedPreferences() call:
public class DatabaseTest extends AndroidTestCase {
public void test_exploreContext() {
exploreContexts("XPLORE1");
getContext().getSharedPreferences("foo", Context.MODE_PRIVATE);
exploreContexts("XPLORE2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
exploreContexts("XPLORE3");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
exploreContexts("XPLORE4");
}
public void exploreContexts(String tag) {
Context testContext = getContext();
Log.i(tag, "testCtx=" + testContext +
" pkg=" + testContext.getApplicationInfo().packageName);
Log.i(tag, "testContext.getAppCtx()=" + testContext.getApplicationContext());
try {
Context appContext = testContext.createPackageContext("com.foo.android", 0);
ApplicationInfo appInfo = appContext.getApplicationInfo();
Log.i(tag, "appContext=" + appContext +
" pkg=" + appContext.getApplicationInfo().packageName);
Log.i(tag, "appContext.getAppCtx()=" + appContext.getApplicationContext());
} catch (NameNotFoundException e) {
Log.i(tag, "Can't get app context.");
}
}
}
And this is a chunk of the resulting logCat (1.6 emulator on SDK11 WinXP via Eclipse):
INFO/TestRunner(465): started: test_exploreContext(test.foo.android.DatabaseTest)
INFO/XPLORE1(465): testCtx=android.app.ApplicationContext@43757368 pkg=com.foo.android
INFO/XPLORE1(465): testContext.getAppCtx()=null
INFO/XPLORE1(465): appContext=android.app.ApplicationContext@437801e8 pkg=com.foo.android
INFO/XPLORE1(465): appContext.getAppCtx()=null
INFO/XPLORE2(465): testCtx=android.app.ApplicationContext@43757368 pkg=com.foo.android
INFO/XPLORE2(465): testContext.getAppCtx()=null
INFO/XPLORE2(465): appContext=android.app.ApplicationContext@43782820 pkg=com.foo.android
INFO/XPLORE2(465): appContext.getAppCtx()=null
INFO/MYAPP(465): this=com.foo.android.MyApplication@43783830
INFO/MYAPP(465): getAppCtx()=com.foo.android.MyApplication@43783830
INFO/XPLORE3(465): testCtx=android.app.ApplicationContext@43757368 pkg=com.foo.android
INFO/XPLORE3(465): testContext.getAppCtx()=com.foo.android.MyApplication@43783830
INFO/XPLORE3(465): appContext=android.app.ApplicationContext@43784768 pkg=com.foo.android
INFO/XPLORE3(465): appContext.getAppCtx()=com.foo.android.MyApplication@43783830
INFO/XPLORE4(465): testCtx=android.app.ApplicationContext@43757368 pkg=com.foo.android
INFO/XPLORE4(465): testContext.getAppCtx()=com.foo.android.MyApplication@43783830
INFO/XPLORE4(465): appContext=android.app.ApplicationContext@43785778 pkg=com.foo.android
INFO/XPLORE4(465): appContext.getAppCtx()=com.foo.android.MyApplication@43783830
INFO/TestRunner(465): finished: test_exploreContext(test.foo.android.DatabaseTest)
Notice that getApplicationContext() returned null for a while, then started returning an instance of MyApp. I have not been able to get the exact same results in different runs of this test (that's how I ended up at 4 iterations, sleeps, and that call to getSharedPreferences() to try to goose the app into existence).
The chunk of LogCat messages above seemed most relevant, but the entire LogCat for that single run of that single test was interesting. Android started 4 AndroidRuntimes; the chunk above was from the 4th. Interestingly, the 3rd runtime displayed messages indicating that it instantiated a different instance of MyApp in process ID 447:
INFO/TestRunner(447): started: test_exploreContext(test.foo.android.DatabaseTest)
INFO/MYAPP(447): this=com.foo.android.MyApplication@437809b0
INFO/MYAPP(447): getAppCtx()=com.foo.android.MyApplication@437809b0
INFO/TestRunner(447): finished: test_exploreContext(test.foo.android.DatabaseTest)
I assume that the TestRunner(447) messages are from a parent test thread reporting on its children in process 465. Still, the question is: why does Android let an AndroidTestCase run before its context is properly hooked up to an Application instance?
Workaround: One of my tests seemed to avoid nulls most of the time if I called getContext().getSharedPreferences("anyname", Context.MODE_PRIVATE).edit().clear().commit();
first, so I'm going with that.
BTW: If the answer is "it's an Android bug, why don't you file it; heck, why don't you fix it?" then I'd be willing to do both. I haven't taken the step of being a bug-filer or contributor yet -- maybe this is a good time.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
Instrumentation 在与主应用程序线程不同的单独线程中运行,因此它可以在不阻塞或中断(或被阻塞)主线程的情况下执行。如果您需要与主线程同步,请使用例如: Instrumentation.waitForIdleSync()
特别是,Application 对象以及所有其他顶级类(如 Activity)都是由主线程初始化的。您的检测线程在初始化的同时运行。如果您正在接触任何这些对象并且没有实现自己的线程安全措施,您可能应该在主线程上运行这样的代码,例如通过: Instrumentation.runOnMainSync(java.lang.Runnable)
Instrumentation runs in a separate thread from the main app thread, so that it can execute without blocking or disrupting (or being blocked by) the main thread. If you need to synchronize with the main thread, use for example: Instrumentation.waitForIdleSync()
In particular, the Application object as well as all other top-level classes like Activity are initialized by the main thread. Your instrumentation thread is running at the same time those are initializing. This if you are touching any of those objects and are not implementing your own measures of thread safety, you should probably have such code run on the main thread such as via: Instrumentation.runOnMainSync(java.lang.Runnable)
正如问题和黛安娜的回答(@hackbod)中提到的,仪器在单独的线程上运行。
AndroidTestCase
要么存在实现缺陷(缺少同步),要么没有正确记录。不幸的是,无法从这个特定的测试用例类调用Instrumentation.waitForIdleSync(),因为无法从中访问Instrumentation。该子类可用于添加轮询 getApplicationContext() 的同步,直到返回非空值:
轮询和睡眠持续时间基于经验,可以根据需要进行调整。
As mentioned in the question and Dianne's answer (@hackbod), the Instrumentation runs on a separate thread.
AndroidTestCase
either has an implementation defect (missing synchronization) or it is not documented correctly. Unfortunately, there is no way to callInstrumentation.waitForIdleSync()
from this particular test case class because the Instrumentation is not accessible from it.This subclass can be used to add synchronization that polls getApplicationContext() until it returns a non-null value:
The polling and sleep durations are based on experience and can be tuned if necessary.