先说明一下,我这里采用的chrome源代码版本是4.1.249.1064。如果你采用的不是此版本,则可能和我描述的源代码文件名、代码位置不一致,后续关于chrome的文章均采用此版本,不再另作说明。采用此版本没有任何特殊理由,仅仅是当我开始学习chrome的那个时间点的最新版本而已。
另外虽然chrome的版本升级非常快,但其核心体系架构是没有变化的。升级的变更的内容主要体现在下面几个方面:
1.安全性、稳定性、速度提升
2.新功能点增加
3.HTML5相关内容
4.整合集成google自己的产品服务
因此从学习角度看,如果不是特别需要对某些新增加的特性感兴趣,如果重点是想学习其基础体系结构和web核心相关模块,那么选择一个较早的版本来学习反而更好。因为这样代码量少,重点突出,干扰少些。当掌握好基本体系结构后,后续的版本自然就很容易上手了。
下面简要说明chrome的启动流程,这里重点不会分析代码细节,而是快速定位几个比较关键的位置,可以在这些位置设置断点,然后你可以查看调用栈以了解具体的调用流程。这样可以先初步进入开始单步跟踪。
为了便于跟踪说明,请在浏览器选项菜单中将启动的主页地址设置为一个具体地址比如http://www.baidu.com/。
注意:因为chrome是多进程结构,所以在调试的时候需要附加到对应的进程才能看到断点效果。如果不想那么麻烦,也可以以单进程模式启动调试,具体是在chrome exe的工程属性中加命令行参数 --single-process。我将以多进程模式为例来讲解。
EXE启动入口
应用程序执行文件的WinMain入口函数在chrome exe工程的chrome_exe_main.cc文件中,在一些初始化代码后,加载
chrome.dll内核文件,代码如下:
...MainDllLoader* loader = MakeMainDllLoader();int rc = loader->Launch(instance, &sandbox_info);
...
MainDllLoader::Launch的实现在client_util.cc中,其中有代码(…为我忽略掉的代码):
int MainDllLoader::Launch(HINSTANCE instance,
sandbox::SandboxInterfaceInfo* sbox_info) {...dll_ = Load(&version, &file);...DLL_MAIN entry_point =reinterpret_cast<DLL_MAIN>(::GetProcAddress(dll_, "ChromeMain"));...int rc = entry_point(instance, sbox_info, ::GetCommandLineW());
return OnBeforeExit(rc);
}
通过调用Win32 API GetProcAddress来动态获取chrome.dll中的入口函数“ChromeMain”的地址然后调用进入。
DLL入口函数
chrome.dll的入口函数是ChromeMain,其工程名为chrome_dll。
ChromeMain的实现在chrome_dll_main.cc中:
DLLEXPORT int __cdecl ChromeMain(HINSTANCE instance,
sandbox::SandboxInterfaceInfo* sandbox_info,TCHAR* command_line){...int rv = -1;
if (process_type == switches::kRendererProcess) {
rv = RendererMain(main_params);} else if (process_type == switches::kExtensionProcess) {// An extension process is just a renderer process. We use a different
// command line argument to differentiate crash reports.
rv = RendererMain(main_params);} else if (process_type == switches::kPluginProcess) {rv = PluginMain(main_params);} else if (process_type == switches::kUtilityProcess) {rv = UtilityMain(main_params);} else if (process_type == switches::kProfileImportProcess) {...} else if (process_type == switches::kWorkerProcess) {rv = WorkerMain(main_params);...} else if (process_type == switches::kZygoteProcess) {...} else if (process_type.empty()) {...ScopedOleInitializer ole_initializer;rv = BrowserMain(main_params);} else {
NOTREACHED() << "Unknown process type";
}...}
我摘录了上面需要重点说明的一段代码,在ChromeMain中,前面进行一些初始化代码后,上述通过进程类型来判断是进入何种类型的进程入口。从上面的代码我们看到了Render进程入口RendererMain,插件进程入口PluginMain,浏览器主进程入口BrowserMain等。对于我们分析chrome主体结构最重要就是BrowserMain和RendererMain。当我们打开浏览器,这里显然进入到浏览器主进程BroserMain入口。可见chrome所有不同类型的进程启动入口都通过ChromeMain来统一处理。具体入口函数是通过命令行参数传递进程类型参数来决定。
注:renderer进程的启动是在Browser主进程代码中动态启动chrome.exe,以传入命令行参数使其进程类型参数为render,然后进入RenderderMain入口从而启动Render内核进程与主进程协作通信。
Browser主进程入口
BrowserMain入口的实现在browser工程的browser_main.cc中:
int BrowserMain(const MainFunctionParams& parameters) {...MessageLoop main_message_loop(MessageLoop::TYPE_UI);...scoped_ptr<BrowserProcessImpl> browser_process;if (parsed_command_line.HasSwitch(switches::kImport)) {
...} else {
browser_process.reset(new BrowserProcessImpl(parsed_command_line));
}...BrowserInit browser_init;browser_process->db_thread();browser_process->file_thread();browser_process->process_launcher_thread();browser_process->io_thread();int result_code = ResultCodes::NORMAL_EXIT;
if (parameters.ui_task) {
...} else {
// We are in regular browser boot sequence. Open initial stabs and enter
// the main message loop.
if (browser_init.Start(parsed_command_line, std::wstring(), profile,
&result_code)) {...RunUIMessageLoop(browser_process.get());}}...browser_process.release();browser_shutdown::Shutdown();return result_code;
}
上面是经过我简化后的代码,其中主要完成了:
1.生成UI类型的消息循环对象main_message_loop
2.生成browser进程对象browser_process,并初始化(启动)文件、数据库、io等辅助线程。
3.调用browser_init.Start启动浏览器主UI界面,该句执行后,浏览器窗口就显示出来了。
4.调用RunUIMessageLoop进入浏览器UI主线程消息循环,后续工作完全基于消息驱动。
RunUIMessageLoop的实现仅仅是调用MessageLoopForUI::current()->Run()进入消息循环而已。
浏览器窗口启动过程
浏览器窗口的显示过程在browser_init.Start中完成。具体我们再看一下这个函数的调用过程,该函数最终会运行到
browser_init.cc文件中的BrowserInit::LaunchWithProfile::OpenURLsInBrowser(),整理如下:
Browser* BrowserInit::LaunchWithProfile::OpenURLsInBrowser(Browser* browser,bool process_startup,
const std::vector<GURL>& urls)
{...if (!browser || browser->type() != Browser::TYPE_NORMAL)
browser = Browser::Create(profile_);for (size_t i = 0; i < urls.size(); ++i) {
if (!process_startup && !URLRequest::IsHandledURL(urls[i]))
continue;
TabContents* tab = browser->AddTabWithURL(urls[i], GURL(), PageTransition::START_PAGE, (i == 0), -1, false, NULL);
if (i < static_cast<size_t>(pin_count))browser->tabstrip_model()->SetTabPinned(browser->tab_count() - 1, true);
if (profile_ && i == 0 && process_startup)
AddCrashedInfoBarIfNecessary(tab);}browser->window()->Show();...return browser;
}
该函数会创建一个Browser对象,每一个浏览器窗口都对应一个Browser对象。然后调用browser对象的AddTabWithURL
方法来打开相应的URL,我们的主页地址也将在AddTabWithURL中被打开。最后调用browser->window()->Show()
显示浏览器UI窗口,对应的url页面也将被显示。
Renderer进程的启动
作为多进程架构,所谓Renderer进程就是内核的渲染进程,每个页面的显示都有Renderer进程中相应的内核对象与之对应。
Renderer进程主要包含了Webkit和V8两大内核对象。
Renderer进程的启动过程是随着URL的打开而启动,即browser->AddTabWithURL调用会触发Renderer进程的启动,具体在browser_render_process_host.cc中的Init方法:
bool BrowserRenderProcessHost::Init(bool is_extensions_process) {...base::Thread* io_thread = g_browser_process->io_thread();scoped_refptr<ResourceMessageFilter> resource_message_filter =new ResourceMessageFilter(g_browser_process->resource_dispatcher_host(),
id(),audio_renderer_host_.get(),PluginService::GetInstance(),g_browser_process->print_job_manager(),profile(),widget_helper_,profile()->GetSpellChecker());const std::string channel_id =ChildProcessInfo::GenerateRandomChannelID(this);
channel_.reset(new IPC::SyncChannel(channel_id, IPC::Channel::MODE_SERVER, this,resource_message_filter,io_thread->message_loop(), true,
g_browser_process->shutdown_event()));if (run_renderer_in_process()) {
...} else {
...child_process_.reset(new ChildProcessLauncher(
#if defined(OS_WIN)
FilePath(),#elif defined(POSIX)base::environment_vector(),channel_->GetClientFileDescriptor(),#endifcmd_line,this));
fast_shutdown_started_ = false;
}return true;}
browser_render_process_host.cc文件是理解体系结构很重要的一个文件,其中的Init方法是关键。可以在该方法中
设置断点,通过查看调用栈可以了解详细的调用路径。在Init中,先构造一个资源消息过滤器(ResourceMessageFilter)
然后构造和Renderer进程通信的IPC通道,并将资源消息过滤器安装在该通道上。最后通过构造一个ChildProcessLauncher
对象来启动Renderer进程,注意ChildProcessLauncher构造函数传递了执行文件路径,命令行等参数。
下面看ChildProcessLauncher构造函数的实现,构造函数最终会调用到child_process_launcher.cc中的
Context嵌套类的Launch函数:
void Launch(
#if defined(OS_WIN)
const FilePath& exposed_dir,
#elif defined(OS_POSIX)const base::environment_vector& environ,
int ipcfd,
#endifCommandLine* cmd_line,Client* client) {client_ = client;CHECK(ChromeThread::GetCurrentThreadIdentifier(&client_thread_id_));ChromeThread::PostTask(ChromeThread::PROCESS_LAUNCHER, FROM_HERE,NewRunnableMethod(this,
&Context::LaunchInternal,#if defined(OS_WIN)
exposed_dir,#elif defined(POSIX)environ,ipcfd,#endifcmd_line));}
这里的关键是ChromeThread::PostTask()的调用。该函数是chrome中线程调度体系的一个重要助手函数。当一个线程需要向另外一个线程投递一个任务时(在另外一个线程上运行指定的代码)调用该函数很方便,第一个参数是目标线程的标识符(chrome的每种线程都有对应的标识符,通过标识符就可以获取到线程对象)。第三个参数是需要在目标线程上运行的代码。chrome提供了很多灵活的模板方法(可以参考base项目中的task.h实现)来构造第三个参数,第三个参数即可以是一个独立的函数入口,又可以是某个类对象的公有方法,且该方法可以有任意的参数数目和数据类型。
在这里,主UI线程把启动Render进程的任务投递给独立的“进程启动者线程”(PROCESS_LAUNCHER)来完成,具体的代码是调用Context::LaunchInternal()方法:
void LaunchInternal(
const FilePath& exposed_dir,
CommandLine* cmd_line){...scoped_ptr<CommandLine> cmd_line_deleter(cmd_line);base::ProcessHandle handle = base::kNullProcessHandle;handle = sandbox::StartProcessWithAccess(cmd_line, exposed_dir);...}
这里通过调用sandbox::StartProcessWithAccess() Renderer子进程就启动起来了。注意上述代码不是运行在主UI线程中,而是运行在前面已经初始化的“进程启动器线程”中。在前面的BrowserMain中有语句:
browser_process->process_launcher_thread(); 即是启动了“进程启动器线程”。
Renderer进程入口
renderer进程的入口如前所述,统一在dll的ChromeMain函数中处理,根据类型是renderer而进入RendererMain函数。
该函数在Renderer工程的render_main.cc中实现。由于采用多进程模式,Render是作为子进程方式以命令行参数方式启动,要跟踪Renderer进程就需要附加到Renderer进程,要附加进程则必须先启动进程,因此对RendererMain的顶点入口我们无法设定断点跟踪进去(进程启动后,RenderMain已经执行)。如果要单步跟踪每个详细步骤,则可以考虑用单进程模式启动。多于多进程模式,虽然无法在RenderMain设点断点,但好在该函数并不复杂,主要也是一个消息循环而已。
所以为了理解Renderer内部的初始化过程,我们需要在Renderer工程的render_view.cc这个关键文件中设定断点来查看调用栈。注意:为了及早跟踪必须尽早附加到启动后的Render进程,所以最早的时机是执行了上面LaunchInternal函数中的语句handle = sandbox::StartProcessWithAccess(cmd_line, exposed_dir); 该语句执行后Render进程就生成了,此时通过vs2008 ”Tools“菜单中的“Attach to Process”来附加到Render进程(chrome.exe)。
附加后,我们在RenderView::Init中设置断点:
void RenderView::Init(gfx::NativeViewId parent_hwnd,
int32 opener_id,
const RendererPreferences& renderer_prefs,
SharedRenderViewCounter* counter,int32 routing_id) {
...webwidget_ = WebView::create(this);
Singleton<ViewMap>::get()->insert(std::make_pair(webview(), this));
webkit_preferences_.Apply(webview());webview()->initializeMainFrame(this);
...OnSetRendererPrefs(renderer_prefs);routing_id_ = routing_id;render_thread_->AddRoute(routing_id_, this);
...}
通过调用栈可以看到该函数的调用路径,从调用路径中可以看出该初始化过程是由一个IPC消息触发,消息通过IPC通道最终被分派到void ChildThread::OnMessageReceived(…),通过消息映射,该消息ViewMsg_New由相应的函数来处理,并最终调用到RenderView::Init完成内核初始化。在RenderView::Init中,webview()->initializeMainFrame(this)此句完成了webkit内核的初始化。 以后的过程就是主进程和Renderer进程通过IPC通道进行消息交互的协作过程。
ViewMsg_New消息是浏览器主进程在打开新页面的时候发送给Renderer进程的控制消息,其目的是打开一个对应的RenderView对象完成内核初始化,在主进程中与之对应的通信对象是RenderViewHost。后续我会详细解释。
总结
本文简要介绍了chrome的启动过程,包括浏览器主进程和内核Renderer进程的启动。可能你会觉得这些内容都是泛泛而谈,让人摸不着头脑,并没有什么实质内容。确实,由于chrome的庞大,要详细深入阐述每个步骤是非常繁琐的,我想最好还是由粗到细逐步推进,且重点是掌握其体系架构。本文的目的主要是为那些想迫不及待的开始跟踪chrome代码进行学习的朋友提供一点简单的线索。后续我将从原理上较深入的讲解chrome的体系架构相关的内容。