iOS底層探索 -- 多線程
1. 多線程概念問題
-
線程的定義
- 線程是進程的基本單元,一個進程的所有任務都在線程中執行
- 進程要想執行任務,必須得有線程,進程至少有有一條線程
- 程序啓動默認會開啓一條線程,這條線程被稱爲主線程或者UI線程
-
進程的定義
- 進程是指在系統中正在運行的一個應用程序
- 每個進程之間是互相獨立的,每個進程均運行在其專用的且受保護的內存中
-
進程和線程的關係
-
地址空間:同一進程的線程共享本進程的地址空間,而進程之間相互獨立
-
資源擁有:同一進程的線程共享本進程的資源(如:內存、CPU),進程之間是相互獨立的
-
一個進程崩潰會,在保護模式下不會對其他進程產生影響,但是一個線程崩潰,整個進程都死掉,所以,多進程要不多線程健壯(iOS沒有多進程)
-
進程切換是,消耗的資源大,效率高,所以涉及頻繁切換進程時,使用線程要優於進程。同樣如果要求同時進行並且又要共享某些變量的併發造作,只能用線程
-
執行過程:每個獨立的進程有一個程序運行的入口,順序執行序列和程序入口,但是線程不能獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制
-
線程是處理器調度的基本單元,進程不是
-
-
多線程的意義
- 能適當提高程序的執行效率
- 能適當提高資源的利用率(CPU,內存)
- 線程上的任務執行完成後,線程會自動銷燬
- 開啓線程需要佔用一定的內存空間(默認 每個線程都佔 512K )
- 如果開啓大量的線程,會佔用大量的內存
- 程序設計更加複雜,比如線程之間的通訊,多線程共享數據
-
線程和
Runloop
的關係runloop
與線程是一一對應的,一個runloop
對應一個核心的線程(runloop
是可以嵌套的,但是核心的線程只能有一個,他們的關係保存在一個字典裏)runloop
是管理線程的,當線程的runloop
被開啓後,線程會在執行完任務後進入休眠狀態,有任務會被喚醒去執行任務runloop
在第一次獲取是被常見,在線程結束是銷燬- 對主線程來說,
runloop
在程序一啓動就默認創建好 - 對於子線程來說,
runloop
是懶加載的,只有當我們使用的時候纔會創建,所以在子線程用定時器要注意,確保子線程的runloop
被創建,不然定時器不會回調。
-
atomic
和nonatomic
的區別-
nonatomic
非原子性,非線程安全,適合內存小的移動設備 -
atomic
原子屬性(線程安全,需要消耗大量的資源),針對多線程設計的,默認值保證同一時間只有一個線程能夠寫入(可以同一時間多個線程取值)
本身是一把自旋鎖 -
建議使用
nonatomic
聲明屬性,儘量避免多線程搶奪同一資源 -
儘量將加鎖,資源搶奪的業務邏輯交給服務器處理,減少客戶端壓力
-
-
C
和OC
的橋接__bridge
只做類型轉換,不修改對象(內存)管理權__bridge_retained
將OC對象
轉換爲Core Foundation對象
,同時將對象(內存)的管理權交給我們,後續需要 使用CFRelease或者相關方法來釋放對象__bridge_transfer
將Core Foundation
的對象 轉換爲Objective-C
的對象,同時將對象(內存)的管理權交給ARC
2. 多線程原理
-
多線程原理
CUP
在單位時間片裏快速在各個線程之前切換- 多核是爲了加快執行的效率
-
多線程生命週期
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-e4PZRCdP-1588931804943)(https://user-gold-cdn.xitu.io/2020/5/8/171f23ec24188bc2?w=1167&h=394&f=png&s=70827)]
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-FaQyPWJ4-1588931804964)(https://user-gold-cdn.xitu.io/2020/5/8/171f2472ba57bdeb?w=1137&h=518&f=png&s=82193)]
飽和策略:
AbortPolicy
直接拋出RejectedExecutionExeception
異常來阻止系統正常運行CallerRunsPolicy
將任務回退到調用者DisOldestPolicy
丟掉等待最久的任務DisCardPolicy
直接丟棄任務- 這四種拒絕策略均實現的
RejectedExecutionHandler
接口
3. 端口通訊
VC
類
//1. 創建主線程的port
// 子線程通過此端口發送消息給主線程
self.myPort = [NSMachPort port];
//2. 設置port的代理回調對象
self.myPort.delegate = self;
//3. 把port加入runloop,接收port消息
[[NSRunLoop currentRunLoop] addPort:self.myPort forMode:NSDefaultRunLoopMode];
self.person = [[KCPerson alloc] init];
[NSThread detachNewThreadSelector:@selector(personLaunchThreadWithPort:)
toTarget:self.person
withObject:self.myPort];
- (void)handlePortMessage:(NSPortMessage *)message{
NSLog(@"VC == %@",[NSThread currentThread]);
NSLog(@"從person 傳過來一些信息:");
//會報錯,沒有這個隱藏屬性
//NSLog(@"from == %@",[message valueForKey:@"from"]);
NSArray *messageArr = [message valueForKey:@"components"];
NSString *dataStr = [[NSString alloc] initWithData:messageArr.firstObject encoding:NSUTF8StringEncoding];
NSLog(@"傳過來一些信息 :%@",dataStr);
NSPort *destinPort = [message valueForKey:@"remotePort"];
if(!destinPort || ![destinPort isKindOfClass:[NSPort class]]){
NSLog(@"傳過來的數據有誤");
return;
}
NSData *data = [@"VC收到!!!" dataUsingEncoding:NSUTF8StringEncoding];
NSMutableArray *array =[[NSMutableArray alloc]initWithArray:@[data,self.myPort]];
// 非常重要,如果你想在Person的port接受信息,必須加入到當前主線程的runloop
[[NSRunLoop currentRunLoop] addPort:destinPort forMode:NSDefaultRunLoopMode];
NSLog(@"VC == %@",[NSThread currentThread]);
BOOL success = [destinPort sendBeforeDate:[NSDate date]
msgid:10010
components:array
from:self.myPort
reserved:0];
NSLog(@"%d",success);
}
Person
類
- (void)personLaunchThreadWithPort:(NSPort *)port{
NSLog(@"VC 響應了Person裏面");
@autoreleasepool {
//1. 保存主線程傳入的port
self.vcPort = port;
//2. 設置子線程名字
[[NSThread currentThread] setName:@"KCPersonThread"];
//3. 開啓runloop
[[NSRunLoop currentRunLoop] run];
//4. 創建自己port
self.myPort = [NSMachPort port];
//5. 設置port的代理回調對象
self.myPort.delegate = self;
//6. 完成向主線程port發送消息
[self sendPortMessage];
}
}
/**
* 完成向主線程發送port消息
*/
- (void)sendPortMessage {
NSData *data1 = [@"Gavin" dataUsingEncoding:NSUTF8StringEncoding];
NSMutableArray *array =[[NSMutableArray alloc]initWithArray:@[data1,self.myPort]];
// 發送消息到VC的主線程
// 第一個參數:發送時間。
// msgid 消息標識。
// components,發送消息附帶參數。
// reserved:爲頭部預留的字節數
[self.vcPort sendBeforeDate:[NSDate date]
msgid:10086
components:array
from:self.myPort
reserved:0];
}
#pragma mark - NSMachPortDelegate
- (void)handlePortMessage:(NSPortMessage *)message{
NSLog(@"person:handlePortMessage == %@",[NSThread currentThread]);
NSLog(@"從VC 傳過來一些信息:");
NSLog(@"components == %@",[message valueForKey:@"components"]);
NSLog(@"receivePort == %@",[message valueForKey:@"receivePort"]);
NSLog(@"sendPort == %@",[message valueForKey:@"sendPort"]);
NSLog(@"msgid == %@",[message valueForKey:@"msgid"]);
NSArray *messageArr = [message valueForKey:@"components"];
NSString *dataStr = [[NSString alloc] initWithData:messageArr.firstObject encoding:NSUTF8StringEncoding];
NSLog(@"傳過來一些信息 :%@",dataStr);
}
解析:
1. 在 VC 中創建主線程的port(子線程通過此端口發消息給主線程),並設置 port 的代理回調對象
2. 將 port 加入到 runloop,接收 port 消息。
3. 實現 handlePortMessage 代理方法
4. GCD 初探
GCD
是一套純 C 語言 API
,提供了非常多的強大的函數,自動管理線程的生命週期(創建線程,調度任務,銷燬線程)。只需要將任務添加到隊列,並且指定執行任務的函數
任務是由block
封裝的(無參數,無返回值)
執行任務的函數有兩種:
- 異步
dispatch_async
,不用等待當前語句執行完畢,就可以執行下一條語句,會開闢新的線程執行任務 - 同步
dispatch_sync
,必須等待當前語句執行完畢,纔會執行下一條語句,不會開闢新的線程,在當前線程執行block
任務
隊列分爲兩種:
- 串行隊列
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-fdzsliej-1588931804967)(https://user-gold-cdn.xitu.io/2020/5/8/171f325d8797889b?w=627&h=222&f=png&s=32586)]
- 併發隊列
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-96uuEc6I-1588931804979)(https://user-gold-cdn.xitu.io/2020/5/8/171f3260da4cf0a3?w=742&h=291&f=png&s=38456)]
那麼還有下面兩種隊列:主隊列和全局隊列
-
主隊列
dispatch_get_main_queue()
- 專門用來在住線程上調度任務的隊列,不會開啓線程
- 如果當前住線程正在有任務執行,那麼無論主隊列中當前添加了什麼任務都不會被調度
-
全局隊列
dispatch_get_global_queue()
- 是一個併發隊列
- 在使用多線程開發是,如果隊列沒有特殊需求,在執行異步任務時,可以直接使用全局隊列
隊列和函數組合有以下四種情況:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-q6RsjtHz-1588931804981)(https://user-gold-cdn.xitu.io/2020/5/8/171f32c21bc01ac7?w=1030&h=513&f=png&s=76597)]
接下來,看一下
GCD
的經典面試題
- (void)textDemo{
dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_async(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
// 1 5 2 4 3
}
上面代碼打印結果是什麼?
其實上面的代碼很簡單,打印結果是1 5 2 4 3
分析:
首先是打印 1 ,然後是併發隊列加異步函數,這個操作是異步耗時,所以先執行打印 5,
然後是耗時操作,打印 2 ,接着又是併發隊列加異步函數,同理先打印 4, 在打印3。
接着看下面的題目
dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_CONCURRENT);
/***
1 2 3
0
7 8 9
*/
dispatch_async(queue, ^{
// sleep(2);
NSLog(@"1");
});
dispatch_async(queue, ^{
NSLog(@"2");
});
// 堵塞 - 護犢子
dispatch_sync(queue, ^{
NSLog(@"3");
});
// **********************
NSLog(@"0");
dispatch_async(queue, ^{
NSLog(@"7");
});
dispatch_async(queue, ^{
NSLog(@"8");
});
dispatch_async(queue, ^{
NSLog(@"9");
});
// A: 1230789
// B: 1237890
// C: 3120798
// D: 2137890
上面的結果是A C
。
分析:
首先是併發隊列,然後是兩個異步耗時操作,緊接着是一個同步函數,這樣會堵塞後面的任務
的執行,再接着是兩個異步操作。
所以,0 是在中間的,前面 1、2、3和後面7、8、9,順序未知(依賴於任務複雜度和cpu的調度)
接着看下面的題目
下面的代碼打印什麼?
dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
上面的結果是1 5 2
,然後死鎖崩潰
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-9fAnpBP5-1588931804983)(https://user-gold-cdn.xitu.io/2020/5/8/171f3abb629de0e6?w=603&h=316&f=png&s=28382)]
分析:
首先是串行隊列,先打印1,然後是異步併發,耗時操作, 先打印 5,然後將打
印 2 的任務加到隊列,然後將代碼塊加入隊列,再將打印4 的任務加入隊列,然後將同步
打印3 的任務加入隊列,
而任務 4 的執行,必須在代碼塊執行完之後,而代碼塊是同步,必須等任務3執行完,
而任務 3,在等待任務 4 執行完,就造成了相互等待,死鎖的問題
同理,將打印 4,放到同步函數前,一樣會死鎖
dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
// 異步函數
dispatch_async(queue, ^{
NSLog(@"2");
NSLog(@"4");
dispatch_sync(queue, ^{
NSLog(@"3");
});
});
NSLog(@"5");