Android之NDK开发(二)JNI调用C

来源:互联网 发布:淘宝日式家具品牌 编辑:程序博客网 时间:2024/05/20 06:37

一、JNI简介

JNI(Java Native interface)又称java本地接口,相当于java和C之间互相调用的媒介,我们这里一般是java调用C或者C++代码,为什么需要使用JNI呢,因为很多功能的处理,java做的并没有C++做的好,而C++在这些方面都有现成的例子,所以我们可以通过调用C代码去实现更好的一个功能,使用JNI技术,其实就是在Java程序中,调用C语言的函数库中提供的函数,来完成一些Java语言无法完成的任务。由于Java语言和C语言结构完全不相同,因此若想让它们二者交互,则需要制定一系列的规范,JNI就是这组规范,此时Java只和JNI交互,而由JNI去和C语言交互。

1>Java语言提供的类库无法满足要求,且在数学运算,实时渲染的游戏上,音视频处理等方面上与C/C++相比效率稍低。
2>Java语言无法直接操作硬件,C/C++代码不仅能操作硬件而且还能发挥硬件最佳性能。
3>使用Java调用本地的C/C++代码所写的库,省去了重复开发的麻烦,并且可以利用很多开源的库提高程序效率。

JNI技术分为两部分:Java端和C语言端。且以Java端为主导。
1>Java程序员在Java端定义一些native方法,并将这些方法以C语言头文件的方式提供给C程序员。
2>C程序员使用C语言,来实现Java程序员提供的头文件中定义的函数。
3> 接着,C程序员将函数打包成一个库文件,并将库文件交给Java程序员。
4>Java程序员在Java程序中导入库文件,然后调用native方法。System.loadLibrary(“media_jni”);

二、NDK简介

NDK全称:Native Development Kit ,NDK是一系列工具的集合,它有很多作用。首先,NDK可以帮助开发者快速开发C(或C++)的动态库。其次,NDK集成了交叉编译器。使用NDK,我们可以将要求高性能的应用逻辑使用C开发,从而提高应用程序的执行效率。NDK工具必须在Linux下运行,它可以在linux环境下编译出可以在arm平台下运行的二进制库文件,在Java程序执行的时候,若在某个类中调用了native方法,则虚拟机会通过JNI来转调用库文件中的C语言代码。提示:C代码最终是在Linux进程中执行的,而不是在虚拟机中。

三、JNI在AndroidStudio中环境的配置

首先,在项目的设置中下载NDK所需要的文件,如图:

这里写图片描述

然后在local.properties中配置SDK和NDK的引用路径,这里一般是默认设置;

ndk.dir=C\:\\Users\\shiqi\\AppData\\Local\\Android\\Sdk\\ndk-bundlesdk.dir=C\:\\Users\\shiqi\\AppData\\Local\\Android\\Sdk

然后,在项目的gradle.properties中最后一行配置ndk的使用

android.useDeprecatedNdk=true

然后,在app的build.gradle中配置ndk的moduleName和sourceSets,moduleName是指所编译的.so库的名称,记住,几个前后引用的库的名称一定要相同,否则会报错

android {    compileSdkVersion 26    buildToolsVersion "26.0.1"    defaultConfig {        applicationId "com.example.jc.myjni"        minSdkVersion 19        targetSdkVersion 26        versionCode 1        versionName "1.0"        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"        ndk{            moduleName "MyFirst"        }        sourceSets.main{            jni.srcDirs = []            jniLibs.srcDir "src/main/libs"            //这里是编译完成后存放so文件的;路径,个人喜欢放在main路径下,比较直观        }    }    buildTypes {        release {            minifyEnabled false            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'        }    }}

四、新建C代码配置环境并且编译so

新建一个类封装调用jni的方法,方法以native命名,System.loadLibrary(“MyFirst”)表示这里将引用MyFirst库,这里和gradle里设置的库名称相同

public class MyNdk {    static {        System.loadLibrary("MyFirst");    }    public native String getString();}

配置NDK的环境变量,因为我们这里要编译java文件生成头文件,使用Javah命令和ndk-build命令,在Terminal中进行,所以需要配置,在环境变量中的path中编辑添加ndk的路径;

这里写图片描述

在Terminal命令行下,通过cd命令进去java文件下下,编译Java文件以包名.类名的形式去编译,调用Javah命令,最终生成头文件

这里写图片描述

新建一个JNI目录,存放C代码

这里写图片描述

在jni目录中新建一个cpp文件,对应头文件中的代码,将头文件代码复制过来,然后自己实现一个简单的调用,返回字符串;

#include "com_example_jc_myjni_MyNdk.h"extern "C"JNIEXPORT jstring JNICALL Java_com_example_jc_myjni_MyNdk_getString        (JNIEnv * env, jobject obj){    return env->NewStringUTF("mmfrom C++");}

然后我们需要配置Android.mk文件和Application.mk文件,在jni目录下新建两个文件Android.mk文件和Application.mk文件,请记住,mk文件的格式一定要正确,否则就会出现各种编译问题,这个在后面说;Application中的APP_MODULES同样也是只调用的so库,与gradle里一样同名,APP_PLATFORM是指兼容的最低版本

Android.mk==========LOCAL_PATH := $(call my-dir)include $(CLEAR_VARS)LOCAL_MODULE := MyFirstLOCAL_SRC_FILES :=MyLibrary.cppinclude $(BUILD_SHARED_LIBRARY)Application.mk==========APP_MODULES := MyFirstAPP_ABI := allAPP_PLATFORM := android-14

然后,在主界面去调用jni函数

public class MainActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        TextView tv = (TextView) findViewById(R.id.tv);        tv.setText(new MyNdk().getString());    }}

执行最关键的一步,编译文件,生成so文件,通过cd命令进入main\jni文件夹下,调用ndk-build

C:\Users\shiqi\Desktop\MyJNI\MyJNI\app\src\main\java>cd..C:\Users\shiqi\Desktop\MyJNI\MyJNI\app\src\main>cd jniC:\Users\shiqi\Desktop\MyJNI\MyJNI\app\src\main\jni>ndk-build[arm64-v8a] Compile++      : MyFirst <= MyLibrary.cpp[arm64-v8a] SharedLibrary  : libMyFirst.so[arm64-v8a] Install        : libMyFirst.so => libs/arm64-v8a/libMyFirst.so[x86_64] Compile++      : MyFirst <= MyLibrary.cpp[x86_64] SharedLibrary  : libMyFirst.so[x86_64] Install        : libMyFirst.so => libs/x86_64/libMyFirst.so[mips64] Compile++      : MyFirst <= MyLibrary.cpp[mips64] SharedLibrary  : libMyFirst.so[mips64] Install        : libMyFirst.so => libs/mips64/libMyFirst.so[armeabi-v7a] Compile++ thumb: MyFirst <= MyLibrary.cpp[armeabi-v7a] StaticLibrary  : libstdc++.a[armeabi-v7a] SharedLibrary  : libMyFirst.so[armeabi-v7a] Install        : libMyFirst.so => libs/armeabi-v7a/libMyFirst.so

到这里一个简单的流程就成功了,运行的结果便是jni的返回的字符串显示,下面看下编译成功的libs,如果编译失败,libs里面不会有so文件;

这里写图片描述

接下来我们来看看怎么调用现成的C代码呢,在JNI中,直接去调用c代码是不行的,需要我们通过ndk去编译成so文件后,在jni方法中调用即可,这里我们把代码及头文件粘贴到jni目录,然后在Android.mk文件中配置编译的C文件信息,如下,注意,LOCAL_SRC_FILES里面的每个cpp或者c文件后面是有空格的,\是换行,如果没有空格,系统就无法辨认这是几个文件,会一直报错,博主之前没搞过mk没注意就被坑了,如果编译出现问题,试试删除obj文件夹后再重新编译,可能因为之前的编译记录导致的问题,重新再命令行中使用ndk-build命令,编译生成新的so文件;

LOCAL_PATH := $(call my-dir)include $(CLEAR_VARS)LOCAL_MODULE := MyFirstLOCAL_SRC_FILES :=MyLibrary.cpp \            functions.c \            helpers.c \            images.c \            menu.c \            main.c \            crc16fast.c \            LEPTON_AGC.c \            LEPTON_I2C_Protocol.c \            LEPTON_I2C_Service.c \            LEPTON_SDK.c \            LEPTON_SYS.c \            LEPTON_VID.c \            raspi_I2C.cinclude $(BUILD_SHARED_LIBRARY)

然后在jni方法中调用c的方法,引用其头文件即可

#include "com_example_jc_myjni_MyNdk.h"//引用C代码的头文件#include "LEPTON_SDK.h"#include "LEPTON_SYS.h"#include "LEPTON_Types.h"LEP_CAMERA_PORT_DESC_T _port;extern "C"JNIEXPORT jstring JNICALL Java_com_example_jc_myjni_MyNdk_getString        (JNIEnv * env, jobject obj){    //调用C代码    LEP_OpenPort(1, LEP_CCI_TWI, 400, &_port);    LEP_RunSysFFCNormalization(&_port);    return env->NewStringUTF("mmfrom C++");}

五、头文件

1>头文件中存放的是对某个库中所定义的函数、宏、类型、全局变量等进行声明,它类似于一份仓库清单。若用户程序中需要使用某个库中的函数,则只需要将该库所对应的头文件include到程序中即可。
2>头文件中定义的是库中所有函数的函数原型。而函数的具体实现则是在库文件中。
3>头文件是给编译器用的,库文件是给连接器用的。
4>在连接器连接程序时,会依据用户程序中导入的头文件,将对应的库函数导入到程序中。头文件以.h为后缀名,库函数,当前系统里面实现,window .dll linux- .a .so
public native String getStringFromJni();

程序运行时,java虚拟机会在加载的本地.so运行库中查找与java本地方法匹配的c函数,并生成映射表,而后将java本地方法与c函数链接在一起,当java虚拟机在本地
的.so库查找不到与本地方法匹配的本地函数时,会抛出UnsatisfiedLinkError错误,那么java虚拟机在加载.so库的时候,如何把java代码的本地方法与运行库的c函数映射在一起呢?函数原型,当生成了函数原型,java虚拟机即可以把java本地方法和.so库的相应函数映射在一起,函数原型由javah工具生成,用来生成包含函数原型的c/c++头文件

JNIEXPORT jstring JNICALL Java_com_example_jnidem01_MainActivity_getStringFromJni  (JNIEnv *, jobject);

JNIEXPORT,JNICALL是JNI的关键字,表示此函数要被JNI调用,函数原型必须要有这两个关键字,JNI才能正确的调用函数,都是宏定义,函数原型遵循一定的规则形式为: Java_类名_本地方法名, 通过函数原型的命名即可以推断出JNI本地函数与哪个java类哪个本地方法对应。在生成的函数原型中,带有两个默认参数,JNIEnv ,jobject,支持JNI的函数必须包含这两个共同参数JNIEnv 是一个JNI接口指针 ,一个指向jni函数表的接口指针,用来调用JNI表中的各种JNI函数(jni.h头文件就是一个函数表) jobject,JNI提供的Java本地类型,用来在C代码中访问java对象,此参数中保存着调用地方法的对象的引用

六、Android.mk文件

Makefile
就是linux环境下进行c/c++开发的自动化编译的脚本,一套编译规则,c /cpp —-gcc–a.o -/.a。Android.mk文件用来告知NDK Build 系统关于Source的信。Android.mk将是GNU Makefile的一部分,且将被Build System解析一次或多次。Android.mk文件语法允许我们将Source打包成一个”modules”. modules可以是:静态库(.a), 动态库(.so)只有动态库可以被 install/copy到应用程序包(APK). 静态库则需要被链接入动态库。可以在一个Android.mk中定义一个或多个modules. 也可以将同一份source 加进多个modules.Build System帮我们处理了很多细节而不需要我们再关心。例如:你不需要在Android.mk中列出头文件和外部依赖文件。NDK Build System自动帮我们提供这些信息。这也意味着,当用户升级NDK后,你将可以受益于新的toolchain/platform而不必再去修改Android.mk.

Android.mk语法
LOCAL_PATH := $(call my-dir)
每个Android.mk文件必须以定义LOCAL_PATH为开始。它用于在开发tree中查找源文件。
宏my-dir 则由Build System提供。返回包含Android.mk的目录路径。

include $(CLEAR_VARS)
CLEAR_VARS 变量由Build System提供。并指向一个指定的GNU Makefile,由它负责清理很多LOCAL_xxx.例如:LOCAL_MODULE, LOCAL_SRC_FILES, LOCAL_STATIC_LIBRARIES等等,但不清理。

LOCAL_PATH.
这个清理动作是必须的,因为所有的编译控制文件由同一个GNU Make解析和执行,其变量是全局的。所以清理后才能避免相互影响。

LOCAL_MODULE := hello-jni
LOCAL_MODULE模块必须定义,以表示Android.mk中的每一个模块。名字必须唯一且不包含空格。Build System会自动添加适当的前缀和后缀。例如,foo,要产生动态库,则生成libfoo.so. 但请注意:如果模块名被定为:libfoo.则生成libfoo.so. 不再加前缀。

LOCAL_SRC_FILES := hello-jni.cpp
LOCAL_SRC_FILES变量必须包含将要打包如模块的C/C++ 源码。不必列出头文件,build System 会自动帮我们找出依赖文件。缺省的C++源码的扩展名为.cpp. 也可以修改,通过LOCAL_CPP_EXTENSION。

include $(BUILD_SHARED_LIBRARY)
BUILD_SHARED_LIBRARY:是Build System提供的一个变量,指向一个GNU Makefile Script。它负责收集自从上次调用 include $(CLEAR_VARS) 后的所有LOCAL_XXX信息,编译打包为动态.so库

BUILD_STATIC_LIBRARY:编译为静态库 .a
BUILD_SHARED_LIBRARY :编译为动态库 .so
BUILD_EXECUTABLE:编译为Native C可执行程序

七、调用多个C文件

add.h  add.cpp LOCAL_PATH := $(call my-dir)include $(CLEAR_VARS)LOCAL_MODULE    := itcastLOCAL_SRC_FILES := itcast.cpp add.cpp  空格include $(BUILD_SHARED_LIBRARY)

c++调用c函数(静态库方式)

1 声明头文件 2 实现函数 3 调用函数 4 修改Android.mk文件LOCAL_PATH := $(call my-dir)include $(CLEAR_VARS)LOCAL_MODULE    := ADDLOCAL_SRC_FILES := add.cinclude $(BUILD_STATIC_LIBRARY)include $(CLEAR_VARS)LOCAL_MODULE    := itcastLOCAL_SRC_FILES := itcast.cppLOCAL_STATIC_LIBRARIES := ADDinclude $(BUILD_SHARED_LIBRARY)