Android事件分發-基礎原理和場景分析

来源:https://www.cnblogs.com/jingdongkeji/archive/2023/04/21/17339345.html
-Advertisement-
Play Games

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


作者:京東零售 郭旭鋒

1 為什麼需要事件分發

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

2 事件分發的關鍵方法

Android 中事件分發是從 Activity 開始的,可以看看各組件中事件分發的關鍵方法

Activity:沒有 onInterceptTouchEvent 方法,因為如果 Activity 攔截事件,將導致整個頁面都沒有響應,而 Activity 是系統應用和用戶交互的媒介,不能響應事件顯然不是系統想要的結果。所以 Activity 不需要攔截事件。

ViewGroup:三個方法都有,Android 中 ViewGroup 是一個佈局容器,可以嵌套多個 ViewGroup 和 View,事件傳遞和攔截都由 ViewGroup 完成。

View:事件傳遞的最末端,要麼消費事件,要麼不消費把事件傳遞給父容器,所以也不需要攔截事件。

3 事件分發流程分析

3.1 事件分發流程概覽

Activity 並不是一個 View,那麼 Activity 是如何將事件分發到頁面的 ViewGroup 和 View 的呢。我們先看看源碼

# Activity
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    // 調用 Window 對象的方法,開始事件分發
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    // 如果事件分發返回 false,也即事件沒被消費,則調用自己的 onTouchEvent 方法
    return onTouchEvent(ev);
}

public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }
    return false;
}



可以看到,Activity 中的事件分發方法 dispatchTouchEvent 調用了 getWindow().superDispatchTouchEvent(ev) 方法,而這裡的 WIndow 實際上是 PhoneWindow。

簡單來說,Window 是一個抽象類,是所有視圖的最頂層容器,視圖的外觀和行為都歸他管,無論是背景顯示、標題欄還是事件處理都是他管理的範疇,而 PhoneWindow 作為 Window的唯一親兒子(唯一實現類),自然就是 View 界的皇帝了。

下來看看 PhoneWindow 的代碼

# PhoneWindow
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}



PhoneWindow 中又調用了 mDecor.superDispatchTouchEvent(event) 方法。mDecor 是 DecorView 對象,再看看 DecorView 的代碼

# DecorView
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
}

# FrameLayout
public class FrameLayout extends ViewGroup {
}

# ViewGroup
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ......
    }
}

# View
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ......
    }
}



可以看到,DecorView 實際上就是 ViewGroup,事件分發方法最終調用到了 ViewGroup 的 dispatchTouchEvent(MotionEvent ev) 方法。

DecorView 是 PhoneWindow 的一個對象,其職位就是跟在 PhoneWindow 身邊專業為 PhoneWindow 服務的,除了自己要幹活之外,也負責消息的傳遞,PhoneWindow 的指示通過 DecorView 傳遞給下麵的 View,而下麵 View 的信息也通過 DecorView 回傳給 PhoneWindow。

Android 中的事件分發是責任鏈模式的一種變形。事件由上往下傳遞,如果事件沒有被消費則繼續傳遞到下一層,如果事件被消費則停止傳遞,如果到最下層事件則沒有被消費,則事件會層層傳遞給上一層處理。我們都知道事件分發的源頭在 Activity 中的 dispatchTouchEvent 方法中,事件從這裡開始,分發到佈局中的各個 View 中,不斷遞歸調用 ViewGroup/View 的 dispatchTouchEvent 方法。通過上面分析可以看到,Activity 在接受到上層派發來的事件後,會把事件傳遞到自己的 dispatchTouchEvent 方法中,然後Activity 會把觸摸、點擊事件傳遞給自己的 mWindow 對象,最終傳遞給 DecorView 的 dispatchTouchEvent 方法,實際調用的是 ViewGroup 的 dispatchTouchEvent 方法。

3.2 事件分發源碼分析

經過分析,可以知道 Android 中事件分發的關鍵方法就是 ViewGroup 和 View 中的相關方法,如下

# View
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {

    public boolean dispatchTouchEvent(MotionEvent event) {
        // ... 省略部分代碼
        boolean result = false;
        // ... 省略部分代碼
        if (onFilterTouchEventForSecurity(event)) {
            // ... 省略部分代碼
            // 1. 主要調用 onTouchEvent 方法,返回 true 說明事件被消費,否則沒被消費
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        // ... 省略部分代碼
        return result;
    }
    
    public boolean onTouchEvent(MotionEvent event) {
        // ... 省略部分代碼
        // 2. 預設可點擊則返回 true,也就是消費事件。Button 或設置過 OnClickListener,則 View 可點擊
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    // ... 省略部分代碼
                    break;
                case MotionEvent.ACTION_DOWN:
                    // ... 省略部分代碼
                    break;
                case MotionEvent.ACTION_CANCEL:
                    // ... 省略部分代碼
                    break;
                case MotionEvent.ACTION_MOVE:
                    // ... 省略部分代碼
                    break;
            }
            return true;
        }

        return false;
    }
}



View 中的方法邏輯比較簡單,如備註 1 所示,dispatchTouchEvent 主要就是做一些安全檢查,檢查通過後會調用 onTouchEvent 方法。而 onTouchEvent 方法中邏輯如備註 2 所示,如果 View 是可點擊的,則預設會認為消費事件,否則不消費,一般 Button 控制項,或設置過 OnClickListener 的控制項,View 會被預設設置為可點擊。

下麵看看 ViewGroup 代碼

# ViewGroup
public abstract class ViewGroup extends View implements ViewParent, ViewManager {

    public boolean dispatchTouchEvent(MotionEvent ev) {
        // ... 省略部分代碼
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // 1. 如果是 DOWN 事件,則重置事件狀態
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            final boolean intercepted;
            // 2. 如果是 DOWN 事件,會判斷當前 ViewGroup 是否要攔截事件。這裡受兩個因素影響:
            //    一是 FLAG_DISALLOW_INTERCEPT,如果設置不攔截,則不會調用 onInterceptTouchEvent,直接設置為不攔截
            //    二是沒設置 FLAG_DISALLOW_INTERCEPT 標誌,預設允許攔截,會調用 onInterceptTouchEvent 方法
            // 3. 如果不是 DOWN 事件,可能是 MOVE 或 UP 事件,mFirstTouchTarget 是記錄需要繼續進行事件分發的下一級子 View,包括ViewGroup 或 View,這裡也分為兩種情況
            //    如果 mFirstTouchTarget 不為空,說明需要繼續向下一級子 View/ViewGroup 分發事件,這時說明上次 DOWN 事件找到了下級有消費事件的子 View,且無攔截事件
            //    如果 mFirstTouchTarget 為空,說明沒找到要消費事件的子 View,或事件被攔截了
            if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }
            // ... 省略部分代碼
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            // 4. 下麵邏輯主要就是遍歷尋找能消費事件的 View,如果事件被攔截,則不需要再尋找
            if (!canceled && !intercepted) {
                // ... 省略部分代碼
                // 5. 只有 DOWN 事件才需要尋找,其他事件時已經確定是否找到,都不需要再找消費事件的 View 了
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    // ... 省略部分代碼
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        // ... 省略部分代碼
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            // ... 省略部分代碼
                            final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
                            // 6. 這個方法是關鍵
                            //    如果 child 不為空,則會再調用 child.dispatchTouchEvent 方法,達到層層遞歸的效果
                            //    如果 child 為空,則會調用 super.dispatchTouchEvent 方法,super 是 View,實際上調用了 onTouchEvent 方法,自己判斷是否消費事件
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // ... 省略部分代碼
                                // 7. 返回 true,說明找到了消費事件的 View,下麵方法會給 mFirstTouchTarget 賦值,下麵 mFirstTouchTarget 將不為空
                                //    註:mFirstTouchTarget 並不是最終消費事件的 View,而是下一級包含消費事件 View 的鏈表對象,或是直接消費事件的 View 的鏈表對象
                                //    每一個 ViewGourp 都會記錄一個 mFirstTouchTarget,mFirstTouchTarget.child 記錄了下一層消費事件的 ViewGroup 或 View
                                //    同時,alreadyDispatchedToNewTouchTarget 變數會設置為 true
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                            // ... 省略部分代碼
                        }
                        // ... 省略部分代碼
                    }
                    // ... 省略部分代碼
                }
            }

            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // 8. 當沒有找到消費事件的 View,或事件被攔截,mFirstTouchTarget 都不會被賦值,這裡 child 為空,會調用自己的 onTouchEvent 方法
                handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    // 9. 說明找到了消費事件的 View,並且已經分發,直接設置為已處理
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
                        // 10. 此方法和備註 6 和 8 都一樣,這裡多了 cancel 的處理邏輯。如果事件被攔截,需要給原來消費事件的 View 發一個 CANCEL 事件
                        if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
            // ... 省略部分代碼
        }
        // ... 省略部分代碼
        return handled;
    }
    
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        // 預設不攔截
        return false;
    }
    
    // 沒有覆寫這個方法,實際調用的是 View 的 onTouchEvent 方法
    public boolean onTouchEvent(MotionEvent event) {
    }
        
}



可以看到,ViewGroup 中的事件分發邏輯還是比較複雜,但抓住關鍵點後則很容易能看清它的本來面貌

(1)分發的事件包括 DOWN、MOVE、UP、CANCEL 幾種,用戶一個完整的動作就是由這幾個事件組合而成的

(2)只有 DOWN 事件中會尋找消費事件的目標 View,其他事件不會再尋找

(3)DOWN 事件尋找到目標 View 後,後續其他事件都會直接分發至目標 View

(4)事件可以被攔截,攔截後原目標 View 會收到 CANCEL 事件,後續將不會再收到任何事件(這也是這套機制不支持豐富的嵌套滑動的原因)

3.3 事件分發情景分析

3.3.1 分發過程沒有任何 View 攔截和消費

(1)事件返回時,為了簡化理解,dispatchTouchEvent 直接指向了父 View 的 onTouchEvent ,實際上它僅僅是返回給父 View 的 dispatchTouchEvent 一個 false 值(影響了 mFirstTouchTarget 的值),父 View 根據返回值來調用自身的onTouchEvent 方法

(2)ViewGroup 是根據 onInterceptTouchEvent 的返回值(影響了 mFirstTouchTarget 的值)確定是調用子 View 的 dispatchTouchEvent 還是自身的 onTouchEvent 方法

(3)如果所有 View 都沒有消費 DOWN 事件,後續 MOVE 和 UP 不會再往下傳遞,會直接傳遞給 Activity 的 onTouchEvent 方法

3.3.2 最底層View消費事件,且上層View沒有攔截事件

(1)若沒有 ViewGroup 對事件進行攔截,而最底層 View 消費了此事件,也就是接收到 DOWN 事件時 View 的 onTouchEvent 返回 true,事件將不會再向上傳遞給各個 ViewGroup 的 onTouchEvent 方法,而是直接返回,後續的 MOVE 和 UP 事件也將會直接交給 View 進行處理

3.3.3 最底層View沒有消費事件,ViewGroup2消費了事件,且上層View沒有攔截事件

(1)如果 View 沒有消費事件,在層層調用父佈局的 onTouchEvent 方法時,有 View 消費此事件,如 ViewGroup2 消費此事件,後續 MOVE 和 UP 事件將會傳遞給 ViewGroup2 的 onTouchEvent 方法,而且不會再調用 ViewGroup2 的 onInterceptTouchEvent 方法

(2)源碼 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {} 這個代碼中主要調用 onInterceptTouchEvent() 方法和處理是否攔截

第一次是 DOWN 事件會進行判斷,所以會調用 onInterceptTouchEvent 攔截方法

第二次非 DOWN 事件,不會再調用 onInterceptTouchEvent 方法。原因如下:

◦ 如果 DOWN 事件的時候進行過攔截,也就是 onInterceptTouchEvent() 方法返回 true,則 mFirstTouchTarget 必定為 null,不會調用 onInterceptTouchEvent 方法。因為後面不會對這個值賦值,會往下走邏輯,直接調用到此 View 或 ViewGroup 的 onTouchEvent() 方法

◦ 如果 DOWN 事件沒有攔截,但子 View 的 onTouchEvent 都返回 false,只有當前 ViewGroup 的 onTouchEvent 返回 true,mFirstTouchTarget 也同樣為 null,也不會調用 onInterceptTouchEvent 方法。因為 mFirstTouchTarget 本質是找能接收事件的子 View,所有子 View 都不接收事件,mFirstTouchTarget 就必然為 null

3.3.4 ViewGroup2攔截了並消費了DOWN事件,其他View沒有攔截事件

(1)ViewGroup2 攔截 DOWN 事件後,View 不會接收到任何事件。ViewGroup2 消費事件後,後續 MOVE 和 UP 事件會交給 ViewGroup2 的 onTouchEvent 方法進行處理,且不會再調用 ViewGroup2 的onInterceptTouchEvent 方法

3.3.5 View消費了DOWN事件,ViewGroup2攔截且消費了MOVE事件,其他View沒有攔截事件

(1)View 中 DOWN 事件正常傳遞

(2)當 ViewGroup2 攔截 MOVE 事件後,當前 mFirstTouchTarget 不為空,首先 View 會收到轉換後的 CANCEL 事件,mFirstTouchTarget 會置為空,下次 MOVE 事件由於 mFirstTouchTarget 為空,會調用到自己的 onTouchEvent 方法

3.3.6 View消費 DOWN 事件,ViewGroup2攔截且消費了MOVE事件,一定條件後,ViewGroup1再次攔截和消費MOVE事件,其他View沒有攔截事件

3.4 事件分發總結

(1)整個分發過程中沒有任何攔截和消費,DOWN 事件會層層往下分發,並層層往上返回 false,MOVE 和 UP 事件則會交給 Activity 的 onTouchEvent 方法進行處理,不再往下分發

(2)分發過程中沒有任何攔截但有消費,DOWN 事件會層層往下分發,並層層往上返回false,直到有消費返回 true,MOVE 和 UP 事件則會層層往下分發,最後直接交給消費事件的 View 進行處理,然後層層返回 true

(3)分發過程中有攔截且攔截後消費,DOWN 事件會層層往下分發,直到有攔截後直接交給消費的 View 進行處理,MOVE 和 UP 事件則會層層往下分發,最後直接交給消費事件的 View 進行處理,然後層層返回true

(4)分發過程中不攔截 DOWN 事件,但攔截 MOVE 事件且攔截後消費,第一次攔截,之前收到 DOWN 事件的子 View 會收到 CANCEL 事件,並層層返回;後續 MOVE 和 UP 會層層往下分發,最後直接交給消費事件的 View 進行處理

(5)分發過程中不攔截 DOWN 事件,但攔截 MOVE 事件且攔截後不消費,第一次攔截,之前收到 DOWN 事件的子 View 會收到 CANCEL 事件,並層層返回;後續 MOVE 和 UP 會層層往下分發,最後交給攔截的 View 進行處理,此時由於攔截的 View 沒有消費,會層層往上返回 false,最後會交給 Activity 的 onTouchEvent 方法進行處理

以上,是個人的一些分析和經驗,歡迎有興趣的小伙伴一起學習和探討!


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

-Advertisement-
Play Games
更多相關文章
  • 我學習的過程中,對於連接池和數據源分得不是很清楚,而且我發現有的人將資料庫等同於數據源,或者將數據源等同於連接池,實際上這些說法並不准確。 ...
  • 功能02-商鋪查詢緩存03 3.功能02-商鋪查詢緩存 3.6封裝redis工具類 3.6.1需求說明 基於StringRedisTemplate封裝一個工具列,滿足下列需求: 方法1:將任意Java對象序列化為json,並存儲在string類型的key中,並且可以設置TTL過期時間 方法2:將任意 ...
  • 摘要:如果你需要一款穩定可靠的高性能企業級KV資料庫,不妨試試GaussDB(for Redis)。 每當網路上爆出熱點新聞,混跡於各個社交媒體的小伙伴們全都開啟了討論模式。一條消息的產生是如何在群聊中傳遞的呢?讓我們一起來探索即時通訊系統(IM)的原理。 IM系統架構的原理 當你在群聊“相親相愛一 ...
  • 我定義了一系列靜態方法,用於執行SQL Server資料庫的增刪改查等操作。其中:ExecuteNonQuery方法用於執行指定的SQL語句並返回受影響的行數;ExecuteScalar方法用於執行指定的SQL語句並返回查詢結果的第一行第一列;ExecuteDataTable方法用於執行指定的SQL ...
  • Redis 作為一個成熟的記憶體型資料庫,對於記憶體占用和操作性能上會有自己的取捨,通過這些知識可以理解為什麼 Redis 的性能有時候會變化得那麼快。 ...
  • 一、MYSQL主從同步概述 1、什麼是MySQL主從同步? 實現數據自動同步的服務結構 主伺服器(master): 接受客戶端訪問連接 從伺服器(slave):自動同步主伺服器數據 2、主從同步原理 Maste:啟用binlog 日誌 Slave:Slave_IO: 複製master主機binlog ...
  • 一、前言 先看一下Swift標準庫中對CustomStringConvertible協議的定義 public protocol CustomStringConvertible { /// A textual representation of this instance. /// /// Calli ...
  • 華為HMS生態攜手流媒體平臺Viu,為海外消費者打造精品移動娛樂應用體驗,並助力提升流量變現能力。Viu在中東非、東南亞等16個國家及地區提供廣告合作和付費會員服務,支持優質視頻內容高清點播和直播。自2019年起,Viu在中東非區域與華為HMS生態開展一系列緊密合作,併在2022年實現47%的用戶增 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...