最近公司項目中須要作秒開 h5 頁面的優化需求,因而調研了下市面上的方案,並結合本公司具體的業務需求作了一次這方面的優化實踐,這篇文章是對此次優化實踐的記錄,文末附上源代碼下載。javascript
首先來看,在 iOS 平臺加載一個 H5 網頁,須要通過哪些步驟:css
初始化 webview -> 請求頁面 -> 下載數據 -> 解析HTML -> 請求 js/css 資源 -> dom 渲染 -> 解析 JS 執行 -> JS 請求數據 -> 解析渲染 -> 下載渲染圖片html
因爲在 dom 渲染前的用戶看到的頁面都是白屏,優化思路具體也是去分析在 dom 渲染前每一個步驟的耗時,去優化性價比最高的部分。這裏面又能夠分爲前端能作的優化,以及客戶端能作的優化,前端這個須要前端那邊配合,暫且不在這篇文章中討論,這邊文章主要討論的是客戶端能作的優化思路。整體思路大概也是這樣:前端
在客戶端加載一個 網頁和在 PC 上加載一個網頁不太同樣,在 PC 上,直接在瀏覽器中輸入一個 url 就開始創建鏈接了,而在客戶端上須要先啓動瀏覽器內核
,初始化一些 webview 的全局服務和資源
,再開始創建鏈接
,能夠看一下美團測試的這個階段的耗時大概是多少:java
在客戶端第一次打開 h5 頁面,會有一個 webview 初始化的耗時,git
能夠看到數據在使用 WKWebView 的狀況下,首次初始化的時間耗時有 760 多毫秒,因此若是可以在打開網頁的時候使用已經初始化好了的 webview 來加載,那麼這部分的耗時就沒有了。github
這邊實現了一個 webview 緩衝池的方案,在 App 啓動的時候就初始化了,在須要打開網頁的時候直接從緩衝池裏面去取 webview 就行:web
+ (void)load
{
[WebViewReusePool swiftyLoad];
}
@objc public static func swiftyLoad() {
NotificationCenter.default.addObserver(self, selector: #selector(didFinishLaunchingNotification), name: UIApplication.didFinishLaunchingNotification, object: nil)
}
@objc static func didFinishLaunchingNotification() {
// 預先初始化webview
WebViewReusePool.shared.prepareWebView()
}
func prepareWebView() {
DispatchQueue.main.async {
let webView = ReuseWebView(frame: CGRect.zero, configuration: self.defaultConfigeration)
self.reusableWebViewSet.insert(webView)
}
}
複製代碼
在 iOS 11 及其以上系統上能夠 WKWebView 提供的 setURLSchemeHandler
方法添加自定義的 Scheme,相比較NSURLProtocol 私有 api 的方案沒有審覈風險,而後就能夠在 WKURLSchemeHandler 協議裏面攔截全部的自定義請求了:算法
// 自定義攔截請求開始
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
let headers = urlSchemeTask.request.allHTTPHeaderFields
guard let accept = headers?["Accept"] else { return }
guard let requestUrlString = urlSchemeTask.request.url?.absoluteString else { return }
if accept.count >= "text".count && accept.contains("text/html") {
// html 攔截
print("html = \(String(describing: requestUrlString))")
// 加載本地的緩存資源
loadLocalFile(fileName: creatCacheKey(urlSchemeTask: urlSchemeTask), urlSchemeTask: urlSchemeTask)
} else if (requestUrlString.isJSOrCSSFile()) {
// js || css 文件
print("js || css = \(String(describing: requestUrlString))")
loadLocalFile(fileName: creatCacheKey(urlSchemeTask: urlSchemeTask), urlSchemeTask: urlSchemeTask)
} else if accept.count >= "image".count && accept.contains("image") {
// 圖片
print("image = \(String(describing: requestUrlString))")
guard let originUrlString = urlSchemeTask.request.url?.absoluteString.replacingOccurrences(of: "customscheme", with: "https") else { return }
// 圖片可使用 SDWebImageManager 提供的緩存策略
SDWebImageManager.shared.loadImage(with: URL(string: originUrlString), options: SDWebImageOptions.retryFailed, progress: nil) { (image, data, error, type, _, _) in
if let image = image {
guard let imageData = image.jpegData(compressionQuality: 1) else { return }
// 資源不存在就從新發送請求
self.resendRequset(urlSchemeTask: urlSchemeTask, mineType: "image/jpeg", requestData: imageData)
} else {
self.loadLocalFile(fileName: self.creatCacheKey(urlSchemeTask: urlSchemeTask), urlSchemeTask: urlSchemeTask)
}
}
} else {
// other resources
print("other resources = \(String(describing: requestUrlString))")
guard let cacheKey = self.creatCacheKey(urlSchemeTask: urlSchemeTask) else { return }
requestRomote(fileName: cacheKey, urlSchemeTask: urlSchemeTask)
}
}
/// 自定義請求結束時調用
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
}
複製代碼
這裏使用 swift 實現了內存和磁盤兩種緩存邏輯,主要參(chao)考(xi)了 YYCache 的思路和源碼,內存緩存利用雙鏈表(邏輯) + hashMap(存儲)
實現LRU
緩存淘汰算法 ,增刪改查都是 O(1) 時間複雜度,磁盤緩存使用了沙盒文件存儲。兩種緩存都實現了緩存時長、緩存數量、緩存大小三個維度的緩存管理。json
使用協議的方式定義了接口 API:
protocol Cacheable {
associatedtype ObjectType
/// 緩存總數量
var totalCount: UInt { get }
/// 緩存總大小
var totalCost: UInt { get }
/// 緩存是否存在
///
/// - Parameter key: 緩存key
/// - Returns: 結果
func contain(forKey key: AnyHashable) -> Bool
/// 返回指定key的緩存
///
/// - Parameter key:
/// - Returns:
func object(forKey key: AnyHashable) -> ObjectType?
/// 設置緩存 k、v
///
/// - Parameters:
/// - object:
/// - key:
func setObject(_ object: ObjectType, forKey key: AnyHashable)
/// 設置緩存 k、v、c
///
/// - Parameters:
/// - object:
/// - key:
/// - cost:
func setObject(_ object: ObjectType, forKey key: AnyHashable, withCost cost: UInt)
/// 刪除指定key的緩存
///
/// - Parameter key:
func removeObject(forKey key: AnyHashable)
/// 刪除全部緩存
func removeAllObject()
/// 根據緩存大小清理
///
/// - Parameter cost: 緩存大小
func trim(withCost cost: UInt)
/// 根據緩存數量清理
///
/// - Parameter count: 緩存數量
func trim(withCount count: UInt)
/// 根據緩存時長清理
///
/// - Parameter age: 緩存時長
func trim(withAge age: TimeInterval)
}
extension Cacheable {
func setObject(_ object: ObjectType, forKey key: AnyHashable) {
setObject(object, forKey: key, withCost: 0)
}
}
複製代碼
/// h5 頁面資源緩存
class H5ResourceCache: NSObject {
/// 內存緩存大小:10M
private let kMemoryCacheCostLimit: UInt = 10 * 1024 * 1024
/// 磁盤文件緩存大小: 10M
private let kDiskCacheCostLimit: UInt = 10 * 1024 * 1024
/// 磁盤文件緩存時長:30 分鐘
private let kDiskCacheAgeLimit: TimeInterval = 30 * 60
private var memoryCache: MemoryCache
private var diskCache: DiskFileCache
override init() {
memoryCache = MemoryCache.shared
memoryCache.costLimit = kMemoryCacheCostLimit
diskCache = DiskFileCache(cacheDirectoryName: "H5ResourceCache")
diskCache.costLimit = kDiskCacheCostLimit
diskCache.ageLimit = kDiskCacheAgeLimit
super.init()
}
func contain(forKey key: String) -> Bool {
return memoryCache.contain(forKey: key) || diskCache.contain(forKey: key)
}
func setData(data: Data, forKey key: String) {
guard let dataString = String(data: data, encoding: .utf8) else { return }
memoryCache.setObject(dataString.data(using: .utf8) as Any, forKey: key, withCost: UInt(data.count))
diskCache.setObject(dataString.data(using: .utf8)!, forKey: key, withCost: UInt(data.count))
}
func data(forKey key: String) -> Data? {
if let data = memoryCache.object(forKey: key) {
print("這是內存緩存")
return data as? Data
} else {
guard let data = diskCache.object(forKey: key) else { return nil}
memoryCache.setObject(data, forKey: key, withCost: UInt(data.count))
print("這是磁盤緩存")
return data
}
}
func removeData(forKey key: String) {
memoryCache.removeObject(forKey: key)
diskCache.removeObject(forKey: key)
}
func removeAll() {
memoryCache.removeAllObject()
diskCache.removeAllObject()
}
}
複製代碼
The task has already been stopped
的錯誤// MARK:- 請求攔截開始
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
holdUrlSchemeTasks[urlSchemeTask.description] = true
}
/// 自定義請求結束時調用
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
holdUrlSchemeTasks[urlSchemeTask.description] = false
}
// 須要用到的 urlSchemeTask 實例的地方,加一層判斷
// urlSchemeTask 是否提早結束,結束了調用實例方法會崩潰
if let isValid = self.holdUrlSchemeTasks[urlSchemeTask.description] {
if !isValid {
return
}
}
複製代碼
添加網絡請求響應接收格式:
manager.responseSerializer.acceptableContentTypes = Set(arrayLiteral: "text/html", "application/json", "text/json", "text/javascript", "text/plain", "application/javascript", "text/css", "image/svg+xml", "application/font-woff2", "application/octet-stream")
複製代碼
// 白屏
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if webview.title == nil {
webview.reload()
}
}
// 白屏
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
webView.reload()
}
複製代碼
分享我的技術學習記錄和跑步馬拉松訓練比賽、讀書筆記等內容,感興趣的朋友能夠關注個人公衆號「青爭哥哥」。