Handler從源碼角度理解

上一個文章講解了Handler的基本使用,同時也有一些問題沒有解決,本篇帶你從源碼的角度理解。
首先讓我們來看看Handler的構(gòu)造方法:

image.png

我靠這么多的構(gòu)造方法啊,我們上一篇只用了一個無參構(gòu)造,還有其他幾個我們都沒有用過啊,同志們不要慌,我們在進(jìn)到源碼仔細(xì)看


image.png

image.png

image.png

這三個構(gòu)造方法都用@hide修飾,這表明這三個都不是我們開發(fā)者可以調(diào)用的。

接下來我們來看其他幾個:

/**
 * Default constructor associates this handler with the {@link Looper} for the
 * current thread.
 *
 * If this thread does not have a looper, this handler won't be able to receive messages
 * so an exception is thrown.
 */
public Handler() {
    this(null, false);
}

/**
 * Constructor associates this handler with the {@link Looper} for the
 * current thread and takes a callback interface in which you can handle
 * messages.
 *
 * If this thread does not have a looper, this handler won't be able to receive messages
 * so an exception is thrown.
 *
 * @param callback The callback interface in which to handle messages, or null.
 */
public Handler(Callback callback) {
    this(callback, false);
}

/**
 * Use the provided {@link Looper} instead of the default one.
 *
 * @param looper The looper, must not be null.
 */
public Handler(Looper looper) {
    this(looper, null, false);
}

/**
 * Use the provided {@link Looper} instead of the default one and take a callback
 * interface in which to handle messages.
 *
 * @param looper The looper, must not be null.
 * @param callback The callback interface in which to handle messages, or null.
 */
public Handler(Looper looper, Callback callback) {
    this(looper, callback, false);
}

通過代碼我們發(fā)現(xiàn)所有的構(gòu)造方法最終都會調(diào)用到以下兩個:
1.當(dāng)構(gòu)造Handler時候沒有主動傳遞Looper

/**
 * Use the {@link Looper} for the current thread with the specified callback interface
 * and set whether the handler should be asynchronous.
 *
 * Handlers are synchronous by default unless this constructor is used to make
 * one that is strictly asynchronous.
 *
 * Asynchronous messages represent interrupts or events that do not require global ordering
 * with respect to synchronous messages.  Asynchronous messages are not subject to
 * the synchronization barriers introduced by {@link MessageQueue#enqueueSyncBarrier(long)}.
 *
 * @param callback The callback interface in which to handle messages, or null.
 * @param async If true, the handler calls {@link Message#setAsynchronous(boolean)} for
 * each {@link Message} that is sent to it or {@link Runnable} that is posted to it.
 *
 * @hide
 */
public Handler(Callback callback, boolean async) {
    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());
        }
    }

    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

2.當(dāng)構(gòu)造函數(shù)中傳遞了Looper

/**
 * Use the provided {@link Looper} instead of the default one and take a callback
 * interface in which to handle messages.  Also set whether the handler
 * should be asynchronous.
 *
 * Handlers are synchronous by default unless this constructor is used to make
 * one that is strictly asynchronous.
 *
 * Asynchronous messages represent interrupts or events that do not require global ordering
 * with respect to synchronous messages.  Asynchronous messages are not subject to
 * the synchronization barriers introduced by {@link MessageQueue#enqueueSyncBarrier(long)}.
 *
 * @param looper The looper, must not be null.
 * @param callback The callback interface in which to handle messages, or null.
 * @param async If true, the handler calls {@link Message#setAsynchronous(boolean)} for
 * each {@link Message} that is sent to it or {@link Runnable} that is posted to it.
 *
 * @hide
 */
public Handler(Looper looper, Callback callback, boolean async) {
    mLooper = looper;
    mQueue = looper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

從代碼的注釋中可以看出傳遞Looper與不傳遞Looper區(qū)別是如果沒有傳遞則使用當(dāng)前Handler在哪個線程中聲明就使用哪個線程的Looper。
接下來再看Hanlder中幾個重要屬性


image.png

這些屬性到底是干啥的呢?我們一個一個來看
1.mCallback這個屬性我們來看它的類型是啥

/**
 * Callback interface you can use when instantiating a Handler to avoid
 * having to implement your own subclass of Handler.
 */
public interface Callback {
    /**
     * @param msg A {@link android.os.Message Message} object
     * @return True if no further handling is desired
     */
    public boolean handleMessage(Message msg);
}

它是Handler一個內(nèi)部接口,它可以用來代替自己實現(xiàn)的Handler來處理消息,那它是怎么處理的呢 我們用代碼來看下
1.在主線程聲明Handler和Callback,并且Callback的handleMessage方法返回True


image.png

image.png

結(jié)果:


image.png

從運(yùn)行接口看出如果我們在構(gòu)建Handler時傳遞了Callback則發(fā)送消息后不會回調(diào)Handler的handleMessage方法。
如果我們的Callback的hanldeMessage方法返回的結(jié)果是false呢的結(jié)果是啥子呢? 不錯您答對了如果返回false會調(diào)用handler的handleMessage方法
image.png

那這個消息處理時的流程是怎么樣的呢?來,讓源碼為我們來解答。

在Handler中有一個方法叫dispatchMessage,源碼是:

/**
 * Handle system messages here.
 */
public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

該方法的參數(shù)是一個Message類型的至于什么是Message下面會講,我們來看出來流程,首先判斷message中的callback是否不為空(此處的callback其實是一個Runable)如果不為空則調(diào)入下面的方法


image.png

我們用代碼來證碼,代碼最會說實話:


image.png
image.png

如果是空則判斷handler的mCallback屬性是否為空,如果不為空則執(zhí)行mCallback的handleMessage方法且如果返回true則return否則調(diào)用Handler的hanleMesssage。

我們再來看另外兩個屬性,mLooper和mQueue,要想弄懂這兩個屬性我們先要弄明白Handler是如何發(fā)送和處理消息的。處理消息的流程在上面我們已經(jīng)講明白了,下面我們講發(fā)送消息的流程。
上一篇我們講過發(fā)送消息的兩種post和sendMessage。我們先來看post


image.png

從源碼中可以看出調(diào)用post實際上也會封裝成一個Message那在調(diào)用post時我們傳遞的Runable類型的參數(shù)跑哪兒去了呢,我們接著往下看


image.png

這下就很清楚了三,runable參數(shù)設(shè)置給了Message的callback所有,所有調(diào)用post方法不會調(diào)用Handler的handleMessage方法。
接下來我們再看sendMessage方法:
/**
 * Enqueue a message into the message queue after all pending messages
 * before the absolute time (in milliseconds) <var>uptimeMillis</var>.
 * <b>The time-base is {@link android.os.SystemClock#uptimeMillis}.</b>
 * Time spent in deep sleep will add an additional delay to execution.
 * You will receive it in {@link #handleMessage}, in the thread attached
 * to this handler.
 * 
 * @param uptimeMillis The absolute time at which the message should be
 *         delivered, using the
 *         {@link android.os.SystemClock#uptimeMillis} time-base.
 *         
 * @return Returns true if the message was successfully placed in to the 
 *         message queue.  Returns false on failure, usually because the
 *         looper processing the message queue is exiting.  Note that a
 *         result of true does not mean the message will be processed -- if
 *         the looper is quit before the delivery time of the message
 *         occurs then the message will be dropped.
 */
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);
}

我們來看這個方法中有一個uptimeMillis那這個時間時用來干嘛的呢,這個時間用來指定發(fā)送這個消息的時間,相當(dāng)于一種延時效果。
現(xiàn)在我們再來看將消息加入消息隊列的方法:

boolean enqueueMessage(Message msg, long when) {
    if (msg.target == null) {
        throw new IllegalArgumentException("Message must have a target.");
    }
    if (msg.isInUse()) {
        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();//設(shè)置當(dāng)前Message是正在使用標(biāo)志位
        msg.when = when;//設(shè)置message的發(fā)送時間
        Message p = mMessages;//獲取當(dāng)前的message
        boolean needWake;
        if (p == null || when == 0 || when < p.when) {//如果當(dāng)前沒有消息或者需要加入消息隊列的消息是立刻發(fā)送,或者需要加入隊列的消息的發(fā)送時間在當(dāng)前消息的發(fā)送時間之前則重新設(shè)置鏈表的頭
            // New head, wake up the event queue if blocked.
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {//否則的話就逐個對比消息隊列中消息的發(fā)送時間來進(jìn)行插入
            // 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;
}

1.如果當(dāng)前msg的target為Null就會拋出異常這是為什么呢?因為target就是處理這個消息的Handler如果都沒有人處理這個消息那這個消息就是不正確的消息。
2.如果當(dāng)前消息正在使用中代表正在隊列中或者馬上被處理,也會拋出異常
3.當(dāng)前handler已經(jīng)沒有與線程綁定也會拋出異常
4.根據(jù)發(fā)送時間來放置Message
5.調(diào)用native方法來喚醒Linux的epoll(消息循環(huán))

從源碼中我們可以看出MessageQuene中并沒有任何保存Message的集合或者數(shù)組,其實這個消息隊列的實現(xiàn)是通過一個鏈表來的每一個Message相當(dāng)于一個結(jié)點,它是一個單向鏈表根據(jù)發(fā)送時間來排序,所以在牽涉到Message的時候又牽涉到了數(shù)據(jù)結(jié)構(gòu)和算法,這個會在以后講。

現(xiàn)在消息已經(jīng)發(fā)送給并且保存到MessageQuene了但是又是如何將消息取出來的呢?我們接著往下面看MessageQuene有下面這個方法:
}

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;
            }

            // 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;
    }
}

首先我們來看第一個if判斷


image.png

判斷的是mPtr,那這個東東到底是個什么玩意兒呢?


image.png

它是MessageQuene中的一個屬性,是提供給native方法使用的
如果mPtr為0則返回null。那么mPtr是什么?值為0又意味著什么?在MessageQueue構(gòu)造方法中調(diào)用了native方法并返回了mPtrmPtr = nativeInit();;在dispose()方法中將其值置0mPtr = 0;并且調(diào)用了nativeDestroy()。而dispose()方法又在finalize()中被調(diào)用。另外每次mPtr的使用都調(diào)用了native的方法,其本身又是long類型,因此推斷它對應(yīng)的是C/C++的指針。因此可以確定,mPtr為一個內(nèi)存地址,當(dāng)其為0說明消息隊列被釋放了。
我們再看其中的另外一個if判斷


image.png

image.png

這里的意思也很明顯,當(dāng)這個消息隊列退出的時候,返回空。而且在返回前調(diào)用了dispose()方法,顯然這意味著該消息隊列將被釋放。
我們來看剩下的代碼,但是這段代碼太長,我們來進(jìn)行篩除

第一個要減的就是pendingIdleHandlerCount,這個局部變量初始為-1,后面被賦值mIdleHandlers.size();。這里的mIdleHandlers初始為new ArrayList(),在addIdleHander()方法中增加元素,在removeIdleHander()方法中移除元素。而我們所用的Handeler并未實現(xiàn)IdleHandler接口,因此在next()方法中pendingIdleHandlerCount的值要么為0,要么為-1,因此可以看出與該變量相關(guān)的部分代碼運(yùn)行情況是確定的,好的,把不影響循環(huán)控制的代碼減掉。

第二個要減的是Binder.flushPendingCommands()這個代碼看源碼說明:

Flush any Binder commands pending in the current thread to the kernel driver. This can be useful to call before performing an operation that may block for a long time, to ensure that any pending object references have been released in order to prevent the process from holding on to objects longer than it needs to.

這段話啥意不懂也沒關(guān)系,這里只需要知道:Binder.flushPendingCommands()方法被調(diào)用說明后面的代碼可能會引起線程阻塞。然后把這段減掉。

第三個要減的是一個log語句if (DEBUG) Log.v(TAG, "Returning message: " + msg);
篩減后的代碼

Message next() {
    int nextPollTimeoutMillis = 0;
    for (;;) {
        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;
                    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;
            }
            if (pendingIdleHandlerCount <= 0) {//上面分析過該變量要么為0要么為-1
            mBlocked = true;
            continue;
            }               
        }
        nextPollTimeoutMillis = 0;
    }
}

先獲取第一個同步的message。如果它的when不晚與當(dāng)前時間,就返回這個message;否則計算當(dāng)前時間到它的when還有多久并保存到nextPollTimeMills中,然后調(diào)用nativePollOnce()來延時喚醒(Linux的epoll,有興趣的自行百度),喚醒之后再照上面那樣取message,如此循環(huán)。代碼中對鏈表的指針操作占了一定篇幅,其他的邏輯很清楚。
那么問題又來了,是誰在調(diào)用這個方法呢,那當(dāng)然是我們還有一個沒有講的Looper洛,我們來看Looper的源碼,同樣我們只保留主流程的代碼:

/**
 * Run the message queue in this thread. Be sure to call
 * {@link #quit()} to end the 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.");
    }
    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);
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
        msg.recycleUnchecked();
    }
}

1.首先判斷Looper如果為Null則會拋出異常,為null的情況是在異步線程聲明Handler時沒有調(diào)用Looper.prepare(),當(dāng)然根本不會出現(xiàn)因為如果在異步線程中沒有調(diào)用該方法運(yùn)行時會直接報錯
2.一個死循環(huán)從消息隊列中循環(huán)消息,如果出現(xiàn)消息為null的情況說明已經(jīng)是被回收了的

  1. msg.target.dispatchMessage(msg),這部很關(guān)鍵我們知道m(xù)sg的targe是Handler所以這是在調(diào)用handler的dispatchMessage方法進(jìn)行處理,這也是為什么Handler在哪個線程聲明中就在哪個線程中處理消息和哪個handler發(fā)送消息哪個handler處理的由來,
    上面就是消息從發(fā)送到處理的流程,通過文字理解起來比較抽象,我們來畫個圖具體的描述下


    image.png

補(bǔ)充:在Handler中我們可以removeRunable或者M(jìn)essage,通過查看源碼我們發(fā)現(xiàn)最終都是調(diào)用的MessageQuene的removeMessages方法,它有兩種方法簽名,但實現(xiàn)邏輯基本相等,我們看其中一個

void removeMessages(Handler h, int what, Object object) {
    if (h == null) {
        return;
    }

    synchronized (this) {
        Message p = mMessages;

        // Remove all messages at front.
        while (p != null && p.target == h && p.what == what
               && (object == null || p.obj == object)) {
            Message n = p.next;
            mMessages = n;
            p.recycleUnchecked();
            p = n;
        }

        // Remove all messages after front.
        while (p != null) {
            Message n = p.next;
            if (n != null) {
                if (n.target == h && n.what == what
                    && (object == null || n.obj == object)) {
                    Message nn = n.next;
                    n.recycleUnchecked();
                    p.next = nn;
                    continue;
                }
            }
            p = n;
        }
    }
}

代碼邏輯很清楚
1.如果該Message沒有handler即target直接返回
2.改變Message鏈表指向,調(diào)用Message的recycleUnchecked()方法

從上面所有我們可以看出Message特別重要我們再來看Message的源碼


image.png

實現(xiàn)了Parcelable接口 這是android種用來進(jìn)行數(shù)據(jù)傳輸?shù)模扔涀『竺娴恼鹿?jié)會具體的講
我們平時聲明Message對象有兩種方法直接new或者M(jìn)essage.obtain()這個方法有以下的幾種重寫


image.png

帶有參數(shù)的都會調(diào)用到無參構(gòu)造,參數(shù)是設(shè)置message的一些屬性值,其中你構(gòu)造方法穿進(jìn)去的hanlder可能不最終處理的hanlder,因為在handler發(fā)送消息加入隊列是會這是message的target為發(fā)送消息的handler我們用demo證明:
image.png
image.png

image.png

我們接著看無參構(gòu)造方法:

/**
 * Return a new Message instance from the global pool. Allows us to
 * avoid allocating new objects in many cases.
 */
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();
}

看到里面有一把鎖,這個是線程同步,如果當(dāng)前message不null則把當(dāng)前的賦值給一個新的引用,sPoolSize是指message池的大小最大為50


image.png

這樣做可以在一定程度上減少內(nèi)存開銷,因為沒有分配對象。應(yīng)用的是享元模式
我們再來看recycleUnchecked方法

/**
 * Recycles a Message that may be in-use.
 * Used internally by the MessageQueue and Looper when disposing of queued Messages.
 */
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++;
        }
    }
}

邏輯也非常清楚,就是重置Message的屬性,并且如果當(dāng)前鏈表的長度小于最大的則進(jìn)行鏈表指向。

從這些源碼種可以看出有些數(shù)據(jù)結(jié)構(gòu)和算法得知識比如:
鏈表,鏈表得排序,多線程知識在以后得章節(jié)種為大家講解。
以上便是Handler以及相關(guān)類的源碼分析,因為是第一次寫,可能有很多地方?jīng)]有寫清楚,也有可能有些地方理解得不對,請指出,謝謝支持。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容