Flutter 动态化热更新的思考与实践(九)---- 二级缓存设计

2,142 阅读6分钟

本篇文章里,和大家简单交流一下Flutter动态化中二级缓存的设计思路。

我们在之前的文章Flutter 动态化热更新的思考与实践(六)---- 动态列表滚动优化 中给出了整个动态渲染的流程图,其中就有本地cache的部分,只是当时没有讲其中的细节。本地cache就是一个二级缓存的设计,包含了两部分缓存:内存和数据库 ,只不过两部分缓存的数据不太一样,这个是和通常的二级缓存概念有点出入的地方,接下来会详细说明。

缓存的流程

double_cache

以上流程图展示了二级缓存的整体流程,下面着重介绍下两个技术细节:

  1. 缓存的数据结构
  2. 内存缓存策略

1. 缓存数据结构

我们定义的缓存数据结构如下:

class AstCache {
	//ast 数据唯一id
	String astId;
	//ast 内容的Hash值
	String hash;
	//ast 数据
	Map ast;
}

逐一说明下:

  1. astId: ast id 的生成算法是对代码文件在项目中的相对路径与注解1声明的类名或全局方法的函数名做md5转换:astId = md5({代码文件相对路径}+{类名或全局方法的函数名}) ,这样得到的ast id 基本上是项目范围内的唯一值。如果代码文件转移了目录位置或更改了类名或全局方法名,对应的ast id 都会有变化。
  2. hash:对注解1声明的代码中内容做的md5 hash ,如果代码有了修改变化,那么生成的hash也一定会变,根据此字段来判断ast 数据版本的一致性。
  3. ast:Ast 具体的数据,由于都是json数据结构,所以直接可以转换成Map来使用。

2. 内存缓存策略

前面提到内存中缓存的数据和数据库的数据是不一样的,内存中缓存的是Widget对象,数据库中缓存的是Ast信息,其中的原因是进入动态化页面中最耗时的部分是Runtime动态解析Ast的过程,在一些稍微低端的老旧设备上,这一时长竟可以将近3s,所以如果只是把Ast数据缓存到内存中是没有多大意义的,我们使用内存缓存的目的是为了下一次更快的加载,所以只能是将第一次构建好的Widget缓存到内存中,再一次进入页面的时候,就可以将缓存的Widget直接渲染出来,减少等待的时间。

我们的内存缓存是以队列来存储的,这个队列会有如下规则:

  • 队尾存放最近一次访问的元素;
  • 队首存放容器大小范围内最久一次访问的元素;

在此规则之上,我们对内存缓存的操作就会做如下处理:

  1. 如果当前访问的Widget已经在缓存队列中,则取出Widget对象的同时,将该Widget移至队尾;
  2. 如果当前访问的Widget没有在缓存队列中,那么从数据库缓存或服务器中获取Ast构建Widget对象,并将该对象添加至内存缓存队列队尾;
  3. 如果内存缓存队列已满,则移除队首元素,并将新的Widget对象添加至队尾;

memory_cache

这样设计的目的是为了保证高频次访问的页面能始终在内存缓存中,对用户的使用体验会是比较友好的。

3. 二级缓存带来的内存占用

我直接拿上一篇文章中的“消息列表”页面来看一下增加了二级缓存后内存的占用情况(运行模式为Profile)。

未缓存Widget 之前内存情况:

A82AC6EC-D56E-44CC-82AB-2355E409A0EB

缓存Widget之后内存情况:

image-20210210143236110

通过上面DevTools对比可以看到,未缓存之前的内存占用70M左右,缓存后内存占用86M左右,多了16M左右的占用空间,并且这还只是一个页面Widget的缓存占用的空间,还是不太理想的,我们目前定义内存缓存容器大小为10个页面,这样的话如果都占满会是多出来200M左右的内存占用,还是挺多的,好在现在手机的运存也比较大,动辄8G、12G的内存。。。不过对程序员来说代码性能的极致是永恒的追求(^ ^),后面如果找到优化的方法还会和大家分享(^ ^)。

写在最后

本篇文章应该是《Flutter动态化热更新的思考与实践》系列最后一篇了,至此我们实现整个Flutter动态化的技术要点都已经分享给了大家,回过头来再看整个方案的话,我个人觉得不完美的地方还是性能方面,虽然我们做了一些工作来缩小用户体验上与原生的差距,但是这个方案的天然技术瓶颈很难突破:Runtime是在单线程中执行,和UI是在一个线程中,如果遇到复杂的业务处理,可能会影响交互的体验,现在就有一个小问题,就是CircularProgressIndicator组件的动画不是很顺畅,在Loading完成将要渲染的最后阶段,这个圈圈动画基本上会卡住。单线程的限制也是无可奈何之举,因为在Flutter的设计里,是不允许多线程共享内存的,所以多线程间的数据传输只能是基本数据类型,而针对Widget这样的对象是无法传输的,这样的话如果在多线程中执行Runtime,那么解析出来的Widget对象就无法传给UI线程渲染。后面我们也会持续思考这个问题,尝试设计一些架构模式来支持Runtime多线程执行,有结果的话会回来再和大家分享~

明天就是除夕了,最后的最后给大家拜个早年:祝大家春节快乐,新一年牛气冲天🐂! (^_^)v

再来一个最后,发个我司的招聘信息,感兴趣的小伙伴让你们的简历飞过来吧~

图片

图片

图片

Footnotes

  1. 主要指需要对代码动态化处理的注解声明,如我们定义的有:@FHClass@FHFunction@FHConstant 等,分别用于动态化classglobal functionglobal constant等内容 2