10

我剛剛完成調試一個非常討厭的UIViewController泄漏,這樣即使在調用dismissViewControllerAnimated之後UIViewController也不會被釋放。爲什麼在performBatchUpdates中對父UIViewController的強引用泄漏了一個活動?

我的問題追查到下面的代碼塊:

self.dataSource.doNotAllowUpdates = YES; 

    [self.collectionView performBatchUpdates:^{ 
     [self.collectionView reloadItemsAtIndexPaths:@[indexPath]]; 
    } completion:^(BOOL finished) { 
     self.dataSource.doNotAllowUpdates = NO; 
    }]; 

基本上,如果我做一個呼叫performBatchUpdates後立即調用dismissViewControllerAnimated的UIViewController中被泄露和UIViewControllerdealloc方法不會被調用。 UIViewController永遠掛起。

有人可以解釋這種行爲嗎?我假設performBatchUpdates運行一段時間間隔,比如500毫秒,所以我會假設在所述間隔之後,它會調用這些方法,然後觸發dealloc。

的修復似乎是這樣的:

self.dataSource.doNotAllowUpdates = YES; 

    __weak __typeof(self)weakSelf = self; 

    [self.collectionView performBatchUpdates:^{ 
     __strong __typeof(weakSelf)strongSelf = weakSelf; 

     if (strongSelf) { 
      [strongSelf.collectionView reloadItemsAtIndexPaths:@[indexPath]]; 
     } 
    } completion:^(BOOL finished) { 
     __strong __typeof(weakSelf)strongSelf = weakSelf; 

     if (strongSelf) { 
      strongSelf.dataSource.doNotAllowUpdates = NO; 
     } 
    }]; 

注意,BOOL成員變量,doNotAllowUpdates,是我添加防止任何類型的數據源/的CollectionView更新到performBatchUpdates一個呼叫正在運行時的變量。

我在網上搜索了一下關於我們是否應該使用performBatchUpdates中的weakSelf/strongSelf模式的討論,但沒有在這個問題上找到任何具體的內容。

我很高興能夠深入到這個bug的底部,但我更喜歡更聰明的iOS開發人員向我解釋我看到的這種行爲。

+0

看看您是否可以確定是否導致保留週期的更新塊或完成塊,這會很有趣。正如你所說的,它們都不應該永久保留它們的塊 - 我只能假設當集合視圖從其超級視圖中被移除時,它將停止運行任何批量更新,並且不會調用或釋放完成塊。在我看來,值得雷達。 – jrturton

+0

@jrturton啊這似乎是最有可能的解釋!基於這種見解,我會看看是否可以在調試器中重新制作這個代碼。 – esilver

+0

@ jrturton alas無法在調試器中重現......看起來好像兩個塊至少總是被調用。也許內部不是無效的,雖然如果解僱被稱爲中間? – esilver

回答

-1

正如你所想的那樣,當不使用weak時,創建一個保留週期。

保留週期是由self引起的,對collectionView有很強的參考,現在collectionViewself有很強的參考。

必須始終假定在執行異步塊之前self可能已被釋放。爲了安全地處理此兩件事情必須做到:

  1. 始終使用弱引用self(或伊娃本身)
  2. 務必確認weakSelf把它當作一個nunnull PARAM

之前存在更新:

把一點點的日誌記錄在performBatchUpdates確認了很多:

- (void)logPerformBatchUpdates { 
    [self.collectionView performBatchUpdates:^{ 
     NSLog(@"starting reload"); 
     [self.collectionView reloadItemsAtIndexPaths:[self.collectionView indexPathsForVisibleItems]]; 
     NSLog(@"finishing reload"); 
    } completion:^(BOOL finished) { 
     NSLog(@"completed"); 
    }]; 

    NSLog(@"exiting"); 
} 

打印:

starting reload 
finishing reload 
exiting 
completed 

這表明完成塊後離開目前的範圍,這意味着它被分派異步回主線程中觸發。

您提到您在完成批量更新後立即關閉視圖控制器。我認爲這是你的問題的根源:

經過一些測試,我能夠重新創建內存泄漏的唯一方法是在解僱之前調度工作。這是一個長鏡頭,但確實是這樣的你的代碼是偶然?:

- (void)breakIt { 
    // dispatch causes the view controller to get dismissed before the enclosed block is executed 
    dispatch_async(dispatch_get_main_queue(), ^{ 
     [self.collectionView performBatchUpdates:^{ 
      [self.collectionView reloadItemsAtIndexPaths:[self.collectionView indexPathsForVisibleItems]]; 
     } completion:^(BOOL finished) { 
      NSLog(@"completed: %@", self); 
     }]; 
    }); 
    [self.presentationController.presentingViewController dismissViewControllerAnimated:NO completion:nil]; 
} 

上面的代碼導致dealloc沒有被調用視圖控制器上。

如果你把你現有的代碼,並簡單地派遣(或執行選擇器:之後:) dismissViewController調用你可能會解決這個問題。

+0

但是,正如你從上面的代碼片斷中看到的那樣,即使對自我的強烈引用,它也應該在完成塊運行並且保持週期被釋放之後清理。我不清楚如何創建一個保留週期,並且UIViewController從未被拒絕,請給我上面的特定代碼片段。 – esilver

+0

保留週期應該很清楚。自我保留了收藏視圖,收藏視圖現在保留了自我。其中一個指針需要很弱以防止保留週期(因此產生'weakSelf')。我不確定你指的是什麼清理,因爲UICollectionView是一個黑匣子...我不希望collectionView將其完成塊清零,你絕對不應該依賴它 – Casey

+0

讓我們來完成你的邏輯:假設UICollectionView沒有沒有完成塊。 UICollectionView開始執行批量更新,調用第一個(強保留)塊。 UIViewController被解僱,但無法清理,因爲第二個完成塊有很強的參考。 100毫秒過去的動畫效果。 UICollectionView調用它的完成塊。現在,所有塊都運行,對所述塊的引用應該被清除/清零等,並且UIViewController沒有更多的引用並且可以釋放。這應該簡單地延遲,而不是阻止,解除分配。對? – esilver

0

這看起來像一個UICollectionView的錯誤。 API用戶不應期望在執行任務之後保留單運行塊參數,因此防止參考週期不應成爲問題。

UICollectionView應該在完成批量更新過程後清除對塊的任何引用,或者批處理更新過程中斷(例如,將集合視圖從屏幕上移除)。

你已經看到,即使集合視圖在更新過程中脫離屏幕,也會調用完成塊,因此集合視圖應該消除它對該完成塊的任何引用 - 它將永遠不會再被調用,無論收集視圖的當前狀態如何。