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
  • 1、預覽地址:http://139.155.137.144:9012 2、qq群:801913255 一、前言 隨著網路的發展,企業對於信息系統數據的保密工作愈發重視,不同身份、角色對於數據的訪問許可權都應該大相徑庭。 列如 1、不同登錄人員對一個數據列表的可見度是不一樣的,如數據列、數據行、數據按鈕 ...
  • 前言 上一篇文章寫瞭如何使用RabbitMQ做個簡單的發送郵件項目,然後評論也是比較多,也是準備去學習一下如何確保RabbitMQ的消息可靠性,但是由於時間原因,先來說說設計模式中的簡單工廠模式吧! 在瞭解簡單工廠模式之前,我們要知道C#是一款面向對象的高級程式語言。它有3大特性,封裝、繼承、多態。 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 介紹 Nodify是一個WPF基於節點的編輯器控制項,其中包含一系列節點、連接和連接器組件,旨在簡化構建基於節點的工具的過程 ...
  • 創建一個webapi項目做測試使用。 創建新控制器,搭建一個基礎框架,包括獲取當天日期、wiki的請求地址等 創建一個Http請求幫助類以及方法,用於獲取指定URL的信息 使用http請求訪問指定url,先運行一下,看看返回的內容。內容如圖右邊所示,實際上是一個Json數據。我們主要解析 大事記 部 ...
  • 最近在不少自媒體上看到有關.NET與C#的資訊與評價,感覺大家對.NET與C#還是不太瞭解,尤其是對2016年6月發佈的跨平臺.NET Core 1.0,更是知之甚少。在考慮一番之後,還是決定寫點東西總結一下,也回顧一下.NET的發展歷史。 首先,你沒看錯,.NET是跨平臺的,可以在Windows、 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 添加節點(nodes) 通過上一篇我們已經創建好了編輯器實例現在我們為編輯器添加一個節點 添加model和viewmode ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...
  • 類型檢查和轉換:當你需要檢查對象是否為特定類型,並且希望在同一時間內將其轉換為那個類型時,模式匹配提供了一種更簡潔的方式來完成這一任務,避免了使用傳統的as和is操作符後還需要進行額外的null檢查。 複雜條件邏輯:在處理複雜的條件邏輯時,特別是涉及到多個條件和類型的情況下,使用模式匹配可以使代碼更 ...
  • 在日常開發中,我們經常需要和文件打交道,特別是桌面開發,有時候就會需要載入大批量的文件,而且可能還會存在部分文件缺失的情況,那麼如何才能快速的判斷文件是否存在呢?如果處理不當的,且文件數量比較多的時候,可能會造成卡頓等情況,進而影響程式的使用體驗。今天就以一個簡單的小例子,簡述兩種不同的判斷文件是否... ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...