Android中以NDK方式使用C++原生代码及一些相关错误

一、前期基础知识储备

Android NDK 是一套允许您使用 C 和 C++ 等语言,以原生代码实现部分应用功能的工具集。在开发某些类型的应用时,这有助于您重复使用以这些语言编写的代码库(C++等语言在执行速度、稳定性、安全性上有很大的优势)。

public class MyActivity extends Activity {
  /**
  * Native method implemented in C/C++
  */
  public native void computeFoo();
}

可以在 Android Studio 2.2 或更高版本 中使用 NDK 将 C 和 C++ 代码编译到原生库中,然后使用 Android Studio 的集成编译系统 Gradle 将原生库打包到 APK 中。Java 代码随后可以通过 Java 原生接口 (JNI) 框架调用原生库中的函数。

Android Studio 编译原生库的默认编译工具是 CMake。由于很多现有项目都使用 ndk-build 编译工具包,因此 Android Studio 也支持 ndk-build。不过,如果您要创建新的原生库,则应使用 CMake。

本文主要分析NDK的方式,暂不涉及CMake的方式处理。

二、上代码,具体实现

在实际开发中,我们使用原生代码的场景常见的有两种,一是有C++原生代码编译好的so库但是没有原生代码,二是两者都有。第一种情况就不好做延伸了,因为没有原生代码所以无法做功能扩展;第二种情况是本文主要讨论的,以博主开发的应用为例,谷歌2019年8月1日起,所有发布到谷歌应用市场的应用,既要求有32位,也要求要64位,32位64位主要针对的就是原生代码提供的so库。

如上图,armeabi-v7a下存放的so库为32位,arm64-v8a下存放的是64位so库,在以前的开源项目来说,往往只有32位的,没有64位的,所以就需要自己手动创建新的64位so库。

如上图,我们现在有C++原生代码的情况下,只需要生成一份新的64位so库即可。步骤如下:

1)gradle.properties文件下加入android.useDeprecatedNdk=true;

2)app的build.gradle文件下加入三处声明;

android {
    compileSdkVersion 28
    defaultConfig {
        ... ...
        ... ...
        ndk {
            moduleName "nativefaceswap"
            abiFilters 'armeabi-v7a','arm64-v8a'
        }
    }

    sourceSets {
        main {
            jni.srcDirs = []
        }
    }

    externalNativeBuild {
        ndkBuild {
            path "src/main/jni/Android.mk"
        }
    }

}

①第一处ndkmoduleName声明,与Android.mk文件中声明的so库名一致;同时指定过滤器,编译出指定架构的so库,本例中需要32位和64位so库,所以过滤器指明架构为armeabi-v7aarm64-v8a

②第二处sourecSets的声明,指明了so库放置的位置,Android Studio默认放置so库的文件夹为jniLibs,若想改为其他目录则需要在这里进行声明以更改。

③第三处externalNativeBuild的声明,指明了Android.mk文件的位置,用以编译时进行定位。

3)如果你的C++文件来自于开源项目,那么需要手动进行修改;

如上图,在main目录下新建jni文件夹,然后里面放入至少三个文件——Android.mk, Application.mk, cpp文件;

如上图,然后打开Android.mk文件,要修改几处地方:

第一处,由于项目依赖到了OpenCV,所以需要将OpenCv的位置指定为自己电脑下的位置,而不再使用原开源项目的位置;

第二处,LOCAL_MODULE,这里决定了包名,可以不进行修改,但注意和build.gradle中声明的保持一致。

4)最后点击"make project"开始编译项目,然后在build目录下寻找,最后新建jniLibs文件夹,然后拷贝到jniLibs文件夹内。

如此一来,就编译出了新的arm64-v8a文件内的so库。

三、上述过程中可能遇到的一些错误

1.编译时,打包时遇到包重复的问题

More than one file was found with OS independent path 'lib/arm64-v8a/libnativefaceswap.so'

More than one file was found with OS independent path 'lib/armeabi-v7a/libnativefaceswap.so'

解决方法:在build.gradle下添加:

    packagingOptions {
        pickFirst 'lib/armeabi-v7a/libnativefaceswap.so'
        pickFirst 'lib/arm64-v8a/libnativefaceswap.so'
    }

上述声明是告诉AS,进行打包,选取第一次出现的so库进行打包。

2.调用JNI时出错,报使用的Native方法没有实现的错误

java.lang.UnsatisfiedLinkError: No implementation found for

错误的可能原因:

1)so 文件路径问题

① 要么你就在main/下新建一个JniLibs文件夹,再把SDK里带的so文件目录诸如armeabi等等文件夹放进去就可以了。

② 要不你就把放so文件的目录诸如armeabi直接放在放jar文件的目录libs里头,然后修改build.gradle文件的内容,添加如下

android { 
  sourceSets{ 
    main{ jniLibs.srcDirs=['Libs'] 
  } 
}

2)jni 调用类的路径一定要一致。也就是说 .so中函数声明涉及到的package nameclass name与调用它的package name和class name不符。因此我们要改变我们工程中的package name和class name。使其与.so文件中函数签名提示的一致,在这个类中加入native方法的声明。

举例来说,我要使用FaceSwap类下的Native方法:

    public native void portraitSwapNative(long addrImg1,
                                          long addrImg2,
                                          int[] landmarksX1,
                                          int[] landmarksY1,
                                          int[] landmarksX2,
                                          int[] landmarksY2,
                                          long addrResult);

那么我使用的C++原生代码就需要可以定位到这里,以下是FaceSwap类的位置信息:

所以对应使用的C++原生代码中定义的Native方法就需要进行对应的修改;

更多时候,我们是直接使用已有的so库而不想去修改原生代码,所以这里推荐使用的方法为,在自己项目目录下新建一个同名的目录,然后将代码移植过去,这样是成本最小的改动,如图:

如图,我们在主项目的目录下新建一级目录,命名依照C++代码内的声明,这样直接使用已有的so库就不会有问题了。

使用CMake编译时个人碰到的一些问题和解决方法

android Ndk开发大坑--创建新项目时编译报错 executing external native build for cmake

Android Studio 报错 executing external native build for cmake xxx CMakeLists.txt

这个问题的出错情况可能有多种原因导致,我的情况是在第一篇文章中找到答案。

 

补充一些知识,JNI相关的内容,开发中不常使用,容易忘记。

SDK:software development kit。软件开发工具包。
被软件开发工程师用于为特定的软件包、软件框架、硬件平台、操作系统等建立应用软件的开发工具的集合。

NDK 其中NDK的全拼是:Native Develop Kit。
Android NDK 是一套允许您使用原生代码语言(例如C和C++) 实现部分应用的工具集。在开发某些类型应用时,这有助于您重复使用以这些语言编写的代码库。

NDK本身其实就是一个交叉工作链,包含了Android上的一些库文件,然后,NDK为了方便使用,提供了一些脚本,使得更容易的编译C/C++代码。
总之,在Android的SDK之外,有一个工具就是NDK,用于进行C/C++的开发。一般情况,是用NDK工具把C/C++编译为.so文件,然后在Java中调用。

上层通过JNI来调用NDK层的,使用这个工具可以很方便的编写和调试JNI的代码。
因为C语言的不跨平台,在Mac系统的下使用NDK编译出在Linux下能执行的函数库——so文件。其本质就是一堆C/C++的头文件和实现文件打包成一个库。

JNI,全称为Java Native Interface,即Java本地接口。通过JNI可以实现Java代码与C/C++代码的交互。
由于JNI是JVM规范的一部分,因此可以将我们写的JNI的程序在任何实现了JNI规范的Java虚拟机中运行。


Java调用C/C++在Java语言里面本来就有的,并非Android自创的,即JNI。JNI就是Java调用C++的规范。
当然,一般的Java程序使用的JNI标准可能和android不一样,Android的JNI更简单。

JNI下一共涉及到三个角色:C/C++代码、本地方法接口类、Java层中具体业务类

#include <jni.h>
#include <string>

extern "C"
JNIEXPORT jstring

JNICALL
Java_com_example_ndkdemo_NDKTools_getStringFromNDK(
        JNIEnv *env, jobject /* this */) {
    std::string hello = "Hello from TEST C++";
    return env->NewStringUTF(hello.c_str());
}

上面的Cpp文件中的函数解释:

jstring 是返回值类型;
com_example_hellojni是包名;
MainActivity 是类名;
stringFromJNI 是方法名;其中JNIExport和JNICALL是不固定保留的关键字不要修改。

两个参数,JNIEnv* 代表了Java环境,通过这个JNIEnv* 指针,就可以对Java端的代码进行操作。

jobject 代表这个native方法的类实例或这个类的class对象。

传统JNI开发流程:

  • 第1步:在Java中先声明一个native方法;
  • 第2步:编译Java源文件javac得到.class文件,点击Build中的Make Project或者Rebuild Project进行编译来获取中间文件;
  • 第3步:通过javah -jni命令导出JNI的.h头文件;
  • 第4步:使用Java需要交互的本地代码,实现在Java中声明的Native方法(如果Java需要与C++交互,那么就用C++实现Java的Native方法);
  • 第5步:将本地代码编译成动态库(Windows系统下是.dll文件,如果是Linux系统下是.so文件,如果是Mac系统下是.jnilib);
  • 第6步:通过Java命令执行Java程序,最终实现Java调用本地代码。

CMake开发流程:

  • 第1步:创建源文件,在main目录下新建一个目录cpp,右键点击此目录,然后选择 New > C/C++ Source File,比如命名为native-lib;
  • 第2步:跟build同级目录创建CMake构建脚本,CMake构建脚本是一个纯文本的文件,而且这个名字必须是是CMakeLists.txt; 模块的根目录并选择 New——> File;
  • 第3步:向CMake脚本文件写入数据;
  • 第4步:向Gradle 关联到原生库;右键点击想要关联到原生库的Module,并从菜单中选择 Link C++ Project with Gradle;
 externalNativeBuild {
        cmake {
            path 'CMakeLists.txt'
        }
    }
  • 第5步: 编写native-lib.cpp;
  • 第6步:NDKTools.java下添加库引用;
public class NDKTools {

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

    public static native String getStringFromNDK();
}

CMake的运转流程

  • 1、Gradle 调用外部构建脚本CMakeLists.txt;
  • 2、CMake 按照构建脚本的命令将 C++ 源文件 native-lib.cpp 编译到共享的对象库中,并命名为 libnative-lib.so ,Gradle 随后会将其打包到APK中;
  • 3、运行时,应用的NDKTools会使用System.loadLibrary()加载原生库。应用就是可以使用库的原生函数stringFromJNI()

CMake文件解析

下面解析一份最基础的文件:

cmake_minimum_required(VERSION 3.4.1)

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 )

find_library( # Defines the name of the path variable that stores the
        # location of the NDK library.
        log-lib

        # Specifies the name of the NDK library that
        # CMake needs to locate.
        log )


target_link_libraries( # Specifies the target library.
        native-lib

        # Links the log library to the target library.
        ${log-lib} )

CMakeLists.txt我们看到这里主要是分为四个部分:

  • cmake_minimum_required(VERSION 3.4.1):指定CMake的最小版本
  • add_library:创建一个静态或者动态库,并提供其关联的源文件路径,开发者可以定义多个库,CMake会自动去构建它们。Gradle可以自动将它们打包进APK中。
    • 第一个参数——native-lib:是库的名称;
    • 第二个参数——SHARED:是库的类别,是动态的还是静态的;
    • 第三个参数——src/main/cpp/native-lib.cpp:是库的源文件的路径;
  • find_library:找到一个预编译的库,并作为一个变量保存起来。由于CMake在搜索库路径的时候会包含系统库,并且CMake会检查它自己之前编译的库的名字,所以开发者需要保证开发者自行添加的库的名字的独特性。
    • 第一个参数——log-lib:设置路径变量的名称;
    • 第一个参数—— log:指定NDK库的名子,这样CMake就可以找到这个库;
  • target_link_libraries:指定CMake链接到目标库。开发者可以链接多个库,比如开发者可以在此定义库的构建脚本,并且预编译第三方库或者系统库。
    • 第一个参数——native-lib:指定的目标库;
    • 第一个参数——${log-lib}:将目标库链接到NDK中的日志库。

参考文章《Android JNI学习(二)——实战JNI之“hello world”》《Android JNI 篇 - 从入门到放弃

 

.so文件

so是shared object的缩写,见名思义就是共享的对象,机器可以直接运行的二进制代码。大到操作系统,小到一个专用软件,都离不开so。so主要存在于Unix和Linux系统。

我们通过C/C++开发的软件,如果以动态链接库的形式输出,那么在Android中它的输出就是一个.so文件。

相比于.a,.so文件是在运行时,才会加载的。所以,当我们将.so文件放入工程时,一定有一段Java代码在运行时,load了这个native库,并通过JNI调用了它的方法。

所以,当我们使用JNI开发时,我们就是在开发一个.so文件。不论我们是开发一个工程,还是开发一个库,只要当我们使用C++开发,都会生成对应的.so文件。

所以JNI开发的核心是,我们生成so的过程,和使用so的过程。

 

两种NDK使用方式的区别《AndroidStudio CMake和传统JNI的区别

CMake优势:

  1. 可以直接的在C/C++代码中加入断点,进行调试
  2. java引用的C/C++中的方法,可以直接ctrl+左键进入
  3. 对于include的头文件或者库,也可以直接进入
  4. 不需要配置命令行操作,手动的生成头文件,不需要配置android.useDeprecatedNdk=true属性

C++访问Java C++调用Java方法 :https://www.aidemx.cn/2923.html

中文的CMake手册

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值