10个迷惑新手的CocoaObjective-c开发问题
?
本文轉(zhuǎn)載至 ?http://blog.csdn.net/lvxiangan/article/details/27964733首先請諒解我可能使用很多英文,畢竟英文資料將來會是你的主要資料來源。
這篇教程將描述一些我見到的眾多Cocoa開發(fā)新手遇到的問題和障礙。并不會手把手教你:“這個函數(shù)什么意思,哪個函數(shù)如何使用”,而是站在一定高度,統(tǒng)觀各種技術(shù)所處的角色,讓你不會迷失在各種技術(shù)細節(jié)中。在你繼續(xù)深入學(xué)習(xí)MacOS編程之前,請停下腳步看清楚這些問題。如果你是新手,這個教程不要希望一次能看的非常透徹,學(xué)一定階段反回來再看看又會有新的體會的。
?
?
1. language background
首先c語言背景,必須。 很多人問:“沒有任何語言基礎(chǔ),我不想學(xué)c直接學(xué)objective-c。是否可以?” 這里我簡單說幾句,objc是c的超集,也就是說大部分objc代碼其實是c、而且眾多傳統(tǒng)開源項目都是c寫成的。你不學(xué)好c在unix世界里只能是個二流開發(fā)者!也許說得過于嚴(yán)厲,不過自己斟酌把。c++呢大概了解一下即可,因為它太龐雜了。
接著English,必須。 蘋果不會把它們文檔都寫成中文的。“什么,有人翻譯?” 等有人閑著翻譯出來了的時候,大家都已經(jīng)賣了很多軟件了。你也是跟著人家屁股后面做開發(fā)。
?
2. runtime(運行時)
Objective-c是動態(tài)語言, 很多新手或者開發(fā)人員常常被runtime這個東西所迷惑。而恰恰這是一個非常重要的概念。我可以這么問:“如果讓你(設(shè)計)實現(xiàn)一個計算機語言,你要如何下手?” 很少程序員這么思考過。但是這么一問,就會強迫你從更高層次思考1以前的問題了。 注意我這句話‘設(shè)計’括起來了,稍微次要點,關(guān)鍵是實現(xiàn)。
我把實現(xiàn)分成3種不同的層次:
第一種是傳統(tǒng)的面向過程的語言開發(fā),例如c語言。實現(xiàn)c語言編譯器很簡單,只要按照語法規(guī)則實現(xiàn)一個LALR語法分析器就可以了,編譯器優(yōu)化是非常難的topic,不在這里討論范圍內(nèi),忽略。 這里我們實現(xiàn)了編譯器其中最最基礎(chǔ)和原始的目標(biāo)之一就是把一份代碼里的函數(shù)名稱,轉(zhuǎn)化成一個相對內(nèi)存地址,把調(diào)用這個函數(shù)的語句轉(zhuǎn)換成一個jmp跳轉(zhuǎn)指令。在程序開始運行時候,調(diào)用語句可以正確跳轉(zhuǎn)到對應(yīng)的函數(shù)地址。 這樣很好,也很直白,但是太死板了。Everything is predetermined.
我們希望語言更加靈活,于是有了第二種改進,開發(fā)面向?qū)ο蟮恼Z言,例如c++。 c++在c的基礎(chǔ)上增加了類的部分。但這到底意味著什么呢?我們再寫它的編譯器要如何考慮呢?其實,就是讓編譯器多繞個彎,在嚴(yán)格的c編譯器上增加一層類處理的機制,把一個函數(shù)限制在它處在的class環(huán)境里,每次請求一個函數(shù)調(diào)用,先找到它的對象, 其類型,返回值,參數(shù)等等,確定了這些后再jmp跳轉(zhuǎn)到需要的函數(shù)。這樣很多程序增加了靈活性同樣一個函數(shù)調(diào)用會根據(jù)請求參數(shù)和類的環(huán)境返回完全不同的結(jié)果。增加類機制后,就模擬了現(xiàn)實世界的抽象模式,不同的對象有不同的屬性和方法。同樣的方法,不同的類有不同的行為! 這里大家就可以看到作為一個編譯器開發(fā)者都做了哪些進一步的思考。雖然面相對象語言有所改進,但還是死板, 我們?nèi)匀唤衏++是static language.
希望更加靈活!于是我們完全把上面哪個類的實現(xiàn)部分抽象出來,做成一套完整運行階段的檢測環(huán)境,形成第三種,動態(tài)語言。這次再寫編譯器甚至保留部分代碼里的sytax名稱,名稱錯誤檢測,runtime環(huán)境注冊所以全局的類,函數(shù),變量等等信息等等,我們可以無限的為這個層增加必要的功能。調(diào)用函數(shù)時候,會先從這個運行時環(huán)境里檢測所以可能的參數(shù)再做jmp跳轉(zhuǎn)。這,就是runtime。編譯器開發(fā)起來比上面更加彎彎繞。但是這個層極大增加了程序的靈活性。 例如當(dāng)調(diào)用一個函數(shù)時候,前2種語言,很有可能一個jmp到了一個非法地址導(dǎo)致程序crash, 但是在這個層次里面,runtime就過濾掉了這些可能性。 這就是為什么dynamic langauge更加強壯。 因為編譯器和runtime環(huán)境開發(fā)人員已經(jīng)幫你處理了這些問題。
好了上面說著這么多,我們再返回來看objective-c的這些語句:
?
| 1 2 3 4 5 6 7 8 9 10 | id obj=self; if ([obj respondsToSelector:@selector(function1:)) { } if ([obj isKindOfClass:[NSArray class]] ) { } if ([obj conformsToProtocol:@protocol(myProtocol)]) { } if ([[obj class] isSubclassOfClass:[NSArray class]]) { } [obj someNonExistFunction]; |
?
看似很簡單的語句,但是為了讓語言實現(xiàn)這個能力,語言開發(fā)者要付出很多努力實現(xiàn)runtime環(huán)境。這里運行時環(huán)境處理了弱類型、函數(shù)存在檢查工作。runtime會檢測注冊列表里是否存在對應(yīng)的函數(shù),類型是否正確,最后確定下來正確的函數(shù)地址,再進行保存寄存器狀態(tài),壓棧,函數(shù)調(diào)用等等實際的操作。
?
| 1 2 3 | id knife=[Knife grateKnife]; NSArray *monsterList=[NSArray array]; [monsterList makeObjectsPerformSelector:@selector(killMonster:) withObject:knife]; |
?
用c,c++完成這個功能還是比較非常麻煩的,但是動態(tài)語言處理卻非常簡單并且這些語句讓objc語言更加intuitive。
在Objc中針對對象的函數(shù)調(diào)用將不再是普通的函數(shù)調(diào)用,[obj function1With:var1];?這樣的函數(shù)調(diào)用將被運行時環(huán)境轉(zhuǎn)換成objc_msgSend(target,@selector(function1With:),var1);。Objc的runtime環(huán)境是開源的,所以我們可以拿出一下實現(xiàn)做簡單介紹,可以看到objc_msgSend由匯編語言實現(xiàn),我們甚至不必閱讀代碼,只需查看注釋就可以了解,運行時環(huán)境在函數(shù)調(diào)用前做了比較全面的安全檢查,已確保動態(tài)語言函數(shù)調(diào)用不會導(dǎo)致程序crash。對于希望深入學(xué)習(xí)的朋友可以自行下載Objc-runtime源代碼來閱讀,這里就不再深入講解。
?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | /******************************************************************** * id?????? objc_msgSend(id self, *??????????SEL op, *??????????...) * * On entry: a1 is the message receiver, *?????????? a2 is the selector ********************************************************************/ ????ENTRY objc_msgSend # check whether receiver is nil ????teq???? a1, #0 ????moveq?? a2, #0 ????bxeq????lr # save registers and load receiver's class for CacheLookup ????stmfd?? sp!, {a4,v1-v3} ????ldr???? v1, [a1, #ISA] # receiver is non-nil: search the cache ????CacheLookup a2, LMsgSendCacheMiss # cache hit (imp in ip) - prep for forwarding, restore registers and call ????teq v1, v1??????/* set nonstret (eq) */ ????ldmfd?? sp!, {a4,v1-v3} ????bx??????ip # cache miss: go search the method lists LMsgSendCacheMiss: ????ldmfd?? sp!, {a4,v1-v3} ????b?? _objc_msgSend_uncached LMsgSendExit: ????END_ENTRY objc_msgSend ????.text ????.align 2 _objc_msgSend_uncached: # Push stack frame ????stmfd?? sp!, {a1-a4,r7,lr} ????add???? r7, sp, #16 ????SAVE_VFP # Load class and selector ????ldr a1, [a1, #ISA]??????/* class = receiver->isa??*/ ????# MOVE??a2, a2??????????/* selector already in a2 */ # Do the lookup ????MI_CALL_EXTERNAL(__class_lookupMethodAndLoadCache) ????MOVE????ip, a1 # Prep for forwarding, Pop stack frame and call imp ????teq v1, v1??????/* set nonstret (eq) */ ????RESTORE_VFP ????ldmfd?? sp!, {a1-a4,r7,lr} ????bx??ip |
?
現(xiàn)在說一下runtime的負面影響: 1. 關(guān)于執(zhí)行效率問題。 “靜態(tài)語言執(zhí)行效率要比動態(tài)語言高”,這句沒錯。因為一部分cpu計算損耗在了runtime過程中,而從上面的匯編代碼也可以看出,大概損耗在哪些地方。而靜態(tài)語言生成的機器指令更簡潔。正因為知道這個原因,所以開發(fā)語言的人付出很大一部分努力為了保持runtime小巧上。所以objecitve-c是c的超集+一個小巧的runtime環(huán)境。 但是,換句話說,從算法角度考慮,這點復(fù)雜度不算差別的,Big O notation結(jié)果不會有差別。( It’s not log(n) vs n2) 2. 另外一個就是安全性。動態(tài)語言由于運行時環(huán)境的需求,會保留一些源碼級別的程序結(jié)構(gòu)。這樣就給破解帶來的方便之門。一個現(xiàn)成的說明就是,java,大家都知道java運行在jre上面。這就是典型的runtime例子。它的執(zhí)行文件.class全部可以反編譯回近似源代碼。所以這里的額外提示就是如果你需要寫和安全有關(guān)的代碼,離objc遠點,直接用c去。
簡單理解:“Runtime is everything between your each function call.”
但是大家要明白,第二點我提到runtime并不只是因為它帶來了這些簡便的語言特性。而是這些簡單的語言特性,在實際運用中需要你從完全不同的角度考慮和解決問題。只是計算1+1,很多語言都是一樣的,但是隨著問題的復(fù)雜,項目的增長,靜態(tài)語言和動態(tài)語言就會演化出完全不同的風(fēng)景。
?
3. thread
“thread synchronization another notorious trouble!”
記得上學(xué)時候?qū)W操作系統(tǒng)這門課,里面都會有專門一章介紹任務(wù)調(diào)度和生產(chǎn)者消費者的問題。 這就是為今后使用進程、線程開發(fā)打基礎(chǔ)。概念很簡單,但難點在synchronization(同步),因為死鎖檢測算法不是100%有效,否則就根本沒有死鎖這個說法了。另一個原因是往往這類錯誤很隱晦,靜態(tài)分析很難找到。同時多線程開發(fā)抽象度較高需要經(jīng)驗去把握。
總體來說,我見到的在這方面的問題可以分為一下幾點:
1. 對系統(tǒng)整體結(jié)構(gòu)認識模糊
不知道多線程開發(fā)的幾個基點,看別人代碼越看越糊涂的。一會NSThread,一會Grand Central Dispatch、block,一會又看到了pthread等等。Apple封裝了很多線程的API, 多線程開發(fā)的基本結(jié)構(gòu)入下圖:
Mac OS Thread Architecture
可以看到在多線程開發(fā)中你可以選擇這上面這4種不同的方式。
Mach是核心的操作系統(tǒng)部分。其實這個我也不是非常熟悉,至少我還沒有讀到過直接使用mach做多線程的代碼。
pthread(POSIX Threads)是傳統(tǒng)的多線程標(biāo)準(zhǔn),靈活、輕巧,但是需要理論基礎(chǔ),開發(fā)復(fù)雜。需要注意一點,根據(jù)apple文檔提示,在Cocoa下使用pthread需要先啟動至少一個NSThread,確定進入多線程環(huán)境后才可以。
NSThread是Mac OS 10.0后發(fā)布的多線程API較為高層,但是缺乏靈活性,而且和pthread相比效率低下。
Grand Central Dispatch?是10.6后引入的開源多線程庫,它介于pthread和NSThread之間。比NSThread更靈活、小巧,并且不需要像pthread一樣考慮很多l(xiāng)ock的問題。同時objective-c 2.0發(fā)布的新語法特性之一blocks,也正是根據(jù)Grand Central Dispatch需求推出的。
所以在你寫多線程代碼或者閱讀多線程代碼時候,心理要先明確使用哪種技術(shù)。
2. thread和Reference Counting內(nèi)存管理造成的問題。
線程里面的方法都要放到NSAutoreleasePool里面嗎?
這類問題很常見,迷惑的原因是不明白 NSAutoreleasePool 到底是干什么用的。NSAutoreleasePool跟thread其實關(guān)系并不顯著,它提供一個臨時內(nèi)存管理空間,好比一個沙箱,確保不會有不當(dāng)?shù)膬?nèi)存分配泄露出來,在這個空間內(nèi)新分配的對象要向這個pool做一下注冊告訴:“pool,我新分配一塊空間了”。當(dāng)pool drain掉或者release,它里面分配過的內(nèi)存同樣釋放掉。可見和thread沒有很大關(guān)系。但是,我們閱讀代碼的時候經(jīng)常會看到,新開線程的函數(shù)內(nèi)總是以NSAutoreleasePool開始結(jié)束。這又是為什么呢!? 因為thread內(nèi)恰好是最適合需要它的地方! 線程函數(shù)應(yīng)該計算量大,時間長(supposed to be heavy)。在線程里面可能會有大量對象生成,這時使用autoreleasepool管理更簡潔。所以這里的答案是,不一定非要在線程里放NSAutoreleasePool,相對的在cocoa環(huán)境下任意地方都可以使用NSAutoreleasePool。如果你在線程內(nèi)不使用NSAutoreleasePool,要記得在內(nèi)部alloc和relase配對出現(xiàn)保證沒有內(nèi)存泄露。
3. 線程安全
每個程序都有一個主線程(main thread),它負責(zé)處理事件響應(yīng),和UI更新。
更新UI問題。很多新手會因為這個問題,導(dǎo)致程序崩潰或出現(xiàn)各種問題。而且逛論壇會看到所以人都會這么告訴你:“不要在后臺線程更新你的UI”。其實這個說法不嚴(yán)密,在多線程環(huán)境里處理這個問題需要謹慎,而且要了解線程安全特性。
首先我們需要把“UI更新”這個詞做一個說明,它可以有2個層次理解,首先是繪制,其次是顯示。這里繪制是可以在任何線程里進行,但是要向屏幕顯示出來就需要在主線程操作了。我舉個例子說明一下,例如現(xiàn)在我們有一個NSImageView,里面設(shè)置了一個NSImage, 這時我想給NSImage加個變色濾鏡,這個過程就可以理解為繪制。那么我完全可以再另外一個線程做這個比較費時的操作,濾鏡增加完畢再通知NSImageView顯示一下。另一個例子就是,Twitter客戶端會把每一條微博顯示成一個cell,但是速度很快,這就是因為它先對cell做了offscreen的渲染,然后再拿出來顯示。
所以通過這點我們也可以得到進一步的認識,合理設(shè)計view的更新是非常重要的部分。很多新手寫得代碼片段沒錯,只是因為放錯了地方就導(dǎo)致整個程序出現(xiàn)各種問題。
根據(jù)蘋果線程安全摘要說明,再其它線程更新view需要使用lockFocusIfCanDraw和unlockFocus鎖定,確保不會出現(xiàn)安全問題。
另外還要知道常用容器的線程安全情況。immutable的容器是線程安全的,而mutable容器則不是。例如NSArray和NSMutableArray。
4. Asynchronous(異步) vs. Synchronous(同步)
我在一個view要顯示多張web圖片,我想問一下,我是應(yīng)該采用異步一個一個下載的方式,還是應(yīng)該采用多線程同時下載的方式,還是2個都用,那種方式好呢?
實際上單獨用這2個方法都不好。并不是簡單的用了更多線程就提高速度。這個問題同時涉及客戶端和服務(wù)器的情況。
處理這種類型的程序,比較好的結(jié)構(gòu)應(yīng)該是:非主線程建立一個隊列(相當(dāng)于Asynchronous任務(wù)),隊列里同時啟動n個下載任務(wù)(相當(dāng)于Synchronous任務(wù))。這里的n在2~8左右就夠了。這個結(jié)構(gòu)下可以認為隊列里面每n個任務(wù)之間是異步關(guān)系,但是這n個任務(wù)之間又是同步關(guān)系,每次同時下載2~8張圖片。這樣處理基本可以滿足速度要求和各類服務(wù)器限制。
5. thread和run-loop
runloop是線程里的一部分,但我覺得有必要單獨拿出來寫,是因為它涉及的東西比較容易誤解,而說明它的文章又不多。
4. run-loop
thread和runloop在以前,開發(fā)者根本不太當(dāng)成一個問題。因為沒有靜態(tài)語言里runloop就是固定的線程執(zhí)行l(wèi)oop。而現(xiàn)在Cocoa新手搞不明白的太多了,因為沒有從動態(tài)角度看它,首先回想一下第2點介紹的runtime概念,接著出一個思考題。
現(xiàn)在有一個程序片段如下:
?
| 1 2 3 4 5 6 7 8 9 10 11 | - (void)myThread:(id)sender { ????NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init]; ????while (TRUE) { ????????//do some jobs ????????//break in some condition ????????usleep(10000); ????????[pool drain]; ????} ????[pool release]; } |
?
現(xiàn)在要求,做某些設(shè)計,使得當(dāng)這個線程運行的同時,還可以從其它線程里往它里面隨意增加或去掉不同的計算任務(wù)。 這,就是NSRunloop的最原始的開發(fā)初衷。讓一個線程的計算任務(wù)更加靈活。 這個功能在c, c++里也許可以做到但是非常難,最主要的是因為語言能力的限制,以前的程序員很少這么去思考。
好,現(xiàn)在我們對上面代碼做一個非常簡單的進化:
?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | NSMutableArray *targetQueue; NSMutableArray *actionQueue; - (void)myThread:(id)sender { ????NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init]; ????while (TRUE) { ????????//do some jobs ????????//break in some condition ????????int n=[targetQueue count]; ????????assert(n==[actionQueue count]); ????????for(int i=0;i<n;i++){ ????????????id target=[targetQueue objectAtIndex:i]; ????????????SEL action=NSSelectorFromString([actionQueue objectAtIndex:i]); ????????????if ([target respondsToSelector:action]) { ????????????????[target performSelector:action withObject:nil]; ????????????} ????????} ????????usleep(10000); ????????[pool drain]; ????} ????[pool release]; } |
?
注意,這里沒有做線程安全處理,記住Mutable container is not thread safe. 這個簡單的擴展,讓我們看到了如何利用runtime能力讓線程靈活起來。當(dāng)我們從另外線程向targetQueue和actionQueue同時加入對象和方法時候,這個線程函數(shù)就有了執(zhí)行一個額外代碼的能力。
有人會問,哪里有runloop? 那個是nsrunloop? 看不出來啊。
?
| 1 2 3 | while (TRUE) { ????//break in some condition } |
?
這個結(jié)構(gòu)就叫線程的runloop, 它和NSRunloop這個類雖然名字很像,但完全不是一個東西。以前在使用靜態(tài)語言開始時候,程序員沒有什么迷惑,因為沒有NSRunloop這個東西。 我接著來說,這個NSRunloop是如何來得。
第二段擴展代碼里面確實沒有NSRunloop這個玩意兒,我們接著做第3次改進。 這次我們的目的是把其中動態(tài)部分抽象出來。
?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | @interface MyNSTimer : NSObject { ????id target; ????SEL action; ????float interval; ????CFAbsoluteTime lasttime; } - (void)invoke; @end @implementation MyNSTimer - (void)invoke; { ????if ([target respondsToSelector:action]) { ????????[target performSelector:action withObject:nil]; ????} } @end #!objc @interface MyNSRunloop : NSObject { ????NSMutableArray *timerQueue; } - (void)addTimer:(MyNSTimer*)t; - (void)executeOnce; @end @implementation MyNSRunloop - (void)addTimer:(MyNSTimer*)t; { ????@synchronized(timerQueue){ ????????[timerQueue addObject:t]; ????} } - (void)executeOnce; { ????CFAbsoluteTime currentTime=CFAbsoluteTimeGetCurrent(); ????@synchronized(timerQueue){ ????????for(MyNSTimer *t in timerQueue){ ????????????if(currentTime-t.lasttime>t.interval){ ????????????????t.lasttime=currentTime; ????????????????[t invoke]; ????????????} ????????} ????} } @end #!objc @interface MyNSThread : NSObject { ????MyNSRunloop *runloop; } - (void)main:(id)sender; @end @implementation MyNSThread - (void)main:(id)sender { ????NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init]; ????while (TRUE) { ????????//do some jobs ????????//break in some condition ????????[runloop executeOnce]; ????????usleep(10000); ????????[pool drain]; ????} ????[pool release]; } @end |
?
走到這里,我們就算是基本把Runloop結(jié)構(gòu)抽象出來了。例如我有一個MyNSThread實例,myThread1。我可以給這個實例的線程添加需要的任務(wù),而myThread1內(nèi)部的MyNSRunloop對象會管理好這些任務(wù)。
?
| 1 2 3 4 5 | MyNSTimer *timer1=[MyNSTimer scheduledTimerWithTimeInterval:1 target:obj1 selector:@selector(download1:)]; [myThread1.runloop addTimer:timer1]; MyNSTimer *timer2=[MyNSTimer scheduledTimerWithTimeInterval:2 target:obj2 selector:@selector(download2:)]; [myThread1.runloop addTimer:timer2]; |
?
當(dāng)你看懂了上面的代碼也許會感嘆,‘原來是這么回事啊!為什么把這么簡單的功能搞這么復(fù)雜呢?’ 其實就是這么回事,把Runloop抽象出來可以使得線程任務(wù)管理更加loose coupling,給設(shè)計模式提供更大的空間。這樣第三方開發(fā)者不需要過深入的涉及線程內(nèi)部代碼而輕松管理線程任務(wù)。另外請注意,這里MyNSRunloop, MyNSTimer等類是我寫得一個模擬情況,真實的NSRunloop實現(xiàn)肯定不是這么簡單。這里為了說明一個思想。這種思想貫穿整個cocoa framework,從界面更新到event管理。
?
5. delegate, protocol
這個會列出來因為,我感覺問它的數(shù)量僅此于內(nèi)存管理部分,它們用得很頻繁,并且這些是設(shè)計模式的重要組成部分。
待寫…
?
6. event respon-der
Interface Builder First Responder
使用過Xcode的開發(fā)者都知道Interface Builder這個開發(fā)組件,在Xcode4版本以后該組件已經(jīng)和xcode整合到一起。它是蘋果軟件開發(fā)中非常重要的部分。ib為開發(fā)者減輕了很大一部分界面設(shè)計工作。但是其中有一個東西讓新接觸ib的開發(fā)者一頭霧水,那就是First Responder, 它是什么東西,為何它會有那么多Actions。這節(jié)我會詳細介紹如何理解Responder和Cocoa下的事件響應(yīng)鏈。
First Responder在IB屬性為Placeholders,這意味著它屬于一個虛擬實例。就好比TextField里面的string placeholder一樣,只是臨時顯示一下。真正的first responder會被其它對象代替。實際上,任何派生自NSResponder類的對象都可以替代First Responder。而First Responder里面的所有Actions就是NSResponder提供的接口函數(shù),當(dāng)然你也可以定義自己的響應(yīng)函數(shù)。
MacOS在系統(tǒng)內(nèi)部會維護一個稱為“The Responder Chain”的鏈表。該列表內(nèi)容為responder對象實例,它們會對各種系統(tǒng)事件做出響應(yīng)。最上面的哪個對象就叫做first responder,它是最先接收到系統(tǒng)事件的對象。如果該對象不處理該事件,系統(tǒng)會將這個事件向下傳遞,直到找到響應(yīng)事件的對象,我們可以理解為該事件被該這個對象截取了。
The Responder Chain基本結(jié)構(gòu)如下圖所示:
The Responder Chain
在理解了上面的概念之后,我希望使用一個例子讓大家對responder有更加具體的認識。大家都知道NSTextField這個控件,它是最常見的控件之一。它最基本功能是顯示一個字符串,如果啟用可選,那么用戶可以選中文本,拷貝文本,如果開啟編輯選項,還可以運行用戶編輯文本,等等基本操作。
下面展示給大家的例子是創(chuàng)建一個我們自己創(chuàng)建的簡單textfield叫LXTextField。它不屬于NSTextField而是派生自NSView,具有功能顯示字符串,全選字符串,響應(yīng)用戶cmd+c的拷貝操作,三個基本功能。注意NSView派生自NSResponder。
?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | // //??LXTextField.h //??lxtextfield // //??Created by xu lian on 12-03-09. //??Copyright (c) 2012 Beyondcow. All rights reserved. // #import <AppKit/AppKit.h> @interface LXTextField : NSView { ????NSString *stringValue; ????BOOL selectAll; } @property(retain,nonatomic) NSString *stringValue; @end #!objc // //??LXTextField.m //??lxtextfield // //??Created by xu lian on 12-03-09. //??Copyright (c) 2012 Beyondcow. All rights reserved. // #import "LXTextField.h" @implementation LXTextField @synthesize stringValue; - (void)awakeFromNib { ????selectAll = NO; } - (id)initWithFrame:(NSRect)frameRect { ????if( self = [super initWithFrame:frameRect] ){ ????????selectAll = NO; ????} ????return self; } - (BOOL)acceptsFirstResponder { ????return YES; } - (BOOL)becomeFirstResponder { ????return YES; } - (BOOL)resignFirstResponder { ????selectAll=NO; ????[self setNeedsDisplay:YES]; ????return YES; } - (void)setStringValue:(NSString *)string{ ????stringValue = string; ????[self setNeedsDisplay:YES]; } - (void)drawRect:(NSRect)dirtyRect { ????if (selectAll) { ????????NSRect r = NSZeroRect; ????????r.size = [stringValue sizeWithAttributes:nil]; ????????[[NSColor selectedControlColor] set]; ????????NSRectFill(r); ????} ????[stringValue drawAtPoint:NSZeroPoint withAttributes:nil]; } - (IBAction)selectAll:(id)sender; { ????selectAll=YES; ????[self setNeedsDisplay:YES]; } - (IBAction)copy:(id)sender; { ????NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; ????[pasteBoard declareTypes:[NSArray arrayWithObjects:NSStringPboardType, nil] owner:nil]; ????[pasteBoard setString:stringValue forType:NSStringPboardType]; } - (void)mouseDown:(NSEvent *)theEvent { ????if ([theEvent clickCount]>=2) { ????????selectAll=YES; ????} ????[self setNeedsDisplay:YES]; } - (void)keyDown:(NSEvent *)theEvent { } @end |
?
運行實例,可以看到隨著LXTextField收到系統(tǒng)發(fā)送的becomeFirstResponder消息,LXTextField變成responder chain中的frist responder, 這時候可以理解為IB里的哪個First Responder虛擬實例被該LXTextField取代。這時候mainMenu上哪些菜單項,例如:全選(cmd+a), 拷貝(cmd+a)等事件都會最先發(fā)給當(dāng)前這個LXTextField。一旦你的LXTextField實現(xiàn)了NSResponder的哪些默認函數(shù),那么該對象就會截取系統(tǒng)事件。當(dāng)然這些事件具體如何實現(xiàn)還是需要你自己寫代碼實現(xiàn)。例如這里的 – (IBAction)copy:(id)sender; 顯然我手動實現(xiàn)了textfield的copy能力。
注意上述代碼中我實現(xiàn)了一個空函數(shù)- (void)keyDown:(NSEvent *)theEvent 這意味著我們希望LXTextField截取鍵盤事件而不再傳遞給responder chain后續(xù)對象。當(dāng)然,如果我們希望LXTextField響應(yīng)特定鍵盤事件,而其他事件繼續(xù)傳給其他響應(yīng)對象,我們可以編寫如下代碼。
?
| 1 2 3 4 5 6 7 8 | - (void)keyDown:(NSEvent *)theEvent { ????if(condition){ ????????do something; ????}else{ ????????[super keyDown:theEvent]; ????} } |
?
待寫…
?
7. mem-ory management
內(nèi)存管理問題,也許是問得最多的問題了吧。
開發(fā)速度不言而喻,你少寫很多release代碼,甚至很少去操心這部分。
執(zhí)行速度呢?這個還要從runtime說起,還記得我在第2點說得一句話么:“Runtime is everything between your each function call.”
RC有一個古老的內(nèi)存管理哲學(xué):誰分配誰釋放。?通過counting來確定一個資源有幾個使用者。道理很簡單,但是往往簡單的東西人卻會犯錯。從來沒有一個程序員可以充滿信心的說,我寫得代碼從來沒有過內(nèi)存泄露。這樣來看,我們就更需要讓程序可以自己處理這個管理機制,這就需要把這個機制放到runtime里。
所以RC->ARC就是把內(nèi)存管理部分從普通開發(fā)者的函數(shù)中移到了函數(shù)外的runtime中。因為runtime的開發(fā)原型簡單,邏輯層次更高,所以做這個開發(fā)和管理出錯的概率更小。實際上編譯器開發(fā)人員對這部分經(jīng)過無數(shù)次測試,所以可以說用arc幾乎不會出錯。另外由于編譯的額外優(yōu)化,使得這個部分比程序員自己寫得代碼要快速很多。而且對于一些通用的開發(fā)模式,例如autorelease對象,arc有更優(yōu)秀的算法保證autoreleasepool里的對象更少。
這個規(guī)則可以讓NSObject決定是不是要釋放內(nèi)存。當(dāng)一個對象alloc時候,系統(tǒng)分配其一塊內(nèi)存并且object自動計數(shù)retainCount=1 這時候每當(dāng)[object retain]一次retainCount+1(這里雖然簡寫也是rc不過是巧合或者當(dāng)時開發(fā)人員故意選的retain這個詞吧)每次[object release]時候retainCount-1 當(dāng)retainCount==0時候object就真正把這快內(nèi)存還給系統(tǒng)。
常用container的Reference Counting特性 這個規(guī)則很簡單把。但是這塊確實讓新手最頭疼的地方。問題出在,新手總想去驗證rc規(guī)則,又總是發(fā)現(xiàn)和自己的期望不符合。 無數(shù)次看到有人寫下如下句子
NSLog(@”%d”,[object retainCount]);
while([object retainCount]>0){ [object release]; }
當(dāng)然了,我也做過類似的動作,那種希望一切盡在掌握中的心態(tài)。但是你會看到其他人告訴這么做完全沒有意義,RC規(guī)則并不是這么用的。
首先,這個數(shù)字也許并不是你心目中的哪個。因為很難跟蹤到底哪些地方引用的該資源。你建立的資源不光只有你的代碼才會用到,你調(diào)用的各種Framework,Framework調(diào)用的Framework,都有可能改變這個資源的retainCount。
其次,這里每個數(shù)字意味著有其它對象引用該資源,這樣的暴力釋放很容易導(dǎo)致程序崩潰。就好比,其它人也許可以翻牌子把門口哪個牌子上的數(shù)字改變,但是這會出現(xiàn)問題。還有很多人在里面,把牌子變成0房間鎖了結(jié)果誰也出不來。又或者,減少牌子上的數(shù)字,人進的過多房間變得過于擁擠。
所以去驗證rc規(guī)則,或者單純的改變retainCount并不是明智之舉。你能做的就是理解規(guī)則,使用規(guī)則,讀文檔了解container的引用特性。或者干脆移到 Automatic Reference Counting (ARC) 上面。
我有一個NSMutableArray里面保存了1000個NSString對象,我在release的時候需要循環(huán)釋放1000個string么?還是只需要release NSMutableArray。
就像上面提到的,如果你了解container的引用特性,這個問題自然就解決了。“NSMutableArray在添加、插入objects時會做retain操作。” 通過這一句話就分析出,用戶不否需要幫助NSMutableArray釋放1000個string。回憶上面提到的管理哲學(xué),“誰分配誰釋放” 編寫NSMutableArray的程序員非常熟悉這個規(guī)則,NSMutableArray內(nèi)部retain了,NSMutableArray自然要負責(zé)release。但是NSMutableArray才不會管你在外面什么地方引用了這1000個string,它只管理好內(nèi)部的rc就夠了。所以如果你在NSMutableArray外面對1000個string retain了,你自然需要release。相應(yīng)的,你作為創(chuàng)建這個NSMutableArray的程序員,你只管release這個NSMutableArray就可以了。
最后說一下不用arc的情況。目前情況來看,有不少第三方的庫并未支持arc,所以如果你的舊項目使用了這些庫,請檢查是否作者發(fā)布了新版本,或者你需要自己修正支持arc。
?
8. class heritage, category and extensions
Objective-C 的 OOP 特性提供 subclass 和 category 這2個非常重要的部分。subclass 應(yīng)該反復(fù)被各種編程書籍介紹過。它是 OOP 繼承特性的關(guān)鍵語法,它給類添加了延續(xù)并且多樣化自己的方法。可以說沒有繼承就沒有 OOP 這玩意。而 category 相對于 subclass 就不那么出名了。其實 category 思想出世于 smalltalk,所以它不能算是一個新生事物。
先說一下這2個特性最主要的區(qū)別。簡單可以這么理解,subclass 體現(xiàn)了類的上下級關(guān)系,而 category 是類間的平級關(guān)系。
Subclass and Category
如上圖所示,左側(cè)是subclass,可以看到class, subclass1, subclass2是遞進關(guān)系。同時下面的子類完全繼承父類的方法,并且可以覆蓋父類的方法。subclass2擁有function1,function2,function3三個函數(shù)方法。function1的執(zhí)行代碼來自subclass1, function2的執(zhí)行代碼來自于subclass2。
右側(cè)是category。可以看到,無論如何擴展類的category,最終就只有一個類class。category可以說是類的不同方法的小集合,它把一個類的方法劃分成不同的區(qū)塊。請注意觀察,每個category塊內(nèi)的方法名稱都沒有重復(fù)的。這正是category的重要要求。
經(jīng)過上面簡單解釋了解了這2點的基本區(qū)別,現(xiàn)在深入說一下category。
在Objective-c語言設(shè)計之初一個主要的哲學(xué)觀點就是盡量讓一個程序員維護龐大的代碼集。(對于龐大的項目‘原則’和‘協(xié)議’是非常重要的東西。甚至編寫良好的文件名都是非常重要的開發(fā)技巧)根據(jù)結(jié)構(gòu)化程序設(shè)計的經(jīng)驗出發(fā),把一個大塊代碼劃分成一些小塊的代碼更便于程序員管理。于是objc借用了smalltalk的categories概念。允許程序員把一系列功能相近的方法組織到一個單獨的文件內(nèi),使得這些代碼更容易識別。
更進一步的,和c,c++這種靜態(tài)語言相比。objc把class categories功能集成到了run-time里面。因此,objc的categories允許程序員為已經(jīng)存在的類添加新的方法而不需要重新編譯舊的類。一旦一個category加入,它可以訪問該類所有方法和實例變量,包括私有變量。
category不僅可以為原有class添加方法,而且如果category方法與類內(nèi)某個方法具有同樣的method signature,那么category里的方法將會替換類的原有方法。這是category的替換特性。利用這個特性,category還可以用來修復(fù)一些bugs。例如已經(jīng)發(fā)布的Framework出現(xiàn)漏洞,如果不便于重新發(fā)布新版本,可以使用category替換特性修復(fù)漏洞。另外,由于category有run-time級別的集成度,所以使得cocoa程序安全性有所下降。許多黑客就是利用 category、posting2、Method Swizzling 等方法破解軟件,或者為軟件增加新功能。一個很典型的例子就是,我原來發(fā)布的QQ表情管理器(目前已經(jīng)不再維護)。
值得注意的一點是,由于一個類的categories之間是平級關(guān)系。所以如果不同categories擁有相同的方法,這個調(diào)用結(jié)果是未知的:
Category methods should not override existing methods (class or instance). Two different categories implementing the same method results in undefined behavior.
(因為posting、Method Swizzling這個話題有些深入,本文里我就不介紹了。有興趣自行Google)
Objc中Categories有其局限的部分,就是你不能為原有的class添加變量,只能添加方法。當(dāng)然方法里可以添加局部變量。在這個局限基礎(chǔ)上就有其它語言做了進一步改進,例如TOM語言就為Categories增加了添加類變量的能力。
自從Objc 2.0以后,語言引入了一個新的特性叫做 Class Extensions, 它可以看做是一類特殊的 category,可以給原有類增加新的屬性和方法。
通過http://developer.apple.com/library/ios/documentation/cocoa/conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html介紹我們可以看出,如果 categories 是為類增加外部方法的話,那么 extensions 就是用做類的內(nèi)部拓展。
Class extensions 的外觀很簡單,就是一個 Category 后面括號內(nèi)的名字為空:
?
| 1 2 | @interface ClassName () @end |
?
接下來,你就可以給你的類里添加屬性,方法了:
?
| 1 2 3 4 5 6 | @interface XYZPerson (){ ????id _someCustomInstanceVariable; } @property NSObject *extraProperty; - (void)assignUniqueIdentifier; @end |
?
Class extensions 常用來定義類的私有變量和方法。
總上所屬,如果你開發(fā)時候遇到無論如何都需要為類添加變量的情況,最好的選擇就是subclass。相反如果你只希望增加一些函數(shù)簇。Categories是最好的選擇。而類內(nèi)部需要用到的私有變量和方法則最好寫在 Class extensions 里。
Categories關(guān)注的重心是代碼設(shè)計,把不同功能的方法分離開。在Objc里因為Categories是runtimes級別的特性,所以這種分離不僅體現(xiàn)在源碼結(jié)構(gòu)上,同時體現(xiàn)在運行時過程中。這意味著一個category里的方法在程序運行中如果沒有被調(diào)用,那么它就不會被加載到內(nèi)存中。所以合理的使用categories會減少你的程序內(nèi)存消耗。
所以我個人給大家的建議是,每個Cocoa程序員都應(yīng)該收集整理自己的一套NS類函數(shù)的Categories擴展庫。這對你今后程序開發(fā)效率和掌控情況都有很大提高。
?
9. Drawing Issues
大家知道,MacOS 是一個非常注重UI的系統(tǒng)。所以在 MacOS 編程里繪制是一個非常重要的部分。第9部分,我會介紹 MacOS 下繪制編程。
從繪制技術(shù)分類上看,Cocoa程序員能接觸的幾種繪制技術(shù)列表如下:
在這里我不打算給大家介紹如何繪制具體的按鈕或者表格。只是介紹一下,它們的代碼風(fēng)格,優(yōu)勢和限制。
Cocoa Drawing
Cocoa Drawing應(yīng)該是學(xué)習(xí)Cocoa程序開發(fā)最先接觸的繪制技術(shù)。也是目前大多數(shù)MacOS程序所使用的繪制技術(shù),其底層使用Quazrtz 2D(Core Graphics)。蘋果對應(yīng)文檔為?Cocoa Drawing Guide。Cocoa Drawing并沒有統(tǒng)一的繪制函數(shù),所有繪制函數(shù)分散在幾個主要的NS類的下面。例如, NSImage, NSBezierPath, NSString, NSAttributedString, NSColor, NSShadow,NSGradient …
所以很簡單,當(dāng)你看到如下代碼就可以判斷,使用的是Cocoa Drawing方法
?
| 1 2 3 4 5 6 7 | [anImage drawInRect:rect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0]; [@"some text" drawAtPoint:NSZeroPoint withAttributes:attrs]; NSBezierPath *p=[NSBezierPath bezierPathWithRect:rect]; [[NSColor redColor] set]; [p fill]; |
?
這種代碼多出現(xiàn)在NSView的drawRect函數(shù)內(nèi)。Cocoa Drawing 的渲染上下文是 NSGraphicsContext,我不斷的看到很多新手把 NSGraphicsContext 和 CoreGraphics 的 CGContextRef 搞混。雖然它們很像并且也確實是有關(guān)系的,不過如果你不了解當(dāng)繪制時候的 render context 很多時候?qū)⒌玫揭粋€空白頁面的結(jié)果。
Core Graphics
Core Graphics 是 Cocoa Drawing layer 的底層技術(shù),在 iOS 開發(fā)中非常普遍,因為 iOS 系統(tǒng)中并不存在 Cocoa layer 所以網(wǎng)上可以找到的多是 Core Graphics 繪制代碼段子,這給那些不了解 Mac 開發(fā)的新手來說造成了很大困擾。Cocoa 是 Mac OS 下的 application framework 而 iOS 下的 application framework 則是 UIKit.framework又叫 Cocoa Touch,它們分享部分代碼基礎(chǔ)但又不完全一樣。例如,Cocoa Touch 下的 UIView 的渲染上下文會使用 UIGraphicsGetCurrentContext() 取得,它得到的是一個 CGContextRef 指針,而在 NSView 里多用 [NSGraphicsContext currentContext] 取得渲染上下文。它得到的是一個 NSGraphicsContext 對象。當(dāng)然 NSView 里也可以通過 CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; 來取得一個 Core Graphics 渲染上下文。 可見 Mac OS 下的開發(fā)更為靈活一些。因為 iOS 中的 UIKit 開發(fā)初期就瞄準(zhǔn)了顯卡硬件加速,所有 UIView 都是默認 layer-backed 的。iOS 開發(fā)者必須使用 Core Graphics 和 Core Animation 這幾個相對底層的繪制技術(shù)。
請看下面等價代碼,作用是繪制一個白色矩形。但是分別使用 Core Graphics 和 Cocoa Drawing:
?
| 1 2 3 4 5 6 7 | const CGFloat white[]={ 1.0, 1.0, 1.0, 1.0 }; CGContextSetFillColor(cgContextRef, white); CGContextSetBlendMode(cgContextRef, kCGBlendModeNormal); CGContextFillRect(cgContextRef, CGRectMake(0, 0, width, height)); [[NSColor whiteColor] set]; NSRectFillUsingOperation(NSMakeRect(0, 0, width, height), NSCompositeSourceOver); |
?
可以看出,這是2種風(fēng)格完全不同的繪制技術(shù)。Cocoa Drawing 是分散式的繪制函數(shù),而 Core Graphics 是傳統(tǒng)的類似 OpenGL 的集成式的繪制方式。其實 Cocoa Drawing 下層是 Core Graphics, Core Graphics 的下層是 OpenGL。
在 OSX 下 NSGraphicsContext 和 CGContextRef 大部分時候是可以相互轉(zhuǎn)換的。
NSGraphicsContext 到 CGContextRef:
?
| 1 2 3 4 | CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; CGContextSaveGState(ctx); //Core Graphic drawing code here CGContextRestoreGState(ctx); |
?
CGContextRef 到 NSGraphicsContext:
?
| 1 2 3 4 5 | NSGraphicsContext *ctx = [NSGraphicsContext graphicsContextWithGraphicsPort:cgContextRef flipped:NO]; [NSGraphicsContext saveGraphicsState]; [NSGraphicsContext setCurrentContext:ctx]; //cocoa drawing code here [NSGraphicsContext restoreGraphicsState]; |
?
大部分時候使用 Cocoa Drawing 可以繪制出需要的效果,但是某些特殊時候需要 Core Graphic 繪制,例如一些特殊的陰影,clip效果,自定義pattern phase,blending style等等。
Core Animation
如果說 Core Graphics 和 Cocoa Drawing 是通用的 UI 繪制框架的話,那么 CA 顯然是界面動畫繪制的高級技術(shù)。 Core Animation 的對應(yīng) Cocoa Animation 部分應(yīng)該是 NSAnimation 和 NSViewAnimation,但這2個差距比較大。NSAnimation 出現(xiàn)與 OS X 10.4,Core Animation 是 10.5 后出現(xiàn)的。NSViewAnimation 功能和使用相對簡單。
簡單來說,Core Animation 的作用對象是 CALayer, NSAnimation 的作用對象是 NSView。了解你的程序界面是在處理那種對象很重要。
Core Image
對于這個繪制技術(shù),這篇文章給了我很多啟示大家也可以看看。Notes on Rendering 2D Graphics on a Mac?雖然是一篇 note 但是,記錄了很多實際應(yīng)用中的經(jīng)歷,可以對個各種繪制技術(shù)有一個比較全面的解析。
根據(jù)此文的介紹。Core Image 適合處理小量大圖,而非常不適合處理大量小圖。因為 CI 利用 GPU 運算,而數(shù)據(jù)到 GPU 的round-trip 時間數(shù)量級在 millisecond。這就意味著,1000 張小圖分別再 GPU 運算,時間至少再 1000*1 ms。此文作者嘗試?yán)L制3000張小圖片,利用 Cocoa Drawing 原本耗時 750ms,但是改用CI后耗時猛增到3秒。
所以,這就是CI在osx繪制技術(shù)里所處的宏觀角色:單圖做實時處理。
openGL
待寫…
?
10. design pattern
待寫…
1:這里其實很有意思,為何我用“更高層次思考”,而不是“更底層次”。作為一個編譯器和語言開發(fā)人員,面對的問題確實更底層沒錯,但是他們思考的維度更高,更抽象,這樣子。一個不算恰當(dāng)?shù)谋确骄秃孟褚粋€三維世界的人處理二維世界的一條線的問題。
2:Posting技術(shù)在10.5以后deprecated,并且64bit run-time也不再支持
from?http://lianxu.me/2012/11/10-cocoa-objc-newbie-problems/
總結(jié)
以上是生活随笔為你收集整理的10个迷惑新手的CocoaObjective-c开发问题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 设置函数环境——setfenv
- 下一篇: Excel插件类库的设计思路