EventBus源碼閱讀記錄

来源:http://www.cnblogs.com/mengdd/archive/2016/03/08/5255049.html
-Advertisement-
Play Games

EventBus是一個Android上用的消息分發的類庫,非常靈活好用,主要的原理是利用了反射註冊以及調用. 本文是在閱讀EventBus的源碼過程中所記錄的東西, 遇到不懂的去查了,然後留下了鏈接. 有點流水賬,講得也不是很深入,如果有錯請幫忙指正.


EventBus源碼閱讀記錄

repo地址:
greenrobot/EventBus

EventBus的構造

雙重加鎖的單例.

static volatile EventBus defaultInstance;
public static EventBus getDefault() {
    if (defaultInstance == null) {
        synchronized (EventBus.class) {
            if (defaultInstance == null) {
                defaultInstance = new EventBus();
            }
        }
    }
    return defaultInstance;
}

但是仍然開放了構造函數,用於構造其他別的對象.

Builder模式: EventBusBuilder.
有一個DEFAULT_BUILDER.

註冊

註冊即添加訂閱者,調用register()方法:
方法參數最全時共有三個參數:

private synchronized void register(Object subscriber, boolean sticky, int priority) {
    List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriber.getClass());
    for (SubscriberMethod subscriberMethod : subscriberMethods) {
        subscribe(subscriber, subscriberMethod, sticky, priority);
    }
}

其中subscriber(訂閱者)傳入的是一個對象,用到了它的class.
SubscriberMethodFinder會去找這個類中的方法.
被找到的方法最後會被緩存到一個map里,key是class, value是ArrayList<SubscriberMethod>().

尋找方法

在一個類(class)中尋找方法的過程, 首先是拿出方法:
在迴圈中skip了一些系統的類, 因為我們不可能在這些類裡加入方法.

while (clazz != null) {
    String name = clazz.getName();
    if (name.startsWith("java.") || name.startsWith("javax.") || name.startsWith("android.")) {
        // Skip system classes, this just degrades performance
        break;
    }

    // Starting with EventBus 2.2 we enforced methods to be public (might change with annotations again)
    try {
        // This is faster than getMethods, especially when subscribers a fat classes like Activities
        Method[] methods = clazz.getDeclaredMethods();
        filterSubscriberMethods(subscriberMethods, eventTypesFound, methodKeyBuilder, methods);
    } catch (Throwable th) {
        // Workaround for java.lang.NoClassDefFoundError, see https://github.com/greenrobot/EventBus/issues/149
        Method[] methods = subscriberClass.getMethods();
        subscriberMethods.clear();
        eventTypesFound.clear();
        filterSubscriberMethods(subscriberMethods, eventTypesFound, methodKeyBuilder, methods);
        break;
    }
    clazz = clazz.getSuperclass();
}

反射

關於反射的性能討論, 代碼中有說:

// This is faster than getMethods, especially when subscribers a fat classes like Activities
Method[] methods = clazz.getDeclaredMethods();

為什麼呢?
getMethods()返回了所有的public方法,包含從所有基類繼承的,也即包含了從Object類中繼承的public方法.
getDeclaredMethods()返回了該類中聲明的所有方法,包括各種訪問級別的,但是只包含本類中的,不包括基類中的方法.

相關DOC:

反射package-summary
getDeclaredMethods()
getMethods()

Issue of NoClassDefFoundError

這裡有一個try catch主要是為瞭解決這個issue: https://github.com/greenrobot/EventBus/issues/149
本來的流程是:

  1. 從自己的class開始,每次都getDeclaredMethods(), 即提取自己類中的方法,不取基類.
  2. 取完之後, getSuperclass(),獲取基類的class,重新進入while迴圈.直到進入java包或者android包才退出.

但是getDeclaredMethods()會檢查一些參數和返回值, 如果找不到類型則拋出NoClassDefFoundError.
getMethods()卻不檢查.

什麼樣的情況會拋出這個Error呢?

Android代碼里可能會有一些方法標明瞭@TargetApi,表明是更高級的sdk上才會有的.
這樣在低版本的機器上遇到了這些代碼,就無法解析出它們的類了.

只要你的作為subscriber的class里含有這種東西,就會出現問題.

為瞭解決這個崩潰, 所以代碼里catch了一把,然後採用第二種方案getMethods(),一次性get所有基類中的方法,這種效率雖然低,但是不會拋異常.
需要把之前的map都清理一把.

篩選方法

得到了所有的方法之後,開始篩選方法:

private void filterSubscriberMethods(List<SubscriberMethod> subscriberMethods, HashMap<String, Class> eventTypesFound, 
StringBuilder methodKeyBuilder, Method[] methods)

這裡第一個參數會作為最後的返回值,即我們方法選擇的結果.

篩選的過程, 遍歷所有找到的方法:

  1. 看它是以”onEvent”開頭,即為我們要找的目標方法.
  2. 然後getModifiers()看它是一個public的方法,並且不是我們要忽略的方法.
    註意這裡用到了位操作&來比較. 結果不為零表示滿足,為零表示不滿足.
    預設的忽略方法是static, bridge, synthetic方法.
    後兩個詞指的其實是同一種東東,但是這是什麼東東呢?
    是編譯器生成的方法, 見參考鏈接:
    https://javax0.wordpress.com/2014/02/26/syntethic-and-bridge-methods/
    https://docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html
    從上面的例子中可以看出,編譯器生成bridge方法主要是為了保證多態的順利進行.它和基類的簽名一樣,但是實現去調用了子類的方法.自己偷偷完成了其中的類型轉換.

  3. 獲取參數類型:必須是一個參數.
  4. 獲取ThreadMode: 即看方法名中onEvent之後還是什麼,一共有四種Mode,對應四種方法名:
    onEvent(), onEventMainThread(), onEventBackgroundThread(), onEventAsync()
    如果獲取不到ThreadMode,則continue;即這個方法不是我們要找的方法.

  5. StringBuilder組成一個key: method name>parameterType class name.
    註意這裡StringBuilder的清理方式是setLength(0).
    然後放進了一個eventTypesFound的HashMap, String是key, Class是value,這裡放的是method.getDeclaringClass();即方法聲明的那個類的類型.

註意這裡還利用了put()方法的返回值,如果map里之前有這個key對應的值,那麼老的value會作為返回值返回.
文檔:
HashMap.put()

這裡還用了這個一個方法: isAssignableFrom
判斷是否自己的class是參數的基類或介面.如果傳入的參數是當前對象的子類或自身,則返回true.

如果有old class存在,並且old class和新的class不能互相轉換, 後者old是new的子類, 那麼eventTypesFound這個map里還是保存老的值.

如果存在old class,但是old class是新加class的父類,會把新的class加進eventTypesFound的map,取代前者,即這個map中儘量放繼承體系下層中更具體的類.
這裡雖然父類沒有被放進eventTypesFound,但是父類的方法仍然會被加進最後返回的methods的map.

篩選結束後,我們就獲取到了所有的目標方法.
把它們都存在了一個cache map裡面,以免同一個類下次我們又要重新篩選一遍:

private static final Map<Class<?>, List<SubscriberMethod>> methodCache = new HashMap<Class<?>, List<SubscriberMethod>>();

訂閱

得到了方法的list(List<SubscriberMethod>)之後,我們要對每一個成員調用
private void subscribe(Object subscriber, SubscriberMethod subscriberMethod, boolean sticky, int priority) 方法.

裡面有一個新的數據類型CopyOnWriteArrayList:
CopyOnWriteArrayList Java doc
CopyOnWriteArrayList android doc

類說明: A thread-safe variant of ArrayList in which all mutative operations (add, set, and so on) are implemented by making a fresh copy of the underlying array.

這個數據類型是一個ArrayList,但是它在每次進行變異操作之前都拷貝一份新的.它底層的數組是volatile的.
這種數據類型的寫操作代價很高.

subscribe()方法中主要是給這兩個欄位放數據:

  1. private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;
    key是eventType的Class, value是Subscription這種數據類型的數組:
    Subscription newSubscription = new Subscription(subscriber, subscriberMethod, priority);

  2. private final Map<Object, List<Class<?>>> typesBySubscriber;
    key是subscriber,即訂閱者的類的對象,value是eventType的class,即事件類.

註銷

unregister()的時候, 傳入subscriber:
首先從typesBySubscriber獲取到事件的List,然後遍歷這個List, 從subscriptionsByEventType中移除該eventType,並且subscriber是當前subscriber的Subscription.
遍歷完成之後,從typesBySubscriber移除該subscriber.

事件觸發

好了,註冊和反註冊到這裡就結束了,看起來也就是找到一些方法和類型,放在一些map裡面,註銷的時候再從map裡面拿出來而已.
真正做事情的代碼呢?

首先看事件的觸發: post()方法, 這裡傳入的參數是事件類對象.

public void post(Object event) {
    PostingThreadState postingState = currentPostingThreadState.get();
    List<Object> eventQueue = postingState.eventQueue;
    eventQueue.add(event);

    if (!postingState.isPosting) {
        postingState.isMainThread = Looper.getMainLooper() == Looper.myLooper();
        postingState.isPosting = true;
        if (postingState.canceled) {
            throw new EventBusException("Internal error. Abort state was not reset");
        }
        try {
            while (!eventQueue.isEmpty()) {
                postSingleEvent(eventQueue.remove(0), postingState);
            }
        } finally {
            postingState.isPosting = false;
            postingState.isMainThread = false;
        }
    }
}

大致地看上去好像就是加入了一個隊列,然後發送出去直到隊列為空.

對每一個事件來說,是調用了postSingleEvent()這個方法.

postSingleEvent()這個方法里eventInheritance為true時(預設行為)會把event的class拿出來,然後取出它的所有基類和介面,和它自己一起放在一個map里.
這是可以理解的,因為可能我們本來的需求是監聽了一個災難事件,後來的需求發展,又寫了個它的子類事件叫地震.
那麼當我post地震事件的時候,除了地震事件後來新加的處理,當然也要採取原先災難事件的相關措施.

取出所有基類和介面的方法:lookupAllEventTypes()

/** Looks up all Class objects including super classes and interfaces. Should also work for interfaces. */
private List<Class<?>> lookupAllEventTypes(Class<?> eventClass) {
    synchronized (eventTypesCache) {
        List<Class<?>> eventTypes = eventTypesCache.get(eventClass);
        if (eventTypes == null) {
            eventTypes = new ArrayList<Class<?>>();
            Class<?> clazz = eventClass;
            while (clazz != null) {
                eventTypes.add(clazz);
                addInterfaces(eventTypes, clazz.getInterfaces());
                clazz = clazz.getSuperclass();
            }
            eventTypesCache.put(eventClass, eventTypes);
        }
        return eventTypes;
    }
}

所有這些費時的遍歷查找操作都是有一個map作為cache的.
註意這裡添加介面的時候,因為介面是多繼承的,所以除了去重以外,還需要深入遍歷:

/** Recurses through super interfaces. */
static void addInterfaces(List<Class<?>> eventTypes, Class<?>[] interfaces) {
    for (Class<?> interfaceClass : interfaces) {
        if (!eventTypes.contains(interfaceClass)) {
            eventTypes.add(interfaceClass);
            addInterfaces(eventTypes, interfaceClass.getInterfaces());
        }
    }
}

獲取到所有類型之後,進行遍歷, 對每一個eventClass進行處理, 真正的對每一個類型post的方法是這個:
private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass)

這裡,從之前那個subscriptionsByEventType裡面,根據eventClass把CopyOnWriteArrayList<Subscription>拿出來.
這裡拿出來的就是一個List,裡面是一個一個的onEventXXX方法的個體,
對每一個Subscription,執行了:
private void postToSubscription(Subscription subscription, Object event, boolean isMainThread)

線程模式

這裡根據線程模式不同,有一個switch case.

private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
    switch (subscription.subscriberMethod.threadMode) {
        case PostThread:
            invokeSubscriber(subscription, event);
            break;
        case MainThread:
            if (isMainThread) {
                invokeSubscriber(subscription, event);
            } else {
                mainThreadPoster.enqueue(subscription, event);
            }
            break;
        case BackgroundThread:
            if (isMainThread) {
                backgroundPoster.enqueue(subscription, event);
            } else {
                invokeSubscriber(subscription, event);
            }
            break;
        case Async:
            asyncPoster.enqueue(subscription, event);
            break;
        default:
            throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
    }
}

這裡invokeSubscriber(Subscription subscription, Object event)方法就是直接通過Method, 反射調用, invoke了那個方法.

`case PostThread`: 直接在當前線程調用這個方法.
`case MainThread`: 如果當前線程是主線程,則直接調用,否則加入mainThreadPoster的隊列.
`case BackgroundThread`: 如果當前是主線程,加入backgroundPoster隊列, 否則直接調用.
`case Async`: 加入asyncPoster隊列.

加入的三個隊列類型如下:

private final HandlerPoster mainThreadPoster; 
private final BackgroundPoster backgroundPoster;
private final AsyncPoster asyncPoster;

HandlerPoster繼承自Handler, 內部有一個PendingPostQueue.
這三個poster裡面都是這個PendingPostQueue, 數據結構是PendingPost

關於Queue的相關知識

隊列Queue: Java中Queue是一個介面, 類文檔:
[Queue Java doc] (https://docs.oracle.com/javase/7/docs/api/java/util/Queue.html)
它是繼承自Collection這個介面:
[Collection] (https://docs.oracle.com/javase/7/docs/api/java/util/Collection.html)

Queue這個數據結構可以自己定義順序, 可以用來做FIFO也可以用來做LIFO.
每一種Queue的實現都必須指定要用什麼順序.
不管是什麼順序,head上的那個元素都是remove()poll()即將移除的元素.

offer()方法將會試圖插入一個元素,如果失敗了就會返回false.
remove()poll()方法都會刪除並返回head元素.
peek()只查詢,不remove.

主線程處理 HandlerPoster

所以這裡看看HandlerPoster是怎麼做的:

  1. 它繼承自Handler, 初始化的時候用的是mainLooper,所以確保了消息處理操作都是在主線程:
    mainThreadPoster = new HandlerPoster(this, Looper.getMainLooper(), 10);
  2. 這個裡面寫了一個自己的queue: PendingPostQueue裡面包含的數據是:PendingPost.
    PendingPost這個類里用了一個pool來實現一個對象池,最大限制是10000.
    obtain的時候, 如果池子里有對象,則從池子里拿出來一個, 如果池中沒有對象,則new一個新的PendingPost; release的時候放回池子去.

HandlerPoster主要做兩件事:

  1. enqueue一個PendingPost, sendMessage,
  2. 在handleMessage()方法裡面處理message.
    handleMessage()裡面是一個while迴圈,從隊列裡面拿出PendingPost然後調用EventBus的invokeSubscriber()方法.
    這裡調用方法之前就會release該PendingPost.

非同步和後臺處理 AsyncPoster和BackgroundPoster

AsyncPosterBackgroundPoster都是一個Runnable.

enqueue的時候把PendingPost加入隊列, 然後調用eventBus.getExecutorService().execute(this);

run()方法裡面就是從隊列中拿出PendingPost,然後invoke,和上面很像.

預設的對象是:
private final static ExecutorService DEFAULT_EXECUTOR_SERVICE = Executors.newCachedThreadPool();
提供了一個線程池,可以非同步地執行操作.

那麼它們兩者有什麼不同呢?

AsyncPoster很簡單, run裡面直接invoke, 沒有過多的判斷. 即對每一個任務都是直接啟動線程執行.
BackgroundPoster比較複雜,有一個boolean來判斷是否正在run, run()方法裡面是一個while true的迴圈,當queue全部被執行完之後才return.
如果隊列中有任務正在執行,這時候enqueue()操作會加入元素到隊列中,等待執行.
即BackgroundPoster只用了一個線程,所有的事件都是按順序執行的,等到前面的任務執行完了才會進行下一個.

對各個模式的說明可以參見ThreadMode.java類.
Async模式下,不管你的post thread是什麼,都是會新啟線程來執行任務的,所以適用於那些比較耗時的操作.
為了避免併發線程過多, EventBus裡面使用了一個線程池來複用線程.

事件取消

有一個public的cancel方法:

public void cancelEventDelivery(Object event) {
    PostingThreadState postingState = currentPostingThreadState.get();
    if (!postingState.isPosting) {
        throw new EventBusException(
                "This method may only be called from inside event handling methods on the posting thread");
    } else if (event == null) {
        throw new EventBusException("Event may not be null");
    } else if (postingState.event != event) {
        throw new EventBusException("Only the currently handled event may be aborted");
    } else if (postingState.subscription.subscriberMethod.threadMode != ThreadMode.PostThread) {
        throw new EventBusException(" event handlers may only abort the incoming event");
    }

    postingState.canceled = true;
}

這個方法的使用可以從測試代碼裡面看出來:
1.首先它只能在handler裡面調用, 即第一個異常.這裡判斷的isPosting這個值在post的時候變為true,處理完就變為false.
這裡用到的currentPostingState:

private final ThreadLocal<PostingThreadState> currentPostingThreadState = new ThreadLocal<PostingThreadState>() {
    @Override
    protected PostingThreadState initialValue() {
        return new PostingThreadState();
    }
};

ThreadLocal類是什麼?
[ThreadLocal類] (https://docs.oracle.com/javase/7/docs/api/java/lang/ThreadLocal.html)

ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

主要是用來給每一個線程保存一個不同的狀態值.
這個currentPostingThreadState在第一次被調用get()方法的時候初始化,也即在public void post(Object event) 方法里.
然後修改了它的狀態, 之後再在同一個線程里,即可訪問到它的狀態.

這裡cancel的測試也寫得很有意思,可以看一下.

黏性事件

什麼叫Sticky?
字面上看是黏性的.

之前的事件都是非黏性的,即有一個register()unregister()方法.
register()了subscriber之後, EventBus會掃描該類中的onEventXXX()方法,建立一些map來記錄.
unregister()即合理地清除了這些數據.

而對於sticky的事件,註冊時調用registerSticky(), 並沒有相應的註銷方法.只有一個單獨的removeAllStickyEvents()方法.

sticky的事件註冊的時候, subscribe()方法中, 除了重覆上面正常的過程之外, 還有一個額外的map:
private final Map<Class<?>, Object> stickyEvents;
這個數據類型是: stickyEvents = new ConcurrentHashMap<Class<?>, Object>();
存的是event的Class和event對象.

註冊時如果發現這個map中相同的event type要處理,會立即觸發, 通知到它的訂閱者.

註意這個sticky event存的是最近的一個事件: most recent event.

sticky事件觸發的時候調用:
public void postSticky(Object event)

sticky的代碼里有一個cast()方法:
看文檔:
Class

這個cast()方法就是用來把對象強轉成當前的這個Class類型.

結語

EventBus是一個Android上用的消息分發的類庫,非常靈活好用,主要的原理是利用了反射註冊以及調用.

本文是在閱讀EventBus的源碼過程中所記錄的東西, 遇到不懂的去查了, 然後留下了鏈接.
有點流水賬,講得也不是很深入,如果有錯請幫忙指正.


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

-Advertisement-
Play Games
更多相關文章
  • HTML 教程 HTML 簡介 html div 標簽介紹 html span 標簽介紹 html a 超鏈接標簽 HTML Br換行標簽介紹 HTML P段落標簽介紹 HTML br與p標簽區別 Html H 標題標簽 html px em pt長度單位 HTML B 加粗標簽 HTML stro
  • 在很多現實的場景中,有的文本框我們希望在選擇“是”的按鈕之後才出現,這就需要js控制TR的隱藏和顯示,如何控制,本文為大家揭曉 下文分享的一段代碼:選擇是的按鈕就顯示身高和體重的文本框的代碼。註意:ready方法必須要引用jquery的庫。 1.html Code <html> <head> <me
  • 看到網上很多展示html5雪花飛動的效果,確實非常引人入勝,我相信大家也跟我一樣看著心動的同時,也很好奇,想研究下代碼如何實現;雖然哦很多地方也能下載這些源碼,不過也不知道別人製作此類動畫時的思路及難點分析。 我這幾天剛好學習了一下,也趁著此刻有時間從需求分析、知識點、程式編寫一步步給大家解剖下。
  • 1. css屬性命名區分大小寫嗎? 不區分,不過一般小寫,便於理解 2. margin-top和margin-bottom對於行內(內斂inline)元素效果一樣麽? 3.padding-top和padding-bottom對於inline元素都會增加元素本身的大小麽? 回答2,3,需要瞭解html...
  • 我們知道任何一個自定義函數都是Function構造器的實例,所以我們可以通過new Function的方式來創建函數,使用語法很簡單, new Function(形參1, 形參2, ..., 形參N, 函數體) 註意,裡面的參數全部是以字元串的形式呈現。比如一個簡單的例子——要求寫一個函數, 求兩個
  • ShineJaie 原創,轉載請註明出處。 昨晚在一個交流群里看到有位網友提了一個他的面試題求助答疑。剛好我也有看到,就對這個問題思考了一下,覺得這道題對理解 JavaScript 作用域還是很有幫助的,特此又把自己的解題思路梳理了一遍,希望對其它人有所幫助。 首先看下麵試題: 1 var arr
  • jQuery 2 的版本與jQuery 1的版本相比,沒有再考慮IE6,7,8的相容問題,因此使用時如果不用考慮IE6,7,8就用jQuery 2的版本,如果需要考慮IE6,7,8就使用jQuery 1的版本。 jQuery對象是一個以DOM為對象的特殊數組,並包含大量方法,簡單可以理解為: fun...
  • 作者: "@gzdaijie" 本文為作者原創,轉載請註明出處:http://www.cnblogs.com/gzdaijie/p/5255084.html 目錄 1.Activity生命周期 1.1 Activity生命周期簡介與測試 1.2 設備旋轉時的生命周期與數據恢復 1.3 Activit
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...