iOS 開發知識小集(1)

發表於2015-05-13

一直想做這樣一個小冊子,來記錄自己平時開發、閱讀部落格、看書、程式碼分析和與人交流中遇到的各種問題。之前有過這樣的嘗試,但都是無疾而終。不過,每天接觸的東西多,有些東西不記下來,忘得也是很快,第二次遇到同樣的問題時,還得再查一遍。好記性不如爛筆頭,所以又決定重拾此事,時不時回頭看看,溫故而知新。

這裡面的每個問題,不會太長。或是讀書筆記,或是摘抄,亦或是驗證,每個問題的篇幅爭取在六七百字的樣子。筆記和摘抄的出處會詳細標明。問題的個數不限,湊齊3500字左右就發一篇。爭取每月至少發兩篇吧,權當是對自己學習的一個整理。

本期主要記錄了以下幾個問題:

  1. NSString屬性什麼時候用copy,什麼時候用strong?
  2. Foundation中的斷言處理
  3. IBOutletCollection
  4. NSRecursiveLock遞迴鎖的使用
  5. NSHashTable

NSString屬性什麼時候用copy,什麼時候用strong?

我們在宣告一個NSString屬性時,對於其記憶體相關特性,通常有兩種選擇(基於ARC環境):strong與copy。那這兩者有什麼區別呢?什麼時候該用strong,什麼時候該用copy呢?讓我們先來看個例子。

示例

我們定義一個類,併為其宣告兩個字串屬性,如下所示:

上面的程式碼宣告瞭兩個字串屬性,其中一個記憶體特性是strong,一個是copy。下面我們來看看它們的區別。

首先,我們用一個不可變字串來為這兩個屬性賦值,

其輸出結果是:

我們要以看到,這種情況下,不管是strong還是copy屬性的物件,其指向的地址都是同一個,即為string指向的地址。如果我們換作MRC環境,列印string的引用計數的話,會看到其引用計數值是3,即strong操作和copy操作都使原字串物件的引用計數值加了1。

接下來,我們把string由不可變改為可變物件,看看會是什麼結果。即將下面這一句

改成:

其輸出結果是:

可以發現,此時copy屬性字串已不再指向string字串物件,而是深拷貝了string字串,並讓_copyedString物件指向這個字串。在MRC環境下,列印兩者的引用計數,可以看到string物件的引用計數是2,而_copyedString物件的引用計數是1。

此時,我們如果去修改string字串的話,可以看到:因為_strongString與string是指向同一物件,所以_strongString的值也會跟隨著改變(需要注意的是,此時_strongString的型別實際上是NSMutableString,而不是NSString);而_copyedString是指向另一個物件的,所以並不會改變。

結論

由於NSMutableString是NSString的子類,所以一個NSString指標可以指向NSMutableString物件,讓我們的strongString指標指向一個可變字串是OK的。

而上面的例子可以看出,當源字串是NSString時,由於字串是不可變的,所以,不管是strong還是copy屬性的物件,都是指向源物件,copy操作只是做了次淺拷貝

當源字串是NSMutableString時,strong屬性只是增加了源字串的引用計數,而copy屬性則是對源字串做了次深拷貝,產生一個新的物件,且copy屬性物件指向這個新的物件。另外需要注意的是,這個copy屬性物件的型別始終是NSString,而不是NSMutableString,因此其是不可變的。

這裡還有一個效能問題,即在源字串是NSMutableString,strong是單純的增加物件的引用計數,而copy操作是執行了一次深拷貝,所以效能上會有所差異。而如果源字串是NSString時,則沒有這個問題。

所以,在宣告NSString屬性時,到底是選擇strong還是copy,可以根據實際情況來定。不過,一般我們將物件宣告為NSString時,都不希望它改變,所以大多數情況下,我們建議用copy,以免因可變字串的修改導致的一些非預期問題。

關於字串的記憶體管理,還有些有意思的東西,可以參考NSString特性分析學習

參考

  1. NSString copy not copying?
  2. NSString特性分析學習
  3. NSString什麼時候用copy,什麼時候用strong

Foundation中的斷言處理

經常在看一些第三方庫的程式碼時,或者自己在寫一些基礎類時,都會用到斷言。所以在此總結一下Objective-C中關於斷言的一些問題。

Foundation中定義了兩組斷言相關的巨集,分別是:

這兩組巨集主要在功能和語義上有所差別,這些區別主要有以下兩點:

  1. 如果我們需要確保方法或函式的輸入引數的正確性,則應該在方法(函式)的頂部使用NSParameterAssert / NSCParameterAssert;而在其它情況下,使用NSAssert / NSCAssert。
  2. 另一個不同是介於C和Objective-C之間。NSAssert / NSParameterAssert應該用於Objective-C的上下文(方法)中,而NSCAssert / NSCParameterAssert應該用於C的上下文(函式)中。

當斷言失敗時,通常是會丟擲一個如下所示的異常:

Foundation為了處理斷言,專門定義了一個NSAssertionHandler來處理斷言的失敗情況。NSAssertionHandler物件是自動建立的,用於處理失敗的斷言。當斷言失敗時,會傳遞一個字串給NSAssertionHandler物件來描述失敗的原因。每個執行緒都有自己的NSAssertionHandler物件。當呼叫時,一個斷言處理器會列印包含方法和類(或函式)的錯誤訊息,並引發一個NSInternalInconsistencyException異常。就像上面所看到的一樣。

我們很少直接去呼叫NSAssertionHandler的斷言處理方法,通常都是自動呼叫的。

NSAssertionHandler提供的方法並不多,就三個,如下所示:

另外,還定義了一個常量字串,

主要是用於線上程的threadDictionary字典中獲取或設定斷言處理器。

關於斷言,還需要注意的一點是在Xcode 4.2以後,在release版本中斷言是預設關閉的,這是由巨集NS_BLOCK_ASSERTIONS來處理的。也就是說,當編譯release版本時,所有的斷言呼叫都是無效的。

我們可以自定義一個繼承自NSAssertionHandler的斷言處理類,來實現一些我們自己的需求。如Mattt Thompson的NSAssertion​Handler例項一樣:

上面說過,每個執行緒都有自己的斷言處理器。我們可以通過為執行緒的threadDictionary字典中的NSAssertionHandlerKey指定一個新值,來改變執行緒的斷言處理器。

如下程式碼所示:

而什麼時候應該使用斷言呢?通常我們期望程式按照我們的預期去執行時,如呼叫的引數為空時流程就無法繼續下去時,可以使用斷言。但另一方面,我們也需要考慮,在這加斷言確實是需要的麼?我們是否可以通過更多的容錯處理來使程式正常執行呢?

Mattt Thompson在NSAssertion​Handler中的倒數第二段說得挺有意思,在此摘抄一下:

參考

  1. NSAssertion​Handler
  2. NSAssertionHandler Class Reference

IBOutletCollection

在IB與相關檔案做連線時,我們經常會用到兩個關鍵字:IBOutlet和IBAction。經常用xib或storyboard的童鞋應該用這兩上關鍵字非常熟悉了。不過UIKit還提供了另一個偽關鍵字IBOutletCollection,我們使用這個關鍵字,可以將介面上一組相同的控制元件連線到同一個陣列中。

我們先來看看這個偽關鍵字的定義,可以從UIKit.framework的標頭檔案UINibDeclarations.h找到如下定義:

另外,在Clang原始碼中,有更安全的定義方式,如下所示:

從上面的定義可以看到,與IBOutlet不同的是,IBOutletCollection帶有一個引數,該引數是一個類名。

通常情況下,我們使用一個IBOutletCollection屬性時,屬性必須是strong的,且型別是NSArray,如下所示:

假定我們的xib檔案中有三個橫向的scrollView,我們便可以將這三個scrollView都連線至scrollViews屬性,然後在我們的程式碼中便可以做一些統一處理,如下所示:

這段程式碼會影響到三個scrollView。這樣做的好處是我們不需要手動通過addObject:方法將scrollView新增到scrollViews中。

不過在使用IBOutletCollection時,需要注意兩點:

  1. IBOutletCollection集合中物件的順序是不確定的。我們通過除錯方法可以看到集合中物件的順序跟我們連線的順序是一樣的。但是這個順序可能會因為不同版本的Xcode而有所不同。所以我們不應該試圖在程式碼中去假定這種順序。
  2. 不管IBOutletCollection(ClassName)中的控制元件是什麼,屬性的型別始終是NSArray。實際上,我們可以宣告是任何型別,如NSSet,NSMutableArray,甚至可以是UIColor,但不管我們在此設定的是什麼類,IBOutletCollection屬性總是指向一個NSArray陣列。

關於第二點,我們以上面的scrollViews為例,作如下修改:

實際上我們在控制檯列印這個scrollViews時,結果如下所示:

可以看到,它指向的是一個NSArray陣列。

另外,IBOutletCollection實際上在iOS 4版本中就有了。不過,現在的Objective-C已經支援object literals了,所以定義陣列可以直接用@[],方便了許多。而且object literals方式可以新增不在xib中的用程式碼定義的檢視,所以顯得更加靈活。當然,兩種方式選擇哪一種,就看我們自己的實際需要和喜好了。

參考

  1. IBAction / IBOutlet / IBOutlet​Collection
  2. IBOutletCollection.m

NSRecursiveLock遞迴鎖的使用

NSRecursiveLock實際上定義的是一個遞迴鎖,這個鎖可以被同一執行緒多次請求,而不會引起死鎖。這主要是用在迴圈或遞迴操作中。我們先來看一個示例:

這段程式碼是一個典型的死鎖情況。在我們的執行緒中,RecursiveMethod是遞迴呼叫的。所以每次進入這個block時,都會去加一次鎖,而從第二次開始,由於鎖已經被使用了且沒有解鎖,所以它需要等待鎖被解除,這樣就導致了死鎖,執行緒被阻塞住了。偵錯程式中會輸出如下資訊:

在這種情況下,我們就可以使用NSRecursiveLock。它可以允許同一執行緒多次加鎖,而不會造成死鎖。遞迴鎖會跟蹤它被lock的次數。每次成功的lock都必須平衡呼叫unlock操作。只有所有達到這種平衡,鎖最後才能被釋放,以供其它執行緒使用。

所以,對上面的程式碼進行一下改造,

這樣,程式就能正常執行了,其輸出如下所示:

NSRecursiveLock除了實現NSLocking協議的方法外,還提供了兩個方法,分別如下:

這兩個方法都可以用於在多執行緒的情況下,去嘗試請求一個遞迴鎖,然後根據返回的布林值,來做相應的處理。如下程式碼所示:

在前面的程式碼中,我們又新增了一段程式碼,增加一個執行緒來獲取遞迴鎖。我們在第二個執行緒中嘗試去獲取遞迴鎖,當然這種情況下是會失敗的,輸出結果如下:

另外,NSRecursiveLock還宣告瞭一個name屬性,如下:

我們可以使用這個字串來標識一個鎖。Cocoa也會使用這個name作為錯誤描述資訊的一部分。

參考

  1. NSRecursiveLock Class Reference
  2. Objective-C中不同方式實現鎖(二)

NSHashTable

在看KVOController的程式碼時,又看到了NSHashTable這個類,所以就此整理一下。

NSHashTable效仿了NSSet(NSMutableSet),但提供了比NSSet更多的操作選項,尤其是在對弱引用關係的支援上,NSHashTable在物件/記憶體處理時更加的靈活。相較於NSSet,NSHashTable具有以下特性:

  1. NSSet(NSMutableSet)持有其元素的強引用,同時這些元素是使用hash值及isEqual:方法來做hash檢測及判斷是否相等的。
  2. NSHashTable是可變的,它沒有不可變版本。
  3. 它可以持有元素的弱引用,而且在物件被銷燬後能正確地將其移除。而這一點在NSSet是做不到的。
  4. 它的成員可以在新增時被拷貝。
  5. 它的成員可以使用指標來標識是否相等及做hash檢測。
  6. 它可以包含任意指標,其成員沒有限制為物件。我們可以配置一個NSHashTable例項來操作任意的指標,而不僅僅是物件。

初始化NSHashTable時,我們可以設定一個初始選項,這個選項確定了這個NSHashTable物件後面所有的行為。這個選項是由NSHashTableOptions列舉來定義的,如下所示:

當然,我們還可以使用NSPointerFunctions來初始化,但只有使用NSHashTableOptions定義的這些值,才能確保NSHashTable的各個API可以正確的工作—包括拷貝、歸檔及快速列舉。

個人認為NSHashTable吸引人的地方在於可以持有元素的弱引用,而且在物件被銷燬後能正確地將其移除。我們來寫個示例:

這段程式碼的輸出結果如下:

可以看到,在離開testWeakMemory方法,obj物件被釋放,同時物件在集合中的引用也被安全的刪除。

這樣看來,NSHashTable似乎比NSSet(NSMutableSet)要好啊。那是不是我們就應用都使用NSHashTable呢?Peter Steinberger在The Foundation Collection Classes給了我們一組資料,顯示在新增物件的操作中,NSHashTable所有的時間差不多是NSMutableSet的2倍,而在其它操作中,效能大體相近。所以,如果我們只需要NSSet的特性,就儘量用NSSet。

另外,Mattt Thompson在NSHash​Table & NSMap​Table的結尾也寫了段挺有意思的話,在此直接摘抄過來:

參考

  1. NSHashTable Class Reference
  2. NSHash​Table & NSMap​Table
  3. NSHashTable & NSMapTable
  4. The Foundation Collection Classes

零碎

(一) “Unknown class XXViewController in Interface Builder file.”“ 問題處理

最近在靜態庫中寫了一個XXViewController類,然後在主工程的xib中,將xib的類指定為XXViewController,程式執行時,報瞭如下錯誤:

之前也遇到這個問題,但已記得不太清楚,所以又開始在stackoverflow上找答案。

其實這個問題與Interface Builder無關,最直接的原因還是相關的symbol沒有從靜態庫中載入進來。這種問題的處理就是在Target的”Build Setting”–>“Other Link Flags”中加上”-all_load -ObjC”這兩個標識位,這樣就OK了。

(二)關於Unbalanced calls to begin/end appearance transitions for …問題的處理

我們的某個業務有這麼一個需求,進入一個列表後需要立馬又push一個web頁面,做一些活動的推廣。在iOS 8上,我們的實現是一切OK的;但到了iOS 7上,就發現這個web頁面push不出來了,同時控制檯給了一條警告訊息,即如下:

在這種情況下,點選導航欄中的返回按鈕時,直接顯示一個黑屏。

我們到stackoverflow上查了一下,有這麼一段提示:

意思是說在當前檢視控制器完成顯示之前,又試圖去顯示一個新的檢視控制器。

於是我們去排查程式碼,果然發現,在viewDidLoad裡面去做了次網路請求操作,且請求返回後就去push這個web活動推廣頁。此時,當前的檢視控制器可能並未顯示完成(即未完成push操作)。

當幾乎同時將兩個檢視控制器push到當前的導航控制器棧中時,或者同時pop兩個不同的檢視控制器,就會出現不確定的結果。所以我們應該確保同一時間,對同一個導航控制器棧只有一個操作,即便當前的檢視控制器正在動畫過程中,也不應該再去push或pop一個新的檢視控制器。

所以最後我們把web活動的資料請求放到了viewDidAppear裡面,並做了些處理,這樣問題就解決了。

參考

  1. “Unbalanced calls to begin/end appearance transitions for DetailViewController” when pushing more than one detail view controller
  2. Unbalanced calls to begin/end appearance transitions for UITabBarController

相關文章