人人中彩票

慎用懒加载!一场由懒加载引起的线上故障...

卖报的小画家Sure 2019-08-29 17:24:52 1144
本文来自,作者 卖报的小画家Sure

前言:最近遇到了一个棘手的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:]复制代码

经过排查,可以排除多线程的问题。我将上述代码再次简化成如下版本,假设在viewControllerviewDidLoad方法中做的为loadingView的显示操作,init方法中做的只是loadingView的隐藏操作。甚至可以简化为只是分别在initviewDidLoad方法调用了loadingViewgetter方法而已。

- (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;
}复制代码

运行结果如下

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会一直显示无法移除,至此找到了问题的根本原因。

文中测试代码可点击链接下载:

总结:问题产生的原因与viewController的生命周期和懒加载的调用未知有关,但通常是这种简单的问题会被我们所疏忽。

慎用懒加载,并不是不建议使用懒加载,而是要注意其使用场景及可能出现的问题。

这个问题也从侧面说明了为什么不要在init方法中调用self来访问属性,其可能会造成的影响是未知的。另外在dealloc方法也不要调用self来访问属性,相关内容在之前也写过一篇文章进行讲述,感兴趣的可以移步进行查看:

暂时写到这里,在日常开发中,往往疏忽了对基础知识的掌握,而导致无法预期的问题。写这篇文章也是为了记录下来引以为戒。共勉!

五分排列3 百度官方彩票 彩票软件大全 2分钟赛车 五分排列3 正规彩票app 多彩网 多彩网 一定牛彩票网 头奖彩票