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

一.开发工具

所谓工欲善其事,必先利其器,在学习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源码下编译一个模块使用mm命令很相似。之所以说他们相似是因为他们都需要一个Android.mk文件,而且文件的格式完全一样,比如说有如下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文件之外,可能有必要添加一个Application.mk,这个文件通常是由一行:

APP_ABI := x86

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

二.jni的初步认识

2.1 JNI的作用

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

2.2 JNI使用流程

我们使用JNI的起点一般都是System.loadLibrary(“xxx”);开始的,xxx代表了需要加载的库名。可以认为是它加载我们c/c++代码到虚拟机中,这样,我们的Java虚拟机就知道了c/c++中的函数了,之后,我们就可以调用它。 
因此,使用jni只需两步: 
1.首先,我们要有一个动态库,这个库我们可以使用ndk-build来编译生成。 
2.其次,我们需要使用System.loadLibrary(“xxx”)来加载这个库,加载完成后,就可以和本地代码交互了。 
2.3 查阅一个使用JNI的c文件 
为了认识JNI,找一个使用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

这里节选了其中的一些,我们会发现其中有很多奇怪的字段,比如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是编程语言中的一种属性,它表征了变量、函数等类型的作用域(可见性)属性,是编程语言中的关键字。当进行编译时,该关键字告诉编译器它所声明的函数和变量等可以在本模块或者文件以及其他模块或文件中使用。 
“C”表明了一种编译规约。 
因此,extern “C”表明了一种编译规约,其中extern是关键字属性,“C”表征了编译器链接规范。 
使用extern “C” 声明的函数将采用C语言的编译方式编译,也就是说只有在C++代码中extern “C”才有意义,之所以这样显示声明适应C语言的编译方式编译该代码块,是因为c和c++是有差异的,举例来说,有如下函数: 
void hello(int,int); 
这个函数在C编译器中,它的函数名师_hello,而在c++编译器中它的函数名是hello_int_int,之所以这样是因为c++支持函数重载,函数名可以相同,表征一个函数的除了函数名还有函数的参数列表。这因为有如此不同,因此我们可以想象如下情景: 
加入我要在c++中调用一个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代码时,避免出现函数找不到的问题。

三.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函数的工作流程了,这个函数打开一个动态库后,会找到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函数进一步注册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方法的注册,这其中用到一个结构体: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 },
};

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

四.实战

4.1静态方式

在随便一个jni目录下添加如下三个文件: 
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的字样了。 
一下是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-8"?>
<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.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) //这里就是关键了,把本地函数和一个java类方法关联起来。不管之前是否关联过,一律把之前的替换掉!
{
printf("register native method failed!\n");
return -1;
} return JNI_VERSION_1_4;
}

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

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

Android jni/ndk编程一:jni初级认识与实战体验的更多相关文章

  1. Delphi使用android的NDK是通过JNI接口,封装好了,不用自己写本地代码,直接调用

    一.Android平台编程方式:      1.基于Android SDK进行开发的第三方应用都必须使用Java语言(Android的SDK基于Java实现)      2.自从ndk r5发布以后, ...

  2. Android之NDK编程(JNI)

    转自:http://www.cnblogs.com/xw022/archive/2011/08/18/2144621.html NDK编程入门--C回调JAVA方法   一.主要流程 1.  新建一个 ...

  3. Android之NDK环境配置+JNI开发+so文件编译

    前言 这边Android作为日常记录,虽然破坏了文章队形~   最近人工智能挺火的,也稍微了解了一些库,比如关于视觉库openCV.要在安卓下调用这些C/C++库,需要用到JNI开发,在此把过程分享一 ...

  4. Android的NDK开发(3)————JNI数据类型的详解

    在Java中有两类数据类型:primitive types,如,int, float, char:另一种为reference types,如,类,实例,数组. 注意:数组,不管是对象数组还是基本类型数 ...

  5. Android的NDK开发(4)————JNI数据结构之JNINativeMethod

    转至:http://blog.csdn.net/conowen/article/details/7524744 1.JNINativeMethod 结构体的官方定义 typedef struct { ...

  6. [转]Android通过NDK调用JNI,使用opencv做本地c++代码开发配置方法

    原文地址:http://blog.csdn.net/watkinsong/article/details/9849973 有一种方式不需要自己配置所有的Sun JDK, Android SDK以及ND ...

  7. Android中NDK的搭建及简单使用 Android.mk相关介绍 JNI的使用

    Android中NDK的搭建及简单使用: 使用NDK,简述其重要步骤:.搭建NDK环境(作用:用于自动生成jni下的.c对应的so文件)---到Android NDK官网或Android官网下载ndk ...

  8. 【转】 Android的NDK开发(1)————Android JNI简介与调用流程

    原文网址:http://blog.csdn.net/conowen/article/details/7521340 ****************************************** ...

  9. 【转】Android用NDK和整套源码下编译JNI的不同

    原文网址:http://www.devdiv.com/android_ndk_jni_-blog-99-2101.html 前些天要写个jni程序,因为才几行代码,想着用ndk开发可能容易些,就先研究 ...

随机推荐

  1. ——Java中的collection和collections的区别

    1.java.util.Collection 是一个集合接口(集合类的一个顶级接口).它提供了对集合对象进行基本操作的通用接口方法.Collection接口在Java 类库中有很多具体的实现.Coll ...

  2. mac 下拉取svn代码

    svn checkout https://113.108.97.187/svn/zkteco/zks-app --username=lucy --password=lucy66 svn checkou ...

  3. js数组的所有方法

    修改器方法 下面的这些方法会改变调用它们的对象自身的值: Array.prototype.copyWithin()  在数组内部,将一段元素序列拷贝到另一段元素序列上,覆盖原有的值. Array.pr ...

  4. 八,kubernetes集群存储卷基础。

    目录 存储卷 存储的分类 emptyDir 测试及使用 hostpath实例 Pod测试挂在共享NFS 写测试清单 测试 pv, pvc 创建几个PV 创建测试的Pod 和 PVC 存储卷 pod运行 ...

  5. 1.Netty 实战前言

    1.参考文档:Netty实战精髓篇 2.Netty介绍:     Netty是基于Java NIO的网络应用框架. Netty是一个NIO client-server(客户端服务器)框架,使用Nett ...

  6. Selenium(1)

    一.Selenium->Se(硒)->功能自动化测试工具=功能自动化测试工具(QTP)<-Mercury(汞) 1.Selenium介绍 (1)Selenium 是针对web被测系统 ...

  7. python操作hive 安装和测试

    方法一:使用pyhive库 如上图所示我们需要四个外部包 中间遇到很多报错.我都一一解决了 1.Connection Issue: thrift.transport.TTransport.TTrans ...

  8. hdu 6088 Rikka with Rock-paper-scissors (2017 多校第五场 1004) 【组合数学 + 数论 + 模意义下的FFT】

    题目链接 首先利用组合数学知识,枚举两人的总胜场数容易得到 这还不是卷积的形式,直接搞的话复杂度大概是O(n^2)的,肯定会TLE.但似乎和卷积有点像?想半天没想出来..多谢Q巨提醒,才知道可以用下面 ...

  9. vue 手机物理返回键关闭弹框

    1.打开弹窗调用 window.history.pishState() 函数 2.关闭弹框 3.mounted 生命周期 监听popstate 事件 4.beforeDestroy 生命周期 移除po ...

  10. http 中文乱码

    string RawUrl = request.Request.RawUrl; string cc= HttpUtility.ParseQueryString(RawUrl.Substring(1, ...