objc_alloc的分析
运行时,alloc方法流程分析
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LGPerson *p = [LGPerson alloc];
Class pClass = [LGPerson class];
NSLog(@"Hello, World! %@ -- %p",p,pClass);
}
return 0;
}
接下来我们摁住conmand,单击alloc:
现在跳到了NSObject的类方法alloc中:【①】
+ (id)alloc {
return _objc_rootAlloc(self);
}
然后跳到_objc_rootAlloc中:【②】
// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
再然后跳到callAlloc中:【③】
// Call [cls alloc] or [cls allocWithZone:nil], with appropriate
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
if (slowpath(checkNil && !cls)) return nil;
#if __OBJC2__
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
// No alloc/allocWithZone implementation. Go straight to the allocator.
// fixme store hasCustomAWZ in the non-meta class and
// add it to canAllocFast's summary
if (fastpath(cls->canAllocFast())) {
// No ctors, raw isa, etc. Go straight to the metal.
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
}
else {
// Has ctor or raw isa or something. Use the slower path.
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
#endif
// No shortcuts available.
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}
上面的代码中,如果我们覆写了该类的allocWithZone方法,那么就会走到第31行的逻辑;不过一般而言我们是不会自己去覆写allocWithZone方法的,所以一般都会走第8~28行的逻辑。
接下来我们看上面的第13行,跳到canAllocFast中:【④】
bool canAllocFast() {
assert(!isFuture());
return bits.canAllocFast();
}
然后再跳到bits的canAllocFast中:【⑤】
#if FAST_ALLOC
size_t fastInstanceSize()
{
assert(bits & FAST_ALLOC);
return (bits >> FAST_SHIFTED_SIZE_SHIFT) * 16;
}
void setFastInstanceSize(size_t newSize)
{
// Set during realization or construction only. No locking needed.
assert(data()->flags & RW_REALIZING);
// Round up to 16-byte boundary, then divide to get 16-byte units
newSize = ((newSize + 15) & ~15) / 16;
uintptr_t newBits = newSize << FAST_SHIFTED_SIZE_SHIFT;
if ((newBits >> FAST_SHIFTED_SIZE_SHIFT) == newSize) {
int shift = WORD_BITS - FAST_SHIFTED_SIZE_SHIFT;
uintptr_t oldBits = (bits << shift) >> shift;
if ((oldBits & FAST_ALLOC_MASK) == FAST_ALLOC_VALUE) {
newBits |= FAST_ALLOC;
}
bits = oldBits | newBits;
}
}
bool canAllocFast() {
return bits & FAST_ALLOC;
}
#else
size_t fastInstanceSize() {
abort();
}
void setFastInstanceSize(size_t) {
// nothing
}
bool canAllocFast() {
return false;
}
#endif
我们发现,上面有一个FAST_ALLOC条件,那么具体是走上面的canAllocFast呢还是下面的canAllocFast呢?这完全取决于FAST_ALLOC条件的取值,所以我们需要看一下FAST_ALLOC的取值:【⑥】
#if !__LP64__
// class or superclass has .cxx_construct implementation
#define RW_HAS_CXX_CTOR (1<<18)
// class or superclass has .cxx_destruct implementation
#define RW_HAS_CXX_DTOR (1<<17)
// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
#define RW_HAS_DEFAULT_AWZ (1<<16)
// class's instances requires raw isa
#if SUPPORT_NONPOINTER_ISA
#define RW_REQUIRES_RAW_ISA (1<<15)
#endif
// class or superclass has default retain/release/autorelease/retainCount/
// _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
#define RW_HAS_DEFAULT_RR (1<<14)
// class is a Swift class from the pre-stable Swift ABI
#define FAST_IS_SWIFT_LEGACY (1UL<<0)
// class is a Swift class from the stable Swift ABI
#define FAST_IS_SWIFT_STABLE (1UL<<1)
// data pointer
#define FAST_DATA_MASK 0xfffffffcUL
#elif 1
// Leaks-compatible version that steals low bits only.
// class or superclass has .cxx_construct implementation
#define RW_HAS_CXX_CTOR (1<<18)
// class or superclass has .cxx_destruct implementation
#define RW_HAS_CXX_DTOR (1<<17)
// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
#define RW_HAS_DEFAULT_AWZ (1<<16)
// class's instances requires raw isa
#define RW_REQUIRES_RAW_ISA (1<<15)
// class is a Swift class from the pre-stable Swift ABI
#define FAST_IS_SWIFT_LEGACY (1UL<<0)
// class is a Swift class from the stable Swift ABI
#define FAST_IS_SWIFT_STABLE (1UL<<1)
// class or superclass has default retain/release/autorelease/retainCount/
// _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
#define FAST_HAS_DEFAULT_RR (1UL<<2)
// data pointer
#define FAST_DATA_MASK 0x00007ffffffffff8UL
#else
// Leaks-incompatible version that steals lots of bits.
// class is a Swift class from the pre-stable Swift ABI
#define FAST_IS_SWIFT_LEGACY (1UL<<0)
// class is a Swift class from the stable Swift ABI
#define FAST_IS_SWIFT_STABLE (1UL<<1)
// summary bit for fast alloc path: !hasCxxCtor and
// !instancesRequireRawIsa and instanceSize fits into shiftedSize
#define FAST_ALLOC (1UL<<2)
// data pointer
#define FAST_DATA_MASK 0x00007ffffffffff8UL
// class or superclass has .cxx_construct implementation
#define FAST_HAS_CXX_CTOR (1UL<<47)
// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
#define FAST_HAS_DEFAULT_AWZ (1UL<<48)
// class or superclass has default retain/release/autorelease/retainCount/
// _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
#define FAST_HAS_DEFAULT_RR (1UL<<49)
// class's instances requires raw isa
// This bit is aligned with isa_t->hasCxxDtor to save an instruction.
#define FAST_REQUIRES_RAW_ISA (1UL<<50)
// class or superclass has .cxx_destruct implementation
#define FAST_HAS_CXX_DTOR (1UL<<51)
// instance size in units of 16 bytes
// or 0 if the instance size is too big in this field
// This field must be LAST
#define FAST_SHIFTED_SIZE_SHIFT 52
// FAST_ALLOC means
// FAST_HAS_CXX_CTOR is set
// FAST_REQUIRES_RAW_ISA is not set
// FAST_SHIFTED_SIZE is not zero
// FAST_ALLOC does NOT check FAST_HAS_DEFAULT_AWZ because that
// bit is stored on the metaclass.
#define FAST_ALLOC_MASK (FAST_HAS_CXX_CTOR | FAST_REQUIRES_RAW_ISA)
#define FAST_ALLOC_VALUE (0)
#endif
这里的第25行和第48行都是else语句,而FAST_ALLOC的定义是在第57行,也就是最后一个else中,但我们仔细看第25行,是#elif 1,这个条件是恒真的,因此永远不会走到最后一个else,也就是说,FAST_ALLOC永远不会被赋值。
因此,上面【⑤】中的#if FAST_ALLOC永远不会走,只会走下面的#else,也就是说,永远canAllocFast返回false。
因此,【③】中第13行if (fastpath(cls->canAllocFast()))条件永远不会走,只会走else中的第23~25行:
编译期,alloc方法流程分析
上面的分析只是alloc方法流程的一个表象,其实,针对alloc、allocWithZone、retain、release、autoRelease这些方法,LLVM编译器会对其进行拦截,拦截之后会在暗地里进行各种内部优化。接下来我们就以alloc方法为例来研究下LLVM是如何对之进行拦截优化的。
我们在下面位置打个断点:
然后查看汇编源码:
如下:
我们发现,当我们调用alloc方法的时候,会调起一个名为objc_alloc的符号,而其他的一些方法基本都是走得正常的objc_msgSend消息转发流程。此时我不禁就有疑问了,为什么这里的objc_alloc是一种符号形式(symbol stub for: objc_alloc)呢,为什么没有走消息转发(objc_msgSend)呢?
实际上,objc_alloc是系统在编译期调用的一种符号。
那么编译期我怎么探索呢?答案是使用LLVM。
在llvm测试工程中,我们搜索【test_alloc_class】,就可以看到下面这段注释:
// Make sure we get a bitcast on the return type as the
// call will return i8* which we have to cast to A*
// CHECK-LABEL: define {{.*}}void @test_alloc_class_ptr
A* test_alloc_class_ptr() {
// CALLS: {{call.*@objc_alloc}}
// CALLS-NEXT: bitcast i8*
// CALLS-NEXT: ret
return [B alloc];
}
通过这段注释我们知道了:
系统在真正调用alloc方法之前会首先调用objc_alloc。也就是说,在编译期会绑定objc_alloc符号,然后在运行时会走本文一开始讲的那一套【运行时alloc流程】,即正常的objc_msg消息转发流程。
接下来我们就来探索objc_alloc。
经过全局搜索,我们发现在下面的emitObjCValueOperation方法中会调用objc_alloc:
/// 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");
}
接下来我们看emitObjCValueOperation的实现:
现在我们知道了,在emitObjCValueOperation方法中会调用objc_alloc,那么哪里会调用emitObjCValueOperation方法呢?
/// 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");
}
我们发现,在EmitObjCAlloc中会调用emitObjCValueOperation
那么,在哪里会调用EmitObjCAlloc呢?
我们发现,在tryGenerateSpecializedMessageSend中,只要方法名是alloc,那么就会调EmitObjCAlloc,进而调emitObjCValueOperation,进而通过EmitCallOrInvoke来调用objc_alloc。
现在我们来想一下,为什么系统会调用tryGenerateSpecializedMessageSend方法呢?苹果在文档中有这样一段解释:
简而言之就是说,就是:
(1)tryGenerateSpecializedMessageSend方法能够比消息转发更快地生成实例对象。
(2)如果Runtime运行时确实支持所需的入口点(alloc、allocWithZone、autoRelease、retain、release),那么此方法将生成一个调用并返回结果值;否则它将返回空值None,此时调用者在外层会直接走一般的msgSend流程。
然后我们再接着看,看一下tryGenerateSpecializedMessageSend方法是在哪里被调用的呢?如下:
现在我们知道了,所有的消息都有两种处理方式,一种是特殊的消息发送,一种是一般消息发送。alloc、allocWithZone、retain、release、autorelease走的就是特殊的消息发送。
将代码转成汇编之后,带有objc_messageSend的就是一般的消息发送,带有symble stub for的就是特殊的消息发送。
实际上,所谓的特殊的消息发送,其实指的就是:LLVM编译器在底层针对一些特殊的方法做了拦截和优化。
需要注意的是,objc_alloc只会被调用一次。原因如下:
LLVM会在编译的时候对alloc方法进行拦截,拦截到之后会调用objc_alloc函数,调用了该函数之后就会对调用该函数的对象即消息的接收者receiver打上一个标记,然后走objc_msgSend;
下次再进来的时候,由于之前已经标记过receiver了,所以就不会走objc_alloc函数,而是直接走正常的objc_msgSend消息转发了。
结构体内存对齐的原则
1,系统定义的数据成员的对齐规则:
结构体(struct)或者联合体(union)的数据成员,第一个数据成员会放在offset为0的地方,之后的每个数据成员存储的起始位置要从该成员大小(如果该成员有子成员,比如数组、结构体等,那么就从子成员大小)的整数倍开始。
2,如果一个结构体里面的成员又是一个结构体,那么该结构体成员要从其内部最大元素大小的整数倍开始存储。
3,收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的需要补齐。
struct NormanStruct1 {
char a;
double b;
int c;
short d;
} NormanStruct1;
struct NormanStruct2 {
double b;
int c;
char a;
short d;
} NormanStruct2;
NSLog(@"NormanStruct1***%lu", sizeof(NormanStruct1)); // NormanStruct1***24
NSLog(@"NormanStruct2***%lu", sizeof(NormanStruct2)); // NormanStruct2***16
NormanStruct1为什么是24:
a-1,b-8,c-4,d-2。a在第0个字节的位置;b的起始位置需要是8的整数倍,所以b的起始位置是8;c的起始位置需要是4的整数倍,所以c的起始位置是16;d的起始位置需要是2的整数倍,所以d的起始位置是20。最后按照内部最大成员大小(double,8字节)倍数补齐,因此NormanStruct1的大小是24。
NormanStruct2为什么是16:
b-8,c-4,a-1,d-2。b在第0个字节的位置;c的起始位置需要是4的整数倍,所以c的起始位置是8;a的起始位置需要是1的整数倍,所以a的起始位置是12;d的起始位置需要是2的整数倍,所以d的起始位置是14。最终正好是16,无需按照内部最大成员大小(double,8字节)倍数补齐,因此NormanStruct2的大小是16。
iOS中获取内存大小的三种方式
1,sizeof
如果传进来的是类型,用于计算这个类型占多大内存,它是在编译器编译阶段确定内存大小,因此不能用来返回动态分配的内存空间的大小。
它的功能是:获得能够容纳所建立的最大对象的内存大小。
如果传进来的是OC对象指针,我们知道,OC对象指针的大小就是8字节,因此就是8字节。
2,class_getInstanceSize
获取对象申请的内存大小。在运行时分析该对象中的各个属性,然后计算出其所需要的内存大小,其具体是多少字节对齐,是由上面的内存对齐原则计算得来。
举个例子,现在有一个Norman类,生成了一个Norman类的实例对象norman,那么:
sizeof(norman)是8
而sizeof(Norman)与class_getInstanceSize(norman)的大小是一样的,都是按照内存对齐原则计算得来。
3,malloc_size
系统给对象实际开辟的内存大小。其参考因素是整个对象,因此必须是16字节对齐。
也许你会有一个疑问,为什么参考因素是对象中的成员的时候是8字节对齐,而参考因素是对象的时候就是16字节对齐呢?
我们知道,系统中的内存空间是连续的,因此呢,对象与对象之间开辟的内存区域也是连续的,如果一个对象内存的尾部与另一个对象内存的首部是紧挨着而没有一丁点儿的缓冲余地的话,那么前面的对象遇到一些特殊情况需要处理的时候就会导致内存溢出(这里需要说明的是,只有中间有空隙而未完全填充的对象才会有内存溢出的风险,那些内存完全填充的对象是没有内存溢出风险的)。因此,对象内存分配的原则是,需要给未安全填充的对象在其内存段的最后留出8字节的缓冲区域。而在未完全填充的对象的内存中,那些间隙可能是在末尾,也可能是在中间,如果是在中间的话,那么按照8字节对齐的原则,有可能就不会在末尾预留充足的缓冲区域了(比如某对象现在是36字节,中间有4字节的间隙,如果按照8字节对齐,那么对象就会在其最后补4字节,而4字节是不够处理内存溢出的);而如果按照16字节对齐,那么就能确保缓冲区域是充足的。
通过二进制位移进行字节对齐
实际上,字节对齐的本质就是让这个数多加【一个对齐位减1】,然后抹零。
8(2^3)字节对齐:
(x + (8 - 1)) >> 3 << 3
16(2^4)字节对齐:
(x + (16 - 1)) >>4 << 4
2^n字节对齐
(x +(2^n - 1)) >> n << n
等同于:(x + WORD_MASK)& ~WORD_MASK
isa指针的结构
定义如下:
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
我们可以看到,isa是一个union,它有一个记录将其绑定到哪个类上面的属性cls。并且,isa_t这个联合体里面还有一个bits成员,说明还有位域的概念。
接下来介绍一下联合体和位域的概念
先来看一个例子:
现在有一个坦克类,它有4个属性:向前、向后、向左、向右,如下:
@interface NormanTank : NSObject
@property (nonatomic, assign)BOOL front;
@property (nonatomic, assign)BOOL back;
@property (nonatomic, assign)BOOL left;
@property (nonatomic, assign)BOOL right;
@end
然后我们在外界创建NormanTank的实例,并且打断点,如下:
在断点处使用x tank来打印tank的内存段,我们发现front、back、left、right这四个属性每个属性都占用了一个字节,这实际上是很耗内存的。
因为一个字节有8位,即00000000,如果我只需要存储一个布尔值,即非0即1,我们没有必要使用8位的,我只需要使用1位即可(0表示no ,1表示yes)。
因此,我就可以定义一个char类型(char是一个字节),一个char有8位,我们就可以使用这8位中的后4位来分别定义前后左右了。这样就能节省很多内存空间。
接下来我们来比较一下结构体struct和联合体union:
结构体中的所有变量是“共存”的——其优点是“有容乃大”、全面;其缺点是内存空间的分配是粗放的,不管用不用,全分配。
联合体(又称为共用体)中的各变量是“互斥”的,其内存空间是共用的——缺点是不够“包容”;优点是,内存使用更为精细灵活,也节省了内存空间。
一般而言,联合体union会搭配位域bitField来使用,如下:
// 联合体
union {
char bits;
// 位域
struct {
char front : 1;
char back : 1;
char left : 1;
char right : 1;
};
} _direction;
实际上,isa就是一个联合体&位域的结构。
isa联合体中有定义位域,它是一个宏,之所以将它定义成宏,是因为这个位域是跟架构有关的,如下:
isa的结构是一个联合体,联合体里面的bits是一个uintptr_t类型,uintptr_t类型的定义如下:
typedef unsigned long uintptr_t;
因此,bits是一个无符号long类型,long类型是占8个字节的,因此isa指针占8个字节。
除了根据bits来知道isa指针占8个字节,根据位域ISA_BITFIELD也可以知道。位域ISA_BITFIELD是一个结构体,而结构体里面的内容算一下的话也是64位,即8个字节:
通过上图我们可以看出来,isa里面可以存储很多东西的,下面我将以arm64架构为例一一罗列。
第1位表示的是该isa是否是nonpointer,即是否对isa指针开启指针优化。
0表示不是nonpointer,即没有开启指针优化,即该isa是一个纯isa指针,不携带其他任何信息。
1表示是nonpointer,即开启了指针优化,也就是说该isa里面除了绑定了类对象的地址,还携带了其他的一些信息。
第二位has_assoc是关联对象标志位,0表示没有关联对象,1表示存在关联对象
第三位has_cxx_dtor表示该对象是否有C++或者Objc的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快地释放对象。
第4位到第36位存储的是shiftcls,它是存储类指针的值。在开启指针优化的情况下,在arm64架构中有33位来存储类指针。
第37位到第42位是magic,它用于调试器判断当前对象是真的对象还是没有初始化的空间
第43位是weakly_referenced,它标志对象是否有弱引用,没有弱引用的对象可以更快被释放
第44位是deallocating,它标志对象是否正在释放内存
第45位是has_sidetable_rc,当对象的引用计数大于10 的时候,需要借助该变量存储进位。
第46到第64位是extra_rc,表示该对象的引用计数值减1。
现在我们可以更深刻地感知到,isa指针是可以存储很多信息的,而这些信息都是跟该对象有关的,如果我直接通过属性来存储这些信息,势必会浪费很多的内存空间。
isa联合体中,类结构的绑定
上面我们有提到,在nonpointer的isa指针中,会有一个shiftcls来存储类指针,即绑定对应类的地址。isa的初始化代码如下:
我们可以看到,如果该指针不是nonpointer类型,那么就直接给其cls赋值,如下:
isa.cls = cls;
如果该指针是nonpointer类型,则会进行相关信息的初始化,其中一个就是对应类信息的初始化:
newisa.shiftcls = (uintptr_t)cls >> 3;
接下来我们就来验证一下上面的这行初始化代码,看看是否真正绑定了对应的类。
首先,我在NormanTank的实例对象创建完成后打了个断点,然后在控制台执行lldb指令 x/4gx tank 来查看tank对象的前4段十六进制内存。
通过之前的讲解我们知道,对象的属性存在内存中的位置可能会因为内存优化而与声明的顺序不一致,因此我们可能会有疑问,isa指针的位置到底是固定的还是变化的呢?
正确答案是:所有实例对象的第一个属性必然都是isa,它在内存中的位置永远都是在最开始。
【题外话】
接下来我们进行二进制打印isa的地址:
(lldb) x/4gx tank
0x600003075910: 0x000000010e764ec8 0x0000000000000000
0x600003075920: 0x00007fcc52401650 0x0000000000000000
(lldb) p/t 0x000000010e764ec8
(long) $2 = 0b0000000000000000000000000000000100001110011101100100111011001000
【题外话结束】
现在咱来想想,如何获取一个对象的的类呢?其中一个方式就是使用RuntimeAPI——object_getClass:
object_getClass(id _Nullable obj)
这个API的作用是通过一个对象获取一个类,通过对象找到对应的类,势必会使用到isa指针。现在我们来瞅瞅该API的源码:
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
然后查看getIsa函数的源码:
inline Class
objc_object::ISA()
{
assert(!isTaggedPointer());
#if SUPPORT_INDEXED_ISA
if (isa.nonpointer) {
uintptr_t slot = isa.indexcls;
return classForIndex((unsigned)slot);
}
return (Class)isa.bits;
#else
return (Class)(isa.bits & ISA_MASK);
#endif
}
ISA_MASK是一个掩码,它的取值是跟架构有关的,定义如下:
现在我获取到了X86上isa的掩码,接下来我将isa的十六进制地址与isa的掩码做与操作,这次获取到的应该就是实例对象所对应的类的地址了:
此时获取到的0x000000010f7a6ec8应该就是NormanTank类的地址,接下来我们通过打印NormanTank类来验证一下:
我们发现,两者完全一致。
因此我们得出结论,对象中的第一个属性必然是isa指针,并且isa指针指向的就是该对象所对应的类的内存地址。
类在内存中只有一份
通过上面的分析我们知道,类的实例对象可以创建多个,并且每个实例对象内部第一个属性isa会指向该实例对象所对应的类,那么现在有个问题,指向的这个类的内存是固定的吗?或者说,类对象可以创建多份吗?
下面我们来验证一下:
Class class1 = NormanTank.class;
Class class2 = object_getClass([NormanTank alloc]);
Class class3 = [NormanTank alloc].class;
Class class4 = [NormanTank alloc].class;
NSLog(@"\n%p\n%p\n%p\n%p\n", class1, class2, class3, class4);
打印如下:
2021-02-07 09:32:18.582448+0800 排序算法[1334:75838]
0x107357ee8
0x107357ee8
0x107357ee8
0x107357ee8
我们可以看到,打印结果是一致的,这说明类在内存中只会存在一份。
类的实例对象是由程序员对类进行实例化得来,而类对象是由系统创建的。
isa走位
我在isa指针中介绍过isa的走位,结论就是:
类的实例对象的isa指向类对象,类对象的isa指向元类对象,元类对象的isa指向根元类对象,根元类对象的isa指向其自身。
接下来进行验证:
第一步,我使用x/4gx tank来打印了实例对象tank的内存地址,第一段地址就是isa存储的内容,即对应类的地址:
(lldb) x/4gx tank
0x600003b64270: 0x0000000106b5eec8 0x0000000000000000
0x600003b64280: 0x000008facd634280 0x00007fcdc4400027
(lldb) po 0x0000000106b5eec8
NormanTank
第二步,打印对应类的内存,第一段地址就是isa存储的内容,即对应元类的地址:
(lldb) x/4gx 0x0000000106b5eec8
0x106b5eec8: 0x0000000106b5eef0 0x00000001074b1200
0x106b5eed8: 0x0000600001a48aa0 0x0001801800000003
(lldb) po 0x0000000106b5eef0
NormanTank
第三步,打印对应元类的内存,第一段地址就是isa存储的内容,即对应根元类的地址:
(lldb) x/4gx 0x0000000106b5eef0
0x106b5eef0: 0x00000001074b11d8 0x00000001074b11d8
0x106b5ef00: 0x0000600000b7e010 0x0004c03100000007
(lldb) po 0x00000001074b11d8
NSObject
第四步,打印对应根元类的内存,第一段地址就是isa存储的内容,细心的你不难发现,根元类的第一段地址就是根元类自身的地址,这说明根元类的isa指向的就是其本身。
(lldb) x/4gx 0x00000001074b11d8
0x1074b11d8: 0x00000001074b11d8 0x00000001074b1200
0x1074b11e8: 0x00007fcdc450e2c0 0x0008c0310000000f
(lldb) po 0x00000001074b11d8
NSObject
需要注意的是,此时打印出来的NSObject是根元类,不是根类,不信的话,你看下面第五步的验证:
(lldb) x/4gx NSObject.class
0x1074b1200: 0x00000001074b11d8 0x0000000000000000
0x1074b1210: 0x00007fcdc4504020 0x000480100000000f
这里我直接打印NSObject类的内存,发现第一段是0x00000001074b11d8,它是NSObject类的isa指针指向的元类,也就是根元类。而上面第四步中也是0x00000001074b11d8,所以说,上面第四步中的NSObject是根元类。
Clang编译
我们在研究的过程中,经常会需要将OC代码编译成C++,如何编译呢?
首先进入到需要编译的文件所在的文件夹,然后在终端执行如下命令(假设需要编译NormanTank.m):
clang -rewrite-objc NormanTank.m -o NormanTank.cpp
如果编译成功,那么在同一个文件目录下会多一个NormanTank.cpp文件,如下:
如果NormanTank.m中引入了UIKit框架,则会报下面的错误:
此时使用如下命令进行编译:
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk NormanTank.m
或者使用如下命令(模拟器):
xcrun -sdk iphonesimulator clang -rewrite-objc NormanTank.m
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
或者(真机):
xcrun -sdk iphoneos clang -rewrite-objc NormanTank.m
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
Xcode在安装的时候会顺带安装了'xcrun'命令,'xcrun'命令是在‘clang’命令的基础上进行了一些封装,会更好用一些。
new VS alloc-init
new方法的底层源码如下:
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
可见,new方法完全等同于alloc + init。
以上。