【原創博文,轉載請注明出處!】
寫在最前面:你們別光看啊,發現我說的不對的地方請指出或留言,希望我們一起進步。
前幾天在iOS圈內流傳著“一個關于歷年來weak的面試題答案”的段子,感覺有點搞怪O(∩_∩)O~~。是的,做技術開發門檻越來越高了。。。
結合objc源碼,我寫了個簡單測試demo,關于對象的三個修飾詞__strong
、__weak
、__unsafe_unretained
,測試結果分別用“01__strong指針引用對象.png”、“02__weak指針引用對象.png”、“03__unsafe_unretained指針引用對象.png”三張圖表示。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-variable"
__strong ZYClass *strongZYClass;
__weak ZYClass *weakZYClass;
__unsafe_unretained ZYClass *unsafeZYClass;
#pragma clang diagnostic pop
NSLog(@"test begin");
{
ZYClass *zyClass = [[ZYClass alloc] init];
strongZYClass = zyClass;
// weakZYClass = zyClass;
// unsafeZYClass = zyClass;
}
NSLog(@"test over%@",strongZYClass);
}
"zyClass"定義的作用域如下:
{
ZYClass *zyClass = [[ZYClass alloc] init];
strongZYClass = zyClass;
// weakZYClass = zyClass;
// unsafeZYClass = zyClass;
}
鑒于__strong
指針對對象有強引用關系,所以"zyClass"在出作用域后并沒有立即銷毀;
__weak
指針對對象是弱引用關系,不持有引用對象。所以"zyClass"在出作用域后就銷毀了;
__unsafe_unretained
指針對對象是弱引用關系,不持有引用對象。所以"zyClass"在出作用域后就銷毀了。(與__weak不同的是,__weak引用的對象銷毀后,系統會將對象置為nil,而__unsafe_unretained不這么做,導致EXC_BAD_ACCESS
錯誤。)
weak指針幫我們干了啥?
當一個對象釋放的時候,會執行"- (void)dealloc {}"方法,在objc源碼的“NSObject.mm”中找到了該函數以及相關調用流程,我將它們抽取出來如下:
// Replaced by NSZombies
- (void)dealloc {
_objc_rootDealloc(self);
}
void _objc_rootDealloc(id obj)
{
assert(obj);
obj->rootDealloc();
}
inline void objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
id object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory.
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
**********************************************************************/
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
//清除對象的成員變量
if (cxx) object_cxxDestruct(obj);
//清除對象的關聯對象
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
inline void objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}
assert(!sidetable_present());
}
// Slow path of clearDeallocating()
// for objects with nonpointer isa
// that were ever weakly referenced
// or whose retain count ever overflowed to the side table.
NEVER_INLINE void objc_object::clearDeallocating_slow()
{
assert(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
SideTable& table = SideTables()[this];
table.lock();
if (isa.weakly_referenced) {
weak_clear_no_lock(&table.weak_table, (id)this);
}
if (isa.has_sidetable_rc) {
table.refcnts.erase(this);
}
table.unlock();
}
/**
* 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 shouldn't 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);
}
/**
* Return the weak reference table entry for the given referent.
* If there is no entry for referent, return NULL.
* Performs a lookup.
*
* @param weak_table
* @param referent The object. Must not be nil.
*
* @return The table of weak referrers to this object.
*/
static weak_entry_t * weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
assert(referent);
weak_entry_t *weak_entries = weak_table->weak_entries;
if (!weak_entries) return nil;
size_t begin = hash_pointer(referent) & weak_table->mask;
size_t index = begin;
size_t hash_displacement = 0;
while (weak_table->weak_entries[index].referent != referent) {
index = (index+1) & weak_table->mask;
if (index == begin) bad_weak_table(weak_table->weak_entries);
hash_displacement++;
if (hash_displacement > weak_table->max_hash_displacement) {
return nil;
}
}
return &weak_table->weak_entries[index];
}
函數inline void objc_object::rootDealloc()
中有一句判斷if (isTaggedPointer()) return; // fixme necessary?
,這個地方條件成立就會return ,而不會釋放對象,為什么?
實際上蘋果在64位系統開始推出了“Tagged Pointer”技術來優化NSNumber、NSString、NSDate等小對象的存儲,在沒有引入“Tagged Pointer”技術之前,NSNumber等對象需要動態分配內存、維護引用技術,NSNumber指針存儲的是NSNumber對象的地址值。iOS引入“Tagged Pointer”技術之后,NSNumber指針里面存儲的數據變成了“Tag+Data”,也就是直接將數據存儲在指針中。僅當指針不夠存儲數據時,才會使用動態分配內存的方式來存儲數據。
“Tagged Pointer”的好處是:一方面節約計算機內存,另一方面因為可以直接從指針中讀取數據,可以節約之前objc_msgSend流程消耗的時間。
那對于下面三個“對象”(這里注意“”修飾,因為a、b本質上屬于Tagged Pointer類型,而不是OC對象):
a. NSNumber *number1 = @4;
b. NSNumber *number2 = @5;
c. NSNumber *number3 = @(0xFFFFFFFFFFFFFFF)。
對于a、b,所對應的二級制編碼分別為0b0100、0b0101,僅僅占用3 bit,用一個字節(8bit)就足夠存儲了,而OC的指針*number1、*number2
都占用8個字節,因此我們完全可以將a、b的值存放在指針中,那么a、b實際上就不是真正的對象了,也就不存在執行所謂的- (void) dealloc{}
流程。如果按照64位之前的策略,那存儲a、b這樣的小對象,需要在堆空間alloc init出一個NSNumber對象,然后將@10放入對象中,這個過程至少占用16字節,然后在棧區用一個指針指向這個NSNumber對象,棧空間指針又占用8字節,所以至少需要24字節存儲a、b這樣的小對象,很浪費內存。詳細請參考談談我對Objective-C對象本質的理解。
回到正題:
當一個對象被回收的時候調用流程:
1 -(void)dealloc ->
2 _objc_rootDealloc(id obj) ->
3 objc_object::rootDealloc() ->
4 object_dispose(id obj) ->
5 objc_destructInstance(id obj) ->
6 objc_object::clearDeallocating() ->
7 objc_object::clearDeallocating_slow() ->
8 weak_clear_no_lock(weak_table_t weak_table, id referent_id) ->
**9 weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
前面1~6都很好理解。7開始到關鍵點:從SideTable取出weak_table和當前對象指針"this"當做實參傳給函數weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
,該函數利用weak_table
和轉換后的referent_id
對象調用weak_entry_t * weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
得到一個關于referent_id
對象的weak引用表的數組。
再看一次"weak_entry_t * weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)"函數實現細節
函數內部利用正在被dealloc的對象地址referent 通過哈希函數hash_pointer()計算,再&weak_table->mask獲得begin索引,所以可推測weak_table是一個散列表結構,weak_table來自于SideTable對象中的“weak_table”成員,有了這個當前對象在散列表中的索引,就可以通過索引獲取當前對象的弱引用數組了(當然根據獲取到的begin索引得到的散列結果可能并不是這個“dealloc對象”的,因為存在散列沖突,所以這里面有while ()循環判斷當前index散列值的“ referent”與我們傳入的“ referent”是否匹配)。
size_t index = begin;
size_t hash_displacement = 0;
while (weak_table->weak_entries[index].referent != referent) {
index = (index+1) & weak_table->mask;
if (index == begin) bad_weak_table(weak_table->weak_entries);
hash_displacement++;
if (hash_displacement > weak_table->max_hash_displacement) {
return nil;
}
}
通過這個while循環,可見這個散列表解決散列沖突采用的是“開放尋址法”。
總結:程序運行時將弱引用存到一個哈希表中,當對象obj要銷毀的時候,哈希函數根據obj地址獲取到索引,然后從哈希表中取出obj對應的弱引用集合weak_entries,遍歷weak_entries并一一清空(也就對應源碼中函數void weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
所做的*referrer = nil;
)。