【MFC】探索程序启动机制的实现原理
# 全局对象的构造
C++ 的对象在创建时,会调用构造函数。
而全局对象的构造时机,
这里以上节编写的示例代码为例,我们在代码中实例化了一个全局对象 g_theApp ,基于 VS 强大的源码调试能力,我们来对 MFC 程序的启动机制一探究竟。
示例
1 |
|
# g_theApp 构造调试
- 让光标停留在
CMyWinApp g_theApp;行,按下F9,设置断点 -
F5运行,让程序中断到当前行F11单步步入,进入到CMyWinApp的构造函数中- 继续单步步入,进入到基类
CWinApp的构造函数中
到这里,我们就开始调试到 MFC 的源码了。
# g_theApp 构造分析
接下来我们选择部分代码进行讲解
CWinApp::CWinApp 部分代码一
1 | CWinApp::CWinApp(LPCTSTR lpszAppName) |
MFC 类库中有一个描述主模块状态的全局对象, _AFX_CMDTARGET_GETSTATE 宏函数就是用于获取该全局对象的地址
以及描述主模块线程状态的全局对象,其地址保存在主模块状态中的成员中。
MFC 在设计时想必已经安排好了构造顺序已确保程序运行的正确性,此处我们的全局对象 g_theApp 的父类部分 CWinApp 在构造时才能够正确使用这些全局对象,这里不再做深究。
将我们创建的 g_theApp 的地址保存到主模块线程状态的 m_pCurrentWinThread 成员中。
CWinApp::CWinApp 部分代码二
1 | // initialize CWinApp state |
将我们创建的 g_theApp 的地址保存到主模块状态的 m_pCurrentWinApp 成员中。
# WinMain 的启动流程
回忆我们的 CMyWinApp 类,在类中我们重写了虚函数 InitInstance 。
见名知意,我们猜测,这是一个初始化函数,但是我们不清楚函数是何时、如何被调用,因此我们需要继续分析 MFC 的源码。
# InitInstance 回调调试
- 在
CMyFrameWnd* frame = new CMyFrameWnd;行设置断点 - 运行程序,中断在此行
- 查看
调用堆栈,我们可以看到InitInstance的调用函数,以及调用函数的调用函数… 等层级关系。- 在这里我们也看到了我们熟悉的
WinMain函数,说明InitInstance是在WinMain执行过程中被调用的。 - 而我们并没有实现
WinMain,那么WinMain自然也是由 MFC 实现的,至此,我们先前的一个疑惑也解决了。
- 在这里我们也看到了我们熟悉的
- 从
调用堆栈中定位到WinMain函数,在此处设置断点,重新运行程序。 -
WinMain函数中仅有一行代码,即调用AfxWinMain并返回,单步进入
# AfxWinMain 源码分析
我们依旧选择我们感兴趣的源码进行讲解
AfxWinMain 是 MFC 实现的全局函数。
以 Afx 开头的函数,基本上都是 MFC 实现的全局函数。
AfxWinMain 部分代码
1 | int AFXAPI AfxWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, |
此函数获取主模块线程状态的 m_pCurrentWinThread 成员。
先前我们在构造 g_theApp 的过程中已经看到,主模块线程状态的 m_pCurrentWinThread 成员保存的是 g_theApp 的地址;
因此,此函数实际上是获取 g_theApp 的地址,也就是说,在 MFC 的启动流程中,可能要使用我们创建的全局对象 g_theApp 。
此函数获取主模块状态的 m_pCurrentWinApp 成员。
先前我们在构造 g_theApp 的过程中已经看到,主模块状态的 m_pCurrentWinApp 成员保存的是 g_theApp 的地址;
与 AfxGetThread 作用类似。
CWinApp* 指向 CMyWinApp 类型的对象,我们是能够理解的;
但是为什么 CWinThread* 的赋值也能被允许呢?因为 CWinApp 类就继承自 CWinThread 类。
当前行实际上调用了 g_theApp 的 InitApplication 函数,我们并没有提供此函数,因此只可能是 g_theApp 的父类部分提供的。
这个函数实际上也是虚函数,我们能够重写它,一般在我们希望做一些应用程序初始化的工作时重写。
终于又到了我们熟悉的部分了, InitInstance 就是我们重写的函数,此时 pThread 指向 g_theApp ,这就是 多态 了。
CMyWinApp::InitInstance
1 | virtual BOOL InitInstance() { |
我们暂时略过具体代码,最终是通过 return TRUE 返回的;
根据 AfxWinMain 中的代码的逻辑,我们会走到 Run 这个函数。
# 不可或缺的消息循环
在直接使用 Win32API 开发界面程序时,我们都会编写消息循环以阻塞主线程,避免 WinMain 返回后终止进程。
MFC 程序自然也不例外,而 MFC 的消息循环究竟编写在哪里呢?
其实读者只要在调试时步过 nReturnCode = pThread->Run(); ,就会使得程序直接运行起来,不再处于中断状态,因而得知, Run 成员函数封装了消息循环。
# CWinApp::Run 的源码分析
CWinApp::Run
1 | int CWinApp::Run() |
我们忽略对 AfxOleGetUserCtrl 函数的调用, m_pMainWnd 是不是有些眼熟?
我们在重写 InitInstance 时,使 m_pMainWnd 指向了 new 出来的 CMyFrameWnd 对象。
而我们在调用 Run 成员函数时,就是以 g_theApp 的身份进行调用的。
因此,在 Run 成员函数中访问 m_pMainWnd ,自然得到我们当时 new 出来的 CMyFrameWnd 对象。
接下来我们调用 CWinApp 的父类 CWinThread 的 Run 成员函数
CWinThread::Run 第一部分
1 | int CWinThread::Run() |
代码并不复杂,这里直接在代码中注释,可以自行阅读。
在 MFC 中程序中调用 Win32API 时,通常都会指明调用的是全局作用域下的函数 :: 。
CWinThread::Run 第二部分
1 | ...... |
if (!PumpMessage())
MFC 在此函数中封装了对 GetMessage 、 TranslateMessage 、 DispatchMessage 函数的调用,读者感兴趣可以自行跟进,这里就不再分析了。
一旦 PumpMessage 返回 FALSE ,就会调用虚成员函数 ExitInstance 并退出消息循环。
而 PumpMessage 返回 FALSE 的条件即是 GetMessage 获得 WM_QUIT 消息,程序结束。
我们也可以重写 ExitInstance ,在程序结束前做必要的资源释放。
从 Run 成员函数返回后,也会一路返回到 WinMain ,程序也就退出了。
# 基本流程
-
首先,我们在编写 MFC 应用时,需要实例化一个类型为
CWinApp的全局对象。- 如果需要重写初始化等成员虚函数,则需要创建继承自
CWinApp的子类的对象 (此处命名为g_theApp)。
- 如果需要重写初始化等成员虚函数,则需要创建继承自
-
在
g_theApp被构造时,会使 MFC 定义的全局变量主模块状态和主模块线程状态的成员指向g_theApp地址。 -
程序进入
WinMain函数,会通过全局变量主模块状态和主模块线程状态得到g_theApp地址,再以g_theApp的身份调用必要的成员虚函数。- 初始化
- 消息循环
- 退出
至此,我们基本上对 MFC 程序的启动流程有了一个大致的认知。
而窗口的创建、消息的接收处理等部分,我们留到下一篇再叙。