1. 程式人生 > >【Bug記錄】懶載入的坑

【Bug記錄】懶載入的坑

一、問題

最近開發過程中,遇到了一個詭異的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...

偽呼叫棧:

  1. [vc setScrollsToTop]
  2. self.collectionView.scrollsToTop = scrollsToTop; 觸發get方法
  3. [self collectionView]
  4. _collectionView=[[UICollectionView alloc] initWithFrame:self.view.bounds
    collectionViewLayout:self.collectionViewLayout]
    self.view觸發viewDidLoad
  5. [self viewDidLoad]
  6. [self.view addSubview:self.collectionView]
    self.collectionView重新觸發get
  7. [self collectionView] 再建立新一個例項
  8. [self.view addSubview:self.collectionView] 返回後addSubview
  9. _collectionView=[[UICollectionView alloc] initWithFrame:self.view.bounds
    collectionViewLayout:self.collectionViewLayout]
    返回初次get裡的init方法,返回新例項將上一個例項覆蓋
  10. self.collectionView.scrollsToTop = scrollsToTop; 此時的collectionView已不是addSubview的那個
  11. 結束

四、方案

使用成員變數_scrollsToTop儲存值,不要在setScrollsToTop使用self.collectionView

- (void)setScrollsToTop:(BOOL)scrollsToTop {
    _scrollsToTop = scrollsToTop;
    _collectionView.scrollsToTop = scrollsToTop;
}

在get方法中建立collectionView時,再將_scrollsToTop重新設定回去_collectionView.scrollsToTop = _scrollsToTop

總結:勿濫用懶載入

像例子裡面這個,業務場景肯定是要有collectionView的,這種必要view的初始化根本就沒必要懶載入,因為這個是必要的元素,用_變數隱藏好就行。

懶載入應該是用在一些展示時非必要(即不是每次開這個頁面都會有)、耗效能的UI或容器類上面。尤其是這種對外暴露的方法,呼叫內部變數時更應該慎用懶載入。