Flutter 热更新无侵入方案(生成注册表)

635 阅读10分钟

Flutter 热更新无侵入方案(生成注册表)

第一篇 Flutter 热更新无侵入方案(探讨)

第二篇 Flutter 热更新无侵入方案(生成运行时库)

第三篇 Flutter 热更新无侵入方案(生成注册表)

第四篇 Flutter 热更新无侵入方案(插件中心)

flutter_runtime_ide 运行时生成IDE

这已经是这个系列的第三篇文章了,虽然研究的进展很慢,但是依然在推进中。这个团队目前还是只有我一个人,虽然也有几个愿意,但是添加我之后就没有下文了,我依然是纹丝不动有空就研究。

我还是希望这个团队能壮大,毕竟一个人研究的思路优先,技术受限,可能一个功能需要试错很多次才能做好。

修复配置系统

自从第二篇可以生成运行时库之后,但是生成的库并不是全部都可以直接可以用的。有一些库还是有一些错误存在需要修复,这也可能因为分析生成没有最好。

从目前生成的情况来看,大部分生成的代码都是可以用的,但是有个别复杂的库会比较麻烦,比如官方 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 的技术来实现。毕竟通过动态运行代码,也只能目前通过这个技术来实现的。

对于这一套插件系统在去年我研究低代码的时候,已经实现过,进行移植并且改造我觉得会节省很大的工作量。

我通过插件会将分析缓存的配置发送给插件,插件通过自定义的写代码,将分析配置重新返回,然后拿着重新返回的分析缓存进行生成代码。

通过插件不同的版本,则可以解决不同版本修复配置不同的需求,这个感觉是目前算是比较好的方案。

分析缓存

image-20230720101327549

分析缓存我创建了上述文件,分别包含了基础的缓存信息,比如当前的节点是否允许生成。

节点包括

  • 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 分钟xx
    9 分钟x
    4 分 24 秒

    下面是对于整个工程第二次启动分析的时间分析

    分析时间分析缓存内存缓存
    13 分钟 30 秒xx
    4 分 42 秒x
    1 分 17 秒

    通过在内存里面保存变量缓存,通过占用大量内存换取速度的提升还是相当很有价值的,从上面的两组数据进行对比。

如何修复

对于具体的修复这个需要具体的具体对待

一个文件存在多个同名类

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'.

对于 StructUnion的子类我们隐藏初始化方法即可

默认值代码使用私有变量

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.

直接对于引入屏蔽删除即可

生成注册表

我准备将每隔文件类等注册在类注册表,将文件注册在库的入口注册表,再将库注册到整个大的注册表中。

文件注册表

image-20230720140448019

对于图中FRb20caf4f53da7daf427485bec888e451类名怎么来的,是对于当前文件所在项目的绝对路径进行 Md5 加密所得,也是为了保证类型的一致性。

运行库入口注册表

image-20230720140616886

运行库的注册表将所有文件的入口进行统一在一个文件进行注册。

全局注册表

这里我在当前项目目录添加了一个.runtime 的文件夹,之后在这个文件夹生成了一个flutter_tuntime_center的依赖库,将支持运行库通过这个库进行分发调用。

生成的依赖配置

image-20230720140812481

入口注册表

image-20230720140839199

对于怎么能具体的找到呢,这里我们在参数添加了 callPath 的参数

/// 调用运行时的路径配置
class FlutterRuntimeCallPath {
  /// 运行时库名称
  final String packageName;

  /// 运行时库的调用具体路径
  final String libraryPath;

  /// 运行时库的调用类
  final String className;

  /// 运行时的实例对象
  final dynamic runtime;

  FlutterRuntimeCallPath(
    this.packageName,
    this.libraryPath,
    this.className,
    this.runtime,
  );
}

这个参数怎么设置呢,可以在分析工程代码将对应值写入配置,通过配置生成这个路径。

截止到上面注册表的功能已经实现了,后续的步骤将实现插件的功能,通过插件功能来解决生成库报错的问题。

等插件功能实现,就可以尝试对于项目功能进行解析,看看和运行库对接运行有什么问题存在,毕竟按照我的推论,需要自己写解析器,这个解析起才是这个过程最难的地方。

最后,还是呼吁大家有兴趣的参与进来。