转载

Android JNI入门教程

Android JNI入门教程

最近公司里有项目要用到jni,就研究并整理了下,现在分享给大家。

下载最新的ndk,新建环境变量NDK_ROOT,值是ndk的根目录,并把它加到PATH。这样做是因为我们要用到ndk的ndk-build.cmd,而且像eclipse要根据NDK_ROOT识别ndk。

现在新建一个最简单的安卓工程NewJNI,修改类MainActivity:

public class MainActivity extends Activity {  public native void Hello(String name);  public void Hi(HelloResponse helloResponse)  {   System.out.println("Hi " + helloResponse.name);  }  @Override  protected void onCreate(Bundle savedInstanceState) {   super.onCreate(savedInstanceState);   setContentView(R.layout.activity_main);   System.loadLibrary("NewJNI");   Hello("mynamepfd");  } } 

Java调C/C++

调用Hello方法,将会打印Hello mynamepfd,但是由于加上了native 关键字,表示这个方法是由C/C++实现的,直接运行当然会抛异常。

Hello方法的实现,在动态库NewJNI中,那么,NewJNI这个动态库,该如何生成呢?

在控制台中,cd到工程根目录/src下,

javah -d ../jni -jni com.example.newjni.MainActivity

这样就把类MainActivity在jni下要用到的头文件生成到了jni目录下。

观察头文件,可以发现,Java调C/C++,参数并不是以基本数据类型传递到C/C++中,而是以在jni.h中定义的以j开头的对象来传递的。还可以观察到,在name之前,还有JNIEnv *jenv、jobject obj这两个参数,其中obj,就是调用者的实例,相当于new出来的类Main。

jstring 比较特殊,它不能直接使用,要通过jenv提供的一组函数来操作:

jstring (*NewString)(JNIEnv*, const jchar*, jsize); jsize (*GetStringLength)(JNIEnv*, jstring); const jchar* (*GetStringChars)(JNIEnv*, jstring, jboolean*); void (*ReleaseStringChars)(JNIEnv*, jstring, const jchar*); jstring (*NewStringUTF)(JNIEnv*, const char*); jsize (*GetStringUTFLength)(JNIEnv*, jstring); const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*); void (*ReleaseStringUTFChars)(JNIEnv*, jstring, const char*);

NewString,根据传入的const char*,构造unicode编码的jstring,

NewStringUTF,根据传入的const char*,构造utf8编码的jstring,

GetStringChars,根据传入的jstring,取出utf8编码的const char*,

GetStringUTFChars,根据传入的jstring,取出unicode编码的const char*,

这两个函数,在调用后,要分别通过ReleaseStringChars、ReleaseStringUTFChars进行释放。

上面的几个函数,都定义在jni.h中,这几个都是C的接口,也就是说,在xxx.c中,要(*jenv)->xxx(jenv, ...)来调用,而在xxx.cpp中,只需要jenv->(...)就可以了。

接下来,依次创建下面的文件:

// com_example_newjni_MainActivity.cpp #include "com_example_newjni_MainActivity.h" #include "JNIStringUTFChars.h" #include "JNILog.h" static JavaVM *GpVM; static jclass GMainActivityClass; static jclass GHelloResponseClass; JNIEXPORT void JNICALL Java_com_example_newjni_MainActivity_Hello   (JNIEnv *jenv, jobject obj, jstring name) {  jenv->GetJavaVM(&GpVM);  GMainActivityClass = (jclass)jenv->NewGlobalRef(jenv->FindClass("com/example/newjni/MainActivity"));  GHelloResponseClass = (jclass)jenv->NewGlobalRef(jenv->FindClass("com/example/newjni/HelloResponse"));  JNIStringUTFChars jniStringUTFChars(jenv);  JPrintf("Hi %s", jniStringUTFChars.Get(name)); } 

由于jni里,jenv、打log的使用很频繁,我对这两者进行了封装。其他的后面解释。

// JNIStringUTFChars.h #ifndef _JNIStringUTFChars_H_ #define _JNIStringUTFChars_H_ #include <jni.h> class JNIStringUTFChars { public:  JNIStringUTFChars(JNIEnv *jenv);  ~JNIStringUTFChars();  const char *Get(jstring str);  void Release(); private:  JNIEnv *m_jenv;  jstring m_str;  const char *m_pchar; }; #endif 
// JNIStringUTFChars.cpp #include "JNIStringUTFChars.h" JNIStringUTFChars::JNIStringUTFChars(JNIEnv *jenv) {  m_jenv = jenv;  m_str = 0;  m_pchar = 0; } JNIStringUTFChars::~JNIStringUTFChars() {  Release(); } const char *JNIStringUTFChars::Get(jstring str) {  m_str = str;  m_pchar = m_jenv->GetStringUTFChars(str, 0);  return m_pchar; } void JNIStringUTFChars::Release() {  if(!m_pchar)  {   return;  }  m_jenv->ReleaseStringUTFChars(m_str, m_pchar);  m_pchar = 0;  m_str = 0; } 

有人可能要问,为什么初始化m_str,m_pchar没使用NULL?因为在只包含了jni.h的情况下,NULL是未声明的。

// JNILog.h #ifndef _JNILog_H_ #define _JNILog_H_  #include <android/log.h>  #define JPrintf(format,...) __android_log_print(ANDROID_LOG_INFO, "ligNewJNI", __FILE__" | %05d | "format"/n", __LINE__, ##__VA_ARGS__)  #endif
// Android.mk LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := NewJNI LOCAL_C_INCLUDES := /  $(LOCAL_PATH)/ LOCAL_SRC_FILES := /  com_example_newjni_MainActivity.cpp/  JNIStringUTFChars.cpp LOCAL_LDLIBS:= -llog include $(BUILD_SHARED_LIBRARY) 
// Application.mk APP_ABI := x86 APP_MODULES := NewJNI

我在这里,只生成了x86上的动态库,要想生成所有处理器上的动态库,将x86改为all;NewJNI是动态库名字,与Android.mk中LOCAL_MODULE相对应。

现在,在控制台中,cd到工程根目录,ndk-build,耐心等待生成libNewJNI.so。让工程跑在安卓模拟器上,就可以在控制台上看到Hi mynamepfd了。

因为直接使用jenv操作对象很繁琐,除了StringUTFChars,大家可以对自己要用到的jenv中的函数,都进行类似的封装。下面,将我封装的一些文件分享给大家。

// JNIStringUTF.h #ifndef _JNIStringUTF_H_ #define _JNIStringUTF_H_ #include <jni.h> #include <string> class JNIStringUTF { public:  JNIStringUTF(JNIEnv *jenv);  ~JNIStringUTF();  jstring New(const std::string &str);  jstring GetString(); private:  JNIEnv *m_jenv;  jstring m_str; }; #endif 
// JNIStringUTF.cpp #include "JNIStringUTF.h" JNIStringUTF::JNIStringUTF(JNIEnv *jenv) {  m_jenv = jenv;  m_str = 0; } JNIStringUTF::~JNIStringUTF() { } jstring JNIStringUTF::New(const std::string &str) {  m_str = m_jenv->NewStringUTF(str.c_str());  return m_str; } jstring JNIStringUTF::GetString() {  return m_str; } 
// JNIByteArray.h #ifndef _JNIByteArray_H_ #define _JNIByteArray_H_ #include <jni.h> class JNIByteArray { public:  JNIByteArray(JNIEnv *jenv);  ~JNIByteArray();  jbyteArray New(jsize length);  jbyteArray GetArray();  void SetRegion(jsize start, jsize len, const jbyte *buf); private:  JNIEnv *m_jenv;  jbyteArray m_array; }; #endif 
// JNIByteArray.cpp #include "JNIByteArray.h" JNIByteArray::JNIByteArray(JNIEnv *jenv) {  m_jenv = jenv;  m_array = 0; } JNIByteArray::~JNIByteArray() { } jbyteArray JNIByteArray::New(jsize length) {  m_array = m_jenv->NewByteArray(length);  return m_array; } jbyteArray JNIByteArray::GetArray() {  return m_array; } void JNIByteArray::SetRegion(jsize start, jsize len, const jbyte *buf) {  if(!m_array)  {   return;  }  m_jenv->SetByteArrayRegion(m_array, start, len, buf); } 
// JNIObject.h #ifndef _JNIObject_H_ #define _JNIObject_H_ #include <jni.h> #include <string> class JNIObject { public:  JNIObject(JNIEnv *jenv, jclass clazz);  ~JNIObject();  jobject New();  void From(jobject object);  jobject GetObject();  void SetIntField(const std::string &strField, int n);  int GetIntField(const std::string &strField);  // set传入gbk编码的字符串,get取得gbk编码的字符串  void SetStringField(const std::string &strField, const std::string &str);  std::string GetStringField(const std::string &strField);  void SetObjectField(const std::string &strField, const std::string &strSign, jobject object);  jobject GetObjectField(const std::string &strField, const std::string &strSign);  void CallVoidMethod(const std::string &strMethod, const std::string &strSign, ...); private:  JNIEnv *m_jenv;  jclass m_clazz;  jobject m_object; }; #endif 
// JNIObject.cpp #include "JNIObject.h" #include "JNIStringUtil.h" #include "JNILog.h" JNIObject::JNIObject(JNIEnv *jenv, jclass clazz) {  m_jenv = jenv;  m_clazz = clazz;  m_object = NULL; } JNIObject::~JNIObject() { } jobject JNIObject::New() {  jmethodID ctor = m_jenv->GetMethodID(m_clazz, "<init>", "()V");  m_object = m_jenv->NewObject(m_clazz, ctor);  return m_object; } void JNIObject::From(jobject object) {  m_object = object; } jobject JNIObject::GetObject() {  return m_object; } void JNIObject::SetIntField(const std::string &strField, int n) {  if(!m_object)  {   return;  }  jfieldID fieldID = m_jenv->GetFieldID(m_clazz, strField.c_str(), "I");  m_jenv->SetIntField(m_object, fieldID, n); } int JNIObject::GetIntField(const std::string &strField) {  if(!m_object)  {   return 0;  }  jfieldID fieldID = m_jenv->GetFieldID(m_clazz, strField.c_str(), "I");  return m_jenv->GetIntField(m_object, fieldID); } void JNIObject::SetStringField(const std::string &strField, const std::string &str) {  if(!m_object)  {   return;  }  SetObjectField(strField, "Ljava/lang/String;", JNIStringUtil::GbkToUtf8(m_jenv, str)); } std::string JNIObject::GetStringField(const std::string &strField) {  if(!m_object)  {   return "";  }  jstring str = (jstring)GetObjectField(strField, "Ljava/lang/String;");  return JNIStringUtil::Utf8ToGbk(m_jenv, str); } void JNIObject::SetObjectField(const std::string &strField, const std::string &strSign, jobject object) {  if(!m_object)  {   return;  }  jfieldID fieldID = m_jenv->GetFieldID(m_clazz, strField.c_str(), strSign.c_str());  m_jenv->SetObjectField(m_object, fieldID, object); } jobject JNIObject::GetObjectField(const std::string &strField, const std::string &strSign) {  if(!m_object)  {   return NULL;  }  jfieldID fieldID = m_jenv->GetFieldID(m_clazz, strField.c_str(), strSign.c_str());  return m_jenv->GetObjectField(m_object, fieldID); } void JNIObject::CallVoidMethod(const std::string &strMethod, const std::string &strSign, ...) {  if(!m_object)  {   return;  }  jmethodID methodID = m_jenv->GetMethodID(m_clazz, strMethod.c_str(), strSign.c_str());  va_list args;  va_start(args, strSign);  m_jenv->CallVoidMethodV(m_object, methodID, args);  va_end(args); } 

JNIObject中的一些函数用到了签名,这是为了让jni识别Java中的重载方法或重载变量。签名的产生可以参考其他文章,也可以在控制台下,cd到工程根目录/bin/classes下,

javap -s com.example.newjni.MainActivity

参数-s表示输出签名

// JNIStringUtil.h #ifndef _JNIStringUtil_H_ #define _JNIStringUtil_H_ #include <jni.h> #include <string> class JNIStringUtil { public:  // 传参数给as时使用  static std::string Utf8ToGbk(JNIEnv *jenv, jstring strUtf8);  // JNIObject使用  static jstring GbkToUtf8(JNIEnv *jenv, const std::string &strGbk); }; #endif 
// JNIStringUtil.cpp #include "JNIStringUtil.h" #include "JNIStringUTF.h" std::string JNIStringUtil::Utf8ToGbk(JNIEnv *jenv, jstring strUtf8) {  std::string strRet;  jclass StringClass = jenv->FindClass("java/lang/String");  jmethodID getBytes = jenv->GetMethodID(StringClass, "getBytes", "(Ljava/lang/String;)[B");  JNIStringUTF jniStringUTF(jenv);  jbyteArray bytes = (jbyteArray)jenv->CallObjectMethod(strUtf8, getBytes, jniStringUTF.New("gbk"));  jsize len = jenv->GetArrayLength(bytes);  jbyte *pBytes = jenv->GetByteArrayElements(bytes, JNI_FALSE);  if(len != 0)  {   char *pBuf = new char[len+1];   memcpy(pBuf, pBytes, len);   pBuf[len] = 0;   strRet = pBuf;   delete[] pBuf;   pBuf = NULL;  }  return strRet; } jstring JNIStringUtil::GbkToUtf8(JNIEnv *jenv, const std::string &strGbk) {  jclass StringClass = jenv->FindClass("java/lang/String");  jmethodID ctor = jenv->GetMethodID(StringClass, "<init>", "([BLjava/lang/String;)V");  jbyteArray bytes = jenv->NewByteArray(strGbk.length());  jenv->SetByteArrayRegion(bytes, 0, strGbk.length(), (const jbyte*)strGbk.c_str());  JNIStringUTF jniStringUTF(jenv);  return (jstring)jenv->NewObject(StringClass, ctor, bytes, jniStringUTF.New("utf-8")); } 

C/C++调Java

与Java调C/C++是传递Java对象给C/C++不同,C/C++调Java,是操作Java对象,大家可以参考类JNIObject。在单线程情况下,也就是在Java调C/C++的函数中,C/C++立即反调Java,需要的jenv是Java传入的,jclass可以通过jenv->FindClass(...)获取。在大多数情况下,也就是多线程情况下,C/C++调Java发生在其他线程中。假设Hello("mynamepfd")不是打印Hello mynamepfd,而是向服务端发送一个请求,C/C++异步收到响应,在另一个函数想反调Java,打印Hi Java。当有这样的应用场景时,要在Java调C/C++的时候保存JavaVM*,还要NewGlobalRef要用到的jclass,因为在其他线程中,即使能取得JNIEnv*,但由于上下文不同,取到的JNIEnv*也是不能通过正确的ClassLoader找到自己定义的类,但是可以找到系统类,比如java.lang.Object、java.lang.String等,使用NewGlobalRef是因为jenv->FindClass返回的是局部引用,在native结束时,会被销毁,不对其分配全局引用,在其他线程使用它将造成不可预料的问题,如崩溃。然后在回调函数中,通过GpVm附加当前线程得到JNIEnv*。为了方便,我实现了如下宏定义:

#define _JNIENV_BEGIN /  JNIEnv *jenv = NULL;/  GpVM->AttachCurrentThread(&jenv, NULL);/  JNIObject MainActivityObj(jenv, GMainActivityClass);/  MainActivityObj.New(); #define _JNIENV_END /  GpVM->DetachCurrentThread(); 

在回调函数中,要这样使用:

void fn(...) {     _JNIENV_BEGIN     {      }     _JNIENV_END }

为什么要加括号?这样做是为了形成_JNIENV_BEGIN和_JNIENV_END之间的局部作用域,让使用了jenv的对象在jenv释放前析构。假如我们使用了JNIStringUTFChars,Get了某个jstring,并且没有手动Release,如果没有括号,那么对该对象的析构将在END之后进行,此时jenv已被释放,再进行析构中的Release将造成不可预料的问题,如崩溃。

正文到此结束
Loading...