关于“UI线程”
http://www.cppblog.com/Streamlet/archive/2013/05/05/199999.html
缘起
这是一篇找喷的文章。
由于一些历史原因和人际渊源,周围同事谈论一些技术话题的时候,经常使用“UI线程”一词。虽然我从来没有看到其确切定义,但心里对其含义可能略懂,因此一直装作心知肚明的样子(以免被嘲讽)。
日前,一同事发了封邮件大谈“UI线程”的概念,分享到大部门。大部门里除了我们一个Windows客户端部门,其他都是做网站的Java开发。因此,在他们面前谈论一些我们并不成熟甚至并不存在的概念,有那么一点点故弄玄虚的味道,这激起了我谈论这个话题的小小欲望。当然,并不是说那封邮件里说的有错误,事实上绝大部分语句都是正确的。不过我看到的最让人豁然开朗的一句话却是“UI线程并不是官方概念”。在此,我想梳理下有关“UI线程”始末和自己理解,望CppBlog的看官们批判。
对了,说明一下,本文的大背景是Win32桌面程序开发,.Net请绕道,WinRT请绕道,Web请绕道,手机请绕道……
“UI线程”语源
据考证,“UI线程”的概念最早可能是在MFC中被引入的。目前能找到的官方提法是在:
http://msdn.microsoft.com/en-us/library/b807sta6(v=vs.110).aspx
MFC的AfxBeginThread提供了两个版本:
CWinThread* AFXAPI AfxBeginThread(AFX_THREADPROC pfnThreadProc, LPVOID pParam, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL); CWinThread* AFXAPI AfxBeginThread(CRuntimeClass* pThreadClass, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL); |
第一个版本用来让人创建“工作线程”,第二个版本让人用来创建“UI线程”。可能由于来自MFC的远古光环,让“UI线程”的提法略有普及。但除此之外,在Windows开发方面,似乎找不到第二个例子了。(如果有,请在评论中告诉我。)不管怎样,既然MFC官方文档里说了,那么在“MFC领域”使用“UI线程”的提法总是可以的。下面,我们先来认识一下MFC中的UI线程以及工作线程。
MFC中的UI线程
我们按照http://msdn.microsoft.com/en-us/library/b807sta6(v=vs.110).aspx的指示,来创建一个“UI线程”。首先,继承CWinThread:
class CMyThread : public CWinThread { DECLARE_DYNCREATE(CMyThread) public: virtual BOOL InitInstance() { return TRUE; } }; IMPLEMENT_DYNCREATE(CMyThread, CWinThread) |
然后,随便找个地方来启动线程:
AfxBeginThread(RUNTIME_CLASS(CMyThread)); |
线程被创建后,就处于CWinThread::Run里的消息循环之中了。来看看CWinThread::Run的实现:
// main running routine until thread exits int CWinThread::Run() { ASSERT_VALID(this); _AFX_THREAD_STATE* pState = AfxGetThreadState(); // for tracking the idle time state BOOL bIdle = TRUE; LONG lIdleCount = 0; // acquire and dispatch messages until a WM_QUIT message is received. for (;;) { // phase1: check to see if we can do idle work while (bIdle && !::PeekMessage(&(pState->m_msgCur), NULL, NULL, NULL, PM_NOREMOVE)) { // call OnIdle while in bIdle state if (!OnIdle(lIdleCount++)) bIdle = FALSE; // assume "no idle" state } // phase2: pump messages while available do { // pump message, but quit on WM_QUIT if (!PumpMessage()) return ExitInstance(); // reset "no idle" state after pumping "normal" message //if (IsIdleMessage(&m_msgCur)) if (IsIdleMessage(&(pState->m_msgCur))) { bIdle = TRUE; lIdleCount = 0; } } while (::PeekMessage(&(pState->m_msgCur), NULL, NULL, NULL, PM_NOREMOVE)); } } |
粗粗看一下,是个夹杂了OnIdle概念的消息循环。
再看一下AfxBeginThread:
CWinThread* AFXAPI AfxBeginThread(CRuntimeClass* pThreadClass, int nPriority, UINT nStackSize, DWORD dwCreateFlags, LPSECURITY_ATTRIBUTES lpSecurityAttrs) { #ifndef_MT pThreadClass; nPriority; nStackSize; dwCreateFlags; lpSecurityAttrs; return NULL; #else ASSERT(pThreadClass != NULL); ASSERT(pThreadClass->IsDerivedFrom(RUNTIME_CLASS(CWinThread))); CWinThread* pThread = (CWinThread*)pThreadClass->CreateObject(); if (pThread == NULL) AfxThrowMemoryException(); ASSERT_VALID(pThread); pThread->m_pThreadParams = NULL; if (!pThread->CreateThread(dwCreateFlags|CREATE_SUSPENDED, nStackSize, lpSecurityAttrs)) { pThread->Delete(); return NULL; } VERIFY(pThread->SetThreadPriority(nPriority)); if (!(dwCreateFlags & CREATE_SUSPENDED)) { ENSURE(pThread->ResumeThread() != (DWORD)-1); } return pThread; #endif//!_MT } |
其中调用了CWinThread::CreateThread:
BOOL CWinThread::CreateThread(DWORD dwCreateFlags, UINT nStackSize, LPSECURITY_ATTRIBUTES lpSecurityAttrs) { #ifndef_MT dwCreateFlags; nStackSize; lpSecurityAttrs; return FALSE; #else ENSURE(m_hThread == NULL); // already created? // setup startup structure for thread initialization _AFX_THREAD_STARTUP startup; memset(&startup, 0, sizeof(startup)); startup.pThreadState = AfxGetThreadState(); startup.pThread = this; startup.hEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL); startup.hEvent2 = ::CreateEvent(NULL, TRUE, FALSE, NULL); startup.dwCreateFlags = dwCreateFlags; if (startup.hEvent == NULL || startup.hEvent2 == NULL) { TRACE(traceAppMsg, 0, "Warning: CreateEvent failed in CWinThread::CreateThread.\n"); if (startup.hEvent != NULL) ::CloseHandle(startup.hEvent); if (startup.hEvent2 != NULL) ::CloseHandle(startup.hEvent2); return FALSE; } // create the thread (it may or may not start to run) m_hThread = (HANDLE)(ULONG_PTR)_beginthreadex(lpSecurityAttrs, nStackSize, &_AfxThreadEntry, &startup, dwCreateFlags | CREATE_SUSPENDED, (UINT*)&m_nThreadID); if (m_hThread == NULL) { ::CloseHandle(startup.hEvent); ::CloseHandle(startup.hEvent2); return FALSE; } // start the thread just for MFC initialization VERIFY(ResumeThread() != (DWORD)-1); VERIFY(::WaitForSingleObject(startup.hEvent, INFINITE) == WAIT_OBJECT_0); ::CloseHandle(startup.hEvent); // if created suspended, suspend it until resume thread wakes it up if (dwCreateFlags & CREATE_SUSPENDED) VERIFY(::SuspendThread(m_hThread) != (DWORD)-1); // if error during startup, shut things down if (startup.bError) { VERIFY(::WaitForSingleObject(m_hThread, INFINITE) == WAIT_OBJECT_0); ::CloseHandle(m_hThread); m_hThread = NULL; ::CloseHandle(startup.hEvent2); return FALSE; } // allow thread to continue, once resumed (it may already be resumed) VERIFY(::SetEvent(startup.hEvent2)); return TRUE; #endif//!_MT } |
线程函数为_AfxThreadEntry:
UINT APIENTRY _AfxThreadEntry(void* pParam) { _AFX_THREAD_STARTUP* pStartup = (_AFX_THREAD_STARTUP*)pParam; ASSERT(pStartup != NULL); ASSERT(pStartup->pThreadState != NULL); ASSERT(pStartup->pThread != NULL); ASSERT(pStartup->hEvent != NULL); ASSERT(!pStartup->bError); CWinThread* pThread = pStartup->pThread; CWnd threadWnd; TRY { // inherit parent's module state _AFX_THREAD_STATE* pThreadState = AfxGetThreadState(); pThreadState->m_pModuleState = pStartup->pThreadState->m_pModuleState; // set current thread pointer for AfxGetThread AFX_MODULE_STATE* pModuleState = AfxGetModuleState(); pThread->m_pModuleState = pModuleState; AFX_MODULE_THREAD_STATE* pState = pModuleState->m_thread; pState->m_pCurrentWinThread = pThread; // forced initialization of the thread AfxInitThread(); // thread inherits app's main window if not already set CWinApp* pApp = AfxGetApp(); if (pApp != NULL && pThread->m_pMainWnd == NULL && pApp->m_pMainWnd->GetSafeHwnd() != NULL) { // just attach the HWND threadWnd.Attach(pApp->m_pMainWnd->m_hWnd); pThread->m_pMainWnd = &threadWnd; } } CATCH_ALL(e) { // Note: DELETE_EXCEPTION(e) not required. // exception happened during thread initialization!! TRACE(traceAppMsg, 0, "Warning: Error during thread initialization!\n"); // set error flag and allow the creating thread to notice the error threadWnd.Detach(); pStartup->bError = TRUE; VERIFY(::SetEvent(pStartup->hEvent)); AfxEndThread((UINT)-1, FALSE); ASSERT(FALSE); // unreachable } END_CATCH_ALL // pStartup is invlaid after the following // SetEvent (but hEvent2 is valid) HANDLE hEvent2 = pStartup->hEvent2; // allow the creating thread to return from CWinThread::CreateThread VERIFY(::SetEvent(pStartup->hEvent)); // wait for thread to be resumed VERIFY(::WaitForSingleObject(hEvent2, INFINITE) == WAIT_OBJECT_0); ::CloseHandle(hEvent2); // first -- check for simple worker thread DWORD nResult = 0; if (pThread->m_pfnThreadProc != NULL) { nResult = (*pThread->m_pfnThreadProc)(pThread->m_pThreadParams); ASSERT_VALID(pThread); } // else -- check for thread with message loop else if (!pThread->InitInstance()) { ASSERT_VALID(pThread); nResult = pThread->ExitInstance(); } else { // will stop after PostQuitMessage called ASSERT_VALID(pThread); nResult = pThread->Run(); } // cleanup and shutdown the thread threadWnd.Detach(); AfxEndThread(nResult); return 0; // not reached } |
林林总总地贴了这么些代码,差不多可以看出MFC的CWinThread的一些实现机制了。总的来说,MFC提供的“UI线程”,默认为线程实现了一个带OnIdle机制的消息循环,同时,它Attach了应用程序主窗口,m_pMainWindow被设为了应用程序主窗口,它在OnIdle以及ProcessMessageFilter中被用到。
注意到在_AfxThreadEntry中有一行AfxInitThread,这里面注册了一个消息钩子,钩子回调函数里面会调用ProcessMessageFilter。当处于帮助模式的时候,这个函数会向m_pMainWindow发送code为ID_HELP的WM_COMMAND消息。
MFC中的工作线程
工作线程由另一个AfxBeginThread启动:
CWinThread* AFXAPI AfxBeginThread(AFX_THREADPROC pfnThreadProc, LPVOID pParam, int nPriority, UINT nStackSize, DWORD dwCreateFlags, LPSECURITY_ATTRIBUTES lpSecurityAttrs) { #ifndef_MT pfnThreadProc; pParam; nPriority; nStackSize; dwCreateFlags; lpSecurityAttrs; return NULL; #else ASSERT(pfnThreadProc != NULL); CWinThread* pThread = DEBUG_NEW CWinThread(pfnThreadProc, pParam); ASSERT_VALID(pThread); if (!pThread->CreateThread(dwCreateFlags|CREATE_SUSPENDED, nStackSize, lpSecurityAttrs)) { pThread->Delete(); return NULL; } VERIFY(pThread->SetThreadPriority(nPriority)); if (!(dwCreateFlags & CREATE_SUSPENDED)) VERIFY(pThread->ResumeThread() != (DWORD)-1); return pThread; #endif//!_MT) } |
它调用了CWinThread的如下构造函数:
CWinThread::CWinThread(AFX_THREADPROC pfnThreadProc, LPVOID pParam) { m_pfnThreadProc = pfnThreadProc; m_pThreadParams = pParam; CommonConstruct(); } |
然后同样用CWinThread::CreateThread创建线程。新线程的入口函数同样为_AfxThreadEntry。与上例不同,这时,程序进入这个if判断的第一个分支:
// first -- check for simple worker thread DWORD nResult = 0; if (pThread->m_pfnThreadProc != NULL) { nResult = (*pThread->m_pfnThreadProc)(pThread->m_pThreadParams); ASSERT_VALID(pThread); } // else -- check for thread with message loop else if (!pThread->InitInstance()) { ASSERT_VALID(pThread); nResult = pThread->ExitInstance(); } else { // will stop after PostQuitMessage called ASSERT_VALID(pThread); nResult = pThread->Run(); } |
直接调用我们传入的线程函数,而不再进入CWinThread::Run。这里,m_pMainWindow的处理与上例相同。
MFC中的UI线程与工作线程的异同
综上,我们可以看到,MFC里的UI线程里,CWinThread实现了一个消息循环,这是工作线程所不具备的。除此之外,差异之处很寥寥。从MFC代码里来看,MFC的开发者对两者的称呼只是“simple worker thread”和“thread with message loop”,事实上两者的代码层面的区别也正是如此。并且,CWinThread::Run被声明为虚的,这意味着我们可以覆盖它——同时在自己的版本里不实现消息循环。
而MSDN里,将两个_AfxBeginThread的使用分别称为创建“User Interface Thread”和创建“Worker Thread”。
尝试定义“UI线程”
现在开始,我们走出MFC,回到通用程序领域。看看“UI线程”是否有必要定义,以及应该如何定义。
首先有一点要明白,在MFC之外,UI线程的官方概念已经不存在了。这时,你去问一个人“你知道什么是UI线程吗?”是很奇怪很愚蠢的事情。如果他说不知道,你会怎么做?你大概会告诉他你心中的定义,这表明你试图让他相信你心中的定义是真理(业界通用说法),并且不指定适用范围(比如MFC内)。这是不道德的。
就像有一次,一位同事“嘲讽”我说:“Cookies是进程内全局共享的,你不知道吗?”我当然不知道呢,Cookies不是HTTP协议里一行文本而已吗?我愿意怎么处理就怎么处理嘛,愿意让它在进程内全局共享它就是进程内全局共享的了,我不愿意让它在进程内全局共享它就不是进程内全局共享的了,不是吗?后来,才知道他说的是“在WinINet中,Cookies是进程内全局共享的”。
既然我们使用了WinINet,那么有时候省略“在WinINet中”的限定或许是情有可原的。但我们如果没有使用MFC,那么不带前提地大谈“UI线程”就显得不太合适了。稍稍总结下,我们谈到“UI线程”一般是这些场景:
- 1. 不要在UI线程中做长时间的操作
- 2. 只能在UI线程中操作HWND
- 3. 我们搞个双UI线程
第三点暂时无视吧,个人觉得无意义,什么单UI线程、双UI线程,这个在系统层面根本没这个提法以及限制,完全取决于开发者。倒是前两者,是有那么一点意义的提法。为了描述方便,在本节中,暂且定义“UI线程”为,具有消息循环,并且在其中至少创建了一个可见窗口的线程。(这里可能有人会问,没有窗口,你需要消息循环干嘛?一个例子:带TimerProc的Timer需要消息循环。)
对于第一条,其实我并无多大异议,只要不去拷问别人什么叫“UI线程”,这样轻描淡写的提及,大家总是心知肚明的。第二条是创造“UI线程”概念的一个的很大的使用场景,然而这个命题本身却是错误的。起码,SendMessage和PostMessage是无论在哪个线程执行都是可行的,那么,那一大堆由GUI库包装SendMessage而成的窗口操作函数自然也是随处可用的,以及本质上由SendMessage实现的一些窗口API也是随处可用的。为了说清楚此问题,好像要总结一下哪些API需要在UI线程中执行,以及哪些不需要——但这是不可能的,下一节我略举几个例子。
本节结束之前,我想将上述1、2改个提法,以避免提及“UI线程”:
1. 不要在窗口回调函数中做长时间的操作
2. 只能在创建HWND的线程中操作HWND
只能在创建HWND的线程中操作HWND?
正例:
l DestroyWindow MSDN Remark 中特意指明:A thread cannot use DestroyWindow to destroy a window created by a different thread.
反例:
l SendMessage、PostMessage 这两个当然不必在创建窗口的线程使用了。
l ShowWindowAsnyc这个函数的用途是“Sets the show state of a window created by a different thread”,虽然文档中没特异指出可以在非创建线程中使用,但简单脑补一下就知道可以。注意,这并不意味着ShowWindow一定需要在创建窗口的线程中使用。ShowWindowAsync做的是“posts a show-window event to the message queue of the given window”,它的意义在于“to avoid becoming nonresponsive while waiting for a nonresponsive application to finish processing a show-window event”。
l GetWindowThreadProcessId用途:“Retrieves the identifier of the thread that created the specified window and, optionally, the identifier of the process that created the window”。同上,脑补。
精力有限,例子就不再举了(各位可以帮忙补充)。按笔者个人理解,文档中没有特意指出一定要在创建窗口的线程中使用的,一般是可以在其他线程甚至其他进程使用的。再来看一下SetWindowPos的一段Remark:
As part of the Vista re-architecture, all services were moved off the interactive desktop into Session 0. hwnd and window manager operations are only effective inside a session and cross-session attempts to manipulate the hwnd will fail.
这里它特别指出Vista之后,跨Session操作HWND会失败。这从侧面表明,跨线程玩一下HWND,或者跨进程玩一下,通常是不会失败的。如果失败了,那才是特例,就像DestroyWindow。
通则、局部规则、家长式规则
从上面的反例,我们可以知道,“只能在创建HWND的线程中操作HWND”这一命题是不成立的。它肯定不是Windows开发领域的通则。那么,这句话从何而来呢?确切地说,我不知道。
第一,它可能是某个GUI库的规则。可能因为这个GUI库的设计问题,导致必须在创建HWND的线程中操作HWND。这句话或许因此成为某些GUI库的局部规则。但可能因为我们在交流的时候,有意无意地忽略了前提条件(“在MFC中”、“在WinINet中”),导致被误解为通则。
第二,有可能有些前辈高人对于后辈跨线程操作HWND导致的一些问题感到厌倦,于是就对他们谆谆教诲:“孩子啊,只能在创建HWND的线程中操作HWND的。”然后世事变幻,沧海桑田,几代以后,这句为防止不太会的人用错的家长式规则被口口相传当成了通则。
顺便说一句,刚才的第一条不是说不要在“UI线程”中做长时间操作么?那么,要提高窗口响应速度,在“UI线程”中做的事自然是越少越好。如果某些API是可以跨线程使用的,在别的线程把该算的全算好,该IO的全做好,最后直接操作HWND,是最理想的状况。而不是在操作HWND的前面一段时间就转入“UI线程”。
API:IsGUIThread
非常有意思的事情,当笔者快写完上面的文字的时候,却发现了“IsGUIThread”这个函数。对此,本文当然有必要把这个函数中的GUI线程的概念考究清楚了。
调查发现,GUI线程是Windows内核中的概念。笔者对此并不无实际开发体验,且摘录一段查到的文字:
普通的Win32线程有两个栈:一个是用户栈,另一个是内核栈;而如果是内核中创建的系统工作线程,则只有内核栈。只要代码在内核中运行,线程就一定是使用其内核栈的。栈的主要作用是维护函数调用帧,以及为局部变量提供空间。
用户栈可以指定其大小,默认是1MB,通过编译指令/stack可改设其他值。
普通内核栈的大小是固定的,由系统根据CPU架构而定,x86系统上为12KB,x64系统上为24KB,安腾系统上为32KB。对于GUI线程,普通内核栈空间可能不够,所以系统又定义了“大内核栈”概念,可以在需要的时候增长栈空间。只有GUI线程才能使用大内核栈,这也是系统规定的。
关于GUI线程,笔者多说几句。Windows的发明,将GDI和USER模块,即“窗口与图形模块”的实现移到了内核中,称为Windows子系统内核服务,并形成一个win32k.sys内核文件。而用户层仅留调用接口,由User32.dll和GDI32.dll两个文件暴露出来。判断一个线程是不是GUI线程的依据,竟非常的简单:线程初建时,都是普通线程,第一次调用Windows子系统内核服务(只要用户程序调用了User32.dll和GDI32.dll中的函数,并导致相关内核服务在内核中被执行),系统即立刻将之转变为GUI线程,并从而切换到“大内核栈”;倘若至线程结束,并未有任何一个子系统内核服务被调用,那么它一直都是普通线程,一直使用普通内核栈。
Windows内核中的内核栈(摘自《竹林蹊径:深入浅出Windows驱动开发》)
http://yvqvan.blog.163.com/blog/static/254151032011321113127651/
从这段文字看,Windows内核的GUI线程概念和我们刚才所谈的“UI线程”完全是两个概念。因此,除了“UI线程不是官方概念”有待商榷以外,上文仍然成立。当然,官方概念叫“GUI线程”,还是说准确点为好。
总结
好了,下面简单概括一下我要表达的观点:
l 向无知者兜售自己创造的概念并试图让他奉为真理,是不厚道的。
l MFC的文档中确实有“UI线程”的提法,它与工作线程分别由两个不同版本的_AfxBeginThread创建,主要区别是“UI线程”具备一个消息循环。不过我们可以覆盖CWinThread::Run使得这个区别不存在。
l Win32用户态中,并不存在官方的“UI线程”概念。
l 并不是说所有操作HWND的函数都必须在创建HWND的线程中使用,事实上可能正好相反,或许只有少数函数有此限制。
l Window内核中确实有GUI线程和工作线程的区分,但这与我们之前所要表达的“UI线程”并不是一个意思。
以上,恳请各位批评指正。如果你看到了这里,那非常感谢你能看完。:)
关于“UI线程”的更多相关文章
- 在非UI线程中自制Dispatcher
在C#中,Task.Run当然是一个很好的启动新并行任务的机制,但是因为使用这个方法时,每次新的任务都会在一个新的线程中(其实就是线程池中的线程)运行 这样会造成某些情形下现场调度的相对困难,即使我隔 ...
- C# 多线程详解 Part.02(UI 线程和子线程的互动、ProgressBar 的异步调用)
我们先来看一段运行时会抛出 InvalidOperationException 异常的代码段: private void btnThreadA_Click(object sender, ...
- UI线程和work线程
UI线程处理消息,比如重绘,响应键盘鼠标等等跟消息有关的处理. 而工作线程一般是你自己写的用来处理数据的,比如操作数据库等等.(work线程)
- jQuery同步Ajax带来的UI线程阻塞问题及解决办法
俗话说不作死就不会死,今天作死了一回,写了一个比较二逼的函数,遇到了同步Ajax引起的UI线程阻塞问题,在此记录一下. 事情起因是这样的,因为页面上有多个相似的异步请求动作,本着提高代码可重用性的原则 ...
- UWP开发入门(二十一)——保持Ui线程处于响应状态
GUI的程序有时候会因为等待一个耗时操作完成,导致界面卡死.本篇我们就UWP开发中可能遇到的情况,来讨论如何优化处理. 假设当前存在点击按钮跳转页面的操作,通过按钮打开的新页面,在初始化过程中存在一些 ...
- Android UI线程和非UI线程
Android UI线程和非UI线程 UI线程及Android的单线程模型原则 当应用启动,系统会创建一个主线程(main thread). 这个主线程负责向UI组件分发事件(包括绘制事件),也是在这 ...
- 学习通过Thread+Handler实现非UI线程更新UI组件
[Android线程机制] 出于性能考虑,Android的UI操作并不是线程安全的,这就意味着如果有多个线程并发操作UI组件,可能导致线程安全问题.为了解决这个问题,Android制定了一条简单的规则 ...
- C# 多线程详解 Part.01(UI 线程、子线程)
基本概念 什么是进程? 当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源.一个进程至少有一个主线程. 什么是线程? 线程是程序中的一 ...
- 网络请求怎么样和UI线程交互? Activity2怎么通知Activity1 更新数据
1.网络请求怎么样和UI线程交互? 目前我的做法是,建立线程池管理网络请求线程,通过添加task来新增网络请求.所有的网络操作通过统一的request来实现,网络返回结果通过回调onError和onS ...
- [原] Android performClick无效,UI线程理解
原因 开发过程中遇到button.performClick()无效,原因是View.performClick()需要再UI线程中调用才会有效执行. 响应系统调用的方法(比如报告用户动作的onKeyDo ...
随机推荐
- jmeter之ip欺骗
说明:我看有的博客说官方文档是在jmeter2.5以上的版本有此功能的实现~ 我的是2.13版本,也可以实现 . 准备工作: 使用IP欺骗功能必须得本地有多个可用IP,通常普通的PC机只有一个物理网卡 ...
- Oracle数据库错误大全
ORA-00001: 违反唯一约束条件 (.)ORA-00017: 请求会话以设置跟踪事件ORA-00018: 超出最大会话数ORA-00019: 超出最大会话许可数ORA-00020: 超出最大进程 ...
- 淘淘商城 本地仓库配置和仓库jar包下载
SVN服务器的搭建请查看该文:<Win7 x64 svn 服务器搭建> 1:仓库包存放位置: 2:setting.xml 文件配置信息 <?xml version="1.0 ...
- css3图片旋转
<!DOCTYPE html> <html lang="en"> <head> <meta http-equiv="Conten ...
- noip 2017 时间复杂度
自认为是少有的复杂的代码 这题思想很简单,就是大模拟 对于for循环,一行读入4个字符串,然后分类讨论: ①:如果是一个正常的O(n),那么累计n的指数加1 ②:如果是一个常数级别的,那么继续循环,但 ...
- pytest十五:pytest-html 生成 html 报告
pytest-HTML 是一个插件,pytest 用于生成测试结果的 HTML 报告.兼容 Python 2.7,3.6 pytest-html1.github 上源码地址[https://githu ...
- python+selenium三:鼠标事件与键盘事件
1.鼠标事件:# 每个模拟事件后需加.perform() 才会执行# context_click() 右击# double_click() 双击# drag_and_drop(source, targ ...
- 微信公众号开发JS-SDK(1.2)
概述 微信js-SDK是微信公众平台面向网页开发者提供的基于微信内的网页开发工具包. 通过使用微信JS-SDK,网页开发者可借助微信高效地使用拍照.选图.语音.位置等手机系统的能力,同时可以直接使用微 ...
- boto3--通过Python的SDK连接aws
通过Python的SDK连接aws 参考: https://aws.amazon.com/cn/developers/getting-started/python/ aws上生成访问密钥 ID 和私有 ...
- JAVA代码设置selector不同状态下的背景
Selector设置button点击效果(详细)以及常见问题https://www.jianshu.com/p/a0ddba6d7969 Android 代码动态设置TextView的背景.颜色Sel ...