高阳Sunny

高阳Sunny 查看完整档案

北京编辑中国科学院  |  联想之星 编辑SegmentFault  |  CEO 编辑 segmentfault.com/lives 编辑
编辑

SegmentFault 思否 CEO
C14Z.Group Founder
Forbes China 30U30

独立思考 敢于否定

曾经是个话痨... 要做一个有趣的人!

任何问题可以给我发私信或者发邮件 sunny@sifou.com

个人动态

高阳Sunny 赞了文章 · 今天 10:56

首届开放原子开发者大会 10 月举办,议题征集中!

开放原子开源基金会计划将在 10 月举办首届开发者大会。

作为开放原子开源基金会首次举办的年度开发者盛会,本次大会面向开源生态各领域中的开发者、意见领袖、核心企业代表、开源代码受益者及终端用户,聚焦开源创新,共促开源生态繁荣。

目前,大会已经开始演讲征集工作,公开征集本次开发者大会的各项议题,并同步征集选题委员会志愿者赞助商以及各领域合作者

CALL FOR PRESENTATIONS

选题方向

本次大会覆盖选题方向包括但不局限于:

  • 开源软件在新技术的运用(如:操作系统、区块链、边缘计算、大数据、物联网、安全、人工智能、工业智能化、机器人、脑机接口等)
  • 开源硬件
  • 开源文化
  • 开源内容
  • 开源DevOps工具
  • 开源编程语言
  • 开源社区运营及治理
  • 开源法律法规及知识产权
  • 开源教育
  • 开发者心理及生理健康
  • 开发者权益保护
  • 开发者职业发展规划
  • 女性开发者

选题形式

  • 单人主题演讲/双人主题演讲:每个选题20分钟
  • 小组专题讨论:3~5名发言人,共20分钟

选题提交要求

如您的选题方向不在上述选题中,请单独说明
请提供选题简介,并提供该选题在创新性、专业性及受众人群的说明
请提供演讲人简历,要包括演讲人在开源领域的贡献及历史参与演讲情况
如果您的演讲需要特殊硬件或特殊资源的支持,请说明

选题请发送到以下邮箱:cfp@openatom.org
报名邮件标题请使用“开发者大会选题提交”

演讲者权益

演讲人将免费获得本次大会VIP门票
演讲人将获得本次大会文化周边大礼包
演讲人可在本次大会宣传材料及网站上获得个人信息露出

时间计划

  • 2021年4月 发布本次大会选题征集令,公开征集大会各选题
  • 2021年5月 发布选题工作初步进展
  • 2021年6月 本次大会征集选题通道关闭,选题委员会确定议题并通知发言人
  • 2021年7月 确定并发布本次大会议题、议程
  • 2021年8月 演讲材料初审
  • 2021年9月 演讲材料终审
  • 2021年10月 召开本次大会

关于开放原子开源基金会

开放原子开源基金会是致力于推动全球开源产业发展的非营利机构,于2020年6月正式获得民政部批准在北京成立,由华为、腾讯、阿里、百度、浪潮、360、招商银行等十家龙头科技企业联合发起,由工信部作为业务指导单位。

开放原子开源基金会拟通过共建、共治、共享的方式,系统性打造信息产业和工业开源开放框架,搭建国际开源社区,提升行业协作效率,赋能千行百业。

目前开放原子开源基金会业务范围主要包括为各类开源软件、开源硬件、开源芯片、开源内容提供中立的知识产权托管、战略咨询、法务咨询、项目运营、品牌营销和教育培训等服务。

相关阅读

中国首个开源基金会成立,已有七个项目加入孵化
一源初始 开放共创 | 开放原子开源基金会2020年度峰会
专访堵俊平:“最好的开源生态模型,是亚马逊的原始森林”
开放原子基金会 TOC 主席堵俊平:开源的本质是“人与人基于代码的联结”
专访中科院副总工程师武延军:“参与开源人数变多是好事,但要小心「开源踩踏事件」”
专访 Tetrate.io 创始工程师吴晟:开源领域需要 40+ 的开发者,也需要更张扬的年轻人

查看原文

赞 3 收藏 0 评论 1

高阳Sunny 赞了文章 · 4月12日

Vue 3 组件开发:搭建基于SpreadJS的表格编辑系统(环境搭建)

image.png

Vue是一套用于构建用户界面的渐进式框架,与其它大型 JS 框架不同,Vue 被设计为可以自底向上逐层应用,更易上手,还便于与第三方库或既有项目整合,因此,Vue完全能够为复杂的单页应用提供驱动。

2020年09月18日,Vue.js 3.0 正式发布,作者尤雨溪将其描述为:更快、更小、更易于维护。

Vue 3都加入了哪些新功能?

本次发布, Vue框架本身迎来了多项更新,如Vue 此前的反应系统是使用 Object.defineProperty 的 getter 和 setter。 但是,在 Vue 3中,将使用 ES2015 Proxy 作为其观察者机制,这样做的好处是消除了以前存在的警告,使速度加倍,并节省了一半的内存开销。

除了基于 Proxy 的观察者机制,Vue 3的其他新特性还包括:

1. Performance(性能提升)

在Vue 2中,当某个DOM需要更新时,需要遍历整个虚拟DOM树才能判断更新点。而在Vue 3中,无需此项操作,仅需通过静态标记,对比虚拟节点上带有patch flag的节点,即可定位更新位置。

对比Vue 2和Vue 3的性能差异,官方文档中给出了具体数据说明:

· SSR速度提高了2~3倍

· Update性能提高1.3~2倍

2. Composition API(组合API)

Vue 2中有data、methods、mounted等存储数据和方法的对象,我们对此应该不陌生了。比如说要实现一个轮播图的功能,首先需要在data里定义与此功能相关的数据,在methods里定义该功能的方法,在mounted里定义进入页面自动开启轮播的代码…… 有一个显而易见的问题,就是同一个功能的代码却要分散在页面的不同地方,维护起来会相当麻烦。

为了解决上述问题,Vue 3推出了具备清晰的代码结构,并可消除重复逻辑的 Composition API,以及两个全新的函数setup和ref。

Setup 函数可将属性和方法返回到模板,在组件初始化的时候执行,其效果类似于Vue 2中的beforeCreate 和 created。如果想使用setup里的数据,需要将值return出来,没有从setup函数返回的内容在模板中不可用。

Ref函数的作用是创建一个引用值,主要是对String、Number、Boolean的数据响应做引用。

相对于Vue 2,Vue 3的生命周期函数也发生了变更,如下所示:

· beforeCreate -> 请使用 setup()

· created -> 请使用 setup()

· beforeMount -> onBeforeMount

· mounted -> onMounted

· beforeUpdate -> onBeforeUpdate

· updated -> onUpdated

· beforeDestroy -> onBeforeUnmount

· destroyed -> onUnmounted

· errorCaptured -> onErrorCaptured

需要注意的是,Vue 2使用生命周期函数时是直接在页面中写入生命周期函数,而在Vue 3则直接引用即可:

import {reactive, ref, onMounted} from 'vue'

3. Tree shaking support(按需打包模块)

有人将“Tree shaking”  称之为“摇树优化”,其实就是把无用的模块进行“剪枝”,剪去没有用到的API,因此“Tree shaking”之后,打包的体积将大幅度减少。

官方将Vue 2和Vue 3进行了对比,Vue 2若只写了Hello World,且没有用到任何的模块API,打包后的大小约为32kb,而Vue 3 打包后仅有13.5kb。         

4. 全新的脚手架工具:Vite

Vite 是一个由原生 ESM 驱动的 Web 开发构建工具。在开发环境下基于浏览器原生 ES imports 开发,在生产环境下基于 Rollup 打包。

和 Webpack相比,具有以下特点:

· 快速的冷启动,不需要等待打包

· 即时的热模块更新

· 真正的按需编译,不用等待整个项目编译完成

由于完全跳过了打包这个概念,Vite的出现大大的撼动了Webpack的地位,且真正做到了服务器随起随用。看来,连尤大神都难逃“真香”理论。

image.png

Vite究竟有什么魔力?不妨让我们通过实际搭建一款基于Vue 3 组件的表格编辑系统,亲自体验一把。

一、环境搭建

使用 Vite 初始化一个 Vue 3 项目

1. 执行代码:


$ npm init vite-app <project-name>

$ cd <project-name> //进入项目目录

$ npm install //安装项目所需依赖

$ npm run dev //启动项目

我们来看下生成的代码, 因为 vite 会尽可能多地镜像 vue-cli 中的默认配置, 所以,这段代码看上去和 vue-cli 生成的代码没有太大区别。

├── index.html

├── package.json

├── public

│ └── favicon.ico

└── src

 ├── App.vue

 ├── assets

 │ └── logo.png

 ├── components

 │ └── HelloWorld.vue

 ├── index.css

 └── main.js

2. 执行下列命令:

image.png

此时如果不通过 npm run dev 来启动项目,而是直接通过浏览器打开 index.html, 会看到下面的报错:

image.png

报错的原因:浏览器的 ES module 是通过 http 请求拿到模块的,所以 vite 的一个任务就是启动一个 web server 去代理这些模块,在 vite 里是借用了 koa 来启动的服务。

export function createServer(config: ServerConfig): Server {
  // ...
  const app = new Koa<State, Context>()
  const server = resolveServer(config, app.callback())
  
  // ...
  const listen = server.listen.bind(server)
  server.listen = (async (...args: any[]) => {
    if (optimizeDeps.auto !== false) {
      await require('../optimizer').optimizeDeps(config)
    }
    return listen(...args)
  }) as any
  
  return server
}

由于浏览器中的 ESM 是获取不到导入的模块内容的,需要借助Webpack 等工具,如果我们没有引用相对路径的模块,而是引用 node_modules,并直接 import xxx from 'xxx',浏览器便无法得知你项目里有 node_modules,只能通过相对路径或者绝对路径去寻找模块。

这便是vite 的实现核心:拦截浏览器对模块的请求并返回处理后的结果(关于vite 的实现机制,文末会深入讲解)。

3. 生成项目结构:

image.png

入口 index.html 和 main.js 代码结构为:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link rel="icon" href="/favicon.ico" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vite App</title>
</head>
<body>
  <div id="app"></div>
  <script type="module" data-original="/src/main.js"></script>
</body>
</html>

// main.js
// 只是引用的是最新的 vue3 语法,其余相同
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

createApp(App).mount('#app')

4. 进入项目目录:cd myVue3 

5. 安装相关模块:npm install

image.png

6. 下载模块:

image.png

7. 启动项目:npm run dev

image.png

8. 进入地址,当我们看到这个页面时,说明项目已经成功启动了。

image.png

Vite 的实现机制

1. /@module/ 前缀

对比工程下的 main.js 和开发环境下实际加载的 main.js,可以发现代码发生了变化。

工程下的 main.js:

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

createApp(App).mount('#app')

实际加载的 main.js:

import { createApp } from '/@modules/vue.js'
import App from '/src/App.vue'
import '/src/index.css?import'

createApp(App).mount('#app')

为了解决 import xxx from 'xxx' 报错的问题,vite 对这种资源路径做了统一处理,即添加一个/@module/前缀。

在 src/node/server/serverPluginModuleRewrite.ts 源码的 koa 中间件里可以看到 vite 对 import 做了一层处理,其过程如下:

· 在 koa 中间件里获取请求 body

· 通过 es-module-lexer 解析资源 ast 拿到 import 的内容

· 判断 import 的资源是否是绝对路径,绝对视为 npm 模块

· 返回处理后的资源路径:"vue" => "/@modules/vue"

2. 支持 /@module/

在 /src/node/server/serverPluginModuleResolve.ts 里可以看到大概的处理逻辑:

· 在 koa 中间件里获取请求 body

· 判断路径是否以 /@module/ 开头,如果是取出包名

· 去node_module里找到这个库,基于 package.json 返回对应的内容

3. 文件编译

通过前文,我们知道了 js module 的处理过程,对于vue、css、ts等文件,其又是如何处理的呢?

以 vue 文件为例,在 webpack 里使用 vue-loader 对单文件组件进行编译,在这里 vite 同样拦截了对模块的请求并执行了一个实时编译。

通过工程下的 App.vue 和实际加载的 App.vue,便发现改变。

工程下的 App.vue:

<template>
  ![](./assets/logo.png)
  <HelloWorld msg="Hello Vue 3.0 + Vite" />
</template>

<script>
import HelloWorld from './components/HelloWorld.vue';

export default {
  name: 'App',
  components: {
    HelloWorld,
  },
};
</script>
<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

实际加载的 App.vue:

import HelloWorld from '/src/components/HelloWorld.vue';

const __script = {
    name: 'App',
    components: {
        HelloWorld,
    },
};

import "/src/App.vue?type=style&index=0&t=1592811240845"
import {render as __render} from "/src/App.vue?type=template&t=1592811240845"
__script.render = __render
__script.__hmrId = "/src/App.vue"
__script.__file = "/Users/wang/qdcares/test/vite-demo/src/App.vue"
export default __script

可见,一个 .vue 文件被拆成了三个请求(分别对应 script、style 和template) ,浏览器会先收到包含 script 逻辑的 App.vue 的响应,然后解析到 template 和 style 的路径后,再次发起 HTTP 请求来请求对应的资源,此时 Vite 对其拦截并再次处理后返回相应的内容。

// App.vue?type=style
import { updateStyle } from "/vite/hmr"
const css = "\n#app {\n  font-family: Avenir, Helvetica, Arial, sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  text-align: center;\n  color: #2c3e50;\n  margin-top: 60px;\n}\n"
updateStyle("7ac74a55-0", css)
export default css

// App.vue?type=template
import {createVNode as _createVNode, resolveComponent as _resolveComponent, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock} from "/@modules/vue.js"

const _hoisted_1 = /*#__PURE__*/
_createVNode("img", {
    alt: "Vue logo",
    src: "/src/assets/logo.png"
}, null, -1 /* HOISTED */
)

export function render(_ctx, _cache) {
    const _component_HelloWorld = _resolveComponent("HelloWorld")

    return (_openBlock(),
    _createBlock(_Fragment, null, [_hoisted_1, _createVNode(_component_HelloWorld, {
        msg: "Hello Vue 3.0 + Vite"
    })], 64 /* STABLE_FRAGMENT */
    ))
}

vite对于其他的类型文件的处理几乎都是类似的逻辑,即根据请求的不同文件类型,做出不同的编译处理结果。

扩展阅读

· Vue 3 组件开发实战:搭建基于SpreadJS的表格编辑系统(组件集成篇)

· Vue 3 组件开发实战:搭建基于SpreadJS的表格编辑系统(功能拓展篇)

· SpreadJS Vue 框架支持

查看原文

赞 16 收藏 3 评论 0

高阳Sunny 赞了文章 · 4月11日

许式伟:相比 Python,我们可能更需要 Go+

ECUG(Effective Cloud User Group,实效云计算用户组)主办的 2021 ECUG Con 于 2021 年 4 月 10 日 - 11 日在上海举办。会上,七牛云 CEO 许式伟以 “数据科学与 Go+” 为主题发表了主题分享,讲述了对数据科学变迁的理解,对新语言 Go+ 的设想和规划,并大胆指出数据科学正迎来爆发期,像字节跳动一样的新型公司只会越来越多。以下为演讲内容整理。

七牛云 CEO 许式伟

刚才在闲聊说 ECUG 变得越来越高大上,其实我也变得越来越像一个单纯的讲师。今年是 ECUG 社区的第 14 个年头,这场活动也是第 14 届 ECUG Con。其实这一届本来应该在去年办,但因为疫情延后了。

其实,我在 ECUG 一直贯彻的理念有两个:

第一,让自己持续地写代码。因为每一次来 ECUG 我都很紧张,不能什么都没有呀。所以这也是挺好的机会,能让自己持续留在技术一线;

另外,我每年分享的主题都有一定的延续性,呈现了我自己对未来思考的脉络。

去年开始,我在聊数据科学,前面有三年是聊在端上的一些实践。原因是我认为云计算的第一个时代应该是属于机器计算,也就是虚拟机;第二代就是云原生,我认为这是一场被称为“基础架构”的革命。也就是说,第一阶段是资源,第二阶段是基础架构。 第三个阶段,我的判断为应用计算,这会涉及前端和后端的协同。

从去年开始,我的分享转向了数据科学,一个很重要的因素和趋势是,数据时代的到来。尤其是 2017 年之后,数据大量地被数字化以后,在各行各业都会有涉及数据科学的广泛应用。

去年也是蛮巧的,我脑子一热就搞了一个语言出来。我以前搞过蛮多语言,受众也有一些。但是那个都很明确,从来没有想过有一天能够商业化。也许碰巧有一些公司用它来做商业化,但是基本上从出生那一刻开始,就不是冲着商业化去的。

2012 年我花了很多精力在布道 Go,因为当时作为一个初创公司,招人太难。一个比较好的招人逻辑就是让别人觉得你有趣,公司技术氛围很不错。Go+ 是我第一个认认真真希望能够把它商业化的语言,但目前宣传得还不多,1.0 还没发布。我想讲讲我自己对 Go+ 和数据科学的一个思考,为什么认为 Go+ 有商业化的机会

我今天聊的话题大概有四个方面:

  1. 语言的发展
  2. 数据科学的发展
  3. Go+ 的设计理念
  4. Go+ 实现的迭代

语言的发展

首先,我们讲讲语言的发展,程序员对这个话题非常感兴趣。我把语言的发展史分为三个部分来说。

第一,静态语言的发展史。我选的是 TOP20 的语言,这个是根据现在最火的语言排行榜排名选的,前 20 名的语言我排了一下大概是这样的,最早发布的是 C,到现在其实还在排行榜前三的位置。第二是 C++,Objective-C、Java、C#、Go、Swift、Go+。我们可以看到一个比较有趣的现象,差不多每 6-8 年会出现一轮新的、具备影响力的静态语言,这是生产力迭代的表征。

image

第二,脚本语言的发展。你会发现它们非常不一样。最早是 Visual Basic,然后是 Python、PHP、JavaScript、Ruby,脚本语言是集中大爆发的,差不多全在 Java 出现的前后,来自 90 年代的前 5 个年头。这是非常有趣的一件事情,也是非常值得思考的,背后一定有一些内在的原因。

image

第三,数据科学相关语言的发展。但数据科学我选的是 TOP50,因为 TOP20 实在太少了。也蛮有意思的,最早的是 SQL,第二个 SAS,MATLAB、Python、R、Julia。Python 最早从来没想过自己会是数据科学语言,但最终变成了人工智能领域最火的语言。

image

这里又存在一个很明显的特征:它的跨度跟静态语言一样大,所以数据科学发展其实是古老而漫长的,但发展得没有那么快。静态语言差不多每 6-8 年有一个迭代,但数据科学语言不是,中间跨度特别大。但我觉得现在正进入数据科学的加速期。

你可能会想,为什么我要分析语言发展史呢?有几个结论是关键。

首先,我认为脚本语言是特定历史阶段下的产物,长期来看,静态语言更有生命力。

第二,数据科学是计算机的最初需求,最早计算机就是用来做计算的。它历史悠久但进步缓慢,因为数据大爆发的时代一直没有到来。

image

数据科学的发展

聊完语言的发展,接下来我们谈谈数据科学的发展。数据科学也可以分为几个阶段,第一个阶段我叫做“原始时期”,也可以叫“数学软件时代”,这个时期基本上可归纳为两个特征,第一个是在有限领域里,最典型的是 BI(商业智能);第二个有限数据规模,典型就像 Excel,行列数都是非常有限的,其他的软件也基本上是这样的。

这个时期的数据科学特点是什么?首先它不是一个基础设施,实际上是数学应用软件,但能力非常全,很强大,包括了统计、预测、洞察、规划、决策等等。

image

第二个时期我叫做“数据科学的基建时期”,真正让数据科学成为了基础设施,最典型的代表是大数据的兴起。Map/Reduce 是 Google 2004 年发布的一篇论文,2006 年就出现了 Hadoop,2009 年出现了 Spark。我认为这算是大数据兴起的一个阶段,也是数据科学基础设施化的开始。这个时期跟刚才的数学软件不一样,是以大规模处理能力为先,并不是以功能强大为先,它的功能相对局限

image

深度学习的兴起和大数据的兴起间隔时间比较长,深度学习 2015 年开始有 TensorFlow,2017 年开始有 Torch,这是两个知名度最高的深度学习框架,深度学习本质就是通过数据自动推导 y=F(x)中的 F 函数。我们平常通常都是程序员实现这个 F,但深度学习最核心的概念是如何让机器自动产生这个 F,来达成最佳曲线拟合。它其实是基于测量结果的自动计算。

image

假设今天没有牛顿三大定理,但我有一堆测量数据,理论上应该能够发现牛顿三大定理,这就是深度学习的核心逻辑。它跟大数据并不是相互取代的关系,而是一种能力的加强,更多其实是如何让大数据的能力更进一步,更强悍。

有种看法认为,今天经济发展背后科技的驱动因子其实核心就只有两个,一个是计算,另外一个是数据

数据核心就是我们今天聊的数据科学,数据科学其实是到了一个新的范式,有一个词叫“第四范式”,中国有一个公司也叫第四范式,我们认为数据是更高阶的一种生产能力,它跟计算相比的话站在更高层次的维度

image

前面是数据科学的两个阶段,那么第三个阶段是什么?我觉得是数据科学的大爆发时期,也就是今天,用马云的话说是“DT 时代”。原始时期是在有限的领域,有限的数据规模下去做的一种能力。未来首先是全领域的,首先领域不局限于的商业智能( BI )这样的范畴,第二个是大规模的数据第三个是随处可见,随处可见包括云、智能手机、嵌入式设备等,这些都会植入我们所谓的数据智能。

image

这就意味着,今天移动互联网的兴起已经让很多公司非常牛,互联网的平民化或互联网应用的诞生,催生了 BAT。但是我们知道,现在新兴的、比较牛的公司,像字节跳动这种,其实不是互联网的成功,而是数据科学的成功。今天仍然不能说,数据科学是平民化的,它的门槛非常高。

但是我们看到,智能应用已经产生了,智能应用不会只局限于抖音这样一个局部领域的生产力放大,各行各业都会被数据智能,也就是刚刚我们提到的第四范式所影响。

数据和数据科学,一定会成为下一代生产力的支撑,今天产生了字节跳动、快手这样的新兴的公司,但他们只是一个开始,绝对不是结局。

在数据科学的原始时期,数据只是副产品。大家想象一下,在 BI 领域,数据只是一个副产品,只是用于后期的运营决策。

但是今天我们看到在大量的应用里,数据就是原材料。这是非常不一样的状态,这也是为什么,我把它叫做数据科学大爆发时期,这是我觉得今天为什么需要 Go+ 的原因,也是其背后的历史背景。

image

数据科学的未来一定是通用语言和数学软件的融合,从而完成真正意义上的数据科学的基础设施化。但在今天,数据科学的基础设施化还远没有完全完成,这是我自己的判断。

image

今天的 Python 已经很好了,为何需要 Go+?

当然很多人会有疑问:今天的 Python 已经很好了,在深度学习领域已经被非常广泛地使用,为什么 Python 还不够,需要 Go+?其实我是认为,Python 是成不了基础设施的,它是一个脚本语言,我认为仅仅是特定历史阶段的需要。

数据科学本身是一种算力革命,哪怕在芯片领域,数据也能干翻计算,这是 Nvidia 干翻 Intel 的核心原因。上层软件领域就更加如此,一定会有一个新的基础设施承载者需要出现。

算力本质上是一种计算密集型业务,Python 的背后是 C,只靠 Python 还是不行。今天是 C 和 Python 支撑了整个深度学习,但数据科学一定还要进一步下沉,下沉的结果是什么?

image

这是我们今天需要 Go+ 的原因!前面主要讲我自己为什么认为 Go+有商业化的机会。当然我所说的商业化不一定是赚钱,大家不要误会这一点,语言可能在大多数人心目中是一个不赚钱的东西,但是这不代表它不重要,它非常重要。

Go+ 的设计理念

聊完数据科学的发展,接下来我们聊聊 Go+ 的设计理念。Go+为什么是今天这个样子?计算背后要的是程序员,而数据科学背后要的是数据科学家或者叫分析师。这两个角色其实还是不一样的,虽然都是技术工作。我认为培养程序员是相对容易的,今天程序员的数量是非常庞大的,但数据科学家的数量相对较少,这也是为什么前几年深度学习兴起以后,所谓的 AI 工程师薪资被炒翻了,比程序员贵很多。其实就是因为数据科学家不容易找。

这个角色承载着技术和商业的连接,要找到同时具备两种能力的人是很难的。数据科学首先是一个技术工作,要的是技术能力,又要懂商业。今天仍然没有非常体系化的培养数据科学家的能力,没有这样一个体系方法论。

image

那么 Go+ 的核心理念又是什么呢?

第一个,我们试图用 Go+ 来统一程序员和数据科学家,让他们之间有共同话语,让双方能自然对话,我觉得这是 Go+ 最核心的一个思考点。Go+ 很重要的一个核心逻辑,是用一门语言让两个角色进行对话。

image

在这个基础上,我们延伸了一些设计逻辑。首先,Go+ 是一个静态语言,语法是完全兼容 Go 的;第二,形式上要比 Go 更像脚本,有更低的学习门槛。Go 虽然在静态语言里,可能学习门槛是低的,但还不够低,没有 Python 那么低;第三,很自然的,我们要做一个数据科学的语言,所以它必然要有更简洁的、数学运算上的语言文法支持;第四是双引擎,同时支持静态编译为可执行文件,也支持编译成字节码来解释执行

image

为什么我们会选择语法完全兼容 Go 呢?首先我个人很坚定地认为,静态语言拥有更强的生命力,更能跨越历史的周期。大家也都很容易理解,语言是需要跨越周期的,语言的生命周期通常都非常长。我们不能很局限地说,当前在流行些什么东西,我就如何决定语言的设计,实际上我们要找到那些能够跨越周期的元素。

image

第二,为什么是 Go?我个人认为,在静态语言里,Go 的语法设计最为精简,学习门槛也是最低的,哪怕你以前没有学过静态语言,也很容易学会 Go。我们公司是最早招聘 Go 程序员的,但大部分招进来的人都不会 Go。我们用 Go 的时候,世界上真没多少人认为 Go 是未来的流行语言。我们自己实践的经验表明,Go 语言两周的学习基本上够了,是门槛非常低的一门静态语言。

但从数据科学语言来讲,Go 的门槛还不够低,Go+ 虽然完全兼容 Go,但我们希望它比 Go 的门槛还要有更低。所以它形式上要比 Go 更像脚本,因为脚本往往更容易理解。我们希望 Go+ 学习门槛和 Python 处于同一个层次

去年 5、6 月份 Go+ 刚诞生,差不多 10 月份左右,我就开始让 13-14 岁,六年级到初一这个阶段的三个小孩尝试学习 Go+。这个实践证明,这个事情是可行的。他们能理解 Go+ 的设计,能够自如地使用 Go+ 写代码。这也证明了我们在 Go 的基础上做的所有简化的努力是非常划算的。

image

我这里简单列了一些 Go+ 的语法,当然不是全部,只是一些我认为还是相对比较简洁的表达。有理数 Python 里面没有,我们认为有理数在数据科学里,尤其在无损数值运算里,还是会非常常见。Go+ 内置了有理数的支持。当然 Map、Slice 基本上 Python 都有。

image

列表理解(List comprehesion)其实也是 Python 有的,但我们对列表理解的支持非常的完整,基本上理解了 Go+ 中 for 循环怎么写也就理解了列表理解。更多的还是数据科学的一些常规操作的简洁表达。以上是一个大概语法示意,如果有朋友没看过 Go+,希望可以大概对 Go+ 有个理解。

image

Go+ 非常有意思的一点,它是唯一一个选择了双引擎的语言,既支持静态编译,也支持可解析执行

为什么要做双引擎呢?因为我认为程序员和数据科学家的诉求是不一样的,数据科学家喜欢单步执行,大家可以在心中回想一下你见过的数学软件,包括 SAS、MATLAB,数学软件交互都是单步执行的方式。

这并不是因为数据科学家懒。程序员理解程序逻辑是可以放在脑子里的,我们脑子里知道程序逻辑写得对不对。但数据科学家做计算的时候,不能知道计算结果对不对,因为人的计算能力比计算机弱太多了,所以一定要单步执行看到计算结果,才能知道自己下一步应该怎么办,这是数据科学家和程序员工作模式完全不同的一个点。

因为他是在做计算而不是在做一种程序逻辑,所以他很难不去做单步执行。

但当数据科学家建了一种模型,最终要使用了,这时他仍然希望最终交付的是最大化的执行效率,他一定不希望代码运行很慢,所以这个时候他就又需要静态编译执行,这也是为什么 Go+ 希望设计成双引擎,因为调试阶段和生产使用阶段,工作模式完全不一样。

image

Go+ 实现上的迭代

聊完 Go+ 的设计理念,我们进入最后一个 session,Go+ 实现上的迭代。当前 Go+ 做到了什么份上?Go+ 虽然还没有推出 1.0 版本,但是语法目前支持百分之六七十肯定有了,语法完成度还是不错的。

Go+ 的源代码,通过扫描器转成一个 Go+ 的 Token,再通过一个 parser 变成 Go+ 的抽象的语法数,常见语言都是这么干的。Go+ 的抽象语法树转化后有两个分支,一个生成 Go 的代码从而使其可以静态编译,另外一个分支生成字节码解析执行,分支的多态是通过引入了一个叫执行规范(exec.spec)的东西,其实就是一个抽象的接口。

image

当前,我个人在迭代的过程中发现了一个问题,对一个初步加入 Go+ 团队的人来说,是需要一段时间熟悉整个业务的。Go+ 执行规范的部分,其实是一种抽象的 SAX 接口,也就是基于事件驱动,我有一个事件发送给接受方,接受方按自己的需要处理这个事件,这在文本处理里面比较常见。

我们之前设计的接口基本上是用事件驱动的模式来把不同组件连接起来。编译器把抽象语法数解析完发出一些事件,这些事件被两个代码生成的模块接收,按照自己的需求去干活。这个模式代码还是有点难理解,尤其是编译器里面又做一些复杂的事情,让代码比较难理解。大家如果了解过 Go 背后的实现逻辑,类型推导在 Go 里面比较复杂,其实我们编译器的复杂性大部分是由类型推导导致的。

我当前在试图重构这个逻辑,想把执行规范部分变得不再是一个抽象的接口,而是一个标准实现的 DOM,这个 DOM 本身包含了类型推导的能力,从而使得编译器相对比较简单。讲实现我今天没法讲的特别细,后面有机会再展开。

image

下面我想讲一下 Go+ 下一步做的重心是什么。

首先,最核心的逻辑,还是希望今年能够发布 1.0 版本,而 1.0 版本最重要的事情是把用户的使用范式做最大化的确认,1.0 以后我希望和 Go 差不多,后面的语法变更是比较少的。当前最重要的工作,是明确 Go+ 需要哪些最核心的语法,并且在 1.0 版本就尽量去支持,除非有一些特定的考量比如说像 Go 的范型这种特别复杂的语法特性,留到后续的版本去支持。Go+ 也是类似的,我们可能会放弃一些特别复杂的语法特性,但是基本上尽可能把大部分我们需要的语法特性在 1.0 版本里确定下来

Go+ 1.0 我们会先进行单引擎的迭代,先做好静态编译的引擎,等 1.0 发布以后再迭代脚本的引擎。这也是基于上面我们说的用户的使用范式优先的理念下的一个决策。

image

最后,我们希望用商业化的方式来运作 Go+,也会招聘 Go+ 的团队成员,欢迎大家加入 Go+ 团队

我认为 Go+的核心是首先统一了程序员和数据科学家的语言,让双方能够自然对话。另外我非常坚定地相信 Go+ 会是数据科学的下一个变革,我自己非常兴奋能够做这样一件事情,也非常欢迎认可这件事的人加入我们。

image

这是联系我们的方法,第一个是项目的地址(https://github.com/goplus),第二个投简历的邮箱(jobs@qiniu.com),第三个是我推特的地址(@xushiwei)。


本文作者许式伟是七牛云创始人兼 CEOGo 语言大中华区首席布道师Go+ 语言创造者ECUG 社区发起人。他曾就职于金山、盛大,在搜索和分布式存储相关技术领域有十几年的研发经验。在金山,他以首席架构师的身份主导了 WPS Office 2005 的架构设计和开发。在创立金山实验室后,作为技术总监主导了分布式存储开发,后加入盛大创新院,并成功推出“盛大网盘”和“盛大云”。许式伟在 2020 年被评选为《2020 中国开源先锋 33 人之心尖上的开源人物》

许式伟

查看原文

赞 15 收藏 2 评论 1

高阳Sunny 赞了文章 · 4月11日

ARM 公布全新 Armv9 架构:10 年最大更新、不受制于美国出口管理条例

image.png

当今世界正处于快速变革之中,而变革的主要焦点就在于人工智能的发展,人工智能的发展又离不开算力、算法和大数据,算力的发展更离不开芯片的进步,近日 Arm 最新架构Armv9 正式推出,Arm 希望这个架构在未来十年可以成为下一代 3000 亿颗芯片计算平台的基础,也就是说 Armv9 将改变当下芯片格局,并让整个时代的算力有质的飞跃。

距离前一代Arm架构Armv8,Armv9的诞生用了整整十年,这是因为 Arm 希望它无论是在通用计算的经济性、设计自由度和可获得性基础上的普遍的专业化、安全及强大的处理需求上都能有非常大的提升。

Arm架构的进化历程

随着 Arm 架构的不断进化,我们的生活也在飞速改变,以手机为例,从ARM v5进化到ARM v6时,把我们从「大哥大」的时代带进了多媒体手机的世界,ARM v6被ARM v7所取代后,则带来了经典的Cortex-A8架构,开辟了触屏智能手机时代,而ARM v7到ARM v8的转变,首次将“64位计算”带到了手机上,让我们能在手机玩大型手游,拍出更高质量的照片,同时也为5G的到来铺平了道路。

image.png

在过去五年中,基于Arm的芯片出货量超过1000亿颗,Arm 推测其合作伙伴在未来十年的出货量将超过3000亿。无论是在终端、数据网络还是云端,未来绝大多数共享数据将在Arm上处理。

并且随着人工智能、物联网(IoT)和5G在全球范围内的飞速发展,Armv9中的新功能将加速每个应用从通用计算到更专业的计算的转变。Armv9也不仅会在智能手机、服务器、笔记本和网络设备领域中出现,未来在汽车无人驾驶、车内智能、物联网设备中我们也将更多的看到它的身影。

Armv9的优势

首先Armv9将使开发人员能够通过弥合硬件和软件之间的关键差距,同时满足对更复杂的基于人工智能的工作负载的需求,从而构建和编程未来值得信赖的计算平台,总体来说它有三大优势:

优势一:安全性大幅度提升

Armv9路线图引入了Arm保密计算架构(CCA),机密计算通过在基于硬件的安全环境中执行计算,保护部分代码和数据在使用中不被访问或修改,甚至不被特权软件访问或修改。Arm CCA将引入动态创建Realms的概念,在一个与安全和非安全世界分开的区域中,所有应用程序都可以使用。例如,在商业应用中,Realms可以保护商业敏感数据和代码,使其在使用、静止和传输中不受系统其他部分的影响。

优势二:AI能力的扩展

Arm与富士通合作创建了可扩展矢量扩展(SVE)技术,该技术是世界上最快的超级计算机Fugaku的核心。在这项工作的基础上,Arm为Armv9开发了SVE2,以便在更广泛的应用中实现增强的机器学习(ML)和数字信号处理(DSP)能力。

SVE2增强了5G系统、虚拟现实和增强现实以及在CPU上本地运行的ML工作负载的处理能力,例如图像处理和智能家居应用。值得一提的是未来几年,Arm除了在GPU和 NPU中不断进行AI创新外,还将进一步扩展其技术的AI能力,大幅提升CPU内的矩阵乘法。

优势三:通过系统设计实现性能最大化

预计未来两代移动和基础设施CPU中,CPU性能将提高30%以上。然而,随着行业从通用计算向无处不在的专业处理发展,每年两位数的CPU性能提升是不够的。除了增强专业处理能力外,Arm的全面计算设计方法还将通过集中的系统级硬件和软件优化以及提高用例性能来加速整体计算性能。

通过在其汽车、客户端、基础设施和物联网解决方案的整个IP组合中应用Total Compute设计原则,Armv9系统级技术将跨越整个IP解决方案,并改善单个IP。此外,Arm正在开发多项技术,以提高频率、带宽和缓存大小,并降低内存延迟,从而最大限度地提高基于Armv9的CPU的性能。

Armv9架构可以出口中国

前面介绍了这么多Armv9信息,我们现在回到最重要的问题上,Armv9 架构中国人可以用吗?答案是可以。

此前由于美国制裁的影响,一度盛传华为将被禁止使用Armv8架构,这无疑会对华为产生致命打击,因为海思芯片、鲲鹏处理器等产品均是基于Arm架构设计。

最终华为创始人任正非公开表示:「华为在美国将其加入实体清单前,已经取得了Armv8架构的永久授权,即便Arm与其终止合作,华为的芯片设计也不会受到大的影响,」才让大家吃了定心丸。

这次Arm官方回应了问题,Arm市场营销副总裁伊恩·斯迈斯在采访中表示,经过全面审查,Arm确定其v9架构不受美国出口管理条例(EAR)的约束,即来自美国的技术占比低于25%。“Arm已将此通知美国政府相关部门,我们将继续遵守美国商务部针对华为及其附属公司海思的指导方针。”斯迈斯表示。

在Arm的官方网站ArmV9介绍文章下方,我们也可以不少中国厂商的身影。

小米集团董事长兼首席执行官雷军表示:“小米与Arm的合作伙伴关系已在全球范围内提供了优质的移动体验。新的Armv9架构将帮助小米通过卓越的计算性能,最高的安全性和先进的机器学习功能,继续为数百万用户带来创新,从而创造更加直观,更智能的移动体验。”

vivo高级副总裁,首席技术官施玉坚:“ Armv9是移动领域的全新架构,它将为我们的产品提供增强的计算和安全功能。我们期待看到基于Armv9 CPU的出色性能,它可以加强我们提供高质量和独特产品的能力,从而一如既往地更好地满足客户的需求。”

Oppo副总裁兼Oppo研究所所长刘文文:“采用Arm技术的OPPO智能手机为我们的用户带来了适当水平的性能,能效和安全性。Arm对新型Arm v9架构的创新承诺将使业界在未来创造新的,更智能的,甚至更安全的体验。”

这些被Arm官方公开展示的「贺词」似乎也在暗示,未来Armv9将不会被受到美国「操控」,中国厂商不仅可以使用基于Armv9架构的芯片,更可以基于此架构设计更多芯片应用于各种产品上。

https://www.arm.com/company/n...

image.png

查看原文

赞 3 收藏 0 评论 0

高阳Sunny 赞了文章 · 4月11日

2021年要了解的34种JavaScript简写优化技术

开发者的生活总是在学习新的东西,跟上变化不应该比现在更难,我的动机是介绍所有JavaScript的最佳实践,比如简写功能,作为一个前端开发者,我们必须知道,让我们的生活在2021年变得更轻松。

你可能做了很长时间的JavaScript开发,但有时你可能没有更新最新的特性,这些特性可以解决你的问题,而不需要做或编写一些额外的代码。这些技术可以帮助您编写干净和优化的JavaScript代码。此外,这些主题可以帮助你为2021年的JavaScript面试做准备。

1.如果有多个条件

我们可以在数组中存储多个值,并且可以使用数组 include 方法。

//Longhand
if (x === 'abc' || x === 'def' || x === 'ghi' || x ==='jkl') {
  //logic
}

//Shorthand
if (['abc', 'def', 'ghi', 'jkl'].includes(x)) {
  //logic
}

2.如果为真…否则简写

这对于我们有 if-else 条件,里面不包含更大的逻辑时,是一个较大的捷径。我们可以简单的使用三元运算符来实现这个简写。

// Longhand
let test: boolean;
if (x > 100) {
  test = true;
} else {
  test = false;
}

// Shorthand
let test = (x > 10) ? true : false;
//or we can use directly
let test = x > 10;
console.log(test);

当我们有嵌套条件时,我们可以采用这种方式。

let x = 300,
test2 = (x > 100) ? 'greater 100' : (x < 50) ? 'less 50' : 'between 50 and 100';
console.log(test2); // "greater than 100"

3.声明变量

当我们要声明两个具有共同值或共同类型的变量时,可以使用此简写形式。

//Longhand 
let test1;
let test2 = 1;

//Shorthand 
let test1, test2 = 1;

4.Null, Undefined,空检查

当我们创建新的变量时,有时我们想检查我们引用的变量的值是否为空或undefined。JavaScript确实有一个非常好的简写工具来实现这些功能。

// Longhand
if (test1 !== null || test1 !== undefined || test1 !== '') {
    let test2 = test1;
}

// Shorthand
let test2 = test1 || '';

5.null值检查和分配默认值

let test1 = null,
    test2 = test1 || '';

console.log("null check", test2); // output will be ""

6.undefined值检查和分配默认值

let test1 = undefined,
    test2 = test1 || '';

console.log("undefined check", test2); // output will be ""

正常值检查

let test1 = 'test',
    test2 = test1 || '';

console.log(test2); // output: 'test'

7.将值分配给多个变量

当我们处理多个变量并希望将不同的值分配给不同的变量时,此简写技术非常有用。

//Longhand 
let test1, test2, test3;
test1 = 1;
test2 = 2;
test3 = 3;

//Shorthand 
let [test1, test2, test3] = [1, 2, 3];

8.赋值运算符简写

我们在编程中处理很多算术运算符,这是将运算符分配给JavaScript变量的有用技术之一。

// Longhand
test1 = test1 + 1;
test2 = test2 - 1;
test3 = test3 * 20;

// Shorthand
test1++;
test2--;
test3 *= 20;

9.如果存在简写

这是我们大家都在使用的常用简写之一,但仍然值得一提。

// Longhand
if (test1 === true) or if (test1 !== "") or if (test1 !== null)

// Shorthand //it will check empty string,null and undefined too
if (test1)

注意:如果test1有任何值,它将在if循环后进入逻辑,该运算符主要用于 nullundefined 的检查。

10.多个条件的AND(&&)运算符

如果仅在变量为 true 的情况下才调用函数,则可以使用 && 运算符。

//Longhand 
if (test1) {
 callMethod(); 
} 

//Shorthand 
test1 && callMethod();

11.foreach循环简写

这是迭代的常用简写技术之一。

// Longhand
for (var i = 0; i < testData.length; i++)

// Shorthand
for (let i in testData) or  for (let i of testData)

每个变量的数组

function testData(element, index, array) {
  console.log('test[' + index + '] = ' + element);
}

[11, 24, 32].forEach(testData);
// logs: test[0] = 11, test[1] = 24, test[2] = 32

12.return中比较

我们也可以在return语句中使用比较。它将避免我们的5行代码,并将它们减少到1行。

// Longhand
let test;
function checkReturn() {
  if (!(test === undefined)) {
    return test;
  } else {
    return callMe('test');
  }
}
var data = checkReturn();
console.log(data); //output test
function callMe(val) {
    console.log(val);
}

// Shorthand
function checkReturn() {
    return test || callMe('test');
}

13.箭头函数

//Longhand 
function add(a, b) { 
   return a + b; 
} 

//Shorthand 
const add = (a, b) => a + b;

更多示例。

function callMe(name) {
  console.log('Hello', name);
}
callMe = name => console.log('Hello', name);

14.短函数调用

我们可以使用三元运算符来实现这些功能。

// Longhand
function test1() {
  console.log('test1');
};
function test2() {
  console.log('test2');
};
var test3 = 1;
if (test3 == 1) {
  test1();
} else {
  test2();
}

// Shorthand
(test3 === 1? test1:test2)();

15. Switch简写

我们可以将条件保存在键值对象中,并可以根据条件使用。

// Longhand
switch (data) {
  case 1:
    test1();
  break;

  case 2:
    test2();
  break;

  case 3:
    test();
  break;
  // And so on...
}

// Shorthand
var data = {
  1: test1,
  2: test2,
  3: test
};

data[something] && data[something]();

16.隐式返回简写

使用箭头函数,我们可以直接返回值,而不必编写return语句。

//longhand
function calculate(diameter) {
  return Math.PI * diameter
}

//shorthand
calculate = diameter => (
  Math.PI * diameter;
)

17.小数基数指数

// Longhand
for (var i = 0; i < 10000; i++) { ... }

// Shorthand
for (var i = 0; i < 1e4; i++) {

18.默认参数值

//Longhand
function add(test1, test2) {
  if (test1 === undefined)
    test1 = 1;
  if (test2 === undefined)
    test2 = 2;
  return test1 + test2;
}

//shorthand
add = (test1 = 1, test2 = 2) => (test1 + test2);
add() //output: 3

19.扩展运算符简写

//longhand

// joining arrays using concat
const data = [1, 2, 3];
const test = [4 ,5 , 6].concat(data);

//shorthand

// joining arrays
const data = [1, 2, 3];
const test = [4 ,5 , 6, ...data];
console.log(test); // [ 4, 5, 6, 1, 2, 3]

对于克隆,我们也可以使用扩展运算符。

//longhand

// cloning arrays
const test1 = [1, 2, 3];
const test2 = test1.slice()

//shorthand

// cloning arrays
const test1 = [1, 2, 3];
const test2 = [...test1];

20.模板文字

如果您厌倦了在单个字符串中使用 + 来连接多个变量,那么这种简写可以消除您的头痛。

//longhand
const welcome = 'Hi ' + test1 + ' ' + test2 + '.'

//shorthand
const welcome = `Hi ${test1} ${test2}`;

21.多行字符串简写

当我们在代码中处理多行字符串时,可以使用以下功能:

//longhand
const data = 'abc abc abc abc abc abc\n\t'
    + 'test test,test test test test\n\t'

//shorthand
const data = `abc abc abc abc abc abc
         test test,test test test test`

22.对象属性分配

let test1 = 'a'; 
let test2 = 'b';

//Longhand 
let obj = {test1: test1, test2: test2}; 

//Shorthand 
let obj = {test1, test2};

23.将字符串转换成数字

//Longhand 
let test1 = parseInt('123'); 
let test2 = parseFloat('12.3'); 

//Shorthand 
let test1 = +'123'; 
let test2 = +'12.3';

24.用解构简写

//longhand
const test1 = this.data.test1;
const test2 = this.data.test2;
const test2 = this.data.test3;

//shorthand
const { test1, test2, test3 } = this.data;

25.用Array.find简写

当我们确实有一个对象数组并且我们想要根据对象属性查找特定对象时,find方法确实很有用。

const data = [
  {
    type: 'test1',
    name: 'abc'
  },
  {
    type: 'test2',
    name: 'cde'
  },
  {
    type: 'test1',
    name: 'fgh'
  },
]
function findtest1(name) {
  for (let i = 0; i < data.length; ++i) {
    if (data[i].type === 'test1' && data[i].name === name) {
      return data[i];
    }
  }
}

//Shorthand
filteredData = data.find(data => data.type === 'test1' && data.name === 'fgh');
console.log(filteredData); // { type: 'test1', name: 'fgh' }

26.查找条件简写

如果我们有代码来检查类型,根据类型需要调用不同的方法,我们可以选择使用多个else ifs或者switch,但是如果我们有比这更好的简写方法呢?

// Longhand
if (type === 'test1') {
  test1();
}
else if (type === 'test2') {
  test2();
}
else if (type === 'test3') {
  test3();
}
else if (type === 'test4') {
  test4();
} else {
  throw new Error('Invalid value ' + type);
}

// Shorthand
var types = {
  test1: test1,
  test2: test2,
  test3: test3,
  test4: test4
};
 
var func = types[type];
(!func) && throw new Error('Invalid value ' + type); func();

27.按位索引简写

当我们遍历数组以查找特定值时,我们确实使用 indexOf() 方法,如果找到更好的方法该怎么办?让我们看看这个例子。

//longhand
if(arr.indexOf(item) > -1) { // item found 
}
if(arr.indexOf(item) === -1) { // item not found
}

//shorthand
if(~arr.indexOf(item)) { // item found
}
if(!~arr.indexOf(item)) { // item not found
}

按位()运算符将返回除-1以外的任何值的真实值。否定它就像做 ~~ 一样简单。另外,我们也可以使用 include() 函数:

if (arr.includes(item)) { 
    // true if the item found
}

28.Object.entries()

此函数有助于将对象转换为对象数组。

const data = { test1: 'abc', test2: 'cde', test3: 'efg' };
const arr = Object.entries(data);
console.log(arr);
/** Output:
[ [ 'test1', 'abc' ],
  [ 'test2', 'cde' ],
  [ 'test3', 'efg' ]
]
**/

29.Object.values()

这也是ES8中引入的一项新功能,该功能执行与 Object.entries() 类似的功能,但没有关键部分:

const data = { test1: 'abc', test2: 'cde' };
const arr = Object.values(data);
console.log(arr);
/** Output:
[ 'abc', 'cde']
**/

30.双按位简写

双重NOT按位运算符方法仅适用于32位整数)

// Longhand
Math.floor(1.9) === 1 // true

// Shorthand
~~1.9 === 1 // true

31.重复一个字符串多次

要一次又一次地重复相同的字符,我们可以使用for循环并将它们添加到同一循环中,但是如果我们有一个简写方法呢?

//longhand 
let test = ''; 
for(let i = 0; i < 5; i ++) { 
  test += 'test '; 
} 
console.log(str); // test test test test test 

//shorthand 
'test '.repeat(5);

32.在数组中查找最大值和最小值

const arr = [1, 2, 3]; 
Math.max(…arr); // 3
Math.min(…arr); // 1

33.从字符串中获取字符

let str = 'abc';

//Longhand 
str.charAt(2); // c

//Shorthand 
Note: If we know the index of the array then we can directly use index insted of character.If we are not sure about index it can throw undefined
str[2]; // c

34.数学指数幂函数的简写

//longhand
Math.pow(2,3); // 8

//shorthand
2**3 // 8
查看原文

赞 89 收藏 62 评论 19

高阳Sunny 赞了文章 · 4月11日

美团图数据库平台建设及业务实践

图数据结构,能够更好地表征现实世界。美团业务相对较复杂,存在比较多的图数据存储及多跳查询需求,亟需一种组件来对千亿量级图数据进行管理,海量图数据的高效存储和查询是图数据库研究的核心课题。本文介绍了美团在图数据库选型及平台建设方面的一些工作。

1 前言

图数据结构,能够很自然地表征现实世界。比如用户、门店、骑手这些实体可以用图中的点来表示,用户到门店的消费行为、骑手给用户的送餐行为可以用图中的边来表示。使用图的方式对场景建模,便于描述复杂关系。在美团,也有比较多的图数据存储及多跳查询需求,概括起来主要包括以下 4 个方面:

  • 图谱挖掘: 美团有美食图谱、商品图谱、旅游图谱、用户全景图谱在内的近 10 个领域知识图谱,数据量级大概在千亿级别。在迭代、挖掘数据的过程中,需要一种组件对这些图谱数据进行统一的管理。
  • 安全风控: 业务部门有内容风控的需求,希望在商户、用户、评论中通过多跳查询来识别虚假评价;在支付时进行金融风控的验证,实时多跳查询风险点。
  • 链路分析: 包括代码分析、服务治理、数据血缘管理,比如公司数据平台上有很多 ETL Job,Job 和 Job 之间存在强弱依赖关系,这些强弱依赖关系形成了一张图,在进行 ETL Job 的优化或者故障处理时,需要对这个图进行实时查询分析。
  • 组织架构: 公司组织架构的管理,实线汇报链、虚线汇报链、虚拟组织的管理,以及商家连锁门店的管理。比如,维护一个商家在不同区域都有哪些门店,能够进行多层关系查找或者逆向关系搜索。

总体来说,美团需要一种组件来管理千亿级别的图数据,解决图数据存储以及多跳查询问题。海量图数据的高效存储和查询是图数据库研究的核心课题,如何在大规模分布式场景中进行工程落地是我们面临的痛点问题。传统的关系型数据库、NoSQL 数据库可以用来存储图数据,但是不能很好处理图上多跳查询这一高频的操作。

Neo4j 公司在社交场景(见图1)里做了传统关系型数据库 MySQL 跟图数据库 Neo4j 的查询性能对比 [1],在一个包含 100 万人、每人约有 50 个朋友的社交网络里找最大深度为 5 的朋友的朋友,实验结果表明多跳查询中图数据库优势明显(见图 2)。然而选取或者自主研发一款高吞吐、低查询延时、能存储海量数据且易用的图数据库非常困难。下面将介绍美团在图数据库选型及平台建设方面的一些工作。

图 1

图 2

2 图数据库选型

在图数据库的选型上我们主要考虑了以下 5 点:(A) 项目开源,暂不考虑需付费的图数据库;(B) 分布式架构设计,具备良好的可扩展性;(C) 毫秒级的多跳查询延迟;(D) 支持千亿量级点边存储;(E) 具备批量从数仓导入数据的能力。

分析 DB-Engines[2] 上排名前 30 的图数据库,剔除不开源的项目,我们将剩余的图数据库分为三类:

  • 第一类:Neo4j[3]、ArangoDB[4]、Virtuoso[5]、TigerGraph[6]、RedisGraph[7]。 此类图数据库只有单机版本开源可用,性能优秀,但不能应对分布式场景中数据的规模增长,即不满足选型要求(B)、(D)。
  • 第二类:JanusGraph[8]、HugeGraph[9]。 此类图数据库在现有存储系统之上新增了通用的图语义解释层,图语义层提供了图遍历的能力,但是受到存储层或者架构限制,不支持完整的计算下推,多跳遍历的性能较差,很难满足 OLTP 场景下对低延时的要求,即不满足选型要求(C)。
  • 第三类:DGraph[10]、NebulaGraph[11]。 此类图数据库根据图数据的特点对数据存储模型、点边分布、执行引擎进行了全新设计,对图的多跳遍历进行了深度优化,基本满足我们的选型要求。

DGraph 是由前 Google 员工 Manish Rai Jain 离职创业后,在 2016 年推出的图数据库产品,底层数据模型是 RDF[12],基于 Go 语言编写,存储引擎基于 BadgerDB[13] 改造,使用 RAFT 保证数据读写的强一致性。

NebulaGraph 是由前 Facebook 员工叶小萌离职创业后,在 2019年 推出的图数据库产品,底层数据模型是属性图,基于 C++ 语言编写,存储引擎基于 RocksDB[14] 改造,使用 RAFT 保证数据读写的强一致性。

这两个项目的创始人都在互联网公司图数据库领域深耕多年,对图数据库的落地痛点有深刻认识,整体的架构设计也有较多相似之处。在图数据库最终的选型上,我们基于 LDBC-SNB 数据集[15]对 NebulaGraph、DGraph、HugeGraph 进行了深度性能测评,测试详情见文章:主流开源分布式图数据库 Benchmark,从测试结果看 NebulaGraph 在数据导入、实时写入及多跳查询方面性能均优于竞品。此外,NebulaGraph 社区活跃,问题响应速度快,所以团队最终选择基于 NebulaGraph 来搭建图数据库平台。

3 NebulaGraph架构

图 3

一个完整的 NebulaGraph 集群包含三类服务,即 Query Service、Storage Service 和 Meta Service。每类服务都有其各自的可执行二进制文件,既可以部署在同一节点上,也可以部署在不同的节点上。下面是NebulaGraph 架构设计(见图 3)的几个核心点16。

  • Meta Service: 架构图中右侧为 Meta Service 集群,它采用 Leader/Follower 架构。Leader 由集群中所有的 Meta Service 节点选出,然后对外提供服务;Followers 处于待命状态,并从 Leader 复制更新的数据。一旦 Leader 节点 Down 掉,会再选举其中一个 Follower 成为新的 Leader。Meta Service 不仅负责存储和提供图数据的 Meta 信息,如 Schema、数据分片信息等;同时还提供 Job Manager 机制管理长耗时任务,负责指挥数据迁移、Leader 变更、数据 compaction、索引重建等运维操作。
  • 存储计算分离: 在架构图中 Meta Service 的左侧,为 NebulaGraph 的主要服务,NebulaGraph 采用存储与计算分离的架构,虚线以上为计算,以下为存储。存储计算分离有诸多优势,最直接的优势就是,计算层和存储层可以根据各自的情况弹性扩容、缩容。存储计算分离还带来了另一个优势:使水平扩展成为可能。此外,存储计算分离使得 Storage Service 可以为多种类型的计算层或者计算引擎提供服务。当前 Query Service 是一个高优先级的 OLTP 计算层,而各种 OLAP 迭代计算框架会是另外一个计算层。
  • 无状态计算层: 每个计算节点都运行着一个无状态的查询计算引擎,而节点彼此间无任何通信关系。计算节点仅从 Meta Service 读取 Meta 信息以及和 Storage Service 进行交互。这样设计使得计算层集群更容易使用 K8s 管理或部署在云上。每个查询计算引擎都能接收客户端的请求,解析查询语句,生成抽象语法树(AST)并将 AST 传递给执行计划器和优化器,最后再交由执行器执行。
  • Shared-nothing 分布式存储层: Storage Service 采用 Shared-nothing 的分布式架构设计,共有三层,最底层是 Store Engine,它是一个单机版 Local Store Engine,提供了对本地数据的get/put/scan/delete 操作,该层定义了数据操作接口,用户可以根据自己的需求定制开发相关 Local Store Plugin。目前,NebulaGraph 提供了基于 RocksDB 实现的 Store Engine。在 Local Store Engine 之上是 Consensus 层,实现了 Multi Group Raft,每一个 Partition 都对应了一组 Raft Group。在 Consensus 层上面是 Storage interfaces,这一层定义了一系列和图相关的 API。 这些 API 请求会在这一层被翻译成一组针对相应 Partition 的 KV 操作。正是这一层的存在,使得存储服务变成了真正的图存储。否则,Storage Service 只是一个 KV 存储罢了。而 NebulaGraph 没把 KV 作为一个服务单独提出,最主要的原因便是图查询过程中会涉及到大量计算,这些计算往往需要使用图的 Schema,而 KV 层没有数据 Schema 概念,这样设计比较容易实现计算下推,是 NebulaGraph 查询性能优越的主要原因。

NebulaGraph 基于 C++ 实现,架构设计支持存储千亿顶点、万亿边,并提供毫秒级别的查询延时。我们在 3 台 48U192G 物理机搭建的集群上灌入 10 亿美食图谱数据对 NebulaGraph 的功能进行了验证。

  • 一跳查询 TP99 延时在 5ms 内,两跳查询 TP99 延时在 20ms 内,一般的多跳查询 TP99 延时在百毫秒内。
  • 集群在线写入速率约为20万 Records/s。
  • 支持通过 Spark 任务离线生成 RocksDB 底层 SST File,直接将数据文件载入到集群中,即类似 HBase BulkLoad 能力。
  • 提供了类 SQL 查询语言,对于新增的业务需求,只需构造 NebulaGraph SQL 语句,易于理解且能满足各类复杂查询要求。
  • 提供联合索引、GEO 索引,可通过实体属性或者关系属性查询实体、关系,或者查询在某个经纬度附近 N 米内的实体。
  • 一个 NebulaGraph 集群中可以创建多个 Space (概念类似 MySQL 的DataBase),并且不同 Space 中的数据在物理上是隔离的。

4 图数据库平台建设

图 4

为了统一管理图数据,减少工程同学在图数据库集群上的运维压力,我们基于开源分布式图数据库 NebulaGraph,搭建了一套一站式图数据库自助管理平台(见图 4),该平台包含以下 4 层:

  • 数据应用层。 业务方可以在业务服务中引入图谱 SDK,实时地对图数据进行增删改查。
  • 数据存储层。 支持两种图数据库集群的部署。

    • 第一种部署方式是 CP 方案,即 Consistency & Partition tolerance。单集群部署,集群中机器数量大于等于副本的数量,副本数量大于等于 3 。只要集群中有大于副本数一半的机器存活,整个集群就可以对外正常提供服务。CP 方案保证了数据读写的强一致性,但这种部署方式下集群可用性不高。
    • 第二种部署方式是 AP 方案,即 Availability & Partition tolerance。在一个应用中部署多个图数据库集群,每个集群数据副本数为 1 ,多集群之间进行互备。这种部署方式的好处在于整个应用对外的可用性高,但数据读写的一致性要差些。
  • 数据生产层。 图数据主要有两种来源,第一种是业务方把数仓中数据通过 ETL Job 转成点和边的 Hive 表,然后离线导入到图数据库中;第二种是业务线上实时产生的数据、或者通过 Spark/Flink 等流式处理产生的近线数据,调用在线批量写接口实时灌到图数据库中。
  • 支撑平台。 提供了 Schema 管理、权限管理、数据质检、数据增删改查、集群扩缩容、图谱画像、图数据导出、监控报警、图可视化、集群包管理等功能。

与业界方案相比,团队主导设计的图数据库平台除了支持存储千亿顶点、万亿边,具备毫秒级别查询能力外,还提供了如下四项能力:应用可用性 SLA 达 99.99%;支持每小时百亿量级数据导入;实时写入数据时保证多集群数据最终一致性;易用的图谱可视化能力。下面将介绍具体的设计思路。

4.1 高可用模块设计

图 5

首先介绍单应用多集群高可用模块的设计(AP 方案)。为什么有 AP 方案的设计呢?因为接入图数据库平台的业务方比较在意的指标是集群可用性。在线服务对集群的可用性要求非常高,最基础的要求是集群可用性能达到 4 个 9,即一年里集群的不可用时间要小于一个小时。对于在线服务来说,服务或者集群的可用性是整个业务的生命线,如果这点保证不了,即使集群提供的能力再多再丰富,那么业务方也不会考虑使用,可用性是业务选型的基础。

另外,公司要求中间件要有跨区域容灾能力,即要具备在多个地域部署多集群的能力。我们分析了平台接入方的业务需求,大约 80% 的场景是 T+1 全量导入数据、线上只读。在这种场景下,对图数据的读写强一致性要求并不高,因此我们设计了单应用多集群这种部署方案。

AP 方案部署方式可以参考图 5,一个业务方在图数据库平台上创建了 1 个应用并部署了 4 个集群,其中北京 2 个、上海 2 个,平时这 4 个集群同时对外提供服务。假如现在北京集群 1 挂了,那么北京集群 2 可以提供服务。如果说真那么不巧,北京集群都挂了,或者北京侧对外的网络不可用,那么上海的集群也可以提供服务。在这种部署方式下,平台会尽可能地通过一些方式来保证整个应用的可用性。然后每个集群内部尽量部署同机房的机器,因为图数据库集群内部 RPC 非常多,如果有跨机房或者跨区域的频繁调用,整个集群对外的性能会比较低。

图 6

高可用模块主要包含下面 4 个部分,如上图 6 所示:

第一部分是右侧的图数据库 Agent,它是部署在图数据库集群的一个进程,用来收集机器和图数据库三个核心模块的信息,并上报到图数据库平台。Agent 能够接收图数据库平台的命令并对图数据库进行操作。

第二部分是图数据库平台,它主要是对集群进行管理,并同步图数据库集群的状态到配置中心。

第三部分是图数据库 SDK,主要负责管理连接到图数据库集群的连接。如果业务方发送了某个查询请求,SDK 会进行集群的路由和负载均衡,选择出一条高质量的连接来发送请求。此外,SDK 还会处理图数据库集群中问题机器的自动降级以及恢复,并且支持平滑切换集群的数据版本。

第四部分是配置中心,类似 ZooKeeper,存储集群的当前状态。

4.2 每小时百亿量级数据导入模块设计

图 7

第二个模块是每小时百亿量级数据导入模块,平台在 2019 年底- 2020 年初全量导入数据的方式是调用 NebulaGraph 对外提供的批量数据导入接口,这种方式的数据写入速率大概是每小时 10 亿级别,导入百亿数据大概要耗费 10 个小时,耗时较长。此外,在以几十万每秒的速度导数据的过程中,会长期占用机器的 CPU、IO 资源,一方面会对机器造成损耗,另一方面数据导入过程中集群对外提供的读性能会变弱。

为了解决上面两个问题,平台进行了如下优化:在 Spark 集群中直接生成图数据库底层文件 SST File,再借助 RocksDB 的 Bulkload 功能直接 ingest 文件到图数据库。

数据导入的核心流程可以参考图 7,当用户执行导数据操作后,图数据库平台会向公司的 Spark 集群提交一个 Spark 任务,在 Spark 任务中会生成图数据库里相关的点、边以及点索引、边索引相关的 SST 文件,并上传到美团的 S3 云存储上。文件生成后,图数据库平台会通知应用中多个集群去下载这些存储文件,之后完成 ingest 跟 compact 操作,最后完成数据版本的切换。

为兼顾各个业务方的不同需求,平台统一了应用导入、集群导入、离线导入、在线导入,以及全量导入、增量导入这些场景,然后细分成下面九个阶段,从流程上保证在导数据过程中应用整体的可用性:SST File 生成 、SST File 下载 、ingest、compact、数据校验、增量回溯、数据版本切换、集群重启、数据预热。

4.3 实时写入多集群数据同步模块设计

图 8

第三个模块是实时写入多集群数据同步模块,平台约有 15% 的需求场景是在实时读取数据时,还要把新产生的业务数据实时写入集群,并且对数据的读写强一致性要求不高。就是说,业务方写到图数据库里的数据,不需要立马能读到。针对上述场景,业务方在使用单应用多集群这种部署方案时,多集群里的数据需要保证最终一致性。针对这一需求,我们做了以下设计。

第一部分是引入 Kafka 组件,业务方在服务中通过 SDK 对图数据库进行写操作时,SDK 并不直接写图数据库,而是把写操作写到 Kafka 队列里,之后由该应用下的多个集群异步消费这个 Kafka 队列。

第二部分是集群在应用级别可配置消费并发度,来控制数据写入集群的速度。具体流程如下:

  • SDK 对用户写操作语句做语法解析,将其中点边的批量操作拆解成单个点边操作,即对写语句做一次改写。
  • Agent 消费 Kafka 时确保每个点及其出边相关操作在单个线程里顺序执行(见图 8),保证这点就能保证各个集群执行完写操作后最终的结果是一致的。
  • 并发扩展:通过改变 Kafka 分片数、Agent 中消费 Kafka 线程数来调整 Kafka 中操作的消费速度。如果未来图数据库支持事务的话,上面的配置需要调整成单分片单线程消费,有必要对设计方案再做优化调整。

图 9

第三部分是在实时写入数据过程中,平台可以同步生成一个全量数据版本,并做平滑切换(见图 9),确保数据的不重、不漏、不延迟。

4.4 图可视化模块设计

图 10

第四个模块是图可视化模块(见图10),主要是用于解决子图探索问题。当用户在图数据库平台通过可视化组件查看图数据时,能尽量通过恰当的交互设计来避免因为节点过多而引发爆屏。主要包括以下几个功能:

  • 通过 ID 或者索引查找顶点。
  • 能查看顶点和边的卡片(卡片中展示点边属性和属性值),可以单选、多选、框选以及按类型选择顶点。
  • 图探索,当用户点击某个顶点时,系统会展示它的一跳邻居信息,包括该顶点有哪些出边?通过这个边它能关联到几个点?该顶点的入边又是什么情况?通过这种一跳信息的展示,用户在平台上探索子图的时候,可快速了解到周边的邻居信息,更快地进行子图探索。在探索过程中,平台也支持通过属性对边进行过滤。
  • 图编辑能力,让平台用户在不熟悉 NebulaGraph 语法的情况下也能增删改点边数据,对线上数据进行临时干预。

5 业务实践

5.1 智能助理

该项目数据是基于美团商户数据、用户评论构建的餐饮娱乐知识图谱,覆盖美食、酒店、旅游等领域,包含 13 类实体和 22 类关系。目前,点边数量大概在百亿级别,数据 T+1 全量更新,主要用于解决搜索或者智能助理里 KBQA(全称:Knowledge Based Question Answer)类问题。核心处理流程是通过 NLP 算法识别关系和实体后构造出 NebulaGraph SQL 语句,再到图数据库获取数据。

典型的应用场景包括商场找店,比如,某个用户想知道望京新荟城这个商场有没有海底捞,系统可以快速查出结果告诉用户;另一个场景是标签找店,用户想知道望京 SOHO 附近有没有适合情侣约会的餐厅,或者可以多加几个场景标签,系统都可以帮忙查找出来。

5.2 搜索召回

该项目数据是基于医美商家信息构建的医美知识图谱,包含 9 类实体和 13 类关系,点边数量在百万级别,同样也是 T+1 全量更新,主要用于大搜底层实时召回,返回与 Query 相关的商户、产品或医生信息,解决医美类搜索词少结果、无结果问题。比如,某个用户搜“啤酒肚”这种症状、或者“润百颜”这类品牌,系统可以召回相关的医美门店。

5.3 图谱推荐理由

该项目数据来自用户的画像信息、商户的特征信息、用户半年内收藏/购买行为,数据量级是 10 亿级别,T+1 全量更新。现在美团 App 和点评 App 上默认的商户推荐列表是由深度学习模型生成的,但模型并不会给出生成这个列表的理由,缺少可解释性。

而在图谱里用户跟商户之间天然存在多条连通路径,项目考虑选出一条合适路径来生成推荐理由,在 App 界面上展示给用户推荐某家店的原因。该项目基于用户的协同过滤算法来生成推荐理由,在家乡、消费水平、偏好类目、偏好菜系等多个组合维度中找出多条路径,然后给这些路径打分,选出一条分值较高的路径,之后按照特定 Pattern 产出推荐理由。通过上述方式,就可以获得“在北京喜欢北京菜的山东老乡都说这家店很赞”,或者“广州老乡都中意他家的正宗北京炸酱面”这类理由。

5.4 代码依赖分析

该项目把代码库中代码依赖关系写入到图数据库。代码库中存在很多服务代码,这些服务会包括对外提供的接口,这些接口的实现依赖于该服务中某些类的成员函数,这些类的成员函数又依赖了本类的成员变量、成员函数或者其它类的成员函数,那么它们之间的依赖关系就形成了一张图,可以把这个图写到图数据库里做代码依赖分析。

典型应用场景是精准测试:当开发同学完成需求并向公司的代码仓库提交了 PR 后,可以把更改实时地写到图数据库中。这样的话,开发同学就能查到他所写的代码影响了哪些外部接口,并且借助图可视化组件查看调用路径。如果开发同学本来是要改接口 A 的行为,改了很多代码,但是他可能并不知道他改的代码也会影响到对外接口 B、C、D,这时候就可以用代码依赖分析来做个 Check,增加测试的完备性。

6 总结与展望

目前,图数据库平台基本具备了对图数据的一站式自助管理功能。如果某个业务方要使用这种图数据库能力,那么业务方可以在平台上自助地创建图数据库集群、创建图的 Schema、导入图数据、配置导入数据的执行计划、引入平台提供的 SDK 对数据进行操作等等。平台侧主要负责各业务方图数据库集群的稳定性。目前,美团有三四十个业务已经在平台上落地,基本满足了各个业务方的需求。

未来规划主要有两个方向,第一,根据业务场景优化图数据库内核,提升平台稳定性,开发的通用 Feature 持续反哺 NebulaGraph 社区。第二,挖掘更多的图数据价值。现在平台仅支持图数据存储及多跳查询这种基本能力,后续将基于 NebulaGraph 去探索图学习、图计算的能力,为平台用户提供更多挖掘图数据价值的功能。

7 作者信息

登昌、梁帅、高辰、杨鑫、尊远、王超等,均为美团搜索与NLP部工程师。

8 招聘信息

如果你对“图存储”、“图学习”、“图计算”感兴趣,欢迎给我们投递简历,投递邮箱:zhaodengchang@meituan.com。

9 参考资料

阅读美团技术团队更多技术文章合集

前端 | 算法 | 后端 | 数据 | 安全 | 运维 | iOS | Android | 测试

| 在公众号菜单栏对话框回复【2020年货】、【2019年货】、【2018年货】、【2017年货】等关键词,可查看美团技术团队历年技术文章合集。

| 本文系美团技术团队出品,著作权归属美团。欢迎出于分享和交流等非商业目的转载或使用本文内容,敬请注明“内容转载自美团技术团队”。本文未经许可,不得进行商业性转载或者使用。任何商用行为,请发送邮件至tech@meituan.com申请授权。

查看原文

赞 5 收藏 2 评论 1

高阳Sunny 赞了文章 · 4月11日

为了让 Android 更安全,谷歌推荐开发者使用 Rust 编写系统代码

google x Android

对于安卓开发者来说,Java 和 Kotlin 是比较流行的选择;而对于从事操作系统以及内部底层的开发者来说,C 和 C++ 是比较热门的选择。但在众多语言中,Rust 作为一款小众的现代系统编程语言,近两年间逐渐受到了各大企业的青睐。

去年,AWS 开源团队宣布聘用 Rust 编译器联合创始人 Felix Klock,未来致力于进一步改进 Rust。苹果也表示对将 C 代码移植到 Rust 感兴趣,希望投身到使用 Rust 编写代码的潮流中,想要在基于 Linux 的服务器端平台上将 C 过渡到 Rust。微软在采访中也表示 C/C++ 无法胜任编写关键任务的软件,业界非常需要高性能、内存安全的编程语言来开发底层系统,而当今市场上最好的选择就是 Rust。

近日,谷歌宣布出于安全性的考虑,推荐开发者使用 Rust 编程语言来开发操作系统。并表示在过去的 18 个月里,它一直在为 Android 开源项目添加 Rust 支持。但在 Android 平台上添加一门新语言是一项巨大的工程。一些工具链和依赖关系需要维护,测试基础设施和工具必须更新,开发人员需要接受培训。

C 和 C++ 的局限性

Android 系统的底层需要 C 和 C++ 等系统编程语言。这些语言为开发者提供了控制和可预测性,这在访问低级系统资源和硬件时非常重要。不幸的是,C 和 C++ 并不能提供内存安全保证,使得它们容易出现错误和安全漏洞。开发者有责任在这些语言上管理内存寿命,但在复杂和多线程的代码库中,这说起来容易做起来难。

C 和 C++ 共同构成了 Android 平台上数以千万计的代码行。这些内存安全漏洞成为最难解决的代码错误来源,占 Android 高严重度安全漏洞的 70% 左右。单纯的修复这些 bug 变得不足以处理问题,更好的方法是在一开始就预防这些 bug。

由于缺乏内存安全保障,迫使开发者在严格约束的无权限沙盒内运行Android进程。但沙盒在资源上的成本很高,会消耗额外的开销,并引入延迟。沙盒也不能完全消除代码的漏洞,而且由于 bug 密度高,沙盒的功效会降低,进一步!

image.png

另一个限制,虽然不是 C 和 C++ 独有的,但适用于所有的内存安全问题,那就是错误状态必须在工具化的代码中实际触发,才能被检测到。所以即使你的代码有很好的测试,实际的 bug 也可能一直没有被发现。而当发现bug时,让它们得到修复又是另一项任务,涉及到一个漫长而昂贵的过程,不一定能得到正确的修复。因此,bug 检测变得不可靠,鉴于这些局限性,bug 预防是更好的方法。

image.png

Rust 及其优势

2020 年底,Rust 官方在调研了全球 8323 位开发者后,最新发布了 2020 年 Rust 调查报告。通过报告,我们发现 Rust 开发者的用户黏度较高,有 83% 的受访者表示他们一直在使用该语言。

众所周知,当提及 Rust 语言时,安全、性能、并发等特性是其优势。Rust 通过使用编译时检查和运行时检查相结合的方式提供内存安全保证,以强制执行对象的寿命/所有权,并确保内存访问是有效的。在实现这种安全性的同时,还能提供与C和C++相当的性能。Rust 还减少了对沙盒的需求,让开发人员有更多的开销空间来引入更安全、更轻量的新功能。

但是据官方调查报告显示,Rust 仍有许多亟需完善之处。譬如以下几点:

  • 与 C++ 的交互兼容需要进一步提升
  • Rust 上手难度太高
  • 编译时间过长
  • 库的支持不够丰富

虽然 Rust 确实有它的好处,但一夜之间将整个 Android 操作系统换成 Rust 是不可行的,并且现阶段的 Rust 也远非尽善尽美。而且可能根本不需要这样做,因为大多数 Android 的内存错误都发生在新的或最近修改的代码中,大约有 50% 的代码是不到一年的。谷歌认为,其内存安全语言的工作最好集中在新的开发上,而不是重写成熟的 C 和 C++ 代码。

据 Android 开发人员关系小组的成员称,Google 目前还不打算发布 Rust NDK。应用开发支持的语言将继续是 Kotlin,Java,C 和 C ++。

部分参考资料:

xda-developers:《Google is developing parts of Android in Rust to improve security》
Rust 官方:《Rust 语言 2020 调查报告》

segmentfault 思否

查看原文

赞 5 收藏 2 评论 0

高阳Sunny 赞了文章 · 4月11日

浅探 Web Worker 与 JavaScript 沙箱

一些「炒冷饭」的背景介绍

本文并不会从头开始介绍 Web Worker 的基础知识和基本 API 的使用等(只是部分有涉及),若还未了解过 Web Worker,可参考查阅 W3C 标准 Workers 文档 中的相关介绍。

自从 2014 年 HTML5 正式推荐标准发布以来,HTML5 增加了越来越多强大的特性和功能,而在这其中,工作线程(Web Worker)概念的推出让人眼前一亮,但未曾随之激起多大的浪花,并被在其随后工程侧的 Angular、Vue、React 等框架的「革命」浪潮所淹没。当然,我们总会偶然看过一些文章介绍,或出于学习的目的做过一些应用场景下的练习,甚或在实际项目中的涉及大量数据计算场景中真的使用过。但相信也有很多人和我一样茫然,找不到这种高大上的技术在实际项目场景中能有哪些能起到广泛作用的应用。

究其原因,Web Worker 独立于 UI 主线程运行的特性使其被大量考虑进行性能优化方面的尝试(比如一些图像分析、3D 计算绘制等场景),以保证在进行大量计算的同时,页面对用户能有及时的响应。而这些性能优化的需求在前端侧一方面涉及频率低,另一方面也能通过微任务或服务端侧处理来解决,它并不能像 Web Socket 这种技术为前端页面下的轮询场景的优化能带来质的改变。

直至 2019 年爆火的微前端架构的出现,基于微应用间 JavaScript 沙箱隔离的需求,Web Worker 才得以重新从边缘化的位置跃入到我的中心视野。根据我已经了解到的 Web Worker 的相关知识,我知道了 Web Worker 是工作在一个独立子线程下(虽然这个子线程比起 Java 等编译型语言的子线程实现得还有点弱,如无法加锁等),线程之间自带隔离的特性,那基于这种「物理」性的隔离,能不能实现 JavaScript 运行时的隔离呢?

本文接下来的内容,将介绍我在探索基于 Web Worker 实现 JavaScript 沙箱隔离方案过程中的一些资料收集、理解以及我的踩坑和思考的过程。虽然可能整篇文章内容都在「炒冷饭」,但还是希望我的探索方案的过程能对正在看这篇文章的你有所帮助。

JavaScript 沙箱

在探索基于 Web Worker 的解决方案之前,我们先要对当前要解决的问题——JavaScript 沙箱有所了解。

提到沙箱,我会先想到出于兴趣玩过的沙盒游戏,但我们要探索的 JavaScript 沙箱不同于沙盒游戏,沙盒游戏注重对世界基本元素的抽象、组合以及物理力系统的实现等,而 JavaScript 沙箱则更注重在使用共享数据时对操作状态的隔离。

在现实与 JavaScript 相关的场景中,我们知道平时使用的浏览器就是一个沙箱,运行在浏览器中的 JavaScript 代码无法直接访问文件系统、显示器或其他任何硬件。Chrome 浏览器中每个标签页也是一个沙箱,各个标签页内的数据无法直接相互影响,接口都在独立的上下文中运行。而在同一个浏览器标签页下运行 HTML 页面,有哪些更细节的、对沙箱现象有需求的场景呢?

当我们作为前端开发人员较长一段时间后,我们很轻易地就能想到在同一个页面下,使用沙箱需求的诸多应用场景,譬如:

  1. 执行从不受信的源获取到的第三方 JavaScript 代码时(比如引入插件、处理 jsonp 请求回来的数据等)。

  2. 在线代码编辑器场景(比如著名的 codesandbox)。

  3. 使用服务端渲染方案。

  4. 模板字符串中的表达式的计算。

  5. ... ...

这里我们先回到开头,先将前提假设在我正在面对的微前端架构设计下。在微前端架构(推荐文章 Thinking in Microfrontend拥抱云时代的前端开发架构——微前端 等)中,其最关键的一个设计便是各个子应用间的调度实现以及其运行态的维护,而运行时各子应用使用全局事件监听、使全局 CSS 样式生效等常见的需求在多个子应用切换时便会成为一种污染性的副作用,为了解决这些副作用,后来出现的很多微前端架构(如 乾坤)有着各种各样的实现。譬如 CSS 隔离中常见的命名空间前缀、Shadow DOM、 乾坤 sandbox css 的运行时动态增删等,都有着确实行之有效的具体实践,而这里最麻烦棘手的,还是微应用间的 JavaScript 的沙箱隔离。

在微前端架构中,JavaScript 沙箱隔离需要解决如下几个问题:

  1. 挂在 window 上的全局方法/变量(如 setTimeout、滚动等全局事件监听等)在子应用切换时的清理和还原。

  2. Cookie、LocalStorage 等的读写安全策略限制。

  3. 各子应用独立路由的实现。

  4. 多个微应用共存时相互独立的实现。

乾坤 架构设计中,关于沙箱有两个入口文件需要关注,一个是 proxySandbox.ts,另一个是 snapshotSandbox.ts,他们分别基于 Proxy 实现代理了 window 上常用的常量和方法以及不支持 Proxy 时降级通过快照实现备份还原。结合其相关开源文章分享,简单总结下其实现思路:起初版本使用了快照沙箱的概念,模拟 ES6 的 Proxy API,通过代理劫持 window ,当子应用修改或使用 window 上的属性或方法时,把对应的操作记录下来,每次子应用挂载/卸载时生成快照,当再次从外部切换到当前子应用时,再从记录的快照中恢复,而后来为了兼容多个子应用共存的情况,又基于 Proxy 实现了代理所有全局性的常量和方法接口,为每个子应用构造了独立的运行环境。

另外一种值得借鉴的思路是阿里云开发平台的 Browser VM,其核心入口逻辑在 Context.js 文件中。它的具体实现思路是这样的:

  1. 借鉴 with 的实现效果,在 webpack 编译打包阶段为每个子应用代码包裹一层代码(见其插件包 breezr-plugin-os 下相关文件),创建一个闭包,传入自己模拟的 window、document、location、history 等全局对象(见 根目录下 相关文件)。

  2. 在模拟的 Context 中,new 一个 iframe 对象,提供一个和宿主应用空的(about:blank) 同域 URL 来作为这个 iframe 初始加载的 URL(空的 URL 不会发生资源加载,但是会产生和这个 iframe 中关联的 history 不能被操作的问题,这时路由的变换只支持 hash 模式),然后将其下的原生浏览器对象通过 contentWindow 取出来(因为 iframe 对象天然隔离,这里省去了自己 Mock 实现所有 API 的成本)。

  3. 取出对应的 iframe 中原生的对象之后,继续对特定需要隔离的对象生成对应的 Proxy,然后对一些属性获取和属性设置,做一些特定的实现(比如 window.document 需要返回特定的沙箱 document 而不是当前浏览器的document 等)。

  4. 为了文档内容能够被加载在同一个 DOM 树上,对于 document,大部分的 DOM 操作的属性和方法仍旧直接使用宿主浏览器中的 document 的属性和方法处理等。

总的来说,在 Browser VM 的实现中, 可以看出其实现部分还是借鉴了 乾坤 或者说其他微前端架构的思路,比如常见全局对象的代理和拦截。并且借助 Proxy 特性,针对 Cookie、LocalStorage 的读写同样能做一些安全策略的实现等。但其最大的亮点还是借助 iframe 做了一些取巧的实现,当这个为每个子应用创建的 iframe 被移除时,写在其下 window 上的变量和 setTimeout、全局事件监听等也会一并被移除;另外基于 Proxy,DOM 事件在沙箱中做记录,然后在宿主中生命周期中实现移除,能够以较小的开发成本实现整个 JavaScript 沙箱隔离的机制。

除了以上社区中现在比较火的方案,最近我也在 大型 Web 应用插件化架构探索 一文中了解到了 UI 设计领域的 Figma 产品也基于其插件系统产出了一种隔离方案。起初 Figma 同样是将插件代码放入 iframe 中执行并通过 postMessage 与主线程通信,但由于易用性以及 postMessage 序列化带来的性能等问题,Figma 选择还是将插件放入主线程去执行。Figma 采用的方案是基于目前还在草案阶段 Realm API,并将 JavaScript 解释器的一种 C++ 实现 Duktape 编译到了 WebAssembly,然后将其嵌入到 Realm 上下文中,实现了其产品下的三方插件的独立运行。这种方案和探索的基于 Web Worker 的实现可能能够结合得更好,持续关注中。

Web Worker 与 DOM 渲染

在了解了 JavaScript 沙箱的「前世今生」之后,我们将目光投回本文的主角——Web Worker 身上。

正如本文开头所说,Web Worker 子线程的形式也是一种天然的沙箱隔离,理想的方式,是借鉴 Browser VM 的前段思路,在编译阶段通过 Webpack 插件为每个子应用包裹一层创建 Worker 对象的代码,让子应用运行在其对应的单个 Worker 实例中,比如:

__WRAP_WORKER__(`/* 打包代码 */ }`);
​
function __WRAP_WORKER__(appCode) {
 var blob = new Blob([appCode]);
 var appWorker = new Worker(window.URL.createObjectURL(blob));
} 

但在了解过微前端下 JavaScript 沙箱的实现过程后,我们不难发现几个在 Web Worker 下去实现微前端场景的 JavaScript 沙箱必然会遇到的几个难题:

  1. 出于线程安全设计考虑,Web Worker 不支持 DOM 操作,必须通过 postMessage 通知 UI 主线程来实现。

  2. Web Worker 无法访问 window、document 之类的浏览器全局对象。

其他诸如 Web Worker 无法访问页面全局变量和函数、无法调用 alert、confirm 等 BOM API 等问题,相对于无法访问 window、document 全局对象已经是小问题了。不过可喜的是,Web Worker 中可以正常使用 setTimeout、setInterval 等定时器函数,也仍能发送 ajax 请求。

所以,当先要解决问题,便是在单个 Web Worker 实例中执行 DOM 操作的问题了。首先我们有一个大前提:Web Worker 中无法渲染 DOM,所以,我们需要基于实际的应用场景,将 DOM 操作进行拆分。

React Worker DOM

因为我们微前端架构中的子应用局限在 React 技术栈下,我先将目光放在了基于 React 框架的解决方案上。

在 React 中,我们知道其将渲染阶段分为对 DOM 树的改变进行 Diff 和实际渲染改变页面 DOM 两个阶段这一基本事实,那能不能将 Diff 过程置于 Web Worker 中,再将渲染阶段通过 postMessage 与主线程进行通信后放在主线程进行呢?简单一搜,颇为汗颜,已经有大佬在 5、6 年前就有尝试了。这里我们可以参考下 react-worker-dom 的开源代码。

react-worker-dom 中的实现思路很清晰。其在 common/channel.js 中统一封装了子线程和主线程互相通信的接口和序列化通信数据的接口,然后我们可以看到其在 Worker 下实现 DOM 逻辑处理的总入口文件在 worker 目录下,从该入口文件顺藤摸瓜,可以看到其实现了计算 DOM 后通过 postMessage 通知主线程进行渲染的入口文件 WorkerBridge.js 以及其他基于 React 库实现的 DOM 构造、Diff 操作、生命周期 Mock 接口等相关代码,而接受渲染事件通信的入口文件在 page 目录下,该入口文件接受 node 操作事件后再结合 WorkerDomNodeImpl.js 中的接口代码实现了 DOM 在主线程的实际渲染更新。

简单做下总结。基于 React 技术栈,通过在 Web Worker 下实现 Diff 与渲染阶段的进行分离,可以做到一定程度的 DOM 沙箱,但这不是我们想要的微前端架构下的 JavaScript 沙箱。先不谈拆分 Diff 阶段与渲染阶段的成本与收益比,首先,基于技术栈框架的特殊性所做的这诸多努力,会随着这个框架本身版本的升级存在着维护升级难以掌控的问题;其次,假如各个子应用使用的技术栈框架不同,要为这些不同的框架分别封装适配的接口,扩展性和普适性弱;最后,最为重要的一点,这种方法暂时还是没有解决 window 下资源共享的问题,或者说,只是启动了解决这个问题的第一步。

接下来,我们先继续探讨 Worker 下实现 DOM 操作的另外一种方案。window 下资源共享的问题我们放在其后再作讨论。

AMP WorkerDOM

在我开始纠结于如 react-worker-dom 这种思路实际落地开发的诸多「天堑」问题的同时,浏览过其他 DOM 框架因为同样具备插件机制偶然迸进了我的脑海,它是 Google 的 AMP

AMP 开源项目 中除了如 amphtml 这种通用的 Web 组件框架,还有很多其他工程采用了 Shadow DOM、Web Component 等新技术,在项目下简单刷了一眼后,我欣喜地看到了工程 worker-dom

粗略翻看下 worker-dom 源码,我们在 src 根目录下可以看到 main-threadworker-thread 两个目录,分别打开看了下后,可以发现其实现拆分 DOM 相关逻辑和 DOM 渲染的思路和上面的 react-worker-dom 基本类似,但 worker-dom 因为和上层框架无关,其下的实现更为贴近 DOM 底层。

先看 worker-thread DOM 逻辑层的相关代码,可以看到其下的 dom 目录 下实现了基于 DOM 标准的所有相关的节点元素、属性接口、document 对象等代码,上一层目录中也实现了 Canvas、CSS、事件、Storage 等全局属性和方法。

接着看 main-thread,其关键功能一方面是提供加载 worker 文件从主线程渲染页面的接口,另一方面可以从 worker.tsnodes.ts 两个文件的代码来理解。

worker.ts 中像我最初所设想的那样包裹了一层代码,用于自动生成 Worker 对象,并将代码中的所有 DOM 操作都代理到模拟的 WorkerDOM 对象上:

const code = `
      'use strict';
      (function(){
        ${workerDOMScript}
        self['window'] = self;
        var workerDOM = WorkerThread.workerDOM;
        WorkerThread.hydrate(
          workerDOM.document,
          ${JSON.stringify(strings)},
          ${JSON.stringify(skeleton)},
          ${JSON.stringify(cssKeys)},
          ${JSON.stringify(globalEventHandlerKeys)},
          [${window.innerWidth}, ${window.innerHeight}],
          ${JSON.stringify(localStorageInit)},
          ${JSON.stringify(sessionStorageInit)}
        );
        workerDOM.document[${TransferrableKeys.observe}](this);
        Object.keys(workerDOM).forEach(function(k){self[k]=workerDOM[k]});
}).call(self);
${authorScript}
//# sourceURL=${encodeURI(config.authorURL)}`;
this[TransferrableKeys.worker] = new Worker(URL.createObjectURL(new Blob([code])));

nodes.ts 中,实现了真实元素节点的构造和存储(基于存储数据结构是否以及如何在渲染阶段有优化还需进一步研究源码)。

同时,在 transfer 目录下的源码,定义了逻辑层和 UI 渲染层的消息通信的规范。

总的来看,AMP WorkerDOM 的方案抛弃了上层框架的约束,通过从底层构造了 DOM 所有相关 API 的方式,真正做到了与框架技术栈无关。它一方面完全可以作为上层框架的底层实现,来支持各种上层框架的二次封装迁移(如工程 amp-react-prototype),另一方面结合了当前主流 JavaScript 沙箱方案,通过模拟 window、document 全局方法的并代理到主线程的方式实现了部分的 JavaScript 沙箱隔离(暂时没看到路由隔离的相关代码实现)。

当然,从我个人角度来看,AMP WorkerDOM 也有其当前在落地上一定的局限性。一个是对当前主流上层框架如 Vue、React 等的迁移成本及社区生态的适配成本,另一个是其在单页应用下的尚未看到有相关实现方案,在大型 PC 微前端应用的支持上还无法找到更优方案。

其实,在了解完 AMP WorkerDOM 的实现方案之后,基于 react-worker-dom 思路的后续方案也可以有个大概方向了:渲染通信的后续过程,可考虑结合 Browser VM 的相关实现,在生成 Worker 对象的同时,也生成一个 iframe 对象,然后将 DOM 下的操作都通过 postMessage 发送到主线程后,以与其绑定的 iframe 兑现来执行,同时,通过代理将具体的渲染实现再转发给原 WorkerDomNodeImpl.js 逻辑来实现 DOM 的实际更新。

小结与一些个人前瞻

首先聊一聊个人的一些总结。Web Worker 下实现微前端架构下的 JavaScript 沙箱最初是出于一点个人灵光的闪现,在深入调研后,虽然最终还是因为这样那样的问题导致在方案落地上无法找到最优解从而放弃采用社区通用方案,但仍不妨碍我个人对 Web Worker 技术在实现插件类沙箱应用上的持续看好。插件机制在前端领域一直是津津乐道的一种设计,从 Webpack 编译工具到 IDE 开发工具,从 Web 应用级的实体插件到应用架构设计中插件扩展设计,结合 WebAssembly 技术,Web Worker 无疑将在插件设计上占据举足轻重的地位。

其次是一些个人的一些前瞻思考。其实从 Web Worker 实现 DOM 渲染的调研过程中可以看到,基于逻辑与 UI 分离的思路,前端后续的架构设计有很大机会能够产生一定的变革。目前不管是盛行的 Vue 还是 React 框架,其框架设计不论是 MVVM 还是结合 Redux 之后的 Flux,其本质上仍旧还是由 View 层驱动的框架设计(个人浅见),其具备灵活性的同时也产生着性能优化、大规模项目层级升上后的协作开发困难等问题,而基于 Web Worker 的逻辑与 UI 分离,将促使数据获取、处理、消费整个流程的进一步的业务分层,从而固化出一整套的 MVX 设计思路。

当然,以上这些我个人还处于初步调研的阶段,不成熟之处还需多加琢磨。且听之,后续再实践之。

作者:ES2049 / 靳志凯
文章可随意转载,但请保留此原文链接。
非常欢迎有激情的你加入 ES2049 Studio,简历请发送至 caijun.hcj@alibaba-inc.com
查看原文

赞 13 收藏 8 评论 0

高阳Sunny 赞了文章 · 4月11日

JavaScript Map –如何使用JS.map()函数

有时,你可能需要获取一个数组并将一些操作应用于其子项,以便获得具有已修改元素的新数组。

无需使用循环手动遍历数组,你可以简单地使用内置Array.map()方法。

Array.map()方法允许你遍历数组并使用回调函数修改其元素。然后,将对数组的每个元素执行回调函数。

例如,假设你具有以下数组元素:

let arr = [3, 4, 5, 6];

现在,假设你需要将数组的每个元素乘以3。你可以考虑for如下使用循环:

let arr = [3, 4, 5, 6];

for (let i = 0; i < arr.length; i++){
  arr[i] = arr[i] * 3;
}

console.log(arr); // [9, 12, 15, 18]

但是实际上可以使用该Array.map()方法来达到相同的结果。这是一个例子:

let arr = [3, 4, 5, 6];

let modifiedArr = arr.map(function(element){
    return element *3;
});

console.log(modifiedArr); // [9, 12, 15, 18]

Array.map()方法通常用于对元素进行一些更改,无论是乘以上面的代码中的特定数字,还是进行应用程序可能需要的任何其他操作。

如何在对象数组上使用map()

例如,您可能有一个对象数组,这些对象存储firstName和存储lastName您的朋友的值,如下所示:

let users = [
  {firstName : "Susan", lastName: "Steward"},
  {firstName : "Daniel", lastName: "Longbottom"},
  {firstName : "Jacob", lastName: "Black"}
]; 

您可以使用map()在阵列上的方法来迭代和加入的价值观 firstNamelastName如下:

let users = [
  {firstName : "Susan", lastName: "Steward"},
  {firstName : "Daniel", lastName: "Longbottom"},
  {firstName : "Jacob", lastName: "Black"}
];

let userFullnames = users.map(function(element){
    return `${element.firstName} ${element.lastName}`;
})

console.log(userFullnames);
// ["Susan Steward", "Daniel Longbottom", "Jacob Black"]

map()方法传递的不仅仅是一个元素。让我们看看传递map()给回调函数的所有参数。

完整的map()方法语法

map()方法的语法如下:

arr.map(function(element, index, array){  }, this);

function()在每个数组元素上调用该回调,并且该map()方法始终将current elementindex当前元素的of和整个array对象传递给它。

this参数将在回调函数中使用。默认情况下,其值为undefined。例如,下面是将this值更改为数字的方法80

let arr = [2, 3, 5, 7]

arr.map(function(element, index, array){
    console.log(this) // 80
}, 80);

console.log()如果你有兴趣,还可以使用测试其他参数:

let arr = [2, 3, 5, 7]

arr.map(function(element, index, array){
    console.log(element);
    console.log(index);
    console.log(array);
    return element;
}, 80);

这就是您需要了解的所有Array.map()方法。通常,您只会element在回调函数中使用参数,而忽略其余参数。这就是我通常在日常项目中所做的事情:)

查看原文

赞 5 收藏 3 评论 0

高阳Sunny 赞了文章 · 4月9日

1W字|40 图|硬核 ES 实战

前言

上篇我们讲到了 Elasticsearch 全文检索的原理《插入文章》,通过在本地搭建一套 ES 服务,以多个案例来分析了 ES 的原理以及基础使用。这次我们来讲下 Spring Boot 中如何整合 ES,以及如何在 Spring Cloud 微服务项目中使用 ES 来实现全文检索。

通过本实战您可以学到如下知识点:

  • Spring Boot 如何整合 ES。
  • 微服务中 ES 的 API 使用。
  • 项目中如何使用 ES 来达到全文检索。

本篇主要内容如下:

主要内容

本文案例都是基于 PassJava 实战项目来演示的。

:+1:Github 地址:https://github.com/Jackson071...

为了让大家更清晰地理解 PassJava 项目中 ES 是如何使用的,我画了三个流程图:

  • 第一步:创建 question 索引。

首先定义 question 索引,然后在 ES 中创建索引。

  • 第二步:存 question 数据进 ES 。

前端保存数据时,保存的 API 请求先进过网关,然后转发到 passjava-question 微服务,然后远程调用 passjava-search 微服务,将数据保存进 ES 中。

  • 第三步:从 ES 中查数据。

前端查询数据时,先经过网关,然后将请求转发给 passjava-search 微服务,然后从 ES 中查询数据。

一、Elasticsearch 组件库介绍

在讲解之前,我在这里再次提下全文检索是什么:

全文检索: 指以全部文本信息作为检索对象的一种信息检索技术。而我们使用的数据库,如 Mysql,MongoDB 对文本信息检索能力特别是中文检索并没有 ES 强大。所以我们来看下 ES 在项目中是如何来代替 SQL 来工作的。

我使用的 Elasticsearch 服务是 7.4.2 的版本,然后采用官方提供的 Elastiscsearch-Rest-Client 库来操作 ES,而且官方库的 API 上手简单。

该组件库的官方文档地址:

https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html

另外这个组件库是支持多种语言的:

支持多语言

注意:Elasticsearch Clients 就是指如何用 API 操作 ES 服务的组件库。

可能有同学会提问,Elasticsearch 的组件库中写着 JavaScript API,是不是可以直接在前端访问 ES 服务?可以是可以,但是会暴露 ES 服务的端口和 IP 地址,会非常不安全。所以我们还是用后端服务来访问 ES 服务。

我们这个项目是 Java 项目,自然就是用上面的两种:Java Rest Client 或者 Java API。我们先看下 Java API,但是会发现已经废弃了。如下图所示:

Java API 已经废弃了

所以我们只能用 Java REST Client 了。而它又分成两种:高级和低级的。高级包含更多的功能,如果把高级比作MyBatis的话,那么低级就相当于JDBC。所以我们用高级的 Client。

高级和低级 Client

二、整合检索服务

我们把检索服务单独作为一个服务。就称作 passjava-search 模块吧。

1.1 添加搜索服务模块

  • 创建 passjava-search 模块。

首先我们在 PassJava-Platform 模块创建一个 搜索服务模块 passjava-search。然后勾选 spring web 服务。如下图所示。

第一步:选择 Spring Initializr,然后点击 Next。

选择 Spring Initializr

第二步:填写模块信息,然后点击 Next。

passjava-search 服务模块

第三步:选择 Web->Spring Web 依赖,然后点击 Next。

mark

1.2 配置 Maven 依赖

  • 参照 ES 官网配置。

进入到 ES 官方网站,可以看到有低级和高级的 Rest Client,我们选择高阶的(High Level Rest Client)。然后进入到高阶 Rest Client 的 Maven 仓库。官网地址如下所示:

https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.9/index.html

Rest Client 官方文档

  • 加上 Maven 依赖。

    对应文件路径:\passjava-search\pom.xml

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>7.4.2</version>
</dependency>
  • 配置 elasticsearch 的版本为7.4.2

    因加上 Maven 依赖后,elasticsearch 版本为 7.6.2,所以遇到这种版本不一致的情况时,需要手动改掉。

    对应文件路径:\passjava-search\pom.xml

<properties>
    <elasticsearch.version>7.4.2</elasticsearch.version>
</properties>

刷新 Maven Project 后,可以看到引入的 elasticsearch 都是 7.4.2 版本了,如下图所示:

设置版本为 7.4.2

  • 引入 PassJava 的 Common 模块依赖。

    Common 模块是 PassJava 项目独立的出来的公共模块,引入了很多公共组件依赖,其他模块引入 Common 模块依赖后,就不需要单独引入这些公共组件了,非常方便。

    对应文件路径:\passjava-search\pom.xml

 <dependency>
     <groupId>com.jackson0714.passjava</groupId>
     <artifactId>passjava-common</artifactId>
     <version>0.0.1-SNAPSHOT</version>
</dependency>

添加完依赖后,我们就可以将搜索服务注册到 Nacos 注册中心了。 Nacos 注册中心的用法在前面几篇文章中也详细讲解过,这里需要注意的是要先启动 Nacos 注册中心,才能正常注册 passjava-search 服务。

1.3 注册搜索服务到注册中心

修改配置文件:src/main/resources/application.properties。配置应用程序名、注册中心地址、注册中心的命名中间。

spring.application.name=passjava-search
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=passjava-search

启动类添加服务发现注解:@EnableDiscoveryClient。这样 passjava-search 服务就可以被注册中心发现了。

因 Common 模块依赖数据源,但 search 模块不依赖数据源,所以 search 模块需要移除数据源依赖:

exclude = DataSourceAutoConfiguration.class

以上的两个注解如下所示:

@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class PassjavaSearchApplication {
    public static void main(String[] args) {
        SpringApplication.run(PassjavaSearchApplication.class, args);
    }
}

接下来我们添加一个 ES 服务的专属配置类,主要目的是自动加载一个 ES Client 来供后续 ES API 使用,不用每次都 new 一个 ES Client。

1.4 添加 ES 配置类

配置类:PassJavaElasticsearchConfig.java

核心方法就是 RestClient.builder 方法,设置好 ES 服务的 IP 地址、端口号、传输协议就可以了。最后自动加载了 RestHighLevelClient。

package com.jackson0714.passjava.search.config;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Author: 公众号 | 悟空聊架构
 * @Date: 2020/10/8 17:02
 * @Site: www.passjava.cn
 * @Github: https://github.com/Jackson0714/PassJava-Platform
 */
@Configuration
public class PassJavaElasticsearchConfig {

    @Bean
    // 给容器注册一个 RestHighLevelClient,用来操作 ES
    // 参考官方文档:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.9/java-rest-high-getting-started-initialization.html
    public RestHighLevelClient restHighLevelClient() {
        return new RestHighLevelClient(
                RestClient.builder(
                        new HttpHost("192.168.56.10", 9200, "http")));
    }
}

接下来我们测试下 ES Client 是否自动加载成功。

1.5 测试 ES Client 自动加载

在测试类 PassjavaSearchApplicationTests 中编写测试方法,打印出自动加载的 ES Client。期望结果是一个 RestHighLevelClient 对象。

package com.jackson0714.passjava.search;

import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class PassjavaSearchApplicationTests {

    @Qualifier("restHighLevelClient")
    @Autowired
    private RestHighLevelClient client;

    @Test
    public void contextLoads() {
        System.out.println(client);
    }
}

运行结果如下所示,打印出了 RestHighLevelClient。说明自定义的 ES Client 自动装载成功。

ES 测试结果

1.6 测试 ES 简单插入数据

测试方法 testIndexData,省略 User 类。users 索引在我的 ES 中是没有记录的,所以期望结果是 ES 中新增了一条 users 数据。

/**
 * 测试存储数据到 ES。
 * */
@Test
public void testIndexData() throws IOException {
    IndexRequest request = new IndexRequest("users");
    request.id("1"); // 文档的 id
    
    //构造 User 对象
    User user = new User();
    user.setUserName("PassJava");
    user.setAge("18");
    user.setGender("Man");
    
    //User 对象转为 JSON 数据
    String jsonString = JSON.toJSONString(user);
    
    // JSON 数据放入 request 中
    request.source(jsonString, XContentType.JSON);

    // 执行插入操作
    IndexResponse response = client.index(request, RequestOptions.DEFAULT);

    System.out.println(response);
}

执行 test 方法,我们可以看到控制台输出以下结果,说明数据插入到 ES 成功。另外需要注意的是结果中的 result 字段为 updated,是因为我本地为了截图,多执行了几次插入操作,但因为 id = 1,所以做的都是 updated 操作,而不是 created 操作。

控制台输出结果

我们再来到 ES 中看下 users 索引中数据。查询 users 索引:

GET users/_search

结果如下所示:

查询 users 索引结果

可以从图中看到有一条记录被查询出来,查询出来的数据的 _id = 1,和插入的文档 id 一致。另外几个字段的值也是一致的。说明插入的数据没有问题。

"age" : "18",
"gender" : "Man",
"userName" : "PassJava"

1.7 测试 ES 查询复杂语句

示例:搜索 bank 索引,address 字段中包含 big 的所有人的年龄分布 ( 前 10 条 ) 以及平均年龄,以及平均薪资。

1.7.1 构造检索条件

我们可以参照官方文档给出的示例来创建一个 SearchRequest 对象,指定要查询的索引为 bank,然后创建一个 SearchSourceBuilder 来组装查询条件。总共有三种条件需要组装:

  • address 中包含 road 的所有人。
  • 按照年龄分布进行聚合。
  • 计算平均薪资。

代码如下所示,需要源码请到我的 Github/PassJava 上下载。

查询复杂语句示例

将打印出来的检索参数复制出来,然后放到 JSON 格式化工具中格式化一下,再粘贴到 ES 控制台执行,发现执行结果是正确的。

打印出检索参数

用在线工具格式化 JSON 字符串,结果如下所示:

然后我们去掉其中的一些默认参数,最后简化后的检索参数放到 Kibana 中执行。

Kibana Dev Tools 控制台中执行检索语句如下图所示,检索结果如下图所示:

控制台中执行检索语句

找到总记录数:29 条。

第一条命中记录的详情如下:

平均 balance:13136。

平均年龄:26。

地址中包含 Road 的:263 Aviation Road。

和 IDEA 中执行的测试结果一致,说明复杂检索的功能已经成功实现。

17.2 获取命中记录的详情

而获取命中记录的详情数据,则需要通过两次 getHists() 方法拿到,如下所示:

// 3.1)获取查到的数据。
SearchHits hits = response.getHits();
// 3.2)获取真正命中的结果
SearchHit[] searchHits = hits.getHits();

我们可以通过遍历 searchHits 的方式打印出所有命中结果的详情。

// 3.3)、遍历命中结果
for (SearchHit hit: searchHits) {
    String hitStr = hit.getSourceAsString();
    BankMember bankMember = JSON.parseObject(hitStr, BankMember.class);
}

拿到每条记录的 hitStr 是个 JSON 数据,如下所示:

{
    "account_number": 431,
    "balance": 13136,
    "firstname": "Laurie",
    "lastname": "Shaw",
    "age": 26,
    "gender": "F",
    "address": "263 Aviation Road",
    "employer": "Zillanet",
    "email": "laurieshaw@zillanet.com",
    "city": "Harmon",
    "state": "WV"
}

而 BankMember 是根据返回的结果详情定义的的 JavaBean。可以通过工具自动生成。在线生成 JavaBean 的网站如下:

https://www.bejson.com/json2javapojo/new/

把这个 JavaBean 加到 PassjavaSearchApplicationTests 类中:

@ToString
@Data
static class BankMember {
    private int account_number;
    private int balance;
    private String firstname;
    private String lastname;
    private int age;
    private String gender;
    private String address;
    private String employer;
    private String email;
    private String city;
    private String state;
}

然后将 bankMember 打印出来:

System.out.println(bankMember);

bankMember

得到的结果确实是我们封装的 BankMember 对象,而且里面的属性值也都拿到了。

1.7.3 获取年龄分布聚合信息

ES 返回的 response 中,年龄分布的数据是按照 ES 的格式返回的,如果想按照我们自己的格式来返回,就需要将 response 进行处理。

如下图所示,这个是查询到的年龄分布结果,我们需要将其中某些字段取出来,比如 buckets,它代表了分布在 21 岁的有 4 个。

ES 返回的年龄分布信息

下面是代码实现:

Aggregations aggregations = response.getAggregations();
Terms ageAgg1 = aggregations.get("ageAgg");
for (Terms.Bucket bucket : ageAgg1.getBuckets()) {
    String keyAsString = bucket.getKeyAsString();
    System.out.println("用户年龄: " + keyAsString + " 人数:" + bucket.getDocCount());
}

最后打印的结果如下,21 岁的有 4 人,26 岁的有 4 人,等等。

打印结果:用户年龄分布

1.7.4 获取平均薪资聚合信息

现在来看看平均薪资如何按照所需的格式返回,ES 返回的结果如下图所示,我们需要获取 balanceAvg 字段的 value 值。

ES 返回的平均薪资信息

代码实现:

Avg balanceAvg1 = aggregations.get("balanceAvg");
System.out.println("平均薪资:" + balanceAvg1.getValue());

打印结果如下,平均薪资 28578 元。

打印结果:平均薪资

三、实战:同步 ES 数据

3.1 定义检索模型

PassJava 这个项目可以用来配置题库,如果我们想通过关键字来搜索题库,该怎么做呢?

类似于百度搜索,输入几个关键字就可以搜到关联的结果,我们这个功能也是类似,通过 Elasticsearch 做检索引擎,后台管理界面和小程序作为搜索入口,只需要在小程序上输入关键字,就可以检索相关的题目和答案。

首先我们需要把题目和答案保存到 ES 中,在存之前,第一步是定义索引的模型,如下所示,模型中有 titleanswer 字段,表示题目和答案。

"id": {
    "type": "long"
},
"title": {
    "type": "text",
    "analyzer": "ik_smart"
},
"answer": {
    "type": "text",
    "analyzer": "ik_smart"
},
"typeName": {
    "type": "keyword"
}

3.2 在 ES 中创建索引

上面我们已经定义了索引结构,接着就是在 ES 中创建索引。

在 Kibana 控制台中执行以下语句:

PUT question
{
    "mappings" : {
        "properties": {
              "id": {
                  "type": "long"
              },
              "title": {
                  "type": "text",
                  "analyzer": "ik_smart"
              },
              "answer": {
                  "type": "text",
                  "analyzer": "ik_smart"
              },
              "typeName": {
                  "type": "keyword"
              }
        }
  }
}

执行结果如下所示:

创建 question 索引

我们可以通过以下命令来查看 question 索引是否在 ES 中:

GET _cat/indices

执行结果如下图所示:

查看 ES 中所有的索引

3.3 定义 ES model

上面我们定义 ES 的索引,接着就是定义索引对应的模型,将数据存到这个模型中,然后再存到 ES 中。

ES 模型如下,共四个字段:id、title、answer、typeName。和 ES 索引是相互对应的。

@Data
public class QuestionEsModel {
    private Long id;
    private String title;
    private String answer;
    private String typeName;
}

3.4 触发保存的时机

当我们在后台创建题目或保存题目时,先将数据保存到 mysql 数据库,然后再保存到 ES 中。

如下图所示,在管理后台创建题目时,触发保存数据到 ES 。

mark

第一步,保存数据到 mysql 中,项目中已经包含此功能,就不再讲解了,直接进入第二步:保存数据到 ES 中。

而保存数据到 ES 中,需要将数据组装成 ES 索引对应的数据,所以我用了一个 ES model,先将数据保存到 ES model 中。

3.5 用 model 来组装数据

这里的关键代码时 copyProperties,可以将 question 对象的数据取出,然后赋值到 ES model 中。不过 ES model 中还有些字段是 question 中没有的,所以需要单独拎出来赋值,比如 typeName 字段,question 对象中没有这个字段,它对应的字段是 question.type,所以我们把 type 取出来赋值到 ES model 的 typeName 字段上。如下图所示:

用 model 来组装数据

3.6 保存数据到 ES

我在 passjava-search 微服务中写了一个保存题目的 api 用来保存数据到 ES 中。

保存数据到 ES

然后在 passjava-question 微服务中调用 search 微服务的保存 ES 的方法就可以了。

// 调用 passjava-search 服务,将数据发送到 ES 中保存。
searchFeignService.saveQuestion(esModel);

3.7 检验 ES 中是否创建成功

我们可以通过 kibana 的控制台来查看 question 索引中的文档。通过以下命令来查看:

GET question/_search

执行结果如下图所示,有一条记录:

mark

另外大家有没有疑问:可以重复更新题目吗?

答案是可以的,保存到 ES 的数据是幂等的,因为保存的时候带了一个类似数据库主键的 id。

四、实战:查询 ES 数据

我们已经将数据同步到了 ES 中,现在就是前端怎么去查询 ES 数据中,这里我们还是使用 Postman 来模拟前端查询请求。

4.1 定义请求参数

请求参数我定义了三个:

  • keyword:用来匹配问题或者答案。
  • id:用来匹配题目 id。
  • pageNum:用来分页查询数据。

这里我将这三个参数定义为一个类:

@Data
public class SearchParam {
    private String keyword; // 全文匹配的关键字
    private String id; // 题目 id
    private Integer pageNum; // 查询第几页数据
}

4.2 定义返回参数

返回的 response 我也定义了四个字段:

  • questionList:查询到的题目列表。
  • pageNum:第几页数据。
  • total:查询到的总条数。
  • totalPages:总页数。

定义的类如下所示:

@Data
public class SearchQuestionResponse {
    private List<QuestionEsModel> questionList; // 题目列表
    private Integer pageNum; // 查询第几页数据
    private Long total; // 总条数
    private Integer totalPages; // 总页数
}

4.3 组装 ES 查询参数

调用 ES 的查询 API 时,需要构建查询参数。

组装查询参数的核心代码如下所示:

组装查询参数

  • 第一步:创建检索请求。
  • 第二步:设置哪些字段需要模糊匹配。这里有三个字段:title,answer,typeName。
  • 第三步:设置如何分页。这里分页大小是 5 个。
  • 第四步:调用查询 api。

4.4 格式化 ES 返回结果

ES 返回的数据是 ES 定义的格式,真正的数据被嵌套在 ES 的 response 中,所以需要格式化返回的数据。

核心代码如下图所示:

格式化 ES 返回结果

  • 第一步:获取查到的数据。
  • 第二步:获取真正命中的结果。
  • 第三步:格式化返回的数据。
  • 第四步:组装分页参数。

4.5 测试 ES 查询

4.5.1 实验一:测试 title 匹配

我们现在想要验证 title 字段是否能匹配到,传的请求参数 keyword = 111,匹配到了 title = 111 的数据,且只有一条。页码 pageNum 我传的 1,表示返回第一页数据。如下图所示:

测试匹配 title

4.5.2 实验二:测试 answer 匹配

我们现在想要验证 answer 字段是否能匹配到,传的请求参数 keyword = 测试答案,匹配到了 title = 测试答案的数据,且只有一条,说明查询成功。如下图所示:

测试匹配 answer

4.5.2 实验三:测试 id 匹配

我们现在想要匹配题目 id 的话,需要传请求参数 id,而且 id 是精确匹配。另外 id 和 keyword 是取并集,所以不能传 keyword 字段。

请求参数 id = 5,返回结果也是 id =5 的数据,说明查询成功。如下图所示:

测试 id 匹配

五、总结

本文通过我的开源项目 passjava 来讲解 ES 的整合,ES 的 API 使用以及测试。非常详细地讲解了每一步该如何做,相信通过阅读本篇后,再加上自己的实践,一定能掌握前后端该如何使用 ES 来达到高效搜索的目的。

当然,ES API 还有很多功能未在本文实践,有兴趣的同学可以到 ES 官网进行查阅和学习。

再次强调:本文的代码都是辛苦调试出来的,请不要忘记点赞和转发哦~w

查看原文

赞 1 收藏 0 评论 0

高阳Sunny 赞了文章 · 4月9日

Swoole v4.6.5 版本发布,增加原生curl multi支持

v4.6.5 版本没有向下不兼容改动,主要对原生 curl hook 进行了一些增强,支持了 curl multi

  • 支持原生 curl multi

使用原生 curl hook 的前提是在编译 Swoole 扩展时开启--enable-swoole-curl选项

可以使用以下代码进行测试:

use Swoole\Runtime;
use function Swoole\Coroutine\run;

Runtime::enableCoroutine(SWOOLE_HOOK_NATIVE_CURL);
run(function () {
    $ch1 = curl_init();
    $ch2 = curl_init();

    // 设置URL和相应的选项
    curl_setopt($ch1, CURLOPT_URL, "http://www.baidu.com/");
    curl_setopt($ch1, CURLOPT_HEADER, 0);
    curl_setopt($ch1, CURLOPT_RETURNTRANSFER, 1);

    curl_setopt($ch2, CURLOPT_URL, "http://www.gov.cn/");
    curl_setopt($ch2, CURLOPT_HEADER, 0);
    curl_setopt($ch2, CURLOPT_RETURNTRANSFER, 1);

    $mh = curl_multi_init();

    curl_multi_add_handle($mh, $ch1);
    curl_multi_add_handle($mh, $ch2);

    $active = null;
    // 执行批处理句柄
    do {
        $mrc = curl_multi_exec($mh, $active);
    } while ($mrc == CURLM_CALL_MULTI_PERFORM);

    while ($active && $mrc == CURLM_OK) {
        $n = curl_multi_select($mh);
        if ($n != -1) {
            do {
                $mrc = curl_multi_exec($mh, $active);
            } while ($mrc == CURLM_CALL_MULTI_PERFORM);
        }
    }

    $info1 = curl_multi_info_read($mh);
    $info2 = curl_multi_info_read($mh);
    $info3 = curl_multi_info_read($mh);

    assert($info1['msg'] === CURLMSG_DONE);
    assert($info2['msg'] === CURLMSG_DONE);
    assert($info3 === false);

    assert(strpos(curl_multi_getcontent($ch1),'baidu.com') !== false);
    assert(strpos(curl_multi_getcontent($ch2),'中央人民政府门户网站') !== false);

    curl_multi_remove_handle($mh, $ch1);
    curl_multi_remove_handle($mh, $ch2);

    curl_multi_close($mh);
});

支持 curl multi 之后,也就间接的支持了 Guzzle,无需更改任何代码,即可支持。

include __DIR__ . '/vendor/autoload.php';

use Swoole\Coroutine\Barrier;
use Swoole\Runtime;
use GuzzleHttp\Client;
use GuzzleHttp\Promise;

use function Swoole\Coroutine\run;
use function Swoole\Coroutine\go;

Runtime::enableCoroutine(SWOOLE_HOOK_NATIVE_CURL);

const N = 4;

run(function () {
    $barrier = Barrier::make();
    $result = [];
    go(function () use ($barrier, &$result) {
        $client = new Client();
        $promises = [
            'baidu' => $client->getAsync('http://www.baidu.com/'),
            'qq' => $client->getAsync('https://www.qq.com/'),
            'gov' => $client->getAsync('http://www.gov.cn/')
        ];
        $responses = Promise\Utils::unwrap($promises);
        assert(strpos($responses['baidu']->getBody(),'百度') !== false);
        assert(strpos(iconv('gbk', 'utf-8', $responses['qq']->getBody()),'腾讯') !== false);
        assert(strpos($responses['gov']->getBody(),'中华人民共和国') !== false);
        $result['task_1'] = 'OK';
    });

    go(function () use ($barrier, &$result) {
        $client = new Client(['base_uri' => 'http://httpbin.org/']);
        $n = N;
        $data = $promises = [];
        while ($n--) {
            $key = 'req_' . $n;
            $data[$key] = uniqid('swoole_test');
            $promises[$key] = $client->getAsync('/base64/' . base64_encode($data[$key]));
        }
        $responses = Promise\Utils::unwrap($promises);

        $n = N;
        while ($n--) {
            $key = 'req_' . $n;
            assert($responses[$key]->getBody() === $data[$key]);
        }
        $result['task_2'] = 'OK';
    });

    Barrier::wait($barrier);
    assert($result['task_1'] === 'OK');
    assert($result['task_2'] === 'OK');
    echo 'Done' . PHP_EOL;
});

同时也还添加了一些 Guzzle 的单元测试。

  • 允许在使用 HTTP/2 的 Response 中使用数组设置 headers

v4.6.0 版本开始 Swoole\Http\Response 支持重复设置相同 $keyHTTP 头,并且 $value 支持多种类型,如 arrayobjectintfloat,底层会进行 toString 转换,并且会移除末尾的空格以及换行。

但是未支持 HTTP/2 的,详情见 issue #4133

在此版本中也进行了支持:

$http = new Swoole\Http\Server('127.0.0.1', 9501);
$http->set(['open_http2_protocol' => true]);

$http->on('request', function ($request, $response) {
    $response->header('Test-Value', [
        "a\r\n",
        'd5678',
        "e  \n ",
        null,
        5678,
        3.1415926,
    ]);

    $response->end("<h1>Hello Swoole. #".rand(1000, 9999)."</h1>");
});
$http->start();

可以使用以上代码进行测试,并使用 curl 命令进行测试结果

$ curl --http2-prior-knowledge -v http://localhost:9501
*   Trying ::1...
* TCP_NODELAY set
* Connection failed
* connect to ::1 port 9501 failed: Connection refused
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 9501 (#0)
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7fe9e9009200)
> GET / HTTP/2
> Host: localhost:9501
> User-Agent: curl/7.64.1
> Accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
< HTTP/2 200
< test-value: a
< test-value: d5678
< test-value: e
< test-value: 5678
< test-value: 3.1415926
< server: swoole-http-server
< date: Fri, 09 Apr 2021 11:04:39 GMT
< content-type: text/html
< content-length: 28
<
* Connection #0 to host localhost left intact
<h1>Hello Swoole. #6944</h1>* Closing connection 0

更新日志

下面是完整的更新日志:

新增 API

  • 在 WaitGroup 中增加 count 方法(swoole/library#100) (@sy-records) (@deminy)

增强

  • 支持原生 curl multi (#4093) (#4099) (#4101) (#4105) (#4113) (#4121) (#4147) (swoole/swoole-src@cd7f51c) (@matyhtf) (@sy-records) (@huanghantao)
  • 允许在使用 HTTP/2 的 Response 中使用数组设置 headers

修复

  • 修复 NetBSD 构建 (#4080) (@devnexen)
  • 修复 OpenBSD 构建 (#4108) (@devnexen)
  • 修复 illumos/solaris 构建,只有成员别名 (#4109) (@devnexen)
  • 修复握手未完成时,SSL 连接的心跳检测不生效 (#4114) (@matyhtf)
  • 修复 Http\Client 使用代理时host中存在host:port产生的错误 (#4124) (@Yurunsoft)
  • 修复 Swoole\Coroutine\Http::request 中 header 和 cookie 的设置 (swoole/library#103) (@leocavalcante) (@deminy)

内核

  • 支持 BSD 上的 asm context (#4082) (@devnexen)
  • 在 FreeBSD 下使用 arc4random\_buf 来实现 getrandom (#4096) (@devnexen)
  • 优化 darwin arm64 context:删除 workaround 使用 label (#4127) (@devnexen)

测试

  • 添加 alpine 的构建脚本 (#4104) (@limingxinleo)

查看原文

赞 5 收藏 1 评论 0

高阳Sunny 赞了文章 · 4月9日

Apache DolphinScheduler 毕业成为 Apache 首个由国人主导的大数据工作流调度领域顶级项目

全球最大的开源软件基金会 Apache 软件基金会(以下简称 Apache)于北京时间 2021 年 4 月 9 日在官方渠道宣布 Apache DolphinScheduler 毕业成为 Apache 顶级项目。这是首个由国人主导并贡献到 Apache 的大数据工作流调度领域的顶级项目

DolphinScheduler™ 已经是联通、IDG、IBM、京东物流、联想、新东方、诺基亚、360、顺丰和腾讯等 400+ 公司在使用的分布式可视化大数据工作流调度系统。

DolphinScheduler 于 2019 年 8 月 29 日由易观捐赠给 Apache 并启动孵化。之后在导师的指导下,由孵化器管理委员会成员进行辅导和孵化,在 2021 年 2 月 19 日在 Apache 孵化器以 8 票支持一次性通过毕业提案投票。3 月 18 日,Apache 董事会通过  DolphinScheduler 毕业决议,结束了为期 18 个月的孵化,正式确定 DolphinScheduler 成为 Apache 顶级项目。

2021 年 4 月 9 日由 Apache 市场副总裁 Sally Khudairi 在 Apache 软件基金会各渠道官号上发布官方通告。

关于 Apache DolphinScheduler

Apache DolphinScheduler 是一个分布式易扩展的带有强大可视化 DAG 界面的新一代工作流调度平台,自 17 年在立项之初就确定了使命 - “解决大数据任务之间错综复杂的依赖关系,使整个数据处理过程直观可见”,从此配置工作流程再也不需要写代码啦!


DolphinScheduler 架构

作为强大的带有有向无环图(DAG)可视化界面的分布式大数据工作流调度平台,Dolphin Scheduler 解决了复杂的任务依赖关系和简化了数据任务编排的工作。它以开箱即用的、易于扩展的方式将众多大数据生态组件连接到可处理 100,000 级别的数据任务调度系统中来。

Apache DolphinScheduler 是全球顶尖架构师与社区认可的数据调度平台,把复杂性留给自己,易用性留给用户,整体有如下特征:

  • 云原生设计:支持多云、多数据中心的跨端调度,同时也支持K8s Docker部署与扩展,性能上可以线性增长,在用户测试情况下最高已经支持10万的并行任务控制;
  • 高可用:去中心化的多Master/Worker的架构,可以自动任务平衡,自动高可用,确保任务在任何节点死机的情况下可以具有完整性完成整体调度;
  • 用户友好的界面:可视化的DAG图,包括子任务,条件调度、脚本管理、多租户等方便功能,并具有让运行任务实例与任务模板分开,让你的平台维护人员和数据科学家都有一个方便易用的开发和管理平台;
  • 支持多种数据场景:支持流数据处理,批数据处理,暂停、回复、多租户等,对于spark,hive,MR,flink,clickhouse等等平台都可以方便直接调用。

DolphinScheduler 在短短 1 年半孵化时间里就能收获如此多用户和其一直倡导的 Slogan 是分不开的:

是的,调用用的对,半夜真能安心睡!最终 Apache Dolphin Scheduler 获得大量用户,例如中国联通、IBM中国,京东物流,联想,新东方,顺丰快递等等,400 多个用户都在线上使用:

Apache DolphinScheduler开始了新的起点!

DolphinScheduler 在设计之初就秉承着大道至简、产品易用易维护的核心理念,以高可用、易扩展作为架构底座,经过诸多打磨,获得了用户群体的肯定,然后才在 2019年 3 月正式对外开源,8 月 29 日成功进入到 Apache 孵化器,成为国内首个 Apache 大数据调度项目,多次登陆 github 月度趋势榜并引起 Hacker News 上的热烈讨论。在 2020 年, DolphinScheduler 也获得了诸多赞誉,InfoQ 中国技术力量年度榜单评其为 “十大开源新锐项目” 和 “最具人气开源新锐项目”,荣获开源中国年度 “最佳技术团队” 等称号。DolphinScheduler社区已经有庞大的 4000+ 人的用户群,据不完全统计,截止 2020 年 10 月,已经有 400+ 家公司将 DolphinScheduler 用在生产环境,DolphinScheduler 在各个领域上持续发光发热!

DolphinScheduler 发展历程

在 Apache 孵化过程中,学习并实践 Apache Way, 真正体会到了 Apache 文化的精髓 “Comuunity Over Code”,社区基于代码,却又大于代码,需要在代码之上去思考如何协作、如何沟通,如何让 Community 健康持续的向前发展。正是这不断的思考,使得 DolphinScheduler 在结束孵化投票上没有遇到任何 challenge,可以说是出乎意料的顺利。

毕业成为顶级项目见证了过去一年半以来来自 Apache DolphinScheduler 社区众多贡献者和用户的日日夜夜的努力,自从进入 Apache 孵化器以来,DolphinScheduler 社区共加入了 14 位 Commiter、2 位 PMC,也在社区的共同努力下发布了 8 个 Apache Releases,感谢给予辛勤支持和无数帮助的各位导师、贡献者、用户和及 Apache 孵化器的支持。

成为顶级项目,只是 Apache DolphinScheduler 新的起点,还有更多的挑战等待着各位社区的小伙伴和用户多多的支持,我们希望未来可以让中国优秀的开源项目走向世界,让世界都了解国内也有顶尖的开源项目,最后也感谢各位大咖对于 Apache DolphinScheduler 项目的帮助和祝福。我们也在此拜谢各位,也希望各位一如既往的支持 Apache DolphinScheduler 项目走向全球!


新一代大数据调度 - DolphinScheduler 的相关资源

查看原文

赞 3 收藏 1 评论 0

高阳Sunny 赞了文章 · 4月9日

⚡️ React Native 启动速度优化——Native 篇(内含源码分析)

Web 开发有一个经典问题:「浏览器中从输入 URL 到页面渲染的这个过程中都发生了什么?

据我考据这个问题起码有十年历史了。在日新月异学不动的前端圈子里,这个问题能一直被问,就是因为因为它是个非常好的问题,涉及非常多的知识点,平时做一些性能优化,都可以从这个问题出发,分析性能瓶颈,然后对症下药进行优化。

不过今天我们不谈 Web 的性能优化,只是借助刚刚的那个那个经典问题的分析思路,从 React Native 的启动到页面的第一次渲染完成,结合 React Native 的源码和 1.0 的新架构,一一分析 React Native 的启动性能优化之路

如果你喜欢我的文章,希望点赞👍 收藏 📁 评论 💬 三连支持一下,谢谢你,这对我真的很重要!
阅读提醒
1.文章中的源码内容为 RN 0.64 版本
2.源码分析内容涉及 Objective-CJavaC++JavaScript 四门语言,我尽量讲得通俗易懂一些,若实在不理解可以直接看结论

0.React Native 启动流程

React Native 作为一个 Web 前端友好的混合开发框架,启动时可以大致分为两个部分:

  • Native 容器的运行
  • JavaScript 代码的运行

其中 Native 容器启动在现有架构(版本号小于 1.0.0)里:大致可以分为 3 个部分:

  • Native 容器初始化
  • Native Modules 的全量绑定
  • JSEngine 的初始化

容器初始化后,舞台就交给了 JavaScript,流程可以细分为 2 个部分:

  • JavaScript 代码的加载、解析和执行
  • JS Component 的构建

最后 JS Thread 把计算好的布局信息发送到 Native 端,计算 Shadow Tree,最后由 UI Thread 进行布局和渲染。

关于渲染部分的性能优化可以见我之前写的《React Native 性能优化指南》,我从渲染图片动画长列表等方向介绍了 RN 渲染优化的常见套路,感兴趣的读者可以前往查看,我这里就不多介绍了。

上面的几个步骤,我画了一张图,下面我以这张图为目录,从左向右介绍各个步骤的优化方向:

提示:React Native 初始化时,有可能多个任务并行执行,所以上图只能表示 React Native 初始化的大致流程,并不和实际代码的执行时序一一对应。

1.升级 React Native

想提升 React Native 应用的性能,最一劳永逸的方法就是升级 RN 的大版本了。我们的应用从 0.59 升级到 0.62 之后,我们的 APP 没有做任何的性能优化工作,启动时间直接缩短了 1/2。当 React Native 的新架构发布后,启动速度和渲染速度都会大大加强。

当然,RN 的版本升级并不容易(横跨 iOS Android JS 三端,兼容破坏性更新),我之前写过一篇《React Native 升级指南(0.59 -> 0.62)》的文章,如果有升级想法的老铁可以阅读参考一下。

2.Native 容器初始化

容器的初始化肯定是从 APP 的入口文件开始分析,下面我会挑选一些关键代码,梳理一下初始化的流程。

iOS 源码分析

1.AppDelegate.m

AppDelegate.m 是 iOS 的入口文件,代码非常精简,主要内容如下所示:

// AppDelegate.m

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  // 1.初始化一个 RCTBridge 实现加载 jsbundle 的方法
  RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];

  // 2.利用 RCTBridge 初始化一个 RCTRootView
  RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                                   moduleName:@"RN64"
                                            initialProperties:nil];

  // 3.初始化 UIViewController
  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  UIViewController *rootViewController = [UIViewController new];
  
  // 4.将 RCTRootView 赋值给 UIViewController 的 view
  rootViewController.view = rootView;
  self.window.rootViewController = rootViewController;
  [self.window makeKeyAndVisible];
  return YES;
}

总的来看入口文件就做了三件事:

  • 初始化一个 RCTBridge 实现加载 jsbundle 的方法
  • 利用 RCTBridge 初始化一个 RCTRootView
  • RCTRootView 赋值给 UIViewController 的 view 实现 UI 的挂载

从入口源码我们可以发现,所有的初始化工作都指向RCTRootView,所以接下来我们看看 RCTRootView 干了些啥。

2.RCTRootView

我们先看一下 RCTRootView 的头文件,删繁就简,我们只看我们关注的一些方法:

// RCTRootView.h

@interface RCTRootView : UIView

// AppDelegate.m 中用到的初始化方法
- (instancetype)initWithBridge:(RCTBridge *)bridge
                    moduleName:(NSString *)moduleName
             initialProperties:(nullable NSDictionary *)initialProperties NS_DESIGNATED_INITIALIZER;

从头文件看出:

  • RCTRootView 继承自 UIView,所以它本质上就是一个 UI 组件;
  • RCTRootView 调用 initWithBridge 初始化时要传入一个已经初始化的 RCTBridge

RCTRootView.m 文件里,initWithBridge 初始化时会监听一系列的 JS 加载监听函数,监听到 JS Bundle 文件加载结束后,就会调用 JS 里的 AppRegistry.runApplication(),启动 RN 应用。

分析到这里,我们发现 RCTRootView.m 只是实现了对 RCTBridge 的的各种事件监听,并不是初始化的核心,所以我们就又要转到 RCTBridge 这个文件上去。

3.RCTBridge.m

RCTBridge.m 里,初始化的调用路径有些长,全贴源码有些长,总之最后调用的是 (void)setUp,核心代码如下:

- (Class)bridgeClass
{
  return [RCTCxxBridge class];
}

- (void)setUp {
  // 获取bridgeClass 默认是 RCTCxxBridge
  Class bridgeClass = self.bridgeClass;
  // 初始化 RTCxxBridge
  self.batchedBridge = [[bridgeClass alloc] initWithParentBridge:self];
  // 启动 RTCxxBridge
  [self.batchedBridge start];
}

我们可以看到,RCTBridge 的初始化又指向了 RTCxxBridge

4.RTCxxBridge.mm

RTCxxBridge 可以说是 React Native 初始化的核心,我查阅了一些资料,貌似 RTCxxBridge 曾用名为 RCTBatchedBridge,所以可以粗暴的把这两个类当成一回事儿。

因为在 RCTBridge 里调用了 RTCxxBridgestart 方法,我们就从 start 方法来看看做了些什么。

// RTCxxBridge.mm

- (void)start {
  // 1.初始化 JSThread,后续所有的 js 代码都在这个线程里面执行
  _jsThread = [[NSThread alloc] initWithTarget:[self class] selector:@selector(runRunLoop) object:nil];
  [_jsThread start];
  
  // 创建并行队列
  dispatch_group_t prepareBridge = dispatch_group_create();
  
  // 2.注册所有的 native modules
  [self registerExtraModules];
  (void)[self _initializeModules:RCTGetModuleClasses() withDispatchGroup:prepareBridge lazilyDiscovered:NO];
  
  // 3.初始化 JSExecutorFactory 实例
  std::shared_ptr<JSExecutorFactory> executorFactory;
  
  // 4.初始化底层 Instance 实例,也就是 _reactInstance
  dispatch_group_enter(prepareBridge);
  [self ensureOnJavaScriptThread:^{
    [weakSelf _initializeBridge:executorFactory];
    dispatch_group_leave(prepareBridge);
  }];
  
  // 5.加载 js 代码
  dispatch_group_enter(prepareBridge);
  __block NSData *sourceCode;
  [self
      loadSource:^(NSError *error, RCTSource *source) {
        if (error) {
          [weakSelf handleError:error];
        }

        sourceCode = source.data;
        dispatch_group_leave(prepareBridge);
      }
      onProgress:^(RCTLoadingProgress *progressData) {
      }
  ];
  
  // 6.等待 native moudle 和 JS 代码加载完毕后就执行 JS
  dispatch_group_notify(prepareBridge, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
    RCTCxxBridge *strongSelf = weakSelf;
    if (sourceCode && strongSelf.loading) {
      [strongSelf executeSourceCode:sourceCode sync:NO];
    }
  });
}

上面代码比较长,里面用到了 GCD 多线程的一些知识点,用文字描述大致是如下的流程:

  1. 初始化 js 线程 _jsThread
  2. 在主线程上注册所有 native modules
  3. 准备 jsNative 之间的桥和 js 运行环境
  4. 在 JS 线程上创建消息队列 RCTMessageThread,初始化 _reactInstance
  5. 在 JS 线程上加载 JS Bundle
  6. 等上面的事情全部做完后,执行 JS 代码

其实上面的六个点都可以深挖下去,但是本节涉及到的源码内容到这里就可以了,感兴趣的读者可以结合我最后给出的参考资料和 React Native 源码深挖探索一下。

Android 源码分析

1.MainActivity.java & MainApplication.java

和 iOS 一样,启动流程我们先从入口文件开始分析,我们先看 MainActivity.java

MainActivity 继承自 ReactActivityReactActivity 又继承自 AppCompatActivity

// MainActivity.java

public class MainActivity extends ReactActivity {
  // 返回组件名,和 js 入口注册名字一致
  @Override
  protected String getMainComponentName() {
    return "rn_performance_demo";
  }
}

我们再从 Android 的入口文件 MainApplication.java 开始分析:

// MainApplication.java

public class MainApplication extends Application implements ReactApplication {

  private final ReactNativeHost mReactNativeHost =
      new ReactNativeHost(this) {
        // 返回 app 需要的 ReactPackage,添加需要加载的模块,
        // 这个地方就是我们在项目中添加依赖包时需要添加第三方 package 的地方
        @Override
        protected List<ReactPackage> getPackages() {
          @SuppressWarnings("UnnecessaryLocalVariable")
          List<ReactPackage> packages = new PackageList(this).getPackages();
          return packages;
        }

        // js bundle 入口文件,设置为 index.js
        @Override
        protected String getJSMainModuleName() {
          return "index";
        }
      };

  @Override
  public ReactNativeHost getReactNativeHost() {
    return mReactNativeHost;
  }

  @Override
  public void onCreate() {
    super.onCreate();
    // SoLoader:加载C++底层库
    SoLoader.init(this, /* native exopackage */ false);
  }
}

ReactApplication 接口很简单,要求我们创建一个 ReactNativeHost 对象:

public interface ReactApplication {
  ReactNativeHost getReactNativeHost();
}

从上面的分析我们可以看出一切指向了 ReactNativeHost 这个类,下面我们就看一下它。

2.ReactNativeHost.java

ReactNativeHost 主要的工作就是创建了 ReactInstanceManager:

public abstract class ReactNativeHost {
  protected ReactInstanceManager createReactInstanceManager() {
    ReactMarker.logMarker(ReactMarkerConstants.BUILD_REACT_INSTANCE_MANAGER_START);
    ReactInstanceManagerBuilder builder =
        ReactInstanceManager.builder()
            // 应用上下文
            .setApplication(mApplication)
            // JSMainModulePath 相当于应用首页的 js Bundle,可以传递 url 从服务器拉取 js Bundle
            // 当然这个只在 dev 模式下可以使用
            .setJSMainModulePath(getJSMainModuleName())
            // 是否开启 dev 模式
            .setUseDeveloperSupport(getUseDeveloperSupport())
            // 红盒的回调
            .setRedBoxHandler(getRedBoxHandler())
            .setJavaScriptExecutorFactory(getJavaScriptExecutorFactory())
            .setUIImplementationProvider(getUIImplementationProvider())
            .setJSIModulesPackage(getJSIModulePackage())
            .setInitialLifecycleState(LifecycleState.BEFORE_CREATE);

    // 添加 ReactPackage
    for (ReactPackage reactPackage : getPackages()) {
      builder.addPackage(reactPackage);
    }
    
    // 获取 js Bundle 的加载路径
    String jsBundleFile = getJSBundleFile();
    if (jsBundleFile != null) {
      builder.setJSBundleFile(jsBundleFile);
    } else {
      builder.setBundleAssetName(Assertions.assertNotNull(getBundleAssetName()));
    }
    ReactInstanceManager reactInstanceManager = builder.build();
    return reactInstanceManager;
  }
}

3.ReactActivityDelegate.java

我们再回到 ReactActivity,它自己并没有做什么事情,所有的功能都由它的委托类 ReactActivityDelegate 来完成,所以我们直接看ReactActivityDelegate是怎么实现的:

public class ReactActivityDelegate {
  protected void onCreate(Bundle savedInstanceState) {
    String mainComponentName = getMainComponentName();
    mReactDelegate =
        new ReactDelegate(
            getPlainActivity(), getReactNativeHost(), mainComponentName, getLaunchOptions()) {
          @Override
          protected ReactRootView createRootView() {
            return ReactActivityDelegate.this.createRootView();
          }
        };
    if (mMainComponentName != null) {
      // 载入 app 页面
      loadApp(mainComponentName);
    }
  }
  
  protected void loadApp(String appKey) {
    mReactDelegate.loadApp(appKey);
    // Activity 的 setContentView() 方法
    getPlainActivity().setContentView(mReactDelegate.getReactRootView());
  }
}

onCreate() 的时候又实例化了一个 ReactDelegate,我们再看看它的实现。

4.ReactDelegate.java

ReactDelegate.java 里,我没看见它做了两件事:

  • 创建 ReactRootView 作为根视图
  • 调用 getReactNativeHost().getReactInstanceManager() 启动 RN 应用
public class ReactDelegate {
  public void loadApp(String appKey) {
    if (mReactRootView != null) {
      throw new IllegalStateException("Cannot loadApp while app is already running.");
    }
    // 创建 ReactRootView 作为根视图
    mReactRootView = createRootView();
    // 启动 RN 应用
    mReactRootView.startReactApplication(
        getReactNativeHost().getReactInstanceManager(), appKey, mLaunchOptions);
  }
}

基础的启动流程本节涉及到的源码内容到这里就可以了,感兴趣的读者可以结合我最后给出的参考资料和 React Native 源码深挖探索一下。

优化建议

对于 React Native 为主体的应用,APP 启动后就要立马初始化 RN 容器,基本上没有什么优化思路;但是 Native 为主的混合开发 APP 却有招:

既然初始化耗时最长,我们在正式进入 React Native 容器前提前初始化不就好了?

这个方法非常的常见,因为很多 H5 容器也是这样做的。正式进入 WebView 网页前,先做一个 WebView 容器池,提前初始化 WebView,进入 H5 容器后,直接加载数据渲染,以达到网页秒开的效果。

RN 容器池这个概念看着很玄乎,其实就是一个 Mapkey 为 RN 页面的 componentName(即 AppRegistry.registerComponent(appName, Component) 中传入的 appName),value 就是一个已经实例化的 RCTRootView/ReactRootView

APP 启动后找个触发时机提前初始化,进入 RN 容器前先读容器池,如果有匹配的容器,直接拿来用即可,没有匹配的再重新初始化。

写两个很简单的案例,iOS 可以如下图所示,构建 RN 容器池:

@property (nonatomic, strong) NSMutableDictionary<NSString *, RCTRootView *> *rootViewRool;

// 容器池
-(NSMutableDictionary<NSString *, RCTRootView *> *)rootViewRool {
  if (!_rootViewRool) {
    _rootViewRool = @{}.mutableCopy;
  }
  
  return _rootViewRool;
}


// 缓存 RCTRootView
-(void)cacheRootView:(NSString *)componentName path:(NSString *)path props:(NSDictionary *)props bridge:(RCTBridge *)bridge {
  // 初始化
  RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                                   moduleName:componentName
                                            initialProperties:props];
  // 实例化后要加载到屏幕的最下面,否则不能触发视图渲染
  [[UIApplication sharedApplication].keyWindow.rootViewController.view insertSubview:rootView atIndex:0];
  rootView.frame = [UIScreen mainScreen].bounds;
  
  // 把缓存好的 RCTRootView 放到容器池中
  NSString *key = [NSString stringWithFormat:@"%@_%@", componentName, path];
  self.rootViewRool[key] = rootView;
}


// 读取容器
-(RCTRootView *)getRootView:(NSString *)componentName path:(NSString *)path props:(NSDictionary *)props bridge:(RCTBridge *)bridge {
  NSString *key = [NSString stringWithFormat:@"%@_%@", componentName, path];
  RCTRootView *rootView = self.rootViewRool[key];
  if (rootView) {
    return rootView;
  }
  
  // 兜底逻辑
  return [[RCTRootView alloc] initWithBridge:bridge moduleName:componentName initialProperties:props];
}

Android 如下构建 RN 容器池:

private HashMap<String, ReactRootView> rootViewPool = new HashMap<>();

// 创建容器
private ReactRootView createRootView(String componentName, String path, Bundle props, Context context) {
    ReactInstanceManager bridgeInstance = ((ReactApplication) application).getReactNativeHost().getReactInstanceManager();
    ReactRootView rootView = new ReactRootView(context);

    if(props == null) {
        props = new Bundle();
    }
    props.putString("path", path);

    rootView.startReactApplication(bridgeInstance, componentName, props);

    return rootView;
}

// 缓存容器
public void cahceRootView(String componentName, String path, Bundle props, Context context) {
    ReactRootView rootView = createRootView(componentName, path, props, context);
    String key = componentName + "_" + path;

    // 把缓存好的 RCTRootView 放到容器池中
    rootViewPool.put(key, rootView);
}

// 读取容器
public ReactRootView getRootView(String componentName, String path, Bundle props, Context context) {
    String key = componentName + "_" + path;
    ReactRootView rootView = rootViewPool.get(key);

    if (rootView != null) {
        rootView.setAppProperties(newProps);
        rootViewPool.remove(key);
        return rootView;
    }

    // 兜底逻辑
    return createRootView(componentName, path, props, context);
}

当然,由于每次 RCTRootView/ReactRootView 都要占用一定的内存,所以什么时候实例化,实例化几个容器,池的大小限制,什么时候清除容器,都需要结合业务进行实践和摸索。

3.Native Modules 绑定

iOS 源码分析

iOS 的 Native Modules 有 3 块儿内容,大头是中间的 _initializeModules 函数:

// RCTCxxBridge.mm

- (void)start {
  // 初始化 RCTBridge 时调用 initWithBundleURL_moduleProvider_launchOptions 中的 moduleProvider 返回的 native modules
  [self registerExtraModules];
  
  // 注册所有的自定义 Native Module
  (void)[self _initializeModules:RCTGetModuleClasses() withDispatchGroup:prepareBridge lazilyDiscovered:NO];
  
  // 初始化所有懒加载的 native module,只有用 Chrome debug 时才会调用
  [self registerExtraLazyModules];
}

我们看看 _initializeModules 函数做了什么:

// RCTCxxBridge.mm

- (NSArray<RCTModuleData *> *)_initializeModules:(NSArray<Class> *)modules
                               withDispatchGroup:(dispatch_group_t)dispatchGroup
                                lazilyDiscovered:(BOOL)lazilyDiscovered
{
    for (RCTModuleData *moduleData in _moduleDataByID) {
      if (moduleData.hasInstance && (!moduleData.requiresMainQueueSetup || RCTIsMainQueue())) {
        // Modules that were pre-initialized should ideally be set up before
        // bridge init has finished, otherwise the caller may try to access the
        // module directly rather than via `[bridge moduleForClass:]`, which won't
        // trigger the lazy initialization process. If the module cannot safely be
        // set up on the current thread, it will instead be async dispatched
        // to the main thread to be set up in _prepareModulesWithDispatchGroup:.
        (void)[moduleData instance];
      }
    }
    _moduleSetupComplete = YES;
    [self _prepareModulesWithDispatchGroup:dispatchGroup];
}

根据 _initializeModules_prepareModulesWithDispatchGroup 的注释,可以看出 iOS 在 JS Bundle 加载的过程中(在 JSThead 线程进行),同时在主线程初始化所有的 Native Modules。

结合前面的源码分析,我们可以看出 React Native iOS 容器初始化的时候,会初始化所有的 Native Modules,若 Native Modules 比较多,就会影响 Android RN 容器的启动时间。

Android 源码分析

关于 Native Modules 的注册,其实在 MainApplication.java 这个入口文件里已经给出了线索:

// MainApplication.java

protected List<ReactPackage> getPackages() {
  @SuppressWarnings("UnnecessaryLocalVariable")
  List<ReactPackage> packages = new PackageList(this).getPackages();
  // Packages that cannot be autolinked yet can be added manually here, for example:
  // packages.add(new MyReactNativePackage());
  return packages;
}

由于 0.60 之后 React Native 启用了 auto link,安装的第三方 Native Modules 都在 PackageList 里,所以我们只要 getPackages() 一下就能获取 auto link 的 Modules。

源码里,在 ReactInstanceManager.java 这个文件中,会运行 createReactContext() 创建 ReactContext,这里面有一步就是注册 nativeModules 的注册表:

// ReactInstanceManager.java

private ReactApplicationContext createReactContext(
  JavaScriptExecutor jsExecutor, 
  JSBundleLoader jsBundleLoader) {
  
  // 注册 nativeModules 注册表
  NativeModuleRegistry nativeModuleRegistry = processPackages(reactContext, mPackages, false);
}

根据函数调用,我们追踪到 processPackages() 这个函数里,利用一个 for 循环把 mPackages 里的 Native Modules 全部加入注册表:

// ReactInstanceManager.java

private NativeModuleRegistry processPackages(
    ReactApplicationContext reactContext,
    List<ReactPackage> packages,
    boolean checkAndUpdatePackageMembership) {
  // 创建 JavaModule 注册表 Builder,用来创建 JavaModule 注册表,
  // JavaModule 注册表将所有的 JavaModule 注册到 CatalystInstance 中
  NativeModuleRegistryBuilder nativeModuleRegistryBuilder =
      new NativeModuleRegistryBuilder(reactContext, this);

  // 给 mPackages 加锁
  // mPackages 类型为 List<ReactPackage>,与 MainApplication.java 里的 packages 对应
  synchronized (mPackages) {
    for (ReactPackage reactPackage : packages) {
      try {
        // 循环处理我们在 Application 里注入的 ReactPackage,处理的过程就是把各自的 Module 添加到对应的注册表中
        processPackage(reactPackage, nativeModuleRegistryBuilder);
      } finally {
        Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
      }
    }
  }

  NativeModuleRegistry nativeModuleRegistry;
  try {
    // 生成 Java Module 注册表
    nativeModuleRegistry = nativeModuleRegistryBuilder.build();
  } finally {
    Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
    ReactMarker.logMarker(BUILD_NATIVE_MODULE_REGISTRY_END);
  }

  return nativeModuleRegistry;
}

最后调用 processPackage() 进行真正的注册:

// ReactInstanceManager.java

private void processPackage(
    ReactPackage reactPackage,
    NativeModuleRegistryBuilder nativeModuleRegistryBuilder
) {
  nativeModuleRegistryBuilder.processPackage(reactPackage);
}

从上面的流程可以看出,Android 注册 Native Modules 的时候是同步全量注册的,若 Native Modules 比较多,就会影响 Android RN 容器的启动时间。

优化建议

说实话,Native Modules 全量绑定在现有的架构里是无解的:不管这个 Native Methods 你有没有用到,容器启动时先全部初始化一遍。在新的 RN 架构里,TurboModules 会解决这个问题(本文下一小节会介绍)。

如果非要说优化,其实还有个思路,你不是全量初始化吗,那我让 Native Modules 的数量减少不就行了?新架构里有一步叫做 Lean Core,就是精简 React Native 核心,把一些功能/组件从 RN 的主工程项目里移出去(例如 WebView 组件),交给社区维护,你想用的时候再单独下载集成。

这样做的好处主要有几点:

  • 核心更加精简,RN 维护者有更多的精力维护主要功能
  • 减小 Native Modules 的绑定耗时和多余的 JS 加载时间,包体积的减小,对初始化性能更友好(我们升级 RN 版本到 0.62 后初始化速度提升一倍,基本都是 Lean Core 的功劳)
  • 加快迭代速度,优化开发体验等

现在 Lean Core 的工作基本已经完成,更多讨论可见官方 issues 讨论区,我们只要同步升级 React Native 版本就可以享用 Lean Core 的成果。

4.RN 新架构如何优化启动性能

React Native 新架构已经跳票快两年了,每次问进度,官方回复都是“别催了别催了在做了在做了”。

我个人去年期待了一整年,但是啥都没等到,所以 RN 啥时候更新到 1.0.0 版本,我已经不在乎了。虽然 RN 官方一直在鸽,但是不得不说他们的新架构还是有些东西的,市面上存在关于 RN 新架构的文章和视频我基本都看了一遍,所以个人对新架构还是有个整体的认知。

因为新架构还没有正式放出,所以具体细节上肯定还存在一些差异,具体执行细节还是要等 React Native 官方为准。

JSI

JSI 的全名是 JavaScript Interface,一个用 C++ 写的框架,作用是支持 JS 直接调用 Native 方法,而不是现在通过 Bridge 异步通讯。

JS 直接调用 Native 如何理解呢?我们举一个最简单的例子。在浏览器上调用 setTimeoutdocument.getElementById 这类 API 的时候,其实就是在 JS 侧直接调用 Native Code,我们可以在浏览器控制台里验证一下:

比如说我执行了一条命令:

let el = document.createElement('div')

变量 el 持有的不是一个 JS 对象,而是一个在 C++ 中被实例化的对象。对于 el 持有的这个对象我们再设置一下相关属性:

el.setAttribute('width', 100)

这时候其实是 JS 同步调用 C++ 中的 setWidth 方法,改变这个元素的宽度。

React Native 新架构中的 JSI,主要就是起这个作用的,借助 JSI,我们可以用 JS 直接获得 C++ 对象的引用(Host Objects),进而直接控制 UI,直接调用 Native Modules 的方法,省去 bridge 异步通讯的开销。

下面我们举个小例子,来看一下 Java/OC 如何借助 JSI 向 JS 暴露同步调用的方法。

#pragma once

#include <string>
#include <unordered_map>

#include <jsi/jsi.h>

// SampleJSIObject 继承自 HostObject,表示这个一个暴露给 JS 的对象
// 对于 JS 来说,JS 可以直接同步调用这个对象上的属性和方法
class JSI_EXPORT SampleJSIObject : public facebook::jsi::HostObject {

public: 

// 第一步
// 将 window.__SampleJSIObject 暴露给JavaScript
// 这是一个静态函数,一般在应用初始化时从 ObjC/Java 中调用
static void SampleJSIObject::install(jsi::Runtime &runtime) {
  runtime.global().setProperty(
      runtime,
      "__sampleJSIObject",
      jsi::Function::createFromHostFunction(
          runtime,
          jsi::PropNameID::forAscii(runtime, "__SampleJSIObject"),
          1,
          [binding](jsi::Runtime& rt, const jsi::Value& thisVal, const jsi::Value* args, size_t count) {
            // 返回调用 window.__SampleJSIObject 时得到的内容
            return std::make_shared<SampleJSIObject>();
          }));
}

// 类似于 getter,每次 JS 访问这个对象的时候,都要经过这个方法,作用类似于一个包装器
// 比如说我们调用 window.__sampleJSIObject.method1(),这个方法就会被调用
jsi::Value TurboModule::get(jsi::Runtime& runtime, const jsi::PropNameID& propName) {
  // 调用方法名
  // 比如说调用 window.__sampleJSIObject.method1() 时,propNameUtf8 就是 method1
  std::string propNameUtf8 = propName.utf8(runtime);

  return jsi::Function::createFromHostFunction(
    runtime,
    propName,
    argCount,
    [](facebook::jsi::Runtime &rt, const facebook::jsi::Value &thisVal, const facebook::jsi::Value *args, size_t count) {
      if (propNameUtf8 == 'method1') {
        // 调用 method1 时,相关的函数处理逻辑
      }
    });
}
  
std::vector<PropNameID> getPropertyNames(Runtime& rt){
}
  
}

上面的例子比较简短,想要深入了解 JSI,可以看《React Native JSI Challenge》这篇文章或直接阅读源码。

TurboModules

经过前面的源码分析,我们可以得知,现有架构里,Native 初始化时会全量加载 native modules,随着业务的迭代,native modules 只会越来越多,这里的耗时会越来越长。

TurboModules 就可以一次性解决这个问题。在新架构里,native modules 是懒加载的,也就是说只有你调用相应的 native modules 时才会初始化加载,这样就解决了初始化全量加载耗时较长的问题。

TurboModules 的调用路径大概是这样的:

  1. 先用 JSI 创建一个顶层的「Native Modules Proxy」,称之为 global.__turboModuleProxy
  2. 访问一个 Native Modules,比如说要访问 SampleTurboModule,我们先在 JavaScript 侧执行 require('NativeSampleTurboModule')
  3. 在 NativeSampleTurboModule.js 这个文件里,我们先调用 TurboModuleRegistry.getEnforcing(),然后就会调用 global.__turboModuleProxy("SampleTurboModule")
  4. 调用 global.__turboModuleProxy 的时候,就会调用第一步 JSI 暴露的 Native 方法,这时候 C++ 层通过传入的字符串 "SampleTurboModule",找到 ObjC/Java 的实现,最后返回一个对应的 JSI 对象
  5. 现在我们得到了 SampleTurboModule 的 JSI 对象,就可以用 JavaScript 同步调用 JSI 对象上的属性和方法

通过上面的步骤,我们可以看到借助 TurboModules, Native Modules 只有初次调用的时候才会加载,这样就彻底干掉 React Native 容器初始化时全量加载 Native Modules 时的时间;同时我们可以借助 JSI 实现 JS 和 Native 的同步调用,耗时更少,效率更高。

总结

本文主要从 Native 的角度出发,从源码分析 React Native 现有架构的启动流程,总结了几个 Native 层的性能优化点;最后又简单介绍了一下React Native 的新架构。下一篇文章我会讲解如何从 JavaScript 入手,优化 React Native 的启动速度。


如果你喜欢我的文章,希望点赞👍 收藏 📁 评论 💬 三连支持一下,谢谢你,这对我真的很重要!

欢迎大家关注我的微信公众号:卤蛋实验室,目前专注前端技术,对图形学也有一些微小研究。

原文链接 👉 ⚡️ React Native 启动速度优化——Native 篇(内含源码分析):更新更及时,阅读体验更佳

参考

React Native 性能优化指南

React Native 升级指南(0.59 -> 0.62)

Chain React 2019 - Ram Narasimhan - Performance in React Native

React Native's new architecture - Glossary of terms

React Native JSI Challenge

RFC0002: Turbo Modules ™

ReactNative与iOS原生通信原理解析-初始化

React Native iOS 源码解析

ReactNative源码篇:源码初识

如何用React Native预加载方案解决白屏问题


查看原文

赞 7 收藏 2 评论 0

高阳Sunny 赞了文章 · 4月8日

[项目实战] Webpack to Vite, 为开发提速!

Webpack to Vite

背景

最近,就 前端开发过程中的痛点及可优化项 做了一次收集。 其中,构建耗时、项目编译速度慢 的字眼出现了好几次。

随着业务的快速发展,我们很多项目的体积也快速膨胀。 随之而来的, 就是打包变慢等问题。

提升研发效率,是技术人永恒的追求。

我们项目也有启动慢的问题,同事也提到过几次。 刚好我之前也做过类似的探索和优化, 于是就借这个机会,改造一下项目, 解决启动耗时的问题

于昨天下午(2021.4.7 23:00), 成功嵌入 Vite, 项目启动时间由约 190s => 20s, 热更新时间缩短为 2s

中间踩了一些坑, 好在最后爬出来了, 相关技术要点都会在下文中呈现。

FBI Warning: 以下文字,只是我结合自己的实际项目, 总结出来的一些浅薄的经验, 如有错误,欢迎指正 :)

今天的主要内容:

  • 为什么 Vite 启动这么快
  • 我的项目如何植入 Vite
  • 我在改造过程中遇到的问题
  • 关于 Vite 开发、打包上线的一些思考
  • 相关代码和结论

正文

为什么 Vite 启动这么快

底层实现上, Vite 是基于 esbuild 预构建依赖的。

esbuild 使用 go 编写,并且比以 js 编写的打包器预构建依赖, 快 10 - 100 倍。

因为 js 跟 go 相比实在是太慢了,js 的一般操作都是毫秒计,go 则是纳秒。

另外, 两者的启动方式也有所差异。

webpack 启动方式

image.png

Vite 启动方式

image.png

Webpack 会先打包,然后启动开发服务器,请求服务器时直接给予打包结果。

而 Vite 是直接启动开发服务器,请求哪个模块再对该模块进行实时编译

由于现代浏览器本身就支持 ES Module,会自动向依赖的 Module 发出请求。

Vite 充分利用了这一点,将开发环境下的模块文件,就作为浏览器要执行的文件,而不是像 W ebpack 那样进行打包合并

由于 Vite 在启动的时候不需要打包,也就意味着不需要分析模块的依赖不需要编译
因此启动速度非常快。当浏览器请求某个模块时,再根据需要对模块内容进行编译。

这种按需动态编译的方式,极大的缩减了编译时间,项目越复杂、模块越多,vite 的优势越明显。

在 HMR(热更新)方面,当改动了一个模块后,仅需让浏览器重新请求该模块即可,不像webpack那样需要把该模块的相关依赖模块全部编译一次,效率更高。

从实际的开发体验来看, 在 Vite 模式下, 开发环境可以瞬间启动, 但是等到页面出来, 要等一段时间。

我的项目如何植入 Vite

新项目

创建一个 Vite 新项目就比较简单:

yarn create @vitejs/app

image.png

image.png

生成好之后, 直接启动就可以了:

image.png

已有项目

已有项目的迁移, 稍微繁琐一些。

首先, 加入 Vite 的相关配置。 这里我使用了一个 cli 工具: wp2vite.

安装好之后, 直接执行:

image.png

这一步, 会自动生成 Vite 的配置文件,并引入相关的依赖。

把依赖安装一下, 启动就可以了。

如果没有意外的话, 你会收获一堆报错

恭喜你,进入开心愉快的踩坑环节。

我在改造过程中遇到的问题

1. alias 错误

image.png

项目代码里配置了一些别名,vite 无法识别,所以需要在vite 里面也配置 alias:

  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
    },
  },

2. 无法识别 less 全局变量

image.png

解决办法:

把自定义的全局变量从外部注入即可, 直接在 vite.config.js 的 css 选项中加入:

  css: {
    preprocessorOptions: {
      less: {
        modifyVars: {
          hack: `true;@import '${resolve('./src/vars.less')}';`,
          ...themeVariables,
        },
        javascriptEnabled: true,
      },
    },
  },

3. Uncaught Error: Target container is not a DOM element.

image.png

根元素未找到。

原因是: 默认生成的 index.html 中:

<div id="root"></div>

id 是 root, 而逻辑中的是#app, 这里直接改成 id=app 即可。

4. typings 文件找不到

image.png

typings 文件未找到

这个错误, 乍一看, 一头雾水。

进去看一下源代码和编译后的代码:

源代码:

image.png

编译后:

image.png

image.png

typings 文件这不是好好的在这吗, 怎么就找不到?

想了一下: Vite 不知道 typeings 文件是不需要被编译的,需要告诉编译器不编译这个文件。

最后在 TS 官方文档里找到了答案:

https://www.typescriptlang.or...

Type-Only Imports and Export

This feature is something most users may never have to think about; however, if you’ve hit issues under --isolatedModules, TypeScript’s transpileModule API, or Babel, this feature might be relevant.

TypeScript 3.8 adds a new syntax for type-only imports and exports.

import type { SomeThing } from "./some-module.js";
export type { SomeThing };

需要单独引入types, 于是把代码改为:

image.png

同时要注意, 如果一个文件有有多个导出, 也要分开引入:

image.png

唯一痛苦的是: 全局都需要改一遍, 体力活。

至此,typeings 问题完美解决。

5. 无法识别 svg

我们在使用 svg 作为图标组件的时候, 一般是:

import Icon from '@ant-design/icons';
import ErrorSvg from '@/assets/ico_error.svg';

const ErrorIcon = (props: any) => <Icon component={ErrorSvg} />;

// ...
<ErrorIcon />

浏览器报错:

image.png

error occurred in the </src/assets/ico_error.svg> component

很明显的看到, 这里是把文件路径作为组件了。

现在要做的是:把这个文件路径, 换成可以识别的组件。

搜索一番, 找到了个插件: vite-plugin-react-svg

加入配置:

const reactSvgPlugin = require('vite-plugin-react-svg');

plugins: [
  reactSvgPlugin(),
],
import MyIcon from './svgs/my-icon.svg?component';

function App() {
  return (
    <div>
      <MyIcon />
    </div>
  );
}

需要注意的是: 引入的 svg 文件需要加 ?component 作为后缀。

看了一下源码, 这个后缀是用来作为标识符的,

image.png

如果后缀匹配上是component, 就解析文件, 并缓存, 最后返回结果:

image.png

知道原理之后, 就需要把全部的 .svg => .svg?component

vscode 一键替换就可以, 不过注意别把 node_module 里面的也替换了。

6. global 未定义

image.png

global 是 Node里面的变量, 会在客户端报错 ?

一层层看下去, 原来是引入的第三方包使用了global。

看 vite 文档里提到了 Client Types:

image.png

追加到 tsconfig 里面:

 "compilerOptions": {
    "types": ["node", "jest", "vite/client"],
 }

然后, 并没有什么乱用。。。

image.png

没办法, 只得祭出 window 大法。

在入口index.tsx 里面加上:

(window as any).global = window;

刷新, 好了。

image.png

7. [未解决] 替代HtmlWebpackPlugin

还需要注入一些外部变量, 修改入口html, favicon, title 之类。

找到一个插件: vite-plugin-singlefile

不过并没有什么用。

有了解的同学请留言赐教。

至此, 整个app 已经能在本地跑起来了, build 也没问题。

7. 线上打包构建时, 内存溢出

本地能跑起来, 打包也没问题, 后面当然是放到线上跑一跑啦。

立刻安排!

image.png

内存不足, 我就给你加点:

image.png

image.png

搞定!

unnamed.gif

关于 Vite 开发、打包上线的一些思考

从实际使用来看, vite 在一些功能上还是无法完全替代 webpack。

毕竟是后起之秀, 相关的生态还需要持续完善。

个人认为,目前一种比较稳妥的方式是:

  • 保留 webpack dev & build 的能力, vite 仅作为开发的辅助

等相关工具再完善一些, 再考虑完全迁移过来。

相关代码和结论

一个完整的 Vite demo

仓库地址: https://github.com/beMySun/re...

image.png

业务项目的 vite.config.js 完整配置

import { defineConfig } from 'vite';
import reactRefresh from '@vitejs/plugin-react-refresh';
import legacyPlugin from '@vitejs/plugin-legacy';
import { resolve } from 'path';

const fs = require('fs');
const lessToJS = require('less-vars-to-js');
const themeVariables = lessToJS(fs.readFileSync(resolve(__dirname, './src/antd-custom.less'), 'utf8'));
const reactSvgPlugin = require('vite-plugin-react-svg');

// https://cn.vitejs.dev/config/
export default defineConfig({
  base: './',
  root: './',
  resolve: {
    alias: {
      'react-native': 'react-native-web',
      '@': resolve(__dirname, 'src'),
    },
  },
  define: {
    'process.env.REACT_APP_IS_LOCAL': '\'true\'',
    'window.__CID__': JSON.stringify(process.env.cid || 'id'),
  },
  server: {
    port: 8080,
    proxy: {
      '/api': {
        target: 'https://stoku.test.shopee.co.id/',
        changeOrigin: true,
        cookieDomainRewrite: {
          'stoku.test.shopee.co.id': 'localhost',
        },
      },
    },
  },
  build: {
    target: 'es2015',
    minify: 'terser',
    manifest: false,
    sourcemap: false,
    outDir: 'build',
    rollupOptions: {},
  },
  esbuild: {},
  optimizeDeps: {},
  plugins: [
    // viteSingleFile({
    //   title: 'dynamic title', // doesn't work
    // }),
    reactSvgPlugin(),
    reactRefresh(),
    legacyPlugin({
      targets: [
        'Android > 39',
        'Chrome >= 60',
        'Safari >= 10.1',
        'iOS >= 10.3',
        'Firefox >= 54',
        'Edge >= 15',
      ],
    }),
    // vitePluginImp({
    //   libList: [
    //     {
    //       libName: 'antd',
    //       style: (name) => `antd/es/${name}/style`,
    //     },
    //   ],
    // }),
  ],
  css: {
    preprocessorOptions: {
      less: {
        modifyVars: {
          hack: `true;@import '${resolve('./src/vars.less')}';`,
          ...themeVariables,
        },
        javascriptEnabled: true,
      },
    },
  },
});

最后

使用 Vite 能大幅缩短项目构建时间,提升开发效率。

不过也要结合项目的实际情况,合理取舍。

对于我的这个项目而言,把 Vite 作为辅助开发的一种方式,还是挺有用的。

期待 Vite 能继续完善,为研发提效。

好了, 内容大概就这么多, 希望对大家有所帮助。

才疏学浅,如有错误, 欢迎指正。

谢谢。

最后,如果觉得内容有帮助, 可以关注下我的公众号,掌握最新动态,一起学习!

image.png

查看原文

赞 24 收藏 14 评论 0

高阳Sunny 赞了文章 · 4月8日

手把手教你接入微信支付

随着微信小程序的发展,越来越多的移动端应用选择了微信产品做为媒介。无论是公众号开发还是小程序开发,微信支付永远都是绕不开的话题。由于微信支付涉及了很多场景,本文我们只学习如何在公众号、小程序中接入微信支付。

一、微信支付的前提条件

1.1 公众号

微信公众号大体上可以分为服务号和订阅号,订阅号和服务号的具体区别在以前写过的一篇文章Spring Boot开发微信公众号中具体介绍了,这里就不再赘述,总的来说就是服务号提供了更高级的功能。

微信支付接入需要已经完成微信认证的服务号。如果是小程序的话,也需要完成微信认证

公众号可以关联同一主体的10个小程序,不同主体的3个小程序,如果是和公众号同一主体的小程序并且公众号已经完成认证,则直接可以在公众号后台的小程序管理中,进行快速注册并认证,这样就无需重复支付微信认证所需的300RMB了。

1.2 微信商户平台

微信认证完成后,在公众号后台的 微信支付 中开通微信支付功能。提交微信支付申请后,3-5个工作日内,会进行审核,审核通过后会往你填写的邮箱里发送一份包含商户号信息的邮件,同时会往你填写的对公账户中打几毛钱的汇款,需要你查看具体金额后在商户平台中验证。

商户分为普通商户和服务商商户,千万不要申请错了。

普通商户是可以进行交易,但是不能拓展商户。

服务商可以拓展商户,但是不能交易。

服务商就是提供统一的支付入口,它需要绑定具体的普通商户,微信支付时会在支付接口中携带普通商户参数,支付成功后金额会直接到具体的普通商户账户上。

申请时直接申请普通商户就可以了。

1.3 绑定商户

微信支付发起依赖于公众号、小程序等应用与商户号的绑定关系。因此在进行开发前,需要将商户与具体应用进行绑定。

如果商户和需要绑定的AppID是同一主体,只需要以下步骤即可完成绑定。

  • 在商户平台-产品中心-AppID账户管理中关联AppID,输入AppId申请绑定
  • 在公众号或小程序后台微信支付-商户号管理中进行确认。

如果商户和需要绑定的AppID是不同主体,步骤和上述一样,除了输入AppId之外,还需要填入AppId的认证信息。

二、微信支付相关配置

2.1 支付产品类型

1. 付款码支付

用户打开微信钱包-付款码的界面,商户扫码后提交完成支付。

2. JSAPI支付

用户通过微信扫码,关注公众号等方式进入商家H5页面,并在微信内调用JSSDK完成支付。

3. Native支付

用户打开微信扫一扫,扫描商户的二维码后完成支付。

4. APP支付

商户APP中集成微信SDK,用户点击后跳转到微信内完成支付。

5. H5支付

用户在微信以外的手机浏览器请求微信支付的场景唤起微信支付。

6. 小程序支付

用户在微信小程序中使用微信支付的场景。

7. 刷脸支付

无需掏出手机,刷脸完成支付,适合线下各种场景。

在商户平台-产品中心-我的产品中申请开通支付产品。

2.2 支付授权目录配置

在商户平台-产品中心-开发配置中进行支付授权目录的配置(即你开发的下单接口地址),需要注意的是授权目录最多可以配置五个,在开发过程中请合理定义支付接口。

2.3 配置商户密钥

在商户平台-账户中心-API安全中设置API密钥。

第一次设置时,需要安装操作证书,傻瓜式安装,按照提示一步一步操作就可以。

API密钥需要时一个32位的随机字符串,记得不要随意更改API密钥

在微信API v3版本中,除了要配置API密钥外,还需要配置APIv3密钥和申请CA颁发的API证书。

  • API v3密钥主要用于平台证书解密、回调信息解密。
  • API证书用于调用更高级别的api接口,包含退款、红包等接口。

如果使用开源的微信开发包,请了解是否支持v3版本

2.4 配置服务器

在公众号后台-开发-基本配置-服务器配置中启用并填写服务器信息。

2.5 白名单配置

在公众号后台-开发-基本配置-公众号开发信息中配置开发者密钥,同时填写IP白名单。

2.6 JS接口安全域名

在公众号后台-公众号设置-功能设置中设置JS接口安全域名。

上面的配置是基于公众号支付配置的,小程序支付没有这么麻烦,小程序支付不用配置支付授权目录和授权域名。

JSAPI小程序
支付协议HTTP/HTTPSHTTPS
支付目录
授权域名

三、微信支付流程

由于微信升级了API接口,在API v3接口中,需要加载申请的API证书,微信已经封装了相关jar包,并且提供了加载示例,具体可参考“https://pay.weixin.qq.com/wik... v2为例详细学习一下微信接入的主要流程(因为API v3的一些接口还在持续升级,v2接口相对完整)。

上面的这张图片来自微信开发文档,我们详细分析一下支付流程。

3.1 微信下单接口

用户通过微信客户端发起支付,在商戶后台生成订单,然后调用微信下单接口,生成预支付订单,返回订单号!

下单接口涉及到的主要参数,只列举重要的几个参数:

请求参数是否必传类型描述
appidString公众号appid
mch_idString商户号
nonce_strString随机字符串,32位以内
signString签名,默认使用MD5进行加密
out_trade_noString系统内部订单号
total_feeInt订单总金额,单位是分
notify_urlString支付结果通知接口

sign的签名也比较通用,涉及了一个保证签名不可预测的nonce_str

  • 将所有发送的非空参数使用字典排序生成键值对(key1=value1&key2=value2);
  • 将商户平台密钥拼接在上述字符串的最后("String"+&key=密钥);
  • 将上述字符串采用MD5加密

3.2 支付

拉起微信支付,输入密码,完成支付。这一步需要在H5网页中执行JS调起支付。

需要以下参数,因此在预付订单返回和,需要将下列参数封装后响应给页面,由页面完成支付。

参数名是否必传类型描述
appIdString公众号id
timeStampString当前时间戳
nonceStrString随机字符串
packageString预支付订单,格式为prepay_id=*
signTypeString签名类型,默认MD5
paySignString签名

签名和下单接口的签名方式一样。

JS伪代码如下:

function onBridgeReady(){
    WeixinJSBridge.invoke(
        'getBrandWCPayRequest', {
            // 公众号ID,由商户传入
            "appId":"wx2421b1c4370ec43b",
            // 时间戳,自1970年以来的秒数
            "timeStamp":"1395712654",  
            // 随机串
            "nonceStr":"e61463f8efa94090b1f366cccfbbb444",
            "package":"prepay_id=u802345jgfjsdfgsdg888", 
            // 微信签名方式
            "signType":"MD5",
            // 微信签名
            "paySign":"70EA570631E4BB79628FBCA90534C63FF7FADD89"
        },
        function(res){
            if(res.err_msg == "get_brand_wcpay_request:ok" ){
                // 使用以上方式判断前端返回,微信团队郑重提示:
                // res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
            } 
        }); 
}
if (typeof WeixinJSBridge == "undefined"){
    if( document.addEventListener ){
        document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
    }else if (document.attachEvent){
        document.attachEvent('WeixinJSBridgeReady', onBridgeReady); 
        document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
    }
}else{
    onBridgeReady();
}

注意伪代码中的这句话 // res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。为什么这么说呢,我举个例子应该就明白了。假如你去超市买东西,是不是你说支付成功了你就可以把东西带走呢?肯定不是,是当商家收到钱后才算你支付成功,你才可以把东西带走。也就是说,这里提示的成功并不能说一定支付成功了,具体是否成功,微信平台会以异步的方式给你进行通知。

3.3 异步通知

异步通知是比较重要的一步,在这里你可以根据通知结果处理你的业务逻辑。但是,可能会由于网络波动等原因通知不到,或者说微信接收到的响应不符合API的规定,微信会持续发起多次通知(请在回调通知接口中合理处理,避免重复通知造成业务重复处理),直到成功为止,通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m)。但是微信不保证通知最终一定会成功。

异步通知响应参数如下:

参数名是否必传类型描述
return_codeString返回状态码,SUCCESS/FAIL
return_msgString返回信息

如果微信一直通知不成功怎么?还是刚才那个例子,你明明支付成功了,但是商家却一直说她没收到钱,这时候你怎么办?肯定是去看一下她的手机是否真的没有收到钱!这里也一样。

3.4 支付状态查询

  1. 商户APP或者前端页面收到支付返回时,商户需要调用商户查单接口确认订单状态,并把查询结果展示给用户。
  2. 商户后台需要准确、高效地处理微信支付发送的异步支付结果通知,并按接口规范把处理结果返回给微信支付。
  3. 商户后台未收到异步支付结果通知时,商户应该主动调用 微信支付查单接口,同步订单状态。
  4. 商户在T+1日从微信支付侧获取T日的交易账单,并与商户系统中的订单核对。如出现订单在微信支付侧成功,但是在商户侧未成功的情况,商户需要给用户补发货或者退款处理。

四、总结

本文主要以公众号支付为例,总结了接入微信支付需要的相关配置和支付流程。其他支付像APP支付也是开发中比较常见的应用场景,APP支付需要在 微信开放平台 去创建应用来接入微信支付。除此之外,微信支付API在向v3平滑升级,有些接口也还没有升级完成,升级完的接口相较于v2发生了一些数据格式方面的变化。如果引用第三方开发包进行开发,需要注意接口对应的版本。

参考

微信支付开发文档

点关注、不迷路

如果觉得文章不错,欢迎关注、点赞、收藏,你们的支持是我创作的动力,感谢大家。

如果文章写的有问题,请不要吝惜文笔,欢迎留言指出,我会及时核查修改。

如果你还想看到更多别的东西,可以微信搜索「Java旅途」进行关注。回复“手册”领取Java面试手册!

查看原文

赞 10 收藏 4 评论 0

高阳Sunny 赞了文章 · 4月8日

团队协作中,如何写出让同事赞不绝口的代码

团队中的每个人都会用不同的视角来’审视‘你的”作品“,那么我们如何拿出一份像艺术品一样的项目代码,然后赢得得同事们的赞许呢?

作者/ 琼虎(安增平)

编辑/ hjy

00 前言

在加入了拥有较高技术底蕴的有道精品课团队后,发现自己在前面的职业生涯中养成的一些‘作坊’习惯必须得到纠正。

在日常工作中,研发同学只在coding阶段中不需要别人关心自己的代码,其他需要将自己的产出展示给别人的场景变得十分常见。

简单举几个例子:

  • feature准入后,同产品业务线的同事需要trans-review

  • mentor每个季度要Lint-review

  • 测试二轮后要diff-review

  • ......

团队中的每个人都会用不同的视角来“审视”你的”作品“,那么我们如何拿出一份像艺术品一样的项目代码,然后获得同事们的称赞呢?

保持在项目中做到以下几点,便可收获殿堂级的艺术代码。

以下几点是在接手销售转化系统及质检系统等几个项目后,针对自己的不a足和团队成员交流得出的结论。

01 使用meaningful的变量命名

在声明一个变量的时候,尽可能的将其作用和充当的角色注入其中:

声明一个函数,使用组合动词而非名词:

声明一个集合内部包含多项内容的时候,要记得使用复数形式:

在使用数学计算公式的时候尽量提前声明好常量,常量的注入有助于提升你在维护代码阶段的可读性:

在回调函数或者函数声明的形参中,尽量保持形参的语义化,避免后期维护过程中看到前面随意声明的i,j,k后,又要折返到原回调处进行查看,影响开发效率:

(同时在使用TS的过程中也尽量避免使用any类型,使用这种类型在codeReview过程中可能会被灵魂拷问)同时在声明boolean类型的时候要以is作为开头:

做到以上这些,在codeReview中就可以保持一个自信的状态去接受同事们领导们的审阅,因为没有犯低级错误可以让查看你代码的人保持心情愉悦,同时这种心情可以对你产生正反馈。

02 每个函数只做一件事

每个函数尽量保持其职责的单一性,不要出现一个非常强健的函数做了很多事情:

And这种单词本身就不是函数的一部分,他会导致添加过多的业务依赖或职责到当前的函数中,从长远的角度看这绝对是弊大于利的。

03 让函数保持"纯洁"

在函数外的任何东西,任何变量都不是他的业务,所以好的函数应该和函数外的任何变量保持好隔离。

下面这段代码可能只有刚入门的新手才会写出来,但是这种混乱的逻辑在业务复杂了之后,很可能会混入‘你’的代码中:

上面的例子可以改成下面这样:

当然在ES6的使用过程中上述问题普遍已经不存在了,但纯函数的思想需要时刻谨记。

04 模块化业务逻辑

当你在创建了一些函数之后,发现他们在当前的业务中做了一些比较类似的行为。例如,验证用户登陆的用户名和密码,那么我们最好可以将其归类为一个模块中。

这里我们可以称之为验证模块,而不是简单的使用一个util或者server将其集中起来就完事了:

05 简化条件逻辑

如果一个业务中出现了大量的if else这种内容,想必开发人员看到会十分头痛。

举个简单的例子:

仔细看下这里的else其实是不需要的,我们可以通过提前返回来remove掉:

06 enrich u Error log

当我们浏览某个App或网站时,经常会在点击某个按钮弹出“An Error Occurs”这种提示,这种提示很不友好,我们无法排查到底出现了什么原因,用户更是一头雾水,但是假如在出现这种错误的时候将描述信息填充的完整些,对用户或是技术支持都会有一个很棒的使用体验。

例如:当用户在表单中没有输入信息:

当用户此时网络出现了故障:

对开发者而言,一个详尽的提示能让你轻松定位到问题,节省了大量的时间:

包含但不限于这几种错误格式,还有showMessage等方法可以提供......

07 利用好编辑器中的插件

在VSCode下开发的同学,可以通过安装prettier来保持漂亮的代码。同时借助ESLint可以让你在开发时注重缩进、空格这些格式化的内容。

假如在开发过程中注入了TS,那么开启typescript-eslint会帮助你规范自己的类型定义,塑造一个风格严谨的代码style。

借助这些插件让我们的代码格式化时间大大降低,从而我们可以将更多的时间放在提升代码质量上。

08 总结

以上列举的几个例子较为简单。通过这些通俗易懂的例子,大家在工作中根据自己的理解举一反三的运用起来。那便是起到了作用。

在开发中切勿眼高手低,在编码上做到一丝不苟,对我们技术的成长会有很大帮助。

唯有持之以恒,几十年如一日的训练才能见证技术圈的匠人诞生。共勉!

-END-

查看原文

赞 9 收藏 1 评论 1

高阳Sunny 赞了文章 · 4月7日

Google Lyra Android版开源 支持3kbps语音聊天

Google Lyra Android版本今日开源,是否还会引发争议?

最新消息:Google于今日发布了Lyra音频编解码器Android开源版本。

据悉,开源发布的Lyra代码是用C++编写的,以提高速度、效率和互操作性,使用了带有Abseil的Bazel构建框架和GoogleTest框架进行全面的单元测试。

前不久,Google推出的Lyra音频编解码器——是一种用于语音压缩的新型超低比特率编解码器。其在于能给在每秒3kb的网络宽带为用户提供自然清晰的语音聊天,还可在仅90ms延迟的情况下在从高端云服务器到中端智能手机的任何设备上运行。(有关Lyra的详细介绍请浏览:在3kbps的带宽下还能清晰地语音聊天?

对此,我们查阅了Google的官方Lyra推送文章,浏览了文中所载的相同视频分别在原版、Opus@6kbps和Lyra@3kbps带宽下的音频质量对比(如下所示)。通过浏览对比,可以明确感知到Lyra在3kbps带宽情况下,虽然比特率降低为Opus的一半,但其音频的输出质量却优于Opus。我们着实被Lyra的音频输出质量所惊艳。以下分别是原版参考 - Opus@6kbps - Lyra@3kbps 视频中的语音对比:https://ai.googleblog.com/202...

因此我们联系到了负责Lyra音频编解码器研发工作的产品经理Jamieson Brettle以及主管工程师Jan Skoglund进行了一次邮件采访。虽然仅是一次简短的采访,但是从字里行间中可以看出谷歌团队对Lyra音频编解码器的音频生成质量充满了信心。

以下内容由LiveVideoStack与Jamieson和Jan的采访整理而成。

Q 、Lyra是如何做到在比特率低到3kbps的情况下,依然能输出高质量的语音?

首先,Lyra的目的并不是传输语音信号本身,而是提取少量的关键特征,使得模型可以利用这些特征来合成语音。这些特征可以被高度压缩,但不会失去产生高质量语音的能力。

Lyra是用于产生语音信号的,当输入信号中存在语音时,该模型可以很好地进行语音再生。

Q 、Lyra为什么以3kbps为节点?

其实,我们的目标是在传输语音时尽可能地少用数据,并且即使在最恶劣的网络条件下也能进行实时通信。我们也都知道虽然Opus能以6kbps这么低的速率满足在2G网络上进行通信,但是其音频质量却下降了。

而我们对Lyra进行了调整,使其能够在一半的数据(3kbps)下工作,同时在这个非常低的比特率下可以提供比Opus更高的音频质量,并且能够运行在我们的目标设备上。

Q 、Lyra目前的应用范围与具体的应用场景有哪些?

目前,我们专注于确保即使在最糟糕的网络状况下Lyra也能让用户进行高质量的端对端语音通话。

Q 、Lyra的下一步计划是什么?

Lyra的下一步计划是为Android开发一个开源的编解码器,并且我们将开始关注更多的操作系统和硬件目标。我们相信,我们所做的研究作为下一代音频编解码器的基石,可以应用于比实时语音更多的用例。

由此可以看出,Lyra的目标是帮助那些所处网络状态恶劣,2G网络或有线拨号连接的用户提供高质量的语音通话,以满足用户对语音通话的最基本需求。除此之外,据悉Google团队还提出Lyra与AV1相结合,将比VP9的编码效率提高了约40%,并且用户可以通过56kbps的拨号调制解调器连接到互联网实现视频聊天。

但是有媒体也对此提出了质疑:AV1与Lyra的组合对于使用2G网络的手机用户来说是行不通的,因为这类手机无法支持AV1所需的处理。

对此,Google Lyra项目的相关负责人也有回应:Lyra并不要求用户使用的一定是5G智能手机,只是需要有一定计算能力的智能手机。在很多发展中国家市场,我们注意到,这几年即使是中低端手机的计算力也在快速提升。

另外,负责人还提到:在很多欧美市场即使是服务商无限制的数据计划,用户通常每个月只有1GB或2GB的高速数据,之后的网络速度一般会降到64kbps-128kbps, Lyra和AV1对这些用户也会有非常有价值。

对此,仅个人认为:有争议也好,有质疑也罢。对于一个新产品来说都需要经历时间与实践的打磨和积累,用户的使用反馈,才能不断提升。所以不要急,现在Lyra开源只是刚刚开始,我们还会持续关注。

关于Lyra开源详情:

https://opensource.googleblog...

Lyra开源代码:https://github.com/google/lyra

查看原文

赞 1 收藏 0 评论 0

高阳Sunny 关注了用户 · 4月7日

yvonne @yvonne

少年(゚∀゚ )有兴趣来鹅厂吗?欢迎投简历至yvonnexchen@tencent.com

关注 57

高阳Sunny 赞了文章 · 4月7日

一招搞定WonderTrader本地仿真环境

image

前言

  最近群里的小伙伴们都在讨论simnow停用的事情,从3月31日开始,要持续一个半月,不出意外的话也要5月中旬才能恢复。于是很多搞CTA的研发人员可能最近都面临着到哪里去找仿真环境的问题。
  笔者不由得想起来,若干年前市面上还没有simnow的时候,要找一个期货仿真环境真的是很麻烦。一方面要看期货公司是不是部署了仿真环境,只有期货公司有仿真环境才好测试;另一方面,能够交易的交易所和合约也是非常有限的;而且,所有的订单都需要对手盘,不然根本不会撮合,所以在测试的过程中还需要请期货公司的人帮忙下对手单,要不然就只有自己搞自成交了。
  本文主要内容就是介绍如何利用WonderTrader搭建本地仿真环境

TraderMocker简介

  WonderTraderv0.3.6发布的时候,发布过一个TraderMocker模块,基于该模块用户可以非常容易地搭建一个纯本地的仿真环境,而不用依赖任何第三方环境。
  笔者最初在设计TraderMocker的时候,正在给WonderTrader适配股票交易。当时接入的是中泰XTP接口,XTP比较流行也比较容易接入,有互联网的测试环境,API也非常友好。
  不过市面上很多股票测试环境多少都有一些问题,总结下来大概如下:

  • 有些仿真环境是基于撮合的,如果没有对手盘,根本无法成交
  • 有些仿真环境是按几率撮合的,如果下单时没有成交,那就永远不会成交了,不符合正常的撮合逻辑
  • 有些仿真环境可能是没有接入实时行情,所以不管当前什么价格,价格如何变动,成交价都是挂单价

  显然在这样的仿真环境下测试,策略仿真的结果其实是有很大的迷惑性的。在这样的情况下,笔者便决定自己开发一个仿真环境。但是WonderTrader作为一个量化交易框架,本身都是各用户独立部署的,也没有中心化的服务,开发一套C/S的仿真环境,还需要硬件投入。对于WonderTrader这样的开源平台来说,成本太高的话是不现实的。另外仿真环境的部署一般要根据市场、品种分别部署,对于WonderTrader这样面向全市场全品种的平台来说,成本就更是翻了好几倍了。笔者纵然愿意分享源码,但是也没有办法持续性的投入资金去维护这样的仿真服务。基于这些基本考量,TraderMocker这种全本地化的、去中心的仿真模块就应运而生了。

  为了尽量的模拟真实的接口调用,TraderMocker在设计上也有一些特点:

  • 异步执行

    异步执行的主要目的是为了还原真实交易的事件发生顺序。以下单为例,生产环境下,调用下单接口遵循以下数据:下单接口调用->下单结果返回->订单回报->成交回报。如果不采用异步执行的机制,那么就会出现下单接口还没有返回的时候,已经收到订单回报和成交回报了,这样就不符合生产环境的真实场景了。
  • 根据价格撮合

    TraderMocker自然不可能是一个完全的仿真环境,所以撮合的机制也相对简单。但是为了尽量模拟实盘环境,TraderMocker会严格按照价格优先的机制进行撮合。这里的撮合,指的是不需要对手盘的撮合,即只要价格条件满足,即直接撮合成交,推送订单回报和成交回报。
  • 支持不同的品种

    TraderMocker作为一个辅助的简化的本地仿真模块,要充分考虑对不同的品种的支持。这样才能增加TraderMocker的应用场景。

  基于以上的设计原则,TraderMocker也表现出一些特点:

  • 仿真的程度有限

    TraderMocker毕竟不是真正的撮合系统,只是利用行情对订单做一个仿真处理。而且接入的行情,就算是期货,也是500ms一笔的快照。另外,考虑到不同行情源档位不同,所以撮合处理的时候只利用了买一卖一的数据。TraderMocker对订单之间的竞争也不考虑进去,统一按照最新的tick的委托量进行处理。
  • 不适合大单测试

    TraderMocker并没有处理订单对限价订单簿的冲击。主要考虑到期货行情只有一档,无法进行冲击的处测算,所以就简化了。所以当一个订单的委托价格高于对手价的时候,一次撮合不完会等到下一笔tick进来再利用对手价进行撮合,这显然不大符合真实场景。因此大单仿真的还原程度会更低,参考性也更低了。
  • 不做验资操作

    TraderMocker因为其特殊性,不会进行资金检查,只要合法的订单都能下单成功。一方面是因为TraderMocker是为通用性设计的,所以无法兼顾所有的币种,如果增加验资的机制会增加复杂性。另外一方面,WonderTrader1+N执行架构,实际上是隔绝了策略对资金的关注,即使做验资,也无法将问题传递给策略。
  • 不做结算处理

    TraderMocker为了简化处理,不会进行结算处理。如果引入结算机制,会增加复杂性,而且还会要求仿真器一直在线直到接收到结算价为止。但是在策略的实操中,其实都主要关心的还是进场价和出厂价,结算的意义对于策略来说并不大。不过如果不结算,所有的持仓都是今仓,因此对于有些品种来说,仿真环境中的手续费的设置就需要把平今设置为跟平昨一致。
  • 订单都在内存中

    对于TraderMocker这样的简易仿真模块,只会把必要的数据落地。目前TraderMocker只会保存持仓数据,而订单数据和成交数据都只在内存中。一单平台重启,订单和成交就都都没有了。
  • 交易指令简化

    TraderMocker为了兼容不同的品种,所以只能实现通用的指令,即买卖和撤单的指令。其他的特殊指令就不支持了,比如ETF申赎、期权的报价和执行等指令。

  当然TraderMocker只是笔者仓促写出来的一个简易的仿真模块,有很多仿真功能因为使用场景有限,并没有完全实现。如果各位读者有兴趣的话,可以自行根据自己的需求进行完善。到时候如果愿意分享给大家的话,也可以提交一个PR

如何搭建本地仿真环境

  前文介绍了一下TraderMocker模块,下面本文就将介绍如何在利用WonderTrader搭建这样的本地仿真环境。搭建本地仿真环境,需要用到datakit_futhft_fut_mocker两个demo。这两个demo笔者已经提交到wtpy/demos下,有需要的读者可以自行获取,如果master分支没有的话,请到dev分支下载即可。

  • 行情配置
    datakit_fut是通过CTP接口落地行情数据的数据组件demo,基本配置如下:

    {
    "basefiles":{
        "session":"./common/sessions.json",
        "commodity":"./common/commodities.json",
        "contract":"./common/contracts.json",
        "holiday":"./common/holidays.json"
    },
    "writer":{
        "path":"./FUT_Data/",
        "savelog":false,
        "async":false,
        "groupsize":20
    },
    "parsers":[
        {
            "active":true,
            "module":"ParserCTP.dll",
            "front":"tcp://180.168.146.187:10111",
            "broker":"9999",
            "user":"你的SIMNOW账号",
            "pass":"你的SIMNOW密码",
            "code":"CFFEX.IF2005,SHFE.au2012"
        }
    ],
    "broadcaster":{
        "active":true,
        "bport":3997,
        "broadcast":[
            {
                "host":"255.255.255.255",
                "port":9001,
                "type":2
            }
        ]
    }

在使用的时候,将parsers小节的CTP前置和账号密码改成生产环境,并将code改成自己需要的合约代码进行订阅,然后启动runDT.py就可以正常运行了。
行情组件运行截图

  • 仿真配置
    hft_fut_mocker则是从UDP广播通道接入行情,并调用TraderMocker进行仿真,配置如下:

    {
    "basefiles":{
        "session":"./common/sessions.json",
        "commodity":"./common/commodities.json",
        "contract":"./common/contracts.json",
        "holiday":"./common/holidays.json",
        "hot":"./common/hots.json"
    },
    "env":{
        "name":"hft",
        "mode": "product",
        "product":{
            "session":"TRADING"
        },
        "filters":"filters.json",
        "fees":"fees.json",
    },
    "data":{
        "store":{
            "path":"./FUT_Data/"
        }
    },
    "traders":[
        {
            "active":true,
            "id":"mocker",
            "module":"TraderMocker.dll",
            "front":"mocker://localhost",
            "mockerid":9999,
            "span":100,
            "newpx":true,
            "maxqty":100,
            "minqty":1,
            "user":"mocker9999",
            "udp_port":9001,
            "savedata":true
        }
    ],
    "parsers":[
        {
            "active":true,
            "id":"parser1",
            "module":"ParserUDP.dll",
            "host":"127.0.0.1",
            "bport":9001,
            "sport":3997,
            "filter":""
        }
    ],
    "bspolicy":"actpolicy.json"
    }

从上面的配置可以看出,TraderMocker仿真器,要从udp广播通道接收最新的行情,才能进行撮合。可能有人会有疑问:为什么不从行情通道通过WonderTrader直接向TraderMocker传递行情数据呢?其实也很好解释,因为Trader模块解耦以后,WonderTrader只和Trader交互交易数据,而行情数据,不在接口支持的数据范围内,所以TraderMocker只能自己解决行情接入的问题。因此TraderMocker自然就会依赖数据伺服组件提供的行情广播服务了。

配置修改好了以后,再检查一下策略启动入口:

from wtpy import WtEngine,EngineType
from strategies.HftStraDemo import HftStraDemo

if __name__ == "__main__":
    #创建一个运行环境,并加入策略
    engine = WtEngine(EngineType.ET_HFT)
    engine.init('./common/', "config.json")
    engine.commitConfig()

    straInfo = HftStraDemo(name="hft_IF", code="CFFEX.IF.2104", expsecs=5, offset=100, freq=0)
    engine.add_hft_strategy(straInfo, 'mocker')

    engine.run()

    kw = input('press any key to exit\n')

最后,运行run.py就可以正常进行仿真测试了,如图:
仿真运行截图
如果有需要进行股票仿真的,只需要修改./common目录下相应的基础文件,并修改配置文件中的行情接入模块的配置文件即可。如有不明白的地方,读者也可以私信咨询笔者。

结束语

  如何利用WonderTrader的仿真模块TraderMocker来搭建本地的仿真环境,总结下来就是两个步骤:先运行一个数据伺服,再运行仿真环境。简单的两个步骤,就可以搞定一个仿真环境。相信对于有需要的人来说,这是一个很轻松的事情。
  对于想用simnow的用户来说,在simnow缺位的这段时间,只需要一个CTPs实盘账号即可进行仿真测试。对于那些不满足于券商提供的股票仿真环境的人来说,本文介绍的这个攻略也许可以提升你仿真测试的效率。
  当然,笔者水平有限,TraderMocker的开发也比较仓促,难免有很多错漏之处,各位读者在使用的时候还需要自行判断一下是否满足自己的需求。另外,仿真毕竟不是实盘,策略的表现是否合理还需要各位仔细辨别。

  最后再安利一下WonderTrader
  WonderTrader旨在给各位量化从业人员提供更好的轮子,将技术相关的东西都封装在平台中,力求给策略研发带来更好的策略开发体验。

WonderTradergithub地址:https://github.com/wondertrad...

WonderTrader官网地址:https://wondertrader.github.io

wtpygithub地址:https://github.com/wondertrad...


市场有风险,投资需谨慎。以上陈述仅作为对于历史事件的回顾,不代表对未来的观点,同时不作为任何投资建议。

查看原文

赞 1 收藏 0 评论 0

高阳Sunny 赞了文章 · 4月7日

Android 高频面试之必考Java基础

如果大家去面Android客户端岗位,那么必问Java基础和Kotlin基础,所以,我打算花3,4篇文章的样子来给大家总结下Android面试中会问到的一些Java基础知识。

1,面向对象和面向过程的区别

面向过程:面向过程性能比面向对象高。因为对象调用需要实例化,开销比较大,较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等,一般采用面向过程开发。但是,面向过程没有面向对象易维护、易复用、易扩展。
面向对象:面向对象易维护、易复用、易扩展。因为面向对象有封装、继承、多态性的特性,所以可设计出低耦合的系统,使得系统更加灵活、更加易于维护。

那为什么,面向过程性能比面向对象高呢?
面向过程也需要分配内存,计算内存偏移量,Java 性能差的主要原因并不是因为它是面向对象语言,而是因为 Java 是半编译语言,最终的执行代码并不是可以直接被 CPU 执行的二进制机器码。而面向过程语言大多都是直接编译成机器码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比 Java 好。

2,面向对象的特征有哪些

  • 封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。
  • 继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。
  • 抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
  • 多态性:多态性是指允许不同子类型的对象对同一消息作出不同的响应。即同一消息可以根据发送对象的不同而采取不同的行为方式。

3,解释下Java的编译与解释并存的现象

当 .class 字节码文件通过 JVM 转为机器可以执行的二进制机器码时,JVM 类加载器首先加载字节码文件,然后通过解释器逐行进行解释执行,这种方式的执行速度相对比较慢。而且有些方法和代码块是反复被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成一次编译后,会将字节码对应的机器码保存下来,下次可以直接调用。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。

4,简单介绍下JVM的内存模型

Java虚拟机所管理的内存包含程序计数器、Java虚拟机栈、本地方法栈、Java堆和方法区5个部分,模型图如下图所示。
在这里插入图片描述

4.1 程序计数器

由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器只会执行一条线程中的指令。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各个线程之间的计数器互不影响,独立存储,这类内存区域为【线程私有】的内存。

程序计数器具有如下的特点:

  • 是一块较小的内存空间。
  • 线程私有,每条线程都有自己的程序计数器。
  • 生命周期方面,随着线程的创建而创建,随着线程的结束而销毁。
  • 是唯一一个不会出现OutOfMemoryError的内存区域。

4.2 Java虚拟机栈

Java虚拟机栈也是线程私有的,它的生命周期与线程的生命周期同步,虚拟机栈描述的是Java方法执行的线程内存模型。每个方法被执行的时候,Java虚拟机都会同步创建一个内存块,用于存储在该方法运行过程中的信息,每个方法被调用的过程都对应着一个栈帧在虚拟机中从入栈到出栈的过程。
在这里插入图片描述

Java虚拟机栈有如下的特点:

  • 局部变量表所需的内存空间在编译期间完成分配,进入一个方法时,这个方法需要在栈帧中分配的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
  • Java虚拟机栈会出现两种异常:StackOverflowError 和 OutOfMemoryError。

4.3 本地方法栈

本地方法栈与虚拟机所发挥的作用很相似,区别在于虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的本地方法服务。

4.4 Java堆

Java堆是虚拟机所管理的内存中最大的一块,Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
此内存区域的唯一目的就是存放对象实例,java中“几乎”所有的对象实例都在这里分配内存。这里使用“几乎”是因为java语言的发展,及时编译的技术发展,逃逸分析技术的日渐强大,栈上分配、标量替换等优化手段,使java对象实例都分配在堆上变得不那么绝对。
Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法(G1之后开始变得不一样,引入了region,但是依旧采用了分代思想),Java堆中还可以细分为:新生代和老年代。再细致一点的有Eden空间、From Survivor空间、ToSurvivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,简写TLAB)。

OOM异常
Java堆的大小既可以固定也可以扩展,但是主流的虚拟机,堆的大小都是支持扩展的。如果需要线程请求分配内存,但堆已满且内存已无法再扩展时,就抛出 OutOfMemoryError 异常。比如:

/**
 * VM Args:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
 */
public class HeapOOMTest {

    public static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        List<Integer[]> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Integer[] ints = new Integer[2 * _1MB];
            list.add(ints);
        }
    }
}

4.5 方法区

方法区和Java堆一样,是各个线程共享的内存区域,他用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

在 HotSpot JVM 中,永久代(永久代实现方法区)中用于存放类和方法的元数据以及常量池,比如Class和Method。每当一个类初次被加载的时候,它的元数据都会放到永久代中。永久代是有大小限制的,因此如果加载的类太多,很有可能导致永久代内存溢出,为此我们不得不对虚拟机做调优。

后来HotSpot放弃永久代(PermGen),jdk1.7版本中,HotSpot已经把原本放在永久代的字符串常量池、静态变量等移出,到了jdk1.8,完全废弃了永久代,方法区移至元空间(Metaspace)。比如类元信息、字段、静态属性、方法、常量等都移动到元空间区。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。

常用的JVM调参如下表:

参数作用描述
-XX:MetaspaceSize分配给Metaspace(以字节计)的初始大小。如果不设置的话,默认是20.79M,这个初始大小是触发首次 Metaspace Full GC 的阈值,例如 -XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize分配给Metaspace 的最大值,超过此值就会触发Full GC,此值默认没有限制,但应取决于系统内存的大小。JVM会动态地改变此值。但是线上环境建议设置,例如-XX:MaxMetaspaceSize=256M
-XX:MinMetaspaceFreeRatio最小空闲比,当 Metaspace 发生 GC 后,会计算 Metaspace 的空闲比,如果空闲比(空闲空间/当前 Metaspace 大小)小于此值,就会触发 Metaspace 扩容。默认值是 40 ,也就是 40%,例如 -XX:MinMetaspaceFreeRatio=40
-XX:MaxMetaspaceFreeRatio最大空闲比,当 Metaspace 发生 GC 后,会计算 Metaspace 的空闲比,如果空闲比(空闲空间/当前 Metaspace 大小)大于此值,就会触发 Metaspace 释放空间。默认值是 70 ,也就是 70%,例如 -XX:MaxMetaspaceFreeRatio=70

运行时常量池
运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期间生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
方法区中存放:类信息、常量、静态变量、即时编译器编译后的代码。常量就存放在运行时常量池中。
当类被 Java 虚拟机加载后, .class文件中的常量就存放在方法区的运行时常量池中。而且在运行期间,可以向常量池中添加新的常量。如String类的intern()方法就能在运行期间向常量池中添加字符串常量。

4.6 直接内存

直接内存并不是虚拟机运行时数据区的组成部分,在 NIO 中引入了一种基于通道和缓冲的 IO 方式。它可以通过调用本地方法直接分配Java虚拟机之外的内存,然后通过一个存储在堆中的DirectByteBuffer对象直接操作该内存,而无须先将外部内存中的数据复制到堆中再进行操作,从而提高了数据操作的效率。

由于直接内存并非Java虚拟机的组成部分,因此直接内存的大小不受 Java 虚拟机控制,但既然是内存,如果内存不足时还是会抛出OutOfMemoryError异常。

下面是直接内存与堆内存的一些异同点:

  • 直接内存申请空间耗费更高的性能;
  • 直接内存读取 IO 的性能要优于普通的堆内存。
  • 直接内存作用链: 本地 IO -> 直接内存 -> 本地 IO
  • 堆内存作用链:本地 IO -> 直接内存 -> 非直接内存 -> 直接内存 -> 本地 IO

服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常。

5,简单介绍下Java的类加载器

Java的类加载器可以分为BootstrapClassLoader、ExtClassLoader和AppClassLoader,它们的作用如下。

  • BootstrapClassLoader:Bootstrap 类加载器负责加载 rt.jar 中的 JDK 类文件,它是所有类加载器的父加载器。Bootstrap 类加载器没有任何父类加载器,如果调用String.class.getClassLoader(),会返回 null,任何基于此的代码会抛出 NUllPointerException 异常,因此Bootstrap 加载器又被称为初始类加载器。
  • ExtClassLoader:Extension 将加载类的请求先委托给它的父加载器,也就是Bootstrap,如果没有成功加载的话,再从 jre/lib/ext 目录下或者 java.ext.dirs 系统属性定义的目录下加载类。Extension 加载器由 sun.misc.Launcher$ExtClassLoader 实现。
  • AppClassLoader:Java默认的加载器就是 System 类加载器,又叫作 Application 类加载器。它负责从 classpath 环境变量中加载某些应用相关的类,classpath 环境变量通常由 -classpath 或 -cp 命令行选项来定义,或者是 JAR 中的 Manifest 的 classpath 属性,Application 类加载器是 Extension 类加载器的子加载器。

类加载会涉及一些加载机制。

  • 委托机制:加载任务委托交给父类加载器,如果不行就向下传递委托任务,由其子类加载器加载,保证Java核心库的安全性。
  • 可见性机制:子类加载器可以看到父类加载器加载的类,而反之则不行。
  • 单一性原则:父加载器加载过的类不能被子加载器加载第二次。

6,谈一下Java的垃圾回收,以及常用的垃圾回收算法。

Java的内存管理主要涉及三个部分:堆 ( Java代码可及的 Java堆 和 JVM自身使用的方法区)、栈 ( 服务Java方法的虚拟机栈 和 服务Native方法的本地方法栈 ) 和 保证程序在多线程环境下能够连续执行的程序计数器。
Java堆是进行垃圾回收的主要区域,故其也被称为GC堆;而方法区的垃圾回收主要针对的是新生代和中生代。总的来说,堆 (包括Java堆 和 方法区)是 垃圾回收的主要对象,特别是Java堆。

6.1 垃圾回收算法

6.1.1 对象存活判断

引用计数

每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法虽然简单,但无法解决对象相互循环引用的问题。

可达性分析

从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。在Java中,GC Roots包括:

  • 虚拟机栈中引用的对象。
  • 方法区中类静态属性实体引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中 JNI 引用的对象。

    6.2 垃圾收集算法

    标记清除法

如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。
标记复杂算法有两个主要的缺点:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法

复制的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
它的优点是每次只需要对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。而缺点也是显而易见的,内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。

标记整理法

复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法
分代收集算法,就是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

7,成员变量和局部变量的区别

  • 从语法形式上看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public、private、static 等修饰符所修饰,而局部变量不能被这些修饰符所修饰;但是它们都可以被 final 所修饰。
  • 从变量在内存中的存储方式来看:如果成员变量被 static 所修饰,那么这个成员变量属于类,如果没有被 static 修饰,则该成员变量属于对象实例。对象存在于堆内存,局部变量存在于栈内存(具体是Java虚拟机栈)。
  • 从变量在内存中的生存时间来看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用结束而自动消失。
  • 成员变量如果没有赋初始值,则会自动以类型的默认值而赋值(例外:被 final 修饰的成员变量必须在初始化时赋值),局部变量则不会自动赋值。

8,Java 中的方法重写(Overriding)和方法重载(Overload)的含义

方法重写
在Java程序中,类的继承关系可以产生一个子类,子类继承父类,它具备了父类所有的特征,继承了父类所有的方法和变量。子类可以定义新的特征,当子类需要修改父类的一些方法进行扩展,增大功能,程序设计者常常把这样的一种操作方法称为重写,也叫称为覆写或覆盖。

方法重写有如下一些特点:

  • 方法名,参数列表必须相同,返回类型可以相同也可以是原类型的子类型
  • 重写方法不能比原方法访问性差(即访问权限不允许缩小)。
  • 重写方法不能比原方法抛出更多的异常。
  • 重写发生在子类和父类之间。
  • 重写实现运行时的多态性。

方法重载
方法重载是让类以统一的方式处理不同类型数据的一种手段。调用方法时通过传递给它们的不同个数和类型的参数来决定具体使用哪个方法,这就是多态性。所谓方法重载是指在一个类中,多个方法的方法名相同,但是参数列表不同。参数列表不同指的是参数个数、参数类型或者参数的顺序不同。

  • 方法名必须相同,参数列表必须不同(个数不同、或类型不同、参数类型排列顺序不同等)。
  • 方法的返回类型可以相同也可以不相同。
  • 重载发生在同一类中。
  • 重载实现编译时的多态性。

9,简单介绍下传递和引用传递

按值传递:值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。简单来说就是直接复制了一份数据过去,因为是直接复制,所以这种方式在传递时如果数据量非常大的话,运行效率自然就变低了,所以Java在传递数据量很小的数据是值传递,比如Java中的各种基本类型:int、float、double、boolean等类型。

引用传递:引用传递其实就弥补了上面说的不足,如果每次传参数的时候都复制一份的话,如果这个参数占用的内存空间太大的话,运行效率会很底下,所以引用传递就是直接把内存地址传过去,也就是说引用传递时,操作的其实都是源数据,这样的话修改有时候会冲突,记得用逻辑弥补下就好了,具体的数据类型就比较多了,比如Object,二维数组,List,Map等除了基本类型的参数都是引用传递。

10,为什么重写 equals 时必须重写 hashCode 方法

下面是使用hashCode()与equals()的相关规定:

  • 如果两个对象相等(即用 equals 比较返回 true),则 hashcode 一定也是相同的;
  • 两个对象有相同的 hashcode 值,它们也不一定是相等的(不同的对象也可能产生相同的 hashcode,概率性问题);
  • equals 方法被覆盖过,则 hashCode 方法也必须被覆盖。

为什么必须要重写 hashcode 方法?其实就是为了保证同一个对象,保证在 equals 相同的情况下 hashcode 值必定相同,如果重写了 equals 而未重写 hashcode 方法,可能就会出现两个没有关系的对象 equals 相同的(因为 equals 都是根据对象的特征进行重写的),但 hashcode 确实不相同的。

11,接口和抽象类的区别和相同点是什么

相同点

  • 接口是绝对抽象的,不可以被实例化,抽象类也不可以被实例化。
  • 类可以不实现抽象类和接口声明的所有方法,当然,在这种情况下,类也必须得声明成是抽象的。

异同点:

  • 从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
  • 定义接口的关键字是 interface ,抽象类的关键字是 abstract class
  • 接口中所有的方法隐含的都是抽象的。而抽象类则可以同时包含抽象和非抽象的方法。
  • 类可以实现很多个接口,但是只能继承一个抽象类,接口可以继承多个接口
  • Java 接口中声明的变量默认都是 public static final 的。抽象类可以包含非 final 的变量。
  • 在JDK1.8之前,接口中不能有静态方法,抽象类中可以有普通方法和静态方法;在 JDK1.8后,接口中可以有默认方法和静态方法,并且有方法体。
  • 抽象类可以有构造方法,但是不能直接被 new 关键字实例化。
  • 在 JDK1.8 前,抽象类的抽象方法默认访问权限为 protected,1.8默认访问权限为 default,共有 default,protected 、 public 三种修饰符,非抽象方法可以使用四种修饰符;在 JDK1.8 前,接口方法默认为 public,1.8时默认为 public,此时可以使用 public 和 default,1.9时接口方法还支持 private。

12,简述下HashMap

HashMap底层采用了数组+链表的数据结构,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。

如果定位到的数组位置不含链表,那么执行查找、添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

HashMap有4个构造器,其他构造器如果用户没有传入initialCapacity 和loadFactor这两个参数,会使用默认值initialCapacity默认为16,loadFactory默认为0.75。

public HashMap(int initialCapacity, float loadFactor) {
     //此处对传入的初始容量进行校验,最大不能超过MAXIMUM_CAPACITY = 1<<30(230)
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        this.loadFactor = loadFactor;
        threshold = initialCapacity;
     
        init();//init方法在HashMap中没有实际实现,不过在其子类如 linkedHashMap中就会有对应实现
    }

加载因子存在的原因,还是因为减缓哈希冲突,如果初始桶为16,等到满16个元素才扩容,某些桶里可能就有不止一个元素了。所以加载因子默认为0.75,也就是说大小为16的HashMap,到了第13个元素,就会扩容成32。

Put过程

  • 判断当前数组是否要初始化。
  • 如果key为空,则put一个空值进去。
  • 根据key计算出hashcode。
  • 根据hsahcode定位出在桶内的位置。
  • 如果桶是链表,则需要遍历判断hashcode,如果key和原来的key是否相等,相等则进行覆盖,返回原来的值。
  • 如果桶是空的,说明当前位置没有数据存入,新增一个 Entry 对象写入当前位置.当调用 addEntry 写入 Entry 时需要判断是否需要扩容。如果需要就进行两倍扩充,并将当前的 key 重新 hash 并定位。而在 createEntry中会将当前位置的桶传入到新建的桶中,如果当前桶有值就会在位置形成链表。

Get过程

  • 根据key计算出hashcode,并定位到桶内的位置。
  • 判断是不是链表,如果是则需要根据遍历直到 key 及 hashcode 相等时候就返回值,如果不是就根据 key、key 的 hashcode 是否相等来返回值。
  • 如果啥也没取到就返回null。

JDK 1.8的HashMap底层采用的是链表+红黑树,增加一个阈值进行判断是否将链表转红黑树,HashEntry 修改为 Node,目的是解决hash冲突造成的链表越来越长、查询慢的问题。

Get过程

  • 判断当前桶是不是空,空就需要初始化;
  • 根据key,计算出hashcode,根据hashcode,定位到具体的桶中,并判断当前桶是不是为空,为空表明没有hsah冲突创建一个新桶即可;
  • 如果有hash冲突,那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进行赋值及返回;
  • 如果当前位置是红黑树,就按照红黑树的方式写入数据;
  • 如果当前位置是链表,则需要把key,value封装一个新的节点,添加到当前的桶后面(尾插法),形成链表;
  • 接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树;
  • 如果在遍历过程中找到 key 相同时直接退出遍历;
  • 如果 e != null 就相当于存在相同的 key,那就需要将值覆盖;
  • 最后判断是否需要进行扩容;

Get过程

  • 首先将 key hash 之后取得所定位的桶。
  • 如果桶为空则直接返回 null 。
  • 否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。
  • 如果第一个不匹配,则判断它的下一个是红黑树还是链表。
  • 红黑树就按照树的查找方式返回值。
  • 不然就按照链表的方式遍历匹配返回值。

13, CurrentHashMap

JDK8中ConcurrentHashMap参考了JDK8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作,那什么是CAS。

CAS是compare and swap的缩写,中文称为【比较交换】。CAS是一种基于锁的操作,而且是乐观锁。在Java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,性能较悲观锁有很大的提高。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,如果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。

14,介绍下什么是乐观锁、悲观锁

Java 按照锁的实现分为乐观锁和悲观锁,乐观锁和悲观锁并不是一种真实存在的锁,而是一种设计思想。

悲观锁
悲观锁是一种悲观思想,它总认为最坏的情况可能会出现,它认为数据很可能会被其他人所修改,所以悲观锁在持有数据的时候总会把资源 或者 数据 锁住,这样其他线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。悲观锁的实现往往依靠数据库本身的锁功能实现。

Java 中的 Synchronized 和 ReentrantLock 等独占锁(排他锁)也是一种悲观锁思想的实现,因为 Synchronzied 和 ReetrantLock 不管是否持有资源,它都会尝试去加锁,生怕自己心爱的宝贝被别人拿走。

乐观锁
乐观锁的思想与悲观锁的思想相反,它总认为资源和数据不会被别人所修改,所以读取不会上锁,但是乐观锁在进行写入操作的时候会判断当前数据是否被修改过(具体如何判断我们下面再说)。乐观锁的实现方案一般来说有两种: 版本号机制 和 CAS实现 。乐观锁多适用于多度的应用类型,这样可以提高吞吐量。

在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

15,谈谈对Java线程的理解

线程是进程中可独立执行的最小单位,也是 CPU 资源(时间片)分配的基本单位,同一个进程中的线程可以共享进程中的资源,如内存空间和文件句柄。线程有一些基本的属性,如id、name、以及priority。
id:线程 id 用于标识不同的线程,编号可能被后续创建的线程使用,编号是只读属性,不能修改。
name:线程的名称,默认值是 Thread-(id)
daemon:分为守护线程和用户线程,我们可以通过 setDaemon(true) 把线程设置为守护线程。守护线程通常用于执行不重要的任务,比如监控其他线程的运行情况,GC 线程就是一个守护线程。setDaemon() 要在线程启动前设置,否则 JVM 会抛出非法线程状态异常,可被继承。
priority:线程调度器会根据这个值来决定优先运行哪个线程(不保证),优先级的取值范围为 1~10,默认值是 5,可被继承。Thread 中定义了下面三个优先级常量:

  • 最低优先级:MIN_PRIORITY = 1
  • 默认优先级:NORM_PRIORITY = 5
  • 最高优先级:MAX_PRIORITY = 10

一个线程被创建后,会经历从创建到消亡的状态,下图是线程状态的变更过程。
在这里插入图片描述
下表是展示了线程的生命周期状态变化:

状态说明
New新创建了一个线程对象,但还没有调用start()方法。
RunnableReady 状态 线程对象创建后,其他线程(比如 main 线程)调用了该对象的 start() 方法。该状态的线程位于可运行线程池中,等待被线程调度选中 获取 cpu 的使用权。Running 绪状态的线程在获得 CPU 时间片后变为运行中状态(running)。
Blocked线程因为某种原因放弃了cpu 使用权(等待锁),暂时停止运行。
Waiting线程进入等待状态因为以下几个方法: Object#wait()、 Thread#join()、 LockSupport#park()
Terminated该线程已经执行完毕。

16, Synchronized、volatile、Lock并发

线程同步和并发通常会问到Synchronized、volatile、Lock的作用。其中,Lock是一个类,而其余两个则是Java关键字。

Synchronized

Synchronized是Java的关键字,也是Java的内置特性,在JVM层面实现了对临界资源的同步互斥访问,通过对对象的头文件来操作,从而达到加锁和释放锁的目的。使用Synchronized修饰的代码或方法,通常有如下特性:

  • Synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生。
  • 不能响应中断。
  • 同一时刻不管是读还是写都只能有一个线程对共享资源操作,其他线程只能等待,性能不高。

正是因为上面的特性,所以Synchronized的缺点也是显而易见的:即如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,因此效率很低。

volatile
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。并且volatile是禁止进行指令重排序。

所谓指令重排序,指的是处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

volatile为了保证原子性,必须具备以下条件:

  • 对变量的写操作不依赖于当前值
  • 该变量没有包含在具有其他变量的不变式中

17,锁

按照作用的不同,Java的锁可以分为如下:
在这里插入图片描述

悲观锁、乐观锁

悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java 中,synchronized 关键字和 Lock 的实现类都是悲观锁。悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。

而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。乐观锁在 Java 中是通过使用无锁编程来实现,最常采用的是 CAS 算法,Java 原子类中的递增操作就通过 CAS 自旋实现。乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

这里说到了CAS算法,那么什么是CAS算法呢?

CAS算法

一个线程失败或挂起并不会导致其他线程也失败或挂起,那么这种算法就被称为非阻塞算法。而CAS就是一种非阻塞算法实现,也是一种乐观锁技术,它能在不使用锁的情况下实现多线程安全,因此是一种无锁算法。

CAS算法的定义:CAS的主要作用是不使用加锁就可以实现线程安全,CAS 算法又称为比较交换算法,是一种实现并发算法时常用到的技术,Java并发包中的很多类都使用了CAS技术。CAS具体包括三个参数:当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。

原子更新的基本操作包括:

  • AtomicBoolean:原子更新布尔变量;
  • AtomicInteger:原子更新整型变量;
  • AtomicLong:原子更新长整型变量;

以AtomicInteger为例,代码如下:

public class AtomicInteger extends Number implements java.io.Serializable {
     //返回当前的值
     public final int get() {
         return value;
     }
     //原子更新为新值并返回旧值
     public final int getAndSet(int newValue) {
         return unsafe.getAndSetInt(this, valueOffset, newValue);
     }
     //最终会设置成新值
     public final void lazySet(int newValue) {
         unsafe.putOrderedInt(this, valueOffset, newValue);
     }
     //如果输入的值等于预期值,则以原子方式更新为新值
     public final boolean compareAndSet(int expect, int update) {
         return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
     }
     //原子自增
     public final int getAndIncrement() {
         return unsafe.getAndAddInt(this, valueOffset, 1);
     }
     //原子方式将当前值与输入值相加并返回结果
     public final int getAndAdd(int delta) {
         return unsafe.getAndAddInt(this, valueOffset, delta);
     }
 }

再如,下面是使用多线程对一个int值进行自增操作的代码,如下所示。

public class AtomicIntegerDemo {

    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    public static void main(String[] args){
        for (int i = 0; i < 5; i++){
            new Thread(new Runnable() {
                public void run() {
                    //调用AtomicInteger的getAndIncement返回的是增加之前的值
                     System.out.println(atomicInteger.getAndIncrement());
                }
            }).start();
        }
        System.out.println(atomicInteger.get());
    }
}

自旋锁、适应性自旋锁

阻塞或唤醒一个 Java 线程需要操作系统切换 CPU 状态来完成,这种状态转换需要耗费处理器时间。在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。

而为了让当前线程【稍等一下】,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销,这就是自旋锁。

死锁

当前线程拥有其他线程需要的资源,当前线程等待其他线程已拥有的资源,都不放弃自己拥有的资源。

18,谈谈你对Java 反射的理解

所谓反射,指的是在运行状态中,对于任意一个类,都能够获取这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性,而这种动态获取的信息以及动态调用对象的方法的功能就被称为Java语言的反射机制。

使用反射前需要事先获取到的字节码,在Java中,获取字节码的方式有三种:

  1. Class.forName(className)
  2. 类名.class
  3. this.getClass()

19, 注解

Java 语言中的类、方法、变量、参数和包等都可以被标注。和 Javadoc 不同,Java 标注可以通过反射获取标注内容。根据作用时机的不同,Java的注解可以分为三种:

  • SOURCE:注解将被编译器丢弃(该类型的注解信息只会保留在源码里,源码经过编译后,注解信息会被丢弃,不会保留在编译好的class文件里),如 @Override。
  • CLASS:注解在class文件中可用,但会被 VM 丢弃(该类型的注解信息会保留在源码里和 class 文件里,在执行的时候,不会加载到虚拟机中),请注意,当注解未定义 Retention 值时,默认值是 CLASS。
  • RUNTIME:注解信息将在运行期 (JVM) 也保留,因此可以通过反射机制读取注解的信息(源码、class 文件和执行的时候都有注解的信息),如 @Deprecated。

20,单例

为了保证只有一个对象存在,可以使用单例模式,网上有,单例模式的七种写法。我们介绍一下常见的几种:

懒汉式
懒汉式使用的是static关键字,因此是线程不安全的。

public class Singleton {  
     private static Singleton instance;  
     private Singleton (){}   
     public static Singleton getInstance() {  
     if (instance == null) {  
        instance = new Singleton();  
     }  
    return instance;  
     }  
 }  

如果要线程安全,那么需要使用synchronized关键字。

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}
    public static synchronized Singleton getInstance() {  
    if (instance == null) {  
       instance = new Singleton();  
     }  
   return instance;  
     }  
 }   

不过,使用synchronized锁住之后,运行效率明显降低。

静态内部类
静态内部类利用了classloder的机制来保证初始化instance时只有一个线程。

public class Singleton {  
   private static class SingletonHolder {  
   private static final Singleton INSTANCE = new Singleton();  
    }  
   private Singleton (){}
   public static final Singleton getInstance() {  
         return SingletonHolder.INSTANCE;  
      }  
 }   

双重校验锁

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}   
    public static Singleton getSingleton() {  
    if (singleton == null) {  
       synchronized (Singleton.class) {  
        if (singleton == null) {  
             singleton = new Singleton();  
        }  
       }  
     }  
    return singleton;  
    }  
 }  

双重检查锁定是synchronized的升级的写法,那为什么要使用volatile关键字呢,是为了禁止初始化实例时的重排序。我们知道,初始化一个实例在java字节码中会有4个步骤:

  1. 申请内存空间
  2. 初始化默认值(区别于构造器方法的初始化)
  3. 执行构造器方法
  4. 连接引用和实例

而后两步是有可能会重排序,而使用volatile可以禁止指令重排序。

查看原文

赞 4 收藏 1 评论 0