最近有这样一个需要,在一个AppDomain中通过UIAutomation做一些操作,并在操作完成后卸载掉这个AppDomain。
然而在卸载这个AppDomain时,总会出现System.CannotUnloadAppDomainException异常,不过从异常的Message里的“HRESULT: 0x80131015”这段能看出来,应该是AppDomain中的某个线程在释放过程中发生异常了。

经过好长时间的debug,终于发现罪魁祸首居然有两个线程,分别来自UIAutomationClient.dll和UIAutomationClientsideProviders.dll

UIAutomationClientsideProviders这个里面起了一个while(true)的线程,就不多展开了,其余部分也和UIAutomationClient差不多,我主要说一下UIAutomationClient的情况
UIAutomationClient源代码在这里:
https://referencesource.microsoft.com/UIAutomationClient/UIAutomationClient.csproj.html

在通过UIAutomationClient的ClientEventManager为控件事件增加监听回调时,不论是什么类别的事件,最终都会调用AddListener这个方法:

internal static void AddListener(AutomationElement rawEl, Delegate eventCallback, EventListener l)
{
lock (_classLock)
{
// If we are adding a listener then a proxy could be created as a result of an event so make sure they are loaded
ProxyManager.LoadDefaultProxies(); if (_listeners == null)
{
// enough space for 16 AddXxxListeners (100 bytes)
_listeners = new ArrayList();
} // Start the callback queue that gets us off the server's
// UI thread when events arrive cross-proc
CheckStartCallbackQueueing(); //
// The framework handles some events on behalf of providers; do those here
// // If listening for BoundingRectangleProperty then may need to start listening on the
// client-side for LocationChange WinEvent (only use *one* BoundingRectTracker instance).
if (_winEventTrackers[(int)Tracker.BoundingRect] == null && HasProperty(AutomationElement.BoundingRectangleProperty, l.Properties))
{
//
AddWinEventListener(Tracker.BoundingRect, new BoundingRectTracker());
} // Start listening for menu event in order to raise MenuOpened/Closed events.
if ( _winEventTrackers [(int)Tracker.MenuOpenedOrClosed] == null && (l.EventId == AutomationElement.MenuOpenedEvent || l.EventId == AutomationElement.MenuClosedEvent) )
{
AddWinEventListener( Tracker.MenuOpenedOrClosed, new MenuTracker( new MenuHandler( OnMenuEvent ) ) );
} // Begin watching for hwnd open/close/show/hide so can advise of what events are being listened for.
// Only advise UI contexts of events being added if the event might be raised by a provider.
// TopLevelWindow event is raised by UI Automation framework so no need to track new UI.
//
if (_winEventTrackers[(int)Tracker.WindowShowOrOpen] == null )
{
AddWinEventListener( Tracker.WindowShowOrOpen, new WindowShowOrOpenTracker( new WindowShowOrOpenHandler( OnWindowShowOrOpen ) ) );
AddWinEventListener( Tracker.WindowHideOrClose, new WindowHideOrCloseTracker( new WindowHideOrCloseHandler( OnWindowHideOrClose ) ) );
} // If listening for WindowInteractionStateProperty then may need to start listening on the
// client-side for ObjectStateChange WinEvent.
if (_winEventTrackers[(int)Tracker.WindowInteractionState] == null && HasProperty(WindowPattern.WindowInteractionStateProperty, l.Properties))
{
AddWinEventListener(Tracker.WindowInteractionState, new WindowInteractionStateTracker());
} // If listening for WindowVisualStateProperty then may need to start listening on the
// client-side for ObjectLocationChange WinEvent.
if (_winEventTrackers[(int)Tracker.WindowVisualState] == null && HasProperty(WindowPattern.WindowVisualStateProperty, l.Properties))
{
AddWinEventListener(Tracker.WindowVisualState, new WindowVisualStateTracker());
} // Wrap and store this record on the client...
EventListenerClientSide ec = new EventListenerClientSide(rawEl, eventCallback, l);
_listeners.Add(ec); // Only advise UI contexts of events being added if the event might be raised by
// a provider. TopLevelWindow event is raised by UI Automation framework.
if (ShouldAdviseProviders( l.EventId ))
{
// .. then let the server know about this listener
ec.EventHandle = UiaCoreApi.UiaAddEvent(rawEl.RawNode, l.EventId.Id, ec.CallbackDelegate, l.TreeScope, PropertyArrayToIntArray(l.Properties), l.CacheRequest);
}
}
}

能看出在第一次调用时,方法会进行一系列初始化操作,并开始监听事件。
其中CheckStartCallbackQueueing()方法是用来启动回调队列线程的,代码如下:

private static void CheckStartCallbackQueueing()
{
if (!_isBkgrdThreadRunning)
{
_isBkgrdThreadRunning = true;
_callbackQueue = new QueueProcessor();
_callbackQueue.StartOnThread();
}
} internal void StartOnThread()
{
_quitting = false; // create and start a background thread for this worker window to run on
// (background threads will exit if the main and foreground threads exit)
ThreadStart threadStart = new ThreadStart(WaitForWork);
_thread = new Thread(threadStart);
_thread.IsBackground = true;
_thread.Start();
}

也就是说ClientEventManager启动了一个线程,通过WaitForWork()方法循环获取消息并处理对应事件回调。
其中WaitForWork()内部循环的终止条件是_quitting == false,只有一处PostQuit()方法能使其暂停

internal void PostQuit()
{
_quitting = true;
_ev.Set();
}

而PostQuit()也只有一处CheckStopCallbackQueueing()方法在调用

private static void CheckStopCallbackQueueing()
{
// anything to stop?
if (!_isBkgrdThreadRunning)
return; // if there are listeners then can't stop
if (_listeners != null)
return; // Are any WinEvents currently being tracked for this client?
foreach (WinEventWrap eventWrapper in _winEventTrackers)
{
if (eventWrapper != null)
{
return;
}
} // OK to stop the queue now
_isBkgrdThreadRunning = false;
_callbackQueue.PostQuit();
// Intentionally not setting _callbackQueue null here; don't want to mess with it from this thread.
}

到这里可能还看不出什么问题,继续往上找,发现有两处在调用这个方法,分别是RemoveWinEventListener()和RemoveAllListeners(),因为我的代码没用到RemoveAllListeners,所以先看看前者

private static void RemoveWinEventListener(Tracker idx, Delegate eventCallback)
{
WinEventWrap eventWrapper = _winEventTrackers[(int)idx];
if (eventWrapper == null)
return; bool fRemovedLastListener = eventWrapper.RemoveCallback(eventCallback);
if (fRemovedLastListener)
{
_callbackQueue.PostSyncWorkItem(new WinEventQueueItem(eventWrapper, WinEventQueueItem.StopListening));
_winEventTrackers[(int)idx] = null; CheckStopCallbackQueueing();
}
}

和AddWinEventListener()只在AddListener()被调用一样,RemoveWinEventListener()也只在RemoveLisener()里集中调用,

internal static void RemoveListener( AutomationEvent eventId, AutomationElement el, Delegate eventCallback )
{
lock( _classLock )
{
if( _listeners != null )
{
bool boundingRectListeners = false; // if not removing BoundingRect listeners no need to do check below
bool menuListeners = false; // if not removing MenuOpenedOrClosed listeners no need to do check below
bool windowInteracationListeners = false; // if not removing WindowsIntercation listeners no need to do check below
bool windowVisualListeners = false; // if not removing WindowsVisual listeners no need to do check below for (int i = _listeners.Count - ; i >= ; i--)
{
EventListenerClientSide ec = (EventListenerClientSide)_listeners[i];
if( ec.IsListeningFor( eventId, el, eventCallback ) )
{
EventListener l = ec.EventListener; // Only advise UI contexts of events being removed if the event might be raised by
// a provider. TopLevelWindow event is raised by UI Automation framework.
if ( ShouldAdviseProviders(eventId) )
{
// Notify the server-side that this event is no longer interesting
try
{
ec.EventHandle.Dispose(); // Calls UiaCoreApi.UiaRemoveEvent
}
// PRESHARP: Warning - Catch statements should not have empty bodies
#pragma warning disable 6502
catch (ElementNotAvailableException)
{
// the element is gone already; continue on and remove the listener
}
#pragma warning restore 6502
finally
{
ec.Dispose();
}
} // before removing, check if this delegate was listening for the below events
// and see if we can stop clientside WinEvent trackers.
if (HasProperty(AutomationElement.BoundingRectangleProperty, l.Properties))
{
boundingRectListeners = true;
} if( eventId == AutomationElement.MenuOpenedEvent || eventId == AutomationElement.MenuClosedEvent )
{
menuListeners = true;
} if (HasProperty(WindowPattern.WindowInteractionStateProperty, l.Properties))
{
windowInteracationListeners = true;
} if (HasProperty(WindowPattern.WindowVisualStateProperty, l.Properties))
{
windowVisualListeners = true;
} // delete this one
_listeners.RemoveAt( i );
}
} // Check listeners bools to see if clientside listeners can be removed
if (boundingRectListeners)
{
RemovePropertyTracker(AutomationElement.BoundingRectangleProperty, Tracker.BoundingRect);
} if (menuListeners)
{
RemoveMenuListeners();
} if (windowInteracationListeners)
{
RemovePropertyTracker(WindowPattern.WindowInteractionStateProperty, Tracker.WindowInteractionState);
} if (windowVisualListeners)
{
RemovePropertyTracker(WindowPattern.WindowVisualStateProperty, Tracker.WindowVisualState);
} // See if we can cleanup completely
if (_listeners.Count == )
{
// as long as OnWindowShowOrOpen is static can just use new here and get same object instance
// (if there's no WindowShowOrOpen listener, this method just returns)
RemoveWinEventListener(Tracker.WindowShowOrOpen, new WindowShowOrOpenHandler(OnWindowShowOrOpen));
RemoveWinEventListener( Tracker.WindowHideOrClose, new WindowHideOrCloseHandler( OnWindowHideOrClose ) ); _listeners = null;
}
}
}
}

从RemoveWinEventListener()和RemoveListener()的逻辑也能大致看出,此处的意图应该是在最后一个用户添加的Listener被移除时,移除初始化中对部分window事件的监听,释放所有资源。
而第一次添加Listener时,ClientEventManager会进行一系列初始化,并创建线程去处理队列信息。
设计思路也是一致的,以第一次Add为起点,最后一次Remove为终点。但问题就出在上面标出来的这一段。

进去时_listeners是一个无元素非空的数组,满足条件。
但RemoveWinEventListener()中的CheckStopCallbackQueueing()会检查_listeners是否为null,如果不为null则表示不应该结束。
然后问题就出现了,最后一次RemoveWinEventListener()时,这是逻辑上最后一次调用中的CheckStopCallbackQueueing()来停止监听的机会,但由于_listeners还不是null,被提前return了,而出来之后又被赋了null。
但这回赋了null,因为后面再也没有机会调用CheckStopCallbackQueueing(),于是线程就停不下来了……

这回我们再往前看,还有个RemoveAllListeners()也包含了但RemoveWinEventListener(),那么如果我在调用了RemoveListener()之后再调用一次RemoveAllListeners()能不能停止进程呢?还是不行
因为RemoveAllListeners()在一开始就会判断_listeners是否为null,而我们的_listeners已经在之前被赋了null了…………

internal static void RemoveAllListeners()
{
lock (_classLock)
{
if (_listeners == null)
return;

看到这我已经想不明白了,为什么?是我哪里理解有问题吗……

而且注释里的这句”(background threads will exit if the main and foreground threads exit)“我感觉也挺耐人寻味的

UIAutomation踩坑的更多相关文章

  1. Spark踩坑记——Spark Streaming+Kafka

    [TOC] 前言 在WeTest舆情项目中,需要对每天千万级的游戏评论信息进行词频统计,在生产者一端,我们将数据按照每天的拉取时间存入了Kafka当中,而在消费者一端,我们利用了spark strea ...

  2. Spark踩坑记——数据库(Hbase+Mysql)

    [TOC] 前言 在使用Spark Streaming的过程中对于计算产生结果的进行持久化时,我们往往需要操作数据库,去统计或者改变一些值.最近一个实时消费者处理任务,在使用spark streami ...

  3. 【踩坑速记】二次依赖?android studio编译运行各种踩坑解决方案,杜绝弯路,总有你想要的~

    这篇博客,只是把自己在开发中经常遇到的打包编译问题以及解决方案给大家稍微分享一下,不求吸睛,但求有用. 1.大家都知道我们常常会遇到dex超出方法数的问题,所以很多人都会采用android.suppo ...

  4. NPOI导出Excel (C#) 踩坑 之--The maximum column width for an individual cell is 255 charaters

    /******************************************************************* * 版权所有: * 类 名 称:ExcelHelper * 作 ...

  5. 我的微信小程序入门踩坑之旅

    前言 更好的阅读体验请:我的微信小程序入门踩坑之旅 小程序出来也有一段日子了,刚出来时也留意了一下.不过赶上生病,加上公司里也有别的事,主要是自己犯懒,就一直没做.这星期一,赶紧趁着这股热乎劲,也不是 ...

  6. router路由去掉#!的踩坑记

    项目中在研究去掉router#!的过程中的踩坑过程.

  7. vue+ vue-router + webpack 踩坑之旅

    说是踩坑之旅 其实是最近在思考一些问题 然后想实现方案的时候,就慢慢的查到这些方案   老司机可以忽略下面的内容了 1)起因  考虑到数据分离的问题  因为server是express搭的   自然少 ...

  8. 记jQuery.fn.show的一次踩坑和问题排查

    最近很少已经很少用jQuery,因为主攻移动端,常用Zepto,其实很多细节和jQuery并不一样.最近又无意中接触到了PC的需求和IE6, 使用了jQuery,刚好踩坑了,特意记录一下. 本文内容如 ...

  9. WebForm路由踩坑 ajax请求多次

    WebForm路由踩坑 再次接触Asp.Net WebForm已是4年后的今天,源起新入职的公司,一个老的项目. Web接触的少,那就多动手写写. WebForm1.aspx <body> ...

随机推荐

  1. 【python基础语法】数据类型:数值、字符串 (第2天课堂笔记)

    """ 数据类型: 一.数值类型:整数 浮点数 布尔值 二.序列类型:字符串.列表 元祖 三.散列类型:字典 集合 可变数据类型: 列表 字典 集合,可以改动内存地址数据 ...

  2. 学习R语言的一点小心得

    1.目前R 语言处于入门阶段吧,能够执行一些简单的模型了,还是有收获的. 但是在跑模型的时候经常遇到各种各样的错误,最常见的错误就是数据带入模型之后,数据的类型不对,因此模型跑不下去,因此说,利用he ...

  3. [大数据技术]Kettle报OPTION SQL_SELECT_LIMIT=DEFAULT错误的解决办法

    百度得到的解决方式都是说mysql通过jdbc链接的时候会进行测试’SET OPTION SQL_SELECT_LIMIT=DEFAULT’,但是5.6以后的版本弃用了set的方式. 我用的MySQL ...

  4. 吴裕雄--天生自然HADOOP操作实验学习笔记:使用hive操作hbase

    实验目的 熟悉hive和hbase的操作 熟悉hadoop.hbase.hive.zookeeper的关系 熟练大数据环境的搭建 学会分析日志排除问题 实验原理 1.hive整合hbase原理 前面大 ...

  5. C# SQLITE 使用文档

    https://www.devart.com/dotconnect/sqlite/docs/Devart.Data.SQLite~Devart.Data.SQLite_namespace.html 有 ...

  6. Java安全笔记

    前言 后端接口开发中,涉及到用户私密信息(用户名.密码)等,我们不能传输明文,必须使用加密方式传输.这次政府项目中,安全测试组提出了明文传输漏洞,抽空研究了下Java加解密相关知识,记录下. 散列函数 ...

  7. 下载thinkphp5.0

    在服务器文件中右击Git_Bash_Here打开 在命令框写入 首先克隆下载应用项目仓库   git clone https://github.com/top-think/think tp5 tp5代 ...

  8. html无卡顿动画实现——requestAnimationFrame

    <!DOCTYPE html> <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> ...

  9. 牛客寒假训练营2-C算概率

    思路 用 f(i,j) 来表示当前做了i道题,共做对了j道题 状态 f[i][j] = f[i-1][j] * (1-p[i]) + f[i-1][j-1] * p[i] 第一种:由于i-1时对了j题 ...

  10. chm文件打开空白无内容

    问题描述 chm文件打开空白无内容 解决方案 选中 chm 文件,右键 - 属性- 在弹出的对话框中点击右下方的“接触锁定”,之后点击“应用”,再点击“确定”