OpenCV学习记录——1.学习Android NDK

1.前言

因为作者选择了题目为《在Android系统上实现全景图拼接》毕设的原因,所以需要学习能够完成全景图拼接技术的框架——OpenCV。当然,在这之前,由于OpenCV的底层是由C++编写,要想在Android上使用这一框架,就需要学习Android NDK,即Native Development Kit,是Android的一种开发工具包。
现在,让我们开始Android NDK的学习吧。为了方便起见,下文将Android NDK简称为NDK

2.什么是NDK

Android NDK 是为了 Android 应用开发人员去嵌入用 C 或者 C++ 这种编译成本地机器码到自己的应用开发包里面,提供的一套开发环境。

开发者可以使用这个开发包,来编译可在 Android 运行的库或者可执行文件,也是可以跑本地应用的,但一般不这么用,因为对于开发者来说,系统服务基本都是 Java 语言实现的,用 C 或者 C++ 调用,不是常规开发思路。

NDK 中提供移植好的,可在 Android 执行的封装库,并且做好了编译工具链,配置方法,方便开发者快速移植,实现功能。

3.为什么要用NDK

  • 为了提升性能,C 、C++ 语言编译出来运行在硬件环境上,比起 Java 虚拟机环境,有性能优势。

  • 有些三方库,或者自己之前开发好的 C 、C++ 源码,需要直接使用,比如一些算法库,游戏中的三角形,四边形的碰撞判定。

  • Java 虚拟机就是 C 、C++ 实现的,不支持两个之间调用,也说不过去。类似与我用 C 、 C++ 语言实现了一套解析固定格式的文件,然后调用对应的本地方法运行。

  • OpenGL ES2.0 、 Cocos2d-x 、FFmpeg 等开发过程中,需要使用 C 、C++ ,而 Android 本身语言是 Java 。 比如 FFmpeg 移植过来,有时会使用 SDL 框架进行渲染,而这个是 C 语言编写的。

  • 目前的Android开发,在很多公司不再是纯粹的Java层开发,更多的会与C++结合,把一些重要的方和行为以及一些私密性质的东西放在C++中,一般遇到多人开发的时候,通常的做法是在Android项目中放入C++的动态库(.so文件)

4.NDK编译出来的目标类型

一般来说,NDK编译出来的目标类型有三种:

  • 动态库(扩展为.so)
    动态库可以有未知的符号,数据,只要指明在哪个动态库去找即可,系统在运行时候,加载这个动态库的时候,会对导入符号进行查找,找到会自动加载进来,找不到就会报错。
  • 静态库(扩展为.a)
    静态库必须将所有符号确定,每个符号都必须存在,才能编出来,否则会在链接过程报错,某个符号找不到,某个方法未定义。
  • 可执行文件 (这个一般没有后缀)
    可执行文件,如果引用一个动态库,只要指明动态库在哪里即可,如果引用一个静态库,静态库必须每个符号都是已定义的,同时如果要编成可执行文件,必须实现一个 main 方法,因为这个是程序的入口点。程序在被加载进入内存后,会先进行环境初始化(堆与栈的设置),然后跳到 main 方法执行。

这几类在 Linux 环境下,都属于 ELF 格式,只是动态库和静态库,可执行文件有区别而已。
另外,谈到NDK,就不得不谈到CMake,CMake允许开发者编写一种平台无关的 CMakeList.txt 文件来定制整个编译流程,然后再根据目标用户的平台进一步生成所需的本地化 Makefile 和工程文件,如 Unix 的 Makefile 或 Windows 的 Visual Studio 工程。从而做到“一次编写,随时运行”。

5.NDK下载和NDK项目的创建

由于使用的IDE为Android Studio 3.5.3,一些功能有些许改动,建议参考此篇博客:https://blog.csdn.net/MLDan/article/details/88391311进行安装。

这里简单谈一下NDK两个比较经典的版本:r8r19

为什么要谈到 r8 呢?因为它里面的 docs 目录很经典,是最好的开发文档。而 r19 是最新的,要在 6.0 以上手机跑起来,需要较新版本编译,因为 -fpic -fpie 这几个要求。PICPIE 代表意思是位置无关的动态库,和位置无关的可执行

关于什么是位置无关,举个例子。操场上站了一排人,你在第九个位置,你距离门 90 米,你后面有个人,在第十个位置,他距离门 100 米。有个人找他,可以说就在距离门 100 米的位置,也可以说在你后面 10 米处。这时候整个队伍移动了 100 米,这时候有人找他,说距离门 100 米的位置就是错误的了,但是说在你后面 10 米,还是对的。

也就是相对位置绝对位置的关系,从指令上说,就是相对寻址和绝对寻址的关系。Android 后续新的版本,强制需要这个位置无关配置。

项目创建完成图,目录图如下所示:
在这里插入图片描述
为了保险起见,建议运行一次,看看能否运行成功,运行成功后如下:

6.写个hello JNI可执行文件

学习一切编程知识的时候,都可以以“Hello World”作为练手,那就让我们现在开始编写相应代码吧!

6.1 程序编写

  1. 在src/main目录下创建cpp文件夹(可命名为jni或者其他文件件名,jni是通用文件名)。因为这里用的Android Studio版本较新,所以已经自动创建好了。接下来编写ndk编译的必要文件Android.mk和Application.mk,代码如下:
    • Android.mk
    LOCAL_PATH := $(call my-dir)//赋值当前目录
    
    include $(CLEAR_VARS)//清理掉无关设置的参数
    
    LOCAL_MODULE:= hello-exe  //配置模块名,最终名字由它决定
    
    LOCAL_SRC_FILES := hello-exe.c //相关的编译文件
    
    LOCAL_LDLIBS := -fpic -fPIE -pie //配置位置无关,链接器参数
    
    include $(BUILD_EXECUTABLE)  //编译成可执行文件
    
    1. Application.mk
    APP_STL := c++_static  //引用配置自己需要的c++运行库
    APP_GNUSTL_FORCE_CPP_FEATURES := exceptions rtti  //异常机制支持  支持 run-time type information (rtti)
    APP_ABI := armeabi-v7a  //编译ABI 配置
    
    1. 目录图
      在这里插入图片描述
  2. 接下来在cpp目录下编写运行文件hello-exe.c,代码如下:
    #include <string.h> //引用头文件
    #include <jni.h>
    #include <stdio.h>

    int main() // main 方法
    {
        printf("Hello World !\n");//打印一段文字
    }

6.2 程序运行

想要单独运行这个文件的话,需要把 android-ndk-r19c 放置到 D:\android-ndk-r19c 这个位置,然后把 hello-exe 目录(即包含以上三个文件)放到这里。然后我们 CMD 打开命令行窗口,切换到D:\android-ndk-r19c\hello-exe\jni,使用..\..\ndk-build.cmd -B V=1这行命令执行,如果执行成功的话,会看到以下内容:

Android NDK: APP_PLATFORM not set. Defaulting to minimum supported version android-16.

[armeabi-v7a] Compile thumb  : hello-exe <= hello-exe.c
D:/android-ndk-r19c/build//../toolchains/llvm/prebuilt/windows-x86_64/bin/ <font color=#ff0000 size=4>clang.exe</font> -MMD -MP -MF D:/android-ndk-r19c/hello-exe/obj/local/armeabi-v7a/objs/hello-exe/hello-exe.o.d -target armv7-none-linux-androideabi16 -fdata-sections -ffunction-sections -fstack-protector-strong -funwind-tables -no-canonical-prefixes <font color=#ff0000 size=4>--sysroot</font> D:/android-ndk-r19c/build//../toolchains/llvm/prebuilt/windows-x86_64/sysroot -g -Wno-invalid-command-line-argument -Wno-unused-command-line-argument  -fno-addrsig -fpic -mfpu=vfpv3-d16  -march=armv7-a -mthumb -Oz -DNDEBUG  -ID:/android-ndk-r19c/hello-exe/jni   <font color=#ff0000 size=4>-DANDROID</font>  -nostdinc++ -Wa,--noexecstack -Wformat -Werror=format-security <font color=#ff0000 size=4>-c</font>  D:/android-ndk-r19c/hello-exe/jni/hello-exe.c <font color=#ff0000 size=4>-o</font> D:/android-ndk-r19c/hello-exe/obj/local/armeabi-v7a/objs/hello-exe/hello-exe.o

[armeabi-v7a] Executable     : hello-exe
D:/android-ndk-r19c/build//../toolchains/llvm/prebuilt/windows-x86_64/bin/clang++.exe -Wl,--gc-sections -Wl,-rpath-link=D:/android-ndk-r19c/build//../toolchains/llvm/prebuilt/windows-x86_64/sysroot/usr/lib/arm-linux-androideabi/16 -Wl,-rpath-link=D:/android-ndk-r19c/hello-exe/obj/local/armeabi-v7a D:/android-ndk-r19c/hello-exe/obj/local/armeabi-v7a/objs/hello-exe/hello-exe.o -lgcc -Wl,--exclude-libs,libgcc.a -latomic -Wl,--exclude-libs,libatomic.a -target armv7-none-linux-androideabi16 -no-canonical-prefixes    -Wl,--build-id -nostdlib++ -Wl,--no-undefined -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now -Wl,--warn-shared-textrel -Wl,--fatal-warnings   -fpic -fPIE -pie <font color=#ff0000 size=4>-lc -lm</font> -o D:/android-ndk-r19c/hello-exe/obj/local/armeabi-v7a/hello-exe

[armeabi-v7a] Install        : hello-exe => libs/armeabi-v7a/hello-exe
copy /b/y "D:\android-ndk-r19c\hello-exe\obj\local\armeabi-v7a\hello-exe" "D:\android-ndk-r19c\hello-exe\libs\armeabi-v7a\hello-exe" > NUL
D:/android-ndk-r19c/build//../toolchains/llvm/prebuilt/windows-x86_64/bin/arm-linux-androideabi-strip --strip-unneeded  D:/android-ndk-r19c/hello-exe/libs/armeabi-v7a/hello-exe

这里专门把编译过程打印了出来( V=1 )。跟这个差不多的是 --just-print 参数,不过 --just-print 是只打印,不编译,那么如果编译过程有一些依赖,就会出现报错。综上,一般我们使用 V=1。

如果编译过程需要依赖别的编译出来的结果,就会报错了。所以就用 V=1 来查看编译过程, -B 参数代表强制重新编译。

编译出来的在这个目录 D:\android-ndk-r19c\hello-exe\libs\armeabi-v7a\hello-exe 我们把它丢到手机中( Root 后扔到 system/bin 目录),也可以是模拟器,我们这里就使用模拟器运行它。

输出为:

Hello World !

7.相关配置文件的详解

  1. Android.mk:都有哪些配置,哪些说明 ,android-ndk-r8\docs\ANDROID-MK.html 非常详尽的描述了都有哪些值,怎么设置,都有例子和解释。
  2. Application.mk:可以参考 APPLICATION-MK.html
  3. NDK-BUILD.html:这个是编译的说明
  4. ndk-build -B V=1: 强制重编译,同时显示编译参数
  5. ndk-build NDK_DEBUG=1:生成可调试的文件

可以回过头再看看第六节中涉及到的输出的命令,简单来讲,主要分为以下几个步骤:
6. [armeabi-v7a] Compile thumb : hello-exe <= hello-exe.c 把 c 编译成 o

  1. [armeabi-v7a] Executable : hello-exe 链接,把依赖的静态库 ,动态库链接进来 ,变成可执行文件

  2. [armeabi-v7a] install : hello-exe => libs/armeabi-v7a/hello-exe 去掉调试信息,减小大小

我们再来看下 D:\android-ndk-r19c\hello-exe 的 obj 目录分析

D:\android-ndk-r19c\hello-exe\obj\local\armeabi-v7a hello-exe 这个是有调试信息的。

D:\android-ndk-r19c\hello-exe\obj\local\armeabi-v7a\objs\hello-exe hello-exe.o 是上面 1 编译出来的结果 hello-exe.o.d 这个文件可以打开瞧瞧,生成了一个makefile的编译规则。( r8 编译出来多一些,可以看看 )

d:/android-ndk-r19c/hello-exe/obj/local/armeabi-v7a/objs/hello-exe/hello-exe.o: \
  d:/android-ndk-r19c/hello-exe/jni/hello-exe.c

ARM GCC 编译参数中主要关注几个内容:

命令 作用
-fxxx 系统的一些编译参数
-Dxxx -Dxxx=2 自定义的编译参数
–sysroot 设定系统搜索路径,设定 base 路径,-I 参数可以以这个路径为当前路径
-I 定义 include 路径,也就是从哪里找头文件
-target 设定目标 ARM 具体类别
-W 设定什么类型对应是否报错之类的
-l 指定链接是需要的库名
-L 指定链接库找到路径

补充一点:我们移植三方库到 Android 平台,默认的开源项目都是 GCC 版本的,一般情况下,将 GCC 的 配置成ARM GCC ,基本就能跑通。同时要多看官方的 ReadMe 文件。./configure --help 能看到一些配置要求,参考这个,同时找一些网上别人移植的过程,基本能够保证移植通过。

8.写个 hello JNI 调用 so

8.1 程序编写

这一节通过编写一个 so 库 ,然后再调用这个 so 中的方法,写一个可执行文件。

  1. 新增一个 so 库的编译规则
LOCAL_MODULE:= hello-so

LOCAL_SRC_FILES := hello-so.c

LOCAL_LDLIBS := -fpic -fPIE -pie

include $(BUILD_SHARED_LIBRARY)
  1. hello-so.c 实现一个 my_print 方法
int my_print()
{
    printf("Hello JNI !\n");
}
  1. 然后我们在我们的可执行文件中引用这个 so 库
LOCAL_SHARED_LIBRARIES:=hello-so //引用动态库

include $(BUILD_EXECUTABLE)//编出一个可执行文件
  1. hello-exe.c 改动比较大,我们看下
  int main()
    {
        void *handle;
        char *error;
        my_print print_func = NULL;

        //打开动态链接库
        handle = dlopen(LIB_PATH, RTLD_LAZY);
        if (!handle) {
                printf("%s\n", dlerror());
                exit(0);
        }

        //获取print函数
        *(void **) (&print_func) = dlsym(handle, "my_print");
        if ((error = dlerror()) != NULL)  {
                printf("%s\n", dlerror());
                exit(0);
        }
        printf("%d",(*print_func)());
        //关闭动态链接库
        dlclose(handle);
    }

8.2 API讲解

部分API讲解如下:

方法 作用
dlopen 打开动态库
dlsym 找到符号
(*print_func)(); 调用方法

我们可以看到,要使用一个 so 库的某个方法,就上面三步骤,加载 ,查找 ,使用 。我们这里调用了 so 库中的 my_print 方法。

8.3 程序运行

想要运行这段程序,需要把 D:\android-ndk-r19c\hello-so\libs\armeabi-v7a 的 libhello-so.so 放到手机的 /system/lib/。把 hello-exe 放到手机的 /system/bin/。 运行hello-exe 可以看到打印结果 Hello JNI !

为什么要放置 libhello-so.so 到 /system/lib/ 目录呢? 因为,这个是 Android 上给配置的默认 so 库搜索路径。 具体 adb 连上手机 ,使用 export 看所有系统环境,使用 echo $PATH 看设置的路径。

9.写个 hello from C++

之前都是脱离IDE去编写C++程序,这次我们使用Android Studio来进行以下程序的编写。
这次,就使用之前创建好并且能成功运行的项目(作者这里是JNILearn)即可。

9.1 目录解析

目录 作用
cpp 放置 native 代码
java 放置 Java 代码
res 放置资源

下面针对各个目录中比较重要的目录\文件进行详述。

9.1.1 JNILearn\app\libs 放置库文件

如果有三方编好的库,放置到这里,然后在工程对应模块 build.gradle 下使用

sourceSets {
    main {
        jniLibs.srcDirs = ['libs']
    }
}

配置上去

9.1.2 JNILearn\app\build 编译过程以及结果

目录 作用
generated 生成目录
intermediates 编译中间过程
outputs 输出内容

其中,一些重要的目录/文件如下:

目录 / 文件 作用
intermediates\cmake\debug\obj\arm64-v8a 这里面的带调试信息
build\intermediates 里面内容细细看,这里面有很多编译过程输出,可以详细阅读。
.externalNativeBuild 本地代码编译,我们的本地代码编译过程,就在这里面输出的。
CMakeCache.txt 规则定义

9.1.3 rules.ninja 生成的编译命令

一条条的具体执行的命令,通过这个,可以看到每种类别的文件是通过什么命令出来的。想学习,阅读编译具体参数的,可以阅读这个文件。

这里贴一段:
在这里插入图片描述
这里出来的是一条条规则,有注释和执行命令。我们想看具体某个文件怎么编译的,可以从这里找到信息。

9.1.4 build.ninja 编译脚本

在这里插入图片描述

这里可以看到生成出来的命令,编译规则,工程的 CMake 生成的。不知道大家写过 Makefile 文件没?那个是人工手动在写,时代发展,就变成我们只给下简单配置,就可以自动出来编译文档。

9.1.5 配置native方法

9.1.5.1 CMakeLists.txt 文件中内容,配置生成一个 so 库

   add_library( # Sets the name of the library.
                     native-lib

                     # Sets the library as a shared library.
                     SHARED

                     # Provides a relative path to your source file(s).
                     src/main/cpp/native-lib.cpp )

9.1.5.2 build.gradle

        externalNativeBuild {
            cmake {
                    cppFlags "" //使用ctrl+鼠标点击,可以跳转到对应源码,然后setxxx这里的xxx就是我们这里写的,每个上面都有官方的demo
                    cFlags ""
                    arguments ""
                    abiFilters "armeabi-v7a"
                 }
        }

还有

        externalNativeBuild {
            cmake {
                path "CMakeLists.txt"
                //buildStagingDirectory 指定编译输出目录
            }
        }

这里配置 cmake 的参数,使用 ctrl+鼠标 点击,会跳到具体的一个代码中。这个是 gradle-3.2.1-sources 里面的。我们不知道该配什么参数的时候,就可以用这个方法,跳到对应的代码里面,这个类或者父类中的属性值,有 setXXXX 的,这里 XXXXX 就是我们可以在这里配置的参数,该配置什么,gradle-3.2.1-sources 源码中,在每个 setXXXX 上面,都有一段参考,并且有简单介绍。

9.2 流程分析

为了了解程序的运行流程,这里简单来分析一下它的程序运行流程

9.2.1 C++的代码,使用 CMake,编译成了 libnative-lib.so 库

9.2.2 MainActivity.java 加载 so 库

 static {
        System.loadLibrary("native-lib");
    }

这个代码会在 MainActivity 初始化时候进行加载。

9.2.3 MainActivity.java 关联 so 库中方法

  public native String stringFromJNI();

这里的 native 指明是个本地方法,系统在调用的时候,会从加载的 so 库中去找。默认查找一个以Java+包名+方法名的方法,这里 stringFromJNI 就会查找 so 库中的 Java_hellojni_codegg_com_hellojni_MainActivity_stringFromJNI 方法。

除了这种默认的方法,有时候我们使用的是三方库,内部方法没法修改,这个时候就不能使用这个默认方法了。 如果是这个情况,就需要我们进行配置,指定 Java 的某个方法和 C 的哪个关联。具体新增一个 so 库中实现一个叫做 JNI_onLoad 的方法,在里面进行注册 Java 中的某个方法,和 C 中的方法匹配。

int jniRegisterNativeMethods(JNIEnv* env, const char* className,
const JNINativeMethod* gMethods, int numMethods) {
    jclass clazz;

    clazz = env->FindClass(className);

    if (clazz == NULL) {
        return -1;
    }

    if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) { //注册
        return -1;
    }

    return 0;    
}

JNI_onLoad 方法是 Java 加载一个 so 库后,系统默认让执行的一个方法。如果找不到,就不执行了。一般我们在JNI_onLoad 来注册 Java 的某个方法和 C 的哪个关联,并且可以设定使用 Java 的具体版本。

9.3 相关文档

https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/functions.html 关于 JNI ,这里就是官方的文档,关于相关的配置,这里都可以找到。后续有一章节,会实战到这个,继续加油。

9.4 JNI 执行调用流程

先回过头,看看第八小节,专门讲解了一个可执行文件怎么调用so库的方法的。 Java 就顺着这来理解。
在这里插入图片描述
所以,第八小节是本质内容,是 JNI 中的实现的底层逻辑,具体研究,可以跟踪 System.loadLibrary 方法 ,一探究竟。

9.5 添加一个新的方法

在这一节里,我们尝试修改一些内容,添加一个新的方法。

  1. MainActivity.java 文件加一行

    public native String myStringFromJNI();
    
  2. 自动化生成 .h 文件
    File | Settings | Plugins 点击 Browse repositories 搜索 Jni helper 安装,重启
    在这里插入图片描述
    然后选中我们的 myStringFromJNI 方法,点击右键,选择生成.h by javah
    在这里插入图片描述
    这样子就生成了对应的 .h 。如果你想手动处理,可以参考 javac 命令。

  3. 在 jni 目录的 .h 复制对应的声明,作者这里为:Java_com_example_jnilearn_MainActivity_myStringFromJNI然后在cpp目录的natie-lib.cpp中实现:

    extern "C"  JNIEXPORT jstring JNICALL
    Java_com_example_jnilearn_MainActivity_myStringFromJNI
        (JNIEnv *env, jobject)
    {
    std::string hello = "my Hello from C++";
    return env->NewStringUTF(hello.c_str());
    }
    
  4. 修改MainActivity,调用我们编写的方法

    tv.setText(StringFromJNI()); // 修改为 tv.setText(myStringFromJNI());
    
  5. 编译,运行 APK,可以看到修改已经生效,效果如图:
    在这里插入图片描述

10.CMake 和 ndk-build 的关系

在之前的章节中,我们编写了一个简单的NDK demo,那么本节我们就来分析下构建工具CMake和ndk-build的关系。

  • CMake 是一个跨平台的安装(编译)工具,可以用简单的语句来描述所有平台的安装(编译过程)。

  • NDK 是一套工具,允许您为 Android 使用 C 和 C++ 代码,并提供众多平台库,您可用其管理原生 Activity 和访问物理设备组件,例如传感器和触摸输入。

CMake 是新的编译规则写法,ndk-build 是老的方案。如果想用 ndk-build 也是可以的,毕竟一些老的项目是,那么该如何解决呢?

前面小节将编译过程在哪里,参数如何配置。希望大家能够再回过头,仔细看一遍,理解 CMake 和 NDK 的关系。 新的项目都会使用 CMake 方式编写编译规则,详情请关注官网:添加链接描述

我们把 CMake 变成 NDK 的配置方式,来看看都有哪些操作。

11.使用 ndk-build 方案

11.1 jni 目录编写 Android.mk 、 Application.mk

Android.mk 编一个 libnative-lib.so 库:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE:= native-lib
LOCAL_SRC_FILES := native-lib.cpp
LOCAL_LDLIBS := -fpic -fPIE -pie
include $(BUILD_SHARED_LIBRARY)

Application.mk 配置一些公共参数:

APP_STL := c++_static
APP_GNUSTL_FORCE_CPP_FEATURES := exceptions rtti
APP_ABI := armeabi-v7a

11.2 修改.\app\build.gradle

    externalNativeBuild {
        ndk { //这里 ndk 配置
            moduleName "native-lib"  //编译模块,还有哪些参数,右键Ctrl+ 鼠标点击即可看到
            abiFilters "armeabi-v7a"  //编译指令体系
        }
    }

这个文件同时修改:

externalNativeBuild {
    ndkBuild {//这里 ndk 配置
        path "src/main/jni/Android.mk"  // 指定从哪找Android.mk
    }
}

11.3 运行程序

编译,运行 APK ,查看结果。不出意外,正常运行起来了。区别就是我们使用的是 NDK 的编译方式。

编译过程,可以查看 .externalNativeBuild\ndkBuild 目录,进行学习。很多时候,多看看 Build 目录,很多过程信息都在这里存档的,是学习过程中,不断验证,不断观察的好的内容。

12.调试 C 代码

12.1 改成 C 写法

这个没啥必要,但是之前都是使用的C++,这里就尝试把它修改了。下面是修改的一部分代码,把 C++ 的写法,改成C 的,同时修改引入头文件。

jstring
Java_hellojni_codegg_com_hellojni_MainActivity_stringFromJNI(JNIEnv *env,
 jobject thiz) {
    char *hello = "Hello from C++";
    return (*env)->NewStringUTF(env, hello);
}

12.2 配置调试参数

点击菜单栏上面的 Run | Run/Debug Configurations ,选择Debugger 菜单的Debug type 为 Dual。
在这里插入图片描述

设置成可以同时调试 Java 和 C 的代码。

12.3 调试

在 C 代码中 Java_com_example_jnilearn_MainActivity_myStringFromJNI设置断点.Debug 运行 App ,可以看到断在了我们设置的断点位置生效了。

在这里插入图片描述

剩下的就是单步调试,查看变量,查看堆栈信息了。

13.C 调用 Java

经过十来节的学习,我们走过了NDK 、 CMake 配置,编译 so 、 ELF(可执行文件),学会使用 Android Studio 调试 C 源码。同时我们在学习实战的过程中,也了解了 JNI 的实现原理,编译中参数如何配置,怎么查找。并且如果你想去看编译过程,给出了目录。

我们已经学会了 Java 调用 C 的方式,下一步我们就学习如何从 C 调用到 Java 。

我们这一节演示一个内容,如何在 C 代码中调用 Java 的方法,以及属性值。我们实现一个,在 C 里面,找到 Java 中的两个变量,然后再调用 Java 的一个方法,让返回两数的相加结果,传回给 Java ,显示出来。

13.1 MainActivity.java 新增一些代码

//两个变量,等会 C 中会读取这两个值
int i = 5;
int n = 10;
//返回相加结果,等会 C 会调用这个方法
public int add(int num1, int num2) {
    return num1 + num2;
}
//从 C 端获取结果
public native int myResultFromJNI();

然后选中我们的 myResultFromJNI 方法,点击右键,选择生成.h by javah

13.2 native-lib.c 实现方法:

JNIEXPORT jint JNICALL Java_hellojni_codegg_com_hellojni_MainActivity_myResultFromJNI
(JNIEnv *env, jobject obj) {
    //获致obj中对象的class
    jclass clazz = (*env)->GetObjectClass(env, obj);
    // 获取java中i字段的ID(最后一个参数是i的签名)
    jfieldID id_num1 = (*env)->GetFieldID(env, clazz, "i", "I");
    // 获取num1字段对应的值
    jint num1 = (*env)->GetIntField(env, obj, id_num1);
    jfieldID id_num2 = (*env)->GetFieldID(env, clazz, "n", "I");
    // 获取num2字段对应的值
    jint num2 = (*env)->GetIntField(env, obj, id_num2);
    //拿到add方法的id,后面的为签名信息,括号内代表两个参数,都是I (int)返回也为I(int)的方法
    jmethodID methodId = (*env)->GetMethodID(env, clazz, "add", "(II)I");
    // 调用它,拿到结果
    jint res = (*env)->CallIntMethod(env, obj, methodId, num1, num2);
    return res;
}

关于参数签名,该如何编写,这里不展开讲解,提供一篇链接 https://www.jianshu.com/p/c85462c3a26e

我们要学习的是整体逻辑,我们 C 找 Java 的依据是类和对象,参数中 JNIEnv *env, jobject obj 。 env 代表当前环境上下文,这个当我们多个线程调用的时候,需要 AttachCurrentThread 进行设定,让 env 关联到当前线程,使用后 DetachCurrentThread 解除绑定。 默认不需要关注,多线程时候,出现调用故障,可以往这个方向思考。

多线程开发,核心是并发下的数据同步。所以我们写代码,就要注意方法的可重用性,也就是尽量不使用全局变量,如果用,有多个线程竞争的时候,就会出问题。

要解决这个全局变量的问题,就会使用到多线程开发中的同步机制。

相关知识,参考《深入理解操作系统》第三版。快速学习,可以阅读如下网站: https://github.com/CyC2018/CS-Notes#computer-%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F

14. 参考文档

  1. 官方 r8 里面的 docs
  2. 官方 r8 里面的 samples,作为学习参考代码
  3. https://developer.android.google.cn/ndk/guides/
  4. https://github.com/googlesamples/android-ndk/tree/master

学习 NDK 这些就足够了,如果你需要继续进阶,可以再查看一些较为进阶的教程,这里不再详述。

发布了253 篇原创文章 · 获赞 52 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_41151659/article/details/104022083