《招聘一個靠譜的 iOS》—參考答案(下)
說明:面試題來源是微博@我就叫Sunny怎么了的這篇博文:《招聘一個靠譜的 iOS》,其中共55題,除第一題為糾錯題外桑李,其他54道均為簡答題。
出題者簡介: 孫源(sunnyxx),目前就職于百度蜒犯,負責百度知道 iOS 客戶端的開發(fā)工作,對技術喜歡刨根問底和總結最佳實踐孕惜,熱愛分享和開源愧薛,維護一個叫 forkingdog 的開源小組。
答案為微博@iOS程序犭袁整理衫画,未經(jīng)出題者校對毫炉,如有紕漏,請向微博@iOS程序犭袁指正削罩。
索引
- 25.
_objc_msgForward
函數(shù)是做什么的瞄勾,直接調用它將會發(fā)生什么费奸? - 26. runtime如何實現(xiàn)weak變量的自動置nil?
- 27. 能否向編譯后得到的類中增加實例變量进陡?能否向運行時創(chuàng)建的類中添加實例變量愿阐?為什么?
- 28. runloop和線程有什么關系趾疚?
- 29. runloop的mode作用是什么缨历?
- 30. 以+ scheduledTimerWithTimeInterval...的方式觸發(fā)的timer,在滑動頁面上的列表時糙麦,timer會暫定回調辛孵,為什么?如何解決赡磅?
- 31. 猜想runloop內部是如何實現(xiàn)的魄缚?
- 32. objc使用什么機制管理對象內存?
- 33. ARC通過什么方式幫助開發(fā)者管理內存焚廊?
- 34. 不手動指定autoreleasepool的前提下冶匹,一個autorealese對象在什么時刻釋放?(比如在一個vc的viewDidLoad中創(chuàng)建)
- 35. BAD_ACCESS在什么情況下出現(xiàn)咆瘟?
- 36. 蘋果是如何實現(xiàn)autoreleasepool的嚼隘?
- 37. 使用block時什么情況會發(fā)生引用循環(huán),如何解決搞疗?
- 38. 在block內如何修改block外部變量嗓蘑?
- 39. 使用系統(tǒng)的某些block api(如UIView的block版本寫動畫時),是否也考慮引用循環(huán)問題匿乃?
- 40. GCD的隊列(dispatch_queue_t)分哪兩種類型桩皿?
- 41. 如何用GCD同步若干個異步調用?(如根據(jù)若干個url異步加載多張圖片幢炸,然后在都下載完成后合成一張整圖)
- 42. dispatch_barrier_async的作用是什么泄隔?
- 43. 蘋果為什么要廢棄dispatch_get_current_queue?
- 44. 以下代碼運行結果如何宛徊?
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(@"1");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2");
});
NSLog(@"3");
}
- 45. addObserver:forKeyPath:options:context:各個參數(shù)的作用分別是什么佛嬉,observer中需要實現(xiàn)哪個方法才能獲得KVO回調?
- 46. 如何手動觸發(fā)一個value的KVO
- 47. 若一個類有實例變量 NSString *_foo 闸天,調用setValue:forKey:時暖呕,可以以foo還是 _foo 作為key?
- 48. KVC的keyPath中的集合運算符如何使用苞氮?
- 49. KVC和KVO的keyPath一定是屬性么湾揽?
- 50. 如何關閉默認的KVO的默認實現(xiàn),并進入自定義的KVO實現(xiàn)?
- 51. apple用什么方式實現(xiàn)對一個對象的KVO库物?
- 52. IBOutlet連出來的視圖屬性為什么可以被設置成weak?
- 53. IB中User Defined Runtime Attributes如何使用霸旗?
- 54. 如何調試BAD_ACCESS錯誤
- 55. lldb(gdb)常用的調試命令?
25. _objc_msgForward
函數(shù)是做什么的戚揭,直接調用它將會發(fā)生什么诱告?
_objc_msgForward
是 IMP 類型,用于消息轉發(fā)的:當向一個對象發(fā)送一條消息民晒,但它并沒有實現(xiàn)的時候精居,_objc_msgForward
會嘗試做消息轉發(fā)。
我們可以這樣創(chuàng)建一個_objc_msgForward
對象:
IMP msgForwardIMP = _objc_msgForward;
在上篇中的《objc中向一個對象發(fā)送消息[obj foo]
和objc_msgSend()
函數(shù)之間有什么關系镀虐?》曾提到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
有關的三個函數(shù)使用偽代碼展示下:
// objc-runtime-new.mm 文件里與 _objc_msgForward 有關的三個函數(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, ...); //調用這個函數(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 用于消息轉發(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)源碼哟沫,但是我們還是能得出結論:
_objc_msgForward
是一個函數(shù)指針(和 IMP 的類型一樣)饺蔑,是用于消息轉發(fā)的:當向一個對象發(fā)送一條消息,但它并沒有實現(xiàn)的時候嗜诀,_objc_msgForward
會嘗試做消息轉發(fā)猾警。
在上篇中的《objc中向一個對象發(fā)送消息
[obj foo]
和objc_msgSend()
函數(shù)之間有什么關系?》曾提到objc_msgSend
在“消息傳遞”中的作用隆敢。在“消息傳遞”過程中发皿,objc_msgSend
的動作比較清晰:首先在 Class 中的緩存查找 IMP (沒緩存則初始化緩存),如果沒找到拂蝎,則向父類的 Class 查找穴墅。如果一直查找到根類仍舊沒有實現(xiàn),則用_objc_msgForward
函數(shù)指針代替 IMP 。最后封救,執(zhí)行這個 IMP 拇涤。
為了展示消息轉發(fā)的具體動作,這里嘗試向一個對象發(fā)送一條錯誤的消息誉结,并查看一下_objc_msgForward
是如何進行轉發(fā)的鹅士。
首先開啟調試模式、打印出所有運行時發(fā)送的消息:
可以在代碼里執(zhí)行下面的方法:
(void)instrumentObjcMessageSends(YES);
或者斷點暫停程序運行惩坑,并在 gdb 中輸入下面的命令:
call (void)instrumentObjcMessageSends(YES)
以第二種為例掉盅,操作如下所示:
之后,運行時發(fā)送的所有消息都會打印到/tmp/msgSend-xxxx
文件里了以舒。
終端中輸入命令前往:
open /private/tmp
可能看到有多條趾痘,找到最新生成的,雙擊打開
在模擬器上執(zhí)行執(zhí)行以下語句(這一套調試方案僅適用于模擬器蔓钟,真機不可用永票,關于該調試方案的拓展鏈接: 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
結合《NSObject官方文檔》侣集,排除掉 NSObject 做的事,剩下的就是_objc_msgForward
消息轉發(fā)做的幾件事:
調用
resolveInstanceMethod:
方法 (或resolveClassMethod:
)兰绣。允許用戶在此時為該 Class 動態(tài)添加實現(xiàn)世分。如果有實現(xiàn)了,則調用并返回YES缀辩,那么重新開始objc_msgSend
流程臭埋。這一次對象會響應這個選擇器,一般是因為它已經(jīng)調用過class_addMethod
臀玄。如果仍沒實現(xiàn)瓢阴,繼續(xù)下面的動作。調用
forwardingTargetForSelector:
方法镐牺,嘗試找到一個能響應該消息的對象炫掐。如果獲取到,則直接把消息轉發(fā)給它睬涧,返回非 nil 對象募胃。否則返回 nil ,繼續(xù)下面的動作畦浓。注意痹束,這里不要返回 self ,否則會形成死循環(huán)讶请。調用
methodSignatureForSelector:
方法祷嘶,嘗試獲得一個方法簽名屎媳。如果獲取不到,則直接調用doesNotRecognizeSelector
拋出異常论巍。如果能獲取烛谊,則返回非nil:創(chuàng)建一個 NSlnvocation 并傳給forwardInvocation:
。調用
forwardInvocation:
方法嘉汰,將第3步獲取到的方法簽名包裝成 Invocation 傳入丹禀,如何處理就在這里面了,并返回非ni鞋怀。調用
doesNotRecognizeSelector:
双泪,默認的實現(xiàn)是拋出異常。如果第3步?jīng)]能獲得一個方法簽名密似,執(zhí)行該步驟焙矛。
上面前4個方法均是模板方法,開發(fā)者可以override残腌,由 runtime 來調用村斟。最常見的實現(xiàn)消息轉發(fā):就是重寫方法3和4,吞掉一個消息或者代理給其他對象都是沒問題的
也就是說_objc_msgForward
在進行消息轉發(fā)的過程中會涉及以下這幾個方法:
resolveInstanceMethod:
方法 (或resolveClassMethod:
)废累。forwardingTargetForSelector:
方法methodSignatureForSelector:
方法forwardInvocation:
方法doesNotRecognizeSelector:
方法
為了能更清晰地理解這些方法的作用邓梅,git倉庫里也給出了一個Demo,名稱叫“ _objc_msgForward_demo
”,可運行起來看看邑滨。
下面回答下第二個問題“直接_objc_msgForward
調用它將會發(fā)生什么?”
直接調用_objc_msgForward
是非常危險的事钱反,如果用不好會直接導致程序Crash掖看,但是如果用得好,能做很多非趁娓纾酷的事哎壳。
就好像跑酷,干得好尚卫,叫“斯殚牛酷”,干不好就叫“作死”吱涉。
正如前文所說:
_objc_msgForward
是 IMP 類型刹泄,用于消息轉發(fā)的:當向一個對象發(fā)送一條消息,但它并沒有實現(xiàn)的時候怎爵,_objc_msgForward
會嘗試做消息轉發(fā)特石。
如何調用_objc_msgForward
?
_objc_msgForward
隸屬 C 語言鳖链,有三個參數(shù) :
-- |
_objc_msgForward 參數(shù) |
類型 |
---|---|---|
1. | 所屬對象 | id類型 |
2. | 方法名 | SEL類型 |
3. | 可變參數(shù) | 可變參數(shù)類型 |
首先了解下如何調用 IMP 類型的方法姆蘸,IMP類型是如下格式:
為了直觀,我們可以通過如下方式定義一個 IMP類型 :
typedef void (*voidIMP)(id, SEL, ...)
一旦調用_objc_msgForward
,將跳過查找 IMP 的過程逞敷,直接觸發(fā)“消息轉發(fā)”狂秦,
如果調用了_objc_msgForward
院尔,即使這個對象確實已經(jīng)實現(xiàn)了這個方法罐监,你也會告訴objc_msgSend
:
“我沒有在這個對象里找到這個方法的實現(xiàn)”
想象下objc_msgSend
會怎么做?通常情況下看靠,下面這張圖就是你正常走objc_msgSend
過程玖姑,和直接調用_objc_msgForward
的前后差別:
有哪些場景需要直接調用_objc_msgForward
愕秫?最常見的場景是:你想獲取某方法所對應的NSInvocation
對象。舉例說明:
JSPatch (Github 鏈接)就是直接調用_objc_msgForward
來實現(xiàn)其核心功能的:
JSPatch 以小巧的體積做到了讓JS調用/替換任意OC方法焰络,讓iOS APP具備熱更新的能力戴甩。
作者的博文《JSPatch實現(xiàn)原理詳解》詳細記錄了實現(xiàn)原理,有興趣可以看下闪彼。
26. runtime如何實現(xiàn)weak變量的自動置nil甜孤?
runtime 對注冊的類, 會進行布局畏腕,對于 weak 對象會放入一個 hash 表中缴川。 用 weak 指向的對象內存地址作為 key,當此對象的引用計數(shù)為0的時候會 dealloc描馅,假如 weak 指向的對象內存地址是a把夸,那么就會以a為鍵, 在這個 weak 表中搜索铭污,找到所有以a為鍵的 weak 對象恋日,從而設置為 nil。
在上篇中的《runtime 如何實現(xiàn) weak 屬性》有論述嘹狞。(注:在上篇的《使用runtime Associate方法關聯(lián)的對象岂膳,需要在主對象dealloc的時候釋放么?》里給出的“對象的內存銷毀時間表”也提到__weak
引用的解除時間磅网。)
我們可以設計一個函數(shù)(偽代碼)來表示上述機制:
objc_storeWeak(&a, b)
函數(shù):
objc_storeWeak
函數(shù)把第二個參數(shù)--賦值對象(b)的內存地址作為鍵值key谈截,將第一個參數(shù)--weak修飾的屬性變量(a)的內存地址(&a)作為value,注冊到 weak 表中涧偷。如果第二個參數(shù)(b)為0(nil)簸喂,那么把變量(a)的內存地址(&a)從weak表中刪除,
你可以把objc_storeWeak(&a, b)
理解為:objc_storeWeak(value, key)
嫂丙,并且當key變nil娘赴,將value置nil。
在b非nil時跟啤,a和b指向同一個內存地址诽表,在b變nil時唉锌,a變nil。此時向a發(fā)送消息不會崩潰:在Objective-C中向nil發(fā)送消息是安全的竿奏。
而如果a是由assign修飾的袄简,則:
在b非nil時,a和b指向同一個內存地址泛啸,在b變nil時绿语,a還是指向該內存地址,變野指針候址。此時向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)?岗仑,變量作用域結束*/
objc_destroyWeak(&obj1);
下面對用到的兩個方法objc_initWeak
和objc_destroyWeak
做下解釋:
總體說來匹耕,作用是:
通過objc_initWeak
函數(shù)初始化“附有weak修飾符的變量(obj1)”,在變量作用域結束時通過objc_destoryWeak
函數(shù)釋放該變量(obj1)荠雕。
下面分別介紹下方法的內部實現(xiàn):
objc_initWeak
函數(shù)的實現(xiàn)是這樣的:在將“附有weak修飾符的變量(obj1)”初始化為0(nil)后稳其,會將“賦值對象”(obj)作為參數(shù),調用objc_storeWeak
函數(shù)炸卑。
obj1 = 0既鞠;
obj_storeWeak(&obj1, obj);
也就是說:
weak 修飾的指針默認值是 nil (在Objective-C中向nil發(fā)送消息是安全的)
然后obj_destroyWeak
函數(shù)將0(nil)作為參數(shù),調用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)的內存地址作為鍵值五续,將第一個參數(shù)--weak修飾的屬性變量(obj1)的內存地址注冊到 weak 表中浑槽。如果第二個參數(shù)(obj)為0(nil),那么把變量(obj1)的地址從weak表中刪除返帕。
27. 能否向編譯后得到的類中增加實例變量?能否向運行時創(chuàng)建的類中添加實例變量篙挽?為什么荆萤?
- 不能向編譯后得到的類中增加實例變量;
- 能向運行時創(chuàng)建的類中添加實例變量铣卡;
解釋下:
因為編譯后的類已經(jīng)注冊在 runtime 中链韭,類結構體中的
objc_ivar_list
實例變量的鏈表 和instance_size
實例變量的內存大小已經(jīng)確定,同時runtime 會調用class_setIvarLayout
或class_setWeakIvarLayout
來處理 strong weak 引用煮落。所以不能向存在的類中添加實例變量敞峭;運行時創(chuàng)建的類是可以添加實例變量,調用
class_addIvar
函數(shù)蝉仇。但是得在調用objc_allocateClassPair
之后旋讹,objc_registerClassPair
之前殖蚕,原因同上。
28. runloop和線程有什么關系沉迹?
總的說來睦疫,Run loop,正如其名鞭呕,loop表示某種循環(huán)蛤育,和run放在一起就表示一直在運行著的循環(huán)。實際上葫松,run loop和線程是緊密相連的瓦糕,可以這樣說run loop是為了線程而生,沒有線程腋么,它就沒有存在的必要咕娄。Run loops是線程的基礎架構部分, Cocoa 和 CoreFundation 都提供了 run loop 對象方便配置和管理線程的 run loop (以下都以 Cocoa 為例)党晋。每個線程谭胚,包括程序的主線程( main thread )都有與之相應的 run loop 對象。
runloop 和線程的關系:
- 主線程的run loop默認是啟動的未玻。
iOS的應用程序里面灾而,程序啟動后會有一個如下的main()函數(shù)
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
重點是UIApplicationMain()函數(shù),這個方法會為main thread設置一個NSRunLoop對象扳剿,這就解釋了:為什么我們的應用可以在無人操作的時候休息旁趟,需要讓它干活的時候又能立馬響應。
對其它線程來說庇绽,run loop默認是沒有啟動的锡搜,如果你需要更多的線程交互則可以手動配置和啟動,如果線程只是去執(zhí)行一個長時間的已確定的任務則不需要瞧掺。
在任何一個 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會暫定回調壹蔓,為什么趟妥?如何解決?
RunLoop只能運行在一種mode下佣蓉,如果要換mode披摄,當前的loop也需要停下重啟成新的亲雪。利用這個機制,ScrollView滾動過程中NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的mode會切換到UITrackingRunLoopMode來保證ScrollView的流暢滑動:只能在NSDefaultRunLoopMode模式下處理的事件會影響scrllView的滑動行疏。
如果我們把一個NSTimer對象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主運行循環(huán)中的時候,
ScrollView滾動過程中會因為mode的切換匆光,而導致NSTimer將不再被調度。
同時因為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內部是如何實現(xiàn)的?
一般來講贞让,一個線程一次只能執(zhí)行一個任務周崭,執(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使用什么機制管理對象內存摸航?
通過 retainCount 的機制來決定對象是否需要釋放。
每次 runloop 的時候舅桩,都會檢查對象的 retainCount酱虎,如果retainCount 為 0,說明該對象沒有地方需要繼續(xù)使用了擂涛,可以釋放掉了读串。
33. ARC通過什么方式幫助開發(fā)者管理內存?
<p><del>編譯時根據(jù)代碼上下文撒妈,插入 retain/release
</del></p>
ARC相對于MRC恢暖,不是在編譯時添加retain/release/autorelease這么簡單。應該是編譯期和運行期兩部分共同幫助開發(fā)者管理內存狰右。
在編譯期杰捂,ARC用的是更底層的C接口實現(xiàn)的retain/release/autorelease,這樣做性能更好棋蚌,也是為什么不能在ARC環(huán)境下手動retain/release/autorelease琼娘,同時對同一上下文的同一對象的成對retain/release操作進行優(yōu)化(即忽略掉不必要的操作);ARC也包含運行期組件附鸽,這個地方做的優(yōu)化比較復雜,但也不能被忽略瞒瘸】辣福【TODO:后續(xù)更新會詳細描述下】
34. 不手動指定autoreleasepool的前提下,一個autorealese對象在什么時刻釋放情臭?(比如在一個vc的viewDidLoad中創(chuàng)建)
分兩種情況:手動干預釋放時機省撑、系統(tǒng)自動去釋放赌蔑。
- 手動干預釋放時機--指定autoreleasepool
就是所謂的:當前作用域大括號結束時釋放。 - 系統(tǒng)自動去釋放--不手動指定autoreleasepool
Autorelease對象出了作用域之后竟秫,會被添加到最近一次創(chuàng)建的自動釋放池中娃惯,并會在當前的 runloop 迭代結束時釋放。
如果在一個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_aurorelease
看函數(shù)名就可以知道证膨,對 autorelease 分別執(zhí)行 push,和 pop 操作鼓黔。銷毀對象時執(zhí)行release操作央勒。
37. 使用block時什么情況會發(fā)生引用循環(huán),如何解決澳化?
一個對象中強引用了block崔步,在block中又使用了該對象,就會發(fā)射循環(huán)引用肆捕。
解決方法是將該對象使用__weak或者__block修飾符修飾之后再在block中使用刷晋。
- id weak weakSelf = self;
或者 weak __typeof(&*self)weakSelf = self該方法可以設置宏 - id __block weakSelf = self;
38. 在block內如何修改block外部變量?
默認情況下慎陵,在block中訪問的外部變量是復制過去的眼虱,即:寫操作不對原變量生效。但是你可以加上__block
來讓其寫操作生效席纽,示例代碼如下:
__block int a = 0;
void (^foo)(void) = ^{
a = 1;
}
f00();
//這里捏悬,a的值被修改為1
參考鏈接:微博@唐巧_boy的著作《iOS開發(fā)進階》中的第11.2.3章節(jié)
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 內部如果引用了 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)引用撵溃。
40. GCD的隊列(dispatch_queue_t
)分哪兩種類型疚鲤?
- 串行隊列Serial Dispatch Queue
- 并行隊列Concurrent Dispatch Queue
41. 如何用GCD同步若干個異步調用?(如根據(jù)若干個url異步加載多張圖片缘挑,然后在都下載完成后合成一張整圖)
使用Dispatch Group追加block到Global Group Queue,這些block如果全部執(zhí)行完畢集歇,就會執(zhí)行Main Dispatch Queue中的結束處理的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
的作用是什么语淘?
在并行隊列中诲宇,為了保持某些任務的順序,需要等待一些任務完成后才能繼續(xù)進行亏娜,使用 barrier 來等待之前任務完成焕窝,避免數(shù)據(jù)競爭等問題。
dispatch_barrier_async
函數(shù)會等待追加到Concurrent Dispatch Queue并行隊列中的操作全部執(zhí)行完之后维贺,然后再執(zhí)行 dispatch_barrier_async
函數(shù)追加的處理它掂,等 dispatch_barrier_async
追加的處理執(zhí)行結束之后溯泣,Concurrent Dispatch Queue才恢復之前的動作繼續(xù)執(zhí)行。
打個比方:比如你們公司周末跟團旅游,高速休息站上,司機說:大家都去上廁所,速戰(zhàn)速決,上完廁所就上高速设凹。超大的公共廁所,大家同時去航唆,程序猿很快就結束了,但程序媛就可能會慢一些,即使你第一個回來剑按,司機也不會出發(fā),司機要等待所有人都回來后,才能出發(fā)撇叁。 dispatch_barrier_async
函數(shù)追加的內容就如同 “上完廁所就上高速”這個動作。
(注意:使用 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. 以下代碼運行結果如何?
- (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回調碌更?
// 添加鍵值觀察
/*
1 觀察者旭绒,負責處理監(jiān)聽事件的對象
2 觀察的屬性
3 觀察的選項
4 上下文
*/
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"Person Name"];
observer中需要實現(xiàn)一下方法:
// 所有的 kvo 監(jiān)聽到事件房午,都會調用此方法
/*
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 之前設置一個初始值贾陷,注冊之后慌洪,設置一個不一樣的值频伤,就可以觸發(fā)了。
想知道如何手動觸發(fā),必須知道自動觸發(fā) KVO 的原理:
鍵值觀察通知依賴于 NSObject 的兩個方法: willChangeValueForKey:
和 didChangevlueForKey:
院仿。在一個被觀察屬性發(fā)生改變之前暮芭, willChangeValueForKey:
一定會被調用,這就
會記錄舊的值欲低。而當改變發(fā)生后辕宏, didChangeValueForKey:
會被調用,繼而 observeValueForKey:ofObject:change:context:
也會被調用砾莱。如果可以手動實現(xiàn)這些調用瑞筐,就可以實現(xiàn)“手動觸發(fā)”了。
那么“手動觸發(fā)”的使用場景是什么恤磷?一般我們只在希望能控制“回調的調用時機”時才會這么做面哼。
具體做法如下:
如果這個 value
是 表示時間的 self.now
,那么代碼如下:最后兩行代碼缺一不可扫步。
// .m文件
// Created by https://github.com/ChenYilong
// 微博@iOS程序犭袁(http://weibo.com/luohanchenyilong/).
// 手動觸發(fā) value 的KVO魔策,最后兩行代碼缺一不可。
//@property (nonatomic, strong) NSDate *now;
- (void)viewDidLoad
{
[super viewDidLoad];
[self willChangeValueForKey:@"now"]; // “手動觸發(fā)self.now的KVO”河胎,必寫闯袒。
[self didChangeValueForKey:@"now"]; // “手動觸發(fā)self.now的KVO”,必寫游岳。
}
但是平時我們一般不會這么干政敢,我們都是等系統(tǒng)去“自動觸發(fā)”∨咂龋“自動觸發(fā)”的實現(xiàn)原理:
比如調用
setNow:
時喷户,系統(tǒng)還會以某種方式在中間插入wilChangeValueForKey:
、didChangeValueForKey:
和observeValueForKeyPath:ofObject:change:context:
的調用访锻。
大家可能以為這是因為 setNow:
是合成方法褪尝,有時候我們也能看到人們這么寫代碼:
- (void)setNow:(NSDate *)aDate {
[self willChangeValueForKey:@"now"]; // 沒有必要
_now = aDate;
[self didChangeValueForKey:@"now"];// 沒有必要
}
這是完全沒有必要的代碼闹获,不要這么做,這樣的話河哑,KVO代碼會被調用兩次避诽。KVO在調用存取方法之前總是調用 willChangeValueForKey:
,之后總是調用 didChangeValueForkey:
璃谨。怎么做到的呢?答案是通過 isa 混寫(isa-swizzling)沙庐。下文《apple用什么方式實現(xiàn)對一個對象的KVO?》會有詳述佳吞。
參考鏈接: Manual Change Notification---Apple 官方文檔
47. 若一個類有實例變量 NSString *_foo
拱雏,調用setValue:forKey:時,可以以foo還是 _foo
作為key底扳?
都可以古涧。
48. KVC的keyPath中的集合運算符如何使用?
- 必須用在集合對象上或普通對象的集合屬性上
- 簡單集合運算符有@avg花盐, @count , @max 菇爪, @min 算芯,@sum,
- 格式 @"@sum.age"或 @"集合屬性.@max.age"
49. KVC和KVO的keyPath一定是屬性么凳宙?
KVO支持實例變量
50. 如何關閉默認的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 方法會負責在調用原 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:
一定會被調用足绅,這就會記錄舊的值捷绑。而當改變發(fā)生后, didChangeValueForKey:
會被調用编检,繼而 observeValueForKey:ofObject:change:context:
也會被調用胎食。可以手動實現(xiàn)這些調用允懂,但很少有人這么做厕怜。一般我們只在希望能控制回調的調用時機時才會這么做。大部分情況下蕾总,改變通知會自動調用粥航。
比如調用 setNow:
時,系統(tǒng)還會以某種方式在中間插入 wilChangeValueForKey:
生百、 didChangeValueForKey:
和 observeValueForKeyPath:ofObject:change:context:
的調用递雀。大家可能以為這是因為 setNow:
是合成方法,有時候我們也能看到人們這么寫代碼:
- (void)setNow:(NSDate *)aDate {
[self willChangeValueForKey:@"now"]; // 沒有必要
_now = aDate;
[self didChangeValueForKey:@"now"];// 沒有必要
}
這是完全沒有必要的代碼蚀浆,不要這么做缀程,這樣的話,KVO代碼會被調用兩次市俊。KVO在調用存取方法之前總是調用 willChangeValueForKey:
杨凑,之后總是調用 didChangeValueForkey:
。怎么做到的呢?答案是通過 isa 混寫(isa-swizzling)摆昧。第一次對一個對象調用 addObserver:forKeyPath:options:context:
時撩满,框架會創(chuàng)建這個類的新的 KVO 子類,并將被觀察對象轉換為新子類的對象。在這個 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
方法并返回原來的類晶伦。 企圖欺騙我們:這個類沒有變,就是原本那個類啄枕。婚陪。。
但是频祝,假設“被監(jiān)聽的對象”的類對象是 MYClass
泌参,有時候我們能看到對 NSKVONotifying_MYClass
的引用而不是對 MYClass
的引用脆淹。借此我們得以知道 Apple 使用了 isa 混寫(isa-swizzling)
。具體探究過程可參考 這篇博文 沽一。
52. IBOutlet連出來的視圖屬性為什么可以被設置成weak?
參考鏈接: Should IBOutlets be strong or weak under ARC?
文章告訴我們:
因為既然有外鏈那么視圖在xib或者storyboard中肯定存在盖溺,視圖已經(jīng)對它有一個強引用了。
不過這個回答漏了個重要知識铣缠,使用storyboard(xib不行)創(chuàng)建的vc烘嘱,會有一個叫_topLevelObjectsToKeepAliveFromStoryboard的私有數(shù)組強引用所有top level的對象,所以這時即便outlet聲明成weak也沒關系
53. IB中User Defined Runtime Attributes如何使用蝗蛙?
它能夠通過KVC的方式配置一些你在interface builder 中不能配置的屬性蝇庭。當你希望在IB中作盡可能多得事情,這個特性能夠幫助你編寫更加輕量級的viewcontroller
54. 如何調試BAD_ACCESS錯誤
重寫object的respondsToSelector方法捡硅,現(xiàn)實出現(xiàn)EXEC_BAD_ACCESS前訪問的最后一個object
-
通過 Zombie
設置全局斷點快速定位問題代碼所在行
-
Xcode 7 已經(jīng)集成了BAD_ACCESS捕獲功能:Address Sanitizer哮内。
用法如下:在配置中勾選?Enable Address Sanitizer
55. lldb(gdb)常用的調試命令?
- breakpoint 設置斷點定位到某一個函數(shù)
- n 斷點指針下一步
- po打印對象
更多 lldb(gdb) 調試命令可查看
- The LLDB Debugger 壮韭;
- 蘋果官方文檔: iOS Debugging Magic 北发。
Posted by 微博@iOS程序犭袁
原創(chuàng)文章,版權聲明:自由轉載-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0