Cocos Creator:使用 JSB 自动绑定

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

使用 JSB 自动绑定

Creator 提供了支持直接从 TypeScript 端调用原生端(Android/iOS/Mac)接口的方法,但经过大量实践,我们发现在大量频繁调用的情况下,尤其是 Android 端,比如调用原生端实现的接口打印日志,这个接口的性能很低。并且很容易导致一些本机崩溃,例如和其他问题。在整个 Cocos 原生代码的实现过程中,基本上所有的接口方法都是基于 JSB 方法实现的,所以本文主要讲解一下 JSB 的自动绑定逻辑,以帮助你快速实现到 JSB 的转换过程。native.reflection.callStaticMethodlocal reference table overflowcallStaticMethod

背景

对于那些使用过 Cocos Creator(或 CC 为方便起见)的人来说,提供从 TypeScript 端调用本机端的能力当然并不陌生。例如,如果我们想调用日志打印和持久接口的 Native 实现,我们可以在 JavaScript 中轻松完成,如下所示:native.reflection.callStaticMethod

import {NATIVE} from 'cc/env';

if (NATIVE && sys.os == sys.OS.IOS) {
    msg = this.buffer_string + '\n[cclog][' + clock + '][' + tag + ']' + msg;
    native.reflection.callStaticMethod("ABCLogService", "log:module:level:", msg, 'cclog', level);
    return;
} else if (NATIVE && sys.os == sys.OS.ANDROID) {
    msg = this.buffer_string + '\n[cclog][' + clock + '][' + tag + ']' + msg;
    native.reflection.callStaticMethod("com/example/test/CommonUtils", "log", "(ILjava/lang/String;Ljava/lang/String;)V", level, 'cclog', msg);
    return;
}

它使用起来非常简单,可以在一行代码中称为跨平台,稍微看一下它的实现就会发现,C++层是通过jni的方式在Android端实现的,iOS端是通过运行时的方式动态调用的。但是,为了兼顾通用性并支持所有方法,Android 端没有针对 jni 相关对象的缓存机制,在短时间内进行大量调用时,可能会导致严重的性能问题。之前我们遇到的情况比较多,就是在下载器中打印日志,一些应用场景在短时间内触发大量的下载操作,导致崩溃,甚至低端机器接口滞后无法加载。local reference table overflow

解决此问题需要对日志调用进行 JSB 修改,以及与 jni 相关的缓存机制以优化性能。jSB 绑定只是在C++层和脚本层之间转换对象,并将脚本层函数调用转发到C++层的过程。

JSB 绑定通常以两种方式完成:手动绑定和自动绑定。可以在使用 JSB 手动绑定文档中找到手动绑定方法。

  • 手动装订的优点是灵活可定制;缺点是需要自己编写所有代码,尤其是C++类型和 TypeScript 类型之间的转换,很容易导致内存泄漏和某些指针或对象无法释放。
  • 自动绑定会省去很多麻烦,一键通过脚本直接生成相关代码,后续如果有新代码或更改,只需重新执行一次脚本即可。自动绑定非常适合不需要进行强自定义然后需要快速完成 JSB 的情况。下面分步说明如何实现自动 JSB 绑定。

环境配置和自动绑定演示

环境配置

自动绑定,简单来说,就是执行一个python脚本自动生成相应的文件。首先,确保计算机具有python运行时环境。如何在Mac上安装它的示例:.cpp.h

要安装 python 3.0,请从 python 网站下载安装包:

https://www.python.org/downloads/release/python-398/

通过 pip3 安装一些 python 依赖项:

 sudo pip3 install pyyaml==5.4.1
 sudo pip3 install Cheetah3

安装 NDK,这在C++方面绝对是必不可少的。建议安装 Android NDK r21e 版本,先设置 in ,然后设置 and in ,因为这两个环境变量将直接在稍后执行的 python 文件中使用。PYTH_profile~/.bash_ profilePYTHON_ROOTNDK_ROOT~/.bash_profile

 export NDK_ROOT=/Users/kevin/android-ndk-r21e
 export PYTHON_BIN=python3

在Windows中,直接参考上面安装模块,记得最后配置环境变量。

自动绑定演示

下面演示了 cocos 引擎下的文件,即 cocos/bindings/auto 目录(如下所示):

其实,这些文件名的开头就有一些特定的规则来命名这些文件,那么这些文件是如何生成的呢?首先,打开一个终端,转到 tools/tojs 目录,然后运行 。cd./genbindings.py

运行大约一分钟左右后,将出现以下消息,表示已成功生成:

完成上述步骤后,cocos/bindings/auto下的所有文件都会自动生成,非常方便。

下面以 TypeScript 层如何通过 JSB 调用原生层日志方法打印日志为例,以及如何实现自动绑定工具,根据编写C++代码生成对应的自动绑定文件。

编写C++层实现

C++是 TypeScript 和 Native 层之间的桥梁。要实现 JSB 调用,第一步是准备头文件和C++层的实现,这里我们在目录中创建一个文件夹来存储相关文件:testcocos

ABCJSBBridge.h,声明了一个函数供 TypeScript 层调用日志记录,并且由于日志记录方法肯定会在 TypeScript 层的许多地方使用,因此此处使用单例模式,提供获取类的当前实例。abcLoggetInstance()

#pragma once

#include <string>

namespace abc
{
    class JSBBridge
    {
    public:
        void abcLog(const std::string& msg);
        /**
        * Returns a shared instance of the director.
        * @js _getInstance
        */
        static JSBBridge* getInstance();

        /** @private */
        JSBBridge();
        /** @private */
        ~JSBBridge();
        bool init();
    };
}

以下是相应的实现:ABCJSBBridge.cpp

#include <cocos/base/Log.h>
#include "ABCJSBBridge.h"

namespace abc
{
    // singleton stuff
    static JSBBridge *s_SharedJSBBridge = nullptr;

    JSBBridge::JSBBridge()
    {
        CC_LOG_ERROR("Construct JSBBridge %p", this);
        init();
    }

    JSBBridge::~JSBBridge()
    {
        CC_LOG_ERROR("Destruct JSBBridge %p", this);
        s_SharedJSBBridge = nullptr;
    }

    JSBBridge* JSBBridge::getInstance()
    {
        if (!s_SharedJSBBridge)
        {
            CC_LOG_ERROR("getInstance JSBBridge ");
            s_SharedJSBBridge = new (std::nothrow) JSBBridge();
            CCASSERT(s_SharedJSBBridge, "FATAL: Not enough memory for create JSBBridge");
        }

        return s_SharedJSBBridge;
    }

    bool JSBBridge::init(void)
    {
        CC_LOG_ERROR("init JSBBridge ");
        return true;
    }

    void JSBBridge::abcLog(const std::string& msg)
    {
        CC_LOG_ERROR("%s", msg.c_str());
    }
}

JSB 配置脚本

tools/tojs 目录中找到脚本,将其复制并重命名为 ,然后修改模块配置以仅保留cocos2dx_test模块。genbindings.pygenbindings_test.pygenbindings_test.py

下一步就是在 tools/tojs 目录下添加一个自定义配置文件,其实和 tools/tojs 下的其他文件类似,主要是让自动绑定工具知道要绑定哪些 API,以什么方式绑定。直接参考 Cocos 现有的文件来写这个,这里是内容:cocos2dx_test.ini.ini.inicocos2dx_test.ini

[cocos2dx_test]
# the prefix to be added to the generated functions. You might or might not use this in your own
# templates
prefix = cocos2dx_test

# create a target namespace (in javascript, this would create some code like the equiv. to `ns = ns || {}`)
# all classes will be embedded in that namespace
target_namespace = abc

macro_judgement  =

android_headers =

android_flags =  -target armv7-none-linux-androideabi -D_LIBCPP_DISABLE_VISIBILITY_ANNOTATIONS -DANDROID -D__ANDROID_API__=14 -gcc-toolchain %(gcc_toolchain_dir)s --sysroot=%(androidndkdir)s/platforms/android-14/arch-arm  -idirafter %(androidndkdir)s/sources/android/support/include -idirafter %(androidndkdir)s/sysroot/usr/include -idirafter %(androidndkdir)s/sysroot/usr/include/arm-linux-androideabi -idirafter %(clangllvmdir)s/lib64/clang/5.0/include -I%(androidndkdir)s/sources/cxx-stl/llvm-libc++/include

clang_headers =
clang_flags = -nostdinc -x c++ -std=c++17 -fsigned-char -mfloat-abi=soft -U__SSE__

cocos_headers = -I%(cocosdir)s/cocos -I%(cocosdir)s/cocos/platform/android -I%(cocosdir)s/external/sources

cocos_flags = -DANDROID -DCC_PLATFORM=3 -DCC_PLATFORM_MAC_IOS=1 -DCC_PLATFORM_MAC_OSX=4 -DCC_PLATFORM_WINDOWS=2 -DCC_PLATFORM_ANDROID=3


cxxgenerator_headers =

# extra arguments for clang
extra_arguments = %(android_headers)s %(clang_headers)s %(cxxgenerator_headers)s %(cocos_headers)s %(android_flags)s %(clang_flags)s %(cocos_flags)s %(extra_flags)s

# what headers to parse
headers = %(cocosdir)s/cocos/test/ABCJSBBridge.h

# cpp_headers = network/js_network_manual.h

# what classes to produce code for. You can use regular expressions here. When testing the regular
# expression, it will be enclosed in "^$", like this: "^Menu*$".
classes = JSBBridge

# what should we skip? in the format ClassName::[function function]
# ClassName is a regular expression, but will be used like this: "^ClassName$" functions are also
# regular expressions, they will not be surrounded by "^$". If you want to skip a whole class, just
# add a single "*" as functions. See bellow for several examples. A special class name is "*", which
# will apply to all class names. This is a convenience wildcard to be able to skip similar named
# functions from all classes.
skip = JSBBridge::[init]

rename_functions = 

rename_classes =

# for all class names, should we remove something when registering in the target VM?
remove_prefix = 

# classes for which there will be no "parent" lookup
classes_have_no_parents = JSBBridge

# base classes which will be skipped when their sub-classes found them.
base_classes_to_skip = Clonable

# classes that create no constructor
# Set is special and we will use a hand-written constructor
abstract_classes = JSBBridge

事实上,里面的注释也非常详细,以下是一些主要属性及其含义:

财产描述
前缀在生成的绑定文件中定义函数的名称。函数名称的组合是 。例如,如果我们在头文件中定义并设置 to ,最终绑定文件中的函数名称将是 .js + prefix + the function name in the header fileJSBBridge_ abcLogprefixcocos2dx testjs_cocos2dx_test_JSBBridge_abcLog
target_namespace脚本中的目标命名空间,例如:、、等。ccspine
要绑定的标头列表,用空格分隔,标头将被递归扫描。
cpp_headers需要包含在绑定代码中但不需要由绑定工具扫描的头文件的列表。
要绑定的类名列表,用空格分隔。
abstract_classes当配置为抽象类时,其构造函数将不绑定,用空格分隔。

完成上述配置后,到目录并运行以自动生成绑定文件。请注意下面的两个绑定:cdtools/tojs./genbindings_test.pycocos/bindings/auto

打开生成的:jsb_cocos2dx_test_auto.cpp

#include "cocos/bindings/auto/jsb_cocos2dx_test_auto.h"
#include "cocos/bindings/manual/jsb_conversions.h"
#include "cocos/bindings/manual/jsb_global.h"
#include "test/ABCJSBBridge.h"

#ifndef JSB_ALLOC
#define JSB_ALLOC(kls, ...) new (std::nothrow) kls(__VA_ARGS__)
#endif

#ifndef JSB_FREE
#define JSB_FREE(ptr) delete ptr
#endif
se::Object* __jsb_abc_JSBBridge_proto = nullptr;
se::Class* __jsb_abc_JSBBridge_class = nullptr;

static bool js_cocos2dx_test_JSBBridge_abcLog(se::State& s) // NOLINT(readability-identifier-naming)
{
    auto* cobj = SE_THIS_OBJECT<abc::JSBBridge>(s);
    SE_PRECONDITION2(cobj, false, "js_cocos2dx_test_JSBBridge_abcLog : Invalid Native Object");
    const auto& args = s.args();
    size_t argc = args.size();
    CC_UNUSED bool ok = true;
    if (argc == 1) {
        HolderType<std::string, true> arg0 = {};
        ok &= sevalue_to_native(args[0], &arg0, s.thisObject());
        SE_PRECONDITION2(ok, false, "js_cocos2dx_test_JSBBridge_abcLog : Error processing arguments");
        cobj->abcLog(arg0.value());
        return true;
    }
    SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", (int)argc, 1);
    return false;
}
SE_BIND_FUNC(js_cocos2dx_test_JSBBridge_abcLog)

static bool js_cocos2dx_test_JSBBridge_getInstance(se::State& s) // NOLINT(readability-identifier-naming)
{
    const auto& args = s.args();
    size_t argc = args.size();
    CC_UNUSED bool ok = true;
    if (argc == 0) {
        abc::JSBBridge* result = abc::JSBBridge::getInstance();
        ok &= nativevalue_to_se(result, s.rval(), nullptr /*ctx*/);
        SE_PRECONDITION2(ok, false, "js_cocos2dx_test_JSBBridge_getInstance : Error processing arguments");
        SE_HOLD_RETURN_VALUE(result, s.thisObject(), s.rval());
        return true;
    }
    SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", (int)argc, 0);
    return false;
}
SE_BIND_FUNC(js_cocos2dx_test_JSBBridge_getInstance)

bool js_register_cocos2dx_test_JSBBridge(se::Object* obj) // NOLINT(readability-identifier-naming)
{
    auto* cls = se::Class::create("JSBBridge", obj, nullptr, nullptr);

    cls->defineFunction("abcLog", _SE(js_cocos2dx_test_JSBBridge_abcLog));
    cls->defineStaticFunction("getInstance", _SE(js_cocos2dx_test_JSBBridge_getInstance));
    cls->install();
    JSBClassType::registerClass<abc::JSBBridge>(cls);

    __jsb_abc_JSBBridge_proto = cls->getProto();
    __jsb_abc_JSBBridge_class = cls;

    se::ScriptEngine::getInstance()->clearException();
    return true;
}
bool register_all_cocos2dx_test(se::Object* obj)
{
    // Get the ns
    se::Value nsVal;
    if (!obj->getProperty("abc", &nsVal))
    {
        se::HandleObject jsobj(se::Object::createPlainObject());
        nsVal.setObject(jsobj);
        obj->setProperty("abc", nsVal);
    }
    se::Object* ns = nsVal.toObject();

    js_register_cocos2dx_test_JSBBridge(ns);
    return true;
}

是不是看起来很眼熟?它与 Cocos 的现有文件完全相同,甚至包括所有自动生成的注册函数和类定义。.cpp

Cocos 编译配置

虽然我们在上述步骤之后已经生成了绑定,但 TypeScript 层不能直接使用,因为我们仍然需要将生成的绑定配置到要与其他C++文件一起编译的文件中,这是编译配置的最后一部分。CMakeLists.txtCMakeLists.txt

打开文件并向其添加初始 和,以及自动绑定生成的 和 文件:CMakeLists.txtABCJSBBridge.hABCJSBBridge.cppjsb_cocos2dx_test_auto.hjsb_cocos2dx_test_auto.cpp

打开并添加cocos2dx_test模块的注册码:cocos/bindings/manual/jsb_module_register.cpp

完成上述配置后,直接从 TypeScript 层调用它,如下所示:

import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('Test')
export class Test extends Component {
    start () {
        // @ts-ignore
        abc.JSBBridge.getInstance().abcLog("JSB binding test success")
    }
}

自动绑定限制

自动绑定依赖于绑定生成器工具。绑定生成器工具可以将C++类的公共方法和公共属性绑定到脚本层。自动绑定工具虽然功能非常强大,但也有一些限制:

  1. 它只能为类生成绑定,而不能生成结构、独立函数等的绑定。
  2. 无法生成类型 API,因为脚本中的对象无法从 C++ 中的类继承并重写其中的函数。DelegateDelegateDelegate
  3. 子类覆盖父类的 API,同时覆盖此 API。
  4. API 实现的一部分未完全反映在其 API 定义中。
  5. C++在运行时主动调用 API。

通知:由于 3.6 自动绑定中涉及的参数类型和返回值也需要绑定,或者需要提供转换方法 /,否则会在编译时保存。在 3.5 之前,它被报告为运行时错误。sevalue_to_nativenativevalue_to_se

总结

综上所述,JSB 的自动绑定只需要开发者编写相关的C++实现类、一个配置文件,然后执行单个命令即可完成整个绑定过程。如果没有特殊的定制,它仍然比手动绑定效率高很多。实际工作可以根据具体情况完成,首先使用自动绑定功能,然后手动修改生成的绑定文件,以两倍的努力达到一半的结果。

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

上一篇:Cocos Creator:JSB 手动绑定 (mvrlink.com)

下一篇:Cocos Creator:介绍 (mvrlink.com)

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