多線程實現之GCD、NSThred、NSOperation

在iOS中實現多線程技術的方法:pthread、NSThread、GCD、NSOperation

多線程的實質就是開闢新的線程、添加隊列、在隊列中添加同步任務或者異步任務

一、pthread是一套通用的C語言的多線程API,適用於Unix,Linux,Windows等系統,可跨平臺,使用難度大,幾乎不用

二、NSThread:

是OC的線程對象,一個NSThread對象就是一條線程;

    1、創建線程的方式:

           1>  先創建在啓動線程 

    //一個線程對應一個runloop,創建一個線程會自動開闢一個runloop
    NSThread * runloopTherd = [[NSThread alloc]initWithTarget:self selector:@selector(runLoop) object:nil];
    //事情做完後纔會死
    [runloopTherd start];

         2>  直接創建並啓動線程

    //直接創建並啓動線程
    [NSThread detachNewThreadSelector:@selector(runLoop) toTarget:self withObject:nil];

            3>  隱式創建 直接創建並啓動

//直接創建並開啓線程
[NSThread performSelectorInBackground:@selector(runThread) withObject:@"my"];

   2、線程之間的通信:當線程A傳遞數據給線程B,在線程B中完成特定的任務之後,再轉到A繼續

例如:圖片下載並顯示

當觸摸屏幕的時候
    //獲取圖片的url
    NSURL * url  = [NSURL URLWithString:@""];
    //由於下載圖片是一個耗時的操作,需要開闢一條線程,object用來傳遞數據
    NSThread * threadImage = [[NSThread alloc]initWithTarget:self selector:@selector(downLoadImage:) object:url];
    [threadImage start];

-(void)downLoadImage:(NSURL *)urlstr
{
    //下載圖片
    NSData * imageData = [NSData dataWithContentsOfURL:urlstr];
    //生成圖片
    UIImage * downImage = [UIImage imageWithData:imageData];
    //返回主線程賦值圖片
    [self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:downImage waitUntilDone:YES];
}

三、GCD實現多線程

GCD:Grand  Central Dispatc,強大的中央調度器,是蘋果公司爲多核的並行原酸提出的解決方案,會自動根據CPU內核來開啓線程執行任務,GCD會自動管理線程的生命週期,創建線程、任務調度、線程銷燬,不需要我們自己手動管理內存

1、基本術語

任務:block 需要執行的操作,下載還是播放等

隊列:Queue 用來放任務的,任務取出的時候應該是先進先出,因此放在隊列中,包括併發隊列和串行隊列

同步:當前線程中可以立即執行任務,不具備開啓線程的能力

異步:當前線程結束時執行任務,具備開啓線程的能力

併發隊列:可以讓多個任務同時進行,自動開啓多個線程同時執行

串行隊列:順序的執行,五張圖片一張一張的下載

2、創建隊列----串行、並行、主隊列、全局隊列

  *** 主隊列就在主線程中執行,並且主隊列不具備開線程的能力

    /*函數 dispatch_queue_create 兩個參數
     const char * label  隊列名稱
     dispatch_queue_attr_t  attr   隊列類型
     DISPATCH_QUEUE_SERIAL   串行
     DISPATCH_QUEUE_CONCURRENT   併發
     */
    //創建串行隊列
    dispatch_queue_t serial =  dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
    //創建併發隊列
    dispatch_queue_t concurrent =  dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
    //全局隊列:全局隊列是併發隊列
    /*
     參數1 : long identifier  隊列的優先級
     
     #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   後臺
     
     參數2 : unsigned long flags  隊列參數,一般寫0
     */
    dispatch_queue_t global = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    
    //獲取主隊列。主隊列中的任務都會在主線程中執行
    dispatch_queue_t mainqueue = dispatch_get_main_queue();
    

3、同步、異步函數

//========================      同步      ============================
    /*
     函數 dispatch_sync ()
        參數:dispatch_queue_t  _Nonnull queue 哪個隊列
        參數:<#^(void)block#>    任務
     */
    
    // 同步串行隊列,馬上執行,在當前線程
    dispatch_sync(serial, ^{
        NSLog(@"~~~%@~~~", [NSThread currentThread]);
    });
    // 同步並行隊列,馬上執行,在當前線程
    dispatch_sync(concurrent, ^{
        NSLog(@"~~~%@~~~", [NSThread currentThread]);
    });
//========================      異步      ============================
    /*
     函數 dispatch_aasync ()
     參數:dispatch_queue_t  _Nonnull queue 哪個隊列
     參數:<#^(void)block#>    任務
     */
    
    //異步函數串行隊列,開闢線程,多個任務按順序執行
    dispatch_async(serial, ^{
        
        dispatch_async(serial, ^{
            NSLog(@"~~~%@~~~", [NSThread currentThread]);
        });
        dispatch_async(serial, ^{
            NSLog(@"~~~%@~~~", [NSThread currentThread]);
        });
        dispatch_async(serial, ^{
            NSLog(@"~~~%@~~~", [NSThread currentThread]);
        });
    });
    
    //異步函數並行隊列,開闢線程,多個任務一起執行
    dispatch_async(concurrent, ^{
        dispatch_async(serial, ^{
            NSLog(@"~~~%@~~~", [NSThread currentThread]);
        });
        dispatch_async(serial, ^{
            NSLog(@"~~~%@~~~", [NSThread currentThread]);
        });
        dispatch_async(serial, ^{
            NSLog(@"~~~%@~~~", [NSThread currentThread]);
        });
    });

使用同步函數添加任務A到串行隊列,說明要在當前串行隊列立即執行A,任務A執行完後,纔會執行任務A後面的代碼。也就是說任務A必須要等到當前串行隊列執行完成任務B後才能執行,因此必須先執行A中立即添加的任務,又要必須等到任務B執行完才能執行下一個任務,會死循環,卡死。誰也無法執行

4、GCD--線程之間的通信     下載圖片的例子

    NSURL * disUrl = [NSURL URLWithString:@""];
    //異步開一個線程下載圖片
    dispatch_async(dispatch_queue_create("image", DISPATCH_QUEUE_PRIORITY_DEFAULT), ^{
        
        NSData * disData = [NSData dataWithContentsOfURL:disUrl];
        UIImage * disImage = [UIImage imageWithData:disData];
        //返回主線程使用圖片
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = disImage;
        });
        
    });

5、GCD其他常用函數

1>  dispatch_barrier  柵欄、障礙、界限

      在barrier之前的先執行,然後執行barrier,再執行barrier後面的,而barrier的隊列不能是全局的併發隊列    

      應用:讀寫鎖

      例子:假如我們在平常編碼中,要保證某個屬性線程安全的讀寫,一般加鎖方式:這就是atomic的加鎖方式,這種方式不一定就是安全的,在訪問屬性時,如果在一個縣城上多次調用getter方法,每次得到的值不一定相同,在兩次讀操作之間也可能會寫入新的shu'xing'zh

- (void)setAge:(NSString *)age
{
    @synchronized(self){
        _age = [age copy];
    }
}
-(NSString *)age
{
    @synchronized(self){
        return _age;
    }
}

所以我們就用到了最優寫法,加上柵欄,也就是barrier

- (void)setAge:(NSString *)age
{
    dispatch_barrier_async(queue, ^{
        _age = [age copy];
    });
}
-(NSString *)age
{
    __block NSString * testAge;
    dispatch_sync(queue, ^{
         testAge = _age;
    });
    return testAge;
}

這段代碼中加上了dispatch_barrier_async函數,也就是說在讀操作中要等之前加的寫操作完成後才能執行。 

2 > dispatch_after 延遲執行

-(void)after{
    //方法1   延遲兩秒執行
    [self performSelector:@selector(run) withObject:@"參數" afterDelay:2.0];
    //方法2
    /*
     dispatch_time(dispatch_time_t when, int64_t delta);
     
     #define DISPATCH_TIME_NOW (0ull)
     #define DISPATCH_TIME_FOREVER (~0ull)
     */
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    });
    //方法3
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
}

3 > 單例模式,在整個應用程序中共享一份資源,只需要初始化一次

+(instancetype)sharePerson
{
    static Person * person = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        person = [[Person alloc]init];
    });
    return person;
}
+(instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        person = [Person allocWithZone:zone];
    });
    return person;
}
-(id)copy
{
    return person;
}

  面試題:口述單例創建的過程

  創建一個用static修飾的全局變量,並置爲nil,使用dispatch_onece函數檢查是否爲nil,如果是nil就創建一個並返回全局實例,需要實現allocwithzone方法,並且爲了防止由於多次訪問而得到新的實例,需要重寫copy方法,返回本身。

4 > dispatch_group 隊列組

隊列組是把相關的任務添加到一個組中進行,通過監聽組內所有任務的情況作出相應處理;比如多張圖片下載,並且合成新圖片

-(void)dispatchGroup
{
    //創建隊列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //創建組
    dispatch_group_t group = dispatch_group_create();
    
    //用組隊列下載圖片1
    dispatch_group_async(group, queue, ^{
        NSData * imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:@""]];
        self.imageOne = [UIImage imageWithData:imageData];
    });
    //用組隊列下載圖片2
    dispatch_group_async(group, queue, ^{
        NSData * imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:@""]];
        self.imageTwo = [UIImage imageWithData:imageData];
    });
    //將圖1和圖2合併成一張新圖片
    dispatch_group_notify(group, queue, ^{
        CGFloat imageH = self.imageView.bounds.size.height;
        CGFloat imageW = self.imageView.bounds.size.width;
        //開啓圖形上下文
        UIGraphicsBeginImageContext(self.imageView.bounds.size);
        //畫圖
        [self.imageOne drawInRect:CGRectMake(0,0,imageW/2,imageH)];
        [self.imageTwo drawInRect:CGRectMake(imageW/2, 0, imageW/2, imageH)];
        //將圖片取出
        UIImage * newImage = UIGraphicsGetImageFromCurrentImageContext();
        //關閉圖形上下文
        UIGraphicsEndImageContext();
        
        //回到主線程加載圖片
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = newImage;
        });
    });
}

5 > 定時器,GCD定時器不受Mode的影響,因此要比NSTimer準確

//
//  GCDTimer.m
//  testGCD
//
//  Created by 李寧 on 2018/8/28.
//  Copyright © 2018年 李壞. All rights reserved.
//

#import "GCDTimer.h"
@interface GCDTimer()
@property (nonatomic,strong)dispatch_source_t  timer;
@end
@implementation GCDTimer

-(void)myTimer
{
    static int count = 0;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0* NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

    });
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //創建一個定時器
    /*
     dispatch_source_create
     參數一:dispatch_source_type_t   定時器的類型
     參數二:uintptr_t handle         句柄
     參數三:unsigned long mask       一般寫0
     參數四:dispatch_queue_t        對列,dispatch_source_t是OC的對象
     */
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    //回調函數的時間間隔,爲了嚴謹,用int64,相乘以後就變了
    int64_t intervarl = (int64_t)(2.0* NSEC_PER_SEC);
    
    //設置開始時間  從現在開始3s後開始
    /*
    函數 dispatch_time
    參數一:dispatch_time_t when
     #define DISPATCH_TIME_NOW (0ull)   現在開始
     #define DISPATCH_TIME_FOREVER (~0ull)    啥時候開始都可以
    參數二:int64_t delta
     */
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0* NSEC_PER_SEC));
    //設置定時器的各種屬性
    /*
     函數:dispatch_source_set_timer
     參數一:dispatch_source_t  _Nonnull source,  timer
     參數二:dispatch_time_t start,               開始時間
     參數三:uint64_t interval,                   時間間隔
     參數四:uint64_t leeway                      不需要傳
     */
    dispatch_source_set_timer(self.timer, start, intervarl, 0);
    
    //設置回調,即每次的事件間隔需要做什麼
    dispatch_source_set_event_handler(self.timer, ^{
        
        NSLog(@"I am a  timer");
        //如果希望三次之後就停止
        count ++;
        if(count > 3){
            dispatch_cancel(self.timer);
            self.timer = nil;
        }
    });
    
    //恢復定時器
    dispatch_resume(self.timer);
}
@end

四、NSOperation

        NSOperation是個抽象類,並不具備封裝操作的能力,他依賴於兩個子類

        1、NSInvocationOperation

        2、NSBlockOperation

        3、自定義子類繼承自NSOperation,實現內部的相應的方法

  2、使用NSOperation實現多線程的步驟

         1 >  創建NSOperation對象

         2 >  創建NSOperationQueue隊列

         3 >  將NSOperation對象添加到NSOperationQueue中

    NSInvocationOperation * option = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(my) object:nil];
    //調用start並不會開闢新的線程而是在當前線程中同步執行,只有將operation對象加到隊列中才會異步
    [option start];
    
    NSBlockOperation * blockOp = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"--%@--",[NSThread currentThread]);
        //打印結果證明是在主線程
    }];
    //增加額外的任務,只有當任務數大於1的時候纔會開異步執行
    [blockOp addExecutionBlock:^{
        NSLog(@"--%@--",[NSThread currentThread]);
    }];
    //自定義Operation:需要實現- (void)main方法,需要做的事情放在mian方法中
    
    //當創建一個隊列,放到這個隊列中的NSOperation對象會自動放到子線程中執行
    NSOperationQueue * queue = [[NSOperationQueue alloc]init];
    //創建一個主線程。放到裏的對象也會自動在子線程中執行
    NSOperationQueue * mainQueue = [NSOperationQueue mainQueue];
    //設置最大併發數:同時執行任務的數量,3表示同時執行3個任務,
    queue.maxConcurrentOperationCount = 3;

3、隊列的取消、暫停、恢復、優先級

    //- (void)cancelAllOperations;

    //- (void)waitUntilAllOperationsAreFinished;

    //取消所有隊列,也可以單個取消隊列,但是一旦開始就不能取消

    [mainQueue cancelAllOperations];

    //yes表示暫停、No表示恢復隊列

    [mainQueue setSuspended:YES];

4、添加依賴

可以跨隊列依賴,但是不能循環依賴,不管NSOperation對象在哪個隊列,只要是兩個NSOperation對象就可以依賴

    NSOperationQueue * queue = [[NSOperationQueue alloc]init];
    NSBlockOperation * block1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"-----%@----",[NSThread currentThread]);
    }];
    NSBlockOperation * block2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"-----%@----",[NSThread currentThread]);
    }];
    NSBlockOperation * block3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"-----%@----",[NSThread currentThread]);
    }];
    NSBlockOperation * block4 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"-----%@----",[NSThread currentThread]);
    }];
    
    //添加依賴,block1和block2執行完成之後再執行block3,叫做block3依賴於block1和block2
    //給block3添加依賴
    [block3 addDependency:block1];
    [block3 addDependency:block2];
    //不能循環依賴,但可以跨隊列依賴,不管是在哪個隊列,只要是NSOperation對象就可以
    [block4 addDependency:block3];
    
    [queue addOperation:block1];
    [queue addOperation:block2];
    [queue addOperation:block3];
    [queue addOperation:block4];

5、線程間的通信:多張圖片下載最後合成

-(void)downLoadImage
{
    __block UIImage * image1 = nil;
    __block UIImage * image2 = nil;
    NSOperationQueue * queue = [[NSOperationQueue alloc]init];
    NSBlockOperation * blcok1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"-----%@----",[NSThread currentThread]);
        image1 = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@""]]];
    }];
    NSBlockOperation * blcok2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"-----%@----",[NSThread currentThread]);
        image2 = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@""]]];
    }];
    CGFloat imageH = imageView.bounds.size.height;
    CGFloat imageW = imageView.bounds.size.width;
    
    NSBlockOperation * block3 = [NSBlockOperation blockOperationWithBlock:^{
        //開啓上下文
        UIGraphicsBeginImageContext(CGSizeMake(imageW, imageH));
        [image1 drawInRect:CGRectMake(0,0,imageW/2, imageH)];
        [image2 drawInRect:CGRectMake(0,imageW/2,imageW/2, imageH)];
        UIImage * image3 = UIGraphicsGetImageFromCurrentImageContext();
        //關閉上下文
        UIGraphicsEndImageContext();
        //回到主線程
        [[NSOperationQueue mainQueue]addOperation:[NSBlockOperation blockOperationWithBlock:^{
            self->imageView.image = image3;
        }]];

    }];
    //添加依賴
    [block3 addDependency:blcok1];
    [block3 addDependency:blcok2];
    
    //將任務添加到隊列中
    [queue addOperation:blcok1];
    [queue addOperation:blcok2];
    [queue addOperation:block3];
}

五、多線程的應用

       SDWebImage框架的底層主要就是基於多線程,實現小圖片的多圖片下載,SDWebImag由兩個緩存區,一個是內存層面上的,一個是硬盤層面上的,內存中是以key--value的形式存儲圖片,當沒有內存空間的時候自動清理圖片,文件是以時間爲單位的,默認圖片存儲一週

  1、入口setImageWithURL:placeHolderImage:option:會先顯示佔位圖片,然後根據URL處理圖片

  2、進入SDWebImageMangaer的downLoadWithURL:delegate:option:userInfo方法交給SDImageCache,從緩存中查找圖片是否已經存在,如果存在SDImageCacheDelegate回調imageCache:didFindImage:forkey到SDWebImageManager顯示,如果沒有

 3、如果內存的緩存中沒有,生成NSINvocationOperation添加到隊列中去硬盤中查找,根據key--value,如果找到了先加到緩存中(如果緩存中空閒內存過小,會先清空緩存)SDImageCacheDelegate回調方法   imageCache:didFindImage:forkeyPath顯示圖片

4、如果在硬盤中沒有找到,說明圖片不存在,需要下載,回調imageCache:didNotFindImage:forKeyPath

5、生成一個下載器:SDWebImageDownLoader開始下載,這一步由NSURLConnection完成,實現相關的delegate來判斷下載完成、失敗、下載中狀態

6、下載完成後交給SDWebImagDecoder做圖片的編碼處理,是在NSOperationQueue中完成的

7、當完成後會調用imageDownloader:didFinishWithImage回調給SDWebImageManager告訴圖片下載完成

8、在NSOperationQueue中分別將圖片在主線程顯示,在子線程中先保存到SDImageCache在保存到沙盒。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章