【Android 系統開發】_“性能優化”篇 -- ANR 之 Service 超時

来源:https://www.cnblogs.com/pepsimaxin/archive/2019/04/29/10789927.html
-Advertisement-
Play Games

不管是系統開發還是應用開發,ANR 一直是揮之不去的存在!本文從 ANR 的觸發機制以及一個簡單的範例來探討 ANR 的原理和處理方法! ...


1. 核心源碼

關鍵類 路徑(/frameworks/base/)
ActiveServices.java services/core/java/com/android/server/am/ActiveServices.java
ActivityManagerService.java services/core/java/com/android/server/am/ActivityManagerService.java
AppErrors.java services/core/java/com/android/server/am/AppErrors.java


2. ANR 基礎認知

2.1 ANR 是什麼?

ANR(Application Not Responding),應用程式無響應,簡單一個定義,卻涵蓋了很多 Android 系統的設計思想。

首先,ANR 屬於應用程式的範疇,這不同於 SNR(System Not Respoding),SNR 反映的問題是系統進程(system_server)失去了響應能力,而 ANR 明確將問題圈定在應用程式。SNR 由 Watchdog 機制保證,ANR 由消息處理機制保證,Android 在系統層實現了一套精密的機制來發現 ANR,核心原理是 消息調度超時處理

其次,ANR 機制 主體實現在系統層。所有與 ANR 相關的消息,都會經過系統進程(system_server)調度,然後派發到應用進程完成對消息的實際處理,同時,系統進程設計了不同的超時限制來跟蹤消息的處理。一旦應用程式處理消息不當,超時限制就起作用了,它收集一些系統狀態,例如:CPU/IO使用情況、進程函數調用棧,並且報告用戶有進程 無響應了(ANR 對話框)

然後,ANR 問題 本質是一個性能問題。ANR 機制實際上對應用程式主線程的限制,要求主線程在限定的時間內處理完一些最常見的操作(啟動服務、處理廣播、處理輸入),如果處理超時,則認為主線程已經失去了響應其他操作的能力。主線程中的 耗時操作,例如:密集CPU運算、大量IO、複雜界面佈局等,都會降低應用程式的響應能力。

最後,部分 ANR 問題是很難分析的,有時候由於系統底層的一些影響,導致消息調度失敗,出現問題的場景又難以復現。這類 ANR 問題往往需要花費大量的時間去瞭解系統的一些行為,超出了 ANR 機制本身的範疇。

2.2 ANR 機制

分析一些初級的 ANR 問題,只需要簡單理解最終輸出的日誌即可,但對於一些由系統問題(例如:CPU 負載過高、進程卡死)引發的 ANR,就需要對整個 ANR 機制有所瞭解,才能定位出問題的原因。

ANR 機制可以分為兩部分:

      ✎  ANR的監測:Android 對於不同的 ANR 類型(Broadcast,Service,InputEvent)都有一套監測機制。

      ✎  ANR的報告:在監測到 ANR 以後,需要顯示 ANR 對話框、輸出日誌(發生 ANR 時的進程函數調用棧、CPU 使用情況等)。

2.3 ANR 的觸發原因

前面我們說過,出現 ANR 之後一個直觀現象就是系統會展示出一個 ANR 對話框。

谷歌文檔中對 ANR 產生的原因是這麼描述的:

Android 系統中的應用被 ActivityManagerServiceWindowManagerService 兩個系統服務監控著,系統會在如下兩種情況展示出 ANR 的對話框!

      ✎  KeyDispatchTimeout ( 5 seconds ) :按鍵或觸摸事件在特定時間內無響應。
      ✎  BroadcastTimeout ( 10 seconds ):BroadcastReceiver 在特定時間內無法處理完成。
      ✎  ServiceTimeout ( 20 seconds ) :Service 在特定的時間內無法處理完成。


3. Service 超時監測機制

Service 運行在應用程式的主線程,如果 Service 的執行時間超過 20 秒,則會引發 ANR。

當發生 Service ANR 時,一般可以先排查一下在 Service 的生命周期函數中有沒有做 耗時的操作,例如複雜的運算、IO 操作等。如果應用程式的代碼邏輯查不出問題,就需要深入檢查當前系統的狀態:CPU 的使用情況、系統服務的狀態等,判斷當時發生 ANR 進程是否受到 系統運行異常 的影響。

那麼,系統是如何檢測 Service 超時的呢?Android 是通過設置定時消息實現的。定時消息是由 AMS 的消息隊列處理的,AMS 有 Service 運行的上下文信息,所以在 AMS 中設置一套超時檢測機制也是合情合理的。

Service ANR 機制相對最為簡單,主體實現在ActiveServices中。

在 Service 的啟動流程中,Service 進程 attach 到 system_server 進程後會調用 realStartServiceLocked() 方法。

3.1 realStartServiceLocked

// frameworks/base/services/core/java/com/android/server/am/ActiveServices.java

public final class ActiveServices {

    private final void realStartServiceLocked(ServiceRecord r,
            ProcessRecord app, boolean execInFg) throws RemoteException {

        // 發送 delay 消息(SERVICE_TIMEOUT_MSG)
        bumpServiceExecutingLocked(r, execInFg, "create");

        boolean created = false;
        try {
            
            // 最終執行服務的 onCreate() 方法
            app.thread.scheduleCreateService(r, r.serviceInfo, mAm.
                compatibilityInfoForPackageLocked(r.serviceInfo.applicationInfo),
                app.repProcState);

            ... ...
        }
    }
    
}

3.2 bumpServiceExecutingLocked

private final void bumpServiceExecutingLocked(...) {

    scheduleServiceTimeoutLocked(r.app);
    
}

3.3 scheduleServiceTimeoutLocked

void scheduleServiceTimeoutLocked(ProcessRecord proc) {
    if (proc.executingServices.size() == 0 || proc.thread == null) {
        return;
    }
    Message msg = mAm.mHandler.obtainMessage(
            ActivityManagerService.SERVICE_TIMEOUT_MSG);
    msg.obj = proc;
    // 當超時後仍沒有 remove 該 SERVICE_TIMEOUT_MSG 消息,
    // 通過 AMS.MainHandler 拋出一個定時消息。
    mAm.mHandler.sendMessageDelayed(msg,
            proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
}

上述方法通過 AMS.MainHandler 拋出一個定時消息 SERVICE_TIMEOUT_MSG

3.4 serviceDoneExecutingLocked

前臺進程中執行 Service,超時時間是 SERVICE_TIMEOUT(20 秒)

    // How long we wait for a service to finish executing.
    static final int SERVICE_TIMEOUT = 20*1000;

後臺進程中執行 Service,超時時間是 SERVICE_BACKGROUND_TIMEOUT(200 秒)

    // How long we wait for a service to finish executing.
    static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10;

當 Service 的生命周期結束時(不會 ANR),會調用 serviceDoneExecutingLocked() 方法,之前拋出的 SERVICE_TIMEOUT_MSG 消息在這個方法中會被清除。

void serviceDoneExecutingLocked(ServiceRecord r, int type, int startId, int res) {
    boolean inDestroying = mDestroyingServices.contains(r);
    if (r != null) {
        ... ...     
        serviceDoneExecutingLocked(r, inDestroying, inDestroying);
    }
}

private void serviceDoneExecutingLocked(ServiceRecord r, boolean inDestroying,
          boolean finishing) {  
    ... ...
    if (r.executeNesting <= 0) {
        if (r.app != null) {    
            ... ...
            // 當前服務所在進程中沒有正在執行的service,清除 SERVICE_TIMEOUT_MSG 消息
            if (r.app.executingServices.size() == 0) {
                mAm.mHandler.removeMessages(
                             ActivityManagerService.SERVICE_TIMEOUT_MSG, r.app);            
                ... ...
            }
    ... ...
}

3.5 handleMessage

如果沒有 Remove 掉 SERVICE_TIMEOUT_MSG 呢?接下來我們看看對於 ANR 的處理邏輯。

在 system_server 進程中有一個 Handler 線程,名叫 ActivityManager

如果在超時時間內,SERVICE_TIMEOUT_MSG 沒有被清除,便會向該 Handler 線程發送一條信息 SERVICE_TIMEOUT_MSG

final class MainHandler extends Handler {
    ... ...

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            ... ...
            
            case SERVICE_TIMEOUT_MSG: {
                mServices.serviceTimeout((ProcessRecord)msg.obj);
            } break;
        ... ...
    }
}

3.6 serviceTimeout

void serviceTimeout(ProcessRecord proc) {
    String anrMessage = null;

    synchronized(mAm) {
        ... ...
        
        long nextTime = 0;

        // 尋找運行超時的 Service
        for (int i = proc.executingServices.size() - 1; i >= 0; i--) {
            ServiceRecord sr = proc.executingServices.valueAt(i);
            if (sr.executingStart < maxTime) {
                timeout = sr;
                break;
            }
            if (sr.executingStart > nextTime) {
                nextTime = sr.executingStart;
            }
        }

        // 判斷執行 Service 超時的進程是否在最近運行進程列表,如果不在,則忽略這個 ANR
        if (timeout != null && mAm.mLruProcesses.contains(proc)) {
            Slog.w(TAG, "Timeout executing service: " + timeout);
            StringWriter sw = new StringWriter();
            PrintWriter pw = new FastPrintWriter(sw, false, 1024);
            pw.println(timeout);
            timeout.dump(pw, "    ");
            pw.close();
            mLastAnrDump = sw.toString();
            mAm.mHandler.removeCallbacks(mLastAnrDumpClearer);
            mAm.mHandler.postDelayed(mLastAnrDumpClearer, 
                                           LAST_ANR_LIFETIME_DURATION_MSECS);
            anrMessage = "executing service " + timeout.shortName;
        ... ...
    }

    if (anrMessage != null) {
        // 當存在 timeout 的 service,則執行 appNotResponding
        mAm.mAppErrors.appNotResponding(proc, null, null, false, anrMessage);
    }
}

上述方法會找到當前進程已經超時的 Service,經過一些判定後,決定要報告 ANR,最終調用 AMS.appNotResponding() 方法。

走到這一步,ANR 機制已經完成了監測報告任務,剩下的任務就是 ANR 結果的輸出,我們稱之為 ANR 的報告機制。ANR 的報告機制是通過 AMS.appNotResponding() 完成的,Broadcast 和 InputEvent 類型的 ANR 最終也都會調用這個方法。


4. ANR 信息收集過程

接下來我們看看 Android ANR 的信息收集過程!

4.1 appNotResponding

// frameworks/base/services/core/java/com/android/server/am/AppErrors.java

class AppErrors {

    final void appNotResponding(ProcessRecord app, ActivityRecord activity,
            ActivityRecord parent, boolean aboveSystem, final String annotation) {
        ... ...

        long anrTime = SystemClock.uptimeMillis();
        if (ActivityManagerService.MONITOR_CPU_USAGE) {
            mService.updateCpuStatsNow();   // 更新 cpu 統計信息
        }

        boolean showBackground = Settings.Secure.
                getInt(mContext.getContentResolver(),
                           Settings.Secure.ANR_SHOW_BACKGROUND, 0) != 0;

        boolean isSilentANR;

        synchronized (mService) {
            if (mService.mShuttingDown) {
                return;
            } else if (app.notResponding) {
                return;
            } else if (app.crashing) {
                return;
            } else if (app.killedByAm) {
                return;
            } else if (app.killed) {
                return;
            }

            // In case we come through here for the same app before completing
            // this one, mark as anring now so we will bail out.
            app.notResponding = true;

            // 記錄 ANR 到 EventLog
            EventLog.writeEvent(EventLogTags.AM_ANR, app.userId, app.pid,
                    app.processName, app.info.flags, annotation);

            // 將當前進程添加到 firstPids
            firstPids.add(app.pid);

            // Don't dump other PIDs if it's a background ANR
            isSilentANR = !showBackground 
                                  && !isInterestingForBackgroundTraces(app);
            if (!isSilentANR) {
                int parentPid = app.pid;
                if (parent != null && parent.app != null && parent.app.pid > 0) {
                    parentPid = parent.app.pid;
                }
                if (parentPid != app.pid) firstPids.add(parentPid);

                // 將 system_server 進程添加到 firstPids
                if (MY_PID != app.pid 
                                && MY_PID != parentPid) firstPids.add(MY_PID);

                for (int i = mService.mLruProcesses.size() - 1; i >= 0; i--) {
                    ProcessRecord r = mService.mLruProcesses.get(i);
                    if (r != null && r.thread != null) {
                        int pid = r.pid;
                        if (pid > 0 && pid != app.pid 
                                       && pid != parentPid && pid != MY_PID) {
                            if (r.persistent) {
                                // 將 persistent 進程添加到 firstPids
                                firstPids.add(pid);
                            } else if (r.treatLikeActivity) {
                                firstPids.add(pid);
                            } else {
                                // 其他進程添加到 lastPids
                                lastPids.put(pid, Boolean.TRUE);
                            }
                        }
                    }
                }
            }
        }

        // 記錄 ANR 輸出到 main log
        StringBuilder info = new StringBuilder();
        info.setLength(0);
        info.append("ANR in ").append(app.processName);
        if (activity != null && activity.shortComponentName != null) {
            info.append(" (").append(activity.shortComponentName).append(")");
        }
        info.append("\n");
        info.append("PID: ").append(app.pid).append("\n");
        if (annotation != null) {
            info.append("Reason: ").append(annotation).append("\n");
        }
        if (parent != null && parent != activity) {
            info.append("Parent: ").append(parent.shortComponentName).append("\n");
        }

        // 創建 CPU tracker 對象 
        ProcessCpuTracker processCpuTracker = new ProcessCpuTracker(true);

        ... ...

        // 輸出 traces 信息
        File tracesFile = ActivityManagerService.dumpStackTraces(
                true, firstPids,
                (isSilentANR) ? null : processCpuTracker,
                (isSilentANR) ? null : lastPids,
                nativePids);

        String cpuInfo = null;
        if (ActivityManagerService.MONITOR_CPU_USAGE) {
            mService.updateCpuStatsNow();
            synchronized (mService.mProcessCpuTracker) {
                cpuInfo = mService.mProcessCpuTracker.printCurrentState(anrTime);
            }
            // 記錄當前 CPU 負載情況
            info.append(processCpuTracker.printCurrentLoad());
            info.append(cpuInfo);
        }

        // 記錄從 anr 時間開始的 Cpu 使用情況
        info.append(processCpuTracker.printCurrentState(anrTime));

        // 輸出當前 ANR 的 reason,以及 CPU 使用率、負載信息
        Slog.e(TAG, info.toString());
        if (tracesFile == null) {
            Process.sendSignal(app.pid, Process.SIGNAL_QUIT);
        }
        ... ...
                        
        // 將 traces 文件和 CPU 使用率信息保存到 dropbox,即 data/system/dropbox 目錄
        mService.addErrorToDropBox("anr", app, app.processName,
                          activity, parent, annotation, cpuInfo, tracesFile, null);
        ... ...

        synchronized (mService) {
            mService.mBatteryStatsService.noteProcessAnr(app.processName, app.uid);

            // 後臺 ANR 的情況, 直接殺掉
            if (isSilentANR) {
                app.kill("bg anr", true);
                return;
            }

            // 設置 app 的 ANR 狀態,病查詢錯誤報告 receiver
            makeAppNotRespondingLocked(app,
                    activity != null ? activity.shortComponentName : null,
                    annotation != null ? "ANR " + annotation : "ANR",
                    info.toString());

            // 彈出 ANR 對話框
            Message msg = Message.obtain();
            msg.what = ActivityManagerService.SHOW_NOT_RESPONDING_UI_MSG;
            msg.obj = new AppNotRespondingDialog.Data(app, activity, aboveSystem);

            // 向 ui 線程發送,內容為 SHOW_NOT_RESPONDING_MSG 的消息
            mService.mUiHandler.sendMessage(msg);
        }
    }
    
}

當發生 ANR 時, 會按順序依次執行:

       ✒ 1、輸出 ANR Reason 信息到 EventLog,也就是說 ANR 觸發的時間點最接近的就是 EventLog 中輸出的 am_anr 信息;
       ✒ 2、收集並輸出重要進程列表中的各個線程的 traces 信息,該方法較耗時;
       ✒ 3、輸出當前各個進程的 CPU 使用情況以及 CPU 負載情況
       ✒ 4、將 traces 文件CPU 使用情況信息保存到 dropbox,即 data/system/dropbox 目錄;
       ✒ 5、根據進程類型,來決定直接後臺殺掉,還是彈框告知用戶

ANR輸出重要進程的traces信息,這些進程包含:

       ✒ 1、firstPids 隊列:第一個是 ANR 進程,第二個是 system_server,剩餘是所有 persistent 進程;
       ✒ 2、Native 隊列:是指 /system/bin/ 目錄的 mediaserversdcard 以及 surfaceflinger 進程;
       ✒ 3、lastPids 隊列: 是指 mLruProcesses 中的不屬於 firstPids 的所有進程。

4.2 dumpStackTraces

繼續看看 dump 出 trace 信息的流程:

// ActivityManagerService.java

    public static File dumpStackTraces(boolean clearTraces, ... ,nativePids) {
        ... ...

        if (tracesDirProp.isEmpty()) {
            // 預設為 data/anr/traces.txt
            String globalTracesPath = 
                          SystemProperties.get("dalvik.vm.stack-trace-file", null);

            tracesFile = new File(globalTracesPath);
            try {
                if (clearTraces && tracesFile.exists()) {
                    tracesFile.delete();      // 刪除已存在的 traces 文件
                }

                // 這裡會保證 data/anr/traces.txt 文件內容是全新的方式,而非追加
                tracesFile.createNewFile();   // 創建 traces 文件
                FileUtils.setPermissions(globalTracesPath, 0666, -1, -1);
            } catch (IOException e) {
                Slog.w(TAG, "Unable to prepare ANR traces file: " + tracesFile, e);
                return null;
            }
        } else {
        }

        // 輸出 trace 內容
        dumpStackTraces(tracesFile.getAbsolutePath(), firstPids, nativePids,
                                         extraPids, useTombstonedForJavaTraces);
        return tracesFile;
    }

4.3 dumpStackTraces

// ActivityManagerService.java

    private static void dumpStackTraces(String tracesFile, ...) {

        final DumpStackFileObserver observer;
        if (useTombstonedForJavaTraces) {
            observer = null;
        } else {
            observer = new DumpStackFileObserver(tracesFile);
        }

        // We must complete all stack dumps within 20 seconds.
        long remainingTime = 20 * 1000;
        try {
            if (observer != null) {
                observer.startWatching();
            }

            // 首先,獲取 firstPids 進程的 stacks
            if (firstPids != null) {
                int num = firstPids.size();
                for (int i = 0; i < num; i++) {
                    final long timeTaken;
                    if (useTombstonedForJavaTraces) {
                        timeTaken = dumpJavaTracesTombstoned(firstPids.get(i),
                                                   tracesFile, remainingTime);
                    } else {
                        timeTaken = observer.dumpWithTimeout(firstPids.get(i),
                                                               remainingTime);
                    }
                    ... ... 
                }
            }

            // 下一步,獲取 native 進程的 stacks
            if (nativePids != null) {
                for (int pid : nativePids) {
                    ... ...
                    
                    // 輸出 native 進程的 trace
                    Debug.dumpNativeBacktraceToFileTimeout(
                            pid, tracesFile, (int) (nativeDumpTimeoutMs / 1000));
                            
                    final long timeTaken = SystemClock.elapsedRealtime() - start;
                    ... ...
                }
            }

            // Lastly, dump stacks for all extra PIDs from the CPU tracker.
            if (extraPids != null) {
                ... ...
                }
            }
        } finally {
            if (observer != null) {
                observer.stopWatching();
            }
        }
    }

4.4 小結

觸發 ANR 時系統會輸出關鍵信息:

       ✒ 1、將 am_anr 信息,輸出到 EventLog
       ✒ 2、獲取重要進程 trace 信息,保存到 /data/anr/traces.txt
       ✒ 3、ANR reason 以及 CPU 使用情況信息,輸出到 main log;
       ✒ 4、再將 CPU使用情況 和進程 trace 文件信息,再保存到 /data/system/dropbox


5. 總結

當 Service 出現 ANR 時,最終調用到 AMS.appNotResponding()方法。

       ✒ 1、對於前臺服務,則超時為 SERVICE_TIMEOUT = 20s

       ✒ 2、對於後臺服務,則超時為 SERVICE_BACKGROUND_TIMEOUT = 200s

       ✒ 3、Service 超時檢測機制:超過一定時間沒有執行完相應操作來觸發延時消息,則會觸發 ANR;


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

-Advertisement-
Play Games
更多相關文章
  • 創建類型名稱:LOGGER_FACTORY Type 說明: CREATE OR REPLACE TYPE "LOGGER_FACTORY" AS OBJECT( v_program_owner VARCHAR2(100), v_program_name VARCHAR2(100), v_progr ...
  • [20190423]oradebug peek測試腳本.txt--//工作測試需要寫一個oradebug peek測試腳本,不斷看某個區域記憶體地址的值。1.環境:SCOTT@book> @ ver1PORT_STRING VERSION BANNER x86_64/Linux 2.4.xx 11.2 ...
  • 筆記記錄自林曉斌(丁奇)老師的《MySQL實戰45講》 (本篇內圖片均來自丁奇老師的講解,如有侵權,請聯繫我刪除) 20) --幻讀是什麼,幻讀有什麼問題? 我們先來看看表結構和初始化數據: 表t除主鍵id外還有一個索引c,初始化語句在表中插入了6行數據。那麼如果有下麵這樣一段語句 請問是怎麼加鎖的 ...
  • 本總結來自美團內部分享,屏蔽了內部數據與工具 知識準備 索引 索引是存儲引擎用於快速找到記錄的一種數據結構 B Tree,適用於全鍵值,鍵值範圍或鍵最左首碼:(A,B,C): A, AB, ABC,B,C,BC 哪些列建議創建索引:WHERE, JOIN , GROUP BY, ORDER BY等語 ...
  • 本次主要介紹flink1.5.1版本的本地環境安裝部署,該版本要求jdk版本1.8以上。 ...
  • 1.約束的定義: 約束(constraint):在建表時,為某些列添加一些特定的規則,這些規則稱為約束。約束是在表上強制執行的數據校驗規則保證資料庫的數據滿足某種用戶的要求。添加約束之後,再往表中(插入、更新)數據時,如果數據不滿足約束,則該條語句不能執行。 2.約束的分類: 2.1 非空約束 no ...
  • 本文同步自http://javaexception.com/archives/76 背景: 這幾天做一個複製文本的需求,突然看到QQ上複製昵稱跟QQ號的效果,覺得很不錯,就想要模仿一波,辦法比較簡單粗暴,反編譯QQ獲取了那個.9圖片,然後就是用PopWindow實現了。 解決辦法: 自定義PopWi ...
  • 目前,蘋果開發者賬號可分為三種類型:個人、公司、企業,且三者的費用以及許可權等不盡相同。 1.個人賬號 費用:99 美元/年 協作人數:僅限開發者自己 不需要填寫公司的鄧百氏編碼( D-U-N-S Number) 支持賬號下的 app 上線 App Store 需要創建 Apple ID 2.公司賬號 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...