iOS底层原理探索—内存管理(一)

探索底层原理,积累从点滴做起

往期回顾

iOS底层原理探索 — OC对象的本质

iOS底层原理探索 — class的本质

iOS底层原理探索 — KVO的本质

iOS底层原理探索 — KVC的本质

iOS底层原理探索 — Category的本质(一)

iOS底层原理探索 — Category的本质(二)

iOS底层原理探索 — 关联对象的本质

iOS底层原理探索 — block的本质(一)

iOS底层原理探索 — block的本质(二)

iOS底层原理探索 — Runtime之isa的本质

iOS底层原理探索 — Runtime之class的本质

iOS底层原理探索 — Runtime之消息机制

iOS底层原理探索 — RunLoop的本质

iOS底层原理探索 — RunLoop的应用

iOS底层原理探索 — 多线程的本质

iOS底层原理探索 — 多线程的经典面试题

iOS底层原理探索 — 多线程的“锁”

前言

内存管理在APP开发过程中占据着一个很重要的地位,在iOS中,系统为我们提供了ARC的开发环境,帮助我们做了很多内存管理的内容,其实在MRC时代,内存管理对于开发者是个很头疼的问题。我们会通过几篇文章的分析,来帮助我们了解iOS中内存管理的原理,以及在ARC的开发环境下系统帮助我们做了哪些内存管理的操作。

iOS程序的内存布局

我们通过一张图展示iOS程序的内存布局:

内存布局.png

在iOS程序的内存中,从底地址开始,到高地址一次分为:程序区域、数据区域、堆区、栈区。其中程序区域主要是代码段,数据区域包括数据段和BSS段。我们具体分析一下各个区域所代表的含义:

代码段: 存放编译后的代码,内存区域较小。程序结束时系统会自动回收存储在代码段中的数据。

数据段: 也叫常量区,保存已初始化的全局变量、静态变量等。直到程序结束的时候才会被回收。

BSS段: 也叫静态区,保存未被初试化的全局变量、静态变量。一旦初始化就会被回收,并且将数据转存到数据段中。

堆区(heap): 保存由alloc创建出来的对象,动态分配内存。需要程序员来进行内存管理。从底地址到高地址分配内存空间

栈区(stack): 保存局部变量,自动分配内存,系统管理。当局部变量的作用域执行完毕后就会被系统立即回收。从高地址到底地址分配内存空间

Tagged Pointer技术

在 2013 年 9 月,苹果推出了 iPhone5s 。iPhone5s 配备了首个采用 64 位架构的 A7 双核处理器。为了节省内存和提高执行效率,苹果提出了Tagged Pointer的概念,用于优化NSNumber、NSDate、NSString等小对象的存储。

在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值。

例如下面这句代码:

NSNumber*number=@10;

在没有使用Tagged Pointer之前,内存中包括一个占8字节的指针变量number,和一个占16字节的NSNumber对象,指针变量number指向NSNumber对象的地址。这样需要耗费24个字节内存空间。

未使用TaggedPointer.png

使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中。

直接将数据10保存在指针变量number中,这样仅占用8个字节。

使用了TaggedPointer.png

当然,当指针不够存储数据时,就会使用动态分配内存的方式来存储数据。

我们用代码来验证一下:

测试.png

在测试代码中创建7个NSNumber类型的对象,分别赋值后打印地址,可以看出使用Tagged Pointer之后,NSNumber指针里面存储着对象的值。其中number7由于赋了一个很大的值,指针不够存储,就使用了动态分配内存的方式来存储number7的值。

当然,以上测试代码要运行在64位环境下。

接下来我们通过一道面试题来帮助我们理解:

以下两段代码的执行结果是什么?

//第1段代码dispatch_queue_t queue=dispatch_get_global_queue(0,0);for(inti=0;i<1000;i++){dispatch_async(queue,^{self.name=[NSString stringWithFormat:@"asdasdefafdfa"];});}NSLog(@"end");

//第2段代码dispatch_queue_t queue=dispatch_get_global_queue(0,0);for(inti=0;i<1000;i++){dispatch_async(queue,^{self.name=[NSString stringWithFormat:@"abc"];});}NSLog(@"end");

答案是第1段代码会崩溃,报出坏内存访问的错误;第2段代码正常打印end

这是为什么呢?

这就涉及到我们上文讲到的Tagged Pointer技术。我们先来看第1段代码中self.name = [NSString stringWithFormat:@"asdasdefafdfa"];这句代码,这句代码的意思将后面的值赋给self.name。注意,此时要赋的值是一长串字符串,name的指针的8个字节已经存储不下这个字符串了,那么就会动态分配内存的方式来存储,就是调用name的set方法。

我们知道,在set方法内部,会首先调用[_name release]释放旧值,再赋新值。但是我们赋值的代码是在子线程中异步执行的,那么就存在同时会有多条线程同时调用[_name release],这就出现问题了。

问题的解决方法很简单,可以把name的nonatomic修饰符改成atomic,这一点我们在iOS底层原理探索 —多线程的读写安全中讲到过atomic的作用,这里不再赘述。或者最直接有效的解决方案就是在异步复制时进行加锁和解锁即可。以保证线程安全。

那么第2段代码为什么能执行成功呢?原因很简单,由于Tagged Pointer技术,name的指针的8个字节足以存放字符串abc,就不涉及调用name的set方法。所以能够成功打印end。

MRC中的内存管理

在iOS中,使用引用计数的技术来管理OC对象的内存:

一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间。调用retain会让OC对象的引用计数+1,调用release或者autorelease会让OC对象的引用计数-1

我们在上文中提到了在set方法内部,会首先调用[_name release]释放旧值,再赋新值。

在MRC时代,程序员需要手动的去管理内存,创建一个对象时,需要在set方法和get方法内部添加释放对象的代码。并且在对象的dealloc里面添加释放的代码。

我们用几个简单的例子来看一下:

使用assign关键字修饰的数据常量,set方法和get方法内部直接赋值和取值

@property(nonatomic,assign)intage;-(void)setAge:(int)age{_age=age;}-(int)age{return_age;}

使用strong关键字修饰的对象,set方法内部需要先释放旧值,再retain新值

@property(nonatomic,strong)Person*person;-(void)setPerson:(Person*)person{if(_person!=person){[_person release];_person=[person retain];}}-(Person*)person{return_person;}

使用copy关键字修饰的对象,set方法内部需要先释放旧值,再copy新值

@property(nonatomic,copy)NSArray*data;-(void)setData:(NSArray*)data{if(_data!=data){[_data release];_data=[data copy];}}

ARC的内存管理

在ARC环境中,我们不再像以前一样自己手动管理内存,系统帮助我们做了release或者autorelease等事情。

ARC是LLVM编译器和RunTime协作的结果。其中LLVM编译器自动生成release、reatin、autorelease的代码,像weak弱引用这些则靠RunTime在运行时释放。

引用计数

上文我们讲到在iOS中,使用引用计数的技术来管理OC对象的内存,那么引用计数是如何存储的呢?我们之前在iOS底层原理探索 — Runtime之isa的本质一文中讲过在__arm64__架构之后,isa指针不单单只存储了类对象和元类对象的内存地址,而是使用共用体的方式存储了更多信息。其中就包括引用计数。

我们再来回顾一下isa指针内部存储的内容:

struct {    // 0代表普通的指针,存储着类对象、元类对象的内存地址。    // 1代表优化后的使用位域存储更多的信息。    uintptr_t nonpointer        : 1;    // 是否有设置过关联对象,如果没有,释放时会更快    uintptr_t has_assoc        : 1;    // 是否有C++析构函数,如果没有,释放时会更快    uintptr_t has_cxx_dtor      : 1;    // 存储着类对象、元类对象对象的内存地址信息    uintptr_t shiftcls          : 33;    // 用于在调试时分辨对象是否未完成初始化    uintptr_t magic            : 6;    // 是否有被弱引用指向过。    uintptr_t weakly_referenced : 1;    // 对象是否正在释放    uintptr_t deallocating      : 1;    // 引用计数器是否过大无法存储在isa中    // 如果为1,那么引用计数会存储在一个叫SideTable的类的属性中    uintptr_t has_sidetable_rc  : 1;    // 里面存储的值是引用计数器减1    uintptr_t extra_rc          : 19;};

我们可以看到,在extra_rc里面存储的值是引用计数器减1,但是当extra_rc的19位内存不够存储引用计数时,has_sidetable_rc的值就会变为1,那么此时引用计数会存储在一个叫SideTable的类的属性中。

SideTable.png

SideTable类中有一个RefcountMap类型的散列表,这个散列表中就存放着引用计数。

我们来到源码文件NSObject.mm文件看一下源码:

在源码中,retainCount方法内部会调用rootRetainCount方法,在rootRetainCount方法,内部会做一系列的引用计数操作:

rootRetainCount源码.png

经过一系列判断,如果has_sidetable_rc的值就会为1时,说明此时引用计数会存储在SideTable的类RefcountMap散列表中。然后通过sidetable_getExtraRC_nolock()函数去获取引用计数。

sidetable_getExtraRC_nolock.png

sidetable_getExtraRC_nolock函数内部,也是先通过key找到对应的SideTable,在SideTable中通过key找到RefcountMap散列表,在散列表中拿到refcnts,即引用计数,然后返回。

今天对于内存管理的分析就到这里,我会在后续的文章中继续为大家分析有关内存管理的知识。

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

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,084评论 1 32
  • iOS中内存管理机制是开发中一项很重要的知识,了解iOS中内存管理的规则不管是在开发中还是在学习中都能很大程度的帮...
    Horson19阅读 1,193评论 0 4
  • 夜寒湿枕巾, 起衣披霜华。 遥望旧里居, 月是故乡明。
    兰亭西94阅读 198评论 0 0
  • 人生最踏实珍贵的幸福,就在日常琐碎的生活中。慢下来,享受生活中小小的幸福,就是在为未来储备力量。只有这样,我们的人...
    布大叔阅读 110评论 0 0
  • 进度:整本书终结。 今天主要是第二十章 智人末日和后记。 印象最深的是,作者在最后的反思: 问题不是“我们究竟想要...
    大林_Rbenefit阅读 171评论 0 0