AFNetworking二次封裝的那些事

AFNetworking但是iOS網絡開發的神器,大大簡便了操做.不過網絡但是重中之重,不能只會用AFNetworking.我以爲網絡開發首先要懂基本的理論,例如tcp/ip,http協議,以後要了解web的請求和響應,會使用蘋果自帶的NSURLSession,最後是把AFNetworking的源碼啃掉.php

前言

一直以來網絡開發用的都是前面同事基於AFNetworking二次封裝好的框架,一直都沒什麼問題,也就沒往深處去了解.而後公司開始新項目了,iOS端由我負責,這但是個人第一次啊,從零開始,構建整個項目.這是個挑戰,心裏仍是有點小激動的.
python

輪子確定是不用重複造的,網絡框架就拿的老項目的,結果出現了兩個問題.web

  • 上傳多張圖片,服務端解析不了
  • 無文件上傳, Content-Type仍是multipart/form-data

爲了解決這個問題,從就沒用過的NSURLSession到http協議,追本溯源,終於解決了.json

http協議

關於http協議的理論就很少講了,主要就講使用POST方法傳遞數據時,發送的請求頭和請求體.數組

Content-Type

咱們提交的數據是什麼編碼方式服務端是不知道的,其實咱們徹底能夠自定義格式,只要服務端取到數據後能解析就能夠了.
通常服務端語言如 php、python 等,以及它們的 framework,都內置了自動解析常見數據格式的功能。因此咱們通常都用那幾種常見的數據格式,數據格式就是請求頭裏的Content-Type,服務端根據Content-Type來解析請求體裏的數據.
通常有四種最多見的方式:服務器

  • application/x-www-form-urlencoded
    這是默認的方式,以key1=val1&key2=val2的方式進行編碼.
    country=0&pro=1023&city=102301
  • multipart/form-data
    這個首先生成了一個 boundary 用於分割不一樣的字段,爲了不與正文內容重複, boundary 很長很複雜,通常有文件上傳的時候用這種方法
--Boundary+A675D0398A56493A
  Content-Disposition: form-data; name="photoFiles";    filename="photoFiles.jpeg"
  Content-Type: image/jpeg
  • application/json
    這個很是常見了,感受它適合格式支持比鍵值對複雜得多的結構化數據.
    {"country":0,"pro":1023,"city":102301}
  • text/xml
    它是一種使用 HTTP 做爲傳輸協議,XML 做爲編碼方式的遠程調用規範.
    移動端通常不會用它,太臃腫了,加的標籤徹底是浪費.

因此傳文件用multipart/form-data.
application/json和application/x-www-form-urlencoded差很少,可是application/json數據更加直觀,更適合結構更加複雜的數據.
我特意抓包了網易新聞等app,沒有文件的狀況下是application/json網絡

NSMutableURLRequest設置

既然知道了請求頭,請求體.那麼給服務器端發送的請求按照上面的格式設置便可.app

//上傳一張圖片爲例
    NSMutableURLRequest *request= [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:5.0f];
    request.HTTPMethod=@"POST";
   
    //數據體設置
    NSMutableData *dataM=[NSMutableData data];
    NSString *strTop=[NSString stringWithFormat:@"--%@\nContent-Disposition: form-data; name=\"file1\"; filename=\"%@\"\nContent-Type: %@\n\n",kBOUNDARY_STRING,fileName,@"image/jpg"];
    NSString *strBottom=[NSString stringWithFormat:@"\n--%@--",kBOUNDARY_STRING];
    NSString *filePath=[[NSBundle mainBundle] pathForResource:fileName ofType:nil];
    NSData *fileData=[NSData dataWithContentsOfFile:filePath];
    [dataM appendData:[strTop dataUsingEncoding:NSUTF8StringEncoding]];
    [dataM appendData:fileData];
    [dataM appendData:[strBottom dataUsingEncoding:NSUTF8StringEncoding]];

    
    //經過請求頭設置
    [request setValue:[NSString stringWithFormat:@"%lu",(unsigned long)dataM.length] forHTTPHeaderField:@"Content-Length"];
    [request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@",kBOUNDARY_STRING] forHTTPHeaderField:@"Content-Type"];
    
    //設置數據體
    request.HTTPBody=dataM;

AFNetworking

AFNetworking就是基於NSMutableURLRequest,NSURLSession的封裝,很是好用.
以POST爲例,它有兩個方法:框架

- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
                             parameters:(nullable id)parameters
                               progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
                                success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
                                failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;

- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
                             parameters:(nullable id)parameters
              constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
                               progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
                                success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
                                failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;

區別就在於constructingBodyWithBlock,這也是我遇到的兩個問題的根源.tcp

往下看,會發現,第一個方法:
[mutableRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];

而第二個方法:

//先執行咱們傳進去的block
if (block) {
    block(formData);
}
return [formData requestByFinalizingMultipartFormData];

//若是參數爲空,則content-type爲默認的,不爲空則爲multipart/form-data
- (NSMutableURLRequest *)requestByFinalizingMultipartFormData {
    if ([self.bodyStream isEmpty]) {
        return self.request;
    }

    // Reset the initial and final boundaries to ensure correct Content-Length
    [self.bodyStream setInitialAndFinalBoundaries];
    [self.request setHTTPBodyStream:self.bodyStream];

    [self.request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", self.boundary] forHTTPHeaderField:@"Content-Type"];
    [self.request setValue:[NSString stringWithFormat:@"%llu", [self.bodyStream contentLength]] forHTTPHeaderField:@"Content-Length"];

    return self.request;
}

這樣個人問題就迎刃而解了.
前同事爲了統一處理,全部的請求都是經第二個函數,因此即便不是傳文件,若是有參數, Content-Type仍是multipart/form-data.
而block是統一處理的:

if ([obj isKindOfClass:[UIImage class]]) {
                UIImage *image = obj;
                if (image.size.height > 1080 || image.size.width > 1080) {
                    
                    image = [image imageWithMaxSide:1080];
                    
                    [formData appendPartWithFileData:UIImageJPEGRepresentation(image, 0.3f) name:key fileName:[NSString stringWithFormat:@"%@.jpeg",key] mimeType:@"image/jpeg"];
                } else if (image.size.height > 600 || image.size.width > 600)  {
                    [formData appendPartWithFileData:UIImageJPEGRepresentation(image, 0.5f) name:key fileName:[NSString stringWithFormat:@"%@.jpeg",key] mimeType:@"image/jpeg"];
                } else {
                    [formData appendPartWithFileData:UIImageJPEGRepresentation(image, 1) name:key fileName:[NSString stringWithFormat:@"%@.jpeg",key] mimeType:@"image/jpeg"];
                }
            }

只處理了傳一張照片,沒有考慮到傳數組裏面放多張圖片的狀況,因此服務器不知道這是圖片啊,怎麼解析呢!
上一個項目也沒有傳多張圖片的狀況,因此一直沒有發現這個問題.

最後的解決方法是:

.h  (繼承AFHTTPSessionManager)
//這個函數調用AF的第一個方法,不傳文件時調用
+ (void)requestDataWithHTTPPath:(NSString *)path
                     parameters:(NSDictionary *)parameters
                        success:(RequestSuccessBlock)success
                        failure:(RequestFailureBlock)failure;

//這個函數調用AF的第二個方法,傳文件時用
+ (void)uploadFileWithHTTPPath:(NSString *)path
                     parameters:(NSDictionary *)parameters
                        success:(RequestSuccessBlock)success
                        failure:(RequestFailureBlock)failure;  

.m
+ (void)requestDataWithHTTPPath:(NSString *)path
                     parameters:(NSDictionary *)parameters
                        success:(RequestSuccessBlock)success
                        failure:(RequestFailureBlock)failure {
    [KGAPIClient sharedRequestDataClient].isUploadFile = NO;
    [[KGAPIClient sharedRequestDataClient] requestDataWithHTTPPath:path parameters:parameters success:success failure:failure];
}

+ (void)uploadFileWithHTTPPath:(NSString *)path
                    parameters:(NSDictionary *)parameters
                       success:(RequestSuccessBlock)success
                       failure:(RequestFailureBlock)failure {
    [KGAPIClient sharedRequestDataClient].isUploadFile = YES;
    [[KGAPIClient sharedRequestDataClient] requestDataWithHTTPPath:path parameters:parameters success:success failure:failure];
}

//統一處理一些錯誤
#pragma mark - Request Method
- (void)requestDataWithHTTPPath:(NSString *)path
                     parameters:(NSDictionary *)parameters
                        success:(RequestSuccessBlock)success
                        failure:(RequestFailureBlock)failure {
    [self requestWithHTTPPath:path parameters:parameters success:^(NSURLSessionDataTask *task, id responseObject) {
        NSString *status = [responseObject valueForKey:kHTTPResponseStatsusKey];
        if (status.length) {
            if ([status isEqualToString:kHTTPSuccessCode0000]) {
                if (success) {
                    success(task, responseObject);
                }
            } else if ([status isEqualToString:kHTTPErrorCode0001]) {
                if (failure) {
                    [SVProgressHUD showErrorWithStatus:[responseObject valueForKey:kHTTPResponseMessageKey]];
                    failure(task, [self requestErrorWithDomin:[responseObject valueForKey:kHTTPResponseMessageKey] errorCode:nil]);
                }
            }//.......還有別的一些狀態碼
        } else {
            if (failure) {
                NSString *message = [responseObject valueForKey:kHTTPResponseMessageKey];
                failure(task, [NSError errorWithDomain:message code:[status integerValue] userInfo:responseObject]);
            }
        }
    } failure:^(NSURLSessionDataTask *task, NSError *error) {
        if (failure) {
            NSError *aError = [NSError errorWithDomain:@"網絡不給力" code:error.code userInfo:error.userInfo];
            failure(task, aError);
        }
    }];
}

//區分兩種POST方式
- (void)requestWithHTTPPath:(NSString *)path
                 parameters:(NSDictionary *)parameters
                    success:(RequestSuccessBlock)success
                    failure:(RequestFailureBlock)failure {
    NSDictionary *tmpDict = [KGAPIClient getNewParamsWithOldParams:parameters.mutableCopy];
    
    if (_isUploadFile) {
        [self POST:path parameters:tmpDict constructingBodyWithBlock:^(id<AFMultipartFormData>  _Nonnull formData) {
            [self appendPartWithparameters:parameters formData:formData];
        } progress:^(NSProgress * _Nonnull uploadProgress) {
            NSLog(@"******progress***");
        } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
            if (success) {
                success(task, responseObject);
            }
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
            if (failure) {
                failure(task, error);
            }
        }];
    }
    else {
        [self POST:path parameters:tmpDict progress:^(NSProgress * _Nonnull uploadProgress) {
            NSLog(@"******progress***");
        } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
            if (success) {
                success(task, responseObject);
            }
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
            if (failure && ![path isEqualToString:APIPathWithUserReportGeo] && ![path isEqualToString:APIPathWithUserNickExsits]) {
                failure(task, error);
            }
        }];
    }
}  

//用於設置data的type,還不全,暫時考慮image,image數組,.mp3文件.
- (void)appendPartWithparameters:(NSDictionary *)parameters formData:(id<AFMultipartFormData>  _Nonnull)formData {
    if (parameters) {
        [parameters enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            if ([obj isKindOfClass:[NSString class]] &&
                ([obj hasSuffix:@".png"] ||
                 [obj hasSuffix:@".jpg"] ||
                 [obj hasSuffix:@".jpeg"])) {
                    [formData appendPartWithFileURL:obj name:key fileName:[NSString stringWithFormat:@"%@.jpg",key] mimeType:@"image/jpeg" error:nil];
                }
            else if ([obj isKindOfClass:[UIImage class]]) {
                UIImage *image = obj;
                if (image.size.height > 1080 || image.size.width > 1080) {
                    
                    image = [image imageWithMaxSide:1080];
                    
                    [formData appendPartWithFileData:UIImageJPEGRepresentation(image, 0.3f) name:key fileName:[NSString stringWithFormat:@"%@.jpeg",key] mimeType:@"image/jpeg"];
                } else if (image.size.height > 600 || image.size.width > 600)  {
                    [formData appendPartWithFileData:UIImageJPEGRepresentation(image, 0.5f) name:key fileName:[NSString stringWithFormat:@"%@.jpeg",key] mimeType:@"image/jpeg"];
                } else {
                    [formData appendPartWithFileData:UIImageJPEGRepresentation(image, 1) name:key fileName:[NSString stringWithFormat:@"%@.jpeg",key] mimeType:@"image/jpeg"];
                }
            }
            else if ([obj isKindOfClass:[NSArray class]]) {
                NSArray *array = parameters[key];
                for (int i = 0; i < array.count; i++) {
                    id subvalue = array[i];
                    if ([subvalue isKindOfClass:[UIImage class]]) {
                        UIImage *image = subvalue;
                        if (image.size.height > 1080 || image.size.width > 1080) {
                            
                            image = [image imageWithMaxSide:1080];
                            
                            [formData appendPartWithFileData:UIImageJPEGRepresentation(image, 0.3f) name:key fileName:[NSString stringWithFormat:@"%@.jpeg",key] mimeType:@"image/jpeg"];
                        } else if (image.size.height > 600 || image.size.width > 600)  {
                            [formData appendPartWithFileData:UIImageJPEGRepresentation(image, 0.5f) name:key fileName:[NSString stringWithFormat:@"%@.jpeg",key] mimeType:@"image/jpeg"];
                        } else {
                            [formData appendPartWithFileData:UIImageJPEGRepresentation(image, 1) name:key fileName:[NSString stringWithFormat:@"%@.jpeg",key] mimeType:@"image/jpeg"];
                        }
                    }
                    else if ([subvalue isKindOfClass:[NSString class]] &&
                             ([subvalue hasSuffix:@".png"] ||
                              [subvalue hasSuffix:@".jpg"] ||
                              [subvalue hasSuffix:@".jpeg"])) {
                                 [formData appendPartWithFileURL:obj name:key fileName:[NSString stringWithFormat:@"%@.jpg",key] mimeType:@"image/jpeg" error:nil];
                             }
                }
            }
            else if ([obj isKindOfClass:[NSData class]]) {
                [formData appendPartWithFileData:obj name:key fileName:[NSString stringWithFormat:@"%@.mp3",key] mimeType:@"audio/mp3"];
            }
        }];
    }
}

對於AF的二次封裝不少,不過追本溯源就是NSURLSession,就是http協議.

結尾

此次獨立帶項目,真的學到了不少,明白了本身的不少不足.更讓我明白理論真的很重要.大學的課程真的有他的必要性.

相關文章
相關標籤/搜索