先看结论与判断条件

  • DirectByteBuffer 的 Java 对象、Native 地址和内存所有者是三项不同状态,保持对象可达不等于阻止生产库提前释放底层内存。
  • 由 Java allocateDirect 创建的内存与 Native 创建后 NewDirectByteBuffer 包装的内存采用不同所有权模型,释放责任不能混用。
  • 每次 JNI 访问都要重新取得直接地址和容量,显式校验 offset、length 与溢出,不能把 Java position、limit 当作 Native 边界。
  • 异步任务需要 OPEN、CLOSING、CLOSED 状态和在途租约计数,close 先阻止新任务,最终释放只能在最后一个租约归还后发生。
  • 跨线程保存 ByteBuffer 需要 Java 强引用或 JNI GlobalRef,但引用只保护对象可达性,底层 Native 所有者仍须独立保活。
  • 进程死亡或设备重启后 jobject、地址和句柄都无效,WorkManager 只能持久化文件、业务 ID 或重建参数,不能持久化指针。

DirectByteBuffer 不是自动内存所有权协议

JNI 可以把一段 Native 地址包装成 DirectByteBuffer,也可以从直接缓冲区取得地址与容量。这个对象提供零拷贝视图,却不自动说明内存由 Java、Native 生产库、映射文件还是设备驱动拥有。Java 侧持有 ByteBuffer 强引用,只能保证这个 Java 对象仍可达;若生产库已经 free 或 unmap 底层区域,后续 get、put 或 Native 访问仍会落到失效地址。

相反,Native 保存从 GetDirectBufferAddress 取得的指针,也不能保证 Java 侧缓冲区永远存活。若内存由 ByteBuffer.allocateDirect 管理,Native 不应自行 free;若由 Native 分配并通过 NewDirectByteBuffer 暴露,则生产库必须定义何时释放。接口必须明确 allocator、owner、capacity、有效期和关闭动作,不能从 `isDirect()` 或类名推断。

本文只回答 DirectByteBuffer 跨异步任务的地址与生命周期契约,不重复一般 JNI LocalRef 与 GlobalRef 教程。没有最终候选、目标 ABI 和设备回执时,不能宣称某个缓冲区不会泄漏或越界。Java 可达性、Native 内存存活、异步任务在途和进程生命周期是四层独立证据。

DirectByteBuffer 生命周期中的四类对象
对象主要所有者有效期错误假设
Java ByteBufferJava 引用图强引用或 GlobalRef 存在对象可达即内存有效
Native 地址分配或映射生产者直到对应 release拿到指针即可永久保存
异步任务任务调度与业务层从租约取得到归还排队即已保活内存
JNI 局部引用当前 JNI 调用局部引用帧可跨线程长期使用
JNI 全局引用Native 显式管理DeleteGlobalRef 前能阻止生产库释放内存
进程地址空间操作系统进程存活期间重启后地址仍可恢复

先确定缓冲区由 Java 还是 Native 创建

Java 使用 ByteBuffer.allocateDirect 创建时,缓冲区对象与其直接内存由运行时约定管理。Native 可以在一次受控调用期间读取地址并操作,但不应把它交给 free、delete 或自定义池。异步使用需要 Java 层保持强引用,Native 侧如果跨 JNI 调用保存对象则建立 GlobalRef,并在工作完成后删除;地址缓存的有效期必须与该对象所有权一致。

Native 先分配内存再调用 NewDirectByteBuffer 时,Java 对象通常只是视图,生产库仍是实际 owner。接口应返回不透明 handle 和 ByteBuffer 视图,close 通过 handle 回到同一生产库销毁。不能只把 ByteBuffer 返回 Java 而丢失 handle,也不能依赖 ByteBuffer 被垃圾回收自动调用生产库 free。若使用映射文件或设备缓冲,还要调用相应 unmap 或 release。

混合模型最危险,例如 Java 包装层以为 Cleaner 会释放,而 Native 也在异步完成后释放;或者 Java allocateDirect 创建的地址被 Native 误用自定义 free。发布前应为每个入口登记 creation_model、owner、release_api 和 async_policy。任何记录缺少所有者或同时出现两个释放者,都应在代码进入设备测试前阻断。

  • 每个 DirectByteBuffer 入口标记 Java 或 Native 创建模型
  • Java 创建的直接内存不会被 Native 自行 free
  • Native 创建的内存保留不透明 owner handle
  • 映射与设备缓冲使用各自专用 release
  • 同一缓冲区只有一个最终释放责任人
  • 对象可达策略与底层内存所有权分别记录

地址、容量和区间必须在每次调用时校验

Native 接收 ByteBuffer 后,先确认它是直接缓冲区,再调用 GetDirectBufferAddress 与 GetDirectBufferCapacity。地址为空、容量异常或与句柄登记容量不一致都应立即失败。Java 的 position、limit 和 mark 属于 ByteBuffer 视图状态,Native 直接拿基址时不会自动受到这些字段保护,因此跨边界必须显式传递 offset 与 length,或者传入已经切片且容量明确的视图。

边界检查要防整数溢出。不能只判断 `offset + length <= capacity`,因为加法可能溢出;应先确认 offset 和 length 非负,再判断 offset 不大于 capacity 且 length 不大于 capacity 减 offset。Native 长度类型、Java int 或 long 和 JNI jlong 转换也要有范围检查。零长度是否允许、对齐是否必须、只读缓冲能否写入,都应写入版本化协议。

容量还要与业务语义分开。capacity 表示可访问字节上限,不代表其中所有字节已经初始化,也不代表消息长度。接口应另传 valid_length、format_version 和 operation_id,Native 只读取已初始化范围。若异步生产者和消费者并发修改长度或内容,需要锁、原子状态或单生产者单消费者协议,不能用 DirectByteBuffer 自身代替内存同步。

缓冲区区间校验的字段责任
字段来源校验失败动作
addressGetDirectBufferAddress非空且与 owner 对应拒绝访问
capacityGetDirectBufferCapacity非负且等于登记容量关闭任务并记录
offset调用协议非负且不大于容量返回范围错误
length调用协议不大于 capacity-offset禁止指针运算
valid_length生产者状态不大于 capacity拒绝读取未初始化区
format_version版本化契约属于支持集合受控升级或失败

异步访问需要租约状态机

异步任务不能只捕获一个 long 地址。owner 应维护 OPEN、CLOSING 和 CLOSED 状态,以及当前 in_flight 计数。任务开始时先确认 OPEN,再原子增加计数并再次确认状态;成功取得租约后才读取 handle 与缓冲区。close 将状态改为 CLOSING,禁止新租约,但若仍有在途任务就延迟实际释放;最后一个租约归还时,生产库销毁 handle 并进入 CLOSED。

租约必须覆盖真实 Native 使用期,而不是只覆盖 enqueue 或 Java 方法返回。若任务把指针交给 Native 线程、GPU、编解码器或异步 I/O,只有底层完成回调到达并停止访问后才能归还。超时不等于底层已停止,取消接口需要明确是请求取消还是已确认取消。close 若需要等待,应避免阻塞主线程,并提供可观察的关闭完成状态。

异常路径与正常路径使用同一归还逻辑。Java 可用 try-with-resources,Kotlin 可用 finally,Native 可用 RAII,但不能让两层都独立释放底层内存。租约 close 只减少 in_flight,owner close 才触发最终 release。重复归还应被包装层阻止,未知 lease ID 和状态漂移要记录为契约错误,不应继续访问。

异步缓冲区状态转换
当前状态事件允许动作最终状态
OPENacquirein_flight 加一并返回租约OPEN
OPENclose阻止新任务CLOSING
CLOSINGacquire拒绝CLOSING
CLOSINGlease release计数减一CLOSING 或 CLOSED
CLOSINGin_flight 归零生产库 releaseCLOSED
CLOSEDread/write/close拒绝访问或幂等关闭CLOSED

JNI 引用、线程和异常仍要独立管理

跨异步 JNI 调用保存 ByteBuffer 对象时,局部引用不能直接留到另一个线程或调用后。Native 需要 NewGlobalRef 保持 Java 对象可达,在工作完成后由有效 JNIEnv 调用 DeleteGlobalRef。JNIEnv 本身不能跨线程缓存,原生线程通过 JavaVM 取得自己的接口。GlobalRef 与 owner handle 应在同一状态机里创建和释放,但两者承担不同责任。

每次 NewGlobalRef、GetDirectBufferAddress、GetDirectBufferCapacity 和 Java 回调后都要检查异常与返回值。若创建全局引用成功但 Native 任务初始化失败,要立即删除引用并归还 owner 租约;若底层任务已启动而 Java 回调失败,仍要等待底层停止后释放。不能在挂起异常状态下继续调用 JNI,也不能因为 ExceptionClear 就忽略资源责任。

C++ 异常、RTTI 和 libc++ 链接方式取决于构建系统和运行库选择。若异步 Native 实现跨 SO 传递 C++ 对象或异常,内存所有权会进一步复杂。公共 JNI 边界应捕获异常、转换稳定状态码,并由同一生产库释放句柄。启用异常或共享运行库不能证明 unwind、type_info 和所有清理路径在目标候选上正确。

  • 跨调用 ByteBuffer 使用 GlobalRef 而非 LocalRef
  • JNIEnv 每线程获取且不跨线程保存
  • GlobalRef 和 Native owner 在同一租约状态机登记
  • 每个 JNI 调用后检查异常与空返回
  • 初始化失败会同时清理引用和租约
  • C++ 异常在 JNI 边界内转换成稳定状态码

持久化任务不能保存地址或 jobject

WorkManager 的持久化调度可以跨进程退出和设备重启保留工作,并受约束、重试和系统配额影响。这正说明 DirectByteBuffer 地址不能作为 Data 或数据库字段持久化:进程结束后原地址空间消失,jobject、GlobalRef 和 Native handle 都失效。持久化任务只能保存文件路径、内容摘要、业务对象 ID 或重建参数,执行时重新创建 owner 和缓冲区。

进程死亡还可能发生在 Native 操作中间。若业务副作用需要恢复,应把可提交状态写到文件或服务端事务,而不是依赖缓冲区中的未提交字节。新进程启动后先检查持久化状态,再决定重建、回滚或重新下载。WorkManager 入队成功不等于原内存继续存活,也不保证任务按准确时间恢复。

ApplicationExitInfo 可以补充进程退出原因、ANR trace,并在较新版本提供 Native tombstone。诊断 use-after-free 或异步崩溃时,应把退出记录与同一 buildId、ABI、时间窗、owner 状态和用户路径关联。退出 reason 或 tombstone 中出现内存函数不能单独证明 DirectByteBuffer 契约是根因,仍需同候选复现与最小变量对照。

  • WorkManager Data 不保存地址、jobject 或 Native handle
  • 持久化输入仅包含可重建标识和内容摘要
  • 进程退出后的任务重新创建 owner 与缓冲区
  • 中间副作用写入可恢复文件或服务端事务
  • ApplicationExitInfo 与 buildId、ABI 和时间窗关联
  • 退出原因不会被直接当作所有权根因

用显式 owner 与租约封装异步访问

下面的 Java 示例展示 Native 分配缓冲区的调用方契约。create 返回不透明 handle,view 返回 DirectByteBuffer,owner 保存容量、状态和在途计数。acquire 先增加计数再确认关闭状态,返回只读 lease;owner.close 只标记 CLOSING,最后一个 lease 归还后才调用同一生产库的 nativeRelease。代码不记录真实地址,也不把 handle 持久化。

示例用同步方法和 AtomicLong 控制关闭,便于公开阅读;生产实现可以采用更严格的锁或原子状态,但必须证明竞态等价。Lease 只在其生命周期内暴露 duplicate 视图,调用者不得把视图继续传到 lease 之外。main 使用直接输入检查容量,并验证 close 后 acquire 的失败路径;Native 方法本身需要由设备测试桩提供。

Java wrapper 只能约束遵守接口的调用方,Native 实现仍需验证 handle、capacity、offset 和状态,并在 release 内使用原分配器。异步设备 API 若在 lease close 后仍持有指针,包装层无法自动发现,因此完成回调必须代表底层真正停止访问。保护或优化变换后也要重跑同一竞态和错误测试。

用 owner 状态和租约延迟释放 DirectByteBuffer
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicLong;

public final class NativeBufferOwner implements AutoCloseable {
  private enum State { OPEN, CLOSING, CLOSED }
  private final AtomicLong handle;
  private final ByteBuffer view;
  private State state = State.OPEN;
  private int inFlight = 0;

  private static native long nativeCreate(int capacity);
  private static native ByteBuffer nativeView(long handle);
  private static native void nativeRelease(long handle);

  public NativeBufferOwner(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException("capacity must be positive");
    long created = nativeCreate(capacity);
    if (created == 0L) throw new IllegalStateException("native create failed");
    ByteBuffer direct = nativeView(created);
    if (direct == null || !direct.isDirect() || direct.capacity() != capacity) {
      nativeRelease(created);
      throw new IllegalStateException("native view contract failed");
    }
    this.handle = new AtomicLong(created);
    this.view = direct;
  }

  public synchronized Lease acquire(int offset, int length) {
    if (state != State.OPEN) throw new IllegalStateException("owner is closing");
    if (offset < 0 || length < 0 || offset > view.capacity() ||
        length > view.capacity() - offset) {
      throw new IndexOutOfBoundsException("invalid buffer range");
    }
    inFlight += 1;
    ByteBuffer range = view.duplicate();
    range.position(offset).limit(offset + length);
    return new Lease(this, range.slice());
  }

  private synchronized void releaseLease() {
    if (inFlight <= 0) throw new IllegalStateException("lease counter underflow");
    inFlight -= 1;
    tryRelease();
  }

  @Override public synchronized void close() {
    if (state == State.CLOSED) return;
    state = State.CLOSING;
    tryRelease();
  }

  private void tryRelease() {
    if (state == State.CLOSING && inFlight == 0) {
      long owned = handle.getAndSet(0L);
      if (owned != 0L) nativeRelease(owned);
      state = State.CLOSED;
    }
  }

  public static final class Lease implements AutoCloseable {
    private NativeBufferOwner owner;
    private final ByteBuffer buffer;
    private Lease(NativeBufferOwner owner, ByteBuffer buffer) { this.owner = owner; this.buffer = buffer; }
    public ByteBuffer buffer() {
      if (owner == null) throw new IllegalStateException("lease is closed");
      return buffer.duplicate();
    }
    @Override public void close() {
      NativeBufferOwner current = owner;
      if (current != null) { owner = null; current.releaseLease(); }
    }
  }

  public static void main(String[] args) {
    if (args.length != 1 || Integer.parseInt(args[0]) <= 0)
      throw new IllegalArgumentException("usage: positive-capacity");
    NativeBufferOwner owner = new NativeBufferOwner(Integer.parseInt(args[0]));
    try (Lease lease = owner.acquire(0, 1)) { lease.buffer().get(0); }
    owner.close();
    owner.acquire(0, 1);
  }
}

用设备回归验证关闭、重试和进程退出

依赖真实 Android 运行时、JNI、线程和系统 API 的 DirectByteBuffer 语义应通过设备端 instrumented test 验证。矩阵至少覆盖 Java 创建与 Native 创建模型、零长度和边界区间、关闭前后访问、多个并发 lease、close 与 acquire 竞态、底层取消、进程死亡、WorkManager 重建和每个交付 ABI。

NDK 常见故障还包括 API level、缺失符号、STL、异常类型和错误依赖装载。出现 Native 崩溃时,需要同时保存分配、取得地址、任务开始、lease 归还和最终 release 的阶段码,以及符号化 tombstone、build ID 和缓冲区状态。单一 free 帧不能证明根因,内存可能更早越界或被错误线程修改。

完整发布材料包括 creation model、owner 与 release API、容量协议、租约状态机、JNI 引用、C++ 运行库、最终 so 摘要、设备矩阵和退出记录。需要评估实际工程时,可通过御盾中央平台提交脱敏的缓冲区接口、异步调用图、状态转换、build ID 与目标 ABI,先确定所有者,再验证关闭与恢复。

  • Java 与 Native 创建模型分别建立设备用例
  • 范围溢出、关闭后访问和重复 lease 归还有失败回执
  • close 与 acquire 并发不会提前释放底层内存
  • 异步取消完成后才归还最后一个租约
  • 进程死亡后仅用可重建标识恢复任务
  • 每个交付 ABI 的 tombstone 和状态码可关联

事实依据与适用边界

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

本文判断事实或工程依据适用限制
JNI 注册、线程、引用、异常与类加载边界会直接影响 Native 桥接稳定性。Android JNI tips 说明 JNI 线程、引用、异常和 DirectByteBuffer 相关实践。JNI 建议不能证明某个 SO 加固变换保持 ABI 或异步内存寿命。
NDK 兼容故障常见于 API level、缺失符号、STL、异常类型和错误依赖装载。Android NDK common problems 汇总常见 Native 构建与运行问题。故障清单不能替代目标 ABI 的缓冲区、线程和异常回归。
C++ 异常、RTTI 与 libc++ 链接方式取决于构建系统和运行库选择。Android C++ library support 说明 NDK C++ 运行库、异常和 RTTI 支持。启用编译选项不能证明跨 SO 的 unwind、type_info 和清理路径完整。
依赖真实 Android 运行时、组件和系统 API 的行为应通过设备端测试验证。Android instrumented tests 说明 instrumented test 适用于真实 Android 环境。单一设备通过不能代表完整 API、ABI 和厂商矩阵。
WorkManager 持久化工作可以跨进程退出和设备重启保留,并受约束、重试和配额影响。Android persistent work 说明可延迟持久化工作的调度与恢复语义。调度保留不意味着原进程地址、jobject、Native handle 或内存内容仍然有效。
ApplicationExitInfo 可以提供进程退出原因、ANR trace,并在较新版本返回 Native tombstone。Android ApplicationExitInfo 说明历史进程退出信息和可用 trace。退出记录仍需与同一版本、时间窗、ABI、状态和用户路径关联。
DirectByteBuffer 对象可达性与底层 Native 内存所有权应分别管理。工程判断:Java 引用只能保护对象,生产库仍可独立 free 或 unmap 底层地址。具体创建模型和释放责任必须由项目接口与最终实现证据确认。
跨异步任务应使用在途租约,并在最后一个租约归还后由原 owner 释放。工程判断:OPEN、CLOSING、CLOSED 与计数协议可以防止新任务和提前释放竞态。状态机无法替代底层设备或线程真实完成回调,也不能修复越界写。

工程常见问题

保持 DirectByteBuffer 的 Java 强引用就能保证地址有效吗?

不一定。强引用只保持 Java 对象可达,若内存由 Native 生产库管理,生产库仍可能提前 free 或 unmap。

GetDirectBufferAddress 返回后可以永久缓存指针吗?

不可以。有效期取决于缓冲区创建模型、owner、Java 引用、异步租约和进程生命周期,应按契约限定。

Native 是否可以 free ByteBuffer.allocateDirect 的地址?

不应这样做。Java 创建模型由运行时管理,Native 只在受控有效期内访问,不能选择自己的释放函数。

为什么 close 后不能立即释放?

可能仍有异步任务持有地址。close 应先阻止新租约,等在途计数归零和底层完成后再由原 owner 释放。

WorkManager 能保存 DirectByteBuffer 供重启后继续用吗?

不能。进程退出后地址和 jobject 失效,只能持久化文件、业务 ID、摘要或重建参数并重新创建缓冲区。

准备 DirectByteBuffer 生命周期排查需要哪些材料?

准备创建模型、owner 和 release API、容量协议、异步调用图、JNI 引用、状态转换、最终 so build ID、目标 ABI 和退出回执。

想用自己的 App 验证?

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

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