原因剖析

UI僵死无非只是因为UI线程因繁忙而无法去接受用户的响应。详细说来内在原因有以下两个:

  1. 正常的业务代码写在UI线程中执行,业务代码的任务繁重导致UI线程无法分身去接受用户的界面输入
  2. UI控件在非UI线程中创建。原因如下如述:
    1. 每一个UI控件创建后都向SystemEvents注册UserPreferenceChanged事件,并且创建了控件的线程会被自动安装WindowsFormsSynchronizationContext作为其同步上下文
    2. 系统默认在UI线程里创建一个隐藏窗口“.NET-BroadcastEventWindow”来获取SystemEvents相关的系统消息
    3. 此隐藏窗口获取到消息后通过系统的PostMessage方法向注册了此事件的各UI控件发送通知并等待(注意不是通过SendMessage)
    4. 若某UI控件未创建在UI线程上,因为其创建控件的线程不会监视和获取本线程的消息队列中的消息,所以UI线程的PostMessage方法会一直等待,UI呈现僵死状态

PostMessage

  • 将消息送至目标window所在线程(可通过系统API获取控件的句柄所属的线程)的“Posted Message Queue”消息队列,消息称为列队型(queued)型消息
  • Control.Invoke与Control.BeginInvoke都调用PostMessage(相比SendMessage可防止死锁),区别是前者会使用WaitForWaitHandle来等待消息处理完毕

SendMessage

  • 将消息送至目标window所在线程的“Sent Message Queue”消息队列,但消息称为非列队型(Non-queued)消息
  • 发送线程调用SendMessage后会挂起并等待返回,如果期间有其他线程发消息给这个发送线程,它可以响应,但仅限于非队列型(Non-queued)消息
  • WH_CALLWNDPROC钩子用于监视SendMessage调用

异常发生后如何诊断

诊断的目的是要确定引发了UI线程繁忙的原因。

  • 若是因为上述第1点原因,即因为正常的业务代码在UI线程中跑的话,直接用VS等调试工具看一下UI线程的堆栈即可;
  • 若若因为上述第2点原因,即因为在非UI线程中创建了UI控件的话,那得先找出此控件。UI线程里此控件因为触发了SynchronizationContext.Send方法而冻结。
    • 使用spy++可以直接查看活动的后台线程上是否有控件。不过若线程将控件创建出来放在堆内存上后线程就消亡了的话,那就无法看到了。
    • 使用Windbg:
      • 获取UI线程堆栈一看便知控件的类名,对照着Windbg的“!dso”命令结果找到此控件的地址,再查找其引用。若UI线程中显示的控件类型因其为内部的子控件且为通用类型而无法直接定位代码的话,那么可以尝试追溯找到此控件的父控件。
      • 也可获取让UI冻结的WindowsFormSynchronizationContext,再通过以下方式找出目标托管线程的ID。不过因为托管线程在消亡后的ID可以被重复使用,所以通过此方式找到的托管线程ID可能是已经消亡的线程的ID,所以之后要找到目标Thread对象再比较其Thread.m_ExecutionContext._syncContext是否不为空且正为让UI冻结的WindowsFormSynchronizationContext。
      1. 使用“!do <synchronizationContext对象地址>”命令显示其数据结构,从成员destinationThreadRef获取指定了创建控件的目标线程的WeakReference对象地址
      2. 使用“!dumpobject <WeakReference对象地址>”显示其数据结构,从成员m_handle获取创建控件的目标线程的句柄
      3. 使用“dd <目标线程的句柄> L1”命令显示此句柄中包含的线程地址
      4. 使用“!dumpobject <目标线程地址>”命令显示目标线程的数据结构,从成员m_ManagedThreadId获取目标线程的托管线程号
      5. 使用“?0n<托管线程号>”命令获取托管线程号的16进制数

防患于未然

写代码时应该遵循这条原则:保证线程安全,尤其是不该在非UI线程上直接进行UI操作,包括控件的创建。

不过团队水平参差不齐,即使是一些老手也难免犯错。

所以如果能够拦截控件创建的过程,那么就可以通过Windows API根据此控件的句柄获取其在运行的线程号,看是否就是主UI线程号来输出日志,以在调试阶段解决问题。

有以下两种途径:

拦截Winodws Message。通过创建Global Hook拦截所有线程的窗口创建消息。

拦截Windows API。通过拦截各线程对窗口创建的API的调用。

使用windbg拉截windows api的调用前不要忘了为其加载符号。如:srv*c:\symbols*http://msdl.microsoft.com/download/symbols

本人通过EasyHook开源库使用了第2种方法,即拦截对WindowsAPI的调用完成了工具的创建,截图如下:

参考

Debugging Windows Forms Application Hangs During SystemEvents.UserPreferenceChanged

Windows Forms application freezes when system settings are changed or the workstation is locked

Mysterious Hang or The Great Deception of InvokeRequired

细说UI线程和Windows消息队列

理解Windows窗体和WPF中的跨线程调用

WinForm二三事(三)Control.Invoke&Control.BeginInvoke

一千个是什么 - Windows消息机制(Windows Messaging)

Invoke and BeginInvoke

Windows 应用程序交互过程

PostMessage与SendMessage

Windows 应用程序交互过程

Using Window Messages to Implement Global System Hooks in C#

PInvoke.net

Windows API函数大全

Deviare API Hook Overview

EasyHook

HOOK API 函数跳转详解

Windows下Hook API技术 inline hook

Change C# Class object to System.IntPtr

GCHandle.Alloc 方法 (Object)

如何获得指定进程的主窗口

EnumWindows function

Control.InvokeRequired 属性

线程句柄

UI僵死分析的更多相关文章

  1. 对石家庄铁道大学网站UI的分析

         作为我们团队的PM,老师对我们提出了一些额外的要求,所以我发表这篇博客来谈一下对石家庄铁道大学网站UI的分析.      首先,PM 对项目所有功能的把握, 特别是UI.最差的UI, 体现了 ...

  2. 针对某一网站的UI进行分析

    本周课上教学通过对PM(项目经理)的学习,我了解到PM 对项目所有功能的把握, 特别是有关的UI内容.最差的UI, 体现了团队的组织架构:其次, 体现了产品的内部结构:最好, 体现了用户的自然需求. ...

  3. Android recovery UI实现分析

    Android recovery模式为何物? 关于这个问题, baidu上已经有无数的答案.不理解的朋友先补习一下. 从纯技术角度来讲, recovery和android本质上是两个独立的rootfs ...

  4. Android 性能优化(2)性能工具之「Hierarchy Viewer 」Optimizing Your UI:分析哪个view有性能问题,查看屏幕上某像素点的坐标,颜色等

    Optimizing Your UI In this document Using Hierarchy Viewer Running Hierarchy Viewer and choosing a w ...

  5. Reveal UI 分析工具分析手机 App

    上篇文章介绍了: Reveal UI 分析工具简单使用 这里介绍如何使用 Reveal UI 分析工具来进行手机 App UI 界面的分析. 前提准备: (1)已安装 Reveal 的 Mac (2) ...

  6. 对石家庄铁道大学网站首页进行UI分析

    对石家庄铁道大学网站首页进行UI界面分析首先,铁道大学的网页首页分为图文热点,学校新闻,校内公告,媒体看铁大,学术咨询等等模块.通过分析这些模块,可以看出,学校网站首页针对的使用对象有很多,包括学校领 ...

  7. BookStore示例项目---菜单栏UI分析

    部署 参照 ABP示例项目BookStore搭建部署 项目解构 1).动态脚本代理 启动项目时,默认会调用两个接口 /Abp/ApplicationConfigurationScript /Abp/S ...

  8. 转——Android应用开发性能优化完全分析

    [工匠若水 http://blog.csdn.net/yanbober 转载请注明出处.] 1 背景 其实有点不想写这篇文章的,但是又想写,有些矛盾.不想写的原因是随便上网一搜一堆关于性能的建议,感觉 ...

  9. DIY Ruby CPU 分析——Part I

    [编者按]原文作者 Emil Soman,Rubyist,除此之外竟然同时也是艺术家,吉他手,Garden City RubyConf 组织者.本文是DIY Ruby CPU Profiling 的第 ...

随机推荐

  1. pig对null的处理(实际,对空文本处理为两种取值null或‘’)

    pig对文本null的处理非常特殊.会处理成两种null,还会处理成''这样的空值. 比方,读name,age,sex日志信息.name取值处理,假设记录为".,,"这样,会将na ...

  2. 积累的VC编程小技巧之列表框

    1.列表框中标题栏(Column)的添加 创建一个List Control,其ID为IDC_LIST,在其Styles属性项下的View项里选择Report.Align项里选择Top.Sort项里选择 ...

  3. MFC 用gdi绘制填充多边形区域

    MFC 用gdi绘制填充多边形区域 这里的代码是实现一个三角形的绘制,并用刷子填充颜色 在OnPaint()函数里面 运用的是给定的三角形的三个点,很多个点可以绘制多边形 CBrush br(RGB( ...

  4. OpenRisc-30-SD card controller模块分析与验证

    引言 ORPSoC的硬件平台是包含SD card controller控制器的,但是对应的linux里面却没有对应的linux的驱动程序,这使ORPSoC的SD card的使用收到了很大的限制.没有驱 ...

  5. opencv如何载入内存中的图像文件

    其实很简单,cv::imdecode 支持 std::vector<uchar>的,只要把char* 转 std::vector<uchar>就行了.用 std::vector ...

  6. Delphi回调函数及其使用

    Delphi回调函数及其使用 1 回调函数的概述 回调函数是这样一种机制:调用者在初始化一个对象(这里的对象是泛指,包括OOP中的对象.全局函数等)时,将一些参数传递给对象,同时将一个调用者可以访问的 ...

  7. cannot load supported formats intellij 解决的方法

    http://stackoverflow.com/questions/20797443/intellij-idea-subversion-checkout-error http://stackover ...

  8. TForm.ShowModal只是接管消息循环,禁止外部键盘和鼠标输入到别的窗口,但并不封锁其它窗口继续获取消息(比如WM_TIMER消息仍可被发送到别的窗口上)

    窗体上放一个TTimer,然后双击输入: procedure TForm1.Timer1Timer(Sender: TObject); var cvs: TCanvas; Rect: TRect; S ...

  9. C#实现树的双亲表示法

    watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbHVja3k1MTIyMg==/font/5a6L5L2T/fontsize/400/fill/I0JBQk ...

  10. 15个nosql

    1.MongoDB 介绍 MongoDB是一个基于分布式文件存储的数据库.由C++语言编写.主要解决的是海量数据的访问效率问题,为WEB应用提供可扩展的高性能数据存 储解决方案.当数据量达到50GB以 ...