Swift内存赋值探索一: 理解对象在内存中的存储状态

这系列记录的文章是由一个实际需求引发的 —— 我们能够在 OC 中畅快的使用 KVC ,而在 Swift 中,如果想要使用 KVC ,那么操作对象应该直接或者间接继承 NSObject 。原因是 KVC 属于 Runtime 中的内容,而 Runtime 专属于 OC ,所以我们要使用 KVC 就必须使用 OC 的类来进行。

KVC 有哪些好处我就不多说,越是深入到 Runtime 中越能体会到它的强大。一个简单的例子就足以说明 —— 如果我们希望对一个实例对象的所有属性赋值,除了最简单而又最繁琐的直接使用.语法逐个赋值之外,还有一种更加简单的方式:通过 Runtime 获取对象所有属性的集合,然后遍历这个属性,使用 KVC 统一赋值 —— 这可以写成一个分类,最后暴露的仅仅是一个接口了。

如果 Swift 要做到这一步该怎么做? 本人不才,除了继承 OC 类使用 KVC ,并没有找到其他现成的方案去实现纯 Swift 的 “KVC” 。好吧,这确实是一个悲伤的故事。
而这一切并非没有转机:直到我使用 HandySwift 这个Json 和 Swift 对象互转的框架之后,不仅感叹作者的思路开阔,当然更多的是惊喜,在纯 Swift 中实现 KVC 就靠它了: HandySwift 使用了内存赋值的方式,在“暗地里”给纯 Swift 类的实例对象赋值,避免了开发者面临非 OC 对象需要逐个点语法赋值的尴尬。这不正是另形式的 “KVC” 吗?

这一系列文章就如何实现 内存赋值 进行的探究做了一个记录。

1. 对象如何存在内存中

我们申请一个对象之后,系统会为它分配内存,至于分配在哪里,这将视情况而定,一般来说,局部变量等都会放在栈区,它的作用域很小。 而一个常量或者全局变量则一般会在堆中。我们今天讨论的主角是 Swift 中的实例对象的堆存赋值情况,大多数的使用场景是在全局的状态下,也即是在堆区讨论。 其实就算是在栈区也是一样的,只要对象还存活,我们只需要获取它的地址,然后赋值就达到了我们的目的。
Swift 中,内存有三种状态:

  • 未绑定类型同时未初始化值
  • 绑定类型但是未初始化值
  • 绑定类型同时初始化值

如果我们申请一个对象,除了基础对象之外,复杂的对象都会申请一个指向它的内存的指针,我们通过指针区访问这个内存中的值。但是为了正确的得到被访问值所占空间的真实大小,还需要指定这个内存的类型,以便快速得到内存中的值。

在 Swift 中,任何一个存活的对象都有对应的存储内存,这个内存初始化的值就是该对象,而相对应的内存的类型就是对象的类型。 在初始化对象的时候,系统根据对象属性的类型和内存对齐的规则计算对象内存的大小,之后,对象的属性修改也将在各个属性的地址上做修改。

比如,如果有一个 Person 类:

class Person {
     var name: String!   
     let  isMan: Bool!
     var age: Int!
     var hight:Double!

     init...
}

let p = Person()

对象p被初始化之后,我们分析一下它的内存。
本身p就是一个指针,这个指针的类型是Person,内存中的值就则为p的内容。而在这个指针p指向的区域中,存放name的那部分内存的类型应该是一个String类型,其内部的值为name指针,除了String是指针之外,其余的都是基本数据类型, 他们对应的内存的类型是各自的类型,并且值则为各自的值。
假设String 类型占2个字节,Bool 占 1 个字节,Int 占4个字节,Double 8个字节。
如果不考虑内存对齐的话,那么 p 占用的内存应该为15个连续字节。排列看起来像这样:

无内存对齐

这样紧密排布,看似很节省空间,但是因为 CPU 寻址的方式的特点,这种连续存储的方式,会导致访问的效率急剧下降。因此,实际上对于编程语言来说,还需要进行专门的内存使用设计 —— 内存布局,以便提升 CPU 的访问效率,其中内存对齐就是其一。
假如,Swift在64位设备上内存对齐模数为 8 ,对于p来说,它的真实内存布局应该是这样的:

内存对齐之后

也即是一共需要 32字节的内存空间。跟我们想象中的不太一样,实际上假如有属性出现可选类的时候,内存的占用将更大:比如将hight属性的类型更改为 Double?,这时候他将占用 9 个字节,如果算上内存对齐,它需要的实际空间是 16 个字节。其他的各种内存布局方式跟编程语言的内存模型有关。

2. 如何给内存中的对象的属性赋值

仅仅知道每个对象的存储原则是不够的,我们不知道对象的每个属性是如何在对象内存区域存储的。当我们已经深入到了内存存储的时候,有关的类的大部分信息就不再能够使用正常的渠道获取。比如,尽管我们知道某一块的内存中存了一些我们想要的信息。但是我们并不够准确知道,每一个地址区域对应的内容是什么。
好在,Swift 为类型对象保留了一个关于对象的内存布局描述 —— MemoryLayout

当使用指针分配或绑定内存时,可以使用MemoryLayout作为关于类型信息的来源。
再看看MemoryLayout中的一些内容:

 static var alignment: Int

The default memory alignment of `T`, in bytes.

static var size: Int

The contiguous memory footprint of `T`, in bytes.

static var stride:<wbr> Int

The number of bytes from the start of one instance of `T` to the start of the next when stored in contiguous memory or in an `Array<T>`.

内部包含了一个对象的占用空间: size, 实际长度 stride, 以及对齐模数 alignmentMemoryLayout很有用,我们通过这个类对不同的设备做内存布局的适配。有了这个三个属性,我们可以对一个对象基本可以确定一个对象在不同的设备上的占用内存信息。但是这些信息依然不够,因为这个内存布局并没有体现每一个属性的内存布局信息。

HandySwift 的作者在实现内存赋值的时候也遇到了这个问题,他通过总结发现 Swift 中,一个类实例的属性内存布局是有规律的:

32位机器上,类前面有4+8个字节存储meta信息,64位机器上,有8+8个字节;
内存中,字段从前往后有序排列;
如果该类继承自某一个类,那么父类的字段在前;
Optional会增加一个字节来存储.None/.Some信息;
每个字段需要考虑内存对齐;(包括 meta 信息)
作者:xycn
链接:https://www.jianshu.com/p/eac4a92b44ef
來源:简书

这个规律在 HandySwift 中使用了很久,并且运行良好,我们可以认为上述规律是可行的。

这其中有个问题:如何获取对象的全部属性,这里注意要包含父类的继承属性。Swift 提供了反射机制 Mirro ,通过 Mirro 我们能够逐一获取到对象的所有属性信息,包含了属性的值和类型,唯一遗憾是我们并不能对属性赋值(不然这篇文章可以烧掉了)。并且这个信息是从父类开始,按字段有序排列,刚好符合我们的规律需求。

至于如何赋值就比较简单了,使用指针赋值就可以了。尽管 Swift 不推荐开发者使用指针,但是并非不能使用,详细的内容在后面的文中会介绍。

到这里,对于如何在赋值我们已经有了思路:

  • 找到一个对象的起始地址,舍去 meta 相关的无用地址。找到第一个有效的地址。
  • 通过遍历 Mirro 反射得到的属性列表,按照每个属性的类型计算它所占的字节数,注意,这里要考虑到内存对齐的情况,不然会出事。同时因为有了首地址,后面的地址逐个累加过去即可。
  • 使用指针给对应的内存赋值。
  • 接下来就是循环2、3。 直到属性列表中所有的属性全部赋值完毕。

以上思路完成了对一个对象的所有属性通过内存赋值的全过程。 实际上的操作肯定比这个要复杂的多了。 我们下篇文章中揭晓。

参考:
内存对齐概念
内存模型
HandyJSON设计思路简析

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

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,612评论 4 59
  • 制定企业文化不是件简单的事儿。好的企业文化会让人有股憧憬,两难选择时指导决策。不适合或者说并不真正认同的企业文化最...
    xxwade阅读 505评论 0 3
  • 火车从很远就开始鸣笛,待到到来离开,不过是一小节。 郁闷,雨渐次落下,打了一滴在她眼下,她用力眯了眯眼,眼前的绿似...
    山谷等风阅读 303评论 8 2
  • 公司的项目里要用到一个自定义的控件,一个窗帘随着手指滑动按钮进行上拉或者下滑,本质上是个很简单的东西,用属性动画分...
    c73c75a05599阅读 1,435评论 0 0
  • 月亮(打油诗) 玉轮彩云间, 滚滚永向前。 偷偷露笑脸, 温暖腊月天。
    金赛月阅读 234评论 0 8