Android 事件分發介紹

来源:https://www.cnblogs.com/zhiqinlin/Undeclared/17957738
-Advertisement-
Play Games

Android 中 View 的佈局是一個樹形結構,各個 ViewGroup 和 View 是按樹形結構嵌套佈局的,從而會出現用戶觸摸的位置坐標可能會落在多個 View 的範圍內,這樣就不知道哪個 View 來響應這個事件,為瞭解決這一問題,就出現了事件分發機制。 ...


目錄

一、目的

        最開始接觸Android時,僅僅是知道Android系統存在的點擊事件、觸摸事件,但是並不清楚這些事件的由來。
        之後,在面試Oppo和美圖時,皆有問到Android的事件分發機制,但是都被問得很懵逼,歸根到底都是對於其實現邏輯的不理解。
        隨後,想去彌補該模塊的不足,瀏覽很多關於Android事件分發的博文,但仍存在一些疑惑,就想著去閱讀下源碼,整理下筆記,希望對同學們有幫助。

二、環境

  1. 版本:Android 11
  2. 平臺:展銳 SPRD8541E

三、相關概念

3.1 事件分發

        Android 中 View 的佈局是一個樹形結構,各個 ViewGroup 和 View 是按樹形結構嵌套佈局的,從而會出現用戶觸摸的位置坐標可能會落在多個 View 的範圍內,這樣就不知道哪個 View 來響應這個事件,為瞭解決這一問題,就出現了事件分發機制。

四、詳細設計

4.1應用佈局

4.1.1 應用佈局結構

        如下為一個Activity打開後,其對應視圖的層級結構。

4.1.2 LayoutInspector

        Layout Inspector是google提供給我們進行佈局分析的一個工具,也是目前google在棄用Hierarchy View後推薦使用的一款佈局分析工具。

4.2 關鍵View&方法

4.2.1 相關View

組件 描述
Activity Android事件分發的起始端,其為一個window視窗,內部持有Decorder視圖,該視圖為當前窗體的根節點,同時,它也是一個ViewGroup容器。
ViewGroup Android中ViewGroup是一個佈局容器,可以嵌套多個 ViewGroup 和 View,事件傳遞和攔截都由 ViewGroup 完成。
View 事件傳遞的最末端,要麼消費事件,要麼不消費把事件傳遞給父容器

4.2.2 相關方法

方法 描述
dispatchTouchEvent 分發事件
onInterceptTouchEvent 攔截事件
onTouchEvent 觸摸事件

4.2.3 View與方法關係

組件 dispatchTouchEvent onInterceptTouchEvent onTouchEvent
Activity
ViewGroup
View

4.3 事件分發概念圖

4.3.1 事件分發類圖

4.3.2 事件分發模型圖

        Android的ACTION_DOWN事件分發如圖,從1-9步驟,描述一個down事件的分發過程,如果大家能懂,就不用看下麵文字描述了(寫完這個篇幅,感覺文字好多,不好理解!)

  1. ACTION_DOWN事件觸發。 當我們手指觸摸屏幕,tp驅動會響應中斷,通過ims輸入系統,將down事件的相關信息發送到當前的視窗,即當前的Activity。
  2. Activity事件分發。 會引用dispatchTouchEvent()方法,對down事件分發。Activity本身會持有一個window對象,window對象的實現類PhoneWindow會持有一個DecorView對象,DecorView是一個ViewGroup對象,即我們可以理解為,Activity最終會將事件分發給下一個節點——ViewGroup。
  3. ViewGroup事件攔截。 ViewGroup接收到事件後,會先引用onInterceptTouchEvent(),查看當前的視圖容器是否做事件攔截。
  4. ViewGroup消費事件。 如當前的ViewGroup對事件進行攔截,即會調用onTouchEvent(),對事件消費。
  5. ViewGroup事件不攔截。 則ViewGroup會繼續遍歷自身的子節點,並且當事件的坐標位於子節點上,則繼續下發到下一個節點。ViewGroup的子節點有可能是View,也可能是ViewGroup(當然,ViewGroup最後也是繼承於View的,突然感覺有點廢話)。
  6. ViewGroup事件分發。 目標視圖如果是ViewGroup,會引用其super類的dispatchTouchEvent()方法,即事件下發,不管目標視圖是View或者ViewGroup最終引用的是View類的分發方法。
  7. View事件消費。 在View的dispatchTouchEvent()方法中會根據當前View是否可以點擊、onTouch()是否消費、onTouchEvent()是否消費等條件,來判斷當前是否為目標View。
  8. View事件未消費。 View事件未消費,則其父節點,即ViewGroup會調用onTouchEvent()方法,並根據返回值來決定是否消費事件。
  9. ViewGroup事件未消費。 ViewGroup事件未消費,擇其父節點,即Actviity會調用onTouchEvent()方法

PS:
(1) ACTION_MOVEACTION_UP事件,流程與ACTION_DOWN的分發過程基本一致,MOVE和UP事件也是通過Activity開始,藉助DOWN事件產生的目標View,逐級分發。
(2) ACTION_CANCEL事件,是在down與up、move事件切換過程中,事件被攔截,兩次的touchTarget目標view不一致,而產生的事件。用於對之前的目標View做恢復處理,避免down與up/move事件不對稱。

4.4 Activity組件

4.4.1 Activity->dispatchTouchEvent()

        底層上報的事件信息,最終會引用到該方法。Activity會持有一個根視圖DecordView,事件最終會往該ViewGroup分發,如所有的View都未消費該事件,則最終由Activity的onTouchEvent()
來兜底處理。

@frameworks\base\core\java\android\app\Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    if (getWindow().superDispatchTouchEvent(ev)) {//Step 1. 查看Window對應的View是否分發該事件
        return true;
    }
    return onTouchEvent(ev);//Step 2. 如果沒有組件消費事件,則由Activity兜底處理
}

4.4.2 Activity->getWindow()

        我們每次啟動一個Activity的組件,會先打開一個window視窗,而PhoneWindow是Window唯一的實現類。

@frameworks\base\core\java\android\app\Activity.java
public Window getWindow() {
    return mWindow;
}

final void attach(Context context, ActivityThread aThread...) {
    ...
    mWindow = new PhoneWindow(this, window, activityConfigCallback);//PhoneWindow是Window視窗唯一的實現類
    ...
}

        PhoneWindow對象內部持有DecorView對象,而該View正是該視窗對應的視圖容器,也是根節點。(此部分不具體分析)

@frameworks\base\core\java\com\android\internal\policy\PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.     Callback {
    ...
    private DecorView mDecor;//
    ...
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);//往View的根節點分發事件
    }
}

4.4.3 Activity->onTouchEvent()

        Activity的onTouchEvent方法,是在沒有任何組件消費事件的情況下,觸發的方法。

@frameworks\base\core\java\android\app\Activity.java
public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }
    return false;
}

4.5 ViewGroup組件

        ViewGroup組件在整個事件分發的模型中,既有分發事件的責任,又要具備處理事件的能力,真的典型的當爹又當媽。
        當Activity調用superDispatchTouchEvent,即最終會使用到DecorView的superDispatchTouchEvent方法,而DecorView是繼承於ViewGroup,即最終會引用ViewGroup的dispatchTouchEvent方法。

4.5.1 ViewGroup->dispatchTouchEvent()

此方法為事件分發最核心的代碼。其主要處理如下四件事情:
Setp 1. 重置事件。 一次完整觸摸的事件:DOWN -> MOVE -> UP,即我們可以理解為DOWN是所有觸摸事件的起始事件。當輸入事件是ACTION_DOWN時,重置觸摸事件狀態信息,避免產生干擾。
Step 2. 攔截事件。 攔截事件是ViewGroup特有的方法,用於攔截事件,並將該事件分發給自己消費,防止事件繼續下發。
Step 3.查找目標View。 查找目標View主要針對於Down事件。當ViewGroup未攔截事件,且輸入事件是ACTION_DOWN時,會遍歷該ViewGroup的所有子節點,並根據觸摸位置的坐標,來決定當前子節點是否是下一級目標View。當找到目標View節點後,會分發Down事件,並記錄該節點信息。
Step 4.下發事件。 如果目標View未找到的話,則會將事件交由自己的onTouchEvent()處理;如果目標View已經找到,則Down事件就此結束(此處暫不考慮多指場景);Move和Up事件將繼續下發(預設情況下Move、Up和Down事件是成對出現的,如果目標View已經存在,則Down事件已經下發,即意味著Move和Up事件也需要下發給對應的目標View)。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    if (actionMasked == MotionEvent.ACTION_DOWN) {//Step 1.重置事件信息,避免影響下一次事件
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }

    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);//Step 2.攔截事件
            ev.setAction(action); // restore action in case it was changed
        }
    } 
    ...
    if (!canceled && !intercepted) {//Step 3.查找目標View
        if (actionMasked == MotionEvent.ACTION_DOWN
                || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            ...
            if (newTouchTarget == null && childrenCount != 0) {
                ...
                for (int i = childrenCount - 1; i >= 0; i--) {//遍歷所有的子節點
                    ...
                    if (!child.canReceivePointerEvents()
                            || !isTransformedTouchPointInView(x, y, child, null)) {// 子節點不可以接收事件,或者觸摸位置不在子節點的範圍上
                        continue;
                    }
                    ...
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {//找到目標View
                        ...
                        break;
                    }
                }
                ...
            }
            ...
        }
    }
    //Step 4.根據找到的目標View情況,繼續下發事件
    if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);//沒有找到目標View或者事件被攔截,事件下發給自己
    } else {
        ...
        while (target != null) {//多組數據,一般是指多指場景
            final TouchTarget next = target.next;
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {//此場景一般是down事件
                handled = true
            } else {
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {//此場景一般是move、up事件
                    handled = true;
                }
                ...
            }
            predecessor = target;
            target = next;
        }
        ...
    }
    ...
    return handled;
}

4.5.2 ViewGroup->dispatchTransformedTouchEvent()

事件分發關鍵方法,主要用於向目標View分發事件,具體邏輯如下:
Step 1.Cancel事件分發。 之前我們提過Down和Up事件是成對存在的,如果Down事件已經下發的情況下,Up事件卻因為事件攔截等原因,未能下發給目標View,目標View未收到Up事件,此時就可能產生一些按壓狀態的異常問題,故,在當前場景下,將會分發一個ACTION_CANCEL事件給目標View。
Step 2.事件處理。 如果事件未找到目標View,則child會為null,此時的事件將由自身處理。
Step 3.事件分發。 如果事件還存在目標View,則此時的事件會再分發。

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        ...
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {//Step 1.下發取消事件
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
        ...
        if (child == null) {//Step 2.如果事件未找到目標View,則觸摸事件會發給自己
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }
            handled = child.dispatchTouchEvent(transformedEvent);//Step 3.找到目標View,事件下發給子節點
        }
        ...
        return handled;
    }

4.6 View組件

        View組件在事件處理模型中,主要是處理事件。我們知道ViewGroup,也是繼承於View,所以ViewGroup也是同樣具備View的處理事件能力。

4.6.1 View->dispatchTouchEvent()

Step 1.觸發onTouch()方法。 如果當前的View是可點擊的,且配置了onTouch事件監聽,則觸發該View的onTouch()方法。
Step 2.觸發onTouchEvent()方法。 如果該事件在上一步的onTouch()函數中未被消費,則觸發onTouchEvent()方法。

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    ...
    if (onFilterTouchEventForSecurity(event)) {
        ...
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {//Step 1.觸發onTouch事件
            result = true;
        }

        if (!result && onTouchEvent(event)) {//Step 2.如onTouch未消費,觸發onTouchEvent事件
            result = true;
        }
    }
    ...
    return result;
}

4.6.2 OnTouchListener->onTouch()

        View可以設置事件監聽,用於監聽onTouch事件的回調,當然,像我們常見的onClick()、onLongClick()等事件也可監聽,其相關源碼如下:

@frameworks\base\core\java\android\view\View.java
public void setOnTouchListener(OnTouchListener l) {//設置onTouch監聽
    getListenerInfo().mOnTouchListener = l;
}

ListenerInfo getListenerInfo() {
    if (mListenerInfo != null) {
        return mListenerInfo;
    }
    mListenerInfo = new ListenerInfo();
    return mListenerInfo;
}

public interface OnTouchListener {//Touch介面,用於回調onTouch事件
    boolean onTouch(View v, MotionEvent event);
}

4.6.3 View->onTouchEvent()

        事件如未被onTouch消費掉,則會引用到onTouchEvent()方法,該方法會涉及ACTION_UP、ACTION_DOWN、ACTION_CANCEL、ACTION_MOVE事件的處理,View的onClick()、onLongClick()也是由該方法觸發。此外,如果當前的View是可點擊的話,則直接消費該事件。

public boolean onTouchEvent(MotionEvent event) {
    ...
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
        || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;//當前View是否可點擊
    ...
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP://抬起
                ...
                if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                    if (!focusTaken) {
                        removeLongPressCallback();//若有長按事件未處理,則移除長按事件
                        if (mPerformClick == null) {
                            mPerformClick = new PerformClick();
                        }
                        if (!post(mPerformClick)) {//通過Hanlder將點擊事件發送到主線程執行
                            performClickInternal();//如果不成功,則直接引用點擊事件
                        }
                    }
                }
                if (mUnsetPressedState == null) {
                    mUnsetPressedState = new UnsetPressedState();//更新按鈕的按壓事件
                }
                ...
                break;
            case MotionEvent.ACTION_DOWN://按下
                ...
                if (isInScrollingContainer) {//在可滾動的容器內,為了容錯,延遲點擊
                    ...
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    setPressed(true, x, y);//設置按下的狀態
                    checkForLongClick(
                            ViewConfiguration.getLongPressTimeout(),
                            x,
                            y,
                            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);//開啟一個長按延時事件
                }
                break;

            case MotionEvent.ACTION_CANCEL://取消
                ...
                break;
            case MotionEvent.ACTION_MOVE://移動
                ...
                break;
        }
        return true;//如果是可點擊的View,即消費事件
    }
    ...
    return false;
}

4.7 例子-點擊事件時序圖

        如下是Android的點擊事件時序圖,如果能夠理解單擊事件的由來,對於整個事件分發的知識要點已大體掌握。

五、小結&問題點

  1. 事件分發流程?包括ACTION_DWON、ACTION_UP、ACTION_MOVE事件的處理過程;
  2. ACTION_CANCEL事件的使用場景?父控制項對move事件攔截場景?
  3. 單擊、長按、觸摸事件的產生過程?
  4. 點擊一個View未抬起,同時move該事件直至離開當前View的範圍,處理過程如何?
  5. 如果所有View都未消費事件,流程如何?
  6. ViewPage+ListView,左右滑動和上下滑動衝突的解決問題?即事件攔截過程?
  7. 普通的View是根據什麼來決定是否消費事件,例如Button?
    =>答:如無重寫onTouchEvent事件,根據當前的View是否可點擊,來決定是否消費事件。

        我最開始沒有看源碼,直接去看博客上的內容,彎彎繞繞,似懂非懂。在面試的過程中,面試官舉個場景分析流程,我都懵逼,分析不出來,現場很尷尬。之後看源碼,整體流程代碼量很少,感嘆於Android事件分發流程的設計,很少的代碼量,卻承載了很重要的功能,而沒有見過該模塊發生過異常。
        多讀書,多看報,少吃零食,多睡覺!

六、代碼倉庫地址

Demo地址:  https://gitee.com/linzhiqin/custom-demo

七、參考資料

https://zhuanlan.zhihu.com/p/623664769?utm_id=0
事件分發視頻(總結很好,但是得先理解基本概念,才方便學習)
https://www.bilibili.com/video/BV1sy4y1W7az?p=1&vd_source=f222e3bf3083cad8d9f660629bc47c16


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

-Advertisement-
Play Games
更多相關文章
  • 使用STM32CubeMX軟體配置STM32F407開發板上串口USART1進行DMA傳輸數據,然後實現與實驗STM32CubeMX教程9 USART/UART 非同步通信相同的目標 ...
  • 文件系統結構 unix的文件系統相關知識 unix將可用的磁碟空間劃分為兩種主要類型的區域:inode區域和數據區域。 unix為每個文件分配一個inode,其中保存文件的關鍵元數據,如文件的stat屬性和指向文件數據塊的指針。 數據區域中的空間會被分成大小相同的數據塊(就像記憶體管理中的分頁)。數據 ...
  • 1月9日,計世資訊(CCW Research)發佈《2022-2023年中國信創資料庫行業市場研究報告》(以下簡稱“報告”),從產品技術能力和市場及戰略能力兩個維度對我國主要資料庫產品服務商進行競爭力分析。其中,中國電信天翼雲憑藉其產品豐富的管理功能、靈活的部署架構,位列雲資料庫產品領域領導者象限。 ...
  • 作者:俊達 引言 MySQL是MySQL安裝包預設的客戶端,該客戶端程式通常位於二進位安裝包的bin目錄中,或者通過rpm安裝包安裝mysql-community-client,是資料庫管理系統的重要組成部分。MySQL客戶端不僅僅是一個簡單的軟體工具,更是連接用戶與資料庫之間的橋梁,對於有效地使用 ...
  • 作者:櫰木 環境準備 本次使用到的二進位軟體包目錄為:系統初始化前提是操作系統已完成安裝、各個主機之間網路互通,系統常用命令已安裝,本預設這些前提條件已具備,不在闡述。 1 主機環境初始化 安裝centos系統完成後需要對主機進行初始化配置和驗證工作,在所有主機上(hd1.dtstack.com-h ...
  • 摘要 隨著任務數量、任務類型需求不斷增長,對我們的數據開發平臺提出了更高的要求。本文主要分享我們將調度引擎升級到 Apache DolphinScheduler 的實踐經驗,以及對數據開發平臺的一些思考。 1. 背景 首先介紹下我們的大數據平臺架構: 數據計算層承接了全公司的數據開發需求,負責運行各 ...
  • 一、背景 為瞭解決應卡頓,分析耗時。 二、原理 Looper中的loop方法: public static void loop() { ... for (;;) { ... // This must be in a local variable, in case a UI event sets th ...
  • ☞ Github ☜ ☞ Gitee ☜ 說明 Binder作為Android系統跨進程通信的核心機制。網上也有很多深度講解該機制的文章,如: Android跨進程通信詳解Binder機制原理 Android系統核心機制Binder【系列】 這些文章和系統源碼可以很好幫助我們理解Binder的實現原 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...