跳到主要内容

NDK入门

· 阅读需 6 分钟

JNI中C与java类型对应表

字符Java类型C类型
Vvoidvoid
Zjbooleanboolean
Ijintint
Jjlonglong
Djdoubledouble
Fjfloatfloat
Bjbytebyte
Cjcharchar
Sjshortshort
数组则以"["开始,用两个字符表示
[IjintArrayint[]
[FjfloatArrayfloat[]
[BjbyteArraybyte[]
[CjcharArraychar[]
[SjshortArrayshort[]
[DjdoubleArraydouble[]
[JjlongArraylong[]
[ZjbooleanArrayboolean[]

开始编写jni

当java调用,

static {
System.loadLibrary("hello");
}

jni会尝试去寻找JNI_OnLoad方法,所以一些初始化的东西就可以在这里面实现。

方法注入

本人表示网上一堆乱七八糟的方式会让刚入门的同学遇到无限的坑,被坑过的我,现在整理了一下,顺便梳理过程

首先列出几个jni的方法,这些方法都可以在jni.h中找到,在jni.h中都大部分提供了有C与C++两个版本的,使用时请注意。

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved);//jni入口

JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved);//结束jni之后调用的

jint GetEnv(void** env, jint version)//传入指定的jni版本,返回是否支持此版本,成功返回JNI_OK

jclass FindClass(const char* name)//找到指定的java类,使用全包名+类名

jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods)//注册本地方法

有了以上的方法,简单的NDK开发就可以开始了,列出代码讲解可能来的更加痛快。

//jni入口
JNIEXPORT jint JNI_OnLoad(JavaVM* jvm, void* reserved){
JNIEnv *env = NULL;
//使用reinterpret_cast进行类型转换,我之前没有使用这种方式转换,下面的代码一直有问题,所以强烈建议使用
if(jvm->GetEnv(reinterpret_cast<void **>(&env),JNI_VERSION_1_4) != JNI_OK){
return JNI_ERR;
}
//registerMethod 自己写的方法,用于方法注册
if(registerMethod(env) == JNI_ERR){
return JNI_ERR;
}
return JNI_VERSION_1_4;

}

这里说明一下 JNI_VERSION_1_4 是系统定义的宏,除了这个还有

  • JNI_VERSION_1_1
  • JNI_VERSION_1_2
  • JNI_VERSION_1_6

现在的版本一般使用 JNI_VERSION_1_4。

下面是JNINativeMethod的结构体:

typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;

对于JNINativeMethod结构体说明,网上一大把的,

  • 第一个变量name是Java中函数的名字。
  • 第二个变量signature,用字符串是描述了函数的参数和返回值,就是方法签名,例如:()V,表示void func();()表示方法参数,V表示返回值,对应的类型可以参考上面的表格,如果参数是java的类,使用"L"开头,";"结束,例如:("Ljava/lang/Object;")I,表示int func(String str);多个参数例子:(IZLjava/lang/String;)V,表示void func(int i,boolean flag,String str,),()里面直接写类型就可以了
  • 第三个变量fnPtr是函数指针,指向C函数。格式都是固定的(void*)xxx,xxx就是C++的方法名

static JNINativeMethod gMethods []= {
{"stringFromJni", "()Ljava/lang/String;", (void*)stringFromJni},
};

int registerMethod(JNIEnv *env){
jclass clazz = env->FindClass(CLASS_MODEL);
if(clazz == NULL){
return JNI_ERR;
}
if(env->RegisterNatives(clazz,gMethods,NELEM(gMethods))<0){
return JNI_ERR;
}
return JNI_OK;
}

CLASS_MODEL是对应java中的全包名+类名,可以使用下面的方式,也可以定义 成static const char *CLASS_MODEL

#define CLASS_MODEL  "com/wawov/nativeapp/activity/MainActivity"

#ifndef NELEM
# define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))
#endif

NELEM用于计算gMethods的size。

本地的方法

jstring stringFromJni(JNIEnv *env,jobject thiz){
return env->NewStringUTF("hello jni");
}

注意C++是至上而下编译的,所以编写是注意方法的位置,到到简单的jni就写好了


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.c 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信息。并决定编译为什么

  • BUILD_STATIC_LIBRARY:编译为静态库。
  • BUILD_SHARED_LIBRARY :编译为动态库
  • BUILD_EXECUTABLE:编译为Native C可执行程序

LOCAL_LDLIBS := -llog LOCAL_LDLIBS可以用它来添加系统库,-lxxx

简单的Android.mk例子:

LOCAL_PATH :=$(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE :=hello #System.loadLibrary("hello")
LOCAL_SRC_FILES :=hello.cpp

LOCAL_LDLIBS := -llog

include $(BUILD_SHARED_LIBRARY)

关于Android.mk文件还有很多的内容,兴趣的同学可以移步到: http://android.mk/ https://developer.android.com/ndk/guides/android_mk.html#mdv


Application.mk

一般简单的只需要这样写:

APP_ABI :=armeabi

这里的是cup的架构除此之外,还有别的一些架构,如:x86、mips。当然在Application.mk中,可写的东西也是挺多的。对于刚入门来说,可以慢慢了解。

有兴趣的同学可以移步到: https://developer.android.com/ndk/guides/application_mk.html

好了,jni开发的重要的几个文件就准备得差不多了,接下来就是这些文件放哪里和怎么调用的问题了。


一般的会在main目录下创建一个jni的文件夹放入hello.cpp、Android.mk和Application.mk文件,同时在建一个jniLibs文件夹放入编译好的.so文件。

接下来我们需要在java类中定义一个native的方法:

public native  String stringFromJni();

还记得我们在hello.cpp文件中定义了一个本地的方法:

jstring stringFromJni(JNIEnv *env,jobject thiz){
return env->NewStringUTF("hello jni");
}

这时候我们cd 到jni文件夹中,使用ndk-build命令,编译成功的话,会在jni目录下生成2个文件夹libs、obj,我们将libs中的armeabi文件夹复制到jniLibs中。接下来,在java类中这样做:


static {
System.loadLibrary("hello");
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = (TextView) findViewById(R.id.id_tv);
tv.setText(stringFromJni());
}

如果启动app,看到hello jni,那恭喜你,一个简单ndk就完成了。