在iOS开发中,如果视图想要显示,除非需要显示的视图有充分的理由显示在window(例如将要显示的视图层级很搞,或者不太方便添加到控制器视图上时)之外, 一般情况下会选择将视图通过添加自视图的方式直接或者间接添加到控制器的根视图上.这里就有一个经常会掉进去的坑点.
场景
一般情况下,我们会选择在视图根视图加载完成之后才添加子视图,也就是在viewDidLoad中添加子视图:
- (void)viewDidLoad { NSLog(@"%s end", __func__); [super viewDidLoad]; [self.webview loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.baidu.com"]]]; // Do any additional setup after loading the view. } - (WKWebView *)webview { if (!_webview) { _webview = [[WKWebView alloc] initWithFrame:self.view.bounds]; _webview.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; } return _webview; }
这是经常使用到的一个操作,viewDidLoad方法由系统在加载到根视图控制器之后自动触发,对于调用的时机尽量不要做太多干预.现在我们假设有这样一种场景:一个应用的keyWindow的根视图控制器是TabbarController(继承自UITabbarController)对象tabbarController,该对象包含了两个子控制器,默认显示第一个控制器firstController.然后在第二个控制器中,添加上边的代码,同时,实现在控制器tabbarController中实现UITabbarControllerDelegate选中第二个控制器时刷新当前界面:
@implementation TabbarController - (void)viewDidLoad { [super viewDidLoad]; self.delegate = self; // Do any additional setup after loading the view. } - (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UINavigationController *)viewController { NSInteger index = [self.viewControllers indexOfObject:viewController]; if (index == 1) { SecondController *vc = (SecondController *)viewController.viewControllers.firstObject; [vc refresh]; } } @end
@implementation SecondController - (void)viewDidLoad { NSLog(@"%s end", __func__); [super viewDidLoad]; [self.view addSubview:self.webview]; _webview.restorationIdentifier = [NSString stringWithFormat:@"%s", __func__]; [self.webview loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.baidu.com"]]]; NSLog(@"%s end", __func__); // Do any additional setup after loading the view. } - (WKWebView *)webview { if (!_webview) { _webview = [[WKWebView alloc] initWithFrame:self.view.bounds]; _webview.restorationIdentifier = @"lazy load"; NSLog(@"lazy load"); _webview.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; } return _webview; } - (void)refresh { NSLog(@"%s start", __func__); if (self.webview.isLoading) { [self.webview stopLoading]; } [self.webview reload]; NSLog(@"_webview.restorationIdentifier == %@", _webview.restorationIdentifier); NSLog(@"%s end", __func__); } @end
准备结束,在应用启动之后,我们手动点击第二个tab,猜猜看:
1. NSLog(@"_webview.restorationIdentifier == %@", _webview.restorationIdentifier);输出结果是什么? 2. 生成的webview懒加载方法执行了几次? 2. 以上几处函数的输出顺序是什么样的?
输出结果为:
-[SecondController refresh] start -[SecondController viewDidLoad] end lazy load -[SecondController viewDidLoad] end lazy load _webview.restorationIdentifier == lazy load -[SecondController refresh] end
原因:
在控制器中在调用self.view时,如果当前控制器的根视图还没有生成,系统会调用loadView方法生成控制器的根视图.流程大概这个样子:
- 首先查看控制器初始化时,是否有传入对应的xib文件,有则会加载,没有进入下一步;
- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil NS_DESIGNATED_INITIALIZER;
- 查看资源中是否有与控制器同名的xib文件,如果有则加载,如果没有则进入下一步:
- 经过以上两个步骤还是没有生成对应的根视图控制器的话,就会在默认生成一个UIView的实例,赋值给self.view.
所以如果想自定义控制器的根视图就可以按照以上流程的时机添加自定义的视图.当loadView调用结束获取到控制器的根视图之后会调用viewDidLoad方法,在这个时机里用户可以添加自定义的代码,包含添加子视图等.
那么问题来了:在上述操作中,refresh中使用了属性webview的getter方法,在这个懒加载的getter中又调用了self.view了,而这时self.view并未生成,所以此时系统只能先生成控制器的根视图,现保存线程去生成根视图,然后再返回继续进行操作.所以这个时候refresh中调用的懒加载方法并未能直接返回结果,而是被暂停了执行, 转向了执行loadView-->viewDidLoad,这一流程.当viewDidLoad执行结束之后,之前被中断的懒加载实现才得以继续执行,执行结束返回refresh中继续执行.
所以在上述执行中,懒加载执行了两次:
- 第一次是由refresh调用了self.webview触发,然后在判断!_webview为真之后,调用了self.view触发了创建加载根视图的流程,执行被中断;
- 第二次是由于创建完根视图之后在viewDidLoad中调用self.webview触发,而此时由于上一次的懒加载被中断执行并没有返回,所以此时的判断!_webview依然为真,于是执行了第二次懒加载.
在第二次懒加载执行结束之后返回了viewDidLoad中继续执行,当该方法执行结束之后,回到第一次中断的创建webview的懒加载方法中继续执行,执行结束之后返回refresh中执行.由于懒加载共执行了两次,所以创建了两个不同webview对象,第一次创建的webview对象(在viewDidLoad中进行了restorationIdentifier):
_webview.restorationIdentifier = [NSString stringWithFormat:@"%s", __func__];
会被第二次的覆盖掉(第二次初始化赋值):
_webview.restorationIdentifier = @"lazy load";
所以refresh中的输出为:
_webview.restorationIdentifier == lazy load
如果我们在viewDidLoad里保存了非重要的属性的话,就会发现当你想要使用它的时候,"它"却不再是它了.
而函数输出的执行顺序为:
2019-09-30 12:04:43.925338+0800 WKWebviewDemo[20505:4071582] -[SecondController refresh] start 2019-09-30 12:04:43.927656+0800 WKWebviewDemo[20505:4071582] -[SecondController viewDidLoad] end 2019-09-30 12:04:43.965971+0800 WKWebviewDemo[20505:4071582] -[SecondController viewDidLoad] end 2019-09-30 12:04:43.981851+0800 WKWebviewDemo[20505:4071582] _webview.restorationIdentifier == lazy load 2019-09-30 12:04:43.981941+0800 WKWebviewDemo[20505:4071582] -[SecondController refresh] end
解决方案
- 在有可能被中断执行的操作里尽可能避免使用self.view属性.例如上述操作可以修改为:
- (WKWebView *)webview { if (!_webview) { _webview = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds]; _webview.restorationIdentifier = @"lazy load"; NSLog(@"lazy load"); _webview.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; } return _webview; }
- 不要在判断在内部属性是空的判断内部调用self.view,将调用放置在判断外部:
- (WKWebView *)webview { CGRect frame = self.view.bounds; if (!_webview) { _webview = [[WKWebView alloc] initWithFrame:frame]; _webview.restorationIdentifier = @"lazy load"; NSLog(@"lazy load"); _webview.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; } return _webview; }
- 如果有操作可能在视图加载完成之后调用self.view,可以尝试将该操作做延迟.
- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UINavigationController *)viewController { NSInteger index = [self.viewControllers indexOfObject:viewController]; if (index == 1) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ SecondController *vc = (SecondController *)viewController.viewControllers.firstObject; [vc refresh]; }); } }