本文由 简悦SimpRead 转码,原文地址 blog.notesnook.com
React Native JSI似乎令人生畏,但迄今为止,JSI是提供原生性能的最佳方式。而t......
如果你不知道什么是JSI,或者对它感到困惑,我建议你在继续阅读本系列的第一部分。
在我的上一篇博客中,我详细解释了如何在React Native中从头开始编写JSI模块。我谈到了基础知识,然后解释了如何在C++中编写函数,然后可以在React Native中调用。
但我们都知道,React Native中的大多数原生模块都是用Java或Objective C编写的。虽然其中一些模块可以用C++重写,但这些原生模块大多使用平台特定的API和SDK,用C++编写是不可能的。
在这篇文章中,我将讨论我们如何将这些原生模块转换为React原生JSI模块。在这篇文章中,我不会触及任何的基础知识。我已经在本系列的前一部分中解释了它们。如果你不知道什么是JSI,或者对它仍然感到困惑,我建议你在继续之前阅读它。
用JSI在Javascript和Android/iOS环境之间进行通信
上图详细介绍了这种通信的工作方式。没有任何React Native桥的迹象。我们使用C++作为中间件,在平台特定代码和Javascript Runtime之间进行双向通信。
- 在iOS上,这个过程非常简单,因为C++代码可以直接和Objective C代码一起运行。
- 在Android上,这个过程有点复杂,因为Android和C++之间的通信是通过JNI(Java Native Interface)进行的。但是一旦你完成了基本的设置,剩下的就只是封装代码了。
我将重新使用react-native-simple-jsi示例库来实现Javascript Runtime和iOS及Android之间的基本通信层,详见上图。我建议你 "git clone "这个 repo,这样你就可以跟着做了。
安卓
在Android Studio中打开example/android文件夹。一旦Gradle构建完成,导航到react-native-simple-jsi -> java -> SimpleJsiModule.java。
SimpleJsiModule.java.
让我们添加一个简单的函数,检索我们的Android设备模型。
public String getModel() {
String manufacturer = Build.MANUFACTURER;
String model = Build.MODEL;
if (model.startsWith(manufacturer)) {
return model;
} else {
return manufacturer + " " + model;
}
}
让我们也添加两个函数来读写Android上的Shared Prefrences数据。
public void setItem(final String key, final String value) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this.getReactApplicationContext());
SharedPreferences.Editor editor = preferences.edit();
editor.putString(key, value);
editor.apply();
}
public String getItem(final String key) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this.getReactApplicationContext());
String value = preferences.getString(key, "");
return value;
}
cpp-adapter.cpp .
为了从C++中调用上述任何函数,我们需要访问Java环境和SimpleJsiModule类的当前实例。
一个天真的方法是重新使用我们已经有的JNIEnv*。
Java_com_reactnativesimplejsi_SimpleJsiModule_nativeInstall(JNIEnv *env, jobject thiz, jlong jsi)
然而缓存一个JNIEnv*并不是一个好主意,因为你不能在多个线程中使用同一个JNIEnv*,甚至可能无法在同一个线程中使用它进行多个本地调用(见 android-developers.blogspot.se/2011/11/jni… )。
一个更好的实现是创建一个函数,帮助我们随时随地检索JNIEnv*,我们需要它。
#include <jni.h>
#include <sys/types.h>
#include "example.h"
#include "pthread.h"
JavaVM *java_vm;
jclass java_class;
jobject java_object;
extern "C" JNIEXPORT void JNICALL
Java_com_reactnativesimplejsi_SimpleJsiModule_nativeInstall(JNIEnv *env,
jobject thiz,
jlong jsi) {
auto runtime = reinterpret_cast<facebook::jsi::Runtime *>(jsi);
if (runtime) {
example::install(*runtime);
}
env->GetJavaVM(&java_vm);
java_object = env->NewGlobalRef(thiz);
}
/**
* A simple callback function that allows us to detach current JNI Environment
* when the thread
* See https://stackoverflow.com/a/30026231 for detailed explanation
*/
void DeferThreadDetach(JNIEnv *env) {
static pthread_key_t thread_key;
// Set up a Thread Specific Data key, and a callback that
// will be executed when a thread is destroyed.
// This is only done once, across all threads, and the value
// associated with the key for any given thread will initially
// be NULL.
static auto run_once = [] {
const auto err = pthread_key_create(&thread_key, [](void *ts_env) {
if (ts_env) {
java_vm->DetachCurrentThread();
}
});
if (err) {
// Failed to create TSD key. Throw an exception if you want to.
}
return 0;
}();
// For the callback to actually be executed when a thread exits
// we need to associate a non-NULL value with the key on that thread.
// We can use the JNIEnv* as that value.
const auto ts_env = pthread_getspecific(thread_key);
if (!ts_env) {
if (pthread_setspecific(thread_key, env)) {
// Failed to set thread-specific value for key. Throw an exception if you
// want to.
}
}
}
/**
* Get a JNIEnv* valid for this thread, regardless of whether
* we're on a native thread or a Java thread.
* If the calling thread is not currently attached to the JVM
* it will be attached, and then automatically detached when the
* thread is destroyed.
*
* See https://stackoverflow.com/a/30026231 for detailed explanation
*/
JNIEnv *GetJniEnv() {
JNIEnv *env = nullptr;
// We still call GetEnv first to detect if the thread already
// is attached. This is done to avoid setting up a DetachCurrentThread
// call on a Java thread.
// g_vm is a global.
auto get_env_result = java_vm->GetEnv((void **)&env, JNI_VERSION_1_6);
if (get_env_result == JNI_EDETACHED) {
if (java_vm->AttachCurrentThread(&env, NULL) == JNI_OK) {
DeferThreadDetach(env);
} else {
// Failed to attach thread. Throw an exception if you want to.
}
} else if (get_env_result == JNI_EVERSION) {
// Unsupported JNI version. Throw an exception if you want to.
}
return env;
}
让我们讨论一下上述代码的实际作用。
- 在文件的顶部,我们定义了三个全局变量。
java_vm一个我们的Java Runtime的全局引用,我们的android应用正在其中运行。我们需要这个来访问JNI环境java_classJava侧的类方法(用 "静态"声明的本地方法)。jclass是对_当前类的引用。-
java_objectJava侧的实例方法(声明了不含"静态"的本地方法)。jobject是对_当前实例的引用。
- 在我们的
nativeInstall方法中,我们从JNI环境中获取当前的Java VM,并将其引用保存在java_vm中。我们还将 "SimpleJsiModule "类的当前实例存储在GlobalRef中。 - 最后,我们有
GetJniEnv()方法,我们将在需要时使用它来获取JNI环境。
由于example::install包括与平台无关的函数,让我们在cpp-adapter.cpp文件中创建另一个install函数,它将安装Android的特定函数。
void install(facebook::jsi::Runtime &jsiRuntime) {
auto getDeviceName = Function::createFromHostFunction(
jsiRuntime, PropNameID::forAscii(jsiRuntime, "getDeviceName"), 0,
[](Runtime &runtime, const Value &thisValue, const Value *arguments,
size_t count) -> Value {
JNIEnv *jniEnv = GetJniEnv();
java_class = jniEnv->GetObjectClass(java_object);
jmethodID getModel =
jniEnv->GetMethodID(java_class, "getModel", "()Ljava/lang/String;");
jobject result = jniEnv->CallObjectMethod(java_object, getModel);
const char *str = jniEnv->GetStringUTFChars((jstring)result, NULL);
return Value(runtime, String::createFromUtf8(runtime, str));
});
jsiRuntime.global().setProperty(jsiRuntime, "getDeviceName",
move(getDeviceName));
}
我们正在安装getDeviceName函数,该函数调用我们在开始时创建的getModel函数。
GetJniEnv();给我们提供当前与Java VM相连的JNI环境。GetObjectClass(java_object);检索包括我们需要调用的所有方法的Java类。GetMethodID(java_class, "getModel", "()Ljava/lang/String;");获取我们想调用的Java函数的方法ID。在我们的例子中,它是getModel。当你在intellisense的第二个参数中选择正确的方法时,这个第三个参数会在Android Studio中自动生成。CallObjectMethod调用我们上面得到的ID的方法。GetStringUTFChars该方法返回一个jobject,我们需要从中获取UTF8字符。- 最后我们将我们的值返回给Javascript。
App.js
现在我们在Javascript中要做的就是调用global.getDeviceName,我们就可以得到我们的值。
console.log(global.getDeviceName);
我在安卓模拟器上运行这个程序,在0.03ms内得到了Google skd_gphone_x86。
但是我在一开始添加的另外两个方法(setItem和getItem)呢?让我们也为它们添加JSI的绑定。
auto setItem = Function::createFromHostFunction(
jsiRuntime, PropNameID::forAscii(jsiRuntime, "setItem"), 2,
[](Runtime &runtime, const Value &thisValue, const Value \*arguments,
size_t count) -> Value {
string key = arguments[0].getString(runtime).utf8(runtime);
string value = arguments[1].getString(runtime).utf8(runtime);
JNIEnv *jniEnv = GetJniEnv();
java_class = jniEnv->GetObjectClass(java_object);
jmethodID set = jniEnv->GetMethodID( java_class, "setItem", "(Ljava/lang/String;Ljava/lang/String;)V");
jstring jstr1 = string2jstring(jniEnv, key);
jstring jstr2 = string2jstring(jniEnv, value);
jvalue params[2];
params[0].l = jstr1; params[1].l = jstr2;
jniEnv->CallVoidMethodA(java_object, set, params);
return Value(true);
});
jsiRuntime.global().setProperty(jsiRuntime, "setItem", move(setItem));
setItem函数有2个参数,分别是key和value。
paramsCount被设置为2GetMethodID(java_class, "setItem","(Ljava/lang/String;Ljava/lang/String;)V");获取Java中setItem函数的方法ID。string2jstring一个辅助函数,将std::string转换为java的String。jvalue params[2];初始化一个有两个参数的jvalue,因为我们的setItem函数有两个参数。CallVoidMethodA因为我们的方法不返回值,并且有参数。- 最后我们返回一个
boolean值给Javascript
auto getItem = Function::createFromHostFunction(
jsiRuntime, PropNameID::forAscii(jsiRuntime, "getItem"), 1,
[](Runtime &runtime, const Value &thisValue, const Value *arguments,
size_t count) -> Value {
string key = arguments[0].getString(runtime).utf8(runtime);
JNIEnv *jniEnv = GetJniEnv();
java_class = jniEnv->GetObjectClass(java_object); jmethodID get = jniEnv->GetMethodID( java_class, "getItem", "(Ljava/lang/String;)Ljava/lang/String;");
jstring jstr1 = string2jstring(jniEnv, key);
jvalue params[1];
params[0].l = jstr1;
jobject result = jniEnv->CallObjectMethodA(java_object, get, params);
const char *str = jniEnv->GetStringUTFChars((jstring)result, NULL);
return Value(runtime, String::createFromUtf8(runtime, str));
});
jsiRuntime.global().setProperty(jsiRuntime, "getItem", move(getItem));
getItem函数有1个参数,key并返回一个字符串值。
paramsCount被设置为1GetMethodID(java_class, "getItem","(Ljava/lang/String;)Ljava/lang/String;");获取Java中getItem函数的方法ID。string2jstring一个辅助函数,将std::string转换为java的String。jvalue params[1];由于我们的setItem函数有一个参数,所以初始化一个`jvalue',有一个参数。CallObjectMethodA因为我们的方法返回一个字符串值并且有参数。- 最后我们将
String值返回给Javascript。
就这样了。至少在Android部分是这样。现在让我们看看如何在iOS上做同样的事情。
iOS系统
让我们在XCode中打开example/ios/example.xcworkspace。导航到Pods > Development Pods > react-native-simple-jsi -> ios并打开SimpleJsi.mm。
与Android类似,我们将创建三个函数,getModel,setItem和getItem。
- (NSString *)getModel {
struct utsname systemInfo;
uname(&systemInfo);
return [NSString stringWithCString:systemInfo.machine
encoding:NSUTF8StringEncoding];
}
- (void)setItem:(NSString *)key:(NSString *)value {
NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];
[standardUserDefaults setObject:value forKey:key];
[standardUserDefaults synchronize];
}
- (NSString *)getItem:(NSString *)key {
NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];
return [standardUserDefaults stringForKey:key];
}
所有这些方法都返回NSString值,但这些值在JSI中不能直接读取。让我们使用awesome YeetJSIUtils模块将JSI值转换为NS值,将NS值转换为JSI值。
你所要做的就是把这两个文件(YeetJSIUtils.mm & YeetJSIUtils.h)添加到项目中,并在SimpleJsi.mm文件中导入它们,以调用所需的辅助函数。
最后让我们在JSI中安装主机函数,就像我们在上面的Android部分一样。
static void install(jsi::Runtime &jsiRuntime, SimpleJsi \*simpleJsi) {
auto getDeviceName = Function::createFromHostFunction(
jsiRuntime, PropNameID::forAscii(jsiRuntime, "getDeviceName"), 0,
[simpleJsi](Runtime &runtime, const Value &thisValue,
const Value \*arguments, size_t count) -> Value {
jsi::String deviceName =
convertNSStringToJSIString(runtime, [simpleJsi getModel]);
return Value(runtime, deviceName);
});
}
这里首先要注意的是,我们的install函数有两个参数。第一个是通常的JS运行时间。而第二个是我们的类SimpleJsi的当前实例。我们将需要它来访问Objective C中的函数。
- 这个函数的参数数是0。
[simpleJsi]我们将simpleJsi实例传递给我们的lambda函数,因为在没有指定capture-default的lambda中,变量不能被隐式捕获。convertNSStringToJSIString来自YeetJSIUtils,帮助我们将NSString转换为JSIString。- 最后我们要返回我们的设备模型名称。
现在我们也来添加其余两个函数setItem和getItem。
auto setItem = Function::createFromHostFunction(
jsiRuntime, PropNameID::forAscii(jsiRuntime, "setItem"), 2,
[simpleJsi](Runtime &runtime, const Value &thisValue,
const Value \*arguments, size_t count) -> Value {
NSString *key =
convertJSIStringToNSString(runtime, arguments[0].getString(runtime));
NSString *value =
convertJSIStringToNSString(runtime, arguments[1].getString(runtime));
[simpleJsi setItem:key:value];
return Value(true);
});
jsiRuntime.global().setProperty(jsiRuntime, "setItem", move(setItem));
auto getItem = Function::createFromHostFunction(
jsiRuntime, PropNameID::forAscii(jsiRuntime, "getItem"), 0,
[simpleJsi](Runtime &runtime, const Value &thisValue,
const Value \*arguments, size_t count) -> Value {
NSString *key =
convertJSIStringToNSString(runtime, arguments[0].getString(runtime));
NSString *value = [simpleJsi getItem:key];
return Value(runtime, convertNSStringToJSIString(runtime, value));
});
jsiRuntime.global().setProperty(jsiRuntime, "getItem", move(getItem));
由于这两个函数都有参数,我们使用convertJSIStringToNSString来转换JSIString为NSString。其余的与我们的getDeviceName函数类似。
还有一点需要注意的是,我们没有在Objective C类中使用[self setItem::]函数,尽管我们的安装函数是在类中。这是因为我们不能在C++中直接使用它。
现在我们运行我们的应用程序,你就可以了。一切都很好,也可以在iOS上运行。
总结
使用上面的代码,你可以把任何Native模块转换成React Native JSI模块。编写React Native JSI的模板似乎令人生畏,但JSI是迄今为止向用户提供本地性能的最佳方式。我想这些额外的工作是值得的,对吗?
最棒的部分是什么?没有不必要的基于Promise的API。没有不必要的参数转换。没有开销。所有的事情都发生在一个单层上,提供了最好的性能(和最好的互操作性)。
你可以在Github上找到这个库的全部代码和例子(github.com/ammarahm-ed… )。
我正在积极使用React Native JSI在Notesnook Android和iOS应用中进行加密和存储。你可以从这里获得这些应用程序。