程序员人生 网站导航

Android jni/ndk编程一:jni初级认识与实战体验

栏目:综合技术时间:2016-11-05 08:50:51

Android平台很多地方都可以看到jni的身影,比如之前接触到1个投屏的项目,主要的代码是c/c++写的,然后通过Jni供java层调用;另外,就拿Android系统中的Service来讲,很多的Service都有java层代码和native层代码组成,native层代码会在android启动的进程中完成向java层的注册。总之,由于没法甩开jni的身影,所以我打算花点时间系统的学习下Android下的jni开发。

1.开发工具

所谓工欲善其事,必先利其器,在学习android系统的jni编程之前,先了解下jni编程使用的工具。1.1NDK(Native Development Kit)
NDK翻译过来就是本地代码开发工具集,本地代码主要指c/c++,因此,我们的c/c++代码可使用NDK中提供的工具完成编译,我们可以把C/C++代码编译成动态库,然后在java层访问动态库,这样就是了java调用C/C++的功能。NDK众多的工具中,ndk-build主要用来编译native代码,它在windows和linux平台下均有响应的版本可使用。它的用法仿佛和在android源码下编译1个模块使用mm命令很类似。之所以说他们类似是由于他们都需要1个Android.mk文件,而且文件的格式完全1样,比如说有以下Android.mk:

LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_PRELINK_MODULE := false LOCAL_SRC_FILES := hello.c LOCAL_MODULE := hello include $(BUILD_EXECUTABLE)

我们在Android源码目录下使用mm命令编译该模块和在windows下使用ndk-build编译该模块都能产生libhello.so库,表面上还真看不出差别。
使用ndk-build编译native代码时,除需要Android.mk文件以外,可能有必要添加1个Application.mk,这个文件通常是由1行:

APP_ABI := x86

这里我们指明了需要编译的2进制库的格式。ABI(Application Binary Interface)与处理器相干,对arm处理器,APP_ABI 可能要配置成 armeabi ,对mips处理器,APP_ABI应当配置为mips,固然,我们还可以1次生成所有平台的库,此时只需要给APP_ABI赋值ALL就能够了。

2.jni的初步认识

2.1 JNI的作用

JNI(Java Native Interface)它提供了若干的API实现了Java和其他语言的通讯(主要是C&C++)。从Java1.1开始,JNI标准成为java平台的1部份,它允许Java代码和其他语言写的代码进行交互。以上是百度百科上copy的话,也算是交代了下JNI的作用吧。

2.2 JNI使用流程

我们使用JNI的出发点1般都是System.loadLibrary(“xxx”);开始的,xxx代表了需要加载的库名。可以认为是它加载我们c/c++代码到虚拟机中,这样,我们的Java虚拟机就知道了c/c++中的函数了,以后,我们就能够调用它。
因此,使用jni只需两步:
1.首先,我们要有1个动态库,这个库我们可使用ndk-build来编译生成。
2.其次,我们需要使用System.loadLibrary(“xxx”)来加载这个库,加载完成后,就能够和本地代码交互了。
2.3 查阅1个使用JNI的c文件
为了认识JNI,找1个使用JNI的文件,比如:android-ndk\android-ndk-r10\samples\hello-gl2\jni\gl_code.cpp:

... extern "C" { JNIEXPORT void JNICALL Java_com_android_gl2jni_GL2JNILib_init(JNIEnv * env, jobject obj, jint width, jint height); JNIEXPORT void JNICALL Java_com_android_gl2jni_GL2JNILib_step(JNIEnv * env, jobject obj); }; JNIEXPORT void JNICALL Java_com_android_gl2jni_GL2JNILib_init(JNIEnv * env, jobject obj, jint width, jint height) { setupGraphics(width, height); } JNIEXPORT void JNICALL Java_com_android_gl2jni_GL2JNILib_step(JNIEnv * env, jobject obj) { r

这里节选了其中的1些,我们会发现其中有很多奇怪的字段,比如JNIEXPORT 、JNICALL等,所以,接下来,我们得先弄清楚它们的意义。

2.4 JNIEXPORT 和 JNICALL

这两个字段定义在jni.h中,定义以下:

#define JNIIMPORT #define JNIEXPORT __attribute__ ((visibility ("default"))) #define JNICALL __NDK_FPABI__

由于在在Windows中编译dll动态库时,如果动态库中的函数要被外部调用,需要在函数声明中添加 attribute ((visibility (“default”)))标识,表示将该函数导出在外部可以调用。在Linux/Unix系统中,这两个宏可以省略不加。由于在Linux/Unix平台下,这两个宏为空,所以加不加都没关系,固然还是建议加上哈,这样linux下的代码就能够直接拿到linux下编译了。

2.4 extern “C”

extern “C”从字面上看有两部份的内容:extern和“C”
extern是编程语言中的1种属性,它表征了变量、函数等类型的作用域(可见性)属性,是编程语言中的关键字。当进行编译时,该关键字告知编译器它所声明的函数和变量等可以在本模块或文件和其他模块或文件中使用。
“C”表明了1种编译规约。
因此,extern “C”表明了1种编译规约,其中extern是关键字属性,“C”表征了编译器链接规范。
使用extern “C” 声明的函数将采取C语言的编译方式编译,也就是说只有在C++代码中extern “C”才成心义,之所以这样显示声明适应C语言的编译方式编译该代码块,是由于c和c++是有差异的,举例来讲,有以下函数:
void hello(int,int);
这个函数在C编译器中,它的函数名师_hello,而在c++编译器中它的函数名是hello_int_int,之所以这样是由于c++支持函数重载,函数名可以相同,表征1个函数的除函数名还有函数的参数列表。这由于有如此不同,因此我们可以想象以下情形:
加入我要在c++中调用1个c函数
1.首先,我要在hello.h中声明hello(int,int)函数,然后在对应的.c文件中实现它。
2.c++文件需要包括hello.h文件,然后履行hello(1,1);完成调用。
那末此时c编译器生成的函数名为_hello。而c++编译器会寻觅_hello_int_int的函数名,这不就找不到了吗?
因此,extern “C”主要用于c++代码调用c代码时,避免出现函数找不到的问题。

3.JVM查找native代码的扼要进程

JVM查找native方法有两种方式:
1.静态方式:依照JNI规范的命名规则
2.动态方式:调用JNI提供的RegisterNatives函数,将本地函数注册到JVM中。

3.1静态方式

静态方式使用的是依照JNI的命名规范来查找native函数,JNI函数命名规则为:
Java_类全路径_方法名
比如我们打算向com.jinwei.hellotest包中的MainActivity类注册名为sayHello的方法,那末,我们的函数命名就应当为:Java_com_jinwei_jnitesthello_MainActivity_sayHello

3.2动态方式

了解动态注册就要触及到System.loadLibrary函数的工作流程了,这个函数打开1个动态库后,会找到JNI_OnLoad这个函数的地址,然后调用这个函数,因此我们可以在这个函数中完成向JVM注册native方法的工作。
比如,Android源码中有以下代码片断:

jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) { JNIEnv* env = NULL; jint result = -1; if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) { ALOGE("ERROR: GetEnv failed\n"); goto bail; } assert(env != NULL); if (register_android_media_ImageWriter(env) != JNI_OK) { ALOGE("ERROR: ImageWriter native registration failed"); goto bail; } ...

JNI_OnLoad调用register_android_media_ImageWriter函数进1步注册native方法:

int register_android_media_ImageWriter(JNIEnv *env) { ... int ret2 = AndroidRuntime::registerNativeMethods(env, "android/media/ImageWriter$WriterSurfaceImage", gImageMethods, NELEM(gImageMethods)); return (ret1 || ret2); }

该函数中使用AndroidRuntime::registerNativeMethods真正完成native方法的注册,这其中用到1个结构体:gImageMethods,其定义以下:

static JNINativeMethod gImageMethods[] = { {"nativeCreatePlanes", "(II)[Landroid/media/ImageWriter$WriterSurfaceImage$SurfacePlane;", (void*)Image_createSurfacePlanes }, {"nativeGetWidth", "()I", (void*)Image_getWidth }, {"nativeGetHeight", "()I", (void*)Image_getHeight }, {"nativeGetFormat", "()I", (void*)Image_getFormat }, };

它是1个函数映照表,前边是java层使用的函数名,后边是native层使用的函数名,中间是函数签名。
签名是1种用参数个数和类型辨别同名方法的手段,即解决方法重载问题。
假设有下面Java方法:
long f (int n, String s, int[] arr);
签名后: “(ILjava/lang/String;[I)J”
其中要特别注意的是:
1. 类描写符开头的’L’与结尾的’;’必须要有
2. 数组描写符,开头的’[‘必须有.
3. 方法描写符规则: “(各参数描写符)返回值描写符”,其中参数描写符间没有任何分隔
符号
签名中使用的符号总结以下:
这里写图片描述

4.实战

4.1静态方式

在随意1个目录下添加以下3个文件:
hello.c,Android.mk,Application.mk
hello.c

#include <stdio.h> #include <jni.h> #include <stdlib.h> JNIEXPORT jstring JNICALL Java_com_jinwei_jnitesthello_MainActivity_sayHello(JNIEnv * env, jobject obj){ return (*env)->NewStringUTF(env,"jni say hello to you"); }

Android.mk

LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_PRELINK_MODULE := false LOCAL_MODULE_PATH := hellolib LOCAL_SRC_FILES := hello.c LOCAL_MODULE := hello include $(BUILD_SHARED_LIBRARY)

Application.mk

APP_ABI := armeabi

然后使用cmd进入到该目录下,履行ndk-build。能履行ndk-build是由于我已把ndk-build工具所在的目录添加到环境变量path中了。编译成功后会在上级目录的libs目录的armeabi目录下生成libhello.so文件。
在Android Studio工程的src/main下新建jniLibs目录,在jniLibs目录下新建armeabi目录,然后把libhello.so拷贝到armeabi目录下。这样,就能够在Android利用程序中访问libhello.so库了。关于jniLibs目录的名字,这是Android gradle默许的jni库目录,我们是可以自定义的,这里就不啰嗦了,可以参考下我的详细配置Android Studio中的Gradle
以后运行app就能够看到jni say hello to you的字样了。
1下是Android Studio中相干的文件:
MainActivity.java

package com.jinwei.jnitesthello; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.widget.TextView; public class MainActivity extends AppCompatActivity { TextView textView = null; static { System.loadLibrary("hello"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView) findViewById(R.id.text); String hehe = this.sayHello(); textView.setText(hehe); } native public String sayHello(); }

activity_main.xml

<?xml version="1.0" encoding="utf⑻"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.jinwei.jnitesthello.MainActivity"> <TextView android:id="@+id/text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" /> </RelativeLayout>

4.2 动态方式

动态注册的使用流程之前已分析过,它和静态的区分也只体现在hello.c文件上,这里只把hello.c文件贴出来:

#include <stdio.h> #include <jni.h> #include <stdlib.h> jstring native_sayHello(JNIEnv * env, jobject obj){ return (*env)->NewStringUTF(env,"jni say hello to you"); } static JNINativeMethod gMethods[] = { {"sayHello", "()Ljava/lang/String;", (void *)native_sayHello}, }; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv* env = NULL; //注册时在JNIEnv中实现的,所以必须首先获得它 jint result = -1; if((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_4) != JNI_OK) //从JavaVM获得JNIEnv,1般使用1.4的版本 return -1; jclass clazz; static const char* const kClassName="com/jinwei/jnitesthello/MainActivity"; clazz = (*env)->FindClass(env, kClassName); //这里可以找到要注册的类,条件是这个类已加载到java虚拟机中。 这里说明,动态库和有native方法的类之间,没有任何对应关系。 if(clazz == NULL) { printf("cannot get class:%s\n", kClassName); return -1; } if((*env)->RegisterNatives(env,clazz,gMethods, sizeof(gMethods)/sizeof(gMethods[0]))!= JNI_OK) //这里就是关键了,把本地函数和1个java类方法关联起来。不管之前是不是关联过,1律把之前的替换掉! { printf("register native method failed!\n"); return -1; } return JNI_VERSION_1_4; }

还是1样,使用ndk-build编译,把编译生成的库文件拷贝到android studio工程src/main/jniLibs/armeabi目录下,然后运行该项目便可。
注意:如果你的android装备或虚拟机使用的x86等其他格式的镜像,注意修改Application.mk文件,修改方法文章的第1小节已介绍过了。

总结:通过以上基础知识的介绍和两个实战的案例,我们应当初步理解了Jni工作的进程,对静态方式和动态方式使用JNI有了直观的体验,但JNI毕竟非常复杂,我们还有很多的知识要学习,下1节主要介绍jni类型的转换,就是怎样把java层的String,int等转换到c/c++层对应的类型。

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐