为了讲解 init 进程,首先了解一下 Android 系统启动流程的前几步,以引入 init 进程。
当电源按下时引导芯片代码从预定义的地方 (固化在 ROM) 开始执行。加载引导程序 BootLoader 到 RAM 中,然后执行。
引导程序 BootLoader 是在 Android 系统启动前的一个小程序,它的作用主要是把系统 OS 拉起来并运行。
当内核启动时,设置缓存、被保护存储器、计划列表、加载驱动。在内核完成系统设置后,它首先在系统文件中寻找 init.rc 文件,并启动 init 进程。
init 进程做的工作比较多,主要用来初始化和属性服务,也用来启动 Zygote 进程。
从上面的步骤可以看出,当我们按下电源时,系统启动会加载引导程序,引导程序又启动 Linux 内核,在 Linux 内核完成后,第一件事就是要启动 init 进程。
在 Linux 内核加载完成后,它首先在系统文件中寻找 init.rc 文件,并启动 init 进程,init 进程的入口函数 main,代码如下:
int main(int argc, char** argv) { |
在注释 1 处 property_init
函数来对属性进行厨师阿虎,并在注释 3 处调用 start_property_service
启动属性服务
注释 2 处调用 sigchld_handler_init
用于设置子进程信号处理函数,主要用于防止 init 进程的子进程成为僵尸进程,为了防止僵尸进程的出现系统会在子进程暂停和终止的时候发出 SIGCHLD 信号,而 sigchld_handler_init
函数就是用来接收这个信号的(其内部只处理进程终止的 SIGCHLD 信号)。
在 UNIX/Linux 中,父进程使用 fork 创建子进程,在子进程终止之后,如果父进程并不知道子进程已经终止了,这时子进程虽然已经推出了,但是在系统进程表中还为它保留了一定的信息(比如进程号、退出状态、运行时间等),这个子进程就被称作僵尸进程。系统进程表示一项有限资源,如果被僵尸进程耗尽的话,系统就可能无法创建新的进程了。
Zygote 也是 init 进程的子进程之一,如果出现子进程终止的情况就会触发 sigchld_handler_init
并触发清理 Zygote 进程的信息,然后 Zygote 就会在注释 5 处被重启。
init.rc
是一个非常重要的配置文件,它是由 Android 初始化语言(Android Init Language)编写的脚本,这种语言主要包含 5 种类型语句:Action、Command、Service、Option 和 Import。
从 Android 8.0 开始对 init.rc
文件进行了拆分,每个服务对应一个 rc 文件,下面是 Zygote 启动脚本,在 init.zygote64.rc
中。代码如下:
service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server |
Service 命令用于通知 init 进程 Zygote 进程的名为 zygote,这个进程执行程序的路径为 system/bin/app_process64
,后面的代码是要传给 app_process64
的参数。class main
指的是 Zygote 的 classname 为 main。
Service 命令会由 init main 函数的代码中解析为 Service 对象,再添加到 ServiceManager 中 vector 类型的 Service 链表中,执行 Service 命令解析的主要涉及文件是是 system/core/init/service.cpp
执行的时候
init.rc 是一个配置文件,在 Service 命令定义 Zygote 后还有如下配置代码:
on nonencrypted |
其中 clas_start 对应的函数执行是 system/core/init/builtins.cpp
中的 do_class_start
,这里就是启动 classname 为 main 的 Service,其中 zygote 就是在此处启动的。如下:
static Result<Success> do_class_start(const BuiltinArguments& args) { |
其中 Zygote 执行程序的路径是 /system/bin/app_process64
对应的文件为 framework/base/cmds/app_process/app_main.cpp
,部分代码如下:
int main(int argc, char* const argv[]) |
Android 系统提供了属性服务,其实是类似于 Windows 系统中的注册表管理器,用于记录用户的一些使用信息,从而在系统或软件重启后,扔能够获取存储的记录,进行相应的初始化工作。
init 进程启动时会启动属性服务,并为其分配内存,用来存储这些属性,如果需要属性直接读取就可以了,上面 init.cpp
的提到的 property_init
, start_property_service
就是用于初始化属性配置和启动属性服务的。
void start_property_service() { |
handle_property_set_fd
函数ps: epoll 为 Linux 内核为处理大批量文件描述符而做了改进的 poll,是 Linux 下多路复用 I/O 接口 select/poll 的增强版本,epoll 内部使用数据结构是红黑树,而 select 使用数组,当存在大量文件描述符时,epoll 查找效率会比 select 速度快。
属性服务中分为控制属性和普通属性,控制属性用来执行一些命令,比如开机的动画就使用了这个属性。控制属性和普通属性的存储、更新逻辑不一致,控制属性的 key 名称以 ctl.
开头。
init 进程启动做了很多工作,总的来说做了三件事
Android 的核心系统服务基于 Linux 内核,再次基础上添加了部分 Android 专用的驱动。这些驱动通常与硬件无关,而是为了上层软件服务的,他们包括以下内容:
硬件抽象层,该层为硬件厂商定义了一套标准接口。有了这套标准接口之后,可以在不影响上层的情况下,调整内部实现。当框架 API 要求访问设备硬件时,Android 系统将为该硬件组件加载库模块。
硬件抽象层是位于操作系统内核与硬件电路之间的接口层,其目的在于将硬件抽象化,为了保护硬件厂商的知识产权,它隐藏了特定平台的硬件接口细节,为操作系统虚拟硬件平台,使其具有硬件无关性,可在多种平台上进行移植。从软硬件测试的角度来看,都可以分别基于硬件抽象层来完成,使得软硬件测试工作的并行进行成功可能。通俗来讲,就是将控制硬件的动作放在硬件抽象层中。
系统运行库层分为两部分,分别是 C/C++ 程序库和 Android 运行时库,下面分别进行介绍。
C/C++ 程序库能被 Android 系统中的不同组件所使用,并通过应用框架层为开发者提供服务,主要的 C/C++ 程序库如下:
运行时库又分为核心库 (Core Libraries) 和 ART (Android 系统 5.0 之后,Dalvik 虚拟机被 ART 取代)。
核心库提供了 Java 语言核心库的大多数功能,这样开发者可以使用 Java 语言来编写 Android 应用。
与 JVM 相比,Dalvik 虚拟机 (DVM) 是专门为移动设备定制的,允许在有限内存中同时运行多个虚拟机的实例,并且每一个 Dalvik 应用被作为一个独立的 Linux 进程执行。而且独立的进程可以防止在虚拟机崩溃的时候所有程序被关闭。
替代 DVM 的 ART 的机制与 DVM 又有不同,DVM 中的应用每次运行时,字节码都需要通过即时编译器 (Just In Time, JIT) 转换为机器码,这会使得应用运行效率降低。而在 ART 中,系统在安装应用时会进行一次预编译 (Ahead Of Time, AOT) 将字节码预先编译成机器码并存储在本地,这样应用每一次运行的时候就不需要执行编译了,这样应用的运行效率也得到了大大的提高。
Framework 层这一层包含了一系列重要的系统服务。对于 App 层的管理及 App 使用的 API 基本上都是在这一层提供的。这里面包含的服务很多,例如:
系统内置的应用程序与非系统级的应用程序都属于应用层,负责与用户进行直接交互,通常都是用 Java 进行开发的。
]]>内存重启
现象引发的思考,是基于Activity 基类的规范状态使用和数据处理。这种设计的关键内存重启后怎么进行恢复关键数据
和预加载数据
,并且规范 Acitivity 基类
,让团队减少处理内存重启的情况,并且减少因为 关键数据缺失
导致的崩溃问题。
内存重启
:表现是 App 从后台进入前台的过程中,因为内存数据之前已经被回收,系统会从最后一个显示的 Activity 开始一步步恢复 Activity 和 Activity 状态。初始状态
:初始状态为 Activity 加载了一个 layout
布局,还没有获取任何的 id 、操作 view 、开始异步加载数据。数据渲染状态
:初始状态以后开始加载数据和渲染改变一些界面的状态,会以一个方法作为入口,正常启动会直接 onCreate
初始状态后进入 数据渲染状态
,但我的做法是内存重启
的时候会在 Activity 的 onActivityResult
才进入数据渲染状态。那么具体怎么做到呢?假定:恢复的 Activity 为 RestoreActivity
,引导页 Activity 为 AppStartActivity
onCreate
发现是内存重启的情况下,调用 RestoreActivity
startActivityForResult
启动一个引导界面AppStartActivity
,同时结束 onCreate
不进入 数据渲染状态
,直到启动页面结束返回,这个恢复的 Activity 被系统调用了 onActivityResult
通知我们结果,然后根据接收到返回的对应 result code 进入 数据渲染状态
。全局数据单例的恢复
(例如:已登录的用户单例信息),当然根据需求可以适当预访问或预加载
一些比较急需的 App 全局数据,加速返回后的恢复页 Activity 的界面展现,因为引导页的时间一般都会有最低3秒或有个最低秒数阈值,所以理论上可以合理预加载
一点数据利用一下,但是一定要注意是在 恢复关键数据
之后,并且到达一定时间就要返回,太长时间的引导界面会让人烦恼,这样设计的关键就是恢复关键数据
和预加载数据
为什么会这么设计呢?有心人注意去测试看看 微信
的启动,真正的内存重启的时候,会让你看到 小人 + 地球
的引导 Activity,再返回真正的对应恢复 Activity,用户体验
上也是内存重启就会出现一个相对较友好的引导界面做了一个长期的用户认知培养,而不是一个 正在加载或容易 crash
的界面。
可以不用依赖于一个 Activity 级别去实现这个 引导界面,通过 Fragment 和 属性变量来实现也可以,若发现是 内存重启
不进入 数据渲染状态
,加载引导 Fragment,做对应的恢复关键数据
和预加载数据
,改变属性变量调用对应的入口方法,继续进入 数据渲染状态
在 Android 设计布局中,我们常常遇到需要根据父组件、相邻组件甚至是相邻组件的边来进行中心点对齐。本文将讲解如何使用 ConstraintLayout
来满足这些中心点对齐的需求。
需要中心点对齐的 Views 一般是有自己不定的内容大小的,经常定义的宽高是 android:layout_[width|height]="wrap_content"
。本文中使用到的例子也会按照这个模式来使用。
在父组件中居中对齐,可以通过对 View 的某个坐标轴的两个边的锚点设置约束条件到父组件相对应的边来进行居中。比如,让子组件垂直居中的话,我们需要设置 top
和 bottom
上下边的锚点约束,而水平居中的话,则需要设置 start
和 end
左右边的锚点约束。此处使用 start
和 end
而不是用 left
和 right
是为了更好的 RTL (Right to Left)布局体验。
在这个例子中,我们设置 View 的 start
边到父组件的 start
边,同时设置了 end
边到父组件的 end
边。
在 XML 其实跟视图编辑器是一样的意思,设置好 app:layout_constraintStart_toStartOf="parent"
以及 app:layout_constraintEnd_toEndOf="parent"
,如下:
<TextView |
这种中心点居中跟相对父组件的中心点居中差不多,唯一的区别就是设置的约束是向相邻组件而不是向父组件,如下图操作:
在 XML 源码中也是一样,非常相似,唯一不同就是约束指向邻组件的 android:id
而不是 parent
:
<TextView |
最后要讲解的这个中心点居中,在其他布局中非常不好实现,对于此 Material Design 指南中已经提到了 如何让浮动按钮对齐到某个组件的边缘 ,通过 ConstraintLayout 可以轻松实现这个效果。假如,我们需要设置垂直向的居中到某一边,那么就设置 View 的 top
和 bottom
边的锚点约束到邻组件的那条边的同一边的中心锚点上,如下图所示:
在 XML 中也是非常的简单明了,直接就是设置约束到同一个锚点,app:layout_constraintTop_toBottomOf="@+id/imageView"
和 app:layout_constraintBottom_toBottomOf="@+id/imageView"
,代码如下:
<TextView |
本文将列举讲述如何使用 ConstraintLayout
来代替常见的三种布局 LinearLayout 、 RelatvieLayout 、 PercentLayout
的用法,本文使用的 Android Studio 都是 2.4 alpha 7
版本的,而 ConstraintLayout 库是使用的 1.0.2
。
LinearLayout
的基本用法就是将子组件 View 在水平或者垂直方向浮动对齐,基于属性 orientation
来设置。在视图编辑器中使用 ConstraintLayout 要实现这个特性非常简单,假如要实现相同的垂直方向浮动对齐,步骤很简单,就是添加 View 然后将每一个 View 的上边缘添加约束向到它位置上的另一个 View 即可,如下图:
在 XML 中实现该特性也仅仅是为每一个 View 实现一个约束属性 app:layout_constraintTop_toBottomOf
到整个浮动布局中在它之前的 View。
|
要想创建跟 LinearLayout 类似的 weight 权重特性的话,我们需要创建约束 Chain 链,详细可以看看我的另一篇文章,表现如下图:
Chain 链创建后,我们只需要在属性视图中为每个需要设置 weight 权重的链组件修改 layout_width
为 match_constraint
或者 0dp
(两者是一样的),然后再设置对应的权重值到 weight
的配置属性,因为这个例子中我们使用的是水平的 Chain 链,所以设置权重的时候设置的属性是 horizontal_weight
,如下图。
最后,我们就可以再 blueprint 蓝图视图下看到如下的展现:
首先要如之前的教程一样,在 XML 创建 Chain 链,然后实现如上的效果只需要对 textView3
修改属性 android:layout_width="0dp"
并且设置新属性 app:layout_constraintHorizontal_weight="1"
,如下:
|
这里 app:layout_constraintHorizontal_weight
属性设置的值与 LinearLayout
中设置的 android:layout_weight
是一样的值并且用法一样,将会根据所有子组件的设置的权重比分割剩余的空间。
RelativeLayout
主要被用于包装布局根据 views 组件之间的关系或与父组件的关系来布局的子 views 。其实如果你对 RelativeLayout
和 ConstraintLayout
都熟悉的话,就会感觉 RelativeLayout
其实只是 ConstraintLayout
的更基础版本,ConstraintLayout
的很多概念来源其实就是 RelativeLayout
。事实上,你还可以认为 ConstraintLayout
就是加强版的 RelativeLayout
,因为你对旧的 Android 布局组件的熟悉,这将是很好的学习了解 ConstraintLayout
的思想体系模型。
因为 RelativeLayout
就是基于描述各个子 Views 之间的关系,而对各个子 Views 添加约束来实现相同的关系以及展现其实也很相似简易实现。举例,创建布局“ X 位于 Y 之上”的约束就对应于 RelativeLayout
中的 android:layout_above
属性:
上面已经提到了 RelativeLayout
和 ConstraintLayout
的基本特性概念非常相似。你可以通过查阅我的另一篇文章来熟悉 ConstraintLayout
的基础,然后使用如下面的表格中对应的属性来转换 RelativeLayout
中的属性到 ConstraintLayout
。
RelativeLayout 属性 | ConstraintLayout 属性 |
---|---|
android:layout_alignParentLeft="true" | app:layout_constraintLeft_toLeftOf="parent" |
android:layout_alignParentLeft="true" | app:layout_constraintLeft_toLeftOf="parent" |
android:layout_alignParentStart="true" | app:layout_constraintStart_toStartOf="parent" |
android:layout_alignParentTop="true" | app:layout_constraintTop_toTopOf="parent" |
android:layout_alignParentRight="true" | app:layout_constraintRight_toRightOf="parent" |
android:layout_alignParentEnd="true" | app:layout_constraintEnd_toEndOf="parent" |
android:layout_alignParentBottom="true" | app:layout_constraintBottom_toBottomOf="parent" |
android:layout_centerHorizontal="true" | app:layout_constraintStart_toStartOf="parent" 和 app:layout_constraintEnd_toEndOf="parent" |
android:layout_centerVertical="true" | app:layout_constraintTop_toTopOf="parent" 和 app:layout_constraintBottom_toBottomOf="parent" |
android:layout_centerInParent="true" | app:layout_constraintStart_toStartOf="parent" , app:layout_constraintTop_toTopOf="parent" , app:layout_constraintEnd_toEndOf="parent" , 和 app:layout_constraintBottom_toBottomOf="parent" |
这里要注意,相对父组件的居中没有一对一即是只用一条属性能设置同样效果的,而是通过设置相同的约束条件到相对的两个边缘来实现。水平居中,意味着需要设置两个相同的约束条件到水平的左和友边缘对齐父组件,而垂直居中,则是需要设置两个相同的约束条件到垂直的上下边缘对齐父组件,自然而然的在两个方向上都居中的话,则是需要设置两对相同的约束条件在水平和垂直方向,即是四个约束条件对齐。提醒一下大家,在这里可以通过设置约束条件的 bias
来设置 View 组件垂直或水平对齐到父组件的百分比位置,如下图所示:
RelativeLayout 属性 | ConstraintLayout 属性 |
---|---|
android:layout_toLeftOf | app:layout_constraintRight_toLeftOf |
android:layout_toStartOf | app:layout_constraintEnd_toStartOf |
android:layout_above | app:layout_constraintBottom_toTopOf |
android:layout_toRightOf | app:layout_constraintLeft_toRightOf |
android:layout_toEndOf | app:layout_constraintStart_toEndOf |
android:layout_below | app:layout_constraintTop_toBottomOf |
android:layout_alignLeft | app:layout_constraintLeft_toLeftOf |
android:layout_alignStart | app:layout_constraintStart_toStartOf |
android:layout_alignTop | app:layout_constraintTop_toTopOf |
android:layout_alignRight | app:layout_constraintRight_toRightOf |
android:layout_alignEnd | app:layout_constraintEnd_toEndOf |
android:layout_alignBottom | app:layout_constraintBottom_toBottomOf |
android:layout_alignBaseline | app:layout_constraintBaseline_toBaselineOf |
这里提醒一下大家,很多 ConstraintLayout
能够实现的约束条件在 RelativeLayout
中不能实现,比如对齐 View 的基线到另一个 View 的上或者下边缘。之所以没有列出来也是因为 RelativeLayout
中并没有相对应的属性实现。
GONE
ViewsRelativeLayout
实现的属性中,ConstraintLayout
没有实现的属性只有一个 android:layout_alignWithParentIfMissing
,这个属性将会让 View 组件能够在对齐对象不显示 GONE
的时候,对齐到父组件。举个例子,如果 A View 需要设置左对齐到toRightOf
另一个 View (这个就命名为 B ) ,当B不显示的时候,就会左边对齐到父组件。
ConstraintLayout
在这点上跟 RelativeLayout
或者说大多数布局都不同,它会考虑显示为 GONE
的组件的位置并且针对不显示任何东西的 View 的约束 Constraint 仍然有效。唯一的缺陷是这个 GONE
的 View 的宽高是 0,而且外边距 margin 也被忽略不考虑。
为了适应这种场景的情况下,ConstraintLayout
拥有一个属性 app:layout_goneMargin[Left|Start|Top|Right|End|Bottom]
可以用于当约束对象是一个 GONE
View 的时候,设置外边距 margin 。在下面的例子中,当按钮消失 gone 的时候,原本存在于输入框对按钮的属性 start_toEndOf
的 24dp
的外边距启用了另一个属性 app:layout_marginGoneStart="56dp"
,如下动态图所示:
PercentLayout
通常被用于响应式布局设计,当需要根据父组件来缩放子组件到百分比的情况。
首先我们要看的特性是,子组件要实现占据父组件的宽度或者高度的固定百分比。它在 PercentLayout
中是通过属性 app:layout_widthPercent
和 app:layout_heightPercent
来实现的(此处的命名空间 app 是因为 PercentLayout 的库引入是来自于 support library)。要实现该特性的话,我们可以通过 ConstraintLayout
中的 Guidelines 参照线来实现。假如我们需要实现 app:layout_widthPercent="25%"
的特性,我们可以首先创建一个参照线,移动到 25%
处:
然后我们就需要创建一个 View 将它的创建约束到父组件的 start
边缘以及 end
约束到参照线。在此处,我们没有使用 left
而使用 start
是为了更友好的支持 RTL 语言(从右到左布局,right to left)
同时,我们还需要注意的是我们需要设置 android:layout_width
是被设置成了 0dp
或者 match_constraint
(源码层面,他们是一样的)。然后移除这个 View 的外边距,那么这个 View 的宽度就会自动设置成父组件的 25%
,进一步操作如下图所示:
以上例子的 XML 源码如下:
|
实际上真正对齐百分比宽高是由 Guidline 完成的,百分比宽的 TextView 只是创建了一个约束到参照线 Guideline就能实现固定的百分比宽高。
PercentLayout
还可以让我们实现相对于父组件的百分比外边距 margin 。相比上面百分比宽高的例子,我们一样需要在指定百分比位置设置一个 Guideline参照线,但不是设置 View 的宽度约束到参照线,而是设置 View 的 start
边缘约束到参照线。举个例子,如果我们需要设置的效果是 app:layout_marginStartPercent="25%"
,我们创建一个在 25%
位置的参照线,然后设置 View 的 start
边缘约束到参照线,如下图所示:
然后,在这个例子中我们还设置这个 View 的宽度 android:layout_width="wrap_content"
,然后移除各个方向的外边距 margin ,然后 View 就会有相对于父组件的 25% 宽度外边距 margin。
在 XML 中,参照线 Guidline 是跟上面的例子一样的设置,如下:
|
区别在于,我们的 View 如何设置约束到这个参照线,在这个例子,我们需要设置 app:layout_constraintStart_toStartOf="@+id/guideline"
然后如上面编辑器中说的一样设置 android:layout_width
为 wrap_content
和 android:layout_marginStart
为 0dp
。
最后一个特性就是实现 PercentLayout
的横纵比特性,通过它可以让高度固定比例为宽度的函数,或者反过来。关于 ConstraintLayout
如何实现横纵比尺寸,我有另一篇文章 更详细的讲解了这个特性。首先我们设置一个固定的比例,然后设置这个 View 的宽高为 match_constraint
或 0dp
:
然后我们设置好水平方向的两个约束条件,然后至少保留一个垂直方向的约束不设置,那么我们的组件 View 高度就会是依赖于宽度的函数,然后通过移动参照线来缩放 View 的宽度的时候就会发现高度也会相应的根据函数变化。
在 XML 中,真正设置了宽高比的属性是 app:layout_constraintDimensionRatio
为想要的值,其他规则跟在视图编辑器中是一样的。
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" |
最后提醒一下,没懂的小伙伴可以看看另一篇文章 ConstraintLayout基础系列之尺寸横纵比 dimensions。
]]>有时候,我们需要创建一些固定方向比的 View 组件,最常使用固定横纵比的就是当 ImageView
用于展示一些固定横纵比的图片的时候。举些例子,书面封面(尺寸横纵比多种多样),电影海报(一般是 4:6 ),电影剧照(一般是 1.85:1 或 2.39:1 ),电视剧(一般是 4:3 或 16:9 )
对于不熟悉什么是横纵比的,横纵比就是表示了 View 的宽度与高度的比例 w:h
。例如,对于一个拥有横纵比为 4:6
拥有宽度为 40dp
的 View 组件有着高度是 60dp
,若它的宽度改为 30dp
则它的高度就是 45dp
。
若我们现实的图片能保证同样的横纵比和像素大小,我们可以简单的在两个方向上使用 wrap_content
即可。然而,现实情况由于数学四舍五入等多种原因都有可能造成实际现实的一些小误差。如果只是现实一个图片可能不会有多大问题,但是如果多个图片展示的时候小问题也会被有很不好的视觉效果,甚至当有 View 对齐于这些图片的 ImageView 的时候,也因此产生了变化,整体就会造成布局不平衡混乱了。
对于这个问题的解决方案之一是,通过创建继承于 ImageView
的子类,并通过覆写 onMeasure()
来实现固定横纵比的布局。常用的 support library 中的 PercentLayout
也提供了一些机制来结局这类横纵比问题。
同样的 ConstraintLayout
也提供了机制来专门解决这个问题,选择想要控制横纵比的 View 然后通过属性视图中修改 ratio
值来改变横纵比,如下图红色圈内设置:
如上图,我们设置的 View 组件有着向父组件的 start 和 top 边缘的约束,它的 end 边缘则约束向一条参考线,而 bottom 边缘则没有被约束,这个 View 的 layout_width
和 layout_height
都被设置成 match_constraint
,表示他们会根据所有的约束来设置宽高。在布局阶段这个组件的宽度就被计算好了,但是它的高度好像没有被确定。然后,因为设置了宽高横纵比,高度其实也被确定了,只是宽度的一个函数输出值(在以上例子中横纵比是 16:9 )
这样设置的好处就是,当宽度变化的时候,高度自动跟着变化,如下图通过移动这个 View 组件 end 边缘约束向的参照线就可以看到效果。
上例中的 XML 源码如下:
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" |
可以发现,设置横纵比的属性是 app:layout_constraintDimensionRatio
,而这个值有两个部分组成:方向和比例值。
通过上面的视图编辑器,我们已经知道了宽度就是输入的固定值,从而设置了方向是 h
标识了 horizontal
。其实这个方向可以不用设置,在运行时的 layout 布局过程就可以计算推断出来,但显示的在 xml 源码中声明避免了所有可能出现模棱两可的情况发生。在大多数情况下,这非常不必要因为本身方向是不言自明的,就像例子中,唯有高度没被约束,很容易推断出来高度是根据宽度来的变量函数。
这种横纵比的组件往往又很大的说服力,当横纵比的权利被赋予的时候。
最后还要提到的是,上文提到的宽高属性被设置成 match_constraint
实际上在 XML 源码中表现是被设置成 0dp
,这就像 LinearLayout
的 weight
属性一样,会在 XML 中设置为 0dp
,而实际大小会根据父组件在布局 layout
过程中的大小来决定计算出来。
如果你熟悉 UI 设计软件你应该已经使用过参照线 guidelines 并对它的作用熟悉了。参照线 guideline 提供了视觉上的参照用于 Views 的对齐,而且不会在运行的时候显示,只要你熟悉它的使用了就会发现它对你的对齐实现非常方便。 Google 的 Material 设计原则推荐了使用 keylines 。该文章将介绍如何通过参照线 guidelines 来快速实现这些。
创建垂直参照线 guidelines 需要在 blueprint 视图上右键打开上下文菜单,然后选择 Add Vertical Guideline
即可创建。如下图所示:
当前版本的视图编辑器(Android Studio 2.4 alpha 7)默认隐藏参照线,选择 blueprint 内的 View 即可看到参照线。
当前的参照线 guidelines 有三种类型,默认的第一种参考线是会有一个固定的偏移向父组件的 start
边缘(偏移量的单位是 dp
)。本文开头创建的参照线对于父组件的 start
边缘参考线为 16dp
。为了适配从右向左的布局设置,所以我们应该采用 start
边缘而不是 left
边缘。
第二种参考线则是有一个固定的偏移向父组件的 end
边缘。而最后一种参考线是根据父组件 ConstraintLayout 的宽度百分比来放置,而且参照线存在一个标识器,可以通过点击这个标识按钮来切换参考线的类型,如下图所示:
向 start
和 end
类型的偏移量参照线非常适用于 keylines 的使用场景,而百分比形式的参照线则提供了类似于 PercentLayout
的一些功能。
只要已经创建了参照线,我们可以通过拖动除类型标志器以外的地方的参照线来移动。
你可以在例子中看到,对于一些特殊位置,如左右方向的 8dp
偏移量以及居中的 50%
位置,会对参照线有吸引力。
到此,我们已经知道参照线 guidelines 的所有类型以及如何创建和移动,现在要讨论一下参照线对于我们的用途,用它来作为其他 views 的约束 constraint 对象,也就是说我们可以创建从 view 的一个锚点到参照线的约束 constraint 对象来根据参照线来对齐这个 view。然后如果我们移动参照线,受约束的 view 也会跟着一起移动:
这个特性其实很强大,例子中只有一个 view 约束指向了参照线,但如果我们有多个 views 约束指向到参照线,移动会让所有的 views 跟着一起动。
对于喜欢追根寻底的开发者,我们可以更深一步看看 Guideline 的内部实现。源码中 Guideline
类其实就是一个 View
,而且它不会渲染任何东西因为它实现了一个 final
的 onDraw()
而且固定了它的可见性为 View.GONE
,这就决定了运行时不会显示任何东西,而在 View
的 layout
布局过程中它会占据一个位置,而其他组件可以通过它来布局对齐。所以实际上的 Guideline
只是一个极其轻量级没有任何显示但是可以用于约束布局对齐的 View
组件。
我们可以看看一个 View 约束对齐到参照线的例子:
|
参照线 Guideline 拥有了一个属性 app:orientation="vertical"
来描述它是一个垂直的参照线(此处也可以设置为 horizontal
)。它还有属性 app:layout_constraintGuide_begin="16dp"
来描述它是一个对齐父组件的 start
边缘的 16dp
偏移量处。再次提醒的是,应该用 start
边缘而不是 left
边缘。当然切换向 end
类型的话,可以使用另一个属性 app:layout_constraintGuide_end="..."
,切换为百分比类型的参照线则是设置属性 app:layout_constraintGuide_percent="0.5"
值得取值范围为 0.0
到 1.0
,描述的是百分比偏移量。
而此处的 TextView
源码则表现了,我们可以从 TextView
像对其他 View
一样对 Guideline
添加约束向量,这样的原因就是刚刚分析的原理,因为 Guildeline 就是一个特殊的 View
。
Chain
链是一种特殊的约束让多个 chain 链连接的 Views 能够平分剩余空间位置。在 Android 传统布局特性里面最相似的应该是 LinearLayout
中的权重比 weight ,但 Chains
链能做到的远远不止权重比 weight 的功能。
前面概要已经提到了 Chain 链是由多个 Views 组合的,所以要创建一个 Chain 链就需要先选择多个想要链接到一起的 Views ,然后再右键选择 ‘Center Horizontally’ 或者 ‘Center Vertically’ 来创建水平链或者垂直链。如下,创建一个水平链:
首先,可以注意到 Chain 链两边末端的两个 View 已经存在了相对于父组件的左边缘和右边缘的约束。 Chain 链的创建定义的是 Chain 链组件之间的间隙关系,并不影响原有的非成员间的约束。如下刚刚创建后的图中,有很多视图上的标识符需要解释一下。
观察截图,可以看到 Chain 链组件之间的连接类似于链条图案,而边缘两端的 View 与 父组件之间的连接类似于弹窗图案。最外面的连接图案代表了 Chain 链的链接模式(chain mode),链接模式决定了 Chain 链如何分配组件之间的剩余空间,你可以从 Chain 链每个组件下面的 “转换 Chain 模式” 按钮来切换 Chain 链模式。
Chain 链模式一共有三种,分别为:spread
,spread_inside
和 packed
。
Chain 链的默认模式就是 spread
模式,它将平分间隙让多个 Views 布局到剩余空间。
Chain 链的另一个模式就是 spread inside
模式,它将会把两边最边缘的两个 View 到外向父组件边缘的距离去除,然后让剩余的 Views 在剩余的空间内平分间隙布局。
最后一种模式是 packed
,它将所有 Views 打包到一起不分配多余的间隙(当然不包括通过 margin 设置多个 Views 之间的间隙),然后将整个组件组在可用的剩余位置居中:
在 packed chain 链模式,打包在一起的 Views 组可以进一步通过控制修改 bias
值来控制打包组的位置,在例子中 bias
模式是 0.5
将 Views 组居中。
spread
和 spread inside
Chain 链可以设置每个组件的 weight 权重,这跟 LinearLayout
的 weight
权重设置很像。当前版本(Android Studio 2.4 alpha 7)的视图编辑器不能直接操作设置这个权重,不过我们可以通过属性视图(properties 视图)来手动设置属性。
对特定的组件设置 spread
权重,首先得选择这个 View 组件,假设该 View 是在一个水平的 Chain 链中,那么需要在属性视图(properties 视图)中设置 android:layout_width="0dp"
然后修改 app:layout_constraintHorizontal_weight="1"
,如下所示:
这时候观察 View
组件在 blueprint 蓝图视图模式中的改变,它的上边和下边缘都从直线变成了类似手风琴的线条,这符号就表示了 spread
或 spread inside
Chain 链模式下的被设置了权重的组件。
同时要注意的是,在 packed
Chain 链模式下设置权重 weight
并没有作用。就是说并不像 spread
和 spread inside
模式中表现的占据尽可能的剩余空间,在 packed
模式下该组件就会被收缩成 0 大小。
虽然假如在 XML 中存在特有的属性设置 Chain 链模式会比较好,但事实上并没有特有的属性,而是现有的约束条件的一种组合。在 XML 中设置 Chain 链模式只需要设置好双向互补的约束。本文中首个例子的 XML 源码如下:
|
在 textView
中设置了约束属性 app:layout_constraintEndToStartOf="@+id/textView2"
,而相对的在 textView2
也设置了约束属性 app:layout_constraintStart_toEndOf="@+id/textView"
,本质上就是创建两个约束条件,同一对锚点但是方向相反的约束条件,这就是 Chain 链的定义方式。
另外,textView
中的约束属性 app:layout_constraintHorizontal_chainStyle="spread"
就是指定了链模式 spread
你可以通过修改成 spread inside
或 packed
来切换链模式,而且这个约束属性必须在链头,即是链组件中的第一个组件。
而设置链模式的 bias
可以通过设置约束属性 app:layout_constraintHorizontal_bias="0.75"
从 0.0
- 1.0
。
最后,我们就可以通过设置属性 android:layout_width="0dp"
以及 app:layout_constraintHorizontal_weight="1"
来设置 Chain 链中组件的权重。
ConstraintLayout
的核心基础就是创建约束。约束定义了布局内两个组件之间的关系,从而控制组件的布局位置。对于刚接触 ConstraintLayout
但对 RelativeLayout
熟悉的开发者来说,约束布局的工作原理很像 RelativeLayout
中通过创建组件间关系来控制布局。
最容易创建约束布局的方式是通过 Android Studio 中的 design
可视化布局编辑器。本文章的例子都通过蓝图 Blueprint
视图来查看展示,我们先简单看看在 Blueprint
视图中的 TextView
。
清晰地可以看到 TextView
组件,以及两个箭头符号表示在这个 TextView
组件上存在约束将它对齐到父组件 ConstraintLayout
的左边缘和上边缘。待会再来看它们是如何创建的,还可以看到存在 16dp 的外边距让父组件 ConstraintLayout
和 TextView
的组件边缘之间保留了一些间隙。选择 TextView
组件就会看到如下的缩放和约束锚点。
边角上的小正方形是缩放的控制点,通过拖拉这些点就可以对 TextView
进行缩放。但是这个大多数情况并不是很适用,因为使用这种方式进行缩放后的组件将保持固定的尺寸,而我们往往更需要 TextView
根据具体情况响应式大小。
每条边中间的锚点就是约束锚点,我们就是用这个锚点来创建约束的。其中左边和上边的锚点里面有蓝点表示这个锚点已经存在了一个约束,相对的右边和下边的空心锚点则表示没有约束。从这个例子,我们就可以看到 TextView
的布局位置就通过定义约束来对齐了父组件。
任何继承了 TextView
的子组件都拥有另一个锚点:被称为基线(baseline)。这就允许我们通过该锚点来调整组件内的文字对齐基线。选择 TextView
后出现下方按钮,点击其中的 ab
按钮来显示这个锚点。
在 TextView
上出现香肠状的控制锚点就是基线约束锚点。我们可以通过给这个锚点添加约束就像下面提到给四个边的约束锚点添加约束一样。
另一个出现的下方按钮中是取消约束按钮(按钮中存在 ‘x’ ),点击将移除该组件上的所有约束。
创建锚点,我们只需要简单的从一个组件的锚点,拖动指向到另一个组件 View
的锚点。此处的例子,我们创建另一个 TextView
(id 为 textView2
,原来的那个 id 是 textview
),而且 textView2
已有一个对齐父组件左边的约束,我们再创建一个约束,从 textView2
的上边到 textview
的下边。而这个约束就会让 textView2
对齐到 textview
正下方,如下所示:
在此处还要注意,我们创建的约束是从 textView2
的上边到 textView
的下边,当我们选择这两个组件的时候,我们只会看到 textView2
的上边约束锚点存在约束,而 textView
的下边约束锚点是空心的不存在约束。
这样的原因是约束是单向的(除非我们谈论的约束是链接 chains ),所以这里例子创建的约束是属于 textView2
的,影响的也是 textView2
的布局位置是相对于 textView
的。因为该约束是只属于 textView2
的,反过来不会影响 textView
的布局位置
上面讲到的是同级组件间创建约束,而对于一个组件要创建相对于父组件的约束,则只是简单的将约束拖的方向到合适的父组件边缘即可,如下:
对于想了解在可视化布局下真正的存储的是如何的开发者,以下就是 上面例子的 XML 源码:
|
代码中的约束都是以 app:layout_constraint
开头的属性。我们可以看到 ConstraintLayout
中所有子组件都存在这些属性。让他们对齐父组件的边缘。你还可以看到 textView2
定义了一个约束声明了该组件的上边相对对齐到 textview
的下边。
值得一提的是,这些属性设置都是使用的 app
命名空间因为 ConstraintLayout
是像 Support libraries
也是作为库引入。它属于你的命名空间 app
而不是属于安卓框架(使用命名空间 android
)。
上面提到我们可以通过选中组件后出现的清空按钮来清除所有的约束。最后,我们还要介绍的是只删除其中一个约束。如果在 XML 源码中可以直接去掉相应的属性。若使用的是可视化编辑器,则通过点击约束锚点来去除约束条件。
]]>组成网站的资源中,图片往往是网络负载的主要组成部分,占据了大部分负载而且随着时间推移,这个现象仍然会保持。虽然现在的网络连接速度持续改进,但是同样出现了越来越多的高 DPI
分辨率设备,为了在这些设备上有更好的表现,就需要有更高清晰度的图片,高清晰的同时就需要更大的文件大小。而因为仍需要支持一些相对非高 DPI
的分辨率设备,就需要有低清晰度的图片,从而就需要有更好的方案来实现针对不同设备提供不同的图片,也就是响应式图片 (Responsive images)。同时还要遵循 Web 资源的准则,保持性能和表现的平衡,合理地提供图片资源而不加载不需要使用的多余的资源。
其实,响应式图片简化来看,关键就是针对不同的设备选取合适的类型和清晰度。
给不同的设备提供刚好合适的清晰度分辨率是对性能最优的,过高的清晰度不仅意味着更多的传输时间还意味着需要缩放渲染时间,过低的清晰度就会让高 DPI
设备得不到应该有的最优体验。理论上来说确实是有可能给所有的设备提供刚好合适的清晰度。但设备的分辨率太多了,还存在不同的 DPI
,所以实践上目前更合理的方案是选择一系列的主要适配的设备产生对应的图片集合,其他设备就适当的选择相近的方案图片进行缩放。
如果设置响应式图片取决于你是在哪里进行适配(CSS、HTML、JS)以及用于哪些用途(UI、UX),其中主要出现在 CSS 和 HTML 中,以下主要讲解如果选择合适的图片类型,以及如果根据清晰度等因素响应式选择图片。
目前,主要支持两个大类:点阵图(位图 bitmap images、栅格图 raster images)、矢量图(可缩放矢量图 SVG)
有损图片的压缩算法往往直接从源文件压缩的过程中丢弃一部分信息,核心就是想通过图片质量根据等级设置 1 ~ 100 降低来换取更小的大小。
最常用的有损图片就是 JPEG ,比如:数码相机拍照的时候存储成无损格式,但当下载到电脑的时候,通过有损压缩算法转换成 JPEG 格式的图片。
JPEG 支持24位真彩色,但是不支持透明,所以经常用于类似照片、不透明的图片等方面。它的缺点也很明显,极致有损的压缩会导致显示效果有明显的不同,特别容易受到二次压缩的时候的影响,即是从一个已经压缩的文件中进行有损压缩的时候效果下降特别明显。但正常来说,注意选择合适的压缩等级,效果下降往往就不会被注意到,同时大小下降百分之几十。
更出色的有损图片: WebP,但 WebP 并不是所有浏览器都支持的,所以需要实现对不支持浏览器的回退到显示 JPEG 或者 PNG格式
无损图片的压缩算法则正好相反,不会丢弃源文件中的信息。在 Web 中当图片质量非常关键的时候,无损图片正好合适,例如网站的 icon。
无损图片根据色值存储的位数又分为 8位图(8-bit images - 256-color)、真彩色图(Full-color images - 16.7-million-colors)
其中,无损的8位图 有代表如:png、gif,其中 gif 支持动画,他们都很适合对图片质量有要求,但是本身不需要全色和多变的透明色。
PS: 虽然 gif 只支持 8位图,但是动图可以根据一些 hack 来实现更清晰的视觉表现,参考 知乎网址
其次,无损真彩色图的代表如:全色的 PNG (24-bit png),无损的 WebP,其中全色的 PNG 支持的色值会比 WebP 更广泛。另外注意的是,当你不需要透明色且对质量要求不是特别高的时候,应优先考虑选择 有损 WebP 或者 JPEG 而不是全色 PNG。
无损图很合适艺术图片、肖像研究、摄影等。选用8位图还是真彩色图需要经验,基础规则就是只使用简单颜色的图片使用8位图格式,当不只是使用简单颜色或者需要全部透明色支持的图片就选用全彩色图。
从互联网早起以来,栅格图格式只有 JPEG,GIF,PNG,为了更优化 Web 图片的加载速度和性能,谷歌(google)开发了全新的图片格式 WebP。图片压缩体积大约只有 JPEG 的2/3,并能节省大量的服务器带宽资源和数据空间,在压缩方面比 JPEG 和 PNG 的效果更优。
虽然截至目前已经有很多浏览器支持了 WebP ,比如 Chrome (谷歌浏览器) 、国内的 QQ浏览器、UC 浏览器等 Webkit内核的浏览器都支持这种格式,但是同样有一些如 IE系列、Firefox系列的浏览器占比较高的主流浏览器对 WebP 不支持,所以使用 WebP 的时候一定要实现相应的回退策略(订阅本系列教程,我会继续讲解如何更方便地解决这个问题)。
WebP 有着比 JPEG 和 PNG 更优的压缩效果,有损 WebP 支持透明度也是 JPEG 做不到的,虽然没有 PNG 色度值多,但是除非是在某些对色值有精度要求的网站图片,大部分场景开发者都会优先选择支持 WebP。
另外一方面,WebP 是支持动画的,而且压缩和表现效果比 GIF 更优,但因为不好统计以及判断浏览器对动画 WebP 是否支持,所以不好控制回退,除非你对这种占比较小的浏览器(支持 WebP 但是不支持动画 WebP)用户不考虑,建议不要使用 WebP 替代 GIF 。
矢量图 SVG 使用向量矩阵来存储几何图形以及比例实现支持任何尺寸的缩放,可以看到以下图片的例子,放大几倍后的 svg 质量依然很完美,这就是 SVG 对比点阵图的最大优点。
不过现在的设备都是以像素为基础单位的,所以所有显示输出最后都是被转化为像素。这就能明显标识矢量图想要显示在这些设备上就要经受一个叫栅格化的过程,每当图片的尺寸有变化、缩放都要经过这个计算栅格化过程,从而保证了每次显示的图片质量。
如果你熟悉创建向量矩阵,你应该熟悉注入 AI 之类的设计软件。即使这些软件的支持的本地文件格式是二进制,SVG 的格式是 XML(文本类型),它根节点标识了它的媒体类型 image/svg+xml
。这个特性使它可以在文本编辑器中直接修改,或者内联到 HTML 中,甚至可以直接在 SVG
中使用 CSS
和 media queries (媒体查询)
。
虽然从 1999 年的 W3C 标准中就已经将 SVG
纳入了,但真正被网站使用就是在最近这几年。因为对于不同分辨率设备以及显示屏的显示无暇让它倍受欢迎。
当然啦,SVG 并不是全能的,优势也主要限制在以下几个方面,例如:呈现 Logo 、图标、线形艺术等。另一方面,那些固定色块由几何图形组成的图片也很适合。
通过上面的大致介绍每种图片格式的使用场景,掌握一个场景选择哪些图片格式最适合显得尤为重要,这里整理了一些常用的注意点以及经验来划分图片格式的选择,你可以依照表格根据要提供的内容来选择最合适的图片格式。
图片格式 | 支持颜色 | 图片类型 | 压缩类型 | 合适场景 |
---|---|---|---|---|
PNG | 全色 | 栅格图 | 无损 | 质量缺失不可接受的情况,亦或是展示内容需要完全的透明度或者全色。适用于任何一种图片格式,但不像适用于照片的 JPEG 压缩度那么高 |
PNG (8) | 256 | 栅格图 | 无损 | 内容不需要全色或者只需要简单 1 位透明度支持的,比如图标、像素阵列 |
GIF | 256 | 栅格图 | 无损 | 没有 PNG (8) 压缩度高,其他一样,但支持动画,也主要用于动画 |
JPEG | 全色 | 栅格图 | 有损 | 内容需要全色,或者没有使用透明度,而且可以接受质量的丢失。比如照片 |
SVG | 全色 | 矢量图 | 无压缩 | 内容支持全色而且要支持缩放的时候质量不变。特别适合线形艺术、几何图形、其他非照片类型的内容,不需要特别处理就优化了多分辨率显示的效果 |
WebP(有损) | 全色 | 栅格图 | 有损 | 其他方面跟 JPEG 一样,但更好的是支持透明度以及压缩性能以及效果 |
WebP(无损) | 全色 | 栅格图 | 无损 | 其他方面跟全色 PNG 一样,但有更好的压缩性能 |
HTTP/2
是 Web 开发的新标准,拥有很多不错的优点能够让 Web 访问更快且开发的工作更轻松简单。比如,引入多路复用传输不用合并资源,服务器推送(Server Push)资源让浏览器预加载。
该文不会讲述 HTTP/2
的所有优势。你可以通过上篇文章了解更多[译] Node.js, Express.js 搭建 HTTP/2 服务器。该文主要关注于在 Node.js
环境使用 Express.js
和 HTTP/2
库 spdy。
服务器推送(Server Push)工作方式是通过在一个 HTTP/2
请求中捆绑多个资源。在底层,服务器会发送一个 PUSH_PROMISE
,客户端(包括浏览器)就可以利用它且不基于 HTML
文件是否需要该资源。如果浏览器检测到需要该资源,就会匹配到收到的服务器推送的 PROMISE
然后让该资源表现的就像正常的浏览器 Get
请求资源。显而易见,如果匹配到有推送,浏览器就不需要重新请求,然后直接使用客户端缓存。这推荐几篇文章关于服务器推送(Server Push)的好处:
这是个关于在 Node.js
实现服务器推送(Server Push)实践教程。为了更清晰精简,我们只实现一个路由地址 /pushy
的 Node.js
和 Express.js
服务器,它会推送一个 JS 文件,正如之前所说,我们会用到一个 HTTP/2
库 spdy。
先解释一下,为啥在 Node.js
环境选择 HTTP/2
库 spdy。当前来说,为 Node.js
主要有两个库实现了 HTTP/2
:
两个库都跟 Node.js
核心模块的 http
和 https
模块 api 很相似。这就意味着如果你不使用 Express
,这两个库就没什么区别。然而, spdy
库支持 HTTP/2
和 Express
,而 http2
库当前不支持 Express
。这就是为什么我们选择使用 spdy
, Express
是 Node.js
适合搭配的实践标准的服务框架。之所以叫 spdy
是来自于 Google 的 SPDY 协议后来升级成 HTTP/2
。
要在浏览器(Firefox, Safari, Chrome, 或者 Edge)中访问使用 HTTPS ,你需要生成密钥和证书。去搜索 “ssl 密钥生成” 或者按照以下步骤去生成密钥、证书。在该文提供的源码中没有上传生成的密钥和证书
$ mkdir http2-node-server-push |
按照以上步骤,你就会产生三个 SSL 文件:
你就可以在 Node.js 的 server
脚本中读取 server.key 和 server.crt。
首先,通过 package.json 初始化项目和下载项目依赖:
npm init -y |
当前的目录结构如下
/http2-node-server-push |
然后,在 package.json
的 scripts
中添加两个脚本行,去简化命令(node-dev、自动重载):
"start": "./node_modules/.bin/node-dev .", |
现在就可以开始使用 Node.js 、 Express.js 、 spdy 编写这个简单实现的带服务器推送 HTTP/2 服务器
首先,创建 index.js
脚本,并引入以及实例化依赖,看看查看上面的项目目录结构。其中,我使用了 ES6/ES2015 的语法 const
来声明依赖,如果你不熟悉该声明语法,你可以进一步阅读Top 10 ES6 Features Every Busy JavaScript Developer Must Know。
const http2 = require('spdy') |
然后,设置 morgan logger
来监听服务器服务了哪些请求。
app.use(logger('dev')) |
设置主页,该页面显示了 /pushy
是我们服务器推送的页面。
app.get('/', function (req, res) { |
服务器推送只需简单的调用 spdy
实现的 res.push
,我们将文件路径名传输进去作为第一个参数,浏览器会使用这个路径名来匹配 push promise
资源。res.push()
的第一个参数 /main.js
一定得跟 HTML 文件中需要的文件名相匹配。
而第二个参数是一个可选的对象,设置了该资源的一些资源信息描述。
app.get('/pushy', (req, res) => { |
你可以看到,stream
对象有两个方法 on
和 end
。前者监听了 error
和 finish
事件,而后者则监听完成传输 end
,然后就会 main.js
就会触发弹窗。
或者,如果你拥有多个数据块,你可以选择使用 res.write()
然后最后使用 res.end()
,其中 res.end()
会自动关闭结束 response
而 res.write()
则让它保持开启。(该文的源码中未实现这种情况)
最后,读取 HTTPS 密钥和证书并使用 spdy
启动运转服务器。
var options = { |
该实现的关键就在于,围绕着 streams(流)
。不是树林中的河流,而是指开发者使用的从源头到客户端的建立起的数据通道流。如果你几乎不懂流以及 Node.js
和 Express.js
的 HTTP 的请求和返回信息,你可以看看该文章 You Don’t Know Node
使用命令 node index.js
或者 npm stat
运行服务端脚本,然后访问 https://localhost:3000/pushy,就可以看到弹窗!而且我们在该路由不存在文件,你可以查看服务器终端的 logs ,只会有一个请求,而不是没使用服务器推送的时候的两个请求(一个 HTML、一个 JS)。
可以在浏览器中检测收到服务器端推送的行为。Chrome 启动开发者工具,打开 Network 标签,你可以看到 main.js
不存在绿色时间条,就是说明没有等待时间 TTFB (Time to First Byte)详细
再仔细看,可以看到请求是由 Push
开始发起的(Initiator列查看),没有使用服务器推送的 HTTP/2 服务器或者 HTTP/1,这一列就会显示文件名称,如 index.html
发起的显示就是 index.html
。
实践就结束了,使用了 Express 和 Spdy 简单就实现了推送 JS 资源,而该资源可以用于后面 HTML 中 <script>
标签引入的。当然你也可以在脚本中使用 fs
来读取文件资源。事实上,这就是作者实现的 Express HTTP/2 静态资源中间件
设计原理,可以看看这篇文章
HTTP/2 拥有很多很好的特性,服务器推送是最被看好的特性之一。它的好处就在于当浏览器请求页面的时候,同时发送必需的资源文件(图片,CSS 样式,JS 文件),而不需要等待客户端浏览器请求这些资源,从而做到更快的第一次渲染时间
HTTP/2
库 spdy 让开发者在基于 Express 的应用能更容易的实现服务器推送特性。
可以下载参考本文的源码,然后为你自己的服务器编写服务器推送你的资源。
]]>现代互联网的 TCP/IP
协议发布于1975年,这项技术在41年前是多么令人惊讶。自它发布开始大部分形式,我们使用 HTTP
和 后续接任者 HTTP/1.1
来实现客户端和服务端的通讯。它能很不错的传输 Web
,但今时今日的开发者建立网站的方式已经发生了巨大的改变。存在各式各样的外部资源链接例如图片、CSS
文件、JavaScript
资源。资源的种类数量只会持续增长。HTTP/2
是针对表现一直不错的旧协议 HTTP
自从1991年发布以来这15年的第一次大的升级改动!它为优化现代浏览器而生。性能更加优越而且不用使用复杂的行为例如域名分片
(通过多个域名发送资源)或者资源文件合并`(提供一个整合的大资源而不是多个小资源)
HTTP/2
是当前 web
的新标准,其雏形是 Google 的 SPDY
协议。当前已经被大多数主流浏览器支持,且很多网站已经通过该协议实现。例如访问 Yahoo 的 Flickr
在使用的是 HTTP/2
协议(截图时间为2016年7月).
HTTP/2
和 HTTP/1.1
的使用没什么区别,仍然可以在 body
中使用类 xml
的语法,使用 header
协议头字段, 状态码, cookies, methods, URLs, 等等。开发者熟悉使用的东西都还可以继续在 HTTP/2
使用。
HTTP/2
的优势如下:
HTML
渲染再去加载其他的 CSS
和 JS
文件;HTTP/1.1
请求的头部总是重复一样的内容,而 HTTP/2
则强制对所有请求的头部进行了去重压缩;TLS(HTTPS)
上的 HTTP/2
。虽然目前对于 HTTP/2
还不能完全满足一些苛求,但是直到更好的技术出现以前,当前是一项明显的技术进步。让我们来看看,作为 Web
开发者需要了解的必要知识。大部分适用于 HTTP/1.1
的优化技巧在 HTTP/2
中变成多余的,其中一些甚至反而会影响 HTTP/2
上的网站性能,例如:
HTTP/2
协议上更好的方式是使用多个的小文件,而不是一个大文件。Grunt
、 Gulp
、 Webpack
将会因此特性被放弃使用,他们使 Web
开发更高的复杂度,极高的学习曲线,以及管理项目的依赖关系。HTTP/1.1
不适用于 HTTP/2
的是,域名分片(为了绕过TCP并行请求数量限制)。虽然它不一定在所有情况下有害,但对于 HTTP/2
的多路复用传输,这样做也已经没好处了。之所以建议不在 HTTP/2
使用域名分片,还因为每个域名会带来额外的查询负载。如果真的有需要,那么更好的方式是解析多个域名到同一个IP,而且保证你使用的是通配符证书或整合了多域名的证书,从而减少域名查询的时间。若想了解更多关于 HTTP/2
的介绍,可以看看官网。
现在,让我们看看怎么通过 Node.js
搭建 HTTP/2
服务器。
创建一个新文件夹以及自己签发的 SSL
证书。
$ mkdir http2-express |
当你访问服务器的时候,因为浏览器默认不信任自己签发的证书,请确保选择 “高级” 和 “继续访问 localhost (不安全)” 或者将 localhost 设置成不安全访问的例外。
通过 npm
,初始化项目 package.json
,安装依赖 spdy
和 express
:
npm init |
创建应用的入口文件 index.js
,主要是引用以及实例化
const port = 3000 |
实现 Express.js
的 route
app.get('*', (req, res) => { |
通过 fs.readFileSync()
读取证书
const options = { |
然后,设置证书选项到 Express
对象:
spdy |
最后,node .
启动服务器
通过浏览器的开发者工具查看协议,就如刚刚我们查看 Yahoo 的 Flickr
协议一样。
可以看到,使用 Node.js 和 Express.js 配合库 node-spdy 实现 HTTP/2
简单易懂。大多数情况下,对你的业务代码是基本不需要修改的,想必,你的网站也已经使用了 HTTPS/SSL
(除非你的服务器只提供静态资源,否则你应该使用安全的 HTTPS/SSL
),即使是不使用 HTTP/2 你也可以替换 HTTP/1.1 而使用 SPDY
当然,在 Node.js 的大环境中,有很多的库,不只是 node-spdy 提供 HTTP/2
实现,例如:node-http2
HTTP/2
提供了更多更优的好处,而且不用使用复杂的优化技巧。开始享受 HTTP/2
给你带来的这些好处。展望光明的未来!
PS: 本文源代码地址在 http2-express
]]>