Android狀態機StateMachine使用舉例及源碼解析

来源:https://www.cnblogs.com/xiaxveliang/archive/2020/03/02/12395670.html
-Advertisement-
Play Games

Android frameworks源碼StateMachine使用舉例及源碼解析 工作中有一同事說到Android狀態機 。作為一名Android資深工程師,我居然沒有聽說過 ,因此抓緊時間學習一下。 不是 中的相關API,其存在於 層源碼中的一個Java類。可能因為如此,許多應用層的開發人員並未 ...


Android frameworks源碼StateMachine使用舉例及源碼解析

工作中有一同事說到Android狀態機StateMachine。作為一名Android資深工程師,我居然沒有聽說過StateMachine,因此抓緊時間學習一下。
StateMachine不是Android SDK中的相關API,其存在於frameworks層源碼中的一個Java類。可能因為如此,許多應用層的開發人員並未使用過。
因此這裡我們先說一下StateMachine的使用方式,然後再對源碼進行相關介紹。

  • StateMachine使用舉例
  • StateMachine原理學習

一、StateMachine使用舉例

StateMachine 處於Android frameworks層源碼frameworks/base/core/java/com/android/internal/util路徑下。應用層若要使用StateMachine需將對應路徑下的三個類拷貝到自己的工程目錄下。
這三個類分別為:StateMachine.javaStateIState

下邊是使用的代碼舉例,這個例子我也是網路上找的(讀懂StateMachine源碼後,我對這個例子進行了一些簡單更改,以下為更改後的案例):

主要分以下幾個部分來說明:

  • PersonStateMachine.java案例代碼
  • PersonStateMachine 使用
  • 案例的簡單說明
  • 案例源碼下載

1.1、PersonStateMachine.java

創建PersonStateMachine繼承StateMachine類。
創建四種狀態,四種狀態均繼承自State

  • 預設狀態 BoringState
  • 工作狀態 WorkState
  • 吃飯狀態 EatState
  • 睡覺狀態 SleepState

定義了狀態轉換的四種消息類型:

  • 喚醒消息 MSG_WAKEUP
  • 睏乏消息 MSG_TIRED
  • 餓了消息 MSG_HUNGRY
  • 狀態機停止消息 MSG_HALTING

下麵來看完整的案例代碼:

public class PersonStateMachine extends StateMachine {

    private static final String TAG = "MachineTest";

    //設置狀態改變標誌常量
    public static final int MSG_WAKEUP = 1; // 消息:醒
    public static final int MSG_TIRED = 2; // 消息:困
    public static final int MSG_HUNGRY = 3; // 消息:餓
    private static final int MSG_HALTING = 4; // 狀態機暫停消息

    //創建狀態
    private State mBoringState = new BoringState();// 預設狀態
    private State mWorkState = new WorkState(); // 工作
    private State mEatState = new EatState(); // 吃
    private State mSleepState = new SleepState(); // 睡

    /**
     * 構造方法
     *
     * @param name
     */
    PersonStateMachine(String name) {
        super(name);
        //加入狀態,初始化狀態
        addState(mBoringState, null);
        addState(mSleepState, mBoringState);
        addState(mWorkState, mBoringState);
        addState(mEatState, mBoringState);

        // sleep狀態為初始狀態
        setInitialState(mSleepState);
    }

    /**
     * @return 創建啟動person 狀態機
     */
    public static PersonStateMachine makePerson() {
        PersonStateMachine person = new PersonStateMachine("Person");
        person.start();
        return person;
    }


    @Override
    protected void onHalting() {
        synchronized (this) {
            this.notifyAll();
        }
    }


    /**
     * 定義狀態:無聊
     */
    class BoringState extends State {
        @Override
        public void enter() {
            Log.e(TAG, "############ enter Boring ############");
        }

        @Override
        public void exit() {
            Log.e(TAG, "############ exit Boring ############");
        }

        @Override
        public boolean processMessage(Message msg) {
            Log.e(TAG, "BoringState  processMessage.....");
            return true;
        }
    }

    /**
     * 定義狀態:睡覺
     */
    class SleepState extends State {
        @Override
        public void enter() {
            Log.e(TAG, "############ enter Sleep ############");
        }

        @Override
        public void exit() {
            Log.e(TAG, "############ exit Sleep ############");
        }

        @Override
        public boolean processMessage(Message msg) {
            Log.e(TAG, "SleepState  processMessage.....");
            switch (msg.what) {
                // 收到清醒信號
                case MSG_WAKEUP:
                    Log.e(TAG, "SleepState  MSG_WAKEUP");
                    // 進入工作狀態
                    transitionTo(mWorkState);
                    //...
                    //...
                    //發送餓了信號...
                    sendMessage(obtainMessage(MSG_HUNGRY));
                    break;
                case MSG_HALTING:
                    Log.e(TAG, "SleepState  MSG_HALTING");

                    // 轉化到暫停狀態
                    transitionToHaltingState();
                    break;
                default:
                    return false;
            }
            return true;
        }
    }


    /**
     * 定義狀態:工作
     */
    class WorkState extends State {
        @Override
        public void enter() {
            Log.e(TAG, "############ enter Work ############");
        }

        @Override
        public void exit() {
            Log.e(TAG, "############ exit Work ############");
        }

        @Override
        public boolean processMessage(Message msg) {
            Log.e(TAG, "WorkState  processMessage.....");
            switch (msg.what) {
                // 收到 餓了 信號
                case MSG_HUNGRY:
                    Log.e(TAG, "WorkState  MSG_HUNGRY");
                    // 吃飯狀態
                    transitionTo(mEatState);
                    //...
                    //...
                    // 發送累了信號...
                    sendMessage(obtainMessage(MSG_TIRED));
                    break;
                default:
                    return false;
            }
            return true;
        }
    }

    /**
     * 定義狀態:吃
     */
    class EatState extends State {
        @Override
        public void enter() {
            Log.e(TAG, "############ enter Eat ############");
        }

        @Override
        public void exit() {
            Log.e(TAG, "############ exit Eat ############");
        }

        @Override
        public boolean processMessage(Message msg) {
            Log.e(TAG, "EatState  processMessage.....");
            switch (msg.what) {
                // 收到 困了 信號
                case MSG_TIRED:
                    Log.e(TAG, "EatState  MSG_TIRED");
                    // 睡覺
                    transitionTo(mSleepState);
                    //...
                    //...
                    // 發出結束信號...
                    sendMessage(obtainMessage(MSG_HALTING));
                    break;
                default:
                    return false;
            }
            return true;
        }

    }
}

1.2、PersonStateMachine 使用

// 獲取 狀態機引用
PersonStateMachine personStateMachine = PersonStateMachine.makePerson();
// 初始狀態為SleepState,發送消息MSG_WAKEUP
personStateMachine.sendMessage(PersonStateMachine.MSG_WAKEUP);
  • SleepState狀態收到MSG_WAKEUP消息後,會執行對應狀態的processMessage方法
  • SleepState類中processMessage方法收到MSG_WAKEUP消息後,執行transitionTo(mWorkState)方法,完成狀態轉換。轉換到WorkState狀態。

1.3、案例的簡單說明

幾種狀態的依賴關係如下:
在這裡插入圖片描述

構造方法中,添加所有狀態,並設置初始狀態:

PersonStateMachine(String name) {
    super(name);
    //加入狀態,初始化狀態
    addState(mBoringState, null);
    addState(mSleepState, mBoringState);
    addState(mWorkState, mBoringState);
    addState(mEatState, mBoringState);
    
    // sleep狀態為初始狀態
    setInitialState(mSleepState);
}

通過以下方法,創建並啟動狀態機:

public static PersonStateMachine makePerson() {
    PersonStateMachine person = new PersonStateMachine("Person");
    person.start();
    return person;
}

1.4、案例源碼下載

Android_StateMachine案例地址

二、實現原理學習

StateMachine中,開啟了一個線程HandlerThread,其對應的Handler為SmHandler。因此上文案例中對應狀態的 processMessage(Message msg)方法,均在HandlerThread線程中執行。

2.1、首先從StateMachine的構造方法說起,對應的代碼如下:

protected StateMachine(String name) {
    // 創建 HandlerThread
    mSmThread = new HandlerThread(name);
    mSmThread.start();
    // 獲取HandlerThread對應的Looper
    Looper looper = mSmThread.getLooper();
    // 初始化 StateMachine
    initStateMachine(name, looper);
}
  • StateMachine的構造方法中,創建並啟動了一個線程HandlerThread
  • initStateMachine方法中,創建了HandlerThread線程對應的Handler SmHandler
private void initStateMachine(String name, Looper looper) {
    mName = name;
    mSmHandler = new SmHandler(looper, this);
}
  • SmHandler構造方法中,向狀態機中添加了兩個狀態:一個狀態為狀態機的暫停狀態mHaltingState、一個狀態為狀態機的退出狀態mQuittingState
private SmHandler(Looper looper, StateMachine sm) {
    super(looper);
    mSm = sm;

    // 添加狀態:暫停 和 退出
    // 這兩個狀態 無父狀態
    addState(mHaltingState, null);
    addState(mQuittingState, null);
}
  • mHaltingState狀態,顧名思義讓狀態機暫停,其對應的processMessage(Message msg)方法,返回值為true,將消息消費掉,但不處理消息。從而使狀態機狀態停頓到mHaltingState狀態
  • mQuittingState狀態,若進入該狀態, 狀態機將退出。HandlerThread線程對應的Looper將退出,HandlerThread線程會被銷毀,所有加入到狀態機的狀態被清空。

2.2、狀態機的start() 方法

狀態機的初始化說完,下邊來說狀態機的啟動方法start()

public void start() {
    // mSmHandler can be null if the state machine has quit.
    SmHandler smh = mSmHandler;
    // StateMachine 未進行初始化,為什麼不拋出一個異常
    if (smh == null) {
        return;
    }
    // 完成狀態機建設
   smh.completeConstruction();
}
  • 從以上代碼可以看到,其中只有一個方法completeConstruction(),用於完成狀態機的建設。
private final void completeConstruction() {
    int maxDepth = 0;
    // 迴圈判斷所有狀態,看看哪一個鏈最長,得出深度
    for (StateInfo si : mStateInfoHashMap.values()) {
        int depth = 0;
        for (StateInfo i = si; i != null; depth++) {
            i = i.parentStateInfo;
        }
        if (maxDepth < depth) {
            maxDepth = depth;
        }
    }
    // 狀態堆棧
    mStateStack = new StateInfo[maxDepth];
    // 臨時狀態堆棧
    mTempStateStack = new StateInfo[maxDepth];
    // 初始化堆棧
    setupInitialStateStack();

    // 發送初始化完成的消息(消息放入到隊列的最前邊)
    sendMessageAtFrontOfQueue(obtainMessage(SM_INIT_CMD, mSmHandlerObj));
}
  • maxDepth是狀態機中,最長依賴鏈的長度。
  • mStateStackmTempStateStack為兩個用數組實現的堆棧。這兩個堆棧的最大長度,即為maxDepth。其用來存儲當前活躍狀態當前活躍狀態的父狀態、父父狀態、...等
  • setupInitialStateStack();完成狀態的初始化,將當前的活躍狀態放入到mStateStack堆棧中。

下邊來具體說setupInitialStateStack();方法中,如何完成棧的初始化。

private final void setupInitialStateStack() {
    // 獲取初始狀態信息
    StateInfo curStateInfo = mStateInfoHashMap.get(mInitialState);
    //
    for (mTempStateStackCount = 0; curStateInfo != null; mTempStateStackCount++) {
        // 初始狀態 放入臨時堆棧
        mTempStateStack[mTempStateStackCount] = curStateInfo;
        // 當前狀態的 所有父狀態 一級級放入堆棧
        curStateInfo = curStateInfo.parentStateInfo;
    }

    // 清空 狀態堆棧
    // Empty the StateStack
    mStateStackTopIndex = -1;
    // 臨時堆棧 換到 狀態堆棧
    moveTempStateStackToStateStack();
}
  • 拿案例中狀態來舉例,將初始化狀態放入 mTempStateStack堆棧中
  • 初始化狀態父狀態父父狀態父父父狀態... 都一一放入到mTempStateStack堆棧中

enter description here

  • 然後moveTempStateStackToStateStack()方法中,mTempStateStack出棧,mStateStack入棧,將所有狀態信息導入到mStateStack堆棧,並清空mTempStateStack堆棧。

enter description here

到這裡,初始化基本完成,但我們還落下一部分代碼沒有說:

// 發送初始化完成的消息(消息放入到隊列的最前邊)
sendMessageAtFrontOfQueue(obtainMessage(SM_INIT_CMD, mSmHandlerObj));
  • 發送一個初始化完成的消息到SmHandler當中。

下邊來看一下SmHandlerhandleMessage(Message msg)方法:

public final void handleMessage(Message msg) {

    // 處理消息
    if (!mHasQuit) {
        // 保存傳入的消息
        mMsg = msg;
        State msgProcessedState = null;
        // 已完成初始化
        if (mIsConstructionCompleted) {
        // ..
        }
        // 接收到 初始化完成的消息
        else if (!mIsConstructionCompleted
                && (mMsg.what == SM_INIT_CMD) && (mMsg.obj == mSmHandlerObj)) {
            /** Initial one time path. */
            // 初始化完成
            mIsConstructionCompleted = true;
            // 調用堆棧中狀態的enter方法,並將堆棧中的狀態設置為活躍狀態
            invokeEnterMethods(0);
        } else {
        // ..
        }
        // 執行Transition
        performTransitions(msgProcessedState, msg);
    }
}
  • 接收到初始化完成的消息後mIsConstructionCompleted = true;對應的標誌位變過來
  • 執行 invokeEnterMethods方法將mStateStack堆棧中的所有狀態設置為活躍狀態,並由父—>子的順序,執行堆棧中狀態的enter()方法
  • performTransitions(msgProcessedState, msg);在start()時,其中的內容全部不執行,因此先不介紹。

invokeEnterMethods方法的方法體如下:

private final void invokeEnterMethods(int stateStackEnteringIndex) {
    for (int i = stateStackEnteringIndex; i <= mStateStackTopIndex; i++) {
        if (mDbg) mSm.log("invokeEnterMethods: " + mStateStack[i].state.getName());
        mStateStack[i].state.enter();
        mStateStack[i].active = true;
    }
}
  • 可以看到,其將mStateStack堆棧中的所有狀態設置為活躍狀態,並由父—>子的順序,執行堆棧中狀態的enter()方法

到此start()完成,最終mStateStack堆棧狀態,也如上圖所示。

2.3、狀態轉化

還是拿案例中的代碼舉例:

// 獲取 狀態機引用
PersonStateMachine personStateMachine = PersonStateMachine.makePerson();
// 初始狀態為SleepState,發送消息MSG_WAKEUP
personStateMachine.sendMessage(PersonStateMachine.MSG_WAKEUP);
  • 通過調用sendMessage(PersonStateMachine.MSG_WAKEUP);方法,向SmHandler中發送一個消息,來觸髮狀態轉化。
  • 可以說 sendMessage(PersonStateMachine.MSG_WAKEUP);消息,為狀態轉化的導火索。

下邊,再次看一下SmHandlerhandleMessage(Message msg)方法:

public final void handleMessage(Message msg) {
    // 處理消息
    if (!mHasQuit) {
        // 保存傳入的消息
        mMsg = msg;
        State msgProcessedState = null;
        // 已完成初始化
        if (mIsConstructionCompleted) {
            // 處理消息的狀態
            msgProcessedState = processMsg(msg);
        }
        // 接收到 初始化完成的消息
        else if (!mIsConstructionCompleted
                && (mMsg.what == SM_INIT_CMD) && (mMsg.obj == mSmHandlerObj)) {
            // 初始化完成
            mIsConstructionCompleted = true;
            // 調用堆棧中狀態的enter方法,並將堆棧中的狀態設置為活躍狀態
            invokeEnterMethods(0);
        } else {
            throw new RuntimeException("StateMachine.handleMessage: "
                    + "The start method not called, received msg: " + msg);
        }
        // 執行Transition
        performTransitions(msgProcessedState, msg);
    }
}
  • 因為初始化已經完成,代碼會直接走到processMsg(msg);方法中。

我們來看processMsg(msg);方法:

private final State processMsg(Message msg) {
    // 堆棧中找到當前狀態
    StateInfo curStateInfo = mStateStack[mStateStackTopIndex];
    // 是否為退出消息
    if (isQuit(msg)) {
        // 轉化為退出狀態
        transitionTo(mQuittingState);
    } else {
        // 狀態返回true 則是可處理此狀態
        // 狀態返回false 則不可以處理
        while (!curStateInfo.state.processMessage(msg)) {
            // 當前狀態的父狀態
            curStateInfo = curStateInfo.parentStateInfo;
            // 父狀態未null
            if (curStateInfo == null) {
                // 回調到未處理消息方法中
                mSm.unhandledMessage(msg);
                break;
            }
        }
    }
    // 消息處理後,返回當前狀態信息
    // 如果消息不處理,則返回其父狀態處理,返回處理消息的父狀態
    return (curStateInfo != null) ? curStateInfo.state : null;
}
  • 代碼會直接走到while (!curStateInfo.state.processMessage(msg))
    執行mStateStack堆棧中,最上層狀態的 processMessage(msg)方法。案例中這個狀態為SleepState
  • 這裡
    如果mStateStack堆棧中狀態的processMessage(msg)方法返回true,則表示其消費掉了這個消息;
    如果其返回false,則表示不消費此消息,那麼該消息將繼續向其父狀態進行傳遞;
  • 最終將返回,消費掉該消息的狀態。

這裡,堆棧對上層的狀態為SleepState。所以我們看一下其對應的processMessage(msg)方法。

public boolean processMessage(Message msg) {
    switch (msg.what) {
        // 收到清醒信號
        case MSG_WAKEUP:
            // 進入工作狀態
            transitionTo(mWorkState);
            //...
            //...
            //發送餓了信號...
            sendMessage(obtainMessage(MSG_HUNGRY));
            break;
        case MSG_HALTING:
        // ...
            break;
        default:
            return false;
    }
    return true;
}
  • 在SleepState狀態的processMessage(Message msg)方法中,其收到MSG_WAKEUP消息後,會調用transitionTo(mWorkState);方法,將目標狀態設置為mWorkState

我們看一下transitionTo(mWorkState);方法:

private final void transitionTo(IState destState) {
    mDestState = (State) destState;
}
  • 可以看到,transitionTo(IState destState)方法,只是一個簡單的狀態賦值。

下邊我們回到SmHandlerhandleMessage(Message msg)方法:

  • 代碼會執行到SmHandler.handleMessage(Message msg)performTransitions(msgProcessedState, msg);方法之中。
  • 而這裡我們傳入的參數msgProcessedStatemSleepState
private void performTransitions(State msgProcessedState, Message msg) {
    // 當前狀態
    State orgState = mStateStack[mStateStackTopIndex].state;
    // ...
    // 目標狀態
    State destState = mDestState;
    if (destState != null) {
        while (true) {
            // 目標狀態 放入temp 堆棧
            // 目標狀態的 父狀態 作為參數 傳入下一級
            StateInfo commonStateInfo = setupTempStateStackWithStatesToEnter(destState);
            // commonStateInfo 狀態的子狀態全部退棧
            invokeExitMethods(commonStateInfo);
            // 目標狀態入棧
            int stateStackEnteringIndex = moveTempStateStackToStateStack();
            // 入棧狀態 活躍
            invokeEnterMethods(stateStackEnteringIndex);
            //...
            moveDeferredMessageAtFrontOfQueue();

            if (destState != mDestState) {
                // A new mDestState so continue looping
                destState = mDestState;
            } else {
                // No change in mDestState so we're done
                break;
            }
        }
        mDestState = null;
    }
    // ...
}
  • 以上方法中 傳入的參數msgProcessedStatemSleepState
  • 方法中destState目標狀態為 mWorkState

此時此刻performTransitions(State msgProcessedState, Message msg)方法中內容的執行示意圖如下:

A、目標狀態放入到mTempStateStack隊列中
// 目標狀態 放入temp 堆棧
// 目標狀態的 父狀態 作為參數 傳入下一級
StateInfo commonStateInfo = setupTempStateStackWithStatesToEnter(destState);
  • 1、將WorkState狀態放入到mTempStateStack堆棧中
  • 2、將WorkState狀態的非活躍父狀態一一入mTempStateStack堆棧
  • 3、因為WorkState狀態的父狀態為BoringState,是活躍狀態,因此只將WorkState放入到mTempStateStack堆棧中
  • 4、返回活躍的父狀態BoringState

以上代碼的執行示意圖如下:
enter description here

B、commonStateInfo狀態在mStateStack堆棧中的子狀態退堆棧

commonStateInfosetupTempStateStackWithStatesToEnter(destState);方法的返回參數。這裡是BoringState

// commonStateInfo 狀態的子狀態全部退棧
invokeExitMethods(commonStateInfo);
  • 1、BoringState作為參數傳入到invokeExitMethods(commonStateInfo);方法中
  • 2、其方法內容為,將BoringState狀態的全部子狀態退堆棧

以上代碼的執行示意圖如下:
在這裡插入圖片描述

C、mTempStateStack全部狀態出堆棧,mStateStack入堆棧
// 目標狀態入棧
int stateStackEnteringIndex = moveTempStateStackToStateStack();
// 入棧狀態 活躍
invokeEnterMethods(stateStackEnteringIndex);
  • moveTempStateStackToStateStack方法中:mTempStateStack全部狀態出堆棧,mStateStack入堆棧
  • invokeEnterMethods(stateStackEnteringIndex);方法中,將新加入的狀態設置為活躍狀態;並調用其對應的enter()方法。

最終的堆棧狀態為:

在這裡插入圖片描述

到此StateMachine的源碼講解完成。
感興趣的同學,還是自己讀一遍源碼吧,希望我的這篇文章可以為你的源碼閱讀提供一些幫助。

========== THE END ==========

wx_gzh.jpg


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

-Advertisement-
Play Games
更多相關文章
  • SparseArray源碼來自:android 25/java/util/SparseArray ArrayMap源碼來自:25.3.1/support compat 25.3.1/android/android.support.v4.util.ArrayMap 一、SparseArray實現源碼學 ...
  • 英文原文地址 "Memory optimization for feeds on Android" 讀後感 在Java中HashSet只能存放繼承自Objcet的對象,這中情況下“基本數據類型”轉化為繼承自Object的( 、`Long`等)會產生很多中間Object對象,占用過多的記憶體,從而引發垃 ...
  • 效果圖 實現源碼(已上傳我的GitHub): "https://github.com/xiaxveliang/GL_AUDIO_VIDEO_RECODE" 參考: "http://bigflake.com/mediacodec/EncodeAndMuxTest.java.txt" 對於以上代碼,我做 ...
  • obj文件是3D模型文件格式。由Alias|Wavefront公司為3D建模和動畫軟體"Advanced Visualizer"開發的一種標準,適合用於3D軟體模型之間的互導,也可以通過Maya讀寫。 + 只支持模型三角面數據和材質信息,無動畫功能支持; + 其中幾何信息由.obj文件提供,材質信息 ...
  • Mac下Jenkins Android打包 一、安裝tomcat a、下載tomcat http://tomcat.apache.org/ 下載完成後解壓到: b、啟動tomcat: c、驗證 二、安裝Jenkins a、下載 jenkins.war https://jenkins.io/index ...
  • 偶然發現 ,僅僅一個Java文件,可在嵌入式設備(例:Android手機)中啟動一個本地伺服器,接收客戶端本地部分請求。 認真學習了其源碼實現,這裡按照我的學習順序寫了一篇簡單的文章(算是學習筆記吧): + 瞭解官方描述 + 寫個Demo使用一下(Android中本地代理,播放Sdcard中的m3u ...
  • Android Q 深色主題舉例 瞭解深色主題如何應用,第一手資料是 "官方文檔" 與 相應的 "Google Sample" 官方文檔:DayNight — Adding a dark theme to your app: "https://medium.com/androiddevelopers ...
  • QQ視頻通話、抖音的視頻回顯 是如何實現的 先說為什麼會有這一篇文章: 2014年聯想曾經做過一款 短視頻軟體,叫“魔力秀”。可以說和現在的抖音基本是一樣的,但因為“魔力秀App”出生於聯想,註定無法在一個硬體公司成長為一棵參天大樹,最終只發了一個版本就結束了。 當時“魔力秀App”的視頻回顯模塊是 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...