聲明:這個筆記的系列是我每天早上打開電腦第一件做的事情,當然使用的時間也不是很多因為還有其他的事情去做,雖然吧自己買了紙質的書但是做筆記和看的時候基本都是看的電子版本,一共52個Tip,每一個Tip的要點我是完全謄寫下來的,害怕自己說的不明白所以就謄寫也算是加強記憶,我會持續修改把自己未來遇到的所有相關的點都加進去,最后希望讀者尊重原著,購買正版書籍。PS:不要打賞要喜歡~
GitHub代碼網址,大大們給個鼓勵Star啊。
整個系列筆記目錄
《Effective Objective-C 2.0》第一份讀書筆記
《Effective Objective-C 2.0》第二份讀書筆記
《Effective Objective-C 2.0》第三份讀書筆記
第六章 block和GCD (block和Grand Central Dispatch)
“塊”是一種可在C,C++以及Objective-C代碼中使用的“語法閉包(lexical closure)”,借用此機制,開發者可講代碼像對象一樣傳遞,令其在不同的情況下運行。
GCD是一種和塊有關的技術,它提供了對線程的抽象,而這種抽象則基于“派發隊列(dispatch queue)”。開發者可將塊排入隊列中,由GCD負責處理所有調用事宜。GCD會根據系統資源情況,適時的創建,復用,摧毀后臺線程。
37.理解“塊”這一概念
塊的基本知識
塊與函數類似,只不過是直接定義在另一個函數里的,和定義它的那個函數共享同一個范圍內的東西。塊使用 “^” 符號來表示,后面跟著花括號。
int additional = 5;
__ block int iSu = 8;
int (^ addBlock) (int a, int b) = ^(int a , int b){
return a + b + additional;
iSu++;
}
int add = addBlock(2,5) // add = 12
聲明變量的時候可以加上_ _block修飾符,變量的地址就可以從stack轉移到heap上,從而持有這個變量,而如果只是局部變量,block會copy變量數值,但是不會再受到變量改變的影響。
如果塊所捕獲的變量是對象類型,那么就會自動保留它。系統在釋放這個塊的時候,那會將其一并釋放。就是引出一個和塊有關的重要問題。塊本身可視為對象。有引用計數。當最后一個指向塊的引用移走之后,塊就回收了。
如果將塊定義在OC的實例方法中,那么除了可以訪問類的所有實例變量之外,還可以使用self變量。塊總能修改實例變量,所以在聲明時無須加__block。不過,如果實例變量與self所指代的實例關聯在一起的。例如,下面這個塊聲明在EOCClass類的方法中。
@interface EOCClass
- (void)anInstancemethod{
void (^someBlock)()= ^{
_anInstanceVariable = “Something”;
NSLog(@“_anInstanceVariable = %@”,_anInstanceVariable);
}
}
如果某個EOCClass實例正在執行anInstanceMethod方法,那么self變量就指向此實例。由于塊里沒有明確使用self變量,所以很容易就會忘記self變量其實也為塊所捕獲了。直接訪問實例變量和通過self來訪問是等效的。
self -> _anInstanceVariable = @“Something” ;
那么這種情況下就會導致“保留環”。
塊的內存布局:
首個變量是指向Class對象的指針,該指針叫做isa。其余內存里含有塊對象正常運轉所需的各種消息。
- invoke變量,這是個函數指針,指向塊的實現代碼。函數原型至少要接受一個void * 型的參數,此參數代表塊。
- descriptor 變量是指向結構體的指針,每個塊里都包含此結構體,其中聲明塊對象的總體大小,還聲明了copy與dispose這兩個輔助函數所對應的函數指針。輔助函數在拷貝及丟棄塊對象時運行。
塊還會把捕捉到的所有變量都拷貝一份。放在descriptor后面。捕捉多少個變量,就會占據多少內存空間。請注意,拷貝的并不是對象本身,而是指向這些對象的指針變量。用于在執行塊的時候,從內存中吧這些捕捉到的變量讀出來。
全局block,棧block以及堆block
定義塊的時候,其所占的內存區域是分配在棧中的,這就是說,塊只在定義它的那個范圍內有效。
void(^block)();
if ( some condition){
block = ^ {
NSLog(@“Block A”);
}
} else {
block = ^ {
NSLog(@“Block B”);
}
}
block();
解決此問題的辦法就是發送copy消息以拷貝。
if ( some condition ){
block = [^{
NSLog(@“Block A”);
} copy];
}else {
block = [^{
NSLog(@“Block B”);
} copy];
}
那么其他文章里面有相關的block類型分類:
全局block(_NSConcreteGlobalBlock)的block要么是空block,要么是不訪問任何外部變量的block。它既不在棧中,也不再堆中。
棧block(_NSConcreteStackBlock)的block有閉包行為,也就是有訪問外部變量,并且block只且只有有一次執行,因棧中的空間是可重復使用的,所以當棧中的block執行一次之后就會被清空出棧,所以無法多次使用。
堆block(_NSConcreteMallocBlock)的block有閉包行為,并且該block需要被多次執行。當需要多次執行時,就會把該block從棧中復制到堆中。
要點:
- 塊是C,C++,Objective-C中的詞語閉包。
- 塊可接受參數,也可返回值。
- 塊可以分配在棧或堆上,也可以是全局的。分配在站上的block可拷貝到堆里,這樣的話,就和標準的Objective-C對象一樣,具備引用計數了。
38.為常用的塊類型創建typedef
要點:
- 以typedef重新定義塊類型,可令塊變量用起來更加簡單。
- 定義新類型時應準從現有的命名習慣,勿使其名稱與別的類型相沖突。
- 不妨為同一塊簽名定義多個類型別名。如果要重構的代碼使用了塊類型的某個別名。那么只需要修改相應typedef中塊簽名即可,無須改動其他的tyoedef。
39.用handler塊降低代碼分散程度
iOS 上有一個叫”系統監控器 ”(system watchdog)在發現某個應用程序的主線程已經阻塞了一段時間之后,就會令其終止。
異步方法在執行任務之后,需要以某種手段通知相關代碼。實現此功能有很多辦法。常用的技巧是設計一個委托協議,令關注此事件的對象遵從該協議。
這里面呢我需要再重新規劃一下委托模式的規范:
第一如果我們要一個類監視另一類的某個屬性,那么。
在監視的那個類里面
@protocol EOCNetworkFecherDelegate<NSObject>
(void)newworkFetcher:(EOCNetworkFetcher *)networkFether didFinishWithData:(NSData *)data;
@end
然后給一個需要賦值給監視類的delegate屬性
@property (nonatomic, weak) id <EOCNetworkFetcherDelegate>delegate;
因為是要監視一個data屬性的 ,所以在被監視的類的.m文件中我們需要:
[_delegate newworkFether:networkFether didFinishWithData:data];
然后再監視的類里面我們首先是
EOCNetworkFecher對象 newEOC newEOC.delegate = self;
然后實現newworkFecher didFinishWithData:
(void)newworkFetcher:(EOCNetworkFetcher *)networkFether didFinishWithData:(NSData *)data{
做對于data數據收取情況的相關對策。
}
該Tip的主題:
那么現在如果我們想要通過block來傳遞監聽的話:
被監聽類:
typedef void (^ iSuNetWorkFecherCompletionHandler)(NSData data,NSError * error);
@interface iSuNetWorkFetcher :NSObject
(void)iSuStartWithCompletionHandler:(iSuNetworkFecherCompletionHandler)completion;
然后再.m里面傳遞block里面的數值
(void)iSuStartWithCompletionHandler:(iSuNetworkFecherCompletionHandler) completion{
int a = 5;
NSError * error;
completion(a,error);
}
然后監聽的類里面 。
首先是對象iSuTest。
[iSuTest iSuStartWithCompletionHandler:^(int data, NSError * error){
NSLog(@“監聽過來的數值為:%@”,data);
}];
要點:
- 在創建對象時,可以使用內聯的handler塊將相關業務邏輯一并聲明。
- 在有多個實例需要監控的時候,如果采用委托模式,那么經常需要根據傳入的對象來切換,而若改用handler塊來實現,則可直接將塊和相關對象放在一起。
- 設計API時如果用到handler塊,那么可以增加一個參數,使調用者可以通過此參數來決定應該把塊安排在哪個隊列上執行。
40.用塊引用其所屬對象時不要出現保留環
//EOCNetworkFetcher.h
#import <Foundation/Foundation.h>
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData * data)
@interface EOCNetworkFether:NSObject
@property (nonatomic, strong, readonly) NSURL * url;
(id) initWithURL:(NSURL *)url;
(void)startWithCompletionHandle:(EOCNetworkFetcherCompletionHandler)completion
@end
//EOCNetworkFetcher.m
#import “EOCNetworkFetcher.h”
@interface EOCNetworkFetcher ()
@property(nonatomic, strong, readwrite) NSURL *url;
@property(nonatomic, copy) EOCNetworkFetcherCompletionHandler completionHandler;
@property(nonatomic, strong) NSData * downloadedData;
@implementation EOCNetworkFetcher
(id)initWithURL:(NSURL *)url{
if (slef = [super init]){
_url = url;
}
return self;
}
(void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion{
self.completionHandler = completion;
//Start the request;
//Request sets downloadedData property
// When request is finished , p_requestCompleted is called;
}
(void)p_requestCompleted{
if(_completionHandler){
_completionHadnler(_downloadedData);
}
}
而另一個類可能會創建這種網絡數據獲取器對象
@implementation EOCClass {
EOCNetworkFetcher * _networkFethcer;
NSData * _fetchedData;
}
- (void)downLoadData{
NSURL * url =[nsurl alloc]initWithString : @“www.baidu.com”;
_networkFethcer =[EOCNetworkFetcher alloc]initwithURL:url];
[_networkFethcer startWithCompletionHandler:^(NSData * data){
NSLog:(@“url = %@’,_networkFethcer.url);
_fetchedData = data;
}];
}
這段代碼看上去是沒有問題的 。 但是存在循環引用。
但是因為handler 里面要設置_fechedData = data ,所以handler是要持有self(EOCClass)的。而實例EOCClass又通過屬性持有著網絡獲取器。
解決辦法就是:要么令_networkFetcher實例變量不再引用獲取器,要么令獲取器的completionHandler屬性不再持有handler塊。在這個例子中,應該等待comletion handler塊執行完畢之后,再去打破保留環,比如:
[_networkFecher startWithCompletionHandler:^(NSData * data){
NSLog(@“Request for URL:%@ finished ” ,_networkFetcher.url );
_fetchedData = data;
_networkFetcher = nil;
}];
但是這種情況很自由在執行handler的時候才會調用,如果completion handler一直不運行,那么保留環就無法被打破,內存就會泄露。
我們可以嘗試 不持有屬性_fetchedData
(void)setcondDownload{
NSURL * url =[[NSURL alloc]initWithString:@"www.baidu.com"];
SecondYearNetworkFetcher * networkFetcher =[[SecondYearNetworkFtcher alloc] initWithURL:url];
[networkFetcher startWithCompletionHandler:^(NSData *data) {
NSLog(@"%@",networkFetcher.url);
_fetchedData = data;
}];
}
//也就是一個局部變量networkFetcher;
但是這樣也是含有保留環的;
因為因為handler獲取網址是通過networkFecher的 那么也就持有了這個類。
但是這個networkFecher又通過block 來持有handler;
那么解決辦法就是在block運行觸發的時候,將completionHandler屬性置為nil
(void)p_requestCompleted{
if (_completionHandler){
_completionHandler(_downloadedData);
}
self.completionHandler = nil;
}
這樣一來,只要下載請求執行完成,保留環就被解除了。
要點:
- 如果塊所捕獲的對象直接或間接地保留了塊本身,那么就要當心保留環問題。
- 一定要找個適當的時機解除保留環,而不能把責任推給API的調用者。
41.多用派發隊列,少用同步鎖
有兩種添加鎖頭的方法:
(void)synchronizedMehod{
@synchronized(self){
}
}
或者是:
_lock =[NSLock alloc] init];
(void)synchronizedMethod{
[_lock lock];
//safe
[_lock unlock];
}
這里面也確定了一下設置屬性的時候為什么是非原子性的,而不是線程安全的原子性的。
因為濫用也就是每一個屬性都用原子性的話會所有的同步快都會彼此搶奪同一個鎖。要是有很多個屬性都這么寫的話,那么每個屬性的同步塊都要等其他所有同步塊執行完畢才能執行。而且屬性的這種知識提供了某種程度的“線程安全(thread safety)”,但是無法保證訪問該對象時絕對是線程安全的。
有個簡單并且高效的辦法可以替代同步塊或者鎖對象,那就是“串行同步隊列”。
_syncQueue = dispatch_queue_create(“com.effectiveobjective.syncQueue”,NULL)
這個最后的一個參數"0"的時候是并行,NULL是串行應該都知道吧。
- (NSString*)someString{
__ block NSString * localSomeString;
dispatch_sync (_syncQueue,^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString{
dispatch_sync(_syncQueue,^{
_someString = someString;
});
}
這樣就變成了同步,優化一下設置數值的時候是不用同步的,
- (void)setSomeString:(NSString *)someString{
dispatch_async(_syncQueue,^{
_someString = someString;
});
}
從調用者的角度來看,這個小改動可以提升設置方法的執行速度,但是如果你測一下程序的性能,那么可能會發現這種寫法比原來慢,因為執行異步派發的時候,需要拷貝塊。如果塊里面的運行操作比較簡單,那么運行的時間就會變慢,但是如果繁瑣的話就會快一點。
那么第二個問題,我們想要讀寫一個屬性。讀的時候可以寫,但是寫的時候停止讀的操作。
這個時候我們可以同步并發隊列的柵欄塊來試下你這個效果。
_syncQueue = dispatch_get_global_queue(DISPATHC_QUEUE_PRIOPRTY_DEFAULT,0);
- (NSString *)someString{
_ _ block NSString * localSomeString;
dispathch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString{
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
})
}
這樣的并發柵欄隊列要比串行要快。這個意思是必須等set完事才能進行get方法。
要點:
- 派發隊列可用來表述同步語義(synchronization semantic),這種做法要比使用@synchronized塊或NSLock對象更簡單。
- 將同步與異步派發結合起來,可以實現與普通加鎖機制一樣的同步行為,而這么做卻不會阻塞執行異步派發的線程。
- 使用同步隊列以及柵欄塊,可以令同步行為更加高效。
42.多用GCD,少用performSelector系列方法
關于perform方法:
object調用perfome方法返回的類型是id,雖然可以是void,如果想返回整數型或者是浮點型那就需要復雜的轉換操作了。而這種轉換很容易出錯。performSelector還有幾個版本,可以在發消息時順便傳遞參數。
所以對比GCD和perform方法我們經常選擇GCD。
[self performSelector:@selector(doSomething) withObject:nil afterDelay:5.0];
//Using dispatch_after
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW,(int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(time,dispatch_get_main_queue(),^{
[self doSomething]
})
對比任務放主線程的兩種形式:
[self performSelectorOnMainThread:withObject:waitUntilDone:];
//Using dispathc_async
dispathc_async(dispathc_get_main_queue(),^{
[self doSomething];
});
要點:
- performSelector系列方法在內存管理方法容易有疏失。它無法確定將要執行的選擇子具體是什么,因而ARC編譯器也就無法插入適當的內存管理方法。
- performSelector系列方法所能處理的選擇子太過局限了,選擇子的返回值類型及發送給方法的參數個數都受到限制。
- 如果想要把任務放在另一個線程上執行,那么最好不要用performSelector系列方法,而是應該把任務封裝到塊里,然后調用GCD的相關方法來實現。
43.掌握GCD及操作隊列的使用時機
NSOperationi以及NSOperationQueue的好處如下:
- 取消某個操作。如果使用派發隊列 ,那么取消某個操作是麻煩的,他只存在fire and forget。而操作隊列只要調用cancel方法就可以了。
- 指定操作間的依賴關系。GCD也能完成響應的操作,只不過多任務的要好好算算邏輯。而操作隊列調用addDependency就可以了。
- 通過鍵值觀測機制監控NSOperation對象的屬性。NSOperation有好多屬性能夠被監聽。比如isCancelled等屬性。
- 指定操作的優先級。
- 重用NSOperation對象
比如NSBlockOperation對象,使用方法是:
第一種:
NSBlockOperation * block1 =[NSBlockOperation blockOperationWithBlock:^{
NSLog(@“調用子類方法的block”);
}];
[block1 setCompletionBlock:^{
NSLog(@“結束調用”);
}];
[block1 start];
第二種
詳細講解
NSBlockOperation * op1 =[NSBlockOperation alloc] init];
[op1 addExecutionBlock:^{
sleep(10)
NSLog(@“第一個線程第一個任務”);
}];
[op1 addExecutionBlock:^{
sleep(4)
NSLog(@“第二個線程第一個任務”);
}];
[op1 addExecutionBlock:^{
sleep(6)
NSLog(@“第一個線程第二個任務”);
}];
結論:這樣方法加載上去的線程 只會是兩個 為啥不是三個或者更多呢?
要點:
- 在解決多線程與任務管理問題時,派發隊列并非唯一方案。
- 操作隊列提供了一套高層的Objective-C API,能實現純GCD所就被的絕大部分功能,而且還能完成一些更為復雜的操作,那些操作若改用GCD來實現,則需另外編寫代碼。
44.通過Disptch Group機制,根據系統資源狀態來執行任務
這里面dispatch group主要的還是并行的隊列,因為串行的就沒有意義了。
創建通過 dispatch_group_dispatch_group_create();
把任務加入到組里面:
void dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
dispatch_group_wait (dispatch_group_t group, dispatch_time_t time);
這個方法同在group結束的時候。算是一種堵塞吧
dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue ,dispatch_block_t block);
這個方法算是一種結束調度組之后調用的block。
要點:
- 一系列任務可歸入一個dispatch group之中。開發者可以在這組任務完畢時獲得通知。
- 通過dispatch group,可以在并發式派發隊列里同時執行多項任務。此時GCD會根據系統資源狀況來調度這些并發執行的任務。開發者若自己來實現此功能,則需編寫大量大媽。
45.使用dispatch_once來執行只需運行一次的線程安全代碼
單例模式我們之前使用的方法是:
- (id)sharedInstance{
static EOCClass * shared = nil;
@synchrnized(self){
if(!sharedInstance){
sharedInstance = [[self alloc] init];
}
}
return shared;
}
使用@synchrnized同步鎖的原因是因為線程安全問題。
關于線程安全問題;
基本就是同一個資源被不同線程同時設置的時候產生的沖突。
而后來我們經常使用的是dispatch_once方法進行單例方法編寫:
- (id)iSuSharedInstance{
static EOCClass * shared = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken ,^{
shared = [[self alloc] init];
})
return shared;
}
事實證明,GCD方法的運行速度要比同步鎖塊的運行速度高了兩倍。
要點:
- 經常需要編寫“只需執行一次的線程安全代碼(thread-safe single-code execution)”。通過GCD所提供的dispathc_once函數,很容易就能實現此功能。
- 標記應該聲明在static或global作用域中,這樣的話,在把只需執行一次的塊傳遞給dispatch_once函數時,傳進去的標記也是相同的。
46.不要使用dispatch_get_current_queue
析構函數:在對象銷毀的時候調用的函數就是析構函數。
我們看下下面的這個兩個函數
- (NSString *)someString{
__block NSString * localSomeString;
dispatch_sync(_syncQueue,^{
localSomeString = _ someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString{
dispatch_async(_syncQueue, ^{
_someString = someString;
});
}
獲取方法的時候可能會死鎖(EXC_BAD_INSTRUCTION)
死鎖是啥子意思呢:
我看哈 就是說單個一個人 要做一個任務 ,然后這個任務的任務是巴拉拉巴拉的任務,所以這個人想要繼續做的話就要先把這個巴拉巴拉的任務做完,然而這個巴拉巴拉德任務又是一個加到了同步隊列里面的任務。因為這個任務列表不是一個棧類型的(LIFO)而是一個先進去先出來的(FIFO)的隊列所以,那么這個人想要完成所有任務就必須要完成外表的隊列任務,然后再完成巴拉巴拉的內部隊列任務。所以任務停在第一個外表任務,而內部巴拉巴拉任務也需要執行,因為這個人已經被第一個任務給拖住了,所以不可能跑去干第二個事情,第二個任務完不成導致第一個任務也返回完成不了。嗨呀,好氣啊,我寫了些啥。
死鎖存在時機就是get和set方法一同被調用。
這里面就帶出了為什么不用dispatch_get_current_queue。
解決方法中含有一個判斷是個否是同步隊列,如果是就直接block() 實現,如果不是就正常的加入隊列運行。
- (NSString *)someString{
_ _block NSString * localSomeString ;
dispatch_block_t accessorBlock = ^{
localSomeString = _someString;
};
if (dispathc_get_current_queue() == _syncQueue){
accessorBlock();
}else {
dispatch_sync(_syncQueue,accessorBlock);
}
}
這個例子這么做當然沒有問題,但是如果是嵌套的隊列
dispatch_queue_t queueA =dispatch_queue_create("com.effective.queueA", NULL);
dispatch_queue_t queueB =dispatch_queue_create("com.effective.queueB", NULL);
dispatch_sync(queueA, ^{
NSLog(@“第一個A");
dispatch_sync(queueB,^{
NSLog(@“第二個B");
dispatch_sync(queueA, ^{
NSLog(@"第三個C");
});
});
});
這個時候就算用getCurrent方法獲取queue的隊列獲取的也是queueB,同樣會產生死鎖。
要解決這個問題,最好的辦法就是通過GCD提供的功能來設定“隊列特有數據”(queue-specific data)
dispatch_queue_t queueC = dispatch_queue_create("com.effectiveobjectivec.queueC", NULL);
dispatch_queue_t queueD =dispatch_queue_create("com.effectiveobjectivec.queueD", NULL);
//將D的運行優先級賦予給C
dispatch_set_target_queue(queueD, queueC);
static int kQueueSpecific;
CFStringRef queueSpecificValue = CFSTR("queueC");
dispatch_queue_set_specific(queueC, &kQueueSpecific, (void *)queueSpecificValue, (dispatch_function_t)CFRelease);
dispatch_sync(queueD, ^{
dispatch_block_t block = ^{
NSLog(@"No deadlock!");
CFStringRef retrievalue = dispatch_get_specific(&kQueueSpecific);
if (retrievalue) {
block();
}else{
dispatch_sync(queueC, block);
}
};
});
那么這邊確認一下。dispatch_set_target_queue(A,B)的意思是講A的優先級變成和B同級別,那么就會變成直線向下運行。
要點:
- dispatch_get_current_queue函數的行為常常與開發者所預期的不同。此函數已經廢棄,只應該在調試的時候運用。
- 由于派發隊列是按層級來組織的,所以無法單用某個隊列對象來描述“當前隊列”這一概念。
- dispatch_get_current_queue函數用于解決由不可重入的代碼所引起的死鎖。然而能用次函數解決的問題,通常也能改變“隊列特定數據”來解決。
第七章 系統框架
47.熟悉系統框架
框架的意義:將一系列代碼封裝為動態庫(dynamic library),并在其中放入描述其接口的頭文件,這樣做出來的東西就叫框架。iOS憑條構建的三方框架使用的是靜態庫(static library),這是因為iOS應用程序不允許在其中包含動態庫。這些東西嚴格講不是真正的框架,不過,所有iOS平臺的系統框架仍然使用動態庫。
在iOS上開發“帶圖形界面的應用程序”時,會用到名為Cocoa的框架,也就是Cocoa Touch,其實Cocoa本身并不是框架,但是里面集成了一批創建應用程序時經常會用到的框架。
開發者會碰到的主要框架就是Foundation 像NSObject,NSArray,NSDictionary等類都在其中。
還有一個Foundation相伴的框架,叫做CoreFoundation。從技術上講,CoreFoundation框架不算是Objective-C框架,但是他編寫了OC應用程序所應熟悉的重要框架,Foundation框架中的許多功能,都可以在CoreFoundation上找到對應的C語言API。CoreFoundation和Foundation框架不僅名字相似,而且還有更加緊密的聯系,叫做“無縫橋接”(toll-free bridging)。
無縫橋接技術是用某些相當復雜的代碼實現出來的。這些代碼可以使運行期系統把CoreFoundation框架中的對象視為普通的Objective-C對象。
- CFNetwork:此框架提供了C語言級別的網絡通信能力。
- CoreAudio:C語言API來才做設備上的音頻硬件。
- CoreData:可將對象放入數據庫,以便持久保存。
- AVFoundation:回放并錄制視頻音頻。
- CoreText:執行文字排版以及渲染操作。
用C語言來實現API的好處是,可以繞過OC的運行期系統,從而提高執行速度。當然由于ARC只負責Objective-C的對象,所以使用這些API的時候需要注意內存管理問題。如果是UI框架的話,主要就是UIKit。
CoreAnimation是OC寫成的,CoreAnimation本身并不是框架,它是QuartzCore框架的一部分。然而在框架的國度里,CoreAnimation仍算是“一等公民”。
CoreGraphics框架是以C語言寫成的。提供了2D渲染所必備的數據結構和函數。例如CGPoint,CGSize,CGRect等。
要點:
- 許多系統框架都可以直接使用,其中最重要的是Foundation和CoreFoundation,這兩個框架構建應用程序所需的許多核心功能。
- 很多常見任務都能用框架來做,例如音頻處理,網絡通訊,數據管理等。
- 請記住:用純C寫成的框架與同OC寫成的一樣重要,若要成為優秀的OC開發者,應該掌握C語言的核心概念。
48.多用塊枚舉,少用for循環
相對于For循環對應的 NSArray,NSDictionary,NSSet,NSEnumerator更加便捷。
Dic : [aDictionary allKeys] 賦值給NSArray;
Set : [aSet allObjects] 賦值給NSArray;
NSEnumerator是個抽象類,其中提供了兩個方法- nextObject
和 - allObjects
NSArray:
NSArray * anArray = @[@"iSu",@"Abner",@"iSuAbner"];
NSEnumerator * enumerator = [anArray objectEnumerator];
id iSuobject;
while ((iSuobject = [enumerator nextObject]) != nil) {
NSLog(@"NSEnumerator保存的數據位%@",iSuobject);
}
NSDictionary:
NSDictionary * aDictionary = @{@"iSu":@"1",@"Abner":@"2",@"KoreaHappend":@"3"};
NSEnumerator * DicEn = [aDictionary keyEnumerator];
id key;
while ((key = [DicEn nextObject])!= nil) {
id value = aDictionary[key];
NSLog(@"NSDictionary的%@對應的是%@",key,value);
}
NSSet:
NSSet * aSet = [NSSet setWithObjects:@"iSu",@"Abner",@"iSuAbner", nil];
NSEnumerator * SetEn = [aSet objectEnumerator];
id Setobject;
while ((Setobject = [SetEn nextObject]) != nil) {
NSLog(@"NSSet里面出來的東西:%@",Setobject);
}
快速遍歷:
也就是forin遍歷:
NSDictionary:
id key;
for (id key in aDictionary) {
id value = aDictionary[key];
//NSLog(@"ForinNSDictionary的%@對應的是%@",key,value);
}
NSSet:
id key;
for (id key in aDictionary) {
//NSLog(@“ForinNSSet:%@",key);
}
基于塊的遍歷方式:
(void)enumerateObjectsUsingBlock:(void(^)(id object, NSUInteger idx ,BOOL * stop)) block
block遍歷的優勢在于有一個stop的指針可以選擇性的結束遍歷。
//塊遍歷
[anArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([anArray[idx] isEqualToString:@"Abner"]) {
*stop = YES;
}else{
NSLog(@"block遍歷:%@",anArray[idx]);
}
}];
要點:
- 遍歷collection有四種方式,最基本的辦法是for循環,其次是NSEnumerator遍歷法以及快速遍歷法,最新,最先進的方式則是“塊枚舉法”。
- “塊枚舉法”本身就能通過GCD來并發執行遍歷操作,無需另行編寫代碼。而采用其他遍歷方法則無法輕易實現這一點。
- 若提前知道待遍歷的collection含有何種對象,則應修改塊簽名,指出對象的具體類型。
49.對自定義內存管理語義的collection使用無縫連接
什么時候能夠用到無縫連接?
那我看了原文基本就是一個改變某些基類的內存管理語義。
比如說我們需要一個NSDictionary的鍵是一個不能被copy的對象,我們知道正常情況下key的數值是被拷貝而不是保存的,而如果這個key不是一個遵守NSCopy對象,那么我們就需要他保留下來,而不是copy。
首先說一下OC的NSArray —> CFArrayRef
NSArray * anNSArray = @[@1,@2,@3,@4,@5];
// __bridge本意是:ARC仍然具備這個OC對象的所有權。而__bridge_retained則與之相反,意味著ARC交出對象的所有權。
// NSArray --> CFArrayRef 用__bridge
CFArrayRef aCFArray = (__bridge CFArrayRef)anNSArray;
NSLog(@"size of array = %li",CFArrayGetCount(aCFArray));
而反向的CFArrayRef --> NSArray 用 __bridge_transfer
這里面提醒一下。如果是從C的不可變數組或者是字典轉化成OC的,那么需要的參數會比可變的要多:
// CFArrayCreateMutable(<#CFAllocatorRef allocator#>, <#CFIndex capacity#>, <#const CFArrayCallBacks *callBacks#>)
CFArrayCreate(<#CFAllocatorRef allocator#>, <#const void **values#>, <#CFIndex numValues#>, <#const CFArrayCallBacks *callBacks#>)
我們用到改變內存管理語義的話基本都是可變的,因為你都不可變了你變化管理語義有什么語義....
這里面我們用CFMutableDictionaryRef為例子需要四個參數:
CFDictionaryCreateMutable(<#CFAllocatorRef allocator#>, <#CFIndex capacity#>, <#const CFDictionaryKeyCallBacks *keyCallBacks#>, <#const CFDictionaryValueCallBacks *valueCallBacks#>)
第一個是內存,第二個是個數,第三個是key屬性的對象內存管理,第四個參數是value屬性的對象內存管理。仔細的demo請看FortyNine。
最后代碼:
CFMutableDictionaryRef aCFDictionary = CFDictionaryCreateMutable(NULL, 0, &keyCallbacks, &valueCallbacks);
NSMutableDictionary * anNSDictionary = (__bridge_transfer NSMutableDictionary *)aCFDictionary;
這樣生成的字典就能滿足健可以是不能copy的對象。
要點:
- 通過無縫橋接計數,可以在Foundation框架中的Objective-C對象與CoreFoundation框架中的C語言結構之間來回轉換。
- 在CoreFoundation層面創建collection時,可以指定許多回調函數,這些函數表示此collection應如何處理其元素。然后,可運用無縫橋接技術,將其轉換成具備特殊內存語義的Objective-C collection。
50.構建緩存時選用NSCache而非NSDictionary
對比NSCache相對于NSDictionary的優勢:
- 當系統資源將要耗盡的時候,它可以自動刪除緩存,字典的話會比較麻煩,而且NSCache會先行刪除“最久未使用的對象”。
NSCache并不會“拷貝”鍵,而是“保留”他,這樣就可以使得健值可以使不能被copy的對象。 - 開發者可以操控緩存刪減其內容的時機。可以調整的有兩個數值,一是緩存中的”對象總數“,其二是對象的“總開銷”,開發者在將對象加入緩存的時候,可以為其指定“開銷值”。當對象總數或開銷超過上限時,緩存就可能會刪減其中的對象了。想緩存中添加對象時候,只能在很快計算出“開銷值”的情況下,才應該考慮采用這個尺度。如果計算的過程長的就不適合這樣了。具體的例子可以看Fifty.demo。
- 還有一個類NSPurgeableData,此類是NSMutableData的子類,這個對象在內存能夠根據需求隨時丟棄。我們可以通過他遵守的NSDiscardableContent協議里面的isContentDiscarded方法來查詢相關內存是否已經釋放。
- 如果想要訪問某個NSPurgeableData對象,可以調用其beginContentAccess方法,告訴它現在不應該被拋棄,使用完之后調用endContentAccess方法來告訴必要的時候可以丟棄。這就和引用計數相似。
要點:
- 實現緩存時應選用NSCache而非NSDictionary對象。因為NSCache可以提供優雅的自動刪除功能,而且是“線程安全的”,此外,它與字典不同,并不會拷貝健。
- 可以給NSCache對象設置上限,用以限制緩存中的對象總個數及“總成本”,而這些尺度則定義了緩存刪除其中對象對象的時機,但是絕對不要把這些尺寸當成可靠的“硬限制(hard limit)”,它們僅對NSCache其指導作用。
- 將NSPurgeableData與NSCache搭配使用,可實現自動清除數據的功能,也就是說當NSPurgeableData對象所占內存為系統所丟棄時,該對象自身也會從緩存中移除。
- 如果緩存使用得當,那么應用程序的響應速度就能提高。只有那種“重新計算起來很費事”數據,才值得放入緩存,比如那些需要從網絡獲取或從磁盤讀取的數據。
51.精簡initialize和load的實現代碼
load方法和initialize方法的區別:
- load方法只會調用一次。雖然initalize也是只調用一次。。
- load方法必須要所有的load運行才能繼續下去,而init是惰性的。不會直接啟動。
- 如果子類沒有實現load方法,那么就不會調用父類的load方法。
而initialize 就是正常的,如果子類沒有實現就實現超類的。如果有,也要實現超類的。 - 在一個類的load方法里面放入了其他的類的方法是不安全的,應為不清楚那個類是否已經load完畢了。
要點:
- 在加載階段,如果類實現了load方法,那么系統就會調用它。分類里也可以定義此方法,類的load方法要比分類中的先調用。和其他類方法不同,load方法不參與覆寫機制。
- 首次使用某個類之前,系統會給其發送initialize消息。由于此方法遵從普通的覆寫規則,所以通常應該在判斷當前要初始化的是哪個類。
- load和init方法都應該實現的精簡一點,這有助于保持應用程序的響應能力,也- - 能減少引入“依賴環”的幾率
- 無法在編譯器設定的全局常量,可以放在initialize方法里初始化。
隨便看到的一個句子:
朋友問我暗戀是什么感覺,下意識的回答:好像在商店看到喜歡的玩具,想買,錢不夠,努力存錢,回頭去看的時候發現漲價了,更加拼命的存錢,等我覺得差不多的時候,再回去發現已經被買走了。希望不會在垃圾堆看到這玩具,不然我依然會把它撿起來。
52.別忘了NSTimer會保留其目標對象
計時器是一個很方便很有用的對象,Foundation框架中有個類叫做NSTimer,開發者可以指定絕對的日期和時間,以便到時執行任務。
由于計時器會保留其目標對象,所以反復執行任務通常會導致應用程序出問題。例如:
- (void)startPolling{
//因為target的對象是self 所以定時器是持有self的,而timer有是self的成員變量,所以就相互保留
_pollTimer =[NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(p_doPoll) userInfo:nil repeats:YES];
}
想要打破保留環:
只能改變實例變量或者令計時器無效。
也就說要不就調用[_pollTimer invalidate]; _pollTime = nil;
要不就改變實例變量。
除非所有的代碼在你的手上,否則我們不確定這個定時器失效方法一定會被調用,而且即使滿足沒這種條件,這種通過調用某個方法來避免內存泄露的方法,不是一個好主意。那我們就選擇通過改變實例變量的方法。
創造一個NSTimer的分類,然后新加入一個類方法
- (NSTimer *)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats;
這段代碼將計時器所執行的任務封裝成了“塊”,在調用計時器函數的時候,把它作為userInfo參數傳進去。這個參數可以用來存放“不透明值(也就是全能數值id)”,只要計時器還有效,就會一直保留它。傳入參數時要通過copy方法將block拷貝到“堆”上,否則等到稍后要執行它的時候,該塊可能已經無效了。計時器現在的target是NSTimer類對象,是個單例,因此計時器是否保留他都無所謂。
然后block還是有個強引用的循環,這個簡單就_ _weak就行了。
要點:
- NSTimer對象會保留其目標,直到計時器本身失效為止,調用invalidate方法可令計數器失效,另外,一次性的計時器在觸發完任務之后也會失效。
- 反復執行任務的計時器(repeating timer),很容易引入保留環,如果這種計時器的目標對象又保留了計時器本身,那肯定會導致保留環。這種環狀保留關系,可能是直接發生的,也可能是通過對象圖里的其他對象間接發生的。
- 可以擴充NSTimer的功能,用”塊“來打破保留環。不過,除非NSTimer將來在公共接口里提供此功能。否則必須創建分類,將相關代碼加到其中。
結尾
自己寫的筆記首先是用Pages寫的,寫完之后放到簡書里面以為也就剩下個排版了,結果發現基本上每一個點的總結都不讓自己滿意,但是又想早點放上去,總感覺自己被什么追趕著,哈哈,本來寫完筆記的時候是2W字的,結果到第二次發表的時候發現就成了2.5W了,需要改進的東西還是太多,希望朋友們有什么改進的提議都可以告訴我,我會一直補充這個筆記,然后抓緊改GitHub上的代碼~