面試題
這道面試題如下泣侮,問最后print
方法能不能調(diào)用成功涂滴?如果能最后打印什么?
@interface Person: NSObject
@property (copy,nonatomic) NSString * name;
-(void)print;
@end
@implementation Person
-(void)print{
NSLog(@"my name is %@",self.name);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Person class];
void * obj = &cls;
[(__bridge id)obj print];
}
@end
這是一道非常好的面試題躬贡,主要考察了iOS
底層的函數(shù)調(diào)用機制以及函數(shù)調(diào)用棧的問題。
解答
pint可以調(diào)用成功
首先我們先看print
函數(shù)是否可以調(diào)用成功的問題眼坏,[Person class]
返回的是Person
的類對象,在底層類對象是一個結(jié)構(gòu)體,結(jié)構(gòu)體里首個變量是isa
指針宰译,接下來是superclass
指針檐蚜,cache
的方法緩存指針以及具體的類信息指針,都指向的是具體結(jié)構(gòu)體沿侈,類對象在底層的結(jié)構(gòu)體結(jié)構(gòu)大概如下:
struct objc_class {
Class isa;
Class superclass;
cache_t cache;
class_data_bits bits;
}
struct class_rw_t {
units32_t flags;
units32_t version;
const class_ro_t *ro;
method_list_t *methods;// 方法列表
property_list_t *properties;//屬性列表
const protocol_list_t *protocols;//協(xié)議列表
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
}
struct class_ro_t {
unit32_t flags;
unit32_t instanceStart;
unit32_t instanceSize;
#ifdef __LP64__
unit32_t reserved;
#endif
const unit8_t *ivarLayout;
const char *name;//類名
method_list_t *baseMethodlist;
protocol_list_t *baseProtocols;
const ivar_list_t *ivars;//成員變量列表
const unit8_t *weakIvarLayout;
property_list_t *baseProperties;
}
從上面的代碼可以看出來obj
存放的是cls
的地址闯第,同時cls
的地址指向的是Person
類對象。根據(jù)Runtime
的底層原理缀拭,iOS
的函數(shù)調(diào)用在底層是通過objc_msgSend
函數(shù)來給函數(shù)調(diào)用者發(fā)送消息咳短,objc_msgSend
的執(zhí)行流程有三大階段:
消息發(fā)送
動態(tài)方法解析
消息轉(zhuǎn)發(fā)
這里我們重點看消息發(fā)送階段,下面這張經(jīng)典的圖基本闡述了消息發(fā)送階段:
首先實例通過isa
指針找到類對象蛛淋,查看類對象的方法列表里是否存在對應的方法咙好,如果沒有則通過類對象里面的superclass
找到其父類的類對象,在父類對象的類對象里繼續(xù)查找褐荷,直至到頂層的NSObject
勾效。
那么在回頭看看[(__bridge id)obj print]
的調(diào)用,同樣是消息發(fā)送叛甫,首先找到obj
的isa
指針层宫,從上面的分析可以看出來,obj
的前8
個字節(jié)存放的應該就是isa
指針其监,因為不管是實例對象還是類對象其底層的struct
結(jié)構(gòu)體中萌腿,前8
個字節(jié)就是isa
指針,由于obj
存放的是cls
的地址抖苦,所以這里的isa
其實就是cls
的地址毁菱,而這個isa
指向的是Person
的類對象,所以最后能在Person
類對象的方法列表中找到print
方法睛约。
最后的打印
既然能調(diào)用方法鼎俘,那么最后打印什么呢?其實最后需要確定的是self->_name
這個成員變量的值是什么辩涝。在底層實例對象的成員變量是緊挨著isa
指針的贸伐,在isa
指針的下面的一段連續(xù)的存儲空間中,所以我們需要弄清楚上面的isa
指針緊挨著的8
個字節(jié)的存儲空間中到底是什么怔揩?
椬叫希空間
viewDidLoad
調(diào)用時會開辟一段棧空間在作為函數(shù)調(diào)用的臨時空間商膊,函數(shù)調(diào)用完畢后就回收此空間伏伐,當然函數(shù)調(diào)用時里面的局部變量也是存在這個棧空間里的晕拆,里面的[super viewDidLoad]
會繼續(xù)開辟一段椕牯幔空間,二段棧空間是連續(xù)的吝镣,椀唐鳎空間的回收是先開辟的后回收,這也符合棧數(shù)據(jù)結(jié)構(gòu)的特點末贾,[super viewDidLoad]
方法在底層是通過objc_msgSendSuper2
來調(diào)用的闸溃,其需要接受二個參數(shù):
- struct objc_super2
- SEL
其中objc_super2
結(jié)構(gòu)體如下:
struct objc_super2 {
id receiver;
Class current_class;
}
receiver
是消息接受者,current_class
是receiver
的Class
對象拱撵,由于此結(jié)構(gòu)體要當參數(shù)傳入方法辉川,所以在開辟的棧空間內(nèi)會存放receiver
和current_class
這二個臨時變量拴测,在這里receiver
為self
乓旗,current_class
為ViewController class
脊框。由于椫弁空間是從高地址到地址的,占空間的內(nèi)部大致如下:
最后按照上面尋找成員變量的方式腮郊,跳過isa
指針就是成員變量抄谐,由于上面已經(jīng)分析指導isa
指針就是cls
渺鹦,所以self
就是找到的第一個成員變量,由于person
只有一個成員變量_name
蛹含,所以這里self
就等于_name
這個成員變量毅厚,最后的打印結(jié)果為:my name is <ViewController: 0x13a110720>。
調(diào)試打印
首先我們打印出obj
的地址值浦箱,然后打印后面的連續(xù)4
個8
字節(jié)的地址吸耿,分別打印地址的內(nèi)容:
這很好的證明了cls
后面的8
個字節(jié)存儲的是viewController
,在往后8
個字節(jié)存放的是viewController
的類對象酷窥。
思考
如果代碼為下面這種情況打印什么咽安?
- (void)viewDidLoad {
[super viewDidLoad];
NSString * str = @"mamba";
id cls = [Person class];
void * obj = &cls;
[(__bridge id)obj print];
}
通過上面的分析,str
這個字符串局部變量會緊挨著cls
的地址蓬推,所以最后輸出是my name is mamba,如果注釋掉
[super viewDidLoad]
的調(diào)用沸伏,則會發(fā)生壞內(nèi)存訪問,程序崩潰毅糟。
總結(jié)
本文根據(jù)一個實際的面試題來回復了Runtime
中的函數(shù)調(diào)用消息機制以及函數(shù)調(diào)用棧的相關(guān)知識,通過這個面試題能到加深對iOS
底層知識的理解姆另。