React Native TurboModules 实战踩坑记 高性能原生模块集成方案

码农婉琳 移动 阅读 1,661
赞 33 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

搞TurboModules这玩意儿也有段时间了,说实话刚开始真是一脸懵逼。React Native那套架构看起来挺复杂,但其实摸清楚套路后也就那么回事。我一般把原生模块分几个层级:头文件定义、实现类、注册绑定,然后JS端调用。

React Native TurboModules 实战踩坑记 高性能原生模块集成方案

先说说最基础的头文件声明吧,这个千万别偷懒。我是这么写的:

// MyTurboModule.h
#pragma once

#include <ReactCommon/TurboModule.h>
#include <jsi/jsi.h>

namespace facebook {
namespace react {

class JSI_EXPORT MyTurboModule : public TurboModule {
public:
  MyTurboModule(const CallInvokerHolder::Shared& jsCallInvoker);
  
  jsi::Value multiply(jsi::Runtime& runtime, jsi::Value value, jsi::Value multiplier) override;
  jsi::Value asyncOperation(jsi::Runtime& runtime, jsi::Value options) override;
};

} // namespace react
} // namespace facebook

这种写法的好处是接口定义清晰,继承关系一目了然。记得那个JSI_EXPORT宏不能少,不然在某些平台上会有链接问题。

实现类里面我比较注重参数验证和异常处理:

// MyTurboModule.cpp
#include "MyTurboModule.h"
#include <stdexcept>

namespace facebook {
namespace react {

MyTurboModule::MyTurboModule(const CallInvokerHolder::Shared& jsCallInvoker)
    : TurboModule("MyTurboModule", jsCallInvoker) {
  methodMap_["multiply"] = MethodMetadata{
      2,
      [this](jsi::Runtime& rt, const jsi::Value* args, size_t count) -> jsi::Value {
        if (count != 2 || !args[0].isNumber() || !args[1].isNumber()) {
          throw std::runtime_error("Invalid arguments for multiply");
        }
        double result = args[0].asNumber() * args[1].asNumber();
        return jsi::Value(result);
      }
  };
}

jsi::Value MyTurboModule::multiply(jsi::Runtime& runtime, jsi::Value value, jsi::Value multiplier) {
  // 实现具体逻辑
  double val1 = value.asNumber();
  double val2 = multiplier.asNumber();
  return jsi::Value(val1 * val2);
}

jsi::Value MyTurboModule::asyncOperation(jsi::Runtime& runtime, jsi::Value options) {
  // 异步操作实现
  auto promise = createPromiseAsJSIValue(
    runtime,
    [options](jsi::Runtime& rt, std::shared_ptr<Promise> promise) {
      // 执行异步任务
      try {
        // 模拟异步处理
        promise->resolve("success");
      } catch (const std::exception& e) {
        promise->reject(e.what());
      }
    });
  return promise;
}

} // namespace react
} // namespace facebook

这里的关键是methodMap_的构建,函数签名必须匹配TurboModule的要求。参数验证这块我踩过不少坑,特别是类型检查,JS端传过来的数据类型可能跟预期不一致,不做校验很容易崩溃。

这几种错误写法,别再踩坑了

之前见过很多新手的写法,真的是让人头疼。最常见的一种就是完全不管内存管理,直接裸指针乱飞:

// 错误写法!千万别学
jsi::Value badFunction(jsi::Runtime& runtime, jsi::Value input) {
  char* buffer = new char[1024]; // 没有对应的delete
  // ... 一堆操作
  return jsi::Value(runtime, jsi::String::createFromUtf8(runtime, buffer));
}

这种写法会导致严重的内存泄漏,而且在高频调用的场景下很快就会OOM。还有那种忘记检查null值的:

// 也是错误写法
jsi::Value getValue(jsi::Runtime& runtime, jsi::Value obj) {
  // 没有检查obj是否为undefined或null
  auto prop = obj.getObject(runtime).getProperty(runtime, "value");
  return prop; // 如果obj为空对象,这里会崩溃
}

还有一个特别容易出问题的地方是异步回调处理不当:

// 危险的异步写法
void dangerousAsync(jsi::Runtime& runtime, jsi::Value callback) {
  std::thread([callback, &runtime]() {
    // 在子线程中直接使用callback,这是错的!
    callback.call(runtime, "result"); // runtime在子线程中不可用
  });
}

JSI的runtime不是线程安全的,必须通过callinvoker来调度回到JS线程执行回调。这些错误我在项目初期都遇到过,调试起来特别痛苦。

实际项目中的坑

真正集成到项目里才发现,光是理论知识完全不够用。最头疼的是平台差异处理,iOS和Android的ABI不一样,类型大小也可能不同。我在做加密模块的时候就遇到过这个问题:

// 原本以为这样就能跨平台,结果发现size_t在不同平台长度不一样
jsi::Value hashData(jsi::Runtime& runtime, jsi::Value data) {
  std::string inputData = data.getString(runtime).utf8(runtime);
  size_t hashValue = calculateHash(inputData); // size_t在32位设备上只有4字节
  // 返回给JS时可能丢失精度
  return jsi::Value(static_cast<double>(hashValue));
}

后来改成返回字符串形式的哈希值才解决精度丢失问题。还有个问题是热重载兼容性,TurboModule在开发模式下热重载经常出问题,主要是native代码重新编译后JS端的引用还在用旧的。

性能优化方面也需要注意,频繁的小对象创建会严重影响性能:

// 性能很差的写法
for (int i = 0; i < 1000; ++i) {
  jsi::Object obj(runtime);
  obj.setProperty(runtime, "index", jsi::Value(i));
  // 每次循环都创建新对象
}

// 更好的做法是批量处理
jsi::Array array(runtime, 1000);
for (int i = 0; i < 1000; ++i) {
  jsi::Object obj(runtime);
  obj.setProperty(runtime, "index", jsi::Value(i));
  array.setValueAtIndex(runtime, i, std::move(obj));
}

另外还要特别注意错误传播,JS端的try-catch可能捕获不到native层抛出的异常,需要统一的错误处理机制。我在网络请求模块里专门加了一个错误包装函数:

jsi::Value wrapNetworkResult(jsi::Runtime& runtime, const NetworkResult& result) {
  jsi::Object obj(runtime);
  if (result.success) {
    obj.setProperty(runtime, "status", jsi::Value("success"));
    obj.setProperty(runtime, "data", jsi::Value(result.data));
  } else {
    obj.setProperty(runtime, "status", jsi::Value("error"));
    obj.setProperty(runtime, "message", jsi::Value(result.errorMsg));
    obj.setProperty(runtime, "code", jsi::Value((double)result.errorCode));
  }
  return obj;
}

这样JS端就能统一处理各种网络状态,不用关心底层的具体异常情况。

调试也是一个大坑,Xcode和Android Studio的调试器对JSI对象支持都不太好,我一般在关键位置打日志,然后通过网络发送到本地服务器查看(比如发到 https://jztheme.com/debug 接收日志)。

以上是我踩坑后的总结,希望对你有帮助

TurboModules确实比老的Native Modules快不少,但复杂度也高了。实际项目中还是得根据具体需求权衡,不是所有模块都需要用TurboModules重写。这套东西最大的优势是性能,劣势是开发和维护成本高。我一般是那些对性能要求特别高的模块才考虑用这个,比如音视频处理、大量数据计算、实时通信等场景。

以上是我个人对TurboModules的完整讲解,有更优的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论