yuhanle / blogbag

我会不断更新这个仓库中的文章
https://latehorse.github.io/
14 stars 1 forks source link

数据序列化框架在 Swift 日常开发中的应用 #6

Open yuhanle opened 6 years ago

yuhanle commented 6 years ago

image

到了 Swift 年代,第三方库 SwiftyJSON 和 ObjectMapper 都曾经作为 JSON 转换的中流砥柱,只是这两者还是免不了“手动指定字段和JSON字典映射关系”的工作。于是阿里想了个黑科技(HandyJSON),通过分析Swift数据结构在内存中的布局,自动分析出映射关系,进一步降低开发者使用的成本。

如今我们就有多个选择:ObjectMapper、HandyJSON、SwiftyJSON、MJExtension 等

其实我们在日常开发中,对于 JSON 数据的处理有两大需求:

  1. json 和 model 互相转换(Android Studio有 Gson format 插件,但Xcode没有类似功能)
  2. 服务端返回的 json 里可能有 null,但是 Swift 语言的空是用 nil 表示,需要空值处理(对象 Optional 类型)

框架简介

ObjectMapper

先看 ObjectMapper : Model 类必须实现 Mappable 协议,即实现 init 和 mapping 函数;适合跟 Alamofire 配合。但是 mapping 函数实现起来过于臃肿耗时,只能借助插件来快速完成。

import ObjectMapper

class PersonOBM: Mappable {
    var username: String?
    var age: Int?
    var weight: Double!
    var sex: Bool!
    var location: String?
    var three_day_forecast: [ForecastOBM]?

    required init?(map: Map) {

    }

    func mapping(map: Map) {
        username    <- map["username"]
        age         <- map["age"]
        weight      <- map["weight"]
        sex         <- map["sex"]
        location    <- map["location"]
        three_day_forecast <- map ["three_day_forecast"]
    }
}

class ForecastOBM: Mappable {
    var conditions: String?
    var day: String?
    var temperature: Double!

    required init?(map: Map) {

    }

    func mapping(map: Map) {
        conditions      <- map["conditions"]
        day             <- map["day"]
        temperature     <- map["temperature"]
    }
}

j2s 是一个 macOS app 能够将 JSON 对象转成 Swift 结构体

HandyJSON

再看 HandyJSON, 写起来比较方便,类和结构体要求继承于 HandyJSON、枚举要继承于 HandyJSONEnum。

import HandyJSON

class PersonHJ: HandyJSON {
    var username: String?
    var age: Int?
    var weight: Double!
    var sex: Bool!
    var locatoin: String?
    var three_day_forecast: [ForecastHJ]?

    required init() {

    }
}

class ForecastHJ: HandyJSON {
    var conditions: String?
    var day: String?
    var temperature: Double!

    required init() {

    }
}

比ObjectMapper使用上要简单, 不用写mapping函数那么多代码了。

SwiftyJSON

SwiftyJSON:取字段值使用比较方便, 但是然并卵? SwiftyJSON 不支持转 Model,如果你只是想要解析某几个字段,那么 SwiftyJSON 是不二选择, 而且适用于 Alamofire。

let jsonString: String = "{\"username\":\"yuhanle\",\"age\":18,\"weight\":65.4,\"sex\":1,\"location\":\"Toronto, Canada\",\"three_day_forecast\":[{\"conditions\":\"Partly cloudy\",\"day\":\"Monday\",\"temperature\":20},{\"conditions\":\"Showers\",\"day\":\"Tuesday\",\"temperature\":22},{\"conditions\":\"Sunny\",\"day\":\"Wednesday\",\"temperature\":28}]}"

let dataFromString = jsonString.data(using: .utf8, allowLossyConversion: false)
do {
    let json = try JSON(data: dataFromString!)
    print(json["username"], json["weight"], json["three_day_forecast"][0]["conditions"])
} catch let error as NSError {
    print ("Error: \(error.domain)")
}

MJExtension

最后看下一下 MJExtension,作为一个从 ObjC 年代就开始流程的转换框架,在如今使用的人仍然很多,但是对于 Swift 的集成却不是特别友好,官方 issue 列表中经常都会有申请支持 swift 的呼声!

import MJExtension

class PersonMJ: NSObject {
    @objc var username: String?
    @objc var age = 0
    @objc var weight = 0.0
    @objc var sex = false
    @objc var location: String?
    @objc var three_day_forecast: [ForecastMJ]?
}

class ForecastMJ: NSObject {
    @objc var conditions: String?
    @objc var day: String?
    @objc var temperature = 0.0
}

尽管在支持上不是特别友好,但是在自定义 Model 的过程中,应该是最轻松的一款,但是在升级 Swift 4 之后,需要在属性前添加 @objc 才可以正常使用,否则转换失败。具体情况可参考:Swift 4 字典转模型失败

另外还有一种情况,就是关于整型属性,需要给定初试值,也就是说,MJExtension 无法序列化/反序列化整型。解决办法很简单, 就是赋个默认值, 即将Optional整型变为整型就可以。

class People: NSObject {
    var name: String?
    var age: Int?   // 请注意:MJExtension不能解析Optional Int类型
    var age = 0     // 正确
}

运行耗时

我们准备了一小段 JSON 数据,循环解析 10000 次,来分析几大框架的运行耗时:

{
    "username": "yuhanle",
    "age": 18,
    "weight": 65.4,
    "sex": 1,
    "location": "Toronto, Canada",
    "three_day_forecast": [
        {
            "conditions": "Partly cloudy",
            "day": "Monday",
            "temperature": 20
        },
        {
            "conditions": "Showers",
            "day": "Tuesday",
            "temperature": 22
        },
        {
            "conditions": "Sunny",
            "day": "Wednesday",
            "temperature": 28
        }
    ]
}

以 HandyJSON 为例,在开始处理数据和结束时,记录时间差,时间差的结果每次会有波动

let maxCount = 10000
func testHandyJSON(json: String) -> Void {
    var start = CFAbsoluteTimeGetCurrent()

    var people: PersonHJ = PersonHJ()
    for _ in 0..<maxCount {
        people = PersonHJ.deserialize(from: json)!
    }

    var executionTime = CFAbsoluteTimeGetCurrent() - start
    print("HandyJSON deserialize time totals: ", executionTime)

    start = CFAbsoluteTimeGetCurrent()

    var res = ""
    for _ in 0..<maxCount {
        res = people.toJSONString()!
    }

    executionTime = CFAbsoluteTimeGetCurrent() - start
    print(res)
    print("HandyJSON toJSONString time totals: ", executionTime)
}

最终记录得到的结果对比图

测试项 JSON -> MODEL MODEL -> JSON
HandyJSON 3.0839329957962 4.97446703910828
ObjeceMapper 1.40153098106384 1.2123589515686
SwiftJSON 不支持 不支持
MJExtension 0.417935013771057 0.418874025344849

image

结果有点出乎意料,HandyJSON 的黑魔法纵然很强大,这也导致了耗时的问题,相比较而言,不太友好的 MJExtension 速度最快。

总结

对于应用开发来说,JSON 数据序列化和反序列号的操作必不可少,上述分析的效率和性能问题,也应该多考虑,选择合适的框架很重要,学习和踩坑也是并存的。

另外,Swift 支持 Codable 协议,对这个需求的处理也有很大的支持!

参考链接