Mirror v0.2 Preview 2 发布 —— Windows开发辅助工具

在发布了0.1 Preview版本之后,又折腾了一个月,Mirror v0.2 Preview 2终于诞生了。
这一次的发布主要是让界面可以自由缩放了,这样能适当的缓解一些界面太大导致的调试困难。另外还增加了一些小功能,并且修复了一些bug。
由于加入的部分功能在64位的机器上只有64位的API才能正常工作,所以之后的Mirro可能都将发布32位和64位两个版本,后面我会尽量想办法将两个版本合并。
欢迎您点此乱入反馈各种意见和问题,或者点此捐助作者鸡蛋灌饼,支持作者继续开发~

下载地址

Mirror v0.2 Preview 2 (0.2.69.102)
项目地址:https://bitbucket.org/bigasp/mirror
打包下载 (推荐):Mirror-0.2.69.102 (15)
下载地址:Mirror-0.2.69.102-x86 (15) (32位) | Mirror-0.2.69.102-x64 (14) (64位)

主要修改

2012-05-12
紧急修正了两个较为严重的问题:
- 拖拽窗口时,如果控件很多,则非常的卡 (#33)
- 窗口最小化时退出程序,之后将再也无法正常显示主界面,除非删除配置文件 (#32)

2012-05-11
新增功能:

- 由于一些API在64位系统上需要64位程序才能正常使用,所以将程序分为32位和64位两个版本
- 界面终于能缩放大小了,并在关闭的时候自动保存其位置
- 状态栏添加反馈问题的快捷入口
- 刷新窗口树时,自动定位到当前观察的窗口
- 保存当前窗口列表中显示的窗口树
- 增加一些新的窗口高亮:
- 高亮窗口中选中的窗口
- 高亮当前正在观察的窗口
- 获取窗口Unicode属性
- 发送窗口消息支持Send和Post两种,并且支持发送自定义的消息
- 在Vista之上的系统,可以检测窗口所在线程的等待链
- 增加了一些新的模拟鼠标输入
- 添加程序日志Tab

修正Bug:
- 修正无法获取进程外窗口的窗口过程的问题 (#19) (通过注入代码解决,在窗口失去响应的时候,可能会有一些不稳定)
- 修正模拟消息输入WPARAM和LPARAM永远为0的Bug (#31)
- 修正没有选中窗口,就不可以刷新部分全局的数据 (#16)
- 其他一些小Bug和崩溃

软件截图

支持自动布局的界面,想摆多大弄多大
mirror-auto-layout:
mirror-auto-layout

以后会陆续完善各种Log,让你不丢失调试中的各种信息
mirror-log:
mirror-log

窗口列表随时存,Dump回去查问题
mirror-save-window-list:
mirror-save-window-list

窗口消息任意发,窗口阻塞不着急(等待链检测需要Vista以上的系统支持)
mirror-window-op:
mirror-window-op

软件历史

Mirror v1.0 Preview (0.1.21.101)

新增功能:
- 查看/修改窗口基本信息
- 查看窗口所在的线程信息
- 查看/高亮显示/设置前景窗口,焦点窗口,激活窗口
- 查看窗口类信息
- 查看/修改窗口Prop
- 发送窗口消息,并提供一些常用的窗口相关的Api调用
- 模拟鼠标,键盘输入
- 查看/修改DWM信息,可以自己设置几乎所有的窗口DWM的属性
- 基本上面提到的所有的功能,都可以对其设置热键,甚至包括切换软件中的Tab
- 应用设置之后,新的热键会被自动保存在mirror.ini配置文件中
- 每隔一天自动检查更新
- 等等等等

更多信息可参阅项目首页:https://bitbucket.org/bigasp/mirror

Mirror v0.1 Preview 发布 —— Windows开发辅助工具

由于前段时间经常在各种奇奇怪怪的窗口相关的问题上纠缠着,而spy++的功能又总是不那么尽如人意,所以就在业余的时间,写了这么一个非常简单的小工具,能查看和修改窗口的一些信息,方便测试窗口,避免每实验一下,都要重新编译工程带来的痛苦。

下载地址

Mirror v0.1 Preview (0.1.21.101)
下载地址:Mirror-0.1.21.101 (45)
项目地址:https://bitbucket.org/bigasp/mirror

基本信息

Mirror的目标是成为一个足够绿色,对开发人员足够友好的观察窗口的行为的小工具。
现在的Mirror还很简单,只有两个主要功能:窗口操作和热键管理,希望他以后会慢慢强大起来。
但是Mirror是一个单纯的孩子,它并不想成为Spy++,所以暂时不会添加关于Hook窗口消息的功能。

软件主要包含如下文件:
mirror.exe:软件主程序
mirror.ini:软件配置文件,在软件第一次启动时会自动生成在exe所在目录下。

本来打算将配置文件生成在appdata目录下,但是这个软件本来只是一个很小的工具,实在没有必要。

如果有兴趣的,也可以点击下面的捐赠页面,让作者每天都能吃上好吃的鸡蛋灌饼,帮我增加继续下去的动力。
捐赠页面:https://me.alipay.com/bigasp

关于开源和反馈

代码写的实在难看,这里就不把源代码开出来了,里面其实就是调用了一些窗口相关的Api而已。
欢迎各位大大指点小弟,让我更好的完善这个小软件。
Mirror将永远免费,所以就请大家不要随意修改它了,如果有任何建议,可以随时向作者留言,或邮件反馈,或者在mirror项目中新建issue,我会尽快修正~

主要功能

软件现在主要包括两个功能:窗口操作和热键管理。

窗口操作

  • 查看/修改窗口基本信息
  • 查看窗口所在的线程信息
  • 查看/高亮显示/设置前景窗口,焦点窗口,激活窗口
  • 查看窗口类信息
  • 查看/修改窗口Prop
  • 发送窗口消息,并提供一些常用的窗口相关的Api调用
  • 模拟鼠标,键盘输入
  • 查看/修改DWM信息,可以自己设置几乎所有的窗口DWM的属性

热键管理

  • 基本上面提到的所有的功能,都可以对其设置热键,甚至包括切换软件中的Tab
  • 应用设置之后,新的热键会被自动保存在mirror.ini配置文件中。

软件截图

主界面,外加可以各种过滤的窗口树,支持自动刷新,方便调试Z-Order相关的问题
mirror-main:
mirror-main

窗口信息,可以看也可以改。注意左上角的猫咪,哪里想看,点哪里~妈妈再也不用担心窗口问题了~
mirror-window-info:
mirror-window-info

窗口高亮,焦点焦点你在哪里?
mirror-window-highlight:
mirror-window-highlight

窗口操作,发送消息,到底是什么消息让我欢喜让我忧~
mirror-window-op:
mirror-window-op

热键设置,选择窗口时,不变化前景窗口,菜单窗口也能看
mirror-hotkey:
mirror-hotkey

后期的计划

1. 优化软件界面,支持界面更改布局,缩小软件的窗口大小
2. 支持自动元素重排,进一步缩小软件窗口大小
3. 添加独立的追踪模式,还是为了缩小软件的窗口大小,尽可能避免对调试的打扰
4. 弄一个英文版啥的,主要是担心一些专有名词翻译不正确,从而误导开发人员
5. 等等等等……

更多的计划和进度,可以关注项目的issues:https://bitbucket.org/bigasp/mirror/issues?status=new&status=open

一次好玩的扯淡——在C++中实现Objective-C的categories

原创文章,转载请注明:转载自Soul Apogee
本文链接地址:一次好玩的扯淡——在C++中实现Objective-C的categories

说明啊说明:本人纯属小菜,对Objective-C神马的完全的不懂,这篇文章纯属扯淡,各位大大路过,欢迎各种指教~

今天一个同事对我说,他花了两个晚上的时间,啃完了Objective-C。我表示非常震惊,这种速度,应该是为了避免跟不上工作进度,穿越回来的时候选早了几天。于是接着我们就围着Objective-C侃了起来。

他说他看到Objective-C里面的一些特性非常好玩,于是说给我听,比如:
这是一个很神奇的特性,虽然我完全没有看过Objective-C,但是听大致的描述是这个意思:只要我们拥有某个类的头文件和其对应的静态链接库,那么我们就可以对这个类进行扩展,比如,给这个类加一个成员函数。

这种用法,对于基本只在C++里面转圈的我来说,确实也是第一次听到,觉得相当新鲜,于是就开始想了:C++里面是不是也能做到这件事情呢?
让我们来开始尝试吧!给一个名为Test的类添加名为Func1的成员函数。

静态链接下的类扩展

我们知道,静态链接生成的.lib文件,实际上可以看成是一个.o文件的集合。而编译器在链接的时候,实际本质上只是在尝试连接名称匹配的Symbol
那么这样看起来加一个成员函数似乎就变成一件很容易的事情了。

我们首先来建两个工程:

  • SimpleStaticLib:Static Library,用于生成我们要扩展的静态链接库。
  • ExpandStaticLib:Console类型的Exe,用于实验类扩展的结果。他依赖于SimpleStaticLib工程。

最终样子如下:
expand-static-lib-sln:
expand-static-lib-sln

然后给SimpleStaticLib添加一个简单的Test类:

test.h

#pragma once

class Test
{
public:
	Test();
	~Test();

	void Func0();

private:
	int a;
};

test.cpp

#include "stdafx.h"
#include "test.h"
#include <stdio.h>

Test::Test()
: a(1)
{

}

Test::~Test()
{

}

void Test::Func0()
{
	printf_s("Test::Func0! a = %d\n", a);
}

在ExpandStaticLib中里面,我们按照如下方法给Test类添加一个Func1的函数,代码如下:

main.cpp

#include "stdafx.h"
#include <stdio.h>

class Test
{
public:
	Test();
	~Test();

	void Func0();
	void Func1();

private:
	int a;
};

void Test::Func1()
{
	printf_s("Test::Func1! a = %d\n", a);
}

int main()
{
	Test o;
	o.Func0();
	o.Func1();

	return 0;
}

编译,运行,我们可以发现,成功了!
expand-static-lib-result:
expand-static-lib-result

原因分析

刚刚我们也提到,Link的本质是链接同名的符号,那么这些符号在真正编译器里面是个什么样子呢?我们来一探究竟。

在Windows上,我们可以使用dumpbin来查看Obj和Lib文件中的具体内容。

dumpbin /SYMBOLS SimpleStaticLib.lib

在SimpleStatictest.lib中,我们可以找到Test类的成员函数的符号。
simple-static-lib-symbols:
simple-static-lib-symbols

在ExpandStaticLib工程的Debug目录下,我们可以找到main.obj,在里面我们可以找到对Test::Func1的引用。
main-symbols-1:
main-symbols-1

我们再来看对Test其他函数的引用。
main-symbols-2:
main-symbols-2

我们可以看出来,这两个符号是不同的,因为Test::Func0的实现是在main.o的外部实现的,所以就没有后面的Section信息,而Test::Test则相反,是一个内部实现,在后续的Section中就可以看到其最后编译出的代码长度。
在链接的时候,链接器会加载所有的.lib和.obj文件,然后在其中查找各自的符号和其对应的实现,最后将他们链接在一起并最终输出成可执行文件。
当然,我们也可以使用link命令简单的来试着生成最后的可执行文件。

link SimpleStaticLib.lib ../ExpandStaticLib/Debug/main.obj

动态链接下的类扩展

那如果我们使用动态链接还可以实现这种效果吗?二话不说来试一试吧。
首先,再建立两个工程:

  • SimpleDynamicLib:和SimpleStaticLib一样,只是是一个DLL的工程。
  • ExpandDynamicLib:和ExpandStaticLib一样,只是依赖于SimpleDynamicLib。

最后样子如下:
expand-dynamic-lib-sln:
expand-dynamic-lib-sln

然后,我们将Test的代码复制到SimpleDynamicLib工程中,将Main的代码复制到ExpandDynamicLib工程中。
接着,将两个工程中的Test类都加入__declspec(dllimport),如下:

class __declspec(dllimport) Test

最后,编译,执行。结果还是通过了!这里为了区分和上面的结果,我在输出中加入了”Dynamic”的文字。
expand-dynamic-lib-result:
expand-dynamic-lib-result

为什么呢?其实原因大家肯定都能猜个大概了。
在dll中导出类的实现,实际上是将类的成员函数全部变成导出函数,在生成的lib中实际上只保存了类的大小信息和成员函数的符号定义。
我们在depends中可以看到SimpleDynamicLib.dll的导出函数和导出变量。
simple-dynamic-lib-exports:
simple-dynamic-lib-exports

所以,在链接的时候,链接器根据同名符号进行链接,就依然是成功的了。

成员变量的扩展

最后,我们在来想想,我们能否对Test类中间的成员变量进行扩展呢?我们来修改一下我们的程序,给Test类加入一个int型变量b。
刚刚我们看到在动态链接和静态链接的时候,链接器做的事情本质上基本是一样的,所以为了方便观察和描述原理,我们这里修改动态链接的工程:ExpandDynamicLib中的Test类。

class __declspec(dllimport) Test
{
public:
	Test();
	~Test();

	void Func0();
	void Func1();

private:
	int a;
	int b;
};

void Test::Func1()
{
	b = 2;
	printf_s("Dynamic Test::Func1! a = %d, b = %d\n", a, b);
}

编译,运行,可以看到,这次也正常执行了!那这次实际上到底发生了什么事情呢?
expand-dynamic-lib-var-result:
expand-dynamic-lib-var-result

我们上面说到链接器的工作原理实际上是链接相同名称的符号,换句话说,就是他并不会修改除了符号信息之外的代码,在编译的过程中,编译出来的最终代码主要是由编译器来生成的。我们刚刚也看到了SimpleDynamicLib的导出函数,其中我们并没有发现这样的一个导出函数:申请类的空间。通过查看汇编代码,我们可以从中看到C++是怎样来处理的。
为了更好的看到C++的行为,在这里请使用Release来编译工程,并且禁用堆栈检查,避免他们对最后汇编代码的影响。
expand-dynamic-lib-var-rt:
expand-dynamic-lib-var-rt

我们可以看到,C++实际上是在ExpandDynamicLib.exe中申请了8个字节的内存,然后对其调用在SimpleDynamicLib.dll中的Test类的构造函数。
既然原理是这样,那么我们就可以想到扩展类成员变量的缺陷了:

  • 扩展的类成员变量无法进行正常的构造。
  • 如果我们在已有的类成员之前添加类成员,而不是添加到类成员的末尾,那么可能会引起对象构造/析构混乱。
  • 扩展类成员时,千万不可以删除原有的类成员,否则会造成构造/析构错误,操作对象以外的内存,造成更多的问题。
  • 无法扩展除了基本类型以外的类成员,因为这些对象是需要在构造函数中加入其成员变量的构造函数的调用的,而这些函数已经被编译进了动态/静态链接库中,无法修改了。
  • 多重继承和虚继承会让对象的内存结构变得更加复杂,所以在进行类成员扩展时,需要对类的内存分布有绝对的把握,否则很容易出问题。

总结

经过一番折腾,我们可以大致了解到一些C++编译器的一些工作原理,但是这个技术本身我们在平时写代码的时候最好不要使用。
如果需要增强其他人提供的Lib,其实还有很多的方式可供我们选择,在《重构——改善既有代码的设计》中,我们就可以找到两种较为简单的方式:

  • Introduce Foreign Method:引入外加函数
  • Introduce Local Extension:引入本地扩展

当然一切都是TradeOff,具体用什么方法来增强外部的代码,在平时写代码时,只要符合工程的需要就行。

Webkit终于准备将工程搬迁到git上去啦?

原创文章,转载请注明:转载自Soul Apogee
本文链接地址:Webkit终于准备将工程搬迁到git上去啦?

将版本库搬迁至git?这个提案在经历了三年之后,Webkit终于又将它摆上了台面,引起了众多人的热烈讨论,不知道这一次的结果如何。

如果想知道这件事情的更多进展,大家可以订阅webkit-dev的mail-list,或者去The webkit-dev Archives进行围观。

以下是翻译过来的wiki,原文地址如下:http://trac.webkit.org/wiki/Moving%20to%20Git
第一次翻译这种文章,感觉和看文章完全是两码事啊!感谢F同学在周末还帮忙纠正我这无可救药的中英文。

================ 我是华丽丽的分割线 ================

将Webkit搬迁到Git的提案

搬迁到Git的好处

  • 大量WebKit开发者已在使用git-svn。
  • 所有参与WebKit开发的主要机构中,已有大量开发者在使用git-svn。
  • Code Review会变得更加简单:
    [译者注:Code Review的好处非常明显,我想这应该也是Webkit想迁移到git的主要原因。关于代码审查,可以去围观一下bumblebee,这个项目由于一个失误名留青史,现在已经彻底关闭(改名?)了。]

讨论

可以将Bugzilla指向一个用于Code Review的私有分支,使Code Review更轻松,更彻底。

我不知道私有分支如何让Code Review更容易或更彻底。从审阅者的角度来看,过程几乎相同。

WebKit目前的普遍做法是使用两种不同的系统来进行Code Review – Pastebin或者将Patch提交到bugzilla。Pastebin的优势是很迅速,能瞬间满足用户需求,因为Code Review是在IRC中实时完成的,但是它的主要缺陷是评审意见和讨论很容易丢失。在Bugzilla上提交bug的方式则解决了这个问题,因为所有的评论都保存在bug中。但是这种方法不如Pastebin方便快捷。

与现时使用两种方法相比,采用Git的私有分支来进行Code Review有如下几个优势。首先,Git和Pastebin一样方便快捷,因为开发者只需要将他的私有分支推送出去,并将URL贴到IRC中,即可开始Code Review。其次,使用GitHub或Gitorious,私有分支能将审查意见记录下来以便后人查询。点击行号即可将评论和补丁内嵌的记录在一起,当开发者修改了分支,你的意见仍会保留,你可轻易查看到开发者是否已经根据你的意见进行了修正。更重要的是,与现行Bugzilla相比, Git私有分支另具以下优势。在Git的私有分支中,你不仅只是查看补丁,还可以在整个分支的上下文中来Review当前补丁。你可以查看补丁,查阅整个文件,翻看历史记录,以前的变更设置以及分支的所有开发历史。而且由于你拥有了分支的所有信息,你可开展任何研究。

最后,还有另外一点,使得Git让Code Review与现行方法相比,简易得多::你可以轻松检验、合并分支本身,并低成本进行实际修改测试。虽然理论上,我们目前的Code Review过程也可以做到,但是参与成本(下载补丁文件,清理自己的代码库,应用补丁,手动修复任何合并问题)大大降低了实际的可操作性。

点击这里可查阅更多信息:代码审查应该是自由的

作为补丁的作者,如果有评审者需要我提供某个已提交到分支的补丁修正信息以及在此之上的其它补丁,我应该怎么办?

我们想要制定出一套处理补丁发布分支的完整策略。其中最简单的处理方法就是将补丁分支当作一个临时分支。补丁的作者可以通过“git rebase -i”命令来改写原来的一系列补丁,然后通知Reviewer分支已修改。然后Reviewer再重新下载分支。不幸的是,由于分支历史发生了改变,他们根本无法将代码拉取到他们现有的分支上来,这是一个缺点。(但是,如果他们只是在网络上查看代码,而不是下载并且建立自己的代码副本,那么这对他们就不会有任何影响了,所以直接通过gitweb的进行Code Review将会变得非常常见)。另一个缺点是,这将覆盖补丁的修改历史,只留下最后的修改结果。

另一种选择是复制现有分支(成本很低),然后在新的分支上进行修改,再使用git-rebase,然后发布新的分支。对于这一点,我们需要为补丁审查分支建立一个标准的命名规则来避免名称混乱,如,在分支名称中包含迭代次数或者日期。

另一种可能性是使用“git revert”回滚掉当前的补丁和建立在它之上的所有补丁。补丁的作者可以对它们进行挑选,按照审查的意见进行修改,并以单独版本形式进行再次提交。

要回答这个问题的最好办法可能是尝试着安装GitHub、Gitorious或者其他Git的Web托管工具,并开始进行尝试,看什么效果最好。

evmar在描述Chrome’s git/ reitveld工作流程的文档中对这些问题进行了更加深入的探讨。

使用git可使我们转向基于git的代码审查工作流程,如Gerrit2。 Gerrit2是开源软件,并且正在被Android团队所使用。

  • 将使我们省去手动编辑ChangeLog文件、手动解决其冲突的步骤。

讨论

我相信将磁盘上的提交日志保存到ChangeLog文件的现行做法,是由于需要确保提交的日志也能被一并接受审查。而Git省去了这一多余步骤,因为将代码推送到私有分支上时会显示所有的提交信息,因此他们可以在被推送到主版本库之前被全部审查。如果发现某个提交存在缺陷,也很容易使用“git commit –amend”命令进行调整。
[译者注:这个我觉得是git给后续代码维护带来的一个很大的好处,历史提交都能保存在版本库里面,如果发生一些隐晦的错误,则很容易通过提交日志来定位错误。通过ChangeLog文件来维护代码,一是定位相关的代码比较麻烦,二是对于很多工程来说,忘记修改ChangeLog文件的问题比比皆是。]

  • git的分布式性质可以让开发者在服务器宕机时进行提交。
  • 那些远程工作和连接速度很慢的开发人员,可以使用本地Git镜像,而不用通过svn服务器进行提交(目前真实存在的问题)。
  • 使用git建立分支的代价很小,这将鼓励开发者组成小组来开发不稳定的特性。由于需要修改平台层的分支都可以在主干之外完成开发和测试,当所有的平台上的修改都可以合并到主干之后再进行合并,所以这也将有助于减少主干版本编译失败的次数。
  • 具有Git知识的开发人员人数众多,可对其他开发者进行培训。
  • Git已覆盖到许多工具、脚本之中。
  • 使用GitHub或者为Webkit设立我们自己的gitorious服务器将使得其他人更容易看到正在进行中的不同项目。
  • 请在这里填写…

迁移到Git的成

  • 不明数量的WebKit开发人员对Git还没有了解,可能要从头开始学习一个新的工具。
  • Git本身比较难学难用。
  • Git Hash使得回归/修订的范围更难理解。
  • SVN客户端无法兼容Git服务器。[译者:这个理由还真是让人想吐槽。]
  • 请在这里填写…

待解决的问题

  • Bugzilla的整合。
  • 需要制定出一个普遍推荐的工作流程。
  • 为那些不愿或无法学习GIT的开发者,提供其它选择?(Easy Git?)(更好的选择:git-cvsserver
  • 请在这里填写…

待办

  • 当前Git在Windows上的状态以及Windows上的开发人员尚未明确。
  • Git的最佳运行环境是cygwin,并且cygwin已经是Windows上编译webkit的必备条件。
  • 弄清楚我们有多少Git用户在为WebKit做积极贡献。(有分析显示,有20%的git用户?)
  • 了解我们有多少SVN用户,以及他们转移到git的感受。
  • 请在这里填写…

================ 我是华丽丽的分割线 ================

从这篇文章来看,使用git还是有不少好处的,难怪现在这么多开源的项目都使用git来管理代码。

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开始搭建起自己的一套控件库,再在这些内容的基础上搭建起自己的主界面。这些后续再继续写。

 

解决Chromium编译出现无法找到DOM和CSS相关文件的问题

原创文章,转载请标明出处:Soul Apogee (http://bigasp.com),谢谢。

这两天重新下了个最新的Chrome的源代码,想编译一下,结果发现怎么都编译不过,在编译webcore的工程时,报了一大堆与如下类似的错误:

error C1083: Cannot open include file: 'EventInterfaces.h': No such file or directory
error C1083: Cannot open include file: 'CSSValueKeywords.h': No such file or directory

问题原因

找了很久终于找到了问题的原因:
EventInterface.h和CSSValueKeywords.h这些文件本身是不存在在代码中的,而是根据对应的idl文件的描述,系统在对应的平台下动态生成的。大家可以在src/third_party/WebKit/Source下搜索到这些idl文件。

而在windows下,由于没有自带的perl环境,所以chromium会在third_party下加入cygwin的目录,并利用cygwin来执行perl和相关的脚本。
由于chromium运行cygwin的方式是将其bin目录设置到PATH环境变量中,从而运行其脚本,所以其中一些目录环境就没有配置,比如/usr/lib文件夹是对应的windows的哪个文件夹。而chromium编译时依赖的perl就在这个文件夹内。

所以总的来说,就是在现在的代码在编译的时候无法找到当前需要的perl脚本,所以就无法正常编译。

解决方法

既然已经找到问题的原因了,那么问题也就很好解决了。
在src/third_party/cygwin目录下,有一个setup_mount.bat的文件,运行这个文件,他会将cygwin相关的目录配置全部导入注册表内。这样perl就可以被正常执行了,那些文件也就可以被生成出来了,这个问题就算是初步解决了。

当然这只是一个临时的解决方案,因为这个修改是无法提交回chromium的,在原来的chromium的代码中,是存在一个叫做cygwin的工程来帮我们在每次编译前都自动运行setup_mount.bat的脚本的,但是现在这个工程消失了。

所以最好的方法其实应该是修改其gyp文件,修复windows下的工程信息,然后提交回chromium,但是我暂时还没有太看懂gyp文件,不知道怎么完全还原cygwin工程的配置,所以这个想法就暂时这么搁浅了。

chromium的网站上也有一个关于这个问题的issue,不知道什么时候才能有结果。现在看来只能静待佳音了。

Posted in 03 Binary Life. Tags: . 2 条评论 »

Mac OS X Lion Spotlight 优化指南

原创文章,转载请注明:转载自Soul Apogee
本文链接地址:Mac OS X Lion Spotlight 优化指南

Spotlight是一个让人又爱又恨的东西,一方面他确实相当好用,但是另外一方面,他也确实占用了不少系统资源,在进行大量文件操作的时候,经常将CPU占满,可以将我可怜的macbook pro烧到80度左右,所以不能不好好的折腾他一下了,这里总结一些这两天摸索出来的Spotlight调优的方法,希望能对大家也有些帮助。

本人使用Mac现在总时间还不超过一周,所以很多东西理解的可能是不对的(什么可能啊,是应该吧),有不正确的地方,还请大家多多指正。

以下优化是在Mac OS X Lion 10.7.2 的系统上实践出来的,其他版本的系统可能有一些不一样,大家可以Google一下具体的方法,但是基本步骤应该是差不多的。
如果没有大量的文件操作,Spotlight对系统的影响还是不大的,占用CPU一般是3% – 5%左右,没有太大必要进行优化。但是如果发现最近电脑温度很高,或者电池消耗很快,就可以检查一下Spotlight,参照这篇文章进行优化了。

Spotlight简介

Spotlight是Mac系统中的查找服务,为了能使查找更加的迅速,Mac系统会在后台运行一个索引服务,在有文件操作或者资料更新,如iTunes,Mail等,就会为他们建立一个索引,用于加速Spotlight的查找。
所以,Spotlight其实分为两个部分:前台Spotlight的查找服务Search.bundle,和后台的索引服务mds。
优化Spotlight,其实就是优化这两个服务。

基本优化方案:减少需要索引的内容

根据上面的介绍,那么我们可以想到,最简单的优化就是减少需要索引的内容。

首先,在Spotlight的设置中选择我们需要索引的内容。
打开系统偏好设置->Spotlight,在搜索结果这个Tab栏内,我们把所有我们不关心的内容全部勾选掉。
我只勾选了:应用程序,系统偏好设置,通讯录和音乐。这些对我来说够用了。
spotlight_search_result:
spotlight_search_result

然后,将我们不需要索引的文件夹都加入Spotlight的黑名单。
在系统偏好设置->Spotlight中的隐私Tab中,我们可以选择那些目录是Spotlight不用去索引的,我们可以利用这个目录来大大减少我们需要索引的文件量。
打开Finder,点击菜单中的前往->前往文件夹,转到根目录/, 将文件夹中除了应用程序和用户的目录,全部放入黑名单。

需要注意的是:
其实用户目录也可以放入黑名单,Spotlight中显示的音乐,联系人和邮件,其实是靠和这些软件数据库直接联系来建立索引的,所以和文件夹的索引没有太大的联系,也可以放入黑名单。这里看大家的喜好,我全放进去了。

另外还需要注意的是:这里需要在Finder里面显示所有隐藏文件,因为一些和系统相关的目录,如/usr,其实都没有索引的必要,都可以放入黑名单。如果显示文件的方法不会的话,大家可以去Google一下。
spotlight_privacy:
spotlight_privacy

最后,我们还需要再设置一下Spotlight需要索引的硬盘,一些移动硬盘和U盘什么的,其实就不需要索引了。
打开终端,输入下面的命令:

# 关闭所有磁盘的索引服务
sudo mdutil -a -i off

# 只打开主硬盘的索引服务,我的主硬盘被挂载到/Volumes/Macintosh HD目录下(默认设置),大家可以根据具体情况修改。
sudo mdutil -i on "/Volumes/Macintosh HD"

如果上面的指令执行错误,可以使用如下命令来恢复对所有磁盘的索引。

sudo mdutil -a -i on

好,到此我们已经完成了第一步优化了。
此时大家可以看到Spotlight在重建索引了,在我的机器上,原来完全建立一次索引需要2小时,现在只需要几分钟了。

替换优化方案 I:替换Spotlight的前端部分

OK,在完成基本优化方案之后,既然我们已经将Spotlight阉割成了这般田地。那我们干脆换掉Spotlight使用其他的软件替代好了。
这里有一个不错的免费软件推荐:Alfred。大家也可以直接在App Store中搜索安装即可,过程就不赘述了。

隐藏任务栏中的Spotlight图标
这个方案问题的关键在于:既然Spotlight都不需要了,我们也就不需要显示它了。
关闭它的方法很简单,在终端里面输入如下指令即可:

sudo mv /System/Library/CoreServices/Search.bundle /System/Library/CoreServices/Search2.bundle
ps aux | grep SystemUIServer | grep -v grep | awk '{print $2}' | xargs kill

现在我们就会发现桌面上的Spotlight图标不见了,这一步优化也就宣告完成了。

当然想要恢复也很简单,输入如下命令即可恢复:

sudo mv /System/Library/CoreServices/Search2.bundle /System/Library/CoreServices/Search.bundle
ps aux | grep SystemUIServer | grep -v grep | awk '{print $2}' | xargs kill

替换优化方案 II:替换整个Spotlight

完全停止Spotlight的意思是不仅仅关闭掉Spotlight的桌面搜索,还停止掉Spotlight的后台索引服务。
所以友情提醒:前方有怪兽!将Spotlight关闭之后,依赖于Spotlight的索引服务的软件将无法正常运行,如Alfred将无法显示搜索结果(依赖于Spotlight索引服务mds),请小心处理,如果引起诡异的问题,与笔者无关。

这个方案是我暂时最喜欢的,因为在系统中Spotlight的索引服务还是随时在运行的,所以它依然会占用我们的系统资源,而我们又无法控制,完全替换他,才是我们最好的选择。
好,让我们开始吧。

完全停止Spotlight后台索引
在完成替换优化方案I之后,我们可以输入如下命令来完全停止Spotlight的索引。

sudo launchctl unload -w /System/Library/LaunchDaemons/com.apple.metadata.mds.plist

在输入完这条指令之后,大家可以查看一下当前系统内是否还存在mds和mdsworker的进程。他们应该都退出了。
至此,Spotlight算是基本被我们给干掉了。

当然,如果我们后悔了,我们也可以通过如下命令来恢复。

sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.metadata.mds.plist

恢复完成之后,不要忘了检查基本优化方案里面的内容哦,因为停止后台索引服务后,索引工作也停止了。

安装替代软件
那在没有后台索引服务的情况下,我们还能不能享受到和Spotlight这种类似的功能呢?
这里再推荐一个小软件:QuickSilver
这款软件里面会建立自己的索引,虽然就功能而言,不及Alfred强大,但也已经相差无几,对于图片,视频的索引是否有必要,个人还是持保留态度的,原因相信大家都懂的,所以基本上对我来说已经够用了。
在这个软件里面,我们可以自己设定软件的索引范围,或者禁止它自动更新索引,这样,就可以让我们最大程度上的控制这个后台服务了。

总结

到此为止,整个Spotlight的优化算是完成了,我可怜的mbp温度终于降低一些了,而且也不会因为大量文件操作导致温度飙升了。总的来说,折腾还是有些效果的,希望能对大家也有些帮助吧。

解决QT在VS下中文乱码的问题

我们用VS编译出来的QT程序,经常会遇见中文乱码,其实解决方法很简单:
在main函数所在文件的包含如下文件:

#include <QtCore/QTextCodec>

然后,在main函数的开头加入这么一句话就可以了。

QTextCodec::setCodecForTr(QTextCodec::codecForName("GB2312"));

之后在调用的时候,所有的字符串记得使用tr()函数包裹起来。比如:

QMessageBox::warning(this, tr("出错啦!"), errorInfo, tr("OK"));

这个是因为VS默认的编码是GB2312,而函数tr的作用,是将默认的字符串转换成QT内部的编码:Unicode,所以我们这里只需要设置好编码即可。
如果碰到一些2312里面没有的生僻字,我们也可以尝试使用Windows默认的多字符编码GB18030来解决。

Posted in 03 Binary Life. Tags: , , . 6 条评论 »

如何正确使用C++多重继承

原创文章,转载请注明:转载自Soul Apogee
本文链接地址:如何正确使用C++多重继承

C++多重继承一直是一个让人搞不太清楚的一个问题,但是有时候为了实现多个接口,多重继承是基本不可避免,当然在Windows下我们有强大的COM来帮我们搞定这个事情,不过如果你想自己实现一套类似于COM的东西出来的时候,麻烦事情就来了。

在COM里面,有两个很基础的,而且我们都会用到的特性:
1. 纯虚接口:一般使用一个只有纯虚函数的类来作为接口
2. 引用计数:在C++中一般都使用引用计数来管理对象的生命周期

这两个功能在一般设计C++接口的时候也经常用到。其实说到底,上面这两个特性牵扯到的是多重继承的二个表现:
1. 多重继承中的数据存储
2. 多重继承中的虚函数

在COM中,纯虚接口是使用的interface来定义的,引用计数是通过IUnknown接口来实现的,所有的接口都是从IUnknown这种接口中派生出来的。当我们需要某一个类实现多个接口的时候,就经常会遇见这样的多重继承:
multi-inheritance-com:
multi-inheritance-com
哦?!是不是很眼熟,ios,istream,ostream,iostream。。各种C++书籍最喜欢用的一个示例。好吧,现在我们先自己实现一个吧,看看到底要怎么使用多重继承。

多重继承中对象的的数据存储

#include <stdio.h>

class IBase
{
public:
    IBase() : n(0) {}
    virtual ~IBase() {}
    void show() { printf("%dn", n); }
    int inc() { return ++n; }
    int dec() { return --n; }

protected:
    int n;
};

class IA : public IBase
{
public:
    virtual ~IA() {}
};

class IB : public IBase
{
public:
    virtual ~IB() {}
};

class CImpl : public IA, public IB
{
public:
    virtual ~CImpl() {}
};

int main(int argc, char* argv[])
{
    CImpl o;
    IA *pA = &o;
    IB *pB = &o;

    pA->inc();
    pA->show();

    pB->dec();
    pB->show();

    return 0;
}

编译,OK,成功了!好,运行试一试。
run-result-1:
run-result-1
为什么是1和-1呢?明明n只在继承的一个类IBase里面有,一次加1,一次减一,结果不是应该是1和0么?是不是很奇怪?

这便是使用多重继承的时候经常产生的第一个问题:多副本的数据存储。
当然这个问题很好解决,只需要使用虚继承即可解决。只需要在IA和IB的定义中,在public IBase前加入virtual关键字即可。

class IA : virtual public IBase
class IB : virtual public IBase

现在再让我们来看一看运行结果:
run-result-2:
run-result-2

结果已经正确了,为什么会发生这种情况呢?虚继承到底干了些什么呢?

我们先来看看在没有使用虚继承的情况下,CImpl的在内存中是怎么样的:
cimpl-memory-normal:
cimpl-memory-normal

对于普通的public继承(非虚继承),C++会将基类和派生类的内容放在一块,合起来组成一个完整的派生类,在内存上看,它们是连在一起的。按照这样的规则,在这里,IA和IB中就会各包含一个IBase,而IA,IB和CImpl中的部分内容又共同组成了CImpl。在将CImpl对象o的指针转型成IA的指针pA过程中,指针将被移动到IA所在的部分区域,同样在转型成IB的过程中,指针将被移动到IB所在的部分区域(也就是说,转型之后,指针的值都不一样)。在之后的操作中,pA操作的便是IA这个部分中的IBase,而pB操作的便是IB这个部分中的IBase,最后IA部分中的IBase变成了1,而IB部分中的IBase变成了-1。所以输出的结果也就变成了1和-1了。

之后我们修改成了虚继承,看看到底发生了什么?
cimpl-memory-virtual:
cimpl-memory-virtual
原来的IA和IB中的IBase部分变成了一个指向基类的指针,而基类也变成了一个单独的部分。这样一旦对基类做任何的修改,都会通过这个指针重定向到这个独立的基类上去,于是,就不存在多副本的数据存储了,这个诡异的问题也就解决了。但是当然从这个图上我们也可以看到,使用虚继承后,访问数据多了一次跳转,这多出的一次跳转将导致效率的下降一倍甚至更多,所以如果一个类使用的非常频繁,很明显应该尽量避免使用虚继承。

二义性

当然数据的存储只是使用多重继承中遇到的一个问题,现在我们来看另外一个问题,函数的二义性。
首先我们先把数据的存储抛开,单纯的来看一个只有函数的继承关系。

class IBase
{
public:
    virtual ~IBase() {}
    void foo() { }
};

class IA : public IBase
{
public:
    virtual ~IA() {}
};

class IB : public IBase
{
public:
    virtual ~IB() {}
};

class CImpl : public IA, public IB
{
public:
    virtual ~CImpl() {}
};

int main(int argc, char* argv[])
{
    CImpl o;
    o.foo();    // 直接调用CImpl的foo函数

    return 0;
}

编译一下,试试。
error C2385: ambiguous access of ‘foo’
could be the ‘foo’ in base ‘IBase’
or could be the ‘foo’ in base ‘IBase’
error C3861: ‘foo’: identifier not found

出错了!杯具。。为什么?错误还这么奇怪,神马叫做可以是IBase中的foo又可以是IBase中的foo呢?

这就是使用多重继承的时候经常产生的第二个问题:二义性。
在使用多重继承时,如果有两个被继承的类拥有共同的基类,那么就很容易出现这种情况。那什么是二义性呢?
我们先来看一个更简单的继承关系:

class A
{
public:
    void foo();
};

class B : public A
{
public:
    void foo();
};

class C : public B
{
public:
    void foo();
};

我们可以把继承关系中,两个类之间沿着基类方向的相隔的继承级数看成一个距离,那么C到A的距离是2,B到A的距离就是1。当然距离不能为负。
当我们对ABC中某个对象调用foo函数的时候,编译器会优先选择离当前指针类型的距离最短的一个函数实现去调用,也就是说,foo函数的查找路径是C->B->A,找到一个最近的去调用。
而对于我们当前这个继承关系来说,IA和IB还是各包含一份IBase的实例,虽然在内存里这里仅仅是包含一份数据,但是在编译的过程中,IA和IB中还包含了一份从IBase中继承下来的函数列表。所以有两个包含有foo函数类与CImpl类的距离是一样的,所以在对CImpl调用foo函数,就产生了所谓的二义性,除非我们指定使用IA::foo或者IB::foo,否则编译器将无法决定使用哪一个基类的foo函数。

o.IA::foo();    // 指定调用CImpl从IA部分继承过来的foo函数,这样就可以编译通过了。

当然如果我们这样写代码也是不行的:

IBase *pBase = &o;    // 指针转义时的二义性,不知道是使用IA中的IBase部分,还是IB中的IBase部分
o.inc();                    // 数据访问时的二义性,不知道是访问IA中IBase部分的n,还是IB中IBase部分的n

多重继承中的虚函数

既然直接使用多重继承会有如此多的问题,那么我们能不能通过虚函数来解决这个问题呢?

这里小小的提一下,刚刚二义性里面说到两个类的距离,对于编译器来说,一般是找离当前的类距离最近的函数实现来调用(或者数据来访问),而虚函数则是让编译器做相反的事情:找一个离当前类反向距离最远的函数实现来调用。

好,我们先把上面的程序做一点点小改变,把foo()函数变成一个虚函数,看看有什么变化。

class IBase
{
public:
    virtual ~IBase() {}
    virtual void foo() {}    // 变成虚函数了
};

编译,结果还是失败。
error C2385: ambiguous access of ‘foo’
could be the ‘foo’ in base ‘IBase’
or could be the ‘foo’ in base ‘IBase’
error C3861: ‘foo’: identifier not found

产生问题的原因依然是二义性。即便换成virtual函数,也不能改变二义性这个问题。为什么呢?
因为我们是用的.运算符来访问的,而不是用指针,所以这里虚函数和普通函数没有任何区别。=.=。。。
好,我们再来小小的修改一下,把他变成指针,让他通过虚表去访问,看看行不行。

CImpl *p = &o;
p->foo();

编译,结果。。。还是一样失败。。。
好吧,我们可以把调用foo()的几句话都去掉,来看看CImpl中生成的虚表到底是个什么样子。
debug-result-vptr-1:
debug-result-vptr-1

在这个实例中,IA和CImpl部分公用一个虚表,而IB则使用另外的一个虚表(两个虚表这个特性主要是在指针类型转换的时候有用,这里就不说了)。
在这IA的虚表中存在一个指向IBase::foo()的指针,在IB的虚表中也存在一个指向IBase::foo()的指针,所以在CImpl中,可以找到两个IBase::foo()函数的指针,这样,编译器就无法确定到底应该使用哪一个IBase::foo()函数作为他自己的foo()函数了。二义性也就产生了。

既然如此,那解决起来就没有什么别的办法了,只能把foo函数的最终在CImpl中实现一次了。

class CImpl : public IA, public IB
{
public:
    virtual ~CImpl() {}
    virtual void foo() { }
};

int main(int argc, char* argv[])
{
    CImpl o;
    o.foo();

    CImpl *p = &o;
    p->foo();

    return 0;
}

编译一下,通过了!对于o.foo()来说,这当然是意料之中,离CImpl距离最近的foo函数实现,就是CImpl自己嘛,当然没有问题。
对于后面这个p->foo()的调用,编译器现在也已经可以决定对于CImpl这个类来说,离他最远的foo函数调用是谁了——也是他自己。
所以这里就不会产生二义性的问题了。

在多重继承中编译器对this指针的修正
这里再让我们来看看这次编译出来的虚表,看看还有什么发现。
debug-result-vptr-2:
debug-result-vptr-2

0x004112a3 [thunk]:CImpl::foo`adjustor{4}’ (void) *
这个看上去很怪的函数是什么呢?我们反汇编一下他看看。
virtual-function-wrapper:
virtual-function-wrapper

这里可以看到有一句汇编指令:sub ecx, 4。这条指令的左右其实是在修正this指针。
因为从IB的虚表来的请求,this指针都是指向CImpl中IB的部分,而当调用CImpl中的foo函数时,如果还使用IB的this指针,那么程序就会出错,所以这里需要先将this指针修正为CImpl所在的地址,才能调用CImpl的foo函数。

在程序运行的时候,this指针一般被存储在ecx寄存器中,或者当前函数的第一个参数传递进去,不过不同的语言或者不同的编译器编译出来的代码可能会不一样。

我们这里的析构函数都是虚函数,所以我们还可以在截图中看到,编译器会对析构函数做同样的处理。

如何同时解决数据访问和二义性问题呢

貌似到现在都只提到最简单的一种多重继承的情况,但是实际上我们已经遇到了很多的问题了,既然多重继承中会有这么多问题,那我们有没有什么比较通用的方法能把他们一起解决了呢?
方法肯定是有的:
1. 使用虚继承
这算是一种确实可行的方法,只是说会带来额外的时间和空间的开销,访问任何一个数据,都需要通过虚继承表进行跳转,不过一般来说够用了。

2. 虚函数当接口,继承多个接口,统一实现
这个思想就类似于COM了,只是说COM用的是纯虚函数,对于那些会产生二义性的类,我们在最后都实现一边,这样就不会有问题了。这样带来的时间开销也仅仅是调用时查询一次虚表。但是麻烦的地方就是,有时候继承一下,你可能就要实现一下了,比如引用计数神马的,当然你也可以通过模版来简化你的代码。

class IBase
{
public:
    virtual ~IBase() {}
    virtual void show() = 0;
};

class IA : public IBase
{
public:
    virtual ~IA() {}
    virtual int inc() = 0;
};

class IB : public IBase
{
public:
    virtual ~IB() {}
    virtual int dec() = 0;
};

class CImpl : public IA, public IB
{
public:
    CImpl() : n(0) {}
    virtual ~CImpl() {}
    int inc() { return ++n; }
    int dec() { return --n; }
    void show() { printf("%dn", n); }

private:
    int n;
};

3. 通过纯虚函数实现模版方法,将函数转移
这种实现比较复杂,wtl中用的比较多,一般是用在引用计数上,好处很明显,就是可以继承,不用每个类都实现一个引用计数,而只用将新的基类的引用计数转移至原本存在的类上就可以了。

class IBase
{
public:
    virtual ~IBase() {}
    void foo() {}
};

class IA : public IBase
{
public:
    virtual ~IA() {}
};

class IShifter
{
public:
    virtual ~IShifter() {}
    void foo() { do_foo(); }

protected:
    virtual void do_foo() = 0;
};

class IB : public IShifter
{
public:
    virtual ~IB() {}
};

class CImpl : public IA, public IB
{
public:
    virtual ~CImpl() {}
    void foo() { IA::foo(); }

protected:
    virtual void do_foo() { IA::foo(); }
};
Posted in 03 Binary Life. Tags: , . 一条评论 »