Android In-app Billing Library 与 API 如何选择
结论
新应用应使用 Google Play Billing Library 提供的 BillingClient。IInAppBillingService 是旧式 AIDL 接口,只适合维护暂时无法迁移的遗留代码,不适合新项目。两者访问的都是 Google Play Billing 服务,并非两套独立的订阅系统。
getPurchases() 返回空列表,只表示 Google Play 当前没有向该设备账号返回有效的已购订阅,不能据此完整、可靠地判断订阅状态:
- 已过期且未处于宽限期等有效状态的订阅,通常不会出现在
getPurchases()的结果中。因此,purchaseDataList通常为空,或至少不包含该订阅。 - Deferred Billing 会延长订阅的到期时间。延期期间订阅仍然有效,客户端查询结果通常和普通有效订阅相同。
- 客户端返回值通常不会说明订阅是否经过延期,也无法可靠区分正常续订、宽限期、账号保留、取消后尚未到期等状态。
- 如需准确判断到期时间、付款失败和延期情况,应将
purchaseToken发送到后端,由后端通过 Google Play Developer API 验证。
应该选择哪个接口
Google Play Billing Library
它适用于绝大多数应用,可以用于:
- 接入新应用的订阅和一次性商品;
- 查询当前购买;
- 发起购买流程;
- 处理待处理交易;
- 确认购买;
- 适配 Google Play Billing 的新要求。
Billing Library 已经封装服务连接、线程调度、响应处理和兼容性细节,维护成本明显低于直接调用 AIDL。
IInAppBillingService
这是早期 Google Play Billing 使用的底层 AIDL 接口。只有在维护旧项目且短期内无法迁移时,才可能继续使用。直接调用该接口时,应用需要自行处理:
- 服务绑定、断开和重连;
Bundle字段和响应码;- continuation token;
- 购买签名校验;
- 不同 Billing API 版本之间的差异。
即使现有 AIDL 代码仍能运行,也应安排迁移到当前受支持的 Billing Library 版本。支持版本和迁移期限可能发生变化,接入时应以 Google Play 官方文档和 Play Console 的要求为准。
过期订阅是否会返回空数组
旧接口的调用方式如下:
Bundle ownedSubscribedItems =
mService.getPurchases(3, getPackageName(), "subs", null);
int responseCode = ownedSubscribedItems.getInt("RESPONSE_CODE");
ArrayList<String> purchaseDataList =
ownedSubscribedItems.getStringArrayList("INAPP_PURCHASE_DATA_LIST");
订阅真正失效后,Google Play 通常不会继续将它作为当前拥有的订阅返回。该订阅不会出现在 purchaseDataList 中;如果账号没有其他有效订阅,列表通常为空。
代码不能只判断列表是否为 null 或空,还要先检查 RESPONSE_CODE。服务错误、账号问题或连接异常也可能导致应用无法取得购买数据,不能把查询失败直接当成「未购买」。
if (responseCode != 0) {
// 查询失败或服务不可用。
// 此时不能直接撤销用户权限,应重试或使用后端保存的状态。
return;
}
if (purchaseDataList == null || purchaseDataList.isEmpty()) {
// 当前查询结果中没有有效订阅。
// 对高价值权限,仍应结合后端验证结果处理。
} else {
// 解析 purchaseData,并把 purchaseToken 交给后端验证。
}
代码还要处理 INAPP_CONTINUATION_TOKEN。收到 continuation token 后,需要继续调用 getPurchases(),否则购买项目较多时可能遗漏结果。
付款失败不代表订阅会立即过期
续订扣款失败后,订阅是否立即失效,要看它在 Google Play 中的实际状态,包括是否配置并进入宽限期或账号保留期。
常见情况有:
- 宽限期内,用户可能仍享有订阅权益;
- 账号保留期间,客户端查询结果可能不再将订阅显示为正常有效;
- 恢复扣款后,订阅可能重新变为有效;
- 扣款最终失败且订阅到期后,它将不再作为当前购买返回。
因此,不能认为续费失败后 getPurchases() 一定会立即返回空列表。客户端查询适合恢复当前购买,但不能完整反映付款生命周期。
Deferred Billing 会返回什么
如果这里的 Deferred Billing 是指通过 Google Play Developer API 延后订阅的下一次扣款日期,那么延期不会暂停用户权益,只会将订阅到期时间向后延长。
延期生效期间:
- 订阅仍然有效;
- 客户端通常仍能查询到该订阅;
- 返回的购买记录通常没有可用于可靠区分延期订阅和普通有效订阅的特殊标志;
- 仅凭
getPurchases()或BillingClient.queryPurchasesAsync()无法判断订阅是否经过延期。
如需识别 Deferred Billing,后端应保存执行延期操作时的业务记录,并重新查询 Google Play Developer API 返回的订阅资源,检查服务端提供的到期时间和相关状态。不要通过客户端购买 JSON 推断延期状态。
如果这里的「Deferred Billing」实际是指付款失败后的宽限期或账号保留期,那么它与主动延期并非同一概念,也需要通过服务端订阅状态加以区分。
使用 Billing Library 查询当前订阅
现代客户端可以使用 queryPurchasesAsync() 恢复当前订阅:
billingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
.build(),
(billingResult, purchases) -> {
if (billingResult.getResponseCode()
!= BillingClient.BillingResponseCode.OK) {
// 查询失败,不要直接判定为未订阅。
return;
}
if (purchases.isEmpty()) {
// 客户端没有查询到当前订阅。
// 是否撤销权益仍应以服务端验证结果为准。
return;
}
for (Purchase purchase : purchases) {
if (purchase.getPurchaseState()
== Purchase.PurchaseState.PURCHASED) {
String purchaseToken = purchase.getPurchaseToken();
// 将 purchaseToken 和 productId 发送到应用后端验证。
// 不要仅凭客户端结果直接授予长期权益。
} else if (purchase.getPurchaseState()
== Purchase.PurchaseState.PENDING) {
// 交易尚未完成,不应授予正式权益。
}
}
});
这个示例只展示状态查询。实际接入时,还要根据所用的 Billing Library 版本处理购买确认、商品信息查询和购买更新回调。
推荐的订阅判断流程
- 客户端通过
BillingClient查询购买并取得purchaseToken。 - 客户端将
purchaseToken、商品标识和用户标识发送到应用后端。 - 后端调用 Google Play Developer API 查询订阅状态。
- 后端根据到期时间、订阅状态、取消信息和付款状态决定是否授予权益。
- 后端保存状态,并通过 Google Play 的实时开发者通知处理续订、取消、恢复、撤销和到期事件。
- 客户端从应用后端读取最终权益状态,不要只在 Splash Screen 中根据空列表作出判断。
Splash Screen 可以触发一次状态刷新,但不应阻塞应用启动,也不应在网络或 Billing 服务暂时失败时立即撤销权益。可以先使用后端最近一次确认的状态,再进行异步刷新。只有服务端明确确认订阅已经失效,才将状态更新为未订阅。
注意事项
- 空列表不等于付款失败,只表示本次查询没有返回当前拥有的订阅。
- 查询错误不能按未购买处理。
- 已取消但尚未到期的订阅通常仍然有效,不能因为用户关闭自动续订就立即撤销权益。
Purchase.getPurchaseTime()不是订阅的最终到期时间。- 客户端购买数据可能被篡改,不适合作为高价值权益的唯一判断依据。
- Deferred Billing 没有可靠的客户端专用标记,应由后端记录并验证。
- 旧 AIDL 接口中的 API 版本参数
3不是 Billing Library 的版本号,迁移时不能进行简单的一一对应。
备注:内容仅供参考。
