PAYATHON 2026

InAppBilling activity 应在何时调用 finish()?

数据小许

明确结论

不要在 requestPurchase() 返回后马上调用 finish()。这个返回值只能说明购买请求已经提交,不能说明购买最终是成功、失败还是取消。

建议按以下方式处理:

  1. 让 InAppBillingActivity 保持存活,等待购买结果回调。
  2. 在成功、取消或不可恢复的失败回调中调用 setResult() 和 finish()。
  3. 无论 Activity 以何种方式结束,都要注销 PurchaseObserver,并解除与 Billing Service 的绑定。
  4. 不要只依赖 Activity 接收购买结果。Activity 可能因屏幕旋转、后台回收等原因被销毁,最终订单状态应交给生命周期更长的组件处理,或通过恢复查询确认。

日志中的 ServiceConnectionLeaked 不是异步请求直接造成的。它表示 Activity 已经销毁,但绑定的 Service 没有执行 unbindService()。

为什么立即调用 finish() 会出问题

调用 finish() 后,Activity 会进入销毁流程。如果 BillingService 使用当前 Activity 作为 Context 调用了 bindService(),就必须在 Activity 销毁前解除绑定。

当前代码中有:

mBillingService.setContext(this);

这意味着 Service 连接与 InAppBillingActivity 的生命周期相关。立即调用 finish() 后,系统发现 Activity 已经销毁,而对应的 ServiceConnection 仍然存在,因此抛出:

android.app.ServiceConnectionLeaked

旧版 In-App Billing 的 requestPurchase() 也是异步流程。它的返回值最多只能说明请求是否被接受或成功发起,不能代表用户已经完成购买。

在购买结果回调中结束 Activity

购买观察者应当把最终状态通知给 Activity,再由 Activity 结束自身。下面是一个结构示例。具体的回调签名需要与项目所使用的旧版 In-App Billing 示例代码保持一致:

public class InAppBillingActivity extends Activity {

    private Handler mHandler;
    private BillingService mBillingService;
    private InAppBillingPurchaseObserver mPurchaseObserver;
    private boolean mCleanupDone;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mHandler = new Handler();

        mPurchaseObserver =
                new InAppBillingPurchaseObserver(mHandler) {

            @Override
            public void onPurchaseCompleted(String productId) {
                Intent result = new Intent();
                result.putExtra("product_id", productId);

                setResult(RESULT_OK, result);
                finish();
            }

            @Override
            public void onPurchaseCancelled() {
                setResult(RESULT_CANCELED);
                finish();
            }

            @Override
            public void onPurchaseFailed(int responseCode) {
                Intent result = new Intent();
                result.putExtra("response_code", responseCode);

                setResult(RESULT_CANCELED, result);
                finish();
            }
        };

        mBillingService = new BillingService();
        mBillingService.setContext(this);

        ResponseHandler.register(mPurchaseObserver);

        boolean supported = mBillingService.checkBillingSupported();

        if (supported) {
            boolean submitted = mBillingService.requestPurchase(
                    "android.test.purchased",
                    "10"
            );

            if (!submitted) {
                setResult(RESULT_CANCELED);
                finish();
            }
        } else {
            setResult(RESULT_CANCELED);
            finish();
        }
    }

    @Override
    protected void onDestroy() {
        cleanupBilling();
        super.onDestroy();
    }

    private void cleanupBilling() {
        if (mCleanupDone) {
            return;
        }

        mCleanupDone = true;

        if (mPurchaseObserver != null) {
            ResponseHandler.unregister(mPurchaseObserver);
        }

        if (mBillingService != null) {
            mBillingService.unbind();
        }
    }
}

这里的 onPurchaseCompleted()、onPurchaseCancelled() 和 onPurchaseFailed() 只是用来说明处理流程,未必与项目中的实际接口完全相同。应把 finish() 放到现有 PurchaseObserver 所提供的最终状态回调中。

正确解除 Service 绑定

早期 Google In-App Billing 示例中的 BillingService 通常会提供类似方法:

public void unbind() {
    try {
        unbindService(this);
    } catch (IllegalArgumentException e) {
        // Service 当前可能没有绑定。
    }
}

更稳妥的做法是记录绑定状态,避免重复解绑:

private boolean mBound;

private void bindBillingService() {
    mBound = bindService(
            billingIntent,
            mServiceConnection,
            Context.BIND_AUTO_CREATE
    );
}

public void unbind() {
    if (mBound) {
        unbindService(mServiceConnection);
        mBound = false;
    }
}

基本要求是,调用 bindService() 的一方必须在生命周期结束时配对调用 unbindService()。

清理操作至少要放在 onDestroy() 中。如果注册和绑定发生在 onStart(),也可以在 onStop() 中注销和解绑,但两边必须使用一致的生命周期边界。

从主 Activity 获取购买结果

购买 Activity 需要返回结果,因此旧代码可以使用 startActivityForResult(),而不是普通的 startActivity():

private static final int REQUEST_PURCHASE = 1001;

Intent intent = new Intent(this, InAppBillingActivity.class);
startActivityForResult(intent, REQUEST_PURCHASE);

然后在主 Activity 中接收结果:

@Override
protected void onActivityResult(
        int requestCode,
        int resultCode,
        Intent data
) {
    super.onActivityResult(requestCode, resultCode, data);

    if (requestCode != REQUEST_PURCHASE) {
        return;
    }

    if (resultCode == RESULT_OK) {
        String productId = data != null
                ? data.getStringExtra("product_id")
                : null;

        // 更新界面,但仍应根据已确认的订单状态授予权益。
    } else {
        // 用户取消、请求失败或购买未完成。
    }
}

现代 Android 项目应优先使用 Activity Result API。不过,这段问题代码来自旧版 In-App Billing,使用 startActivityForResult() 更容易与现有结构配合。

不要把“请求响应”当成“购买成功”

旧版 Billing API 通常有多个层次的回调:

  • 请求已被市场应用接受;
  • 购买界面已经启动;
  • 用户完成购买、取消购买或购买失败;
  • 购买状态通过广播或恢复交易查询返回。

例如,名为 onRequestPurchaseResponse() 的回调可能只表示 REQUEST_PURCHASE 请求得到了响应,并不代表交易已经完成。只有收到明确的购买状态,并完成必要的订单验证或确认后,才能授予商品权益。

因此,不能在收到第一个响应回调时就直接执行:

finish();

应先确认该回调表示的是“请求提交结果”还是“最终购买结果”。

Activity 被提前销毁时怎么办

Activity 不一定能存活到异步结果返回。以下情况都可能让它提前销毁:

  • 用户按返回键;
  • 屏幕旋转或配置发生变化;
  • 应用进入后台;
  • 系统回收进程;
  • 市场购买界面覆盖当前 Activity。

因此,不要只把购买状态保存在 Activity 字段中。更可靠的设计是:

  • Activity 负责发起购买和显示状态;
  • 广播接收器、Repository、Service 或 BillingClient 连接管理器负责接收并保存订单状态;
  • Activity 恢复后重新查询已有购买;
  • 确认订单真实存在后再发放权益;
  • 清理 UI 观察者时,不要丢弃已经写入持久层的购买结果。

如果用户主动返回,可以结束 Activity,但要先注销观察者并解除 Service 绑定。用户再次进入页面时,应通过恢复购买或查询购买记录来确认最终状态。

还需要注意的几个问题

不要只根据 checkBillingSupported() 的同步返回值设计整个流程。某些旧版封装会在内部绑定 Service 并把请求放入队列,实际行为取决于项目使用的 BillingService 实现。

调用 ResponseHandler.register() 后,必须有对应的注销操作:

ResponseHandler.unregister(mPurchaseObserver);

否则,即使没有发生 Service 泄漏,也可能因为静态引用长期持有 Activity 而造成内存泄漏。

还要避免重复发起购买。配置变化会让 onCreate() 再次执行,当前代码可能因此再次调用 requestPurchase()。可以保存“购买是否已启动”的状态,也可以把购买流程移到能够跨配置变化保存状态的组件中。

这套 BillingService、PurchaseObserver 和 ResponseHandler 结构属于早期 Google In-App Billing API。新项目应使用当前版本的 Google Play Billing Library,通过 PurchasesUpdatedListener、购买查询和确认流程管理交易。无论使用哪一代 API,都应等待最终交易状态,让业务状态独立于 Activity 生命周期,并在 Activity 销毁时完整释放所有注册和连接。

备注:内容仅供参考。