我們先從下面案例看看alloc和init分別做了什么事?
BKPerson *p1 = [BKPerson alloc];
BKPerson *p2 = [p1 init];
BKPerson *p3 = [p1 init];
BKNSLog(@"%@ - %p - %p",p1,p1,&p1);
BKNSLog(@"%@ - %p - %p",p2,p2,&p2);
BKNSLog(@"%@ - %p - %p",p3,p3,&p3);
輸出打印結(jié)果

打印的分別是對象描述、指針指向的地址(即當前指針存放的對象的地址)、當前指針地址(當前指針被存儲在哪里)。
可以看出p1,p2,p3指向了同一個內(nèi)存地址
0x600001138790,而分別使用了3個指針保存這個內(nèi)存地址的值。
猜想:在alloc一步就已經(jīng)創(chuàng)建出了一個對象??
我們再通過objc源碼來驗證這個猜想
源碼探究
一步一步跟進alloc的源碼
- alloc ——> _objc_rootAlloc
+ (id)alloc {
return _objc_rootAlloc(self);
}`
- _objc_rootAlloc——> callAlloc
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
- callAlloc——> _objc_rootAllocWithZone
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));
}
這里面系統(tǒng)用到了兩個宏定義
#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
-
__builtin_expect這個指令是gcc引入的,作用是允許程序員將最有可能執(zhí)行的分支告訴編譯器。這個指令的寫法為:__builtin_expect(EXP, N)。意思是:EXP==N的概率很大。 -
__builtin_expect()是 GCC (version >= 2.96)提供給程序員使用的,目的是將“分支轉(zhuǎn)移”的信息提供給編譯器,這樣編譯器可以對代碼進行優(yōu)化,以減少指令跳轉(zhuǎn)帶來的性能下降。 - 也就是說
fastpath(x)告訴編譯器x值為真的可能性最大。編譯器會更大可能編譯fastpath條件分支里的指令,也就是告訴編譯器很大概率走這條分支。 -
slowpath(x)告訴編譯器x值為假的可能性最大,也就是x為真的可能性很小,當x==0為真才執(zhí)行這個條件分支下的語句。編譯器會更大可能編譯else部分的指令,也就是告訴編譯器很小概率會走if slowpath(x)這條分支的指令。 - 通過這種方式,編譯器在編譯過程中,會將可能性更大的代碼緊跟著前面的代碼,從而減少指令跳轉(zhuǎn)帶來的性能上的下降。
在回到callAlloc方法里,由于這里有幾個分支,可以通過斷點調(diào)試跟方法,看是走的哪一個分支。
在if (fastpath(!cls->ISA()->hasCustomAWZ()))中,cls->ISA()->hasCustomAWZ())判斷一個類是否有自定義的 +allocWithZone 實現(xiàn),所以fastpath(!cls->ISA()->hasCustomAWZ())表示這個類沒有自定義的+allocWithZone 時走這部分代碼。所以執(zhí)行了_objc_rootAllocWithZone(cls, nil);
- _objc_rootAllocWithZone——> _class_createInstanceFromZone
NEVER_INLINE
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);
}
- _class_createInstanceFromZone
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:需要開辟的內(nèi)存大小,可以看到外部傳入的extraBytes為0
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;
}
// 3: 關(guān)聯(lián)到相應的類
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)存大小、申請內(nèi)存返回地址指針、關(guān)聯(lián)到對應的類。下面分析這個三個方法都做了哪些工作
alloc核心方法
1.計算內(nèi)存大小
計算需要開辟的內(nèi)存空間大小是通過size = cls->instanceSize(extraBytes);內(nèi)部實現(xiàn)過程如下
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;
}
在這里可以發(fā)現(xiàn),if (size < 16) size = 16;不夠16個字節(jié),會手動分配16個字節(jié)。之后調(diào)試跟進走的是cache.fastInstanceSize快速計算實例大小的方法。
在fastInstanceSize中會執(zhí)行到align16
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);
}
}
align16的實現(xiàn),可以看到當前使用的是16字節(jié)對齊的方式。
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
圖示16字節(jié)對齊運算

16字節(jié)對齊的目的
-
提高性能,加快存取速度:通常內(nèi)存是由一個個字節(jié)組成的,cpu在存取數(shù)據(jù)時,并不是以字節(jié)為單位存儲,而是以塊為單位存取。頻繁存取字節(jié)未對齊的數(shù)據(jù),會極大降低cpu的性能。固定16字節(jié)的存取長度,可以更快存取數(shù)據(jù)。 -
更安全:蘋果如今采用16字節(jié)對齊,由于在一個對象中,isa占8字節(jié),而對象每個屬性也占8字節(jié),當對象無屬性時,會預留8字節(jié),即16字節(jié)對齊,如果不預留,CPU存取時以16字節(jié)長度會導致訪問到相鄰的其他對象,造成訪問混亂。
在執(zhí)行完cls->instanceSize(extraBytes)就可以打印出計算出的內(nèi)存大小size為16

我們知道OC代碼底層是用C/C++實現(xiàn)的,將OC轉(zhuǎn)成C/C++代碼可以發(fā)現(xiàn),NSObject實際是一個結(jié)構(gòu)體,結(jié)構(gòu)體里只有個isa指針,既然是指針,在64位機環(huán)境下占了8個字節(jié),在32位環(huán)境下占了4個字節(jié)。而NSObject這個結(jié)構(gòu)體內(nèi)部只有這么一個指針,那NSObject本身也是占用8字節(jié)。
struct NSObject_IMPL {
Class isa;
};
可以用runtime的一個函數(shù),獲取類的實例大小
class_getInstanceSize(Class _Nullable cls)
需要引入#import <objc/runtime.h>,打印結(jié)果果然是8個字節(jié)
NSLog(@"InstanceSize:%zd",class_getInstanceSize([NSObject class]));
結(jié)果:InstanceSize:8
我們還能通過導入#import <malloc/malloc.h>,查看到這個函數(shù)
extern size_t malloc_size(const void *ptr);
使用這個函數(shù)打印
NSLog(@"%zd",malloc_size((__bridge const void *)(obejct)));
結(jié)果是 16
由此看出對象本身占用8個字節(jié),而系統(tǒng)開辟了16個字節(jié)用來保存這個對象。
進一步驗證
上面只是NSObject類的情況,而通常類都會有相關(guān)屬性。
給BKPerson類增加4個屬性,兩個字符串類型,兩個int類型
@interface BKPerson : NSObject
@property (nonatomic,strong) NSString *name; // Dog
@property (nonatomic,strong) NSString *nick; // KK
@property (nonatomic) int age;
@property (nonatomic) int hobby;
@end
運行調(diào)試

查看對象內(nèi)存地址

可以看到對象isa指針占據(jù)了8字節(jié),兩個string屬性每個占據(jù)8字節(jié),一共8+8+8=24個字節(jié),但是系統(tǒng)開辟了32字節(jié),也就是有8個字節(jié)為空。
再賦值兩個int屬性,而64位機下int類型占據(jù)了4個字節(jié),可以看到之前空的8個字節(jié),剛好放了age和hobby兩個屬性的值。

結(jié)合以上16字節(jié)對齊分配空間法則,進一步得出結(jié)論:
類的實例占用8字節(jié),而每一次申請的內(nèi)存空間是16字節(jié)。類的實例在第一次申請內(nèi)存空間就申請了包括isa和所有屬性的內(nèi)存空間大小,跟屬性有沒有賦值無關(guān)
2.申請內(nèi)存,返回地址指針
return _class_createInstanceFromZone(cls, 0, nil, OBJECT_CONSTRUCT_CALL_BADALLOC);從第三個參數(shù)可以看到傳入的zone為nil,由于iOS 8以后廢除了用zone開辟內(nèi)存的方式,所以是用obj = (id)calloc(1, size);的方式申請內(nèi)存。這里面size就是我們上面字節(jié)對齊算法得出的內(nèi)存大小。
打印執(zhí)行calloc之后的地址

而這里只打印出了開辟的內(nèi)存地址,沒有類的信息例如
<BKPerson: 0x101019160>,驗證了calloc這一步只是向系統(tǒng)申請內(nèi)存空間。
3.關(guān)聯(lián)相應的類
將地址指針與類相關(guān)聯(lián)
obj->initInstanceIsa(cls, hasCxxDtor);
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
這一步會init一個isa指針,與類關(guān)聯(lián)起來
打印指針描述,可以看到已經(jīng)關(guān)聯(lián)上類了

總結(jié):以上對alloc源碼的探究,可以得知alloc的主要作用就是使用16字節(jié)對齊算法計算內(nèi)存,開辟內(nèi)存,關(guān)聯(lián)類。
alloc的整體流程圖示

那么init幫我們做了什么事呢?
+ (id)init {
return (id)self;
}
- (id)init {
return _objc_rootInit(self);
}
id
_objc_rootInit(id obj)
{
// In practice, it will be hard to rely on this function.
// Many classes do not properly chain -init calls.
return obj;
}
- 由上面源碼可以知曉
init的類方法和對象方法返回的都是對象本身。 - 不同的是類方法返回了一個
id類型的self,這是為了可以給開發(fā)者提供自定義構(gòu)造方法的入口,通過id強轉(zhuǎn)類型實現(xiàn)工廠設計,返回我們定義的類型。
new
我們習慣于用new一個對象,可以更省略代碼,通過源碼可以知道它跟alloc+init的方式本質(zhì)并沒有區(qū)別
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
但是一般開發(fā)中并不建議使用new,主要是工廠設計重載init方法我們?nèi)绻鲆恍I(yè)務的操作,用new初始化則無法執(zhí)行到里面去。