前言:最近遇到了一個棘手的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:]
複製代碼
通過排查,能夠排除多線程的問題。我將上述代碼再次簡化成以下版本,假設在viewController的viewDidLoad方法中作的爲loadingView的顯示操做,init方法中作的只是loadingView的隱藏操做。甚至能夠簡化爲只是分別在init與viewDidLoad方法調用了loadingView的getter方法而已。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的初始化方法,將loadingView以init形式初始化,而後在loadingView初始化前調用下viewController view的getter方法。
- (UIView *)loadingView {
if (!_loadingView) {
NSLog(@"LoadingView LazyLoad");
[self view];
_loadingView = [[UIView alloc] init];
}
return _loadingView;
}
複製代碼
咱們能夠經過斷點或者打印來進行觀察,首先執行了viewController的init方法,在init方法中調用了loadingView的getter方法,首次調用,在這裏識別到**_loadingView不存在,所以進入判斷,在判斷中由於調用了[self view],所以接下來會調用viewDidLoad方法,在viewDidLoad方法中咱們一樣調用了loadingView的getter方法。這時又執行到loadingView的getter方法,由於主線程中是順序執行的,首次調用的loadingView還沒被初始化,因此仍然識別到_loadingView不存在,這時咱們會發現if (_loadingView) {}**的判斷已經被執行了兩次。所以打印結果是這樣的:
init
LoadingView LazyLoad
viewDidLoad
LoadingView LazyLoad
複製代碼
調用流程如圖所示
懶加載判斷被執行兩次,而兩次建立互不影響,所以loadingView也被建立了兩次。能夠嘗試在init和viewDidLoad調用**[self loadingView]後打印_loadingView**的地址,會發現徹底是兩個不一樣地址的對象。
迴歸在最初的案例中,首先將loadingView添加到首頁,當首頁數據請求中未回調時,用戶登陸成功用戶態發生變化發送通知給主頁從新刷新數據移除loadingView,但所移除的並非首頁數據開始加載時添加的loadingView,所以loadingVeiw會一直顯示沒法移除,至此找到了問題的根本緣由。
文中測試代碼可點擊連接下載: github.com/LSure/LazyL…
總結:問題產生的緣由與viewController的生命週期和懶加載的調用未知有關,但一般是這種簡單的問題會被咱們所疏忽。
慎用懶加載,並非不建議使用懶加載,而是要注意其使用場景及可能出現的問題。
這個問題也從側面說明了爲何不要在init方法中調用self來訪問屬性,其可能會形成的影響是未知的。另外在dealloc方法也不要調用self來訪問屬性,相關內容在以前也寫過一篇文章進行講述,感興趣的能夠移步進行查看:內存管理-dealloc方法到底應該怎麼寫?
暫時寫到這裏,在平常開發中,每每疏忽了對基礎知識的掌握,而致使沒法預期的問題。寫這篇文章也是爲了記錄下來引覺得戒。共勉!