Dispatch Source學(xué)習(xí)

關(guān)于Dispatch Source

Dispatch Source是GCD中的一種基本數(shù)據(jù)類型,從字面意思可稱其為調(diào)度源,它用于處理特定的系統(tǒng)底層事件,即:當(dāng)一些特定的系統(tǒng)底層事件發(fā)生時,調(diào)度源會捕捉到這些事件,然后可以做相應(yīng)的邏輯處理。

Dispatch Source可用來監(jiān)聽以下幾類事件:

  • Timer Dispatch Source:定時調(diào)度源。
  • Signal Dispatch Source:監(jiān)聽UNIX信號調(diào)度源,比如監(jiān)聽代表掛起指令的SIGSTOP信號。
  • Descriptor Dispatch Source:監(jiān)聽文件相關(guān)操作和Socket相關(guān)操作的調(diào)度源。
  • Process Dispatch Source:監(jiān)聽進(jìn)程相關(guān)狀態(tài)的調(diào)度源。
  • Mach port Dispatch Source:監(jiān)聽Mach相關(guān)事件的調(diào)度源。
  • Custom Dispatch Source:監(jiān)聽自定義事件的調(diào)度源。

使用Dispatch Source時,通常是先指定一個希望監(jiān)聽的系統(tǒng)事件類型,再指定一個捕獲到事件后進(jìn)行邏輯處理的閉包或者函數(shù)作為回調(diào)函數(shù),然后再指定一個該回調(diào)函數(shù)執(zhí)行的Dispatch Queue即可。當(dāng)監(jiān)聽到指定的系統(tǒng)事件發(fā)生時,Dispatch Source會將已指定的回調(diào)函數(shù)作為一個任務(wù)放入指定的隊列中執(zhí)行,也就是說當(dāng)監(jiān)聽到系統(tǒng)事件后就會觸發(fā)一個任務(wù),并自動將其加入隊列執(zhí)行。
這里與通常的手動添加任務(wù)的模式不同,一旦將Diaptach Source與Dispatch Queue關(guān)聯(lián)后,只要監(jiān)聽到系統(tǒng)事件,Dispatch Source就會自動將任務(wù)(回調(diào)函數(shù))添加到關(guān)聯(lián)的隊列中,直到我們調(diào)用函數(shù)取消監(jiān)聽。

為了保證監(jiān)聽到事件后回調(diào)函數(shù)能夠都到執(zhí)行,已關(guān)聯(lián)的Dispatch Queue會被Diaptach Source強引用。

有些時候回調(diào)函數(shù)執(zhí)行的時間較長,在這段時間內(nèi)Dispatch Source又監(jiān)聽到多個系統(tǒng)事件,理論上就會形成事件積壓,但好在Dispatch Source有很好的機制解決這個問題,當(dāng)有多個事件積壓時會根據(jù)事件類型,將它們進(jìn)行關(guān)聯(lián)和結(jié)合,形成一個新的事件。

Dispatch Source類型

Dispatch Source一共可以監(jiān)聽六類事件,根據(jù)同類事件的不同操作,Dispatch Source分為11個類型:

  • DISPATCH_SOURCE_TYPE_DATA_ADD:屬于自定義事件,可以通過dispatch_source_get_data函數(shù)獲取事件變量數(shù)據(jù),在我們自定義的方法中可以調(diào)用dispatch_source_merge_data函數(shù)向Dispatch Source設(shè)置數(shù)據(jù),下文中會有詳細(xì)的演示。
  • DISPATCH_SOURCE_TYPE_DATA_OR:屬于自定義事件,用法同上面的類型一樣。
  • DISPATCH_SOURCE_TYPE_MACH_SEND:Mach端口發(fā)送事件。
  • DISPATCH_SOURCE_TYPE_MACH_RECV:Mach端口接收事件。
  • DISPATCH_SOURCE_TYPE_PROC:與進(jìn)程相關(guān)的事件。
  • DISPATCH_SOURCE_TYPE_READ:讀文件事件。
  • DISPATCH_SOURCE_TYPE_WRITE:寫文件事件。
  • DISPATCH_SOURCE_TYPE_VNODE:文件屬性更改事件。
  • DISPATCH_SOURCE_TYPE_SIGNAL:接收信號事件。
  • DISPATCH_SOURCE_TYPE_TIMER:定時器事件。
  • DISPATCH_SOURCE_TYPE_MEMORYPRESSURE:內(nèi)存壓力事件。

創(chuàng)建Dispatch Source

使用dispatch_source_create函數(shù)創(chuàng)建Dispatch Source,函數(shù)原型如下:


dispatch_source_t dispatch_source_create(dispatch_source_type_t type,
                                         uintptr_t handle,
                                         unsigned long mask,
                                         dispatch_queue_t _Nullable queue);

dispatch_source_create函數(shù)有四個參數(shù):

  • type:指定Dispatch Source類型,共有11個類型,特定的類型監(jiān)聽特定的事件。
  • handle:取決于要監(jiān)聽的事件類型,比如如果是監(jiān)聽Mach端口相關(guān)的事件,那么該參數(shù)就是mach_port_t類型的Mach端口號,如果是監(jiān)聽事件變量數(shù)據(jù)類型的事件那么該參數(shù)就不需要,設(shè)置為0就可以了。
  • mask:取決于要監(jiān)聽的事件類型,比如如果是監(jiān)聽文件屬性更改的事件,那么該參數(shù)就標(biāo)識文件的哪個屬性,比如DISPATCH_VNODE_RENAME
    。
  • queue:設(shè)置回調(diào)函數(shù)所在的隊列。

<h1 id = "jumpid1">設(shè)置事件處理器

前文中提到過,當(dāng)Dispatch Source監(jiān)聽到事件時會調(diào)用指定的回調(diào)函數(shù)或閉包,該回調(diào)函數(shù)或閉包就是Dispatch Source的事件處理器。我們可以使用dispatch_source_set_event_handler或dispatch_source_set_event_handler_f函數(shù)給創(chuàng)建好的Dispatch Source設(shè)置處理器,前者是設(shè)置閉包形式的處理器,后者是設(shè)置函數(shù)形式的處理器:

既然是事件處理器,那么肯定需要獲取一些Dispatch Source的信息,GCD提供了三個在處理器中獲取Dispatch Source相關(guān)信息的函數(shù),比如handle、mask。而且針對不同類型的Dispatch Source,這三個函數(shù)返回數(shù)據(jù)的值和類型都會不一樣,下面來看看這三個函數(shù):

  • dispatch_source_get_handle:這個函數(shù)用于獲取在創(chuàng)建Dispatch Source時設(shè)置的第二個參數(shù)handle。
    • 如果是讀寫文件的Dispatch Source,返回的就是描述符。
    • 如果是信號類型的Dispatch Source,返回的是int類型的信號數(shù)。
    • 如果是進(jìn)程類型的Dispatch Source,返回的是pid_t類型的進(jìn)程id。
    • 如果是Mach端口類型的Dispatch Source,返回的是mach_port_t類型的Mach端口。
  • dispatch_source_get_data:該函數(shù)用于獲取Dispatch Source監(jiān)聽到事件的相關(guān)數(shù)據(jù)。
    • 如果是讀文件類型的Dispatch Source,返回的是讀到文件內(nèi)容的字節(jié)數(shù)。
    • 如果是寫文件類型的Dispatch Source,返回的是文件是否可寫的標(biāo)識符,正數(shù)表示可寫,負(fù)數(shù)表示不可寫。
    • 如果是監(jiān)聽文件屬性更改類型的Dispatch Source,返回的是監(jiān)聽到的有更改的文件屬性,用常量表示,比如DISPATCH_VNODE_RENAME等。
    • 如果是進(jìn)程類型的Dispatch Source,返回監(jiān)聽到的進(jìn)程狀態(tài),用常量表示,比如DISPATCH_PROC_EXIT等。
    • 如果是Mach端口類型的Dispatch Source,返回Mach端口的狀態(tài),用常量表示,比如DISPATCH_MACH_SEND_DEAD等。
    • 如果是自定義事件類型的Dispatch Source,返回使用dispatch_source_merge_data函數(shù)設(shè)置的數(shù)據(jù)。
  • dispatch_source_get_mask:該函數(shù)用于獲取在創(chuàng)建Dispatch Source時設(shè)置的第三個參數(shù)mask。在進(jìn)程類型,文件屬性更改類型,Mach端口類型的Dispatch Source下該函數(shù)返回的結(jié)果與dispatch_source_get_data一樣。

設(shè)置取消處理器

取消處理器就是當(dāng)Dispatch Source被釋放時用來處理一些后續(xù)事情,比如關(guān)閉文件描述符或者釋放Mach端口等。我們可以使用dispatch_source_set_cancel_handler函數(shù)或者dispatch_source_set_cancel_handler_f函數(shù)給Dispatch Source注冊取消處理器。

更改目標(biāo)隊列

在上文中,我們說過可以使用dispatch_source_create函數(shù)創(chuàng)建Dispatch Source,并且在創(chuàng)建時會指定回調(diào)函數(shù)執(zhí)行的隊列,那么如果事后想更改隊列,比如說想更改隊列的優(yōu)先級,這時我們可以使用dispatch_set_target_queue函數(shù)實現(xiàn)。

這里需要注意的是,如果在更改目標(biāo)隊列時,Dispatch Source已經(jīng)監(jiān)聽到相關(guān)事件,并且回調(diào)函數(shù)已經(jīng)在之前的隊列中執(zhí)行了,那么會一直在舊的隊列中執(zhí)行完成,不會轉(zhuǎn)移到新的隊列中去。

恢復(fù)與暫停Dispatch Source

暫停Dispatch Source使用dispatch_suspend函數(shù);恢復(fù)Dispatch Source使用dispatch_resume函數(shù)。

需要注意的是,因為Dispatch Source創(chuàng)建之后,需要進(jìn)行一些配置,比如設(shè)置事件處理器等,所以剛創(chuàng)建好的Dispatch Source是處于暫停狀態(tài)的,因此使用時需要用dispatch_resume函數(shù)將其啟動。

廢棄Dispatch Source

如果我們不再需要使用某個Dispatch Source時,可以使用dispatch_source_cancel函數(shù)廢除,該函數(shù)只有一個參數(shù),那就是目標(biāo)Dispatch Source。

關(guān)聯(lián)用戶數(shù)據(jù)

設(shè)置事件處理器中已提到獲取Dispatch Source信息的幾個函數(shù),但是對于自定義事件類型的Dispatch Source,dispatch_source_merge_data函數(shù)設(shè)置的數(shù)據(jù)為unsigned long類型,因而通過dispatch_source_get_data獲取的數(shù)據(jù)也只支持unsigned long類型,嚴(yán)重影響了自定義類型事件的應(yīng)用范圍。
對于這一點,可以通過給Dispatch Source關(guān)聯(lián)用戶數(shù)據(jù)的來解決:

void dispatch_set_context(dispatch_object_t object, void *context);

這個函數(shù)可以給自定義事件類型的Dispatch Source關(guān)聯(lián)任何我們需要的數(shù)據(jù),當(dāng)執(zhí)行事件處理器時,調(diào)用dispatch_get_context就能獲取最近一次關(guān)聯(lián)的數(shù)據(jù)。

注意,如果關(guān)聯(lián)了用戶數(shù)據(jù),那么不再需要這個數(shù)據(jù)時就應(yīng)當(dāng)及時釋放。

Dispatch Source實踐

定時器

使用定時器時需要調(diào)用 dispatch_source_set_timer函數(shù)來配置定時器,這個函數(shù)有四個參數(shù):

  • source:待配置的定時器類型的 Dispatch Source

  • start:控制定時器第一次觸發(fā)的時刻。參數(shù)類型是 dispatch_time_t,這是一個opaque類型,我們不能直接操作它。我們得需要 dispatch_time 和 dispatch_walltime 函數(shù)來創(chuàng)建它們。另外,常量 DISPATCH_TIME_NOW 和 DISPATCH_TIME_FOREVER 通常很有用。

  • interval:觸發(fā)間隔

  • leeway:定時器進(jìn)度,單位納秒;如果設(shè)為0,系統(tǒng)只是最大程度滿足精度需求。精度越高功耗越大。


 - (void)timerDispatchSource
{
    dispatch_source_t timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,0, 0, dispatch_get_global_queue(0, 0));
    
    if (timerSource)
    {
        dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, 0 * NSEC_PER_SEC);  //1, the timer dispatch source uses the default system clock to determine when to fire. However, the default clock does not advance while the computer is asleep.
//        dispatch_time_t startTime = dispatch_walltime(NULL, 0 * NSEC_PER_SEC); //2  the timer dispatch source tracks its firing time to the wall clock time
        
        NSString *desc = timerSource.description;
        dispatch_source_set_timer(timerSource, startTime, 1 * NSEC_PER_SEC, 0);
        dispatch_source_set_event_handler(timerSource, ^{
            static NSInteger i = 0;
            ++i;
            NSLog(@"Timer %@ Task: %ld",desc,i);
//            NSLog(@"Timer %@ Task: %ld",timerSource,i);
            
            
        });
        dispatch_source_set_cancel_handler(timerSource, ^{
//            NSLog(@"Timer:%@ canceled",timerSource);
            NSLog(@"Timer:%@ canceled",desc);
        });
        dispatch_resume(timerSource);
    }
    
    _myTimerSource = timerSource; ///< 必須要保存,除非在hander中引用timerSource,否則出了作用域,Timer就會被釋放
}
         

NSTimer 與 GCD Timer比較
NSTimer

  • 依賴NSRunloop
  • 容易導(dǎo)致內(nèi)存泄漏
  • NSTimer的創(chuàng)建與撤銷必須在同一個線程操作、 performSelector的創(chuàng)建與撤銷必須在同一個線程操作

GCD Timer

  • 可以被當(dāng)做對象放入數(shù)組或字典中
  • GCD Timer必須強引用,否則出了棧就會失效,這種失效不會觸發(fā)取消處理器
  • GCD Timer精度可控
  • 如果使用dispatch_walltime來設(shè)置定時器的起始時間,定時器默認(rèn)使用walltime來觸發(fā)定時器;如果使用dispatch_time來設(shè)置定時器的起始時間,定時器默認(rèn)使用系統(tǒng)時鐘來觸發(fā)定時器,然而當(dāng)計算機休眠時,系統(tǒng)時鐘也是休眠的。對于時間間隔比較大的定時器,使用dispatch_walltime來設(shè)置定時器的起始時間

監(jiān)聽信號


- (void)signalDispatchSource
{
    signal(SIGCHLD, SIG_IGN);
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_source_t signalSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGCHLD, 0, queue);
    
    if (signalSource)
    {
        dispatch_source_set_event_handler(signalSource, ^{
            static NSInteger i = 0;
            ++i;
            NSLog(@"Signal Detected: %ld",i);
        });
        dispatch_source_set_cancel_handler(signalSource, ^{
            NSLog(@"Signal canceled");
        });
    
        dispatch_resume(signalSource);
    }
    
    _mySignalSource = signalSource; // 不能省,原因同定時器
}

注意
* SIGILL, SIGBUS,  SIGSEGV不能監(jiān)聽
* 只是監(jiān)聽信號,并不處理信號

讀寫文件


- (void)writeDispatchSource
{
    NSString *filePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *fileName = [filePath stringByAppendingString:@"/test.txt"];
    int fd = open([fileName UTF8String], O_WRONLY | O_CREAT | O_TRUNC,
                  (S_IRUSR | S_IWUSR | S_ISUID | S_ISGID));
    NSLog(@"Write fd:%d",fd);
    if (fd == -1)
        return ;
    fcntl(fd, F_SETFL); // Block during the write.
    
    dispatch_source_t writeSource = nil;
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE,fd, 0, queue);
    
    dispatch_source_set_event_handler(writeSource, ^{
        size_t bufferSize = 100;
        void *buffer = malloc(bufferSize);
        
        static NSString *content = @"Write Data Action: ";
        content = [content stringByAppendingString:@"=New info="];
        
        NSString *writeContent = [content stringByAppendingString:@"\n"];
        void *string = [writeContent UTF8String];
        size_t actual = strlen(string);
        memcpy(buffer, string, actual);
        
        write(fd, buffer, actual);
        NSLog(@"Write to file Finished");
        
        free(buffer);
        // Cancel and release the dispatch source when done.
        //        dispatch_source_cancel(writeSource);
        dispatch_suspend(writeSource);  //不能省,否則只要文件可寫,寫操作會一直進(jìn)行,直到磁盤滿,本例中,只要超過buffer容量就會崩潰
//        close(fd);   //會崩潰
    });
    dispatch_source_set_cancel_handler(writeSource, ^{
        NSLog(@"Write to file Canceled");
        close(fd);
    });

    if (!writeSource)
    {
        close(fd);
        return;
    }

    _myWriteSource = writeSource;
}

- (void)readDataDispatchSource
{
    if (_myReadSource)
    {
        dispatch_source_cancel(_myReadSource);
    }
    
    NSString *filePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *fileName = [filePath stringByAppendingString:@"/test.txt"];
    // Prepare the file for reading.
    int fd = open([fileName UTF8String], O_RDONLY);
    NSLog(@"read fd:%d",fd);
    if (fd == -1)
        return ;
    fcntl(fd, F_SETFL, O_NONBLOCK);  // Avoid blocking the read operation
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_source_t readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fd, 0, dispatch_get_main_queue());
    if (!readSource)
    {
        close(fd);
        return ;
    }
    
    
    // Install the event handler
    //只要文件寫入了新內(nèi)容,就會自動讀入新內(nèi)容
    dispatch_source_set_event_handler(readSource, ^{
        long estimated = dispatch_source_get_data(readSource);
        NSLog(@"Read From File, estimated length: %ld",estimated);
        if (estimated < 0)
        {
            NSLog(@"Read Error:");
            dispatch_source_cancel(readSource);  //如果文件發(fā)生了截短,事件處理器會一直不停地重復(fù)
        }
        
        // Read the data into a text buffer.
        char *buffer = (char *)malloc(estimated);
        if (buffer)
        {
            ssize_t actual = read(fd, buffer, (estimated));
            NSLog(@"Read From File, actual length: %ld",actual);
            NSLog(@"Readed Data: \n%s",buffer);
//            Boolean done = MyProcessFileData(buffer, actual);  // Process the data.
            
            // Release the buffer when done.
            free(buffer);
            
            // If there is no more data, cancel the source.
//            if (done)
//                dispatch_source_cancel(readSource);
        }
    });
    
    // Install the cancellation handler
    dispatch_source_set_cancel_handler(readSource, ^{
        NSLog(@"Read from file Canceled");
        close(fd);
    });
    
    // Start reading the file.
    dispatch_resume(readSource);
    
    _myReadSource = readSource; //can be omitted
}

注意:
* 確保非阻塞方式進(jìn)行讀寫,否則當(dāng)讀寫出錯的時候,會導(dǎo)致線程阻塞

自定義事件

以更新progressView進(jìn)度為例,定時器每0.05觸發(fā)一次,隨機更新progressView的進(jìn)度


- (void)customDispatchSourceForProgressView
{
    static dispatch_source_t timerSource = nil;
    static dispatch_source_t source = nil;
    
    _progressView.progress = 0;
    if (timerSource)
    {
        dispatch_source_cancel(timerSource);
        timerSource = nil;
        source = nil;
    }
    else
    {
        source =dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
        dispatch_resume(source);
        dispatch_source_set_event_handler(source, ^{
        // 方法1,獲取dispatch_source_merge_data傳入的值,只支持unsigned long類型
        // 如果出現(xiàn)合并,那么得到的值是合并的那幾次提交傳入的值的累加結(jié)果
            unsigned long data = dispatch_source_get_data(source);
            CGFloat accumulate = ((CGFloat)data)/100;
            CGFloat progress = _progressView.progress;
            progress += accumulate;
            
            //方法2,獲取關(guān)聯(lián)數(shù)據(jù)
//            char *c = dispatch_get_context(source);
//            NSLog(@"%c",*c);
//            NSNumber *num = (__bridge NSNumber *)(dispatch_get_context(source));
//            NSLog(@"%@",num);

            _progressView.progress = progress;
            if (progress >= 1)
            {
                timerSource = nil;
                source = nil;
            }
        });
        dispatch_async(dispatch_get_global_queue(0, 0), ^
                       {
                           timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,0, 0, dispatch_get_global_queue(0, 0));
                           if (timerSource)
                           {
                               dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, 0 * NSEC_PER_SEC);
                               
                               dispatch_source_set_timer(timerSource, startTime, 0.05 * NSEC_PER_SEC, 0);
                               dispatch_source_set_event_handler(timerSource, ^{
                                   
                                   //關(guān)聯(lián)用戶自定義信息
                                   int i = rand()%5;
//                                   char c = i + 65;
//                                   dispatch_set_context(source, &c);
//                                   NSNumber *num = @(i);
//                                   dispatch_set_context(source, (__bridge void * _Nullable)(num));
                                   
                                   dispatch_source_merge_data(source, i);
                               });
                               dispatch_resume(timerSource);
                           }
                       });
    }
}

參考

  1. 蘋果官方文檔
  2. 選擇 GCD 還是 NSTimer
  3. iOS dispatch_source_t的理解](http://www.cnblogs.com/wjw-blog/p/5903441.html))
  4. iOS_GCD_講解三_Dispatch Sources
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容