編寫高質量iOS與OS X代碼的有效方法

oc語言特性

oc使用動態綁定的消息結構,在運行時才會檢查對象類型。接收消息后,執行代碼,由運行環境而非編譯器來決定。

面向對象語言,"對象"就是"基本構造單元",開發者通過對象來存儲并傳遞數據。在對象之間傳遞數據并執行任務的過程就叫做"消息傳遞"。

使用消息結構的語言,其運行時所執行的代碼由運行環境來決定;而使用函數調用的語言,由編譯器決定。如果調用的函數是多態的,那么在運行時就按照“虛方法表”來查出到底應該執行哪個函數實現。而采用消息結構的語言,不論是否多態,總是在運行時才會去查找所要執行的方法。實際上,編譯器甚至不關心接收消息的對象是何種類型。接收消息的對象問題也要在運行時處理,甚至過程叫做“動態綁定”。

方法

1. 在類的頭文件中盡量少引入其他頭文件

  • 在編譯使用PersonTest類文件時,不需要知道PersonTest類的全部細節,只需知道類名就好,使用向前聲明(@class PersonTest;)就行。
  • 除非確有必要,否則不要引入頭文件。一般應在某個類的頭文件中使用向前聲明(@class 類名)來提及別的類,并在實現文件中引用那些類的頭文件。這樣做盡量降低類之間的耦合。
  • 有時無法使用向前聲明,比如要聲明某個類遵循一項協議。這種情況,盡量吧“該類遵循某協議”的這條聲明移至“calss-continuation分類”中。若不行,就把協議單獨放在一個頭文件中,然后將其引入。

2. 多用字面量語法,少用與之等價的方法

  • 使用字面量語法創建字符串、數值、數組、字典,與常規方法比更簡明扼要
  • 應該通過取小標操作來訪問數組下標或字典中鍵所對應的元素
  • 用字面量語法創建數組或字典時,若值中有nil,則會拋出異常,終止程序,其語法更安全
  • 局限性:除字符串外,所創建的對象必須屬于Foundation框架才行
    NSNumber *number = [NSNumber numberWithInt:1];
    NSNumber *num2 = @1;
    NSNumber *booln = @YES;
    NSNumber *cn = @'a';
    NSArray *ary = [NSArray arrayWithObjects:@"cat",@"tom",@"mouse", nil];
    NSArray *ary1 = @[@"cat",@"tom"];
    NSString *dog = [ary objectAtIndex:1];
    NSString *dog1 = ary[1];
    NSDictionary * pd = [NSDictionary dictionaryWithObjectsAndKeys:@"Tom",@"name",[NSNumber numberWithInt:26],@"age", nil];
    NSDictionary *pd1 = @{@"name":@"Tom",
                          @"age":@26};

3. 多用類型常量,少用 #define 預處理指令

  • 不要用預處理指令定義常量。這樣定義出來的常量不含類性信息,編譯器只會在編譯前據此執行查找與替換操作。即使有人重新定義了常量值,編譯器也不會產生警告
  • 在實現文件中使用static const 來定義"只在編譯單元內可見的常量"。由于此類常量不會在全局符號表中,所以無需為其名稱加前綴
  • 在頭文件中使用extern 來聲明全局常量,并在相關實現文件中定義其值。這種常量要出現在全局符號表中,所以其名稱應加以區隔,通常用與之相關的類名做前綴
    #define ANIMATION_DURATION 0.3
    static const NSTimeInterval kAnimationDuration = 0.3;
    extern const NSTimeInterval TestAnimationDuration;
    const NSTimeInterval TestAnimationDuration = 0.3;

4. 用枚舉表示狀態、選項、狀態碼

  • 應該使用枚舉來表示狀態機的狀態、傳遞給方法的選項以及狀態碼等值
  • 如果把傳遞給某個方法的選項表示為枚舉類型,而多個選項又可同時使用,那么就將個選項值定義為2的冪,以便通過按位或操作將其組合起來
  • 用NS_ENUM與NS_OPTIONS宏來定義枚舉,并指明其底層數據類型。這樣做可以確保枚舉是用開發者所選的底層數據類型實現的,而不會采用編譯器所選的類型
  • 在處理枚舉類型的switch語句中不要實現default分支。這樣的話,加入新枚舉之后,編譯器就會提示開發者:switch語句并未處理所有枚舉

對象、消息、運行期

5. 理解"屬性"這一概念

"屬性"用于封裝對象中的數據。對象通常會把其所需要的數據保存為各種實例變量。實例變量一般通過"存取方法"來訪問。其中,"獲取方法"(getter)用于讀取變量值,而"設置方法"(setter)用于寫入變量值.開發者可令編譯器自動編寫與屬性相關的存取方法。此特性引入了一種新的“點語法”,使開發者可以更容易的依照類對象來訪問其中的數據。

存取方法有著嚴格的命名規范,所以OC語言才能根據名稱自動創建存取方法,@property語法等同與寫一套存取方法,@property NSString*name就是編譯器自動寫出一套存取方法。

若不想令編譯器自動合成存取方法,則可以自己實現,如果你只實現了其中一個存取方法,那么另一個還是由編譯器來合成。使用@dynamic關鍵字,可以阻止編譯器自動合成存取方法。

屬性特質: 原子性、讀/寫權限、內存管理語義、方法名

@property(nonatomic,readwrite,copy) NSString *firstName
@property(nonatomic,readwrite,copy,getter=isOn) BOOL  on
  • 原子性
    默認情況下,由編譯器所合成的方法會通過鎖定機制確保其原子性。
    atomic與nonatomic區別?

  • 具備atomic特質的獲取方法會通過鎖定機制來確保其操作的原子性,就是說,如果兩個線程讀寫同一屬性,那么不論何時,總能看到有效的屬性值。若是不加鎖的話(或者使用nonatomic語義),那么當其中一個線程在改寫某屬性值時,另外一個線程也許會突然闖入,把尚未修改好的屬性值讀取出來。發生這種情況時,線程讀到的屬性值可能不對。

  • 使用nonatomic歷史原因:在iOS中使用同步鎖的開銷較大,會帶來性能問題。一般情況下并不要求屬性必須是“原子的”,因為這樣并不能保證“線程安全”,若要實現“線程安全”的操作,還需采用更深層次的鎖定機制。如一個線程在連續多次讀取屬性值的過程中有別的線程在同時修改該值,即使用atomic,也還是會讀到不同的屬性值。

  • 讀寫權限

  • readwrite(讀寫)特質屬性擁有"獲取方法"與"設置方法";

  • readonly(只讀)特質屬性只擁有"獲取方法";

  • 內存管理語義

  • assign "設置方法"只會執行針對"純量類型"(如CGFloat、NSInteger等)的簡單賦值操作

  • strong 此特質表明該屬性定義了一種"擁有關系"。為這種屬性設置新值時,設置方法會先保留新值,并釋放舊值,然后將新值設置上去

  • weak 此特質表明該屬性定義了一種"非擁有關系"。 為這種屬性設置新值時,設置方法既不保留新值,也不釋放舊值。此特質同assign類似,然而在屬性所指的對象遭到摧毀時,屬性也會清空

  • unsafe_undertained 此特質的語義和assign相同,但是它適合用于"對象類型",該特質表達一種"非擁有關系",當目標對象遭到摧毀時,屬性值不會自動清空,這一點與weak區別

  • copy 此特質所表達的所屬關系與strong類似。然而設置方法并不保留新值,而是將其"拷貝"。

  • 方法名

  • getter =<name>

  • setter=<name>

  • 多使用nonatomic屬性,因為atomic屬性會影響性能

  • 通過"特質"來指定存儲數據所需的正確語義

6. 在對象內部盡量直接訪問實例變量

不經過OC的"方法派送"步驟,所以直接訪問實例變量的速度比較快,此情況下,編譯器所生成的代碼會直接訪問保存對象實例變量的那塊內存。
直接訪問實例變量不會觸發“鍵值觀測”通知;
直接訪問實例變量有助于排查與之相關的錯誤,加“斷點”,監控該屬性的調用者及訪問時機;

直接訪問實例變量不會調用其"設置方法",繞過了相關屬性所定義的"內存管理語義"。

  • 在對象內部讀取數據時,應該直接通過實例變量來讀,而寫入數據時,則通過那個屬性來寫
  • 在初始化方法及dealloc方法中,總是應該直接通過實例變量來讀寫數據
  • 有時會使用惰性初始化技術配置某份數據,此情況,需要通過屬性來讀取數據

7. 理解"對象等同性"概念

  • 若檢測對象的等同性,提供"isEqual:"與"hash"方法
  • 相同對象具有相同的哈希碼,但是兩個哈希碼相同的對象卻未必相同
  • 不要盲目的逐個檢測每條屬性,而是應該依照具體需求來制定檢測方案
  • 編寫hash方法時,應該使用計算速度快而且哈希碼碰撞幾率低的算法

8. 以"類族模式"隱藏實現細節

  • 類族模式可以把實現細節隱藏在一套簡單的公共接口后面
  • 系統框架常使用類族
  • 從類族的公共抽象類中繼承子類要當心
* 子類應該繼承自類族中的抽象基類
* 子類應該定義自己的數據存儲方式
* 子類應當覆寫超類文檔中指明需要覆寫的方法

9. 在既有類中使用關聯對象存放自定義數據

有時候類的實例可能是有某種機制所創建的,而開發者無法令這種機制創建出自己所寫的字類實例,這時候就需要關聯對象解決問題。

* void objc_setAssociatedObject(id object,void *key,id value,objc_AssociationPolicy policy)
此方法以給定的鍵和策略為某對象設置關聯對象值
* id objc_getAssociatedObject(id object,void *key)
此方法根據給定的鍵從某對象中獲取相應的關聯對象值
* void objc_removeAssociatedObject(id object)
此方法移除指定對象的全部關聯對象
例子將創建警告視圖與處理操作結果的代碼放在一起:
#import <objc/runtime.h>
static void *EOCMyAlertViewKey = "ECOMyAlertViewKey";
- (void)askUserAQuestion{
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question" message:@"What are you doing?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil];
    void(^block)(NSUInteger) = ^(NSUInteger buttonIndex){
        if(buttonIndex == 0 ){
            NSLog(@"doCancel");
        }else{
            NSLog(@"doContinue");
        }
    };
    
    objc_setAssociatedObject(alert, EOCMyAlertViewKey, block, OBJC_ASSOCIATION_COPY);
    [alert show];
    
}
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
    void(^block)(NSUInteger) = objc_getAssociatedObject(alertView, EOCMyAlertViewKey);
    block(buttonIndex);
}
  • 可以通過"關聯對象"機制來把兩個對象連起來
  • 定義關聯對象時可指定內存管理語義,用以模仿定義屬性所采用的"擁有關系"與"非擁有關系"
  • 只有在其他做法不可行時才因選用關聯對象,因為這種做法通常會引入難于查找的bug

給分類添加屬性:
//使用前記得#import <objc/runtime.h>

- (void)setName:(NSString *)name{
    // 保存name
    // 動態添加屬性 = 本質:讓對象的某個屬性與值產生關聯
    /*
     object:保存到那個對象中
     key:用什么屬性保存 屬性名
     value:保存值
     policy:策略,strong,weak
     */
    objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    // _name = name;
}
- (NSString *)name{
    return objc_getAssociatedObject(self, "name");
    // return _name;
}

10. 理解objc_msgSend的作用

對象調用方法,oc術語叫"消息傳遞",消息有"名稱"(name)或"選者子"(selector),可以接受參數,可有返回值。

void objc_msgSend(id self,SEL cmd,...)
這個是"參數個數可變的函數",能接受兩個或兩個以上的參數.第一個參數代表接收者,第二個參數代表選擇子(SEL是選擇子的類型),后續參數是消息中的參數,其順序不變。選擇子指的就是方法的名字。

objc_msgSend函數會根據接收者與選擇子來調用適當的方法。為了完成此操作,該方法需要在接收者所屬的類中搜尋其"方法列表",如果能找到與選擇子名稱相符的方法,就跳至其實現代碼。若找不到,那就沿著繼承體系繼續向上查找,等找到合適的方法之后再跳轉。如果最終還是找不到相符的方法,那就執行"消息轉發"操作。

  • 消息由接收者、選擇子及參數構成。給某對象"發送消息"也就是相當于在該對象上"調用方法"
  • 發給某對象全部消息都要由"動態消息派發系統"來處理,該系統會查出對應的方法,并執行起代碼

11. 消息轉發機制

消息轉發分為兩大階段:
第一階段先征詢接收者,所屬的類,看其是否能動態添加方法,以處理這個"未知的選擇子"(unknow selector),這叫做"動態方法分析"(先判斷這個類是否能新增一個實例方法用以處理此選擇子)。
第二個階段涉及"完整的消息轉發機制"。如果運行期系統已經把第一階段執行完了,那么接收者自己無法在已動態新增方法的手段來響應包含該選擇子的消息。此時,運行期系統會請求接收者已其他手段來處理與消息相關的方法調用。細分兩小步1.請接收者看看有沒有其他對象能處理這條消息。若有,則運行系統會把消息傳給那個對象,于是消息轉發結束。若沒有“備援接收者”則啟動完整的消息轉發機制,運行系統會把與消息有關的全部細節都封裝到NSInvocation對象中,再給接收者最后一個機會,令其設法解決當前還未處理的這條消息.

動態方法解析:
對象在收到無法解讀的消息后,首先將調用其所屬類的類方法:
+(BOOL)resolveInstanceMethod:(SEL)selector
該方法的參數就是那個未知的選擇子,返回類型為bool類型,表示這個類是否能新增一個實例方法用以處理此選擇子。
使用這方法前提是:相關方法的實現代碼已經寫好,只等著運行的時候動態插在類里面就行了。此方案常用來實現@dynamic屬性,比如要訪問CoreData框架中NSManagedObjects對象的屬性時就可以用,因為實現這些屬性所需的存取方法在編譯器就能確定。

備援接收者:
當前接收者還有第二次機會能處理未知的選擇子,這一步中,運行期系統會問他:能不能把這條消息轉發給其它接收者來處理。該步驟對應處理方法:
-(id)forwardingTargetForSelector:(SEL)selector
方法參數代表未知的選擇子,若當前選擇子能找到備援對象,則將其返回,若找不到,返回nil。

完整的消息轉發:
若轉發算法來到這一步,那么只能啟用完整的消息轉發機制了。
首先創建NSInvocation對象,把與尚未處理的那條消息有關的全部細節都封裝與其中。此對象包含選擇子、目標及參數。在觸發NSInvocation對象時,"消息派發系統"將親自出馬,把消息指派給目標對象。此步驟會調用下列方法來轉發消息:- (void)forwardInvocation:(NSInvocation*)invocation
此方法很簡單:只需改變調用目標,使其消息在新目標上得以調用即可。實現此方法,若發現某調用操作不應由本類處理,則需調用超類的同名方法。這樣,繼承體系中的每個類都有機會處理此調用方法,直至NSObject。若最后調用NSObject類的方法,那么該方法還會繼續調用"doesNotRecognizeSelector:"以拋出異常,此異常表明選擇子最終未能得到處理。

  • 若對象無法響應某個選擇子,則進入消息轉發流程
  • 通過運行期的動態方法解析功能,我們可以在需要用到某個方法再將其加入類中
  • 對象可以把其無法解讀的某些選擇子轉交給其他對象來處理
  • 經過上述兩步之后,如果還是沒辦法處理選擇子,那就啟動完整的消息轉發機制

12. 用"方法調配技術"調試"黑盒方法"

類的方法列表會把選擇子的名稱映射到相關的方法實現上,使得"動態信息派發系統"能夠據此找到應該調用的方法。這些方法均已函數指針的形式來表示,此指針叫做IMP。
oc運行期系統提供了幾種方法可以讓我們操作IMP(指針),開發者可以向其新增選擇子,也可以改變某選擇子所對應的方法實現,還可以交換兩個選擇子所映射到的指針。

交換實現方法
void method_exchangeImplementations(Method m1,Method m2)
獲取實現方法
Method class_getInstanceMethod(Class aClass,SEL aSelector)
  • 在運行期,可以向類中新增或替換選擇子所對應的方法實現
  • 使用另一份實現來替換原來的方法實現,這道工序叫做"方法調配",開發者常用此技術向原有的實現中添加新功能
  • 一般只有調試程序的時候才需要在運行期修改方法實現,這種做法不宜濫用

13. 理解"類對象"的用意

"在運行期檢視對象類型"這一操作也叫做"類型信息查詢",這個強大而有用的特性內置于Foundation框架的NSObject協議里,凡是由公共根類繼承而來的對象都要遵從此協議。

每個OC對象對象實例都是指向某塊內存數據的指針。所以在聲明變量時,類型后面要加上"*"字符:NSString * s = @"someString";

對象數據結構
typedef struct objc_object{
    Class isa;
} *id;

每個對象數據結構的首個成員時Class類的變量。該變量定義了對象所屬的類,通常稱為"is a"指針。

Class對象也定義在運行期程序庫的頭文件中:
typedef struct objc_class *Class;
struct objc class{
    Class isa;
    Class super_class;
    const char *name;
    long version;
    long info;
    long instance_size;
    struct objc_ivar_list *ivars;
    struct objc_method_list **methodLists;
    struct objc_cache *cache;
    struct objc_protocol_list *protocols;
};

此結構體存放類的"元數據",例如類的實例實現了幾個方法,具備多少個實例變量等信息。此結構體的首個變量也是isa指針,這說明Class本身亦為OC對象。結構體里還有個變量叫做super_class,它定義了本類的超類。類對象所屬的類型(也就是isa指針所指向的類型)是另外一個類,叫做"元類",用來表述對象本身所具備的元數據。"類方法"就定義在此處,因為這些方法可以理解成類對象的實例方法。每個類僅有一個"類對象",而每個"類對象"僅有一個與之相關的"元類"。super_class指針確立了繼承關系,而isa指針描述里實例所屬的類。

  • 每個實例都有一個指向Class對象的指針,用以表面其類型,而這些Class對象則構成了類的繼承體系
  • 如果對象類型無法在編譯期確定,那么就應該使用類信息查詢方法來探知
  • 盡量使用類型信息查詢方法來確定對象類型,而不要直接比較類對象,因為某些對象實現了消息轉發功能

接口與API設計

14. 用前綴避免命名空間沖突

  • 選擇公司、應用程序或二者關聯之名稱作為類名前綴
  • 若自己所開發的程序庫中用到了第三方庫,則應該為其中的名稱加上前綴

15. 提供"全能初始化方法"

  • 若全能初始化方法與超類不同,則需要覆寫超類中的對應方法
  • 若超類的初始化方法不適應于子類,那么應該覆寫這個超類方法,并在其中拋出異常

16. 實現description方法

  • 實現description方法返回一個有意義的字符串,用以描述該實例
  • 若想在調試時打印出更詳盡的對象描述信息,則應實現debugDescription方法

17. 盡量使用不可變對象

  • 若某屬性僅可與對象內部修改,則在"class-continuation分類"中將其由readonly屬性擴展為readwrite屬性
  • 不要把可變的collection作為屬性公開,而應該提供相關方法,以此修改對象中的可變collection

18. 使用清晰而協調的命名方式

  • 遵從oc命名規范

19. 為私有方法名加前綴

  • 給私有方法的名稱加前綴,這樣可以很容易的將其同公共方法區分開
  • 不要單用一個下劃線做私有方法的前綴,因為這種做法是預留給蘋果公司用的

20. 理解oc錯誤類型

  • 只有發生可使整個應用程序崩潰的嚴重錯誤時,才應使用異常
  • 在錯誤不嚴重的情況下,可以指派"委托方法"來處理錯誤,
    也可以把錯誤信息放在NSError對象里,經由"輸出參數"返回給調用者

21. 理解NSCopying協議

  • 要想令自己所寫的對象具有拷貝功能,則需實現NSCopying協議,該協議只有一個方法:-(id)copyWithZone:(NSZone*)zone;
  • 如果自定義的對象分為可變版本和不可變版本,那么就要同時實現NSCopying與NSMUtableCopying協議
  • 如果你寫的對象需要深拷貝,那么可以考慮新增一個專門執行深拷貝的方法
  • 深拷貝:在拷貝對象時,將其底層數據也一并復制過去。
    淺拷貝:只拷貝容器對象本身

協議與分類

OC語言特性:“協議”,與Java的"接口"類似。OC不支持多重繼承,因而把某個類應該實現的一系列方法定義在協議里面。

OC語言特性:"分類",利用分類機制,無需繼承子類即可直接為當前類添加方法。

22. 通過委托與數據源協議進行對象間通信

委托模式:定義一套接口,某對象若想接受另一個對象的委托,則需遵從此接口,以便成為其"委托對象",而這"另一個對象"則可以給其委托對象回傳一些信息,也可以在發生相關事件時通知委托對象。
此模式可將數據與業務邏輯解耦;

  • 委托模式為對象提供了一套接口,使其可由此將相關事件告知其他對象
  • 將委托對象應該支持的接口定義成協議,在協議中把可能需要處理的時間定義成方法
  • 當某對象需要從另外一個對象中獲取數據時,可以使用委托模式,此情況,該模式亦稱"數據源協議"
  • 若有必要,可實現含有位段的結構體,將委托對象是否能響應相關協議方法這一信息緩存至其中

23. 將類的實現代碼分散到便于管理的數個分類之中

  • 通過分類機制,可把類代碼分成多個容易管理的小塊,以便單獨檢視,便于調試;
  • 將應該視為"私有"的方法歸入Private的分類中,以隱藏實現細節;

24. 為第三方類的分類名稱加前綴

  • 向第三方類中添加分類時,給其名稱加前綴,給其中的方法加前綴

25. 勿在分類中聲明屬性

  • 分類機制,應該理解為一種手段,目標在于擴展類的功能,而非封裝數據;
  • 把封裝數據所用的全部屬性都定義在主接口里,這樣更加清晰
    *在"class-continuation分類" 之外的其它分類中盡量不要定義屬性

26. 使用"class-continuation分類"隱藏實現細節

"class-continuation分類"和普通的分類不同,它必須定義在其所接續的那個類的實現文件里。中重要之處在于,這是唯一能聲明實例變量的分類,而且此分類沒有特定的實現文件,其中的方法都應該定義在類的主實現文件里。此分類沒有名字,比如有個類叫做Person,其"class-continuation分類"寫法如下:
@interface Person(){
NSString * _anInstanceVariable; // 實例變量
}
// Methods here

@end

  • 通過"class-continuation分類'向類中新增實例變量
  • 把私有方法的原型聲明在"class-continuation分類"中
  • 如果某屬性在主接口中聲明為"只讀",而類的內部又要用設置方法修改此屬性,那么就在"class-continuation分類"中將其擴展為"可讀寫"
  • 若想使類所遵循的協議不為人知,則可于"class-continuation分類"中聲明

27. 通過協議提供匿名對象

可以用協議把自己所寫的API之中的實現細節隱藏起來,將返回的對象設計為遵從此協議的純id類型。這樣的話,想要隱藏的類名就不會出現在API之中了。此概念稱為"匿名對象".

  • 協議可在某種程度上提供匿名類型。具體的對象類型可以淡化成遵從某種協議的id類型,協議里規定了對象所應實現的方法
  • 使用匿名對象來隱藏類型名稱(或類名)
  • 如果具體類型不重要,重要的是對象能夠響應(定義在協議里)特定方法,那么可使用匿名對象來表示

內存管理

OC語言使用引用計數來管理內存,即每個對象都有個可以遞增或遞減的計數器。

28. 引用計數

引用計數機制通過可以遞增遞減的計數器來管理內存。

對象創建出來時,其保留計數至少為1.若想令其繼續存活,則調用retain方法。要是某部分代碼不再使用此對象,不想令其繼續存活,那就調用release或autorelease方法。最終當保留計數歸零時,對象就回收了,也就是說,系統會將其占用的內存標記為"可重用"(reuse)。此時,所有指向該對象的引用也就變得無效了。

29. ARC

ARC實際上也是一種引用計數機制,ARC幾乎把所有的內存管理事宜都交給編譯器來決定。
ARC自動執行retain,release,autorelease等操作,ARC在調用這些方法時,并不通過普通的OC消息派發機制,而是直接調用底層C語言版本。這樣做性能更好,因為保留及釋放操作需要頻繁執行,所以直接調用底層函數能節省很多CPU周期。

  • ARC管理oc對象內存,注意:CoreFoundation對象不歸ARC管理,開發者需適度調用CFRetain/CFRelease

30. 在dealloc方法中只釋放引用并解除監聽

對象在經歷生命周期后,最終會被系統回收,當保留計數降為o的時候,執行dealloc方法。

  • dealloc方法主要是釋放對象所擁有的引用,也就是把所有OC對象都釋放掉。另一件事,就是把原來配置過的觀測行為都清理掉。
  • 如果對象持有文件描述符等系統資源,那么應該專門編寫一個方法來釋放此種資源。這樣的類要和其使用者約定:用完資源后必須調用close方法;
    開銷大或系統內稀缺的資源不應該在dealloc中釋放,文件描述,套接字,大塊內存等都屬于這種資源。不能指望dealloc方法必定會在某個特定的時機調用,因為有一些無法預料的東西可能也持有此對象。此情況,如果非要等到系統調用dealloc方法時才釋放,那么保留這些稀缺資源的時間就有些過長了,通常做法是:實現另一個方法,當應用程序用完資源對象后,就調用此方法。這樣,資源對象的生命周期就變得更明確了。
  • 執行異步任務等方法不應該在dealloc里調用,只能在正常狀態下執行的那些方法也不應在dealloc里調用,因為此時對象已處于正在回收的狀態了。

31. 編寫"異常安全代碼"時注意內存管理問題

TestObject *object;
@try{// 測試代碼
}@catch(){
NSLog(@" there was an error");
}@finally{
[object release];
}
  • 捕獲異常時,要注意將try塊內所創立的對象清理干凈
  • 默認情況,ARC不生成安全處理異常所需要的清理代碼。開啟編譯器標志后,可生成這種代碼,不過會導致應用程序變大,而且會降低運行效率

32. 以弱引用避免保留環

  • 將某些引用設為weak可避免出現"保留環"
  • weak引用可以自動清空,也可以不自動清空。

33. 以"自動釋放池"降低內存峰值

@autoreleasepool{
    // ...
}
  • 自動釋放池排布在棧中,對象受到autorelease消息后,系統將其放入最頂端的池里
  • 合理利用自動釋放池,可降低應用程序的內存峰值

34. 用"僵尸對象"調試內存管理問題

Cocoa提供了"僵尸對象",啟用這項調試功能后,運行期系統會把所有的已經回收的實例轉化成特殊的"僵尸對象",而不會真正回收它們。這種對象所在的核心內存無法重用,因此不可能遭到覆寫。僵尸對象收到消息后,會拋出異常,其中準確的說明了發送過來的消息,并描述了回收之前的那個對象。僵尸對象是調試內存管理問題的最佳方式。
將NSZombieEnabled環境變量設置為YES,即開啟此功能。
Xcode開啟:Scheme-Diagnostics-enable Zombie Objects

僵尸對象工作原理:系統在即將回收對象時,如果發現通過環境變量啟用了僵尸對象功能,那么還將執行一個附加步驟。這一步就是把對象轉化為僵尸對象,而不徹底回收。

  • 系統在回收對象時,可以不將其真的回收,而是把它轉化為僵尸對象。通過環境變量NSZombieEnabled可以開啟此功能
  • 系統會修改對象的isa指針,令其指向特殊的僵尸類,從而使該對象變為僵尸對象。將是類能夠響應所有的選擇子,響應方式為:打印一條包含消息內容及其接收者的消息,然后終止應用程序

35. 不要使用retainCount

  • 對象的保留計數看似有用,實則不然,因為任何給定時間點上的"絕對保留計數"都無法反映對象生命期的全貌
  • 引用ARC后,retainCount方法就正式廢止了,在ARC下調用該方法會導致編譯器報錯

塊與大中樞派發

36. 塊

塊與函數類似,只不過是直接定義在另一個函數里的,和定義它的那個函數共享同一個范圍內的東西。塊用"^"符號表示,后面跟一對花括號,括號里面是塊的實現代碼。
^{
// 實現代碼
}
塊其實就是個值,而且自有其相關類型。與int,float或OC對象一樣,也可以把塊賦值給變量,然后像是用其他變量那樣使用它。
塊類型語法結構:return_type (^block_name)(parameters)

塊的強大之處:在聲明它的范圍里,所有變量都可以為其所捕獲。也就是說,那個范圍里的全部變量,在塊里依然可用。

int (^addBlock)(int a,int b) = ^(int a,int b){
return a+b;
};
int add = addBlock(2,5);

聲明變量的時候加上_block修飾符,就可以在塊內修改其變量值了;

塊可視為對象,可有引用計數,當最后一個指向塊的引用被移走之后,塊就回收了。
定義塊的時候,其所占的內存區域是分配在棧中的,也就是說,塊只在定義它的那個范圍內有效。

  • 塊是C、C++、OC中的語法閉包
  • 塊可以接受參數,也可返回值
  • 塊可分配在棧或堆上,也可以是全局的。分配在棧上的塊可拷貝到堆里,這樣就和標準的OC對象一樣,具備引用計數了。

37. 為常用的塊類型創建typedef

每個塊都具備其"固有類型",因而可將其賦給適當類型的變量。這個類型由塊所接受的參數及其返回值組成。

  • typedef重新定義塊類型,可令塊變量用起來更簡單
  • 不妨為同一個塊簽名定義多個類型別名。如果要重構的代碼使用了塊類型的某個別名,那么只需修改相應typedef中的塊簽名即可,無需改動其他typedef。

38. 用handler塊降低代碼的分散程度

  • 在創建對象時,可使用內聯的handler塊將其相關業務邏輯一并聲明
  • 由多個實例需要監控時,如果采用委托模式,那么經常需要根據傳入的對象來切換,而若該用handler塊來實現,則可直接將塊與相關對象放在一起
  • 設計API時若果用到了handler塊,那么可以增加一個參數,使其調用者可以通過此參數來覺得應該把塊安排在哪個隊列上執行

39. 用塊引用其所屬對象時不要出現保留環

40. 多用派發隊列,少用同步鎖

  • 派發隊列可用來表述同步語義,這種做法要比使用@synchronized塊或NSLock對象更簡單
  • 將同步與異步派發結合起來,可以實現與普通加鎖機制一樣的同步行為,而這么做卻不會阻塞執行派發的線程
  • 使用同步隊列及柵欄塊,可以令同步行為更加高效

41. 多用GCD,少用performSelector系列方法

  • performSelector系列方法在內存管理方面容易有疏失。它無法確定將要執行的選擇子具體是什么,因為ARC編譯器也就無法插入適當的內存管理方法
  • performSelector系列方法所能處理的選擇子太過局限了,選擇子的返回值類型及發送給方法的參數個數都受限制

42. 通過Dispatch Group機制,根據系統資源狀況來執行任務

43. 使用dispatch_once來執行只需運行一次的線程安全代碼

44. 不要使用dispatch_get_current_queue

  • dispatch_get_current_queue此函數已經廢棄,只應做調試用
  • 由于派發隊列是按層級來組織的,所以無法單用某個隊列對象來描述"當前隊列"這一概念

系統框架

45. 多用塊枚舉,少用for循環

46. 對自定義其內存管理語義的collection使用無縫橋接

47. 構建緩存時選用NSCache而非NSDictionary

  • NSCache可以提供優雅的自動刪減功能,而且是"線程安全的",此外,它與字典不同,并不會拷貝鍵
  • 將NSPurgeableData與NSCache搭配使用,可實現自動清除數據的功能
  • 可給NSCache對象設置上限,用以限制緩存中的對象總個數及"總成本",而這些尺度則定義了緩存刪減其中對象的時機

48. 精簡initialize與load的實現代碼

  • 在加載階段,如果類實現了load方法,那么系統就會調用它。分類里也可以定義此方法,類的load方法要比分類中的先調用。與其他方法不同,load方法不參與覆寫機制
  • 首次使用某類之前,系統會向其發送initialize消息。由于此方法遵從普通的覆寫規則,所以通常應在里面判斷當前要初始化的是哪個類
  • load與initailize方法都應該實現的精簡一些,這些有助于保持應用程序的響應能力,也能減少引入"依賴環"的幾率
  • 無法在編譯期設定的全局變量,可以放在initialize方法里初始化

49. 別忘了NSTimer會保留其目標對象

  • NSTimer對象會保留其目標,直到計時器本身失效為止,調用invalidate方法可令計時器失效,另外,一次性的計時器在觸發完任務之后也會失效
  • 可以擴充NSTimer的功能,用"塊"來打破保留環

50.細節

  • CGRect是C結構體,定義:
struct CGRect{
  CGPoint origin;
  CGSize size;
};
typedef struct CGRect CGRect;

整個系統框架都是使用這種結構體,因為如果改用Objective-C對象來做的話,性能會受影響。與重建結構體相比,創建對象還需要額外開銷,例如分配及釋放堆內存等。如果只需保存int,float,double,char等"非對象類型",那么通常使用CGRect這種結構體就可以了。

51.開發SDK

  • 注意事項一:所有類名都應該加前綴

  • 注意事項二:所有 category 方法加前綴

  • 注意事項三:不要將第三方庫打包進 SDK
    說明:盡量不要將第三方庫打包進 SDK,如果要打包,最好也要將該第三方庫重命名,以避免沖突。

  • 注意事項四:做基本的檢查和測試
    說明:SDK 對外公布前應該進行基本的編譯檢查,不應該有編譯器警告存在。

  • 注意事項五:文檔完整并且正確

  • 注意事項六:支持最新的 CPU 版本

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容