在本文中,您将找到使用 Flash Professional 创建的应用程序的性能优化战略。 优化过程包括编辑 FLA 项目文件,确保所发布的应用程序已实现的(或实际的)帧速率足以能流畅地回放动画。
如果以前运行过 Flash 项目并且看到了经常停顿的动画,那么这就是您希望避免的行为。 如果希望复制一个有停顿的动画测试,可创建一个具有简单动画的项目并分配小于 10(如 5)的帧速率。 通过发布 SWF 文件来测试影片时,您会看到有停顿的动画示例。
有两个主要因素决定了 Flash 性能: CPU/GPU 的使用和内存的使用。 这些因素彼此之间没有依赖关系。 一些适用于改进一个方面的优化调整将对另一个方面产生负面影响。 在下面几节中,我将介绍改进工作的原理,提供为什么需要明智地决定增加内存的使用来降低 CPU/GPU 负载的原因。
如果为移动设备开发 Flash 游戏,您可能需要下面探讨的一些技术来实现可接受的帧速率。 如果为桌面创建非游戏应用程序,则只需稍微熟悉(不熟悉也可以)本文中介绍的技术就能实现可接受的帧速率。
判断和测量游戏性能
在理想世界中,Flash 测试环境允许您模拟目标平台,判断应用程序在该目标平台上的可能性能。 不幸的是,除非您的开发平台和目标平台是相同的,否则目前不可能准确评估项目在测试环境中的运行方式。
相反,您可在开发平台上测量应用程序的性能,然后通过在目标平台上观察应用程序的性能,定期确认所有功能在目标平台上运行良好。
实现优化技术
在下面几节中,我首先探讨内存管理指南,按字母顺序列出其中的次级主题。 接下来,我将通过相关的次级主题提供 CPU/GPU 管理相关信息。
似乎用两节内容来提供这些技术比较合理。 但是在阅读本文时,请记住内存管理会影响 CPU/GPU 的使用,所以可将内存管理一节中列出的建议与 CPU/GPU 一节中列出的技巧结合使用。
在提供可供您使用的具体最佳实践之前,我认为包含有关技术的信息会很有帮助,这样您可了解最容易或最难实现的目标是哪些。 我还将提供另一个列表,按照从最高收益到最低收益的顺序确定技术的优先级。
请记住,这些列表是主观性的。 排列顺序依赖于各位开发人员的经验和能力,以及测试条件和测试环境。
最容易到最难实现的优化技术
- 不使用滤镜。
- 始终使用反向的 for 循环。 避免编写 do 循环和 while 循环。
- 显式停止计时器,让它们准备好进行垃圾回收。
- 使用弱事件侦听器并在不需要时删除这些侦听器。
- 尽可能严格地键入变量。
- 在不需要鼠标交互性时显式禁用鼠标交互性。
- 尽可能将 dispatchEvents 替换为回调函数。
- 停止 Sounds,以支持对 Sounds 和 SoundChannels 执行垃圾回收。
- 使用每个元素所需的最基本 DisplayObject。
- 始终将 cacheAsBitmap 和 cacheAsBitmapMatrix 用于 AIR 应用程序(移动设备)。
- 尽可能重用对象。
- Event.ENTER_FRAME 循环: 使用不同的侦听器和不同的侦听器功能,这些侦听器和侦听器功能要适用于尽可能少的 DisplayObjects。
- 将对象加入池中,而不创建对象并对其执行垃圾回收(对象池)。
- 使用部分块传输(blitting)。
- 使用舞台块传输。
- 使用 Stage3D。
最大到最小的优化技术收益
- 使用舞台块传输(如果有足够的系统内存)。
- 使用 Stage3D。
- 使用部分块传输。
- 将 cacheAsBitmap 和 cacheAsBitmapMatrix 用于移动设备。
- 在不需要鼠标交互性时显式禁用鼠标交互性。
- 不使用滤镜。
- 使用所需要的最基本的 DisplayObject。
- 尽可能重用对象。
- Event.ENTER_FRAME 循环: 使用不同的侦听器和不同的侦听器功能,这些侦听器和侦听器功能要适用于尽可能少的 DisplayObjects。
- 使用反向 for 循环。 避免编写 do 循环和 while 循环。
- 将对象加入池中,而不创建对象并对其执行垃圾回收。
- 尽可能严格地键入变量。
- 使用弱事件侦听器和删除侦听器。
- 尽可能将 dispatchEvents 替换为回调函数。
- 显式停止计时器,让它们准备好进行垃圾回收。
- 停止 Sounds,以支持对 Sounds 和 SoundChannels 执行垃圾回收。
记住这些优先级,然后继续阅读下一节,了解如何更新 Flash 对象,从而更高效地管理内存。
管理内存
下面列出的建议并不详尽,但它包含可能对 Flash 内容的性能带来重大影响的战略。
使用回调函数与 dispatchEvent
在分派事件时内存使用量会增加,因为必须创建每个事件并向它分配内存。 此行为是合理的: 事件是对象,因此需要内存。
我测试了少量事件,发现每个事件使用 40 到 128 字节的内存。 我还发现使用回调函数比使用事件所占用的内存更少,运行效率更高。 (请参阅示例文件文件夹中的测试文件: callback_v_dispatchEvent
。)
应用滤镜
在应用动态滤镜时内存使用会增加。 按照 Adobe 帮助文档的说法,使用滤镜会让内存使用量翻倍。 在真实世界中,通过对 Flash Professional CS6 进行测试,我发现尽管滤镜确实会导致内存使用量增加,但不会使内存使用量翻倍。 (要查看测试示例,请查阅 filters 文件夹中的示例文件。)
为每个元素使用正确的显示对象类型
Shape、Sprite 和 MovieClip 对象会使用的内存量不同。 Shape 对象需要 236 字节,Sprite 需要 412 字节,而 Movieclip 需要 448 字节。
如果在项目中使用了数千个 DisplayObjects,并且不需要交互性,您或许能够通过使用 Shape 节省大量内存。 或者在不需要时间轴时使用 Sprite。
对象池
应用程序启动时,会创建您在应用程序打开的整个期间需要的所有对象引用,将这些引用填入(存储)在一个数组中。 任何时候需要一个对象,就会从该数组检索它。
任何时候不再需要一个对象,可将它返回到该数组。 一种常见的做法是用 Vector 代替数组来存储具有相同类型的对象。 使用一个 Vector 的速度可能是使用数组的两倍,但除非会执行成千上万个操作,否则您可能不会注意到区别,因为在操作数目不到一数千个时它们都很快。 (例如,请参阅 array_v_vector
文件夹中的示例文件。)
尽管使用对象池具有性能优势,但它的主要好处是让内存管理工作更容易。 如果内存利用率无限制地增长,对象池可预防该问题。 此技术通常可提高性能和减少内存的使用。
我在测试一个在每个帧上包含许多被垃圾回收和回收的对象的 SWF 文件时,发现使用对象池时,每秒帧数会增加 10%,内存使用量会减少一半。 (请参阅文件夹 pooling_v_gc
中的示例文件。)
重用对象
任何时候在一个循环中创建对象时,尽力将对象创建在循环外部并在循环内反复重用它。 并非所有对象总是能够这么做,但在许多情形下此技术很有帮助。
介绍块传输的一节提供了一个重用许多对象的示例。 可查阅该测试文件来了解如何完成此操作。
使用声音
相比内存使用而言,声音的问题相对较小。 播放声音时,它无法被垃圾回收(使用 Flash Professional CS6 测试该文件时)。 播放完声音或一个 SoundChannel 实例被用于对声音执行 stop()
时,就已经已准备好对该声音进行垃圾回收。 (要了解更多信息,请参阅文件夹 sound_test
中的示例测试文件。)
使用计时器
计时器的问题更加重要。 如果一个计时器未停止(因为其 currentCount
小于 repeatCount 或因为没有向它应用 stop()
方法),该计时器不会响应垃圾回收,即使您删除它的侦听器并清空所有引用也是如此。 一旦删除了侦听器,就不会再次调用计时器的侦听器功能,但计时器仍然会占用内存。
一个计时器仅使用 72 字节内存,所以这不可能在桌面/浏览器 Flash 游戏中成为一个明显的问题。 但是,如果在一个移动设备上反复打开、玩和关闭一个 Flash 游戏,而从不重新启动该游戏,您可能会看到明显的问题。
要查看该代码,可打开文件夹 gc_timer_test
中的文件。
弱侦听器与强侦听器
MT 类测试的另一个意外的结果是,无论您使用弱侦听器还是强侦听器,结果都没有区别。 它们在我对 Flash Professional CS6 的测试中都被视为弱侦听器。 (参阅 strong_v_weak_listeners
文件夹中的测试文件。)
管理 CPU / GPU 使用
目前,我知道直接测量此结果的唯一方式是使用一个操作系统工具。 Windows 提供 Windows 任务管理器(“性能”选项卡),Mac OS 则提供了活动监视器。 两个工具都允许您查看 CPU 使用率,但通常它们都对测试 Flash 性能的用处不大。
因此,您可能会通过检查应用程序的实际帧速率来间接测量 CPU/GPU 的使用情况。 MT 类支持您检查项目的帧速率,还提供了内存使用报告和内存跟踪。
使用 cacheAsBitmap 和 cacheAsBitmapMatrix
启用 DisplayObject 的 cacheAsBitmap 属性会显著改善性能(并增加内存的使用),前提是 DisplayObject 没有经历需要频繁更新位图的频繁更改。 在本质上这意味着验证 DisplayObject 是否未以不同于更改它在舞台上所处位置的任何方式来更改外观。 如果存在频繁的位图更新,性能将下降。
以多快的频率更新缓存的位图时仍能获得性能收益,这取决于多个因素。 毫无疑问,最重要的因素是更新位图的频率。
在任何情况下,都使用 MT 类测试您的具体项目,无论是否为需要位图更新的 DisplayObject 启用了 cacheAsBitmap。 (决定是否为不需要位图更新的 DisplayObject 使用 cacheAsBitmap 很简单: 使用它!)
如果有一个 DisplayObject(影片剪辑),并且希望启用它的 cacheAsBitmap 属性,可添加此行代码:
mc.cacheAsBitmap = true;
启用 cacheAsBitmap 始终有好处,甚至在向移动设备发布时更改 DisplayObject 的缩放、倾斜、alpha 和/或旋转(但不更改影片剪辑的帧)时也是如此。
具体来讲,为移动设备发布一个项目时,您可启用 cacheAsBitmap 并指定 DisplayObject 的 cacheAsBitmapMatrix 属性,实现显著的性能提升,如下所示:
mc.cacheAsBitmap = true;
mc.cacheAsBitmapMatrix = new Matrix();
您无需使用默认的身份矩阵。 但是,您将会发现使用除默认矩阵以外的内容的理由很少。
舞台块传输
舞台块传输是一个描述位块传输的词汇,它涉及到使用位图来渲染最终的显示效果。 无需将 DisplayObject 添加到显示列表中,像素会绘制到一个由舞台调整的、已添加到舞台上的位图中。 为了表现动画效果,会在一个循环中更新位图的像素。 通常将一个使用 BitmapData 类的 copyPixel()
方法的 Event.ENTER_FRAME 循环,会应用于使用其他 bitmapData 对象在动画循环外部创建的、舞台调整位图的 bitmapData 属性。
此技术比直接将对象添加到显示列表中更复杂,但它的效率要高得多 – 这常常是Flash 应用程序出现不可接受的帧速率与出色的帧速率之间的区别。 毫无疑问,除非您需要更高的帧速率,否则完全没有理由使用此战略。
我对比了一个 SWF 文件,它使用影片剪辑在整个舞台上移动和旋转 10,000 个方块(参阅名为blit_test/blit_test_mc.fla
的示例文件)。 然后我使用一些基本的优化技术(参阅名为blit_test/blit_test_basic_optimizations.fla
的示例文件)和舞台块传输(参阅blit_test/blit_test2
)更新了同一个 SWF 文件。
第一个 SWF 文件的帧速率大约为 15 fps,这是无法接受的。 但是,在采用更难以制定的技术(如块传输)之前,可轻松应用并能改善性能的基本调节方法少之又少。
首先,我反转 for 循环以获得细微的性能提升(参阅下面介绍的循环一节),而更重要的是,我使用了一些常量来代替反复重新计算相同的值。 这些更改带来了显著的(大约 40%)速度提升,达到了几乎可接受的帧速率(约 21fps)。
使用舞台块传输功能来编码相同的显示内容,得到了 54 fps 的帧速率,帧速率的提升达到了令人惊叹的 350% 以上。
但是我在前面说过,块传输过程要更加复杂。 具体步骤包括:
- 初始化舞台显示位图资产(Bitmap 实例、BitmapData 实例和 Rectangle 实例),在每个 Event.ENTER_FRAME 事件循环期间所有显示的像素都会复制到该资产上。
- 向一个数据数组中填入所有用于更新显示内容的数据。 (这一步并非总是必须的。)
- 填充 BitmapData 对象数组。 如果在一个影片剪辑的时间轴上有一个动画,那么这是您存储每帧 BitmapData 对象的地方(例如通过使用一个 sprite 表)。 在示例测试文件中,我使用 ActionScript 为矩形可旋转的每个角创建了一个 BitmapData 实例。
- 创建一个 Event.ENTER_FRAME 事件循环。
- 更新 Event.ENTER_FRAME 循环中的数据,将合适的像素从第 3 步中创建的数组复制到在第 1 步中创建的 BitmapData 实例的适当位置(使用来自第 2 步的数据数组进行确定)。
有关更多细节,请参阅 blit_test/blit_test2
中的文件。 它包含大量注释。
舞台块传输的不足(除了编码困难)是,在创建所需要的位图时可能使用大量内存。 在为 iPad 等具有高分辨率(第 1、2 代 iPad 的分辨率为 1024 x 768,第 3 代 iPad 的分辨率为 2048 x 1536)和相对较低内存 (RAM) 容量(第 1、2 和 3 代的内存分别为 256MB、512MB 和 1 GB)的设备创建应用程序时,这是一个很重要的因素。
一般而言,您的游戏应使用的内存不超过可用 RAM 的一半。 这不仅包括位图,还包括您游戏中使用 RAM 的其他所有要素。
部分块传输
顾名思义,部分块传输结合了 Flash 显示列表的使用和将像素复制到 BitmapData 对象。 通常,舞台上显示的每个对象是一个位图,它被添加到显示列表中并像平常一样使用显示对象(如影片剪辑)进行操作。 会将每个对象的动画会块传输到一个 BitmapData 对象数组中。
例如,使用前面在整个舞台上旋转和移动方块的示例,我对这些方块和它们的各种旋转执行块传输,将这些 BitmapData 对象存储在一个数组中,将位图添加到显示列表中,然后像任何显示对象(如上面介绍的影片剪辑)一样在 Event.ENTER_FRAME 循环中操作这些位图。 然后最终我将位图的 bitmapData 属性分配给合适的数组元素。 (要查看具体的工作原理,请参阅 blit_test/partial_blitting_test.fla
文件。)
在我的 PC 上进行测试时,部分块传输测试并没有舞台块传输那么快 (24-26 fps)。 但请不要抱有成见,因为部分块传输可能在其他条件下比舞台块传输更快。 此外,部分块传输比舞台块传输更容易编码,所以如果可以通过部分块传输实现可接受的帧速率,就无需舞台块传输所需的额外工作了。
使用 Event.ENTER_FRAME 循环
创建多个 Event.ENTER_FRAME 侦听器并将其来应用于一个调用多个侦听器函数的实例,这比创建一个 Event.ENTER_FRAME 侦听器来调用一个侦听器函数(后者然后调用其他函数)稍微高效一点。
但是,与单个具有一个 Event.ENTER_FRAME 侦听器的对象相比,拥有多个对象且每个对象拥有自己的 Event.ENTER_FRAME 侦听器是一种不同的情形。 与分别拥有自己的 Event.ENTER_FRAME 侦听器的多个对象相比,使用单个具有一个 Event.ENTER_FRAME 侦听器的对象时性能大约要高一倍。 (要查看测试,请查阅enterframe_test_one_v_many_loops_with_different_movieclips
文件夹中的文件。)
理解 for 循环、while 循环和 do 循环之间的区别
在 Flash 中,反向 for 循环是执行速度最快的循环。 如果循环中需要一个已存储的相同类型对象的列表,使用 Vector 引用该对象列表的反向 for 循环是最快的方式。
如果使用 int 作为迭代参数,而不使用 uint,所有 3 个循环的执行速度都更快。 如果递减循环变量,而不是递增,所有 3 个循环的执行速度都更快。(注意: 如果递减循环变量 i 并使用 i>=0 作为终止条件,并且如果 i 是一个 uint,您将触发一个无限循环。)
如果使用一个变量或常量作为终止条件,而不是用表达式或对象属性,所有 3 个循环的执行速度都更快。 因为初始条件仅需要计算一次(而不是在每次循环迭代中都计算),所以在任何这些循环中为初始条件使用表达式还是对象属性并没有显著的区别。
任何可在不影响结果的前提下移动到循环外部的内容都应该移动。 这包括在循环外部声明对象(参阅有关重用对象的章节),其中在循环内对新构造函数的使用有时可移动到循环外,并且如果终止循环条件是一个表达式,应该在循环外计算它。
我听人说,如果使用的每个对象引用下一个对象,则比使用一个数组来引用对象更快。 在我的测试中,我发现此说法是错误的。
使用数组不但更容易,还能更快地完成初始化并使用它。 当然,使用 Vector 代替数组会更快。 (参阅for_loop_v_sequential_loop
文件夹中的示例测试文件。)
所有这些建议都不可能在大部分情况下具有显著成效。 但是,如果希望竭尽全力提升编码的效率,或者如果项目涉及到迭代大量循环,还是值得实现这些调节的。
禁用鼠标交互性
影片剪辑和 sprite 可与鼠标交互。 甚至在您没有为任何鼠标交互性编写代码时,Flash Player 也会在存在这些对象时检查鼠标交互性。 您可通过为不需要鼠标交互性的对象禁用鼠标交互性,来节省一些 CPU 周期。
在舞台上移动鼠标时注意到性能问题(或者计算机的风扇转速增加时)时,此战略非常有帮助。 禁用鼠标交互性会改善性能,可使您的计算机风扇变得更安静。
在测试期间,我看到在禁用一个测试文件中的所有影片剪辑时,帧速率会增长大约 2.5 倍。 示例测试代码位于mouse_interactivity
文件夹中。
删除事件侦听器
即使更高的 Flash Player 版本会在对象被垃圾回收时删除侦听器,并且拥有强侦听器也不会推迟垃圾回收,您仍然应该尽快地显式删除所有事件侦听器。 删除侦听器的速度越快,侦听器使用的 CPU 周期就越少。 此外,您可能不知道用户安装了哪个 Flash Player 版本。 更老的 Flash Player 版本可能不会对对象执行垃圾回收 – 即使是具有弱侦听器的对象。 不要依靠更新的 Flash Player 功能来优化糟糕的编码