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