圖片加載庫 kean/Nuke ,源代碼看看

加載一張網絡圖片流程,3 步:

  • 1, 把這張網絡圖片,下載到本地;

這時候有了一個 Datagit

  • 2, 對圖片 Data 解碼,

即拿 Data, 生成 UIImage,github

  • 3, 顯示圖片,

把生成的 UIImage, 賦給 UIImageView 的 imageapi

nuke 的調用

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 ImageViewControllermarkdown

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 ImagePipelinesession

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)
    }
複製代碼

支線,下載網絡上的資源圖片,再存一份最終的處理後的 Data 到磁盤

從上面的方法 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)
    }
複製代碼

訂閱和發佈,Subscriber 和 Publisher

蘋果官方的框架 Combine , 有 Publisher`

ReactiveCocoaRxSwift 專門搞這個的

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)
        }
    }



複製代碼

示例 repo

相關文章
相關標籤/搜索