OC底層原理二十八:Dispatch_source & Synchronized

OC底層原理 學(xué)習(xí)大綱

上節(jié)對源碼進(jìn)行了深耕,看官與作者都辛苦??,本節(jié)較為輕松通孽,主要分析dispatch_sourcesynchronized鎖。

  1. dispatch_source源
  2. synchronized鎖
  3. 面試題分析

準(zhǔn)備工作:

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é)果如下:
image.png

源的類型有很多烘豌,大家可以自行嘗試。其中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);
    
}
image.png

上述是一個最簡單示例廊佩,完整的計時器代碼囚聚,可在?? 這里下載

Q:Dispatch_source_t的計時器與NSTimerCADisplayLink比較标锄?

1. NSTimer

  • 存在延遲顽铸,與RunLoopRunLoop 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é)論:
  1. @synchronized鎖的對象很關(guān)鍵雕薪,它需要保障生命周期
    (因為被鎖對象一旦不存在了,會導(dǎo)致解鎖晓淀,失去所袁,鎖內(nèi)代碼就不安全了。)

  2. @synchronized是一把遞歸互斥鎖凶掰。鎖的內(nèi)部結(jié)構(gòu)如下:

    image.png

  • 接下來我們從兩個方面來分析@synchronized
  1. @synchronized的使用
  2. @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
image.png

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:

image.png

  • 可以看到objc_sync_enter鎖進(jìn)入和objc_sync_exit鎖退出關(guān)鍵函數(shù)银受。

clang編譯文件,也可以看到objc_sync_enterobjc_sync_exit

image.png

image.png

objc_sync_enter處加斷點鸦采,運(yùn)行到此處時宾巍,

image.png

  • 運(yùn)行到此處時Ctrl + 鼠標(biāo)左鍵點擊進(jìn)入內(nèi)部
    Ctrl + 鼠標(biāo)左鍵 點擊

image.png

再進(jìn)入內(nèi)部,可以看到代碼是在libobjc.A.dylib庫中:
image.png

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

image.png

一共分為三步進(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):

    1. ACQUIRE 進(jìn)行中: 當(dāng)前線程內(nèi)任務(wù)數(shù)加1券盅,更新相應(yīng)數(shù)據(jù)
    2. RELEASE 釋放中: 當(dāng)前線程內(nèi)任務(wù)數(shù)減1更新相應(yīng)數(shù)據(jù)
    3. CHECK檢查: 啥也不干

補(bǔ)充: 每個被鎖的object對象擁有一個或多個線程膛檀。
(我們尋找線程前锰镀,都需先判斷當(dāng)前線程的持有對象object是否與鎖對象objec一致)

  • 其中fetch_cache函數(shù),是進(jìn)行緩存查詢開辟的:

createNO: 僅查詢
createYES查詢開辟/擴(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_enterobjc_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践美。

驗證:

  1. 打開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); // 信號量釋放
        });
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蜡娶,隨后出現(xiàn)的幾起案子混卵,更是在濱河造成了極大的恐慌,老刑警劉巖窖张,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件淮菠,死亡現(xiàn)場離奇詭異,居然都是意外死亡荤堪,警方通過查閱死者的電腦和手機(jī)合陵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來澄阳,“玉大人拥知,你說我怎么就攤上這事∷橛” “怎么了低剔?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長肮塞。 經(jīng)常有香客問我襟齿,道長,這世上最難降的妖魔是什么枕赵? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任猜欺,我火速辦了婚禮,結(jié)果婚禮上拷窜,老公的妹妹穿的比我還像新娘开皿。我一直安慰自己涧黄,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布赋荆。 她就那樣靜靜地躺著笋妥,像睡著了一般。 火紅的嫁衣襯著肌膚如雪窄潭。 梳的紋絲不亂的頭發(fā)上春宣,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天,我揣著相機(jī)與錄音嫉你,去河邊找鬼月帝。 笑死,一個胖子當(dāng)著我的面吹牛均抽,可吹牛的內(nèi)容都是我干的嫁赏。 我是一名探鬼主播其掂,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼油挥,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了款熬?” 一聲冷哼從身側(cè)響起深寥,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎贤牛,沒想到半個月后惋鹅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡殉簸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年闰集,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片般卑。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡武鲁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蝠检,到底是詐尸還是另有隱情沐鼠,我是刑警寧澤,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布叹谁,位于F島的核電站饲梭,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏焰檩。R本人自食惡果不足惜憔涉,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望析苫。 院中可真熱鬧监氢,春花似錦布蔗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至议街,卻和暖如春泽谨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背特漩。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工吧雹, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人涂身。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓雄卷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蛤售。 傳聞我的和親對象是個殘疾皇子丁鹉,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,933評論 2 355

推薦閱讀更多精彩內(nèi)容