你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> Chun 閱讀筆記

Chun 閱讀筆記

編輯:IOS開發基礎

1.jpg

Chun 是 葉純俊 在 Github 上開源的一個圖片緩存庫,基於 Swift 編寫。學習 Swift 有一段時間了,記錄一些閱讀源碼的一些收獲。

代碼組織

Swift 中通過 extension 組織代碼會讓整個類更加清晰可讀,尤其是對於 UITableViewDataSource 和 UITableViewDelegate 這種情況。在 Chun 這個項目中的 Demo 文件就是這樣的:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        ...
    }
}
extension ViewController: UITableViewDataSource, UITableViewDelegate {
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        ...
    }
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        ...
    }
    func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        ...
    }
}

在 viewDidLoad 中,為了避免初始化代碼過長導致難以閱讀,可以通過內嵌函數將代碼分段:

override func viewDidLoad() {
    super.viewDidLoad()
    func loadTableView() {
        ...
    }
    loadTableView()
}

添加屬性

在給 UIImageView 加載圖片的時候,我們最好可以在對象中存儲它所要加載的 URL ,可以通過 AssociatedObject 來實現。在 Swift 中,可以用一個私有計算量來封裝一下:

private var imageURLForChun: NSURL? {
    get {
        return objc_getAssociatedObject(self, &key) as? NSURL
    }
    set (url) {
        objc_setAssociatedObject(self, &key, url, UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC))
    }
}

這樣在調用的時候就和真實屬性沒什麼區別了:

if let imageURL = self.imageURLForChun {
    ...
}

weak 和 unowned

在避免循環強引用的時候,如果某些時候引用沒有值,那就用 weak ,如果引用總是有值,則用 unowned 。

在 Chun 這個項目中,獲取圖片之後的回調裡用的是 weak ,因為有可能圖片加載完了但是 UIImageView 已經銷毀了:

Chun.sharedInstance.fetchImageWithURL(url, complete: { [weak self](result: Result) -> Void in
    ...
})

然後在查詢本地緩存的時候,用的是 unowned ,因為這裡的 self 是單例,永遠不會銷毀:

cache.diskImageExistsWithKey(key, completion: { [unowned self](exist: Bool, diskURL: NSURL?) -> Void in
    ...
})

枚舉的正確打開方式

使用枚舉來表示返回結果是個不錯的方案,在面向軌道編程 - Swift 中的異常處理中有過詳細的探討。在 Chun 中是這樣使用的:

public enum Result {
    case Success(image: UIImage, fetchedImageURL: NSURL)
    case Error(error: NSError)
}

加載圖片完成之後的回調則是這樣:

public func fetchImageWithURL(url: NSURL, complete: (Result) -> Void) {
    let key = cacheKeyForRemoteURL(url)
    if let image = cache.imageForMemeoryCacheWithKey(key) {
        let result = Result.Success(image: image, fetchedImageURL: url)
        complete(result)
    } else {
        ...
    }
}

圖片渲染

直接從網上下載獲取到的圖片並不能直接使用,先解碼成位圖然後再渲染可以減少開銷:

func decodedImageWithImage(image: UIImage) -> UIImage {
    if image.images != nil {
        return image
    }
    let imageRef = image.CGImage
    let imageSize: CGSize = CGSizeMake(CGFloat(CGImageGetWidth(imageRef)), CGFloat(CGImageGetHeight(imageRef)))
    let imageRect = CGRectMake(0, 0, imageSize.width, imageSize.height)
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let originalBitmapInfo = CGImageGetBitmapInfo(imageRef)
    let alphaInfo = CGImageGetAlphaInfo(imageRef)
    var bitmapInfo = originalBitmapInfo
    switch (alphaInfo) {
    case .None:
        bitmapInfo &= ~CGBitmapInfo.AlphaInfoMask
        bitmapInfo |= CGBitmapInfo(CGImageAlphaInfo.NoneSkipFirst.rawValue)
    case .PremultipliedFirst, .PremultipliedLast, .NoneSkipFirst, .NoneSkipLast:
        break
    case .Only, .Last, .First:
        return image
    }
    if let context = CGBitmapContextCreate(nil, CGImageGetWidth(imageRef), CGImageGetHeight(imageRef), CGImageGetBitsPerComponent(imageRef), 0 , colorSpace, bitmapInfo) {
        CGContextDrawImage(context, imageRect, imageRef)
        let decompressedImageRef = CGBitmapContextCreateImage(context)
        if let decompressedImage = UIImage(CGImage: decompressedImageRef, scale: image.scale, orientation: image.imageOrientation) {
            return decompressedImage
        } else {
            return image
        }
    } else {
        return image
    }
}

從 NSData判斷圖片類型

在判斷圖片格式的時候,通過不同格式的第一個字節進行判斷,在 contentTypeForImageData(data: NSData) -> String? 方法裡實現了獲取 NSData 類型的方法:

func contentTypeForImageData(data: NSData) -> String? {
    var value : Int16 = 0
    if data.length >= sizeof(Int16) {
        data.getBytes(&value, length:1)
        switch (value) {
        case 0xff:
            return "image/jpeg"
        case 0x89:
            return "image/png"
        case 0x47:
            return "image/gif"
        case 0x49:
            return "image/tiff"
        case 0x4D:
            return "image/tiff"
        case 0x52:
            if (data.length < 12) {
                return nil
            }
            if let temp = NSString(data: data.subdataWithRange(NSMakeRange(0, 12)), encoding: NSASCIIStringEncoding) {
                if (temp.hasPrefix("RIFF") && temp.hasSuffix("WEBP")) {
                    return "image/webp"
                }
            }
            return nil
        default:
            return nil
        }
    }
    else {
        return nil
    }
}

判斷的依據是不同圖片格式的前幾個字節都是特殊且唯一的,具體在 File magic numbers 裡有個比較完整的表,可以對照看下。比如 jpeg 的前四個字節都是 ff d8 ff e0 。

Fetcher 的玩兒法

在獲取圖片的時候都是通過 Fetcher 獲取,根據任務不同,區分是從服務器下載還是從本地加載。

首先是 ImageFetcher 這個大基類,封裝了一些基本的屬性和方法:

class ImageFetcher {
    typealias CompeltionClosure = (FetcherResult) -> Void
    let imageURL: NSURL
    init(imageURL: NSURL) {
        self.imageURL = imageURL
    }
    deinit {
        self.completion = nil
    }
    var cancelled = false
    var completion: CompeltionClosure?
    static func fetchImage(url: NSURL, completion: CompeltionClosure?) -> ImageFetcher {
        var fetcher: ImageFetcher
        if url.fileURL {
            fetcher = DiskImageFetcher(imageURL: url)
        } else {
            fetcher = RemoteImageFetcher(imageURL: url)
        }
        fetcher.completion = completion
        fetcher.startFetch()
        return fetcher
    }
    func cancelFetch() {
        self.cancelled = true
    }
    func startFetch() {
        fatalError("Subclass need to override this method called: \"startFetch\" ")
    }
    final func failedWithError(error: NSError) {
    }
    final func succeedWithData(imageData: NSData) {
    }
}

在 fetchImage 這個方法裡,通過 url.fileURL 判斷是網絡請求還是本地請求,然後初始化不同的 fetcher 。然後對於一定需要子類實現的方法,用 fatalError 報錯提醒;對於一定不能讓子類重寫的方法,用 final 保護起來。比如請求成功之後的回調方法 succeedWithData(imageData: NSData) :

final func succeedWithData(imageData: NSData) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), { [weak self]() -> Void in
        if let strongSelf = self {
            var finalImage: UIImage!
            if let image = imageWithData(imageData) {
                finalImage = scaledImage(image)
                finalImage = decodedImageWithImage(finalImage)
                dispatch_main_async_safe {
                    if !strongSelf.cancelled {
                        if let completionClosure = strongSelf.completion {
                            let result = FetcherResult.Success(image: finalImage, imageData: imageData)
                            completionClosure(result)
                        }
                    }
                }
            } else {
                let error = NSError(domain: CHUN_ERROR_DOMAIN, code: 404, userInfo: [NSLocalizedDescriptionKey: "create Image with data failed"])
                strongSelf.failedWithError(error)
            }
        }
    })
}

不管是從本地加載還是從遠程獲取的,最終的返回結果都是 NSData ,所以在這裡統一處理。然後對於取消了的事件,其實並沒有取消下載任務,而是在下載成功之後通過 strongSelf.cancelled 判斷是不是要調用加載成功的回調方法。

然後再分別看下本地加載和網絡獲取的部分。本地加載相對而言簡單一些,通過 NSData(contentsOfURL: self.imageURL) 就可以加載圖片了。然後對於網絡請求則使用了 NSURLSession 來實現。 對 NSURLSession 不熟悉的同學可以閱讀《從 NSURLConnection 到 NSURLSession》了解一下。

網絡請求成功之後做了如下操作:

  • 檢查 self 是否還活著

  • 檢查當前任務是否被取消了

  • 檢查回調的 error 是否不為空

  • 獲取 response 並查看狀態碼是否為 200

在一切正常的前提下,還進行了如下操作:

let expected = response.expectedContentLength
var validateLengthOfData: Bool {
    if expected > -1 {
        if Int64(data!.length) >= expected {
            return true
        } else {
            return false
        }
    }
    return true
}
if validateLengthOfData {
    strongSelf.succeedWithData(data!)
    return
} else {
    let error = NSError(domain: CHUN_ERROR_DOMAIN, code: response.statusCode, userInfo: [NSLocalizedDescriptionKey: "Received bytes are not fit with expected"])
    strongSelf.failedWithError(error)
    return
}

主要是檢查實際獲取到的數據大小是否等於應有大小,通過 validateLengthOfData 這個計算量標記是否校驗通過。

緩存

圖片的緩存都是通過 ImageCache 這個類進行統一處理。初始化的時候新建了 ioQueue 這個用來專門進行 IO 操作的隊列,然後用 NSCache 在內存中緩存圖片。對於 NSCache 在 NSHipster 上有些吐槽,但這並沒有太大影響,基本可以滿足日常開發的需要。

系統事件的處理

在收到 UIApplicationDidEnterBackgroundNotification 的通知的時候,做了 backgroundCleanDisk 的處理:

private func backgroundCleanDisk() {
    let application = UIApplication.sharedApplication()
    var backgroundTask: UIBackgroundTaskIdentifier!
    backgroundTask = application.beginBackgroundTaskWithExpirationHandler {
        application.endBackgroundTask(backgroundTask)
        backgroundTask = UIBackgroundTaskInvalid
    }
    self.cleanDisk {
        application.endBackgroundTask(backgroundTask)
        backgroundTask = UIBackgroundTaskInvalid
    }
}

通過 beginBackgroundTaskWithExpirationHandler 在退到後台之後清空了本地的過期文件。

過期文件

判斷過期文件的關鍵在於這個方法:

let expirationDate = NSDate(timeIntervalSinceNow: ImageCache.defaultCacheMaxAge)
let modificationDate = resourceValues[NSURLContentModificationDateKey] as! NSDate
if modificationDate.laterDate(expirationDate).isEqualToDate(expirationDate) {
    ...
}

通過遍歷檢查所有的過期文件,存到 cacheFiles 數組中,然後統一刪除。

小結

通過 Chun 這個項目學習了如何實現一個簡單的圖片緩存庫,包括圖片加載和本地緩存兩個核心功能。然後通過 public class 把一些公用接口封裝並暴露出去。也看到了很多 Swift 中的小技巧,總之就是, Excited 嗯!

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved