先看结论与判断条件

  • 分别记录 linker 映射、ELF 构造、Java 显式加载、JNI_OnLoad、JNI 注册和业务 ready,禁止用一个 loaded 布尔值代表全部阶段。
  • DT_NEEDED 说明静态依赖关系,但不能直接推导所有库都会执行 JNI_OnLoad;需要显式声明哪些库由 VM 加载并期望回调。
  • 初始化契约以稳定库 ID、事件、前置依赖、线程、幂等、失败传播和重试政策表达,不依赖偶然的源码或日志顺序。
  • 加固前后使用同一 APK、ABI、进程入口和设备条件采集事件,比较缺失、逆序、重复、失败和未就绪调用,而不是只比较启动是否崩溃。
  • linker namespace、缺失符号、STL 与异常是初始化失败的相邻原因,应分层诊断,但本文不重复完整依赖图和 namespace 专项。
  • 静态依赖与运行事件都通过后,仍要覆盖冷启动、并发首次调用、进程恢复、错误注入和目标 API/ABI 设备,结论绑定真实候选。

初始化顺序至少包含五种不同事件

多个 Native 库启动时,经常把 System.loadLibrary 调用顺序当成全部顺序。实际需要区分 linker 映射依赖、ELF 初始化函数、Java 或 Kotlin 的显式加载、VM 对目标库调用 JNI_OnLoad、Native 注册完成,以及业务组件可以安全调用的 ready 事件。它们发生在相关但不相同的边界,日志里出现库名不代表所有阶段完成。

例如主库的 DT_NEEDED 可以让依赖库由 linker 映射并执行其 ELF 初始化,但这不等于依赖库一定获得独立 JNI_OnLoad 回调。JNI_OnLoad 与 VM 装载入口、JavaVM 和类加载环境有关。若依赖库必须注册 JNI,就应有明确显式加载或由主库统一注册的契约,不能依赖某次设备上的偶然回调顺序。

业务 ready 应是项目定义的最终门槛:必要依赖已映射、所需构造完成、JNI 方法注册成功、配置和线程前置满足、失败状态可见。上层只能在 ready 后调用核心接口;加载失败或注册失败时保持失败关闭,不把 loaded=true 提前写入缓存。这样加固导致某一阶段变化时,可以指出具体边界。

Native 初始化事件的责任差异
事件执行者表示什么不能表示
mapped动态 linker库进入进程映射ELF 构造成功
constructor_completeELF 初始化流程本库构造完成JNI 已注册
load_success显式加载调用者System.loadLibrary 或等价入口返回业务状态就绪
jni_onload_successVM 与目标库JNI_OnLoad 成功返回所有业务依赖已初始化
native_ready项目初始化器声明的业务前置完成未测试线程都安全
load_failure任一加载阶段当前候选未完成契约可继续调用核心能力

静态图只描述依赖输入,不等于运行时间线

GNU readelf 可以读取 dynamic section、符号、重定位和其他 ELF 信息。用它从最终 SO 提取 DT_NEEDED,可以建立库级静态边:某库声明需要哪些依赖。该图适合发现缺库、循环设计、第三方来源和加固前后依赖变化,但静态字段不能告诉测试人员某个业务入口何时调用 System.loadLibrary,也不能证明构造和 JNI 回调成功。

静态图必须来自最终 APK 或设备 split 中的每个 ABI SO,记录 SO SHA-256、APK SHA-256、Build ID、Machine、库名、DT_NEEDED、运行路径和来源。中间 build 目录可能包含未 strip、未加固或未打包版本,不能替代最终候选。更完整的库级依赖建模可参考同站 [DT_NEEDED 依赖图](/zh-cn/articles/elf-dt-needed-dependency-graph/),本页只把图作为顺序契约输入。

运行时间线由设备事件产生。至少记录 sequence、单调时钟、进程、线程、libraryId、event、result 和 candidateId。多线程并发时,时间戳相近不等于存在先后保证;只有同步、依赖或显式契约才能建立 happens-before。报告把“观察到先发生”和“必须先发生”分开,避免一次日志顺序被永久固化为不必要约束。

静态依赖图与运行时间线的字段
字段静态图运行时间线用途
candidateIdAPK 与 SO 摘要同一候选标识防止证据串包
libraryId稳定库身份事件所属库连接两类证据
neededDT_NEEDED 边映射现场可观测解释 linker 依赖
event不适用mapped、JNI 或 ready确定具体阶段
sequence/thread不适用顺序与执行线程识别并发和逆序
source构建与供应方设备与测试入口限定证据来源

初始化契约写必须先后,而不是写理想日志

契约由若干必要关系组成,每条关系写 beforeEvent、afterEvent、reason、owner、failurePolicy 和 testEntry。例如 support:mapped 必须先于 engine:load_success,engine:jni_onload_success 必须先于 engine:native_ready。只有业务或平台要求的关系进入契约;没有同步依据的并行构造只记录观察,不强行排序。

每个库还声明 explicitLoad、jniOnLoadExpected 和 readyOwner。显式加载库若期望 JNI_OnLoad,运行时间线必须出现成功事件;仅作为 DT_NEEDED 依赖且不拥有 JNI 注册的库可以不期待 JNI_OnLoad。这个字段避免测试器误把所有依赖都要求同一种回调,也能发现某库从显式加载改成依赖加载后丢失注册。

失败政策必须具体。某依赖失败是否阻断整个 Native 子系统,是否允许非相关功能继续,能否重试,重试是否需要新进程,用户看到什么,以及日志怎样关联,都要写清。初始化不是普通幂等函数时,重复调用可能造成二次注册、重复线程或状态污染;契约为每个阶段标注 once、idempotent 或 retry-new-process,测试据此判定。

初始化契约的最小字段
字段问题示例值缺失后果
before/after哪两个事件必须排序mapped 到 load_success无法判定逆序
reason为何需要顺序依赖符号或注册表偶然日志变契约
jniOnLoadExpectedVM 是否应调用回调true 或 false依赖库被误判
threadPolicy在哪个线程初始化loader、main 或 worker线程差异被忽略
retryPolicy失败后怎样再次尝试never 或 new-process重复初始化污染
testEntry如何从真实业务触发启动、SDK 或功能页只有人工日志

JNI_OnLoad 只承担小而确定的注册工作

Android JNI tips 讨论 JNI 注册、线程、引用、异常和类加载边界。JNI_OnLoad 常用于取得 JavaVM、执行 RegisterNatives 或缓存经过审阅的 ID,但不适合塞入长耗时网络、复杂业务、不可控锁等待或依赖 UI 的工作。回调返回失败时,加载结果必须向上层传播,不能记录警告后继续调用未注册方法。

类查找与类加载器需要明确。由 Java 或 Kotlin 触发加载时,JNI_OnLoad 所处上下文与任意 Native 工作线程不同;后台线程直接 FindClass 可能使用不同类加载边界。契约记录注册类、签名、加载入口和线程,设备测试从正式入口触发。缓存引用还要区分局部与全局生命周期,避免初始化顺序问题被引用失效伪装。

异常同样是初始化结果。每次 JNI 调用后按项目契约检查异常,注册失败保留首个错误并停止后续依赖初始化。清理只能释放当前阶段已拥有的资源,不删除其他库的全局状态。JNI 建议能指导设计,但不能证明加固后 ABI、寄存器、异常或线程行为已经保持,仍需同候选运行回归。

JNI_OnLoad 的可接受职责
职责适合程度关键断言失败动作
保存 JavaVM常见指针有效且只初始化一次返回失败
RegisterNatives常见类、签名和方法完整停止依赖 ready
缓存全局引用逐项评审创建、所有权和释放明确清理当前所有权
启动后台线程谨慎线程与退出契约独立不得半初始化继续
读取远端配置不适合移出装载临界路径由业务初始化负责
调用其他未就绪库禁止隐式必须有显式 before/after阻断并修契约

namespace、符号和运行库错误要从顺序问题中拆开

AOSP linker namespace 文档说明 Android 动态链接器按调用者 namespace 处理 DT_NEEDED、dlopen、搜索路径和允许库。库未映射可能是 namespace 不可达、路径错误或依赖缺失,不一定是 Java 加载顺序错。顺序回归记录调用者、目标库名、namespace 现场和 linker 错误,把可达性问题转交专项,不通过多调用几次 loadLibrary 掩盖。

Android NDK common problems 汇总 API level、缺失符号、STL、异常类型和错误依赖装载等故障。某库在 JNI_OnLoad 前失败,先看 linker 首个错误和缺失符号;构造完成后崩溃,再看构造依赖与线程;JNI 注册失败则看类、签名和异常。使用最早失败阶段路由,比把所有 Native 启动问题称为初始化顺序更准确。

加固前后对比时保持 APK、ABI、设备 API、安装集合和入口一致。若调整加载顺序后现象消失,要恢复原顺序复现或补同步与依赖证据,证明因果。永久添加 sleep、重复 loadLibrary、捕获所有 Throwable 或忽略 JNI_OnLoad 返回,会让问题暂时不可见,却破坏错误传播和可维护性。

加载失败按最早阶段路由
最早失败优先检查顺序证据错误修复
找不到目标库打包、路径和 namespace显式 load 入口重复 loadLibrary
缺少依赖或符号DT_NEEDED、API 和提供者mapped 前置是否满足改变 Java 调用延时
ELF 构造崩溃构造依赖、锁和全局状态constructor 事件吞掉退出
JNI_OnLoad 失败类、签名、异常与线程显式加载和回调继续标记 ready
ready 前业务调用门控与并发入口before/after 契约加固定 sleep
重试后异常幂等与部分清理retryPolicy无限循环重试

用静态图和设备事件生成顺序差异表

自动门禁接收静态图和运行事件两个 JSON。静态图列出 libraryId、needed、jniOnLoadExpected 与 orderContracts;运行事件列出 sequence、libraryId 和 event。脚本先验证库身份、依赖引用、事件类型和序号唯一,再检查每条 DT_NEEDED 依赖是否在依赖者 load_success 前出现 mapped,并检查显式 before/after 契约。

下面 Python 示例不加载任何 SO、不修改产物,也不包含真实库名。它不会从 DT_NEEDED 推导依赖库一定执行 JNI_OnLoad;只有 jniOnLoadExpected=true 才要求该事件。它还阻断运行日志中的未知库、重复关键事件、load_failure 和逆序契约,最后输出 Markdown 差异表,便于把静态关系、运行序号与状态放进发布审阅。

事件采集器应在测试构建中使用稳定、低侵入的标记,不能把敏感符号、密钥或用户数据写入日志。sequence 由单一进程内记录器分配,跨进程分别建表。若事件丢失,结论是证据不足而不是默认成功。真实项目还要绑定候选摘要、进程、线程和单调时间,并保留原始日志。

  • 静态图和运行事件使用同一候选 ID
  • 每个 libraryId 与 sequence 必须唯一
  • DT_NEEDED 只检查映射前置,不推导 JNI 回调
  • JNI_OnLoad 仅按显式 expected 字段要求
  • 缺失、逆序、重复和 load_failure 均阻断
  • 差异表与原始事件日志一起归档
比较静态依赖与运行初始化事件并输出差异表
from pathlib import Path
import json
import sys

if len(sys.argv) != 3:
    raise SystemExit("usage: compare_init_order.py static-graph.json runtime-events.json")

graph_file = Path(sys.argv[1])
events_file = Path(sys.argv[2])
for required in (graph_file, events_file):
    if not required.is_file():
        raise SystemExit(f"missing input file: {required}")

graph = json.loads(graph_file.read_text(encoding="utf-8"))
runtime = json.loads(events_file.read_text(encoding="utf-8"))
libraries = graph.get("libraries")
events = runtime.get("events")
if not isinstance(libraries, list) or not isinstance(events, list):
    raise SystemExit("libraries and events arrays are required")

known_events = {
    "mapped", "constructor_complete", "load_success",
    "jni_onload_success", "native_ready", "load_failure"
}
lib_by_id = {}
failures = []
for item in libraries:
    library_id = item.get("libraryId")
    if not library_id or library_id in lib_by_id:
        failures.append(f"invalid or duplicate libraryId: {library_id!r}")
    else:
        lib_by_id[library_id] = item

positions = {}
seen_sequences = set()
for item in events:
    sequence = item.get("sequence")
    library_id = item.get("libraryId")
    event = item.get("event")
    if not isinstance(sequence, int) or sequence in seen_sequences:
        failures.append(f"invalid or duplicate sequence: {sequence!r}")
        continue
    seen_sequences.add(sequence)
    if library_id not in lib_by_id or event not in known_events:
        failures.append(f"unknown runtime event: {library_id}:{event}")
        continue
    key = (library_id, event)
    if key in positions:
        failures.append(f"duplicate critical event: {library_id}:{event}")
    else:
        positions[key] = sequence
    if event == "load_failure":
        failures.append(f"runtime load failure recorded for {library_id}")

rows = []
def compare(before, after, basis):
    before_seq = positions.get(tuple(before))
    after_seq = positions.get(tuple(after))
    status = "pass"
    if before_seq is None or after_seq is None:
        status = "missing"
    elif before_seq >= after_seq:
        status = "reversed"
    rows.append((basis, ":".join(before), before_seq, ":".join(after), after_seq, status))
    if status != "pass":
        failures.append(f"{basis}: {status}")

for library_id, item in lib_by_id.items():
    for dependency in item.get("needed", []):
        if dependency not in lib_by_id:
            failures.append(f"{library_id}: unknown dependency {dependency}")
            continue
        compare((dependency, "mapped"), (library_id, "load_success"), "DT_NEEDED")
    if item.get("jniOnLoadExpected") is True:
        compare((library_id, "load_success"), (library_id, "jni_onload_success"), "JNI_OnLoad")

for contract in graph.get("orderContracts", []):
    compare(contract.get("before", []), contract.get("after", []), contract.get("reason", "contract"))

print("| basis | before | before_seq | after | after_seq | status |")
print("|---|---|---:|---|---:|---|")
for row in rows:
    print(f"| {row[0]} | {row[1]} | {row[2]} | {row[3]} | {row[4]} | {row[5]} |")

if failures:
    for failure in failures:
        print(failure, file=sys.stderr)
    raise SystemExit(2)

冷启动、并发和进程恢复要分别回归

Android instrumented tests 运行在设备或模拟器上,适合从 Application、Activity、Service、Worker 或 SDK 真实入口触发 Native 初始化。冷启动验证第一次完整时间线,热路径验证已就绪状态不重复构造或注册,并发首次调用验证只有一个所有者执行初始化、其他调用者等待或得到明确失败。直接调用测试辅助初始化函数不能替代正式入口。

进程死亡后所有进程内 Native 状态都会重建,磁盘或服务端标记却可能保留。测试杀进程后从通知、深链、后台任务和普通启动恢复,确认每个进程重新形成自己的事件序列,旧进程的 ready 不被复用。多进程应用为每个进程维护独立契约;主进程通过不代表 remote service 进程。

失败注入使用公开安全条件,例如让测试适配器返回注册失败、缺少可选能力或拒绝业务配置,不删除系统库、不篡改设备。验证失败传播、部分资源清理、用户回退和下一次允许的重试。单一设备通过不能代表全部 API、ABI 和厂商,矩阵按实际 minSdk、目标 ABI 和加载入口扩展。

初始化顺序的运行矩阵
场景起始状态关键断言主要风险
冷启动新进程无 Native 状态完整事件按契约到 ready缺失或逆序
重复进入已 ready不重复非幂等初始化二次注册和线程
并发首次调用多个入口同时触发单一所有者与明确等待竞态和半初始化
失败注入指定阶段返回失败停止依赖调用并回退失败后继续执行
进程恢复旧进程已退出新进程重新建立状态复用旧 ready
多进程各进程独立加载每个进程有完整回执主进程结果外推

退出证据和首个失败阶段决定发布结论

Android ApplicationExitInfo 可以提供进程退出原因等信息,并在支持版本和条件下提供 ANR trace 或 Native tombstone 相关输入。退出记录要按包版本、进程、时间窗和用户路径与初始化事件关联,不能看到一次 Native crash 就自动归因到 JNI_OnLoad。低版本或没有可用 trace 时明确证据缺口,结合日志和同候选复现。

定位按最早失败阶段进行:未 mapped 查打包与 namespace,构造前缺符号查依赖和 API,constructor 失败查全局状态与锁,JNI_OnLoad 失败查类、签名、异常和线程,ready 前业务调用查门控与并发。修复一次只改变一个变量并生成新候选;如果顺序调整有效,恢复旧变量复现或补同步证据。

放行结论应限定为指定 APK 和 SO 摘要在列出的 API、ABI、进程与入口中,静态依赖和运行事件满足声明契约,并且失败和恢复路径完成回归。不能写成初始化永久无竞态、所有设备兼容或加固不影响任何库。准备评估时,可整理最终 APK、DT_NEEDED 图、显式加载清单、JNI_OnLoad 注册、事件日志、退出证据和设备矩阵,再通过御盾中央平台提交申请。

发布证据包的最小组成
证据绑定字段支持的判断不能外推
最终候选APK、SO、ABI 和 Build ID检查对象唯一同名文件相同
静态图readelf 与最终 SODT_NEEDED 关系运行时间线
顺序契约版本、所有者和入口必要先后关系所有事件都串行
运行事件设备、进程和候选列出阶段实际顺序未测设备结果
退出证据版本、时间窗和路径首个失败辅助定位单条记录给出根因
放行记录通过、失败和未执行当前矩阵结论永久兼容或零风险

事实依据与适用边界

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

本文判断事实或工程依据适用限制
JNI 注册、线程、引用、异常和类加载边界会影响 Native 桥接稳定性。Android JNI tips 描述相关实践与限制。JNI 建议不能证明某个加固 SO 保持 ABI、线程和异常语义。
readelf 可以检查 ELF dynamic section、符号、重定位和其他结构。GNU readelf 描述对应选项和输出。DT_NEEDED 与静态字段不能证明动态加载、构造和 JNI_OnLoad 成功。
Android linker 会按调用者 namespace 处理 DT_NEEDED、dlopen、搜索路径和允许库。AOSP linker namespace 描述 namespace 解析与隔离模型。VNDK 文档不能替代普通 App 进程、目标设备和实际候选的装载测试。
Native 常见故障包括 API level、缺失符号、STL、异常类型和错误依赖装载。Android NDK common problems 汇总相关问题与诊断方向。问题清单只能路由首个失败,不能替代目标 ABI 的运行回归。
依赖 Android linker、JNI、组件和系统 API 的语义适合设备端测试。Android instrumented tests 描述在设备或模拟器上访问 Android 框架的测试。单一设备通过不能代表全部 API、ABI、进程和厂商环境。
ApplicationExitInfo 可以提供进程退出原因,并在支持条件下提供相关 trace 输入。Android ApplicationExitInfo API 描述退出信息、trace 与 Native tombstone 相关能力。退出记录仍需按候选版本、进程、时间窗和用户路径关联,不能单独证明根因。
初始化契约应分开 DT_NEEDED 映射、构造、显式加载、JNI_OnLoad 和业务 ready。工程判断:这些事件由不同执行者触发,失败语义和可观测证据不同。具体 before/after 关系必须来自项目依赖、同步和业务要求,不能从一次日志猜测。
静态依赖、顺序契约、运行事件和退出证据必须绑定同一候选身份。工程判断:只有同候选证据才能排除重构建、ABI 替换和旧日志造成的漂移。证据绑定完整仍不能外推未执行设备或证明初始化永远没有竞态。

工程常见问题

System.loadLibrary 的代码顺序就是 Native 初始化顺序吗?

不是全部。还要区分 linker 映射、ELF 构造、JNI_OnLoad、注册完成和业务 ready,并考虑依赖与并发。

DT_NEEDED 依赖库是否一定会执行 JNI_OnLoad?

不能这样推导。DT_NEEDED 描述 linker 依赖;是否期望 JNI_OnLoad 要按 VM 的显式加载入口和项目注册契约单独声明。

为什么加载成功后还要定义 native_ready?

load 返回或 JNI_OnLoad 成功不一定代表配置、依赖和业务初始化完成。ready 提供上层可调用的唯一门槛。

添加 sleep 能否修复初始化逆序?

不能作为正式修复。sleep 没有建立依赖和同步保证,应明确 before/after 契约、所有者、失败传播和真实同步。

一次 Native 崩溃能否说明 JNI_OnLoad 有问题?

不能。要把退出记录与同候选事件序列关联,先确定最早失败在映射、构造、注册、ready 还是业务调用。

申请 Native 初始化顺序评估要准备什么?

准备最终 APK 与 SO、DT_NEEDED 静态图、显式加载清单、JNI_OnLoad 注册、顺序契约、设备事件和退出证据,再通过御盾中央平台提交申请。

想用自己的 App 验证?

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

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