在移动设备上,Hybrid 混合开发的性能问题一直为人诟病,对它的各种优化、hack 层出不穷,但你听说过用游戏引擎来优化 Hybrid 性能的吗?这次我们的 GMTC 邀请到了腾讯高级工程师潘伟洲分享《基于 Cocos 的高性能跨平台应用开发方案》,我们提前对他进行了采访,了解了一些技术细节,分享给大家。
InfoQ:Cocos 作为开源的跨平台游戏引擎框架,是一个什么样的契机让你和你的团队想到了用 Cocos 来改造原本 Hybrid 形式的产品的?
潘伟洲:先从动力说起:ABCmouse 原来的版本出于跨平台的目的,项目的大部分采用的是 Hybrid 的形式,大部分的页面加载都非常耗时,以一个画图的功能为例,从打开到加载完成可能要耗费十几秒的时间,这对于大部分用户来说是难以忍受的。
通过初期技术预研后,我们决定使用 Cocos 来改造这个项目,主要出于以下几个考虑:
- 跨平台。Cocos 支持使用同一套代码构建生成 Web、iOS、Android 等几个端,最新的版本还支持发布到微信小游戏、Facebook Instant Games 和 QQ 玩一玩;
- 性能。Cocos 的原理是在 Activity 中绘制一个 OpenGL 的 SurfaceView ,并由其完成页面的渲染的。与基于 WebView 渲染的 Hybrid 应用相比,Cocos 的渲染速度更快,性能更好。
- 效率。借助可视化的 Cocos Creator 工具,界面的开发和资源的管理非常便捷,设计团队也可以参与进来设计界面和动效,提升开发效率。
- 表现力。ABCmouse 中包含了很多诸如游戏、画图、音乐等带游戏和娱乐性质的场景,而 Cocos 本身是为游戏开发设计的,更适合用在我们的产品中。
InfoQ:在实际开发过程当中,与其他跨平台的开发框架相比较,你觉得用 Cocos 的优势在哪里?
潘伟洲:首先是生成的平台更多,除了常见的 Web、iOS、Android 外,难得的还支持微信小游戏、Facebook Instant Games 和 QQ 玩一玩的发布。其次,因为 Cocos 的核心开发团队来自中国,能提供更好的技术支持。腾讯也是 Cocos 的合作伙伴,我们与他们建立了良好的合作关系。
InfoQ:使用 Cocos 开发,你们在技术选型,基础组件储备上做了哪些尝试与创新呢?
潘伟洲:技术选型方面,我们先是为 UI 层设计开发了一些 Cocos 所缺失的一些通用组件,包括:对话框、日期选择器、Toast、Loading 组件等,这些组件能在全部目标平台上通用,并且遵循着统一的接口风格,使得 UI 层的开发更加便利;
在图片加载方面,我们实现了一个带缓存功能的图片加载组件,优化二次加载在线图片的性能,并自动管理内存回收。另外, 为了提高图片的加载性能,我们改进了 Cocos 的图片的加载方式,让其支持使用 ETC2 纹理渲染,使得内存消耗降低了接近 70% 。
由于应用中有非常多的音乐、音效、语音,为了减小包大小,大部分的语音素材放在 CDN 上,需要的时候才从 CDN 上拉取播放。少部分常见的音效会直接打进应用包中。而 Cocos 自带的 AudioEngine 组件在 Native 端只支持本地资源的播放。因此,我们又封装了一个跨平台的音频播放器,可以自动根据指定的音频路径决定使用播放方式:
- 对于 Web 端或者 Native 端的本地资源文件,直接使用 AudioEngine 来播放。
- 对于 Native 端的远程音频,使用 Native 的播放器来播放。
由于对外的接口只有一套,开发者无需考虑具体的平台和底层播放器的选择。并且可以使用同样的接口来统一管理不同的音频。
由于客户端大部分的功能都是使用 JS 写的,为了更快速地定位问题,我们在底层对 JS Exception 做了拦截,一旦出现 JS Exception,就将错误信息通过 Toast 展示出来,并且详细的给出了堆栈信息和场景信息。这个错误展示帮助我们发现并修复了很多 JS 错误。
除了以上所提到的几个能力,我们在 Cocos 层还提供了通用的日志记录、事件上报等。这些在不同的端有不同的实现,但都通过 Cocos 层进行了适配,使得对开发者都是统一的一套接口。
InfoQ:在开发设计 ScrollView 控件时,你们做了哪些优化?
潘伟洲:官方 ScrollView 组件需要配合 layout 组件,当一次加载大量的子节点组件,或者分帧加载单个子节点组件时,初始化 ScrollView 节点视图会比较慢,在加载完成前存在拖动掉帧的问题。另外,一次性加载所有节点,也会导致内存资源的浪费。
我们对 ScrollView 进行了重写,基本的优化思路是:一次仅加载页面可容纳的少量数目子节点。并在滚动过程中,回收不可视的子节点组件并重用。
具体来说,ScrollView 大多数情况下表现为列表组件和宫格组件,以列表组件为例,可以根据子节点数目和子节点大小,计算出整个 ScrollView 内容的宽高,同时计算出屏幕可视区域最多可以容纳的子节点行数 rows,加载时仅加载 rows + 2 个子节点组件,其中添加的 2 个字节点组件作为滚动回收缓冲。举个具体点的例子,当手势向上,内容往下滚动时,一旦最上面一个子节点组件 A 不可视,就立马回收掉 A 并将其重用于将要渲染的子节点组件。
我们做过测试,我们有个页面加载了非常多的内容,在优化前,拖动的时候帧率可能会跌到 8 fps,而使用优化后的 ScrollView,帧率能够稳定在 60 fps。
InfoQ:在实际运用 Cocos 开发过程中,你们有踩过哪些坑呢?包大小的问题又是怎么解决的呢?
潘伟洲:我们遇到的第一个坑和 Cocos 的 VideoPlayer 组件有关:Cocos 的基本原理是在 Activity 里头展示一个 OpenGL 的 SurfaceView ,这个 SurfaceView 不能直接绘制视频。
为了解决视频的播放问题,Cocos 的引擎开发者们将视频播放器作为独立的一层 View ,并保持置顶。这带来的问题是:VideoPlayer 组件上无法绘制任何其他组件。比如,如果希望定制播放按钮、进度条等元素,使用 Cocos 的 VideoPlayer 是做不到的。
最终我们放弃了在 Cocos 层开发视频播放器的功能,而是在底层为各个端开发视频播放器,并各自实现界面的定制。
第二个坑是跨层调用的响应问题。Native 层和 Cocos 层交互时,存在一定的响应时间。
比如,前面说到,我们的视频播放器是在 Native 层开发的,我们有一个需求:当视频播放完后,立即跳转到另一个 Cocos 的场景。然而,由于跨层调用存在一定响应时间,当把视频播放器的 activity 关闭时,此时页面会回到调起视频播放器前的场景,然后才进入一个新的场景。
为了解决这个问题,我们在 Cocos 层设计了一个隐藏的常驻节点,在关闭 activity 前,先把这个节点设为可见,并将其颜色改为下一个场景的背景色,直到下一个场景加载完毕时才重新将该节点设为隐藏。由于设置节点的可见性远比加载场景要快,此时再关闭 activity 时,用户先看到的是这个节点,给用户一种即将进入下一个场景的错觉,就不会觉得场景的跳转很突兀。
最后我们遇到的一个比较严重的问题是 local reference table overflow error 问题。
为了复用 Native 端的能力,我们在 Cocos 层大量地使用反射机制来调用 Native 端提供的方法。然而,我们经常会遇到 local reference table overflow error 错误导致的界面卡死问题。最初,我们怀疑是反射调用使用得太频繁导致。因此,我们对诸如打 log、事件上报等 Native 方法进行了频率限制,例如使用缓冲的方法将多个 log 合并后再打印。
然而,虽然这个做法减少了界面卡死的发生,但依然没有彻底杜绝问题的再次出现,就像是一个定时炸弹一样,威胁着我们应用的稳定性。通过阅读引擎的代码,我们发现 Cocos 的引擎在反射阶段处理字符串参数时,使用了 NewStringUTF()
方法将其转换为 JNI 层的字符串,然而在调用执行完成后并没有相应地使用 DeleteLocalRef()
释放该字符串的引用,从而导致了引用表的溢出。了解到这个原因后,我们给 Cocos 的引擎提交了一个 pull request,修复了这个问题。
在包大小的控制方面,我们都知道图片资源往往是导致应用包臃肿的元凶。因此,为了减少包大小,我们应该能够找出工程里头的无用资源,以及可优化的资源。
针对无用资源,Cocos 建议将静态引用的图片存在 res 目录,而将动态引用的图片放在 resource 目录。这么区分的好处是,res 目录下的图片只有存在引用时才会被打包进应用,而没有被引用的图片则不会被打包进应用。
因此,我们首先编写了静态分析脚本,找出工程中不合理放置的图片资源(例如将静态引用的图片放置到 resource 目录的情况),并移除 resource 中没有使用的图片。另外,对于 res 目录,我们也会经常检查是否存在不必要的引用,比如移除废弃的场景或者节点等。
可以优化的资源指的是可以进一步压缩的资源,这个和原生应用的优化思路差不多。比如合理的使用有损压缩 / 无损压缩工具来压缩图片文件。对于不需要透明度的图片资源,则使用 JPEG 格式代替 PNG 格式。
另外,在实现一些动效时,我们也尽量使用属性动画而不是帧动画,以节省空间。在这一点上,Cocos 的优势比较明显。我们的动效通常是设计团队直接使用 Cocos Creator 的动画编辑器创建的动画剪辑,并非简单的帧动画。
InfoQ:在什么样的场景下,才适合做基于 Cocos 的产品开发呢?
潘伟洲:取决于产品的定位。Cocos 主要是为游戏开发设计的,所以产品的表现力更佳,但在性能、内存占用、耗电上不如普通原生应用。当你的产品要求有更高的表现力,UI 开发更灵活,并且希望能实现跨平台开发时,Cocos 是一个值得考虑的选择。
InfoQ:在众多的跨平台 app 开发框架当中,开发者如何找到适合自己的框架?
潘伟洲:建议结合产品的需求、定位、框架成熟度、社区支持情况等维度进行综合考量。
比如,如果你要开发一个视频播放器,那么像 Cocos 这种使用 OpenGL 绘制 UI 的框架就难以实现跨平台开发。而如果要开发一个高表现力的应用,比如内置小游戏或者是带翻书特效的阅读应用,那么就应该考虑 Cocos 这类支持 OpenGL 绘制的框架,或者与 H5 页面结合的 Hybrid 框架。
另外,如果产品对性能要求更高,那么应该选择更 “Native” 的框架,例如 React Native、Weex ,或者 Flutter 。而如果产品比较轻量,也更看重开发效率,那么可以选择 Hybrid 的框架。
除了从产品自身的需求定位出发,选择一个框架还应该考虑这个框架本身的成熟度以及社区支持情况。比如,Google 的 Flutter 是最近比较受关注的跨平台开发框架,它的亮点在于跨平台、高性能和不俗的表现力。然而这个框架目前还不够成熟,社区的轮子还不够丰富,能够得到的帮助也比较有限,所以建议现阶段可以保持关注,但小团队不要贸然去尝试。
感谢覃云对本文的审校。
评论