[ReactNative翻译]React Native JSI: 第二部分 - 将Native模块转换为JSI模块

409 阅读10分钟

本文由 简悦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之间进行双向通信。

  1. 在iOS上,这个过程非常简单,因为C++代码可以直接和Objective C代码一起运行。
  2. 在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;
}

让我们讨论一下上述代码的实际作用。

  1. 在文件的顶部,我们定义了三个全局变量。
    1. java_vm 一个我们的Java Runtime的全局引用,我们的android应用正在其中运行。我们需要这个来访问JNI环境
    2. java_class Java侧的类方法(用 "静态"声明的本地方法)。jclass是对_当前类的引用。
      1. java_object Java侧的实例方法(声明了不含"静态"的本地方法)。jobject是对_当前实例的引用。
  2. 在我们的nativeInstall方法中,我们从JNI环境中获取当前的Java VM,并将其引用保存在java_vm中。我们还将 "SimpleJsiModule "类的当前实例存储在GlobalRef中。
  3. 最后,我们有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函数。

  1. GetJniEnv();给我们提供当前与Java VM相连的JNI环境。
  2. GetObjectClass(java_object);检索包括我们需要调用的所有方法的Java类。
  3. GetMethodID(java_class, "getModel", "()Ljava/lang/String;");获取我们想调用的Java函数的方法ID。在我们的例子中,它是getModel。当你在intellisense的第二个参数中选择正确的方法时,这个第三个参数会在Android Studio中自动生成。
  4. CallObjectMethod调用我们上面得到的ID的方法。
  5. GetStringUTFChars 该方法返回一个jobject,我们需要从中获取UTF8字符。
  6. 最后我们将我们的值返回给Javascript。

App.js

现在我们在Javascript中要做的就是调用global.getDeviceName,我们就可以得到我们的值。

console.log(global.getDeviceName);

我在安卓模拟器上运行这个程序,在0.03ms内得到了Google skd_gphone_x86

但是我在一开始添加的另外两个方法(setItemgetItem)呢?让我们也为它们添加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个参数,分别是keyvalue

  1. paramsCount被设置为2
  2. GetMethodID(java_class, "setItem","(Ljava/lang/String;Ljava/lang/String;)V");获取Java中setItem函数的方法ID。
  3. string2jstring 一个辅助函数,将std::string转换为java的String
  4. jvalue params[2];初始化一个有两个参数的jvalue,因为我们的setItem函数有两个参数。
  5. CallVoidMethodA 因为我们的方法不返回值,并且有参数。
  6. 最后我们返回一个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并返回一个字符串值。

  1. paramsCount被设置为1
  2. GetMethodID(java_class, "getItem","(Ljava/lang/String;)Ljava/lang/String;");获取Java中getItem函数的方法ID。
  3. string2jstring 一个辅助函数,将std::string转换为java的String
  4. jvalue params[1];由于我们的setItem函数有一个参数,所以初始化一个`jvalue',有一个参数。
  5. CallObjectMethodA因为我们的方法返回一个字符串值并且有参数。
  6. 最后我们将String值返回给Javascript。

就这样了。至少在Android部分是这样。现在让我们看看如何在iOS上做同样的事情。

iOS系统

让我们在XCode中打开example/ios/example.xcworkspace。导航到Pods > Development Pods > react-native-simple-jsi -> ios并打开SimpleJsi.mm

与Android类似,我们将创建三个函数,getModelsetItemgetItem

- (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中的函数。

  1. 这个函数的参数数是0。
  2. [simpleJsi]我们将simpleJsi实例传递给我们的lambda函数,因为在没有指定capture-default的lambda中,变量不能被隐式捕获。
  3. convertNSStringToJSIString来自YeetJSIUtils,帮助我们将NSString转换为JSIString
  4. 最后我们要返回我们的设备模型名称。

现在我们也来添加其余两个函数setItemgetItem

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来转换JSIStringNSString。其余的与我们的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应用中进行加密和存储。你可以从这里获得这些应用程序。


www.deepl.com 翻译