先看结论与判断条件

  • Local Reference 的有效范围受当前 Native 调用、本地引用帧和线程约束;循环中持续创建而不释放,会把瞬时对象积累成稳定性风险。
  • Global Reference 必须拥有明确的业务所有者、创建入口、唯一释放事件和幂等策略,不能把进程退出当作日常资源管理方案。
  • Weak Global Reference 只表达弱可达关系,使用前需要在 JNI 语义下确认对象仍可用,不能把缓存命中等同于强引用存活。
  • 早退、Java 异常、Native 错误与组件销毁路径都必须经过同一清理责任,成功路径配对并不能覆盖真实泄漏面。
  • 静态扫描和事件账本只能发现可见的不配对、跨作用域使用与本地帧失衡,最终结论仍需目标 ABI 设备端的重复调用、回收和退出回归。
  • SO 加固验收要绑定候选摘要、ABI、符号、测试路径和退出记录;保护变换不是引用错误的修复器,也不能替代基线归因。

先把引用种类、所有者和失效边界写成契约

JNI 引用审计的起点不是统计 API 次数,而是回答三个问题:谁创建、谁可以使用、什么事件必须释放。Local Reference 通常服务一次 Native 调用或一个显式本地帧,Global Reference 跨调用保存 Java 对象身份,Weak Global Reference 则允许对象被回收。三类引用的寿命和可用性不同,混在一张“句柄列表”里会丢失最重要的边界。

Android JNI tips 把本地引用、全局引用、异常、线程与类加载列为 JNI 正确性的重要边界。可核查事实是 JNI 环境和引用存在明确语义;工程判断是发布团队应把这些语义转换为可审计字段,例如 refId、kind、owner、thread、scope、createSite、releaseEvent 和 candidateSha256。这样的账本能解释某个句柄为什么此刻仍应有效,而不是只证明代码里出现过删除函数。

保护变换前必须先让未加固发布基线通过同一套契约。若基线已经存在未释放全局引用、跨线程复用局部引用或弱引用失效后的继续调用,加固候选出现崩溃时就无法判断是历史缺陷、时序放大还是变换回归。本文提供的是检查和门禁方法,不声称任何具体库、候选包或御盾能力已经通过验证。

JNI 引用的最小生命周期契约
引用种类典型所有者允许使用范围必须记录的释放或失效事件
Local当前 Native 调用或本地帧创建线程和有效作用域内返回 Java、DeleteLocalRef 或 PopLocalFrame
GlobalNative 缓存、组件或进程级对象显式删除前可跨调用使用对应所有者终止时 DeleteGlobalRef
Weak Global允许回收的 Native 索引确认仍存活并取得可用引用后DeleteWeakGlobalRef 或对象已不可用
临时提升引用一次读取或回调当前受控调用范围使用结束立即删除局部强引用

Local Reference 要按调用、本地帧与循环压力检查

局部引用通常由 JNI 参数、对象创建、字段读取或方法返回值产生。它们看似会在 Native 方法返回时统一失效,但长循环、批量解析和常驻 Native 线程可能在返回前创建大量对象。只依赖“最终会自动释放”会掩盖峰值压力,因此审计必须定位循环体内的创建点、每轮释放点、退出分支和本地帧边界。

检查循环时,应把每次迭代的新增局部引用数、可到达的最大迭代次数、是否调用 DeleteLocalRef、是否用 PushLocalFrame 与 PopLocalFrame 分段,以及失败分支是否跳过清理写进记录。这里的数字只能来自真实代码路径和观测,不能凭经验给出通用安全上限。门禁关注的是增长是否有界、边界是否可解释,而不是宣称某个固定数量适合所有运行时。

跨调用保存 Local Reference 是更直接的错误。把 jobject 放进静态变量、Native 单例、异步队列或另一个线程的任务结构,并不自动把它变成全局引用。事件审计应在 scope-end 时检查仍被标记为 retained、queued 或 stored 的局部引用;发现后阻断发布,要求调用方明确创建 Global Reference,或把所需数据复制为真正属于 Native 的值对象。

Local Reference 路径检查表
路径需要观测阻断信号修正方向
短方法入口、创建、删除、返回返回后仍保存 refId删除或显式提升
长循环每轮新增与释放未配对数量随迭代增长逐轮删除或使用本地帧
早退每个 return 前的清理失败分支残留引用统一清理出口或 RAII
异步任务入队对象的引用种类Local 被跨线程保存复制数据或创建受管 Global
本地帧push、pop 与保留结果帧深度不平衡成对退出并核对保留对象

Global Reference 要绑定业务所有者和唯一释放事件

Global Reference 的危险不只在忘记 DeleteGlobalRef,还在没有人能说清它属于谁。Native 缓存可能跟随 Application、某个 Service、一个引擎实例或一次媒体会话,但这些对象的结束条件完全不同。审计清单必须把 ownerType 与 ownerId 写进 create 事件,并为每种所有者定义可观察的 releaseEvent,禁止使用“以后统一清理”这样的模糊责任。

创建和释放还需要处理幂等与并发。初始化重试可能为同一逻辑对象重复创建 Global Reference,组件销毁与错误回滚也可能同时尝试删除。正确门禁不是简单要求 create 数等于 delete 数,而是核对每个 refId 的状态迁移只能从 absent 到 live,再从 live 到 released;重复创建、未知删除、释放后使用和所有者结束后的残留都要单独报告。

把所有 Global Reference 留到进程退出并不适合可重复创建的组件。Android 进程何时终止不由业务代码完全控制,且用户可能在一个长进程内反复进入和离开同一功能。工程上应优先选择与最窄业务寿命一致的所有者,只有确实服务整个进程且数量有界的对象,才可以采用进程级寿命,并仍要保留初始化失败和测试卸载路径。

Global Reference 所有权决策
所有者创建事件释放事件重点失败条件
组件实例组件初始化成功组件确定销毁重建后旧引用仍活跃
会话会话建立并登记结束、取消或错误回滚异常分支未释放
Native 引擎引擎实例完成构造实例 shutdown 完成重复 shutdown 或并发使用
进程级缓存一次性初始化测试卸载或进程清理条目数量无界增长
异步回调任务被接受回调完成或取消取消后继续回调

Weak Global Reference 必须把存活判断和实际使用分开

Weak Global Reference 适合表达“不应阻止 Java 对象被回收”的关系,但它不是廉价的永久句柄。对象可能在 Native 计划使用前已经不可用,单纯判断 Native 侧地址非空不能建立强可达性。调用路径应先执行符合 JNI 语义的存活确认或取得临时可用引用,再在受控范围内访问对象,最后释放这次临时引用。

弱引用审计要区分 weak-create、weak-promote、weak-use、temporary-release 和 weak-delete。若日志只出现 weak-use 而没有同一 scope 内成功的 promote,就无法证明使用时对象仍存活;若提升成功后把临时 Local Reference 放入队列,又会重新引入跨作用域问题。事件模型因此要同时追踪弱句柄和本次使用产生的临时强引用。

缓存未命中或对象已回收应当是正常分支,而不是崩溃前的异常状态。业务层需要定义降级行为,例如重新解析对象、取消本次回调或返回明确错误,并确保该分支不继续调用实例方法。测试要主动制造组件离开、Java 对象不可达和回收压力,但不能把一次未观察到回收写成弱引用永远安全。

Weak Global Reference 使用门禁
阶段需要的证据禁止假设失败处理
创建owner 与用途已登记弱引用会保持对象存活创建失败立即回滚
提升当前 scope 取得可用临时引用句柄地址非空等于对象有效返回未命中或取消
使用仅使用本次提升结果可以跨线程保存 Local复制数据或另建 Global
临时释放同一 scope 内完成方法返回前无需关注统一清理出口
弱引用删除所有者终止并登记对象回收会自动删除 Native 句柄幂等删除并阻止再用

异常、早退和回滚路径必须共享清理责任

JNI 调用可能留下待处理的 Java 异常,此时继续执行普通 JNI 操作会使后续行为难以解释。审计不能只在函数尾记录 cleanup,而要在每个可能触发异常的调用后确认控制流:异常是转换为 Native 错误、清除后降级,还是保留并立即返回 Java。无论采用哪种策略,已经创建的 Local、Global 与临时提升引用都必须按所有权规则完成回滚。

C++ RAII 可以把删除动作绑定到对象析构,但包装器自身也需要明确 JNIEnv、线程和移动语义。一个持有 Local Reference 的 RAII 对象若跨线程移动,析构时使用错误环境并不会因为“自动释放”而正确。本文的范围是引用生命周期,因此线程附加细节应由独立的 JNI 线程契约检查覆盖;此处只要求释放发生在合法上下文并留下可关联事件。

Android NDK common problems 汇总的故障还包括 API level、缺失符号、STL、异常类型和依赖装载。它说明 Native 故障有多个相邻原因,所以引用审计通过后不能排除其他兼容问题。工程门禁应保留错误类别:reference-lifecycle、symbol-load、runtime-library、exception-boundary 与 unknown,避免把所有 Native 崩溃强行归入引用泄漏。

异常路径的清理责任
控制流引用动作异常动作验收证据
成功返回释放临时引用并关闭本地帧确认无待处理异常scope-end 无残留
Java 异常传播回滚已创建资源保留异常并立即返回异常与清理事件同 scope
转换为错误码回滚已创建资源按策略检查并清除错误码和原始调用点关联
Native 早退执行统一 cleanup不掩盖已有异常所有 return 分支被覆盖
初始化失败删除已建 Global保存首个根因owner 未进入 live 状态

静态清单与事件账本负责发现不同类型的问题

静态检查适合寻找 NewGlobalRef、NewWeakGlobalRef、DeleteGlobalRef、DeleteWeakGlobalRef、PushLocalFrame 和 DeleteLocalRef 的调用点,并标记存入全局容器、静态字段或异步任务的 jobject。它能发现明显缺口,却不能可靠重建宏、模板、回调和异常下的真实控制流,更不能证明运行时对象在某一时刻存活。

事件账本来自调试或测试构建中的公开安全埋点,记录抽象 refId、种类、scope、thread、owner、操作和候选摘要,不记录对象内容、用户数据或真实指针。账本审计可以检查未知删除、重复创建、scope 结束后的 Local 残留、未关闭本地帧、弱引用未提升直接使用,以及 owner 结束后的 Global 残留。它仍然依赖被测试路径确实执行,不能把未出现的事件解释为代码不存在。

两类证据要在同一候选上汇合:静态清单给出理论入口,设备事件证明选定路径被触发。若静态发现十个创建点而测试只覆盖两个,报告应明确剩余覆盖缺口;若事件出现静态清单没有解释的操作,则检查生成代码、第三方库或埋点映射,而不是删除不方便的记录。

检查方法的能力边界
方法擅长发现不能证明合格输出
源码静态清单API 点和明显保存路径真实控制流已执行文件、函数、行与所有者
事件账本已执行路径的状态失衡未覆盖路径不存在问题候选、ABI、scope 与发现项
内存趋势重复路径后的增长信号增长一定来自 JNI 引用基线、候选和复现步骤
崩溃与退出记录进程终止时间和原因单凭堆栈确定所有权根因同版本、时间窗、路径关联
设备端回归Android 运行时真实语义全部 API、ABI、厂商均通过设备、系统、ABI 与结果矩阵

目标 ABI 设备回归要重复生命周期而非只做一次启动

Android instrumented tests 适合验证依赖真实 Android 运行时、组件与系统 API 的行为。JNI 引用回归应在每个发布 ABI 的代表设备上重复创建和销毁组件、开启和取消会话、触发成功与异常分支、切换前后台,并让长循环达到业务可辩护的批量规模。测试次数和阈值必须来自产品路径与基线测量,不能由文章虚构统一数值。

对比应使用相同业务输入、相同设备状态和明确候选身份。未加固基线先证明引用事件闭合,加固候选再执行同一路径,比较未配对引用、峰值变化、异常类型、退出原因和 Native 堆栈。若只保留“测试通过”而没有 APK 或 AAB 摘要、SO 摘要、ABI、系统版本、路径和时间窗,结果不能进入发布结论。

单一设备通过不代表完整 API、ABI 和厂商矩阵。设备覆盖要以实际发布范围为准,并把未覆盖项保留为限制。某个 x86_64 模拟器上的重复测试能帮助发现控制流问题,却不能替代 arm64-v8a 真机装载和运行证据;同理,一个新系统版本的退出信息丰富,也不能覆盖旧系统上的行为差异。

JNI 引用设备回归矩阵
测试维度基线路径候选路径失败判定
组件重建进入、离开、再次进入完全相同旧 owner 的 Global 仍存活
异常回滚触发可控 Java 异常完全相同异常分支遗留引用或帧
批量循环业务上限内重复处理完全相同Local 未配对持续增长
弱引用失效对象离开并制造回收机会完全相同未提升直接使用或崩溃
会话取消异步任务建立后取消完全相同取消后回调或 owner 残留
进程退出记录版本、路径和时间窗完全相同出现不可解释的新退出类别

用退出证据、符号和发布记录完成归因

Android ApplicationExitInfo 可以提供进程退出原因、ANR trace,并在较新条件下提供 Native tombstone。使用这些信息时,要把记录与同一包版本、候选摘要、时间窗和用户路径关联。看到 Native 崩溃或低内存退出,只能说明进程如何结束,不能单凭退出类别证明某个 Global Reference 泄漏,更不能把其他进程共享压力排除在外。

Debug Android native code 说明 Native 调试和 tombstone 还原依赖正确符号、架构与构建产物。加固候选的诊断包应保存可追溯的符号映射,并与发布产物摘要一一对应。错误候选的符号可能产生看似合理却完全错误的函数位置,因此任何引用生命周期归因都要先通过 candidate、ABI 和 symbol identity 门禁。

NIST SP 800-218 SSDF 要求在开发与发布过程中保留来源、构建、验证、变更和供应链证据。对本检查而言,最低发布包包括源码清单摘要、事件账本摘要、设备矩阵、异常与退出回执、符号身份和批准人。SSDF 是组织级实践框架,不证明具体加固产品功能;若要提交实际 SO 兼容评估,应先准备这些材料,再通过御盾中央平台提交申请。

发布证据包的关联键
证据必须绑定可支持的结论不能单独支持
引用事件账本candidate、ABI、scope、path已覆盖路径是否配对未覆盖代码没有缺陷
ApplicationExitInfo版本、时间窗、用户路径退出原因与诊断入口引用泄漏是唯一根因
Native tombstone同候选符号和架构崩溃位置与线程上下文所有权责任自动成立
设备矩阵设备、API、ABI、业务输入具体覆盖范围内的结果全部 Android 环境通过
变更记录源码、构建、审核与批准结论可追溯到候选产品具备未验证能力
  • 输入只含抽象 refId 和测试元数据,不写对象内容、真实指针、用户数据或凭据
  • 每个事件 seq 严格递增,并绑定候选 SHA-256 与实际发布 ABI
  • Local、Global、Weak Global 和临时提升引用分别维护状态,不用总数相抵
  • scope-end 同时检查局部引用、本地帧、待处理异常和临时提升引用
  • owner-end 后仍存活的 Global 与 Weak Global 明确阻断,不用进程退出掩盖
  • 静态与事件门禁通过后,仍执行目标设备重复路径和退出证据复核
校验 JNI 引用事件并阻断生命周期失衡
#!/usr/bin/env python3
import hashlib
import json
import sys
from collections import defaultdict
from pathlib import Path

ALLOWED_ABIS = {"armeabi-v7a", "arm64-v8a", "x86", "x86_64"}
ALLOWED_OPS = {
    "frame-push", "frame-pop", "local-create", "local-delete",
    "local-retain", "global-create", "global-delete",
    "weak-create", "weak-delete", "weak-promote",
    "weak-use", "temporary-release", "exception-pending",
    "exception-cleared", "owner-end", "scope-end",
}

def fail(message, code=2):
    print(message, file=sys.stderr)
    raise SystemExit(code)

def sha256_file(path):
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for block in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(block)
    return digest.hexdigest()

def load_document(path):
    try:
        value = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, UnicodeError, json.JSONDecodeError) as exc:
        fail("cannot read event document: " + str(exc))
    if not isinstance(value, dict):
        fail("event document must be an object")
    return value

def require_text(event, field):
    value = event.get(field)
    if not isinstance(value, str) or not value.strip():
        fail("event field is missing: " + field)
    return value.strip()

def add_finding(findings, event, kind, detail):
    findings.append({
        "seq": event.get("seq"),
        "kind": kind,
        "detail": detail,
    })

def audit(document):
    artifact = document.get("artifactSha256")
    abi = document.get("abi")
    events = document.get("events")
    if not isinstance(artifact, str) or len(artifact) != 64:
        fail("artifactSha256 must contain 64 hexadecimal characters")
    try:
        int(artifact, 16)
    except ValueError:
        fail("artifactSha256 is not hexadecimal")
    if abi not in ALLOWED_ABIS:
        fail("unsupported or missing ABI")
    if not isinstance(events, list) or not events:
        fail("events must be a non-empty array")

    findings = []
    last_seq = 0
    frames = defaultdict(int)
    locals_by_scope = defaultdict(set)
    retained_locals = set()
    globals_live = {}
    weak_live = {}
    promoted = defaultdict(set)
    pending_exception = set()
    ended_owners = set()

    for event in events:
        if not isinstance(event, dict):
            fail("every event must be an object")
        seq = event.get("seq")
        if not isinstance(seq, int) or seq <= last_seq:
            fail("event seq must be a strictly increasing integer")
        last_seq = seq
        op = require_text(event, "op")
        if op not in ALLOWED_OPS:
            fail("unsupported event operation: " + op)
        thread = require_text(event, "thread")
        scope = require_text(event, "scope")
        scope_key = (thread, scope)
        ref_id = str(event.get("ref", "")).strip()
        owner = str(event.get("owner", "")).strip()

        if op == "frame-push":
            frames[scope_key] += 1
        elif op == "frame-pop":
            if frames[scope_key] == 0:
                add_finding(findings, event, "frame-underflow", str(scope_key))
            else:
                frames[scope_key] -= 1
                locals_by_scope[scope_key].clear()
        elif op == "local-create":
            if not ref_id:
                fail("local-create requires ref")
            if ref_id in locals_by_scope[scope_key]:
                add_finding(findings, event, "duplicate-local", ref_id)
            locals_by_scope[scope_key].add(ref_id)
        elif op == "local-delete":
            if ref_id not in locals_by_scope[scope_key]:
                add_finding(findings, event, "unknown-local-delete", ref_id)
            else:
                locals_by_scope[scope_key].remove(ref_id)
                retained_locals.discard((scope_key, ref_id))
        elif op == "local-retain":
            if ref_id not in locals_by_scope[scope_key]:
                add_finding(findings, event, "retain-unknown-local", ref_id)
            else:
                retained_locals.add((scope_key, ref_id))
        elif op == "global-create":
            if not ref_id or not owner:
                fail("global-create requires ref and owner")
            if ref_id in globals_live:
                add_finding(findings, event, "duplicate-global", ref_id)
            if owner in ended_owners:
                add_finding(findings, event, "create-after-owner-end", owner)
            globals_live[ref_id] = {"owner": owner, "seq": seq}
        elif op == "global-delete":
            if ref_id not in globals_live:
                add_finding(findings, event, "unknown-global-delete", ref_id)
            else:
                del globals_live[ref_id]
        elif op == "weak-create":
            if not ref_id or not owner:
                fail("weak-create requires ref and owner")
            if ref_id in weak_live:
                add_finding(findings, event, "duplicate-weak", ref_id)
            weak_live[ref_id] = {"owner": owner, "seq": seq}
        elif op == "weak-promote":
            if ref_id not in weak_live:
                add_finding(findings, event, "promote-unknown-weak", ref_id)
            else:
                promoted[scope_key].add(ref_id)
        elif op == "weak-use":
            if ref_id not in promoted[scope_key]:
                add_finding(findings, event, "weak-use-without-promotion", ref_id)
        elif op == "temporary-release":
            if ref_id not in promoted[scope_key]:
                add_finding(findings, event, "unknown-temporary-release", ref_id)
            else:
                promoted[scope_key].remove(ref_id)
        elif op == "weak-delete":
            if ref_id not in weak_live:
                add_finding(findings, event, "unknown-weak-delete", ref_id)
            else:
                del weak_live[ref_id]
        elif op == "exception-pending":
            pending_exception.add(scope_key)
        elif op == "exception-cleared":
            if scope_key not in pending_exception:
                add_finding(findings, event, "clear-without-pending", str(scope_key))
            pending_exception.discard(scope_key)
        elif op == "owner-end":
            if not owner:
                fail("owner-end requires owner")
            ended_owners.add(owner)
            for live_ref, record in sorted(globals_live.items()):
                if record["owner"] == owner:
                    add_finding(findings, event, "global-after-owner-end", live_ref)
            for live_ref, record in sorted(weak_live.items()):
                if record["owner"] == owner:
                    add_finding(findings, event, "weak-after-owner-end", live_ref)
        elif op == "scope-end":
            for live_ref in sorted(locals_by_scope[scope_key]):
                add_finding(findings, event, "local-at-scope-end", live_ref)
            if frames[scope_key] != 0:
                add_finding(findings, event, "unbalanced-local-frame", str(frames[scope_key]))
            if promoted[scope_key]:
                add_finding(findings, event, "promoted-weak-at-scope-end", ",".join(sorted(promoted[scope_key])))
            if scope_key in pending_exception:
                add_finding(findings, event, "pending-exception-at-scope-end", str(scope_key))
            for retained_scope, live_ref in sorted(retained_locals):
                if retained_scope == scope_key:
                    add_finding(findings, event, "retained-local-crosses-scope", live_ref)

    for live_ref, record in sorted(globals_live.items()):
        findings.append({"seq": None, "kind": "global-not-released", "detail": live_ref + " owner=" + record["owner"]})
    for live_ref, record in sorted(weak_live.items()):
        findings.append({"seq": None, "kind": "weak-not-released", "detail": live_ref + " owner=" + record["owner"]})

    return {
        "status": "blocked" if findings else "runtime-scope-passed",
        "artifactSha256": artifact.lower(),
        "abi": abi,
        "eventCount": len(events),
        "findings": findings,
        "boundary": "This audit covers only executed and recorded paths.",
    }

def main():
    if len(sys.argv) != 2:
        print("Usage: audit_jni_reference_events.py EVENTS_JSON", file=sys.stderr)
        raise SystemExit(2)
    input_path = Path(sys.argv[1])
    if not input_path.is_file():
        print("event document does not exist", file=sys.stderr)
        raise SystemExit(2)
    document = load_document(input_path)
    result = audit(document)
    result["inputSha256"] = sha256_file(input_path)
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    if result["status"] == "blocked":
        raise SystemExit(20)

if __name__ == "__main__":
    main()

事实依据与适用边界

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

本文判断事实或工程依据适用限制
JNI 的引用、异常、线程和类加载边界会直接影响 Native 桥接稳定性。Android JNI tips 说明 JNI 引用及相关运行边界。这些建议不证明某个加固候选保持 ABI,也不代替目标设备验证。
Local、Global 与 Weak Global 需要按不同寿命和可用性建模。Android JNI tips 对本地引用、全局引用和弱全局引用给出不同使用语义。文章中的 owner、scope 和事件字段属于工程审计设计,不是 Android 平台自动生成的结论。
Native 兼容故障还可能来自 API level、缺失符号、STL、异常类型和依赖装载。Android NDK common problems 汇总常见 NDK 故障类别。故障目录不能替代具体目标 ABI 的启动、加载、异常和业务回归。
Native 调试和 tombstone 还原依赖正确符号、架构与构建产物。Debug Android native code 说明 Android Native 调试和符号条件。调试能力不能当作兼容通过,错误符号也不能支持引用所有权归因。
依赖真实 Android 运行时与组件语义的行为应通过设备端测试验证。Android instrumented tests 说明设备端 instrumented test 的适用范围。单一设备通过不能代表完整 API、ABI 和厂商矩阵。
进程退出记录可提供退出原因、ANR trace,并在部分新版本条件下提供 Native tombstone。Android ApplicationExitInfo 描述可查询的进程退出信息。退出记录必须绑定同一版本、时间窗和用户路径,不能单独证明 JNI 引用是根因。
安全发布应保留来源、构建、验证、变更和供应链证据。NIST SP 800-218 SSDF 给出组织级安全软件开发实践。SSDF 不定义御盾或任何具体 App 加固产品的功能与效果。
未知删除、重复创建、Local 跨 scope、弱引用未提升使用和 owner 结束后的残留应阻断发布。工程判断:这些状态使引用所有权或使用有效性无法得到唯一解释。事件审计只覆盖已执行且已记录路径,静态通过或账本通过都不能外推到未覆盖代码。

工程常见问题

Native 方法返回时 Local Reference 会释放,为什么还要手动检查?

方法返回前的长循环可能积累大量 Local Reference,异步保存还可能让引用跨越有效 scope。要检查每轮释放、本地帧、早退和保存路径,不能只依赖最终返回。

只要 NewGlobalRef 与 DeleteGlobalRef 数量相等就算通过吗?

不算。总数相等可能掩盖删除了错误 refId、重复创建、释放后使用或 owner 已结束但引用仍存活。门禁要按每个 refId 和所有者检查状态迁移。

Weak Global Reference 非空是否说明 Java 对象仍然可用?

不能这样判断。弱引用不阻止对象回收,使用前应按 JNI 语义取得本次调用可用的临时引用,并在失败时走未命中或取消路径。

使用 C++ RAII 是否能彻底避免 JNI 引用泄漏?

RAII 能集中清理责任,但包装器仍要处理正确线程、JNI 环境、移动语义、异常和 owner 寿命。错误的所有权模型不会因为析构函数存在而自动正确。

事件审计脚本通过后能否直接发布加固候选?

不能。脚本只检查输入账本中已执行路径的状态一致性,还需核对静态入口、目标 ABI 设备回归、候选与符号身份、异常和退出记录。

申请 JNI 引用生命周期兼容评估需要准备哪些材料?

准备未加固基线与加固候选摘要、发布 ABI、源码调用点清单、抽象事件账本、设备矩阵、符号映射、异常和退出回执,再通过御盾中央平台提交申请。

想用自己的 App 验证?

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

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