iOS底层分析(汇总)

Apple相关源码地址

https://opensource.apple.com/source/ 可以在线查看Apple相关开源代码,将source改成tarballs,可以以压缩包的形式下载。后面的数字表示版本号(比如objc4-680.tar.gz,680是版本号),版本号的数值越大,版本越新。
Objective-C源码:http://opensource.apple.com/source/objc4/
XNU内核源码:https://opensource.apple.com/source/xnu/
内存分配源码:https://opensource.apple.com/source/libmalloc/
gcd源码:https://opensource.apple.com/source/libdispatch/
pthread源码:https://opensource.apple.com/source/libpthread/
block源码:https://opensource.apple.com/source/libclosure/
平台库源码:https://opensource.apple.com/source/libplatform/
ANSI C 函数库源码:https://opensource.apple.com/source/libc/
动态链接器源码:https://opensource.apple.com/source/dyld/
CoreFoundation源码:https://opensource.apple.com/source/CF/
GNUStep源码:http://www.gnustep.org/resources/downloads.php
可编译的objc4源码:https://github.com/LGCooci/objc4_debug

GNUstepCocoa框架的互换框架,两者实现效果差不多,但内部实现细节略有差异。
因为NSObject类的Foundation框架是不开源的,不过,Foundation框架使用的Core Foundation框架的源码和通过调用NSObject类进行内存管理部分的源码是公开的。
所以,我们先来了解GNUstep的实现方式,有助于我们去理解Apple的实现方式。

逆向工具

hopper:https://www.hopperapp.com
MachOView: https://github.com/mythkiven/MachOView
Hopper是一款反汇编和反编译的交互式工具。可以对MAC程序、Windows程序和iOS程序进行调试、反编译等。Hopper最初诞生于Mac平台,故在MAC OS X上有更好的表现。

iOS 内存分区

iOS中内存一般可分为5个区域,从低地址向高地址分别是 代码区,常量区,全局区,堆区,栈区。此外,在代码区之前有部分地址处于保留状态,在栈区之后,还有内核区。
当一个app启动后,代码区,常量区,全局区地址已固定,而堆区和栈区是时时刻刻变化的(可创建和释放)。
代码区(.text):存放当前App编译后的代码,当App冷启动时,操作系统会把存储在ROM(硬盘)里面的部分代码拷贝到到App的虚拟内存空间(虚拟内存指App的寻址空间,使用地址时最终都会映射成物理内存地址)。代码区的数据在程序运行后不可修改。

  • 预留区域:用于给系统提供一些必要的空间
    常量区(.data):存储常量字符串和const常量。常量区的数据在程序运行后不可修改。
  • 全局区(.data+.bbs):存储全局变量和静态变量。全局区又可分为已初始化全局区:data段和未初始化全局区:.bss段。举例:int a = 10;已初始化;int a;未初始化。
  • 堆区(.heap): 需要由程序员主动管理内存释放,比如alloc, newcopy方法创建的对象(不过在arc下编译器在编译时会自动生成release/autorelease代码,不需要程序员主动管理)。arc不支持 Core Foundation框架,Core Foudation中使用包含createcopy等字符串的函数名创建的对象需要程序员管理内存释放;C语言malloccallocrealloc函数创建的对象需要程序员管理内存释放。
    malloc 不能初始化所分配的内存空间,而函数calloc能。如果malloc函数分配的内存空间原来没有被使用过,则其中的每一位可能都是0;反之,如果这部分内存曾经被分配过,则其中可能遗留各种各样的数据,也就是说,使用malloc函数的程序开始时(内存还没有被重新分配)能正常进行,但经过一段时间(内存已经被重新分配,可能会出现一些问题)。
    calloc 会将所分配的空间中的每一位都初始化为零,也就是说如果你是字符类型或整数类型的元素分配内存,那么这些元素将保证会被动的初始化为0,如果你为指针类型的元素分配内存,那么这些元素通常会被初始化为空指针。如果你为实型元素分配内存,则这些元素会被初始化为浮点型的0。
    realloc 可以对给定的指针所指向的空间进行扩大或缩小,无论是扩大还是缩小,原有的内存中的内容将保持不变,当然,对于缩小,则被缩小的那一部分的内容将会丢失,realloc并不保持调整后的内存空间和原来的 内存空间保持同一内存地址,realloc返回的指针很可能指向新的地址。
    堆区内存一般都是通过存储在栈区或全局区的指针间接访问。
    因为objective-c开辟内存都遵循内存对齐(后文有介绍),对象分配内存是16的倍数,因此堆区的对象地址都是0x0(或者说0b0000)结尾。
  • 栈区(.stack):一般由编译器管理内存释放,局部变量,函数参数等会存储在栈区。栈区和其它区域不同,数据占用空间是从高地址开始分配。大量递归调用容易造成栈区空间不足。由于栈区的创建和释放是依次连续的,遵循FILO(First In Last Out)原则,所以区别于堆区,不会产生内存碎片,创建与释放快速且高效。栈区的空间比较小,iOS一般是1M,但因为一般都是存储基本数据类型和指针,所以一般都足够使用。
  • 内核区(core):系统动态库代码等。


    iOS 内存分区

举例:

    NSString *str1 = @"123";
    NSString *str2 = @"123";
    NSString *str3 = [NSString stringWithFormat:@"123"];
    NSString *str4 = [NSString stringWithFormat:@"123123123123123123123123123123"];
    NSString *str5 = [NSString stringWithString:str1];
    NSString *str6 = [NSString stringWithString:str3];
    NSString *str7 = [NSString stringWithString:str4];

上面的代码:
str1,str2,str5指向同一个常量区字符串123。
str3和str6因为字符串比较短,是个伪指针,内部存放的是tagged pointer,且值相同。
str4和str7指向同一个堆区对象(后文介绍原因)。

举例:在如下代码中,constant作为常量存储在常量区;global作为全局变量存储在全局区;person指针作为局部变量,存储在栈区;而person指针指向的对象Person,存储在堆区。customImp是用户自己实现的代码,编译时在ipa(app)文件中,并在程序启动加载时载入代码区;systemImp是iOS SDK的动态库代码,编译时不会链接到ipa文件中,而是存在iOS设备中,一台设备所有程序共用一份,加载时会映射到内核区的虚拟内存空间。

@interface LJPerson : NSObject
@property (nonatomic, assign) int num;
@property (nonatomic, assign) int age;
@end

@implementation LJPerson
@end

const int constant = 5;
int global = 5;
void test() {
    LJPerson *person = [[LJPerson alloc] init];
    IMP customImp = [person methodForSelector:@selector(setNum:)];
    IMP systemImp = [person methodForSelector:@selector(class)];

    NSLog(@"text:%p constant:%p global:%p heap:%p stack:%p kernel:%p", customImp, &constant, &global, person, &person, systemImp);
}

以下输出结果可以看到代码区,常量区和全局区占用字节数比较短(前面是0),一般以0x1开头;堆区和栈区占用字节数比较长,一般分别以0x6和0x7开头;内核区位于最后。

输出结果:
text:0x10d777dc0
constant:0x10d779e38
global:0x10d77f8d8
heap:0x600001580150
stack:0x7ffee2487c68
kernel:0x7fff2018ec9b

Tagged Pointer

从64位开始,iOS引入Tagged Pointer技术,用于优化NSNumber,NSDate,NSString等小对象的存储。Tagged Pointer优化了内存空间,降低了内存分配开销,并在使用过程中,直接通过按位与&操作取值,而不需要通过消息查找,整体提升了性能。
使用Tagged Pointer之后,这些小对象指针存储的不再是堆区相应对象的地址,而是成为一个伪指针,内部存储的是Tag + Data形式的数据。其中Tag用于标记这是一个Tagged Pointer和对应的数据类型(NSNumber,NSDate或NSString),Data存储小对象的值,比如字符串abc,数字100 。因为Tagged Pointer并没有开辟新的对象空间,因此也没有内存引用计数的概念。
通过反汇编分析,编译器并没有对Tagged Pointer特殊处理,调用intValue仍然会进入objc_msgSend函数。

NSNumber *number = @100;
int a = [number intValue];
// 等价于
objc_msgSend(number, @selector(intValue));

反汇编分析

Objective-C源码可以看出,在方法调用(发送消息objc_msgSend)时都会先判断这个指针是不是Tagged Pointer。

/********************************************************************
 *
 * id objc_msgSend(id self, SEL _cmd, ...);
 * IMP objc_msgLookup(id self, SEL _cmd, ...);
 * 
 * objc_msgLookup ABI:
 * IMP returned in x17
 * x16 reserved for our use but not used
 *
 ********************************************************************/

#if SUPPORT_TAGGED_POINTERS
    .data
    .align 3
    .globl _objc_debug_taggedpointer_ext_classes
_objc_debug_taggedpointer_ext_classes:
    .fill 256, 8, 0

// Dispatch for split tagged pointers take advantage of the fact that
// the extended tag classes array immediately precedes the standard
// tag array. The .alt_entry directive ensures that the two stay
// together. This is harmless when using non-split tagged pointers.
    .globl _objc_debug_taggedpointer_classes
    .alt_entry _objc_debug_taggedpointer_classes
_objc_debug_taggedpointer_classes:
    .fill 16, 8, 0

// Look up the class for a tagged pointer in x0, placing it in x16.
.macro GetTaggedClass

最终通过_objc_isTaggedPointer函数判断是否为taggedPointer。在arm64下,_objc_isTaggedPointer内部会通过判断最高位是否为1来确定是否为tagged pointer

#if __arm64__
// ARM64 uses a new tagged pointer scheme where normal tags are in
// the low bits, extended tags are in the high bits, and half of the
// extended tag space is reserved for unobfuscated payloads.
#   define OBJC_SPLIT_TAGGED_POINTERS 1
#else
#   define OBJC_SPLIT_TAGGED_POINTERS 0
#endif

#if OBJC_SPLIT_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)
#elif OBJC_MSB_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)
#else
#   define _OBJC_TAG_MASK 1UL

static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr)
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

关于Tagged Pointer更深入分析《聊聊伪指针 Tagged Pointer》(https://www.jianshu.com/p/3176e30c040b?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

Objective-C 本质

从XCode5开始,内置的GCC编译器被完全废弃,采用Clang+LLVM的编译器组合(LLVM架构),如下图。可以简单理解Clang对应编译器的前端,LLVM对应编译器的后端。这里的LLVM指狭义的LLVM,广义的LLVM包括整个LLVM架构。
Clang是Apple主导编写的基于LLVM的C/C++/Objective-C编译器,性能数倍于 GCC 编译器。

Clang+LLVM

最新的LLVM会对Objective-C会做如下代码转换:
Objective-C -> LLVM中间代码 -> 汇编语言 -> 机器语言
在早期版本,代码转换过程如下:
Objective-C -> C/C++ -> 汇编语言 -> 机器语言

LLVM中间代码是一种跨平台的代码,会根据不同cpu架构转换成对应的汇编代码。

为了更好理解Objective-C内部实现,可以通过指令对代码进行部分转换:

#pragma mark - 将Objective-C代码转换为c/c++
// xrcun代表 xcode run; clang是自带的c语言编译器;-arch arm64指定以 arm64指令集;-rewrite-objc 重写 objc文件;-o 代表output输出
clang -rewrite-objc main.m -o main.cpp
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main_arm64.cpp // 指定arm64架构,iphone

#pragma mark - 将oc代码转换为llvm中间代码
clang -emit-llvm -S main.m
#pragma mark - 将oc代码转换为汇编代码
xcrun -sdk iphoneos clang -S -arch arm64 -fobjc-arc main.m -o main.s

因为LLVM中间代码的可读性较差,一般使用C/C++来分析Objective-C的底层。虽然LLVM保留了C/C++重写代码功能,但该功能只用于给开发者提供参考,并不是最终执行的程序。所以转换后的C/C++代码和LLVM中间代码可能会略有差异。如果想要查看真实的代码逻辑,可通过阅读中间代码,反汇编调试或阅读苹果开源代码。
对如下Objective-C代码进行转换:

@interface Vehicle : NSObject
{
    long _price;
    int _speed;
}
@end
@implementation Vehicle
@end

@interface Car: Vehicle
{
    int _no;
}
@end
@implementation Car
@end

int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [[NSObject alloc] init];
    }
    return 0;
}

得到C/C++代码:

struct NSObject_IMPL {
    Class isa;
};
struct Vehicle_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    long _price;
    int _speed;
};
struct Car_IMPL {
    struct Vehicle_IMPL Vehicle_IVARS;
    int _no;
};

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        NSObject *obj = objc_msgSend(objc_msgSend(objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
    }
    return 0;
}

可以看到类将会转换成结构体的形式,并且父类以结构体的形式放在成员变量最前面。NSObject只含有一个Class类型成员变量isa
Objective-C里面的方法调用,最终也是调用runtimeobjc_msgSend发送消息。

内存分配

objective-c是在alloc方法内部进行内存空间的分配,可以通过objective-c源码 (地址: https://opensource.apple.com/tarballs/objc4/) 查看到分配的过程。最终是调用c语言的malloc_zone_calloccalloc函数进行内存空间的分配。

+ (id)alloc {
    return _objc_rootAlloc(self);
}
+ (id)allocWithZone:(struct _NSZone *)zone {
    return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}
id objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
id objc_allocWithZone(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, true/*allocWithZone*/);
}
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
}
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    ASSERT(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;

    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (fastpath(!hasCxxCtor)) {
        return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

导致实际分配大于所需内存的原因如下:

  1. objective-c源码得知,Core Foundation框架下对象会至少分配16个字节内存空间。alloc方法最终会调用Class对象的instanceSize方法来获取所需空间大小if (size < 16) size = 16;
    inline size_t instanceSize(size_t extraBytes) const {
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
            return cache.fastInstanceSize(extraBytes);
        }

        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;
        return size;
    }
  1. 结构体内存对齐。
    内存对齐可以大大提升内存访问速度,是一种用空间换时间的方法。
    对齐原则:
    2.1 数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员的大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储)。min(当前开始的位置m n) m = 9 n = 4 9 10 11 12
    2.2 结构体作为成员:如果一个结构体有其他结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。(struct a里存有struct b,b里有char,int,double等元素,那么b应该从8的整数倍开始存储)
    2.3 收尾工作:结构体的总大小,也就是sizeof的结果,必须是内部成员最大成员的整数倍,不足的要补齐。
  2. objective-c内存字节对齐。字节对齐本质就是将字节数调整为对齐系数的整数倍,iOS默认对齐系数为8。
    objective-c内存字节对齐源码:
#ifdef __LP64__
#   define WORD_SHIFT 3UL
#   define WORD_MASK 7UL
#   define WORD_BITS 64
#else
#   define WORD_SHIFT 2UL
#   define WORD_MASK 3UL
#   define WORD_BITS 32
#endif

uint32_t alignedInstanceSize() const {
   return word_align(unalignedInstanceSize());
}
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}
  1. Apple系统内存对齐。
    Apple内存分配源码地址:https://opensource.apple.com/tarballs/libmalloc/
#define NANO_MAX_SIZE           256 /* Buckets sized {16, 32, 48, ..., 256} */

Apple系统内存分配相关文件

Apple系统针对不同大小的内存空间申请,拥有不同的方案,其中nano_malloc.c定义了对于极小的内存申请相关函数。
NANO_MAX_SIZE 定义了极小内存最大值 256字节,Buckets sized {16, 32, 48, ..., 256} 说明Apple的NANO内存对齐是16字节倍数。

class_getInstanceSize函数进行上面第3点:objective-c内存字节对齐。从源码得知,class_getInstanceSize调用了objective-c内存字节对齐函数,并传入未对齐的大小,返回大小是8的整数倍。
malloc_size返回的是实际分配的大小。因为内存对齐等原因,实际分配的大小大于等于实例对象所需占用的大小。

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

大小端模式(Endian)

在计算机业界,Endian表示数据在存储器中的存放顺序。大端模式,是指数据的高字节保存在内存的低地址中,小端模式,是指数据的高字节保存在内存的高地址中。iOS普遍使用的是小端模式。

前面的代码例子,对几个属性依次赋值。然后使用View Memory查看内存数据,可以看到iOS采用的是小端模式,前8字节是isa,后8字节的_price低字节0x01保存在内存低地址。

Car * v = [[Car alloc] init];
v->_price = 1;
v->_speed = 2;
v->_no = 3;
内存数据

实例对象(Instance),类对象(Class),元类对象(Meta Class)

iOS中按对象在内存中的存储分为三种类型,分别是实例对象,类对象,元类(Meta Class)对象。
实例对象存储的是成员变量的值和isa指针。
类对象存储的主要是某个类的实例方法,协议,成员变量信息,属性信息,isasuperclass,一般在程序初始化时分配,是Class类型,也可在运行时动态创建。
元类对象存储的主要是某个类的类方法,isasuper_class,和类对象成对创建。元类对象可以理解成一种特殊的类对象。

Class类型(类对象)内部结构

类对象结构
objc_class/Class

从objc的源码可以看出Class 是类对象,objc_class是类对象指针,类对象是结构体格式。
(对象是类的实例化,但一般将结构体实例称为对象以方便说明,在C++中类与结构体区别不大)

typedef struct objc_class *Class;

在ObjC 2.0以前 objc_class结构体结构如下:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    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;
    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;
/* Use `Class` instead of `struct objc_class *` */

在ObjC 2,结构如下:

struct objc_object {
    Class _Nonnull isa;
};
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // & FAST_DATA_MASK 得到 class_rw_t
    ...
}

#define FAST_DATA_MASK          0x00007ffffffffff8UL

struct class_data_bits_t {
    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    void setData(class_rw_t *newData)
    {
        ASSERT(!data()  ||  (newData->flags & (RW_REALIZING | RW_FUTURE)));
        // Set during realization or construction only. No locking needed.
        // Use a store-release fence because there may be concurrent
        // readers of data and data's contents.
        uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
        atomic_thread_fence(memory_order_release);
        bits = newBits;
    }
    ...
}

struct class_rw_t {
    // 初始信息,只读数据
    const class_ro_t *ro() const {
        auto v = get_ro_or_rwe();
        if (slowpath(v.is<class_rw_ext_t *>())) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;
        }
        return v.get<const class_ro_t *>(&ro_or_rw_ext);
    }
    //方法列表
    const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
        }
    }
    // 属性列表
    const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
        }
    }
    // 协议列表
    const protocol_array_t protocols() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
        } else {
            return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
        }
    }
    ...
}

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

    union {
        const uint8_t * ivarLayout;
        Class nonMetaclass;
    };

    explicit_atomic<const char *> name;
    // With ptrauth, this is signed if it points to a small list, but
    // may be unsigned if it points to a big list.
    void *baseMethodList;
    protocol_list_t * baseProtocols;
    // 成员变量信息
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
    ...
 }

从源码可以知道,类对象objc_class继承自objc_object,包含superclass,cache和bits三个成员变量。

objc_object

objc_object是ObjC底层所有对象的基础类,只包含isa指针。id类型即objc_object指针。关于objc_object就是NSObject的说法是不准确的,因为NSProxy也可以使用id指向,objc_object与NSObject,NSProxy拥有相同的内存结构。

typedef struct objc_object *id;

When you create an instance of a particular class, the allocated memory contains an objc_object data structure, which is directly followed by the data for the instance variables of the class.
https://developer.apple.com/documentation/objectivec/objc_object

在objc-private.h文件内部,有objc_object的另一种定义:

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    uintptr_t bits;

private:
    // Accessing the class requires custom ptrauth operations, so
    // force clients to go through setClass/getClass by making this
    // private.
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        uintptr_t nonpointer        : 1;                                       \
        uintptr_t has_assoc         : 1;                                       \
        uintptr_t has_cxx_dtor      : 1;                                       \
        uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
        uintptr_t magic             : 6;                                       \
        uintptr_t weakly_referenced : 1;                                       \
        uintptr_t unused            : 1;                                       \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 19
    };

    bool isDeallocating() {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif

    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated);
    Class getDecodedClass(bool authenticated);
};


isa_t::getClass(MAYBE_UNUSED_AUTHENTICATED_PARAM bool authenticated) {
    ···
    uintptr_t clsbits = bits;
    clsbits &= ISA_MASK;
    return (Class)clsbits;
    ···
}

union isa_t {
    uintptr_t bits;
    Class cls;
    Class getClass(bool authenticated);
    ···
};

struct objc_object {
private:
    isa_t isa;
...
}

事实上,这两种定义是共存的,两者拥有相同的内存结构,objc-private.h文件中的定义类似成私有变量和方法,只对内部暴露。

cache_t

cache_t是方法缓存,其内部结构如下:

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;
#if __LP64__
            uint16_t                   _flags;
#endif
            uint16_t                   _occupied;
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    };
public:
    unsigned capacity() const;
    struct bucket_t *buckets() const;
    void insert(SEL sel, IMP imp, id receiver);
...
}

/// bucket_t中存储方法名和方法指针。
struct bucket_t {
private:
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
public:
    void set(bucket_t *base, SEL newSel, IMP newImp, Class cls);
}

explicit_atomic<T>是一个泛型的结构,保证对模版传入的类型T的操作是线程安全的。uintptr_t是8字节的指针。
_bucketsAndMaybeMask内存储的主要是缓存内所有方法的selimp,可以通过bucket()取出,每个方法缓存对应一个bucket_t。(sel指selector方法名,imp指implementation pointer方法实现指针)此外,可能还有mask(掩码)。为了节省空间,mask和首个bucket_t指针共用8字节,mask占高16位,首个bucket_t占低48位。
_maybeMask是哈希运算时的mask掩码(效率比%取余高),值为2的N次方-1,也是总容量-1。
_occupied代表已经被占用的桶数量。

cache_t内部结构

当方法缓存中新增方法时会触发cache_t::insert

INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2),

void cache_t::insert(SEL sel, IMP imp, id receiver)
{
    runtimeLock.assertLocked();

    // Never cache before +initialize is done
    if (slowpath(!cls()->isInitialized())) {
        return;
    }

    if (isConstantOptimizedCache()) {
        _objc_fatal("cache_t::insert() called with a preoptimized cache for %s",
                    cls()->nameForLogging());
    }

#if DEBUG_TASK_THREADS
    return _collecting_in_critical();
#else
#if CONFIG_USE_CACHE_LOCK
    mutex_locker_t lock(cacheUpdateLock);
#endif

    ASSERT(sel != 0 && cls()->isInitialized());

    // Use the cache as-is if until we exceed our expected fill ratio.
    mask_t newOccupied = occupied() + 1;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE;
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
    else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
        // Cache is less than 3/4 or 7/8 full. Use it as-is.
    }
#if CACHE_ALLOW_FULL_UTILIZATION
    else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
        // Allow 100% cache utilization for small buckets. Use it as-is.
    }
#endif
    else {
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);
    }

    bucket_t *b = buckets();
    mask_t m = capacity - 1;
    mask_t begin = cache_hash(sel, m);
    mask_t i = begin;

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot.
    do {
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(b, sel, imp, cls());
            return;
        }
        if (b[i].sel() == sel) {
            // The entry was added to the cache by some other thread
            // before we grabbed the cacheUpdateLock.
            return;
        }
    } while (fastpath((i = cache_next(i, m)) != begin));

    bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}

可以看到首次插入方法时,会初始化4个桶的容量空间。
非首次插入方法,会判断容量占用是否超过75%,超过进行扩容,扩容后会清除所有旧的方法缓存。
begin = cache_hash(sel, m);是通过哈希函数获取方法对应的索引。

#if defined(__arm64__) && TARGET_OS_IOS && !TARGET_OS_SIMULATOR && !TARGET_OS_MACCATALYST
#define CONFIG_USE_PREOPT_CACHES 1
#else
#define CONFIG_USE_PREOPT_CACHES 0
#endif
// Class points to cache. SEL is key. Cache buckets store SEL+IMP.
// Caches are never built in the dyld shared cache.
static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
    value ^= value >> 7;
#endif
    return (mask_t)(value & mask);
}

可以看到哈希运算只用到了sel,这很好理解,在一个类对象中不可能出现两个相同的方法名。& mask相对%取余效率更高。

发生哈希碰撞时,调用cache_next:

static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}

可以看到cache_t中哈希碰撞的处理方式是开发寻址法的线性探测,不过是倒序寻址。
cache_t在哈希运算获得索引或倒序寻址过程中,会先判断桶是否为空,若为空则代表之前没有插入sel,插入空空桶中。然后判断是否存在相同sel,若存在则return不需处理。

查找sel是否存在相同和插入sel是同时进行的,这样效率更高。但如果哈希表中存在删除功能,则不能同时进行,因为中间可能因为删除出现空桶。如果希望支持删除功能,参考:https://www.jianshu.com/p/306a68df4f7f

也可以用以下两种方式来实时分析cache_t:

  1. 使用ObjC源码编译配合指针内存平移:
指针内存平移
int a[6] = {1,2,3,4,5,6};
int *b = a;
long *c = a;
NSLog(@"b:%d --- b+1:%d --- b+2:%d",*b,*(b+1),*(b+2));
NSLog(@"c:%d --- c+1:%d --- c+2:%d",*c,*(c+1),*(c+2));

以上代码输出结果如下:

b:1 --- b+1:2 --- b+2:3
c:1 --- c+1:3 --- c+2:5

可以看到指针平移时,会根据当前指针类型来计算偏移量,c是long类型,因此c+1的指针为+8字节(long长度)。

实现以下代码,在第二行打上断点:

NSObject *obj = [[NSObject alloc] init];
Class objClass = [obj class];

然后通过p/x objClass获取类对象地址addr,p (cache_t *)addr+地址偏移16字节(isa和superclass),p *$编号输出cache_t信息。

  1. 不依赖objc4源码编译,伪造objc_class内部结构并通过__block,以伪造结构的方式访问类对象内部数据。
    以下为伪造objc_class内部的结构:
struct lj_bucket_t {
    SEL _sel;
    IMP _imp;
};
struct lj_cache_t {
    struct lj_bucket_t * _buckets;
    uint32_t _mask;
    uint16_t _flags;
    uint16_t _occupied;
};
struct lj_class_data_bits_t {
    uintptr_t bits;
};
struct lj_objc_class {
    Class ISA;
    Class superclass;
    struct lj_cache_t cache;
    struct lj_class_data_bits_t bits;
};

输出cache_t内部缓存方法。

struct lj_objc_class *lj_pClass = (__bridge struct lg_objc_class *)([pClass class]);
  for (int i = 0; i<lj_pClass->cache._mask; i++) {
      struct lj_bucket_t bucket = lj_pClass->cache._buckets[i];
      NSLog(@"%@ - %p",NSStringFromSelector(bucket._sel),bucket._imp);
  }
superclass

superclass是父类对象的指针。

class_data_bits_t

类对象的信息主要都存储在class_data_bits_t里,通过对其进行 & FAST_DATA_MASK 计算可以得到最终的数据指针,指向class_rw_t结构体(rw指readwrite),class_rw_t内的class_ro_t *ro(ro指readonly)代表类对象的初始数据,创建类对象的时会赋值,不可修改,其中baseMethodListbasePropertiesbaseProtocolsivars分别代表原始的方法列表,属性列表,协议列表和成员变量信息列表。
class_rw_t内的method_array_t methods()property_array_t properties()protocol_array_t protocols() 分别代表可修改的(最终的)方法列表(类对象指对象方法,元类对象指类方法),属性列表和协议列表。

问:为什么不能在类创建后动态添加成员变量(ivars),而可以动态添加方法(methods)?
答:因为实例对象中包含成员变量的值,实例对象占用空间是每个变量占用空间之和再进行内存对齐后的大小,而方法并不存储于实例变量中。methods()存储着方法的二维数组指针,可进行扩容。但对于Ivar,相应的实例对象已经被分配,无法进行扩大空间。所以Objective-C在设计时将类对象中的成员变量描述信息ivars放在class_ro_t只读初始数据中。这也是为什么分类category可添加方法,但不能添加实例变量。

objc_class/Class对象内部结构

isa

isa是类对象指针,在实例对象,类对象(元类对象)中都包含isa
isaClass类型,即objc_class指针类型。存储着类对象的内存地址(需要通过&= ISA_MASK运算)。实例对象的isa指向类对象的内存地址,类对象的isa指向元类对象的内存地址,元类对象的isa指向元类根类的内存地址。
从源码的objc-private.h私有文件可以看到isa的另一种定义,isa_t类型,前文已经说过,这两种定义是共存的。
isa_t是个共用体,提供Class getClass(bool authenticated)方法返回isa指向的最终类对象(或元类对象)地址,isa_t::getClass方法可以看到,其中对bits进行位运算 &= ISA_MASK得到最终指向的类对象地址。在arm64下,ISA_MASK的值为define ISA_MASK 0x007ffffffffffff8ULL

isa &= ISA_MASK

问:为什么Objective-C在64位下,要将多做一步&= ISA_MASK来获取地址的值,而不直接在isa中存储地址。
答:节约空间存储其它数据。Objective-C在64位中使用部分位来存储真实的地址,其它位存储类对象的一些状态等信息,这样可以尽最大能力利用空间,&= ISA_MASK操作代表将状态等信息的值都置为0,得到的值就是最终的类对象地址值。由于iOS类对象分配的地址二进制表示都是000结尾(内存对齐8字节的整数倍),isa_t将最后三位用于存储其它信息。

问:为什么源码的objc-private.h私有文件中isaisa_t类型,而对外的文件是Class类型?
答:避免对外暴露isa_t结构。这么做并不影响,因为两者都是占用相同的内存空间,且取值都会通过&= ISA_MASK运算过滤非地址信息。

superclass

superclassClass类型,即objc_class指针,存储父类对象(父元类对象)的指针。需要特别注意的是,元类对象的根类(NSObject)的superclass指向NSObject类对象。

isa和superclass
method_array_t

可以看到class_rw_t内方法列表methods()返回的是method_array_t类型。method_array_t是个通用的二维数组模版类,方法列表method_array_t,属性列表property_array_t,协议列表protocol_array_t都继承自它。

class method_array_t : 
    public list_array_tt<method_t, method_list_t> 
{
    typedef list_array_tt<method_t, method_list_t> Super;

 public:
    method_list_t **beginCategoryMethodLists() {
        return beginLists();
    }
    
    method_list_t **endCategoryMethodLists(Class cls);

    method_array_t duplicate() {
        return Super::duplicate<method_array_t>();
    }
};

template <typename Element, typename List, template<typename> class Ptr>
class list_array_tt {
    struct array_t {
        uint32_t count;
        Ptr<List> lists[0];
// ...
}

可以看到list_array_tt模版第一个参数是二维数组的基本元素,第二个参数是二维数组的一维元素。
class method_array_t : public list_array_tt<method_t, method_list_t>分别初始化method_t为基本元素,method_list_t为一维列表。

method_array_t内部结构

method_list_t继承自entsize_list_tt,后者是一维数组模版结构体。一维的方法列表method_list_t,属性列表property_list_t,成员变量信息列表ivar_list_t都继承自它。
在类初始化时,类中的所有方法都会放在一个method_list_t里面。分类的方法会在所有类初始化后进行加载,一个分类的所有方法会放到一个method_list_t中,并动态添加到类对象的list_array_tt中。

method_t对应一个单一的方法,结构如下:

struct method_t {
    SEL name;  // 方法名
    const char *types;  // 返回值类型,参数类型编码后字符串
    IMP imp; // 指向方法代码的指针(方法地址)
};
SEL

从源码可以看出,SEL指 objc_selector结构体指针。

typedef struct objc_selector *SEL;

创建或获取SEL的方式有sel_registerName , @selector() , NSSelectorFromString()method_getName(),后面三种方式内部都是调用sel_registerName函数。

从sel_registerName源码可知,该函数主要做了以下事情:

  1. 当传入方法名(字符串)为空时,直接返回0。
  2. 在内建函数列表中进行方法名查找,如果找到则返回。
  3. 在全局的namedSelectors方法名集合中查找方法,找到则直接返回,若没找到则创建并返回。
static objc::ExplicitInitDenseSet<const char *> namedSelectors;

SEL sel_registerName(const char *name) {
    return __sel_registerName(name, 1, 1);     // YES lock, YES copy
}
static SEL __sel_registerName(const char *name, bool shouldLock, bool copy) 
{
    SEL result = 0;

    if (shouldLock) selLock.assertUnlocked();
    else selLock.assertLocked();

    if (!name) return (SEL)0;

    result = search_builtins(name);
    if (result) return result;
    
    conditional_mutex_locker_t lock(selLock, shouldLock);
    auto it = namedSelectors.get().insert(name);
    if (it.second) {
        // No match. Insert.
        *it.first = (const char *)sel_alloc(name, copy);
    }
    return (SEL)*it.first;
}

从源码可得知,相同的方法名字符串会用同一块内存地址,或者说只有一个对应的SEL。

types

types是方法返回值和参数编码后的字符串,也叫做方法签名,Foundation中使用NSMethodSignature类对其进行了面向对象封装。有两个方法隐含参数self和_cmd(方法名)也会带上。

返回值和参数编码对照表

IMP

IMP代表方法的具体实现(实现代码),存储的内容是方法地址,指向的内存区域是代码区。也就是说当找到imp的时候就可以找到方法实现。

objc_msgSend

ObjC中对对象或者self调用方法,最终都会转换为id objc_msgSend(id theReceiver,SEL theSelector, ...)发送消息。
objc_msgSend底层是汇编实现,这里简单介绍下流程:

  1. 拿到theReceiver 的isa指向对象,若theReceiver为实例对象,则指向对象为类对象;若theReceiver为类对象,则指向对象为元类对象。
  2. 从指向对象的方法缓存cache_t中开始找,若找到方法名与theSelector相同的bucket_t,则直接调用bucket_t的IMP,结束。
  3. 方法缓存未找到,则去method_array_t中找,若找到方法名与theSelector相同的method_t,则直接调用method_t的IMP,并将方法加入缓存cache_t,结束。
  4. 若method_array_t中未找到,则从superclass指向对象的方法缓存cache_t中找,回到第2步。若最终superclass指向nil,则进入消息转发阶段,前面的步骤是消息发送(方法查找)阶段。
  5. 消息转发最多出现三个步骤:1. 动态方法解析 2. 备援接收者 3.方法签名。
  6. 如果消息转发三步都失败,则调用doesNotRecognizeSelector:方法,内部默认实现是抛出异常unrecognized selector sent to XX并crash。
objc_msgSendSuper

ObjC中对super调用方法,最终都会转换为id objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)发送消息。

// 原代码:
[super test];

// llvm转换后:
objc_msgSendSuper({self, class_getSuperclass(objc_getClass("LJStudent"))}
, sel_registerName("test"));

使用llvm代码转换,可以看到转换为上面的代码,{self, class_getSuperclass(objc_getClass("LJStudent"))}即为objc_super结构体。

objc_super结构定义如下,其中receiver为调用者self;superClass是调用者isa指向类对象的superclass(class_getSuperclass(objc_getClass("LJStudent"))})。当调用者为实例对象时,superClass是它类对象的superclass;当调用者为类对象时,superClass是它元类对象的superclass。
使用super调用方法,会先根据调用者信息生成objc_super结构,然后使用objc_msgSendSuper函数发送消息。
objc_msgSendSuper进行方法查找时,并不像objc_msgSend直接从isa指向类对象的方法缓存开始查找。而是会先通过isa找到与objc_super. superClass相同的类对象,再从该类对象的方法缓存开始查找。

struct objc_super {
    id receiver;
    Class superClass;
};
self和super

Objective-C中,self 是方法的隐藏的参数,指向当前方法的调用者。在实例方法中的self是实例对象,在类方法中的self是类对象。另一个隐藏参数是_cmd,代表当前方法的SEL。比如下面test方法,可以理解为隐藏添加了self_cmd两个参数。

- (void)test {
}
- (void)test:(id)self cmd:(SEL)_cmd {
}

通过如下Objective-C源码可知,self类方法self实例方法都返回self(注:self方法self是不同的,self方法内部返回的值是self,即调用者自身):

+ (id)self {
    return (id)self;
}
- (id)self {
    return self;
}

添加以下测试代码:

Car * carInstance = [[Car alloc] init];
id classSelf = [Car self];
id instanceSelf = [carInstance self];

self地址

lldb输入p &(instanceSelf->isa),则输出: (Class *) $1 = 0x000060000181e200(isa地址,即实例对象地址)。输入p/x instanceSelf->isa,则输出(Class) $3 = 0x000000010f4c5860 Car(isa指向类对象地址)。

即类对象地址为 0x000000010f4c5860,实例对象地址为0x60000181e200
总结:类方法中的self指向类对象,实例方法中的self指向实例对象。

ObjC调用方法,本质是发送消息。super和self的区别只在于,super最终会转换为objc_msgSendSuper函数发送消息。

class方法

通过NSObjectclass方法源码可知:
调用实例方法class,返回object_getClass结果,即self->getIsa(),此时self是实例对象,getIsa()返回类对象地址。
调用类方法class,直接返回self,即返回类对象地址(而不是元类对象地址)。

+ (Class)class {
    return self;
}
- (Class)class {
    return object_getClass(self);
}
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
KVO

KVO 指 Key-Value Observing 键值监听。iOS 实现KVO主要依次做了如下几件事,举例:某对象监听Person对象的属性age:

  1. 通过runtime动态创建继承自B的子类NSKVONotifying_Person,并在其中添加setAge:classdealloc,和_isKVOA方法。

  2. 添加的setAge:方法,IMP实现指向Foundation中的私有函数_NSSetIntValueAndNotify。_NSSetIntValueAndNotify会依次调用willChangeValueForKey:方法,[super setAge:XX]方法和DidChangeValueForKey:方法。其中willChangeValueForKey:方法内会记录旧值,DidChangeValueForKey:会触发observer的observeValueForKeyPath方法,通知观察者数据变化。(willChangeValueForKey:DidChangeValueForKey:成对调用才会触发observeValueForKeyPath

  3. 重写的NSObjectclass实例方法,内部调用class_getSuperclass函数并返回Person类对象指针,避免NSKVONotifying_Person对外暴露。

  4. 修改Person实例对象的isa指针,指向NSKVONotifying_Person类对象。

    KVO本质

注:因为KVO的原理是重写了set方法,所以只有通过set方法修改成员变量才会触发KVO,而直接修改成员变量不会触发KVO。

下面使用代码调试来了解KVO本质:

@interface LJPerson : NSObject
@property (nonatomic, assign) int num;
@end

@implementation LJPerson
@end

int main(int argc, const char * argv[]) {
    LJPerson *person = [[LJPerson alloc] init];
    // 断点1
    [person addObserver:person forKeyPath:@"num" options:NSKeyValueObservingOptionNew context:nil];
    // 断点2
    return 0;
}

在进入断点1和断点2时,都在lldb输入p person->isa

(lldb) p person->isa
(Class) $1 = LJPerson
(lldb) p person->isa
(Class) $2 = NSKVONotifying_LJPerson

可以看到当person添加完Observer后,isa变为指向NSKVONotifying_LJPerson类对象。这印证了上面第4个步骤。

实现以下代码,查看NSKVONotifying_LJPerson中动态添加了哪些方法:

// 打印类对象所有方法
void printMethodNamesOfClass(Class cls) {
    unsigned int count;
    Method *methodList = class_copyMethodList(cls, &count);
    NSMutableString *methodNames = [NSMutableString string];
    
    for (int i = 0; i < count; i++) {
        Method method = methodList[i];
        NSString *methodName = NSStringFromSelector(method_getName(method));
        [methodNames appendString:methodName];
        [methodNames appendString:@", "];
    }
    free(methodList);
    NSLog(@"%@ %@", cls, methodNames);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LJPerson *person = [[LJPerson alloc] init];
        [person addObserver:person forKeyPath:@"num" options:0 context:nil];
        printMethodNamesOfClass([person class]);
        printMethodNamesOfClass(object_getClass(person));
    }
    return 0;
}

输出:
LJPerson num, setNum:,
NSKVONotifying_LJPerson setNum:, class, dealloc, _isKVOA,

可以看到class被重写后,改为调用class_getSuperclass返回父类Person类对象。同时在NSKVONotifying_LJPerson中添加了四个方法。这印证了第1和第3步骤。

实现以下代码,看下是否有替换setNum方法:

LJPerson *person = [[LJPerson alloc] init];
[person addObserver:person forKeyPath:@"num" options:0 context:nil];

NSLog(@"%p", [person methodForSelector:@selector(setNum:)]);

输出:
0x7fff207bc2b7

lldb查看IMP指向的方法:

(lldb) p (IMP)0x7fff207bc2b7
(IMP) $0 = 0x00007fff207bc2b7 (Foundation`_NSSetIntValueAndNotify)

这一段测试代码显示setNum:的IMP指向了Foundation中的_NSSetIntValueAndNotify函数,那么可以理解为iOS内部的实现代码如下。这印证了第2步骤。

Class kvoCls = object_getClass(self);
Class superCls = class_getSuperclass(kvoCls);
const char *encoding = method_getTypeEncoding(class_getInstanceMethod(superCls, @selector(setNum:)));
class_addMethod(kvoCls, @selector(setNum:), (IMP) _NSSetIntValueAndNotify, encoding);
KVO检查属性值正确性(避免KVC错误导致的crash)
if ([person validateValue:&value forKey:@"age" error:&error]) {
}
KVO实现数组变化监听
[[self mutableArrayValueForKey:@"list"] addObject:@"test"];

mutableArrayValueForKey的实现原理与KVO类似,ObjC会动态生成Array的子类,对子类的addObject:等方法都会触发observeValueForKeyPath

KVO实现AOP无痕监控页面加载时间

一种方案是使用Method Swizzling+AssociatedObject,在UIViewController中交换viewWillAppear:和viewDidLoad,在viewDidLoad中记录开始时间,在viewWillAppear:计算耗时。

// UIViewController Category
- (void)setDate:(NSDate *)date {
    objc_setAssociatedObject(self, @selector(date), date, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSDate *)date {
    return objc_getAssociatedObject(self, _cmd);
}

- (void)swizzled_viewDidLoad
{
    self.date = [NSDate date];
    [self swizzled_viewDidLoad];
}

- (void)swizzled_viewWillAppear:(BOOL)animated {
    [self swizzled_viewWillAppear:animated];
    
    NSTimeInterval duration = [[NSDate date] timeIntervalSinceDate:self.date];
    NSLog(@"Load %@ cost %f", [self class], duration);
}

但这种方案有缺陷,因为swizzled_viewDidLoad是UIViewController的viewDidLoad,只有当子类ViewController执行super viewDidLoad才会加载。

// ViewController
- (void)viewDidLoad {
    sleep(5);
    [super viewDidLoad];
}

像上面的情况,就无法记录到[super viewDidLoad];之前操作的耗时。

那如果改成如下形式,记录viewDidLoad的耗时,同样也是不行的。因为本质是交换了UIViewController类对象中SEL叫做viewDidLoad的methods_t的IMP指针。而ViewController调用的是自身类方法的viewDidLoad。

- (void)swizzled_viewDidLoad
{
    self.date = [NSDate date];
    [self swizzled_viewDidLoad];

    NSTimeInterval duration = [[NSDate date] timeIntervalSinceDate:self.date];
    NSLog(@"Load %@ cost %f", [self class], duration);
}

使用KVO可以实现无痕监控的页面加载时间,具体步骤如下:

  1. 对UIViewController的两个初始化方法initWithCoder:/initWithCoder:initWithNibName:bundle: 进行Method Swizzling,并对VC添加一个任意观察者。这一步的目的是让UIViewController和继承自UIViewController的子类(XXViewController)在初始化时,会动态创建一个新的子类NSKVONotifying_XXViewController
  2. 使用object_getClass函数获得NSKVONotifying_XXViewController类对象,再通过class_getSuperclass函数获取其父类对象XXViewController。在NSKVONotifying_XXViewController类中调用class_addMethod动态添加viewDidLoad方法,覆盖XXViewControllerviewDidLoad
  3. NSKVONotifying_XXViewController添加的viewDidLoad方法中,首先记录时间,然后通过method_getImplementation获取父类XXViewControllerviewDidLoad IMP并调用,最后记录总耗时。

注:关于XXViewController在释放时需要移除观察者的问题,可以利用对象在销毁时,会清除所有的关联对象的特性。在交换的初始化方法中,为对象关联一个对象AssociatedA(记录观察者和keyPath)。当XXViewController销毁时,同时会触发AssociatedA对象销毁并进入dealloc,在dealloc中处理观察者和keyPath的移除操作。

KVC

KVC 指 Key-Value Coding 键值编码。通过setValue: forKey:setValue: forKeyPath: 修改变量,可以修改私有变量,只读变量;也可以通过valueForKey:valueForKeyPath:获取值。
KVC setValue的执行步骤如下:

  1. 查找对象有没有和key配对的set方法,若找到则执行方法并结束,没找到执行步骤2。
  2. 获取对象的类方法accessInstanceVariablesDirectly返回值,默认为true,代表会继续查找实例变量。返回false执行步骤4,true执行步骤3。
@property (class, readonly) BOOL accessInstanceVariablesDirectly;

Returns a Boolean value that indicates whether the key-value coding methods should access the corresponding instance variable directly on finding no accessor method for a property.
@property (class, readonly)给类对象增加了set方法,不会生成实例变量,因为readonly也不会生成get方法。

  1. 查找对象和key配对的成员变量,找到执行赋值并结束,没找到执行步骤4。
  2. 调用setValue:forUndefinedKey:方法,该方法默认实现抛出异常NSUnknownKeyException

KVC set方法也会触发KVO,KVC内部实现可以理解为:

- (void)setValue:(id)value forKey:(NSString *)key {
    [self willChangeValueForKey:key];
    if 设置value成功 {
        [self didChangeValueForKey:key];
    }
}

KVCget方法执行步骤和set类似,也会通过accessInstanceVariablesDirectly判断是否查找成员变量。

Category

Category基于ObjC的runtime机制,在程序启动时进行加载,这一点不同于Extension,Extension是类的一部分,在编译时已经和类绑定在一起。因此,Extension可以添加实例变量,而Category是无法添加实例变量的(因为在运行期间,实例对象的内存布局已经确定)。

Category在内存中是个结构体,其定义如下:

struct category_t {
    const char *name; // 类名
    classref_t cls; // 一般传0
    WrappedPtr<method_list_t, PtrauthStrip> instanceMethods; // 实例方法
    WrappedPtr<method_list_t, PtrauthStrip> classMethods; // 类方法
    struct protocol_list_t *protocols; // 协议
    struct property_list_t *instanceProperties; // 实例属性(声明)
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties; 
};

下面来看下程序启动时,分类是在何时加载的。
_objc_init是runtime的入口函数,并在map_images函数中完成类的初始化等操作。

/***********************************************************************
* _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();
    runtime_init();
    exception_init();
#if __OBJC2__
    cache_t::init();
#endif
    _imp_implementationWithBlock_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

void map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{
    mutex_locker_t lock(runtimeLock);
    return map_images_nolock(count, paths, mhdrs);
}

map_images 内部最终会调用_read_images函数,以下为简化后的_read_images函数。分类的加载在load_categories_nolock中。

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)

    ts.log("IMAGE TIMES: first time tasks");
    ts.log("IMAGE TIMES: fix up selector references");
    ts.log("IMAGE TIMES: discover classes");
    ts.log("IMAGE TIMES: remap classes");
    ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");
    ts.log("IMAGE TIMES: discover protocols");
    ts.log("IMAGE TIMES: fix up @protocol references");

    // Discover categories. Only do this after the initial category
    // attachment has been done. For categories present at startup,
    // discovery is deferred until the first load_images call after
    // the call to _dyld_objc_notify_register completes. rdar://problem/53119145
    if (didInitialAttachCategories) {
        for (EACH_HEADER) {
            load_categories_nolock(hi);
        }
    }
    ts.log("IMAGE TIMES: discover categories");

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

}

load_categories_nolock最终会调用attachCategories函数。
attachCategories负责将所有关于类cls的分类cats_list添加到cls中,其中cats_list是个数组,每个元素locstamped_category_t中存有一个分类category_t。cats_count是cats_list数组的长度。
分类数组的顺序为分类文件的编译顺序,可以在Compile Sources中调节顺序。

下面是简化后的attachCategories方法。

struct locstamped_category_t {
    category_t *cat;
    struct header_info *hi;
};

static void attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    // 获取类对象的rw表。
    auto rwe = cls->data()->extAllocIfNeeded();

    for (uint32_t i = 0; i < cats_count; i++) {
        // entry代表一个分类
        auto& entry = cats_list[i];

        // 取出一个分类的所有方法
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                // 将分类的方法加入到类对象的rw表的methods中。  
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        // 取出某个分类的所有属性
        property_list_t *proplist =
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            if (propcount == ATTACH_BUFSIZ) {
                // 将分类的属性加入到类对象的rw表的properties中。  
                rwe->properties.attachLists(proplists, propcount);
                propcount = 0;
            }
            proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
        }
        // 取出某个分类的所有协议
        protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
        if (protolist) {
            if (protocount == ATTACH_BUFSIZ) {
                // 将分类的协议加入到类对象的rw表的protocols中。  
                rwe->protocols.attachLists(protolists, protocount);
                protocount = 0;
            }
            protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
        }
    }

    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);

    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

类对象的方法列表method_array_t在类加载时默认只有1个元素method_list_t,method_list_t中包含类编译时确认的所有方法,每个方法是一个method_t。
当加载分类时,会将所有分类的方法放在mlists数组中,每个mlists元素method_list_t代表一个分类的所有方法。
判断method_array_t空间是否能装下所有分类,若需要扩容,则将所有元素整体后移,然后将mlists加到method_array_t前面。

Load方法

类和分类中都支持Load方法,当runtime加载类和分类时,分别会触发类和分类的Load方法。

回到runtime的入口函数objc_init,前文已经讲过map_images中进行了类和分类的加载,在map_images执行完后,会调用load_images函数,并在其中触发Load方法。

// objc_init
_dyld_objc_notify_register(&map_images, load_images, unmap_image);

void load_images(const char *path __unused, const struct mach_header *mh)
{
   if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
       didInitialAttachCategories = true;
       loadAllCategories();
   }

   // Return without taking locks if there are no +load methods here.
   if (!hasLoadMethods((const headerType *)mh)) return;

   recursive_mutex_locker_t lock(loadMethodLock);

   // Discover load methods
   {
       mutex_locker_t lock2(runtimeLock);
       prepare_load_methods((const headerType *)mh);
   }

   // Call +load methods (without runtimeLock - re-entrant)
   call_load_methods();
}

在load_images函数最后一行,可以看到调用了call_load_methods,从名称能看出在这里进行了Load方法的调用。
prepare_load_methods函数的作用是将所有的load方法封装成loadable_class和loadable_category结构,分别代表class的load与分类的load,然后放入全局数组loadable_classes和loadable_categories中。

struct loadable_class {
    Class cls;  // may be nil
    IMP method;
};
struct loadable_category {
    Category cat;  // may be nil
    IMP method;
};
// List of classes that need +load called (pending superclass +load)
// This list always has superclasses first because of the way it is constructed
static struct loadable_class *loadable_classes = nil;
static int loadable_classes_used = 0;
static int loadable_classes_allocated = 0;

// List of categories that need +load called (pending parent class +load)
static struct loadable_category *loadable_categories = nil;
static int loadable_categories_used = 0;
static int loadable_categories_allocated = 0;

call_load_methods方法中,先调用call_class_loads初始化类的load方法,再调用call_category_loads初始化分类的load方法。

void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;
    // 开启自动释放池
    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            // 先调用类的load方法
            call_class_loads();
        }

        // 2. Call category +loads ONCE
            // 再调用类的load方法
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

可以看到call_class_loads中对loadable_classes中所有类进行遍历,并调用load方法。
注意此时(*load_method)(cls, @selector(load));调用是直接通过函数地址IMP调用,并没有通过objc_msgSend发送消息。

static void call_class_loads(void)
{
    int i;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, @selector(load));
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

从上面的代码可以看出,load方法的调用顺序即loadable_classes数组的顺序。那我们要了解load的顺序,可以回到prepare_load_methods函数,看看里面初始化loadable_classes数组的顺序。

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertLocked();

    classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        realizeClassWithoutSwift(cls, nil);
        ASSERT(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

_getObjc2NonlazyClassList函数返回所有的的非懒加载类列表,其顺序受到编译顺序影响。
schedule_class_load函数会将指定类的加入到loadable_classes数组中。

static void schedule_class_load(Class cls)
{
    if (!cls) return;
    ASSERT(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    // Ensure superclass-first ordering
    schedule_class_load(cls->getSuperclass());

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = cls->getLoadMethod();
    if (!method) return;  // Don't bother if cls has no +load method
    
    if (PrintLoading) {
        _objc_inform("LOAD: class '%s' scheduled for +load", 
                     cls->nameForLogging());
    }
    
    if (loadable_classes_used == loadable_classes_allocated) {
        loadable_classes_allocated = loadable_classes_allocated*2 + 16;
        loadable_classes = (struct loadable_class *)
            realloc(loadable_classes,
                              loadable_classes_allocated *
                              sizeof(struct loadable_class));
    }
    
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}

add_class_to_loadable_list(cls);的作用是查找该类的原始ro(只读)方法列表中,是否有名为load的方法,如果有则将该类和方法IMP包装成loadable_class结构体,添加到loadable_classes中。
注意,这里不会遍历superclass的方法列表;使用ro方法列表而不是rw方法列表,因为rw方法列表包含分类添加的方法,ro只有类编译时的方法。

static void schedule_class_load(Class cls)
{
    if (!cls) return;
    ASSERT(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    // Ensure superclass-first ordering
    schedule_class_load(cls->getSuperclass());

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

可以看到schedule_class_load是个递归调用,即使传入子类,但最终会现将父类包装的loadable_class结构体加入到loadable_classes中。
同时为了防止一个类的load被多次调用,类对象class_rw_t中的flags成员变量中有一位用于标识该类是否调用了类的load方法。

#define RW_LOADED             (1<<23)

// objc_class
void setInfo(uint32_t set) {
    ASSERT(isFuture()  ||  isRealized());
    data()->setFlags(set);
}

下面来看下分类的调用顺序。回到上面的prepare_load_methods函数,_getObjc2NonlazyCategoryList函数返回所有的的非懒加载分类列表,其顺序受到编译顺序影响。
之后for循环正序遍历调用add_category_to_loadable_list方法,因此分类中load顺序不受到继承关系影响。

load调用顺序总结:
类的load方法优先于分类的load方法。
类的load方法中优先调用父类的load方法。
不存在继承关系的不同类的load方法按照编译顺序调用。
不同分类的load方法按照编译顺序调用。

Initialize方法

initialize方法在类第一次接收到消息时调用。
与load不同的是,initialize方法是发送消息的方式调用,而不是方法指针IMP直接调用。

发送消息objc_msgSend方法,查找类方法时会调用class_getClassMethod方法,class_getClassMethod内部调用顺序如下。

Method class_getClassMethod(Class cls, SEL sel)
Method class_getInstanceMethod(Class cls, SEL sel)
lookUpImpOrForward(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER); //IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
void initializeNonMetaClass(Class cls)

initializeNonMetaClass和前面load的加载方法schedule_class_load类似,内部是个递归调用,若父类未初始化则对父类递归调用,最终优先调用父类的callInitialize方法。
callInitialize方法内部实现就是发送消息。

/***********************************************************************
* class_initialize.  Send the '+initialize' message on demand to any
* uninitialized class. Force initialization of superclasses first.
**********************************************************************/
void initializeNonMetaClass(Class cls)
{
    ASSERT(!cls->isMetaClass());

    Class supercls;
    bool reallyInitialize = NO;

    // Make sure super is done initializing BEFORE beginning to initialize cls.
    // See note about deadlock above.
    supercls = cls->getSuperclass();
    if (supercls  &&  !supercls->isInitialized()) {
        initializeNonMetaClass(supercls);
    }

    // ...

    callInitialize(cls);

  // ...
    cls->setInitialized();

}

void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
    asm("");
}

class是否被初始化,存储在元类对象的rw表flags字段中。

#define RW_INITIALIZED        (1<<29)

// objc_class
bool isInitialized() {
    return getMeta()->data()->flags & RW_INITIALIZED;
}

initialize调用顺序总结:
父类的initialize方法优先于子类的initialize方法。

initialize要点:
因为initialize调用最终是发送消息,所以一般分类的initialize会覆盖类的initialize。(method_array_t中分类方法排在前面)
因为initialize调用最终是发送消息,可能会出现一个类的initialize被多次调用。
像下面的代码,LJStudent最终会递归给LJStudent和LJPerson的元类对象发送initialize消息。而LJStudent中未实现initialize方法,会在方法查找过程中调用[LJPerson initialize]方法,最终导致[LJPerson initialize]两次调用。因此,有时需要配合dispatch_once防止initialize被多次调用。

@interface LJPerson : NSObject
@end

@implementation LJPerson
+ (void)initialize {
    NSLog(@"%s", __func__);
}
@end

@interface LJStudent : LJPerson
@end

@implementation LJStudent
@end

int main(int argc, const char * argv[]) {
    [LJStudent alloc];
    return 0;
}

输出结果:
+[LJPerson initialize]
+[LJPerson initialize]

initialize相比load优点:
load的调用在runtime初始化时,会影响程序启动速度。initialize类似懒加载,首次收到消息时调用。一般必须在启动时就初始化好的工作放在load中,比如method swizzling,其它工作尽量放在initialize中进行。
initialize与load调用的区别:
load调用会递归类继承树并在每个类中查找是否存在load方法,找到则通过IMP直接调用,若未找到则无法调用。这样一个类的load只会被调用一次。

initialize调用是消息发送,递归类继承树并对每个类发送消息,这样一些类未实现initialize则会交给父类,可能会导致一个类的initialize被多次调用。

AssociatedObject

与关联对象有关的类有四个,分别是AssociationsManager,AssociationHashMap,ObjectAssociationMap和ObjectAssociation。


AssociatedObject

从下面代码可以看到AssociationsManager中有个静态变量_mapStorage(AssociationsHashMap)。

class AssociationsManager {
    using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
    static Storage _mapStorage;

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

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

    static void init() {
        _mapStorage.init();
    }
};

可以通过如下代码获取AssociationsHashMap。可以看到manager并没有做成单例,但所有的manager共同使用一个AssociationsHashMap。
AssociationsHashMap存储着程序中所有的关联对象信息。

AssociationsManager manager;
AssociationsHashMap &associations(manager.get());

AssociationsHashMap的原始类型如下,是DenseMap模版类型的哈希表。

typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;

template <typename KeyT, typename ValueT,
          typename ValueInfoT = DenseMapValueInfo<ValueT>,
          typename KeyInfoT = DenseMapInfo<KeyT>,
          typename BucketT = detail::DenseMapPair<KeyT, ValueT>>
class DenseMap : public DenseMapBase<DenseMap<KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>,
                                     KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT> {

}

通过模版可以看到,AssociationsHashMap的Key是objc_object指针类型(DisguisedPtr是对指针的封装),Value是ObjectAssociationMap。
而ObjectAssociationMap的原始类型如下,也是DenseMap模版类型的哈希表。从这里可以看出,这是个嵌套哈希表结构。

typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;

ObjectAssociationMap哈希表的Key是const void *指针类型,Value是ObjcAssociation。

class ObjcAssociation {
    uintptr_t _policy;
    id _value;
}

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

可以看到ObjcAssociation的成员变量对应着objc_setAssociatedObject函数的value和policy参数。那么可以知道ObjcAssociation封装了一个关联对象的信息,包括关联的对象和关联策略。

总结:
AssociationsHashMap是个哈希表,存储所有对象的关联对象哈希表。以对象的地址作为Key,Value是ObjectAssociationMap。
ObjectAssociationMap是个哈希表,存储着一个对象的所有关联对象。其中以一个关联对象的传参key作为哈希表的Key,哈希表的Value是ObjcAssociation。
ObjcAssociation存储一个关联对象信息,包括关联的对象和关联策略。

在objc_setAssociatedObject的最后执行了以下代码:

if (isFirstAssociation)
{
    object->setHasAssociatedObjects();
}

// objc_object::setHasAssociatedObjects
inline void objc_object::setHasAssociatedObjects()
{
    if (isTaggedPointer()) return;

    // ...

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

在setHasAssociatedObjects中,通过isa_t类型访问isa指针,并通过newisa.has_assoc = true;操作,将isa中标识是否有关联对象的位域设置为true。
has_assoc位域的作用是标识该对象是否存在关联对象。当对象销毁时,若has_assoc为true,则会去AssociationsHashMap将与该对象有关的ObjectAssociationMap销毁。

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        // assoc代表该对象是否存在关联对象
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        // 若该对象存在关联对象,则从AssociationsHashMap中移除该对象的关联对象ObjectAssociationMap
        if (assoc) _object_remove_assocations(obj, /*deallocating*/true);
        obj->clearDeallocating();
    }

    return obj;
}

objc_object::hasAssociatedObjects()
{
    if (isTaggedPointer()) return true;
    if (isa.nonpointer) return isa.has_assoc;
    return true;
}
method swizzling

下面是一段方法交换代码:

+ (void)load {

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 当前类
        Class class = [self class];

        // 原方法名 和 替换方法名
        SEL originalSelector = @selector(originalFunction);
        SEL swizzledSelector = @selector(swizzledFunction);

        // 原方法结构体 和 替换方法结构体
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        /* 如果当前类没有 原方法的 IMP,说明在从父类继承过来的方法实现,
         * 需要在当前类中添加一个 originalSelector 方法,
         * 但是用 替换方法 swizzledMethod 去实现它 
         */
        BOOL didAddMethod = class_addMethod(class,
                                            originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            // 原方法的 IMP 添加成功后,修改 替换方法的 IMP 为 原始方法的 IMP
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            // 添加失败(说明已包含原方法的 IMP),调用交换两个方法的实现
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

class_addMethod函数返回成功,代表类中并没有originalSelector,子类成功添加originalSelector指向swizzledMethod;class_addMethod函数返回失败,代表子类中已经有originalSelector,添加失败。
若添加成功,则将子类的swizzledSelector指向父类originalMethod。
若添加失败,则将子类的originalSelector指向子类swizzledMethod,子类的方法swizzledSelector指向子类originalMethod。

从objc4源码看出Method即method_t结构体指针。前文已经讲过,method_t中包含方法SEL,方法地址IMP,和方法签名信息types。

typedef struct method_t *Method;

下面的代码可以看到,method_exchangeImplementations方法本质是将两个method_t中的IMP更换了下位置。在更换完成后,调用flushCaches刷新了方法缓存中的方法。

void method_exchangeImplementations(Method m1, Method m2)
{
    if (!m1  ||  !m2) return;

    mutex_locker_t lock(runtimeLock);

    IMP imp1 = m1->imp(false);
    IMP imp2 = m2->imp(false);
    SEL sel1 = m1->name();
    SEL sel2 = m2->name();

    m1->setImp(imp2);
    m2->setImp(imp1);

    flushCaches(nil, __func__, [sel1, sel2, imp1, imp2](Class c){
        return c->cache.shouldFlush(sel1, imp1) || c->cache.shouldFlush(sel2, imp2);
    });

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

推荐阅读更多精彩内容