線程概述
有些程序是一條直線阔涉,起點到終點;有些程序是一個圓蛾默,不斷循環(huán)懦铺,直到將它切斷
一個運行著的程序就是一個進程或者叫做一個任務,一個進程至少包含一個線程支鸡,線程就是程序的執(zhí)行流冬念。Mac和iOS中的程序啟動,創(chuàng)建好一個進程的同時牧挣, 一個線程便開始運行急前,這個線程叫主線程。主線程在程序中的地位和其他線程不同瀑构,它是其他線程最終的父線程裆针,且所有界面的顯示操作即AppKit或 UIKit的操作必須在主線程進行。
系統(tǒng)中的每一個進程都有自己獨立的虛擬內(nèi)存空間寺晌,而同一個進程中的多個線程則共用進程的內(nèi)存空間世吨。每創(chuàng)建一個新的線程,都需要一些內(nèi)存(如每個線程有自己的Stack空間)和消耗一定的CPU時間呻征。另外當多個線程對同一個資源出現(xiàn)爭奪的時候需要注意線程安全問題
多線程的實現(xiàn)原理:雖然在同一時刻耘婚,CPU只能處理1條線程,但是CPU可以快速地在多條線程之間調(diào)度(切換)陆赋,造成了多線程并發(fā)執(zhí)行的假象沐祷。
多線程的優(yōu)點
能適當提高程序的執(zhí)行效率嚷闭。
能適當提高資源利用率(CPU、內(nèi)存利用率)戈轿。
多線程的缺點
創(chuàng)建線程是需要成本的:iOS下主要成本包括:在椓枋埽空間的子線程512KB、主線程1MB思杯,創(chuàng)建線程大約需要90毫秒的創(chuàng)建時間胜蛉。
線程越多,CPU在調(diào)度線程上的開銷就越大色乾。
線程越多誊册,程序設計就越復雜:因為要考慮到線程之間的通信,多線程的數(shù)據(jù)共享暖璧。
下面開始擼代碼:
------------------------------------------------------------------華麗的分割線
1.耗時操作的問題演示
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self longOperation];
}
- (void)longOperation
{
NSLog(@"start");
NSTimeInterval start = CACurrentMediaTime();
for (int i = 0; i < 10000000; i++) {
// 存儲在棧區(qū)
// int num = 10;
// 存儲在常量區(qū)
// NSString *str1 = @"hello";
// 存儲在堆區(qū)
// NSString *str2 = [NSString stringWithFormat:@"hello_%d",i];
// I/O操作 : 把數(shù)據(jù)從內(nèi)存輸出到外接設備,或者由外接設備輸入到內(nèi)存;
NSLog(@"%d",i);
}
NSLog(@"over %f", CACurrentMediaTime() - start);
}
結(jié)論
- 空的for循環(huán)不耗時
- 操作內(nèi)存的棧區(qū)速度很快;棧區(qū)存儲空間地址是連續(xù)的;
- 操作內(nèi)存的常量區(qū)速度很快;內(nèi)存空間只開辟一次;
- 操作內(nèi)存的堆區(qū)速度相對棧區(qū)和常量區(qū)要慢些;堆區(qū)內(nèi)存空間不連續(xù),需要尋址;
- I/O操作是很耗時的; (把數(shù)據(jù)從內(nèi)存輸出到外接設備,或者由外接設備輸入到內(nèi)存)
- 耗時操作對UI交互的影響 : 卡死了主屏幕,直到耗時操作執(zhí)行完,屏幕的交互才能正常進行;
- 解決耗時操作卡頓UI的辦法 : 多線程技術(shù);
- 學習多線程的目的 : 把耗時操作放在后臺執(zhí)行,不讓耗時操作卡頓UI;
2.解決耗時操作卡頓UI的辦法
使用多線程
技術(shù) : 解決屏幕卡死的問題
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// [self longOperation];
// 使用多線程技術(shù)
[self performSelectorInBackground:@selector(longOperation) withObject:nil];
}
3.多線程基本概念
同步 & 異步
同步
和異步
是任務執(zhí)行的兩種方式
-
同步
- 我們之前寫程序的時候代碼都是從上往下,順序執(zhí)行的,就叫做
同步執(zhí)行
. - 1個人執(zhí)行多個任務,是要依次執(zhí)行的.因為1個人同一時間只能執(zhí)行1個任務.
-
多個任務按序依次執(zhí)行
,就是同步執(zhí)行
.
- 我們之前寫程序的時候代碼都是從上往下,順序執(zhí)行的,就叫做
-
異步
-
多個任務同時執(zhí)行
,就是異步執(zhí)行
. - 異步是多線程的代名詞.
- 我們學習多線程就是為了實現(xiàn)如何讓任務異步執(zhí)行.
-
進程 & 線程
-
進程
- 在系統(tǒng)中
正在運行
的一個應用程序叫進程. - 通過
活動監(jiān)視器
可以查看MAC系統(tǒng)中正在運行
的所有應用程序. - 每個進程之間都是
獨立
的,均運行在其專用
且受保護
的內(nèi)存空間內(nèi). - 兩個進程之間是無法通信的,迅雷無法幫助酷我下載正在播放的音樂.
-
進程
可以類比成正在正常運營
的公司
.
- 在系統(tǒng)中
-
線程
- 線程可以類比成公司中的
員工
. - 進程要想執(zhí)行任務,必須要有線程,且每個進程至少有一條線程.
- 線程是進程的
基本執(zhí)行單元
,進程中的所有任務都在線程中執(zhí)行. - 程序啟動(進程開啟)會默認開啟一條線程.
- 1個進程中可以有多個線程.
- 線程可以類比成公司中的
多線程
-
多線程 : 一個進程中可以開啟多條線程,多條線程可以
**同時**
執(zhí)行不同的任務. - 進程-公司案怯,線程-員工,老板是什么澎办?
- 多線程可以解決程序阻塞的問題
- 多線程可以提高程序的執(zhí)行效率,給用戶良好的使用體驗.
- 比如,酷我音樂的邊下載邊聽歌,迅雷的邊下載邊播放.
4.多線程執(zhí)行原理
-
單核CPU
同一時間,CPU只能處理1個線程,只有1個線程在執(zhí)行任務. 多線程的同時執(zhí)行 : 其實是CPU在多條線程之間快速切換(調(diào)度任務).
- 如果CPU調(diào)度線程的速度足夠快,就造成了多線程
**同時**
執(zhí)行的**假象**
- 如果線程非常多,CPU會在多條線程之間不斷的調(diào)度任務,結(jié)果就是消耗了大量的CPU資源,CPU會累趴下.
- 每個線程調(diào)度的頻率會降低
- 線程的執(zhí)行效率會下降
5.多線程優(yōu)缺點
優(yōu)點
- 能"適當"提高程序的執(zhí)行效率.
- 能"適當"提高CPU和內(nèi)存的利用率.
- 線程上的任務執(zhí)行完成后,線程會自動銷毀,節(jié)省內(nèi)存.
缺點
- 開啟線程需要占用一定的內(nèi)存空間,如果開啟的線程過多,會占用大量的CPU資源,降低程序的性能
- 占用內(nèi)存空間:默認情況下,子線程512KB,主線程1M.PS:iOS8中,主線程512KB.
- 線程越多,CPU調(diào)度線程的開銷就越大.
- 時間開銷
- 空間開銷
- 程序設計更加復雜:比如線程之間的通信,多線程的數(shù)據(jù)共享
6.主線程
-
一個程序運行后,默認會開啟1個線程,稱為
主線程
或UI線程
.- 關(guān)注
main
函數(shù)的執(zhí)行
- 關(guān)注
主線程一般用來
刷新UI界面
,處理UI事件
.處理UI事件
:點擊
嘲碱、滾動
、拖拽
等事件-
主線程使用注意
- 別將耗時的操作放到主線程中
- 耗時操作會卡住主線程,嚴重影響UI的流暢度,給用戶一種卡的壞體驗,影響UI交互質(zhì)量.
7.多線程的實現(xiàn)方案
(二)創(chuàng)建線程三種方式
1.準備新線程執(zhí)行的方法
- (void)demo:(id)obj
{
NSLog(@"傳入?yún)?shù) => %@",obj);
NSLog(@"hello %@",[NSThread currentThread]);
}
2.對象方法創(chuàng)建
- 實例化線程對象的同時指定線程執(zhí)行的方法
@selector(demo:)
. - 需要
手動開啟線程
.
- (void)threadDemo1
{
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(demo:) object:@"alloc"];
// 手動啟動線程
[thread start];
}
3.類方法創(chuàng)建
- 分離出一個線程,并且
自動開啟線程
執(zhí)行@selector(demo:)
. - 無法獲取到線程對象
- (void)threadDemo2
{
[NSThread detachNewThreadSelector:@selector(demo:) toTarget:self withObject:@"detach"];
}
4.NSObject(NSThreadPerformAdditions)
的分類創(chuàng)建
- 方便任何繼承自
NSObject
的對象,都可以很容易的調(diào)用線程方法 - 無法獲取到線程對象
-
自動開啟線程
執(zhí)行@selector(demo:)
.
- (void)threadDemo3
{
[self performSelectorInBackground:@selector(demo:) withObject:@"perform"];
}
5.總結(jié)
- 以上三種創(chuàng)建線程的方式,各有不同.隨意選擇.
- 使用哪種方式需要根據(jù)具體的需求而定.比如 : 如果需要線程對象,就使用對象方法創(chuàng)建.
(三)target和selector的關(guān)系
1.target和selector的關(guān)系分析
-
target
: 指方法從屬于的對象.- 比如 : 本對象--
self
;其他對象--self.person
.
- 比如 : 本對象--
-
@selector
: 指對象里面的方法.- 比如 : 要執(zhí)行的是
self
中或者self.person
中的哪個方法.
- 比如 : 要執(zhí)行的是
-
提示 : 不要看見
target
就寫self
. -
target
和@selector
的關(guān)系 : 執(zhí)行哪個對象上的哪個方法.
2.代碼演練
準備Person對象
@interface Person : NSObject
/// 人名
@property (nonatomic,copy) NSString *name;
/// 創(chuàng)建人的構(gòu)造方法
+ (instancetype)personWithDict:(NSDictionary *)dict;
/// 人有個方法
- (void)personDemo:(id)obj;
@end
@implementation Person
+ (instancetype)personWithName:(NSString *)name
{
Person *person = [[Person alloc] init];
person.name = name;
return person;
}
- (void)personDemo:(id)obj
{
NSLog(@"創(chuàng)建的人名 => %@",self.name);
NSLog(@"hello %@",[NSThread currentThread]);
}
@end
控制器中的使用
定義屬性
@interface ViewController ()
@property (nonatomic,strong) Person *person;
@end
懶加載Person
@implementation ViewController
- (Person *)person
{
if (_person==nil) {
_person = [Person personWithName:@"zhangjie"];
}
return _person;
}
新的實例化方法
使用
self
調(diào)用@selector(personDemo:)
就會崩潰.因為self
中沒有@selector(personDemo:)
.分類方法
// 崩潰
[self performSelectorInBackground:@selector(personDemo:) withObject:@"perform"];
// 正確的調(diào)用方式
[self.person performSelectorInBackground:@selector(personDemo:) withObject:@"perform"];
- 類方法
// 崩潰
[NSThread detachNewThreadSelector:@selector(personDemo:) toTarget:self withObject:@"detach"];
// 正確的調(diào)用方式
[NSThread detachNewThreadSelector:@selector(personDemo:) toTarget:self.person withObject:@"detach"];
- 對象方法
// 崩潰
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(personDemo:) object:@"alloc"];
// 手動開啟線程
[thread start];
// 正確的調(diào)用方式
NSThread *thread = [[NSThread alloc] initWithTarget:self.person selector:@selector(personDemo:) object:@"alloc"];
// 手動開啟線程
[thread start];
(四)線程狀態(tài)-生命周期
線程生命周期的控制
- 新建
- 內(nèi)存中創(chuàng)建了一個線程對象
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadDemo) object:nil];
- 就緒
- 將線程放進
可調(diào)度線程池
,等待被CPU調(diào)度
- 將線程放進
[thread start];
-
運行
- CPU負責調(diào)度 可調(diào)度線程池 中的處于 就緒狀態(tài) 的線程
- 線程執(zhí)行結(jié)束之前,狀態(tài)可能會在 就緒狀態(tài) 和 運行狀態(tài) 之間來回的切換
- 就緒狀態(tài) 和 運行狀態(tài) 之間的狀態(tài)切換由CPU來完成,程序員無法干涉
-
阻塞
-
正在運行的線程,當滿足某個條件時,可以用
休眠
或者鎖
來阻塞線程的執(zhí)行- sleepForTimeInterval:休眠指定時長
[NSThread sleepForTimeInterval:1.0];
- sleepUntilDate:休眠到指定日期
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
- 互斥鎖
@synchronized(self)
-
-
死亡
- 正常死亡:線程執(zhí)行結(jié)束
- 非正常死亡
- 程序突然崩潰
- 當滿足某個條件后局蚀,在線程內(nèi)部強制線程退出,調(diào)用
exit
方法
代碼演練
創(chuàng)建線程對象和就緒狀態(tài)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 新建狀態(tài)
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadDemo) object:nil];
// 就緒狀態(tài) : 將線程放進"可調(diào)度線程池",等待被CPU調(diào)度.
[thread start];
}
新線程執(zhí)行的方法
- (void)threadDemo
{
// 提示 : 能執(zhí)行到這里說明線程是運行狀態(tài)
NSLog(@"%@",[NSThread currentThread]);
// 使當前線程休眠2秒鐘 : 休眠指定時長
[NSThread sleepForTimeInterval:2.0];
NSLog(@"第一次睡醒");
// 使當前線程休眠到指定日期
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0]];
NSLog(@"第二次睡醒");
// 使當前線程退出 : 當前線程一旦退出,后續(xù)的所有代碼都不會執(zhí)行
// 注意 : 該方法不能在主線程使用,會使主線程退出
[NSThread exit];
NSLog(@"沒戲了?");
}
關(guān)于exit
的結(jié)論
- 使
當前線程
退出. - 不能在主線程中調(diào)用該方法.會使主線程退出.
-
當前線程死亡
之后,這個線程中的剩下的所有代碼都不會被執(zhí)行. - 在調(diào)用此方法之前一定要注意釋放之前由C語言框架創(chuàng)建的對象.
- 調(diào)用
exit
方法屬于在線程內(nèi)部取消線程,有時候需要在線程外部,當某一條件滿足時就取消線程
線程的取消 (在線程執(zhí)行的方法的外部取消)
- 取消線程的方法
- (void)cancel NS_AVAILABLE(10_5, 2_0);
- 使用
[thread cancel]
- 這個方法只是修改了線程的狀態(tài)而已,并沒有真正的取消線程
- 如果想真正的取消線程需要在線程執(zhí)行的過程中判斷線程的狀態(tài)是否是已取消
- 如果該線程已經(jīng)被取消,就直接返回,不再執(zhí)行后面的代碼
if ([NSThread currentThread].isCancelled) {
NSLog(@"該線程已經(jīng)被取消");
return;
}
(五)線程屬性
1.常用屬性
-
name
- 線程名稱- 設置線程名稱可以當線程執(zhí)行的方法內(nèi)部出現(xiàn)異常時麦锯,記錄異常和當前線程
-
stackSize
- 棧區(qū)大小- 默認情況下,無論是主線程還是子線程琅绅,棧區(qū)大小都是
512K
- 棧區(qū)大小可以設置
[NSThread currentThread].stackSize = 1024 * 1024;
- 必須是 4KB 的倍數(shù)
- 默認情況下,無論是主線程還是子線程琅绅,棧區(qū)大小都是
isMainThread
- 是否主線程-
threadPriority
- 線程優(yōu)先級- 優(yōu)先級扶欣,是一個浮點數(shù),取值范圍從
0~1.0
-
1.0
表示優(yōu)先級最高 -
0.0
表示優(yōu)先級最低 - 默認優(yōu)先級是
0.5
- 優(yōu)先級高只是保證
CPU
調(diào)度的可能性會高
- 優(yōu)先級扶欣,是一個浮點數(shù),取值范圍從
-
qualityOfService
- 服務質(zhì)量(iOS 8.0 推出)-
NSQualityOfServiceUserInteractive
- 用戶交互千扶,例如繪圖或者處理用戶事件 -
NSQualityOfServiceUserInitiated
- 用戶需要 -
NSQualityOfServiceUtility
- 實用工具料祠,用戶不需要立即得到結(jié)果 -
NSQualityOfServiceBackground
- 后臺 -
NSQualityOfServiceDefault
- 默認,介于用戶需要和實用工具之間
-
關(guān)于優(yōu)先級和服務質(zhì)量
- 多線程的目的:是將耗時的操作放在后臺澎羞,不阻塞主線程和用戶的交互髓绽!
- 多線程開發(fā)的原則:簡單
- 在開發(fā)時,最好不要修改優(yōu)先級妆绞,不要相信 用戶交互 服務質(zhì)量
- 內(nèi)核調(diào)度算法在決定該運行哪個線程時梧宫,會把線程的優(yōu)先級作為考量因素
* 較高優(yōu)先級的線程會比較低優(yōu)先級的線程具有更多的運行機會
* 較高優(yōu)先級不保證你的線程具體執(zhí)行的時間,只是相比較低優(yōu)先級的線程摆碉,更有可能被調(diào)度器選擇執(zhí)行而已
2.代碼演示
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"主線程棧區(qū)空間大小 == %tu KB 是否是主線程 %zd",[NSThread currentThread].stackSize / 1024,[NSThread currentThread].isMainThread);
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];
// 給線程起名字
thread1.name = @"download A";
// 設置線程優(yōu)先級
thread1.threadPriority = 1.0;
// 線程就緒
[thread1 start];
NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];
thread2.name = @"download B";
thread2.threadPriority = 0;
[thread2 start];
}
- (void)demo
{
NSLog(@"子線程棧區(qū)空間大小 == %tu KB 是否是主線程 %zd",[NSThread currentThread].stackSize / 1024,[NSThread currentThread].isMainThread);
for (int i = 0; i < 10; i++) {
NSLog(@"%@",[NSThread currentThread]);
}
}
3.補充
-
NSInteger
有符號整數(shù)(有正負數(shù))用%zd
-
NSUInteger
無符號整數(shù)(沒有負數(shù))用%tu
- 是為了
自適應32位和64位CPU
的架構(gòu).
(六)線程安全-資源共享
1.多線程操作共享資源的問題
-
共享資源
- 資源 : 一個全局的對象、一個全局的變量脓豪、一個文件.
- 共享 : 可以被多個對象訪問.
- 共享資源 :可以被多個對象訪問的資源.比如全局的對象,變量,文件.
在
多線程
的環(huán)境下,共享的資源
可能會被多個線程共享
巷帝,也就是多個線程可能會操作同一塊資源.當多個線程操作同一塊資源時,很容易引發(fā)數(shù)據(jù)錯亂和數(shù)據(jù)安全問題,數(shù)據(jù)有可能丟失,有可能增加,有可能錯亂.
經(jīng)典案例 : 賣票.
-
線程安全
- 同一塊資源,被多個線程同時讀寫操作時,任然能夠得到正確的結(jié)果,稱之為線程是安全的.
開發(fā)提示
- 實際開發(fā)中確定開發(fā)思路邏輯比及時的寫代碼更重要.
- 多線程開發(fā)的復雜度相對較高,在開發(fā)時可以按照以下套路編寫代碼
- 首先確保單個線程執(zhí)行正確
- 然后再添加線程
代碼實現(xiàn)賣票邏輯
- 先定義共享資源
@interface ViewController ()
/// 總票數(shù)(共享的資源)
@property (nonatomic,assign) int tickets;
@end
- 初始化余票數(shù)
共享資源
- (void)viewDidLoad {
[super viewDidLoad];
// 設置余票數(shù)
self.tickets = 20;
}
- 賣票邏輯實現(xiàn)
- (void)saleTickets
{
// while 循環(huán)保證每個窗口都可以單獨把所有的票賣完
while (YES) {
// 判斷是否有票
if (self.tickets>0) {
// 模擬網(wǎng)絡延遲 : 放大出錯時的效果,沒有實際意義
[NSThread sleepForTimeInterval:1.0];
// 有票就賣一張
self.tickets--;
// 賣完一張票就提示用戶余票數(shù)
NSLog(@"剩余票數(shù) => %zd %@",self.tickets,[NSThread currentThread]);
} else {
// 沒有就提示用戶
NSLog(@"沒票了");
// 此處要結(jié)束循環(huán),不然會死循環(huán)
break;
}
}
}
單線程
- 先確保單線程中運行正常
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 在主線程中賣票
[self saleTickets];
}
多線程
- 如果單線程運行正常,就修改代碼,實現(xiàn)多線程環(huán)境
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 在主線程中賣票
// [self saleTickets];
// 售票口 A
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
thread1.name = @"售票口 A";
[thread1 start];
// 售票口 B
NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
thread2.name = @"售票口 B";
[thread2 start];
}
資源搶奪結(jié)果
-
數(shù)據(jù)錯亂,數(shù)據(jù)增加.
出錯原因分析
2.解決多線程操作共享資源的問題
- 解決辦法 : 使用
互斥鎖/同步鎖
.
添加互斥鎖
- (void)saleTickets
{
// while 循環(huán)保證每個窗口都可以單獨把所有的票賣完
while (YES) {
// 添加互斥鎖
@synchronized(self) {
// 判斷是否有票
if (self.tickets>0) {
// 模擬網(wǎng)絡延遲 : 放大出錯時的效果,沒有實際意義
[NSThread sleepForTimeInterval:1.0];
// 有票就賣一張
self.tickets--;
// 賣完一張票就提示用戶余票數(shù)
NSLog(@"剩余票數(shù) => %zd",self.tickets);
} else {
// 沒有就提示用戶
NSLog(@"沒票了");
// 此處要結(jié)束循環(huán),不然會死循環(huán)
break;
}
}
}
}
互斥鎖小結(jié)
- 互斥鎖,就是使用了線程同步技術(shù).
- 同步鎖/互斥鎖:可以保證被鎖定的代碼,同一時間,只能有一個線程可以操作.
-
self
:鎖對象,任何繼承自NSObject的對像都可以是鎖對象,因為內(nèi)部都有一把鎖,而且默認是開著的. - 鎖對象 : 一定要是全局的鎖對象,要保證所有的線程都能夠訪問,
self
是最方便使用的鎖對象. - 互斥鎖鎖定的范圍應該盡量小,但是一定要鎖住資源的
讀寫
部分. - 加鎖后程序執(zhí)行的效率比不加鎖的時候要低.因為線程要等待解鎖.
- 犧牲了性能保證了安全性.
(七)原子屬性
1.原子屬性相關(guān)概念
nonatomic
: 非原子屬性-
atomic
: 原子屬性- 線程安全的,針對多線程設計的屬性修飾符,是默認值.
- 特點 : 單寫多讀
- **單寫多讀 : **保證同一時間,只有一個線程能夠執(zhí)行
setter
方法,但是可以有多個線程執(zhí)行getter
方法. -
atomic
屬性的setter
里面里面有一把鎖,叫做自旋鎖
. - 原子屬性的
setter
方法是線程安全的;但是,getter
方法不是線程安全的.
-
nonatomic
和atomic
對比-
nonatomic
: 非線程安全,適合內(nèi)存小的移動設備. -
atomic
: 線程安全,需要消耗大量的資源.性能比非原子屬性要差一點兒點兒.
-
2.模擬原子屬性
模擬原子屬性的核心思想 : 在屬性的
setter
方法里面加鎖.但是getter
方法里面不加鎖;定義屬性
/// 非原子屬性
@property (nonatomic,strong) NSObject *obj1;
/// 原子屬性:內(nèi)部有"自旋鎖"
@property (atomic,strong) NSObject *obj2;
/// 用于模擬原子屬性
@property (atomic,strong) NSObject *obj3;
- 重寫非原子屬性的
setter
和getter
方法- 重寫了原子屬性的
setter
方法之后,會覆蓋原子屬性內(nèi)部的自旋鎖
,使其失效.然后我們加入互斥鎖
,來模擬單寫多讀
. - 重寫了屬性的
setter
和getter
方法之后,系統(tǒng)就不會再幫我們生成待下劃線的成員變量.使用合成指令@synthesize
,就可以手動的生成帶下劃線的成員變量.
- 重寫了原子屬性的
3.模擬原子屬性
// 合成指令
@synthesize obj3 = _obj3;
/// obj3的setter方法
- (void)setObj3:(NSObject *)obj3
{
// 使用互斥鎖替代看不見的自旋鎖
@synchronized(self) {
_obj3 = obj3;
}
}
/// obj3的getter方法
- (NSObject *)obj3
{
return _obj3;
}
4.性能測試
/// 測試"非原子屬性","互斥鎖","自旋鎖"的性能
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSInteger largeNum = 1000*1000;
NSLog(@"非原子屬性");
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
for (int i = 0; i < largeNum; i++) {
self.obj1 = [[NSObject alloc] init];
}
NSLog(@"非原子屬性 => %f",CFAbsoluteTimeGetCurrent()-start);
NSLog(@"原子屬性");
start = CFAbsoluteTimeGetCurrent();
for (int i = 0; i < largeNum; i++) {
self.obj2 = [[NSObject alloc] init];
}
NSLog(@"原子屬性 => %f",CFAbsoluteTimeGetCurrent()-start);
NSLog(@"模擬原子屬性");
start = CFAbsoluteTimeGetCurrent();
for (int i = 0; i < largeNum; i++) {
self.obj3 = [[NSObject alloc] init];
}
NSLog(@"模擬原子屬性 => %f",CFAbsoluteTimeGetCurrent()-start);
}
測試結(jié)果
5.互斥鎖和自旋鎖對比
共同點
- 都能夠保證同一時間,只有一條線程執(zhí)行鎖定范圍的代碼
不同點
-
互斥鎖
:如果發(fā)現(xiàn)有其他線程正在執(zhí)行鎖定的代碼,線程會進入休眠
狀態(tài),等待其他線程執(zhí)行完畢,打開鎖之后,線程會重新進入就緒
狀態(tài).等待被CPU重新調(diào)度. -
自旋鎖
:如果發(fā)現(xiàn)有其他線程正在執(zhí)行鎖定的代碼,線程會以死循環(huán)
的方式,一直等待鎖定代碼執(zhí)行完成.
6.開發(fā)建議
- 所有屬性都聲明為
nonatomic
,原子屬性和非原子屬性的性能幾乎一樣. - 盡量避免多線程搶奪同一塊資源.
- 要實現(xiàn)線程安全,必須要用到
鎖
.無論什么鎖,都是有性能消耗的. - 自旋鎖更適合執(zhí)行非常短的代碼.死循環(huán)內(nèi)部不適合寫復雜的代碼.
- 盡量將加鎖,資源搶奪的業(yè)務邏輯交給服務器端處理,減小移動客戶端的壓力.
- 為了流暢的用戶體驗,UIKit類庫的線程都是不安全的,所以我們需要在主線程(UI線程)上更新UI.
- 所有包含
NSMutable
的類都是線程不安全的.在做多線程開發(fā)的時候,需要注意多線程同時操作可變對象的線程安全問題.
(八)NSThread線程間通信
1.ATS
使用http地址時Xcode會認為不夠安全從而保存,為解決此問題需要在info文件的Xml文件內(nèi)添加下列代碼
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
2.代碼實現(xiàn)
定義屬性
@interface ViewController ()
/// 滾動視圖
@property (nonatomic,strong) UIScrollView *scrollView;
/// 圖片視圖
@property (nonatomic,weak) UIImageView *imageView;
@end
loadView
方法復習
- 當
self.view == nil
時,會調(diào)用; - 先于
viewDidLoad
調(diào)用; - 一旦重寫了這個方法,
storyboard
里面就不會去加載根視圖了;
加載視圖層次
- (void)loadView
{
// 創(chuàng)建滾動視圖
self.scrollView = [[UIScrollView alloc] initWithFrame:[UIScreen mainScreen].bounds];
// 將滾動視圖設置成根視圖
self.view = self.scrollView;
self.scrollView.backgroundColor = [UIColor redColor];
// 創(chuàng)建圖片視圖
UIImageView *imageView = [[UIImageView alloc] init];
[self.view addSubview:imageView];
self.imageView = imageView;
}
異步下載圖片
- (void)viewDidLoad {
[super viewDidLoad];
// 主線程中下載圖片
// [self downloadImageData];
// 開啟新線程異步下載圖片
[self performSelectorInBackground:@selector(downloadImageData) withObject:nil];
}
下載圖片主方法
- (void)downloadImageData
{
// 圖片資源地址
NSURL *url = [NSURL URLWithString:@"http://h.hiphotos.baidu.com/image/pic/item/c995d143ad4bd1130c0ee8e55eafa40f4afb0521.jpg"];
// 所有的網(wǎng)絡數(shù)據(jù)都是以二進制的形式傳輸?shù)?所以用NSData來接受
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
// 回到主線程更新UI
// waitUntilDone:是否等待主線程中的`updateUIwWithImage`方法執(zhí)行結(jié)束再執(zhí)行"下一行代碼",一般設置成NO,不用等待
[self performSelectorOnMainThread:@selector(updateUIwWithImage:) withObject:image waitUntilDone:NO];
// 測試 waitUntilDone:
NSLog(@"下一行代碼");
}
刷新UI
- (void)updateUIwWithImage:(UIImage *)imgae
{
NSLog(@"updateUIwWithImage");
// 設置圖片視圖
self.imageView.image = image;
// 設置圖片視圖的大小跟圖片一般大
[self.imageView sizeToFit];
// 設置滾動視圖的滾動:滾動范圍跟圖片一樣大
[self.scrollView setContentSize:image.size];
}
線程間通信
- 因為多線程共享地址空間和數(shù)據(jù)空間
所以一個線程的數(shù)據(jù)可以直接提供給其他線程使用,叫做線程間通信;