1 系統KVO通知的設置
首先需要覆蓋isConcurrent
屬性并返回值YES
// 必須的,這個方法的返回值用來標識一個 operation 是否是并發的 operation ,我們需要重寫這個方法并返回 YES
- (BOOL)isConcurrent
{
return YES;
}
其次要覆蓋isReady
isExecuting
isFinished
這三個屬性,返回值看下載情況而定
- (BOOL)isReady {
return self.state == SYOperationReady && [super isReady];
}
- (BOOL)isExecuting {
return self.state == SYOperationExecuting;
}
- (BOOL)isFinished {
return self.state == SYOperationFinished;
}
作用:并發執行的 operation
需要負責配置它們的執行環境,并且向外界客戶報告執行環境的狀態。因此,一個并發執行的 operation
必須要維護一些狀態信息,用來記錄它的任務是否正在執行,是否已經完成執行等。此外,當這兩個方法所代表的值發生變化時,我們需要生成相應的 KVO 通知(此時還沒有進行KVO通知的設置下面會進行),以便外界能夠觀察到這些狀態的變化
參考AFN中的AFURLConnectionOperation
類定制NSOperation
的思路進行了以下整理:
首先,設置一個表示下載狀態的枚舉,其中有準備狀態、執行中狀態、完成狀態、暫停狀態(定制模式該狀態實際并無意義)
/// 操作的狀態
typedef NS_ENUM(NSUInteger, SYOperationState) {
/// 暫停狀態
SYOperationPaused,
/// 準備下載狀態
SYOperationReady,
/// 執行中狀態
SYOperationExecuting,
/// 完成狀態
SYOperationFinished,
};
設置好枚舉之后對應的設置一個表示下載狀態的屬性
/// 操作狀態
@property (readwrite, nonatomic, assign) SYOperationState state;
其次,設計改變狀態需要發送KVO
通知的值的名稱
- (NSString *)systemVariableNameByOperationState:(SYOperationState)state
{
switch (state) {
case SYOperationReady:
return @"isReady";
break;
case SYOperationExecuting:
return @"isExecuting";
break;
case SYOperationFinished:
return @"isFinished";
break;
case SYOperationPaused:
return @"isPaused";
break;
default:
break;
}
}
該方法的效果是:通過傳入一個下載狀態枚舉值獲得需要發送KVO
通知的系統屬性名進而方便后面發送改變值之后發送的KVO
通知
再次,需要判斷更改的值是否有效,假如從已完成狀態更改為準備狀態就肯定是無效的。
- (BOOL)stateTransitionIsValidFrom:(SYOperationState)from To:(SYOperationState)to isCanceled:(BOOL)isCanceled
{
switch (from) {
case SYOperationReady: // 從準備狀態過渡到暫?;驁绦袪顟B表示有效, 過渡到完成狀態需要看是否被取消了, 如果被取消了表示有效否則表示無效
{
switch (to) {
case SYOperationPaused:
case SYOperationExecuting:
return YES;
case SYOperationFinished:
return isCanceled;
default:
return NO;
}
}
case SYOperationExecuting: // 從執行狀態過渡到暫?;蛲瓿蔂顟B表示有效, 否則表示無效
{
switch (to) {
case SYOperationPaused:
case SYOperationFinished:
return YES;
default:
return NO;
}
}
case SYOperationFinished: // 從完成狀態過渡到其他狀態都表示無效的
{
return NO;
}
case SYOperationPaused: // 從暫停狀態過渡到準備狀態表示有效的, 否則表示無效的
{
return to == SYOperationReady;
}
default:
break;
}
}
最后,在合適的地方更改下載狀態必須保證系統可以接收到相對應的KVO
通知,因此需要重寫state
屬性的set
方法
- (void)setState:(SYOperationState)state {
// 如果狀態改變是無效的就直接返回
if (![self stateTransitionIsValidFrom:self.state To:state isCanceled:[self isCancelled]]) {
return;
}
@synchronized (self) {
NSString *oldStateKey = [self systemVariableNameByOperationState:self.state];
NSString *newStateKey = [self systemVariableNameByOperationState:state];
[self willChangeValueForKey:newStateKey];
[self willChangeValueForKey:oldStateKey];
_state = state;
[self didChangeValueForKey:oldStateKey];
[self didChangeValueForKey:newStateKey];
}
}
2 NSOperation的操作方法
首先有個main
方法可以重寫, 通常這個方法就是專門用來實現與該 operation
相關聯的任務的。盡管我們可以直接在 start
方法中執行我們的任務,但是用 main
方法來實現我們的任務可以使設置代碼和任務代碼得到分離,從而使 operation
的結構更清晰(然而并沒有什么亂用,本地不使用該方法前面所說的只作為參考了解一下main
方法的用處)
//- (void)main
//{
//}
本地真正用的方法是start
方法和cancel
方法
start
方法是一個 operation
的起點,所有并發執行的 operation
都必須要重寫這個方法,替換掉 NSOperation
類中的默認實現。我們可以在這里配置任務執行的線程或者一些其它的執行環境。另外,需要特別注意的是,在我們重寫的 start
方法中一定不要調用父類的實現
- (void)start
{
// 0. 設置互斥鎖防止多個線程同時改變某個屬性
@synchronized (self) {
// 1. 第一步需要檢測是否被取消了, 如果被取消了要實現相應的KVO,在真正開始執行任務前,我們通過檢查 isCancelled 方法的返回值來判斷 operation 是否已經被 cancel ,如果是就直接返回了
if (self.isCancelled) {
/** 2.
有一個非常重要的點需要引起我們的注意,那就是即使一個 operation 是被 cancel 掉了,我們仍然需要手動觸發 isFinished 的 KVO 通知。因為當一個 operation 依賴其他 operation 時,它會觀察所有其他 operation 的 isFinished 的值的變化,只有當它依賴的所有 operation 的 isFinished 的值為 YES 時,這個 operation 才能夠開始執行。因此,如果一個我們自定義的 operation 被取消了但卻沒有手動觸發 isFinished 的 KVO 通知的話,那么所有依賴它的 operation 都不會執行。
*/
self.state = SYOperationFinished;
return;
}
// 3. 根據請求創建會話任務
self.dataTask = [_session dataTaskWithRequest:_request];
// 更改操作狀態為執行中狀態
self.state = SYOperationExecuting;
}
// 4. 手動開啟會話任務
[self.dataTask resume];
// 5. 判斷會話任務是否存在 -- 如果存在改為下載中 否則 改為下載失敗
[self.downloadRecord setState:self.dataTask ? SYSourceDownloading : SYSourceDownloadFailed];
[self archiveDownloadRecordFile];
[self downloadStateChanged];
}
cancel
方法很多地方都會調用,1. 手動取消會調用 2. 下載失敗會調用 3. 下載完成會調用,表明只要想結束某個操作就必須調用cancel
方法
- (void)cancel
{
@synchronized (self) {
// 1. 判斷此時是否已經取消了
if (self.isCancelled) return;
// 2. 假如此時沒有取消,調用父類的取消方法
[super cancel];
// 3. 判斷dataTask是否存在, 如果存在調用其取消功能
if (self.dataTask) {
[self.dataTask cancel];
}
// 5. 讓dataTask置空
self.dataTask = nil;
// 銷毀定時器
[self.speedTimer invalidate];
self.speedTimer = nil;
// 回調下載速度block
dispatch_async(dispatch_get_main_queue(), ^{//用GCD的方式,保證在主線程上更新UI
if (_sizeBlock) {
_sizeBlock(0, @"0KB/s");
}
});
}
}
值得注意的是: 當操作被添加到隊列中后會自動調用start方法,因此不需要手動調用start方法,手動再次調用可能會出現混亂
3 下載功能實現
首先設計一個初始化方法,通過該方法傳入下載所需的會話對象NSURLSession
下載請求對象NSURLRequest
以及下載的資源保存到本地的文件夾路徑folderPath
- (instancetype)initWithRequest:(NSURLRequest *)request inSession:(NSURLSession *)session saveTo:(NSString *)folderPath
{
self = [super init];
if (self) {
self.folderPath = folderPath;
// 0. 保存資源下載地址
_sourceURL = request.URL.absoluteString;
// 1. 保存請求對象 -- 以便于獲得請求url字符串
_request = request;
// 2. 保存操作所在會話對象 -- 以便于以后根據會話對象創建dataTask
_session = session;
// 設置操作狀態初始化時為準備狀態
_state = SYOperationReady;
}
return self;
}
由于操作在添加到隊列中的時候自動調用start
方法,在start
方法中實現了會話對象根據請求對象所創建的會話任務并啟動會話任務,因此會調用NSURLSessionDataDelegate
代理方法中部分方法
1 當接收到了服務器的反饋會調用URLSession: dataTask: didReceiveResponse: completionHandler:
方法,這個Response包括了HTTP的header(數據長度,類型等信息),這里可以決定DataTask以何種方式繼續(繼續,取消,轉變為Download)
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
// ’304 沒有修改‘ 是一個異常 -- ('304 Not Modified' is an exceptional one)
// 如果response沒有實現statusCode屬性或方法 或者 (NSHTTPURLResponse *)response的statusCode狀態碼小于400并且不等于304 -- 表示成功
if (![response respondsToSelector:@selector(statusCode)] || (((NSHTTPURLResponse *)response).statusCode < 400 && ((NSHTTPURLResponse *)response).statusCode != 304)) {
// 1. 獲得求取文件的總長度
int64_t expected = response.expectedContentLength;
// 3. 設置下載文件的字節總長度
if (!self.downloadRecord.totalBytes || self.downloadRecord.totalBytes == 0) {
// 如果模型中下載文件字節總數不存在計算并保存
self.downloadRecord.totalBytes = self.downloadRecord.totalBytesWritten + expected;
// 歸檔一次
[self archiveDownloadRecordFile];
}
if (expected != -1) {
// 2. 拼接保存到該目錄下的下載文件的全路徑
NSString *fileFullPath = [self.folderPath stringByAppendingPathComponent:self.downloadRecord.fileName];
// 3. 創建輸出流 -- 意味著下載下來的文件拼接到該路徑的文件后
self.outputStream = [[NSOutputStream alloc] initToFileAtPath:fileFullPath append:YES];
// 4. 打開輸出流
[self.outputStream open];
dispatch_async(dispatch_get_main_queue(), ^{//用GCD的方式,保證在主線程上更新UI
// 5. 打開計時器
self.speedTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(speedTimerAction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.speedTimer forMode:UITrackingRunLoopMode];
});
_lastSecondSize = self.downloadRecord.totalBytesWritten;
}else {
completionHandler(NSURLSessionResponseCancel);//如果Response里不包括數據長度的信息,就取消數據傳輸
SYLog(@"錯誤信息: Response里不包括數據長度的信息");
[self.downloadRecord setState:SYSourceDownloadFailed];
// 歸檔
[self archiveDownloadRecordFile];
// 回調狀態
[self downloadStateChanged];
}
}
else if (![response respondsToSelector:@selector(statusCode)] || (((NSHTTPURLResponse *)response).statusCode == 416))
{
// response沒有實現statusCode屬性或方法 或者 (NSHTTPURLResponse *)response的statusCode狀態碼是416 表示 該資源已經被下載完了
// 2. 改變下載狀態為完成 并 歸檔下載記錄文件 并 調用下載狀態改變block
[self.downloadRecord setState:SYSourceDownloadCompleted];
// 歸檔
[self archiveDownloadRecordFile];
// 回調
[self downloadStateChanged];
}
else
{
// 1. 發送下載停止(取消)通知
// 2. 調用下載完成回調block
[self.downloadRecord setState:SYSourceDownloadCancel];
[self archiveDownloadRecordFile];
[self downloadStateChanged];
}
// 5. 是否接收服務器的響應
/*
NSURLSession在接收到響應的時候要先對響應做允許處理:completionHandler(NSURLSessionResponseAllow);,才會繼續接收服務器返回的數據,進入后面的代理方法.值得一提的是,如果在接收響應的時候需要對返回的參數進行處理(如獲取響應頭信息等),那么這些處理應該放在前面允許操作的前面.
*/
if (completionHandler) {
completionHandler(NSURLSessionResponseAllow);
}
}
2 接收到數據之后會調用URLSession: dataTask: didReceiveData:
方法,且每次接收到數據都會調用一次
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
// 1. 輸出流寫入
[self.outputStream write:data.bytes maxLength:data.length];
// 2. 把下載的字節計數累加到下載記錄模型中的已下載字節屬性中
self.downloadRecord.totalBytesWritten += data.length;
[self archiveDownloadRecordFile];
// 3. 調用下載進度block
dispatch_async(dispatch_get_main_queue(), ^{//用GCD的方式,保證在主線程上更新UI
if (self.progressBlock) {
self.progressBlock(self.downloadRecord.totalBytesWritten, self.downloadRecord.totalBytes);
}
});
}
3 是否把response存儲到cache中會調用以下方法
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse * _Nullable))completionHandler{
SYLog(@"是否把Response存儲到Cache中");
// 如果調用此方法,這意味著響應不是從緩存讀取
NSCachedURLResponse *cachedResponse = proposedResponse;
if (completionHandler) {
completionHandler(cachedResponse);
}
}
4 當資源下載完或下載出錯會調用以下方法
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
#warning 如果沒網的情況下調用回調函數可能在處理的地方出現崩潰
[self didCompleteWithError:error];
// 0. 配置互斥鎖防止多個線程同時改變某一屬性
@synchronized (self) {
// 1. 讓dataTask置空
self.dataTask = nil;
self.state = SYOperationFinished;
// 2. 返回到主線程發送下載停止或者下載完成通知
[self cancel];
}
// 1. 關閉輸出流
[self.outputStream close];
// 2. 輸出流指針置空
self.outputStream = nil;
}
此處有一個問題是當沒有網絡的時候執行到此處會直接崩潰程序
最后有必要在類銷毀方法中打印一下方便查看前面設置的KVO
是否成功了
- (void)dealloc
{
SYLog(@"SYDownloadOperation類銷毀了");
}