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 函数,进行装载 Classdyldimage 等操作。

预备知识

在了解运行时加载 Category 之前,需要提前了解一下 Class 在加载到内存里,是怎么封装这些结构体的。主要从 class_ro_tclass_rw_t 结构体,以及 realizeClass 方法说起。

class_ro_t 结构体

objc_class 包含了 class_data_bits_tclass_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_infoheader_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 别人的方法。目前许多第三方库都是利用这种思想:无埋点、听云、友盟统计、性能监控等。

参考文档

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容