最近公司里有项目要用到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"); } }
调用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")); }
与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将造成不可预料的问题,如崩溃。