redhu's net blog home

知行合一

在线演示开发总结以及一些零碎的程序设计思考

一、架构

架构,核心是如何用最科学的方式把项目分解成独立的“层次”和“模块”,把一个复杂的系统抽象成简单可以理解的几个“模块”或者“层次”。

二、优化

以下记录了优化技术上总结的一些点,来自于这几个月踩过的坑:

文字绘制为什么会不清晰?第一版设计是把shape单独绘制到一个小canvas,然后再用贴图的方式贴到主canvas。这种方案会导致shape绘制模糊,以前的解决方案是采用像素对其的方式。后来由于canvas过多会导致在ios下莫名其妙的问题,这种方案起不到缓存shape绘制结果的作用。因此改为采用shape直接绘制到主canvas的方式,完全避免了这种问题。

尽量不要直接绘制已经在dom树中的canvas,因为这样每次绘制,都会导致canvas触发图形渲染动作。比较好的方案是采用两个canvas,一个负责后台绘制,绘制完后再贴到用于显示结果的canvas上(借鉴自安卓)。此种方案,可以较好地避免canvas绘制过程中的视觉闪动。除了canvas,我们在设计普通应用的时候也可以参考这种做法,如果遇到一些费时的中间过程,那我们就不要先改变界面,待中间过程执行完成后再一次性显示,避免闪动。

缓存canvas中间状态可以用:1、直接缓存canvas;2、转成imageData;3、转成图片。第一种在ios小程序下canvas过多会崩程序。第二种canvas->imageData代价比较大,占用空间也大。第三种如果保留一比一的方式比较耗性能,但是如果把canvas先绘制到一个比较小的canvas上,然后再执行toDataURL,则可以提升数十倍性能(取决于两个canvas的大小比例)。缩略图就是采用的第三种方案,所有slide绘制后,都会采用第三种方案缓存一张小图,用作缩略图。

图片、canvas、imageData等对象会占用较多的资源,我们缓存到内存时要注意控制,不要过度。例如我们在演示里引入了“池(pool)”的概念。一旦超过池子限制,便会采用最早没使用的策略清除一些对象。而在使用时,优先查看池子里有没有,没有则创建新的对象并放到池子里。

Dom操作:create canvas, append image, append canvas, append div这些dom操作在chrome浏览器都是非常快速的,普通的canvas的操作效率也很高。例如draw image to canvas(1M)只需要0.xms。但有几个操作要注意,特别是在canvas比较大的时候,getImageData, putImageData非常耗时,最耗性能的是toDataURL。以下是针对1000*1000的canvas操作性能数据: Get imageData 1000 times: 9326ms Put imageData 1000 times: 1231ms To DataUrl 1000 times: 18627ms Draw canvas 1000 times: 435ms Draw Image 1000 times: 1033ms 注意:采用console.time测试性能误差较大,较好的方式是一次执行多次,然后求平均值。

数据流向,建议严格遵循数据单向流动的原则。单向数据流的数据模型简单、能较好地保证数据一致性。对程序问题排查和后期扩展都是十分有利的。写程序时要避免为了一时方便,随意更改数据,要搞清楚这个数据属于哪个层(模块),哪个模块改比较合适,它有没有提供相应接口等。举个例子,插入一个新幻灯片的数据操作流程应该是:应用层操作内核接口->内核数据更改->内核视图更改->应用层接收到消息后更改UI->插入完成。

局部绘制:1、slide放大400%以后,layout的高宽会非常大。如果创建一个canvas和layout的高宽一致,光一个canvas就会申请几十M内存,如果高清屏会更大。除了占用资源,绘制性能也会大大降低。目前采用的方式是只申请可视区域大小的canvas,且会检测shape是否在这个区域内,如果shape不在此区域内,则不会绘制。 2、在编辑的时候,canvas会有不断重绘的操作。如果一个slide比较复杂,则会造成操作卡顿。以目前的数据,我们可以检测到shape级别,即只重新绘制被改变的shape区域(经讨论可以做到行排),在上编辑以后可以优化一下。

对于放大、滚动等重绘频繁、计算量大的操作,目前采用了异步+缓存的方式。当选中slide后,我们会缓存一张1:1的图片。当图片在连续放大的过程中,我们实际上是把这个1:1的图片不断贴到canvas上,而把实时绘制放到后台延迟处理,拖动也是一样。因此我们在操作放大和拖动的时候,会看到绘制图像有一个从模糊到清晰的过程。

异步操作:为了避免线程卡顿导致交互不流畅,我们在程序里采用了很多异步操作。比较推荐采用requestAnimationFrame方法。但是异步操作会导致程序的设计变得更加复杂,也会为程序稳定性埋下一些隐患。我们在使用异步方法的时候,一定要记得保留该异步方法的“句柄”!!!一旦该模块/对象被销毁的时候,一定要清除这些异步调用。举个例子,我在之前写event模块的时候,采用了事件异步fire。在清除事件的时候,没有清除异步队列里的任务。这个bug导致了后面很多问题。例如在缩略图滑动到最后一页的时候,后台还触发了第一页绘制完成的消息(对于调试来说非常古怪)。

在设计模块的时候,要遵照低耦合高内聚的规则。不仅是内核层,应用层也要尽量遵守。文件大小不要过大,尽量限制在一百行内。如果是一个大模块,通常用一个index文件描述主线,把其它功能放到子文件中去。Index主要提供api出口,和主要逻辑流程,总体来说index是要方便调用者阅读的(只有需要了解详细细节才用阅读其它子文件)。

关于多线程: (1)如果一个方法体要执行超过几个毫秒,可以考虑放到子线程中去执行。 (2)主线程到子线程通信的代价其实也还好,如果不是很频繁这种代价可以忽略。 (3)多线程编程会增加程序复杂度(异步设计)。 (4)在子线程里操作OffscreenCanvas还不是很成熟,只有部分高级浏览器支持,现阶段还不要把绘制动作迁移到canvas里去。 (5)子线程里最好只处理和上下文无关的计算,搬运上下文代价非常大。曾经改造了一版程序,打算在子线程中绘制。为了重新构造layout和shapeData依赖的上下文,花了很大的代价。 关于新技术的应用,一定要先考察新技术的兼容性,再决定是否应用(多线程绘制踩过一次坑,没考察好兼容性导致后面写了很多废代码,浪费了很多时间)。

2019年1月5日记录于武汉金山