前言:先自己嘗試去回答,回答不了再看參考答案,你才能學的更多! 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,他們都有非常流暢的體驗,對于複雜的清單來說都是神器。用好其中一種都可以解決大部分問題了,那麼還有一部分問題來自于哪裡呢?繼續向下看。 • SDWebImage 應該是使用最為廣泛的圖檔庫了吧。上面也提到了,我們正常的使用是沒有大問題的,但是如果對性能要求比較高,或者要處理gif圖,SD 難免會拖慢速度。特别是 gif 的記憶體暴增問題,SD 一直沒有一個較好的解決方案。 • YYWebImage 這個就是在清單優化時我推薦使用的網絡圖檔庫,在對性能要求比較高的情況下,YY 可以直接以 layer 作為圖檔的載體這樣減少了相當的一部分資源消耗。具體在什麼情況下使用layer 什麼情況下使用 imageView 戳這裡 。 對于gif圖,YY 還提供了 分享 gif 的方案。 小結 •如果項目比較緊,我更推薦 IB + 基礎的優化準則,既可以保證整體效率也不至于卡的太嚴重。 •如果項目已經到後期,并且有時間進行大量的優化。我傾向于使用 純代碼 + 異步繪制,這一部分在蘋果現在的多種螢幕尺寸上适配工作量并不小。但是效果卻也是很明顯的。 •如果想獲得流暢的體驗,但是有沒有太多的時間去做優化,那就可以使用一些封裝好的第三方庫,比如 YYText, AsyncDisplayKit 。
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
- -(id)copyWithZone:(NSZone *)zone{
- Person *person = [[[self Class]allocWithZone:zone]init];
- p.name = _name;
- p.age = _age;
- return person;
- }
深拷貝的實作 [cpp] view plain copy
- -(void)copyWithZone:(NSZone *)zone{
- Person *person = [[[self Class]allocWithZone:zone]init];
- person.name = [_name copy];
- person.age = [_age copy];
- return person;
- }
深淺拷貝和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
- 思考得怎麼樣了?相信在你心中已經有答案了。那麼讓我們一起來看看 console 輸出:
__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_); } - // 場景 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 中看到類似的輸出,說明我們已經成功地設定了一個觀察點:
-
阿裡P6一面Objective-C Autorelease Pool 的實作原理 - 設定好觀察點後,點選 Continue program execution 按鈕,繼續運作程式,我們将看到如下圖所示的界面:
-
阿裡P6一面Objective-C Autorelease Pool 的實作原理 我們先看 console 中的輸出,注意到 string_weak_ 變量的值由 0x00007f9b886567d0 變成了 0x0000000000000000 ,也就是 nil 。說明此時它所指向的對象被釋放了。另外,我們也可以注意到一個細節,那就是 console 中列印了兩次對象的值,說明此時 viewWillAppear 也已經被調用了,而 viewDidAppear 還沒有被調用。
接着,我們來看看左側的線程堆棧。我們看到了一個非常敏感的方法調用 -[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 。
阿裡P6一面Objective-C Autorelease Pool 的實作原理 -
- magic 用來校驗 AutoreleasePoolPage 的結構是否完整;
- next 指向最新添加的 autoreleased 對象的下一個位置,初始化時指向 begin() ;
- thread 指向目前線程;
- parent 指向父結點,第一個結點的 parent 值為 nil ;
- child 指向子結點,最後一個結點的 child 值為 nil ;
- depth 代表深度,從 0 開始,往後遞增 1;
- hiwat 代表 high water mark 。
Autorelease Pool Blocks
我們使用 clang -rewrite-objc 指令将下面的 Objective-C 代碼重寫成 C++ 代碼: -
1 2 3 @autoreleasepool { }
将會得到以下輸出結果(隻保留了相關代碼):
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; }; { __AtAutoreleasePool __autoreleasepool; } |
不得不說,蘋果對 @autoreleasepool {} 的實作真的是非常巧妙,真正可以稱得上是代碼的藝術。蘋果通過聲明一個 __AtAutoreleasePool 類型的局部變量 __autoreleasepool 來實作 @autoreleasepool {} 。當聲明 __autoreleasepool 變量時,構造函數 __AtAutoreleasePool()被調用,即執行 atautoreleasepoolobj = objc_autoreleasePoolPush(); ;當出了目前作用域時,析構函數 ~__AtAutoreleasePool() 被調用,即執行 objc_autoreleasePoolPop(atautoreleasepoolobj); 。也就是說 @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 函數在執行一個具體的插入操作時,分别對三種情況進行了不同的處理:
- 目前 page 存在且沒有滿時,直接将對象添加到目前 page 中,即 next 指向的位置;
- 目前 page 存在且已滿時,建立一個新的 page ,并将對象添加到新建立的 page 中;
- 目前 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 的:
- 如果你編寫的程式不是基于 UI 架構的,比如說指令行工具;
- 如果你編寫的循環中建立了大量的臨時對象;
- 如果你建立了一個輔助線程。
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運作在其關聯的模式下才會被傳遞。 下面讨論這幾種輸入源。 http://www.cnblogs.com/scorpiozj/ 基于端口的源: 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會自動處理之前未處理的消息,并通知觀察者。具體的順序,如下:
- 通知觀察者,run loop啟動
- 通知觀察者任何即将要開始的定時器
- 通知觀察者任何非基于端口的源即将啟動
- 啟動任何準備好的非基于端口的源
- 如果基于端口的源準備好并處于等待狀态,立即啟動;并進入步驟9。
- 通知觀察者線程進入休眠
- 将線程之于休眠直到任一下面的事件發生
- 某一事件到達基于端口的源
- 定時器啟動
- 設定了run loop的終止時間
- run loop喚醒
- 通知觀察者線程将被喚醒。
- 處理未處理的事件
- 如果使用者定義的定時器啟動,處理定時事件并重新開機run loop。進入步驟2
- 如果輸入源啟動,傳遞相應的消息
- run loop喚醒但未終止,重新開機。進入步驟2
- 通知觀察者run loop結束。
(标号應該連續,不知道怎麼改) 因為觀察者的消息傳遞是在相應的事件發生之前,是以兩者之間可能存在誤差。如果需要精确時間控制,你可以使用休眠和喚醒通知以此來校對實際發生的事件。 因為定時器和其它周期性事件那是在run loop運作後才啟動,撤銷run loop也會終止消息傳遞。典型的例子就是滑鼠路徑追蹤。因為你的代碼直接擷取到消息而不是經由程式傳遞,進而不會在實際的時間開始而須使得滑鼠追蹤結束并将控制權交給程式後才行。 使用run loop對象可以喚醒Run loop。其它消息也可以喚醒run loop。例如,添加新的非基于端口的源到run loop進而可以立即執行輸入源而不是等待其他事件發生後再執行。 何時使用Run Loop http://www.cnblogs.com/scorpiozj/archive/2011/05/26/2058167.html 隻有在為你的程式建立次線程的時候,才需要運作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會調用輸入源的處理部分,由它來執行指令緩沖區中相應的指令。
轉載于:https://www.cnblogs.com/noun/p/8451260.html