OC底层知识点之 - 内存管理(上)

系列文章:OC底层原理系列OC基础知识系列html

前言

以前在使用clang将.m文件转成.cpp文件,查看里面的内容,发现属性上的编译也颇有意思,因此本篇探究下iOS内存管理顺便探究下属性,成员变量,实例变量c++

ARC & MRC

iOS中的内存管理方案,大体能够分为两类:MRC(手动内存管理)和ARC(自动内存管理)面试

  • MRC
    • MRC时代,系统是经过对象的引用计数来判断一个是否销毁,有如下规则
    • 对象被建立时引用计数都为1
    • 当对象被其余指针引用时,须要手动调用[objc retain],使对象的引用计数+1
    • 当指针变量再也不使用对象时,须要手动调用[objc release]释放对象,使对象的引用计数-1
    • 当一个对象的引用计数为0时,系统就会销毁这个对象

【总结】:在MRC模式下,必须遵照:谁建立谁释放谁引用谁管理数组

  • ARC
    • ARC模式是在WWDC2011和iOS5引入的自动管理机制,即自动引用计数,是编译器的一种特性
    • 规则与MRC一致,区别在于,ARC模式下不须要手动retain、release、autorelease。编译器会在适当的位置插入release和autorelease

内存布局

以前在OC基础知识点之-内存管理初识(内存分区)介绍了内存的五大区,其实除了五大区还有内核区保留区,以4G手机为例,以下图所示:系统将其中的3GB给了五大区+保留区,剩余的1GB给内核区使用 安全

  • 内核区:系统用来进行内核处理操做的区域
  • 保留区:预留给系统处理nil等

【说明】:之因此最后的内存地址是从0x00400000开始的,是由于0x00000000表示nil,不能直接用nil表示一个段,因此单独给一段内存用于处理nil等状况markdown

内存管理方案

内存管理方案除了前文说起的MRCARC,还有如下三种app

  • 1.Tagged Pointer:专门用来处理小对象,例如NSNumber、NSDate、小NSString等
  • 2.Nonpointer_isa:非指针类型的isa,主要是用来优化64位地址。这个在OC底层原理之-OC对象(下)isa指针结构分析对isa进行了介绍
  • 3.SideTables:散列表,在散列表中主要有两个表,分别是引用计数表弱引用表

这里主要介绍Tagged PointerSideTableside

Tagged Pointer

咱们经过一个面试题来引入Tagged Pointer 上面代码运行有没有问题,为何?函数

运行上面的代码咱们发现taggedPointerDemo正常的,可是在touchesBegan方法出现了崩溃(taggedPointerDemo执行在viewDidLoad方法中) oop

崩溃缘由是多条线程同一个对象进行释放,致使对象过分释放,因此才会崩溃。

【思考】:taggedPointerDemo和touchesBegan内部实现基本同样,惟一的区别就是nameStr不同,可是一个没有任何问题,一个却崩溃了,是否是由于nameStr形成的呢?

验证

咱们先看下nameStr有什么不同的地方,在NSLog处打断点,运行代码,分别打印nameStr 咱们发现他们的类型不一样,taggedPointerDemo中的nameStr的类型是NSTaggedPointerString类型,而touchesBegan中的类型是__NSCFString类型。

  • 1.NSTaggedPointerString类型小对象,存储在常量区,由于nameStr在alloc本来分配是在堆区,可是因为taggedPointerDemo的nameStr较小,通过iOS优化,就成了NSTaggedPointerString类型,存在常量区
  • 2.touchesBegan方法中的nameStr类型是NSCFString类型,存储在堆区

NSString的内存管理

咱们能够经过NSString初始化的两种方式,来测试NSString的内存管理

  • 1.经过withString+@""方式初始化
  • 2.经过WithFormat方式初始化

运行结果: 经过打印咱们能够看到NSString的`内存管理``主要分为3种

  • 1.NSTaggedPointerString:标签指针,是苹果在64位环境下对NSString、NSNumber等对象作的优化。对于NSString对象来讲
    • 字符串是由数字、英文字母组合且长度小于等于9时,会自动成为NSTaggedPointerString类型,存储在常量区
    • 当有中文或者其余特殊符号时,会直接成为__NSCFString类型,存储在堆区
  • 2.__NSCFString:是在运行时建立的NSString子类,建立后引用计数会加1存储在堆上
  • 3.__NSCFConstantString字符串常量,是一种编译时常量retainCount值很大,对其操做,不会引发引用计数变化,存储在字符串常量区

agged Pointer 小对象底层原理

上面咱们经过面试题引出了Tagged Pointer,那么我下面就来探究下Tagged Pointer底层实现,看看为何Tagged Pointer类型不会存在过分释放问题,咱们进入objc源码中查看

小对象的引用计数处理分析

查看reallySetProperty源码,后面咱们会仔细将reallySetProperty方法

看到不过不是copy修饰就会经过objc_retain赋新值objc_release释放旧值,再看objc_retain,objc_release底层实现

经过源码咱们能够看到,在objc_retainobjc_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位上是否有为1taggedpointer指针地址即表示指针地址,也表示值)

  • 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表示NSString3表示NSNumber 验证下:咱们能够定义一个NSDate对象,来验证其tagType是否为6 经过打印结果,其地址高位是0xe,转换为二进制为1 110,排除64位的1,剩余的3位正好转换为十进制是6符合上面的枚举值

Tagged Pointer 总结

  • 1.Tagged Pointer小对象类型(用于存储NSNumber、NSDate、小NSString),小对象指针再也不是简单的地址,而是地址 + 值,即真正的值,因此,实际上它再也不是一个对象了,它只是一个披着对象外衣的普通变量而以。因此能够直接进行读取。优势是占用空间小,节省内存
  • 2.Tagged Pointer小对象,不会进入retain和release,而是直接返回了,意味着不须要ARC进行管理,因此能够直接被系统自主的释放和回收
  • 3.Tagged Pointer内存并不存储在堆区中,而是在常量区中,也不须要malloc和free,因此能够直接读取,相比存储在堆区的数据读取,效率上快了3倍左右。建立效率相比堆区了近100倍左右。taggedPointer的内存管理方案,比常规的内存管理,要快不少
  • 4.Tagged Pointer的64位地址中,前4位表明类型后4位主要适用于系统作一些处理中间56位用于存储值
  • 5.优化内存建议:对于NSString来讲,当字符串较小时,建议直接经过@""初始化,由于存储在常量区,能够直接进行读取。会比WithFormat初始化方式更加快速

探究strong和copy的内存管理

准备代码

咱们在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

探究copy

咱们探究下为何会使用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,由于以前不存在nameStrobjc_retain(id obj)方法里进行判断,若是存在就返回不存在就进行建立(经过objc_msgSend的方式发送retain方法)。咱们发现调用objc_retain方法,下面咱们探究下strong属性(objc_retain后面讲)

探究strong

咱们准备下代码:

@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

探究objc_retain

上面咱们知道对属性进行copy,以及属性使用strong修饰,都会走objc_retain,咱们看看objc_retain究竟作了些什么属性使用copy修饰是不走objc_retain。 咱们全局搜objc_retain,发现以下代码

  • 1586:若是obj不存在,就直接返回
  • 1587:若是obj是Tagged Pointer对象也直接返回
  • 1588:上面都不知足调用retain()

下面看下retain()方法

  • 455:判断不是Tagged Pointer对象,这个方法不但愿处理Tagged Pointer
  • 457:经过hasCustomRR函数检查类(包括其父类)中是否含有默认的retain方法
  • 458:若是没有调用rootRetain()
  • 461:若是有就进行消息转发,去调用自定义的retain方法

走下来咱们发现只会走461行,不会走458行。这是由于hasCustomRR再检查的时候,会经过isa指针一直向上查找,直到找到NSObject,在NSObject中重写了retain方法 _objc_rootRetain会调用rootRetain()方法。 下面咱们看下rootRetain()方法 发现调用了rootRetain(false, false)注意传值:false,false,下面咱们看下rootRetain方法。 rootRetain的方法比较多,咱们会挑比较重要的地方进行解释说明

  • 489:若是是Tagged Pointer直接返回
  • 492:transcribeToSideTable用于表示extra_rc是否溢出,默认为false(不抄写到SideTable)
  • 499:经过atomic(原子性)获取isa
  • 501:isa是否是被优化过nonpointer就是isa结构体中的nonpointer,具体看OC底层原理之-OC对象(下)isa指针结构分析
  • 502:若是不是,就清空对象的isa.bits
  • 503:若是是元类,说明是类对象,就直接返回
  • 504:若是tryRetainfalse且sideTable被锁,就打开锁retain必需要为true,若是当前线程锁住了sideTable对象,则须要解锁)。
  • 505:经过判断tryRetain是否为true来肯定retain是否成功,若是成功,就判断sidetable_retain()是否存在,若是存在就返回this,若是不存在就返回nil。若是失败继续往下走调用sidetable_retain()
  • 515:将newisa的bits进行处理,进行addc操做(注释说对引用计数进行+1RC_ONE1想左偏移56位,而isa指针的extra_rc:引用计数,在63-56之间。注:在x86下)
  • 519:有进位,说明溢出了,这时候表示extra_rc已经不能存储在isa指针中了。
  • 520-521:若是不处理溢出状况,在520行对bits进行清空,在521行再次调用rootRetain,此时的handleOverflow的会被rootRetain_overflow置为true。从而直接走下
  • 525-529:525行就是对sideTable进行加锁,528行是将引用计数减半(RC_HALF为1左移7为,extra_rc满为8位,少了一位就是减半),继续存在isa中,529行将has_sidetable_rc设置为true,代表借用了sideTable存储
  • 533:由于上面讲若是溢出了,就会将transcribeToSideTable置为true。因此若是溢出了就会进来
  • 535:将上面说的引用计数溢出,一半放在isa指针中,另外一半就存在sideTable中
  • 538:若是tryRetain为falseSideTable锁了,那就解锁
  • 539:返回

经过上面的解释咱们能够肯定几个问题:1.调用rootRetain会让引用计数+1. 2.当引用计数过大溢出时,会将引用计数一半存在isa的extra_rc中,另外一半存在sideTable中 咱们上面说了,属性直接copy以及strong修饰属性赋值时都会调用rootRetain,说明对对象copy以及strong修饰属性赋值时都会致使引用计数+1由于strong是强引用,因此+1,而copy修饰属性,只是调用copyWithZone,而不可变对象进行copy时是对内存地址进行copy,也就是此处也指向该内存,因此须要+1 可看下图 咱们看到内存地址是同样的。

SideTables 散列表

上面咱们说了引用计数存储到必定的值时,就不会存在isa指针中的extra_rc中,而是将一半存到SideTables散列表中,为何是将一半存在SideTables而不是所有呢?

缘由:若是都存储在散列表中,每次对散列表操做都须要开解锁,操做耗时,消耗性能大,因此对半分的操做目的是为了提升性能 咱们看下sidetable_addExtraRC_nolock源码 发现获取SideTable是从SideTables取的,说明SideTable是有多张的

问题1.为何在内存中有多张?最多可以多少张?**

  • 若是散列表只有一张表,意味着全局全部的对象都会存储在一张表中,操做任意一个对象,都会进行解锁(锁是锁整个表的读写)。当开锁时,其它对象可能也操做这张表,则意味着数据不安全
  • 若是每一个对象都开一个表,会耗费性能,因此也不能有无数个表

咱们看下SideTable结构,SideTables的底层实现

咱们发现sideTable包含互斥锁slock,引用计数表refcnts,以及一个弱引用表weak_table,而SideTables经过SideTablesMap的get方法获取,而SideTablesMap是经过StripedMap<SideTable>定义的。咱们再看下StripedMap源码

从这里能够看到,同一时间,真机中的散列表最多只能有8张

问题2.为何在用散列表,而不用数组、链表?

  • 数组:特色在于查询方便(即经过下标访问)增删比较麻烦(相似于以前讲过的methodList经过memcopy、memmove增删,很是麻烦),因此数组的特性是读取快,但存储不方便
  • 链表:特色在于增删方便查询慢(须要从头节点开始遍历查询),因此链表的特性是存储快,但读取慢
  • 散列表:其本质就是一张哈希表,哈希表集合了数组和链表的长处增删改查都比较方便,例如拉链哈希表(在以前锁的文章中,讲过的tls的存储结构就是拉链形式的),是最经常使用的链表

上面对对象的copy,strong(retain是同样的,都会调用retain()方法,结合上面的小对象。咱们总结下retain()做了什么操做

总结retain做了什么

  • 1.retain在底层首先会判断是不是Nonpointer isa,若是不是,则直接操做散列表 进行+1操做
  • 2.若是是Nonpointer isa,还须要判断是否正在释放,若是正在释放,则执行dealloc流程,释放弱引用表和引用计数表,最后free释放对象内存
  • 3.若是不是正在释放,则对Nonpointer isa进行常规的引用计数+1。这里须要注意一点的是,extra_rc在真机上只有8位用于存储引用计数的值,当存储满了时,须要借助散列表用于存储。须要将满了的extra_rc对半分一半(即2^7)存储在散列表中另外一半仍是存储在extra_rc中,用于常规的引用计数的+1或者-1操做,而后再返回

release 源码分析

上面分析了+1操做,下面分析下-1操做:release,看下release底层实现 上面的reallySetProperty最后对旧值进行objc_release,咱们就从objc_release开始,objc_release->release()->rootRelease()->rootRelease()

大体上和上面的rootRetain方法相似,只不过是相反的操做。下面简要分析一下

  • 1.判断是不是Nonpointer isa,若是不是,则直接对散列表进行-1操做
  • 2.若是是Nonpointer isa,则对extra_rc中的引用计数值进行-1操做,并存储此时的extra_rc状态到carry
  • 3.若是此时的状态carray为0,则走到underflow流程
  • 4.underflow流程有如下几步:
    • 判断散列表是否存储了一半的引用计数
    • 若是是,则从散列表取出存储的一半引用计数,进行-1操做,而后存储到extra_rc中
    • 若是此时extra_rc没有值散列表中也是空的,则直接进行析构,即dealloc操做,属于自动触发

dealloc 源码分析

在retain和release的底层实现中,都说起了dealloc析构函数,下面来分析dealloc的底层的实现,经过delloc->_objc_rootDealloc->rootDealloc

  • 1.根据条件判断是否有isa、cxx、关联对象、弱引用表、引用计数表,若是没有,则直接free释放内存
  • 2.若是,则进入object_dispose方法

经过上面能够看到,object_dispose方法的目的有一下几个

  • 1.销毁实例,主要有如下操做
    • 1.调用c++析构函数
    • 2.删除关联引用
    • 3.释放散列表
    • 4.清空弱引用表
  • 2.free释放内存

到如今为止,retain -> release -> dealloc就所有串联起来了

retainCount 源码分析

上面提到了retainCount,咱们来看下retainCount底层是如何操做的,咱们先看个面试题 问:打印结果是多少?答案:1,为何?若是回答由于NSObject被alloc了,因此引用计数+1,那么你是说对告终果,可是不知道缘由

咱们在文章对alloc理解历来没说过allock会对引用计数+1。那为何答案会是1呢,下面咱们来分析下 上面就是retainCount源码,咱们在rootRetainCount打断点,进行调试

答案:alloc建立的对象实际的引用计数为0,其引用计数打印结果为1,是由于在底层rootRetainCount方法中,引用计数默认+1了,可是这里只有对引用计数的读取操做,是没有写入操做的,简单来讲就是:为了防止alloc建立的对象被释放(引用计数为0会被释放),因此在编译阶段,程序底层默认进行了+1操做。实际上在extra_rc中的引用计数仍然为0

总结

  • 1.alloc建立的对象没有retain和release
  • 2.alloc建立对象的引用计数为0,会在编译时期,程序默认加1,因此读取引用计数时为1

扩展

咱们再看上面的.cpp文件发现以下: 咱们看到每一个属性都有一个get方法和set方法

咱们解释下红框部分的意思,红框是签名,以@16@0:8为例,

  • 第一个@是返回值为id类型
  • 16表示的返回值为16字节
  • 第二个@表示第一个参数
  • 0表示从0开始,到8(0-8)
  • :是指sel方法编号
  • 8是指8-16

"v24@0:8@16",v指无返回值

为了方便理解,我附上一张官方解释的图,以及官方链接,你们能够去官方看更详细的解释。 附:Type Encoding-官方文档Property Type String-官方文档

相关文章
相关标签/搜索