多線程開(kāi)發(fā)的性能

一、簡(jiǎn)介

  • performSelectorInBackground:withObject: 這是讓代碼在主線程之外運(yùn)行的最簡(jiǎn)單方法,當(dāng)任務(wù)很簡(jiǎn)單且需求明確時(shí)這個(gè)方法當(dāng)效果很好。使用這個(gè)方法,系統(tǒng)不會(huì)承擔(dān)有關(guān)任務(wù)任何額外當(dāng)管理工作,所以它最適用與非循環(huán)當(dāng)任務(wù)。
  • NSOperationQueue:這個(gè)方法稍微有點(diǎn)復(fù)雜,它提供了一些額外的控制功能,比如串行運(yùn)行、并發(fā)運(yùn)行或者按照任務(wù)間的依賴(lài)關(guān)系運(yùn)行。隊(duì)列操作最適用于重復(fù)任務(wù)和定義明確的異步任務(wù),比如網(wǎng)絡(luò)調(diào)用和解析。
  • GCD隊(duì)列:這是讓代碼在主線程外運(yùn)行所使用的最底層的方法,其靈活性也是最好的。示例程序?qū)?huì)演示使用GCD隊(duì)列以串行和并發(fā)的方式運(yùn)行任務(wù)。GCD的使用范圍很廣,從實(shí)現(xiàn)后臺(tái)程序和主隊(duì)列間的通信到為列表中的每一個(gè)元素快速執(zhí)行代碼段都可以使用,還可以處理大型、重復(fù)的異步任務(wù)。

二、隊(duì)列介紹

有關(guān)并發(fā)處理的各種術(shù)語(yǔ)會(huì)讓人覺(jué)得很困惑。線程就是其中一個(gè)常用術(shù)語(yǔ);在iOS應(yīng)用的上下文中,一個(gè)線程就是一個(gè)標(biāo)準(zhǔn)的POSIX線程。從技術(shù)上講,一個(gè)線程就是在進(jìn)程(一個(gè)應(yīng)用可以看作一個(gè)進(jìn)程)中的一組可以獨(dú)立處理的指令,一個(gè)進(jìn)程可以包含多個(gè)線程,它們共享內(nèi)存和資源。因?yàn)榫€程的功能是獨(dú)立的,所以可以通過(guò)將線程分開(kāi)工作來(lái)獲得更快的運(yùn)行速度。當(dāng)多個(gè)線程需要訪問(wèn)同樣的資源或數(shù)據(jù)時(shí),有可能出現(xiàn)問(wèn)題。所以iOS應(yīng)用都具有一個(gè)主線程用于處理運(yùn)行環(huán)路和更新UI界面。應(yīng)用要保持對(duì)用戶交互都響應(yīng),主線程的任務(wù)必須是可以在六十分只一秒內(nèi)完成的任務(wù)。

隊(duì)列是蘋(píng)果公司在Grand Central Dispatch中提出的用于描述一種上下文的術(shù)語(yǔ)。隊(duì)列是由GCD管理的一組需要執(zhí)行的任務(wù)。根據(jù)所處當(dāng)前系統(tǒng)的情況,GCD會(huì)動(dòng)態(tài)確定隊(duì)列中用來(lái)執(zhí)行的線程個(gè)數(shù)。主隊(duì)列是一個(gè)由GCD管理的特殊隊(duì)列,它和主線程相關(guān)聯(lián)。所以當(dāng)你在主隊(duì)列運(yùn)行一個(gè)任務(wù)是,GCD也會(huì)在主線程上執(zhí)行該任務(wù)。

人們會(huì)經(jīng)常認(rèn)為線程和隊(duì)列兩個(gè)概念是可以互換的,一定要記住隊(duì)列就是一組被管理的線程,而“主”的概念只是對(duì)于處理主運(yùn)行環(huán)路和UI的線程而言的。

三、在主線程上運(yùn)行

運(yùn)行示例程序,表視圖中的5個(gè)初始元素都是可見(jiàn)的,不過(guò)它們不能滑動(dòng),當(dāng)額外當(dāng)元素被添加進(jìn)來(lái)時(shí)UI完全沒(méi)有響應(yīng)。當(dāng)額外當(dāng)元素添加時(shí),UI是凍結(jié)當(dāng),通過(guò)在運(yùn)行應(yīng)用時(shí)查看輸出日志和調(diào)試控制臺(tái)就可以看出。凍結(jié)當(dāng)UI顯然是糟糕的用戶體驗(yàn),并且遺憾的是,在應(yīng)用中很容易找到類(lèi)似的情況。

#import "ICFMainThreadLongRunningTaskViewController.h"

@interface ICFMainThreadLongRunningTaskViewController ()
- (void)performLongRunningTaskForIteration:(id)iteration;
@end

@implementation ICFMainThreadLongRunningTaskViewController

- (void)performLongRunningTaskForIteration:(id)iteration
{
    NSNumber *iterationNumber = (NSNumber *)iteration;
    
    NSMutableArray *newArray =
    [[NSMutableArray alloc] initWithCapacity:10];
    
    for (int i=1; i<=10; i++)
    {
        
        [newArray addObject:
         [NSString stringWithFormat:@"Item %@-%d",
          iterationNumber,i]];
        
        [NSThread sleepForTimeInterval:.1];
        
        NSLog(@"Main Added %@-%d",iterationNumber,i);
    }
    
    [self.displayItems addObject:newArray];
    [self.tableView reloadData];
}

- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.displayItems =
    [[NSMutableArray alloc] initWithCapacity:45];
    
    [self.displayItems addObject:@[@"Item Initial-1",
     @"Item Initial-2",@"Item Initial-3",
     @"Item Initial-4",@"Item Initial-5"]];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    for (int i=1; i<=5; i++)
    {
        NSNumber *iteration = [NSNumber numberWithInt:i];
        [self performLongRunningTaskForIteration:iteration];
    }
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [self.displayItems count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[self.displayItems objectAtIndex:section] count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"ICFMainThreadCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    
    NSMutableArray *itemsForRow = [self.displayItems objectAtIndex:indexPath.section];
    NSString *labelForRow = [itemsForRow objectAtIndex:indexPath.row];
    [cell.textLabel setText:labelForRow];
    
    return cell;
}


#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
}

@end

在這種情況下,調(diào)用 sleepForTimeInterval: 方法會(huì)很快且經(jīng)常地阻塞主線程,即使for循環(huán)完成后,在所有performLongRunningTaskForIteration:方法沒(méi)有執(zhí)行完成之前,主線程仍然沒(méi)有足夠的時(shí)間更新UI。

四、在后臺(tái)運(yùn)行

從以下代碼中可以看到,使用后臺(tái)執(zhí)行的方法對(duì) selector 進(jìn)行設(shè)置。NSObject 定義的方法 performSelectorInBackground:withObject:,需要傳遞一個(gè) Objective-C 對(duì)象作為 withObject: 的參數(shù)。這個(gè)方法將會(huì)生成一個(gè)新的線程,根據(jù)傳入的參數(shù)在新線程上執(zhí)行該方法,并立即返回調(diào)用線程。這個(gè)新的線程由開(kāi)發(fā)者負(fù)責(zé)管理,所以頻繁調(diào)用這個(gè)方法完全有可能創(chuàng)建許多新的線程而使系統(tǒng)壓力增大。如果測(cè)試發(fā)現(xiàn)有錯(cuò)誤出現(xiàn),可以使用操作隊(duì)列或調(diào)度隊(duì)列在任務(wù)執(zhí)行過(guò)程中進(jìn)行更準(zhǔn)確的控制,以便更好地管理系統(tǒng)資源。

#import "ICFPerformBackgroundViewController.h"

@interface ICFPerformBackgroundViewController ()
- (void)performLongRunningTaskForIteration:(id)iteration;
- (void)updateTableData:(id)moreData;
@end

@implementation ICFPerformBackgroundViewController

- (void)performLongRunningTaskForIteration:(id)iteration
{
    NSNumber *iterationNumber = (NSNumber *)iteration;
    
    NSMutableArray *newArray =
    [[NSMutableArray alloc] initWithCapacity:10];
    
    for (int i=1; i<=10; i++)
    {
        
        [newArray addObject:
         [NSString stringWithFormat:@"Item %@-%d",
          iterationNumber,i]];
        
        [NSThread sleepForTimeInterval:.1];
        
        NSLog(@"Background Added %@-%d",iterationNumber,i);
    }
    
    [self performSelectorOnMainThread:@selector(updateTableData:)
                           withObject:newArray
                        waitUntilDone:NO];
}

- (void)updateTableData:(id)moreData
{
    NSArray *newArray = (NSArray *)moreData;
    [self.displayItems addObject:newArray];
    [self.tableView reloadData];    
}

- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.displayItems = [[NSMutableArray alloc] initWithCapacity:5];
    [self.displayItems addObject:@[@"Item Initial-1",@"Item Initial-2",@"Item Initial-3",@"Item Initial-4",@"Item Initial-5"]];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    SEL taskSelector =
    @selector(performLongRunningTaskForIteration:);

    for (int i=1; i<=5; i++)
    {
        NSNumber *iteration = [NSNumber numberWithInt:i];
        [self performSelectorInBackground:taskSelector
                               withObject:iteration];
    }
}


- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [self.displayItems count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[self.displayItems objectAtIndex:section] count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"ICFBackgroundCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    
    // Configure the cell...
    NSMutableArray *itemsForRow = [self.displayItems objectAtIndex:indexPath.section];
    NSString *labelForRow = [itemsForRow objectAtIndex:indexPath.row];
    [cell.textLabel setText:labelForRow];
    
    return cell;
}

#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
}

@end

方法 performLongRunningTaskForIteration: 執(zhí)行任務(wù)的方法同 Main Thread 方法中的一樣,不過(guò)這里不再是直接向 displayItems 中添加 newArray,而是使用 NSObject 的方法 performSelectorOnMainThread:withObject:waitUntilDone: 調(diào)用 updateTableData: 方法。使用這個(gè)方法的原因有兩個(gè):
首先,包含在表視圖中的UIKit對(duì)象只有在主線程上才能更新UI界面;
其次,displayItems屬性被聲明為 nonatomic 類(lèi)型,這意味著生成的 getter 和 setter 方法并不是線程安全的。要 “修復(fù)”這個(gè)問(wèn)題,可以將displayItems 聲明為 atomic 類(lèi)型,這樣就需要在數(shù)組更新前將其鎖定,這會(huì)增加性能上的開(kāi)銷(xiāo)。如果屬性在主線程上更新,就不需要鎖定了。

updateTableData: 方法用于將新創(chuàng)建的元素添加到 displayItems 數(shù)組中并通知表視圖重載和更新UI界面。
一個(gè)有意思的副作用是這些后添加的行的順序是不確定的,即應(yīng)用每次啟動(dòng)時(shí)都有可能不同。

2017-11-30 13:28:28.712214+0800 LongRunningTasks[895:181158] Background Added 1-1
2017-11-30 13:28:28.713574+0800 LongRunningTasks[895:181159] Background Added 2-1
2017-11-30 13:28:28.714598+0800 LongRunningTasks[895:181161] Background Added 4-1
2017-11-30 13:28:28.722606+0800 LongRunningTasks[895:181160] Background Added 3-1
2017-11-30 13:28:28.723873+0800 LongRunningTasks[895:181162] Background Added 5-1
2017-11-30 13:28:28.881256+0800 LongRunningTasks[895:181158] Background Added 1-2
2017-11-30 13:28:28.882564+0800 LongRunningTasks[895:181159] Background Added 2-2
2017-11-30 13:28:28.883513+0800 LongRunningTasks[895:181161] Background Added 4-2
2017-11-30 13:28:28.897932+0800 LongRunningTasks[895:181160] Background Added 3-2
2017-11-30 13:28:28.899231+0800 LongRunningTasks[895:181162] Background Added 5-2
2017-11-30 13:28:29.054651+0800 LongRunningTasks[895:181158] Background Added 1-3
2017-11-30 13:28:29.055960+0800 LongRunningTasks[895:181159] Background Added 2-3
2017-11-30 13:28:29.057052+0800 LongRunningTasks[895:181161] Background Added 4-3
2017-11-30 13:28:29.064670+0800 LongRunningTasks[895:181160] Background Added 3-3
2017-11-30 13:28:29.065975+0800 LongRunningTasks[895:181162] Background Added 5-3
2017-11-30 13:28:29.228852+0800 LongRunningTasks[895:181158] Background Added 1-4
2017-11-30 13:28:29.231612+0800 LongRunningTasks[895:181159] Background Added 2-4
2017-11-30 13:28:29.232979+0800 LongRunningTasks[895:181161] Background Added 4-4
2017-11-30 13:28:29.233937+0800 LongRunningTasks[895:181160] Background Added 3-4
2017-11-30 13:28:29.234966+0800 LongRunningTasks[895:181162] Background Added 5-4
2017-11-30 13:28:29.398651+0800 LongRunningTasks[895:181158] Background Added 1-5
2017-11-30 13:28:29.400140+0800 LongRunningTasks[895:181159] Background Added 2-5
2017-11-30 13:28:29.400921+0800 LongRunningTasks[895:181161] Background Added 4-5
2017-11-30 13:28:29.402092+0800 LongRunningTasks[895:181160] Background Added 3-5
2017-11-30 13:28:29.410778+0800 LongRunningTasks[895:181162] Background Added 5-5
2017-11-30 13:28:29.565031+0800 LongRunningTasks[895:181158] Background Added 1-6
2017-11-30 13:28:29.565506+0800 LongRunningTasks[895:181161] Background Added 4-6
2017-11-30 13:28:29.566321+0800 LongRunningTasks[895:181160] Background Added 3-6
2017-11-30 13:28:29.565091+0800 LongRunningTasks[895:181159] Background Added 2-6
2017-11-30 13:28:29.581668+0800 LongRunningTasks[895:181162] Background Added 5-6
2017-11-30 13:28:29.677827+0800 LongRunningTasks[895:181158] Background Added 1-7
2017-11-30 13:28:29.678298+0800 LongRunningTasks[895:181160] Background Added 3-7
2017-11-30 13:28:29.731737+0800 LongRunningTasks[895:181161] Background Added 4-7
2017-11-30 13:28:29.731737+0800 LongRunningTasks[895:181159] Background Added 2-7
2017-11-30 13:28:29.748420+0800 LongRunningTasks[895:181162] Background Added 5-7
2017-11-30 13:28:29.845071+0800 LongRunningTasks[895:181160] Background Added 3-8
2017-11-30 13:28:29.846579+0800 LongRunningTasks[895:181161] Background Added 4-8
2017-11-30 13:28:29.852906+0800 LongRunningTasks[895:181158] Background Added 1-8
2017-11-30 13:28:29.902459+0800 LongRunningTasks[895:181159] Background Added 2-8
2017-11-30 13:28:29.924039+0800 LongRunningTasks[895:181162] Background Added 5-8
2017-11-30 13:28:30.015191+0800 LongRunningTasks[895:181160] Background Added 3-9
2017-11-30 13:28:30.016704+0800 LongRunningTasks[895:181161] Background Added 4-9
2017-11-30 13:28:30.029297+0800 LongRunningTasks[895:181158] Background Added 1-9
2017-11-30 13:28:30.078941+0800 LongRunningTasks[895:181159] Background Added 2-9
2017-11-30 13:28:30.098572+0800 LongRunningTasks[895:181162] Background Added 5-9
2017-11-30 13:28:30.181942+0800 LongRunningTasks[895:181160] Background Added 3-10
2017-11-30 13:28:30.220771+0800 LongRunningTasks[895:181161] Background Added 4-10
2017-11-30 13:28:30.224771+0800 LongRunningTasks[895:181158] Background Added 1-10
2017-11-30 13:28:30.248644+0800 LongRunningTasks[895:181159] Background Added 2-10
2017-11-30 13:28:30.265323+0800 LongRunningTasks[895:181162] Background Added 5-10

這是一個(gè)事實(shí),所以使用這種技術(shù)對(duì)于任務(wù)何時(shí)完成或者說(shuō)任務(wù)執(zhí)行的順序都是無(wú)法確定的,因?yàn)槿蝿?wù)都是在不同線程上執(zhí)行的。如果操作的順序不重要,就適用該技術(shù);如果順序重要,操作隊(duì)列或調(diào)度隊(duì)列就需要以串行的方式執(zhí)行任務(wù)(后面我們會(huì)對(duì)這兩種隊(duì)列進(jìn)行描述,分別在“串行操作”和“串行調(diào)度隊(duì)列”兩個(gè)小節(jié)中)。

五、在操作隊(duì)列中運(yùn)行

操作隊(duì)列(NSOperationQueue)可以對(duì)一組任務(wù)進(jìn)行管理或操作(NSOperation)。一個(gè)操作隊(duì)列可以指定多個(gè)操作并發(fā)運(yùn)行、可以掛起任務(wù)和重啟、可以取消所有暫停的操作。操作可以是簡(jiǎn)單的方法調(diào)用、代碼快或自定義的操作類(lèi)。操作可以具有使其能夠串行運(yùn)行的依賴(lài)關(guān)系。操作和操作隊(duì)列實(shí)際上由 Grand Central Dispathc 進(jìn)行管理,并在調(diào)度隊(duì)列中實(shí)現(xiàn)。
以下將逐一介紹并發(fā)操作、帶有依賴(lài)關(guān)系的串行操作和支持取消的自定義操作。

5.1 并發(fā)操作

運(yùn)行以下代碼,表視圖中顯示了5個(gè)初始元素,在長(zhǎng)線任務(wù)執(zhí)行時(shí)它們?nèi)匀豢梢曰瑒?dòng)。任務(wù)完成后,額外的行就可見(jiàn)了。

#import "ICFOperationQueueConcurrentViewController.h"

@interface ICFOperationQueueConcurrentViewController ()
- (void)performLongRunningTaskForIteration:(id)iteration;
- (void)updateTableData:(id)moreData;
@end

@implementation ICFOperationQueueConcurrentViewController

- (IBAction)cancelButtonTouched:(id)sender
{
    [self.processingQueue cancelAllOperations];
}

- (void)performLongRunningTaskForIteration:(id)iteration
{
    NSNumber *iterationNumber = (NSNumber *)iteration;
    
    NSMutableArray *newArray =
    [[NSMutableArray alloc] initWithCapacity:10];
    
    for (int i=1; i<=10; i++)
    {
        [newArray addObject:
         [NSString stringWithFormat:@"Item %@-%d",
          iterationNumber,i]];
        
        [NSThread sleepForTimeInterval:.1];
        NSLog(@"OpQ Concurrent Added %@-%d",iterationNumber,i);
    }
    
    [self performSelectorOnMainThread:@selector(updateTableData:)
                           withObject:newArray
                        waitUntilDone:NO];
}

- (void)updateTableData:(id)moreData
{
    NSArray *newArray = (NSArray *)moreData;
    [self.displayItems addObject:newArray];
    [self.tableView reloadData];
}

- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.displayItems = [[NSMutableArray alloc] initWithCapacity:2];
    
    [self.displayItems addObject:@[@"Item Initial-1",
     @"Item Initial-2",@"Item Initial-3",
     @"Item Initial-4",@"Item Initial-5"]];
    
    self.processingQueue = [[NSOperationQueue alloc] init];
    [self.tableView setTableHeaderView:self.statusView];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    SEL taskSelector =
    @selector(performLongRunningTaskForIteration:);

    for (int i=1; i<=5; i++)
    {
        
        NSNumber *iteration = [NSNumber numberWithInt:i];
        
        NSInvocationOperation *operation =
        [[NSInvocationOperation alloc] initWithTarget:self
        selector:taskSelector object:iteration];
        
        [operation setCompletionBlock:^{
            NSLog(@"Operation #%d completed.",i);
        }];
        
        [self.processingQueue addOperation:operation];
    }
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [self.displayItems count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[self.displayItems objectAtIndex:section] count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"ICFOperationConcurrentCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    
    // Configure the cell...
    NSMutableArray *itemsForRow = [self.displayItems objectAtIndex:indexPath.section];
    NSString *labelForRow = [itemsForRow objectAtIndex:indexPath.row];
    [cell.textLabel setText:labelForRow];
    
    return cell;
}


#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
}

@end

閱讀以上代碼,了解并發(fā)操作是如何設(shè)置的。在添加操作前,需要在viewDidLoad:方法中設(shè)置操作隊(duì)列:

self.processingQueue = [[NSOperationQueue alloc] init];

這個(gè)方法設(shè)置初始數(shù)據(jù)的方式同 Main Thread 方法一樣。在初始數(shù)據(jù)設(shè)置完且視圖可見(jiàn)之后,長(zhǎng)線任務(wù)作為一個(gè) NSInvocationOperation 實(shí)例被添加到操作隊(duì)列中:

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    SEL taskSelector =
    @selector(performLongRunningTaskForIteration:);

    for (int i=1; i<=5; i++)
    {
        
        NSNumber *iteration = [NSNumber numberWithInt:i];
        
        NSInvocationOperation *operation =
        [[NSInvocationOperation alloc] initWithTarget:self
        selector:taskSelector object:iteration];
        
        [operation setCompletionBlock:^{
            NSLog(@"Operation #%d completed.",i);
        }];
        
        [self.processingQueue addOperation:operation];
    }
}

每個(gè)操作都被分配了一個(gè) completion block, 操作處理結(jié)束時(shí)會(huì)運(yùn)行這個(gè)塊代碼。performLongRunningTaskForIteration:方法所執(zhí)行都任務(wù)完全同 Perform Background 方式一樣,實(shí)際上,并發(fā)操作上用到都這個(gè)方法并沒(méi)有變化。updateTableData:方法也沒(méi)有變化。結(jié)果也和Perform Background 方式類(lèi)似,元素都添加也沒(méi)有確定都順序。

2017-11-30 13:55:40.130648+0800 LongRunningTasks[895:191148] OpQ Concurrent Added 2-1
2017-11-30 13:55:40.130731+0800 LongRunningTasks[895:186723] OpQ Concurrent Added 1-1
2017-11-30 13:55:40.130984+0800 LongRunningTasks[895:191150] OpQ Concurrent Added 3-1
2017-11-30 13:55:40.130990+0800 LongRunningTasks[895:191151] OpQ Concurrent Added 4-1
2017-11-30 13:55:40.131134+0800 LongRunningTasks[895:191152] OpQ Concurrent Added 5-1
2017-11-30 13:55:40.236213+0800 LongRunningTasks[895:191148] OpQ Concurrent Added 2-2
2017-11-30 13:55:40.236503+0800 LongRunningTasks[895:186723] OpQ Concurrent Added 1-2
2017-11-30 13:55:40.236647+0800 LongRunningTasks[895:191151] OpQ Concurrent Added 4-2
2017-11-30 13:55:40.236783+0800 LongRunningTasks[895:191150] OpQ Concurrent Added 3-2
2017-11-30 13:55:40.236920+0800 LongRunningTasks[895:191152] OpQ Concurrent Added 5-2
2017-11-30 13:55:40.340567+0800 LongRunningTasks[895:191148] OpQ Concurrent Added 2-3
2017-11-30 13:55:40.340859+0800 LongRunningTasks[895:186723] OpQ Concurrent Added 1-3
2017-11-30 13:55:40.341004+0800 LongRunningTasks[895:191151] OpQ Concurrent Added 4-3
2017-11-30 13:55:40.341141+0800 LongRunningTasks[895:191150] OpQ Concurrent Added 3-3
2017-11-30 13:55:40.341278+0800 LongRunningTasks[895:191152] OpQ Concurrent Added 5-3
2017-11-30 13:55:40.444323+0800 LongRunningTasks[895:191148] OpQ Concurrent Added 2-4
2017-11-30 13:55:40.444609+0800 LongRunningTasks[895:186723] OpQ Concurrent Added 1-4
2017-11-30 13:55:40.444753+0800 LongRunningTasks[895:191151] OpQ Concurrent Added 4-4
2017-11-30 13:55:40.444890+0800 LongRunningTasks[895:191150] OpQ Concurrent Added 3-4
2017-11-30 13:55:40.445027+0800 LongRunningTasks[895:191152] OpQ Concurrent Added 5-4
2017-11-30 13:55:40.544916+0800 LongRunningTasks[895:191148] OpQ Concurrent Added 2-5
2017-11-30 13:55:40.545177+0800 LongRunningTasks[895:186723] OpQ Concurrent Added 1-5
2017-11-30 13:55:40.550485+0800 LongRunningTasks[895:191151] OpQ Concurrent Added 4-5
2017-11-30 13:55:40.550890+0800 LongRunningTasks[895:191150] OpQ Concurrent Added 3-5
2017-11-30 13:55:40.551086+0800 LongRunningTasks[895:191152] OpQ Concurrent Added 5-5
2017-11-30 13:55:40.650279+0800 LongRunningTasks[895:186723] OpQ Concurrent Added 1-6
2017-11-30 13:55:40.650279+0800 LongRunningTasks[895:191148] OpQ Concurrent Added 2-6
2017-11-30 13:55:40.655973+0800 LongRunningTasks[895:191151] OpQ Concurrent Added 4-6
2017-11-30 13:55:40.655979+0800 LongRunningTasks[895:191150] OpQ Concurrent Added 3-6
2017-11-30 13:55:40.656027+0800 LongRunningTasks[895:191152] OpQ Concurrent Added 5-6
2017-11-30 13:55:40.755762+0800 LongRunningTasks[895:186723] OpQ Concurrent Added 1-7
2017-11-30 13:55:40.755762+0800 LongRunningTasks[895:191148] OpQ Concurrent Added 2-7
2017-11-30 13:55:40.759462+0800 LongRunningTasks[895:191150] OpQ Concurrent Added 3-7
2017-11-30 13:55:40.761635+0800 LongRunningTasks[895:191152] OpQ Concurrent Added 5-7
2017-11-30 13:55:40.761635+0800 LongRunningTasks[895:191151] OpQ Concurrent Added 4-7
2017-11-30 13:55:40.859583+0800 LongRunningTasks[895:191148] OpQ Concurrent Added 2-8
2017-11-30 13:55:40.861219+0800 LongRunningTasks[895:186723] OpQ Concurrent Added 1-8
2017-11-30 13:55:40.861464+0800 LongRunningTasks[895:191150] OpQ Concurrent Added 3-8
2017-11-30 13:55:40.864972+0800 LongRunningTasks[895:191152] OpQ Concurrent Added 5-8
2017-11-30 13:55:40.867129+0800 LongRunningTasks[895:191151] OpQ Concurrent Added 4-8
2017-11-30 13:55:40.965054+0800 LongRunningTasks[895:191148] OpQ Concurrent Added 2-9
2017-11-30 13:55:40.965415+0800 LongRunningTasks[895:186723] OpQ Concurrent Added 1-9
2017-11-30 13:55:40.965562+0800 LongRunningTasks[895:191150] OpQ Concurrent Added 3-9
2017-11-30 13:55:40.969690+0800 LongRunningTasks[895:191152] OpQ Concurrent Added 5-9
2017-11-30 13:55:40.972404+0800 LongRunningTasks[895:191151] OpQ Concurrent Added 4-9
2017-11-30 13:55:41.070392+0800 LongRunningTasks[895:191148] OpQ Concurrent Added 2-10
2017-11-30 13:55:41.099595+0800 LongRunningTasks[895:186723] OpQ Concurrent Added 1-10
2017-11-30 13:55:41.100562+0800 LongRunningTasks[895:191150] OpQ Concurrent Added 3-10
2017-11-30 13:55:41.100942+0800 LongRunningTasks[895:191152] OpQ Concurrent Added 5-10
2017-11-30 13:55:41.101133+0800 LongRunningTasks[895:191151] OpQ Concurrent Added 4-10
2017-11-30 13:55:41.101590+0800 LongRunningTasks[895:186723] Operation #3 completed.
2017-11-30 13:55:41.101729+0800 LongRunningTasks[895:186723] Operation #4 completed.
2017-11-30 13:55:41.101839+0800 LongRunningTasks[895:186723] Operation #1 completed.
2017-11-30 13:55:41.101981+0800 LongRunningTasks[895:191148] Operation #2 completed.
2017-11-30 13:55:41.102147+0800 LongRunningTasks[895:191152] Operation #5 completed.

這里主要都區(qū)別是NSOperationQueue現(xiàn)在負(fù)責(zé)管理線程,并且只有當(dāng)隊(duì)列中的線程到達(dá)默認(rèn)最大并發(fā)操作時(shí)才進(jìn)行處理。當(dāng)應(yīng)用中有很多不同當(dāng)競(jìng)爭(zhēng)任務(wù)同時(shí)出現(xiàn)時(shí)這一點(diǎn)很重要,并且需要對(duì)其進(jìn)行管理以避免系統(tǒng)超負(fù)荷。

注意:
一個(gè)操作隊(duì)列的默認(rèn)最大并發(fā)量是根據(jù)現(xiàn)實(shí)中的系統(tǒng)動(dòng)態(tài)確定的,會(huì)基于當(dāng)前系統(tǒng)的負(fù)載而變化。也可以為操作隊(duì)列指定最大并發(fā)數(shù)量,這種情況下只有當(dāng)操作并發(fā)達(dá)到指定當(dāng)數(shù)量時(shí)隊(duì)列才會(huì)處理。

5.2 串行操作

運(yùn)行以下代碼,表視圖中現(xiàn)實(shí)了5個(gè)初始元素,在長(zhǎng)線任務(wù)執(zhí)行時(shí)它們?nèi)匀豢梢曰瑒?dòng)。任務(wù)完成后,額外當(dāng)行就可見(jiàn)了。

#import "ICFOperationQueueSerialViewController.h"

@interface ICFOperationQueueSerialViewController ()
- (void)performLongRunningTaskForIteration:(id)iteration;
- (void)updateTableData:(id)moreData;
@end

@implementation ICFOperationQueueSerialViewController

- (void)performLongRunningTaskForIteration:(id)iteration
{
    NSNumber *iterationNumber = (NSNumber *)iteration;
    
    NSMutableArray *newArray = [[NSMutableArray alloc] initWithCapacity:10];
    
    for (int i=1; i<=10; i++)
    {
        [newArray addObject:
         [NSString stringWithFormat:@"Item %@-%d",
          iterationNumber,i]];
        
        [NSThread sleepForTimeInterval:.1];
        NSLog(@"OpQ Serial Added %@-%d",iterationNumber,i);
    }
    
    [self performSelectorOnMainThread:@selector(updateTableData:)
                           withObject:newArray
                        waitUntilDone:YES];
}

- (void)updateTableData:(id)moreData
{
    NSArray *newArray = (NSArray *)moreData;
    [self.displayItems addObject:newArray];
    [self.tableView reloadData];
}

- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.displayItems = [[NSMutableArray alloc] initWithCapacity:2];
    [self.displayItems addObject:@[@"Item Initial-1",
     @"Item Initial-2",@"Item Initial-3",
     @"Item Initial-4",@"Item Initial-5"]];
    
    self.processingQueue = [[NSOperationQueue alloc] init];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    SEL taskSelector =
    @selector(performLongRunningTaskForIteration:);

    NSMutableArray *operationsToAdd =
    [[NSMutableArray alloc] init];
    
    NSInvocationOperation *prevOperation = nil;
    for (int i=1; i<=5; i++)
    {
        
        NSNumber *iteration = [NSNumber numberWithInt:i];
        //新創(chuàng)建的操作
        NSInvocationOperation *operation =
        [[NSInvocationOperation alloc] initWithTarget:self
        selector:taskSelector object:iteration];
        
        if (prevOperation) {
            //新創(chuàng)建的操作會(huì)將一個(gè)依賴(lài)關(guān)系添加到之前的操作,直到前面的操作完成時(shí)才運(yùn)行新的操作
            [operation addDependency:prevOperation];
        }
        //新操作被添加到操作對(duì)象數(shù)組
        [operationsToAdd addObject:operation];
        
        prevOperation = operation;
    }
    //遍歷操作對(duì)象數(shù)組,將操作添加到隊(duì)列中
    for (NSInvocationOperation *operation in operationsToAdd) {
        [self.processingQueue addOperation:operation];
    }
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [self.displayItems count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[self.displayItems objectAtIndex:section] count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"ICFOperationSerialCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    
    // Configure the cell...
    NSMutableArray *itemsForRow = [self.displayItems objectAtIndex:indexPath.section];
    NSString *labelForRow = [itemsForRow objectAtIndex:indexPath.row];
    [cell.textLabel setText:labelForRow];
    
    return cell;
}

#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
}

@end

初始數(shù)據(jù)和操作隊(duì)列當(dāng)設(shè)置同 并發(fā)操作 方式一樣。要讓操作進(jìn)程按照正確當(dāng)順序串行執(zhí)行,需要對(duì)其設(shè)置依賴(lài)關(guān)系。要完成這個(gè)任務(wù),viewDidAppear:方法會(huì)添加一個(gè)數(shù)組,用于保存新創(chuàng)建當(dāng)操作,并使用NSInvocationOperation(prevOperation)跟蹤之前創(chuàng)建當(dāng)操作。

NSMutableArray *operationsToAdd =
    [[NSMutableArray alloc] init];

NSInvocationOperation *prevOperation = nil;

當(dāng)創(chuàng)建操作時(shí),該方法會(huì)跟蹤之前創(chuàng)建當(dāng)操作。新創(chuàng)建當(dāng)操作會(huì)將一個(gè)依賴(lài)關(guān)系添加到之前的操作,直到前面的操作完成時(shí)才運(yùn)行新的操作。新操作被添加到操作對(duì)象數(shù)組,然后被添加到隊(duì)列中。

當(dāng)所有當(dāng)操作都創(chuàng)建完成并將它們添加到數(shù)組后,就會(huì)將它們添加到隊(duì)列中。由于操作一旦被加到操作隊(duì)列中就會(huì)立即開(kāi)始執(zhí)行,因此應(yīng)該一次添加所有操作,這樣隊(duì)列就可以明確操作間到依賴(lài)關(guān)系了。

    //遍歷操作對(duì)象數(shù)組,將操作添加到隊(duì)列中
    for (NSInvocationOperation *operation in operationsToAdd) {
        [self.processingQueue addOperation:operation];
    }

操作隊(duì)列會(huì)對(duì)添加到操作和依賴(lài)關(guān)系進(jìn)行分析,并確定執(zhí)行它們的最優(yōu)順序。觀察調(diào)試控制臺(tái)可以看到,操作按照正確到串行順序執(zhí)行。

2017-11-30 14:18:50.109129+0800 LongRunningTasks[895:200030] OpQ Serial Added 1-1
2017-11-30 14:18:50.211273+0800 LongRunningTasks[895:200030] OpQ Serial Added 1-2
2017-11-30 14:18:50.314411+0800 LongRunningTasks[895:200030] OpQ Serial Added 1-3
2017-11-30 14:18:50.418526+0800 LongRunningTasks[895:200030] OpQ Serial Added 1-4
2017-11-30 14:18:50.523856+0800 LongRunningTasks[895:200030] OpQ Serial Added 1-5
2017-11-30 14:18:50.629020+0800 LongRunningTasks[895:200030] OpQ Serial Added 1-6
2017-11-30 14:18:50.734499+0800 LongRunningTasks[895:200030] OpQ Serial Added 1-7
2017-11-30 14:18:50.840192+0800 LongRunningTasks[895:200030] OpQ Serial Added 1-8
2017-11-30 14:18:50.944854+0800 LongRunningTasks[895:200030] OpQ Serial Added 1-9
2017-11-30 14:18:51.047135+0800 LongRunningTasks[895:200030] OpQ Serial Added 1-10
2017-11-30 14:18:51.194856+0800 LongRunningTasks[895:194777] OpQ Serial Added 2-1
2017-11-30 14:18:51.300335+0800 LongRunningTasks[895:194777] OpQ Serial Added 2-2
2017-11-30 14:18:51.405780+0800 LongRunningTasks[895:194777] OpQ Serial Added 2-3
2017-11-30 14:18:51.507019+0800 LongRunningTasks[895:194777] OpQ Serial Added 2-4
2017-11-30 14:18:51.612334+0800 LongRunningTasks[895:194777] OpQ Serial Added 2-5
2017-11-30 14:18:51.717758+0800 LongRunningTasks[895:194777] OpQ Serial Added 2-6
2017-11-30 14:18:51.823200+0800 LongRunningTasks[895:194777] OpQ Serial Added 2-7
2017-11-30 14:18:51.928654+0800 LongRunningTasks[895:194777] OpQ Serial Added 2-8
2017-11-30 14:18:52.032277+0800 LongRunningTasks[895:194777] OpQ Serial Added 2-9
2017-11-30 14:18:52.133318+0800 LongRunningTasks[895:194777] OpQ Serial Added 2-10
2017-11-30 14:18:52.244936+0800 LongRunningTasks[895:194777] OpQ Serial Added 3-1
2017-11-30 14:18:52.350393+0800 LongRunningTasks[895:194777] OpQ Serial Added 3-2
2017-11-30 14:18:52.455856+0800 LongRunningTasks[895:194777] OpQ Serial Added 3-3
2017-11-30 14:18:52.558032+0800 LongRunningTasks[895:194777] OpQ Serial Added 3-4
2017-11-30 14:18:52.662302+0800 LongRunningTasks[895:194777] OpQ Serial Added 3-5
2017-11-30 14:18:52.767775+0800 LongRunningTasks[895:194777] OpQ Serial Added 3-6
2017-11-30 14:18:52.872079+0800 LongRunningTasks[895:194777] OpQ Serial Added 3-7
2017-11-30 14:18:52.977675+0800 LongRunningTasks[895:194777] OpQ Serial Added 3-8
2017-11-30 14:18:53.082886+0800 LongRunningTasks[895:194777] OpQ Serial Added 3-9
2017-11-30 14:18:53.183973+0800 LongRunningTasks[895:194777] OpQ Serial Added 3-10
2017-11-30 14:18:53.295841+0800 LongRunningTasks[895:194777] OpQ Serial Added 4-1
2017-11-30 14:18:53.401299+0800 LongRunningTasks[895:194777] OpQ Serial Added 4-2
2017-11-30 14:18:53.504616+0800 LongRunningTasks[895:194777] OpQ Serial Added 4-3
2017-11-30 14:18:53.608967+0800 LongRunningTasks[895:194777] OpQ Serial Added 4-4
2017-11-30 14:18:53.714370+0800 LongRunningTasks[895:194777] OpQ Serial Added 4-5
2017-11-30 14:18:53.819861+0800 LongRunningTasks[895:194777] OpQ Serial Added 4-6
2017-11-30 14:18:53.921612+0800 LongRunningTasks[895:194777] OpQ Serial Added 4-7
2017-11-30 14:18:54.027221+0800 LongRunningTasks[895:194777] OpQ Serial Added 4-8
2017-11-30 14:18:54.129037+0800 LongRunningTasks[895:194777] OpQ Serial Added 4-9
2017-11-30 14:18:54.234498+0800 LongRunningTasks[895:194777] OpQ Serial Added 4-10
2017-11-30 14:18:54.346365+0800 LongRunningTasks[895:194777] OpQ Serial Added 5-1
2017-11-30 14:18:54.448219+0800 LongRunningTasks[895:194777] OpQ Serial Added 5-2
2017-11-30 14:18:54.553680+0800 LongRunningTasks[895:194777] OpQ Serial Added 5-3
2017-11-30 14:18:54.659157+0800 LongRunningTasks[895:194777] OpQ Serial Added 5-4
2017-11-30 14:18:54.764694+0800 LongRunningTasks[895:194777] OpQ Serial Added 5-5
2017-11-30 14:18:54.870224+0800 LongRunningTasks[895:194777] OpQ Serial Added 5-6
2017-11-30 14:18:54.971051+0800 LongRunningTasks[895:194777] OpQ Serial Added 5-7
2017-11-30 14:18:55.073305+0800 LongRunningTasks[895:194777] OpQ Serial Added 5-8
2017-11-30 14:18:55.177415+0800 LongRunningTasks[895:194777] OpQ Serial Added 5-9
2017-11-30 14:18:55.280122+0800 LongRunningTasks[895:194777] OpQ Serial Added 5-10

串行方式增加了完成所有任務(wù)所消耗到時(shí)間,不過(guò)它能夠成功確保任務(wù)按照正確到順序執(zhí)行。

5.3 自定義操作

運(yùn)行以下代碼,表視圖中顯示了5個(gè)初始元素,在長(zhǎng)線任務(wù)執(zhí)行時(shí)它們?nèi)匀豢梢曰瑒?dòng)。在操作完成前快速點(diǎn)擊表視圖上方都Cancel按鈕。注意,此時(shí)任務(wù)立即停止了。

#import "ICFOperationQueueCustomViewController.h"

@interface ICFOperationQueueCustomViewController ()
- (void)updateTableData:(id)moreData;
@end

@implementation ICFOperationQueueCustomViewController

- (IBAction)cancelButtonTouched:(id)sender
{
    [self.processingQueue cancelAllOperations];
}

- (void)updateTableWithData:(NSArray *)moreData
{
    [self performSelectorOnMainThread:@selector(updateTableData:) withObject:moreData waitUntilDone:YES];
}

- (void)updateTableData:(id)moreData
{
    NSArray *newArray = (NSArray *)moreData;
    [self.displayItems addObject:newArray];
    [self.tableView reloadData];    
}

- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.displayItems = [[NSMutableArray alloc] initWithCapacity:2];
    [self.displayItems addObject:@[@"Item Initial-1",@"Item Initial-2",@"Item Initial-3",@"Item Initial-4",@"Item Initial-5"]];
    
    self.processingQueue = [[NSOperationQueue alloc] init];
    [self.tableView setTableHeaderView:self.statusView];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    NSMutableArray *operationsToAdd =
    [[NSMutableArray alloc] init];
    
    ICFCustomOperation *prevOperation = nil;
    for (int i=1; i<=5; i++)
    {
        
        NSNumber *iteration = [NSNumber numberWithInt:i];
        
        ICFCustomOperation *operation =
        [[ICFCustomOperation alloc] initWithIteration:iteration
                                          andDelegate:self];
        NSLog(@"...not cancelled, execute logic here");
        if (prevOperation)
        {
            [operation addDependency:prevOperation];
        }
        
        [operationsToAdd addObject:operation];
        
        prevOperation = operation;
    }
    
    for (ICFCustomOperation *operation in operationsToAdd)
    {
        [self.processingQueue addOperation:operation];
    }
}


- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [self.displayItems count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[self.displayItems objectAtIndex:section] count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"ICFOperationCustomCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    
    // Configure the cell...
    NSMutableArray *itemsForRow = [self.displayItems objectAtIndex:indexPath.section];
    NSString *labelForRow = [itemsForRow objectAtIndex:indexPath.row];
    [cell.textLabel setText:labelForRow];
    
    return cell;
}

#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
}

@end

初始數(shù)據(jù)和操作隊(duì)列都設(shè)置幾乎同串行操作方式一樣。唯一都區(qū)別時(shí)這里用到都自定義NSOperation子類(lèi)稱(chēng)作ICFCustomOperation:
ICFCustomOperation.h

#import <Foundation/Foundation.h>

@protocol ICFCustomOperationDelegate <NSObject>

- (void)updateTableWithData:(NSArray *)moreData;

@end

@interface ICFCustomOperation : NSOperation

@property (nonatomic, weak) id<ICFCustomOperationDelegate> delegate;
@property (nonatomic, strong) NSNumber *iteration;

- (id)initWithIteration:(NSNumber *)iterationNumber andDelegate:(id)myDelegate;

@end

ICFCustomOperation.m

#import "ICFCustomOperation.h"

@implementation ICFCustomOperation

- (id)initWithIteration:(NSNumber *)iterationNumber
            andDelegate:(id)myDelegate
{
    if (self = [super init])
    {
        self.iteration = iterationNumber;
        self.delegate = myDelegate;
    }
    return self;
}

- (void)main
{
    NSMutableArray *newArray =
    [[NSMutableArray alloc] initWithCapacity:10];
    
    for (int i=1; i<=10; i++)
    {
        //在for循環(huán)開(kāi)始時(shí),檢查取消狀態(tài)
        if ([self isCancelled])
        {
            break;
        }
        
        [newArray addObject:
         [NSString stringWithFormat:@"Item %@-%d",
          self.iteration,i]];
        
        [NSThread sleepForTimeInterval:.1];
        NSLog(@"OpQ Custom Added %@-%d",self.iteration,i);
    }
    
    [self.delegate updateTableWithData:newArray];
}

@end

在for循環(huán)開(kāi)始是,檢查取消狀態(tài):

 if ([self isCancelled])
        {
            break;
        }

這個(gè)檢查可以讓操作對(duì)取消請(qǐng)求做到立即響應(yīng)。當(dāng)設(shè)計(jì)一個(gè)自定義操作時(shí),要謹(jǐn)慎考慮取消操作應(yīng)該如何執(zhí)行,以及是否需要任何回滾邏輯。
正確處理取消動(dòng)作不僅對(duì)創(chuàng)建一個(gè)自定義操作子類(lèi)有幫助,同時(shí)也是一個(gè)將復(fù)雜邏輯進(jìn)行封裝對(duì)有效方法,這樣它就可以很好地在操作隊(duì)列中運(yùn)行了。

六、在調(diào)度隊(duì)列中運(yùn)行

調(diào)度隊(duì)列由 Grand Central Dispathc 提供,用于在受控環(huán)境內(nèi)執(zhí)行一段代碼。GCD被設(shè)計(jì)為最大化實(shí)現(xiàn)并發(fā)處理,同時(shí)基于系統(tǒng)對(duì)狀態(tài)充分利用多核處理器的性能對(duì)大量動(dòng)態(tài)分布的隊(duì)列進(jìn)行管理。
GCD提供了3中類(lèi)型的隊(duì)列,分別是主隊(duì)列、并發(fā)隊(duì)列、串行隊(duì)列。主隊(duì)列是由系統(tǒng)創(chuàng)建的特殊隊(duì)列,同應(yīng)用的主線程綁定。在iOS中,可以使用一些全局并發(fā)的隊(duì)列,分為高、普通、低和后臺(tái)運(yùn)行這4個(gè)優(yōu)先級(jí)隊(duì)列。私有的并發(fā)和串行隊(duì)列可以由應(yīng)用創(chuàng)建并一定要像其他應(yīng)用資源一樣被系統(tǒng)管理。

注意:
從iOS6開(kāi)始,創(chuàng)建的調(diào)度隊(duì)列都由ARC管理,不需要對(duì)他們進(jìn)行retain和release操作。

6.1 GCD并發(fā)調(diào)度隊(duì)列

執(zhí)行以下代碼,表視圖中顯示5個(gè)初始元素,在長(zhǎng)線任務(wù)執(zhí)行時(shí)它們?nèi)匀豢梢曰瑒?dòng)。任務(wù)完成后,額外的行就可見(jiàn)了。

#import "ICFDispatchQueueConcurrentViewController.h"

@interface ICFDispatchQueueConcurrentViewController ()
- (void)performLongRunningTaskForIteration:(id)iteration;
- (void)updateTableData:(id)moreData;
@end

@implementation ICFDispatchQueueConcurrentViewController

- (void)performLongRunningTaskForIteration:(id)iteration
{
    NSNumber *iterationNumber = (NSNumber *)iteration;

    //用于跟蹤所創(chuàng)建元素的 newArray 對(duì)象需要一個(gè) _block 修飾符,這樣代碼塊才能更新它
    __block NSMutableArray *newArray =
    [[NSMutableArray alloc] initWithCapacity:10];
    
    //獲取一個(gè)對(duì)低優(yōu)先級(jí)并發(fā)調(diào)度隊(duì)列的引用
    dispatch_queue_t detailQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);

    //同步處理整個(gè)枚舉
    dispatch_apply(10, detailQueue, ^(size_t i)
    {
        [NSThread sleepForTimeInterval:.1];
        
        [newArray addObject:[NSString stringWithFormat:
                             @"Item %@-%zu",iterationNumber,i+1]];
        
        NSLog(@"DispQ Concurrent Added %@-%zu",iterationNumber,i+1);
    });
    
    dispatch_async(dispatch_get_main_queue(), ^{
        [self updateTableData:newArray];
    });
}

- (void)updateTableData:(id)moreData
{
    NSArray *newArray = (NSArray *)moreData;
    [self.displayItems addObject:newArray];
    [self.tableView reloadData];
}

- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.displayItems = [[NSMutableArray alloc] initWithCapacity:2];
    [self.displayItems addObject:@[@"Item Initial-1",@"Item Initial-2",@"Item Initial-3",@"Item Initial-4",@"Item Initial-5"]];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    dispatch_queue_t workQueue =
    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    
    for (int i=1; i<=5; i++)
    {
        
        NSNumber *iteration = [NSNumber numberWithInt:i];

        dispatch_async(workQueue, ^{
            [self performLongRunningTaskForIteration:iteration];
        });
    }
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [self.displayItems count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[self.displayItems objectAtIndex:section] count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"ICFDispatchConcurrentCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    
    // Configure the cell...
    NSMutableArray *itemsForRow = [self.displayItems objectAtIndex:indexPath.section];
    NSString *labelForRow = [itemsForRow objectAtIndex:indexPath.row];
    [cell.textLabel setText:labelForRow];
    
    return cell;
}

#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
}

@end

注意,這種方式的完成速度明顯比前面任何一種方式都要快。

要在 viewDidAppear: 方法中開(kāi)始一個(gè)長(zhǎng)線任務(wù),應(yīng)用需要獲得一個(gè)對(duì)高優(yōu)先級(jí)發(fā)調(diào)度隊(duì)列的引用:

dispatch_queue_t workQueue =
    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

使用 dispatch_get_global_queue 可以訪問(wèn)3個(gè)全局且有系統(tǒng)維護(hù)的并發(fā)調(diào)度隊(duì)列。對(duì)這些隊(duì)列的引用不需要進(jìn)行 retain 和 release 操作。當(dāng)隊(duì)列引用完成后,可以在代碼段內(nèi)編寫(xiě)任務(wù)的處理邏輯。

for (int i=1; i<=5; i++)
    {
        
        NSNumber *iteration = [NSNumber numberWithInt:i];

        dispatch_async(workQueue, ^{
            [self performLongRunningTaskForIteration:iteration];
        });
    }

使用 dispatch_async 表示邏輯是異步執(zhí)行的。如果這樣,這段代碼內(nèi)的工作將會(huì)提交給隊(duì)列,并且對(duì)其調(diào)用會(huì)立即返回,不會(huì)阻塞主線程。該代碼還可以使用 dispatch_sync 同步提交給隊(duì)列,這樣在代碼段內(nèi)對(duì)邏輯處理完成前調(diào)用線程只能在那里等待。

performLongRunningTaskForIteration: 方法中,需要強(qiáng)調(diào)與前面方式相比不同的幾點(diǎn)。
用于跟蹤所創(chuàng)建元素的 newArray 對(duì)象需要一個(gè) _block 修飾符,這樣代碼塊才能更新它。

__block NSMutableArray *newArray =
    [[NSMutableArray alloc] initWithCapacity:10];

之后該方法會(huì)得到一個(gè)對(duì)低優(yōu)先級(jí)并發(fā)調(diào)度隊(duì)列的引用

dispatch_queue_t detailQueue =
    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);

之后,低優(yōu)先級(jí)調(diào)度隊(duì)列會(huì)用在強(qiáng)大的GCD技術(shù)上,即同步處理整個(gè)枚舉。

//同步處理整個(gè)枚舉
    dispatch_apply(10, detailQueue, ^(size_t i)
    {
        [NSThread sleepForTimeInterval:.1];
        
        [newArray addObject:[NSString stringWithFormat:
                             @"Item %@-%zu",iterationNumber,i+1]];
        
        NSLog(@"DispQ Concurrent Added %@-%zu",iterationNumber,i+1);
    });

使用 dispatch_apply 所需的參數(shù)包括迭代次數(shù)、關(guān)于調(diào)度隊(duì)列的引用、用于表示迭代正在進(jìn)行的變量(一定要為 size_t 類(lèi)型)和每次迭代需要用到的邏輯代碼塊。GCD會(huì)使用可能的迭代隊(duì)列填滿,在系統(tǒng)約束的范圍內(nèi)它們執(zhí)行會(huì)盡可能同步。這一技術(shù)使得該方式在任務(wù)執(zhí)行方面比其他方式都快,如果任務(wù)的順序不重要,這種方式就非常高效。

注意:
方法通過(guò)高級(jí)別的抽象后,用在集合類(lèi)中也可以起到同樣的效果。比如,NSArray具有一個(gè)名為 enumerateObjectsWithOptions:usingBlock:的方法。這個(gè)方法可以按數(shù)組序列遍歷對(duì)象,也可以反向遍歷或并發(fā)遍歷。

迭代完成并且由新元素組成的數(shù)組創(chuàng)建好之后,dispatch_async 方法需要通知UI更新表視圖。

dispatch_async(dispatch_get_main_queue(), ^{
        [self updateTableData:newArray];
    });

dispatch_async調(diào)用使用函數(shù)dispatch_get_main_queue來(lái)訪問(wèn)主隊(duì)列。注意,該技術(shù)可以在任何地方訪問(wèn)主隊(duì)列,并且可以很容易地更新UI,通知長(zhǎng)線任務(wù)當(dāng)前的狀態(tài)。

6.2 GCD串行調(diào)度隊(duì)列

執(zhí)行以下代碼,表視圖中顯示了5個(gè)初始元素,在長(zhǎng)線任務(wù)執(zhí)行時(shí)它們?nèi)匀豢梢曰瑒?dòng)。任務(wù)完成后,額外的行就可見(jiàn)了。這種方式?jīng)]有并發(fā)調(diào)度隊(duì)列方式那么快,不過(guò)可以按照任務(wù)添加到隊(duì)列中的順序來(lái)執(zhí)行它們。

#import "ICFDispatchQueueSerialViewController.h"

@interface ICFDispatchQueueSerialViewController ()
- (void)performLongRunningTaskForIteration:(id)iteration;
- (void)updateTableData:(id)moreData;
@end

@implementation ICFDispatchQueueSerialViewController

- (void)performLongRunningTaskForIteration:(id)iteration
{
    NSNumber *iterationNumber = (NSNumber *)iteration;
    
    NSMutableArray *newArray =
    [[NSMutableArray alloc] initWithCapacity:10];
    
    for (int i=1; i<=10; i++)
    {
        [newArray addObject:[NSString stringWithFormat:
                            @"Item %@-%d",iterationNumber,i]];
        
        [NSThread sleepForTimeInterval:.1];
        NSLog(@"DispQ Serial Added %@-%d",iterationNumber,i);
    }
    
    dispatch_async(dispatch_get_main_queue(), ^{
        [self updateTableData:newArray];
    });
}

- (void)updateTableData:(id)moreData
{
    NSArray *newArray = (NSArray *)moreData;
    [self.displayItems addObject:newArray];
    [self.tableView reloadData];
}

- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.displayItems = [[NSMutableArray alloc] initWithCapacity:2];
    [self.displayItems addObject:@[@"Item Initial-1",@"Item Initial-2",@"Item Initial-3",@"Item Initial-4",@"Item Initial-5"]];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    //創(chuàng)建一個(gè)串行隊(duì)列
    dispatch_queue_t workQueue =
    dispatch_queue_create("com.icf.serialqueue", NULL);
    
    //用異步的方法向串行隊(duì)列中添加實(shí)際工作的代碼段
    for (int i=1; i<=5; i++)
    {
        
        NSNumber *iteration = [NSNumber numberWithInt:i];
        
        dispatch_async(workQueue, ^{
            [self performLongRunningTaskForIteration:iteration];
        });
    }
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [self.displayItems count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[self.displayItems objectAtIndex:section] count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"ICFDispatchSerialCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    
    // Configure the cell...
    NSMutableArray *itemsForRow = [self.displayItems objectAtIndex:indexPath.section];
    NSString *labelForRow = [itemsForRow objectAtIndex:indexPath.row];
    [cell.textLabel setText:labelForRow];
    
    return cell;
}

#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
}

@end

要在 viewDidAppear: 中開(kāi)始一個(gè)長(zhǎng)線任務(wù),應(yīng)用需要?jiǎng)?chuàng)建一個(gè)串行調(diào)度隊(duì)列:

dispatch_queue_t workQueue =
    dispatch_queue_create("com.icf.serialqueue", NULL);

可以用異步的方法向串行隊(duì)列中添加實(shí)際工作的代碼段:

for (int i=1; i<=5; i++)
    {
        
        NSNumber *iteration = [NSNumber numberWithInt:i];
        
        dispatch_async(workQueue, ^{
            [self performLongRunningTaskForIteration:iteration];
        });
    }

performLongRunningTaskForIteration: 方法執(zhí)行的任務(wù)同主線程非常類(lèi)似,可以后臺(tái)運(yùn)行和使用并發(fā)操作隊(duì)列方法;不過(guò)這個(gè)方法使用 dispatch_async 對(duì)主隊(duì)列調(diào)用 updateTableData:方法。

串行調(diào)度隊(duì)列將會(huì)按照任務(wù)添加到隊(duì)列中對(duì)順序來(lái)執(zhí)行長(zhǎng)線任務(wù):先進(jìn)先出。調(diào)試控制臺(tái)會(huì)顯示正確順序執(zhí)行對(duì)這些操作信息。

2017-11-30 15:19:49.208132+0800 LongRunningTasks[1515:214591] DispQ Serial Added 1-1
2017-11-30 15:19:49.313596+0800 LongRunningTasks[1515:214591] DispQ Serial Added 1-2
2017-11-30 15:19:49.419005+0800 LongRunningTasks[1515:214591] DispQ Serial Added 1-3
2017-11-30 15:19:49.524378+0800 LongRunningTasks[1515:214591] DispQ Serial Added 1-4
2017-11-30 15:19:49.626864+0800 LongRunningTasks[1515:214591] DispQ Serial Added 1-5
2017-11-30 15:19:49.732279+0800 LongRunningTasks[1515:214591] DispQ Serial Added 1-6
2017-11-30 15:19:49.834632+0800 LongRunningTasks[1515:214591] DispQ Serial Added 1-7
2017-11-30 15:19:49.940270+0800 LongRunningTasks[1515:214591] DispQ Serial Added 1-8
2017-11-30 15:19:50.043781+0800 LongRunningTasks[1515:214591] DispQ Serial Added 1-9
2017-11-30 15:19:50.149343+0800 LongRunningTasks[1515:214591] DispQ Serial Added 1-10
2017-11-30 15:19:50.277230+0800 LongRunningTasks[1515:214591] DispQ Serial Added 2-1
2017-11-30 15:19:50.382699+0800 LongRunningTasks[1515:214591] DispQ Serial Added 2-2
2017-11-30 15:19:50.484582+0800 LongRunningTasks[1515:214591] DispQ Serial Added 2-3
2017-11-30 15:19:50.590035+0800 LongRunningTasks[1515:214591] DispQ Serial Added 2-4
2017-11-30 15:19:50.693442+0800 LongRunningTasks[1515:214591] DispQ Serial Added 2-5
2017-11-30 15:19:50.794298+0800 LongRunningTasks[1515:214591] DispQ Serial Added 2-6
2017-11-30 15:19:50.895178+0800 LongRunningTasks[1515:214591] DispQ Serial Added 2-7
2017-11-30 15:19:50.996306+0800 LongRunningTasks[1515:214591] DispQ Serial Added 2-8
2017-11-30 15:19:51.100765+0800 LongRunningTasks[1515:214591] DispQ Serial Added 2-9
2017-11-30 15:19:51.204737+0800 LongRunningTasks[1515:214591] DispQ Serial Added 2-10
2017-11-30 15:19:51.311088+0800 LongRunningTasks[1515:214591] DispQ Serial Added 3-1
2017-11-30 15:19:51.416623+0800 LongRunningTasks[1515:214591] DispQ Serial Added 3-2
2017-11-30 15:19:51.522090+0800 LongRunningTasks[1515:214591] DispQ Serial Added 3-3
2017-11-30 15:19:51.626325+0800 LongRunningTasks[1515:214591] DispQ Serial Added 3-4
2017-11-30 15:19:51.727578+0800 LongRunningTasks[1515:214591] DispQ Serial Added 3-5
2017-11-30 15:19:51.828270+0800 LongRunningTasks[1515:214591] DispQ Serial Added 3-6
2017-11-30 15:19:51.929195+0800 LongRunningTasks[1515:214591] DispQ Serial Added 3-7
2017-11-30 15:19:52.030298+0800 LongRunningTasks[1515:214591] DispQ Serial Added 3-8
2017-11-30 15:19:52.131340+0800 LongRunningTasks[1515:214591] DispQ Serial Added 3-9
2017-11-30 15:19:52.232437+0800 LongRunningTasks[1515:214591] DispQ Serial Added 3-10
2017-11-30 15:19:52.338867+0800 LongRunningTasks[1515:214591] DispQ Serial Added 4-1
2017-11-30 15:19:52.439489+0800 LongRunningTasks[1515:214591] DispQ Serial Added 4-2
2017-11-30 15:19:52.544343+0800 LongRunningTasks[1515:214591] DispQ Serial Added 4-3
2017-11-30 15:19:52.649939+0800 LongRunningTasks[1515:214591] DispQ Serial Added 4-4
2017-11-30 15:19:52.754357+0800 LongRunningTasks[1515:214591] DispQ Serial Added 4-5
2017-11-30 15:19:52.856091+0800 LongRunningTasks[1515:214591] DispQ Serial Added 4-6
2017-11-30 15:19:52.958520+0800 LongRunningTasks[1515:214591] DispQ Serial Added 4-7
2017-11-30 15:19:53.061895+0800 LongRunningTasks[1515:214591] DispQ Serial Added 4-8
2017-11-30 15:19:53.165070+0800 LongRunningTasks[1515:214591] DispQ Serial Added 4-9
2017-11-30 15:19:53.270664+0800 LongRunningTasks[1515:214591] DispQ Serial Added 4-10
2017-11-30 15:19:53.377775+0800 LongRunningTasks[1515:214591] DispQ Serial Added 5-1
2017-11-30 15:19:53.478816+0800 LongRunningTasks[1515:214591] DispQ Serial Added 5-2
2017-11-30 15:19:53.584319+0800 LongRunningTasks[1515:214591] DispQ Serial Added 5-3
2017-11-30 15:19:53.689961+0800 LongRunningTasks[1515:214591] DispQ Serial Added 5-4
2017-11-30 15:19:53.794692+0800 LongRunningTasks[1515:214591] DispQ Serial Added 5-5
2017-11-30 15:19:53.899292+0800 LongRunningTasks[1515:214591] DispQ Serial Added 5-6
2017-11-30 15:19:54.004695+0800 LongRunningTasks[1515:214591] DispQ Serial Added 5-7
2017-11-30 15:19:54.107205+0800 LongRunningTasks[1515:214591] DispQ Serial Added 5-8
2017-11-30 15:19:54.212483+0800 LongRunningTasks[1515:214591] DispQ Serial Added 5-9
2017-11-30 15:19:54.318010+0800 LongRunningTasks[1515:214591] DispQ Serial Added 5-10

同樣,串行方法增加來(lái)完成所有任務(wù)所需的時(shí)間開(kāi)銷(xiāo),不過(guò)可以很好地確保任務(wù)按正確的順序執(zhí)行。使用調(diào)度隊(duì)列串行執(zhí)行任務(wù)要比管理有依賴(lài)關(guān)系的操作隊(duì)列容易,不過(guò)沒(méi)有提供同樣高級(jí)別的管理選項(xiàng)。

七、小結(jié)

本文介紹了幾種在不影響UI界面的前提下處理長(zhǎng)線任務(wù)的技術(shù),包括:

  • performSelectorInBackground:withObject:
  • 操作隊(duì)列
  • GCD隊(duì)列

對(duì) NSObject 使用 performSelectorInBackground:withObject: 函數(shù)讓任務(wù)在后臺(tái)執(zhí)行是最簡(jiǎn)單對(duì)方法,不過(guò)有關(guān)這個(gè)方法對(duì)支持和管理則很有限。

操作隊(duì)列可以并發(fā)或串行地處理任務(wù),這通過(guò)使用方法調(diào)用、代碼塊或自定義操作類(lèi)來(lái)實(shí)現(xiàn)。操作隊(duì)列可以指定最大并發(fā)操作數(shù),操作還可以掛起和重啟,所有尚未完成的操作都可以被取消。操作隊(duì)列可以處理自定義操作類(lèi)。操作隊(duì)列通過(guò)GCD來(lái)實(shí)現(xiàn)。

調(diào)度隊(duì)列也可以并發(fā)或串行地處理任務(wù)。一共有3個(gè)全局并發(fā)隊(duì)列,應(yīng)用還可以創(chuàng)建自己都串行隊(duì)列。調(diào)度隊(duì)列能夠接手要執(zhí)行都代碼塊,可以同步或異步執(zhí)行代碼塊。

沒(méi)有一種技術(shù)是“最好”都,因?yàn)槊糠N技術(shù)都有自身都優(yōu)勢(shì)和劣勢(shì),所以要通過(guò)充分的測(cè)試才知道哪一種技術(shù)是最適合的。

本文摘自 精通iOS框架 第2版

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容