深入理解 Handler 消息機制

来源:https://www.cnblogs.com/bingxinshuo/archive/2019/09/05/11470573.html
-Advertisement-
Play Games

記得很多年前的一次面試中,面試官問了這麼一個問題, 他的本意應該是考察 RxJava 的使用,只是我的答案是 ,他也就沒有再追問下去了。在早期 Android 開發的荒蕪時代,Handler 的確承擔了項目中大部分的線程切換工作,通常包括子線程更新 UI 和消息傳遞。不光在我們自己的應用中,在整個 ...


記得很多年前的一次面試中,面試官問了這麼一個問題,你在項目中一般如何實現線程切換? 他的本意應該是考察 RxJava 的使用,只是我的答案是 Handler,他也就沒有再追問下去了。在早期 Android 開發的荒蕪時代,Handler 的確承擔了項目中大部分的線程切換工作,通常包括子線程更新 UI 和消息傳遞。不光在我們自己的應用中,在整個 Android 體系中,Handler 消息機制也是極其重要的,不亞於 Binder 的地位。 ActivityThread.java 中的內部類 H 就是一個 Handler,它內部定義了幾十種消息類型來處理一些系統事件。

Handler 的重要性毋庸置疑,今天就通過 AOSP 源碼來深入學習 Handler。相關類的源碼包含註釋均已上傳到我的 Github 倉庫 android_9.0.0_r45 :

Handler.java

Looper.java

Message.java

MessageQueue.java

Handler

Handler 用來發送和處理線程對應的消息隊列 MessageQueue 中存儲的 Message。每個 Handler 實例對應一個線程以及該線程的消息隊列。當你創建一個新的 Handler,它會綁定創建它的線程和消息隊列,然後它會向消息隊列發送 Message 或者 Runnable,並且在它們離開消息隊列時執行。

Handler 有兩個主要用途:

  1. 規劃 Message 或者 Runnable 在未來的某個時間點執行
  2. 在另一個線程上執行代碼

以上翻譯自官方註釋。說白了,Handler 只是安卓提供給開發者用來發送和處理事件的,而消息如何存儲,消息如何迴圈取出,這些邏輯則交給 MessageQueueLooper 來處理,使用者並不需要關心。但要真正瞭解 Handler 消息機制,認真讀一遍源碼就必不可少了。

構造函數

Handler 的構造函數大致上可以分為兩類,先來看第一類:

public Handler() {
    this(null, false);
}

public Handler(Callback callback) {
    this(callback, false);
}

public Handler(Callback callback, boolean async) {
    // 如果是匿名類、內部類、本地類,且沒有使用 static 修飾符,提示可能導致記憶體泄漏
    if (FIND_POTENTIAL_LEAKS) {
        final Class<? extends Handler> klass = getClass();
        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                (klass.getModifiers() & Modifier.STATIC) == 0) {
            Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                klass.getCanonicalName());
        }
    }

    // 從當前線程的 ThreadLocal獲取 Looper
    mLooper = Looper.myLooper();
    if (mLooper == null) {  // 創建 Handler 之前一定要先創建 Looper。主線程已經自動為我們創建。
        throw new RuntimeException(
            "Can't create handler inside thread " + Thread.currentThread()
                    + " that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue; // Looper 持有一個 MessageQueue
    mCallback = callback; // handleMessage 回調
    mAsynchronous = async; // 是否非同步處理
}

這一類構造函數最終調用的都是兩個參數的方法,參數中不傳遞 Looper,所以要顯式檢查是否已經創建 Looper。創建 Handler 之前一定要先創建 Looper,否則會直接拋出異常。在主線程中 Looper 已經自動創建好,無需我們手動創建,在 ActivityThread.javamain() 方法中可以看到。Looper 持有一個消息隊列 MessageQueue,並賦值給 Handler 中的 mQueue 變數。Callback 是一個介面,定義如下:

public interface Callback {
    public boolean handleMessage(Message msg);
}

通過構造器參數傳入 CallBack 也是 Handler 處理消息的一種實現方式。

再回頭看一下在上面的構造函數中是如何獲取當前線程的 Looper 的?

 mLooper = Looper.myLooper(); // 獲取當前線程的 Looper

這裡先記著,回頭看到 Looper 源碼時再詳細解析。

看過 Handler 的第一類構造函數,第二類其實就很簡單了,只是多了 Looper 參數而已:

public Handler(Looper looper) {
    this(looper, null, false);
}
    
public Handler(Looper looper, Callback callback) {
    this(looper, callback, false);
}
    
public Handler(Looper looper, Callback callback, boolean async) {
    mLooper = looper;
    mQueue = looper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

直接賦值即可。

除此之外還有幾個標記為 @hide 的構造函數就不作說明瞭。

發送消息

發送消息大家最熟悉的方法就是 sendMessage(Message msg) 了,可能有人不知道其實還有 post(Runnable r) 方法。雖然方法名稱不一樣,但最後調用的都是同一個方法。

sendMessage(Message msg)
sendEmptyMessage(int what)
sendEmptyMessageDelayed(int what, long delayMillis)
sendEmptyMessageAtTime(int what, long uptimeMillis)
sendMessageAtTime(Message msg, long uptimeMillis)

幾乎所有的 sendXXX() 最後調用的都是 sendMessageAtTime() 方法。

post(Runnable r)
postAtTime(Runnable r, long uptimeMillis)
postAtTime(Runnable r, Object token, long uptimeMillis)
postDelayed(Runnable r, long delayMillis)
postDelayed(Runnable r, Object token, long delayMillis)

所有的 postXXX() 方法都是調用 getPostMessage() 將 參數中的 Runnable 包裝成 Message,再調用對應的 sendXXX() 方法。看一下 getPostMessage() 的代碼:

private static Message getPostMessage(Runnable r) {
    Message m = Message.obtain();
    m.callback = r;
    return m;
}

private static Message getPostMessage(Runnable r, Object token) {
    Message m = Message.obtain();
    m.obj = token;
    m.callback = r;
    return m;
}

主要是把參數中的 Runnable 賦給 Message 的 callback 屬性。

殊途同歸,發送消息的重任最後都落在了 sendMessageAtTime() 身上。

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;
    if (queue == null) {
        RuntimeException e = new RuntimeException(
                this + " sendMessageAtTime() called with no mQueue");
        Log.w("Looper", e.getMessage(), e);
        return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis);
}
    
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis); // 調用 Messagequeue 的 enqueueMessage() 方法
}

Handler 就是一個撒手掌柜,發送消息的任務轉手又交給了 MessageQueue 來處理。

再額外提一點,enqueueMessage() 方法中的參數 uptimeMillis 並不是我們傳統意義上的時間戳,而是調用 SystemClock.updateMillis() 獲取的,它表示自開機以來的毫秒數。

MessageQueue

enqueueMessage()

Message 的入隊工作實際上是由 MessageQueue 通過 enqueueMessage() 函數來完成的。

boolean enqueueMessage(Message msg, long when) {
    if (msg.target == null) { // msg 必須有 target
        throw new IllegalArgumentException("Message must have a target.");
    }
    if (msg.isInUse()) { // msg 不能正在被使用
        throw new IllegalStateException(msg + " This message is already in use.");
    }

    synchronized (this) {
        if (mQuitting) { // 正在退出,回收消息並直接返回
            IllegalStateException e = new IllegalStateException(
                    msg.target + " sending message to a Handler on a dead thread");
            Log.w(TAG, e.getMessage(), e);
            msg.recycle();
            return false;
        }

        msg.markInUse();
        msg.when = when;
        Message p = mMessages;
        boolean needWake;
        if (p == null || when == 0 || when < p.when) {
            // New head, wake up the event queue if blocked.
            // 插入消息隊列頭部,需要喚醒隊列
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            // Inserted within the middle of the queue.  Usually we don't have to wake
            // up the event queue unless there is a barrier at the head of the queue
            // and the message is the earliest asynchronous message in the queue.
            needWake = mBlocked && p.target == null && msg.isAsynchronous();
            Message prev;
            for (;;) {
                prev = p;
                p = p.next;
                if (p == null || when < p.when) { // 按消息的觸發時間順序插入隊列
                    break;
                }
                if (needWake && p.isAsynchronous()) {
                    needWake = false;
                }
            }
            msg.next = p; // invariant: p == prev.next
            prev.next = msg;
        }

        // We can assume mPtr != 0 because mQuitting is false.
        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

從源碼中可以看出來,MessageQueue 是用鏈表結構來存儲消息的,消息是按觸發時間的順序來插入的。

enqueueMessage() 方法是用來存消息的,既然存了,肯定就得取,這靠的是 next() 方法。

next()

Message next() {
    // Return here if the message loop has already quit and been disposed.
    // This can happen if the application tries to restart a looper after quit
    // which is not supported.
    final long ptr = mPtr;
    if (ptr == 0) {
        return null;
    }

    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    int nextPollTimeoutMillis = 0;
    for (;;) {
        if (nextPollTimeoutMillis != 0) {
            Binder.flushPendingCommands();
        }

        // 阻塞方法,主要是通過 native 層的 epoll 監聽文件描述符的寫入事件來實現的。
        // 如果 nextPollTimeoutMillis = -1,一直阻塞不會超時。
        // 如果 nextPollTimeoutMillis = 0,不會阻塞,立即返回。
        // 如果 nextPollTimeoutMillis > 0,最長阻塞nextPollTimeoutMillis毫秒(超時),如果期間有程式喚醒會立即返回。
        nativePollOnce(ptr, nextPollTimeoutMillis);

        synchronized (this) {
            // Try to retrieve the next message.  Return if found.
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            if (msg != null && msg.target == null) {
                // Stalled by a barrier.  Find the next asynchronous message in the queue.
                // msg.target == null表示此消息為消息屏障(通過postSyncBarrier方法發送來的)
                // 如果發現了一個消息屏障,會迴圈找出第一個非同步消息(如果有非同步消息的話),
                // 所有同步消息都將忽略(平常發送的一般都是同步消息)
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
                if (now < msg.when) {
                    // 消息觸發時間未到,設置下一次輪詢的超時時間
                    // Next message is not ready.  Set a timeout to wake up when it is ready.
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // Got a message.
                    // 得到 Message
                    mBlocked = false;
                    if (prevMsg != null) {
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                    msg.markInUse(); // 標記 FLAG_IN_USE
                    return msg;
                }
            } else {
                // No more messages.
                // 沒有消息,會一直阻塞,直到被喚醒
                nextPollTimeoutMillis = -1;
            }

            // Process the quit message now that all pending messages have been handled.
            if (mQuitting) {
                dispose();
                return null;
            }

            // If first time idle, then get the number of idlers to run.
            // Idle handles only run if the queue is empty or if the first message
            // in the queue (possibly a barrier) is due to be handled in the future.
            // Idle handle 僅當隊列為空或者隊列中的第一個消息將要執行時才會運行
            if (pendingIdleHandlerCount < 0
                    && (mMessages == null || now < mMessages.when)) {
                pendingIdleHandlerCount = mIdleHandlers.size();
            }
            if (pendingIdleHandlerCount <= 0) {
                // No idle handlers to run.  Loop and wait some more.
                // 沒有 idle handler 需要運行,繼續迴圈
                mBlocked = true;
                continue;
            }

            if (mPendingIdleHandlers == null) {
                mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
            }
            mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
        }

        // Run the idle handlers.
        // We only ever reach this code block during the first iteration.
        // 只有第一次迴圈時才會執行下麵的代碼塊
        for (int i = 0; i < pendingIdleHandlerCount; i++) {
            final IdleHandler idler = mPendingIdleHandlers[i];
            mPendingIdleHandlers[i] = null; // release the reference to the handler

            boolean keep = false;
            try {
                keep = idler.queueIdle();
            } catch (Throwable t) {
                Log.wtf(TAG, "IdleHandler threw exception", t);
            }

            if (!keep) {
                synchronized (this) {
                    mIdleHandlers.remove(idler);
                }
            }
        }

        // Reset the idle handler count to 0 so we do not run them again.
        // 將 pendingIdleHandlerCount 置零保證不再運行
        pendingIdleHandlerCount = 0;

        // While calling an idle handler, a new message could have been delivered
        // so go back and look again for a pending message without waiting.
        nextPollTimeoutMillis = 0;
    }
}

next() 方法是一個死迴圈,但是當沒有消息的時候會阻塞,避免過度消耗 CPU。nextPollTimeoutMillis 大於 0 時表示等待下一條消息需要阻塞的時間。等於 -1 時表示沒有消息了,一直阻塞到被喚醒。

這裡的阻塞主要靠 native 函數 nativePollOnce() 來完成。其具體原理我並不瞭解,想深入學習的同學可以參考 Gityuan 的相關文 Android消息機制2-Handler(Native層)

MessageQueue 提供了消息入隊和出隊的方法,但它自己並不是自動取消息。那麼,誰來把消息取出來並執行呢?這就要靠 Looper 了。

Looper

創建 Handler 之前必須先創建 Looper,而主線程已經為我們自動創建了 Looper,無需再手動創建,見 ActivityThread.javamain() 方法:

public static void main(String[] args) {
...
 Looper.prepareMainLooper(); // 創建主線程 Looper
...
}

prepareMainLooper()

public static void prepareMainLooper() {
    prepare(false);
    synchronized (Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        sMainLooper = myLooper();
    }
}

sMainLooper 只能被初始化一次,也就是說 prepareMainLooper() 只能調用一次,否則將直接拋出異常。

prepare()

public static void prepare() {
        prepare(true);
}

private static void prepare(boolean quitAllowed) {
    // 每個線程只能執行一次 prepare(),否則會直接拋出異常
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    // 將 Looper 存入 ThreadLocal
    sThreadLocal.set(new Looper(quitAllowed));
}

主線程中調用的是 prepare(false),說明主線程 Looper 是不允許退出的。因為主線程需要源源不斷的處理各種事件,一旦退出,系統也就癱瘓了。而我們在子線程調用 prepare() 來初始化 Looper時,預設調動的是 prepare(true),子線程 Looper 是允許退出的。

每個線程的 Looper 是通過 ThreadLocal 來存儲的,保證其線程私有。

再回到文章開頭介紹的 Handler 的構造函數中 mLooper 變數的初始化:

mLooper = Looper.myLooper();
public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

也是通過當前線程的 ThreadLocal 來獲取的。

構造函數

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed); // 創建 MessageQueue
    mThread = Thread.currentThread(); // 當前線程
}

再對照 Handler 的構造函數:

public Handler(Looper looper, Callback callback, boolean async) {
    mLooper = looper;
    mQueue = looper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

其中的關係就很清晰了。

  • Looper 持有 MessageQueue 對象的引用
  • Handler 持有 Looper 對象的引用以及 Looper 對象的 MessageQueue 的引用

loop()

看到這裡,消息隊列還沒有真正的運轉起來。我們先來看一個子線程使用 Handler 的標準寫法:

class LooperThread extends Thread {
    public Handler mHandler;
  
    public void run() {
        Looper.prepare();
  
        mHandler = new Handler() {
            public void handleMessage(Message msg) {
                // process incoming messages here
            }
        };
  
        Looper.loop();
    }
}

讓消息隊列轉起來的核心就是 Looper.loop()

public static void loop() {
    final Looper me = myLooper(); // 從 ThreadLocal 中獲取當前線程的 Looper
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    final MessageQueue queue = me.mQueue; // 獲取當前線程的消息隊列

   ...  // 省略部分代碼

    for (;;) { // 迴圈取出消息,沒有消息的時候可能會阻塞
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }

        ...  // 省略部分代碼
       

        try {
            msg.target.dispatchMessage(msg); // 通過 Handler 分發 Message
            dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }

        ...  // 省略部分代碼

        msg.recycleUnchecked(); // 將消息放入消息池,以便重覆利用
    }
}

簡單說就是一個死迴圈不停的從 MessageQueue 中取消息,取到消息就通過 Handler 來進行分發,分發之後回收消息進入消息池,以便重覆利用。

從消息隊列中取消息調用的是 MessageQueue.next() 方法,之前已經分析過。在沒有消息的時候可能會阻塞,避免死迴圈消耗 CPU。

取出消息之後進行分發調用的是 msg.target.dispatchMessage(msg)msg.target 是 Handler 對象,最後再來看看 Handler 是如何分發消息的。

public void dispatchMessage(Message msg) {
    if (msg.callback != null) { // callback 是 Runnable 類型,通過 post 方法發送
        handleCallback(msg);
    } else {
        if (mCallback != null) { // Handler 的 mCallback參數 不為空時,進入此分支
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg); // Handler 子類實現的  handleMessage 邏輯
    }
}

private static void handleCallback(Message message) {
    message.callback.run();
}
  • Message 的 callback 屬性不為空時,說明消息是通過 postXXX() 發送的,直接執行 Runnable 即可。
  • Handler 的 mCallback 屬性不為空,說明構造函數中傳入了 Callback 實現,調用 mCallback.handleMessage(msg) 來處理消息
  • 以上條件均不滿足,只可能是 Handler 子類重寫了 handleMessage() 方法。這好像也是我們最常用的一種形式。

Message

之所以把 Message 放在最後說,因為我覺得對整個消息機制有了一個完整的深入認識之後,再來瞭解 Message 會更加深刻。首先來看一下它有哪些重要屬性:

int what :消息標識
int arg1 : 可攜帶的 int 值
int arg2 : 可攜帶的 int 值
Object obj : 可攜帶內容
long when : 超時時間
Handler target : 處理消息的 Handler
Runnable callback : 通過 post() 發送的消息會有此參數

Message 有 public 修飾的構造函數,但是一般不建議直接通過構造函數來構建 Message,而是通過 Message.obtain() 來獲取消息。

obtain()

public static Message obtain() {
    synchronized (sPoolSync) {
        if (sPool != null) {
            Message m = sPool;
            sPool = m.next;
            m.next = null;
            m.flags = 0; // clear in-use flag
            sPoolSize--;
            return m;
        }
    }
    return new Message();
}

sPool 是消息緩存池,鏈表結構,其最大容量 MAX_POOL_SIZE 為 50。obtain() 方法會直接從消息池中取消息,迴圈利用,節約資源。當消息池為空時,再去新建消息。

recycleUnchecked()

還記得 Looper.loop() 方法中最後會調用 msg.recycleUnchecked() 方法嗎?這個方法會回收已經分發處理的消息,並放入緩存池中。

void recycleUnchecked() {
    // Mark the message as in use while it remains in the recycled object pool.
    // Clear out all other details.
    flags = FLAG_IN_USE;
    what = 0;
    arg1 = 0;
    arg2 = 0;
    obj = null;
    replyTo = null;
    sendingUid = -1;
    when = 0;
    target = null;
    callback = null;
    data = null;

    synchronized (sPoolSync) {
        if (sPoolSize < MAX_POOL_SIZE) {
            next = sPool;
            sPool = this;
            sPoolSize++;
        }
    }
}

總結

說到這裡,Handler 消息機制就全部分析完了,相信大家也對整個機制瞭然於心了。

  • Handler 被用來發送消息,但並不是真正的自己去發送。它持有 MessageQueue 對象的引用,通過 MessageQueue 來將消息入隊。
  • Handler 也持有 Looper 對象的引用,通過 Looper.loop() 方法讓消息隊列迴圈起來。
  • Looper 持有 MessageQueue 對象應用,在 loop() 方法中會調用 MessageQueue 的 next() 方法來不停的取消息。
  • loop() 方法中取出來的消息最後還是會調用 Handler 的 dispatchMessage() 方法來進行分發和處理。

最後,關於 Handler 一直有一個很有意思的面試題:

Looper.loop() 是死迴圈為什麼不會卡死主線程 ?

看起來問的好像有點道理,實則不然。你仔細思考一下,loop() 方法的死迴圈和卡死主線程有任何直接關聯嗎?其實並沒有。

回想一下我們經常在測試代碼時候寫的 main() 函數:

public static void main(){
    System.out.println("Hello World");
}

姑且就把這裡當做主線程,它裡面沒有死迴圈,執行完就直接結束了,沒有任何卡頓。但是問題是它就直接結束了啊。在一個 Android 應用的主線程上,你希望它直接就結束了嗎?那肯定是不行的。所以這個死迴圈是必要的,保證程式可以一直運行下去。Android 是基於事件體系的,包括最基本的 Activity 的生命周期都是由事件觸發的。主線程 Handler 必須保持永遠可以相應消息和事件,程式才能正常運行。

另一方面,這並不是一個時時刻刻都在迴圈的死迴圈,當沒有消息的時候,loop() 方法阻塞,並不會消耗大量 CPU 資源。

關於 Handler 就說到這裡了。還記得文章說過線程的 Looper 對象是保存在 ThreadLocal 中的嗎?下一篇文章就來說說 ThreadLocal 是如何保存 線程局部變數 的。

文章首發微信公眾號: 秉心說 , 專註 Java 、 Android 原創知識分享,LeetCode 題解。

更多最新原創文章,掃碼關註我吧!


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

-Advertisement-
Play Games
更多相關文章
  • 一、簡介 在提交大數據作業到集群上運行時,通常需要先將項目打成 JAR 包。這裡以 Maven 為例,常用打包方式如下: 不加任何插件,直接使用 mvn package 打包; 使用 maven assembly plugin 插件; 使用 maven shade plugin 插件; 使用 mav ...
  • Application管理 YARN中,Application是指應用程式,他可能啟動多個運行實例,每個運行實例由一個ApplicationMaster與一組該ApplicationMaster啟動的任務組成,他擁有名稱、隊列名、優先順序等屬性,是一個比較寬泛的概念,可以是一個MapReduce作業、 ...
  • SQL語句的概述 SQL語言的分類 數據定義語言(Data Definition Language)主要用於修改、創建和刪除資料庫對象,其中包括CREATE ALTER DROP語句。 數據查詢語言(Data Query Language)主要用於查詢資料庫中的數據,其主要是SELECT語句,SEL ...
  • MongoDB 更類似 MySQL,支持欄位索引、游標操作,其優勢在於查詢功能比較強大,擅長查詢 JSON 數據,能存儲海量數據,但是不支持事務。 Redis 是一個開源(BSD許可)的,記憶體中的數據結構存儲系統,支持多種類型的數據結構,可用作資料庫,高速緩存和消息隊列代理。 1、記憶體管理機制 Re ...
  • 確保埠與服務正常。 如果都正常,那麼服務有可能沒有添加遠程訪問。 本人鏈接報錯10060,那麼問題出在阿裡雲的主機預設不開放3306埠,那麼在安全組策略中加入對應的埠 登錄後輸入 use mysql,選擇使用mysql資料庫,因為修改遠程連接的基本信息保存在mysql資料庫中,所以使用mysq ...
  • 數據傳輸和採集 Sqoop數據傳輸工具 實際項目開發中,往往很多業務數據是存放在關係型資料庫中,如 MySQL資料庫。我們需要將這些數據集中到數據倉庫中進行管理,便於使用計算模型進行統計、挖掘這類操作。 Sqoop是Apache軟體基金會的⼀一款頂級開源數據傳輸工具,用於在 Hadoop與關係型數據 ...
  • 一、整合說明 Storm 官方對 Kafka 的整合分為兩個版本,官方說明文檔分別如下: + "Storm Kafka Integration" : 主要是針對 0.8.x 版本的 Kafka 提供整合支持; + "Storm Kafka Integration (0.10.x+)" : 包含 Ka ...
  • 首先下載配置android studio ndk 1.打開sdkManager下載CMake和LLDB 2.配置ndk 項目新建 項目建立完畢後,工程目錄如下,cpp文件夾是系統自動生成的 3.自定義 navite方法 接下來開始寫自定義的一個native方法,新建一個Hello.java文件,裡面 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...