???我們都知道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
替換之后:
可以看到
???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;
}
下圖大概表示了增加緩存寻歧,讀取緩存然后刪除緩存的一個過程:
使用內(nèi)存柵欄不僅提高了讀寫效率,還有個好處是由于增加和減少緩存是同步實(shí)現(xiàn)的秩仆,所以不需要對緩存的字典用鎖码泛,因?yàn)閮?nèi)存柵欄本來就是一種同步機(jī)制