In-app-billing 的 Play Store 缓存为何很快失效?
结论
不要把 Play Store 的购买缓存作为 premium 权限唯一的离线凭据。
In-app Billing v3 的 queryInventory 通常会通过 Google Play 返回本地缓存的购买信息,但 Google 没有承诺缓存的固定有效期,也不保证它在所有离线状态下都可用。如果 Play Store 数据被清理、账号状态发生变化、Google Play 服务异常、缓存刷新失败,或者 Billing Service 暂时不可用,查询都可能失败。
需要注意的是,查询失败不代表用户没有购买。只有查询成功,而且结果明确表明用户未拥有该商品时,才可以考虑撤销本地 premium 状态。断网或服务异常时直接降级,往往正是问题的来源。
为什么离线几小时后会失效
queryInventory 依赖设备上的 Google Play Store 及其 Billing Service。即使购买记录曾经缓存到本地,应用也无法控制缓存何时刷新、失效或被删除。以下情况都可能导致离线查询拿不到有效结果:
- Play Store 尚未完成初始化,或者 Billing Service 连接失败。
- 系统或用户清理了 Play Store、Google Play 服务或应用的数据。
- 当前 Google 账号已切换、退出或需要重新验证。
- 查询超时,或者服务暂时不可用。
- 购买记录需要联网同步,而本地数据不足。
- 应用没有区分”查询异常”、“空结果”和”明确未购买”。
因此,不能假定缓存一定会保留数小时、数天或更久。Billing v3 也没有公开的缓存有效期参数可供应用设置或依赖。
推荐的处理方式
1. 成功购买后保存本地 entitlement
购买完成并通过签名验证后,在本地保存用户已解锁 premium 的状态。应用下次启动时先读取该状态,让已经购买的用户可以离线使用。
private static final String PREFS_NAME = "entitlements";
private static final String KEY_PREMIUM = "premium_enabled";
private void setPremiumEnabled(Context context, boolean enabled) {
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.edit()
.putBoolean(KEY_PREMIUM, enabled)
.apply();
}
private boolean isPremiumEnabled(Context context) {
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.getBoolean(KEY_PREMIUM, false);
}
应用启动时,可以先根据本地状态开放相应功能,再在后台尝试通过 Google Play 刷新购买信息:
boolean premiumEnabled = isPremiumEnabled(this);
applyPremiumState(premiumEnabled);
refreshPurchasesFromGooglePlay();
2. 区分”未购买”和”无法查询”
查询结果至少要分为三种状态:
- 查询成功,并确认用户拥有该商品。
- 查询成功,并确认用户未拥有该商品。
- 查询失败,当前购买状态未知。
第三种状态不应触发降级:
private void handleInventoryResult(IabResult result, Inventory inventory) {
if (result.isFailure()) {
// 临时无法确认,继续沿用最后一次已验证的本地状态。
applyPremiumState(isPremiumEnabled(this));
return;
}
Purchase purchase = inventory.getPurchase("premium_upgrade");
boolean ownsPremium = purchase != null
&& verifyDeveloperPayloadOrOtherChecks(purchase);
setPremiumEnabled(this, ownsPremium);
applyPremiumState(ownsPremium);
}
这里的 verifyDeveloperPayloadOrOtherChecks 只是示意。实际校验方式要与应用现有的购买流程和安全设计一致,不能在所有场景下都只用 purchase != null 完成安全校验。
3. 只在得到可信结果后更新状态
可以采用”最后一次可信状态”策略:
查询成功且验证通过 -> 保存 premium
查询成功且明确未拥有 -> 根据业务规则撤销 premium
断网、超时、服务不可用 -> 保留原状态,稍后重试
即使查询成功但列表为空,也要谨慎处理。例如,Play Store 刚刚切换账号或本地状态还没有完成同步时,立即永久撤销权限可能误伤已经购买的用户。可以设置宽限期,等设备联网后再次确认。
4. 高价值权益使用服务端验证
SharedPreferences 很容易被 root、备份恢复或应用篡改工具修改,不适合作为高价值权益唯一的安全依据。安全要求较高时,可以:
- 将购买 token 发送到自己的服务端。
- 由服务端调用 Google Play Developer API 验证购买。
- 保存最近一次成功验证的 entitlement 和验证时间。
- 离线时提供一定的宽限期。
- 恢复联网后重新验证,并同步退款、取消或撤销状态。
服务端验证可以提高防篡改能力,但仍然要有离线策略。否则一旦网络出现故障,合法用户还是可能无法使用已经购买的功能。
一次性商品和订阅需要区别处理
如果 premium 是永久解锁的一次性非消耗型商品,通常可以长期保存最近一次确认的 entitlement。除非检测到退款、撤销或账号变化,否则不要因为一次临时查询失败就立即降级。
如果 premium 来自订阅,就不能无限期沿用旧状态,因为订阅可能到期、取消或被撤销。可以保存最近一次验证结果,并设置合理的离线宽限期。宽限期多长取决于业务和风险判断,并不是 Billing v3 提供的固定缓存期限。
永久解锁商品也不应被错误地 consume。商品一旦被消费,后续购买查询通常不会再将它作为当前拥有的非消耗型商品返回。
注意事项
- 不要通过清除 Play Store 数据来模拟普通断网。两种操作对购买缓存的影响不同。
- 记录 Billing Service 的连接结果、
queryInventory返回码和异常信息,以便判断结果究竟是”没有购买”还是”查询失败”。 - 校验购买数据及签名,不要只相信本地保存的布尔值。
- 不要在客户端中硬编码可用于伪造验证结果的敏感服务端凭据。
- Billing v3 已经是旧接口。仍在维护的应用应计划迁移到当前受支持的 Google Play Billing Library,但迁移不能替代 entitlement 持久化和离线容错设计。
Play Store 缓存可以减少查询成本,但它不是具有固定有效期保证的离线授权机制。可靠的做法是保存最近一次可信的购买状态,并将临时查询失败视为”未知”,而不是”未购买”。
备注:内容仅供参考。
