溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

怎么掌握Handler消息機制

發(fā)布時間:2021-10-21 16:55:20 來源:億速云 閱讀:158 作者:iii 欄目:編程語言

本篇內(nèi)容主要講解“怎么掌握Handler消息機制”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學(xué)習(xí)“怎么掌握Handler消息機制”吧!

一、題目層次

  1. Handler 的基本原理

  2. 子線程中怎么使用 Handler

  3. MessageQueue 獲取消息是怎么等待

  4. 為什么不用 wait 而用 epoll 呢?

  5. 線程和 Handler Looper MessageQueue 的關(guān)系

  6. 多個線程給 MessageQueue 發(fā)消息,如何保證線程安全

  7. Handler 消息延遲是怎么處理的

  8. View.post 和 Handler.post 的區(qū)別

  9. Handler 導(dǎo)致的內(nèi)存泄漏

  10. 非 UI 線程真的不能操作 View 嗎

二、題目詳解

代碼分析基于 Android SDK 28

大家可以先看上面的問題思考一下,如果都清楚的話,下面的文章也沒必要看了~

1. Handler 的基本原理

關(guān)于 Handler 的原理,相比不用多說了,大家都應(yīng)該知道,一張圖就可以說明(圖片來自網(wǎng)絡(luò))。

怎么掌握Handler消息機制

2. 子線程中怎么使用 Handler

除了上面 Handler 的基本原理,子線程中如何使用 Handler 也是一個常見的問題。 子線程中使用 Handler 需要先執(zhí)行兩個操作:Looper.prepare 和 Looper.loop。 為什么需要這樣做呢?Looper.prepare 和 Looper.loop 都做了什么事情呢? 我們知道如果在子線程中直接創(chuàng)建一個 Handler 的話,會報如下的錯誤:

"Can't create handler inside thread xxx that has not called Looper.prepare()

我們可以看一下 Handler 的構(gòu)造函數(shù),里面會對 Looper 進行判斷,如果通過 ThreadLocal 獲取的 Looper 為空,則報上面的錯誤。

    public Handler(Callback callback, boolean async) {
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
    }

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

那么 Looper.prepare 里做了什么事情呢?

    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

可以看到,Looper.prepare 就是創(chuàng)建了 Looper 并設(shè)置給 ThreadLocal,這里的一個細節(jié)是每個 Thread 只能有一個 Looper,否則也會拋出異常。 而 Looper.loop 就是開始讀取 MessageQueue 中的消息,進行執(zhí)行了。

這里一般會引申一個問題,就是主線程中為什么不用手動調(diào)用這兩個方法呢?相信大家也都明白,就是 ActivityThread.main 中已經(jīng)進行了調(diào)用。 通過這個問題,又可以引申到 ActivityThread 相關(guān)的知識,這里就不細說了。

3. MessageQueue 如何等待消息

上面說到 Looper.loop 其實就是開始讀取 MessageQueue 中的消息了,那 MessageQueue 中沒有消息的時候,Looper 在做什么呢?我們知道是在等待消息,那是怎么等待的呢?

通過 Looper.loop 方法,我們知道是 MessageQueue.next() 來獲取消息的,如果沒有消息,那就會阻塞在這里,MessageQueue.next 是怎么等待的呢?

    public static void loop() {
        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;
            }
        }
    }
    Message next() {
        for (;;) {
            nativePollOnce(ptr, nextPollTimeoutMillis);
            // ...
        }
    }

在 MessageQueue.next 里調(diào)用了 native 方法 nativePollOnce。

// android_os_MessageQueue.cpp
static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
        jlong ptr, jint timeoutMillis) {
    NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
    nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}

void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
    // ...
    mLooper->pollOnce(timeoutMillis);
    // ...
}

// Looper.cpp
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
    // ...
    result = pollInner(timeoutMillis);
    // ...
}

int Looper::pollInner(int timeoutMillis) {
    // ...
    int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
}

從上面代碼中我們可以看到,在 native 側(cè),最終是使用了 epoll_wait 來進行等待的。 這里的 epoll_wait 是 Linux 中 epoll 機制中的一環(huán),關(guān)于 epoll 機制這里就不進行過多介紹了,大家有興趣可以參考 https://segmentfault.com/a/1190000003063859

那其實說到這里,又有一個問題,為什么不用 java 中的 wait / notify 而是要用 native 的 epoll 機制呢?

4. 為什么不用 wait 而用 epoll 呢?

說起來 java 中的 wait / notify 也能實現(xiàn)阻塞等待消息的功能,在 Android 2.2 及以前,也確實是這樣做的。 可以參考這個 commit https://www.androidos.net.cn/android/2.1_r2.1p2/xref/frameworks/base/core/java/android/os/MessageQueue.java 那為什么后面要改成使用 epoll 呢?通過看 commit 記錄,是需要處理 native 側(cè)的事件,所以只使用 java 的 wait / notify 就不夠用了。 具體的改動就是這個 commit https://android.googlesource.com/platform/frameworks/base/+/fa9e7c05c7be6891a6cf85a11dc635a6e6853078%5E%21/#F0

Sketch of Native input for MessageQueue / Looper / ViewRoot

MessageQueue now uses a socket for internal signalling, and is prepared
to also handle any number of event input pipes, once the plumbing is
set up with ViewRoot / Looper to tell it about them as appropriate.

Change-Id: If9eda174a6c26887dc51b12b14b390e724e73ab3

不過這里最開始使用的還是 select,后面才改成 epoll。 具體可見這個 commit https://android.googlesource.com/platform/frameworks/base/+/46b9ac0ae2162309774a7478cd9d4e578747bfc2%5E%21/#F16

至于 select 和 epoll 的區(qū)別,這里也不細說了,大家可以在上面的參考文章中一起看看。

5. 線程和 Handler Looper MessageQueue 的關(guān)系

這里的關(guān)系是一個線程對應(yīng)一個 Looper 對應(yīng)一個 MessageQueue 對應(yīng)多個 Handler。

6. 多個線程給 MessageQueue 發(fā)消息,如何保證線程安全

既然一個線程對應(yīng)一個 MessageQueue,那多個線程給 MessageQueue 發(fā)消息時是如何保證線程安全的呢? 說來簡單,就是加了個鎖而已。

// MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
    synchronized (this) {
        // ...
    }
}
7. Handler 消息延遲是怎么處理的

Handler 引申的另一個問題就是延遲消息在 Handler 中是怎么處理的?定時器還是其他方法? 這里我們先從事件發(fā)起開始看起:

// Handler.java
public final boolean postDelayed(Runnable r, long delayMillis)
{
    return sendMessageDelayed(getPostMessage(r), delayMillis);
}

public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
    // 傳入的 time 是 uptimeMillis + delayMillis
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    // ...
    return enqueueMessage(queue, msg, uptimeMillis);
}

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    // 調(diào)用 MessageQueue.enqueueMessage
    return queue.enqueueMessage(msg, uptimeMillis);
}

從上面的代碼邏輯來看,Handler post 消息以后,一直調(diào)用到 MessageQueue.enqueueMessage 里,其中最重要的一步操作就是傳入的時間是 uptimeMillis + delayMillis。

boolean enqueueMessage(Message msg, long when) {
    synchronized (this) {
        // ...
        msg.when = when;
        Message p = mMessages; // 下一條消息
        // 根據(jù) when 進行順序排序,將消息插入到其中
        if (p == null || when == 0 || when < p.when) {
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            // 找到 合適的節(jié)點
            Message prev;
            for (;;) {
                prev = p;
                p = p.next;
                if (p == null || when < p.when) {
                    break;
                }
            }
            // 插入操作
            msg.next = p; // invariant: p == prev.next
            prev.next = msg;
        }

        // 喚醒隊列進行取消息
        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

通過上面代碼我們看到,post 一個延遲消息時,在 MessageQueue 中會根據(jù) when 的時長進行一個順序排序。 接著我們再看看怎么使用 when 的。

Message next() {
    // ...
    for (;;) {
        // 通過 epoll_wait 等待消息,等待 nextPollTimeoutMillis 時長
        nativePollOnce(ptr, nextPollTimeoutMillis);

        synchronized (this) {
            // 當(dāng)前時間
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            if (msg != null && msg.target == null) {
                // 獲得一個有效的消息
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
                if (now < msg.when) { // 說明需要延遲執(zhí)行,通過; nativePollOnce 的 timeout 來進行延遲
                    // 獲取需要等待執(zhí)行的時間
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else { // 立即執(zhí)行的消息,直接返回
                    // 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;
            }

            if (pendingIdleHandlerCount < 0
                    && (mMessages == null || now < mMessages.when)) {
                        // 當(dāng)前沒有消息要執(zhí)行,則執(zhí)行 IdleHandler 中的內(nèi)容
                pendingIdleHandlerCount = mIdleHandlers.size();
            }
            if (pendingIdleHandlerCount <= 0) {
                // 如果沒有 IdleHandler 需要執(zhí)行,則去等待 消息的執(zhí)行
                mBlocked = true;
                continue;
            }

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

        // 執(zhí)行 idle handlers 內(nèi)容
        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;

        // 如果執(zhí)行了 idle handlers 的內(nèi)容,現(xiàn)在消息可能已經(jīng)到了執(zhí)行時間,所以這個時候就不等待了,再去檢查一下消息是否可以執(zhí)行, nextPollTimeoutMillis 需要置為 0
        nextPollTimeoutMillis = 0;
    }
}

通過上面的代碼分析,我們知道了執(zhí)行 Handler.postDelayd 時候,會執(zhí)行下面幾個步驟:

  1. 將我們傳入的延遲時間轉(zhuǎn)化成距離開機時間的毫秒數(shù)

  2. MessageQueue 中根據(jù)上一步轉(zhuǎn)化的時間進行順序排序

  3. 在 MessageQueue.next 獲取消息時,對比當(dāng)前時間(now)和第一步轉(zhuǎn)化的時間(when),如果 now < when,則通過 epoll_wait 的 timeout 進行等待

  4. 如果該消息需要等待,會進行 idel handlers 的執(zhí)行,執(zhí)行完以后會再去檢查此消息是否可以執(zhí)行

8. View.post 和 Handler.post 的區(qū)別

我們最常用的 Handler 功能就是 Handler.post,除此之外,還有 View.post 也經(jīng)常會用到,那么這兩個有什么區(qū)別呢? 我們先看下 View.post 的代碼。

// View.java
public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }

    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().post(action);
    return true;
}

通過代碼來看,如果 AttachInfo 不為空,則通過 handler 去執(zhí)行,如果 handler 為空,則通過 RunQueue 去執(zhí)行。 那我們先看看這里的 AttachInfo 是什么。 這個就需要追溯到 ViewRootImpl 的流程里了,我們先看下面這段代碼。

// ViewRootImpl.java
final ViewRootHandler mHandler = new ViewRootHandler();

public ViewRootImpl(Context context, Display display) {
    // ...
    mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
            context);
}

private void performTraversals() {
    final View host = mView;
    // ...
    if (mFirst) {
        host.dispatchAttachedToWindow(mAttachInfo, 0);
        mFirst = false;
    }
    // ...
}

代碼寫了一些關(guān)鍵部分,在 ViewRootImpl 構(gòu)造函數(shù)里,創(chuàng)建了 mAttachInfo,然后在 performTraversals 里,如果 mFirst 為 true,則調(diào)用 host.dispatchAttachedToWindow,這里的 host 就是 DecorView,如果有讀者朋友對這里不太清楚,可以看看前面【面試官帶你學(xué)安卓-從View的繪制流程】說起這篇文章復(fù)習(xí)一下。

這里還有一個知識點就是 mAttachInfo 中的 mHandler 其實是 ViewRootImpl 內(nèi)部的 ViewRootHandler。

然后就調(diào)用到了 DecorView.dispatchAttachedToWindow,其實就是 ViewGroup 的 dispatchAttachedToWindow,一般 ViewGroup 中相關(guān)的方法,都是去依次調(diào)用 child 的對應(yīng)方法,這個也不例外,依次調(diào)用子 View 的 dispatchAttachedToWindow,把 AttachInfo 傳進去,在 子 View 中給 mAttachInfo 賦值。

// ViewGroup
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
    super.dispatchAttachedToWindow(info, visibility);
    mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;

    final int count = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < count; i++) {
        final View child = children[i];
        child.dispatchAttachedToWindow(info,
                combineVisibility(visibility, child.getVisibility()));
    }
    final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
    for (int i = 0; i < transientCount; ++i) {
        View view = mTransientViews.get(i);
        view.dispatchAttachedToWindow(info,
                combineVisibility(visibility, view.getVisibility()));
    }
}

// View
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mAttachInfo = info;
    // ...
}

看到這里,大家可能忘記我們開始剛剛要做什么了。

我們是在看 View.post 的流程,再回顧一下 View.post 的代碼:

// View.java
public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }

    getRunQueue().post(action);
    return true;
}

現(xiàn)在我們知道 attachInfo 是什么了,是 ViewRootImpl 首次觸發(fā) performTraversals 傳進來的,也就是觸發(fā) performTraversals 之后,View.post 都是通過 ViewRootImpl 內(nèi)部的 Handler 進行處理的。

如果在 performTraversals 之前或者 mAttachInfo 置為空以后進行執(zhí)行,則通過 RunQueue 進行處理。

那我們再看看 getRunQueue().post(action); 做了些什么事情。

這里的 RunQueue 其實是 HandlerActionQueue。

HandlerActionQueue 的代碼看一下。

public class HandlerActionQueue {
    public void post(Runnable action) {
        postDelayed(action, 0);
    }

    public void postDelayed(Runnable action, long delayMillis) {
        final HandlerAction handlerAction = new HandlerAction(action, delayMillis);

        synchronized (this) {
            if (mActions == null) {
                mActions = new HandlerAction[4];
            }
            mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
            mCount++;
        }
    }

    public void executeActions(Handler handler) {
        synchronized (this) {
            final HandlerAction[] actions = mActions;
            for (int i = 0, count = mCount; i < count; i++) {
                final HandlerAction handlerAction = actions[i];
                handler.postDelayed(handlerAction.action, handlerAction.delay);
            }

            mActions = null;
            mCount = 0;
        }
    }
}

通過上面的代碼我們可以看到,執(zhí)行 getRunQueue().post(action); 其實是將代碼添加到 mActions 進行保存,然后在 executeActions 的時候進行執(zhí)行。

executeActions 執(zhí)行的時機只有一個,就是在 dispatchAttachedToWindow(AttachInfo info, int visibility) 里面調(diào)用的。

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mAttachInfo = info;
    if (mRunQueue != null) {
        mRunQueue.executeActions(info.mHandler);
        mRunQueue = null;
    }
}

看到這里我們就知道了,View.post 和 Handler.post 的區(qū)別就是:

  1. 如果在 performTraversals 前調(diào)用 View.post,則會將消息進行保存,之后在 dispatchAttachedToWindow 的時候通過 ViewRootImpl 中的 Handler 進行調(diào)用。

  2. 如果在 performTraversals 以后調(diào)用 View.post,則直接通過 ViewRootImpl 中的 Handler 進行調(diào)用。

這里我們又可以回答一個問題了,就是為什么 View.post 里可以拿到 View 的寬高信息呢? 因為 View.post 的 Runnable 執(zhí)行的時候,已經(jīng)執(zhí)行過 performTraversals 了,也就是 View 的 measure layout draw 方法都執(zhí)行過了,自然可以獲取到 View 的寬高信息了。

9. Handler 導(dǎo)致的內(nèi)存泄漏

這個問題就是老生常談了,可以由此再引申出內(nèi)存泄漏的知識點,比如:如何排查內(nèi)存泄漏,如何避免內(nèi)存泄漏等等。

10. 非 UI 線程真的不能操作 View 嗎

我們使用 Handler 最多的一個場景就是在非主線程通過 Handler 去操作 主線程的 View。 那么非 UI 線程真的不能操作 View 嗎? 我們在執(zhí)行 UI 操作的時候,都會調(diào)用到 ViewRootImpl 里,以 requestLayout 為例,在 requestLayout 里會通過 checkThread 進行線程的檢查。

// ViewRootImpl.java
public ViewRootImpl(Context context, Display display) {
    mThread = Thread.currentThread();
}

public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

我們看這里的檢查,其實并不是檢查主線程,是檢查 mThread != Thread.currentThread,而 mThread 指的是 ViewRootImpl 創(chuàng)建的線程。 所以非 UI 線程確實不能操作 View,但是檢查的是創(chuàng)建的線程是否是當(dāng)前線程,因為 ViewRootImpl 創(chuàng)建是在主線程創(chuàng)建的,所以在非主線程操作 UI 過不了這里的檢查。

三、總結(jié)

一個小小的 Handler,其實可以引申出很多問題,這里這是列舉了一些大家可能忽略的問題,更多的問題就等待大家去探索了~ 這里來總結(jié)一下:

1. Handler 的基本原理

一張圖解釋(圖片來自網(wǎng)絡(luò)) 怎么掌握Handler消息機制

2. 子線程中怎么使用 Handler
  1. Looper.prepare 創(chuàng)建 Looper 并添加到 ThreadLocal 中

  2. Looper.loop 啟動 Looper 的循環(huán)

3. MessageQueue 獲取消息是怎么等待

通過 epoll 機制進行等待和喚醒。

4. 為什么不用 wait 而用 epoll 呢?

在 Android 2.2 及之前,使用 Java wait / notify 進行等待,在 2.3 以后,使用 epoll 機制,為了可以同時處理 native 側(cè)的消息。

5. 線程和 Handler Looper MessageQueue 的關(guān)系

一個線程對應(yīng)一個 Looper 對應(yīng)一個 MessageQueue 對應(yīng)多個 Handler。

6. 多個線程給 MessageQueue 發(fā)消息,如何保證線程安全

通過對 MessageQueue 加鎖來保證線程安全。

7. Handler 消息延遲是怎么處理的
  1. 將傳入的延遲時間轉(zhuǎn)化成距離開機時間的毫秒數(shù)

  2. MessageQueue 中根據(jù)上一步轉(zhuǎn)化的時間進行順序排序

  3. 在 MessageQueue.next 獲取消息時,對比當(dāng)前時間(now)和第一步轉(zhuǎn)化的時間(when),如果 now < when,則通過 epoll_wait 的 timeout 進行等待

  4. 如果該消息需要等待,會進行 idel handlers 的執(zhí)行,執(zhí)行完以后會再去檢查此消息是否可以執(zhí)行

8. View.post 和 Handler.post 的區(qū)別

View.post 最終也是通過 Handler.post 來執(zhí)行消息的,執(zhí)行過程如下:

  1. 如果在 performTraversals 前調(diào)用 View.post,則會將消息進行保存,之后在 dispatchAttachedToWindow 的時候通過 ViewRootImpl 中的 Handler 進行調(diào)用。

  2. 如果在 performTraversals 以后調(diào)用 View.post,則直接通過 ViewRootImpl 中的 Handler 進行調(diào)用。

9. Handler 導(dǎo)致的內(nèi)存泄漏

略過不講~

10. 非 UI 線程真的不能操作 View 嗎

不能操作,原因是 ViewRootImpl 會檢查創(chuàng)建 ViewRootImpl 的線程和當(dāng)前操作的線程是否一致。而 ViewRootImpl 是在主線程創(chuàng)建的,所以非主線程不能操作 View。

到此,相信大家對“怎么掌握Handler消息機制”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進入相關(guān)頻道進行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI