我們平時開發(fā)的時候經(jīng)常創(chuàng)建一個對象然后通過 alloc 跟 init 方法創(chuàng)建對象。那么有沒有想過 alloc 跟 init 里面到底做了哪些事情呢?
1. alloc & init 初探
1.1 準備
為了方便直觀的體現(xiàn)差異,定義一個宏,打印 NSString 的 isa、內(nèi)存地址、值。
#define Log(_var) ({ NSString *name = @#_var; NSLog(@"%@: %@ -> %p : %@ ", name, [_var class], &_var, _var); })
1.2.1 alloc 探索
下面我們創(chuàng)建寫段測試代碼,如下:
Person *person0 = [Person alloc];
Person *person1 = [person0 init];
Person *person2 = [person0 init];
Log(person0);
Log(person1);
Log(person2);
打印執(zhí)行結(jié)果如下

三個對象除了指針地址不一樣以外。其他完全一樣。也就是說三個對象指向的地址一樣,指針本身地址不一樣。
-
alloc已經(jīng)創(chuàng)建了對象,怎么創(chuàng)建的呢? -
init又是做啥的?
下面介紹三種方式觀察 alloc里面做了什么
1、在對象 alloc 時下斷點,然后當執(zhí)行到斷點處,按住Control + 點擊
2、下符號斷點 : libobjc.A.dylib`+[NSObject alloc]:
3、匯編 : Debug -> Debug Workflow -> Always Show Disassembly



我們發(fā)現(xiàn)alloc方法之后走的第一個方法就是libdyld.dylib下的objc_alloc
我們在https://opensource.apple.com/tarballs/ 看到有相應(yīng)的objc4的源碼
本文引用的版本為objc4-781

我們來直接把源碼加到項目中方便調(diào)試觀察內(nèi)部是如何進行的
通過源碼觀察發(fā)現(xiàn)以此走過了一下幾個方法。附上源碼
+ (id)alloc {
return _objc_rootAlloc(self);
}
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
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));
}
這里的slowpath和slowpath不影響其中功能,他們兩個條件分別是fastpath大概率還是slowpath小概率事件,從而讓編譯器對其優(yōu)化處理
_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);
}
_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;
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
// alloc 開辟內(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) {
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);
}
這其中的核心代碼有三部分,也是整個對象初始化最重要的三步
第一步先計算出需要的內(nèi)存空間大小
size = cls->instanceSize(extraBytes);
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;
}
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);
}
}
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
從align16的方法發(fā)現(xiàn)是都是16的倍數(shù)為了字節(jié)對齊。不足16字節(jié)補0,這里要說明一點蘋果早期是8字節(jié)對齊。之后都是16字節(jié)對齊,我們?nèi)魏我粋€對象創(chuàng)建之后都具備一個isa指針占8字節(jié),補齊的意義在于通過對讀取的標準確定來提升讀取速度。補0之后不會越界讀取到其他對象內(nèi)存中。如果我們現(xiàn)在給Person定義一個字符串屬性name占8個字節(jié),那么返回的size還是16,其中8字節(jié)isa,8字節(jié)name,剛好占滿16個字節(jié)。如果再多定義一個nickName屬性的話。這里一共占用24個字節(jié)。但是通過對齊返回32個字節(jié)。所以無論如何分配對象內(nèi)存空間大小一定是16的倍數(shù)。影響size大小的因素就是對象的屬性。
第二步向系統(tǒng)申請開辟內(nèi)存,返回地址指針
obj = (id)calloc(1, size);
第三步關(guān)聯(lián)到相應(yīng)的類(Person)返回對象
obj->initInstanceIsa(cls, hasCxxDtor);
總結(jié)大致流程圖如下

1.2.2 那init呢?
很抱歉的告訴你。其實init什么也沒做就是返回對象,代碼如下
// Replaced by CF (throws an NSException)
+ (id)init {
return (id)self;
}
- (id)init {
return _objc_rootInit(self);
}
那init意義呢?我們經(jīng)??吹絠nitWithXXXX的方法。它其實就是構(gòu)造方法。初始化一些成員變量或者屬性