关闭

SDWebImage内部实现

108人阅读 评论(0) 收藏 举报
typedef NS_ENUM(NSInteger, SDImageCacheType) {
    /**
     * The image wasn't available the SDWebImage caches, but was downloaded from the web.
     该图像是不可用的SDWebImage缓存,但是从网络下载的.
     */
    SDImageCacheTypeNone,
    /**
     * The image was obtained from the disk cache.
     图像从磁盘高速缓存获得.
     */
    SDImageCacheTypeDisk,
    /**
     * The image was obtained from the memory cache.
     图像从存储器高速缓存获得
     */
    SDImageCacheTypeMemory
};

SDWebImage是iOS开发者经常使用的一个开源框架,这个框架的主要作用是:一个异步下载图片并且支持缓存的UIImageView分类。

UIImageView+WebCache

我们最常用的方法就是这个:

1
[_fineImageView sd_setImageWithURL:picURL placeholderImage:nil];

现在开始我们一步步地看这个方法的内部实现:

1
2
3
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
}

这里会调用下面这个方法:

1
2
3
4
- (void)sd_setImageWithURL:(NSURL *)url
          placeholderImage:(UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock

我们看UIImageView+WebCache.h文件,我们可以发现为开发者提供了很多类似于- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder的方法。

这些方法最终都会调用- sd_setImageWithURL: placeholderImage: options: progress: completed:,这个方法可以算是核心方法,下面我们看一下- sd_setImageWithURL: placeholderImage: options: progress: completed:

方法的实现:

首先执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//移除UIImageView当前绑定的操作.当TableView的cell包含的UIImageView被重用的时候首先执行这一行代码,保证这个ImageView的下载和缓存组合操作都被取消
[self sd_cancelCurrentImageLoad];
接下来我们来看看[self sd_cancelCurrentImageLoad]内部是怎么执行的:
- (void)sd_cancelCurrentImageLoad {
    [self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"];
}
//然后会调用UIView+WebCacheOperation的
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key
UIView+WebCacheOperation
下面我们先来看看UIView+WebCacheOperation里面都写了些什么:
UIView+WebCacheOperation这个分类提供了三个方法,用于操作绑定关系
#import #import "SDWebImageManager.h"
@interface UIView (WebCacheOperation)
/**
 *  Set the image load operation (storage in a UIView based dictionary)
设置图像加载操作(存储在和UIView做绑定的字典里面)
 *
 *  @param operation the operation
 *  @param key       key for storing the operation
 */
- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key;
/**
 *  Cancel all operations for the current UIView and key
  用这个key找到当前UIView上面的所有操作并取消
 *
 *  @param key key for identifying the operations
 */
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key;
/**
 *  Just remove the operations corresponding to the current UIView and key without cancelling them
 *
 *  @param key key for identifying the operations
 */
- (void)sd_removeImageLoadOperationWithKey:(NSString *)key;

为了方便管理和找到视图正在进行的一些操作,WebCacheOperation将每一个视图的实例和它正在进行的操作(下载和缓存的组合操作)绑定起来,实现操作和视图一一对应关系,以便可以随时拿到视图正在进行的操作,控制其取消等,如何进行绑定我们在下面分析:

UIView+WebCacheOperation.m文件内

- (NSMutableDictionary *)operationDictionary用到了中定义的两个函数:

  • objc_setAssociatedObject

  • objc_getAssociatedObject

NSObject+AssociatedObject.h

1
2
3
@interface NSObject (AssociatedObject)
@property (nonatomic, strong) id associatedObject;
@end

NSObject+AssociatedObject.m

1
2
3
4
5
6
7
8
@implementation NSObject (AssociatedObject)
@dynamic associatedObject;
-(void)setAssociatedObject:(id)object{
objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(id)associatedObject {
return objc_getAssociatedObject(self, @selector(associatedObject));
}

objc_setAssociatedObject作用是对已存在的类在扩展中添加自定义的属性,通常推荐的做法是添加属性的key最好是static char类型的,通常来说该属性的key应该是常量唯一的。

objc_getAssociatedObject根据key获得与对象绑定的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
- (NSMutableDictionary *)operationDictionary {
/*
这个loadOperationKey 的定义是:static char loadOperationKey;
它对应的绑定在UIView的属性是operationDictionary(NSMutableDictionary类型)
operationDictionary的value是操作,key是针对不同类型视图和不同类型的操作设定的字符串
注意:&是一元运算符结果是右操作对象的地址(&loadOperationKey返回static char loadOperationKey的地址)
*/
    NSMutableDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
//如果可以查到operations,就rerun,反正给视图绑定一个新的,空的operations字典
    if (operations) {
        return operations;
    }
    operations = [NSMutableDictionary dictionary];
    objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return operations;
}
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
    // 取消正在下载的队列
    NSMutableDictionary *operationDictionary = [self operationDictionary];
//如果 operationDictionary可以取到,根据key可以得到与视图相关的操作,取消他们,并根据key值,从operationDictionary里面删除这些操作
    id operations = [operationDictionary objectForKey:key];
    if (operations) {
        if ([operations isKindOfClass:[NSArray class]]) {
            for (id  operation in operations) {
                if (operation) {
                    [operation cancel];
                }
            }
        else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
            [(id) operations cancel];
        }
        [operationDictionary removeObjectForKey:key];
    }
}

接下来我们继续探索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- sd_setImageWithURL: placeholderImage: options: progress: completed:
- (void)sd_setImageWithURL:(NSURL *)url
          placeholderImage:(UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionBlock)completedBlock {
    [self sd_cancelCurrentImageLoad];
    //将 url作为属性绑定到ImageView上,用static char imageURLKey作key
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
/*options & SDWebImageDelayPlaceholder这是一个位运算的与操作,!(options & SDWebImageDelayPlaceholder)的意思就是options参数不是SDWebImageDelayPlaceholder,就执行以下操作
#define dispatch_main_async_safe(block)\
    if ([NSThread isMainThread]) {\
        block();\
    } else {\
        dispatch_async(dispatch_get_main_queue(), block);\
    }
*/

这是一个宏定义,因为图像的绘制只能在主线程完成,所以dispatch_main_sync_safe就是为了保证block在主线程中执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
//设置imageView的placeHolder
            self.image = placeholder;
        });
    }
    if (url) {
        // 检查是否通过`setShowActivityIndicatorView:`方法设置了显示正在加载指示器。如果设置了,使用`addActivityIndicator`方法向self添加指示器
        if ([self showActivityIndicatorView]) {
            [self addActivityIndicator];
        }
        __weak __typeof(self)wself = self;
//下载的核心方法
        id  operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options
progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
          //移除加载指示器
            [wself removeActivityIndicator];
          //如果imageView不存在了就return停止操作
            if (!wself) return;
            dispatch_main_sync_safe(^{
                if (!wself) return;
/*
SDWebImageAvoidAutoSetImage,默认情况下图片会在下载完毕后自动添加给imageView,但是有些时候我们想在设置图片之前加一些图片的处理,就要下载成功后去手动设置图片了,不会执行`wself.image = image;`,而是直接执行完成回调,有用户自己决定如何处理。
*/
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
                {
                    completedBlock(image, error, cacheType, url);
                    return;
                }
/*
如果后两个条件中至少有一个不满足,那么就直接将image赋给当前的imageView
,并调用setNeedsLayout
*/
                else if (image) {
                    wself.image = image;
                    [wself setNeedsLayout];
                else {
/*
image为空,并且设置了延迟设置占位图,会将占位图设置为最终的image,,并将其标记为需要重新布局。
  */
                    if ((options & SDWebImageDelayPlaceholder)) {
                        wself.image = placeholder;
                        [wself setNeedsLayout];
                    }
                }
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
 // 为UIImageView绑定新的操作,以为之前把ImageView的操作cancel了
        [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
    else {
 // 判断url不存在,移除加载指示器,执行完成回调,传递错误信息。
        dispatch_main_async_safe(^{
            [self removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

SDWebImageManager

-sd_setImageWithURL:forState:placeholderImage:options:completed:中,下载图片方法是位于SDWebImageManager类中- downloadImageWithURL: options:progress:completed:函数

我们看下文档是对SDWebImageManager怎么描述的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
 * The SDWebImageManager is the class behind the UIImageView+WebCache category and likes.
 * It ties the asynchronous downloader (SDWebImageDownloader) with the image cache store (SDImageCache).
 * You can use this class directly to benefit from web image downloading with caching in another context than
 * a UIView.
 *
 * Here is a simple example of how to use SDWebImageManager:
 *
 * @code
/*
概述了SDWenImageManager的作用,其实UIImageVIew+WebCache这个Category背后执行操作的就是这个SDWebImageManager.它会绑定一个下载器也就是SDwebImageDownloader和一个缓存SDImageCache
*/
/**
 * Downloads the image at the given URL if not present in cache or return the cached version otherwise.
   若图片不在cache中,就根据给定的URL下载图片,否则返回cache中的图片
 *
 * @param url            The URL to the image
 * @param options        A mask to specify options to use for this request
 * @param progressBlock  A block called while image is downloading
 * @param completedBlock A block called when operation has been completed.
 *
 *   This parameter is required.
 *
 *   This block has no return value and takes the requested UIImage as first parameter.
 *   In case of error the image parameter is nil and the second parameter may contain an NSError.
 *
 *   The third parameter is an `SDImageCacheType` enum indicating if the image was retrieved from the local cache
 *   or from the memory cache or from the network.
 *
 *   The last parameter is set to NO when the SDWebImageProgressiveDownload option is used and the image is
 *   downloading. This block is thus called repeatedly with a partial image. When image is fully downloaded, the
 *   block is called a last time with the full image and the last parameter set to YES.
 *
 * @return Returns an NSObject conforming to SDWebImageOperation. Should be an instance of SDWebImageDownloaderOperation
 */
/*
*第一个参数是必须的,就是image的url
*第二个参数options你可以定制化各种各样的操作
*第三个参数是一个回调block,用于图片下载过程中的回调
*第四个参数是一个下载完成的回调,会在图片下载完成后回调
*返回值是一个NSObject类,并且这个NSObject类是遵循一个协议这个协议叫SDWebImageOperation,这个协议里面只写了一个协议,就是一个cancel一个operation的协议
*/
- (id )downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;

我们继续看SDWebImageManager .m

  • 初始化方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
*初始化方法
*1.获得一个SDImageCache的单例
*2.获得一个SDWebImageDownloader的单例
*3.新建一个MutableSet来存储下载失败的url
*4.新建一个用来存储下载operation的可遍数组
*/
- (id)init {
    if ((self = [super init])) {
        _imageCache = [self createCache];
        _imageDownloader = [SDWebImageDownloader sharedDownloader];
        _failedURLs = [NSMutableSet new];
        _runningOperations = [NSMutableArray new];
    }
    return self;
}

利用image的url生成一个缓存时需要的key,cacheKeyFilter的定义如下:

1
2
@property (nonatomic, copy) SDWebImageCacheKeyFilterBlock cacheKeyFilter;
typedef NSString *(^SDWebImageCacheKeyFilterBlock)(NSURL *url);

是一个可以返回一个字符串的block

1
2
3
4
5
6
7
8
9
10
//如果检测到cacheKeyFilter不为空的时候,利用cacheKeyFilter来生成一个key
//如果为空,那么直接返回URL的string内容,当做key.
- (NSString *)cacheKeyForURL:(NSURL *)url {
    if (self.cacheKeyFilter) {
        return self.cacheKeyFilter(url);
    }
    else {
        return [url absoluteString];
    }
}
  • 检查一个图片是否被缓存的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
- (BOOL)cachedImageExistsForURL:(NSURL *)url {
  //调用上面的方法取到image的url对应的key
    NSString *key = [self cacheKeyForURL:url];
//首先检测内存缓存中时候存在这张图片,如果已有直接返回yes
    if ([self.imageCache imageFromMemoryCacheForKey:key] != nil) return YES;
//如果内存缓存里面没有这张图片,那么就调用diskImageExistsWithKey这个方法去硬盘找
    return [self.imageCache diskImageExistsWithKey:key];
}
// 检测硬盘里是否缓存了图片
- (BOOL)diskImageExistsForURL:(NSURL *)url {
 NSString *key = [self cacheKeyForURL:url];
 return [self.imageCache diskImageExistsWithKey:key];
}

下面两个方法比较类似,都是先根据图片的url创建对应的key

  • 第一个方法先用BOOL isInMemoryCache = ([self.imageCache imageFromMemoryCacheForKey:key] != nil);判断图片有没有在内存缓存中,如果图片在内存缓存中存在,就在主线程里面回调block,如果图片没有在内存缓存中就去查找是不是在磁盘缓存里面,然后在主线程里面回到block

  • 第二个方法只查询图片是否在磁盘缓存里面,然后在主线程里面回调block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- (void)cachedImageExistsForURL:(NSURL *)url
                     completion:(SDWebImageCheckCacheCompletionBlock)completionBlock {
    NSString *key = [self cacheKeyForURL:url];
    BOOL isInMemoryCache = ([self.imageCache imageFromMemoryCacheForKey:key] != nil);
    if (isInMemoryCache) {
        // making sure we call the completion block on the main queue
        dispatch_async(dispatch_get_main_queue(), ^{
            if (completionBlock) {
                completionBlock(YES);
            }
        });
        return;
    }
    [self.imageCache diskImageExistsWithKey:key completion:^(BOOL isInDiskCache) {
        // the completion block of checkDiskCacheForImageWithKey:completion: is always called on the main queue, no need to further dispatch
        if (completionBlock) {
            completionBlock(isInDiskCache);
        }
    }];
}
- (void)diskImageExistsForURL:(NSURL *)url
                   completion:(SDWebImageCheckCacheCompletionBlock)completionBlock {
    NSString *key = [self cacheKeyForURL:url];
    [self.imageCache diskImageExistsWithKey:key completion:^(BOOL isInDiskCache) {
        // the completion block of checkDiskCacheForImageWithKey:completion: is always called on the main queue, no need to further dispatch
        if (completionBlock) {
            completionBlock(isInDiskCache);
        }
    }];
}

上面的方法都是用于查询图片是否在内存缓存或磁盘的缓存下面的方法可以算是SDWebImageManager核心方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
//通过url建立一个operation用来下载图片.
- (id )downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock
 //防止开发者把传入NSString类型的url,如果url的类型是NSString就给转换成NSURL类型
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }
    //如果转换NSURL失败,就把传入的url置为nil下载停止
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }

首先我们先来看看__block和__weak的区别

__block用于指明当前声明的变量在被block捕获之后,可以在block中改变变量的值。因为在block声明的同时会截获该block所使用的全部自动变量的值,这些值只在block中只有"使用权"而不具有"修改权"。而block说明符就为block提供了变量的修改权,**block不能避免循环引用**,这就需要我们在 block 内部将要退出的时候手动释放掉 blockObj,blockObj = nil

__weak是所有权修饰符,__weak本身是可以避免循环引用的问题的,但是其会导致外部对象释放之后,block内部也访问不到对象的问题,我们可以通过在block内部声明一个__strong的变量来指向weakObj,使外部既能在block内部保持住又能避免循环引用

1
2
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;

我们再来看看SDWebImageCombinedOperation到底有一些什么内容:

SDWebImageCombinedOperation它什么也不做,保存了两个东西(一个block,可以取消下载operation,一个operation,cacheOperation用来下载图片并且缓存的operation)

并且SDWebImageCombineOperation遵循协议,所以operation可以作为返回值返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@interface SDWebImageCombinedOperation : NSObject @property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock;
@property (strong, nonatomic) NSOperation *cacheOperation;
@end
@implementation SDWebImageCombinedOperation
- (void)setCancelBlock:(SDWebImageNoParamsBlock)cancelBlock {
    // check if the operation is already cancelled, then we just call the cancelBlock
    if (self.isCancelled) {
        if (cancelBlock) {
            cancelBlock();
        }
        _cancelBlock = nil; // don't forget to nil the cancelBlock, otherwise we will get crashes
    else {
        _cancelBlock = [cancelBlock copy];
    }
}
- (void)cancel {
    self.cancelled = YES;
    if (self.cacheOperation) {
        [self.cacheOperation cancel];
        self.cacheOperation = nil;
    }
    if (self.cancelBlock) {
        self.cancelBlock();
        // TODO: this is a temporary fix to #809.
        // Until we can figure the exact cause of the crash, going with the ivar instead of the setter
//        self.cancelBlock = nil;
        _cancelBlock = nil;
    }
}
@end

@synchronized是OC中一种方便地创建互斥锁的方式--它可以防止不同线程在同一时间执行区块的代码

self.failedURLs是一个NSSet类型的集合,里面存放的都是下载失败的图片的url,failedURLs不是NSArray类型的原因是:

在搜索一个个元素的时候NSSet比NSArray效率高,主要是它用到了一个算法hash(散列,哈希) ,比如你要存储A,一个hash算法直接就能找到A应该存储的位置;同样当你要访问A的时候,一个hash过程就能找到A存储的位置,对于NSArray,若想知道A到底在不在数组中,则需要遍历整个数据,显然效率较低了

并且NSSet里面不含有重复的元素,同一个下载失败的url只会存在一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (BOOL)containsObject:(ObjectType)anObject;,判断集合里面是否含有这个obj
    BOOL isFailedUrl = NO;
//创建一个互斥锁防止现有的别的线程修改failedURLs
//判断这个url是否是fail过的,如果url failed过的那么isFailedUrl就是true.
    @synchronized (self.failedURLs) {
        isFailedUrl = [self.failedURLs containsObject:url];
    }
//如果url不存在那么直接返回一个block,如果url存在那么继续
//!(options & SDWebImageRetryFailed) 之前就提过一个类似的了,它的意思看这个options是不是和SDWebImageRetryFailed不相同
//如果不相同并且isFailedUrl是true.那么就回调一个error的block
    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        dispatch_main_sync_safe(^{
            NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
            completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
        });
        return operation;
    }

把operation加入到self.runningOperations的数组里面,并创建一个互斥线程锁来保护这个操作

获取image的url对应的key

    @synchronized (self.runningOperations) {

        [self.runningOperations addObject:operation];

    }

    NSString *key = [self cacheKeyForURL:url];

其实看到这里,下面牵扯的代码就会越来越越多了,会牵扯到的类也越来越多,我们先一步步地顺着往下看,然后再看涉及到的类里面写了些什么,最后再做总结,在这之前如果你对NSOperation还不够了解建议你先暂时停下看看下这篇文章NSOperation。然后再继续往下阅读。

1
- (NSOperation *)queryDiskCacheForKey:(NSString *)key

done:(SDWebImageQueryCompletedBlock)doneBlock;是SDImageCache的一个方法,根据图片的key,异步查询磁盘缓存的方法

我们先来看下这个方法里面都有什么:

_ioQueue的定义是:@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t ioQueue;

_ioQueue的初始化是:

1
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);

DISPATCH_QUEUE_SERIAL代表的是创建一个串行的队列,所以_ioQueue是一个串行队列(任务一个执行完毕才执行下一个)

PS:如果你对GCD队列不太了解可以先看下GCD使用经验与技巧浅谈,然后再继续阅读额

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181