弱者才会相信运气,强者只相信因果

0%

Cocos 客户端开发中的三座桥(其一) JNI

Cocos Creator ts 项目构建后在 Andorid 上的开发,我们会用到三种语言:Java、C++和 Javascript。往往一个简单的功能就需要同时用到这几种语言,例如微信的登录,需要在 Javascript 响应按钮的点击操作,在 Java 中调用 SDK 的 login 接口,最后再将 SDK 的结果返回给 Javascript 层用于进行后续逻辑。

在 Java、C++、Javascript 各自的语言上进行开发我们这里不做详解。基本上有编程经验的人都能很快上手。

一、JNI(Java native interface)

JNI 是第一座桥,用于在 Java 和 C++间进行交互。

Java jni 本意是 Java native interface(Java 本地接口),是为了方便 Java 调用 c、c++等本地代码所封装的一层接口。

Java 的跨平台特性导致其本地交互的能力不够强大,一些和操作系统相关的特性 Java 无法完成,于是 Java 提供了 JNI 专门用于和本地代码交互,这样就增强了 Java 语言的本地交互能力。

通过 Java jni,用户可以调用用 c、c++所编写的 native code。Native code 也可以调用 Java code。

一个普通的 Android 程序是这样的架构

通过 JNI 在 Native(C++)和 Java 间进行交互,Android App 使用 Java 进行开发。

1、Java 调用 C++

当 Java 要使用 C++的方法时,方法的实现是在 C++中编写的,首先需要在 Java 中使用 native 关键字对该方法进行声明。

1
2
3
4
5
6
// Baz.java
package com.foo.bar
public class Baz {
public static void native qux();
public static int native quux(int param1, boolean param2, String param3);
}

然后在 C++中仅仅需要按照规定的函数命名方式进行函数命名即可。

注意: C++编译器可能会对普通的c函数进行函数名变形,所以别忘了加上extern "C"。
1
2
3
4
5
6
7
8
9
10
11
// BazJni.cpp
#include <jni.h>
extern "C" {
JNIEXPORT void JNICALL Java_com_foo_bar_Baz_qux(JNIEnv *env, jobject thiz) {
// balabala
}
JNIEXPORT jint JNICALL Java_com_foo_bar_Baz_quux(JNIEnv *env, jobject thiz, jint param1, jboolean param2, jobject param3) {
jobject ref = env->NewLocalRef(param3);
// balabala
}
}

这样,JNI 会自动将两者关联并进行调用,无需我们做更多处理。

需要注意的是,虽然 C++和 Java 之间可以交互,对于传入 C++的 Java 对象进行简单赋值是不会在 Java 内对其产生引用效果的,所以需要显式地创建 Java 引用来防止其在 Java 内被回收掉。

1
2
jobject ref = env->NewGlobalRef(param) // 创建全局引用
jobject ref = env->NewLocalRef(param) // 创建局部引用

2、C++调用 Java

C++调用 Java 相对麻烦点,需要采用一系列 C++函数来 找到 并 执行 它。可想而知,一个简简单单的 Java 语句到了 C++层来调用,要花掉好几行。

例如在 Java 中有这么一个方法需要在 C++中被调用

1
2
3
4
5
// Baz.java
package com.foo.bar
public class Baz {
public static int qux(int param1, boolean param2, String param3);
}

C++中是这么写的

1
2
3
4
5
6
7
8
// BazJni.cpp
#include <jni.h>
// Baz.qux(123, true, "HelloWorld");
env = getEnv();
jclass classID = _getClassID("com/foo/bar/Baz");
jmethodID methodID = env->GetStaticMethodID(classID, "qux", "(IZLjava/lang/String;)I");
jobject jtext = env->NewStringUTF("HelloWorld");
int ret = env->CallStaticIntMethod(classID, methodID, 123, true, jtext);

这里 getEnv 和_getClassID 里面是如何实现我就不展开了,那又是一大段故事了。

简单的一行 Baz.qux(123, true, “HelloWorld”);竟然花了这么多行!!!而且其中还混入了奇怪的东西,”(IZLjava/lang/String;)I”又是什么鬼啊???

这又要引入新的概念了,就是数据类型签名。我们知道像 Java 这种高级语言是有 重载 功能的,即允许在同一范围中声明几个功能类似的同名函数,但是这些同名函数的形式参数必须不同。那么问题就来了,我想调用 qux 这个方法,但它如果有好几个重载,JNI 如何知道该调用哪一个呢?程序又不会意念感应,当然不知道你想使用哪一个。

1
2
3
4
int qux();
int qux(int param1);
int qux(int param1, boolean param2);
int qux(int param1, boolean param2, String param3);

数据类型签名就是解决这个问题的,它需要程序员明确地注册参数列表样式,让 JNI 知道被调用的是哪一个方法。

数据签名如下表:

本地类型 JNI 类型 Java 类型 类型签名(signature) 描述
void void V - -
bool jboolean boolean Z 无符号 8 位 -
signed char jbyte byte B
short jshort short C 无符号 16 位
int jint int I 有符号 32 位
long long jlong long J
float jfloat float F 32 位浮点
double jdouble double D 64 位浮点
[] [ 数组 ] 数组

使用方式是直接拼接成一个字符串

如上面的(int, boolean, String)就是变成了”(IZLjava/lang/String;)”,其中 String 的签名是 Ljava/lang/String;,因为它的 package 是 package java.lang。同样,返回值也是需要签名的。放在括号的后面,例如”(IZLjava/lang/String;)I”表示返回值是 int 类型。

讲了辣么多,是不是觉得 C++ 调用 Java 简直就是 地狱模式。

所幸 Cocos 的 JniHelper 类为我们提供了简单的方法,也得益于 C++的模版功能,我们有了这么一系列简单的接口。

1
2
3
4
5
6
template <typename... Ts>
void callStaticVoidMethod(const std::string& className, const std::string& methodName, Ts... xs);
bool callStaticBooleanMethod(const std::string& className, const std::string& methodName, Ts... xs);
int callStaticIntMethod(const std::string& className, const std::string& methodName, Ts... xs);
float callStaticFloatMethod(const std::string& className, const std::string& methodName, Ts... xs);
... etc.

只需要在后面直接列出参数即可,如此神奇。如下:

1
int ret = JniHelper::callStaticIntMethod("com/foo/bar/Baz", "qux", 123, true, "HelloWorld");

具体实现,可以移步”JniHelper.h”中了解。