iOS 全面深入理解 Category 類(lèi)別笆焰,+ (void)load 與 + (void)initialize及關(guān)聯(lián)對(duì)象實(shí)現(xiàn)原理

引言

類(lèi)別 category 允許你在沒(méi)有源代碼情況下威蕉,仍然可以向已有的類(lèi)中添加方法的诵。它的功能很強(qiáng)大莫绣,允許你無(wú)需子類(lèi)化而擴(kuò)展現(xiàn)有類(lèi)喧枷。使用類(lèi)別,還可以將類(lèi)的實(shí)現(xiàn)分發(fā)到多個(gè)文件中沮焕。類(lèi)擴(kuò)展 extension 與此類(lèi)似吨岭,但允許在主類(lèi)@interface 塊內(nèi)以外的位置為類(lèi)聲明額外的 API。


  • 代碼示例

Category:

#import "ClassName.h"
 
@interface ClassName (CategoryName)
// method declarations
@end

Extension:

@interface MyClass : NSObject
@property (nonatomic, copy, readonly) NSString *name;
@end
 
// Private extension, typically hidden in the main implementation file.
@interface MyClass ()
@property (nonatomic, copy, readwrite) NSString *name;
@end

  • 本質(zhì)

您可以通過(guò)在接口文件中遇汞,以類(lèi)別名稱(chēng)聲明它們未妹,并在實(shí)現(xiàn)文件中以相同名稱(chēng)定義它們來(lái)將方法添加到類(lèi)簿废。類(lèi)別名稱(chēng)表明這些方法是對(duì)在別處聲明的類(lèi)的添加空入,而不是一個(gè)新類(lèi)。但是族檬,不能通過(guò)類(lèi)別添加實(shí)例變量到類(lèi)中歪赢。

類(lèi)別添加的方法成為類(lèi)類(lèi)型的一部分。例如单料,在一個(gè)類(lèi)別中添加到 NSArray類(lèi)中的方法埋凯,是編譯器期望 NSArray 實(shí)例在其配置表中包含的方法点楼。然而,子類(lèi)中添加到 NSArray 類(lèi)中的方法并不包含在 NSArray 類(lèi)型中白对。(這只對(duì)靜態(tài)類(lèi)型的對(duì)象有影響掠廓,因?yàn)殪o態(tài)類(lèi)型是編譯器知道對(duì)象類(lèi)的唯一方式。)


  • 常用介紹

類(lèi)別:

Category 在經(jīng)歷過(guò)編譯后里面的內(nèi)容:對(duì)象方法甩恼、類(lèi)方法蟀瞧、協(xié)議、屬性都轉(zhuǎn)化為類(lèi)型為 category_t 的結(jié)構(gòu)體變量:

struct category_t {
    const char *name;
    classref_t cls;
    WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;
    WrappedPtr<method_list_t, PtrauthStrip> classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
    
    protocol_list_t *protocolsForMeta(bool isMeta) {
        if (isMeta) return nullptr;
        else return protocols;
    }
};

具體 category 都能做什么条摸,常用的大致有如下幾個(gè)場(chǎng)景:

  1. 在不修改原有類(lèi)的基礎(chǔ)上給原有類(lèi)添加方法悦污,因?yàn)?category 的結(jié)構(gòu)體指針中沒(méi)有屬性列表,只有方法列表钉蒲。所以原則來(lái)說(shuō)只能給 category 添加方法切端,不能添加屬性,如果需要給 category 添加類(lèi)似屬性功能顷啼,可以通過(guò)關(guān)聯(lián)對(duì)象實(shí)現(xiàn)踏枣,下面會(huì)有具體介紹;
  2. Category 中的方法優(yōu)先于原有類(lèi)同名的方法钙蒙,即會(huì)優(yōu)先調(diào)用 category 中的方法椰于,忽略原有類(lèi)的方法。即 category 與原有類(lèi)同名方法調(diào)用的優(yōu)先級(jí)為: category > 本類(lèi) > 父類(lèi)仪搔。開(kāi)發(fā)中盡量不要覆蓋本類(lèi)的方法瘾婿,如果覆蓋會(huì)導(dǎo)致本類(lèi)方法失效;
  3. 如果給 category 添加屬性 @property烤咧,只會(huì)生成 setter/getter 方法的聲明偏陪,并不會(huì)有具體的代碼實(shí)現(xiàn),詳細(xì)解釋可參考?xì)v史文章:iOS 屬性 @property 詳細(xì)探究
  4. Category 中可以訪問(wèn)原有類(lèi)中 .h 中聲明的成員變量煮嫌;

類(lèi)的擴(kuò)展 Extension:

@interface Person ()

@end

類(lèi)的 extension 看起來(lái)很像一個(gè)匿名的 category笛谦。通常用來(lái)聲明私有方法,私有屬性和私有成員變量昌阿。

extension 在編譯期決議饥脑, category 在運(yùn)行期決議。

類(lèi)擴(kuò)展不能像類(lèi)別 category 那樣擁有獨(dú)立的實(shí)現(xiàn)部分(@implementation 部分)懦冰。也就是說(shuō)灶轰,類(lèi)的擴(kuò)展所聲明的方法必須依托原類(lèi)的實(shí)現(xiàn)代碼部分來(lái)實(shí)現(xiàn)。

因此刷钢,我們不能給系統(tǒng)類(lèi)添加類(lèi)擴(kuò)展笋颤。即擴(kuò)展的方法只能在原類(lèi)中實(shí)現(xiàn)。例如我們擴(kuò)展 NSString 内地,那么只能在 NSString的.m 中實(shí)現(xiàn)伴澄,但我們拿不到 NSString.m 的源碼赋除。因此,我們不能給 NSString 添加擴(kuò)展非凌,只能給 NSString 添加 category 举农。

定義在 .m 文件中的類(lèi)擴(kuò)展方法為私有的,如果需要聲明私有方法敞嗡,這種方式特別合適并蝗。定義在 .h 文件(頭文件)中的類(lèi)擴(kuò)展方法為公有的。


類(lèi)別 Category 與擴(kuò)展 Extension 的區(qū)別

  1. Category 有名字秸妥,extension 沒(méi)有名字滚停,像是一個(gè)匿名的 category;
  2. Category 是運(yùn)行時(shí)決議粥惧,而 extension 是編譯時(shí)決議键畴。所以 category 中的方法沒(méi)有實(shí)現(xiàn)不會(huì)警告,而 extension 聲明的方法不實(shí)現(xiàn)則會(huì)出現(xiàn)警告突雪;
  3. Category 原則上可以增加屬性起惕,實(shí)例方法,類(lèi)方法咏删,而且外部類(lèi)是可以訪問(wèn)的惹想。extension 能添加屬性、方法督函、實(shí)例變量嘀粱,且默認(rèn)是私有的;
  4. Category 有自己的實(shí)現(xiàn)部分辰狡,extension 沒(méi)有自己的實(shí)現(xiàn)部分锋叨,只能依賴(lài)類(lèi)本身來(lái)實(shí)現(xiàn);
  5. 可以為系統(tǒng)類(lèi)添加 category宛篇,而不能為系統(tǒng)類(lèi)添加 extension娃磺;

關(guān)于類(lèi)的 + (void)load+ (void)initialize 的區(qū)別

+ (void)load

+ (void)initialize

  • 兩者的區(qū)別如下:
  1. 相同點(diǎn):
  • 兩個(gè)函數(shù)都是系統(tǒng)自動(dòng)調(diào)用,因此無(wú)需手動(dòng)調(diào)用(如果手動(dòng)調(diào)用則與普通函數(shù)調(diào)用類(lèi)似)叫倍;
  • 兩個(gè)函數(shù)都會(huì)隱士調(diào)用各自父類(lèi)對(duì)應(yīng)的 + (void)load+ (void)initialize 方法偷卧,即子類(lèi)調(diào)用方法之前,會(huì)優(yōu)先調(diào)用其父類(lèi)對(duì)應(yīng)的方法吆倦;
  • 兩個(gè)函數(shù)內(nèi)部都使用了鎖听诸,因此兩個(gè)函數(shù)都是線(xiàn)程安全的;
  1. 不同點(diǎn):
  1. 調(diào)用時(shí)機(jī)不同:+ (void)loadmain 函數(shù)之前執(zhí)行逼庞,即 objc_init Runtime初始化時(shí)調(diào)用蛇更,且只會(huì)調(diào)用一次。 + (void)initialize 在類(lèi)的方法首次被調(diào)用時(shí)執(zhí)行赛糟,每個(gè)類(lèi)只會(huì)調(diào)用一次派任,但父類(lèi)可能會(huì)調(diào)用多次;
  2. 調(diào)用方式不同:+ (void)load 是根據(jù)函數(shù)地址直接調(diào)用璧南,+ (void)initialize 是通過(guò)消息發(fā)送機(jī)制即 objc_msgSend(id self, SEL _cmd, ...) 調(diào)用掌逛;
  3. 子類(lèi)父類(lèi)調(diào)用關(guān)系不同:
  • 如果子類(lèi)沒(méi)有實(shí)現(xiàn) + (void)load,則不會(huì)調(diào)用其父類(lèi)的 + (void)load 方法司倚。
  • 如果子類(lèi)沒(méi)有實(shí)現(xiàn) + (void)initialize豆混,則會(huì)調(diào)用其父類(lèi)的方法,因此父類(lèi)的 + (void)initialize 可能會(huì)調(diào)用多次动知;
  1. 類(lèi)別 category 對(duì)調(diào)用的影響不同:
  • 如果 category 中實(shí)現(xiàn)了 + (void)load皿伺,則會(huì)優(yōu)先調(diào)用原類(lèi)的的 + (void)load,再調(diào)用 category 的盒粮,即優(yōu)先級(jí)為:父類(lèi) > 原類(lèi) > category
  1. 沒(méi)有繼承關(guān)系的不同類(lèi)中的 + (void)load 的調(diào)用順序跟 Compile Sources 順序有關(guān)鸵鸥,即在前面的優(yōu)先編譯的類(lèi)或者 category 先調(diào)用( 備注: 所有類(lèi)的 + (void)load 優(yōu)先級(jí)大于 category 的優(yōu)先級(jí));
  2. 同一個(gè)類(lèi)的 category+ (void)load 的調(diào)用順序跟 Compile Sources 順序有關(guān)丹皱,即在前面的優(yōu)先編譯的 category 會(huì)先調(diào)用妒穴;
  3. 同一鏡像中主工程的 + (void)load 方法優(yōu)先調(diào)用,然后再調(diào)用靜態(tài)庫(kù)的 + (void)load 方法摊崭。有多個(gè)靜態(tài)庫(kù)時(shí)讼油,靜態(tài)庫(kù)之間的執(zhí)行順序與編譯順序有關(guān),即它們?cè)?Link Binary With Libraries 中的順序呢簸;
  4. 不同鏡像中矮台,動(dòng)態(tài)庫(kù)的 + (void)load 方法優(yōu)先調(diào)用,然后再調(diào)用主工程的 + (void)load根时,多個(gè)動(dòng)態(tài)庫(kù)的 + (void)load 方法的調(diào)用順序跟編譯順序有關(guān)嘿架,即它們?cè)?Link Binary With Libraries 中的順序;
  • 如果 category 中實(shí)現(xiàn)了 + (void)initialize啸箫,則原類(lèi)的 + (void)initialize 將不會(huì)再調(diào)用
  1. 多個(gè) category 中同時(shí)實(shí)現(xiàn)了 + (void)initialize 方法時(shí)耸彪,Compile Sources中順序最下面的一個(gè),即最后一個(gè)被編譯 Category 的 + (void)initialize 會(huì)執(zhí)行忘苛;

類(lèi)別 Category 中添加關(guān)聯(lián)對(duì)象

Category 中添加屬性 @property 在前文已做過(guò)簡(jiǎn)單介紹蝉娜,具體可查看 iOS 屬性 @property 詳細(xì)探究,這里我們重點(diǎn)說(shuō)一下關(guān)聯(lián)對(duì)象的實(shí)現(xiàn)原理:

操作關(guān)聯(lián)對(duì)象有三個(gè)核心方法:

  1. 設(shè)置關(guān)聯(lián)對(duì)象方法:

objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)

    1. id _Nonnull object: 給哪個(gè)對(duì)象添加關(guān)聯(lián)對(duì)象扎唾,通常是當(dāng)前對(duì)象召川,即用 self 即可;
    1. const void * _Nonnull key: 關(guān)聯(lián)對(duì)象的 key胸遇,作為關(guān)聯(lián)對(duì)象的唯一標(biāo)識(shí)存在荧呐,它只要是一個(gè)非空指針即可;
    1. id _Nullable value: 關(guān)聯(lián)對(duì)象的值,通過(guò)關(guān)聯(lián) key 進(jìn)行設(shè)值及獲取值倍阐,如果需要清除一個(gè)已存在的關(guān)聯(lián)對(duì)象概疆,將其值設(shè)置為 nil 即可;
    1. objc_AssociationPolicy policy: 關(guān)聯(lián)策略峰搪,即關(guān)聯(lián)對(duì)象的存儲(chǔ)形式岔冀,其可選枚舉值如下:
public enum objc_AssociationPolicy : UInt {
    case OBJC_ASSOCIATION_ASSIGN = 0 // 指定對(duì)關(guān)聯(lián)對(duì)象的弱引用
    case OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1 // 指定對(duì)關(guān)聯(lián)對(duì)象的強(qiáng)引用,非原子性
    case OBJC_ASSOCIATION_COPY_NONATOMIC = 3 // 指定復(fù)制關(guān)聯(lián)的對(duì)象概耻,非原子性
    case OBJC_ASSOCIATION_RETAIN = 769 // 指定對(duì)關(guān)聯(lián)對(duì)象的強(qiáng)引用使套,原子性
    case OBJC_ASSOCIATION_COPY = 771 // 指定復(fù)制關(guān)聯(lián)的對(duì)象,原子性
} 

根據(jù)源碼鞠柄,我們可以知道 objc_setAssociatedObject 實(shí)際調(diào)用的是 _object_set_associative_reference:

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    // This code used to work when nil was passed for object and key. Some code
    // probably relies on that to not crash. Check and handle it explicitly.
    // rdar://problem/44094390
    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};

    // retain the new value (if any) outside the lock.
    association.acquireValue();

    bool isFirstAssociation = false;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());

        if (value) {
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {
                /* it's the first association we make */
                isFirstAssociation = true;
            }

            /* establish or replace the association */
            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);

                    }
                }
            }
        }
    }

    // Call setHasAssociatedObjects outside the lock, since this
    // will call the object's _noteAssociatedObjects method if it
    // has one, and this may trigger +initialize which might do
    // arbitrary stuff, including setting more associated objects.
    if (isFirstAssociation)
        object->setHasAssociatedObjects();

    // release the old value (outside of the lock).
    association.releaseHeldValue();
}

根據(jù)上述源碼可以發(fā)現(xiàn)侦高,ObjcAssociation 根據(jù)傳入的 valuepolicy 創(chuàng)建對(duì)象,并經(jīng)過(guò) acquireValue 函數(shù)處理生成新的 _value 厌杜。acquireValue 函數(shù)內(nèi)部是通過(guò)對(duì)策略 policy 的判斷進(jìn)行相應(yīng)處理奉呛,生成新值,其實(shí)現(xiàn)如下:

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

接下來(lái)我們首先需要了解一下 AssociationsManagerAssociationsHashMap

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 是以 DisguisedPtr<objc_object> 即一個(gè)指針地址作為 key侧馅,以 ObjectAssociationMap 即一個(gè)關(guān)聯(lián)表作為 value 的哈希表來(lái)使用的。其內(nèi)部是使用一個(gè)全局靜態(tài)變量 static Storage _mapStorage 來(lái)存儲(chǔ)程序中所有的關(guān)聯(lián)對(duì)象呐萌。

這里重點(diǎn)介紹一下全局靜態(tài)變量 static Storage _mapStorage 的初始化時(shí)機(jī)馁痴。App 啟動(dòng)過(guò)程中,在 _objc_init 函數(shù)中會(huì)調(diào)用 void _dyld_objc_notify_register(...)肺孤,具體如下:

void _objc_init(void)
{
    //...
    // 此處僅保留談到的函數(shù)
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
    //...
}

dyld 源碼中可以看到罗晕,函數(shù) _dyld_objc_notify_register 中的三個(gè)參數(shù)為三個(gè)回調(diào)函數(shù)的指針,如下圖:

回調(diào)函數(shù)會(huì)在所有鏡像文件初始化完成之后赠堵,回調(diào) map_images(unsigned count, const char * const paths[], const struct mach_header * const mhdrs[]) 函數(shù)小渊。詳細(xì)調(diào)用流程如下圖:

備注:圖中已對(duì)無(wú)關(guān)代碼進(jìn)行刪減,僅用來(lái)展示調(diào)用流程

從上圖我們可以知道茫叭,在 App 啟動(dòng)過(guò)程中 AssociationsManager 中的靜態(tài)變量 static Storage _mapStorage 的初始化時(shí)機(jī)酬屉。在 App 啟動(dòng)之后,所有用到關(guān)聯(lián)對(duì)象的地方揍愁,程序都是從這個(gè)全局靜態(tài)變量 _mapStorage 中獲取 AssociationsHashMap 來(lái)對(duì)關(guān)聯(lián)對(duì)象進(jìn)行進(jìn)一步處理呐萨。

AssociationsManager 中,我們可以看到是由一個(gè) AssociationsManagerLock 叫做 spinlock_t 的互斥鎖:

using spinlock_t = mutex_tt<LOCKDEBUG>;

它是用來(lái)保障 AssociationsManager 中對(duì) AssociationsHashMap 操作的線(xiàn)程安全莽囤。

AssociationsHashMap &get() {
    return _mapStorage.get();
}

對(duì)于 AssociationsHashMap 這個(gè)哈希表谬擦,則是由全局靜態(tài)變量 _mapStorage 獲取而來(lái),因此不管任何時(shí)候操作關(guān)聯(lián)對(duì)象朽缎,程序始終都是在操作這個(gè) AssociationsHashMap 全局唯一的哈希表惨远。

再回到上面 _object_set_associative_reference 源碼中谜悟,當(dāng)我們添加一個(gè)關(guān)聯(lián)對(duì)象時(shí),AssociationsHashMap 會(huì)調(diào)用如下函數(shù):

auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});

try_emplace 函數(shù)的源碼如下:

  template <typename... Ts>
  std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args) {
    BucketT *TheBucket;
    if (LookupBucketFor(Key, TheBucket))
      return std::make_pair(
               makeIterator(TheBucket, getBucketsEnd(), true),
               false); // Already in map.

    // Otherwise, insert the new element.
    TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);
    return std::make_pair(
             makeIterator(TheBucket, getBucketsEnd(), true),
             true);
  }

首先根據(jù)傳來(lái)的 keydisguisedAssociationsHashMap 中查找對(duì)應(yīng)的 ObjectAssociationMap 是否已在映射表中北秽,如果不在則將元素插入葡幸。如果鍵不在,則創(chuàng)建一個(gè) BucketT 即一個(gè)空的桶羡儿。在第二次調(diào)用 try_emplace 時(shí)將 ObjcAssociation (里面包含了 _policy_value )存儲(chǔ)到這個(gè) BucketT 空桶中礼患。

當(dāng)設(shè)置的關(guān)聯(lián) value 為空 nil 的時(shí)候會(huì)進(jìn)入 if 判斷的 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);

        }
    }
}

先去 AssociationsHashMap 里面查找 disguised 是钥,如果找到則根據(jù) key 查找到指定的關(guān)聯(lián)對(duì)象掠归,然后進(jìn)行清除 erase 操作。之后判斷當(dāng)前 object 的關(guān)聯(lián)對(duì)象是否為0悄泥,如果為0虏冻,則將當(dāng)前關(guān)聯(lián)對(duì)象從全局的 AssociationsHashMap 中移除。

  1. 獲取關(guān)聯(lián)對(duì)象方法:

objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)

    1. id _Nonnull object: 獲取哪個(gè)對(duì)象里面的關(guān)聯(lián)對(duì)象弹囚;
    1. const void * _Nonnull key: 關(guān)聯(lián)對(duì)象的 key 厨相,與 objc_setAssociatedObject 中的 key 相對(duì)應(yīng),通過(guò) key 值取出 value 即關(guān)聯(lián)對(duì)象鸥鹉;

其內(nèi)部調(diào)用的是 _object_get_associative_reference 蛮穿,內(nèi)部具體實(shí)現(xiàn)如下:

如果我們理解了設(shè)置關(guān)聯(lián)對(duì)象的過(guò)程,上面的代碼理解起來(lái)就比較簡(jiǎn)單了毁渗,從全局的 AssociationsHashMap 中取得 object 對(duì)象對(duì)應(yīng)的 ObjectAssociationMap 践磅,然后根據(jù) keyObjectAssociationMap 獲取對(duì)應(yīng)的 ObjcAssociation ,然后根據(jù)關(guān)聯(lián)策略 _policy 判斷是否需要對(duì) _value 執(zhí)行 retain 操作灸异。最后根據(jù)關(guān)聯(lián)策略 _policy 判斷是否需要將 _value 添加到自動(dòng)釋放池府适,并返回 _value

  1. 移除關(guān)聯(lián)對(duì)象:
    上面已經(jīng)提到肺樟,如果想要清除某一個(gè)特定關(guān)聯(lián)對(duì)象檐春,設(shè)置關(guān)聯(lián)對(duì)象的 valuenil 即可。如果想要移除所有關(guān)聯(lián)對(duì)象么伯,則可以使用:

objc_removeAssociatedObjects(id _Nonnull object)

    1. id _Nonnull object: 移除指定對(duì)象的所有關(guān)聯(lián)對(duì)象

其內(nèi)部實(shí)現(xiàn)代碼如下:


當(dāng)調(diào)用移除關(guān)聯(lián)對(duì)象操作時(shí)疟暖,會(huì)先判斷 object 是否為空及是否有關(guān)聯(lián)對(duì)象存在,如果存儲(chǔ)則會(huì)調(diào)用 _object_remove_assocations 函數(shù)田柔。

從上圖其內(nèi)部實(shí)現(xiàn)代碼可以看到俐巴,程序會(huì)獲取全局的 AssociationsHashMap 然后從中獲取對(duì)象對(duì)應(yīng)的 ObjectAssociationMap ,注釋說(shuō)如果不是 deallocating凯楔,則系統(tǒng)的關(guān)聯(lián)對(duì)象將會(huì)保留窜骄。而 objc_removeAssociatedObjects 函數(shù)傳入的 deallocating 參數(shù)為 false,因此我們可以推斷摆屯,解除關(guān)聯(lián)必定不是在調(diào)用 objc_removeAssociatedObjects 時(shí)邻遏。

于是糠亩,我搜索了一下 _object_remove_assocations,發(fā)現(xiàn)了真正的調(diào)用時(shí)機(jī)准验,即在 objc_destructInstance 函數(shù)調(diào)用時(shí)赎线,如上圖。

那什么時(shí)候會(huì)調(diào)用 objc_destructInstance 函數(shù)呢糊饱?帶著這個(gè)疑問(wèn)垂寥,我查了一下源碼,這里簡(jiǎn)單說(shuō)一下調(diào)用流程另锋,后續(xù)會(huì)專(zhuān)門(mén)針對(duì) dealloc 寫(xiě)相關(guān)文章滞项,其大體流程如下:

dealloc.png

圖中函數(shù)調(diào)用流程非常清晰,此處不做過(guò)多解釋夭坪。由此文判,我們知道解除關(guān)聯(lián)對(duì)象是在源對(duì)象 dealloc 時(shí)進(jìn)行的。


拓展知識(shí)

  1. iOS 中變量修飾詞 @public室梅、@protected戏仓、@package@private 的作用:

@package // 常用于框架類(lèi)的實(shí)例變量亡鼠,使用 @private 太限制赏殃,使用 @protected 或者 @public 又太開(kāi)放,這時(shí)可以使用 @package

@private // 作用范圍只能在自身類(lèi)间涵,即使子類(lèi)也無(wú)法使用仁热,但 category 及 extension 類(lèi)中可以使用

@protected // 系統(tǒng)默認(rèn)為 @protected,作用范圍在自身類(lèi)及子類(lèi)

@public // 公開(kāi)類(lèi)型浑厚,作用域大股耽,只要能拿到所屬實(shí)例對(duì)象就可以使用

實(shí)例變量范圍圖(@package 的范圍圖中未展示)

@interface Person : NSObject {
@package
    NSString *_country; // 框架內(nèi)拿到 Person 及其子類(lèi)的實(shí)例變量都可以使用

@protected
    NSString *_birthday; // 只能在自身類(lèi)及子類(lèi)中使用,包括 category 及 extension

@private
    NSString *_weight; // 只能在自身類(lèi)中使用钳幅,包括 category 及 extension

@public
    NSString *_height; // 全局任意拿到 Person 及其子類(lèi)實(shí)例變量的地方都可以使用
}

具體實(shí)例如下: Son 繼承自 Person

@interface Son : Person

@end

@protected
從上圖示例代碼可以看到物蝙,在子類(lèi)中是可以訪問(wèn)父類(lèi)的 @protected _birthday 成員變量,但不能訪問(wèn)父類(lèi)的 @private _weight 成員變量敢艰。

從上圖示例代碼可以看到诬乞,在其他類(lèi)中是可以訪問(wèn)父類(lèi)的 @protected _birthday 成員變量,但不能訪問(wèn)父類(lèi)的 @private _weight 成員變量钠导。


總結(jié)

類(lèi)別 category 和擴(kuò)展 extension 涉及到的東西還是挺多的震嫉,這里僅對(duì)其核心要關(guān)注的一些點(diǎn)進(jìn)行了詳細(xì)介紹。另外還有關(guān)于 category 裝載的過(guò)程牡属,有興趣的同學(xué)可以查閱一下票堵。以上就是本文對(duì)類(lèi)別 category 和 擴(kuò)展 extension 相關(guān)知識(shí)點(diǎn)的介紹,感謝閱讀逮栅。


參考資料:


關(guān)于技術(shù)組

iOS 技術(shù)組主要用來(lái)學(xué)習(xí)悴势、分享日常開(kāi)發(fā)中使用到的技術(shù)窗宇,一起保持學(xué)習(xí),保持進(jìn)步特纤。文章倉(cāng)庫(kù)在這里:https://github.com/minhechen/iOSTechTeam
微信公眾號(hào):iOS技術(shù)組军俊,歡迎聯(lián)系進(jìn)群學(xué)習(xí)交流,感謝閱讀捧存。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末粪躬,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子昔穴,更是在濱河造成了極大的恐慌镰官,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件傻咖,死亡現(xiàn)場(chǎng)離奇詭異朋魔,居然都是意外死亡岖研,警方通過(guò)查閱死者的電腦和手機(jī)卿操,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)孙援,“玉大人害淤,你說(shuō)我怎么就攤上這事⊥厥郏” “怎么了窥摄?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)础淤。 經(jīng)常有香客問(wèn)我崭放,道長(zhǎng),這世上最難降的妖魔是什么鸽凶? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任币砂,我火速辦了婚禮,結(jié)果婚禮上玻侥,老公的妹妹穿的比我還像新娘决摧。我一直安慰自己,他們只是感情好凑兰,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布掌桩。 她就那樣靜靜地躺著,像睡著了一般姑食。 火紅的嫁衣襯著肌膚如雪波岛。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,007評(píng)論 1 284
  • 那天音半,我揣著相機(jī)與錄音则拷,去河邊找鬼灰蛙。 笑死,一個(gè)胖子當(dāng)著我的面吹牛隔躲,可吹牛的內(nèi)容都是我干的摩梧。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼宣旱,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼仅父!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起浑吟,我...
    開(kāi)封第一講書(shū)人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤笙纤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后组力,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體省容,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年燎字,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了腥椒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡候衍,死狀恐怖笼蛛,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蛉鹿,我是刑警寧澤滨砍,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站妖异,受9級(jí)特大地震影響惋戏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜他膳,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一响逢、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧矩乐,春花似錦龄句、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至欧漱,卻和暖如春职抡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背误甚。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工缚甩, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留谱净,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓擅威,卻偏偏與公主長(zhǎng)得像壕探,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子郊丛,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容