我所知道的Handler

来源:https://www.cnblogs.com/android-lol/archive/2023/05/17/17410693.html
-Advertisement-
Play Games

簡單講,handler就是兩個功能 插入消息,enqueuemessage,msg,when 從消息隊列中遍歷所有消息,比對msg.when和當前的when,找到合適的位置插入 處理消息,looper.loop會從messagequeue中調用next。取消息,如果消息還沒到時間該執行,就會比對時間 ...


簡單講,handler就是兩個功能

插入消息,enqueuemessage,msg,when
從消息隊列中遍歷所有消息,比對msg.when和當前的when,找到合適的位置插入

處理消息,looper.loop會從messagequeue中調用next。取消息,如果消息還沒到時間該執行,就會比對時間,下次輪詢就通過binder寫入,native函數休眠,到時間喚醒執行。

handler記憶體泄漏

GCRoot 一般是靜態變數或者常量可以作為GCROOT
GCROOT 是ThreadLocal,存在於Looper中,Looper被載入就存在,
handler持有activity或者fragment,handler又被message持有,message的target屬性,message被messagequeue持有,messagequeue被looper中的threadlocal持有

java中匿名內部類會預設持有外部類的引用

打斷持有鏈

  1. handler.removemesage handler.removecallbacks
  2. handler使用static修飾。

主線程的Looper不允許退出

處理消息,looper取出來後,調用message.tager.dispatchemesage後面調用了handler的handlemessage方法。

還有個callback對象,如果有callback,dispatch會先執行callback的處理,calllback返回true,後面就不處理了,callback返回false就給handler的handlemessage處理了

Meesage對象創建

message創建用的obtain,池化,頻繁的創建銷毀會導致記憶體不穩定,抖動,造成卡頓 oom等問題

message pool的最大緩存50

阻塞和休眠,阻塞是被動的,休眠是主動的,阻塞不會讓出cpu,休眠會,thread.yield會讓出cpu。

子線程主線程通信

handler,livedata,eventbus,flow,rxjava,broadcast,觀察者模式不能跨線程

最終都是handler完成的。

Handler監聽卡頓

原理是在looper內完成的,looper處理消息的時候,會列印內容,就是Printer,looper可以設置它。

Message消息的分類

同步消息,普通的消息都是同步消息
非同步消息,創建handler的時候設置async為true即可
同步屏障,需要通過反射調用,app層無法直接調用,是messagequeue提供的posSyncBarrier方法實現的,返回一個token,時msg的arg1值,用它來取消同步屏障。和普通消息的區別是,msg。target屬性為null。

刷新UI的消息是非同步消息,發送前先插入了一個同步屏障消息,非同步消息處理完成後,要將同步屏障消息移除隊列

消息入隊

handler消息加入隊列,有一系列方法,如下:

// 發送空消息
public final boolean sendEmptyMessage(int what){}
public final boolean sendEmptyMessageDelayed(int what,long delay){}
public final boolean sendEmptyMessageAtTime(int what,long when){}
// 發送消息
public final boolean sendMessage(@NonNull Message msg){}
public final boolean sendMessageDelayed(@NonNull Message msg,long time){}
public final boolean sendMessageAtTime(@NonNull Message msg,long when){}

public final boolean sendMessageAtFrontOfQueue(Message msg) {}
// post發送
public final boolean post(@NonNull Runnable r) {}
public final boolean postAtTime(@NonNull Runnable r, long uptimeMillis) {}
public final boolean postAtTime(  
@NonNull Runnable r, @Nullable Object token, long uptimeMillis) {}
public final boolean postDelayed(@NonNull Runnable r, long delayMillis) {}
public final boolean postDelayed(Runnable r, int what, long delayMillis) {}
public final boolean postDelayed(  
        @NonNull Runnable r, @Nullable Object token, long delayMillis) {}
public final boolean postAtFrontOfQueue(@NonNull Runnable r) {}

// enqueue
public final boolean executeOrSendMessage(@NonNull Message msg) {}
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,  
        long uptimeMillis) {}

最終都是掉用的enqueueMessage加入隊列

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,  
        long uptimeMillis) {  
    msg.target = this;  // 綁定消息處理對象
    msg.workSourceUid = ThreadLocalWorkSource.getUid();  
  
    if (mAsynchronous) { // 根據handler創建是否是非同步的,來將消息標記為非同步消息 
        msg.setAsynchronous(true);  
    }  
    // 調用messagequeue的方法,加入隊列。
    return queue.enqueueMessage(msg, uptimeMillis);  
}

下麵看下消息如何入隊的

boolean enqueueMessage(Message msg, long when) {  
    if (msg.target == null) {  
        throw new IllegalArgumentException("Message must have a target.");  
    }  
  
    synchronized (this) {  
        if (msg.isInUse()) {  
            throw new IllegalStateException(msg + " This message is already in use.");  
        }  
  
        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.  
            // 消息隊列為null,或者消息是立刻執行的,或者要執行的時間先於頭消息的時間,將當前消息作為新的隊列的頭
            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;  
        }  
		// 如果需要的話,喚醒隊列,開始處理消息,就是MessageQueue的next方法開始執行
        // We can assume mPtr != 0 because mQuitting is false.  
        if (needWake) {  
            nativeWake(mPtr);  
        }  
    }  
    return true;  
}

取消息以及消息處理

取消息1

取消息入口是Looper的loop方法處理的

public static void loop() {  
    final Looper me = myLooper();  
    if (me == null) {  
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");  
    }  
    if (me.mInLoop) {  
        Slog.w(TAG, "Loop again would have the queued messages be executed"  
                + " before this one completed.");  
    }  
  
    me.mInLoop = true;  
  
    // Make sure the identity of this thread is that of the local process,  
    // and keep track of what that identity token actually is.    Binder.clearCallingIdentity();  
    final long ident = Binder.clearCallingIdentity();  
  
    // Allow overriding a threshold with a system prop. e.g.  
    // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'    final int thresholdOverride =  
            SystemProperties.getInt("log.looper."  
                    + Process.myUid() + "."  
                    + Thread.currentThread().getName()  
                    + ".slow", 0);  
  
    me.mSlowDeliveryDetected = false;  
	// 無限迴圈,通過loopOnce取消息
    for (;;) {  
        if (!loopOnce(me, ident, thresholdOverride)) {  
            return;  
        }  
    }  
}

處理消息

private static boolean loopOnce(final Looper me,  
        final long ident, final int thresholdOverride) {  
    // 取消息
    Message msg = me.mQueue.next(); // might block  
    if (msg == null) {  
        // No message indicates that the message queue is quitting.  
        return false;  
    }  
  
    // This must be in a local variable, in case a UI event sets the logger  
    final Printer logging = me.mLogging;  
    if (logging != null) {  
        logging.println(">>>>> Dispatching to " + msg.target + " "  
                + msg.callback + ": " + msg.what);  
    }  
    // Make sure the observer won't change while processing a transaction.  
    final Observer observer = sObserver;  
  
    final long traceTag = me.mTraceTag;  
    long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;  
    long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;  
    if (thresholdOverride > 0) {  
        slowDispatchThresholdMs = thresholdOverride;  
        slowDeliveryThresholdMs = thresholdOverride;  
    }  
    final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);  
    final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);  
  
    final boolean needStartTime = logSlowDelivery || logSlowDispatch;  
    final boolean needEndTime = logSlowDispatch;  
  
    if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {  
        Trace.traceBegin(traceTag, msg.target.getTraceName(msg));  
    }  
  
    final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;  
    final long dispatchEnd;  
    Object token = null;  
    if (observer != null) {  
        token = observer.messageDispatchStarting();  
    }  
    long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);  
    try {  
	    // 這裡取出msg的target屬性,就是handler對象,進行消息的處理。註意:屏障消息是沒有target的。
        msg.target.dispatchMessage(msg);  
        if (observer != null) {  
            observer.messageDispatched(token, msg);  
        }  
        dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;  
    } catch (Exception exception) {  
        if (observer != null) {  
            observer.dispatchingThrewException(token, msg, exception);  
        }  
        throw exception;  
    } finally {  
        ThreadLocalWorkSource.restore(origWorkSource);  
        if (traceTag != 0) {  
            Trace.traceEnd(traceTag);  
        }  
    }  
    if (logSlowDelivery) {  
        if (me.mSlowDeliveryDetected) {  
            if ((dispatchStart - msg.when) <= 10) {  
                Slog.w(TAG, "Drained");  
                me.mSlowDeliveryDetected = false;  
            }  
        } else {  
            if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",  
                    msg)) {  
                // Once we write a slow delivery log, suppress until the queue drains.  
                me.mSlowDeliveryDetected = true;  
            }  
        }  
    }  
    // 這裡會列印那些執行慢的消息
    if (logSlowDispatch) {  
        showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);  
    }  
  
    if (logging != null) {  
        logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);  
    }  
  
    // Make sure that during the course of dispatching the  
    // identity of the thread wasn't corrupted.    final long newIdent = Binder.clearCallingIdentity();  
    if (ident != newIdent) {  
        Log.wtf(TAG, "Thread identity changed from 0x"  
                + Long.toHexString(ident) + " to 0x"  
                + Long.toHexString(newIdent) + " while dispatching to "  
                + msg.target.getClass().getName() + " "  
                + msg.callback + " what=" + msg.what);  
    }  
  
    msg.recycleUnchecked();  
  
    return true;  
}

取消息2

再看下實際的取消息的方法,MessageQueue的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();  
        }  
		// 這個方法會在沒有消息的時候阻塞
        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.  
                // 這裡處理同步屏障消息,如果當前消息是非同步消息,就跳出迴圈,否則繼續迴圈
                do {  
                    prevMsg = msg;  
                    msg = msg.next;  
                } while (msg != null && !msg.isAsynchronous()); 
                // 迴圈的作用,找出隊列里的非同步消息,存儲在msg里,或者將同步屏障消息存儲在msg中,prevMsg存儲的是同步消息 
            }  
            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.  
                    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(); 
                    // 將取出的消息交給handler處理。 
                    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.            
            if (pendingIdleHandlerCount < 0  
                    && (mMessages == null || now < mMessages.when)) {  
                pendingIdleHandlerCount = mIdleHandlers.size();  
            }  
            if (pendingIdleHandlerCount <= 0) {  
                // No idle handlers to run.  Loop and wait some more.  
                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 = 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;  
    }  
}

handler的消息處理

是從dispatchMessage方法開始的,裡面進行消息處理

public void dispatchMessage(@NonNull Message msg) {
	// 檢查message是否設置了callback對象(runnable類型的)
	if (msg.callback != null) {
		// 執行其run方法
		handleCallback(msg);
	} else {
		// 檢查handler是否設置了callback對象
		if (mCallback != null) {
			if (mCallback.handleMessage(msg)) {
				// 如果返回true,後續就不執行了。
				return;
			}
		}
		// 自定義handler的時候,實現該方法處理消息
		handleMessage(msg);
	}
}

IdleHandler是什麼?乾什麼?繼續看MessageQueue類。

IdleHandler是MessageQueue的靜態內部介面。如下

public static interface IdleHandler {  
    /**  
     * Called when the message queue has run out of messages and will now     * wait for more.  Return true to keep your idle handler active, false     * to have it removed.  This may be called if there are still messages     * pending in the queue, but they are all scheduled to be dispatched     * after the current time.     */   
    
     boolean queueIdle();  
}

當前線程的消息對立內當前沒有消息要處理時,會取出idlehander執行任務,因為執行在主線程,禁止執行耗時操作。返回true表示執行完並不會移除該對象,false執行完一次就移除。
而且,執行時機是不確定的。
執行的地方在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();  
        }  
  
        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.  
                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.  
                    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();  
                    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;  
            }  
			// 執行到這裡,說明沒有找到message對象需要執行了,且線程沒有退出。
            // 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.            
            if (pendingIdleHandlerCount < 0  
                    && (mMessages == null || now < mMessages.when)) {  
                pendingIdleHandlerCount = mIdleHandlers.size();  
            }  
            if (pendingIdleHandlerCount <= 0) {  
                // No idle handlers to run.  Loop and wait some more.  
                mBlocked = true;  
                continue;  
            }  
			// 取出idlehandlers
            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 {  
	            // 執行idlehandler的方法
                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 = 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;  
    }  
}

補充,同步屏障消息的入隊與出隊

入隊方法由MessageQueue提供,並且標記不對用戶app開放訪問

@UnsupportedAppUsage  
@TestApi  
public int postSyncBarrier() {  
    return postSyncBarrier(SystemClock.uptimeMillis());  
}  
  
private int postSyncBarrier(long when) {  
    // Enqueue a new sync barrier token.  
    // We don't need to wake the queue because the purpose of a barrier is to stall it.    synchronized (this) {  
        final int token = mNextBarrierToken++;  
        final Message msg = Message.obtain();  
        msg.markInUse();  
        msg.when = when;  
        msg.arg1 = token; // token標記,當處理完非同步消息之後,要根據這個token移除屏障消息的 
  
        Message prev = null;  
        Message p = mMessages;  
        // 插入到消息對列中
        if (when != 0) {  
            while (p != null && p.when <= when) {  
                prev = p;  
                p = p.next;  
            }  
        }  
        if (prev != null) { // invariant: p == prev.next  
            msg.next = p;  
            prev.next = msg;  
        } else {  
            msg.next = p;  
            mMessages = msg;  
        }  
        return token;  
    }  
}

出隊

@UnsupportedAppUsage  
@TestApi  
public void removeSyncBarrier(int token) {  
    // Remove a sync barrier token from the queue.  
    // If the queue is no longer stalled by a barrier then wake it.    synchronized (this) {  
        Message prev = null;  
        Message p = mMessages;  
        while (p != null && (p.target != null || p.arg1 != token)) {  
            prev = p;  
            p = p.next;  
        }  
        if (p == null) {  
            throw new IllegalStateException("The specified message queue synchronization "  
                    + " barrier token has not been posted or has already been removed.");  
        }  
        final boolean needWake;  
        if (prev != null) {  
            prev.next = p.next;  
            needWake = false;  
        } else {  
            mMessages = p.next;  
            needWake = mMessages == null || mMessages.target != null;  
        }  
        p.recycleUnchecked();  
  
        // If the loop is quitting then it is already awake.  
        // We can assume mPtr != 0 when mQuitting is false.        if (needWake && !mQuitting) {  
            nativeWake(mPtr);  
        }  
    }  
}

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

-Advertisement-
Play Games
更多相關文章
  • DQL語句 1、格式 select 列名*N from 表名 where 查詢條件1 and/or 查詢條件2 group by 列 Having 分組條件 Order by 排序 2、規則 sql在書寫時除了查詢條件外,大小寫都可以 select * from user where uname=' ...
  • [MySQL事務一文搞懂] 1、什麼是事務? 事務(Transaction),顧名思義就是要做的或所做的事情,資料庫事務指的則是作為單個邏輯工作單元執行的一系列操作(SQL語句)。這些操作要麼全部執行,要麼全部不執行。 2、為什麼需要事務 把一系列sql放入一個事務中有兩個目的: 為資料庫操作提供了 ...
  • 摘要:5月16日,“數智深耕 讓美好發生 2023華為雲城市峰會廣州站”成功舉行。 5月16日,“數智深耕 讓美好發生 2023華為雲城市峰會廣州站”成功舉行。大會聚集了眾多城市管理者、產業領袖、企業家和媒體,共同探討工業數字化發展新趨勢,共謀工業數字化發展之路。華為公司副總裁、華為雲中國區總裁張修 ...
  • 前言 從今天開始本系列文章就帶各位小伙伴學習資料庫技術。資料庫技術是Java開發中必不可少的一部分知識內容。也是非常重要的技術。本系列教程由淺入深, 全面講解資料庫體系。 非常適合零基礎的小伙伴來學習。 全文大約 【1297】字,不說廢話,只講可以讓你學到技術、明白原理的純乾貨!本文帶有豐富案例及配 ...
  • 在企業級應用中,數據的安全性和隱私保護是極其重要的。Spark 作為數棧底層計算引擎之一,必須確保數據只能被授權的人員訪問,避免出現數據泄露和濫用的情況。為了實現Spark SQL 對數據的精細化管理及提高數據的安全性和可控性,數棧基於 Apache Ranger 實現了 Spark SQL 對數據 ...
  • 隨著 Redis 資料庫的流行和廣泛應用,Redis 的開發、管理需求日益增多,數據管理產品的好用與否將直接影響研發效能的高低。在 Redis 官網提供的 RedisInsight、Redis CLI 提供一定的可視化管理、命令執行及語法提示等能力,但缺乏人員操作許可權管控(6.0以前的低版本)、人員... ...
  • 問題描述 新建表或者修改表varchar欄位長度的時候,出現這個錯誤 Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. This includes stora ...
  • GreatSQL社區原創內容未經授權不得隨意使用,轉載請聯繫小編並註明來源。 GreatSQL是MySQL的國產分支版本,使用上與MySQL一致。 作者: 亮 文章來源:GreatSQL社區原創 概念介紹 首先需要知道MySQL中觸發器特點,以及表table相關觸發器載入方式 MySQL中單個tri ...
一周排行
    -Advertisement-
    Play Games
  • Dapr Outbox 是1.12中的功能。 本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文檔 。本文中appID=order-processor,topic=orders 本文前提知識:熟悉Dapr狀態管理、Dapr發佈訂閱和Outbox 模式。 Outbo ...
  • 引言 在前幾章我們深度講解了單元測試和集成測試的基礎知識,這一章我們來講解一下代碼覆蓋率,代碼覆蓋率是單元測試運行的度量值,覆蓋率通常以百分比表示,用於衡量代碼被測試覆蓋的程度,幫助開發人員評估測試用例的質量和代碼的健壯性。常見的覆蓋率包括語句覆蓋率(Line Coverage)、分支覆蓋率(Bra ...
  • 前言 本文介紹瞭如何使用S7.NET庫實現對西門子PLC DB塊數據的讀寫,記錄了使用電腦模擬,模擬PLC,自至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1.Windows環境下鏈路層網路訪問的行業標準工具(WinPcap_4_1_3.exe)下載鏈接:http ...
  • 從依賴倒置原則(Dependency Inversion Principle, DIP)到控制反轉(Inversion of Control, IoC)再到依賴註入(Dependency Injection, DI)的演進過程,我們可以理解為一種逐步抽象和解耦的設計思想。這種思想在C#等面向對象的編 ...
  • 關於Python中的私有屬性和私有方法 Python對於類的成員沒有嚴格的訪問控制限制,這與其他面相對對象語言有區別。關於私有屬性和私有方法,有如下要點: 1、通常我們約定,兩個下劃線開頭的屬性是私有的(private)。其他為公共的(public); 2、類內部可以訪問私有屬性(方法); 3、類外 ...
  • C++ 訪問說明符 訪問說明符是 C++ 中控制類成員(屬性和方法)可訪問性的關鍵字。它們用於封裝類數據並保護其免受意外修改或濫用。 三種訪問說明符: public:允許從類外部的任何地方訪問成員。 private:僅允許在類內部訪問成員。 protected:允許在類內部及其派生類中訪問成員。 示 ...
  • 寫這個隨筆說一下C++的static_cast和dynamic_cast用在子類與父類的指針轉換時的一些事宜。首先,【static_cast,dynamic_cast】【父類指針,子類指針】,兩兩一組,共有4種組合:用 static_cast 父類轉子類、用 static_cast 子類轉父類、使用 ...
  • /******************************************************************************************************** * * * 設計雙向鏈表的介面 * * * * Copyright (c) 2023-2 ...
  • 相信接觸過spring做開發的小伙伴們一定使用過@ComponentScan註解 @ComponentScan("com.wangm.lifecycle") public class AppConfig { } @ComponentScan指定basePackage,將包下的類按照一定規則註冊成Be ...
  • 操作系統 :CentOS 7.6_x64 opensips版本: 2.4.9 python版本:2.7.5 python作為腳本語言,使用起來很方便,查了下opensips的文檔,支持使用python腳本寫邏輯代碼。今天整理下CentOS7環境下opensips2.4.9的python模塊筆記及使用 ...