阅读 2817

小程序 LRU 存储设计

写在前面

为了解决小程序生成分享到朋友圈图片的问题,我们开启了画家计划--- 一个小程序图片生成库。该计划已开源,可移步:github.com/Kujiale-Mob… 。 大家知道 Canvas 的绘制有很多很蛋疼的坑,其中一个是它的 drawImage 方法,该方法在 IDE 中可以直接设置为网络图片的 url 进行绘制,但在真机上无法这样做。有这个坑后,我们就需要先把图片通过 download 下载到本地后,才能进行绘制。所以你的小程序中,如果有频繁绘制一些图片的需求,而又需要用到网络图片素材,这就导致每次绘制都要重新下载素材图片,产生很大的绘制性能问题。

小程序本身是未提供文件 LRU 之类的缓存机制的。为了让我们的画家计划图片生成的更快,我们自己开发了的小程序文件进行 LRU 存储的相关代码。这样我们就无需重复下载可能会频繁使用到的绘图素材,大大增加了绘图速度。

介绍下小程序的缓存系统

小程序的缓存分为数据缓存,和文件缓存两部分。而文件缓存又分为临时文件缓存,和本地文件存储。其中本地文件存储的大小限制为 10M。

数据缓存

我们可以使用小程序提供的一套异步和同步的方法来增删查结构化数据。同一个微信用户,同一个小程序的存储上限为 10MB。如果空间不足时会在小程序级别进行 LRU,也就是不经常使用的小程序的数据缓存区域会被全部清空。

详细见微信官方文档:

developers.weixin.qq.com/miniprogram…

注:数据缓存区在体验版、开发版、和线上版都共用一套,并不会隔离。

文件临时缓存

我们在调用 wx.downloadFile 或者 wx.chooseImage 等获取文件或图片的方式成功后,我们会得到这个文件或图片的临时存储路径。文档上写的是临时路径的生命周期是在本次小程序启动期间内

不过没有对存储大小的限制进行说明,所以理论上不管多大文件都可以进行临时缓存,当然如果太大肯定会造成某些神奇的错误吧。

本地存储

我们在获得临时文件后,可以通过调用接口 wx.saveFile 把临时文件存储到本地空间中,本地空间存储限制为 10M。如果存储满了后,后面的文件就无法存储成功了,会报超出最大存储上限的错误。

而我们现在需要做的就是在这个本地存储空间上,开辟一个空间,作为我们下载文件的存储空间,因空间有限,所以我们需要对这块空间进行 LRU 管理。

有关本地存储相关的接口可看以下文档:

developers.weixin.qq.com/miniprogram…

注:把临时文件通过调用 saveFile 成功后,这个临时文件路径就无效了。切记切记。

文件 LRU 存储实现

小程序端的本地存储有 10M 限制,但却无 LRU,现在我们需要结合上面提到的小程序三种存储方式来实现一套小程序文件下载的 LRU 机制。

数据结构设计

{
'key': {
        'path': // 文件的存储路径
        'time': // 时间戳,用来记录文件的最后访问时间,当存储不够时,会选择最远未被访问的文件进行删除
        'size': // 文件大小
       }
....
'totalSize': // 所有存储文件的当前总大小
}
复制代码

其中我们用下载的 url 作为 key。 以上数据结构会存在在数据缓存区(后续我们会把这个区域称为 storage 区),并且在下载器构建之时会从 storage 中读取到内存中。以后的文件操作,也会实时同步到 storage 中记录的文件信息。

你可以理解为,storage 中存储了文件的基本信息,而 path 就相当于指向这个实际文件的指针。

总体流程设计

容错

因为 storage 的存储,和文件操作都是异步的,所以有可能存在两者不一致的情况。此处的不一致情况分两种

第一种,storage 的某文件信息被删除了,但文件本身却因为出现神奇错误而未被删除。另外文件添加成功了,但 storage 中却未添加成功也属于此情况。

第二种,storage 中文件信息删除失败,但是文件却被删除了。

以上两种性质不同,所以也需要区别对待。针对第一种会导致文件的存储空间和 storage 中记录的文件信息不一致,也即出现了游离的文件(未被 storage 跟踪)。

而第二种,相当于存在了空指针,此种情况是绝对需要避免的,因为这会导致你在拿出一个不存在的文件使用。会直接导致严重bug。

针对以上两种特殊情况,做了以下容错的处理。首先我们要保证文件的删除操作一定要在 storage 成功之后进行。这样保证了第二种不会出错。

而针对第一种游离文件的情况。我们这边会在 saveFile 的时机进行兜底处理。如果存在了游离文件,最终会导致我们空间总大小计算不一致,这可能最终会导致,我们外部逻辑认为可以存储,但实际存储空间已经满了,这样就会导致 saveFile 报错,在 saveFile 出错后,不管啥原因,我们都把涉及到本策略存储相关的内容全部清空掉,重新来过。因为我们一直有 tempFilePath 兜底,所以即使这种情况出现,也不会影响用户正常使用。只是会影响一点用户体验(毕竟一下子没有以前的缓存了)。

注:之所以不像保证第二种情况的方式来保证第一种情况,是因为我觉得不需要为处理极少会出现的错误场景而去浪费性能,影响用户体验。只要我们做好兜底,即使这种错误情况万一真的出现,整个系统也不会因此出问题,还是会正常使用。

写在后面

小程序有很多的坑。目前市面上很多小程序性能体验并不是很好。所以为了做一款高性能的小程序,是需要我们花大量的时间去试错,琢磨的。踩坑不止,生命不惜。

目前这一套 LRU 的文件存储机制已经在我们开源的 Painter 库中使用,如有兴趣请移步: github.com/Kujiale-Mob…