更新2012年2月13日:接受了答案,解释说这种行为是一个错误,并指出它似乎已经比v 1.6更好地消失在模拟器上,这使得它对我们大多数人来说都不是问题。解决方法只是循环/休眠,直到getContext()。getApplicationContext()返回非null。 END UPDATE
根据android.app.Application javadoc,我定义了一个单例(称为数据库),我的所有活动都访问状态和持久数据,而Database.getDatabase(Context)通过Context.getApplicationContext()获取应用程序上下文。当活动将自己传递给getDatabase(Context)时,此设置的工作方式与广告一致,但是当我从AndroidTestCase运行单元测试时,getApplicationContext()调用通常会返回null,尽管测试越长,返回非null的频率越高值。
以下代码在AndroidTestCase中重现null - 演示不需要单例。
首先,要记录app-instantiation消息,在测试中的应用程序中我定义了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个AndroidRuntimes;上面的块是从第4个。有趣的是,第3个运行时显示消息,指示它在进程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在其上下文正确连接到Application实例之前让AndroidTestCase运行?
解决方法:如果我先调用getContext().getSharedPreferences("anyname", Context.MODE_PRIVATE).edit().clear().commit();
,我的其中一个测试似乎大部分时间都会避免空值,所以我就是这样。
BTW :如果答案是“这是一个Android错误,你为什么不提交它?哎呀,你为什么不修复它?”然后我愿意做两件事。我还没有采取过成为bug-filer或贡献者的步骤 - 也许现在是个好时机。
答案 0 :(得分:12)
Instrumentation在与主应用程序线程的单独线程中运行,因此它可以在不阻塞或中断(或被主线程阻塞)的情况下执行。如果需要与主线程同步,请使用例如:Instrumentation.waitForIdleSync()
特别是,Application对象以及Activity之类的所有其他顶级类都由主线程初始化。您的检测线程正在初始化时运行。如果您触摸任何这些对象并且没有实现自己的线程安全措施,则应该在主线程上运行此类代码,例如:Instrumentation.runOnMainSync(java.lang.Runnable)
答案 1 :(得分:4)
正如问题和Dianne的答案(@hackbod)中所提到的,Instrumentation在一个单独的线程上运行。 AndroidTestCase
要么有实现缺陷(缺少同步),要么没有正确记录。遗憾的是,无法从此特定测试用例类中调用Instrumentation.waitForIdleSync()
,因为无法从中访问Instrumentation。
此子类可用于添加轮询getApplicationContext()的同步,直到它返回非null值:
public class MyAndroidTestCase extends AndroidTestCase {
@Override
public void setContext(Context context) {
super.setContext(context);
long endTime = SystemClock.elapsedRealtime() + TimeUnit.SECONDS.toMillis(2);
while (null == context.getApplicationContext()) {
if (SystemClock.elapsedRealtime() >= endTime) {
fail();
}
SystemClock.sleep(16);
}
}
}
轮询和睡眠持续时间基于经验,如有必要可以调整。