00 JNI
@Reference:https://www.cnblogs.com/longfurcat/p/9830129.html
0.1 什么是JNI
JNI 全称 Java Native Interface。Java本地方法接口,它是Java语言允许Java代码与C、C++代码交互的标准机制。维基百科是这样解释的:“当应用无法完全用Java编程语言实现的时候,(例如,标准Java类库不支持的特定平台特性或者程序库时),JNI使得编程者能够编写native方法来处理这种情况”。这就意味着,在一个Java应用程序中,我们可以使用我们需要的C++类库,并且直接与Java代码交互,而且在可以被调用的C++程序内,反过来调用Java方法(回调函数)。
0.2 JNI的优点
-
JNI使得一些"过程"无需在Java中实现。例如,硬件敏感的,或者直接与操作系统API关联的命令。
-
由于使用底层的库,如图形,计算,各种类型的渲染等等,可以提高应用的运行性能。
-
已经有大量的库已经被实现,编程者可直接使用,不用再自行编写。这里的库指的是用其他编程语言实现的程序库,例如IO流或者线程等底层与OS交互的操作都是由C/C++实现的。
0.3 Java代码调用C/C++代码的流程
- 创建一个有native标识的方法,并且从其他Java方法调用它
- Java编译器生成字节码
- C/C++ 编译器生成动态库 .so文件(Linux)或 .dll文件(Windows)
- 运行程序,执行字节码
- 执行到loadLibary或load调用的时候,添加一个 .so文件到这个进程中
- 执行到native方法的时候,通过方法签名,在已打开的.so文件中进行搜索。
- 如果链接库内有对应方法,就会被执行,否则程序崩溃
01 Android-JNI流程
@Reference:https://blog.csdn.net/sevenjoin/article/details/104531190
1.1 创建项目
-
新建项目
选择native c++
-
这里选择使用c++11
-
项目结构
-
标线部分的配置文件需要改动,后续介绍。
-
这里可以将cpp文件夹改成jni。
-
但对应的build.gradle中涉及的路径需要手动修改:
-
-
建议新增资源文件夹assets,后续存放模型文件:
-
1.2 Android页面逻辑
这里自动生成的默认是页面逻辑跟JNI在一起,建议分离
-
这部分涉及的内容包括MainActivity.java、res文件夹下布局文件和资源文件、AndroidManifest.xml
-
删掉原本的MainActivity.java中的内容:
-
实现onCreat方法,关联xml文件
-
后续跳转逻辑略,参考TestJNI项目
-
-
通常这里调用摄像头并进行识别结果绘制时,通常采用SurfaceView组件,所以需要main去实现SurfaceHolder.Callback,这时需要实现其抽象方法:(这里implement后报错,直接alt enter实现其抽象方法即可)
public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback{ @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } @Override public void surfaceCreated(@NonNull SurfaceHolder holder) {} @Override public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {} @Override public void surfaceDestroyed(@NonNull SurfaceHolder holder) {} }
-
布局文件中通常会使用下拉列表更换不同模型,所以可以给Spinner设置entries属性,绑定values/strings.xml中的内容:
-
activity_main.xml:
<Spinner android:id="@+id/sp_detect" android:layout_width="wrap_content" android:layout_height="match_parent" android:drawSelectorOnTop="true" android:entries="@array/model_detect"/> <Spinner android:id="@+id/sp_pose" android:layout_width="wrap_content" android:layout_height="match_parent" android:drawSelectorOnTop="true" android:entries="@array/model_pose"/>
-
values/strings.xml:
- 在resources标签下设置string-array标签,item则表示下拉列表每个元素内容
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">NCNN-Pose</string> <string-array name="model_detect"> <item>yolox-nano</item> <item>yolox-tiny</item> </string-array> <string-array name="model_pose"> <item>lite-mspn-50</item> <item>lpn-50</item> </string-array> <string-array name="cpugpu_array"> <item>CPU</item> <item>GPU</item> </string-array> </resources>
-
1.3 配置文件
1.3.1 局部的build.gradle
- 添加以下内容到defaultConfig下:
externalNativeBuild {
cmake {
cppFlags ""
arguments '-DANDROID_PLATFORM=android-24', '-DANDROID_STL=c++_static', '-DANDROID_STL=c++_shared'
}
}
-
修改defaultConfig外的externalNativeBuild中cmake版本,与后续保持一致
externalNativeBuild { cmake { path file('src/main/jni/CMakeLists.txt') version '3.10.2' } }
1.3.2 local.properties
-
添加cmake的路径:
cmake.dir=D\:\\Android_Studio\\sdk\\cmake\\3.10.2.4988404
-
其中具体内容和版本需要根据下载的一致
-
并且这里的3.10.2要对应局部的build.gradle中的配置
-
1.3.3 CMakeLists.txt
这部分的内容比较关键,且涉及知识多,这里以一个例子展示
cmake_minimum_required(VERSION 3.10) # 1.这里设置cmake版本号
project("testjni") # 2.声明项目名称,与后续JNI中的System.loadLibrary("testjni");保持一致
# 3. set和find_package是为了导入外部包,比如opencv、ncnn、mnn等
# set函数定义了一个变量OpenCV_DIR,并且变量的值为${CMAKE_SOURCE_DIR}/opencv-mobile-4.6.0-android/sdk/native/jni
# 其中,CMAKE_SOURCE_DIR 是一个cmake内置变量,指定了CMakeLists.txt所在的目录
set(OpenCV_DIR ${CMAKE_SOURCE_DIR}/opencv-mobile-4.6.0-android/sdk/native/jni)
# find_package指令用于查找并载入一个外部包的设置。(查找的包名 REQUIRED:一定要找到包 后续可选字段:表示查找的包中必须要找到的组件)
find_package(OpenCV REQUIRED core imgproc)
set(ncnn_DIR ${CMAKE_SOURCE_DIR}/ncnn-20221128-android-vulkan/${ANDROID_ABI}/lib/cmake/ncnn)
find_package(ncnn REQUIRED)
# 4.通过add_library函数添加项目中自己的c/c++文件编译(设置lib名称 lib类型 c/c++文件相对路径)
add_library( # Sets the name of the library.
testjni
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
TestJNI.cpp) # 这里很关键,建议先写这里再去实现对应cpp
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log)
# 5. target_link_libraries将所有的库链接起来
target_link_libraries( # Specifies the target library.
testjni
# Links the target library to the log library
# included in the NDK.
ncnn
${OpenCV_LIBS}
${log-lib})
1.4 JNI部分
1.4.1 准备
-
导入外部包均放在这里,比如opencv、ncnn等
- 注意:需要在CMakeLists.txt中导入,见上述。
-
在java文件夹的包下创建一个java JNI文件,这里是WzyJNI.java
-
在jni文件夹下新建一个cpp文件,这里是WzyJNI.cpp。前面已经导入过cmakelist中。
1.4.2 Java-JNI代码
-
编写native方法;加载jni so库
package com.example.testfortypora; /** * @auther: wzy * @date : 2023/2/3 14:37 * @desc : */ public class WzyJNI { //这里需要加载这个jni so库, 这个testfortypora名字就是最终编译产出的so的名字 // 其与CMakeLists中add_library下库名一致。 static{ System.loadLibrary("testfortypora"); } /** * 定义native方法 * 调用C代码对应的方法 * @return */ public native String sayHello(); //声明了一个sayHello的本地接口。 public native void printAge(int age); }
1.4.3 CPP代码:
-
注意此时上面java的两个函数都会报错,此时alt enter生成函数会直接出现在定义的WzyJNI.cpp中。
- JNI本地接口函数名的命名规则为Java_包名_类名_函数名。即本例sayHello来说:Java_com_example_testfortypora_WzyJNI_sayHello();
- 然后具体实现相应功能即可
-
代码展示
- 这里使用了android的log库进行日志打印。函数用法类似logd
#include <jni.h> #include <android/log.h> #include <iostream> using namespace std; extern "C"{ JNIEXPORT jstring JNICALL Java_com_example_testfortypora_WzyJNI_sayHello(JNIEnv *env, jobject thiz) { // TODO: implement sayHello() __android_log_print(ANDROID_LOG_DEBUG,"wzy","WzyJNI_printAge"); return (jstring) "Hello world";//需要强转成jstring类型 } JNIEXPORT void JNICALL Java_com_example_testfortypora_WzyJNI_printAge(JNIEnv *env, jobject thiz, jint age) { // TODO: implement printAge() printf("wzy的年龄是%d",age); } }
1.4.4 Main中测试使用
- 实例化JNI对象并调用本地化方法
public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 创建JNI实例,并调用本地声明的方法
WzyJNI wzyJNI = new WzyJNI();
int result = wzyJNI.printAge(22);
Log.d("wzy", "wzy的年龄是:"+result);
}
……
}
-
日志输出结果
com.example.testfortypora D/wzy: WzyJNI_printAge //这是CPP中的log com.example.testfortypora D/wzy: wzy的年龄是:22 //main中的log
- 可见,通过调用Java本地声明方法,c/c++实现了对应的功能