Dungeons billing service 调用 bindService 出现 NullPointerExcept
结论
这个 NullPointerException 多半不是因为 Intent、ServiceConnection 或 BIND_AUTO_CREATE 为 null,而是因为调用 bindService() 的 Service 实例没有经过 Android 框架的正确初始化。
Service 继承自 ContextWrapper。如果通过 new 手动创建 Service,或者在它尚未附加基础 Context 时调用 bindService(),ContextWrapper 内部的 mBase 就会是 null。此时执行类似下面的内部调用便会抛出异常:
return mBase.bindService(service, conn, flags);
因此,即使传给 bindService() 的三个参数看起来都不为 null,堆栈仍会指向 ContextWrapper.java。
为什么会出现这种情况
Android 的 Activity、Service 等组件不能像普通 Java 对象一样使用。下面的代码会创建一个尚未附加 Context 的 Service:
BillingService service = new MyBillingService();
service.checkBillingSupported();
虽然对象本身不是 null,但它并非由 Android 根据 manifest 创建,Service 的生命周期和基础 Context 都没有完成初始化。如果随后在该对象中直接调用:
bindService(...);
内部的 mBase 可能仍为 null,进而导致崩溃。
旧版 Dungeons 示例曾采用一种比较特殊的写法:手动创建 BillingService,再显式为它设置 Context。移植代码时如果漏掉了类似 setContext() 的步骤,或者自定义的继承结构没有保留这段初始化,就可能出现这个异常。
推荐解决方式
方案一:使用有效的 Context 调用 bindService()
不要把手动创建的 Service 对象本身当作 Context。可以将有效的应用 Context 传给 billing helper:
public final class BillingManager implements ServiceConnection {
private final Context context;
public BillingManager(Context context) {
this.context = context.getApplicationContext();
}
public boolean bindToBillingService() {
Intent intent = new Intent(Consts.MARKET_BILLING_SERVICE_ACTION);
return context.bindService(
intent,
this,
Context.BIND_AUTO_CREATE);
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// 保存远程 billing service 接口
}
@Override
public void onServiceDisconnected(ComponentName name) {
// 清理远程接口引用
}
}
在 Activity 中使用:
public class MainActivity extends Activity {
private BillingManager billingManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
billingManager = new BillingManager(this);
billingManager.bindToBillingService();
}
}
这种设计也更适合 library project。公共逻辑可以放在普通的 Java helper 中,不必把辅助对象设计成 Android Service。
方案二:让 Android 创建真正的 Service
如果这个类确实需要作为应用的 Service 运行,应在应用模块中提供具体实现,并在 manifest 中声明:
public class AppBillingService extends BaseBillingService {
// 每个应用版本可以在这里提供自己的配置
}
<service
android:name=".AppBillingService"
android:exported="false" />
之后通过 startService() 或 bindService() 请求该组件,让 Android 负责创建实例。不要手动构造:
AppBillingService service = new AppBillingService();
如果需要从 Activity 调用它的方法,应绑定本地 Service,并通过返回的 IBinder 通信,而不是保存一个手动构造的 Service 实例。
兼容旧版 Dungeons 代码
如果暂时必须保留旧示例的结构,需要确认用于初始化 Context 的代码没有在重构时丢失。例如,旧式实现可能类似:
public void setContext(Context context) {
attachBaseContext(context);
}
调用顺序必须是:
billingService = new ConcreteBillingService();
billingService.setContext(this);
billingService.checkBillingSupported();
不过,直接调用 attachBaseContext() 是一种对 Android 组件初始化机制的特殊用法,容易受到继承关系、生命周期和配置变化的影响。更稳妥的方式仍是让 helper 保存 context.getApplicationContext(),再通过这个 Context 调用 bindService()。
其他几个因素是否相关
library project 和 abstract service
将公共 Service 声明为 abstract,本身不会直接导致这次异常。但需要满足两个条件:
- manifest 中声明的是可实例化的具体子类,而不是 abstract 基类;
- 具体 Service 由 Android 创建,而不是通过
new构造。
如果问题出在继承关系上,通常会在组件实例化、manifest 解析或启动 Service 时出现其他异常,不会典型地表现为 ContextWrapper.mBase 为 null。
在 Activity.onCreate() 中调用
执行到 Activity.onCreate() 时,通常已经有可用的 Context,因此从这里发起绑定并不算太早。需要确认的是,调用 bindService() 的究竟是当前 Activity,还是一个手动创建且尚未初始化的 Service 对象。
下面的调用通常不会遇到这个问题:
bindService(intent, connection, Context.BIND_AUTO_CREATE);
下面这种写法则可能出错:
BillingService service = new BillingServiceSubclass();
service.checkBillingSupported();
所以,问题的关键不在于调用是否发生在 onCreate(),而在于调用者使用的 Context 是否有效。
Android 3.1
Android 3.1 不太可能直接引发这个特定的 NullPointerException。如果系统找不到匹配的 billing service,bindService() 通常会返回 false。权限、组件导出或 Intent 限制等问题,一般也会表现为其他异常或绑定失败,而不是 ContextWrapper 内部的基础 Context 为 null。
Eclipse 提示 customIntent 为 null
customIntent 很可能是调试器显示的框架内部局部变量,也可能是经过优化后无法可靠查看的变量。它并不是这里传入的 new Intent(...) 的变量名,不能只凭它显示为 null 就认定 Intent 有问题。
可以直接验证外部 Intent:
Intent intent = new Intent(Consts.MARKET_BILLING_SERVICE_ACTION);
if (intent == null) {
throw new IllegalStateException("Intent is null");
}
实际上,只要 new Intent(...) 成功返回,结果就不可能是 null。更应该检查的是当前 Context:
Log.d("Billing", "context=" + context);
Log.d("Billing", "applicationContext=" + context.getApplicationContext());
如果堆栈停在 ContextWrapper.bindService() 调用 mBase 的位置,应先检查 Service 是否通过 new 创建、Context 初始化是否遗漏,以及能否改用 Activity 或 application Context 完成绑定。
备注:内容仅供参考。
