iOS 開發(fā) -《Effective Objective-C 2.0:編寫高質量 iOS 與 OS X 代碼的 52 個有效方法》讀書筆記(3)

文章共分為三篇:

第一篇:iOS 開發(fā) -《Effective Objective-C 2.0:編寫高質量 iOS 與 OS X 代碼的 52 個有效方法》讀書筆記(1)
第二篇:iOS 開發(fā) -《Effective Objective-C 2.0:編寫高質量 iOS 與 OS X 代碼的 52 個有效方法》讀書筆記(2)
第三篇:iOS 開發(fā) -《Effective Objective-C 2.0:編寫高質量 iOS 與 OS X 代碼的 52 個有效方法》讀書筆記(3)

接上篇:iOS 開發(fā) -《Effective Objective-C 2.0:編寫高質量 iOS 與 OS X 代碼的 52 個有效方法》讀書筆記(2)

第 6 章 塊與大中樞派發(fā)

第 35 條:理解 “塊”這一概念

blcok和函數類似,它是直接定義在另一個函數里的,和定義它的那個函數共享同一個范圍的東西。用^來表示,后面接一對大括號,括號里是blcok的實現(xiàn)代碼。

  • 格式: 返回類型 (^blockName)(參數){實現(xiàn)代碼};
    // 無返回值無參數
    void(^testBlcok)(void) = ^{
        NSLog(@"testBlcok");
    };
    testBlcok(); // print:"testBlcok"

    // 無返回值有參數
    void(^testBlcok)(int a) = ^(int a) {
        NSLog(@"%d", a);
    };
    testBlcok(5); // print:"5"

    // 有返回值有參數
    int (^addBlock)(int a, int b) = ^(int a, int b) {
        return a + b;
    };
    NSLog(@"%d", addBlock(13, 6)); // print:"19"
  • block可以訪問局部變量,但是不能修改,如果修改局部變量,需要加__block
    __block NSInteger count = 0;
    int additional = 5;
    int (^addBlock)(int a, int b) = ^(int a, int b) {
        count = count + 10;
        return a + b + additional;
    };
    NSLog(@"addBlock = %d", addBlock(13, 6)); // print:"24"
    NSLog(@"count = %ld", count); // print:"10"
  • 另外關于block的修飾符應該注意:
    1.如果用copy修飾Block,該Block就會存儲在堆空間。則會對Block的內部對象進行強引用,導致循環(huán)引用。內存無法釋放。
    解決方法:新建一個指針(__weak typeof(Target) weakTarget = Target )指向Block代碼塊里的對象,然后用weakTarget進行操作。就可以解決循環(huán)引用問題。
    2.如果用weak修飾Block,該Block就會存放在??臻g。不會出現(xiàn)循環(huán)引用問題。

第 36 條:為常用的塊類型創(chuàng)建 typedef

為常用的塊類型創(chuàng)建 typedef,主要是為了代碼的易讀性,用的時候也較為方便。請看下面代碼對比:

// 第一種寫法
- (void)testWithBlockString:(NSString *)string withBlock:(void(^)(id dataSource))block;
/* 這種寫法非常難記,也很難懂,用的時候不方便 */

// 第二種寫法
typedef void (^testBlock)(id dataSource);
- (void)testWithBlockString:(NSString *)string withBlockName:(testBlock)block;
/* 用 typedef 關鍵字,為常用的塊類型起個別名,方便易懂 */

第 37 條:用 handler 塊降低代碼分散程度

iOS開發(fā)中,我們經常會異步處理一些任務,然后等任務執(zhí)行結束后通知相關方法。實現(xiàn)此需求的方法有很多,比如可以選擇代理委托,也可以選擇blockblock更輕型,使用更簡單,能夠直接訪問上下文,這樣類中不需要存儲臨時數據,使用block的代碼通常會在同一個地方,這樣使代碼更連貫,可讀性好。

typedef void (^testBlock)(id dataSource);
- (void)testWithBlockString:(NSString *)string withBlockName:(testBlock)block;

第 38 條:用塊引用其所屬對象時不要出現(xiàn)保留環(huán)

這條講的比較基礎,是iOSblock的循環(huán)引用問題。所謂循環(huán)引用,就是兩個對象相互持有,這樣就會造成循環(huán)引用。

  • 請看下面代碼:
typedef void (^testBlock)(id dataSource);
@property (copy, nonatomic) testBlock block;
@property (nonatomic, copy) NSString *blockString;

- (void)testBlock {
    self.block = ^(id dataSource) {
        NSString *blockString = self.blockString;
        NSLog(@"blockString = %@", blockString);
    };
}
  • 代碼截圖:
block 循環(huán)引用
  • 解決方法:
- (void)testBlock {
    __weak typeof(self) weakSelf = self;
    self.block = ^(id dataSource) {
        NSString *blockString = weakSelf.blockString;
        NSLog(@"blockString = %@", blockString);
    };
}

但并非所有 block 都會造成循環(huán)引用,在開發(fā)中,一些同學只要有block的地方就會用__weak來修飾對象,其實沒有必要,以下幾種block是不會造成循環(huán)引用的:

  • 大部分GCD方法
    dispatch_async(dispatch_get_main_queue(), ^{
        NSString *blockString = self.blockString;
        NSLog(@"blockString = %@", blockString);
    });

代碼解讀:因為self并沒有對GCD中的block進行持有,所以不會形成循環(huán)引用。

  • block 屬于另外一個類
    [NNHomeViewController testWithBlockName:^(id dataSource) {
        NSString *blockString = self.blockString;
        NSLog(@"blockString = %@", blockString);
    }];

代碼解讀:同上,block不是被self所持有的。

  • block并不是屬性值,而是臨時變量
- (void)viewDidLoad {
    [super viewDidLoad];
    [self testWithBlock:^{
        NSString *blockString = self.blockString;
        NSLog(@"blockString = %@", blockString);
    }];
}

- (void)testWithBlock:(void(^)(void))block {
    block();
}

第 39 條:多用派發(fā)隊列,少用同步鎖

在 OC 中,如果有多個線程要執(zhí)行同一份代碼,有時可能會出問題。這種情況下,通常要使用鎖來實現(xiàn)某種同步機制。

  • GCD 出現(xiàn)之前通常使用兩種方法:

第一種:內置的“同步塊”

- (void)synchronizedMethod {
    @synchronized(self) {
        // safe
    }
}

這種寫法會根據給定的對象,自動創(chuàng)建一個鎖,并等待塊中代碼執(zhí)行完畢。執(zhí)行到這段代碼結尾處,鎖就釋放了。但濫用@synchronized(self)會很大程度上降低代碼效率,因此不推薦使用。

第二種:直接使用 NSLock 對象(也可以使用 NSRecursiveLock 遞歸鎖)

_lock = [[NSLock alloc] init];

- (void)synchronizedMethod {
    [_lock lock];
    // safe
    [_lock unlock];
}

這種寫法也有缺陷,在極端情況下,同步塊會導致死鎖,另外與 GCD 相比效率也很低。

  • 推薦:GCD 方式
- (NSString *)testString {
    __block NSString *localTestString;
    dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        localTestString = self.testString;
    });
    return localTestString;
}

- (void)setTestString:(NSString *)testString {
    dispatch_barrier_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        self.testString = testString;
    });
}

使用同步隊列及柵欄塊,可以令同步行為更加高效。

第 40 條:多用 GCD,少用 performSelector 系列方法

GCD 出現(xiàn)之前,開發(fā)者延遲調用一些方法,或者指定運行方法的線程會用 performSelector,但是在 GCD 出來之后就不需要再使用performSelector了。

  • performSelector 系列的方法:
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;

- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
  • performSelector 方法存在的缺點

    • 內存管理問題:在ARC下使用performSelector編譯器經常出現(xiàn)警告,因為它無法確定將要執(zhí)行的選擇子具體是什么,因而ARC編譯器也就無法插入適當的內存管理方法。
    • performSelector系列方法所能處理的選擇子太過于局限,performSelector的返回值只能是void或對象類型;而且它無法處理帶有多個參數的選擇子,最多只能處理兩個參數。
  • 用 GCD 代替 performSelector 系列方法

  1. 延遲調用方法:
// GCD
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)); dispatch_after(time, dispatch_get_main_queue(), ^(void){
    [self doSomething];
});

// performSelector
[self performSelector:@selector(doSomething) withObject:nil  afterDelay:5.0];
  1. 指定運行方法的線程:
// GCD
dispatch_async(dispatch_get_main_queue(), ^{
        [self doSomething];
});

// performSelector
[self performSelectorOnMainThread:@selector(doSomething) withObject:nil waitUntilDone:NO];

第 41 條:掌握 GCD 及操作隊列的使用時機

這條講的是什么時候該用 GCD,什么時候不該用GCD。GCD技術確實很棒,但GCD并不總是最佳解決方案。比如當我們想取消隊列中的某個操作時,或者需要后臺執(zhí)行任務時,這時我們可以用NSOperationQueue,其實NSOperationQueueGCD有很多相像之處。NSOperationQueueGCD之前就已經有了,GCD就是在其某些原理上構建的。GCDC層次的API,而NSOperation是重量級的OC對象。

  • 使用NSOperationNSOperationQueue的好處如下:
    • 取消某個操作。
    • 指定操作間的依賴關系。
    • 通過鍵值觀察機制監(jiān)控NSOperation對象的屬性。
    • 指定操作的優(yōu)先級。
    • 重用NSOperation對象。

第 42 條:通過 Dispatch Group 機制,根據系統(tǒng)資源狀況來執(zhí)行任務

dispatch groupGCD的一項特性,能夠把任務分組。調用者可以等待這組任務執(zhí)行完畢,也可以在提供回調函數之后繼續(xù)往下執(zhí)行,這組任務完成時,調用者會得到通知,開發(fā)者可以拿到結果然后繼續(xù)下一步操作。

請看以下代碼:

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 創(chuàng)建一個隊列組
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, queue, ^{
        // 添加操作...
        NSLog(@"1%@", [NSThread currentThread]);
    });
    dispatch_group_async(group, queue, ^{
        // 添加操作...
        NSLog(@"2%@", [NSThread currentThread]);
    });
    dispatch_group_async(group, queue, ^{
        // 添加操作...
        NSLog(@"3%@", [NSThread currentThread]);
    });
    // 收到通知,回到主線程刷新UI
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"回到主線程刷新UI");
    });

多個任務可歸入一個dispatch group之中。開發(fā)者可以在這組任務執(zhí)行完畢時獲得通知。

第 43 條:使用 dispatch_once 來執(zhí)行只需運行一次的線程安全代碼

這條講的單例模式,即常用的dispatch_once。使用 dispatch_once 可以簡化代碼并且徹底保證線程安全,我們根本無須擔心加鎖或同步,另外它沒有使用重量級的同步機制,所以也更高效。

+ (id)shareInstance {
    static EOCClass *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

第 44 條:不要使用 dispatch_get_current_queue

  • dispatch_get_current_queue 函數的行為常常與開發(fā)者所預期的不同,此函數已經廢棄,只應做調試之用。
  • 由于GCD是按層級來組織的,所以無法單用某個隊列對象來描述"當前隊列"這一概念。
  • dispatch_get_current_queue 函數用于解決由不可以重入的代碼所引發(fā)的死鎖,然后能用此函數解決的問題,通常也可以用"隊列特定數據"來解決。

第 7 章 系統(tǒng)框架

第 45 條:熟悉系統(tǒng)框架

開發(fā)者會碰到的主要框架就是Fundation。另外還有個與Fundation相伴的框架,叫做CoreFoundation。除了FundationCoreFoundation,還有很多系統(tǒng)庫,其中包括但不限于下面列出的這些:

  • CFNetwork:此框架提供了 C 語言級別的網絡通信能力,它將 BSD 套接字(BSD socket)抽象成易于使用的網絡接口。而 Foundation 則將該框架里的部分內容封裝為OC語言的接口,以便進行網絡通信。

  • CoreAudio:此框架所提供的C語言API可以用來操作設備上的音頻硬件。

  • AVFoundation:此框架所提供的OC對象可用來回訪并錄制音頻及視頻,比如能夠在UI視圖類里播放視頻。

  • CoreData:此框架所提供的OC接口可以將對象放入數據庫,將數持久保存。

  • CoreText:此框架提供的C語言接口可以高效執(zhí)行文字排版以及渲染操作。

  • 要點:

    • 許多系統(tǒng)框架都可以直接使用。其中最重要的是FundationCoreFoundation,這兩個框架提供了構架應用程序所需的許多核心功能。
    • 很多常見任務都能用框架來做。例如音頻與視頻處理、網絡通信、數據管理等。
    • 用純C寫成的框架與用OC寫成的一樣重要,若想成為優(yōu)秀的OC開發(fā)者,應該掌握C語言的核心概念。

第 46 條:多用塊枚舉,少用 for 循環(huán)

遍歷collection有四種方式。最基本的辦法是for循環(huán),其次是NSEnumerator遍歷法快速遍歷法,最新最先進的方式則是“塊枚舉法”。

  • 四種遍歷方法:
    NSArray *testArray = @[@1, @2, @3, @4, @5];
    // for 循環(huán)遍歷
    for (int i = 0; i < testArray.count; i ++) {
        NSLog(@"testArray[%d] = %@", i, testArray[i]);
    }
    
    // NSEnumerator遍歷法
    NSEnumerator *enumerator = [testArray objectEnumerator];
    id object;
    while ((object = [enumerator nextObject]) != nil) {
        NSLog(@"object = %@", object);
    }
    
    // 快速遍歷
    for (NSObject *obj in testArray) {
        NSLog(@"obj = %@", obj);
    }
    
    // 塊枚舉遍歷數組
    [testArray enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSLog(@"idx = %zd, obj = %@", idx, obj);
    }];

    // 塊枚舉遍歷字典
    NSDictionary *testDic = @{@"name":@"liu zhong ning",@"age":@"25"};
    [testDic enumerateKeysAndObjectsUsingBlock:^(NSString * key,id object,BOOL * stop){
        NSLog(@"testDic[%@] = %@", key, object);
    }];

塊枚舉法擁有其他遍歷方式都具備的優(yōu)勢,而且還能帶來更多好處,在遍歷字典的時候,還可以同時提供鍵和值,而且還有選項可以開啟并發(fā)迭代功能,所以多寫點代碼還是值的。

第 47 條:對自定義其內存管理語義的collection使用無縫橋接

通過無縫橋接技術,可以在Foundation框架中的OC對象與CoreFoundation框架中的C語言數據結構之間來回轉換。

  • 簡單的無縫橋接示例:
    NSArray *testNSArray = @[@1, @2, @3, @4, @5];
    CFArrayRef testCFArray = (__bridge CFArrayRef)testNSArray;
    NSLog(@"Size of array = %li", CFArrayGetCount(testCFArray));
    // Output:Size of array = 5
  • __bridge:ARC仍然具備這個Objective對象的所有權。
  • __bridge_retained:ARC將交出對象的所有權。
  • __bridge_transfer:C轉化為OC

想深入了解無縫橋接技術的童鞋可以點擊這里:iOS無縫橋接官方文檔

第 48 條:構建緩存時選用 NSCache 而非 NSDictionary

  • 實現(xiàn)緩存時應選用NSCache而非NSDictionary對象。因為NSCache可以提供優(yōu)雅的自動刪減功能,而且是“線程安全的”,此外,它與字典不同,并不會拷貝鍵。
  • 可以給NSCache對象設置上限,用以限制緩存中的對象總個數及“總成本”,而這些尺度則定義了緩存刪減其中對象的時機。但是絕對不要把這些尺度當成可靠的“硬限制”,他們僅對NSCache起指導作用。
  • NSPurgeableDataNSCache搭配使用,可實現(xiàn)自動清除數據的功能,也就是說,當NSPurgeableData對象所占內存為系統(tǒng)所丟棄時,該對象自身也會從緩存中移除。
  • 如果緩存使用得當,那么應用程序的相應速度就能提高。只有那種“重新計算起來很費事的”數據,才值得放入緩存,比如那些需要從網絡獲取或從磁盤讀取的數據。

第 49 條:精簡 initialize 與 load 的實現(xiàn)代碼

  • 當程序啟動的時候,類和分類,必定會調動且僅調用一次load方法。先調用類的load方法,再調用分類的load方法。先調用超類的load方法,再調用子類的load方法。load方法需要實現(xiàn)得精簡一些,因為整個應用程序會在執(zhí)行load方法時都會阻塞。
  • initialize方法會在程序首次用該類之前調用,且只調用一次。initialize是“懶加載”的,如果某個類一直都沒有使用,就不會執(zhí)行該類的initialize方法。initialize方法可以安全使用并調用任意類中的任意方法。initialize方法只應該用來設置內部數據,不應該在其中調用其他方法。
  • 在加載階段,如果類實現(xiàn)了load方法,那么系統(tǒng)就會調用它。分類里也可以定義此方法,類的load方法要比分類中的先調用。與其他方法不同,load方法不參與覆寫機制。
  • 首次使用某個類之前,系統(tǒng)會向其發(fā)送initialize消息。由于此方法遵從普通的覆寫規(guī)則,所以通常應該在里面判斷當前要初始化的是哪個類。
  • loadinitialize 方法都應該實現(xiàn)的精簡一點,這樣有助于保持應用程序的響應能力,也可以減少引入依賴環(huán)的幾率。
  • 無法在編譯器設定的全局變量,可以放在initialize方法里初始化。

第 50 條:別忘了 NSTimer 會保留其目標對象

開發(fā)中經常會用到NSTimer,由于定時器NSTimer會保留其目標對象,所以反復執(zhí)行任務通常會導致應用程序出問題,也就是說很容易造成循環(huán)引用。請看以下代碼:

#import <Foundation/Foundation.h>

@interface NNTimer: NSObject

- (void)startPolling;
- (void)stopPolling;

@end

@implementation NNTimer {
    NSTimer *_pollTimer;
}

- (id)init {
    return [super init];
}

- (void)dealloc {
    [_pollTimer invalidate];
}

- (void)stopPolling {
    
    [_pollTimer invalidate];
    _pollTimer = nil;
}

- (void)startPolling {
    _pollTimer = [NSTimer scheduledTimerWithTimeInterval:5.0
                                                  target:self
                                                selector:@selector(p_doPoll)
                                                userInfo:nil
                                                 repeats:YES];
}

- (void)p_doPoll {
    // Poll the resource
}

@end

上面這段代碼是存在問題的。如果創(chuàng)建了本類的實例,并調用其startPolling方法,那么會如何呢?創(chuàng)建計時器的時候,由于目標對象是self,所以要保留此實例。然而,因為計時器是用實例變量存放的,所以實例也保留了計時器,于是就產生了保留環(huán)。

單從計時器本身入手,你會發(fā)現(xiàn)很難解決這個問題,那么如何解決這個問題呢?我們可以通過“塊”來解決。雖然計時器當前不直接支持塊,但是可以用下面這段代碼為其添加此功能:
.h文件:

@interface NSTimer (NNBlocksSupport)
+ (NSTimer *)nn_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                          block:(void(^)(void))block
                                       repeats:(BOOL)repeats;
@end

.m文件:

#import "NSTimer+NNBlocksSupport.h"

@implementation NSTimer (NNBlocksSupport)

+ (NSTimer *)nn_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                         block:(void(^)(void))block
                                       repeats:(BOOL)repeats {
    return [self scheduledTimerWithTimeInterval:interval
                                         target:self
                                       selector:@selector(nn_blockInvoke:)
                                       userInfo:[block copy]
                                        repeats:repeats];
    
}

+ (void)nn_blockInvoke:(NSTimer *)timer {
    void (^block)(void) = timer.userInfo;
    if (block) {
        block();
    }
}

@end

結束語:由于個人能力有限,這三篇讀書筆記難免有錯誤或不足之處,還望各位道友能不吝賜教,謝謝。

最后安利一下這本書:PDF版

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容