iOS 多線程:『RunLoop』詳盡總結

来源:https://www.cnblogs.com/jiuyi/archive/2019/02/25/10432894.html
-Advertisement-
Play Games

1. RunLoop 簡介 1.1 什麼是 RunLoop? 可以理解為字面意思:Run 表示運行,Loop 表示迴圈。結合在一起就是運行的迴圈的意思。哈哈,我更願意翻譯為『跑圈』。直觀理解就像是不停的跑圈。 RunLoop 實際上是一個對象,這個對象在迴圈中用來處理程式運行過程中出現的各種事件(比 ...


1. RunLoop 簡介

1.1 什麼是 RunLoop?

可以理解為字面意思:Run 表示運行,Loop 表示迴圈。結合在一起就是運行的迴圈的意思。哈哈,我更願意翻譯為『跑圈』。直觀理解就像是不停的跑圈。

  • RunLoop 實際上是一個對象,這個對象在迴圈中用來處理程式運行過程中出現的各種事件(比如說觸摸事件、UI刷新事件、定時器事件、Selector事件),從而保持程式的持續運行。
  • RunLoop 在沒有事件處理的時候,會使線程進入睡眠模式,從而節省 CPU 資源,提高程式性能。

1.2 RunLoop 和線程

RunLoop 和線程是息息相關的,我們知道線程的作用是用來執行特定的一個或多個任務,在預設情況下,線程執行完之後就會退出,就不能再執行任務了。這時我們就需要採用一種方式來讓線程能夠不斷地處理任務,並不退出。所以,我們就有了 RunLoop。

  1. 一條線程對應一個RunLoop對象,每條線程都有唯一一個與之對應的 RunLoop 對象。
  2. RunLoop 並不保證線程安全。我們只能在當前線程內部操作當前線程的 RunLoop 對象,而不能在當前線程內部去操作其他線程的 RunLoop 對象方法。
  3. RunLoop 對象在第一次獲取 RunLoop 時創建,銷毀則是線上程結束的時候。
  4. 主線程的 RunLoop 對象系統自動幫助我們創建好了(原理如 1.3 所示),而子線程的 RunLoop對象需要我們主動創建和維護。

1.3 預設情況下主線程的 RunLoop 原理

我們在啟動一個iOS程式的時候,系統會調用創建項目時自動生成的 main.m 的文件。main.m文件如下所示:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

其中 UIApplicationMain 函數內部幫我們開啟了主線程的 RunLoop,UIApplicationMain 內部擁有一個無限迴圈的代碼,只要程式不退出/崩潰,它就一直迴圈。上邊的代碼中主線程開啟 RunLoop 的過程可以簡單的理解為如下代碼:

int main(int argc, char * argv[]) {        
    BOOL running = YES;
    do {
        // 執行各種任務,處理各種事件
        // ......
    } while (running);  // 判斷是否需要退出

    return 0;
}

從上邊可看出,程式一直在 do-while 迴圈中執行,所以 UIApplicationMain 函數一直沒有返回,我們在運行程式之後程式不會馬上退出,會保持持續運行狀態。

下圖是蘋果官方給出的 RunLoop 模型圖。

  官方 RunLoop 模型圖

從上圖中可以看出,RunLoop 就是線程中的一個迴圈,RunLoop 會在迴圈中會不斷檢測,通過 Input sources(輸入源)和 Timer sources(定時源)兩種來源等待接受事件;然後對接受到的事件通知線程進行處理,併在沒有事件的時候讓線程進行休息。

2. RunLoop 相關類

下麵我們來瞭解一下Core Foundation框架下關於 RunLoop 的 5 個類,只有弄懂這幾個類的含義,我們才能深入瞭解 RunLoop 的運行機制。

  1. CFRunLoopRef:代表 RunLoop 的對象
  2. CFRunLoopModeRef:代表 RunLoop 的運行模式
  3. CFRunLoopSourceRef:就是 RunLoop 模型圖中提到的輸入源 / 事件源
  4. CFRunLoopTimerRef:就是 RunLoop 模型圖中提到的定時源
  5. CFRunLoopObserverRef:觀察者,能夠監聽 RunLoop 的狀態改變

下邊詳細講解下幾種類的具體含義和關係。

先來看一張表示這 5 個類的關係圖幫助理解(來源:http://blog.ibireme.com/2015/05/18/runloop/)。

  RunLoop相關類關係圖

接著來講解這 5 個類的相互關係:

一個RunLoop對象(CFRunLoopRef)中包含若幹個運行模式(CFRunLoopModeRef)。而每一個運行模式下又包含若幹個輸入源(CFRunLoopSourceRef)、定時源(CFRunLoopTimerRef)、觀察者(CFRunLoopObserverRef)。

  • 每次 RunLoop 啟動時,只能指定其中一個運行模式(CFRunLoopModeRef),這個運行模式(CFRunLoopModeRef)被稱作當前運行模式(CurrentMode)。
  • 如果需要切換運行模式(CFRunLoopModeRef),只能退出當前 Loop,再重新指定一個運行模式(CFRunLoopModeRef)進入。
  • 這樣做主要是為了分隔開不同組的輸入源(CFRunLoopSourceRef)、定時源(CFRunLoopTimerRef)、觀察者(CFRunLoopObserverRef),讓其互不影響 。

下邊我們來詳細講解下這五個類:

2.1 CFRunLoopRef 類

CFRunLoopRef 是 Core Foundation 框架下 RunLoop 對象類。我們可通過以下方式來獲取 RunLoop 對象:

  • Core Foundation
    • CFRunLoopGetCurrent(); // 獲得當前線程的 RunLoop 對象
    • CFRunLoopGetMain(); // 獲得主線程的 RunLoop 對象

當然,在Foundation 框架下獲取 RunLoop 對象類的方法如下:

  • Foundation
    • [NSRunLoop currentRunLoop]; // 獲得當前線程的 RunLoop 對象
    • [NSRunLoop mainRunLoop]; // 獲得主線程的 RunLoop 對象

2.2 CFRunLoopModeRef

系統預設定義了多種運行模式(CFRunLoopModeRef),如下:

  1. kCFRunLoopDefaultMode:App的預設運行模式,通常主線程是在這個運行模式下運行
  2. UITrackingRunLoopMode:跟蹤用戶交互事件(用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他Mode影響)
  3. UIInitializationRunLoopMode:在剛啟動App時第進入的第一個 Mode,啟動完成後就不再使用
  4. GSEventReceiveRunLoopMode:接受系統內部事件,通常用不到
  5. kCFRunLoopCommonModes:偽模式,不是一種真正的運行模式(後邊會用到)

其中kCFRunLoopDefaultModeUITrackingRunLoopModekCFRunLoopCommonModes是我們開發中需要用到的模式,具體使用方法我們在 2.3 CFRunLoopTimerRef 中結合CFRunLoopTimerRef來演示說明。

2.3 CFRunLoopTimerRef

CFRunLoopTimerRef是定時源(RunLoop模型圖中提到過),理解為基於時間的觸發器,基本上就是NSTimer(哈哈,這個理解就簡單了吧)。

下麵我們來演示下CFRunLoopModeRef和CFRunLoopTimerRef結合的使用用法,從而加深理解。

  1. 首先我們新建一個iOS項目,在Main.storyboard中拖入一個Text View。
  2. 在ViewController.m文件中加入以下代碼,Demo中請調用[self ShowDemo1];來演示。
- (void)viewDidLoad {
    [super viewDidLoad];

    // 定義一個定時器,約定兩秒之後調用self的run方法
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

    // 將定時器添加到當前RunLoop的NSDefaultRunLoopMode下
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}

- (void)run
{
    NSLog(@"---run");
}
  1. 然後運行,這時候我們發現如果我們不對模擬器進行任何操作的話,定時器會穩定的每隔2秒調用run方法列印。

  2. 但是當我們拖動Text View滾動時,我們發現:run方法不列印了,也就是說NSTimer不工作了。而當我們鬆開滑鼠的時候,NSTimer就又開始正常工作了。

這是因為:

  • 當我們不做任何操作的時候,RunLoop處於NSDefaultRunLoopMode下。
  • 而當我們拖動Text View的時候,RunLoop就結束NSDefaultRunLoopMode,切換到了UITrackingRunLoopMode模式下,這個模式下沒有添加NSTimer,所以我們的NSTimer就不工作了。
  • 但當我們鬆開滑鼠的時候,RunLoop就結束UITrackingRunLoopMode模式,又切換回NSDefaultRunLoopMode模式,所以NSTimer就又開始正常工作了。

你可以試著將上述代碼中的[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];語句換為[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];,也就是將定時器添加到當前RunLoop的UITrackingRunLoopMode下,你就會發現定時器只會在拖動Text View的模式下工作,而不做操作的時候定時器就不工作。

那難道我們就不能在這兩種模式下讓NSTimer都能正常工作嗎?

當然可以,這就用到了我們之前說過的偽模式(kCFRunLoopCommonModes),這其實不是一種真實的模式,而是一種標記模式,意思就是可以在打上Common Modes標記的模式下運行。

那麼哪些模式被標記上了Common Modes呢?

NSDefaultRunLoopModeUITrackingRunLoopMode

所以我們只要我們將NSTimer添加到當前RunLoop的kCFRunLoopCommonModes(Foundation框架下為NSRunLoopCommonModes)下,我們就可以讓NSTimer在不做操作和拖動Text View兩種情況下愉快的正常工作了。

具體做法就是講添加語句改為[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

既然講到了NSTimer,這裡順便講下NSTimer中的scheduledTimerWithTimeInterval方法和RunLoop的關係。添加下麵的代碼:

[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

這句代碼調用了scheduledTimer返回的定時器,NSTimer會自動被加入到了RunLoop的NSDefaultRunLoopMode模式下。這句代碼相當於下麵兩句代碼:

NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

2.4 CFRunLoopSourceRef

CFRunLoopSourceRef是事件源(RunLoop模型圖中提到過),CFRunLoopSourceRef有兩種分類方法。

  • 第一種按照官方文檔來分類(就像RunLoop模型圖中那樣):
    • Port-Based Sources(基於埠)
    • Custom Input Sources(自定義)
    • Cocoa Perform Selector Sources
  • 第二種按照函數調用棧來分類:
    • Source0 :非基於Port
    • Source1:基於Port,通過內核和其他線程通信,接收、分發系統事件

這兩種分類方式其實沒有區別,只不過第一種是通過官方理論來分類,第二種是在實際應用中通過調用函數來分類。

下邊我們舉個例子大致來瞭解一下函數調用棧和Source。

  1. 在我們的項目中的Main.storyboard中添加一個Button按鈕,並添加點擊動作。
  2. 然後在點擊動作的代碼中加入一句輸出語句,並打上斷點,如下圖所示:
  添加Button.png
  1. 然後運行程式,並點擊按鈕。
  2. 然後在項目中單擊下下圖紅色部分。
  函數調用棧展示圖
  1. 可以看到如下圖所示就是點擊事件產生的函數調用棧。
  函數調用棧

所以點擊事件是這樣來的:

  1. 首先程式啟動,調用16行的main函數,main函數調用15行UIApplicationMain函數,然後一直往上調用函數,最終調用到0行的BtnClick函數,即點擊函數。

  2. 同時我們可以看到11行中有Sources0,也就是說我們點擊事件是屬於Sources0函數的,點擊事件就是在Sources0中處理的。

  3. 而至於Sources1,則是用來接收、分發系統事件,然後再分發到Sources0中處理的。

2.5 CFRunLoopObserverRef

CFRunLoopObserverRef是觀察者,用來監聽RunLoop的狀態改變

CFRunLoopObserverRef可以監聽的狀態改變有以下幾種:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),               // 即將進入Loop:1
    kCFRunLoopBeforeTimers = (1UL << 1),        // 即將處理Timer:2    
    kCFRunLoopBeforeSources = (1UL << 2),       // 即將處理Source:4
    kCFRunLoopBeforeWaiting = (1UL << 5),       // 即將進入休眠:32
    kCFRunLoopAfterWaiting = (1UL << 6),        // 即將從休眠中喚醒:64
    kCFRunLoopExit = (1UL << 7),                // 即將從Loop中退出:128
    kCFRunLoopAllActivities = 0x0FFFFFFFU       // 監聽全部狀態改變  
};

下邊我們通過代碼來監聽下RunLoop中的狀態改變。

  1. 在ViewController.m中添加如下代碼,Demo中請調用[self showDemo2];方法。
- (void)viewDidLoad {
    [super viewDidLoad];

    // 創建觀察者
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"監聽到RunLoop發生改變---%zd",activity);
    });

    // 添加觀察者到當前RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

    // 釋放observer,最後添加完需要釋放掉
    CFRelease(observer);
}
  1. 然後運行,看下列印結果,如下圖。
  列印結果

可以看到RunLoop的狀態在不斷的改變,最終變成了狀態 32,也就是即將進入睡眠狀態,說明RunLoop之後就會進入睡眠狀態。

 

3. RunLoop原理

好了,五個類都講解完了,下邊開始放大招了。這下我們就可以來理解RunLoop的運行邏輯了。

下邊上一張之前提到的文章中博主提供的運行邏輯圖(來源:http://blog.ibireme.com/2015/05/18/runloop/

  RunLoop運行邏輯圖

這張圖對於我們理解RunLoop來說太有幫助了,下邊我們可以來說下官方文檔給我們的RunLoop邏輯。

在每次運行開啟RunLoop的時候,所線上程的RunLoop會自動處理之前未處理的事件,並且通知相關的觀察者。

具體的順序如下:

  1. 通知觀察者RunLoop已經啟動
  2. 通知觀察者即將要開始的定時器
  3. 通知觀察者任何即將啟動的非基於埠的源
  4. 啟動任何準備好的非基於埠的源
  5. 如果基於埠的源準備好並處於等待狀態,立即啟動;併進入步驟9
  6. 通知觀察者線程進入休眠狀態
  7. 將線程置於休眠知道任一下麵的事件發生:
    • 某一事件到達基於埠的源
    • 定時器啟動
    • RunLoop設置的時間已經超時
    • RunLoop被顯示喚醒
  8. 通知觀察者線程將被喚醒
  9. 處理未處理的事件
    • 如果用戶定義的定時器啟動,處理定時器事件並重啟RunLoop。進入步驟2
    • 如果輸入源啟動,傳遞相應的消息
    • 如果RunLoop被顯示喚醒而且時間還沒超時,重啟RunLoop。進入步驟2
  10. 通知觀察者RunLoop結束。

4. RunLoop實戰應用

哈哈,講了這麼多雲里霧裡的原理知識,下邊終於到了實戰應用環節。

光弄懂是沒啥用的,能夠實戰應用才是硬道理。下麵講解一下RunLoop的幾種應用。

4.1 NSTimer的使用

NSTimer的使用方法在講解CFRunLoopTimerRef類的時候詳細講解過,具體參考上邊 2.3 CFRunLoopTimerRef

4.2 ImageView推遲顯示

有時候,我們會遇到這種情況:
當界面中含有UITableView,而且每個UITableViewCell裡邊都有圖片。這時候當我們滾動UITableView的時候,如果有一堆的圖片需要顯示,那麼可能會出現卡頓的現象。

怎麼解決這個問題呢?

這時候,我們應該推遲圖片的顯示,也就是ImageView推遲顯示圖片。有兩種方法:

1. 監聽UIScrollView的滾動

因為UITableView繼承自UIScrollView,所以我們可以通過監聽UIScrollView的滾動,實現UIScrollView相關delegate即可。

2. 利用PerformSelector設置當前線程的RunLoop的運行模式

利用performSelector方法為UIImageView調用setImage:方法,並利用inModes將其設置為RunLoop下NSDefaultRunLoopMode運行模式。代碼如下:

[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"tupian"] afterDelay:4.0 inModes:NSDefaultRunLoopMode];

下邊利用Demo演示一下該方法。

  1. 在項目中的Main.storyboard中添加一個UIImageView,並添加屬性,並簡單添加一下約束(不然無法顯示)如下圖所示。
  添加UIImageView
  1. 在項目中拖入一張圖片,比如下圖。
 
  1. 然後我們在touchesBegan方法中添加下麵的代碼,在Demo中請在touchesBegan中調用[self showDemo3];方法。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"tupian"] afterDelay:4.0 inModes:@[NSDefaultRunLoopMode]];
}
  1. 運行程式,點擊一下屏幕,然後拖動UIText View,拖動4秒以上,發現過了4秒之後,UIImageView還沒有顯示圖片,當我們鬆開的時候,則顯示圖片,效果如下:
  UIImageView延遲顯示效果

這樣我們就實現了在拖動完之後,在延遲顯示UIImageView。

4.3 後臺常駐線程(很常用)

我們在開發應用程式的過程中,如果後臺操作特別頻繁,經常會在子線程做一些耗時操作(下載文件、後臺播放音樂等),我們最好能讓這條線程永遠常駐記憶體。

那麼怎麼做呢?

添加一條用於常駐記憶體的強引用的子線程,在該線程的RunLoop下添加一個Sources,開啟RunLoop。

具體實現過程如下:

  1. 在項目的ViewController.m中添加一條強引用的thread線程屬性,如下圖:
  添加thread屬性
  1. 在viewDidLoad中創建線程self.thread,使線程啟動並執行run1方法,代碼如下。在Demo中,請在viewDidLoad調用[self showDemo4];方法。
  1. - (void)viewDidLoad {
        [super viewDidLoad];
    
        // 創建線程,並調用run1方法執行任務
        self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
        // 開啟線程
        [self.thread start];    
    }
    
    - (void) run1
    {
        // 這裡寫任務
        NSLog(@"----run1-----");
    
        // 添加下邊兩句代碼,就可以開啟RunLoop,之後self.thread就變成了常駐線程,可隨時添加任務,並交於RunLoop處理
        [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
    
        // 測試是否開啟了RunLoop,如果開啟RunLoop,則來不了這裡,因為RunLoop開啟了迴圈。
        NSLog(@"未開啟RunLoop");
    }
    運行之後發現列印了----run1-----,而未開啟RunLoop則未列印。

這時,我們就開啟了一條常駐線程,下邊我們來試著添加其他任務,除了之前創建的時候調用了run1方法,我們另外在點擊的時候調用run2方法。

那麼,我們在touchesBegan中調用PerformSelector,從而實現在點擊屏幕的時候調用run2方法。Demo地址。具體代碼如下:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{   
    // 利用performSelector,在self.thread的線程中調用run2方法執行任務
    [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void) run2
{
    NSLog(@"----run2------");
}

 

經過運行測試,除了之前列印的----run1-----,每當我們點擊屏幕,都能調用----run2------
這樣我們就實現了常駐線程的需求。


iOS多線程詳盡總結系列文章:


作者:行走少年郎


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Oracle監聽器日誌文件(通常叫做listener.log)是一個純文本文件,它的大小是一直不斷增長的,在一個生產Oracle伺服器上,DBA會每日查看該文件,如檢查監聽器是否有異常停止,是否有惡意攻擊連接等,當這個文件特別大的時候,打開和瀏覽文件內容時可能比較慢。這時可能會想到將當前的日誌文件備 ...
  • oracle判斷是否為null nvl(參數1,參數2) ;如果參數1為null則返回參數2,否則返回參數1 mysql判斷是否為null ifnull(參數1,參數2) ;如果參數1為null則返回參數2,否則返回參數1 select nvl(null,'空值') from dual 結果:空值 ...
  • 相信很多SQL Server DBA或開發人員在重建或重組大表索引時,都會相當鬱悶,不知道索引重建的進度,這個對於DBA完全是一個黑盒子,對於系統負載非常大的系統或維護視窗較短的系統,你會遇到一些挑戰。例如,你創建索引的時候,很多會話被阻塞,你只能取消創建索引的任務。查看這些索引維護操作的進度、預估... ...
  • 最近在學習林曉斌(丁奇)老師的《MySQL實戰45講》,受益匪淺,做一些筆記整理一下,幫助學習。如果有小伙伴感興趣的話推薦原版課程,很不錯。 1) --基礎架構,一條SQL查詢語句如何執行 MySQL大體來說可以分為Server層和存儲引擎層兩部分. Server層包括:連接器,查詢緩存,分析器,優 ...
  • 轉自:http://www.maomao365.com/?p=5471 摘要: 下文主要講述動態行列轉換語句,列名會根據行數據的不同, 動態的發生變化 實現思路: 主要將待生成的動態列名,採用腳本拼接起來,然後採用pivot函數 運行,得到相應的結果 本腳本運行環境: sql server 2008 ...
  • Question: SQL SERVER 通過Linkserver連接A和B 2台,A對B執行單條的增刪改查沒有異常(沒有配置DTC) 但是開啟事務後就會出現報錯 Solution: 在A和B上配置DTC(控制面板→管理工具→組件服務),配置參數如下: 再次測試無異常 開啟事務前Set XACT_A ...
  • 原文鏈接:https://www.cnblogs.com/wtujvk/p/7497723.html 運行程式時拋出異常: 基礎提供程式在 Open 上失敗,詳細信息:該帳戶當前被鎖定,所以用戶sa登錄失敗。系統管理員無法將該帳戶解鎖。 1.考慮連接字元串是否正常 登錄資料庫,發現登錄報同樣的錯誤 ...
  • 一、 PCH文件的作用 Xcode中,PCH文件在程式編譯的時候會自動包含進去。也就是說PCH中的內容是全局的,可以使用在程式的任何地方,通過這個特性,我們可以概括到PCH的作用有以下幾個方面: (1)將經常使用的巨集定義在該文件,可以避免多次定義的麻煩 (2)包含多次使用的.h文件 (3)其他需要全 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...