Android.NDK入门

ndk入门

通常在以下几种情况下考虑使用JNI:

  • 对处理速度有要求
    Java代码执行速度要比本地代码(C/C++)执行速度慢一些,如果对程序的执行速度有较高的要求,可以考虑使用C/C++编写代码,然后在通过Java代码调用基于C/C++编写的部分。
  • 硬件控制
    如前面所述,Java运行在虚拟机中,和真实运行的物理硬件之间是相互隔离的,通常我们使用本地代码C实现对硬件驱动的控制,然后再通过Java代码调用本地硬件控制代码。
  • 复用本地代码
    如果程序的处理逻辑已经由本地代码实现并封装成了库,就没有必要再重新使用Java代码实现一次,直接复用该本地代码,即提高了编程效率,又确保了程序的安全性和健壮性。

环境变量设置

» echo $PATH
~/Mac_tras/ndk/android-ndk-r10d

jni/Android.mk

LOCAL_PATH := $(call my-dir)              //表示当前目录
include $(CLEAR_VARS)                      //清除全局变量
LOCAL_MODULE    := hello-jni           //编译的目标对象,系统将会生成 'libhello-jni.so'文件,供java文件调用 
LOCAL_SRC_FILES := hello-jni.c         //编译的源文件,系统将根据该文件来生成目标对象 
LOCAL_LDLIBS := -lz                     //第三方库如 -llog
include $(BUILD_EXECUTABLE)           //elf $(BUILD_SHARED_LIBRARY)动态库hello-jni.so,$(BUILD_STATIC_LIBRARY)静态库hello-jni.a,$(BUILD_EXECUTABLE)是可执行文件hello-jni

jni/Application.mk

 APP_ABI := armeabi x86 //目标架构如 arm/x86...

c/c++文件需和 Android.mk 一起放在 jni 目录下,编译

» ndk-build  
[armeabi] Compile thumb  : hello <= hello.c
[armeabi] Executable     : hello
[armeabi] Install        : hello => libs/armeabi/hello

运行

» adb push libs/armabi/hello /data/local/tmp
» adb shell  /data/local/tmp/hello

java使用 native 方法基本流程:

  • 将需要本地实现的 java 方法加上 native 申明

      public native String  stringFromJNI();
    
  • 使用 javah 生成.h 头文件
  • 在本地代码中实现 native 方法

      jstring
      Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,
                                                        jobject thiz )
      {
      #if defined(__arm__)
         #define ABI "armeabi"
      #elif defined(__i386__)
         #define ABI "x86"
      #else
         #define ABI "unknown"
      #endif
          return (*env)->NewStringUTF(env, "Hello from JNI ! Compiled with ABI " ABI ".");   //在C中必须先对env和vm间接寻址(得到的内容仍然是一个指针),在调用方法时要将env或vm传入作为第一个参数.
      }
    
  • 编译上述本地方法,生成.so 动态链接库
  • 在 java 类中加载上述动态链接库

      static {
          System.loadLibrary("hello-jni");
      }
    

    本地代码库的加载系统方法System.loadLibrary在static静态代码块中实现的,这是因为静态代码块只会在Java类加载时被调用,并且只会被调用一次。

    在Java代码中通过System.loadLibrary方法来加载本地代码库,当本地代码动态库被JVM加载时,JVM会自动调用本地代码中的JNI_OnLoad函数

  • java 代码中其他地方都可以郑成功调用这一 native 方法

关键结构体JNIEnv&JavaVM

JNIEnv是一个与线程相关的变量,不同线程的JNIEnv彼此独立。JavaVM是虚拟机在JNI层的代表,在一个虚拟机进程中只有一个JavaVM,因此该进程的所有线程都可以使用这个JavaVM。当后台线程需要调用JNI native时,在native库中使用全局变量保存JavaVM尤为重要,这样使得后台线程能通过JavaVM获得JNIEnv。

Java语言的执行环境是Java虚拟机(JVM),JVM其实是主机环境中的一个进程,每个JVM虚拟机进程在本地环境中都有一个JavaVM结构体,该结构体在创建Java虚拟机时被返回,在JNI中创建JVM的函数为JNI_CreateJavaVM。

在JVM进程中,JavaVM是全局唯一的,而JNIEnv则是按线程分配。

JNI_CreateJavaVM(JavaVM **pvm, void **penv, void*args);  //AndroidRuntime的成员函数startVm调用另外一个函数JNI_CreateJavaVM来创建以及初始化一个Dalvik虚拟机实例

每一个Dalvik虚拟机实例还有一个JNI环境列表

JNIEnv* dvmCreateJNIEnv(Thread* self)   //Dalvik虚拟机启动过程中,会调用dvmCreateJNIEnv方法创建JNIEnv

JNI_CreateJavaVM主要完成以下四件事情。

  1. 为当前进程创建一个Dalvik虚拟机实例,即一个JavaVMExt对象。

  2. 为当前线程创建和初始化一个JNI环境,即一个JNIEnvExt对象,这是通过调用函数dvmCreateJNIEnv来完成的。

  3. 将参数vm_args所描述的Dalvik虚拟机启动选项拷贝到变量argv所描述的一个字符串数组中去,并且调用函数dvmStartup来初始化前面所创建的Dalvik虚拟机实例。

  4. 调用函数dvmChangeStatus将当前线程的状态设置为正在执行NATIVE代码,并且将面所创建和初始化好的JavaVMExt对象和JNIEnvExt对象通过输出参数p_vm和p_env返回给调用者

jni.h

struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif

在C和C++中JavaVM的定义有所不同,在C中JavaVM是JNIInvokeInterface类型指针,而在C++中又对JNIInvokeInterface进行了一次封装,比C中少了一个参数,这也是为什么JNI代码更推荐用C++来编写的原因。

结构体存储大多都指针函数 (函数表结构)

jstring     (*NewStringUTF)(JNIEnv*, const char*);

这里是一些常见的数据结构的定义,可以看出这里预处理命令#ifdef __cplusplus,如果jni是C++实现的话,相关变量其实是一些class的指针.在C里面则更加简单,全部都是”void*“的typedef.

JNIEnv* env实际上等价于声明 const struct JNINativeInterface** env ,因此要调用JNINativeInterface结构体内的函数指针就必须先对env间接寻址.

(env)的类型是const struct JNINativeInterface(指向JNINativeInterface结构体的指针),这时候可以用这个指针调用结构体的成员函数指针,(env)-> GetMethodID(env, jclass, const char, const char*)

关键函数

/* * Prototypes for functions exported by loadable shared libs. These are * called by JNI, not provided by JNI. */
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved);
JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved);

native中 logcat

单元头部加上

#include <android/log.h>
#define LOG_TAG "I'm tag"

log.h声明了函数int __android_log_print(int prio, const char *tag, const char *fmt, ...)

链接器指定__android_log_print函数所在的库文件liblog.so.在Android.mk文件中加上一行LOCAL_LDLIBS := -llog

在native函数中可以用如下语句输出了

__android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,"name=%s,age=%d",”卢斌晖”,28);

第一个参数ANDROID_LOG_DEBUG是枚举常量,它在log.h中定义.

typedef enum android_LogPriority   
{  
    ANDROID_LOG_UNKNOWN = 0,  
    ANDROID_LOG_DEFAULT,    /* only for SetMinPriority() */  
    ANDROID_LOG_VERBOSE,  
    ANDROID_LOG_DEBUG,  
    ANDROID_LOG_INFO,  
    ANDROID_LOG_WARN,  
    ANDROID_LOG_ERROR,  
    ANDROID_LOG_FATAL,  
    ANDROID_LOG_SILENT,     /* only for SetMinPriority(); must be last */  
} android_LogPriority;  

JNI访问Java成员

在JNI中通过下面的函数来获得Java运行环境中的类。

jclass FindClass(const char *name);

name:类全名,包含包名,其实包名间隔符用“/”代替“.”

为了在C/C++中表示Java的属性和方法,JNI在jni.h头文件中定义了jfieldID和jmethodID类型来分别代表Java对象的属性和方法。

使用JNIEnv提供的JNI方法,我们就可以获得属性和方法相对应的jfieldID和jmethodID

jni.h

// 根据属性签名返回 clazz类中的该属性ID
jfieldID GetFieldID(jclass clazz, const char*name, const char *sig);
// 如果获得静态属性ID,则调用下面的函数
jfieldID GetStaticFieldID(jclass clazz, constchar *name, const char *sig);

// 根据方法签名返回clazz类中该方法ID
jmethodID GetMethodID(jclass clazz, const char*name, const char *sig);
// 如果是静态方法,则调用下面的函数实现
jmethodID GetStaticMethodID(jclass clazz, constchar *name, const char *sig);

可以看到这四个方法的参数列表都是一模一样的,下面来分析下每个参数的含义:

  • jclass clazz:要取得成员对应的类
  • const char *name:代表我们要取得的方法名或者属性名
  • const char *sig:代表我们要取得的方法或属性的签名

JNI类型签名 : 可以参照 smali 语法

取得了代表属性和静态属性的jfieldID,就可以使用JNIEnv中提供的方法来获取和设置属性/静态属性

jclass

jclass代表JAVA中的java.lang.Class.

jni.h中jclass的定义如下

#ifdef __cplusplus  
/*Reference types, in C++*/  
class _jobject {};  
class _jclass : public _jobject {}; /*_jclass继承_jobject*/  
typedef _jclass*        jclass;  
#else  
/*Reference types, in C.*/  
typedef void*           jobject;  
typedef jobject         jclass;  
#endif 

在C中jclass代表类型void,在C++中代表类型_jclass.因此jclass是指针,我们能够在log中输出jclass变量值

 __android_log_print(ANDROID_LOG_DEBUG,"native函数中输出","地址=%p",jclass变量);

每次JAVA调用native都必须重新获得jclass,上次调用native所得到的jclass在下次调用native时再使用是无效的.下面是C代码

static jclass StudentClazz;   //全局变量 
jint JNI_OnLoad(JavaVM* vm,void* reserved)  
{  
    JNIEnv* env = NULL;  
    jint result=-1;  
    if( (*vm)->GetEnv(vm,(void**)&env , JNI_VERSION_1_4) != JNI_OK)  
       return result;  
    StudentClazz=(*env)->FindClass(env,"com/example/hellojni/HelloJni$Student");    //初始化 
    return JNI_VERSION_1_4;  
}  

JNIEXPORT void JNICALL jobjectProcess(JNIEnv *env, jobject instance,jobject student,jobject flag)  
{  
   /*StudentClazz=(*env)->FindClass(env,"com/example/hellojni/HelloJni$Student");*/  
   __android_log_print(ANDROID_LOG_DEBUG,"在jobjectProcess中输出","StudentClazz=%p",StudentClazz);  
   nameFieldId=(*env)->GetFieldID(env,StudentClazz,"name","Ljava/lang/String;");  
   jstring name=(jstring)((*env)->GetObjectField(env,student,nameFieldId));  
} 

下面是Activity的代码

static  
{  
    System.loadLibrary("hello-jni");  
}  
public native void jobjectProcess(Student student,Integer flag);  
public static class Student{/*省略的代码*/}  
protected void onResume()  
{  
    jobjectProcess(new Student(),new Integer(20));  
    super.onResume();  
}  

上面的C代码在JNI_OnLoad函数中对StudentClazz初始化,在jobjectProcess函数内没有再对StudentClazz赋值。此时运行程序会出错并在LogCat输出如下信息:

DEBUG/在jobjectProcess中输出(8494): StudentClazz=0x44c0a8f0
WARN/dalvikvm(8286): JNI WARNING: 0x44c0a8f0 is not a valid JNI reference
WARN/dalvikvm(8286): in Lcom/example/hellojni/HelloJni;.jobjectProcess (Lcom/example/hellojni/HelloJni$Student;Ljava/lang/Integer;)V (GetFieldID)

提示StudentClazz所指向的地址(0x44c0a8f0)不是合法的JNI引用。如果把jobjectProcess函数的第一行注释解除掉,再次给StudentClazz赋值程序便正常执行。

其实不管在哪个native函数内得到的StudentClazz值都是相同的,但每次native调用还是必须执行一次FindClass重新给StudentClazz赋值。

native的char*和JAVA的String相互转换

首先确保C/C++源文件的字符编码是UTF-8与JAVA的class文件字符编码保持一致

JNI提供了jstring来引用JAVA的String类型变量,如果native函数需要返回 String或者接受String类型参数就必须使用到jstring

而C/C++用char引用字符串起始地址,当native函数接到jstring后要转换为char所指向的字符串才能处理

referrence

http://blog.csdn.net/freechao/article/details/7692239

Android JNI机制

Dalvik虚拟机的启动过程分析

原文地址: http://www.codefrom.com/c/85

你可能感兴趣的:(Android.NDK入门)