HolyShit!懶加載執行兩次?

前言:最近遇到了一個棘手的Bug,查找Bug的過程是心力憔悴。故抽空書寫這篇文章記錄下。git

咱們從App的頁面加載提及,一般App首頁展示邏輯大概是這樣的:展現加載欄loadingView後請求首頁數據,在數據回調返回後移除loadingView,回調成功顯示正確內容,失敗則展現異常佔位圖。但同時存在的問題是,爲了讓App首頁能更加快速、優先的展現,一般對於用戶登陸或其餘操做是與主頁請求是保持異步請求的,所以當用戶態發生變化或其餘狀態改變時需從新刷新首頁數據。 github

頁面展現邏輯

依照上述流程,但確由此產生了一個棘手的Bug,偶現loadingView在數據成功返回後仍然沒法移除。bash

基礎的代碼以下:網絡

@implementation MainViewController

- (instancetype)init {
    if (self = [super init]) {
        // 登陸成功通知,刷新首頁數據
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(loginSucess:)
                                                     name:LSureLoginSucessNoti
                                                   object:nil];
    }
    return self;
}

- (void)loginSucess:(NSNotification *)noti {
    // 清除數據
    
    // 從新請求
    [self loadMainRequestData];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // 展現LoadingView
    [self showLoadingView];
    // 請求主頁數據
    [self loadMainRequestData];
}

- (void)loadMainRequestData {
    // 模擬網絡請求
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        dispatch_async(dispatch_get_main_queue(), ^{
           // 網絡回調移除LoadingView
            [self hideLoadingView];
        });
    });
}

- (void)showLoadingView {
    [self.loadingView setHidden:NO];
}

- (void)hideLoadingView {
    [self.loadingView setHidden:YES];
}

- (UIView *)loadingView {
    if (!_loadingView) {
        NSLog(@"LoadingView LazyLoad");
        _loadingView = [[UIView alloc] initWithFrame:self.view.bounds];
    }
    return _loadingView;
}
複製代碼

外部調用MainViewController初始化,並模擬在MainViewController初始化或跳轉後觸發登陸成功的通知多線程

// 外部跳轉
    [self.navigationController pushViewController:self.mainVC animated:YES];
    // 模擬登陸請求回調
    [[NSNotificationCenter defaultCenter] postNotificationName:LSureLoginSucessNoti
                                                        object:nil];
}

- (MainViewController *)mainVC {
    if (!_mainVC) {
        _mainVC = [[MainViewController alloc] init];
    }
    return _mainVC;
}

複製代碼

上述代碼爲簡化模擬版,感興趣的童鞋能夠先停下來檢查上述代碼。異步

開始的懷疑點在線程方面,確實在多線程場景操做UI會多建立出UI對象,但一般在子線程建立或修改UI控件,XCode會有相應的Log與警告:async

Main Thread Checker: UI API called on a background thread: -[UIView initWithFrame:]
複製代碼

通過排查,能夠排除多線程的問題。我將上述代碼再次簡化成以下版本,假設在viewControllerviewDidLoad方法中作的爲loadingView的顯示操做,init方法中作的只是loadingView的隱藏操做。甚至能夠簡化爲只是分別在initviewDidLoad方法調用了loadingViewgetter方法而已。ide

- (instancetype)init {
    if (self = [super init]) {
        NSLog(@"init");
        [self loadingView];
    }
    return self;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"viewDidLoad");
    [self loadingView];
}

- (UIView *)loadingView {
    if (!_loadingView) {
        NSLog(@"LoadingView LazyLoad");
        _loadingView = [[UIView alloc] initWithFrame:self.view.bounds];
    }
    return _loadingView;
}

複製代碼

運行結果以下post

init
LoadingView LazyLoad
viewDidLoad
LoadingView LazyLoad
複製代碼

經過Debug和Log打印發現LoadingView懶加載被執行兩次!!!這真是顛覆了個人認知。測試

但更讓人匪夷所思的是,若是將loadingView的建立形式更改成等同屏幕大小的frame或單純以init的形式建立,就不會出現懶加載被執行兩次的狀況!

_loadingView = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];
複製代碼

打印結果:

init
LoadingView LazyLoad
viewDidLoad
複製代碼

下面咱們來揭開謎底

真相只有一個!

要解決這個問題,咱們先想清楚viewController的生命週期方法的調用順序

init->loadView->viewDidLoad->viewWillAppear->viewDidAppear->...
複製代碼

初始化後加載view,接着視圖加載完成,即將顯示到最終顯示完成。

那何時viewDidLoad會被觸發呢? 答案是當調用當前viewController的view getter方法時(即調用self.view||[self view])!

咱們能夠這麼理解,對於viewController而言,視圖均是放置在self.view上的,所以當調用了self.view可認爲父子視圖加載完成,所以回調了viewDidLoad生命週期方法。經過Debug也可驗證這一點。(猜想viewController的views屬性也是以懶加載的形式存在的)

咱們再改寫下上述代碼loadingView的初始化方法,將loadingViewinit形式初始化,而後在loadingView初始化前調用下viewController viewgetter方法。

- (UIView *)loadingView {
    if (!_loadingView) {
        NSLog(@"LoadingView LazyLoad");
        [self view];
        _loadingView = [[UIView alloc] init];
    }
    return _loadingView;
}
複製代碼

咱們能夠經過斷點或者打印來進行觀察,首先執行了viewControllerinit方法,在init方法中調用了loadingViewgetter方法,首次調用,在這裏識別到**_loadingView不存在,所以進入判斷,在判斷中由於調用了[self view],所以接下來會調用viewDidLoad方法,在viewDidLoad方法中咱們一樣調用了loadingViewgetter方法。這時又執行到loadingViewgetter方法,由於主線程中是順序執行的,首次調用的loadingView還沒被初始化,因此仍然識別到_loadingView不存在,這時咱們會發現if (_loadingView) {}**的判斷已經被執行了兩次。所以打印結果是這樣的:

init
LoadingView LazyLoad
viewDidLoad
LoadingView LazyLoad
複製代碼

調用流程如圖所示

loadingView調用流程圖

懶加載判斷被執行兩次,而兩次建立互不影響,所以loadingView也被建立了兩次。能夠嘗試在initviewDidLoad調用**[self loadingView]後打印_loadingView**的地址,會發現徹底是兩個不一樣地址的對象。

迴歸在最初的案例中,首先將loadingView添加到首頁,當首頁數據請求中未回調時,用戶登陸成功用戶態發生變化發送通知給主頁從新刷新數據移除loadingView,但所移除的並非首頁數據開始加載時添加的loadingView,所以loadingVeiw會一直顯示沒法移除,至此找到了問題的根本緣由。

文中測試代碼可點擊連接下載: github.com/LSure/LazyL…

總結:問題產生的緣由與viewController的生命週期和懶加載的調用未知有關,但一般是這種簡單的問題會被咱們所疏忽。

慎用懶加載,並非不建議使用懶加載,而是要注意其使用場景及可能出現的問題。

這個問題也從側面說明了爲何不要在init方法中調用self來訪問屬性,其可能會形成的影響是未知的。另外在dealloc方法也不要調用self來訪問屬性,相關內容在以前也寫過一篇文章進行講述,感興趣的能夠移步進行查看:內存管理-dealloc方法到底應該怎麼寫?

暫時寫到這裏,在平常開發中,每每疏忽了對基礎知識的掌握,而致使沒法預期的問題。寫這篇文章也是爲了記錄下來引覺得戒。共勉!

簡書地址:www.jianshu.com/u/57d9688d4…

相關文章
相關標籤/搜索