当前位置: 首页 > 科技观察

iOS开发——你真的会用SDWebImage吗?

时间:2023-03-14 23:52:13 科技观察

SDWebImage是目前最流行的图片下载第三方框架,使用率很高。但是你真的知道怎么用吗?接下来,本文将通过实例来分析如何合理使用SDWebImage。使用场景:自定义UITableViewCell上有图片需要显示。当网络状态为WiFi时,要求图片显示高清;当网络状态为蜂窝移动网络时,需要显示图片的缩略图。下图为示例:图中显示的图片满足根据网络情况下载的要求。由于需要监控网络状态,笔者这里推荐使用AFNetWorking。1)在GitHub上的项目中导入第三方框架AFNetWorking或者使用cocoaPod。2)在AppDelegate.m文件中的application:didFinishLaunchingWithOptions:方法中监听网络状态。//AppDelegate.m文件中-(BOOL)application:(UIApplication*)applicationdidFinishLaunchingWithOptions:(NSDictionary*)launchOptions{//监控网络状态[[AFNetworkReachabilityManagersharedManager]startMonitoring];}//下面的代码在需要的方法中监控网络状态使用AFNetworkReachabilityManager*mgr=[AFNetworkReachabilityManagersharedManager];if(mgr.isReachableViaWiFi){//使用Wifi,下载原图}else{//其他,下载小图}}这时候iOS学习者会开始抱怨:这还不简单?于是三遍五遍二次写完了下面的代码。//使用MVC,设置cell的模型属性时,下载图片-setItem:(CustomItem*)item{_item=item;UIImage*placeholder=[UIImageimageNamed:@"placeholderImage"];AFNetworkReachabilityManager*mgr=[AFNetworkReachabilityManagersharedManager];if(mgr.isReachableViaWiFi){//使用Wifi时,下载原图[self.imageViewsd_setImageWithURL:[NSURURRLWithString:item.originalImage]placeholderImage:placeholder];}else{//其他,下载小图[self.imageViewsd_setImageWithURL:[NSURURRLWithString:[NSURURRLWithString:item.thumbnailImage]placeholderImage:placeholder];}}至此,确实可以根据当前网络状态完成对应图片的基本下载,但是在实际开发中,这其实是不合理。以下是需要注意的细节:1)SDWebImage会自动帮开发者缓存图片(包括内存缓存、沙箱缓存),所以我们需要在WiFi环境下设置用户下载的高清图片,然后打开应用蜂窝网络状态下的时间也应该显示高分辨率图像而不是下载缩略图。2)很多应用设置模块都有一个功能:在移动网络环境下依然显示高清图片。这个功能其实就是记录沙盒中的设置。本地保存数据可以查看我的另一篇简书主页文章:iOS本地数据访问,看这里就可以了。3)用户离线时,业务无法合理处理。所以,开始改进吧。为了方便读者理解,我先贴出伪代码:-setItem:(CustomItem*)item{_item=item;if(缓存中有原图){self.imageView.image=originalimage;}else{if(Wifi环境){下载并显示原图}elseif(手机自带网络){if(3G\4G环境下仍下载原图){下载并显示原图}else{下载并显示小图}}else{if(缓存中有小图){self.imageView.image=小图;}else//处理离线状态{self.imageView.image=占用图片;}}}}实现上面的伪代码:读者可以将上面的伪代码一一对应代码。练习时,建议先写伪代码,再写真代码。多注意“评论”解释。-setItem:(CustomItem*)item{_item=item;//占用图片UIImage*placeholder=[UIImageimageNamed:@"placeholderImage"];//从内存\sandbox缓存中获取原始图片,UIImage*originalImage=[[SDImageCachesharedImageCache]imageFromDiskCacheForKey:item.originalImage];if(originalImage){//如果内存\sandbox缓存有原图,则直接显示原图(不管当前网络状态如何)self.imageView.image=originalImage;}else{//Memory\Sandbox缓存没有原图AFNetworkReachabilityManager*mgr=[AFNetworkReachabilityManagersharedManager];if(mgr.isReachableViaWiFi){//使用Wifi,下载原图[self.imageViewsd_setImageWithURL:[NSURURLWithString:item.originalImage]placeholderImage:placeholder];}elseif(mgr.isReachableViaWWAN){//当使用手机自带网络时//用户的配置项假设使用NSUserDefaults存储在沙箱中//[[NSUserDefaultsstandardUserDefaults]setBool:NOforKey:@"alwaysDownloadOriginalImage"];//[[NSUserDefaultsstandardUserDefaults]synchronize];#warning从沙盒中读取用户的配置项:3G\4G环境下是否依然下载原图BOOLalwaysDownloadOriginalImage=[[NSUserDefaultsstandardUserDefaults]boolForKey:@"alwaysDownloadOriginalImage"];if(总是下载原始图片loadOriginalImage){//下载原图[self.imageViewsd_setImageWithURL:[NSURURRLWithString:item.originalImage]placeholderImage:placeholder];}else{//下载小图[self.imageViewsd_setImageWithURL:[NSURURRLWithString:item.thumbnailImage]placeholderImage:placeholder];}}else{//无网络UIImage*thumbnailImage=[[SDImageCachesharedImageCache]imageFromDiskCacheForKey:item.thumbnailImage];if(thumbnailImage){//内存\sandbox缓存中有小图self.imageView.image=thumbnailImage;}else{//处理离线状态,没有缓存self.imageView.image=placeholder;}}}}解决了吗?真正的陷阱才刚刚开始。在描述上面代码的坑之前,我们先分析一下UITableViewCell的缓存机制。请看下图:有一个tableView同时显示三个UITableViewCells,每个tableViewCell包含一个imageView的子控件,每个cell都有对应的model属性来设置imageView的图片内容。注意:此时缓冲池为空,因为没有细胞被推出屏幕。该单元尚未被推入缓存池。当一个cell被推出屏幕时,系统会自动将这个cell放入自动缓存池中。注意:cell对应的UIImage图片数据模型还没有被清除!它仍然指向最后使用的单元格。当一个cell被放入缓存池,下一个cell进入屏幕时,系统会根据tableView中注册的ID找到对应的cell进行应用。将上述进入缓存池的cell重新添加到tableView中,并在tableView的DataSource方法tableView:cellForRowAtIndexPath:中设置变化的cell对应的model数据。此时对应的cell如图所示:cell放入缓存池上面是tableView复用机制的简单介绍。再回来,那么上面说的真正的坑在哪里呢?用一个场景来描述一下:当用户环境的wifi网速不够快时(图片无法立即下载),而在上面的代码中,在wifi环境下下载了高清大图。因此需要一些时间才能完成下载。此时用户不想等待,想看到上次打开应用时显示的图片。这时候用户会滑动到下面的单元格去查看。注意:此时,上述cell图片下载操作并未暂停,仍处于下载图片状态。当用户正在查看上次打开App时显示的图片(从上次打开App下载的图片,SDWebImage会帮我们缓存而不下载),以及上次打开App时刚好需要显示图片的cell时候使用了tableView的复用机制,对于从缓冲池中取出的cell,当上层cell的高清大图下载完毕后,SDWebImage默认的方式是立即将下载的图片设置为ImageView,所以我们将此时显示在底部单元格中显示上图,造成数据混乱,是一个很严重的BUG。那么如何解决这个棘手的问题呢?如果我们能在从缓冲池中取出cell时去掉cell放入缓冲池之前的下载操作,那么就不会出现数据混乱的情况。这时候你可能会问我:如何去除下载操作?下载操作不就是SDWebImage帮我们完成的吗?没错,帮我们下载图片的确实是SDWebImage。下面我们来看一下SDWebImage的源码,看看它是怎么做到的。-(void)sd_setImageWithURL:(NSURL*)urlplaceholderImage:(UIImage*)placeholderoptions:(SDWebImageOptions)optionsprogress:(SDWebImageDownloaderProgressBlock)progressBlockcompleted:(SDWebImageCompletionBlock)completedBlock{//关闭当前图片的下载操作[selfsd_cancelself;obrentciImageLoad],url,OBJC_ASSOCIATION_RETAIN_NONATOMIC);if(!(options&SDWebImageDelayPlaceholder)){dispatch_main_async_safe(^{self.image=placeholder;});}if(url){//checkifactivityViewisenabledornotif([selfshowActivityIndi??catorView]){[selfaddActivityIndi??cator];}__weak__typeof(self))wself=self;idoperation=[SDWebImageManager.sharedManagerdownloadImageWithURL:urloptions:optionsprogress:progressBlockcompleted:^(UIImage*image,NSError*error,SDImageCacheTypecacheType,BOOLfinished,NSURL*imageURL){[wselfremoveActivityIndi??cator];if(!wself)return;dispatch_main_sync_safe(^{if(!wself)return;if(image&&(options&SDWebImageAvoidAutoSetImage)&&completedBlock){completedBlock(image,error,cacheType,url);return;}elseif(image){wself.image=image;[wselfsetNeedsLayout];}else{if((options&SDWebImageDelayPlaceholder)){wself.image=placeholder;[wselfsetNeedsLayout]];}}if(completedBlock&&finished){completedBlock(image,error,cacheType,url);}});}];[selfsd_setImageLoadOperation:operationforKey:@"UIImageViewImageLoad"];}else{dispatch_main_async_safe(^{[selfremoveActivityIndi??cator];if(completedBlock){NSError*error=[NSErrorerrorWithDomain:SDWebImageErrorDomaincode:-1userInfo:@{NSLocalizedDescriptionKey:@"Tryingtoloadanilurl"}];completedBlock(nil,error,SDImageCacheTypeNone,url);}});}}我们惊奇地发现原来SDWebImage在下载图片的时候,首先要做的就是关闭imageView当前的下载操作!是不是开始感叹SDWebImage的牛逼了?没错,我们只需要用SDWebImage把我们写的代码的所有直接访问本地缓存的代码都设置好,就OK了!下面是完成的代码-setItem:(CustomItem*)item{_item=item;//占用图片UIImage*placeholder=[UIImageimageNamed:@"placeholderImage"];//从内存\sandbox缓存中获取原图UIImage*originalImage=[[SDImageCachesharedImageCache]imageFromDiskCacheForKey:item.originalImage];if(originalImage){//如果内存\sandbox缓存中有原图,则直接显示原图(不管当前网络状态如何)[self.imageViewsd_setImageWithURL:[NSURURLWithString:item.originalImage]placeholderImage:placeholder];}else{//内存\sandbox缓存中没有原图AFNetworkReachabilityManager*mgr=[AFNetworkReachabilityManagersharedManager];if(mgr.isReachableViaWiFi){//使用Wifi,下载原图[self.imageViewsd_setImageWithURL:[NSURURLWithString:item.originalImage]placeholderImage:placeholder];}elseif(mgr.isReachableViaWWAN){//当使用手机自带网络时//假设用户的配置项存放在沙盒usingNSUserDefaults//[[NSUserDefaultsstandardUserDefaults]setBool:NOforKey:@"alwaysDownloadOriginalImage"];//[[NSUserDefaultsstandardUserDefaults]synchronize];#warning从沙箱中读取用户的配置项:是否仍然下载3G\4G中的原图环境BOOLalwaysDownloadOriginalImage=[[NSUserDefaultsstandardUserDefaults]boolForKey:@"alwaysDownloadOriginalImage"];if(alwaysDownloadOriginalImage){//下载原图[self.imageViewsd_setImageWithURL:[NSURURRLWithString:item.originalImage]placeholderImage:placeholder];}else{//下载小图[self.imageViewsd_setImageWithURL:[NSURURRLWithString:item.thumbnailImage]placeholderImage:placeholder];}}else{//没有网络UIImage*thumbnailImage=[[SDImageCachesharedImageCache]imageFromDiskCacheForKey:item.thumbnailImage];if(thumbnailImage){//有缩略图在memory\sandboxcache[self.imageViewsd_setImageWithURL:[NSURURRLWithString:item.thumbnailImage]placeholderImage:placeholder];}else{[self.imageViewsd_setImageWithURL:nilplaceholderImage:placeholder];}}}}这篇文章就到这里,有什么问题或者错误,请指出