以前在使用clang将.m文件转成.cpp文件,查看里面的内容,发现属性上的编译也颇有意思,因此本篇探究下iOS内存管理顺便探究下属性,成员变量,实例变量c++
iOS中的内存管理方案,大体能够分为两类:MRC
(手动内存管理)和ARC
(自动内存管理)面试
MRC
时代,系统是经过对象的引用计数来判断一个是否销毁,有如下规则建立时
引用计数都为1
被其余指针引用
时,须要手动调用[objc retain]
,使对象的引用计数+1
[objc release]
来释放对象
,使对象的引用计数-1
引用计数为0
时,系统就会销毁
这个对象【总结】:在MRC模式下,必须遵照:谁建立
,谁释放
,谁引用
,谁管理
数组
ARC
模式是在WWDC2011
和iOS5引入的自动管理机制
,即自动引用计数,是编译器的一种特性ARC模式下不须要手动retain、release、autorelease。编译器会在适当的位置插入release和autorelease
。以前在OC基础知识点之-内存管理初识(内存分区)介绍了内存的五大区,其实除了五大区还有内核区
和保留区
,以4G手机为例,以下图所示:系统将其中的3GB给了五大区+保留区
,剩余的1GB给内核区使用
安全
内核区
:系统用来进行内核处理操做的区域保留区
:预留给系统处理nil等【说明】:之因此最后的内存地址是从0x00400000
开始的,是由于0x00000000表示nil
,不能直接用nil
表示一个段,因此单独给一段内存用于处理nil
等状况markdown
内存管理方案除了前文说起的MRC
和ARC
,还有如下三种app
Tagged Pointer
:专门用来处理小对象
,例如NSNumber、NSDate、小NSString等Nonpointer_isa
:非指针类型的isa,主要是用来优化64位地址
。这个在OC底层原理之-OC对象(下)isa指针结构分析对isa进行了介绍SideTables
:散列表,在散列表中主要有两个表
,分别是引用计数表
、弱引用表
这里主要介绍Tagged Pointer
和SideTables
ide
咱们经过一个面试题来引入Tagged Pointer
上面代码运行有没有问题,为何?函数
运行上面的代码咱们发现taggedPointerDemo
是正常
的,可是在touchesBegan
方法出现了崩溃
(taggedPointerDemo执行在viewDidLoad方法中) oop
崩溃缘由是
多条线程
对同一个对象
进行释放
,致使对象过分释放
,因此才会崩溃。
【思考】:taggedPointerDemo和touchesBegan内部实现基本同样,惟一的区别就是nameStr不同,可是一个没有任何问题,一个却崩溃了,是否是由于nameStr形成的呢?
咱们先看下nameStr有什么不同的地方,在NSLog处打断点,运行代码,分别打印nameStr
咱们发现他们的类型不一样,
taggedPointerDemo
中的nameStr
的类型是NSTaggedPointerString类型
,而touchesBegan
中的类型是__NSCFString
类型。
NSTaggedPointerString类型
的小对象
,存储在常量区
,由于nameStr在alloc本来分配是在堆区
,可是因为taggedPointerDemo的nameStr较小
,通过iOS优化
,就成了NSTaggedPointerString类型
,存在常量区
touchesBegan
方法中的nameStr
类型是NSCFString
类型,存储在堆区
上咱们能够经过NSString初始化的两种方式,来测试NSString的内存管理
withString+@""
方式初始化WithFormat
方式初始化 运行结果:
经过打印咱们能够看到NSString的`内存管理``主要分为3种
NSTaggedPointerString
:标签指针,是苹果在64位
环境下对NSString、NSNumber
等对象作的优化
。对于NSString对象来讲
字符串是由数字、英文字母组合且长度小于等于9
时,会自动成为NSTaggedPointerString
类型,存储在常量区
中文或者其余特殊符号
时,会直接成为__NSCFString
类型,存储在堆区
__NSCFString
:是在运行时
建立的NSString子类
,建立后引用计数会加1
,存储在堆上
__NSCFConstantString
:字符串常量
,是一种编译时常量
,retainCount值很大
,对其操做,不会引发引用计数变化,存储在字符串常量区
上面咱们经过面试题引出了Tagged Pointer,那么我下面就来探究下Tagged Pointer底层实现,看看为何Tagged Pointer类型不会存在过分释放问题,咱们进入objc源码中查看
查看reallySetProperty源码,后面咱们会仔细将reallySetProperty方法
看到不过
不是copy修饰
就会经过objc_retain赋新值
,objc_release释放旧值
,再看objc_retain,objc_release底层实现
经过源码咱们能够看到,在
objc_retain
,objc_release
中都对agged Pointer
进行了判断
,若是小对象
,就直接返回
。由此咱们能够得出一个结论:小对象是不会进行retain和release操做的
咱们继续以NSString为例,对于NSString来讲
NSString对象指针
,都是string值 + 指针地址
,二者是分开
的Tagged Pointer指针
,其指针+值
,都能在小对象中体现
。因此Tagged Pointer 既包含指针,也包含值
在以前的文章OC底层原理之-类的加载过程-上( _objc_init实现原理)中讲类加载时,其中_read_images
源码有一个方法对小对象进行了处理
,即initializeTaggedPointerObfuscator方法
,下面咱们查看下initializeTaggedPointerObfuscator
方法实现 在iOS12后,Tagged Pointer采用了混淆处理,咱们能够设置OBJC_DISABLE_TAG_OBFUSCATION为YES来关闭Tagged Pointer的混淆
咱们能够经过源码中objc_debug_taggedpointer_obfuscator查找taggedPointer的编码和解码,来查看底层是如何混淆处理的
上面咱们知道编码
_objc_encodeTaggedPointer
是经过objc_debug_taggedpointer_obfuscator异或传入值
,解码_objc_decodeTaggedPointer
也是经过objc_debug_taggedpointer_obfuscator异或传入值
,至关因而两层异或
。下面咱们举例说明:传入值1010 0100,mask值0101 0010
1010 0100
^0101 0010 mask (编码)
1111 0110
^0101 0010 mask (解码)
1010 0100
复制代码
咱们看下解码后的小对象地址,其中61
表示a的ASCII码
,63
表示c的ASCII码
,咱们再以NSNumber为例 咱们看到
地址确实存储值
了。可是小对象后面的0xa,0xb
又是什么含义呢?最后咱们在判断是否为小对象的判断里
找到了答案 因此
0xa、0xb
主要是用于判断是不是小对象TaggedPointer
,判断第64位
上是否有为1
(taggedpointer
指针地址即表示指针地址,也表示值)
0xa
转换成二进制为1 010
(64为为1,63~61后三位表示tagType类型
-2
),表示NSString类型
0xb
转换为二进制为1 011
(64为为1,63~61后三位表示tagType类型
-3
),表示NSNumber类型
,这里须要注意一点,若是NSNumber
的值是-1
,其地址中的值是用补码
表示的这里能够经过_objc_makeTaggedPointer
方法的参数tag类型objc_tag_index_t
进入其枚举,其中2表示NSString
,3表示NSNumber
验证下:咱们能够定义一个
NSDate对象
,来验证其tagType
是否为6
。 经过打印结果,其地址高位是
0xe
,转换为二进制为1 110
,排除64位的1,剩余的3位正好转换为十进制是6
,符合上面的枚举值
Tagged Pointer
小对象类型(用于存储NSNumber、NSDate、小NSString
),小对象指针再也不是简单的地址,而是地址 + 值
,即真正的值
,因此,实际上它再也不是一个对象
了,它只是一个披着对象外衣的普通变量
而以。因此能够直接进行读取。优势是占用空间小,节省内存
Tagged Pointer
小对象,不会进入retain和release
,而是直接返回了,意味着不须要ARC进行管理
,因此能够直接被系统自主的释放和回收
Tagged Pointer
的内存并不存储在堆区
中,而是在常量区
中,也不须要malloc和free
,因此能够直接读取
,相比存储在堆区的数据读取,效率
上快了3倍
左右。建立
的效率
相比堆区快
了近100倍
左右。taggedPointer的内存管理方案,比常规的内存管理,要快不少
Tagged Pointer
的64位地址中,前4位表明类型
,后4位主要适用于系统作一些处理
,中间56位用于存储值
NSString
来讲,当字符串较小
时,建议直接经过@""
初始化,由于存储在常量区
,能够直接
进行读取
。会比WithFormat初始化方式更加快速
咱们在ViewController.h文件写的有以下属性
@interface ViewController ()
{
Man *man;
NSString *workTime;
NSInteger times;
}
@property (nonatomic, strong)Student *student;
@property (nonatomic, copy)NSString *schoolName;
@property (nonatomic, copy)NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong)NSMutableArray *houses;
@end
复制代码
属性:有前缀 @property修饰的变量
成员变量:就是{}内的变量,上面咱们写的man,workTime,times都是成员变量
实例变量:若是成员变量的数据类型是个类,能被实例化,那它就是实例变量,上面咱们写的man就是实例变量
经过clang转成.cpp文件。咱们在它的.cpp文件发现属性他们在c++底层是如图(截取部分) 这是属性的get和set方法,发现name的set方法和age,houses有区别(区别用下划线标记出来了)
缘由是name1使用的是copy,二age用的assign,houses用的strong
。
咱们探究下为何会使用objc_setProperty,咱们去先取LLVM源码中找一下objc_setProperty,咱们找到了getOptimizedSetPropertyFn放发,看下图: 经过图咱们看到对属性使用不一样的修饰词,对象的set方法修饰也不一样
使用natomic和copy修饰:objc_setProperty_atomic_copy
使用natomic和非copy修饰:objc_setProperty_atomic
使用非natomic和copy修饰:objc_setProperty_nonatomic_copy
使用非natomic和非copy修饰:objc_setProperty_nonatomic
咱们的name是非natomic和copy修饰,因此应该是objc_setProperty_nonatomic_copy 下面咱们再去源码中查看下objc_setProperty_nonatomic_copy如何实现。 上图就是咱们找到的源码实现,咱们注意到方法reallySetProperty,去这个方法看看
上图
咱们知道若是是copy就是调用copyWithZone方法
,因为不是atomic,因此会走90,91这里就是取出以前的值,将新值赋给*slot,最后将旧值释放
。 下面咱们代码验证下
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [Person alloc];
p.name = @"小明";
p.name = @"小张";
NSLog(@"--->");
NSString *nameStr = [p.name copy];
}
return 0;
}
复制代码
运行代码,打断点 打印由于小明是第一次赋值,因此不存在旧值。咱们继续
此次
再给name赋值小张
,旧值就存在
了。最后在100行会将旧值释放掉
。 咱们再看下NSString *nameStr = [p.name copy]
;咱们发现并没有走
咱们打断点的reallySetProperty
方法,那这个方法会走哪呢?咱们打断点,在断点停留处看看汇编(截取关键部分) 咱们发现后面会调用
objc_storeStrong
,下面咱们在源码中找一下该方法 打断点,再也不看汇编,继续下一步
但此时的
obj是nil
,由于以前不存在nameStr
,objc_retain(id obj)
方法里进行判断,若是存在就返回
,不存在就进行建立
(经过objc_msgSend的方式发送retain方法)。咱们发现调用objc_retain方法
,下面咱们探究下strong属性(objc_retain后面讲)
咱们准备下代码:
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [Person alloc];
p.name = @"aaa";
}
return 0;
}
复制代码
打断点,当来到这里是,咱们查看汇编 咱们发现这个方法和上面讲的nameStr同样,都会走objc_storeStrong
此时咱们打印,发现此时
·obj为咱们对name的赋值
。接着对obj进行objc_retain
。
上面咱们知道对属性进行copy
,以及属性使用strong修饰
,都会走objc_retain
,咱们看看objc_retain
究竟作了些什么属性使用copy修饰是不走objc_retain
。 咱们全局搜objc_retain,发现以下代码
- 1586:若是obj不存在,就直接返回
下面看下retain()方法
- 455:判断不是Tagged Pointer对象,这个方法不但愿处理Tagged Pointer
hasCustomRR函数检查类(包括其父类)
中是否含有默认的retain方法
没有调用rootRetain()
。有就进行消息转发
,去调用自定义的retain方法
走下来咱们发现只会走461行,不会走458行。这是由于hasCustomRR再检查的时候,会经过isa指针一直向上查找,直到找到NSObject,在NSObject中重写了retain方法
而
_objc_rootRetain
会调用rootRetain()
方法。 下面咱们看下rootRetain()方法 发现调用了
rootRetain(false, false)注意传值:false,false
,下面咱们看下rootRetain方法。 rootRetain的方法比较多,咱们会挑比较重要的地方进行解释说明
是Tagged Pointer直接返回
transcribeToSideTable
用于表示extra_rc是否溢出
,默认为false
(不抄写到SideTable)atomic(原子性)获取isa
isa是否是被优化过nonpointer
就是isa结构体中的nonpointer
,具体看OC底层原理之-OC对象(下)isa指针结构分析清空对象的isa.bits
元类,说明是类对象
,就直接返回
tryRetain
为false且sideTable被锁
,就打开锁
(retain必需要为true
,若是当前线程锁住了sideTable对象
,则须要解锁
)。判断tryRetain
是否为true
来肯定retain是否成功
,若是成功,就判断sidetable_retain()
是否存在,若是存在就返回this
,若是不存在就返回nil
。若是失败
继续往下走调用sidetable_retain()
。newisa的bits进行处理
,进行addc操做
(注释说对引用计数进行+1
,RC_ONE
是1想左偏移56
位,而isa指针的extra_rc
:引用计数,在63-56之间
。注:在x86
下)有进位,说明溢出
了,这时候表示extra_rc已经不能存储在isa指针中
了。若是不处理溢出状况
,在520行对bits进行清空
,在521行再次调用rootRetain
,此时的handleOverflow的会被rootRetain_overflow置为true
。从而直接走下对sideTable进行加锁
,528行是将引用计数减半
(RC_HALF为1左移7为,extra_rc满为8位,少了一位就是减半),继续存在isa中
,529行将has_sidetable_rc设置为true
,代表借用了sideTable存储
transcribeToSideTable置为true
。因此若是溢出了就会进来
。一半放在isa指针中,另外一半就存在sideTable中
。tryRetain为false
,SideTable锁了
,那就解锁
。经过上面的解释咱们能够肯定几个问题:1.调用rootRetain会让引用计数+1. 2.当引用计数过大溢出时,会将引用计数一半存在isa的extra_rc中,另外一半存在sideTable中
咱们上面说了,属性直接copy以及strong修饰
属性赋值时都会调用rootRetain
,说明对对象copy以及strong修饰属性赋值时都会致使引用计数+1
。由于strong是强引用,因此+1
,而copy修饰属性
,只是调用copyWithZone
,而不可变对象进行copy时
是对内存地址进行copy
,也就是此处也指向该内存,因此须要+1
。 可看下图 咱们看到内存地址是同样的。
上面咱们说了引用计数存储到必定的值
时,就不会存在isa指针中的extra_rc
中,而是将一半存到SideTables散列表
中,为何是将一半存在SideTables而不是所有呢?
缘由:若是都存储在散列表中,每次对散列表操做都须要开解锁,操做耗时,消耗性能大,因此对半分的操做目的是为了提升性能
。 咱们看下sidetable_addExtraRC_nolock源码 发现获取
SideTable
是从SideTables
取的,说明SideTable是有多张的
散列表只有一张表
,意味着全局全部的对象
都会存储在一张表中
,操做任意一个对象
,都会进行解锁
(锁是锁整个表的读写)。当开锁
时,其它对象可能也操做这张表,则意味着数据不安全
每一个对象都开一个表
,会耗费性能
,因此也不能有无数个表
咱们看下SideTable结构,SideTables的底层实现
咱们发现
sideTable
包含互斥锁slock
,引用计数表refcnts
,以及一个弱引用表weak_table
,而SideTables
是经过SideTablesMap的get方法获取
,而SideTablesMap
是经过StripedMap<SideTable>定义
的。咱们再看下StripedMap源码
从这里能够看到,同一时间,真机中的
散列表最多只能有8张
数组
:特色在于查询方便(即经过下标访问)
,增删比较麻烦
(相似于以前讲过的methodList
,经过memcopy、memmove增删
,很是麻烦),因此数组的特性是读取快,但存储不方便
链表
:特色在于增删方便
,查询慢
(须要从头节点开始遍历查询
),因此链表的特性是存储快,但读取慢
散列表
:其本质就是一张哈希表
,哈希表集合了数组和链表的长处
,增删改查都比较方便
,例如拉链哈希表
(在以前锁的文章中,讲过的tls
的存储结构就是拉链形式
的),是最经常使用的链表上面对对象的copy,strong(retain是同样的,都会调用retain()方法,结合上面的小对象。咱们总结下retain()做了什么操做
retain
在底层首先会判断是不是Nonpointer isa
,若是不是,则直接操做散列表 进行+1操做
是Nonpointer isa
,还须要判断是否正在释放
,若是正在释放,则执行dealloc流程
,释放弱引用表和引用计数表,最后free释放对象内存不是正在释放,则对Nonpointer isa进行常规的引用计数+1
。这里须要注意一点的是,extra_rc在真机上只有8位用于存储引用计数的值
,当存储满了
时,须要借助散列表用于存储
。须要将满了的extra_rc对半分
,一半(即2^7)存储在散列表中
。另外一半仍是存储在extra_rc中
,用于常规的引用计数的+1或者-1操做
,而后再返回上面分析了+1操做,下面分析下-1操做:release,看下release底层实现 上面的reallySetProperty
最后对旧值进行objc_release
,咱们就从objc_release
开始,objc_release->release()->rootRelease()->rootRelease()
大体上和上面的rootRetain方法相似,只不过是相反的操做。下面简要分析一下
Nonpointer isa
,若是不是,则直接对散列表进行-1操做
Nonpointer isa
,则对extra_rc
中的引用计数值进行-1操做
,并存储此时的extra_rc状态到carry
中carray为0
,则走到underflow流程
underflow
流程有如下几步:
散列表
中是否存储了一半的引用计数
散列表
中取出
存储的一半引用计数,进行-1操做
,而后存储到extra_rc中
extra_rc没有值
,散列表中也是空
的,则直接进行析构
,即dealloc操做
,属于自动触发
在retain和release的底层实现中,都说起了dealloc析构函数,下面来分析dealloc的底层的实现,经过delloc->_objc_rootDealloc->rootDealloc
判断是否有isa、cxx、关联对象、弱引用表、引用计数表
,若是没有
,则直接free释放内存
有
,则进入object_dispose方法
经过上面能够看到,object_dispose方法的目的有一下几个
到如今为止,retain -> release -> dealloc就所有串联起来了
上面提到了retainCount,咱们来看下retainCount底层是如何操做的,咱们先看个面试题 问:打印结果是多少?答案:1,为何?若是回答由于NSObject被alloc了,因此引用计数+1,那么你是说对告终果,可是不知道缘由
咱们在文章对alloc理解中历来没说过allock会对引用计数+1
。那为何答案会是1呢,下面咱们来分析下
上面就是retainCount源码,咱们在rootRetainCount打断点,进行调试
答案:
alloc
建立的对象实际的引用计数为0
,其引用计数打印结果为1
,是由于在底层rootRetainCount
方法中,引用计数默认+1
了,可是这里只有
对引用计数的读取
操做,是没有写入
操做的,简单来讲就是:为了防止alloc建立的对象被释放(引用计数为0会被释放),因此在编译阶段,程序底层默认进行了+1操做。实际上在extra_rc中的引用计数仍然为0
alloc
建立的对象没有retain和release
alloc
建立对象的引用计数为0
,会在编译时期
,程序默认加1
,因此读取引用计数时为1咱们再看上面的.cpp文件发现以下: 咱们看到
每一个属性都有一个get方法和set方法
咱们解释下红框部分的意思,红框是签名,以
@16@0:8
为例,
第一个@是返回值为id类型
,16表示的返回值为16字节
,第二个@表示第一个参数
"v24@0:8@16",v指无返回值
为了方便理解,我附上一张官方解释的图,以及官方链接,你们能够去官方看更详细的解释。 附:Type Encoding-官方文档 ,Property Type String-官方文档