浅议Swift和OC不同

浅议Swift和OC不同

Objective-C几乎只有面向对象编程,Swift更注重值类型的数据结构,而objective-c遵循C语官的老一套,注重指针和索引!Swift是静态类型语言,而Objective-C是动态类型语言。本节会从数据结构、编程思路和语言特性三个角度来比较Swift和Objective-C这两种语言的异同。从比较当中我们也更能体会,尽管两者都是为iOS开发而定制的语言,但是Objectivec和Swift有着天壤之别。

数据结构

Swift为什么将String,·Array和Dictionary设计成值类型
关键词: #引用类型 #值类型 #值类型 #协议

在OC中,String,Array,和Dictionary均被设计为引用类型.
* 值引用相比引用类型,最大的优势在于可以高效的使用内存。值类型在栈上操作,引用类型在堆上操作。栈上的操作仅仅是耽搁指针的上下移动,而堆上的操作则牵连涉合并、移位、重新链接等。也就是说,Swift这样的设计大幅度减少了堆上的内存分配和回收的次数。同时copy-on-wirte又将值传递和复制的开销降到最低。
* Swift将String,Array和Dictionary设计成值类型,也是为了线程安全。通过Swift的let的设置,使得这些数据达到了真正意义上的“不变”,也从根本上解决了多线程中的内存访问和操作的顺序问题。
* Swift将String,Array和Dictionary设计成值类型还可以提升API的灵活度,例如,通过实现Collection这样的协议,可以遍历String,使得整个开发更加灵活、高效。

语言特性

OC和Swift动态特性的理解
关键词: #动态特性 #@tuntime #面向协议编程

runtime其实就是 Objective-C代 的动态机制runtime执行的是编译后的代码,这时它可以动态加载对象、添加方法、修改属性、传递信息等。具体的过程是,在Obective中,对象调用方法时,如[self.tablevrew reload].经历了两个阶段:

  • 编译阶段: 编译器(compiler)会把这句话翻译成ohjc_msgSend(self.tableView .
  • 运行阶段: 接收者(self.tableView) 会响应这个消息,期间可能会直接执行,转发消息,也可能会找不到方法导致程序崩溃.

    所以.整个流程是:编器翻译->给接收者发送消息一>接收者响响应消息。
    例如,在[self.tableview reload]中.self.tableview 及时接受者。reload就是消息。所以,方法调用的格式在编译器看来就是[receiver message]。

    其中接受者如何响应代码,就发生在运行时(runtime)。runtime执行的是编译后的代码,这是他客户已动态加载对象、添加方法、修改属性、传递信息等。runtime的运行时机制就是Objective-C的动态特性

    Swift目前被认为是一门静态语言.它的动态将性都是通过桥接OC 来实现的.如果要把其动态特性写得更“Swift“一点,则可以用Protocol 来处理.比如,可以将OC中的 reflection 这样写:

    if([someImage respondsToSelector:@selector(shake)]){
        [some Image performSelector:shake];
    } 
    

    在Swift中可以这样写:

    if let shakeableImage=someImage as?Shakeable{ 
        shakeableImage.shake()
    }
    

动画编辑器TimeLine动作流设计与实现

背景

最近在着手绘本课堂的设计与实现,中间有不少困难点需要攻克,大致分为以下几个模块:
– 容器配置化、工程化。
– 组件的集中管理,插件机制。
– 场景的切换和设计。
– TimeLine动作流处理。
– 多端同步的处理。
以上几点中,对于TimeLine动作流的处理尤为重要,同时也是最不好做的一个模块,下面简述一下怎么对TimeLine动作流进行处理。

以下具体内容请上confluence进行访问阅读:动画编辑器TimeLine动作流设计与实现

基于阿里云实现kubernetes cluster-autoscaler

概述

在业务上了kubernetes集群以后,容器编排的功能之一是能提供业务服务弹性伸缩能力,但是我们应该保持多大的节点规模来满足应用需求呢?这个时候可以通过cluster-autoscaler实现节点级别的动态添加与删除,动态调整容器资源池,应对峰值流量。在Kubernetes中共有三种不同的弹性伸缩策略,分别是HPA(HorizontalPodAutoscaling)、VPA(VerticalPodAutoscaling)与CA(ClusterAutoscaler)。其中HPA和VPA主要扩缩容的对象是容器,而CA的扩缩容对象是节点。
我们线上转码业务从每天下午4点开始,负载开始飙高,在晚上10点左右负载会回落;目前是采用静态容量规划导致服务器资源不能合理利用,所以记录下如何通过cluster-autoscaler来动态伸缩pod资源所需的容器资源池

应用aliyun-cluster-autoscaler

前置条件:

  • aliyun-cloud-provider

1. 权限认证

由于autoscaler会调用阿里云ESS api来触发集群规模调整,因此需要配置api 访问的AK。

自定义权限策略如下:

{
  "Version": "1",
  "Statement": [
    {
      "Action": [
        "ess:Describe*",
        "ess:CreateScalingRule",
        "ess:ModifyScalingGroup",
        "ess:RemoveInstances",
        "ess:ExecuteScalingRule",
        "ess:ModifyScalingRule",
        "ess:DeleteScalingRule",
        "ess:DetachInstances",
        "ecs:DescribeInstanceTypes"
      ],
      "Resource": [
        "*"
      ],
      "Effect": "Allow"
    }
  ]
}

创建一个k8s-cluster-autoscaler的编程访问用户,将其自定义权限策略应用到此用户,并创建AK.

2.ASG Setup

自动扩展kubernetes集群需要阿里云ESS(弹性伸缩组)的支持,因此需要先创建一个ESS。
进入ESS控制台. 选择北京Region(和kubernetes集群所在region保持一致),点击【创建伸缩组】,在弹出的对话框中填写相应信息,注意网络类型选择专有网络,并且专有网络选择前置条件1中的Kubernetes集群所在的vpc网络名,然后选择vswitch(和kubernetes节点所在的vswitch),然后提交。如下图:

image.png
其中伸缩配置需要单独创建,选择实例规格(建议选择多种资源一致的实例规格,避免实力规格不足导致伸缩失败)、安全组(和kubernetes node所在同个安全组)、带宽峰值选择0(不分配公网IP),设置用户数据等等。注意用户数据取使用文本形式,同时将获取kubernetes集群的添加节点命令粘贴到该文本框中,并在之前添加#!/bin/bash,下面是将此节点注册到集群的实例示例:

#!/bin/bash 
curl https://dl.ipalfish.com/kubernetes-stage/attach_node.sh | bash -s -- --kubeconfig [kubectl.kubeconfig | base64] --cluster-dns 172.19.0.10 --docker-version 18.06.2-ce-3 --labels type=autoscaler 

然后完成创建,启用配置

  1. 部署Autoscaler到kubernetes集群中
    需要手动指定上面刚刚创建伸缩组ID以及伸缩最小和最大的机器数量,示例:
--nodes=1:d:asg-2ze9hse7u4udb6y4kd25  
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: cloud-autoscaler-config
  namespace: kube-system
data:
  access-key-id: "xxxx"
  access-key-secret: "xxxxx"
  region-id: "cn-beijing"
---
---
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
  name: cluster-autoscaler
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: cluster-autoscaler
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
rules:
- apiGroups: [""]
  resources: ["events","endpoints"]
  verbs: ["create", "patch"]
- apiGroups: [""]
  resources: ["pods/eviction"]
  verbs: ["create"]
- apiGroups: [""]
  resources: ["pods/status"]
  verbs: ["update"]
- apiGroups: [""]
  resources: ["endpoints"]
  resourceNames: ["cluster-autoscaler"]
  verbs: ["get","update"]
- apiGroups: [""]
  resources: ["nodes"]
  verbs: ["watch","list","get","update"]
- apiGroups: [""]
  resources: ["pods","services","replicationcontrollers","persistentvolumeclaims","persistentvolumes"]
  verbs: ["watch","list","get"]
- apiGroups: ["extensions"]
  resources: ["replicasets","daemonsets"]
  verbs: ["watch","list","get"]
- apiGroups: ["policy"]
  resources: ["poddisruptionbudgets"]
  verbs: ["watch","list"]
- apiGroups: ["apps"]
  resources: ["statefulsets"]
  verbs: ["watch","list","get"]
- apiGroups: ["storage.k8s.io"]
  resources: ["storageclasses"]
  verbs: ["watch","list","get"]

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
  name: cluster-autoscaler
  namespace: kube-system
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
rules:
- apiGroups: [""]
  resources: ["configmaps"]
  verbs: ["create","list","watch"]
- apiGroups: [""]
  resources: ["configmaps"]
  resourceNames: ["cluster-autoscaler-status", "cluster-autoscaler-priority-expander"]
  verbs: ["delete","get","update","watch"]

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: cluster-autoscaler
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-autoscaler
subjects:
  - kind: ServiceAccount
    name: cluster-autoscaler
    namespace: kube-system

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
  name: cluster-autoscaler
  namespace: kube-system
  labels:
    k8s-addon: cluster-autoscaler.addons.k8s.io
    k8s-app: cluster-autoscaler
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: cluster-autoscaler
subjects:
  - kind: ServiceAccount
    name: cluster-autoscaler
    namespace: kube-system
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: cluster-autoscaler
  namespace: kube-system
  labels:
    app: cluster-autoscaler
spec:
  replicas: 1
  selector:
    matchLabels:
      app: cluster-autoscaler
  template:
    metadata:
      labels:
        app: cluster-autoscaler
    spec:
      priorityClassName: system-cluster-critical
      serviceAccountName: cluster-autoscaler
      containers:
        - image: registry.cn-hangzhou.aliyuncs.com/acs/autoscaler:v1.3.1-567fb17
          name: cluster-autoscaler
          resources:
            limits:
              cpu: 100m
              memory: 300Mi
            requests:
              cpu: 100m
              memory: 300Mi
          command:
            - ./cluster-autoscaler
            - --v=4
            - --stderrthreshold=info
            - --cloud-provider=alicloud
            - --nodes={MIN_NODE}:{MAX_NODE}:{ASG_ID}
            - --skip-nodes-with-system-pods=false
            - --skip-nodes-with-local-storage=false
          imagePullPolicy: "Always"
          env:
          - name: ACCESS_KEY_ID
            valueFrom:
              configMapKeyRef:
                name: cloud-autoscaler-config
                key: access-key-id
          - name: ACCESS_KEY_SECRET
            valueFrom:
              configMapKeyRef:
                name: cloud-autoscaler-config
                key: access-key-secret
          - name: REGION_ID
            valueFrom:
              configMapKeyRef:
                name: cloud-autoscaler-config
                key: region-id

测试自动扩展节点效果

Autoscaler根据用户应用的资源静态请求量来决定是否扩展集群大小,因此需要设置好应用的资源请求量。

测试前节点数量如下,配置均为2核4G ECS,其中两个节点可调度。

[root@iZ2ze190o505f86pvk8oisZ cluster-autoscaler]# kubectl get node
NAME                                STATUS   ROLES         AGE   VERSION
cn-beijing.i-2ze190o505f86pvk8ois   Ready    master,node   46h   v1.12.3
cn-beijing.i-2zeef9b1nhauqusbmn4z   Ready    node          46h   v1.12.3

接下来我们创建一个副本nginx deployment, 指定每个nginx副本需要消耗2G内存。

[root@iZ2ze190o505f86pvk8oisZ cluster-autoscaler]# cat <<EOF | kubectl apply -f -
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: nginx-example
spec:
  replicas: 2
  revisionHistoryLimit: 2
  template:
    metadata:
      labels:
        app: nginx-example
    spec:
      containers:
      - image: nginx:latest
        name: nginx
        ports:
          - containerPort: 80
        resources:
          requests:
            memory: 2G
EOF
[root@iZ2ze190o505f86pvk8oisZ cluster-autoscaler]# kubectl get pod
NAME                             READY   STATUS    RESTARTS   AGE
nginx-example-6669fc6b48-ndclg   1/1     Running   0          15s
nginx-example-6669fc6b48-tn5wp   1/1     Running   0          15s

看到由于有足够的cpu内存资源,所以pod能够正常调度。接下来我们使用kubectl scale 命令来扩展副本数量到4个。

[root@iZ2ze190o505f86pvk8oisZ cluster-autoscaler]# kubectl scale deploy nginx-example --replicas 3
deployment.extensions/nginx-example scaled
[root@iZ2ze190o505f86pvk8oisZ ~]# kubectl get pod
NAME                            READY   STATUS    RESTARTS   AGE
nginx-example-584bdb467-2s226   1/1     Running   0          13m
nginx-example-584bdb467-lz2jt   0/1     Pending   0          4s
nginx-example-584bdb467-r7fcc   1/1     Running   0          4s
[root@iZ2ze190o505f86pvk8oisZ cluster-autoscaler]# kubectl describe pod nginx-example-584bdb467-lz2jt | grep -A 4 Event
Events:
  Type     Reason            Age               From               Message
  ----     ------            ----              ----               -------
  Warning  FailedScheduling  1s (x5 over 19s)  default-scheduler  0/2 nodes are available: 2 Insufficient memory.

发现由于没有足够的cpu内存资源,该pod无法被调度(pod 处于pending状态)。这时候autoscaler会介入,尝试创建一个新的节点来让pod可以被调度。看下伸缩组状态,已经创建了一台机器

接下来我们执行一个watch kubectl get no 的命令来监视node的添加。大约几分钟后,就有新的节点添加进来了。

[root@iZ2ze190o505f86pvk8oisZ ~]# kubectl get node
NAME                                STATUS   ROLES             AGE     VERSION
cn-beijing.i-2ze190o505f86pvk8ois   Ready    master,node       17m     v1.12.3
cn-beijing.i-2zedqvw2bewvk0l2mk9x   Ready    autoscaler,node   2m30s   v1.12.3
cn-beijing.i-2zeef9b1nhauqusbmn4z   Ready    node              2d17h   v1.12.3
[root@iZ2ze190o505f86pvk8oisZ ~]# kubectl get pod
NAME                            READY   STATUS    RESTARTS   AGE
nginx-example-584bdb467-2s226   1/1     Running   0          19m
nginx-example-584bdb467-lz2jt   1/1     Running   0          5m47s
nginx-example-584bdb467-r7fcc   1/1     Running   0          5m47s

可以观察到比测试前新增了一个节点,并且pod也正常调度了。

测试自动收缩节点数量

当Autoscaler发现通过调整Pod分布时可以空闲出多余的node的时候,会执行节点移除操作。这个操作不会立即执行,通常设置了一个冷却时间,300s左右才会执行scale down。
通过kubectl scale 来调整nginx副本数量到1个,观察集群节点的变化。

[root@iZ2ze190o505f86pvk8oisZ cluster-autoscaler]# kubectl scale deploy nginx-example --replicas 1
deployment.extensions/nginx-example scaled
[root@iZ2ze190o505f86pvk8oisZ cluster-autoscaler]# kubectl get node
NAME                                STATUS   ROLES         AGE   VERSION
cn-beijing.i-2ze190o505f86pvk8ois   Ready    master,node   46h   v1.12.3
cn-beijing.i-2zeef9b1nhauqusbmn4z   Ready    node          46h   v1.12.3


TODO: 

  • 模糊调度
    创建多个伸缩组,每个伸缩组对应不同实例规格机器比如高IO\高内存的,不同应用弹性伸缩对应类型的伸缩组
  • cronHPA + autoscaler
     由于node弹性伸缩存在一定的时延,这个时延主要包含:采集时延(分钟级) + 判断时延(分钟级) + 伸缩时延(分钟级)结合业务,根据时间段,自动伸缩业务(CronHPA)来处理高峰数据,底层自动弹性伸缩kubernetes node增大容器资源池

参考地址:

https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/FAQ.md

https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler/cloudprovider/alicloud

如何优雅的处理”💩”

💩导致了什么问题

最近上线的一个活动,线上偶尔报“High surrogate without following low surrogate”这个错误。最后定位到是用户昵称截取的时候把emoji表情截成了一半,输入到canvas的时候会报一个fatal error,导致用户无法进行下步操作。

为什么会导致💩问题

这就要从字符编码说起,由于js出生的年代是在太久远,所以使用的是过时的UCS-2编码。因为”💩”在 JS 的编码是”\uD83D\uDCA9″,而 JS 认为每 16 位 (2 字节)即表示一个字符,所以一坨大便是占 2 个字符的。例如


"\uD83D\uDCA9" === "💩" true "💩".length // 多字节字符在js中的长度有问题 2 "💩"[0] // 取便便的第一个字符会输入半坨乱码 "�" "我是一坨便便💩".split('').reverse().join('') // 便便倒过来也会有问题 "��便便坨一是我" // 甚至还有更奇怪的family emoji 👩‍👩‍👧‍👦,长度是11个字节(为什么family是两个妈妈一个儿子一个女儿不要问我🙅) "👩‍👩‍👧‍👦".length // family 的字符长度是11 11

如何安全的处理💩

ES6 已经在很努力地填坑了,可以用es6的解构语法或者Array.from 安全的将string转换为数组来处理。例如

"我是一坨便便💩".length // 常规js把emoji当做两个长度来处理
8

[..."我是一坨便便💩"].length  //解构方法把emoji表情当做一个长度处理
7
 Array.from("我是一坨便便💩").length // es6 的数组处理也可以把表情当做一个字符处理
7


Palfish JSBridge Chrome extension技术方案

Palfish JSBridge Chrome extension是一款为辅助前端开发调试而编写的chrome插件。

通常我们的开发环境中不能或者说很难调试一些依赖客户端的操作,不利于编码和排查问题。
Palfish JSBridge Chrome extension能够模拟客户端的一些基本操作,方便开发调试。
该插件无需更改任何代码即可直接使用,开启注入功能后,将自动注入PalfishBridgeAndroid全局变量,可实现客户端的登录、HTTP请求等功能。

chrome插件的5种运行环境

chrome插件有5种运行环境,分别为inject/content/popup/background/devtools,下面分别介绍一下这5种环境
1.inject js即注入到页面里的js,和我们开发时写的js没啥区别
2.content script与inject功能相似,但和inject js运行在不同的上下文里,即不能访问页面中的js变量方法等,但可以操作dom元素,可以通过PostMessage与页面通信,通过runtime和popup/backgound通信
3.popup js是我们点击浏览器的插件图标弹出的页面中的js,不能操作页面中的dom,但可以无限制跨域等
4.background js与popup的功能类似,但没有页面,而且生命周期比较长
5.devtools js常见的功能类似于在调试窗口增加一些标签等

总结一下
js类型 功能 访问DOM 访问页面js 跨域
inject 和页面普通js一样 ×
content 和页面js运行在隔离环境里,功能类似 × ×
popup 弹出框中的js × ×
background 与其他js通信,跨域请求等 × ×
devtool 在devtool面板增加功能页面 ×

chrome插件的通信方法

js类型 通信方法
inject 只能通过postMessage和content通信
content 通过runtime与popup/background通信,通过postMessage与content通信
popup 通过tabs与content通信,通过getBackgroundPage与background通信
background 通过tabs与content通信,通过getViews与popup通信
devtool 通过runtime与popup/background通信,通过inspectedWindow.eval与inject通信
本插件的消息传递流程

本插件通过inject js(PalfishBridgeAndroid变量)发送postMessage到content,再由content转发至background,background收到消息后执行相关操作(http请求等),并将执行结果返回到content,由content PostMessage到inject,从而完成模拟

本插件的技术难点

1.如何在页面添加变量
插件实际上是不能直接操作页面(inject)js的,也就没法直接添加变量和方法,但插件可以操作dom,故我们通过增加script标签的方式将一段js代码注入到页面中,从而实现添加变量。
2.页面如何与background通信
显然页面js是不能直接用runtime与background通信的,但由于inject与content共享dom,故我们可以使用addEventListener和postMessage来实现inject与content通信,再由content将数据转发给background

具体代码可以看这儿

https://gitlab.pri.ibanyu.com/front/picturebook/palfish-jsbridge-extension

如何定制方法/参与开发

本插件目前只实现了core.info和http.post方法,其他方法可以在mock标签自行添加,返回值必须是json格式,如果有比较通用的方法,可以提mr到项目,增加方法只需要修改src/background.js的methodTable变量即可

在使用过程中发现问题或者需求,可以与我联系,有时间尽量给予支持

网页唤起客户端

在h5网页中唤起app在很多业务场景中都会有使用,尤其是在一些app推广运营页面中。刚好在之前的开发工作中有用到,好记性不如烂笔头,方便自己之后又遇到了这种场景可以快速实现。

唤醒native APP的几种方式

在Android端,常用的方式是Schame + Android Itent,在IOS端,常用的方式是Schema + Universal links(IOS9+);使用的前提都是客户端程序实现了Schema协议。本文只设计H5端的代码实现。

下面对这3种方式做下简单介绍

1.Schema

在Android和IOS浏览器中(非微信浏览器),可以通过schema协议的方式唤醒本地app客户端;schema协议在App开发注册之后,与前端进行统一约定,通过H5页面访问某个具体的协议地址,即可打开对应的App客户端 页面;

访问协议地址,目前有三种方式:
下面以之前公司定义的协议为例

1) *** 通过a标签打开 ***,点击标签时启动APP

<a href="ycmath://yangcong345.com/userProfile">唤起app</a>

2) *** 通过iframe打开 ***,设置iframe.src即会启动

<iframe src="ycmath://yangcong345.com/userProfile"></iframe>

3) *** 直接通过window.location 进行跳转 ***

window.location.href= "ycmath://yangcong345.com/userProfile";

安卓实现注册schema协议,可以参考Android手机上实现WebApp直接调起NativeApp

注:由于微信的白名单限制,无法通过schema来唤起本地app,只有白名单内的app才能通过微信浏览器唤醒,这个问题暂时可能最好的办法就是提示用户使用其他浏览器……

2.Android Intent

在Android Chrome浏览器中,版本号在chrome 25+的版本不再支持通过传统schema的方法唤醒APP,比如通过设置window.location = “xxxx://xxxx”将无法唤醒本地客户端。需要通过Android Intent 来唤醒APP; 使用方式如下:

首先需要构建intent字符串:

intent:
login                                                // 特定的schema uri,例如login表示打开NN登陆页
#Intent;
    package=cn.xxxx.xxxxxx;                          // apk相关信息
    action=android.intent.action.VIEW;               // apk相关信息
    category=android.intent.category.DEFAULT;        // apk相关信息
    component=[string];                              // apk相关信息,可选
    scheme=xxxx;                                     // 协议类型
    S.browser_fallback_url=[url]                     //可选,schema启动客户端失败时的跳转页,一般为下载页,需通过encodeURIComponent编码
end;

然后构造一个a标签,将上面schame 字符串作为其href值,当点击a标签时,即为通过schema打开某客户端登陆页,如果未安装客户端,则会跳转到指定页,这里会跳转到下载页;

<a href="intent://loin#Intent;scheme=ycmath;package=cn.futu.trader;category=android.intent.category.DEFAULT;action=android.intent.action.VIEW;S.browser_fallback_url=http%3A%2F%2Fa.app.qq.com%2Fo%2Fsimple.jsp%3Fpkgname%3Dcn.futu.trader%26g_f%3D991653;end">打开登录页</a>

intent官方使用文档

3.Universal links

Universal links为 iOS 9 上一个所谓 通用链接 的深层链接特性,一种能够方便的通过传统 HTTP 链接来启动 APP, 使用相同的网址打开网站和 APP;通过唯一的网址, 就可以链接一个特定的视图到你的 APP 里面, 不需要特别的 schema;

在IOS中,对比schema的方式,Universal links有以下优点:

通过schema启动app时,浏览器会有弹出确认框提示用户是否打开,而Universal links不会提示,体验更好;
Universal link可在再微信浏览器中打开外部App;

注:网易新闻客户端IOS 9上目前采用这种Universal links方式
针对这部分内容可以参考以下博文
打通 iOS 9 的通用链接(Universal Links)
浏览器中唤起native app || 跳转到应用商城下载(二) 之universal links

4.微信JS SDK

微信内的网页是一个特殊的存在:对于schema,微信会做出来拦截,导致通过 schema协议无法唤起APP;想要在微信中唤起APP,需要通过微信js sdk提供的接口进行唤起APP,目前微信并未在其开放平台上说明其接口,只有部分在微信白名单(其实就是和腾讯有合作关系的公司)中的应用程序可使用对应的接口进行唤起APP。

QQ webview本身可以通过schema的方式唤起APP,但现在还好不好使就不清楚了

实现过程

首先,通过浏览器是无法判断是否安装了客户端程序的,因此整体的思路就是:尝试去通过上面的唤起方式来唤起本地客户端如果唤起超时,则直接跳转到下载页;整个实现过程围绕这一点展开。

具体实现代码可以在github上看到h5callapp ps: 不是我写的,但里面的方法在实际工作用到,也确实有用,虽然里面依然有一堆问题,但起码提供了解决思路

使用canvas绘制唱跳rap🏀

背景介绍

最近唱跳rap挺流行的,我经常在各种地方看到相关的信息,有个朋友的头像和签名都换成这个了,我想逗他一下,所以想着用canvas画一个。

最终效果如下:

如果好奇可以先把代码下下来跑跑看哈,代码链接在最后。

前期准备

绘制序列帧

其中那个跳舞的人只能用序列帧来做,这几天我买了一个数位板,正好用了一下,以为有了数位板就能画的很好是想多了,最终结合ps和数位板勉强画完了整个跳舞的过程。放在了一张图上:

有没有种武功秘籍的感觉~

createjs简介

自己实现帧动画还是挺复杂的,所以我使用了createjs

先简单介绍一下我对createjs所了解的一些知识,createjs包括几个部分
easeljs用来绘制各种图形的,包括了Stage、Container、Sprite还有Graphics等,帧动画也是这部分实现的
tweenjs是用来制作动画的,各种属性动画在这里实现
preloadjs是用来预加载资源的,绘制时难免会用到各种assets,可以用preloadjs提前加载。
soundjs是音频相关的一些api

easeljs组织各种图形是使用stage、container、spite三层来管理的,stage只有一个,container和sprite可以多个。我感觉比较方便的一点是之前多个canvas中绘制的内容,现在使用多个container就可以了。

我们主要用到了easeljs来组织各种图形和绘制帧动画,包括文字、跳舞的人、星星、心,使用了tweenjs来绘制属性动画,主要是星星的下落和文字的出现动画,使用了preloadjs来预加载资源,也就是跳舞的人需要的那个序列帧图片。

使用了vue、webpack,使用vue-cli生成的基本结构,但基本没用到vue的啥特性,感兴趣的自己去了解下。

绘制

初始化

首先放一个大的canvas在页面中:

<canvas ref="can" width=1200 height=600></canvas>

然后创建根stage,和一个container

const can = this.$refs.can;

const stage = new createjs.Stage(can);     
const container = new createjs.Container();
stage.addChild(container);

加载资源

接下来我们使用preloadjs来加载资源,只要给出一个清单就可以了,id方便后面取资源,src就是资源的路径。

const queue = new createjs.LoadQueue();
queue.loadManifest([
    { id: 'singjumprap', src: './images/singjumprap.png' },
]);
queue.on('complete', () => {
   const img =  queue.getResult('singjumprap')

});

在加载完成之后开始我们的绘制,通过getResult拿到图片,这里的图片已经是HTMLImageElement了,可以直接用。

帧率设置

canvas绘制一般都是使用requestAnimationFrame来做定时的重绘,但帧率是不可控的,这里我们使用了createjs的Ticker来做帧率控制:

createjs.Ticker.addEventListener("tick", tick);
createjs.Ticker.setFPS(30);

tick是每次重绘都会执行的回调函数,有一个event的参数可以拿到每次的间隔事件等信息。

function tick() {
    stage.update();
}

需要在tick里调用stage.uopdate来更新。

绘制跳舞的人

拿到资源了,帧率设置好了,接下来就是绘制跳舞的人的帧动画了。我封装了一个方法:

function startDrawPerson(container, queue) {
    const frames = [
        [13, 250, 840, 1018],
        [1104, 250, 740, 1018],
        [2105, 250, 740,1018],
        [2994, 250, 740,1018],
        [4147, 250, 740,1018],
        [5263, 250, 740,1018],
        [6174, 250, 680,1018],
        [7100, 250, 580,1018],
        [7899, 250, 580,1018],
        [8457, 250, 720,1018],
        [9130, 250, 740,1018],
        [9899, 250, 840,1018],
        [10959, 250, 860,1018],
        [12255, 250, 740,1018],
        [13650, 250, 840,1018],
        [14457, 250, 740,1018],
        [15245, 250, 740,1018],
        [16081, 250, 740,1018],
        [16918, 250, 740,1018],
        [17716, 250, 740,1018],
        [18476, 250, 740,1018] 
    ];
    const spriteSheet = new createjs.SpriteSheet({
        images: [queue.getResult('singjumprap')],
        frames,
        animations: {
            person: [0],
            singjumprap: frames.map((item, index) => index)
        },
    });
    const person = new createjs.Sprite(spriteSheet, 'person');
    person.set({x:300,y:100,scaleX:0.5,scaleY:0.5}); 
    container.addChild(person);
    person.gotoAndPlay('singjumprap');
    return person;
}

这个方法需要传入container和用来拿资源的queue,返回绘制完的sprite。

frames是序列帧的信息,每一个都包括x、y、width、height来从图中截取一部分,这就像web中的雪碧图一样。

images是我们加载好的图片,frames是从中截取出的序列帧,animations就是帧动画信息了,我们从上面的frames中取出一些帧来组成一个帧动画。

我这里取了两个帧动画,一个是开始的person,另一个是跳舞的动作。

然后初始化sprite,调整下大小和位置,之后添加到container中,执行跳舞动画。

跳舞过程的控制

现在已经可以实现跳舞了,但是有两个问题,一个是每一帧切换过快,因为我们设置了FPS是30,并且每一帧都调用了stage.update;二是跳舞是循环的,我们只希望跳一次。

这些可以在tick里控制:

let delTime = 0;
let frameIndex = 0;
let isStop = false;
function tick(evt) {
    delTime += evt.delta
    if( delTime < 300) {
        person.paused = true;
    } else {
        if (frameIndex < frames.length -1) {
            person.paused = false;
            delTime = 0;
            frameIndex++;                     
        } else {
            if(!isStop){
                startDrawHeart(container, heartCan);
                isStop = true;
            }
        }
    }
    stage.update();
}

person就是跳舞的人的sprite对象,可以通过设置paused为true来暂停序列帧的播放,我们几率一个delTime,当超过300ms时让他动一次。

同时每次动的时候记录index,当跳到最后一个动作的时候就设置paused为true,并且不再修改。

然后开始绘制❤️。

绘制跳动的心

首先我们先把心绘制出来,我使用了另一个canvas:

<canvas ref="heartCan" width=50 height=60 style="display:none;"></canvas>

拿到context,然后开始绘制

const heartCan = this.$refs.heartCan;
const heartCtx = heartCan.getContext('2d');
drawHeart(heartCtx, 25,25,10, 0);

绘制心的函数,需要传入context,以及x、y、r以及旋转角度rot

function drawHeart(ctx,x,y,R,rot) { 
    function heartPath(ctx) { 
        ctx.beginPath(); 
        ctx.arc(-1,0,1,Math.PI,0,false); 
        ctx.arc(1,0,1,Math.PI,0,false); //貝塞尔曲线画心 
        ctx.bezierCurveTo(1.9, 1.2, 0.6, 1.6, 0, 3.0); 
        ctx.bezierCurveTo( -0.6, 1.6,-1.9, 1.2,-2,0); 
        ctx.closePath(); 
    }
    ctx.save(); 
    ctx.translate(x,y); 
    ctx.rotate(rot/180*Math.PI); 
    ctx.scale(R, R); 
    heartPath(ctx);
    ctx.fillStyle = "red"; 
    ctx.shadowColor = "gray"; 
    ctx.shadowOffsetX = 2; 
    ctx.shadowOffsetY = 2; 
    ctx.shadowBlur = 2; 
    ctx.fill(); 
} 

心的是使用贝塞尔曲线来绘制的心形路径,之后设置了fillStyle和shadow来填充。

接下来把绘制的心通过Bitmap类型的Sprite创建并添加到container中,并且开始执行跳动的动画。

const startDrawHeart = (container, heartImg) => {
    var bitmap = new createjs.Bitmap(heartImg);
    bitmap.x = 385;
    bitmap.y = 280;
    bitmap.width = 50;
    bitmap.height = 50;
    container.addChild(bitmap);
    const heartBounce = () => {
        setTimeout(heartBounce, 1000);
        createjs.Tween.get(bitmap).to({scale: 1.3}, 500).to({scale: 1}, 500);
    }
    heartBounce();
}

跳动动画逻辑比较简单,就是scale从1到1.3到1之间变换,每次时间间隔都是500ms。然后1秒执行一次动画。

绘制文字和星星坠落

画完之后我觉得表意不够明确,所以我加了一行文字,之后又在文字上加了一些星星。

星星

先绘制星星:

<canvas ref="canStar" width=20 height=20 style="display:none;"></canvas>
const starCan = this.$refs.canStar;
const starCtx = starCan.getContext('2d');
drawStar(starCtx, 1);
const drawStar = (ctx, scale, color = 'gold') => {
    ctx.save();
    ctx.clearRect(0, 0, 100, 100);
    ctx.strokeStyle = color;
    ctx.scale(scale, scale);
    ctx.beginPath();
    ctx.moveTo(0, 10);
    ctx.lineTo(20, 10);
    ctx.moveTo(10, 0);
    ctx.lineTo(10, 20);
    ctx.moveTo(5, 5);
    ctx.lineTo(15, 15);   
    ctx.moveTo(5, 15);
    ctx.lineTo(15, 5);
    ctx.closePath();
    ctx.stroke();
    ctx.restore();
}

和绘制心是一样的过程,我画的星星很简单,就是横竖斜线~

文字
const texts = "从未有人像你让我如此怦然心动";
const textAnimInfos = texts.split('').map((item, index) => {
    let text = new createjs.Text(item, "40px monospace", "#000000");
    text.x = 150 + index * 50;
    text.y = 50;
    text.rotation = random( -30, 30);
    text.scale = random(1.2, 1.5);
    return text;
});
let i = 0;
function renderText() {
    const text = textAnimInfos[i];
    container.addChild(text);
    createjs.Tween.get(text).to({rotation: random(-10, 10), scale: random(0.8, 1)}, 100);

    i++;    
    if (i < texts.length) {
        setTimeout(renderText, 350); 
    }          
}
setTimeout(() => {
    renderText();
}, 2000);

因为每个文字都有出现动画,所以每个是一个单独的sprite,先循环生成每个文字的x、y以及初始的rotation和scale等信息,放到textAnimInfos中,每两秒往container中添加一个,出现时执行一些scale和rotation的动画。

然后绘制星星和星星坠落动画:

let j =0;
function renderStar() {
    const text = textAnimInfos[j];
    for (let i =0; i< 10; i++){
        const startX =text.x + random(0, 20);
        const startY =text.y + random(0, 20);
        var star = new createjs.Bitmap(starCan);
        star.x = startX;
        star.y = startY;
        star.width = 10;
        star.height = 10;
        container.addChild(star);
        createjs.Tween.get(star).wait(0).to({y: random(1000, 1200), x: random(0, 1200)}, random(5000, 8000));
    }

    j++;
    if (j < texts.length) {  
        setTimeout(renderStar, 350); 
    }
}

setTimeout(() => {
    renderText();
    renderStar();
}, 2000);

星星因为是和文字结合的,这里位置信息和文字关联,每次绘制从textAnimInfos中取出文字的x、y,然后创建10个(这里可以调整)随机位置的星星添加到容器中,并且执行一个坠落动画。

这里的坠落动画没用啥运动公式,只是随机了一个下方的结束位置并且随机了运动时间。但感觉坠落的还挺好看的~

源码链接

代码在这,需要自取,有问题可以提issue哈
canvas-singjumprap

简述虚拟DOM中的VNode种类

VNode 是真实 DOM 的描述,在vue中,使用tag来描述vnode的种类(相关源码),比如我们可以用如下对象描述一个 div 标签:

const elementVnode = {
  tag: 'div'
}

VNode 描述不同的 DOM 时,其属性的值也各不相同。比如一个 VNode 对象是 html 标签的描述,那么其 tag 属性值就是一个字符串,即标签的名字;如果是组件的描述,那么其 tag 属性值则引用组件类(或函数)本身;如果是文本节点的描述,那么其 tag 属性值为 null。最终我们发现,不同类型的 VNode 拥有不同的设计,这些差异积少成多,所以我们完全可以将它们分门别类。

VNode的种类

总的来说,在vue中把 VNode 分成五类,分别是:html/svg 元素组件纯文本Fragment 以及 Portal

组件

基于现有设计,我们可以把组件可以分为为 有状态组件函数式组件。同时在vue中,有状态组件还可以细分为三部分:普通的有状态组件需要被 keepAlive 的有状态组件 以及 已经被 keepAlive 的有状态组件 。但无论是普通的有状态组件还是 keepAlive 相关的有状态组件,它们都是有状态组件。所以我们在设计 VNode 时可以将它们作为一类看待。

Fragment

Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。一种常见模式是组件返回一个子元素列表,而我们不希望使用一个元素标签包裹占位,这个时候 Fragment 就比较适合。

Portal

Portal 提供了将子节点渲染到存在于父组件以外的 DOM 节点的方案。而且 Portal 可以不严谨地认为是可以被到处挂载的 Fragment,但是虽然 Portal 的内容可以被渲染到任意位置,但它的行为仍然像普通的DOM元素一样,如事件的捕获/冒泡机制仍然按照代码所编写的DOM结构实施。要实现这个功能就必须需要一个占位的DOM元素来承接事件。

如何标识 VNode

既然 VNode 有类别之分,我们就有必要使用一个唯一的标识,来标明某一个 VNode 属于哪一类。同时给 VNode 添加 flags 也是 Virtual DOM 算法的优化手段之一。

比如在 Vue2 中区分 VNodehtml 元素还是组件亦或是普通文本,是这样做的:

  • 1、拿到 VNode 后先尝试把它当作组件去处理,如果成功地创建了组件,那说明该 VNode 就是组件的 VNode
  • 2、如果没能成功地创建组件,则检查 vnode.tag 是否有定义,如果有定义则当作普通标签处理
  • 3、如果 vnode.tag 没有定义则检查是否是注释节点
  • 4、如果不是注释节点,则会把它当作文本节点对待

以上这些判断都是在挂载(或patch)阶段进行的,换句话说,一个 VNode 到底描述的是什么是在挂载或 patch 的时候才知道的。这就带来了两个难题:无法从 AOT 的层面优化开发者无法手动优化

if (flags & VNodeFlags.ELEMENT) {
  // VNode 是普通标签
  mountElement(/* ... */)
} else if (flags & VNodeFlags.COMPONENT) {
  // VNode 是组件
  mountComponent(/* ... */)
} else if (flags & VNodeFlags.TEXT) {
  // VNode 是纯文本
  mountText(/* ... */)
}

如上,采用了位运算,在一次挂载任务中如上判断很可能大量的进行,使用位运算在一定程度上再次拉升了运行时性能。

这就意味着我们在设计 VNode 对象时,应该包含 flags 字段:

// VNode 对象
{
  flags: ...
}

枚举值 VNodeFlags

那么一个 VNode 对象的 flags 可以是哪些值呢?那就看 VNode 有哪些种类就好了,每一个 VNode种类我们都为其分配一个 flags 值即可,我们把它设计成一个枚举值并取名为 VNodeFlags,在 javascript 里就用一个对象来表示即可:

const VNodeFlags = {
  // html 标签
  ELEMENT_HTML: 1,
  // SVG 标签
  ELEMENT_SVG: 1 << 1,

  // 普通有状态组件
  COMPONENT_STATEFUL_NORMAL: 1 << 2,
  // 需要被keepAlive的有状态组件
  COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE: 1 << 3,
  // 已经被keepAlive的有状态组件
  COMPONENT_STATEFUL_KEPT_ALIVE: 1 << 4,
  // 函数式组件
  COMPONENT_FUNCTIONAL: 1 << 5,

  // 纯文本
  TEXT: 1 << 6,
  // Fragment
  FRAGMENT: 1 << 7,
  // Portal
  PORTAL: 1 << 8
}

我们注意到,这些枚举属性的值基本都是通过将十进制数字 1 左移不同的位数得来的。根据这些基本的枚举属性值,我们还可以派生出额外的三个标识:

// html 和 svg 都是标签元素,可以用 ELEMENT 表示
VNodeFlags.ELEMENT = VNodeFlags.ELEMENT_HTML | VNodeFlags.ELEMENT_SVG
// 普通有状态组件、需要被keepAlive的有状态组件、已经被keepAlice的有状态组件 都是“有状态组件”,统一用 COMPONENT_STATEFUL 表示
VNodeFlags.COMPONENT_STATEFUL =
  VNodeFlags.COMPONENT_STATEFUL_NORMAL |
  VNodeFlags.COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE |
  VNodeFlags.COMPONENT_STATEFUL_KEPT_ALIVE
// 有状态组件 和  函数式组件都是“组件”,用 COMPONENT 表示
VNodeFlags.COMPONENT = VNodeFlags.COMPONENT_STATEFUL | VNodeFlags.COMPONENT_FUNCTIONAL

其中 VNodeFlags.ELEMENTVNodeFlags.COMPONENT_STATEFUL 以及 VNodeFlags.COMPONENT 是由基本标识通过按位或(|)运算得到的,这三个派生值将用于辅助判断。

有了这些 flags 之后,我们在创建 VNode 的时候就可以预先为其打上 flags,以标明该 VNode 的类型:

// html 元素节点
const htmlVnode = {
  flags: VNodeFlags.ELEMENT_HTML,
  tag: 'div',
  data: null
}

// svg 元素节点
const svgVnode = {
  flags: VNodeFlags.ELEMENT_SVG,
  tag: 'svg',
  data: null
}

// 函数式组件
const functionalComponentVnode = {
  flags: VNodeFlags.COMPONENT_FUNCTIONAL,
  tag: MyFunctionalComponent
}

// 普通的有状态组件
const normalComponentVnode = {
  flags: VNodeFlags.COMPONENT_STATEFUL_NORMAL,
  tag: MyStatefulComponent
}

// Fragment
const fragmentVnode = {
  flags: VNodeFlags.FRAGMENT
  // 注意,由于 flags 的存在,我们已经不需要使用 tag 属性来存储唯一标识
  tag: null
}

// Portal
const portalVnode = {
  flags: VNodeFlags.PORTAL
  // 注意,由于 flags 的存在,我们已经不需要使用 tag 属性来存储唯一标识,tag 属性用来存储 Portal 的 target
  tag: target
}

如下是利用 VNodeFlags 判断 VNode 类型的例子,比如判断一个 VNode 是否是组件:

// 使用按位与(&)运算
functionalComponentVnode.flags & VNodeFlags.COMPONENT // 真
normalComponentVnode.flags & VNodeFlags.COMPONENT // 真
htmlVnode.flags & VNodeFlags.COMPONENT // 假

熟悉位运算的话,理解起来很简单。这实际上是多种位运算技巧中的一个小技巧。我们可以列一个表格:

VNodeFlags 左移运算 32 位的 bit 序列(出于简略,只用 9 位表示)
ELEMENT_HTML 000000001
ELEMENT_SVG 1 << 1 000000010
COMPONENT_STATEFUL_NORMAL 1 << 2 000000100
COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE 1 << 3 000001000
COMPONENT_STATEFUL_KEPT_ALIVE 1 << 4 000010000
COMPONENT_FUNCTIONAL 1 << 5 000100000
TEXT 1 << 6 001000000
FRAGMENT 1 << 7 010000000
PORTAL 1 << 8 100000000

根据上表展示的基本 flags 值可以很容易地得出下表:

VNodeFlags 32 位的比特序列(出于简略,只用 9 位表示)
ELEMENT 00000001 1
COMPONENT_STATEFUL 00001 1 100
COMPONENT 0001 1 1 100

所以只有 VNodeFlags.ELEMENT_HTMLVNodeFlags.ELEMENT_SVGVNodeFlags.ELEMENT 进行按位与(&amp;)运算才会得到非零值,即为真,这样我们就可以很方便的判断出一个元素到底是什么类型的VNode了。

实现彩带动画

实现思路

当录音打分100分的时候,需要弹出一个撒彩带的动效(可在绘本上看效果)。大概可以通过自定义的View和自定义动画解决。设计给出彩带的切图,在自定义动画的回调中调用view的invilidate方法,然后更新彩带的位置,将彩带画到屏幕上。随着动画的进行,就可以看到撒彩带的效果。

动画实现

设想动画的过程,对任一个彩带,以一定的速度和角度从某个发射点喷出,初识速度的方向朝上,收到重力和阻力的影响,运动到最高点后速度将为0;然后收到重力的影响,开始加速下降,给定一个大小正比与速度的阻力,当速度达到一定时,与重力相同,开始匀速下降。整个过程就比较符合现实的撒花效果。
所以我们对位置做动画,随着时间的进行,系统会回调我们的自定义方法。

mAnimator = ObjectAnimator.ofInt(this, "position", 0, DURATION);

public void setPosition(int time) {
    for (int i = 0; i < mEmitter.size(); ++i) {
        Ribbon ribbon = mEmitter.get(i);
        calculateCurrentX(ribbon, time / 1000f);
        calculateCurrentY(ribbon, time / 1000f);
    }
    ViewCompat.postInvalidateOnAnimation(this);
}

现在我们需要考虑如何计算彩带的位置。我们可以分别计算X和Y方向的位置。
分析X方向,相对简单一点,有一个初始的速度,以及正比与速度的阻力。我们可以根据这些条件列出如下方程

说明,在x方向上,收到与速度方向相反,大小正比于速度大小的阻力,比值为resistance,由牛顿第二定律可得1式,初识的速度v是已知的,有式子2,由这两个式子可以解出彩带在x方向的位移随时间的变化。

private void calculateCurrentX(Ribbon ribbon, float time) {
        ribbon.currentX = (int) (ribbon.initVelecityX / (RESISTANCE_X * 1.0f) * (1 - Math.pow(Math.E, RESISTANCE_X * time * -1)));
    }

分析Y方向,同样有个初始的速度,但同时受到向下的重力以及与速度方向相反的阻力,同样列出方程

与x方向类型,可以得到上面两个式子,可以解出彩带在y方向的位移随时间的变化

private void calculateCurrentY(Ribbon ribbon, float time) {
        ribbon.currentY = (int) (((1 - Math.pow(Math.E, RESISTANCE_Y * time * -1)) / (RESISTANCE_Y * 1.0f)) *
                (ribbon.initVelecityY + (GRAVITY * 1.0f) / RESISTANCE_Y) - (GRAVITY * time) / (RESISTANCE_Y * 1.0f));
    }

View实现

我们首先需要初始化彩带对象,一次动画需要喷出20个彩带,每个彩带有一个初始化的速度。为了动画更好看,可以设定彩带在的速度分布在各个角度。
调用startAnimation后,启动自定义动画,在自定义动画的回调里,计算了彩带当前时刻的位置。最后调用invidate通知重绘view。在onDraw中将每个彩带绘制到View上。

@Override
    protected void onDraw(Canvas canvas) {
        for (int i = 0; i < mEmitter.size(); ++i) {
            int offset = OFFSET_X;
            if (i < mEmitter.size() / 2) {
                offset = -1 * OFFSET_X;
            }
            drawRibbon(canvas, mEmitter.get(i), offset);
        }
    }

计算机系统中的时间

前言

对于计算机系统中的时间,如果你曾经思考过下面的问题,但是没有结论,那么通过本文将给你详细的解答:
1. 闰秒是怎么产生的,在2012年6月30日UTC插入一个闰秒后,大量linux服务器宕机的原因是什么?
2. 计算机系统是怎么保证自己的时间是准确的?
3. 计算机系统我们经常使用微妙甚至纳秒,它怎么来提供这么高精度的时间?
4. 计算机系统是没有时间概念的机器,那么它是怎么来计算与管理时间的?

背景

时间是一个非常抽象的问题,吸引着许多伟大的神学家、哲学家和物理学家花毕生精力去解释时间的本质是什么,然而依然没有定论。幸运的是我们仅仅需要讨论计算机系统中的时间相关的问题,可以不用关心宇宙、黑洞、相对论和量子力学等等繁复的课题,仅仅局限在计算机这一个很小的范畴中,这看似非常简单的主题,然而现实却并不会如此简单。

计算机系统的时钟

在计算机系统中主要有两种时钟:一种是墙上时钟,一种是单调时钟。它们都可以衡量时间,但却有着本质的区别。下面我们一一来分析。

墙上时钟

墙上时钟又称为钟表时间,顾名思义,和我们平时使用的钟表的时间一样,表示形式为日期与时间。在linux系统中墙上时钟的表示形式为UTC时间,记录的是自公元1970年1月1日0时0分0秒以来的秒数和毫秒数(不含闰秒),linux系统需要处理闰秒的逻辑就是由于linux系统使用UTC时间,但是系统中记录的UTC时间是不含闰秒导致的,后面会闰秒相关的部分会有详细的介绍。

时间同步

根据定义可以发现,墙上时钟的标准是在计算机外部定义的,所以需要确保墙上时钟的准确性就变成一个问题。
计算机内部的计时器为石英钟,但是它不够精确,存在过快或者过慢的问题,这主要取决于机器的温度。所以依靠计算机自身来维持墙上时钟的准确性是不可能的。
目前普遍采取的一种方式为计算机与NTP时间服务器进行定期通过网络同步。当然这个方式受限于网络环境的影响,一般来说至少会有35毫秒的偏差,最大的时候可能会超过1秒。
对于一些对时间精度要求很高的系统,通过NTP进行同步是远远不够的,而通过GPS接收机接受标准的墙上时钟,然后在机房内部通过精确时间协议(PTP)进行同步。PTP是一种高精度时间同步协议,可以到达亚微秒级精度,有资料说可达到30纳秒左右的偏差精度,但需要网络的节点(交换机)支持PTP协议,才能实现纳秒量级的同步。
对于时间同步,Google的做法更酷,通过GPS接收机接受标准的墙上时钟,然后通过机房内部部署原子钟(精度可以达到每2000万年才误差1秒)来防止GPS接收机的故障。通过这些时间协调装置会连接到特定数量的主服务器,然后再由主服务器向整个谷歌网络中运行的其他计算机传输时间读数(TrueTime API)。

闰秒出现的原因

目前存在两种时间计量系统:基于地球自转的世界时(UT1),它以地球自转运动来计量时间,但由于地球自转速率正在变慢,所以世界时的秒长会有微小的变化,每天达到千分之几秒。原子时是取微观世界的铯原子两个超精细能级间跃迁辐射频率来度量时间,精确度非常高,每天快慢不超过千万分之一秒。
从上面可以看出,原子时是度量时间均匀的尺度,但是与地球空间位置无关;世界时度量时间的均匀性不好,但是它定义地球自转一周为一天,绕太阳公转一周为一年,这对人们的日常生产生活非常重要。
为了统一原子时与时间时直接的差距,就产生了协调世界时(UTC)。从1972年1月1日0时起,协调世界时秒长采用原子时秒长,时刻与世界时时刻之差保持在正负0.9秒之内,必要时用阶跃1整秒的方式来调整。这个1整秒的调整,就称为闰秒(增加1秒为正闰秒,较少1秒为负闰秒)。UTC从1972年1月正式成为国际标准时间,它是原子时和世界时这两种时间尺度的结合。

闰秒的处理

由于linux系统记录的是自公元1970年1月1日0时0分0秒以来的秒数和毫秒数,但是不含闰秒,这表示在linux系统中每分钟有60秒,每天有86400秒是系统定义死的。所以linux系统需要额外的逻辑来处理闰秒。

跳跃式调整

当UTC时间插入一个正闰秒后,linux系统需要跳过1秒,因为闰秒的这一秒钟在linux系统中不能被表示;当UTC时间插入一个负闰秒后,linux系统需要插入1秒,因为闰秒的这一秒钟在linux系统中不存在。目前linux系统就是采用该方式来处理闰秒的。在2012年6月30日UTC时间插入一个正闰秒的时候,由于linux系统的某些版本的闰秒处理逻辑触发了一个死锁的bug,造成了大规模的linux服务器内核死锁而宕机。

NTP服务的slew模式

NTP服务的slew模式并不使用跳跃式修改时间,而是渐进式的调整。比如当UTC时间需要插入一个正闰秒,NTP服务会每秒调整一定ms来缓慢修正时间。这样linux系统从NTP服务同步时间的时候就不会感知闰秒的存在了,内核也就不需要启动闰秒相关的逻辑了。

单调时钟

单调时钟它总是保证时间是向前的,不会出现墙上时钟的回拨问题。它非常适合用来测量持续时间段,比如在一个时间点读取单调时钟的值,完成某项工作后再次获得单调时钟的值,时钟值之差为两次检测之间的时间间隔。
但是单调时钟的绝对值没有任何意义,它可能是计算机自启动以后经历的纳秒数等等。因此比较不同节点上的单调时钟的值是没有意义的。

时间的管理

时间的概念对于计算机来说有些模糊,计算机必须在硬件的帮助下才能计算和管理时间。前面说的石英钟就是用来做计算机的系统定时器的,系统定时器以某中固定的频率自行触发时钟中断。由于时钟中断的频率是编程预定的,所以内核知道连续两次时钟中断的间隔时间。这个间隔时间就称为节拍,它等于千节拍分之一秒。通过时钟中断,内核周期性地更新系统的墙上时钟和单调时钟,从而计算和管理好时间。

时间的精度

目前系统定时器的中断频率为1000HZ,那么计算机能处理的时间精度为1ms。然而很多时候需要更加精确的时间,比如1微妙,计算机是怎么来解决这个问题的呢?
在每一次计算机启动的时候,计算机都会计算一次BogoMIPS的值,这个值的意义是处理器在给定的时间内执行指令数,通过BogoMIPS值,计算机就可以得到很小很小的精度了。比如1秒计算机执行了N条指令,那么计算机的精度就可以达到N分之一秒。很明显N是一个非常非常大的数目,因而计算机可以得到非常非常精确的时间。

总结

在本文中,我们讨论了计算机系统时间同步的方式,同时分析了闰秒产生的原因,以及linux系统应对的办法,然后概览性的讲了linux系统是怎么进行时间的计算与管理的,最后分析了linux系统可以提高高精度时间的方法。