5首页加载慢_UIViewController 预加载方案浅谈
作者 |?hite,目前在網易嚴選iOS 組,主要工作內容 webview 相關,業余時間會寫一些胡思亂想產品策劃稿,各類游戲云玩家。
一. 引子
預加載作為常規性能優化手段,在所有性能敏感的場景都有使用。不同的場景會有不同的方案。舉個例子,網易郵箱簡約郵里,收件箱列表使用了數據預加載,首頁加載完畢后會加載后一頁的分頁數據,在用戶繼續翻頁時,能極大提升響應速度;在微信公眾號列表,不僅預加載了多個分頁數據,還加載了某個公眾文章的文字部分,所以當列表加載完畢之后,你走到了沒有網絡的電梯里,依然可以點擊某個文字,閱讀文字部分,圖片是空白。
在 iOS 常規的優化方案中,預加載也是極常見的手段,多見于:預加載圖片、配置文件、離線包等業務資源。查閱后知, ASDK 有一套很智能的預加載策略;
在滾動方向(Leading)上 Fetch Data 區域會是非滾動方向(Trailing)的兩倍,ASDK 會根據滾動方向的變化實時改變緩沖區的位置;在向下滾動時,下面的 Fetch Data 區域就是上面的兩倍,向上滾動時,上面的 Fetch Data 區域就是下面的兩倍。
系統層面,iOS 10 里UIKit還為開發者新增了UITableViewDataSourcePrefetching
@protocol?UITableViewDataSourcePrefetching?<NSObject>@required//?indexPaths?are?ordered?ascending?by?geometric?distance?from?the?table?view-?(void)tableView:(UITableView?*)tableView?prefetchRowsAtIndexPaths:(NSArray<NSIndexPath?*>?*)indexPaths;@optional//?indexPaths?that?previously?were?considered?as?candidates?for?pre-fetching,?but?were?not?actually?used;?may?be?a?subset?of?the?previous?call?to?-tableView:prefetchRowsAtIndexPaths:-?(void)tableView:(UITableView?*)tableView?cancelPrefetchingForRowsAtIndexPaths:(NSArray<NSIndexPath?*>?*)indexPaths;@end
等新的協議來提供UITableView\UICollectionView預加載 data 的能力。
但是對于整個 App 的核心組件?UIViewController卻少見預加載的策略。極少數場景是這樣的:整個界面包含多個?UIViewController的層級,除了顯示第一個?UIViewController外 ,預加載其他的?UIViewController。
二. UIViewController 到底能不能預加載?
在和同事解決嚴選 App 內“領取津貼”彈窗慢的問題時,我思考了這個問題,所以查閱了 Developer Documentation, 大概有以下的收獲;
在同一個?navigation stack里不能 push 相同的一個UIViewController,否則會崩潰;而來自不同?navigation stack的?UIViewController是可以被壓入 stack 的,這也是預加載的關鍵。
當某個?UIViewController執行了?viewDidLoad()之后,整個?UIViewController對象已經在內存內。如果我們要使用 VC 時,可以直接從內存里獲取,將會獲得速度提升
UIViewController作為?UIWindow和?vc.view中間層,負責事件分發、響應鏈,?UIViewController子元素容器,子元素根據?UIViewController的尺寸 layout
UIViewController.view是個懶加載屬性,由?loadView()初始化,在 viewDidLoad 事件開始時,就已經完成
UIViewController在被添加到?navigation stack后是否會被渲染,取決于所在的 window 是不是 hidden = NO,和在不在屏幕上沒有關系
答案:可以被預加載,除了本文嘗試的多個navigation stack的方式外, apple 自己在早期推廣 storyboard 和 xib 文件模式開發 iOS 應用時,也抱有相同的意圖
三. UIViewController 渲染的流程?
因為 UIKit 沒有開源,我從 Apple Documents 和?Chameleonproject 的重寫源碼里試圖還原真實的?UIViewController在 UIKit 中的渲染邏輯。以下是我根據自己的理解畫的 UIViewController 被添加到 UIWindow 的渲染流程,肯定有錯誤和遺漏,僅供理解本文使用。
圖例參考 Safari,序號后面的圖形,表示本階段 ViewController 的 view 層級,認清這些事件,可以知道哪個階段做哪些操作是合適的?
注意:以上為 iOS 12 里的情況,在 iOS 13 里,第 5 序號的 View 比目前 iOS 12 要多兩個 View,UIDropShadowView,UITransitionView。
四. ViewControllerPreRender
在整理出上面的流程結論后,編寫了ViewControllerPreRender,雖然不到 100 行,前后卻花了一周,主要是為了解決下面這個 XCode 警告。
"Unbalanced?calls?to?begin/end?appearance?transitions?for?0xa98e050>"
幸好通過多次嘗試,最終解決掉。
代碼很短,全文摘錄,以下以注釋的方式詳細解讀。
//.h?文件@interface?ViewControllerPreRender?:?NSObject+?(instancetype)defaultRender;-?(void)showRenderedViewController:(Class)viewControllerClass?completion:(void?(^)(UIViewController?*vc))block;@end//.m?文件#import?"ViewControllerPreRender.h"@interface?ViewControllerPreRender?()@property?(nonatomic,?strong)?UIWindow?*windowNO2;/**
?已經被渲染過后的?ViewController,池子,在必要時候?purge?掉
?*/@property?(nonatomic,?strong)?NSMutableDictionary?*renderedViewControllers;@endstatic?ViewControllerPreRender?*_myRender?=?nil;@implementation?ViewControllerPreRender+?(instancetype)defaultRender{????static?dispatch_once_t?onceToken;????dispatch_once(&onceToken,?^{????????_myRender?=?[ViewControllerPreRender?new];????????_myRender.renderedViewControllers?=?[NSMutableDictionary?dictionaryWithCapacity:3];????????//?增加一個監聽,當內存緊張時,丟棄這些預加載的對象不會造成功能錯誤,????????//?這樣也要求?UIViewController?的?dealloc?都能正確處理資源釋放????????[[NSNotificationCenter?defaultCenter]?addObserver:_myRender?????????????????????????????????????????????????selector:@selector(dealMemoryWarnings:)?????????????????????????????????????????????????????name:UIApplicationDidReceiveMemoryWarningNotification???????????????????????????????????????????????????object:nil];????});????return?_myRender;}/**
?內部方法,用來產生可用的?ViewController,如果第一次使用。
?直接返回全新創建的對象,同時也預熱一個相同類的對象,供下次使用。
?支持預熱多個?ViewController,但是不易過多,容易引起內存緊張
?@param?viewControllerClass?UIViewController?子類
?@return?UIViewControllerd?實例
?*/-?(UIViewController?*)getRendered:(Class)viewControllerClass{????if?(_windowNO2?==?nil)?{????????CGRect?full?=?[UIScreen?mainScreen].bounds;????????//?對于?no2?的尺寸多少為合適。我自己做了下實驗????????//?這里設置的尺寸會影響被緩存的?VC?實例的尺寸。但在預熱好的?VC?被添加到當前工作的?navigation?stack?時,它的?View?的尺寸是正確的和?no2?的尺寸無關。????????//?同樣的,在被添加到?navigation?stack?時,會觸發?viewLayoutMarginsDidChange?事件。????????//?而且對于內存而言,尺寸越小內存占用越少,理論上?(1,1,1,1)?的?no2?有能達到預熱?VC?的效果。????????//?但是有些?view?不是被?presented?或者?pushed,而是作為子?ViewController?的子?view?來渲染界面的。這需要?view?有正確的尺寸。????????//?所以這里預先設置將來真正展示時的尺寸,減少?resize、和作為子?ViewController?使用時出錯,在本?demo?中,默認大部分的尺寸是全屏。????????UIWindow?*no2?=?[[UIWindow?alloc]?initWithFrame:CGRectOffset(full,?CGRectGetWidth(full),?0)];????????UINavigationController?*nav?=?[[UINavigationController?alloc]?initWithRootViewController:[UIViewController?new]];????????no2.rootViewController?=?nav;????????no2.hidden?=?NO;//?必須是顯示的?window,才會觸發預熱?ViewController,隱藏的?window?不可用。但是和是否在屏幕可見沒關系????????no2.windowLevel?=?UIWindowLevelStatusBar?+?14;????????_windowNO2=?no2;????}????NSString?*key?=?NSStringFromClass(viewControllerClass);????UIViewController?*vc?=?[self.renderedViewControllers?objectForKey:key];????if?(vc?==?nil)?{?//?下次使用緩存????????vc?=?[viewControllerClass?new];????????//?解決?Unbalanced?calls?to?begin/end?appearance?transitions?for??關鍵點????????//?1.?使用?UINavigationController??作為?no2?的?rootViewController????????//?2.?如果使用?UIViewController?作為?no2?的?rootViewController,始終有?Unbalanced?calls?的錯誤????????//?雖然是編譯器警告,實際上?Unbalanced?calls??會影響被緩存的?vc,?當它被添加到當前活動的?UINavigation?stack?時,它的生命周期是錯誤的????????//?所以這個警告必須解決。????????UINavigationController?*nav?=?(UINavigationController?*)_windowNO2.rootViewController;????????[nav?pushViewController:vc?animated:NO];????????[self.renderedViewControllers?setObject:vc?forKey:key];????????//????????return?[viewControllerClass?new];????}??else?{?//?本次使用緩存,同時儲備下次????????//?必須是先設置?no2?的新?rootViewController,之后再復用從緩存中拿到的?viewControllerClass。否則會奔潰????????UINavigationController?*nav?=?(UINavigationController?*)_windowNO2.rootViewController;????????[nav?popViewControllerAnimated:NO];????????UIViewController?*fresh?=?[viewControllerClass?new];????????[nav?pushViewController:fresh?animated:NO];????????//?在?setObject?to?renderedViewControllers?字典時,保證被渲染過????????[self.renderedViewControllers?setObject:fresh?forKey:key];????????return?vc;????}}/**
?主方法。傳入一個?UIViewController?的?class?對象,在調用的?block?中同步的返回一個預先被渲染的?ViewController
?@param?viewControllerClass??必須是?UIViewController?的?Class?對象
?@param?block?業務邏輯回調
?*/-?(void)showRenderedViewController:(Class)viewControllerClass?completion:(void?(^)(UIViewController?*vc))block{????//?CATransaction?為了避免一個?push?動畫和另外一個?push?動畫同時進行的問題。????[CATransaction?begin];????UIViewController?*vc1?=?[self?getRendered:viewControllerClass];????//?這里包含一個陷阱——?必須先渲染將要被?cached?的?ViewController,然后再執行真實的?block????//?理想情況,應該是先執行?block,然后執行?cache?ViewController,因為?block?更重要些。暫時沒想到方法????[CATransaction?setCompletionBlock:^{????????block(vc1);????}];????[CATransaction?commit];}-?(void)dealMemoryWarnings:(id)notif{????NSLog(@"release?memory?pressure");????[self.renderedViewControllers?removeAllObjects];}@end
五. 性能提升如何?
以 native 體驗中通常體驗最差的 webview 為例, 目標是嚴選商城的 h5 ,http://m.you.163.com,分別以傳統的,每次都新創建?ViewController的方式;第二次之后使用預熱的?ViewController加載嚴選首頁兩種方式測試,保持?ViewController內部邏輯相同,詳見 demo 工程里注釋。
測試方案:模擬器,每種方式測試時都重啟,各測試了 20 次左右,統計表格如下,navigationStart 作為網絡加載時間的開始標志,以 document.onload 作為頁面加載完畢的標志;
1) 傳統方式
2) 使用預加載方式
從測試數據可見,使用預加載的方式顯著的提升了?navigationStart的性能,443 ms減少到?56 ms,相應的?document.onload事件也提前,2357到?2067。
相比之下,預加載方式提前 400ms 發送網絡請求(但是完成加載耗時只少 300ms,猜測是 CPU 資源調度問題)。以上數據只作為性能提升參考,對于加載 WebView 的 VC 而言,預初始化 WebView 以及其他元素,可以提高加載 h5 頁面的速度。
六. 原因探析
對?ViewControllerPrerender的邏輯分析解釋為什么會有提速,在使用ViewControllerPreRender時,需要特別留意什么地方,以免掉入誤區。
根據 preRender 的原理,我大概畫了圖例來解釋。
上半部分,所有階段是線性的;下半部分,可以做到并行,尤其是第三個 VC 的顯示,將異步加載數據也放到并行邏輯了,這對有性能瓶頸的界面優化不失為一種方式
總結:預加載利用了并行這一傳統性能優化技術,同時對 ViewController 的生命周期也提出更高的要求,譬如:
被預熱的 ViewController,需要劃分職責,在viewDidLoad里搭建框架,,而在另一個單獨的接口如本 demo 里的setUrl用來使用業務數據渲染頁面。
被預加載的 ViewController 的viewDidLoad不宜占用太多主線程資源,避免對當前界面打開產生負面影響。
七. preRender 適宜的場景
在 App 性能問題中, native 自己的 ViewController性能表現并不是瓶頸,所以目前業界對 UIViewController 的預加載并沒有太多可參考的案例,不過對于某些場景優化還是有指導意義。在本文開始時提到的嚴選商品詳情頁里領取津貼是彈窗,常規情況下彈出是比較慢的,經過討論后,我們決定對津貼彈窗做兩個優化
在彈窗出現時使用縮放動畫,h5 加載也使用 loading
使用預加載彈窗的 ViewController。
從測試數據來看,從點擊到最后加載完畢,大概節省了 300 ms,還需要進一步考慮 h5 的頁面優化。
題外話,App 作為嚴選用戶體驗的重要載體,App 性能是極其重要一環。我們對彈窗的體驗做了少許優化。
在嚴選里彈窗有兩種,一種是被動彈窗,比方說從后臺數據返回中,得知有彈窗需要顯示,native 根據全局彈窗排序,決定顯示那個——當后臺數據返回指定的 url 被加載完畢之后,才彈出遮罩,顯示被加載好的 url;如果 url 加載失敗,就不會彈出彈窗。
而對于用戶主動彈出的彈窗,如用戶在詳情頁點擊 cell,彈出領取津貼,我們分 native 加速(使用預加載)和 h5 加速兩部分。
另外比較適合 preRender 的地方如,
我的訂單界面,當用戶某個訂單有商家已發貨未收貨時,根據行為統計,用戶大概率會打開第一條已發貨的訂單去查看當前物流(物流數據來自第三方,響應速度沒有保證),所以在進入我的訂單時,可以預先加載一個查看最新未完成訂單的物流的 ViewController。
用戶在詳情頁面,點擊了我好評率,那么大概率,用戶還會打開用戶曬單的視頻和圖片。這時候可以預加載一個視頻播放器和圖片瀏覽器,提供用戶的響應速度等。
對于大部分功能也能而言, prefetch 并不是必選項,還需要根據自身的業務來決定使用可以 prefetch 的思想解決 App 體驗的瓶頸問題,不要隨意使用?ViewControllerPrefetch,增加額外復雜度。
八. xib 和 storyboard 帶來的啟示
當我接觸 iOS 開發時,已經到了 iOS 推銷 storyboard 開發方式失敗的時候,大部分可需要持續迭代的 App,其實不適合用 xib 和 storyboard 來開發,它的可視化帶來的好處相比項目協作迭代里遇到的 diff 困難、復用困難、啟動慢等壞處,不值一提。
時至今日,當我思考預加載方式在?viewDidiLoad里還要多少操作空間時,我發現 xib 和 storyboard 在被蘋果推廣時沒有被提到它預加載的優點,一直沒有引起重視。
相同的 ViewController 使用的 xib 和 storyboard 文件被 init 為 實例之后,后續相同的ViewController 都會來 copy 被初始化好的 storyboard 來構建界面。開發人員創建完 xib 和 storyboard,需要持久化為文件,使用 initWithCoder:方法實現序列化,打開 xib 和 storyboard 時,先從文件反序列化解析得到 xml 文件,然后用 xml 文件繪制 interface builder。它的底層機制決定了它在開發啟動、App 啟動時會有性能損耗,不過也為我們做了一個例子—— 如何預加載 View 片段乃至 ViewController 本身。以 storyboard 為例,你可以在 storyboard 里做以下操作;
繪制 ViewController 的 view 層次,特別的,會首先限制 storyboard 里繪制的靜態數據
添加 view 之間的約束
轉場(segue)和按鈕動作跳轉
而最終的用戶界面需要等待網絡返回真實數據后重新渲染,在此期間,顯示靜態的等待界面。所以在需要被緩存的?UIViewController需要可以安全的編寫 UI、事件和轉場等邏輯,將動態部分(網絡請求)的發起邏輯寫在轉場結束之后。
九. 補記
Unbalanced calls to begin/end appearance transitions for?
,這個警告必須解決,否則會導致被緩存的 ViewController 被添加到活動 stack 時,生命周期紊亂導致一些依賴生命周期執行的邏輯失效,如電商行業里很看重的曝光統計數據不正確
Demo 工程里已經有 calc.rb 可以直接將從 console 里拿到的數據實現為報表,方便你測試自己的頁面性能加載提升對比。
參考
[1]預加載與智能預加載(iOS)https://draveness.me/preload
[2]iOS性能優化系列篇之“列表流暢度優化” https://juejin.im/post/5b72aaf46fb9a009764bbb6a
[3]UIWindow 源碼 of Chameleon https://github.com/BigZaphod/Chameleon/blob/master/UIKit/Classes/UIWindow.m
[4]https://developer.apple.com/documentation/uikit/uiviewcontroller?language=objc https://developer.apple.com/documentation/uikit/uiviewcontroller?language=objc
[5]Sharing the Same UIViewController as the rootViewController with Two UINavigationControllers https://stackoverflow.com/questions/9710676/sharing-the-same-uiviewcontroller-as-the-rootviewcontroller-with-two-uinavigatio
[6]Storyboards vs. the old XIB way https://stackoverflow.com/questions/13834999/storyboards-vs-the-old-xib-way
[7]Unbalanced calls to begin/end appearance transitions for?https://stackoverflow.com/questions/14412890/unbalanced-calls-to-begin-end-appearance-transitions-for-uinavigationcontroller
[8]ViewControllerPreRender https://github.com/hite/ViewControllerPreRender
推薦閱讀
iOS匯編快速入門
如何評價 SwiftUI?
從 SwiftUI 談聲明式 UI 與類型系統
在看就點點吧?
總結
以上是生活随笔為你收集整理的5首页加载慢_UIViewController 预加载方案浅谈的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: cesium鼠标控制键盘_用 Pytho
- 下一篇: 柱形图无数据可选中_这种漂亮的“连体”柱