2016年1月13日 星期三

iOS多執行緒以及UITableView

這篇主要紀錄我如何設計一個多執行緒存取NSMutableArray以及使用UITableView呈現的方法。

關於Thread、GCD、NSOperation的基本知識可以參考KKBox的教材

@property (strong, atomic) NSMutableArray<Order *> *orders;

把property宣告成atomic只能保證setOrders:執行到一半不會被其他thread插入而已,對保護NSMutableArray裡面的東西一點效果都沒有。可以看Objc.io這一篇

我們必須保證一次只能有一個執行緒修改NSMutableArray,這時需要使用保護機制,有
- NSLock
- @synchronized
- pthread_mutex_t
- NSRecursiveLock
- NSConditionLock
- NSDistributedLock
- OSSPinlock

可以在GCD裡用的有
- Serial queue
- dispatch_barrier_async + dispatch_sync
- dispatch group
- dispatch semaphore

可以參考https://www.zybuluo.com/MicroCai/note/64272
另外也有人做效能評比http://lijianfei.sinaapp.com/?p=655

有了保護機制之後就要決定要保護的區塊有多大。
考慮以下情況,假設我們正在實做一個remove方法,而這段code會跑在GCD的global queue,所以可以用dispatch semaphore。

if (_orders.count > 0) {
    NSInteger index = [self indexOfOrderByUUID:uuid];
    if (index != NSNotFound) {
        Order *order = _orders[index];
        [self removeObjectFromOrdersAtIndex:index];
    }
}

我最後的作法是底下這樣,一進critical section之後檢查有沒有order可以刪,沒有就閃人。如果有,不管有沒有找到目標,最後都要signal一下讓別人可以進critical section繼續做事,不然就會卡死。

dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
if (_orders.count > 0) {
    NSInteger index = [self indexOfOrderByUUID:uuid];
    if (index != NSNotFound) {
        Order *order = _orders[index];
        [self removeObjectFromOrdersAtIndex:index];
        dispatch_semaphore_signal(_semaphore);
    }
    else {
        dispatch_semaphore_signal(_semaphore);
    }
}
else {
    dispatch_semaphore_signal(_semaphore);
}

保護好NSMutableArray確保沒有race condition之後,就要考慮如何讓UITableView呈現資料了。

如果直接讓UITableView對NSMutableArray做KVO的話會有一個問題,由於UITableView是呼叫dataSource的
numberOfSectionsInTableView以及cellForRowAtIndexPath決定資料數目與cell的設置,這些都是在main thread執行,但是如果有許多background thread在這兩個delegate被呼叫之間新增或刪除了NSMutableArray裡面的內容,那麼資料就會不同步了。

我的解法是在view controller或是view model設置一個NSArray做cache,當KVO觀察到NSMutableArray變化時,copy到這個cache。如果單純使用self.cacheOrders = 這種accessor的話,其實只是做shallow copy,並不會有太大的overhead。每次KVO觀察到NSMutableArray變化都會先更新cache,而background thread並不會去動到這份cache,這樣應該就可以解決不同步的問題了。

@property (copy, nonatomic) NSArray *cacheOrders;

[self.KVOController observe:_service keyPath:@keypath(_service, orders) options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew block:^(OrderListTableViewController *observer, OrderService *service, NSDictionary *change) {
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            self.cacheOrders = _service.orders;
            [observer.tableView reloadData];
        }];
}];

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return _cacheOrders.count;
}


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    OrderListTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:OrderListTableViewControllerCellIdentifier forIndexPath:indexPath];
    Order *order = self.cacheOrders[indexPath.row];

    return cell;
}