再談 iOS App Crash 防護

来源:https://www.cnblogs.com/Julday/archive/2020/06/13/13113710.html
-Advertisement-
Play Games

在移動開發中,App 的閃退率是工程師十分關註且又頭疼的事情。去年,網易杭州研究院曾經針對 crash 的防護有提出『大白健康系統--iOS APP 運行時 Crash 自動修複系統』方案,使得 crash 防護這個想法真正被落實,但至今該方案的具體實現並沒有被開源。經過一年的時間,圈子裡也有一些開 ...


在移動開發中,App 的閃退率是工程師十分關註且又頭疼的事情。去年,網易杭州研究院曾經針對 crash 的防護有提出『大白健康系統--iOS APP 運行時 Crash 自動修複系統』方案,使得 crash 防護這個想法真正被落實,但至今該方案的具體實現並沒有被開源。經過一年的時間,圈子裡也有一些開發朋友,基於這套方案設計並開源了自己的 “Baymax”,比如『老司機 iOS 周報第七期』中曾提到的 BayMaxProtector。本文將會針對網易 Baymax 這套方案,結合團隊內的實踐結果,總結其在生產環境中可能遇到的問題及其解決方案,並提出一些自己對這套方案的思考。友情提示,閱讀本文前需對網易『大白健康系統--iOS APP 運行時 Crash 自動修複系統』一文有所瞭解,該文中已有的實現方案,本文不會再花更多筆墨進行贅述。

Crash 防護可選的方案

Crash 是什麼?

在探討 Crash 防護的方案之前,我們有必要對電腦領域 Crash 這個概念進行重新認識。對於 Crash 的概念,維基百科中是這麼定義的:

In computing, a crash (or system crash) occurs when a computer program, such as a software application or an operating system, stops functioning properly and exits.

An application typically crashes when it performs an operation that is not allowed by the operating system. The operating system then triggers an exception or signal in the application. Unix applications traditionally responded to the signal by dumping core. Most Windows and Unix GUI applications respond by displaying a dialogue box (such as the one shown to the right) with the option to attach a debugger if one is installed. Some applications attempt to recover from the error and continue running instead of exiting.

對於我們 iOS 應用層的 App,可簡單總結為應用執行了某些不被允許的操作觸發了系統拋出異常信號但又沒有處理這些異常信號從而被殺掉的現象,比如常見的閃退(crash to desktop)。在我們開發領域從拋出異常的對象上來看,一共可以分為三類內核導致的異常、應用自身的異常或其他進程導致的異常:

  • 由操作系統內核捕獲硬體產生的異常信號,比如 EXC_BAD_ACCESS,這類異常如果沒有被處理掉的話,會被轉發到 SIGBUS 或 SIGSEGV 等類型的 BSD 信號;
  • 由 SDK 開發者或上層應用開發者主動拋出的異常信號,比如各種常見的 NSException,這類異常蘋果為了統一處理,最終會被轉發為 SIGABRT 類的 BSD 信號;
  • 其他進程殺死你的應用;

這裡我們主要談最常見的前兩種異常。

可選的 Crash 防護方案

上面已經提到了 Crash 實際上我們觸發了異常,但又沒有去處理這些異常而導致的結果。那麼很自然的第一個防護方案便可以想到是去處理這些異常。

通過 NSUncaughtExceptionHandler 來捕獲並處理異常

蘋果的確提供有異常捕獲的 API 以供開發者使用——NSSetUncaughtExceptionHandler,開發者只需要傳入處理函數的指針,便可以處理掉應用中拋出的 NSException 類的異常。代碼寫起來就是:

NSSetUncaughtExceptionHandler(&HandleException);

通過 BSD 的 signal 來捕獲並處理異常

由於蘋果將所有異常最終都轉換成了 BSD 信號的發出,那麼我們就可以去捕獲這個信號來處理這些異常,從而達到 Crash 防護的目的。系統也有提供相關 API 實現:

void    (*signal(int, void (*)(int)))(int);

前一個參數為異常類型,可以是 SIGSEGV 等這類,後一個參數為回調的函數,代碼寫起來就可以是:

signal(SIGABRT, SignalHandler);
signal(SIGILL, SignalHandler);
signal(SIGSEGV, SignalHandler);
signal(SIGFPE, SignalHandler);
signal(SIGBUS, SignalHandler);
signal(SIGPIPE, SignalHandler);

註意:由於 Xcode 預設會開啟 debug executable,它會在我們捕獲這些異常信號之前攔截掉,因此做這個測試需要手動將 debug executable 功能關閉,或者不在 Xcode 連接調試下進行測試。

  image

 

至此,似乎一切看起來都很順利,然而實踐過程中你會發現程式並沒有在你處理完這些異常後就能繼續進行。這與 iOS 的 Runloop 機制有關,在觸發異常後,Main Runloop 將不會繼續運行,這也就意味著 App 跑不起來了。當然,你可能會很自然地聯想到,我自己再把 Main Runloop 繼續掛起來跑不就行了嗎?如以下類似代碼:

//這裡取到的是 Main Runloop
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);

while (YES)
{
    for (NSString *mode in (NSArray *)allModes)
    {
        CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
    }
}

CFRelease(allModes);

這樣一試,確實程式在捕獲異常之後又能夠繼續運行了。但『通過 NSUncaughtExceptionHandler 來捕獲並處理異常』和『通過 BSD 的 signal 來捕獲並處理異常』這兩種方式去做 Crash 防護並不是一種靠譜的方式,原因有以下幾點:

  • iOS/OSX 在被拋出異常後,被認為是不可恢復的,如果我們強行恢復 Runloop,整個 App 的不確定性將會更大,crash 的部分可能會再次發生;
  • 內核拋出的異常一般都是較嚴重的底層硬體問題,如果這類問題不及時停止程式運行,可能會進一步影響整個系統的運行,乃至損壞硬體;
  • 以上兩種做法,通常是用於 Crash 日誌收集上,如果我們防護層也通過這個方案去做的話,衝突的可能性會很大;

這裡附帶下『App Architecture book』作者 Matt Gallagher 早年對於這部分研究後的一個 demo,由於是 MRC 時代的代碼了,修改了部分配置使得能夠正常編譯且測試。

通過 try-catch 的組合拳來捕獲異常

和其他編程語言一樣,Objective-C 中也有萬能的 try-catch 組合來捕獲異常,這樣處理不就可以了?這種方案確實是可行的,我也確實有見過一些人使用 try-catch 來做一些常見的 Crash 防護。但 Objective-C 的 try-catch 實際上有先天缺陷的,首先是效率並不高,甚至某些情況下會導致記憶體泄漏,不可控。

  • 效率不高是由於 try-catch 是基於 block 的處理方案,會多出額外的開銷(不過蘋果已經重寫了 64 bit 機器上的 try-catch,而且聲明是 zero-cost);
  • 可能會記憶體泄漏是由於 Xcode 預設並不會對 try-catch 中的代碼進行 ARC 管理。try 在捕捉到 Exception 之後,會立即轉到 catch 中執行,這樣就導致瞭如果 release 代碼是寫在 try 中 throw 異常的代碼之後的話,就會不被執行而導致記憶體泄漏。如果為了防止這個泄漏而去配置 -fobjc-arc-exceptions 選項,更會因為生成低效代碼而得不償失,這也是蘋果並不推薦的方式。

但這不能完全否定 try-catch 組合在我們日常編程中的作用,在一些容易出現異常的操作上,比如文件讀寫或者需要配合使用 throw 的情況等。這裡指的不適合,只是針對在大範圍防護並不適合。

Baymax 的方案

在綜合分析了以上幾個防護方案後,我們再來看看 Baymax 中採用的方案。如果說上面三種方案都是在已經拋出了異常之後再去捕獲處理,也就是“喝後悔藥”的機制,那麼 Baymax 的方案便是不讓這些異常產生。不讓錯誤異常產生可以通過多種做法,往項目管理上說提高代碼質量,增加 Code Review 等,從編碼角度來說,我們可以通過各種保護性代碼進行。Baymax 中的大部分防護方案都可以理解為一種為你自動增加保護性代碼的措施。比如,各種 Collection 類型,String 類型等。

實踐 Baymax 方案中可能遇到的問題

高頻調用方法的性能問題

Baymax 是基於 AOP 思想而設計的,方案中會充斥著各種 Hook 系統方法,這對於高頻調用的方法,性能上的損耗是不可忽略的。為了將損耗儘量降低,我們可以通過只防護特定類來進行,比如只針對我們的自定義類和部分在防護名單內的類,而對於系統的類,我們不進行防護,這樣就能在一定限度上降低性能損耗。對於判斷自定義類可以通過以下方法進行:

如果只是判斷 main bundle 的話可以通過以下代碼進行:

+ (BOOL)isMainBundleClass:(Class)cls {
    return cls && [[NSBundle bundleForClass:cls] isEqual:[NSBundle mainBundle]] ;
}

但在組件化開發中,我們的代碼會通過各種私有 pod 的形式導入,這樣只判斷 main bundle 的方式就不夠用了,我們可以通過以下代碼進行:

+ (BOOL)isCustomClass:(Class)cls {
    ///var/containers/Bundle/Application/CB0D354B-DD08-4845-A084-A22FF01097FE/Example.app
    NSString *mainBundlePath = [NSBundle mainBundle].bundlePath;
    ///var/containers/Bundle/Application/CB0D354B-DD08-4845-A084-A22FF01097FE/Example.app/Frameworks/Baymax.framework
    NSString *clsBundlePath = [NSBundle bundleForClass:cls].bundlePath;

    return cls && mainBundlePath && clsBundlePath && [clsBundlePath hasPrefix:mainBundlePath];
}

另外,由於判斷是否防護的條件會相對比較多,這裡可以引入名單緩存來做進一步的效率優化,將本次判斷結果存儲到 NSCache 中,下回優先從 Cache 里讀取防護狀態,性能提升將會十分顯著。大致代碼如下:

//先從緩存中讀取狀態
NSNumber *status = [baymax needBaymaxStatusInProtectionCache:clsStr];
//如果有在緩存中 則直接返回緩存中的狀態 若不在緩存中 則繼續走判斷邏輯
if (status != nil) return [status boolValue];

UnrecognizedSelector 防護的坑

蘋果在 KVO 的實現中,為每種類型都封裝了一個特定的 set 方法,原因未知(或許又是 Historical Reasons 吧),這裡涵蓋了 CoreFoundation 里的所有基礎類型。

_NSSetBoolValueAndNotify、_NSSetCharValueAndNotify、_NSSetDoubleValueAndNotify、_NSSetFloatValueAndNotify、_NSSetIntValueAndNotify、_NSSetLongLongValueAndNotify、_NSSetLongValueAndNotify、_NSSetObjectValueAndNotify、_NSSetPointValueAndNotify、_NSSetRangeValueAndNotify、_NSSetRectValueAndNotify、_NSSetShortValueAndNotify、_NSSetSizeValueAndNotify、_NSSetUnsignedCharValueAndNotify、_NSSetUnsignedIntValueAndNotify、_NSSetUnsignedLongLongValueAndNotify、_NSSetUnsignedLongValueAndNotify、_NSSetUnsignedShortValueAndNotify

除這些類型外的其他類型(比如 UIKit 中的 struct 或者其他自定義的 struct)被作為 property 觀察時,都會走以下的轉發邏輯。這樣的處理邏輯在特定的情況下就會影響防護,比如 UIEdgeInsets 類型的 property 被加入 KVO 檢測,那麼之後再 set 這個 property 的時候,set 方法就會進入轉發邏輯,這樣就會被誤識別為一次UnrecognizedSelector 的 Crash,且導致原有的 KVO 邏輯失效。

<_NSCallStackArray 0x100700630>(
       0   ???                                 0x00000001001f3ecd 0x0 + 4297014989,
       1   KVOAnalysisDemo                     0x0000000100001850 main + 0,
       2   Foundation                          0x00007fff981fd67d NSKeyValueNotifyObserver + 350,
       3   Foundation                          0x00007fff981fcf14 NSKeyValueDidChange + 486,
       4   Foundation                          0x00007fff981cbdf6 -[NSObject(NSKeyValueObserverNotification) didChangeValueForKey:] + 118,
       5   Foundation                          0x00007fff9829cc11 NSKVOForwardInvocation + 325,
       6   CoreFoundation                      0x00007fff967c65fa ___forwarding___ + 538,
       7   CoreFoundation                      0x00007fff967c6358 _CF_forwarding_prep_0 + 120,
       8   KVOAnalysisDemo                     0x000000010000198b main + 315,
       9   libdyld.dylib                       0x00007fffabf2d235 start + 1
       )

解決方案是通過判斷是否重寫相關轉發方法決定是否需要防護,主要代碼如下:

BOOL isMethodOverride = ([self isMethodOverride:cls selector:@selector(forwardInvocation:)] || [self isMethodOverride:cls
selector:@selector(forwardingTargetForSelector:)]);

if (!isMethodOverride) {
           return YES;
}

+ (BOOL)isMethodOverride:(Class)cls selector:(SEL)sel {
    IMP selfIMP = class_getMethodImplementation(cls, sel);
    IMP superIMP = class_getMethodImplementation(class_getSuperclass(cls), sel);

    return selfIMP != superIMP;
}

iOS SDK 在不斷調整

由於 iOS 系統的封閉性,系統 API 的實現我們是無法直接看到的。而蘋果有可能在更新系統版本的時候,出於各種原因對一些 API 進行調整。在測試中已發現有以下幾個系統類在 iOS8-iOS10 中被調整過:

po [@[] class] before iOS8:__NSArrayI later:__NSArray0
po [@[@1] class] before iOS9:__NSArrayI iOS10:__NSSingleObjectArrayI
po [objc_getClass("NSTaggedPointerString") superclass] before iOS8:NSObject after iOS8:NSString

以上這些實現的調整,造成的影響均是 method-swizzling 的失敗。但從實際測試情況來看,雖然以上類有做了調整,但其實並不影響防護。比如,__NSArray0 在 iOS8 中是__NSArrayI 代替,而 __NSArrayI 這個類在 iOS8 或者之後的系統都是會被防護的。

BadAccess 防護中原 dealloc 方法的延遲調用

BadAccess 防護的核心原理是延遲記憶體釋放,這裡就需要在之後的某個合適的時機,手動去調用原有的釋放方法來執行真正的記憶體釋放。但在實際開發中,發現直接去調用保存的原 dealloc,並不能做到正確釋放記憶體。排查搜索之後,發現這可能是在 ARC 環境下,蘋果對 dealloc 方法的特殊處理導致的,在 method-swizzling 後,原 dealloc 的 selector 實際上已經變成了轉發後的 selector 了,而猜測目前 ARC 的對 dealloc 的處理只認 dealloc 這個 selector,所以唯一的方法處理便是還是通過 imp(obj, NSSelectorFromString(@"dealloc")) 來調用。

目前的解決方法:直接用 c 函數傳 imp 和 dealloc 調用,主要代碼如下:

// Get Original Dealloc IMP.
// See more in JSPatch:https://github.com/bang590/JSPatch/blob/master/JSPatch/JPEngine.m
Class objCls = object_getClass(obj);
Method deallocMethod = class_getInstanceMethod(objCls, NSSelectorFromString(@"wycd_dealloc"));
void (*originalDealloc)(__unsafe_unretained id, SEL) = (__typeof__(originalDealloc))method_getImplementation(deallocMethod);
originalDealloc(obj, NSSelectorFromString(@"dealloc"));

NSArray 防護後出現的奇葩問題

Hook 掉 objectAtIndex:方法後,在這樣一個場景下會出現意外的 crash:調出系統鍵盤再把 App 切到後臺,就出現 [uikeyboardlayoutstar release] message sent to deallocated instance crash。這其實是 iOS 系統在 ARC 下的一個坑,ARC 導致了 over-released 的 crash,暫時沒有其他更好的解決方案,只能把這部分防護改為 MRC 編寫。

如何保證 SDK 更新的穩定性

Baymax 方案涉及到很多的系統方法,那麼怎麼保證每一次更新迭代不會造成嚴重的線上問題呢?這最終還是要落實到單元測試上,我們可以給 Baymax 編寫足夠完善的單元測試用例,然後配置一個觸發腳本,來自動地在我們每次 push 到開發分支時跑這些測試用例。當然,必須值得註意的是,測試必須覆蓋到你當前支持的所有 iOS 版本,如果是使用 GitLab Runner 可以按如下配置做:

test_job:
  only:
    - UnitTest
  stage: test
  script:
    - export LC_ALL='en_US.UTF-8'
    - xcodebuild clean -workspace Example/Baymax.xcworkspace -scheme Baymax-Example | xcpretty
    - pod install --project-directory=Example
    - xcodebuild test -workspace Example/Baymax.xcworkspace -scheme Baymax-Example -destination 'platform=iOS Simulator,name=iPhone 5s,OS=11.2' -destination 'platform=iOS Simulator,name=iPhone 5s,OS=9.3' -destination 'platform=iOS Simulator,name=iPhone 5s,OS=8.4' | xcpretty -s

大致的單元測試代碼可以如下:

- (void)testCrashProtection {
    //given when
    Baymax *baymax = [Baymax sharedInstance];
    [baymax configBaymaxType:BaymaxAll];
    [baymax start];

    //then
    for (int i = 0 ; i < kBaymaxType; i++) {
        NSUInteger type = 1 << i;
        Tester *tester = [Tester tester:type];

        NSUInteger caseCount = [[tester testCaseSelectors] count];

        for (int j = 0; j < caseCount; j++) {
            XCTAssertNoThrow([tester executeTestCase:j]);
        }
    }
}

防護的代價是什麼

任何事物我們都從正反兩方面考慮,既然 Baymax 提供了防護功能,那其必然也存在著弊端。

首先,第一點就是上面提到的性能問題,在方案調研階段,筆者曾經使用 XCTest 對 Collection 類型的防護做了部分的性能測試,結果大致如下:

不做 Hook
Test Case '-[PerformanceTests testPerformance_Collection]' measured [Time, seconds] average: 0.000, relative standard deviation: 151.327%, values: [0.000011, 0.000002, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001]

做了 Hook 但是不觸發防護邏輯
Test Case '-[PerformanceTests testPerformance_Collection]' measured [Time, seconds] average: 0.000, relative standard deviation: 83.636%, values: [0.000021, 0.000005, 0.000005, 0.000009, 0.000003, 0.000003, 0.000003, 0.000003, 0.000009, 0.000003]

做了 Hook 且觸發了防護邏輯
Test Case '-[PerformanceTests testPerformance_Collection]' measured [Time, seconds] average: 0.000, relative standard deviation: 47.857%, values: [0.000026, 0.000010, 0.000009, 0.000009, 0.000008, 0.000009, 0.000009, 0.000008, 0.000009, 0.000009]

從上面數據可以很直觀地看到,在不做任何優化的前提下性能下降十分明顯,效率損失甚至高達 3 倍以上,所以如果要做防護,必須充分考慮到性能優化這些點。

其次,需要合理權衡開啟的防護類型,目前我們僅預設開啟線上反饋的常見類型,而不是開啟所有類型,其他類型可以配置為動態開啟,根據用戶設備的閃退日誌開啟防護。其中,Baymax 中提到的野指針防護,在實踐中發現用處很有限,因為只是做了延遲釋放,而不是真正意義上對野指針這種 crash 進行防護,且由於對系統的釋放時機進行了處理,與 Xcode 原來的 Zombie 機制有一定衝突,也會產生一些很奇葩的問題,不確定性很高。

再次,各種Hook帶來的未知性,Crash 本身是非正常情況下才產生的,如果一味地規避這種異常,可能會產生更多的異常情況,特別是業務邏輯上會出現不可控制的流程。

最後,這套防護方案的作用究竟有多大呢?根據筆者個人經驗來說,對於越成熟的團隊,防護方案帶來的效果會越小。因為成熟團隊的代碼質量相對更高,一些低級錯誤出現的概率極小。但對於小團隊,或者歷史比較久的項目而言,這套方案帶來的幫助會比較大,畢竟坑總是防不勝防的。

推薦

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

-Advertisement-
Play Games
更多相關文章
  • 如何查看SQL執行計劃 使用 PL/SQL 查看,具體使用方法如下: 新建 解釋計劃視窗 ,將 SQL 複製進去執行,即可顯示執行計劃。 選中 SQL 語句,點擊菜單 工具-解釋計劃 或 按快捷鍵 F5 執行計劃結果說明 表掃描 table access by index rowid 通過ROWID ...
  • 現在有如下表 id name age 1 張三 23 2 李四 34 3 張三 23 4 李四 32 需求 : 按照name和age欄位聯合去重 sql如下 select * from user group by name,age 文章轉自:https://blog.csdn.net/qq_2898 ...
  • 1.重置密碼的第一步就是跳過MySQL的密碼認證過程,方法如下: #vim /etc/my.cnf(註:windows下修改的是my.ini) 很多老鐵,在開始時設置了 MySQL 的密碼,後來一段時間沒有用 MySQL之後,密碼忘了~ QAQ,請別急,現在有以下方法解決密碼忘了的情況。 1.首先我 ...
  • 最近一個電子看板小項目上線,由於資料庫非常小,而且數據也不太重要。因此未選擇XtraBackup備份,打算用AutoMySQLBackup來備份,結果部署後測試發現,有一些小問題是之前解決過的。有一些是MySQL 5.7版本才有的。下麵記錄一下解決過程。關於AutoMySQLBackup的基礎知識,... ...
  • 42.統計APP應用的DB連接及IP情況 select b.hostname ,a.client_net_address, b.program_name ,count(1) as Qtyfrom sys.dm_exec_connections a(nolock) inner join sys.sys ...
  • 在開發過程中,埋點可以解決兩大類問題:一是瞭解用戶使用 App 的行為,二是降低分析線上問題的難度。目前,iOS 開發中常見的埋點方式,主要包括: 代碼埋點 可視化埋點 無埋點 代碼埋點 代碼埋點主要就是通過手寫代碼的方式來埋點,能很精確的在需要埋點的代碼處加上埋點的代碼,可以很方便地記錄當前環境的 ...
  • 分享近期 GitHub 上比較流行的 22 個和 iOS 開發相關的開源項目。 包括開發輔助工具,非同步編程庫,JSON 解析,移動端資料庫,圖像視頻處理,網路請求,UI 框架、組件,演算法、數據結構等內容。 Accio 使用 Swift 編寫的 iOS/tvOS/watchOS/macOS 依賴管理工 ...
  • 背景 過完年來北京之後,有準備看看機會,也是想瞭解下市場行情。簡歷沒有投太多,只定向投了頭條教育部門、抖音、快手、阿裡,這些公司。 頭條和阿裡的簡歷都沒過,肯定是亮點太少吧。只有快手簡歷過了,快手是三輪技術面+一輪HR面,前兩輪技術都比較順利,到第三輪卻栽了,很痛心o(╥﹏╥)o。目前就不考慮換工作 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...