48. 成员变量修饰符的作用。
那么我们常用的 __strong
、__weak
、__unsafe_unretained
等等修饰成员变量的修饰符系统又是如何处理的呢?
当我们定义一个类的成员变量的时候,可以为其指定其修饰符 __strong
、__weak
、__unsafe_unretained
(未指定时默认为 __strong
),这使得成员变量可以像 strong
、weak
、unsafe_unretained
修饰符修饰的属性一样在 ARC
下进行正确的引用计数管理。定义如下 Person 类:
// Person.m 什么都不用实现
@interface LGPerson : NSObject {
NSObject *ivar_none; // 未明确指定修饰符的成员变量默认为 __strong 修饰
__strong NSObject *ivar_strong;
__weak NSObject *ivar_weak;
__unsafe_unretained NSObject *ivar_unsafe_unretained;
}
@end
// 编写如下代码,分别进行测试:
Person *person = [[Person alloc] init];
NSObject *temp = [[NSObject alloc] init];
NSLog(@"START");
person->ivar_none = temp;
// person->ivar_strong = temp;
// NSLog(@"TTT %@", person->ivar_strong);
// person->ivar_weak = temp;
// NSLog(@"read weak: %@", person->ivar_weak);
// person->ivar_unsafe_unretained = temp;
NSLog(@"END"); // ⬅️ 在这里打断点
在 END
行打断点,然后 xcode
菜单栏依次 Debug -> Debug Workflow -> Always Show Disassembly
勾选 Always Show Disassembly
,运行程序当断点执行时,我们的代码会被编译为汇编代码,依次看到 temp
为成员变量赋值时的指令跳转:
-
ivar_none
赋值时bl 0x10092e470; symbol stub for: objc_storeStrong
-
ivar_strong
赋值时bl 0x1009be470; symbol stub for: objc_storeStrong
-
ivar_weak
赋值时bl 0x1009be47c; symbol stub for: objc_storeWeak
,读取时调用了objc_loadWeakRetained
和objc_release
-
ivar_unsafe_unretained
赋值时没有发生任何指令跳转,只是单纯的根据地址存储值。
结果和我们前面面的不同修饰符修饰属性时测试的结果完全相同。分析上面属性的汇编代码时我们已知编译器在生成属性的 getter
setter
函数时会针对不同的属性修饰符做不同的处理来正确管理对象的引用计数,那么我们为不同的成员变量指定的修饰符信息又是保存在哪里?又是怎么起作用的呢?
struct class_ro_t
的 const uint8_t * ivarLayout
和 const uint8_t * weakIvarLayout
两个成员变量分别记录了那些成员变量是 strong
或是 weak
,都未记录的就是基本类型和 __unsafe_unretained
的对象类型。这两个值可以通过 runtime
提供的如下几个 API
来访问和修改:
OBJC_EXPORT const uint8_t * _Nullable class_getIvarLayout(Class _Nullable cls);
OBJC_EXPORT const uint8_t * _Nullable class_getWeakIvarLayout(Class _Nullable cls);
OBJC_EXPORT void class_setIvarLayout(Class _Nullable cls, const uint8_t * _Nullable layout);
OBJC_EXPORT void class_setWeakIvarLayout(Class _Nullable cls, const uint8_t * _Nullable layout);
ivarLayout
和 weakIvarLayout
类型是 uint8_t *
,一个 uint8_t
在 16
进制下是两位。
ivarLayout
是一系列的字符,每两个一组,比如 \xmn
,每一组 Ivar Layout
中第一位表示有 m
个非强引用成员变量,第二位表示接下来有 n
个强引用成员变量。
对于 ivarLayout
来说,每个 uint8_t
的高 4
位代表连续是非 storng
类型 Ivar
的数量(m
),m ∈ [0x0, 0xf]
,低 4
位代表连续是 strong
类型 Ivar
的数量(n
),n ∈ [0x0, 0xf]
。
对于 weakIvarLayout
来说,每个 uint8_t
的高 4
位代表连续是非 weak
类型 Ivar
的数量(m
),m ∈ [0x0, 0xf]
,低 4
位代表连续是 weak
类型 Ivar
的数量(n
),n ∈ [0x0, 0xf]
。
无论是 ivarLayout
还是 weakIvarLayout
,都在末尾填充 \x00
表示结尾。
对于 ivarLayout
来说,它只关心 strong
成员变量的数量,而记录前面有多少个非 strong
变量的数量无非是为了正确移动索引值而已。在最后一个 strong
变量后面的所有非 strong
变量,都会被自动忽略。weakIvarLayout
同理,apple
这么做的初衷是为了尽可能少的内存去描述类的每一个成员变量的内存修饰符。像上面的例子中 20
个成员变量,ivarLayout
用了 2 + 1 = 3
个字节 weakIvarLayout
用了 2 + 1 = 3
个字节,就描述了 20
个变量的内存修饰符。
49. objc_class 的函数列表,快速浏览即可。
class_rw_t *data() const;
data
函数是直接调用的 class_data_bits_t bits
的 data
函数,内部实现的话也很简单,通过掩码 #define FAST_DATA_MASK 0x00007ffffffffff8UL
(二进制第 3
-46
位是 1
,其他位都是 0
) 从 class_data_bits_t bits
的 uintptr_t bits
(uintptr_t bits
是 struct class_data_bits_t
仅有的一个成员变量)中取得 class_rw_t
指针。
void setData(class_rw_t *newData);
同样 setData
函数调用的也是 class_data_bits_t bits
的 setData
函数,在 struct class_data_bits_t
的 setData
函数中, uintptr_t bits
首先和 ~FAST_DATA_MASK
做与操作把掩码位置为 0
,其它位保持不变,然后再和 newData
做或操作,把 newData
中值为 1
的位也设置到 uintptr_t bits
中。
void setInfo(uint32_t set);
setInfo
函数调用的是 class_rw_t
的 setFlags
函数,通过原子的或操作把 set
中值为 1
的位也设置到 class_rw_t
的 uint32_t flags
中。(flags
和 set
都是 uint32_t
类型,都是 32
位。)
void clearInfo(uint32_t clear);
clearInfo
函数调用的是 class_rw_t
的 clearFlags
函数,通过原子的与操作把 ~clear
中值为 0
的位也设置到 class_rw_t
的 uint32_t flags
中。(flags
和 clear
都是 uint32_t
类型,都是 32
位。)
void changeInfo(uint32_t set, uint32_t clear);
changeInfo
函数调用的同样也是 class_rw_t
的 changeFlags
函数,先取得 class_rw_t
的 uint32_t flags
赋值到一个临时变量 uint32_t oldf
中,然后 oldf
和 uint32_t set
做一个或操作,把 set
值为 1
的位也都设置到 oldf
中,然后再和 ~clear
做与操作,把 ~clear
中是 0
的位也都设置其中,并把 !OSAtomicCompareAndSwap32Barrier(oldf, newf, (volatile int32_t *)&flags)
作为 do while
循环的循环条件,保证 uint32_t oldf
的值都能写入 uint32_t flags
中。
下面是一些掩码的使用。
FAST_HAS_DEFAULT_RR / RW_HAS_DEFAULT_RR
FAST_HAS_DEFAULT_RR
用以在 __LP64__
平台下判断 objc_class
的 class_data_bits_t bits
第二位的值是否为 1
,以此表示该类或者父类是否有如下函数的默认实现,对应在 非 __LP64
平台下,则是使用 RW_HAS_DEFAULT_RR
,且判断的位置发生了变化,RW_HAS_DEFAULT_RR
用以判断从 objc_class
的 class_data_bits_t bits
中取得 class_rw_t
指针指向的 class_rw_t
实例的 uint32_t flags
的第 14
位的值是否为 1
,以此表示该类或者父类是否有如下函数的默认实现:
retain/release/autorelease/retainCount
_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
- 当从 class_rw_t->flags 取值时,使用
RW_*
做前缀, - 当从 cache_t->_flags 取值时,使用
FAST_CACHE_*
做前缀 - 当从 class_data_bits_t->bits 取值时,使用
FAST_*
做前缀
FAST_*
和 FAST_CACHE_*
前缀开头的值,分别保存在 objc_class
的 class_data_bits_t bits
和 cache_t cache
两个成员变量中,直接减少了指针间接寻值,RW_*
前缀开头的值则首先要从 class_data_bits_t bits
中找到 class_rw_t
的指针,然后根据指针再去寻 class_rw_t
实例的值,然后再读取它的 flags
成员变量中的值。
hasCustomRR/setHasDefaultRR/setHasCustomRR
在__LP64__
平台和其它平台下的判断、设置、清除 objc_class
的默认 RR
函数的标记位。
FAST_CACHE_HAS_DEFAULT_AWZ / RW_HAS_DEFAULT_AWZ
FAST_CACHE_HAS_DEFAULT_AWZ
用以在 __LP64__
平台下判断 objc_class
的 cache_t cache
的 uint16_t _flags
二进制表示时第 14
位的值是否为 1
,以此表示该类或者父类是否有 alloc/allocWithZone
函数的默认实现。(注意,这里和上面的 RR
不同,RR
是一组实例方法保存在类中,而 alloc/allocWithZone
是一组类方法保存在元类中。)而在 非 __LP64__
平台下,则是使用 RW_HAS_DEFAULT_AWZ
,且判断的位置发生了变化,RW_HAS_DEFAULT_AWZ
用以判断从 objc_class
的 class_data_bits_t bits
中取得的 class_rw_t
指针指向的 class_rw_t
实例的 uint32_t flags
的第 16
位的值是否为 1
,以此表示该类或者父类是否有 alloc/allocWithZone
函数的默认实现。
hasCustomAWZ/setHasDefaultAWZ/setHasDefaultAWZ
在 __LP64__
平台和其它平台下判断、设置、清除 objc_class
的默认 AWZ
函数的标记位。
FAST_CACHE_HAS_DEFAULT_CORE / RW_HAS_DEFAULT_CORE
FAST_CACHE_HAS_DEFAULT_CORE
用以在 __LP64__
平台下判断 objc_class
的 cache_t cache
的 uint16_t _flags
二进制表示时第 15
位的值是否为 1
,以此表示该类或者父类是否有 new/self/class/respondsToSelector/isKindOfClass
函数的默认实现。而在 非 __LP64__
平台下,则是使用 RW_HAS_DEFAULT_CORE
,且判断的位置发生了变化,RW_HAS_DEFAULT_CORE
用以判断从 objc_class
的 class_data_bits_t bits
中取得 class_rw_t
指针指向的 class_rw_t
实例的 uint32_t flags
的第 13
位的值是否为 1
,以此表示该类或者父类是否有 new/self/class/respondsToSelector/isKindOfClass
函数的默认实现。
hasCustomCore/setHasDefaultCore/setHasCustomCore
在 __LP64__
平台和其它平台下判断、设置、清除 objc_class
的默认 Core
函数的标记位。
FAST_CACHE_HAS_CXX_CTOR / RW_HAS_CXX_CTOR / FAST_CACHE_HAS_CXX_DTOR / RW_HAS_CXX_DTOR
FAST_CACHE_HAS_CXX_CTOR
用以在 __LP64__
平台下判断 objc_class
的 cache_t cache
的 uint16_t _flags
二进制表示时第 1
位的值是否为 1
,以此表示该类或者父类是否有 .cxx_construct
函数实现。而在 非 __LP64__
平台下,则是使用 RW_HAS_CXX_CTOR
,且判断的位置发生了变化,RW_HAS_CXX_CTOR
用以判断从 objc_class
的 class_data_bits_t bits
中取得 class_rw_t
指针指向的 class_rw_t
实例的 uint32_t flags
的第 18
位的值是否为 1
,以此表示该类或者父类是否有 .cxx_construct
函数实现。对应的 FAST_CACHE_HAS_CXX_DTOR
和 RW_HAS_CXX_DTOR
表示该类或者父类是否有 .cxx_destruct
函数实现。
这里需要注意的是在 __LP64__ && __arm64__
平台下 FAST_CACHE_HAS_CXX_DTOR
是 1<<0
,而在 __LP64__ && !__arm64__
平台下 FAST_CACHE_HAS_CXX_DTOR
是 1<<2
。
hasCxxCtor/setHasCxxCtor/hasCxxDtor/setHasCxxDtor
在 __LP64__
平台和其它平台下判断、设置(注意这里没有清除)objc_class
的 .cxx_construct/.cxx_destruct
函数实现的标记位。
FAST_CACHE_REQUIRES_RAW_ISA / RW_REQUIRES_RAW_ISA
FAST_CACHE_REQUIRES_RAW_ISA
用以在 __LP64__
平台下判断 objc_class
的 cache_t cache
的 uint16_t _flags
二进制表示时第 13
位的值是否为 1
,以此表示类实例对象(此处是指类对象,不是使用类构建的实例对象,一定要记得)是否需要原始的 isa
。而在 非 __LP64__
且 SUPPORT_NONPOINTER_ISA
的平台下,则是使用 RW_REQUIRES_RAW_ISA
,且判断的位置发生了变化,RW_REQUIRES_RAW_ISA
用以判断从 objc_class
的 class_data_bits_t bits
中取得 class_rw_t
指针指向的 class_rw_t
实例的 uint32_t flags
的第 15
位的值是否为 1
,以此表示类实例对象(此处是指类对象,不是使用类构建的实例对象,一定要记得)是否需要原始的 isa
。
instancesRequireRawIsa/setInstancesRequireRawIsa
在 __LP64__
平台和其它平台下判断、设置类实例(此处是指类对象,不是使用类构建的实例对象,一定要记得)需要原始 isa
的标记位。
- 当
! __LP64__
平台下,所有掩码都存储在class_rw_t
的uint32_t flags
。 __LP64__
平台下,FAST_HAS_DEFAULT_RR
存储在class_data_bits_t bits
的uintptr_t bits
。(1UL<<2
)(retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
)__LP64__
平台下,FAST_CACHE_HAS_DEFAULT_AWZ
存储在cache_t cache
的uint16_t _flags
下。(1<<14
)(alloc/allocWithZone:
)__LP64__
平台下,FAST_CACHE_HAS_DEFAULT_CORE
存储在cache_t cache
的uint16_t _flags
下。(1<<15
)(new/self/class/respondsToSelector/isKindOfClass
)__LP64__
平台下,FAST_CACHE_HAS_CXX_CTOR
存储在cache_t cache
的uint16_t _flags
下。(1<<1
)(.cxx_construct
)__LP64__
平台下,FAST_CACHE_HAS_CXX_DTOR
存储在cache_t cache
的uint16_t _flags
下。(1<<2
/1<<0
)(.cxx_destruct
)__LP64__
平台下,FAST_CACHE_REQUIRES_RAW_ISA
存储在cache_t cache
的uint16_t _flags
下。(1<<13
)(requires raw isa
)
我们如何才能获取一个类的所有子类呢 ?这里正式使用到了 struct class_rw_t
的两个成员变量 Class firstSubclass
和 Class nextSiblingClass
。
bool canAllocNonpointer();
表示 objc_class
的 isa
是非指针,即类对象不需要原始 isa
时,能根据该函数返回值设置 isa_t isa
的 uintptr_t nonpointer : 1
字段,标记该类的 isa
是非指针。
下面的一些掩码相关的操作我们开始用到 struct class_ro_t
的 uint32_t flags
了!它们的宏定义名都是以 RO_
开头的。
bool hasAutomaticIvars();
从 class_data_bits_t bits
中取出 class_rw_t
指针,然后从 struct class_rw_t
中取出 explicit_atomic<uintptr_t> ro_or_rw_ext
对应的 class_rw_ext_t
指针,然后从 struct class_rw_ext_t
中取出 const class_ro_t *ro
,然后取出 uint32_t flags
和 (RO_IS_ARC | RO_HAS_WEAK_WITHOUT_ARC
(二进制表示第 7
位和第 9
位是 1
,其它位都是 0
))做与操作。
如果类的 ivars 由 ARC 管理,或者该类是 MRC 但具有 ARC 样式的 weak ivars,则返回 YES。(weak 修饰符是可以在 MRC 中使用的,weak 是和 ARC 一起推出的,根据之前 weak 的实现原理也可知它的实现流程和 ARC 或者 MRC 是完全没有关系的。)
bool isARC();
同上,最后取出 class_ro_t
的 uint32_t flags
和 RO_IS_ARC
做与操作。如果类的 ivar 由 ARC 管理,则返回 YES。
RW_FORBIDS_ASSOCIATED_OBJECTS 禁止类的实例对象进行关联对象的掩码,看到它前缀是 RW
开始的,表示它用在 struct class_rw_t
的 uint32_t flags
中。
bool forbidsAssociatedObjects();
禁止该类的实例对象进行 Associated Object。从 class_data_bits_t bits
中取出 class_rw_t
指针,然后从 struct class_rw_t
中取出 uint32_t flags
和 RW_FORBIDS_ASSOCIATED_OBJECTS
(第 20
位值为 1
)与操作的结果。
instancesHaveAssociatedObjects / setInstancesHaveAssociatedObjects
在 struct class_rw_t
的 uint32_t flags
做掩码操作。
RW_INITIALIZING 判断 objc_class
是否正在进行初始化的掩码。判断位置在 struct class_rw_t
的 uint32_t flags
中。
bool isInitializing();
RW_INITIALIZING
是 RW
前缀开头,可直接联想到其判断位置在 struct class_rw_t
的 uint32_t flags
中,与前面的一些判断相比这里 objc_class
的位置发生了变化,前面我们所有的判断都是在当前的 objc_class
中进行的,而此处的判断要转移到当前 objc_class
的元类中,元类的类型也是 struct objc_class
,所以它们同样也有 class_data_bits_t bits
、cache_t cache
等成员变量,这里 isInitializing
函数使用的正是元类的 class_data_bits_t bits
成员变量。getMeta
函数是取得当前 objc_class
的元类,然后 data
函数从元类的 class_data_bits_t bits
中取得 class_rw_t
指针,然后取得 struct class_rw_t
的 uint32_t flags
和 RW_INITIALIZING
做与操作,取得 flags
二进制表示的第 28
位的值作为结果返回。
void setInitialized();
标记该类初始化完成。
IMP getLoadMethod();
获取一个类的 +load
函数且仅从 mlist = ISA()->data()->ro()->baseMethods();
中获取,如果找不到的话返回 nil,会忽略分类中的 +load
函数。
首先我们要对 +load
函数和别的函数做出一些理解上的区别,首先我们在任何时候都不应该自己主动去调用 +load
函数,它是由系统自动调用的,且它被系统调用时是直接通过它的函数地址调用的,它是不走 objc_msgSend
消息发送流程的。当我们在自己的类定义中添加了 +load
函数,编译过程中编译器会把它存储在元类的 struct class_ro_t
的 method_list_t * baseMethodList
成员变量中。那么 category
中的 +load
函数在编译过程中会被放在哪里呢?
FAST_CACHE_META / RW_META / RO_META 在 __LP64__
平台下标识 objc_class
是否是元类的值在 cache_t cache
中,其它情况则是在 struct class_rw_t
的 uint32_t flags
中(需要根据指针进行寻址)。
bool isMetaClass();
如果 FAST_CACHE_META
存在,则从 cache_t cache
的 uint16_t _flags
二进制表示的第 2/0
位判断当前 objc_class
是否是元类。其它情况则从 class_data_bits_t bits
中取得 class_rw_t
指针指向的 class_rw_t
实例的 uint32_t flags
二进制表示的第 0
位进行判断。
Class getMeta();
取得当前类的元类。如果当前类是元类,则直接返回 this,如果不是元类,则调用 objc_object::ISA() 函数取得元类。(从 isa 中根据掩码取出类的地址:(Class)(isa.bits & ISA_MASK))
bool isRootClass();
判断一个类是否是根类,只是判断一个类的 superclass
是否为 nil
即可。
- 根类的父类是
nil
,根类的元类是根元类。 - 根元类的父类是根类,根元类的元类是它自己。
bool isRootMetaclass();
判断是否是根元类,判断依据是根元类的元类是它自己,即如果一个 objc_class 的元类是自己的话,那么它是根元类。(ISA() == (Class)this;
)
50. method_t / ivar_t / property_t
method_t 是方法的数据结构,包括方法名、方法类型(参数和返回值)、方法实现(IMP)。
struct method_t {
SEL name; // 方法名、选择子
const char *types; // 方法类型
// using MethodListIMP = IMP;
MethodListIMP imp; // 方法实现
// 根据选择子的地址进行排序
struct SortBySELAddress :
public std::binary_function<const method_t&, const method_t&, bool>
{
bool operator() (const method_t& lhs, const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};
struct ivar_t {
// 首先这里要和结构体中成员变量的偏移距离做出理解上的区别。
// offset 它是一个指针,那它指向谁呢,它指向一个全局变量,
// 编译器为每个类的每个成员变量都分配了一个全局变量,用于存储该成员变量的偏移值。
// 如果我们仅有成员变量的名字,能想到的成员变量的读取可能是这样的:
// 当我们读取成员变量时从实例对象的 isa 找到类,然后 data() 找到 class_rw_t
// 然后再找到 class_ro_t 然后再找到 ivar_list_t,
// 然后再比较 ivar_t 的 name 和我们要访问的成员变量的名字是否相同,然后再读出 int 类型的 offset,
// 再进行 self + offset 指针偏移找到这个成员变量。
// 现在当我们访问一个成员变量时,只需要 self + *offset 就可以了。
// 下面会验证 offset 指针。
int32_t *offset;
// 成员变量名称(如果类中有属性的话,编译器会自动生成 _属性名 的成员变量)
const char *name;
// 成员变量类型
const char *type;
// alignment is sometimes -1; use alignment() instead
// 对齐有时为 -1,使用 alignment() 代替。
// 原始对齐值是 2^alignment_raw 的值
// alignment_raw 应该叫做对齐值的指数
uint32_t alignment_raw;
// 成员变量的类型的大小
uint32_t size;
// #ifdef __LP64__
// # define WORD_SHIFT 3UL
// #else
// # define WORD_SHIFT 2UL
// #endif
uint32_t alignment() const {
// 应该没有类型的 alignment_raw 会是 uint32_t 类型的最大值吧!
// WORD_SHIFT 在 __LP64__ 下是 3,表示 8 字节对齐,
// 在非 __LP64__ 下是 2^2 = 4 字节对齐。
if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
// 2^alignment_raw
return 1 << alignment_raw;
}
};
我们首先定义一个 Person
类:
// 定义一个 LGPerson 类
// Person.h 如下,.m 为空即可
@interface Person : NSObject
@property (nonatomic, strong) NSMutableArray *arr;
@end
然后在终端执行 clang -rewrite-objc Person.m
生成 Person.cpp
。
摘录 Person.cpp
:
ivar_list_t
如下,arr
为仅有的成员变量,它对应的 ivar_t
初始化部分 int32_t *offset
值使用了 (unsigned long int *)&OBJC_IVAR_$_LGPerson$_arr
。
static struct /*_ivar_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count;
struct _ivar_t ivar_list[1];
} _OBJC_$_INSTANCE_VARIABLES_LGPerson __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_ivar_t),
1,
{{(unsigned long int *)&OBJC_IVAR_$_LGPerson$_arr, "_arr", "@\"NSMutableArray\"", 3, 8}}
};
在 LGPerson.cpp
文件中全局搜索 OBJC_IVAR_$_LGPerson$_arr
有如下结果:
// 全局变量声明和定义赋值
extern "C" unsigned long OBJC_IVAR_$_LGPerson$_arr;
extern "C" unsigned long int
OBJC_IVAR_$_LGPerson$_arr
__attribute__ ((used, section ("__DATA,__objc_ivar"))) =
__OFFSETOFIVAR__(struct LGPerson, _arr);
// arr 的 setter getter 函数,看到都是直接使用了 OBJC_IVAR_$_LGPerson$_arr
static NSMutableArray * _Nonnull _I_LGPerson_arr(LGPerson * self, SEL _cmd) {
return (*(NSMutableArray * _Nonnull *)((char *)self + OBJC_IVAR_$_LGPerson$_arr));
}
static void _I_LGPerson_setArr_(LGPerson * self, SEL _cmd, NSMutableArray * _Nonnull arr) {
(*(NSMutableArray * _Nonnull *)((char *)self + OBJC_IVAR_$_LGPerson$_arr)) = arr;
}
看到 property_t
极其简单,编译器会自动生成一个对应的 _属性名的成员变量保存在 ivars
中。
struct property_t {
// 属性名字
const char *name;
// 属性的 attributes,例如:
// @property (nonatomic, strong) NSObject *object;
// object 的 attributes:"T@\"NSObject\",&,N,V_object"
//
// @property (nonatomic, copy) NSArray *array2;
// array2 的 attributes:"T@\"NSArray\",C,N,V_array2"
const char *attributes;
};
51. dealloc 函数执行。
dealloc
函数的调用是在 rootRelease
函数的最后通过 ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc))
来执行的,正常情况下我们不能主动调用 dealloc
函数。dealloc
函数内部调用 rootDealloc
,其内部有一个这样的判断:if (fastpath(isa.nonpointer && !isa.weakly_referenced && !isa.has_assoc && !isa.has_cxx_dtor && !isa.has_sidetable_rc)) {...}
如下条件全部为真的话,可以直接调用 free
进行快速内存释放:
- 对象的
isa
是优化的isa
。 - 对象不存在弱引用。
- 对象没有关联对象。
- 对象没有自定义的
C++
的析构函数。 - 对象的引用计数没有保存在
SideTable
中。
52. AutoreleasePool 自动释放池。
一个线程的自动释放池就是一个存放指针的栈(自动释放池整体结构是由 AutoreleasePoolPage 组成的双向链表,而每个 AutoreleasePoolPage 里面则有一个存放对象指针的栈)。栈里面的每个指针要么是等待 autorelease 的对象,要么是 POOL_BOUNDARY 自动释放池边界(实际为 #define POOL_BOUNDARY nil,同时也是 next
的指向)。一个 pool token 是指向 POOL_BOUNDARY 的指针。When
the pool is popped, every object hotter than the sentinel is released. 当自动释放池执行 popped,every object hotter than the sentinel is released.。(这句没有看懂)这些栈分散位于由 AutoreleasePoolPage 构成的双向链表中。AutoreleasePoolPage 会根据需要进行添加和删除。hotPage 保存在当前线程中,当有新的 autorelease 对象添加进自动释放池时会被添加到 hotPage,hotPage 即为当前 AutoreleasePoolPage 组成的双向链表中,有空位的一个节点。
根据 AutoreleasePoolPageData
的构造函数可知,第一个节点的 parent
和 child
都是 nil
,当第一个 AutoreleasePoolPage
满了,会再创建一个 AutoreleasePoolPage
,此时会拿第一个节点作为 newParent
参数来构建这第二个节点,即第一个节点的 child
指向第二个节点,第二个节点的 parent
指向第一个节点。
begin
函数超关键的,首先要清楚一点 begin
是 AutoreleasePoolPage
中存放的 自动释放对象 的起点。回顾上面的的 new
函数的实现我们已知系统总共给 AutoreleasePoolPage
分配了 4096
个字节的空间,这么大的空间除了前面一部分空间用来保存 AutoreleasePoolPage
的成员变量外,剩余的空间都是用来存放自动释放对象地址的。
AutoreleasePoolPage
的成员变量都是继承自 AutoreleasePoolPageDate
,它们总共需要 56
个字节的空间,然后剩余 4040
字节空间,一个对象指针占 8
个字节,那么一个 AutoreleasePoolPage
能存放 505
个需要自动释放的对象。(可在 main.m
中引入 #include "NSObject-internal.h"
打印 sizeof(AutoreleasePoolPageData)
的值确实是 56
。)
id * begin() {
// (uint8_t *)this 是 AutoreleasePoolPage 的起始地址,
// 且这里用的是 (uint8_t *) 的强制类型转换,uint8_t 占 1 个字节,
// 然后保证 (uint8_t *)this 加 56 时是按 56 个字节前进的
// sizeof(*this) 是 AutoreleasePoolPage 所有成员变量的宽度是 56 个字节,
// 返回从 page 的起始地址开始前进 56 个字节后的内存地址。
return (id *) ((uint8_t *)this+sizeof(*this));
}
next
指针通常指向的是当前自动释放池内最后面一个自动释放对象的后面,如果此时 next
指向 begin
的位置,表示目前自动释放池内没有存放自动释放对象。
"冷" page
,首先找到 hotPage
然后沿着它的 parent
走,直到最后 parent
为 nil
,最后一个 AutoreleasePoolPage
就是 coldPage
,返回它。这里看出来其实 coldPage
就是双向 page
链表的第一个 page
。
autoreleaseFast
把对象快速放进自动释放池。
static inline id *autoreleaseFast(id obj)
{
// hotPage
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
// 如果 page 存在并且 page 未满,则直接调用 add 函数把 obj 添加到 page
return page->add(obj);
} else if (page) {
// 如果 page 满了,则调用 autoreleaseFullPage 构建新 AutoreleasePoolPage,并把 obj 添加进去
return autoreleaseFullPage(obj, page);
} else {
// 连 hotPage 都不存在,可能就一 EMPTY_POOL_PLACEHOLDER 在线程的存储空间内保存
// 如果 page 不存在,即当前线程还不存在自动释放池,构建新 AutoreleasePoolPage,并把 obj 添加进去
return autoreleaseNoPage(obj);
}
}
53. retain 函数。
// Equivalent to calling [this retain], with shortcuts if there is no override
// 等效于调用 [this retain],如果没有重载此函数,则能快捷执行(快速执行)
inline id
objc_object::retain()
{
// Tagged Pointer 不参与引用计数管理,它的内存在栈区,由系统自行管理
ASSERT(!isTaggedPointer());
// 如果没有重载 retain/release 函数,则调用根类的 rootRetain() 函数
//(hasCustomRR() 函数定义在 objc_class 中,等下面分析 objc_class 时再对其进行详细分析)
if (fastpath(!ISA()->hasCustomRR())) {
return rootRetain();
}
// 如果重载了 retain 函数,则以 objc_msgSend 调用重载的 retain 函数
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
}
Base retain 函数实现,忽略重载。这不检查 isa.fast_rr; 如果存在 RR 重载,则它已经被调用,并选择调用 [super retain]。 当 tryRetain=true 会执行 -_tryRetain 路径,会执行一个(return sidetable_tryRetain() ? (id)this : nil;) 当 handleOverflow=false 时是 frameless 的快速路径。 当 handleOverflow=true 时是一个 framed 的慢速路径会把溢出的引用计数转移到 SideTable 的 refcnts 中。 当 SIDE_TABLE_DEALLOCATING 被标记时,会返回 nil,其它情况都返回 (id)this 以这种方式构造代码以防止重复。
ALWAYS_INLINE id
objc_object::rootRetain()
{
// tryRetain 和 handleOverflow 都传入的 false,不执行 -_tryRetain path.
// handleOverflow=false 处理 extra_rc++ 溢出的情况
return rootRetain(false, false);
}
tryRetain
参数如其名,尝试持有,它涉及到的只有一个 return sidetable_tryRetain() ? (id)this : nil;
操作,只有当对象处于正在销毁状态时,才会返回 false
。当对象的 isa
是原始指针时,对象的引用计数全部保存在 SideTable
的 refcnts
里面的。sidetable_tryRetain
函数只会在对象的 isa
是原始指针时才会被调用。
handleOverflow
参数是处理 newisa.extra_rc++ overflowed
情况的,当溢出情况发生后,如果 handleOverflow
传入的是 false
时,则会调用 return rootRetain_overflow(tryRetain)
,它只有一件事情,就是把 handleOverflow
传递为 true
再次调用 rootRetain
函数。所以无论如何,当引用计数溢出时都是必会进行处理的。
当对象的 isa
是优化的 isa
且 extra_rc
溢出时,会把一部分引用计数转移到 RefcountMap
中,只是转移一部分,并不是 extra_rc
溢出以后,对象的引用计数全部交给 RefcountMap
管理了,每次溢出后会把 extra_rc
置为 RC_HALF
,然后下次增加引用计数增加的还是 extra_rc
,直到再次溢出再转移到 RefcountMap
中。大概是因为操作 extra_rc
的消耗要远低于操作 RefcountMap
。
循环结束的条件是: !StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)
,这句代码意思是,&isa.bits
和 oldisa.bits
进行原子比较字节逐位相等的话,则把 newisa.bits
复制(同 std::memcpy
函数)到 &isa.bits
中,并且返回 true
。如果 &isa.bits
和 oldisa.bits
不相等的话,则把 &isa.bits
中的内容加载到 oldisa.bits
中。所以这里的循环的真实目的就是为了把: newisa.bits
复制到 &isa.bits
中,保证对象 bits
中的数据能正确的进行修改。
- 当对象的
isa
是非优化的isa
时,对象的引用计数全部保存在SideTable
中,当要增加引用计数时就调用sidetable_tryRetain/sidetable_retain
增加SideTable
中的引用计数。 - 当对象的
isa
是优化的isa
且对象的引用计数保存在extra_rc
字段中且加 1 后未溢出时,此时也是比较清晰的,执行完加 1 后,函数也直接return (id)this
结束了。 - 只有第三种情况比较特殊,当对象的
isa
是优化的isa
且对象的引用计数保存在extra_rc
中,此时extra_rc++
后发生溢出,此时会把extra_rc
赋值为RC_HALF
,把has_sidetable_rc
赋值为true
,然后调用sidetable_addExtraRC_nolock(RC_HALF)
。其实疑问就发生在这里,如果对象的extra_rc
中的引用计数已经溢出过了,并转移到了SideTable
中一部分,此时extra_rc
是被置为了RC_HALF
,那下次增加对象的引用计数时,并不是直接去SideTable
中增加引用计数,其实是增加extra_rc
中的值,直到增加到再次溢出时才会跑到SideTable
中增加引用计数。这里还挺迷惑的,觉的最好的解释应该是尽量在extra_rc
字段中增加引用计数,少去操作SideTable
,毕竟操作SideTable
还要加锁解锁,还要哈希查找等,整体消耗肯定是大于直接操作extra_rc
字段的。
我们首先要清楚一件很重要的事情,当对象的 isa
是原始类指针时,在 SideTable
的 RefcountMap refcnts
中取出 objc_object
对应的 size_t
的值并不是单纯的对象的引用计数这一个数字,它是明确有一些标志位存在的,且有些标志位所代表的含义与 isa
是非指针的 objc_object
的 isa_t isa
中的一些位是相同的。所以这里我们不能形成定式思维,觉的这些标志位只存在于 isa_t isa
中。
如下列举当对象的 isa
是原始指针时,一些标志位所代表的含义:
SIDE_TABLE_WEAKLY_REFERENCED
是size_t
的第 0 位,表示该对象是否有弱引用。(此时是针对isa
是原始指针的对象,对应于isa
是非指针时,x86_64
下isa_t isa
的uintptr_t weakly_referenced : 1;
字段)SIDE_TABLE_DEALLOCATING
是size_t
的第 1 位,表示对象是否正在进行释放。(同上,对应于x86_64
下isa_t isa
的uintptr_t deallocating : 1;
字段)SIDE_TABLE_RC_ONE
是size_t
的第 2 位,这时才正式开始表示该对象的引用计数。(当对象的isa
是非指针时,引用计数也是从第 2 位开始的,但是它的前两位不做任何标记位使用,只是单纯的被舍弃了。)SIDE_TABLE_RC_PINNED
在__LP64__
64
位系统架构下是size_t
的第 63 位,32
位系统架构下是第 31 位,也就是size_t
的最后一位,表示在SideTable
中的引用计数溢出。(大概是不会存在一个对象的引用计数大到连SideTable
都存不下的吧)SIDE_TABLE_RC_SHIFT
帮助我们从size_t
中拿出真实引用计数用的,即从第 2 位开始,后面的数值都表示对象的引用计数了。SIDE_TABLE_FLAG_MASK
是SIDE_TABLE_RC_ONE
的值减 1,它的二进制表示是0b11
后两位是1
,其它位都是0
,做掩码使用。
54. rootRetainCount 计算引用计数的值。
如果对象的 isa
是非指针的话,引用计数同时在 extra_rc
字段和 SideTable
中保存,要求它们的和。如果对象的 isa
是原始 isa
的话,对象的引用计数数据只保存在 SideTable
中。
inline uintptr_t
objc_object::rootRetainCount()
{
// 如果是 Tagged Pointer 的话,获取它的引用计数则直接返回 (uintptr_t)this
if (isTaggedPointer()) return (uintptr_t)this;
// 加锁
sidetable_lock();
// 以原子方式加载 &isa.bits 数据
isa_t bits = LoadExclusive(&isa.bits);
// 如果是 __arm64__ && !__arm64e__ 平台下,要清除独占标记
ClearExclusive(&isa.bits);
if (bits.nonpointer) {
// 如果对象的 isa 是非指针的话,引用计数同时在 extra_rc 字段和 SideTable 中保存,要求它们的和
// 这里加 1, 是因为 extra_rc 存储的是对象本身之外的引用计数的数量
uintptr_t rc = 1 + bits.extra_rc;
// 如果 has_sidetable_rc 位为 1,则表示在 SideTable 中也保存有对象的引用计数数据
if (bits.has_sidetable_rc) {
// 找到对象的在 SideTable 中的引用计数并增加到 rc 中
rc += sidetable_getExtraRC_nolock();
}
// 解锁
sidetable_unlock();
// 返回 rc
return rc;
}
sidetable_unlock();
// 如果对象的 isa 是原始 isa 的话,对象的引用计数数据只保存在 SideTable 中
return sidetable_retainCount();
}