Android JNI详解
Posted 疯过无痕201314
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android JNI详解相关的知识,希望对你有一定的参考价值。
目录
一、前言:
本篇文章是针对android 开发过程中的使用的jni技术做一些的原理上的解析,不再介绍具体的jni的使用,关于如何在android中使用jni开发的教程可以去网上搜索然后自行尝试。本篇文章主要介绍的比如jni函数的注册、jni和java层的线程映射关系等
二、JNI简介
2.1 JNI 是Java Native Interface的缩写,表示"Java本地调用"。通过JNI技术可以实现
Java调用C程序
C程序调用Java代码
我们的android源码中有很多代码都是Jni的实现的。例如MediaScanner的实现。就是通过jni的技术让我们在java层扫描到媒体相关的资源的
2.2 JNi的使用步骤:
Jni的使用的步骤大概可以如下几步
a) java声明native函数
b) jni实现对应的c函数
c) 编译生成so库
d) java 加载so库,并调用native函数
三、JNI函数注册
我们知道,jni的函数在使用的时候是直接调用java层的native函数的,我们平时在开发jni的时候,可能就是按部就班的,定义一个类,生命几个native函数,然后使用javac、javah等命令,然后再实现对应的C文件,最终编译成so库使用。那么java层的native函数是如何调用到c语言那边的函数的呢?这边就设计到JNI函数的注册问题了。JNI函数分为静态注册和动态注册的方式
3.1静态注册:
静态注册的方式我们平时用的比较多。我们通过javac和javah编译出头文件,然后再实现对应的cpp文件的方式就是属于静态注册的方式。这种调用的方式是由于JVM按照默认的映射规则来匹配对应的native函数,如果没匹配,则会报错。具体的匹配的规则是什么样的呢?可以看一下下面的例子
//Java 层代码JniSdk.java
public class JniSdk
static
System.loadLibrary("test_jni");
public native String showJniMessage();
//Native层代码 jnidemo.cpp
extern "C"
JNIEXPORT jstring JNICALL Java_com_example_dragon_androidstudy_jnidemo_JniSdk_showJniMessage
(JNIEnv* env, jobject job)
return env->NewStringUTF("hello world");
从上面的例子可以看到java层的showJniMessage函数对应的就是c语言那边的Java_com_example_dragon_androidstudy_jnidemo_JniSdk_showJniMessage函数。那么这个是如何映射的呢?其实这个映射是JVM实现的。我们在调用showJniMessage函数的时候,JVM会从JNI库寻找对应的函数并调用。寻找的时候按照如下规则:
我们的JniSdk.java的包名是com.example.dragon.androidstudy.jnidemo。那么showMessage方法完整的路径就是com.example.dragon.androidstudy.jnidemo.JniSdk.showJniMessage。而.在C里面有特殊的函数,所以JVM就将其他替换成了_。并再前面加了Java_标识,就变成了上面的方法
3.2 动态注册
不知道大家注意到没有,每次使用JNI的时候,都要先生命native,然后编译成class。在生成头文件。最后再按照特定的规则去实现native函数。这一整套流程下来不仅繁琐,很多步骤还是没必要的(我们根据没必要生成.h文件,只要在.c文件里面根据对应的规则声明函数即可)。而且JVM在根据静态注册匹配的规则调用函数的时候效率也会比较低。因此有了动态注册的方法,所谓的动态注册,就是我们不用默认的映射规则,直接由我们告诉JVM。java的native函数对应的是C文件里面的哪个函数。废话不多说,直接上例子
public class JniSdk
static
System.loadLibrary("test_jni");
public static native int numAdd(int a, int b);
public native void dumpMessage();
JNINativeMethod g_methods[] =
"numAdd", "(II)I", (void*)add,
"dumpMessage","()V",(void*)dump,
;
jint JNI_OnLoad(JavaVM *vm, void *reserved)
j_vm = vm;
JNIEnv *env = NULL;
if (vm->GetEnv((void**)&env, JNI_VERSION_1_2) != JNI_OK)
LOGI("on jni load , get env failed");
return JNI_VERSION_1_2;
jclass clazz = env->FindClass("com/example/dragon/androidstudy/jnidemo/JniSdk");
//clazz对应的类名的完整路径。把.换成/ g_methods定义的全局变量 1 是g_methods的数组长度。也可以用sizeof(g_methods)/sizeof(g_methods[0])
jint ret = env->RegisterNatives(clazz, g_methods, 2);
if (ret != 0)
LOGI("register native methods failed");
return JNI_VERSION_1_2;
上面的JNI_OnLoad函数是在我们通过System.loadlibrary函数的时候,JVM会回调的一个函数,我们就是在这里做的动态注册的事情,可以看到,我们通过env->RegisterNatives注册。这个env是jni函数实现的核心,我们后面再讲。这里主要讲解一下g_methods对象,下面是JNINativeMethods结构体的定义
typedef struct
const char* name; //对应java中native的函数名
const char* signature; //java中native函数的函数签名
void* fnPtr; //C这边实现的函数指针
JNINativeMethod;
其中signature指的是函数的签名。这个在下个小节具体讲解
四、函数签名
根据第三小节中聊到的,我们在动态注册的时候需要用到函数的签名。那么什么是函数的签名?为什么需要函数的签名呢?如何获取函数的签名?接下来针对这三个问题做一个讲解
4.1 什么是函数签名:
所谓函数签名,简单点的理解可以理解成一个函数的唯一标识,一个签名对应着一个函数的签名。这个是一一对应的关系。有些人可能会问:函数名不能作为标识么?答案当然是否定的
4.2 为什么需要函数的签名:
我们知道,java是支持函数重载的。一个类里面可以有多个同名但是不同参数的函数,所以函数名+参数名才唯一构成一个函数标识,因此我们需要针对参数做一个签名标识。这样jni层才能唯一识别到一个函数
4.3 如何获取函数的签名
函数的签名是针对函数的参数以及返回值进行组成的。它遵循如下格式(参数类型1;参数类型2;参数类型3.....)返回值类型。例如我们上面的numAdd函数一样。他在java层的函数声明是
int numAdd(int a, int b)
这里面有两个参数都是int,并且返回值也是int。所以的函数签名是(II)I。而dumpMessage函数没有任何参数,并且返回值也是空,所以它的签名是()V。具体的函数类型对应的签名的映射关系如下
类型标识 | Java类型 | 类型标识 | Java类型 |
Z | boolean | F | float |
B | byte | D | double |
C | char | L/java/language/String | String |
S | short | [I | int[] |
I | int | [Ljava/lang/object | Object[] |
J | long | V | void |
我们如果自己手动去写这种签名的话。很容易出错,有一个工具可以很方便的列出每个函数的签名,我们可以先通过javac命令编译出class文件。然后再通过javap -s -p xxx.class命令列出这个class文件所有的函数签名
public native java.lang.String showJniMessage();
descriptor: ()Ljava/lang/String;
public static native int numAdd(int, int);
descriptor: (II)I
public native void dumpMessage();
descriptor: ()V
五、JNIEnv
前面几个小节都是讲了java函数和jni函数的一个映射和调用关系,一旦映射关系建立之后,在java层调用native函数就变得很简单了。但是我们的程序并不仅仅是这么简单的需求,绝大多数的时候需要jni函数调用java层的函数,比如我们进行后台文件操作后将结果通知到上层,这个时候就需要调用到java的函数了。那么这个时候该如何实现呢?这个时候就要到我们的主角JNIEnv出场了。这个JNIEnv可以说是贯穿了整个JNI技术的核心,因此我们会着重讲解这个
5.1 何为JNIEnv:
JNIEnv是JVM内部维护的一个和线程相关的代表JNI环境的结构体,这个结构体是和线程相关的。并且C函数里面的线程与java函数中的线程是一一对应关系。也就是说,如果在java里的某个线程调用jni接口,不管调用多少个JNI接口,传递的JNIEnv都是同一个对象。因为这个时候java只有一个线程,对应的JNI也只有一个线程,而JNIEnv是跟线程绑定的,因此也只有一个
mShowTextBtn.setOnClickListener(new View.OnClickListener()
@Override
public void onClick(View v)
JniSdk.getInstance().dumpArgs(arg);
JniSdk.getInstance().dumpArgsBak(arg);
);
void dumpArg(JNIEnv *env, jobject call_obj, jobject arg_obj)
LOGI("on dump arg function, env :%p", env);
void dumpArgBak(JNIEnv *env, jobject call_obj, jobject arg_obj)
LOGI("on dump arg bak function, env :%p", env);
//实际输出
//on dump arg function, env :0xb7976980
//on dump arg bak function, env :0xb7976980
可以看到,在不同的两个JNI接口里面打印的JNIEnv是一样的,如果是不同的线程,打印的JNIEnv是不一样的。感兴趣的朋友可以自己去做实验
5.2 通过JNIEnv调用java对象方法
通过JNIEnv调用方法大致可以分为以下两步:
a、获取到对象的class,并且通过class获取成员属性
b、通过成员属性设置获取对应的值或者调用对应的方法
这个网上的例子很多,具体的不在本篇的讨论的范围。大家可以自行百度实验
public class JniSdk
private int mIntArg = 5;
public int getArg()
return mIntArg;
void dump(JNIEnv *env, jobject obj)
LOGI("this is dump message call: %p", obj);
jclass jc = env->GetObjectClass(obj);
jmethodID jmethodID1 = env->GetMethodID(jc,"getArg","()I");
jfieldID jfieldID1 = env->GetFieldID(jc,"mIntArg","I");
jint arg1 = env->GetIntField(obj,jfieldID1);
jint arg = env->CallIntMethod(obj, jmethodID1);
LOGI("show int filed: %d, %d",arg, arg1);
这边需要注意的一点是:如果jni方法是通过static方式调用的话,这边的jobject表示的是jclass对象,需要进行强转,并不表示一个独立的对象
5.3 跨线程如何调用java方法
上面的java调用jni或者jni调用java的方法,大家感觉没什么特别之处对吧。无非就是调用JNIEnv的几个接口而已。但事实上并非如此,上面可以直接调用的原因是java调用到jni层的时候始终都在同一个线程,因此再jni层可以直接操作从java层传递下来的JNIEnv对象来实现各种操作。但是如果是在JNI层创建的一个额外的线程想调用Java方法呢?这个时候又该如何操作呢?
上面说过,JNIEnv是与线程一一对应的。实际上这里的一一对应是指跟java和jni的线程共同对应。什么意思呢?这里可以用几张图来表示
上面的图表示的JVM内部维护一个关于线程映射的表,一个java线程和一个jni线程共同拥有一个JNIEnv。如果java线程调用native函数的时候,JVM还没有为这两个线程建立起映射关系,那么就会新创建一个JNIEnv并且传递到jni线程,如果之前已经有创建过映射关系。那么就直接采用原来的JNIEnv 。如5.1所描述的那样,两个JNIEnv的对象是相同的。反之也一样,如果jni调用java线程的话,那么需要向JVM申请获取到已经映射的JNIEnv,如果之前未映射过的话。那么就重新创建一个。这个方法就是AttachCurrentThread。
JNIEnv *g_env;
void *func1(void* arg)
LOGI("into another thread");
//使用全局保存的g_env,进行操作java对象的时候程序会崩溃
jmethodID jmethodID1 = g_env->GetMethodID(jc,"getArg","()I");
jint arg = g_env->CallIntMethod(obj, jmethodID1);
//通过这种方法获取的env,然后再进行获取方法进行操作不会崩溃
JNIEnv *env;
j_vm->AttachCurrentThread(&env,NULL);
void dumpArg(JNIEnv *env, jobject call_obj, jobject arg_obj)
LOGI("on dump arg function, env :%p", env);
g_env = env;
pthread_t *thread;
pthread_create(thread,NULL, func1, NULL);
上面的demo表示JNIEnv跟每个线程是捆绑的,无法在线程B访问到线程A的JNIEnv。所以通过保存g_env的方式去使用是不行的。而是应该要通过AttachCurrentThread方法进行获取新的JNIEnv,然后再进行调用
六、垃圾回收
我们都知道java创建的对象是由垃圾回收器来回收和释放内存的。那么java的那种方式在jni那边是否行得通的呢?答案是否?在JNI层。如果使用ObjectA = ObjectB的方式来保存变量的话。这种是没办法保存变量的。随时会被回收,我们必须要通过env->NewGlobalRef和env->NewLocalRef的方式来创建,还有一个env->NewWeakGlobalRef(这种很少使用)
那么这两种的生命周期如何呢?这边直接给出结论:
NewLocalRef创建的变量再函数调用结束后会被释放掉
NewGlobalRef创建的变量除非手动delete掉,否则会一直存在
至此,JNI的运行机制的理解已经总结结束了
JNI_Android项目中调用.so动态库实现详解
转自:http://www.yxkfw.com/?p=7223
1. 在Eclipse中创建项目:TestJNI
2. 新创建一个class:TestJNI.java
package com.wwj.jni;
public class TestJNI {
public native boolean Init();
public native int Add(int x, int y);
public native void Destory();
}
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_wwj_jni_TestJNI */
#ifndef _Included_com_wwj_jni_TestJNI
#define _Included_com_wwj_jni_TestJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_wwj_jni_TestJNI
* Method: Init
* Signature: ()Z
*/
JNIEXPORT jboolean JNICALL Java_com_wwj_jni_TestJNI_Init
(JNIEnv *, jobject);
/*
* Class: com_wwj_jni_TestJNI
* Method: Add
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_com_wwj_jni_TestJNI_Add
(JNIEnv *, jobject, jint, jint);
/*
* Class: com_wwj_jni_TestJNI
* Method: Destory
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_wwj_jni_TestJNI_Destory
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
#ifndef _TEST_JNI_ADD_H_
#define _TEST_JNI_ADD_H_
class CAdd {
public:
CAdd();
~CAdd();
int Add(int x, int y);
};
#endif
Add.cpp
#include "Add.h"
CAdd::CAdd() {
}
CAdd::~CAdd() {
}
int CAdd::Add(int x, int y) {
return x + y;
}
com_wwj_jni_TestJNI.cpp的实现:
#include <stdio.h>
#include <stdlib.h>
#include "com_wwj_jni_TestJNI.h"
#include "Add.h"
CAdd *pCAdd = NULL;
JNIEXPORT jboolean JNICALL Java_com_wwj_jni_TestJNI_Init(JNIEnv *env,
jobject obj) {
if (pCAdd == NULL) {
pCAdd = new CAdd;
}
return pCAdd != NULL;
}
JNIEXPORT jint JNICALL Java_com_wwj_jni_TestJNI_Add(JNIEnv *env, jobject obj,
jint x, jint y) {
int res = -1;
if (pCAdd != NULL) {
res = pCAdd->Add(x, y);
}
return res;
}
JNIEXPORT void JNICALL Java_com_wwj_jni_TestJNI_Destory(JNIEnv *env, jobject obj)
{
if (pCAdd != NULL)
{
pCAdd = NULL;
}
}
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := TestJNI
LOCAL_SRC_FILES := com_wwj_jni_TestJNI.cpp
LOCAL_SRC_FILES += Add.cpp
include $(BUILD_SHARED_LIBRARY)
APP_PROJECT_PATH := $(call my-dir)
APP_MODULES := TestJNI
package com.wwj.jni;
import android.os.Bundle;
import android.widget.TextView;
import android.app.Activity;
public class TestJNIActivity extends Activity {
private TextView textView;
static {
// 加载动态库
System.loadLibrary("TestJNI");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView) findViewById(R.id.textview);
TestJNI testJNI = new TestJNI();
// 调用native方法
boolean init = testJNI.Init();
if (init == true) {
// 调用Add函数
int sum = testJNI.Add(100, 150);
textView.setText("你真是个" + sum);
} else {
textView.setText("你比二百五还要二百五");
}
testJNI.Destory();
}
}
运行项目,效果图如下:
以上是关于Android JNI详解的主要内容,如果未能解决你的问题,请参考以下文章