Dart Json 序列化方案探索-睡前开发笔记(一)

506 阅读9分钟

之前的文章: <<Dart Json序列化方案探索-插件展示>>已经展示过json_object插件的基础功能了, 我们现在分享json_object的开发笔记. 这是笔记的第一天, 今天我们要做的事情有:

  1. 创建JsonObject类
  2. 构思JsonObject的核心功能
  3. 找到实现上述的核心功能的具体思路

创建JsonObject类

class JsonObject {
	/// Code...
}

构思JsonObject的核心功能

  1. 我需要转换json string为JsonObject, 大概是这样:
var jsonObject = JsonObject.fromString(jsonString);
  1. 我需要像类对象使用类的成员那样访问JsonObject内的数据成员. 也就是说我需要能这样写:
var value = jsonObject.key;
  1. 当我需要修改JsonObject成员的值, 或者添加某一个新的键值对(key, value), 我需要能够像这样写:
/// 假设jsonObject里原来是这样的
/// {"name": "李四"}. 现在我们这样写:

/// 修改name的值为"张三"
jsonObject.name = "张三";
/// jsonObject里事先没有key为detail的数据, 这里添加一个Map进来
jsonObject.detail = {
    "age": 21,
    "height: 200,
};

差不多实现上述3个需求, 今天的笔记就算是写完了.

找到实现上述的核心功能的具体思路

之前在<<Dart Json序列化方案探索-插件展示>>Github的仓库里已经提到, 本插件的灵感来源于dartwatch-JsonObject. 那么要实现上述三条构思, 我们得先有一丢丢的理论基础, 请花点时间了解如下内容:

Dart的dynamic关键字

先说结论: dynamic类型使得编译时编译期不会揣测dynamic变量的类型, 而在程序运行时才会去确认.

这句话没看懂的, 请看接下来的解释. 秒懂的兄弟们可以跳过这一部分去看最后一个三级标题(JsonObject实现的具体思路)了.


咱争取把dynamic弄得透彻点, 弄清dynamic能大大方便后续的开发工作. dynamic是一个关键字, 中文翻译为"动态的", 顾名思义, 它是一个动态类型. 这个动态类型和我们熟知的Object, var有什么区别呢?

我们先看看通常情况下声明一个变量的写法:

Object

上图我们可以看到, 我们声明了一个int类型的c和String类型的d, 并且分别打印了c和d的变量类型, 确认分别是int和String无误. 这就是说, 当我们在赋值, 或者说赋值以前就给定变量类型时, 那么这个变量的类型就能为以确定了: int c; c = 1;int c = 1;不论是哪种, c的类型都能唯一确定为int类型, 此时若是使c = d, 那必是会报错的.

而当变量类型为Object, 又是另一个稍许特别的故事了. 我们从Object源码中可以知道, Dart的所有类均继承于Object, 所有类都最终继承于Object. 由面向对象继承的特性可以知道, 对于父类声明的变量, 是可以用子类去赋值的, 也就是图中a和b的代码. 但是我们也知道, 一旦声明的变量是父类的类型, 也就是图中Object的类型, 我们能访问的也就只有Object里提供的成员和方法了, 这意味着虽然我们从编译器的运行结果看出b的类型是String, 我们却无法调用String类型的方法或成员, 比如说我们想使用String.contains(...), 会发生下面这样的事:

Object

这刚好证明了上面的说法:

  1. 当使用常规的模式声明变量的时候, 变量的类型会唯一确定.
  2. 由继承关系可知, 子类的值可以赋值给父类的变量, 但由于该变量类型唯一确定, 我们只能使用声明的变量类型的方法和成员(此例为Object b), 而无法使用子类的方法或成员(此例为String b).
  3. 但这样也能在某些情况下带来便捷性, 比如我们需要a = b, 这种赋值是完全成立的, 因为两者都是Object类型.

相比之下, 我们再看看关键字var是怎么回事呢?

var

这里我们定义了var a = 1, 再次打印a的类型, 发现是int. 这中间我们可以发现, var声明的变量, 在我们编译的时候(或者说在我们IDE内编码的时候), 会遵循它第一次被赋值时的变量类型. 在上图中, 我们使var a = 1后, 由于数字1本身是int类型, 所以a被赋值为1以后, 编译器成功确认a的类型为int.

这个时候会有聪明的小伙伴问了: 要是我让a再被赋值一次, 即a = "Hello world"可不可以呢? 答案是不可以:

var

我们可以看到不仅IDE里报错了, 程序编译的时候也报了错, 错误的意思是: 我们不可以把一个String类型的变量赋给一个int类型的变量. 这也证明了上面的说法: var声明的变量在首次赋值后, 编译器对其类型会直接确认, 后面再想赋其他类型的值就会报错了.


从上文var和Object的解读, 我们可以总结出以下几件事:

  1. 所有的常规类型声明, 我们可以发现一旦类型确认, 其类的实现将会完全被曝光.
  2. 对于确认的类型, dart的编译器会自动对该变量进行类型审查, 这意味着我们仅能使用该类型所有的成员和方法, 如果我们需要访问一个不存在的方法或成员, 是不可能的事情.
  3. 当我们需要访问一个运行时动态获取的数据, 比如json string, 使用上述的变量和方法的声明方式, 我们无法使用类的访问方式来访问到json内的数据. 我们仅能退而求其次, 通过json.decode(jsonString)转换的Map和List来访问json内的数据, 但这样的做法需要考虑的因素太多, 在复杂场景下十分不便. 为了使得json string内的数据能被最大程度的掌握, 我们需要手动或使用自动化工具实现json string对应的类, 以及这个类下的一系列数据结构规范. 这样做无疑也是十分繁琐的, 我们需要一个手动或自动管理的model层来专门储存这些model类, 这些类内的代码有些必须相当的复杂, 而有些又不需要十分复杂, 这样的多尺度下, 管理整个model层会显得些许吃力.

在这样的情况下, 我们不妨尝试一下dynamic呢?

dynamic

上图中我们声明了一个dynamic类型的变量a, 并赋值一个String类型'Hello world!'. 这个时候我们打印a的变量类型是String. 接下来的操作是访问String.contains方法, 也都很正常. 看上去dynamic好像没有什么不同的样子. 那这样如何呢?

dynamic

我们调用一个String里没有的方法, 叫做yesWeCan. 一运行, 报错了, 说是String类型里没有这个方法. 站在理性的角度, 我们也知道本身就没有yesWeCan这个方法, 可是IDE在检查代码的时候却没有报错. 从这个现象我们可以知道:

  1. dynamic和var在变量类型的确定机制上是大致相同的.
  2. dynamic和var的不同之处在于, 程序在编译期不会对dynamic的变量进行类型检查, 而对var声明的变量会进行类型检查.
  3. 由于dynamic的变量不会在编译期被检查, 所以它的内部相当于是一个黑盒 --IDE和编译器永远无法知道这个变量里有什么东西, 所以该变量一切符合Dart语法的写法全部都在编译期被判断为"正确", 编译成功.
  4. 当dynamic声明的变量在运行时出现差错, 会抛出noSuchMethod异常, 分析该运行时出现的问题细节.

等等...分析该运行时出现的问题细节? 运行时怎么知道的? Flutter里Dart的代码以AOT的方式打包, 在运行时如何能进行类似反射的操作以拿到运行出错的对象的内部细节呢? 之前我们知道, Dart里所有的类全部继承于Object, dynamic也不能例外, 它不过是经过了特殊处理的一个子类, 编译器不会在编译期对其类型安全斤斤计较, 剩下, 无他. 问题的突破点看来是从Object开始, 从noSuchMethod结束了.

Dart的noSuchMethod方法

noSuchMethod

果不其然, 在Object的源码里出现了noSuchMethod方法, 它传入了一个叫做Invocation的东西, Invocation意为"调用", 顾名思义, 是对方法调用的具体描述. 这个东西看起来跟Flutter禁用的dart:mirrors好像很有点关系啊, 看看具体的实现呢?

noSuchMethod

Invocation被放在了flutter sdk的sky_engine下(也就是后来的flutter_engine), 作为核心实现的内容之一, 在完整版Dart Sdk里, 我们也可以找到invocation.dart, 跟这个invcation.dart完全相同.

noSuchMethod

仔细阅读源码不难看出, 借助这个Invocation, 我们基本可以分析出某个方法调用的全部信息, 包括它是set方法还是get方法, 传入的参数是哪些...全部都能知道. 这样的话, 要实现之前的三个需求, 就不是难事了.

JsonObject实现的具体思路

我们知道, value = json.key, 这种.key的写法其实在Dart里不仅可以使用变量来实现, 也可以通过分成get和set方法来实现, 也就是说对于json取值操作形如value = json.key, 用Java的写法相当于调用get方法, 即value = json.getKey(). 而对于json赋值操作形如json.key = newValue, 用Java的写法相当于调用set方法, 即json.setValue(key, newValue), 在noSuchMethod里, 这样的调用模式会被Invocation截获, 而我们可以使用这个invocation, 判断当前这个dynamic类型的变量是在set还是在get, 从而写一个具体的响应方法, 最后把执行结果传回去.


写到这里, 具体的思路就成型了. 由于篇幅和时间的关系, 今天只能记录到这里了. 在下一篇文章, 也就是即将到来的<<Dart Json 序列化方案探索-睡前开发笔记(二)>>里, 我将会一点一点码上代码, 实现本文中的这个具体思路, 同时也是对现有源码的详细剖析, 敬请期待吧~