HolyShit!懒加载执行两次?
前言:最近遇到了一个辣手的Bug,查找Bug的过程是心力憔悴。故抽空书写这篇文章记录下。
我们从App的页面加载说起,通常App首页展示逻辑大概是这样的:展现加载栏loadingView后请求首页数据,在数据回调返回后移除loadingView,回调成功显示正确内容,失败则展现异常占位图。但同时存在的问题是,为了让App首页能更加快速、优先的展现,通常对于客户登录或者其余操作是与主页请求是保持异步请求的,因而当客户态发生变化或者其余状态改变时需重新刷新首页数据。
页面展现逻辑
依照上述流程,但确由此产生了一个辣手的Bug,偶现loadingView在数据成功返回后依然无法移除。
基础的代码如下:
@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与警告:
Main Thread Checker: UI API called on a background thread: -[UIView initWithFrame:]经过排查,可以排除多线程的问题。我将上述代码再次简化成如下版本,假设在viewController的viewDidLoad方法中做的为loadingView的显示操作,init方法中做的只是loadingView的隐藏操作。甚至可以简化为只是分别在init与viewDidLoad方法调用了loadingView的getter方法而已。
- (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;}运行结果如下
initLoadingView LazyLoadviewDidLoadLoadingView LazyLoad通过Debug和Log打印发现LoadingView懒加载被执行两次!!!这真是颠覆了我的认知。
但更让人匪夷所思的是,假如将loadingView的创立形式更改为等同屏幕大小的frame或者单纯以init的形式创立,就不会出现懒加载被执行两次的情况!
_loadingView = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];打印结果:
initLoadingView LazyLoadviewDidLoad下面我们来揭开谜底
真相只有一个!
要处理这个问题,我们先想清楚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) {}的判断已经被执行了两次。因而打印结果是这样的:
initLoadingView LazyLoadviewDidLoadLoadingView LazyLoad调用流程如图所示
loadingView调用流程图
懒加载判断被执行两次,而两次创立互不影响,因而loadingView也被创立了两次。可以尝试在init和viewDidLoad调用[self loadingView]后打印_loadingView的地址,会发现完全是两个不同地址的对象。
回归在最初的案例中,首先将loadingView增加到首页,当首页数据请求中未回调时,客户登录成功客户态发生变化发送通知给主页重新刷新数据移除loadingView,但所移除的并不是首页数据开始加载时增加的loadingView,因而loadingVeiw会一直显示无法移除,至此找到了问题的根本起因。
文中测试代码可点击链接下载:
LSure/LazyLoad
总结:问题产生的起因与viewController的生命周期和懒加载的调用未知有关,但通常是这种简单的问题会被我们所疏忽。
慎用懒加载,并不是不建议使用懒加载,而是要注意其使用场景及可能出现的问题。
这个问题也从侧面说明了为什么不要在init方法中调用self来访问属性,其可能会造成的影响是未知的。另外在dealloc方法也不要调用self来访问属性,相关内容在之前也写过一篇文章进行讲述,感兴趣的可以移步进行查看:内存管理-dealloc方法究竟应该怎样写?
暂时写到这里,在日常开发中,往往疏忽了对基础知识的掌握,而导致无法预期的问题。写这篇文章也是为了记录下来引以为戒。共勉!
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » HolyShit!懒加载执行两次?