Cocos Creator:JSB 手动绑定

Cocos Creator:JSB 手动绑定
推荐:将NSDT场景编辑器加入你的3D工具链
3D工具集:NSDT简石数字孪生

JSB 手动绑定

背景

ABCmouse项目中的整体JS/Native通信调用结构始终基于该方法。该方法使得通过反射机制直接在 JavaScript 中调用静态方法成为可能。使用该方法,我们可以执行JS代码,以便我们可以相互通信。callStaticMethod <-> evalStringcallStaticMethodJava/Objective-CevalString

新的ABCmouse应用架构:基于callStaticMethod与evalString的通信

虽然基于这种方法将接口封装在上层后更容易添加新的业务逻辑。然而,过度依赖evalString往往会带来一些陷阱。作为Android方面的一个例子。

CocosJavascriptJavaBridge.evalString("window.sample.testEval('" + param + "',JSON.stringify(" + jsonObj + "))");

对于常见的参数结构,这工作正常,但是,基于现实世界的场景,我们发现控制报价尤为重要。正如代码所示,为了确保JS代码正确执行,我们必须清楚连接字符串的使用和时间,如果我们不小心,可能会导致失败。我们从Cocos官方论坛上的许多反馈中知道,这是一个非常容易陷入麻烦的地方。另一方面,对于我们的项目来说,过度依赖带来的不确定性往往难以控制,我们不能只是尝试/catchevalString',直接基于JSB绑定进行通信。'"evalStringevalStringto solve them. Fortunately, after global business troubleshooting, the majority of the project is currently in the project, so after reviewing the official documentation, we decided to bypass

下面是下载程序访问的示例。在我们的项目中,下载器在Android和iOS端分别实现。在以前的版本中,下载程序调用和回调基于该方法。callStaticMethod <-> evalString

每个下载调用都需要像这样执行。

import {NATIVE} from 'cc/env';

if(NATVE && sys.os == sys.OS.IOS) {
    jsb.reflection.callStaticMethod('ABCFileDownloader', 'downloadFileWithUrl:cookie:savePath:', url, cookies, savePath);
} else if(NATVE && sys.os == sys.OS.ANDROID) {
    jsb.reflection.callStaticMethod("com/tencent/abcmouse/downloader/ABCFileDownloader", "downloadFileWithUrl", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", url, cookies, savePath);
}

成功或失败的下载都需要通过将如下所示的语句拼接在一起来执行 JS。

StringBuilder sb = new StringBuilder(JS_STRING_ON_DOWNLOAD_FINISH + "(");
sb.append("'" + success + "',");
sb.append("'" + url + "',");
sb.append("'" + savePath + "',");
sb.append("'" + msg + "',");
sb.append("'" +code + "')");
CocosJavascriptJavaBridge.evalString(sb.toString());

无论是调用还是回调都是繁琐且容易出错的拼接,所有的数据都要转换成字符串~~(emmmmm也不漂亮)~~,还要兼顾实现效率问题。如果只有少数业务场景在使用中还勉强可以接受,但是当业务越来越复杂和庞大的时候,如果非要写这个,又没有详细的文档来规范约束,其后期维护成本可想而知。evalString

在使用JSB转换的时候,我们只调用几行代码,不需要区分平台,不用担心上面提到的拼接隐性问题,相比逻辑就清晰多了。

jsb.fileDownloader.requestDownload(url, savePath, cookies, options, (success, url, savePath, msg, code) => {
    // do whatever you want
});

所以下一步就是以最简单的下载器绑定过程为例,我将带大家完成JSB手动绑定的一般过程。
(虽然 Cocos Creator 非常人性化,并且提供了自动绑定配置文件,但你可以用一些配置直接生成目标文件,这减少了大量的工作。但是,手工完成手工装订过程将有助于更全面地了解装订的整个过程,并有助于加深理解。(另一方面,当有特殊需求无法通过自动装订来满足时,手动装订往往更灵活)

前期工作

在开始之前,我们需要了解 ScriptEngine 抽象层、相关 API 和其他相关知识,如果您已经从 Cocos Creator 文档中了解,则可以跳过这部分,直接进入实践部分。

抽象层

JSB2.0-体系结构

在 1.7 版本中,抽象层被设计为一个与引擎没有关系的独立模块,并且 JS 引擎的管理从类中移出,保留下来希望通过将引擎的一些事件传递给包装层来充当适配器。这个抽象层为各种可选的JS执行引擎提供了包装器,如JavaScriptCore,SpiderMonkey,V8,ChakraCore等。JSB 的大部分工作实际上是为与 JS 相关的操作设置C++回调,并在回调函数中关联C++对象。它实际上包含以下两种主要类型。ScriptingCorese::ScriptEngineScriptingCore

  • 注册 JS 函数(包括全局函数、类构造函数、类析构函数、类成员函数、类静态成员函数)并绑定C++回调
  • 注册 JS 对象属性读写访问器,并绑定单独的读写C++回调

鉴于关键方法的定义因 JS 引擎而异,Cocos 团队使用来消除回调函数定义和参数类型的这种差异,您可以在本文末尾的 Cocos Creator 文档中详细阅读。
值得一提的是,ScriptEngine 层是由 Cocos 团队设计的,是一个完全不依赖 Cocos 引擎的独立模块。 我们开发者可以将抽象层的所有源代码移植到 cocos/bindings/jswrapper 下,并直接在其他项目中使用它。

SE 类型

所有类型的C++抽象层都在命名空间下,该命名空间代表 ScriptEngine。se

se::脚本引擎

它是JS引擎的管理员,负责JS引擎初始化、销毁、重启、原生模块注册、加载脚本、强制垃圾回收、JS异常清理,以及是否启用调试器。它是单个实例,可以通过 获取相应的实例。se::ScriptEngine::getInstance()

se::值

JS 变量有六种类型、、,因此使用联合来包含 、、、 和 。stringbooleannullundefined_type'。objectnumberstringbooleannullundefinedse::Valueobjectnumberstringbooleannull,. The non-valued types:,can be directly represented by the private variable

如果存储基本数据类型,例如 、、,则直接在内部存储值的副本。的存储是特殊的,因为它是通过 对 JS 对象的弱引用。se::Valuenumberstringbooleanobjectse::Object*

se::对象

继承自引用计数器管理器类,该类保存对 JS 对象的弱引用。如果我们需要在绑定回调中使用当前对象的对应,我们只需要用.其中 s 是类型 。se::RefCounterse::Objects.thisObject()se::State

se::类

创建类类型后,无需手动释放内存,它由包装层自动处理。 提供了一些用于定义类创建、静态/动态成员函数、属性读写等的 API,稍后将在本实践中介绍。完整的内容可以在 Cocos Creator 文档中找到。se::Class

se::状态

它是绑定回调中的一个环境,我们可以在其中获取当前C++指针、对象指针、参数列表、返回值引用。se::Objectse::State

宏观

如前所述,抽象层使用宏来消除 JS 引擎之间关键函数定义和参数类型的差异,以便开发人员使用一个函数定义,而不管底层引擎如何。

例如,抽象层中所有 JS to C++ 回调函数都定义为

bool foo(se::State& s)
{
    ...
    ...
}
SE_BIND_FUNC(foo) // Here is an example of the definition of a callback function

编写回调函数后,我们需要记住使用一系列宏包装回调函数。完整的宏集目前如下所示。SE_BIND_XXXSE_BIND_XXX

  • SE_BIND_PROP_GET:包装 JS 对象属性读取回调函数
  • SE_BIND_PROP_SET:包装 JS 对象属性写回调函数
  • SE_BIND_FUNC:包装可用作全局函数、类成员函数或类静态函数的 JS 函数
  • SE_DECLARE_FUNC:声明一个JS函数,通常在头文件中使用.h
  • SE_BIND_CTOR:包装 JS 构造函数
  • SE_BIND_SUB_CLS_CTOR:包装可以继承的 JS 子类的构造函数
  • SE_BIND_FINALIZE_FUNC:在 GC 回收 JS 对象回调函数后包装该函数
  • SE_DECLARE_FINALIZE_FUNC:在 GC 回收 JS 对象后声明 JS 对象的回调函数
  • _SE:包装器回调函数的名称,转义到每个JS引擎识别的回调函数的定义,注意第一个字符是下划线,类似于Windows下用来包装Unicode或多字节字符串的_T(“xxx”)

在我们的简化示例中,仅使用 。SE_DECLARE_FUNCSE_BIND_FUNC

类型转换帮助程序函数

类型转换帮助程序函数位于 cocos/bindings/manual/jsb_conversions.h 中,包含用于相互转换为C++类型的方法。主要有两个如下:se::Value

  • bool sevalue_to_native(const se::Value &from, T *to, se::Object * /*ctx*/)、从 到 C++类型se::Value
  • bool nativevalue_to_se(const T &from, se::Value& out, se::Object * /*ctx*/),从C++类型到se::Value
第三个参数可以直接传递,因为在大多数情况下,目前只有类型依赖于 .nullptrfunctionctx

实践

在开始之前,我们需要明确JSB绑定的过程,简单来说就是在C++层实现一些类库,经过一些具体的处理后,在JS端调用相应的方法的过程。由于 JS 是主要的业务语言,因此我们对本机功能(例如文件、网络和其他相关操作)的作用受到限制。

SkeletonRendererspine.SkeletonRenderernewjs_spine_SkeletonRenderer _constructor_ctor_ctor' 函数根据参数的类型和数量调用不同的 init 函数,这些 init 函数也是C++函数绑定。in the Cocos Creator documentation, for example, if you call theconstructor with theoperator in JSB, you will actually call thefunction. In this C++ function, memory is allocated for the skeleton object, it is added to the auto-recycle pool, and then the JS-levelfunction is called to complete the initialization. The

#define SE_BIND_CTOR(funcName, cls, finalizeCb) \
    void funcName##Registry(const v8::FunctionCallbackInfo<v8::Value>& _v8args) \
    { \
        v8::Isolate* _isolate = _v8args.GetIsolate(); \
        v8::HandleScope _hs(_isolate); \
        bool ret = true; \
        se::ValueArray args; \
        se::internal::jsToSeArgs(_v8args, &args); \
        se::Object* thisObject = se::Object::_createJSObject(cls, _v8args.This()); \
        thisObject->_setFinalizeCallback(_SE(finalizeCb)); \
        se::State state(thisObject, args); \
        ret = funcName(state); \
        if (!ret) { \
            SE_LOGE("[ERROR] Failed to invoke %s, location: %s:%d\n", #funcName, __FILE__, __LINE__); \
        } \
        se::Value _property; \
        bool _found = false; \
        _found = thisObject->getProperty("_ctor", &_property); \
        if (_found) _property.toObject()->call(args, thisObject); \
    }

三层的方法对应如下。

爪哇语JSB科科斯创造者
JSB.SkeletonRenderer.initWithSkeletonjs_spine_SkeletonRenderer_initWithSkeletonspine::SkeletonRenderer::initWithSkeleton
JSB.SkeletonRenderer.initWithUUIDjs_spine_SkeletonRenderer_initWithUUIDspine::SkeletonRenderer::initWithUUID

此调用过程的时间安排如下。

调用时序图(引自 Cocos Creator 文档)

该过程与上述过程类似。首先,我们需要定义接口和字段,让我们绘制最简单的下载器,它具有接口,并在 我们需要获取 , .为了便于使用,我们将它安装在对象下,因此我们可以使用以下代码简单地调用它:FileDownloaderdownload(url, path, callback)callbackcodemsgjsb

jsb.fileDownloader.download(url, path, (msg, code) => {
    // do whatever you want
});

定义接口后,我们可以开始对C++部分进行编码。首先,我们来看看 ,这是一个适用于 Android/iOS 的公共头文件。然后 Android/iOS 实现自己的特定下载实现(此处跳过),并用于存储回调对应关系。FileDownloader.hreqCtx

class FileDownloader {
    public:
        typedef std::function<void(const std::string& msg, const int code)> ResultCallback;
        static FileDownloader* getInstance();
        static void destroyInstance();
        void download(const std::string& url,
                                        const std::string& savePath,
                                        const ResultCallback& callback);
        void onDownloadResult(const std::string msg, const int code);
        ... ...
    protected:
        static FileDownloader* s_sharedFileDownloader;
        std::unordered_map<std::string, ResultCallback> reqCtx;
};

接下来,我们进入绑定的最关键部分。
由于下载器在功能上被归类为网络模块,我们可以选择在现有的 Cocos 源代码中实现我们的绑定。声明 JS 函数如下FileDownloaderjsb_cocos_network_autojsb_cocos_network_auto.h

SE_DECLARE_FUNC(js_network_FileDownloader_download); // Declare member functions, download calls
SE_DECLARE_FUNC(js_network_FileDownloader_getInstance); // Declare static functions to get a single instance

然后将两个新声明的函数注册到 JS 虚拟机。首先编写对应的两个方法实现并留空,然后在注册逻辑完成后填写空白。FileDownloaderjsb_cocos_network_auto.cpp

static bool js_network_FileDownloader_download(se::State &s) { // The method name is the same as when it was declared
    // TODO
}

SE_BIND_FUNC(js_network_FileDownloader_download); // Wrapping the method

static bool js_network_FileDownloader_getInstance(se::State& s) { // The method name is the same as when it was declared
    // TODO
}

SE_BIND_FUNC(js_network_FileDownloader_getInstance); // Wrapping the method

现在,让我们开始编写注册逻辑,并添加新的注册方法来收集 的所有注册逻辑。FileDownloader

bool js_register_network_FileDownloader(se::Object* obj) {
    auto cls = se::Class::create("FileDownloader", obj, nullptr, nullptr);
    cls->defineFunction("download", _SE(js_network_FileDownloader_download));
    cls->defineStaticFunction("getInstance", _SE(js_network_FileDownloader_getInstance));
    cls->install();
    JSBClassType::registerClass<FileDownloader>(cls);
    se::ScriptEngine::getInstance()->clearException();
    return true;
}

让我们看看在这种方法中做了哪些重要的事情。

  1. 调用该方法以创建名为 的类。注册成功后,可以通过 let xxx = new FileDownloader() ;' 在 JS 层中创建一个实例。2.se::Class::create(className, obj, parentProto, ctor)FileDownloaderlet xxx = new FileDownloader() After registration, you can create an instance in the JS layer by
  2. 调用该方法以定义成员函数并将其实现绑定到包装的 .defineFunction(name, func)downloadjs_network_FileDownloader_download
  3. 调用该方法,该方法定义一个静态成员函数并将其实现绑定到包装的 .defineStaticFunction(name, func)getInstancejs_network_FileDownloader_getInstance
  4. 调用该方法以将自身注册到 JS 虚拟机。install()
  5. 调用该方法将生成的类映射到C++级类(通过 内部实现)。JSBClassType::registerClassstd::unordered_map<std::string, se::Class*>

通过这些步骤,我们已经完成了关键的注册部分,但当然不要忘记添加对模块注册门户的调用:。js_register_network_FileDownloadernetworkjs_register_network_FileDownloader

bool register_all_cocos_network(se::Object* obj)
{
    // Get the ns
    se::Value nsVal;
    if (!obj->getProperty("jsb", &nsVal))
    {
        se::HandleObject jsobj(se::Object::createPlainObject());
        nsVal.setObject(jsobj);
        obj->setProperty("jsb", nsVal);
    }
    se::Object* ns = nsVal.toObject();

    ... ...
    // Set the Class registration generated earlier to a property of jsb so that we can pass
    // let downloader = new jsb.FileDownloader();
    // Get the instance
    js_register_network_FileDownloader(ns);
    return true;
}

完成此步骤后,我们的类已成功绑定,因此现在我们回来优化我们留空的方法。

首先是。getInstance()

static bool js_network_FileDownloader_getInstance(se::State& s)
{
    const auto& args = s.args();
    size_t argc = args.size();
    CC_UNUSED bool ok = true;
    if (argc == 0) {
        FileDownloader* result = FileDownloader::getInstance(); // C++ 单例
        ok &= nativevalue_to_se(result, s.rval(), nullptr);
        SE_PRECONDITION2(ok, false, "js_network_FileDownloader_getInstance : Error processing arguments");
        return true;
    }
    SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", (int)argc, 0);
    return false;
}

如前所述,我们可以通过 获取C++指针、对象指针、参数列表、返回值引用。逻辑排序如下。se::Objectse::State

  1. args()获取 JS 带来的所有参数(向量)。se::Value
  2. 参数的数量,因为这里不需要额外的参数,所以参数是0。getInstance()
  3. native_ptr_to_seval()用于基于 C++ 对象指针在绑定级别获取 a,并将返回值分配给 JS 级别。se::Valuerval()

至此,绑定层逻辑全部完成,我们已经可以通过 .getInstance()let downloader = jsb.FileDownloader.getInstance()

接下来是 .download()

static bool js_network_FileDownloader_download(se::State &s) {
    FileDownloader *cobj = (FileDownloader *) s.nativeThisObject();
    SE_PRECONDITION2(cobj, false,
                     "js_network_FileDownloader_download : Invalid Native Object");
    const auto &args = s.args();
    size_t argc = args.size();
    CC_UNUSED bool ok = true;
    if (argc == 3) {
        std::string url;
        std::string path;
        ok &= sevalue_to_native(args[0], &url, nullptr); // Converted to ::string url
        ok &= sevalue_to_native(args[1], &path, nullptr); // Converted to ::string path
        std::function<void(const std::string& msg,
                           const int code)> callback;
        do {
            if (args[2].isObject() && args[2].toObject()->isFunction())
            {
                se::Value jsThis(s.thisObject());
                // Get JS callbacks
                se::Value jsFunc(args[2]);
                // If the target class is a singleton, it cannot be associated with se::Object::attachObject
                // You must use se::Object::root and do not care about unroot. The unroot operation will trigger the destruct of jsFunc with the destruction of the lambda, and the unroot operation will be performed in the destructor of se::Object.
                // If s.thisObject->attachObject(jsFunc.toObject); is used, the corresponding func and target will never be freed and a memory leak will occur.
                jsFunc.toObject()->root(); 
                auto lambda = [=](const std::string& msg,
                                  const int code) -> void {
                    se::ScriptEngine::getInstance()->clearException();
                    se::AutoHandleScope hs;
                    CC_UNUSED bool ok = true;
                    se::ValueArray args;
                    args.resize(2);
                    ok &= nativevalue_to_se(msg, args[0], nullptr);
                    ok &= nativevalue_to_se(code, args[1], nullptr);
                    se::Value rval;
                    se::Object* thisObj = jsThis.isObject() ? jsThis.toObject() : nullptr;
                    se::Object* funcObj = jsFunc.toObject();
                    // Execute JS method callbacks
                    bool succeed = funcObj->call(args, thisObj, &rval);
                    if (!succeed) {
                        se::ScriptEngine::getInstance()->clearException();
                    }
                };
                callback = lambda;
            }
            else
            {
                callback = nullptr;
            }
        } while(false);
        SE_PRECONDITION2(ok, false, "js_network_FileDownloader_download : Error processing arguments");
        cobj->download(url, path, callback);
        return true;
    }
    SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", (int) argc, 3);
    return false;
}
  1. 通过方法C++转换后获取 url、路径参数和原始 jsFunc。seval_to_std_string
  2. 手动构造回调函数,将消息和代码转换为 .3.se::Value
  3. 通过 执行 JS 方法进行回调。funcObj->call

最后,考虑到内存释放的风险,我们还需要在方法中做相关的回收。close()Application.cpp

network::FileDownloader::destroyInstance();

================================================

以上是整个绑定过程,分别编译到 Android/iOS 环境后,我们将能够通过 进行下载调用。
(PS:使用前一定要记得进行宏观判断,因为非JSB环境不能使用)jsb.fileDownloader.download()NATIVE

import {NATIVE} from 'cc/env';
...
if(NATIVE) {
 // JSB Related Logic
}

总结

现在我们来总结一下手动绑定转换的详细过程。一般来说,常用JSB的转换过程大致如下。

  • 确定方法接口和 JS/本机公共字段
  • 声明头文件并分别实现 Android JNI 和 OC 特定的业务代码
  • 编写抽象层代码以在JS虚拟机中注册必要的类和相应的方法
  • 在 JS 中的指定对象(如命名空间)中挂载绑定类

3D建模学习工作室 翻译整理,转载请注明出处

上一篇:Cocos Creator:简化使用 JavaScript 调用 Java 方法 (mvrlink.com)

下一篇:Cocos Creator:使用 JSB 自动绑定 (mvrlink.com)

NSDT场景编辑器 | NSDT 数字孪生 | GLTF在线编辑器 | 3D模型在线转换 | UnrealSynth虚幻合成数据生成器 | 3D模型自动纹理化工具
2023 power by nsdt©鄂ICP备2023000829号