《招聘一個靠譜的 iOS》—參考答案(下)
說明:面試題來源是微博@我就叫Sunny怎么了的這篇博文:《招聘一個靠譜的 iOS》劫瞳,其中共55題,除第一題為糾錯題外秸侣,其他54道均為簡答題笼平。
出題者簡介: 孫源(sunnyxx)立镶,目前就職于百度,負責百度知道 iOS 客戶端的開發(fā)工作司倚,對技術(shù)喜歡刨根問底和總結(jié)最佳實踐豆混,熱愛分享和開源,維護一個叫 forkingdog 的開源小組动知。
答案為微博@iOS程序犭袁整理皿伺,未經(jīng)出題者校對,如有紕漏盒粮,請向微博@iOS程序犭袁指正鸵鸥。
18. 什么時候會報unrecognized selector的異常?
簡單來說:
當調(diào)用該對象上某個方法,而該對象上沒有實現(xiàn)這個方法的時候丹皱,
可以通過“消息轉(zhuǎn)發(fā)”進行解決妒穴。
簡單的流程如下,在上一題中也提到過:
objc是動態(tài)語言摊崭,每個方法在運行時會被動態(tài)轉(zhuǎn)為消息發(fā)送讼油,即:objc_msgSend(receiver, selector)。
objc在向一個對象發(fā)送消息時爽室,runtime庫會根據(jù)對象的isa指針找到該對象實際所屬的類汁讼,然后在該類中的方法列表以及其父類方法列表中尋找方法運行,如果阔墩,在最頂層的父類中依然找不到相應(yīng)的方法時嘿架,程序在運行時會掛掉并拋出異常unrecognized selector sent to XXX 。但是在這之前啸箫,objc的運行時會給出三次拯救程序崩潰的機會:
- Method resolution
objc運行時會調(diào)用+resolveInstanceMethod:
或者 +resolveClassMethod:
耸彪,讓你有機會提供一個函數(shù)實現(xiàn)。如果你添加了函數(shù)忘苛,那運行時系統(tǒng)就會重新啟動一次消息發(fā)送的過程蝉娜,否則 ,運行時就會移到下一步扎唾,消息轉(zhuǎn)發(fā)(Message Forwarding)召川。
- Fast forwarding
如果目標對象實現(xiàn)了-forwardingTargetForSelector:
,Runtime 這時就會調(diào)用這個方法胸遇,給你把這個消息轉(zhuǎn)發(fā)給其他對象的機會荧呐。
只要這個方法返回的不是nil和self,整個消息發(fā)送的過程就會被重啟,當然發(fā)送的對象會變成你返回的那個對象倍阐。否則概疆,就會繼續(xù)Normal Fowarding。
這里叫Fast峰搪,只是為了區(qū)別下一步的轉(zhuǎn)發(fā)機制岔冀。因為這一步不會創(chuàng)建任何新的對象,但下一步轉(zhuǎn)發(fā)會創(chuàng)建一個NSInvocation對象概耻,所以相對更快點使套。
- Normal forwarding
這一步是Runtime最后一次給你挽救的機會。首先它會發(fā)送-methodSignatureForSelector:
消息獲得函數(shù)的參數(shù)和返回值類型咐蚯。如果-methodSignatureForSelector:
返回nil童漩,Runtime則會發(fā)出-doesNotRecognizeSelector:
消息,程序這時也就掛掉了春锋。如果返回了一個函數(shù)簽名矫膨,Runtime就會創(chuàng)建一個NSInvocation對象并發(fā)送-forwardInvocation:
消息給目標對象。
為了能更清晰地理解這些方法的作用期奔,git倉庫里也給出了一個Demo侧馅,名稱叫“ _objc_msgForward_demo
”,可運行起來看看。
19. 一個objc對象如何進行內(nèi)存布局呐萌?(考慮有父類的情況)
- 所有父類的成員變量和自己的成員變量都會存放在該對象所對應(yīng)的存儲空間中.
- 每一個對象內(nèi)部都有一個isa指針,指向他的類對象,類對象中存放著本對象的
- 對象方法列表(對象能夠接收的消息列表馁痴,保存在它所對應(yīng)的類對象中)
- 成員變量的列表,
- 屬性列表,
它內(nèi)部也有一個isa指針指向元對象(meta class),元對象內(nèi)部存放的是類方法列表,類對象內(nèi)部還有一個superclass的指針,指向他的父類對象。
每個 Objective-C 對象都有相同的結(jié)構(gòu)肺孤,如下圖所示:

翻譯過來就是
Objective-C 對象的結(jié)構(gòu)圖 |
---|
ISA指針 |
根類的實例變量 |
倒數(shù)第二層父類的實例變量 |
... |
父類的實例變量 |
類的實例變量 |
根對象就是NSObject罗晕,它的superclass指針指向nil
類對象既然稱為對象,那它也是一個實例赠堵。類對象中也有一個isa指針指向它的元類(meta class)小渊,即類對象是元類的實例。元類內(nèi)部存放的是類方法列表茫叭,根元類的isa指針指向自己酬屉,superclass指針指向NSObject類。
如圖:

20. 一個objc對象的isa的指針指向什么揍愁?有什么作用呐萨?
指向他的類對象,從而可以找到對象上的方法
21. 下面的代碼輸出什么?
@implementation Son : Father
- (id)init
{
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
答案:
都輸出 Son
NSStringFromClass([self class]) = Son
NSStringFromClass([super class]) = Son
這個題目主要是考察關(guān)于 Objective-C 中對 self 和 super 的理解莽囤。
我們都知道:self 是類的隱藏參數(shù)谬擦,指向當前調(diào)用方法的這個類的實例。那 super 呢朽缎?
很多人會想當然的認為“ super 和 self 類似怯屉,應(yīng)該是指向父類的指針吧蔚舀!”。這是很普遍的一個誤區(qū)锨络。其實 super 是一個 Magic Keyword, 它本質(zhì)是一個編譯器標示符狼牺,和 self 是指向的同一個消息接受者羡儿!他們兩個的不同點在于:super 會告訴編譯器,調(diào)用 class 這個方法時是钥,要去父類的方法掠归,而不是本類里的。
上面的例子不管調(diào)用[self class]
還是[super class]
悄泥,接受消息的對象都是當前 Son *xxx
這個對象虏冻。
當使用 self 調(diào)用方法時,會從當前類的方法列表中開始找弹囚,如果沒有厨相,就從父類中再找;而當使用 super 時鸥鹉,則從父類的方法列表中開始找蛮穿。然后調(diào)用父類的這個方法。
這也就是為什么說“不推薦在 init 方法中使用點語法”毁渗,如果想訪問實例變量 iVar 應(yīng)該使用下劃線( _iVar
)践磅,而非點語法( self.iVar
)。
點語法( self.iVar
)的壞處就是子類有可能覆寫 setter 灸异。假設(shè) Person 有一個子類叫 ChenPerson府适,這個子類專門表示那些姓“陳”的人。該子類可能會覆寫 lastName 屬性所對應(yīng)的設(shè)置方法:
//
// ChenPerson.m
//
//
// Created by https://github.com/ChenYilong on 15/8/30.
// Copyright (c) 2015年 http://weibo.com/luohanchenyilong/ 微博@iOS程序犭袁. All rights reserved.
//
#import "ChenPerson.h"
@implementation ChenPerson
@synthesize lastName = _lastName;
- (instancetype)init
{
self = [super init];
if (self) {
NSLog(@"??類名與方法名:%s(在第%d行)肺樟,描述:%@", __PRETTY_FUNCTION__, __LINE__, NSStringFromClass([self class]));
NSLog(@"??類名與方法名:%s(在第%d行)檐春,描述:%@", __PRETTY_FUNCTION__, __LINE__, NSStringFromClass([super class]));
}
return self;
}
- (void)setLastName:(NSString*)lastName
{
//設(shè)置方法一:如果setter采用是這種方式,就可能引起崩潰
// if (![lastName isEqualToString:@"陳"])
// {
// [NSException raise:NSInvalidArgumentException format:@"姓不是陳"];
// }
// _lastName = lastName;
//設(shè)置方法二:如果setter采用是這種方式儡嘶,就可能引起崩潰
_lastName = @"陳";
NSLog(@"??類名與方法名:%s(在第%d行)喇聊,描述:%@", __PRETTY_FUNCTION__, __LINE__, @"會調(diào)用這個方法,想一下為什么?");
}
@end
在基類 Person 的默認初始化方法中蹦狂,可能會將姓氏設(shè)為空字符串誓篱。此時若使用點語法( self.lastName
)也即 setter 設(shè)置方法,那么調(diào)用將會是子類的設(shè)置方法凯楔,如果在剛剛的 setter 代碼中采用設(shè)置方法一窜骄,那么就會拋出異常,
為了方便采用打印的方式展示摆屯,究竟發(fā)生了什么邻遏,我們使用設(shè)置方法二糠亩。
如果基類的代碼是這樣的:
//
// Person.m
// nil對象調(diào)用點語法
//
// Created by https://github.com/ChenYilong on 15/8/29.
// Copyright (c) 2015年 http://weibo.com/luohanchenyilong/ 微博@iOS程序犭袁. All rights reserved.
//
#import "Person.h"
@implementation Person
- (instancetype)init
{
self = [super init];
if (self) {
self.lastName = @"";
//NSLog(@"??類名與方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, NSStringFromClass([self class]));
//NSLog(@"??類名與方法名:%s(在第%d行)准验,描述:%@", __PRETTY_FUNCTION__, __LINE__, self.lastName);
}
return self;
}
- (void)setLastName:(NSString*)lastName
{
NSLog(@"??類名與方法名:%s(在第%d行)赎线,描述:%@", __PRETTY_FUNCTION__, __LINE__, @"根本不會調(diào)用這個方法");
_lastName = @"炎黃";
}
@end
那么打印結(jié)果將會是這樣的:
??類名與方法名:-[ChenPerson setLastName:](在第36行),描述:會調(diào)用這個方法,想一下為什么糊饱?
??類名與方法名:-[ChenPerson init](在第19行)垂寥,描述:ChenPerson
??類名與方法名:-[ChenPerson init](在第20行),描述:ChenPerson
我在倉庫里也給出了一個相應(yīng)的 Demo(名字叫:Demo_21題_下面的代碼輸出什么)另锋。有興趣可以跑起來看一下滞项,主要看下他是怎么打印的,思考下為什么這么打印夭坪。
接下來讓我們利用 runtime 的相關(guān)知識來驗證一下 super 關(guān)鍵字的本質(zhì)文判,使用clang重寫命令:
$ clang -rewrite-objc test.m
將這道題目中給出的代碼被轉(zhuǎn)化為:
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_0, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_1, NSStringFromClass(((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Son")) }, sel_registerName("class"))));
從上面的代碼中,我們可以發(fā)現(xiàn)在調(diào)用 [self class] 時室梅,會轉(zhuǎn)化成 objc_msgSend
函數(shù)戏仓。看下函數(shù)定義:
id objc_msgSend(id self, SEL op, ...)
我們把 self 做為第一個參數(shù)傳遞進去竞惋。
而在調(diào)用 [super class]時柜去,會轉(zhuǎn)化成 objc_msgSendSuper
函數(shù)〔鹜穑看下函數(shù)定義:
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
第一個參數(shù)是 objc_super
這樣一個結(jié)構(gòu)體嗓奢,其定義如下:
struct objc_super {
__unsafe_unretained id receiver;
__unsafe_unretained Class super_class;
};
結(jié)構(gòu)體有兩個成員,第一個成員是 receiver, 類似于上面的 objc_msgSend
函數(shù)第一個參數(shù)self 浑厚。第二個成員是記錄當前類的父類是什么股耽。
所以,當調(diào)用 [self class] 時钳幅,實際先調(diào)用的是 objc_msgSend
函數(shù)物蝙,第一個參數(shù)是 Son當前的這個實例,然后在 Son 這個類里面去找 - (Class)class這個方法敢艰,沒有诬乞,去父類 Father里找,也沒有钠导,最后在 NSObject類中發(fā)現(xiàn)這個方法震嫉。而 - (Class)class的實現(xiàn)就是返回self的類別,故上述輸出結(jié)果為 Son牡属。
objc Runtime開源代碼對- (Class)class方法的實現(xiàn):
- (Class)class {
return object_getClass(self);
}
而當調(diào)用 [super class]
時票堵,會轉(zhuǎn)換成objc_msgSendSuper函數(shù)
。第一步先構(gòu)造 objc_super
結(jié)構(gòu)體逮栅,結(jié)構(gòu)體第一個成員就是 self
悴势。
第二個成員是 (id)class_getSuperclass(objc_getClass(“Son”))
, 實際該函數(shù)輸出結(jié)果為 Father窗宇。
第二步是去 Father這個類里去找 - (Class)class
,沒有特纤,然后去NSObject類去找军俊,找到了。最后內(nèi)部是使用 objc_msgSend(objc_super->receiver, @selector(class))
去調(diào)用捧存,
此時已經(jīng)和[self class]
調(diào)用相同了蝇完,故上述輸出結(jié)果仍然返回 Son。
參考鏈接:微博@Chun_iOS的博文刨根問底Objective-C Runtime(1)- Self & Super
22. runtime如何通過selector找到對應(yīng)的IMP地址矗蕊?(分別考慮類方法和實例方法)
每一個類對象中都一個方法列表,方法列表中記錄著方法的名稱,方法實現(xiàn),以及參數(shù)類型,其實selector本質(zhì)就是方法名稱,通過這個方法名稱就可以在方法列表中找到對應(yīng)的方法實現(xiàn).
23. 使用runtime Associate方法關(guān)聯(lián)的對象,需要在主對象dealloc的時候釋放么氢架?
- 在ARC下不需要傻咖。
- <p><del> 在MRC中,對于使用retain或copy策略的需要 。</del></p>在MRC下也不需要
無論在MRC下還是ARC下均不需要岖研。
2011年版本的Apple API 官方文檔 - Associative References 一節(jié)中有一個MRC環(huán)境下的例子:
// 在MRC下卿操,使用runtime Associate方法關(guān)聯(lián)的對象,不需要在主對象dealloc的時候釋放
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
// https://github.com/ChenYilong
// 摘自2011年版本的Apple API 官方文檔 - Associative References
static char overviewKey;
NSArray *array =
[[NSArray alloc] initWithObjects:@"One", @"Two", @"Three", nil];
// For the purposes of illustration, use initWithFormat: to ensure
// the string can be deallocated
NSString *overview =
[[NSString alloc] initWithFormat:@"%@", @"First three numbers"];
objc_setAssociatedObject (
array,
&overviewKey,
overview,
OBJC_ASSOCIATION_RETAIN
);
[overview release];
// (1) overview valid
[array release];
// (2) overview invalid
文檔指出
At point 1, the string
overview
is still valid because theOBJC_ASSOCIATION_RETAIN
policy specifies that the array retains the associated object. When the array is deallocated, however (at point 2),overview
is released and so in this case also deallocated.
我們可以看到孙援,在[array release];
之后害淤,overview就會被release釋放掉了。
既然會被銷毀拓售,那么具體在什么時間點窥摄?
根據(jù) WWDC 2011, Session 322 (第36分22秒) 中發(fā)布的內(nèi)存銷毀時間表漓摩,被關(guān)聯(lián)的對象在生命周期內(nèi)要比對象本身釋放的晚很多移剪。它們會在被 NSObject -dealloc 調(diào)用的 object_dispose() 方法中釋放杨赤。
對象的內(nèi)存銷毀時間表堕扶,分四個步驟:
// 對象的內(nèi)存銷毀時間表
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
// https://github.com/ChenYilong
// 根據(jù) WWDC 2011, Session 322 (36分22秒)中發(fā)布的內(nèi)存銷毀時間表
1. 調(diào)用 -release :引用計數(shù)變?yōu)榱? * 對象正在被銷毀南用,生命周期即將結(jié)束.
* 不能再有新的 __weak 弱引用李剖, 否則將指向 nil.
* 調(diào)用 [self dealloc]
2. 子類 調(diào)用 -dealloc
* 繼承關(guān)系中最底層的子類 在調(diào)用 -dealloc
* 如果是 MRC 代碼 則會手動釋放實例變量們(iVars)
* 繼承關(guān)系中每一層的父類 都在調(diào)用 -dealloc
3. NSObject 調(diào) -dealloc
* 只做一件事:調(diào)用 Objective-C runtime 中的 object_dispose() 方法
4. 調(diào)用 object_dispose()
* 為 C++ 的實例變量們(iVars)調(diào)用 destructors
* 為 ARC 狀態(tài)下的 實例變量們(iVars) 調(diào)用 -release
* 解除所有使用 runtime Associate方法關(guān)聯(lián)的對象
* 解除所有 __weak 引用
* 調(diào)用 free()
對象的內(nèi)存銷毀時間表:參考鏈接尖飞。
24. objc中的類方法和實例方法有什么本質(zhì)區(qū)別和聯(lián)系琉挖?
類方法:
- 類方法是屬于類對象的
- 類方法只能通過類對象調(diào)用
- 類方法中的self是類對象
- 類方法可以調(diào)用其他的類方法
- 類方法中不能訪問成員變量
- 類方法中不能直接調(diào)用對象方法
實例方法:
- 實例方法是屬于實例對象的
- 實例方法只能通過實例對象調(diào)用
- 實例方法中的self是實例對象
- 實例方法中可以訪問成員變量
- 實例方法中直接調(diào)用實例方法
- 實例方法中也可以調(diào)用類方法(通過類名)
25. _objc_msgForward
函數(shù)是做什么的玻侥,直接調(diào)用它將會發(fā)生什么决摧?
_objc_msgForward
是 IMP 類型,用于消息轉(zhuǎn)發(fā)的:當向一個對象發(fā)送一條消息凑兰,但它并沒有實現(xiàn)的時候掌桩,_objc_msgForward
會嘗試做消息轉(zhuǎn)發(fā)。
我們可以這樣創(chuàng)建一個_objc_msgForward
對象:
IMP msgForwardIMP = _objc_msgForward;
在上篇中的《objc中向一個對象發(fā)送消息[obj foo]
和objc_msgSend()
函數(shù)之間有什么關(guān)系票摇?》曾提到objc_msgSend
在“消息傳遞”中的作用拘鞋。在“消息傳遞”過程中,objc_msgSend
的動作比較清晰:首先在 Class 中的緩存查找 IMP (沒緩存則初始化緩存)矢门,如果沒找到盆色,則向父類的 Class 查找灰蛙。如果一直查找到根類仍舊沒有實現(xiàn),則用_objc_msgForward
函數(shù)指針代替 IMP 隔躲。最后摩梧,執(zhí)行這個 IMP 。
Objective-C運行時是開源的宣旱,所以我們可以看到它的實現(xiàn)仅父。打開 Apple Open Source 里Mac代碼里的obj包 下載一個最新版本,找到 objc-runtime-new.mm
浑吟,進入之后搜索_objc_msgForward
笙纤。
里面有對_objc_msgForward
的功能解釋:
/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup.
* initialize==NO tries to avoid +initialize (but sometimes fails)
* cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use initialize==YES and cache==YES.
* inst is an instance of cls or a subclass thereof, or nil if none is known.
* If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use
* must be converted to _objc_msgForward or _objc_msgForward_stret.
* If you don't want forwarding at all, use lookUpImpOrNil() instead.
**********************************************************************/
對 objc-runtime-new.mm
文件里與_objc_msgForward
有關(guān)的三個函數(shù)使用偽代碼展示下:
// objc-runtime-new.mm 文件里與 _objc_msgForward 有關(guān)的三個函數(shù)使用偽代碼展示
// Created by https://github.com/ChenYilong
// Copyright (c) 微博@iOS程序犭袁(http://weibo.com/luohanchenyilong/). All rights reserved.
// 同時,這也是 obj_msgSend 的實現(xiàn)過程
id objc_msgSend(id self, SEL op, ...) {
if (!self) return nil;
IMP imp = class_getMethodImplementation(self->isa, SEL op);
imp(self, op, ...); //調(diào)用這個函數(shù)组力,偽代碼...
}
//查找IMP
IMP class_getMethodImplementation(Class cls, SEL sel) {
if (!cls || !sel) return nil;
IMP imp = lookUpImpOrNil(cls, sel);
if (!imp) return _objc_msgForward; //_objc_msgForward 用于消息轉(zhuǎn)發(fā)
return imp;
}
IMP lookUpImpOrNil(Class cls, SEL sel) {
if (!cls->initialize()) {
_class_initialize(cls);
}
Class curClass = cls;
IMP imp = nil;
do { //先查緩存,緩存沒有時重建,仍舊沒有則向父類查詢
if (!curClass) break;
if (!curClass->cache) fill_cache(cls, curClass);
imp = cache_getImp(curClass, sel);
if (imp) break;
} while (curClass = curClass->superclass);
return imp;
}
雖然Apple沒有公開_objc_msgForward
的實現(xiàn)源碼省容,但是我們還是能得出結(jié)論:
_objc_msgForward
是一個函數(shù)指針(和 IMP 的類型一樣),是用于消息轉(zhuǎn)發(fā)的:當向一個對象發(fā)送一條消息燎字,但它并沒有實現(xiàn)的時候腥椒,_objc_msgForward
會嘗試做消息轉(zhuǎn)發(fā)。
在上篇中的《objc中向一個對象發(fā)送消息
[obj foo]
和objc_msgSend()
函數(shù)之間有什么關(guān)系候衍?》曾提到objc_msgSend
在“消息傳遞”中的作用笼蛛。在“消息傳遞”過程中,objc_msgSend
的動作比較清晰:首先在 Class 中的緩存查找 IMP (沒緩存則初始化緩存)蛉鹿,如果沒找到滨砍,則向父類的 Class 查找。如果一直查找到根類仍舊沒有實現(xiàn)榨为,則用_objc_msgForward
函數(shù)指針代替 IMP 惨好。最后,執(zhí)行這個 IMP 随闺。
為了展示消息轉(zhuǎn)發(fā)的具體動作日川,這里嘗試向一個對象發(fā)送一條錯誤的消息,并查看一下_objc_msgForward
是如何進行轉(zhuǎn)發(fā)的矩乐。
首先開啟調(diào)試模式龄句、打印出所有運行時發(fā)送的消息:
可以在代碼里執(zhí)行下面的方法:
(void)instrumentObjcMessageSends(YES);
或者斷點暫停程序運行,并在 gdb 中輸入下面的命令:
call (void)instrumentObjcMessageSends(YES)
以第二種為例散罕,操作如下所示:
之后分歇,運行時發(fā)送的所有消息都會打印到/tmp/msgSend-xxxx
文件里了。
終端中輸入命令前往:
open /private/tmp
可能看到有多條欧漱,找到最新生成的职抡,雙擊打開
在模擬器上執(zhí)行執(zhí)行以下語句(這一套調(diào)試方案僅適用于模擬器,真機不可用误甚,關(guān)于該調(diào)試方案的拓展鏈接: Can the messages sent to an object in Objective-C be monitored or printed out? )缚甩,向一個對象發(fā)送一條錯誤的消息:
//
// main.m
// CYLObjcMsgForwardTest
//
// Created by http://weibo.com/luohanchenyilong/.
// Copyright (c) 2015年 微博@iOS程序犭袁. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "CYLTest.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
CYLTest *test = [[CYLTest alloc] init];
[test performSelector:(@selector(iOS程序犭袁))];
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
你可以在/tmp/msgSend-xxxx
(我這一次是/tmp/msgSend-9805
)文件里谱净,看到打印出來:
+ CYLTest NSObject initialize
+ CYLTest NSObject alloc
- CYLTest NSObject init
- CYLTest NSObject performSelector:
+ CYLTest NSObject resolveInstanceMethod:
+ CYLTest NSObject resolveInstanceMethod:
- CYLTest NSObject forwardingTargetForSelector:
- CYLTest NSObject forwardingTargetForSelector:
- CYLTest NSObject methodSignatureForSelector:
- CYLTest NSObject methodSignatureForSelector:
- CYLTest NSObject class
- CYLTest NSObject doesNotRecognizeSelector:
- CYLTest NSObject doesNotRecognizeSelector:
- CYLTest NSObject class
結(jié)合《NSObject官方文檔》,排除掉 NSObject 做的事擅威,剩下的就是_objc_msgForward
消息轉(zhuǎn)發(fā)做的幾件事:
調(diào)用
resolveInstanceMethod:
方法 (或resolveClassMethod:
)壕探。允許用戶在此時為該 Class 動態(tài)添加實現(xiàn)。如果有實現(xiàn)了郊丛,則調(diào)用并返回YES李请,那么重新開始objc_msgSend
流程。這一次對象會響應(yīng)這個選擇器厉熟,一般是因為它已經(jīng)調(diào)用過class_addMethod
导盅。如果仍沒實現(xiàn),繼續(xù)下面的動作揍瑟。調(diào)用
forwardingTargetForSelector:
方法认轨,嘗試找到一個能響應(yīng)該消息的對象。如果獲取到月培,則直接把消息轉(zhuǎn)發(fā)給它,返回非 nil 對象恩急。否則返回 nil 杉畜,繼續(xù)下面的動作。注意衷恭,這里不要返回 self 此叠,否則會形成死循環(huán)。調(diào)用
methodSignatureForSelector:
方法随珠,嘗試獲得一個方法簽名灭袁。如果獲取不到,則直接調(diào)用doesNotRecognizeSelector
拋出異常窗看。如果能獲取茸歧,則返回非nil:創(chuàng)建一個 NSlnvocation 并傳給forwardInvocation:
。調(diào)用
forwardInvocation:
方法显沈,將第3步獲取到的方法簽名包裝成 Invocation 傳入软瞎,如何處理就在這里面了,并返回非ni拉讯。調(diào)用
doesNotRecognizeSelector:
涤浇,默認的實現(xiàn)是拋出異常。如果第3步?jīng)]能獲得一個方法簽名魔慷,執(zhí)行該步驟只锭。
上面前4個方法均是模板方法,開發(fā)者可以override院尔,由 runtime 來調(diào)用蜻展。最常見的實現(xiàn)消息轉(zhuǎn)發(fā):就是重寫方法3和4喉誊,吞掉一個消息或者代理給其他對象都是沒問題的
也就是說_objc_msgForward
在進行消息轉(zhuǎn)發(fā)的過程中會涉及以下這幾個方法:
resolveInstanceMethod:
方法 (或resolveClassMethod:
)。forwardingTargetForSelector:
方法methodSignatureForSelector:
方法forwardInvocation:
方法doesNotRecognizeSelector:
方法
為了能更清晰地理解這些方法的作用铺呵,git倉庫里也給出了一個Demo裹驰,名稱叫“ _objc_msgForward_demo
”,可運行起來看看。
下面回答下第二個問題“直接_objc_msgForward
調(diào)用它將會發(fā)生什么片挂?”
直接調(diào)用_objc_msgForward
是非常危險的事幻林,如果用不好會直接導(dǎo)致程序Crash,但是如果用得好音念,能做很多非郴龋酷的事。
就好像跑酷闷愤,干得好整葡,叫“耍酷”讥脐,干不好就叫“作死”遭居。
正如前文所說:
_objc_msgForward
是 IMP 類型,用于消息轉(zhuǎn)發(fā)的:當向一個對象發(fā)送一條消息旬渠,但它并沒有實現(xiàn)的時候俱萍,_objc_msgForward
會嘗試做消息轉(zhuǎn)發(fā)。
如何調(diào)用_objc_msgForward
告丢?
_objc_msgForward
隸屬 C 語言枪蘑,有三個參數(shù) :
-- |
_objc_msgForward 參數(shù) |
類型 |
---|---|---|
1. | 所屬對象 | id類型 |
2. | 方法名 | SEL類型 |
3. | 可變參數(shù) | 可變參數(shù)類型 |
首先了解下如何調(diào)用 IMP 類型的方法,IMP類型是如下格式:
為了直觀岖免,我們可以通過如下方式定義一個 IMP類型 :
typedef void (*voidIMP)(id, SEL, ...)
一旦調(diào)用_objc_msgForward
岳颇,將跳過查找 IMP 的過程,直接觸發(fā)“消息轉(zhuǎn)發(fā)”颅湘,
如果調(diào)用了_objc_msgForward
话侧,即使這個對象確實已經(jīng)實現(xiàn)了這個方法,你也會告訴objc_msgSend
:
“我沒有在這個對象里找到這個方法的實現(xiàn)”
想象下objc_msgSend
會怎么做闯参?通常情況下掂摔,下面這張圖就是你正常走objc_msgSend
過程,和直接調(diào)用_objc_msgForward
的前后差別:
有哪些場景需要直接調(diào)用_objc_msgForward
赢赊?最常見的場景是:你想獲取某方法所對應(yīng)的NSInvocation
對象乙漓。舉例說明:
JSPatch (Github 鏈接)就是直接調(diào)用_objc_msgForward
來實現(xiàn)其核心功能的:
JSPatch 以小巧的體積做到了讓JS調(diào)用/替換任意OC方法,讓iOS APP具備熱更新的能力释移。
作者的博文《JSPatch實現(xiàn)原理詳解》詳細記錄了實現(xiàn)原理叭披,有興趣可以看下。
同時 RAC(ReactiveCocoa) 源碼中也用到了該方法。
26. runtime如何實現(xiàn)weak變量的自動置nil涩蜘?
runtime 對注冊的類嚼贡, 會進行布局,對于 weak 對象會放入一個 hash 表中同诫。 用 weak 指向的對象內(nèi)存地址作為 key粤策,當此對象的引用計數(shù)為0的時候會 dealloc,假如 weak 指向的對象內(nèi)存地址是a误窖,那么就會以a為鍵叮盘, 在這個 weak 表中搜索,找到所有以a為鍵的 weak 對象霹俺,從而設(shè)置為 nil柔吼。
在上篇中的《runtime 如何實現(xiàn) weak 屬性》有論述。(注:在上篇的《使用runtime Associate方法關(guān)聯(lián)的對象丙唧,需要在主對象dealloc的時候釋放么愈魏?》里給出的“對象的內(nèi)存銷毀時間表”也提到__weak
引用的解除時間。)
我們可以設(shè)計一個函數(shù)(偽代碼)來表示上述機制:
objc_storeWeak(&a, b)
函數(shù):
objc_storeWeak
函數(shù)把第二個參數(shù)--賦值對象(b)的內(nèi)存地址作為鍵值key想际,將第一個參數(shù)--weak修飾的屬性變量(a)的內(nèi)存地址(&a)作為value培漏,注冊到 weak 表中。如果第二個參數(shù)(b)為0(nil)胡本,那么把變量(a)的內(nèi)存地址(&a)從weak表中刪除北苟,
你可以把objc_storeWeak(&a, b)
理解為:objc_storeWeak(value, key)
,并且當key變nil打瘪,將value置nil。
在b非nil時傻昙,a和b指向同一個內(nèi)存地址闺骚,在b變nil時,a變nil妆档。此時向a發(fā)送消息不會崩潰:在Objective-C中向nil發(fā)送消息是安全的僻爽。
而如果a是由assign修飾的,則:
在b非nil時贾惦,a和b指向同一個內(nèi)存地址胸梆,在b變nil時,a還是指向該內(nèi)存地址须板,變野指針碰镜。此時向a發(fā)送消息極易崩潰。
下面我們將基于objc_storeWeak(&a, b)
函數(shù)习瑰,使用偽代碼模擬“runtime如何實現(xiàn)weak屬性”:
// 使用偽代碼模擬:runtime如何實現(xiàn)weak屬性
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
id obj1;
objc_initWeak(&obj1, obj);
/*obj引用計數(shù)變?yōu)?绪颖,變量作用域結(jié)束*/
objc_destroyWeak(&obj1);
下面對用到的兩個方法objc_initWeak
和objc_destroyWeak
做下解釋:
總體說來,作用是:
通過objc_initWeak
函數(shù)初始化“附有weak修飾符的變量(obj1)”甜奄,在變量作用域結(jié)束時通過objc_destoryWeak
函數(shù)釋放該變量(obj1)柠横。
下面分別介紹下方法的內(nèi)部實現(xiàn):
objc_initWeak
函數(shù)的實現(xiàn)是這樣的:在將“附有weak修飾符的變量(obj1)”初始化為0(nil)后窃款,會將“賦值對象”(obj)作為參數(shù),調(diào)用objc_storeWeak
函數(shù)牍氛。
obj1 = 0晨继;
obj_storeWeak(&obj1, obj);
也就是說:
weak 修飾的指針默認值是 nil (在Objective-C中向nil發(fā)送消息是安全的)
然后obj_destroyWeak
函數(shù)將0(nil)作為參數(shù),調(diào)用objc_storeWeak
函數(shù)搬俊。
objc_storeWeak(&obj1, 0);
前面的源代碼與下列源代碼相同紊扬。
// 使用偽代碼模擬:runtime如何實現(xiàn)weak屬性
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
/* ... obj的引用計數(shù)變?yōu)?,被置nil ... */
objc_storeWeak(&obj1, 0);
objc_storeWeak
函數(shù)把第二個參數(shù)--賦值對象(obj)的內(nèi)存地址作為鍵值悠抹,將第一個參數(shù)--weak修飾的屬性變量(obj1)的內(nèi)存地址注冊到 weak 表中珠月。如果第二個參數(shù)(obj)為0(nil),那么把變量(obj1)的地址從weak表中刪除楔敌。
27. 能否向編譯后得到的類中增加實例變量啤挎?能否向運行時創(chuàng)建的類中添加實例變量?為什么卵凑?
- 不能向編譯后得到的類中增加實例變量庆聘;
- 能向運行時創(chuàng)建的類中添加實例變量;
解釋下:
因為編譯后的類已經(jīng)注冊在 runtime 中勺卢,類結(jié)構(gòu)體中的
objc_ivar_list
實例變量的鏈表 和instance_size
實例變量的內(nèi)存大小已經(jīng)確定伙判,同時runtime 會調(diào)用class_setIvarLayout
或class_setWeakIvarLayout
來處理 strong weak 引用。所以不能向存在的類中添加實例變量黑忱;運行時創(chuàng)建的類是可以添加實例變量宴抚,調(diào)用
class_addIvar
函數(shù)。但是得在調(diào)用objc_allocateClassPair
之后甫煞,objc_registerClassPair
之前菇曲,原因同上。
28. runloop和線程有什么關(guān)系抚吠?
總的說來常潮,Run loop,正如其名楷力,loop表示某種循環(huán)喊式,和run放在一起就表示一直在運行著的循環(huán)。實際上萧朝,run loop和線程是緊密相連的岔留,可以這樣說run loop是為了線程而生,沒有線程检柬,它就沒有存在的必要贸诚。Run loops是線程的基礎(chǔ)架構(gòu)部分, Cocoa 和 CoreFundation 都提供了 run loop 對象方便配置和管理線程的 run loop (以下都以 Cocoa 為例)。每個線程酱固,包括程序的主線程( main thread )都有與之相應(yīng)的 run loop 對象械念。
runloop 和線程的關(guān)系:
- 主線程的run loop默認是啟動的。
iOS的應(yīng)用程序里面运悲,程序啟動后會有一個如下的main()函數(shù)
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
重點是UIApplicationMain()函數(shù)龄减,這個方法會為main thread設(shè)置一個NSRunLoop對象,這就解釋了:為什么我們的應(yīng)用可以在無人操作的時候休息班眯,需要讓它干活的時候又能立馬響應(yīng)希停。
對其它線程來說,run loop默認是沒有啟動的署隘,如果你需要更多的線程交互則可以手動配置和啟動宠能,如果線程只是去執(zhí)行一個長時間的已確定的任務(wù)則不需要。
在任何一個 Cocoa 程序的線程中磁餐,都可以通過以下代碼來獲取到當前線程的 run loop 违崇。
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
參考鏈接:《Objective-C之run loop詳解》。
29. runloop的mode作用是什么诊霹?
model 主要是用來指定事件在運行循環(huán)中的優(yōu)先級的羞延,分為:
- NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默認,空閑狀態(tài)
- UITrackingRunLoopMode:ScrollView滑動時
- UIInitializationRunLoopMode:啟動時
- NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合
蘋果公開提供的 Mode 有兩個:
- NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
- NSRunLoopCommonModes(kCFRunLoopCommonModes)
30. 以+ scheduledTimerWithTimeInterval...的方式觸發(fā)的timer脾还,在滑動頁面上的列表時伴箩,timer會暫定回調(diào),為什么鄙漏?如何解決嗤谚?
RunLoop只能運行在一種mode下,如果要換mode怔蚌,當前的loop也需要停下重啟成新的巩步。利用這個機制,ScrollView滾動過程中NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的mode會切換到UITrackingRunLoopMode來保證ScrollView的流暢滑動:只能在NSDefaultRunLoopMode模式下處理的事件會影響ScrollView的滑動媚创。
如果我們把一個NSTimer對象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主運行循環(huán)中的時候,
ScrollView滾動過程中會因為mode的切換,而導(dǎo)致NSTimer將不再被調(diào)度彤恶。
同時因為mode還是可定制的钞钙,所以:
Timer計時會被scrollView的滑動影響的問題可以通過將timer添加到NSRunLoopCommonModes(kCFRunLoopCommonModes)來解決。代碼如下:
//
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
// https://github.com/ChenYilong
//將timer添加到NSDefaultRunLoopMode中
[NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(timerTick:)
userInfo:nil
repeats:YES];
//然后再添加到NSRunLoopCommonModes里
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0
target:self
selector:@selector(timerTick:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
31. 猜想runloop內(nèi)部是如何實現(xiàn)的声离?
一般來講芒炼,一個線程一次只能執(zhí)行一個任務(wù),執(zhí)行完成后線程就會退出术徊。如果我們需要一個機制本刽,讓線程能隨時處理事件但并不退出,通常的代碼邏輯
是這樣的:
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
或使用偽代碼來展示下:
//
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
// https://github.com/ChenYilong
int main(int argc, char * argv[]) {
//程序一直運行狀態(tài)
while (AppIsRunning) {
//睡眠狀態(tài),等待喚醒事件
id whoWakesMe = SleepForWakingUp();
//得到喚醒事件
id event = GetEvent(whoWakesMe);
//開始處理事件
HandleEvent(event);
}
return 0;
}
參考鏈接:
- 《深入理解RunLoop》
- 摘自博文CFRunLoop子寓,原作者是微博@我就叫Sunny怎么了
32. objc使用什么機制管理對象內(nèi)存暗挑?
通過 retainCount 的機制來決定對象是否需要釋放。
每次 runloop 的時候斜友,都會檢查對象的 retainCount炸裆,如果retainCount 為 0,說明該對象沒有地方需要繼續(xù)使用了鲜屏,可以釋放掉了烹看。
33. ARC通過什么方式幫助開發(fā)者管理內(nèi)存?
<p><del>編譯時根據(jù)代碼上下文洛史,插入 retain/release
</del></p>
ARC相對于MRC惯殊,不是在編譯時添加retain/release/autorelease這么簡單。應(yīng)該是編譯期和運行期兩部分共同幫助開發(fā)者管理內(nèi)存也殖。
在編譯期土思,ARC用的是更底層的C接口實現(xiàn)的retain/release/autorelease,這樣做性能更好毕源,也是為什么不能在ARC環(huán)境下手動retain/release/autorelease浪漠,同時對同一上下文的同一對象的成對retain/release操作進行優(yōu)化(即忽略掉不必要的操作);ARC也包含運行期組件霎褐,這個地方做的優(yōu)化比較復(fù)雜址愿,但也不能被忽略《沉В【TODO:后續(xù)更新會詳細描述下】
34. 不手動指定autoreleasepool的前提下响谓,一個autorealese對象在什么時刻釋放?(比如在一個vc的viewDidLoad中創(chuàng)建)
分兩種情況:手動干預(yù)釋放時機省艳、系統(tǒng)自動去釋放娘纷。
- 手動干預(yù)釋放時機--指定autoreleasepool
就是所謂的:當前作用域大括號結(jié)束時釋放。 - 系統(tǒng)自動去釋放--不手動指定autoreleasepool
Autorelease對象出了作用域之后跋炕,會被添加到最近一次創(chuàng)建的自動釋放池中赖晶,并會在當前的 runloop 迭代結(jié)束時釋放。
釋放的時機總結(jié)起來辐烂,可以用下圖來表示:
下面對這張圖進行詳細的解釋:
從程序啟動到加載完成是一個完整的運行循環(huán)遏插,然后會停下來,等待用戶交互纠修,用戶的每一次交互都會啟動一次運行循環(huán)胳嘲,來處理用戶所有的點擊事件、觸摸事件扣草。
我們都知道:
所有 autorelease 的對象了牛,在出了作用域之后颜屠,會被自動添加到最近創(chuàng)建的自動釋放池中。
但是如果每次都放進應(yīng)用程序的 main.m
中的 autoreleasepool 中鹰祸,遲早有被撐滿的一刻甫窟。這個過程中必定有一個釋放的動作。何時福荸?
在一次完整的運行循環(huán)結(jié)束之前蕴坪,會被銷毀。
那什么時間會創(chuàng)建自動釋放池敬锐?運行循環(huán)檢測到事件并啟動后背传,就會創(chuàng)建自動釋放池。
子線程的 runloop 默認是不工作台夺,無法主動創(chuàng)建径玖,必須手動創(chuàng)建。
自定義的 NSOperation 和 NSThread 需要手動創(chuàng)建自動釋放池颤介。比如: 自定義的 NSOperation 類中的 main 方法里就必須添加自動釋放池梳星。否則出了作用域后,自動釋放對象會因為沒有自動釋放池去處理它滚朵,而造成內(nèi)存泄露冤灾。
但對于 blockOperation 和 invocationOperation 這種默認的Operation ,系統(tǒng)已經(jīng)幫我們封裝好了辕近,不需要手動創(chuàng)建自動釋放池韵吨。
@autoreleasepool 當自動釋放池被銷毀或者耗盡時,會向自動釋放池中的所有對象發(fā)送 release 消息移宅,釋放自動釋放池中的所有對象归粉。
如果在一個vc的viewDidLoad中創(chuàng)建一個 Autorelease對象带射,那么該對象會在 viewDidAppear 方法執(zhí)行前就被銷毀了腻扇。
參考鏈接:《黑幕背后的Autorelease》
35. BAD_ACCESS在什么情況下出現(xiàn)?
訪問了野指針莉兰,比如對一個已經(jīng)釋放的對象執(zhí)行了release浅乔、訪問已經(jīng)釋放對象的成員變量或者發(fā)消息倔喂。
死循環(huán)
36. 蘋果是如何實現(xiàn)autoreleasepool的?
autoreleasepool 以一個隊列數(shù)組的形式實現(xiàn),主要通過下列三個函數(shù)完成.
objc_autoreleasepoolPush
objc_autoreleasepoolPop
objc_autorelease
看函數(shù)名就可以知道靖苇,對 autorelease 分別執(zhí)行 push席噩,和 pop 操作。銷毀對象時執(zhí)行release操作顾复。
舉例說明:我們都知道用類方法創(chuàng)建的對象都是 Autorelease 的班挖,那么一旦 Person 出了作用域鲁捏,當在 Person 的 dealloc 方法中打上斷點芯砸,我們就可以看到這樣的調(diào)用堆棧信息:
37. 使用block時什么情況會發(fā)生引用循環(huán)萧芙,如何解決?
一個對象中強引用了block假丧,在block中又強引用了該對象双揪,就會發(fā)射循環(huán)引用。
解決方法是將該對象使用__weak或者__block修飾符修飾之后再在block中使用包帚。
- id weak weakSelf = self;
或者 weak __typeof(&*self)weakSelf = self該方法可以設(shè)置宏 - id __block weakSelf = self;
或者將其中一方強制制空 xxx = nil
渔期。
檢測代碼中是否存在循環(huán)引用問題,可使用 Facebook 開源的一個檢測工具 FBRetainCycleDetector 渴邦。
38. 在block內(nèi)如何修改block外部變量疯趟?
默認情況下,在block中訪問的外部變量是復(fù)制過去的谋梭,即:寫操作不對原變量生效信峻。但是你可以加上 __block
來讓其寫操作生效,示例代碼如下:
__block int a = 0;
void (^foo)(void) = ^{
a = 1;
};
foo();
//這里瓮床,a的值被修改為1
這是 微博@唐巧_boy的《iOS開發(fā)進階》中的第11.2.3章節(jié)中的描述盹舞。你同樣可以在面試中這樣回答,但你并沒有答到“點子上”隘庄。真正的原因踢步,并沒有書這本書里寫的這么“神奇”,而且這種說法也有點牽強丑掺。面試官肯定會追問“為什么寫操作就生效了获印?”真正的原因是這樣的:
我們都知道:Block不允許修改外部變量的值,這里所說的外部變量的值吼鱼,指的是棧中指針的內(nèi)存地址蓬豁。
__block
所起到的作用就是只要觀察到該變量被 block 所持有,就將“外部變量”在棧中的內(nèi)存地址放到了堆中菇肃。進而在block內(nèi)部也可以修改外部變量的值地粪。
Block不允許修改外部變量的值。Apple這樣設(shè)計琐谤,應(yīng)該是考慮到了block的特殊性蟆技,block也屬于“函數(shù)”的范疇,變量進入block斗忌,實際就是已經(jīng)改變了作用域质礼。在幾個作用域之間進行切換時,如果不加上這樣的限制织阳,變量的可維護性將大大降低眶蕉。又比如我想在block內(nèi)聲明了一個與外部同名的變量,此時是允許呢還是不允許呢唧躲?只有加上了這樣的限制造挽,這樣的情景才能實現(xiàn)碱璃。于是棧區(qū)變成了紅燈區(qū),堆區(qū)變成了綠燈區(qū)饭入。
我們可以打印下內(nèi)存地址來進行驗證:
__block int a = 0;
NSLog(@"定義前:%p", &a); //棧區(qū)
void (^foo)(void) = ^{
a = 1;
NSLog(@"block內(nèi)部:%p", &a); //堆區(qū)
};
NSLog(@"定義后:%p", &a); //堆區(qū)
foo();
2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] 定義前:0x16fda86f8
2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] 定義后:0x155b22fc8
2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] block內(nèi)部: 0x155b22fc8
“定義后”和“block內(nèi)部”兩者的內(nèi)存地址是一樣的嵌器,我們都知道 block 內(nèi)部的變量會被 copy 到堆區(qū),“block內(nèi)部”打印的是堆地址谐丢,因而也就可以知道爽航,“定義后”打印的也是堆的地址。
那么如何證明“block內(nèi)部”打印的是堆地址乾忱?
把三個16進制的內(nèi)存地址轉(zhuǎn)成10進制就是:
- 定義后前:6171559672
- block內(nèi)部:5732708296
- 定義后后:5732708296
中間相差438851376個字節(jié)讥珍,也就是 418.5M 的空間,因為堆地址要小于棧地址窄瘟,又因為iOS中一個進程的棧區(qū)內(nèi)存只有1M串述,Mac也只有8M,顯然a已經(jīng)是在堆區(qū)了寞肖。
這也證實了:a 在定義前是棧區(qū)纲酗,但只要進入了 block 區(qū)域,就變成了堆區(qū)新蟆。這才是 __block
關(guān)鍵字的真正作用觅赊。
__block
關(guān)鍵字修飾后,int類型也從4字節(jié)變成了32字節(jié)琼稻,這是 Foundation 框架 malloc 出來的吮螺。這也同樣能證實上面的結(jié)論。(PS:居然比 NSObject alloc 出來的 16 字節(jié)要多一倍)帕翻。
理解到這是因為堆棧地址的變更鸠补,而非所謂的“寫操作生效”,這一點至關(guān)重要嘀掸,要不然你如何解釋下面這個現(xiàn)象:
以下代碼編譯可以通過紫岩,并且在block中成功將a的從Tom修改為Jerry。
NSMutableString *a = [NSMutableString stringWithString:@"Tom"];
NSLog(@"\n 定以前:------------------------------------\n\
a指向的堆中地址:%p睬塌;a在棧中的指針地址:%p", a, &a); //a在棧區(qū)
void (^foo)(void) = ^{
a.string = @"Jerry";
NSLog(@"\n block內(nèi)部:------------------------------------\n\
a指向的堆中地址:%p泉蝌;a在棧中的指針地址:%p", a, &a); //a在棧區(qū)
a = [NSMutableString stringWithString:@"William"];
};
foo();
NSLog(@"\n 定以后:------------------------------------\n\
a指向的堆中地址:%p;a在棧中的指針地址:%p", a, &a); //a在棧區(qū)
這里的a已經(jīng)由基本數(shù)據(jù)類型揩晴,變成了對象類型勋陪。block會對對象類型的指針進行copy,copy到堆中硫兰,但并不會改變該指針所指向的堆中的地址诅愚,所以在上面的示例代碼中,block體內(nèi)修改的實際是a指向的堆中的內(nèi)容劫映。
但如果我們嘗試像上面圖片中的65行那樣做违孝,結(jié)果會編譯不通過壕曼,那是因為此時你在修改的就不是堆中的內(nèi)容,而是棧中的內(nèi)容等浊。
上文已經(jīng)說過:Block不允許修改外部變量的值,這里所說的外部變量的值摹蘑,指的是棧中指針的內(nèi)存地址筹燕。棧區(qū)是紅燈區(qū),堆區(qū)才是綠燈區(qū)衅鹿。
39. 使用系統(tǒng)的某些block api(如UIView的block版本寫動畫時)撒踪,是否也考慮引用循環(huán)問題?
系統(tǒng)的某些block api中大渤,UIView的block版本寫動畫時不需要考慮制妄,但也有一些api 需要考慮:
所謂“引用循環(huán)”是指雙向的強引用,所以那些“單向的強引用”(block 強引用 self )沒有問題泵三,比如這些:
[UIView animateWithDuration:duration animations:^{ [self.superview layoutIfNeeded]; }];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.someProperty = xyz; }];
[[NSNotificationCenter defaultCenter] addObserverForName:@"someNotification"
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification * notification) {
self.someProperty = xyz; }];
這些情況不需要考慮“引用循環(huán)”耕捞。
但如果你使用一些參數(shù)中可能含有 ivar 的系統(tǒng) api ,如 GCD 烫幕、NSNotificationCenter就要小心一點:比如GCD 內(nèi)部如果引用了 self俺抽,而且 GCD 的其他參數(shù)是 ivar,則要考慮到循環(huán)引用:
__weak __typeof__(self) weakSelf = self;
dispatch_group_async(_operationsGroup, _operationsQueue, ^
{
__typeof__(self) strongSelf = weakSelf;
[strongSelf doSomething];
[strongSelf doSomethingElse];
} );
類似的:
__weak __typeof__(self) weakSelf = self;
_observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey"
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
__typeof__(self) strongSelf = weakSelf;
[strongSelf dismissModalViewControllerAnimated:YES];
}];
self --> _observer --> block --> self 顯然這也是一個循環(huán)引用较曼。
檢測代碼中是否存在循環(huán)引用問題磷斧,可使用 Facebook 開源的一個檢測工具 FBRetainCycleDetector 。
40. GCD的隊列(dispatch_queue_t
)分哪兩種類型捷犹?
- 串行隊列Serial Dispatch Queue
- 并行隊列Concurrent Dispatch Queue
41. 如何用GCD同步若干個異步調(diào)用弛饭?(如根據(jù)若干個url異步加載多張圖片,然后在都下載完成后合成一張整圖)
使用Dispatch Group追加block到Global Group Queue,這些block如果全部執(zhí)行完畢萍歉,就會執(zhí)行Main Dispatch Queue中的結(jié)束處理的block侣颂。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{ /*加載圖片1 */ });
dispatch_group_async(group, queue, ^{ /*加載圖片2 */ });
dispatch_group_async(group, queue, ^{ /*加載圖片3 */ });
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 合并圖片
});
42. dispatch_barrier_async
的作用是什么?
在并行隊列中枪孩,為了保持某些任務(wù)的順序横蜒,需要等待一些任務(wù)完成后才能繼續(xù)進行,使用 barrier 來等待之前任務(wù)完成销凑,避免數(shù)據(jù)競爭等問題丛晌。
dispatch_barrier_async
函數(shù)會等待追加到Concurrent Dispatch Queue并行隊列中的操作全部執(zhí)行完之后,然后再執(zhí)行 dispatch_barrier_async
函數(shù)追加的處理斗幼,等 dispatch_barrier_async
追加的處理執(zhí)行結(jié)束之后澎蛛,Concurrent Dispatch Queue才恢復(fù)之前的動作繼續(xù)執(zhí)行。
打個比方:比如你們公司周末跟團旅游蜕窿,高速休息站上谋逻,司機說:大家都去上廁所呆馁,速戰(zhàn)速決,上完廁所就上高速毁兆。超大的公共廁所浙滤,大家同時去,程序猿很快就結(jié)束了气堕,但程序媛就可能會慢一些纺腊,即使你第一個回來,司機也不會出發(fā)茎芭,司機要等待所有人都回來后揖膜,才能出發(fā)。 dispatch_barrier_async
函數(shù)追加的內(nèi)容就如同 “上完廁所就上高速”這個動作梅桩。
(注意:使用 dispatch_barrier_async
壹粟,該函數(shù)只能搭配自定義并行隊列 dispatch_queue_t
使用。不能使用: dispatch_get_global_queue
宿百,否則 dispatch_barrier_async
的作用會和 dispatch_async
的作用一模一樣趁仙。 )
43. 蘋果為什么要廢棄dispatch_get_current_queue
?
dispatch_get_current_queue
容易造成死鎖
44. 以下代碼運行結(jié)果如何垦页?
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(@"1");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2");
});
NSLog(@"3");
}
只輸出:1 幸撕。發(fā)生主線程鎖死。
45. addObserver:forKeyPath:options:context:各個參數(shù)的作用分別是什么外臂,observer中需要實現(xiàn)哪個方法才能獲得KVO回調(diào)坐儿?
// 添加鍵值觀察
/*
1 觀察者,負責處理監(jiān)聽事件的對象
2 觀察的屬性
3 觀察的選項
4 上下文
*/
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"Person Name"];
observer中需要實現(xiàn)一下方法:
// 所有的 kvo 監(jiān)聽到事件宋光,都會調(diào)用此方法
/*
1. 觀察的屬性
2. 觀察的對象
3. change 屬性變化字典(新/舊)
4. 上下文貌矿,與監(jiān)聽的時候傳遞的一致
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
46. 如何手動觸發(fā)一個value的KVO
所謂的“手動觸發(fā)”是區(qū)別于“自動觸發(fā)”:
自動觸發(fā)是指類似這種場景:在注冊 KVO 之前設(shè)置一個初始值,注冊之后罪佳,設(shè)置一個不一樣的值逛漫,就可以觸發(fā)了。
想知道如何手動觸發(fā)赘艳,必須知道自動觸發(fā) KVO 的原理:
鍵值觀察通知依賴于 NSObject 的兩個方法: willChangeValueForKey:
和 didChangevlueForKey:
酌毡。在一個被觀察屬性發(fā)生改變之前, willChangeValueForKey:
一定會被調(diào)用蕾管,這就
會記錄舊的值枷踏。而當改變發(fā)生后, observeValueForKey:ofObject:change:context:
會被調(diào)用掰曾,繼而 didChangeValueForKey:
也會被調(diào)用旭蠕。如果可以手動實現(xiàn)這些調(diào)用,就可以實現(xiàn)“手動觸發(fā)”了。
那么“手動觸發(fā)”的使用場景是什么掏熬?一般我們只在希望能控制“回調(diào)的調(diào)用時機”時才會這么做佑稠。
具體做法如下:
如果這個 value
是 表示時間的 self.now
,那么代碼如下:最后兩行代碼缺一不可旗芬。
相關(guān)代碼已放在倉庫里舌胶。
// .m文件
// Created by https://github.com/ChenYilong
// 微博@iOS程序犭袁(http://weibo.com/luohanchenyilong/).
// 手動觸發(fā) value 的KVO,最后兩行代碼缺一不可疮丛。
//@property (nonatomic, strong) NSDate *now;
- (void)viewDidLoad {
[super viewDidLoad];
_now = [NSDate date];
[self addObserver:self forKeyPath:@"now" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"1");
[self willChangeValueForKey:@"now"]; // “手動觸發(fā)self.now的KVO”幔嫂,必寫。
NSLog(@"2");
[self didChangeValueForKey:@"now"]; // “手動觸發(fā)self.now的KVO”这刷,必寫。
NSLog(@"4");
}
但是平時我們一般不會這么干娩井,我們都是等系統(tǒng)去“自動觸發(fā)”暇屋。“自動觸發(fā)”的實現(xiàn)原理:
比如調(diào)用
setNow:
時洞辣,系統(tǒng)還會以某種方式在中間插入wilChangeValueForKey:
咐刨、didChangeValueForKey:
和observeValueForKeyPath:ofObject:change:context:
的調(diào)用。
大家可能以為這是因為 setNow:
是合成方法扬霜,有時候我們也能看到有人這么寫代碼:
- (void)setNow:(NSDate *)aDate {
[self willChangeValueForKey:@"now"]; // 沒有必要
_now = aDate;
[self didChangeValueForKey:@"now"];// 沒有必要
}
這完全沒有必要定鸟,不要這么做,這樣的話著瓶,KVO代碼會被調(diào)用兩次联予。KVO在調(diào)用存取方法之前總是調(diào)用 willChangeValueForKey:
,之后總是調(diào)用 didChangeValueForkey:
材原。怎么做到的呢?答案是通過 isa 混寫(isa-swizzling)沸久。下文《apple用什么方式實現(xiàn)對一個對象的KVO?》會有詳述余蟹。
參考鏈接: Manual Change Notification---Apple 官方文檔
47. 若一個類有實例變量 NSString *_foo
卷胯,調(diào)用setValue:forKey:時,可以以foo還是 _foo
作為key威酒?
都可以窑睁。
48. KVC的keyPath中的集合運算符如何使用?
- 必須用在集合對象上或普通對象的集合屬性上
- 簡單集合運算符有@avg葵孤, @count 担钮, @max , @min 尤仍,@sum裳朋,
- 格式 @"@sum.age"或 @"集合屬性.@max.age"
49. KVC和KVO的keyPath一定是屬性么?
KVO支持實例變量
50. 如何關(guān)閉默認的KVO的默認實現(xiàn),并進入自定義的KVO實現(xiàn)鲤嫡?
請參考:
51. apple用什么方式實現(xiàn)對一個對象的KVO送挑?
Apple 的文檔對 KVO 實現(xiàn)的描述:
Automatic key-value observing is implemented using a technique called isa-swizzling... When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class ...
從Apple 的文檔可以看出:Apple 并不希望過多暴露 KVO 的實現(xiàn)細節(jié)。不過暖眼,要是借助 runtime 提供的方法去深入挖掘惕耕,所有被掩蓋的細節(jié)都會原形畢露:
當你觀察一個對象時,一個新的類會被動態(tài)創(chuàng)建诫肠。這個類繼承自該對象的原本的類司澎,并重寫了被觀察屬性的 setter 方法。重寫的 setter 方法會負責在調(diào)用原 setter 方法之前和之后栋豫,通知所有觀察對象:值的更改挤安。最后通過
isa 混寫(isa-swizzling)
把這個對象的 isa 指針 ( isa 指針告訴 Runtime 系統(tǒng)這個對象的類是什么 ) 指向這個新創(chuàng)建的子類,對象就神奇的變成了新創(chuàng)建的子類的實例丧鸯。我畫了一張示意圖蛤铜,如下所示:
KVO 確實有點黑魔法:
Apple 使用了
isa 混寫(isa-swizzling)
來實現(xiàn) KVO 。
下面做下詳細解釋:
鍵值觀察通知依賴于 NSObject 的兩個方法: willChangeValueForKey:
和 didChangevlueForKey:
丛肢。在一個被觀察屬性發(fā)生改變之前围肥, willChangeValueForKey:
一定會被調(diào)用,這就會記錄舊的值蜂怎。而當改變發(fā)生后穆刻, observeValueForKey:ofObject:change:context:
會被調(diào)用,繼而 didChangeValueForKey:
也會被調(diào)用杠步∏馕埃可以手動實現(xiàn)這些調(diào)用,但很少有人這么做幽歼。一般我們只在希望能控制回調(diào)的調(diào)用時機時才會這么做腐芍。大部分情況下,改變通知會自動調(diào)用试躏。
比如調(diào)用 setNow:
時猪勇,系統(tǒng)還會以某種方式在中間插入 wilChangeValueForKey:
、 didChangeValueForKey:
和 observeValueForKeyPath:ofObject:change:context:
的調(diào)用颠蕴。大家可能以為這是因為 setNow:
是合成方法泣刹,有時候我們也能看到有人這么寫代碼:
- (void)setNow:(NSDate *)aDate {
[self willChangeValueForKey:@"now"]; // 沒有必要
_now = aDate;
[self didChangeValueForKey:@"now"];// 沒有必要
}
這完全沒有必要,不要這么做犀被,這樣的話椅您,KVO代碼會被調(diào)用兩次。KVO在調(diào)用存取方法之前總是調(diào)用 willChangeValueForKey:
寡键,之后總是調(diào)用 didChangeValueForkey:
掀泳。怎么做到的呢?答案是通過 isa 混寫(isa-swizzling)。第一次對一個對象調(diào)用 addObserver:forKeyPath:options:context:
時,框架會創(chuàng)建這個類的新的 KVO 子類员舵,并將被觀察對象轉(zhuǎn)換為新子類的對象脑沿。在這個 KVO 特殊子類中, Cocoa 創(chuàng)建觀察屬性的 setter 马僻,大致工作原理如下:
- (void)setNow:(NSDate *)aDate {
[self willChangeValueForKey:@"now"];
[super setValue:aDate forKey:@"now"];
[self didChangeValueForKey:@"now"];
}
這種繼承和方法注入是在運行時而不是編譯時實現(xiàn)的庄拇。這就是正確命名如此重要的原因。只有在使用KVC命名約定時韭邓,KVO才能做到這一點措近。
KVO 在實現(xiàn)中通過 isa 混寫(isa-swizzling)
把這個對象的 isa 指針 ( isa 指針告訴 Runtime 系統(tǒng)這個對象的類是什么 ) 指向這個新創(chuàng)建的子類,對象就神奇的變成了新創(chuàng)建的子類的實例女淑。這在Apple 的文檔可以得到印證:
Automatic key-value observing is implemented using a technique called isa-swizzling... When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class ...
然而 KVO 在實現(xiàn)中使用了 isa 混寫( isa-swizzling)
瞭郑,這個的確不是很容易發(fā)現(xiàn):Apple 還重寫、覆蓋了 -class
方法并返回原來的類鸭你。 企圖欺騙我們:這個類沒有變屈张,就是原本那個類。苇本。袜茧。
但是菜拓,假設(shè)“被監(jiān)聽的對象”的類對象是 MYClass
瓣窄,有時候我們能看到對 NSKVONotifying_MYClass
的引用而不是對 MYClass
的引用。借此我們得以知道 Apple 使用了 isa 混寫(isa-swizzling)
纳鼎。具體探究過程可參考 這篇博文 俺夕。
那么 wilChangeValueForKey:
、 didChangeValueForKey:
和 observeValueForKeyPath:ofObject:change:context:
這三個方法的執(zhí)行順序是怎樣的呢贱鄙?
wilChangeValueForKey:
劝贸、 didChangeValueForKey:
很好理解,observeValueForKeyPath:ofObject:change:context:
的執(zhí)行時機是什么時候呢逗宁?
先看一個例子:
代碼已放在倉庫里映九。
- (void)viewDidLoad {
[super viewDidLoad];
[self addObserver:self forKeyPath:@"now" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"1");
[self willChangeValueForKey:@"now"]; // “手動觸發(fā)self.now的KVO”,必寫瞎颗。
NSLog(@"2");
[self didChangeValueForKey:@"now"]; // “手動觸發(fā)self.now的KVO”件甥,必寫。
NSLog(@"4");
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
NSLog(@"3");
}
如果單單從下面這個例子的打印上哼拔,
順序似乎是 wilChangeValueForKey:
引有、 observeValueForKeyPath:ofObject:change:context:
、 didChangeValueForKey:
倦逐。
其實不然譬正,這里有一個 observeValueForKeyPath:ofObject:change:context:
, 和 didChangeValueForKey:
到底誰先調(diào)用的問題:如果 observeValueForKeyPath:ofObject:change:context:
是在 didChangeValueForKey:
內(nèi)部觸發(fā)的操作呢? 那么順序就是: wilChangeValueForKey:
、 didChangeValueForKey:
和 observeValueForKeyPath:ofObject:change:context:
不信你把 didChangeValueForKey:
注視掉曾我,看下 observeValueForKeyPath:ofObject:change:context:
會不會執(zhí)行粉怕。
了解到這一點很重要,正如 46. 如何手動觸發(fā)一個value的KVO 所說的:
“手動觸發(fā)”的使用場景是什么您单?一般我們只在希望能控制“回調(diào)的調(diào)用時機”時才會這么做斋荞。
而“回調(diào)的調(diào)用時機”就是在你調(diào)用 didChangeValueForKey:
方法時。
52. IBOutlet連出來的視圖屬性為什么可以被設(shè)置成weak?
參考鏈接: Should IBOutlets be strong or weak under ARC?
文章告訴我們:
因為既然有外鏈那么視圖在xib或者storyboard中肯定存在虐秦,視圖已經(jīng)對它有一個強引用了平酿。
不過這個回答漏了個重要知識,使用storyboard(xib不行)創(chuàng)建的vc悦陋,會有一個叫_topLevelObjectsToKeepAliveFromStoryboard的私有數(shù)組強引用所有top level的對象蜈彼,所以這時即便outlet聲明成weak也沒關(guān)系
53. IB中User Defined Runtime Attributes如何使用?
它能夠通過KVC的方式配置一些你在interface builder 中不能配置的屬性俺驶。當你希望在IB中作盡可能多得事情幸逆,這個特性能夠幫助你編寫更加輕量級的viewcontroller
54. 如何調(diào)試BAD_ACCESS錯誤
重寫object的respondsToSelector方法,現(xiàn)實出現(xiàn)EXEC_BAD_ACCESS前訪問的最后一個object
-
通過 Zombie
enter image description here 設(shè)置全局斷點快速定位問題代碼所在行
-
Xcode 7 已經(jīng)集成了BAD_ACCESS捕獲功能:Address Sanitizer暮现。
用法如下:在配置中勾選?Enable Address Sanitizer
enter image description here
55. lldb(gdb)常用的調(diào)試命令还绘?
- breakpoint 設(shè)置斷點定位到某一個函數(shù)
- n 斷點指針下一步
- po打印對象
更多 lldb(gdb) 調(diào)試命令可查看
- The LLDB Debugger ;
- 蘋果官方文檔: iOS Debugging Magic 栖袋。
Posted by 微博@iOS程序犭袁
原創(chuàng)文章拍顷,版權(quán)聲明:自由轉(zhuǎn)載-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0