一. 引子
預(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)列表加載完畢之后愈涩,你走到了沒有網(wǎng)絡(luò)的電梯里,依然可以點(diǎn)擊某個(gè)文字加矛,閱讀文字部分履婉,圖片是空白。
在 iOS 常規(guī)的優(yōu)化方案中斟览,預(yù)加載也是極常見的手段毁腿,多見于:預(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
還為開發(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
卻少見預(yù)加載的策略庭瑰。極少數(shù)場(chǎng)景是這樣的:整個(gè)界面包含多個(gè) UIViewController
的層級(jí)星持,除了顯示第一個(gè) UIViewController
外 ,預(yù)加載其他的 UIViewController
弹灭。
二. UIViewController
到底能不能預(yù)加載督暂?
在和同事解決嚴(yán)選 App 內(nèi)“領(lǐng)取津貼”彈窗慢的問題時(shí),我思考了這個(gè)問題穷吮,所以查閱了 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 事件開始時(shí)饥臂,就已經(jīng)完成 -
UIViewController
在被添加到navigation stack
后是否會(huì)被渲染,取決于所在的 window 是不是 hidden = NO似踱,和在不在屏幕上沒有關(guān)系
答案:可以被預(yù)加載隅熙,除了本文嘗試的多個(gè)navigation stack
的方式外, apple 自己在早期推廣 storyboard 和 xib 文件模式開發(fā) iOS 應(yīng)用時(shí)屯援,也抱有相同的意圖
三. UIViewController
渲染的流程猛们?
因?yàn)?UIKit 沒有開源,我從 Apple Documents 和 Chameleon
project 的重寫源碼里試圖還原真實(shí)的 UIViewController
在 UIKit 中的渲染邏輯狞洋。以下是我根據(jù)自己的理解畫的 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é)論后邦泄,編寫了ViewControllerPreRender
,雖然不到 100 行裂垦,前后卻花了一周顺囊,主要是為了解決下面這個(gè) XCode 警告。
"Unbalanced calls to begin/end appearance transitions for <UIViewController: 0xa98e050>"
幸好通過多次嘗試蕉拢,最終解決掉特碳。
代碼很短诚亚,全文摘錄,以下以注釋的方式詳細(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)被渲染過后的 ViewController站宗,池子,在必要時(shí)候 purge 掉
*/
@property (nonatomic, strong) NSMutableDictionary *renderedViewControllers;
@end
static 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)聽,當(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缺谴,但是不易過多但惶,容易引起內(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 不可用诈皿。但是和是否在屏幕可見沒關(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 <UIViewController: 0xa98e050> 關(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í)瘪松,保證被渲染過
[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)畫和另外一個(gè) push 動(dòng)畫同時(shí)進(jì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í)沒想到方法
[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)部邏輯相同缺脉,詳見 demo 工程里注釋。
測(cè)試方案:模擬器悦穿,每種方式測(cè)試時(shí)都重啟攻礼,各測(cè)試了 20 次左右,統(tǒng)計(jì)表格如下栗柒,navigationStart 作為網(wǎng)絡(luò)加載時(shí)間的開始標(biāo)志礁扮,以 document.onload 作為頁(yè)面加載完畢的標(biāo)志;
> 1. 傳統(tǒng)方式
點(diǎn)擊到網(wǎng)絡(luò)加載時(shí)間(ms) | 點(diǎn)擊到頁(yè)面加載完畢時(shí)間(ms) |
---|---|
409.042969 | 2237.258057 |
382.000244 | 2294.206055 |
421.780762 | 2377.906250 |
435.476318 | 2358.933350 |
443.190186 | 2261.447998 |
379.502930 | 2243.837158 |
386.897949 | 2322.465088 |
508.499023 | 2385.695068 |
490.614014 | 2639.933105 |
407.436035 | 2384.422852 |
478.447998 | 2305.270264 |
426.408691 | 2340.742920 |
598.571777 | 2465.007812 |
453.924072 | 2424.213135 |
441.053955 | 2371.049805 |
399.669922 | 2218.141113 |
779.028809 | 2659.640625 |
68.835938 | 1934.873047 |
515.513916 | 2552.829834 |
439.666016 | 2268.033936 |
440.330811 | 2357.508789 |
Avg of 21: | |
443.14 | 2352.54 |
> 2. 使用預(yù)加載方式
點(diǎn)擊到網(wǎng)絡(luò)加載時(shí)間(ms) | 點(diǎn)擊到頁(yè)面加載完畢時(shí)間(ms) |
---|---|
63.797852 | 2538.381836 |
63.152832 | 2333.105957 |
64.150146 | 2302.843750 |
59.484863 | 2155.601074 |
57.637207 | 2382.412842 |
55.749756 | 2050.655762 |
51.270020 | 1895.146729 |
54.883789 | 1793.544922 |
53.313965 | 1897.723877 |
78.262207 | 1777.684814 |
48.425049 | 1828.953857 |
50.403320 | 2075.978027 |
48.640625 | 2168.324951 |
58.913818 | 1946.458984 |
40.200928 | 1850.614990 |
54.635010 | 2198.915039 |
51.363770 | 1956.969971 |
Avg of 17: | |
56.13 | 2067.84 |
從測(cè)試數(shù)據(jù)可見瞬沦,使用預(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)度問題)芳悲。以上數(shù)據(jù)只作為性能提升參考立肘,對(duì)于加載 WebView 的 VC 而言,預(yù)初始化 WebView 以及其他元素名扛,可以提高加載 h5 頁(yè)面的速度谅年。
六,原因探析
對(duì) ViewControllerPrerender
的邏輯分析解釋為什么會(huì)有提速肮韧,在使用ViewControllerPreRender
時(shí)融蹂,需要特別留意什么地方,以免掉入誤區(qū)弄企。
根據(jù) preRender 的原理超燃,我大概畫了圖例來(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)前界面打開產(chǎn)生負(fù)面影響呻右。
七跪妥,preRender 適宜的場(chǎng)景
在 App 性能問題中, native 自己的 ViewController性能表現(xiàn)并不是瓶頸声滥,所以目前業(yè)界對(duì) UIViewController 的預(yù)加載并沒有太多可參考的案例眉撵,不過對(duì)于某些場(chǎng)景優(yōu)化還是有指導(dǎo)意義。在本文開始時(shí)提到的嚴(yán)選商品詳情頁(yè)里領(lǐng)取津貼是彈窗落塑,常規(guī)情況下彈出是比較慢的纽疟,經(jīng)過討論后,我們決定對(duì)津貼彈窗做兩個(gè)優(yōu)化
- 在彈窗出現(xiàn)時(shí)使用縮放動(dòng)畫憾赁,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)彈窗官紫,比方說從后臺(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ì)打開第一條已發(fā)貨的訂單去查看當(dāng)前物流(物流數(shù)據(jù)來(lái)自第三方贰拿,響應(yīng)速度沒有保證)蛤袒,所以在進(jìn)入我的訂單時(shí),可以預(yù)先加載一個(gè)查看最新未完成訂單的物流的 ViewController膨更。
-
用戶在詳情頁(yè)面妙真,點(diǎn)擊了我好評(píng)率,那么大概率荚守,用戶還會(huì)打開用戶曬單的視頻和圖片隐孽。這時(shí)候可以預(yù)加載一個(gè)視頻播放器和圖片瀏覽器,提供用戶的響應(yīng)速度等健蕊。
對(duì)于大部分功能也能而言菱阵, prefetch 并不是必選項(xiàng),還需要根據(jù)自身的業(yè)務(wù)來(lái)決定使用可以 prefetch 的思想解決 App 體驗(yàn)的瓶頸問題缩功,不要隨意使用 ViewControllerPrefetch
晴及,增加額外復(fù)雜度。
八嫡锌,xib 和 storyboard 帶來(lái)的啟示
當(dāng)我接觸 iOS 開發(fā)時(shí)虑稼,已經(jīng)到了 iOS 推銷 storyboard 開發(fā)方式失敗的時(shí)候琳钉,大部分可需要持續(xù)迭代的 App,其實(shí)不適合用 xib 和 storyboard 來(lái)開發(fā)蛛倦,它的可視化帶來(lái)的好處相比項(xiàng)目協(xié)作迭代里遇到的 diff 困難歌懒、復(fù)用困難、啟動(dòng)慢等壞處溯壶,不值一提及皂。
時(shí)至今日,當(dāng)我思考預(yù)加載方式在 viewDidiLoad
里還要多少操作空間時(shí)且改,我發(fā)現(xiàn) xib 和 storyboard 在被蘋果推廣時(shí)沒有被提到它預(yù)加載的優(yōu)點(diǎn)验烧,一直沒有引起重視。
相同的 ViewController 使用的 xib 和 storyboard 文件被 init 為 實(shí)例之后又跛,后續(xù)相同的ViewController 都會(huì)來(lái) copy 被初始化好的 storyboard 來(lái)構(gòu)建界面碍拆。開發(fā)人員創(chuàng)建完 xib 和 storyboard,需要持久化為文件慨蓝,使用 initWithCoder:方法實(shí)現(xiàn)序列化感混,打開 xib 和 storyboard 時(shí),先從文件反序列化解析得到 xml 文件礼烈,然后用 xml 文件繪制 interface builder浩习。它的底層機(jī)制決定了它在開發(fā)啟動(dòng)、App 啟動(dòng)時(shí)會(huì)有性能損耗济丘,不過也為我們做了一個(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
需要可以安全的編寫 UI鲫寄、事件和轉(zhuǎn)場(chǎng)等邏輯,將動(dòng)態(tài)部分(網(wǎng)絡(luò)請(qǐng)求)的發(fā)起邏輯寫在轉(zhuǎn)場(chǎng)結(jié)束之后疯淫。
十地来,補(bǔ)記
- [Unbalanced calls to begin/end appearance transitions for <UIViewController: 0xa98e050> ,這個(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)
[2] iOS性能優(yōu)化系列篇之“列表流暢度優(yōu)化”
[3] UIWindow 源碼 of Chameleons
[4]https://developer.apple.com/documentation/uikit/uiviewcontroller?language=objc
[5] Sharing the Same UIViewController as the rootViewController with Two UINavigationControllers
[6] Storyboards vs. the old XIB way
[7] Unbalanced calls to begin/end appearance transitions for <UINavigationController: 0xa98e050>
[8] ViewControllerPreRender