月度归档:2012年09月

Chromium 源码分析: 进程启动和参数传递

ContentShell相对于TestShell,不同之处在于使用了Content API,引入了多进程模型,在移植到android平台时,首先需要解决的问题就是启动多进程以及进程之间的通信。下面就分析一下android下是如何启动进程,以及进程间如何通信。

进程启动

我们知道,linux系统创建进程非常简单,调用fork即可以启动一个新进程。Android系统本身基于linux,是不是直接调用fork就可以了呢?我们可以使用ndk编写一个简单的程序验证一下,的确可以调用fork启动进程,例子在git://github.com/mogoweb/android-testcode.git上可以获取到。但是用fork启动的进程不受控,会引起一些奇怪的问题,所以不推荐使用fork。

chromium for android采用的是android的Service机制来启动进程。关于Service机制,<<Pro Android 3>>这本书有专门的一章讲述,android官方文档也有一篇专门讲Service的文章,请参看http://developer.android.com/guide/components/services.html。简单而言,Service就是在后台运行,无用户界面,它可以和应用程序在同一进程中运行,也可以在一个独立的进程中运行。前者称为local service,后者称为remote service。chromium采用后者,达到开启新进程的目的。看看ContentShell应用程序的AndroidManifest.xml文件,有如下行:

<!- The following service entries exist in order to allow us to
     start more than one sandboxed process. ->

<!- NOTE: If you change the values of "android:process" for any of the below services,
     you also need to update kHelperProcessExecutableName in chrome_constants.cc. ->
<service android:name="org.chromium.content.app.SandboxedProcessService0"
         android:process=":sandboxed_process0"
         android:permission="org.chromium.content_shell.permission.SANDBOX"
         android:exported="false" />
<service android:name="org.chromium.content.app.SandboxedProcessService1"
         android:process=":sandboxed_process1"
         android:permission="org.chromium.content_shell.permission.SANDBOX"
         android:exported="false" />
<service android:name="org.chromium.content.app.SandboxedProcessService2"
         android:process=":sandboxed_process2"
         android:permission="org.chromium.content_shell.permission.SANDBOX"
         android:exported="false" />
<service android:name="org.chromium.content.app.SandboxedProcessService3"
         android:process=":sandboxed_process3"
         android:permission="org.chromium.content_shell.permission.SANDBOX"
         android:exported="false" />
<service android:name="org.chromium.content.app.SandboxedProcessService4"
         android:process=":sandboxed_process4"
         android:permission="org.chromium.content_shell.permission.SANDBOX"
         android:exported="false" />

可见Service在使用前必须声明,不能随心所欲的创建,ContentShell事先声明了5个Service,如果查看SandboxedProcessService0 ~ SandboxedProcessService4的源码,可以了解到它们实际上是一样的。考虑到移动设备内存有限,加上一些复用技巧,定义5个Service足够满足需要了,当然最大多同时可开启的进程数限制为5个。

在ContentShell中,子进程被称为SandboxedProcess,启动过程如下图所示,其中灰色部分表示Java部分。启动过程中还涉及到不同的线程,SandboxedProcessService实际是在另一个进程中,图中并没有表现出来,请注意。

进程间参数传递

linux进程间通信的方式有很多,比如管道/命名管道/消息队列/socket等,它们各有优缺点。chromium android选择的是socketpair(出于什么样的考虑,选择socketpair不得而知,chromium windows版本选择的是named pipe)。使用socketpair面临一个重要的问题就是主进程创建的socket fd如何传递到SandboxedProcess。如果在linux下用fork创建新进程就不用操心这个问题了,因为主进程创建的fd,子进程也会复制一份(并非简单的把fd值赋值过去)。Android Service的方法调用差不多算是同一台机器上的RPC,首先需要解决的是函数参数及返回值的marshal/unmarshal,因为处在不同的地址空间,是无法靠指针来传递参数的。Android中Parcel类就是做这件事情的,而自定义类需要Parcelable接口,才能跨进程传递。

socket fd如何写入到Parcel类呢,直接当作int类型写入肯定是不对的,这时该ParcelFileDescriptor类登场了。ParcelFileDescriptor类有一个fromFd的静态方法,用于从fd构造ParcelFileDescriptor类,而ParcelFileDescriptor类实现了Parcelable接口。

将fd封装的代码位于SandboxedProcessConnection::doConnectionSetup(),将fd解包出来的代码位于SandboxedProcessService.java中。当子进程解出了socket fd后,就可以通过这个fd和主进程发送和接收消息了。

chromium将进程间的通信代码封装一层又一层,理解起来有些困难,在之后的文章中再慢慢剖析吧。

chromium中的智能指针

C/C++程序员最不愿意面对但又必须面对的就是指针引起的内存问题。围绕指针问题也有很多解决方案,比如android中的strong pointer/weak pointer,WebKit中的RefPtr和PassRefPtr,C++标准库中的auto_ptr,等等。毫不意外,chromium也有自己的指针管理类(看来还是自己发明的轮子用起来顺手一些):scoped_ptr/scoped_array和scoped_refptr。

scoped_ptr/scoped_array

顾名思义,模板类scoped_ptr用于管理指针拥有权,在离开作用域后自动delete指针。scoped_array则用于数组的管理。scoped_ptr和scoped_array分别对应着new/delete和new[]/delete[]。首先我们来看看scoped_ptr的用法:

{

  scoped_ptr<Foo> foo(new Foo(“wee”));

} // foo离开生命周期,释放所拥有的指针

{

  scoped_ptr<Foo> foo;           // 没有管理指针

  foo.reset(new Foo(“wee”));   // 现在管理了指针

  foo.reset(new Foo(“wee2”)); // Foo(“wee”)被销毁

  foo.reset(new Foo(“wee3”)); // Foo(“wee2”)被销毁

  foo->Method();                      // 调用Foo::Method()

  foo.get()->Method();             // 调用Foo::Method()

  SomeFunc(foo.release());      // SomeFunc获得所有权,foo不再管理指针

  foo.reset(new Foo(“wee4”));// foo又管理指针

  foo.reset();                           // Foo(“wee4”)销毁,foo不再管理指针

} // foo没有管理指针,所以没有指针需要销毁

scoped_ptr还实现了C++11 unique_ptr的部分功能,所以它能转移所有权,但不能复制。所以如下代码是错误的:


{

  scoped_ptr<Foo> foo(new Foo(“wee”));

  scoped_ptr<Foo> other = foo;  // 错误,scoped_ptr不能复制

scoped_ptr不能复制,是否意味着scoped_ptr不能作为函数参数和函数返回值呢?答案是可以,不过在使用时需要改变一下写法,下面是scoped_ptr作为函数参数和返回值的例子:

void TakesOwnership(scoped_ptr<Foo> arg) {

}

scoped_ptr<Foo> CreateFoo() {

  return scoped_ptr<Foo>(new Foo(“new”));

}

scoped_ptr<Foo> PassThru(scoped_ptr<Foo> arg) {

  return arg.Pass();

}

{

  scoped_ptr<Foo> ptr(new Foo(“yay”));  // ptr管理Foo(“yay”)

  TakesOwnership(ptr.Pass());                  // ptr不再管理Foo(“yar”)

  scoped_ptr<Foo> ptr2 = CreateFoo();   // ptr2管理返回的Foo对象

  scoped_ptr<Foo> ptr3 = PassThru(ptr2.Pass()); // ptr3管理ptr2之前拥有的对象,ptr2不再管理任何对象

}

从上面的代码可以看出,在从函数返回scoped_ptr对象,或者调用带scoped_ptr参数的函数时,必须借助Pass()方法。CreateFoo有些特殊,因为在返回的代码行构造了一个临时对象,所以不需要调用Pass()方法。Pass()方法还可以正确的处理向上转型(upcast),例如:

scoped_ptr<Foo> foo(new Foo());

scoped_ptr<FooParent> parent = foo.Pass();

另外还有一个方法PassAs()用于在函数返回值进行向上转型。

scoped_ptr<Foo> CreateFoo() {

  scoped_ptr<FooChild> result(new FooChild());

  return result.PassAs<Foo>();

}

scoped_ptr模板类的优点是小、简单、效率高,但是存在一个缺点,不能共享所有权,如果多处需要同时持有某个指针对象,scoped_ptr就不能满足要求了。不过chromium中提供了另一个模板类scoped_refptr来弥补这一问题。

scoped_refptr

scoped_refptr是用于引用计数对象的智能指针类,使用该类后,无需手工调用引用计数对象的AddRef和Release方法,从而有效避免由于忘记调用Release引起内存泄漏。通常的用法如下:

class MyFoo : public RefCounted<MyFoo> {

   …

};

void some_function() {

  scoped_refptr<MyFoo> foo = new MyFoo();

  foo->Method(param);

  // foo在函数返回时自动释放

}

void some_other_function() {

  scoped_refptr<MyFoo> foo = new MyFoo();

  …

  foo = NULL; // 显式释放foo

  if (foo)

    foo->Method(param);

}

从上面的例子可以看到,使用scoped_refptr<T>就如同使用指向T的指针,只是不再担心指针的释放了。如果有两个scoped_refptr<T>对象,还可以交换彼此的引用,如下所示:

{

  scoped_refptr<MyFoo> a = new MyFoo();

  scoped_refptr<MyFoo> b;

  b.swap(a);

  // 现在b引用到MyFoo对象,而a则引用空指针

}

使用赋值语句,上面例子中的a和b可以引用到同一个MyFoo对象:

{

  scoped_refptr<MyFoo> a = new MyFoo();

  scoped_refptr<MyFoo> b;

  b = a;

  // 现在a和b都引用同一个MyFoo对象

}

需要注意的是,引用计数的功能并不是由scoped_refptr提供的,而是由scoped_refptr引用的对象本身提供的,也就说使用scoped_refptr的对象必须具备引用计数功能。Chromium中的引用计数功能由RefCouneted类提供。

Chromium源码分析:ContentShell启动流程

ContentShell是基于Content API的一个简单的浏览器外壳,下面我将分析chromium for android中的ContentShell程序的启动流程。

由于android程序的特殊性(一部分Java代码,一部分c++代码),所以看似简单的启动流程,在android版本的ContentShell却七拐八弯的,难以理清脉络。在android版本中,ContentShell采用了多进程模型,一个Browser进程加上若干Sandboxed子进程(也称作Render Process)。所以接下来我将以Browser进程和Sandboxed进程的创建为突破口,来弄清ContentShell的启动流程。

Browser进程初始化

ContentShell的主Activity为ContentShellActivity,初始化和加载网页在其onCreate成员函数中实现。序列图如下:

1)创建ShellManager,ShellManager除了管理Shell外,本身也是一个FrameLayout。在native端进行初始化后,设置启动URL

2)调用ContentView的静态成员方法enableMultiProcess,最终调用到AndroidBrowserProcess的genericChromiumProcessInit()进行初始化,包括loadlibrary, loadresource,setcommandLine(设置render的数量), nativeInitApplicationContext()等

3)在AndroidBrowserProcess::genericChromiumProcessInit()中,会调用ContentMain的nativeStart(),在native端初始化一个全局的ContentMainRunner对象g_content_runner

4)ContentMainRunner::RunNamedProcessTypeMain() 中调用BrowserMain(), BrowserMain()中create,init并Run()一个BrowserMainRunner对象。

5)在Initialize过程中创建BrowserMainLoop并初始化一个BrowserThreadImpl做为Browser进程的主线程,开始message_loop,并通过BrowserThread::PostTask给主进程的消息队列发送任务。

至此Browser进程的创建和初始化完成.


Shell创建与初始化

在上图中,初始化过程并没有结束,下面就接着上图的start继续分析之后Shell的创建流程:

1)ContentMain类的start方法会调用native端的Start方法,在该方法中,会创建ContentMainRunnerImpl实例,并调用其Intialize方法进行初始化,初始化后调用Run方法。

2)Run方法中调用一个全局函数RunNamedProcessTypeMain,在该函数中,调用ShellMainDelegate的RunProcess方法,运行Browser进程。

3)RunProcess中会创建BrowserMainRunnerImpl实例,在进行初始化时,创建BrowserMainLoop对象,并执行其Init方法,在Init方法中,会创建一个非常重要的对象ShellBrowserMainParts

4)正是在ShellBrowserMainParts::PreMainMessageLoopRun中,调用Shell的静态成员方法CreateNewWindow,开启Shell创建过程的序幕。

Sanboxed进程的创建与初始化

Sandboxed进程的个数不定,与所选择的进程模型有关。chromium支持如下几种进程模型: Process-per-site-instance,Process-per-site,Process-per-tab,Single process。默认为Process-per-site-instance。

接着上图,在Shell的静态方法CreateNewWindow中,会进行一系列的初始化动作:

1)Shell::CreateNewWindow()用来创建新的Tab。此处会先create一个WebContents, WebContents负责Tab页面内网页内容的显示,跳转等。WebContents的render_manager_(RenderViewHostManager)成员负责管理RenderViewHost的创建和切换。Render_manager_初始化时会create新的SiteInstance和RenderViewHost, SiteInstance对应当前的url地址,而RenderViewHost则负责管理一个RenderView.

2)默认情况下,Chromium中的process是和SiteInstance一一对应的,即相同网站的tab使用相同的render进程处理,当tab在不同的网站切换时,也会同时切换render进程。在RenderViewHost构造时会获取SiteInstance对应的render进程,如果没有,就会创建一个新的render子进程RenderProcessHostImpl.

3)RenderProcessHost仍然是在Browser进程下的,它是RenderProcess子进程在Browser端的代表。 在RenderProcessHost的Init()函数中,通过ChildProcessLauncher工具类Launch()一个新的子进程,并最终调用到sandboxed_process_launcher.cc中的StartSandboxedProcess()函数

4)StartSandboxedProcess()将调用到java端的代码进行SandboxedProcessService进程的create, bind,此时第一个Sandboxed子进程SandboxedProcessService0将被创建,并调用nativeInitSandboxedProcess()对该进程进行初始化。

至此,ContentShell的初始化过程暂告一段落,此时Browser进程和Sandboxed进程将会创建出来。

补充

其实在ContentShellActivity的onCreate方法调用之前,ContentShellApplication::onCreate代码已经执行了,这里进行了一些设置操作。另外,在加载native动态库so时,会调用JNI_OnLoad方法,在这里,除了注册native方法外,还执行了content::SetContentMainDelegate函数。

另一个需要注释的地方是:ContentShellActivity中的mShellManager.launchShell(shellUrl)语句并不会调用。开始分析代码的时候,以为是从这里开始加载流程,实际上由于ContentView.enableMultiProcess返回true,导致这段代码不会执行。