不一样的系统版本对 App 运行时占用内存的限制不一样,超过限制时,App就会被强制杀死,因此对于内存的要求也就愈来愈高,因此本章来探索一下iOS中的内存管理方案c++
移动端的内存管理技术,主要有 GC(Garbage Collection
,垃圾回收)的标记清除算法和苹果公司使用的引用计数方法。程序员
相比较于 GC 标记清除算法,引用计数法能够及时地回收引用计数为 0 的对象,减小查找次数。可是,引用计数会带来循环引用的问题,好比当外部的变量强引用 Block 时,Block 也会强引用外部的变量,就会出现循环引用。咱们须要经过弱引用,来解除循环引用的问题。面试
另外,在 ARC(自动引用计数)以前,一直都是经过 MRC(手动引用计数)这种手写大量内存管理代码的方式来管理内存,所以苹果公司开发了 ARC 技术,由编译器来完成这部分代码管理工做。可是,ARC 依然须要注意循环引用的问题。当 ARC 的内存管理代码交由编译器自动添加后,有些状况下会比手动管理内存效率低,因此对于一些内存要求较高的场景,咱们仍是要经过 MRC 的方式来管理、优化内存的使用。算法
首先咱们再来回顾一下,OS/iOS系统的内存布局c#
在iOS程序的内存中,从底地址开始,到高地址一次分为:程序区域、数据区域、堆区、栈区。其中程序区域主要是代码段,数据区域包括数据段和BSS段。咱们具体分析一下各个区域所表明的含义数组
new
,alloc
、block
、copy
建立的对象存储在这里,是由开发者管理的,须要告诉系统何时释放内存。ARC下编译器会自动在合适的时候释放内存,而在MRC下须要开发者手动释放。堆区的内存地址通常是0x6开头,从底地址到高地址分配内存空间首先先查看下面代码,age
的打印结果是多少呢?bash
static int age = 10;
@interface Person : NSObject
-(void)add;
+(void)reduce;
@end
@implementation Person
- (void)add {
age++;
NSLog(@"Person内部:%@-%p--%d", self, &age, age);
}
+ (void)reduce {
age--;
NSLog(@"Person内部:%@-%p--%d", self, &age, age);
}
@end
@implementation Person (WY)
- (void)wy_add {
age++;
NSLog(@"Person (wy)内部:%@-%p--%d", self, &age, age);
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"vc:%p--%d", &age, age);
age = 40;
NSLog(@"vc:%p--%d", &age, age);
[[Person new] add];
NSLog(@"vc:%p--%d", &age, age);
[Person reduce];
NSLog(@"vc:%p--%d", &age, age);
[[Person new] wy_add];
}
复制代码
经过打印结果,咱们能够得出这么一个结论:数据结构
静态变量的做用域与对象、类、分类不要紧,只与文件有关系。架构
iOS除了使用ARC来进行自动引用计数之外,还有一些其余的内存优化方案,主要有Tagged Ponter
,NONPOINTER_ISA
,SideTable
3种async
在 2013 年 9 月,苹果推出了 iPhone5s,与此同时,iPhone5s 配备了首个采用 64 位架构的 A7 双核处理器,为了节省内存和提升执行效率,苹果提出了Tagged Pointer
的概念。
使用Tagged Pointer
以前,若是声明一个NSNumber *number = @10;
变量,须要一个占8字节的指针变量number,和一个占16字节的NSNumber对象,指针变量number指向NSNumber对象的地址。这样须要耗费24个字节内存空间。
而使用Tagged Pointer
以后,NSNumber指针里面存储的数据变成了:Tag + Data
,也就是将数据直接存储在了指针中。直接将数据10保存在指针变量number中,这样仅占用8个字节。
可是当指针不够存储数据时,就会使用动态分配内存的方式来存储数据。
经过下面面试题的打印结果能够很好的反映出来
//第1段代码
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 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 (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abc"];
});
}
NSLog(@"end");
复制代码
上面两段代码的打印结果过是: 第1段会发生崩溃,而第2段不会。
按道理讲,建立多个线程来对name
进行操做时,name
的值会不断的retain
和release
,此时就会出现资源竞争而崩溃,可是第二段却不会崩溃,说明在Tagged Pointer
下,较小的值,不会调用set和get等方法,因为其值是直接存储在指针变量中的,因此能够直接修改。
经过源码,咱们也能够比较直观的看出,Tagged Pointer
类型对象是直接返回的。
总结一下使用Tagged Pointer的好处
Tagged Pointer
是专⻔⽤来存储⼩的对象,例如NSNumber
,NSDate
等。Tagged Pointer
指针的值再也不是地址了,⽽是真正的值。因此,实际上它再也不是⼀个对象了,它只是⼀个披着对象⽪的普通变量⽽已。因此,它的内存并不存储在堆中,也不须要malloc
和free
。static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
if (tag <= OBJC_TAG_Last60BitPayload) {
uintptr_t result =
(_OBJC_TAG_MASK |
((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) |
((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
return _objc_encodeTaggedPointer(result);
} else {
uintptr_t result =
(_OBJC_TAG_EXT_MASK |
((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
return _objc_encodeTaggedPointer(result);
}
}
复制代码
从上面的这代码能够看出来,系统调用了_objc_decodeTaggedPointer
和_objc_taggedPointersEnabled
这两个方法对于taggedPointer
对象的指针进行了编码和解编码,这两个方法都是将指针地址和objc_debug_taggedpointer_obfuscator
进行异或操做
咱们都知道将a和b异或操做获得c再和a进行异或操做即可以从新获得a的值,一般可使用这个方式来实现不用中间变量实现两个值的交换。Tagged Pointer
正是使用了这种原理。经过这种解码的方法,咱们能够获得对象真正的指针地址
下面是系统定义的各类Tagged Pointer
标志位。
enum objc_tag_index_t : uint16_t
{
// 60-bit payloads
OBJC_TAG_NSAtom = 0,
OBJC_TAG_1 = 1,
OBJC_TAG_NSString = 2,
OBJC_TAG_NSNumber = 3,
OBJC_TAG_NSIndexPath = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate = 6,
// 60-bit reserved
OBJC_TAG_RESERVED_7 = 7,
// 52-bit payloads
OBJC_TAG_Photos_1 = 8,
OBJC_TAG_Photos_2 = 9,
OBJC_TAG_Photos_3 = 10,
OBJC_TAG_Photos_4 = 11,
OBJC_TAG_XPC_1 = 12,
OBJC_TAG_XPC_2 = 13,
OBJC_TAG_XPC_3 = 14,
OBJC_TAG_XPC_4 = 15,
OBJC_TAG_First60BitPayload = 0,
OBJC_TAG_Last60BitPayload = 6,
OBJC_TAG_First52BitPayload = 8,
OBJC_TAG_Last52BitPayload = 263,
OBJC_TAG_RESERVED_264 = 264
};
复制代码
关于NONPOINTER_ISA
咱们在以前的博客中有讲过,这也是苹果的一种内存优化的方案。用 64 bit 存储一个内存地址显然是种浪费,毕竟不多有那么大内存的设备。因而能够优化存储方案,用一部分额外空间存储其余内容。isa 指针第一位为 1 即表示使用优化的 isa 指针。能够阅读☞iOS底层学习 - OC对象前世此生
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8
};
#endif
};
复制代码
在NONPOINTER_ISA
中有两个成员变量has_sidetable_rc
和extra_rc
,当extra_r
c的19位内存不够存储引用计数时has_sidetable_rc
的值就会变为1,那么此时引用计数会存储在SideTable中。
SideTable
s能够理解为一个全局的hash数组
,里面存储了SideTable
类型的数据,其长度为64,就是里面有64个SideTable。
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
}
复制代码
查看SideTable的源码,其3个成员变量,表明的意义以下:
spinlock_t
:自旋锁,用于上锁/解锁 SideTable。RefcountMap
:用来存储OC对象的引用计数的 hash表(仅在未开启isa优化或在isa优化状况下isa_t的引用计数溢出时才会用到)。weak_table_t
:存储对象弱引用指针的hash表。是OC中weak功能实现的核心数据结构。有关于弱引用表weak_table_t
,能够阅读☞iOS底层学习 - 内存管理之weak原理探究
// RefcountMap disguises its pointers because we
// do not want the table to act as a root for `leaks`.
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,RefcountMapValuePurgeable> RefcountMap;
复制代码
实质上是模板类型objc::DenseMap。模板的三个类型参数DisguisedPtr<objc_object>
、size_t
、true
分别表示DenseMap的 key类型
、value类型
、是否须要在value == 0 的时候自动释放掉响应的hash节点,这里是true。
在MRC时代,程序员须要手动的去管理内存,建立一个对象时,须要在set方法和get方法内部添加释放对象的代码。而且在对象的dealloc里面添加释放的代码。
@property (nonatomic, strong) Person *person;
- (void)setPerson:(Person *)person {
if (_person != person) {
[_person release];
_person = [person retain];
}
}
- (Person *) person {
return _person;
}
复制代码
在ARC环境中,咱们再也不像之前同样本身手动管理内存,系统帮助咱们作了release
或者autorelease
等事情。 ARC是LLVM编译器
和RunTime
协做的结果。其中LLVM编译器自动生成release
、reatin
、autorelease
的代码,像weak
弱引用这些则靠RunTime在运行时释放。
引用计数是一种内存管理技术,是指将资源(能够是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。使用引用计数技术能够实现自动资源管理的目的。同时引用计数还能够指使用引用计数技术回收未使用资源的垃圾回收算法。
在iOS中,对象执行reatin
等操做后,该对象的引用计数就会+1
,调用release
等操做后,改对象的引用计数就会-1
关于引用计数的规则,能够总结以下:
有关alloc
的流程,能够阅读☞iOS底层学习 - OC对象前世此生来进行查看,就不作过多的赘述。
可是有一个细节须要注意,alloc
自己是只申请内存空间,不增长引用计数的。此时isa
中extra_rc
为0。
可是为何咱们打印retainCount
时,显示的是1呢,咱们经过查看源码能够发现uintptr_t rc = 1 + bits.extra_rc;
其自己前面是会加一个常量1的,用来标记本身生成的对象的引用计数。
inline uintptr_t
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
if (bits.nonpointer) {
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
复制代码
经过如下源码,咱们能够知道:
hasCustomRR
,会走消息发送inline id objc_object::retain()
{
assert(!isTaggedPointer());
if (fastpath(!ISA()->hasCustomRR())) {
return rootRetain();
}
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}
复制代码
接着咱们查看rootRetain
方法,其主要处理以下:
TaggedPointer
,若是是则返回。extra_rc
进行加1操做。extra_rc
中已经存储满了,则调用sidetable_addExtraRC_nolock
方法将一半的引用计数移存到SideTable中。ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
✅//若是是TaggedPointer 直接返回
if (isTaggedPointer()) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
isa_t oldisa;
isa_t newisa;
do {
transcribeToSideTable =false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
✅// 若是isa未通过NONPOINTER_ISA优化
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();//引用计数存储于SideTable中
}
// donot check newisa.fast_rr; we already called any RR overrides
✅//检查对象是都正在析构
if (slowpath(tryRetain && newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
return nil;
}
uintptr_t carry;
✅//isa的bits中的extra_rc进行加1
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
✅//若是bits的extra_rc已经存满了,则将其中的一半存储到sidetable中
if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
if (!handleOverflow) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);
}
// Leave half of the retain counts inline and
// prepare to copy the other half to the side table.
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;//extra_rc置空一半的数值
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
✅//将另外的一半引用计数存储到sidetable中
sidetable_addExtraRC_nolock(RC_HALF);
}
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
return (id)this;
}
复制代码
关于release
的相关原理,和retain
的原理相反,你们能够本身去探究一下;
关于dealloc
的相关原理,在iOS底层学习 - 内存管理之weak原理探究中也有解析
栈区
(自动处理内存)、堆区
(开发者管理,ARC下会自动释放)、静态区
(全局变量,静态变量)、数据段
(常量)、代码段
(编写的二进制代码区)Tagged Pointer
NONPOINTER_ISA
nonpointer
标志是否开启指针优化extra_rc
来存储引用计数,根据系统架构不一样,长度不一样has_sidetable_rc
来判断超出extra_rc
后,是否有全局SideTable
存储引用计数SideTable
SideTables
是一个全局的哈希数组,里面存储了SideTable
类型数据SideTable
由spinlock_t
(自旋锁,用于上/解锁)、RefcountMap
(存储extra_rc
溢出或者未开启优化的引用计数)、weak_table_t
(存储弱引用表)rootRetainCount
其自己前面是会加一个常量1的,用来标记本身生成的对象的引用计数。TaggedPointer
,若是是则返回。NONPOINTER_ISA
优化,若是未通过优化,则将引用计数存储在SideTable
中。64位的设备不会进入到这个分支。extra_rc
中已经存储满了,则调用sidetable_addExtraRC_nolock
方法将一半的引用计数移存到SideTable中。