关于“幽灵架构”的补充说明3:为什么不会产生“循环引用”

来源:互联网 发布:戴森无叶风扇 知乎 编辑:程序博客网 时间:2024/04/30 09:13

承接上文,已经简明阐述了使用Struct代替Class的好处,使用Class会使我们的程序出现“意外的共享”以及“循环引用”之类的危险,传统面向对象开发中对Class的依赖主要来自于我们对“继承”的依赖。Swift2.0引入协议扩展后,之前的“类-继承”所能实现的功能使用“结构体(枚举)-协议-协议扩展”都可以实现,并且更加高效和灵活。回到主题上来,首先回顾下“幽灵架构”中的两个主体:View和Model所对应的协议:

//视图使用的协议protocol ViewType{    func getData<M:ModelType>(model:M)}//数据使用的协议protocol ModelType{}//定义默认方法giveDataextension ModelType{    func giveData<V:ViewType>(view:V){        view.getData(self)    }}

在控制器中生成了Model和View的实例之后,需要将二者绑定起来:

dataList[indexPath.row].giveData(cell)

如果你尝试修改顺序使用:

cell.getData(dataList[indexPath.row])

在dataList是[ModelType]类型的情况下编译无法通过,但是对于[Festival]或者[Event]这样同构的数组,在捆绑时调用getData方法是可行的,这是因为getData的参数是泛型,而数组中的元素是ModelType类型的,不符合泛型的使用规范,所以编译器会报错,但是为什么同样是泛型方法,反过来调用giveData就可以呢?giveData其实有两层类型检查的“关口”,第一层是giveData要求参数是遵守ViewType的泛型类型,而Demo中的每一个cell实例都是具体的类型,所以cell可以作为giveData的参数,而一旦giveData的参数通过考验,那么在giveData默认实现的方法体中,调用了参数的getData方法,这个方法也要求传入一个遵守协议的具体类型,这里传入了self属性,这个属性返回的是实例本身,所以可以顺利通过编译器的检测。所以真正进行数据绑定的是getData方法,giveData方法的作用是避免getData的参数是协议类型的,从而产生不必要的开销,使得即使数据源是异构的,依然可以通过giveData的方式传递真实的数据类型。
这里不得不再提一句泛型的好处,泛型保证了原始数据的传递,而不会像协议类型那样多一步寻址的过程,保证了高效。所以“幽灵架构”中的数据绑定虽然被移出了控制器代码,但是其实和在控制器中直接进行数据绑定同样高效,为了证明这一点,我们在TableViewCell的子类的getData方法中加入一个sizeOfValue方法,检查参数model的长度:

func getData<M : ModelType>(model: M) {        print(sizeofValue(model))        //这里不能写成guard let dateModel = model as? DateViewModel else{}令我有些意外        guard let dateModel = model as? hasDate else{            return        }        //处理相同属性        dateLabel.text = dateModel.date        //处理数据源异构        if let event = dateModel as? Event{            MixLabel.text = event.eventTitle            backgroundColor = UIColor.redColor()        } else if let festival = dateModel as? Festival{            MixLabel.text = festival.festivalName        }    }

运行,打印结果:
这里写图片描述
因为我们定义的Festival和Event中有两个String类型的属性,每个String的大小是24字节,所以Festival和Event都是48字节,现在我们把Event改成一个Class:
这里写图片描述
class是引用类型的,所以它在栈上的长度是8个字节(一个指针的长度),可以看到在“幽灵架构”体系中,Model和View的绑定不会产生中间层,二者是直接绑定的,那么问题来了,这种互相绑定会不会产生“循环引用”?
首先,如果Model都是结构体的话,是不会产生循环引用的,每个值类型都只有一个拥有者(因为Copy),在值类型的拥有者执行完毕后,值类型随着拥有者一同销毁。虽然struct可以定义init,但是你会发现当你想在struct中定义一个deinit方法时,编译器提示你deinit只能被定义在类中:
这里写图片描述
所以你不用关心值类型的生命周期,那么已经被改成了class的Event呢?为了制造可能造成循环引用的场景,我们在事件节日提醒控制器前面再加一个控制器,把新加的控制器和事件节日提醒控制器用导航控制器连接起来,现在通过导航栏返回的时候事件节日提醒控制器会被系统回收,所有的Model和View实例也应该被回收,此时可以检验有没有出现“循环引用”,在Event中定义一个deinit:

class Event:DateViewModel{    var date = ""    var eventTitle = ""    init(date:String,eventTitle:String){        self.date = date        self.eventTitle = eventTitle    }    deinit{        print("deinit")    }}

现在运行程序,点击按钮来到事件节日提醒列表页面,然后点击返回,可以看到中控台打印:
这里写图片描述
谢天谢地^ ^。现在来解释一下,Swift中的实例方法其实并不会被保存在实例中,实例方法和全局方法不同的地方是多了一个命名空间,就好像类方法那样的格式,举个例子:

struct Test{    var a = ""    var b = ""    var c = ""}

使用sizeof查看这个结构体的大小,显示为72,然后向其中增加一个方法:

struct Test{    var a = ""    var b = ""    var c = ""    func ab(str:String){        print(str)    }}

再次查看Test的大小,仍旧是72。在调用这个方法的时候我们常用的格式是:

a.ab("111")

但其实我们也可以这样用:

Test.ab(a)("111")

这里用到了柯里化,对于实例方法来说,先传入一个实例a,返回一个接受String类型的参数的方法,再传入我们定义的参数类型。所以使用方法进行数据和视图的绑定是安全的,因为二者是同等关系的,并不存在依赖关系。这也解释了为什么在官方文档中讲解capture list的时候,普通的闭包和方法不会产生循环引用,只有把闭包作为参数的时候才会产生循环引用,如上面看到的,因为实例只会持有自己的参数而不会持有方法。

2 0
原创粉丝点击