iOS开发技巧系列---打造强大的BaseModel(篇四:使用Swift反射)

本文是iOS开发技巧系列---打造强大的BaseModel中的第四篇,前面三篇文章都可以看我有主页面里面找到,这里就不贴链接了。如果你没有看过前面三篇文章的话,建议在看这篇文章之前先去看看,熟悉一下iOS Runtime的一些东西以及纯Swift类型和Objc类型的异同。而这篇文章则讨论了Swift的反射功能,的iOS Runtime不一样,Swift的反射用了另一套API,实现机制也完全不一样,倒是和目前其他的面向对象语言有些相似,比如C#和Java。不过Swift作为一门很新的语言,目前正在高速发展中,其反射功能和和主流的高级语言还没法比,但是我相信Apple会在将来大大加强Swift的反射功能,以追上目前主流语言的脚步。

2018年Swift4已经发布,现在需要更新这些文章了,里面的代码可能都跑不起了。所以我要修正这些代码让其跑起来。我把这些代码都放在iOSDemo项目里
https://github.com/DuckDeck/iOSDemo

iOS Runtime目前存在的问题

关于iOS Runtime的文章有很多,在简书里一搜就能找出一大堆,但是大多数都是介绍什么是iOS Runtime及怎么使用Runtime。其实基于Objc的Runtime是iOS开发的黑魔法,甚至可以是说奇技淫巧,比如神奇的Method Swizzle可以交换任何iOS的系统方法,在里面加上自己定义的一些功能。再比如消息转发机制,又比如说一些位于<objc/runtime.h>中的方法,比如class_copyIvarList等方法,可以动态获取一个类里面所有的方法和属性,还有就是动态给一个类添加属性和方法。Objc的Runtime是如此的强大,再加上KVC和KVO这两个利器,可以实现很多你根本就想不到的功能,给iOS开发带来极大的便捷。
使用iOS Runtime好处非常多,但缺点也是显而易见的,主要有下面几个:

  • 基于Objc的Runtime不是类型安全的,需要开发者保证所有数据都是正确的类型。
  • Runtime还是需要进行数据类型的检查,影响了执行效率。
  • Runtime的API并不是特别好用,C语言命名风格的API和一些指针操作等使得和现代化的Swift语法格格不入。
  • Apple推出全新的Swift语言后,单纯的Swift类型不再兼容原先的Objc的Runtime
    新的Swift4已经可以实现正确地识别出这几非Objc类型,

其中前面两个问题影响不大,关键在于第三个。基于Swift作为一门静态语言,所有数据的类型都是在编译时就确定好了的,但是Apple为了让Swift兼容Objc,让Swift也使用了Runtime。这显然会拖累Swift的运行效率,和Apple所宣称Swift具有超越Objective-C的性能的观点完全不符。而Swift在将来是会慢慢替代 Objective-C的成为iOS或者OSX开发的主流语言,所以为了性能,我们应该尽量使用原生的Swift数据类型,避免让Runtime进行Swift类型->Objc类型的隐式转换 (至于最新的Swift4我对此持保留态度,也许苹果的优化会让Swift的性能和Objective-C持平甚至超越)。

所以目前的问题是使用Swift原生的数据类型和想要使用Objc的Runtime有了冲突,那么Swift语言里有没有类似于Objc的Runtime的一套机制,让Swift数据类型也能实现Objc的Runtime的一些功能呢?

很遗憾,这个答案是NO,Swift目前只有有限的反射功能,完全不能和Objc的Runtime相比。

现在Swift4已经发布了,让我们来看看Swift4里面的反射功能有什么改进。但是很遗憾,Swift4里面的反射功能和以前还没什么区别。

什么是反射

反射是一种计算机处理方式。是程序可以访问、检测和修改它本身状态或行为的一种能力。

上面的话来自百度百科。使用反射有什么用,看一些iOS Runtime的文章应该会很明白。下面再列举一下

  • 动态地创建对象和属性,
  • 动态地获取一个类里面所有的属性,方法。
  • 获取它的父类,或者是实现了什么样的接口(协议)
  • 获取这些类和属性的访问限制(Public 或者 Private)
  • 动态地获取运行中对象的属性值,同时也能给它赋值(KVC)
  • 动态调用实例方法或者类方法
  • 动态的给类添加方法或者属性,还可以交换方法(只限于Objective-C)

上面的一系列功能的细节和计算机语言的不同而不同。对于Objective-C来说,位于<objc/runtime.h>中的一系列方法就是完成这些功能的,严格来说Runtime并不是反射。而Swift真正拥有了反射功能,但是功能非常弱,目前只能访问和检测它本身,还不能修改。

Swift的反射

Swift的反射机制是基于一个叫Mirror的Stuct来实现的。具体的操作方式为:首先创建一个你想要反射的类的实例,再传给Mirror的构造器来实例化一个Mirror对象,最后使用这个Mirror来获取你想要的东西。
首先我们来写一些测试用的类

protocol Drive{
    func run()
}


public  class Tire{ //轮胎
    var brand:String? //品牌
    var size:Float = 0 //大小
}

public class Vehicle:Drive{
    var carType:String?
    var tires:[Tire]?
    var host:String?// 主人
    var brand:String?//汽车品牌
    func run() {
        if let h = host{
            print("\(h)Drive a \(brand) \(carType) car run")
        }
        else{
            print("this car is not selled")
        }
    }
}

public class Trunk:Vehicle{
    public var packintBox:String?
}

public struct TranGroup{ //货运集团
    var trunks = {
        return [Trunk]()
    }()
    
    var country:String?
    var turnover:Float?
}

//一个中国的货运集团
var tranGroup = TranGroup()

tranGroup.country = "天朝"
tranGroup.turnover = 2222
let trunk1 = Trunk()
trunk1.brand = "MAN"
trunk1.host = "Stan"
trunk1.packintBox = "Big And Long"
tranGroup.trunks.append(trunk1)
let mirrorTran = Mirror(reflecting: tranGroup)
print(tranGroup) //打印出   TranGroup 相关信息
print(mirrorTran.subjectType) //打印出  TranGroup
print(mirrorTran.displayStyle) //Optional(Swift.Mirror.DisplayStyle.Struct),是个Struct类型
print(mirrorTran.superclassMirror) //nil,因为没有父类
for (key,value) in mirrorTran.children{
    print("\(key) : \(value)")
}

//打印结果
Optional("trunks") : [DemoConsole.Trunk]
Optional("country") : Optional("China")
Optional("turnover") : Optional(1.23457e+08)

可以看出,和第一篇文章一样,打印出个每个属性和其值。不同的是,对于自定义对象,不能自动打出里面的属性内容。

在利用Swift的反射来改进BaseModel之前,让我们来看看Mirror里面都有什么东西吧

public init(reflecting subject: Any) //构造器
    
public typealias Child = (label: String?, value: Any)

public typealias Children = AnyCollection<Mirror.Child>

 public enum DisplayStyle {

        case `struct`

        case `class`

        case `enum`

        case tuple

        case optional

        case collection

        case dictionary

        case set
    }

public enum AncestorRepresentation {
    case Generated /// 为所有 ancestor class 生成默认 mirror。
    case Customized(() -> Mirror)/// 使用最近的 ancestor 的 customMirror() 实现来给它创建一个 mirror。
    case Suppressed /// 禁用所有 ancestor class 的行为。Mirror 的 superclassMirror() 返回值为 nil。
}

public let subjectType: Any.Type
public let children: Children
public let displayStyle: Mirror.DisplayStyle?
@warn_unused_result
public func superclassMirror() -> Mirror?

一个一个来看

  • 第一个是构造器,它传入的参数类型是Any类型。说明Mirror支持对任意类型的反射。
  • 下面定义了两个typealias,分别是Child和Children,Child是个元组(label: String?, value: Any),label是指属性名,是个可空值,因为不是所有支持反射的数据结构都包含有名字的子节点。 struct 会以属性的名字做为 label,但是 Collection 只有下标,没有名字。Tuple 同样也可能没有给它们的条目指定名字。是Value是个Any,也就是说属性可以是任意类型。
  • DisplayStyle是个enum,它会告诉你对象的类型。这里面其他囊括了所有Cocoa的类型,唯一的例外是个Closure,或者Block。
  • AncestorRepresentation也是个enum,这个enum用来定义被反射的对象的父类应该如何被反射。也就是说,这只应用于 class 类型的对象。默认情况(正如你所见)下 Swift 会为每个父类生成额外的 mirror。然而,如果你需要做更复杂的操作,你可以使用 AncestorRepresentation enum 来定义父类被反射的细节。具体都有什么样的类型看上面的注释。
    +subjectType是个Any.Type,从上面打印出来的东西可以看出,它应该是AnyClass的名称,不同的是AnyClass是AnyObject.Type.后面可以写代码验证
  • children是个AnyForwardCollection<(label: String?, value: Any)>类型,也就是个Child的集合。AnyForwardCollection是个什么玩意呢,Apple是这么说的
  • Swift4.1中已经改成AnyCollection了
/// A type-erased wrapper over any collection with indices that
/// support forward traversal.
///
/// Forwards operations to an arbitrary underlying collection having the
/// same `Element` type, hiding the specifics of the underlying
/// `CollectionType`.

这个确实有点难以理解,我试着翻译过来:一个类型可擦除的包装器,适用于任何支持向前遍历的集合,前向遍历操作任意一个有着相同“元素”类型的底层集合,隐藏底层集合类型的细节。翻译过来还是不明白什么意思,也许需要大神指点,我只好把它看作一个可以遍历普通集合算了。
对于AnyCollection,苹果的解释和AnyForwardCollection也不多,所以上面的话对AnyCollection也是适用的。

  • displayStyle是DisplayStyle enum,表示反射的对象属于什么样的类型
  • superclassMirror()是获取父类的Mirror,如果没有父类,则为nil

下面看看用各种类型来看看Mirror的各个属性可以打印出什么

extension Mirror{
    func printMirror(){
        print("mirror:\(self) type:\(self.subjectType) displayStyle \(self.displayStyle) superClassMirror \(self.superclassMirror) ")
        for (key,value) in self.children{
            print("\(key) : \(value)")
        }
    }
}
var s = {(i:Int) -> Int in  return i + i }
let mir = Mirror(reflecting: s)
mir.printMirror()
typealias item = (key:String,value:Any)
let a:item = ("111",222)
let mirty = Mirror(reflecting: a)
mirty.printMirror()
Mirror(reflecting: "1").printMirror()
Mirror(reflecting: 1.1).printMirror()
Mirror(reflecting: NSData()).printMirror()
Mirror(reflecting: NSNull()).printMirror()
enum week:Int{
    case Mon = 1,Thu,Wed,Tur,Fri,Sat,Sun
}
Mirror(reflecting: week.Fri).printMirror()

//打印结果
**mirror:Mirror for (Int) -> Int type:(Int) -> Int displayStyle nil superClassMirror nil 
mirror:Mirror for (key: String, value: Any) type:(key: String, value: Any) displayStyle Optional(Swift.Mirror.DisplayStyle.tuple) superClassMirror nil 
Optional("key") : 111
Optional("value") : 222
mirror:Mirror for String type:String displayStyle nil superClassMirror nil 
mirror:Mirror for Double type:Double displayStyle nil superClassMirror nil 
mirror:Mirror for _NSZeroData type:_NSZeroData displayStyle Optional(Swift.Mirror.DisplayStyle.class) superClassMirror Optional(Mirror for NSData) 
mirror:Mirror for NSNull type:NSNull displayStyle Optional(Swift.Mirror.DisplayStyle.class) superClassMirror Optional(Mirror for NSObject) 
mirror:Mirror for week type:week displayStyle Optional(Swift.Mirror.DisplayStyle.enum) superClassMirror nil 
** 

Colsure或者Block的displayStyle是nil,而typealias则转化成了正确的类型。其他所有类型都正确在获取并打印出来了。

Mirror类还有一个些其他的构造器,还有一些扩展,可能一些特殊场合会用到,这里就省略了,总之来说,Swift的反射可以用在以下场景

  • 遍历Turple
  • 对类做分析,获取属性和值
  • 运行时分析对象的一至性

总体来说,因为功能比较弱,使用场景也比较窄。远远比不上Objc的Runtime,更别说Java和C#了。
但是相对于Objc的Runtime,Swift的反射是可以获取全部属性的,而且API相对于Objc也简单许多。有时侯,可以用来代替Runtime中获取所有属性名的方法

   func getSelfProperty()->[String]{  //和description属性一样
        var selfProperties = [String]()
        var count:UInt32 =  0
        let vars = class_copyIvarList(type(of: self), &count)
        for i in 0..<count {
            let t = ivar_getName((vars?[Int(i)])!)
            if let n = NSString(cString: t!, encoding: String.Encoding.utf8.rawValue) as String?
            {
                selfProperties.append(n)
            }
        }
        free(vars)
        return selfProperties
    }

可以用这个来重写description方法,代码看起来要简洁不少。但是-----这个和Runtime比起来还是不好用。

class GrandModel: NSObject {
    func getSelfProperty()->[String]{
        var dict = [String:Any]()

        let mir = Mirror(reflecting: self)
        for (key,value) in mir.children{
                dict[key!] = value
        }
        return ["\(type(of: self)):\(dict)"]
    }
  
    required override init() {
    }

}

class DemoClass: GrandModel {
    var demoString:String? = ""
    var demoInt = 0
    var demoFloat:Float = 0.0
    var demoDate = NSDate()
    var demoRect = CGRect(x: 1, y: 1, width: 1, height: 1)
    var demoSelf:DemoClass?
}

let demo = DemoClass()
demo.demoString = "this is a test"
let demo2 = DemoClass()
demo2.demoString = "this is a test2test2test2test2test2test2"
demo.demoSelf = demo2
print(demo.getSelfProperty())

打印结果:
["DemoClass:[\"demoFloat\": 0.0, \"demoString\": Optional(\"this is a test\"), \"demoInt\": 0, \"demoSelf\": Optional(<ConsoleSwift.DemoClass: 0x102010580>), \"demoRect\": (1.0, 1.0, 1.0, 1.0), \"demoDate\": 2018-04-09 10:00:37 +0000]"]
可以看出,对于内部的复杂类型,反射只能打印出地址

Mirror中的children无法识别复杂类型,对于自定义类型,或者是没有赋值的可空类型,获取的value是nil,这样也就无法像第一篇文章中那样正确地打印出各种个属性名和值,我的如意算盘落空了。

总结

就目前而言,怪异的API(API适应了还算不错),功能的缺失,让Swift的反射使用场景还不算多。我们之所以要用反射,就是要利用基于反射动态编程的特性,实现在运行时动态地给属性赋值,动态调用方法等。不知道是Apple认为Objc的Runtime功能已经足够强大好用,还是认为Swift的安全更重要,目前在Swift2.2版本反射还是根本没法用。我希望未来Apple可以好好的加强Swift的反射,目前的Swift4.1基本没怎么变。可能是苹果的开发理念并不想让用这个功能。但我还是希望可以好好加强一下Swift的反射功能,跟上现在主流语言的脚步。

打造强大的BaseModel系列文章到此就完结了,在写这些文章的过程中,我发现自己对iOS的Runtime有了更深的理解,也明白了Swift和Objc是怎么相互操作的,可见写文章的最大收获还是自己。所以我还会写更多的iOS开发技巧系列文章,请大家期待。

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

推荐阅读更多精彩内容

  • 新工作,发放第一次工资,很开心。
    爱游泳的Danny阅读 130评论 3 1
  • 左邻右居 相互温暖 一句话就排除困难 看 他们正热火朝天 稳重而利索 瞬间几间房修好 家人如释重负,尽开颜 有备无...
    浅若灿阳阅读 307评论 4 15
  • 对象object分为: 内置对象 String、Date、Array 、Number、Boolean 自定义对象 ...
    五秋木阅读 276评论 0 0
  • 如果你向客户推销屋面瓦,一般我们会这样和客户说:你如果用市面上其他的水泥瓦,有可能会导致屋面漏水。现在,把这句话变...
    小橙小橙小橙阅读 154评论 0 0