ANDROID NDK(Native Develop Kit)
是一套工具集合,google提供给开发者在java中调用C/C++的代码的功能,一般情况下,用NDK工具可以将C/C++代码编译成so文件,在java中通过loadlibrary加载so来使用C/C++代码。
NDK的价值:
- 平台移植
- 代码复用
- 性能提升
- 代码保护
- 使用第三方的C、C++库
JNI(java Native interface)
jni 是java调用Native语言特性,通过jni使得java和c++可以进行交互,jni 是一种在java 虚拟机机制下执行的代码标准机制。
jni 在Android中的应用场景十分的广泛,主要有音视频、热修复和插件化、逆向开发、系统源码调用等
JNIEXPORT void JNICALL Java_com_seleuco_mame4all_Emulator_init(JNIEnv *env, jclass c, jstring s1, jstring s2) { __android_log_print(ANDROID_LOG_INFO, "mame4all-jni", "init"); const char *str1 = (*env)->GetStringUTFChars(env, s1, 0); load_lib(str1); (*env)->ReleaseStringUTFChars(env, s1, str1); setVideoCallbacks(&myJNI_initVideo,&myJNI_dumpVideo,&myJNI_changeVideo); setAudioCallbacks(&myJNI_openAudio,&myJNI_dumpAudio,&myJNI_closeAudio); const char *str2 = (*env)->GetStringUTFChars(env, s2, 0); __android_log_print(ANDROID_LOG_INFO, "mame4all-jni", "path %s",str2); setGlobalPath(str2); (*env)->ReleaseStringUTFChars(env, s2, str2); android_main(0, NULL); } static void load_lib(const char *str) { char str2[256]; strcpy(str2,str); strcpy(str2+strlen(str),"/libMAME4all.so"); #ifdef DEBUG __android_log_print(ANDROID_LOG_DEBUG, "mame4all-jni", "Attempting to load %s\n", str2); #endif if(libdl!=NULL) return; libdl = dlopen(str2, RTLD_NOW); if(!libdl) { __android_log_print(ANDROID_LOG_ERROR, "mame4all-jni", "Unable to load libMAME4all.so: %s\n", dlerror()); return; } android_main = dlsym(libdl, "android_main"); setVideoCallbacks = dlsym(libdl, "setVideoCallbacks"); setAudioCallbacks = dlsym(libdl, "setAudioCallbacks"); setPadStatus = dlsym(libdl, "setPadStatus"); setGlobalPath = dlsym(libdl, "setGlobalPath"); setMyValue = dlsym(libdl, "setMyValue"); getMyValue = dlsym(libdl, "getMyValue"); setMyAnalogData = dlsym(libdl, "setMyAnalogData"); }
以上jni的命名来自MAME模拟器的一段源码,其中:
- Java_com_seleuco_mame4all: 是包名
- Emulator:是类名
- init : 是方法名
- JNIExport 和 JNICALL 是关键字
JNI 实现步骤
1. 在java中声明相应的NATIVE方法
protected static native void init(String libPath,String resPath);
synchronized public static native void setPadData(int i, long data);
synchronized public static native void setAnalogData(int i, float v1, float v2);
public static native int getValue(int key);
public static native void setValue(int key, int value);
2. 编译java源码得到.class文件,再通过javah -jni 导出.h文件(这个过程也可直接手动完成)
JNIEXPORT jint JNICALL Java_com_seleuco_mame4all_Emulator_getValue (JNIEnv *env, jclass c, jint key);
JNIEXPORT void JNICALL Java_com_seleuco_mame4all_Emulator_setValue(JNIEnv *env, jclass c, jint key, jint value);
3. 实现相应的C端和JAVA端方法
JNIEXPORT jint JNICALL Java_com_seleuco_mame4all_Emulator_getValue
(JNIEnv *env, jclass c, jint key)
{
#ifdef DEBUG
// __android_log_print(ANDROID_LOG_DEBUG, "mame4all-jni", "getValue %d",key);
#endif
if(getMyValue!=NULL)
return getMyValue(key);
else
return -1;
}JNIEXPORT void JNICALL Java_com_seleuco_mame4all_Emulator_setValue
(JNIEnv *env, jclass c, jint key, jint value)
{
#ifdef DEBUG
__android_log_print(ANDROID_LOG_DEBUG, "mame4all-jni", "setValue %d=%d",key,value);
#endif
if(setMyValue!=NULL)
setMyValue(key,value);
}
4. 将C代码编译成动态库
5. java 加载动态库
static {
try{
System.loadLibrary("mame4all-jni");
}catch(java.lang.Error e){
e.printStackTrace();
}
}
6. 执行相应的调用
Native方法注册
1. 静态注册
上述示例为静态注册,不过它有一些缺点:
- jni的函数名称太长
- 声明的native方法的类需要javah生成
- 初次调用需要建立关联
2. 动态注册
jni 中有一种结构用来记录java的native 方法和 jni方法的关联关系,他就是JNINativeMethod,在jni.h中定义
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
动态注册基本思想是在JNI_Onload()函数中通过JNI中提供的RegisterNatives()方法来将C/C++方法和java方法对应起来(注册), 我们在调用 System.loadLibrary的时候,会在C/C++文件中回调一个名为 JNI_OnLoad ()的函数,在这个函数中一般是做一些初始化相关操作, 我们可以在这个方法里面注册函数, 注册整体流程如下:
- 编写Java端的相关native方法
- 编写C/C++代码, 实现JNI_Onload()方法
- 将Java 方法和 C/C++方法通过签名信息一一对应起来
- 通过JavaVM获取JNIEnv, JNIEnv主要用于获取Java类和调用一些JNI提供的方法
- 使用类名和对应起来的方法作为参数, 调用JNI提供的函数RegisterNatives()注册方法
//.java void native_init(){ } //.cpp /*需要注册的函数列表,放在JNINativeMethod 类型的数组中, 以后如果需要增加函数,只需在这里添加就行了 参数: 1.java中用native关键字声明的函数名 2.签名(传进来参数类型和返回值类型的说明) 3.C/C++中对应函数的函数名(地址) */ static JNINativeMethod getMethods[] = { {"native_init","()V",(void*)android_native_init}, }; //此函数通过调用RegisterNatives方法来注册我们的函数 static int registerNativeMethods(JNIEnv* env, const char* className,JNINativeMethod* getMethods,int methodsNum){ jclass clazz; //找到声明native方法的类 clazz = env->FindClass(className); if(clazz == NULL){ return JNI_FALSE; } //注册函数 参数:java类 所要注册的函数数组 注册函数的个数 if(env->RegisterNatives(clazz,getMethods,methodsNum) < 0){ return JNI_FALSE; } return JNI_TRUE; } static int registerNatives(JNIEnv* env){ //指定类的路径,通过FindClass 方法来找到对应的类 const char* className = "**/JniTest"; return registerNativeMethods(env,className,getMethods, sizeof(getMethods)/ sizeof(getMethods[0])); } //回调函数 JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved){ JNIEnv* env = NULL; //获取JNIEnv if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { return -1; } assert(env != NULL); //注册函数 registerNatives ->registerNativeMethods ->env->RegisterNatives if(!registerNatives(env)){ return -1; } //返回jni 的版本 return JNI_VERSION_1_6; }
JNI引用类型
1. 本地引用
JNIEnv提供的函数所返回的引用基本上都是本地引用,本地引用时jni中最常见的引用类型.
- 当native函数返回时,这个本地引用就会被自动释放
- 只在创建它的线程中有效,不能够跨线程使用
- 局部引用是JVM负责的引用类型,受JVM管理
static void android_media_MediaRecorder_native_init(JNIEnv *env){
jclass clazz;
clazz = env->FindClass("android/media/MediaRecorder");
if(clazz == NULL){
return;
}
}
clazz会在函数调用返回后被自动释放,也可以通过JNIEnv的deleteLocalRef函数来手动删除本地引用,DeleteLocalRef函数的使用场景主要是在native函数返回前占用用了大量内存,需要调用DeleteLocalRef函数立即删除本地引用
2. 全局引用
- native函数返回时不会自动释放,因此全局引用需要手动释放,并且不会被GC回收
- 全局引用时可以跨线程使用的
- 全局引用不受得到JVM管理
static void android_media_MediaRecorder_native_init(JNIEnv *env){
jclass clazz;
clazz = env->FindClass("android/media/MediaRecorder");
if(clazz == NULL){
return;
}
//将clazz转变为全局引用mClass
mClass = (jclass)env->NewGlobalRef(clazz);
}
JNIMediaRecorderListener::~JNIMediaRecorderListener(){
JNIEnv* env = AndroidRuntime::getJNIEnv();
//在析构函数中用来释放全局引用
env->DeleteGlobalRef(mClass);
}
3. 弱全局引用
弱全局引用是一种特殊的全局引用,他和全局引用的特性很相似,不同的是弱全局是可以被GC回收的,弱全局引用GC回收之后会指向NULL。JNIEnv的NewWeakGlobalRef函数用来创建弱全局引用,调用DeleteWeakGlobalRef函数来释放弱全局引用;
由于弱全局函数会被GC回收,所以在使用之前需要先判断它是否被回收:
env->IsSameObject(weakGlobalRef,NULL);//弱全局引用和null进行比较,相等返回true,否则false
JNI结构
JNI 接口指针仅在当前线程中起作用,这意味着指针不能从一个线程进入另一个线程。
JNIEXPORT void JNICALL Java_com_seleuco_mame4all_Emulator_setValue
(JNIEnv *env, jclass c, jint key, jint value)
{
#ifdef DEBUG
__android_log_print(ANDROID_LOG_DEBUG, "mame4all-jni", "setValue %d=%d",key,value);
#endif
if(setMyValue!=NULL)
setMyValue(key,value);
}
1. JNIEnv *env
JNIEnv是当前Java线程的执行环境,一个JVM对应一个JavaVM结构,而一个JVM中可能创建多个Java线程,每个线程对应一个JNIEnv结构,它们保存在线程本地存储TLS中
a. 创建Java中的对象
jobject NewObject(JNIEnv *env, jclass clazz,jmethodID methodID, ...):
b. 创建Java类中的String对象
jstring NewString(JNIEnv *env, const jchar *unicodeChars,jsize len):
jstring NewStringUTF(JNIEnv *env, const char *bytes)
c. 创建一个数组
NewBooleanArray()
NewByteArray()
NewCharArray()
NewShortArray()
NewIntArray()
NewLongArray()
NewFloatArray()
NewDoubleArray()
NewObjectArray()
d. 获取数组中某个位置的元素和数组的长度
jobject GetObjectArrayElement(JNIEnv *env,jobjectArray array, jsize index);
jsize GetArrayLength(JNIEnv *env, jarray array);
2. jclass c
3. jint key , jint value
a. 基本数据类型
b. 引用数据类型
c. 描述符:
i. 基本类型描述符
ii. 引用类型描述符
类型 | 描述符 |
String | Ljava/lang/String; |
String[ ] | [Ljava/lang/String; |
Object[ ] | [Ljava/lang/Object; |
iii. 方法描述符
方法 | 描述符 |
String test ( ) | Ljava/lang/String; |
int f (int i, Object object) | (ILjava/lang/Object;)I |
void set (byte[ ] bytes) | ([B)V |
JNI 原理
1. javaVM
java语言的执行环境是java虚拟机(JVM),JVM 其实就是主机环境中的一个进程,每个jvm进程都会有一个javaJVM结构体,该结构体在创建java虚拟机时被返回,在jni环境中创建JVM的函数为:
JNI_CreateJavaVM(JavaVM** pvm,void**penv,void* args)
其中JavaVM是Java虚拟机在JNI层的代表,JNI全局仅仅有一个JavaVM结构中封装了一些函数指针(或叫函数表结构),JavaVM中封装的这些函数指针主要是对JVM操作接口。另外,在C和C++中的JavaVM的定义有所不同,在C中JavaVM是JNIInvokeInterface_类型指针,而在C++中有对JNIInvokeInterface_进行了一次封装,比C中少了一个参数,这也是为什么JNI代码更推荐使用C++来编写的原因。
a. c 文件
JNIEXPORT void JNICALL Java_com_seleuco_mame4all_Emulator_init
(JNIEnv *env, jclass c, jstring s1, jstring s2)
{
__android_log_print(ANDROID_LOG_INFO, "mame4all-jni", "init");
const char *str1 = (*env)->GetStringUTFChars(env, s1, 0);
load_lib(str1);
(*env)->ReleaseStringUTFChars(env, s1, str1);
setVideoCallbacks(&myJNI_initVideo,&myJNI_dumpVideo,&myJNI_changeVideo);
setAudioCallbacks(&myJNI_openAudio,&myJNI_dumpAudio,&myJNI_closeAudio);
const char *str2 = (*env)->GetStringUTFChars(env, s2, 0);
__android_log_print(ANDROID_LOG_INFO, "mame4all-jni", "path %s",str2);
setGlobalPath(str2);
(*env)->ReleaseStringUTFChars(env, s2, str2);
android_main(0, NULL);
}
b. cpp文件
struct FJniMethod
{
JNIEnv* env;
jclass classID;
jmethodID methodID;
};
static JavaVM* GJavaVM = NULL;
static jobject GJavaObject = NULL;
void appNativeInit(void* env ,void* obj)
{
JNIEnv* theEnv = (JNIEnv*)env;
jobject theObj = (jobject)obj;
theEnv->GetJavaVM(&GJavaVM);
GJavaObject = theEnv->NewGlobalRef(theObj);
}
static bool GetMethodID(FJniMethod& m, const char* methodName, const char* param)
{
if (GJavaVM == NULL || GJavaObject == NULL)
{
log(_T("Java VM or JavaObject is null"));
return FALSE;
}
if (GJavaVM->AttachCurrentThread(&m.env, 0) < 0)
{
log(_T("Failed to get the environment using AttachCurrentThread()"));
return FALSE;
}
m.classID = m.env->GetObjectClass(GJavaObject);
if (!m.classID)
{
log(_T("Failed to GetObjectClass"));
return FALSE;
}
m.methodID = m.env->GetMethodID(m.classID, methodName, param);
if (!m.methodID)
{
log(_T("Failed to find method id of %s(%s)"), methodName, param);
return FALSE;
}
return TRUE;
}
2. JNIEnv
JNIEnv是当前Java线程的执行环境,一个JVM对应一个JavaVM结构,而一个JVM中可能创建多个Java线程,每个线程对应一个JNIEnv结构,它们保存在线程本地存储TLS中
JNIEnv结构也是一个函数表,在本地代码中通过JNIEnv的函数表来操作Java数据或者调用Java方法。也就是说,只要在本地代码中拿到了JNIEnv结构,就可以在本地代码中调用Java代码
JNIEnv是一个指针,指向一个线程相关的结构,线程相关结构指向JNI函数指针数组,这个数组中存放了大量的JNI函数指针,这些指针指向了详细的JNI函数。