PAYATHON 2026

Android 调用支付宝支付时 APayEntranceActivity 崩溃

支付小周

结论

崩溃发生在支付宝 SDK 内部。APayEntranceActivity 执行 finish() 时使用了空的 Map key,最终调用 ConcurrentHashMap.remove(null) 并抛出异常。

ConcurrentHashMap 不允许 key 为 null。这通常不是业务代码直接调用 remove() 导致的,而是 APayEntranceActivity 启动时没有拿到 SDK 需要的标识参数。可能的原因包括:

  • 业务代码直接或间接启动了 APayEntranceActivity。
  • 支付流程被重复或并发调用,或者 Activity 状态异常。
  • 支付宝 SDK 版本较旧,内部生命周期处理存在兼容性问题。
  • 工程中存在多个支付宝 SDK,造成类或 Manifest 配置冲突。
  • App 进程被系统回收后恢复了 APayEntranceActivity,但 SDK 保存在内存中的上下文已经丢失。
  • 加固、热修复、代码混淆或 Manifest 修改破坏了 SDK 的参数传递。

只看这段堆栈还无法判断具体属于哪种情况,但可以确定,SDK 没有妥善处理空标识。

不要直接启动 APayEntranceActivity

APayEntranceActivity 是支付宝 SDK 内部使用的跳转页,业务代码不应通过 Intent 启动它。例如,不要这样写:

Intent intent = new Intent();
intent.setClassName(
        getPackageName(),
        "com.alipay.sdk.app.APayEntranceActivity"
);
startActivity(intent);

应通过 SDK 提供的 PayTask 发起支付,由 SDK 创建跳转所需的参数:

ExecutorService executor = Executors.newSingleThreadExecutor();

executor.execute(() -> {
    PayTask payTask = new PayTask(activity);
    Map<String, String> result = payTask.payV2(orderInfo, true);

    activity.runOnUiThread(() -> {
        String resultStatus = result.get("resultStatus");
        String memo = result.get("memo");

        // 更新页面状态。
        // 最终支付结果应以服务端异步通知或服务端主动查询为准。
    });
});

orderInfo 应由服务端生成并签名。不要把商户私钥保存在客户端,也不要在客户端拼接包含私钥签名逻辑的订单。

建议的排查步骤

1. 检查 SDK 接入方式

确认项目只使用一种方式引入支付宝 SDK,不要同时保留 Maven 依赖、本地 AAR 和旧版 JAR。

重点检查:

  • app/libs 中是否残留旧版支付宝 JAR 或 AAR。
  • Gradle 依赖是否引入了多个不同版本。
  • 其他支付组件是否传递依赖了另一个支付宝 SDK。
  • 最终 APK 中是否包含重复的 com.alipay.sdk 类。

不要只替换新的 AAR,却继续保留旧 JAR。这样很容易造成代码版本与资源、Manifest 不一致。

2. 升级到当前官方推荐版本

如果项目使用的是旧版 SDK,应优先按照支付宝开放平台当前的接入文档升级。异常发生在 SDK 内部。如果问题来自某个版本的生命周期缺陷,业务层通常很难可靠规避。

升级时一并检查:

  • Gradle 依赖配置。
  • Manifest 合并结果。
  • 混淆配置。
  • ABI 和打包配置。
  • 旧 SDK 文件是否已彻底移除。

不能仅凭 APayEntranceActivity.java:59 判断 SDK 的具体版本,因为不同构建版本的代码行号可能不同。

3. 检查最终合并后的 Manifest

不要只检查源码中的 AndroidManifest.xml,还要查看构建生成的 merged manifest,确认 APayEntranceActivity 没有被重复声明,也没有被第三方配置覆盖。

尤其要检查是否通过 tools:replace 修改了 SDK Activity 的以下属性:

  • android:launchMode
  • android:taskAffinity
  • android:process
  • android:exported
  • android:theme

除非官方接入文档明确要求,否则不要手动修改内部 Activity 的启动模式、进程或任务栈属性。通常应让 SDK 自带的 Manifest 配置随依赖正常合并。

4. 防止重复发起支付

连续快速点击支付按钮,或者多个回调同时发起支付,都可能打乱 SDK 内部保存的请求与回调对应关系。可以在业务层增加单次支付锁:

private final AtomicBoolean paying = new AtomicBoolean(false);

public void startAlipay(Activity activity, String orderInfo) {
    if (activity == null || activity.isFinishing()) {
        return;
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1
            && activity.isDestroyed()) {
        return;
    }

    if (orderInfo == null || orderInfo.trim().isEmpty()) {
        return;
    }

    if (!paying.compareAndSet(false, true)) {
        return;
    }

    executor.execute(() -> {
        try {
            PayTask payTask = new PayTask(activity);
            Map<String, String> result = payTask.payV2(orderInfo, true);

            activity.runOnUiThread(() -> handlePayResult(result));
        } catch (Throwable throwable) {
            activity.runOnUiThread(() -> handlePayError(throwable));
        } finally {
            paying.set(false);
        }
    });
}

这里的 try/catch 只能捕获调用线程中的异常。题目中的异常出现在另一个 Activity 的主线程启动阶段,因此,在 payV2() 外层捕获异常并不能彻底阻止这次崩溃。解决问题仍需修正接入方式或升级 SDK。

5. 检查 Activity 和进程生命周期

记录发起支付前后的这些信息:

Log.d("AlipayDebug", "activity=" + activity);
Log.d("AlipayDebug", "finishing=" + activity.isFinishing());

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
    Log.d("AlipayDebug", "destroyed=" + activity.isDestroyed());
}

Log.d("AlipayDebug", "taskId=" + activity.getTaskId());
Log.d("AlipayDebug", "pid=" + Process.myPid());

如果崩溃主要发生在以下场景,应重点检查进程恢复或任务栈问题:

  • 在支付宝停留较长时间后返回。
  • App 进入后台后被系统回收。
  • 开发者选项启用了“不保留活动”。
  • App 使用多进程。
  • 从支付页面返回时发生横竖屏切换。
  • 支付期间宿主 Activity 被销毁,或者任务栈被清理。

可以在开发者选项中开启“不保留活动”,再通过限制后台、切换应用等方式尝试复现。如果问题只在进程重建后出现,应把 SDK 版本、系统版本、支付宝客户端版本和完整日志一并提交给支付宝技术支持。

6. 排除混淆、加固和热修复的影响

分别测试 Debug 包、未混淆的 Release 包和正式加固包:

  • 只有混淆后崩溃:检查是否遗漏或覆盖了 SDK 随包提供的混淆规则。
  • 只有加固后崩溃:检查加固工具是否修改了 Activity、Intent extras 或类加载过程。
  • 只有热修复版本崩溃:检查新旧 APK 中的支付宝 SDK 是否发生了不完整替换。
  • 所有构建都崩溃:更可能与调用方式、依赖冲突或 SDK 本身有关。

混淆规则应以当前 SDK 随附的说明为准,不要直接照搬来源不明的旧版规则。

为什么业务层无法直接修复 finish()

堆栈中的关键调用是:

APayEntranceActivity.finish()
ConcurrentHashMap.remove()
NullPointerException

这说明 SDK 在 finish() 中执行了类似下面的代码:

map.remove(requestKey);

此时 requestKey 是 null。即使 Map 中不存在对应记录,remove(nonNullKey) 也不会抛出这个异常。问题在于传入的 key 本身为空。

这个字段和 Map 都属于 SDK 的内部实现,不建议通过反射、继承或替换同名类来修补,否则可能造成签名跳转失败、回调丢失或后续升级冲突。

提交问题时应保留的信息

清理依赖并升级 SDK 后,如果问题仍能复现,应收集以下信息:

  • 支付宝 SDK 的准确版本和引入方式。
  • 手机品牌、Android 版本和 ROM 版本。
  • 支付宝客户端版本。
  • 是否使用加固、热修复或插件化框架。
  • 是否启用多进程。
  • 从发起支付到崩溃之间的完整 Logcat。
  • merged manifest 中 APayEntranceActivity 的最终配置。
  • 问题是否只在进程回收、重复点击或取消支付后出现。

日志中不要包含完整订单字符串、签名、用户标识或其他敏感数据。

注意事项

  • 支付结果不能只看客户端返回值,应以服务端异步通知或主动查询结果为准。
  • 不要在捕获该异常后直接判定支付失败,因为支付可能已经在支付宝侧完成。
  • 不要自行注册、启动或覆写 APayEntranceActivity。
  • Android 12 的 android:exported 配置问题通常表现为安装失败或 Activity 启动异常,无法单独解释这里的 ConcurrentHashMap.remove(null)。
  • 如果崩溃集中出现在某个 SDK 版本或特定 ROM 上,应先用最小工程验证。若最小工程运行正常,再逐项恢复加固、混淆、热修复和支付封装代码,以便定位冲突来源。

备注:内容仅供参考。