這里只是對《招聘一個靠譜的 iOS》個人覺得比較重要的一些筆記摘抄以便日后復習,有需要的建議看原文录粱。
26. runtime如何實現(xiàn)weak變量的自動置nil?
runtime 對注冊的類画拾, 會進行布局啥繁,對于 weak 對象會放入一個 hash 表中。 用 weak 指向的對象內存地址作為 key青抛,當此對象的引用計數(shù)為0的時候會 dealloc旗闽,假如 weak 指向的對象內存地址是a,那么就會以a為鍵蜜另, 在這個 weak 表中搜索适室,找到所有以a為鍵的 weak 對象,從而設置為 nil蚕钦。
27. 能否向編譯后得到的類中增加實例變量亭病?能否向運行時創(chuàng)建的類中添加實例變量?為什么嘶居?
不能向編譯后得到的類中增加實例變量罪帖;
能向運行時創(chuàng)建的類中添加實例變量促煮;
解釋下:因為編譯后的類已經注冊在 runtime 中,類結構體中的 objc_ivar_list 實例變量的鏈表 和 instance_size 實例變量的內存大小已經確定整袁,同時runtime 會調用 class_setIvarLayout 或 class_setWeakIvarLayout 來處理 strong weak 引用菠齿。所以不能向存在的類中添加實例變量;
運行時創(chuàng)建的類是可以添加實例變量坐昙,調用 class_addIvar 函數(shù)绳匀。但是得在調用 objc_allocateClassPair 之后,objc_registerClassPair 之前炸客,原因同上疾棵。
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;
}
34. 不手動指定autoreleasepool的前提下众弓,一個autorealese對象在什么時刻釋放恩溅?(比如在一個vc的viewDidLoad中創(chuàng)建)
分兩種情況:手動干預釋放時機、系統(tǒng)自動去釋放谓娃。
- 手動干預釋放時機--指定autoreleasepool 就是所謂的:當前作用域大括號結束時釋放脚乡。
- 系統(tǒng)自動去釋放--不手動指定autoreleasepool
Autorelease對象出了作用域之后,會被添加到最近一次創(chuàng)建的自動釋放池中滨达,并會在當前的 runloop 迭代結束時釋放每窖。
釋放的時機總結起來,可以用下圖來表示:
下面對這張圖進行詳細的解釋:
從程序啟動到加載完成是一個完整的運行循環(huán)弦悉,然后會停下來,等待用戶交互蟆炊,用戶的每一次交互都會啟動一次運行循環(huán)稽莉,來處理用戶所有的點擊事件、觸摸事件涩搓。
我們都知道: 所有 autorelease 的對象污秆,在出了作用域之后,會被自動添加到最近創(chuàng)建的自動釋放池中昧甘。
但是如果每次都放進應用程序的 main.m 中的 autoreleasepool 中良拼,遲早有被撐滿的一刻。這個過程中必定有一個釋放的動作充边。何時庸推?
在一次完整的運行循環(huán)結束之前常侦,會被銷毀。
那什么時間會創(chuàng)建自動釋放池贬媒?運行循環(huán)檢測到事件并啟動后聋亡,就會創(chuàng)建自動釋放池。
子線程的 runloop 默認是不工作际乘,無法主動創(chuàng)建坡倔,必須手動創(chuàng)建。
自定義的 NSOperation 和 NSThread 需要手動創(chuàng)建自動釋放池脖含。比如: 自定義的 NSOperation 類中的 main 方法里就必須添加自動釋放池罪塔。否則出了作用域后,自動釋放對象會因為沒有自動釋放池去處理它养葵,而造成內存泄露征堪。
但對于 blockOperation 和 invocationOperation 這種默認的Operation ,系統(tǒng)已經幫我們封裝好了港柜,不需要手動創(chuàng)建自動釋放池请契。
@autoreleasepool 當自動釋放池被銷毀或者耗盡時,會向自動釋放池中的所有對象發(fā)送 release 消息夏醉,釋放自動釋放池中的所有對象爽锥。
如果在一個vc的viewDidLoad中創(chuàng)建一個 Autorelease對象,那么該對象會在 viewDidAppear 方法執(zhí)行前就被銷毀了畔柔。
參考鏈接:《黑幕背后的Autorelease》
38. 在block內如何修改block外部變量氯夷?
默認情況下,在block中訪問的外部變量是復制過去的靶擦,即:寫操作不對原變量生效腮考。但是你可以加上 __block 來讓其寫操作生效,示例代碼如下:
__block int a = 0;
void (^foo)(void) = ^{
a = 1;
};
foo();
//這里玄捕,a的值被修改為1
這是 微博@唐巧_boy的《iOS開發(fā)進階》中的第11.2.3章節(jié)中的描述踩蔚。你同樣可以在面試中這樣回答,但你并沒有答到“點子上”枚粘。真正的原因馅闽,并沒有書這本書里寫的這么“神奇”,而且這種說法也有點牽強馍迄。面試官肯定會追問“為什么寫操作就生效了福也?”真正的原因是這樣的:
我們都知道:Block不允許修改外部變量的值,這里所說的外部變量的值攀圈,指的是棧中指針的內存地址暴凑。__block 所起到的作用就是只要觀察到該變量被 block 所持有,就將“外部變量”在棧中的內存地址放到了堆中赘来。進而在block內部也可以修改外部變量的值现喳。
Block不允許修改外部變量的值凯傲。Apple這樣設計,應該是考慮到了block的特殊性拿穴,block也屬于“函數(shù)”的范疇泣洞,變量進入block,實際就是已經改變了作用域默色。在幾個作用域之間進行切換時球凰,如果不加上這樣的限制,變量的可維護性將大大降低腿宰。又比如我想在block內聲明了一個與外部同名的變量呕诉,此時是允許呢還是不允許呢?只有加上了這樣的限制吃度,這樣的情景才能實現(xiàn)甩挫。于是棧區(qū)變成了紅燈區(qū),堆區(qū)變成了綠燈區(qū)椿每。
我們可以打印下內存地址來進行驗證:
__block int a = 0;
NSLog(@"定義前:%p", &a); //棧區(qū)
void (^foo)(void) = ^{
a = 1;
NSLog(@"block內部:%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內部: 0x155b22fc8
“定義后”和“block內部”兩者的內存地址是一樣的伊者,我們都知道 block 內部的變量會被 copy 到堆區(qū),“block內部”打印的是堆地址间护,因而也就可以知道亦渗,“定義后”打印的也是堆的地址。
那么如何證明“block內部”打印的是堆地址汁尺?
把三個16進制的內存地址轉成10進制就是:
- 定義后前:6171559672
- block內部:5732708296
- 定義后后:5732708296
中間相差438851376個字節(jié)法精,也就是 418.5M 的空間,因為堆地址要小于棧地址痴突,又因為iOS中一個進程的棧區(qū)內存只有1M搂蜓,Mac也只有8M,顯然a已經是在堆區(qū)了辽装。
這也證實了:a 在定義前是棧區(qū)帮碰,但只要進入了 block 區(qū)域,就變成了堆區(qū)拾积。這才是 __block 關鍵字的真正作用收毫。
__block 關鍵字修飾后,int類型也從4字節(jié)變成了32字節(jié)殷勘,這是 Foundation 框架 malloc 出來的。這也同樣能證實上面的結論昔搂。(PS:居然比 NSObject alloc 出來的 16 字節(jié)要多一倍)玲销。
理解到這是因為堆棧地址的變更,而非所謂的“寫操作生效”摘符,這一點至關重要贤斜,要不然你如何解釋下面這個現(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\
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已經由基本數(shù)據類型锁荔,變成了對象類型蟀给。block會對對象類型的指針進行copy,copy到堆中阳堕,但并不會改變該指針所指向的堆中的地址跋理,所以在上面的示例代碼中,block體內修改的實際是a指向的堆中的內容恬总。
但如果我們嘗試像上面圖片中的65行那樣做前普,結果會編譯不通過,那是因為此時你在修改的就不是堆中的內容壹堰,而是棧中的內容拭卿。
上文已經說過:Block不允許修改外部變量的值,這里所說的外部變量的值贱纠,指的是棧中指針的內存地址峻厚。棧區(qū)是紅燈區(qū),堆區(qū)才是綠燈區(qū)并巍。
50. 如何關閉默認的KVO的默認實現(xiàn)目木,并進入自定義的KVO實現(xiàn)?
添加Observer
通過runtime偷偷實現(xiàn)了一個子類懊渡,并且以NSKVONotifying_+類名來命名
將之前那個對象的isa指針指向了這個子類刽射,并重寫class方法偽裝成原類。
重寫了觀察的對象setter方法剃执,并且在重寫的中添加了willChangeValueForKey:以及didChangeValueForKey:
移除Observer
只是簡單的將對象的isa指向原來的類對象中誓禁。
請參考: