Lark新特性解读(一)

Posted on July 13, 2015
更新: Lark项目的新特性现已全面合并到Egret Engine 2.5+中,不再独立维护,Lark GUI(Swan)扩展库,对应Egret Engine 2.5+中的EUI扩展库。

什么是Lark?

lark

Github开源地址:https://github.com/egret-labs/Lark

Lark 是一款基于 HTML5 技术构建跨平台移动 Web 应用,微站和富媒体广告营销的交互应用框架。Lark和Egret同属白鹭时代旗下的两款开源产品,今后会并行发展:Egret主要面向游戏行业,而Lark主要面向应用开发行业。并行发展能够让两个产品更加专注在细分领域,根据特定开发需求实现针对性优化。目前Lark只发布了框架源码部分,第一个版本重在提供一个精简高效的渲染核心,除了专门为应用开发精简或优化的部分,其他大部分API跟Egret都比较类似。但随着后续版本迭代,各种工具集和扩展库逐步丰富起来,两个产品的差距也会越来越大。这系列文章主要面向已经熟悉Egret的开发者,重点在于解读Lark相对Egret所引入的新特性,让部分开发者能快速上手Lark。更加详细的教程文档,也会在最近几周内陆续开放出来。可以关注此页面:https://github.com/egret-labs/Lark/tree/master/docs

这系列文章将会从开发者角度,将值得关注的重点功能特性逐一解读。这系列文章预计会有三篇,前两篇主要讲解Lark核心库内容,第三篇讲解全新的GUI库(Swan)。前两篇所涉及的核心库大部分改动都是围绕着这个主题:提升性能,精简包体,拥抱Web标准。Lark核心库去除掉了大量的中间层封装代码,直接暴露Web已有的API结构。这样能在有效提升性能的情况下,同时降低最终包体大小,目前Lark的核心库大小仅有68k(未GZIP压缩)。大部分重构的新特性,也更加偏向前端JS通用开发方式。

多个Lark应用共存

之前一个页面内只允许运行一个程序实例,主要是因为引擎内部存在全局单例,当多个程序实例共存时,会互相影响公共的单例数据,导致各种意外的冲突。而Lark从源头上解决了单例问题,每个Lark应用实例都会创建一个对应的Stage,应用以Stage为中心进行操作,而不是全局单例,因此可以在一个页面内运行多个Lark应用实例。Lark的JS库文件现在更加类似于JQuery之类的通用JS库,引用到网页里之后,可以让多个独立的应用程序调用公共的Lark API。当然,开发者同样需要自己处理好各个独立应用之间的模块或类名重名问题。

支持同一个页面运行多个应用实例只是一处应用场景,Lark从源头上解决这个问题之后,能让第三方开发具有更加强的扩展性,例如支持在Lark应用中载入另一个Lark应用,虽然这部分API还未实现,但是理论上是完全可行的,我们将会在未来提供此功能。

全自动脏矩形渲染

脏矩形渲染简单说就是只重绘屏幕发生改变的区域。相对与传统的显示列表渲染方式,也就现在各种框架普遍使用的,是一种全屏刷新模式:引擎会按照你设置的帧率,通常也就每秒60次的频率去刷新,每次刷新都会清空整个屏幕,然后遍历整个显示列表,把所有显示对象绘制一遍到屏幕上。这是一种实现最简单,也不容易出错的机制,但是性能比较低。因为绝大多数情况下,我们屏幕只有部分区域会发生改变,并且也不是每秒60次都在改变。而脏矩形渲染机制能够彻底过滤掉这部分不需要重绘的情况,帧率只是一个检查点,当检测到此帧并未发生改变,直接跳过渲染什么也不做。而如果发生部分改变,将只清空并重绘改变的区域。试想一下,相比原先每秒60次的全屏刷新,能节省下来的开销是极其可观的。

因此脏矩形渲染直观上能带来性能的提升,我们做了一个简单的测试:一款H5游戏引入脏矩形渲染后,平均性能约有2~4倍提升,极端情况下上不封顶,因为脏矩形模式在静止情况下开销为0。而脏矩形渲染的好处并不仅限于此:两个版本在相同浏览器和设备上运行相同时间,脏矩形渲染的版本,耗电量降到了原先的30%,设备温度也由原先的46摄氏度维持在了31~32摄氏度之间,低于人体体温,能够明显感觉到清凉。

程序优化通常都是用开销小的操作去替代开销大的,部分开发者可能会比较担心脏矩形算法自身的开销。目前测试结果表明,Canvas上drawImage()方法仍然是HTML5上渲染的最大瓶颈,脏矩形渲染使用的纯数字加减运算,在开销量级上远远小于drawImage(),所以我们提升性能的方式还是尽可能避免不必要的drawImage()调用。不过Lark里还是对这部分做了额外优化,设置了一个阈值,当检测到屏幕刷新面积已经到临界点时,会直接放弃脏矩形计算而进入全屏刷新模式。这样在全屏都改变的极端情况下,也能自动保持原有的性能。

最后值得一提的是,目前Lark里提供的脏矩形渲染已经实现完全自动,开发者无需关心内部实现细节,只要操作基本的显示列表API即可。另外,Egret目前也正在移植Lark的渲染核心,并且移植已经接近尾声。

全自动位图缓存

位图缓存是一个非常实用且显著的性能优化方式。当设置一个容器的cacheAsBitmap为true后,它会创建一个位图,缓存下容器内部所有的子项的叠加显示结果。这样到渲染屏幕阶段时,就直接使用这个缓存数据来渲染,而不用遍历到容器内部的子项。对于一些非常复杂的容器,但是内部通常是很少发生改变的情况,开启此功能能大幅度提高性能。不过应该注意,位图缓存功能是以内存换CPU时间的优化方式,若过度使用,将会导致内存不足,另外对频繁改变的对象使用位图缓存也会导致性能下降。

位图缓存其实并不是一个新概念。之前Egret的版本中也存在此API,但是在内部子项发生改变时需要手动充值cacheAsBitmap属性来重新绘制。现在Lark里引入了全自动的脏矩形绘制机制后,这部分也变成了全自动的,大大提升了这个API的可用性。开发者只需要将cacheAsBitmap设置为true,之后内部发生任何的改变,都会自动更新缓存的位图。跟进一步,位图缓存的更新如今也使用了脏矩形渲染来局部更新。因此开启位图缓存的节点,相当于是一个独立的显示列表,仅在需要更新缓存时启用脏矩形渲染来局部更新。这样的改进让位图缓存内部发生改变时,内部更新性能非常接近于常规整体舞台更新的模式。但仍然建议开发者正确使用,仅对内部不经常改变,又特别复杂的显示对象开启。

另外,新版本的位图缓存机制,也同时解决了某些浏览器限制Canvas创建数量,而造成的渲染错误问题。当开发者设置cacheAsBitmap为true时,引擎并不一定会启用位图缓存,若当前无法继续创建Canvas,将自动放弃位图缓存,以确保显示渲染正确。不过好在限制Canvas创建数量只是个别浏览器行为,最新的版本已经放开这一限制。今后我们也会考虑将内存检查加入到判断条件,在内存不足时自动放弃位图缓存,从而能够有效避免程序在某些低端机型上崩溃。

可设置帧刷新频率

现在Lark里可以直接通过stage.frameRate属性来设置帧率,默认帧率为30(能够减小耗电量)。Lark里重新设计了帧频的触发机制,现在更加接近于网上流传的「Flash滑动跑道模型」:在全局有一个共享的心跳计时器,它能确保在设备刷新屏幕前执行回调,也就是说在回调期间执行的渲染改变能够立即显示到屏幕上,通常都是以60fps的固定帧率回调。而现在帧率设计为与心跳计时器是分离的。例如,当你设置帧率为30时,会每隔两个时间片触发一次屏幕刷新,当你设置帧率为24时,会以2个时间片,3个时间片的间隔交替触发屏幕刷新。开发者可以设置1~60的任意帧频(不需要特定倍数关系),都将保持稳定帧率,并保证每次回调的时机都是60fps中的一次,也就是渲染能立即呈现的时机。 将帧频与设备的60fps心跳独立开后,好处是Timer等计时器仍然按60FPS频率检查刷新。所以帧率设置完全不会影响计时器的逻辑表现。

强制刷新屏幕

基于以上的帧率和心跳计时器分离的设计,Lark新增了一个updateAfterEvent()的API,熟悉Flash的开发者可能已经对这个比较了解。它能够忽略帧频限制,强制执行一次屏幕渲染。这个方法主要存在于TimerEvent和TouchEvent事件对象上,也就是在这两种事件的回调函数中,你能调用event.updateAfterEvent()来通知系统强制刷新屏幕。为什么是在这两个事件上呢?因为TimerEvent和TouchEvent发生的时机,通常代表着播放缓动动画或者用户发生交互操作的时刻,这两种情况下,都需要高的帧频刷新率来保证流畅的视觉体验。这样设计的好处是,你可以将帧频设置的很低,比如12fps,但只要在有播放缓动动画,或者用户交互行为时持续调用event.updateAfterEvent()方法,就能临时突破帧频限制,强制以60fps的帧率刷新屏幕。而在没有交互操作或播放动画时,大部分是静态的,可以按12fps缓慢地更新,节省设备资源。

简化初始化流程

Lark里不再额外封装一层JS加载机制,而是直接使用前端JS开发非常熟悉且通用的初始化方式,类似JQuery直接在HTML页面里嵌入标签,即可完成对lark.js类库的引用。用此方法同样也可以嵌入第三方库的JS文件,不再需要写任何额外的配置文件。另外,完成应用初始化也只需一个div标签即可,不再需要复杂的JS代码定制。所有的初始化属性都可以直接在div标签上声明。如下图:

Lark_DIV

只需将DIV的class属性声明为约定的”lark-player”即可,在网页加载完成后,Lark会获取网页中所有class值为”lark-player”的div标签,为其实例化一个对应的Stage并启动入口程序。具体每个参数的含义,可以参考在线的Lark核心库开发指南,这里不在赘述。改成直接用div标签初始化之后,即使没有Lark开发经验,任何前端开发者都能根据简洁的XML语法正确配置程序,也让美术人员更加容易定制HTML页面样式。如文章开头所说,Lark允许多个应用共存,也就是开发者可以定义这样多个div标签,只要让data-entry-class入口类都不相同即可。

网络加载

Lark的网络请求使用HTTPRequest类,API设计为与Web端的XMLHTTPRequest接口一致,不再包装为URLLoader的形式。这样能够极大简化发送POST请求时需要编写的代码量。同时底层实现直接映射到XMLHTTPRequest,有效减少了额外的封装代码量,也一定程度提升了脚本性能。而图片加载也从HTTPRequest中独立出来,成为ImageLoader类,这么设计有一部分原因是底层实现更加简洁,另外一部分原因是目前浏览器API普及率不一致,图片加载暂时还无法抛出兼容所有浏览器的加载进度事件。独立出来,可以让HTTPRequest能够正常抛出进度事件,而不用因为图片加载的特殊性,而屏蔽了普通文件加载的进度事件。另外,开发者一直强烈需求的从Base64字符串加载图片的方式,现在ImageLoader也原生支持了:直接将Base64字符串作为ImageLoader要加载的目标url字符串传入即可。

简化的事件流API

熟悉Flash或Egret API的开发者,可能在刚接触Lark API时,最为困惑的就是找不到事件抛出类对应的API了。Lark将之前的EventDispatcher类重命名为了EventEmitter类。EventEmitter类的功能并没有发生太大变化,主要是新增了一个once()方法,监听事件后只回调一次就自动移除。这个方法在监听添加到舞台或加载完成事件时非常有用,能够让开发者编写的代码更加简洁。配合once()的API风格,同时将之前的addEventListener()方法重命名为了更加简短的on()方法。on()方法在现代的前端JS框架内非常流行,现在主流的JS框架例如NodeJS和JQuery都采用了on()做为事件监听方法名。重命名的另外一方面原因:addEventListener()有16个字符,对比on()只有2个字符,而事件监听的API在代码内调用的频率非常之高, 并且uglifyjs等JS代码混淆工具,对于公开变量是不混淆的。因此对调用频率过高的API,采用更简短的命名,能有效减小最终的JS包体大小。

完备的矢量绘图API

之前的矢量绘图API是在Canvas标准绘图API基础上,封装了一层类似Flash Graphics的API。而在Lark里,Graphics类直接暴露了Canvas的标准绘图API,不再额外封装一层。Lark里Graphics类与Canvas的绘图API区别在于,Canvas的绘图API是相对于整个画布的绘制,并且无法记录绘制过程,而Graphics的绘制是相对于单个显示对象的,能够记录绘图命令,同时与显示列表的脏矩形渲染机制有机结合。Lark将矢量绘图API与Canvas统一后,同样也是简化了底层实现,绘图命令能够直接一一映射到Canvas的API,提高运行时性能。同时减少了大量封装层代码,也就减少了最终的包体大小。并且Canvas的矢量绘图API是非常完备的,开发者不用再担心上层封装不全,导致的无法实现某些绘图效果。而如果后续依然有很多开发者对Flash风格的绘图API有需求,现有的设计同样可以满足,将Flash风格的绘图API再封装为一个可选的扩展库即可,并不影响核心库高效精简的设计。

位图数据接口

Lark核心库里不再提供含有偏移量数据的Texture类,而是定义了一个BitmapData接口,用于直接暴露HTML原生的Image,Canvas,Video对象作为BitmapData实例。这样做比较明显的好处是,简化了核心渲染的复杂度,让整体渲染能够以更高的性能运行。而Texture类的偏移量,乃至SpriteSheet类,即使有需求,更加合理的方式也应是放在扩展库中实现,而不需要放在核心库中。另外这一改动也减少了实例嵌套,之前是实例化一个Texture类,它内部再持有一个HTML原生的Image对象实例。现在BitmapData接口,直接能够接收HTML原生的Image对象实例。这样的改动直接受益的是底层渲染器的封装,Canvas的drawImage()方法接收的参数是HTML原生的Image对象,而原先的版本因为传入的是Texture类,额外嵌套了一层,导致整个渲染器也对应要额外封装一层。现在重新定义位图数据接口后,整个封装层都可以被移除。渲染器也直接调用Canvas的绘图上下文。结果当然还是提升性能且减少包体大小。