深度探究HandyJSON(四) 解析 struct

在这个系列的前两篇文章中, 我们讲了 Swift指针的使用, Mirror 的原理, 这些其实都是为接下来的几篇文章做铺垫. 这个系列我并不打算将 HandJSON 的每个细节都讲到, 主要围绕如何通过 strcut / class 对象实现反序列化, 最后实现一个 Swift 版的 KVC.

在这篇文章里, 我们将主要关注 struct 对象实现反序列化.

在开始之前, 先梳理一下 HandJSON 的结构.


HandyJSON大致结构图

HandyJSON 的初代版本和 Reflection 相似, 如果你也对反射感兴趣, 可以去看一下这个项目.

回顾一下, 如何在内存上为实例的属性赋值呢?

  • 获取到属性的名称和类型.
  • 找到实例在内存中的 headPointer, 通过属性的类型计算内存中的偏移值, 确定属性在内存中的位置.
  • 在内存中为属性赋值.

那么 HandyJSON 内部是怎么处理的呢?

废话少说, 直接上代码, 我将 HandyJSON 中的代码做了最简化处理.

第 0 步: 定义 Model

struct Person {
    var isBoy: Bool = true
    var age: Int = 0
    var height: Double = 130.1
    var name: String = "jack"
}

第 1 步: 获取属性数量, 以及属性偏移矢量

struct _StructContextDescriptor {
    var flags: Int32
    var parent: Int32
    var mangledName: Int32
    var fieldTypesAccessor: Int32
    var numberOfFields: Int32
    var fieldOffsetVector: Int32
}

var personType = Person.self as Any.Type

// 类型转化 
let pointer = unsafeBitCast(personType, to: UnsafePointer<Int>.self)

let base = pointer.advanced(by: 1)  // contextDescriptorOffsetLocation

// 相对指针偏移值
let relativePointerOffset = base.pointee - Int(bitPattern: base)

以 _StructContextDescriptor 类型访问数据
let descriptor = UnsafeRawPointer(base).advanced(by: relativePointerOffset).assumingMemoryBound(to: _StructContextDescriptor.self)

print(descriptor.pointee)
// _StructContextDescriptor(flags: 262225, parent: -24, mangledName: -16, fieldTypesAccessor: 54167296, numberOfFields: 4, fieldOffsetVector: 2)
  • _StructContextDescriptor 类型访问内存数据, 得到属性数量 numberOfFields, 属性偏移矢量 fieldOffsetVector, 通过这两个参数可以获取每个属性的偏移值.
  • _StructContextDescriptor 的内部结构来源于 Swift 源码中TargetContextDescriptor, TargetTypeContextDescriptor 这两个类. 大致如下
// 所有上下文描述符的基类。
struct TargetContextDescriptor {
    // 描述上下文的标志,包括其种类和格式版本
    ContextDescriptorFlags Flags;
    
    // 父上下文,如果这是顶级上下文,则为null
    RelativeContextPointer<Runtime> Parent;

}

struct TargetExtensionContextDescriptor final
    : TargetContextDescriptor<Runtime> {
    
    RelativeDirectPointer<const char> ExtendedContext;
    
    // MangledName
  StringRef getMangledExtendedContext() const {
    return Demangle::makeSymbolicMangledNameStringRef(ExtendedContext.get());
  }
        
}

class TargetTypeContextDescriptor
    : public TargetContextDescriptor<Runtime> {
    
    // type 的名字
    TargetRelativeDirectPointer<Runtime, const char, /*nullable*/ false> Name;
    
    int32_t getGenericArgumentOffset() const;
    
    const TargetMetadata<Runtime> * const *getGenericArguments(
                               const TargetMetadata<Runtime> *metadata) const { }
}
  • TargetContextDescriptor, TargetTypeContextDescriptor 中我们能发现很多相对指针 relative pointer, 这些指针实际上是指向被引用数据的偏移量,相对于存储指针的位置. 这还意味着可以重新定位数据而无需重写任何值.

第 2 步: 获取属性内存偏移值

extension UnsafePointer {
    init<T>(_ pointer: UnsafePointer<T>) {
        self = UnsafeRawPointer(pointer).assumingMemoryBound(to: Pointee.self)
    }
}

let contextDescriptor = descriptor.pointee
let numberOfFields = Int(contextDescriptor.numberOfFields)
let fieldOffsetVector = Int(contextDescriptor.fieldOffsetVector)

// 成员变量的偏移值
let fieldOffsets = (0..<numberOfFields).map {
    return Int(UnsafePointer<Int32>(pointer)[fieldOffsetVector * 2 + $0])
}
print(fieldOffsets)
// [0, 8, 16, 24]

第 3 步: 获取属性名字和类型并进行包装

struct PropertyDescription {
    public let key: String
    public let type: Any.Type
    public let offset: Int
}

// 类对象
let selfType = unsafeBitCast(pointer, to: Any.Type.self)  // Person

// 属性的包装
var propertyDescriptions: [PropertyDescription] = []

class NameAndType {
    var name: String?
    var type: Any.Type?
}

// 下面这是编译器特性
// 可跳过桥接文件和.h头文件与C代码交互
@_silgen_name("swift_getFieldAt")
func _getFieldAt(
    _ type: Any.Type,
    _ index: Int,
    _ callback: @convention(c) (UnsafePointer<CChar>, UnsafeRawPointer, UnsafeMutableRawPointer) -> Void,
    _ ctx: UnsafeMutableRawPointer
)

for i in 0..<numberOfFields {
    // 属性name, type 的包装类
    var nameAndType = NameAndType()
    
    // 获取属性name, type
    _getFieldAt(selfType, i, { (namePointer, typePointer, nameTypePointer) in
        let name = String(cString: namePointer)
        let type = unsafeBitCast(typePointer, to: Any.Type.self)
        let nameType = nameTypePointer.assumingMemoryBound(to: NameAndType.self).pointee
        nameType.name = name
        nameType.type = type
    }, &nameAndType)
    
    // 将name , type, offset进行包装
    if let name = nameAndType.name, let type = nameAndType.type {
        propertyDescriptions.append(PropertyDescription(key: name, type: type, offset: fieldOffsets[i]))
    }
}

print(propertyDescriptions)
  • 在这部分代码中, 看到了我们比较熟悉的 _getFieldAt 方法, 这个方法曾今在 Mirror 被使用过, 获取字段信息. 在 Swift 代码中要访问 C++ 代码, 需要加上 @_silgen_name

testAdd.c 文件中, 定义如下方法.

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}
int mul(int a, int b) {
    return a * b;
}

testAdd.Swift 中要使用 testAdd.c 中的 add, mul 方法, 我们可以这么做.

@_silgen_name("add")
func c_add(i:Int32,j:Int32)->Int32
@_silgen_name("mul")
func c_mul(i:Int32,times:Int32)->Int32

extension ViewController {
    
    // 不使用桥接文件或者.h文件直接调用.c 文件的函数
    func testCBridge(){
        print(c_add(i: 10, j: 20))  // 30
        print(c_mul(i: 10, times: 20)) // 200
    }
}

第 4 步: 将 JSON 数据进行解析, 反序列化到实例

// 获取头指针
func headPointerOfStruct<T>(instance: inout T) -> UnsafeMutablePointer<Int8> {
    return withUnsafeMutablePointer(to: &instance) {
        return UnsafeMutableRawPointer($0).bindMemory(to: Int8.self, capacity: MemoryLayout<T>.stride)
    }
}

// 获取头指针
var personStruct = Person()
let rawPointer = headPointerOfStruct(instance: &personStruct)

// 获取数据
let dict: [String: Any] = ["isBoy": true, "name": "lili", "age": 18, "height": 100.123]

// 遍历属性
for property in propertyDescriptions {
    let propAddr = rawPointer.advanced(by: property.offset)
    
    if let rawValue = dict[property.key] {
        extensions(of: property.type).write(rawValue, to: propAddr)
    }
}
print("\n person \n", personStruct)
// Person(isBoy: true, age: 18, height: 100.123, name: "lili")
// 写入数据成功

protocol AnyExtensions {}

extension AnyExtensions {
    public static func write(_ value: Any, to storage: UnsafeMutableRawPointer) {
        guard let this = value as? Self else {
            print("类型转换失败, \(type(of: value))无法转为\(Self.self)")
            
            return
        }
        storage.assumingMemoryBound(to: self).pointee = this
    }
}
func extensions(of type: Any.Type) -> AnyExtensions.Type {
    struct Extensions : AnyExtensions {}
    var extensions: AnyExtensions.Type = Extensions.self
    
    withUnsafePointer(to: &extensions) { pointer in
        UnsafeMutableRawPointer(mutating: pointer).assumingMemoryBound(to: Any.Type.self).pointee = type
    }
    return extensions
}

这段代码有一个位置比较有意思, 在第一篇 Swift中指针的使用 这一篇文章中我们就提到

// 将内存临时重新绑定到其他类型进行访问.
let namePtr = pStructHeadRawP.advanced(by: offset).assumingMemoryBound(to: String.self)

// 设置属性值
namePtr.pointee = "lily"
  • 拿到头指针后, 我们可以直接根据实例的头指针以及每个属性的偏移值, 获取到每个属性在内存中的位置,
  • 再将其重新绑定到指定的属性类型进行访问, 就可以获取到属性的指针,
  • 通过这个指针就可以为属性赋值.

在本例子中, 我们可以直接采用下面这种方式赋值, 但问题是我们从 JSON 数据中获取到的值是Any类型的, 在这其中必须将其转化为对应属性类型, 如果手动转就比较麻烦了.

propAddr.assumingMemoryBound(to: String.self).pointee = rawValue as! String

文中将这个类型直接传给第三方来处理, 通过判断传入的数据类型与属性的类型是否匹配, 来进行赋值, 无需强转数据类型, 这就比较方便了.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容