## 1. 支付SDK Crash的典型分类与根因分析:平台差异、线程模型、内存管理、网络异常、SDK版本兼容性

1.1 概述:Crash 是支付体验的“断点”

支付 SDK 接入后发生的 Crash 直接导致交易中断、用户流失甚至资损。根据生产环境数据统计,支付 SDK 的 Crash 分布集中在 5 大根因域:平台适配差异、线程调度冲突、内存资源耗尽、网络故障传播、版本兼容断裂。本章从原理层分解每一类的触发条件、故障传播链路与治理切入点。

治理前提: 稳定的 Crash 分类是监控告警与自动化归因的基础。本章列出的 5 类根因应作为 SDK 稳定性看板的必定义维度。
支付SDK Crash根因分类与故障传播 Crash事件 平台差异 Android/iOS/Harmony 线程模型 主线程阻塞/死锁 内存管理 OOM/野指针/泄漏 网络异常 超时/重连风暴 SDK版本兼容性 API break / 模块冲突 非托管 Crash 产生 异常传播到宿主进程 稳定性水位下降 归因→分级→自动止血

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 更新线程。不合理的线程调度会导致主线程阻塞、死锁或线程爆炸。

典型故障模式

  1. 主线程 I/O: 在网络响应回调中将数据直接写入 UI 组件,导致 ANR / 主线程卡死。
  2. 死锁: 后台线程持有锁 A 等待锁 B,主线程持有锁 B 等待锁 A。
  3. 线程泄漏: 每次支付创建新线程但未回收,导致线程数超限(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 或二进制兼容断裂。

典型兼容性场景

  1. 方法签名变更: 新版本移除 @deprecated 方法,旧代码仍调用。
  2. 模块冲突: SDK 内嵌的 JSON 库版本与宿主其他 SDK 冲突。
  3. 多 dex 问题: ClassLoader 加载顺序导致 SDK 类未被正确加载。

兼容性检查清单

检查项 方法 触发时机
API 兼容性 反射+@Deprecated 扫描 SDK 启动预热阶段
依赖版本映射 将内部依赖版本写入 Manifest 构建期 / 运行时校验
ClassLoader 校验 Class.forName("com.pay.sdk.core.SDKConfig") 初始化入口

1.7 本章小结:从分类到稳定防线