
能力说明:
了解变量作用域、Java类的结构,能够创建带main方法可执行的java应用,从命令行运行java程序;能够使用Java基本数据类型、运算符和控制结构、数组、循环结构书写和运行简单的Java程序。
暂时未有相关云产品技术能力~
阿里云技能认证
详细说明如果大家关注 JDK,会发现在频繁发布的 JDK 版本中,和垃圾回收相关的 JEP (JDK Enhancement Proposals,Java 增强提案)越来越多了,垃圾回收(Garbage Collection,GC)正处于方兴未艾的阶段。譬如,在 JEP-248 中 G1 替代了并行垃圾回收器成为 JVM 中默认的垃圾回收器,JEP-333 加入了实验性质的 ZGC;最新的 JEP-189 引入了名为 Shenandoah GC 的垃圾回收器。 对于这么一个有趣的话题,我决定写篇文章来介绍,与很多介绍垃圾回收器的文章不同,本文不会涉及「某某垃圾回收器特性」和「如何使用某某垃圾回收器」等「what&how」的内容,而是从底层的垃圾回收算法开始,着重去阐释不同垃圾回收器在算法设计和实现时的一些技术细节,去探索「why」这一部分,通过对比不同的垃圾回收算法和其实现,进一步感知目前垃圾回收的发展脉络。 本文主要分为上下两个部分: 第一部分为「算法篇」,主要介绍一些重要的 GC 算法,去领略 GC 独特的思维方式和各算法的特性,这些是和具体的编程语言无关的; 第二部分为「实现篇」,主要介绍 JVM 上的一些垃圾回收器实现,包括 G1、ZGC、Shenandoah GC 等,通过了解这些商业垃圾回收器的设计理念,加深对垃圾回收算法的理解。 下面是第一部分,「算法篇」的内容。 一 垃圾回收概述 垃圾回收(Garbage Collection,GC)引起大家的关注,是从1995 年 Java 发布后开始的。事实上,GC 作为计算机科学领域非常热的研究话题之一,最早可以追溯到 1959 年的夏天,起初是用用来简化 Lisp 内存管理的。在接下来60余年的时间里, 通过 Cheney、Baker 等大师的不断努力,GC 的世界里出现了标记清除、复制、分代、增量回收等一系列 GC 算法,基于这些算法,又出现了种类繁复的垃圾回收器。 GC 的定义 首先我们来看一下什么是 GC。 GC 把程序不用的内存空间视为「垃圾」,(几乎所有的)GC 要做的就只有两件事: 找到内存空间里的垃圾,使其和活对象分开来。 回收垃圾对象的内存,使得程序可以重复使用这些内存。 GC 给我们带来的好处不言而喻,选择 GC 而不是手动释放资源的原因很简单:程序比人更可靠。即便是 C/C++ 这种没有 GC 的语言,也有类似 Boehm GC 这样的第三方库来实现内存的自动管理了。可以毫不夸张地说,GC 已经是现代编程语言的标配。 GC 的流派 GC 从其底层实现方式(即 GC 算法)来看,大体可以分为两大类:基于可达性分析的 GC和基于引用计数法的 GC。当然,这样的分类也不是绝对的,很多现代 GC 的设计就融合了引用计数和可达性分析两种。 可达性分析法 基本思路就是通过根集合(gc root)作为起始点,从这些节点出发,根据引用关系开始搜索,所经过的路径称为引用链,当一个对象没有被任何引用链访问到时,则证明此对象是不活跃的,可以被回收。使用此类算法的有JVM、.NET、Golang等。 引用计数法 引用计数法没有用到根集概念。其基本原理是:在堆内存中分配对象时,会为对象分配一段额外的空间,这个空间用于维护一个计数器,如果有一个新的引用指向这个对象,则计数器的值加1;如果指向该对象的引用被置空或指向其它对象,则计数器的值减1。每次有一个新的引用指向这个对象时,计数器加1;反之,如果指向该对象的引用被置空或指向其它对象,则计数器减1;当计数器的值为0时,则自动删除这个对象。使用此类算法的有 Python、Objective-C、Per l等。 基于可达性分析法的 GC 垃圾回收的效率较高,实现起来比较简单(引用计算法是是算法简单,实现较难),但是其缺点在于 GC 期间,整个应用需要被挂起(STW,Stop-the-world,下同),后面很多此类算法的提出,都是在解决这个问题(缩小 STW 时间)。 基于引用计数法的 GC,天然带有增量特性(incremental),GC 可与应用交替运行,不需要暂停应用;同时,在引用计数法中,每个对象始终都知道自己的被引用数,当计数器为0时,对象可以马上回收,而在可达性分析类 GC 中,即使对象变成了垃圾,程序也无法立刻感知,直到 GC 执行前,始终都会有一部分内存空间被垃圾占用。 上述两类 GC 各有千秋,真正的工业级实现一般是这两类算法的组合,但是总体来说,基于可达性分析的 GC 还是占据了主流,究其原因,首先,引用计数算法无法解决「循环引用无法回收」的问题,即两个对象互相引用,所以各对象的计数器的值都是 1,即使这些对象都成了垃圾(无外部引用),GC 也无法将它们回收。当然上面这一点还不是引用计数法最大的弊端,引用计数算法最大的问题在于:计数器值的增减处理非常繁重,譬如对根对象的引用,此外,多个线程之间共享对象时需要对计数器进行原子递增/递减,这本身又带来了一系列新的复杂性和问题,计数器对应用程序的整体运行速度的影响,这里的细节可以参考文章:Boost's shared_ptr up to 10× slower than OCaml's garbage collection[1]。 本文后面介绍的垃圾回收算法,主要就是可达性分析类算法及其变种。 二 垃圾回收核心概念 在深入研究垃圾回收算法的实现细节之前,有必要知道 GC 算法中的一些基本概念,这对了解 GC 算法的基本原理和演进过程是有帮助的。除了算法基础名词外,我们需要深入理解GC 世界里极其重要的两个核心概念:读/写屏障和三色标记法。 基础名词 根节点(GC Roots) 在 GC 的世界里,根是执行可达性分析的「起点」部分,在 Java 语言中,可以作为 GC Roots 的对象包括: 虚拟机栈中(栈帧中的本地变量表)引用的对象 方法区中的类静态属性引用的对象 方法区中常量引用的对象 本地方法栈中 JNI(Native 方法) 引用的对象 是否作为根的判定依据:程序是否可以直接引用该对象(譬如调用栈中的变量指针)。同时还需要注意的是:不同的垃圾回收器,选择 GC Roots 的范围是不一样的。 并行回收&&串行回收 根据垃圾回收的运行方式不同,GC 可以分为三类: 串行执行:垃圾回收器执行的时候应用程序挂起,串行执行指的是垃圾回收器有且仅有一个后台线程执行垃圾对象的识别和回收; 并行执行:垃圾回收器执行的时候应用程序挂起,但是在暂停期间会有多个线程进行识别和回收,可以减少垃圾回收时间; 并发执行:垃圾回收器执行期间,应用程序不用挂起正常运行(当然在某些必要的情况下垃圾回收器还是需要挂起的)。 上面并发和并行容易混淆,因为在 Java 中,我们提到的并发天然会联想到是「同一类多个线程」执行「同一类任务」,在 GC 中,并发描述的是「GC 线程」和「应用线程」一起工作。 当我们说到某种垃圾回收器支持并发时,并不意味着在垃圾回收的过程中都是并发的,譬如,G1 和 CMS 垃圾回收器支持并发标记,但是在对象转移、引用处理、符号表和字符串表处理、类卸载时,是不支持并发的。总之,并发的表述具有「阶段性」。 三色标记法 可达性分析类 GC 都属于「搜索型算法」(标记阶段经常用到深度优先搜索),这一类算法的过程可以用 Edsger W. Dijkstra 等人提出的三色标记算法(Tri-color marking)来进行抽象(算法详情可以参考论文:On-the-fly Garbage Collection:An Exercise in Cooperation)[2]。顾名思义,三色标记算法背后的首要原则就是把堆中的对象根据它们的颜色分到不同集合里面,这三种颜色和所包含的意思分别如下所示: 白色:还未被垃圾回收器标记的对象 灰色:自身已经被标记,但其拥有的成员变量还未被标记 黑色:自身已经被标记,且对象本身所有的成员变量也已经被标记 在 GC 开始阶段,刚开始所有的对象都是白色的,在通过可达性分析时,首先会从根节点开始遍历,将 GC Roots 直接引用到的对象 A、B、C 直接加入灰色集合,然后从灰色集合中取出 A,将 A 的所有引用加入灰色集合,同时把 A 本身加入黑色集合。最后灰色集合为空,意味着可达性分析结束,仍在白色集合的对象即为 GC Roots 不可达,可以进行回收了。下面是第一轮标记结束后,各个对象的颜色分布。 基于可达性分析的 GC 算法,标记过程几乎都借鉴了三色标记的算法思想,尽管实现的方式不尽相同,比如标记的方式有栈、队列、多色指针等。 读屏障&&写屏障 在标记对象是否存活的过程中,对象间的引用关系是不能改变的,这对于串行 GC 来说是可行的,因为此时应用程序处于 STW 状态。对于并发 GC 来说,在分析对象引用关系期间,对象间引用关系的建立和销毁是肯定存在的,如果没有其他补偿手段,并发标记期间就可能出现对象多标和漏标的情况。还是以上面三色标记法中的例子说明: (1)多标 假设 C 被标为灰色后,在进行下面的标记之前,A 和 C 之间的引用关系解除了(应用程序),按照三色标记法,C 和 E 都应该是垃圾,而事实上,C 不会在本轮 GC 活动中被回收,这部分本应该回收但是没有回收到的内存,被称之为「浮动垃圾」。 (2)漏标 如下图所示,对象 C 在被标记为灰色后,对象 C 断开了和对象 E 之间的引用,同时对象 A 新建了和对象 E 之间的引用。在进行后面的标记时,因为 C 没有对 E 的引用,所以不会将 E 放到灰色集合,虽然 A 重新引用了 E,但因为 A 已经是黑色了,不会再返回重新进行深度遍历了。最终导致的结果是:对象 E 会一直停留在白色集合中,最后被当作垃圾回收,事实上 E 却是活动对象,这种情况也是不可接受的。 多标不会影响程序的正确性,只会推迟垃圾回收的时机,漏标会影响程序的正确性,需要引入读写屏障来解决漏标的问题。GC 里的读屏障(Read barrier)和写屏障(Write barrier)指的是程序在从堆中读取引用或更新堆中引用时,GC 需要执行一些额外操作,其本质是一些同步的指令操作,在进行读/写引用时,会额外执行这些指令。读/写屏障实现的是「对读/写引用这个操作的环切」,即该操作前后都在屏障的范畴内,可以将读/写屏障类比于 Spirng 框架里的拦截器。下面所示的代码,当从 foo 的成员变量第一次从堆上被加载时,就会触发读屏障(后续使用该引用不会触发 ),而当 bar 的成员变量(引用类型的)被分配/写入时,会触发写屏障。 void example(Foo foo) { Bar bar = foo.bar; // 这里触发读屏障 bar.otherObj = makeOtherValue(); // 这里触发写屏障 } 读写屏障是如何解决并发标记时的漏标的?总结一下发生漏标的充分必要条件是: 应用线程插入了一个从黑色对象(A)到白色对象(E)的新引用。 应用线程删除了从灰色对象(C)到白色对象(E)的直接或者间接引用。 要避免对象的漏标,只需要打破上述两个条件中的任何一个即可,两种不同的方法核心都是采用了读写屏障: (a)方法一: 开启写屏障,当新增引用关系后,触发写屏障,发出引用的黑色或者白色对象会被标记成灰色(例子中 A 将被标记为灰色并进入灰色集合),或者将被引用对象标记为灰色。 开启读屏障,当检测到应用即将要访问白色对象时,触发读屏障,GC 会立刻访问该对象并将之标为灰色。这种方法被称为「增量更新(Increment Update)」。 (b)方法二: 开启写屏障。当删除引用关系前,将所有即将被删除的引用关系的旧引用记录下来(C -> E),最后以这些旧引用为根重新扫描一遍,这种方法实际上是「SATB(Snapshot At The Begining) 算法」的一种具体实现。 注:SATB 算法是由 Taiichi Yuasa 为增量式标记清除垃圾收集器开发的一个算法,其核心思想是:GC 开始之前,会复制一份引用关系快照,如果某个指针的地址被改变了,那么之前的地址会被加入待标记栈中,便于后面再次检查,这样就可以保证在 GC 时,所有的对象都会被遍历到,即使指向它们的指针发生了改变。鉴于篇幅原因,这里不再讲述,感兴趣的读者可自行查看 Yuasa 的论文(Real-time garbage collection on general-purpose machines[3])。 通过读写屏障可以解决并发标记时的漏标问题,具体在工程实践中,不同的垃圾回收器又有不同实现,譬如针对 HotSpot 虚拟机,CMS 使用了「写屏障 + 增量更新」的方法,G1 和 Shenandoah是通过「写屏障 + SATB」来完成的,而 ZGC 则采取了「读屏障」的方式。 下面是 HotSpot 虚拟机中写屏障的一段代码,这段代码记录下了所有的引用关系的变化情况。 void post_write_barrier(oop* field, oop val) { jbyte* card_ptr = card_for(field); *card_ptr = dirty_card; } 需要注意的是,读/写屏障只是一种理念,触发读写屏障后具体执行什么,取决于垃圾回收器的实现。由于从堆读取引用是非常频繁的操作,因此这两种屏障需要非常高效,在常见情况下就是一些汇编代码,读屏障的开销通常比写屏障大一个数量级(这也是为何大多数 GC 没有使用或者很少使用读屏障的原因,因为引用的读操作要远多于写操作),读屏障更多的时候是用在解决并发转移时的引用更新问题上。 一些公司可能会在硬件层面对读写屏障做专门的设计,便于达到最高效的垃圾回收效率。譬如,Azul 公司为 Pauseless GC 算法专门定制了一整套系统(包括CPU、芯片组、主板和操作系统),用来运行具备垃圾收集功能的虚拟机,定制的 CPU 内置了「读屏障指令」,来实现并行的、具有碎片压缩功能的高并发(无 STW 暂停)垃圾收集算法。 注意:JVM 里还有另外一组内存屏障的概念:读屏障(Load Barrier)和写屏障(Store Barrier),这两组指令和上面我们谈及的屏障不同,Load Barrier 和 Store Barrier主要用来保证主缓存数据的一致性以及屏障两侧的指令不被重排序。 三 垃圾回收算法 这一部分将从最简单的垃圾回收算法开始,介绍垃圾回收算法的演进情况,主要介绍的算法有: 标记-清除算法 标记-压缩算法 标记-复制算法 分代算法 增量算法 并发算法 前三种是最基础的算法,后面三种是对前面三种算法在某些方面的改进。了解到上述这些算法后,我们可以看到,现在的很多垃圾回收器,无非是是把文中提到的几种算法进行组合或取舍。如 CMS 垃圾回收器,就是「标记-清除 + 并发算法」的组合,其全称 Concurrent Mark-Sweep 也表明了这一点,而 G1 是「标记-复制算法 + 增量算法 + 并发算法」的组合。 基础垃圾回收算法 基础的垃圾回收算法有标记-清除算法、标记-压缩算法和标记-复制算法这三种,后面两种可以视为是对标记-清除算法中「清除」阶段的优化。 标记-清除算法(Mark-Sweep) 在之前介绍三色标记法时,其实已经能看到标记-清除算法的影子了,正是因为如此,它是最简单也是最重要的一种算法。 标记-清除算法由标记阶段和清除阶段构成。标记阶段是把所有活动对象都做上标记的阶段,有对象头标记和位图标记(bitmap marking)这两种方式,后者可以与写时复制技术(copy-on-write)相兼容。清除阶段是把那些没有标记的对象,也就是非活动对象回收的阶段,回收时会把对象作为分块,连接到被称为「空闲链表(free-lis)」的链表中去。 清除操作并不总是在标记阶段结束后就全部完成的,一种「延迟清除(Lazy Sweep)」的算法可以缩减因清除操作导致的应用 STW 时间。延迟清除算法不是一下遍历整个堆(清除所花费的时间与堆大小成正比),它只在分配对象时执行必要的堆遍历,同时其算法复杂度只与活动对象集的大小成正比。 下图是标记-清除算法执行前后,堆空间的变化情况: 从上图可以看到,标记-清除算法执行完成后,会让堆出现碎片化,这会带来两个问题: 大量的内存碎片会导致大对象分配时可能失败,从而提前触发了另一次垃圾回收动作; 具有引用关系的对象可能会被分配在堆中较远的位置,这会增加程序访问所需的时间,即「访问的局部性(Locality)」较差。 上述两个问题,将分别由下面介绍的标记-压缩算法和标记-复制算法来解决。 标记-压缩算法(Mark-Compact) 标记-压缩算法是在标记-清除算法的基础上,用「压缩」取代了「清除」这个回收过程,如下图所示,GC 将已标记并处于活动状态的对象移动到了内存区域的起始端,然后清理掉了端边界之外的内存空间。 压缩阶段需要重新安排可达对象的空间位置(reloacate)以及对移动后的对象引用重定向(remap),这两个过程都需要搜索数次堆来实现,因此会增加了 GC 暂停的时间。标记-压缩算法的好处是显而易见的:在进行这种压缩操作之后,新对象的分配会变得非常方便——通过指针碰撞即可实现。与此同时,因为 GC 总是知道可用空间的位置,因此也不会带来碎片的问题。 标记-压缩算法算法还有很多变种,如 Robert A. Saunders 研究出来的名为 Two-Finger 的压缩算法(论文:The LISP system for the Q-32 computer. In The Programming Language LISP: Its Operation and Applications[4]),可以把堆搜索的次数缩短到2次, Stephen M. Blackburn 等研究出来的 ImmixGC 算法(论文:Cyclic reference counting with lazy mark-scan[5])结合了标记-清除和标记-压缩两种算法,可以有效地解决碎片化问题。 标记-复制算法(Mark-Copy) 标记-复制算法与标记-压缩算法非常相似,因为它们会对活动对象重新分配(reloacate)空间位置。两个算法区别是:在标记-复制算法中,reloacate 目标是一个不同的内存区域。 标记清除算法的优点很多,譬如: 不会发生碎片化 优秀的吞吐率 可实现高速分配 良好的 locality 对比算法执行前后堆空间的变化,可以看到,不难发现标记-复制算法最大缺点在于所需空间翻倍了,即堆空间的利用率很低。 标记-复制在复制阶段,需要递归复制对象和它的子对象,递归调用带来的开销是不容忽视的。C. J. Cheney 于 1970 年研究出了迭代版本的复制算法,可以抑制调用函数的额外负担和栈的消耗,感兴趣的同学可以参考论文:A Nonrecursive List Compacting Algorithm[6]。 垃圾回收算法的改进 下面介绍的三种垃圾回收算法,会针对基础算法中诸如堆碎片化、暂停时间过长、空间利用率不高等不足进行改进。 分代算法(Generational GC) 分代算法对基础算法的改进主要体现在该算法减小了 GC 的作用范围。如前所述,标记过程和对象的 reloacate 过程都需要完全停止应用程序进行堆搜索,堆空间越大,进行垃圾回收所需的时间就越长,如果 GC 的堆空间变小,应用暂停时间也会相应地降低。 分代算法基于这样一个假说(Generational Hypothesis):绝大多数对象都是朝生夕灭的,该假说已经在各种不同类型的编程范式或者编程语言中得到证实了。分代算法把对象分类成几代,针对不同的代使用不同的 GC 算法:刚生成的对象称为新生代对象,对新对象执行的 GC 称为新生代 GC(minor GC),到达一定年龄的对象则称为老年代对象,面向老年代对象的 GC 称为老年代 GC(major GC),新生代对象转为为老年代对象的情况称为晋升(promotion)。注:代数并不是划分的越多越好,虽然按照分代假说,如果分代数越多,最后抵达老年代的对象就越少,在老年代对象上消耗的垃圾回收的时间就越少,但分代数增多会带来其他的开销,综合来看,代数划分为 2 代或者 3 代是最好的。 在经过新生代 GC 而晋升的对象把老年代空间填满之前,老年代 GC 都不会被执行。因此,老年代 GC 的执行频率要比新生代 GC 低。通过使用分代垃圾回收,可以改善 GC 所花费的时间(吞吐量)。 分代算法由于其普适性,已经被大多数的垃圾回收器采用(ZGC 目前不支持,但也在规划中了),其细节就不赘述了,这里我们主要关注引入分代算法后,GC 过程会出现哪些问题。 (1)问题1:不同分代在堆空间之中如何划分? Ungar 提出的分代算法(论文:Generation Scavenging[7])是目前使用最多的分代划分方案,该算法即为目前 CMS 垃圾回收器的原型:堆空间由 eden、survivor0/survivor1、old 共四个区域组成。Ungar 的论文里新生代 GC 采用的是标记-复制算法,主要是利用该算法高吞吐的特性;老年代 GC 使用的是标记-清除算法,因为老年代空间比整体堆要小,如果使用标记-复制算法,能利用的堆空间会变得更小。 分代算法的堆空间组织方式,不只 Ungar 这一种方案。譬如,在一些基于 Ungar 的 Generation GC 的实现中,会把老年代的最后一个代通过标记-复制算法处理(Lisp Machine),还有的算法会把最后一个代通过标记-压缩算法回收,降低复制算法出现的频繁换页的问题。 (2)问题2:如何标记代际之间的引用关系? 分代算法引入,需要考虑跨代/区之间对象引用的变化情况。新生代对象不只会被根对象和新生代里的对象引用,也可能被老年代对象引用,GC 算法需要做到「在不回收老年代对象的同时,安全地回收新生代里面的对象」,新生代回收时,不适合也不可能去扫描整个老年代(变相搜索堆中的所有对象),否则就失去了对堆空间进行分代的意义了。 解决上述引用问题的关键是引入写屏障:如果一个老年代的引用指向了一个新生代的对象,就会触发写屏障。写屏障执行过程的伪代码如下所示,其中参数 obj 的成员变量为 field,该变量将要被更新为 new_obj 所指向的对象,记录集 remembered_sets 被用于记录从老年代对象到新生代对象的引用,新生代 GC 时将会把记录集视为 GC Roots 的一部分。 write_barrier(obj, field, new_obj){ if(obj.old == TRUE && new_obj.young == TRUE && obj.remembered == FALSE){ remembered_sets[rs_index] = obj rs_index++ obj.remembered = TRUE } *field = new_obj } 在写入屏障里,首先会判断: 发出引用的对象是不是老年代对象; 目标引用标对象是不是新生代对象; 发出引用的对象是否还没有加入记录集。 如果满足以上三点,则本次新建的引用关系中,老年代的对象会被加入到记录集。上述过程可能会带来「浮动垃圾」,原因是所有由老年代->新生代的引用都会被加入记录集,但老年代内对象的存活性,只有在下一次老年代GC 时才知道。 分代算法的优点在于减小了 GC 的作用范围后带来的高吞吐,但与此同时我们需要注意的是,其假说「绝大多数对象都是朝生夕灭的」并不适用于所有程序,在某些应用中,对象会活得很久,如果在这样的场景下使用分代算法,老年代的 GC 就会很频繁,反而降低了 GC 的吞吐。此外,由于在记录代际引用关系时引入了写屏障,这也会带来一定的性能开销。 增量算法(Incremental GC) 增量算法对基础算法的改进主要体现在该算法通过并发的方式,降低了 STW 的时间。下图是增量算法和基础的标记-清除算法在执行时间线上的对比,可以看到,增量算法的核心思想是:通过 GC 和应用程序交替执行的方式,来控制应用程序的最大暂停时间。 增量算法的「增量」部分,主要有「增量更新(Incremental Update)」和「增量拷贝(Incremental Copying)」两种,前者主要是做「标记」增量,后者是在做「复制」增量。 增量更新(Incremental Update)我们已经比较熟悉了,在介绍读/写屏障的时候,我们提到过由于存在并发,会出现对象漏标的情况。同样的,在增量算法中,由于 GC 线程和应用线程是交替执行的,也会出现黑色节点指向白色节点的情况,增量算法中的漏标,同样是通过写屏障来解决的,主要有以下两种(基于快照的 SATB 也可以解决增量更新时出现的漏标,在此不再赘述)。 (1)写入屏障1(Dijkstra 写入屏障) write_barrier(obj, field, new_obj){ if(new_obj == FALSE){ new_obj.mark == TRUE push(new_obj, mark_stack) } *field = new_obj } 在上面的代码中,如果新引用的对象 new_obj 没有被标记过,会将它标记后放到 mark_stack 这个标记栈中,对比三色标记法,就是将这个新对象从白色对象涂成了灰色(下图中的 E)。 (2)写入屏障2(Steele 写入屏障) write_barrier(obj, field, new_obj){ if(obj.mark == TRUE && new_obj == FALSE){ obj.mark = FALSE push(obj, mark_stack) } *field = new_obj } 在上面的代码中,如果新引用的对象 new_obj 没有被标记过,且将要引用它的对象 obj 已经被标记过了,那么会把发出引用的对象去除标记,将其放入标记栈中,对比三色标记法,就是将发出引用的对象从黑色涂成了灰色(下图中的 A)。 Steele 的写入屏障相较于 Dijkstra 的写入屏障来说,多了一个判断条件,缺点是带来的额外的负担,优点是严格的条件减少了被标记的对象的个数,防止了因疏忽而造成垃圾残留的后果,譬如 A 和 E 引用关系被标记后,如果 E 在本轮标记过程中又称为了垃圾,Dijkstra 的写入屏障还需要对 E 及其子节点进行标记,而 Steele 的写入屏障就避免了这一点。 增量拷贝(Incremental Copying)大部分逻辑与标记-复制算法相似,还是会通过遍历引用关系图,把所有引用的对象拷贝到另一半堆内存,不过这个过程是并发执行的。当应用程序访问到老的堆空间对象时,会触发读屏障,对象会从老的空间被拷贝至新的堆空间。 增量算法中大量使用了读写屏障(主要是写屏障),给应用程序带来了负担,结果就是 GC 的吞吐相较于其他的算法来说不高。 并发算法(Concurrent GC) 广义上的并发算法指的是在 GC 过程中存在并发阶段的算法,如 G1 中存在并发标记阶段,可将其整个算法视为并发算法。 狭义上的并发垃圾回收算法是以基础的标记-复制算法为基础,在各个阶段增加了并发操作实现的。与复制算法的3个阶段相对应,分为并发标记(mark)、并发转移(relocate)和并发重定位(remap): (1)并发标记 从 GC Roots 出发,使用遍历算法对对象的成员变量进行标记。同样的,并发标记也需要解决标记过程中引用关系变化导致的漏标记问题,这一点通过写屏障实现; (2)并发转移 根据并发标记后的结果生成转移集合,把活跃对象转移(复制)到新的内存上,原来的内存空间可以回收,转移过程中会涉及到应用线程访问待转移对象的情况,一般的解决思路是加上读屏障,在完成转移任务后,再访问对象; (3)并发重定位 对象转移后其内存地址发生了变化,所有指向对象老地址的指针都要修正到新的地址上,这一步一般通过读屏障来实现。 并发算法是 ZGC、Shenandoah、C4 等垃圾回收器的算法基础,在具体的实现中,不同的垃圾回收器又有自己的选择和取舍。 至此,GC 算法的理论知识就告一段落了,有一些知识点是没有提到的,如部分标记-清除算法(Partial Mark & Sweep)的原理、保守式 GC(Conservative GC)对数据和指针的识别、基于引用计数法的若干 GC 算法等,感兴趣的同学可以参考文中列出的论文。 相关链接 [1]http://flyingfrogblog.blogspot.com/2011/01/boosts-sharedptr-up-to-10-slower-than.html[2]https://lamport.azurewebsites.net/pubs/garbage.pdf[3]https://www.sciencedirect.com/science/article/pii/016412129090084Y[4]https://www.semanticscholar.org/paper/The-lisp-system-for-the-q-32-computer-Saunders/ad2b04c404dc40e142332a030a146b487b6e3cf2[5]https://www.sciencedirect.com/science/article/pii/002001909290088D[6]https://dl.acm.org/doi/10.1145/362790.362798[7]https://people.cs.umass.edu/~emery/classes/cmpsci691s-fall2004/papers/p157-ungar.pdf
“因为你,我愿意成为一个更好的人,不想成为你的包袱,因此发奋努力,只是为了想要证明我足以与你相配。” —— 《侧耳倾听》 大家好,我是张舒迪,花名圣司(取自吉卜力的动画《侧耳倾听》),2014年校招应届进入阿里巴巴担任前端开发,目前在飞猪技术部负责「用户前端和数字化经营平台团队」。 一路走来虽然时常会反思自己成长道路的关键节点,以及判断和选择,但很少会落到纸上,一方面因为自认尚算年轻,阅历不多;另一方面人生没有大起大落、跌宕起伏的经历,缺乏故事感,不过是平凡人过着平凡的生活。这次很荣幸收到邀稿,借这个机会,梳理一下进入互联网行业、成为一名前端开发道路上的那些转折点,于我个人而言是个内观的机会;如果能给在看本文的你引起一些共鸣、带来一些输入,那更是甚好。 一 少年时代:心之所向,一苇以航 “我相信自己,生来如同璀璨的夏日之花,不凋不败,妖冶如火,承受心跳的负荷和呼吸的累赘,乐此不疲。” —— 《生如夏花》 泰戈尔 “你是怎么接触前端的?” 我面试时时常会这样问候选人。 我在西南一个小城市长大,父母都是坐办公室的公务员,所以我得以较早地接触电脑。90年代还在读小学的时候,父亲单位有台四通打字机,除了最基本的文本输入外,还提供了不少搜索、替换这样的功能,书本上带着墨香的文字以数字化的方式呈现,增删改查无需斑驳的修正带,仿佛有了生命一般,于是开始伸着两个食指学着敲起了键盘。出于兴趣,下课会带上一本《我们爱科学》杂志早早去 “接父亲下班”,噼里啪啦把里面有意思的文章和连载的科幻小说敲成电子版,学着做简单的文字排版,再打印出来带到学校分给小伙伴,乐此不疲。 初中阶段学校开设了微机课,电脑教室为了防尘,需要脱鞋光脚进去,这也是绝大部分同学为数不多可以接触到计算机的机会。经典的 win98,每人会配发一张 3.5 寸的软盘用以备份资料,课上老师除了教一些办公软件的基本操作,大部分时候让学生自由探索。那段时期也是我最早的编程启蒙,在老师的指导下接触了 Basic 和 Pascal,会做一些简单的编程题目,也对类似 “数组第一位下标为什么是 0 而不是 1”、“x = x+3 这样的等式为什么能成立” 这样的问题感到好奇。 2001 年家里买了第一台电脑,用于文字处理及娱乐,装了「猫」可以上网,每次联网滴滴当当的声音现在还记忆犹新。假期玩了很多游戏,开始思考像《仙剑奇侠传》这样的游戏是怎么做出来的?在一个交流群里第一次接触到「RPG Maker」—— 一款入门级的游戏制作软件,除了可视化的贴图、角色、地图、事件、触发器等编辑能力外,还支持基于 Ruby 的脚本开发,能实现从简单的角色对白字幕、自定义货币,到复杂的 ARPG 甚至第一人称射击游戏开发。从此课余爱好变成了逛各种 RM 开发论坛,尝试着画地图、捏人物、做剧情,依葫芦画瓢写 Ruby 代码,做自己的游戏,并打包发布到论坛上分享交流;当时国内最大的 RM 社区是幻想森林和 66RPG,我在幻森当版主,认识了一波热爱游戏开发的小伙伴,其中不少人现在还在行业中摸爬滚打,有的已成中流砥柱。刚刚随手搜了一下,幻想森林虽然还在,最新帖子却基本停留在 2016年;66RPG 则早已更名「橙光」,专注于原创 IP 的 AVG 游戏制作与发行,一阵唏嘘感慨。 这段「游戏开发」经历断断续续持续了 2 年多,创造一个虚拟世界带来的兴奋感逐渐转变为对计算机世界的热爱,并开始驱使我立志报考相关专业,憧憬未来能够进入这个行业一窥究竟,并能有所作为。现在回头看少年时代的这段经历,兴趣是最好的老师,在国内应试教育的大背景下,学校、家庭能够给到我学业之外自由探索的环境与空间,这么早就找到自己的兴趣点与未来方向,我感到无比幸运。 二 我的大学:二八年华,如丧青春 “我年轻时候喜欢说如来,就是如同要来,还没来,但终归会来。如丧,就是如同要丧,还未丧,但终归会丧。” —— 《如丧》 高晓松 故事不会总按预设的剧本往下演,不然人生该多无趣啊。07 年高考,浙大计算机系落榜,被调剂到北邮的工业设计专业,工科院校、通信背景、设计专业,感觉老天爷给我开了个玩笑,如鲠在喉;两个月暑假过得无比煎熬,每天都在去留间反复纠结,最后思量再三接受了调剂。三个原因:1)北邮也有计算机系,大一结束绩点 Top 4% 的学生有一次转系的机会,努努力可以曲线救国;2)学校有浓厚的互联网氛围,师兄师姐给了不少鼓励;3)从没去过一线城市,对北京的向往。9月入学,由于要筹划转系,大一一年又过起了白天泡画室、晚上泡自习室的苦行僧生活,然而天不遂人愿,年末综合绩点专业第 4,又一次与计算机学院擦肩而过。 我从小学开始一直是 A 等生,接连而来的高考失利/转系失败,打击不可谓不大,加之生活上也出现一些问题,整个人跌入谷底,负能量满满;那时候年轻比较文艺,会在博客上写矫情的文字与诗歌,一个人跑到景山望着故宫出神,翘课、也不参加班级活动。这样浑浑噩噩的状态持续了几个月,拯救我的是书本和音乐,是路遥的《平凡的世界》、克里希那穆提的《重新认识你自己》、张国荣的《我》、杨千嬅的《再见二丁目》……生而为人,本无所谓功过成败、高低贵贱,正是每段旅程与经历、每次跃起与沉沦、每分收获与成长塑造了一个个独一无二的个体,凑近看都那么平凡却又那么不凡;尝试品味自己的人生旅程,自我欣赏并自我和解,会发现当下所经历的就是生活本身;推己及人,曾经感觉拥挤不堪的校园、做事毛糙的同学、能力平庸的老师都开始显得立体而高大起来。 “I am what I am,我永远都爱这样的我。快乐是,快乐的方式不止一种,最荣幸是,谁都是造物者的光荣。不用闪躲,为我喜欢的生活而活,不用粉墨,就站在光明的角落。我就是我,是颜色不一样的烟火,天空海阔,要做最坚强的泡沫;我喜欢我,让蔷薇开出一种结果,孤独的沙漠里,一样盛放的赤裸裸。” —— 《我》 张国荣 生命的韧性就在于,无论被打趴多少次,只要你全情投入,自驱的力量总能一次次再把你拉起来。调整状态,抱着与其怨天尤人,不如活在当下的想法,开始尝试把生活重新拉回正轨,继续追寻梦想: 确定跨专业考研的长期目标,开始自学&旁听计算机系课程,同时开始刷 OJ(那时候北邮的 OJ 做得是真不错)。 包揽了本专业几乎所有团队课设大作业的编程相关工作,做 Flash ActionScript & Flex 开发,折腾 J2SE、LAMP,帮忙维护基于 .net 的院系官网,总之只要编程相关工作我都干。 进入校学生会科技部,开始混社团,一路做到副部长,主要承担各类大型活动开闭幕视频的制作工作,涉及一些 AE 粒子特效脚本的开发,这应该也是第一次与 JavaScript 结缘,除此之外 PS、PR、AU、CorelDraw 这些软件也玩得贼溜。 由于本身专业的缘故,接触了 3DMax 和 AutoCAD,凭借手艺课余接了几个外包项目。 大三下半年又开始一边在专业教室赶大作业,一边泡自习室、逛王道论坛备考的日子;期间靠专业排名拿到本专业保研名额,没做太多思考便放弃了,算是一个小插曲吧。年底怀着惴惴不安的心情再次走进考场,初试、复试、面试,次年初结果公布,如愿进入北航计算机学院,师从院长,算是又一次人生转轨,了了一桩心愿。 顺境享受掌声,逆境享受人生,这段持续四年的曲折经历给我带来莫大的力量与精神财富,教会我诸多道理,也很大程度奠定了我日后的事业观与人生观: 追寻热爱:支撑自己向前的应该是源自内心的热爱,对当下的热爱,对生活的热爱,也是对爱与美的追求,这种热爱很少是与生俱来的,更多时候需要自我暗示;一如写这篇文章,刚动笔也枯燥不堪,尝试说服自己沉进去,文字从笔下缓缓流淌的感觉却也让人感到平静。 日拱一卒:人会高估短期的成果,却低估长期的成就,每天哪怕花 10 分钟看 5 页书,一年就是 6~7本;每天解一个 leetcode 题目,一年你能超过 95% 的人。其实走得慢不要紧,关键要走得久,时间长了自然能走很远。 接纳失败:“那些没能将我击倒的,都使我更加强大”,挫折来得早一点,跌得狠一点不一定是坏事,与自己和解,从情绪中跳脱出来,逆境更能激发人的思考,也更能锻炼人的韧性。 留条后路:人生不是非黑即白,避免情绪化决策,给自己留条后路;我参加考研没影响保研,不喜欢设计专业也拿到了 Top 2% 的校级优秀毕设,临毕业参加校招面了一些互联网公司的产品岗位,多几个选择总不是坏事;类比到现在流行的裸辞,是不是也可以更平滑一些?先换行业再换岗位?先做副业再做主业? 三 转型前端:意料之外,情理之中 “生活中多数东西,最好与普通之间的差距不超过两倍,好比说纽约的出租车司机,最棒的司机与普通司机之间的差距大概是 30%;最棒的 CD 机与普通 CD 机的差距有多大?20%?这种差距很少超过两倍。但是在软件行业,还有硬件行业,这种差距有可能超过 15 倍,甚至 100 倍,这种现象很罕见,能进入这个行业,我感到很幸运。”—— 《遗失的访谈》 史蒂夫·乔布斯 如果说上阶段是兴趣驱使进入 CS 世界,那接下来闯入前端领域则纯属误打误撞了。 我硕士主要研究方向是基于浮动车(Floating Vehicle)的智能交通系统(ITS,Intelligent Transportation System),简单说就是通过各类车辆实时定位数据、城市路网数据、地感线圈/摄像头等传感器数据,动态计算路况、识别事故、预测拥堵、控制信号灯等,领域属于智慧城市,技术上偏 GIS 和海量时空大数据处理。 专业方向与前端也有一定关联性,主要是基于 Web 地图的数据展示,例如和北京市交通委合作的公交到站预测项目,除了预测算法本身,还基于 Tomcat + PHP + Angular + BMap 搭了 PC 的数据展示平台,这也是第一次接触到 MVVM 框架,第一次认识到原来前端还能这么玩。这个项目的研究成果支持了北京市第一套公交到站实时预测系统,通过多个 App 向社会发布到站预测信息,算是个民生工程。 那时候不知道国内互联网公司里前端是个独立方向,直到研二在一家国企实习,由于成员离职被临时调剂到前端岗顶包,承担了一小部分 Web 应用和绝大部分前端开发工作,主要是基于 VMWare VCloud 和 VSphere 搭建虚拟机 Web 可视化终端,类似 WebVNC。当时公司的技术栈是 RoR + JQuery + Bootstrap,三个月时间搭建了完整的虚机管控、可视化及配套功能模组,开始被 Web 开发这种所见即所得的爽快感吸引。 最后促使我决策进入前端领域的,是两个机缘巧合: 毕业季拿到了阿里 Java 研发的 offer,入职前部门组织了一次圆桌,新人可以跟后端、前端、客户端、测试等岗位 TL 作直接的沟通,并在入职后转到任意方向。跟拔赤(也是我现在的主管)聊了一下前端技术的现状与未来发展,聊到 PWA,他演示了一个 Wap 页面怎么通过添加到桌面的方式变成一个 Web App,B/S 无需安装、互联互通的体验让人印象深刻,聊到类比 PC 时代 B/S 逐步替代 C/S 的案例也十分生动,跟其他几个 TL 更偏业务分享、工作内容、培训方案的交流相比,一个领域未来的可能性无疑更让人神往。 那段时间有幸读了 Paul Graham 的《黑客与画家》,对我触动很大的反倒是书名,本科设计、硕士计算机,中二地感觉自己像极了左手画笔、右手键盘的「黑客与画家」,而前端恰好是视觉交互设计与计算机的交叉学科,更能发挥我的优势。最后,这次圆桌与这本书将我引入了前端的世界,职场生涯就这样从挂着研发工程师 title 写前端代码开始了。 现在再回头看,这段经历也让我受益良多,以为踏错的那些「弯路」,都在某个时间点串成一线,共同组成了人生,沿途足迹清晰可见,没有白走一步: 活在当下 四年的工业设计学习生涯,教会我的不只是怎么切图,更是对好产品的审美,作为前端开发有这样一段经历弥足珍贵;小到每个项目、大到每个人生阶段,我们很难预见当下的经历对未来的影响,因此认真对待每件身边事,尊重每段人生经历独特的价值,历久必会弥新。 不给自己设限 学理科考上大学,也能坐在画室折腾素描/水粉/石膏/三大构成;本科四年一身机械+设计的本领,硕士也能跟一帮 CS 大拿聊聊设计模式与系统架构;折腾两年半大数据&算法,进到公司也可以靠前端手艺吃饭;所以别给自己设限,能力不只是经历决定的,也有意识与胆量。 “模仿” 身边最优秀的人 接触一个全新的领域,学习一门全新的技能,最快也是最好的方式,便是模仿身边这个领域最优秀的人;没做过工程化/可视化/低代码化?看看集团对应领域的同学在做什么/想什么;没牵过虚拟项目?看看身边做得最好的虚拟组组长是怎么做的,多久组织一次会议,怎么制定项目组目标,怎么驱动项目组同学。 坚持阅读 书本能够提供的不只是经验与知识,有时候是灵光一闪、虎躯一震,有时候则是更平和而充实的内心,与自己的和解;尝试带着意义感去品味,音乐、电影、诗歌、话剧、动漫、游戏...亦复如是。 四 我在飞猪做前端:因势利导,顺势而为 “譬之若寒暑之序,时至而事生之。圣人不能为时,而能以事适时。事适於时者,其功大。” —— 《吕氏春秋》“得时无怠,时不再来,天予不取,反为之灾。” —— 《国语·越语》 14年进入阿里之后的开局其实并不好。 大环境上正逢 “All in 无线” 的高潮,前端从 Mobile first 到 Mobile only,资源开始大幅从 PC 往 H5 倾斜;进入公司毕竟是个新兵蛋子,按当时「新人做老业务,老人做新业务」的分配方式,我负责了国内机票 PC 业务: 2014 年还需要写 YUI 代码、调 IE6 兼容性、走前后端统一部署,非常边缘。 由于阿里本硕校招生无差别定级策略,跟我同一天入职同样是 P5 的小兄弟小我整整 3 岁,起跑线堪忧。 刚转岗残缺的前端知识体系完全写不出工整而高效的代码,基本盘被吊打。 ... 如此种种,瞬间让我感受到身处一线互联网公司的巨大压力。 出于危机感和自救本能,开始找出路,接了一个多月需求后,我发现其实业务研发本身工作量并不大,但因为代码历史包袱太重(6年数万行老代码,first commit by 癫总),架构腐化严重,改动往往牵一发动全身,效率奇低。当时想法很简单,重构提升研发效率,节约出来的时间去做 H5 业务,还是得紧跟无线大潮,别与时代脱钩。 与主管聊了想法,之后三个月时间断断续续一边老代码正常承接业务,一边抽晚上和周末的时间做重构,一份需求做两遍。年末顺利上线,藉由这个项目我成了比 PD 还了解这块业务的开发,顺便输出了一次前端架构分享和 BU 40% 的 PC 组件,研发提效/架构升级/物料输出,大大超出主管原本只需要维稳的期望。藉由这个项目,15年初顺利晋升,也如愿开始接触无线领域,感恩。 我把这段经历称为「对现状不满,就尝试把自己 “做没”」。如果负责了一个边缘业务,可以尝试从这些方面去思考: 调整心态:认可合理性,少抱怨(脏活累活也是活,总得有人干)。 发挥优势:更低的预期意味着更容易超出预期(“他做边缘业务都能有这么多产出”)。 寻找痛点:主导并落地解决方案(实在没痛点,尝试一下智能化方向把自己 “做没”?或者挤时间参与一下工程化基建?)。 纳投名状:需要证明自己的能力,也需要韬光养晦等待机会。 专业为先:前端是相对垂直的技术体系,业务价值不高不代表技术价值不高,不要放弃思考与改变。 随着 PC 业务工作量逐步减轻,15年我的主要精力逐渐转移到无线 H5 上来,负责了一些体量偏小的新业务,比如签证、用车,练手性质偏多。坦白说那个时期的 H5 开发是比较枯燥的,一方面业务复杂度不高,逻辑尽可能往服务端下沉,端侧少了很多抽象封装、继承拓展、设计模式的需要,带来的代码爽感大大降低;另一方面屏幕变小了,精细化要求也变高了,视觉交互上的细节问题被极速放大;加之真机调试相较 Chrome devtools 的原始感,往往一个 px 偏移或动画卡顿的调整就能耗进去几个小时。 到了一个新的环境,最先要做的还是破局,从大环境上看,那段时期业界都在攻坚性能体验的课题,围绕 Hybrid 架构各类技术方案如雨后春笋。而旅行行业的特殊性在于,用户有大量在旅行途中使用 App 的场景,国内景区要么人满为患,通信基站负荷过重导致网络降级;要么各类自然风景区、高速路/铁路沿线直接没有信号覆盖,没办法查询电子票根,更不能享受各类行中服务;同时动画滥用、Webkit BUG 也导致 CPU 彪高、耗电量增加,在类似神农架这种缺少充电手段的场景非常致命。因此相比其他行业,性能体验对于旅行业务的影响面更大也更深远。 明确性能作为突破方向后,开始找突破口。当时架构组性能优化手段集中在容器架构层面,如iDNS/SPDY 改造解决网络传输问题、离线化体系解决资源加载问题、脚手架模板优化解决 JS 阻塞渲染问题,鲜有目光聚焦在前端业务优化层面。这一方面导致跑分与体感指标存在差异,另一方面业务上基础库未预加载、combo 失效、图片未压缩、错误的构建配置等问题都可能将底层优化积累的优势损失殆尽;加之性能数据上报链路和配套度量体系不够完善,指标对业务开发同学日常工作的指导意义不足。 抓住问题之后,我从手上小业务开始逐步落地全链路的性能体验优化方案: 重新定义性能质量标准,从最初的页面秒开率,转为 三端(飞猪、手淘、支付宝)、四网(2G、3G、4G、wifi)、五维度的度量模型,在白屏、DOM Ready、Onload 等通用评估标准之外定义了「页面可见时间」与「可交互时间」等体感指标。 重构数据采集上报链路,维护基础库 Base 及 tracker 模组,制定业务埋点规范并推进埋点上报率、准确性治理。 和搭档搭建数据分析平台「鱼眼」,打通 ODPS、Node 工程、数据清洗及可视化链路,提供页面性能、接口性能、JS/接口异常、在离线分析、红黑榜等度量分析报表。 工程化链路升级,从 dns-prefetch 到 mtop-prefetch,从 SSR、静态资源缓存,到数据、DOM 结构、占位符多级缓存,一毫秒一毫秒、一 kb 一 kb 打磨业务实现细节,优化加载性能。 构建泛性能体验相关最佳实践,包括帧率、CPU、内存、电量等指标优化,及全链路 Tap 化、转场动画优化、长列表滚动优化等体验优化。 “‘自己’这个东西往往是看不见的,你要撞上一些别的什么东西,反弹回来,才会了解‘自己’。所以,跟很强的东西、可怕的东西、水准很高的东西相碰撞,然后才知道‘自己’是什么,这才是自我。因此,交不同类型的朋友,看不同类的书,去不同的地方,在碰撞中不断反思自己!” —— 山本耀司 藉由一些业务优化上拿到的结果,15年中进入架构组开始承担「性能体验虚拟小组」组长的工作,着力提升整个飞猪 H5 业务的性能体验,包括已有经验向团队的输出以及 Hybrid 基建改造。经过几个月的持续迭代,当年双十一拿到超过 93% 的页面秒开率,体感指标优于手淘,各项体验/能耗指标也大幅提升。 随后 Native 化渲染方案兴起,开始推进 ReactNative 体系建设,持续了大半年时间,期间打通整个工程研发、运行时、配套工具与组件库链路,并上线了 3 个业务。但总的来说这段实践经历不算成功: 技术基建重视程度足够,但缺少对业务价值的衡量,业务方感知弱。 脸皮薄,推动力不足,问题修复拖了一个又一个版本。 稳定性问题,RN 最初面向独立 App 场景的定位与成熟客户端水土不服,投入产出比低。彼时集团 Weex 开始崭露头角,相较 RN 更适合阿里的业务土壤,基建随即开始往 Weex 方向倾斜。 之后经历了团队调整,我被调到营销组顶替已离职 TL 开始带团队,再之后不到 2 个月时间又迎来了入职以来最大的「变化」,在此先卖个关子。回头看架构组这段经历,也同样教给我很多: 技术与业务的围城 做业务就像在浅水区游泳,你知道有个底在那里,起雾下雨了、倦了累了、抽筋儿了,双腿往下一伸就能站起来,支持好业务至少不会有太差的结果,当然如果没有 130% 的自我加压,也很难有超出期望的结果;而做技术尤其是基础技术,更像是在深水区甚至大江大海中扑腾,没有兜底、没有退路,哪怕狂风大作、巨浪滔天,一旦出发必须游到岸边才能停下休息,而很多时候你并不知道岸有多远,很容易迷失。业务线同学羡慕基础线同学有广阔的天地可以遨游,殊不知很多时候是幸存者偏差,看见的都是靠岸的人,那些还在前行的、迷失的、甚至葬身海底的人,你却不会知道他的名字。 印象即事实 职场是个小社会,在别人眼中的印象需要有意识去管理,好的印象会让周围人对你有更高期待,从而驱使你继续前行;而差的印象则可能带来旁人的不信任,乃至破罐破摔的心态;回望在阿里这些年的发展,第一印象法则帮到了我,机票重构项目的顺利上线带给我的不只是 “我也能做好前端” 自信心,也有主管和协作方的信任与更大的成长机会。 收起羽毛与玻璃心 写得了一手好代码不一定当得了一个好 PM。15年我负责飞猪域名切换及全站 HTTPS 化改造工作,项目涉及 10+ BU 过百参与者,在推进过程中,从最开始纠结中午 1点半发旺旺是不是可能吵着别人睡觉,到半夜 1点联系 PE 解决域名配置问题;从凡是求人就是 “你好师兄打扰了” 的书生气,到顶着 P5 Title 跨 BG Push P9 大佬出解决方案;从被动执行到主动推动,被逼着磨厚了脸皮。工作这些年,看过不少新人(玻璃心)或老人(自尊心,爱惜羽毛)这一关过不去,遇事不敢问、不敢说,惧怕后果,技术同学折在非技术问题上实在可惜;胸怀是委屈撑大的,收起羽毛与玻璃心,跳出墨菲定律的怪圈,其实结果远没有想象中的那么坏。 魔鬼都在细节中 性能体验、研发效率与稳定性是前端三驾马车,围绕这些大方向很多技术规划都是类似的,但落到执行结果上千差万别;类比为什么每个公司都在公布自己的战略方向,却鲜有人能执行到位或抄得到位?因为顶层设计之下,对每个细节一点一滴的执行与积累存在差异,而这些差异聚合在一起决定了成败,互联网行业尤其如此。 五 北京→杭州:纵身一跃,逐风而行 “你是否和我一样,一路走来我好匆忙,模糊了来时的方向,清晰了北京的茫茫。你是否也和我一样,找到了梦开始的地方,日复一日谈着理想,年复一年的那条小巷。他们说远方是大海,那里会有海风吹来,看着海鸥自由自在,不要让我的梦醒来。” —— 《海风》 郝云 从北京到杭州、从技术架构到业务支撑、从一线开发到团队 TL,2016 年是充满变化的一年。 飞猪的前身叫做航旅事业部,最初是从淘宝北京研发中心旅行相关业务分拆出来的,因此当时绝大部分研发团队都在北京。当年为响应集团京杭双中心战略,需要成立杭州研发中心,一线同学可以自行选择是否转 base 到杭州,同时基本明确了未来资源向杭州倾斜的策略。我当时面临的情况是,生活上在北京待了近 10 年,在这里娶了妻、安了家、积累了不少人脉,也早已熟悉了这里的气候与氛围;工作上刚刚从一线开发成为 TL,接手营销团队并做完了财年规划,准备放开手脚大干一场。事业与生活对抗的焦虑感在团队内蔓延。与家里人思量再三,考虑未来发展,决定响应号召转 base 到杭州,自此开始了 4 年多的双城生活。 7月份到杭州之后,先做了一段流媒体、直播业务,这期间有个小插曲,直播底层能力由淘系支持,由于优先级问题需求一直排不上,这种跨 BU 项目推进的痛苦相信很多人都经历过。我当时手上就这一件事,退无可退,索性把工位搬到对方团队坐了两周,软磨硬泡,直至参与对方的项目去写代码作资源置换。最终项目如期上线,而我也借这个机会学习了一下淘系的工程化基础设施和研发流程,收获颇丰。拿结果有时需要非常手段或另辟蹊径,找不到方法的时候问问自己是不是被逼得还不够狠。 16年底组织架构再次调整,开始接手 8 个人的杭州度假前端团队,以及 30+ KISSY 存量项目。 当时在中台化大背景下,大量基建收口共建;飞猪是阿里一个业务 BU,而我们是飞猪的一个业务团队,在集团中台(研发框架、搭建平台等设施)与飞猪架构组(工程化、基础组件等基建)之上,向下探索是重复建设,单纯支持好业务开发又缺少沉淀(相信现在不少团队也有类似问题);加之接连的组织架构和方向调整,士气不高,第一要务是明确团队定位和方向,一起赢一场大仗。 我的选择是坚决往上走,到业务最前线去找机会,底层基建不争不抢。由于历史原因,飞猪当时是行业化的组队方式,分交通、酒店、度假等业务线分别有独立的运营、产品、技术等职能团队;对于前端研发来说,这也意味着每个行业诸如频道、搜索、详情、下单等页面都需要独立开发一套,带来诸多问题: 前端行业化组队收益有限 行业化本质是在解决供应链差异性问题(如机票,基于 GDS 的运价体系结合全球库存分销体系,构成了非常深的行业壁垒),垂直化组队模式有助于底层业务模型抽象,但行业间 C 端表达层面区别并不大(价格/库存等行业业务语义差异极大的字段到端上都是数字),端同学垂直分工带来的收益十分有限。 难以在行业间形成合力 行业数据模型不统一、定制化功能不收口导致端侧重复建设,研发效率较低,同时面向多行业串联打通的场景无法高效支撑。 难以保障端侧体验一致性 行业化过程中由于视觉交互规范不互通,相似场景产生多套方案,端侧体验难以保障一致性(如机票搜索筛选面板固底而汽车票固顶,同样是 OD 类商品产生了两种交互形式)。 不利于人的成长 行业化组队方式前端同学同时负责一条业务线的全链路业务,而类似频道的纯展示页面与类似订单填写页的表单页面本身依赖的技术方案差异性大,难以抽象复用,也因此很难有更体系化的产出。 抓住上面这些问题,紧靠业务上横向打通的大方向,技术策略定为「打破三个壁垒」: 打破前端与设计师的壁垒:拉通 PD 和 UED,构建蜂鸟设计中台体系,提供跨行业一致的视觉交互规范,以及五端一致的配套组件库(H5、Weex、Rax、Android、iOS);形成「行业提报-信息收口-规范升级-沟通决策-落地实施」的标准化链路,保障规范稳定持续。 打破端与云的壁垒:拉通后端,构建 BFF 层(Node/Java),面向各个业务领域抽象视图层模型,对下屏蔽业务差异,对上提供一致的数据结构。 打破端与端的壁垒:拉通各个业务前端团队,面向各领域 BFF 构建领域驱动的前端业务框架,并以业务组件形式封装行业实现,支持面向各类场景的功能组合与插拔。 在此基础上,结合集团中台化方向,逐步沉淀出度假业务导购域(泰坦+奥恩)、搜索域(猪搜+TUS)、详情域(travelDetail+TUD)以及交易域(tripbuy+奥创)解决方案。 18年整个飞猪的平台化战役打响,我负责了其中导购与端侧两部分的平台化工作,本质上是度假已有实践经验结合各行业特点向整个飞猪的输出。双十一前顺利完成改造切流,平稳度过流量洪峰。这次改造对于业务团队来说,价值体现在几个方面: 解决研发效率瓶颈问题,支持跨行业物料互通,统一商品/交易,也为后续支持全域氛围控制、结构化表达、全域投放、资源位互通等打下基础。 为业务团队一线开发同学打开思路,在基础架构与上层业务之间明确了业务架构层,各域可以更聚焦作领域解决方案的演进,也为日后频道批量生产、搜索卡片动态化、详情域 DX 动态模板化、交易域新奥创迁移等项目打下基础。 推动团队完成组织架构调整,从按行业化纵切的组队方式,改为按领域横切,职能更加聚焦。 今天回过头再看京杭调整这次旋涡对不同个体的影响,现在 4 年过去了,选择纵身一跃转战杭州的同学,与选择留守北京在聚光灯外坚守的同学,都收获了不错的成长,也拿到了结果;把时间的尺度拉长,并不存在决策的好坏,关键在于能否坚持。这段经历带给我的: 谋前不惧,谋后不悔 做决策最忌讳的是不作为与左右摇摆,正确决策>错误决策>不决策,在全面评估、权衡利弊之后,没有最优解的情况下,相信直觉是个不错的选择;坚持与选择同样重要,路都是越走越宽的,早决策早出发,把时间用来奔跑而非等待。 横向跨端,纵向跨栈 前端是应用架构最上层,浏览器屏蔽了硬件层、虚拟层、OS 层、框架层的绝大部分细节,架构很轻薄,因而也容易面临瓶颈问题;因此横向跨端的大前端体系,或纵向跨栈的全栈开发工程师体系是个人职业发展的必由之路,面对问题打开思路能够发现更多解法。 小处着手,大处着眼 从 PC 首页改版 PM,到 HTTPS 改造/域名切换牵头上百人虚拟团队;从 H5 小业务的性能优化方案,到「性能体验虚拟小组」负责人;从度假业务架构升级,到推进飞猪平台化建设,我的经历很多都是从一件小事开始积累量变,在某个时间点借势质变。规划不用开始就做得很大,重要的是从身边小事做起并做到极致,过程中不断涌现的新问题会带来深度与壁垒;脚踏实地的同时仰望星空,看看有什么设施可以复用,手上方案又可以解决别人什么问题,加以时日技术体系与产品自然能长起来。 定目标,画大图 我从一线开发到 TL 的跃迁是从画大图开始的,相比一人吃饱全家不饿的爽快感,团队协同需要做到一张图、一场仗;好的大图首先要考虑面向的受众与要定义的问题域,摒除不相关信息的干扰,其次要能结合规划突出重点,另外还要明确表达分工及相互间的关联关系;大图务虚的表象背后,是对一个领域运筹帷幄的能力与信心。 塑造团队,使众人行 打造一个奋进、卓越的团队需要对权利与责任有更清晰的认识,带好一个一线团队由浅入深可以拆分为四个阶段: 活水之源:昨日因今日果,招聘要凭良心。 换位思考:眼里要有事更要有人,多做 1v1 沟通,培养同理心(我懂你)、信任感(你懂我)与利他心态(能成事)。 榜样作用:我说你听,我做你看,你说我听,你做我看,带领团队赢一场大仗。 使命与愿景:沉淀共同的文化、目标与路径,攀更高的山,看更美的风景。 六 我在飞猪做导购:谋定后动,知止有得 “梁丘据谓晏子曰:‘吾至死不及夫子矣!’晏子曰:‘婴闻之,为者常成,行者常至。婴非有异于人也。常为而不置,常行而不休者,故难及也?’”—— 《晏子春秋》 18年底,平台化战役一期结束后,伴随组织架构调整,我所负责团队的业务范围从垂直的度假业务,转型横向大导购方向,专注于购前链路的流量获取与高效转化。同时组建 Java 导购和无线服务端团队,开始从纯前端职能团队,向混合栈业务团队转变,2 年时间团队规模也从最初的 8 个人慢慢发展到 30+ 人。从前端到导购的变化带来了诸多挑战: 第一个挑战,是可对话,具备跨职能平等沟通的能力。 接手后端团队后最先涌来的是一堆新的技术或业务名词、知识体系,印象很深的是一次开会与酒店团队聊 DUMP 数据对接方案,双方一线同学你来我往,云里雾里唇枪舌剑一番没有达成一致结论,幽幽地转头看向我,我憋红了脸吐出四个字:“啥叫 DUMP?”。 类似的场景其实在工作中比比皆是:担任项目 PM,后端同步的 fgc 问题有多严重,是否需要协调其他角色协助排查?记业务会议纪要,从 CPM 切换到 CPC 模式这个 Action 应该指派给谁?跟进客户端发版,iOS abort 率万 7 是什么水平,发版是否需要延期? 接触一个全新的领域,有大量陌生的名词和知识体系需要补足,解决鸡同鸭讲的问题,然而最大的阻碍不在于新领域的壁垒,而在于我们过往的经验、资历、光环,不想示弱、害怕伤自尊,缺失放下身段与周围人请教学习、比拼技艺的勇气,这些都可能阻碍沟通,影响理解与表达,提高进入新领域的门槛。 快速具备可对话能力没有太多诀窍,重点就是放下身段,磨厚脸皮,不懂就问。每个人都会有知识盲区,抓住机会向身边任何层次/背景的人请教,收获的是自己的成长;况且刚开始就主动坦诚、积极沟通学习,也远好过死扛一段后在关键场合被打回原形(比如晋升场)。 第二个挑战,是思想跨栈,从技术/产品思维到业务思维的转变。 前端技术有其特点:1)天然开源;2)交互设计强相关;3)产出物可见。这些特点带来了我们这个群体对开源与分享的热衷、对美感与细节的追求、以及对用户体验的极度关注,因此优秀的前端工程师产品 sense 多半不错,从琳琅满目的技术产品、炫酷的官网、完善而规范的文档/Demo 就可以看出。但技术/产品 sense 与业务 sense 之间仍有巨大鸿沟,以 Push 平台的 case 举例,不同思维方式考虑的点: 前端思维:配置页面打开速度怎么样,大表单有没有缓存机制,Push 文案在不同尺寸屏下被截断的效果,承接页兼容性如何。 后端思维:消息能不能准确送到,任务如何拆分与合并,线程池如何设计,海量消息高并发如何解决吞吐率和时效性问题。 产品思维:消息类型能不能横向扩展,推送时间是否支持自定义,数据报表能不能与触达任务无缝衔接打通。 业务思维:哪类 Push 拉新效果更好,什么时间发打开率更高,哪些消息面向活跃率/又有哪些是面向成交,两者比例如何拿捏。 做业务要为最终结果负责,而前端角色从技术/产品思维到业务思维的一跃有很多天然的瓶颈与鸿沟: 过度的产品化执念导致容易陷入细节。 缺少与业务方长时间高频度的互动,对商业模式的理解、数据的敏感度不足。 从阿里内部看,更偏垂直专业领域的 KPI 与晋升体系一定程度上也决定了技术产品化的重要性会略高于业务结果。 从产品/技术思维到业务思维的转变,可以尝试从以下几个方面来培养: 培养对目标与数字的敏感度,尝试收集并形成自己的订阅报表,定期 Review,多追问指标升降背后的原因。 加强与业务方互动,多从业务目标视角看待每个需求,使用 STAR 法则梳理关联关系,多问几个为什么。 尝试结合掌握的信息去做公式拆解与沙盘推演,例如 App DAU = (MAU * 月均访问频次)/30 + 域内日均拉新 + 域外日均拉新,目前的现状每个指标分别在什么量级,每个需求又分别服务于哪个指标,能够提升多少,提升后是否能推导出目标达成,拆解事项并梳理优先级。 抛下过于超前的产品执念,避免陷入细节,以产出最小可行产品(MVP)为原则快速试错与迭代,区分好「锦上添花」与「雪中送碳」。 第三个挑战,是目标与定位,划定在部门中的位置与边界。 飞猪采用的是大前端的团队组织形式,内部分工协作相对清晰,协调成本也比较低,因此各团队除了对接固定业务方需求外,基建分工一般更面向技术栈,如小程序一码多端(Rax/小程序)、中后台(PC/表格/表单)、互动玩法(Canvas、WebGL)、工程化架构等,这样的分工天生会有相对清晰的边界。刚开始带导购后端团队,由于更贴近业务以及横向支撑的特性,带来不少问题: 对接业务方多,各业务方 KPI 目标存在差异,需求承接缺少主线。 技术上各应用缺少清晰的定位与领域划分,怎么快怎么来,架构腐化快。 边界不清、业务功能模块重复建设导致与营销、行业等协同团队踩脚的事时有发生。 因此,拆解 BU 规划,确定团队绑定的业务目标会比前端更为重要。前期重点做了几件事情: 确定团队的北极星指标,前期关注导购平台承载流量规模,在研发资源投入不变情况下,提升多样化业务承接能力;后期关注业务 IPVUV/成交转化,通过业务策略、算法的快速迭代试错优化流量承接效率,再通过规模化效应快速赋能全渠道提升转化。 围绕北极星指标确定不同阶段的核心业务方,并通过目标规划对齐确定产品技术规划与优先级;如最初的业务收口阶段,首先拉取了各个资源位渠道流量规模,然后由大至小逐步沟通推进收口,同时对于 App 首页等重点场景提供底层统一能力之上的定制化产品支撑; 对于技术规划中涉及到的各系统模块,与协作 BU、中台团队沟通确认是否有已有系统能够支撑并且规划能够 match,不满足条件的情况下明确定位区别与各自边界,自建支持。 进入业务深水区,很多时候需要攻坚的不只是技术难题,还有组织难题;在有限资源情况下,决策不做什么也比要做什么更重要。近两年的飞猪导购生涯,推动了导购域三个阶段的升级改造: 阶段一 建设飞猪物料流通大动脉(2018) 解决行业物料不互通,平台能力不互通的问题,完成面向飞猪业务的选品平台(星辰)、投放平台(泰坦)、搭建平台(页匠)建设,围绕多种类型的行业物料完成统一分发链路的构建,通过一条大动脉串联了云与端。 阶段二 端侧资源位收口,业务承接物料加(2019) 构建资源位管理体系完成端侧资源位收口,包括主动触达渠道(如 Push)、静态资源位(如 App 闪屏)、动态资源位(如 SmartBanner);构建 LandingPage 生产体系,基于飞猪行业化物料快速批量化搭建场景频道、榜单、清单等 Landing 页。提高人货场匹配效率。 阶段三,构建精细化运营体系,推动运营模式转型(2020,ing) 构建用户增长、人货场理解、流量调控、策略编排、商业化、数据可视化等数据化/智能化基础运营能力,实现精细化人群/场景运营,推动飞猪从面向行业运营、流量运营的模式向面向用户运营、策略运营的模式转变。同时沉淀高质量运营物料与策略,并通过人群包、数据银行、商家运营平台、生意参谋等产品渠道赋能商家优化运营能力,提升运营效率。 这段时期从前端技术层面也做了不少事情,推进 Serverless 和 H5/小程序一码多端技术体系落地,重新定义云+端的研发模式;完成基于微前端+业务 SDK 化的中后台模式落地,面向运营场景对不同产品功能原子作一体化整合;构建互动玩法体系,赋能产品与 UED 更丰富的业务表现形态。目前聚焦在前端标准对接 Flutter Engine 自绘渲染的 Web on Flutter 体系构建,以及阿里经济体的跨端研发基础设施落地,继续提升研发效率及性能体验。 两年时间导购平台逐步承接了飞猪全域超过 70% 流量,同时实现了公域推荐搜索链路 GMV 占比 5% → 15% 的增长。我个人伴随业务一路成长也有不少收获: 技术跨栈,职能跨栈,思想跨栈 跨栈的本质是不设限,能写 H5 也能写 Node,能做前端也能做后端,能搞定技术也能搞定业务,关键在于投入够不够果断和持久。两年导购平台建设,不同阶段涵盖了前端、工程、算法、产品、业务、UED 等不同职能角色的突破需要,随着问题复杂度的提升,个人的力量会显得羸弱,尝试跳出自身技术栈与职能角色,围绕目标协同更多角色打大仗,挑战巨大却也充满价值。 因势而谋,应势而动,顺势而为 “风口之上,猪都能飞”,个人成长也好,业务发展也好,借好势能保证事半功倍。导购平台从 0 做到承载飞猪全域超过 70% 的流量,抓住了人员流失的风口重抓研发提效,抓住了全域个性化的风口提供统一算法埋点能力,抓住了场景化串联行业的风口统一数据模型与选品/投放能力,抓住了平台化的风口完成大部分公域投放业务收口。磨炼对业务方向与目标的敏锐感,顺势而为。 从管理者,到管理者的管理者 最近两年团队规模一路成长到大概 35 人,带队精力明显不够,于是在年中拆分了二级团队,直观感受到的变化: 信息链路变长,很多信息直接 get 不到了。 对一线同学的产出与状态感知变得不太清晰。 团队内部开始出现分工与边界问题。 在「一线管理补位」与「跨级管理顾虑」间反复横跳。一些会议逐渐不再参与,负责的事情也逐步下放,在 BU 内的存在感变弱,坦白说会有短暂的焦虑,尤其是下面同学能力很强时。 我也一直在调整自己的状态与精力分配: 努力往信息与规划的上游走,多与产品业务一号位、各技术域一号位沟通,保证信息通达。 把控宏观方向,从日常事务中抽出身来,结合业务思考未来 2~3 年的规划,跟进前期运作。 对新履职的 TL 给空间、做兜底,退二进三。 更多思考团队发展与人员成长的问题,使众人行。早前参加过一次管理培训,授课老师提到管理分几个阶段:不带人 - 带几个人 - 带几个管理者 - 做一号位;从第二到第三阶段不是人数变多的量变,而被类比为不带人-带人的质变,当时不理解,现在深以为然; 关于 Work life balance 职业成长到达一定阶段难以突破有时不是因为能力或机遇,而是源于家庭与生活,并不是说家庭不重要或者拖后腿,而正是由于家庭生活非常重要,而我们在努力工作过程中会潜意识忽视掉对家庭的参与与贡献,最后矛盾激化。其实 WLB 指的并不是 6 点还是 7 点下班,而是保证有限的家庭生活中能够全情投入(不玩手机、参与家务、制造惊喜感等等),不存在顾家就不能加班,热爱工作就无法兼顾家庭的两极分化;是否能像对待工作一样对待生活,高质量陪伴,共同谋划未来,甚至一起定一些 KPI——比如每月带孩子去一次游乐园——并努力拿到 3.75,才是 WLB 的真谛。 “人生就是一列开往坟墓的列车,路途上会有很多站,很难有人可以自始至终陪着走完。当陪你的人要下车时,即使不舍也该心存感激,然后挥手道别。” —— 《千与千寻》 本文起稿于9月中,刚起笔时笔者还在负责飞猪大导购方向,月末适逢团队调整,拖了一个多月才最终完稿;当下团队方向已然转移到飞猪的数字化运营平台体系(偏中后台)建设上来,变化不可谓不快。分分合合间,有人会留下,也有人会离开,选择本身没有对错,作为曾经一起拼搏的同路人,心怀感恩,也期待未来能够在某个路口再相逢。 借这个机会,也简单聊聊我对 “拥抱变化” 的理解。排斥变化是人性,我们潜意识里都会惧怕未知,首先认可这一点。把人的能力成长抽象成两根曲线: 一根是真实能力曲线,代表内在部分。 可以想象成一根弹簧,把它立在桌上,自顶向下俯看下去一直在绕圈,一如进入一家公司成为新人,几年时间变成老油条,换一家公司又变回新人;又如每次新技术的推陈出新,都从新手入门开始,继而写 Demo、做项目、看源码、fork 拓展,最后又被更新的技术替代。俯看每次画完一圈似乎都回到原点,但从侧面看过去,高度却一直在上升,无非是有时快一点,有时慢一点。 另一根是表象能力曲线,代表真正被别人看到的部分。 可以想象成一系列不连贯的正切曲线,把每段曲线看作一段经历,向上的势能往往是缓慢增加,到达峰值后缓慢下降的过程;而每次 “拥抱变化” 就是两段曲线间的纵身一跃,对技术、环境、人的陌生看似短时间内降低了我们的表象能力,但也意味着更大的潜在势能和未来更大的空间。 “拥抱变化”这件事并不是阿里特色,无论身处何时何地都会反复经历。关键点在于如何看待并接纳 “变化”,在不确定中寻找确定性,抓住每次惊险一跃的机会去提升和突破,并在表象能力背后去感受真实能力的成长。 结语 “今天的每个人都只是约 70 亿人类中的一员,而人类又只是地球上约 100 万物种中的一个。地球只是银河系约 1000 亿颗行星中的一个,而全宇宙有约 2 万亿个星系。人的寿命只是人类历史的约 3000 分之一,而人类历史又只是地球历史的 2 万分之一。换言之,我们渺小、短命得不可思议,无论取得什么样的成就,其影响都是微不足道的。同时,我们又本能地希望有意义,希望进化,而我们只能产生一丁点儿意义。所有这些一点点的意义加起来,才是宇宙进化的推动力。” —— 《原则》 瑞·达利欧“人固有一死,但正因为此,我们才是幸运儿。大多数人根本就不会死去,因为他们压根就没有诞生。这些潜在的、没有出生的人,数量超过了阿拉伯大沙漠中的沙粒;他们本来可能活着与我们同列,但事实上却永远也没有机会见到一丝阳光。显然,在那些没有诞生的生灵中,会有比济慈更伟大的诗人、比牛顿更伟大的科学家。我们知道这些,是因为我们的 DNA 所允许的‘可能存在的人’的数量,大大超过有机会出生的人。拥有这些罕见机会的幸运儿,就是你和我,就是生活在这世上的平淡无奇的人们。” —— 《解析彩虹》 理查德·道金斯 这篇文章是我在阿里一路成长的总纲,经验与教训都杂糅其中,如果你能看到这里,希望能有所收获,或有所启发。回头看在阿里一路的成长,校招毕业 4 年时间从 P5 到 P8,除了足够努力这个必要非充分条件外,我自知是无比的幸运,感恩这个时代,以及所处的行业与平台带来的巨大红利,感恩家人/朋友/导师/伙伴的支持。 今天团队新人越来越多,30 岁的眼睛望过去,满是自己 20 岁的影子,一样的阳光开朗、意气风发,想要成就一番事业;却也在满是荆棘的路上,一样充满汗水与泪水、挫败与迷茫。人的成长更多靠自己,环境只能催化与启发,要相信风雨之后是晴天,乌云之后有太阳,播下爱与梦想的种子,耐心等待她破土而出,生根发芽。 今年疫情、山火、洪水、蝗虫,天灾不断;灾难来临,站在社会与国家的层面 “人” 确实是太渺小了,“时代” 掸掸灰尘落到每个个体身上都是千钧重压。然而生活在这个欣欣向荣时代的我们又是如此幸运,身边每个人都充满希望,活在明天收入会更多、福利会更好、交通会更便利、物资会更丰富的预期里,坦然地认为这就是平凡得不能再平凡的人生常态,殊不知把 5000 年文明史放在坐标轴上,这样的时代甚至短到看不见;原来我们才是茫茫历史长河中的幸运儿,没有常年的饥荒、战乱、瘟疫、残酷的剥削,而是粮仓殷实、社会进步、科技发展、国泰民安。 因此,怀揣感恩的心态,享受当下的生活,你若盛开,清风自来。 最后的最后 我是圣司,这里是飞猪前端团队,一个「胜则举杯相庆,败则拼死相救」的集体。我们目前在 Serverless 、云端一体化、微前端、互动技术、智能化、跨端技术、导购技术等方向有所涉猎,如果你也有兴趣/思考,欢迎沟通交流或者直接加入一起开创未来(shudi.zsd@alibaba-inc.com)~ 飞猪是阿里巴巴集团旗下旅行品牌,围绕吃、住、行、游、购、娱构建一站式服务能力,服务全球 10 亿消费者,致力于让旅行更美好。同时,我们有强大的技术团队及极客的技术氛围,除前端外,客户端、服务端、算法等岗位也在热招中,期待你的加入!
一 SDK是什么 SDK全称是“Software Development Kit”,直译就是软件开发工具集。说的再通俗点就是一个面向开发者,针对特定领域的软件包。比如Java SDK(JDK),就是一个Java领域的软件包。基于它,开发人员就可以快速构建自己的Java应用。比较规范的SDK一般都会包含若干的API、开发工具集和说明文档。 JS SDK也无外于此,不过鉴于JS语言本身的特性,基于Ta封装的SDK更多常见于UI组件库、统计分析、web服务接口封装、前端稳定性和性能监控等场景。岳鹰前端监控SDK[1]即属于前端稳定性和性能监控这一领域范畴的SDK。 二 设计原则 如何设计SDK,其实更多取决于你的场景,或者SDK最终的用途。比如实现一个给网页调用的SDK与用于服务端的SDK就有明显的差异,但这之间确实存在着一些共通的原则,或者方法论: 最小可用性原则,即用最少的代码,如无必要勿增实体。 最少依赖原则,即最低限度的外部依赖,如无必要勿增依赖。 进一步阐述,即我们打造的SDK要符合以下的要求。 1 满足功能需求 SDK一般都是偏于面向某个领域,所以,同时在设计和实现的时候明确职责和边界很重要,同时还应该足够精简,专注领域内的业务。 2 足够稳定 绝不能导致宿主应用崩溃,这是最基础也是最严格的要求。 较好的性能,比如SDK体积应尽量小,运行速度尽量快。 可测试,保障每一次变更。 向后兼容,不轻易出现 Breakchange。 3 少依赖,易扩展 最小程度的第三方依赖,尽可能自行实现,确实无法避免则最小化引入。 插件化,最大限度支持扩展。 Hook机制,满足个性化诉求。 三 如何实现 下面我们将通过剖析岳鹰前端监控SDK的设计过程,来看看上述的设计原则是如何应用到实际的开发过程中的。 1 明职责,定边界 前面章节提到,岳鹰前端监控SDK是前端稳定性和性能监控的SDK,主要面向前端H5领域。因此,稍加分析即可得出以下结论: 前端领域,稳定性方面主要的关注点 JS异常 资源加载异常 API请求异常 白屏异常 性能方面,核心的关注点 白屏时间 可交互时间(TTI) 首屏时间 FP / FMP / FCP 等 上述监控内容实际上都相对独立,因此我们可以把它们横向划分为如下几大部分: 明确了SDK的边界以及各部分的职责,结合前端监控的特性,我们可以开始设计SDK的整体框架了。 2 筑框架,夯基础 俗话说千里之行始于足下,因此筑牢基础十分重要。总得来说,我们需要做好下面几点: 确定SDK的引用形式 SDK整体而言是一个大模块,前端模块有多种表现形式:ES Module、CommonJS、AMD/CMD/UMD,而在引用方面则大体分 CDN和 NPM两种。即无论我们实现的是哪种形式的模块,最终都是通过CDN或者NPM的方式提供给用户引用。 // ES Module import wpkReporter from 'wpkReporter' // CommonJS const wpkReporter = require('wpkReporter') // AMD,requireJS引用 require.config({ paths: { "wpk": "https://g.alicdn.com/woodpeckerx/jssdk/wpkReporter.js", } }) require(['wpk', 'test'], function (wpk) { // do your business }) 乍看有点眼花,但事实上今时今日的前端工程领域,已有很多利器可以帮助我们达到目的。比如webpack,通过简单的配置就可以构建出一个UMD的bundle。 // webpack.config.js module.exports = { output: { filename: '[name].js', path: `${__dirname}/dist`, globalObject: 'this', library: '[name]', libraryTarget: 'umd' } } 综上,我们可以通过webpack将SDK构建为一个UMD bundle,这样可以自动适配所有形式的模块。同时我们也将同时提供CDN和NPM两种引用方式,给用户更多选择。 确定SDK的版本管理机制 现有较成熟的版本管理机制当属语义化版本号[2],表现形式为 {主版本}.{次版本}.{补丁版本},简单易记好管理。 一般重大的变更才会触发主版本号的更替,而且很可能新旧版本不兼容。次版本主要对应新特性或者较大的调整,因此也有可能出现breakchange。其他小的优化或bugfix就基本都是在补丁版本号体现。 看到此处,是否有点似曾相识的感觉?没错,所有NPM模块都遵循语义化版本规范,因此结合第一点,我们可以将SDK初始化为一个NPM模块,结合webpack的能力就可以实现基础的版本管理及模块构建。 确定SDK的基础接口 接口是SDK和用户沟通的桥梁,每一个接口对应着一个独立的SDK功能,并且有明确的输入和输出。我们可以先来看看岳鹰前端监控SDK的核心接口有哪些? // 上报相关 wpk.report(logData) wpk.reportJSError(error) wpk.reportAPIError(apiData) // 配置变更 wpk.setConfig(data) // SDK诊断 wpk.diagnose() // 添加插件 wpk.addPlugin(plugin) 总结接口的设计原则,如下: 职责单一 一个接口只做一件事情 命名简单清晰,参数尽量少但可扩展 好的接口命名就是最好的注释,一看即明其用处 参数尽可能适用Object封装 做好参数校验和逻辑保护 3 领域分析,模块划分 定边界的时候,我们已经清楚划分了SDK的几个关键的部分:全局异常、API异常、页面性能和白屏,实际上监控SDK通常也会内置对页面流量的监控,以方便用户对异常的影响面做出评估。这几个核心的关键组成部分,每一块都对应一个专业的领域,因此对应到SDK也是每一个独立的模块。 除了这些核心的偏领域的模块,SDK还需要有更基础的与领域无关的模块,包括SDK内核(构造方法、插件机制、与下游服务的交互、上报队列机制、不同环境的管理等等)和工具类库。 我们可以先看一下岳鹰前端监控SDK最后的整体模块划分: SDK底层提供基础的能力,包括上面提到的内核、插件机制的实现、工具类库以及暴露给用户的基础API。 可以看到,我们前面提到的所有模块都以插件的形式存在,即各领域的功能都各自松散的做实现,这样使得底层能力更具通用性,同时扩展能力也更强,用户甚至也可以封装自己的插件。 Biz部分更多是对于不同宿主环境的多入口适配,当前支持浏览器、Weex以及NodeJS。 4 测试覆盖,线上无忧 SDK是一个基础服务,相对于前台业务而言可能更底层些。其影响面跟应用的范围是正比的关系,更多的用户意味着更大的责任。所以SDK的质量保障也是很重要的一个环节。 岳鹰前端监控SDK的质量保障策略很简单,只有两条: 核心接口100%的单元测试覆盖率 发布卡点:再小的版本发布也需要走集成测试回归 事实上,除了核心接口,工具类库的所有功能我们都实现了100%的单元测试覆盖,我们采用的前端测试工具是轻量好用的Jest[3]。 // 小巧精炼的 Jest,笔者力荐 test('isError: real error', function () { var err = new Error('this is an error') expect(util.isError(err)).toBeTruthy() }) 5 细节打磨,极致体验 快捷引入 极尽所能提高用户引用的效率 一行代码,快速引入,享用监控全家桶功能 <script> !(function(c,i,e,b){var h=i.createElement("script");var f=i.getElementsByTagName("script")[0];h.type="text/javascript";h.crossorigin=true;h.onload=function(){c[b]||(c[b]=new c.wpkReporter({bid:"dta_1_203933078"}));c[b].installAll()};f.parentNode.insertBefore(h,f);h.src=e})(window,document,"https://g.alicdn.com/woodpeckerx/jssdk/wpkReporter.js","__wpk"); </script> 动态采样 即通过云端下发数据采样率的方式,控制客户端上报数据的频率 更好的保护监控下游 自我诊断 除了接口,SDK整体对用户而言就是一个黑盒,因此用户在遇到问题时很容易蒙圈 (如:为啥没有上报数据) SDK可以提供一个自我诊断的接口,快速排除基础问题。 比如,SDK是否已正常初始化、关键参数是否正常设置等。 增加调试模式,输出更详细的过程日志,方便定位问题 渐进式的指引文档 图文并茂,循序渐进 入门,一步步引导用户初识SDK,领略概貌,学会基本的使用 进阶,安利SDK的深度用法,帮助用户更好的使用SDK 四 结语 实际在SDK的设计和开发过程中,要处理的问题还远不止本文所述的内容,比如NPM模块开发时本地如何引用,构建的bundle大小如何调优等等。不过还是希望阅完此文,对你有所启发。同时文中若有不对之处,还望不吝赐教。 相关链接 [1]https://yueying-docs.effirst.com/api-usage.html[2]https://semver.org/[3]https://jestjs.io/
Widget 简介 Widget 是 iOS 14 重磅推出的新功能,使得用户可以在主屏幕添加小组件,快速浏览 app 提供的重要信息。它的设计与旧版本 macOS 的 Widget 一脉相承,甚至连添加的动画也是去掉了拟物化的水波纹效果。 设计定位 用户可以通过 Widget 对主屏幕进行个性化定制,但是 iOS 14 的 Widget 跟其他系统上的小组件有很大的区别。在 Widget 的设计上苹果也保持了一贯的克制,定位于轻量化、仅用作关键信息的展示。比如系统自带 Widget 中的股票、天气、电量、运动信息,他们的共同特征是更新频率高、提供的信息重要,让用户不用打开 app 就可以浏览关心的内容。苹果也不希望开发者将 Widget 仅仅当作 app 的一个快捷入口,这样的需求更适合用 contextual menu 来实现。 限制 苹果基于上面的设计定位,同时也为了节省系统资源保证续航,对 Widget 的做了一些限制: 不支持动画,仅支持静态页面展示。 更新频率由系统通过机器学习来动态分配。 不支持拖拽、滚动等复杂的交互,不支持 Switch 等控件。 用户点击 Widget 一定会跳转到 app。 Widget 开发实践 盒马小镇 Widget 设计 盒马小镇是盒马 app 内的一个小游戏,用户可以通过签到、购物、内容互动来获取盒花,用户最关心的就是有没有盒花可以收取,或者是否有盒花可以帮摘等信息。这正好可以契合 Widget 的设计初衷,我们期望能借此提升用户粘性。 要实现一个 Widget 首先要升级到 Xcode 12,然后在工程中增加一个新的 Widget Extension,Xcode 会自动生成需要的模板文件。 我们现在要做的只有两件事:数据加载、渲染界面。 登录及授权 加载数据之前首先要解决的是授权问题。由于集团的登录 SDK 暂时不支持 extension,加上全家桶依赖复杂、体积很大,我们不能使用 MTOP 来进行 API 调用,因此我们设计了一套认证机制,主 app 在登录后调用 MTOP 接口获取 token,并且将 token 写入 app group 中,Widget 通过 HTTPS 接口加上 token 来访问用户数据。 其中 token 通过 UserDefaults 来共享到 Widget,因为开发过程中不同的证书打包的 bundle id 不同,因此我们将 group name 设置成 group.[bundle id] 的形式,保证能正确读取 token。 var token: String? { let bundleIdentifier = Bundle.main.bundleIdentifier! let defaults = UserDefaults(suiteName: "group." + bundleIdentifier) return defaults?.object(forKey: "widget.town.homeinfo.token") as? String} Token 的同步有几个时机: App 启动后,如果已经登录并且 app group 中没有 token,则获取 token。 用户进入盒马小镇时获取 token。 用户登录时获取 token。 用户登出时删除 token。 数据更新 这里苹果借用了 timeline 的概念,timeline 里面包含了一个个 entry,entry 就是我们在特定时间需要展示的数据。系统先回调我们来获取 timeline 数据,再在特定时间来回调我们渲染界面。 我们在返回 timeline 时可以设置更新策略: atEnd:在 timeline 中所有的 entry 都展示完之后更新。 never:仅在主 app 触发更新。 after:在特定时间后触发更新。 当然这些都只是我们建议系统的更新的时机,实际是否更新还是取决于系统。对于可以预测信息的,比如天气预报,可以在 timeline 中添加多个 entry,并且选择 atEnd 作为更新时机;对于不可以预测信息的,比如股市,可以选择 after 策略,在一定时间后更新信息。 除此之外,我们还可以在主 app 中调用 WidgetCenter 的方法来触发更新,经我们测试使用这种方法每次调用都是能够真正触发数据更新的。更多信息可以参考官方文档[1]。 在盒马小镇的场景下,我们是无法预知到将来的数据的,因此每次返回的 timeline 中只包含一个 entry,选取的策略是 after,每 20 分钟更新一次数据。 值得注意的是,在 getSnapshot 方法中也要返回真实的数据,这样用户在 Widget 的添加过程中看到的就是正确的界面,真正做到所见即所得。 在用户在主 app 中收取盒花之后,我们还会主动触发一次数据的更新。 渲染界面 苹果为了推广 SwiftUI,规定 Widget 只能使用 SwiftUI 来编写界面。SwiftUI 与 Flutter 类似,是随 iOS 13 推出一套声明式的 UI 框架。SwiftUI 在这一年中有了很大的进步,补齐了很多能力上的不足。它使得跨平台(仅苹果生态)的界面开发变得非常简单。此外 SwiftUI 还支持 preview(类似 Flutter 的 hot reload),代码的改动可以近乎实时地反应在 UI 上,极大提高了开发效率和体验。 如果有 Flutter 开发经验,只要熟悉 VStack、HStack、ZStack 以及常见的几个 modifier 就可以轻松上手。 Bundle 拆分 与集团的大多数 app 一样,我们的 app 工程也分为壳工程和业务 bundle,我们不希望每次修改 Widget 都去修改壳工程,于是要把 Widget 的业务逻辑拆分开来。经过尝试发现一个可行的办法: 新建一个 Swift framework bundle,将 Widget 实现放在里面,并且将里面的类设置成 public。 在壳工程中新建一个 Widget Extension,引入上面的 framework,在里面保留 @main 方法。 @main struct HMTownWidget: Widget { let kind: String = "HMTownWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: Provider()) { entry in HMTownWidgetEntryView(entry: entry) } .configurationDisplayName("盒马小镇") .description("快速查看盒花动态,访问盒马小镇收盒花") .supportedFamilies([.systemSmall, .systemMedium]) } } 这种方法可以减少壳工程改动的风险,并且可以正常使用 SwiftUI 的 preview 和调试功能,缺点是 Widget 的配置信息仍在保留在壳工程中,暂时没有更好的办法在保证调试功能的同时完全分离壳工程和业务代码。 Swift Bridge 前面提到在用户摘取盒花之后,我们需要触发 Widget 的更新,这里需要调用 WidgetCenter 的 API,而这个 API 仅提供了 Swift 版本,而我们的主 app 是纯 OC 写的,需要做一个 bridge。要实现 bridge 必须开启 Define Modules,而我们的工程由于历史原因在开启后无法编译通过,现在的解决方法是新建一个 Swift framework,里面调用 Widget Center API,再由业务去引用这个 bridge 的 framework。 埋点 Widget 的曝光事件我们是无法感知的,由于点击 Widget 会直接跳转到主 app,所以我们在跳转到主 app 的 URL 上增加了埋点参数,主 app 解析 URL 中的参数调用 UT 来埋点。 审核 在我们一开始打完包提交到 TF 审核时被拒了: 理由是我们使用了无效的字体,这个字体是我们在很早的版本就已经依赖的 Flutter 三方库引入的,之前的审核也一直没问题,在去掉了依赖之后成功过审,原因不明。Flutter 官方 issue[2] 提到只有在增加了 Widget 之后才会遇到这个问题。 Swift 的副作用 目前盒马 app 最低支持 iOS 9.0,而 iOS 直到 12.2 才将 Swift 基础库集成到系统中,因此我们需要在 Build Settings 里面将“ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES”设置为 “YES” 才能在 12.2 及以下的系统上运行。 Swift 虽然在 5.0 版本完成了 ABI 稳定,但是在低版本操作系统中 (iOS 12.2 以下) 仍旧会有一些不够完美的地方。 在低于 iOS12.2 以下的操作系统会带来约有 3MB 的包大小问题,但幸运的是苹果放开了蜂窝数据网络下 200M 的下载大小。在低于 iOS12.2 以下的操作系统会有 100-200ms 不等的启动耗时增加,但在团队同学的努力下上线了二进制重排,启动性能大幅度上升。具体分析请查看:手淘架构组最新实践 | iOS 基于静态库插桩的⼆进制重排启动优化[3]。——《手淘 Swift 2019 大事记》 好在 iOS 的升级率比较高,相信随着时间的推移 Swift 的应用会越来越多。 最后 目前新版本已经正式发布(4.54.1),欢迎下载体验,App Clip 商品溯源也同步上线。现在的版本仍存在一些问题,比如用户如果已经打开小镇页面,通过点击 Widget 打开 app 仍然会再次打开小镇页面,需要从路由的层面优化;中尺寸的卡片相对于小尺寸也并没有透出更多的信息,与苹果的设计初衷相违背。不过也算是对新技术的一次尝试,后面会持续优化,期待能产生业务上的一些增量。 相关链接 [1]https://developer.apple.com/documentation/widgetkit/keeping-a-widget-up-to-date[2]https://github.com/flutter/flutter/issues/65991[3]https://mp.weixin.qq.com/s/YDO0ALPQWujuLvuRWdX7dQ
概览 因为长期在做跟阿里云飞天大数据平台相关的前端工作,也一直在思考一个问题:“大数据的前端跟其他业务的前端有什么不一样”,具体来说就是,在大数据和人工智能的浪潮下,到底对前端技术的发展带来了什么影响。 以团队在负责在做的阿里云飞天大数据平台为例,从在 2009 年写下第一行代码,现在已经是阿里大数据发展的第 11 个年头。我是 2011 年加入阿里的,之后就一直在负责做大数据相关的前端工作,基本上参与了阿里绝大部分大数据发展的历史进程。现在回头看,很庆幸自己在一个历史的变革时期入行,更有幸见证了一些划时代意义的数据产品的诞生,以及它们对前端技术带来的变革。 如果我们把 2010 年当做大数据 Web 产品应用的元年,会发现它是一个有趣的年份,为什么这样讲? 回看前端的发展历史,在 2005 年前后有一波大的技术变革,就是从 Web1.0 到 Web2.0 的过渡。 在此之前,前端更多地是做纯内容的静态展示,比如下图中的那个时期的苹果和雅虎的官网。 之后前端开始逐渐做成复交互的动态网页,这其中一个重要的历史性标志就是 Gmail 对 Ajax 等新技术的应用。 而在 2010 年前后,各种大数据应用进入一个爆发期间,阿里很多知名的应用基本都在那段时间展露头角,现在回头再来看那段历史,这其中很大的一个原因,随着互联网的大发展,特别是 Web2.0 之后,数据的有了大爆发的增长。 下图就很好地展现了这个趋势,如果说之前的 Web 应用更多在“产生”数据阶段,那在 2010 年之后如何更好的“展现”数据被提上了新的高度,很多前端技术也因之打开了新的篇章。 后面会结合自己的实践,以三条主线来讲讲数据智能浪潮对前端技术发展的影响,分别是数据可视化,软件泛 Web 化和交互多样化。 数据可视化 大数据浪潮下,最明显的一个特征就是数据的指数型增长,从上图中就能看到这个趋势,随之而来的挑战就是如何更形象地展现数据并进行交互展示,也就是我们通常讲的“数据可视化”。 回到技术本身,那数据可视化对前端最大的影响应该是大大促进了 SVG,Canvas 和 WebGL 的发展。 而这当中,除了浏览器底层技术的升级,在上层可视化库和可视化应用也涌现了大量优秀的作品,其中佼佼者包括: 开源技术组件层面 AntV Echarts HighLights ... 重数据可视化的产品 阿里云大屏可视化产品 DataV 阿里云的 Quick BI BI 分析工具 Tebleau 特色领域的分析产品,比如 Plantir 在专业的细分领域,比如地理,安防,新零售,等领域中不同场景就有很多机会。具体比如在我们阿里云的一站式大数据开发治理平台的 DataWorks[1] 产品就有用于做流程编排的 DAG,图分析[2],数据的血缘分析等有意思的可视化。 软件 Web 化 大家最近应该注意到一个现象那就是:Web 系统做得越来越复杂,很多原先桌面端的复交互应用逐渐 “泛 Web 化”,甚至很多应用一上来就是 Web 的技术做第一版。 这里说的泛 Web,从表现中又可以分为两种: 一是直接用前端技术去做桌面软件,其中标志性事件就是 NW.js 和 Electron 在 2013 起步后的蓬勃发展;大家熟悉的 IDE VSCode 就是这当中的典型代表;阿里的桌面版钉钉 UI 层大量用到的 Web 的技术。 另外一种就是直接在 Web 上实现,比如 大家最近能看到各种 Web'X' 系统( Google Docs )。 这背后推动力,一是随着浏览器相关逐渐走向统一,用它的技术可以更便捷地实现跨端,另一个就是云计算大数据的兴起,特别云端的存储和算力逐渐突破了原先的本地 PC 的性能边界,因而重塑了原先人机交互的入口。 关于跨端的好处自不用多讲,我想想重点讲讲第二点。要讲这个逻辑,我又得简单讲讲计算机的发展,从占地 170 平方米的世界上第一台通用计算机 “ENIAC”,到苹果和微软时代的个人 PC,移动时代的 iPhone 和 Andriod,再到云计算时代的大型计算集群。 对开发者工具而言,之前前很多软件很多都是本地,因为它往往用本地 PC 的计算力就够了,但大数据的场景下计算本地算力肯定是不够的,它是依赖云端的计算集群(以我们阿里飞天大数据平台而言,我们已经 10 万台计算集群的规模),如何在用户侧用上更方便和灵活地使用这些算力就是我们前端重点要做的,而这是原先软件的架构要不不能让你做定制,要不定制的成本很高(有时候甚至超过了重新做一套的成本),因此很多系统会选择重新起航做一版。 这其中,我们负责阿里云的 Dataworks 中的两大件:WebIDE 和 WebExcel ,就非常典型的例子。 Dataworks 从一开始就是根据云原生的思路设计开发的,后端需要通过云计算提供强大的算力替换原先的本地算力,前端需要实现更精巧的架构设计来对应日益复杂的交互能力;具体到我们的应用,它包括但不限于: 架构层面 状态管理 插件化 ... 复交互的组件 Editor Form/Excel Tree Logivew ... 交互多样化 最近今年在以数据驱动的人工智能的大力发展下,特别在图像识别,语音识别,自然语言处理方面获得了很大的突破,让前端的新交互也获得了长足的进步。 UX 在面向使用者(UX)产品由 GUI(Graphical User Interface)变成 XUI,用户不仅可以用通过鼠标键盘方式操作图形界面,更可以通过面部表情,身体动作,语音交互等形式提供新的交互形态。 下图就是在 2016 年左右,我们在阿里云ET中一些人机对话,互动游戏中的一些实践,具体可以看这里[3]。 这一轮的技术变革,有两个大的宏观的背景。 AI 技术的第三波潮起 随着 2010 年前后,深度学习技术的成熟,计算力的提升,以及互联网时代积累的大数据财富,人工智能技术开始一段与以往大为不同的复兴之路;分别在语音识别,图像识别,自然语言处理等相关技术上获得根本的突破。 例如, 2012 年到 2015 年,在代表计算机智能图像识别最前沿发展水平的 ImageNet 竞赛(ILSVRC)中,参赛的人工智能算法在识别准确率上突飞猛进。2014 年,在识别图片中的人、动物、车辆或其他常见对象时,基于深度学习的计算机程序超过了普通人类的肉眼识别准确率。 下图就摘自李开复老师的《人工智能》就体现了这个趋势: WebRTC 对于前端来讲,另一个必备条件就是 WebRTC (Web Real-Time Communication)技术的成熟,它于 2011 年 6 月 1 日开源并在 Google、Mozilla、Opera 支持下被纳入万维网联盟的 W3C 推荐标准。通过它,前端可以便捷地处理图像,视频,语音等内容。大家目前看到很多有意思的交互底层就是依赖他。 DX 在面向前端开发者(DX):智能化手段可以提升我们的研发效率和体验,以我们阿里和蚂蚁自身的实现看,Imgcook(D2C:Desgin to Code),代码智能提示[4],智能可视化 AVA[5],前端机器学习 pipcook[6] 都是挺有意思的尝试。 总结 以上就是我在实践中关于数据浪潮下前端技术发展的一些思考。当然前端技术技术这几年能获得这么长足进步,除了数据智能,其他大趋势(比如移动互联,5G,IoT)也深刻影响了前端技术的走向,但这些就不在本文讨论的范围内,有机会再跟大家讨论。 一直很喜欢吴军在《智能时代》一书中提到的一个观点:“2% 的人将控制未来,成为他们或者被淘汰”。期望各位前端同学都能在这波数据智能化的浪潮中找到自己的定位。 写在最后 如果大家对这块感兴趣,也希望来阿里巴巴一起做大数据和人工智能相关的工作,随时欢迎私信或者发简历给我:jifeng.zjd@taobao.com。大家一起合作,做件有意义的事情,团队长期招人。 相关链接 [1]https://www.aliyun.com/product/bigdata/ide[2]https://zhuanlan.zhihu.com/p/132393588[3]https://www.zhihu.com/question/56560321/answer/203249193[4]https://zhuanlan.zhihu.com/p/115377444[5]https://github.com/antvis/AVA[6]https://github.com/alibaba/pipcook
2020年11月
2020年10月
浙江省疾控中心宣布,利用阿里达摩院AI算法,首次将原本数小时的疑似病例基因分析缩短至半小时!大大提升了检测效率,为后续疫苗与药物研发打下坚实基础!
作为一名足球迷,在年终盘点中,怎么能少了足球呢。如果你是关注欧洲足球的球迷(没办法,人家欧洲是足球的世界的中心),那么你一定知道,2019年是属于利物浦的一年,不多说,红军的首个英超冠军已经遥遥在望了,fighting!
数据获取
我们还是先来看看如何拿到我们需要的数据呢,我们这里选择的是一个国外的统计网站,可以查看历年的足球俱乐部得分和排名
https://footballdatabase.com/ranking/world/1
获取俱乐部信息 首先我们需要拿到俱乐部相关的信息,这里选取前200名俱乐部,使用 BeautifulSoup 解析网页即可
for name in name_list:
rank_list = []
try:
rank_point = name.find_all('td', attrs={'class': 'rank'})
rank = rank_point[0].text
point = rank_point[1].text
club_info = name.find('td', attrs={'class': 'club text-left'}).find_all('a')
club_url = club_info[0]['href']
club_name = club_info[0].find('div', attrs={'class': 'limittext'}).text
club_country_url = club_info[1]['href']
club_country_name = club_info[1].text
rank_list.append([rank, club_name, club_country_name, club_url, club_country_url, point])
list_rank.append(rank_list)
save_club_name(rank_list)
except:
pass
拿到各个俱乐部的信息后,就可以遍历该列表,获取俱乐部历年的得分和排名数据
获取俱乐部历史数据
俱乐部的历史数据也是保存在网页的 Javascript 变量中的,对于如何获取这种数据,我们在前面的文章中也都有介绍,这里就不过多赘述了
his_rank_list = []
for data in data_list:
print(data)
url = data[0][3]
his_res = requests.get(base_url + url)
his_content = BeautifulSoup(his_res.text, "html.parser")
js = his_content.find_all('script')[1].string
src_text = js2xml.parse(js)
src_tree = js2xml.pretty_print(src_text)
data_tree = BeautifulSoup(src_tree, 'html.parser')
array_list = data_tree.find_all('array')
club_name = data[0][1]
for array in array_list[2:-2]:
his_rank_list = []
array_date = array.find('string').text
date = array_date
try:
month = month_map[array_date.split(' ')[0]]
year = array_date.split(' ')[1]
date = str(year) + '-' + str(month)
except:
pass
array_data = array.find_all('number')
try:
point = array_data[0]['value']
rank = array_data[1]['value']
his_rank_list.append([rank, point, date])
save_his_data(club_name, his_rank_list)
except:
raise
这样我们就可以得到两个文件,分别是 club_data.csv 和 rank_his_data.csv
俱乐部排行
我们这里整理的当前世界俱乐部前20的排名,可以看到利物浦以2120分高居榜首,曼城则紧随其后,看来世界第一联赛还是很给力的!
接下来就是西超的巴塞和德甲霸主拜仁,在意甲实现了八连冠的老夫人以及无敌于法甲的大巴黎。
欧洲五大联赛,名不虚传
怎么样,快来看看有没有你熟悉的队徽上榜呢!
国家分布
下面我们再来看看哪些国家的俱乐部最多呢,首先是前20名的国家分布
可以看到,英格兰和西班牙的俱乐部最多,都有4家上榜,而次之的是意大利以及德国,都是3家,这四个国家就占去了大半。而唯一一家非欧洲的俱乐部则位于巴西,足球王国也是要面子的哇!
我们再把榜单扩大到所有的抓取数据,即前200名的俱乐部数据
西班牙的俱乐部最多,达到了20家,其次是英格兰也有17家之多。这些年西甲英超的球队垄断欧冠冠军,还是有其深厚的足球底蕴作为依托的!
而欧洲传统足球强国西班牙、英格兰、德国、法国以及意大利再加上南美双珠阿根廷和巴西,这七大豪强的俱乐部基本占据了榜单的半壁江山,足球的格局,从来如此!
当然中超也有两家上榜俱乐部,分别是133名的上海上港和166名的广州恒大,任重而道远呐!
top7 排名走势
这七家俱乐部既有欧陆的传统豪门,也有金元新秀,都是当今俱乐部中的顶级强队。
我们可以看出,对于“皇萨仁”来说,它们的成绩都是极其稳定的,常年处于极高的竞技水平上。而当红的利物浦则在本世纪初的前十年遭遇到了低谷,一路前行,着实不易。
再有就是金元足球的代表大巴黎和蓝月亮,其中大巴黎尤为明显,从2011年开始,得益于卡塔尔财团的进入,其排名开始急速攀升,在世界范围内的影响力也大幅提高,战绩更是不断突破,常年垄断法甲冠军,不过对于欧冠赛场,巴黎还有很长的路要走。
而对于老牌劲旅尤文图斯,相信大家都会想起著名的“电话门”事件,此事件之后,不仅尤文图斯跌落低谷,就连意大利足球,曾经的亚平宁联赛的风采,都一去不复返了。不过从2011年开始,尤文走在了复兴的路上,也扛起了意大利足球的大旗!
各大洲排名
我们从上面的排名不难看出,所谓的世界排名,其实大多是欧洲俱乐部,那么其他大洲的俱乐部呢,我们一起来看下。
亚洲:
非洲:
南美洲:
最后还是来看看近十年世界足球俱乐部排行的变化情况:https://mp.weixin.qq.com/s/PJ8AS_OZJNWiyZ-GXCUnmg
来自:今日头条,作者:聚IT
链接:https://www.toutiao.com/i6752317753866060299/
导读
在实际项目中Redis常被应用于做缓存,分布式锁、消息队列等。但是在搭建配置好Redis服务器后很多朋友应该会发现和有这样的疑问,为什么Redis默认建立了16个数据库,如下图所示。
椐调查发现:93.7%的程序员!竟然都不知道Redis为什么默认16个数据库?
一、16个数据库的由来
Redis是一个字典结构的存储服务器,一个Redis实例提供了多个用来存储数据的字典,客户端可以指定将数据存储在哪个字典中。这与在一个关系数据库实例中可以创建多个数据库类似(如下图所示),所以可以将其中的每个字典都理解成一个独立的数据库。
以MySQL实例为例
Redis默认支持16个数据库,可以通过调整Redis的配置文件redis/redis.conf中的databases来修改这一个值,设置完毕后重启Redis便完成配置。
客户端与Redis建立连接后会默认选择0号数据库,不过可以随时使用SELECT命令更换数据库。
# 切库
redis> SELECT 1 # 默认0号db,切换为1号db
OK
redis [1] > GET username # 从1号库中获取 username
(nil)
在实际项目中则可以通过以Redis配置文件的形式指定数据库,如下图所示
二、正确理解Redis的“数据库”概念
由于Redis不支持自定义数据库的名字,所以每个数据库都以编号命名。开发者则需要自己记录存储的数据与数据库的对应关系。另外Redis也不支持为每个数据库设置不同的访问密码,所以一个客户端要么可以访问全部数据库,要么全部数据库都没有权限访问。但是,要正确地理解Redis的“数据库”概念这里不得不提到一个命令:
# 清空一个Redis实例中所有数据库中的数据
redis 127.0.0.1:6379> FLUSHALL
该命令可以清空实例下的所有数据库数据,这与我们所熟知的关系型数据库所不同。关系型数据库多个库常用于存储不同应用程序的数据 ,且没有方式可以同时清空实例下的所有库数据。所以对于Redis来说这些db更像是一种命名空间,且不适宜存储不同应用程序的数据。比如可以使用0号数据库存储某个应用生产环境中的数据,使用1号数据库存储测试环境中的数据,但不适宜使用0号数据库存储A应用的数据而使用1号数据库B应用的数据,不同的应用应该使用不同的Redis实例存储数据。Redis非常轻量级,一个空Redis实例占用的内在只有1M左右,所以不用担心多个Redis实例会额外占用很多内存。
三、集群情况下是否支持一个实例多个db?
要注意以上所说的都是基于单体Redis的情况。而在集群的情况下不支持使用select命令来切换db,因为Redis集群模式下只有一个db0。再扩展一些集群与单机Reids的区别,感兴趣的朋友可以去查阅相关的资料深入理解,这里就不做讨论了。
四、总结
Redis实例默认建立了16个db,由于不支持自主进行数据库命名所以以dbX的方式命名。默认数据库数量可以修改配置文件的database值来设定。对于db正确的理解应为“命名空间”,多个应用程序不应使用同一个Redis不同库,而应一个应用程序对应一个Redis实例,不同的数据库可用于存储不同环境的数据。最后要注意,Redis集群下只有db0,不支持多db。
随着前端技术的发展,前端开发从静态网页的开发到复杂的前后端交互再到基于node.js的全栈开发,前端需要做的事情越来也多,前端代码的逻辑和交互效果越来越复杂,越来越不易于管理。模块化开发和预处理框架把项目分成若干个小模块,增加了最后发布的困难,没有一个统一的标准,让前端的项目结构千奇百怪。在新技术不断涌入的多元化发展模式中,Web前端工程师的工作量也越来越大。前端自动化构建在整个项目开发中越来越重要。如果能合理地采用一些自动化的工具,Web前端开发工作就会轻松很多。
现在介绍4款Web前端自动化工具,提高你的工作效率。
Web前端自动化工具之一:LiveReload
LiveReload技术+两块显示屏可以帮你省去重复刷新浏览器这一枯燥的工作。目前实现LiveReload的方式很多,如果你倾向于图形化的桌面应用,可以尝试一下。这款应用同时有Mac版和Windows版,使用起来也很简单,设置好需要监听文件所在的文件夹,然后将一段脚本插入到HTML页面即可。
Web前端自动化工具之二:Webpack
现在做前端开发,通常还会涉及到预处理器,虽然技术的多样化给我们带来了更多选择,但要这些技术产生的代码在浏览器中获得一致的表现,还得将其转化为浏览器支持的类型。Webpack是一款模块加载兼打包工具,丰富的插件让这款工具非常实用。虽然现在Grunt 和Gulp作为两款前端自动化工具非常流行,但其实Webpack结合Npm脚本在大多数场合就已经足够了。
Web前端自动化工具之三:WeFlow
WeFlow 是最近腾讯团队推出的一款前端开发工作流工具。WeFlow一个高效、强大、跨平台的前端开发工作流工具,具体的说就是一个GUI的前端工具,可以为用户提供一套标准化、规范化的工作流程,从而让大家在交接协作的时候更为高效有序。
Web前端自动化工具之四:CodeKit
除了免费的工具,还有一款付费工具值得一提。CodeKit是Mac下老牌的前端开发辅助工具,目前价格32美刀。虽然不便宜,但功能强大,号称可以编译目前所有的前端脚本,支持浏览器自动刷新,内置Bower,第三方包的安装只需要一次点击即可完成。图形化的界面操作起来也很方便,不差钱的同学可以考虑。
以上就是我为大家介绍的目前常用的Web前端自动化工具。前端作为互联网产品研发的重要环节,工作量势必会越来越繁重,所以能合理的运营一些自动化的工具,不仅仅可以提高自己的工作效率。同时也可以让前端开发工作变得更加简单。
这其实就是“前端例外”论的一部分,不管是想用来证明“前端不值一提”还是想证明“前端的地位不可替代”,都只能证明说这句话的人目光短浅。
不管什么种类的程序员都首先是程序员。不排除少数特定的工种(比如切图师)可能不需要数据结构,但是这不等于整个领域都没有啊。
写前端总不能不和数组打交道吧?这就是数据结构。
哈希对象也是数据结构,DOM 树也是数据结构。更不用说前端要用到的模型基本上都是数据结构了。整天都在和数据结构打交道,还说前端没有数据结构,这就是明目张胆的撒谎了。
没有一个初步的战略
大多数没有计算机科学或数据分析背景的工程师想要在数据科学中开始一个新的职业生涯,他们没有一个明确的战略,没有成为数据科学家、分析师或工程师的明确步骤。他们试图尽可能快地用信息填满自己的脑袋,而不是真正深入到特定的主题;他们倾向于一次注册多个在线课程,从不同的网站下载几个备忘单,阅读许多作者的文章,但没有一个结构化的计划。在开始这段旅程之前,我强烈建议你制定一个学习计划,并列出一些日常习惯,以实现你的目标,增强你的分析和编程技能。对你想从事的行业使用的最流行的编程语言和软件进行自己的研究,搜索最广泛使用的库和包,并根据你的目标选择最适合你的编程语言和软件。坚持和练习会使你成为大师。
尝试同时学习几种编程语言和软件
新程序员常常会受到诱惑,想要同时学习几种编程语言和软件,把它们作为技术技能写进简历。虽然你可能认为这是一种营销自己的策略,但它往往会适得其反。拥有数据科学、数据分析师和数据工程职位的公司和组织更有可能要求应聘者具备一种或两种或最多三种编程语言和软件的坚实背景。很少有职位要求你同时精通Python, R, SQL, C, c , c#, Matlab, Java, Ruby。相反,你应该研究一下你更可能在某个特定行业或公司使用的编程语言和软件;掌握你的编程和分析技能,并成为真正的专家。你将认识到,所有编程语言之间共享一个公共逻辑和类似的函数,在此之后,从一种语言到另一种语言的转换只需要学习一种不同的语法,而不需要学习它背后的整个逻辑。
没有在代码上写注释
尽管这听起来很明显,而且是一个无关紧要的任务,但它代表了一种很好的策略,可以跟踪每一行或每一块代码执行的操作,以便返回到暂停的项目。在最初的代码编写过程中,程序员对项目的目的和目标有了清晰而清晰的认识;他们知道自己想要编写的程序背后的逻辑步骤和追求的结果。然而,由于多种原因(经济约束、信息缺失、优先级的改变),所有的项目都很容易暂停,这将迫使程序员切换到不同的任务,而让先前的任务保持不变。一个中断的项目需要的时间越长,就越不容易记住它的位置和缺失的点。这里是注释发挥作用的地方。试着在你认为有必要的地方使用它们;记住要足够清晰,并记住它们应该允许代码程序员和执行者理解代码背后的逻辑步骤。
在代码编写过程中不要求反馈
在你的经理要求你做什么,他/她希望你做什么,客户要求什么,和你实际做什么之间总是有很大的差距。当你在开发一个程序或新代码时,试着把它分成几个阶段,并在进入下一个阶段之前征求反馈。在每个阶段结束后得到反馈,这将让你知道你是否正确,或者是否需要根据客户的要求进行更改。这并不意味着你无法理解其他人的要求,而是将其视为利益相关者之间的想法和期望的统一。如果在偏离正轨的情况下,你收到反馈的频率越高,你需要进行的修改就越少。请记住,持续的沟通对于每一个项目的成功实施都是至关重要的。
没有测试你当前的知识
你可能已经看了很多逐步编程教程。你可能也读过许多数据科学书籍和编程书。你可能已经完成了许多编程训练营的练习。下一步是什么?测试你目前的知识。这种训练营和课程的真正价值不在于证书本身,而在于你学到的知识,并能成功地应用于解决某个问题。老实说,每个人都可以通过参加在线课程来获得证书,只要跳过大部分的课程就可以了;公司和组织都非常清楚这一点。尝试把自己推向新的极限,在网上寻找编程挑战,尝试头脑风暴,在没有太多帮助资源的情况下编写代码。这并不意味着你在实际工作中不会用到它们,但它会让你感觉更舒服,更安全,更少依赖它们。
没有充分利用优缺点
在某种程度上,你可能会觉得使用一种特定的编程语言和软件是很舒服的,而你可能会发现学习一种新的语言和软件是没有用的。我曾多次听到数据分析师争论哪种编程语言在能力、可用库和包、在线资源和流行程度方面是最好的。但是,你必须足够谦虚,认识到总有从另一种语言、库、包或软件中学习新东西的空间。每种编程语言和软件都有其优点和缺点,但是我们的目标是充分利用它们,并具有足够的灵活性,以确定最适合用于特定任务以解决特定问题的语言和软件。
假设你什么都知道 相信我,没有人什么都知道。数据科学领域非常广泛,每天都要学习新东西。库、包、函数、方法和算法的总数非常多。永远保持好奇,保持谦虚,如果你认为你知道的很多,你实际知道的就很少。
原文链接: https://blog.csdn.net/fendouaini/article/details/103252444
01. 前言
关于程序员接私活,社会各界说法不一。按照作者的观点来说如果你确实急用钱,价格又合适,那就去做。如果不怎么缺钱,那就接私活之前要好好考虑。私活的钱不好挣是一个方面,更重要的是如果你把做私活的时间花在提升自己上,产生的价值就要大得多。等你提升了自己,提升了固定薪水,远比拿的这点私活的钱划算。千万不要“捡了芝麻丢了西瓜”。
如果你主业上遇到了瓶颈,平时的时间比较充分,想有一些额外的收入,同时为了保持技术的熟练度,这种情况下,是可以考虑接一些私活的。对于那种投入时间巨大,回报很可怜的项目,千万不要接。
下面介绍一些常用的平台可以接私活。
02. 程序员客栈
程序员客栈中国非常领先的自由工作平台,为中高端程序员、产品经理和设计师等等互联网相关人员提供稳定的线上工作机会,包括自由工作、远程工作和兼职工作,还支持按需雇佣,工作模式非常多,感兴趣的推荐大家尝试一下。虽然名称叫程序员客栈,但是除了程序员,像产品经理,设计师等等互联网相关人员,都能在上面找到适合自己的项目。感兴趣的可以体验一下。
程序员客栈官网:https://www.proginn.com/
03. 码市
码市是 Coding 推出的互联网软件外包服务平台,意在连接需求方与广大开发者。让项目的需求方快速的找到合适的开发者,完成项目开发工作。
码市官方网站:https://codemart.com/
04. 猪八戒网
猪八戒网创建于2006年,是服务中小微企业的人才共享平台。开创式地为人才与雇主搭建起双边市场,通过线上线下资源整合与大数据服务,实现人才与雇主精准无缝对接。找兼职的地方,主要是入门级项目,不适合专业程序员,只适合新手。
猪八戒官网:https://luoyang.zbj.com/
05. 开源众包
开源众包–专业的软件众包平台,350万+ 优质开发者为您提供网站、APP、微信/小程序、企业应用等软件开发服务,有效降低企业 IT 软件开发成本、解决技术资源不足等问题。
开源中国的众包平台,主要是以众包为主。
开源众包官网:https://zb.oschina.net/
06. 智城外包网
智城外包网,聚合全国软件团队资源,官方认证,1小时响应,零交易佣金,托管安全保障。十年口碑运营,万家靠谱团队。免费比价,免费一站式外包项目管理工具。平台汇集软件咨询专家,软件技术专家,软件开发专家,软件开发公司,软件外包公司,软件外派公司。在线竞标模式,让IT外包项目和短期IT招聘、人力派遣需求可以获得高性价比的候选。海量资源池包括:网站设计、网站开发、手机应用开发、移动应用开发、安卓应用开发、苹果应用开发、微信应用开发、Java技术、C#技术、Web前端开发、IT人力外包、IT人力外派、IT人力短期招聘、技术合伙人、通用软件开发,SaaS软件实施,软件运维等服务门类。
网官方网站:http://www.taskcity.com/
07. 实现网
北京实现与爱科技有限公司是一个互联网工程师兼职平台。解决创业公司招人难、成本高的问题。
创业公司通过实现网可以快速预约知名互联网企业的工程师、设计师到自己的团队工作。上午预约工程师,最快晚上即可到班兼职。
互联网工程师可以在实现网注册成为技术顾问,利用业余时间助力创业公司,并且获得以时薪为单位的报酬。
目前已有9000+工程师或设计师可在线预约和支付,支付后工程师会到团队里坐班沟通,快速推进创业者的产品开发进度。
实现网为企业提供BAT等名企背景的、靠谱的开发设计兼职人才和自由职业者,满足企业项目外包、驻场开发、远程兼职、技术咨询等短期人力需求。已服务2000多家企业,包括好未来、方正、人人贷、秒拍等知名企业。
官方网站:https://shixian.com/
08. 猿急送
猿急送,一个高级技术共享平台,这里汇聚知名互联网公司的技术、设计、产品大牛,通过实际坐班、远程等方式,一对一为创业公司解决问题,提高创业效率。
猿急送为您提供兼职程序员,兼职工程师信息,猿急送是一个高级技术共享平台,是优质的程序员兼职网站,这里汇聚BAT等知名互联网公司的技术开发、产品、设计大牛,通过实际坐班等方式,一对一为创业公司解决程序员、工程师等开发、产品设计人力问题。
官方网站:https://www.yuanjisong.com/
09. 人人开发
人人开发基于可视化快速开发平台 - 捷得(Joget)/捷得云(Joget Cloud)(PaaS),集众多开发者资源,为企业提供企业管理软件服务。应用市场提供应用产品、插件的在线试用和销售,服务市场以威客众包模式提供管理软件定制开发服务,各类企业级应用开发服务,例如:协同OA产品,ERP,CRM,人事管理,项目管理,资产管理,设备管理等。
官方网站:http://rrkf.com/
10. 开发邦
公司位于北京中关村科技园区核心区海淀园,成立于2010年,专注于为客户提供互联网软件技术开发与咨询服务,致力于利用互联网软件技术为客户提高效率、降低成本、提升效能、优化管理。
团队核心成员均具有十年以上软件互联网技术开发经验,毕业于工科名校。至今,已成功执行近百个项目,涵盖管理软件、互联网系统、移动APP、前端互动开发等。
先后为华为公司、商汤科技、工信部中国软件评测中心、神州数码、深鉴科技、中软集团、中国万网、中石油吐哈气举中心、华北电力大学、中科院科技政策与管理研究所、浪潮集团、ADI、世界五百强伊顿中国、北京外国语大学、51talk、勤邦生物、安龙基因等知名企业及机构提供过互联网软件技术开发与技术咨询服务。
开发邦致力于成为企业业务互联网软件服务与咨询的定制方案提供商。
官方网站:https://www.kaifabang.com/
11. 电鸭社区
电鸭社区旨在帮助更多人走上「只工作,不上班」的自由工作之路,我们是一个「分布式组织」,通过分享及行动带来积极的影响,相信点滴的力量能改变潮水的方向。
官方网站:https://eleduck.com/
12. 快码
深圳快码科技成立于2014年11月,是一家创新型的互联网公司,致力于通过创新的开发方式,为软件技术开发行业带来改变,提供更快速、更高性价比的软件定制服务。
“快码”的意思是“快速编写代码”。公司采用“专属项目经理 + 自有开发团队 + 平台程序员”的创新开发方式,严格按照互联网公司的标准来管理开发团队,确保每个项目都有充足的人员投入,确保项目的进度和开发质量。2015年,我们和全球最大的手游、APP云测试平台Testin达成战略合作协议,并获得Testin数百万的战略投资。
目前平台已注册的开发者达到3万多人,涵盖各种开发语言与类型,可以提供开发的项目有iOS APP、安卓APP、微信公众号、PC网站、手机网站、微信小程序、桌面软件、智能硬件APP等。上线以来,我们已经完成了数千项目&任务的开发。
创业灵感来自于快码团队的从业经验。在近十年的互联网技术经历中,对由于创业公司、外包公司人员不稳定,招聘困难、人手有限等问题而导致现有团队开发任务过重,开发进度缓慢等问题有着切身之痛,将在P2P旅游行业2年多的共享经济经验,和自身最熟悉的“软件开发”结合,创立了“快码”。
快码将立足于代码开发,深耕行业,面向未来,通过持续的产品创新,为广大项目方、开发者提供专业的服务,为软件技术开发行业带来改变。
快码是一个创新的软件开发平台。项目方可以更省钱、高效地完成项目的开发;开发者可以充分利用闲置时间,实现更高的商业价值!
官方网站:https://www.kuai.ma/
13. 英选
英选,可信赖的软件外包服务。用优秀的人,做漂亮的产品,写干净的代码。平台以定制开发外包服务为主,也是外包项目平台。
官方网站:https://www.yingxuan.io/
14. Upwork
Upwork 是全球最大的、最优秀的、最规范的综合类人力外包服务平台,由著名的 Elance 和 oDesk 合并。这里聚集 900 万来自全球各地的自由工作者,你肯定可以在找到适合你的职位。
官方网站:https://www.upwork.com/
15. Freelancer
Freelancer 的工作类型覆盖了很多不同的领域,由程序开发到市场营销、广告、会计、法务等一系列的可以远程的工作。
官方网站:https://www.freelance.com/
16. Dribbble
Dribbble 不只是全球最受欢迎的设计师社区,同样是设计师寻找远程工作的好出处。自从被 Tiny 收购后,Dribbble 的招聘属性正在慢慢增强,试着持续 PO 出自己的好作品,等待你的伯乐,同样你可以关注 Jobs 页面,给心仪的 Team 提交简历。
官方网站:https://dribbble.com/jobs
17. Remoteok
Remoteok 不仅提供最初的兼职类远程工作,还有全职类,签署合同类和实习类的工作。网站创始人 Pieter Levels 本身就是一名数字游民,他同样是 Nomadlist 的创始人。
官方网站:https://remoteok.io/
18. Toptal
Toptal 是一个高端一些的自由职业者平台,适合比较有经验和工作尽力的远程工作者。它将企业与全球的软件工程师,设计师和业务顾问联系起来。
官方网站:https://www.toptal.com/
19. AngelList
AngelList 主要是服务于初创公司和天使投资人的平台,这里还有初创公司提供的远程工作的机会,如果对远程加入初创公司感兴趣的,可以尝试一下。
官方网站:https://angel.co/remote
20. Topcoder
Topcoder 通过算法比赛吸引世界顶级的程序员,他会将一下大型项目分割成很多小模块,通过竞赛的模式交给用户来做,优胜者可以拿到制定模块的奖金。
官方网站:https://www.topcoder.com/
大家好,我是 Rocky0429,一个最近正在学习 SQL 的蒟蒻…
在看完了某 《xxx 必知必会》以后,我觉得我膨胀了,立马某度 “xxx SQL 面试 100 题”、”SQL 岗位 xxx 个面试题“,准备大展手脚…
几天过去了,我有点懵逼,我发现身为蒟蒻的我不知道我写的 SQL 是不是丝滑,这种练习让我产生了一种没有丝毫提升的迷茫,问题是还得自己建个表,想想数据集,真的是太麻烦了。我,有点郁闷…
如果,有能在线练习的地方就好了… 如果,题目有层次就更好了…
苦心人天不负,卧薪尝胆,三千越甲可吞吴,经过我不懈的努力和本着不要脸的搜索精神,终于找到了…古人诚不我欺!
那一刻我泪流满面,经过激烈的脑腔共鸣,我决定把这些丝滑的网站公之于众,来吧,继续赞美我吧!
0x00 SQLZOO
网址:https://sqlzoo.net/
是SQLZOO包括了 SQL 学习的教程和参考资料,支持 SQL Sever、Oracle、MySQL、DB2、PostgreSQL等多个 SQL 搜索引擎,现在支持英德日中四国语言,不过中文是繁体的,看起来别别扭扭,建议还是用英文来看,就算英语不好,翻译大法好…
遇到不会的题,祭出 Google 大法,看看别人的一些高质量的回答,然后赶紧自己再试一下,就算自己做对了,也看看和别人的想法差距在哪,这样自己的提升才快。
0x01 SQLBolt
网址:https://sqlbolt.com/
SQLBolt 是一个适合小白学习 SQL 的网站,这里由浅及深的介绍了 SQL 的知识,每一个章节是一组相关的 SQL 知识点,且配备着相应的练习。
这个特别适合学完某个知识点之后,立马想检验自己学习情况的同学。对一些同学来说,唯一难受的可能网站是英文的,这个不慌,接着往下看,有惊喜。
0x02 XUESQL
网址:http://xuesql.cn/
在 0x01 中因为英文而感觉不适的可以看这个,相当于 SQLBolt 的中文版,包含手册、在线练习,还有一点好的是,XUESQL 还有自己讲解 SQL 的视频可以食用。
0x03 SQL Fiddle
网址:http://sqlfiddle.com/
SQL Fiddle 支持 MySQL、SQL Server、SQLite 等主流的 SQL 引擎,在这里可以选择练习的数据库以及版本号。
0x04 leetcode
英文网址:https://leetcode.com/
中文网址:https://leetcode-cn.com/
一般大家看到 leetcode 第一印象是刷算法题的,其实 leetcode 上也有关于 SQL 的题目,而且可以在线测评,和评测算法题一样,也会让你很直观的看到自己所写的 SQL 的运行速度等,同样可以去借鉴别人优化好的 SQL,与自己的对比,将好的用法学会,这样的提升是很快的。
0x05 写在之后
当然类似的这种 SQL 在线练习的网站有很多,我只介绍了其中的一些有代表性的,关于更多类似的网站,欢迎大家和我交流。
作者Info:
【作者】:Rocky0429
【原创公众号】:Python空间。
【简介】:CSDN 博客专家, 985 计算机在读研究生,ACM 退役狗 & 亚洲区域赛银奖划水选手。这是一个坚持原创的技术公众号,专注Python 编程,每天坚持推送各种 Python 基础/进阶文章,数据分析,爬虫实战,数据结构与算法,不定期分享各类资源。
转自DT财经 ,作者陷入焦虑的DT君
前两周播出的《明星大侦探—MGQ时尚风云》中,节目组设计了一个与中年危机相关的案件:
撒贝宁在这期节目中饰演一位40岁“上有老,下有狗”的杂志社时装编辑,虽然月入12w,但是每个月刨去房贷、车贷、儿子补习班费用、父母医疗保健费用以及各种家庭开支后,只能剩下1块钱。被主编无理辞退后,他由于年龄过大而迟迟找不到新工作,在家庭经济重担的支配下,对老板心生杀机……
节目中的设定确实有些夸张,但剧本却是来源于真实生活的情绪。2019年“太南了”的一系列感叹中,充满了中年职场人的辛酸与苦涩。
微博上#35岁以上职场人去了哪儿#、#35岁定律#等话题屡屡冲上热搜,随便就能收获近亿的阅读量,知乎、朋友圈、头条……到处充斥着怎么应对35岁危机的回答和文章,“35岁危机”俨然已经成为一个大家公认的专有名词。
(图片说明:百度百科收录的“35岁危机”词条)
这让还未到35岁的小编,内心多少有些焦虑:35岁的职场人就业真有这么难吗?他们都面临着怎样的中年危机?难道除了卖保险,真的就无处可去了吗?
DT财经联合智联招聘共同做了个小研究,通过招聘方与应聘者的数据,来看看35岁职场人真实的生存情况。
35岁真的很难吗?
首先,我们想知道,35岁真的面临一个更残酷的世界吗?
从人口结构的大盘面粗略判断,当代中年所面临的竞争,其实并不如他们的前辈或后辈那样激烈。
根据2010年国家第六次人口普查数据,到2019年我国34-43岁的中年人口数量为1.98亿,而44-53岁人口数量为2.42亿,24-33岁人口数量为2.27亿。
也就是说,不管是曾经的,还是未来的中年人,都比现在叫嚷着中年危机的劳动力数量旺盛不少,而他们所面临的,可并不是高速发展的黄金十年。
既然如此,为什么35岁危机的话题一直不绝于耳呢?
招聘方对岗位需求的描述,一定程度上能反映出职场的变幻风向。
从招聘需求来看,2019年第三季度所有的招聘岗位中,只有5%明确要求求职者的年龄低于35岁。
这个看起来并不高的比例,可能并不能完全说明市场的真实需求情况,毕竟从大家的日常吐槽和我们的小范围调研来看,许多公司对年龄的苛刻要求很多时候秉持着只干不说的原则,并不会明晃晃地写在岗位简介里。
但我们注意到一个变化趋势,明确要求求职者35岁以下的岗位比例,与两年前相比上涨了2个百分点,涨幅其实挺大的(60%)。这事实上表明,35岁职场人面临的境遇,确实是比两年前更难了些。
以这5%要求35岁以下的招聘职位作为研究样本,我们大致研究了下不同地方对中年职场人的友好度。
在各类企业中,要求35岁以下的职位比例最高的是国企,而民营公司在岗位要求中对35岁的求职者其实相对温和。
而从公司规模来看,规模越大的公司,要求35岁以下的职位比例就越高。10000人以上的超大公司在2017年就已超过6%;500-999人肩部大公司在两年前的比例还仅有2.1%,但两年间提高了近4个百分点至6%。
哪些行业更看重35岁的年龄线?
要求35岁以下的职位比例越来越高,其实与就业大环境和新兴行业的发展有关。
Linda在一家大数据创业公司做HR,她告诉小编:“这两年在招人的时候确实会优先选择35岁以下,甚至是30岁以下的年轻人,尤其是18年的裁员潮过后,大家其实都不好过。一般达到35岁的职场人在公司都担任比较重要的职务,而我们大数据属于新兴行业,公司的业务变化很快,在探索阶段并不需要那么多指挥官,我们更需要富有创造力、行动力强听指挥的战士。”
Linda的话意味着,是否能在35岁前找到一家托付终身的企业至关重要。
可是,在35岁就找到能够让自己奋斗一生的公司,就和20岁就找到长相厮守的老伴一样不靠谱。这时候,选对行业很关键。
小编整理了2019年招聘需求中要求35岁以下职位比例最高和最低的行业。
我们发现,在众多行业中,银行、外包服务、中介服务、保险和通信行业对年龄的限制最大。也就是说,这些行业中有相对更多的岗位只需要年轻力壮的劳动力。
但也有计算机行业、检验检测、IT服务、学术科研和教育培训等行业,在招聘需求中对35岁以上的职场人相对比较宽容。
如果看一下这些行业给大家的固有印象,我们可以认为,在招聘中更看重35岁年龄线的行业,多属于大家印象中的传统行业;而在要求35岁以下职位比例最低的队伍中,新兴行业的比例会更高些。
从岗位来看,商超、保健、银行、房地产、社区等技术门槛较低的基础类岗位,在招聘中要求35岁以下的职位比例最高。
总结下来就是,工作内容更简单或对知识性技能要求更低的岗位,会更偏爱年轻人,毕竟人到中年,精力和体力都无法和年轻人相比。对于用人单位来说,与其去招一个成本更高的中年员工,不如多招几个应届大学生,年轻力壮,生产力更强。
而要求35岁以下职位比例最低的10个岗位中,IT、软件、硬件开发、IT管理、互联网产品、IT运维等互联网/技术岗位占去了大半,这都是对专业技术、创新性要求更高的行业岗位。至少从招聘需求来看,他们并不会太多强调年龄。
显然,这与我们的日常感受并不一致。
35岁话题中最常被吐槽的,恰好是这些不那么强调年龄的岗位从业者,而那些在招聘需求中有更多年龄限制的岗位,存在感并不太强。
这到底是为啥?
为什么35岁的你觉得“太南了”
我们需要明确的是,一个行业或岗位对年龄的限制少,并不意味着中年的求职者就会获得青睐。
35岁求职者的优势在于有更多经验与技能积累,而劣势在于体力和精力不够充沛,只有在更需要经验而非体力的岗位,中年职场人才会有更强的竞争力。
即使我们默认那些并没有对35岁划线的岗位都不需要干体力活,也并不意味着在这些岗位上,经验、积累和阅历就会有比较大的增值效果——如果年龄buff不能增效,用人单位自然会聘请更年轻、薪资要求更低的求职者。
所以,关键点在于,中年职场人是否进入了自己具备优势的行业。
根据智联招聘2019年第三季度的数据,互联网、房地产、教育培训、专业服务、计算机软件等行业,挤下了最多的35岁及以上求职者,而行政/后勤、销售业务、财务/审计/税务、人力资源和软件/互联网开发/系统集成等则是他们投递最多的职位。
我们计算了各个行业和岗位中简历投递数量与最终录用数量的比值,命名为竞争指数,以此来衡量应聘竞争的激烈程度。
我们发现,互联网、房地产、计算机软件、IT服务的行业竞争都格外激烈,而从职位来看,软件/互联网开发/系统集成的竞争指数更是一骑绝尘,97个人参与摸奖,只有1个人能获得offer,另外财务/审计/税务、人力资源、行政等职位的竞争指数也挺高。
这就意味着,上述岗位中,中年职场人不仅要和同龄人竞争,还要和年轻人竞争。
那么,在这些岗位中,中年职场人能获得年龄加成从而脱颖而出吗?
我们统计了各个职位要求工作经验10年以上的岗位数量,数量排名越靠前,则意味着这个职位对经验和阅历的需求度就更高,中年职场人也就相对更有优势。
从结果来看,要求工作经验10年以上岗位数量最多的职位主要可以分为两类,一类是管理向的,包括占比最多的高级管理,以及销售管理、生产管理、项目管理、质量管理等各类管理岗;一类是专业向的,包括土木建筑、财务审计、医院医疗、机械设计等等。
前面提到的竞争激烈的财务/审计/税务、人力资源和房地产相关职位,对工作经验的需求度都排在前列,中年职场人在这些赛道上握着更多本钱。
但相信你也注意到了,挤满了35岁以上求职者的互联网/IT/技术相关职位中,对于工作经验的需求度并没有那么高。虽然这是个典型的高薪赛道,但年龄并不会给中年求职者带来加成效果,如果薪资要求还比较高,实在很难竞争得过除了经验、什么都有的年轻人。
我们总结上述分析,被35岁危机困扰的核心问题可能在于,“理想”的薪资和岗位期望与“现实”竞争力存在分歧,而互联网、IT服务、计算机软件等行业的分歧更为严重。
38岁的陈斌曾经在一家互联网公司做新媒体运营,现在和朋友经营一家外贸公司,他也认同有些中年求职者理想与现实不匹配的问题:“见识过各行各业形形色色的中年求职者,他们对薪资的要求都很高,但被问到有什么核心竞争力的时候,又讲不出个所以然,对自己的未来也没有什么明确的规划。”
同样经历过35岁的他,认为并不存在什么35岁危机,没有能力每个年龄段其实都有危机:“足球运动员到了30岁一般都要大幅降薪,我觉得35岁的普通职场人应该要接受这个事实。你不接受,社会的毒打会让你清醒的。勿骄勿躁,努力提升自己,同时做好期望管理,少听媒体瞎bb。”
这话说得挺残酷,但从“35岁危机”中脱困的最好办法,确实是找到问题根本那个“理想”与“现实”的差距,实实在在地缩短它。
当然,对于已经有了家庭羁绊、要努力维持生活品质的中年人,这又是另外一个要不断感叹“太南了”的过程。
写到这里,我们突然想用《黄金时代》里的一段话作为结尾:
“那一天我二十一岁,在我一生的黄金时代。我有好多奢望。我想爱,想吃,还想在一瞬间变成天上半明半暗的云。后来我才知道,生活就是个缓慢受锤的过程,人一天天老下去,奢望也一天天消失,最后变得像挨了锤的牛一样。可是我过二十一岁生日时没有预见到这一点。我觉得自己会永远生猛下去,什么也锤不了我。”
(应受访者要求,Linda、陈斌为化名)