Android中使用JNI(深度学习模型移动端部署初步)

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++代码的流程

  1. 创建一个有native标识的方法,并且从其他Java方法调用它
  2. Java编译器生成字节码
  3. C/C++ 编译器生成动态库 .so文件(Linux)或 .dll文件(Windows)
  4. 运行程序,执行字节码
  5. 执行到loadLibary或load调用的时候,添加一个 .so文件到这个进程中
  6. 执行到native方法的时候,通过方法签名,在已打开的.so文件中进行搜索。
  7. 如果链接库内有对应方法,就会被执行,否则程序崩溃

01 Android-JNI流程

@Reference:https://blog.csdn.net/sevenjoin/article/details/104531190

1.1 创建项目

  1. 新建项目

    选择native c++

    在这里插入图片描述

  2. 这里选择使用c++11

    在这里插入图片描述

  3. 项目结构

    在这里插入图片描述

    • 标线部分的配置文件需要改动,后续介绍。

    • 这里可以将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++实现了对应的功能
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值