一、问题
最近开发过程中,遇到了一个诡异的bug:
vc下有个collectionView属性,并通过懒加载方式获取:
- (UICollectionView *)collectionView {
if (!_collectionView) {
_collectionView =
[[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:self.collectionViewLayout];
_collectionView.dataSource = self;
_collectionView.delegate = self;
}
return _collectionView;
}
在页面viewDidLoad中通过[self.view addSubview:self.collectionView];
加入,并设置好dataSource和delegate。在请求数据后reloadData
,页面没有任何更新
二、原因
在初步的debug中,发现在执行reloadData时,collectionView.superview 为 nil
而我们明明在viewDidLoad 中已经addSubview了
定位原因:
问题出在VC中有个对外暴露的方法:
- (void)setScrollsToTop:(BOOL)scrollsToTop {
self.collectionView.scrollsToTop = scrollsToTop;
}
在外部init了VC后,便调用了此方法。
当调用此方法时,viewDidLoad是还没开始调用的。
在self.collectionView调用其get方法时,get方法内部又使用了self.view
[[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:self.collectionViewLayout];
由于self.view的调用,提前触发了viewDidLoad方法中的
[self.view addSubview:self.collectionView];
而self.collectionView又会执行collectionView的get方法,而此时,_collectionView还为nil(第一次执行的[collectionView init]
还未返回),因此get会再新建一个新实例,最终返回add到self.view中
接着,堆栈返回上一个调用栈(即是第一次调用get方法中的[collectionView init]
),返回collectionView实例,但此时_collectionView已经有了(被addSubview了),结果就是把_collectionView重新覆盖掉了
三 、 代码例子
@interface ViewController : UIViewController
@property (strong, nonatomic) UICollectionView *collectionView;
- (void)setScrollsToTop:(BOOL)scrollsToTop;
@end
@implementation
- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubview:self.collectionView];
}
- (void)setScrollsToTop:(BOOL)scrollsToTop {
self.collectionView.scrollsToTop = scrollsToTop;
}
- (UICollectionView *)collectionView {
if (!_collectionView) {
_collectionView =
[[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:self.collectionViewLayout];
_collectionView.dataSource = self;
_collectionView.delegate = self;
}
return _collectionView;
}
@end
外部调用如下:
ViewController *vc = [[ViewController alloc] init];
[vc setScrollsToTop:YES];
//...doSomething...
伪调用栈:
- [vc setScrollsToTop]
- self.collectionView.scrollsToTop = scrollsToTop; 触发get方法
- [self collectionView]
- _collectionView=[[UICollectionView alloc] initWithFrame:self.view.bounds
collectionViewLayout:self.collectionViewLayout]
self.view触发viewDidLoad - [self viewDidLoad]
- [self.view addSubview:self.collectionView]
self.collectionView重新触发get - [self collectionView] 再创建新一个实例
- [self.view addSubview:self.collectionView] 返回后addSubview
- _collectionView=[[UICollectionView alloc] initWithFrame:self.view.bounds
collectionViewLayout:self.collectionViewLayout]
返回初次get里的init方法,返回新实例将上一个实例覆盖 - self.collectionView.scrollsToTop = scrollsToTop; 此时的collectionView已不是addSubview的那个
- 结束
四、方案
使用成员变量_scrollsToTop保存值,不要在setScrollsToTop使用self.collectionView
- (void)setScrollsToTop:(BOOL)scrollsToTop {
_scrollsToTop = scrollsToTop;
_collectionView.scrollsToTop = scrollsToTop;
}
在get方法中创建collectionView时,再将_scrollsToTop重新设置回去_collectionView.scrollsToTop = _scrollsToTop
总结:勿滥用懒加载
像例子里面这个,业务场景肯定是要有collectionView的,这种必要view的初始化根本就没必要懒加载,因为这个是必要的元素,用_变量隐藏好就行。
懒加载应该是用在一些展示时非必要(即不是每次开这个页面都会有)、耗性能的UI或容器类上面。尤其是这种对外暴露的方法,调用内部变量时更应该慎用懒加载。