在研究Hash表的过程当中,想看iOS当中有哪些场景应用,最为你们所知的应该就是weak关键字的底层原理,利用网上的资料深究了一下,同时更进一步了解到了iOS内存管理方面的知识,因此但愿本身可以保留这份记忆,就记录一下。ios
Hash或者说散列表,它是一种基础数据结构,这里为何会说到它,由于我感受理解了Hash对weak关键字底层的理解有很大的帮助。编程
Hash表是一种特殊的数据结构,它同数组、链表以及二叉树等相比有很明显的区别,可是它又是在数组和链表的基础上演化而来。数组
Hash表的本质是一个数组,数组中每个元素称为一个箱子,箱子中存放元素。
存储过程以下:安全
Hash表采用一个映射函数f:key->address将关键字映射到该记录在表中存储位置,从而想要查找该记录时,能够直接根据关键字和映射关系计算出该记录在表中的存储位置,一般状况下,这种映射关系称做Hash函数,而经过Hash函数和关键字计算出来的存储位置(这里的存储位置只是表中的存储位置,并非实际的物理地址)称做Hash地址。bash
先看一个列子: 假如联系人信息采用Hash表存储,当想要找到“lisi”的信息时,直接根据“lisi”和Hash函数计算出Hash地址便可。 由于咱们是用数组大小对哈希值进行取模,有可能不一样的键值产生的索引值相同,这就是所谓的冲突。 markdown
在使用分离连接法解决哈希冲突时,每一个箱子实际上是一个链表,将属于同一个箱子里的元素存储在一张线性表中,而每张表的表头的序号即为计算获得的Hash地址,以下图最左边是数组结构,数组内的元素为链表结构。 网络
这里的Hash表咱们只作简单的了解,想要详细了解的请参考:
笔记-数据结构之 Hash(OC的粗略实现)
深刻理解哈希表
哈希算法详解数据结构
其实不论ARC仍是MRC都遵循该方式,只是在ARC模式下这些工做被编译器作了多线程
苹果的实现:(这部份内容是根据 《Objective-C高级编程 iOS与OS X多线程和内存管理》 来的)
- retainCount
__CFDoExternRefOperation
CFBasicHashGetCountOfKey
复制代码
- retain
__CFDoExternRefOperation
CFBasicHashAddValue
复制代码
- release
__CFDoExternRefOperation
CFBasicHashRemoveValue
(CFBasicHashRemoveValue返回0时,-release调用dealloc)
复制代码
各个方法都经过同一个调用来__CFDoExternRefOperation
函数,调用来一系列名称类似的函数。如这些函数名的前缀“CF”所示,它们包含于Core Foundation
框架源代码中,便是CFRuntime.c
的__CFDoExternRefOperation
函数。
__CFDoExternRefOperation
函数按retainCount/retain/release
操做进行分发,调用不一样的函数,NSObject类的retainCount/retain/release
实例方法也许以下面代码所示:
- (NSUInteger)retainCount { return (NSUInteger)__CFDoExternRefOperation(OPERATION_retainCount,self); } - (id)retain { return (id)__CFDoExternRefOperation(OPERATION_retain,self); } - (void)release { return __CFDoExternRefOperation(OPERATION_release,self); } 复制代码
int __CFDoExternRefOperation(uintptr_r op,id obj) { CFBasicHashRef table = 取得对象对应的散列表(obj); int count; switch(op) { case OPERATION_retainCount: count = CFBasicHashGetCountOfKey(table,obj); return count; case OPERATION_retain: CFBasicHashAddValue(table,obj); return obj; case OPERATION_release: count = CFBasicHashRemoveValue(table,obj): return 0 == count; } } 复制代码
从上面代码能够看出,苹果大概就是采用散列表(引用计数表)来管理引用计数,当咱们在调用retain、retainCount、release
时,先调用_CFDoExternRefOperation()
从而获取到引用计数表的内存地址以及本对象的内存地址,而后根据对象的内存地址在表中查询获取到引用计数值。
如果retain
则加1,如果retainCount
就直接返回值,如果release
则减1。(在CFBasechashRemoveValue
中将引用计数减小到0时会调用dealloc
废弃对象)
做用: autorelease
做用是将对象放入自动释放池中,当自从释放池销毁时对自动释放池中的对象都进行一次release操做。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];
复制代码
原理: ARC下,使用@autoreleasepool{}
来使用一个AutoreleasePool
,随后编译器会改为下面的样子:
void *context = objc_autoreleasePoolPush();
// 执行的代码
objc_autoreleasePoolPop(context);
复制代码
而这两个函数都是对AutoreleasePoolPage
的简单的封装,因此自动释放机制的核心就在于这个类。 AutoreleasePoolPage
是一个C++实现的类
AutoreleasePool
并无单独的结构,而是由若干个AutoreleasePoolPage
以双链表的形式组合而成(分别对应结构中的parent
指针和child
指针)AutoreleasePool
是按线程一一对应的(结构中的thread
指针指向当前线程)AutoreleasePoolPage
每一个对象开辟一个虚拟内存一页的大小,除了上面实例变量所占空间,剩下的空间所有用来存储autorelease
对象的地址id *next
指针做为游标指向栈顶最新add进来的autorelease
对象的下一个位置AutoreleasePoolPage
的空间被占满时,会新建一个AutoreleasePoolPage
对象,链接链表,后来的autorelease
对象在新的page加入因此,若当前线程中只有一个AutoreleasePoolPage
对象,并记录了不少autorelease
对象地址时内存以下:
autorelease
对象就要满了(也就是
next
指针立刻指向栈顶),这时就要执行上面说的操做,创建下一页page对象,与这一页链表连接完成后,新page的
next
指针被初始化在栈底(
begin
的位置),而后继续向栈顶添加新对象。
因此,向一个对象发送- autorelease
消息,就是将这个对象加入到当前AutoreleasePoolPage
的栈顶next
指针指向的位置
每当执行一个objc_autoreleasePoolPush
调用时,runtime
向当前的AutoreleasePoolPage
中add
进一个哨兵对象
,值为0(也就是nil
),那么page就变成了下面的样子:
objc_autoreleasePoolPush
的返回值正式这个哨兵对象的地址,被
objc_autoreleasePoolPop(哨兵对象)
做为入参,
autorelease
对象都发送一次- release
消息,并向回移动next
指针到正确位置刚才的objc_autoreleasePoolPop
执行后,最终变成了下面样子:
__strong
表示强引用,指向并持有该对象。该对象只要引用计数不为0,就不会被销毁。若是在声明引用时,不加修饰符,那么引用将默认为强引用。
alloc、new、copy、mutableCopy
来分配内存的id __strong obj = [[NSObject alloc] init];
复制代码
编译器会转换成下面代码:
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
// ...
objc_release(obj);
复制代码
当使用alloc、new、copy、mutableCopy
进行对象内存分配时,强指针直接指向一个引用计数为1的对象
id __strong obj = [NSMutableArray array];
复制代码
在这种状况下,obj
也指向一个引用计数为1的对象内存。编译器会转换成下面代码:
id obj = objc_msgSend(NSMutableArray, @selector(array));
//替代咱们调用retain方法,是obj持有该对象
objc_retainAutoreleaseReturnValue(obj);
objc_release(obj);
复制代码
从而使得obj指向了一个引用计数为1的对象,不过, objc_retainAutoreleaseReturnValue
有一个成对的函数objc_autoreleaseReturnValue
,这两个函数能够用于最优化程序的运行,代码以下:
+ (id)array { return [[NSMutableArray alloc] init]; } 复制代码
编译器转换以下:
+ (id)array { id obj = objc_msgSend(NSMutableArray,@selector(alloc)); objc_msgSend(obj,@selector(init)); // 代替咱们调用autorelease方法 return objc_autoreleaseReturnValue(obj); } 复制代码
其实autorelease
这个开销不小,runtime
机制解决了这个问题。
优化
Thread Local Storage(TLS)
线程局部存储,目的很简单,将一块内存做为某个线程专有的存储,以key-value
的形式进行读写,好比在非arm架构下,使用pthread
提供的方法实现:
void *pthread_getspecific(pthread_key_t);
int pthread_setspecific(pthread_key_t, const void *);
复制代码
在返回值身上调用objc_autoreleaseReturnValue
方法时,runtime
将这个返回值object
储存在TLS
中,而后直接返回这个object
(不调用autorelease
),同时,在外部接收这个返回值的objc_retainAutoreleaseReturnValue
里,发现TLS
中正好存在这个对象,那么直接返回这个object
(不调用retain
)。 因而乎,调用方和被调用利用TLS
作中转,颇有默契的免去了对返回值的内存管理。
关系图以下:
__weak
表示弱引用,弱引用不会影响对象的释放,而当对象被释放时,全部指向它的弱引用都会自动被置为nil
,这样能够防止野指针。
id __weak obj = [[NSObject alloc] init];
复制代码
根据咱们的了解,能够知道obj
对象在生成以后立马就会被释放,主要缘由是由于__weak
修饰的指针没有引发对象内部的引用计数发生变化。
__weak
的几个使用场景:
weak实现原理的归纳:
Runtime
维护了一个weak
表,用于存储指向某个对象的全部weak
指针。weak
表实际上是一个Hash(哈希)表(这就是为何在本文开始我要简单介绍一下Hash表的缘由),Key
是所指对象的地址,Value
是weak
指针的地址(这个地址的值是所指对象的地址)数组。
weak
的实现原理能够归纳成三步:
runtime
会调用objc_initWeak
函数,初始化一个新的weak
指针指向对象的地址。objc_initWeak
函数会调用objc_storeWeak()
函数,objc_storeWeak()
的做用是更新指针指向,建立对应的弱引用表。clearDeallocating
函数。clearDeallocating
函数首先根据对象地址获取全部weak
指针地址的数组,而后遍历这个数组把其中的数据设为nil
,最后把这个entry
从weak
表中删除,最后清理对象的记录。weak表
weak
表是一个弱引用表,实现为一个weak_table
结构体
struct weak_table_t { weak_entry_t *weak_entries; // 保存来全部指向指定对象的weak指针 weak_entries的对象 size_t num_entries; // weak对象的存储空间 uintptr_t mask; // 参与判断引用计数辅助量 uintptr_t max_hash_displacement;// hash key 最大偏移值 }; 复制代码
这是一个全局弱引用Hash表。使用不定类型对象的地址做为key
,用weak_entry_t
类型结构体对象做为value
,其中的weak_entries
成员,从字面意思上看,即为弱引用表的入口。
weak
全局表中的存储weak
定义的对象的表结构weak_entry_t
,weak_entry_t
是存储在弱引用表中的一个内部结构体,它负责维护和存储指向一个对象的全部弱引用Hash表。定义以下:
typedef objc_object ** weak_referrer_t; struct weak_entry_t { DisguisedPtr<objc_object> referent; //范型 union { struct { weak_referrer_t *referrers; uintptr_t out_of_line : 1; uintptr_t num_refs : PTR_MINUS_1; uintptr_t mask; uintptr_t max_hash_displacement; }; struct { // out_of_line=0 is LSB of one of these (don't care which) weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]; }; } }; 复制代码
即:
weak_table_t
(weak
全局表):采用Hash表的方式把全部weak
引用的对象,存储全部引用weak
对象。weak_entry_t
(weak_table_t
表中Hash表的value
值,weak
对象体):用于记录Hash表中weak
对象。objc_objct
(weak_entry_t
对象中的范型对象,用于标记对象weak
对象):用于标示weak
引用对象。下面详细看下weak
底层实现原理:
id __weak obj = [[NSObject alloc] init];
复制代码
编译器转换后代码以下:
id obj;
id tmp = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(tmp,@selector(init));
objc_initWeak(&obj,tmp);
objc_release(tmp);
objc_destroyWeak(&obj);
复制代码
对于objc_initWeak()
的实现:
id objc_initWeak(id *location, id newObj) { // 查看对象实例是否有效,无效对象直接致使指针释放 if (!newObj) { *location = nil; return nil; } // 存储weak对象 return storeWeak(location, newObj); } 复制代码
存储weak
对象的方法:
/** * This function stores a new value into a __weak variable. It would * be used anywhere a __weak variable is the target of an assignment. * * @param location The address of the weak pointer itself * @param newObj The new object this weak ptr should now point to * * @return \e newObj */ id objc_storeWeak(id *location, id newObj) { // 更新弱引用指针的指向 id oldObj; SideTable *oldTable; SideTable *newTable; spinlock_t *lock1; #if SIDE_TABLE_STRIPE > 1 spinlock_t *lock2; #endif // Acquire locks for old and new values. // Order by lock address to prevent lock ordering problems. // Retry if the old value changes underneath us. /** 获取新值和旧值的锁存位置(用地址做为惟一标示) 经过地址来创建索引标志,防止桶重复 下面指向操做会改变旧值 */ retry: // 更改指针,得到以oldObj为索引所存储的值地址 oldObj = *location; oldTable = SideTable::tableForPointer(oldObj); // 更改新值指针,得到以newObj为索引所存储的值地址 newTable = SideTable::tableForPointer(newObj); // 加锁操做,防止多线程中竞争冲突 lock1 = &newTable->slock; #if SIDE_TABLE_STRIPE > 1 lock2 = &oldTable->slock; if (lock1 > lock2) { spinlock_t *temp = lock1; lock1 = lock2; lock2 = temp; } if (lock1 != lock2) spinlock_lock(lock2); #endif spinlock_lock(lock1); if (*location != oldObj) { spinlock_unlock(lock1); #if SIDE_TABLE_STRIPE > 1 if (lock1 != lock2) spinlock_unlock(lock2); #endif goto retry; } // 旧对象解除注册操做 weak_unregister_no_lock(&oldTable->weak_table, oldObj, location); // 新对象添加注册操做 newObj = weak_register_no_lock(&newTable->weak_table, newObj, location); // weak_register_no_lock returns nil if weak store should be rejected // Set is-weakly-referenced bit in refcount table. if (newObj && !newObj->isTaggedPointer()) { // 弱引用位初始化操做 // 引用计数那张散列表的weak引用对象的引用计数中标识为weak的引用 newObj->setWeaklyReferenced_nolock(); } // Do not set *location anywhere else. That would introduce a race. // 前面不要设置location对象,这里须要更改指针指向 *location = newObj; spinlock_unlock(lock1); #if SIDE_TABLE_STRIPE > 1 if (lock1 != lock2) spinlock_unlock(lock2); #endif return newObj; } 复制代码
这里一样引用一个比较直观的初始化弱引用对象流程图:
总之根据以上对weak进行的存储过程,能够经过下面流程图帮助理解:
weak释放为nil的过程
释放对象基本流程以下:
objc_release
dealloc
dealloc
中,调用来_objc_rootDealloc
函数_objc_rootDealloc
中,调用来object_dispose
函数objc_destructInstance
objc_clear_deallocating
clearDeallocating
函数首先根据对象地址获取全部weak指针地址的数组,而后遍历这个数组把其中的数据设为nil
,最后把这个entry
从weak
表中删除,最后清理对象的记录。
void objc_clear_deallocating(id obj) { assert(obj); assert(!UseGC); if (obj->isTaggedPointer()) return; obj->clearDeallocating(); } //执行 clearDeallocating方法 inline void objc_object::clearDeallocating() { sidetable_clearDeallocating(); } // 执行sidetable_clearDeallocating,找到weak表中的value值 void objc_object::sidetable_clearDeallocating() { SideTable *table = SideTable::tableForPointer(this); // clear any weak table items // clear extra retain count and deallocating bit // (fixme warn or abort if extra retain count == 0 ?) spinlock_lock(&table->slock); RefcountMap::iterator it = table->refcnts.find(this); if (it != table->refcnts.end()) { if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) { weak_clear_no_lock(&table->weak_table, (id)this); } table->refcnts.erase(it); } spinlock_unlock(&table->slock); } 复制代码
最终经过调用weak_clear_no_lock
方法,将weak
指针置空,函数实现以下:
/** * Called by dealloc; nils out all weak pointers that point to the * provided object so that they can no longer be used. * * @param weak_table * @param referent The object being deallocated. */ void weak_clear_no_lock(weak_table_t *weak_table, id referent_id) { objc_object *referent = (objc_object *)referent_id; weak_entry_t *entry = weak_entry_for_referent(weak_table, referent); if (entry == nil) { // XXX should not happen, but does with mismatched CF/objc //printf("XXX no entry for clear deallocating %p\n", referent); return; } // zero out references weak_referrer_t *referrers; size_t count; if (entry->out_of_line) { referrers = entry->referrers; count = TABLE_SIZE(entry); } else { referrers = entry->inline_referrers; count = WEAK_INLINE_COUNT; } for (size_t i = 0; i < count; ++i) { objc_object **referrer = referrers[i]; if (referrer) { if (*referrer == referent) { *referrer = nil; } else if (*referrer) { _objc_inform("__weak variable at %p holds %p instead of %p. " "This is probably incorrect use of " "objc_storeWeak() and objc_loadWeak(). " "Break on objc_weak_error to debug.\n", referrer, (void*)*referrer, (void*)referent); objc_weak_error(); } } } weak_entry_remove(weak_table, entry); } 复制代码
objc_clear_deallocating
函数的操做以下:
weak
表中获取废弃对象的地址为键值的记录weak
修饰符变量的地址,置为nil
weak
表中该记录删除说了这么多,仍是为了说明一开始说的那句话:
Runtime
维护了一个weak表,用于存储指向某个对象的全部weak指针。weak表实际上是一个Hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。
__unsafe_unretained
做用须要和weak对比,它不会引发对象的内部引用计数的变化,可是,当其指向的对象被销毁是__unsafe_unretained
修饰的指针不会置为nil。是不安全的全部权修饰符,它不归入ARC的内存管理。
将对象赋值给附有__autoreleasing
修饰符的变量等同于MRC时调用对象的autorelease
方法。
@autoeleasepool {
// 若是看了上面__strong的原理,就知道实际上对象已经注册到自动释放池里面了
id __autoreleasing obj = [[NSObject alloc] init];
}
复制代码
编译器转换以下代码:
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSObject,@selector(alloc));
objc_msgSend(obj,@selector(init));
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);
@autoreleasepool {
id __autoreleasing obj = [NSMutableArray array];
}
复制代码
编译器转换上述代码以下:
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSMutableArray,@selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);
复制代码
上面两种方式,虽然第二种持有对象的方法从alloc
方法变为了objc_retainAutoreleasedReturnValue
函数,都是经过objc_autorelease
,注册到autoreleasePool
中。
篇幅太长了,不少底层上面的东西,网上都有相关的资料,之前看不是很懂,如今回过头来细细研读,感受仍是能理解的,因此参考了网络上的资料整理出来了,增长本身的印象,也但愿个人理解可以帮助到小伙伴们,若有错误,但愿指出,共同进步,谢谢
参考资料:
《Objective-C高级编程 iOS于OS X多线程和内存管理》
iOS 底层解析weak的实现原理(包含weak对象的初始化,引用,释放的分析
黑幕后的Autorelease