前言:最近遇到了一个棘手的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方法到底应该怎么写?
暂时写到这里,在平常开发中,每每疏忽了对基础知识的掌握,而致使没法预期的问题。写这篇文章也是为了记录下来引觉得戒。共勉!