一步一步構建你的iOS網絡層 - HTTP篇

從簡書遷移到掘金...git

前言

本文參考casa先生的網絡層架構設計從網絡請求的構建到請求結果的處理爲你概述如何構建一個方便易用的iOS網絡層, 全文約五千字, 預計花費閱讀時間20 - 30分鐘.程序員

目錄

  • 網絡請求的構建github

  • 網絡請求的派發數組

    1. 請求的派發與取消
    2. 多服務器的切換
  • 合理的使用請求派發器緩存

    1. 協議仍是配置對象?
    2. 簡單的請求結果緩存器
    3. 請求結果的格式化
    4. 兩個小玩意兒

一.網絡請求的構建

網絡請求的構建很簡單, 根據一個請求須要的條件如URL, 請求方式, 請求參數, 請求頭等定義請求生成的接口便可. 定義以下:安全

@interface HHURLRequestGenerator : NSObject

+ (instancetype)sharedInstance;

- (void)switchService;
- (void)switchToService:(HHServiceType)serviceType;

- (NSMutableURLRequest *)generateRequestWithUrlPath:(NSString *)urlPath
                                           useHttps:(BOOL)useHttps
                                             method:(NSString *)method
                                             params:(NSDictionary *)params
                                             header:(NSDictionary *)header;

- (NSMutableURLRequest *)generateUploadRequestUrlPath:(NSString *)urlPath
                                             useHttps:(BOOL)useHttps
                                               params:(NSDictionary *)params
                                             contents:(NSArray<HHUploadFile *> *)contents
                                               header:(NSDictionary *)header;

@end
複製代碼

能夠看到方法參數都是生成請求基本組成部分, 固然, 這裏的參數比較少, 由於在個人項目中像請求超時時間都是同樣的, 相似這些公用的設置我都偷懶直接寫在請求配置文件裏面了. 咱們看看請求接口的具體實現, 以數據請求爲例:bash

- (NSMutableURLRequest *)generateRequestWithUrlPath:(NSString *)urlPath useHttps:(BOOL)useHttps method:(NSString *)method params:(NSDictionary *)params header:(NSDictionary *)header {
    
    NSString *urlString = [self urlStringWithPath:urlPath useHttps:useHttps];
    NSMutableURLRequest *request = [self.requestSerialize requestWithMethod:method URLString:urlString parameters:params error:nil];
    request.timeoutInterval = RequestTimeoutInterval;
    [self setCookies];//設置cookie
    [self setCommonRequestHeaderForRequest:request];// 在這裏作公用請求頭的設置
    [header enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull value, BOOL * _Nonnull stop) {
        [request setValue:value forHTTPHeaderField:key];
    }];
    return request;
}
複製代碼
- (NSString *)urlStringWithPath:(NSString *)path useHttps:(BOOL)useHttps {
    
    if ([path hasPrefix:@"http"]) {
        return path;
    } else {
        
        NSString *baseUrlString = [HHService currentService].baseUrl;
        if (useHttps && baseUrlString.length > 4) {
            
            NSMutableString *mString = [NSMutableString stringWithString:baseUrlString];
            [mString insertString:@"s" atIndex:4];
            baseUrlString = [mString copy];
        }
        return [NSString stringWithFormat:@"%@%@", baseUrlString, path];
    }
}
複製代碼

代碼很簡單, 接口根據參數調用urlStringWithPath:useHttps:經過BaseURL和URLPath拼裝出完整的URL, 而後用這個URL和其餘參數生成一個URLRequest, 而後調用setCommonRequestHeaderForRequest:設置公用請求, 最後返回這個URLRequest.服務器

BaseURL來自HHService, HHService對外暴露各個環境(測試/開發/發佈)下的baseURL和切換服務器的接口, 內部走工廠生成當前的服務器, 個人設置是默認鏈接第一個服務器且APP關閉後恢復此設置, APP運行中可根據須要調用switchService切換服務器. HHService定義以下:cookie

@protocol HHService <NSObject>

@optional
- (NSString *)testEnvironmentBaseUrl;
- (NSString *)developEnvironmentBaseUrl;
- (NSString *)releaseEnvironmentBaseUrl;

@end

@interface HHService : NSObject<HHService>

+ (HHService *)currentService;

+ (void)switchService;
+ (void)switchToService:(HHServiceType)serviceType;

- (NSString *)baseUrl;
- (HHServiceEnvironment)environment;
@end
複製代碼
#import "HHService.h"

@interface HHService ()

@property (assign, nonatomic) HHServiceType type;
@property (assign, nonatomic) HHServiceEnvironment environment;

@end

@interface HHServiceX : HHService
@end

@interface HHServiceY : HHService
@end

@interface HHServiceZ : HHService
@end

@implementation HHService

#pragma mark - Interface

static HHService *currentService;
static dispatch_semaphore_t lock;
+ (HHService *)currentService {
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        lock = dispatch_semaphore_create(1);
        currentService = [HHService serviceWithType:HHService0];
    });
    
    return currentService;
}

+ (void)switchService {
    [self switchToService:self.currentService.type + 1];
}

+ (void)switchToService:(HHServiceType)serviceType {
    
    dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
    currentService = [HHService serviceWithType:(serviceType % ServiceCount)];
    dispatch_semaphore_signal(lock);
}

+ (HHService *)serviceWithType:(HHServiceType)type {
    
    HHService *service;
    switch (type) {
        case HHService0: service = [HHServiceX new];  break;
        case HHService1: service = [HHServiceY new];  break;
        case HHService2: service = [HHServiceZ new];  break;
    }
    service.type = type;
    service.environment = BulidServiceEnvironment;
    return service;
}

- (NSString *)baseUrl {
    
    switch (self.environment) {
        case HHServiceEnvironmentTest: return [self testEnvironmentBaseUrl];
        case HHServiceEnvironmentDevelop: return [self developEnvironmentBaseUrl];
        case HHServiceEnvironmentRelease: return [self releaseEnvironmentBaseUrl];
    }
}

@end
複製代碼

2.網絡請求的派發

請求的派發是經過一個單例HHNetworkClient來實現的, 若是把請求比做炮彈的話, 那麼這個單例就是發射炮彈的炮臺, 使用炮臺的人只須要告訴炮臺須要發射什麼樣的炮彈和炮彈的打擊目標即可發射了. 另外, 應該提供取消打擊的功能以處理沒必要要的打擊的狀況, 那麼, 根據炮臺的做用. HHNetworkClient定義以下:網絡

@interface HHNetworkClient : NSObject

+ (instancetype)sharedInstance;

- (NSURLSessionDataTask *)dataTaskWithUrlPath:(NSString *)urlPath
                                     useHttps:(BOOL)useHttps
                                  requestType:(HHNetworkRequestType)requestType
                                       params:(NSDictionary *)params
                                       header:(NSDictionary *)header
                            completionHandler:(void (^)(NSURLResponse *response,id responseObject,NSError *error))completionHandler;

- (NSNumber *)dispatchTaskWithUrlPath:(NSString *)urlPath
                             useHttps:(BOOL)useHttps
                          requestType:(HHNetworkRequestType)requestType
                               params:(NSDictionary *)params
                               header:(NSDictionary *)header
                    completionHandler:(void (^)(NSURLResponse *response,id responseObject,NSError *error))completionHandler;

- (NSNumber *)dispatchTask:(NSURLSessionTask *)task;

- (NSNumber *)uploadDataWithUrlPath:(NSString *)urlPath
                           useHttps:(BOOL)useHttps
                             params:(NSDictionary *)params
                           contents:(NSArray<HHUploadFile *> *)contents
                             header:(NSDictionary *)header
                    progressHandler:(void(^)(NSProgress *))progressHandler
                  completionHandler:(void (^)(NSURLResponse *response,id responseObject,NSError *error))completionHandler;

- (void)cancelAllTask;
- (void)cancelTaskWithTaskIdentifier:(NSNumber *)taskIdentifier;

@end
複製代碼
@interface HHNetworkClient ()

@property (strong, nonatomic) AFHTTPSessionManager *sessionManager;
@property (strong, nonatomic) NSMutableDictionary<NSNumber *, NSURLSessionTask *> *dispathTable;

@property (assign, nonatomic) CGFloat totalTaskCount;
@property (assign, nonatomic) CGFloat errorTaskCount;
@end

複製代碼

1.請求的派發與取消

外部暴露數據請求和文件上傳的接口, 參數爲構建請求所需的必要參數, 返回值爲這次請求任務的taskIdentifier, 調用方能夠經過taskIdentifier取消正在執行的請求任務. 內部聲明一個dispathTable保持着此時正在執行的任務, 並在任務執行完成或者任務取消時移除任務的引用, 以數據請求爲例, 具體實現以下:

- (NSURLSessionDataTask *)dataTaskWithUrlPath:(NSString *)urlPath useHttps:(BOOL)useHttps requestType:(HHNetworkRequestType)requestType params:(NSDictionary *)params header:(NSDictionary *)header completionHandler:(void (^)(NSURLResponse *, id, NSError *))completionHandler {
    
    NSString *method = (requestType == HHNetworkRequestTypeGet ? @"GET" : @"POST");
    NSMutableURLRequest *request = [[HHURLRequestGenerator sharedInstance] generateRequestWithUrlPath:urlPath useHttps:useHttps method:method params:params header:header];
    NSMutableArray *taskIdentifier = [NSMutableArray arrayWithObject:@-1];
    NSURLSessionDataTask *task = [self.sessionManager dataTaskWithRequest:request completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
        
        dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
        [self checkSeriveWithTaskError:error];
        [self.dispathTable removeObjectForKey:taskIdentifier.firstObject];
        dispatch_semaphore_signal(lock);
        
        completionHandler ? completionHandler(response, responseObject, error) : nil;
    }];
    taskIdentifier[0] = @(task.taskIdentifier);
    return task;
}

- (NSNumber *)dispatchTaskWithUrlPath:(NSString *)urlPath useHttps:(BOOL)useHttps requestType:(HHNetworkRequestType)requestType params:(NSDictionary *)params header:(NSDictionary *)header completionHandler:(void (^)(NSURLResponse *, id, NSError *))completionHandler {
    
    return [self dispatchTask:[self dataTaskWithUrlPath:urlPath useHttps:useHttps requestType:requestType params:params header:header completionHandler:completionHandler]];
}

- (NSNumber *)dispatchTask:(NSURLSessionDataTask *)task {
    
    if (task == nil) { return @-1; }
    
    dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
    self.totalTaskCount += 1;
    [self.dispathTable setObject:task forKey:@(task.taskIdentifier)];
    dispatch_semaphore_signal(lock);
    [task resume];
    return @(task.taskIdentifier);
}
複製代碼

代碼很簡單, 經過參數生成URLRequest, 而後經過AFHTTPSessionManager執行任務, 在任務執行前咱們以task.taskIdentifier爲key保持一下執行的任務, 而後在任務執行後咱們移除這個任務, 固然, 外部也能夠在必要的時候經過咱們返回的task.taskIdentifier手動移除任務.

注意咱們先聲明一個NSMutableArray來標誌taskIdentifier, 而後在任務生成後設置taskIdentifier[0]爲task. taskIdentifier, 最後在任務完成的回調block中使用taskIdentifier[0]來移除這個已經完成的任務. 可能有人會有疑問爲何不直接使用task.taskIdentifier, block不是能夠捕獲task嗎? 下面解釋一下爲何這樣寫:

咱們知道block之於函數最大的區別就在於它能夠捕獲自身做用域外的對象, 並在block執行的時候訪問被捕獲的對象, 具體的, 對於值類型對象block會生成一份此對象的拷貝, 對於引用類型對象block會生成一個此對象的引用並使該對象的引用計數+1(這裏咱們只描述非__block修飾的狀況). 那麼代入到上面的代碼, 咱們來一步一步分析:

  • 直接捕獲task的寫法
NSURLSessionDataTask *task = [self.sessionManager dataTaskWithRequest:request completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
  ...略
        [self.dispathTable removeObjectForKey:@(task.taskIdentifier)];
...略
    }];
[self.dispathTable setObject:task forKey:@(task.taskIdentifier)];
複製代碼

咱們把它拆開來看:

NSURLSessionDataTask *task; 
NSURLSessionDataTask *returnTask = [self.sessionManager dataTaskWithRequest:request completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
  ...略
        [self.dispathTable removeObjectForKey:@(task.taskIdentifier)];
...略
    }];
task =  returnTask;
[self.dispathTable setObject:task forKey:@(task.taskIdentifier)];
複製代碼

能夠看到returnTask是咱們實際存儲的任務, 而task只是一個臨時變量, 此時task指向nil, 那咱們生成returnTask的block此時捕獲到的task也就是nil, 因此在任務完成的時候咱們的task.taskIdentifier必定是0, 這樣寫的結果就是dispathTable只會添加不會刪除(系統的taskIdentifier是從0開始依次遞增的), 固然, 由於進行中的returnTask咱們是作了存儲的, 因此在任務未完成的時候咱們仍是能夠作取消的.

  • 若是一開始給task一個佔位對象呢不讓它爲nil能夠嗎?
NSURLSessionDataTask *task = [NSObject new]; //1.suspend
NSURLSessionDataTask *returnTask = [self.sessionManager dataTaskWithRequest:request completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
  ...略
        [self.dispathTable removeObjectForKey:@(task.taskIdentifier)];//3.completed
...略
    }];//2.alloc
task =  returnTask;
[self.dispathTable setObject:task forKey:@(task.taskIdentifier)];
複製代碼

這樣其實就是一個簡單的引用變換題了, 咱們來看看各個指針的指向狀況:

suspend: pTask->NSObject block.pTask->nil pReturnTask->nil

alloc: pTask-> NSObject block.pTask->NSObject pReturnTask->returnTask

completed: pTask->returnTask block.pTask->NSObject pReturnTask->returnTask

能夠看到在任務執行完成時咱們訪問block.pTask時也不過是咱們一開始的佔位對象, 因此這個方案也不行, 固然, 取消任務依然可用

事實上block.pTask確實是捕獲了佔位對象, 只是咱們在那以後沒有替換block.pTask指向到returnTask, 然而block.pTask咱們是訪問不了的, 因此這個方案行不通.

  • 若是咱們的佔位對象是一個容器呢?
NSMutableArray *taskIdentifier = [NSMutableArray arrayWithObject:@-1];
NSURLSessionDataTask *returnTask = [self.sessionManager dataTaskWithRequest:request completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
  ...略
        [self.dispathTable removeObjectForKey:@(taskIdentifier.firstObject)];
...略
    }];
taskIdentifier[0] = @(returnTask.taskIdentifier);
[self.dispathTable setObject:task forKey:@(task.taskIdentifier)];
複製代碼

既然咱們訪問不了block.pTask那就訪問block.pTask指向的對象嘛, 更改這個對象的內容不就至關於更改了block.pTask麼, 你們照着2的思路走一下應該很容易就能想通, 我就很少說了.

2.多服務器的切換

關於多服務器其實我也沒有實際的經驗, 公司正在部署第二臺服務器, 具體需求是若是訪問第一臺服務器老是超時或者出錯, 那就切換到第二臺服務器, 基於此需求我簡單的實現一下:

- (NSNumber *)dispatchTask:(NSURLSessionDataTask *)task {
    ...略
    dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
    self.totalTaskCount += 1;
    [self.dispathTable setObject:task forKey:@(task.taskIdentifier)];
    dispatch_semaphore_signal(lock);
    ...略
}
複製代碼
- (NSURLSessionDataTask *)dataTaskWithUrlPath:(NSString *)urlPath useHttps:(BOOL)useHttps requestType:(HHNetworkRequestType)requestType params:(NSDictionary *)params header:(NSDictionary *)header completionHandler:(void (^)(NSURLResponse *, id, NSError *))completionHandler {
    
    NSString *method = (requestType == HHNetworkRequestTypeGet ? @"GET" : @"POST");
    ...略
    NSURLSessionDataTask *task = [self.sessionManager dataTaskWithRequest:request completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
       ...略
        [self checkSeriveWithTaskError:error];
       ...略
    }];
    ...略
}
複製代碼
- (void)checkSeriveWithTaskError:(NSError *)error {
    
    if ([HHAppContext sharedInstance].isReachable) {
        switch (error.code) {
                
            case NSURLErrorUnknown:
            case NSURLErrorTimedOut:
            case NSURLErrorCannotConnectToHost: {
                self.errorTaskCount += 1;
            }
            default:break;
        }
        
        if (self.totalTaskCount >= 40 && (self.errorTaskCount / self.totalTaskCount) == 0.1) {
            
            self.totalTaskCount = self.errorTaskCount = 0;
            [[HHURLRequestGenerator sharedInstance] switchService];
        }
    }
}
複製代碼
- (void)didReceivedSwitchSeriveNotification:(NSNotification *)notif {
    
    dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
    self.totalTaskCount = self.errorTaskCount = 0;
    dispatch_semaphore_signal(lock);
    [[HHURLRequestGenerator sharedInstance] switchToService:[notif.userInfo[@"service"] integerValue]];
}
複製代碼

假設認爲APP在這次使用過程當中網絡任務的錯誤率達到10%那就應該切換一下服務器, 咱們在任務派發前將任務總數+1, 而後在任務結束後判斷任務是否成功, 失敗的話將任務失敗總數+1再判斷是否到達最大錯誤率, 進而切換到另外一臺服務器. 另外還有一種狀況是大部分服務器都掛了, 後臺直接走APNS推送可用的服務器序號過來, 就不用挨個挨個切換了.

三.合理的使用請求派發器

OK, 炮彈有了, 炮臺也就緒了, 接下來看看如何使用這個炮臺.

#pragma mark - HHAPIConfiguration

typedef void(^HHNetworkTaskProgressHandler)(CGFloat progress);
typedef void(^HHNetworkTaskCompletionHander)(NSError *error, id result);

@interface HHAPIConfiguration : NSObject

@property (copy, nonatomic) NSString *urlPath;
@property (strong, nonatomic) NSDictionary *requestParameters;

@property (assign, nonatomic) BOOL useHttps;
@property (strong, nonatomic) NSDictionary *requestHeader;
@property (assign, nonatomic) HHNetworkRequestType requestType;
@end

@interface HHDataAPIConfiguration : HHAPIConfiguration

@property (assign, nonatomic) NSTimeInterval cacheValidTimeInterval;

@end

@interface HHUploadAPIConfiguration : HHAPIConfiguration

@property (strong, nonatomic) NSArray<HHUploadFile *> * uploadContents;

@end

#pragma mark - HHAPIManager

@interface HHAPIManager : NSObject

- (void)cancelAllTask;
- (void)cancelTaskWithtaskIdentifier:(NSNumber *)taskIdentifier;
+ (void)cancelTaskWithtaskIdentifier:(NSNumber *)taskIdentifier;
+ (void)cancelTasksWithtaskIdentifiers:(NSArray *)taskIdentifiers;

- (NSURLSessionDataTask *)dataTaskWithConfiguration:(HHDataAPIConfiguration *)config completionHandler:(HHNetworkTaskCompletionHander)completionHandler;
- (NSNumber *)dispatchDataTaskWithConfiguration:(HHDataAPIConfiguration *)config completionHandler:(HHNetworkTaskCompletionHander)completionHandler;
- (NSNumber *)dispatchUploadTaskWithConfiguration:(HHUploadAPIConfiguration *)config progressHandler:(HHNetworkTaskProgressHandler)progressHandler completionHandler:(HHNetworkTaskCompletionHander)completionHandler;

@end
複製代碼
- (void)cancelAllTask {
    
    for (NSNumber *taskIdentifier in self.loadingTaskIdentifies) {
        [[HHNetworkClient sharedInstance] cancelTaskWithTaskIdentifier:taskIdentifier];
    }
    [self.loadingTaskIdentifies removeAllObjects];
}

- (void)cancelTaskWithtaskIdentifier:(NSNumber *)taskIdentifier {
    
    [[HHNetworkClient sharedInstance] cancelTaskWithTaskIdentifier:taskIdentifier];
    [self.loadingTaskIdentifies removeObject:taskIdentifier];
}

+ (void)cancelTaskWithtaskIdentifier:(NSNumber *)taskIdentifier {
    [[HHNetworkClient sharedInstance] cancelTaskWithTaskIdentifier:taskIdentifier];
}

+ (void)cancelTasksWithtaskIdentifiers:(NSArray *)taskIdentifiers {

    for (NSNumber *taskIdentifier in taskIdentifiers) {
        [[HHNetworkClient sharedInstance] cancelTaskWithTaskIdentifier:taskIdentifier];
    }
}

- (NSURLSessionDataTask *)dataTaskWithConfiguration:(HHDataAPIConfiguration *)config completionHandler:(HHNetworkTaskCompletionHander)completionHandler {
    
    return [[HHNetworkClient sharedInstance] dataTaskWithUrlPath:config.urlPath useHttps:config.useHttps requestType:config.requestType params:config.requestParameters header:config.requestHeader completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
        completionHandler ? completionHandler([self formatError:error], responseObject) : nil;
    }];
}
複製代碼

HHAPIManager對外提供數據請求和取消的接口, 內部調用HHNetworkClient進行實際的請求操做.

1.協議仍是配置對象?

HHAPIManager的接口咱們並無像以前同樣提供多個參數, 而是將多個參數組合爲一個配置對象, 下面說一下爲何這樣作:

  • 爲何多個參數的接口方式很差?

一個APP中調用的API一般都是數以百計甚至千計, 若是有一天須要對已成型的全部的API都追加一個參數, 此時的改動之多, 足使男程序員沉默, 女程序員流淚. 舉個例子: APP1.0已經上線, 1.1版本總監忽然要求對數據請求加上緩存, 操做請求不用加緩存, 若是是參數接口的形式通常就是這樣寫:

//老接口
- (NSNumber *)dispatchTaskWithUrlPath:(NSString *)urlPath
                             useHttps:(BOOL)useHttps
                               method:(NSString *)method
                               params:(NSDictionary *)params
                               header:(NSDictionary *)header;
//新接口
- (NSNumber *)dispatchTaskWithUrlPath:(NSString *)urlPath
                             useHttps:(BOOL)useHttps
                               method:(NSString *)method
                               params:(NSDictionary *)params
                               header:(NSDictionary *)header
                          shouldCache:(BOOL)shouldCache;
複製代碼

而後原來的老接口全都調用新接口shouldCache默認傳NO, 不須要緩存的API不用作改動, 而須要緩存的API都得改調用新接口而後shouldCache傳YES.

這樣能暫時解決問題, 工做量也會小一些, 而後過了兩天總監過來講, 爲何沒有對API區分緩存時間? 還有, 咱們又有新需求了. 呵呵!

  • 使用協議提高拓展性
@protocol HHAPIManager <NSObject>

@required
- (BOOL)useHttps;
- (NSString *)urlPath;
- (NSDictionary *)parameters;
- (OTSNetworkRequestType)requestType;

@optional
- (BOOL)checkParametersIsValid;
- (NSTimeInterval)cacheValidTimeInterval;
- (NSArray<OTSUploadFile *> *)uploadContents;
@end
複製代碼
@interface HHAPIManager : NSObject<HHAPIManager>
...略
- (NSNumber *)dispatchTaskWithCompletionHandler:(OTSNetworkTaskCompletionHander)completionHandler;
...略
@end
複製代碼

其實最初的設計是走協議的, HHAPIManager遵照這個協議, 內部給上默認參數, dispatchTaskWithCompletionHandler:會去挨個獲取這些參數, 各個子類自行實現本身自定義的部分, 這樣之後就算有任何拓展, 只須要在協議裏面加個方法基類給上默認值, 有須要的子類API重寫一下就好了.

  • 替換協議爲配置對象
- (NSURLSessionDataTask *)dataTaskWithConfiguration:(HHDataAPIConfiguration *)config completionHandler:(HHNetworkTaskCompletionHander)completionHandler;
- (NSNumber *)dispatchDataTaskWithConfiguration:(HHDataAPIConfiguration *)config completionHandler:(HHNetworkTaskCompletionHander)completionHandler;
- (NSNumber *)dispatchUploadTaskWithConfiguration:(HHUploadAPIConfiguration *)config progressHandler:(HHNetworkTaskProgressHandler)progressHandler completionHandler:(HHNetworkTaskCompletionHander)completionHandler;
複製代碼

協議的方案其實很好, 也是我想要的設計. 可是協議是針對類而言的, 這意味着從此的每添加一個API就須要新建一個HHAPIManager的子類, 很容易就有了幾百個API類文件, 維護起來很麻煩, 找起來很麻煩(以上是同事要求替換協議的理由, 我仍然支持協議, 可是他們人多). 因此將協議替換爲配置對象, 而後API以模塊功能劃分, 每一個模塊一個類文件給出多個API接口 ,內部每一個API搭上合適的配置對象, 這樣一來只須要十幾個類文件.

總之, 考慮到配置對象既能夠實現單個API單個類的設計, 也能夠知足同事的需求, 協議被換成了配置對象. 另外, 全部的block參數都不寫在配置對象裏, 而是直接在接口處聲明, 看着彆扭寫着方便(block作參數和作屬性哪一個寫起來簡單你們都懂的).

2.簡單的請求結果緩存器

上面簡單提到了請求緩存, 其實咱們是沒有作緩存的, 由於我司HTTP的API如今基本上都被廢棄了, 全是走TCP, 然而TCP的緩存又是另外一個故事了.可是仍是簡單實現一下吧:

#define HHCacheManager [HHNetworkCacheManager sharedManager]

@interface HHNetworkCache : NSObject

+ (instancetype)cacheWithData:(id)data;
+ (instancetype)cacheWithData:(id)data validTimeInterval:(NSUInteger)interterval;

- (id)data;
- (BOOL)isValid;

@end

@interface HHNetworkCacheManager : NSObject

+ (instancetype)sharedManager;

- (void)removeObejectForKey:(id)key;
- (void)setObjcet:(HHNetworkCache *)object forKey:(id)key;
- (HHNetworkCache *)objcetForKey:(id)key;

@end
複製代碼
#define ValidTimeInterval 60

@implementation HHNetworkCache

+ (instancetype)cacheWithData:(id)data {
    return [self cacheWithData:data validTimeInterval:ValidTimeInterval];
}

+ (instancetype)cacheWithData:(id)data validTimeInterval:(NSUInteger)interterval {
    
    HHNetworkCache *cache = [HHNetworkCache new];
    cache.data = data;
    cache.cacheTime = [[NSDate date] timeIntervalSince1970];
    cache.validTimeInterval = interterval > 0 ? interterval : ValidTimeInterval;
    return cache;
}

- (BOOL)isValid {
    
    if (self.data) {
        return [[NSDate date] timeIntervalSince1970] - self.cacheTime < self.validTimeInterval;
    }
    return NO;
}

@end

#pragma mark - HHNetworkCacheManager

@interface HHNetworkCacheManager ()

@property (strong, nonatomic) NSCache *cache;

@end

@implementation HHNetworkCacheManager

+ (instancetype)sharedManager {
    static HHNetworkCacheManager *sharedManager;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        sharedManager = [[super allocWithZone:NULL] init];
        [sharedManager configuration];
    });
    return sharedManager;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    return [self sharedManager];
}

- (void)configuration {
    
    self.cache = [NSCache new];
    self.cache.totalCostLimit = 1024 * 1024 * 20;
}

#pragma mark - Interface

- (void)setObjcet:(HHNetworkCache *)object forKey:(id)key {
    [self.cache setObject:object forKey:key];
}

- (void)removeObejectForKey:(id)key {
    [self.cache removeObjectForKey:key];
}

- (HHNetworkCache *)objcetForKey:(id)key {
    
    return [self.cache objectForKey:key];
}

@end
複製代碼
- (NSNumber *)dispatchDataTaskWithConfiguration:(HHDataAPIConfiguration *)config completionHandler:(HHNetworkTaskCompletionHander)completionHandler{
        
    NSString *cacheKey;
    if (config.cacheValidTimeInterval > 0) {
        
        NSMutableString *mString = [NSMutableString stringWithString:config.urlPath];
        NSMutableArray *requestParameterKeys = [config.requestParameters.allKeys mutableCopy];
        if (requestParameterKeys.count > 1) {
            [requestParameterKeys sortedArrayUsingComparator:^NSComparisonResult(NSString * _Nonnull obj1, NSString * _Nonnull obj2) {
                return [obj1 compare:obj2];
            }];
        }
        [requestParameterKeys enumerateObjectsUsingBlock:^(NSString *  _Nonnull key, NSUInteger idx, BOOL * _Nonnull stop) {
            [mString appendFormat:@"&%@=%@",key, config.requestParameters[key]];
        }];
        cacheKey = [self md5WithString:[mString copy]];
        HHNetworkCache *cache = [HHCacheManager objcetForKey:cacheKey];
        if (!cache.isValid) {
            [HHCacheManager removeObejectForKey:cacheKey];
        } else {
            
            completionHandler ? completionHandler(nil, cache.data) : nil;
            return @-1;
        }
    }
    
    NSMutableArray *taskIdentifier = [NSMutableArray arrayWithObject:@-1];
    taskIdentifier[0] = [[HHNetworkClient sharedInstance] dispatchTaskWithUrlPath:config.urlPath useHttps:config.useHttps requestType:config.requestType params:config.requestParameters header:config.requestHeader completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
        
        if (!error && config.cacheValidTimeInterval > 0) {
            
            HHNetworkCache *cache = [HHNetworkCache cacheWithData:responseObject validTimeInterval:config.cacheValidTimeInterval];
            [HHCacheManager setObjcet:cache forKey:cacheKey];
        }
        
        [self.loadingTaskIdentifies removeObject:taskIdentifier.firstObject];
        completionHandler ? completionHandler([self formatError:error], responseObject) : nil;
    }];
    [self.loadingTaskIdentifies addObject:taskIdentifier.firstObject];
    return taskIdentifier.firstObject;
複製代碼

簡單定義一個HHCache對象, 存放緩存數據, 緩存時間, 緩存時效, 而後HHNetworkCacheManager單例對象內部用NSCache存儲緩存對象, 由於NSCache自帶線程安全特效, 連鎖都不用.

在任務發起以前咱們檢查一下是否有可用緩存, 有可用緩存直接返回, 沒有就走網絡, 網絡任務成功後存一下請求數據便可.

3.請求結果的格式化

網絡任務完成後帶回的數據以什麼樣的形式返回給調用方, 分兩種狀況: 任務成功和任務失敗.這裏咱們定義一下任務成功和失敗, 成功表示網絡請求成功且帶回了可用數據, 失敗表示未獲取到可用數據. 舉個例子: 獲取一個話題列表, 用戶但願看到的看到是一排排彩色頭像, 若是你調用API拿不到這一堆數據那對於用戶來講就是失敗的. 那麼沒拿到數據多是網絡出錯了, 或者網絡沒有問題只是用戶沒有關注過任何話題, 那麼相應的展現網絡錯誤提示或者推薦話題提示.

任務成功的話很簡單, 直接作相應JSON解析正常返回就行, 若是某個XXXAPI有特殊需求那就新加一個XXXAPIConfig繼承APIConfig基類, 在裏面添加屬性或者方法描述一下你有什麼特殊需求, XXXAPI負責格式好返回就好了(因此仍是一個API一個類好, 乾淨).

任務失敗的話就麻煩一點, 我但願任何API都能友好的返回錯誤提示, 具體的, 若是有錯誤發生了, 那麼返回給調用方的error.code必定是可讀的枚舉而不是301之類的須要比對文檔的錯誤碼(必須), error.domain一般就是錯誤提示語(可選), 這就要求程序員寫每一個API時都定義好錯誤枚舉(因此仍是一個API一個類好, 乾淨)和相應的錯誤提示.大概是這樣子:

//HHNetworkTaskError.h 通用錯誤
typedef enum : NSUInteger {
    HHNetworkTaskErrorTimeOut = 101,
    HHNetworkTaskErrorCannotConnectedToInternet = 102,
    HHNetworkTaskErrorCanceled = 103,
    HHNetworkTaskErrorDefault = 104,
    HHNetworkTaskErrorNoData = 105,
    HHNetworkTaskErrorNoMoreData = 106
} HHNetworkTaskError;

static NSError *HHError(NSString *domain, int code) {
    return [NSError errorWithDomain:domain code:code userInfo:nil];
}

static NSString *HHNoDataErrorNotice = @"這裏什麼也沒有~";
static NSString *HHNetworkErrorNotice = @"當前網絡差, 請檢查網絡設置~";
static NSString *HHTimeoutErrorNotice = @"請求超時了~";
static NSString *HHDefaultErrorNotice = @"請求失敗了~";
static NSString *HHNoMoreDataErrorNotice = @"沒有更多了~";
複製代碼
- (NSError *)formatError:(NSError *)error {
    
    if (error != nil) {
        switch (error.code) {
            case NSURLErrorCancelled: {
                error = HHError(HHDefaultErrorNotice, HHNetworkTaskErrorCanceled);
            }   break;
                
            case NSURLErrorTimedOut: {
                error = HHError(HHTimeoutErrorNotice, HHNetworkTaskErrorTimeOut);
            }   break;
                
            case NSURLErrorCannotFindHost:
            case NSURLErrorCannotConnectToHost:
            case NSURLErrorNotConnectedToInternet: {//應產品要求, 全部連不上服務器都是用戶網絡的問題
                error = HHError(HHNetworkErrorNotice, HHNetworkTaskErrorCannotConnectedToInternet);
            }   break;
                
            default: {
                error = HHError(HHNoDataErrorNotice, HHNetworkTaskErrorDefault);
            }   break;
        }
    }
    return error;
}
複製代碼

通用的錯誤枚舉和提示語定義在一個.h中, 之後有新增通用描述都在這裏添加, 便於管理. HHAPIManager基類會先格式好某些通用錯誤, 而後各個子類定義本身特有的錯誤枚舉(不可和通用描述衝突)和錯誤描述, 像這樣:

//HHTopicAPIManager.h
typedef enum : NSUInteger {
    HHUserInfoTaskErrorNotExistUserId = 1001,//用戶不存在
    HHUserInfoTaskError1,//瞎寫的, 意思到就行
    HHUserInfoTaskError2
} HHUserInfoTaskError;

typedef enum : NSUInteger {
    HHUserFriendListTaskError0 = 1001,
    HHUserFriendListTaskError1,
    HHUserFriendListTaskError2,
} HHTopicListTaskError;
複製代碼
//HHTopicAPIManager.m
- (NSNumber *)fetchUserInfoWithUserId:(NSUInteger)userId completionHandler:(HHNetworkTaskCompletionHander)completionHandler {
    
    HHDataAPIConfiguration *config = [HHDataAPIConfiguration new];
    config.urlPath = @"fetchUserInfoWithUserIdPath";
    config.requestParameters = nil;

    return [super dispatchDataTaskWithConfiguration:config completionHandler:^(NSError *error, id result) {
        
        if (!error) {//通用錯誤基類已經處理好, 作好本身的數據格式就行
            
            switch ([result[@"code"] integerValue]) {
                case 200: {
                    //                    請求數據無誤作相應解析
                    //                    result = [HHUser objectWithKeyValues:result[@"data"]];
                }   break;
                    
                case 301: {
                    error = HHError(@"用戶不存在", HHUserInfoTaskErrorNotExistUserId);
                }  break;
                    
                case 302: {
                    error = HHError(@"xxx錯誤", HHUserInfoTaskError1);
                }   break;
                    
                case 303: {
                    error = HHError(@"yyy錯誤", HHUserInfoTaskError2);
                }   break;
                default:break;
            }
        }
        completionHandler ? completionHandler(error, result) : nil;
    }];
}

複製代碼

而後調用方通常狀況下只須要這樣:

[[HHTopicAPIManager new] fetchUserInfoWithUserId:123 completionHandler:^(NSError *error, id result) {
       error ? [self showToastWithText:error.domain] : [self reloadTableViewWithNames:result];
    }];
複製代碼

固然, 狀況複雜的話只能這樣, 代碼多一點, 可是有枚舉讀起來也不麻煩:

[[HHTopicAPIManager new] fetchUserInfoWithUserId:123 completionHandler:^(NSError *error, id result) {
        error ? [self showErrorViewWithError:error] : [self reloadTableViewWithNames:result];
    }];

- (void)showErrorViewWithError:(NSError *)error {

    switch (error.code) {//若是狀況複雜就本身switch
                case HHNetworkTaskErrorTimeOut: {
                    //                    展現請求超時錯誤頁面
                }   break;
                case HHNetworkTaskErrorCannotConnectedToInternet: {
                    //                    展現網絡錯誤頁面
                }
                case HHUserInfoTaskErrorNotExistUserId: {
                    //                    ...
                }
                    //                    ...
                default:break;
            }
}
複製代碼

這裏多扯兩句, 請求的回調我是以(error, id)的形式返回的, 而不是像AFN那樣分別給出successBlock和failBlock. 其實我自己是很支持AFN的作法的, 區分紅功和錯誤強行讓兩種業務的代碼出如今兩個不一樣的部分, 這很好, 不一樣的業務處理就該在不一樣函數/方法裏面.

可是實際開發中有不少成功和失敗都會執行的操做, 典型的例子就是HUD, 兩個block的話我須要在兩個地方都加上[HUD hide], 這樣的代碼寫的多了就會很煩, 而我又懶, 因此就成功失敗都在一個回調返回了.

可是! 你也應該區分不一樣的業務寫出兩個不一樣方法(像上面那樣作), 至於公用的部分就只寫一次就夠了.像這樣:

[hud show:YES];
[[HHTopicAPIManager new] fetchUserInfoWithUserId:123 completionHandler:^(NSError *error, id result) {
      [hud hide:YES];
       error ? [self showToastWithText:error.domain] : [self reloadTableViewWithNames:result];
    }];
複製代碼

再說一句, 即便你比我還懶, 不聲明兩個方法那也應該將較短的邏輯寫在前面, 較長的寫在後面, 易讀, 像這樣:

if (!error) {
            ...短
            ...短
        } else {
            
            switch (error.code) {//若是狀況複雜就本身switch
                case HHNetworkTaskErrorTimeOut: {
                    //                    展現請求超時錯誤頁面
                }   break;
                case HHNetworkTaskErrorCannotConnectedToInternet: {
                    //                    展現網絡錯誤頁面
                }
                case HHUserInfoTaskErrorNotExistUserId: {
                    //                    ...長
                }
                    //                    ...長
                default:break;
            }
        }
    }
複製代碼

4.兩個小玩意兒

文章到這基本上這個網絡層該說的都說的差很少了, 各位能夠根據本身的需求改動改動就能用了, 最後簡單介紹下兩個和它相關的小玩意兒就結尾吧:

  • HHNetworkTaskGroup
@protocol HHNetworkTask <NSObject>

- (void)cancel;
- (void)resume;

@end

@interface HHNetworkTaskGroup : NSObject

- (void)addTaskWithMessgeType:(NSInteger)type message:(id)message completionHandler:(HHNetworkTaskCompletionHander)completionHandler;
- (void)addTask:(id<HHNetworkTask>)task;

- (void)cancel;
- (void)dispatchWithNotifHandler:(void(^)(void))notifHandler;

@end
複製代碼
@interface HHNetworkTaskGroup ()

@property (copy, nonatomic) void(^notifHandler)(void);
@property (assign, nonatomic) NSInteger signal;
@property (strong, nonatomic) NSMutableSet *tasks;
@property (strong, nonatomic) dispatch_semaphore_t lock;

@property (strong, nonatomic) id keeper;

@end

@implementation HHNetworkTaskGroup

//- (void)addTaskWithMessgeType:(HHSocketMessageType)type message:(PBGeneratedMessage *)message completionHandler:(HHNetworkCompletionHandler)completionHandler {
//    
//    HHSocketTask *task = [[HHSocketManager sharedManager] taskWithMessgeType:type message:message completionHandler:completionHandler];
//    [self addTask:task];
//}

- (void)addTask:(id<HHNetworkTask>)task {
    
    if ([task respondsToSelector:@selector(cancel)] &&
        [task respondsToSelector:@selector(resume)] &&
        ![self.tasks containsObject:task]) {
        
        [self.tasks addObject:task];
        [(id)task addObserver:self forKeyPath:NSStringFromSelector(@selector(state)) options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
    }
}

- (void)dispatchWithNotifHandler:(void (^)(void))notifHandler {
    
    if (self.tasks.count == 0) {
        
        dispatch_async(dispatch_get_main_queue(), ^{
            notifHandler ? notifHandler() : nil;
        });
        return;
    }
    
    self.lock = dispatch_semaphore_create(1);
    self.keeper = self;
    self.signal = self.tasks.count;
    self.notifHandler = notifHandler;
    for (id<HHNetworkTask> task in self.tasks.allObjects) {
        [task resume];
    }
}

- (void)cancel {
    
    for (id<HHNetworkTask> task in self.tasks.allObjects) {
        
        if ([(id)task state] < NSURLSessionTaskStateCanceling) {
            
            [(id)task removeObserver:self forKeyPath:NSStringFromSelector(@selector(state))];
            [task cancel];
        }
    }
    [self.tasks removeAllObjects];
    self.keeper = nil;
}

#pragma mark - KVO

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:NSStringFromSelector(@selector(state))]) {
        
        NSURLSessionTaskState oldState = [change[NSKeyValueChangeOldKey] integerValue];
        NSURLSessionTaskState newState = [change[NSKeyValueChangeNewKey] integerValue];
        if (oldState != newState && newState >= NSURLSessionTaskStateCanceling) {
            [object removeObserver:self forKeyPath:NSStringFromSelector(@selector(state))];
            
            dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER);
            self.signal--;
            dispatch_semaphore_signal(self.lock);

            if (self.signal == 0) {
                
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    
                    self.notifHandler ? self.notifHandler() : nil;
                    [self.tasks removeAllObjects];
                    self.keeper = nil;
                });
            }
        }
    }
}

#pragma mark - Getter

- (NSMutableSet *)tasks {
    if (!_tasks) {
        _tasks = [NSMutableSet set];
    }
    return _tasks;
}

@end
複製代碼

看名字應該就知道這個是和dispatch_group_notif差很少的東西, 不過是派發的對象不是dispatch_block_t而是id. 代碼很簡單, 說說思路就好了.

  • keeper 系統大部分帶有Block的API都有一個特性就是隻須要生成不須要持有, 也不用擔憂Block持有咱們的對象而形成循環引用, 例如:dispatch_async, dataTaskWithURL:completionHandler:等等, 其實具體的實現就是先循環引用再破除循環引用, 好比dispatch_async的queue和block會循環引用, 這樣在block執行期間雙方都不會釋放, 而後等到block執行完成後再將queue.block置nil破除循環引用, block沒了, 那它捕獲的queue和其餘對象計數都能-1,也就都能正常釋放了.代碼裏面的keeper就是來製造這個循環引用的.

  • signal和tasks signal其實就是tasks.count, 爲何咱們不直接在task完成後直接tasks.remove而後判斷tasks.count == 0而是要間接給一個signal來作這事兒? 緣由很簡單: forin過程當中是不能改變容器對象的. 當咱們forin派發task的時候, task是異步執行的, 有可能在task執行完成觸發KVO的時候咱們的forin還在遍歷, 此時直接remove就會crash. 若是不用forin, 而是用while或者for(;;)就會漏發. 因此就聲明一個signal來作計數了. 另外addObserve和removeObserve必須成對出現, 控制好就行.

  • dispatch_after 在全部任務執行完成後並無立刻執行notif(), 而是等待0.1秒之後再執行notif(), 這是由於task.state的設置會在task.completionHandler以前執行, 因此咱們須要等一下, 確認completionHandler執行後在走咱們的notif().

  • 如何使用

HHNetworkTaskGroup *group = [HHNetworkTaskGroup new];
    HHTopicAPIManager *manager = [HHTopicAPIManager new];
    for (int i = 1; i < 6; i++) {
        
        NSURLSessionDataTask *task = [manager topicListDataTaskWithPage:i pageSize:20 completionHandler:^(NSError *error, id result) {
            //...completionHandler... i
        }];
        
        [group addTask:(id)task];
    }
    [group dispatchWithNotifHandler:^{
        //notifHandler
    }];
複製代碼

強調一下, 絕對不該該直接調用HHNetworkClient或者HHAPIManger的dataTaskxxx...這些通用接口來生成task, 應該在該task所屬的API暴露接口生成task, 簡單說就是不要跨層訪問. 每一個API的參數甚至簽名規則都是不同的, API的調用方應該只提供生成task的相應參數而不該該也不須要知道這些參數具體的拼裝邏輯.

  • HHNetworkAPIRecorder
@interface HHNetworkAPIRecorder : NSObject

@property (strong, nonatomic) id rawValue;
@property (assign, nonatomic) int pageSize;
@property (assign, nonatomic) int currentPage;
@property (assign, nonatomic) NSInteger itemsCount;
@property (assign, nonatomic) NSInteger lastRequestTime;

- (void)reset;
- (BOOL)hasMoreData;
- (NSInteger)maxPage;
@end
複製代碼

平常請求中有不少接口涉及到分頁, 然而毫無疑問分頁的邏輯在每一個頁面都是如出一轍的, 可是卻須要每一個調用頁面都保持一下currentPage而後調用邏輯都寫一次, 其實直接在API內部實現一下分頁的邏輯, 而後對外暴露第一頁和下一頁的接口就不用聲明currentPage和重複這些無聊的邏輯了. 像這樣:

//XXXAPI.h
- (NSNumber *)refreshTopicListWithCompletionHandler:(HHNetworkTaskCompletionHander)completionHandler;//第一頁
- (NSNumber *)loadmoreTopicListWithCompletionHandler:(HHNetworkTaskCompletionHander)completionHandler;//當前頁的下一頁
- (NSNumber *)fetchTopicListWithPage:(NSInteger)page completionHandler:(HHNetworkTaskCompletionHander)completionHandler;//指定頁(通常外部用不到, 看狀況暴露)
複製代碼
//XXXAPI.m
- (NSNumber *)refreshTopicListWithCompletionHandler:(HHNetworkTaskCompletionHander)completionHandler {
    
    [self.topicListAPIRecorder reset];
    return [self fetchTopicListWithPage:self.topicListAPIRecorder.currentPage completionHandler:completionHandler];
}

- (NSNumber *)loadmoreTopicListWithCompletionHandler:(HHNetworkTaskCompletionHander)completionHandler {
    
    self.topicListAPIRecorder.currentPage++;
    return [self fetchTopicListWithPage:self.topicListAPIRecorder.currentPage completionHandler:completionHandler];
}
複製代碼
//SomeViewController
self.topicAPIManager = [HHTopicAPIManager new];
...
self.tableView.header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{//下拉刷新
        [weakSelf.topicAPIManager refreshTopicListWithCompletionHandler:^(NSError *error, id result) {
                ...
        }];
    }];
self.tableView.footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{//上拉加載
        [weakSelf.topicAPIManager loadmoreTopicListWithCompletionHandler:^(NSError *error, id result) {
                ...
        }];
    }];
複製代碼

總結

HHURLRequestGenerator: 網絡請求的生成器, 公用的請求頭, cookie都在此設置.

HHNetworkClient: 網絡請求的派發器, 這裏會記錄每個服役中的請求, 並在必要的時候切換服務器.

HHAPIManager: 網絡請求派發器的調用者, 這裏對請求的結果作相應的數據格式化後返回給API調用方, 提供請求模塊的拓展性支持, 並提供合理的Task供TaskGroup派發.

本文附帶的demo地址

一步一步構建你的iOS網絡層 - TCP篇

相關文章
相關標籤/搜索