iOS-多線程詳解

歡迎大家指出文章中需要改正或者需要補(bǔ)充的地方,我會(huì)及時(shí)更新,非常感謝。

一. 多線程基礎(chǔ)

1. 進(jìn)程

進(jìn)程是指在系統(tǒng)中正在運(yùn)行的一個(gè)應(yīng)用程序
每個(gè)進(jìn)程之間是獨(dú)立的,每個(gè)進(jìn)程均運(yùn)行在其專用且受保護(hù)的內(nèi)存空間內(nèi)

2.線程

1個(gè)進(jìn)程要想執(zhí)行任務(wù),必須得有線程(每1個(gè)進(jìn)程至少要有1條線程,稱為主線程)
一個(gè)進(jìn)程(程序)的所有任務(wù)都在線程中執(zhí)行

3. 進(jìn)程和線程的比較

1.線程是CPU調(diào)用(執(zhí)行任務(wù))的最小單位。
2.進(jìn)程是CPU分配資源的最小單位。
3.一個(gè)進(jìn)程中至少要有一個(gè)線程。
4.同一個(gè)進(jìn)程內(nèi)的線程共享進(jìn)程的資源。

4. 線程的串行

1個(gè)線程中任務(wù)的執(zhí)行是串行的
如果要在1個(gè)線程中執(zhí)行多個(gè)任務(wù),那么只能一個(gè)一個(gè)地按順序執(zhí)行這些任務(wù)
也就是說(shuō),在同一時(shí)間內(nèi),1個(gè)線程只能執(zhí)行1個(gè)任務(wù)

5. 多線程

1個(gè)進(jìn)程中可以開啟多條線程,每條線程可以并行(同時(shí))執(zhí)行不同的任務(wù)
多線程技術(shù)可以提高程序的執(zhí)行效率

6. 多線程原理

同一時(shí)間,CPU只能處理1條線程,只有1條線程在工作(執(zhí)行),多線程并發(fā)(同時(shí))執(zhí)行,其實(shí)是CPU快速地在多條線程之間調(diào)度(切換),如果CPU調(diào)度線程的時(shí)間足夠快,就造成了多線程并發(fā)執(zhí)行的假象。
那么如果線程非常非常多,會(huì)發(fā)生什么情況?
CPU會(huì)在N多線程之間調(diào)度,CPU會(huì)累死,消耗大量的CPU資源,同時(shí)每條線程被調(diào)度執(zhí)行的頻次也會(huì)會(huì)降低(線程的執(zhí)行效率降低)。
因此我們一般只開3-5條線程。

7. 多線程優(yōu)缺點(diǎn)

多線程的優(yōu)點(diǎn)
能適當(dāng)提高程序的執(zhí)行效率
能適當(dāng)提高資源利用率(CPU、內(nèi)存利用率)
多線程的缺點(diǎn)
創(chuàng)建線程是有開銷的,iOS下主要成本包括:內(nèi)核數(shù)據(jù)結(jié)構(gòu)(大約1KB)、棧空間(子線程512KB、主線程1MB,也可以使用-setStackSize:設(shè)置,但必須是4K的倍數(shù),而且最小是16K),創(chuàng)建線程大約需要90毫秒的創(chuàng)建時(shí)間
如果開啟大量的線程,會(huì)降低程序的性能,線程越多,CPU在調(diào)度線程上的開銷就越大。
程序設(shè)計(jì)更加復(fù)雜:比如線程之間的通信、多線程的數(shù)據(jù)共享等問題。

8. 多線程的應(yīng)用

主線程的主要作用
顯示\刷新UI界面
處理UI事件(比如點(diǎn)擊事件、滾動(dòng)事件、拖拽事件等)
主線程的使用注意
別將比較耗時(shí)的操作放到主線程中
耗時(shí)操作會(huì)卡住主線程,嚴(yán)重影響UI的流暢度,給用戶一種“卡”的壞體驗(yàn)
將耗時(shí)操作放在子線程中執(zhí)行,提高程序的執(zhí)行效率

二. 多線程實(shí)現(xiàn)方案

多線程實(shí)現(xiàn)的四種方案

1. pthread的簡(jiǎn)單使用(了解)

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //創(chuàng)建線程
    pthread_t thread;
    /*
     第一個(gè)參數(shù)pthread_t *restrict:線程對(duì)象
     第二個(gè)參數(shù)const pthread_attr_t *restrict:線程屬性
     第三個(gè)參數(shù)void *(*)(void *) :指向函數(shù)的指針
     第四個(gè)參數(shù)void *restrict:函數(shù)的參數(shù)
     */
    pthread_create(&thread, NULL,run ,NULL);
}
//void *(*)(void *)
void *run(void *param)
{
    for (NSInteger i =0 ; i<10000; i++) {
        NSLog(@"%zd--%@-",i,[NSThread currentThread]);
    }
    return NULL;
}

2. NSThread的使用

2.1 創(chuàng)建線程
// 方法一:創(chuàng)建線程,需要自己開啟線程
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run) object:nil];
// 開啟線程
[thread start];

// 方法二:創(chuàng)建線程后自動(dòng)啟動(dòng)線程
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];

// 方法三:隱式創(chuàng)建并啟動(dòng)線程
[self performSelectorInBackground:@selector(run) withObject:nil];

后面兩種方法都不用我們開啟線程,相對(duì)方便快捷,但是沒有辦法拿到子線程對(duì)象,沒有辦法對(duì)子線程進(jìn)行更詳細(xì)的設(shè)置,例如線程名字和優(yōu)先級(jí)等。

2.2 NSThread的屬性
 // 獲取當(dāng)前線程
 + (NSThread *)currentThread;
 // 創(chuàng)建啟動(dòng)線程
 + (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;
 // 判斷是否是多線程
 + (BOOL)isMultiThreaded;
 // 線程休眠 NSDate 休眠到什么時(shí)候
 + (void)sleepUntilDate:(NSDate *)date;
 // 線程休眠時(shí)間
 + (void)sleepForTimeInterval:(NSTimeInterval)ti;
 // 結(jié)束/退出當(dāng)前線程
 + (void)exit;
 // 獲取當(dāng)前線程優(yōu)先級(jí)
 + (double)threadPriority;
 // 設(shè)置線程優(yōu)先級(jí) 默認(rèn)為0.5 取值范圍為0.0 - 1.0 
 // 1.0優(yōu)先級(jí)最高
 // 設(shè)置優(yōu)先級(jí)
 + (BOOL)setThreadPriority:(double)p;
 // 獲取指定線程的優(yōu)先級(jí)
 - (double)threadPriority NS_AVAILABLE(10_6, 4_0);
 - (void)setThreadPriority:(double)p NS_AVAILABLE(10_6, 4_0);
 
 // 設(shè)置線程的名字
 - (void)setName:(NSString *)n NS_AVAILABLE(10_5, 2_0);
 - (NSString *)name NS_AVAILABLE(10_5, 2_0);

 // 判斷指定的線程是否是 主線程
 - (BOOL)isMainThread NS_AVAILABLE(10_5, 2_0);
 // 判斷當(dāng)前線程是否是主線程
 + (BOOL)isMainThread NS_AVAILABLE(10_5, 2_0); // reports whether current thread is main
 // 獲取主線程
 + (NSThread *)mainThread NS_AVAILABLE(10_5, 2_0);
 
 - (id)init NS_AVAILABLE(10_5, 2_0);    // designated initializer
 // 創(chuàng)建線程
 - (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument NS_AVAILABLE(10_5, 2_0);
 // 指定線程是否在執(zhí)行
 - (BOOL)isExecuting NS_AVAILABLE(10_5, 2_0);
 // 線程是否完成
 - (BOOL)isFinished NS_AVAILABLE(10_5, 2_0);
 // 線程是否被取消 (是否給當(dāng)前線程發(fā)過取消信號(hào))
 - (BOOL)isCancelled NS_AVAILABLE(10_5, 2_0);
 // 發(fā)送線程取消信號(hào)的 最終線程是否結(jié)束 由 線程本身決定
 - (void)cancel NS_AVAILABLE(10_5, 2_0);
 // 啟動(dòng)線程
 - (void)start NS_AVAILABLE(10_5, 2_0);
 
 // 線程主函數(shù)  在線程中執(zhí)行的函數(shù) 都要在-main函數(shù)中調(diào)用,自定義線程中重寫-main方法
 - (void)main NS_AVAILABLE(10_5, 2_0);    // thread body metho
2.3 NSThread線程的狀態(tài)(了解)
線程的狀態(tài)
啟動(dòng)線程
- (void)start; 
// 進(jìn)入就緒狀態(tài) -> 運(yùn)行狀態(tài)。當(dāng)線程任務(wù)執(zhí)行完畢,自動(dòng)進(jìn)入死亡狀態(tài)

阻塞(暫停)線程
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 進(jìn)入阻塞狀態(tài)

強(qiáng)制停止線程
+ (void)exit;
// 進(jìn)入死亡狀態(tài)
2.4 NSThread多線程安全隱患

多線程安全隱患的原因:1塊資源可能會(huì)被多個(gè)線程共享,也就是多個(gè)線程可能會(huì)訪問同一塊資源,比如多個(gè)線程訪問同一個(gè)對(duì)象、同一個(gè)變量、同一個(gè)文件。
那么當(dāng)多個(gè)線程訪問同一塊資源時(shí),很容易引發(fā)數(shù)據(jù)錯(cuò)亂和數(shù)據(jù)安全問題

安全隱患分析

通過上圖我們發(fā)現(xiàn),當(dāng)線程A訪問數(shù)據(jù)并對(duì)數(shù)據(jù)進(jìn)行操作的同時(shí),線程B訪問的數(shù)據(jù)還是沒有更新的數(shù)據(jù),線程B同樣對(duì)數(shù)據(jù)進(jìn)行操作,當(dāng)兩個(gè)線程結(jié)束返回時(shí),就會(huì)發(fā)生數(shù)據(jù)錯(cuò)亂的問題。
那么我們看下圖的解決方法:添加互斥鎖
安全隱患解決

我們可以看出,當(dāng)線程A訪問數(shù)據(jù)并對(duì)數(shù)據(jù)進(jìn)行操作的時(shí)候,數(shù)據(jù)被加上一把鎖,這個(gè)時(shí)候其他線程都無(wú)法訪問數(shù)據(jù),知道線程A結(jié)束返回?cái)?shù)據(jù),線程B此時(shí)在訪問數(shù)據(jù)并修改,就不會(huì)造成數(shù)據(jù)錯(cuò)亂了。
下面我們來(lái)看一下互斥鎖的使用:
互斥鎖使用格式

@synchronized(鎖對(duì)象) { 
// 需要鎖定的代碼  
}

互斥鎖的使用前提:多條線程搶奪同一塊資源時(shí)
注意:鎖定1份代碼只用1把鎖,用多把鎖是無(wú)效的
互斥鎖的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):能有效防止因多線程搶奪資源造成的數(shù)據(jù)安全問題
缺點(diǎn):需要消耗大量的CPU資源

下面通過一個(gè)售票實(shí)例來(lái)看一下線程安全的重要性

#import "ViewController.h"

@interface ViewController ()

@property(nonatomic,strong)NSThread *thread01;
@property(nonatomic,strong)NSThread *thread02;
@property(nonatomic,strong)NSThread *thread03;
@property(nonatomic,assign)NSInteger numTicket;

//@property(nonatomic,strong)NSObject *obj;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 總票數(shù)為30
    self.numTicket = 30;
    self.thread01 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.thread01.name = @"售票員01";
    self.thread02 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.thread02.name = @"售票員02";
    self.thread03 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.thread03.name = @"售票員03";
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self.thread01 start];
    [self.thread02 start];
    [self.thread03 start];
}
// 售票
-(void)saleTicket
{
    while (1) {
        // 創(chuàng)建對(duì)象
        // self.obj = [[NSObject alloc]init];
        // 鎖對(duì)象,本身就是一個(gè)對(duì)象,所以self就可以了
        // 鎖定的時(shí)候,其他線程沒有辦法訪問這段代碼
        @synchronized (self) {
            // 模擬售票時(shí)間,我們讓線程休息0.05s 
            [NSThread sleepForTimeInterval:0.05];
            if (self.numTicket > 0) {
                self.numTicket -= 1;
                NSLog(@"%@賣出了一張票,還剩下%zd張票",[NSThread currentThread].name,self.numTicket);
            }else{
                NSLog(@"票已經(jīng)賣完了");
                break;
            }
        }
    }
}

@end

當(dāng)沒有加互斥鎖的時(shí)候我們看一下輸出

沒有加互斥鎖的輸出

我們發(fā)現(xiàn)第29張,第27張都被銷售了3次,這顯然是不允許的,這就是數(shù)據(jù)錯(cuò)亂,那么當(dāng)我們加上互斥鎖時(shí),其鎖定的時(shí)候其他線程沒有辦法訪問鎖定的內(nèi)容,等其訪問完畢之后,其他線程才可以訪問,我們愛來(lái)看一下輸出


加上互斥鎖的輸出

此時(shí)就不會(huì)出現(xiàn)同一張票被多次出售的數(shù)據(jù)錯(cuò)亂的情況了。

2.5 NSThread線程之間的通信

什么叫做線程間通信
在1個(gè)進(jìn)程中,線程往往不是孤立存在的,多個(gè)線程之間需要經(jīng)常進(jìn)行通信,例如我們?cè)谧泳€程完成下載圖片后,回到主線程刷新UI顯示圖片
線程間通信的體現(xiàn)
1個(gè)線程傳遞數(shù)據(jù)給另1個(gè)線程
在1個(gè)線程中執(zhí)行完特定任務(wù)后,轉(zhuǎn)到另1個(gè)線程繼續(xù)執(zhí)行任務(wù)

線程間通信常用的方法

// 返回主線程
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
// 返回指定線程
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;

下面我們通過一個(gè)實(shí)例看一下線程之間的通信


#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end
@implementation ViewController
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [NSThread detachNewThreadSelector:@selector(donwLoadImage) toTarget:self withObject:nil];
}
-(void)donwLoadImage
{
    // 獲取圖片url地址 http://www.itunes123.com/uploadfile/2016/0421/20160421014340186.jpg
    NSURL *url = [NSURL URLWithString:@"http://www.itunes123.com/uploadfile/2016/0421/20160421014340186.jpg"];
    // 下載圖片二進(jìn)制文件
    NSData *data = [NSData dataWithContentsOfURL:url];
    // 將圖片二進(jìn)制文件轉(zhuǎn)化為image;
    UIImage *image = [UIImage imageWithData:data];
    // 參數(shù) waitUntilDone 是否等@selector(showImage:) 執(zhí)行完畢以后再執(zhí)行下面的操作  YES :等 NO:不等
    // 返回主線程顯示圖片
    // [self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:YES];
    // self.imageView 也可以直接調(diào)用這個(gè)方法 直接選擇 setImage方法,傳入?yún)?shù)image即可
    // [self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
    // 返回特定的線程,[NSThread mainThread] 獲得主線程
    [self performSelector:@selector(showImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
}
-(void)showImage:(UIImage *)image
{
    self.imageView.image = image;
}
@end

3. GCD的使用(重點(diǎn))

GCD的全稱是Grand Central Dispatch,是純C語(yǔ)言,提供了非常多強(qiáng)大的函數(shù)

GCD的優(yōu)勢(shì)
GCD是蘋果公司為多核的并行運(yùn)算提出的解決方案
GCD會(huì)自動(dòng)利用更多的CPU內(nèi)核(比如雙核、四核)
GCD會(huì)自動(dòng)管理線程的生命周期(創(chuàng)建線程、調(diào)度任務(wù)、銷毀線程)
程序員只需要告訴GCD想要執(zhí)行什么任務(wù),不需要編寫任何線程管理代碼

3.1 任務(wù)和隊(duì)列

GCD中有2個(gè)核心概念:任務(wù)和隊(duì)列
任務(wù):執(zhí)行什么操作,任務(wù)有兩種執(zhí)行方式: 同步函數(shù)異步函數(shù),他們之間的區(qū)別是
同步:只能在當(dāng)前線程中執(zhí)行任務(wù),不具備開啟新線程的能力,任務(wù)立刻馬上執(zhí)行,會(huì)阻塞當(dāng)前線程并等待 Block中的任務(wù)執(zhí)行完畢,然后當(dāng)前線程才會(huì)繼續(xù)往下運(yùn)行

異步:可以在新的線程中執(zhí)行任務(wù),具備開啟新線程的能力,但不一定會(huì)開新線程,當(dāng)前線程會(huì)直接往下執(zhí)行,不會(huì)阻塞當(dāng)前線程

隊(duì)列:用來(lái)存放任務(wù),分為串行隊(duì)列并行隊(duì)列
串行隊(duì)列(Serial Dispatch Queue)
讓任務(wù)一個(gè)接著一個(gè)地執(zhí)行(一個(gè)任務(wù)執(zhí)行完畢后,再執(zhí)行下一個(gè)任務(wù))
并發(fā)隊(duì)列(Concurrent Dispatch Queue)
可以讓多個(gè)任務(wù)并發(fā)(同時(shí))執(zhí)行(自動(dòng)開啟多個(gè)線程同時(shí)執(zhí)行任務(wù))
并發(fā)功能只有在異步(dispatch_async)函數(shù)下才有效

GCD的使用就2個(gè)步驟

  1. 定制任務(wù)
    確定想做的事情

  2. 將任務(wù)添加到隊(duì)列中
    GCD會(huì)自動(dòng)將隊(duì)列中的任務(wù)取出,放到對(duì)應(yīng)的線程中執(zhí)行
    任務(wù)的取出遵循隊(duì)列的FIFO原則:先進(jìn)先出,后進(jìn)后出

3.2 GCD的創(chuàng)建
  1. 隊(duì)列的創(chuàng)建
// 第一個(gè)參數(shù)const char *label : C語(yǔ)言字符串,用來(lái)標(biāo)識(shí)
// 第二個(gè)參數(shù)dispatch_queue_attr_t attr : 隊(duì)列的類型
// 并發(fā)隊(duì)列:DISPATCH_QUEUE_CONCURRENT
// 串行隊(duì)列:DISPATCH_QUEUE_SERIAL 或者 NULL
dispatch_queue_t queue = dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);

創(chuàng)建并發(fā)隊(duì)列

dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_CONCURRENT);

創(chuàng)建串行隊(duì)列

dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_SERIAL);

GCD默認(rèn)已經(jīng)提供了全局并發(fā)隊(duì)列,供整個(gè)應(yīng)用使用,可以無(wú)需手動(dòng)創(chuàng)建

    /** 
     第一個(gè)參數(shù):優(yōu)先級(jí) 也可直接填后面的數(shù)字
     #define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高
     #define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默認(rèn)
     #define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低
     #define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后臺(tái)
     第二個(gè)參數(shù): 預(yù)留參數(shù)  0
     */
    dispatch_queue_t quque1 =  dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

獲得主隊(duì)列

dispatch_queue_t  queue = dispatch_get_main_queue();
  1. 任務(wù)的執(zhí)行
    隊(duì)列在queue中,任務(wù)在block塊中
    開啟同步函數(shù) 同步函數(shù):要求立刻馬上開始執(zhí)行
     /*
     第一個(gè)參數(shù):隊(duì)列
     第二個(gè)參數(shù):block,在里面封裝任務(wù)
     */
    dispatch_sync(queue, ^{
        
    });

開啟異步函數(shù) 異步函數(shù) :等主線程執(zhí)行完畢之后,回過頭開線程執(zhí)行任務(wù)

    dispatch_async(queue, ^{
        
    });
  1. 任務(wù)和隊(duì)列的組合
    任務(wù):同步函數(shù) 異步函數(shù)
    隊(duì)列:串行 并行
    異步函數(shù)+并發(fā)隊(duì)列:會(huì)開啟新的線程,并發(fā)執(zhí)行
 dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
 dispatch_async(queue, ^{
       NSLog(@"---download1---%@",[NSThread currentThread]);
 });

異步函數(shù)+串行隊(duì)列:會(huì)開啟一條線程,任務(wù)串行執(zhí)行

  dispatch_queue_t queue =  dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_SERIAL);
   dispatch_async(queue, ^{
       NSLog(@"---download1---%@",[NSThread currentThread]);
 });

同步函數(shù)+并發(fā)隊(duì)列:不會(huì)開線程,任務(wù)串行執(zhí)行

   dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
   dispatch_sync(queue, ^{
       NSLog(@"---download1---%@",[NSThread currentThread]);
   });

同步函數(shù)+串行隊(duì)列:不會(huì)開線程,任務(wù)串行執(zhí)行

   dispatch_queue_t queue =  dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_SERIAL);   
   dispatch_sync(queue, ^{
       NSLog(@"---download1---%@",[NSThread currentThread]);
});

異步函數(shù)+主隊(duì)列:不會(huì)開線程,任務(wù)串行執(zhí)行
使用主隊(duì)列(跟主線程相關(guān)聯(lián)的隊(duì)列)
主隊(duì)列是GCD自帶的一種特殊的串行隊(duì)列,放在主隊(duì)列中的任務(wù),都會(huì)放到主線程中執(zhí)行

//1.獲得主隊(duì)列
dispatch_queue_t queue =  dispatch_get_main_queue();
//2.異步函數(shù)
dispatch_async(queue, ^{
        NSLog(@"---download1---%@",[NSThread currentThread]);
    });

同步函數(shù)+主隊(duì)列:死鎖

//1.獲得主隊(duì)列
dispatch_queue_t queue =  dispatch_get_main_queue();
//2.同步函數(shù)
dispatch_sync(queue, ^{
       NSLog(@"---download1---%@",[NSThread currentThread]);
});

因?yàn)檫@個(gè)方法在主線程中,給主線程中添加任務(wù),而同步函數(shù)要求立刻馬上執(zhí)行,因此就會(huì)相互等待而發(fā)生死鎖。將這個(gè)方法放入子線程中,則不會(huì)發(fā)生死鎖,任務(wù)串行執(zhí)行。
總結(jié):


任務(wù)隊(duì)列組合總結(jié)
  1. 同步函數(shù)和異步函數(shù)的執(zhí)行順序
    同步函數(shù):立刻馬上執(zhí)行,會(huì)阻塞當(dāng)前線程
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self syncConcurrent];
}
//同步函數(shù)+并發(fā)隊(duì)列:不會(huì)開線程,任務(wù)串行執(zhí)行
-(void)syncConcurrent
{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    NSLog(@"--syncConcurrent--start-");
    dispatch_sync(queue, ^{
        NSLog(@"---download1---%@",[NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"---download2---%@",[NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"---download3---%@",[NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"---download4---%@",[NSThread currentThread]);
    });
    NSLog(@"--syncConcurrent--end-");
}

我們看一下輸出

同步函數(shù)會(huì)阻塞線程,立即執(zhí)行

異步函數(shù):當(dāng)前線程會(huì)直接往下執(zhí)行,不會(huì)阻塞當(dāng)前線程

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
      [self syncConcurrent];
}
//異步函數(shù)+并發(fā)隊(duì)列:會(huì)開啟新的線程,并發(fā)執(zhí)行
-(void)asyncCONCURRENT
{
    NSLog(@"--asyncCONCURRENT--start-");
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        NSLog(@"---download1---%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"---download2---%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"---download3---%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"---download4---%@",[NSThread currentThread]);
    });
      NSLog(@"--asyncCONCURRENT--end-");
}

我們來(lái)看一下輸出


異步函數(shù)不會(huì)阻塞當(dāng)前線程

注意:GCD中開多少條線程是由系統(tǒng)根據(jù)CUP繁忙程度決定的,如果任務(wù)很多,GCD會(huì)開啟適當(dāng)?shù)淖泳€程,并不會(huì)讓所有任務(wù)同時(shí)執(zhí)行。

3.3 GCD線程間的通信

我們同樣通過一個(gè)實(shí)例來(lái)看

#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end
@implementation ViewController

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        // 獲得圖片URL
        NSURL *url = [NSURL URLWithString:@"http://upload-images.jianshu.io/upload_images/2301429-d5cc0a007447e469.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"];
        // 將圖片URL下載為二進(jìn)制文件
        NSData *data = [NSData dataWithContentsOfURL:url];
        // 將二進(jìn)制文件轉(zhuǎn)化為image
        UIImage *image = [UIImage imageWithData:data];
        NSLog(@"%@",[NSThread currentThread]);
        // 返回主線程 這里用同步函數(shù)不會(huì)發(fā)生死鎖,因?yàn)檫@個(gè)方法在子線程中被調(diào)用。
        // 也可以使用異步函數(shù)
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = image;
            NSLog(@"%@",[NSThread currentThread]);
        });
    }); 
}
@end

GCD線程間的通信非常簡(jiǎn)單,使用同步或異步函數(shù),傳入主隊(duì)列即可。

3.4 GCD其他常用函數(shù)
  1. 柵欄函數(shù)(控制任務(wù)的執(zhí)行順序)
    dispatch_barrier_async(queue, ^{
        NSLog(@"--dispatch_barrier_async-");
    });

我們來(lái)看一下柵欄函數(shù)的作用

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
     [self barrier];
}
-(void)barrier
{
    //1.創(chuàng)建隊(duì)列(并發(fā)隊(duì)列)
    dispatch_queue_t queue = dispatch_queue_create("com.xxccqueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i<3; i++) {
            NSLog(@"%zd-download1--%@",i,[NSThread currentThread]);
        }
    });
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i<3; i++) {
            NSLog(@"%zd-download2--%@",i,[NSThread currentThread]);
        }
    });
    //柵欄函數(shù)
    dispatch_barrier_async(queue, ^{
        NSLog(@"我是一個(gè)柵欄函數(shù)");
    });
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i<3; i++) {
            NSLog(@"%zd-download3--%@",i,[NSThread currentThread]);
        }
    });
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i<3; i++) {
            NSLog(@"%zd-download4--%@",i,[NSThread currentThread]);
        }
    });
}

我們來(lái)看一下輸出


柵欄函數(shù)

柵欄函數(shù)可以控制任務(wù)執(zhí)行的順序,柵欄函數(shù)之前的執(zhí)行完畢之后,執(zhí)行柵欄函數(shù),然后在執(zhí)行柵欄函數(shù)之后的

  1. 延遲執(zhí)行(延遲·控制在哪個(gè)線程執(zhí)行)
    /*
     第一個(gè)參數(shù):延遲時(shí)間
     第二個(gè)參數(shù):要執(zhí)行的代碼
     如果想讓延遲的代碼在子線程中執(zhí)行,也可以更改在哪個(gè)隊(duì)列中執(zhí)行 dispatch_get_main_queue() -> dispatch_get_global_queue(0, 0)
     */
     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"---%@",[NSThread currentThread]);
    });

延遲執(zhí)行的其他方法:

// 2s中之后調(diào)用run方法
[self performSelector:@selector(run) withObject:nil afterDelay:2.0];
// repeats:YES 是否重復(fù)
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
  1. 一次性代碼
    -(void)once
    {
        //整個(gè)程序運(yùn)行過程中只會(huì)執(zhí)行一次
        //onceToken用來(lái)記錄該部分的代碼是否被執(zhí)行過
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            NSLog(@"-----");
        });
    }

一次性代碼主要應(yīng)用在單例模式中,關(guān)于單例模式詳解大家可以去看iOS-單例模式寫一次就夠了這里不在贅述。

  1. 快速迭代(開多個(gè)線程并發(fā)完成迭代操作)
    /*
     第一個(gè)參數(shù):迭代的次數(shù)
     第二個(gè)參數(shù):在哪個(gè)隊(duì)列中執(zhí)行
     第三個(gè)參數(shù):block要執(zhí)行的任務(wù)
     */
dispatch_apply(10, queue, ^(size_t index) {
});

快速迭代:開啟多條線程,并發(fā)執(zhí)行,相比于for循環(huán)在耗時(shí)操作中極大的提高效率和速度

  1. 隊(duì)列組(同柵欄函數(shù))
    // 創(chuàng)建隊(duì)列組
    dispatch_group_t group = dispatch_group_create();
    // 創(chuàng)建并行隊(duì)列 
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    // 執(zhí)行隊(duì)列組任務(wù)
    dispatch_group_async(group, queue, ^{   
    });
    //隊(duì)列組中的任務(wù)執(zhí)行完畢之后,執(zhí)行該函數(shù)
    dispatch_group_notify(group, queue, ^{
    });

下面看一了實(shí)例使用group下載兩張圖片然后合成在一起

#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property (nonatomic, strong) UIImage  *image1; /**< 圖片1 */
@property (nonatomic, strong) UIImage  *image2; /**< 圖片2 */
@end
@implementation ViewController
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self group];
}
-(void)group
{
    //下載圖片1
    //創(chuàng)建隊(duì)列組
    dispatch_group_t group =  dispatch_group_create();
    //1.開子線程下載圖片
    //創(chuàng)建隊(duì)列(并發(fā))
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_group_async(group, queue, ^{
        //1.獲取url地址
        NSURL *url = [NSURL URLWithString:@"http://www.huabian.com/uploadfile/2015/0914/20150914014032274.jpg"];
        //2.下載圖片
        NSData *data = [NSData dataWithContentsOfURL:url];
        //3.把二進(jìn)制數(shù)據(jù)轉(zhuǎn)換成圖片
        self.image1 = [UIImage imageWithData:data];
        NSLog(@"1---%@",self.image1);
    });
    //下載圖片2
    dispatch_group_async(group, queue, ^{
        //1.獲取url地址
        NSURL *url = [NSURL URLWithString:@"http://img1.3lian.com/img2011/w12/1202/19/d/88.jpg"]; 
        //2.下載圖片
        NSData *data = [NSData dataWithContentsOfURL:url];
        //3.把二進(jìn)制數(shù)據(jù)轉(zhuǎn)換成圖片
        self.image2 = [UIImage imageWithData:data];
        NSLog(@"2---%@",self.image2);
    });
    //合成,隊(duì)列組執(zhí)行完畢之后執(zhí)行
    dispatch_group_notify(group, queue, ^{  
        //開啟圖形上下文
        UIGraphicsBeginImageContext(CGSizeMake(200, 200));
        //畫1
        [self.image1 drawInRect:CGRectMake(0, 0, 200, 100)];
        //畫2
        [self.image2 drawInRect:CGRectMake(0, 100, 200, 100)];
        //根據(jù)圖形上下文拿到圖片
        UIImage *image =  UIGraphicsGetImageFromCurrentImageContext();
        //關(guān)閉上下文
        UIGraphicsEndImageContext();
        //回到主線程刷新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = image;
            NSLog(@"%@--刷新UI",[NSThread currentThread]);
        });
    });
}

4. NSOperation的使用(重點(diǎn))

NSOperation 是蘋果公司對(duì) GCD 的封裝,完全面向?qū)ο螅⒈菺CD多了一些更簡(jiǎn)單實(shí)用的功能,所以使用起來(lái)更加方便易于理解。NSOperation 和NSOperationQueue 分別對(duì)應(yīng) GCD 的 任務(wù) 和 隊(duì)列。

NSOperation和NSOperationQueue實(shí)現(xiàn)多線程的具體步驟
1.將需要執(zhí)行的操作封裝到一個(gè)NSOperation對(duì)象中
2.將NSOperation對(duì)象添加到NSOperationQueue中
系統(tǒng)會(huì)自動(dòng)將NSOperationQueue中的NSOperation取出來(lái),并將取出的NSOperation封裝的操作放到一條新線程中執(zhí)行

4.1 NSOperation的創(chuàng)建

NSOperation是個(gè)抽象類,并不具備封裝操作的能力,必須使用它的子類
使用NSOperation子類的方式有3種

  1. NSInvocationOperation
    /*
     第一個(gè)參數(shù):目標(biāo)對(duì)象
     第二個(gè)參數(shù):選擇器,要調(diào)用的方法
     第三個(gè)參數(shù):方法要傳遞的參數(shù)
     */
NSInvocationOperation *op  = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(download) object:nil];
//啟動(dòng)操作
[op start];
  1. NSBlockOperation(最常用)
//1.封裝操作
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
       //要執(zhí)行的操作,在主線程中執(zhí)行
       NSLog(@"1------%@",[NSThread currentThread]); 
}];
//2.追加操作,追加的操作在子線程中執(zhí)行,可以追加多條操作
[op addExecutionBlock:^{
        NSLog(@"---download2--%@",[NSThread currentThread]);
    }];
[op start];
  1. 自定義子類繼承NSOperation,實(shí)現(xiàn)內(nèi)部相應(yīng)的方法
// 重寫自定義類的main方法實(shí)現(xiàn)封裝操作
-(void)main
{
    // 要執(zhí)行的操作
}
// 實(shí)例化一個(gè)自定義對(duì)象,并執(zhí)行操作
CLOperation *op = [[CLOperation alloc]init];
[op start];

自定義類封裝性高,復(fù)用性高。

4.2 NSOperationQueue的使用

NSOperation中的兩種隊(duì)列

主隊(duì)列:通過mainQueue獲得,凡是放到主隊(duì)列中的任務(wù)都將在主線程執(zhí)行
非主隊(duì)列:直接alloc init出來(lái)的隊(duì)列。非主隊(duì)列同時(shí)具備了并發(fā)和串行的功能,通過設(shè)置最大并發(fā)數(shù)屬性來(lái)控制任務(wù)是并發(fā)執(zhí)行還是串行執(zhí)行

NSOperationQueue的作用

NSOperation可以調(diào)用start方法來(lái)執(zhí)行任務(wù),但默認(rèn)是同步執(zhí)行的
如果將NSOperation添加到NSOperationQueue(操作隊(duì)列)中,系統(tǒng)會(huì)自動(dòng)異步執(zhí)行NSOperation中的操作

添加操作到NSOperationQueue中

- (void)addOperation:(NSOperation *)op;
- (void)addOperationWithBlock:(void (^)(void))block;

注意:將操作添加到NSOperationQueue中,就會(huì)自動(dòng)啟動(dòng),不需要再自己?jiǎn)?dòng)了addOperation 內(nèi)部調(diào)用 start方法
start方法 內(nèi)部調(diào)用 main方法

4.3 NSOperation和NSOperationQueue結(jié)合使用創(chuàng)建多線程
    注:這里使用NSBlockOperation示例,其他兩種方法一樣
    // 1. 創(chuàng)建非主隊(duì)列 同時(shí)具備并發(fā)和串行的功能,默認(rèn)是并發(fā)隊(duì)列
    NSOperationQueue *queue =[[NSOperationQueue alloc]init];
    //NSBlockOperation 不論封裝操作還是追加操作都是異步并發(fā)執(zhí)行
    // 2. 封裝操作
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"download1 -- %@",[NSThread currentThread]);
    }];
    // 3. 將封裝操作加入主隊(duì)列
    // 也可以不獲取封裝操作對(duì)象 直接添加操作到隊(duì)列中
    //[queue addOperationWithBlock:^{
    // 操作
    //}];
    [queue addOperation:op1];
4.4 NSOperation和NSOperationQueue的重要屬性和方法

NSOperation

  1. NSOperation的依賴 - (void)addDependency:(NSOperation *)op;
// 操作op1依賴op5,即op1必須等op5執(zhí)行完畢之后才會(huì)執(zhí)行
// 添加操作依賴,注意不能循環(huán)依賴,如果循環(huán)依賴會(huì)造成兩個(gè)任務(wù)都不會(huì)執(zhí)行
// 也可以夸隊(duì)列依賴,依賴別的隊(duì)列的操作
    [op1 addDependency:op5];
  1. NSOperation操作監(jiān)聽void (^completionBlock)(void)
// 監(jiān)聽操作的完成
// 當(dāng)op1線程完成之后,立刻就會(huì)執(zhí)行block塊中的代碼
// block中的代碼與op1不一定在一個(gè)線程中執(zhí)行,但是一定在子線程中執(zhí)行
op1.completionBlock = ^{
        NSLog(@"op1已經(jīng)完成了---%@",[NSThread currentThread]);
    };

NSOperationQueue

  1. maxConcurrentOperationCount
    //1.創(chuàng)建隊(duì)列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    /*
     默認(rèn)是并發(fā)隊(duì)列,如果最大并發(fā)數(shù)>1,并發(fā)
     如果最大并發(fā)數(shù)==1,串行隊(duì)列
     系統(tǒng)的默認(rèn)是最大并發(fā)數(shù)-1 ,表示不限制
     設(shè)置成0則不會(huì)執(zhí)行任何操作
     */
     queue.maxConcurrentOperationCount = 1;
  1. suspended
//當(dāng)值為YES的時(shí)候暫停,為NO的時(shí)候是恢復(fù)
queue.suspended = YES;
  1. -(void)cancelAllOperations;
//取消所有的任務(wù),不再執(zhí)行,不可逆
[queue cancelAllOperations];

注意:暫停和取消只能暫停或取消處于等待狀態(tài)的任務(wù),不能暫停或取消正在執(zhí)行中的任務(wù),必須等正在執(zhí)行的任務(wù)執(zhí)行完畢之后才會(huì)暫停,如果想要暫停或者取消正在執(zhí)行的任務(wù),可以在每個(gè)任務(wù)之間即每當(dāng)執(zhí)行完一段耗時(shí)操作之后,判斷是否任務(wù)是否被取消或者暫停。如果想要精確的控制,則需要將判斷代碼放在任務(wù)之中,但是不建議這么做,頻繁的判斷會(huì)消耗太多時(shí)間

4.5 NSOperation和NSOperationQueue的一些其他屬性和方法

NSOperation

// 開啟線程
- (void)start;
- (void)main;
// 判斷線程是否被取消
@property (readonly, getter=isCancelled) BOOL cancelled;
// 取消當(dāng)前線程
- (void)cancel;
//NSOperation任務(wù)是否在運(yùn)行
@property (readonly, getter=isExecuting) BOOL executing;
//NSOperation任務(wù)是否已結(jié)束
@property (readonly, getter=isFinished) BOOL finished;
// 添加依賴
- (void)addDependency:(NSOperation *)op;
// 移除依賴
- (void)removeDependency:(NSOperation *)op;
// 優(yōu)先級(jí)
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};
// 操作監(jiān)聽
@property (nullable, copy) void (^completionBlock)(void) NS_AVAILABLE(10_6, 4_0);
// 阻塞當(dāng)前線程,直到該NSOperation結(jié)束。可用于線程執(zhí)行順序的同步
- (void)waitUntilFinished NS_AVAILABLE(10_6, 4_0);
// 獲取線程的優(yōu)先級(jí)
@property double threadPriority NS_DEPRECATED(10_6, 10_10, 4_0, 8_0);
// 線程名稱
@property (nullable, copy) NSString *name NS_AVAILABLE(10_10, 8_0);
@end

NSOperationQueue

// 獲取隊(duì)列中的操作
@property (readonly, copy) NSArray<__kindof NSOperation *> *operations;
// 隊(duì)列中的操作數(shù)
@property (readonly) NSUInteger operationCount NS_AVAILABLE(10_6, 4_0);
// 最大并發(fā)數(shù),同一時(shí)間最多只能執(zhí)行三個(gè)操作
@property NSInteger maxConcurrentOperationCount;
// 暫停 YES:暫停 NO:繼續(xù)
@property (getter=isSuspended) BOOL suspended;
// 取消所有操作
- (void)cancelAllOperations;
// 阻塞當(dāng)前線程直到此隊(duì)列中的所有任務(wù)執(zhí)行完畢
- (void)waitUntilAllOperationsAreFinished;
4.6 NSOperation線程之間的通信

NSOperation線程之間的通信方法

// 回到主線程刷新UI
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
      self.imageView.image = image;
}];

我們同樣使用下載多張圖片合成綜合案例

#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property(nonatomic,strong)UIImage *image1;
@property(nonatomic,strong)UIImage *image2;
@end
@implementation ViewController

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // 創(chuàng)建非住隊(duì)列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    // 下載第一張圖片
    NSBlockOperation *download1 = [NSBlockOperation blockOperationWithBlock:^{
        NSURL *url = [NSURL URLWithString:@"http://img2.3lian.com/2014/c7/12/d/77.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        self.image1 = [UIImage imageWithData:data];
    }];
    // 下載第二張圖片
    NSBlockOperation *download2 = [NSBlockOperation blockOperationWithBlock:^{
        NSURL *url = [NSURL URLWithString:@"http://img2.3lian.com/2014/c7/12/d/77.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        self.image2 = [UIImage imageWithData:data];
    }];
    // 合成操作
    NSBlockOperation *combie = [NSBlockOperation blockOperationWithBlock:^{
        // 開啟圖形上下文
        UIGraphicsBeginImageContext(CGSizeMake(375, 667));
        // 繪制圖片1
        [self.image1 drawInRect:CGRectMake(0, 0, 375, 333)];
        // 繪制圖片2
        [self.image2 drawInRect:CGRectMake(0, 334, 375, 333)];
        // 獲取合成圖片
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        // 關(guān)閉圖形上下文
        UIGraphicsEndImageContext();
        // 回到主線程刷新UI
        [[NSOperationQueue mainQueue]addOperationWithBlock:^{
            self.imageView.image = image;
        }];
    }];
    // 添加依賴,合成圖片需要等圖片1,圖片2都下載完畢之后合成
    [combie addDependency:download1];
    [combie addDependency:download2];
    // 添加操作到隊(duì)列
    [queue addOperation:download1];
    [queue addOperation:download2];
    [queue addOperation:combie];
}
@end

注意:子線程執(zhí)行完操作之后就會(huì)立即釋放,即使我們使用強(qiáng)引用引用子線程使子線程不被釋放,也不能給子線程再次添加操作,或者再次開啟。
?建議收藏,用到的時(shí)候一查就明白了。
?本文借鑒了很多前輩的文章,如果有不對(duì)的地方請(qǐng)指正,歡迎大家一起交流學(xué)習(xí) xx_cc 。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • NSThread 第一種:通過NSThread的對(duì)象方法 NSThread *thread = [[NSThrea...
    攻城獅GG閱讀 853評(píng)論 0 3
  • 在這篇文章中,我將為你整理一下 iOS 開發(fā)中幾種多線程方案,以及其使用方法和注意事項(xiàng)。當(dāng)然也會(huì)給出幾種多線程的案...
    張戰(zhàn)威ican閱讀 615評(píng)論 0 0
  • 有時(shí)候真的不知道自己走了什么狗屎運(yùn) 遇到了一個(gè)這樣的你呀 人生中唯一一件情侶裝 是和你在一起 嘿萬(wàn)圣節(jié)快樂 不給糖...
    南無(wú)是個(gè)好姑娘_閱讀 692評(píng)論 0 0
  • 那日,小雨淅淅,微風(fēng)輕蕩,如夢(mèng)一場(chǎng)。 我檐下躲雨,望進(jìn)一雙深邃眼眸。 心中泛起漣漪,想與他相識(shí)。 他神色淡漠,眸中...
    狐蝠閱讀 400評(píng)論 0 1
  • 不知道要寫什么,只是有些有感而發(fā)。在這個(gè)多云的天氣,我這個(gè)不想長(zhǎng)大的老孩子,突然希望有個(gè)關(guān)心我、愛我的人,來(lái)狠狠地...
    清清小妖閱讀 192評(píng)論 0 0