Chrome学习笔记(三):UI组件,皮肤引擎 —— 控件库

Table of contents for Chrome学习笔记

  1. Chrome学习笔记(一):线程模型,消息循环
  2. Chrome学习笔记(二):UI组件,皮肤引擎 —— 基础设施篇
  3. Chrome学习笔记(三):UI组件,皮肤引擎 —— 控件库

原创文章,转载请注明:转载自Soul Apogee
本文链接地址:Chrome学习笔记(三):UI组件,皮肤引擎 —— 控件库

这篇文章是接着上篇文章继续聊的,Chrome的代码实在太多,每一个东西单拿出来都可以说很很多,单就一个breakpad都说了两篇。恩,不过也许是我太啰嗦了。

1. UI控件库(Control)简介

我们知道Chrome做这一套皮肤引擎是为了替换掉Windows原生的控制UI的方式,所以这个皮肤引擎上怎么能没有控件呢?所以在建立好各种基础的UI元素和默认处理之后,Chrome在上面开始封装各种基础的控件,比如button等等。
其相关代码主要分布在src/ui/views/control目录下。

为了进一步的方便开发,Chrome的UI控件库中包括了很多基础的控件,这些控件现在包括如下几种:

  • button:基本的按钮控件和其常用的变种,类似于CButton。
  • combobox:下拉列表和原生的下拉列表,类似于CComboBox。
  • menu:菜单。
  • scrollbar:滚动条。
  • tabbed_pane:封装了自绘的和系统原生的Tab分页控件,类似于CTabCtrl。
  • table:封装列表控件,类似于CListCtrl。
  • textfield:封装输入控件,类似于CEdit。
  • tree:树形控件,类似于CTreeCtrl。
  • 其他:Label,进度条,分栏等等等等。

这些控件中有一些并不一定是全部自绘的,而是使用系统原生的控件,比如tabbed_pane,tree和table。按照Chrome的文档来看,Chrome团队应该并不喜欢使用系统原生的控件,所以从长远来看,这些代码应该是中间代码,毕竟很好的实现一个这样的控件还是比较复杂的,所以Chrome就暂时使用着原生的控件。

另外还有一种我们在控件库中找不到,但是却十分重要的控件:容器。
Chrome的皮肤引擎有一个特点:万物皆容器。所有的控件都继承于一个同一个基类:View,所以所有的控件都可以有子元素。在Chrome里面,你可以建立一个其他什么都不做的View,只用它来排布他的子元素。用过GTK的朋友们肯定对GtkHBoxGtkVBox这个类有一定的印象,这两个类对辅助控件的布局是很有帮助的。在Chrome里面,你也可以使用类似的用法来辅助控件的布局,而且在UI里面还提供了几种基础的布局方法来帮助大家开发。

2. 实现方式

提供的控件确实比较全面,那么为了更好的帮助我们理解和使用这些控件,在使用这些控件之前,先让我们来看一下Chrome的UI控件的实现方法。

2.1. 自绘控件实现

我们知道自绘控件的关键是三个方面:绘制、数据提供和事件回调。所以Chrome在代码里面也就是针对着这样三个方面来实现他的封装。
真是熟悉的三个方面啊,想必很多朋友已经能对Chrome控件的实现方式猜个大概了,如果还对于Chrome UI绘制机制有一定了解,那么代码估计自己也能写出个大概了。
没错,就是MVC模型

  • 使用Canvas来实现绘制的接口,在控件的OnPaint回调中进行自绘。
  • 采用MVC的设计思想,对于复杂的控件,如TreeTable等等,提取出Model接口和Controller接口,分别用于管理数据和处理事件回调并控制控件行为。

我们拿Tree来举例,Chrome将一个Tree分为三个部分:TreeViewTreeModelTreeViewController

  • TreeView主要用于绘制。现在TreeView已经被系统原生控件接管,但是在Chrome代码里面,我们依然能找到自绘的TreeView
  • TreeModel主要用于管理数据。
  • TreeViewController主要用于处理事件回调,控制控件行为,如:控制树中某一项能不能被编辑。
chrome-ui-control-tree:
chrome-ui-control-tree

这样Chrome就实现了自绘控件。

2.2. 与原生控件的兼容

由于Chrome的控件还有一部分控件是直接使用的系统原生的控件,所以就会牵涉到自绘控件和原生控件如何在View控件树兼容的问题。
一个很自然的解决方法就是建立一个继承自View的原生控件基类,而具体的控件和逻辑则放入他的子类。这个基类就是NativeControl,通过继承他,来将原生控件纳入Views的层次结构中。在Chrome的代码中,Tree就是这样来实现的。

chrome-ui-native-control:
chrome-ui-native-control

但是Chrome认为这样做存在问题,于是Chrome对其结构进行了改进,以求更好的支持跨平台和代码复用。
所以现在更多的控件的实现是建立一个Wrapper封装NativeControl,Wrapper的实现则继承自NativeControlWin,以便更加方便的控制控件,或者使用View进行替换,如Combobox

3. 使用范例

好了,扯了这么多,我们来看一下如何使用这个皮肤引擎吧。

3.1. 建立一个工程

由于Chrome UI库和其他工程关联太紧,所以我们如果要建立一个测试工程其实并没有那么容易,我们可以在view_example_exe这个工程上直接进行修改,或者利用gclient生成一个测试工程。

  1. 打开src/ui/views/view.gyp,将views_examples_lib的描述段复制一份,粘贴在# target_name: views_examples_lib之后。
  2. 修改其工程名为你想要的工程名,如:view_test。
  3. 删除其source区域下除了.rc文件以外的所有源代码。
  4. 在dependencies中加入一项:views。
  5. 打开配置好的cygwin或者命令行,进入chromium源代码根目录,也就是存放.gclient文件的目录,输入gclient runhooks。
  6. 重新打开src/ui/views/views.sln,我们就可以在(views)目录下,看到view_test的工程了。
chrome-add-ui-proj:
chrome-add-ui-proj

3.2. 准备工程

为了能让这个UI工程运行起来,我们需要写一些准备的代码:

#include "base/at_exit.h"
#include "base/command_line.h"
#include "base/message_loop.h"
#include "base/i18n/icu_util.h"
#include "ui/base/ui_base_paths.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"

using namespace views;

void test()
{
	return;
}

int main(int argc, char** argv)
{
    // 以下内容必不可少,如果不添加会导致程序无法运行
    OleInitialize(NULL);                                        // Windows上必备,OLE初始化
    CommandLine::Init(argc, argv);                              // 初始化命令行参数
    base::AtExitManager at_exit;                                // 为MessageLoop所使用的,用于在退出时清理对象的工具类
    ui::RegisterPathProvider();                                 // 注册UI组件所需要的路径,不然会出现资源找不到的问题
    bool icu_result = icu_util::Initialize();                   // 注册ICU,用于国际化
    CHECK(icu_result);
    ui::ResourceBundle::InitSharedInstanceWithLocale("en-US");  // 初始化国际化资源包
    // 以上内容必不可少,如果不添加会导致程序无法运行

    // 初始化消息循环
    MessageLoopForUI msg_loop;

    test();

    msg_loop.Run();

    OleUninitialize();

    return 0;
}

之后我们把测试代码都加载test()这个函数中就可以了,另外这里之后的代码可能会泄漏的问题,这里我们先暂时不去理会他,后面会单独聊。

3.3. 实现一个简单的窗口

建立好了工程之后,我们就可以添加代码了,先让我们来建立一个最简单的窗口:一个空白的Widget。

void test()
{
    // 创建窗口并显示
    Widget::CreateWindowWithBounds(NULL, gfx::Rect(0, 0, 320, 240))->Show();
}

短短几行我们就创建了一个最基本的窗口了,但是这个窗口实在是。。。有点难看啊。。

chrome-base-ui:
chrome-base-ui

3.4. 添加一个按钮吧

既然难看,我们就来添加一些小控件到里面吧,先加一个小按钮吧。
首先,让我们来回想一下Chrome UI的元素结构,还记得这幅图么:
chrome-view-hierarchy:
chrome-view-hierarchy

所以为了增加按钮,我们需要创建一个按钮的控件,并且为Widget的ClientView生成一个ContentsView来保存我们的这个按钮。

首先我们先添加几个头文件:

#include "ui/views/controls/button/text_button.h"
#include "ui/views/layout/fill_layout.h"

另外我们添加了一个TestWidgetDelegate的类,用于设置Widget样式并且提供ContentsView。另外我们还给它添加了一个FillLayout,让按钮与窗口保持一样的大小。

class TestWidgetDelegate : public WidgetDelegateView, public ButtonListener
{
public:
    TestWidgetDelegate() {
        // 设置窗口背景,让其不为黑色
        set_background(Background::CreateStandardPanelBackground());

        // 添加子按钮
        Button *button = new TextButton(this, L"test");
        button->set_tag(1);    // Button的tag是用于区分按钮的,在回调时,通过这个tag来区分不同的按钮
        AddChildView(button);

        // 设置子按钮保持和窗口一样大小的布局方式
        SetLayoutManager(new FillLayout);
    }

    virtual ~TestWidgetDelegate() {}

    // 可以变化大小
    virtual bool CanResize() const { return true; }

    // 可以最大化
    virtual bool CanMaximize() const { return true; }

    // 初始化焦点
    virtual View* GetInitiallyFocusedView() OVERRIDE { return this; }

    // 提供ContentsView
    virtual View* GetContentsView() OVERRIDE { return this; }

    // 窗口关闭是退出消息循环
    virtual void WindowClosing() OVERRIDE { MessageLoopForUI::current()->Quit(); }

    // 如果按钮发生点击,则回调此事件
    virtual void ButtonPressed(Button* sender, const views::Event& event) {
        if(sender->tag() == 1) {  // 回调时,通过这个tag来区分不同的按钮
            // .....
        }
    }
};

另外生成Widget的代码也要做少许的改动:

void test()
{
    // 创建窗口并显示
    Widget::CreateWindowWithBounds(new TestWidgetDelegate, gfx::Rect(0, 0, 320, 240))->Show();
}

编译运行,可以看到一个按钮已经出现啦~

chrome-base-ui-with-button:
chrome-base-ui-with-button

3.5. 添加一个原生控件

好,我们已经可以添加一个自绘的控件了,现在让我们来试着添加一个系统原生的控件吧。

由于Chrome是在View的基础上封装的原生控件,所以添加原生控件也并非难事。比如我们现在来添加一个Tab栏,我们只需要添加一个头文件,再稍稍修改一下TestWidgetDelegate的构造函数就可以了。

添加头文件:

#include "ui/views/controls/tabbed_pane/tabbed_pane.h"

修改TestWidgetDelegate的构造函数:

    TestWidgetDelegate() {
        // 设置窗口背景,让其不为黑色
        set_background(Background::CreateStandardPanelBackground());

        // 添加Tab栏
        TabbedPane *tabbedpane = new TabbedPane();
        AddChildView(tabbedpane);   // 此处创建完成需要立刻添加到View中,因为其后端实现是在此时被创建的,如果不添加,调用AddTab接口会发生崩溃。

        // 添加子按钮
        Button *button = new TextButton(this, L"test");
        button->set_tag(1);    // Button的tag是用于区分按钮的,在回调时,通过这个tag来区分不同的按钮
        tabbedpane->AddTab(L"Tab1", button);

        // 设置子按钮保持和窗口一样大小的布局方式
        SetLayoutManager(new FillLayout);
    }

这里需要注意的一点是:Chrome很多原生控件的真实实现类都是在View层次关系发生改变的时候创建的,所以在Tab等原生控件创建完成之后,需要马上将其加入View中,不然后续调用其接口就会发生崩溃。

让我们来看看最终的效果:

chrome-base-ui-with-tab:
chrome-base-ui-with-tab

3.6. 控件的生命周期

Chrome控件的生命周期是比较晦涩的,在上面的代码,我们可以看见我们new出来了很多对象,但是从未调用过delete,那中间会有内存泄漏么?

答案是:不会。这些的对象都会在窗口接收到最后一个消息的时候把所有在View树中的对象都释放掉。在Windows下,也就是在WM_NCDESTROY消息中的处理中,主动释放所有的对象的。
所以我们在使用中,只需要保存好这些对象的裸指针,并且在合适的时机将其置空即可。对于置空的时机,Widget和View也有对应的回调,如Widget::DeleteDelegate,或者在析构函数里面来进行。

4. 写在最后

对于Chrome UI控件库,这里只是做了写简要的记录。对于各种控件的使用,在Chrome的代码里面也提供了非常详细的实例程序,大家可以在src/ui/views/examples下找到这些代码。在VS中也提供了相应的工程:views_examples_exe,供大家参考。

Chrome学习笔记(二):UI组件,皮肤引擎 —— 基础设施篇

Table of contents for Chrome学习笔记

  1. Chrome学习笔记(一):线程模型,消息循环
  2. Chrome学习笔记(二):UI组件,皮肤引擎 —— 基础设施篇
  3. Chrome学习笔记(三):UI组件,皮肤引擎 —— 控件库

原创文章,转载请注明:转载自Soul Apogee
本文链接地址:Chrome学习笔记(二):UI组件,皮肤引擎 —— 基础设施篇

Chrome的UI是很奇妙的,因为看起来能很好的跨平台,而且可以很好的兼容各个平台的特性,比如在Mac下最小化和关闭按钮在左侧,还兼容全屏的特性,在Linux上,也能加载GTK的外框,外加现在Chrome在推的Aura,更是直接接管了桌面合成器。。。这一切让人不得不想去弄清楚Chrome到底是怎么来实现这么强大的UI呢?有一句话我非常喜欢:“源码面前,了无秘密”,读了几天的源代码,也总结些东西,以免后面忘记。
对使用比较感兴趣的朋友也可以先看看如何使用这套皮肤引擎,再来回头看实现。

1. 基本概念

由于Chrome不满Windows没有自带好用的皮肤引擎,所以在一顿折腾之后,就自己设计了一套平台无关的皮肤引擎:Views。它是一个典型的DirectUI,关于它,Chromium的网站上有三篇文章对其的设计进行了阐述:Views frameworkviews Windowing systemNativeControls。这三篇文章虽然是在09年的时候写的,但是后面的设计基本没有太大的改动,所以还是比较有用的,感兴趣的童鞋可以先看看。

在Chrome皮肤引擎里面两个非常重要的概念:WidgetView
Widget对应着一个原生的窗口,而View对应着窗口里面的一个控件,如容器,Button,Tab等等。这样在Widget和View之上,Chrome搭建起了自己跨平台的皮肤引擎。

关于跨平台,这里可能大家需要注意的一点是:这个皮肤引擎并不会封装的非常的完善,这里从Chromium的文档上对于Views framework的定义可以看出来:Our UI layout layer used on Windows/Chrome OS,能在Windows和ChromeOS上用用就可以了。而且Chrome团队也在文档中坦言,支持跨平台会遇到很多问题,如不好处理特殊的窗口消息等等。

2. 基础库:base

基本上每个程序库都有着自己的基础库,Chrome的皮肤引擎也不例外,在src/ui/base这个目录下放着的就是它的基础库。

由于已经有了Chrome本身的基础库,和一些第三方的组件的支撑,这个基础库下面主要放的就只是一些和UI相关的基本定义和基础的功能实现了。
以下是他所包含的目录和其对应的功能:

  • accelerators:快捷键的处理。
  • animation:动画效果的抽象,这里并不管绘制,只是负责计算动画效果的进度。
  • clipboard:平台无关的剪贴板操作。
  • cocoa:Mac上和cocoa相关的代码。
  • dragdrop:和拖拽相关的代码,并在内部封装了平台无关的统一数据传输接口。
  • glib:一些Linux上用的代码。
  • gtk:Linux上使用的和gtk相关的封装,简化事件处理什么的。
  • ime:和输入法相关的代码。
  • keycodes:平台无关的键盘的KeyCode的封装。
  • l10n:本地化工具函数。
  • models:定义了一些控件的数据接口。
  • range:用于表示范围的基本类型。
  • strings:用于本地化的字符串表。
  • touch:触屏相关的代码?
  • wayland:Linux和wayland相关的代码。
  • win:Windows下才会用到的代码,里面包含Windows原生窗口的封装,IME的处理等等。
  • x:Linux下和X11相关的代码。

3. 窗口封装:Widget

一个Widget对应着一个真实的窗口,在Windows下它就对应一个HWND。
其相关的代码在src/ui/views/widget目录下。

为了将平台相关的窗口细节隐藏在Widget内部,Chromium为平台相关的窗口抽取出了一个接口:NativeWidgetPrivate,用以封装平台相关的代码,而在里面将平台相关的消息转化为平台无关的消息,再通过NativeWidgetDelegate回调出来。而NativeWidgetDelegate除了接收回调以外,他还有很多用以指定原生窗口风格的回调函数,供NativeWidgetPrivate创建时调用。

这些处理过后的消息,就可以被统一来处理了。这里Widget本身作为NativeWidgetDelegate接收并处理这些消息或者分发给所有的控件,也就是马上要提到的View,由这些控件来触发真实的逻辑。

chrome-native-widget:
chrome-native-widget

这里我们可以发现一件事情:那就是为什么没有看到mac平台下的NativeWidget呢?答案估计你也猜到了:那就是。。。。mac下面用cocoa重写了一套,貌似压根就没有实现widget。=.=|||

4. 界面元素:View

Chrome的开发者说:Windows既然没有自带好用的界面库,那我们就自己搞!所以在对原生窗口抽象完毕之后,接下来的工作就是搭建自己的界面了。这个就是View
其相关的代码主要分布在src/ui/viewssrc/ui/views/window目录下。

4.1. Windows原生窗口的特征

我们先回忆一下,在开发原生的Windows程序时的窗口结构:
程序一般都有一个主窗口,主窗口下面有子窗口或者各种控件,他们形成一个树形的关系,这些我们在Spy++里面可以很好的观察到。
另外一个窗口的内容实际上分成两个部分:

  • 非客户区:一个窗口只有一个非客户区,这部分包括标题栏,关闭按钮等等
  • 客户区:这部分包括很多内容,按钮,工具条等等我们用到的控件

通过这些窗口和控件,我们搭建起了程序的主界面。
我们应该能想到,chrome要干的也是这件事情,所以现在我们不用看chrome的源代码,也能将他里面的代码猜个大概。

4.2. Chrome的实现

现在让我们来看Chrome是如何实现的。
为了方便理解各个不同的类的职责,我们首先来看看最后的层级关系,对照着这个关系来看代码。在Chrome的代码里面有一副很GEEK的字符图很形象的表示了这个关系:

chrome-view-hierarchy:
chrome-view-hierarchy

在Chrome里面,各种不同的View成树形的关系组织在一起,他们的根节点就是Widget,Widget接收到系统原生的消息,并通过RootView将消息分发给下层的View,这里Widget和RootView是一一对应的。

下层的View主要分为三种:

  • 用于表示整个窗体非客户区的NonClientView,负责NCHitTest和设置窗口边框大小。他也是其他两种View的父,原因很简单:他管着整个窗体的边框,所以其他的View必须是他的子。
  • 用于表示非客户区的内容的NonClientFrameView,负责绘制非客户区里面的元素,如标题栏,关闭按钮等等。
  • 用于表示客户区和其内容的ClientView,负责生成各种窗口元素。

另外Chromium还提供了几种不同的默认的非客户区方便编程:

chrome-views:
chrome-views

通过这样的一个关系,chrome将所有的界面元素都管理了起来。

5. 绘制封装:gfx

在封装好了界面元素之后,如何实现跨平台统一的绘制呢?这就是gfx要做的事情。
其相关代码主要分布在src/ui/gfx目录下。

gfx里面其实封装了不少和界面绘制相关的内容,其中最重要的就是Canvas。
为了实现跨平台的界面绘制,Chrome定义了一个Canvas的接口,来进行绘制的操作。我们在View的接口中可以看到一个View::Paint的函数,这个函数就是主要来控制绘图的。我们拿Windows来举例,窗口绘制的回调逻辑主要分如下这么几步:

  1. 在原生窗口收到了WM_PAINT消息之后,NativeWidget会对其进行处理,将其转化为Chrome内部的事件,并利用系统原生的绘图方式生成Canvas,回调给Widget进行分发。
  2. Widget在Widget::OnNativeWidgetPaint中将消息分发给其对应的RootView,由其分发给自己和各个子View。
  3. 每个View在自己的View::OnPaint函数中进行重绘。

为了实现Canvas,Chrome使用Skia作为其2D图形渲染库,来接管所有图形的绘制。而绘制文字的部分,则在不同平台上使用其原生的Api来实现,如在Windows上,则使用Api DrawText进行绘制。

chrome-ui-canvas:
chrome-ui-canvas

另外在gfx里面还有一个很重要的类,叫做NativeTheme,这个类中保存这当前系统的主题设置,甚至还可以用它来直接画系统默认的一些风格样式。比如:Windows窗口中右下角的表示窗口可以拖拽的小三角。在Windows下,NativeThemeWin优先会使用uxtheme.dll提供的Api进行风格绘图,如果没有这个dll,chrome会使用自定义的风格进行绘制。

chrome-native-theme:
chrome-native-theme

6. 布局策略:LayoutManager

写过界面的人都知道,皮肤布局是一件很繁琐的事情,每个元素如何排布,可能都有其各自的策略,而且每个窗口所包含的元素也不尽相同,所以chrome中可以为每一个View创建了一个专门用于控制布局的LayoutManager。这其实是一个典型的策略模式,将复杂且多变的布局封装起来。
其相关代码主要分布在src/ui/views/layout目录下。

在layout目录中,可以发现Chrome还提供几种不同的布局策略:

  • FillLayout,用于将第一个子View保持和当前View一样大的策略。
  • GridLayout,将子View排布成表格状。
  • BoxLayout,排布成一个贴一个的格子。
chrome-ui-layout:
chrome-ui-layout

现在我们可以猜到,RootView肯定使用的是FillLayout,从而让NonClientView永远保持和其本身一样大。
当然一个View也可以没有LayoutManager ,这样除非你重载View的Layout函数,或者使用其他的方法来主动布局,不然里面的元素就不会布局了。

7. 焦点管理:FocusManager

一旦所有界面元素都自己来管理了,那么很明显,这些元素的焦点也就需要自己来管理了。关于焦点的相关代码主要分布在src/ui/views/focus目录下。

7.1. 焦点问题的类型

在看焦点管理的时候,我们需要先意识到一个问题,焦点虽然说起来简单,谁接收鼠标事件谁就是窗口的焦点,但是对于Chrome这种DirectUI的皮肤引擎来说,焦点分为两种类型:

  • 原生窗口的焦点:原生的窗口在被点击的时候会被赋予焦点,皮肤引擎必须能够很好的响应这些事件。
  • 窗体中元素的焦点: 对于窗口中的所有元素,由于他们都不包含句柄,所以的焦点和键盘消息需要Chrome自己来实现转发。

为了实现上面两种类型的焦点,Chrome建立了一个专门用于管理焦点的类:FocusManager。Chrome会为每一个Widget建立一个对应的FocusManager。利用他来处理这两种焦点问题。

7.2. 和焦点有关的消息的分发

和焦点有关的消息分发流程主要包含这么几步:

  1. 当一个原生窗口在有焦点的状态时,系统会将发生的键盘和部分鼠标输入交给这个窗口来处理。
  2. 在窗口收到消息时,NativeWidget会首先处理这些消息并将其转化为KeyEvent,交给Widget来处理。(NativeWidgetWin::OnKeyEvent
  3. Widget将此消息转交给他所对应的RootView由他来分发消息。(Widget::OnKeyEvent
  4. RootView从当前Widget所对应的FocusManager中获取出当前的焦点窗口分发消息。(RootView::OnKeyEvent
  5. 焦点窗口处理消息。

7.3. 焦点变化的处理

在Widget接收到原生窗口的焦点变化的时候(Widget::OnNativeFocus),他会回调WidgetFocusManager来广播焦点变化的事件,但是从皮肤引擎的代码里面来看,默认的,没有类会关心这个事件。

对于窗体元素的焦点,如果某个元素获取了焦点,那么这个元素对应的View会调用当前View所在Widget的FocusManager::SetFocusedView方法,将自己设为焦点。此时FocusManager也会将这个消息广播给其他关心焦点变化的事件的监听者。但是在View里面只有DialogClientView看上去比较关心这个事件。

7.4. 焦点与控件显示的关系

我们发现,这些焦点变化的消息居然没有人关心?那么焦点是怎么影响控件显示的呢?
这里会涉及到两个不同的,但是容易混淆的概念:FocusActive。这里有一个较为简单的区分这两个概念的方法,当然不一定完全对:

  • Focus:可以是非顶层窗口,主要影响键盘鼠标等消息的接收。
  • Activate:必须是顶层窗口,影响窗口绘制。

当我们点击非Chrome窗口导致Chrome窗体发生颜色变化,这个主要是由于Active消息对应的处理。
当地址栏在可以输入时会出现一个边框,这个是由Focus来控制的。这个控制Chrome其实实现很简单,通过判断FocusManager中的FocusedView是不是自己来进行不同种类的绘图。

8. Chrome皮肤引擎总结

到此为止Chrome皮肤引擎的基础设施算是基本写完了。总的来说,这一套皮肤引擎算是一个比较容易理解的跨平台的DirectUI设计了。

在这一整套基础设施上,Chrome开始搭建起自己的一套控件库,再在这些内容的基础上搭建起自己的主界面。这些后续再继续写。

 

Chrome学习笔记(一):线程模型,消息循环

原创文章,转载请注明:转载自Soul Apogee
本文链接地址:Chrome学习笔记(一):线程模型,消息循环

看Chrome已经有一段时间了,但是一直都没有沉淀些内容下来,是该写写笔记什么的了,免得自己忘记了。看的都是Windows平台下的代码,所以记录也都是记录的。。。废话。。
那么首先,先从最基础的东西记录起吧:Chrome的线程模型和消息循环。

多线程的麻烦

多线程编程一直是一件麻烦的事情,线程执行的不确定性,资源的并发访问,一直困扰着众多程序员们。为了解决多线程编程的麻烦,大家想出了很多经典的方案:如:对资源直接加锁,角色模型CSPFP等等。他们的思想基本分为两类:一类是对存在并发访问资源直接加锁,第二类是避免资源被并发访问。前者存在许多问题,如死锁,优先级反转等等,而相对来说,后者会好很多,角色模型,CSP和FP就都属于后者,Chrome也是使用后者的思想来实现多线程的。

Chrome的线程模型

为了实现多线程,Chrome思路是简单且尽可能的少用锁,所以它在实现中并没有使用如角色模型之类的复杂的结构,而只是引入了自己的消息循环作为多线程的基础。它足够简单,方便使用,而且很容易实现跨平台。
相比平时的消息循环(如:Windows的消息循环,Linux中的epoll模型),它唯一增加的功能就是可以运行自定义的任务:Task。
如果在一个线程里面需要访问另一个线程的数据,则把接下来要运行的函数和参数包装成一个Task,并将其传递给另外一个线程,由另外一个线程来执行这个Task,从而避免关键数据的并发访问,而又因为任务执行是有顺序的,这样就保证了代码执行的确定性。
chrome-messageloop-task-simple:
chrome-messageloop-task-simple
其实,这就是一个典型的Command模式,而通过这个模式,简单的在线程之间传递Task,就实现了Chrome的多线程模型。

Task

1. Task
为了统一所有消息循环中的任务调用方式,所有的任务的基类都是这个Task类,他唯一的方法就是run(),MessageLoop只需要调用这个虚函数即可。
如果为了简化大家的开发,Chrome可谓下足了功夫,光是一个Task,就提供了各式各样的派生类供大家使用,并提供了良好的实现。

  • 派生出来的Task有:CancalableTask,ReleaseTask,QuitTask等等。
  • 根据调用条件的不同,将Task又分为即时处理的Task、延时处理的Task和Idle时处理的Task。
  • 为了简化开发,还引入了RunnableMethod,封装对象的方法,减少我们自己实现Task的时间。
  • 调用PostTask时,还需要传入一个TrackedObject,用于追踪Task的产生位置,为调试做准备。

2. RunnableMethod
RunnableMethod是一个很非常有用的类,这个方法通过模版将一个对象和他的方法和参数封装成一个Task,抛入另外一个线程去工作,其中为了保证对象的生命周期,对象的指针必须有引用计数,如果这个Task跨线程调用的话,这个引用计数必须是线程安全的。参数则通过Tuple来进行封装。在Task执行的时候通过另外一个模版将Tuple解开成参数即可。

线程和消息循环

Chrome将其线程分为了三类:普通线程,UI线程和IO线程。他们之间的区别是:

  • 普通线程:只能执行Task,没有其他的功能。
  • UI线程:所有的窗口都需要跑在UI线程上,它除了能执行Task以外,还能执行和界面相关的消息循环。
  • IO线程:和本地文件读写,或者网络收发相关的操作都运行在这个线程上,它除了能执行Task以外,还能执行和IO操作相关的事件回调。

由于这三类线程中Task的执行部分基本是一样的,而其他的功能却完成不同,为了实现这不同的三类线程,Chrome将消息循环分成了两个部分:MessageLoop和MessagePump。
chrome-thread-and-messageloop:
chrome-thread-and-messageloop
MessagePump被提取出来负责执行Task的时机和处理线程本身的消息 ,如:UI线程的Windows消息,IO线程的IO事件。
MessageLoop则仅仅是做Task的管理,它实现了MessagePump的Delegate的接口,这样MessagePump就可以告诉MessageLoop何时应该处理Task了。
另外实现上虽然Chrome为这三种线程实现了三套MessageLoop,但是它们之间的区别,也仅限于暴露出现的MessagePump的接口不同而已。
chrome-messageloop-class-diagram:
chrome-messageloop-class-diagram

消息循环之MessageLoop

1. 减少锁的请求
一般我们在实现任务队列时,会简单的将任务队列加锁,以避免多线程的访问,但是这样每取一次任务,都要访问一次锁。一旦任务变多,效率上必然成问题。
Chrome为了实现上尽可能少的使用锁,在接收任务时用了两个队列:输入队列和工作队列。
当向MessageLoop中内抛Task时,首先会将Task抛入MessageLoop的输入队列中,等到工作队列处理完成之后,再将当前的输入队列放入工作队列中,继续执行。
chrome-messageloop-task:
chrome-messageloop-task
这样,就只有当将Task放入输入队列时才需要加锁,而平时执行Task时是无锁的,这样就减少了对锁的占用时间。

2. 延时任务
为了实现延时任务,在MessageLoop中除了输入队列和工作队列,还有两个队列:延迟延迟任务队列和需在顶层执行的延迟任务队列。
在工作队列执行的时候,如果发现当前任务是延迟任务,则将任务放入此延迟队列,之后再处理,而如果发现当前消息循环处于嵌套状态,而任务本身不允许嵌套,则放入需在顶层执行的延迟任务队列。
一旦MessagePump产生了执行延迟任务的回调,则将从这两个队列中拿出任务出来执行。

消息循环之MessagePump

MessagePump是用来从系统获取消息回调,触发MessageLoop执行Task的类,对于不同种类的MessageLoop都有一个相对应的MessagePump,这是为了将不同线程的任务执行触发方式封装起来,并且为MessageLoop提供跨平台的功能,chrome才将这个变化的部分封装成了MessagePump。所以在MessagePump的实现中,大家就可以找到很多不同类型的MessagePump:如MessagePumpWin,MessagePumpLibEvent,这些就是不同平台上或者不同线程上的封装。

Windows上的MessagePump有三种:MessagePumpDefault,MessagePumpForUI和MessagePumpForIO,他们分别对应着MessageLoop,MessageLoopForUI和MessageLoopForIO。

下面我们从底层循环的实现,如何实现延时Task等等方面来看一下这些不同的MessagePump的实现方式:

1. MessagePumpDefault
MessagePumpDefault是用于支持最基础的MessageLoop的消息泵,他中间其实是用一个for循环,在中间死循环,每次循环都回调MessageLoop要求其处理新来的Task。不过这样CPU还不满了?当然Chrome不会仅仅这么傻,在这个Pump中还申请了一个Event,在Task都执行完毕了之后,就会开始等待这个Event,直到下个Task到来时SetEvent,或者通过等待超时到某个延迟Task可以被执行。

2. MessagePumpForUI
MessagePumpForUI是用于支持MessageLoopForUI的消息泵,他和默认消息泵的区别是他中间会运行一个Windows的消息循环,用于分发窗口的消息,另外他还增加了一些和窗口相关的Observer等等。
各位应该也想到了一个问题:如果在某个任务中出现了模态对话框等等的Windows内部的消息循环,那么新来的消息应该如何处理呢?
其实在这个消息泵启动的时候,会创建一个消息窗口,一旦有新的任务到来,都会像这个窗口Post一个消息,这样利用这个窗口,即便在Windows内部消息循环存在的情况下,也可以正常触发执行Task的逻辑。
既然有了消息窗口,那么触发延时Task的就很简单了,只需要给这个窗口设置一个定时器就可以了。

3. MessagePumpForIO
MessagePumpForIO是用于支持MessageLoopForIO的消息泵,他和默认消息泵的区别在于,他底层实际上是一个用完成端口组成的消息循环,这样不管是本地文件的读写,或者是网络收发,都可以看作是一次IO事件。而一旦有任务或者有延时Task到来,这个消息泵就会向完成端口中发送一个自定义的IO事件,从而触发MessageLoop处理Task。

测试一下ScribeFire

Firefox越来越臃肿了,现在开始转投到Chrome的阵营了,发现用起来实在是快!非常的爽!赞一个。

发现自己更新博客的频率非常慢,所以不得不思考自己为什么会发这个懒筋,实际上,在微博上,感觉自己还是挺活跃的。最后得到的结论就是,现在已经习惯吃快餐的我已经非常非常懒了,懒到连登录到博客后台对我来说都是一个不小的开销。所以需要一个更加轻量的写博客的工具来帮我完成这个任务。于是,我找到了ScribeFire,这个是原来FF上的一个插件,所以功能肯定不会弱才对。现在就先发个测试帖,以后想写博客的时候,在浏览器中按一个按钮,就什么都搞定了!这种感觉肯定大爽!