這時候有了一個 Datagit
即拿 Data, 生成 UIImage,github
把生成的 UIImage, 賦給 UIImageView 的 imageapi
let request = ImageRequest(
url: URL(string: "https://user-images.githubusercontent.com/1567433/59150453-178bbb80-8a24-11e9-94ca-fd8dff6e2a9a.jpeg")!,
processors: processors
)
var options = ImageLoadingOptions(transition: .fadeIn(duration: 0.5))
options.pipeline = pipeline
// request, 網絡圖片資源連接 ( 對應第一步 )
// options, 效果
// imageView, 來顯示的 UIImageView, ( 對應第 3 步 )
loadImage(with: request, options: options, into: imageView)
複製代碼
全局方法,調用入口緩存
public func loadImage(with request: ImageRequestConvertible,
options: ImageLoadingOptions = ImageLoadingOptions.shared,
into view: ImageDisplayingView,
progress: ImageTask.ProgressHandler? = nil,
completion: ImageTask.Completion? = nil) -> ImageTask? {
// 判斷主線程
assert(Thread.isMainThread)
// ImageViewController 是一個 manager
let controller = ImageViewController.controller(for: view)
return controller.loadImage(with: request.asImageRequest(), options: options, progress: progress, completion: completion)
}
複製代碼
進入下一層,性能優化
膠水層 class ImageViewController
markdown
func loadImage(with request: ImageRequest,
options: ImageLoadingOptions,
progress progressHandler: ImageTask.ProgressHandler? = nil,
completion: ImageTask.Completion? = nil) -> ImageTask? {
// 重置狀態
cancelOutstandingTask()
// ...
let pipeline = options.pipeline ?? ImagePipeline.shared
// ...
// 通用的兩級緩存,先查內存有無圖片
// ...
// 先放佔位圖
task = pipeline.loadImageB(with: request, isMainThreadConfined: true, queue: .main) { [weak self] task, event in
switch event {
case .progress:
// ...
// 過程處理
case let .value(response, isCompleted):
if isCompleted {
// 完成了,展現圖片
self?.handle(result: .success(response), fromMemCache: false, options: options)
completion?(.success(response))
} else {
if options.isProgressiveRenderingEnabled {
self?.handle(partialImage: response, options: options)
}
progressHandler?(response, task.completedUnitCount, task.totalUnitCount)
}
case let .error(error):
// ...
// 錯誤處理
}
}
return task
}
複製代碼
主要作事情的,網絡
是圖片下載與解碼管道 class ImagePipeline
session
func loadImage(with request: ImageRequest,
isMainThreadConfined: Bool,
queue: DispatchQueue?,
observer: @escaping (ImageTask, Task<ImageResponse, Error>.Event) -> Void) -> ImageTask {
let request = inheritOptions(request)
// 拿相關信息,封裝爲ImageTask
let task = ImageTask(taskId: nextTaskId.increment(), request: request, isMainThreadConfined: isMainThreadConfined, isDataTask: false, queue: queue)
task.pipeline = self
self.queue.async {
// 開啓圖片任務
// observer 是上面的 event 事件代碼
self.startImageTask(task, observer: observer)
}
return task
}
複製代碼
開啓圖片任務併發
func startImageTask(_ task: ImageTask, observer: @escaping (ImageTask, Task<ImageResponse, Error>.Event) -> Void) {
// ...
// 獲取待解碼的圖片
tasks[task] = getDecompressedImage(for: task.request)
.subscribe(priority: task._priority) { [weak self, weak task] event in
guard let self = self, let task = task else { return }
// ...
// 事件完成,重置狀態
if event.isCompleted {
self.tasks[task] = nil
}
// 拿到過程數據,傳遞給上一步
(task.queue ?? self.configuration.callbackQueue).async {
guard !task.isCancelled else { return }
if case let .progress(progress) = event {
task.setProgress(progress)
}
observer(task, event)
}
}
}
複製代碼
獲取圖片框架
func getDecompressedImage(for request: ImageRequest) -> DecompressedImageTask.Publisher {
let key = request.makeLoadKeyForFinalImage()
return decompressedImageFetchTasks.task(withKey: key, starter: { task in
// 實際獲取圖片的方法,其餘都是簡單封裝
self.performDecompressedImageFetchTask(task, request: request)
}).publisher
}
複製代碼
實際獲取圖片的方法
func performDecompressedImageFetchTask(_ task: DecompressedImageTask, request: ImageRequest) {
// 圖片兩級緩存
// 先看內存中的圖片
if let image = cachedImage(for: request) {
let response = ImageResponse(container: image)
if image.isPreview {
task.send(value: response)
} else {
return task.send(value: response, isCompleted: true)
}
}
guard let dataCache = configuration.dataCache, configuration.dataCacheOptions.storedItems.contains(.finalImage), request.cachePolicy != .reloadIgnoringCachedData else {
// 下載網絡上的資源圖片
return loadDecompressedImage(for: request, task: task)
}
// 先看磁盤中的圖片
let key = cacheKey(for: request, item: .finalImage)
let operation = BlockOperation { [weak self, weak task] in
guard let self = self, let task = task else { return }
// ...
// 先拿二進制 Data
let data = dataCache.cachedData(for: key)
// ...
self.queue.async {
if let data = data {
// 若是存在二進制數據 Data, 就解碼獲得 UIImage
self.decodeProcessedImageData(data, for: request, task: task)
} else {
// 若是不存在二進制數據 Data, 就下載網絡上的資源圖片
self.loadDecompressedImage(for: request, task: task)
}
}
}
task.operation = operation
configuration.dataCachingQueue.addOperation(operation)
}
複製代碼
I , 下載網絡上的資源圖片
func loadDecompressedImage(for request: ImageRequest, task: DecompressedImageTask) {
// 拿網絡請求 request, 去獲取圖片
task.dependency = getProcessedImage(for: request).subscribe(task) { [weak self] image, isCompleted, task in
// 拿到最終的圖片數據,先編碼 encode,存一份到磁盤
self?.storeDecompressedImageInDataCache(image, request: request)
// 把 Data 數據解壓爲 UIImage,存一份到內存
// 發送完成事件,給回調代碼消費
self?.decompressProcessedImage(image, isCompleted: isCompleted, for: request, task: task)
}
}
複製代碼
繼續網絡請求
func getProcessedImage(for request: ImageRequest) -> ProcessedImageTask.Publisher {
guard !request.processors.isEmpty else {
// 沒有濾鏡處理,直接下載
return getOriginalImage(for: request) // No processing needed
}
// 下載,並進行濾鏡處理
let key = request.makeLoadKeyForFinalImage()
return processedImageFetchTasks.task(withKey: key, starter: { task in
self.performProcessedImageFetchTask(task, request: request)
}).publisher
}
複製代碼
進行網絡請求圖片
func getOriginalImage(for request: ImageRequest) -> OriginalImageTask.Publisher {
let key = request.makeLoadKeyForOriginalImage()
return originalImageFetchTasks.task(withKey: key, starter: { task in
let context = OriginalImageTaskContext(request: request)
// 發起網絡請求任務
task.dependency = self.getOriginalImageData(for: request)
.subscribe(task) { [weak self] value, isCompleted, task in
// 解碼圖片,提供回調
self?.decodeData(value.0, urlResponse: value.1, isCompleted: isCompleted, task: task, context: context)
}
}).publisher
}
複製代碼
發起網絡請求任務
unc getOriginalImageData(for request: ImageRequest) -> OriginalImageDataTask.Publisher {
let key = request.makeLoadKeyForOriginalImage()
return originalImageDataFetchTasks.task(withKey: key, starter: { task in
let context = OriginalImageDataTaskContext(request: request)
if self.configuration.isRateLimiterEnabled {
// 延遲加載網絡資源圖片
self.rateLimiter.execute { [weak self, weak task] in
guard let self = self, let task = task, !task.isDisposed else {
return false
}
self.performOriginalImageDataTask(task, context: context)
return true
}
} else {
// 直接加載,網絡資源圖片
self.performOriginalImageDataTask(task, context: context)
}
}).publisher
}
複製代碼
直接加載,網絡資源圖片
經過 OperationQueue, 限制磁盤緩存的線程併發數目
func performOriginalImageDataTask(_ task: OriginalImageDataTask, context: OriginalImageDataTaskContext) {
// 能用磁盤圖片,就用磁盤圖片
// ...
let key = cacheKey(for: context.request, item: .originalImageData)
let operation = BlockOperation { [weak self, weak task] in
guard let self = self, let task = task else { return }
let log = Log(self.log, "Read Cached Image Data")
log.signpost(.begin)
// 不停檢查內存圖片
let data = cache.cachedData(for: key)
log.signpost(.end)
self.queue.async {
if let data = data {
task.send(value: (data, nil), isCompleted: true)
} else {
// 網絡資源加載
self.loadImageData(for: task, context: context)
}
}
}
task.operation = operation
configuration.dataCachingQueue.addOperation(operation)
}
複製代碼
網絡資源加載
經過 OperationQueue, 限制下載的線程併發數目
func loadImageData(for task: OriginalImageDataTask, context: OriginalImageDataTaskContext) {
let operation = Operation(starter: { [weak self, weak task] finish in
guard let self = self, let task = task else {
return finish()
}
self.queue.async {
self.loadImageData(for: task, context: context, finish: finish)
}
})
configuration.dataLoadingQueue.addOperation(operation)
task.operation = operation
}
複製代碼
func loadImageData(for task: OriginalImageDataTask, context: OriginalImageDataTaskContext, finish: @escaping () -> Void) {
// ...
// 任務取消的時機
var urlRequest = context.request.urlRequest
// 作斷點下載
// ...
let dataTask = configuration.dataLoader.loadData(
with: urlRequest,
didReceiveData: { [weak self, weak task] data, response in
// 接收數據的過程
guard let self = self, let task = task else { return }
self.queue.async {
self.imageDataLoadingTask(task, context: context, didReceiveData: data, response: response, log: log)
}
},
completion: { [weak self, weak task] error in
// 完成下載
finish() // Finish the operation!
guard let self = self, let task = task else { return }
self.queue.async {
log.signpost(.end, "Finished with size \(Log.bytes(context.data.count))")
self.imageDataLoadingTask(task, context: context, didFinishLoadingDataWithError: error)
}
})
task.onCancelled = { [weak self] in
// 取消下載任務
guard let self = self else { return }
log.signpost(.end, "Cancelled")
dataTask.cancel()
finish() // Finish the operation!
self.tryToSaveResumableData(for: context)
}
}
複製代碼
下載圖片的最後一環
進入 DataLoader
這個類
調用內部類 _DataLoader
public func loadDataZ(with request: URLRequest,
didReceiveData: @escaping (Data, URLResponse) -> Void,
completion: @escaping (Swift.Error?) -> Void) -> Cancellable {
return impl.loadData(with: request, session: session, didReceiveData: didReceiveData, completion: completion)
}
複製代碼
DataLoader
這個類, 初始化的時候,就設置好了網絡代理
public init(configuration: URLSessionConfiguration = DataLoader.defaultConfiguration,
validate: @escaping (URLResponse) -> Swift.Error? = DataLoader.validate) {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
// 設置網絡代理,給內部類 `_DataLoader`
self.session = URLSession(configuration: configuration, delegate: impl, delegateQueue: queue)
self.impl.validate = validate
self.impl.observer = self
}
複製代碼
內部類 _DataLoader
具體請求網絡
請求網絡圖片
/// Loads data with the given request.
func loadData(with request: URLRequest,
session: URLSession,
didReceiveData: @escaping (Data, URLResponse) -> Void,
completion: @escaping (Error?) -> Void) -> Cancellable {
let task = session.dataTask(with: request)
let handler = _Handler(didReceiveData: didReceiveData, completion: completion)
session.delegateQueue.addOperation { // `URLSession` is configured to use this same queue
self.handlers[task] = handler
}
task.resume()
send(task, .resumed)
return task
}
複製代碼
接收網絡圖片的數據
// MARK: URLSessionDelegate
func urlSession(_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive response: URLResponse,
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
send(dataTask, .receivedResponse(response: response))
guard let handler = handlers[dataTask] else {
completionHandler(.cancel)
return
}
if let error = validate(response) {
handler.completion(error)
completionHandler(.cancel)
return
}
completionHandler(.allow)
}
複製代碼
從上面的方法
I , 下載網絡上的資源圖片
繼續
保存最終的圖片二進制數據,到磁盤
func storeDecompressedImageInDataCache(_ response: ImageResponse, request: ImageRequest) {
//...
// 若是不用處理,就沒有最終版圖片,就算了
let context = ImageEncodingContext(request: request, image: response.image, urlResponse: response.urlResponse)
let encoder = configuration.makeImageEncoder(context)
configuration.imageEncodingQueue.addOperation { [weak self] in
guard let self = self else { return }
// ...
// 先把圖片編碼爲二進制 Data
let encodedData = encoder.encode(response.container, context: context)
// ...
guard let data = encodedData else { return }
let key = self.cacheKey(for: request, item: .finalImage)
// 把二進制 Data 寫入磁盤
dataCache.storeData(data, for: key)
}
}
複製代碼
把二進制 Data 寫入磁盤
進入 DataCache
這個類
public func storeData(_ data: Data, for key: Key) {
// 執行操做
stage {
// 定義行爲
staging.add(data: data, for: key)
}
}
複製代碼
執行操做
用了一個線程鎖, NSLock
private func stage(_ change: () -> Void) {
lock.lock()
change()
setNeedsFlushChanges()
lock.unlock()
}
複製代碼
進入磁盤刷新
改標記
private func setNeedsFlushChanges() {
guard !isFlushNeeded else { return }
isFlushNeeded = true
scheduleNextFlush()
}
複製代碼
延遲調度
private func scheduleNextFlush() {
guard !isFlushScheduled else { return }
isFlushScheduled = true
queue.asyncAfter(deadline: .now() + flushInterval, execute: flushChangesIfNeeded)
}
複製代碼
寫入磁盤, 調度
private func flushChangesIfNeeded() {
// Create a snapshot of the recently made changes
let staging: Staging
lock.lock()
guard isFlushNeeded else {
return lock.unlock()
}
staging = self.staging
isFlushNeeded = false
lock.unlock()
// 寫入磁盤
performChanges(for: staging)
// ...
// 調度機制
}
複製代碼
寫入磁盤,
用了一個自動對象釋放池
關心結果,不在乎過程,
過程當中處理的對象內存較大,
autoreleasepool
private func performChanges(for staging: Staging) {
autoreleasepool {
if let change = staging.changeRemoveAll {
perform(change)
}
for change in staging.changes.values {
perform(io: change)
}
}
}
複製代碼
寫入磁盤, 關鍵代碼很少,
線程管理,和性能優化上,繞來繞去
經過標記延時操做,儘量同一時間段,寫入磁盤
不會頻繁觸發寫入
關鍵代碼,就建立文件夾,寫入數據
private func perform(io change: Staging.Change) {
guard let url = url(for: change.key) else {
return
}
switch change.type {
case let .add(data):
do {
// 寫入數據
try data.write(to: url)
} catch let error as NSError {
// 寫入數據失敗
guard error.code == CocoaError.fileNoSuchFile.rawValue && error.domain == CocoaError.errorDomain else { return }
// 建立文件夾
try? FileManager.default.createDirectory(at: self.path, withIntermediateDirectories: true, attributes: nil)
// 寫入數據
try? data.write(to: url) // re-create a directory and try again
}
case .remove:
try? FileManager.default.removeItem(at: url)
}
}
複製代碼
autoreleasepool
上面的文件 IO ,用了一次
圖片解碼,也用到了
圖片解碼,耗 CPU, 流程多 ( 顏色模式轉換、採樣、分塊、離散餘弦變換 ...)
咱們只關心處理好的圖片
func decode(_ data: Data, urlResponse: URLResponse?, isCompleted: Bool) -> ImageResponse? {
func _decode() -> ImageContainer? {
if isCompleted {
return decode(data)
} else {
return decodePartiallyDownloadedData(data)
}
}
guard let container = autoreleasepool(invoking: _decode) else {
return nil
}
ImageDecompression.setDecompressionNeeded(true, for: container.image)
return ImageResponse(container: container, urlResponse: urlResponse)
}
複製代碼
蘋果官方的框架 Combine
, 有 Publisher`
ReactiveCocoa
和 RxSwift
專門搞這個的
Nuke
經過匿名函數 Block 和簡單的結構體,封裝實現
Task
這個類,包含一個類 Publisher
類 Publisher
包含一個任務,可被訂閱,
訂閱以後,有 3 種行爲,下載失敗,下載成功有值,
下載過程當中,獲得部分數據
final class Task<Value, Error>{
struct Publisher {
let task: Task
// 訂閱後,有三種行爲
// 訂閱 Publisher, 就得傳入行爲
// 傳入存在事件的時候,須要執行的匿名函數行爲
func subscribe<NewValue>(_ task: Task<NewValue, Error>, onValue: @escaping (Value, Bool, Task<NewValue, Error>) -> Void) -> TaskSubscription? {
return subscribe { [weak task] event in
guard let task = task else { return }
switch event {
case let .value(value, isCompleted):
onValue(value, isCompleted, task)
case let .progress(progress):
task.send(progress: progress)
case let .error(error):
task.send(error: error)
}
}
}
}
}
複製代碼
Publisher 結構體的訂閱, 走任務的訂閱
func subscribeX(priority: TaskPriority = .normal, _ observer: @escaping (Event) -> Void) -> TaskSubscription? {
task.subscribe(priority: priority, observer)
}
複製代碼
Task 類中的訂閱,是創建任務和行爲之間的對應關係,並存爲屬性
private func subscribe(priority: TaskPriority = .normal, _ observer: @escaping (Event) -> Void) -> TaskSubscription? {
// ...
// 創建任務索引
nextSubscriptionId += 1
let subscriptionKey = nextSubscriptionId
let subscription = TaskSubscription(task: self, key: subscriptionKey)
// 創建任務和行爲之間的對應關係,並存爲屬性
subscriptions[subscriptionKey] = Subscription(observer: observer, priority: priority)
//...
return subscription
}
複製代碼
創建訂閱以後,須要的時候,
能夠發佈事件,去執行
Task
類中
func send(value: Value, isCompleted: Bool = false) {
send(event: .value(value, isCompleted: isCompleted))
}
func send(error: Error) {
send(event: .error(error))
}
func send(progress: TaskProgress) {
send(event: .progress(progress))
}
private func send(event: Event) {
guard !isDisposed else { return }
switch event {
case let .value(_, isCompleted):
if isCompleted {
terminate(reason: .finished)
}
case .progress:
break // Simply send the event
case .error:
terminate(reason: .finished)
}
for context in subscriptions.values {
context.observer(event)
}
}
複製代碼