1.1 概述:Crash 是支付体验的“断点”
支付 SDK 接入后发生的 Crash 直接导致交易中断、用户流失甚至资损。根据生产环境数据统计,支付 SDK 的 Crash 分布集中在 5 大根因域:平台适配差异、线程调度冲突、内存资源耗尽、网络故障传播、版本兼容断裂。本章从原理层分解每一类的触发条件、故障传播链路与治理切入点。
治理前提: 稳定的 Crash 分类是监控告警与自动化归因的基础。本章列出的 5 类根因应作为 SDK 稳定性看板的必定义维度。
1.2 平台差异:碎片化适配的“暗礁”
支付 SDK 需要在 Android、iOS、HarmonyOS 等多终端运行。不同操作系统在权限模型、渲染管线、文件系统、后台生命周期上的差异,是 Crash 的首要来源。
| 维度 | Android | iOS | HarmonyOS |
|---|---|---|---|
| 权限模型 | 运行时权限(动态申请) | 系统弹窗(统一管理) | 低版本静态+高版本动态 |
| 后台限制 | doze / standby | Background Task 时限严格 | 进程管控策略不同 |
| 文件系统 | /data/data 沙箱 | App Sandbox | 分布式文件映射 |
| 热更新机制 | 无系统级热修复 | App Store 审核 | HAP 分包 / 快速修复 |
根因示例:Android 动态权限缺失
当 SDK 在 Android 6.0+ 未申请 READ_PHONE_STATE 权限就尝试读取设备标识时,直接抛出 SecurityException。支付 SDK 常因获取设备指纹做风控触发该 Crash。
// Crash 日志摘录
java.lang.SecurityException: getImei requires READ_PHONE_STATE
at android.os.Parcel.createException(Parcel.java:1966)
at android.app.ActivityManagerProxy.getImei(ActivityManagerNative.java:4567)
at com.pay.sdk.device.DeviceIdentifier.getImei(DeviceIdentifier.java:123)
治理要点: 所有平台特有 API 调用必须包裹 try-catch,并设置降级值或预案接口。在 SDK 初始化时预先检查权限状态。
1.3 线程模型:主线程的“交通堵塞”
支付 SDK 内部常存在独立网络线程池、签名计算线程、UI 更新线程。不合理的线程调度会导致主线程阻塞、死锁或线程爆炸。
典型故障模式
- 主线程 I/O: 在网络响应回调中将数据直接写入 UI 组件,导致 ANR / 主线程卡死。
- 死锁: 后台线程持有锁 A 等待锁 B,主线程持有锁 B 等待锁 A。
- 线程泄漏: 每次支付创建新线程但未回收,导致线程数超限(Android 默认线程栈 1MB)。
死锁检测示例(Thread Dump)
// 死锁场景摘录
"main" prio=5 tid=1 BLOCKED
waiting for <0x0c4e> (a com.pay.sdk.core.LockB)
locked <0x0c4f> (a com.pay.sdk.core.LockA)
"PayWorker-1" prio=5 tid=12 BLOCKED
waiting for <0x0c4f> (a com.pay.sdk.core.LockA)
locked <0x0c4e> (a com.pay.sdk.core.LockB)
治理策略
- 所有 SDK 内部长时间任务使用独立线程池,并设置
setRejectedExecutionHandler。 - 回调到主线程时必须校验
Looper.myLooper() == Looper.getMainLooper()。 - 引入
ThreadMXBean或 platform 的 ANR 监控,自动 dump 线程栈。
1.4 内存管理:OOM 与野指针的“幽灵”
支付 SDK 常涉及图片缓存、签名密钥池、订单结果队列。内存分配后未及时释放,或 object 被提前回收后继续访问,直接导致 OutOfMemoryError 或 NullPointerException。
泄漏场景
- Bitmap 未 recycle: 收银台页面的支付码图片在跳转后未释放。
- 匿名内部类持有 Activity: 异步回调中持有 Activity 引用,导致垃圾回收无法回收宿主。
- 全局静态集合: 通知监听器列表无限增长。
Android 内存泄漏检测命令
# 导出 Heap 文件(生产环境需定时自动导出)
adb shell am dumpheap com.pay.example /sdcard/pay_heap.hprof
# 使用 MAT 或 LeakCanary 分析
# 重点关注 com.pay.sdk 包下的实例滞留
治理策略
- 所有 SDK 内 Bitmap 使用
SoftReference并设置最大缓存水位。 - 回调使用弱引用 + 生命周期绑定(如
LifecycleObserver)。 - 上线前通过
LeakCanary+OOM Profiling进行全路径扫描。
1.5 网络异常:重连风暴与超时多米诺
支付请求依赖网络链路。当网络抖动时,SDK 若未做指数退避重试,可能出现 重连风暴——大量并发请求打垮网关,导致连接超时、线程池撑满、内存飙升。
故障传播链
| 阶段 | 现象 | Crash 类型 |
|---|---|---|
| 网络抖动 | 连续 3 次 connect timeout | SocketTimeoutException |
| 重试并发 | 10 个请求同时重试 | RejectedExecutionException |
| 线程池满 | 等待队列 300+ | OOM / ANR |
治理策略
- 重试策略:首次 1s,后续 2s / 4s / 8s 指数退避,最大 3 次。
- 设置连接超时 ≤ 5s,读取超时 ≤ 10s。
- 线程池核心线程数 ≤ 4,最大 ≤ 8,队列 ≤ 100,超出时直接回调失败。
关键指标: 网络异常导致的 Crash 在 SDK 总 Crash 中占比约 25%–35%,是治理优先级最高的域之一。
1.6 SDK版本兼容性:旧版遗留的“地雷”
宿主 App 升级 SDK 时,若未同步更新内部依赖的签名库、网络库、JSON 解析框架,极易发生 NoSuchMethodError、ClassNotFoundException 或二进制兼容断裂。
典型兼容性场景
- 方法签名变更: 新版本移除 @deprecated 方法,旧代码仍调用。
- 模块冲突: SDK 内嵌的 JSON 库版本与宿主其他 SDK 冲突。
- 多 dex 问题: ClassLoader 加载顺序导致 SDK 类未被正确加载。
兼容性检查清单
| 检查项 | 方法 | 触发时机 |
|---|---|---|
| API 兼容性 | 反射+@Deprecated 扫描 |
SDK 启动预热阶段 |
| 依赖版本映射 | 将内部依赖版本写入 Manifest | 构建期 / 运行时校验 |
| ClassLoader 校验 | Class.forName("com.pay.sdk.core.SDKConfig") |
初始化入口 |