热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

深入解析CocosCreatorJSB绑定原理以及应用实践

一直以来,ABCmouse项目中的整体JSNative通信调用结构都是基于callStaticMethod<->evalString的方式。通过callStaticMethod方法我们可以通过反射机制直接在JavaScript中调用JavaObjective-C的静态方法。而通过evalString方式,则可以执行JS代码,这样便可以进行双端通信。虽然基于这个方式上层封装接口后,新增业

背景

一直以来,ABCmouse 项目中的整体 JS/Native 通信调用结构都是基于 callStaticMethod <-> evalString 的方式。通过 callStaticMethod 方法我们可以通过反射机制直接在 Javascript 中调用 Java / Objective-C 的静态方法。而通过 evalString 方式,则可以执行 JS 代码,这样便可以进行双端通信。

深入解析Cocos Creator JSB绑定原理以及应用实践

虽然基于这个方式上层封装接口后,新增业务逻辑会比较方便。但是过度依赖 evalString ,往往也会带来一些隐患。举个 Android 侧的例子:

对于常见的参数结构,这样运行是没有问题的,然而基于实际场景的种种情况,我们会发现针对引号的控制格外重要。如代码所示,为了保证 JS 代码能够被正确执行,我们在拼接字符串时必须明确 '与 " 的使用,稍有不慎就会出现 evalString 失败的情况。在 Cocos 的官方论坛上,从大量的反馈中我们也能了解这里的确是一个十分容易踩坑的地方。而另一方面,对于我们项目本身而言,过度依赖 evalString 所产生的种种不确定因素也往往很难掌控,我们又不能一味地通过 try/catch 去解决。所幸的是,经过全局业务排查,目前项目中在绝大多数因此,在查阅官方文档后,我们决定绕过 evalString ,直接基于 JSB 绑定的方式进行通信。

这里以下载器的接入为例。在我们的项目中,下载器是在 Android 与 iOS 侧分别各自实现。在改造之前的版本中,下载器的调用与回调基于 callStaticMethod <-> evalString 的方式。

每次调用下载都需要这样执行:

下载成功抑或是失败都需要通过拼接出类似如下的语句执行 JS:

无论是调用抑或是回调都拼接繁琐又容易出错,全部数据不得不转化为字符串~~(emmmm也不美观)~~,而且还要考虑到evalString的执行效率问题。如果只是仅有的少数业务场景在使用尚勉强接受,但是当业务日趋复杂庞大,如果都要这样写,同时又没有详细的文档去规范约束,其后期维护成本可想而知。

而当使用 JSB 改造后,我们调用只需如下寥寥几行代码且无需区分平台,更不必担心上述拼接隐患,相比之下逻辑要清晰许多:

那么接下来就以一个最简单的下载器的绑定流程为例,我来带大家学习下 JSB 手动绑定的大致流程。 (虽然 Cocos 很人性化提供了自动绑定的配置文件,可以通过一些配置直接生成目标文件,减少了很多工作量。但是亲手来完成一次手动绑定的流程会帮助更为全面地了解整个绑定的实现流程,有助于加深理解。另一方面,当存在特殊需要自动绑定无法满足时,手动绑定也往往会更为灵活)

前置

在开始之前,我们需要需要知道有关 ScriptEngine 抽象层、相关 API 等相关知识,这部分内容如果已从 Cocos 文档了解可跳过直接进行 实践 部分。

抽象层

深入解析Cocos Creator JSB绑定原理以及应用实践

首先先来看一下上图 Cocos 官方提供的一张抽象层架构,在1.7版本中,抽象层被设计为一个与引擎没有关系的独立模块,对 JS 引擎的管理从 ScriptingCore 被移动到了 se::ScriptEngine 类中,ScriptingCore 被保留下来是希望通过它把引擎的一些事件传递给封装层,充当适配器的角色。在这个抽象层提供了对 JavascriptCore、SpiderMonkey、V8、ChakraCore 等多种可选的 JS 执行引擎的封装。JSB 的大部分工作其实就是设定 JS 相关操作的 C++ 回调,在回调函数中关联 C++ 对象。它其实主要包含如下两种类型:

  • 注册 JS 函数(包含全局函数,类构造函数、类析构函数、类成员函数,类静态成员函数),绑定一个 C++ 回调

  • 注册 JS 对象的属性读写访问器,分别绑定读与写的 C++ 回调

考虑到不同多种 JS 引擎的关键方法的定义各不相同,Cocos 团队使用宏来抹平这种回调函数定义与参数类型的差异,这里就不展开,详细可阅读文末Cocos Creator 的相关文档。 值得一提的是,ScriptEngine 这层设计之初 Cocos 团队就将其定义为一个独立模块,完全不依赖 Cocos 引擎。我们开发者完全可以把 cocos/scripting/js-bindings/jswrapper 下的所有抽象层源码移植到其他项目中直接使用。

SE 类型

C++ 抽象层所有的类型都在 se 命名空间下,其为 ScriptEngine 的缩写。

  • se::ScriptEngine它是 JS 引擎的管理员,掌管 JS 引擎初始化、销毁、重启、Native 模块注册、加载脚本、强制垃圾回收、JS 异常清理、是否启用调试器。 它是一个单例,可通过 se::ScriptEngine::getInstance() 得到对应的实例。

  • se::Value可以被理解为 JS 变量在 C++ 层的引用。JS 变量有 object, number, string, boolean, null, undefined 六种类型,因此 se::Value 使用 union 包含 object, number, string, boolean 4 种有值类型,无值类型: null, undefined 可由私有变量 _type 直接表示。 如果 se::Value 中保存基础数据类型,比如 number,string,boolean,其内部是直接存储一份值副本。 object 的存储比较特殊,是通过 se::Object* 对 JS 对象的弱引用。

  • se::Object继承于 se::RefCounter 引用计数管理类,它保存了对 JS 对象的弱引用。我们在绑定回调中如果需要用到当前对象对应的 se::Object,只需要通过 s.thisObject() 即可获取。其中 s 为 se::State 类型。

  • se::Class用于暴露 C++ 类到 JS 中,它会在 JS 中创建一个对应名称的构造函数。Class 类型创建后,不需要手动释放内存,它会被封装层自动处理。se::Class提供了一些 API 用于定义 Class 的创建、静态/动态成员函数、属性读写等等,后面在实践时用到会做介绍。完整内容可查阅 Cocos 文档。

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

前面有提到, 抽象层使用宏来抹平不同 JS 引擎关键函数定义与参数类型的不同,不管底层是使用什么引擎,开发者统一使用一种函数的定义。

例如,抽象层所有的 JS 到 C++ 的回调函数的定义为:

我们在编写完回调函数后,需要记住使用 SE_ BIND_ XXX 系列的宏对回调函数进行包装。目前全部的 SE_ 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 子类的构造函数,此子类使用 cc.Class.extend 继承 Native 绑定类

  • SE_ FINALIZE_ FUNC:包装一个 JS 对象被 GC 回收后的回调函数

  • SE_ DECLARE_ FINALIZE_FUNC:声明一个 JS 对象被 GC 回收后的回调函数

  • SE:包装回调函数的名称,转义为每个 JS 引擎能够识别的回调函数的定义,注意,第一个字符为下划线,类似 Windows 下用的 T("xxx")来包装 Unicode 或者 MultiBytes 字符串

在我们的简化版例子中,只需要用到 SE_ DECLARE_ FUNC、SE_ BIND_ FUNC 即可。

类型转换辅助函数

类型转换辅助函数位于 cocos/scripting/js-bindings/manual/jsb_conversions.hpp/.cpp 中,包含了多种 se::Value 与 C++ 类型相互转化的方法。

  • bool std string to_seval(const std::string& v, se::Value* ret);

  • bool seval to std_string(const se::Value& v, std::string* ret);

  • bool boolean to seval(bool v, se::Value* ret);

  • bool seval to boolean(const se::Value& v, bool* ret); ... ...

实践

在开始之前,我们需要明确一下流程。JSB 绑定简单来讲就是在C++层实现一些类库,然后经过一些特定处理可以在 JS 端进行对应方法调用的过程。因为采用 JS 为主要业务编写语言,使得我们在做一些 Native 的功能时会比较受限,例如文件、网络等等相关操作。

以 Cocos2d-js 文档中 cc.Sprite 为例,在 JSB 中 如果使用 new 操作符来调用 cc.Sprite 的构造函数,实际上在 C++ 层会调用 js cocos2dx Sprite_constructor 函数。在这个 C++ 函数中,会为这个精灵对象分配内存,并把它添加到自动回收池,然后调用 JS 层的 _ctor 函数来完成初始化。在 _ctor 函数中会根据参数类型和数量调用不同的init函数,这些init函数也是C++函数的绑定:

三层的方法对应关系如下:

深入解析Cocos Creator JSB绑定原理以及应用实践

这个调用过程的时序如下:

深入解析Cocos Creator JSB绑定原理以及应用实践

和上面的过程类似。首先,我们需要确定接口和字段,我们随便拟定一个最简单的下载器 FileDownloader,它所具备的是 download(url, path, callback) 接口,而在 callback 中我们需要拿到的则是 code,msg。并且为了方便使用,我们将它挂载在 jsb 对象下,这样我们便可以使用如下代码进行简单地调用:

确定接口后,我们可以开始着手码 C++ 部分了。首先来一发 FileDownloader.h,作为公共头文件供 Android/iOS 使用。接着 Android/iOS 分别实现各自的具体下载实现即可(此处略过),reqCtx 则用于存储回调对应关系:

接下来我们进行最关键的绑定部分。 因为下载器就功能上分类属于 network 模块,我们可以选择将我们的 FileDownloader 的绑定实现在 Cocos 源码中现有的 jsb cocos2dx network auto 中。在 jsb cocos2dx network auto.hpp 中声明 JS 函数:

随后在 jsb cocos2dx network_auto.cpp 中来注册 FileDownloader 和新声明的这两个函数到 JS 虚拟机中。首先先写好对应的两个方法实现留空,等注册逻辑完成后再来补全:

现在我们开始编写注册逻辑,新增一个注册方法用于收归 FileDownloader 的全部注册逻辑:

我们来看看这个方法里做了些什么重要的事情:

  1. 调用 se::Class::create(className, obj, parentProto, ctor) 方法,创建了一个名为 FileDownloader 的 Class,注册成功后,在 JS 层中可以通过 let xxx = new FileDownloader();的方式创建实例。

  2. 调用 defineFunction(name, func) 方法,定义了一个成员函数 download,并将其实现绑定到包装后的

    js cocos2dx network FileDownloader download 上。

  3. 调用 defineStaticFunction(name, func) 方法,定义了一个静态成员函数 getInstance,并将其实现绑定到包装后的 js cocos2dx network FileDownloader getInstance 上。

  4. 调用 install() 方法,将自己注册到 JS 虚拟机中。

  5. 调用 JSBClassType::registerClass 方法,将 生成的 Class 与 C++ 层的类对应起来(内部通过 std::unordered_map <:string> 实现)。

通过以上这几步,我们完成了关键的注册部分,当然不要忘记在 network 模块的注册入口添加 js register cocos2dx network FileDownloader 的调用:

完成这一步,我们的 Class 已经成功绑定,现在回来继续完善刚才留空的方法。

首先是 getInstance():

前面提到,我们可以通过 se::State 获取到 C++ 指针、se::Object 对象指针、参数列表、返回值引用。梳理逻辑如下:

  1. args() 获取 JS 带过来的全部参数(se::Value 的 vector);

  2. 参数个数判断,因为这里的 getInstance() 并不需要额外参数,因此参数为 0;

  3. native ptr to_seval() 用于在绑定层根据一个 C++ 对象指针获取一个se::Value,并赋返回值给rval()至 JS 层;

到这里,getInstance()的绑定层逻辑已全部完成,我们已经可以通过:let downloader = jsb.FileDownloader.getInstance() 获取实例了。

接着是 download():

  1. 通过 seval to std_string 方法获取转化 C++ 后的 url、path 参数和原始 jsFunc。

  2. 手动构造回调 function,将 msg 和 code 转化为 se::Value。

  3. 通过 funcObj->call 执行 JS 方法进行回调。

以上即为一次普通调用的回调的执行过程的绑定。现在我们还剩下一些收尾工作,我们需要将 FileDownloader 真正成为单例,在 JS 层无需手动实例化即可使用。

因为下载器属于通用组件,所以我们需要尽早将其实例化并成功挂载,因此我们需要修改 jsb_boot.js,这个文件会在 Cocos 引擎初始化时调用,我们在其中补充如下代码:

最后,考虑到内存释放的风险,我们还需要在 CCDirector.cpp 中的 reset() 方法中进行相关回收:

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

以上就是全部的绑定流程,在分别编译到 Android/iOS 环境后,我们就能够通过 jsb.fileDownloader.download() 进行下载调用了。 (PS:一定切记在使用前进行 CC_JSB 的宏判断,因为非 JSB 环境下是无法使用的)

总结

我们现在来总结一下手动绑定改造的详细流程。一般而言,常用的 JSB 的改造流程大致如下:

  • 确定方法接口与 JS/Native 公共字段

  • 声明头文件,并分别实现 Android JNI 与 OC 具体业务代码

  • 编写抽象层代码,将必要的类与对应方法注册到 JS 虚拟机中

  • 将绑定的类挂载在 JS 中的指定对象(类似命名空间)中

在下一篇(原理篇)中,我们会继续从源码详细分析 ScriptEngine 的结构和完整的 JSB 调用原理,敬请期待。


以上所述就是小编给大家介绍的《深入解析Cocos Creator JSB绑定原理以及应用实践》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 我们 的支持!


推荐阅读
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • baresip android编译、运行教程1语音通话
    本文介绍了如何在安卓平台上编译和运行baresip android,包括下载相关的sdk和ndk,修改ndk路径和输出目录,以及创建一个c++的安卓工程并将目录考到cpp下。详细步骤可参考给出的链接和文档。 ... [详细]
  • Android Studio Bumblebee | 2021.1.1(大黄蜂版本使用介绍)
    本文介绍了Android Studio Bumblebee | 2021.1.1(大黄蜂版本)的使用方法和相关知识,包括Gradle的介绍、设备管理器的配置、无线调试、新版本问题等内容。同时还提供了更新版本的下载地址和启动页面截图。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • C# 7.0 新特性:基于Tuple的“多”返回值方法
    本文介绍了C# 7.0中基于Tuple的“多”返回值方法的使用。通过对C# 6.0及更早版本的做法进行回顾,提出了问题:如何使一个方法可返回多个返回值。然后详细介绍了C# 7.0中使用Tuple的写法,并给出了示例代码。最后,总结了该新特性的优点。 ... [详细]
  • 解决Cydia数据库错误:could not open file /var/lib/dpkg/status 的方法
    本文介绍了解决iOS系统中Cydia数据库错误的方法。通过使用苹果电脑上的Impactor工具和NewTerm软件,以及ifunbox工具和终端命令,可以解决该问题。具体步骤包括下载所需工具、连接手机到电脑、安装NewTerm、下载ifunbox并注册Dropbox账号、下载并解压lib.zip文件、将lib文件夹拖入Books文件夹中,并将lib文件夹拷贝到/var/目录下。以上方法适用于已经越狱且出现Cydia数据库错误的iPhone手机。 ... [详细]
  • 本文详细介绍了在ASP.NET中获取插入记录的ID的几种方法,包括使用SCOPE_IDENTITY()和IDENT_CURRENT()函数,以及通过ExecuteReader方法执行SQL语句获取ID的步骤。同时,还提供了使用这些方法的示例代码和注意事项。对于需要获取表中最后一个插入操作所产生的ID或马上使用刚插入的新记录ID的开发者来说,本文提供了一些有用的技巧和建议。 ... [详细]
  • Python正则表达式学习记录及常用方法
    本文记录了学习Python正则表达式的过程,介绍了re模块的常用方法re.search,并解释了rawstring的作用。正则表达式是一种方便检查字符串匹配模式的工具,通过本文的学习可以掌握Python中使用正则表达式的基本方法。 ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
author-avatar
天黑啲雨_165
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有