在开发iOS的客户端应用时,经常需要从服务器下载图片,虽然系统提供了下载工具:NSData、NSURLSession等等方法,但是考虑到图片下载过程中,需要考虑的因素比较多,比如:异步下载、图片缓存、错误处理、编码解码等,以及实际需要中根据不同网络加载不同画质的图片等等需求,因此下载操作不是一个简单的下载动作就可以解决。

针对上述问题,目前常用的开源库就是SDWebImage,它很好的解决了图片的异步下载、图片缓存、错误处理等问题,得到了广泛的应用,使得设置UIImageViewUIButton对象的图片十分方便。本文就从源码的角度,剖析一下这款优秀的开源库的具体实现。

类结构图

SDWebImage的源码的类结构图和下载流程图在官方的说明文档里有介绍,通过UML类结构图详细的介绍了该框架的内部结构,以及通过流程图介绍了具体的下载过程。

下图是我总结的SDWebImage的结构图,简单的把SDWebImage源码文件按照功能进行了划分,方便在阅读源码时,能快速的对源码有一个总体的认识,加快阅读效率。

![](//upload-images.jianshu.io/upload_images/1843940-c51585b28704fae9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

关键类功能简介:

  • 1.下载类

SDWebImageDownloader:提供下载的方法给SDWebImageManager使用,提供了最大并发量的下载控制、超时时间、取消下载、下载挂起、是否解压图片等等功能。同时,还提供了开始下载和停止下载的通知,给使用者监测下载状态,如果使用者不用监测下载状态,就不用监测该通知,这种设计模式很灵活,给使用者提供了更方便的选择。

extern NSString * _Nonnull const SDWebImageDownloadStartNotification;
extern NSString * _Nonnull const SDWebImageDownloadStopNotification;

SDWebImageDownloaderOperation:继承自NSOperation,是图片下载的具体实现类,通过加入到NSOperationQueue中,然后在start方法中来开启下载操作。

  • 2.图片缓存

SDImageCacheConfig:主要提供缓存的配置信息,如:是否解压图片、是否缓存到内存、最大缓存时间(默认是一周)和最大缓存的字节数等等。

SDImageCache:缓存实现类,提供最大缓存字节、最大缓存条目的控制,以及缓存到内存及磁盘、从内存或磁盘删除、查询检索和查询缓存信息等功能。

  • 3.分类

UIImageView+WebCacheUIImageView的分类,提供了设置UIImageView对象图片的多种方法,下面的方法可以说是SDWebImage框架中最常用的方法。

- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options; // 带完成block的赋值方法
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
completed:(nullable SDExternalCompletionBlock)completedBlock;

UIButton+WebCacheUIButton的分类,提供了设置按钮图片和按钮背景图片的功能

- (void)sd_setImageWithURL:(nullable NSURL *)url
forState:(UIControlState)state
placeholderImage:(nullable UIImage *)placeholder;
  • 4.工具类

SDWebImageDecoder:图像解码的工具类,通过imageNames:加载图片会立即进行解码,而通过imageWithContentsOfFile:则不会

SDWebImagePrefetcher:批量图像下载工具,针对UI界面中需要下载多个图片时,又要在滑动中保持流畅体验,则可以使用该工具类批量下载图片,然后在给具体的UI控件设置图片时,就会直接从缓存中取

SDWebImageManager:下载管理类工具,是SDWebImage的核心类,从官方文档的类图中也可以看出,提供了查看图片是否已经缓存、下载图片、缓存图片、取消所有的下载等等功能

  • 5.图片格式类

NSData+ImageContentType:根据图片数据的第一个字节来获取图片的格式,可以区分PNGJPEGGIFTIFFWebP

以上只是对SDWebImage类结构图的简单分析,如果需要进一步了解各个类的具体实现,请参考文末的资料,已有人详细的介绍了各个类的功能实现原理或方法。

应用

下面介绍一个在应用SDWebImage设置UI图片的源码实现过程

在UIImageView上的应用

设置图片

通过设置URL、占位图片、图片配置、图片下载进度回调和设置完成回调来给UIImageView对象设置图片

// ViewController.m
[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://rescdn.qqmail.com/dyimg/20140302/73EB27F4A350.jpg"] placeholderImage:[UIImage imageNamed:@"gift-icon"] options:0 progress:nil completed:nil];

上述代码调用UIImageView+WebCache.m里的方法

- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_internalSetImageWithURL:url
placeholderImage:placeholder
options:options
operationKey:nil
setImageBlock:nil
progress:progressBlock
completed:completedBlock];
}

然后调用UIView+WebCache.m中的方法获取图片,然后根据option的类型进行不同的设置

// UIView+WebCache.m
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
operationKey:(nullable NSString *)operationKey
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock
{
...
if (url) {
... __weak __typeof(self)wself = self;
// 开始加载图片
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
...
dispatch_main_async_safe(^{
if (!sself) {
return;
}
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
// 把图片放到completedBlock里处理,一般是手动设置图片,因为这样可以对图片做进一步处理
completedBlock(image, error, cacheType, url);
return;
} else if (image) {
// 设置图片
[sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];
} else {
// 延迟加载占位图(获取图片之后)
if ((options & SDWebImageDelayPlaceholder)) {
[sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];
}
} // 回调完成block,如果是nil,则不调用
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
} else {
// 处理url为nil的情况
dispatch_main_async_safe(^{
[self sd_removeActivityIndicator];
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}

加载图片的具体实现代码在SDWebImageManager里面,先从缓存中取图片,如果缓存中没有图片,就从网络下载,然后设置图片,最后再缓存该图片

// SDWebImageManager.m
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock
{
...
// 从缓存中取图片
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
if (operation.isCancelled) {
// 如果操作被取消,就从runningOperations操作数组从把该操作删除
[self safelyRemoveOperationFromRunning:operation];
return;
} if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
if (cachedImage && options & SDWebImageRefreshCached) {
// 如果options设置为更新缓存,那么就需要从服务器从新下载图片,然后更新本地缓存
[self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}
...
// 创建下载器,从服务器下载图片
SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
...
else {
// 设置了options为失败了重试,则会把失败的url加入failedURLs数组
if ((options & SDWebImageRetryFailed)) {
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
}
} ...
} else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
// 对图片进行transform操作
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url]; if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
// pass nil if the image was transformed, so we can recalculate the data from the image
[self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
} [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
});
} else {
// 缓存图片,有缓存到内存和磁盘两种方式
if (downloadedImage && finished) {
[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
}
// 回调完成的block
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
} if (finished) {
// 下载完成,就从runningOperations数组中删除操作
[self safelyRemoveOperationFromRunning:strongOperation];
}
}];
// 设置取消下载的回调
operation.cancelBlock = ^{
[self.imageDownloader cancel:subOperationToken];
__strong __typeof(weakOperation) strongOperation = weakOperation;
[self safelyRemoveOperationFromRunning:strongOperation];
};
} else if (cachedImage) {
// 从缓存在取到图片,回调完成block
__strong __typeof(weakOperation) strongOperation = weakOperation;
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
}
...
}]; return operation;
}

从缓存中取图片,是先从内存中取,如果在内存中取到,就在当前线程中直接回调doneBlock;如果内存中没有,就开子线程从磁盘中取,如果取到图片,就回调doneBlock

// SDImageCache.m
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
...
// First check the in-memory cache...
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
NSData *diskData = nil;
if ([image isGIF]) {
diskData = [self diskImageDataBySearchingAllPathsForKey:key];
}
if (doneBlock) {
doneBlock(image, diskData, SDImageCacheTypeMemory);
}
return nil;
} NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
// do not call the completion if cancelled
return;
} @autoreleasepool {
// 从磁盘中取图片的data
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
// 从磁盘中直接取图片
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
// 缓存到内存中
[self.memCache setObject:diskImage forKey:key cost:cost];
} if (doneBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
});
}
}
}); return operation;
}

图片的下载过程是在SDWebImageDownloader.m中进行的,实质是通过SDWebImageDownloaderOperation(继承自NSOperation)对象,把该对象加入到downloadQueue里,然后在start方法里通过NSURLSession来下载图片。(其中,NSOperation有两个方法:mainstart,如果想使用同步,那么最简单方法的就是把逻辑写在main()中,使用异步,需要把逻辑写到start()中,然后加入到队列之中)

// SDWebImageDownloader.m
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
__weak SDWebImageDownloader *wself = self; return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
__strong __typeof (wself) sself = wself;
NSTimeInterval timeoutInterval = sself.downloadTimeout;
// 设置超时时间为15s
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
} // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (sself.headersFilter) {
request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = sself.HTTPHeaders;
}
SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
operation.shouldDecompressImages = sself.shouldDecompressImages; ...
// 加入操作队列,开始下载
[sself.downloadQueue addOperation:operation];
... return operation;
}];
}

SDWebImageDownloaderOperation对象加入到操作队列,就开始调用该对象的start方法。

// SDWebImageDownloaderOperation.m
- (void)start {
// 如果操作被取消,就reset设置
@synchronized (self) {
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
} ...
NSURLSession *session = self.unownedSession;
if (!self.unownedSession) {
// 创建session的配置
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15; // 创建session对象
self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
session = self.ownedSession;
} self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
}
// 开始下载任务
[self.dataTask resume]; if (self.dataTask) {
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
} else {
// 创建任务失败
[self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]];
} ...
}

在下载过程中,会涉及鉴权、响应的statusCode判断(404304等等),以及收到数据后的进度回调等等,在最后的didCompleteWithError里做最后的处理,然后回调完成的block,下面仅分析一下didCompleteWithError的方法

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {

    ...
if (error) {
[self callCompletionBlocksWithError:error];
} else {
if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
/**
* See #1608 and #1623 - apparently, there is a race condition on `NSURLCache` that causes a crash
* Limited the calls to `cachedResponseForRequest:` only for cases where we should ignore the cached response
* and images for which responseFromCached is YES (only the ones that cannot be cached).
* Note: responseFromCached is set to NO inside `willCacheResponse:`. This method doesn't get called for large images or images behind authentication
*/
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached && [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request]) {
// 如果options是忽略缓存,而图片又是从缓存中取的,就给回调传入nil
[self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
} else if (self.imageData) {
UIImage *image = [UIImage sd_imageWithData:self.imageData];
// 缓存图片
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
// 跳转图片的大小
image = [self scaledImageForKey:key image:image]; // Do not force decoding animated GIFs
if (!image.images) {
// 不是Gif图像
if (self.shouldDecompressImages) {
if (self.options & SDWebImageDownloaderScaleDownLargeImages) {
#if SD_UIKIT || SD_WATCH
image = [UIImage decodedAndScaledDownImageWithImage:image];
[self.imageData setData:UIImagePNGRepresentation(image)];
#endif
} else {
image = [UIImage decodedImageWithImage:image];
}
}
}
if (CGSizeEqualToSize(image.size, CGSizeZero)) {
// 下载是图片大小的0
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
} else {
// 把下载的图片作为参数回调
[self callCompletionBlocksWithImage:image imageData:self.imageData error:nil finished:YES];
}
} else {
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
}
}
}
...
}

以上就是给UIImageView对象设置图片的过程,可以看出还是比较复杂的,考虑的情况也比较多,不得不佩服作者的编码能力。至于UIButton的图片设置过程,分析情况类似,在此不做分析。

SDWebImage的源码中在设置图片的过程中,还应用了多种技术:GCD的线程组、锁机制、并发控制、队列、图像解码、缓存控制等等,是一个综合性十分强的项目了,通过阅读源码,对这些技术的使用也有了进一步的认知,对作者的编程功力的深厚深深折服。


SDWebImage的解析到此结束,本文只是简单的从源码结构、UIImageView的使用角度进行了简单的分析,希望对阅读源码的朋友有一些帮助,如果文中有不足之处,还望不吝指出,互相学习。

参考资料

SDWebImage源码

SDWebImage源码解读

SDWebImage源码(一)——SDWebImage概览

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

SDWebImage4.0.0 源码解析的更多相关文章

  1. Masonry1.0.2 源码解析

    在了解Masonry框架之前,有必要先了解一下自动布局的概念.在iOS6之前,UI布局的方式是通过frame属性和Autoresizing来完成的,而在iOS6之后,苹果公司推出了AutoLayout ...

  2. SpringBoot 2.0.3 源码解析

    前言 用SpringBoot也有很长一段时间了,一直是底层使用者,没有研究过其到底是怎么运行的,借此机会今天试着将源码读一下,在此记录...我这里使用的SpringBoot 版本是  2.0.3.RE ...

  3. YYModel V1.0.4源码解析

    YYKit出现了很长时间了,一直想要详细解析一下它的源码,都是各种缘由推迟了. 最近稍微闲了一点,决定先从最简单的YYModel开始吧. 首先,我也先去搜索了一下YYModel相关的文章,解析主要AP ...

  4. 【JUC源码解析】CyclicBarrier

    简介 CyclicBarrier,一个同步器,允许多个线程相互等待,直到达到一个公共屏障点. 概述 CyclicBarrier支持一个可选的 Runnable 命令,在一组线程中的最后一个线程到达之后 ...

  5. Redis系列(十):数据结构Set源码解析和SADD、SINTER、SDIFF、SUNION、SPOP命令

    1.介绍 Hash是以K->V形式存储,而Set则是K存储,空间节省了很多 Redis中Set是String类型的无序集合:集合成员是唯一的. 这就意味着集合中不能出现重复的数据.可根据应用场景 ...

  6. ArrayList、CopyOnWriteArrayList源码解析(JDK1.8)

    本篇文章主要是学习后的知识记录,存在不足,或许不够深入,还请谅解. 目录 ArrayList源码解析 ArrayList中的变量 ArrayList构造函数 ArrayList中的add方法 Arra ...

  7. EventBus3.0源码解析

    本文主要介绍EventBus3.0的源码 EventBus是一个Android事件发布/订阅框架,通过解耦发布者和订阅者简化 Android 事件传递. EventBus使用简单,并将事件发布和订阅充 ...

  8. solr&lucene3.6.0源码解析(四)

    本文要描述的是solr的查询插件,该查询插件目的用于生成Lucene的查询Query,类似于查询条件表达式,与solr查询插件相关UML类图如下: 如果我们强行将上面的类图纳入某种设计模式语言的话,本 ...

  9. solr&lucene3.6.0源码解析(三)

    solr索引操作(包括新增 更新 删除 提交 合并等)相关UML图如下 从上面的类图我们可以发现,其中体现了工厂方法模式及责任链模式的运用 UpdateRequestProcessor相当于责任链模式 ...

  10. Heritrix 3.1.0 源码解析(三十七)

    今天有兴趣重新看了一下heritrix3.1.0系统里面的线程池源码,heritrix系统没有采用java的cocurrency包里面的并发框架,而是采用了线程组ThreadGroup类来实现线程池的 ...

随机推荐

  1. 安装WIA组件

    下载地址: http://pan.baidu.com/s/1bnGU5Nx 安装方法: 将下载后的WIAAutSDK.zip解压,复制wiaaut.dll到C:\Windows\System32,注册 ...

  2. zookeeper的集群介绍、搭建、环境、安装

    zookeeper是本身是一种分布式协调服务(英文意思动物园园长因为Hadoop就是一个动物园,storm.hadoop.kafkaka.hbaser都是基于zookeeper开发的) 原理:Zook ...

  3. Echarts折线图表断点如何补全

    Echarts折线图如何补全断点以及如何隐藏断点的title 做报表的时候,尤其是做图表的时候时常会碰到某一记录的值中缺少某个时间段(比如月份或季度)的值,导致图表显示残缺不全,for example ...

  4. PHPCMS修改域名

    有时候服务器域名解析时,需要修改网站域名,那么在phpcms中,像一些附件地址什么的都需要修改.下面介绍一下怎么系统全面的修改这些地址. 1.在后台管理中心--设置--站点管理--修改,站点域名改为新 ...

  5. Golang 基于libpcap/winpcap的底层网络编程——gopacket安装

    Go简介 Go是一种编译型语言,它结合了解释型语言的游刃有余,动态类型语言的开发效率,以及静态类型的安全性. 语法类似C/C++,但是又带有一点python的味道 其中个人认为最出色的特点就是他的包管 ...

  6. form表单提交图片禁止跳转

    问题: 最近在做项目时,遇到上传图片需求,且在不跳转的情况下获取到返回信息 思路: 1.  使用ajax发送异步请求,经多次测试,最终以失败告终 2. iframe 禁止跳转(未尝试) 3. 修改fo ...

  7. 【SqlServer系列】集合运算

    1   概述 已发布[SqlServer系列]文章如下: [SqlServer系列]SQLSERVER安装教程 [SqlServer系列]数据库三大范式 [SqlServer系列]表单查询 [SqlS ...

  8. Storm/JStorm之TopologyBuilder源码阅读

    在Strom/JStorm中有一个类是特别重要的,主要用来构建Topology的,这个类就是TopologyBuilder. 咱先看一下简单的例子: public static void main(S ...

  9. Java中的clone()----深复制,浅复制

    这篇文章主要介绍了Java中对象的深复制(深克隆)和浅复制(浅克隆) ,需要的朋友可以参考下 1.浅复制与深复制概念 ⑴浅复制(浅克隆) 被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他 ...

  10. 故障公告:docker swarm集群“群龙无首”引发部分站点无法访问

    今天傍晚 17:38-18:18 左右,由于 docker swarm 集群出现 "The swarm does not have a leader" 问题,造成博问.闪存.园子. ...