iOS 2021 面试前的准备(总结各知识点方便面试前快速复习使用)(七)

1,817 阅读12分钟

55. 类声明中的成员变量的顺序和实际的成员变量的顺序。

 在面向对象(oop)的编程语言中,每一个对象都是某个类的实例。在 Objective-C 中,所有对象的本质都是一个 objc_object 结构体,且每个实例对象的第一个成员变量都是 isa,可从中取得该对象所属的类,每一个类描述了一系列它的实例对象的信息,包括对象占用内存大小、成员变量列表、该对象能执行的函数列表...等等。

 在一个类的实例对象的内存布局中,第一个成员变量是 isa,然后根据该对象所属类的继承体系依次对成员变量排序,排列顺序是: 根类的成员变量、父类的成员变量、最后才是自己的成员变量,且每个类定义中的成员变量(仅包含使用 @property 声明属性后由编译器生成的同名的 _成员变量)相互之间的顺序可能会与定义时的顺序不同,编译器会在内存对齐的原则下对类定义时的成员变量的顺序做出优化,保证内存占用最少。(还会涉及到 .h 中的成员变量和属性,.mextension 中添加的成员变量和属性,它们之间的排序顺序)

 验证代码:

// SubObject 类定义
@interface SubObject : BaseObject {
    NSArray *cus_array;
}

@property (nonatomic, assign) int cus_int;
@property (nonatomic, assign) double cus_dou;
@property (nonatomic, assign) int cus_int2;
@property (nonatomic, copy) NSString *cus_string;

@end

// 添加断点,控制台打印
(lldb) p *sub
(SubObject) $2 = {
  BaseObject = {
    NSObject = {
      isa = SubObject
    }
    baseString = nil
    _baseArray = nil
  }
  cus_array = nil
  _cus_int = 0
  _cus_int2 = 0
  _cus_dou = 0
  _cus_string = nil
}

 可看到 NSObjectisa 在最前面,然后是 BaseObject 的成员变量,最后才是 SubObject 的成员变量,然后注意 _cus_int2 跑到了 _cus_dou 前面,而在类定义时 cus_dou 属性是在 cus_int2 属性前面的。(由于内存对齐时不用再为 double 补位,这样至少减少了 4 个字节的内存浪费)


56. 为什么不能动态的给类添加成员变量却可以添加方法?

 类的成员变量布局以及其实例对象大小在编译时就已确定,设想一下,如果 Objective-C 中允许给一个类动态添加成员变量,会带来一个问题:为基类动态增加成员变量会导致所有已创建出的子类实例都无法使用。我们所说的 “类的实例”(对象),指的是一块内存区域,里面存储了 isa 指针和所有的成员变量。所以假如允许动态修改类已固定的成员变量的布局,那么那些已经创建出的对象就不符合类的定义了,那就变成无效对象了。而方法的定义都是在类对象或元类对象中的,不管如何增删方法,都不会影响对象的内存布局,已经创建出的对象仍然可以正常使用。


57. ISA_BITFIELD 中的 64 位分别都代表什么。

#   define ISA_BITFIELD                                                      \
      // 表示 isa 中只是存放的 Class cls 指针还是包含更多信息的 bits
      uintptr_t nonpointer        : 1;                                       \
      // 标记该对象是否有关联对象,如果没有的话对象能更快的销毁,
      // 如果有的话销毁前会调用 _object_remove_assocations 函数根据关联策略循环释放每个关联对象
      uintptr_t has_assoc         : 1;                                       \
      // 标记该对象所属类是否有自定义的 C++ 析构函数,如果没有的话对象能更快销毁,
      // 如果有的话对象销毁前会调用 object_cxxDestruct 函数去执行该类的析构函数
      uintptr_t has_cxx_dtor      : 1;                                       \
      // isa & ISA_MASK 得出该实例对象所属的的类的地址
      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
      // 用于调试器判断当前对象是真的对象还是没有初始化的空间
      uintptr_t magic             : 6;                                       \
      // 标记该对象是否有弱引用,如果没有的话对象能更快销毁,
      // 如果有的话对象销毁前会调用 weak_clear_no_lock 函数把该对象的弱引用置为 nil,
      // 并调用 weak_entry_remove 把对象的 entry 从 weak_table 中移除
      uintptr_t weakly_referenced : 1;                                       \
      // 标记该对象是否正在执行销毁
      uintptr_t deallocating      : 1;                                       \
      // 标记 refcnts 中是否也有保存实例对象的引用计数,当 extra_rc 溢出时会把一部分引用计数保存到 refcnts 中去,
      uintptr_t has_sidetable_rc  : 1;                                       \
      // 保存该对象的引用计数 -1 的值(未溢出之前,溢出后存放 RC_HALF)
      uintptr_t extra_rc          : 19 // 最大保存 2^19 - 1,觉得这个值很大呀, mac 下是 2^8 - 1 = 255
#   define RC_ONE   (1ULL<<45)
#   define RC_HALF  (1ULL<<18)

58. ISA() 函数返回的是 objc_object 所属的类地址。

 三种情况,一种是 isa 的位域(indexcls)中保存的是类对象在全局类表中的索引。一种是 isa 就是一个类指针。一种是 isa 的位域(shiftcls)中保存的是类对象的地址。

// ISA() assumes this is NOT a tagged pointer object
// 假定不是 tagged pointer 对象时调用该函数
inline Class 
objc_object::ISA() 
{
    // 如果是 tagged pointer 则直接执行断言
    ASSERT(!isTaggedPointer()); 
#if SUPPORT_INDEXED_ISA
    // 支持在 isa 中保存类的索引的情况下
    
    if (isa.nonpointer) {
        uintptr_t slot = isa.indexcls;
        
        // 根据索引返回 class table 中的 Class
        return classForIndex((unsigned)slot);
    }
    
    // 如果是非优化指针直接返回 isa 中的 bits,由于 isa 是 union 所以 (Class)isa.bits 和 (Class)isa.cls 的值是一样的。
    return (Class)isa.bits;
#else
    // 从 shiftcls 位域取得 Class 指针,这个是我们平时用到的最多的,我们日常使用的实例对象获取所属的类都是通过这种方式。
    return (Class)(isa.bits & ISA_MASK);
#endif
}

59. isWeaklyReferenced / sidetable_isWeaklyReferenced

 判断对象是否存在弱引用。

inline bool
objc_object::isWeaklyReferenced()
{
    // 如果是 Tagged Pointer 执行断言
    ASSERT(!isTaggedPointer());
    
    // 如果是非指针则返回 weakly_referenced 标记位
    if (isa.nonpointer) return isa.weakly_referenced;
    
    // 其他情况调用 sidetable_isWeaklyReferenced
    //(当 isa 是 objc_class 指针时,对象的弱引用标识位在 SideTable 的 refcnts 中)
    else return sidetable_isWeaklyReferenced();
}

isa 是原始类指针的对象的是否有弱引用的标识在 refcnts 中。

bool 
objc_object::sidetable_isWeaklyReferenced()
{
    bool result = false;
    
    // 取得对象所处的 SideTable
    SideTable& table = SideTables()[this];
    // 加锁
    table.lock();
    
    RefcountMap::iterator it = table.refcnts.find(this);
    // 判断当前对象是否存在 SideTable 的 refcnts 中
    if (it != table.refcnts.end()) {
        // 如果存在 
        // it->second 是引用计数 与 SIDE_TABLE_WEAKLY_REFERENCED 进行与操作
        // 引用计数值的第 1 位是弱引用的标识位哦
        result = it->second & SIDE_TABLE_WEAKLY_REFERENCED;
    }
    
    // 解锁
    table.unlock();

    return result;
}

60. Tagged Pointer 解读。

 2013 年 9 月,苹果首次在 iOS 平台推出了搭载 64 位架构处理器的 iPhone(iPhone 5s),为了节省内存和提高运行效率,提出了 Tagged Pointer 概念。

 Tagged Pointer 是苹果为了在 64 位架构的处理器下节省内存占用和提高运行效率而提出的概念。它的本质是把一些占用内存较小的对象的数据直接放在指针的内存空间内,然后把这个指针直接作为对象使用,直接省去了为对象在堆区开辟空间的过程。

 这里引出了一个疑问,“对象的内存都是位于堆区吗?” 是的。下面是我自己的推测:默认这里说的对象都是 NSObject 的子类,当深入看 + (id)alloc 函数时,可看到最后面开辟空间都是使用的 malloc(calloc 函数内部是调用 malloc 后再调用 bzero 置 0)函数,而 malloc 是 C 的运行库函数,向它申请的内存都是 C 运行库管理,采用堆的内存管理方式。该函数实际上会向操作系统申请内存,然后分配给请求者,同时其内部维护有它申请的内存的分配情况,以便管理其拥有的内存。

 指针变量的长度与地址总线有关。从 32 位系统架构切换到 64 位系统架构后,指针变量的长度也会由 32 位增加到 64 位。如果不考虑其它因素,64 位指针可表示的地址长度可达到 2^64 字节即 2^34 TB,以目前的设备的内存来看,使用 8 个字节存储一个地址数据,其实有很多位都是空余的,而 Tagged Pointer 正是为了把这些空余的空间利用起来。(例如,在 iPhone 真机下,在堆区创建一个 NSObject 对象,打印的它的地址,看到只占用了 36 位,剩下 28 位都是零。)

 明确一点,NSInteger/NSUInteger 是使用 typedef 声明的基本类型 long/int/unsigned long/unsigned int。NSNumber、NSString、NSDate 等都是继承自 NSObject 的子类。

 在 objc-runtime-new.h,CF 要求所有对象至少为 16 个字节。(对象内部成员变量多为 8 字节对齐,如果最后对齐后对象内存小于 16 字节,则扩展为 16 字节。)

 _OBJC_TAG_MASK 表示在字符串高位优先排序的平台下指针变量的第 64 位标记该指针为 Tagged Pointer,在字符串低位优先排序的平台下指针变量的第 1 位标记该指针为 Tagged Pointer。

 在 iOS 真机上判断是否是 Tagged Pointer 看指针的第 64 比特位是否是 1,在 x86_64 架构的 Mac 下看指针的第 1 个比特位是否是 1。(即在 iOS 中判断最高位,在 mac 中判断最低位)

 分析打印结果,可看到所有 Tagged Pointer 的 64 位内存使用几乎都是满的,最高位都是 1,malloc_size 返回的都是 0,对比最后非 Tagged Pointer 系统没有为对象开辟空间。正常的 Objective-C 实例对象的第一个成员变量都是指向类对象内存地址的 isa 指针,通过打断点,可看到所有 Tagged Pointer 的 isa 都是 0x0,且当 Tagged Pointer 是 NSNumber 类型时,class 函数的打印依然是 __NSCFNumber,苹果并没有设计一个单独的 Class 来表示 Tagged Pointer,NSString 则打印的是 NSTaggedPointerString,那这里引出了另外一个问题,Tagged Pointer 又是怎么获取所属的类呢?

 为何可通过设定最高位或最低位来标识 Tagged Pointer? 这是因为在分配内存的时候,都是按 2 的整数倍来分配的,这样分配出来的正常内存地址末位不可能为 1,通过将最低标识为 1,就可以和其他正常指针做出区分。那么为什么最高位为 1,也可以标识呢 ?(目前 iOS 设备的内存都是固定的,如 iPhone、iPad、iWatch 都是固定的,不像是 mac 产品我们可以自己加装内存条)这是因为 64 位操作系统,设备一般没有那么大的内存,所以内存地址一般只有 48 个左右有效位(64 位 iOS 堆区地址只使用了 36 位有效位),也就是说高位的 16 位左右都为 0,所以可以通过最高位标识为 1 来表示 Tagged Pointer。那么既然 1 位就可以标识 Tagged Pointer 了,其他的信息是干嘛的呢?我们可以想象的,首先要有一些 bit 位来表示这个指针对应的类型,例如 NSNumber 在 LSB 中第一高位除外的接下来的三位表示所属类型在 Tagged Pointer 类表中的索引,然后接下的 60 位则是用来存储值,负载数据容量,用来存储对象数据。

* Tagged pointer 指针对象将 class 和对象数据存储在对象指针中,指针实际上不指向任何东西。
* Tagged pointer 当前使用此表示形式:
*
* (LSB)(字符串低位优先排序,64 位的 mac 下)
*  1 bit   set if tagged, clear if ordinary object pointer // 值为 1 标记是 tagged pointer,如果是普通对象指针则是 0
*  3 bits  tag index // 标记类型
* 60 bits  payload // 负载数据容量,(存储对象数据)
*
* (MSB)(64 位 iPhone 下)
* tag index 表示对象所属的 class。负载格式由对象的 class 定义。
*
* 如果 tag index 是 0b111(7), tagged pointer 对象使用 “扩展” 表示形式,允许更多类,但有效载荷更小: 
* (LSB)(字符串低位优先排序,64 位的 mac 下)
*  1 bit   set if tagged, clear if ordinary object pointer // 值为 1 标记是 tagged pointer,如果是普通对象指针则是 0
*  3 bits  0b111 
*  8 bits  extended tag index // 扩展的 tag index
* 52 bits  payload // 负载数据容量,此时只有 52 位
* (MSB)

 另外的一个延展,Tagged Pointer 可存储的最大值。当 Tagged Pointer 是 NSNumber 类型时,在 x86_64 Mac 平台下:

NSNumber *number = [[NSNumber alloc] initWithInteger: pow(2, 55) - 2];;
NSLog(@"number %p %@ %zu", number, [number class], malloc_size(CFBridgingRetain(number)));
// 打印:
number 0x10063e330 __NSCFNumber 32

NSNumber *number = [[NSNumber alloc] initWithInteger: pow(2, 55) - 3];;
NSLog(@"number %p %@ %zu", number, [number class], malloc_size(CFBridgingRetain(number)));
// 打印:
number 0x21a60cf72f053d4b __NSCFNumber 0

 在 x86_64 Mac 平台下存储 NSString 类型的 Tagged Pointer,一个指针 8 个字节,64 个比特位,第 1 个比特位用于标记是否是 Tagged Pointer,第 2~4 比特位用于标记 Tagged Pointer 的指针类型,解码后的最后 4 个比特位用于标记 value 的长度,那么用于存储 value 的比特位只有 56 个了,此时如果每个字符用 ASCII 编码的话 8 个字符应该就不是 Tagged Pointer 了,但其实 NSTaggedPointerString 采用不同的编码方式:

  1. 如果长度介于 0 到 7,直接用八位编码存储字符串。
  2. 如果长度是 8 或 9,用六位编码存储字符串,使用编码表 eilotrm.apdnsIc ufkMShjTRxgC4013bDNvwyUL2O856P-B79AFKEWV_zGJ/HYX。
  3. 如果长度是 10 或 11,用五位编码存储字符串,使用编码表 eilotrm.apdnsIc ufkMShjTRxgC4013。

 @"aaaaaaaa"(8 个 a) 解码后的 TaggedPointer 值为 0x2082082082088,扣除最后 4 个比特位代表的长度,则为 0x208208208208,只有 6 个字节,但是因为长度为 8,需要进行分组解码,6 个比特位为一组,分组后为 0x0808080808080808,刚好 8 个字节,长度符合了。采用编码表 eilotrm.apdnsIc ufkMShjTRxgC4013bDNvwyUL2O856P-B79AFKEWV_zGJ/HYX,下标为 8 的刚好是 a。

 @"aaaaaaaaaa"(10 个 a) 解码后的 TaggedPointer 值为 0x1084210842108a,扣除最后 4 个比特位代表的长度,则为 0x1084210842108,只有 6.5 字节,但是因为长度为 10,需要进行分组解码,5 个比特位为一组,分组后为 0x08080808080808080808,刚好 10 个字节,长度符合了。采用编码表 eilotrm.apdnsIc ufkMShjTRxgC4013,下标为 8 的刚好是 a。

 在编码表中并没有看到 + 字符,使用 + 字符做个测试,7 个 + 应为 NSTaggedPointerString,而 8 个 + 则为普通的 __NSCFString 对象。

 关于字符串的存储可以参考: 《译】采用Tagged Pointer的字符串》


61. 在 category 中为现有类添加属性时。

 使用 Category 为已经存在的类添加方法是我们很熟悉的常规操作,但是如果在 Category 中为类添加属性 @property,则编译器会立即给我们如下警告:

Property 'categoryProperty' requires method 'categoryProperty' to be defined - use @dynamic or provide a method implementation in this category.
Property 'categoryProperty' requires method 'setCategoryProperty:' to be defined - use @dynamic or provide a method implementation in this category

 提示我们需要手动为属性添加 setter、gettr 方法或者使用 @dynamic 标记告诉编译器是在运行时实现这些方法,即这也明确的告诉了我们在分类中 @property 并不会自动生成下划线实例变量以及 setter 和 getter 存取方法。

 不是说好的使用 @property,编译器会自动帮我们生成下划线实例变量和对应的 setter 和 getter 方法吗?此机制只能在类定义中实现,因为在分类中,类的实例变量的内存布局已经固定,使用 @property 已经无法向固定的内存布局中添加新的实例变量,所以我们需要使用关联对象以及两个方法 objc_getAssociatedObject、objc_setAssociatedObject 来模拟构成属性的三个要素。

 示例代码:

#import "HMObject.h"

NS_ASSUME_NONNULL_BEGIN

@interface HMObject (category)

// 在分类中添加一个属性
@property (nonatomic, copy) NSString *categoryProperty;

@end

NS_ASSUME_NONNULL_END
#import "HMObject+category.h"
#import <objc/runtime.h> 

@implementation HMObject (category)

- (NSString *)categoryProperty {
    // _cmd 代指当前方法的选择子,即 @selector(categoryProperty)
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setCategoryProperty:(NSString *)categoryProperty {
    objc_setAssociatedObject(self,
                             @selector(categoryProperty),
                             categoryProperty,
                             OBJC_ASSOCIATION_COPY_NONATOMIC);
}

@end

 此时我们可以使用关联对象 Associated Object 来手动为 categoryProperty 属性添加存取方法。


62. 在类定义中添加属性时。

 在类定义中我们使用 @property 为类添加属性时,如果不使用 @dynamic 标识该属性的话,编译器会自动帮我们生成一个名字为下划线加属性名的实例变量和该属性的 setter 和 getter 方法。我们编写如下代码:

// .h 中如下书写
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface HMObject : NSObject
@property (nonatomic, copy) NSString *cusProperty;
@end

NS_ASSUME_NONNULL_END

// .m 中什么都不做
#import "HMObject.h"
@implementation HMObject
// @dynamic cusProperty;

@end

 编译器会自动帮我们做如下三件事:

  1. 添加实例变量 _cusProperty
  2. 添加 setter 方法 setCusProperty
  3. 添加 getter 方法 cusProperty  即如下 HMObject.m 的代码实现:
#import "HMObject.h"

@implementation HMObject
// @dynamic cusProperty;
{
    NSString *_cusProperty;
}

- (void)setCusProperty:(NSString *)cusProperty {
    _cusProperty = cusProperty;
}

- (NSString *)cusProperty {
    return _cusProperty;
}

@end

 下面我们通过 LLDB 进行验证,首先我们把 HMObject.m 的代码都注释掉,只留下 HMObject.h 中的 cusProperty 属性。然后在 main 函数中编写如下代码:

Class cls = NSClassFromString(@"HMObject");
NSLog(@"%@", cls); // ⬅️ 这里打一个断点

 开始验证:

 这里我们也可以使用 runtime 的 class_copyPropertyList、class_copyMethodList、class_copyIvarList 三个函数来分别获取 HMObject 的属性列表、方法列表和成员变量列表来验证编译器为我们自动生成了什么内容,但是这里我们采用一种更为简单的方法,仅通过控制台打印即可验证。

  1. 找到 cls 的 bits:
(lldb) x/5gx cls
0x1000022e8: 0x00000001000022c0 (isa) 0x00000001003ee140 (superclass)
0x1000022f8: 0x00000001003e84a0 0x0000001c00000000 (cache_t)
0x100002308: 0x0000000101850640 (bits)
  1. 强制转换 class_data_bits_t 指针
(lldb) p (class_data_bits_t *)0x100002308
(class_data_bits_t *) $1 = 0x0000000100002308
  1. 取得 class_rw_t *
(lldb) p $1->data()
(class_rw_t *) $2 = 0x0000000101850640
  1. 取得 class_ro_t *
(lldb) p $2->ro
(const class_ro_t *) $3 = 0x0000000100002128
  1. 打印 ro 内容
(lldb) p *$3
(const class_ro_t) $4 = {
  flags = 388
  instanceStart = 8
  instanceSize = 16
  reserved = 0
  ivarLayout = 0x0000000100000ee6 "\x01"
  name = 0x0000000100000edd "HMObject" // 类名
  baseMethodList = 0x0000000100002170 // 方法列表
  baseProtocols = 0x0000000000000000 // 遵循协议为空
  ivars = 0x00000001000021c0 // 成员变量
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x00000001000021e8 // 属性
  _swiftMetadataInitializer_NEVER_USE = {}
}
  1. 打印 ivars
(lldb) p $4.ivars
(const ivar_list_t *const) $5 = 0x00000001000021c0
(lldb) p *$5
(const ivar_list_t) $6 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0> = {
    entsizeAndFlags = 32
    count = 1 // 有 1 个成员变量
    first = {
      offset = 0x00000001000022b8
      // 看到名字为 _cusProperty 的成员变量
      name = 0x0000000100000ef6 "_cusProperty"
      type = 0x0000000100000f65 "@\"NSString\""
      alignment_raw = 3
      size = 8
    }
  }
}
  1. 打印 baseProperties
(lldb) p $4.baseProperties
(property_list_t *const) $7 = 0x00000001000021e8
(lldb) p *$7
(property_list_t) $8 = {
  entsize_list_tt<property_t, property_list_t, 0> = {
    entsizeAndFlags = 16
    count = 1
    first = (name = "cusProperty", attributes = "T@\"NSString\",C,N,V_cusProperty")
  }
}

 看到只有一个名字是 cusProperty 的属性,属性的 attributes 是:"T@"NSString",C,N,V_cusProperty"

codemeaning
T类型
Ccopy
Nnonatomic
V实例变量

 关于它的详细信息可参考 《Objective-C Runtime Programming Guide》

  1. 打印 baseMethodList
(lldb) p $4.baseMethodList
(method_list_t *const) $9 = 0x0000000100002170
(lldb) p *$9
(method_list_t) $10 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 3 // 有 3 个 method
    first = {
      // 第一个正是 cusProperty 的 getter 函数
      name = "cusProperty"
      types = 0x0000000100000f79 "@16@0:8"
      imp = 0x0000000100000c30 (KCObjcTest`-[HMObject cusProperty])
    }
  }
}

 看到方法的 TypeEncoding 如下:

 types = 0x0000000100000f79 "@16@0:8" 从左向右分别表示的含义是: @ 表示返回类型是 OC 对象,16 表示所有参数总长度,再往后 @ 表示第一个参数的类型,对应函数调用的 self 类型,0 表示从第 0 位开始,分隔号 : 表示第二个参数类型,对应 SEL,8 表示从第 8 位开始,因为前面的一个参数 self 占 8 个字节。下面开始是自定义参数,因为 getter 函数没有自定义函数,所以只有 self 和 SEL 参数就结束了。对应的函数原型正是 objc_msgSend 函数:

void
objc_msgSend(void /* id self, SEL op, ... */ )
  1. 打印剩下的两个 method
(lldb) p $10.get(1)
(method_t) $11 = {
  name = "setCusProperty:"
  types = 0x0000000100000f81 "v24@0:8@16"
  imp = 0x0000000100000c60 (KCObjcTest`-[HMObject setCusProperty:])
}
(lldb) p $10.get(2)
(method_t) $12 = {
  name = ".cxx_destruct"
  types = 0x0000000100000f71 "v16@0:8"
  imp = 0x0000000100000c00 (KCObjcTest`-[HMObject .cxx_destruct])
}

 看到一个是 cusProperty 的 setter 函数,一个是 C++ 的析构函数。

 为了做出对比,我们注释掉 HMObject.h 中的 cusProperty 属性,然后重走上面的流程,可打印出如下信息:

(lldb) x/5gx cls
0x100002240: 0x0000000100002218 0x00000001003ee140
0x100002250: 0x00000001003e84a0 0x0000001000000000
0x100002260: 0x00000001006696c0
(lldb) p (class_data_bits_t *)0x100002260
(class_data_bits_t *) $1 = 0x0000000100002260
(lldb) p $1->data()
(class_rw_t *) $2 = 0x00000001006696c0
(lldb) p $2->ro
(const class_ro_t *) $3 = 0x0000000100002118
(lldb) p *$3
(const class_ro_t) $4 = {
  flags = 128
  instanceStart = 8
  instanceSize = 8
  reserved = 0
  ivarLayout = 0x0000000000000000
  name = 0x0000000100000f22 "HMObject"
  baseMethodList = 0x0000000000000000
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000000000000
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x0000000000000000
  _swiftMetadataInitializer_NEVER_USE = {}
}
(lldb) 

 可看到 ivars、baseProperties 和 baseMethodList 都是 0x0000000000000000,即编译器没有为 HMObject 生成属性、成员变量和函数。至此 @property 的作用可得到完整证明。

 @property 能够为我们自动生成实例变量以及存取方法,而这三者构成了属性这个类似于语法糖的概念,为我们提供了更便利的点语法来访问属性:

 self.property 等价于 [self property];  self.property = value; 等价于 [self setProperty:value];

 习惯于 C/C++ 结构体和结构体指针取结构体成员变量时使用 .->。初见 OC 的点语法时有一丝疑问,self 明明是一个指针,访问它的成员变量时为什么可以用 . 呢?如果按 C/C++ 的规则,不是应该使用 self->_property 吗?

 这里我们应与 C/C++ 的点语法做出区别理解,OC 中点语法是用来帮助我们便捷访问属性的,在类内部我们可以使用 _proerty、self->_propery 和 self.property 三种方式访问同一个成员变量,区别在于使用 self.property 是通过调用 property 的 setter 和 getter 来读取成员变量,而前两种则是直接读取,因此当我们重写属性的 setter 和 getter 并在内部做一些自定义操作时,我们一定要记得使用 self.property 来访问属性而不是直接访问成员变量。


63. Associated Object 原理。

 我们使用 objc_setAssociatedObject 和 objc_getAssociatedObject 来分别模拟属性的存取方法,而使用关联对象模拟实例变量。runtime.h 中定义了如下三个与关联对象相关的函数接口:

 objc_setAssociatedObject 使用给定的键和关联策略为给定的源对象设置关联的值。

/** 
 * @param object 要进行关联行为的源对象
 * @param key 关联的 key
 * @param value 与源对象的键相关联的值。传递 nil 以清除现有的关联。
 * @param policy 关联策略
 * 
 * @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_getAssociatedObject 返回与源对象的给定键关联的值。

/** 
 * @param object 关联的源对象
 * @param key The 关联的 key
 * @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_removeAssociatedObjects 删除给定对象的所有关联。

/** 
 * 意指此函数会一下删除对象全部的关联对象,如果我们想要删除指定的关联对象,
 * 应该使用 objc_setAssociatedObject 函数把 value 参数传递 nil 即可。
 *
 * 此功能的主要目的是使对象轻松返回 “原始状态”,因此不应从该对象中普遍删除关联,
 * 因为它还会删除其他 clients 可能已添加到该对象的关联。
 * 通常,你应该将 objc_setAssociatedObject 与 nil 一起使用以清除指定关联。
 * 
 * @see objc_setAssociatedObject
 * @see objc_getAssociatedObject
 */
OBJC_EXPORT void
objc_removeAssociatedObjects(id _Nonnull object);

 存取函数中的参数 key 我们都使用了 @selector(categoryProperty),其实也可以使用静态指针 static void * 类型的参数来代替,不过这里强烈建议使用 @selector(categoryProperty) 作为 key 传入,因为这种方法省略了声明参数的代码,并且能很好地保证 key 的唯一性。

 policy 代表关联策略:

/**
 * 与关联引用相关的策略。
 */
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_ASSIGN = 0,    
    
    /**< Specifies a strong reference to the associated object. 
    *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, 
    
    /**< Specifies that the associated object is copied. 
    *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
    
    /**< Specifies a strong reference to the associated object.
    *   The association is made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,
    
    /**< Specifies that the associated object is copied.
    *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          
};

 注释已经解释的很清楚了,即不同的策略对应不同的修饰符:

objc_AssociationPolicy修饰符
OBJC_ASSOCIATION_ASSIGNassign
OBJC_ASSOCIATION_RETAIN_NONATOMICnonatomic、strong
OBJC_ASSOCIATION_COPY_NONATOMICnonatomic、copy
OBJC_ASSOCIATION_RETAINatomic, strong
OBJC_ASSOCIATION_COPYatomic, copy

 下面我们看一下 Associated Object 机制中几个关键的数据结构。

class ObjcAssociation 用于保存关联策略 和关联值。

class ObjcAssociation {
    // typedef unsigned long uintptr_t;
    uintptr_t _policy; // 关联策略
    id _value; // 关联值
public:

    ...
    
    // 在 SETTER 时调用,根据关联策略 _policy 判断是否需要持有 _value
    inline void acquireValue() {
        if (_value) {
            switch (_policy & 0xFF) {
            case OBJC_ASSOCIATION_SETTER_RETAIN:
                _value = objc_retain(_value); // retain,调用 objc_retain 函数
                break;
            case OBJC_ASSOCIATION_SETTER_COPY:
                _value = ((id(*)(id, SEL))objc_msgSend)(_value, @selector(copy)); // copy,调用 copy 函数
                break;
            }
        }
    }
    
    // 在 SETTER 时调用:与上面的 acquireValue 函数对应当需要进行释放旧值 _value 时调用 
    inline void releaseHeldValue() {
        if (_value && (_policy & OBJC_ASSOCIATION_SETTER_RETAIN)) {
            objc_release(_value); // release 减少引用计数
        }
    }

    // 在 GETTER 时调用:根据关联策略判断是否对 ReturnedValue 进行 retain 操作
    inline void retainReturnedValue() {
        if (_value && (_policy & OBJC_ASSOCIATION_GETTER_RETAIN)) {
            objc_retain(_value);
        }
    }
    
    // 在 GETTER 时使用:判断是否需要把 _value 放进自动释放池
    inline id autoreleaseReturnedValue() {
        if (slowpath(_value && (_policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE))) {
            return objc_autorelease(_value);
        }
        return _value;
    }
};

 typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap; ObjectAssociationMap 是一个 key 是 const void *,value 是 ObjcAssociation 的哈希表。(const void * 是我们用于关联对象时使用的 key)

 typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap; AssociationsHashMap 是一个 key 是 DisguisedPtr<objc_object>,value 是 ObjectAssociationMap 的哈希表。(DisguisedPtr<objc_object> 是我们的源对象的指针地址,伪装为一个整数使用)

 AssociationsManager 的类定义不复杂,从数据结构角度来看的话它是作为一个 key 是 DisguisedPtr<objc_object>,value 是 ObjectAssociationMap 的哈希表来用的,这么看它好像和上面的 AssociationsHashMap 有些重合,其实它内部正是存储了一个局部静态的 AssociationsHashMap 用来存储程序中 Associated Object 机制所有的关联对象。

class AssociationsManager {
    // Storage 模版类名
    using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
    // 静态变量 _mapStoreage,用于存储 AssociationsHashMap 数据
    static Storage _mapStorage;
    
    ...
    // 返回内部的保存的 AssociationsHashMap,
    AssociationsHashMap &get() {
        return _mapStorage.get();
    }
    ...
};

 综上 Associated Object 所使用的数据结构可总结如下:

  1. 通过 AssociationsManager 的 get 函数取得一个全局的 AssociationsHashMap。
  2. 根据我们源对象的 DisguisedPtr<objc_object> 从 AssociationsHashMap 取得 ObjectAssociationMap。
  3. 根据我们指定的关联 key(const void *key)从 ObjectAssociationMap 取得 ObjcAssociation。
  4. ObjcAssociation 的两个成员变量分别保存了我们的关联策略 _policy 和关联值 _value。

 forbidsAssociatedObjects(表示是否允许某个类的实例对象进行 Associated Object)

// class does not allow associated objects on its instances
#define RW_FORBIDS_ASSOCIATED_OBJECTS       (1<<20)

bool forbidsAssociatedObjects() {
    return (data()->flags & RW_FORBIDS_ASSOCIATED_OBJECTS);
}

 setHasAssociatedObjects 设置对象的 isa 中的 uintptr_t has_assoc : 1; 位,标记该对象存在关联对象,该对象进行 dealloc 时则要进行清理工作。

 _object_set_associative_reference 即是 objc_setAssociatedObject 函数的内部实现,是完整的为源对象添加 Associated Object 的过程。

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    // This code used to work when nil was passed for object and key. Some code
    // probably relies on that to not crash. Check and handle it explicitly.
    // rdar://problem/44094390
    if (!object && !value) return; // 判空对象和关联值都为 nil 则 return

    // 判断该类是否允许关联对象
    if (object->getIsa()->forbidsAssociatedObjects())
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));

    // 伪装 object 指针为 disguised
    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    // 根据入参创建一个 association (关联策略和关联值)
    ObjcAssociation association{policy, value};

    // retain the new value (if any) outside the lock.
    // 在加锁之前根据关联策略判断是否 retain/copy 入参 value 
    association.acquireValue();

    {
        // 创建 mananger 临时变量
        // 这里还有一步连带操作
        // 在其构造函数中 AssociationsManagerLock.lock() 加锁
        AssociationsManager manager;
        // 取得全局的 AssociationsHashMap
        AssociationsHashMap &associations(manager.get());

        if (value) {
            // 这里 DenseMap 对我们而言是一个黑盒,这里只要看 try_emplace 函数
            
            // 在全局 AssociationsHashMap 中尝试插入 <DisguisedPtr<objc_object>, ObjectAssociationMap> 
            // 返回值类型是 std::pair<iterator, bool>
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            // 如果新插入成功
            if (refs_result.second) {
                /* it's the first association we make */
                // 第一次建立 association
                // 设置 uintptr_t has_assoc : 1; 位,标记该对象存在关联对象 
                object->setHasAssociatedObjects();
            }

            /* establish or replace the association */
            // 重建或者替换 association
            auto &refs = refs_result.first->second;
            
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {
                // 替换
                // 如果之前有旧值的话把旧值的成员变量交换到 association
                // 然后在 函数执行结束时把旧值根据对应的关联策略判断执行 release
                association.swap(result.first->second);
            }
        } else {
            // value 为 nil 的情况,表示要把之前的关联对象置为 nil
            // 也可理解为移除指定的关联对象
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                auto &refs = refs_it->second;
                auto it = refs.find(key);
                if (it != refs.end()) {
                    association.swap(it->second);
                    // 清除指定的关联对象
                    refs.erase(it);
                    // 如果当前 object 的关联对象为空了,则同时从全局的 AssociationsHashMap
                    // 中移除该对象
                    if (refs.size() == 0) {
                        associations.erase(refs_it);
                    }
                }
            }
        }
        
        // 析构 mananger 临时变量
        // 这里还有一步连带操作
        // 在其析构函数中 AssociationsManagerLock.unlock() 解锁
    }

    // release the old value (outside of the lock).
    // 开始时 retain 的是新入参的 value, 这里释放的是旧值,association 内部的 value 已经被替换了
    association.releaseHeldValue();
}

 _object_get_associative_reference 即是 objc_getAssociatedObject 函数的内部实现,是根据 key 读取源对象指定的 Associated Object。

id
_object_get_associative_reference(id object, const void *key)
{
    // 局部变量
    ObjcAssociation association{};

    {
        // 加锁
        AssociationsManager manager;
        // 取得全局唯一的 AssociationsHashMap
        AssociationsHashMap &associations(manager.get());
        
        // 从全局的 AssociationsHashMap 中取得对象对应的 ObjectAssociationMap
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            // 如果存在
            ObjectAssociationMap &refs = i->second;
            // 从 ObjectAssocationMap 中取得 key 对应的 ObjcAssociation 
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
                // 如果存在
                association = j->second;
                // 根据关联策略判断是否需要对 _value 执行 retain 操作
                association.retainReturnedValue();
            }
        }
        // 解锁
    }
    // 返回 _value 并根据关联策略判断是否需要放入自动释放池
    return association.autoreleaseReturnedValue();
}

 _object_remove_assocations 移除所有源对象的 Associated Objects。

// Unlike setting/getting an associated reference, 
// this function is performance sensitive because
// of raw isa objects (such as OS Objects) that can't
// track whether they have associated objects.

// 与 setting/getting 关联引用不同,此函数对性能敏感,
// 因为原始的 isa 对象(例如 OS 对象)无法跟踪它们是否具有关联的对象。
void
_object_remove_assocations(id object)
{
    // 对象对应的 ObjectAssociationMap
    ObjectAssociationMap refs{};

    {
        // 加锁
        AssociationsManager manager;
        // 取得全局的 AssociationsHashMap
        AssociationsHashMap &associations(manager.get());
        
        // 取得对象的对应 ObjectAssociationMap,里面包含所有的 (key, ObjcAssociation)
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            // 把 i->second 的内容都转入 refs 对象中
            refs.swap(i->second);
            // 从全局 AssociationsHashMap 移除对象的 ObjectAssociationMap
            associations.erase(i);
        }
        
        // 解锁
    }

    // release everything (outside of the lock).
    // 遍历对象的 ObjectAssociationMap 中的 (key, ObjcAssociation)
    // 对 ObjcAssociation 的 _value 根据 _policy 进行释放
    for (auto &i: refs) {
        i.second.releaseHeldValue();
    }
}

 一个延伸:

 在分类中到底能否实现属性?首先要知道属性是什么,属性的概念决定了这个问题的答案。

  • 如果把属性理解为通过方法访问的实例变量,那这个问题的答案就是不能,因为分类不能为类增加额外的实例变量。
  • 如果属性只是一个存取方法以及存储值的容器的集合,那么分类可以实现属性。

 分类中对属性的实现其实只是实现了一个看起来像属性的接口而已。

🎉🎉🎉 未完待续...