Android开发——JNI和NDK

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. 引用类型描述符

类型描述符
StringLjava/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函数。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值