JNI层的反射有哪些坑?

374 阅读5分钟

有的时候,有些业务逻辑我们不希望别人能看懂,而是尽可能让别人看不懂。对于一个职场老司机来说,能写只有自己能看懂的代码对于维护自己在公司的核心开发者地位,是有一定作用的。在当今互联网裁员如家常便饭的年代,我想,很多人都有这么想过。我今天讲的内容,不但是使你的代码变得让你的同事看不懂,而是没法看。那么来了,只要你们公司没有代码评审,那我会让你卡别人脖子到你自己都讨厌你自己。做人怎么可以这么狠?公司把所有资源都给你好吗?

NDK的so文件有哪几种引入方式?

没错,有2种。

android {
    externalNativeBuild {
        cmake {
            path = file("src/main/cpp/CMakeLists.txt")
            version = "3.22.1"
        }
    }
}

以上为第一种,这种是要将源代码写到项目中,将源代码一起参与编译构建的。

android {
    sourceSets {
        getByName("main") {
            jniLibs.srcDir("src/main/jniLibs")
        }
    }
}

以上为第二种,这种只需要将编译好的so库文件放到jniLibs目录下,即可。这种方式,是你成为公司核心开发者的第一步,大量核心的代码被你隐藏,你只要保证功能能用。甚至你只需要保证测试测到的地方的功能可用。怎么说呢?比如你在代码里面集成cURL+jsoncpp+openssl,偷偷加密请求你的服务器,弄一个开关,这样公司的项目的生死就掌握在你手里了,哈哈!你想什么时候崩溃就什么时候崩溃,甚至可以把你公司的客户引流到自己的项目。以上操作纯属玩笑,千万不要说是我教你的,不要这样操作,你会进去的!

复习JNI层的反射

Java/Kotlin代码,本身也是带有反射机制的。本篇我来讲一下JNI层的反射。以下为常用的方法:

  • env->FindClass:用于查找 Java 类。

  • env->GetMethodIDenv->GetStaticMethodID:用于获取方法的引用,分别用于实例方法和静态方法。

  • env->GetFieldIDenv->GetStaticFieldID:用于获取属性的引用,分别用于实例属性和静态属性。

  • env->CallVoidMethodenv->CallStaticVoidMethod:分别用于调用非静态方法和静态方法。

  • env->SetObjectFieldenv->SetStaticObjectField:分别用于修改非静态属性和静态属性,其它数据类型类似。

  • env->NewStringUTF:创建 Java 字符串对象,即jstring,这样才能返回出去。

  • env->NewObject:调用构造函数创建 Java 对象。

  • env->DeleteLocalRef:删除局部引用,防止内存泄漏。

  • env->GetStringUTFChars:将 Java 字符串转换为 C++ 字符串。

  • env->ReleaseStringUTFChars:删除 Java 字符串和 C++ 字符串,释放内存。

转入正题,有哪些常见的坑?

主要的坑在于Kotlin不完全是Java,和纯Java代码的反射还是有一些差别的。

数据类型对应表

Java类型Native类型C类型有无符号,长度签名
booleanjbooleanunsigned charuint8_tZ
bytejbytesigned charint8_tB
charjcharunsigned shortuint16_tC
shortjshortsigned shortint16_tS
intjintintint32_tI
longjlonglongint64_tJ
floatjfloatfloatint32_tF
doublejdoubledoubleint64_tD
int[]jintArrayint[]/[I
double[]jdoubleArraydouble[]/[D
voidvoidvoid/V
Stringjstringchar*/Ljava/lang/String;
Objectjobject//Ljava/lang/Object;
Classjclass//Ljava/lang/Class;
Listjobject//Ljava/util/List;

方法签名的坑

基本规则

需要注意的是,如果以L加全类名表示,不要忘记把.换成/,并在结尾加;。万物皆对象,任何实例数据类型都可以用jobject接,包括jclass。布尔型Z和长整型J在这里跟其它的长的有点不一样。方法签名的格式为(参数1参数2参数n)返回值,例如参数没有就为()返回值,返回值没有就为(参数1参数2参数n)V

Kotlin中的可空方法参数

比如在data class中,有些参数是可空的类型,比如Boolean?。重点来了,如果是可空的,则用Ljava/lang/Boolean;,不能为空的,则用Z,代表的是Java的boolean类型。Kotlin中的方法,一律要把所有参数的签名都写上,包括可为空的,传参的时候传空给它就可以了。

Kotlin中的object类

我们知道object class是可以给方法加上@JvmStatic注解的,表示它翻译成Java的一个静态方法,在Java中可以直接用.调用。如果没有加,则要用.INSTANCE.调用。举个例子:

// 没有加@JvmStatic的yy方法的调用方式
jclass xxClass = env->FindClass(XX_CLASS_PATH);
if (xxClass == nullptr) {
    LOGE("Failed to find class: %s", XX_CLASS_PATH);
    return;
}
jfieldID instanceField = env->GetStaticFieldID(xxClass, "INSTANCE", "Lcom/xx/XX;");
jobject instance = env->GetStaticObjectField(xxClass, instanceField)
if (instance == nullptr) {
    LOGE("Failed to get XX.INSTANCE");
    return;
}
jmethodID yyMethod = env->GetMethodID(xxClass, "yy", "此处省略方法签名...");
env->CallVoidMethod(instance, yyMethod, 参数若干);

注意这里没有使用CallStaticVoidMethod,且用实例调用的。

Kotlin中的高阶函数

Kotlin中的高阶函数,有的人也叫lambda表达式,如()->Unit。没有参数的和带有1个参数的分别用Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;。而不是Ljava/lang/Object

写到最后

如果你不想把业务逻辑暴露出去,比如SDK的开发者,则可以使用JNI去调用Java的方法,这样只开源Java层的源码就可以了。最后重申一遍,惹出事来,不要提我的名字,好自为之。