在工作中或多或少使用到了 jni ,这是作为自己 工作的总结吧。
我从jni 的几个概念讲起,jvm
env
反射
java异常拦截
异常捕获
数据强转
生命周期
这几个方面进行总结。
都知道 java 是运行在java 虚拟机之上的。 jvm 帮我们做了 硬件 兼容相关的事情,所以 java 可以做到跨平台。
首先祭出 官方api文档
再祭出 jni源码地址
jvm & env
jvm 进程唯一,env 线程唯一。
大家直到 JNI_OnLoad
是装载so 第一次运行的位置,就跟 application
一样,进程首次进入的位置。
每个线程都有自己的 env
,但是需要自己使用 jvm
进行 绑定才可以拥有 env
,就跟 thread
要 looper
才可以 有 handler
的功能。
如何获取 全局唯一的jvm呢?
/*
* System.loadLibrary("lib")时调用
* 如果成功返回JNI版本, 失败返回-1
*/
JNIEXPORT jint JNICALL JNI_OnLoad(
JavaVM* vm,
void* reserved)
{
// 这里将vm 进行保存即可。
setJvm(vm);
return JNI_VERSION_1_4;
}
为啥要获取env呢?
在jni的开发中,env
相当于android 的 context
上下文环境。
需要获取上下文环境才可以做其他的事情。申请,释放,等。
如何在不同的线程中获取 env呢?
static JavaVM *s_jvm;
void setJvm(JavaVM *vm)
{
__android_log_print(ANDROID_LOG_DEBUG, "telenewbie", "message:[%s:%p]you set jvm ", __FUNCTION__, vm);
s_jvm = vm;
}
thread_local JNIEnv *tl_env = nullptr;
static void createCurThreadEvn()
{
if (tl_env == nullptr)
{
if (s_jvm == nullptr)
{
__android_log_print(ANDROID_LOG_ERROR, "telenewbie", "message:[%s:%d]you must invoke onload first", __FUNCTION__, __LINE__);
}
// 需要绑定
s_jvm->AttachCurrentThread(&tl_env, nullptr);
}
}
切记:如果切换了 线程 ,必须要 通过 jvm
重新获取 env
才可。
生命周期
在java 里面 我们很少去关注 对象的消亡,因为有 GC 的存在。 GC 每个一定的时间 就去 扫描 root 节点访问不到的节点并清理,当然这不是一连串 持续执行的动作,因为不能耗时太久,造成卡顿感,所以拆成了好几步。
在jni 里面,其实就是 c/c++
类似,秉承一个原则 谁申请谁释放
。
作用域分为 ,局部,全局,弱引用
jobject (*NewGlobalRef)(JNIEnv*, jobject);
void (*DeleteGlobalRef)(JNIEnv*, jobject);
void (*DeleteLocalRef)(JNIEnv*, jobject);
// 以下很少用,或不用,我没用过。
jweak (*NewWeakGlobalRef)(JNIEnv*, jobject);
void (*DeleteWeakGlobalRef)(JNIEnv*, jweak);
一句话总结: 基本上所有用 NewXXX
进行赋值的变量 都需要 DeleteXXX
char name [50] ={0};
jstring className = env->NewStringUTF(name);
const char* s_dev_name = env->GetStringUTFChars(className, 0);
env->ReleaseStringUTFChars(className, s_dev_name);//释放资源
env->DeleteLocalRef(className);//释放局部变量
std::string strData;
jbyteArray data = env->NewByteArray(strData.size());
jbyte* pData = env->GetByteArrayElements(data, NULL);
env->ReleaseByteArrayElements(data, pData, 0);
env->DeleteLocalRef(data);
static jclass s_clsCommJNI = NULL;
s_clsCommJNI = reinterpret_cast< jclass > (env->NewGlobalRef(cls));
env->DeleteGlobalRef(s_clsCommJNI);
以上 其实和 c/c++
差不多,都是 malloc/new
和 free/delete
都要成对调用 。只是在这里面你也许 会疑惑 GetXXX
这种竟然也要 ReleaseXXX
。
static const char* GetStringUTFChars(JNIEnv* env, jstring jstr, jboolean* isCopy) {
ScopedJniThreadState ts(env);
if (jstr == NULL) {
/* this shouldn't happen; throw NPE? */
return NULL;
}
if (isCopy != NULL) {
*isCopy = JNI_TRUE;
}
StringObject* strObj = (StringObject*) dvmDecodeIndirectRef(ts.self(), jstr);
char* newStr = dvmCreateCstrFromString(strObj);// 其实是调用了 malloc
if (newStr == NULL) {
/* assume memory failure */
dvmThrowOutOfMemoryError("native heap string alloc failed");
}
return newStr;
}
/*
* Release a string created by GetStringUTFChars().
*/
static void ReleaseStringUTFChars(JNIEnv* env, jstring jstr, const char* utf) {
ScopedJniThreadState ts(env);
free((char*) utf);
}
/*
* Create a new C string from a java/lang/String object.
*
* Returns NULL if the object is NULL.
*/
char* dvmCreateCstrFromString(const StringObject* jstr)
{
assert(gDvm.classJavaLangString != NULL);
if (jstr == NULL) {
return NULL;
}
int len = dvmGetFieldInt(jstr, STRING_FIELDOFF_COUNT);
int offset = dvmGetFieldInt(jstr, STRING_FIELDOFF_OFFSET);
ArrayObject* chars =
(ArrayObject*) dvmGetFieldObject(jstr, STRING_FIELDOFF_VALUE);
const u2* data = (const u2*)(void*)chars->contents + offset;
assert(offset + len <= (int) chars->length);
int byteLen = utf16_utf8ByteLen(data, len);
char* newStr = (char*) malloc(byteLen+1); // 在这里
if (newStr == NULL) {
return NULL;
}
convertUtf16ToUtf8(newStr, data, len);
return newStr;
}
从上述的源码不难看出,其实 GetXXX
其实内部 采用了 malloc
进行 申请内存,所以 需要 用 free
进行释放。 再平时开发过程中,如果自己内部 申请内存,需要对方进行释放,则需要相应的再提供一个 释放的接口。如下:
char * GetXX(){
char* newStr = malloc(4);
return newStr;
}
void ReleaseXX(char* str){
free(str);
}
也许上述的例子有点简单,但是已经可以清晰讲明白了。
反射
再平时开发过程中,难免涉及 反射调用java api 的情形。
#define GET_METHOD(methodVar, env, clazz, method, methodSig) \
do{ \
methodVar = env->GetStaticMethodID(clazz, method,methodSig); \
if (methodVar == nullptr) { \
__android_log_print(ANDROID_LOG_ERROR,"telenewbie", "[%d]can't find method %s",__LINE__,method); \
break; \
} \
}while(0)
#define GET_CLASS(classPtr,env,className) \
do{ \
jclass tmp = env->FindClass(className);\
if (tmp == nullptr) {\
__android_log_print(ANDROID_LOG_ERROR, "telenewbie", "can't find class %s",\
c_class_TsrVadDetector);\
}\
classPtr = (jclass)(env->NewGlobalRef(tmp));\
env->DeleteLocalRef(tmp); \
}while(0)
GET_CLASS(m_clazz_TsrVacDetector, env, c_class_TsrVadDetector);
GET_METHOD(m_method_setReqParam, env, m_clazz_TsrVacDetector, c_method_setReqParam,
c_method_setReqParamSig);
前方高能:这里有两个坑,
如果没有改方法,怎么办?
先看下java 代码
public static void main(String[] args) {
try {
Class a = Class.forName("a");
a.getMethod("a");
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
可以看到 如果是Java ,会强制抛出异常。然后 jni 不会强制抛出异常。但是 运行时如果找不到这个类,或方法,不会直接crash 哦。会等你退出 栈的时候再抛异常,是不是很神奇,呵呵了。
下一节就讲如何捕获这种异常。
明明定义了类,却找不到
就如 java的双亲委派机制一样。
可以认为是 切换了线程之后,classloader
也跟着改变了 ,所以你再切换线程之后,再去调用 env->FindClass
你会发现怎么找都找不到的。
static jclass FindClass(JNIEnv* env, const char* name) {
ScopedJniThreadState ts(env);
const Method* thisMethod = dvmGetCurrentJNIMethod();
assert(thisMethod != NULL);
Object* loader;
Object* trackedLoader = NULL;
if (ts.self()->classLoaderOverride != NULL) {
/* hack for JNI_OnLoad */
assert(strcmp(thisMethod->name, "nativeLoad") == 0);
loader = ts.self()->classLoaderOverride;
} else if (thisMethod == gDvm.methDalvikSystemNativeStart_main ||
thisMethod == gDvm.methDalvikSystemNativeStart_run) {
/* start point of invocation interface */
if (!gDvm.initializing) {
loader = trackedLoader = dvmGetSystemClassLoader();
} else {
loader = NULL;
}
} else {
loader = thisMethod->clazz->classLoader;
}
char* descriptor = dvmNameToDescriptor(name);
if (descriptor == NULL) {
return NULL;
}
ClassObject* clazz = dvmFindClassNoInit(descriptor, loader);
free(descriptor);
jclass jclazz = (jclass) addLocalReference(ts.self(), (Object*) clazz);
dvmReleaseTrackedAlloc(trackedLoader, ts.self());
return jclazz;
}
说白了就是 classloader 变更了。
解决办法就是再 加载jvm 的时候 主动将 class 先装载好。然后给其他 线程的env进行调用.
异常捕获
如上节讲到的,我们要捕获 类似java层的异常。
jclass classFromName(JNIEnv* env, const char* name) {
jclass clsClass = env->FindClass("java/lang/Class");
jmethodID midForName = env->GetStaticMethodID(clsClass, "forName",
"(Ljava/lang/String;)Ljava/lang/Class;");
jstring className = env->NewStringUTF(name);
jobject jobj = env->CallStaticObjectMethod(clsClass, midForName, className);
jthrowable exception = env->ExceptionOccurred(); // 这里获取异常
if (exception != NULL) {
env->ExceptionClear(); // 这里清掉异常,否则会引起 crash 的
env->DeleteLocalRef(exception); // 注意 生命周期
if (jobj != NULL) {
env->DeleteLocalRef(jobj);
jobj = NULL;
}
}
env->DeleteLocalRef(className);
return (jclass) jobj;
}
其他类似的java 问题 都可以通过上述的方式进行 清理。
接下来是纯 c/c++
的知识了。怎么捕获 c/c++
的异常呢?通过信号量
class _CrashMonitor
{
public:
_CrashMonitor()
{
// 文件开关
static bool b = (access("/sdcard/newbie/catchsig_enable", 0) == 0);
if (!b)
{
return;
}
// Try to catch crashes...
struct sigaction handler;
memset(&handler, 0, sizeof(struct sigaction));
handler.sa_sigaction = my_android_sigaction;
handler.sa_flags = SA_RESETHAND;
#define CATCHSIG(X) sigaction(X, &handler, &old_sa[X])
CATCHSIG(SIGILL);
CATCHSIG(SIGABRT);
CATCHSIG(SIGBUS);
CATCHSIG(SIGFPE);
CATCHSIG(SIGSEGV);
CATCHSIG(SIGSTKFLT);
CATCHSIG(SIGPIPE);
}
} s_CrashMonitor;
在这个cpp被装载的时候就会自动初始化完毕。
处理的方式类似这样
#include "traceblock.h"
//static struct sigaction old_sa[SIGRTMAX];
static std::vector<struct sigaction> old_sa(SIGRTMAX);
static void my_android_sigaction(
int signal,
siginfo_t *info,
void *reserved)
{
// 获取时间戳
// 获取堆栈
// 保存文件
// 消息转发 ,否则消息会被你吃了的。
old_sa[signal].sa_handler(signal);
}
数据转换
使用 jni 最重要的就是 和 c/c++
原生对象的转换
Jstring string 互转
std::string t_jstring2string(JNIEnv *env, jstring jStr) {
if (!jStr)
return "";
const char *buf = env->GetStringUTFChars(jStr, nullptr);
std::string ret = std::string(buf, static_cast<unsigned int>(env->GetStringUTFLength(jStr)));
env->ReleaseStringUTFChars(jStr, buf);
return ret;
}
jstring string2jstring(std::string data){
return env->NewStringUTF(data.c_str());
}
jbytearray String 互转
//转换成字符串
#define STRING_TO_JNI_BYTE_ARRAY(_env,_str,_jb) \
do \
{ \
_jb = (_env)->NewByteArray((_str).size()); \
if (NULL == _jb) break; \
(_env)->SetByteArrayRegion(_jb, 0, (jsize)(_str).size(), (jbyte*) (_str).data()); \
}while(0)
#define JNI_BYTE_ARRAY_TO_STRING(_env, _arr, _str) \
do \
{ \
if (NULL == (_arr))break; \
jbyte* ___byte_array_data = (_env)->GetByteArrayElements((_arr), NULL); \
(_str).assign((const char*)___byte_array_data, (_env)->GetArrayLength((_arr))); \
(_env)->ReleaseByteArrayElements((_arr), ___byte_array_data, 0); \
} while(0)
这里介绍了两种类型的互转,其实 最重要的是 jbytearray 和string 的互转.
其实通过之前的文章说过, 其实再 java 和 c/c++
里面 尽量 用 byte 进行传递,否则会出现各种奇怪的问题。
说在最后
jni 为我们提供了两套API 分别对应 c 和 c++
// 对应 C
jstring (*NewStringUTF)(JNIEnv*, const char*);
// 对应C++
jstring NewStringUTF(const char* bytes)
所以再调用 c 的时候入参 相应要比 c++ 的函数多一个 env
//c
NewStringUTF(env,"");
//c++
env->NewStringUTF("");
你更喜欢哪种方式的调用呢?
建议使用c++吧,毕竟面向oop 呢。而且 c++20 的功能 越来越丰富。