多線程(詳解)

1.iOS 開發(fā)中多線程出現(xiàn)的本質妇拯?
  • 原因:一個iOS程序運行后,默認會開啟1條線程洗鸵,稱為“主線程”或“UI線程”來處理任務越锈。當任務量變得很大時,主線程疲于處理當前耗時操作膘滨,對于其他需要處理的任務就會響應不過來甘凭,宏觀體現(xiàn) —— 好卡。 為了解決這種“卡”的用戶體驗火邓,出現(xiàn)了多線程處理任務策略丹弱。
  • 注意:多線程并發(fā)執(zhí)行任務本質是cpu的快速切換。
  • 技巧:iOS中模擬耗時操作铲咨,只需寫一個很大的for循環(huán)
2.iOS中多線程的實現(xiàn)方案(4種)

①pthread

  • 使用步驟:
  • 導入頭文件 #import <pthread.h>
  • 調用創(chuàng)建線程函數
#import "ViewController.h"
#import <pthread.h>

@interface ViewController ()
@end

@implementation ViewController

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{

// 創(chuàng)建線程
/*
 第一個參數:線程對象地址
 第二個參數:屬性
 第三個參數:要調用的方法指針
 第四個參數:要傳遞給函數的參數
 */
pthread_t thread = nil;
pthread_create(&thread, NULL, run, NULL);
}
//(*) =>函數名稱
void *run(void *str)
{
    for (NSInteger i =0 ; i<300; i++) {
    NSLog(@"%zd--%@",i,[NSThread currentThread]);  //均為子線程
    }
    return NULL;
}
@end

②NSThread

  • 使用步驟:直接創(chuàng)建線程躲胳,不需要導入頭文件

  • 創(chuàng)建線程方式:

    <方式1>
    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run:) object:@"ios"];
    [thread start];
    優(yōu)點:能拿到線程對象 缺點:需要手動的啟動線程

    <方式2>
    [NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:nil];
    優(yōu)點:自動啟動線程 缺點:不能拿到線程對象

    <方式3>
    [self performSelectorInBackground:@selector(run:) withObject:@"后臺線程"];
    優(yōu)點:自動啟動線程 缺點:不能拿到線程對象

    <方式4>自定義,重寫main方法封裝任務
    NewThread *threadB = [[NewThread alloc]init];
    [threadB start];
    優(yōu)點:能拿到線程對象 缺點:需要手動的啟動線程
    => 線程封裝任務是在main方法里面纤勒。

  • 注意:

  • 生命周期:線程當任務執(zhí)行完畢的時候自動銷毀

  • 線程對象先執(zhí)行完任務的先銷毀

  • 線程優(yōu)先級只在任務相對較多時更好的體現(xiàn)坯苹,第一次執(zhí)行還是按照線程開始的先后順序執(zhí)行。(線程優(yōu)先級threadPriority摇天,范圍:0~1.0 默認是0.5)

③GCD

1.使用步驟:任務 + 隊列

//1 獲得隊列
 第一個參數:C語言的字符串 對隊列的名稱(com.520it.www.DownloadQueue)
 第二個參數:隊列的類型
 DISPATCH_QUEUE_SERIAL  串行隊列
 DISPATCH_QUEUE_CONCURRENT 并發(fā)隊列

dispatch_queue_t queue = dispatch_queue_create("com.520ios.www.DownloadQueue”,DISPATCH_QUEUE_SERIAL);
//dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

//2 封裝任務并把任務添加到隊列
dispatch_async(queue, ^{
    NSLog(@"download1---%@",[NSThread currentThread]);
});

拓展:GCD常用函數

1)一次性代碼

  • 特點:
  • 整個程序運行過程中只會執(zhí)行一次
  • 線程安全
  • 程序每次啟動都會執(zhí)行一次
  • 一次性代碼不能放在懶加載中:因為創(chuàng)建多個對象時粹湃,也只會執(zhí)行一次。單例中才可以泉坐。
-(void)once
{
    static dispatch_once_t onceToken;  //typedef long dispatch_once_t;
    //內部的實現(xiàn)原理:最開始的時候onceToken == 0 如果onceToken == 0 那么就執(zhí)行一次,執(zhí)行一次之后onceToken = -1
    dispatch_once(&onceToken, ^{
      NSLog(@"once"); 
    });
  }
應用場景:單例(注意:單例中使用一次性代碼或同步鎖均可为鳄,都是線程安全)

2)延遲執(zhí)行

  • 特點:
    • dispatch_after本身是一個異步函數
    • 該函數的隊列參數選項,除了隊列設置的為“主隊列”則延時執(zhí)行的任務在主線程中執(zhí)行外坚冀,其他隊列做參數济赎,均在子線程中執(zhí)行延時任務,包括參數隊列設置為自己創(chuàng)建的串行隊列记某。
    • 延遲執(zhí)行的單位:納秒 司训,精度高。
  dispatch_queue_t queue = dispatch_queue_create("TestQueue", DISPATCH_QUEUE_SERIAL);
  //   延遲2秒,然后再把任務提交到隊列
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), queue, ^{
    NSLog(@"GCD----%@",[NSThread currentThread]); //子線程=> 異步函數
});
  • 拓展總結:延遲執(zhí)行方法
    //延遲方法一(NSTimer)
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(task) userInfo:nil repeats:NO]; //默認添加進“當前RunLoop”液南,且是默認模式
    [NSTimer timerWithTimeInterval:2.0 repeats:YES block:nil]; //此種方法要將定時器手動添加進RunLoop

    //延遲方法二(NSRunloop)
    [self performSelector:@selector(task) withObject:nil afterDelay:2.0];
    
    //延遲方法三:(GCD中的延遲執(zhí)行/GCD定時器)
    dispatch_queue_t queue = dispatch_queue_create("TestQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 *     NSEC_PER_SEC)), queue, ^{
      NSLog(@"GCD----%@",[NSThread currentThread]); //子線程
    });
    

3)快速迭代

  • 特點:
  • 快速迭代屬于:同步函數 (在主隊列中會發(fā)生死鎖 + 打印【顯示它沒開子線程?遣隆!】共同推導出的)
  • 但注意:在并發(fā)隊列中還是會同時開啟"主線程"和"子線程"一起并發(fā)執(zhí)行任務(主流)
  • 在自己創(chuàng)建的串行隊列中會在主線程中串行執(zhí)行滑凉,不開子線程故無意義统扳,因為開子線程還要耗費性能喘帚!性能更低。
  • 快速迭代:本質就是遍歷執(zhí)行任務咒钟,只不過對比for循環(huán)吹由,多了開啟子線程的能力而已,故效率更高朱嘴。
 dispatch_queue_t queue =  dispatch_get_global_queue(0, 0);// 開啟子線程協(xié)助主線程完成任務
//     dispatch_queue_t queue = dispatch_queue_create("good", DISPATCH_QUEUE_CONCURRENT);
//     dispatch_queue_t queue = dispatch_get_main_queue();//死鎖 => 同步函數 + 主隊列
//     dispatch_queue_t queue = dispatch_queue_create("TestQueue", DISPATCH_QUEUE_SERIAL);

dispatch_apply(10, queue, ^(size_t i) {
    NSLog(@"%zd---%@",i,[NSThread currentThread]); //內部執(zhí)行順序是并發(fā)的倾鲫,外部卻認為它沒開子線程!萍嬉!
});


應用場景 :遍歷并剪切文件

-(void)moveFile
{
    //01 得到上層文件夾的路徑
    NSString *fromPath = @"/Users/wuyuanping/Desktop/from";
    NSString *toPath = @"/Users/wuyuanping/Desktop/to";

    //02 得到上層文件夾中所有的文件
    NSArray *subPaths =  [[NSFileManager defaultManager] subpathsAtPath:fromPath];

    //03 遍歷并剪切文件
    dispatch_apply(subPaths.count, dispatch_get_global_queue(0, 0), ^(size_t index) {
   
        NSString *fileName = subPaths[index];
        NSString *fromFullPath = [fromPath stringByAppendingPathComponent:fileName];
        NSString *toFullPath = [toPath stringByAppendingPathComponent:fileName];
    
        [[NSFileManager defaultManager] moveItemAtPath:fromFullPath toPath:toFullPath error:nil];
    
        NSLog(@"%@--%@--%@",fromFullPath,toFullPath,[NSThread currentThread]); //子線程和主線程都有
   });
}

4)柵欄函數

  • 特點:
  • 攔截柵欄后面的任務乌昔,必須等前面的任務執(zhí)行完才執(zhí)行柵欄當前的block,且必須等柵欄的block執(zhí)行完才執(zhí)行柵欄后面任務
  • dispatch_barrier_async 柵欄Block創(chuàng)建了子線程則在子線程中執(zhí)行任務(因為“主隊列”只能在主線程中執(zhí)行任務);dispatch_barrier_sync 主線程執(zhí)行柵欄任務
  • 子線程中可以出現(xiàn):同步函數 + 主隊列 不會發(fā)生死鎖 壤追,不建議子線程中開子線程,系統(tǒng)空閑就可以開磕道,否則不能
  • 柵欄函數在使用中不能使用系統(tǒng)提供的全局并發(fā)隊列(會喪失攔截的功能),只能用自己創(chuàng)建的并發(fā)隊列
dispatch_queue_t queue = dispatch_queue_create("TestQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_barrier_async(queue, ^{
      NSLog(@"+++++%@",[NSThread currentThread]);
 });
應用場景:需求是哪些子線程任務先執(zhí)行

5)隊列組

  • 特點:

  • 當隊列組中所有的任務都執(zhí)行完畢,那么就會執(zhí)行dispatch_group_notify的block

  • dispatch_group_notify該函數本身是異步的

  • 隊列組的使用:

方式一:
// 添加任務進組函數
//dispatch_group_async =>異步函數封裝任務|提交到隊列|監(jiān)聽任務是否執(zhí)行完畢
//dispatch__async      =>異步函數封裝任務|提交到隊列

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("TestQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
    NSLog(@"1----%@",[NSThread currentThread]);
});

dispatch_group_async(group, queue, ^{
    NSLog(@"2----%@",[NSThread currentThread]);
});

dispatch_group_async(group, queue, ^{
    NSLog(@"3----%@",[NSThread currentThread]);
});

dispatch_group_async(group, queue, ^{
    NSLog(@"4----%@",[NSThread currentThread]);
});

dispatch_group_notify(group, queue, ^{ //組任務都執(zhí)行完畢之后會調用此方法
    NSLog(@"---end---%@",[NSThread currentThread]); //子線程
});

方式二:
 //使用函數對來監(jiān)聽任務(dispatch_group_enter|dispatch_group_leave)
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("TestQueue", DISPATCH_QUEUE_CONCURRENT);

//在該函數后面的異步任務會被group監(jiān)聽
dispatch_group_enter(group);
dispatch_async(queue, ^{
    NSLog(@"1----%@",[NSThread currentThread]);
    dispatch_group_leave(group);
});

dispatch_group_enter(group);
dispatch_async(queue, ^{
    NSLog(@"2----%@",[NSThread currentThread]);
    
    dispatch_group_leave(group);
});

dispatch_group_enter(group);
dispatch_async(queue, ^{
    NSLog(@"3----%@",[NSThread currentThread]);
    
    dispatch_group_leave(group);
});

dispatch_group_notify(group, queue, ^{ //組任務都執(zhí)行完畢之后會調用此方法
    NSLog(@"---end---%@",[NSThread currentThread]);
});

拓展:GCD定時器

  -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//1 創(chuàng)建定時器對象(技巧:輸入dispatch_source 再選GCD的會一次性出來四個函數)
/*
 第一個參數:創(chuàng)建的source的類型 DISPATCH_SOURCE_TYPE_TIMER 定時器事件
 第二個參數:描述信息
 第三個參數:更詳細的描述信息
 第四個參數:隊列(線程)[主隊列那么回調就在主線程中執(zhí)行行冰,否則就在子線程中執(zhí)行]
 */
 dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL));

//2 設置定時器對象
/*
 第一個參數:定時器對象
 第二個參數:開始時間(第一次執(zhí)行的時間)
 第三個參數:間隔時間 GCD的時間單位是納秒
 第四個參數:精準度(允許的誤差溺蕉,一般取0)
 */
dispatch_time_t timeT = dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC);//tips:取消一開始就執(zhí)行一次,即達到也是延時兩秒執(zhí)行
dispatch_source_set_timer(timer, timeT, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);

//3 設置定時器的事件
dispatch_source_set_event_handler(timer, ^{
    NSLog(@"GCD---%@",[NSThread currentThread]); //非主隊列悼做,則為子線程
});

//4 執(zhí)行定時器對象
dispatch_resume(timer);

self.timer = timer;//強引用焙贷,防止定時器一出作用域就銷毀
}

GCD總結

  • 具體是否創(chuàng)建子線程?
  • 異步函數 + 并發(fā)隊列: => 能夠開啟多條子線程“并發(fā)”的執(zhí)行隊列中的任務(驗證“線程間”及“各個子線程中任務”并發(fā)執(zhí)行贿堰?打印)
  • 異步函數 + 串行隊列: => 一個串行隊列對應只能開啟一條子線程串行執(zhí)行任務辙芍,即嚴格按照任務代碼塊從上往下"串行"執(zhí)行任務。
    注意:多個串行隊列羹与,就相當于開啟了多條子線程故硅,線程間則是異步執(zhí)行任務的。同一條子線程則串行
  • 同步函數 + 并發(fā)隊列/串行隊列: => 不會開啟子線程,所有的任務在當前線程中串行執(zhí)行
  • 異步函數 + 主隊列:=> 不會開子線程,所有的任務均在主線程中的串行執(zhí)行
    (異步函數可以并發(fā)執(zhí)行纵搁,不會一直等待前面線程執(zhí)行完才輪到自己執(zhí)行吃衅,故不會發(fā)生死鎖)
  • 同步函數 + 主隊列: => 代碼寫在主線程中會發(fā)生死鎖(會一直傻等主線程來執(zhí)行故發(fā)生死鎖) 但代碼寫在子線程中就不會發(fā)生死鎖。故可以用在線程間通信(子線程跳到主線程)
  • “并發(fā)”的本質是:取出上一個任務之后腾誉,無論是否執(zhí)行完直接接著取下一個任務徘层,多個Blcok塊各有很多任務時,按一開始執(zhí)行子線程的順序你一下我一下并肩完成利职。
  • 開子線程的數量并不是僅僅由任務的數量決定的趣效,要看系統(tǒng)是否空閑。出于性能考慮iOS中一般只能開6條子線程
  • 一個Block任務代碼塊中只能開啟一個子線程
  • 當系統(tǒng)空閑時猪贪,子線程執(zhí)行任務的順序等于創(chuàng)建子線程的先后順序跷敬,當任務變多,順序就不確定热押。
  • 只要不涉及到創(chuàng)建子線程西傀,代碼執(zhí)行的順序就是老老實實的從上往下串行執(zhí)行斤寇。(因為均在主線程,默認主隊列)
  • 創(chuàng)建子線程本質原因:“并發(fā)”從而避免主線程忙不過來(卡)拥褂,或者說充分利用cpu性能娘锁。

④NSOperation

  • 步驟: 操作 + 隊列
  • 注意:
  • NSOperation是抽象類不能直接使用,可以使用它的子類(系統(tǒng)提供了NSInvocationOperation饺鹃,NSBlockOperation和自定義的)
    【自定義繼承自NSOperation :重寫內部的Main方法致盟,說明start方法內部也是調用的main方法 ,優(yōu)點:有利于代碼的封裝和復用】
  • 單獨封裝操作并執(zhí)行時并不會開啟子線程尤慰,只會在主線程中執(zhí)行,除非配合非主隊列一起使用時才會生成子線程并發(fā)執(zhí)行任務
  • 單獨封裝操作并執(zhí)行時雷蹂,也可以通過添加額外任務addExecutionBlock,即操作對象中封裝的任務數量>1也會開子線程執(zhí)行任務
    但注意:額外操作才會開子線程去執(zhí)行伟端,之前的操作 仍然在主線程中執(zhí)行,這是與操作直接添加進非主隊列的區(qū)別所在匪煌。
  • GCD和NSOperation隊列對比:
    GCD:
    并發(fā):自己創(chuàng)建|全局并發(fā)
    串行:自己創(chuàng)建|主隊列
    NSOperation:
    并發(fā):自己創(chuàng)建[[NSOperationQueue alloc]init]默認是并發(fā)隊列
    串行: 自己創(chuàng)建[[NSOperationQueue alloc]init]設置最大并發(fā)數等于1 | 主隊列
    [NSOperationQueue MainQueue]
    注意:通過設置最大并發(fā)數為1而達到串行執(zhí)行任務目的 與 主隊列 的串行 是有區(qū)別的责蝠。前者還是會生成子線程,只不過是控制子線程間只能串行執(zhí)行任務萎庭,具體生成子線程個數不確定霜医。 順序就為添加進隊列(開始)順序執(zhí)行
    后者不會生成子線程,在主線程中串行執(zhí)行任務驳规。
    maxConcurrentOperationCount 最大并發(fā)數:允許并發(fā)執(zhí)行的子線程數
非主流開發(fā)演示:
//1.封裝操作
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"3---%@",[NSThread currentThread]); //主線程
}];

//NSBlockOperation如果操作對象中封裝的操作數量>1那么添加的代碼塊中就會開子線程和當前線程一起執(zhí)行任務
[op3 addExecutionBlock:^{
    NSLog(@"+++++++4++++%@",[NSThread currentThread]); //子線程
}];
[op3 addExecutionBlock:^{
    NSLog(@"+++++++5++++%@",[NSThread currentThread]);//子線程
}];

//2 執(zhí)行操作
[op3 start];


主流開發(fā)代碼演示:
-(void)BlockOperationWithQueue
{   //操作 + 隊列:只要不是主隊列肴敛,就會開子線程并發(fā)執(zhí)行任務
    
  //1.封裝操作
  NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"1----%@",[NSThread currentThread]);
  }];
  //NSInvocationOperation *op1 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(download) object:nil];

  //2.把操作添加到隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue addOperation:op1];  //addOperation 內部調用start方法,故你可以不寫start

  //該方法內部先封裝操作,然后把操作添加到隊列中(簡便寫法:直接操作添加到隊列)
  [queue addOperationWithBlock:^{
    NSLog(@"3---%@",[NSThread currentThread]); //子線程
  }];
}

拓展:操作隊列的其他用法

  • 設置最大并發(fā)數量 (maxConcurrentOperationCount)
  • 設置操作依賴(addDependency)和監(jiān)聽(completionBlock)
  • 操作隊列的暫停(setSuspended)和取消(cancel/cancelAllOperations)
    注意:
  • 操作的狀態(tài): 正在執(zhí)行|等待執(zhí)行|執(zhí)行完畢
  • 暫停 只能暫停下一個操作,當前正處于執(zhí)行狀態(tài)的操作是不能暫停的
  • 取消隊列中所有的任務 當前正在執(zhí)行的操作不能馬上取消,需要等當前操作執(zhí)行完畢才能取消后面的操作
  • 如果自定義NSOperation蘋果官方的建議:每執(zhí)行完一段耗時操作之后就檢查當前操作是否被取消
自定義NSOperation:
#import “YPOperation.h"
@implementation YPOperation

-(void)main
{
  for (NSInteger i = 0; i<1000; i++) {
      NSLog(@"1---%zd--%@",i,[NSThread currentThread]);
      //if(self.isCancelled) 
      return;
  }

  //每執(zhí)行完一段耗時操作之后就判斷當前操作是否被取消,如果被取消了那么就直接退出
  if(self.isCancelled) return;

  NSLog(@"++++++++++++++");

  for (NSInteger i = 0; i<1000; i++) {
      NSLog(@"2---%zd--%@",i,[NSThread currentThread]);
       //if(self.isCancelled) return;
  }
   if(self.isCancelled) return;

  for (NSInteger i = 0; i<1000; i++) {
       if(self.isCancelled) return;
      NSLog(@"3---%zd--%@",i,[NSThread currentThread]);
   }
}
@end


例子:設置監(jiān)聽和操作依賴
 NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"3----%@",[NSThread currentThread]);
}];

 NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"4-下載電影-%@",[NSThread currentThread]);
}];   
 NSBlockOperation *op5 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"+++++++5+%@+++",[NSThread currentThread]);
}];
//設置監(jiān)聽
op4.completionBlock = ^{
    NSLog(@"我已經被下載完了,快點來看我吧--%@",[NSThread     currentThread]);
};

//設置依賴(不能設置循環(huán)依賴)
[op3 addDependency:op4];
[op4 addDependency:op5];

注意:因為開啟子線程自定義隊列默認異步執(zhí)行,故只能保證op4任務結束會執(zhí)行他的監(jiān)聽Block吗购,即block在op4后面執(zhí)行医男,
     但無法保證一定緊接其后,要看具體情況)  

綜合案例: 多圖下載(利用上面所學的多線程技術)

  • 總結:

    • 基本數據展示
      問題:UI不流暢 => 在子線程中下載圖片
      注意:cell的復用造成數據錯亂捻勉,通過設置占位圖片解決該問題
    • 性能優(yōu)化
      問題:重復下載圖片
      ①已經下載好了圖片的 => 二級緩存(內存緩存磁盤緩存)
      ②已經開始執(zhí)行下載任務但還沒有下載完 => 緩存下載任務(操作)
  • 注意點
    ①圖片下載完成之后把操作從緩存中移除
    ②圖片下載完成后镀梭,對獲得的圖片數據做容錯處理
    ③發(fā)生內存警告后的處理:1)移除內存緩存中的所有元素 2)取消下載圖片隊列中所有的操作

  • 拓展:磁盤(沙盒)緩存的目錄結構:

  • ①Documents :該目錄下面的數據在連接手機時會備份,蘋果官方不允許把下載的數據存放于該目錄下

  • ②Libriary
    1)caches
    2)perference 該目錄用來存放偏好設置如登錄名密碼等等

  • ③Temp :會被隨機刪除

3.線程狀態(tài)
 新建 -> 就緒 <-> 運行 -> 死亡
          |
         阻塞
  • 注意:
  • “運行”狀態(tài)可以直接切換為“阻塞”狀態(tài)踱启,阻塞狀態(tài)不能直接切換為運行报账,必須先切換為“就緒”狀態(tài),再從就緒狀態(tài)切換為“運行”狀態(tài)埠偿。
  • 線程死亡之后不能重新開啟,得重新創(chuàng)建透罢。
  • 阻塞線程的方式:
    [NSThread sleepForTimeInterval:3.0];
    [NSThread sleepUntilDate: [NSDatedateWithTimeIntervalSinceNow:2.0]];
  • 線程死亡的方式:
    ①任務執(zhí)行完畢(默認)
    ②強制退出線程 + (void)exit;
    ③提前讓任務結束 break和return
4.線程安全
  • 問題產生的原因:多個線程訪問同一塊資源會發(fā)生數據安全問題
  • 解決方式: 線程同步(同一時間只有一個線程訪問同一塊資源)
    @synchronized(鎖對象){
    要鎖住的代碼
    }
    鎖對象:要求是全局唯一的屬性,一般用self
  • 注意點:
  • 要注意加鎖的位置
  • 加鎖需要耗費性能,因此需要注意加鎖的條件(多線程訪問同一塊資源冠蒋,不得已才去加鎖)
  • 原子和非原子屬性:
    atomic會對setter方法加鎖 (OSX應用琐凭,常用atomic,電腦性能好浊服,更多的是關注安全)
    noatomic不會對Setter方法加鎖(性能高统屈,安全性低常用于iOS 應用)
應用:售票
-(void)saleTicket
{
     while (1) {  //讓線程一直執(zhí)行
         @synchronized (self) {
          //檢查余票
          NSInteger count = self.totalCount;
          if (count >0) {
              //賣出去一張
              self.totalCount = count - 1;
              NSLog(@"%@賣出去了一張票還剩下%zd張票",[NSThread   currentThread].name,self.totalCount);
            }else
          {
              NSLog(@"%@發(fā)現(xiàn)票已經賣完了",[NSThread currentThread].name);
              break; //讓線程死亡
          }
       }
    }
}
5.線程間通信
①利用NSThread方法(2種)
 [self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:NO];
 [self performSelector:@selector(showImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
 //直接調用本質方法
 [self.imageView performSelector:@selector(setImage:) onThread:
 [NSThread mainThread] withObject:image waitUntilDone:YES];

②利用GCD方法
//同步函數+主隊列(異步函數+ 主隊列更主流)
 dispatch_sync(dispatch_get_main_queue(), ^{// 注意:當“同步函數+主隊列”是寫在子線程當中時是不會發(fā)生死鎖的
        self.imageView.image = image;
        NSLog(@"UI---%@",[NSThread currentThread]);//主線程
});

③利用NSOperation方法
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
        
        self.imageView.image = image;
        NSLog(@"UI----%@",[NSThread currentThread]);
}];
6.RunLoop
  • 基本作用
    ①保持程序的持續(xù)運行(ios程序為什么能一直活著不會死,如果沒有Runloop,那么程序一啟動就會退出)
    ②處理app中的各種事件(比如觸摸事件胚吁、定時器事件、selector事件)
    ③節(jié)省CPU資源愁憔,提高程序性能腕扶,有事情就做事情,沒事情就休息

  • 注意:
    1>如果有了Runloop吨掌,那么相當于在內部有一個死循環(huán)
    2>main函數中的Runloop :在UIApplicationMain函數內部就啟動了一個Runloop,這個默認啟動的Runloop是跟主線程相關聯(lián)的

  • RunLoop使用
    (在iOS開發(fā)中有兩套api來訪問Runloop半抱,它們是等價的,可以互相轉換)
    ①Foundation框架 :(OC)
    [NSRunLoop mainRunLoop]膜宋;獲得主線程對應的Runloop
    [NSRunLoop currentRunLoop]窿侈;獲得執(zhí)行當前方法的線程對應的Runloop

    ②CoreFoundation框架 :(C)
                          CFRunLoopGetMain();獲得主線程對應的Runloop
                          CFRunLoopGetCurrent()秋茫;獲得執(zhí)行當前方法的線程對應的Runloop
    
    相互轉換: CFRunLoopRef runloop = mainRunLoop.getCFRunLoop史简;
             NSRunLoop和CFRunLoopRef都代表著RunLoop對象
    
  • Runloop與線程關系
    ①一個Runloop對應著一條唯一的線程
    ②主線程Runloop已經創(chuàng)建好了,子線程的runloop需要手動創(chuàng)建并開啟(模式不能為空肛著,得有任務干Runloop才不會退出)
    ③Runloop在第一次獲取時創(chuàng)建圆兵,在線程結束時銷毀

  • RunLoop相關5個類
    ①CFRunloopRef(Runloop對象)
    ②CFRunloopModeRef(Runloop的運行模式)
    ③CFRunloopSourceRef(Runloop要處理的事件源)
    ④CFRunloopTimerRef(Timer事件)
    ⑤CFRunloopObserverRef(Runloop的觀察者(監(jiān)聽者))

  • RunLoop使用總結:

  • <1>CFRunloopModeRef (代表著Runloop的運行模式5種)
    ①kCFRunLoopDefaultMode:App的默認Mode,通常主線程是在這個Mode下運行
    ②UITrackingRunLoopMode:界面跟蹤 Mode枢贿,用于 ScrollView 追蹤觸摸滑動
    ③UIInitializationRunLoopMode: 在剛啟動 App 時進入的第一個 Mode殉农,啟動完成后就不再使用
    ④GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內部 Mode,通常用不到
    ⑤kCFRunLoopCommonModes: 這是一個占位用的Mode局荚,不是一種真正的Mode超凳,標記mode
    (kCFRunLoopCommonModes == UITrackingRunLoopMode +kCFRunLoopDefaultMode)

    • 注意:
      1. NSTimer在各種模式下運行的效果:
      ①scheduledTimerWithTimeInterval方法:創(chuàng)建定時器并默認添加到當前線程的Runloop中指定默認運行模式
      ②timerWithTimeInterval:創(chuàng)建定時器,如果該定時器要工作還需要添加到runloop中并指定相應的運行模式
      2.GCD中的定時器在各種模式下運行的效果:
      ①GCD的定時器不會受到Runloop運行模式的影響(因為source不包括這個定時器)
      ②GCD定時器對象需要添加強引用耀态,防止被銷毀
  • <2>CFRunloopSourceRef(Runloop要處理的事件源)
    按照蘋果官方文檔進行劃分的:
    ①Port-Based Sources (基于端口)
    ②Custom Input Sources (自定義輸入源)
    ③Cocoa Perform Selector Sources (Perform Selector 輸入源)
    函數調用棧劃分:
    ①Source0:非基于Port的
    ②Source1:基于Port的

  • <3>CFRunLoopObserverRef
    作用:監(jiān)聽指定模式下運行循環(huán)的狀態(tài)
    如何監(jiān)聽?
    1.創(chuàng)建觀察者對象
    2.給RunLoop添加觀察者
    3.注意對象的釋放

  -(void)observer
{
  //1 創(chuàng)建觀察者對象
  //block調用:當監(jiān)聽者發(fā)現(xiàn)runloop狀態(tài)改變的時候會調用block
  CFRunLoopObserverRef observer =   CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    
    switch (activity) {
        case kCFRunLoopEntry:
            NSLog(@"進入runloop");
            break;
        case kCFRunLoopBeforeTimers:
            NSLog(@"即將處理time事件");
            break;
        case kCFRunLoopBeforeSources:
            NSLog(@"即將處理source事件");
            break;
        case kCFRunLoopBeforeWaiting:
            NSLog(@"即將休眠");
            break;
        case kCFRunLoopAfterWaiting:
            NSLog(@"runloop被喚醒");
            break;
        case kCFRunLoopExit:
            NSLog(@"runloop退出");
            break;
            
        default:
            break;
      }
  });

  //2 給RunLoop添加觀察者
/*
 NSDefaultRunLoopMode = kCFRunLoopDefaultMode
 NSRunLoopCommonModes = kCFRunLoopCommonModes  //追蹤模式下的RunLoop也可以追蹤
 */
  CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

  //3.釋放Observer
  CFRelease(observer);
}
RunLoop總結
  • 說明:

  • 一個Runloop中可以有多個mode,一個mode里面又可以有多個source\observer\timer等等

  • 每次Runloop啟動的時候聪建,只能指定其中一個mode,這個mode被稱為該Runloop的當前mode

  • 如果需要切換mode,只能先退出當前Runloop,再重新指定一個mode進入當前Runloop,目的:為了分割不同組的定時器等茫陆,讓他們相互之間不受影響

  • runloop要想持續(xù)運行金麸,必須選中的mode不為空(即mode里面source或Timer至少有一個)

  • 子線程的RunLoop必須手動創(chuàng)建并開啟,要想持續(xù)運行簿盅,mode不能為空挥下,得要有任務處理才不會退出。

  • 線程與RunLoop一一對應桨醋,不過子線程的Runloop需要手動創(chuàng)建并開啟棚瘟,否則執(zhí)行完一次任務就退出。

  • 線程執(zhí)行完任務就會死亡喜最,故保持后臺常駐線程 — RunLoop一直有活干偎蘸,一般加定時器,或端口(source三種)

  • Runloop應用
    1.ImageView顯示(利用PerformSelector 可以設置運行模式)
    2.常駐線程
    3.自動釋放池
    第一次創(chuàng)建:Runloop啟動的時候創(chuàng)建
    最后一次銷毀:Runloop退出的時候銷毀
    其它時候的創(chuàng)建和銷毀:當Runloop即將進入休眠狀態(tài)的時候會把當前的自動釋放池釋放并創(chuàng)建一個新的

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市迷雪,隨后出現(xiàn)的幾起案子蠢甲,更是在濱河造成了極大的恐慌剿干,老刑警劉巖棘催,帶你破解...
    沈念sama閱讀 216,692評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件弯洗,死亡現(xiàn)場離奇詭異,居然都是意外死亡赁严,警方通過查閱死者的電腦和手機扰柠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來疼约,“玉大人卤档,你說我怎么就攤上這事〕贪” “怎么了劝枣?”我有些...
    開封第一講書人閱讀 162,995評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長倡缠。 經常有香客問我,道長茎活,這世上最難降的妖魔是什么昙沦? 我笑而不...
    開封第一講書人閱讀 58,223評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮载荔,結果婚禮上盾饮,老公的妹妹穿的比我還像新娘。我一直安慰自己懒熙,他們只是感情好丘损,可當我...
    茶點故事閱讀 67,245評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著工扎,像睡著了一般徘钥。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上肢娘,一...
    開封第一講書人閱讀 51,208評論 1 299
  • 那天呈础,我揣著相機與錄音,去河邊找鬼橱健。 笑死而钞,一個胖子當著我的面吹牛,可吹牛的內容都是我干的拘荡。 我是一名探鬼主播臼节,決...
    沈念sama閱讀 40,091評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了网缝?” 一聲冷哼從身側響起巨税,我...
    開封第一講書人閱讀 38,929評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎途凫,沒想到半個月后垢夹,有當地人在樹林里發(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,346評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡维费,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,570評論 2 333
  • 正文 我和宋清朗相戀三年果元,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片犀盟。...
    茶點故事閱讀 39,739評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡而晒,死狀恐怖,靈堂內的尸體忽然破棺而出阅畴,到底是詐尸還是另有隱情倡怎,我是刑警寧澤,帶...
    沈念sama閱讀 35,437評論 5 344
  • 正文 年R本政府宣布贱枣,位于F島的核電站监署,受9級特大地震影響,放射性物質發(fā)生泄漏纽哥。R本人自食惡果不足惜钠乏,卻給世界環(huán)境...
    茶點故事閱讀 41,037評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望春塌。 院中可真熱鬧晓避,春花似錦、人聲如沸只壳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吼句。三九已至锅必,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間惕艳,已是汗流浹背况毅。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留尔艇,地道東北人尔许。 一個月前我還...
    沈念sama閱讀 47,760評論 2 369
  • 正文 我出身青樓,卻偏偏與公主長得像终娃,于是被迫代替她去往敵國和親味廊。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,647評論 2 354

推薦閱讀更多精彩內容

  • 從哪說起呢? 單純講多線程編程真的不知道從哪下嘴余佛。柠新。 不如我直接引用一個最簡單的問題,以這個作為切入點好了 在ma...
    Mr_Baymax閱讀 2,757評論 1 17
  • 歡迎大家指出文章中需要改正或者需要補充的地方辉巡,我會及時更新恨憎,非常感謝。 一. 多線程基礎 1. 進程 進程是指在系...
    xx_cc閱讀 7,185評論 11 70
  • 在了解GCD之前郊楣,我們首先要知道幾個概念憔恳。關于隊列和同/異步函數。為了讓讀者更簡單直觀的理解這些概念净蚤,我盡可能用最...
    fou7閱讀 890評論 1 2
  • 線程的串行 一個線程中任務的執(zhí)行是串行的如果要再一個線程中執(zhí)行多個任務,那么只能一個一個的按順序執(zhí)行這些任務也就是...
    Coder007閱讀 242評論 0 2
  • demo:MultiThreadingPractice 進程:系統(tǒng)正在運行的一個應用程序橘荠,沒打開一個app系統(tǒng)就開...
    YY_Lee閱讀 470評論 0 6