Flutter 热更新无侵入方案(生成注册表)
这已经是这个系列的第三篇文章了,虽然研究的进展很慢,但是依然在推进中。这个团队目前还是只有我一个人,虽然也有几个愿意,但是添加我之后就没有下文了,我依然是纹丝不动有空就研究。
我还是希望这个团队能壮大,毕竟一个人研究的思路优先,技术受限,可能一个功能需要试错很多次才能做好。
修复配置系统
自从第二篇可以生成运行时库之后,但是生成的库并不是全部都可以直接可以用的。有一些库还是有一些错误存在需要修复,这也可能因为分析生成没有最好。
从目前生成的情况来看,大部分生成的代码都是可以用的,但是有个别复杂的库会比较麻烦,比如官方 flutter 库生成时候,出错的代码比较多。
修复配置 1.0 版本
为了可以修复生成的运行时库,我最开始的时候特意做了对于修复的配置。
class FixRuntimeConfiguration with FixSelectItem {
@override
late String name;
late String version;
List<FixConfig> fixs = [];
}
class FixConfig with FixSelectItem {
late String path;
List<FixClassConfig> classs = [];
List<FixExtensionConfig> extensions = [];
List<FixImportConfig> imports = [];
List<FixMethodConfig> methods = [];
@override
String get name => path;
}
class FixClassConfig with FixSelectItem {
@override
late String name;
bool isEnable = true;
List<FixMethodConfig> methods = [];
List<FixMethodConfig> constructors = [];
}
...
上面的代码我只贴出了一部分,主要对于生成所需要的属性可以配置 JSON,在生成的时候可以读取本地修复的 JSON 进行替换或者其他逻辑,比如屏蔽生成来做到。
后面为了方便使用者最简单的进行配置,包括保证字段不会写错,特意做了界面操作,也不错是对于目前能想到的修复方案进行实现,界面实现也有很大工作量。
这个方案在我提出做分析缓存功能时候就给废弃掉了,因为每一次生成都需要对于代码进行分析,对于一个工程所有依赖进行分析来说,真的是一个漫长等待的过程,特别是对于源代码级别的分析。
本来做分析缓存和修复没有强关联的,但是之后进行修复的时候都是从一个地方获取生成的所需要的参数。从其他地方获取需要修复的配置,在生成代码之前根据修复配置进行修改对应的参数值,这个感觉就像一个膏药一样,让整体的逻辑变得很复杂,并且维护起来十分的困难。
修复配置 2.0 版本
对于分析缓存,我准备把所需要生成的属性值在分析代码之后进行保存,之后在分析数据之前看缓存时候存在,如果存在就返回缓存数据,如果不存在就返回分析代码的结果。
既然这个分析的值可以通过 JSON 进行缓存下来,为什么修复的配置为什么不能合并在一起呢?
最开始设计是修复的配置作为分析缓存里面一部分,但是还需要额外的逻辑将值进行关联,后面索性就
将字段合并成一个了。
先进行分析生成分析缓存文件,之后通过修复配置界面对于分析缓存的值进行修改。后来发现一个比较严重的逻辑问题。
最开始的设置让字段都是 Get 获取的,这样导致通过修复配置界面修改的值无法保存到本地,只能在当前进行生成修复,下一次重新生成还需要配置。
为了解决这个问题,我将所有字段都可以被修改。但是之前配置的字段被新的代码分析的属性覆盖,虽然少但是这种情况还是存在的。
修复配置 3.0 版本
为了能解决上面的问题,为了能够将其他人修复配置可以被重复使用,不然每个人都需要自己修复一次,这样的热更新技术不用也罢。
为了可以做到方便的重复利用,也为了可以解决掉有一些修复无法通过配置进行修复,我决定将来废弃掉 2.0 的版本,将 3.0 版本的通过插件来做。
这个插件的实现也就是我之前所说的,通过 Dart Command Program 的技术来实现。毕竟通过动态运行代码,也只能目前通过这个技术来实现的。
对于这一套插件系统在去年我研究低代码的时候,已经实现过,进行移植并且改造我觉得会节省很大的工作量。
我通过插件会将分析缓存的配置发送给插件,插件通过自定义的写代码,将分析配置重新返回,然后拿着重新返回的分析缓存进行生成代码。
通过插件不同的版本,则可以解决不同版本修复配置不同的需求,这个感觉是目前算是比较好的方案。
分析缓存
分析缓存我创建了上述文件,分别包含了基础的缓存信息,比如当前的节点是否允许生成。
节点包括
-
file
- classs
- fields
- methods
- constructors
- extensions
- fields
- methods
- parameters
- topLevelVariables
- functions
- parameters
- enums
- fields
- methods
- mixins
- fields
- methods
- constructors
- imports
- namespace
以上的节点不能完全包括所有的代码,但是应该能覆盖 99%以上的代码生成,应该也是能够符合日常的热更新的需求的。
对于以上所做的分析缓存的逻辑对于分析生成库确实加速的不少,但是整体的速度还是比较慢。通过分析原因在于,每一次走缓存都从本地加载 JSON,这个如果能将分析缓存的 JSON 放在内存,先判断内存存在就直接返回内存就是不是能提升不少呢?。
下面是对于整个工程第一次分析生成的时间分析。
分析时间 分析缓存 内存缓存 16 分钟 x x 9 分钟 ✓ x 4 分 24 秒 ✓ ✓ 下面是对于整个工程第二次启动分析的时间分析
分析时间 分析缓存 内存缓存 13 分钟 30 秒 x x 4 分 42 秒 ✓ x 1 分 17 秒 ✓ ✓ 通过在内存里面保存变量缓存,通过占用大量内存换取速度的提升还是相当很有价值的,从上面的两组数据进行对比。
- classs
如何修复
对于具体的修复这个需要具体的具体对待
一个文件存在多个同名类
The name 'AnalysisContext' is defined in the libraries 'package:analyzer/dart/analysis/analysis_context.dart' and 'package:analyzer/src/generated/engine.dart'.
Try using 'as prefix' for one of the import directives, or hiding the name from all but one of the imports.
如果出现这种,则将当前文件之外的对应的类名全部进行 hide
// 例子
import 'package:analyzer/src/generated/engine.dart' hide AnalysisContext;
这个理论上可以通过便利其他文件公开的类,如果存在自动隐藏,这个放在后续尝试实现自动修复
默认 dynamic 无法满足泛型参数
Couldn't infer type parameter 'T'.
Tried to infer 'dynamic' for 'T' which doesn't work:
Type parameter 'T' is declared to extend 'Element' producing 'Element'.
The type 'dynamic' was inferred from:
Parameter 'element' declared as 'T'
but argument is 'dynamic'.
Consider passing explicit type argument(s) to the generic.
直接将类型添加对应的泛型类型
// 例子
args['element'] as Element
对于扩展的类型私有类型
The name '_NotInstantiatedExtension' isn't a type, so it can't be used as a type argument.
Try correcting the name to an existing type, or defining a type named '_NotInstantiatedExtension'.
这一种直接对于整个扩展隐藏不生成运行库代码即可
泛型类型包含泛型的
Couldn't infer type parameter 'K'.
Tried to infer 'Comparable<Object?>' for 'K' which doesn't work:
Type parameter 'K' is declared to extend 'Comparable<K>' producing 'Comparable<Comparable<Object?>>'.
Consider passing explicit type argument(s) to the generic.
对于这种的情况目前也只能将整个方法或者对应类屏蔽,后续看看有没有方法自己写对应解析器支持
扩展的类型包含泛型
The name 'T' isn't a type, so it can't be used as a type argument.
Try correcting the name to an existing type, or defining a type named 'T'.
将对应泛型转换基础的类型即可
// 例子
class $IterableNullableExtension$ extends FlutterRuntime<Iterable<Object?>>
没有默认初始化
Subclasses of 'Struct' and 'Union' are backed by native memory, and can't be instantiated by a generative constructor.
Try allocating it via allocation, or load from a 'Pointer'.
对于 Struct和 Union的子类我们隐藏初始化方法即可
默认值代码使用私有变量
const Border(
top: BorderSide(
color: _kDefaultTabBarBorderColor,
width: 0.0,
),
),
Undefined name '_kDefaultTabBarBorderColor'.
Try correcting the name to one that is defined, or defining the name.
找到私有变量的值直接替换成支持的
const Color _kDefaultTabBarBorderColor = CupertinoDynamicColor.withBrightness(
color: Color(0x4C000000),
darkColor: Color(0x29000000),
);
const Border(
top: BorderSide(
color: CupertinoDynamicColor.withBrightness(
color: Color(0x4C000000),
darkColor: Color(0x29000000),
),
width: 0.0,
),
)
私有默认值找不到替代修复方法
const _HeroTag _defaultHeroTag = _HeroTag(null);
这种的可以直接直接屏蔽默认值,后续看一下情况怎么支持。
Dart 一些私有库无法在 flutter 项目引用
Target of URI doesn't exist: 'dart:mirrors'.
Try creating the file referenced by the URI, or try using a URI for a file that does exist.
直接对于引入屏蔽删除即可
生成注册表
我准备将每隔文件类等注册在类注册表,将文件注册在库的入口注册表,再将库注册到整个大的注册表中。
文件注册表
对于图中FRb20caf4f53da7daf427485bec888e451类名怎么来的,是对于当前文件所在项目的绝对路径进行 Md5 加密所得,也是为了保证类型的一致性。
运行库入口注册表
运行库的注册表将所有文件的入口进行统一在一个文件进行注册。
全局注册表
这里我在当前项目目录添加了一个.runtime 的文件夹,之后在这个文件夹生成了一个flutter_tuntime_center的依赖库,将支持运行库通过这个库进行分发调用。
生成的依赖配置
入口注册表
对于怎么能具体的找到呢,这里我们在参数添加了 callPath 的参数
/// 调用运行时的路径配置
class FlutterRuntimeCallPath {
/// 运行时库名称
final String packageName;
/// 运行时库的调用具体路径
final String libraryPath;
/// 运行时库的调用类
final String className;
/// 运行时的实例对象
final dynamic runtime;
FlutterRuntimeCallPath(
this.packageName,
this.libraryPath,
this.className,
this.runtime,
);
}
这个参数怎么设置呢,可以在分析工程代码将对应值写入配置,通过配置生成这个路径。
截止到上面注册表的功能已经实现了,后续的步骤将实现插件的功能,通过插件功能来解决生成库报错的问题。
等插件功能实现,就可以尝试对于项目功能进行解析,看看和运行库对接运行有什么问题存在,毕竟按照我的推论,需要自己写解析器,这个解析起才是这个过程最难的地方。
最后,还是呼吁大家有兴趣的参与进来。