如何釋放含有NSTimer的UITableViewCell(SubView)

weixin_33782386發表於2016-02-24

關鍵詞: UIViewController, UITableViewCell, NSTimer, 釋放,資源清理,RAC, rac_willDeallocSignal, 響應鏈

發現問題

628177-f7896411cb66de01.jpg
ActivityViewController

上圖是一個活動列表,其中第二個Cell中有一個距開始多久的提示,這就是一個倒數計時,用來提示該活動還有多久開始。我們滿足這個需求的做法是在Cell中新增了一個NSTimer來每秒倒數計時時間。但當點選返回按鈕返回的時候,含有NSTimer的Cell是不會自動呼叫dealloc釋放資源的。Timer會持有這個Cell,如果要釋放Cell就需要在一個適當時間去invalidate這個Timer,來解除他對Cell的持有。

解決方法

跟隨上面的問題,最好是在點選返回按鈕,退出該頁面的時候。這時UIViewController會自動釋放掉,相應的去釋放Subview應該是順理成章的事。沿著這個思路我們來演進一下解決方法。

方法演進版本1

最簡單直接的方法當然是想辦法在UIViewController的delloc函式裡面去停止掉所有Subview(這裡是UITableViewCell)的Timer。為了能在上層獲取到Cell中的NSTimer,我們不得不把Cell自相關的timer從.m檔案移動到.h中。類似下面這樣


@interface ActivityTableViewCell : UITableViewCell

@property (nonatomic)NSTimer *countDownTimer;

@end

但UITableView沒有提供一個介面來獲取它持有的所有Cell的介面,我們就只有另外想辦法來記錄下所有的Timer。一個簡單的辦法就是在UIViewController中新增一個NSSet,然後在cellForRowAtIndexPath中將所有的Timer新增到這個集合中,這裡為什麼用set呢,當然是為了去掉重複的Timer。然後在UIViewController的dealloc中遍歷這個set,並依次呼叫invalidate。程式碼片段如下

//在UIViewController中新增set
@interface ActivityViewController()

@property (nonatomic)NSMutableSet *timerSet;

@end
//將Timer新增到set中
- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    ActivityTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ActivityTableViewCell" forIndexPath:indexPath];
    [self.timerSet addObject:countDownTimer];
    
    return cell;
}
//最後在dealloc中遍歷set
- (void)dealloc
{
    for (NSTimer *timer in self.timerSet){
        [timer invalidate];
    }
}

方法演進版本2

上面的解決辦法直接,簡單但存在幾個比較明顯的問題:

  • 在Cell滾動複用過程中,會重新建立Timer,但上面的解決辦法沒有移除原來不用Timer的方法,這樣當滾動次數過多的時候,set裡面就會有很多無用的物件。
  • 在UIViewControler中增加了一個timerSet,這個本和UIViewController無關的屬性,使邏輯看起來變得複雜了,別人咋看之下沒法明白這個set是幹嘛的,這不是一份易維護的程式碼。
  • 在Cell中把本來應該隱藏在.m實現檔案中的countDownTimer放到了.h檔案中,所有這個類的使用者都可以看到。這不符合封裝的原則,也不是好的程式碼風格。

為了解決上面這些問題,我們應該把主要工作移到相關的Cell裡面去做,這樣就可以在最小影響UIViewController的情況下,實現相應的功能。

這時候如果你接觸過RAC就會有些想法。RAC為NSObject新增了一個rac_willDeallocSignal的訊號,它在一個物件將要被dealloc的時候傳送。那麼我們可以在Cell裡面訂閱上層UIViewController的rac_willDeallocSignal訊號,在訊號裡面invalidate相應的timer。但我們要怎麼獲取到這個UIViewController呢,最直接簡單的辦法當然是從上層傳下去。


@interface ActivityTableViewCell : UITableViewCell

//這裡需要使用weak,不然會造成迴圈引用,你懂得!
@property (nonatomic, weak)UIViewController *controller;

- (void)configCellWithModel:(NSObject *)model;

@end

@interface ActivityTableViewCell ()

//將timer隱藏在.m檔案中
@property (nonatomic)NSTimer *countDownTimer;

@end

@implementation ActivityTableViewCell

//使用Model更新cell的函式
- (void)configCellWithModel:(NSObject *)model
{
    if (self.controller){
        @weakify(self)
        //這裡需要注意rac_willDeallocSignal沒有next,只能訂閱Completed
        [self.controller.rac_willDeallocSignal
         subscribeCompleted:^{
             @strongify(self)
             [self.countDownTimer invalidate];
             self.countDownTimer = nil;
         }];
    }
}

@end
//在UIViewController去掉set屬性,並在提供cell的函式裡面如下呼叫
- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    ActivityTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ActivityTableViewCell" forIndexPath:indexPath];
    cell.controller = self;
    [cell configCellWithModel: someModel];
    
    return cell;
}

上面說的三點問題都避免了,是不是很完美!但是...我們接著看!

方法演進版本3

還有哪裡看著不舒服,當然有!

把UIViewController傳入Cell裡面是我們很少見到的做法,細想之下也覺得不妥。這本來是兩個獨立的部分,如果採用上面的方式就是兩個獨立模組存在了一定耦合,不妥。
那有什麼辦法在Cell中直接獲取到對應的UIViewController嗎?當然有!那就是響應鏈的妙用了。如果你還不清楚響應鏈是什麼,請自行google。這裡我新增了一個UIView的擴充套件來獲取對應的UIViewController。


@implementation UIView (GetViewController)

- (UIViewController*)getViewController
{
    for (UIView* next = [self superview]; next; next = next.superview)
    {
        UIResponder* nextResponder = [next nextResponder];
        
        if ([nextResponder isKindOfClass:[UIViewController class]])
        {
            return (UIViewController*)nextResponder;
        }
    }
    
    return nil;
}

@end

哈哈,有了這個神器上面的耦合不就自然解除了!但是還有一個地方需要注意,那就是訂閱的時機。使用上面的擴充套件需要Cell被新增到檢視樹之後才能獲取到需要的UIViewController,不然得到會是一個空。那麼怎麼保證Cell一定被新增到檢視樹呢。UIView有個方法叫didMoveToSuperview,它會在該檢視的父檢視改變的時候被呼叫,我們在這裡面判斷一個就可以保證一定獲取到了對應的UIViewController。


- (void)didMoveToSuperview
{
    UIViewController *controller = [self getViewController];
    //這裡需要判斷相應的controller是否存在
    if (controller){
        @weakify(self)
        [controller.rac_willDeallocSignal
         subscribeCompleted:^{
             @strongify(self)
             [self.countDownTimer invalidate];
             self.countDownTimer = nil;
         }];
    }
}

哈哈,終於完美了!

總結

經過努力,讓原來一坨醜陋的程式碼最後簡化成了一個函式。雖然這是一個小問題,但深究之下東西還不少。對比幾種解決辦法,基本都是依據通用的程式碼規範和好的程式設計原則來優化!該場景下使用的是Cell,其實可以推廣到任何的Subview。其他的清理工作也可以使用類似的思路來實現!還有一點就是了解一下其他的程式設計方式一定會在某些時候影響你的程式碼。這裡使用了RAC,其實也是響應式程式設計的一點簡單應用。

相關文章