alloc&init 探索
- 首先要明確alloc做了什么,init做了什么。
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
Person *p1 = [Person alloc];
Person *p2 = [p1 init];
Person *p3 = [p1 init];
SSNSLog(@"%@",p1);
SSNSLog(@"%@",p2);
SSNSLog(@"%@",p3);
}
@end
上方的p1/p2/p3經(jīng)打印是一模一樣的

分別打印p1/p2/p3的地址是否相同?

分別打印&p1/&p2/&p3的地址是否相同?

-
總結(jié):為什么會(huì)出現(xiàn)這種情況?
因?yàn)閕nit并沒有對(duì)alloc出來的內(nèi)存空間的指針進(jìn)行任何修改,p2/p3的指針都是指向這塊內(nèi)存空間,這是為什么第二個(gè)是相等的。其實(shí)<Person: 0x600000b94040> 中冒號(hào)后面的0x600000b94040 就是指向的地址。
下圖示意:
截屏2021-03-22 下午2.36.02.png 那么alloc是怎么開辟內(nèi)存空間的呢,這里就需要看底層的實(shí)現(xiàn)了。這里記錄三種方法去看
1.下符號(hào)斷點(diǎn)的形式(Symbolic BreakPoint)跟流程,配合objc源碼看源碼;
2.通過摁住control-step into跟流程,配合objc源碼;
3.匯編(Denug->Debug Workflow->Always Show Disassembly)查看跟流程。(最常用)
這里貼出蘋果開源源碼地址: https://opensource.apple.com
這個(gè)地址?的更直接 https://opensource.apple.com/tarballs/
- 我們可以在源碼地址直接下載源碼工程直接設(shè)置一個(gè)走讀的工程,這樣就可以直接打斷點(diǎn)看源碼了。這塊的資料有很多就不再寫了。我這里在github上防止一個(gè)配置好的objc4-781的工程,有需要的可以下載。
+ (id)alloc {
return _objc_rootAlloc(self);
}
// Replaced by ObjectAlloc
+ (id)allocWithZone:(struct _NSZone *)zone {
return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}
// Replaced by CF (throws an NSException)
+ (id)init {
return (id)self;
}
上面可以看到alloc調(diào)用的 _objc_rootAlloc 方法,接著往下找_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*/);
}
可以看到_objc_rootAlloc去調(diào)用了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 __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
// No shortcuts available.
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
上面我們看到callAlloc可能走兩個(gè)方法:_objc_rootAllocWithZone 和 objc_msgSend。我們先看_objc_rootAllocWithZone
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
可以看到_objc_rootAllocWithZone直接調(diào)用的_class_createInstanceFromZone,我們已經(jīng)走到了關(guān)鍵點(diǎn)位置
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)
{
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;
//1.計(jì)算需要的內(nèi)存空間大小
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
// 2.向系統(tǒng)申請開辟內(nèi)存,返回地址指針
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
if (!zone && fast) {
//3.關(guān)聯(lián)到相應(yīng)的類
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);
}
這里_class_createInstanceFromZone 有三個(gè)關(guān)鍵的步驟instanceSize 和calloc和initInstanceIsa 作用備注了一下
首先看instanceSize方法
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;
}
這面是cache.fastInstanceSize
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);
// 16 0 8
}
}
上面size=16 ;extra = 0 ; FAST_CACHE_ALLOC_DELTA16 = 8 最后16+0-8 = 8
#define FAST_CACHE_ALLOC_MASK 0x1ff8
#define FAST_CACHE_ALLOC_MASK16 0x1ff0
#define FAST_CACHE_ALLOC_DELTA16 0x0008
再看下align16的實(shí)現(xiàn)
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15); //16字節(jié)對(duì)齊
}
這個(gè)函數(shù)明顯是16字節(jié)對(duì)齊
0000 0000 0001 0111 8+15 = 23
& 1111 1111 1111 0000 ->15取反
相與后得到: 0000 0000 0001 0000 即16后面的都抹零,最后是16的倍數(shù)
- 所以,instanceSize作用是計(jì)算需要的內(nèi)存空間大小。
現(xiàn)在去看calloc的實(shí)現(xiàn),打斷點(diǎn)po一下obj,打印出來是nil

走完calloc的實(shí)現(xiàn),打斷點(diǎn)po一下obj,打印出來是一個(gè)地址,現(xiàn)在已經(jīng)開辟了內(nèi)存了。但是只是一個(gè)指針,還沒有關(guān)聯(lián)到Person類

下面走到initInstanceIsa,把指針和類關(guān)聯(lián)起來

- 所以alloc的流程圖為:

- 從上面的結(jié)論看alloc已經(jīng)完成了開辟空間并與Person類關(guān)聯(lián)的工作,那么init的作用是什么呢?貼一下init的源碼實(shí)現(xiàn)
+ (id)init {
return (id)self;
}
這是重寫init的構(gòu)造方法,工廠設(shè)計(jì),給用戶提供相應(yīng)的入口
- 然后new的作用是什么呢?
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
可以看到調(diào)用了alloc流程中的callAlloc流程,它把類對(duì)象也傳了進(jìn)去(Self),相當(dāng)于調(diào)用了alloc+init
- 在這里回頭看下_class_createInstanceFromZone 中的 size = cls->instanceSize(extraBytes); 當(dāng)時(shí)走到這里的時(shí)候,看到大小是16,那么什么可以影響這個(gè)size大小呢?再把源碼貼過來
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);
// 16 0 8
}
}
在這里我們先給Person增加一個(gè)name屬性,再次走到這個(gè)方法看下size大小,可以看到仍然是16

然后我們再給Person增加一個(gè)nickName屬性,再次走到這個(gè)方法看下size大小,可以看到已經(jīng)變成了32

- 這里是字節(jié)對(duì)齊,所以對(duì)對(duì)象來說影響size大小的因素是屬性,這里 8+8+8 = 24 然后字節(jié)對(duì)齊后是 32
- 今天聽說[NSObject alloc]的流程的問題,跟了一下,附在這個(gè)后面,我是這樣做的,在791的源碼工程里main.m文件里
#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *objc1 = [NSObject alloc];
Person *objc2 = [Person alloc];
NSLog(@"Hello, World! %@",objc2);
}
return 0;
}
打上斷點(diǎn),下圖:


結(jié)果發(fā)生了,在main函數(shù)的14行直接走了第15行,并沒有走+ (id)alloc。然后發(fā)現(xiàn)走的是objc_alloc函數(shù)
// Calls [cls alloc].
id
objc_alloc(Class cls)
{
return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
然后進(jìn)入callAlloc 走 _objc_rootAllocWithZone 函數(shù)
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
// No shortcuts available.
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
- 而[Person alloc] 的流程不是這樣的,而是先后走了兩次 callAlloc 函數(shù)
下面寫下順序:
- 先走objc_alloc函數(shù)
// Calls [cls alloc].
id
objc_alloc(Class cls)
{
return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
- 在callAlloc 函數(shù)中走的最后一行
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc))
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
// No shortcuts available.
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
這句去調(diào)用了alloc 函數(shù),之后走的時(shí)候上面的alloc的流程。這才是一個(gè)最完整的流程
+ (id)alloc {
return _objc_rootAlloc(self);
}
- 所以為什么走兩次呢?這里的分歧點(diǎn)在callAlloc 函數(shù)里 !cls->ISA()->hasCustomAWZ() 結(jié)果的不同導(dǎo)致的,在第一次進(jìn)去callAlloc時(shí),取true調(diào)用_objc_rootAllocWithZone,第二次取得值為false,走的最后((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc)), 去調(diào)用了alloc。這里大部分都寫的是系統(tǒng)自己走的第一遍,沒有過多研究。其實(shí)這里跟isa密切相關(guān)。
首先研究下ISA()這個(gè)函數(shù),貼出源碼
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
}
這里我們打斷點(diǎn)走一下,在開始開始打印下isa 可以看到

然后向下走發(fā)現(xiàn)走的是最后一句return (Class)(isa.bits & ISA_MASK); 這句返回的是什么呢?ISA_MASK 查看宏定義是 0x00007ffffffffff8ULL , 這里我特別看了下NSobject 和 Person的不同,如果是NSobject 打印的是

但是Person打印的是 :

- 所以總結(jié):Person 在第一次進(jìn)入callAlloc里 !cls->ISA()->hasCustomAWZ()取得false(在ISA()函數(shù)里取得Person類,然后去獲取hasCustomAWZ()取得false),取走的最后一句發(fā)消息調(diào)用alloc,而后第二次進(jìn)去callAlloc,這時(shí)!cls->ISA()->hasCustomAWZ()拿到的true,才去開辟空間并關(guān)聯(lián)了類。而NSobject的alloc流程早就在系統(tǒng)層面走完了。系統(tǒng)層面去研究就要看LLVM的源碼了
