上節(jié)對源碼
進(jìn)行了深耕
,看官與作者都辛苦??,本節(jié)較為輕松通孽,主要分析dispatch_source
和synchronized
鎖。
- dispatch_source源
- synchronized鎖
- 面試題分析
準(zhǔn)備工作:
- 可編譯的
objc4-781
源碼: http://www.reibang.com/p/45dc31d91000
1. dispatch_source源
-
CPU負(fù)荷
非常小
,盡量不占資源
-
任何線程
調(diào)用它的函數(shù)dispatch_source_merge_data
后产弹,會執(zhí)行DispatchSource
事先定義好的句柄
(可以把句柄簡單理解
為一個block
),這個過程叫custom event
弯囊,用戶事件痰哨。是dispatch_source
支持處理的一種事件。
句柄
是一種指向指針的指針
匾嘱。它指向
的是一個類
或結(jié)構(gòu)
斤斧,它和系統(tǒng)有很密切的關(guān)系。
HINSTANCE
實例句柄霎烙、HBITMAP
位圖句柄撬讽、HDC
設(shè)備表述句柄、HICON
圖標(biāo)句柄 等悬垃。其中還有一個通用句柄
游昼,就是HANDLE
。
常用方法:
dispatch_source_create
:創(chuàng)建源dispatch_source_set_event_handler
: 設(shè)置源事件回調(diào)dispatch_source_merge_data
:置源事件設(shè)置數(shù)據(jù)dispatch_source_get_data
:獲取源事件數(shù)據(jù)dispatch_resume
: 繼續(xù)dispatch_suspend
: 掛起dispatch_cancel
: 取消
- 通過案例熟悉一下:
(源類型為DISPATCH_SOURCE_TYPE_DATA_ADD
)
- (void)viewDidLoad {
[super viewDidLoad];
__block NSInteger totalComplete = 0;
// 創(chuàng)建串行隊列
dispatch_queue_t queue = dispatch_queue_create("ht", NULL);
// 創(chuàng)建主隊列源尝蠕,源類型為 DISPATCH_SOURCE_TYPE_DATA_ADD
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
// 設(shè)置源事件回調(diào)
dispatch_source_set_event_handler(source, ^{
NSLog(@"%@",[NSThread currentThread]);
NSUInteger value = dispatch_source_get_data(source);
totalComplete += value;
NSLog(@"進(jìn)度: %.2f", totalComplete/100.0);
});
// 開啟源事件
dispatch_resume(source);
// 發(fā)送數(shù)據(jù)源
for (int i= 0; i<100; i++) {
dispatch_async(queue, ^{
sleep(1);
// 發(fā)送源數(shù)據(jù)
dispatch_source_merge_data(source, 1);
});
}
}
- 打印結(jié)果如下:
源的類型有很多烘豌,大家可以自行嘗試。其中DISPATCH_SOURCE_TYPE_TIMER計時器
使用很頻繁:
//MARK: -ViewController
@interface ViewController ()
@property (nonatomic, strong) dispatch_source_t timer;
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, assign) double duration; // 總時長
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.duration = 10; // 總時長10秒
_queue = dispatch_queue_create("HT_dispatch_source_timer", DISPATCH_QUEUE_PRIORITY_DEFAULT);
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _queue);
// 從現(xiàn)在`DISPATCH_TIME_NOW`開始看彼,每1秒執(zhí)行一次
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0);
__block double currentDuration = self.duration;
__weak typeof(self) weakself = self;
dispatch_source_set_event_handler(_timer, ^{
dispatch_async(dispatch_get_main_queue(), ^{
if (currentDuration <= 0) {
NSLog(@"結(jié)束");
//取消
dispatch_cancel(weakself.timer);
return;
}
currentDuration--;
// 回到主線程,操作UI
NSLog(@"還需打印%.0f次",currentDuration + 1);
});
});
// 開始執(zhí)行
dispatch_resume(_timer);
}
上述是一個最簡單
的示例
廊佩,完整的計時器代碼
囚聚,可在?? 這里下載
Q:
Dispatch_source_t
的計時器與NSTimer
、CADisplayLink
比較标锄?1. NSTimer
存在延遲
顽铸,與RunLoop
和RunLoop Mode
有關(guān)
(如果Runloop
正在執(zhí)行一個連續(xù)性運(yùn)算,timer
會被延時觸發(fā)
)- 需要手動
加入RunLoop
料皇,且Model需要設(shè)置為forMode:NSCommonRunLoopMode
(NSDefaultRunLoopMode模式
跋破,觸摸事件會
讓計時器暫停
)NSTimer *timer = [NSTimer timerWithTimeInterval:5 target:self selector:@selector(timerAction) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSCommonRunLoopMode];
2. CADisplayLink
屏幕刷新時
調(diào)用CADisplayLink
,以和屏幕刷新頻率
同步的頻率將特定內(nèi)容
畫在屏幕上
的定時器類瓶蝴。
CADisplayLink
以特定模式
注冊到runloop
后毒返,每當(dāng)屏幕顯示內(nèi)容刷新結(jié)束
的時候,runloop
就會向CADisplayLink
指定的target
發(fā)送一次指定的selector
消息舷手,CADisplayLink
類對應(yīng)的selector
就會被調(diào)用一次
拧簸。所以通常情況下,按照iOS設(shè)備屏幕
的刷新率60次/秒
CADisplayLink
在正常情況下會在每次刷新結(jié)束
都被調(diào)用
男窟,精確度
相當(dāng)高
盆赤。
但如果調(diào)用的方法
比較耗時
,超過
了屏幕刷新周期
歉眷,就會
導(dǎo)致跳過
若干次回調(diào)調(diào)用機(jī)會
牺六。
如果CPU
過于繁忙
,無法保證
屏幕60次/秒
的刷新率
汗捡,就會導(dǎo)致跳過
若干次調(diào)用回調(diào)方法
的機(jī)會淑际,跳過
次數(shù)取決CPU
的忙碌程度
。3. dispatch_source_t 計時器
時間準(zhǔn)確
扇住,可以使用子線程
春缕,解決
跑在主線程
上卡UI
的問題- 不依賴
runloop
,基于系統(tǒng)內(nèi)核
進(jìn)行處理艘蹋,準(zhǔn)確性
非常高
區(qū)別
NSTimer
會受到主線程
的任務(wù)的影響
锄贼,CADisplayLink
會受到CPU負(fù)載
的影響,產(chǎn)生延遲女阀。dispatch_source_t
可以使用子線程
宅荤,而且可以使用leeway
參數(shù)指定
可以接受的誤差
來降低
資源消耗
2. synchronized鎖
- 各種類型
鎖
的耗時比較
:
image.png 鎖
,是為了確保線程安全
浸策,數(shù)據(jù)
寫入安全
冯键。- 我們在開發(fā)中使用最多的,就是
@synchronized
的榛。因為它使用方便
琼了,不用
手動解鎖
逻锐。但是它是所有鎖中最耗時
的一種夫晌。
- 我們先展示結(jié)論:
@synchronized
鎖的對象
很關(guān)鍵雕薪,它需要保障鎖
的生命周期
(因為被鎖對象
一旦不存在
了,會導(dǎo)致解鎖
晓淀,失去鎖
所袁,鎖內(nèi)代碼
就不安全了。)
@synchronized
是一把遞歸互斥鎖
凶掰。鎖的內(nèi)部結(jié)構(gòu)如下:
image.png
- 接下來我們從兩個方面來分析
@synchronized
:
-
@synchronized
的使用 -
@synchronized
源碼探究
2.1 @synchronized
的使用
- 售票案例測試:
加入@synchronized
確保內(nèi)部代碼安全
(代碼進(jìn)入
時加鎖
燥爷,代碼離開
時移除鎖
)
@interface ViewController ()
@property (nonatomic, assign) NSUInteger ticketCount;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.ticketCount = 20;
[self saleTicketDemo];
}
- (void)saleTicketDemo{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 3; i++) {
[self saleTicket];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 10; i++) {
[self saleTicket];
}
});
}
- (void)saleTicket{
@synchronized (self) {
if (self.ticketCount > 0) {
self.ticketCount--;
sleep(0.1);
NSLog(@"當(dāng)前余票還剩:%ld張",self.ticketCount);
}else{
NSLog(@"當(dāng)前車票已售罄");
}
}
}
@end
Q1:為什么
鎖定
對象寫self
?
- 因為
被鎖對象
不能提前釋放
懦窘,會觸發(fā)解鎖
操作前翎,鎖內(nèi)代碼
不安全。Q2:為什么
@synchronized
耗時嚴(yán)重畅涂?
- 因為
對象被鎖
后(比如self)港华,該對象的所有操作
,都變成了加鎖
操作午衰,為了確保鎖內(nèi)代碼安全
立宜,我們鎖了
對象(比如self)的所有操作
。- 最直接的影響是臊岸,
被鎖線程
變多橙数,執(zhí)行
操作時,查找線程
和查找任務(wù)
都變得很耗時
帅戒,而且每個被鎖線程
內(nèi)的任務(wù)
還是遞歸持有
灯帮,更耗時
。
好了逻住,結(jié)論
和原因
都解釋清楚
了施流,應(yīng)用層
知道這些就夠了。
- 如果你不僅想
知其然
鄙信,還想知其所以然
瞪醋,那么我們開始源碼探究
:
2.2 @synchronized
源碼探究
我們在@synchronized
代碼處加入斷點
,運(yùn)行代碼装诡,打開Debug
->Debug Workflow
->Always show Disassemble
:
- 可以看到
objc_sync_enter
鎖進(jìn)入和objc_sync_exit
鎖退出關(guān)鍵函數(shù)
银受。
clang編譯文件,也可以看到
objc_sync_enter
和objc_sync_exit
:
image.pngimage.png
在objc_sync_enter
處加斷點鸦采,運(yùn)行到此處時宾巍,
- 運(yùn)行到此處時
Ctrl + 鼠標(biāo)左鍵
點擊進(jìn)入內(nèi)部
:
Ctrl + 鼠標(biāo)左鍵 點擊
再進(jìn)入
內(nèi)部,可以看到代碼是在libobjc.A.dylib
庫中:2.2.1 objc_sync_enter 加鎖
- 進(jìn)入
objc4
源碼渔伯,搜索objc_sync_enter
顶霞,代碼注釋
上標(biāo)注,這是一個遞歸互斥鎖
。
image.png
- 如果
對象存在
选浑,id2data
處理數(shù)據(jù)
蓝厌,類型為ACQUIRE
,設(shè)置鎖
古徒。 - 如果
不存在
拓提,啥也不干
。
(內(nèi)部:->BREAKPOINT_FUNCTION
->調(diào)用asm("");
就是啥也沒干)
我們進(jìn)入id2data
:
一共分為
三步
進(jìn)行查找
和處理
:
【第一步】如果支持
快速緩存
隧膘,就從快速緩存
中讀取線程
和任務(wù)
代态,進(jìn)行相應(yīng)操作
并返回
。【第二步】快速緩存
沒找到
疹吃,就從線程緩存
中讀取線程
和任務(wù)
蹦疑,進(jìn)行相應(yīng)操作
并返回
。【第三步】線程緩存也
沒找到
萨驶,就循環(huán)遍歷
一個個線程
和任務(wù)
必尼,進(jìn)行相應(yīng)操作
并跳到done
。【Done】 如果
錯誤
:異常報錯
篡撵。如果正確
判莉,就存
入快速緩存
和線程緩存
中,便于
下次查找
育谬。其中【相應(yīng)操作】包括三種狀態(tài):
ACQUIRE
進(jìn)行中: 當(dāng)前線程
內(nèi)任務(wù)
數(shù)加1
券盅,更新
相應(yīng)數(shù)據(jù)
RELEASE
釋放中: 當(dāng)前線程
內(nèi)任務(wù)
數(shù)減1
,更新
相應(yīng)數(shù)據(jù)
CHECK
檢查: 啥也不干補(bǔ)充: 每個被鎖的
object對象
可擁有
一個或多個線程
膛檀。
(我們尋找線程
前锰镀,都需先判斷
當(dāng)前線程的持有對象object
是否與鎖對象objec
一致)
- 其中
fetch_cache
函數(shù),是進(jìn)行緩存查詢
和開辟
的:
create
為NO
: 僅查詢
create
為YES
:查詢
并開辟
/擴(kuò)容內(nèi)存
image.png
2.2.2 objc_sync_exit 解鎖
-
搜索
objc_sync_exit
:
image.png 如果
對象存在
咖刃,id2data
處理數(shù)據(jù)泳炉,類型為RELEASE
,嘗試解鎖
嚎杨。如果
不存在
花鹅,啥也不干
。(這次直接代碼得懶得寫了 ??)
id2data
我們在上面已經(jīng)分析過了枫浙。只是類型為RELEASE
而已刨肃。
至此,我想你應(yīng)該知道上述2個問題
的底層原理
了箩帚。
Q1:為什么
鎖定對象
寫self
真友?
因為
被鎖對象
不能提前釋放
,會觸發(fā)解鎖
操作紧帕,鎖內(nèi)代碼
不安全盔然。【補(bǔ)充】
當(dāng)對象被釋放
時,調(diào)用objc_sync_enter
和objc_sync_exit
,底層代碼
顯示:啥也不會做
愈案。這把鎖
已經(jīng)完全失去作用
了挺尾。Q2:為什么
@synchronized
耗時嚴(yán)重?
因為
對象被鎖
后(比如self)刻帚,該對象的所有操作
,都變成了加鎖
操作涩嚣,為了確保鎖內(nèi)代碼安全
崇众,我們鎖了
對象(比如self)的所有操作
。最直接的影響是航厚,
被鎖線程
變多顷歌,執(zhí)行
操作時,查找線程
和查找任務(wù)
都變得很耗時
幔睬,而且每個被鎖線程
內(nèi)的任務(wù)
還是遞歸持有
眯漩,更耗時
。【補(bǔ)充】
我們查詢?nèi)蝿?wù)
時麻顶,可能經(jīng)歷3次查詢
(快速緩存
查詢->線程緩存
查詢->遍歷所有線程
查詢)赦抖,需要尋找線程
、匹配被鎖對象
辅肾,nextData遞歸尋找任務(wù)
队萤。這些,就是耗時的點矫钓。
(self
需要處理的事務(wù)越多
要尔,占有的線程數(shù)
threadCount和每個線程
內(nèi)的鎖數(shù)量
lockCount都會越多
,查詢也更耗時
新娜。)?? 希望
補(bǔ)充內(nèi)容
赵辕,可以讓你回答
得更為專業(yè)
。
3. 面試題分享
- Q:
下面操作
造成crash
的原因?
- (void)demo {
NSLog(@"123");
for (int i = 0; i < 20000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.dataSources = [NSMutableArray array];
});
}
}
- A:觸發(fā)
set
方法概龄,set方法
本質(zhì)是新值retain
还惠,舊值release
。
dispatch_async
異步線程調(diào)用時私杜,可能造成多次release
船殉,過度釋放
,形成野指針
世蔗。所以crash
践美。
驗證:
- 打開
Zombie Objects
僵尸對象
僵尸對象
一種用來檢測內(nèi)存錯誤
(EXC_BAD_ACCESS
)的對象
,它可以捕獲
任何對嘗試訪問壞內(nèi)存
的調(diào)用
寄猩。如果給
僵尸對象
發(fā)送消息
時嫉晶,那么將在運(yùn)行期間崩潰
和輸出錯誤日志
。通過日志可以定位
到野指針對象
調(diào)用的方法
和類名
。
image.png
運(yùn)行代碼替废,錯誤日志顯示:
image.png調(diào)用
[__NSArrayM release]
時箍铭,是發(fā)送給了deallocated已析構(gòu)釋放
的對象。驗證
了我們的猜想
- 嘗試1: 加入
@synchronized (self.dataSources)
鎖:
- (void)demo {
NSLog(@"123");
self.dataSources = [NSMutableArray array];
for (int i = 0; i < 20000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (self.dataSources) { // 這是【錯誤實例】
self.dataSources = [NSMutableArray array];
}
});
}
}
發(fā)現(xiàn)還是
Crash
椎镣。是否知道原因
诈火?你是【學(xué)會了】
還是【學(xué)廢了】
??
- 這個問題答案,就是本文
Q1問題
的答案
状答。- 因為
synchronized
鎖的對象是self.dataSources
冷守,它釋放了
等于這把鎖形同虛設(shè)
。
synchronized
鎖的對象惊科,需要確保鎖內(nèi)代碼
的聲明周期
拍摇。所以將鎖對象
改為self
。就解決問題了馆截。
- (void)demo {
NSLog(@"123");
self.dataSources = [NSMutableArray array];
for (int i = 0; i < 20000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (self) { // 這是【正確實例】但耗時高
self.dataSources = [NSMutableArray array];
}
});
}
}
- 可以使用其他鎖來代替
@synchronized
充活,如:NSLock
- (void)demo {
NSLog(@"123");
self.dataSources = [NSMutableArray array];
NSLock * lock = [NSLock new]; // 創(chuàng)建
for (int i = 0; i < 20000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lock]; // 加鎖
self.dataSources = [NSMutableArray array];
[lock unlock]; // 解鎖
});
}
}
- 使用
dispatch_semaphore
信號量:
- (void)demo {
NSLog(@"123");
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1); // 設(shè)置信號量(同時最多執(zhí)行1個任務(wù))
for (int i = 0; i < 20000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // 信號量等待
self.dataSources = [NSMutableArray array];
dispatch_semaphore_signal(semaphore); // 信號量釋放
});
}
}