AFNetworking 3.0 源碼解讀 總結(干貨)(上)


養成記筆記的習慣,對於一個軟件工程師來說,我覺得很重要。記得在知乎上看到過一個問題,說是人類最大的缺點是什么?我個人覺得記憶算是一個缺點。它就像時間一樣,會自己消散。

前言

終於寫完了 AFNetworking 的源碼解讀。這一過程耗時數天。當我回過頭又重頭到尾的讀了一篇,又有所收獲。不禁讓我想起了當初上學時的種種情景。我們應該對知識進行反復的記憶和理解。下邊是我總結的 AFNetworking 中能夠學到的知識點。

1.枚舉(enum)

使用原則:當滿足一個有限的並具有統一主題的集合的時候,我們就考慮使用枚舉。這在很多框架中都驗證了這個原則。最重要的是能夠增加程序的可讀性

示例代碼:

/**
 *  網絡類型 (需要封裝為一個自己的枚舉)
 */
typedef NS_ENUM(NSInteger, AFNetworkReachabilityStatus) {
    /**
     *  未知
     */
    AFNetworkReachabilityStatusUnknown          = -1,
    /**
     *  無網絡
     */
    AFNetworkReachabilityStatusNotReachable     = 0,
    /**
     *  WWAN 手機自帶網絡
     */
    AFNetworkReachabilityStatusReachableViaWWAN = 1,
    /**
     *  WiFi
     */
    AFNetworkReachabilityStatusReachableViaWiFi = 2,
};

2.注釋

我們必須知道一個事實,注釋的代碼是不會編譯到目標文件的,因此放心大膽的注釋吧。在平日里的開發中,應該經常問問自己是否把每段代碼都當成寫API那樣對待?

曾經看過兩種不同的說辭,一種是說把代碼注釋盡量少些,要求代碼簡介可讀性強。另一種是說注釋要詳細,着重考慮他人讀代碼的感受。個人感覺還是寫詳細一點比較好,因為可能過一段時間之后,自己再去看自己當時寫的代碼可能就不記得了。很有可能在寫這些繁瑣的注釋的過程中,能夠想到些什么,比如如何合並掉一些沒必要的方法等等。

示例代碼:

/*!
	@header SCNetworkReachability
	@discussion The SCNetworkReachability API allows an application to
		determine the status of a system's current network
		configuration and the reachability of a target host.
		In addition, reachability can be monitored with notifications
		that are sent when the status has changed.

		"Reachability" reflects whether a data packet, sent by
		an application into the network stack, can leave the local
		computer.
		Note that reachability does <i>not</i> guarantee that the data
		packet will actually be received by the host.
 */

/*!
	@typedef SCNetworkReachabilityRef
	@discussion This is the handle to a network address or name.
 */
typedef const struct CF_BRIDGED_TYPE(id) __SCNetworkReachability * SCNetworkReachabilityRef;

3.BOOL屬性的property書寫規則

通常我們在定義一個BOOL屬性的時候,要自定義getter方法,這樣做的目的是為了增加程序的可讀性。Apple中的代碼也是這么寫的。

示例代碼:

/**
 Whether or not the network is currently reachable.
 */
@property (readonly, nonatomic, assign, getter = isReachable) BOOL reachable;

// setter
self.reachable = YES;
// getter
if (self.isReachable) {}

4.按功能區分代碼

假如我們寫的一個控制器中大概有500行代碼,我們應該保證能夠快速的找到我們需要查找的內容,這就需要把代碼按照功能來分隔。

通常在.h中 我們可以使用一個自定義的特殊的注釋來分隔,在.m中使用#pragma mark -來分隔。

示例代碼:

///---------------------
/// @name Initialization
///---------------------

///------------------------------
/// @name Evaluating Server Trust
///------------------------------

#pragma mark - UI
...設置UI相關
#pragma mark - Data
...處理數據
#pragma mark - Action
...點擊事件

5.通知

我們都知道通知可以用來傳遞事件和數據,但要想用好它,也不太容易。在 AFNetworking 事件和數據的傳遞使用的是通知和Block,按照AFNetworking對通知的使用習慣。我總結了幾點:

  • 原則:如果我們需要傳遞事件或數據,可采用代理和Block,同時額外增加一個通知。因為通知具有跨多個界面的優點。
  • 釋放問題:在接收通知的頁面,一定要記得移除監聽。
  • 使用方法:在.h中 FOUNDATION_EXPORT + NSString * const +通知名 在.m中賦值。如果在別的頁面用到這個通知,使用extern + NSString * const +通知名就可以了。

ps: FOUNDATION_EXPORT 和#define 都能定義常量。FOUNDATION_EXPORT 能夠使用==進行判斷,效率略高。而且能夠隱藏定義細節(就是實現部分不在.中)

示例代碼:

FOUNDATION_EXPORT NSString * const AFNetworkingReachabilityDidChangeNotification;
FOUNDATION_EXPORT NSString * const AFNetworkingReachabilityNotificationStatusItem;
/**
 *  網絡環境發生改變的時候接受的通知
 */
NSString * const AFNetworkingReachabilityDidChangeNotification = @"com.alamofire.networking.reachability.change";
/**
 *  網絡環境發生變化是會發送一個通知,同時攜帶一組狀態數據,根據這個key來去除網絡status
 */
NSString * const AFNetworkingReachabilityNotificationStatusItem = @"AFNetworkingReachabilityNotificationStatusItem";

6.國際化的問題

我個人認為在開發一個APP之初,就應該考慮國際化的問題,不管日后會不會用到這個功能。當你有了國際化的思想之后,在對控件進行布局的時候,就會比只在一種語言下考慮的更多,這會讓一個人對控件布局的視野更加寬闊。好了,這個問題就說這么多。有興趣的朋友請自行查找相關內容。

7.私有方法

在開發中,難免會使用私有方法來協助我們達到某種目的或獲取某個數據。在oc中,我看到很多人都會這樣寫:- (void)funName {}。個人是不贊成這樣寫了,除非方法內部使用了self。總之,類似於這樣的方法,其實跟我們的業務並沒有太大的關系。我進入一個控制器的文件中,目光應該集中在業務代碼上才對。

AFNetworking 中,一般都會把私有方法,也可以叫函數,放到頭部,你即使不看這些代碼,對於整個業務的理解也不會受到影響。所以,這種寫法值得推薦。可以適當的使用內聯函數,提高效率.

示例代碼:

/**
 *  把枚舉的值轉換成字符串
 */
NSString * AFStringFromNetworkReachabilityStatus(AFNetworkReachabilityStatus status) {
    switch (status) {
        case AFNetworkReachabilityStatusNotReachable:
            return NSLocalizedStringFromTable(@"Not Reachable", @"AFNetworking", nil);
        case AFNetworkReachabilityStatusReachableViaWWAN:
            return NSLocalizedStringFromTable(@"Reachable via WWAN", @"AFNetworking", nil);
        case AFNetworkReachabilityStatusReachableViaWiFi:
            return NSLocalizedStringFromTable(@"Reachable via WiFi", @"AFNetworking", nil);
        case AFNetworkReachabilityStatusUnknown:
        default:
            return NSLocalizedStringFromTable(@"Unknown", @"AFNetworking", nil);
    }
}

- (NSString *)AFStringFromNetworkReachabilityStatus:(AFNetworkReachabilityStatus)status {
    switch (status) {
        case AFNetworkReachabilityStatusNotReachable:
            return NSLocalizedStringFromTable(@"Not Reachable", @"AFNetworking", nil);
        case AFNetworkReachabilityStatusReachableViaWWAN:
            return NSLocalizedStringFromTable(@"Reachable via WWAN", @"AFNetworking", nil);
        case AFNetworkReachabilityStatusReachableViaWiFi:
            return NSLocalizedStringFromTable(@"Reachable via WiFi", @"AFNetworking", nil);
        case AFNetworkReachabilityStatusUnknown:
        default:
            return NSLocalizedStringFromTable(@"Unknown", @"AFNetworking", nil);
    }
}

8.SCNetworkReachabilityRef(網絡監控核心實現)

SCNetworkReachabilityRef 是獲取網絡狀態的核心對象,創建這個對象有兩個方法:

  • SCNetworkReachabilityCreateWithName
  • SCNetworkReachabilityCreateWithAddress

我們看看實現網絡監控的核心代碼:

示例代碼:

- (void)startMonitoring {
    [self stopMonitoring];

    if (!self.networkReachability) {
        return;
    }

    __weak __typeof(self)weakSelf = self;
    AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
        __strong __typeof(weakSelf)strongSelf = weakSelf;

        strongSelf.networkReachabilityStatus = status;
        if (strongSelf.networkReachabilityStatusBlock) {
            strongSelf.networkReachabilityStatusBlock(status);
        }

    };

    SCNetworkReachabilityContext context = {0, (__bridge void *)callback, AFNetworkReachabilityRetainCallback, AFNetworkReachabilityReleaseCallback, NULL};
    SCNetworkReachabilitySetCallback(self.networkReachability, AFNetworkReachabilityCallback, &context);
    SCNetworkReachabilityScheduleWithRunLoop(self.networkReachability, CFRunLoopGetMain(), kCFRunLoopCommonModes);

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0),^{
        SCNetworkReachabilityFlags flags;
        if (SCNetworkReachabilityGetFlags(self.networkReachability, &flags)) {
            AFPostReachabilityStatusChange(flags, callback);
        }
    });
}

上邊的方法中涉及了一些 CoreFoundation 的知識,我們來看看:

SCNetworkReachabilityContext點進去,會發現這是一個結構體,一般c語言的結構體是對要保存的數據的一種描述

示例代碼:

typedef struct {
	CFIndex		version;
	void *		__nullable info;
	const void	* __nonnull (* __nullable retain)(const void *info);
	void		(* __nullable release)(const void *info);
	CFStringRef	__nonnull (* __nullable copyDescription)(const void *info);
} SCNetworkReachabilityContext;
  1. 第一個參數接受一個signed long 的參數
  2. 第二個參數接受一個void * 類型的值,相當於oc的id類型,void * 可以指向任何類型的參數
  3. 第三個參數 是一個函數 目的是對info做retain操作
  4. 第四個參數是一個函數,目的是對info做release操作
  5. 第五個參數是 一個函數,根據info獲取Description字符串

設置網絡監控分為下邊幾個步驟:

1.我們先新建上下文

   SCNetworkReachabilityContext context = {0, (__bridge void *)callback, AFNetworkReachabilityRetainCallback, AFNetworkReachabilityReleaseCallback, NULL};

2.設置回調

SCNetworkReachabilitySetCallback(self.networkReachability, AFNetworkReachabilityCallback, &context);

3.加入RunLoop池

SCNetworkReachabilityScheduleWithRunLoop(self.networkReachability, CFRunLoopGetMain(), kCFRunLoopCommonModes);

9.鍵值依賴

注冊鍵值依賴,這個可能大家平時用的比較少。可以了解一下。舉個例子:

比如說一個類User中有兩個屬性
還有一個卡片的類card
我們寫一個info的setter 和 getter 方法,

這么做的目的是,如果我監聽info這個屬性,當user中的name或者age有一個改變了,能夠出發info的這個監聽事件。

示例代碼:

@interface User :NSObject
@property (nonatomic,copy)NSString *name;
@property (nonatomic,assign)NSUInteger age;
@end



@interface card :NSObject
@property (nonatomic,copy)NSString *info;
@property (nonatomic,strong)User *user;
@end
@implementation card

- (NSString *)info {
    return [NSString stringWithFormat:@"%@/%lu",_user.name,(unsigned long)_user.age];
}
- (void)setInfo:(NSString *)info {
    
    NSArray *array = [info componentsSeparatedByString:@"/"];
    _user.name = array[0];
    _user.age = [array[1] integerValue];
    
}

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    NSArray * moreKeyPaths = nil;

    if ([key isEqualToString:@"info"])
    {
        moreKeyPaths = [NSArray arrayWithObjects:@"user.name", @"user.age", nil];
    }

    if (moreKeyPaths)
    {
        keyPaths = [keyPaths setByAddingObjectsFromArray:moreKeyPaths];
    }
    
    return keyPaths;
}

@end

10.HTTP

  1. HTTP協議用於客戶端和服務器端之間的通信
  2. 通過請求和相應的交換達成通信
  3. HTTP是不保存狀態的協議
    • HTTP自身不會對請求和相應之間的通信狀態進行保存。什么意思呢?就是說,當有新的請求到來的時候,HTTP就會產生新的響應,對之前的請求和響應的保溫信息不做任何存儲。這也是為了快速的處理事務,保持良好的可伸展性而特意設計成這樣的。
  4. 請求URI定位資源
    • URI算是一個位置的索引,這樣就能很方便的訪問到互聯網上的各種資源。
  5. 告知服務器意圖的HTTP方法
    • ①GET: 直接訪問URI識別的資源,也就是說根據URI來獲取資源。
    • ②POST: 用來傳輸實體的主體。
    • ③PUT: 用來傳輸文件。
    • ④HEAD: 用來獲取報文首部,和GET方法差不多,只是響應部分不會返回主體內容。
    • ⑤DELETE: 刪除文件,和PUT恰恰相反。按照請求的URI來刪除指定位置的資源。
    • ⑥OPTIONS: 詢問支持的方法,用來查詢針對請求URI指定的資源支持的方法。
    • ⑦TRACE: 追蹤路徑,返回服務器端之前的請求通信環信息。
    • ⑧CONNECT: 要求用隧道協議連接代理,要求在與代理服務器通信時建立隧道,實現用隧道協議進行TCP通信。SSL(Secure Sockets Layer)和TLS(Transport Layer Security)就是把通信內容加密后進行隧道傳輸的。
  6. 管線化讓服務器具備了相應多個請求的能力
  7. Cookie讓HTTP有跡可循

11.HTTPS

HTTPS是一個通信安全的解決方案,可以說相對已經非常安全。為什么它會是一個很安全的協議呢?下邊會做出解釋。大家可以看看這篇文章,解釋的很有意思 。《簡單粗暴系列之HTTPS原理》.

HTTP + 加密 + 認證 + 完整性保護 = HTTPS

其實HTTPS是身披SSL外殼的HTTP,這句話怎么理解呢?

大家應該都知道HTTP是應用層的協議,但HTTPS並非是應用層的一種新協議,只是HTTP通信接口部分用SSL或TLS協議代替而已。

通常 HTTP 直接和TCP通信,當使用SSL時就不同了。要先和SSL通信,再由SSL和TCP通信。

這里再說一些關於加密的題外話:

現如今,通常加密和解密的算法都是公開的。舉個例子: a * b = 200,加入a是你知道的密碼,b是需要被加密的數據,200 是加密后的結果。那么這里這個*號就是一個很簡單的加密算法。這個算法是如此簡單。但是如果想要在不知道a和b其中一個的情況下進行破解也是很困難的。就算我們知道了200 然后得到a b 這個也很難。假設知道了密碼a 那么b就很容易算出b = 200 / a 。

實際中的加密算法比這個要復雜的多。

介紹兩種常用加密方法:

  1. 共享密鑰加密

  2. 公開密鑰加密

共享密鑰加密就是加密和解密通用一個密鑰,也稱為對稱加密。優點是加密解密速度快,缺點是一旦密鑰泄露,別人也能解密數據。

公開密鑰加密恰恰能解決共享密鑰加密的困難,過程是這樣的:

  • ①發文方使用對方的公開密鑰進行加密

  • ②接受方在使用自己的私有密鑰進行解密

關於公開密鑰,也就是非對稱加密 可以看看這篇文章 RSA算法原理

原理都是一樣的,這個不同於剛才舉得a和b的例子,就算知道了結果和公鑰,破解出被機密的數據是非常難的。這里邊主要涉及到了復雜的數學理論。

HTTPS采用混合加密機制

HTTPS采用共享密鑰加密和公開密鑰加密兩者並用的混合加密機制。

注意黃色的部分,這個指明了,我們平時使用的一個場景。這篇文章會很長,不僅僅是為了解釋HTTPS,還為了能夠增加記憶,當日后想看看的時候,就能通過讀這邊文章想起大部分的HTTPS的知識。下邊解釋一些更加詳細的HTTPS過程。

12.如何獲取證書中的PublicKey

// 在證書中獲取公鑰
static id AFPublicKeyForCertificate(NSData *certificate) {
    id allowedPublicKey = nil;
    SecCertificateRef allowedCertificate;
    SecCertificateRef allowedCertificates[1];
    CFArrayRef tempCertificates = nil;
    SecPolicyRef policy = nil;
    SecTrustRef allowedTrust = nil;
    SecTrustResultType result;

    // 1. 根據二進制的certificate生成SecCertificateRef類型的證書
    // NSData *certificate 通過CoreFoundation (__bridge CFDataRef)轉換成 CFDataRef
    // 看下邊的這個方法就可以知道需要傳遞參數的類型
    /* 
     SecCertificateRef SecCertificateCreateWithData(CFAllocatorRef __nullable allocator,
     CFDataRef data) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_2_0);
     */
    allowedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificate);
    
    // 2.如果allowedCertificate為空,則執行標記_out后邊的代碼
    __Require_Quiet(allowedCertificate != NULL, _out);

    // 3.給allowedCertificates賦值
    allowedCertificates[0] = allowedCertificate;
    
    // 4.新建CFArra: tempCertificates
    tempCertificates = CFArrayCreate(NULL, (const void **)allowedCertificates, 1, NULL);

    // 5. 新建policy為X.509
    policy = SecPolicyCreateBasicX509();
    
    // 6.創建SecTrustRef對象,如果出錯就跳到_out標記處
    __Require_noErr_Quiet(SecTrustCreateWithCertificates(tempCertificates, policy, &allowedTrust), _out);
    // 7.校驗證書的過程,這個不是異步的。
    __Require_noErr_Quiet(SecTrustEvaluate(allowedTrust, &result), _out);

    // 8.在SecTrustRef對象中取出公鑰
    allowedPublicKey = (__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust);

_out:
    if (allowedTrust) {
        CFRelease(allowedTrust);
    }

    if (policy) {
        CFRelease(policy);
    }

    if (tempCertificates) {
        CFRelease(tempCertificates);
    }

    if (allowedCertificate) {
        CFRelease(allowedCertificate);
    }

    return allowedPublicKey;
}

在二進制的文件中獲取公鑰的過程是這樣

  • ① NSData *certificate -> CFDataRef -> (SecCertificateCreateWithData) -> SecCertificateRef allowedCertificate
  • ②判斷SecCertificateRef allowedCertificate 是不是空,如果為空,直接跳轉到后邊的代碼
  • ③allowedCertificate 保存在allowedCertificates數組中
  • ④allowedCertificates -> (CFArrayCreate) -> SecCertificateRef allowedCertificates[1]
  • ⑤根據函數SecPolicyCreateBasicX509() -> SecPolicyRef policy
  • ⑥SecTrustCreateWithCertificates(tempCertificates, policy, &allowedTrust) -> 生成SecTrustRef allowedTrust
  • ⑦SecTrustEvaluate(allowedTrust, &result) 校驗證書
  • ⑧(__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust) -> 得到公鑰id allowedPublicKey

這個過程我們平時也不怎么用,了解下就行了,真需要的時候知道去哪里找資料就行了。

這里邊值得學習的地方是:

__Require_Quiet 和 __Require_noErr_Quiet 這兩個宏定義。

我們看看他們內部是怎么定義的


可以看出這個宏的用途是:當條件返回false時,執行標記以后的代碼


可以看出這個宏的用途是:當條件拋出異常時,執行標記以后的代碼

這樣就有很多使用場景了。當必須要對條件進行判斷的時候,我們有下邊幾種方案了

  1. #ifdef 這個是編譯特性

  2. if else 代碼層次的判斷

  3. __Require_XXX


_out 就是一個標記,這段代碼__Require_Quiet 到_out之間的代碼不會執行

13.URL編碼

關於什么叫URI編碼和為什么要編碼,請看我轉載的這篇文章url 編碼(percentcode 百分號編碼)
根據RFC 3986的規定:URL百分比編碼的保留字段分為:

  • ':' '#' '[' ']' '@' '?' '/'
  • '!' '$' '&' ''' '(' ')' '*' '+' ',' ';' '='

在對查詢字段百分比編碼時,'?'和'/'可以不用編碼,其他的都要進行編碼。我記得在使用支付寶支付時,在對數據進行URL編碼時要求編碼'/'.

NSString * AFPercentEscapedStringFromString(NSString *string) {
    static NSString * const kAFCharactersGeneralDelimitersToEncode = @":#[]@"; // does not include "?" or "/" due to RFC 3986 - Section 3.4
    static NSString * const kAFCharactersSubDelimitersToEncode = @"!$&'()*+,;=";
    // '?'和'/'在query查詢允許不被轉譯,因此!$&'()*+,;=和:#[]@都要被轉譯,也就是在URLQueryAllowedCharacterSet中刪除掉這些字符
    NSMutableCharacterSet * allowedCharacterSet = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
    [allowedCharacterSet removeCharactersInString:[kAFCharactersGeneralDelimitersToEncode stringByAppendingString:kAFCharactersSubDelimitersToEncode]];

	// FIXME: https://github.com/AFNetworking/AFNetworking/pull/3028
    // return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];
    static NSUInteger const batchSize = 50;
    NSUInteger index = 0;
    NSMutableString *escaped = @"".mutableCopy;

    while (index < string.length) {
        //http://www.jianshu.com/p/eb03e20f7b1c
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wgnu"
        NSUInteger length = MIN(string.length - index, batchSize);
#pragma GCC diagnostic pop
        NSRange range = NSMakeRange(index, length);
        // To avoid breaking up character sequences such as 👴🏻👮🏽
        range = [string rangeOfComposedCharacterSequencesForRange:range];

        NSString *substring = [string substringWithRange:range];
        NSString *encoded = [substring stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];
        [escaped appendString:encoded];

        index += range.length;
    }
	return escaped;
}

上邊的這個方法可以作為URL編碼的通用方法,可以直接使用,也可以寫到NSString的分類中。YYModel就有這個方法。

這里值得注意的是:

  • 字符串需要經過過濾 ,過濾法則通過 NSMutableCharacterSet 實現。添加規則后,只對規則內的因子進行編碼。
  • 為了處理類似emoji這樣的字符串,rangeOfComposedCharacterSequencesForRange 使用了while循環來處理,也就是把字符串按照batchSize分割處理完再拼回。

14.HTTPBody

我們有必要了解下請求提body的組成部分。先看下一個HTTTP請求是什么樣的?

某app的一個登錄POST請求:

POST / HTTP/1.1
Host: log.nuomi.com
Content-Type: multipart/form-data; boundary=Boundary+6D3E56AA6EAA83B7
Cookie: access_log=7bde65268e2260bb0a85c7de2c67c468; BAIDUID=428D86FDBA6028DE2A5496BE3E7FC308:FG=1; BAINUOCUID=4368e1b7499c455dcd437da336ca1ca9feb8f57d; BDUSS=Ecwa3NvN1NjNWhsVGxWZktFfkc2bzJxQjZ3RFJpTFBiUzZqZUJZU0ZTSmZsN0ZXQVFBQUFBJCQAAAAAAAAAAAEAAABxbLRYWXV1dXV3dXV1AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF8KilZfCopWR; bn_na_copid=60139b4b2ba75706fc384d987c2e4007; bn_na_ctag=W3siayI6Imljb25fMSIsInMiOiJ0dWFuIiwidiI6IjMyNiIsInQiOiIxNDUxODg2OTE0In1d; channel=user_center%7C%7C; channel_content=; channel_webapp=webapp; condition=6.0.3; domainUrl=sh; na_qab=6be39bfce918bb7b51887412e009faa6; UID=1488219249
Connection: keep-alive
Accept: */*
User-Agent: Bainuo/6.1.0 (iPhone; iOS 9.0; Scale/2.00)
Accept-Language: zh-Hans-CN;q=1, en-CN;q=0.9
Content-Length: 22207
Accept-Encoding: gzip, deflate

--Boundary+6D3E56AA6EAA83B7 /// 開始
Content-Disposition: form-data; name="app_version"

6.1.0
--Boundary+6D3E56AA6EAA83B7

HTTP請求頭我們就暫時不說了,看這個body的內容

--Boundary+6D3E56AA6EAA83B7 /// 開始
Content-Disposition: form-data; name="app_version"

6.1.0
--Boundary+6D3E56AA6EAA83B7

組成分為4個部分: 1.初始邊界 2.body頭 3.body 4.結束邊界。 下邊就會用着這些知識。

15.保證方法在主線程執行

有時候我們必須要確保某個方法在主線程調用,就可以使用下邊的思路來做。

- (BOOL)transitionToNextPhase {
    
    // 保證代碼在主線程
    if (![[NSThread currentThread] isMainThread]) {
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self transitionToNextPhase];
        });
        return YES;
    }
}

16.代碼跟思想的碰撞

示例代碼:

- (BOOL)transitionToNextPhase {
    
    // 保證代碼在主線程
    if (![[NSThread currentThread] isMainThread]) {
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self transitionToNextPhase];
        });
        return YES;
    }

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wcovered-switch-default"
    switch (_phase) {
        case AFEncapsulationBoundaryPhase:
            _phase = AFHeaderPhase;
            break;
        case AFHeaderPhase:  // 打開流,准備接受數據
            [self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
            [self.inputStream open];
            _phase = AFBodyPhase;
            break;
        case AFBodyPhase: // 關閉流
            [self.inputStream close];
            _phase = AFFinalBoundaryPhase;
            break;
        case AFFinalBoundaryPhase:
        default:
            _phase = AFEncapsulationBoundaryPhase;
            break;
    }
    // 重置offset
    _phaseReadOffset = 0;
#pragma clang diagnostic pop

    return YES;
}

回過頭來看這段代碼,我又有新的想法。原本對數據的操作,對body的操作,是一件很復雜的事情。但作者的思路非常清晰。就像上邊這個方法一樣,它只實現一個功能,就是切換body組成部分。它只做了這一件事,我們在開發中,如遇到有些復雜的功能,在寫方法的時候,可能考慮了很多東西,當時所有的考慮可能都寫到一個方法中了。

能不能寫出一個思路圖,先不管思路的實現如何,先一一列出來,最后在一一實現,一一拼接起來。

17.NSInputStream

NSInputStream有好幾種類型,根據不同的類型返回不同方法創建的NSInputStream

示例代碼:

- (NSInputStream *)inputStream {
    if (!_inputStream) {
        if ([self.body isKindOfClass:[NSData class]]) {
            _inputStream = [NSInputStream inputStreamWithData:self.body];
        } else if ([self.body isKindOfClass:[NSURL class]]) {
            _inputStream = [NSInputStream inputStreamWithURL:self.body];
        } else if ([self.body isKindOfClass:[NSInputStream class]]) {
            _inputStream = self.body;
        } else {
            _inputStream = [NSInputStream inputStreamWithData:[NSData data]];
        }
    }

    return _inputStream;
}

18.對文件的操作

  1. NSParameterAssert() 用來判斷參數是否為空,如果為空就拋出異常
  2. 使用isFileURL 判斷一個URL是否為fileURL 使用checkResourceIsReachableAndReturnError判斷路徑能夠到達
  3. 使用 [[NSFileManager defaultManager] attributesOfItemAtPath:[fileURL path] error:error] 獲取本地文件屬性
  4. lastPathComponent ,https://www.baidu.com/abc.html 結果就是abc.html
  5. pathExtension https://www.baidu.com/abc.html 結果就是html

19.NSURLRequestCachePolicy緩存策略

這個要仔細介紹下,在某些特殊的場景下還是能用到的。我們點開NSURLRequestCachePolicy 可以看到是一個枚舉值

typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy)
{
    NSURLRequestUseProtocolCachePolicy = 0,

    NSURLRequestReloadIgnoringLocalCacheData = 1,
    NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, // Unimplemented
    NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,

    NSURLRequestReturnCacheDataElseLoad = 2,
    NSURLRequestReturnCacheDataDontLoad = 3,

    NSURLRequestReloadRevalidatingCacheData = 5, // Unimplemented
};

NSURLRequestUseProtocolCachePolicy 這個是默認的緩存策略,緩存不存在,就請求服務器,緩存存在,會根據response中的Cache-Control字段判斷下一步操作,如: Cache-Control字段為must-revalidata, 則詢問服務端該數據是否有更新,無更新的話直接返回給用戶緩存數據,若已更新,則請求服務端。

  • NSURLRequestReloadIgnoringLocalCacheData 這個策略是不管有沒有本地緩存,都請求服務器。
  • NSURLRequestReloadIgnoringLocalAndRemoteCacheData 這個策略會忽略本地緩存和中間代理 直接訪問源server
  • NSURLRequestReturnCacheDataElseLoad 這個策略指,有緩存就是用,不管其有效性,即Cache-Control字段 ,沒有就訪問源server
  • NSURLRequestReturnCacheDataDontLoad 這個策略只加載本地數據,不做其他操作,適用於沒有網路的情況
  • NSURLRequestReloadRevalidatingCacheData 這個策略標示緩存數據必須得到服務器確認才能使用,未實現。

20.管線化

在HTTP連接中,一般都是一個請求對應一個連接,每次建立tcp連接是需要一定時間的。管線化,允許一次發送一組請求而不必等到相應。但由於目前並不是所有的服務器都支持這項功能,因此這個屬性默認是不開啟的。管線化使用同一tcp連接完成任務,因此能夠大大提交請求的時間。但是響應要和請求的順序 保持一致才行。使用場景也有,比如說首頁要發送很多請求,可以考慮這種技術。但前提是建立連接成功后才可以使用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM