OC底层 - 对象探索
前言
一、探索问题的方法
二、对象的创建流程
三、alloc的流程
四、init 和 new
五、“为什么MyTextObjc(NSObject的子类),callAlloc流程会走两遍“ 和 “为什么MyTextObjc和NSObject的alloc流程不一样。”
六. 通过地址找到内存
前言
作为一个经历了漫长业务开发的老程序员,今天回归本质,对“对象”进行深入的探索。那么在探索之前,我们需要带有一定的疑问和好奇心。
首先抛出3个日常开发过程中很少去思考的问题:
1、我们要探索对象,那么应该通过什么样子的方式去探索?
2、对象创建的流程是什么?
3、什么是对象,对象的本质是什么?
一、探索问题的方法
1、 设置符号断点

如上图,根据启动流程设置3个符号断点,然后通过汇编分析底层逻辑。
2、根据苹果开源的部分源码来分析
改为最新的objc4 - 818.2源码编译运行这篇博客来进行源码探索。
这里无论探索什么,如果苹果已经开源出部分的源码,那么我更喜欢从源码来分析底层,这样可以更直接的了解底层逻辑。所以下面的探索,我会通过下符号断点结合源码来进行探索。
二、对象的创建流程
MyTextObjc *objc1 = [[MyTextObjc alloc] init];
上面的代码,就是创建一个对象objc1。
我们分析objc1是怎么创建出来的过程,其实就是需要探索alloc 和 init 在底层都做了什么事情。
首先我把代码改成这样:
MyTextObjc *objc1 = [MyTextObjc alloc];
MyTextObjc *objc2 = [objc1 init];
MyTextObjc *objc3 = [objc1 init];
NSLog(@"%@ - %p - %p",objc1,objc1,&objc1);
NSLog(@"%@ - %p - %p",objc2,objc2,&objc2);
NSLog(@"%@ - %p - %p",objc3,objc3,&objc3);
然后我打印objc1和objc2的内存地址和地址指针,打印结果如下:
<MyTextObjc: 0x600000f940d0> - 0x600000f940d0 - 0x7ffee74761d8
<MyTextObjc: 0x600000f940d0> - 0x600000f940d0 - 0x7ffee74761d0
<MyTextObjc: 0x600000f940d0> - 0x600000f940d0 - 0x7ffee74761c8
通过打印结果可以发现,objc1、objc2、objc3的内存地址是一样的,objc2和objc3并没有因为objc1的init方法而改变内存地址。而objc1、objc2、objc3的地址指针是不同的,但是它们都是指向同一个内存(0x600000f940d0)。
另外可以发现,objc1、objc2、objc3的地址指针是连续的,并且间隔8个字节,而一个指针就是8个字节,这里其实也验证了栈内存是连续的。
得出结论,objc1 的创建是执行了alloc之后,创建出来的。所以我们就可以把开头提出的“问题2:对象创建的流程是什么?”转化为“alloc的流程是什么 或者 alloc是怎么开辟内存空间的”。
三、alloc的流程
通过符号断点以及源码,我总结了[MyTextObjc alloc]的执行流程(这个流程是818.2源码的):

另外我又跟了一下[NSObject alloc]的执行流程:

看到上面的流程图,我个人认为对于源码中的其他各种判断不用做过多的研究。
第一是因为每一个objc源码版本都会有一些小改动,我们去死记每一步的判断流程没有什么必要性。
第二是我们只关心以下2个问题,就可以达到我们探索的目的了:
1、_class_createInstancceFormZone 方法中,都做什么事情。
2、为什么MyTextObjc(NSObject的子类),callAlloc流程会走两遍,和NSObject的alloc流程不一样
首先我们先来分析一下_class_createInstancceFormZone这个核心方法。
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
// 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;
//需要开辟多少内存 (在realizeClassWithoutSwift中,会调用:
// Set fastInstanceSize if it wasn't set already.
// cls->setInstanceSize(ro->instanceSize);)
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
//obj为id类型,而id类型的底层就是 typedef struct objc_object *id;
// 是objc_object结构体指针。
//所以id类型,可以指向任何一个继承了Object(或者NSObject)类的对象。
id obj;
if (zone) {
//可以忽略,大概ios8的时候就废弃了
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
//申请内存
//calloc() 在内存中动态地分配 num 个长度为 size 的连续空间,
//并将每一个字节都初始化为 0。
//所以它的结果是分配了 num*size 个字节长度的内存空间,
//并且每个字节的值都是0
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 {
///系统级别的类,这里就不做深入研究了。
obj->initIsa(cls);
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
在这个方法中,有3个方法是最核心的方法:instanceSize、calloc、initInstanceIsa
1 - 1、instanceSize (需要开辟多少内存)
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;
}
bool hasFastInstanceSize(size_t extra) const
{
if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
}
return _flags & FAST_CACHE_ALLOC_MASK;
}
size_t fastInstanceSize(size_t extra) const
{
ASSERT(hasFastInstanceSize(extra));
if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
} else {
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
// remove the FAST_CACHE_ALLOC_DELTA16 that was added
// by setFastInstanceSize
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
void setFastInstanceSize(size_t newSize)
{
// Set during realization or construction only. No locking needed.
uint16_t newBits = _flags & ~FAST_CACHE_ALLOC_MASK;
uint16_t sizeBits;
// Adding FAST_CACHE_ALLOC_DELTA16 allows for FAST_CACHE_ALLOC_MASK16
// to yield the proper 16byte aligned allocation size with a single mask
sizeBits = word_align(newSize) + FAST_CACHE_ALLOC_DELTA16;
sizeBits &= FAST_CACHE_ALLOC_MASK;
if (newSize <= sizeBits) {
newBits |= sizeBits;
}
_flags = newBits;
}
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
根据源码先来解释几个函数的意义:
(1) 代码中 fastpath 和 slowpath 对应的是2个宏定义
#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
这2个宏的意义在于:
通过这种方式,编译器在编译过程中,会将可能性更大的代码紧跟着前面的代码,从而减少指令跳转带来的性能上的下降。
所以我们一般只跟fastpath下的代码,其他的判断我们理解一下就ok了。
(2) __builtin_constant_p(extra)
这函数代表:判断extra是否在编译时就可以确定其为常量。
如果extra为常量,该函数返回1,否则返回0。
如果extra为常量,可以在代码中做一些优化来减少处理extra的复杂度。
这个方法进入看了一下,已经废弃了,另外extra从外层传进来也并不是常量,所以我们可以跳过这个判断。
(3) align16这个函数中,是一个字节对齐的算法。
这个算法的核心就是:15 转二进制 为 0000 1111,那么对15取反,就是将后4位抹0,那么(x + size_t(15)) & ~size_t(15) 即都为16的倍数了。这里我解释的可能不太清楚,有位运算的基础,这个算法就很好理解。
那么这里就是16字节对齐,在之前的objc版本这里走的是8字节对齐。
16字节对齐的原因:首先是访问内存的时候,可以更快的读取。另外就是防止内存读取出错,不会产生混乱,更加安全。
(4) hasFastInstanceSize、fastInstanceSize、setFastInstanceSize 通过cache缓存来获取到size
1 - 2、成员变量和属性的区别
简单的来说:
成员变量就是没有get、set方法,无法通过.语法来获取和设置值。同时可以对成员变量设置访问权限: @public 、 @protected、@private ,也就是说oc和java一样,其实也有私有变量和共有变量之说。
属性是通过@property在底层自动生成了带下划线的成员变量(这个成员变量的访问权限是@private) 以及 get,set方法。
那么还有什么好玩的区别呢?
看下面的代码
@interface MyTextObjc : NSObject {
@public NSString *magicName;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@end
我在MyTextObjc中同时设置了成员变量和属性。然后我在外层,对其magicName、name、nickName都设置值。
此时我用x/4gx 去打印对象的内存布局情况,发现magicName一定是排在name、nickName之前的。无论我@public NSString *magicName;是设置在interface中,还是设置在内部。都是如此。
所以证明一点,在对象的内存布局中,属性生成的成员变量一定排在自己设置的成员变量之后。
1 - 3、内存对齐
通过对instanceSize分析,得出一个结论就是对象最少要分配16字节的空间,也就是16字节对齐。
通过对对象的分析,可以看到:
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
也可以通过 clang命令。
Clang 是一个由Apple主导编写,基于LLVM的c/c++/Objective-C编译器。
clang -rewrite-objc main.m -o main.cpp,生成c++文件。
或者使用 xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
看其中MyTextObjc是什么。
#ifndef _REWRITER_typedef_MyTextObjc
#define _REWRITER_typedef_MyTextObjc
typedef struct objc_object MyTextObjc;
typedef struct {} _objc_exc_MyTextObjc;
#endif
struct MyTextObjc_IMPL {
struct NSObject_IMPL NSObject_IVARS; //c中的伪继承
};
可以看到,MyTextObjc 的实现,就是一个结构体。NSObject的底层就是objc_object。
对象的底层会编译成结构体,也就是对象的本质。
所以内存对齐,我们要通过结构体来进行分析。
首先结构体的内存对齐规则是:
(1) 结构体或者联合体的数据成员,第一个数据成员放在offset为0的位置,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小的整数倍开始。
例如:
struct S1 {
double a; //8 (0 7)
int b; //4 (8 4) 9 10 11
char c; //1 (12 1) 12
short d; //2 (13 2) 14 15
char e; //1 (16 1) 16
}s1;
其中a存放在0-7的位置,那么第8位是b大小的整数倍,所以b存放在第8-11位。
然后第12位是c大小的整数倍,所以c存在第12位。
继续第13位不是d大小的整数倍,向后排,第14位是d的整数倍,所以d存放在14-15位
最后第16位是e大小的整数倍,所以e存放在16位置。
整体的长度为17,根据(3)的规则,其总大小为24。
(2)如果一个结构体里面嵌套了结构体,则结构体成员要从其内部最大元素大小的整数倍地址开始存放。
例如:
struct S2 {
double a; //8 (0 7)
struct S1 s1; //8 (8 24)31
int b; //4 (32 4) 32 33 34 35
char c; //1 (36 1) 36
short d; //2 (37 2) 38 39
}s2;
其中a存放在0-7的位置,s1内部成员最大元素大小是8,那么第8位是s1内部成员最大元素大小的整数倍,而s1的大小为24,所以s1存放在第8-31位。
然后第32位是b大小的整数倍,所以b存放在32-35位。
继续第33位是c大小的整数倍,所以c存放在第36位。
最后第37位不是d大小的整数倍,向后排,第38位是d大小的整数倍,所以d存放在38-39位。
整体的长度为40,根据(3)的规则,其总大小为40。
(3) 结构体的总大小,就是sizeof(当前数据类型的大小)的结果,必须是其内部最大成员的整数倍数,不足的要补齐。
1 - 4 、class_getInstanceSize 和 malloc_size的区别
class_getInstanceSize 是对象需要的真正的内存,是8字节对齐。
malloc_size 是系统开辟内存时,用的是16字节对齐。这里就是一个用空间换时间的思想。
3、calloc (申请内存)
通过对libmalloc源码分析:(我们只看有关的核心代码)
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
size_t k, slot_bytes;
if (0 == size) {
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior 16
}
//(x + 15) >> 4 << 4
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
slot_bytes = k << SHIFT_NANO_QUANTUM; // multiply by power of two quanta size
*pKey = k - 1; // Zero-based!
return slot_bytes;
}
上面代码,是真正对象开辟空间的时的大小。其中也有一个16字节对其的算法:
(x + 15) >> 4 << 4
最后当开辟完之后返回内存地址。
4、initInstanceIsa (绑定内存和类)
先补充一下:联合体&位域
然后跟initInstanceIsa这个方法会来到下面核心的方法initIsa:
inline void
objc_object::initIsa(Class cls,
bool nonpointer,
UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
isa_t newisa(0);
if (!nonpointer) {
newisa.setClass(cls, this);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
# if ISA_HAS_CXX_DTOR_BIT
newisa.has_cxx_dtor = hasCxxDtor;
# endif
newisa.setClass(cls, this);
#endif
//这里默认设置为1
newisa.extra_rc = 1;
}
isa = newisa;
}
首先我们需要分析一下isa_t
# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# define ISA_MASK 0x007ffffffffffff8ULL
# define ISA_MAGIC_MASK 0x0000000000000001ULL
# define ISA_MAGIC_VALUE 0x0000000000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 0
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t weakly_referenced : 1; \
uintptr_t shiftcls_and_sig : 52; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
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
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
# endif
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# error unknown architecture for packed isa
# endif
// SUPPORT_PACKED_ISA
#endif
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 {
ISA_BITFIELD; // defined in isa.h
};
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_BITFIELD 对应的就是位域列表,我们通过对位域列表的分析就知道isa_t都存储了哪些东西了。
(1)nonpointer 表示是否对isa指针开启指针优化。0:纯isa指针,1:不止是类对象地址,isa中包含了类信息、对象的引用计数等。
(2)has_assoc: 关联对象标志位。0 没有 1有。
(3)has_cxx_dtor:该对象是否有c++或者objc的析构器,如果有需要做析构逻辑,如果没有则可以更快的释放对象。
(4)shiftcls:存储类指针的值。开启指针优化的情况下,在arm64架构中有33位用来存储类指针。
(5)magic:用于调试器判断当前对象是真的对象还是没有初始化的空间。
(6)weakly_referenced: 标志对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放。
(7)deallocating:判断对象是否正在释放。
(8)has_sidetable_rc:当对象引用计数大于10时,则需要借用该变量存储进位。
(9)extra_rc:当表示该对象的引用计数值。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc,可以看到取retainCount的函数:
inline uintptr_t
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = __c11_atomic_load((_Atomic uintptr_t *)&isa.bits,
__ATOMIC_RELAXED);
if (bits.nonpointer) {
uintptr_t rc = bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
知道了isa_t的结构之后,我们来看initIsa是如何关联类信息的。
首页 创建了一个newisa, 然后进行了一系列的赋值,最后可以看到newisa.setClass(cls, this);的方法。
inline void
isa_t::setClass(Class newCls, UNUSED_WITHOUT_PTRAUTH objc_object *obj)
{
// Match the conditional in isa.h.
#if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# if ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_NONE
// No signing, just use the raw pointer.
uintptr_t signedCls = (uintptr_t)newCls;
# elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ONLY_SWIFT
// We're only signing Swift classes. Non-Swift classes just use
// the raw pointer
uintptr_t signedCls = (uintptr_t)newCls;
if (newCls->isSwiftStable())
signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls,
ISA_SIGNING_KEY,
ptrauth_blend_discriminator(obj,
ISA_SIGNING_DISCRIMINATOR));
# elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ALL
// We're signing everything
uintptr_t signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls,
ISA_SIGNING_KEY,
ptrauth_blend_discriminator(obj,
ISA_SIGNING_DISCRIMINATOR));
# else
# error Unknown isa signing mode.
# endif
shiftcls_and_sig = signedCls >> 3;
#elif SUPPORT_INDEXED_ISA
// Indexed isa only uses this method to set a raw pointer class.
// Setting an indexed class is handled separately.
cls = newCls;
#else // Nonpointer isa, no ptrauth
shiftcls = (uintptr_t)newCls >> 3;
#endif
}
我们可以看到shiftcls = (uintptr_t)newCls >> 3;也就是将对象所属类的地址向右移3位(在取值进行& ISA_MASK时又补足这3位了)。
向右移3的是因为:类也是一个对象,也占8个字节(64位),在类信息里面只有中间的33(44 x86_64)位是有信息的。那么后3位是没有信息的,所以直接右移了3位。将末尾抹0。
拿到newCls的值然后右移了3位,存在了shiftcls。这就绑定了类的地址了。
通过lldb,通过左移右移来取得MyTextObjc的类信息。

也可以用isa的内存地址 & ISA_MASK 来得到类地址。
四、init 和 new
通过源码的分析就很容易能看到,init只是一个构造方法,工厂设计模式。
而new就是对 [alloc init] 的封装方法。
五、“为什么MyTextObjc(NSObject的子类),callAlloc流程会走两遍“ 和 “为什么MyTextObjc和NSObject的alloc流程不一样。”
首先有一个非常重要的一点: 无论NSObject还是其子类,alloc调用之后其实走的是objc_alloc。
为什么会这样呢,其实不难想到,肯定是编译的时候或者运行时,alloc方法符号(SEL)对应的实现IMP重新绑定成objc_alloc的函数地址了。通过对Mach-o的分析也能验证。
系统首先是通过llvm来实现这层绑定:
//特殊消息发送
static Optional<llvm::Value *>
tryGenerateSpecializedMessageSend(CodeGenFunction &CGF, QualType ResultType,
llvm::Value *Receiver,
const CallArgList& Args, Selector Sel,
const ObjCMethodDecl *method,
bool isClassMessage) {
auto &CGM = CGF.CGM;
if (!CGM.getCodeGenOpts().ObjCConvertMessagesToRuntimeCalls)
return None;
auto &Runtime = CGM.getLangOpts().ObjCRuntime;
switch (Sel.getMethodFamily()) {
case OMF_alloc:
if (isClassMessage &&
Runtime.shouldUseRuntimeFunctionsForAlloc() &&
ResultType->isObjCObjectPointerType()) {
// [Foo alloc] -> objc_alloc(Foo) or
// [self alloc] -> objc_alloc(self)
if (Sel.isUnarySelector() && Sel.getNameForSlot(0) == "alloc")
return CGF.EmitObjCAlloc(Receiver, CGF.ConvertType(ResultType));
// [Foo allocWithZone:nil] -> objc_allocWithZone(Foo) or
// [self allocWithZone:nil] -> objc_allocWithZone(self)
if (Sel.isKeywordSelector() && Sel.getNumArgs() == 1 &&
Args.size() == 1 && Args.front().getType()->isPointerType() &&
Sel.getNameForSlot(0) == "allocWithZone") {
const llvm::Value* arg = Args.front().getKnownRValue().getScalarVal();
if (isa<llvm::ConstantPointerNull>(arg))
return CGF.EmitObjCAllocWithZone(Receiver,
CGF.ConvertType(ResultType));
return None;
}
}
break;
case OMF_autorelease:
if (ResultType->isObjCObjectPointerType() &&
CGM.getLangOpts().getGC() == LangOptions::NonGC &&
Runtime.shouldUseARCFunctionsForRetainRelease())
return CGF.EmitObjCAutorelease(Receiver, CGF.ConvertType(ResultType));
break;
case OMF_retain:
if (ResultType->isObjCObjectPointerType() &&
CGM.getLangOpts().getGC() == LangOptions::NonGC &&
Runtime.shouldUseARCFunctionsForRetainRelease())
return CGF.EmitObjCRetainNonBlock(Receiver, CGF.ConvertType(ResultType));
break;
case OMF_release:
if (ResultType->isVoidType() &&
CGM.getLangOpts().getGC() == LangOptions::NonGC &&
Runtime.shouldUseARCFunctionsForRetainRelease()) {
CGF.EmitObjCRelease(Receiver, ARCPreciseLifetime);
return nullptr;
}
break;
default:
break;
}
return None;
}
/// Allocate the given objc object.
/// call i8* \@objc_alloc(i8* %value)
llvm::Value *CodeGenFunction::EmitObjCAlloc(llvm::Value *value,
llvm::Type *resultType) {
return emitObjCValueOperation(*this, value, resultType,
CGM.getObjCEntrypoints().objc_alloc,
"objc_alloc");
}
从llvm源码中就可以找到tryGenerateSpecializedMessageSend 特殊消息发送。也就是在编译的时候,会将alloc对应的方法实现改成objc_alloc。
然后再回调objc源码中:
static void
fixupMessageRef(message_ref_t *msg)
{
msg->sel = sel_registerName((const char *)msg->sel);
if (msg->imp == &objc_msgSend_fixup) {
if (msg->sel == @selector(alloc)) {
msg->imp = (IMP)&objc_alloc;
} else if (msg->sel == @selector(allocWithZone:)) {
msg->imp = (IMP)&objc_allocWithZone;
} else if (msg->sel == @selector(retain)) {
msg->imp = (IMP)&objc_retain;
} else if (msg->sel == @selector(release)) {
msg->imp = (IMP)&objc_release;
} else if (msg->sel == @selector(autorelease)) {
msg->imp = (IMP)&objc_autorelease;
} else {
msg->imp = &objc_msgSend_fixedup;
}
}
else if (msg->imp == &objc_msgSendSuper2_fixup) {
msg->imp = &objc_msgSendSuper2_fixedup;
}
else if (msg->imp == &objc_msgSend_stret_fixup) {
msg->imp = &objc_msgSend_stret_fixedup;
}
else if (msg->imp == &objc_msgSendSuper2_stret_fixup) {
msg->imp = &objc_msgSendSuper2_stret_fixedup;
}
#if defined(__i386__) || defined(__x86_64__)
else if (msg->imp == &objc_msgSend_fpret_fixup) {
msg->imp = &objc_msgSend_fpret_fixedup;
}
#endif
#if defined(__x86_64__)
else if (msg->imp == &objc_msgSend_fp2ret_fixup) {
msg->imp = &objc_msgSend_fp2ret_fixedup;
}
#endif
}
// SUPPORT_FIXUP
#endif
可以看到这里有一个fixupMessageRef的消息修复的方法。
在程序启动读取Mach-o镜像文件时,通过
message_ref_t *refs = _getObjc2MessageRefs(hi, &count); 获取引用消息。然后遍历进行fixupMessageRef修正。
从而再次验证,alloc之后必将会走objc_alloc。
那么为什么苹果要这么做呢?我猜测是,中间将alloc重新绑定为objc_alloc,其实就是一层系统级别的hook,类似于平时业务开发时进行的埋点,比如系统可以监控统计有多少对象进行alloc等。
接下来,NSObject和起子类的流程不一致,其实就是因为:cls->ISA()->hasCustomAWZ()决定的。
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
hasCustomAWZ 是判断有没有实现自定义的allocWithZone方法。
在MRC中会用到allocWithZone来实现单利。
可如今,allocWithZone已经废弃了。
而NSObjec是系统实现的这个方法。所以NSObject和其子类的流程不一致。
六. 通过地址找到内存
MyTextObjc *objc = [[MyTextObjc alloc]init];
objc.sub = [[MyTextSubObjc alloc] init];
看上面代码,我用x/4gx 打印
(lldb) x/4gx objc
0x1038b8da0: 0x011d800100008395 0x00000001038b9420
0x1038b8db0: 0x0000000000000000 0x0000000000000000
(lldb)
其中 0x1038b8da0 是objc的首地址,它指向的是0x011d800100008395(isa)。
那么我想要通过地址去打印sub这个属性。
通过0x1038b8da0和0x1038b8db0可以判断出,内存地址是连续的,0x00000001038b9420这个内存对应的地址是0x1038b8da8,我继续操作lldb
(lldb) p (MyTextSubObjc **)0x1038b8da8
(MyTextSubObjc **) $3 = 0x00000001038b8da8
(lldb)
我将0x1038b8da8这个地址强转为MyTextSubObjc **,用$3保存。
(lldb) p *$3
(MyTextSubObjc *) $4 = 0x00000001038b9420
(lldb) po 0x00000001038b9420
<MyTextSubObjc: 0x1038b9420>
(lldb)
最后p *$3,也就找到了0x00000001038b8da8指向的内存地址为:0x00000001038b9420。
做这个调试,主要是为了去区分地址和对应内存的关系。