(iOS8.1) UITableViewで行を削除するとUITableViewやセルがメモリリークする


テーブルを編集モードにして、標準のマイナスボタンタップ、右端の削除ボタンをタップしたときに、
`UITableView`の`deleteRowsAtIndexPaths:withRowAnimation:`メソッドを呼び出すと、後で`UITableView`を破棄してもメモリ中に残ってしまう...というお話。

**そんな馬鹿な、これは流石に自分のミスでしょ**、と疑って掛かっているのですが、なかなか解決できません。
そろそろAppleのバグと言ってもいい頃合い。

## 検証コード

新規プロジェクト＞シングルビュー

* Xcode6.1
* ARC
* Objective-C
* iOS8.1

```objc:ViewController.h
#import <UIKit/UIKit.h>

@interface ViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>
@end
```

```objc:ViewController.m
#import "ViewController.h"

/// カスタムテーブルビュー
@interface MyTableView : UITableView
@end
@implementation MyTableView
- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style {
    self = [super initWithFrame:frame style:style];
    if (self) {
        NSLog(@"MyTableView init");
    }
    return self;
}
- (void)dealloc {
    NSLog(@"MyTableView dealloc");
}
@end

/// カスタムテーブルビューセル
@interface MyTableViewCell : UITableViewCell
@end
@implementation MyTableViewCell
@end

/// テスト用ビューコントローラー
@implementation ViewController {
    UITableView *_tableView;
    NSMutableArray *_items;
}

- (void)loadView {
    [super loadView];

    [self addButtonInRect:CGRectMake(0, 20, 100, 44) title:@"Create Table" action:@selector(didTouchCreateTable)];
    [self addButtonInRect:CGRectMake(100, 20, 100, 44) title:@"Delete Table" action:@selector(didTouchDeleteTable)];
}

/// ボタンを生成します。
- (void)addButtonInRect:(CGRect)rect title:(NSString *)title action:(SEL)action {
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
    btn.frame = rect;
    btn.layer.borderWidth = 1;
    btn.layer.borderColor = [UIColor blackColor].CGColor;
    [btn setTitle:title forState:UIControlStateNormal];
    [btn addTarget:self action:action forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btn];
}

/// テーブルを生成します。
- (void)didTouchCreateTable {
    if (!_tableView) {
        NSLog(@"didTouchCreateTable");
        
        // アイテムをついでに作っておく
        _items = [NSMutableArray new];
        for (int i = 0; i < 6; i++) {
            [_items addObject:[NSString stringWithFormat:@"Item %d", i]];
        }
        
        // テーブル生成
        CGRect frame = [UIScreen mainScreen].applicationFrame;
        frame = CGRectInset(frame, 5, 5);
        frame.origin.y += 70;
        frame.size.height -= 70;
        _tableView = [[MyTableView alloc] initWithFrame:frame style:UITableViewStylePlain];
        _tableView.layer.borderWidth = 1;
        _tableView.layer.borderColor = [UIColor blackColor].CGColor;
        _tableView.dataSource = self;
        _tableView.delegate = self;
        [_tableView setEditing:YES animated:NO];
        [self.view addSubview:_tableView];
    }
}

/// テーブルを破棄します。
- (void)didTouchDeleteTable {
    if (_tableView) {
        NSLog(@"didTouchDeleteTable");
        
        [_tableView removeFromSuperview];
        _tableView = nil;
    }
}

#pragma mark - UITableViewDataSource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [_items count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *cellIdentifier = @"Cell";
    MyTableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (!cell) {
        cell = [[MyTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
    }
    cell.textLabel.text = _items[indexPath.row];
    return cell;
}

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    
    switch (editingStyle) {
        case UITableViewCellEditingStyleDelete:
            NSLog(@"delete row %d", (int)indexPath.row);
            [_items removeObjectAtIndex:indexPath.row];
            [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        
        default:
            break;
    }
}

#pragma mark - UITableViewDelegate

- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
    return UITableViewCellEditingStyleDelete;
}

@end
```

## 実行結果

* iPhone 6 Plus (iOS8.1)
* iPhone 4s (iOS8.1)　
* iPad3 (iOS8.1)

どれでも再現しますが、4sのスクショにします。

起動後
![Screen Shot 2014-11-07 at 10.39.23.png](https://qiita-image-store.s3.amazonaws.com/0/31347/b9ab0dcd-7f12-1736-bbbf-884eb4b7029e.png "Screen Shot 2014-11-07 at 10.39.23.png")

Create Tableボタンタップ：
![Screen Shot 2014-11-07 at 10.39.25.png](https://qiita-image-store.s3.amazonaws.com/0/31347/85161bf0-77d9-ab9c-295b-428f5df6b59e.png "Screen Shot 2014-11-07 at 10.39.25.png")

Delete Tableボタンタップ：
![Screen Shot 2014-11-07 at 10.39.27.png](https://qiita-image-store.s3.amazonaws.com/0/31347/5f135919-85a9-0629-6fce-f55ab3de6cb5.png "Screen Shot 2014-11-07 at 10.39.27.png")

ログ：

```text:
2014-11-07 10:39:22.819 TableTest[1018:34604] didTouchCreateTable
2014-11-07 10:39:22.824 TableTest[1018:34604] MyTableView init
2014-11-07 10:39:25.097 TableTest[1018:34604] didTouchDeleteTable
2014-11-07 10:39:25.101 TableTest[1018:34604] MyTableView dealloc
```

ちゃんとMyTableViewのdeallocが呼ばれていますね。

----

一旦アプリを再起動して、今度は行削除をやってみます。

起動：（省略）
Create Tableボタンタップ：（省略）

[Item 0]の赤丸マイナスボタンをタップ：
![Screen Shot 2014-11-07 at 10.40.15.png](https://qiita-image-store.s3.amazonaws.com/0/31347/2c1e38e9-0b3c-1ee9-ba63-9d45e428acbe.png "Screen Shot 2014-11-07 at 10.40.15.png")

削除ボタンタップ：
![Screen Shot 2014-11-07 at 10.40.18.png](https://qiita-image-store.s3.amazonaws.com/0/31347/33282384-2693-09c8-de7d-d63f2fafc2fb.png "Screen Shot 2014-11-07 at 10.40.18.png")

Delete Tableボタンタップ：
![Screen Shot 2014-11-07 at 10.40.23.png](https://qiita-image-store.s3.amazonaws.com/0/31347/3f9f77df-db24-7e36-e3d6-b58250cb9610.png "Screen Shot 2014-11-07 at 10.40.23.png")

ログ：

```text:
2014-11-07 10:40:08.325 TableTest[1064:35319] didTouchCreateTable
2014-11-07 10:40:08.331 TableTest[1064:35319] MyTableView init
2014-11-07 10:40:15.018 TableTest[1064:35319] delete row 0
2014-11-07 10:40:20.516 TableTest[1064:35319] didTouchDeleteTable
```

MyTableViewのdealloc来ません。

## 一応Profileしてみた

行削除＞Delete Table後でのスクショです。

![スクリーンショット 2014-11-07 11.02.52.png](https://qiita-image-store.s3.amazonaws.com/0/31347/e549c4bc-4c11-f5e6-51b7-52215338a05a.png "スクリーンショット 2014-11-07 11.02.52.png")

MyTableView消えませんね。

ちなみにMyTableViewの参照カウンタは1でした。
![スクリーンショット 2014-11-07 11.20.45.png](https://qiita-image-store.s3.amazonaws.com/0/31347/ee50691a-bd1b-da73-032b-56f494115176.png "スクリーンショット 2014-11-07 11.20.45.png")

## メモ

* `deleteRowsAtIndexPaths`辺りがトリガーとなってリークを起こしているっぽいのは判っているのですが、回避方法が不明です（stackoverflowなどを彷徨い中）。
* テーブル行のハイフンボタン操作の後、`deleteRowsAtIndexPaths`を呼ぶとリークしますが、単純な`UIButton`をタップされたときに、適当な行を`deleteRowsAtIndexPaths`してもリークにはなりません（ので削除UIを独自のものにしてしまえば回避できる可能性が高い...けどAppleっぽくなくて抵抗はありますね）。
* 駄目元でバグレポート提出済み。
* [iOS 7.0 and ARC: UITableView never deallocated after rows animation(stackoverflow)](http://stackoverflow.com/questions/18919870/ios-7-0-and-arc-uitableview-never-deallocated-after-rows-animation/)と現象としては同じかな？
* 最悪同じUITableViewのインスタンスを使い回すというのも手かも...
* 回避策引き続き模索中...

## 最後に

何か基本的な見落としをしている気がしますので、何かお気付きの点がありましたらコメントをお願いいたします。