根據網絡圖片來自定義佈局是一件很蛋疼的事情,若是須要根據圖片的大小來決定頁面控件的佈局,或者說在一個 TableView 上面有多張大小不一的圖片,咱們要根據圖片的大小的決定 Cell 的高度。玩過 Tumblr 的人可能都知道,不像微信微博之類的 App,Tumblr 在圖片佈局的時候是徹底按照圖片的大小來的(原本想截個圖的,找了半天,全是不能放出來的內容😅)。在研究了 TMTumblrSDK 以後發現,著名圖片視頻類博客 App Tumblr 有一套本身的解決方案,咱們先來看看經過 TMTumblrSDK 咱們拿到的原始數據是什麼樣的:php
{
"photoset_layout" = 1221121;
}
複製代碼
"original_size" = {
height = 1278;
url = "https://66.media.tumblr.com/this_is_iamge_url.jpg";
width = 960;
};
"alt_sizes" = ({
height = 1278;
url = "https://66.media.tumblr.com/this_is_iamge_url_1280.jpg";
width = 960;
},
{
height = 852;
url = "https://66.media.tumblr.com/this_is_iamge_url_640.jpg";
width = 640;
},
...
複製代碼
每一篇 photoSet 的博文都帶有以上的字段。不用試了, URL 都處理過😂。前端
不難理解 Tumblr 在 Sever 端返回圖片 URL 的時候,就直接給出了圖片的大小,已經相應縮略圖及其大小。另外 photoset_layout
這個字段表示一共 7 行每行分別是 1,2,2,1,1,2,1 張圖片。git
真好! 這徹底符合輕客戶端的設計,客戶端只須要拿到數據,而後佈局就能夠了。不須要再對原始數據作其餘的計算。github
若是世界都是這樣運轉的,那就完美的,惋惜。記得好久之前接到過一個項目,以前是用 Cordova 寫的,須要所有改爲 native 實現,咱們知道前端的佈局是彈性的,而 iOS 中的佈局是居於 Frame 的。在作到某個詳情頁面的時候,我拿到了幾個圖片的 URL,可惡的是他們的高度還很不同....json
都知道,圖片實際上都是結構無缺的二進制的數據流,圖片文件的頭部存儲了這個圖片的相關信息。從中咱們能夠讀取到尺寸、大小、格式等相關信息。所以,若是隻下載圖片的頭部信息,就能夠知道這個圖片的大小。而相對於下載整張圖片這個操做只須要不多的字節。swift
很明顯,這些數據的結構是跟圖片格式相關的,咱們要作的首先就是讀取圖片的頭部信息。api
這些格式的文件的開始都是相對應的簽名信息,這個簽名信息告訴咱們這個文件編碼的格式,在這段簽名信息以後就是咱們須要的圖片大小信息了。緩存
在 WIKI 上能夠看到 PNG 圖像格式文件由一個 8 字節的 PNG 文件標識域和 3 個以上的後續數據塊組成。PNG 文件的前 8 個字節老是包含了一個固定的簽名,它是用來標識這個文件的其他部分是一個 PNG 的圖像。微信
PNG定義了兩種類型的數據塊:一種是PNG文件必須包含、讀寫軟件也都必需要支持的關鍵塊(critical chunk);另外一種叫作輔助塊(ancillary chunks),PNG容許軟件忽略它不認識的附加塊。這種基於數據塊的設計,容許PNG格式在擴展時仍能保持與舊版本兼容。網絡
關鍵數據塊中有4個標準數據塊:
咱們須要關心的是 IHDR ,也就是文件頭數據塊
咱們只關心 WIDTH 以及 HEIGHT 兩個信息,所以,要得到 PNG 文件的寬高信息,只須要 33 字節。
GIF 是一種位圖圖形文件格式。他以固定長度的頭開始,緊接着是固定長度的邏輯屏幕描述符用來標記圖片的邏輯顯示大小及其餘特徵。
只須要 10 個字節咱們就可以獲取到 GIF 圖片的大小了
JPEG 格式的文件有兩種不一樣的格式:
FF D8 FF E0
開始)FF D8 FF E1
開始)因爲第一種是最爲通用的圖片格式,這篇文章只會處理這種類型的圖片格式。JPEG 格式的文件由一系列數據段組成,每格段都是由 0xFF
開頭的。他以後的一個字節用來顯示這個數據段的類型。frame 信息的數據段位於一個叫作 SOF[n]
的區段中,由於這些數據段沒有特定的順序,要找到 SOF[n]
咱們必需要跳過它前面的標記, 因此咱們須要根據前面的數據段的長度來跳過這些數據段。知道咱們找到了跟 frame 相關的標記(FFC0、FFC一、FFC2
)。
既然咱們已經知道了圖像格式的一些內部機制,咱們就能夠寫一個類來預加載圖片的大小。此外咱們還須要在這個類中維護一個 NSCache 來緩存已經預加載 frame 的 url。在實際狀況中咱們應該將這個東西保存在磁盤中。
作這個需求咱們須要至少三個類:
如上文所說,這個類是用來管理操做隊列,操做緩存、管理 URLSession 的。
public class ImageSizeFetcher: NSObject, URLSessionDataDelegate {
/// Callback type alias
public typealias Callback = ((Error?, ImageSizeFetcherParser?) -> (Void))
/// 用來下載數據的 URLSession
private var session: URLSession!
/// Queue of active operations
private var queue = OperationQueue()
/// 內置的緩存
private var cache = NSCache<NSURL,ImageSizeFetcherParser>()
/// 請求超時的時間
public var timeout: TimeInterval
/// 初始化方法
public init(configuration: URLSessionConfiguration = .ephemeral, timeout: TimeInterval = 5) {
self.timeout = timeout
super.init()
self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}
/// 請求圖片信息的方法
///
/// - Parameters:
/// - url: 圖片的 URL
/// - force: 強制從網絡獲取(不實用緩存的大小)
/// - callback: 回調
public func sizeFor(atURL url: URL, force: Bool = false, _ callback: @escaping Callback) {
guard force == false, let entry = cache.object(forKey: (url as NSURL)) else {
// 不須要緩存,或者須要直接獲取
let request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: self.timeout)
let op = ImageSizeFetcherOp(self.session.dataTask(with: request), callback: callback)
queue.addOperation(op)
return
}
// 回調緩存的數據
callback(nil,entry)
}
//MARK: - Helper Methods
private func operation(forTask task: URLSessionTask?) -> ImageSizeFetcherOp? {
return (self.queue.operations as! [ImageSizeFetcherOp]).first(where: { $0.url == task?.currentRequest?.url })
}
//MARK: - URLSessionDataDelegate
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
operation(forTask: dataTask)?.onReceiveData(data)
}
public func urlSession(_ session: URLSession, task dataTask: URLSessionTask, didCompleteWithError error: Error?) {
operation(forTask: dataTask)?.onEndWithError(error)
}
}
複製代碼
這個類是 Operation 的子類,他用來執行數據下載的邏輯。
一個 Operation 從 URLSession 中獲取數據。當接收到數據的時候立刻調用 ImageParser,當獲取到有效的結果的時候,取消下載任務,並將結果回調回去。
internal class ImageSizeFetcherOp: Operation {
let callback: ImageSizeFetcher.Callback?
let request: URLSessionDataTask
private(set) var receivedData = Data()
var url: URL? {
return self.request.currentRequest?.url
}
init(_ request: URLSessionDataTask, callback: ImageSizeFetcher.Callback?) {
self.request = request
self.callback = callback
}
///MARK: - Operation Override Methods
override func start() {
guard !self.isCancelled else { return }
self.request.resume()
}
override func cancel() {
self.request.cancel()
super.cancel()
}
//MARK: - Internal Helper Methods
func onReceiveData(_ data: Data) {
guard !self.isCancelled else { return }
self.receivedData.append(data)
// 數據太少
guard data.count >= 2 else { return }
// 嘗試解析數據,若是獲得了足夠的信息,取消任務
do {
if let result = try ImageSizeFetcherParser(sourceURL: self.url!, data) {
self.callback?(nil,result)
self.cancel()
}
} catch let err {
self.callback?(err,nil)
self.cancel()
}
}
func onEndWithError(_ error: Error?) {
self.callback?(ImageParserErrors.network(error),nil)
self.cancel()
}
}
複製代碼
它是這個組件的核心,他拿到 Data ,而後用支持的格式解析數據。
首先在流開始的時候檢查文件的簽名,若是沒有找到,返回不支持的格式異常。
確認簽名以後,檢查數據的長度,只有拿到足夠長度的數據以後,解析起纔會進一步檢索 frame。
若是有足夠的數據,開始檢索 frame。這個過程是很是快的。由於除了 JPEG 之外,全部的格式都只須要拿到固定的長度。
由於 JPEG 的格式問題,他須要在內部進行一下遍歷。
public class ImageSizeFetcherParser {
/// 支持的圖片類型
public enum Format {
case jpeg, png, gif, bmp
// 須要下載的最小的字節數。當獲取到了該長度的字節以後,就回中止下載操做
// 爲 nil 表示這個文件格式須要下載的長度不固定。
var minimumSample: Int? {
switch self {
case .jpeg: return nil // will be checked by the parser (variable data is required)
case .png: return 25
case .gif: return 11
case .bmp: return 29
}
}
/// 用來識別文件格式
///
/// - Throws: 若是沒有支持的格式,就跑出異常
internal init(fromData data: Data) throws {
var length = UInt16(0)
(data as NSData).getBytes(&length, range: NSRange(location: 0, length: 2))
switch CFSwapInt16(length) {
case 0xFFD8: self = .jpeg
case 0x8950: self = .png
case 0x4749: self = .gif
case 0x424D: self = .bmp
default: throw ImageParserErrors.unsupportedFormat
}
}
}
public let format: Format
public let size: CGSize
public let sourceURL: URL
public private(set) var downloadedData: Int
internal init?(sourceURL: URL, _ data: Data) throws {
let imageFormat = try ImageSizeFetcherParser.Format(fromData: data) // 獲取圖片格式
// 若是成功的獲取到了圖片格式,就去獲取 frame
guard let size = try ImageSizeFetcherParser.imageSize(format: imageFormat, data: data) else {
return nil
}
// 找到了圖片的大小
self.format = imageFormat
self.size = size
self.sourceURL = sourceURL
self.downloadedData = data.count
}
// 獲取圖片的大小
private static func imageSize(format: Format, data: Data) throws -> CGSize? {
if let minLen = format.minimumSample, data.count <= minLen {
return nil
}
switch format {
case .bmp:
var length: UInt16 = 0
(data as NSData).getBytes(&length, range: NSRange(location: 14, length: 4))
var w: UInt32 = 0; var h: UInt32 = 0;
(data as NSData).getBytes(&w, range: (length == 12 ? NSMakeRange(18, 4) : NSMakeRange(18, 2)))
(data as NSData).getBytes(&h, range: (length == 12 ? NSMakeRange(18, 4) : NSMakeRange(18, 2)))
return CGSize(width: Int(w), height: Int(h))
case .png:
var w: UInt32 = 0; var h: UInt32 = 0;
(data as NSData).getBytes(&w, range: NSRange(location: 16, length: 4))
(data as NSData).getBytes(&h, range: NSRange(location: 20, length: 4))
return CGSize(width: Int(CFSwapInt32(w)), height: Int(CFSwapInt32(h)))
case .gif:
var w: UInt16 = 0; var h: UInt16 = 0
(data as NSData).getBytes(&w, range: NSRange(location: 6, length: 2))
(data as NSData).getBytes(&h, range: NSRange(location: 8, length: 2))
return CGSize(width: Int(w), height: Int(h))
case .jpeg:
var i: Int = 0
// 檢查 JPEG 是否是 文件交換類型(SOI)
guard data[i] == 0xFF && data[i+1] == 0xD8 && data[i+2] == 0xFF && data[i+3] == 0xE0 else {
throw ImageParserErrors.unsupportedFormat //不是 SOI
}
i += 4
// 肯定是 JFIF 類型
guard data[i+2].char == "J" &&
data[i+3].char == "F" &&
data[i+4].char == "I" &&
data[i+5].char == "F" &&
data[i+6] == 0x00 else {
throw ImageParserErrors.unsupportedFormat
}
var block_length: UInt16 = UInt16(data[i]) * 256 + UInt16(data[i+1])
repeat {
i += Int(block_length)
if i >= data.count {
return nil
}
if data[i] != 0xFF {
return nil
}
if data[i+1] >= 0xC0 && data[i+1] <= 0xC3 { // 找到了 C0 C1 C2 C3
var w: UInt16 = 0; var h: UInt16 = 0;
(data as NSData).getBytes(&h, range: NSMakeRange(i + 5, 2))
(data as NSData).getBytes(&w, range: NSMakeRange(i + 7, 2))
let size = CGSize(width: Int(CFSwapInt16(w)), height: Int(CFSwapInt16(h)) );
return size
} else {
i+=2;
block_length = UInt16(data[i]) * 256 + UInt16(data[i+1]);
}
} while (i < data.count)
return nil
}
}
}
複製代碼
如今只須要這樣就能獲取到圖片的大小了:
let imageURL: URL = ...
fetcher.sizeFor(atURL: $0.url) { (err, result) in
print("Image size is \(NSStringFromCGSize(result.size))")
}
複製代碼
仍是強烈建議使用 Tumblr 的方案。畢竟輕客戶端纔是王道啊😂