内容简介:在研究Hash表的过程中,想看iOS当中有哪些场景应用,最为大家所知的应该就是weak关键字的底层原理,利用网上的资料深究了一下,同时更进一步了解到了iOS内存管理方面的知识,所以希望自己能够保留这份记忆,就记录一下。Hash或者说散列表,它是一种基础数据结构,这里为什么会说到它,因为我感觉理解了Hash对weak关键字底层的理解有很大的帮助。Hash表是一种特殊的数据结构,它同数组、链表以及二叉树等相比有很明显的区别,但是它又是在数组和链表的基础上演化而来。
在研究Hash表的过程中,想看iOS当中有哪些场景应用,最为大家所知的应该就是weak关键字的底层原理,利用网上的资料深究了一下,同时更进一步了解到了iOS内存管理方面的知识,所以希望自己能够保留这份记忆,就记录一下。
Hash
Hash或者说散列表,它是一种基础数据结构,这里为什么会说到它,因为我感觉理解了Hash对weak关键字底层的理解有很大的帮助。
Hash表是一种特殊的数据结构,它同数组、链表以及二叉树等相比有很明显的区别,但是它又是在数组和链表的基础上演化而来。
Hash表的本质是一个数组,数组中每一个元素称为一个箱子,箱子中存放元素。
存储过程如下:
- 根据key计算出它的哈希值h。
- 假设箱子的个数为n,那么这个键值对应该放在第(h % n)个箱子中。
- 如果该箱子中已经有了键值对,就使用方法解决冲突(这里值说分离链接法解决冲突,还有一个方法是开放定址法)。
Hash表采用一个映射函数f:key->address将关键字映射到该记录在表中存储位置,从而想要查找该记录时,可以直接根据关键字和映射关系计算出该记录在表中的存储位置,通常情况下,这种映射关系称作Hash函数,而通过Hash函数和关键字计算出来的存储位置( 这里的存储位置只是表中的存储位置,并不是实际的物理地址 )称作Hash地址。
先看一个列子: 假如联系人信息采用Hash表存储,当想要找到“lisi”的信息时,直接根据“lisi”和Hash函数计算出Hash地址即可。 因为我们是用数组大小对哈希值进行取模,有可能不同的键值产生的索引值相同,这就是所谓的冲突。
显然这里“sizhang”元素和“zhangsi”元素产生了冲突,解决该冲突的方法就是改变数据结构,将数组内的元素改变为一个链表,这样就能容下足够多的元素。
在使用分离链接法解决哈希冲突时,每个箱子其实是一个链表,将属于同一个箱子里的元素存储在一张线性表中,而每张表的表头的序号即为计算得到的Hash地址,如下图最左边是数组结构,数组内的元素为链表结构。
这里的Hash表我们只做简单的了解,想要详细了解的请参考:
内存管理的思考
ARC的核心思想:
- 自己生成的对象,自己持有
- 非自己生成的对象,自己也可以持有
- 自己持有的对象不需要时,需要对其进行释放
- 非自己持有的对象无法释放
其实不论ARC还是MRC都遵循该方式,只是在ARC模式下这些工作被编译器做了
引用计数
retain、release、etainCount
苹果的实现:(这部分内容是根据 《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
作用: 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(哨兵对象)
作为入参,
- 根据传入的哨兵对象地址找到哨兵对象所处的page
- 在当前page中,将晚于哨兵对象插入的所有
autorelease对象都发送一次- release消息,并向回移动next指针到正确位置 - 从最新加入的对象一直向前清理,可以向前跨越若干个page,知道哨兵所在的page
刚才的 objc_autoreleasePoolPop 执行后,最终变成了下面样子:
关键字
__strong
__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
__weak 表示弱引用,弱引用不会影响对象的释放,而当对象被释放时,所有指向它的弱引用都会自动被置为 nil ,这样可以防止野指针。
id __weak obj = [[NSObject alloc] init]; 复制代码
根据我们的了解,可以知道 obj 对象在生成之后立马就会被释放,主要原因是因为 __weak 修饰的指针没有引起对象内部的引用计数发生变化。
__weak 的几个使用场景:
- 在Delegate关系中防止循环引用
- 在Block中防止循环引用
- 用来修饰指向有Interface Builder创建的控件
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 - 因为对象的引用计数为0,所以执行
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
__unsafe_unretained 作用需要和weak对比,它不会引起对象的内部引用计数的变化,但是,当其指向的对象被销毁是 __unsafe_unretained 修饰的指针不会置为nil。是不安全的所有权修饰符,它不纳入ARC的内存管理。
__autoreleasing
将对象赋值给附有 __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 中。
篇幅太长了,很多底层上面的东西,网上都有相关的资料,以前看不是很懂,现在回过头来细细研读,感觉还是能理解的,所以参考了网络上的资料整理出来了,增加自己的印象,也希望我的理解能够帮助到小伙伴们,如有错误,希望指出,共同进步,谢谢
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
菜鸟侦探挑战数据分析
[日] 石田基广 / 支鹏浩 / 人民邮电出版社 / 2017-1 / 42
本书以小说的形式展开,讲述了主人公俵太从大学文科专业毕业后进入征信所,从零开始学习数据分析的故事。书中以主人公就职的征信所所在的商业街为舞台,选取贴近生活的案例,将平均值、t检验、卡方检验、相关、回归分析、文本挖掘以及时间序列分析等数据分析的基础知识融入到了生动有趣的侦探故事中,讲解由浅入深、寓教于乐,没有深奥的理论和晦涩的术语,同时提供了大量实际数据,使用免费自由软件RStudio引领读者进一步......一起来看看 《菜鸟侦探挑战数据分析》 这本书的介绍吧!