PAYATHON 2026

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 完成绑定。

备注:内容仅供参考。