控制器self.view坑

在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];
        });
    }
    
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值