Category原理解析

前言

iOS 開發中,使用的編程語言主要是 Objective-C。這一種編程語言雖然是 C/C++ 的擴展,但是得益于 Runtime 的機制,使得 Objective-CC/C++ 更加具備可控性。實際上說 Objective-C 是一門可編譯的動態編程語言,C/C++ 則是一門靜態編程語言。
Objective-C 編程語言的 Runtime 機制里,每一個 class 都是以 Struct 的形式存在。文章主要是討論 Category 的本質以及運行過程。本文將會結合官方 Runtime 的源碼進行解析,這里Xcode可編譯的源碼已經放到 github 上了。由于 AppleGNU 公司一直在維護兩個不一樣的版本。隨著發展,代碼也會相應的有所變化,目前源代碼是基于 objc runtime 723 版本進行分析。

預知知識

闡述 Category 之前,就必須需要預先了解 Class 的的本質以及運行時,畢竟 Category 是依賴于 Class 而存在。不過本文只對 Class 作必要的簡述,畢竟本文主要是為了闡述 Category 的原理。接下來這一節本文會通過以下幾個方面了解 Class。

Class 的本質

本文講述什么是 Class,并不是從概念上說這個問題,而是針對 Objective-C 這門編程語言而言,在 C/C++ 是怎么利用 Runtime 進行封裝,讓 Objective-C 具有動態性。這里就為了能夠解釋清楚 Category,講述有關 Class 的兩個 Struct 分別是 objc_objectobjc_class。這些相關代碼可以在 runtime.h 頭文件中找到。

objc_object 實例對象

如下代碼,創建的實例對象,其實是 objc_object 結構體。Runtime 會去通過結構體里的 isa 指針找到對應的 Class。

//實例對象
struct objc_object {
    //指向當前類結構體(objc_class)
    Class isa  OBJC_ISA_AVAILABILITY;
};

objc_class 類

類結構體(objc_class)的作用就是實例對象(objc_object)調用的實例方法和實例屬性都會在這里能夠找得到。如下就是類結構體(objc_class)的代碼結構:

//類
struct objc_class {
    //指向元類(MetaClass),類型也是 objc_class(主要存放對應的類屬性和方法)
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    //指向父類(objc_class)
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    //類名
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    //版本號
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    //實例對象大小
    long instance_size                                       OBJC2_UNAVAILABLE;
    //變量列表(Category不支持添加變量)
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    //方法列表(指針的指針)
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    //方法緩存列表(調用過的方法會緩存起來方便下次快速調用)
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    //遵守的協議列表
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

以上類結構體(objc_class)中,需要關注的是變量列表objc_ivar_list和方法列表objc_method_list。

首先,變量列表objc_ivar_list的代碼結構如下:

//變量列表結構體
struct objc_ivar_list {
    //變量屬性數量
    int ivar_count                                           OBJC2_UNAVAILABLE;
#ifdef __LP64__
    //列表所需空間
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    // 可變長度結構體,用于存儲變量數組
    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
}

//變量
struct objc_ivar {
    //變量名
    char * _Nullable ivar_name                               OBJC2_UNAVAILABLE;
    //變量類型
    char * _Nullable ivar_type                               OBJC2_UNAVAILABLE;
    //變量存儲偏移量
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    //所需空間
    int space                                                OBJC2_UNAVAILABLE;
#endif
}

Meta Class 元類

objc_class(類)里的 isa 指針指向的是 Meta Class(元類),而 Meta Class 其實本質上也是對應的 objc_class 結構體。不過 Meta Class 存儲的屬性和方法都是類方法和類成員屬性。每個 Class 的 Meta Class 中的 isa 指針會直接指向 NSObject 的 Meta Class。

Class 的運行時

Objective-C 對象在編譯的時候回先轉化為中間語言 IR 編程語言,然后會進行代碼優化。最后一步,代碼優化完成之后會編譯成二進制代碼執行。另一方面,二進制兼容涉及到另外一個 Non Fragile ivars 機制。本文不再做介紹,參考文檔有詳細介紹。

接下來,就簡單說明一下 Objective-C 對象在運行時的內存存儲類型和指向。詳情看以下圖片:

Class 運行時

如上圖可知,Objective-C 的對象對應的結構體最終都會指向根類 NSObject。調用的方法和屬性都會在結構體里面相應的表里去找。方法列表可以認為是以 SELKeyIMPValueHash 表。

Category 的基本使用

在 Xcode 中使用 Category 很簡單,可以在里面添加方法和遵守相應的協議。當然,也可以重寫方法(蘋果不建議),或者添加屬性(沒有成員變量的屬性)。基本的使用由以下代碼展示:

@interface NSObject (Log)
// Category 中不能添加成員變量(這樣寫需要自己實現get&set方法)
@property (nonatomic, copy) NSString *name;

- (void)testLog;

@end

@implementation NSObject (Log)

- (void)testLog {
    NSLog(@"添加方法");
}

#pragma mark - Getter & Setter
const void *kNameKey; //可用這樣關聯屬性的key
- (NSString *)name { //屬性的get方法
    //從 AssociationsHashMap 取值,以 @selector(name) 作為 Key
    NSString *kname = objc_getAssociatedObject(self, _cmd);
    return kname;
}

- (void)setName:(NSString *)name { //屬性的set方法
    //在 AssociationsHashMap 添加 key-value
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY);
}

@end

以上代碼可知,在 Category 里添加屬性,變量是使用全局的 Hash 表進行管理。具體原因還是因為 Category 是在運行時才進行加載的,下文會進行詳細說明。

Category 與 Extension 的區別

CategoryExtension 從使用代碼層面上看是沒什么區別。這里在用法上可以說 ExtensionCategory特例。不過需要注意的是 Extension 只有聲明,沒有實現。具體可看以下代碼:

@interface NSObject ()
//添加成員屬性
@property (nonatomic, assign) NSInteger age;
//添加方法聲明
- (void)agePerson;

@end

以上代碼,聲明了屬性和方法。不過注意的是這里的屬性是有成員變量的。而不是需要存儲全局的 Hash 表中。其實本質上原因是 Extension編譯時期就加載到類中,而 Category 卻是在運行時期才加載到 Class 結構體中。

Category 的本質

Category 的本質主要分為編譯時期運行時期兩個階段分別闡述。首先,需要知道的是編譯時期的 Category 并不會把其內容編譯進 Class 中,只有在運行時期 Class 加載完之后,才會添加到已加載在內存中的 Class 中。
首先,先查看 objc-runtime-new.h 文件中的 Category 結構體,代碼如下:

// 分類結構體
struct category_t {
    // 分類名稱
    const char *name;
    // 類型
    classref_t cls;
    // 實例方法列表
    struct method_list_t *instanceMethods;
    // 類方法列表
    struct method_list_t *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);
};

如上代碼可知,Category 可以向 Class 中添加實例方法、類方法遵守的協議以及成員屬性。不過,需要注意的是這里說的是屬性不是成員變量,Category 是不可以添加成員變量的。

Category 的運行時

Objective-C 在運行時,會調用初始化入口 _objc_init 函數,進行裝載 Classdyld、image 等操作。

預備知識

在了解運行時加載 Category 之前,需要提前了解一下 Class 在加載到內存里,是怎么封裝這些結構體的。主要從 class_ro_tclass_rw_t 結構體,以及 realizeClass 方法說起。

class_ro_t 結構體

objc_class 包含了 class_data_bits_t,class_data_bits_t 存儲了 class_rw_t 的指針,而 class_rw_t 結構體又包含 class_ro_t 的指針。
class_ro_t 中的 method_list_t, ivar_list_t, property_list_t 結構體都繼承自 entsize_list_tt<Element, List, FlagMask>。結構為 xxx_list_t 的列表元素結構為 xxx_t,命名很工整。protocol_list_t 與前三個不同,它存儲的是 protocol_t * 指針列表,實現比較簡單。
entsize_list_tt 實現了 non-fragile 特性的數組結構。假如蘋果在新版本的 SDK 中向 NSObject 類增加了一些內容,NSObject 的占據的內存區域會擴大,開發者以前編譯出的二進制中的子類就會與新的 NSObject 內存有重疊部分。于是在編譯期會給 instanceStartinstanceSize 賦值,確定好編譯時每個類的所占內存區域起始偏移量和大小,這樣只需將子類與基類的這兩個變量作對比即可知道子類是否與基類有重疊,如果有,也可知道子類需要挪多少偏移量。

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

class_ro_t->flags 存儲了很多在編譯時期就確定的類的信息,也是 ABI 的一部分。下面這些 RO_ 前綴的宏標記了 flags 一些位置的含義。其中后三個并不需要被編譯器賦值,是預留給運行時加載和初始化類的標志位,涉及到與 class_rw_t 的類型強轉。

#define RO_META               (1<<0) // class is a metaclass
#define RO_ROOT               (1<<1) // class is a root class
#define RO_HAS_CXX_STRUCTORS  (1<<2) // class has .cxx_construct/destruct implementations
// #define RO_HAS_LOAD_METHOD    (1<<3) // class has +load implementation
#define RO_HIDDEN             (1<<4) // class has visibility=hidden set
#define RO_EXCEPTION          (1<<5) // class has attribute(objc_exception): OBJC_EHTYPE_$_ThisClass is non-weak
// #define RO_REUSE_ME           (1<<6) // this bit is available for reassignment
#define RO_IS_ARC             (1<<7) // class compiled with ARC
#define RO_HAS_CXX_DTOR_ONLY  (1<<8) // class has .cxx_destruct but no .cxx_construct (with RO_HAS_CXX_STRUCTORS)
#define RO_HAS_WEAK_WITHOUT_ARC (1<<9) // class is not ARC but has ARC-style weak ivar layout 

#define RO_FROM_BUNDLE        (1<<29) // class is in an unloadable bundle - must never be set by compiler
#define RO_FUTURE             (1<<30) // class is unrealized future class - must never be set by compiler
#define RO_REALIZED           (1<<31) // class is realized - must never be set by compiler

class_rw_t 結構體

class_rw_t 提供了運行時對類拓展的能力,而 class_ro_t 存儲的大多是類在編譯時就已經確定的信息。二者都存有類的方法、屬性(成員變量)、協議等信息,不過存儲它們的列表實現方式不同。
class_rw_t 中使用的 method_array_t, property_array_t, protocol_array_t 都繼承自 list_array_tt<Element, List>, 它可以不斷擴張,因為它可以存儲 list 指針,內容有三種:

  1. 一個 entsize_list_tt 指針
  2. entsize_list_tt 指針數組
    class_rw_t 的內容是可以在運行時被動態修改的,可以說運行時對類的拓展大都是存儲在這里的。
struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;

#if SUPPORT_INDEXED_ISA
    uint32_t index;
#endif

    void setFlags(uint32_t set) 
    {
        OSAtomicOr32Barrier(set, &flags);
    }

    void clearFlags(uint32_t clear) 
    {
        OSAtomicXor32Barrier(clear, &flags);
    }

    // set and clear must not overlap
    void changeFlags(uint32_t set, uint32_t clear) 
    {
        assert((set & clear) == 0);

        uint32_t oldf, newf;
        do {
            oldf = flags;
            newf = (oldf | set) & ~clear;
        } while (!OSAtomicCompareAndSwap32Barrier(oldf, newf, (volatile int32_t *)&flags));
    }
};

class_rw_t->flags 存儲的值并不是編輯器設置的,其中有些值可能將來會作為 ABI 的一部分。下面這些 RW_ 前綴的宏標記了 flags 一些位置的含義。這些 bool 值標記了類的一些狀態,涉及到聲明周期和內存管理。有些位目前甚至還空著。

#define RW_REALIZED           (1<<31) // class_t->data is class_rw_t, not class_ro_t
#define RW_FUTURE             (1<<30) // class is unresolved future class
#define RW_INITIALIZED        (1<<29) // class is initialized
#define RW_INITIALIZING       (1<<28) // class is initializing
#define RW_COPIED_RO          (1<<27) // class_rw_t->ro is heap copy of class_ro_t
#define RW_CONSTRUCTING       (1<<26) // class allocated but not yet registered
#define RW_CONSTRUCTED        (1<<25) // class allocated and registered
// #define RW_24 (1<<24) // available for use; was RW_FINALIZE_ON_MAIN_THREAD
#define RW_LOADED             (1<<23) // class +load has been called
#if !SUPPORT_NONPOINTER_ISA
#define RW_INSTANCES_HAVE_ASSOCIATED_OBJECTS (1<<22) // class instances may have associative references
#endif
#define RW_HAS_INSTANCE_SPECIFIC_LAYOUT (1 << 21) // class has instance-specific GC layout
// #define RW_20       (1<<20) // available for use
#define RW_REALIZING          (1<<19) // class has started realizing but not yet completed it
#define RW_HAS_CXX_CTOR       (1<<18) // class or superclass has .cxx_construct implementation
#define RW_HAS_CXX_DTOR       (1<<17) // class or superclass has .cxx_destruct implementation
// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
#define RW_HAS_DEFAULT_AWZ    (1<<16)
#if SUPPORT_NONPOINTER_ISA
#define RW_REQUIRES_RAW_ISA   (1<<15) // class's instances requires raw isa
#endif

demangledName 是計算機語言用于解決實體名稱唯一性的一種方法,做法是向名稱中添加一些類型信息,用于從編譯器中向鏈接器傳遞更多語義信息。

realizeClass 分析

在某個類初始化之前,objc_class->data() 返回的指針指向的其實是個 class_ro_t 結構體。等到 static Class realizeClass(Class cls) 靜態方法在類第一次初始化時被調用,它會開辟 class_rw_t 的空間,并將 class_ro_t 指針賦值給 class_rw_t->ro。
整體的方法代碼如下:

/***********************************************************************
* realizeClass
* Performs first-time initialization on class cls, 
* including allocating its read-write data.
* Returns the real class structure for the class. 
* Locking: runtimeLock must be write-locked by the caller
**********************************************************************/
static Class realizeClass(Class cls)
{
    runtimeLock.assertWriting();

    const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;

    if (!cls) return nil;
    if (cls->isRealized()) return cls;
    assert(cls == remapClass(cls));

    // fixme verify class is not in an un-dlopened part of the shared cache?
    // 結構體轉換過程
    ro = (const class_ro_t *)cls->data();
    if (ro->flags & RO_FUTURE) {
        // This was a future class. rw data is already allocated.
        rw = cls->data();
        ro = cls->data()->ro;
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // Normal class. Allocate writeable class data.
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
        rw->ro = ro;
        rw->flags = RW_REALIZED|RW_REALIZING;
        cls->setData(rw);
    }

    isMeta = ro->flags & RO_META;

    rw->version = isMeta ? 7 : 0;  // old runtime went up to 6


    // Choose an index for this class.
    // Sets cls->instancesRequireRawIsa if indexes no more indexes are available
    cls->chooseClassArrayIndex();

    if (PrintConnecting) {
        _objc_inform("CLASS: realizing class '%s'%s %p %p #%u", 
                     cls->nameForLogging(), isMeta ? " (meta)" : "", 
                     (void*)cls, ro, cls->classArrayIndex());
    }

    // Realize superclass and metaclass, if they aren't already.
    // This needs to be done after RW_REALIZED is set above, for root classes.
    // This needs to be done after class index is chosen, for root metaclasses.
    supercls = realizeClass(remapClass(cls->superclass));
    metacls = realizeClass(remapClass(cls->ISA()));

#if SUPPORT_NONPOINTER_ISA
    // Disable non-pointer isa for some classes and/or platforms.
    // Set instancesRequireRawIsa.
    bool instancesRequireRawIsa = cls->instancesRequireRawIsa();
    bool rawIsaIsInherited = false;
    static bool hackedDispatch = false;

    if (DisableNonpointerIsa) {
        // Non-pointer isa disabled by environment or app SDK version
        instancesRequireRawIsa = true;
    }
    else if (!hackedDispatch  &&  !(ro->flags & RO_META)  &&  
             0 == strcmp(ro->name, "OS_object")) 
    {
        // hack for libdispatch et al - isa also acts as vtable pointer
        hackedDispatch = true;
        instancesRequireRawIsa = true;
    }
    else if (supercls  &&  supercls->superclass  &&  
             supercls->instancesRequireRawIsa()) 
    {
        // This is also propagated by addSubclass() 
        // but nonpointer isa setup needs it earlier.
        // Special case: instancesRequireRawIsa does not propagate 
        // from root class to root metaclass
        instancesRequireRawIsa = true;
        rawIsaIsInherited = true;
    }
    
    if (instancesRequireRawIsa) {
        cls->setInstancesRequireRawIsa(rawIsaIsInherited);
    }
// SUPPORT_NONPOINTER_ISA
#endif

    // Update superclass and metaclass in case of remapping
    cls->superclass = supercls;
    cls->initClassIsa(metacls);

    // Reconcile instance variable offsets / layout.
    // This may reallocate class_ro_t, updating our ro variable.
    if (supercls  &&  !isMeta) reconcileInstanceVariables(cls, supercls, ro);

    // Set fastInstanceSize if it wasn't set already.
    cls->setInstanceSize(ro->instanceSize);

    // Copy some flags from ro to rw
    if (ro->flags & RO_HAS_CXX_STRUCTORS) {
        cls->setHasCxxDtor();
        if (! (ro->flags & RO_HAS_CXX_DTOR_ONLY)) {
            cls->setHasCxxCtor();
        }
    }

    // Connect this class to its superclass's subclass lists
    if (supercls) {
        addSubclass(supercls, cls);
    } else {
        addRootClass(cls);
    }

    // Attach categories
    methodizeClass(cls);

    return cls;
}

注意之前 RORWflags 宏標記的一個細節:

#define RO_FUTURE             (1<<30)
#define RO_REALIZED           (1<<31)

#define RW_REALIZED           (1<<31)
#define RW_FUTURE             (1<<30)

也就是說 ro = (const class_ro_t *)cls->data(); 這種強轉對于接下來的 ro->flags & RO_FUTURE 操作完全是 OK 的,兩種結構體第一個成員都是 flags,RO_FUTURERW_FUTURE 值一樣的。
經過 realizeClass 函數處理的類才是『真正的』類,調用它時不能對類做寫操作。

調用函數鏈條分析

大致 Category 加載、添加附加內容到對應的 Class 的調用鏈條:_objc_init -> map_images -> _read_images -> unattachedCategoriesForClass -> remethodizeClass -> attachCategories -> attachLists。下文對比較重要的方法進行分析。

_objc_init 分析

首先,從入口函數 _objc_init 進行分析。這個函數主要做的工作是引導程序初始化。說白了就是加載所需要的 dyld 動態庫,裝載鏡像。

/***********************************************************************
* _objc_init 引導初始化函數
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();
    // 加載并映射鏡像文件到內存中
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

_read_images 分析

由上一個 _objc_init 入口方法可知,引導程序需要去加載動態庫dyld。_read_images 方法做的事情很多,主要幾個重要的操作是裝載 Class 以及其對應的類擴展,最后才會去加載 Category
整體的方法代碼如下:

/***********************************************************************
* _read_images
* Perform initial processing of the headers in the linked 
* list beginning with headerList. 
*
* Called by: map_images_nolock
*
* Locking: runtimeLock acquired by map_images
**********************************************************************/
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
    header_info *hi;
    uint32_t hIndex;
    size_t count;
    size_t I;
    Class *resolvedFutureClasses = nil;
    size_t resolvedFutureClassCount = 0;
    static bool doneOnce;
    TimeLogger ts(PrintImageTimes);

    runtimeLock.assertWriting();

#define EACH_HEADER \
    hIndex = 0;         \
    hIndex < hCount && (hi = hList[hIndex]); \
    hIndex++

    if (!doneOnce) {
        doneOnce = YES;

#if SUPPORT_NONPOINTER_ISA
        // Disable non-pointer isa under some conditions.

# if SUPPORT_INDEXED_ISA
        // Disable nonpointer isa if any image contains old Swift code
        for (EACH_HEADER) {
            if (hi->info()->containsSwift()  &&
                hi->info()->swiftVersion() < objc_image_info::SwiftVersion3)
            {
                DisableNonpointerIsa = true;
                if (PrintRawIsa) {
                    _objc_inform("RAW ISA: disabling non-pointer isa because "
                                 "the app or a framework contains Swift code "
                                 "older than Swift 3.0");
                }
                break;
            }
        }
# endif

# if TARGET_OS_OSX
        // Disable non-pointer isa if the app is too old
        // (linked before OS X 10.11)
        if (dyld_get_program_sdk_version() < DYLD_MACOSX_VERSION_10_11) {
            DisableNonpointerIsa = true;
            if (PrintRawIsa) {
                _objc_inform("RAW ISA: disabling non-pointer isa because "
                             "the app is too old (SDK version " SDK_FORMAT ")",
                             FORMAT_SDK(dyld_get_program_sdk_version()));
            }
        }

        // Disable non-pointer isa if the app has a __DATA,__objc_rawisa section
        // New apps that load old extensions may need this.
        for (EACH_HEADER) {
            if (hi->mhdr()->filetype != MH_EXECUTE) continue;
            unsigned long size;
            if (getsectiondata(hi->mhdr(), "__DATA", "__objc_rawisa", &size)) {
                DisableNonpointerIsa = true;
                if (PrintRawIsa) {
                    _objc_inform("RAW ISA: disabling non-pointer isa because "
                                 "the app has a __DATA,__objc_rawisa section");
                }
            }
            break;  // assume only one MH_EXECUTE image
        }
# endif

#endif

        if (DisableTaggedPointers) {
            disableTaggedPointers();
        }
        
        if (PrintConnecting) {
            _objc_inform("CLASS: found %d classes during launch", totalClasses);
        }

        // namedClasses
        // Preoptimized classes don't go in this table.
        // 4/3 is NXMapTable's load factor
        int namedClassesSize = 
            (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
        gdb_objc_realized_classes =
            NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);

        ts.log("IMAGE TIMES: first time tasks");
    }


    // Discover classes. Fix up unresolved future classes. Mark bundle classes.

    for (EACH_HEADER) {
        if (! mustReadClasses(hi)) {
            // Image is sufficiently optimized that we need not call readClass()
            continue;
        }

        bool headerIsBundle = hi->isBundle();
        bool headerIsPreoptimized = hi->isPreoptimized();

        classref_t *classlist = _getObjc2ClassList(hi, &count);
        for (i = 0; i < count; i++) {
            Class cls = (Class)classlist[I];
            Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);

            if (newCls != cls  &&  newCls) {
                // Class was moved but not deleted. Currently this occurs 
                // only when the new class resolved a future class.
                // Non-lazily realize the class below.
                resolvedFutureClasses = (Class *)
                    realloc(resolvedFutureClasses, 
                            (resolvedFutureClassCount+1) * sizeof(Class));
                resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
            }
        }
    }

    ts.log("IMAGE TIMES: discover classes");

    // Fix up remapped classes
    // Class list and nonlazy class list remain unremapped.
    // Class refs and super refs are remapped for message dispatching.
    
    if (!noClassesRemapped()) {
        for (EACH_HEADER) {
            Class *classrefs = _getObjc2ClassRefs(hi, &count);
            for (i = 0; i < count; i++) {
                remapClassRef(&classrefs[I]);
            }
            // fixme why doesn't test future1 catch the absence of this?
            classrefs = _getObjc2SuperRefs(hi, &count);
            for (i = 0; i < count; i++) {
                remapClassRef(&classrefs[I]);
            }
        }
    }

    ts.log("IMAGE TIMES: remap classes");

    // Fix up @selector references
    static size_t UnfixedSelectors;
    sel_lock();
    for (EACH_HEADER) {
        if (hi->isPreoptimized()) continue;

        bool isBundle = hi->isBundle();
        SEL *sels = _getObjc2SelectorRefs(hi, &count);
        UnfixedSelectors += count;
        for (i = 0; i < count; i++) {
            const char *name = sel_cname(sels[i]);
            sels[i] = sel_registerNameNoLock(name, isBundle);
        }
    }
    sel_unlock();

    ts.log("IMAGE TIMES: fix up selector references");

#if SUPPORT_FIXUP
    // Fix up old objc_msgSend_fixup call sites
    for (EACH_HEADER) {
        message_ref_t *refs = _getObjc2MessageRefs(hi, &count);
        if (count == 0) continue;

        if (PrintVtables) {
            _objc_inform("VTABLES: repairing %zu unsupported vtable dispatch "
                         "call sites in %s", count, hi->fname());
        }
        for (i = 0; i < count; i++) {
            fixupMessageRef(refs+i);
        }
    }

    ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");
#endif

    // Discover protocols. Fix up protocol refs.
    for (EACH_HEADER) {
        extern objc_class OBJC_CLASS_$_Protocol;
        Class cls = (Class)&OBJC_CLASS_$_Protocol;
        assert(cls);
        NXMapTable *protocol_map = protocols();
        bool isPreoptimized = hi->isPreoptimized();
        bool isBundle = hi->isBundle();

        protocol_t **protolist = _getObjc2ProtocolList(hi, &count);
        for (i = 0; i < count; i++) {
            readProtocol(protolist[i], cls, protocol_map, 
                         isPreoptimized, isBundle);
        }
    }

    ts.log("IMAGE TIMES: discover protocols");

    // Fix up @protocol references
    // Preoptimized images may have the right 
    // answer already but we don't know for sure.
    for (EACH_HEADER) {
        protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count);
        for (i = 0; i < count; i++) {
            remapProtocolRef(&protolist[I]);
        }
    }

    ts.log("IMAGE TIMES: fix up @protocol references");

    // Realize non-lazy classes (for +load methods and static instances)
    for (EACH_HEADER) {
        classref_t *classlist = 
            _getObjc2NonlazyClassList(hi, &count);
        for (i = 0; i < count; i++) {
            Class cls = remapClass(classlist[i]);
            if (!cls) continue;

            // hack for class __ARCLite__, which didn't get this above
#if TARGET_OS_SIMULATOR
            if (cls->cache._buckets == (void*)&_objc_empty_cache  &&  
                (cls->cache._mask  ||  cls->cache._occupied)) 
            {
                cls->cache._mask = 0;
                cls->cache._occupied = 0;
            }
            if (cls->ISA()->cache._buckets == (void*)&_objc_empty_cache  &&  
                (cls->ISA()->cache._mask  ||  cls->ISA()->cache._occupied)) 
            {
                cls->ISA()->cache._mask = 0;
                cls->ISA()->cache._occupied = 0;
            }
#endif

            realizeClass(cls);
        }
    }

    ts.log("IMAGE TIMES: realize non-lazy classes");

    // Realize newly-resolved future classes, in case CF manipulates them
    if (resolvedFutureClasses) {
        for (i = 0; i < resolvedFutureClassCount; i++) {
            realizeClass(resolvedFutureClasses[I]);
            resolvedFutureClasses[i]->setInstancesRequireRawIsa(false/*inherited*/);
        }
        free(resolvedFutureClasses);
    }    

    ts.log("IMAGE TIMES: realize future classes");

    // Discover categories. 
    for (EACH_HEADER) {
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        for (i = 0; i < count; i++) {
            category_t *cat = catlist[I];
            Class cls = remapClass(cat->cls);

            if (!cls) {
                // Category's target class is missing (probably weak-linked).
                // Disavow any knowledge of this category.
                catlist[i] = nil;
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class", 
                                 cat->name, cat);
                }
                continue;
            }

            // Process this category. 
            // First, register the category with its target class. 
            // Then, rebuild the class's method lists (etc) if 
            // the class is realized. 
            bool classExists = NO;
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    remethodizeClass(cls);
                    classExists = YES;
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category -%s(%s) %s", 
                                 cls->nameForLogging(), cat->name, 
                                 classExists ? "on existing class" : "");
                }
            }

            if (cat->classMethods  ||  cat->protocols  
                ||  (hasClassProperties && cat->_classProperties)) 
            {
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized()) {
                    remethodizeClass(cls->ISA());
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category +%s(%s)", 
                                 cls->nameForLogging(), cat->name);
                }
            }
        }
    }

    ts.log("IMAGE TIMES: discover categories");

    // Category discovery MUST BE LAST to avoid potential races 
    // when other threads call the new category code before 
    // this thread finishes its fixups.

    // +load handled by prepare_load_methods()

    if (DebugNonFragileIvars) {
        realizeAllClasses();
    }


    // Print preoptimization statistics
    if (PrintPreopt) {
        static unsigned int PreoptTotalMethodLists;
        static unsigned int PreoptOptimizedMethodLists;
        static unsigned int PreoptTotalClasses;
        static unsigned int PreoptOptimizedClasses;

        for (EACH_HEADER) {
            if (hi->isPreoptimized()) {
                _objc_inform("PREOPTIMIZATION: honoring preoptimized selectors "
                             "in %s", hi->fname());
            }
            else if (hi->info()->optimizedByDyld()) {
                _objc_inform("PREOPTIMIZATION: IGNORING preoptimized selectors "
                             "in %s", hi->fname());
            }

            classref_t *classlist = _getObjc2ClassList(hi, &count);
            for (i = 0; i < count; i++) {
                Class cls = remapClass(classlist[i]);
                if (!cls) continue;

                PreoptTotalClasses++;
                if (hi->isPreoptimized()) {
                    PreoptOptimizedClasses++;
                }
                
                const method_list_t *mlist;
                if ((mlist = ((class_ro_t *)cls->data())->baseMethods())) {
                    PreoptTotalMethodLists++;
                    if (mlist->isFixedUp()) {
                        PreoptOptimizedMethodLists++;
                    }
                }
                if ((mlist=((class_ro_t *)cls->ISA()->data())->baseMethods())) {
                    PreoptTotalMethodLists++;
                    if (mlist->isFixedUp()) {
                        PreoptOptimizedMethodLists++;
                    }
                }
            }
        }

        _objc_inform("PREOPTIMIZATION: %zu selector references not "
                     "pre-optimized", UnfixedSelectors);
        _objc_inform("PREOPTIMIZATION: %u/%u (%.3g%%) method lists pre-sorted",
                     PreoptOptimizedMethodLists, PreoptTotalMethodLists, 
                     PreoptTotalMethodLists
                     ? 100.0*PreoptOptimizedMethodLists/PreoptTotalMethodLists 
                     : 0.0);
        _objc_inform("PREOPTIMIZATION: %u/%u (%.3g%%) classes pre-registered",
                     PreoptOptimizedClasses, PreoptTotalClasses, 
                     PreoptTotalClasses 
                     ? 100.0*PreoptOptimizedClasses/PreoptTotalClasses
                     : 0.0);
        _objc_inform("PREOPTIMIZATION: %zu protocol references not "
                     "pre-optimized", UnfixedProtocolReferences);
    }

#undef EACH_HEADER
}

以上方法可以看出,裝載 Class 以及內存處理完后才會對 Category 進行加載。在這個方法代碼太長,將挑一些有關 Category 加載的代碼進行查看,請查看一下代碼:

// Process this category. 
// First, register the category with its target class. 
// Then, rebuild the class's method lists (etc) if 
// the class is realized. 
bool classExists = NO;
if (cat->instanceMethods ||  cat->protocols  
    ||  cat->instanceProperties) 
{
    // 獲取元類中還未添加的類別列表
    addUnattachedCategoryForClass(cat, cls, hi);
    if (cls->isRealized()) {
        remethodizeClass(cls);
        classExists = YES;
    }
    if (PrintConnecting) {
        _objc_inform("CLASS: found category -%s(%s) %s", 
                     cls->nameForLogging(), cat->name, 
                     classExists ? "on existing class" : "");
    }
}

if (cat->classMethods  ||  cat->protocols  
    ||  (hasClassProperties && cat->_classProperties)) 
{
    // 獲取類中還未添加的類別列表
    addUnattachedCategoryForClass(cat, cls->ISA(), hi);
    if (cls->ISA()->isRealized()) {
        remethodizeClass(cls->ISA());
    }
    if (PrintConnecting) {
        _objc_inform("CLASS: found category +%s(%s)", 
                     cls->nameForLogging(), cat->name);
    }
}

這段代碼,主要做的操作是調用 addUnattachedCategoryForClass 方法獲取元類中還未添加的類別列表然后再調用 remethodizeClass 整理添加。

remethodizeClass 分析

remethodizeClass 方法的代碼比較簡潔,主要操作是將 Category 的內容添加到已經存在的 Class 中,最后刷新下 method caches。
具體完成代碼如下:

/***********************************************************************
* remethodizeClass
* Attach outstanding categories to an existing class.
* Fixes up cls's method list, protocol list, and property list.
* Updates method caches for cls and its subclasses.
* Locking: runtimeLock must be held by the caller
**********************************************************************/
static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;
    // 加鎖,保證線程安全
    runtimeLock.assertWriting();
    // 判斷是否為元類
    isMeta = cls->isMetaClass();

    // Re-methodizing: check for more categories
    // 獲取該類未加載的分類列表
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
        //  添加分類到指定類中
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}

attachCategories 分析

在調用 attachCategories 函數之前,會先使用 unattachedCategoriesForClass 函數獲取類中還未添加的類別列表。這個列表類型為 locstamped_category_list_t,它封裝了 category_t 以及對應的 header_info。header_info 存儲了實體在鏡像中的加載和初始化狀態,以及一些偏移量,在加載 Mach-O 文件相關函數中經常用到。
整體方法代碼如下:

// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order, 
// oldest categories first.
// Class 加載完之后會加載分類列表并添加到類結構體中去
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return; // 判斷有沒有分類列表
    // 打印需要替換的方法
    if (PrintReplacedMethods) printReplacements(cls, cats);
    // 是否為元類
    bool isMeta = cls->isMetaClass();

    // fixme rearrange to remove these intermediate allocations
    // 分配相應的實例/類方法、屬性、協議列表指針,相當于二維鏈表,一個分類對應一個一維鏈表
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));

    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;
    // 循環分類列表
    while (i--) {
        // 取出第 i 個分類
        auto& entry = cats->list[I];
        // 從分類里取出對應的實例/類方法表
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            // 將實例/類方法列表賦值到對應的二維鏈表中
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
        // 從分類里取出對應的實例/類屬性列表,并加到對應的二維鏈表中
        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }
        // 從分類里取出遵守的協議列表,并加到對應的二維鏈表中
        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }
    
    // 遍歷完分類后,取出類/元類加載到內存(堆區)的 class_rw_t 結構體
    auto rw = cls->data();
    // 準備方法列表:加鎖掃描方法列表,將新方法放在每一個分類的方法前面(對每個分類方法進行排序)
    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    // 添加方法到類/元類中
    rw->methods.attachLists(mlists, mcount);
    // 釋放二維方法列表
    free(mlists);
    // 刷新方法緩存
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
    // 添加屬性到類/元類中
    rw->properties.attachLists(proplists, propcount);
    // 釋放二維屬性列表
    free(proplists);
    // 添加遵守的協議到類/元類中
    rw->protocols.attachLists(protolists, protocount);
    // 釋放二維協議列表
    free(protolists);
}

attachLists 分析

attachLists 這個方法是把二維數組中添加多一行的操作。其實是 list_array_tt 類中的一個方法,但需要注意的是這里只是 C++ 的類而已,而不是 Objective-C 的類。
具體的添加代碼如下:

void attachLists(List* const * addedLists, uint32_t addedCount) {
    if (addedCount == 0) return;

    if (hasArray()) {
        // many lists -> many lists
        uint32_t oldCount = array()->count;
        uint32_t newCount = oldCount + addedCount;
        setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
        array()->count = newCount;
        memmove(array()->lists + addedCount, array()->lists, 
                oldCount * sizeof(array()->lists[0]));
        memcpy(array()->lists, addedLists, 
               addedCount * sizeof(array()->lists[0]));
    }
    else if (!list  &&  addedCount == 1) {
        // 0 lists -> 1 list
        list = addedLists[0];
    } 
    else {
        // 1 list -> many lists
        List* oldList = list;
        uint32_t oldCount = oldList ? 1 : 0;
        uint32_t newCount = oldCount + addedCount;
        setArray((array_t *)malloc(array_t::byteSize(newCount)));
        array()->count = newCount;
        if (oldList) array()->lists[addedCount] = oldList;
        memcpy(array()->lists, addedLists, 
               addedCount * sizeof(array()->lists[0]));
    }
}

Category 加載原理

在 App 啟動加載鏡像文件時,會在 _read_images 函數間接調用到 attachCategories 函數,完成向類中添加 Category 的工作。原理就是向 class_rw_t 中的 method_array_t, property_array_t, protocol_array_t 數組中分別添加 method_list_t, property_list_t, protocol_list_t 指針。

Category 不能添加成員變量的原因

由于一個結構體都是連續分配的內存空間,所以,這里就涉及到了一個問題為什么在運行時,Category 不能添加成員變量,因為這樣需要根據需要調整整個結構體的內存空間,影響性能。

常見的使用場景

學會 Category 對于 iOS 開發者來說十分重要,市面上常見的性能相關的第三方庫都是使用常見 AOP 面向切面編程思想進行設計。這樣的話就需要用到 Categoryhook 掉類中的方法。一般常見的用法:

  1. Class 中添加方法;
  2. Class 中添加屬性;
  3. hook Class 中的方法;

首先,這里我先定義一個 Person 類,之后就拿這個代碼舉例子:

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger sex;
@property (nonatomic, assign) NSUInteger age;

- (void)showInfo;

- (void)eat;

- (void)drink;

@end

@implementation Person

- (instancetype)init {
    self = [super init];
    if (self) {
        _name = @"無名";
        _sex = 1;
        _age = 9999;
    }
    return self;
}

- (void)showInfo {
    NSLog(@"\n個人信息:\n名字:%@\n性別:%@\n年齡:%@\n", self.name, @(self.sex), @(self.age));
}

- (void)eat {
    NSLog(@"%@吃飯", self);
}

- (void)drink {
    NSLog(@"%@喝水", self);
}

- (void)privateSex {
    NSLog(@"%@有秘密", self);
}

- (NSString *)description {
    return self.name;
}

@end

對 Class 中添加方法

Class 中添加方法是最常用的方式,主要目的就是能夠拆分代碼,開發者不用把類的所有方法都都寫在同一個地方。這樣做的好處是可以使代碼布局更加簡潔。
下面舉個例子可以這樣使用:

@interface Person (Fatter)

- (void)dance;

@end

@implementation Person (Fatter)

// 重寫的公開方法,會優先調用 Category 里的方法
// 報警告:Category is implementing a method which will also be implemented by its primary class
- (void)drink {
    NSLog(@"%@喝飲料", self);
}

// 重寫的私有方法,會優先調用 Category 里的方法
// 原理:運行時,分類的方法會添加到原有方法的 IMP 的前面
- (void)privateSex {
    NSLog(@"%@有好多好多秘密", self);
}

// 添加的方法
- (void)dance {
    NSLog(@"%@跳舞", self);
}

@end

如以上代碼所示,Apple 不建議在分類里重寫方法。有這么一種情況,重寫的方法原本在分類里,同樣是分類,只能看誰是最晚加載的,才能決定調用誰的了。

添加屬性

Category 中添加屬性,就必須自己實現對應的 gettersetter 方法。而且不能生成成員變量,所以需要關聯對象來存儲這些值。

關聯對象原理

關聯對象方法 void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) 實際上是調用 _object_set_associative_reference 方法進行關聯。先看以下方法代碼:

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            // setting the association to nil breaks the association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}

關聯對象是由 AssociationsManager 管理,接下來看其代碼:

// class AssociationsManager manages a lock / hash table singleton pair.
// Allocating an instance acquires the lock, and calling its assocations()
// method lazily allocates the hash table.

spinlock_t AssociationsManagerLock;

class AssociationsManager {
    // associative references: object pointer -> PtrPtrHashMap.
    static AssociationsHashMap *_map;
public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }
    
    AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};

AssociationsManager 是一個靜態的全局 AssociationsHashMap,用來存儲所有的關聯對象,key是對象的內存地址,value則是另一個 AssociationsHashMap,其中存儲了關聯對象的 key-value。對象銷毀的工作則交給 objc_destructInstance(id obj)

使用方式

Category 中添加屬性是可以的,但是不能添加成員變量。一般的實現代碼如下:

@interface Person (Fatter)

@property (nonatomic, copy) NSString *liking;

@end

// 關聯對象的Key
const void *kPersonLikingKey;

@implementation Person (Fatter)

# pragma mark - getter&setter
// 必須自己實現 getter 和 setter 方法
- (NSString *)liking {
    NSString *str = objc_getAssociatedObject(self, kPersonLikingKey);
    return str;
}

- (void)setLiking:(NSString *)liking {
    // 關聯對象
    objc_setAssociatedObject(self, kPersonLikingKey, liking, OBJC_ASSOCIATION_COPY);
}

@end

相關的 API 以及說明代碼如下:

/** 
 * Sets an associated value for a given object using a given key and association policy.
 * 
 * @param object The source object for the association.
 * @param key The key for the association.
 * @param value The value to associate with the key key for object. Pass nil to clear an existing association.
 * @param policy The policy for the association. For possible values, see “Associative Object Behaviors.”
 * 
 * @see objc_setAssociatedObject
 * @see objc_removeAssociatedObjects
 */
OBJC_EXPORT void
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

/** 
 * Returns the value associated with a given object for a given key.
 * 
 * @param object The source object for the association.
 * @param key The key for the association.
 * 
 * @return The value associated with the key \e key for \e object.
 * 
 * @see objc_setAssociatedObject
 */
OBJC_EXPORT id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

/** 
 * Removes all associations for a given object.
 * 
 * @param object An object that maintains associated objects.
 * 
 * @note The main purpose of this function is to make it easy to return an object 
 *  to a "pristine state”. You should not use this function for general removal of
 *  associations from objects, since it also removes associations that other clients
 *  may have added to the object. Typically you should use \c objc_setAssociatedObject 
 *  with a nil value to clear an association.
 * 
 * @see objc_setAssociatedObject
 * @see objc_getAssociatedObject
 */
OBJC_EXPORT void
objc_removeAssociatedObjects(id _Nonnull object)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

關聯政策是一組枚舉常量:

/**
 * Policies related to associative references.
 * These are options to objc_setAssociatedObject()
 */
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

替換系統類的方法

在 AOP 編程中,iOS需要實現切面式編程,就必須 hook 掉別人的方法。這樣就必須用到 Runtimemethod_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 方法實現方法交換。交換過程可以看下圖:

方法交換

由上圖可知,交換方法只是交換 IMP(方法實現地址)而已。還有一種情況就是 hook 別人代碼之后還需要執行原來的方法,那就是 Swizzle Method 了。具體交換使用還需要看一下代碼:

@interface UIViewController (swizzle)

@end

@implementation UIViewController (swizzle)
// 每個類裝載的時候都會調用這個方法
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzle_viewDidLoad];
    });
}

+ (void)swizzle_viewDidLoad {
    [self exchangeMethodWithSelecter:@selector(viewDidLoad) toSelecter:@selector(swiz_viewDidLoad)];
}

// 需要交換的方法
- (void)swiz_viewDidLoad {
    NSLog(@"先執行我的代碼");
    // 執行完代碼后,調用原來的方法。此時的 swiz_viewDidLoad(SEL)會去找到 原來的方法(IMP),因為在 Load 方法里已經替換掉實現了
    [self swiz_viewDidLoad];
}

/**
 交換方法實現 IMP

 @param origin 原始方法
 @param destination 交換的方法
 */
+ (void)exchangeMethodWithSelecter:(SEL)origin toSelecter:(SEL)destination {
    Method originMethod = class_getInstanceMethod(self, origin);
    Method destinationMethod = class_getInstanceMethod(self, destination);
    method_exchangeImplementations(originMethod, destinationMethod);
}

@end

使用注意事項

雖然說,Category + Runtime 能做很多事情。但是還是需要謹慎使用,最好是了解它原理的情況下使用。
Category 加載順序方面,表面上可以由下圖看出:

image

越往后編譯的文件,就會替換掉最原始的方法。其實本質而言就是 Category 最后面執行 + (void)load 的方法,系統自動調動 viewDidLoad,會優先走最后執行 + (void)load 的分類里的方法。

總結

Category + Runtime 的使用對于 iOS 開發者來說是非常重要。利用 AOP 編程思想無切入別人代碼,就可以做到讓方法執行自己的自定義代碼,也就是 hook 別人的方法。目前許多第三方庫都是利用這種思想:無埋點、聽云、友盟統計、性能監控等。

參考文檔

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容