在當前iOS開發中,主流的開發語言依然是Objective-C(後面簡稱OC)語言,當然現在的Swift語言喧囂塵上,但在大部分較大型的iOS軟件系統構建中,OC依然占據著不可替代的地位。學習並深刻的認識OC這種語言依然是當前iOS開發中不可或缺的一項任務,一種編程語言的誕生不是偶然,那麼,OC是如何誕生的呢?我們首先對它做個總覽。
OC的起源
1980年代初布萊德·考克斯(Brad Cox)在其公司Stepstone發明了Objective-C語言,並於1986年將這種語言構建成書。Brad Cox一直專注於軟件工程,軟件重用性和組建化是OC裡面的核心思想。Brad當時想打造一門流行的、可移植的C語言與優雅的Smalltalk的結合體,Smalltalk是歷史上第二個面向對象的程序設計語言和第一個真正的集成開發環境 (IDE),由於Smalltalk編程語言對近代面向對象編程語言的影響,因此被稱為“面向對象編程之母”。Cox在1983年修改了C編譯器用於面向對象編程,因此編譯面向對象的C也被稱為OOP C。Cox將Smalltalk的object和message passing分層構造在C語言之上,這點讓程序設計師可以持續使用熟悉的C語言開發,又可以使用面向對象特性,OC語言就在這個基礎上誕生了。
此時,在1976年創建Apple的史提夫·喬布斯因為內部斗爭被趕出了蘋果公司,這一年是1985年。喬布斯離開後創立了NeXT電腦公司,致力於開發強大且經濟的工作站。NeXT獲得了Stepstone公司的Objective-C語言授權並且可以發布自己的 Objective-C Compiler和libraries。NeXT使用Objective-C開發了一套NeXTSTEP操作系統,並創建了NeXTSTEP Toolkit軟件包,這個工具包用於開發用戶界面,功能十分強大。NeXTSTEP系統是以Mach為kernel,加上BSD所打造出來的類unix的操作系統,以Objective-C為本地語言與運行環境,包含有很多面向對象的軟件開發套件和各種開發工具(Project Builder, Interface Builder),並且具有很先進的GUI接口。NeXT擁有當時最先進的技術,但是卻不能成為最流行的電腦,NeXT Workstations僅僅銷售了5000套。
1993年,NeXT終止了硬件業務,轉為專注於NeXTSTEP(或稱為OpenStep)的軟件市場,並推出了一套網絡程序架構WebObjects用於進行動態頁面的生成。OpenStep實際上是NeXT和SUN公司合作開發的一套系統,可以運行在Soloris和Windows NT上。1994年NeXT與Sun共同制定了OpenStep API標准,其中兩個重要的部分是Foundation跟Application kit,此時開始使用命名前綴NS,之後在Objective-C語言的程序中便會看到NX與NS字樣,因為Mac OS X、iPhone SDK、Xcode都可追溯到NeXT、NeXTStep系統。
歷史的轉機來到了1996年,這一年Apple買下了NeXT,喬布斯也於1997年重回Apple。這次並購的主要用意就是要以NeXTStep系統取代老舊的Mac OS,NeXTSTEP被重命名為Cocoa,WebObjects則集成到Mac OS Server和Xcode中。Objective-C自然而然成為Mac平台的首選開發語言,並受到Macintosh編程人員的廣泛認可。Cocoa成為蘋果免費提供的開發工具,提供Mac平台應用開發的環境。
自此,我們迎來了一個時代,一個喬布斯帶給我們的時代。
OC的特性
Objective-C是面向對象的語言,遵從ANSI C標准C語法,是在C語言的基礎上,增加了一層最小的面向對象語言。它是一種靜態輸入語言,在構建中必須先聲明數據中每個變量(或者容器)的數據類型。因為使用了Smalltalk的方法在運行時可以靈活處理,因而也是一個動態語言,代碼中的某一部分可以在app運行的時候被擴展和修改。
運行時非常靈活,包含有Dynamic Binding(動態綁定)、Dynamic Typing(動態檢查)和Dynamic Linking(動態鏈接)。Dynamic Language幾乎所有的工作都可以在運行時處理,使用運行時特性可以最大靈活性的減少RAM和CPU使用。OC完全兼容C語言,在代碼中可以混用c,甚至是c++代碼。Objective-C可以在任何gcc支持的平台上進行編譯,因為gcc原生支持Objective-C。
Objective-C是非常“實際”的語言,它使用一個用C寫成的很小的運行庫,只會令應用程序的大小增加很小,和大部分OO(面向對象)系統使用極大的VM(如JVM等)執行取代整個系統的運作相反,OC寫成的程序通常不會比其原始代碼大很多。 Objective-C最初版本並不支持垃圾回收,即是鑒於當時的面向對象語言回收時有漫長的“死亡時間”,會使整個系統失去功用,Objective-C為避免此問題才不擁有這個功能。與之相對,Objective-C的內存管理采用引用計數的方式,後期引入ARC(自動引用計數)。
OC不包括命名空間機制(namespace mechanism),取而代之的是必須在其類別名稱前加上前綴,因而會有引致沖突的可能。所有Mac OS X的類別和函式均有“NS”作為前綴,使用“NS”是由於這些類別的名稱在NeXTSTEP開發時定下的。雖然C是Objective-C的母集,但它並不視C的基本型別為第一級的對象。和C++不同,Objective-C不支持運算子多載(不支持ad-hoc多型),Objective-C只容許對象繼承一個類別(不許多重繼承),不過可以使用Categories和protocols實現多重繼承。
由於OC使用動態運行時類型,而且所有的方法都是函數調用(有時甚至連系統調用syscalls也如此),很多常見的編譯時性能優化方法都不能應用於OC(例如:內聯函數、常數傳播、交互式優化、純量取代與聚集等)。這使得OC性能劣於類似的對象抽象語言(如C++等)。OC運行時消耗較大,致使OC不適於當前使用C++的常見的底層抽象,這也是靜態處理和動態處理的重要區別。但是,OC本來就不是設計來做這些的,強求於此也大可未必。
對於OC有了一個初步的認識之後,我們將從OC的起源,面向對象原則談起。
面向對象三原則(封裝,繼承,多態)
C語言是面向過程的語言(關注的是函數),OC、C++、JAVA、C#、PHP、Swift是面向對象的,面向過程關注的是解決問題涉及的步驟,而面向對象關注的是設計能夠實現解決問題所需功能的類,抽象是面向對象的思想基礎。想要深刻的了解OC這種語言,面向對象三原則是一個無法繞過的坎。
面向對象具有四個基本特征:抽象,封裝,繼承和多態。
抽象包括兩個方面,一是過程抽象,二是數據抽象。過程抽象是指任何一個明確定義功能的操作都可被使用者看作單個的實體看待,盡管這個操作實際上可能由一系列更低級的操作來完成。數據抽象定義了數據類型和施加於該類型對象上的操作,並限定了對象的值只能通過使用這些操作修改和觀察。抽象是一種思想,封裝繼承和多態是這種思想的實現。
封裝
封裝是把過程和數據包圍起來(即函數和數據結構,函數是行為,數據結構是描述),有限制的對數據進行訪問。面向對象是基於這個基本概念開始的(因為面向對象更注重的是類),即現實世界可以被描繪成一系列完全自治、封裝的對象,這些對象通過一個受保護的接口訪問其他對象。一旦定義了一個對象的特性,則有必要決定這些特性的可見性,封裝保證了模塊具有較好的獨立性,使得程序維護修改較為容易。對應用程序的修改僅限於類的內部,因而可以將應用程序修改帶來的影響減少到最低限度。但是封裝會導致並行效率問題,因為執行部分和數據部分被綁定在一起,制約了並行程度。面向對象思想將函數和數據綁在一起,擴大了代碼重用時的粒度,而且封裝下的拆箱裝箱過程中也會導致內存的浪費。
繼承
繼承是一種層次模型,允許和鼓勵類的重用,並提供了一種明確表述共性的方法。新類繼承了原始類的特性,新類稱為原始類的派生類(子類和父類)。派生類可以從它的基類那裡繼承方法和實例變量,並且類可以修改或增加新的方法使之更適合特殊的需要。繼承性很好的解決了軟件的可重用性問題,但是,不恰當地使用繼承導致的最大的一個缺陷特征就是高耦合(是設計類時層次沒分清導致的)。解決方案是用組合替代繼承,將模塊拆開,然後通過定義好的接口進行交互,一般來說可以選擇代理模式。
使用繼承其實是如何給一類對象劃分層次的問題,在正確的繼承方式中,父類應當扮演的是底層的角色,子類是上層的業務。父類只是給子類提供服務,並不涉及子類的業務邏輯,層級關系明顯,功能劃分清晰,父類的所有變化,都需要在子類中體現,此時耦合已經成為需求。
多態
多態性是指允許不同類的對象對同一消息作出響應。多態性包括參數化多態性和包含多態性,很好的解決了應用程序函數同名問題。多態一般都要跟繼承結合起來說,其本質是子類通過覆蓋或重載父類的方法,來使得對同一類對象同一方法的調用產生不同的結果。
覆蓋是對接口方法的實現,繼承中也可能會在子類覆蓋父類中的方法。重載,是指我們可以定義一些名稱相同的方法,通過定義不同的輸入參數來區分這些方法,然後在調用時,VM就會根據不同的參數樣式,來選擇合適的方法執行。在使用重載時只能通過不同的參數樣式,例如,不同的參數類型,不同的參數個數,不同的參數順序(當然,同一方法內的幾個參數類型必須不一樣)。於此相對,繼承會在多態使用混亂的境況產生耦合。更好的方法是使用接口,通過IOP將子類與可能被子類引入的不相關邏輯剝離開來,即提高了子類的可重用性,又降低了遷移時可能的耦合。
接口規范了子類哪些必須實現,哪些可選實現。那些不在接口定義的方法列表裡的父類方法,事實上就是不建議覆重的方法。如果引入多態之後導致對象角色不夠單純,那就不應當引入多態,如果引入多態之後依舊是單純角色,那就可以引入多態;如果要覆重的方法是角色業務的其中一個組成部分,那麼就最好不要用多態的方案,轉而使用用IOP實現,因為在外界調用的時候其實並不需要通過多態來滿足定制化的需求。
OC的動態性
Objective-C是面相運行時的語言,它會盡可能的把編譯和鏈接時要執行的邏輯延遲到運行時。使用Runtime可以按需要把消息重定向給合適的對象,交換方法的實現等等。
Runtime簡稱運行時,其中最主要的是消息機制,它是一個主要使用C和匯編寫成的庫。OC的函數調用稱為消息發送,屬於動態調用過程,在編譯的時候並不能決定真正調用哪個函數(在編譯階段,OC可以調用任何函數,即使這個函數並未實現,只要聲明過就不會報錯,而C語言在編譯階段就會報錯)。只有在真正運行的時候才會根據函數的名稱找 到對應的函數來調用。
例如:
[obj makeTest];
將轉化為
objc_msgSend(obj,@selector(makeTest));
objc_msgSend方法包含兩個必要參數:receiver、方法名(即:selector),如:[receiver message];將被轉換為objc_msgSend(receiver, selector);此外objc_msgSend方法也能使用message的參數,如:
objc_msgSend(receiver, selector, arg1, arg2, …);
回到上面的示例,objc_msgSend方法按照一定的順序進行操作,以完成動態綁定。objc_msgSend函數會依據接收者與selector的類型來調用適當的方法。編譯器執行上述轉換時,在objc_msgSend函數中首先通過obj的isa指針找到obj對應的class。每個對象內部都默認有一個isa指針指向這個對象所使用的類,isa是對象中的隱藏指針,指向創建這個對象的類,具體的流向可以參照下圖。
在Class中先去cache中通過SEL查找對應函數(cache中method列表是以SEL為key通過hash表來存儲的,這樣能提高函數查找速度),若cache中未找到,再去methodList中查找,若methodlist中未找到,則去superClass中查找,若能找到,則將method加入到cache中,以方便下次查找,並通過method中的函數指針跳轉到對應的函數中去執行。如果最終還是找不到相符的方法,那就執行“消息轉發”(message forwarding)操作。
調用實現時,將一系列參數傳遞過去,最後將該實現的返回值作為自己的返回值返回。objc_msgSend等函數一旦找到應該調用的方法實現之後,就會跳轉過去。之所以能這樣做,是因為Objective-C對象的每個方法都可以視為簡單的C函數。由上面的分析可知,每個類裡都有一張表格,其中的指針都會指向這種函數,而selector的名稱則是查表時所用的“鍵”,objc_msgSend等函數正是通過這張表格來尋找應該執行的方法並跳至其實現的,並且利用了“尾調用優化”(tail-call optimization)技術。
“尾調用優化”技術用在某函數的最後一項操作是調用另外一個函數的情況,編譯器會生成調轉至另一函數所需的指令碼,並且不會向調用堆棧中推入新的“棧幀”(frame stack),只有當某函數的最後一個操作僅僅是調用其他函數而不會將其返回值另作他用時,才能執行“尾調用優化”。這項優化對objc_msgSend非常關鍵,如果不這麼做的話,那麼每次調用Objective-C方法之前,都需要為調用objc_msgSend函數准備“棧幀”,並且還會有過早地發生“棧溢出”(stack overflow)的現象。
因此,消息傳遞的關鍵是編譯器構建每個類和對象時所采用的數據結構。每個類都包含以下兩個必要元素:一個指向父類的指針;一個調度表(dispatch table),該調度表將類的selector與方法的實際內存地址關聯起來。調用一個方法需要很多步驟,但objc_msgSend會將匹配結果緩存在“快速映射表”(fast map)裡面,每個類都有這樣一塊緩存,若是稍後還向該類發送與selector相同的消息,那麼執行起來就很快了。當然啦,這種“快速執行路徑”(fast path)還是不如“靜態綁定的函數調用操作”(statically bound function call)那樣迅速,不過只要把selector緩存起來也就不會太慢了。
動態類型、動態綁定和動態加載
OC的動態特性表現為了三個方面:動態類型、動態綁定、動態加載。之所以叫做動態,是因為必須到運行時(runtime)才會做一些事情。
動態類型,就是id類型。動態類型是跟靜態類型相對的,內置的基本類型都屬於靜態類型(int、NSString等)。靜態類型在編譯的時候就能被識別出來(即前面說的靜態輸入),因此,若程序發生了類型不對應,編譯器就會發出警告,但動態類型在編譯器編譯的時候是不能被識別的,要等到運行時(runtime),即程序運行的時候才會根據語境來識別。所以這裡面就有兩個概念要分清:編譯時跟運行時。
我們可以把任何想要的消息發送給代碼中類型為“id”的變量,然後Objective-C的動態消息處理就會在運行時讓這一調用正確地工作。但在實際情況中,即使方法的查找是發生在運行時期,這也只夠確保正確的方法被調用,但卻不足以確保參數是有效的。編譯器肯定是需要推斷出一些與所涉及的方法簽名有關的信息的,即使編譯器不需要知道id的類型,但它確實需要知道所有參數的字節長度,以及任何返回值的確切類型。這是因為參數的列集(壓入棧以及從棧中彈出它們)是在編譯時配置的。通常情況下,我們不需要采取任何步驟來使之發生,參數的信息是通過查看你試圖調用的方法的名稱來獲取的,搜索整個被包含進來的頭文件查找與被調用方法的名稱吻合的方法,然後從其找到的第一個匹配方法中獲取參數的長度。即使你真正指向的確切方法不能被明確分辨出來,匹配方法之間的參數也有可能會是相同的,因為Objective-C中的方法名稱通常就暗示了數據的類型。
假設有一個類MyClass,該類有一個名為currentPoint的實例方法,該方法返回一個int類型的值。如何希望調用保存在數組中的對象的currentPoint,就會使用下面的代碼:
int result = [[someArray objectAtIndex:0] currentPoint];
運行時調用的方法是正確的,問題是編譯器為這一調用列集的參數是不正確的,這導致了數據的返回類型被破壞。[someArray objectAtIndex:0]不能明確的指出得到的一定是類MyClass,解決辦法就是強制轉換。
int result = [(MyClass *)[someArray objectAtIndex:0] currentPoint];
在消息發送之前,編譯器需要正確地把參數壓入到棧中,然後執行消息發送,使用正確的objc_msgSend來取回返回值,而這就是出現故障的地方。
編譯器使用方法簽名(通過查看接收者的類型和該接收者的所有有效方法名稱來獲取的)來准備參數,並試圖找出你可能想要調用的方法是哪一個。由於接收者的類型(比如說objectAtIndex:的調用結果)僅為id,這樣的話我們就沒有顯式的類型信息,因此編譯器就會查看所有已知方法的一個列表。如果編譯器找到的不是我們的MyClass的方法,而決定匹配NSBezierPath的名為currentPoint的方法,並准備了與該方法的簽名相匹配的參數。NSBezierPath的方法返回一個struct類型的NSPoint,而這就導致我們的返回類型被破壞了。
對於實例方法來說,我們通過強制轉換成所需的特定對象類型來修正問題,但在兩個對象都是類方法的情況下,就不可能強制轉換所涉及的特定Class了,在Objective-C中,你不能強制轉換類方法。如果你不能夠改變方法的名稱的話,那麼唯一的權變之法看起來就像是這個樣子:
int result = objc_msgSend([someArray objectAtIndex:0], @selector(currentPoint));
是的,需要使用objc_msgSend方法,這是一個極端的案例。
動態語言和靜態語言的另一個區別是靜態語言提前編譯好文件,即所有的邏輯已在編譯時確定,運行時直接加載編譯後的文件;而動態語言是在運行時才確定實現。典型的靜態語言是C++,動態語言包括OC,JAVA,C#等;因為靜態語言提前編譯好了執行文件,也就是通常所說的靜態語言效率較高的原因。
下面我們來看看動態綁定,動態綁定(dynamic binding)需要用到@selector/SEL。關於“函數”,對於其他一些靜態語言,比如c++,一般在編譯的時候就已經將要調用的函數的函數簽名都告訴編譯器了,是靜態的,不能改變。而在OC中,其實是沒有函數的概念的,我們叫“消息機制”,所謂的函數調用就是給對象發送一條消息。這時,動態綁定的特性就來了。OC可以先跳過編譯,到運行的時候才動態地添加函數調用,在運行時才決定要調用什麼方法,需要傳什麼參數進去,這就是動態綁定。要實現它就必須用SEL變量綁定一個方法,最終形成的這個SEL變量就代表一個方法的引用。這裡要注意一點,SEL並不是C裡面的函數指針,雖然很像。SEL變量只是一個整數,它是使用方法的ID,以前的函數調用,是根據函數名,也就是字符串去查找函數體。但現在,我們是根據一個ID整數來查找方法,整數的查找自然要比字符串的查找快得多!所以,動態綁定的特性不僅方便,而且效率更高。
動態加載就是根據需求動態地加載資源,在運行時加載新類。在運行時創建一個新類,只需要3步:
1、為 class pair分配存儲空間 ,使用 objc_allocateClassPair 函數
2、增加需要的方法使用 class_addMethod 函數,增加實例變量用class_addIvar
3、用objc_registerClassPair函數注冊這個類,以便它能被別人使用。
其實就是這麼簡單。
結合上面的理解,我們詳細的看一下示例。
- (void)ex_registerClassPair { Class TestClass= objc_allocateClassPair([NSObject class], "TestClass", 0); //為類添加變量 class_addIvar(TestClass, "_name", sizeof(NSString*), log2(sizeof(NSString*)), @encode(NSString*)); //為類添加方法 IMP 是函數指針 typedef id (*IMP)(id, SEL, ...); IMP i = imp_implementationWithBlock(^(id this,id other){ NSLog(@"%@",other); return @123; }); //注冊方法名為 testMethod: 的方法 SEL s = sel_registerName("testMethod:"); class_addMethod(TestClass, s, i, "i@:"); //結束類的定義 objc_registerClassPair(TestClass); 1 //創建對象 id t = [[TestClass alloc] init]; //KVC 動態改變 對象t 中的實例變量 [t setValue:@"測試" forKey:@"name"]; NSLog(@"%@",[t valueForKey:@"name"]); //調用 t 對象中的 s 方法選擇器對於的方法 id result = [t performSelector:s withObject:@"測試內容"]; NSLog(@"%@",result); }
打印結果:
測試 測試內容 123
完全正確。
基於以上的認識,我們來看一道網上的面試題吧(注:此處采用自網絡):
@implementation Son : Father - (id)init { self = [super init]; if (self) { NSLog(@"%@", NSStringFromClass([self class])); NSLog(@"%@", NSStringFromClass([super class])); } return self; } @end
答案:都輸出”Son”
因為oc的方法尋找是在編譯期執行的,self表示本類Son,super表示父類Father,NSObject是Son和Father的父類。class方法在NSObject中定義,並且沒有被Father和Son覆蓋。[self class]的方法尋找方式是:Son,Father,NSObject,[super class]的方法尋找方式是:Father,NSObject,最終執行的都是NSObject裡面定義的class方法。要執行的方法找完之後,程序進入運行期,即執行剛才找到的方法。而方法的實際調用者是對象。[super class]和[self class]的實際調用者都是Son對象。因此最後的打印結果應該都是Son。
Method Swizzling
在Objective-C中調用一個方法,其實是向一個對象發送消息,查找消息的唯一依據是selector的名字。利用Objective-C的動態特性,可以實現在運行時偷換selector對應的方法實現,達到給方法掛鉤的目的。每個類都有一個方法列表,存放著selector的名字和方法實現的映射關系。IMP類似函數指針,指向具體的Method實現。
用 method_exchangeImplementations 來交換2個方法中的IMP,
用 class_replaceMethod 來修改類,
用 method_setImplementation 來直接設置某個方法的IMP,歸根結底,都是偷換了selector的IMP。
示例如下:
- (void)viewDidLoad { [super viewDidLoad]; Method ori_Method = class_getInstanceMethod([self class], @selector(testOne)); Method my_Method = class_getInstanceMethod([self class], @selector(testTwo)); method_exchangeImplementations(ori_Method, my_Method); [self testOne]; } - (void)testOne { NSLog(@"原來的"); } - (void)testTwo { NSLog(@"改變了"); }
結果:
改變了
其實,就是這麼簡單。
RunLoop
RunLoop是一個讓線程能隨時處理事件但不退出的機制。RunLoop實際上是一個對象,這個對象管理了其需要處理的事件和消息,並提供了一個入口函數來執行Event Loop的邏輯。線程執行了這個函數後,就會一直處於這個函數內部的"接受消息->等待->處理"的循環中,直到這個循環結束(比如傳入 quit 的消息),函數返回,讓線程在沒有處理消息時休眠以避免資源占用,在有消息到來時立刻被喚醒。總結來說,一個runloop就是一個事件處理循環,用來不停的監聽和處理輸入事件並將其分配到對應的目標上進行處理。
RunLoop有四個作用:使程序一直運行接受用戶輸入;決定程序在何時應該處理哪些Event;調用解耦;節省CPU時間。線程和 RunLoop 之間是一一對應的,其關系是保存在一個全局的 Dictionary 裡。線程剛創建時並沒有 RunLoop,如果你不主動獲取,那它一直都不會有。RunLoop 的創建是發生在第一次獲取時,RunLoop 的銷毀是發生在線程結束時,並且只能在一個線程的內部獲取其RunLoop(主線程除外)。主線程的runloop默認是啟動的。
OS X/iOS 系統中,提供了兩個這樣的對象:NSRunLoop和CFRunLoopRef。CFRunLoopRef 是在 CoreFoundation 框架內的,它提供了純 C 函數的 API,所有這些 API 都是線程安全的。NSRunLoop 是基於 CFRunLoopRef 的封裝,提供了面向對象的 API,但是這些 API 不是線程安全的。
NSRunLoop是一種更加高明的消息處理模式,在對消息處理過程進行了更好的抽象和封裝,不用處理一些很瑣碎很低層次的具體消息,在NSRunLoop中每一個消息打包在input source或者是timer source中,使用run loop可以使你的線程在有工作的時候工作,沒有工作的時候休眠,可以大大節省系統資源。
對其它線程來說,runloop默認是沒有啟動的,如果你需要更多的線程交互則可以手動配置和啟動,如果線程只是去執行一個長時間的已確定的任務則不需要。在任何一個Cocoa程序的線程中,都可以通過:
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
獲取到當前線程的runloop。
Cocoa中的NSRunLoop類並不是線程安全的,我們不能在一個線程中去操作另外一個線程的runloop對象,那很可能會造成意想不到的後果。但是CoreFundation中的不透明類CFRunLoopRef是線程安全的,而且兩種類型的runloop完全可以混合使用。Cocoa中的NSRunLoop類可以通過實例方法:
- (CFRunLoopRef)getCFRunLoop;
獲取對應的CFRunLoopRef類,來達到線程安全的目的。
Runloop的管理並不完全是自動的。我們仍必須設計線程代碼以在適當的時候啟動runloop並正確響應輸入事件,當然前提是線程中需要用到runloop。而且,我們還需要使用while/for語句來驅動runloop能夠循環運行,下面的代碼就成功驅動了一個run loop:
BOOL isRunning = NO; do { isRunning = [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDatedistantFuture]]; } while (isRunning);
Runloop同時也負責autorelease pool的創建和釋放,在使用手動的內存管理方式的項目中,會經常用到很多自動釋放的對象,如果這些對象不能夠被即時釋放掉,會造成內存占用量急劇增大。Runloop就為我們做了這樣的工作,每當一個運行循環結束的時候,它都會釋放一次autorelease pool,同時pool中的所有自動釋放類型變量都會被釋放掉。
系統默認注冊的5個Mode
kCFRunLoopDefaultMode: App的默認 Mode,通常主線程是在這個 Mode 下運行的。
UITrackingRunLoopMode: 界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響。
UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成後就不再使用
GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,通常用不到。
kCFRunLoopCommonModes: 這是一個占位的 Mode,沒有實際作用。
輪播圖中的NSTimer問題
創建定時器的第一種方法:
NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(changeImage) userInfo:nil repeats:YES];
此方法創建的定時器,必須加到NSRunLoop中。
NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addTimer:timer forMode: NSRunLoopCommonModes];
forMode的參數有兩種類型可供選擇:NSDefaultRunLoopMode , NSRunLoopCommonModes,第一個參數為默認參數,當下面有textView,textfield等控件時,拖拽控件,此時輪播器會停止輪播,是因為NSRunLoop的原因,NSRunLoop是一個死循環,實時監測有無事件響應,如果當前線程是主線程,也就是UI線程時,某些UI事件,比如UIScrollView的拖動操作,會將Run Loop切換成NSEventTrackingRunLoopMode模式,在這個過程中,默認的NSDefaultRunLoopMode模式中注冊的事件是不會被執行的。NSRunLoopCommonModes 能夠在多線程中起作用,這個模式等效於NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的結合,這也是將modes換為NSRunLoopCommonModes便可解決的原因。
創建定時器的第二種方法:
self.timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(changeImage) userInfo:nil repeats:YES];
此種創建定時器的方式,默認加到了runloop,使用的是NSDefaultRunLoopMode。
main函數的運行
在main.m中:
int main(int argc, char *argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([appDelegate class])); } }
UIApplicationMain() 函數會為main thread 設置一個NSRunLoop 對象,這就解釋了app應用可以在無人操作的時候休息,需要讓它干活的時候又能立馬響應。僅僅在為你的程序創建輔助線程的時候,你才需要顯式運行一個runloop。Runloop是程序主線程基礎設施的關鍵部分,所以,Cocoa和Carbon程序( Carbon是蘋果電腦操作系統的應用程序編程接口API之一)提供了代碼運行主程序的循環並自動啟動runloop。iOS程序中UIApplication的run方法(或Mac OS X中的NSApplication)作為程序啟動步驟的一部分,它在程序正常啟動的時候就會啟動程序的主循環。如果你使用xcode提供的模板創建你的程序,那你永遠不需要自己去顯式的調用這些例程。
對於輔助線程,你需要判斷一個runloop是否是必須的。如果是必須的,那麼你要自己配置並啟動它。你不需要在任何情況下都去啟動一個線程的runloop,比如,你使用線程來處理一個預先定義的長時間運行的任務時,你應該避免啟動runloop。runloop在你要和線程有更多的交互時才需要,比如以下情況:
1.使用端口或自定義輸入源來和其他線程通信。
2.使用線程的定時器。
3.使線程周期性工作。
其實,在日常開發中,很少會單獨處理這一塊的運行。
事件響應鏈
對於iOS設備用戶來說,操作設備的方式主要有三種:觸摸屏幕、晃動設備、通過遙控設施控制設備。對應的事件類型有以下三種:
1、觸屏事件(Touch Event)
2、運動事件(Motion Event)
3、遠端控制事件(Remote-Control Event)
事件的傳遞和響應分兩個鏈:
傳遞鏈:由系統向離用戶最近的view傳遞。
UIKit –> active app’s event queue –> window –> root view –>……–>lowest view
響應鏈:由離用戶最近的view向系統傳遞。
initial view –> super view –> …..–> view controller –> window –> Application
響應者鏈(Responder Chain)是由多個響應者對象連接起來的鏈條,作用是能很清楚的看見每個響應者之間的聯系,並且可以讓一個事件為多個對象處理。響應者對象(Responder Object)指的是有響應和處理事件能力的對象,響應者鏈就是由一系列的響應者對象構成的一個層次結構。
UIResponder是所有響應對象的基類,在UIResponder類中定義了處理上述各種事件的接口。我們熟悉的UIApplication、 UIViewController、UIWindow和所有繼承自UIView的UIKit類都直接或間接的繼承自UIResponder,所以它們的實例都是可以構成響應者鏈的響應者對象。
響應者鏈有以下特點:
響應者鏈通常是由視圖(UIView)構成的。
一個視圖的下一個響應者是它視圖控制器(UIViewController)(如果有的話)。
視圖控制器的下一個響應者為其管理的視圖的父視圖(如果有的話)。
單例的窗口(如UIWindow)的內容視圖將指向窗口本身作為它的下一個響應者,Cocoa Touch應用不像Cocoa應用,它只有一個UIWindow對象,因此整個響應者鏈要簡單一點。
單例的應用(UIApplication)是一個響應者鏈的終點,它的下一個響應者指向nil,以結束整個循環。
iOS系統檢測到手指觸摸(Touch)操作時會將其打包成一個UIEvent對象,並放入當前活動Application的事件隊列,單例的UIApplication會從事件隊列中取出觸摸事件並傳遞給單例的UIWindow來處理,UIWindow對象首先會使用hitTest:withEvent:方法尋找此次Touch操作初始點所在的視圖(View),即需要將觸摸事件傳遞給其處理的視圖。hitTest:withEvent:方法會在其視圖層級結構中的每個視圖上調用pointInside:withEvent:(該方法用來判斷點擊事件發生的位置是否處於當前視圖范圍內,以確定用戶是不是點擊了當前視圖),如果pointInside:withEvent:返回YES,則向當前視圖的所有子視圖(subviews)發送hitTest:withEvent:消息,所有子視圖的遍歷順序是從最頂層視圖一直到到最底層視圖,即從subviews數組的末尾向前遍歷,直到有子視圖返回非空對象或者全部子視圖遍歷完畢;若第一次有子視圖返回非空對象,則hitTest:withEvent:方法返回此對象,處理結束;如所有子視圖都返回非,則hitTest:withEvent:方法返回自身(self)。逐級調用,直到找到touch操作發生的位置,這個視圖也就是要找的hit-test view。
引用計數器(ARC 和 MRC)
ARC是自動引用計數器(Automatic Reference Counting),MRC是手動引用計算器(現在幾乎不用了,但對於內存管理的理解是有幫助的)。Objective-c中提供了兩種內存管理機制MRC(MannulReference Counting)和ARC(Automatic Reference Counting),分別提供對內存的手動和自動管理來滿足不同的需求,Xcode 4.1及其以前版本沒有ARC。
在MRC的內存管理模式下,與變量的管理相關的方法有:retain,release和autorelease。retain和release方法操作的是引用記數,當引用記數為零時,便自動釋放內存。並且可以用NSAutoreleasePool對象對加入自動釋放池的變量進行管理,當內存緊張時回收內存。
具體分析如下:
retain,該方法的作用是將內存數據的所有權附給另一指針變量,引用數加1,即retainCount+= 1;
release,該方法是釋放指針變量對內存數據的所有權,引用數減1,即retainCount-= 1;
autorelease,該方法是將該對象內存的管理放到autoreleasepool中。
在ARC中與內存管理有關的標識符,可以分為變量標識符和屬性標識符,對於變量默認為__strong,而對於屬性默認為unsafe_unretained,但也存在autoreleasepool。其中assign、retain、copy與MRC下property的標識符意義相同,strong類似與retain,assign類似於unsafe_unretained,strong、weak、unsafe_unretained與ARC下變量標識符意義相同,只是一個用於屬性的標識,一個用於變量的標識(一般帶兩個下劃短線__)。
為了加深理解,我們就來從早期的經驗中來詳細的分析下OC的內存管理吧。OC內存管理四句箴言:自己生成的自己持有;非自己生成的對象自己也可持有;不再需要自己持有時釋放;非自己持有的對象無法釋放。
Objective-C中的內存管理機制跟C語言中指針的內容是同樣重要的,要開發一個程序並不難,但是優秀的程序則更測重於內存管理,它們往往占用內存更少,運行更加流暢。在Xcode4.2及之後的版本中由於引入了ARC(Automatic Reference Counting)機制,程序編譯時Xcode可以自動給你的代碼添加內存釋放代碼。內存管理是開發中不可忽略的一塊,雖然ARC幫我們節省了很多精力,不過作為一名合格的開發人員,最基本的知識還是要理解的。
在開發中,如果使用了alloc、new、copy、mutableCopy或以其開頭的方法名,意味著自己生成的對象只有自己持有,如:allocMyJob,newItName,copyAfter,mutableCopyYourName。但是以allocate,newer,copying,mutableCopyed開頭的是不自己持有的,很明顯它們是假冒的。
以下為例:
id obj = [NSMutableArray array]; // 取得非自己生成並持有的對象
取得的對象存在,但自己並不持有對象。NSMutableArray類對象被賦給變量obj,但變量obj自己並不持有該對象。持有時需要用retain方法,如:
id obj = [NSMutableArray array]; [obj retain];
通過retain,非自己用alloc等生成的對象也可以自己持有了。
不需要時,使用release釋放,對象一經釋放就不可訪問了。用alloc/new/copy/mutableCopy持有和retain持有的對象,不需要時一定要用release釋放。同理使用如allocObject也可以取得自己持有對象。
id obj1 = [obj0 allocObject];
當使用autorelease時,即使取得了對象存在,但是自己不持有對象。如:
- (id)object { id obj = [[NSObject alloc] init]; //自己持有對象 [obj autorelease]; return obj;// 取得的對象存在,但自己不持有 }
[NSMutableArray array]就是因為用了autorelease使得誰都不持有。當然再次使用retain就持有了。程序裡所有autorelease pool都是以棧(stack)的形式組織的。新創建的pool位於棧的最頂端。當發送autorelease消息給一個對象時,這個對象被加到棧頂的那個pool中。發送drain給一個pool時,這個pool裡所有對象都會受到release消息,而且如果這個pool不是位於棧頂,那麼位於這個pool“上端”的所有pool也會受到drain消息。
[pool drain] 和 [pool release] 的區別是:
release,在引用計數環境下,由於NSAutoReleasePool是一個不可以被retain的類型,所以release會直接dealloc pool對象。當pool被dealloc的時候,pool向所有在pool中的對象發出一個release的消息,如果一個對象在這個pool中autorelease了多次,pool對這個對象的每一次autorelease都會release。在GC(garbage-collected environment)環境下release是一個no-op操作(代表沒有操作,是一個占據進行很少的空間但是指出沒有操作的計算機指令)。
drain,在引用計數環境下,它的行為和release是一樣的。在GC的環境下,這個方法調用objc_collect_if_needed 觸發GC。重點是:在GC環境下,release是一個no-op,所以除非你不希望在GC環境下觸發GC,你都應該使用drain而不是使用release來釋放pool。
對於iOS來說drain和release的作用其實是一樣的。
一個對象被加到一個pool很多次,只要多次發送autorelease消息給這個對象就可以。同時,當這個pool被回收時,這個對象也會收到同樣多次release消息。簡單地可以認為接收autorelease消息等同於接收一個retain消息,同時加入到一個pool裡,這個pool用來存放這些暫緩回收的對象,一旦這個pool被回收(drain),那麼pool裡面的對象會收到同樣次數的release消息。UIKit框架已經幫你自動創建一個autorelease pool。大部分時候,你可以直接使用這個pool,不必自己創建;所以你給一個對象發送autorelease消息,那麼這個對象會加到這個UIKit自動創建的pool裡。
某些時候,可能需要創建一個pool:
1.沒有使用UIKit框架或者其它內含autorelease pool的框架,那麼要使用pool,就要自己創建。
2.如果一個循環體要創建大量的臨時變量,那麼創建自己的pool可以減少程序占用的內存峰值。(如果使用UIKit的pool,那麼這些臨時變量可能一直在這個pool裡,只要這個pool受到drain消息;完全不使用autorelease pool應該也是可以的,可能只是要發一些release消息給這些臨時變量,所以使用autorelease pool還是方便一些)
3.創建線程時必須創建這個線程自己的autorelease pool。使用alloc和init消息來創建pool,發送drain消息則表示這個pool不再使用。pool的創建和drain要在同一上下文中,比如循環體內。
ObjC中沒有垃圾回收機制,在ObjC中內存的管理是依賴對象引用計數器來進行的。在ObjC中每個對象內部都有一個與之對應的整數(retainCount),叫“引用計數器”,當一個對象在創建之後它的引用計數器為1,當調用這個對象的alloc、retain、new、copy方法之後引用計數器自動在原來的基礎上加1(OC中調用一個對象的方法就是給這個對象發送一個消息),當調用這個對象的release方法之後它的引用計數器減1,如果一個對象的引用計數器為0,則系統會自動調用這個對象的dealloc方法來銷毀這個對象。手動管理內存有時候並不容易,因為對象的引用有時候是錯綜復雜的,對象之間可能互相交叉引用,此時需要遵循一個法則:誰創建,誰釋放。
深淺復制和屬性為copy,strong時值的變化
淺復制只復制指向對象的指針,而不復制引用對象本身。對於淺復制來說,A和A_copy指向的是同一個內存資源,復制的只不個是一個指針,對象本身資源還是只有一份,那如果我們對A_copy執行了修改操作,那麼發現A引用的對象同樣被修改了。深復制就好理解了,內存中存在了兩份獨立對象本身。
在Objective-C中並不是所有的對象都支持Copy、MutableCopy,遵守NSCopying協議的類才可以發送Copy消息,遵守NSMutableCopying協議的類才可以發送MutableCopy消息。
[immutableObject copy] // 淺拷貝 [immutableObject mutableCopy] //深拷貝 [mutableObject copy] //深拷貝 [mutableObject mutableCopy] //深拷貝
屬性設為copy,指定此屬性的值不可更改,防止可變字符串更改自身的值的時候不會影響到對象屬性(如NSString、NSArray、NSDictionary)的值。strong屬性的指會隨著變化而變化,即copy是內容拷貝,strong是指針拷貝。
生命周期
app應用程序有5種狀態:
Not running未運行:程序沒啟動。
Inactive未激活:程序在前台運行,不過沒有接收到事件。在沒有事件處理情況下程序通常停留在這個狀態。
Active激活:程序在前台運行而且接收到了事件。這也是前台的一個正常的模式。
Backgroud後台:程序在後台而且能執行代碼,大多數程序進入這個狀態後會在這個狀態上停留一會,時間到了之後會進入掛起狀態(Suspended)。有的程序經過特殊的請求後可以長期處於Backgroud狀態。
Suspended掛起:程序在後台不能執行代碼,系統會自動把程序變成這個狀態而且不會發出通知。當掛起時,程序還是停留在內存中的,當系統內存低時,系統就把掛起的程序清除掉,為前台程序提供更多的內存。
iOS的入口在main.m文件的main函數,根據UIApplicationMain函數,程序將進入AppDelegate.m,這個文件是xcode新建工程時自動生成的。AppDelegate.m文件,關乎著應用程序的生命周期。它有如下幾個方法:
application didFinishLaunchingWithOptions:當應用程序啟動時執行,應用程序啟動入口,只在應用程序啟動時執行一次。若用戶直接啟動,lauchOptions內無數據,若通過其他方式啟動應用,lauchOptions中包含對應方式的內容。
applicationWillResignActive:在應用程序將要由活動狀態切換到非活動狀態時候要執行的委托調用,如按下 home 按鈕,返回主屏幕,或全屏之間切換應用程序等。
applicationDidEnterBackground:在應用程序已進入後台程序時,要執行的委托調用。
applicationWillEnterForeground:在應用程序將要進入前台時(被激活)要執行的委托調用,剛好與applicationWillResignActive 方法相對應。
applicationDidBecomeActive:在應用程序已被激活後要執行的委托調用,剛好與applicationDidEnterBackground 方法相對應。
applicationWillTerminate:在應用程序要完全推出的時候要執行的委托調用,這個需要設置UIApplicationExitsOnSuspend的鍵值。
初次啟動時執行一下步驟:
didFinishLaunchingWithOptions
applicationDidBecomeActive
按下home鍵:
applicationWillResignActive
applicationDidEnterBackground
點擊程序圖標進入:
applicationWillEnterForeground
applicationDidBecomeActive
當應用程序進入後台時,應該保存用戶數據或狀態信息,所有沒寫到磁盤的文件或信息,在進入後台時最後都要寫到磁盤去,因為程序可能在後台被殺死。釋放盡可能釋放的內存。
- (void)applicationDidEnterBackground:(UIApplication *)application
該方法有大概5秒的時間讓你完成這些任務,如果超過時間還有未完成的任務,你的程序就會被終止而且從內存中清除。如果還需要長時間的運行任務,可以在以下方法中調用。
[application beginBackgroundTaskWithExpirationHandler:^{ NSLog(@"begin Background Task With Expiration Handler"); }];
程序終止。
程序只要符合以下情況之一,只要進入後台或掛起狀態就會終止:iOS4.0以前的系統;app是基於iOS4.0之前系統開發的;設備不支持多任務;在Info.plist文件中,程序包含了 UIApplicationExitsOnSuspend 鍵。
系統常常是為其他app啟動時由於內存不足而回收內存最後需要終止應用程序,但有時也會是由於app很長時間響應而終止。如果app當時運行在後台並且沒有暫停,系統會在應用程序終止之前調用app的代理的方法 - (void)applicationWillTerminate:(UIApplication *)application,這樣可以讓你可以做一些清理工作或保存一些數據或app的狀態。
與其他動態語言比較
OC中方法的實現只能寫在@implementation··@end中,對象方法的聲明只能寫在@interface···@end中間。對象方法都以-號開頭,類方法都以+號開頭。函數屬於整個文件,可以寫在文件中的任何位置,包括@interface··@end中,但寫在@interface···@end會無法識別。
對象方法只能由對象來調用,類方法只能由類來調用,不能當做函數一樣調用。對象方法歸類對象所有,類方法調用不依賴於對象,類方法內部不能直接通過成員變量名訪問對象的成員變量。OC只支持單繼承,沒有接口,但可以用delegate代替。
Objective-C與其他語言最大的區別是其運行時的動態性,它能讓你在運行時為類添加方法或者去除方法以及使用反射,極大的方便了程序的擴展。
參考文獻
《松本行宏的程序世界》
蘋果開發文檔