源码

java: https://gitee.com/guangyang1314666/springboot-demo/tree/master/demo-modules/demo-javacpp
c++工程: https://gitee.com/guangyang1314666/javacpp-native-cpp

前言

关于JavaCP

JavaCPP 使得Java 应用可以在高效的访问本地C++方法,JavaCPP底层使用了JNI技术,可以广泛的用在Java SE应用中(也包括安卓),以下两个特性是JavaCPP的关键,稍后咱们会用到:

  • 提供一些注解,将Java代码映射为C++代码
  • 提供一个jar,用java -jar命令可以将C++代码转为java应用可以访问的动态链接库文件;
  • 目前JavaCPP团队已经用JavaCPP为多个著名C++项目生成了完整的接口,这意味着咱们的java应用可以很方便的使用这些C++库

    javacpp-1.5.7.jar 下载

    github仓库地址: https://github.com/bytedeco/javacpp
    maven地址: https://mvnrepository.com/artifact/org.bytedeco/javacpp
    我的oss地址: oss://sgy-data-disk/javacpp/

    c++ 工程结构说明

  1. application: 应用层, 主要用于对moudles层的调用, 形成一个业务逻辑体系
    2. build: 存放构建结构
    3. cmake: 存放封装的cmake相关工具
    4. modules: 存放功能模块, Utils, algo等等
    5. samples: 用于存放对modules测试的相关代码, 也可以对application层进行测试
    直接生成的是可执行文件用于验证是否符合预期
    8. build.sh: 用于构建c++工程脚本, 构建结果放到了build目录下

    javacpp如何生成的c++文件

  2. javacpp通过InfoMap来定义java和c++的映射关系的, 叫做preset

preset文件放在什么位置?

  1. 必须放在org.bytedeco.${moduleName}.presets, 包下, 这是javacpp官网的规定

image.png

preset文件长什么样?

  1. 这里以javacpp_example为例

    1. @Properties(
    2. inherit = {javacpp.class},
    3. target = "org.bytedeco.javacppexample.jni",
    4. global = "org.bytedeco.javacppexample.global.javacpp_example_global",
    5. value = {
    6. @Platform(
    7. // 如果c++中有智能指针等, 必须在这定义宏, 而不是在c++文件中定义, 否则会出现意向不到的事情
    8. // 比如编译报错
    9. // 如果你在c++文件中定义 #define SHARED_PTR_NAMESPACE std, 那编译也会报错, 因为javacpp将std
    10. // 当初了整形变量, 定义在XxxxGolbal中了
    11. // 如果你在c++文件中定义#define SHARED_PTR_NAMESPACE, 有可能编译不报错, 但是很有可能出现内存
    12. // 泄露或者程序运行一段时间崩溃的问题
    13. define = {
    14. "SHARED_PTR_NAMESPACE std",
    15. "UNIQUE_PTR_NAMESPACE std"
    16. },
    17. value = {
    18. "linux-x86",
    19. "linux-x86_64",
    20. "macosx-x86_64",
    21. "windows-x86",
    22. "windows-x86_64"
    23. },
    24. include = {"javacpp_example.h"},
    25. link = { "javacpp_example"}
    26. ),
    27. @Platform(value = "android", preload = ""),
    28. @Platform(value = "ios", preload = {"liblibjpeg", "liblibpng", "liblibprotobuf", "liblibwebp", "libzlib", "libopencv_core"}),
    29. @Platform(value = "linux", preloadpath = {"/usr/lib/", "/usr/lib32/", "/usr/lib64/"}),
    30. @Platform(value = "linux-armhf", preloadpath = {"/usr/arm-linux-gnueabihf/lib/", "/usr/lib/arm-linux-gnueabihf/"}),
    31. @Platform(value = "linux-arm64", preloadpath = {"/usr/aarch64-linux-gnu/lib/", "/usr/lib/aarch64-linux-gnu/"}),
    32. @Platform(value = "linux-x86", preloadpath = {"/usr/lib32/", "/usr/lib/"}),
    33. @Platform(value = "linux-x86_64", preloadpath = {"/usr/lib64/", "/usr/lib/"})
    34. }
    35. )
    36. public class javacpp_example implements InfoMapper {
    37. // static { Loader.checkVersion("com.concise", "demo"); }
    38. @Override
    39. public void map(InfoMap infoMap) {
    40. infoMap
    41. // .put(new Info("std::shared_ptr<ImageTrans>").valueTypes("ImageTrans"))
    42. // 1. 如果std::vector中嵌套 std::vector 或者 std::shared_ptr, 最后一个>前要空一格, 否则底层解析会报错
    43. // 错误示范: std::vector<std::shared_ptr<ImageTrans>>
    44. // 正确: std::vector<std::shared_ptr<ImageTrans> >
    45. // 2. std::vector嵌套std::vector或者std::shared_ptr, 则需要单独put一下new Info,
    46. // cppNams = std::shared_ptr<ImageTrans>, pointerTypes指定容器元素类型
    47. // 也即是说: javacpp每次只能处理一个嵌套层级std::vector<...> ,我估计内部是通过循环检查来实现的
    48. // .put(new Info("std::shared_ptr<ImageBuffer>").pointerTypes("ImageBuffer").define())
    49. // 用户传递对象集合数据
    50. .put(new Info("std::shared_ptr<ImageBuffer>")
    51. .annotations("@SharedPtr")
    52. .valueTypes("@Cast({\"\", \"std::shared_ptr<ImageBuffer>\"}) ImageBuffer")
    53. .pointerTypes("ImageBuffer"))
    54. // .put(new Info("std::shared_ptr<ImageBuffer>&")
    55. // .annotations("@UniquePtr", "@Const")
    56. // .valueTypes("@Cast({\"\", \"std::shared_ptr<ImageBuffer>&\"}) ImageBuffer")
    57. // .pointerTypes("ImageBuffer"))
    58. // > > 之间一定要有一个空格, 且要加上.define()
    59. .put(new Info("std::vector<std::shared_ptr<ImageBuffer> >").pointerTypes("ImageBufferVector").define())
    60. // 用于传递字符串集合数据
    61. .put(new Info("std::vector<std::string>").pointerTypes("StringVector").define())
    62. // .put(new Info("JavaCppExample::getImageData").javaText(
    63. // "public native @ByVal ImageBuffer getImageData();"))
    64. // java methods, 用于java和c++互相回调
    65. .put(new Info("JavaCppExample::callback1").javaText(
    66. "@Virtual public native void callback1(@Cast(\"uint8_t*\") BytePointer img, int length);"))
    67. .put(new Info("JavaCppExample::callback2").javaText(
    68. "@Virtual public native void callback2(@SharedPtr @Cast({\"\", \"std::shared_ptr<ImageBuffer>\"}) ImageBuffer data);"));
    69. }
    70. }

    动态库加载方式

    当我们调用jni接口的时候, 会自动调用 Loader.load(), 内部会通过返回获取到类上@Properties注解指定的preset类

    @NoOffset @Properties(inherit = org.bytedeco.javacppexample.presets.image_buffer.class)
    public class ImageBuffer extends Pointer {
     static { Loader.load(); }
     // ...
    }
    

而javacpp会获取到image_buffer类上的@Properties注解数据, 并从中获取需要加载的动态库, 即如下注解的links属性, 如果指定了多个动态库, 那都将被加,所以, 如果你希望在加载一个类动态库的时候不希望加载另一个类动态库, 那必须单独写一个preset类, 并指定相应的动态库 (动态库分开)

@Properties(
        inherit = {javacpp.class},
        target = "org.bytedeco.javacppexample.jni",
        global = "org.bytedeco.javacppexample.global.image_buffer_global",

        value = {
                @Platform(
                        // 如果c++中有智能指针等, 必须在这定义宏, 而不是在c++文件中定义, 否则会出现意向不到的事情
                        // 比如编译报错
                        // 如果你在c++文件中定义 #define SHARED_PTR_NAMESPACE std, 那编译也会报错, 因为javacpp将std
                        // 当初了整形变量, 定义在XxxxGolbal中了

                        // 如果你在c++文件中定义#define SHARED_PTR_NAMESPACE, 有可能编译不报错, 但是很有可能出现内存
                        // 泄露或者程序运行一段时间崩溃的问题
                        define = {
                                "SHARED_PTR_NAMESPACE std",
                                "UNIQUE_PTR_NAMESPACE std"
                        },
                        value = {
                                "linux-x86",
                                "linux-x86_64",
                                "macosx-x86_64",
                                "windows-x86",
                                "windows-x86_64"
                        },
                        include = {"image_buffer.h"},
                        link = {"jni_utils" }
                ),
                @Platform(value = "android", preload = ""),
                @Platform(value = "ios", preload = {"liblibjpeg", "liblibpng", "liblibprotobuf", "liblibwebp", "libzlib", "libopencv_core"}),
                @Platform(value = "linux",        preloadpath = {"/usr/lib/", "/usr/lib32/", "/usr/lib64/"}),
                @Platform(value = "linux-armhf",  preloadpath = {"/usr/arm-linux-gnueabihf/lib/", "/usr/lib/arm-linux-gnueabihf/"}),
                @Platform(value = "linux-arm64",  preloadpath = {"/usr/aarch64-linux-gnu/lib/", "/usr/lib/aarch64-linux-gnu/"}),
                @Platform(value = "linux-x86",    preloadpath = {"/usr/lib32/", "/usr/lib/"}),
                @Platform(value = "linux-x86_64", preloadpath = {"/usr/lib64/", "/usr/lib/"})
        }
)
public class image_buffer implements InfoMapper {
    @Override
    public void map(InfoMap infoMap) {

    }
}

自动生成jni接口步骤

javacpp是支持根据c++的头文件生成jni接口的, 不需要我们手动编写jni,但是对于一些采用函数指针的方式回调java,需要我们自己编写jni接口, 然后使用javacpp, 通过jni生成c++的头文件的

  1. 编写c++头文件
  2. 在java端编写preset, 映射关系 ===> InfoMap
  3. 执行javacpp-1.5.7.jar命令, 生成jni接口

    代码示例

    请移步到源码工程

    presets文件编写

    presets是一个java文件用于描述java和c++之间的映射关系的

    常用注解

    @Properties(
         inherit = {javacpp.class},
         target = "org.bytedeco.javacppexample.jni",
         global = "org.bytedeco.javacppexample.global.javacpp_example_global",
         value = {
                 @Platform(
                         // 如果c++中有智能指针等, 必须在这定义宏, 而不是在c++文件中定义, 否则会出现意向不到的事情
                         // 比如编译报错
                         // 如果你在c++文件中定义 #define SHARED_PTR_NAMESPACE std, 那编译也会报错, 因为javacpp将std
                         // 当初了整形变量, 定义在XxxxGolbal中了
    
                         // 如果你在c++文件中定义#define SHARED_PTR_NAMESPACE, 有可能编译不报错, 但是很有可能出现内存
                         // 泄露或者程序运行一段时间崩溃的问题
                         define = {
                                 "SHARED_PTR_NAMESPACE std",
                                 "UNIQUE_PTR_NAMESPACE std"
                         },
                         value = {
                                 "linux-arm64",
                                 "linux-x86",
                                 "linux-x86_64",
                                 "macosx-x86_64",
                                 "windows-x86",
                                 "windows-x86_64"
                         },
                         include = {"javacpp_example.h"},
                         link = { "javacpp_example"}
                 ),
                 @Platform(value = "android", preload = ""),
                 @Platform(value = "ios", preload = {"liblibjpeg", "liblibpng", "liblibprotobuf", "liblibwebp", "libzlib", "libopencv_core"}),
                 @Platform(value = "linux",        preloadpath = {"/usr/lib/", "/usr/lib32/", "/usr/lib64/"}),
                 @Platform(value = "linux-armhf",  preloadpath = {"/usr/arm-linux-gnueabihf/lib/", "/usr/lib/arm-linux-gnueabihf/"}),
                 @Platform(value = "linux-arm64",  preloadpath = {"/usr/aarch64-linux-gnu/lib/", "/usr/lib/aarch64-linux-gnu/"}),
                 @Platform(value = "linux-x86",    preloadpath = {"/usr/lib32/", "/usr/lib/"}),
                 @Platform(value = "linux-x86_64", preloadpath = {"/usr/lib64/", "/usr/lib/"})
         }
    )
    public class javacpp_example implements InfoMapper {
     // static { Loader.checkVersion("com.concise", "demo"); }
     @Override
     public void map(InfoMap infoMap) {
    
     }
    }
    

说明:

  • target: 生成的jni类所存放的包名
  • global:生成的全局类所存放的包名, 用于存放非类中的函数, 枚举, 宏定义等非类中内容数据
  • Platform:构建动态库时配置

解决内存泄漏问题

javacpp中生成的jni类和指针类 (Pointer 以及子类), 需要调用 releaseReference() 函数释放引用, 否则会有内存问题

解决jni崩溃问题

c++返回的uchar*导致崩溃

错误现象

image.png

如何复现错误

通过grpc服务端流可以快速复现
proto

syntax = "proto3";

package demo;
option java_multiple_files = true;
option java_package = "com.concise.jni.javacpp.grpc.sdk";
option java_outer_classname = "DemoProto";
option objc_class_prefix = "HLW";

service JavacppDemoService {
  // 以服务端流的形式发送数据 (给客户端发送数据)
  rpc findDemo (FindDemoRequest) returns (stream FindDemoResponse){}
}

message FindDemoResponse {
  string message = 1;
  bytes image = 2;
}

message FindDemoRequest {
  string message = 1;
}

grpc服务端代码

package com.concise.jni.javacpp.grpc;

import cn.hutool.core.io.FileUtil;
import com.concise.jni.javacpp.grpc.sdk.FindDemoRequest;
import com.concise.jni.javacpp.grpc.sdk.FindDemoResponse;
import com.concise.jni.javacpp.grpc.sdk.JavacppDemoServiceGrpc;
import com.google.protobuf.ByteString;
import io.grpc.stub.StreamObserver;
import net.devh.boot.grpc.server.service.GrpcService;
import org.bytedeco.javacpp.BytePointer;
import org.bytedeco.javacppexample.jni.JavaCppExample;
import org.bytedeco.javacppexample.jni.MediaData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author shenguangyang
 * @date 2022-05-15 16:12
 */
@GrpcService
public class JavacppDemoGrpcServer extends JavacppDemoServiceGrpc.JavacppDemoServiceImplBase {
    private static final Logger log = LoggerFactory.getLogger(JavacppDemoGrpcServer.class);
    private static final AtomicInteger requestNum = new AtomicInteger(1);
    private static final String IMAGE_PATH_PRE = "/mnt/project/javacpp-native/images";

    /**
     */
    @Override
    public void findDemo(FindDemoRequest request, StreamObserver<FindDemoResponse> responseObserver) {
        String message = request.getMessage();
        String id = String.valueOf(requestNum.addAndGet(1));
        while (true) {
            JavaCppExample javaCppExample = new JavaCppExample("hjw34er2345".getBytes(StandardCharsets.UTF_8), "测试数据", 34);
            long count = 1;
            while (count++ < 2000) {
                MediaData mediaData = javaCppExample.demo10_1();
                BytePointer imagePointer = mediaData.get_data();
                try {
                    if (imagePointer == null) {
                        TimeUnit.MILLISECONDS.sleep(200);
                        continue;
                    }
                    long length = mediaData.get_length();
                    log.info("id: {}, start read image length: {}", id, length);
                    byte[] image = new byte[(int) length];
                    imagePointer.get(image, 0, (int) length);
                    log.info("id: {}, end read image length: {}", id, length);
                    if (count % 100 == 0) {
                        String imagePath = IMAGE_PATH_PRE + "/demo10-" + id + ".jpg";
                        log.info("id: {}, grpc demo10, count: {}, length: {}", id, count, length);
                        FileUtil.writeBytes(image, imagePath);
                    }
                    FindDemoResponse response = FindDemoResponse.newBuilder()
                            .setImage(ByteString.copyFrom(image)).build();
                    responseObserver.onNext(response);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (imagePointer != null) {
                        imagePointer.releaseReference();
                    }
                    mediaData.releaseReference();
                }

            }
            javaCppExample.releaseReference();
        }
        // responseObserver.onCompleted();
    }
}

客户端代码, 注意, 不调用demo.next()方法可以快速复现崩溃问题

@Component
public class JavacppDemoGrpcClient {
    private static final Logger log = LoggerFactory.getLogger(JavacppDemoGrpcClient.class);
    /**
     * 异步 stub
     */
    @GrpcClient(value = "javacppDemoService")
    JavacppDemoServiceGrpc.JavacppDemoServiceStub asyncStub;

    /**
     * 阻塞模式
     * 拦截器需要在注解里写明
     */
    @GrpcClient(value = "javacppDemoService")
    JavacppDemoServiceGrpc.JavacppDemoServiceBlockingStub blockingStub;

    public void findDemo() throws InterruptedException {
        FindDemoRequest request = FindDemoRequest.newBuilder().build();
        // 接收服务端不断返回的数据
        Iterator<FindDemoResponse> demo = blockingStub.findDemo(request);
        while (demo.hasNext()) {
//            FindDemoResponse next = demo.next();
            // log.info(demo.next().getMessage());
            // TimeUnit.MILLISECONDS.sleep(10);
        }
        log.info("grpc client end ==> findDemo");
    }
}

查找错误位置

查看/mnt/project/javacpp-native/java/jni/hs_err_pid32607.log日志

Stack: [0x00007fb0bd9d2000,0x00007fb0bdad3000],  sp=0x00007fb0bdad1368,  free space=1020k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
C  [libc.so.6+0x18b733]

Java frames: (J=compiled Java code, j=interpreted, Vv=VM code)
j  org.bytedeco.javacpp.BytePointer.get([BII)Lorg/bytedeco/javacpp/BytePointer;+0
j  com.concise.jni.javacpp.grpc.JavacppDemoGrpcServer.findDemo(Lcom/concise/jni/javacpp/grpc/sdk/FindDemoRequest;Lio/grpc/stub/StreamObserver;)V+144
j  com.concise.jni.javacpp.grpc.sdk.JavacppDemoServiceGrpc$MethodHandlers.invoke(Ljava/lang/Object;Lio/grpc/stub/StreamObserver;)V+33
j  io.grpc.stub.ServerCalls$UnaryServerCallHandler$UnaryServerCallListener.onHalfClose()V+53
j  io.grpc.PartialForwardingServerCallListener.onHalfClose()V+4
j  io.grpc.ForwardingServerCallListener.onHalfClose()V+1
j  io.grpc.ForwardingServerCallListener$SimpleForwardingServerCallListener.onHalfClose()V+1
j  io.grpc.Contexts$ContextualizedServerCallListener.onHalfClose()V+9
j  io.grpc.internal.ServerCallImpl$ServerStreamListenerImpl.halfClosed()V+39
j  io.grpc.internal.ServerImpl$JumpToApplicationThreadServerStreamListener$1HalfClosed.runInContext()V+26
j  io.grpc.internal.ContextRunnable.run()V+9
j  io.grpc.internal.SerializingExecutor.run()V+31
j  java.util.concurrent.ThreadPoolExecutor.runWorker(Ljava/util/concurrent/ThreadPoolExecutor$Worker;)V+95
j  java.util.concurrent.ThreadPoolExecutor$Worker.run()V+5
j  java.lang.Thread.run()V+11
v  ~StubRoutines::call_stub

导致这个错误的地方是在java中调用了c++函数获取返回对象,并从返回对象中获取图片数据崩溃的, 位置是 imagePointer.get(image, 0, (int) length);

package com.concise.jni.javacpp.grpc;

import cn.hutool.core.io.FileUtil;
import com.concise.jni.javacpp.grpc.sdk.FindDemoRequest;
import com.concise.jni.javacpp.grpc.sdk.FindDemoResponse;
import com.concise.jni.javacpp.grpc.sdk.JavacppDemoServiceGrpc;
import com.google.protobuf.ByteString;
import io.grpc.stub.StreamObserver;
import net.devh.boot.grpc.server.service.GrpcService;
import org.bytedeco.javacpp.BytePointer;
import org.bytedeco.javacppexample.jni.JavaCppExample;
import org.bytedeco.javacppexample.jni.MediaData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author shenguangyang
 * @date 2022-05-15 16:12
 */
@GrpcService
public class JavacppDemoGrpcServer extends JavacppDemoServiceGrpc.JavacppDemoServiceImplBase {
    private static final Logger log = LoggerFactory.getLogger(JavacppDemoGrpcServer.class);
    private static final AtomicInteger requestNum = new AtomicInteger(1);
    private static final String IMAGE_PATH_PRE = "/mnt/project/javacpp-native/images";

    /**
     */
    @Override
    public void findDemo(FindDemoRequest request, StreamObserver<FindDemoResponse> responseObserver) {
        String message = request.getMessage();
        String id = String.valueOf(requestNum.addAndGet(1));
        while (true) {
            JavaCppExample javaCppExample = new JavaCppExample("hjw34er2345".getBytes(StandardCharsets.UTF_8), "测试数据", 34);
            long count = 1;
            while (count++ < 2000) {
                MediaData mediaData = javaCppExample.demo10_1();
                BytePointer imagePointer = mediaData.get_data();
                try {
                    if (imagePointer == null) {
                        TimeUnit.MILLISECONDS.sleep(200);
                        continue;
                    }
                    long length = mediaData.get_length();
                    log.info("id: {}, start read image length: {}", id, length);
                    byte[] image = new byte[(int) length];
                    imagePointer.get(image, 0, (int) length);
                    log.info("id: {}, end read image length: {}", id, length);
                    if (count % 100 == 0) {
                        String imagePath = IMAGE_PATH_PRE + "/demo10-" + id + ".jpg";
                        log.info("id: {}, grpc demo10, count: {}, length: {}", id, count, length);
                        FileUtil.writeBytes(image, imagePath);
                    }
                    FindDemoResponse response = FindDemoResponse.newBuilder()
                            .setImage(ByteString.copyFrom(image)).build();
                    responseObserver.onNext(response);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (imagePointer != null) {
                        imagePointer.releaseReference();
                    }
                    mediaData.releaseReference();
                }

            }
            javaCppExample.releaseReference();
        }
        // responseObserver.onCompleted();
    }
}

进入到BytePointer到get方法, 可以看到这是javacpp的native函数

/**
 * Reads a portion of the native array into a Java array.
 *
 * @param array the array to write to
 * @param offset the offset into the array where to start writing
 * @param length the length of data to read and write
 * @return this
 */
public native BytePointer get(byte[] array, int offset, int length);

接下来我们查看native中的函数

通过javacpp生成jni类和jni源文件, 在如下红框目录下执行命令
image.png

java -jar javacpp-1.5.7.jar -encoding "utf-8" -Xcompiler "-std=c++14" org/bytedeco/javacppexample/jni/*.java \
org/bytedeco/javacppexample/global/*.java

# 如果不想要编译和删除cpp文件, 添加-nocompile
java -jar javacpp-1.5.7.jar -encoding "utf-8" -nocompile -Xcompiler "-std=c++14" org/bytedeco/javacppexample/jni/*.java \
org/bytedeco/javacppexample/global/*.java

生成的文件中有一个叫javacpp文件,找到get对应的函数, 经过调试是在memcpy位置崩掉的, 那盲猜测应该是c++返回的uchar*有问题, 很有可能离开作用域变成了野指针, 后来也验证了这一点

JNIEXPORT jobject JNICALL Java_org_bytedeco_javacpp_BytePointer_get___3BII(JNIEnv* env, jobject obj, jbyteArray arg0, jint arg1, jint arg2) {
    static int count = 1;
    signed char* ptr = (signed char*)jlong_to_ptr(env->GetLongField(obj, JavaCPP_addressFID));
    if (ptr == NULL) {
        env->ThrowNew(JavaCPP_getClass(env, 20), "This pointer address is NULL.");
        return 0;
    }
    jlong position = env->GetLongField(obj, JavaCPP_positionFID);
    ptr += position;
    signed char* ptr0 = arg0 == NULL ? NULL : (jbyte*)env->GetPrimitiveArrayCritical(arg0, NULL);
    jobject rarg = obj;
    memcpy(ptr0 + arg1, ptr, arg2 * sizeof(*ptr0));
    if (arg0 != NULL) env->ReleasePrimitiveArrayCritical(arg0, ptr0, 0);
    return rarg;
}

解决方案

  • 进入到c++中查看对应的函数实现, std::vector很有可能被c++给释放调了 (离开作用域)

    MediaData JavaCppExample::demo10_1()
    {   
      MediaData media_data;
      cv::Mat tempFrame = this->frame.clone();
      if (tempFrame.empty())
      {
          std::cout << "cpp => demo10_1 ===> tempFrame is null" << std::endl;
          return media_data;
      }
    
      std::vector<uchar> buff;
      matToJpeg(tempFrame, buff);
    
      // 这种方式会有很多不确定因素, 比如在java端通过javacpp从uchar* 指针中拷贝数据到java的byte[]时候, 由于uchar* 很有可能变成野指针
      //, 而javacpp拷贝是调用c++的memcpy, 你拷贝一个野指针到java的byte[], 你不崩溃谁崩溃
      media_data.set_data(buff.data());
      media_data.set_length(buff.size());
      std::cout << "cpp => demo10_1 v1 ===> length: " << buff.size() << std::endl;
      return media_data;
    }
    

正确的写法

MediaData JavaCppExample::demo10_1()
{   
    MediaData media_data;
    cv::Mat tempFrame = this->frame.clone();
    if (tempFrame.empty())
    {
        std::cout << "cpp => demo10_1 ===> tempFrame is null" << std::endl;
        return media_data;
    }

    std::vector<uchar> buff;
    matToJpeg(tempFrame, buff);

    // 这种方式会有很多不确定因素, 比如在java端通过javacpp从uchar* 指针中拷贝数据到java的byte[]时候, 由于uchar* 很有可能变成野指针
    //, 而javacpp拷贝是调用c++的memcpy, 你拷贝一个野指针到java的byte[], 你不崩溃谁崩溃
    // media_data.set_data(buff.data());

    // 正确方式, 注意在析构要手动删除uchar*, delete[] data;
    uchar* data = new uchar[buff.size()];
    memcpy(data, buff.data(), buff.size());
    media_data.set_data(data);
    media_data.set_length(buff.size());
    std::cout << "cpp => demo10_1 v1 ===> length: " << buff.size() << std::endl;
    return media_data;
}

MediaData类

//
// Created by sheng on 2022/3/12.
//

#ifndef __MEDIA_DATA__H
#define __MEDIA_DATA__H

#include <iostream>
#include <unistd.h>
#include <string.h>
#include <memory>
#include <malloc.h>
#include <thread>
#include <pthread.h>
#include <time.h>
#include <vector>
typedef unsigned char uchar;

enum MediaType 
{
    IMAGE_JPEG, VIDEO_MP4
};

/**
 * @brief 媒体信息, 只存放视频和图片
 * 
 */
class MediaData {
public:
    MediaData() {

    };

    // 一定要加上析构函数,否则会有内存溢出问题
    ~MediaData() {
        delete[] data;
        std::cout << "release MediaData and delete data" << std::endl;
    };

    MediaData(uchar* data, long length) : data(data), length(length) {};

    uchar* get_data() { return this->data; }
    void set_data(uchar* data) { this->data = data; }

    const long get_length() { return this->length; }
    void set_length(const long length) { this->length = length; }

    const MediaType get_type() { return this->type; }
    void set_type(const MediaType type) { this->type = type; }

    const int get_width() { return this->width; }
    void set_width(const int width) { this->width = width; }

    const int get_height() { return this->height; }
    void set_height(const int height) { this->height = height; }

private:
    // 对于jni调用, 需要通过new uchar[length]创建, 否则会有很有不确定因素
    // 比如野指针
    uchar* data = nullptr;
    long length = 0;
    MediaType type;
    // 宽度和高度
    int width = 0;
    int height = 0;
};

#endif //__IMAGE_BUFFER_PLUS__H

注意事项(重要)

如果c++和java之间传输大量图片, 使用不当很有可能存在内存溢出问题

  1. BytePointer需要进行手动释放, 否则会有内存溢出的风险
  2. 假如,你的项目依赖了某些公共的cpp代码,而这个公共模块还是独立的,如果你修改了公共的代码,最好将依赖公共模块的工程重新编译,以及通过javacpp重新生成动态库, 防止出现意外问题
    1. 我在实际项目中,就是犯了这种错误,我修改了算法代码并重新编译了,而没有重新编译我自己的c++项目和jni,导致java调c++中某个函数且该函数中启动了一个线程调用了算法的动态库,每当运行一段时间就发生BytePointe (0 bytes)这种致命错误

如果c++中有使用智能指针, 需要在preset定义宏, 也可以定义一些其他的宏

package org.bytedeco.javacppexample.presets;

import org.bytedeco.javacpp.annotation.Platform;
import org.bytedeco.javacpp.annotation.Properties;
import org.bytedeco.javacpp.presets.javacpp;
import org.bytedeco.javacpp.tools.InfoMap;
import org.bytedeco.javacpp.tools.InfoMapper;

/**
 * @author shenguangyang
 * @date 2022-03-15 21:36
 */
@Properties(
        inherit = {javacpp.class},
        target = "org.bytedeco.javacppexample.jni",
        global = "org.bytedeco.javacppexample.global.image_buffer_global",

        value = {
                @Platform(
                        // 如果c++中有智能指针等, 必须在这定义宏, 而不是在c++文件中定义, 否则会出现意向不到的事情
                        // 比如编译报错
                        // 如果你在c++文件中定义 #define SHARED_PTR_NAMESPACE std, 那编译也会报错, 因为javacpp将std
                        // 当初了整形变量, 定义在XxxxGolbal中了

                        // 如果你在c++文件中定义#define SHARED_PTR_NAMESPACE, 有可能编译不报错, 但是很有可能出现内存
                        // 泄露或者程序运行一段时间崩溃的问题
                        define = {
                                "SHARED_PTR_NAMESPACE std",
                                "UNIQUE_PTR_NAMESPACE std"
                        },
                        value = {
                                "linux-x86",
                                "linux-x86_64",
                                "macosx-x86_64",
                                "windows-x86",
                                "windows-x86_64"
                        },
                        include = {"image_buffer.h"},
                        link = {"jni_utils" }
                ),
                @Platform(value = "android", preload = ""),
                @Platform(value = "ios", preload = {"liblibjpeg", "liblibpng", "liblibprotobuf", "liblibwebp", "libzlib", "libopencv_core"}),
                @Platform(value = "linux",        preloadpath = {"/usr/lib/", "/usr/lib32/", "/usr/lib64/"}),
                @Platform(value = "linux-armhf",  preloadpath = {"/usr/arm-linux-gnueabihf/lib/", "/usr/lib/arm-linux-gnueabihf/"}),
                @Platform(value = "linux-arm64",  preloadpath = {"/usr/aarch64-linux-gnu/lib/", "/usr/lib/aarch64-linux-gnu/"}),
                @Platform(value = "linux-x86",    preloadpath = {"/usr/lib32/", "/usr/lib/"}),
                @Platform(value = "linux-x86_64", preloadpath = {"/usr/lib64/", "/usr/lib/"})
        }
)
public class image_buffer implements InfoMapper {
    @Override
    public void map(InfoMap infoMap) {

    }
}