HandyJSON是如何实现的?

如果你也和我一样使用过 HandyJSON 这个库,你会一定会好奇,它是如何实现的,尤其是在 Swift 还没有发布 Codable 协议之前,因为当时市面上,就没有一款特别简单好用的 Swift 的序列化和反序列化的三方库,直到我遇到 HandyJSON 这个库,不管里面怎么实现的,至少用起来,确实挺方便的。

虽然现在早已经可以使用 Codable 协议,进行 JSON 的序列化和反序列化的操作了,但是对于 HandyJSON 的实现,我还是很好奇,于是就有了这篇文章。

一,Swift 存在的限制

当时流行的做法是: 在 Swift 中定义 Model 时继承 NSObject,让这个 Model 的实例存在于 objc 运行时中,上述的 class_copyPropertyList 方法和 KVC 就能用上了。目前看见的 Swift 中不需要指明映射关系的 JSON 库,都是这种方式。

Objective-C 通过 class_copyPropertyList 方法加上 KVC 机制,也能轻松实现。而 Swift 会卡在最后一步: 无法赋值。Swift 的反射是只读的,就是说,我们能在运行时获取一个 Model 实例的所有字段、字段值,但却无法给它赋值。事实上,我们拿到的 value 是原值的一个只读拷贝,即使获取到这个拷贝的地址写入新值,也是无效的。

二,具体实现

下面是转了作者的文章原话

Swift 中,一个类实例的内存布局是有规律的:

  1. 32 位机器上,类前面有 4+8 个字节存储 meta 信息,64 位机器上,有 8+8 个字节;

  2. 内存中,字段从前往后有序排列;

  3. 如果该类继承自某一个类,那么父类的字段在前;

  4. Optional 会增加一个字节来存储。None/.Some 信息;

  5. 每个字段需要考虑内存对齐;

这方面尚未从官方的资料找到参考,上述规律一些是从网上其他大神的总结中收集,一些从 Clang 的一些说明文档中挖掘,加上自己的反复验证得到。

有法子计算内存布局,剩下的事情就比较简单了。对一个实例:

  1. 获取它的起始指针,移动到有效起点;

  2. 通过 Mirror 获取每一个字段的字段名和字段类型;

  3. 根据字段名在 JSON 中取值,转换为和字段一样的类型,通过指针写入;

  4. 根据本字段类型的占位大小和下一个字段类型计算下一个字段的对齐起点;

  5. 移动指针,继续处理;

实现原理的逻辑图

上面是转了作者的文章原话

其实上面的概述已经可以描述的很清楚了,核心原理图片上面画的也很清楚了。

我再重复一篇,最核心的原理就是,找到你自定义的模型类型的属性和属性的类型,然后再准备你要反序列化的 JSON,将 JSON 转换成 Native 的基础类型,比如字典,数组,或者 JSON 里面类型能映射过来的类型,然后创建一个你自定义模型的实例,然后找到这个实例的内部内存布局的头指针,然后根据属性所占的内存大小,进行赋值然后偏移,找下一个属性,这样递归完成整个流程。

三,其他人分析

本来我想好好总结一下,然后发出来,但是看见作者和其他人已经总结的很好了,确实没有必要了,比如 https://mp.weixin.qq.com/s/zIkB9KnAt1YPWGOOwyqY3Q 这篇文章已经讲解的非常细致了,如果有兴趣的 同学可以好好了解一下。

其实我之前在写日志库的时候,就是操作内存指针偏移,一点一点赋值的,用 C 去写的,当时是改的美团的 Logan日志,操作指针偏移那一套操作,真的没啥区别,类型和类型的大小别搞错了就行。

四,我的实践

光说不练假把式,我在 Swift 中使用 Swift 提供的和 C 交互的类型,还基本没使用过,因为平时确实没地方用,今儿正好,我就试试,做了一个最简单的通过操作内存对象进行给一个 Swift 的结构体的实例进行属性赋值。

例子中不清楚什么意思的方法 查看这篇文章 https://mp.weixin.qq.com/s/zIkB9KnAt1YPWGOOwyqY3Q 说的很清楚。

也可以下载这篇文章的图片版: http://47.99.237.180:2088/files/766f7abfabdbabc9e5fa1c6af6e8cbfe

环境 Swift5

struct Point {
    let x: Int
    let y: Int
    let z: Int

    //返回指向 Point 实例头部的指针
    mutating func headPointerOfStruct() -> UnsafeMutablePointer<UInt8> {
            return withUnsafeMutablePointer(to: &self) {
                return UnsafeMutableRawPointer($0).bindMemory(to: UInt8.self, capacity: MemoryLayout<Self>.stride)
            }
    }
}

var instance = Point(x: 1, y: 2, z: 3)
// 得到Point类型实例的内部布局的首地址 指针
let p: UnsafeMutablePointer<UInt8> = instance.headPointerOfStruct()
// 准备设置的值
let value: Int = 3
let p1 = UnsafeMutableRawPointer(p)
let p2 = p1.advanced(by: 0).assumingMemoryBound(to: Int.self)
p2.initialize(to: value)
print("p2.pointee:\(p2.pointee)")
`print("instance.x:\(instance.x)")`

// 输出 
// p2.pointee:3
// instance.x:3

五,总结

虽然 HandyJson 很好用,但是从 Swift4.0 之后,苹果官方提供了 Codable 协议,彻底可以使用 Codable 协议对 JSON 的序列化和反序列化,这个是推荐的,肯定不再推荐使用 HandyJson 了,因为从实现原理可以看出来,HandyJson 的实现好多步骤都是在边缘行走,这是不安全的,比如对象的内存结构等,随着 Swift 编译器的更新,就要考虑是否还要兼容。

虽然不建议使用 HandyJson 了,但是 HandyJson 的实现原理,似乎给我们开启了一扇窗,让开发者知道还可以这样去通过操作内存去改变一个对象的属性,虽然这样看着很不安全的样子,稍有不慎就入坑了,但是如果走投无路的情况,也未必不可。

参考: HandyJson 作者原文(我不知道简书的地址是不是原文地址,但是口吻是作者的口吻): https://www.jianshu.com/p/eac4a92b44ef

最后更新于