2020 阿里、字節iOS面試題之Runtime相關問題2(附答案) 目錄 runtime相關問題之 內存管理 總結 推薦

目錄

runtime相關問題之 內存管理

基本內容包括:

  • weak的實現原理?SideTable的結構是什麼樣的
  • 關聯對象的應用?系統如何實現關聯對象的
  • 關聯對象的如何進行內存管理的?關聯對象如何實現weak屬性
  • Autoreleasepool的原理?所使用的的數據結構是什麼
  • ARC的實現原理?ARC下對retain, release做了哪些優化
  • ARC下哪些情況會造成內存泄漏

weak的實現原理?SideTable的結構是什麼樣的

先說結論:

  • weak表其實是一個hash(哈西)表.Key是所指對象的地址,Valueweak指針的地址數組.實現原理是通過新舊錶的更新指針方式,對weak對象單獨存儲於SideTable中的weak_table_t(類型) weak_table表中,通過函數objc_initWeak()->storeWeak()函數中的新舊SideTable(結構體)表來實現
  • SideTable是一個結構體,內部主要有引用計數表和弱引用表兩個成員,內存存儲的其實都是對象的地址和引用計數和weak變量的地址,而不是對象本身的數據,它的結構如下

|

struct SideTable {
     spinlock_t slock;
     RefcountMap refcnts;
     weak_table_t weak_table;
     SideTable() {
         memset(&weak_table, 0, sizeof(weak_table));
 }
     ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
 }
     void lock() { slock.lock(); }
     void unlock() { slock.unlock(); }
     void forceReset() { slock.forceReset(); }
     // Address-ordered lock discipline for a pair of side tables.
     template<HaveOld, HaveNew>
     static void lockTwo(SideTable *lock1,SideTable *lock2);
     template<HaveOld, HaveNew>
     static void unlockTwo(SideTable *lock1, SideTable *lock2);
};

weak實現原理

實現原理概括分爲3個時機

  • 1.初始化
  • 2.添加引用
  • 3.釋放

1.初始化時候

runtime會調用objc_initWeak函數,初始化一個新的weak指針指向對象的地址.

我們引入一段測試代碼

NSObject *obj = [[NSObject alloc] init];
id __weak obj1 = obj;

當我們初始化一個weak變量時,runtime會調用NSObject.mm中的objc_initWeak()函數。這個函數在Clang中的聲明如下:

id objc_initWeak(id *location, id newObj) {
     if (!newObj) { // 查看對象實例是否有效 無效對象直接導致指針釋放
         *location = nil;
         return nil;
 }
     // 這裏傳遞了三個 bool 數值 old, new, crash.使用 template 進行常量參數傳遞是爲了優化性能
     return storeWeak<DontHaveOld, DoHaveNew, DontCrashIfDeallocating>
         (location, (objc_object*)newObj);
}

可以看出,這個函數僅僅是一個深層函數的調用入口,而一般的入口函數中,都會做一些簡單的判斷(例如 objc_msgSend 中的緩存判斷),這裏判斷了其指針指向的類對象是否有效,無效直接釋放,不再往深層調用函數。否則,object將被註冊爲一個指向value的__weak對象。而這事應該是objc_storeWeak函數乾的.

注意: objc_initWeak函數有一個前提條件:就是object必須是一個沒有被註冊爲__weak對象的有效指針。而value則可以是null,或者指向一個有效的對象.

2.添加引用時

objc_initWeak函數會調用 objc_storeWeak()函數,objc_storeWeak()則會調用storeWeak()函數, storeWeak()的作用是更新指針指向,創建對應的弱引用表

模板

// HaveOld:  true - 變量有值 ,false - 需要被及時清理,當前值可能爲 nil
// HaveNew:  true - 需要被分配的新值,當前值可能爲nil, false - 不需要分配新值
// CrashIfDeallocating: true - 說明 newObj 已經釋放或者 newObj 不支持弱引用,該過程需要暫停,false - 用 nil 替代存儲
template <HaveOld haveOld, HaveNew haveNew,CrashIfDeallocating crashIfDeallocating>

weak實現函數 該過程用來更新弱引用指針的指向.

static id 
storeWeak(id *location, objc_object *newObj)
{
    ASSERT(haveOld  ||  haveNew);
    if (!haveNew) ASSERT(newObj == nil);  
    // 初始化 previouslyInitializedClass 指針.
    Class previouslyInitializedClass = nil;
    id oldObj;
    // 聲明兩個 SideTable,① 新舊散列創建
    SideTable *oldTable;
    SideTable *newTable;
    //獲得新值和舊值的鎖存位置(用地址作爲唯一標示),通過地址來建立索引標誌,防止桶重複,下面指向的操作會改變舊值.
    if (haveOld) {
        oldObj = *location;// 更改指針,獲得以 oldObj 爲索引所存儲的值地址
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (haveNew) {
        newTable = &SideTables()[newObj];// 更改新值指針,獲得以 newObj 爲索引所存儲的值地址
    } else {
        newTable = nil;
    }
    // 加鎖操作,防止多線程中競爭衝突
    SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
    // 避免線程衝突重處理,location 應該與 oldObj 保持一致,如果不同,說明當前的 location 已經處理過 oldObj 可是又被其他線程所修改
    if (haveOld  &&  *location != oldObj) {
        SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
        goto retry;
    }
    // 防止弱引用間死鎖,並且通過 +initialize 初始化構造器保證所有弱引用的 isa 非空指向
    if (haveNew  &&  newObj) {
        Class cls = newObj->getIsa();// 獲得新對象的 isa 指針
        // 判斷 isa 非空且已經初始化
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized()) 
        { 
            SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);/ 解鎖
            class_initialize(cls, (id)newObj); //如果該類已經完成執行 +initialize 方法是最理想情況,如果該類 +initialize 在線程中,例如 +initialize 正在調用 storeWeak 方法,需要手動對其增加保護策略,並設置 previouslyInitializedClass 指針進行標記
            previouslyInitializedClass = cls;
            goto retry; //重試
        }
    }
    // ② 清除舊值
    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }
     // ③ 分配新值
    if (haveNew) {
        newObj = (objc_object *)
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location, 
                                  crashIfDeallocating);
        //如果弱引用被釋放 weak_register_no_lock 方法返回 nil,在引用計數表中設置若引用標記位
        if (newObj  &&  !newObj->isTaggedPointer()) {
            //弱引用位初始化操作,引用計數那張散列表的weak引用對象的引用計數中標識爲weak引用
            newObj->setWeaklyReferenced_nolock();
        }
        //之前不要設置 location 對象,這裏需要更改指針指向
        *location = (id)newObj;
    }
    else {
        // 沒有新值,則無需更改
    }
    
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

    return (id)newObj;
}
SideTable

SideTable就是一個結構體,內部主要有引用計數表和弱引用表兩個成員,內存存儲的其實都是對象的地址和引用計數和weak變量的地址,而不是對象本身的數據. > 主要用於管理對象的引用計數和 weak 表.

我們來看圖

操作系統維護64個SideTable,通過對象的地址位置hash之後模64(就是%64求餘數)找到指定的SideTable 每個SideTable維護了一個RefcountMap的引用計數表,key就是對象地址,value就是此對象的引用計數

struct SideTable {
    spinlock_t slock; //保證原子操作的自旋鎖
    RefcountMap refcnts; //引用計數的 hash 表
    weak_table_t weak_table; //weak 引用全局 hash 表
    ...
};
  • slock 防止競爭的自旋鎖
  • refcnts 協助對象的 isa 指針的extra_rc共同引用計數的變量
weak表

弱引用hash表,weak_table_t類型的結構體,存儲某個實例對象相關的所有弱引用信息. 定義如下:

struct weak_table_t {
    weak_entry_t *weak_entries; // 保存了所有指向指定對象的 weak 指針
    size_t    num_entries;       // 存儲空間
    uintptr_t mask;                 // 參與判斷引用計數輔助量
    uintptr_t max_hash_displacement;     // hash key 最大偏移值
};

這是一個全局弱引用hash表。使用不定類型對象的地址作爲key,用weak_entry_t類型結構體對象作爲value,其中的weak_entries 成員,即爲弱引用表入口.

其中weak_entry_t是存儲在弱引用表中的一個內部結構體,它負責維護和存儲指向一個對象的所有弱引用hash表。其定義如下:

typedef DisguisedPtr<objc_object *> weak_referrer_t;
struct weak_entry_t {
    DisguisedPtr<objc_object> referent;
    union {
        struct {
            weak_referrer_t *referrers;
            uintptr_t        out_of_line_ness : 2;
            uintptr_t        num_refs : PTR_MINUS_2;
            uintptr_t        mask;
            uintptr_t        max_hash_displacement;
        };
        struct {
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    };
    ...
};

其中DisguisedPtr類型的referent變量是對泛型對象的指針的封裝,通過這個泛型類來解決內存泄露的問題.

註釋中有個很重要的out_of_line成員,它代表最低的有效位,當它爲0的時候,weak_referrer_t成員將擴展爲多行靜態的hask table.

其中weak_referrer_t 是一個二維objc_object的別名(typedef),通過一個二維指針地址偏移,用下標作hash的key,做成了一個弱引用的散列。

那麼weak_entry_t中的各成員out_of_linenum_refsmaskmax_hash_displacement 在有效位未生效的時候有什麼作用?

  • out_of_line:最低有效位,也是標誌位。當標誌位 0 時,增加引用表指針緯度。
  • num_refs: 引用數值。這裏記錄弱引用表中引用有效數字,因爲弱引用表使用的是靜態 hash 結構,所以需要使用變量來記錄數目。
  • mask:計數輔助量。
  • max_hash_displacement:hash元素上限閥值。

其實 out_of_line 的值通常情況下是等於零的,所以弱引用表總是一個objc_objective指針二維數組。一維 objc_objective指針可構成一張弱引用散列表,通過第三緯度實現了多張散列表,並且表數量爲 WEAK_INLINE_COUNT.

以上是weak表的實現原理.

3.釋放

釋放時,調用clearDeallocating函數。clearDeallocating函數首先根據對象地址獲取所有weak指針地址的數組,然後遍歷這個數組把其中的數據設爲nil,最後把這個entryweak表中刪除,最後清理對象的記錄.

當weak引用指向的對象被釋放時,又是如何去處理weak指針的呢?當釋放對象時,其基本流程如下:
  • 1.調用objc_release
  • 2.因爲對象的引用計數爲0,所以執行dealloc
  • 3.在dealloc中,調用了_objc_rootDealloc函數
  • 4.在_objc_rootDealloc中,調用了object_dispose函數
  • 5.調用objc_destructInstance
  • 6.最後調用objc_clear_deallocating

重點看對象被釋放時調用的objc_clear_deallocating函數。該函數實現如下:

void objc_clear_deallocating(id obj)  
{
    ASSERT(obj);
    if (obj->isTaggedPointer()) return;
    obj->clearDeallocating();
}

調用了clearDeallocating(),點擊源碼進去追蹤發現,它最終是使用了迭代器來取weak表的value,然後調用weak_clear_no_lock()查找對應value,將該weak指針置空.

weak_clear_no_lock()函數的實現如下:

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);
}

objc_clear_deallocating()該函數的動作如下:

  • 1.從weak表中獲取廢棄對象的地址爲鍵值的記錄
  • 2.將包含在記錄中的所有附有 weak修飾符變量的地址,賦值爲nil
  • 3.將weak表中該記錄刪除
  • 4.從引用計數表中刪除廢棄對象的地址爲鍵值的記錄

參考

關聯對象的應用?系統如何實現關聯對象的

關聯對象的應用?

一般應用在category(分類)中爲 當前類 添加關聯屬性,因爲不能直接添加成員變量,但是可以通過runtime的方式間接實現添加成員變量的效果。

當我們在category中聲明如下代碼:

@interface ClassA : NSObject (Category)
@property (nonatomic, strong) NSString *property;
@end

實際上@property這個objc標準庫的內建關鍵字幫我們實現了 setter和 getter,但是在category中並不能幫我們聲明成員變量 property 我們需要通過runtime提供的兩個C函數的api間接實現 動態添加 成員變量property.

  • objc_setAssociatedObject()
  • objc_getAssociatedObject()
#import "ClassA+Category.h"
#import <objc/runtime.h>

@implementation ClassA (Category)

- (NSString *) property {
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setProperty:(NSString *)categoryProperty {
    objc_setAssociatedObject(self, @selector(property), categoryProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

看到上面的關聯方法,我們來仔細研究一下下面經常使用的關聯屬性相關的API

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, const void *key);
void objc_removeAssociatedObjects(id object);
  1. objc_setAssociatedObject()以鍵值對形式添加關聯對象
  2. objc_getAssociatedObject()根據 key 獲取關聯對象
  3. objc_removeAssociatedObjects()移除所有關聯對象

objc_setAssociatedObject()的調用棧

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
└── SetAssocHook.get()(object, key, value, policy)
    └── void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy)

上述調用棧中的_object_set_associative_reference()函數實際完成了設置關聯對象的任務:

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
     if (!object && !value) return;
    if (object->getIsa()->forbidsAssociatedObjects())
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    ObjcAssociation association{policy, value};
    association.acquireValue();
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        if (value) {
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {
                object->setHasAssociatedObjects();
            }
            auto &refs = refs_result.first->second;
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {
                association.swap(result.first->second);
            }
        } else {
            ...
        }
    }
    association.releaseHeldValue();
}

省略的很多代碼,上述代碼中就是應用場景,上面調用的類AssociationsManager就是我們下面要講的系統如何實現關聯對象的原理.

系統如何實現關聯對象的(關聯對象實現原理)

實現關聯對象技術的核心對象 有如下這麼幾個:

  1. AssociationsManager

  2. AssociationsHashMap

  3. ObjectAssociationMap

  4. ObjcAssociation

其中Map同我們平時使用的字典類似。通過key-value的形式對應存值.

下面我們通過源碼來一探究竟

objc_setAssociatedObject()函數

runtime源碼

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    _object_set_associative_reference(object, key, value, policy);
}

源碼調用過程有hook函數,有點長,這裏我簡化一下,直接調用核心的函數

下面看下_object_set_associative_reference()函數的代碼實現

void _object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    if (object->getIsa()->forbidsAssociatedObjects())
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    ObjcAssociation association{policy, value}; //4. 我們用到的ObjcAssociation
    association.acquireValue();
    {
        AssociationsManager manager; //1. 我們用到的AssociationsManager
        AssociationsHashMap &associations(manager.get()); //2.我們上面列舉的AssociationsHashMap
        if (value) {
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{}); //3.我們用到的ObjectAssociationMap
            if (refs_result.second) {
                object->setHasAssociatedObjects();
            }
            auto &refs = refs_result.first->second;
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {
                association.swap(result.first->second);
            }
        } else {
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                auto &refs = refs_it->second;
                auto it = refs.find(key);
                if (it != refs.end()) {
                    association.swap(it->second);
                    refs.erase(it);
                    if (refs.size() == 0) {
                        associations.erase(refs_it);
                    }
                }
            }
        }
    }
    association.releaseHeldValue();
}

上述代碼可以找到我們實現關聯對象技術的核心對象. 下面我們分別介紹一下幾個核心對象的內部實現.

AssociationsManager
typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;
class AssociationsManager {
    using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
    static Storage _mapStorage;

public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }

    AssociationsHashMap &get() {
        return _mapStorage.get();
    }
    static void init() {
        _mapStorage.init();
    }
};

AssociationsManager內部有一個get()函數返回一個AssociationsHashMap對象

AssociationsHashMap

AssociationsHashMapDenseMap的typedef(可以理解爲別名) 只不過它被定義成符合某些元組的條件的DenseMap類型

實際上 AssociationsHashMap 用與保存從對象的 disguised_ptr_tObjectAssociationMap的映射,這個數據結構保存了當前對象對應的所有關聯對象

typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;

這裏的ObjectAssociationMap是另一類型的typedef,裏面存着ObjcAssociation類型的對象指針的key,value形式.

下面再看下 ObjcAssociation ,這是一個C++的類對象,最關鍵的ObjcAssociation包含了policy以及value.

class ObjcAssociation {
    uintptr_t _policy;
    id _value;
public:
    ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {}
    ObjcAssociation() : _policy(0), _value(nil) {}
    ObjcAssociation(const ObjcAssociation &other) = default;
    ObjcAssociation &operator=(const ObjcAssociation &other) = default;
    ObjcAssociation(ObjcAssociation &&other) : ObjcAssociation() {
        swap(other);
    }
    inline void swap(ObjcAssociation &other) {
        std::swap(_policy, other._policy);
        std::swap(_value, other._value);
    }
    inline uintptr_t policy() const { return _policy; }
    inline id value() const { return _value; }
    ...
};
關聯對象在內存中以什麼形式存儲的?

示例代碼

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [NSObject new];
        objc_setAssociatedObject(obj, @selector(hello), @"Hello", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return 0;
}

這個調用函數objc_setAssociatedObject(OBJC_ASSOCIATION_RETAIN_NONATOMIC, @"Hello")在內存中是這樣的存儲結構

objc_setAssociatedObject()

我們回頭來詳細分解一下objc_setAssociatedObject()函數中的真實實現部分,_object_set_associative_reference()

這個函數需要傳入(id object, const void *key, id value, uintptr_t policy),這麼幾個參數,我們拿第3個value參數來分解.

我們分解爲2步

  1. value != nil 設置或者更新關聯對象的值
  2. value == nil 刪除一個關聯對象.

下面是具體是代碼解釋 注意看代碼註釋!!!

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    // 判空
    if (!object && !value) return;

    // 判斷本類對象是否允許關聯其他對象.如果允許則進入代碼塊
    if (object->getIsa()->forbidsAssociatedObjects())
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));

    // 將被關聯的對象封裝成DisguisedPtr方便在後邊hash表中的管理,它的作用就像是一個指針
    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    // 將需要關聯的對象,封裝成ObjcAssociation,方便管理
    ObjcAssociation association{policy, value};

    // 處理policy爲retain和copy的修飾情況,
    association.acquireValue();

    {
        // 獲取關聯對象管理者對象
        AssociationsManager manager;
        // 根據管理者對象獲取對應關聯表(HashMap)
        AssociationsHashMap &associations(manager.get());

        if (value) {
            // 如果這個disguised存在於ObjectAssociationMap()中,則替換,如果不存在則初始化後在插入
            // 這裏說明一下,我們關聯的對象關係存在於ObjectAssociationMap中,而
            //  ObjectAssociationMap有多個,所以,這一步是對ObjectAssociationMap的一個管理,下邊纔是對我們要關聯的對象的操作
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            // 如果這是此對象第一次被關聯
            if (refs_result.second) {
               // 修改isa_t中的has_assoc字段,標記其被關聯狀態
                object->setHasAssociatedObjects();
            }

            // 這裏纔是對我們要關聯的對象操作
            auto &refs = refs_result.first->second;
            // 想map中插入key value對
            auto result = refs.try_emplace(key, std::move(association));
            // 這裏沒有看懂,爲什麼沒有第二個就要交換一下..
            if (!result.second) {
                association.swap(result.first->second);
            }
        } else {
            // value爲空, 並且在associations中有記錄,則進行擦除操作 
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                auto &refs = refs_it->second;
                auto it = refs.find(key);
                if (it != refs.end()) {
                    association.swap(it->second);
                    refs.erase(it);
                    if (refs.size() == 0) {
                        associations.erase(refs_it);
                    }
                }
            }
        }
    }

    // release the old value (outside of the lock).
    association.releaseHeldValue();
}
objc_setAssociatedObject()函數的作用是什麼?
inline void
objc_object::setHasAssociatedObjects()
{
    if (isTaggedPointer()) return;

 retry:
    isa_t oldisa = LoadExclusive(&isa.bits);
    isa_t newisa = oldisa;
    if (!newisa.nonpointer  ||  newisa.has_assoc) {
        ClearExclusive(&isa.bits);
        return;
    }
    newisa.has_assoc = true;
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
}

它會將isa結構體中的標記位has_assoc標記爲true,也就是表示當前對象有關聯對象,如下圖isa中的各個標記位都是幹什麼的.

objc_getAssociatedObject()

這個函數的調用棧如下

id objc_getAssociatedObject(id object, const void *key)
└── id _object_get_associative_reference(id object, const void *key);

通過上面我們介紹,理解這個函數相當簡單了

id
_object_get_associative_reference(id object, const void *key)
{
    ObjcAssociation association{};
    {
        AssociationsManager manager; //1
        AssociationsHashMap &associations(manager.get()); //1
        AssociationsHashMap::iterator i = associations.find((objc_object *)object); //2
        if (i != associations.end()) {
            ObjectAssociationMap &refs = i->second;
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
                association = j->second;
                association.retainReturnedValue();
            }
        }
    }
    return association.autoreleaseReturnedValue();
}
  1. 通過AssociationsManager拿到AssociationsHashMap哈西表
  2. 通過哈西表尋找關聯對象
  3. 剩下的就是更新對象是否初次創建等標記 然後返回對象
objc_removeAssociatedObjects()

調用棧如下:

void objc_removeAssociatedObjects(id object)
└── void _object_remove_assocations(id object)

代碼具體實現

void objc_removeAssociatedObjects(id object) 
{
    if (object && object->hasAssociatedObjects()) { 
        _object_remove_assocations(object);
    }
}

check對象是否爲nil 且 關聯對象是否存在

然後調用實現跟上邊的get差不多

void
_object_remove_assocations(id object)
{
    ObjectAssociationMap refs{};
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            refs.swap(i->second);
            associations.erase(i);
        }
    }
    // release everything (outside of the lock).
    for (auto &i: refs) {
        i.second.releaseHeldValue();
    }
}

通過AssociationsManager -> AssociationsHashMap -> object 是否存在,如果存在就擦除.- > releaseHeldValue()是否對象

小結

關聯對象的應用和系統如何實現關聯對象的大概順序如下:
AssociationsManager關聯對象管理器->AssociationsHashMap哈希映射表->ObjectAssociationMap關聯對象指針->ObjcAssociation關聯對象

關聯對象的如何進行內存管理的?關聯對象如何實現weak屬性?

關聯對象的如何進行內存管理的?

當我調用關聯對象函數objc_setAssociatedObject()的時候會調用如下函數:

_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy),這裏面有個方法

ObjcAssociation association{policy, value};
// retain the new value (if any) outside the lock.
association.acquireValue();

這裏的 policy就是具體絕對內存使用retain還是其它相關的內存枚舉.

enum {
    OBJC_ASSOCIATION_SETTER_ASSIGN      = 0,
    OBJC_ASSOCIATION_SETTER_RETAIN      = 1,
    OBJC_ASSOCIATION_SETTER_COPY        = 3,            // NOTE:  both bits are set, so we can simply test 1 bit in releaseValue below.
    OBJC_ASSOCIATION_GETTER_READ        = (0 << 8),
    OBJC_ASSOCIATION_GETTER_RETAIN      = (1 << 8),
    OBJC_ASSOCIATION_GETTER_AUTORELEASE = (2 << 8)
};

通過 acquireValue()函數判斷使用那種內存關鍵字.

inline void acquireValue() {
    if (_value) {
        switch (_policy & 0xFF) {
        case OBJC_ASSOCIATION_SETTER_RETAIN:
            _value = objc_retain(_value);
            break;
        case OBJC_ASSOCIATION_SETTER_COPY:
            _value = ((id(*)(id, SEL))objc_msgSend)(_value, @selector(copy));
            break;
        }
    }
}

關聯對象如何實現weak屬性?

首先說一下 這個問題問的非常有技術含量,完全考驗iOS開發者對底層瞭解的程度.

在爲NSObject對象綁定 associated object 時可以指定如下依賴關係:

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0, //弱引用
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, //強引用,非原子操作
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  //先 copy,然後強引用
    OBJC_ASSOCIATION_RETAIN = 01401, //強引用,原子操作
    OBJC_ASSOCIATION_COPY = 01403 //先 copy,然後強引用,原子操作
};

根據上述的枚舉我們發現一個很奇怪的問題,這裏的枚舉中並沒有OBJC_ASSOCIATION_WEAK這樣的選項.

基於上述的代碼介紹我們知道Objective-C在底層使用AssociationsManager統一管理各個對象的 associated objects關聯對象.然後通過static key(一般是一個固定值)去訪問對應的associated object關聯對象.然後在dealloc的時候調用擦除函數(associations.erase())來解除對這些關聯對象的引用:

dealloc
    object_dispose
        objc_destructInstance
            _object_remove_assocations  // 移除必要的associated objects

也就是說,在NSObject對象的內存空間裏,並沒有爲 associated objects(關聯對象) 分配任何變量.

我們知道weak變量和 assign變量的區別是:weak指向的對象銷燬的時候,Objective-C會自動幫我們設置nil,而assign卻不能.

這個邏輯是如何實現的呢?

Runtime在底層維護一個weak表(也就是本文開頭講的SlideTable中的weak_table_t weak_tabl),每每分配一個weak指針並賦值有效對象的地址時,會將對象地址和weak指針地址註冊到weak表中,其中對象地址作爲key;當對象被廢棄時,可根據對象地址快速尋找到指向它的所有weak 指針,這些weak指針會被賦值0(即nil)並移出`weak表。

所以,實現weak引用(而非assign引用)的前提是存在一個__weak指針指向到被引用對象的地址,只有這樣,當對象被銷燬時,指針才能被runtime找到然後被設置爲nilNSObject對象和其associated object關聯對象的關係,並不存在指針這樣的中間媒介,因此只存在OBJC_ASSOCIATION_ASSIGN選項,而不存在OBJC_ASSOCIATION_WEAK選項.

那我們怎麼解決爲關聯對象實現weak屬性呢?

可以通過曲線救國的方式聲明一個class類 持有一個weak的成員變量,然後通過 實例化 我們自定義的class的實例,然後把這個實例作爲關聯對象即可.

聲明封裝weak對象的類

@interface WeakAssociatedObjectWrapper : NSObject
@property (nonatomic, weak) id object;
@end

@implementation WeakAssociatedObjectWrapper
@end

調用

@interface UIView (ViewController)
@property (nonatomic, weak) UIViewController *vc;
@end

@implementation UIView (ViewController)
- (void)setVc:(UIViewController *)vc {
    WeakAssociatedObjectWrapper *wrapper = [WeakAssociatedObjectWrapper new];
    wrapper.object = vc;
    objc_setAssociatedObject(self, @selector(vc), wrapper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIViewController *)vc {
    WeakAssociatedObjectWrapper *wrapper = objc_getAssociatedObject(self, _cmd);
    return wrapper.object;
}
@end

看明白沒有,曲線救國.代碼引入自Weak Associated Object

關聯對象參考

Autoreleasepool的原理?所使用的的數據結構是什麼?

在ARC下我們使用@autoreleasepool{} 關鍵字 把需要自動管理的代碼塊圈起來 ,這個過程就是在使用一個AutoReleasePool

@autoreleasepool {
     <#statements#> //代碼塊
}

以上代碼編譯器 最終會把它改寫成下面的樣子

void *context = objc_autoreleasePoolPush();

既然有壓棧一定就有 出棧操作objc_autoreleasePoolPop(context);

  • objc_autoreleasePoolPush()
  • objc_autoreleasePoolPop()

這倆函數都是對AutoreleasePoolPage的封裝,自動釋放機制的核心就是這個類

AutoreleasePoolPage

AutoreleasePoolPage是個C++的類

  • AutoreleasePool並沒有單獨的結構,而是由若干個AutoreleasePoolPage雙向鏈表的形式組合成的,根據上圖可以看出,這個雙向鏈表有前驅parent後繼child.
  • AutoreleasePool是按線程一一對應的(thread 成員變量)
  • AutoreleasePoolPage就是自動釋放池存儲對象的數據結構每個Page佔用4KB內存,本身的成員變量佔用56字節,剩下的空間用來存放調用了autorelease方法的對象地址,同時將一個哨兵插入到Page中,這個哨兵其實就是一個空地址
  • 當一個page被佔滿以後會新建一個新的AutoreleasePoolPage對象,並插入哨兵標記. 具體代碼如下:
class AutoreleasePoolPage {
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)
#   define POOL_BOUNDARY nil
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    static size_t const SIZE = 
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif
    static size_t const COUNT = SIZE / sizeof(id);
    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
};
  • magic 檢查校驗完整性的變量
  • next 指向新加入的autorelease對象
  • thread page當前所在的線程,AutoreleasePool是按線程一一對應的(結構中的thread指針指向當前線程)
  • parent 父節點 指向前一個page
  • child 子節點 指向下一個page
  • depth 鏈表的深度,節點個數
  • hiwat high water mark 數據容納的一個上限
  • EMPTY_POOL_PLACEHOLDER 空池佔位
  • POOL_BOUNDARY 是一個邊界對象 nil,之前的源代碼變量名是 POOL_SENTINEL哨兵對象,用來區別每個page即每個 AutoreleasePoolPage 邊界
  • PAGE_MAX_SIZE = 4096, 爲什麼是4096呢?其實就是虛擬內存每個扇區4096個字節,4K對齊的說法。
  • COUNT 一個page裏對象數

下面看下工作機制圖

這張圖來自快手同事 周學運,如果大佬看到這張圖的話希望能允許授權給我使用哈.

根據上面的示意圖我們大概明白, AutoreleasePoolPage是以棧的形式存在,並且內部對象通過進棧出棧來對應着objc_autoreleasePoolPushobjc_autoreleasePoolPop

如果嵌套AutoreleasePool 就是通過哨兵對象來標識,每次更新鏈表的next和前驅``後繼來完成表的創建銷燬.

當我們對一個對象發送一條autorelease消息的時候實際上就是將這個對象加入到當前AutoreleasePoolPage的棧頂next指針指向的位置

這裏只拿了一張page舉例.

小結

  • 自動釋放池是有N張AutoreleasePoolPage組成,每張page 4K大小, AutoreleasePoolPage是c++的類, AutoreleasePoolPage以雙向鏈表連接起來形成一個自動釋放池
  • 當對象調用 autorelease 方法時,會將對象加入 AutoreleasePoolPage 的棧中
  • pop 時是傳入邊界對象(哨兵對象),然後對page 中的對象發送release 的消息

自動釋放池原理 AutoreleasePool底層實現原理

ARC的實現原理?ARC下對retain, release做了哪些優化

ARC自動引用計數,是蘋果objc4引入的編譯器自動在適當位置 幫助實例對象進行 自動retain後者release的一套機制.

它的實現原理就是在編譯層面插入相關代碼,幫助補全MRC時代需要開發者手動填寫的和管理的對象的相關內存操作的方法.

爲了解釋清楚具體實現原理 ,我找到一篇有代碼示例的文章,從代碼編譯成彙編過程中 編譯器做了很多優化工作. 更新isa指針的信息.

理解 ARC 實現原理

這裏有個點需要跟大家說一下, 上文 中我們講了SlideTable,但是還是有不懂得地方下面我們來通過isa串聯起來

isa的組成

union isa_t 
{
    Class cls;
    uintptr_t bits;
    struct {
         uintptr_t nonpointer        : 1;//->表示使用優化的isa指針
         uintptr_t has_assoc         : 1;//->是否包含關聯對象
         uintptr_t has_cxx_dtor      : 1;//->是否設置了析構函數,如果沒有,釋放對象更快
         uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000 ->類的指針
         uintptr_t magic             : 6;//->固定值,用於判斷是否完成初始化
         uintptr_t weakly_referenced : 1;//->對象是否被弱引用
         uintptr_t deallocating      : 1;//->對象是否正在銷燬
         uintptr_t has_sidetable_rc  : 1;//1->在extra_rc存儲引用計數將要溢出的時候,藉助Sidetable(散列表)存儲引用計數,has_sidetable_rc設置成1
        uintptr_t extra_rc          : 19;  //->存儲引用計數
    };
};

其中nonpointerweakly_referencedhas_sidetable_rcextra_rc都是 ARC有直接關係的成員變量,其他的大多也有涉及到。

retain,release做了哪些優化

大概可以分爲如下

  • TaggedPointer 指針優化
  • !newisa.nonpointer:未優化的 isa 的情況下retain或者release
  • newisa.nonpointer:已優化的 isa , 這其中又分 extra_rc 溢出區別 我把相關代碼站在下面並且把結論輸出出來.
內存操作 objc_retain objc_release
TaggedPointer 值存在指針內,直接返回 直接返回 false。
!nonpointer 未優化的isa,使用sidetable_retain() 未優化的isa執行sidetable_release
nonpointer 已優化的isa,這其中又分extra_rc溢出和未溢出的兩種情況 已優化的isa,分下溢和未下溢兩種情況
nonpointer已優化isa的extra_rc objc_retain objc_release
未溢出時 isa.extra_rc+1 NA
溢出時 isa.extra_rc中一半值轉移至sidetable中,然後將isa.has_sidetable_rc設置爲true,表示使用了sidetable來計算引用次數 NA
未下溢 NA extra_rc--
下溢 NA sidetable中借位給extra_rc達到半滿,如果無法借位則說明引用計數歸零需要進行釋放,其中借位時可能保存失敗會不斷重試

NA -> non available 不可獲得

下面我們看下retain源碼

ALWAYS_INLINE id objc_object::rootRetain(bool tryRetain, bool handleOverflow) {
    if (isTaggedPointer()) return (id)this;     // 如果是 TaggedPointer 直接返回
    bool sideTableLocked = false;
    bool transcribeToSideTable = false;
    isa_t oldisa;
    isa_t newisa;
    do {
        transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);  // 獲取 isa
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);// 未優化的 isa 部分
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();
        }
        if (slowpath(tryRetain && newisa.deallocating)) { // 正在被釋放的處理
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            return nil;
        }
        // extra_rc 未溢出時引用計數++
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
        // extra_rc 溢出
        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);   // 重新調用該函數 入參 handleOverflow 爲 true
            } 
            // 保留一半引用計數,準備將另一半複製到 side table.
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
        //  更新 isa 值
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
    if (slowpath(transcribeToSideTable)) {
        sidetable_addExtraRC_nolock(RC_HALF); // 將另一半複製到 side table side table.
    }
    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    return (id)this;
}

release源碼

ALWAYS_INLINE bool objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
    if (isTaggedPointer()) return false;
    bool sideTableLocked = false;
    isa_t oldisa;
    isa_t newisa;
 retry:
    do {
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);// 未優化 isa
            if (sideTableLocked) sidetable_unlock();
            return sidetable_release(performDealloc);// 入參是否要執行 Dealloc 函數,如果爲 true 則執行 SEL_dealloc
        }
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        if (slowpath(carry)) {
            // donot ClearExclusive()
            goto underflow;
        }
        // 更新 isa 值
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                             oldisa.bits, newisa.bits)));
    if (slowpath(sideTableLocked)) sidetable_unlock();
    return false;
 underflow:
    // 處理下溢,從 side table 中借位或者釋放
    newisa = oldisa;
    if (slowpath(newisa.has_sidetable_rc)) { // 如果使用了 sidetable_rc
        if (!handleUnderflow) {
            ClearExclusive(&isa.bits);// 調用本函數處理下溢
            return rootRelease_underflow(performDealloc);
        }
        size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF); // 從 sidetable 中借位引用計數給 extra_rc

        if (borrowed > 0) {
        // extra_rc 是計算額外的引用計數,0 即表示被引用一次
            newisa.extra_rc = borrowed - 1;  // redo the original decrement too
            bool stored = StoreReleaseExclusive(&isa.bits, 
                                                oldisa.bits, newisa.bits);                                    
            // 保存失敗,恢復現場,重試                                    
            if (!stored) {
                isa_t oldisa2 = LoadExclusive(&isa.bits);
                isa_t newisa2 = oldisa2;
                if (newisa2.nonpointer) {
                    uintptr_t overflow;
                    newisa2.bits = 
                        addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
                    if (!overflow) {
                        stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits, 
                                                       newisa2.bits);
                    }
                }
            }
        // 如果還是保存失敗,則還回 side table
            if (!stored) {
                sidetable_addExtraRC_nolock(borrowed);
                goto retry;
            }
            sidetable_unlock();
            return false;
        }
        else {
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }
    // 沒有使用 sidetable_rc ,或者 sidetable_rc 計數 == 0 的就直接釋放
    // 如果已經是釋放中,拋個過度釋放錯誤
    if (slowpath(newisa.deallocating)) {
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        return overrelease_error();
    }
    // 更新 isa 狀態
    newisa.deallocating = true;
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
    if (slowpath(sideTableLocked)) sidetable_unlock();
    // 執行 SEL_dealloc 事件
    __sync_synchronize();
    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return true;
}

小結

到這裏可以知道 引用計數分別保存在isa.extra_rcsidetable中,當isa.extra_rc溢出時,將一半計數轉移至sidetable中,而當其下溢時,又會將計數轉回。當二者都爲空時,會執行釋放流程

ARC下哪些情況會造成內存泄漏

  • block中的循環引用
  • NSTimer的循環引用
  • addObserver的循環引用
  • delegate的強引用
  • 大次數循環內存爆漲
  • 非OC對象的內存處理(需手動釋放)

總結

以上就是我們討論上述一套面試題的 runtime相關問題之 內存管理部分, 感謝各位支持!

推薦

收錄:原文地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章