iOS開發剛開始做的是什么?從初步、比較普遍的結果來看,可能是做一個穩定的好用的App給用戶,這App可以是一個小世界,通過用戶點擊行為等產生一系列的響應,進而成為工具、游戲等。
寫到這里,我們以捏泥巴類比,今天我也想探索下蘋果生態系統下的App是怎么來的,蘋果是依據什么原則去捏對象,進而形成App的呢?
一、 先來一個常見的現象
我們來看下一段代碼:
//YPerson的.h聲明文件
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface YPerson : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, assign) double height;
@property (nonatomic, copy) NSString *name;
@end
NS_ASSUME_NONNULL_END
第二段代碼:
#import "ViewController.h"
#import "YPerson.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSMutableArray *array = [NSMutableArray array];
YPerson *p = [[YPerson alloc] init];
YPerson *p2 = [[YPerson alloc] init];
}
@end
二、方法調用底層初步思考(該用什么工具、從哪里入手):
1. 問題:@selector(array)和@selector(alloc)是否殊途同歸?
以上體現了一個常見的YPerson類的創建方式,即Alloc、init方式,以及對象Array的表面上看非alloc的創建方式,這里先把結果,實際上@selector(array)這個方法最終也是調用的alloc,這是怎么知道的呢?
2.探究工具:符號斷點與混編
這是我們通過這里方法似乎有很多,只講一種我覺得方便的,先斷點調試到目標代碼,如下圖所示:
紅色標記斷點后,因為OC的底層封裝是以動態庫的形式給到開發者的,點擊查詢調試,找不到@selector(array)的實現,進而無法找到array方法的調用鏈,即到這里就沒了:
怎么辦呢,這里講一種比較習慣用的即符號斷點和混編的混用:
走到斷點后,插入symbol(方法名)符號斷點如下:
符號斷點創建好后,點擊繼續往下走箭頭,可以看到混編內容如下:
3.結論:都是走了objc_alloc方法
可以看到,@selector(array)緊接著是走了objc_alloc方法的,同樣的方法調試@selector(alloc),也可以發現是走了objc_alloc方法,的確是一樣的鏈路。
PS:OC有一個非常特性-封裝,很多我們只能看到聲明,看不到實現,這好急啊,那就把墻砸了吧,依次點擊查詢調試是可以看到完整的調用鏈路的,如下提供源代碼鏈接:
objc4-818.2源碼下載
三、alloc干了什么,整體調用鏈路什么樣的:
前面都是鋪墊,這里來到了正主,即捏對象中的關鍵一步,alloc都干了些什么呢?先把結果公布,如下圖所示:
下載源碼對初始化行斷點調試,接著對alloc符號斷點調試,進而objc_alloc斷點調試,進入系統庫斷點后,依次點擊查看調試我們發現如下的調用鏈:
①.objc_alloc
objc_alloc(Class cls)
{
return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
②.callAlloc
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
#warning 咱們只研究OBJC2
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
// No shortcuts available.
if (allocWithZone) {
#warning 基本棄用
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
③._objc_rootAllocWithZone
_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;
#PersonalMark 核心方法1:instanceSize,計算需要多大的內存(捏個對象,估計身高體重,考量需要多少泥巴)
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
#PersonalMark 核心方法2:calloc,根據計算出的內存size來給對象劃分內存,開辟內存空間(已經根據第一步捏出來大致的東西了,需要有放置的地方,有存才能有取)
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
if (!zone && fast) {
#PersonalMark 核心方法3:initInstanceIsa,將創建的對象分類,和對象的class綁定(確定捏的對象的類別,戰士、法師啥的)
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);
}
到這里,結合流程圖及代碼片段,我們已經對Alloc方法在實際的調用鏈路用了較清晰的認知,那么為什么是這樣的呢?去掉繼承、封裝的干擾,那么整體的思路就是什么呢,我想捏個東西搞事情,我得去計算這個東西的用料及成本,并最終捏個雛形。這個工序我認為有很多思路,蘋果的思路非常棒,將它封裝工廠化,所有的對象創建都可以走這套流程,非常方便、規范;
前面也講了,大部分是封裝,核心的東西是計算并捏出來,這個該怎么做呢?
四、Alloc核心代碼挖掘(泥人內存計算、開辟內存、綁定Class)
①.instanceSize(計算對象所需內存)
inline size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
#mark --- 快速計算
return cache.fastInstanceSize(extraBytes);
}
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
#mark --- 最小16字節
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) {
#mark 16字節對齊
return (x + size_t(15)) & ~size_t(15);
}
以上是計算所需內存的代碼,這保證了創建的size是16的整數倍,那么這里為什么要對齊,并且是16字節對齊呢? 首先從對齊的角度來說,要保證在cpu在按塊讀取的時候,每次讀取一樣大小的塊,更方便快捷,所以要對齊; 那么為什么要16字節對齊呢不是8字節或是32字節對齊呢?可以這么理解,一個對象都含有一個isa至少8字節,如果8字節對齊,對象之間會更緊湊,訪問錯誤發生的概率就會變大,而如果以32字節對齊,又會在一定程度上浪費內存,所以這里采取了16字節對齊。
②.calloc(開辟內存,用準備好的泥巴捏個雛形對象)
void *
calloc(size_t num_items, size_t size)
{
return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}
calloc的封裝不深挖了,看方法執行后的結果吧,附圖:
id obj
這句代碼給予obj一個臟地址后(爛泥巴),通過calloc
分配了具體的內存,obj有了最終的地址{這個地址和(YPerson*) $4 = 后跟的地址一樣的
},即我們最終打印對象的地址
③.initInstanceIsa(將對象和類綁定,isa指向明確,泥人的出廠值設定)
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
這句代碼執行后的效果,打印地址如下圖所示:
我們可以看到這個obj已經是我們可以正常調試的對象了,包含了類名、指向的內存地址。
講到這里,已經為對象的創建在底層做了什么講了該大概,那么這里還有些疑問,為何斷點先執行objc_alloc沒直接走alloc?
五、補充(llvm編譯階段對alloc的hook)
看一段非常特別的源碼貼圖:
有事、待補充