GCD

关于GCD,我想我前面的有些博客已经介绍的很清楚了。我们就来谈一下它单纯的入门级别的应用。
 

Grand Central Dispatch,或者简称 GCD,是一个与 Block Object 产生工作的低级的 C API。GCD 真正的用途是将任务分配到多个核心又不让程序员担心哪个内核执行哪个任务。 在 Max OS X 上,多内核设备,包括笔记本,用户已经使用了相当长的时间。通过多核设备 比如 iPad2 的介绍,程序员能为 iOS 写出神奇的多核多线程 APP。

GCD 的核心是分派队列。不论在 iOS 还是 Max OS X 分派队列,正如我们快看到的是 由位于主操作系统的 GCD 来管理的线程池。你不会直接与线程有工作关系。你只在分派队 列上工作,将任务分派到这个队列上并要求队列来调用你的任务。GCD 为运行任务提供了 几个选择:同步执行、异步执行和延迟执行等。

 
GCD+Block 可以是一对好兄弟啊!配合起来使用真的太犀利!
GCD和Block都是苹果后来推崇的方式。GCD,自我感觉主要封装了多线程编程的复杂性!而且,使用方便。
Block 主要就是语法类似C的一种块语言。对某一个功能控制有了更好的封装,比如可以调用对象内变量,无需再传参;可以在实现方法中直接写实现后的代码,而不用再进行委托跳转,使代码更加简洁。等等,好处多多吧,自己体会!
 
那就开始吧!
先看一个例子:
 

// 主线程队列,由系统自动创建并且与应用撑血的主线程相关联。

    dispatch_queue_t mainQueue = dispatch_get_main_queue();

    

    // 要在主线程队列中,执行的Block

    dispatch_async(mainQueue, ^(void) {

        [[[UIAlertView alloc] initWithTitle:@"GCD"

                                    message:@"GCD is amazing!"

                                   delegate:nil cancelButtonTitle:@"OK"

                          otherButtonTitles:nil, nil] show];

    });

 
dispatch_get_main_queue()
 
用来得到主队列。主队列又系统自动创建并且与应用程序主线程相关联。
也就是我们常说的能调用主线程修改UI的队列,即:主线程队列。
 
dispatch_async 方法
 

 void dispatch_async(dispatch_queue_t queue,dispatch_block_t block);

queue:队列
block:代码Block
 
根据指定的队列执行相应的Block。
 

dispatch_async_f 方法

 

void dispatch_async_f(dispatch_queue_t queue,void *context,dispatch_function_t work);

 

queue:指定执行该work的队列

void *context:所使用的 application-defined(应用程序范围内有效的,也就是全局的)级别的参数。这是个C语法,void * 是一个无类型指针。也就是说,用它可以指向任何内存数据。

 

work:在指定队列(queue 参数)中要执行的方法。在该方法中,第一个参数所指代的数据,也就是dispatch_async_f方法所使用的第二个参数(void *context)所指带的数据。

 
例子:
 

// 定义结构体

typedef struct{

    char *title;

    char *message;

    char *cancelButtonTitle;

} AlertViewData;

// 定义dispatch_function_t 所执行的方法

void displayAlertView(void *paramContext){

    AlertViewData *alertData = (AlertViewData *)paramContext;

    

    NSString *title =[NSString stringWithUTF8String:alertData->title];

    NSString *message =[NSString stringWithUTF8String:alertData->message];

    NSString *cancelButtonTitle =[NSString stringWithUTF8String:alertData->cancelButtonTitle];

    

    [[[UIAlertView alloc] initWithTitle:title message:message

                               delegate:nil

                      cancelButtonTitle:cancelButtonTitle otherButtonTitles:nil,nil] show];

    

    

    // 释放结构体所占用的内存

    free(alertData);

    

}

// **执行dispatch_async_f

    

    dispatch_queue_t mainQueue = dispatch_get_main_queue();

    

    // 为结构体分配内存

    AlertViewData *context = (AlertViewData *) malloc(sizeof(AlertViewData));

    

    // 初始化结构体

    if (context != NULL){

        context->title = "GCD";

        context->message = "GCD is amazing.";

        context->cancelButtonTitle = "OK";

        

        // GCD执行异步方法:指定主队列(mainQueue),传递结构体数据(context)来执行displayAlertView方法。

        dispatch_async_f(mainQueue,(void *)context, displayAlertView);

    }

 
有了上面的说明铺垫,我想很快就能明白其中的含义了!
 

dispatch_async 方法

 

 void dispatch_async(dispatch_queue_t queue,dispatch_block_t block);

 
在指定的队列上,执行响应的Block。
 
例子:

dispatch_queue_t mainQueue1 = dispatch_get_main_queue();

    dispatch_async(mainQueue1, ^(void) {

        NSLog(@"Current thread = %@", [NSThread currentThread]);

        NSLog(@"Main thread = %@", [NSThread mainThread]);

    });

输出:

2013-03-27 15:45:54.501 DemoVideo[21802:707] Current thread = {name = (null), num = 1}

2013-03-27 15:45:54.503 DemoVideo[21802:707] Main thread = {name = (null), num = 1}

可见,dispatch_get_main_queue() 队列所执行的Block就是在主线程上执行的。

 
用GCD 同步执行Non-UI-Related 任务
 
同步:

例如,你想下载一个图片并想在下载完成之后展现给用 户。下载过程却和 UI 没有任何关系。对于任何与 UI 无关的任务,你可以使用 GCD 中的全 局并发队列。它们允许同步和异步执行。如果你同步提交一个任务到一个并发队列,同时提交另一个同步任务到另一个并发队列;相对而言这两个同步任务将异步运行,因为他们运行在两个不同的并发队 列上。你想确定在 B 任务开始之前 A 任务完 成了。那么必须把它们同时提交一个相同的队列。

dispatch_get_global_queue 方法

 

dispatch_queue_t dispatch_get_global_queue(long priority,unsigned long flags);

priority:优先级

flags:暂时还没有用到,为0.

获得全局队列,根据优先级。
 
优先级参数取值:
 

#define DISPATCH_QUEUE_PRIORITY_HIGH 2

#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0

#define DISPATCH_QUEUE_PRIORITY_LOW (-2)

#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN

 

dispatch_sync 方法

 

void dispatch_sync(dispatch_queue_t queue,dispatch_block_t block);

 

queue:指定的队列

block:执行的代码Block

在指定的队列中同步执行Block。同步是指,当循环执行某个Block时,当第一个Block执行完成以后,再执行下一个Block。
 
例子:

// 得到默认优先级队列

    dispatch_queue_t concurrentQueue =dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    dispatch_sync(concurrentQueue, printFrom1To1000);// 同步执行

    dispatch_sync(concurrentQueue, printFrom1To1000);// 同步执行

// 同步执行的Block

void (^printFrom1To1000)(void) = ^{

    NSUInteger counter = 0;

    for (counter = 1;counter <= 1000;counter++){

        NSLog(@"Counter = %lu - Thread = %@",(unsigned long)counter, [NSThreadcurrentThread]);

    }

};

 
 
用GCD 异步执行Non-UI-Related 任务
 
异步:

在主队列、串行队列和并发队列上异步执行代码块才能见识到 GCD 的真正实力。你将会完全相信 GCD 是多线程应用的未来,并将完全取代 现代应用中的线程。 

我们换一种方式讲解异步。前面代码也稍微提了一些关于异步。
 
一般我们异步执行,需要用的两个方法为:
 
dispatch_async 方法
dispatch_async_f 方法
 
我们来一个综合的例子:
 

- (void) viewDidAppear:(BOOL)paramAnimated{

    dispatch_queue_t concurrentQueue =dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    dispatch_async(concurrentQueue, ^{

        __block UIImage *image = nil;

        dispatch_sync(concurrentQueue, ^{

            // 同步下载图片

            NSString *urlAsString =@"http://images.apple.com/mobileme/features/images/ipad_findyouripad_20100518.jpg";

            NSURL *url = [NSURL URLWithString:urlAsString];

            NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];

            NSError *downloadError = nil;

            NSData *imageData = [NSURLConnectionsendSynchronousRequest:urlRequest

                                                      returningResponse:nil

                                                                 error:&downloadError];

            if (downloadError == nil && imageData != nil){

                image = [UIImage imageWithData:imageData]; 

            }else if (downloadError != nil){

                NSLog(@"Error happened = %@", downloadError);

            } else {

                NSLog(@"No data could get downloaded from the URL."); }

        });

        

        dispatch_sync(dispatch_get_main_queue(), ^{

            // 直到下载图片完成,再调用主线程,更新UI

            if (image != nil){

                UIImageView *imageView = [[UIImageView alloc]initWithFrame:self.view.bounds];

                [imageView setImage:image];

                [imageView setContentMode:UIViewContentModeScaleAspectFit];

                [self.view addSubview:imageView];

            } else {

                NSLog(@"Image isn't downloaded. Nothing to display.");

            }

        });

    });

}

 
当页面显示时,用异步方式去下载图片,下载图片过程将同步执行,先下载图片数据,然后再调用主线程队列来更新UI。
 
用GCD延迟执行任务
 
如果,我们需要某个方法在某一段时间后,执行,那么我们常常会调用这样的方法:
 

- (void)viewDidLoad{

    [super viewDidLoad];

    

    [self performSelector:@selector(printString:)

               withObject:@"Grand Central Dispatch"

               afterDelay:3.0];

    

}

- (void) printString:(NSString *)paramString{

    NSLog(@"%@", paramString);

}

 
但是,我们要讲的时GCD方式。那么再GCD的世界里,也有类似的2个方法,来实现延迟执行的功能。
 
dispatch_after 方法
 

void dispatch_after(dispatch_time_t when,dispatch_queue_t queue,dispatch_block_t block);

 

when:所指定的时间

queue:指定的队列

block:执行的Block

 
将Block加入指定队列,并按照指定时间执行。
 
先上例子:

- (void)viewDidLoad{

    [super viewDidLoad];

    

    double delayInSeconds = 2.0;

    

    // 创建延期的时间 2S,因为dispatch_time使用的时间是纳秒,尼玛,比毫秒还小,太夸张了!!!

    dispatch_time_t delayInNanoSeconds =dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);

    // 得到全局队列

    dispatch_queue_t concurrentQueue =dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    // 延期执行

    dispatch_after(delayInNanoSeconds, concurrentQueue, ^(void){

        NSLog(@"Output GCD !");

        

    });

    

}

 

dispatch_time 方法

 

dispatch_time_t dispatch_time(dispatch_time_t when,int64_t delta);

when:指定的开始点,可以用 DISPATCH_TIME_NOW 来指定一个当前的时间点

delta:纳秒数 

 
创建一个时间点(dispatch_time_t),返回的时间点为:delta+ when
 
 
dispatch_time_t:纯粹就是一个时间点。
 

dispatch_after 方法

void dispatch_after(dispatch_time_t when,dispatch_queue_t queue,dispatch_block_t block);

when:时间点

queue:指定的队列

block:执行的Block

 
将Block加入到指定的队列,并且在指定的时间点执行。
 

dispatch_after_f 方法

 
我想有了,前面知识的铺垫,这个方法理解起来,就像在切菜!!!
 
直接上例子:
 

- (void)viewDidLoad{

    [super viewDidLoad];

    

    double delayInSeconds = 2.0;

    dispatch_time_t delayInNanoSeconds =dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);

    dispatch_queue_t concurrentQueue =dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    

    dispatch_after_f(delayInNanoSeconds,concurrentQueue, @"GCD",processSomething);

}

void processSomething(void *paramContext){

    

    NSLog(@"This is %@",paramContext);

}

 
注意:dispatch_after_f 执行的是一个纯C函数(processSomething)!
 
使用GCD来实现单例模式
 
在 APP 的生命周期内你想确保每段代码只执行一次,即使它在代码的不同地方多次调用(比如单例的初始化)。 
 
直接代码:
 

static dispatch_once_t onceToken;

void (^executedOnlyOnce)(void) = ^{

    static NSUInteger numberOfEntries = 0;

    numberOfEntries++;

    NSLog(@"Executed %lu time(s)", (unsigned long)numberOfEntries);

};

 
调用:

dispatch_queue_t concurrentQueue =dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    

    dispatch_once(&onceToken, ^{

        dispatch_async(concurrentQueue,executedOnlyOnce);

    });

    

    dispatch_once(&onceToken, ^{

        dispatch_async(concurrentQueue,executedOnlyOnce);

    });

 
可以发现,输出只有一行,虽然我们调用了2次。因为我们传入的dispatch_once_t是相同的。编译器只执行一次操作。
 

dispatch_once 方法

 

void dispatch_once(dispatch_once_t *predicate,dispatch_block_t block);

predicate:单例标识符

block:执行的Block 

 
在Application周期内,只执行一次Block。即:单例模式
 
dispatch_once_t:可以理解为单例标识符,dispatch_once方法用它来测试Block是否被执行过了。如果执行过了,那么就不在执行。
 
用 GCD 将任务分组    
 
有时候,我们可能执行一系列的任务。由于彼此之间的依赖关系。比如有3个任务:A、B、C;我们必须执行了任务A,才能执行任务B,最后执行任务C。这样的话,我们可以用GCD的分组机制来将多个任务来按照预定的顺序来执行。
 
 
先看例子吧:
 
 
 
我们先来搞清楚以下的关于调度组的一些常用方法
 

dispatch_group_t 方法

typedef struct dispatch_group_s *dispatch_group_t;

指代一个调度组。
调度组负责监视加入到该组的多个Block。每个Block可以是异步或者同步(取决与你自己)。
调度组中的Block可以在不同的队列中执行;并且一个Block可以被添加到多个调度组中。
 

dispatch_group_create 方法

dispatch_group_t dispatch_group_create(void);

创建一个调度组

 

dispatch_group_async 方法

void dispatch_group_async(dispatch_group_t group,dispatch_queue_t queue,dispatch_block_t block);

group:要分发到的调度组

queue:执行Block的队列

block:要执行的Block

将Block分发到指定的队列(用指定的队列来执行Block),并且将该Block加入到指定的调度组中。

dispatch_group_notify 方法

void dispatch_group_notify(dispatch_group_t group,dispatch_queue_t queue,dispatch_block_t block);

当调度组中的所有Block被执行完成后,将调用被分配到指定队列的Block。

dispatch_release 方法

void dispatch_release(dispatch_object_t object);

object:调度对象

销毁指定的调度组。

 
理论一大堆了,还是用事实来说话:
 
例子:
 

- (void)viewDidAppear:(BOOL)animated{

    

    [super viewDidAppear:animated];

    

    dispatch_group_t taskGroup = dispatch_group_create();// 创建一个调度组

    dispatch_queue_t mainQueue = dispatch_get_main_queue();// 创建队列

    

    // 任务1

    // 将Block添加到指定的调度组(taskGroup)中,并且该Block用指定的队列(mainQueue)执行。

    dispatch_group_async(taskGroup, mainQueue, ^{

        

        [self reloadTableView];

    });

    

    // 任务2

    // 将Block添加到指定的调度组(taskGroup)中,并且该Block用指定的队列(mainQueue)执行。

    dispatch_group_async(taskGroup, mainQueue, ^{

        

        [self reloadScrollView];

    });

    

    // 任务3

    // 将Block添加到指定的调度组(taskGroup)中,并且该Block用指定的队列(mainQueue)执行。

    dispatch_group_async(taskGroup, mainQueue, ^{

        

        [self reloadImageView];

    });

    

    // 当指定调度组(taskGroup)中的所有Block都执行完成后,将执行给定的Block,用指定的队列(mainQueue)。

    dispatch_group_notify(taskGroup, mainQueue, ^{

        // 指定的Block

        [[[UIAlertView alloc] initWithTitle:@"Finished"

                                    message:@"All tasks are finished"delegate:nil

                          cancelButtonTitle:@"OK" otherButtonTitles:nil, nil]show];

        

    });

    

    // 最后,必须release 掉调度组(taskGroup)

    dispatch_release(taskGroup);

 

#pragma mark - 执行的多个方法

- (void) reloadTableView{

    

    NSLog(@"%s", __FUNCTION__);

}

- (void) reloadScrollView{

    NSLog(@"%s", __FUNCTION__);

}

- (void) reloadImageView{

    NSLog(@"%s", __FUNCTION__);

    

}

 
执行结果:
 

2013-03-28 23:00:57.394 DemoVideo[25096:707] -[MoreViewController reloadTableView]

2013-03-28 23:00:57.395 DemoVideo[25096:707] -[MoreViewController reloadScrollView]

2013-03-28 23:00:57.396 DemoVideo[25096:707] -[MoreViewController reloadImageView]

 
可见,执行顺序,跟加入到调度组中的次序是一样的。
 
等等,有点疑惑是不是?!
我对每个任务用的是方法dispatch_group_async,是异步的啊。为什么顺序却不变呢?!
呵呵,基础不扎实啊!因为他们都是分配给同一个队列dispatch_get_main_queue() 中的!在一个队列中是串行的啊。所以,还是按照顺序来。
 
对,在GCD中,每个方法往往都有两种,一种是执行标准Block,一种是执行C函数的。
 
那么,就ok,我们来看看执行C函数的写法:
 
例子:
 

- (void)viewDidAppear:(BOOL)animated{

    

    [super viewDidAppear:animated];

    

    dispatch_group_t taskGroup = dispatch_group_create();

    dispatch_queue_t mainQueue = dispatch_get_main_queue();

    dispatch_group_async_f(taskGroup, mainQueue,(void *)self,reloadAllComponents);

    

    dispatch_group_notify(taskGroup, mainQueue, ^{

        

        [[[UIAlertView alloc] initWithTitle:@"Finished"

                                    message:@"All Tasks are Finished"

                                   delegate:nil

                          cancelButtonTitle:@"OK" otherButtonTitles:nil, nil] show];

        });

    

    dispatch_release(taskGroup);

        

}

 

// 定义调度组要执行的C函数

void reloadAllComponents(void *context){

    MoreViewController *self =(MoreViewController *)context;

    [self reloadTableView];

    [self reloadScrollView];

    [self reloadImageView];

}

 
注意:
因为dispatch_group_async_f不像dispatch_group_t那样使用Block,可以访问当前类的变量。
 
由于 dispatch_group_async_f 接受 C 函数作为一个代码块来执行,所以,我们要执行reloadTableView方法,reloadScrollView方法,reloadImageView方法,必须有个当前类的引用!!!!!
那么 C 函数必须有一 个引用到 Self,这个 Self 能够调用当前对象的实例方法 
 
用 GCD 创建自己的分发队列   
 
当然,GCD也给予了我们很多自主性质的操作。就是可以定义我们自己的分发队列。
 
有时候,突发奇想,自己定义一个队列,做自己的事情!那该多自由!这一定可以有的,哟,亲!!
爽啊!!!
 
注意:我们自定义的队列,往往不会在主队列中执行。而是单独分配一个线程来维护我们所定义的队列。
 
先来看代码吧:
 

- (void)viewDidAppear:(BOOL)animated{

    

    [super viewDidAppear:animated];

    

    // 创建指定的自定义的串行队列

    dispatch_queue_t firstSerialQueue =dispatch_queue_create("com.pixolity.GCD.serialQueue1", NULL);

    

    // 让队列异步执行Block

    dispatch_async(firstSerialQueue, ^{

        NSUInteger counter = 0;

        for (counter = 0; counter < 5; counter++){

            NSLog(@"First iteration, counter = %lu", (unsigned long)counter); }

            NSLog(@"Current thread = %@", [NSThread currentThread]);

    });

    

    dispatch_async(firstSerialQueue, ^{

        NSUInteger counter = 0; for (counter = 0;counter < 5;counter++){

            NSLog(@"Second iteration, counter = %lu", (unsigned long)counter);

            NSLog(@"Current thread = %@", [NSThread currentThread]);

        }

    });

    

    dispatch_async(firstSerialQueue, ^{ NSUInteger counter = 0;

        for (counter = 0;counter < 5;counter++){

            NSLog(@"Third iteration, counter = %lu", (unsigned long)counter);

            NSLog(@"Current thread = %@", [NSThread currentThread]);

        }

    });

    

    // 销毁队列

    dispatch_release(firstSerialQueue);

    

    // 输出主队列,比较会发现,我们自定义的队列,并不在主线程上,效率还是蛮高的。

    dispatch_queue_t mainQueue1 = dispatch_get_main_queue();

    dispatch_async(mainQueue1, ^(void) {

        NSLog(@"Main thread = %@", [NSThread mainThread]);

    });

        

}

 
代码,不解释!
 
对,上面是Block形式的。还有一个C函数的自定义吧!
 
贴,贴更健康!
 

// 定义任务1-C函数形式

void firstIteration(void *paramContext){

    NSUInteger counter = 0;

    for (counter = 0;counter < 5;counter++){

        NSLog(@"First iteration, counter = %lu", (unsigned long)counter);

    }

}

// 定义任务2-C函数形式

void secondIteration(void *paramContext){

    NSUInteger counter = 0;

    for (counter = 0;counter < 5;counter++){

        NSLog(@"Second iteration, counter = %lu", (unsigned long)counter);

    }

}

// 定义任务3-C函数形式

void thirdIteration(void *paramContext){

    NSUInteger counter = 0;

    for (counter = 0;counter < 5;counter++){

        NSLog(@"Third iteration, counter = %lu", (unsigned long)counter);

    }

}

 

- (void)viewDidAppear:(BOOL)animated{

    

    [super viewDidAppear:animated];

    

    dispatch_queue_t firstSerialQueue =dispatch_queue_create("com.pixolity.GCD.serialQueue1", 0);

    dispatch_async_f(firstSerialQueue, NULL, firstIteration);

    dispatch_async_f(firstSerialQueue, NULL, secondIteration);

    dispatch_async_f(firstSerialQueue, NULL, thirdIteration);

    dispatch_release(firstSerialQueue);

        

}

 
用 GCD 进行信号量的管理操作
 

dispatch_semaphore_create 方法

dispatch_semaphore_t dispatch_semaphore_create(long value);

创建一个信号量对象

value:信号量的值,必须大于等于0。

什么是信号量?

我想举个例子你就会明白了。古时候执行某种命令需要令牌。创建一个信号量,就是创建一个命令。

dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);

也就是说,一项任务semaphore,有3个令牌,就是说,最多可以同时分派3名不同的将领去执行。

dispatch_semaphore_wait

Waits for (decrements) a semaphore.

long dispatch_semaphore_wait(dispatch_semaphore_t dsema,dispatch_time_t timeout);

递减信号量。

dsema:信号量

 

timeout:等待信号量的策略

返回0,说明递减信号量成功。

也就是说,拿走一个令牌去执行某个任务。这时候,令牌数量会减去1。当令牌用完时,使用FIFO的原则进行等待。

dispatch_semaphore_signal

long dispatch_semaphore_signal(dispatch_semaphore_t dsema);

增加信号量

也就是说,某一个将领执行任务回来。并且将令牌上交。这样处于等待中的将领可以得到令牌并执行相应任务

 
例子:
使用信号量实现锁操作。
 

//主线程中

    TestObj *obj = [[TestObj alloc] init];

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

    

    //线程1

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        long thecount= dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

        NSLog(@"%ld",thecount);

        [obj method1];

        sleep(10);

        dispatch_semaphore_signal(semaphore);

    });

    

    //线程2

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        sleep(1);

        long thecount= dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

        NSLog(@"----%ld",thecount);

        [obj method2];

        dispatch_semaphore_signal(semaphore);

    });

 
希望对你有所帮助!
原文地址:https://www.cnblogs.com/liyufeng2013/p/3979954.html