AFNetworking源碼:AFNetworking中的那些巧妙設(shè)計

???我們都知道AFNetworking是一個非常好用且常見的網(wǎng)絡(luò)庫,那么AFNetworking的開發(fā)者是如何做到的呢?AFNetworking中有哪些巧妙設(shè)計是我們還不知道时鸵,以后開發(fā)中可以借鑒的呢缸废?
???這篇文章將不定期更新AFNetworking中那些巧妙的設(shè)計仆百,如果你覺得有哪些設(shè)計是我沒收錄的糖权,也可留言以告訴我堵腹。

一.利用runtime黑魔法

  • 1.方法交換(swizzle)

目的:

???這里方法替換的目的主要是想在調(diào)用系統(tǒng)的NSURLSessionTask 的resume方法時,能夠發(fā)送AFNSURLSessionTaskDidResumeNotification通知星澳,以達(dá)到監(jiān)測系統(tǒng)方法調(diào)用的目的疚顷。

實(shí)現(xiàn):

???_AFURLSessionTaskSwizzling類在+load方法中將_AFURLSessionTaskSwizzling 中的af_resume方法與NSURLSessionTask的resume方法交換。

@interface _AFURLSessionTaskSwizzling : NSObject

@end

@implementation _AFURLSessionTaskSwizzling
+ (void)load {
    if (NSClassFromString(@"NSURLSessionTask")) {
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
        NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wnonnull"
        //通過[session dataTaskWithURL:nil]得到一個NSURLSessionDataTask實(shí)例
        NSURLSessionDataTask *localDataTask = [session dataTaskWithURL:nil];
#pragma clang diagnostic pop
        IMP originalAFResumeIMP = method_getImplementation(class_getInstanceMethod([self class], @selector(af_resume)));
        //通過NSURLSessionDataTask實(shí)例的class獲得當(dāng)前的類
        Class currentClass = [localDataTask class];
        
        //while循環(huán)確保每個類的resume都會被替換。
        while (class_getInstanceMethod(currentClass, @selector(resume))) {
            Class superClass = [currentClass superclass];
            IMP classResumeIMP = method_getImplementation(class_getInstanceMethod(currentClass, @selector(resume)));
            IMP superclassResumeIMP = method_getImplementation(class_getInstanceMethod(superClass, @selector(resume)));
            if (classResumeIMP != superclassResumeIMP &&
                originalAFResumeIMP != classResumeIMP) {
                [self swizzleResumeAndSuspendMethodForClass:currentClass];
            }
            currentClass = [currentClass superclass];
        }
        
        [localDataTask cancel];
        [session finishTasksAndInvalidate];
    }
}

- (void)af_resume {
    NSAssert([self respondsToSelector:@selector(state)], @"Does not respond to state");
    NSURLSessionTaskState state = [self state];
    [self af_resume];
    
    if (state != NSURLSessionTaskStateRunning) {
        [[NSNotificationCenter defaultCenter] postNotificationName:AFNSURLSessionTaskDidResumeNotification object:self];
    }
}
疑問

???通常我們需要實(shí)現(xiàn)這種操作的方式是實(shí)現(xiàn)一個子類腿堤,然后使用的時候使用子類阀坏。但是AFNetworking并不想改變我們使用NSURLSessionTask的方式,所以采用了這種巧妙的方式笆檀。
到這里大部分人可能會有以下三個疑問忌堂,理解了這幾個疑問也就理解了為什么說這里設(shè)計很巧妙。

  • a .為什么要在+load中實(shí)現(xiàn)交換?
    因?yàn)閘oad方法的實(shí)現(xiàn)肯定是在方法調(diào)用之前酗洒,在這里實(shí)現(xiàn)交換可以確保調(diào)用在交換之后發(fā)生士修。
    load方法里實(shí)現(xiàn)還有一個好處,那就這個方法由系統(tǒng)自動調(diào)用樱衷,不用去在乎調(diào)用時機(jī)和由誰發(fā)起調(diào)用棋嘲。_AFURLSessionTaskSwizzling是一個內(nèi)嵌類,也就是說這個類只在.m中定義和實(shí)現(xiàn)不需要暴露給用戶矩桂,這個類的唯一作用就是替換方法沸移,也不需要被實(shí)例化或者被別的實(shí)例引用。

  • b.af_resume的實(shí)現(xiàn)里又調(diào)用了[self af_resume]侄榴,不會造成死循環(huán)嗎雹锣?
    解釋這個問題很簡單,因?yàn)橹婪椒ń粨Q的原理就不難理解了牲蜀。
    替換之前:


    NSURLSessionTask的resume.png

替換之后:


resume的調(diào)用.png

可以看到
???1 . 給resume發(fā)送消息的時候笆制,實(shí)際是調(diào)用af_resume的實(shí)現(xiàn)。
???2 .在af_resume中給af_resume 發(fā)送消息涣达,實(shí)際是調(diào)用resume的實(shí)現(xiàn)在辆。

  • c.為什么想替換NSURLSessionTask的resume方法沒有直接使用NSURLSessionTask類,而是通過遍歷localDataTask的父類逐級替換度苔?
    這個疑問其實(shí)在代碼注釋中已經(jīng)給出了解釋:
/**
         iOS 7 and iOS 8 differ in NSURLSessionTask implementation, which makes the next bit of code a bit tricky.
         Many Unit Tests have been built to validate as much of this behavior has possible.
         Here is what we know:
            - NSURLSessionTasks are implemented with class clusters, meaning the class you request from the API isn't actually the type of class you will get back.
            - Simply referencing `[NSURLSessionTask class]` will not work. You need to ask an `NSURLSession` to actually create an object, and grab the class from there.
            - On iOS 7, `localDataTask` is a `__NSCFLocalDataTask`, which inherits from `__NSCFLocalSessionTask`, which inherits from `__NSCFURLSessionTask`.
            - On iOS 8, `localDataTask` is a `__NSCFLocalDataTask`, which inherits from `__NSCFLocalSessionTask`, which inherits from `NSURLSessionTask`.
            - On iOS 7, `__NSCFLocalSessionTask` and `__NSCFURLSessionTask` are the only two classes that have their own implementations of `resume` and `suspend`, and `__NSCFLocalSessionTask` DOES NOT CALL SUPER. This means both classes need to be swizzled.
            - On iOS 8, `NSURLSessionTask` is the only class that implements `resume` and `suspend`. This means this is the only class that needs to be swizzled.
            - Because `NSURLSessionTask` is not involved in the class hierarchy for every version of iOS, its easier to add the swizzled methods to a dummy class and manage them there.
        
         Some Assumptions:
            - No implementations of `resume` or `suspend` call super. If this were to change in a future version of iOS, we'd need to handle it.
            - No background task classes override `resume` or `suspend`
         
         The current solution:
            1) Grab an instance of `__NSCFLocalDataTask` by asking an instance of `NSURLSession` for a data task.
            2) Grab a pointer to the original implementation of `af_resume`
            3) Check to see if the current class has an implementation of resume. If so, continue to step 4.
            4) Grab the super class of the current class.
            5) Grab a pointer for the current class to the current implementation of `resume`.
            6) Grab a pointer for the super class to the current implementation of `resume`.
            7) If the current class implementation of `resume` is not equal to the super class implementation of `resume` AND the current implementation of `resume` is not equal to the original implementation of `af_resume`, THEN swizzle the methods
            8) Set the current class to the super class, and repeat steps 3-8
         */

大意是:
???1. 在OC的實(shí)現(xiàn)中匆篓,NSURLSessionTask的類并不是NSURLSessionTask而是依靠類族.
也就是[NSURLSessionTask class]返回的結(jié)果并不是我們想要的結(jié)果,__NSCFURLSessionTask才是實(shí)際的類寇窑。
???2. iOS8中的resmue是唯一的實(shí)現(xiàn)鸦概,而iOS7中__NSCFLocalSessionTask并沒有調(diào)用super的resume,__NSCFURLSessionTask和__NSCFLocalSessionTask都實(shí)現(xiàn)了resume甩骏,所以需要循環(huán)調(diào)用superclass把兩個實(shí)現(xiàn)都替換掉窗市。
所以開發(fā)者采用了這種方式確保所有版本的所有resume方法都會被替換掉。

關(guān)于method swizzle饮笛,AFNetworking的作者M(jìn)attt大神在這篇文章中已經(jīng)講的很清楚了:
https://nshipster.com/method-swizzling/

  • 2.關(guān)聯(lián)變量

目的

在UIImageView的分類中的類方法中咨察,給UIImageView的類添加關(guān)聯(lián)變量。

實(shí)現(xiàn)
@implementation UIImageView (AFNetworking)
+ (AFImageDownloader *)sharedImageDownloader {
    return objc_getAssociatedObject([UIImageView class], @selector(sharedImageDownloader)) ?: [AFImageDownloader defaultInstance];
}
+ (void)setSharedImageDownloader:(AFImageDownloader *)imageDownloader {
    objc_setAssociatedObject([UIImageView class], @selector(sharedImageDownloader), imageDownloader, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
疑問

同樣的理解了一下幾個疑問的原因福青,也就知道了設(shè)計的巧妙之處摄狱。

  • a .為什么要用分類和關(guān)聯(lián)變量?
    不用改變UIImageView的類脓诡,也不用繼承。
  • b .為什么要在UIImageView類中添加關(guān)聯(lián)變量?
    因?yàn)檫@個imageDownloader是屬于所有UIImageView的媒役,并不屬于某一個UIImageView的實(shí)例祝谚。也就是說所有UIImageView的實(shí)例都是使用這個imageDownloader去請求圖片。所以把imageDownloader與UIImageView的類關(guān)聯(lián)是合理的酣衷。
  • c .這么做的好處交惯?
    調(diào)用imageDownloader有類似單例的便捷:
- (void)testResponseIsNilWhenLoadedFromCache {
    AFImageDownloader *downloader = [UIImageView sharedImageDownloader]; 
...
}

其實(shí)這里設(shè)計的巧妙之處不僅是這些,關(guān)聯(lián)變量的key使用@selector(sharedImageDownloader)也是一個很巧妙的應(yīng)用穿仪,因?yàn)檫@樣就不需要單獨(dú)去聲明一個key商玫,而且利用了屬性本身的名稱,即簡單又明了牡借。
關(guān)于關(guān)聯(lián)變量的使用拳昌,Mattt大神有一篇文章專門講到了:
https://nshipster.com/associated-objects/
感興趣的可以去看看。

二.充分利用GCD

???我們都知道AFNetworking中使用GCD和NSOpreationQueue來管理多線程的钠龙。這么做的原因一是GCD性能強(qiáng)大炬藤,內(nèi)核直接調(diào)度線程。二是GCD和NSOpreationQueue使用起來及其簡單碴里,編程人員不用直接調(diào)度管理線程卻可以實(shí)現(xiàn)多線程編程沈矿。

  • 1.圖片緩存AFAutoPurgingImageCache的實(shí)現(xiàn)

??? AFAutoPurgingImageCache,是一個可以自動清理的緩存工具咬腋。允許我們設(shè)置一個最大內(nèi)存容量和一個刻度容量(刻度容量就是在緩存達(dá)到最大容量的時候羹膳,觸發(fā)自動清理內(nèi)存,清理后剩下的容量小于等于刻度容量)根竿。而且這個工具是支持多線程的陵像。
??? AFAutoPurgingImageCache看似強(qiáng)大,以為實(shí)現(xiàn)起來會很復(fù)雜寇壳。但是看了源碼的代碼量時還是被震驚了醒颖,內(nèi)部實(shí)現(xiàn)非常簡單明了,代碼量也很小壳炎。建議大家結(jié)合源碼一起看泞歉,因?yàn)檫@里這個類的只用了不到200行代碼就實(shí)現(xiàn)了!看起來并不會困難匿辩。(真正的大神并不是寫的代碼復(fù)雜的讓人看不懂腰耙,反而是寫完之后讓人一眼就能看能懂。)

@interface AFAutoPurgingImageCache ()
@property (nonatomic, strong) NSMutableDictionary <NSString* , AFCachedImage*> *cachedImages;
@property (nonatomic, assign) UInt64 currentMemoryUsage;
@property (nonatomic, strong) dispatch_queue_t synchronizationQueue;
@end

???AFAutoPurgingImageCache 有一個叫做synchronizationQueue的私有屬性铲球。不用被它的名字欺騙挺庞,它并不是一個同步隊列,其實(shí)它是一個并發(fā)隊列:

self.synchronizationQueue = dispatch_queue_create([queueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);

???作者巧妙的利用了GCD的barrier(內(nèi)存柵欄)來實(shí)現(xiàn)讀寫的同步化睬辐。
簡單的說內(nèi)存柵欄的作用就是可以把一個異步隊列分隔開挠阁,保證在柵欄前的所有追加操作完成之后再執(zhí)行barrier追加的操作,這個操作執(zhí)行完成以后溯饵,在barrier之后追加的操作繼續(xù)異步執(zhí)行侵俗。
關(guān)于內(nèi)存柵欄的詳細(xì)描述我這里就不展開了。

- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier {
    //增加一個圖片緩存
    dispatch_barrier_async(self.synchronizationQueue, ^{
        AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];

        AFCachedImage *previousCachedImage = self.cachedImages[identifier];
        if (previousCachedImage != nil) {
            self.currentMemoryUsage -= previousCachedImage.totalBytes;
        }

        self.cachedImages[identifier] = cacheImage;
        self.currentMemoryUsage += cacheImage.totalBytes;
    });
    //每次增加的時候檢查是否超出了最大容量丰刊,如果超出就移除直到內(nèi)存小于刻度內(nèi)存容量
    dispatch_barrier_async(self.synchronizationQueue, ^{
        if (self.currentMemoryUsage > self.memoryCapacity) {
            UInt64 bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge;
            NSMutableArray <AFCachedImage*> *sortedImages = [NSMutableArray arrayWithArray:self.cachedImages.allValues];
            NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastAccessDate"
                                                                           ascending:YES];
            [sortedImages sortUsingDescriptors:@[sortDescriptor]];

            UInt64 bytesPurged = 0;

            for (AFCachedImage *cachedImage in sortedImages) {
                [self.cachedImages removeObjectForKey:cachedImage.identifier];
                bytesPurged += cachedImage.totalBytes;
                if (bytesPurged >= bytesToPurge) {
                    break;
                }
            }
            self.currentMemoryUsage -= bytesPurged;
        }
    });
}
- (BOOL)removeImageWithIdentifier:(NSString *)identifier {
    __block BOOL removed = NO;
    dispatch_barrier_sync(self.synchronizationQueue, ^{
        AFCachedImage *cachedImage = self.cachedImages[identifier];
        if (cachedImage != nil) {
            [self.cachedImages removeObjectForKey:identifier];
            self.currentMemoryUsage -= cachedImage.totalBytes;
            removed = YES;
        }
    });
    return removed;
}

???作者在增加或減少緩存的時候都使用了dispatch_barrier_(a)sync方法追加操作隘谣,確保增加或減少緩存的操作是同步的,并且這個操作是在之前所有異步操作完成之后再執(zhí)行的啄巧。這么做就可以放心的異步讀取了:

- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier {
    __block UIImage *image = nil;
    dispatch_sync(self.synchronizationQueue, ^{
        AFCachedImage *cachedImage = self.cachedImages[identifier];
        image = [cachedImage accessImage];
    });
    return image;
}

下圖大概表示了增加緩存寻歧,讀取緩存然后刪除緩存的一個過程:


未命名文件-3.png

使用內(nèi)存柵欄不僅提高了讀寫效率,還有個好處是由于增加和減少緩存是同步實(shí)現(xiàn)的秩仆,所以不需要對緩存的字典用鎖码泛,因?yàn)閮?nèi)存柵欄本來就是一種同步機(jī)制

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市澄耍,隨后出現(xiàn)的幾起案子噪珊,更是在濱河造成了極大的恐慌,老刑警劉巖齐莲,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件痢站,死亡現(xiàn)場離奇詭異,居然都是意外死亡选酗,警方通過查閱死者的電腦和手機(jī)阵难,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來芒填,“玉大人呜叫,你說我怎么就攤上這事〉钏ィ” “怎么了怀偷?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長播玖。 經(jīng)常有香客問我椎工,道長,這世上最難降的妖魔是什么蜀踏? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任维蒙,我火速辦了婚禮,結(jié)果婚禮上果覆,老公的妹妹穿的比我還像新娘颅痊。我一直安慰自己,他們只是感情好局待,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布斑响。 她就那樣靜靜地躺著菱属,像睡著了一般。 火紅的嫁衣襯著肌膚如雪舰罚。 梳的紋絲不亂的頭發(fā)上纽门,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天,我揣著相機(jī)與錄音营罢,去河邊找鬼赏陵。 笑死,一個胖子當(dāng)著我的面吹牛饲漾,可吹牛的內(nèi)容都是我干的蝙搔。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼考传,長吁一口氣:“原來是場噩夢啊……” “哼吃型!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起僚楞,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤败玉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后镜硕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體运翼,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年兴枯,在試婚紗的時候發(fā)現(xiàn)自己被綠了血淌。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡财剖,死狀恐怖悠夯,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情躺坟,我是刑警寧澤沦补,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站咪橙,受9級特大地震影響夕膀,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜美侦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一产舞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧菠剩,春花似錦易猫、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽哈蝇。三九已至,卻和暖如春攘已,著一層夾襖步出監(jiān)牢的瞬間炮赦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工贯被, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人妆艘。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓彤灶,卻偏偏與公主長得像,于是被迫代替她去往敵國和親批旺。 傳聞我的和親對象是個殘疾皇子幌陕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容