源码阅读-Kingfisher
最后更新:2018-01-16
使用教程:
1. 开始使用
- 桥接 KingFisher, 利用 KingfisherCompatible协议来处理, 
 此处与 SnapKit的处理方式还是有点不同, SnapKit 是使用 ConstraintViewDSL 对象来设置, 对View 来设置方法, 当然这两种方式都可以;
- 设置Image 利用 - 扩展extension来处理- /**
 A type that has Kingfisher extensions.
 */
 public protocol KingfisherCompatible {
 associatedtype CompatibleType
 var kf: CompatibleType { get }
 } public extension KingfisherCompatible {
 public var kf: Kingfisher<Self> {
 get { return Kingfisher(self) }
 }
 } extension Image: KingfisherCompatible { }
 #if !os(watchOS)
 extension ImageView: KingfisherCompatible { }
 extension Button: KingfisherCompatible { }
 #endif extension Kingfisher where Base: ImageView {}
 extension Kingfisher where Base: Image {}
 extension Kingfisher where Base: UIApplication {}
 extension Kingfisher where Base: UIButton {}
此处有一个极佳的使用场景,就是可以利用这种方式将 String 、NSString、NSAttributeString转换为 NSAttributeString
- 协议 Resource 
 协议- Resource里面定义了缓存时使用的- var cacheKey: String { get }以及 下载时候使用的- var downloadURL: URL { get }; 对于- cacheKey没什么好说的,但是对于- downloadURL, 作者定义为- URL类型, 查看 Alamofire 源码,我们可以看到一个 protocol URLConvertible,直接将 String 转换为 URL, 不知道这样做对用户来说,是否更加方便?
- 协议 Placeholder 
 在 SDWebImage - UIImageView+WebCache中,- placeholderImage为一张- UIImage对象. 此处作者进行了扩展, 协议- Placeholder定义了- add与- remove方式, 任何对象只要遵循协议即可, 作者默认实现了- Image, 如果你需要一个View来充当 PlaceHolder, 你只要让你的 View 遵循这个协议即可。
- KingfisherOptionsInfoItem & - defaultOptions
 作者在源码中, 一直传递着- defaultOptions值,- defaultOptions是用于存储 枚举- KingfisherOptionsInfoItem值, 其里面可以定义一系列的操作,最基础的如 下载- downloader(ImageDownloader)与 缓存- .targetCache(cache), 刚看时候,非常懵逼, 深入进去, 截取核心部分代码:- precedencegroup ItemComparisonPrecedence {
 associativity: none
 higherThan: LogicalConjunctionPrecedence
 } infix operator <== : ItemComparisonPrecedence // This operator returns true if two `KingfisherOptionsInfoItem` enum is the same, without considering the associated values.
 func <== (lhs: KingfisherOptionsInfoItem, rhs: KingfisherOptionsInfoItem) -> Bool {
 switch (lhs, rhs) {
 case (.targetCache(_), .targetCache(_)): return true
 case (.originalCache(_), .originalCache(_)): return true
 case (.downloader(_), .downloader(_)): return true
 case (.transition(_), .transition(_)): return true
 case (.downloadPriority(_), .downloadPriority(_)): return true
 case (.forceRefresh, .forceRefresh): return true
 case (.fromMemoryCacheOrRefresh, .fromMemoryCacheOrRefresh): return true
 case (.forceTransition, .forceTransition): return true
 case (.cacheMemoryOnly, .cacheMemoryOnly): return true
 case (.onlyFromCache, .onlyFromCache): return true
 case (.backgroundDecode, .backgroundDecode): return true
 case (.callbackDispatchQueue(_), .callbackDispatchQueue(_)): return true
 case (.scaleFactor(_), .scaleFactor(_)): return true
 case (.preloadAllAnimationData, .preloadAllAnimationData): return true
 case (.requestModifier(_), .requestModifier(_)): return true
 case (.processor(_), .processor(_)): return true
 case (.cacheSerializer(_), .cacheSerializer(_)): return true
 case (.imageModifier(_), .imageModifier(_)): return true
 case (.keepCurrentImageWhileLoading, .keepCurrentImageWhileLoading): return true
 case (.onlyLoadFirstFrame, .onlyLoadFirstFrame): return true
 case (.cacheOriginalImage, .cacheOriginalImage): return true
 default: return false
 }
 } extension Collection where Iterator.Element == KingfisherOptionsInfoItem {
 func lastMatchIgnoringAssociatedValue(_ target: Iterator.Element) -> Iterator.Element? {
 return reversed().first { $0 <== target }
 } func removeAllMatchesIgnoringAssociatedValue(_ target: Iterator.Element) -> [Iterator.Element] {
 return filter { !($0 <== target) }
 }
 } public extension Collection where Iterator.Element == KingfisherOptionsInfoItem {
 /// The `ImageDownloader` which is specified.
 public var downloader: ImageDownloader { if let item = lastMatchIgnoringAssociatedValue(.downloader(.default)),
 case .downloader(let downloader) = item
 {
 return downloader
 }
 return ImageDownloader.default
 } /// Or the placeholder will be used while downloading.
 public var keepCurrentImageWhileLoading: Bool {
 return contains { $0 <== .keepCurrentImageWhileLoading }
 }
 }
 - 此处作者重载了操作符, 更多内容可以参考SwiftTips-操作符、 Apple官方-operator-precedence、Operator Declarations; 取值的时候,先 - reversed(), 然后在取·- first(), 是不是觉得很妙?
 哦, 顺便提一句, 代码中的- if case 语法可以参考: 模式匹配第四弹:if case,guard case,for case- 考虑一下: 作者这个做法根 Optionset 实现很像,能否使用 Optionset 来处理 KingfisherOptionsInfoItem 呢? 
2. 下载图片 DownloadImage
前面说了这么多, 还没提到真正下载的部分, 在extension Kingfisher where Base: ImageView{} 中,通过调用  KingfisherManager.shared.retrieveImage() 方法来下载, 下载的任务顺利转交到 KingfisherManager去了. KingfisherManager通过 options.forceRefresh 判断是直接去下载图片 (ImageDownloader)还是 去从缓存(ImageCache)中获取;
2.1 ImageDownloader 下载图片
ImageDownloader是KingFisher中的下载器。 简单查看一下文档, 定义了一个内部类: ImageFetchLoad, URLSession 以及相关的配置, 三个DispatchQueue。 值得一提的是, 喵神在设计NSURLSessionTaskDelegate的时候, 单独设计出一个ImageDownloaderSessionHandler, 原因可以查看issues-235。
现在, 让我们一起来看一下具体的实现吧, 里面的核心方法是:
open func downloadImage(with url: URL,
                       retrieveImageTask: RetrieveImageTask? = nil,
                       options: KingfisherOptionsInfo? = nil,
                       progressBlock: ImageDownloaderProgressBlock? = nil,
                       completionHandler: ImageDownloaderCompletionHandler? = nil) -> RetrieveImageDownloadTask? { }
前面几个是构建 URLRequest, 对 URLRequest进行modifier, 对 url进行判断等一系列操作,  在这个方法里面调用了-setup: 方法:
func setup(progressBlock: ImageDownloaderProgressBlock?, with completionHandler: ImageDownloaderCompletionHandler?, for url: URL, options: KingfisherOptionsInfo?, started: @escaping ((URLSession, ImageFetchLoad) -> Void)) {
        func prepareFetchLoad() {
            barrierQueue.sync(flags: .barrier) {
                let loadObjectForURL = fetchLoads[url] ?? ImageFetchLoad()
                let callbackPair = (progressBlock: progressBlock, completionHandler: completionHandler)
                loadObjectForURL.contents.append((callbackPair, options ?? KingfisherEmptyOptionsInfo))
                fetchLoads[url] = loadObjectForURL
                if let session = session {
                    started(session, loadObjectForURL)
                }
            }
        }
        if let fetchLoad = fetchLoad(for: url), fetchLoad.downloadTaskCount == 0 {
            if fetchLoad.cancelSemaphore == nil {
                fetchLoad.cancelSemaphore = DispatchSemaphore(value: 0)
            }
            cancelQueue.async {
                _ = fetchLoad.cancelSemaphore?.wait(timeout: .distantFuture)
                fetchLoad.cancelSemaphore = nil
                prepareFetchLoad()
            }
        } else {
            prepareFetchLoad()
        }
    }
2.1.1 dispatch_barrier_sync
现在我们好好分析这部分的代码, 一开始, 调用if let fetchLoad = fetchLoad(for: url), 我们进入这个方法:
 func fetchLoad(for url: URL) -> ImageFetchLoad? {
        var fetchLoad: ImageFetchLoad?
        barrierQueue.sync(flags: .barrier) { fetchLoad = fetchLoads[url] }
        return fetchLoad
    }
可以看到有一个 barrierQueue.sync(flags: .barrier) {}, 这是一个栅栏, 如果你曾经看过 SDWebImage的源码, 你可以在里面看到 dispatch_barrier_sync; 这个保证了线程安全, 可以查看:SDWebImage-关于dispatch_barrier_sync的issue 以及 Kingfisher-关于线程安全问题;
看到这里,你也就明白为什么用 dispatch_barrier_sync了吧。 在作者初始化的时候, 使用的是 barrierQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Barrier.\(name)", attributes: .concurrent), 一个并发的队列, 我一开始没想明白为什么会这样。后来在查看官方文档中看到这么一段话:
The queue you specify should be a concurrent queue that you create yourself using the dispatch_queue_create function. If the queue you pass to this function is a serial queue or one of the global concurrent queues, this function behaves like the dispatch_sync function.
延伸阅读:通过GCD中的dispatch_barrier_(a)sync加强对sync中所谓等待的理解
2.1.2 DispatchSemaphore 信号量
接下来存在着一段这样的代码:
if fetchLoad.cancelSemaphore == nil {
    fetchLoad.cancelSemaphore = DispatchSemaphore(value: 0)
}
cancelQueue.async {
    _ = fetchLoad.cancelSemaphore?.wait(timeout: .distantFuture)
        fetchLoad.cancelSemaphore = nil
        prepareFetchLoad()
}
简答说一下信号量, 可以去看我的简易的测试文件
- 信号量 value 为0的时候,阻塞当前线程, value大于0的时候,当前线程执行;
- 当执行semaphonre.wait()的时候, value值减一;
- 当执行semaphonre.signal()的时候, value值加一;
- 初始化的时候, value值不能小于0, wait()与signal()必须配对;
那我们来分析这段代码, 初始化DispatchSemaphore(value: 0)阻塞了, 那么在接下来 _ = fetchLoad.cancelSemaphore?.wait(timeout: .distantFuture) 一直阻塞这里。 当下载失败的时候, 调用leftSignal = fetchLoad.cancelSemaphore?.signal() ?? 0。 如果下载成功了, 会直接 self.cleanFetchLoad(for: url);   如果没有失败, 是不是感觉会一直阻塞着?
当然不会, 这是因为, 当你取消任务的时候func cancelDownloadingTask(_ task: RetrieveImageDownloadTask), task.internalTask.cancel() 会发通知给func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) , 但不是立即的,系统在后台做了一些不为人知的事情, 如果就在此时, 同样的url请求进来了,那么你就需要阻塞住, 等前面的取消任务完成再执行。
2.1.3 prepareFetchLoad()
func prepareFetchLoad() {
            barrierQueue.sync(flags: .barrier) {
                let loadObjectForURL = fetchLoads[url] ?? ImageFetchLoad()
                let callbackPair = (progressBlock: progressBlock, completionHandler: completionHandler)
                loadObjectForURL.contents.append((callbackPair, options ?? KingfisherEmptyOptionsInfo))
                fetchLoads[url] = loadObjectForURL
                if let session = session {
                    started(session, loadObjectForURL)
                }
            }
        }
首先查看 fetchLoads里面是否有对应的 ImageFetchLoad 对象, 然后将回调数组保存: loadObjectForURL.contents.append((callbackPair, options ?? KingfisherEmptyOptionsInfo)); 这样就可以使得一个url, 下载一次, 但是可以多个回调的问题;
2.1.4 下载完成处理 processImage
代码太长, 就不贴了, 你可以去文件查看;
异步对下载好的图片数据进行处理, 同一个url 可能会有多个回调,因此遍历来处理, 处理完成之后回调回去;
至此, ImageDownloader的任务已经结束了;
2.1 ImageCache 缓存图片
如果你仔细看过 KingfisherOptionsInfoItem枚举, 你会发现存在两个Cache: targetCache(ImageCache) & originalCache(ImageCache);
targetCache(ImageCache)是用来缓存最终的图片的, 例如你下载好一张原图之后, 你利用 ImageProcessor 进行了处理, 例如缩小到原来的一半,  处理后的图片就是利用 targetCache(ImageCache) 来处理;
originalCache(ImageCache) 就是缓存原图;
虽然枚举值不一样, 但是都是用 ImageCache来处理;
ImageCache利用的是 NSCache 来缓存图片.
2.2.1 存储图片
首先将图片存储在缓存 NSCache中, 如果需要存储在磁盘上,利用串行队列异步的进行存储原图;
open func store(_ image: Image,
                      original: Data? = nil,
                      forKey key: String,
                      processorIdentifier identifier: String = "",
                      cacheSerializer serializer: CacheSerializer = DefaultCacheSerializer.default,
                      toDisk: Bool = true,
                      completionHandler: (() -> Void)? = nil)
    {
        let computedKey = key.computedKey(with: identifier)
        memoryCache.setObject(image, forKey: computedKey as NSString, cost: image.kf.imageCost)
        func callHandlerInMainQueue() {
            if let handler = completionHandler {
                DispatchQueue.main.async {
                    handler()
                }
            }
        }
        if toDisk {
            ioQueue.async {
                if let data = serializer.data(with: image, original: original) {
                    if !self.fileManager.fileExists(atPath: self.diskCachePath) {
                        do {
                            try self.fileManager.createDirectory(atPath: self.diskCachePath, withIntermediateDirectories: true, attributes: nil)
                        } catch _ {}
                    }
                    self.fileManager.createFile(atPath: self.cachePath(forComputedKey: computedKey), contents: data, attributes: nil)
                }
                callHandlerInMainQueue()
            }
        } else {
            callHandlerInMainQueue()
        }
    }
延伸阅读: NSCache
2.2.2 获取图片
获取图片首先从内存中获取,如果没有,在根据条件判断是否从磁盘上获取,
@discardableResult
    open func retrieveImage(forKey key: String,
                               options: KingfisherOptionsInfo?,
                     completionHandler: ((Image?, CacheType) -> Void)?) -> RetrieveImageDiskTask?
    {
        // No completion handler. Not start working and early return.
        guard let completionHandler = completionHandler else {
            return nil
        }
        var block: RetrieveImageDiskTask?
        let options = options ?? KingfisherEmptyOptionsInfo
        let imageModifier = options.imageModifier
        if let image = self.retrieveImageInMemoryCache(forKey: key, options: options) {
            options.callbackDispatchQueue.safeAsync {
                completionHandler(imageModifier.modify(image), .memory)
            }
        } else if options.fromMemoryCacheOrRefresh { // Only allows to get images from memory cache.
            options.callbackDispatchQueue.safeAsync {
                completionHandler(nil, .none)
            }
        } else {
            var sSelf: ImageCache! = self
            block = DispatchWorkItem(block: {
                // Begin to load image from disk
                if let image = sSelf.retrieveImageInDiskCache(forKey: key, options: options) {
                    if options.backgroundDecode {
                        sSelf.processQueue.async {
                            let result = image.kf.decoded
                            sSelf.store(result,
                                        forKey: key,
                                        processorIdentifier: options.processor.identifier,
                                        cacheSerializer: options.cacheSerializer,
                                        toDisk: false,
                                        completionHandler: nil)
                            options.callbackDispatchQueue.safeAsync {
                                completionHandler(imageModifier.modify(result), .memory)
                                sSelf = nil
                            }
                        }
                    } else {
                        sSelf.store(image,
                                    forKey: key,
                                    processorIdentifier: options.processor.identifier,
                                    cacheSerializer: options.cacheSerializer,
                                    toDisk: false,
                                    completionHandler: nil
                        )
                        options.callbackDispatchQueue.safeAsync {
                            completionHandler(imageModifier.modify(image), .disk)
                            sSelf = nil
                        }
                    }
                } else {
                    // No image found from either memory or disk
                    options.callbackDispatchQueue.safeAsync {
                        completionHandler(nil, .none)
                        sSelf = nil
                    }
                }
            })
            sSelf.ioQueue.async(execute: block!)
        }
        return block
    }
关于 DispatchWorkItem相关资料,你可以看这里
2.2.3 删除图片
作者给我们提供了如下方法来删除内存和缓存的图片
open func removeImage(forKey key: String,
                          processorIdentifier identifier: String = "",
                          fromMemory: Bool = true,
                          fromDisk: Bool = true,
                          completionHandler: (() -> Void)? = nil) {}
@objc public func clearMemoryCache() {}
open func clearDiskCache(completion handler: (()->())? = nil) {}
// 删除过期的缓存的文件
open func cleanExpiredDiskCache(completion handler: (()->())? = nil) {} 
在清除过期缓存文件的时候,作者将过期的文件全部删除, 如果超过了磁盘文件的大小,也会按照使用的顺序来进行删除.
open func cleanExpiredDiskCache(completion handler: (()->())? = nil) {
        // Do things in cocurrent io queue
        ioQueue.async {
            var (URLsToDelete, diskCacheSize, cachedFiles) = self.travelCachedFiles(onlyForCacheSize: false)
            for fileURL in URLsToDelete {
                do {
                    try self.fileManager.removeItem(at: fileURL)
                } catch _ { }
            }
            if self.maxDiskCacheSize > 0 && diskCacheSize > self.maxDiskCacheSize {
                let targetSize = self.maxDiskCacheSize / 2
                // Sort files by last modify date. We want to clean from the oldest files.
                let sortedFiles = cachedFiles.keysSortedByValue {
                    resourceValue1, resourceValue2 -> Bool in
                    if let date1 = resourceValue1.contentAccessDate,
                       let date2 = resourceValue2.contentAccessDate
                    {
                        return date1.compare(date2) == .orderedAscending
                    }
                    // Not valid date information. This should not happen. Just in case.
                    return true
                }
                for fileURL in sortedFiles {
                    do {
                        try self.fileManager.removeItem(at: fileURL)
                    } catch { }
                    URLsToDelete.append(fileURL)
                    if let fileSize = cachedFiles[fileURL]?.totalFileAllocatedSize {
                        diskCacheSize -= UInt(fileSize)
                    }
                    if diskCacheSize < targetSize {
                        break
                    }
                }
            }
            DispatchQueue.main.async {
                if URLsToDelete.count != 0 {
                    let cleanedHashes = URLsToDelete.map { $0.lastPathComponent }
                    NotificationCenter.default.post(name: .KingfisherDidCleanDiskCache, object: self, userInfo: [KingfisherDiskCacheCleanedHashKey: cleanedHashes])
                }
                handler?()
            }
        }
    }
    fileprivate func travelCachedFiles(onlyForCacheSize: Bool) -> (urlsToDelete: [URL], diskCacheSize: UInt, cachedFiles: [URL: URLResourceValues]) {
        let diskCacheURL = URL(fileURLWithPath: diskCachePath)
        let resourceKeys: Set<URLResourceKey> = [.isDirectoryKey, .contentAccessDateKey, .totalFileAllocatedSizeKey]
        let expiredDate: Date? = (maxCachePeriodInSecond < 0) ? nil : Date(timeIntervalSinceNow: -maxCachePeriodInSecond)
        var cachedFiles = [URL: URLResourceValues]()
        var urlsToDelete = [URL]()
        var diskCacheSize: UInt = 0
        for fileUrl in (try? fileManager.contentsOfDirectory(at: diskCacheURL, includingPropertiesForKeys: Array(resourceKeys), options: .skipsHiddenFiles)) ?? [] {
            do {
                let resourceValues = try fileUrl.resourceValues(forKeys: resourceKeys)
                // If it is a Directory. Continue to next file URL.
                if resourceValues.isDirectory == true {
                    continue
                }
                // If this file is expired, add it to URLsToDelete
                if !onlyForCacheSize,
                    let expiredDate = expiredDate,
                    let lastAccessData = resourceValues.contentAccessDate,
                    (lastAccessData as NSDate).laterDate(expiredDate) == expiredDate
                {
                    urlsToDelete.append(fileUrl)
                    continue
                }
                if let fileSize = resourceValues.totalFileAllocatedSize {
                    diskCacheSize += UInt(fileSize)
                    if !onlyForCacheSize {
                        cachedFiles[fileUrl] = resourceValues
                    }
                }
            } catch _ { }
        }
        return (urlsToDelete, diskCacheSize, cachedFiles)
    }
源码阅读-Kingfisher的更多相关文章
- 【原】FMDB源码阅读(三)
		[原]FMDB源码阅读(三) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 FMDB比较优秀的地方就在于对多线程的处理.所以这一篇主要是研究FMDB的多线程处理的实现.而 ... 
- 【原】FMDB源码阅读(二)
		[原]FMDB源码阅读(二) 本文转载请注明出处 -- polobymulberry-博客园 1. 前言 上一篇只是简单地过了一下FMDB一个简单例子的基本流程,并没有涉及到FMDB的所有方方面面,比 ... 
- 【原】FMDB源码阅读(一)
		[原]FMDB源码阅读(一) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 说实话,之前的SDWebImage和AFNetworking这两个组件我还是使用过的,但是对于 ... 
- 【原】AFNetworking源码阅读(六)
		[原]AFNetworking源码阅读(六) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 这一篇的想讲的,一个就是分析一下AFSecurityPolicy文件,看看AF ... 
- 【原】AFNetworking源码阅读(五)
		[原]AFNetworking源码阅读(五) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 上一篇中提及到了Multipart Request的构建方法- [AFHTTP ... 
- 【原】AFNetworking源码阅读(四)
		[原]AFNetworking源码阅读(四) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 上一篇还遗留了很多问题,包括AFURLSessionManagerTaskDe ... 
- 【原】AFNetworking源码阅读(三)
		[原]AFNetworking源码阅读(三) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 上一篇的话,主要是讲了如何通过构建一个request来生成一个data tas ... 
- 【原】AFNetworking源码阅读(二)
		[原]AFNetworking源码阅读(二) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 上一篇中我们在iOS Example代码中提到了AFHTTPSessionMa ... 
- 【原】AFNetworking源码阅读(一)
		[原]AFNetworking源码阅读(一) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 AFNetworking版本:3.0.4 由于我平常并没有经常使用AFNetw ... 
随机推荐
- DIY兼容机装苹果系统
			遇到问题: 无法用变色龙引导:删除原WIN系统前隐藏分区 变色龙引导画面无法进安装界面:a,wowpc.iso版本低,换新版;b,复制EXTRA进MAC安装盘 MAC OS安装完成后重新启动卡在苹果图 ... 
- NPM和webpack的关系(转载)
			NPM和webpack的关系(转载):https://blog.csdn.net/cwh0908/article/details/90769823 NPM和webpack的关系(转载) 入门前端的坑也 ... 
- python格式化当前时间,暂停一秒输出
			python格式化输出当前系统时间,可以实现暂停1秒输出时间 import timefor i in range(10): print(time.strftime("%Y-%m-%d %H: ... 
- Ehcache 入门详解 (转)
			一:目录 EhCache 简介 Hello World 示例 Spring 整合 二: 简介 2.1.基本介绍 EhCache 是一个纯Java的进程内缓存框架,具有快速.精干等特点,是Hiberna ... 
- 数据库中的round()
			Round函数返回一个数值,该数值是按照指定的小数位数进行四舍五入运算的结果.可是当保留位跟着的即使是5,有可能进位,也有可能舍去,机会各50%.这样就会造成在应用程序中计算有误. 参数规范 语法 r ... 
- Kibana多用户创建及角色权限控制
			1 介绍 ELK日志管理属于基础设施平台,接入多个应用系统是正常现象,如果接入多个系统的索引文件没有进行权限划分,那么很大程度会出现索引文件误处理现象,为了避免这种情况发生,多用户及权限设置必不可少. ... 
- Python的is和==区别
			字符串比较 1.比较字符串是否相同: ==:比较两个字符串内的value值是否相同 is:比较两个字符串的id值. 以上结果不同 比较数字时不能使用is,结果有时是True,有时是False,is 相 ... 
- 087、日志管理之 Docker logs (2019-05-09)
			参考https://www.cnblogs.com/CloudMan6/p/7749304.html 高效的监控和日志管理对保持生产系统只需稳定的运行以及排查问题至关重要. 在微服务架构中,由 ... 
- java  io   文件下载功能
			一. @RequestMapping(value = "/download/{filename}") public void downloadFile(HttpServletReq ... 
- sql:union 与union的使用和区别
			SQL UNION 操作符 UNION 操作符用于合并两个或多个 SELECT 语句的结果集. 请注意,UNION 内部的 SELECT 语句必须拥有相同数量的列.列也必须拥有相似的数据类型.同时,每 ... 
