Android调用JNI的方式
Android JNI 的实现包括两种实现方法:静态和动态。两种方法的区别如下:
静态:先由Java得到本地方法的声明“System.loadLibrary(“hello_jni”);”,然后再通过JNI实现该声明方法。
根据函数名找到对应的JNI函数:Java层调用函数时,会从对应的JNI中寻找该函数,如果没有就会报错,如果存在则会建立一个关联联系,以后在调用时会直接使用这个函数,这部分的操作由虚拟机完成。
静态方法就是根据函数名来遍历java和jni函数之间的关联,而且要求jni层函数的名字必须遵循
特定的格式,其缺点在于:
1)javah生成的jni层函数特别长;
2)初次调用native函数时要根据名字搜索对应的jni层函数来建立关联联系,这样影响效率。
静态方法(固定函数名映射方式)实现流程:
1.先在Java中加载so库
static{
System.loadLibrary("strRcgMain");
Log.i("MyView", "loadLibrary");
}
2.Java层调用函数:
public native String recgnt(String filePath,String dicPath);
3.根据Java层的类名函数名,生成jni接口,可直接用ndk生成:
①定位到src位置(JDK1.7及以后)
E:>cd /d E:\Android\work\数字串识别1
E:\Android\work\数字串识别1>cd src
②用javah生成c相关头文件
E:\Android\work\数字串识别1\src>javah -jni -encoding UTF-8 com.ppl.number.MyView
③选中项目点击F5刷新后生成相关的.h文件
.h文件 中存在Java层需要调用的c中的函数。
④将该.h文件剪切至jni文件夹中,实现头文件中声明的函数。同时将其他所需的cpp h文件copy至jni文件夹下,同时更改Android.mk文件。
strRcgMain.cpp的作用就是实现com_ppl_number_MyView.h中声明的函数。
#include <jni.h>
#include "NumRecognView.h"
#include <vector>
#include "LogPrint.h"
using namespace std;
#include <android/log.h>
#include "com_ppl_number_MyView.h"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#define DLL_EXP __attribute__ ((visibility("default")))
#define DLL_LOCAL __attribute__ ((visibility("hidden")))
extern __mallocfunc void* malloc(size_t);
extern void* memcpy(void *, const void *, size_t);
JNIEXPORT jstring JNICALL Java_com_ppl_number_MyView_recgnt
(JNIEnv * env, jobject clazz, jstring filePath, jstring dicPath){
//功能实现区
}
⑤编译生成库文件
生成库文件所在目录:libs/armeabi/libstrRcgMain.so
⑥直接运行程序,我第一个交叉编译的项目是将研究生期间的空中手写字符串识别算法,通过SurfaceView跟踪手指轨迹,结果如下:
动态方法(注册函数映射表方式)
JNI 允许提供一个函数映射表,注册给Jave虚拟机,这样Jvm就可以用函数映射表来调用相应的函数,就可以不必通过函数名来查找需要调用的函数了。
Java与JNI通过JNINativeMethod的结构来建立联系,它在jni.h中被定义。
结构内容如下:
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
第一个变量name是Java中函数的名字。
第二个变量signature,用字符串是描述了函数的参数和返回值
第三个变量fnPtr是函数指针,指向C函数。
当java通过System.loadLibrary加载完JNI动态库后,紧接着会查找一个JNI_OnLoad的函数,如果有,就调用它,
而动态注册的工作就是在这里完成的。
1)JNI_OnLoad()函数
JNI_OnLoad()函数在VM执行System.loadLibrary(xxx)函数时被调用,它有两个重要的作用:
指定JNI版本:告诉VM该组件使用那一个JNI版本(若未提供JNI_OnLoad()函数,VM会默认该使用最老的JNI 1.1版),如果要使用新版本的JNI,
例如JNI 1.4版,则必须由JNI_OnLoad()函数返回常量JNI_VERSION_1_4(该常量定义在jni.h中) 来告知VM。
初始化设定,当VM执行到System.loadLibrary()函数时,会立即先呼叫JNI_OnLoad()方法,因此在该方法中进行各种资源的初始化操作最为恰当,
2)RegisterNatives
RegisterNatives在AndroidRunTime里定义
syntax:
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods)
实现流程:
①先在Java中加载so库
static{
System.loadLibrary("strRcgMain");
Log.i("MyView", "loadLibrary");
}
②Java层调用函数:
public native String recgnt(String filePath,String dicPath);
③在jni目录下新建newRec.cpp:
- 实现识别算法
//实现recgnt方法
jstring recgnt_number(JNIEnv * env, jobject clazz, jstring filePath, jstring dicPath)
{
//功能实现区
}
- 需要注册的函数列表,放在JNINativeMethod 类型的数组中
/*需要注册的函数列表,放在JNINativeMethod 类型的数组中,
以后如果需要增加函数,只需在这里添加就行了
参数:
1.java代码中用native关键字声明的函数名字符串
2.签名(传进来参数类型和返回值类型的说明)
3.C/C++中对应函数的函数名(地址)
*/
static JNINativeMethod getMethods[] = {
{
"recgnt",
"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",
(void *)recgnt_number
},
};
- 注册回调函数
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved){
JNIEnv* env = NULL;
//判断虚拟机状态是否有问题
if(vm->GetEnv((void**)&env,JNI_VERSION_1_6)!= JNI_OK){
return -1;
}
assert(env != NULL);
//开始注册函数 registerNatives -》registerNativeMethods -》env->RegisterNatives
//指定类的路径,通过FindClass 方法来找到对应的类
const char* className = "com/ppl/number/MyView";
jclass clazz;
//找到声明native方法的类
clazz = env->FindClass(className);
if(clazz == NULL){
return JNI_FALSE;
}
//注册函数 参数:java类 所要注册的函数数组 注册函数的个数
if(env->RegisterNatives(clazz,getMethods,sizeof(getMethods)/ sizeof(getMethods[0])) < 0){
return JNI_FALSE;
}
//返回jni 的版本
return JNI_VERSION_1_6;
}
JNINativeMethod的定义如下:
typedef struct {
constchar*name; // Java中申明的Native函数名称
constchar* signature; // 描述了函数的参数和返回值
void* fnPtr; // 函数指针,指向C函数
} JNINativeMethod;
通过method_table,就将本地的recgnt_number()函数和注册到Java中的recgnt()绑定起来了。当我们在Java中调用recgnt()时,实际调用的是recgnt_number()。
④. 在jni目录下新建Android.mk,Android.mk的代码如。
⑤ 生成.so库文件:
“libs/armeabi/libstrRcgMain.so”库文件
⑥ 在eclipse下执行工程,OK。
使用RegisterNatives的好处有三点:
1、C++中函数命名自由,不必像javah自动生成的函数声明那样,拘泥特定的命名方式;
2、效率高。传统方式下,Java类call本地函数时,通常是依靠VM去动态寻找.so中的本地函数(因此它们才需要特定规则的命名格式),而使用RegisterNatives将本地函数向VM进行登记,可以让其更有效率的找到函数;
3、运行时动态调整本地函数与Java函数值之间的映射关系,只需要多次call RegisterNatives()方法,并传入不同的映射表参数即可。
jni开发中的常见错误
- java.lang.UnsatisfiedLinkError: Native method not found: 本地方法没有找到
- 本地函数名写错
- 忘记加载.so文件 没有调用System.loadlibrary
- findLibrary returned null
- System.loadLibrary(“libhello”); 加载动态链接库时 动态链接库名字写错
- 平台类型错误 把只支持arm平台的.so文件部署到了 x86cpu的设备上
- 在jni目录下创建 Application.mk 在里面指定
- APP_ABI := armeabi
APP_PLATFORM := android-14