UIViewController 預(yù)加載方案淺談

一. 引子

預(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逻翁, 大概有以下的收獲;

  1. 在同一個(gè) navigation stack里不能 push 相同的一個(gè)UIViewController 捡鱼,否則會(huì)崩潰八回;而來(lái)自不同 navigation stackUIViewController 是可以被壓入 stack 的,這也是預(yù)加載的關(guān)鍵。
  2. 當(dāng)某個(gè) UIViewController 執(zhí)行了 viewDidLoad()之后缠诅,整個(gè) UIViewController 對(duì)象已經(jīng)在內(nèi)存內(nèi)溶浴。如果我們要使用 VC 時(shí),可以直接從內(nèi)存里獲取管引,將會(huì)獲得速度提升
  3. UIViewController 作為 UIWindowvc.view中間層士败,負(fù)責(zé)事件分發(fā)、響應(yīng)鏈褥伴, UIViewController 子元素容器谅将,子元素根據(jù) UIViewController 的尺寸 layout
  4. UIViewController.view 是個(gè)懶加載屬性,由 loadView() 初始化重慢,在 viewDidLoad 事件開始時(shí)饥臂,就已經(jīng)完成
  5. 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è)階段做哪些操作是合適的?

vc render flow.png

注意:以上為 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事件也提前逛钻,23572067僚焦。
相比之下,預(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)解釋。

old vs new vc route.png

上半部分拘领,所有階段是線性的淋纲;下半部分,可以做到并行院究,尤其是第三個(gè) VC 的顯示,將異步加載數(shù)據(jù)也放到并行邏輯了本涕,這對(duì)有性能瓶頸的界面優(yōu)化不失為一種方式
總結(jié):預(yù)加載利用了并行這一傳統(tǒng)性能優(yōu)化技術(shù)业汰,同時(shí)對(duì) ViewController 的生命周期也提出更高的要求,譬如:

  1. 被預(yù)熱的 ViewController菩颖,需要?jiǎng)澐致氊?zé)样漆,在viewDidLoad里搭建框架,晦闰,而在另一個(gè)單獨(dú)的接口如本 demo 里的setUrl用來(lái)使用業(yè)務(wù)數(shù)據(jù)渲染頁(yè)面放祟。
  2. 被預(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)化

  1. 在彈窗出現(xiàn)時(shí)使用縮放動(dòng)畫憾赁,h5 加載也使用 loading
  2. 使用預(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 的地方如,

  1. 我的訂單界面缨该,當(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膨更。
  2. 用戶在詳情頁(yè)面妙真,點(diǎn)擊了我好評(píng)率,那么大概率荚守,用戶還會(huì)打開用戶曬單的視頻和圖片隐孽。這時(shí)候可以預(yù)加載一個(gè)視頻播放器和圖片瀏覽器,提供用戶的響應(yīng)速度等健蕊。


    好評(píng)跳轉(zhuǎn)到帶圖評(pí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 里做以下操作摹迷;

  1. 繪制 ViewController 的 view 層次,特別的郊供,會(huì)首先限制 storyboard 里繪制的靜態(tài)數(shù)據(jù)
  2. 添加 view 之間的約束
  3. 轉(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ǔ)記

  1. [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ù)不正確
  2. 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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末府阀,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子芽突,更是在濱河造成了極大的恐慌试浙,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,602評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件寞蚌,死亡現(xiàn)場(chǎng)離奇詭異田巴,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)睬澡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門固额,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人煞聪,你說我怎么就攤上這事斗躏。” “怎么了昔脯?”我有些...
    開封第一講書人閱讀 152,878評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵啄糙,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我云稚,道長(zhǎng)隧饼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評(píng)論 1 279
  • 正文 為了忘掉前任静陈,我火速辦了婚禮燕雁,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘鲸拥。我一直安慰自己拐格,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評(píng)論 5 373
  • 文/花漫 我一把揭開白布刑赶。 她就那樣靜靜地躺著捏浊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪撞叨。 梳的紋絲不亂的頭發(fā)上金踪,一...
    開封第一講書人閱讀 49,071評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音牵敷,去河邊找鬼胡岔。 笑死,一個(gè)胖子當(dāng)著我的面吹牛枷餐,可吹牛的內(nèi)容都是我干的姐军。 我是一名探鬼主播,決...
    沈念sama閱讀 38,382評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼奕锌!你這毒婦竟也來(lái)了著觉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,006評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤惊暴,失蹤者是張志新(化名)和其女友劉穎饼丘,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體辽话,經(jīng)...
    沈念sama閱讀 43,512評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡肄鸽,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了油啤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片典徘。...
    茶點(diǎn)故事閱讀 38,094評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖益咬,靈堂內(nèi)的尸體忽然破棺而出逮诲,到底是詐尸還是另有隱情,我是刑警寧澤幽告,帶...
    沈念sama閱讀 33,732評(píng)論 4 323
  • 正文 年R本政府宣布梅鹦,位于F島的核電站,受9級(jí)特大地震影響冗锁,放射性物質(zhì)發(fā)生泄漏齐唆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評(píng)論 3 307
  • 文/蒙蒙 一冻河、第九天 我趴在偏房一處隱蔽的房頂上張望箍邮。 院中可真熱鬧,春花似錦叨叙、人聲如沸锭弊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至全封,卻和暖如春马昙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背刹悴。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工行楞, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人土匀。 一個(gè)月前我還...
    沈念sama閱讀 45,536評(píng)論 2 354
  • 正文 我出身青樓子房,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子证杭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,089評(píng)論 1 32
  • 今天什么都不想做田度。為了打卡,就敷衍一篇文章解愤。所以接下來(lái)的內(nèi)容會(huì)很無(wú)聊镇饺。 1. 我最討厭放長(zhǎng)假,意味著我要安排這個(gè)假...
    宇楓Sai閱讀 279評(píng)論 7 0
  • 今天有兩股感情不停在心間交織送讲。 回家的期待與考試的惶恐奸笤。 回家的熱情與回家的冷漠。 偌大空蕩的候車室哼鬓。 坐不熱的冰...
    何何何妨微瑕閱讀 259評(píng)論 1 0
  • 自嘲獨(dú)諷圣人心监右,拙句劣詞寫不平。一枝竹筆誅天地异希,三寸肉舌笑古今健盒。
    寒菊閱讀 297評(píng)論 2 1
  • 看過?太多的IP存在電視版與電影版有出入味榛,為了保持《使徒行者》電視版在我心目中的美好,一直沒有去看電影版予跌,雖然里面...
    東東_ning閱讀 455評(píng)論 0 2