虽然目前的浏览器的功能很强 ,但仍然有其局限性。早期的浏览器能力十分有限,Web前端开发者希望能够通过一定的机制来扩展浏览器的能力。早期的方法就是插件机制,现在流行次啊用混合编程(Hybird Programming)模式。插件一直伴随着浏览器的发展,最著名莫过于Adobe公司的Flash插件。对于插件的接口定义,差别也很大,比较著名的是微软公司的ActiveX插件机制和网景公司 的NPAPI产检。随后,Chromium项目考虑到性能引入PPAPI插件机制,同时为了安全方面的考虑,引入NativeClient机制。这些插件机制扩展了浏览器的能力,极大地丰富了网页的应用场景,同时,随着HTML5的发展,很多HTML5功能同样需要扩展JavaScript的编程接口,以便开发者可以使用JavaScript代码来调用,而这样的扩展就需要相应的机制来实现。
1 NPAPI插件
1.1 NPAPI简介
NPAPI(Netscape Plugin Application Programming Interface)的全称是网景插件应用程序编程接口,最早是由网景公司提出的,用于让浏览器执行外部程序,以支持网页中各种格式的文件,典型的例子是视频、音频和PDF文件等(通过内容类型来区分)。对于这些网络资源或者文件,浏览器本身并不支持它们。但是,经过第三方开发者开发的插件程序,浏览器就可以做到支持了。图10-1是Chrome浏览器使用NPAPI插件的列表中一个示例(在地址栏中输入chrome://plugins/就可以查看到所有插件)。当遇到上述格式PDF文件的时候,Chrome浏览器会调用该阅读器插件,通过NPAPI规范定义的接口使浏览器同插件之间得以交互。
图10-1 Chrome浏览器中Adobe阅读器插件
现实中,NPAPI机制被广泛地应用,很多厂商或者开发者基于该接口规范编写了数量众多的插件实现,因而Chromium项目也必须对它提供支持,不过Chromium还有自己独特的插件架构,后面会详细介绍。使用插件的方法也非常简单,在网页中申明如下语句即可,它表示使用上述插件来打开一个PDF文件并显示在网页中:
<embed id="plugin" type="application/pdf" src="src/abc.pdf">
NPAPI提供两组接口,一类以NPP开始,由插件来实现,被浏览器调用,主要包括一些插件创建、初始化、关闭、销毁、信息查询及事件处理、数据流、窗口设置、URL等。另一类以NPN开始,由浏览器来实现,被插件所调用,主要包括图形绘制、数据流处理、浏览器信息查询、内存分配和释放、浏览器的插件设置、URL等。这两类接口足够满足大多数双方交互的需求。
原始的NPAPI接口使用起来不是很方便,因而有开发者对其进行了封装以便于其使用。一个比较著名的开源项目是Firebreath。它将原始C风格的NPAPI接口封装成C++风格的接口,非常方便用户使用,而且有针对Windows和X Window的移植,用户无须对底层接口特别了解。更为有趣的是,Firebreath也有对ActiveX接口规范的封装,因而对于现在主流的两种插件接口,开发者都可以基于Firebreath的接口进行编程,极大地增强了移植性和通用性。详情请参考Firebreath项目的主页,网址如下所示:http://www.firebreath.org/display/documentation/FireBreath+Home。
下面主要介绍WebKit和Chromium中是如何支持插件机制的,所以上面的使用Firebreath等项目开发插件的实现不在本书的范围内,有兴趣的读者请自行学习。
1.2 WebKit和Chromium的实现
1.2.1 WebKit基础设施
NPAPI插件获得了WebKit的支持,因为它的广泛使用性。在HTML网页中,可以通过两种类型的元素“embed”和“object”来使用插件。两者都可以用来在网页中内嵌插件,看起来“embed”元素更老一些,之前的一些浏览器只支持“embed”而不支持“object”,不过在WebKit中,两者都得到了支持,一个简单的例子如“<embed src='webkit.pdf'/>”。那么,WebKit中是如何支持它们的呢?
图10-2给出WebKit中支持插件机制所使用的类及其结构,初看起来比较复杂和杂乱无章,那么就分成左、中、右三个部分分别介绍它们。左边部分就是表示插件元素在DOM树和RenderObject树中的节点类,因为有两种HTML元素可以表示插件,所以为它们抽象出来了一个基类。对于插件元素在DOM树中的对应节点,RenderObject树中对应就是RenderWidget对象,用于表示这是个可视化的元素。在某些WebKit移植中,甚至引入了硬件加速机制来加速插件的绘制,例如WebKit的Qt移植。它的基本思想是将插件元素作为单独的一个层(PlatformLayer)来处理,插件的实例将绘制所有内容在这一层上,就像视频元素一样。
图10-2 WebKit中支持插件的相关类
图中右侧部分表示的是WebKit如何管理插件库,主要使用两个类:
- PluginDatabase :注册和管理所有的插件实现,一个插件通常是一个动态库,插件的信息包括名字、描述、版本,还有最重要的MIME类型和文件的扩展名(File extensions),例如图10-1中的PDF插件能够支持MIME类型“application/pdf”和扩展名“.pdf”。当然,一个插件也可以支持多种类型的文件。同时,它能够根据MIME类型和文件扩展名来查找相应的插件库。
- PluginPackage :表示一个插件库,也就是PluginDatabase类管理的对象。它包含两个非常重要的变量,就是m_pluginFuncs和m_browserFuncs,对应的就是前面介绍的NPP开头的函数组和NPN开头的函数组。
在中间部分的表示是插件的视图部分,它和DOM元素或者RenderWidget对象一一对应,其作用当然是绘制插件的可视化结果,同时它需要调用最右侧的类来获取插件。
- NPP :使用PluginPackage的接口来创建的插件实例。
- PluginViewBase :抽象类,主要是定义一些接口,这些接口会被HTMLPlugIn- Element类调用,用来处理视图方面的一些操作,如鼠标、聚焦(focus)等。
- PluginView :表示的是一个插件的视图,它非常重要,连接了插件库和网页中DOM接口和可视化RenderObject节点,包含所需的插件库和插件实例。
- NPObject :表示的是插件和浏览器(这里是WebKit)之间数据的交互类型,因为插件能够访问DOM树和JavaScript对象,所以JavaScript中的基本类型和JavaScript对象都会包装成NPObject来在两者之间传递。
对于PluginDataBase、PluginPackage和Pluginview类,在不同的移植中,它们可能会需要一些不同的实现,所以移植通常可能会扩展它们,当然主要工作逻辑可以共享。对于WebKit的Chromium移植来说,它的实现更为复杂,在下一节详细介绍。
对于插件机制,有几个问题要回答,第一是插件库的注册、查找等管理机制。第二是WebKit中的插件节点的处理,包括DOM树和RenderObject树如何支持插件。第三是如何使用注册的插件来创建插件示例并绘制需要的结果到网页最终结果中去。下面我们来具体说明一下。
首先是插件库管理机制。管理的基础是MIME类型和文件扩展名,例如对于“<embed src='webkit.pdf'/>”这样的例子,PluginView类会将“.pdf”文件扩展名当作参数传递给PluginDatabase并期待返回一个PluginPackage对象。对于某个MIME类型,当出现多个插件支持的时候,管理机制需要决定如何选择它们。
第二是插件节点的处理。当网页中出现“embed”和“object”元素的时候,WebKit会首先创建HTMLPlugInElement(应该是它的子类)对象,之后需要创建RenderWidget节点,当出现硬件加速机制的时候,可能还需要创建相应的RenderLayer节点。同时,还要创建PluginView对象,并根据DOM元素的属性来查找并创建相应的实例。
第三是绘图工作。本身NPAPI没有提供绘图的接口,只是让插件将绘制完的结果传给浏览器或者提供一个绘制的目标存储结构,从而让插件直接在它上面绘制,这就是插件的Window和Windowless模式,关于这两种模式,后面还会做介绍。另外一个方面是跟浏览器交互以通知某些区域需要重绘等消息。
虽然插件机制是用来支持“object”或者“embed”元素,但是,该机制也能够扩展JavaScript中对象和对象的方法,例如希望在JavaScript中增加W3C组织定义的一些标准接口,如设备相关的对象和方法。
NPAPI插件虽然功能强大,但是,通常它是浏览器不稳定的重要原因之一,这是因为插件由各个厂家自行维护,质量和稳定性也千差万别,插件的不稳定通常会导致浏览器的不稳定,这在现在多页面同时浏览的模式下会带来非常差的用户体验。同时,NPAPI的性能不是很高效,而且存在一些局限性,特别是绘图方面。最后,NPAPI插件拥有访问任何本地资源的能力,这会带来安全性问题,所有未经过认证的插件都非常危险,随意使用第三方插件的网页也不无可能对系统造成灾难性的后果。这与ActiveX插件很像,它同样也是很多病毒攻击的对象。因为插件通常是网络攻击的对象,一旦这些插件被攻击成功,那么攻击者就能够随意访问本地资源。
在WebKit的这种插件设计架构中,渲染引擎同插件的运行通常在同一进程中,这一设计将会带来稳定性和安全性方面的灾难性后果。为了避免这些方面的问题,Chromium在WebKit/Blink插件架构的基础上引入了跨进程的插件机制,这为浏览器的稳定性提供了保证,下一小节将详细介绍。同时,考虑到性能方面的问题,Google提出了新的PPAPI插件机制,考虑到安全性和支持本地代码的问题,Chromium引入了Native Client机制,为安全性提供了保证,这在后面也会作详细介绍。
1.2.2 Chromium的插件架构
为了解决插件的稳定性问题,同时因为Chromium的沙箱模型机制(第12章会介绍,它会限制插件访问本地资源的能力),插件实例不能够在Renderer进程中运行,因为除了访问IO之外,没有访问其他接口和资源的能力,所以在Chromium中,插件是被放在单独的进程中来执行,这就是Chromium的插件多进程模型。图10-3显示的是Chromium的插件进程示例图。
图10-3 Chromium的插件多进程模型
在Chromium中,每一个插件库只会有一个进程,这就是说,如果有两个或者多个Renderer进程同时使用同一个插件库,那么这些Renderer进程会共享同一个插件进程。因为多个Renderer进程共享同一种的Plugin进程,那么Plugin进程如何为它们服务呢?答案是Chromium在加载插件库后为每个插件使用点在plugin进程中创建一个对应插件实例(PluginInstance)。
值得注意的是,插件进程是由Browser进程来负责创建和销毁,而不是Renderer进程。原因在于Renderer进程没有创建的权限,而且Plugin进程也应该由Browser进程来统一管理,这样也更方便。当Plugin进程创建成功时,Browser进程会返回进程间通信的句柄,用于创建和Plugin进程通讯的PluginChannelHost。那它什么时候被销毁呢?当没有任何插件实例并且空闲一段事件后,它才会被销毁,这样做的好处是避免频繁地创建和销毁Plugin进程。
图10-4描述了Browser进程和Plugin进程间的通信机制及其所涉及的相关的模块(类)。Browser进程通过PluginProcessHost发送消息调用Plugin进程的函数,响应动作由PluginThread完成。而Plugin进程则是通过WebPluginProxy发送消息调用Browser进程的响应函数,响应动作由PluginProcessHost完成。
图10-4 Brower进程和Plugin进程的交互过程
Browser进程和Plugin进程仅有较少的消息传递,用于插件的创建等管理工作。其实,主要的工作在Renderer进程和Plugin进程之间,机制也相对更为复杂一些。根据前面介绍,HTMLPluginElement节点是DOM树中的一个节点,在Chromium的实现中会包含一个WebPluginContainerImpl,该节点是WebKit::Widget的子类,也就是Chromium中的一个对PluginView的具体实现类,而它包含一个WebPluginImpl,对plugin的调用有WebPluginDelegateProxy负责中转。在Plugin进程中,由WebPluginDelegateStub处理所有Renderer进程发送过来的请求,并由WebPlugin-DelegateImpl调用创建好的PluginInstance对象。PluginInstance最终调用PluginLib读取的插件库(libxxx.so)的各个函数入口地址,最终完成对插件库实现的调用。而对插件实现中对NPN开头函数的调用,则是通过PluginHost来完成。
PluginHost主要负责实现NPN开头的函数,如前面所描述,这些函数被plugin进程所调用。可以在plugin和renderer进程被调用。当在plugin进程调用这些函数时,chromium会覆盖PluginHost的部分函数,而这些新的callback函数会调用NPObjectProxy来通过IPC发送请求到renderer进程。
PluginInstance实现了NPP开头的函数,被Renderer进程所调用(WebPluginImpl通过WebPluginDelegateImpl来调用),PluginInstance通过PluginLib获得了插件库中这些函数的地址,从而把实际的调用桥接到具体的插件中。具体的如图10-5所示,主要结构来源于Chromium项目的官方网站,略有修改。
图10-5 Chromium的跨进程插件和Renderer进程交互过程
对于NPObject相关的函数调用,有专门的类来处理。NPObject的调用或者访问是双向的(renderer进程<->plugin进程),他们的具体实现是通过NPObjectProxy和NPObjectStub来完成。NPObjectProxy接受来自对方的访问请求,转发给NPObjectStub,最后NPObjectStub调用真正的NPObject并返回结果,如图10-6所示。
图10-6 NPObject对象的跨进程使用
1.2.3 Chromium插件的工作过程
插件工作过程主要是创建并完成插件和浏览器的交互过程。首先来看一下插件实例是如何被创建的,图10-7给出一个插件如何被Renderer进程触发创建的过程。
图10-7 Renderer进程创建插件实例的过程
如果页面中包含一个“embed”或者“object”元素,Renderer进程会创建一个HTMLEmbedElement元素,当该元素被JavaScript代码或者其他地方使用的时候,会触发创建相应的插件。HTMLEmbedElement对象会请求创建自己对应的RenderWidget(WebPluginContainerImpl),进而创建WebPluginImpl和WebPluginDelegate-Proxy。如果该插件的进程还不存在,WebPluginDelegateProxy会发送消息到Browser进程,请求该进程来创建Plugin进程。Plugin进程被Browser进程创建后,会响应Renderer进程的请求来创建PluginInstance并将它初始化,这样它们之间的联系就建立好了。注意,图中的WebPluginDelegateProxy类调用的操作“创建和初始化PluginInstance”,是通过进程间通信发送消息到Plugin进程,最终由该进程完成的。
接下来的工作主要是浏览器和插件通过NPP和NPN接口进行互相调用,这些调用在Chromium浏览器中都通过IPC机制来完成,具体的过程就是使用图10-5所描述类的调用过程。
1.2.4 Window和WindowIess插件
根据规范,可以通过设置“embed”或者“object”元素的属性来让浏览器来决定如何提供绘制结果的存储方式。Window模式插件由Renderer进程提供一个窗口(window),插件直接在该窗口上进行绘制,所以它不需要和网页的内容再进行合并,而是一个独立的绘制目标。而Windowless模式的插件则不同,插件将绘制的结果(如Pixmap)通过共享内存的方式(如Transport DIB)传递给Renderer进程,Renderer进程然后绘制该内容到自己内部的存储结构(Backing Store)上。
从上面的论述不难看出,Window模式的性能是要高于Windowless的。但是,对于Window模式的插件来说,它不能跟网页的内部内容构成很好的前后关系,例如在网页的某些元素之后,某些元素之前,这不得不说是一个局限。而对于Windowless模式的插件来说,性能较差的问题带来的好处是,可以把插件绘制的结构和网页上的其他内容做各种形式的合成。
跨进程带来稳定性的同时,由于访问对象和操作都需要经过进程间通信,所以额外的负担也比较重。为此,Chromium的PPAPI插件机制诞生。