浏览器工作原理

看到一篇很好的文章,浏览器的工作原理,http://www.html5rocks.com/zh/tutorials/internals/howbrowserswork/

看一遍,把要点梳理一下。

1.浏览器的结构

1)UI  主界面、地址栏、前进后退等

2)Browser engine,adapter层。

3)render engine,如果adapter层发来html的请求,render会解析html和css,并将结果最终渲染到屏幕上。

4)network,底层网络模块,与平台无关,负责所有网络接口。

5)JavaScript解释器,负责解析和执行JS

6)UI后端,绘制基本的窗口小控件,和平台无关,在底层使用操作系统的绘制方法。

7)数据存储,如cookie和indexdb等

layers

Render Engine,渲染引擎

渲染引擎是web的核心,因为web中一般不做复杂的计算等操作。页面需要及时响应用户操作,流畅的渲染。那么来看一下渲染的基本流程

flow

1.解析HTML,生成DOM-TREE,解析CSS,生成CSSOM,合成RenderTree,就是包含一堆矩形的树,树结构代表了显示顺序。

2.layout,计算位置

3.paint

下图是webkit的渲染过程

webkitflow

解析过程

上面的流程中,HTML Parser CSS Parser都涉及到了一个解析的过程,什么是解析?

解析就是把文档转化成代码能理解的结构,生成结果被称为解析树或语法树。

解析基于确定的语法规则,即与上下文无关的语法。

解析可以分为两个过程:词法分析和语法分析。

解析把两个过程交给两个组件,词法分析器和解析器。

词法分析器负责把输入内容分成一个个标记。

解析器负责把标记构建成解析树。解析器的解析是一个迭代过程,即向词法分析器请求一个新标记,如果匹配语法规则,则把对应该标记的节点添加到解析树中,如果不匹配,则把这个标记缓存,继续请求标记,直到找到一个匹配的语法规则。

解析树往往还不是最终产品,这个时候需要使用编译器把解析树转成机器代码。

image013

一般的解析可以去看龙书,Html语言不适用于常规解析器,因为HTML不是一种上下文无关的语言,这源于HTML有一定的容错能力。CSS和JavaScript可以。

HTML有自定义的解析器,采用DTD格式定义。

解析算法分为两个步骤,标记化和构建树:

标记化算法使用状态机驱动,该算法比较复杂,通过一个例子叙述:

1.初始状态为 数据状态,

2.遇到<时,更改为标记打开状态,

3.之后读取一个a-z字符,进入标记名状态

4.直到遇到>,进入数据状态

5.数据状态下去读Hello world,直到<,进入标记打开状态

6.标记打开状态下遇到/号,创建 end tag token并进入标记名状态

7.读取a-z字符直到>,回到数据状态。

解析器创建时,会创建Document对象。在解析器工作的时候,已Document为根节点的DOM树不断更新,解析器生成的元素不仅仅会加入DOM树,还会放到堆栈中去,堆栈用来纠正嵌套错误和未关闭的标记。

树构建过程

树构建阶段的输入是一个来自标记化阶段的标记序列。第一个模式是“initial mode”。接收 HTML 标记后转为“before html”模式,并在这个模式下重新处理此标记。这样会创建一个 HTMLHtmlElement 元素,并将其附加到 Document 根对象上。

然后状态将改为“before head”。此时我们接收“body”标记。即使我们的示例中没有“head”标记,系统也会隐式创建一个 HTMLHeadElement,并将其添加到树中。

现在我们进入了“in head”模式,然后转入“after head”模式。系统对 body 标记进行重新处理,创建并插入 HTMLBodyElement,同时模式转变为“in body”

现在,接收由“Hello world”字符串生成的一系列字符标记。接收第一个字符时会创建并插入“Text”节点,而其他字符也将附加到该节点。

接收 body 结束标记会触发“after body”模式。现在我们将接收 HTML 结束标记,然后进入“after after body”模式。接收到文件结束标记后,解析过程就此结束。

image022

树构建过程

解析过程结束后,浏览器将文档注为交互状态,开始加载 defferd模式下的脚本。然后文档状态成为完成,随之触发加载事件。

容错

浏览器底层有大量的代码来对诸如 无效tag,结束符错误等容错。

来看看webkit的容错说明

解析器对标记化输入内容进行解析,以构建文档树。如果文档的格式正确,就直接进行解析。

遗憾的是,我们不得不处理很多格式错误的 HTML 文档,所以解析器必须具备一定的容错性。

我们至少要能够处理以下错误情况:

  1. 明显不能在某些外部标记中添加的元素。在此情况下,我们应该关闭所有标记,直到出现禁止添加的元素,然后再加入该元素。
  2. 我们不能直接添加的元素。这很可能是网页作者忘记添加了其中的一些标记(或者其中的标记是可选的)。这些标签可能包括:HTML HEAD BODY TBODY TR TD LI(还有遗漏的吗?)。
  3. 向 inline 元素内添加 block 元素。关闭所有 inline 元素,直到出现下一个较高级的 block 元素。
  4. 如果这样仍然无效,可关闭所有元素,直到可以添加元素为止,或者忽略该标记。
  5. 示例网站 www.liceo.edu.mx 嵌套了约 1500 个标记,全都来自一堆 <b> 标记。我们只允许最多 20 层同类型标记的嵌套,如果再嵌套更多,就会全部忽略。
  6. 支持格式非常糟糕的 HTML 代码。我们从不关闭 body 标记,因为一些愚蠢的网页会在实际文档结束之前就关闭。我们通过调用 end() 来执行关闭操作。

CSS解析是遵循一般的解析规则的,这里不赘述。

脚本和文档处理的顺序

当遇到一个script标签时,会立即停止文档的解析,进行js解析。

如果script标签带有defer标记,则会在文档解析完成后再解析。

h5增加了一个标记,将script标记为异步,则会开线程去解析它。

预解析

这是一项优化。在执行脚本的时候,其他线程回去解析剩下的文档部分,找出并加载需要下载的网络资源。预解析器不会动DOM树,只是寻找需要下载的资源。

样式表

理论上CSS和HTML不是一个解析器,但有些JS会请求CSS,所以浏览器在解析CSS的时候会禁止有交互的脚本。

Render-Tree

在DOM-Tree构建的时候,同时也在生成一个Render-Tree。Render-Tree是DOM-Tree的子集,它不包含那些display:none的元素,以及head等。

一般情况下DOM-Tree和Render-Tree的位置是对应的,但不包括absolute和float等非文档流内容,它们位于其他位置,原位置放一个占位符。

样式计算

样式计算是个复杂而庞大的工程,如 匹配规则为 div div div div,这样的匹配会很耗时,而且找了很久可能找到一个三层的。

浏览器为了样式计算进行了很多工作

共享样式数据
规则树
结构划分
使用规则树计算样式上下文
对规则进行处理以简化匹配
以正确的层叠顺序应用规则
样式表层叠顺序
特异性

特异性这里详细记一下

某个样式的属性声明可能会出现在多个样式表中,同一个样式表中也可能出现多次,那么他们的重要性为(优先级从低到高):

1.浏览器声明

2.用户普通声明

3.作者普通声明

4.作者重要声明

5.用户重要声明

同样顺序的声明,按照特异性排序

特异性是 a-b-c-d

a:声明来自style,标记为1,否则为0

b:选择器中ID属性的个数

c:选择器中class属性和伪类的个数

d:选择器中元素名称和伪类的个数

规则排序

渐进式处理

这里不详细叙述。

布局-layout

layout是一个比较重的环节,不仅仅是layout消耗时间,而是layout后一定会引发paint。

HTML采用流式布局,意味着可以遍历一次就计算出来所有位置,从左到右,从上到下。有些特殊的如 表格需要多次遍历。

每一个节点都有一个layout/reflow方法,是一个递归过程。

Dirty位系统

为了避免细小改动都要进程整体布局,浏览器采用了Dirty位的设计。如果某个节点发生了改变,则将其与子代标注为dirty。还有一种标记,children are dirty,表示节点至少有一个子代需要重新布局。

布局分全局和增量两种

全局指必须重新布局,增量是只布局dirty节点(可能还需要布局其他元素)。全局布局是同步的,增量布局一般是异步的。

布局优化,如果只是位置改变或者缩放等,可以直接从缓存取原大小。某些子树修改,不必全局重新layout,如input,不会每输入一次就来一次全局更新。

Paint

paint的内容比较少,只是调用节点的paint方法。paint也有全局绘制和增量绘制两种。

优化

webkit在paint时会缓存之前的矩形,只绘制新旧矩形的差异部分。而当元素有了变化,浏览器会尽量做最小的改动。

 

线程

Render engin是单线程。

loop

主线程是一个loop,事件循环。

 

CSS模型

可视化模型,指画布

框模型,由框大小,padding,margin,border组成,display指定

image046

position

普通  浮动 绝对

display:block  普通的矩形,拥有自己的区域

display:inline 没有自己的区域,位于容器block中

image061

block是垂直的,inline是水平的

分层

z-index表示了框的第三个维度

 

adb shell下向应用发送GC命令

DDMS里有个Cause GC命令,用来要求虚拟机强制GC,如下图:
1329cc2bed09b4fdd38b63d6440b1fb9
但是在有些环境下面,测试同学,或者用户机器上面没有安装Android Sdk,这个时候使用adb shell来执行GC命令是比较合适的,但是网上搜索了一下,发现adb shell下没有提供这个命令。

根据网上介绍,执行GC命令实际上是通过kill命令向进程发送了数字为10的自定义信号而已,代码如下:

相同道理,我们只要直接在adb shell里面直接向进程发送数字为10的自定义信号就可以了。

参考链接


关于使用 adb 命令触发 GC 操作的问题

C/C++中的正则表达式库——PCRE, PCRE++

1. 什么是PCRE? 什么是PCRE++?
PCRE,全称是Perl Compatible Regular Expressions。从名字我们可以看出PCRE库是与Perl中正则表达式相兼容的一个正则表达式库。PCRE是免费开源的库,它是由C语言实现的,这里是它的官方主页:http://www.pcre.org/,感兴趣的朋友可以在这里了解更多的内容。
要得到PCRE库,可以从这里下载:http://sourceforge.net/projects/pcre/files/

PCRE++是一个对PCRE库的C++封装,它提供了更加方便、易用的C++接口。这里是它的官方主页:http://www.daemon.de/PCRE,感兴趣的朋友可以在这里了解更多的内容。
要得到PCRE++库,可以从这里下载:http://www.daemon.de/PcreDownload

2. PCRE接口介绍
(1). pcre_compile

(2). pcre_exec

3. PCRE++接口介绍
PCRE++PCRE库封装成了两个类,一个是RE_Options, 用来指定匹配选项,一个是RE,用来提供匹配相关的接口。RE_options类在这里我就不介绍了,我主要介绍一下RE类:
(1)RE的构造函数传入正则表达式,并在构造函数中调用Init函数,将该正则表达进行编译
(2)REpattern()成员用来得到初始传入的正则表达式字符串
(3)REerror()成员用来得到匹配过程中的出错信息
(4)REFullMatch()成员用来判断某字符串整体是否匹配指定正则表达式
(5)REPartialMatch()成员用来判断某字符串的部分是否匹配指定正则表达式

4. PCRE/PCRE++使用注意事项
(1)使用pcre请包含pcre.h头文件
(2)使用pcre_compile, pcre_exec后,记得调用pcre_free释放内存,以免造成内存泄露
(3)使用pcre编译的时候需要依赖libpcre.a
(4)使用pcre++请包含pcrecpp.h头文件
(5)使用pcre++RE类的析构函数会自动释放相关内存,因此不用担心内存泄露
(6)使用pcre++编译的时候需要依赖libpcrecpp.a
(7)使用pcrecpp要使用pcrecpp命名空间

5. PCRE使用举例
下面是例程:

下面是运行结果:

6. PCRE++使用举例
下面是例程:

下面是运行结果:

参考链接


深入浅出C/C++中的正则表达式库(三)——PCRE, PCRE++

js文件中获取${pageContext.request.contextPath}

一般从JSP文件中,可以直接使用${pageContext.request.contextPath}非常方便的获得当前页面的路径,用来处理被Apache2代理之后出现URL变化的问题,比如增加了某些前缀,方便转发,即使是JSP内嵌的JavaScript脚本,也是可以如此操作。但是如果是一个独立的JavaScript文件,通过

这样的方式引入,则在JavaScript文件内部是无法直接调用${pageContext.request.contextPath}获取前缀的,因为${pageContext.request.contextPath}是需要JSP文件处理的变量,而对于独立的JavaScript文件,默认是不做任何处理的。

因此这个变量只能是通过某个全局变量传递到JavaScript文件中。
目前比较有效的实现方法是通过设置一个隐藏的文本框的方式来处理。

JavaScript文件中获取这个变量的方法如下:

代码行数统计-cloc

cloc是一个很方便的代码行数统计工具,官网 http://cloc.sourceforge.net/;

安装

我这里使用的npm安装(需要安装node),npm install -g cloc安装好后,命令行敲cloc会给出使用指令,这里不一一详述,只给一个最简单的使用方法。

把要统计的文件打成zip包,然后 cloc  xx.zip,就可以了。

cloc

保护wp-login.php后台登陆入口

最近在观察后台的登陆统计的时候,发现来自"乌克兰","美国"的某个IP的访问数量特别多,访问记录显示是大量访问登陆页面https://www.mobibrw.com/wp-login.php,明显是发生了密码破解攻击。

TopVisitors

login-recorders

本来以为WordPress 4.4版本已经对密码的访问次数进行了限制,结果试了一下,发现完全没有任何限制,很容易导致密码穷举攻击。比较简单的解决方法是对登陆页面进行密码尝试次数限制。

最方便的是安装Limit Login Attempts插件即可,这个插件很久不更新了。目前从测试来看,与WordPress 4.4版本是兼容的。

Ubuntu 14.04隐藏Apache-2.4的版本号与操作系统类型

一般情况下,软件的漏洞信息和特定版本,特定操作系统是相关的,因此,软件的版本号以及操作系统类型对攻击者来说是很有价值的。

在默认情况下,Apache会在返回信息中把自身的版本号,操作系统类型都显示出来,如下图:
Apache2-403

这样做会造成潜在的安全风险,导致不必要的攻击行为。

Ubuntu 14.04系统上隐藏Apache-2.4的版本号与操作系统类型的方法如下:

把文件中的的ServerTokens OS修改为ServerTokens Prod,ServerSignature On修改为ServerSignature Off,如下图所示:

apache2-security-conf

修改完成后,重启Apache2的服务

修改后的结果如下图所示,已经没有系统类型信息了,仅仅返回了一个403错误。

Apache2-404-Modify

阿里云服务器从Ubuntu 12.04升级到Ubuntu 14.04

阿里云的服务器是Ubuntu 12.04根据Canonical发布的支持路线图,可以看到2017年4月份之后就不再提供支持。因此很有升级导致Ubuntu 14.04的必要,更别说很多软件在Ubuntu 12.04上已经比较过时了。

Ubuntu LTS版本支持路线图如下图:
1_201204291858511OI60

升级的流程如下所示:(执行下面操作之前,请务必先备份重要数据,阿里云服务器推荐使用自带的系统快照功能,非常好用)

1.首先保证当前系统上的软件都是最新的


2.安装系统升级模块


1-do-release-upgrade

3.升级流程


允许系统在升级期间开放1022端口用来处理系统升级异常,当系统升级异常的时候,可以通过这个端口进行某些恢复操作。(实际上没太大作用,出问题就快照回滚了,更快速安全方便)
2-ssh-port-query-yes

输入y,点击回车(Enter)。

3-iptables-add-port-press-enter

点击回车(Enter),允许在iptable上面开放1022端口出来,这个端口在安装完成后会自动关闭,不需要过多关心。

4-rewrite-sources-list-yes

允许升级程序更新sources.list用来获取升级所需要的文件,输入y,点击回车(Enter)。

5-upgrade-confirm

询问是否确认系统升级,输入y,点击回车(Enter)。

6-disable-ssh-password-no

询问是否禁止root用户通过ssh访问系统,这个一定要选择No,否则升级完成后,我们无法远程登陆系统。

7-restart-services-without-asking-yes

询问在升级期间是否允许自动重启需要升级的服务,这个一定要选择Yes,否则会不断的询问你是不是确定重启服务,非常麻烦。

8-serurity-limits-conf-replace-enter

询问是否用新系统的文件替换原系统的/etc/security/limits.conf文件,直接回车(Enter),不允许替换,使用原系统的配置。

9-etc-default-rcS-enter

同上,直接回车(Enter)。

9-etc-default-rcS-enter

同上,直接回车(Enter)。

10-etc-sysctl-conf-enter

同上,直接回车(Enter)。

11-etc-vsftpd-conf-enter

同上,直接回车(Enter)。

12-etc-php5-fpm-php-fpm-conf-enter

同上,直接回车(Enter)。

13-etc-php5-fpm-php-ini-enter

询问是否替换文件,同上,直接回车(Enter),不允许替换。

14-etc-php5-cgi-php-ini-enter

同上,直接回车(Enter)。

15-etc-init-mounted-run-conf-enter

同上,直接回车(Enter)。

16-etc-apache2-mods-available-fcgid-conf-enter

同上,直接回车(Enter)。

17-etc-sv-git-daemon-run-enter

同上,直接回车(Enter)。

18-etc-default-tomcat7-enter

同上,直接回车(Enter)。

19-upgrade-phpmyadmin-enter

询问是否升级数据库,此处选择Yes,回车(Enter)。

20-upgrade-phpmyadmin-password-enter

输入数据库的密码,完成后点击回车(Enter)。

21-remove-obsolete-packages-yes

询问是否删除不再使用的安装包,输入y后点击回车(Enter)。

22-restart-required-yes

升级完成,询问是否重启系统,输入y后点击回车(Enter)。系统会重启,远程连接会断开,需要稍等几分钟后重新连接服务器。

3.恢复被修改后的系统配置信息


安装Apache2PHP扩展libapache2-mod-php5,Ubuntu 12.04版本的库,在升级的过程中被丢弃了,需要重新手动安装。

23-after-restart-install-libapache2-mod-php5

询问是否替换已经存在的PHP配置文件,直接点击回车,不允许替换。

24-after-restart-install-libapache2-mod-php5-php-ini-enter

修改Apache2的配置文件

25-after-restart-vim-apache2-conf

原有Apache 2.2配置为:

发现升级后变更为:

导致PHP无法正常工作,因此需要修改回来。

修改前:
26-after-restart-vim-apache2-conf-IncludeOptional

修改后:27-after-restart-vim-apache2-conf-IncludeOptional-modify-complete

Apache 2.4修改了默认目录位置(这导致2.2版本设置的禁止目录流量功能失效),并且默认开启了目录浏览功能,会导致潜在的安全问题,需要手工关闭.

修改前:29-after-restart-apache2-disable-indexs

修改后:30-after-restart-apache2-disable-indexs-modify

重启Apache2

28-after-restart-apache2-restart

到此,整个系统升级完成,所有功能恢复正常。

从升级的效果来看,服务器的响应明显变快,非常值得升级!

Shared Preferences的分析

The SharedPreferences class provides a general framework that allows you to save and retrieve persistent key-value pairs of primitive data types. You can use SharedPreferences to save any primitive data: booleans, floats, ints, longs, and strings. This data will persist across user sessions (even if your application is killed).

调用getSharedPreferences()获取对应的的文件,该函数实现功能如下:

//Context类静态数据集合,以键值对保存了所有读取该xml文件后所形成的数据集合

可以看到他有一个Map, 而针对SharedPreferencesImpl里面,由会有map, 这样也就可以证明, 为什么SharedPreference被广泛使用了。 他在普通时刻,内容是从内存里面直接读取的, 只有在第一次启动时,是IO操作。

2. apply()commit()的区别

/** boolean commit();的注释如下:

apply方法的注释:

apply不同于commitcommit是同步的去更改硬盘上的东西,而apply是先直接更改内存中的, 然后异步的去更改应硬盘中的内容。

不用去担心线程安全问题, 因为如果一个其他的线程去commit,而刚好有一个还没有完成的applycommit会被阻塞到异步线程提交完成。

参考链接


Shared Preferences的分析

Android Studio中的productFlavors指定默认编译执行的任务

Android Studio中指定了productFlavors如下:

整个的build.gradle里面的内容如下:

但是这个时候我们点击Android Studio的调试按钮的时候,不知道究竟是使用哪个Flavors来编译,比如在Android Studio 1.5的时候,是按照从上到下的顺序处理的,默认是使用排在第一个的Daily,而到了Android Studio 2.1 Preview 5版本,却变成了按照字母排序,结果变成了默认编译Advance

网上搜索了一下,找到如下解决方法:

选择"Build Variant",然后在出现的窗口中选择其中一个选项作为默认的编译,运行选项即可。

BuildVariantSel

参考链接


Gradle Build only a flavour