阿里P6一面

前言:先本身嘗試去回答,回答不了再看參考答案,你才能學的更多!
1.MVC具備什麼樣的優點,各個模塊之間怎麼通訊,好比點擊 Button 後 怎麼通知 Model?
2.兩個無限長度鏈表(也就是可能有環) 判斷有沒有交點
3.UITableView的相關優化
4.KVO、Notification、delegate各自的優缺點,效率還有使用場景
5.如何手動通知KVO
6.Objective-C 中的copy方法
7.runtime 中,SEL和IMP的區別
8.autoreleasepool的使用場景和原理
9.RunLoop的實現原理和數據結構,何時會用到
10.block爲何會有循環引用
11.有沒有本身設計過網絡控件? 12.NSOperation和GCD的區別
13.CoreData的使用,如何處理多線程問題
14.如何設計圖片緩存?
15.有沒有本身設計過網絡控件? 
 
 
1.MVC 具備什麼樣的優點,各個模塊之間怎麼通訊,好比點擊 Button 後 怎麼通知 Model?
MVC 是一種設計思想,一種框架模式,是一種把應用中全部類組織起來的策略,它把你的程序分爲三塊,分別是:
 
M(Model):實際上考慮的是「什麼」問題,你的程序本質上是什麼,獨立於 UI 工做。是程序中用於處理應用程序邏輯的部分,一般負責存取數據。
 
C(Controller):控制你 Model 如何呈如今屏幕上,當它須要數據的時候就告訴 Model,你幫我獲取某某數據;當它須要 UI 展現和更新的時候就告訴 View,你幫我生成一個 UI 顯示某某數據,是 Model 和 View 溝通的橋樑。
V(View):Controller 的手下,是 Controller 要使用的類,用於構建視圖,一般是根據 Model 來建立視圖的。
要了解 MVC 如何工做,首先須要瞭解這三個模塊間如何通訊。
MVC通訊規則
 
Controller to Model
能夠直接單向通訊。Controller 須要將 Model 呈現給用戶,所以須要知道模型的一切,還須要有同 Model 徹底通訊的能力,而且能任意使用 Model 的公共 API。
Controller to View
能夠直接單向通訊。Controller 經過 View 來佈局用戶界面。
Model to View
永遠不要直接通訊。Model 是獨立於 UI 的,並不須要和 View 直接通訊,View 經過 Controller 獲取 Model 數據。
View to Controller
View 不能對 Controller 知道的太多,所以要經過間接的方式通訊。
Target action。首先 Controller 會給本身留一個 target,再把配套的 action 交給 View 做爲聯繫方式。那麼 View 接收到某些變化時,View 就會發送 action 給 target 從而達到通知的目的。這裏 View 只須要發送 action,並不須要知道 Controller 如何去執行方法。
代理。有時候 View 沒有足夠的邏輯去判斷用戶操做是否符合規範,他會把判斷這些問題的權力委託給其餘對象,他只需得到答案就好了,並不會管是誰給的答案。
DataSoure。View 沒有擁有他們所顯示數據的權力,View 只能向 Controller 請求數據進行顯示,Controller 則獲取 Model 的數據整理排版後提供給 View。
Model 訪問 Controller
一樣的 Model 是獨立於 UI 存在的,所以沒法直接與 Controller 通訊,可是當 Model 自己信息發生了改變的時候,會經過下面的方式進行間接通訊。
Notification & KVO一種相似電臺的方法,Model 信息改變時會廣播消息給感興趣的人 ,只要 Controller 接收到了這個廣播的時候就會主動聯繫 Model,獲取新的數據並提供給 View。
從上面的簡單介紹中咱們來簡單歸納一下 MVC 模式的優勢。
1.低耦合性
2.有利於開發分工
3.有利於組件重用
4.可維護性

 
2.兩個無限長度鏈表(也就是可能有環) 判斷有沒有交點?

單鏈表是否存在環?環的入口是什麼?

是否存在環

1) 判斷是否存在環:設置快慢指針fast和slow,fast步速爲2,slow爲1,若最終fast==slow,那麼就證實單鏈表中必定有環。若是沒有環的話,fast必定先到達尾節點
2) 簡單證實:利用相對運動的概念,以slow爲參考點(靜止不動),那麼fast的步速實際爲1,當fast超過slow以後,fast以每步一個節點的速度追趕slow,若是鏈表有環的話,fast必定會追趕到slow,即fast==slow。

如何找到環的入口

第一次相遇
字母表明的量:
  • a:鏈表頭結點到環入口的距離
  • r:環長
  • 藍色線:fast指針所走的距離2s
  • 黑色線:slow指針所走的距離s
假設鏈表總長度爲L,且fast與slow相遇時fast已經繞環走了n圈,則有以下關係:
2s = s + nr
將s移到左邊得:
s = nr
轉換:
s = (n-1)r + r = (n-1)r + L-a
a+x = (n-1)r + L-a
得:
a = (n-1)r + L-a-x
由圖可知,(L-a-x)爲相遇點到環入口點的距離。由上式可知:
從鏈表頭到環入口的距離 = (n-1)圈內環循環 + 相遇點到環入口點的距離
將r視爲週期的話,a與L-a-x在某種意義上是相等的(實際並不必定相等)。
那麼由此咱們便找到了突破點,爲了找到環的入口點,在fast與slow相遇時,將slow指針從新指向單鏈表的頭節點,fast仍然留在相遇點,只不過步速降爲與slow相同的1,每次循環只通過一個節點,如此,當fast與slow再次相遇時,那個新的相遇點即是咱們苦苦尋找的入口點了。

如何知道環的長度

紀錄下相遇點,讓slow與fast從該點開始,再次碰撞所走過的操做數就是環的長度r。

帶環的鏈表的長度是多少?

經過以上分析咱們已經知道了如何求環入口,環長,那麼鏈表長度顯然就是二者之和,即:
L = a + r

判斷兩個鏈表是否相交

分析問題以前咱們要搞清楚鏈表相交的一些基本概念
  • 明確概念:兩個單向鏈表相交,只能是y型相交,不多是x型相交。
  • 分析:有兩個鏈表,La,Lb,設他們的交點設爲p,假設在La中,p的前驅爲pre_a,後繼爲next_a,在Lb中,前驅爲pre_b,後繼爲next_b,則
    pre_a->next=p,pre_b->next=p,接下來看後繼,p->next=next_a,p->next=next_b;那麼問題就出來了,一個單鏈表的next指針只有一個,
    怎麼跑出兩個來呢,因此必有next_a==next_b,因而咱們得出兩個鏈表相交只能是Y型相交。明確了這個概念,咱們再來堆相交問題進行分析。

狀況一:兩個鏈表都無環

1) 問題簡化。將鏈表B接到鏈表A的後面,若是A、B有交點,則構成一個有環的單鏈表,而咱們剛剛在上面已經討論瞭如何判斷一個
單鏈表是否有環。
2) 若兩個鏈表相交則必爲Y型,由此可知兩個鏈表從相交點到尾節點是相同的,咱們並不知道他們的相交點位置,可是咱們能夠遍歷得出A、B鏈表的
尾節點,如此,比較他們的尾節點是否相等即可以求證A、B是否相交了。

狀況二:鏈表有環

1) 其中一個鏈表有環,另一個鏈表無環。則兩個鏈表不可能相交。(啥?你不知道爲啥?本身看看前面的「明確概念」檢討吧)
2) 那麼有環相交的狀況只有當兩個鏈表都有環時纔會出現,若是兩個有環鏈表相交,則他們擁有共通的環,即環上任意一個節點都存在於
兩個鏈表上。所以,經過判斷A鏈表上的快慢指針相遇點是否也在B鏈表上即可以得出兩個鏈表是否相交了。

求相交鏈表的相交點

題目描述:若是兩個無環單向鏈表相交,怎麼求出他們相交的第一個節點呢?
分析:採用對齊的思想。計算兩個鏈表的長度 L1 , L2,分別用兩個指針 p1 , p2 指向兩個鏈表的頭,而後將較長鏈表的 p1(假設爲 p1)向後移動L2 - L1個節點,而後再同時向後移動p1 , p2,直到 p1 = p2。相遇的點就是相交的第一個節點。

 

3.UITableView 的相關優化

前言
1.這篇文章對 UITableView 的優化主要從如下3個方面分析:
◦基礎的優化準則(高度緩存, cell 重用...)
◦學會使用調試工具分析問題
◦異步繪製
2.涉及到 tableView 請必定要 用真機調試!用真機調試!用真機調試!
手機的性能比起電腦仍是差異很大,不要老想着用模擬器調試。必定要用真機才能看出效果。
 
3.不要過早的作複雜的優化
雖然這篇文章講的是如何優化table,可是根據個人經驗,不要一開始就去作這些工做(基本的優化除外),由於無論怎麼說,PM不會閒着的,產品的變更並非由開發人員控制。可是大的優化對代碼的結構仍是有很大影響的,這意味着過早優化可能會拖慢工程的進度。在項目初期能用 xib 就用吧,原本大部分這樣的文章都是不推薦使用 IB 的東西,可是不得不說,在效率上 IB 實在是有了數日然的優點。
 
4.優化老是在 空間 和 時間 之間權衡
通常優化的後期老是以更多的空間換取更短期的響應。這表示可能會增長額外的內存和CPU資源的開銷,須要緩存高度,緩存佈局...,固然也可能有別的考量以時間換取空間。具體怎麼作,還得根據項目相關的業務邏輯肯定。其實我想表達的是目前並無十全十美的方案既能夠節省內存,又能夠加快速度,若是非要說好的話,也只能是在資源調度上下了功夫(若是你知道更好的請告訴我,謝謝)。若是你追求的是很是完美,仍是不要朝下看了。
 
 
基礎的優化準則
1.正確地使用UITableViewCell的重用機制
UITableView最核心的思想就是 UITableViewCell 的重用機制。UITableView 只會建立一屏幕(或一屏幕多一點)的 UITableViewCell ,每當 cell 滑出屏幕範圍時,就會放入到一重用池當中,當要顯示新的 cell 時,先去重用池中取,若沒有可用的,纔會從新建立。這樣能夠極大的減小內存的開銷。
比較早的一種寫法
 static NSString *cellID = @"Cell";
2. UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];
3. if (!cell) {
4.    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellID];
5.    //cell 初始化
6. }
7. // cell 設置數據
8. return cell;
或者經過註冊cell的方式
//註冊cell
9.[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"cell"];
10.//獲取cell   
11.UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
 
12.提早計算好 cell 的高度和佈局。
UITableView有兩個重要的回調方法:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
13.
14.- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
UITableView的回調順序是先屢次調用tableView:heightForRowAtIndexPath: 用來肯定 contentSize 及 Cell 的位置,而後纔會調用 tableView:cellForRowAtIndexPath:,從而來顯示在當前屏幕的 cell 。
iOS8會更厲害,還會邊滑動邊調用 tableView:heightForRowAtIndexPath: ,可見這個方法裏必定不能重複進行着大量的計算。咱們應該提早計算好 cell 的高度而且緩存起來,在回調時直接把高度值直接返回。
這裏說一種我常常採用的策略:
通常在網絡請求結束後,在更新界面以前就把每一個 cell 的高度算好,緩存到相對應的 model 中。
這裏可能有人說要是一個 model 對應多種 cell 怎麼辦?
model 能夠添加多個高度屬性啊,這點空間上的開銷仍是能夠接受的吧。
固然這個時候最好把佈局也都算好了最好,下面的YY的作法會介紹。
 
15.避免阻塞主線程。
不少時候咱們須要從網絡請求圖片等,把這些操做放在後臺執行,而且緩存起來。如今咱們大都使用 SDWebImage 進行網絡圖片處理,正常的使用是沒有大問題的,可是若是對性能要求比較高,或者要處理gif圖,我仍是推薦 YYWebImage,詳細內容請自行移步到github查看,固然這只是我的建議。
還有就是不要在主線程作一些文件的I/O操做。
 
16.按需加載。
這一條真的是看各位喜愛了,我是以爲滾動的過程當中有大量的 「留白」 並不太好,不過做爲優化的建議仍是要考慮的。
如快速滾動時,僅繪製目標位置的 cell ,能夠提升滾動的順暢程度。
具體能夠參考 VVebo
 
17.減小SubViews的數量。
總以爲這條有點多餘,能簡單點的咱們確定不會作複雜了吧。這更多的取決於UI界面的複雜度。
 
18.儘量重用開銷比較大的對象。
如NSDateFormatter 和 NSCalendar等對象初始化很是慢,咱們能夠把它加入類的屬性當中,或者建立單例來使用。
 
19.儘可能減小計算的複雜度
在高分屏儘可能用 ceil 或 floor 或 round 取整。不要出現 1.7,10.007這樣的小數。
 
20.不要動態的add 或者 remove 子控件
最好在初始化時就添加完,而後經過hidden來控制是否顯示。
 
 
學會使用調試工具分析問題
Instruments裏的:
•Core Animation instrument
•OpenGL ES Driver instrument
模擬器中的:
•Color debug options View debugging
Xcode的:
•View debugging
Xcode 已經集成了 Instruments 工具,經過菜單 profile 便可打開。
在模擬器中你能夠在 Debug 中找到以下的菜單:

 

D5806B24-BB86-449D-81C2-82DA247E053C.png
下面是一些常見的調試選項的含義:
1. Color Blended Layers
Instruments能夠在物理機上顯示出被混合的圖層Blended Layer(用紅色標註),
Blended Layer是由於這些Layer是透明的(Transparent),
系統在渲染這些view時須要將該view和下層view混合(Blend)後才能計算出該像素點的實際顏色。
解決辦法:檢查紅色區域view的opaque屬性,記得設置成YES;檢查backgroundColor屬性是否是[UIColor clearColor]
 
2. Color Copied Images
這個選項主要檢查咱們有無使用不正確圖片格式,如果GPU不支持的色彩格式的圖片則會標記爲青色,
則只能由CPU來進行處理。咱們不但願在滾動視圖的時候,CPU實時來進行處理,由於有可能會阻塞主線程。
解決辦法:檢查圖片格式,推薦使用png。
 
3. Color Misaligned Images
這個選項檢查了圖片是否被放縮,像素是否對齊。
被放縮的圖片會被標記爲黃色,像素不對齊則會標註爲紫色。   
若是不對齊此時系統須要對相鄰的像素點作anti-aliasing反鋸齒計算,會增長圖形負擔
一般這種問題出在對某些View的Frame從新計算和設置時產生的。
解決辦法:參考 基本優化準則的第7點
 
4. Color Offscreen-Rendered
這個選項將須要offscreen渲染的的layer標記爲黃色。
離屏渲染意思是iOS要顯示一個視圖時,須要先在後臺用CPU計算出視圖的Bitmap,
再交給GPU作Onscreen-Rendering顯示在屏幕上,由於顯示一個視圖須要兩次計算,
因此這種Offscreen-Rendering會致使app的圖形性能降低。
大部分Offscreen-Rendering都是和視圖Layer的Shadow和Mask相關。
下列狀況會致使視圖的Offscreen-Rendering:
- 使用Core Graphics (CG開頭的類)。
- 使用drawRect()方法,即便爲空。
- 將CALayer的屬性shouldRasterize設置爲YES。
- 使用了CALayer的setMasksToBounds(masks)和setShadow*(shadow)方法。
- 在屏幕上直接顯示文字,包括Core Text。
- 設置UIViewGroupOpacity。
解決辦法:只能減小各類 layer 的特殊效果了。
這篇博文 Designing for iOS: Graphics & Performance 對offsreen以及圖形性能有個很棒的介紹,
 
異步繪製
這個屬於稍高級點的技能。
若是咱們使用 Autolayout 可能就無能爲力了。這也是爲何那麼多人在優化這塊拒絕使用 IB 開發。
可是這裏並不展現具體的繪製代碼。給個簡單的形式參考:
//異步繪製
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    CGRect rect = CGRectMake(0, 0, 100, 100);
    UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0);
    CGContextRef context = UIGraphicsGetCurrentContext();
    [[UIColor lightGrayColor] set];
    CGContextFillRect(context, rect);
 
    //將繪製的內容以圖片的形式返回,並調主線程顯示
    UIImage *temp = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
 
    // 回到主線程
    dispatch_async(dispatch_get_main_queue(), ^{
        //code
    });
});
另外繪製 cell 不建議使用 UIView,建議使用 CALayer。
從形式來講:UIView 的繪製是創建在 CoreGraphic 上的,使用的是 CPU。CALayer 使用的是 Core Animation,CPU,GPU 通吃,由系統決定使用哪一個。View的繪製使用的是自下向上的一層一層的繪製,而後渲染。Layer處理的是 Texure,利用 GPU 的 Texture Cache 和獨立的浮點數計算單元加速 紋理 的處理。
從事件的響應來講:UIView是 CALayer 的代理,layer自己並不能響應事件,由於layer是直接繼承自NSObject,不具有處理事件的能力。而 UIView 是繼承了UIResponder 的,這也是事件轉發的角度上說明,view要比單純的layer複雜的多。在滑動的列表上,多層次的view再加上各類手勢的處理勢必致使幀數的降低。
在這一塊還有個問題就是當 TableView 快速滑動時,會有大量異步繪製任務提交到後臺線程去執行。線程並非越多越好,太多了只會增長 CPU 的負擔。因此咱們須要在適當的時候取消掉不重要的線程。
目前這裏有兩種作法:
YY的作法是:
儘可能快速、提早判斷當前繪製任務是否已經被取消;在繪製每一行文本前,都會調用 isCancelled() 來進行判斷,保證被取消的任務能及時退出,不至於影響後續操做。
VVebo的作法是:
當滑動時,鬆開手指後,馬上計算出滑動中止時 Cell 的位置,並預先繪製那個位置附近的幾個 Cell,而忽略當前滑動中的 Cell。忽略的代價就是快速滑動中會出現大量空白內容。
二者都是不錯的優化方法,各位自行取捨。
 
番外
YYText 的使用
這個框架涉及到的方面仍是不少的,在這裏說再多的理論仍是不如本身去看代碼的好。
關於用法,沒什麼比YY做者說的更明白的了:
當獲取到 API JSON 數據後,我會把每條 Cell 須要的數據都在後臺線程計算並封裝爲一個佈局對象 CellLayout。CellLayout 包含全部文本的 CoreText 排版結果、Cell 內部每一個控件的高度、Cell 的總體高度。每一個 CellLayout 的內存佔用並很少,因此當生成後,能夠所有緩存到內存,以供稍後使用。這樣,TableView 在請求各個高度函數時,不會消耗任何多餘計算量;當把 CellLayout 設置到 Cell 內部時,Cell 內部也不用再計算佈局了。
對於一般的 TableView 來講,提早在後臺計算好佈局結果是很是重要的一個性能優化點。爲了達到最高性能,你可能須要犧牲一些開發速度,不要用 Autolayout 等技術,少用 UILabel 等文本控件。但若是你對性能的要求並不那麼高,能夠嘗試用 TableView 的預估高度的功能,並把每一個 Cell 高度緩存下來。這裏有個來自百度知道團隊的開源項目能夠很方便的幫你實現這一點: FDTemplateLayoutCell
 
一開始我是想要在這裏長篇大論的,不事後來想一想千言萬語仍是不及一個 Demo (還沒作完)。
 
AsyncDisplayKit 的使用
這個框架是 facebook 團隊開源的,它的使用代價有點大,由於它已經不是按照咱們正常的UIKit框架來寫了。因爲這個框架就是創建在各類 Display Node 上的,因此要使用該框架,那麼就須要使用 Display Node 層次結構替換視圖層次結構和/或 Layer 樹。可是這個框架仍是值得嘗試的,由於 AsyncDisplayKit 支持在非主線程執行以前只能在主線程才能執行的任務。這就能減輕主線程的工做量以執行其餘操做,例如處理觸摸事件,滑動事件。
下面附一篇 AsyncDisplayKit 教程
固然,無論是 YYText 仍是 AsyncDisplayKit,他們都有很是流暢的體驗,對於複雜的列表來講都是神器。用好其中一種均可以解決大部分問題了,那麼還有一部分問題來自於哪裏呢?繼續向下看。
 
應該是使用最爲普遍的圖片庫了吧。上面也提到了,咱們正常的使用是沒有大問題的,可是若是對性能要求比較高,或者要處理gif圖,SD 不免會拖慢速度。特別是 gif 的內存暴增問題,SD 一直沒有一個較好的解決方案。
 
這個就是在列表優化時我推薦使用的網絡圖片庫,在對性能要求比較高的狀況下,YY 能夠直接以 layer 做爲圖片的載體這樣減小了至關的一部分資源消耗。具體在什麼狀況下使用layer 什麼狀況下使用 imageView 戳這裏
對於gif圖,YY 還提供了 分享 gif 的方案
 
小結
•若是項目比較緊,我更推薦 IB + 基礎的優化準則,既能夠保證總體效率也不至於卡的太嚴重。
 
•若是項目已經到後期,而且有時間進行大量的優化。我傾向於使用 純代碼 + 異步繪製,這一部分在蘋果如今的多種屏幕尺寸上適配工做量並不小。可是效果卻也是很明顯的。
 
•若是想得到流暢的體驗,可是有沒有太多的時間去作優化,那就可使用一些封裝好的第三方庫,好比 YYTextAsyncDisplayKit
 
 

4.KVO、Notification、delegate 各自的優缺點,效率還有使用場景

在開發ios應用的時候,咱們會常常遇到一個常見的問題:在不過度耦合的前提下,controllers間怎麼進行通訊。在IOS應用不斷的出現三種模式來實現這種通訊:
1.委託delegation;
2.通知中心Notification Center;
3.鍵值觀察key value observing,KVO
delegate的優點:
1.很嚴格的語法,全部能響應的時間必須在協議中有清晰的定義
2.由於有嚴格的語法,因此編譯器能幫你檢查是否實現了全部應該實現的方法,不容易遺忘和出錯
3.使用delegate的時候,邏輯很清楚,控制流程可跟蹤和識別
4.在一個controller中能夠定義多個協議,每一個協議有不一樣的delegate
5.沒有第三方要求保持/監視通訊過程,因此假如出了問題,那咱們能夠比較方便的定位錯誤代碼。
6.可以接受調用的協議方法的返回值,意味着delegate可以提供反饋信息給controller
delegate的缺點:
須要寫的代碼比較多
有一個「Notification Center」的概念,他是一個單例對象,容許當事件發生的時候通知一些對象,知足控制器與一個任意的對象進行通訊的目的,這種模式的基本特徵就是接收到在該controller中發生某種事件而產生的消息,controller用一個key(通知名稱),這樣對於controller是匿名的,其餘的使用一樣地key來註冊了該通知的對象能對通知的事件做出反應。
notification的優點:
1.不須要寫多少代碼,實現比較簡單
2.一個對象發出的通知,多個對象能進行反應,一對多的方式實現很簡單
缺點:
1.編譯期不會接茬通知是否能被正確處理
2.釋放註冊的對象時候,須要在通知中心取消註冊
3.調試的時候,程序的工做以及控制流程難跟蹤
4.須要第三方來管理controller和觀察者的聯繫
5.controller和觀察者須要提早知道通知名稱、UserInfo dictionary keys。若是這些沒有在工做區間定義,那麼會出現不一樣步的狀況
6.通知發出後,發出通知的對象不能從觀察者得到任何反饋。
KVO
KVO是一個對象能觀察另外一個對象屬性的值,前兩種模式更適合一個controller和其餘的對象進行通訊,而KVO適合任何對象監聽另外一個對象的改變,這是一個對象與另一個對象保持同步的一種方法。KVO只能對屬性作出反應,不會用來對方法或者動做作出反應。
優勢:
1.提供一個簡單地方法來實現兩個對象的同步
2.能對非咱們建立的對象作出反應
3.可以提供觀察的屬性的最新值和先前值
4.用keypaths 來觀察屬性,所以也能夠觀察嵌套對象
缺點:
1.觀察的屬性必須使用string來定義,所以編譯器不會出現警告和檢查
2.對屬性的重構將致使觀察不可用
3.複雜的「if」語句要求對象正在觀察多個值,這是由於全部的觀察都經過一個方法來指向
KVO有顯著的使用場景,當你但願監視一個屬性的時候,咱們選用KVO
而notification和delegate有比較類似的用處,
當處理屬性層的消息的事件時候,使用KVO,其餘的儘可能使用delegate,除非代碼須要處理的東西確實很簡單,那麼用通知很方便

5.如何手動通知 KVO

重寫Controller裏面某個屬性的setter方法,聯動給View賦值,使用Controller監控Model裏面某個值的變化,在controller的dealloc函數中用一行代碼告終:removeObserver。

6.Objective-C 中的copy方法
對象的複製就是複製一個對象做爲副本,他會開闢一塊新的內存(堆內存)來存儲副本對象,就像複製文件同樣,即源對象和副本對象是兩塊不一樣的內存區域。對象要具有複製功能,必須實現<NSCopying>協議或者<NSMutableCopying>協議,經常使用的可複製對象有:NSNumber、NSString、NSMutableString、NSArray、NSMutableArray、NSDictionary、NSMutableDictionary
copy:產生對象的副本是不可變的
mutableCopy:產生的對象的副本是可變的
淺拷貝和深拷貝
      淺拷貝值複製對象自己,對象裏的屬性、包含的對象不作複製
      深拷貝則既複製對象自己,對象的屬性也會複製一份
Foundation中支持複製的類,默認是淺複製
對象的自定義拷貝
    對象擁有複製特性,須實現NSCopying,NSMutableCopying協議,實現該協議的CopyWithZone:方法或MutableCopyWithZone:方法。
淺拷貝實現
 
[cpp]  view plain  copy
 
  1. -(id)copyWithZone:(NSZone *)zone{  
  2.              
  3.            Person *person = [[[self Class]allocWithZone:zone]init];  
  4.            p.name = _name;  
  5.            p.age = _age;  
  6.            return person;  
  7.        }  
深拷貝的實現
 
 
[cpp]  view plain  copy
 
  1. -(void)copyWithZone:(NSZone *)zone{  
  2.             Person *person = [[[self Class]allocWithZone:zone]init];  
  3.             person.name = [_name copy];  
  4.             person.age = [_age copy];  
  5.             return person;  
  6.               
  7.         }  
深淺拷貝和retain之間的關係
 
   copy、mutableCopy和retain之間的關係
   Foundation中可複製的對象,當咱們copy的是一個不可變的對象的時候,它的做用至關與retain(cocoa作的內存優化)
   當咱們使用mutableCopy的時候,不管源對象是否可變,副本是可變的
    當咱們copy的 是一個可變對象時,複本不可變
 

 


7.runtime 中,SEL 和 IMP 的區別

方法名 SEL – 表示該方法的名稱;
 
一個 types – 表示該方法參數的類型;
一個IMP     – 指向該方法的具體實現的函數指針,說白了IMP就是實現方法。

8.autoreleasepool 的使用場景和原理

Autorelease Pool全名叫作NSAutoreleasePool,是OC中的一個類。autorelease pool並非天生就有的,你須要手動的去建立它。通常地,在新建一個iphone項目的時候,xcode會自動地爲你建立一個Autorelease Pool,這個pool就寫在Main函數裏面。在NSAutoreleasePool中包含了一個可變數組,用來存儲被聲明爲autorelease的對象。當NSAutoreleasePool自身被銷燬的時候,它會遍歷這個數組,release數組中的每個成員(注意,這裏只是release,並無直接銷燬對象)。若成員的retain count 大於1,那麼對象沒有被銷燬,形成內存泄露。默認的NSAutoreleasePool 只有一個,你能夠在你的程序中建立NSAutoreleasePool,被標記爲autorelease的對象會跟最近的NSAutoreleasePool匹配。能夠嵌套使用NSAutoreleasePool。

Objective-C Autorelease Pool 的實現原理

內存管理一直是學習 Objective-C 的重點和難點之一,儘管如今已是 ARC 時代了,可是瞭解 Objective-C 的內存管理機制仍然是十分必要的。其中,弄清楚 autorelease 的原理更是重中之重,只有理解了 autorelease 的原理,咱們纔算是真正瞭解了 Objective-C 的內存管理機制。注:本文使用的  runtime 源碼是當前的最新版本 objc4-646.tar.gz 。

autoreleased 對象何時釋放

autorelease 本質上就是延遲調用 release ,那 autoreleased 對象究竟會在何時釋放呢?爲了弄清楚這個問題,咱們先來作一個小實驗。這個小實驗分 3 種場景進行,請你先自行思考在每種場景下的 console 輸出,以加深理解。注:本實驗的源碼能夠在這裏  AutoreleasePool 找到。
特別說明:在蘋果一些新的硬件設備上,本實驗的結果已經再也不成立,詳細狀況以下:
  • iPad 2
  • iPad Air
  • iPad Air 2
  • iPad Pro
  • iPad Retina
  • iPhone 4s
  • iPhone 5
  • iPhone 5s
  • iPhone 6
  • iPhone 6 Plus
  • iPhone 6s
  • iPhone 6s Plus
  • __weak NSString *string_weak_ = nil;
    - (void)viewDidLoad {
        [super viewDidLoad];
        // 場景 1
        NSString *string = [NSString stringWithFormat:@"leichunfeng"];
        string_weak_ = string;
        // 場景 2
    //    @autoreleasepool {
    //        NSString *string = [NSString stringWithFormat:@"leichunfeng"];
    //        string_weak_ = string;
    //    }
        // 場景 3
    //    NSString *string = nil;
    //    @autoreleasepool {
    //        string = [NSString stringWithFormat:@"leichunfeng"];
    //        string_weak_ = string;
    //    }
        NSLog(@"string: %@", string_weak_);
    }
    - (void)viewWillAppear:(BOOL)animated {
        [super viewWillAppear:animated];
        NSLog(@"string: %@", string_weak_);
    }
    - (void)viewDidAppear:(BOOL)animated {
        [super viewDidAppear:animated];
        NSLog(@"string: %@", string_weak_);
    }
     思考得怎麼樣了?相信在你心中已經有答案了。那麼讓咱們一塊兒來看看 console 輸出:
  • // 場景 1
    2015-05-30 10:32:20.837 AutoreleasePool[33876:1448343] string: leichunfeng
    2015-05-30 10:32:20.838 AutoreleasePool[33876:1448343] string: leichunfeng
    2015-05-30 10:32:20.845 AutoreleasePool[33876:1448343] string: (null)
    // 場景 2
    2015-05-30 10:32:50.548 AutoreleasePool[33915:1448912] string: (null)
    2015-05-30 10:32:50.549 AutoreleasePool[33915:1448912] string: (null)
    2015-05-30 10:32:50.555 AutoreleasePool[33915:1448912] string: (null)
    // 場景 3
    2015-05-30 10:33:07.075 AutoreleasePool[33984:1449418] string: leichunfeng
    2015-05-30 10:33:07.075 AutoreleasePool[33984:1449418] string: (null)
    2015-05-30 10:33:07.094 AutoreleasePool[33984:1449418] string: (null)
  • 跟你預想的結果有出入嗎?Any way ,咱們一塊兒來分析下爲何會獲得這樣的結果。
    分析:3 種場景下,咱們都經過 [NSString stringWithFormat:@"leichunfeng"] 建立了一個 autoreleased 對象,這是咱們實驗的前提。而且,爲了可以在 viewWillAppear 和 viewDidAppear中繼續訪問這個對象,咱們使用了一個全局的 __weak 變量 string_weak_ 來指向它。由於 __weak 變量有一個特性就是它不會影響所指向對象的生命週期,這裏咱們正是利用了這個特性。
    場景 1:當使用 [NSString stringWithFormat:@"leichunfeng"] 建立一個對象時,這個對象的引用計數爲 1 ,而且這個對象被系統自動添加到了當前的 autoreleasepool 中。當使用局部變量 string 指向這個對象時,這個對象的引用計數 +1 ,變成了 2 。由於在 ARC 下 NSString *string 本質上就是 __strong NSString *string 。因此在 viewDidLoad 方法返回前,這個對象是一直存在的,且引用計數爲 2 。而當 viewDidLoad 方法返回時,局部變量 string 被回收,指向了 nil 。所以,其所指向對象的引用計數 -1 ,變成了 1 。
    而在 viewWillAppear 方法中,咱們仍然能夠打印出這個對象的值,說明這個對象並無被釋放。咦,這不科學吧?我讀書少,你表騙我。不是一直都說當函數返回的時候,函數內部產生的對象就會被釋放的嗎?若是你這樣想的話,那我只能說:騷年你太年經了。開個玩笑,咱們繼續。前面咱們提到了,這個對象是一個 autoreleased 對象,autoreleased 對象是被添加到了當前最近的 autoreleasepool 中的,只有當這個 autoreleasepool 自身 drain 的時候,autoreleasepool 中的 autoreleased 對象纔會被 release 。
    另外,咱們注意到當在 viewDidAppear 中再打印這個對象的時候,對象的值變成了 nil ,說明此時對象已經被釋放了。所以,咱們能夠大膽地猜想一下,這個對象必定是在 viewWillAppear 和 viewDidAppear 方法之間的某個時候被釋放了,而且是因爲它所在的 autoreleasepool 被 drain 的時候釋放的。
    你說什麼就是什麼咯?有本事你就證實給我看你媽是你媽。額,這個我真證實不了,不過上面的猜想我仍是能夠證實的,不信,你看!
    在開始前,我先簡單地說明一下原理,咱們能夠經過使用 lldb 的 watchpoint 命令來設置觀察點,觀察全局變量 string_weak_ 的值的變化,string_weak_ 變量保存的就是咱們建立的 autoreleased 對象的地址。在這裏,咱們再次利用了 __weak 變量的另一個特性,就是當它所指向的對象被釋放時,__weak 變量的值會被置爲 nil 。瞭解了基本原理後,咱們開始驗證上面的猜想。
    咱們先在第 35 行打一個斷點,當程序運行到這個斷點時,咱們經過 lldb 命令 watchpoint set v string_weak_ 設置觀察點,觀察 string_weak_ 變量的值的變化。以下圖所示,咱們將在 console 中看到相似的輸出,說明咱們已經成功地設置了一個觀察點:
  • 設置好觀察點後,點擊 Continue program execution 按鈕,繼續運行程序,咱們將看到以下圖所示的界面:html

  • 咱們先看 console 中的輸出,注意到 string_weak_ 變量的值由 0x00007f9b886567d0 變成了 0x0000000000000000 ,也就是 nil 。說明此時它所指向的對象被釋放了。另外,咱們也能夠注意到一個細節,那就是 console 中打印了兩次對象的值,說明此時 viewWillAppear 也已經被調用了,而 viewDidAppear 尚未被調用。ios

    接着,咱們來看看左側的線程堆棧。咱們看到了一個很是敏感的方法調用 -[NSAutoreleasePool release] ,這個方法最終經過調用 AutoreleasePoolPage::pop(void *) 函數來負責對 autoreleasepool 中的 autoreleased 對象執行 release 操做。結合前面的分析,咱們知道在 viewDidLoad 中建立的 autoreleased 對象在方法返回後引用計數爲 1 ,因此通過這裏的 release 操做後,這個對象的引用計數 -1 ,變成了 0 ,該 autoreleased 對象最終被釋放,猜想得證。
    另外,值得一提的是,咱們在代碼中並無手動添加 autoreleasepool ,那這個 autoreleasepool 到底是哪裏來的呢?看完後面的章節你就明白了。
    場景 2:同理,當經過 [NSString stringWithFormat:@"leichunfeng"] 建立一個對象時,這個對象的引用計數爲 1 。而當使用局部變量 string 指向這個對象時,這個對象的引用計數 +1 ,變成了 2 。而出了當前做用域時,局部變量 string 變成了 nil ,因此其所指向對象的引用計數變成 1 。另外,咱們知道當出了 @autoreleasepool {} 的做用域時,當前 autoreleasepool 被 drain ,其中的 autoreleased 對象被 release 。因此這個對象的引用計數變成了 0 ,對象最終被釋放。
    場景 3:同理,當出了 @autoreleasepool {} 的做用域時,其中的 autoreleased 對象被 release ,對象的引用計數變成 1 。當出了局部變量 string 的做用域,即 viewDidLoad 方法返回時,string 指向了 nil ,其所指向對象的引用計數變成 0 ,對象最終被釋放。
    理解在這 3 種場景下,autoreleased 對象何時釋放對咱們理解 Objective-C 的內存管理機制很是有幫助。其中,場景 1 出現得最多,就是不須要咱們手動添加 @autoreleasepool {} 的狀況,直接使用系統維護的 autoreleasepool ;場景 2 就是須要咱們手動添加 @autoreleasepool {} 的狀況,手動干預 autoreleased 對象的釋放時機;場景 3 是爲了區別場景 2 而引入的,在這種場景下並不能達到出了 @autoreleasepool {} 的做用域時 autoreleased 對象被釋放的目的。
    PS:請讀者參考場景 1 的分析過程,使用 lldb 命令 watchpoint 自行驗證下在場景 2 和場景 3 下 autoreleased 對象的釋放時機,you should give it a try yourself 。

    AutoreleasePoolPage

    細心的讀者應該已經有所察覺,咱們在上面已經提到了 -[NSAutoreleasePool release] 方法最終是經過調用 AutoreleasePoolPage::pop(void *) 函數來負責對 autoreleasepool 中的 autoreleased 對象執行 release 操做的。
    那這裏的 AutoreleasePoolPage 是什麼東西呢?其實,autoreleasepool 是沒有單獨的內存結構的,它是經過以 AutoreleasePoolPage 爲結點的雙向鏈表來實現的。咱們打開 runtime 的源碼工程,在  NSObject.mm 文件的第 438-932 行能夠找到 autoreleasepool 的實現源碼。經過閱讀源碼,咱們能夠知道:
    • 每個線程的 autoreleasepool 其實就是一個指針的堆棧;
    • 每個指針表明一個須要 release 的對象或者 POOL_SENTINEL(哨兵對象,表明一個 autoreleasepool 的邊界);
    • 一個 pool token 就是這個 pool 所對應的 POOL_SENTINEL 的內存地址。當這個 pool 被 pop 的時候,全部內存地址在 pool token 以後的對象都會被 release ;
    • 這個堆棧被劃分紅了一個以 page 爲結點的雙向鏈表。pages 會在必要的時候動態地增長或刪除;
    • Thread-local storage(線程局部存儲)指向 hot page ,即最新添加的 autoreleased 對象所在的那個 page 。
    一個空的 AutoreleasePoolPage 的內存結構以下圖所示:
     
  •  
    1. magic 用來校驗 AutoreleasePoolPage 的結構是否完整;
    2. next 指向最新添加的 autoreleased 對象的下一個位置,初始化時指向 begin() ;
    3. thread 指向當前線程;
    4. parent 指向父結點,第一個結點的 parent 值爲 nil ;
    5. child 指向子結點,最後一個結點的 child 值爲 nil ;
    6. depth 表明深度,從 0 開始,日後遞增 1;
    7. hiwat 表明 high water mark 。
    另外,當 next == begin() 時,表示 AutoreleasePoolPage 爲空;當 next == end() 時,表示 AutoreleasePoolPage 已滿。

    Autorelease Pool Blocks

    咱們使用 clang -rewrite-objc 命令將下面的 Objective-C 代碼重寫成 C++ 代碼:
  • 1
    2
    3
     
    @autoreleasepool {
    }

將會獲得如下輸出結果(只保留了相關代碼):git

extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);
struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
}
 
 
 不得不說,蘋果對 @autoreleasepool {} 的實現真的是很是巧妙,真正能夠稱得上是代碼的藝術。蘋果經過聲明一個 __AtAutoreleasePool 類型的局部變量 __autoreleasepool 來實現 @autoreleasepool {} 。當聲明 __autoreleasepool 變量時,構造函數 __AtAutoreleasePool()被調用,即執行 atautoreleasepoolobj = objc_autoreleasePoolPush(); ;當出了當前做用域時,析構函數 ~__AtAutoreleasePool() 被調用,即執行 objc_autoreleasePoolPop(atautoreleasepoolobj); 。也就是說 @autoreleasepool {} 的實現代碼能夠進一步簡化以下:
/* @autoreleasepool */ {
    void *atautoreleasepoolobj = objc_autoreleasePoolPush();
    // 用戶代碼,全部接收到 autorelease 消息的對象會被添加到這個 autoreleasepool 中
    objc_autoreleasePoolPop(atautoreleasepoolobj);
}
 
 
 所以,單個 autoreleasepool 的運行過程能夠簡單地理解爲 objc_autoreleasePoolPush()、[對象 autorelease] 和 objc_autoreleasePoolPop(void *) 三個過程。

push 操做

上面提到的 objc_autoreleasePoolPush() 函數本質上就是調用的 AutoreleasePoolPage 的 push 函數。
 
void *
objc_autoreleasePoolPush(void)
{
    if (UseGC) return nil;
    return AutoreleasePoolPage::push();
}
所以,咱們接下來看看 AutoreleasePoolPage 的 push 函數的做用和執行過程。一個 push 操做其實就是建立一個新的 autoreleasepool ,對應 AutoreleasePoolPage 的具體實現就是往 AutoreleasePoolPage 中的 next 位置插入一個 POOL_SENTINEL ,而且返回插入的 POOL_SENTINEL 的內存地址。這個地址也就是咱們前面提到的 pool token ,在執行 pop 操做的時候做爲函數的入參。
static inline void *push()
{
    id *dest = autoreleaseFast(POOL_SENTINEL);
    assert(*dest == POOL_SENTINEL);
    return dest;
}push 函數經過調用 autoreleaseFast 函數來執行具體的插入操做。
 
1
2
3
4
5
6
7
8
9
10
11
 
static inline id *autoreleaseFast(id obj)
{
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
        return page->add(obj);
    } else if (page) {
        return autoreleaseFullPage(obj, page);
    } else {
        return autoreleaseNoPage(obj);
    }
}
 
autoreleaseFast 函數在執行一個具體的插入操做時,分別對三種狀況進行了不一樣的處理:
  1. 當前 page 存在且沒有滿時,直接將對象添加到當前 page 中,即 next 指向的位置;
  2. 當前 page 存在且已滿時,建立一個新的 page ,並將對象添加到新建立的 page 中;
  3. 當前 page 不存在時,即尚未 page 時,建立第一個 page ,並將對象添加到新建立的 page 中。
每調用一次 push 操做就會建立一個新的 autoreleasepool ,即往 AutoreleasePoolPage 中插入一個 POOL_SENTINEL ,而且返回插入的 POOL_SENTINEL 的內存地址。

autorelease 操做

經過  NSObject.mm 源文件,咱們能夠找到 -autorelease 方法的實現:
 
1
2
3
 
- (id)autorelease {
    return ((id)self)->rootAutorelease();
}
 
經過查看 ((id)self)->rootAutorelease() 的方法調用,咱們發現最終調用的就是 AutoreleasePoolPage 的 autorelease 函數。
 
1
2
3
4
5
6
7
 
__attribute__((noinline,used))
id
objc_object::rootAutorelease2()
{
    assert(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}
 
AutoreleasePoolPage 的 autorelease 函數的實現對咱們來講就比較容量理解了,它跟 push 操做的實現很是類似。只不過 push 操做插入的是一個 POOL_SENTINEL ,而 autorelease 操做插入的是一個具體的 autoreleased 對象。
 
1
2
3
4
5
6
7
8
 
static inline id autorelease(id obj)
{
    assert(obj);
    assert(!obj->isTaggedPointer());
    id *dest __unused = autoreleaseFast(obj);
    assert(!dest  ||  *dest == obj);
    return obj;
}
 

pop 操做

同理,前面提到的 objc_autoreleasePoolPop(void *) 函數本質上也是調用的 AutoreleasePoolPage 的 pop 函數。
 
1
2
3
4
5
6
7
8
9
10
 
void
objc_autoreleasePoolPop(void *ctxt)
{
    if (UseGC) return;
    // fixme rdar://9167170
    if (!ctxt) return;
    AutoreleasePoolPage::pop(ctxt);
}
 
pop 函數的入參就是 push 函數的返回值,也就是 POOL_SENTINEL 的內存地址,即 pool token 。當執行 pop 操做時,內存地址在 pool token 以後的全部 autoreleased 對象都會被 release 。直到 pool token 所在 page 的 next 指向 pool token 爲止。
下面是某個線程的 autoreleasepool 堆棧的內存結構圖,在這個 autoreleasepool 堆棧中總共有兩個 POOL_SENTINEL ,即有兩個 autoreleasepool 。該堆棧由三個 AutoreleasePoolPage 結點組成,第一個 AutoreleasePoolPage 結點爲 coldPage() ,最後一個 AutoreleasePoolPage 結點爲 hotPage() 。其中,前兩個結點已經滿了,最後一個結點中保存了最新添加的 autoreleased 對象 objr3 的內存地址。
此時,若是執行 pop(token1) 操做,那麼該 autoreleasepool 堆棧的內存結構將會變成以下圖所示:

NSThread、NSRunLoop 和 NSAutoreleasePool

根據蘋果官方文檔中對  NSRunLoop 的描述,咱們能夠知道每個線程,包括主線程,都會擁有一個專屬的 NSRunLoop 對象,而且會在有須要的時候自動建立。
Each NSThread object, including the application’s main thread, has an NSRunLoop object automatically created for it as needed.
一樣的,根據蘋果官方文檔中對  NSAutoreleasePool 的描述,咱們可知,在主線程的 NSRunLoop 對象(在系統級別的其餘線程中應該也是如此,好比經過 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) 獲取到的線程)的每一個 event loop 開始前,系統會自動建立一個 autoreleasepool ,並在 event loop 結束時 drain 。咱們上面提到的場景 1 中建立的 autoreleased 對象就是被系統添加到了這個自動建立的 autoreleasepool 中,並在這個 autoreleasepool 被 drain 時獲得釋放。
The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event.
另外,NSAutoreleasePool 中還提到,每個線程都會維護本身的 autoreleasepool 堆棧。換句話說 autoreleasepool 是與線程緊密相關的,每個 autoreleasepool 只對應一個線程。
Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects.
弄清楚 NSThread、NSRunLoop 和 NSAutoreleasePool 三者之間的關係能夠幫助咱們從總體上了解 Objective-C 的內存管理機制,清楚系統在背後到底爲咱們作了些什麼,理解整個運行機制等。

總結

看到這裏,相信你應該對 Objective-C 的內存管理機制有了更進一步的認識。一般狀況下,咱們是不須要手動添加 autoreleasepool 的,使用線程自動維護的 autoreleasepool 就行了。根據蘋果官方文檔中對  Using Autorelease Pool Blocks 的描述,咱們知道在下面三種狀況下是須要咱們手動添加 autoreleasepool 的:
  1. 若是你編寫的程序不是基於 UI 框架的,好比說命令行工具;
  2. 若是你編寫的循環中建立了大量的臨時對象;
  3. 若是你建立了一個輔助線程。
 

9.RunLoop 的實現原理和數據結構,何時會用到
 
答案一:
Run loops是線程的基礎架構部分。一個run loop就是一個事件處理循環,用來不停的調配工做以及處理輸入事件。使用run loop的目的是使你的線程在有工做的時候工做,沒有的時候休眠。
Run loop的管理並不徹底是自動的。你仍必須設計你的線程代碼以在適當的時候啓動run loop並正確響應輸入事件。Cocoa和CoreFundation都提供了run loop對象方便配置和管理線程的run loop。你建立的程序不須要顯示的建立run loop;每一個線程,包括程序的主線程(main thread)都有與之相應的run loop對象。可是,本身建立的次線程是須要手動運行run loop的。在carbon和cocoa程序中,程序啓動時,主線程會自行建立並運行run loop。
接下來的部分將會詳細介紹run loop以及如何爲你的程序管理run loop。關於run loop對象能夠參閱sdk文檔。
解析Run Loop
run loop,顧名思義,就是一個循環,你的線程在這裏開始,並運行事件處理程序來響應輸入事件。你的代碼要有實現循環部分的控制語句,換言之就是要有while或for語句。在run loop中,使用run loop對象來運行事件處理代碼:響應接收到的事件,啓動已經安裝的處理程序。
Run loop處理的輸入事件有兩種不一樣的來源:輸入源(input source)和定時源(timer source)。輸入源傳遞異步消息,一般來自於其餘線程或者程序。定時源則傳遞同步消息,在特定時間或者必定的時間間隔發生。兩種源的處理都使用程序的某一特定處理路徑。
圖1-1顯示了run loop的結構以及各類輸入源。輸入源傳遞異步消息給相應的處理程序,並調用runUntilDate:方法退出。定時源則直接傳遞消息給處理程序,但並不會退出run loop。
 
 
參考答案二:
Run loops是線程的基礎架構部分。一個run loop就是一個事件處理循環,用來不停的調配工做以及處理輸入事件。使用run loop的目的是使你的線程在有工做的時候工做,沒有的時候休眠。
Run loop的管理並不徹底是自動的。你仍必須設計你的線程代碼以在適當的時候啓動run loop並正確響應輸入事件。Cocoa和CoreFundation都提供了run loop對象方便配置和管理線程的run loop。你建立的程序不須要顯示的建立run loop;每一個線程,包括程序的主線程(main thread)都有與之相應的run loop對象。可是,本身建立的次線程是須要手動運行run loop的。在carbon和cocoa程序中,程序啓動時,主線程會自行建立並運行run loop。
接下來的部分將會詳細介紹run loop以及如何爲你的程序管理run loop。關於run loop對象能夠參閱sdk文檔。
解析Run Loop
run loop,顧名思義,就是一個循環,你的線程在這裏開始,並運行事件處理程序來響應輸入事件。你的代碼要有實現循環部分的控制語句,換言之就是要有while或for語句。在run loop中,使用run loop對象來運行事件處理代碼:響應接收到的事件,啓動已經安裝的處理程序。
Run loop處理的輸入事件有兩種不一樣的來源:輸入源(input source)和定時源(timer source)。輸入源傳遞異步消息,一般來自於其餘線程或者程序。定時源則傳遞同步消息,在特定時間或者必定的時間間隔發生。兩種源的處理都使用程序的某一特定處理路徑。
圖1-1顯示了run loop的結構以及各類輸入源。輸入源傳遞異步消息給相應的處理程序,並調用runUntilDate:方法退出。定時源則直接傳遞消息給處理程序,但並不會退出run loop。

 

   圖1-1 run loop結構和幾種源
除了處理輸入源,run loop也會生成關於run loop行爲的notification。註冊的run-loop 觀察者能夠收到這些notification,並作相應的處理。可使用Core Foundation在你的線程註冊run-loop觀察者。
下面介紹run loop的組成,以及其運行的模式。同時也說起在處理程序中不一樣時間發送不一樣的notification。
Run Loop Modes
Run loop模式是全部要監視的輸入源和定時源以及要通知的註冊觀察者的集合。每次運行run loop都會指定其運行在哪一個模式下。之後,只有相應的源會被監視並容許接收他們傳遞的消息。(相似的,只有相應的觀察者會收到通知)。其餘模式關聯的源只有在run loop運行在其模式下才會運行,不然處於暫停狀態。
一般代碼中經過指定名字來肯定模式。Cocoa和core foundation定義了默認的以及一系列經常使用的模式,都是用字符串來標識。固然你也能夠指定字符串來自定義模式。雖然你能夠給模式指定任何名字,可是全部的模式內容都是相同的。你必須添加輸入源,定時器或者run loop觀察者到你定義的模式中。
經過指定模式可使得run loop在某一階段只關注感興趣的源。大多數時候,run loop都是運行在系統定義的默認模式。可是模態面板(modal panel)能夠運行在 「模態」模式下。在這種模式下,只有和模態面板相關的源能夠傳遞消息給線程。對於次線程,可使用自定義模式處理時間優先的操做,即屏蔽優先級低的源傳遞消息。
Note:模式區分基於事件的源而非事件的種類。例如,你不可使用模式只選擇處理鼠標按下或者鍵盤事件。你可使用模式監聽端口,暫停定時器或者其餘對源或者run loop觀察者的處理,只要他們在當前模式下處於監聽狀態。
表1-1列出了cocoa和Core Foundation預先定義的模式。
表1-1
輸入源
輸入源向線程發送異步消息。消息來源取決於輸入源的種類:基於端口的輸入源和自定義輸入源。基於端口的源監聽程序相應的端口,而自定義輸入源則關注自定義的消息。至於run loop,它不關心輸入源的種類。系統會去實現兩種源供你使用。兩類輸入源的區別在於如何顯示的:基於端口的源由內核自動發送,而自定義的則須要人工從其餘線程發送。
當你建立輸入源,你須要將其分配給run loop中的一個或多個模式。模式只會在特定事件影響監聽的源。大多數狀況下,run loop運行在默認模式下,可是你也可使其運行在自定義模式。若某一源在當前模式下不被監聽,那麼任何其生成的消息只有當run loop運行在其關聯的模式下才會被傳遞。
下面討論這幾種輸入源。
基於端口的源:
cocoa和core foundation爲使用端口相關的對象和函數建立的基於端口的源提供了內在支持。Cocoa中你從不須要直接建立輸入源。你只須要簡單的建立端口對象,並使用NSPort的方法將端口對象加入到run loop。端口對象會處理建立以及配置輸入源。
在core foundation,你必須手動的建立端口和源,你均可以使用端口類型(CFMachPortRef,CFMessagePortRef,CFSocketRef)來建立。
更多例子能夠看 配置基於端口的源。
自定義輸入源:
在Core Foundation程序中,必須使用CFRunLoopSourceRef類型相關的函數來建立自定義輸入源,接着使用回調函數來配置輸入源。Core Fundation會在恰當的時候調用回調函數,處理輸入事件以及清理源。
除了定義如何處理消息,你也必須定義源的消息傳遞機制——它運行在單獨的進程,並負責傳遞數據給源和通知源處理數據。消息傳遞機制的定義取決於你,但最好不要過於複雜。
關於建立自定義輸入源的例子,見 定義自定義輸入源。關於自定義輸入源的信息參見CFRunLoopSource。
Cocoa Perform Selector Sources:
除了基於端口的源,Cocoa提供了能夠在任一線程執行函數(perform selector)的輸入源。和基於端口的源同樣,perform selector請求會在目標線程上序列化,減緩許多在單個線程上容易引發的同步問題。而和基於端口的源不一樣的是,perform selector執行完後會自動清除出run loop。
當perform selector在其它線程中執行時,目標線程須有一活動中的run loop。對於你建立的線程而言,這意味着線程直到你顯示的開始run loop不然處於等待狀態。然而,因爲主線程本身啓動run loop,在程序調用applicationDidFinishlaunching:的時候你會遇到線程調用的問題。由於Run loop經過每次循環來處理全部排列的perform selector調用,而不時經過每次的循環迭代來處理perform selector。
表1-2列出了NSObject能夠在其它線程使用的perform selector。因爲這些方法時定義在NSObject的,你能夠在包括POSIX的全部線程中使用只要你有objc對象的訪問權。注意這些方法實際上並無建立新的線程以運行perform selector。
                      表1-2
定時源
定時源在預設的時間點同步地傳遞消息。定時器時線程通知本身作某事的一種方法。例如,搜索控件可使用定時器,當用戶連續輸入的時間超過必定時間時,就開始一次搜索。這樣,用戶就能夠有足夠的時間來輸入想要搜索的關鍵字。
儘管定時器和時間有關,但它並非實時的。和輸入源同樣,定時器也是和run loop的運行模式相關聯的。若是定時器所在的模式未被run loop監視,那麼定時器將不會開始直到run loop運行在相應的模式下。相似的,若是定時器在run loop處理某一事件時開始,定時器會一直等待直到下次run loop開始相應的處理程序。若是run loop再也不運行,那定時器也將永遠不開始。
你能夠選擇定時器工做一次仍是定時工做。若是定時工做,定時器會基於安排好的時間而非實際時間,自動的開始。舉個例子,定時器在某一特定時間開始並設置5秒重複,那麼定時器會在那個特定時間後5秒啓動,即便在那個特定時間定時器延時啓動了。若是定時器延遲到接下來設定的一個會多個5秒,定時器在這些時間段中也只會啓動一次,在此以後,正常運行。(假設定時器在時間1,5,9。。。運行,若是最初延遲到7才啓動,那仍是從9,13,。。。開始)。
Run Loop觀察者
源是同步或異步的傳遞消息,而run loop觀察者則是在運行run loop的時候在特定的時候開始。你可使用run loop觀察者來爲某一特定事件或是進入休眠的線程作準備。你能夠將觀察者將如下事件關聯:
  • Run loop入口
  • Run loop將要開始定時
  • Run loop將要處理輸入源
  • Run loop將要休眠
  • Run loop被喚醒但又在執行喚醒事件前
  • Run loop終止
你能夠給cocoa和carbon程序隨意添加觀察者,可是若是你要定義觀察者的話就只能使用core fundation。使用CFRunLoopObserverRed類型來建立觀察者實例,它會追蹤你自定義的回調函數以及其它你感興趣的地方。
和定時器相似,觀察者能夠只用一次或循環使用。若只用一次,那在結束的時候會移除run loop,而循環的觀察者則不會。你須要制定觀察者是一次/屢次使用。
消息的run loop順序
每次啓動,run loop會自動處理以前未處理的消息,並通知觀察者。具體的順序,以下:
  1. 通知觀察者,run loop啓動
  2. 通知觀察者任何即將要開始的定時器
  3. 通知觀察者任何非基於端口的源即將啓動
  4. 啓動任何準備好的非基於端口的源
  5. 若是基於端口的源準備好並處於等待狀態,當即啓動;並進入步驟9。
  6. 通知觀察者線程進入休眠
  7. 將線程之於休眠直到任一下面的事件發生
  • 某一事件到達基於端口的源
  • 定時器啓動
  • 設置了run loop的終止時間
  • run loop喚醒
  1. 通知觀察者線程將被喚醒。
  2. 處理未處理的事件
  • 若是用戶定義的定時器啓動,處理定時事件並重啓run loop。進入步驟2
  • 若是輸入源啓動,傳遞相應的消息
  • run loop喚醒但未終止,重啓。進入步驟2
  1. 通知觀察者run loop結束。
(標號應該連續,不知道怎麼改)
由於觀察者的消息傳遞是在相應的事件發生以前,因此二者之間可能存在偏差。若是須要精確時間控制,你可使用休眠和喚醒通知以此來校對實際發生的事件。
由於定時器和其它週期性事件那是在run loop運行後才啓動,撤銷run loop也會終止消息傳遞。典型的例子就是鼠標路徑追蹤。由於你的代碼直接獲取到消息而不是經由程序傳遞,從而不會在實際的時間開始而須使得鼠標追蹤結束並將控制權交給程序後才行。
使用run loop對象能夠喚醒Run loop。其它消息也能夠喚醒run loop。例如,添加新的非基於端口的源到run loop從而能夠當即執行輸入源而不是等待其餘事件發生後再執行。
什麼時候使用Run Loop
只有在爲你的程序建立次線程的時候,才須要運行run loop。對於程序的主線程而言,run loop是關鍵部分。Cocoa和carbon程序提供了運行主線程run loop的代碼同時也會自動運行run loop。IOS程序UIApplication中的run方法在程序正常啓動的時候就會啓動run loop。一樣的這部分工做在carbon程序中由RunApplicationEventLoop負責。若是你使用xcode提供的模板建立的程序,那你永遠不須要本身去啓動run loop。
而對於次線程,你須要判斷是否須要run loop。若是須要run loop,那麼你要負責配置run loop並啓動。你不須要在任何狀況下都去啓動run loop。好比,你使用線程去處理一個預先定義好的耗時極長的任務時,你就能夠毋需啓動run loop。Run loop只在你要和線程有交互時才須要,好比如下狀況:
  • 使用端口或自定義輸入源和其餘線程通訊
  • 使用定時器
  • cocoa中使用任何performSelector
  • 使線程履行週期性任務
若是決定在程序中使用run loop,那麼配置和啓動都須要本身完成。和全部線程編程同樣,你須要計劃好什麼時候退出線程。在退出前結束線程每每是比被強制關閉好的選擇。詳細的配置和推出run loop的信息見 使用run loop對象。
使用Run loop對象
run loop對象提供了添加輸入源,定時器和觀察者以及啓動run loop的接口。每一個線程都有惟一的與之關聯的run loop對象。在cocoa中,是NSRunLoop對象;而在carbon或BSD程序中則是指向CFRunLoopRef類型的指針。
得到run loop對象
得到當前線程的run loop,能夠採用:
  • cocoa:使用NSRunLoop的currentRunLoop類方法
  • 使用CFRunLoopGetCurrent函數
雖然CFRunLoopRef類型和NSRunLoop對象並不徹底等價,你仍是能夠從NSRunLoop對象中獲取CFRunLoopRef類型。你可使用NSRunLoop的getCFRunLoop方法,返回CFRunLoopRef類型到Core Fundation中。由於二者都指向同一個run loop,你能夠任一替換使用。
配置run loop
在次線程啓動run loop前,你必須至少添加一類源。由於若是run loop沒有任何源須要監視的話,它會在你啓動之際立馬退出。
此外,你也能夠添加run loop觀察者來監視run loop的不一樣執行階段。首先你能夠建立CFRunLoopObserverRef類型並使用CFRunLoopAddObserver將它添加金run loop。注意即便是cocoa程序,run loop觀察者也須要由core foundation函數建立。
如下代碼3-1實現了添加觀察者進run loop,代碼簡單的創建了一個觀察者來監視run loop的全部活動,並將run loop的活動打印出來。
 Creating a run loop observer
 
- (void)threadMain
 
{
 
// The application uses garbage collection, so no autorelease pool is needed.
 
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
 
 
 
// Create a run loop observer and attach it to the run loop.
 
CFRunLoopObserverContext context = {0, self, NULL, NULL, NULL};
 
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
 
kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
 
 
 
if (observer)
 
{
 
CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop];
 
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
 
}
 
 
 
// Create and schedule the timer.
 
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self
 
selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
 
 
 
NSInteger loopCount = 10;
 
do
 
{
 
// Run the run loop 10 times to let the timer fire.
 
[myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
 
loopCount--;
 
}
 
while (loopCount);
 
}
 
 
若是線程運行事件長,最好添加一個輸入源到run loop以接收消息。雖然你可使用定時器,可是定時器一旦啓動後當它失效時也會使得run loop退出。雖然定時器能夠循環使得run loop運行相對較長的時間,可是也會致使週期性的喚醒線程。與之相反,輸入源會等待某事件發生,因而線程只有當事件發生後纔會從休眠狀態喚醒。
啓動run loop
run loop只對程序的次線程有意義,而且必須添加了一類源。若是沒有,在啓動後就會退出。有幾種啓動的方法,如:
  • 無條件的
  • 預設的時間
  • 特定的模式
無條件的進入run loop是最簡單的選擇,但也最不提倡。由於這樣會使你的線程處在一個永久的run loop中,這樣的話你對run loop自己的控制就會很小。你能夠添加或移除源,定時器,可是隻能經過殺死進程的辦法來退出run loop。而且這樣的run loop也沒有辦法運行在自定義模式下。
用預設時間來運行run loop是一個比較好的選擇,這樣run loop在某一事件發生或預設的事件過時時啓動。若是是事件發生,消息會被傳遞給相應的處理程序而後run loop退出。你能夠從新啓動run loop以處理下一個事件。若是是時間過時,你只需重啓run loop或使用定時器作任何的其餘工做。**
此外,使run loop運行在特定模式也是一個比較好的選擇。模式和預設時間不是互斥的,他們能夠同時存在。模式對源的限制在run loop模式部分有詳細說明。
Listing3-2代碼描述了線程的整個結構。代碼的關鍵是說明了run loop的基本結構。必要時,你能夠添加本身的輸入源或定時器,而後重複的啓動run loop。每次run loop返回,你要檢查是否有使線程退出的條件發生。代碼中使用了Core Foundation的run loop程序,這樣就能檢查返回結果從而判斷是否要退出。如果cocoa程序,也不須要關心返回值,你也可使用NSRunLoop的方法運行run loop(代碼見listing3-14)
Listing 3-2 Running a run loop
 
- (void)skeletonThreadMain
 
{
 
// Set up an autorelease pool here if not using garbage collection.
 
BOOL done = NO;
 
 
 
// Add your sources or timers to the run loop and do any other setup.
 
 
 
do
 
{
 
// Start the run loop but return after each source is handled.
 
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
 
 
 
// If a source explicitly stopped the run loop, or if there are no
 
// sources or timers, go ahead and exit.
 
if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
 
done = YES;
 
 
 
// Check for any other exit conditions here and set the
 
// done variable as needed.
 
}
 
while (!done);
 
 
 
// Clean up code here. Be sure to release any allocated autorelease pools.
 
}
 
 
由於run loop有可能迭代啓動,也就是說你可使用CFRunLoopRun,CFRunLoopRunInMode或者任一NSRunLoop的方法來啓動run loop。這樣作的時候,你可使用任何模式啓動迭代的run loop,包括被外層run loop使用的模式。
退出run loop
在run loop處理事件前,有兩種方法使其退出:
  • 設置超時限定
  • 通知run loop中止
若是能夠配置的話,使用第一種方法是較好的選擇。這樣,可使run loop完成全部正常操做,包括髮送消息給run loop觀察者,最後再退出。
使用CFRunLoopStop來中止run loop也有相似的效果。Run loop也會把全部未發送的消息發送完後再退出。與設置時間的區別在於你能夠在任何狀況下中止run loop。
儘管移除run loop的輸入源和定時器也可使run loop退出,但這並非可靠的退出run loop的辦法。一些系統程序會添加輸入源來處理必須的事件。而你的代碼未必會考慮到這些,這樣就沒有辦法從系統程序中移除,從而就沒法退出run loop。
線程安全和run loop對象
線程是否安全取決於你使用哪一種API操縱run loop。Core Foundation中的函數一般是線程安全的能夠被任意線程調用。可是,若是你改變了run loop的配置而後須要進行某些操做,你最好仍是在run loop所在線程去處理。若是可能的話,這樣是個好習慣。
至於Cocoa的NSRunLoop則不像Core Foundation具備與生俱來的線程安全性。你應該只在run loop所在線程改變run loop。若是添加yuan或定時器到屬於另外一個線程的run loop,程序會崩潰或發生意想不到的錯誤。
Run loop 源的配置
下面的例子說明了若是使用cocoa和core foundation來創建不一樣類型的輸入源。
定義自定義輸入源
遵循下列步驟來建立自定義的輸入源:
  • 輸入源要處理的信息
  • 使感興趣的客戶知道如何和輸入源交互的調度程序
  • 處理客戶發送請求的程序
  • 使輸入源失效的取消程序
因爲你本身建立源來處理消息,實際配置設計得足夠靈活。調度,處理和取消程序是你建立你得自定義輸入源時總會須要用到得關鍵程序。可是,輸入源其餘的大部分行爲都是由其餘程序來處理。例如,由你決定數據傳輸到輸入源的機制,還有輸入源和其餘線程的通訊機制。
圖3-2列舉了自定義輸入源的配置。在這個例子中,程序的主線程保持了輸入源,輸入源所需的命令緩衝區和輸入源所在的run loop的引用。當主線程有任務,須要分發給目標線程,主線程會給命令緩衝區發送命令和必須的信息,這樣活動線程就能夠開始執行任務。(由於主線程和輸入源所在線程都須訪問命令緩衝區,因此他們的操做要注意同步。)一旦命令傳送了,主線程會通知輸入源而且喚醒活動線程的run loop。而一收到喚醒命令,run loop會調用輸入源的處理部分,由它來執行命令緩衝區中相應的命令。
相關文章
相關標籤/搜索