重讀 Effective Objective-C 2.0 小記

ZeroJ發表於2019-01-22

最近再次拜讀了<>這本書, 經典的書確實值得閱讀, 並且裡面的很多東西, 並不過時, 書中有52條建議, 但這裡筆者只是選取了其中的幾條來分享, 這幾條可能是我們在開發中比較常用的, 還有就是因為其他的不是能用很短的語言寫出來的, 如果你沒有讀過這本經典的書, 還是建議閱讀一下原書.


  1. 對於OC中的物件宣告例如NSObject *obj1 = [NSObject new];, obj1這個指標變數是分配在棧上的, 他指向的是這一個分配在堆上面的例項物件, 如果進行下面的賦值操作NSObject *obj2 = obj1;,那麼並沒有新生成一個例項物件, 只是在棧上分配了一個新的指標變數obj2, 而obj2和obj1指向的例項物件是同一個.
  2. 關於檔案標頭檔案的引入問題, 一般情況下不建議在A.h檔案中引入其他的B.h檔案, 因為在別人引入A.h的時候, 同時也引入了B.h檔案, 增加不必要的檔案耦合和編譯時間, 一般在.h檔案中使用前向宣告@class B, 而在.m檔案中才真的引入標頭檔案, 當然對於protocol不能使用前向宣告, 如果將protocol放在了另一個.h檔案中, 那麼就必須要引入這個標頭檔案了.
  3. 儘量使用字面量語法來初始化字串, 陣列, 字典等, 因為字面量語法其實是一種語法糖, 使用它可以讓程式碼可讀性更高, 當然對於一些必須要使用到初始化方法的時候字面量語法就不好用了.例如:
    NSString *str = @"string";
    NSArray *arr = @[obj1, obj2];
    arr[1]// 讀取使用下標而儘量不使用對應的函式...
    [array setObject:<#(nonnull id)#=""> atIndexedSubscript:<#(nsuinteger)#>]複製程式碼
  4. 少用#define來定義常量, 因為巨集定義只是簡單的程式碼替換, 並沒有型別判斷, 不便於我們閱讀判斷, 同時巨集定義可以被覆蓋, 當別人引入了我們的標頭檔案的時候, 可能會覆蓋我們裡面定義的巨集, 帶來很麻煩的除錯, 我們應該使用C語言風格的 const, static, extern相結合來定義常量
    /// 使用static 和const 定義檔案內部的常量 一般使用k開頭命名
    static float const kAnimationTime = 2.0f;
    /// 使用const定義全域性的常量, 在其他檔案中可以通過 extern float const kAnimationTime引入使用, 一般不用k開頭命名, 而使用class名字
    float const CustomAnimationTime = 2.0f;複製程式碼
  5. 用好列舉, 使用列舉來表示選項, 狀態碼, 可以讓程式碼更清晰, 這個在系統的API中也經常看到, 比如按鈕的狀態, autoresizing… , 例如如果你需要用一些狀態碼來表示網路請求的結果: 你可能會有兩種方法
     1. 定義一個整形變數, 然後說明, 不同的整數代表不同的狀態, 那麼這樣對於開發就很不方便, 必須得很清楚並且很正確的輸入對應的整數才能表示相應的狀態, 那麼就很容易出錯, 和不便於維護
     int statusCode;
     if (statusCode == 200) { }/// 請求成功
     else if () ....
     2. 使用列舉, 對不同的狀態定義不同的名字, 這樣就很清晰方便了, 當然定義的時候使用NS_ENUM比使用enum要`好`
    typedef NS_ENUM(NSInteger, ErrorCode) {
     ErrorCodeNotFind,
     ErrorCodeLostConnection,
     ErrorCodeUnknow
    };複製程式碼

    顯然上面你應該選用列舉, 同時還有一種情況就是, 定義多選項, 這個你是會把他們都放進一個陣列中麼?? 當然不要這樣做, 這個時候也應該使用列舉來定義, 不過會有一點的小技巧, Apple對這種進行了一個包裝, 使用NS_OPTIONS而不是enum

    typedef NS_OPTIONS(NSInteger, ErrorOptions) {
     ErrorOptionsNone = 0,
     ErrorOptionsOne = 1 << 0, ///左移操作    --- 1 --- 0001
     ErrorOptionsTwo = 1 << 1,               --- 2 --- 0010
     ErrorOptionsThree = 1 << 2              --- 4 --- 0100
    };複製程式碼

    因為上面定義的列舉值都為2的整數次冪值, 所以後面就可以使用位操作符 與(&)和或(|)來進行選項的篩選

    ErrorOptions options = ErrorOptionsOne | ErrorOptionsTwo; //--- 0011
     if (options & ErrorOptionsOne) {// ErrorOptionsOne
         // 結束判斷後面
     }
     else if (options & ErrorOptionsTwo) {// ErrorOptionsTwo
         // ...
     }
     else {
         // ...
     }複製程式碼
  6. 需要遍歷操作的時候, 儘量不要用C語言風格的for遍歷, 而是採用OC的 for-in方式的快速列舉, 當然使用block的方式來遍歷未必不是更好的一種方式, 尤其是遍歷字典的時候.
  7. 需要快取的時候使用NSCache而不要使用NSArray或者NSDictionary, 因為使用NSCache來進行快取當記憶體不足的時候系統會自動清理快取, 並且會首先清理快取時間較長的東西, 如果使用NSArray或者NSDictionary就沒有這個福利了
  8. 不要在load方法裡面執行耗時的操作, 因為這個時候會阻塞當前的執行緒, 如果是主執行緒被阻塞, 那麼…就不能接受使用者的響應, 同時不要在load方法裡面使用其他的類和呼叫函式, 因為這個時候程式是脆弱的, 有可能使用的class還沒有被載入到系統中來, 當然使用Foundation裡面的NSString…這些是沒有問題的
  9. initialize這個方法在文件中寫明瞭是在第一次使用這個類的時候才會呼叫一次(懶載入), 但是需要注意的是, 如果父類中實現了initialize這個方法, 而子類中沒有實現這個方法, 當初始化子類的時候, 父類的這個initialize方法是會被呼叫多次的(訊息轉發機制), 比如
       ZJChildClass類裡面沒有重寫initialize方法, 但是他的父類重寫了, 所以在初始化ZJChildClass的時候, 父類的initialize會被呼叫兩次, 即會列印兩條
     @interface ZJBaseClass : NSObject
     @end
     @implementation ZJBaseClass
     + (void)initialize {
         NSLog(@"載入一次-----");
     }
     @end
     @interface ZJChildClass : ZJBaseClass
     @end複製程式碼

    所以一般都是這樣來重寫initialize方法的, 保證只會像我們期望的那樣呼叫一次

     + (void)initialize {
         if (self == [ZJBaseClass class]) { /// 不能用 [self class]
             NSLog(@"載入一次-----");
        }
     }複製程式碼
  10. 對只需要執行一次的程式碼使用dispatch_once, 這樣可以保證執行緒安全, 並且只執行一次, 最常見的是用來實現單例
    + (instancetype)sharedInstance {
    static Object *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [self new];
    });
    return sharedInstance;
    }複製程式碼
  11. 多用GCD少用NSObject的一些performSelector等方法, 因為使用performSelector這種方式可能會造成記憶體洩漏, 一般情況下使用GCD都可以完成, 比如dispatch_after來實現延時後執行
  12. 使用NSTimer的時候要特別注意記憶體洩漏的問題, 因為NSTimer會持有目標物件, 很容易造成迴圈引用的問題, 也許你會想到在這個目標物件的dealloc裡面讓NSTimer失效(呼叫 invalidation 並且置為nil), 但是這根本就沒有用, 因為迴圈引用的原因, 根本就不會呼叫dealloc方法, 所以在裡面銷燬是沒有用的, 需要在物件被銷燬之前手動銷燬計時器

相關文章