10個迷惑新手的Cocoa及Objective-C開發難點和問題

from:http://www.xcoder.cn/html/2012/objc_1228/272.html



1. C,C++ baCkground

很多人問 “沒有任何語言基礎,我不想學C直接學Objective-C”

這裏簡單幾句,Objective-C 90%代碼是C、衆多開源代碼是C,C++。你不學好C在unix世界裏只能是個二流開發者!也許說得過於嚴厲,不過自己斟酌。

2. Runtime(運行時)

Objective-C是動態語言, 很多新手或者開發人員常常被Runtime這個東西所迷惑。而恰恰這是一個非常重要的概念。 爲什麼重要呢!?我可以這麼問:“如果讓你(設計、)實現一個計算機語言,你要如何下手?” 很少程序員這麼思考過。但是這麼一問,就會強迫你從更高層次思考(1)以前的問題了。 注意我這句話‘設計’括起來了,稍微次要點,關鍵是實現。

我把實現分成3鐘不同的層次:

1. 傳統的面向過程的語言開發,例如C語言。實現C語言編譯器很簡單,只要按照語法規則實現一個LALR語法分析器就可以了,編譯器優化是非常難的 topiC,不在這裏討論範圍內,忽略。 這裏我們實現了編譯器其中最最基礎和原始的目標之一就是把一份代碼裏的函數名稱,轉化成一個相對內存地址,把調用這個函數的語句轉換成一個jmp跳轉指 令。在程序開始運行時候,調用語句可以正確跳轉到對應的函數地址。 這樣很好,也很直白,但是。。。太死板了。everything is per-determined

2. 我們希望靈活,於是需要開發面向對象的語言,例如C++。 C++在C的基礎上增加了類的部分。但這到底意味着什麼呢?我們再寫它的編譯器要如何考慮呢?其實,就是讓編譯器多繞個彎,在嚴格的C編譯器上增加一層類 處理的機制,把一個函數限制在它處在的Class環境裏,每次請求一個函數調用,先找到它的對象, 其類型,返回值,參數等等,確定了這些後再jmp跳轉到需要的函數。這樣很多程序增加了靈活性同樣一個函數調用會根據請求參數和類的環境返回完全不同的結 果。增加類機制後,就模擬了現實世界的抽象模式,不同的對象有不同的屬性和方法。同樣的方法,不同的類有不同的行爲! 這裏大家就可以看到作爲一個編譯器開發者都做了哪些進一步的思考。但是。還是死板, 我們仍然叫C++是static language。

3. 希望更加靈活! 於是我們完全把上面哪個類的實現部分抽象出來,做成一套完整運行階段的檢測環境。這次再寫編譯器甚至保留部分代碼裏的sytax名稱,名稱錯誤檢 測,runtime環境註冊所以全局的類,函數,變量等等信息等等,我們可以無限的爲這個層增加必要的功能。調用函數時候,會先從這個運行時環境裏檢測所 以可能的參數再做jmp跳轉。這,就是runtime。編譯器開發起來比上面更加彎彎繞。但是這個層極大增加了程序的靈活性。 例如當調用一個函數時候,前2種語言,很有可能一個jmp到了一個非法地址導致程序Crash, 但是在這個層次裏面,runtime就過濾掉了這些可能性。 這就是爲什麼dynamic langauge更加強壯。 因爲編譯器和runtime環境開發人員已經幫你處理了這些問題。

好了上面說着這麼多,我們再返回來看Objective-C. 現在你是不是能理解這樣的語句了呢?

  1. id obj=self;

  2. if ([obj respondsToSelector:@selector(function1:)) {

  3. }

  4. if ([obj isKindOfClass:[NSArray class]] ) {

  5. }


  6. if ([obj conformsToProtocol:@protocol(myProtocol)]) {

  7. }


  8. if ([[obj class] isSubclassOfClass:[NSArray class]]) {

  9. }

  10. [obj someNonExistFunction];

看似很簡單的語句,但是爲了讓語言實現這個能力,語言開發者要付出很多努力實現runtime環境。這裏運行時環境處理了弱類型、函數存在檢查工 作。runtime會檢測註冊列表裏是否存在對應的函數,類型是否正確,最後確定下來正確的函數地址,再進行保存寄存器狀態,壓棧,函數調用等等實際的操 作。

  1. id knife=[Knife grateKnife];

  2. NSArray *monsterList=[NSArray array];

  3. [monsterList makeObjectsPerformSelector:@selector(killMonster:) withObject:knife];

在c,c++年代去完成這個功能是非常麻煩的,但是動態語言卻非常簡單。

關於執行效率問題。 “靜態語言執行效率要比動態語言高”,這句沒錯。因爲一部分cpu計算損耗在了runtime過程中。而靜態語言生成的機器指令更簡潔。正因爲知道這個原 因,所以開發語言的人付出很大一部分努力爲了保持runtime小巧上。所以objecitve-c是c的超集+一個小巧的runtime環境。 但是,換句話說,從算法角度考慮,這點複雜度不算差別的,Big O notation結果不會有差別。( It's not log(n) vs n^2 )

簡單理解:“Runtime is everything between your each function call.”

Runtime好比objective-c的靈魂。很多東西都是在這個基礎上出現的。所以它是指的你花功夫去理解的。

3. thread

"thread synchronization another notorious trouble!"

記不記得上學時候學得操作系統這門課,裏面都會有專門一章介紹任務調度和生產者消費者的問題。 這就是爲了今後使用進程、線程開發打基礎。概念很簡單,但是心知肚明的人很少。難點在synchronization(同步),因爲1. There is no 100% deadlock detection algorithm. If there is, no deadlock at all. 2. 往往這類錯誤很隱晦,靜態分析很難找到。 3. 抽象度較高需要經驗去把握。

總體來說,我見到的在這方面的問題可以分爲一下幾點:

1. 不知道多線程開發的幾個基點,看別人代碼越看越糊塗的。一會NSThread、一會*****、block等等。。。Apple封裝了很多線程的api, down to core多線程的結構基本是

\

可以看到在多線程開發中你可以選擇這幾種不同的方式。Mach是最和心的操作系統部分,你可以用但是沒必要,太累。

pthread靈活、輕巧,但是需要理論基礎還是開發複雜,最主要的POSIX開的線程不能使用cocoa根據apple文檔只在pthread下 使用cocoa需要先detach at least one NSThread object. 這樣確定[NSThread isMultiThreaded]纔可以使用。

NSThread是Mac OS 10.0後發佈的多線程API較爲高層,但是缺乏靈活性。

Grand Central Dispatch 10.6引入的開源多線程庫, *****介於pthread和NSThread之間。比NSThread更靈活,小巧但有不需要像pthread一樣考慮很多lock的問題。而 objective-c 2.0發佈的新語法特性之一blocks也正是根據這種多線程需求推出的。

在你寫多線程代碼或者閱讀多線程代碼時候,心理先明確了這是那種。

2. thread和runloop造成的問題

其實thread和runloop放在以前開發者根本不太當成一個問題。因爲沒有runtime能力,runloop就是固定的線程執行loop。 而現在cocoa開發新手搞不明白的太多了。 NSRunloop和NSThread啥關係?由於這個問題比較多,我單獨列到第4點裏講解把。

3. thread和Reference Counting內存管理造成的問題。

引用

線程裏面的方法都要放到NSAutoreleasePool裏面嗎

這類問題很常見,主要原因是 NSAutoreleasePool 到底是幹什麼用得不明白。 NSAutoreleasePool跟thread其實關係並不顯著,它提供一個臨時內存管理空間,好比一個沙箱,確保不會有不當的內存分配泄露出來,在 這個空間內新分配的對象要向這個pool做一***冊告訴:“pool,我新分配一塊空間了”。當pool drain掉或者release,它裏面分配過的內存同樣釋放掉。可見和thread沒有很大關係。但是,我們閱讀代碼的時候經常會看到,新開線程的函數 內總是以NSAutoreleasePool開始結束。這又是爲什麼呢!? 因爲thread內恰好是最適合需要它的地方! 線程函數應該計算量大,時間長(supposed to be heavy)。在線程裏面可能會有大量對象生成,這時使用autoreleasepool管理更簡潔。所以這裏的答案是,不一定非要在線程裏放 NSAutoreleasePool,相對的在cocoa環境下任意地方都可以使用NSAutoreleasePool。如果你在線程內不使用 NSAutoreleasePool,要記得在內部alloc和relase配對出現保證沒有內存泄露。

這裏還有一個值得提出的是autorelease. NSObject爲何會有autorelease這個方法? 它是根據什麼auto的?

4. mainthread和secondary thread疑惑

引用

NSThread的detachNewThreadSelector和self的performSelectorOnMainThread方法有什麼不同

5. Asynchronous(異步) vs. Synchronous(同步)

引用

我在一個view要顯示多張web圖片,我想問一下,我是應該採用異步一個一個下載的方式,還是應該採用多線程同時下載的方式,還是2個都用,那種方式好呢?

大家可以看一下這個問題。這句有一點在我看來是非常奇怪的,因爲我覺得問問題的人並不理解同步異步是什麼意思。"一個一個下載的方式"是同步的行爲,“多線程同時下載”是異步的行爲。 都搞混了把!

4. runloop

現在說說runloop爲何會成爲cocoa開發中迷惑的點。因爲很多新手沒有從動態角度看它。 首先回想一下第2點介紹的runtime的概念。 接着我出一個題思考一下。

現在我有一個程序片段如下:

  1. - (void)myThread:(id)sender

  2. {

  3. NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];

  4. while (TRUE) {

  5.  

  6. //do some jobs

  7. //break in some condition

  8.  

  9. usleep(10000);

  10.  

  11. [pool drain];

  12. }

  13.  

  14. [pool release];

  15. }

現在要求,做某些設計,使得當這個線程運行的同時,還可以從其它線程裏往它裏面隨意增加或去掉不同的計算任務。 這,就是NSRunloop的最原始的開發初衷。讓一個線程的計算任務更加靈活。 這個功能在c, c++裏也許可以做到但是非常難,最主要的是因爲語言能力的限制,以前的程序員很少這麼去思考。

好,現在我們對上面代碼做一個非常簡單的進化:

  1. - (void)myThread:(id)sender

  2. {

  3. NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];

  4. while (TRUE) {

  5.  

  6. //do some jobs

  7. //break in some condition

  8.  

  9. usleep(10000);

  10.  

  11. [pool drain];

  12. }

  13.  

  14. [pool release];

  15. }

現在要求,做某些設計,使得當這個線程運行的同時,還可以從其它線程裏往它裏面隨意增加或去掉不同的計算任務。 這,就是NSRunloop的最原始的開發初衷。讓一個線程的計算任務更加靈活。 這個功能在c, c++裏也許可以做到但是非常難,最主要的是因爲語言能力的限制,以前的程序員很少這麼去思考。

好,現在我們對上面代碼做一個非常簡單的進化:

  1. NSMutableArray *targetQueue;

  2. NSMutableArray *actionQueue;

  3.  

  4. - (void)myThread:(id)sender

  5. {

  6. NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];

  7. while (TRUE) {

  8.  

  9. //do some jobs

  10. //break in some condition

  11. int n=[targetQueue count];

  12. assert(n==[actionQueue count]);

  13. for(int i=0;i<n;i++){

  14. id target=[targetQueue objectAtIndex:i];

  15. SEL action=NSSelectorFromString([actionQueue objectAtIndex:i]);

  16. if ([target respondsToSelector:action]) {

  17. [target performSelector:action withObject:nil];

  18. }

  19. }

  20.  

  21. usleep(10000);

  22.  

  23. [pool drain];

  24. }

  25.  

  26. [pool release];

  27. }

注意,這裏沒有做線程安全處理,記住Mutable container is not thread safe.

這個簡單的擴展,讓我們看到了如何利用runtime能力讓線程靈活起來。當我們從另外線程向targetQueue和actionQueue同時加入對象和方法時候,這個線程函數就有了執行一個額外代碼的能力。

但,有人會問,哪裏有runloop? 那個是 nsrunloop? 看不出來啊。

  1. while (TRUE) {

  2. //break in some condition

  3. }

一個線程內這個結構就叫線程的runloop, 它和NSRunloop這個類雖然名字很像,但完全不是一個東西。以前在使用靜態語言開始時候,程序員沒有什麼迷惑,因爲沒有NSRunloop這個東西。 我接着來說,這個NSRunloop是如何來得。

第二段擴展代碼裏面確實沒有NSRunloop這個玩意兒,我們接着做第3次改進。 這次我們的目前是把哪個動態部分抽象出來。

5. delegate, protocol

這個會列出來因爲,我感覺問它的數量僅此於內存管理部分,它們用得很頻繁,並且它們是多鍾設計模式的重要組成部分。

6. responder chain

7. Memory Reference Counting(RC) & Automatic Reference Counting(ARC)

這個也許是問得最多的問題了吧。所有這些問題往往來源於3個地方,1、不瞭解底層機制;2、沒有吃透規則; 3、不瞭解常用container的Reference Counting特性,或着說沒有下功夫去看對應文檔。

1. 底層機制

大家是否知道從舊時代的RC到ARC機制到底意味着什麼呢? 爲什麼ARC從開發速度,到執行速度和穩定性都要優於rc?

開發速度不言而喻,你少寫很多release代碼,甚至很少去操心這部分。

執行速度呢?這個還要從runtime說起,還記得我在第2點說得一句話麼:“Runtime is everything between your each function call.”

RC是一個古老的內存管理哲學,誰分配誰釋放。通過counting來計數到底該資源有幾個使用者。道理很簡單,但是往往簡單的東西人卻會犯錯。從 來沒有一個程序員可以充滿信心的說,我寫得代碼從來沒有過內存泄露。這樣來看,我們就更需要讓程序可以自己處理這個管理機制,這就需要把這個機制放到 runtime裏。

所以RC->ARC就是把內存管理部分從普通開發者的函數中移到了函數外的runtime中。因爲runtime的開發原型簡單,邏輯層次更 高,所以做這個開發和管理出錯的概率更小。實際上編譯器開發人員對這部分經過無數次測試,所以可以說用arc幾乎不會出錯。另外由於編譯的額外優化,使得 這個部分比程序員自己寫得代碼要快速很多。而且對於一些通用的開發模式,例如autorelease對象,arc有更優秀的算法保證 autoreleasepool裏的對象更少。

2. RC規則

首先說一下rc是什麼,r-Reference參照,引用 c-counting計數, rc就是引用計數。俗話說就是記錄使用者的數量。 例如現在我有一個房間空着,大家可以進去隨意使用,但是你進門前,需要給門口的計數牌子+1, 出門時候-1。 這時候這個門口的牌子就是該房間裏的人數。一但這個牌子變爲0我就可以把房間關閉。

這個規則可以讓NSObject決定是不是要釋放內存。當一個對象alloc時候,系統分配其一塊內存並且object自動計數 retainCount=1 這時候每當[object retain]一次retainCount+1(這裏雖然簡寫也是rc不過是巧合或者當時開發人員故意選的retain這個詞吧)每次[object release]時候retainCount-1 當retainCount==0時候object就真正把這快內存還給系統。

3. 常用container的Reference Counting特性

這個規則很簡單把。但是這塊確實讓新手最頭疼的地方。 問題出在,新手總想去驗證rc規則,又總是發現和自己的期望不符合。

無數次看到有人寫下如下句子

  1. NSLog(@"%d",[object retainCount]);

  1. while([object retainCount]>0){

  2. [object release];

  3. }

當然了,我也做過類似的動作,那種希望一切盡在掌握中的心態。但是你會看到其他人告訴這麼做完全沒有意義。rc does not work this way. 也許這樣的暴力釋放會起作用,但是retainCount並不是用來做這個的。每個數字意味着有其它對象引用該資源,這樣的暴力釋放很容易導致程序崩潰。 這個數字也許並不是你心目中的哪個。因爲你很難跟蹤到底哪些對象引用的該資源。你用代碼建立的資源不光只有你的代碼纔會用到,你調用的各種 Framework,Framework調用的Framework,都有可能改變這個資源的retainCount. 所以去驗證rc規則不是明智之舉。

你能做的就是理解規則,使用規則,讀文檔瞭解container的引用特性。或者乾脆移到arc上面,讓runtime環境處理這些問題。

最後說一下不用arc的情況。目前情況來看,有不少第三方的庫並未支持arc,所以如果你的舊項目使用了這些庫,請檢查是否作者發佈了新版本,或者你需要自己修正支持arc。

8. class heritage

9. English

10. Just trying to be smart

其實剩下這個有好幾點要說,但綜合一下把。思路有些相似

例如剛看到這個問題:

引用

現在有A *a;A*b

[NSMutableArray addObject : a];

[NSMutableArray replaceObjectAtIndex:0 withObject:b]

執行完這兩個之後,拿可變數組裏面的0 的位置 就是b元素了,那這個時候a到哪裏去了??是否還佔用着內存,如果佔用內存的話,又如何去釋放??

It's kind of silly. 我並不是想諷刺問問題的朋友。其實如果你真的瞭解了上面這些知識點,就不會再問這種問題的。 爲什麼不多思考一層呢,在問這個問題之前想想,到底爲什麼會問出這個問題? ”如果讓你給NSMutableArray實現一個replaceObjectAtIndex函數你會怎麼寫?“ 難道連個[obj release]都考慮不到麼?然後根據ARC,它到底釋放了沒不言自明瞭把。

其實這種問題論壇裏很多的。不妨在迷惑的時候,先問問自己爲什麼會迷惑。

(1)這裏其實很有意思,爲何我用“更高層次思考”,而不是“更底層次”。作爲一個編譯器和語言開發人員,面對的問題確實更底層沒錯,但是他們思考的維度更高,更抽象,這樣子。一個不算恰當的比方就好像一個三維世界的人處理二維世界的一條線的問題。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章