編寫高質量iOS與OS X代碼有效方法讀書筆記(持續更新中)

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

  • 除非確有必要,否則不要引入頭文件。一般來說,應在某個類的頭文件中使用向前聲明來提及別的類,并在實現文件中引入那些類的頭文件。這樣做可以降低類之間的耦合性以及解決循環引用頭文件的問題。
  • 父類以及protocol是不能使用向前聲明的,必須要import。某個類必須要遵循某一項協議,這種情況下盡量把“該類遵循某協議”的這條聲明移至"class-continuation"分類中。如果不行的話,最好將協議放在某個單獨的文件中,然后再引入。
//移至"class-continuation"分類中,而不是放在.h文件中
@interface YQCircleViewController ()<UISearchBarDelegate,UISearchDisplayDelegate>{
}

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

使用字面量語法可以縮減源代碼長度,使其更加易讀,減少代碼出錯機率。字面量語法實際是一種 “語法糖”,也稱 “糖衣語法”,是指計算機語言中與另外一套語法等效但是開發者用起來卻更加方便的語法。

//字面數值
NSNumber *someNumner = @1;
NSNumber *intNumner = @1;
NSNumber *floatNumner = @2.5f;
NSNumber *doubleNumner = @3.14159;
NSNumber *charNumner = @'s';

//字面量數組
NSArray *array = @[@"a",@"b"@"c"];
NSString *string = array[0];  //直接使用下標操作數組

//字面量字典
NSDictionary *dict = @{@"key":@"value"};
NSString *string = dict[@"key"];  //直接使用鍵來操作字典

//可變數組與字典
NSMutableArray *mutable = [@[@"a",@"b"] mutableCopy];

然后應該通過下標操作來訪問數組或字典中的鍵來獲取字典中的元素,不過注意語法糖語法的局限性:

  • 字面量所創建的對象必須屬于Foundation 框架,如果自定義這些類的子類,則無法用字面量語法創建其對象;
  • 創建出的對象都是不可變的,需要可變則需多加一步mutableCopy;
  • 字面量語法創建數組或字典時,注意不能有nil否則拋出異常

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

  • 不要用預處理指令定義常量。這樣定義出來的常量不含類型信息,編譯器只是會在編譯前據此執行查找與替換操作。即使有人重新定義了常量值,編譯器也不會產生警告信息,這將導致應用程序中的常量值不一致。
  • 在實現文件中使用static const 來定義 “只在編譯單元內可見的常量“(translation-unitspecific constant)。由于此類常量不在全局符號表中,所以無須為其名稱加前綴。
  • 在頭文件中使用extern 來聲明全局變量,并在相關實現文件中定義其值。這種常量要出現在全局符號表中,所以其名稱應加以區隔,通常用與之相關的類型做前綴。

示例:

image.png
image.png

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

  • 應該用枚舉來表示狀態機的狀態、傳遞給方法的選項以及狀態碼等值,給這些值起個易懂的名字。
  • 在定義選項時,尤其是可以組合的選項時,可將各選項值定義為2的冪,以便通過按位操作將其組合起來。
typedef NS_OPTIONS(NSUInteger, UIInterfaceOrientationMask) {
    UIInterfaceOrientationMaskPortrait = (1 << UIInterfaceOrientationPortrait),
    UIInterfaceOrientationMaskLandscapeLeft = (1 << UIInterfaceOrientationLandscapeLeft),
    UIInterfaceOrientationMaskLandscapeRight = (1 << UIInterfaceOrientationLandscapeRight),
    UIInterfaceOrientationMaskPortraitUpsideDown = (1 << UIInterfaceOrientationPortraitUpsideDown),
    UIInterfaceOrientationMaskLandscape = (UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight),
    UIInterfaceOrientationMaskAll = (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight | UIInterfaceOrientationMaskPortraitUpsideDown),
    UIInterfaceOrientationMaskAllButUpsideDown = (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight),
} __TVOS_PROHIBITED;
  • 處理枚舉類型的switch語句中不要實現default分支,這樣的話,加入新枚舉之后,編譯器會提示開發者:編譯器并未處理所有的異常

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

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

6 理解對象等同性的概念

  • 想要檢測對象等同性,需提供isEqual和hash方法
  • 相同的對象必須有相同的hash碼,有相同hash碼的對象未必相同
  • 應該根據特定需求制定檢測等同性方案,NSObject默認的isEqual方法判斷的是兩個指針指向的內存地址完全相同才相等
  • 編寫hash方法時,應該使用計算速度快且哈希碼碰撞幾率低的算法

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

  • 可以通過"關聯對象"機制來把兩個對象關聯起來
  • 定義關聯對象時可指定內存管理語義,用以模仿屬性時所采用的"擁有關系"與"非擁有關系"
  • 只有其他做法不可取時才應選用關聯對象,因為這種做法通常會引入難以查找的bug
    可以給某對象關聯許多其他的對象,這些對象通過“鍵”來區分。比如某個類里要處理多個警告視圖,那么就必須在警告視圖的delegate方法里判斷傳入的alertView參數,然后選取相應的邏輯。那么可以將alertView按鈕邏輯的方法關聯到alertView上,如下例:
#import <objc/runtime.h>
static void *EOCMyAlertViewKey = @"EOCMyAlertViewKey";

- (IBAction)testAlertAssociation:(id)sender {
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Quetion" message:@"" delegate:self cancelButtonTitle:@"cancel" otherButtonTitles:@"OK",nil];
    void (^block)(NSInteger) = ^(NSInteger buttonIndex) {
        NSLog(@"click index %i",buttonIndex);
    };
    objc_setAssociatedObject(alert, EOCMyAlertViewKey, block, OBJC_ASSOCIATION_COPY);
    [alert show];
}

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
    void (^block)(NSInteger) = objc_getAssociatedObject(alertView, EOCMyAlertViewKey);
    block(buttonIndex);
}

8 盡量使用不可變對象

  • 盡量創建不可變的對象。
  • 不要把可變的集合類對象作為屬性公開,而應提供相關方法,以此修改對象中的可變集合類對象的內容。

9 使用 “類擴展(class-continuation)”隱藏實現細節

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

10 為私有方法名加前綴

編寫類的實現代碼時,經常要寫一些只在內部使用的方法,編碼時應為這些方法加上某些前綴,如p_有助于調試,以便很容易的把公有方法和私有方法區分開,而且還便于修改且不影響已公開的公有API接口。但不要單用一個下劃線_來做私有方法的前綴,這種做法是預留給蘋果公司用的

11總是為第三方類的分類名稱加前綴

  • 向第三方類中添加分類時,總應給其名稱加上你專用的前綴
  • 向第三方類中添加分類時,總應給其中的方法名加上你專用的前綴

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

  • 使用分類機制把類的實現代碼劃分于易管理的小塊
  • 將應該歸為私有的方法歸入名為Private的分類中,以隱藏實現細節

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

  • 在dealloc方法里,應該做的事情就是釋放指向其他對象的引用,取消原來訂閱的KVO鍵值觀測或者NSNotificationCenter等通知
  • 如果對象持有文件描述等系統資源,那么應專門編寫一個方法來釋放此種資源。這樣的類要和使用者約定,用完資源后必須調用close方法
  • 執行異步任務的方法不應在dealloc方法里調用,只能在正常狀態下調用的那些方法也不應在dealloc方法里調用,因為此時對象已經處于回收狀態了

14 多用塊枚舉,少用for 循環

  • 遍歷集合類對象有四種方式。最基本的辦法就是for 循環,其次是NSEnumerator 遍歷法及快速遍歷法,最新、最先進的方式則是 “塊枚舉法”。
  • “塊枚舉法” 本身就能通過GCD 來并發執行遍歷操作,無須另行編寫代碼。而采用其他遍歷則無法輕易實現這一點。

15 構建緩存時選用 NSCache 而非 NSDictionay

  • NSCache 是專門來處理緩存的,在系統資源將要耗盡時,它可以自動刪減緩存。
  • NScache 并不會 “拷貝” 鍵,而是會 “保留” 它。NScache 鍵很多時候都是由不支持拷貝操作對象充當的。NSCache 是線程安全的,不用編寫加鎖代碼,多個線程便可以同時訪問NSCache。

16 以自動釋放池降低內存峰值

程序中有時會遇到大量的for循環,而占用的這些內存需要到下一次事件循環才會被釋放掉,適當的增加自動釋放池可以降低內存峰值,因為在新增的這個池末尾,會將臨時變量釋放掉。

NSMutableArray *array = [[NSMutableArray alloc] init];
    for (NSInteger i=0; i<1000; i++) {
        @autoreleasepool {
            //加上自動釋放池后,可以提早釋放o這個臨時變量
            NSObject *o = [NSObject new];
            [array addObject:o];
        }
    }
  • 自動釋放池排布在棧中,系統創建好自動釋放池,就將其放在棧頂,清空釋放池時,就相當于將其從棧中彈出。當對象收到autorelease消息后,系統將其放入最頂端的自動釋放池中
  • 合理使用自動釋放池,可以減低內存峰值
  • 采用@autoreleasepool這種新式寫法能創建出更為輕便的自動釋放池

17 為常用的塊類型創建typedef

  • 以typedef重新定義塊類型,可令變量用起來更簡單
typedef int(^EOCSomeBlock) (BOOL flag);
@property (nonatomic,copy) EOCSomeBlock block;
  • 定義新類型應遵循現有命名習慣,勿使其名稱與別的類型沖突
  • 不妨為同一個塊定義多個類型別名,如果要重構代碼使用了塊類型的某個別名,那么只需要修改typedef中的塊簽名即可,無需改動其他typedef。類型定義的簽名相同,但用在不同的地方,開發者看到類型別名及簽名中的參數之后,很容易就能理解此類型的用途。
typedef void(^ACAccountStoreSaveCompletionHandler)(BOOL success, NSError *error);
typedef void(^ACAccountStoreRemoveCompletionHandler)(BOOL success, NSError *error);
typedef void(^ACAccountStoreRequestAccessCompletionHandler)(BOOL granted, NSError *error);

18 多用GCD,少用perfromSelector的方法

  • performSelector系列方法在內存管理方面容易有疏失,它無法確定將要執行的選擇子具體是什么,因為ARC編譯器無法為其插入適當的內存管理方法
  • performSelector系列方法所能處理的選擇子太過局限,選擇子的返回類型及發送給方法的參數個數比較受限制
  • 如果想把任務放到另外一個線程執行,最好不要用perfromSelector系列方法,應將任務封裝到塊里,調用GCD相關的方法來實現

19 不要使用dispatch_get_current_queue

  • dispatch_get_current_queue函數的行為常與開發者所預期的不同,此函數已經廢棄,只應做調試之用

20 熟悉系統框架

  • 許多系統框架可以直接使用,其中最重要的便有Foundation與CoreFoundation,這兩個框架提供了構建應用程序所需的很多核心功能。能使用系統框架的地方,盡量使用系統框架。

21 精簡intialize與load方法的實現代碼

21.1 load

對于加入運行期系統中的每個類class及分類category來說,一定會調用load方法,而且僅調用一次。比如app啟動時,肯定會執行一次,但是在執行該方法時,運行期系統處于“脆弱狀態”(fragile state),在執行子類的load方法時,必先執行所有超類的load方法,如果代碼還依賴了其他程序類庫,那么程序庫相關類的load方法也必定會先執行。然而給定一個程序庫,是無法判斷出各個類的加載順序,因此在類的load方法里調用其他的類是不安全的

+ (void)load {
    NSLog(@"Loading EOCClassB");
    //在EOCClassB類的load方法里調用EOCClassA,但此時卻不確定EOCClassA是否已經load完畢
    EOCClassA *o = [EOCClassA new];
}

還有個重要的事情要注意,load方法不像普通方法遵循繼承的規則,如果本類沒有實現load方法,那么不管其各級超累是否實現load方法都不會執行。分類和其所屬的類都可能實現load方法,那么類一定比分類的load方法先執行。而且load方法務必實現的精簡一些,因為整個應用程序在執行load方法時都會阻塞。
實際上,凡是想通過load在類加載之前執行某些任務的,基本都不太對,筆者目前自己見過最多的便是在load方法里寫方法交換邏輯。

21.2 initialize

如果想執行與類相關的初始化操作,還有個辦法就是覆寫initialize方法。initialize方法是當程序運用到了相關類,才會調用,且只調用一次,如果某個類一直沒有用則initialize方法一直不會被執行,就是所謂的“惰性調用”。initialize方法執行時,運行期系統是處于正常狀態的,從運行期系統完整度調用來說可以調用任何類的任意方法。而且運行期系統能確保initialize方法一定會在線程安全環境中執行,因此只有執行initialize方法的線程可以操作類或者實例,其他的線程全部都要先阻塞。
initialize方法遵循繼承規則,如果某個類沒有實現它,而其超類實現了,那么就會運行超類的實現代碼。
initialize方法也要盡量保持精簡,如果在initialize方法里調用了其他類,如果這個類沒有被初始化,那么此時系統會迫使其初始化,但如果這個類又應用了本類(互相依賴),但本類此時還沒初始化完,則會出現問題。
總結來說:

  • 在加載階段,如果類實現了load方法,那么系統會調用它。分類里也可以定義load方法,但是類的load方法會比分類的load方法先調用。與其他方法不同,load方法不參與覆寫機制。子類的load方法時,必先執行所有超類的load方法,但子類如果沒有實現load方法,則也不會調用父類的load方法。
  • 首次使用某個類時,系統會向其發送initialize消息,由于此方法遵循覆寫機制,因此最好在方法里區分當前初始化的是哪個類。
  • load和initialize方法都應實現的精簡一些,有助于保持應用程序的響應能力,也能減少依賴環的產生
  • 無法在編譯期設定的全局常量,可以放在initialize方法里初始化
static void *EOCMyAlertViewKey = @"EOCMyAlertViewKey";

//NSMutableArray在這里初始化編譯器會報錯
static NSMutableArray *ksomeObjects;         //= [NSMutableArray new];

typedef int(^EOCSomeBlock) (BOOL flag);

@interface ViewController ()<UIAlertViewDelegate>{
}
@property (nonatomic,copy) EOCSomeBlock block;
@end

@implementation ViewController
+ (void)initialize {
    //放在這里初始化
    ksomeObjects = [NSMutableArray new];
}
@end

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

  • 委托模式為對象提供了一套接口,使其可由將相關事件告知其他對象
  • 將委托對象應該支持的接口定義成協議,在協議中把可能需要處理的事件定義成方法
  • 當某對象需要從另外一個對象中獲取數據時,可以使用委托模式,這種情景下亦稱“數據源協議”(data source protocol)
  • 如有必要,可實現含有位段的結構體,將委托協議對象是否能響應相關協議方法這一信息緩存至其中。因為反復的通過respondToSeletor方法判斷是否能響應某方法會很多余,基本第一次判斷能響應則能響應,不能響應則不能響應,無需反復判斷,因此建議將判斷的值存儲起來。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容