JNI 总结篇

392 阅读7分钟

在工作中或多或少使用到了 jni ,这是作为自己 工作的总结吧。

我从jni 的几个概念讲起,jvm env 反射 java异常拦截 异常捕获 数据强转 生命周期 这几个方面进行总结。

都知道 java 是运行在java 虚拟机之上的。 jvm 帮我们做了 硬件 兼容相关的事情,所以 java 可以做到跨平台。

首先祭出 官方api文档

再祭出 jni源码地址

jvm & env

jvm 进程唯一,env 线程唯一。

大家直到 JNI_OnLoad 是装载so 第一次运行的位置,就跟 application 一样,进程首次进入的位置。

每个线程都有自己的 env,但是需要自己使用 jvm 进行 绑定才可以拥有 env ,就跟 threadlooper 才可以 有 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/newfree/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 的功能 越来越丰富。