先看结论与判断条件

  • 页面销毁只发起关闭,不在主线程同步等待不可控的 Native 工作。
  • 停止接收、发出取消、唤醒等待、汇合线程和释放引用是有顺序的独立阶段。
  • 条件变量等待必须使用状态谓词,关闭时更新状态并通知,不能只设置一个无人观察的布尔值。
  • worker 的阻塞 I/O、外部库调用和不可中断计算需要各自的取消或超时策略。
  • 迟到回调同时核对 worker generation、页面 generation 和回调目标状态,关闭后不得提交 UI。
  • 发布回执绑定候选哈希、阶段时间线、ANR 诊断和设备矩阵,不能用一次顺利退出外推兼容性。

主线程的责任是发起关闭,不是等待所有工作结束

页面 onStop 或 onDestroy 往往运行在主线程。如果这里直接调用 pthread_join、std::thread::join 或等待一个没有上限的条件变量,UI 线程就把退出速度交给 Native worker 当前所做的事情:它可能在等待队列、执行长计算、读取文件、等待 Binder 或调用不可取消的第三方库。页面生命周期方法返回不了,最终表现可能是卡顿或 ANR。

正确边界是把关闭请求拆成快速入口和异步收敛。主线程只原子切换 state 为 stopping、递增 generation、拒绝新任务、发出 cancellation token,并通知所有可能睡眠的 worker。随后把 join 责任交给专用协调线程或受控执行器;UI 可以继续完成页面销毁,关闭结果通过内部回执观察,而不是通过阻塞页面线程取得。

不在主线程 join 不等于放任线程泄漏。协调器必须拥有 worker 集合和唯一关闭 future,确保同一实例只启动一次收敛流程。重复 destroy 可以取得已有 future 或返回 already_stopping,不能再创建一套 join 线程。若进程结束前仍未收敛,记录所处阶段和候选信息,不能把线程静默遗忘。

页面线程与关闭协调器的责任
动作主线程协调线程失败终态
停止新任务提交状态切换验证队列不再增长accepting_after_stop
发出取消设置代际与 token观察 worker 确认cancel_not_seen
唤醒等待发送通知核对等待点退出worker_still_waiting
等待 worker不执行按阶段预算汇合join_budget_exceeded
释放 JNI 引用不抢先释放全部回调收敛后执行reference_still_in_use

关闭状态机要先阻断生产者,再让消费者退出

如果先让 worker 退出,再关闭 Java 或 Native 生产者,新的任务可能在退出窗口进入队列,留下无人处理的数据。关闭事务第一步把 accepting 从 true 切为 false,所有 submit 入口在锁内读取状态,stopping 之后返回明确的 rejected_after_stop。队列长度、活动任务数和 generation 同时写入关闭快照,便于判断后续是否又有生产者越界。

随后设置 stopRequested,并递增 workerGeneration。generation 使关闭前创建的任务、定时器和回调在未来即使被调度,也能识别自己属于旧代次。仅有布尔 stop 容易在对象重建后串线:新 worker 把 stop 重置为 false,旧任务误以为又可以提交。每个任务必须保存创建代次,任何提交都与当前代次比较。

状态推进应单向且幂等,例如 running、stopping、draining、joining、closed、failed。只有协调器能从 draining 进入 joining,只有确认全部线程退出且回调计数归零后才能进入 closed。错误也保留最终状态和阶段,不把 failed 改写为 closed;否则监控看到完成,实际仍有线程或 JNI 引用没有收敛。

Native worker 关闭状态机
状态是否接收任务允许动作转换条件
runningsubmit 与执行收到唯一关闭请求
stopping取消与通知生产者门禁已生效
draining处理或放弃活动任务所有 worker 已观察取消
joining协调线程等待退出没有新活动任务
closed返回完成回执引用与线程均收敛
failed记录阶段并升级处理预算或不变量失败

条件变量必须把取消写进谓词并可靠唤醒

worker 经常在条件变量上等待 queue 非空。如果谓词只写成 !queue.empty(),关闭线程即使 notify_all,worker 醒来后发现队列仍空,又会继续睡眠。谓词必须包含 stopRequested,例如 stopRequested || !queue.empty();唤醒后先判断停止策略,再决定丢弃队列、处理已提交任务或执行受控 drain。

状态更新和通知的顺序要与锁协议一致。关闭方在持有相同互斥量时写 stopRequested、generation 和队列策略,释放锁后通知;worker 在循环中用谓词等待,并在取出任务前核对代次。虚假唤醒不是异常,循环谓词本来就应处理。仅靠 sleep 轮询会增加退出延迟,也无法证明关闭事件何时被观察。

一个 worker 可能等待多个来源,例如工作队列、文件描述符、平台回调或第三方库。条件变量通知只能唤醒自己的等待点。设计清单要列出每个可阻塞点及其解除方法:关闭描述符副本、触发 eventfd、调用库提供的 cancel、设置超时或让操作运行在可放弃的隔离任务。没有解除方法时不能承诺 join 会及时返回。

常见阻塞点与关闭动作
阻塞点取消信号唤醒方式仍需验证
条件变量stopRequestednotify_all谓词包含停止状态
内部任务队列generation 失效队列通知旧任务不得提交
文件或 socket I/O关闭协议或取消句柄按 API 语义执行竞态与错误码
第三方计算库库级 cancel库定义回调不可中断区间
JNI 回调等待页面 generation停止投递全局引用生命周期

join 要在专用协调线程按阶段预算执行

join 本身不是错误,错误是把未知耗时的 join 放在主线程,或者在持有 worker 需要的锁时 join。协调线程开始汇合前先释放队列锁、回调锁和 Java 桥接锁,记录 joining 阶段,然后逐个等待 worker。若 worker 退出需要更新状态,它必须能够取得相关互斥量;否则关闭方与 worker 形成确定性死锁。

时间预算应来自项目而不是通用文章的固定数字。可以分别为观察取消、活动任务 drain、线程 join 和引用释放设置预算,并记录实际耗时、最后活动点和未退出线程。预算到期不应在 UI 线程继续无限等待,也不能粗暴释放 worker 仍使用的内存;状态进入 failed 或 quarantined,由进程生命周期和产品策略处理。

多 worker 场景不要盲目串行等待第一个卡住的线程而失去其他状态。协调器先广播取消,再收集所有 worker 的 acknowledged、lastCheckpoint 和 exited 标志,然后执行汇合。回执显示哪个线程卡在何种阶段。若平台只提供阻塞 join,可由每个 worker 在退出时完成 future,协调器先等待这些可观测事件,最终再安全回收线程对象。

关闭阶段的预算和证据
阶段开始证据完成证据超预算记录
stop_accepting状态版本提交入口拒绝越界生产者
cancel_broadcastgeneration 与 tokenworker acknowledged未观察线程
drain活动任务集合任务为零或已放弃最后检查点
join线程 ID 集合全部 exited未退出线程
release_refs全局引用计数计数归零并删除仍持有者

JNI 引用和迟到回调必须晚于线程收敛释放

Native worker 回调 Java 时会涉及线程附着、JNIEnv 获取、局部或全局引用以及 pending exception。页面销毁后如果立即 DeleteGlobalRef,而 worker 仍可能回调,就会形成使用已释放引用的窗口;反过来永不释放引用又会泄漏页面。关闭协议要先禁止新回调,等待活动回调计数归零,再在合法 JNI 环境中释放全局引用。

每次回调捕获 workerGeneration、pageGeneration 和 callbackId。投递前检查 Native 状态仍允许,Java 接收端再检查页面 generation 和 lifecycle owner;任一步不匹配都记录 dropped_late_callback,不执行 UI 或业务提交。弱引用只能帮助观察目标是否还活着,不能替代 generation,因为新页面实例可能拥有相同业务角色却不是旧任务的目标。

线程附着与分离要按 JNI 边界单独设计。本文关注关闭顺序,不把 attach 或 detach 细节扩写成另一套答案;必要条件是每个 worker 的 JNI 资源有明确 owner,异常在回调后被检查和转换,线程退出前完成其责任。通用 JNI 指南不能证明变换后的候选已保持引用与线程语义,仍要设备端取证。

用关闭时间线校验器发现主线程 join 和迟到回调

下面的 Python 校验器读取脱敏事件 JSON,只检查阶段、线程角色、generation 和预算结果。事件必须按 sequence 单调排列;stop_accepting 必须早于 cancel_broadcast,join_start 必须发生在 coordinator 线程,release_refs 只能出现在所有 worker_exit 之后。任何违反都会以非零状态退出,适合放进设备测试产物的门禁。

校验器还检查 shutdown_generation 之后的 callback_commit。如果回调属于更旧 generation,或事件发生在 release_refs 之后,就直接拒绝回执。它不读取客户数据、内部地址或线程堆栈,只需要 workerId、event、threadRole、generation 和 elapsedBudgetState 等有限字段,因此可以公开解释而不暴露运行环境。

示例无法判断 worker 是否真的释放全部 Native 内存,也无法代替 ANR trace 分析。生产回执还应包含候选哈希、ABI、API level、设备标识类别和操作路径。若某阶段没有事件,校验器按缺失失败,不能根据 shutdown_complete 一条总结事件推断所有中间步骤发生过。

Native worker 关闭时间线门禁
import json
import sys
from pathlib import Path

REQUIRED = [
    "stop_accepting",
    "cancel_broadcast",
    "join_start",
    "worker_exit",
    "release_refs",
]

def stop(message):
    print(f"shutdown trace rejected: {message}", file=sys.stderr)
    raise SystemExit(2)

def load_trace(path_text):
    path = Path(path_text)
    if not path.is_file():
        stop("trace file is missing")
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as exc:
        stop(f"cannot parse trace: {exc}")

def validate(trace):
    events = trace.get("events", [])
    if not events:
        raise SystemExit("shutdown trace rejected: events are missing")
    sequence = [event.get("sequence") for event in events]
    if any(not isinstance(value, int) for value in sequence):
        stop("sequence must contain integers")
    if sequence != sorted(sequence) or len(sequence) != len(set(sequence)):
        stop("sequence is not strictly increasing")
    names = [event.get("event") for event in events]
    for required in REQUIRED:
        if required not in names:
            stop(f"required event is missing: {required}")
    first = {name: names.index(name) for name in REQUIRED}
    if not first["stop_accepting"] < first["cancel_broadcast"] < first["join_start"]:
        stop("shutdown phases are out of order")
    join = events[first["join_start"]]
    if join.get("threadRole") == "main":
        stop("join started on the main thread")
    exit_positions = [i for i, name in enumerate(names) if name == "worker_exit"]
    if not exit_positions or max(exit_positions) > first["release_refs"]:
        stop("JNI references were released before workers exited")
    generation = trace.get("shutdownGeneration")
    for event in events:
        if event.get("event") == "callback_commit" and event.get("generation") != generation:
            stop("stale callback committed after shutdown")
        if event.get("budgetState") == "exceeded" and not event.get("failureReceipt"):
            stop("budget overrun has no failure receipt")
    print("shutdown trace is internally consistent")

if __name__ == "__main__":
    if len(sys.argv) != 2:
        stop("usage: validate_shutdown.py trace.json")
    validate(load_trace(sys.argv[1]))

设备测试要控制竞态并用 ANR 证据定位等待链

测试需要可控地把 worker 停在各类等待点,然后触发页面销毁。用测试闩锁让它分别停在条件变量、任务执行、回调提交和引用释放之前,验证主线程快速返回、协调器推进正确、迟到回调被拒绝。随机快速进出页面可以补充压力,但不能替代可重现的事件序列。

ANR 诊断要从主线程堆栈开始,却不能停在表面帧。若主线程显示等待 Native 方法,要继续查锁 owner、worker 最后检查点、Binder 或 I/O 依赖和协调器状态。Android 的 ANR 指南强调按主线程阻塞、锁竞争、Binder、I/O 与组件超时分层定位;trace 中当前堆栈可能只是等待结果,不一定是最初阻塞源。

ApplicationExitInfo 可以辅助取得退出原因、时间和受版本限制的 ANR 或 Native 诊断信息。回执要把记录关联到同一候选哈希、设备、时间窗和页面路径。没有匹配关系的历史退出记录不能证明本次 join 问题,单个版本未出现 ANR 也不能证明所有厂商和负载下安全。

Native worker 关闭回归矩阵
用例worker 停点主线程预期关闭回执
空队列等待条件变量只发起关闭通知后退出
活动计算取消检查点前不 join观察取消或超预算
阻塞 I/O系统调用中不持锁等待记录解除方式
回调竞争提交 UI 前页面可销毁旧 generation 丢弃
重复销毁已在 joining复用关闭 future不重复 join
进程退出任意阶段无伪完成退出原因与阶段关联

发布门禁绑定阶段顺序、预算和候选证据

发布前列出每个 Native worker 的创建者、任务生产者、所有阻塞点、取消信号、唤醒方式、join owner、JNI 引用和回调目标。代码审查搜索主线程 join、持锁 join、只写 stop 不通知、无 generation 回调和过早 DeleteGlobalRef。清单能发现明显问题,但不替代设备上的生命周期和异常回归。

JNI 签名、线程模型、第三方库、阻塞 I/O、API level、加固边界或编译选项变化后,应生成新候选哈希并重跑关键时间线。NDK 常见问题资料帮助定位符号、运行库、异常和依赖装载,不能证明 worker 关闭契约在目标 ABI 上成立。预算数字应来自产品要求和实测,不从公共文章硬编码。

站内关于 JNI 线程附着与分离的文章可继续核对 worker 进入 JVM 的边界;准备评估商业 App 的 SO 加固和线程生命周期时,可通过页面行动按钮进入御盾中央平台提交候选包、worker 清单与关闭时间线。提交只启动评估,是否避免主线程阻塞仍以同一候选的设备回执为准。

  • 页面线程只切换状态、递增 generation、取消和通知。
  • 所有 submit 入口在 stopping 后明确拒绝新任务。
  • 条件变量谓词包含 stopRequested,不依赖单次通知。
  • 每个阻塞点都有可核验的解除或超预算策略。
  • join 由专用协调线程执行,且不持有 worker 需要的锁。
  • 回调提交前核对 worker、页面和请求 generation。
  • 全部 worker 与活动回调收敛后才释放 JNI 全局引用。
  • 候选变化后重跑竞态、ANR、异常和退出矩阵。

事实依据与适用边界

以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。

本文判断事实或工程依据适用限制
JNI 线程、引用、异常和类加载边界会影响 Native worker 回调的稳定性。Android JNI tips 说明线程附着、引用生命周期、异常检查和类加载相关实践。通用建议不证明某个候选 SO 的关闭顺序、ABI 或回调语义已经通过。
Native 兼容排查要考虑 API level、符号、运行库、异常和依赖装载。Android NDK common problems 列出这些常见故障类别及排查方向。清单不能代替目标 ABI 上的 worker 取消、唤醒、join 与引用释放测试。
ANR 需要区分主线程阻塞、锁竞争、Binder、I/O 和组件超时。Diagnose Android ANRs 提供按阻塞类型定位 ANR 的官方诊断框架。trace 中看到 join 或 Native 帧不一定是根因,仍要追踪锁 owner 和上游依赖。
退出原因和受版本限制的诊断数据可辅助关联关闭失败。Android ApplicationExitInfo 提供进程退出原因、时间和部分 ANR 或 Native 诊断入口。记录必须与同一候选和时间窗关联,不能用无关历史退出证明本次问题。
涉及真实 Android 生命周期和系统 API 的行为应使用设备端测试。Android instrumented tests 说明这类测试运行在 Android 设备环境并可使用框架 API。单台设备和单一路径通过不能覆盖完整 API、ABI、芯片与厂商矩阵。
安全发布需要保留变更、构建和验证证据。NIST SP 800-218 SSDF 将安全开发、验证和供应链风险组织为实践框架。SSDF 不定义 Native worker 的状态机,也不形成任何具体加固产品验收结论。
主线程只发起关闭、协调线程执行 join,可隔离不可控等待对 UI 生命周期的影响。工程判断:join 耗时由 worker 当前阻塞点决定,放在主线程会直接占用 UI 事件处理。实际预算和效果需要同一候选的时间线与 ANR 回执,本文不提供性能数字。
御盾项目中的 worker 收敛和 ANR 结果当前不能从通用资料推出。项目证据尚未接入:需要候选哈希、worker 清单、阶段事件、设备矩阵和退出记录。回执齐备前不得声称已消除阻塞、通过兼容测试、阻断攻击或取得客户效果。

工程常见问题

页面 onDestroy 中调用 join 为什么容易卡住?

onDestroy 通常在主线程执行,join 的返回取决于 worker 是否观察取消、能否离开 I/O 或锁等待。任何不可控阶段都会直接阻塞 UI 生命周期。

把 join 放到后台线程后就一定不会泄漏吗?

不一定。仍需唯一协调器、阶段预算、worker 退出回执和引用释放顺序;后台无限等待只是把不可见泄漏移到另一条线程。

设置 stopRequested 后为什么还要 notify_all?

正在条件变量中睡眠的 worker 不会因布尔值改变自动醒来。谓词必须包含停止状态,关闭方更新状态后通知,worker 醒来再按谓词退出。

遇到不可中断的第三方 Native 调用怎么办?

先确认库是否提供取消或超时;没有时隔离任务并设置项目级预算,不能在超预算后释放仍被使用的对象,也不能承诺立即 join。

页面销毁后弱引用为空是否足以处理迟到回调?

不足。新页面可能已经创建,旧任务还可能写业务状态。回调需同时核对 worker generation、页面 generation 和请求代次。

SO 加固后需要重新测试 worker 关闭吗?

需要。候选变化可能影响 JNI、异常、线程、库装载和阻塞路径,旧时间线不能证明新候选仍按相同顺序收敛。

想用自己的 App 验证?

提交候选包、目标系统和关键业务路径,申请御盾 PoC 与兼容性评估。

继续阅读: SO 加固与 Native 兼容性检查清单