注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

大专前端,三轮面试,终与阿里无缘

web
因为一些缘故,最近一直在找工作,再加上这个互联网寒冬的大环境,从三月找到六月了,一直没有合适的机会 先说一下背景,目前三年半年经验,base 杭州,大专学历+自考本科 就在前几天,Boss 上收到了阿里某个团队的投递邀请(具体部门就不透露了),因为学历问题...
继续阅读 »

因为一些缘故,最近一直在找工作,再加上这个互联网寒冬的大环境,从三月找到六月了,一直没有合适的机会



先说一下背景,目前三年半年经验,base 杭州,大专学历+自考本科



就在前几天,Boss 上收到了阿里某个团队的投递邀请(具体部门就不透露了),因为学历问题,基本上大厂简历都不会通过初筛,但还是抱着破罐子破摔的心态投递给了对方,出乎意料的是简历评估通过了,可能是因为有两个开源项目和一个协同文档加分吧。


进入到面试环节,首先是两道笔试题,算是前置面试:


第一道题目是算法题:


提供了一个数组结构的 data,要求实现一个 query 方法,返回一个新的数组,query 方法内部有 过滤排序分组 等操作,并且支持链式调用,调用最终的 execute 方法返回结果:


const result = query(list)
.where(item => item.age > 18)
.sortBy('id')
.groupBy('name')
.execute();

console.log(result);

具体实现这里就不贴了,过滤用原生的数组 filter 方法,排序用原生的数组 sort 方法,分组需要手写一下,类似 lodash/groupBy 方法。


过滤和排序实现都比较顺利,在实现分组方法的时候不是很顺利,有点忘记思路了,不过最后还是写出来了,关于链式调用,核心是只需要在每一步的操作最后返回 this 即可。


第二道题目是场景题:


要求用 vue 或者 react 实现一个倒计时抢券组件,页面加载时从 10s 开始倒计时,倒计时结束之后点击按钮请求接口进行抢券,同时更新文案等等功能。因为我对 react 比较熟悉一点,所以这里就选择了 react。


涉及到的知识点有 hook 中对 setTimeout 的封装、异步请求处理、状态更新CSS基本功 的考察等等……


具体实现这里也不贴了,写了一堆自定义 hook,因为平时也在参与 ahooks 的维护工作,ahooks 源码背的滚瓜烂熟,所以直接搬过来了,这道题整体感觉没啥难度,算是比较顺利的。


笔试题整个过程中唯一不顺利的是在线编辑器没有类似 vscode 这样的 自动补全 功能,不管是变量还是保留字,很多单词想不起来怎么拼写,就很尴尬,英文太差是硬伤 :(


笔试过程最后中出现了一点小插曲,因为笔试有时间限制,需要在规定的时间内完成,但是倒计时还没结束,不知道为什么就自动交卷了,不过那个时候已经写的差不多了,功能全部实现了,还剩下卡片的样式没完成,css 还需要完善一下,于是就在 Boss 上跟对方解释了一下,说明了情况。


过了几分钟,对面直接回复笔试过了,然后约了面试。


一面:



  • 自我介绍


    这里大概说了两分钟,介绍了过往工作经历,做过的业务以及技术栈。


  • 七层网络模型、和 DNS 啥的


    计网这方面属于知识盲区了,听到这个问题两眼一黑,思索了一会儿,直接说回答不上来。


  • 然后问了一些 host 相关的东西



    • 很遗憾也没回答上来,尴尬。对方问我是不是计算机专业的,我坦诚的告诉对方是建筑工程。



  • React 代码层的优化可以说一下么?



    • 大概说了 class 组件和 function 组件两种情况,核心是通过减少渲染次数达到优化目的,具体的优化手段有 PureComponentshouldComponentUpdateReact.memoReact.useMemoReact.useCallbackReact.useRef 等等。



  • 说一下 useMemouseCallback 有什么区别



    • 很基础的问题,这里就不展开说了。



  • 说一下 useEffectuseLayoutEffect 有什么区别



    • 很基础的问题,这里就不展开说了。



  • 问了一下 useEffect 对应在 class 中都生命周期怎么写?



    • 很基础的问题,这里就不展开说了。



  • 如果在 if 里面写 useEffect 会有什么表现?



    • 开始没听清楚,误解对方的意思了,以为他说的是在 useEffect 里面写 if 语句,所以胡扯了一堆,后面对方纠正了一下,我才意识到对方在问什么,然后回答了在条件语句里面写 useEffect 控制台会出现报错,因为 hook 的规则就是不能在条件语句或者循环语句里面写,这点在 react 官方文档里面也有提到。



  • 说一下 React 的 Fiber 架构是什么



    • 这里说了一下 Fiber 本质上就是一个对象,是 React 16.8 出现的东西,主要有三层含义:



      1. 作为架构来说,在旧的架构中,Reconciler(协调器)采用递归的方式执行,无法中断,节点数据保存在递归的调用栈中,被称为 Stack Reconciler,stack 就是调用栈;在新的架构中,Reconciler(协调器)是基于 fiber 实现的,节点数据保存在 fiber 中,所以被称为 fiber Reconciler。

      2. 作为静态数据结构来说,每个 fiber 对应一个组件,保存了这个组件的类型对应的 dom 节点信息,这个时候,fiber 节点就是我们所说的虚拟 DOM。

      3. 作为动态工作单元来说,fiber 节点保存了该节点需要更新的状态,以及需要执行的副作用。


      (这里可以参考卡颂老师的《自顶向下学 React 源码》课程)




  • 前面提到,在 if 语句里面写 hook 会报错,你可以用 fiber 架构来解释一下吗?



    • 这里说了一下,因为 fiber 是一个对象,多个 fiber 之间是用链表连接起来的,有一个固定的顺序…… 其实后面还有一些没说完,然后对方听到这里直接打断了,告诉我 OK,这个问题直接过了。



  • 个人方面有什么规划吗?



    • 主要有两个方面,一个是计算机基础需要补补,前面也提到,我不是科班毕业的,计算机底层这方面比起其他人还是比较欠缺的,尤其是计网,另一方面就是英文水平有待提高,也会在将来持续学习。



  • 对未来的技术上有什么规划呢?



    • 主要从业务转型工程化,比如做一些工具链什么的,构建、打包、部署、监控几个大的方向,node 相关的,这些都是我感兴趣的方向,未来都可以去探索,当然了现在也慢慢的在做这些事情,这里顺便提了一嘴,antd 的 script 文件夹里面的文件是我迁移到 esm + ts 的,其中一些逻辑也有重构过,比如收集 css token、生成 contributors 列表、预发布前的一些检查等等…… 所以对 node 这块也有一些了解。



  • 能不能从技术的角度讲一下你工作中负责业务的复杂度?



    • 因为前两份工作中做的是传统的 B 端项目和 C 端项目,并没有什么可以深挖的技术难点,所以这里只说了第三份工作负责的项目,这是一个协同文档,既不算 B 端,也不算 C 端,这是一款企业级的多人协作数据平台,竞品有腾讯文档、飞书文档、语雀、WPS、维卡表格等等。


      协同文档在前端的难点主要有两个方面:



      1. 实时协同编辑的处理:当两个人同时进入一个单元格编辑内容,如果保证两个人看到的视图是同步的?那么这个时候就要提到冲突处理了,冲突处理的解决方案其实已经相对成熟,包括:



        • 编辑锁:当有人在编辑某个文档时,系统会将这个单元格锁定,避免其他人同时编辑,这种方法实现方式最简单,但也会直接影响用户体验。

        • diff-patch:基于 Git 等版本管理类似的思想,对内容进行差异对比、合并等操作,也可以像 Git 那样,在冲突出现时交给用户处理。

        • 最终一致性实现:包括 Operational Transformation(OT)、 Conflict-free replicated data type(CRDT,称为无冲突可复制数据类型)。



      2. 性能问题



        • 众所周知,互联网一线大厂的协同文档工具都是基于 canvas 实现,并且有一套自己的 canvas 渲染引擎,但是我们没有,毕竟团队规模没法跟大厂比,这个项目前端就 2 个人,所以只能用 dom 堆起来(另一个同事已经跑路,现在就剩下我一个人了)。这导致页面卡顿问题非常严重,即使做了虚拟滚动,但是也没有达到很好的优化效果。老板的要求是做到十万量级的数据,但是实际上几千行就非常卡了,根本原因是数据量太大(相当于一张很大的 Excel 表格,里面的每一个单元格都是一个富文本编辑器),渲染任务多,导致内存开销太大。目前没有很好的解决方案,如果需要彻底解决性能问题,那么就需要考虑用 canvas 重写,但是这个基本上不太现实。

        • 因为卡顿的问题,暴露出来另一个问题,状态更新时,视图同步缓慢,所以这时候不得不提到另一个优化策略:乐观更新。乐观更新的思想是,当用户进行交互的时候,先更新视图,然后再向服务端发送请求,如果请求成功,那么什么都不用管,如果请求失败,那么就回滚视图。这样做的好处是,用户体验会好很多,在一些强交互的场景,不会阻塞用户操作,比如抖音的点赞就是这样做的。但是也会带来一些问题,比如:如果用户在编辑某个单元格时,另一个用户也在编辑这个单元格,那么就会出现冲突,这个时候就需要用到前面提到的冲突处理方案了。







  • 可以讲一下你在工作中技术上的建设吗?



    • 这里讲了一下对 hooks 仓库的建设,封装了 100 多个功能 hook业务 hook,把不变的部分隐藏起来,把变化的部分暴露出去,在业务中无脑传参即可,让业务开发更加简单,同时也提高了代码的复用性。然后讲了一下数据流重构之类的 balabala……



  • 你有什么想问我的吗?



    • 问了一下面试结果大概多久能反馈给我,对方说两三天左右,然后就结束了。





结束之后不到 20 分钟,对方就在 Boss 上回复我说面试过了,然后约了二面。



二面:



  • 自我介绍



    • 跟上一轮一样,大概说了两分钟,介绍了过往工作经历,做过的业务以及技术栈。



  • 在 js 中原型链是一个很重要的概念,你能介绍一下它吗?



    • 要介绍原型链,首先要介绍一下原型,原型是什么…… 这块是纯八股,懒得打字了,直接省略吧。



  • object 的原型指向谁?



    • 回答了 null。(我也不知道对不对,瞎说的)



  • 能说一下原型链的查找过程吗?



    • 磕磕绊绊背了一段八股文,这里省略吧。



  • node 的内存管理跟垃圾回收机制有了解过吗?



    • 暗暗窃喜,这个问题问到点子上了,因为两年前被问到过,所以当时专门写了一篇文章,虽然已经过去两年了,但还是背的滚瓜烂熟:

    • 首先分两种情况:V8 将内存分成 新生代空间老生代空间



      • 新生代空间: 用于存活较短的对象



        • 又分成两个空间: from 空间 与 to 空间

        • Scavenge GC 算法: 当 from 空间被占满时,启动 GC 算法



          • 存活的对象从 from space 转移到 to space

          • 清空 from space

          • from space 与 to space 互换

          • 完成一次新生代 GC





      • 老生代空间: 用于存活时间较长的对象



        • 新生代空间 转移到 老生代空间 的条件(这个过程称为对象晋升



          • 经历过一次以上 Scavenge GC 的对象

          • 当 to space 体积超过 25%



        • 标记清除算法:标记存活的对象,未被标记的则被释放



          • 增量标记:小模块标记,在代码执行间隙执,GC 会影响性能

          • 并发标记:不阻塞 js 执行









  • js 中的基础类型和对象类型有什么不一样?



    • 基础类型存储在栈中,对象类型存储在堆中。



  • 看你简历上是用 React,你能简单的介绍一下 hooks 吗?



    • 本质上就是一个纯函数,大概介绍了一下 hooks 的优点,以及 hooks 的使用规则等等。



  • 简单说一下 useEffect 的用法:



    • useEffect 可以代替 class 中的一些生命周期,讲了一下大概用法,然后讲了一下 useEffect 的执行时机,以及 deps 的作用。



  • 说一下 useEffect 的返回值用来做什么?



    • 返回一个函数,用来做清除副作用的工作,比如:清除定时器清除事件监听等等。



  • 你知道 useEffect 第二个参数内部是怎么比较的吗?



    • 说了一下内部是浅比较,源码中用 for 循环配合 Object.is 实现。(感觉这个问题就是在考察有没有读过 React 源码)



  • 前端的话可能跟网络打交道比较多,网络你了解多少呢?



    • 这里直接坦诚的说了一下,网络是我的弱项,前面一面也问到了网络七层模型,没回答出来。



  • 那你回去了解过七层模型吗?我现在再问你一遍,你能回答出来吗?



    • 磕磕绊绊回答出来了。



  • 追问:http 是在哪一层实现的?



    • 应用层。



  • 说一下 getpost 有什么区别?



    • 两眼一黑,脑子一片空白,突然不知道说什么了,挤了半天挤出来一句:get 大多数情况下用来查询,post 大多数情况下用来提交数据。get 的入参拼在 url 上,post 请求的入参在 body 里面。面试官问我还有其它吗?我说想不起来了……



  • 说一下浏览器输入 url 到页面加载的过程:



    • 输入网址发生以下步骤:



      1. 通过 DNS 解析域名的实际 IP 地址

      2. 检查浏览器是否有缓存,命中则直接取本地磁盘的 html,如果没有命中强缓存,则会向服务器发起请求(先进行下一步的 TCP 连接)

      3. 强缓存协商缓存都没有命中,则返回请求结果

      4. 然后与 WEB 服务器通过三次握手建立 TCP 连接。期间会判断一下,若协议是 https 则会做加密,如果不是,则会跳过这一步

      5. 加密完成之后,浏览器发送请求获取页面 html,服务器响应 html,这里的服务器可能是 server、也可能是 cdn

      6. 接下来是浏览器解析 HTML,开始渲染页面



    • 顺便说了渲染页面的过程:



      1. 浏览器会将 HTML 解析成一个 DOM 树,DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。

      2. 将 CSS 解析成 CSS Rule Tree(css 规则树)。

      3. 解析完成后,浏览器引擎会根据 DOM 树CSS 规则树来构造 Render Tree。(注意:Render Tree 渲染树并不等同于 DOM 树,因为一些像 Headerdisplay:none 的东西就没必要放在渲染树中了。)

      4. 有了 Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的 CSS 定义以及他们的从属关系。下一步进行 layout,进入布局处理阶段,即计算出每个节点在屏幕中的位置。

      5. 最后一个步骤就是绘制,即遍历 RenderTree,层绘制每个节点。根据计算好的信息绘制整个页面。



    • 渲染完成之后,开始执行其它任务:



      1. dom 操作

      2. ajax 发起的 http 网络请求等等……

      3. 浏览器处理事件循环等异步逻辑等等……





  • 菜单左中右布局,两边定宽,中间自适应,说一下有几种实现方式



    • 比较经典的面试题,说了 flexfloat 两种方式。



  • 项目难点



    • 和一面一样,说了协同文档的两大难点,这里就不重复了。



  • 你有什么想问我的吗?



    • 和一面一样,问了一下面试结果大概多久能反馈给我,对方说两三天左右,然后就结束了。



  • 最后问了期望薪资什么的,然后就结束了。


二面结束之后,大概过了几个小时,在 Boss 上跟对方说了一声,如果没过的话也麻烦跟我说一下,然后这时候,对方在 Boss 上问我,第一学历是不是专科?我说是的,感觉到不太妙的样子,


然后又过了一会儿,对方说定级应该不会高,他后续看一下面试官的反馈如何……


然后又追问我,换工作的核心诉求是涨薪还是能力的提升,这里我回答的比较委婉,其实两个都想要 QAQ


今天已经是第二天了,目前没有下文,看起来二面是过了,但是因为学历不够,中止了三面的流程,基本上是失败了,我也不会报有什么希望了,所以写个面经记录一下。


作者:三年没洗澡
来源:juejin.cn/post/7239715208792342584
收起阅读 »

创新故事:食堂有了扫脸支付后,我扔掉了工牌

今天跟大家分享一个创新故事。 我们公司原来一直是有工牌的,工牌除了刷门禁还能够刷各种餐饮,取夜宵等等。 当然这种也是最早时期的工牌了,随后公司就上线了一个方式,就是可以用扫脸来刷门禁,不管在阿里巴巴的大园区还是在各个分公司都有扫脸的工具。 那时候我就想对于科...
继续阅读 »

今天跟大家分享一个创新故事。



我们公司原来一直是有工牌的,工牌除了刷门禁还能够刷各种餐饮,取夜宵等等。


当然这种也是最早时期的工牌了,随后公司就上线了一个方式,就是可以用扫脸来刷门禁,不管在阿里巴巴的大园区还是在各个分公司都有扫脸的工具。


那时候我就想对于科技公司来说,增加一个扫脸工具并不是什么难事儿,但是会解决了很多问题,比如工牌找不到了,或者忘记带工牌了,这个时候。匆匆忙忙又回家取工牌特别的麻烦,有的扫脸工具就24小时通行无忧了。


但是还是有一点那个在食堂支付的时候还是需要刷工牌,这个从扫脸门禁上线后,持续了一年多还是没有扫脸支付餐饮。


所以如果工牌没带刷门禁是可以进公司的,但是要吃饭或者拿夜宵,还是要去借一个临时的工牌再去刷卡。


我当时就有这么一个疑问,为什么这种食堂或者吃夜宵的地方不能够也用刷脸工具呢?我当时自己给他解释是支付可能比较敏感,用人脸识别可能存在风险,另外一点就是食堂系统和公司的系统没有打通问题,毕竟食堂系统是公司找的外包服务商。


总而言之,我把这个场景给预设很难了,用自己的想法去认为当前存在是合理的,而且是很难改变的,要不然也不至于一年多了还是依赖工牌。


这个就是面对于创新的一个非常大的一个思维局限性,就是我们会预设当前的问题没有解决是存在瓶颈,把当前的困难合理化了,而不加以推测或者说去质疑当前的解决方案。


随后过了差不多几个月之后,我发现食堂的刷餐饮的也支持扫脸支付了。


所以这个就给我打了一个脸,我原来自己给出了一个理由和解释,其实完全的不成立,只是因为各种原因,比如说系统各方面的原因没有支持而已,并不是支持不了。而且真正要支持起来也非常的快,我发现在用扫脸支付餐饮的时候的人已经非常多了,说明了这个需求本来就是一个大众型的需求。


但这个需求却足足等了至少有一两年才全部上线。


所以从现在来看,这个问题其实是一个非常刚需的问题,也是被很多用户视为痛点的问题,但是却花了一到两年才覆盖和普及。当然这也说明了这个需求可能就不是那么特别阻断性的,因为假如你没有带卡,你也可以取临时工牌,甚至很多人干脆就不在这里支付了,有各种备选的方案。


但这个例子还是告诉我们身边的创新点其实非常的多,就看我们愿不愿意做,就看有没有投入产出比。


但是不得不说这样的一个微小的创新就能够使我完全不依赖工牌,轻轻松松无卡上班,吃饭,回家。有了这个扫脸工具以后,我就可以彻底扔掉了工牌。


作者:ali老蒋
来源:juejin.cn/post/7307090059354669066
收起阅读 »

简历中不写年龄、毕业院校、预期薪资会怎样?

无意中看到一条视频,点赞、转发量都非常高,标题是“不管你有多自信,简历中的个人信息都不要这样写”。看完之后简直有些无语,不仅哗众取宠,甚至会误导很多人。 之所以想写这篇文章,主要是分享给大家一种思维方式:如果别人说的事实或观点,只有情绪、结论,没有事实依据和推...
继续阅读 »

无意中看到一条视频,点赞、转发量都非常高,标题是“不管你有多自信,简历中的个人信息都不要这样写”。看完之后简直有些无语,不仅哗众取宠,甚至会误导很多人。


之所以想写这篇文章,主要是分享给大家一种思维方式:如果别人说的事实或观点,只有情绪、结论,没有事实依据和推导,那么这些事实和观点是不足信的,需要慎重对待。


视频的内容是这样的:“不管你有多自信,简历中的个人信息都不要这样写。1、写了期望薪资,错!2、写了户籍地址,错!3、写了学历文凭,错!4、写了离职原因,错!5、写了生日年龄,错!6、写了自我评价,错!


正确写法,只需要写姓名和手机号、邮箱及求职意向即可,简历个人信息模块的作用是让HR顺利联系到你,所有任何其他内容都不要写在这里……”


针对这条视频的内容,有两个不同的表现:第一就是分享和点赞数量还可以,都破千了;第二就是评论区很多HR和求职着提出了反对意见。


第一类反对意见是:无论求职者或HR都认为这样的简历是不合格的,如果不提供这些信息,根本没有预约面试的机会,甚至国内的招聘平台的简历模板都无法通过。第二类,反对者认为,如果不写这些信息,特别是预期薪资,会导致浪费双方的时间。


针对上述质疑,作者的回复是:”看了大家的评论,我真的震惊,大家对简历的误解是如此至深……“


仔细看完视频和评论,在视频的博主和评论者之间产生了一个信息差。博主说的”个人信息“不要写,给人了极大的误导。是个人信息栏不要写,还是完全不写呢?看评论,大多数人都理解成了完全不写。博主没有说清楚是不写,还是写在别处,这肯定是作者的锅。


本人也筛选过近千份简历,下面分享一下对这则视频中提到的内容的看法:


第一,户籍、离职原因可以不写


视频中提到的第2项和第4项的确可以不写。


户籍这一项,大多数情况下是可以不写的,只用写求职城市即可,方便筛选和推送。比如,你想求职北京或上海的工作,这个是必须有的,而你的户籍一般工作没有强制要求。但也有例外,比如财务、出纳或其他特殊岗位,出于某些原因,某些公司会要求是本地的。写不写影响没那么大。


离职原因的确如他所说的,不建议写,是整个简历中都不建议写。这个问到了再说,或者填写登记表时都会提到,很重要,要心中有准备,但没必要提前体现。


第二,期望薪资最好写上


关于期望薪资这个有两种观点,有的说可以不写,有的说最好写上。其实都有道理,但就像评论中所说:如果不写,可能面试之后,薪资相差太多,导致浪费了双方的时间。


其实,如果可以,尽量将期望薪资写上,不仅节省时间,这里还稍微有一个心理锚定效应,可以把薪资写成范围,而范围的下限是你预期的理想工资。就像讨价还价时先要一个高价,在简历中进行这么一个薪资的锚定,有助于提高最终的薪资水平。


第三,学历文凭一定要写


简历中一定要写学历文凭,如果没有,基本上是会默认为没有学历文凭的,是不会拿到面试邀约的。仔细想了一下,那则视频的像传达的意思可能是不要将学历文凭写作个人信息栏,而是单独写在教育经历栏中。但视频中没有明说,会产生极大的误导。


即便是个人信息栏,如果你的学历非常漂亮,也一定要写到个人信息栏里面,最有价值,最吸引眼球的信息,一定要提前展现。而不是放在简历的最后。


第四,年龄要写


视频中提到了年龄,这个是招聘衡量面试的重要指标,能写尽量写上。筛选简历中有一项非常重要,就是年龄、工作经历和职位是否匹配。在供大于求的市场中,如果不写年龄,为了规避风险,用人方会直接放弃掉。


前两个月在面试中,也有遇到因为年龄在30+,而在简历中不写年龄的。作为面试官,感觉是非常不好的,即便不写,在面试中也需要问,最终也需要衡量年龄与能力是否匹配的问题。


很多情况下,不写年龄,要么认为简历是不合格的,拿不到面试机会,要么拿到了面试机会,但最终只是浪费了双方的时间。


第五,自我评价


这一项与文凭一样,作者可能传达的意思是不要写在个人信息栏中,但很容易让人误解为不要写。


这块真的需要看情况,如果你的自我评价非常好,那一定要提前曝光,展现。


比如我的自我评价中会写到”全网博客访问量过千万,CSDN排名前100,出版过《xxx》《xxx》书籍……“。而这些信息一定要提前让筛选简历的人感知到,而不是写在简历的最后。


当然,如果没有特别的自我评价,只是吃苦耐劳、抗压、积极自主学习等也有一定的积极作用,此时可以考虑放在简历的后面板块中,而不是放在个人信息板块中。这些主观的信息,更多是一个自我声明和积极心态的表现。


最后的小结


经过上面的分析,你会看到,并不是所有的结论都有统一的标准的。甚至这篇文章的建议也只是一种经验的总结,一个看问题的视角而已,并不能涵盖和适用所有的场景。而像原始视频中那样,没有分析,没有推导,没有数据支撑,没有对照,只有干巴巴的结论,外加的煽动情绪的配音,就更需要慎重对待了。


在写这篇文章的过程中,自己也在想一件事:任何一个结论,都需要在特定场景下才能生效,即便是牛顿的力学定律也是如此,这才是科学和理性的思维方式。如果没有特定场景,很多结论往往是不成立的,甚至是有害的。


作者:程序新视界
来源:juejin.cn/post/7268593569782054967
收起阅读 »

前几天有个雏鹰问我,说怎么创建Menu???

这个很简单了哈,直接上代码算了 自己在这个路径下面创建一个这个的这个这个这个,很直观吧 <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.a...
继续阅读 »

这个很简单了哈,直接上代码算了
自己在这个路径下面创建一个这个的这个这个这个,很直观吧


截屏2023-11-29 22.13.12.png


<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<item
android:id="@+id/list_view"
android:title="@string/listview">

<menu>
<item
android:id="@+id/list_view_vertical_only"
android:title="垂直标准"
tools:ignore="DuplicateIds" />

<item
android:id="@+id/list_view_vertical_reverse"
android:title="垂直反向" />

<item
android:id="@+id/list_view_horizontal_only"
android:title="水平标准" />

<item
android:id="@+id/list_view_horizontal_reverse"
android:title="水平反转" />

</menu>
</item>
</menu>

然后读取目录路面的条目的时候有一个过滤器,把你自己添加的目录放进来,点击事件也帮你写好了,里面想怎么整自己搞,


@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu, menu);
return super.onCreateOptionsMenu(menu);
}

@SuppressLint("NonConstantResourceId")
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
if (itemId != 0)
switch (itemId){
case R.id.list_view:
break;
case R.id.list_view_vertical_only:
break;
case R.id.list_view_vertical_reverse:
break;
case R.id.list_view_horizontal_only:
break;
case R.id.list_view_horizontal_reverse:
break;
}
return super.onOptionsItemSelected(item);
}

结束结束,希望下次雏鹰可以自己看,或者自己搜下,很简单的东西


作者:贾炬山
来源:juejin.cn/post/7306706954678763556
收起阅读 »

可以,很6!微信这波改造,一刀斩断了一条“灰色”业务线。

你好呀,我是歪歪。 微信官方今天“悄悄咪咪”的搞了一个小动作,而这个小动作我关注了接近两个月时间,今天终于是上线了。 对微信来说,这也许就是一个小迭代。 这个迭代对于用户来说,算是一个利好的动作,体现了微信订阅号“以用户为中心”的指导思想。但是对于一些行业来说...
继续阅读 »

你好呀,我是歪歪。


微信官方今天“悄悄咪咪”的搞了一个小动作,而这个小动作我关注了接近两个月时间,今天终于是上线了。


对微信来说,这也许就是一个小迭代。


这个迭代对于用户来说,算是一个利好的动作,体现了微信订阅号“以用户为中心”的指导思想。但是对于一些行业来说,它直接是一刀斩断了一条业务线。


看我说的这么牛逼,那么到底是啥迭代呢?


我还是先给你上示例个图片吧:



这个是充值话费之后,运营商的官方账号会推送一条消息给我,告知我话费充值成功。这条消息的官方叫法叫做:微信模板消息。


但是你仔细一看,会发现这个模板消息里面夹带了一个私货“备注”字段。这个“备注”字段的内容和我本次充值的动作没有任何关系,是一个纯粹的营销动作。


今天,我同样进行了充值的动作,但是收到的消息是这样的:



一眼望去,全是变化。


这样对比起来看更加明显:



对于用户来说,确实是清爽了不少。微信官方完全屏蔽了一些“营销”推送对于用户的打扰。


看起来确实是一个小改造,至少站在开发的角度,不过是隐藏了部分字段而已。


但是我想聊一下关于这个改造,我知道的一点背后的故事。


需要提前说明的是,由于我是在工作中密切接触微信模板消息的,所以洋洋洒洒写了这么一篇文章。


如果你没有接触过这块内容的话,那么可能看起来觉得有点莫名其妙,很正常,顺便划拉两下走人得了。


反复横跳


因为我在公司里面就负责对客微信消息推送的功能,而且我们推送给客户的消息都是非常关键的业务消息,一旦这个对客触达环节整个断开,势必要领个生产事件的。


所以对于微信官方的这个“小迭代”我是密切关注,非常害怕由于微信改动之后,我们配套改动不及时,导致对客微信消息推送失败。


在整个过程中,如果让我用一个词来形容这次变更的话,那么就是四个字:反复横跳。


光是关于这次变化的官方公告,就前后发了三次,后两次都是对于第一次的补丁:



从官方推送上来看,只有两次公告,还有一次我一会说,我先给你盘一下这两次公告到底是在干啥。


首先 3 月 24 日发布了第一条公告,公告里面第一段话是这样的:



公众号的模板消息能力旨在方便开发者为用户提供服务凭证通知。近期平台发现,部分公众号下发的模板消息涉及包括但不限于商品营销、服务推广等内容。且利用颜色/表情符号诱导用户查看、点击。上述行为损害了用户消息接收体验,给用户带来极大困扰,引起大量用户投诉。



然后官方提供了两个案例。


第一个案例是这样的:



这个案例和我刚刚举的充话费的案例其实是一回事儿。


在用户触发了业务场景之后,对客户进行了微信模板消息的触达,但“备注”字段的内容是营销推广内容,同时在里面使用了自定义颜色、emoji 表情符号,意图诱导用户进行点击。


u1s1,我作为微信用户,这种夹带私货的推送我勉勉强强能够接受。当然了,营销的部分完全被去掉了,当然是更好的。


我不能接受的是官方下面给出的这种案例:



你有没有看出什么端倪?


使用的是官方的“退款成功通知”模板,但是内容是纯纯的营销推广内容,这不就是挂羊头卖狗肉吗?


甚至更加过分的是,对于一个类型为服务号的微信公众号来说,他下发这种模板消息给用户,并不需要用户触发业务场景。换句话说,不需要经过用户同意直接发就完事了。


你想想,你正在打团呢,突然微信弹出一个“退款成功通知”,你一脸懵逼的切过去,发现是营销内容,恶狠狠的骂几句之后切回游戏,发现已经团灭了。


你说遭不遭得住?


遭不住,对不对。



但是,注意,我要说但是了。


这个地方其实就是存在一条产业链的。有的品牌运营方,专门找服务号去推送这种模板消息,给号主一个难于拒绝的价格,然后推送一条“扰民的”、“灰色的”消息:



我理解微信可能更想解决这类问题,斩断这个在“灰色地带”的产业链,因为它确实是扰民,而且营销的内容确实是灰色的,甚至违法的。


但是微信采取的方案是:掀桌子,大家都别玩了。


快刀斩乱麻,一刀切掉“备注字段”、“自定义颜色”、“emoji表情”,完事:



同时官方说这个变更会在 3 月 30 日开始生效:



我看到这个公告并仔细研读了公告的内容后,确定会对我们产生一定的业务影响,我第一时间拉了个小群,把这个消息同步给了对应负责的业务同事,他们在当天就在内部同步了这个信息。


然后就开始盼星星,盼月亮的等 3 月 30 日的到来,想看看微信这波改造之后的效果是怎么样的。


时间很快,来到了 3 月 30 日凌晨,我迫不及待的做了一个测试,发现并没有发生任何变化,备注字段还在:



想着也正常,微信这么大的体量,肯定是有一个逐步覆盖的过程。


于是我在 3 月 30 日中午 11 点又做了一个测试:



发现还是没啥动静,开始觉得事情可能有变。果然在中午 12 点 33 分,收到的官方的补丁通知:



这次的公告内容如下:



带来一个好消息和坏消息。


好消息是改造生效的时间推迟了一个月。


坏消息是这次补丁公告直接把首行内容也噶了,一顿阉割之后,只剩下了最纯粹的通知属性。


于是等啊等,终于等到了 5 月 4 日,早中晚几个时间段一顿测试之后,发现并没有生效。


然后在晚上 22 点 38 分,等来一个通知:



这个通知就是关于模板消息的第三次通知。通知里面第四点,也是打了一个补丁。


然后,5 月 5 日,全面生效了。


可以说是反复横跳了。


另外,我还发现在“微信公众平台运营中心”公众号中针对“单个中间主内容不超过 20 字”对应的内容并没有单独推文说明,而是通过在 5 月 5 日修改 3 月 30 日推文的方式来进行“打补丁”:



哦,对了,说到文章最开始的话费充值,由于我是直接用的微信钱包充值的,所以我同时收到了这样的一条推送:



咱也不知道啥情况啊,为什么“腾讯手机充值”的头部信息和备注信息还在呢?


我猜应该是还在逐步覆盖中吧,微信不可能因为是腾讯旗下自家的号,就区别对待的。


你说对吧。



一点思考


我再带你捋一捋这个事情的全过程。


首先,我们抛开字段颜色和 emoji 表情不说。


3 月 24 日的时候,官方说要变,但是只动备注字段,3 月 30 日生效。


3 月 30 日的时候,官方说我们再缓一缓吧,5 月 4 日生效,但是这次我要把首行内容也噶了。


5 月 4 日的时候,官方说马上就生效了,对了,还有一个“小改动”,就是其他字段内容不超过 20 个字,且不支持换行哦。


5 月 5 日,生效。


最后一个补丁从晚上发出通知,到第二天一早生效,只过了一晚上的时间。


但是这个补丁,刚好就是我们之前想到的应对之策。可以把业务字段扩长,然后达到类似的引导用户点击的效果:



当时也测试了,确实是可以的。


结果,突然一下,啪的一声,方案没用了。


有一种被官方绝杀了的感觉。


在整个过程中,我作为一个在工作中使用了模板消息的开发者,感受到的是“不专业”,感觉整个事件从提出到落地,都是拍脑子,没有想清楚细节就开始搞事情,导致一个事情反复打补丁,反复被开发者吐槽。


但是据我考察,实际情况是,微信官方想动“模板消息”已经是想了很久了,比如我就找到了 2021 年 1 月 27 日官方发布的这个公告:



developers.weixin.qq.com/community/d…




这个灰度测试对应的方案吧,我只能说...


算了,我还是不说了吧,截个该公告下的评论:



既然两年前就打算动这个东西了,两年后真的动起手来的时候,还是搞成这样。


哎,一言难尽,一言难尽啊。


在这个过程中,我最害怕的还是微信突然发公告说,开发者也需要做对应的改动,比如如果调用接口的时候传递了备注字段,则不会发送给客户。幸好,这次并没有出现这种情况,不然我真的会好好的“谢谢”提出这个需求的 PM。


这样“不专业”的感受更多的还体现在官方的接口文档中,在全过程中,截至我写文章的时候,官方的接口文档对于首行(first.DATA)和备注(remark.DATA)字段,一直在变化,但是一直都没有处理干净:



developers.weixin.qq.com/doc/offiacc…





同时,对于最后一次补丁公告中的“中间的主内容中,单个字段内容不超过 20 个字,且不支持换行”这部分描述,在接口文档中没有任何的体现。


别问,问就是自己去试,或者靠得就是一个口口相传。



然后还有一个感受是和大家的感受相同的:



一刀切,确实很简单。但是对于开发者来说,里面少了一点关怀,多了一点躺枪。因为大部分开发者基于“备注”字段做的都是对于这次消息推送的进一步说明,而不是对客营销。


对于这部分开发者来说,官方的这次阉割是比较致命的。


所以更加人性的做法应该是谁滥用,就惩罚谁。而不是采取乌鸦哥行为:



最后的一个感悟,也是最大的一个感悟:对于依托微信服务号模板消息来触达客户,开展业务的公司来说,还是咬咬牙做自己的 APP 吧。虽然开发和运营成本上去了,至少触达客户的时候,字段不会被说砍就砍。


自主研发,总比卡脖子好。


作者:why技术
来源:juejin.cn/post/7229895584038305851
收起阅读 »

Android APP合规检查,你可能需要这个工具~

虽迟但到,这是一个通过拦截Java方法调用用以检测应用是否合规的工具,如果你的APP正饱受监管部门或应用市场时不时下发整改通知的折磨,那么用它来检查你的代码以及引用的三方库是再好不过的选择了! 如何引入 Step 1. 添加 mavenCentral all...
继续阅读 »

logo.png


虽迟但到,这是一个通过拦截Java方法调用用以检测应用是否合规的工具,如果你的APP正饱受监管部门或应用市场时不时下发整改通知的折磨,那么用它来检查你的代码以及引用的三方库是再好不过的选择了!


如何引入


Step 1. 添加 mavenCentral


allprojects {
repositories {
...
mavenCentral()
}
}

Step 2. 添加 Gradle 依赖


dependencies {
...
implementation 'io.github.loper7:miit-rule-checker:0.1.1'
}

如何使用


检查APP内是否存在不合规的方法调用



检查MIITRuleChecker内置的不合规的方法,具体可见下方方法列表



 MIITRuleChecker.checkDefaults()


如果内置的方法不满足当前需求,可自定义方法添加到list中进行检查;

比如新增一个 MainActivity 的 onCreate 方法的调用检查;



val list = MIITMethods.getDefaultMethods()
list.add(MainActivity::class.java.getDeclaredMethod("onCreate" , Bundle::class.java)) MIITRuleChecker.check(list)

当然,如果你想检查多个内置方法外的方法,只需要创建一个新的集合,往集合里放你想检查的方法member,然后传入 MIITRuleChecker.check()内即可。


log打印如下所示:


method_androidid.png


检查指定方法调用并查看调用栈堆


//查看 WifiInfo classgetMacAddress 的调用栈堆
MIITRuleChecker.check(MIITMethods.WifiInfo.getMacAddress)

log打印如下所示:


method_macaddress.png


检查一定时间内指定方法调用次数统计


//多个方法统计 (deadline 为从方法调用开始到多少毫秒后截至统计)
val list = mutableListOf().apply {
add(MIITMethods.LocationManager.getLastKnownLocation)
add(MIITMethods.LocationManager.requestLocationUpdates)
add(MIITMethods.Secure.getString)
}
MIITMethodCountChecker.startCount(list , 20 * 1000)

//单个方法统计(deadline 为从方法调用开始到多少毫秒后截至统计)
MIITMethodCountChecker.startCount(MIITMethods.LocationManager.getLastKnownLocation , deadline = 20 * 1000)

log打印如下所示:


log_count.png


检查完成并完成整改后务必移除方法 miit-rule-checker 库内的所有方法调用,将库一起移除最好


内置方法表


内置常量对应的系统方法备注
MIITMethods.WifiInfo.getMacAddressandroid.net.wifi.WifiInfo.getMacAddress()获取MAC地址
MIITMethods.WifiInfo.getIpAddressandroid.net.wifi.WifiInfo.getIpAddress()获取IP地址
MIITMethods.LocationManager.getLastKnownLocationandroid.location.LocationManager.getLastKnownLocation(String)获取上次定位的地址
MIITMethods.LocationManager.requestLocationUpdatesandroid.location.LocationManager.requestLocationUpdates(String,Long,Float,LocationListener)
MIITMethods.NetworkInterface.getHardwareAddressjava.net.NetworkInterface.getHardwareAddress()获取主机地址
MIITMethods.ApplicationPackageManager.getInstalledPackagesandroid.app.ApplicationPackageManager.getInstalledPackages(Int)获取已安装的应用
MIITMethods.ApplicationPackageManager.getInstalledApplicationsandroid.app.ApplicationPackageManager.getInstalledApplications(Int)获取已安装的应用
MIITMethods.ApplicationPackageManager.getInstallerPackageNameandroid.app.ApplicationPackageManager.getInstallerPackageName(String)获取应用安装来源
MIITMethods.ApplicationPackageManager.getPackageInfoandroid.app.ApplicationPackageManager.getPackageInfo(String,Int)获取应用信息
MIITMethods.PackageManager.getInstalledPackagesandroid.content.pm.PackageManager.getInstalledPackages(Int)获取已安装的应用
MIITMethods.PackageManager.getInstalledApplicationsandroid.content.pm.PackageManager.getInstalledApplications(Int)获取已安装的应用
MIITMethods.PackageManager.getInstallerPackageNameandroid.content.pm.PackageManager.getInstallerPackageName(String)获取应用安装来源
MIITMethods.PackageManager.getPackageInfoandroid.content.pm.PackageManager.getPackageInfo(String,Int)获取应用信息
MIITMethods.PackageManager.getPackageInfo1android.content.pm.PackageManager.getPackageInfo(String,PackageInfoFlags)获取应用信息(版本号大于33)
MIITMethods.PackageManager.getPackageInfo2android.content.pm.PackageManager.getPackageInfo(VersionedPackage,Int)获取应用信息(版本号大于26)
MIITMethods.PackageManager.getPackageInfo3android.content.pm.PackageManager.getPackageInfo(VersionedPackage,PackageInfoFlags)获取应用信息(版本号大于33)
MIITMethods.Secure.getStringandroid.provider.Settings.Secure.getString(ContentResolver,String)获取androidId
MIITMethods.TelephonyManager.getDeviceIdandroid.telephony.TelephonyManager.getDeviceId()获取 DeviceId
MIITMethods.TelephonyManager.getDeviceIdWithIntandroid.telephony.TelephonyManager.getDeviceId(Int)获取 DeviceId
MIITMethods.TelephonyManager.getImeiandroid.telephony.TelephonyManager.getImei()获取 Imei
MIITMethods.TelephonyManager.getImeiWithIntandroid.telephony.TelephonyManager.getImei(Int)获取 Imei
MIITMethods.TelephonyManager.getSubscriberIdandroid.telephony.TelephonyManager.getSubscriberId()获取 SubscriberId

作者:LOPER7
来源:juejin.cn/post/7307470097663688731
收起阅读 »

这就是为什么我不接受你是Senior软件工程师

软件行业的一个令人担忧的趋势 工程类职位的面试极其复杂, 压力大, 而且我必须为所有敢于接受面试以实现职业梦想的人鼓掌, 并对他们表示赞赏. 过去几年里, 我一直在为不同的公司面试工程师, 但最近, 我前所未有地拒绝了很多应聘者. 我还是用同样的问题, 没有任...
继续阅读 »

软件行业的一个令人担忧的趋势


工程类职位的面试极其复杂, 压力大, 而且我必须为所有敢于接受面试以实现职业梦想的人鼓掌, 并对他们表示赞赏.


过去几年里, 我一直在为不同的公司面试工程师, 但最近, 我前所未有地拒绝了很多应聘者. 我还是用同样的问题, 没有任何变化, 问题是人们根本不知道高级工程师意味着什么, 而这些专业人员比以往任何时候都更稀缺.


编程 10 年并不能让你成为高级工程师. 这与时间无关.


以下是我的努力, 只为探讨软件开发行业高级工程师究竟意味着什么.



在互联网上发现的流行笑话.


什么是高级工程师?


我向 ChatGPT 询问了关于什么是高级软件工程师的通用描述, 结果它一语中的:



高级工程师对编程语言, 软件设计原则和开发方法论有深刻的理解.


— ChatGPT, 在抢走我们的饭碗之前.



这正是经典高级面试的通用结构:


开发方法论


开发方法论是旨在提高团队效率的组织方法. 这些对我们来说可能很枯燥, 但我们希望你能掌握这方面的专业知识.


在多年的工作中, 我对非敏捷开发方法产生了排斥心理, 不仅如此, 我认为 Scrum 还不够敏捷, 它的详尽使用最终会让项目经理而非程序员的自负增强.



互联网上的笑话.


在面试时, 我希望开发人员具备批判能力, 因为仅仅了解 Scrum 是不够的, 你还必须知道它的缺点, 并提出解决方案.


我还想看看开发人员是否了解 Scrum 和 Kanban 之外的其他方法, 比如 RUP. 常识可以帮助你形成更好的观点, 并表明你愿意学习自己领域以外的知识.


软件设计原则


这些原则可能一辈子只读一次就会忘记, 但真正熟练的程序员每天都在使用.



明星工程师之所以比同行更有价值, 原因并不局限于编程. 伟大的软件工程师具有惊人的创造力, 能看到别人看不到的概念模式.


Reed Hastings. Netflix 联合创始人.



每次面试结束后, 在给应聘者写反馈时, 我发誓每次都能复制粘贴同样的回复:



我建议你多读读 Python 设计模式, 本指南就是一个很好的资源.



用设计原则筛选候选人真是太容易了...只有在非常奇怪的情况下, 我才会发现有人能回答所有这些问题.



这是一个笑话, 记录了大多数开发人员在被问及设计模式时的表情.


当你熟悉了软件设计模式之后, 在实践中实现这些模式可能会相当具有挑战性. 我本人就曾面临过这样的挑战, 因为在编码时回忆并应用它们并非易事. 但是, 强迫自己在每次编程时进行这一检查, 一定会让你领先一步.


编程语言


为什么我们在 Python 中使用 len(array) 而在其他语言中使用 array.length()? 这背后有什么优化吗?


你对自己的语言了解多少?


在你成长的过程中, 阅读一本关于你所使用的语言的书是绝对必要的. 任何高级工程面试都会包括一些只有通过阅读才能了解的高级问题.


态度不好扼杀了许多面试机会



你们能快点提供反馈吗? 现在有其他公司向我发出邀请.



  • 一位候选人在面试时说的话, 当时他有时间向我们提出有关该职位的问题.



公司在提供高薪的同时, 也在寻找优秀的人才, 而其中的一部分就是做个好人.


面试时, 我们不仅要评估应聘者的知识, 还要评估她/他的态度. 在我几个月前做的一个具体工作中, 公司特别要求我们寻找"善良, 诚实"的人, 而不是优秀的程序员.


与一个虚荣的人共事是非常困难的, 他们会让你一直感觉不好, 并在团队中造成不好的氛围. 从长远来看, 这样的人对公司造成的损害可能是非常大的.


态度恶劣或撒谎可能意味着在选拔过程中被立即淘汰.


我一直向大家推荐<如何赢得朋友和影响他人>一书, 这是一本每个人都应该读一读的书, 以便在生活中拥有更好的人际关系.



互联网上找到的笑话.


记住, 经验丰富的工程师应该能够领导一个团队, 你需要真正强大的社交能力才能胜任这个职位.


初级/中级开发人员要成为高级开发人员, 应该读些什么?


这里有一些让你成长为高级开发人员的绝佳资源:


代码技能如何?


几个月前, 我购买了LeetCode Premium, 这是我找到现在这份工作的一个伟大决定. 在使用过程中, 你会从其他用户那里发现有趣的数学奇闻, 模式和优化方法.


模拟大公司的面试也有助于了解他们的组织结构.



瞄着星星, 才有可能够着天空
— Reinhold Niebuhr




Leetcode 的大公司问题列表.


还有几个网站与 LeetCode 类似, 如AlgoExpertCodeSignal.


残酷的真相


即使你阅读了大量的资料, 每天进行练习, 并积累了丰富的经验, 你也有可能因为不符合公司的要求而被拒绝.


几天前, 我在听一个播客, 他们谈到了一个实验: 让孩子们解决问题, 并把他们分成两组:


当让他们接触新问题时, 被表扬有毅力的孩子倾向于选择更难的问题来解决, 而那些被说成聪明的孩子最终在他们解决的问题上表现得更差.


这说明, 你唯一能指望自己做到的就是坚韧不拔和坚持不懈. 这些不仅是保证你未来工作的技能, 也是我们对高级工程师的主要期望.


作者:bytebeats
来源:juejin.cn/post/7307723756410896411
收起阅读 »

华为自研的前端框架是什么样的?

web
大家好,我卡颂。 最近,华为开源了一款前端框架 —— openInula。根据官网提供的信息,这款框架有3大核心能力: 响应式API 兼容ReactAPI 官方提供6大核心组件 并且,在官方宣传视频里提到 —— 这是款大模型驱动的智能框架。 ...
继续阅读 »

大家好,我卡颂。


最近,华为开源了一款前端框架 —— openInula。根据官网提供的信息,这款框架有3大核心能力:



  1. 响应式API




  1. 兼容ReactAPI




  1. 官方提供6大核心组件



并且,在官方宣传视频里提到 —— 这是款大模型驱动智能框架


那么,这究竟是款什么样的前端框架呢?我在第一时间体验了Demo,阅读了框架源码,并采访了框架核心开发者。本文将包括两部分内容:



  1. 对框架核心开发者陈超涛的采访

  2. 卡颂作为一个老前端,阅读框架源码后的一些分析

采访核心开发者


开发Inula的初衷是?


回答:


华为内部对于业务中强依赖的软件,考虑到竞争力,通常会开发一个内部使用的版本。


Inula在华为内部,从立项到现在两年多,基本替换了公司内绝大部分React项目。



卡颂补充背景知识:Inula兼容React 95% API,最初开发的目的就是为了替换华为内部使用的React。为了方便理解,你可以将Inula类比于华为内部的React



为什么开源?


回答:


华为对于自研软件的公司策略,只要是公司内部做的,觉得还ok的自研都会开源。



接下来的提问涉及到官网宣传的内容



宣传片提到的大模型赋能、智能框架是什么意思?


回答:


这主要是Inula团队与其他外部团队在AI低代码方向的一些探索。比如:



  1. 团队与上海交大的一个团队在探索大模型赋能chrome调试业务代码方面有些合作,目的是为了自动定位问题

  2. 团队与华为内部的大模型编辑器团队合作,探索框架与编辑器定制可能性


以上还都属于探索阶段。


Inula未来有明确的发展方向么?


回答:


团队正在探索引入响应式API,相比于React的虚拟DOM方案,响应式API能够提高运行时性能。24年可能会从Vue composition API中寻求些借鉴。


新的发展方向会在项目仓库以RFC的形式展开。



补充:RFCRequest for Comments的缩写。这是一种协作模式,通常用于提出新的特性、规范或者改变现有的一些规则。RFC的目的是收集不同的意见和反馈,以便在最终确定一个决策前,考虑尽可能多的观点和影响。



为什么要自研核心组件而不用社区成熟方案?



卡颂补充:所谓核心组件,是指状态管理、路由、国际化、请求库、脚手架这样的框架生态相关的库。既然Inula兼容React,为什么不直接用React生态的成熟产品,而要自研呢?毕竟,这些库是没有软件风险的。




回答:


主要还是丰富Inula生态,根据社区优秀的库总结一套Inula官方推荐的最佳实践。至于开发者怎么选择,我们并不强求。


卡颂的分析


以上是我对Inula核心开发者陈超涛的采访。下面是我看了Inula源码后的一些分析。


要分析一款前端框架,最重要的是明白他是如何更新视图的?这里我选择了两种触发时机来分析:



  1. 首次渲染


触发的方式类似如下:


Inula.render(<App />, document.getElementById("root"));


  1. 执行useState的更新方法触发更新


触发的方式类似如下:


function App() {
const [num, update] = useState(0);
// 触发更新
update(xxx);
// ...
}

顺着调用栈往下看,他们都会执行两步操作:



  1. 创建名为update的数据结构

  2. 执行launchUpdateFromVNode方法


比如这是首屏渲染时:



这是useState更新方法执行时:



launchUpdateFromVNode方法会向上遍历到根结点(源码中遍历的节点叫VNode),再从根节点开始遍历树。由此可以判断,Inula的更新机制与React类似。


所有主流框架在触发更新后,都不会立刻执行更新,中间还有个调度流程。这个流程的存在是为了解决:



  1. 哪些更新应该被优先执行?

  2. 是否有些更新是冗余的,需要合并在一块执行?


Vue中,更新会在微任务中被调度并统一执行,在React中,同时存在微任务(promise)与宏任务(MessageChannel)的调度模式。


Inula中,存在宏任务的调度模式 —— 当宿主环境支持MessageChannel时会使用它,不支持则使用setTimeout调度:



同时,与这套调度机制配套的还有个简单的优先级算法 —— 存在两种优先级,其中:



  • ImmediatePriority:对应正常情况触发的更新

  • NormalPriority:对应useEffect回调


每个更新会根据更新的ID(一个自增的数字)+ 优先级对应的数字 作为优先级队列中的排序依据,按顺序执行。


假设先后触发2次更新,优先级分别是ImmediatePriorityNormalPriority,那么他们的排序依据分别是:



  1. 100(假设当前ID到100了)- 1(ImmediatePriority对应-1) = 99

  2. 101(100自增到101)+ 10000(NormalPriority对应10000)= 10101


99 < 10101,所以前者会先执行。


需要注意的是,Inula中对更新优先级的控制粒度没有React并发更新细,比如对于如下代码:


useEffect(function cb() {
update(xxx);
update(yyy);
})

React中,控制的是每个update对应优先级。在Inula中,控制的是cb回调函数与其他更新所在回调函数之间的执行顺序。


这意味着本质来说,Inula中触发的所有更新都是同步更新,不存在React并发更新中高优先级更新打断低优先级更新的情况。


这也解释了为什么Inula兼容 95% 的React API,剩下 5% 就是并发更新相关API(比如useTransitionuseDeferredvalue)。


现在我们已经知道Inula的更新方式类似React,那么官网提到的响应式API该如何实现呢?这里存在三条路径:



  1. 一套外挂的响应式系统,类似ReactMobx的关系

  2. 内部同时存在两套更新系统(当前一套,响应式一套),调用不同的API使用不同的系统

  3. 重构内部系统为响应式系统,通过编译手段,使所有API(包括当前的React API与未来的类 Vue Composition API)都走这套系统



其中第一条路径比较简单,第二条路径应该还没框架使用,第三条路径想象空间最大。不知道Inula未来会如何发展。


总结


当前,Inula是一款类React的框架,功能上可以类比为React并发更新之前的版本


下一步,Inula会引入响应式API,目的是提高渲染效率。


对于未来的发展,主要围绕在:



  • 探索类 Vue Composition API的可能性

  • 迭代官方核心生态库


对于华为出的这款前端框架,你怎么看?


作者:魔术师卡颂
来源:juejin.cn/post/7307451255432249354
收起阅读 »

程序员提高效率的 10 个方法

1. 早上不要开会 📅 每个人一天是 24 小时,时间是均等的,但是时间的价值却不是均等的,早上 1 小时的价值是晚上的 4 倍。为什么这么说? 因为早晨是大脑的黄金时间,经过一晚上的睡眠,大脑经过整理、记录、休息,此时的状态是最饱满的,适合专注度高的工作,比...
继续阅读 »

1. 早上不要开会 📅


每个人一天是 24 小时,时间是均等的,但是时间的价值却不是均等的,早上 1 小时的价值是晚上的 4 倍。为什么这么说?


因为早晨是大脑的黄金时间,经过一晚上的睡眠,大脑经过整理、记录、休息,此时的状态是最饱满的,适合专注度高的工作,比如编程、学习外语等,如果把时间浪费在开会、刷手机等低专注度的事情上,那么就会白白浪费早上的价值。


2. 不要使用番茄钟 🍅


有时候在专心编程的时候,会产生“心流”,心流是一种高度专注的状态,当我们专注的状态被打破的时候,需要 15 分钟的时候才能重新进入状态。


有很多人推荐番茄钟工作法,设定 25 分钟倒计时,强制休息 5 分钟,之后再进入下一个番茄钟。本人在使用实际使用这种方法的时候,经常遇到的问题就是刚刚进入“心流”的专注状态,但番茄钟却响了,打破了专注,再次进入这种专注状态需要花费 15 分钟的时间。


好的替换方法是使用秒表,它跟番茄钟一样,把时间可视化,但却是正向计时,不会打破我们的“心流”,当我们编程专注度下降的时候中去查看秒表,确定自己的休息时间。


3. 休息时间不要玩手机 📱


大脑处理视觉信息需要动用 90% 的机能,并且闪烁的屏幕也会让大脑兴奋,这就是为什么明明休息了,但是重新回到工作的时候却还是感觉很疲惫的原因。


那么对于休息时间内,我们应该阻断视觉信息的输入,推荐:



  • 闭目养神 😪

  • 听音乐 🎶

  • 在办公室走动走动 🏃‍♂️

  • 和同事聊会天 💑

  • 扭扭脖子活动活动 💁‍♂️

  • 冥想 or 正念 🧘


4. 不要在工位上吃午饭 🥣


大脑经过一早上的编程劳累运转之后,此时的专注度已经下降 40%~50%,这个时候我们需要去重启我们的专注度,一个好的方法是外出就餐,外出就餐的好处有:



  • 促进血清素分泌:我们体内有一种叫做血清素的神经递质,它控制着我们的睡眠和清醒,外出就餐可以恢复我们的血清素,让我们整个人神经气爽:

    • 日光浴:外出的时候晒太阳可以促进血清素的分泌

    • 有节奏的运动:走路是一种有节奏的运动,同样可以促进血清素分泌



  • 激发场所神经元活性:场所神经元是掌控场所、空间的神经细胞,它存在于海马体中,外出就餐时场所的变化可以激发场所神经元的活性,进而促进海马体活跃,提高我们的记忆力

  • 激活乙酰胆碱:如果外出就餐去到新的餐馆、街道,尝试新的事物的话,可以激活我们体内的乙酰胆碱,它对于我们的“创作”和“灵感”起到非常大的作用。


5. 睡午觉 😴


现在科学已经研究表现,睡午觉是非常重要的一件事情,它可以:



  • 恢复我们的身体状态:26 分钟的午睡,可以让下午的工作效率提升 34%,专注力提升 54%。

  • 延长寿命:中午不睡午觉的人比中午睡午觉的人更容易扑街

  • 预防疾病:降低老年痴呆、癌症、心血管疾病、肥胖症、糖尿病、抑郁症等


睡午觉好处多多,但也要适当,15 分钟到 30 分钟的睡眠最佳,超过的话反而有害。


6. 下午上班前运动一下 🚴


下午 2 点到 4 点是人清醒度最低的时候,10 分钟的运动可以让我们的身体重新清醒,提高专注度,程序员的工作岗位和场所如果有限,推荐:



  • 1️⃣ 深蹲

  • 2️⃣ 俯卧撑

  • 3️⃣ 胯下击掌

  • 4️⃣ 爬楼梯(不要下楼梯,下楼梯比较伤膝盖,可以向上爬到顶楼,再坐电梯下来)


7. 2 分钟解决和 30 秒决断 🖖


⚒️ 2 分钟解决是指遇到在 2 分钟内可以完成的事情,我们趁热打铁把它完成。这是一个解决拖延的小技巧,作为一个程序员,经常会遇到各种各样的突发问题,对于一些问题,我们没办法很好的决策要不要立即完成, 2 分钟解决就是一个很好的辅助决策的办法。


💣 30 秒决断是指对于日常的事情,我们只需要用 30 秒去做决策就好了,这源于一个“快棋理论”,研究人员让一个著名棋手去观察一盘棋局,然后分别给他 30 秒和 1 小时去决定下一步,最后发现 30 秒和 1 小时做出的决定中,有 90% 都是一致的。


8. 不要加班,充足睡眠 💤


作为程序员,我们可能经常加班到 9 点,到了宿舍就 10 点半,洗漱上床就 12 点了,再玩会儿手机就可以到凌晨 2、3 点。


压缩睡眠时间,大脑就得不到有效的休息,第二天的专注度就会降低,工作效率也会降低,这就是一个恶性循环。


想想我们在白天工作的时候,其实有很多时间都是被无效浪费的,如果我们给自己强制设定下班时间,创新、改变工作方式,高效率、高质量、高密度的完成工作,那是否就可以减少加班,让我们有更多的自由时间去学习新的知识技术,进而又提高我们的工作效率,形成一个正向循环。


9. 睡前 2 小时 🛌



  1. 睡前两小时不能做的事情:

    • 🍲 吃东西:空腹的时候会促进生长激素,生长激素可以提高血糖,消除疲劳,但如果吃东西把血糖提高了,这时候生长激素就停止分泌了

    • 🥃 喝酒

    • ⛹️ 剧烈运动

    • 💦 洗澡水温过高

    • 🎮 视觉娱乐(打游戏,看电影等)

    • 📺 闪亮的东西(看手机,看电脑,看电视)

    • 💡 在灯光过于明亮的地方



  2. 适合做的事情

    • 📖 读书

    • 🎶 听音乐

    • 🎨 非视觉娱乐

    • 🧘‍♂️ 使身体放松的轻微运动




10. 周末不用刻意补觉 🚫


很多人以周为单位进行休息,周一到周五压缩睡眠,周末再补觉,周六日一觉睡到下午 12 点,但这与工作日的睡眠节奏相冲突,造成的后果就是星期一的早上起床感的特别的厌倦、焦躁。


其实周末并不需要补觉,人体有一个以天为单位的生物钟,打破当前的生物钟周期,就会影响到下一个生物钟周期,要调节回来也需要花费一定时间。


我们应该要以天为单位进行休息,早睡早起,保持每天的专注度。


参考


以上大部分来源于书籍 《为什么精英都是时间控》,作者桦泽紫苑,是一个脑神经专家


作者:吴楷鹏
来源:juejin.cn/post/7253605936144285757
收起阅读 »

领导:我有个需求,你把我们项目的技术栈升级一下

故事的开始 在一个风和日丽的下午,我正喝着女神请的9.9咖啡,逛着本站摸着🐟,一切的一切都是这么的美好。 霎那间,只见耳边响起了声音,”系统觉醒中,请。。“,啊?我都外挂到账了? 呸,是”帅哥,领导叫你去开会“。 ”哦“,某位帅哥站了起来,撇了撇帅气的刘海,走...
继续阅读 »

故事的开始


在一个风和日丽的下午,我正喝着女神请的9.9咖啡,逛着本站摸着🐟,一切的一切都是这么的美好。


霎那间,只见耳边响起了声音,”系统觉醒中,请。。“,啊?我都外挂到账了?


呸,是”帅哥,领导叫你去开会“。


”哦“,某位帅哥站了起来,撇了撇帅气的刘海,走向了办公室。


胖虎00002-我真的很不错.gif


会议室情节


”咦,不是开会吗,怎么就只有领导一个人“。


昏暗的灯光,发着亮光的屏幕,在幽闭的空间里气氛显得有那么一丝的~~~暧昧,不对,是紧张。


”帅哥你来了,那我直接说事情吧“,领导说到。


突然我察觉到那么一丝不安,但是现在走好像来不及了,房门紧闭,领导又有三头六臂,凭着我这副一米八五,吴彦祖的颜值的身躯根本就逃不了。


”是这样的,上面有个新需求,我看完之后发现我们目前的项目技术包袱有点重,做起来比较麻烦,看看你做个项目技术栈升级提高的方案吧“。


听到这里我松了一口气,还好只是升级提高的方案,不是把屁股抬高的方案。


哆啦A梦00009-噢那你要试试看么.png


进入正题


分析了公司项目当前的技术栈,确实存在比较杂乱的情况,一堆的技术包袱,惨不忍睹,但还是能跑的,人和项目都能跑。



技术栈:vue2全家桶 + vuetify + 某位前辈自己开发的组件库 + 某位前辈搞的半成品bff



我用了一分钟的时间做足了思想功课,这次的升级决定采用增量升级的方案,为什么用增量升级,因为线上的项目,需求还在开发,为了稳定和尽量少的投入人力,所以采取增量升级。



这里会有很多小伙伴问,什么是增量升级啊,我会说,自己百度



又经过了一分钟的思想斗争,决定使用微前端的方案进行技术升级,框架选择阿里的qiankun



这里又会有小伙伴说,为什么用qiankun啊,我会说,下面会说



胖虎00004我大雄今天就是要刁难你胖虎.gif


微前端---qiankun


为什么使用微前端,最主要是考虑到他与技术栈无关,能够忽略掉一些历史的包袱。


为什么要用qiankun,最主要还是考虑到稳定问题,qiankun目前的社区比较大,方案也多,出了问题能找到方案,本人之前也有使用过的经验,所以这次就决定是它。



技术为业务服务,在面对技术选型的时候要考虑到现实的问题,不能一味的什么都用新的,稳是第一位



那么应该如何在老项目中使用微前端去升级,我给出了我的方案步骤



  1. 对老项目进行改造(路由,登录,菜单)

  2. 编写子应用的开发模板

  3. 逐个逐个模块进行重构


下面会和大家分析一下三个步骤的内容


66b832f0c3f84bc99896d7f5c4367021_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.webp


老项目的改造(下面大多数为代码)


第一步,我们要将老项目改造成适合被子应用随便进入的公交车。🚗


先对老项目进行分析,老项目是一个后台项目,大多数的后台项目布局都是上中左布局,头部导航栏,左边菜单栏,中间内容。那么我们只需要当用户选择的菜单属于微前端模块时,将中间内容变成微前端容器就好了。


image.png


那我们现在制作一个layout,里面的UI库我懒得换了,讲究看吧🌈


// BasicLayout.vue

<a-layout-sider collapsible>
//菜单
a-layout-sider>

<a-layout>
<a-layout-header>
//头部
a-layout-header>

<a-layout-content>
//内容
<router-view/>
<slot>slot>
a-layout-content>
a-layout>


然后对App.vue进行一定的修改


// App.vue
"zh-cn">
<component v-if="layout" :is="layout">

<div id="SubappViewportWrapper">div>

component>
// 初始化子应用时传入容器,这个容器不能后续修改,目前方案是将下面的容器动态append到SubappViewportWrapper
<div id="SubappViewport">div>


import { BasicLayout, UserLayout } from '@/layouts'
import { MICRO_APPS } from './qiankun'
import { start } from 'qiankun';
export default defineComponent({
components: {
BasicLayout,
UserLayout
},
data () {
return {
locale: zhCN,
layout: ''
}
},
methods: {
isMicroAppUrl (url) {
let result = false;
MICRO_APPS.forEach(mUrl => {
if (url.includes(mUrl)) {
result = true;
}
});
return result;
},
checkMicroApp (val) {
if (isMicroAppUrl(val.fullPath)) {
// 展示微前端容器
console.log('是微前端应用....');
document.body.classList.toggle(cName, false);
console.log(document.body.classList);
} else {
// 隐藏微前端容器
console.log('不是微前端应用');
document.body.classList.toggle(cName, true);
}
const oldLayout = this.layout;
this.layout = val.meta.layout || 'BasicLayout';
if (oldLayout !== this.layout) {
const cNode = document.getElementById('SubappViewport');
this.$nextTick(function () {
const pNode = document.getElementById('SubappViewportWrapper');
if (pNode && cNode) {
pNode.appendChild(cNode);
}
});
}
}
},
watch: {
$route (val) {
this.checkMicroApp(val);
}
},
mounted () {
start()
}
})


<style lang="less">
style>



修改目的,判断路由中是否为微前端模块,如果是的话,就插入微前端模块容器。


然后新建一个qiankun.js文件


// qiankun.js
import { registerMicroApps, initGlobalState } from 'qiankun';
export const MICRO_APPS = ['test-app']; // 子应用列表
const MICRO_APPS_DOMAIN = '//localhost:8081'; // 子应用入口域名
const MICRO_APP_ROUTE_BASE = '/test-app'; // 子应用匹配规则

const qiankun = {
install (app) {
// 加载子应用提示
const loader = loading => console.log(`加载子应用中:${loading}`);
const registerMicroAppList = [];
MICRO_APPS.forEach(item => {
registerMicroAppList.push({
name: item,
entry: `${MICRO_APPS_DOMAIN}`,
container: '#SubappViewport',
loader,
activeRule: `${MICRO_APP_ROUTE_BASE}`
});
});
// 注册微前端应用
registerMicroApps(registerMicroAppList);

// 定义全局状态
const { onGlobalStateChange, setGlobalState } = initGlobalState({
token: '', // token
});
// 监听全局变化
onGlobalStateChange((value, prev) => {
console.log(['onGlobalStateChange - master'], value);
});
}
};
export default qiankun;

这个文件我们引入了qiankun并对使用它的API进行子应用的注册,之后直接在main.js注册,


// main.js
...
import qiankun from './qiankun'
Vue.use(qiankun)
...

然后我们只需要对路由做一点点的修改就可以用了


新建RouteView.vue页面,用于接受微前端模块的内容


//RouteView.vue


修改路由配置


//router.js
const routes = [
{
path: '/home',
name: 'Home',
meta: {},
component: () => import('@/views/Home.vue')
},
// 当是属于微前端模块的路由, 使用RouteView组件
{
path: '/test-app',
name: 'test-app',
meta: {},
component: () => import('@/RouteView.vue')
}
]

最后就新增一个名为test-app的子应用就可以了。


关于子应用的内容,这次先不说了,码字码累了,下次再说吧。


哆啦A梦00007-嘛你慢慢享受吧.png


下集预告(子应用模板编写)


你们肯定会说,不要脸,还搞下集预告。


哼,我只能说,今天周五,准备下班,不码了。🌈


胖虎00014-怎么了-我胖虎说的有毛病吗.gif


作者:小酒星小杜
来源:juejin.cn/post/7307469610423664655
收起阅读 »

Android启动优化实践 - 秒开率从17%提升至75%

一、前言 启动性能是App使用体验的门面,耗时过长会降低用户使用兴趣。对于开发者来说,通过各种技术手段来提升启动性能缩减启动时长,对整站业务的各项指标提升都会有较大帮助。因此,秒开率优化也成为了各个客户端团队在体验优化方向上十分重要的一环。 本文将会结合我自己...
继续阅读 »

一、前言


启动性能是App使用体验的门面,耗时过长会降低用户使用兴趣。对于开发者来说,通过各种技术手段来提升启动性能缩减启动时长,对整站业务的各项指标提升都会有较大帮助。因此,秒开率优化也成为了各个客户端团队在体验优化方向上十分重要的一环。


本文将会结合我自己在项目中优化启动速度的经验,跟大家分享下,我眼里的科学的启动速度优化思路。


在我的眼里,科学的优化策略是通用的,不管是针对什么性能指标不管是针对什么课题,思路是差不多的。比如这期的分享是启动优化,其实跟上期分享的 如何科学的进行Android包体积优化 - 掘金 (juejin.cn) 是类似的,在那篇分享里我总结了基本思想:




  1. 抓各个环节

  2. 系统化方案先行,再来实操

  3. 明确风险收益比及成本收益比

  4. 明确指标以及形成一套监控防劣化体系

  5. 把包体积这个指标刻在脑子里



那放在启动优化里,基本思想长啥样呢?其实是差不多的,我认为唯一的差别就是启动优化会包含着许多前期调研和计划过程考虑不到或者甚至无法提前考虑的问题点和可优化点。




  1. 抓各个环节

  2. 系统化方案先行,再来实操

  3. 从各个角度死磕各个角落,寻找可优化点

  4. 明确风险收益比及成本收益比

  5. 明确指标以及形成一套监控防劣化体系

  6. 把秒开率这个指标刻在脑子里



我们调研了市面上几乎所有的优化方案,并结合项目现有情况,针对启动阶段各方面、各阶段进行针对性的优化,加上后期不断的调优,将Android客户端的主版本秒开率由 17% 优化到了 75+%,90 分位 App的启动耗时从 2800ms 优化到 1500 ms,大幅提升app的启动体验。此外,我们还建立了一系列的线上监控、防劣化体系,以确保优化效果可持续。


二、评估优化需求


在开展工作前,我们首先得先有两点判断:



  1. 是否需要进行优化

  2. 优化到多少算符合预期


那具体应该如何进行判断呢?有个比较简单的方法就是对标,找到对标对象进行对标。


对于小厂来说,一般对标对象有:



  1. 竞品App

  2. 业内人气App


基于对标对象,我们可以粗略的有如下判断:



  1. 如果我们App跟竞品App启动速度差不多甚至略高,那就有必要进行启动速度优化。当然,在许多厂不缺人力和资源的时候,并不需要这一步判断过程哈

  2. 优化到业内人气App中启动速度最快那一档即可


上述判断还是基于用户视角,从用户使用App的意愿去出发,那从业务或者技术的视角出发的话,肯定是越快越好,不废话了,下面直接进入主题。


三、秒开率定义 / 启动速度指标定义


既然要讨论秒开率,必须得明确以下问题:



  1. 用户启动app的耗时如何定义?

  2. 多长时间内app启动完成才算秒开?


3.1 用户启动app的耗时如何定义?



我们这里的定义是:进程启动 -> 用户看见Feed流卡片/内容



业内常见的app启动过程阶段一般分为「启动阶段」和「首刷阶段」。



  • 启动阶段:指用户点击icon到见到app的首页

  • 首刷阶段:指用户见到app的首页到首页列表内容展现



很多厂在谈论启动优化时,只涉及到第一步。对用户而言,他并不知道所谓的启动阶段和首刷阶段都是些什么东西,他只知道 看没看到预期的内容和能不能开始消费内容 ,既然要讨论秒开率,必然是用户期待的内容秒开,所以仅以「启动阶段」作为秒开率的指标依据并不能代表用户的真实用户体验。假如用户1s就看到首页了,但是feed流过了10s才有内容的展现,这显然不能算作秒开,这也不能代表用户的体验就是好的。当然,以 启动阶段 + 首刷阶段 认定为需要秒开的时间段,挑战也自然而然增大许多,环节越多,不可控和不可抗因素就越多。


3.2多长时间内app启动完成才算秒开?



1秒



既然谈论的目标是秒开率,那必然是需要 1s 内完成用户的启动流程。


四、认识自家App的启动流程


为什么要认识自家App的启动过程?



知彼知己者,百战不殆。 --《孙子·谋攻》



对启动过程作业务层面、系统层面的全路径分析,有利于我们发现全路径上各个阶段的耗时点及潜在耗时点。


4.1 从业务角度看启动过程


为什么要从业务角度看启动过程?这是因为我们既然要优化的是秒开率,而我们的秒开率指标又是与业务强相关的指标,所以我们必须从业务的角度出发,找到对启动流程影响最大的以及会block启动流程的业务,以他们为切入点尝试寻求启动速度更优的解法。


启动过程最大的不确定性因素来自于网络请求,如果App启动过程中,需要等待网络请求完成才能进入下一阶段,当出现弱网、慢网等情况时,启动时长就无法预估了,我们从下图中可以看到两处网络依赖:开屏广告请求、Feed列表请求。其他的初始化任务都不依赖网络,自然而然的执行时长在同一机器、同一环境是比较稳定的,也是可观测的。


根据业务流程,我们想要优化启动速度,需要进行如下考虑:



  • 开屏广告接口尽量早的发出请求

  • 等待开屏接口过程中,尽量完成更多的对启动流程有 block 的启动任务

  • feed列表的第一屏数据尽量走缓存


(下图画的有点粗略,意会就行)



4.2 从系统角度看启动过程


从系统角度来看自家App的启动路径,与大多数App是类似的。整体分为 Application 阶段、Activity阶段、RN阶段(业务页面阶段)。



4.2.1 Application阶段


在Application阶段中,有两个子阶段需要我们重点关注:



  1. Install provider,现在许多的三方库为了追求使用的便利性以及能够轻松的获取到Application上下文,会选择通过注册ContentProvider来实现库的初始化,然而正是由于过于便利,导致我们在排查启动过程的三方库初始化情况时,容易忽略掉这些隐式初始化的三方库。

  2. Application#onCreate,一般来说,项目本身模块的初始化、各种三方库初始化、业务前置环境初始化都会在 Application#onCreate 这个生命周期里干,往往这个生命周期里的任务是非常臃肿的,我们优化Application流程的大部分目光也集中在这里,也是我们通过异步、按需、预加载等各种手段做优化的主要时机。


4.2.2 Activity阶段


Activity阶段的起点来自于 ActivityThread#performLaunchActivity 的调用,在 performLaunchActivity 方法中,将会创建Activity的上下文,并且反射创建Activity实例,如果是App的冷启动(即 Application 并未创建),则会先创建Application并调用Application的onCreate方法,再初始化Activity,创建Window对象(PhoneWindow)并实现Activity和Window相关联,最终调用到Activity的onCreate生命周期方法。


在启动优化的专项中,Activity阶段最关键的生命周期是 Activity#onCreate,这个阶段中包含了大量的 UI 构建、首页相关业务初始化等耗时任务,是我们在优化启动过程中非常重要的一环,我们可以通过异步、预加载、延迟执行等手段做各方面的优化。


4.2.3 RN阶段(首页页面业务阶段)


在我们App中,有着非常大量的 react native(RN) 技术栈的使用,面首页也是采用 RN 开发,在目前客户端团队配置下,对 RN 这个大项目的源码进行优化的空间是比较小的,考虑到成本收益比,本文几乎不会涉及对 RN 架构的直接正向优化,尽管 RN 的渲染性能可能是启动流程中非常大的瓶颈点。


五、衡量App启动时长


5.1 线上大盘观测


为了量化指标、找出线上用户瓶颈以及衡量后续的优化效果,我们对线上用户的启动时长进行了埋点统计,用来观测用户从点击icon到看到feed流卡片的总时长以及各阶段的时长。


通过细化、量化的指标监控,能够很好的观测线上用户启动耗时大盘的版本变化以及各个阶段的分位数版本变化,同时我们也需要依赖线上的性能监控统计来判断我们在某个版本上上线的某个优化是否有效,是否能真实反映在大盘指标以及用户体验上,因为本地用测试机器去跑平均启动时间,受限于运行环境的不稳定,总是会有数据波动的。当进行了某项优化之后,能通过本地测试大概跑出来是否有正向优化、优化了大概多少毫秒,但是具体反映到秒开率指标上,只能依赖大盘,本地无法做上线前的优化预估。


启动流程终点选取



终点不需要完全准确,尽量准就足够了



大多数的 App 在选择冷启动启动流程终点时,会选择首页 Activity 的 onWindowFocusChanged 时机,此时首页 Activity 已经可见但其内部的 View 还不可见,对于用户侧已经可以看见首页背景,同时会将首页内 View 绘制归入首刷过程中。


但是我们期望的终点是用户看到 Feed 流卡片的时刻,上面也说了,我们优化目标就是 「启动阶段」 + 「首刷阶段」,由于首页里的feed tab是RN 开发的,我们无法轻易的去精准到卡片的绘制阶段,于是我们将终点的选取选在了「ReactScrollView 的子 View onViewAttachedToWindow 回调时」,指RN feed流卡片View ready并且添加到了 window 上,可以大致认为卡片用户可见。


启动性能指标校准


由于启动路径十分复杂,在添加了相应的埋点之后还需要进行额外的校准,确保启动性能面板能正确反映用户正常打开app看到首页的完整不间断流程的性能。因此,我们对于许多的边缘case进行了剔除:



  • 进程后台被唤起,这种情况进程在后台早早的被唤起,三方sdk以及一些其他模块也早早的初始化完成,只是MainActivity没有被启动,这种case下,我们通过进程启动时,读取进程当前的 importance 进行过滤。

  • 启动过程中App退后台

  • 用户未登录场景

  • 特殊场景下的开屏广告,比如有复杂的联动动效

  • push、deeplink拉起

  • 点开app第一个页面非首页,这种场景常见的就是 push、deeplink,但是还会有一些其他的站内逻辑进其他tab或者其他二级页面,所以这里统一做一个过滤。


启动性能大盘建设


首先要明确我们建设的性能大盘是以什么形式从什么角度监控什么指标,在启动速度这种场景下,分位数指标更加适合去进行全面监控,因为其波动较小,不会被极端的case影响曲线,所以我们在进行启动性能的优化时,最关注的就是分位数的性能。所以我们的整体监控面板分为:



  • 秒开率、2秒开率、3秒开率、5秒以上打开率

  • 90分位总体性能、90分位各阶段性能。

  • 分版本各阶段各项指标、整体各阶段各项指标、主版本各阶段各项指标。

  • 分场景,如有无广告等等

  • ...


5.2 Method Trace


除了线上对性能指标进行监控,在开发环境下我们想要去优化启动时长,必须得有方法知道瓶颈在哪儿,是哪个方法太耗时,还是哪些逻辑不合理,哪些能优化,哪些没法优化。Method Trace就是其中手段之一,我们通过 Method Trace能看到每个线程的运行情况,每个方法、方法栈耗时情况如何。


Profiler


看method trace有两种方式,一种是直接启动 Android Studio 自带的Profiler工具,attach上进程之后就可以选择 “sample java methods” 来进行 cpu profiling了,但是这种方式不支持release包使用,只能在debug包上面使用,且性能损耗大,会让问题的分析产生误差


Debug.startMethodTracingSamping


我们也可以通过代码去抓取 method trace:


Application#onCreate:
File file = new File(FileUtils.getCacheDir(application), "trace_" + System.currentTimeMillis());
Debug.startMethodTracing(file.getAbsolutePath(), 200000000);
Debug.startMethodTracingSamping(file.getAbsolutePath(), 200000000, 100);

StartupFlow#afterStartup:
Debug.stopMethodTracing();

在开启 MethodTracing 时,更加推荐使用 startMethodTracingSamping,这样性能损耗比调用startMethodTracing进行的完全的 Method Tracing低非常非常多, 这样抓到的性能窗口误差也小很多。而且抓到的 trace 文件也小很多,用Android Studio直接打开的话,不会有啥问题,当文件足够大时,用Android Studio打开可能会失败或者直接卡死。



5.3 System Trace


Perfetto


Perfetto 是 Android 10 中引入的全新平台级跟踪工具。这是适用于 Android、Linux 和 Chrome 的更加通用和复杂的开源跟踪项目。与 Systrace 不同,它提供数据源超集,可让你以 protobuf 编码的二进制流形式记录任意长度的跟踪记录。你可以在 Perfetto 界面中打开这些跟踪记录,可以理解成如果开发机器是 Android 10 以下,就用 Systrace,如果是 Android 10 及以上,就用 Perfetto,但是 Perfetto跟Systrace一样,抓取到的报告是不包含 App 进程的代码执行情况的。文章后续也会给用Perfetto找到待优化点的案例。


六、优化实践


经过上面的理论分析、现状分析以及大盘指标的建立之后,其实大致对哪些需要优化以及大致如何排查、如何优化会有一个整体认知。在小厂里,由于开发资源有限,所以实际上在优化实践阶段对能进行的但是进行人力成本比较高的优化项会做一轮筛查,我们通过调研市面公开的资料、技术博客了解各大场以及各个博主分享的启动优化思路和方案,再结合自身场景做出取舍,在做取舍的过程中,衡量一个任务是否要启动有两个关键指标:“投入产出比”、“产出风险比”



  1. 投入产出比:很容易理解,当优化一个点需要 3人/天,能收获90分位 100ms 的优化产出,另一个点需要 3人/天,但只能收获90分位 10ms 的优化产出,谁先谁后、谁要做谁不做其实显而易见,因为优化的前期追求的一个很重要的点必然是优化收益。等到后续开启二期任务、三期任务需要做到更加极致时,才会考虑成本高但收益低的任务。

  2. 产出风险比:在我们做启动优化过程中,必然会有一些方案有产出但是可能会有风险点的,可能是影响某个业务,也可能影响所有业务,我们需要在启动优化的过程中,不断的衡量一个优化任务是否有风险,风险影响范围如何,风险影响大小如何,最后整体衡量甚至跟业务方进行商讨对他们造成的劣化是否能够接受,最后才能敲定相关的任务是否要排上日程。


所以大致的思路可以总结为:



  1. 前期低成本低风险快速降低大盘启动耗时

  2. 后期高成本突破各个瓶颈

  3. 全期加强监控,做好防劣化


下面我们就将会按照文章一开始提过的启动流程顺序来分享在启动加速项目中的一些案例。


6.1 Application流程


6.1.1 启动任务删减与重排



这里我多提两嘴。我个人觉得在启动优化中,删减和重排启动任务是最为复杂的,特别是对于中大型App,业务过于多,甚至过于杂乱。但是在小厂中,完全可以冲着 删除和延后所有首页无关业务、模块、SDK代码 的目标去,前提是能理清所有业务的表面和隐藏逻辑,这里需要对整个App启动阶段的业务代码和业务流程全盘掌控。




你也许可以通过奇技淫巧让启动过程中业务B反序列化时间无限变短,而我可以从启动过程中删掉业务B逻辑



在App的启动流程中,有非常多的启动任务全部在Application的onCreate里被执行,有主线程的有非主线程的,但是不可避免的是,二者都会对启动的性能带来损耗。所以我们需要做的第一件重要的事情就是 减少启动任务。
我们通过逐个排查启动任务,同时将他们分为几类:



  • 刚需任务:不可延迟,必须第一时间最高优先级执行完成,比如网络库、存储库等基础库的初始化。如果不在启动阶段初始化完成,根本无法进入到后续流程。

  • 非刚需高优任务:这类任务的特征就是高优,但是并非刚需,并不是说不初始化完成后续首页就没法进没法用,比如拉取ab实验配置、ip直连、push、长链接相关非刚需基础建设项,这类可以高优在启动阶段执行,但是没必要放在 UI 线程 block 执行,就可以放到启动阶段的后台工作线程中去跑。

  • 非刚需低优任务:这类任务常见的特征就是对业务能否运作无决定性影响或者业务本身流程靠后,完全可以放在我们认为的启动阶段结束之后再后台执行,比如 x5内核初始化、在线客服sdk预初始化 之类的。

  • 可删除任务:这类任务完全不需要存在于启动流程,可能是任务本身无意义,也可能是任务本身可以懒加载,即在用到的时候再初始化都不迟。


将任务分类之后,我们就能大概知道如何去进行优化。



  • 拉高刚需任务优先级

  • 非刚需高优 异步化

  • 非刚需低优任务 异步化+延迟化

  • 可删除任务 删除


6.1.2 任务排布框架


为了更加方便的对启动任务进行排布,我们自己实现了一套用于启动过程的任务排布框架TaskManager。TaskManager具有以下几个特性:



  1. 支持优先级

  2. 支持依赖关系

  3. 提供超时、失败机制以供 fallback

  4. 支持在关键时间阶段运行任务,如MainActivity某个生命周期、启动流程结束后


大致使用方式为:


TaskManager.getInstance().beginWith(A)
.then(B)
.then(C, D)
.then(E)
.enqueue();

TaskManager.getInstance().runAfterStartup({ xxx; })

通过任务的大致非精细化的排布,我们不仅仅可以对启动任务能够很好的进行监控,还可以更加容易的找出不合理项。


6.1.3 实现runAfterStartup机制 + idleHandler



这玩意儿十分重要,我通过昏天黑地的梳理业务,将启动流程中原先可能超过一半的代码任务非常方便的放到了启动流程之后



我们通过提供 runAfterStartup 的API,用于更加容易的支持各种场景、各种业务把自己的启动过程任务或者非启动过程任务放在启动流程结束之后运行,这也有助于我们自己在优化的过程中,更加轻松的将上面的非刚需低优任务进行排布。


runAfterStartup的那些任务,应该在什么时候去执行呢?
这里我们认定的启动流程结束是有几个关键点的:



  1. 首页tab的feed流渲染完成

  2. 首页tab加载、渲染失败

  3. 用户进入了二级页面

  4. 用户退后台

  5. 用户在首页有 tab 切换操作


通过TaskManager的使用以及我们对各业务的逐一排查分析,我们将原先在启动阶段一股脑无脑运行的任务进行了拆解和细化,该延后的延后,该异步的异步,该按需的按需。


6.2 Activity流程


接下来将分享一下 Activity 阶段的一些相关优化的典型案例。


6.2.1 SplashActivity与MainActivity合并


原先的 launcher activity 是SplashActivity,主要承载广告逻辑,当App启动时,先进入SplashActivity,死等广告接口判断是否有开屏广告,如果有则展示,没有则跳转进MainActivity,这里流程的不合理性影响最大的点是:SplashActivity在死等开屏接口时,根本无法对业务本身做一些预加载或者并发加载,首页的业务都在MainActivity里面,同时启动阶段需要连续启动两个Activity,至少带来 百毫秒 级别的劣化。



当然,将SplashActivity承接的所有内容转移到MainActivity上,有哪几个挑战又该如何解决?


1. MainActivity 作为launch activity之后的单实例问题



  • MainAcitvity 的 launch mode 需要设置为 singleTop,否则会出现 App从后台进前台,非MainActivity走生命周期的现象

  • 同时,作为首页,需要满足另一个条件就是跳转到首页之后,其他二级页面需要全部都关闭掉,站内跳转到 MainActivity 则附带 CLEAR_TOP | NEW_TASK 的标记


2. 广告以什么形式展现



  • 广告原先是以Activity的形式在展现,当 launcher 换掉之后,广告被抽离成 View 的形式去承载逻辑,在 MainActivity#onCreate 中,通过将广告View添加进 DecorView中,完成对首页布局的遮罩,这种方式还有一个好处就是在广告View展示白屏logo图时,底下首页框架是可以进行预加载的。

  • 这里其实还需要实现以下配套设施:

    • 首页弹出层管理器,管理弹窗、页面跳转等各种可能弹出的东西,在广告View覆盖在上面时,先暂停弹出层管理器的生命周期,避免出现其他内容盖住广告




6.2.2 异步预加载布局



使用异步加载布局时,可以对AsyncLayoutInflater小小改造下,如单线程变线程池,调高线程优先级等,提升预加载使用率



在Android中,其实有封装较好的 AsyncLayoutInflater 用于进行布局的异步加载,我们在App的启动阶段启动异步加载View的任务,同时调高工作线程优先级以尽量在使用View之前就 inflate 结束,这样在首页上要使用该布局时,就可以直接从内存中读取。


异步加载布局容易出问题的点有:



  1. Context的替换



    • 使用MutableContextWrapper,在使用时替换为对应的 Activity 即可



  2. theme问题



    • 当异步创建 TabLayout 等Matrials组件时,由于Application的主题并没有所谓的 AppCompat 主题,会抛出异常 You need to use a Theme.AppCompat theme 。这时需要在 xml 中加上 android:theme="@style/AppCompatTheme" 即可。




但是异步预加载布局有一个点是非常关键的:使用前一定要确认,异步创建的这个布局大部分时候或者绝大部分时候,都能在使用前创建好,不然的话不仅没有优化的效果,还会增加启动阶段的任务从而对启动性能带来一定程度上的劣化。


6.2.3 多Tab懒加载


我们App的首页结构非常复杂,一共有着三层的tab切换器。底部tab负责切换大tab,首页的tab还有着两层tab用于切换二三级子页面。原先由于代码设计使然,首页Tab的其他所有子页面都会在 App 的启动阶段被加载,只是说不进行 RN 页面的渲染,这其实会占据 UI 线程非常多的时间。


我们做启动优化的过程中,将一二三级tab全部懒加载,同时由于 我们App存在首页其他 Tab 的预加载技术建设,目的是为了实现当用户切到其他tab时,完全去除加载过程,因此我们也将预加载的逻辑延迟到了启动流程之后,即:


StartupFlow.runAfterStartup{ preload() }

6.2.4 懒加载布局


Tab懒加载其实也算是布局懒加载的一部分,但又不全是,所以给拆开了。这部分讲的布局懒加载是指:



  • 启动过程不一定会需要用上的布局,可以完全在需要时被加载,比如:

    • 广告View,完全可以在后端广告接口返回可用的广告数据结构且经过了素材校验等流程确定要开始渲染广告时,进行布局的加载。

    • 首页上其他非全量的布局,比如其他广告位、首页上并不一定会出现的搜索框、banner 组件等。这些组件的特性是根据不同的配置来决定是否展示,跟广告类似。




我们用上的布局懒加载的手段分几种:



  • ViewStub,在开屏广告的布局中非常常见,因为广告有多种类型,如视频、图片、其他类型广告等,每次的开屏又是确定的只有一种,因此就可以将不同类型的广告容器布局用 ViewStub 来处理

  • Kotlin by lazy,这种就是适用于 布局是动态加载的场景,假如上面描述的开屏广告的各种不同类型素材的布局都是在代码中动态的 inflate 并且 add 到根View上的话,其实也可以通过 by lazy 的方式来实现。所以其实很多时候 by lazy 用起来会更加方便,比如下图,对 binding 使用 by lazy ,这样只有在真正要使用 binding 时,才会去 进行 inflate。
    image.png


6.2.5 xml2Code


用开源项目可以,自己实现也可以,当然,搭配异步加载更可以。


6.2.6 减少布局层级、优化过度绘制


这个就需要自己通过LayoutInspector和Android的调试工具去各自分析了,如果打开LayoutInspector肉眼可见都是红色,赶紧改。。


6.3 RN流程



这里也顺带吐槽下吧,用 RN 写首页 feed 流的app真的不多哈,一般来说随着公司的发展,往往会越来越注重关键页面的性能,我们项目是我见过的为数不多的进 App 第一个页面还是RN的。如果首页不是RN,上面提到的秒开率指标、启动耗时应该会更加好一些才对。



image.png


先直面 RN 页面作为首页的加载流程,在页面进行渲染前,会比 native 页面多几个前置任务:



  1. RN 框架初始化(load各种so,初始化各种东西),RN init

  2. RN bundle 文件准备,find RN bundle

  3. RN bundle 文件加载,load RN bundle

  4. 开启RN渲染流程,start react application


又因为 RN 的 js 和 native 层进行通信,又有引擎开销和线程调度开销,所以我个人认为 RN 是不足以胜任主流 app 的搭载首页业务的任务的


吐槽归吐槽,能尽量优化点是一点:


6.3.1 无限提前 RN init


如题,由于后续所有的RN流程都依赖RN环境的初始化,所以必须将这坨初始化调用能放多前面,就放多前面,当然,该子线程的还是子线程哈,不可能主线程去初始化这东西。


6.3.2 前置 RN bundle 的完整性校验


当我们在使用 RN 的 bundle 文件时,往往需要校验一下 md5,看看从CDN上下载或者更新的 bundle 文件是否完整,但是启动流程如果进行 bundle 文件的 md5 校验,往往是一个比较2的举动,因此我们通过调整下载流程和加载流程,让下载/更新流程进行完整性校验的保护,确保要加载的所有 bundle 都是完整的可用的就行了,就不需要在启动流程校验bundle完整性了。


6.3.3 page cache


给首页的feed流增加页面级别缓存,让首页首刷不依赖接口返回


6.3.4 三方库的初始化优化


部分的三方 RN package,会在RN初始化的时候,同步的去初始化一堆耗时玩意或者执行耗时逻辑,可以通过修改三方库代码,让他们变成异步执行,不要拖慢整体的RN流程。


6.4 其他优化


6.4.1 Webview非预期初始化


在我们使用 Perfetto 进行性能观测时,在 UI 线程发现了一段 几十毫秒接近百毫秒 的非预期Webview初始化的耗时(机器环境:小米10 pro),在线上用户机器上这段代码执行时间可能会更长。为什么说非预期呢:



  • 首页没有WebView的使用、预加载

  • X5内核的初始化也在启动流程之后


1701274423005.png


我们从perfetto的时序图中可以看到,堆栈的调用入口是WebViewChromiumAwInit.startChromiumLocked,由于 Perfetto 并看不到 App 相关的堆栈信息,所以我们无法直接知道到底是哪行代码引起的。这里感谢一下 抖音团队 分享的启动优化案例解析中的案例,了解到 WebViewFactory 实现的 WebViewFactoryProvider 接口的 getStatics、 getGeolocationPermission、createWebView 等多个方法的首次调用都会触发 WebViewChromiumAwInit#ensureChromiumStartedLocked ,随之往主线程 post 一个 runnable,这个runnable的任务体就是 startChromiumLocked 函数的调用。


所以只要我们知道谁在调用 WebViewFactoryProvider 的接口方法,就能知道调用栈来自于哪儿。于是乎我们开始对 WebViewFactoryProvider 进行动态代理,用代理对象去替换掉 WebViewFactory 内部的 sProviderInstance。同时通过断点、打堆栈的形式来查找调用源头。


    ##WebViewFactory
@SystemApi
public final class WebViewFactory{
//...
@UnsupportedAppUsage
private static WebViewFactoryProvider sProviderInstance;
//...
}


##动态代理
try {
Class clas = Class.forName("android.webkit.WebViewFactory");
Method method = clas.getDeclaredMethod("getProvider");
method.setAccessible(true);
Object obj = method.invoke(null);

Object hookService = Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getSuperclass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.d("zttt", "hookService method: " + method.getName());
new RuntimeException(method.getName()).printStackTrace();
return method.invoke(obj, args);
}
});

Field field = clas.getDeclaredField("sProviderInstance");
field.setAccessible(true);
field.set(null, hookService);
} catch (Exception e) {
e.printStackTrace();
}

替换掉 sProviderInstance 之后,我们就可以在我们的代理逻辑中,加上断点来进行调试,最终找到了造成 WebView非预期初始化的始作俑者:WebSettings.getDefaultUserAgent



事情到这里就好解决了,只需要对 WebSettings.getDefaultUserAgent 进行编译期的Hook重定向到带缓存defaultUserAgent 的相关方法就行了,本地有缓存则直接读取,本地没有则立即读取,得益于我们项目中使用方便的 配置化 Hook 框架,这种小打小闹的 Hook 工作不到一分钟就能完成。



参考:


基于 Booster ASM API的配置化 hook 方案封装 - 掘金 (juejin.cn)


AndroidSimpleHook-github





当然,这里还需要考虑一个问题,那就是当用户机器的 defaultUserAgent 发生变化之后,怎么才能及时的更新本地缓存以及网络请求中用上新的defaultUserAgent。我们的做法是:



  • 当本地没有缓存时,立刻调用 WebSettings.getDefaultUserAgent 拿值并更新缓存;

  • 每次App启动阶段结束之后,会在子线程中去调用WebSettings.getDefaultUserAgent 拿值并更新缓存。


这样处理之后,将 defaultUserAgent 发生变化之后的影响最小化,系统 WebView 升级本身就是极度不频繁的事情,在这种 case 下我们舍弃了下一次 App 打开前几个网络请求的 defaultUserAgent 正确性也是合理的,这也是我们考量 「风险收益比」的一个经典case。


6.4.2 启动阶段 IO 优化



更加高级的做法是进行请求合并,当我们将50+个网络请求优化到十来个的时候,如果再对一些实时性不是极高的接口进行合并,会更加优雅。不过小厂没资源做。



前面说的都是优化 UI 线程相关的耗时,实际上在启动阶段,不仅仅 UI 线程执行太多耗时任务会影响启动速度,工作线程执行太多的耗时任务也会影响到启动速度,特别是重IO的任务。


在我们App的启动流程里,首页完成渲染前需要发送50+个网络请求,包含:



  • 业务预拉取数据

  • 业务拉取各种配置、开关、实验变量

  • 多次的badge请求

  • 多次的 IM 拉消息的请求

  • 各种首页阶段是否展示引导的请求,如push引导权限

  • 其他莫名奇妙的请求,可能业务都废弃了,还在发送请求


在优化这一项时,还是秉承着前面所说的得谨慎的考虑 “收益风险比”,毕竟我们不是对所有业务都非常了解,且公司现有研发也不是对所有业务都非常了解。通过一波深入的调研和调整之后,我们将 App 首页渲染完成前的网络请求数量,控制在 10 个左右,大大的减少了启动阶段的网络请求量


6.4.3 大对象反序列化优化


我们App中,对象的反序列化、序列化用的是谷歌亲儿子 - Gson。Gson 是 Google 推出的一个 json 解析库,其具有接入成本低、使用便捷、功能扩展性良好等优点,但是其也有一个比较明显的弱点,那就是对于它在进行某个 Model 的首次解析时会比较耗时,并且随着 Model 复杂程度的增加,其耗时会不断膨胀。
而我们在启动过程中,需要用 Gson 反序列化非常多的数据,特别是某些大对象,如:



  • Global config:顾名思义,是一个全局配置,里面包含着各个业务的配置信息,是非常大的

  • user info:这是用户信息的描述对象,里面包含着非常多的用户属性、标签,在App 启动过程中如果主线程去初始化,往往需要几十甚至上百毫秒。


针对这种启动阶段必须进行复杂对象序列化,我们进行了:



  • 用Gson解的,自定义 TypeAdapter,属于正面优化反序列化操作本身(市面上有现成的一些通过添加注解自动生成TypeAdapter的框架,通过一个注解就能够很轻松的给对应的类生成 TypeAdapter并注册到 Gson 中)

  • 又大又乱又不好改的直接读磁盘然后 JSON 解析的大对象(没想到还有这种的吧),提前子线程预加载,避免在 UI 线程反序列化,能解决部分问题,并非完全的解法


6.4.4 广告流程优化


其实聊到启动优化,必然会涉及的肯定是 开屏广告 的业务流程。首先要搞清楚一个问题。


启动优化优化的是什么?
启动优化优化的是用户体验,优化的是用户看到内容的速度,那么开屏的内容怎么就不算呢?所以实际上加速用户看到开屏也能一定程度上让用户体感更加的好。而且由于我们进首页的时间依赖于广告流程结束,即需要等待广告流程结束,我们App才会跳过logo的全屏等待页面进入首页,那么优化广告流程耗时实际上也是在优化进入首页的速度,即用户可以更加快速的看到首页框架。



原先的广告流程如上图,业务流程本身可能没什么问题,问题出在了两次的往主线程post runnable来控制流程前进。已知 App 启动流程是非常繁忙的,当一个 runnable 被post到 UI 线程的队列中之后不会立即执行,可能需要等上百甚至几百毫秒,而且由于启动过程中有着许多的耗时大的 runnable 在被执行,就算 mainHandler.postAtFrontOfQueue 都无济于事。



因此我们对广告流程做了调整,去掉了其中一次的消息抛回主线程执行的逻辑,加快了广告业务的流程执行速度,同时,受益于我们前面说的 View 的异步预加载、懒加载等手段,广告流程的执行速度被全面优化。


6.4.5 GC 抑制



实现的花可以参考大佬博客: 速度优化:GC抑制 - 掘金 (juejin.cn)


大家如果只是想本地测试下 GC 抑制在自己项目里的效果,反编译某些大厂App,从里面把相关 so 文件捞出来,copy 下JNI声明,放自己项目里测试用就行。



自动垃圾回收的Java特性相对于C语言来说,确实是能够让开发人员在开发时提高效率,不需要去考虑手动分配内存和分配的内存什么时候去手动回收,但是对于Java程序来说,垃圾回收机制真实让人又爱又恨,不过如果开发人员仅在乎业务而不在乎性能的话,确实是不会垃圾回收恨起来。这里需要明确一点,垃圾回收是有代价的,会占 CPU 资源甚至导致我们线程被挂起。


App在启动过程中,由于任务众多而且涉及到的sdk、模块也众多,非常容易在启动阶段发生一次或者多次的GC,这往往会带来比较多的性能损耗。


针对这种启动容易触发 GC 的场景,我们有两种方式去减少 GC 次数以及降低 GC 发生的可能。



  • 通过 profiler 去排查启动过程中的对象创建、分配,找出分配过于频繁明显不正常的case

  • 影响 ART 虚拟机的默认垃圾回收行为和机制,进行 GC 抑制,这里由于我们 App 的 minSdk 是 Api 24,所以仅涉及 ART 虚拟机上的 GC 抑制。


不过鉴于以下几点,我们最终并没有自己去实现和上线无稳定性、兼容性隐患的GC抑制能力:



  1. Android 10 及以上,官方实际上已经在 framework 中添加了 App 启动提高 GC 阈值的逻辑。cs.android.com/android/_/a…

  2. 由于我们在启动任务重排和删减用例很大,线上对 启动阶段的 GC 次数进行了统计,发现 80 分位的用户 GC 次数为0。也就是说启动优化上线之后线上至少 80% 的用户在启动阶段都不会发生 GC。监听 GC 发生可以简单的用 WeakReference 来包装重写了 finalize 方法的自定义对象。

  3. 不满足对于收益成本比、风险收益比的要求


6.4.6 高频方法



排查高频方法可以通过 method trace + 插桩记录函数调用来做



比如在我们App的场景中,日志库、网络库上拼接公共参数时,会反复调用许多耗时且无缓存的方法去获取参数值,其中包括:



  • Binder调用,如 push enable 等

  • 每次都需要反序列化的取值,如 反序列化 global config,从中取得一个配置值。

  • ...


针对这几类问题,我们做了如下优化:



  • 能加缓存加缓存

  • 拼接公参异步化


6.4.7 binder 调用优化



想要优化启动过程中的binder调用,必须得先知道有哪些binder调用,不考虑来自Framework代码的调用的话,可以通过hook工具来先检查一下。同时打印下调用耗时摸个底
基于 Booster ASM API的配置化 hook 方案封装 - 掘金 (juejin.cn)



binder是android提供的IPC的方式。android许多系统服务都是运行在system_server进程而非app进程,比如判断网络,获取电量,加密等,当通过binder调用去调用到相关的api之后,app线程会挂起等待binder驱动返回数据,因此IPC 调用是比较耗时的,而且可能会出现比预期之内的耗时更加耗时的现象。


针对binder调用的耗时现象,主要做了:



  1. 对反复调用的 binder 调用结果进行缓存,合适的时机进行更新

  2. 通过 hook 框架统一收拢这些调用进缓存逻辑


比如 push enable,这种总不能启动过程变来变去吧,再比如网络状态,也不能启动过程变来变去吧。


当然,上面举的例子,也完全可以用于App全局场景,比如通知权限状态,完全可以app进前台检查下就行,比如网络状态,监听网络状态变化去更新就行。


七、验收优化效果



再次强调一下,我们统计的 App 启动耗时是「启动阶段」+ 「首刷阶段」



7.1 App 启动耗时分位数


90 分位 App的启动耗时从 2800 左右 下降到 1500 左右。降幅47%



7.2 主版本秒开率



从图中也能看到,整体稳定,但部分天波动较大,是因为开屏广告接入了程序化平台,接口时长、素材大小等都不是很好控制,尽管后端已经限制了请求程序化的超时时长,但是迫于无奈,无法将程序化平台接口请求超时时长设定在一个我满意的情况下,毕竟是收入部门。



Android 主版本秒开率由原先的约 17% 提升到 76%



7.3 两秒打开率


Android 主版本两秒打开率由原先的 75% 提升到了 93%


八、总结与展望


回顾秒开率优化的这一期工作,是立项之后的第一期,在优化项选型过程中,除了优化效果之外,人力成本是我们考虑的最多的因素,由于团队人力不充裕,一些需要比较高成本去研究、去做的优化项,也都暂时搁置留做二期任务或者无限搁置。启动优化本身就是个需要持续迭代、持续打磨的任务,我们在优化的过程中始终秉承着 高收益成本比、低风险收益比 的思想,所以后续我们还会继续钻研,继续将之前没有开展的任务排上日程,技术之路,永无止境。


防劣化跟优化一样重要


在线上优化工作开展完成且取得了相应成果后,绝对不能放松警惕,优化这一下获得了效果并不是最重要的,最重要的是要有持续的、稳定的优化效果。对于线上用户来说,其实可能并不关心这个版本或者这几个版本是不是变快了,大家可能更需要的是长时间的良好体验,对于我们这些开发者来说,长时间的良好体验可能才能改变大家对 自家 App 的性能印象,让大家认可自家 App 的启动性能,这也是我们优化的初衷。因此,防劣化跟优化一样重要!


其他


做性能优化往往是比较枯燥的,可能很长时间没有进展,但是当有成果出来时,会收获到一些幸福感和满足感。希望大家都能在遇到瓶颈很难再往前迈步时,再努力挣扎一下,如果不出意外的话,这一路一定很精彩。


作者:邹阿涛涛涛涛涛涛
来源:juejin.cn/post/7306692609497546752
收起阅读 »

屠龙少年终成恶龙,前端转产品的我给前端挖了个坑

前端转产品3周左右,把自己的一些感受通过《我转产品了-前端转产品是一种什么样的体验》这篇文章与大家分享,评论区惊现一波大佬。由于比较忙,不知不觉好像转眼间已经又过去一个多月,这次趁着周末没有开成会,给大家分享一下最近的『趣事』。 目前在并行中的一个项目主要是类...
继续阅读 »

前端转产品3周左右,把自己的一些感受通过《我转产品了-前端转产品是一种什么样的体验》这篇文章与大家分享,评论区惊现一波大佬。由于比较忙,不知不觉好像转眼间已经又过去一个多月,这次趁着周末没有开成会,给大家分享一下最近的『趣事』。


目前在并行中的一个项目主要是类似文件管理的功能,在这个项目中主要是旁听的角色,这个项目的产品和需求是由公司的一产品朋友来做。在文章中产品功能描述部分由于众所周知的原因,我只会粗略的提一下。


客户说的


用于简化传统 OA 系统的某些繁复操作。最好能编辑、预览,如果没有的话也可以,先上一版。


我们准备给客户做的


我们给客户画了第一批原型,浏览器登录一个网址,注册一个账号、密码…… 然后在文件系统中可能在线预览图片、PDF、word、excel 之类的。


客户其实想要的


在演示的时候,第一个界面,客户质问为什么我们的系统还需要输入网址?还需要登录?大家一脸懵逼。然后解释因为数据是服务器器上的,在浏览器里输入地址,就能访问我们的系统了。然后如果不使用用户名和密码登录的话就不知道谁是谁。


客户说:『我们云桌面对每个人都是唯一的啊,不需要再搞一个账户……』


我们说:『因为我们系统是在浏览器里的,浏览器是访问不了操作系统里的用户信息的,所以需要注册一个用户密码……如果觉得麻烦,我们也可以后台先注册后,然后登录之后记住登录状态,就不用每次都登录了。』


客户:『行吧。』


然后讲业务流程……


当讲到某文档的审核功能时,比如如果审核人需要对 word 进入批注,需要下载,然后添加批注,再上传到审核意见附件中。


客户:『为什么要下载?』


我们:『因为我们的是浏览器。』


客户:『行吧。』


矛盾分析


经过了几周、几轮之后的演示,客户还经常时不时问一些为什么我们系统不能直接进行某操作的想法。想要不用写到这里大家都很清楚了,客户想做的东西其实是一个便于操作的文件管理系统。要实现最大的便利性,最好是与操作系统打通。


但我们这边技术栈主要是 B/S 架构的经验较多,桌面程序的经验基本没有。并且应该我们这边认为浏览器里的文件管理操作也是很常见的,比如各大网盘都可以在浏览器里进行文件操作。


我们调研的方向都甚至是通过各种 js sdk 实现浏览器中预览 word、excel 的功能。


一些自己的想法,不知该如何讲


这个产品讨论会来来回回5-6次到现在还没结束,虽然从第2次我基本就确定客户这种应该使用桌面端来实现比较好。但是这种东西不好讲,这是产品和技术人员去决定的东西。


不能随便讲的原因在哪里:



  • 角色只是旁听者就不要去定一些方向性的东西。


这个应该大家都明白,角色问题。



  • 每个方向最小和最大会有什么后果,团队是不是能承担?


这个也比较清楚,难度问题。首先讲,如果做成桌面端,团队没有这方面的经验,遇到操作系统相关的 API 调不起来怎么搞?兼容性怎么搞?我了解的前端肯定是实现不了,虽然 node/electron 可以与操作系统交互,但当前前端团队无相关经验的人。虽然后端 java 可以与操作系统交互,但我不可能给客户说这东西我们这边后端 java 能做。


所以我的想法是:如果他们与客户能达成使用浏览器完成这个系统的共识,清楚浏览器与桌面程序的能力边界。那么何乐不为呢?


我方开始妥协


第6次演示原型时,客户又讲到,那如果一个文件要下载也行。但使用这个系统的人可能年龄都比较大了,下载到哪里自己经常都找不到。


这也确实,每个文件都要下载来操作再上传已经很麻烦了,再像在垃圾里去翻刚刚下载的文件,就更麻烦了。客户问能不能下载的位置我们系统可以指定的?


我们再次说下载位置是浏览器规定的,系统指定不了。然后客户提到,实现不行像 ftp 这些工具一样,能把文件传到某个指定的位置也行……


然后我们技术负责人说,那这样的话,看能不能把两台设备互通,当浏览器里要下载某文件时,向后端发起请求,后端从服务器上操作客户端电脑。客户说可以啊!


我理解上一段话下来是这样:


客户端与服务端有某个通道,允许服务端操作客户端,比如文件创建、删除。然后客户端的浏览器里要下载某个文件到 C 盘时,向服务器发起请求,服务端去后台下载文件到客户端的 C 盘。


看起来到也可行,这对前端浏览器而言还是一样的。直接向服务端发请求就行了。


但是我的想法是这样的:???


准备好了吗?坑要来了


上面说的功能结果是可行的,并且客户也是接受的,而且前端也是一样的只需向服务器发起请求即可。但是我有以下想法:



  • 有点绕。

  • 服务端要如何远程操作客户端?这是个问题。在客户端上装个自己写好的 curd 操作的程序?服务器通过 telnet 等现有工具远程后台操作客户端?


据我所知有一种从天而降的掌法,叫 electron,主要是前端来弄,可是这东东体积太大了,当然体积在当前客户这里不是首要问题。首要问题是这东西虽然很厉害,但这里的前端不会 node。还有一些方案,浏览器插件配合、WPF 这些也都不用讲了。


所以有没有一种方法,不需要会 node,不需要后端语言(比如java/C#),不需要安装依赖(比如运行库、浏览器插件)、兼容现有前端已写好的页面和接口,如果需要调用系统 API(例如文件、IO、进程),前端只需要调用 js 方法传参即可,有点像 JSBridge。


就针对于我个人的见识层面和需求层面来说,约等于没有。所以我打算自己弄个(挖坑)。但我不能说我要挖个坑啊?


开始挖坑


我尝试性的问技术负责人,如果使用套壳浏览器的方案,前端正常写,如果要操作文件时,前台能直接调用 js 方法,例如创建文件、打开文件、定位文件等等。您看行不?负责人问,那要检测文件修改上传可以吗?我愣了两秒,说可以(就算不能监控修改,也还有通过获取文件MD5对比的方式)。然后我能说那我会后给您提供一些 demo 你看看。


给出一些 demo,让坑看起来没有坑


为了证明方案的可行性和便利性,我用团队当前技术栈 Vue 为当前文件系统可能用到的操作都提供了示例:



  • 进程操作。创造子进程、使用系统程序。

  • 文件定位。给定一个文件路径,让资源管理器直接定位到它。

  • 文件后台下载。后台下载文件到指定位置。

  • 文件后台上传。后台上传上件到指定接口。

  • 文件操作。文件创建、修改、重命名、删除……

  • 文件打开。以默认程序或指定的系统程序打开指定文件。


定制好图标、描述。打包成这个单独的 exe,体积 1.2M,好像有点大了,将就吧。


image.png


实测几次没问题之后,发给技术负责人。


技术负责人:『???』


不知道当时负责人心里是不是在想:什么玩意?没事做吗?给我发个 exe?病毒吗?你很能吗?产品试用期过了吗?


// 可以编程式创建窗口。
const view = await new main.View(`http://baidu.com`)
view.form.show() // 显示窗口

前端对该方案的实测效果


也可以指定本地文件,例如 desktop.html 。


{
"page": "desktop.html",
"pageShow": true
}

过一会,前端同事转发负责人与他的沟通给我,然后一脸懵逼的问他到底要做什么?沟通一波之后,原来会议内容还没同步给前端同事,讲了好半天客户需求,才慢慢知道要搞一个客户端。但他又再次陷入震惊,我一前端,凭什么让我搞客户端?我不会啊!


我突然感觉我有点难。然后我说:『不是我要你搞客户端。是客户要客户端。我只是提供了一个方案,你看得行不』。


然后让其先把我提供的 dmeo 在他电脑上试试看各功能是否正常。


image.png


很感动,果然没有问题。


然后同事问:『那这东西把我现在写的系统放进去能正常用吗?』


真是个问题,我说:『不知道,你试试,建议路由 hash 模式。』


然后同事直接把自己的系统的链接放进去,尝试了一下一些自己原有的功能,是正常的。


image.png


然后同事问:『这东西基于什么技术,关键词?』


我答:『webview。』


然后同事搜索了一下甩我一张截图,接着问:『和 edge 一样吗?』


我说:『不严格等于,但约等于。』


也不知道同事看了这句话有没有骂我,搁这给我玩哲学?


同事又问:『客户电脑是 win7 的,客户电脑没装 edge 浏览器怎么办?』


我:『拿我的 demo 上去,能打开就是支持的。』


理论上,如果客户电脑上没有 webview 环境,会自动安装。但客户那边网络环境是不通外网的,所以我让直接试。


image.png


然后一会之后,实测下来,还好就是支持的。


接下来就是前端同事尝试在已有的项目中去使用我提供的 api 去操作系统上的内容了。经过一波沟通,实测也是没有问题。


疯狂暗示,我挖的坑,与我无关


一边在沟通如何入坑我的轮子的同时,一边我又提供了前端实现桌面程序的其他方案供其选择,时时不忘提醒,你看哪个合适你用哪个。这个 exe 是我封装的,API 只有示例,详细文档来不及写,桌面程序我也没有太多的经验。


image.png


PS:意思就是,方案由你选,如果选择我的轮子,某些功能我也要先研究一会,某些功能我也可能解决不了。还有我现在主要重心在产品上,所以可能没时间研究新功能。 /手动狗头保命。


1700618482469.png


小惊喜


虽然知道 electorn 麻烦,但我这确实没有文档,也麻烦,但是过了几天,突然收到消息:


image.png


image.png


然后我邪魅一笑(哈哈哈哈,入坑了!入坑了!入坑了!)。


后记


这个坑不是我故意挖的,是有意挖的。因为有一个想法,开发一个简单的桌面程序,只使用前端语言开发,暂只考虑在 windows 上运行,希望开发体验像在浏览器中一样,然后程序的样子像是本地应用一样,调用本地文件、系统命令、后台运行、托盘菜单这些都没有问题。


我调研了一些常见的方案,发现他们大多数都不喜欢,经常体积太大或要求其他语言,有一 neutralino 看起来实现上是想要的,但 api 太少,所以决定含泪造坑,在 api 设计上会考虑贴近 neutralino 便于两者迁移。


代码仓库在这里 github.com/wll8/sys-sh… 


作者:程序媛李李李李李蕾
来源:juejin.cn/post/7303798788720115749
收起阅读 »

如何科学的进行Android包体积优化

这篇文章会分享小厂如何做包体积优化相关主题,涉及内容包括:1) Android包体积优化的一种可能是比较标准的推进做法,2) 大致流程的心路历程和思考方式,3) 如何去总结和分享你们进行过的包体积优化项目。本文不仅仅是一篇分享,还是我个人比较喜欢的总结报告写法...
继续阅读 »

这篇文章会分享小厂如何做包体积优化相关主题,涉及内容包括:1) Android包体积优化的一种可能是比较标准的推进做法,2) 大致流程的心路历程和思考方式,3) 如何去总结和分享你们进行过的包体积优化项目。本文不仅仅是一篇分享,还是我个人比较喜欢的总结报告写法。



一、前言


移动 App 特别关注投放转化率指标,而 App 包体积是影响用户新增的重要因素,而 App 的包体积又是影响投放转化率的重要因素。


Google 2016 年公布的研究报告显示,包体积每上升 6MB 就会带来下载转化率降低 1%, 当包体积增大到 100MB 时就会有断崖式的下跌 。对于应用商店,普遍有一个流量自动下载的最大阈值,如 应用宝,下载的app超过100M,用流量下载时,会弹窗提示用户是否继续下载,这对下载转化率影响是比较大的。


现在流量虽然变得更廉价一点,但是用户的心理是不会变的,当 App 出现在应用市场的相同位置时,包体积越大,用户下载意愿可能越低。


而且包体积或直接或间接地影响着下载转化率、安装时间、运行内存、磁盘空间等重要指标,所以投入精力扫除积弊、发掘更深层次的体积优化项是十分必要的。



某手:



  • 1M = 一年kw级推广费


某条极速版:



  • Google 2016 年公布的研究报告显示,包体积每上升 6MB 就会带来下载转化率降低 1%,当包体积增大到 100MB 时就会有断崖式的下跌。

  • 通过插件化,将常规优化后达 120M+的包体积降到了 13M 左右,最小版本降至 4M,包体积缩小至原先的 3.33%。


某德:



  • 包体积大小,是俞xx直接拍的,就要求 x年x月x日 前削减到100M。


某淘:



  • 包大小做得比较“霸权”“独裁”,新业务超过 1M 要总裁审批,一般在平台组都卡掉了。



二、评估优化需求


在开展工作前,我们首先得先有两点判断:



  1. 是否需要进行优化

  2. 优化到多少算符合预期


那具体应该如何进行判断呢?有个比较简单的方法就是对标,找到对标对象进行对标。


对于小厂来说,一般对标对象有:



  1. 竞品App

  2. 业内人气App


基于对标对象,我们可以粗略的有如下判断:



  1. 如果我们App跟竞品App包体积差不多或略高,那就有必要进行包体积优化。

  2. 优化到跟业内人气App中包体积最小的那一档差不多即可。


上述判断还是基于用户视角,假如你的 用户需求 比较简单,好几个app都可以满足, 你会安装200M的产品,还是50M的产品。再有,假如用户在App商店无意间看到你们的App,有点兴趣体验体验,但是看到包体积是200M,他有多大概率会继续下载,换成50M呢?


三、包体积优化基本思想


我们在做包体积优化前,一定要定好我们的大方向,我们怎么优化,能做哪些,哪些可以现在做,哪些没必要现在做,


1. 抓各个环节


我们最终是要优化App的包体积,那么App包组成部分有哪些,我们针对每一个部分去研究如何减少其体积,就可以达到我们最终的效果。


2. 系统化方案先行,再来实操


优化Android包体积这个事情,有一定的探索性,但是很多内容或者说手段都是业内在广为流传的,既然如此,我们应该总结业内可优化手段,并逐一进行分析,研究是否适合你们App,是否需要应用到你们App。如果没有方案的埋头扎进去,往往会因为陷入细节,而丢失了全局视野。


3. 明确风险收益比及成本收益比


方案与方案之间是有区别的。如果一个方案能减少包体积2M,但是线上可能会崩溃,你会做吗? 如果一个方案能减少包体积2M,但是开发成本要一个月,你会做吗?


4. 明确指标以及形成一套监控防劣化体系


干任何一件以优化为目标的事情时,一定要明确优化的指标,我们要做App的包体积优化,那么我们的指标为:减少App安装包 apk 的大小。


当我们指标明确之后,我们还需要对现有指标进行监控,这样有两个好处:



  • 明确优化收益

  • 防止劣化


那我们就可以在某个关键的时间节点进行包体积的统计和上报,一般时间节点有:



  • App发版打包时(粒度更粗)

  • 开发分支有新的 commit 合入时(粒度更细)


两种粒度各有各的好处,但是目标是一样的:



  • 监控每次打包的包体积,可以行成指标曲线进行持续观察

  • 在包体积增加超出预期时进行及时报警


5. 把包体积这个指标刻在脑子里


自动化能发现已经发生的问题,但是把包体积这个指标刻在脑子里,能避免问题的发生。


四、自家App包体积一览


1. Android Apk结构


APK 主要由五个部分组成,分别是:



  • Dex:.class 文件处理后的产物,Android 系统的可执行文件

  • Resource:资源文件,主要包括 layout、drawable、animator,通过 R.XXX.id 引用

  • Assets:资源文件,通过 AssetManager 进行加载

  • Library:so 库存放目录

  • META-INF:APK 签名有关的信息


132d21f3-a1b6-43f6-99d0-fad7ffb81412.png


2. 你们Apk各个部分都长啥样,长多大?


这里选取示例App某个版本的安装包来做举例分析,下面是包结构图:


QQ截图20231109021600.png


浅浅分析一波包内内容


成分体积备注
assets文件夹77.8M下载.png 能看到Assets文件夹里,有着75M的RN bundle
lib75.2Ma7d410c9-75ab-4c27-9d21-9c8953567d00.png 由于我们App是兼容包,即同时兼容64位、32位,所以lib目录很大
dex16M这部分涉及到我们自己的代码及三方库代码
res6.3M这里包含各种图片资源、布局文件等
resources.arsc1.2M
其他若干

五、优化方案草案


通过调研业内常规、非常规手段,以及结合你们App的包体积现状,可以提前对优化包体积做出比较详尽的可实现、低风险、高收益方案,注意这几个点非常重要:



  • 可实现 - 可实现可以简单理解为实现成本低,一般各种性能稳定性指标都是循序渐进的推进,所以往往一期优化时,选的实操方案都是实现成本比较低的,这样能相对快速的得到比较符合心里预期的优化效果。

  • 低风险 - 线上风险必须控制在可接受的程度,这里的风险不仅仅是说App运行的性能稳定性风险,还需要判断是否会增加线上问题排查的难度,当然还会有其他的我没提到的风险项。

  • 高收益 - 不解释


所以基于我们需要的是可实现、低风险、高收益的方案,我们可以基于上面我贴的APK案例,来大致预演可能会采用哪些方案:


1. 缩减动态化方案内置代码包、资源包


一般的小厂都会比较大量的采用如RN、H5等动态化方案,不可避免的App内就会有一堆内置文件,我们看到我们示例的App中,RN内置包占了整个包体积超过 30%。当出现这种情况时,可以针对内置代码包、资源包单独立子项去推进。


那么如何进行推进呢?有同学就会说了,业务方不让我把这些玩意儿从APK里面删掉,说影响他们打开页面速度,影响页面打开速度就会影响一级指标影响收入


这时为了说服业务方,我们就得拿出一些证据,用来证明内置包的全部移除或者部分移除并不会对业务产生影响,或者说影响足够小。那就可以采取如下一些推进步骤:



  • 明确全部内置包或者部分内置包不内置的影响,假如内置包是 RN 的页面bundle,那给业务方两个数据基本上就能够说明影响



    • 页面bundle现下比例,假如因为本地没有内置的bundle,打开页面需要同步进行等待下载完成才能加载的话,现场下载比例是个比较有说服力的数据。

    • 线上bundle更新耗时,我们可以统计用户启动App后的一段指定时间,90分位能下载多少个bundle,50分位能下载多少个,10分位、5分位能下载多少个,来告诉业务方,老用户、新用户、老用户新登录等各种场景,到达业务页面的时候,有多少比例的用户能完成bundle更新。



  • 明确什么样的资源需要内置,同样用RN页面bundle举例,假如App的首页就是RN页面,那这玩意儿就必须内置了,假如一个页面在犄角旮旯,日pv才不到100,那就完全可以不需要内置。

  • 给出内置资源名单

  • 拿着内置名单和上面明确的不内置影响统计,找业务方拉会, 这一步最好是从上往下进行推进,而不是同级推进


2. 分架构打包


分架构打包能减少libs文件夹体积,libs文件夹里会包含不同架构的 so 库集合。


首先我们最终apk包是要上传到应用商店的,应用商店得支持双包上传。答案确实是支持,且应用商店推荐双包上传。


Android 官方也是有相关的api支持分架构打包:


splits {
// 基于不同的abi架构配置不同的apk
abi {
// 必须为true,打包才会为不同的abi生成不同的apk
enable true
// 默认情况下,包含了所有的ABI。
// 所以使用reset()清空所有的ABI,再使用include指定我们想要生成的架构armeabi-v7a、arm-v8a
reset()
// 逗号分隔列表的形式指定 Gradle 应针对哪些 ABI 生成 APK。只与 reset() 结合使用,以指定确切的 ABI 列表。
include "armeabi-v7a", "arm64-v8a"
// 是否生成通用的apk,也就是包含所有ABI的apk。如果设为 true,那么除了按 ABI 生成的 APK 之外,Gradle 还会生成一个通用 APK。
universalApk true
}
}

这里需要注意的是,线上并不是所有的手机都支持 64位 的安装包,应用商店可以双包上传,线上灰度更新可以下发32位的安装包或者是 32/64 兼容包。


3. So 压缩


分架构打包是减少so的数量,so压缩是减少so的单个体积。


android:extractNativeLibs="true"

android:extractNativeLibs = true时,gradle打包时会对工程中的so库进行压缩,最终生成apk包的体积会减小。


但用户在手机端进行apk安装时,系统会对压缩后的so库进行解压,从而造成用户安装apk的时间变长。


若开发人员未对android:extractNativeLibs进行特殊配置,android:extractNativeLibs默认值:



  • minSdkVersion < 23 或 Android Gradle plugin < 3.6.0情况下,打包时 android:extractNativeLibs=true

  • minSdkVersion >= 23 并且 Android Gradle plugin >= 3.6.0情况下,打包时android:extractNativeLibs=false


4. 大so动态下发


我们能看到有些so库单个体积超大,放在apk里,就算能压缩,压缩后体积仍然很大,可能会占到 app体积超过 10%。针对这种情况,选择动态下发。


a91934eb-420a-4e74-8b38-5e899876d89a.png


动态下发的so如何进行加载


我们采用ASM的方案,对代码中所有的 System.load、System.loadLibrary 进行hook,进入到我们自己的逻辑,这样我们就可以走下面流程:



  1. 下载so库

  2. 解压so库

  3. 校验so库

  4. 加载so库


这里需要注意的一点就是,当动态下发的so没有下载、解压、校验、加载完之前,如果用户进入到了相关的业务场景,必须有兜底机制。比如在样例App的场景中,使用了 opencv 库来做图片的二维码识别,当so没下载下来时,要识别二维码就会被兜底到 zxing。


而且由于我们有较好的Hook框架的封装,所以我们需要hook时,仅仅需要进行配置即可:



这里可以参考我之前的博客和github上demo项目:


基于 Booster ASM API的配置化 hook 方案封装 - 掘金 (juejin.cn)


AndroidSimpleHook-github


5. 大文件压缩优化,对内置的未压缩大文件进行,压缩文件用高压缩率的压缩算法


假如apk里有内置的大文件,可以通过对其进行压缩从而减少包体积,压缩时可以选用高压缩率的算法。


6. 代码优化



  • 去除无用代码、资源


去除无用代码我们可以用官方的Lint检查工具



  • 去除无用三方库



  • 减少ENUM的使用


每减少一个ENUM可以减少大约1.0到1.4 KB的大小,假如有10000个枚举对象,那不就减少了14M?美滋滋啊,但实际上具体还是要看项目代码情况来考虑,毕竟不是所有的项目里都有 10000 个枚举。


7. 资源优化



  • 无用资源文件清理


去除无用资源文件可以通过lint工具来做,也可以通过微信开源的 ApkChecker来完成。


github.com/Tencent/mat…


图片压缩、转webp

图片压缩可以使用TinyPng,AndroidStudio也有相关插件,官方术语就是:


使用智能的无损压缩技术来减少图片文件的大小,通过智能的选择颜色的数量,减少存储的字节,但是效果基本是和压缩前一样的。


图片着色器

相同图片只是颜色不同的话,完全可以只放一个图片,在内存里操作 Drawable,完成颜色替换。


图片动态下发

如果本地有大图,且使用要求为未压缩,或者压缩之后仍然很大,可以适当的选择动态下载该图。


resources.arsc资源混淆

resources.arsc这个文件是存放在APK包中的,他是由AAPT工具在打包过程中生成的,他本身是一个资源的索引表,里面维护者资源ID、Name、Path或者Value的对应关系,AssetManager通过这个索引表,就可以通过资源的ID找到这个资源对应的文件或者数据。


通过对apk 中的resources.arsc进行内容修改,来对apk进行深度压缩。这里可以采用微信的AndResGuard方案。


github.com/shwenzhang/…


8. 三方库优化


移除无用三方库

移除无用三方库需要人肉扫描 build.gradle 文件,一个一个的去检查依赖的三方库是否被我们代码所使用。


功能重复的三方库整合

特别常见的case,RN 用的图片加载库是 Fresco,客户端用的图片加载库是 Glide,他们都是用来加载图片,可以通过删除一个库,让项目依赖的库少一个。



  • 修改三方库源码,不需要的代码进行剔除


一个三方库往往不会被用到全部功能,比如曾经很火的 XUtils github.com/wyouflf/xUt…


XUtils是一个工具大杂烩,但是假如我只用它来加载图片,其他工具是不是就完全无用,可以进行剔除。


9. 去除 DebugItem 包含的 debug信息与行号信息


在讲解什么是 deubg 信息与行号信息之前,我们需要先了解 Dex 的一些知识。


我们都知道,JVM 运行时加载的是 .class 文件,而 Android 为了使包大小更加紧凑、运行时更加高效就发明了 Dalvik 和 ART 虚拟机,两种虚拟机运行的都是 .dex 文件,当然 ART 虚拟机还可以同时运行 oat 文件。


所以 Dex 文件里的信息内容和 Class 文件包含的信息是一样的,不同的是 Dex 文件对 Class 中的信息做了去重,一个 Dex 包含了很多的 Class 文件,并且在结构上有比较大的差异,Class 是流式的结构,Dex 是分区结构,Dex 内部的各个区块间通过 offset 来进行索引。


为了在应用出现问题时,我们能在调试的时候去显示相应的调试信息或者上报 crash 或者主动获取调用堆栈的时候能通过 debugItem 来获取对应的行号,我们都会在混淆配置中加上下面的规则:


-keepattributes SourceFile, LineNumberTable

这样就会保留 Dex 中的 debug 与行号信息。根据 Google 官方的数据,debugItem 一般占 Dex 的比例有 5% 左右


10. ReDex


ReDex 是 Facebook 开发的一个 Android 字节码的优化工具。它提供了 .dex 文件的读写和分析框架,并提供一组优化策略来提升字节码。官方提供预期优化效果:对dex文件优化为 8%


github.com/facebook/re…


11. R 文件瘦身


当 Android 应用程序被编译,会自动生成一个 R 类,其中包含了所有 res/ 目录下资源的 ID。包括布局文件layout,资源文件,图片(values下所有文件)等。在写java代码需要用这些资源的时候,你可以使用 R 类,通过子类+资源名或者直接使用资源 ID 来访问资源。R.java文件是活动的Java文件,如MainActivity.java的和资源如strings.xml之间的胶水


通过R文件常量内联,达到R文件瘦身的效果。


github.com/bytedance/B…


12. 可能的更多方案


除了我上面列到的一些,市面上还有一些其他的方案,有复杂的有不复杂的,有收益高的有收益低的,大家可以在掘金上搜索Android包体积优化,就能搜到大部分了,当然,在大厂里,还会有很多很极致的方案,比如:



  • 去掉kotlin生成的许多模板代码、判空代码

  • 去除布局文件里不需要的冗余内容

  • ...


思想是这么个思想,大家在实操的时候,思路就是先调研方案,调研完成之后再选型。


六、基于风险收益比及成本收益比敲定最终实现方案


这一步的重点是:明确风险收益比及成本收益比


方案与方案之间是有区别的。



  • 如果一个方案能减少包体积2M,但是线上可能会崩溃,你会做吗?

  • 如果一个方案能减少包体积2M,但是开发成本要一个月,你会做吗?


这里我们在示例App的基础上,对每个手段进行仔细分析,包括:



  1. 预期效果

  2. 成本

  3. 风险


就这样,当我们制定完成我们的目标方案之后,就可以放手干了。


手段预期效果成本是否要做进度备注
重点优化项
- 缩减RN 内置bundle预期效果:177.43M -> 114.43MRN 内置bundle缩减,xxxx版本带上
- 分架构打包,64位、32位分开打包预期效果:32位:117.43M -> 71.9M 64位:117.43M -> 87.6Mxxxx
- so压缩方案预期效果:32位:71.9M -> 55.5M 64位:87.6M -> 58.3Mxxxx
- 大so文件动态下发预期效果:32位:55.5M -> 50.7M 64位:58.3M -> 51.7Mxxxx
大文件优化
- zip优化,对内置的压缩文件替换压缩算法预期针对 assets 文件针对不同类型文件选取不同高压缩率算法
代码优化 (dex文件为 15.6M)
- 去除无用代码Android Lintxxx
- 减少ENUM的使用全部代码 enum类 一共60个,就算全删了也只是减少 84kxxx每减少一个ENUM可以减少大约1.0到1.4 KB的大小
资源优化 (目前res目录大小为 6.3M,emoji目录大小为 770k)
- 无用资源文件清理Android Lintxxx用ApkChecker再跑一次
- 图片压缩、转webpTinyPngxxx
- 图片着色器xxx
- 图片动态下发主要是针对比较大的图,实际上经过TinyPng 压缩后,图片大小已经大大减小xxx
- resources.arsc资源混淆AndResGuard两年不维护,花了一小时没完全跑起来,但看到了大致优化效果,1.3M -> 920kgithub.com/shwenzhang/…
三方库优化 (dex文件为 15.6M)
- 移除无用三方库检查一下
- 移除无用三方so库
- 功能重复三方库整合
- 修改三方库源码,不需要的代码进行剔除
极致优化,附 ByteX 插件情况
- 去除 DebugItem 包含的 debug信息与行号信息mp.weixin.qq.com/s/_gnT2kjqp…
- ReDex对dex文件优化为 8%,即在当前dex总和 15.6M的基础上,可以减少 1.2MDex 压缩,首次启动解压dexhttps://github.com/facebook/redexhttps://juejin.cn/post/7104228637594877965
- R 文件瘦身现成方案:github.com/bytedance/B… failed for task ':app:transformClassesWithShrinkRFileForQaRelease'.> java.lang.RuntimeException: This feature requires ASM7

七、确定优化效果


当我们进行了一系列的或大或小的改动之后,如何描述最终优化效果?给两张对比图不就行了,无图言X。




八、总结


大家在进行一些有挑战性或者是比较有意义的项目时,其实可以多进行总结,总结的好处有什么我就不多解释了,懂的都懂哈。


比如我们这里可以装模作样的这样总结一下:


做的好的方面



  1. 足够系统化

  2. 前置调研足够充分

  3. 风险、收益、成本考虑足够充分

  4. 各方面沟通足够充分

  5. 优化决心足够大


也可以告诉自己及读者几句话



  1. 这是一个系统的需要持续去投入人力的事情,万万不可有了一定结果之后放松警惕

  2. 别人能做的,我们也能做,只要有足够的决心去做

  3. 做事不能太讲究所谓的方法论,不然会掉入陷阱,但是确实要讲究方法论

  4. 有些事情你做好了,可能仅仅是因为做这个事情的人是你,如果是别人来做,也能将这件事情做好


九、展望


一般来说,进行总结之后,都得来一些展望,给未来的自己挖点坑,给总结的读者画点饼。比如我们这里就可以这样继续装模作样的展望一下:


上面已经反复提及了,当前这一期的优化工作,重点考量的指标是风险收益比及成本收益比,所以一些极致的或者成本收益比较高的优化手段并没有被采用,所以后续还是有很多事情可以深入的干下去。



  1. resources.arsc资源混淆

  2. 去除 DebugItem 包含的 debug信息与行号信息

  3. ReDex

  4. R 文件瘦身

  5. So unwind 优化

  6. kotlin相关优化

  7. ...


十、真正的总结


这里我就发散性的随便总结下吧。。。也不深入纠结了。



  1. 包体积优化是个庞大的工程项目,不仅仅需要优化,还需要防劣化,优化过程中还会涉及到业务冲突,说白了就是某些东西从APK包中移除了,或多或少会有些影响,还需要去跟业务方达成意见一致。

  2. 大家不管在做什么优化课题时,最好是分步骤分工期的去进行,不要一口吃成胖子,如果上来就追求完全极致的优化效果,往往会带来两个负面风险:1)  优化工期拉长,时间成本成倍增加,2)可能影响线上App或者线下各种工具的运行稳定性。

  3. 系统化的调研、成本 + 风险 + 收益的总和考虑非常重要,任何优化项目开始进行或者进行过程中,都需要牢牢的印在脑子里,每日三省你身。

  4. 遇到困难不要畏惧,各种优化项目往往会遇到很多阻力,比如方案实现太难、业务沟通太难等等,一块石头硬磕磕不动的时候换个方向磕,换方向也磕不动那就换块石头磕,比如假设业务方沟通不动,那就换个角度,把你和业务方放在同一角色上,给业务方找收益或者。

  5. 做的项目是啥或者说研究的方向是啥其实不是最重要的,我们这种普通程序员更重要的是解决问题的能力,因为你们做的事情,换个人用同样的时间成本或者更多的时间成本,往往也能做好,所以是你做好的这件事情其实没那么重要,更重要的是遇到其他问题或者有其他的疑难杂症和系统性问题时,知道你一定能做好。


作者:邹阿涛涛涛涛涛涛
来源:juejin.cn/post/7302982924987039796
收起阅读 »

HarmonyOS 页面传值跳转

介绍 本篇主要介绍如何在HarmonyOS中,在页面跳转之间如何传值 HarmonyOS 的页面指的是带有@Entry装饰器的文件,其不能独自存在,必须依赖UIAbility这样的组件容器 如下是官方关于State模型开发模式下的应用包结构示意图,Page就是...
继续阅读 »

介绍


本篇主要介绍如何在HarmonyOS中,在页面跳转之间如何传值


HarmonyOS 的页面指的是带有@Entry装饰器的文件,其不能独自存在,必须依赖UIAbility这样的组件容器


如下是官方关于State模型开发模式下的应用包结构示意图,Page就是带有@Entry装饰器的文件


0000000000011111111.20231123162458.56374277887047155204379708661912.png


那么在页面跳转时,在代码层面最长路径其实是有两步 1,打开UIAbility 2. 打开Page


整体交互效果


页面传值demo.png


传值理论



  1. 基于LocalStorage

  2. 基于EventHub

  3. 基于router


准备


请参照官方指导,创建一个Demo工程,选择Stage模型


代码实践


1.定制主入口页面


功能



  1. 页面曝光停留时长计算

  2. 增加进入二级页面入口


import systemDateTime from '@ohos.systemDateTime'
import router from '@ohos.router'

@Entry
@Component
struct Index {
@State message: string = '页面跳转'

private showDuration: number = 0

onPageShow() {

this.showDuration = 0
systemDateTime.getCurrentTime(false, (error, data) => {
if(!error){
this.showDuration = data
}
})

}

build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(()=>{
systemDateTime.getCurrentTime(false, (error, data) => {
router.pushUrl({ url: 'pages/OpenPage', params: {
"from": "pages/Home.ets",
"data": {
"duration":(data - this.showDuration)
}
} })
.then(() => {
console.info('Succeeded in jumping to the second page.')
}).catch((error) => {
console.log(error)
})
})
})
}
.width('100%')
}
.height('100%')
}

}

2.添加二级页面


注意

OpenPage.ets需要在main_pages.json中的注册


{
"src": [
"pages/Index" //主入口页面
,"pages/OpenPage" //二级页面
,"pages/Test" //三级页面
,"pages/LocalStorageAbilityPage" //三级页面
]
}

功能



  1. 展示主入口页面停留时间

  2. 添加通过UIAbility方式打开页面的入口

  3. 添加通过router.pushUrl方式打开页面的入口


/**
* 路由 3.1/4.0 文档
* https://developer.harmonyos.com/cn/docs/documentation/doc-references-V3/js-apis-router-0000001478061893-V3#ZH-CN_TOPIC_0000001523808578__routerpushurl9
*
*/

import router from '@ohos.router';
import common from '@ohos.app.ability.common';


@Entry
@Component
struct OpenPageIndex{
@State extParams: string = ''
private expParamsO: Object
private context = getContext(this) as common.UIAbilityContext;

aboutToAppear(){
this.expParamsO = router.getParams();
this.extParams = JSON.stringify(this.expParamsO, null, '\t');
}

build(){
Column(){

List(){
ListItemGr0up() {
ListItem() {
Text(this.extParams)
.width('96%')
.fontSize(18)
.fontColor(Color.Green)
.backgroundColor(Color.White)
}.width('100%')
.align(Alignment.Start)
.backgroundColor(0xFFFFFF)
.borderRadius('16vp')
.padding('12vp')

}.divider({
strokeWidth: 1,
startMargin: 0,
endMargin: 0,
color: '#ffe5e5e5'
})

ListItemGr0up() {

ListItem() {
Text('启动UIAbility页面')
.width('96%')
.fontSize(18)
.fontColor(Color.Black)
.backgroundColor(Color.White)
}.width('100%')
.height(50)
.align(Alignment.Start)
.backgroundColor(0xFFFFFF)
.padding({ left: 10 })
.onClick(() => {
this.startAbilityTest('LocalStorageAbility')
})

ListItem() {
Text('启动@Entry页面')
.width('96%')
.fontSize(18)
.fontColor(Color.Black)
.backgroundColor(Color.White)
}.width('100%')
.height(50)
.align(Alignment.Start)
.backgroundColor(0xFFFFFF)
.padding({ left: 10 })
.onClick(() => {
router.pushUrl({ url: 'pages/Test', params: {
"from": "pages/OpenPage.ets"
} })
.then(() => {
console.info('Succeeded in jumping to the second page.')
}).catch((error) => {
console.log(error)
})
})

}.divider({
strokeWidth: 1,
startMargin: 0,
endMargin: 0,
color: '#ffe5e5e5'
})

}.width('100%').height('90%')
.divider({
strokeWidth: px2vp(20),
startMargin: 0,
endMargin: 0,
color: '#ffe5e5e5'
})

}.width('100%').height('100%')
.padding({ top: px2vp(111) , left: '12vp', right: '12vp'})
.backgroundColor('#ffe5e5e5')
}

async startAbilityTest(name: string) {
try {
let want = {
deviceId: '', // deviceId为空表示本设备
bundleName: 'com.harvey.testharmony',
abilityName: name,
parameters:{
from: 'OpenPage.ets',
data: {
hello: 'word',
who: 'please'
}
}
};
let context = getContext(this) as common.UIAbilityContext;
await context.startAbility(want);
console.info(`explicit start ability succeed`);
} catch (error) {
console.info(`explicit start ability failed with ${error.code}`);
}

}

}


3. 添加三级页面


注意

先要添加注册一个新的容器,这里命名为:LocalStorageAbility.ets
容器需要在module.json5中声明


  {
"name": "LocalStorageAbility",
"srcEntry": "./ets/entryability/LocalStorageAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:icon",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background"
}

import window from '@ohos.window';
import UIAbility from '@ohos.app.ability.UIAbility';


let para:Record<string,string> = { 'PropA': JSON.stringify({ 'from': 'LocalStorageAbility'}) };
let localStorage: LocalStorage = new LocalStorage(para);

export default class LocalStorageAbility extends UIAbility {

storage: LocalStorage = localStorage

onCreate(want, launchParam) {

}

onWindowStageCreate(windowStage: window.WindowStage) {
super.onWindowStageCreate(windowStage)

windowStage.loadContent('pages/LocalStorageAbilityPage', this.storage, (err, data) => {
if (err.code) {
return;
}

setTimeout(()=>{
let eventhub = this.context.eventHub;
console.log(para['PropA'])
eventhub.emit('parameters', para['PropA']);
}, 0)

});
}

}

Test.ets和LocalStorageAbilityPage.ets需要在main_pages.json中的注册


{
"src": [
"pages/Index" //主入口页面
,"pages/OpenPage" //二级页面
,"pages/Test" //三级页面
,"pages/LocalStorageAbilityPage" //三级页面
]
}

功能



  1. 展示基于LocalStorage,EventHub,router 三种传值方式的数据


LocalStorageAbilityPage.ets 文件



  • 展示LocalStorage,EventHub方式的数据


import router from '@ohos.router';
import common from '@ohos.app.ability.common';

// 通过GetShared接口获取stage共享的LocalStorage实例
let storage = LocalStorage.GetShared()

@Entry(storage)
@Component
struct LocalStorageAbilityPageIndex {
@State message: string = ''
// can access LocalStorage instance using
// @LocalStorageLink/Prop decorated variables
@LocalStorageLink('PropA') extLocalStorageParms: string = '';

context = getContext(this) as common.UIAbilityContext;

aboutToAppear(){
this.eventHubFunc()
}

build() {
Row() {
Column({space: 50}) {

Column({space: 10}){
Text('LocalStorage传值内容')
Text(JSON.stringify(JSON.parse(this.extLocalStorageParms), null, '\t'))
.fontSize(18)
.fontColor(Color.Green)
.backgroundColor(Color.White)
.width('100%')
.padding('12vp')
.borderRadius('16vp')
}

Column({space: 10}){
Text('eventHub传值内容')
Text(this.message)
.fontSize(18)
.fontColor(Color.Green)
.backgroundColor(Color.White)
.width('100%')
.padding('12vp')
.borderRadius('16vp')
}

}.width('100%').height('100%')
.padding({ top: px2vp(111) , left: '12vp', right: '12vp'})
.backgroundColor('#ffe5e5e5')
}
.height('100%')

}

eventHubFunc() {
this.context.eventHub.on('parameters', (...data) => {
this.message = JSON.stringify(JSON.parse(data[0]), null, '\t')
});
}

}

作者:harvey_fly
来源:juejin.cn/post/7306447457151942690
收起阅读 »

android之阿拉伯语适配及注意细节

1.  AndroidManifest.xml配置文件中的 标签下,配置元素 android:supportsRtl="true"。此时当系统语言切换的时候,你的 App 也会跟着切换 UI 布局为镜像后的效果。若未增加该元素,在xml中切换语言时,...
继续阅读 »

1.  AndroidManifest.xml配置文件中的 标签下,配置元素 android:supportsRtl="true"。此时当系统语言切换的时候,你的 App 也会跟着切换 UI 布局为镜像后的效果。

若未增加该元素,在xml中切换语言时,会提示 image.png 增加后,可在xml文件中查看反转后的效果 2.  新增value-ar文件夹

image.png

image.png

image.png 把values/strings.xml文件复制到values-ar文件中,逐条翻译即可。

  1. layout中的Left/Right修改为Start/End

可使用Android Studio中自带的工具:“工具栏”-“Refactor”-“Add right-to-Left(RTL)Support” image.png

注意事项:

  • 1).此时会把所依赖gradle里的xml文件列出,记得删除,不要转换。

image.png

  • 2). 该工具只适用于项目的app模块,无法直接应用于依赖模块。如果需要在依赖模块中进行RTL转换,要逐个打开并手动进行相应的修改。
  • 3). Start属性在LTR中对应Left,End属性在LTR中对应Right,在API 17开始支持,为了兼容低版本,可以同时有Left和Start。

    即在“Add right-to-Left(RTL)Support”工具中,不勾选“Replace Left/Right Properties with Start/End Properties”

image.png

  1. 返回icon、下一个icon等,要针对阿拉伯语新建一个文件夹,放镜像后的图片,规则如下:

mipmap-xhdpi->mipmap-ldrtl-xhdpi

drawable->drawable-ldrtl

最终镜像的图片要UI同事提供,临时修改看效果可以使用镜像图片的网站:http://www.lddgo.net/image/flip

  1. TextView、EditText:利用全局样式,在style.xml中定义,在xml里使用style=”@style/xxx”即可
  • 1). TextView
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
       ...
       <item name="android:textViewStyle">@style/TextViewStyle.TextDirectionitem>
       ...
style>
<style name="TextViewStyle.TextDirection" parent="android:Widget.TextView">
        <item name="android:textDirection">localeitem>
style>
  • 2). EditText
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
       ...
       <item name="editTextStyle">@style/EditTextStyle.Alignmentitem>
       ...
style>
<style name="EditTextStyle.Alignment" parent="@android:style/Widget.EditText">
        <item name="android:textAlignment">viewStartitem>
        <item name="android:gravity">startitem>
        <item name="android:textDirection">localeitem>
style>
  1. 其他细节
  • 1).固定ltr,如阿拉伯语下的“99%”要从左到右展示,可在xml中使用
android:layoutDirection ="ltr"
  • 2).获取当前系统语言Locale.getDefault().getLanguage()

判断是否为阿拉伯语:"ar".equals(Locale.getDefault().getLanguage())

判断是否为英语:"en".equals(Locale.getDefault().getLanguage())

  • 3). drawable/xxx_selector.xml中item里有android:drawable,如勾选框。

drawable有android:autoMirrored属性,将selector的该属性设置为true,就可以让drawable在RTL布局下进行反转

image.png

  • 4).进度条的默认进度指示是从左到右,使用leftMargin;在阿拉伯语下,进度指示从右到左,使用rightMargin属性
  • 5).阿拉伯语环境下,使用SimpleDateFormat格式化时间字符串的时候,会显示为:٢٠١٥-٠٩-١٨ ٠٧:٠٣:٤٩。若要展示:2023-09-067:10:45,可以使用Locale.ENGLISH参数
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH);
Date now=new Date();
System.out.println(sdf .format(now));
  • 6). 加载html可用 tv.setText(Html.fromHtml(getResources().getString(R.String.xxx));
  • 7). 开机导航中设置了阿拉伯语,当前页面布局要刷新,可以重写activity的onConfigurationChanged()方法,如在该方法里重置下一步箭头、指示器样式等

image.png

  • 8).ViewPager

若是ViewPager,可使用第三方控件RtlViewPager替换: 521github.com/diego-gomez…,添加依赖,单纯替换原ViewPager即可

implementation 'com.booking:rtlviewpager:1.0.1' 

类似三方控件: 521github.com/duolingo/rt…

或者使用androidx的ViewPager2替换: developer.android.google.cn/jetpack/and…,支持RTL布局

image.png

image.png

  • 9). 固定RTL字符串的顺序

问题现象:EditText带hint,密码可见、不可见时,会调用如下方法进行设置

image.png 此时会影响hint的展示:在勾选时,hint的结束字符在右侧;不勾选时,hint的结束字符在左侧。

image.png

image.png

解决方法:此时要使用Unicode控制字符来限制整个字符串的显示方向:\u202B 和 \u202C。

image.png

有以下两种方法

a.  java代码

image.png

b.  strings.xml

image.png

最终效果:

image.png

image.png

10). Blankj的toast展示异常

android工具类Blankj的toast工具类在展示阿拉伯语时为空或者部分展示,建议使用1.30.6 及以上版本

image.png

github.com/Blankj/Andr…

11). RTL布局中出现双光标/光标截断的情形

image.png

在布局文件内加上如下两个属性即可:

android:textDirection="anyRtl"
android:textAlignment="viewStart"

若还未解决

1.可查看是否使用了android:textCursorDrawable=“@null”,若有,可尝试去掉该句。

2.在AndroidManifest.xml中查看当前App/Activity的主题,比较老的项目可能使用了android:Theme.NotitleBar/android:Theme.Light等轻量级主题,如下所示:




收起阅读 »

谨以此文,纪念 查理·芒格

今早起床刷手机,看到有位朋友发了芒格的在《穷查理宝典》中的一段话,当时以为只是普通的一句话。在中午的时候,才刷到,原来芒格竟然在今天去世了。 上班的时候,就一直在想,之前正好写过《穷查理宝典》的读书笔记,今天一定得发篇文章纪念下,奈何,加班到现在(晚上11点)...
继续阅读 »

今早起床刷手机,看到有位朋友发了芒格的在《穷查理宝典》中的一段话,当时以为只是普通的一句话。在中午的时候,才刷到,原来芒格竟然在今天去世了。


上班的时候,就一直在想,之前正好写过《穷查理宝典》的读书笔记,今天一定得发篇文章纪念下,奈何,加班到现在(晚上11点),才开始动笔。


其实也谈不上怎么崇拜了解芒格本人,但《穷查理宝典》却真的影响了我很多。


记得20年的冬天,刚毕业的那段时光,还在北京的时候,经常坐了一个多小时的地铁,从大屯路到广渠门地铁站,去那里的中信书店,待上个一整天。那阵子,估计是我看书最多的时光。而《穷查理宝典》,也是花了恰好一个周末看完的,看到头晕眼花的那种。


当时看完的时候,真的觉得整个人都被洗涤了一番,尤其是最后的误判心理学和多元思维模型,是真的精彩,还写了篇文章,记录了一些读书感悟。当时,公众号的简介也是写着:争做跨学科思维模型者。


图片


一晃,原来已经3年多过去了啊,虽然也买了《穷查理宝典》的实体书了,但这3年来,好像真就没怎么翻起来过。重新了解芒格,也是在看了上次成甲老师的直播后,开始看成甲老师写的《好好思考》,开始再一次了解跨学科思维模型。


虽然说没有再翻起来过《穷查理宝典》,但今天再看到下面的这些内容,却意外的发现,原来很多内容,早已经在潜移默化的影响我了,或许,这就是好书的魅力吧。




1 持续学习



  • 每天睡觉前都比当天早晨聪明一点。《巴菲特传》中一半醒来时间都在读书的巴菲特、《穷查理宝典》中追求跨学科思维模型的芒格、通过阅读《Redis》小论文就能理解缓存机制的周老师、不断在微信读书中分享读书感悟的子乾哥、喜欢晚上在办公室静静思考的立明哥,告诉我要多去问为什么并思考的曙光哥。芒格说的对,身边优秀的人,没有一个是不坚持学习的。

  • 持续学习不仅仅指读书,也包括在实践、工作中的思考,成长。《大国大民》中说: 知识包括“知道”和“见识”两部分,只知道读书充其量能当一个“知道”分子,需要在实践中去不断强化、塑造自己知道的东西,去增长见识,才能称之为“知识”分子。

  • 即使连最简单的读书,自己也没有做好,之前看书总有种囫囵吐枣,完成任务的感觉。看完一本书后,往往都没多深的印象,也没有系统的获得多少东西,甚至于说过一段时间后,连书中讲了什么都记不太清楚。当然,读书还是有个好处的,有什么会莫名的对书中的一两个点产生触动,然后你会在生活中去反复利用这些点,并收获一些东西。如《拥抱可能》中的“你就算不念博士,照样会52岁”、《一生的旅途》中的:”试一试呗,反正我又不会损失什么,万一成功了呢“。


2 跨学科思维模型



  • 利用跨学科的思维模型并不断对其进行实践,以打破固有的意识形态和“铁锤人倾向“。

  • 人总有一种避免不一致的倾向,很容易只相信自己想要的东西(就像我在做读书笔记时,记录的往往是和自己想法一致的内容);b: 不愿意去做改变,在做出一个决定后,会坚信这决定是对的,会过高的高估自己所拥有的东西(其实就是禀赋效应,就好像很多父母会高看自己的孩子,很多人觉得自己拥有的东西的最宝贵)。

  • 作为一名工科生,之前总是采取一种工科的、直线型的思维去看待事情,在看《产品思维》时,发现里面涉及很多心理学和社会学的概念,甚至于产品大牛喻军直接说:“大学没有专门的产品经理专业,最适合当产品经理的是经济学和心理学的毕业生”。任重而道远!


3 德行合一



  • 要想得到一样东西,最可靠的方式让你自己配得上它,无论是工作还是配偶。

  • 之前的《职场适应力》培训中也说到,当你想要某个职位的时候,不是期盼好运降临到你头上,而是去提高自己的能力,只有能力到了,时机到了才会有机会(这其实和芒格说的:“只要做好准备,在人生中抓住机会,迅速的采取适当的行动“不谋而合)。在当下这刚入职几个月的阶段,啥不用想,去使劲干就是了,先多后精。

  • “如果想拥有一个好配偶,得先让自己配得上才行,毕竟,好配偶可不会眼瞎”。努力吧,放平心态,先去提升自己。


    3年过去了,真的很庆幸,认识了现在的夫人,真的是这辈子做的最正确的决定了



4 逆向思维



  • 如说如果想过的幸福,那就反过来想,去避免那些让生活变得更坏的(懒惰、嫉妒、言而无信)的行为。

  • 想一想自己身上其实还是存在很多坏习惯: 作息不规律、阅读浅尝辄止(缺乏系统性的、深入的学习。芒格说:“其实我们不用去了解很多复杂的理论知识,很多学科的基本概念就那么几个,如经济学的成本-效益原理,数学上的统计概率原理等”)、自我服务偏好(认为“自我”有资格去做它想做的事情,比如说超前透支去满足一些实际不需要的物质需求)、自怜倾向(为自己的失败找各种理由,觉得是任务太复杂之类的,其实这就是逃脱)。

  • 上面的这些坏习惯虽说不致命,但始终是拖累自己前进的障碍,有人说:发现问题比解决问题更重要。真希望这句话也适用于坏习惯上,既然意识到了这些坏习惯,就争取努力去改正吧。


5 近朱者赤



  • 《乌合之众》提到社会群体具有两个重要的特性:传染性 和 容易被暗示性,传染性说简单点就是从众心理,个体总是不愿意去违背群体的状态,容易被暗示性则指个体总是做出与独处时完全想法的行动。群体之于个人,其实就是环境的重要性,要去塑造让自己成长、前进的环境,远离让自己沉沦的环境。

  • 烟台集训最大的收获就是:有幸和这么一群优秀的人认识,自己要多努力,保持开放、积极的状态,才能不至于落后。今天老妈也说到一个类似的观点:在成长期,要永远向前看,向比你优秀的看,这样到达更高的高度。在你老了,需要知足常乐的时候,才需要朝后看。沉稳型的人格的自己,需要更多的动力,更多的向掌控型人格学习,才能成就更好的自己。

  • 之前也喜欢阅读,不过大都是在家里看书,很容易就被其他事情分散注意力,来书店后,看到身边这么多人在认真的看书,你多玩会手机都会觉得自惭形愧(啊哈,这好像有点像芒格说的社会认同倾向)。从烟台回来周末,看了《产品思维》,虽说不能立马应用于工作中,但至少了解了一些概念,还是稍有收获的。而且,一杯咖啡、一本书就能过一天的日子,随着自己承担的工作越来越多,只会越来越少,好好珍惜吧。记录一句,kindle,手机啥的适合读小说类的书,复杂一点的还是纸质版舒服。

  • 吴老师在集训时说:为未来多挤出当下的一点时间。其实,对职业的规划或者未来,自己有个模糊的概念,但总归不够清晰,更谈不上为此去努力当下,将很多时间浪费在了不必要的、快餐娱乐上。

  • 现在在做的事情,让我遇见了很多优秀的朋友和榜样,确实很明显的能感觉一个厉害的圈子,能让我极大的成长。


6 始终保持感恩之心



  • 恰巧今天是教师节,想一想到了25的这个年纪了,真正的去表达感恩的行动真是少之又少,连自己老师、导师都很少去表达感激之情,今早和高中老师发了个简单节日快乐,这显然是不够的,在此也立个flag吧:今年过年,去一一给老师们拜年!


    好的,现在看,这个flag也没实现,甚至于说,研究生导师已经与世长辞。


  • 感恩身边的每一个人,走到现在,觉得生命中碰到的每个人,每个人都有很有闪光点、优点,值得我去学习、去领悟。不仅仅包括父母、长辈、师长、领导、同事、朋友、室友。甚至于连在路上碰到的陌上人、或者素不相识的网友、明星等,每个人都拥有独特的特质,要始终保持开放的心,去接受、发现每一个人好。


       最后,感谢芒格先生和《穷查理宝典》,希望我也能在这辈子,输出一些真正有价值的,被世人铭记的东西。


作者:刘卡卡
来源:juejin.cn/post/7306762343177502739
收起阅读 »

比亚迪面试,全程八股!

比亚迪最近几年凭借着其新能源汽车的板块大火了一把,无论是名声还是股价都涨得嘎嘎猛,但是迪子招聘编程技术岗位的人员却有两个特点: 面试难度低,对学校有一定的要求。 薪资给的和面试难度一样低。 但不管怎么,迪子也算是国内知名公司了,所以今天咱们来看看,他的校招...
继续阅读 »

比亚迪最近几年凭借着其新能源汽车的板块大火了一把,无论是名声还是股价都涨得嘎嘎猛,但是迪子招聘编程技术岗位的人员却有两个特点:



  1. 面试难度低,对学校有一定的要求。

  2. 薪资给的和面试难度一样低。


但不管怎么,迪子也算是国内知名公司了,所以今天咱们来看看,他的校招 Java 技术岗的面试题都问了哪些知识点?面试题目如下:
image.png


1.int和Integer有什么区别?


参考答案:int 和 Integer 都是 Java 中用于表示整数的数据类型,然而他们有以下 6 点不同:



  1. 数据类型不同:int 是基础数据类型,而 Integer 是包装数据类型;

  2. 默认值不同:int 的默认值是 0,而 Integer 的默认值是 null;

  3. 内存中存储的方式不同:int 在内存中直接存储的是数据值,而 Integer 实际存储的是对象引用,当 new 一个 Integer 时实际上是生成一个指针指向此对象;

  4. 实例化方式不同:Integer 必须实例化才可以使用,而 int 不需要;

  5. 变量的比较方式不同:int 可以使用 == 来对比两个变量是否相等,而 Integer 一定要使用 equals 来比较两个变量是否相等;

  6. 泛型使用不同:Integer 能用于泛型定义,而 int 类型却不行。


2.什么时候用 int 和 Integer?


参考答案:int 和 Integer 的典型使用场景如下:



  • Integer 典型使用场景:在 Spring Boot 接收参数的时候,通常会使用 Integer 而非 int,因为 Integer 的默认值是 null,而 int 的默认值是 0。如果接收参数使用 int 的话,那么前端如果忘记传递此参数,程序就会报错(提示 500 内部错误)。因为前端不传参是 null,null 不能被强转为 0,所以使用 int 就会报错。但如果使用的是 Integer 类型,则没有这个问题,程序也不会报错,所以 Spring Boot 中 Controller 接收参数时,通常会使用 Integer。

  • int 典型使用场景:int 常用于定义类的属性类型,因为属性类型,不会 int 不会被赋值为 null(编译器会报错),所以这种场景下,使用占用资源更少的 int 类型,程序的执行效率会更高。


3.HashMap 底层实现?


HashMap 在 JDK 1.7 和 JDK 1.8 的底层实现是不一样的。



  • 在 JDK 1.7 中,HashMap 使用的是数组 + 链表实现的。

  • 而 JDK 1.8 中使用的是数组 + 链表或红黑树实现的


HashMap 在 JDK 1.7 中的实现如下图所示:
image.png
HashMap 在 JDK 1.8 中的实现如下图所示:


4.HashMap 如何取值和存值?


参考答案:HashMap 使用 put(key,value) 方法进行存值操作,而存值操作的关键是根据 put 中的 key 的哈希值来确定存储的位置,如果存储的位置为 null,则直接存储此键值对;如果存储的位置有值,则使用链地址法来解决哈希冲突,找到新的位置进行存储。


HashMap 取值的方法是 get(key),它主要是通过 key 的哈希值,找到相应的位置,然后通过 key 进行判断,从而获取到存储的 value 信息。


5.SpringBoot 如何修改端口号?


参考答案:在 Spring Boot 中的配置文件中设置“server.port=xxx”就可以修改端口号了。


6.如何修改 Tomcat 版本号?


参考答案:在 pom.xml 中添加 tomcat-embed-core 依赖就可以修改 Spring Boot 中内置的 Tomcat 版本号了,如下图所示:
image.png
但需要注意的是 Spring Boot 和 Tomcat 的版本是有对应关系的,要去 maven 上查询对应的版本关系才能正确的修改内置的 Tomcat 版本号,如下图所示:
image.png


7.SpringBoot如何配置Redis?


参考答案:首先在 Spring Boot 中添加 Redis 的框架依赖,然后在配置文件中使用“spring.redis.xxx”来设置 Redis 的相关属性,例如以下这些:


spring:
redis:
# Redis 服务器地址
host: 127.0.0.1
# Redis 端口号
port: 6379
# Redis服务器连接密码,默认为空,若有设置按设置的来
password:
jedis:
pool:
# 连接池最大连接数,若为负数则表示没有任何限制
max-active: 8
# 连接池最大阻塞等待时间,若为负数则表示没有任何限制
max-wait: -1
# 连接池中的最大空闲连接
max-idle: 8


8.MySQL 左连接和右连接有什么区别?


参考答案:在 MySQL 中,左连接(Left Join)和右连接(Right Join)是两种用来进行联表查询的 SQL 语句,它们的区别如下:



  1. 左连接:左连接是以左边的表格(也称为左表)为基础,将左表中的所有记录和右表中匹配的记录联接起来。即使右表中没有匹配的记录,左连接仍然会返回左表中的记录。如果右表中有多条匹配记录,则会将所有匹配记录返回。左连接使用 LEFT JOIN 关键字来表示。

  2. 右连接:右连接是以右边的表格(也称为右表)为基础,将右表中的所有记录和左表中匹配的记录联接起来。即使左表中没有匹配的记录,右连接仍然会返回右表中的记录。如果左表中有多条匹配记录,则会将所有匹配记录返回。右连接使用 RIGHT JOIN 关键字来表示。


例如以下图片,左连接查询的结果如下图所示(红色部分为查询到的数据):
image.png
右连接如下图红色部分:
image.png


9.内连接没有匹配上会怎么?


参考连接:内连接使用的是 inner join 关键字来实现的,它会匹配到两张表的公共部分,如下图所示:
image.png
所以,如果内连接没有匹配上数据,则查询不到此数据。


小结


以上是比亚迪的面试题,但并不是说比亚迪的面试难度一定只有这么低。因为面试的难度通常是根据应聘者的技术水平决定的:如果应聘者的能力一般,那么通常面试官就会问一下简单的问题,然后早早结束面试;但如果应聘者的能力比较好,面试官通常会问的比较难,以此来探寻应聘者的技术能力边界,从而为后续的定薪、定岗来做准备,所以大家如果遇到迪子的面试也不要大意。


作者:Java中文社群
来源:juejin.cn/post/7306723594816733235
收起阅读 »

苹果M3系列登场,性能翻倍,M1、M2已成时代眼泪

M3 芯片来了。 在苹果今天的 Scary Fast 发布会上,苹果正式发布了 M3、M3 Pro、M3 Max 芯片。苹果表示,这是首款采用 3 纳米工艺技术的 PC 芯片,允许将更多晶体管封装到更小的空间中,以提高速度和效率。除了芯片更新,苹果还带来了搭...
继续阅读 »

M3 芯片来了。



在苹果今天的 Scary Fast 发布会上,苹果正式发布了 M3、M3 Pro、M3 Max 芯片。苹果表示,这是首款采用 3 纳米工艺技术的 PC 芯片,允许将更多晶体管封装到更小的空间中,以提高速度和效率。除了芯片更新,苹果还带来了搭载 M3 系列芯片的 MacBook Pro 以及 24 英寸版 iMac。


图片


除了提供更快、更高效的 CPU,这三款芯片还配备了全新的 GPU,支持光线追踪、网格着色和动态缓存。


M3 芯片提供高达 128GB 的统一内存,其中最强大的 M3 Max 芯片配备多达 920 亿个晶体管、40 核 GPU 和 16 核 CPU。


「Apple Silicon 彻底重新定义了 Mac 体验,其架构的每个方面都是为了性能和能效而设计的。」苹果硬件技术高级副总裁 Johny Srouji 说道。「凭借 3 纳米技术、全新 GPU 架构、更高性能的 CPU、更快的神经引擎以及对更多统一内存的支持,M3、M3 Pro 和 M3 Max 是迄今为止为个人 PC 打造的最先进的芯片。」


图片


M3 系列芯片采用业界领先的 3 纳米工艺


动态缓存、网格着色和硬件加速光线追踪


M3 系列芯片中的 GPU 代表了苹果芯片图形架构的最大飞跃。与传统 GPU 不同,它具有动态缓存功能,可以实时分配硬件中本地内存的使用,这是业界首创。


借助 M3 系列芯片,硬件加速光线追踪也首次出现在 Mac 上。光线追踪可对光与场景交互时的属性进行建模,使应用程序能够创建极其逼真且物理精确的图像。再加上新的图形架构,专业应用程序的速度可达 M1 系列芯片的 2.5 倍。


此外,新的 GPU 为 Mac 带来了硬件加速的网格着色功能,为几何处理提供了更强大的功能和效率,并在游戏和图形密集型应用程序中实现了视觉上更复杂的场景。事实上,M3 GPU 能够以近一半的功耗提供与 M1 相同的性能,并且在峰值时性能提高高达 65%。


图片


M3 系列芯片中的最新 GPU 代表了苹果芯片图形架构的最大飞跃,具有动态缓存,网格着色和硬件加速光线追踪功能。


更快更高效的 CPU


接下来介绍 CPU 部分。苹果对 M3 系列的最新 CPU 进行了改进。高性能核心比 M1 系列芯片快 30%。与 M1 芯片的能效核心相比,M3 中的能效核心带来的速度提升最高可达 50%。这些内核共同打造出一款 CPU,可提供与 M1 相同的多线程性能,而功耗仅为 M1 的一半,并且在峰值功率下性能提高高达 35%。


图片


图片


统一内存架构,最高可达 128GB


M3 系列中的每个芯片都采用统一的内存架构,可提供高带宽、低延迟和无与伦比的功效。此外,M3 芯片支持的内存容量最高达 128GB,从而使得过去无法在笔记本电脑上处理的工作流成为可能,例如 AI 开发者现在可运行包含数十亿个参数规模的 Transformer 模型。


图片


M3 的统一内存架构,支持高达 24GB 的高速统一内存。


图片


M3 Pro 芯片的统一内存架构支持高达 36GB 的高速统一内存,确保用户能够随时随地使用 MacBook Pro 处理更大型的项目。 图片


M3 Max 芯片的统一内存架构支持高达 128GB 的高速统一内存,可以完成过去无法在笔记本电脑上完成的任务


用于 AI 和视频的自定义引擎


M3、M3 Pro 和 M3 Max 还具有增强的神经引擎,可加速 ML 模型。神经引擎比 M1 系列芯片快 60%,使 AI/ML 工作流程更快,同时还能将数据保留在设备上以保护隐私。


M3 系列中的三款芯片还具有先进的媒体处理引擎,为最流行的视频编解码器提供硬件加速,包括 H.264、HEVC、ProRes 和 ProRes RAW。并且媒体引擎首次支持 AV1 解码,实现流媒体服务的节能播放,进一步延长电池寿命。


图片


最后,我们一起来看看 M3 系列芯片的各个配置。


M3 拥有 250 亿个晶体管,比 M2 多 50 亿个,支持高达 24GB 的统一内存,标准 8 核 CPU+10 核 GPU 配置。


图片


M3 的最新 GPU 在游戏《Myst》上的表现


M3 Pro 由 370 亿个晶体管和 18 核 GPU 组成,可在处理图形密集型任务时提供极快的性能。GPU 的速度比 M1 Pro 快 40%。对统一内存的支持高达 36GB,使用户在外出时也能在 MacBook Pro 上处理大型项目。12 核 CPU 设计有 6 个性能核心和 6 个能效核心,单线程性能比 M1 Pro 快 30%。


图片


借助 M3 Pro 的 GPU 和 CPU,在 Adobe Photoshop 中拼接和处理巨幅全景照片等任务的速度比以往任何时候都快。


M3 Max 将晶体管数量提升到 920 亿个,将专业性能提升到新的水平。40 核的 GPU 比 M1 Max 快 50%,支持高达 128G 的统一内存,这使 AI 开发人员能够处理更大的、具有数十亿参数的 Transformer 模型。16 核 GPU 具有 12 个性能核心和 4 个能效核心,性能惊人,比 M1 Max 快 80%。M3 Max 拥有两个 ProRes 引擎,无论是使用 DaVinci Resolve、Adobe Premiere Pro 还是 Final Cut Pro,都能快速流畅地处理最高分辨率内容的视频后期制作工作。M3 Max 专为需要在 MacBook Pro 中获得最高性能和业界领先电池续航时间的专业人士而设计。


图片


参考链接:


http://www.apple.com/newsroom/20…


http://www.theverge.com/2023/10/30/…


作者:机器之心
来源:juejin.cn/post/7296016154408714294
收起阅读 »

只改了五行代码接口吞吐量提升了10多倍

背景 公司的一个ToB系统,因为客户使用的也不多,没啥并发要求,就一直没有经过压测。这两天来了一个“大客户”,对并发量提出了要求:核心接口与几个重点使用场景单节点吞吐量要满足最低500/s的要求。 当时一想,500/s吞吐量还不简单。Tomcat按照100个线...
继续阅读 »

背景


公司的一个ToB系统,因为客户使用的也不多,没啥并发要求,就一直没有经过压测。这两天来了一个“大客户”,对并发量提出了要求:核心接口与几个重点使用场景单节点吞吐量要满足最低500/s的要求。


当时一想,500/s吞吐量还不简单。Tomcat按照100个线程,那就是单线程1S内处理5个请求,200ms处理一个请求即可。这个没有问题,平时接口响应时间大部分都100ms左右,还不是分分钟满足的事情。


然而压测一开,100 的并发,吞吐量居然只有 50 ...


image.png


而且再一查,100的并发,CPU使用率居然接近 80% ...




从上图可以看到几个重要的信息。


最小值: 表示我们非并发场景单次接口响应时长。还不足100ms。挺好!


最大值: 并发场景下,由于各种锁或者其他串行操作,导致部分请求等待时长增加,接口整体响应时间变长。5秒钟。有点过分了!!!


再一看百分位,大部分的请求响应时间都在4s。无语了!!!


所以 1s钟的 吞吐量 单节点只有 50 。距离 500 差了10倍。 难受!!!!


分析过程


定位“慢”原因



这里暂时先忽略 CPU 占用率高的问题



首先平均响应时间这么慢,肯定是有阻塞。先确定阻塞位置。重点检查几处:



  • 锁 (同步锁、分布式锁、数据库锁)

  • 耗时操作 (链接耗时、SQL耗时)


结合这些先配置耗时埋点。



  1. 接口响应时长统计。超过500ms打印告警日志。

  2. 接口内部远程调用耗时统计。200ms打印告警日志。

  3. Redis访问耗时。超过10ms打印告警日志。

  4. SQL执行耗时。超过100ms打印告警日志。


上述配置生效后,通过日志排查到接口存在慢SQL。具体SQL类似与这种:


<!-- 主要类似与库存扣减 每次-1 type 只有有限的几种且该表一共就几条数据(一种一条记录)-->
<!-- 压测时可以认为 type = 1 是写死的 -->
update table set field = field - 1 where type = 1 and filed > 1;

上述SQL相当于并发操作同一条数据,肯定存在锁等待。日志显示此处的等待耗时占接口总耗时 80% 以上。


二话不说先改为敬。因为是压测环境,直接先改为异步执行,确认一下效果。实际解决方案,感兴趣的可以参考另外一篇文章:大量请求同时修改数据库表一条记录时应该如何设计


PS:当时心里是这么想的: 妥了,大功告成。就是这里的问题!绝壁是这个原因!优化一下就解决了。当然,如果这么简单就没有必要写这篇文章了...


优化后的效果:


image.png


嗯...


emm...


好! 这个优化还是很明显的,提升提升了近2倍。




此时已经感觉到有些不对了,慢SQL已经解决了(异步了~ 随便吧~ 你执行 10s我也不管了),虽然对吞吐量的提升没有预期的效果。但是数据是不会骗人的。


最大值: 已经从 5s -> 2s


百分位值: 4s -> 1s


这已经是很大的提升了。


继续定位“慢”的原因


通过第一阶段的“优化”,我们距离目标近了很多。废话不多说,继续下一步的排查。


我们继续看日志,此时日志出现类似下边这种情况:


2023-01-04 15:17:05:347 INFO **.**.**.***.50 [TID: 1s22s72s8ws9w00] **********************
2023-01-04 15:17:05:348 INFO **.**.**.***.21 [TID: 1s22s72s8ws9w00] **********************
2023-01-04 15:17:05:350 INFO **.**.**.***.47 [TID: 1s22s72s8ws9w00] **********************

2023-01-04 15:17:05:465 INFO **.**.**.***.234 [TID: 1s22s72s8ws9w00] **********************
2023-01-04 15:17:05:467 INFO **.**.**.***.123 [TID: 1s22s72s8ws9w00] **********************

2023-01-04 15:17:05:581 INFO **.**.**.***.451 [TID: 1s22s72s8ws9w00] **********************

2023-01-04 15:17:05:702 INFO **.**.**.***.72 [TID: 1s22s72s8ws9w00] **********************

前三行info日志没有问题,间隔很小。第4 ~ 第5,第6 ~ 第7,第7 ~ 第8 很明显有百毫秒的耗时。检查代码发现,这部分没有任何耗时操作。那么这段时间干什么了呢?



  1. 发生了线程切换,换其他线程执行其他任务了。(线程太多了)

  2. 日志打印太多了,压测5分钟日志量500M。(记得日志打印太多是有很大影响的)

  3. STW。(但是日志还在输出,所以前两种可能性很高,而且一般不会停顿百毫秒)


按照这三个思路做了以下操作:


首先,提升日志打印级别到DEBUG。emm... 提升不大,好像增加了10左右。


然后,拆线程 @Async 注解使用线程池,控制代码线程池数量(之前存在3个线程池,统一配置的核心线程数为100)结合业务,服务总核心线程数控制在50以内,同步增加阻塞最大大小。结果还可以,提升了50,接近200了。


最后,观察JVM的GC日志,发现YGC频次4/s,没有FGC。1分钟内GC时间不到1s,很明显不是GC问题,不过发现JVM内存太小只有512M,直接给了4G。吞吐量没啥提升,YGC频次降低为2秒1次。


唉,一顿操作猛如虎。


PS:其实中间还对数据库参数一通瞎搞,这里不多说了。




其实也不是没有收获,至少在减少服务线程数量后还是有一定收获的。另外,已经关注到了另外一个点:CPU使用率,减少了线程数量后,CPU的使用率并没有明显的下降,这里是很有问题的,当时认为CPU的使用率主要与开启的线程数量有关,之前线程多,CPU使用率较高可以理解。但是,在砍掉了一大半的线程后,依然居高不下这就很奇怪了。


此时关注的重点开始从代码“慢”方向转移到“CPU高”方向。


定位CPU使用率高的原因


CPU的使用率高,通常与线程数相关肯定是没有问题的。当时对居高不下的原因考虑可能有以下两点:



  1. 有额外的线程存在。

  2. 代码有部分CPU密集操作。


然后继续一顿操作:



  1. 观察服务活跃线程数。

  2. 观察有无CPU占用率较高线程。


在观察过程中发现,没有明显CPU占用较高线程。所有线程基本都在10%以内。类似于下图,不过有很多线程。


image.png


没有很高就证明大家都很正常,只是多而已...


此时没有下一步的排查思路了。当时想着,算了打印一下堆栈看看吧,看看到底干了啥~


在看的过程中发现这段日志:


"http-nio-6071-exec-9" #82 daemon prio=5 os_prio=0 tid=0x00007fea9aed1000 nid=0x62 runnable [0x00007fe934cf4000]
java.lang.Thread.State: RUNNABLE
at org.springframework.core.annotation.AnnotationUtils.getValue(AnnotationUtils.java:1058)
at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory$AspectJAnnotation.resolveExpression(AbstractAspectJAdvisorFactory.java:216)
at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory$AspectJAnnotation.<init>(AbstractAspectJAdvisorFactory.java:197)
at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory.findAnnotation(AbstractAspectJAdvisorFactory.java:147)
at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(AbstractAspectJAdvisorFactory.java:135)
at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvice(ReflectiveAspectJAdvisorFactory.java:244)
at org.springframework.aop.aspectj.annotation.InstantiationModelAwarePointcutAdvisorImpl.instantiateAdvice(InstantiationModelAwarePointcutAdvisorImpl.java:149)
at org.springframework.aop.aspectj.annotation.InstantiationModelAwarePointcutAdvisorImpl.<init>(InstantiationModelAwarePointcutAdvisorImpl.java:113)
at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvisor(ReflectiveAspectJAdvisorFactory.java:213)
at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvisors(ReflectiveAspectJAdvisorFactory.java:144)
at org.springframework.aop.aspectj.annotation.BeanFactoryAspectJAdvisorsBuilder.buildAspectJAdvisors(BeanFactoryAspectJAdvisorsBuilder.java:149)
at org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator.findCandidateAdvisors(AnnotationAwareAspectJAutoProxyCreator.java:95)
at org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator.shouldSkip(AspectJAwareAdvisorAutoProxyCreator.java:101)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.wrapIfNecessary(AbstractAutoProxyCreator.java:333)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.postProcessAfterInitialization(AbstractAutoProxyCreator.java:291)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:455)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1808)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:353)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:233)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1282)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1243)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:494)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:349)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:342)
at cn.hutool.extra.spring.SpringUtil.getBean(SpringUtil.java:117)
......
......

上边的堆栈发现了一个点: 在执行getBean的时候,执行了createBean方法。我们都知道Spring托管的Bean都是提前实例化好放在IOC容器中的。createBean要做的事情有很多,比如Bean的初始化,依赖注入其他类,而且中间还有一些前后置处理器执行、代理检查等等,总之是一个耗时方法,所以都是在程序启动时去扫描,加载,完成Bean的初始化。


而我们在运行程序线程堆栈中发现了这个操作。而且通过检索发现竟然有近200处。


通过堆栈信息很快定位到执行位置:


<!--BeanUtils 是 hutool 工具类。也是从IOC容器获取Bean 等价于 @Autowired 注解 -->
RedisTool redisTool = BeanUtils.getBean(RedisMaster.class);

而RedisMaster类


@Component
@Scope("prototype")
public class RedisMaster implements IRedisTool {
// ......
}

没错就是用了多例。而且使用的地方是Redis(系统使用Jedis客户端,Jedis并非线程安全,每次使用都需要新的实例),接口对Redis的使用还是比较频繁的,一个接口得有10次左右获取Redis数据。也就是说执行10次左右的createBean逻辑 ...


叹气!!!


赶紧改代码,直接使用万能的 new 。


在看结果之前还有一点需要提一下,由于系统有大量统计耗时的操作。实现方式是通过:


long start = System.currentTimeMillis();
// ......
long end = System.currentTimeMillis();
long runTime = start - end;


或者Hutool提供的StopWatch:


这里感谢一下huoger 同学的评论,当时还误以为该方式能够降低性能的影响,但是实际上也只是一层封装。底层使用的是 System.nanoTime()。


StopWatch watch = new StopWatch();
watch.start();
// ......
watch.stop();
System.out.println(watch.getTotalTimeMillis());

而这种在并发量高的情况下,对性能影响还是比较大的,特别在服务器使用了一些特定时钟的情况下。这里就不多说,感兴趣的可以自行搜索一下。





最终结果:



image.png





排查涉及的命令如下:



查询服务进程CPU情况: top –Hp pid


查询JVM GC相关参数:jstat -gc pid 2000 (对 pid [进程号] 每隔 2s 输出一次日志)


打印当前堆栈信息: jstack -l pid >> stack.log


总结


结果是好的,过程是曲折的。总的来说还是知识的欠缺,文章看起来还算顺畅,但都是事后诸葛亮,不对,应该是事后臭皮匠。基本都是边查资料边分析边操作,前后花费了4天时间,尝试了很多。



  • Mysql : Buffer Pool 、Change Buffer 、Redo Log 大小、双一配置...

  • 代码 : 异步执行,线程池参数调整,tomcat 配置,Druid连接池配置...

  • JVM : 内存大小,分配,垃圾收集器都想换...


总归一通瞎搞,能想到的都试试。


后续还需要多了解一些性能优化知识,至少要做到排查思路清晰,不瞎搞。




最后5行代码有哪些:



  1. new Redis实例:1

  2. 耗时统计:3

  3. SQL异步执行 @Async: 1(上图最终的结果是包含该部分的,时间原因未对SQL进行处理,后续会考虑Redis原子操作+定时同步数据库方式来进行,避免同时操数据库)


TODO


问题虽然解决了。但是原理还不清楚,需要继续深挖。



为什么createBean对性能影响这么大?



如果影响这么大,Spring为什么还要有多例?


首先非并发场景速度还是很快的。这个毋庸置疑。毕竟接口响应时间不足50ms。


所以问题一定出在,并发createBean同一对象的锁等待场景。根据堆栈日志,翻了一下Spring源码,果然发现这里出现了同步锁。相信锁肯定不止一处。


image.png


org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean


image.png



System.currentTimeMillis并发度多少才会对性能产生影响,影响有多大?



很多公司(包括大厂)在业务代码中,还是会频繁的使用System.currentTimeMillis获取时间戳。比如:时间字段赋值场景。所以,性能影响肯定会有,但是影响的门槛是不是很高。



继续学习性能优化知识




  • 吞吐量与什么有关?


首先,接口响应时长。直接影响因素还是接口响应时长,响应时间越短,吞吐量越高。一个接口响应时间100ms,那么1s就能处理10次。


其次,线程数。现在都是多线程环境,如果同时10个线程处理请求,那么吞吐量又能增加10倍。当然由于CPU资源有限,所以线程数也会受限。理论上,在 CPU 资源利用率较低的场景,调大tomcat线程数,以及并发数,能够有效的提升吞吐量。


最后,高性能代码。无论接口响应时长,还是 CPU 资源利用率,都依赖于我们的代码,要做高性能的方案设计,以及高性能的代码实现,任重而道远。



  • CPU使用率的高低与哪些因素有关?


CPU使用率的高低,本质还是由线程数,以及CPU使用时间决定的。


假如一台10核的机器,运行一个单线程的应用程序。正常这个单线程的应用程序会交给一个CPU核心去运行,此时占用率就是10%。而现在应用程序都是多线程的,因此一个应用程序可能需要全部的CPU核心来执行,此时就会达到100%。


此外,以单线程应用程序为例,大部分情况下,我们还涉及到访问Redis/Mysql、RPC请求等一些阻塞等待操作,那么CPU就不是时刻在工作的。所以阻塞等待的时间越长,CPU利用率也会越低。也正是因为如此,为了充分的利用CPU资源,多线程也就应运而生(一个线程虽然阻塞了,但是CPU别闲着,赶紧去运行其他的线程)。



  • 一个服务线程数在多少比较合适(算上Tomcat,最终的线程数量是226),执行过程中发现即使tomcat线程数量是100,活跃线程数也很少超过50,整个压测过程基本维持在20左右。


作者:FishBones
来源:juejin.cn/post/7185479136599769125
收起阅读 »

2023燃烧自己-外包仔的年终总结

2023 年终总结 哈喽,大家好啊,深漂2年多的原子。今天下班,地铁上看手机时不自觉的看了眼日期,居然快12月了,思绪良多,年近末尾,2023即将过去,趁着拖延症还没犯,赶紧写下年终总结。今年是波澜曲折的一年,各种新闻怪事充斥在时间长河里,这一年中做成了些许事...
继续阅读 »

2023 年终总结


哈喽,大家好啊,深漂2年多的原子。今天下班,地铁上看手机时不自觉的看了眼日期,居然快12月了,思绪良多,年近末尾,2023即将过去,趁着拖延症还没犯,赶紧写下年终总结。今年是波澜曲折的一年,各种新闻怪事充斥在时间长河里,这一年中做成了些许事情,也有特殊的经历...


0x01 小小的成就



  • 读的书



    • 经济类



      • 《小岛经济学》



        • 阅读的章节:前六章

        • 推荐理由:以原始经济进化为现代经济,阐述经济中的基本概念,通俗易懂,经济入门的读物

        • 缺点:部分观点有失偏颇,需要阅读其他书籍做样本对比



      • 《1844经济学哲学手稿》



        • 阅读的章节:工资、资本利润、地租、异化劳动和私有财产

        • 推荐理由:作为上面《小岛经济学》的补充,独特的角度切入,如果想要找到为什么工作痛苦的答案,这本书就有,基本上是怼上面《小岛经济学》的





    • 计算机类



      • 《UNIX环境高级编程》



        • 阅读的章节:Unix 基础、标准与实现、文件目录、标准I/O库

        • 推荐理由:底层API,比如我阅读I/O相关章节之后,就很容易理解 Kafka、MySQL、Redis 等软件不同的I/O实现是为什么。标准之间的关系是什么,例如ISO C和 IEEE POSIX,以及Linux之间的关系



      • 《图解设计模式》



        • 阅读的章节:全部

        • 推荐理由:设计模式入门读物,有配套代码,缺点就是缺乏实际项目的示例(最好是带有优化前后对比的效果的书)



      • 《SQL经典实例》



        • 阅读的章节:基本上作为工具书,查对于写SQL的例子

        • 推荐理由:SQL示例比较齐全,遇到比较复杂的SQL可以参考





    • 英语(看的少)



      • 《赖氏经典英语语法》

      • 《英語魔法師之語法俱樂部》





  • 做的事



    • B站(学习方向)



      • 《图解设计模式》全章视频

      • 牛客网SQL专题全视频

      • 牛津书虫(两个故事)阅读视频、基本句型语法视频



    • smallchat 源码阅读

    • 工作一:



      • Spring Batch 项目落地,用于数据同步,解决了实际的使用问题,扩展些许接口,供组内同学编写标准化处理代码

      • 部分项目微服务化改造



    • 工作二:



      • Redis 缓存使用,高并发接口支持 QPS 30000 实现,涉及多级缓存和分布式锁的使用

      • Kafka MQ使用,理解高性能原理(需要理解 《UNIX环境高级编程》书中的I/O相关章节)

      • 树形SQL查询,with recursive + 索引优化查询,效果相差几十倍

      • 并发 version 插入问题,insert (select max(version) + 1) 解决,利用SQL的串行执行

      • 解析数据构建 ClickHouse SQL语句,动态参数,涉及一点点编译的问题(如何判断一串字符串是一个符合语法规则的表达式?),antlr4没研究明白,后面也没解决

      • 了解到主流数据清洗方案,物化视图,袋鼠云等






0x02 特殊的经历


由于工作的项目,每个迭代都需要做相应重复的工作,日积月累,处理事情的风格不一致,导致我差点失业。后来又经过自己准备的面试,第一个团队基金方向,面试说我太骄傲不好管理???,第二个团队是内容方向,面试也是顺利通过。


这件事情的反思就是微信请屏蔽当前领导!不要过多投入,与领导只有工作利益的关系。如果工作不顺心应该乘早了断,如果不能够短期换工作,请做好情绪宣泄口(比如培养爱好,我今年就去学了游泳)


以前总是天天加班到12点,后面被告知离场,当时是非常气愤的,觉得卸磨杀驴,后面想想如果当时不良的情绪不能控制,慢慢的整个团队都会充斥这种情绪,显然作为管理者的角度考虑,必须得弄走。


前面说到差点失业是啥意思?因为是在外包公司,离场对于公司而言你就是一个负资产了。他们会想尽办法赶你走并且不给你任何赔偿,我当然是很气愤的,但气愤没用,后面咨询律师,研究劳动法。要考虑收益和风险,值不值得和公司硬刚,然后被通知的第二天(这里通知之后的有一个月缓冲,继续在客户驻场)我就开始收集证据(打卡记录、微信工作聊天、邮件等信息),准备跟公司硬刚,劳动仲裁和诉讼。在这里我想提醒的是各位至少要明白,如果开除你你赔偿能拿多少钱,这种小案子请律师处理大概在6k左右,如果起诉胜利后,可以要求用人单位承担5k的费用,就是说你用1K去博赔偿,而且还会有时间上的法律执行,所以要考虑风险与收益后在决定。还有就是社保、医保、公积金不实缴,离职后可以去申请要求实缴,这个申请不需要你跟公司刚,是公积金管理局跟公司之间的博弈,你只需要申请即可。


外包公司第一招就是通知你待业,跟你谈待业,这个期间不要签署任何文件,电子邮件要明确回复拒绝待业,隔三岔五的发送邮件给公司要求提供劳动条件,作为你想要劳动的依据。可能是公司扛不住看我态度很坚决,后面又安排我面试,后面还是在原来的客户的公司转去了其他团队。


所以总结一下就是:第一要有工作情绪的控制,要有职业规划(尽早离开),要屏蔽该屏蔽的人,第二是要有法律意识,拿不定注意可以咨询律师,再不济可以委托给律师,律师会告诉你怎么固定证据。


0x03 反思总结


技术上:计算机其实也就那些东西,计算机的素养四大件(算法数据结构、网络、编译原理、操作系统),那些东西就在那,没有什么学习路径,工作面试的无非就是八股文+场景题+算法+画饼艺术(让对方觉得你很值得)


学习上:不要犹豫不觉,纠结于哪本书好,在样本不足的情况下听别人说其实没什么用,直接去豆瓣关键字搜索看起来,当你看了一部分有了样本才能辨别好坏,大部分人停留纠结在哪个好而原地踏步。


从我自己的观察来看,我是一个受环境影响的人,周围的人很强,我就很想跟他们保持同一水平超过一点点就行了,所以要认识多一些优秀的人来激励自己,例如推特圈刘能大师、迟先生、等等,作为保持学习的目标,(我发现推上好多前 pingcap 的人啊,哈哈哈 pingcap 输送人才?


0x04 未来展望


想把英语这个穷人核心技能学好,早日脱离外包的苦难命运,向偏底层一点点的工作转变,让工作提供给自己情绪价值实现正向循环。


对象?不存在的,来就来,不来就不来,其实一个人精神充裕,每天都有自己的事情做也是很开心的~


我其实没什么生活的压力,所以在30岁之前尽可能多的去经历,去做想做的事情,像小虎一样燃烧自己(说个笑话)


然后就是更多的书籍,经济学要看完,哲学要开始学起来


作者:原子jk
来源:juejin.cn/post/7303797715392708660
收起阅读 »

电话背调,我给他打了8分

前段时间招聘的一位开发,待了两三周,拿到了京东的offer,离职了。在离职的后一天,接到了他新公司的背调电话,几乎每项都给他打了8分。这个分数打的有点虚,单纯只是为了不影响他下家的入职。 离职之前,收到他在飞书上查看电话号码的消息,大概也猜到是在填写背调人信息...
继续阅读 »

前段时间招聘的一位开发,待了两三周,拿到了京东的offer,离职了。在离职的后一天,接到了他新公司的背调电话,几乎每项都给他打了8分。这个分数打的有点虚,单纯只是为了不影响他下家的入职。


离职之前,收到他在飞书上查看电话号码的消息,大概也猜到是在填写背调人信息,但自始至终,他也没打一声招呼,让给个好评。


离职最后一天,办完手续,没跟任何人打一个招呼,不知什么时候就消失了。


当初他刚入职一周时,其实大家都已经看出他在沟通上有很大问题,还想着如何对他有针对性的安排工作和调整,发挥他的长处,避免他的短处。但没想到这么快就离职了。在他提离职时,虽没过多挽留,但给了一些过来人的建议,很明显也听不进去。


站在旁观者的角度来看,他的职业生涯或即将面临到的事几乎能看得清清楚楚,但他有自己的坚持,别人是没办法的。


就着这事,聊聊最近对职场上关于沟通的一些思考:


第一,忌固执己见


职场中最怕遇到的一种人就是固执己见的人。大多数聪明人,在遇到固执己见的人时,基本上都会在三言两语之后停止与其争辩。因为,人一旦在自己的思维层次形成思维闭环,是很难被说服的。


而对于固执己见的人,失去的是新的思维、新的思想、纠错学习的机会,甚至是贵人的相助。试想一下,本来别人好像给你提建议,指出一条更好的路,结果换来的是争辩,是抬杠,聪明人都会敬而远之,然后默默地在旁边看着你掉坑里。


真正牛的人,基本上都是兼听则明,在获得各类信息、建议之后,综合分析,为己所用。


第二,不必说服,尊重就好


站在另外一个方面,如果一件事与己无关,别人有不同的意见,或者这事本身就是别人负责,那么尊重就好,不必强行说服对方,不必表现自己。


曾看到两个都很有想法的人,为一件事争论好几天,谁也无法说服谁。一方想用权力压另一方,另一方也不care,把简单的事情激化,急赤白脸的。


其实争论的核心只是展现形式不同而已,最终只是在争情绪、争控制感、争存在感而已,大可不必。


对于成年人,想说服谁都非常难的。而工作中的事,本身就没有对错,只有优劣,大多数时候试一下就知道了。


有句话说的非常好,“成年人的世界只做筛选,不做教育”。如果说还能做点什么,那就是潜移默化的影响别人而已。


第三,不懂的领域多听少说


如果自己对一个领域不懂,最好少发表意见,多虚心学习、请教即可。任正非辞退写《万言书》的员工的底层逻辑就是这个,不懂,不了解情况,还草率提建议,只是哗众取宠、浪费别人时间。


如果你不懂一个领域,没有丰富的背景知识和基础理论支撑,在与别人沟通的过程中,强行提建议,不仅露怯,还会惹人烦。即便是懂,也需要先听听别人的看法和视角解读。


站在另一个角度,如果一个不懂的人来挑战你的权威,质疑你的决定,笑一笑就好,不必与其争辩。


郭德纲的一段相声说的好:如果你跟火箭专家说,发射火箭得先抱一捆柴,然后用打火机把柴点着,发射火箭。如果火箭专家看你一眼,就算他输。


第四,没事多夸夸别人


在新公司,学到的最牛的一招就是夸人。之前大略知道夸人的效果,但没有太多的去实践。而在新公司,团队中的几个大佬,身体力行的在夸人。


当你完成一件事时,夸“XXX,真牛逼!”,当你解决一个问题时,夸“还得是XXX,不亏是这块的专家”。总之,每当别人有好的表现时,总是伴随着夸赞和正面响应。于是整个团队的氛围就非常好。


这事本身也不需要花费什么成本,就是随口一句话的事,而效果却非常棒。与懂得“人捧人,互相成就彼此,和气生财”的人相处,是一种非常愉悦的体验。


前两天看到一条视频,一位六七岁的小姑娘指派正在玩游戏的父亲去做饭,父亲答应了。她妈妈问:你是怎么做到的?她说:夸他呀。


看看,这么小的小孩儿都深谙的人性,我们很多成人却不懂,或不愿。曾经以为开玩笑很好,现在发现“夸”才是利器,同时一定不要开贬低性的玩笑。


其实,职场中还有很多基本的沟通规则,比如:分清无效沟通并且及时终止谈话、适当示弱、认真倾听,积极反馈、少用反问等等。


当你留意和思考这些成型的规则时,你会发现它们都是基于社会学和心理学的外在呈现。很有意思,也很有用。


作者:程序新视界
来源:juejin.cn/post/7265978883123298363
收起阅读 »

副业奇谈

本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。 楔子 在一家名为叹佬鸡煲餐馆的小桌子上,坐着我和他,榴莲鸡煲溢出的香味,让人垂涎...
继续阅读 »

本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。



楔子


在一家名为叹佬鸡煲餐馆的小桌子上,坐着我和他,榴莲鸡煲溢出的香味,让人垂涎欲滴,鸡肉和榴莲嫩滑的口感,仿佛让人重回到那个十七八岁的青春时光。他叫小润,高中时期经常带着我为非作歹,中午午休跑去打篮球,晚自习溜去操场趟草地上吹牛逼,最刻骨铭心的,还是晚自习偷偷溜去学校附近大厦最高层天台,再爬上去水塔仰望星空,俯视我们的高中,看着每个教室里面一个个奋发图强的同学,我丝毫没有半点做错事的羞愧,眼里只有天上的繁星,地下的灯光,还有旁边的那个他。


小聚


“小饿,我们95后的都已经老了,现在社会都是年轻人00后的天下,比学历能力,985、211一抓一大把,比耐力,我们身体大不如前,精力强壮的年轻人比比皆是...”


“难道你不行了?”


“你别打岔,你这一行不是也有一个35岁的梗吗,你这个前端开发岗位我了解过,是需要不断学习的,技术迭代如此之快,你跟的上吗?”


我默默的摇了摇头,诚然,我是跟不上的,vue2我都没学完vue3就已经出来了,不过我相信我还是极少数。因为我安于现状太久了,拿着不上不下的薪资,没有房贷车贷育儿的压力,不像以前住在城中村每天晚上睡觉听着管道排泄物的声音,没有压力,就没有动力,我就是这么一个充满惰性的人。


小润跟我是高中同学,那时我们的关系不错,但是毕业后各自去往自己的大学,有自己的生活,便没怎么联系了,这次出来也是近三年第一次小聚。他在一个比较老牌的做文具,做设备的大厂工作,主要内容是去一些大型物业竞标,为了竞争得到那个“标”,付出的也不少,陪酒送礼一样不落,但就算得到那个“标”,公司的绩效奖励分配制度却让小润很不满,所以他不禁感慨,“我们每个月累死累活得到的薪资,除去日常花销,本来就已经所剩不多,而且社会上还存在一种叫通货膨胀的东西,想想我们年龄越来越大,面临的职场危机,手上的筹码到底能不能支撑我们维持当前消费水平,过上自己想要的生活,这是一个比较大的问题。”我听得津津有味,虽然心里认为他的说法有点过度焦虑,但有这么一个意识,总是好的,小润看到我向他投向肯定的目光,便继续说道,“这几年我都在看书,其中看到一个企业家有一句创业名言————空手套白狼”。


空手套白狼


小润看着我一脸的疑惑,嘴角微微一笑,一脸正经的告诉我,“空手套白狼虽然百度翻译是个贬义词,但是在创业翻译过来就是用最低的成本,创造最大的价值。我想要做一些0成本,价值高,只需要付出时间的生意”。


“那么请问哪里有那么大的馅饼?”据我所知,现在谈起普通人做副业,想要0成本,要不就是什么做信息差买卖,或者视频搬运,网上一搜一大把,现在根本不是能真正获利的渠道了。当然,也可能有很多人的确做着0成本生意,闷声发大财


微信图片_20230307134118.jpg


小润从煲里夹了一块榴莲肉,放入嘴中品尝了几番后吞入腹中,真诚的向我道来,“之前你有跟我聊过你做的副业,上面的功能我看了,感觉你比较厉害,对小程序开发这一块也是比较熟悉。你有没有看过小区的停车场,白天的时候很多车位都是空闲的,极大部分都是车主开车上班,那么车子不就空闲起来了?我们可以做一个平台,让车主在平台上面登记,只要车位空闲,可以告诉平台某一个时间段空闲,让平台的其他需要在附近停车的用户看到,用户微信支付停留相对应的时间,这样不仅解决了车位紧张的问题,车位车主也能利用闲置的车位赚到一笔钱,平台也能进行抽成。”


我一听,陷入了沉思,感觉好像很有道理的样子,但又觉得哪里不对,“这种做法当然是不可能的,物业停车场大都是一个车牌对应一个停车位,不可能给别人钻这种空子。”


“那你说个der啊”


微信图片_20230307134254.jpg


“刚刚只是我在生活中发现的一些奇思妙想,就是利用闲置这个属性,接下来才是我要说的重点。你平时看街边上停着的电车多吗?”我点了点头,电车在广州这所大城市,那肯定是多的。突然,小润用筷子翻了翻鸡煲中的食物,一脸愤然的对着我说“我擦,那些肥牛都被你吃完了?”我又用筷子探寻了一下,的确,肥牛还真被我吃完了,软嫩的肥牛搭配着由榴莲和鸡煲化学反应产生的汤底,让我感觉到味蕾在跳动,入口即化,难以言喻,自然而然就多吃了几片,我尴尬又不失礼貌的问他,“要不多点一份?”


他笑了笑,摆了摆手,继续说道,“我的想法是将空闲的电车利用起来,做一个平台,平台的载体是小程序,像膜拜小程序一样,用户能找到附近的单车,而我们则是电车,但是我们不需要成本,因为在平台中,电车的信息是由车主自己主动上传上来的,所以就有两个群体,一个是车主,一个是需要用电车的用户。车主能在电车空闲的时间将电车上传到我们的平台,通过出租自己的电车进行赚钱,当出租的次数多了,不仅能回本,到时候或许还能赚点小钱。而普通用户想用电车的时候,根据小程序提供的定位,找到离他最近的那台电车,进行微信支付就能骑走,按照骑行时间进行收费,收费标准由电车车主自己提供。而我们平台的收入,则是对每笔订单进行抽成”。


我一听,又陷入了沉思,又感觉好像很有道理的样子,但又觉得哪里不对,咦,我为什么要说又?


QA



用户场景有哪些,用户需求多吗?



多,平时使用电车都是上班族居多,那上班族使用完电车后电车就闲置了,可以进行出租赚点奶茶钱,何乐而不为?况且平时下班我想去别的地方玩一下,也可以租一台电车去逛一逛,就再也不需要每个人都要买一台电车了。确实,之前去湛江游玩,也有电车提供出租,骑着电车到处逛逛吃吃,真的十分快乐,不过电车是由公司统一提供。



普通用户怎么开启这些电车呢,电车五花八门,难道要让车主统一购买我们提供的电锁进行控制?



目标电车当前只试行小牛和九号电车,用户需要开启电车的时候,在小程序可以找到电车车主联系方式,通过电话联系让他用电车钥匙开启电车,同时在小程序按下开启按钮告诉平台和用户已经开启,开始计费。用户骑行完电车后,用户致电车主进行结算并关闭电车。



客户借车后,将车的某些零件换改,偷窃,损坏,如何处理?例如将电瓶车电池换成低端电池,也能用,,但车主不知道?



这的确是个问题,我也在思考是否有必要弄押金,但是电车的押金弄小了没啥用,弄大了也不合适,没人想进行支付,所以如何平衡这个问题,是我们这个项目后续所要思考的。



用户把电车开到离起始点十万八千里,这样车主怎么找回自己的电车?



好问题,我也有想过,车主在上传电车到平台的时候,可以设置自己的使用类型,可以规定使用用户骑行归还到原位置,也可以不规定,全由车主自由设定



听起来好像真的可以落地,但是用户附近可用的电车如果多起来,在地图上展示密密麻麻,这个需要点技术,我得研究研究



我们初期可能不需要那么复杂,只需要展示一个列表,可以让用户进行筛选,用户能看到每台电车的外观,点击电车详情,就能知道用户与电车的相对位置,不需要在同一个页面展示那么多的标记(如此甚好)


// 小程序在地图上显示用户与标记方法

// js
const markers = [
{
id: 1,
// 标记的大小
width: '40px',
height: '40px',
// 标记的经纬度
longitude,
latitude,
// 标记的icon图标
iconPath
}
]
this.setData({ markers })

// wxml
// center.longitude center.latitude 为中心经纬度
<map class='map' id='map' longitude='{{center.longitude}}' latitude='{{center.latitude}}' markers="{{markers}}" scale='16'></map>


政治问题...



******<-内容加密了


我们聊了很多细节,包括首页如何设计,一键控制电车上线下线,越聊越兴奋,感觉真的可以落地,说到尽情之处,还说日后被大厂收购,实现财富自由指日可待,因为我们都知道,一个产品成熟了,稍微露出苗头,就会被人借鉴。当天晚上我回到家,就把整个大纲梳理了出来,并发给小润看。


dianche.png


但同时我们也发现问题,如果用户在骑行的途中,被车主通过车钥匙远程停车会发生什么事情,之前我们一致认为电车平台会有相对应的API提供,不仅可以获取电车信息(车辆电池,型号,外观元素等),也能有启动车辆和关停车辆的接口,但浏览了两个电车平台的官网,发现平台并没有这种东西,我们的思路一下子遇到卡壳,而且押金问题也是一个重点,热情一下子就冷却了下来,这场看似热血沸腾的副业计划就此搁置了下来。


对于做副业,我个人是非常感兴趣的,低成本的副业能赚钱是根本条件,更主要能拓展人的视野,之前我第一个副业,进行的比较顺利,但前提是市场已经有先驱,可以有模板进行复刻,而这一次纯属天马行空,没有前车之鉴,需要考虑到很多细节,如果有一些致命因素导致项目行不通,那可能这个项目就真的凉了。其实也很合理,世界上人才千千万,一个脑暴出来能赚钱的项目,为什么市场没有落地,或许不是因为没有人能想出来,更大因素是有人想出来了,但是此路不通。


省流


不亏,那顿鸡煲很香,而且是小润掏的钱


作者:很饿的男朋友
来源:juejin.cn/post/7207634883988635705
收起阅读 »

得物App安卓冷启动优化-Application篇

前言 冷启动指标是App体验中相当重要的指标,在电商App中更是对用户的留存意愿有着举足轻重的影响。通常是指App进程启动到首页首帧出现的耗时,但是在用户体验的角度来看,应当是从用户点击App图标,到首页内容完全展示结束。 将启动阶段工作分配为任务并构造出有向...
继续阅读 »

前言


冷启动指标是App体验中相当重要的指标,在电商App中更是对用户的留存意愿有着举足轻重的影响。通常是指App进程启动到首页首帧出现的耗时,但是在用户体验的角度来看,应当是从用户点击App图标,到首页内容完全展示结束。


将启动阶段工作分配为任务并构造出有向无环图的设计已经是现阶段组件化App的启动框架标配,但是受限于移动端的性能瓶颈,高并发度的设计使用不当往往会让锁竞争、磁盘IO阻塞等耗时问题频繁出现。如何百尺竿头更进一步,在启动阶段有限的时间里,将有限的资源最大化利用,在保障业务功能稳定的前提下尽可能压缩主线程耗时,是本文将要探讨的主题。


本文将介绍我们是如何通过对启动阶段的系统资源做统一管控,按需分配和错峰加载等手段将得物App的线上启动指标降低10%,线下指标降低34%,并在同类型的电商App中提升至Top3


一、指标选择


传统的性能监控指标,通常是以Application的attachBaseContext回调作为起点,首页decorView.postDraw任务执行作为结束时间点,但是这样并不能统计到dex加载以及contentProvider初始化的耗时。


因此为了更贴近用户真实体验,在启动速度监控指标的基础上,我们添加了一个线下的用户体感指标,通过对录屏文件逐帧分析,找到App图标点击动画开始播放(图标变暗)作为起始帧,首页内容出现的第一帧作为结束帧,计算出结果作为启动耗时。


例:启动过程为03:00 - 03:88,故启动耗时为880ms。


1.png


二、Application优化


App在不同的业务场景下可能会落到不同的首页(社区/交易/H5),但是Application运行的流程基本是固定的,且很少变更,因此Application优化是我们的首要选择。


得物App的启动框架任务在近几年已经先后做过多轮优化,常规的抓trace寻找耗时点并异步化已经不能带来明显的收益,得从锁竞争,CPU利用率的角度去挖掘优化点,这类优化可能短期收益不会特别明显,但从长远来看能够提前规避很多劣化问题。


1.WebView优化


App在首次调用webview的构造方法时会拉起系统对webview的初始化流程,一般会耗时200+ms,如此耗时的任务常规思路都是直接丢到子线程去执行,但是chrome内核中加入了非常多的线程检查,使得webview只能在构造它的线程中使用。


01.png


为了加速H5页面的启动,App通常会选择在Application阶段就初始化webview并缓存,但是webview的初始化涉及跨进程交互和读文件,因此CPU时间片,磁盘资源和binder线程池中任何一种不足都会导致其耗时膨胀,而Application阶段任务繁多,恰恰很容易出现以上资源短缺的情况。


02.png


因此我们将webview拆分成三个步骤,分散到启动的不同阶段来执行,这样可以降低因为竞争资源导致的耗时膨胀问题,同时还可以大幅度降低出现ANR的几率。


04.png


1.1 任务拆分


a. provider预加载


WebViewFactoryProvider是用于和webview渲染进程交互的接口类,webview初始化的第一步就是加载系统webview的apk文件,构建出classloader并反射创建了WebViewFactoryProvider的静态实例,这一操作并没有涉及线程检查,因此我们可以直接将其交给子线程执行。


10.png


b. 初始化webview渲染进程


这一步对应着chrome内核中的WebViewChromiumAwInit.ensureChromiumStartedLocked()方法,是webview初始化最耗时的部分,但是和第三步是连续执行的。走码分析发现WebViewFactoryProvider暴露给应用的接口中,getStatics这个方法会正好会触发ensureChromiumStartedLocked方法。


至此,我们就可以通过执行WebSettings.getDefaultUserAgent()来达到仅初始化webview渲染进程的目的。


150.png


c. 构造webview


即new Webview()


1.2 任务分配


为了最大程度缩短主线程耗时,我们的任务安排如下:



  • a. provider预加载,可以异步执行,且没有任何前置依赖,因此放在Application阶段最早的时间点异步执行即可。

  • b. 初始化webview渲染进程,必须在主线程,因此放到首页首帧结束之后。

  • c. 构造webview,必须在主线程,在第二步完成时post到主线程执行。这样可以确保和第二步不在同一个消息中,降低ANR的几率。


160.png


1.3 小结


尽管我们已经将webview初始化拆分为了三个部分,但是耗时占比最高的第二步在低端机或者极端情况还是可能触达ANR的阈值,因此我们做了一些限制,例如当前设备会统计并记录webview完整初始化的耗时,仅当耗时低于配置下发的阈值时,开启上述的分段执行优化。


App如果是通过推送、投放等渠道打开,一般打开的页面大概率是H5营销页,因此这类场景不适用于上述的分段加载,所以需要hook主线程的messageQueue,解析出启动页面的intent信息,再做判断。


受限于开屏广告功能,我们目前只能对无开屏广告的启动场景开启此优化,后续将计划利用广告倒计时的间隙执行步骤2,来覆盖有开屏广告的场景。


170.png


2.ARouter优化


在当下组件化流行的时代,路由组件已经几乎是所有大型安卓App必备的基础组件,目前得物使用的是开源的ARouter框架。


ARouter 框架的设计是它默认会将注解中注册path路径中第一个路由层级 (例如 "/trade/homePage"中的trade)作为该路由信息所的Gr0up, 相同Gr0up路径的路由信息会合并到最终生成的同一个类 的注册函数中进行同步注册。在大型项目中,对于复杂业务线同一个Gr0up下可能包含上百个注册信息,注册逻辑执行过程耗时较长,以得物为例,路由最多的业务线在初始化路由上的耗时已经来到了150+ms。


190.png


路由的注册逻辑本身是懒加载的,即对应Gr0up之下的首个路由组件被调用时会触发路由注册操作。然而ARouter通过SPI(服务发现)机制来帮助业务组件对外暴露一些接口,这样不需要依赖业务组件就可以调用一些业务层的视线,在开发这些服务时,开发者一般会习惯性的按照其所属的组件为其设置路由path,这使得首次构造这些服务的时候也会触发同一个Gr0up下的路由加载。


而在Application阶段肯定需要用到业务模块的服务中的一些接口,这就会提前触发路由注册操作,虽然这一操作可以在异步线程执行,但是Application阶段的绝大部分工作都需要访问这些服务,所以当这些服务在首次构造的耗时增大时,整体的启动耗时势必会随之增长。


2.1 ARouter Service路由分离


ARouter采用SPI设计的本意是为了解耦,Service的作用也应该只是提供接口,所以应当新增一个空实现的Service专门用于触发路由加载,而原先的Service则需要更换一个Gr0up,后续只用于提供接口,如此一来Application阶段的其他任务就不需要等待路由加载任务的完成。


001.png


2.2 ARouter支持并发装载路由


我们在实现了路由分离之后,发现现有的热点路由装载耗时总和是大于Application耗时,而为了保证在进入闪屏页之前完成对路由的加载,主线程不得不sleep等待路由装载完毕。


分析可知ARouter的路由装载方法加了类锁,因为他需要将路由装载到仓库类中的map,这些map是线程不安全的HashMap,相当于所有的路由装载操作其实都是在串行执行,而且存在锁竞争的情况,最终导致耗时累加大于Application耗时。


002.png


分析trace可知耗时主要来自频繁调用装载路由的loadInto操作,再分析这里锁的作用,可知加类锁是主要是为了确保对仓库WareHouse中map操作的线程安全。


003.png


因此我们可以将类锁降级对Gr0upMeta这个class对象加锁(这个class是ARouter apt生成的类,对应apk中的ARouterProviderProviderxxx类),来确保路由装载过程中的线程安全,至于在此之前对map操作的线程安全问题,则完全可以通过将这些map替换为concurrentHashMap解决,在极端并发情况下会有一些线程安全问题,也可以按照图中添加判空来解决。


009.png


010.png


至此,我们就实现了路由的并发装载,随后我们根据木桶效应对要预载的service进行合理分组,再放到协程中并发执行,确保最终整体耗时最短。


011.png


012.png


3.锁优化


Application阶段执行的任务多为基础SDK的初始化,其运行的逻辑通常相对独立,但是SDK之间会有依赖关系(例如埋点库会依赖于网络库),且大部分都会涉及读文件,加载so库等操作,Application阶段为了压缩主线程的耗时,会尽可能地将耗时操作放到子线程中并发运行,充分利用CPU时间片,但是这也不可避免的会导致一些锁竞争的问题。


3.1 Load so锁


System.loadLibrary()方法用于加载当前apk中的so库,这个方法对Runtime对象加了锁,相当于一个类锁。


基础SDK在设计上通常会将load so的操作写到类的静态代码块中,确保在SDK初始化代码执行之前就准备好了so库。如果这个基础SDK恰巧是网络库这类基础库,会被很多其他SDK调用,就会出现多个线程同时竞争这个锁的情况。那么在最坏的情况下,此时IO资源紧张,读so文件变慢,并且主线程是锁等待队列中最后一个,那么启动耗时将远超预期。


034.png


为此,我们需要将loadSo的操作统一管控并收敛到一个线程中执行,强制他们以串行的方式运行,这样就可以避免以上情况的出现。值得一提的是,前面webview的provider预加载的过程中也会加载webview.apk中的so文件,因此需要确保preloadProvider的操作也放到这个线程。


so的加载操作会触发native层的JNI_onload方法,一些so可能会在其中执行一些初始化工作,因此我们不能直接调用System.loadLibrary()方法来进行so加载,否则可能会重复初始化出现问题。


我们最终采用了类加载的方式,即将这些so加载的代码全部挪到相关类的静态代码块中,然后再去触发这些类的加载即可,利用类加载的机制确保这些so的加载操作不会重复执行,同时这些类加载的顺序也要按照这些so使用的顺序来编排。


78.png


除此之外,so的加载任务不建议和其他需要IO资源的任务并发执行,在得物App中实测这两种情况下该任务的耗时相差巨大。


4.启动框架优化


目前常见的启动框架设计是将启动阶段的工作分配到一组任务节点中,再由这些任务节点的依赖关系构造出一个有向无环图,但是随着业务迭代,一些历史遗留的任务依赖已经没有存在的必要,但是他会拖累整体的启动速度。


启动阶段大部分工作都是基础SDK的初始化,他们之间往往有着复杂的依赖关系,而我们在做启动优化时为了压缩主线程的耗时,通常都会找出主线程的耗时任务并丢到子线程去执行,但是在依赖关系复杂的Application阶段,如果只是将其丢到异步执行未必能有预期的收益。


99.png


我们在做完webview优化之后发现启动耗时并没有和预期一样直接减少了webview初始化的耗时,而是只有预期的一半左右,经分析发现我们的主线程任务依赖着子线程的任务,所以当子线程任务没有执行完时,主线程会sleep等待。


并且webview之所以放在这个时间点初始化不是因为有依赖限制这它,而是因为这段时间主线程正好有一段比较长的sleep时间可以利用起来,但是异步的任务工作量是远大于主线程的,即便是七个子线程并发在跑,其耗时也是大于主线程的任务。


因此想进一步扩大收益,就得对启动框架中的任务依赖关系做优化。


66.png


671.jpeg


以上第一张图为优化之前得物App启动阶段任务的有向无环图,红框表示该任务在主线程执行。我们着重关注阻塞主线程任务执行的任务。


可以观察到主线程任务的依赖链路上存在几个出口和入口特别多的任务,出口多表明这类任务通常是非常重要的基础库(例如图中的网络库),而入口多表明这个任务的前置依赖太多,他开始执行的时间点波动较大。这两点结合起来就说明这个任务执行结束的时间点很不稳定,并且将直接影响到后续主线程的任务。


这类任务优化的思路主要是:



  • 拆解任务自身,将可以提前执行或者延后执行的操作分出去,但是分出去之前要考虑到对应的时间段还有没有时间片余量,或者会不会加重IO资源竞争的情况出现;

  • 优化该任务的前置任务,让该任务执行结束的时间点尽可能提早,就可以降低后续任务等待该任务的耗时;

  • 移除非必要的依赖关系,例如埋点库初始化只是需要注册一个监听器到网络库,并非发起网络请求。(推荐)


可以看到我们在优化之后的第二张有向无环图里,任务的依赖层级明显变少,入口和出口特别多的任务也都基本不再出现。


044.png


320.png


对比优化前后的trace,也可以看到子线程的任务并发度明显提高,但是任务并发度并不是越高越好,在时间片本身就不足的低端机上并发度越高表现可能会越差,因为更容易出锁竞争,IO等待之类的问题,因此要适当留下一定空隙,并在中低端机上进行充分的性能测试之后再上线,或者针对高中低端机器使用不同的任务编排。


三、首页优化


1.通用布局耗时优化


系统解析布局是通过inflate方法读取布局xml文件并解析构建出view树,这一过程涉及IO操作,很容易受到设备状态影响,因此我们可以在编译期通过apt解析布局文件生成对应的view构建类。然后在运行时提前异步执行这些类的方法来构建并组装好view树,这样可以直接优化掉页面inflate的耗时。


601.png


602.png


2.消息调度优化


在启动阶段我们通常会注册一些ActivityLifecycleListener来监听页面生命周期,或者是往主线程post了一些延时任务,如果这些任务中有耗时操作,将会影响到启动速度,因此可以通过hook主线程的消息队列,将页面生命周期回调和页面绘制相关的msg移动到消息队列的队头,这样就可以加快首页首帧内容展示的速度。


102.png


详情可期待本系列后续内容。


四、稳定性


性能优化对App只能算作锦上添花,稳定性才是生命红线,而启动优化改造的又都是执行时机非常早的Application阶段,稳定性风险程度非常高,因此务必要在准备好崩溃防护的前提下做优化,即便有不可避免的稳定性问题,也要将负面影响降到最低。


1.崩溃防护


由于启动阶段执行的任务都是重要的基础库初始化,因此发生崩溃时将异常识别并吃掉的意义不大,因为大概率会导致后续崩溃或功能异常,因此我们主要的防护工作都是发生问题之后的止血


配置中心SDK的设计通常都是从本地文件中读出缓存的配置使用,待接口请求成功后再刷新。所以如果当启动阶段命中了配置之后发生了crash,是拉不到新配置的。这种情况下只能清空App缓存或者卸载重装,会造成非常严重的用户流失。


109.png
崩溃回退


对所有改动点加上try-catch保护,捕捉到异常之后上报埋点并往MMKV中写入崩溃标记位,这样该设备在当前版本下都不会再开启启动优化相关的变更,随后再抛出原异常让他崩溃掉。至于native crash则是在Crash监控的native崩溃回调里执行同样操作即可。


1100.png
运行状态检测


Java Crash我们可以通过注册unCaughtExceptionHandler来捕捉到,但是native crash则需要借助crash监控SDK来捕捉,但是crash监控未必能在启动最早的时间点初始化,例如Webview的Provider的预加载,以及so库的预加载都是早于crash监控,而这些操作都涉及native层的代码。


为了规避这种场景下的崩溃风险,我们可以在Application的起始点埋入MMKV标记位,在结束点改为另一个状态,这样一些执行时间早于配置中心的代码就可以通过获取这个标记位来判断上一次运行是否正常,如果上次启动发生了一些未知的崩溃(例如发生在crash监控初始化之前的native崩溃),那么通过这个标记位就可以及时关闭掉启动优化的变更。


结合崩溃之后自动重启的操作,在用户视角其实是观察不到闪退的,只是会感觉到启动的耗时约是平时的1-2倍。


0456.png
配置有效期


线上的技改变更通常都会配置采样率,结合随机数实现逐渐放量,但是配置下发SDK的设计通常都是默认取上次的本地缓存,在发生线上崩溃等故障时,尽管及时回滚了配置,但是缓存的设计会导致用户还会因为缓存遭遇至少一次的崩溃。


为此,我们可以为每一个开关配置加一个配套的过期时间戳,限制当前放量的开关只在该时间戳之前生效,这样在遇到线上崩溃等故障时确保可以及时止血,而且时间戳的设计也可以避免线上配置生效的滞后性导致的crash。


457.png


用户视角下,添加配置有效期前后对比:


678.jpeg


五、总结


至此,我们已经对安卓App中比较通用的冷启动耗时案例做了分析,但是启动优化最大的痛点往往还是App自身的业务代码,应当结合业务需求合理的进行任务分配,如果一味的靠预加载,延迟加载和异步加载是不能从根本上解决耗时问题的,因为耗时并没有消失只是转移,随之而来的可能是低端机启动劣化或功能异常。


做性能优化不仅需要站在用户的视角,还要有全局观,如果因为启动指标算是首页首帧结束就把耗时任务都丢到首帧之后,势必会造成用户后续的体验有卡顿甚至ANR。所以在拆分任务时不仅需要考虑是否会和与其并发的任务竞争资源,还需要考虑启动各个阶段以及启动后一段时间内的功能稳定性和性能是否会受之影响,并且需要在高中低端机器上都验证下,至少要确保都没有劣化的表现。


1.防劣化


启动优化绝不是一次性的工作,它需要长时间的维护和打磨,基础库的一次技改可能就会让指标一夜回到解放前,因此防劣化必须要尽早落地。


通过在关键点添加埋点,可以做到在发现线上指标劣化时迅速定位到劣化代码大概位置(例如xxActivity的onCreate)并告警,这样不仅可以帮助研发迅速定位问题,还可以避免线上特定场景指标劣化线下无法复现的情况,因为单次启动的耗时波动范围最高能有20%,如果直接去抓trace分析可能连劣化的大概范围都难以定位。


例如两次启动做trace对比时,其中一次因为遇到IO阻塞导致某次读文件的操作都明显变慢,而另一次IO正常,这就会误导开发者去分析这些正常的代码,而实际导致劣化的代码可能因为波动正好被掩盖。


2.展望


对于通过点击图标启动的普通场景,默认会在Application执行完整的初始化工作,但是一些层级比较深的功能,例如客服中心,编辑收货地址这类,即使用户以最快速度直接进入这些页面,也是需要至少1s以上的操作时间,所以这些功能相关的初始化工作也是可以推迟到Application之后的,甚至改为懒加载,视具体功能的重要性而定。


通过投放,push来做召回/拉新的启动场景通常占比较少,但是其业务价值要远大于普通场景。由于目前启动耗时主要来源于webview初始化以及一些首页预载相关的任务,如果启动落地页并不需要所有基础库(例如H5页面),那么这些我们就可以将它不需要的任务统统延迟加载,这样启动速度可以得到大幅度增长,做到真正意义上的秒开。


*文/Jordas


作者:得物技术
来源:juejin.cn/post/7306447634204770319
收起阅读 »

终于把国外大佬的跨窗口量子纠缠粒子效果给肝出来

web
前言 上篇文章 尝试实现了国外大佬用Web做出来跨窗口渲染动画效果反响很大,但是仅仅只是实现了跨窗口动画效果,严格说就没有动画,还有些bug和遗憾,尤其是粒子效果,得入three.js坑,怎么办?跳啊! 硬肝了两天,实在肝不动了,看效果吧。 第一版v2效果,大...
继续阅读 »

前言


上篇文章 尝试实现了国外大佬用Web做出来跨窗口渲染动画效果反响很大,但是仅仅只是实现了跨窗口动画效果,严格说就没有动画,还有些bug和遗憾,尤其是粒子效果,得入three.js坑,怎么办?跳啊!


硬肝了两天,实在肝不动了,看效果吧。


第一版v2效果,大粒子,粒子数量较少:
v2 (1).gif


第二版v2.1,小粒子,粒子数量多:
v2.1 (1).gif


three.js


官方文档:threejs.org/,中文文档:three.js docs


我第一次接触three.js,之前只是听说过,比如能弄车展啥的,感觉很厉害,就想借此机会学习下,跳进坑里才发现,这坑也太深了。随便找个教程,里面各种名词就给我弄吐了。


先按文档 three.js docs 画个立方体,跑起来了,但是我想要球体啊,还有粒子围着中心转,这么多api学不起啊,搜教程也是杂乱无章,无从学起,咋整?找现成的啊!


看到官网有很多现成的例子,找了一个相近的:threejs.org/examples/#w…,截了个静态图长这样:
image.png


找源码:three.js/examples/we…,copy到本地,代码就200行,用到的three api就几个,搜下api大概了解代码各部分的功能,基本都能看懂,然后删了多余功能,一个粒子绕球中心旋转功能就出来了。


现在关于粒子移动、渲染、球体旋转缩放等变化api都已经基本搞懂了,然后就是痛苦折磨的计算调试了,不想再回忆了。


动画效果的移动都是靠循环对象、计算坐标,改变粒子的position来实现的,感觉应该会有更好的现成api能简化这个过程,而且有缓冲、阻尼效果等。如果有更好的例子,欢迎大佬分享。


总结下用到的api吧,就几个:


构造方法



  1. THREE.PerspectiveCamera:透视投影相机,3D场景的渲染中使用得最普遍的投影模式。

  2. THREE.SceneTHREE.WebGLRenderer:场景和渲染器。

  3. THREE.TextureLoader:创建纹理,用于加载粒子贴图。

  4. THREE.SpriteMaterial:创建精灵材质。

  5. THREE.Sprite:创建精灵,用于表示粒子。

  6. THREE.Gr0up:创建对象容器,用于整体控制多个粒子,达到旋转等效果。


属性



  1. .position.x\y\z:坐标位移;

  2. .rotation.x\y\z:粒子绕球体旋转;

  3. .position.multiplyScalar(radius):对三个向量x\y\z上分别乘以给定标量radius,用于设置粒子距球体中心距离;

  4. .scale.set:设置粒子自身的缩放

  5. .visible:控制Gr0up或粒子显隐;


难点


THREE.PerspectiveCamera透视投影相机下,由于是模拟人的眼睛从远处看的,所以会导致坐标上的单位跟html里的像素单位是不一致的,有一定的比例。但是判断浏览器窗口位置都是像素单位的,所以得算出这个比例、或者找到一种办法让两个单位是一致的。在外网搜到一个方案:forum.babylonjs.com/t/how-to-se…


const perspective = 800;
const fov = (180 * (2 * Math.atan(window.innerHeight / 2 / perspective))) / Math.PI;
const camera = new THREE.PerspectiveCamera(fov, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.set(0, 0, perspective);

这样设置相机后,俩个单位就是一致的。至于原理。。。看见Math.atan就头大,过!


BroadcastChannel


BroadcastChannel的api很简单,在一个窗口中postMessage,另一个窗口就会通过message事件接受到了。


const channel = new BroadcastChannel('editor_channel');
channel.postMessage({ aa: '123' });
channel.addEventListener('message', ({ data }) => {
console.log(data);
});

在此例逻辑是,进页面初始化时、或者坐标改变时,需要把当前窗口坐标postMessage发送到别的窗口中,然后再把所有窗体坐标数据都存在js全局变量里使用。


但是这里有个问题,如果刷新其中一个窗口时,没办法立即获取别的窗口数据,因为别的窗口只有在坐标变化时才会发送数据(为了提高效率,不会在requestAnimationFrame里一直发数据),这样就得主动postMessage一个标记到别的窗口,然后别的窗口再把自己的数据postMessage回来,是个异步过程,有些繁琐。


使用上不比LocalStorage简单多少,不过BroadcastChannel确实可以解决LocalStorage的全局影响和缓存不自动清空问题。有兴趣可以自己实现下。(可以重写storage.js里方法)


优化窗口数据监听与更新



  • 注册window storage事件,监听storage变化时(当其它窗口位置变化时),判断最新窗口总数,当数量变化时,在当前窗口重新实例化所有球体及粒子对象。

  • 注册window resize事件,更新摄像机比例和渲染器size。

  • 将所有窗口数据保存在js全局变量里,用于在requestAnimationFrame中读取渲染动画,并且只在需要时更新:

    • 其它窗口位置变化时(通过window storage事件);

    • requestAnimationFrame中判断当前窗口位置变化时(比较全局变量与当前window位置),更新全局变量和storage;




通过以上逻辑优化,可以有效提高渲染速度,减少代码重复执行,减小客户端压力。


待改进



  1. three.js实现上:学习的还是太浅了,有些动画效果应该会有更好的实现方式,希望有大佬能指点下。

  2. three.js效果:跟国外原大佬比不了,他那是粒子,我这个就是个球。

  3. 拖动窗口位置时的球体移动阻尼效果,这个实现了下,有了个效果,但是卡顿感明显,不顺畅,而且在连线动画下效果更差。

  4. 当改变窗口大小时,球体大小会随着窗口大小变化,想固定大小没找到解决方法,然后计算球体位置也没有考虑窗体大小,所以现在多窗口要求窗口大小必须是一样的。

  5. 球体之间的连线粒子移动效果不佳,特别在窗口移动时,还需优化算法。


总结


总结下相比之前的例子 尝试实现了国外大佬用Web做出来跨窗口渲染动画效果,有以下提升:



  1. 引入three.js,画出了球体、粒子旋转动画,多窗口球体,球体间粒子连线动画效果。

  2. BroadcastChannel代替LocalStorage。(技术选型没选上,未实现)

  3. 支持多个窗口(理论上没有限制),并且窗口重叠时不会有连线缺失。


跨窗口通信、存储窗口坐标、在每个窗口画出所有球体和连线,这个机制流程已经很成熟了,没有太大的优化提升空间了,所以要实现国外大佬视频效果,就只剩three.js了,实在是学不动了,水太深。


源码已上传至GitHub,代码里有详细注释,希望能有所帮助:github.com/markz-demo/…


做了两版效果,可以通过代码里注释查看效果,README.md 中有说明。


Demo:markz-demo.github.io/mark-cross-…


作者:Mark大熊
来源:juejin.cn/post/7307057492059471899
收起阅读 »

拼多多股价首超阿里,电商行业的史诗级时刻

拼多多股价首超阿里,这绝对算是电商行业史诗级的颠覆时刻,以下图为证: 还记得2004年,将近20年前的那个年度经济人物颁奖现场吗?马云说出了那句很经典的话:“我就是打着望远镜也找不到对手。” 现在,马云已经不需要那个望远镜了,可以在闲鱼二手交易平台把望远镜卖...
继续阅读 »

拼多多股价首超阿里,这绝对算是电商行业史诗级的颠覆时刻,以下图为证:



还记得2004年,将近20年前的那个年度经济人物颁奖现场吗?马云说出了那句很经典的话:“我就是打着望远镜也找不到对手。”


现在,马云已经不需要那个望远镜了,可以在闲鱼二手交易平台把望远镜卖掉了。


其实早在11月28日,拼多多股价暴涨18%的时候,有个阿里员工在内网发帖,说了如下言论:


此刻难眠,也不敢想,拼多多市值直接来到1855亿美金,相比我们的1943亿,差距仅80亿,着实吓一跳。那个看不起眼的砍一刀,快成老大哥了。本不想发出这个没内容营养的帖子,但思绪下,还是留下这个帖子,用作备忘,也用作自己的勉励。期望和集团兄弟一起努力贡献点滴,超越回来。


随后,有阿里同事进行跟帖说:


简单买、简单退,少一点套路、多一点实惠,从未感觉用天猫、京东比用拼多多就显得高端了。让天下没有难做的生意的初衷,本就应该是服务和成就更广大的人民群众么?


我相信,此时此刻老阿里人的心情是复杂且沉重的。后来,马云也在内网进行了回帖打气,原话如下:


特别好。请大家多提建设性意见和建议。特别是创新想法。我相信今天的阿里人大家都在看都在听。我更坚信阿里会变,阿里会改。所有伟大的公司都诞生在冬天里。AI电商时代刚刚开始,对谁都是机会,也是挑战。


要祝贺pdd过去几年的决策,执行和努力。谁都牛x过,但能为了明天后天牛而改革的人,并且愿意付出任何代价和牺牲的组织才令人尊重。回到我们的使命和愿景,阿里人,加油!合伙人马云。


不得不说,这时的马云还能祝贺拼多多,至少企业家的格局和底蕴还是有的。


今年完全不一样


前几年,中国这四大电商巨头(阿里、拼多多、京东和美团),它们的股价走势基本上都是一个模子刻出来的,一涨俱涨,一跌俱跌,无非是涨跌幅上下差一点儿而已。


但是今年完全不一样,原本是小老弟的拼多多忽然发力,不仅把美团和京东远远甩在身后,甚至去试图掀翻阿里在电商行业的霸主地位了。


甚至开始有股民说:“阿里是电商界的诺基亚,而拼多多则是后来居上的颠覆者——苹果。”


还有的股民出来现身说法:“拼多多,淘宝,京东用户质量对比,同一款鞋,同样价格,淘宝评论200+,带图39,京东评论500+,带图10+,多多评论1146,带图883。多多的用户质量和配合度是最高的,多多赢。”


总之就是一句话,成者王侯败者寇,股价涨了,怎么看怎么顺眼。


外行看热闹,内行看门道,作为一个在电商行业浸淫多年的老枪,我还是尽量从更多的角度来分析一下。


(1)消费降级的大环境


我们可以看下,保证正品和用户体验的京东股价跌得最狠,而主打低端廉价的拼多多股价涨得最猛,不得不说,这离不开经济放缓,消费降级的大环境,以及用户对未来的悲观态度。


现在的年轻人,不但房子不买了,汽车不买了,奢侈品的包包香水不买了,日餐西餐也都不吃了,甚至前两天,还出了个个这样的新闻,“涨价的羽绒服把市场让给了军大衣”,此中缘由自不必多说。



在此大环境下,虽然阿里也出了个“淘特”,但整体的动作和布局显然已经慢了很多,拼多多的廉价的烙印已经深深地刻在了用户的心中,并不是那么轻易就被撼动的。


(2)员工的驱动欲望


拼多多员工的平均年龄是27岁,而阿里员工则是31岁,四年之差,做事情的驱动欲望是完全不一样的。



有人会说,别逗了,不就差个四年吗,让你说的跟70后和90后的差距似的。


但别忘了,从25岁硕士毕业到35岁的职场魔咒,也仅仅不过十年而已,这样算起来,你还觉得四年时间短吗?


27岁的时候,大多数互联网人还没结婚,父母的年龄也基本在60岁以下,正是心无旁骛地一门心思奔事业的时候,往往就是吃饭睡觉干工作,公司家里两点一线的状态。


而31岁的时候,互联网人的结婚比例会增加很多,其中有相当一部分人孩子正好一两岁,又有一部分的父母开始往医院跑。这些都是非常牵扯精力的事情,再想达到“生死看淡,不服就干”的工作状态,已经是完全不可能了。


况且30岁+的年龄,应该充分地体验了职场的冷暖百态,年轻时的鸡血已经褪去,老板的那些洗脑言论也已经是左耳听风。


取而代之的是,他们开始对并不久远的35岁现象有了哲学性的思考,“到了那个时候我要去干什么”、“到了那个时候我应该到哪里去”等等。


尤其是看到公司里一波又一波裁员的时候,这种思考甚至变成了穷极所思。而所有的这些,都大大地影响了他们的战斗力。


(3)创始人的势能和心力


2023年,黄铮43岁,马云59岁。再牛逼的人,也终究敌不过时间。在黄铮的狼顾鹰视下,马云也会有力不从心的一天。


就像麦克阿瑟在西点军校的告别演说中,最经典的那一句:“老兵永远不死,只会慢慢凋零(Old soldiers never die,they just fade away)。”



在唯快不破的互联网行业,能够称霸电商领域20年,其实马云的成就已经旷古烁今了。


结语


阿里和拼多多的这场双雄争霸究竟鹿死谁手,将会以怎样的形态进行演变,让我们拭目以待吧。


作者:库森学长
来源:juejin.cn/post/7307026637823066122
收起阅读 »

突发:鸿蒙之祖华为在 openInula 官网声称可“避免重复运行组件”但网友挖出“组件渲染次数”是写死的

消息来源 看到群里这个消息,我的想法是:群里怎么炸锅了?华为之前的鸿蒙被指疑似安卓二开不说,现在出个 openInula 好像是什么欧拉又被人挖出幺蛾子?哦有 la 后缀但好像又不像欧拉。 到底怎么回事?我也不敢说话甚至都不敢参与讨论。 求真过程 不过华为好...
继续阅读 »


消息来源



看到群里这个消息,我的想法是:群里怎么炸锅了?华为之前的鸿蒙被指疑似安卓二开不说,现在出个 openInula 好像是什么欧拉又被人挖出幺蛾子?哦有 la 后缀但好像又不像欧拉。


到底怎么回事?我也不敢说话甚至都不敢参与讨论。


求真过程


不过华为好不好是个大企业,并且又是风口浪尖一样的存在,褒贬两级分化。真,搞得我一直到现在我都不知道遥遥领先到底是一个什么词语,时常怀疑我自己是不是出轨了。


官网现象


我打开 openInula 的官网 http://www.openinula.net/ ,看样子还是很高大尚的。


Alt text


上来就是 相比传统虚拟DOM方式,提升渲染效率30%以上。完全兼容React API,支持React应用无缝切换至openInula。 这种兼容传统但吊打传统的描述,很难让人不把他是电、他是光、他是唯一的希望等联想在一起。


继续向下:我们看网友说的造假现象到底是不是存在。


我小心翼翼的向下滑动页面,目不转睛的注视着每一个窗口,全神贯注的查找目标文字组件渲染次数,内心忐忑不安的希望这不是真的。


但是,现实总是这么残酷,左右两栏的对比里,左边的数据是动态的,右边为什么就要写死?难道页面在跳动开发测试都看不出来吗?为了避免是我的错觉问题,我决定对其 GIF 录制:



注意看

注意看

注意看,组件渲染次数1与源码中定死的1。



Alt text


好了,网友没说错。从结果上来看,数据确实就是写死的。这种行为如果用在官网和对比上,确实很难让人接受。


但是我还注意到一个问题,官网上并没有华为的大 logo,那么这东西到底是不是华为的?别啥脏水都往华为身上泼好吧!


然后我又再次陷入沉思:与华为什么关系?


inula 与华为的关系




9月21日,在华为全联接大会2023开源分论坛上,华为表示国内软件根技术创新之势已起,目前处于战略机遇期,有较大的市场空间。在这一契机下,华为发布了国内首个全面自研密码套件openHiTLS及全场景智慧前端框架openInula。这两款开源基础中间件助力软件根技术自主创新,对构筑业务数字化的核心竞争力有重要意义。



开发团队合影:


Alt text



华为ICT开源产业与生态发展部总经理周俊懿在发布会上表示,“国内软件根技术创新之势已起,正处于发展战略机遇期。在此我们发布更快、更简单、更智能的新一代智慧前端框架openInula,构筑前端生态创新底座,共建国内前端繁荣生态。”



Alt text



目前,华为公司内部已有多个产品采用openInula作为其前端框架运用于实际商用项目,支撑上千万行前端业务代码。openInula带来的性能提升解决了产品较多的前端性能瓶颈问题,保证了项目健康、高效地运行。



功能说明PPT:


Alt text


根据以上图文,我暂且觉得可以理解为 openInula 和华为有一定关系,是华为公司或旗下的团队。


简直不敢相信自己的眼睛!


Alt text


肯定是什么地方弄错了,openInula 就是有这么牛笔,人家发布会在那,官网在那,仓库在那,地面在那,还有假?BUG!肯定是BUG!


于是我开始从试图从代码上来实践看看,他确实 比传统虚拟DOM渲染效率更高、渲染次数更少


代码实践一


根据官网的步骤,我开始 npx create-inula <项目名>,这样就完全使用官方的脚手架模板生成项目,避免误差。



然后根据官方指示运行命令 npm run start



这怎么回事?我还没进入状态你就结束了?


Alt text


咦?不对,好像打开方式不对。


看到了 node:path 勾起了我仅有的记忆,大概是 node 版本过低。


这个我熟啊!于是我直接打开 package.json 文件,并加入以下代码:


"env": {
"node": "18.18.2"
}

然后再次运行命令,这时候项目就在 node v18 的环境下运行成功啦。




注意看

注意看

注意看,上图中在 package.json 中声明了当前项目依赖的环境。



当我打开控制台 url 时,页面并没有问题(没有使用官网声明的响应式API)。然后当我把官网上的响应式API示例代码放过来的时候:



啊啊啊!页面一片空白。


然后发现官网声明中的代码:


import { useRef, useReactive, useComputed } from 'openinula';

function ReactiveComponent() {
const renderCount = ++useRef(0).current;

const data = useReactive({ count: 0 });
const countText = useComputed(() => {
return `计时: ${data.count.get()}`;
});

setInterval(() => {
data.count.set(c => c + 1);
}, 1000);

return (
<div>
<div>{countText}</div>
<div>组件渲染次数:{renderCount}</div>
</div>

);
}

其中 openinula 的 useReactive, useComputed 两个 api 都是 undefined。


官网 api 文档中全局搜索 useReactive 也无任何讯息。


Alt text


好了,我累了,毁灭吧。因为它可能还在 ppt 里或者还没有发布。


然后我就开始相关如何结束本文,如何思考如何更中立一些,应放入哪些参考材料。


比如我准备去找代码仓库、去找 npm 官方以及其他镜像仓库来大概看看 openinula 的下载量时:



这是什么情况?



  • 版本 0.0.1,总历史版本 2个 -- 代表很新很新。难道是内网发布?

  • 周下载量 65 -- 代表 npm 上很少很少人用。 说好的支撑上千万行前端项目呢?难道是内网人用?

  • 代码仓库为空 -- ???

  • readem 为空 -- ???


有一说一,如果就上面这样子的话,真的是一点都不像官网所描述的光环拉满的样子。


真的,说实话到这里我的失望值已经累计到 0.2 了。


但是我真的还想再挣扎一下:难道是因为这是国产框架,所以大都使用国内镜像,所以 npm 上的下载量少?


然后去大家都在用的淘宝镜像上看了一下:



我再次被震惊了,请看图片左边是淘宝镜像,为 0.0.4 版,右边是 npm 惊喜,为 0.0.1 版本。大家都知道,通常都是 taobao 镜像比 npm 仓库更新要慢一些。但 openinula 在这里怎么就直接遥遥领先了 3 个版本?



虽然不理解,但大受震撼。不过,如果这是假的,我希望这是真的(因为我可以试试新的0.0.4版本)。如果这是真的,我希望这是假的,因为这太反常了。


image-5.png


所以,那就再试试淘宝镜像上的 0.0.4 是不是遥遥领先,吊打传统虚拟 dom hook api 吧。


代码实践二


这下我们直接:


nrm use taobao
npm i openinula@latest
npm run start


然后仍然是:



Alt text


页面一片空白,直接报错:jsx-dev-runtime 404 。



注意看

注意看

注意看:上图浏览器网络控制台。



真的,我累了,毁灭吧。


结论


确实在官网上,传统 Hook API 一栏中组件渲染次数是动态改变的,而在响应式 API声称响应式 API 避免重复运行组件,无需对比虚拟DOM树,可精准更新DOM,大幅提升网页性能。中组件渲染次数是写死的


但是,这么做的原因到底是什么,有有意为之,还是不小心写错了?就得继续等待后续了。


我斗胆猜测一下,有几个可能:


一:还没有准备好


虽然从发布会上和官网上来看,光环拉满,但从已发布的包、仓库、文档来看,还亟待完善……


二:失误


失误就是,真的那个地方是要再加个 t,让其出现动态渲染的效果。不过我尝试修复了这个问题,把 t 加上,也发现只是简单的定时器,所以应该不是少加个 t 的事情。




注意看

注意看

注意看上面的动图中,响应式 API 真的动起来了!



那种有没有可能是,这个地方是真的想做个动态渲染效果比较,但还没做出来?


另外,根据官方代码仓库中的源码(是活跃分支,最近提交2小时前)看来:


readme 中的开发工具都还是随手写的错误网址:


Alt text


package.json 声明当前版本为 0.0.1 (那 taobao 上的 0.0.4 是怎么回事)。
Alt text


三:有意为之


在不少关于 Inula 的文章,以及发表大会中,都是以响应式、区别传统虚拟DOM、提高效率号称的。并且代码也是开源的,如果是如实开源的情况下,到底效果是不是如官网所说,大佬们一挖代码便知。虽然目前官网也没有提供详细评测仓库,或三方评测结果。


可能真是有那么强的,并且也是有意在官网中用渲染次数1来体现效果以达到视觉要求,但没想到有些程序员非要扒代码看说这个地方一定要写个实例动态渲染出来。


参考



作者:程序媛李李李李李蕾
来源:juejin.cn/post/7307125383786119209
收起阅读 »

历时一个月,终于找到自己满意的工作了

由于公司经营遇到了巨大问题,出现严重亏损。 不得不忍痛告诉全体员工团队解散一事 衷心感谢全体小伙伴们在公司付出努力与汗水 目前待定截止今日,后续事宜人事小姐姐会逐步跟进安排。 再次感谢各位伙伴们 希望大家都能有一个好的前程,在未来的时候一定要努力奋斗,前程似锦...
继续阅读 »

由于公司经营遇到了巨大问题,出现严重亏损。
不得不忍痛告诉全体员工团队解散一事
衷心感谢全体小伙伴们在公司付出努力与汗水
目前待定截止今日,后续事宜人事小姐姐会逐步跟进安排。
再次感谢各位伙伴们
希望大家都能有一个好的前程,在未来的时候一定要努力奋斗,前程似锦



2023年6月16日我正在开开心心的写着代码,突然来了这么一条消息;我直接原地呆住了,沉思良久我才发现办公室内寂静的可怕,没有了键盘的敲击声;有的只有同事们的呼吸声,大家都依靠在椅子上,像是在思考什么。很快大家也都接受了这个不愿看到的实事。过了一两天就在开始办理离职一类的事情了。


投递简历


在离职以后,我们同事之间约好一起打了2个小时的球,吃了一个饭;在我们经常唱歌的地方,唱了几个小时歌,我们玩的很开心、吃的很开心,就像从来没有经历过这件事情一样。


在过了一段时间以后,我开始修改简历投递简历了。沟通的第一天就给我狠狠的打了一次脸,我在沟通了20/30个公司的时候,恢复我消息的不足5个,已读的大约有一半左右;后面我就开始加大了沟通的力度;大约在沟通了140家以后,收到了第一个面试机会。


然后第二个面试机会是在人才市场去找的。
第三个面试机会是在沟通了60多家以后得到的机会也是我满意的一家。


第一家


该公司是一家外包公司在成都的分公司,我进去面试的时候没有笔试题,直接是主管来面试的,主要的问题还是围绕业务层面以及上家的一些工作经历,然后就是一些关于vue的一些原理以及简单的算法问题。当时在面试完以后我自己感觉很好;觉得肯定能面试上,结果真的面试上了;下午的时候这家公司就给我打电话了,给出的工资是11k;但是我觉得外包不是很喜欢,而且是单双休,后面就给拒绝了,没想到周一的时候人事又给我打电话了,说主管这边商量了以后决定给你涨一千;请问你愿意来吗?说实话,当时是真的心动了;我考虑了一天以后还是拒绝了,因为我实在是不喜欢外包。


第二家


该公司属于半外包性质的,经历了两面,第一面是技术面(超级简单),第二面是主管过来的(主管是后端)面试的,第二面主要就是业务方面的,当时说的是智慧数字一类的产品,我当时确实被该概念吸引了,后面再谈了以后,发现并没有给我offer,我就发了一个消息过去问,然后人事告诉我说是:工资要的太高了,公司给不了,然后我说可以调薪;在多次沟通后给到了9.5k;然后半年有一次涨薪的机会、年底双薪以及试用期交社保一类的。我进去待了几天发现他们并不是什么智慧数字,而且技术用的不是很好的,所以我就放弃了,我个人觉得对我的技术提升没有太多的帮助


第三家


该公司是一家完全自研的公司,并且产品已经上线,用户量达到了千万,日活也有10多20万的样子;让我觉得很不错,所以在知晓了之后就对自己说一定要好好面一定要进去,哈哈哈


在该公司经历了三次面试吧!


第一次是技术面,问的问题也是一些业务问题;然后会涉及到一些js的基础原理以及vue中的一些实现原理等等问题(主要是大部分都忘了,哈哈哈);


然后第二次就是人事小姐姐问了一些问题,问了问题以后,人事小姐姐叫来了一个领导然后跟我谈,主要谈的话就是一些收获啊、自豪感啊、研究等等问题。


在面试完以后都过去了4个小时了,又遇到了下大雨;我骑上我的小电驴穿梭在城市的街头,却充满了一点小小的期待。


过了几个小时以后,通知我说面试通过了,试用期也是有社保、有年终、也有涨薪制度等等


补充:在这家公司中可能工资不高,但是这家公司的技术能力比较强而且用的很多技术我都不会,并且我也很想去学习此类的技术,刚好有这个机会,所以我是很开心的。


总结


以上是我这段时间面试的一些经历;但是工作确实并不好找。


主要原因还是面试机会少,很多公司都要求本科以及本专业等等,其实面试的话都还好基本上跟原来差别不大,还有就是对刚出来的这些小伙伴可能不是很友好。所以希望大家如果有工作的话,就先好好上班吧!目前大环境都是这样的,加油哦!


作者:雾恋
来源:juejin.cn/post/7263274550074769465
收起阅读 »

滴滴崩溃,损失几个亿的k8s 方案

起因从震惊吃瓜开始 从 2023 年 11 月 27 日晚上 10 点左右截止 2023 年 11 月 28 日中午 12 点期间,DD发生了长达12小时的p0级bug,造成的影响大家通过各种平台或者亲身经历如何我就不多说了,单说对企业造成的损失超千万单和超4...
继续阅读 »

起因从震惊吃瓜开始


从 2023 年 11 月 27 日晚上 10 点左右截止 2023 年 11 月 28 日中午 12 点期间,DD发生了长达12小时的p0级bug,造成的影响大家通过各种平台或者亲身经历如何我就不多说了,单说对企业造成的损失超千万单和超4个亿的交易额。我只想说不愧是大企业,这也太狠了


简单整理下崩溃原因


DD自己在微博上说的是底层系统软件发生故障,身为底层开发的我对此还是挺感兴趣的,所以简单吃了下瓜,网传是滴滴未正常升级k8s导致集群崩溃,且由于集群规模过大(相信这么大规模集群一定跑着相当多的业务)导致造成影响肯定很大


滴滴.png


DD在微博的致歉中说是底层系统软件故障


网传.png


网传是因为升级导致的故障


恰巧DD技术在公众号上曾经发布过一篇# DD弹性云基于 K8S 的调度实践文章,文章里介绍了他们选择的升级方案,以及如此选择升级方案的原因


image.png


DD的升级方案


dd 不愧是大厂,还有这么老版本的k8s集群,估计是很早就开始引入k8s集群了。

通用的解决方案


首先两种方案的对比,DD已经在他们的技术文章中给明了优缺点,身为一个菜鸟我估计是不适合评论别人的方案,所以我只从我实际工作中遇到类似的问题是如何解决的,


问题一 集群规模过大


kubernetes 官方推荐了5000个node 上限,虽然并不代表超出上限一定会出问题,但是此次事故明显告诉我们超出上限的集群一旦发生事故有多可怕了


通用的方案


实际生产环境当集群规模达到上限我们一般是怎么处理的呢,很简单——联邦集群,让多个集群打通成联邦集群,网络和k8s资源互通,提高了业务容纳的上限,同时将风险分摊给多个集群。增加了些许运维压力,但是明显要比疯狂给单个集群加节点要安全多了


问题二 如何选择升级方案


目前我遇到的大规模集群,基本上都是像dd 这样选择晚上的窗口期升级的,这点倒是没什么可说的,但是很少有直接原地升级的,基本上都是有备份升级的,流量也不会直接全部涌入升级后的集群的,要经过逐步验证才会切换到新集群的,原地升级我只能说是艺高人胆大了。


通用的方案


从dd 的技术博文上能猜出来,原地升级的方案肯定是经过他们内部验证了,最起码短期内是没出问题,才敢拿到生产集群上实践,但是很抱歉生产集群的扛风险能力还是太小了,所以还是建议老老实实选择替换升级的方案吧


问题三多控制节点


最后一点就是网传的控制节点崩溃的问题,我觉得这太离谱了,这种大厂应该知道多master 节点,以及master 不在同一机房的问题吧,不说多数据中心方案,基本的灾备思想还是要有的吧


胡言乱语


最近好像很多大厂的产品崩溃,先是阿里后是滴滴,加上最近的裁员潮,网上流出了很多笑话最知名的莫过开猿节流,降本增笑。诚然互联网企业最大成本就是人力成本,当业务成熟后开掉开发人员来降低成本似乎是一个不错的方案。但是当企业剩下的大部分都是ppt高手,真正干活的人黯然退场。如此这般难免会遇到这样那样的技术问题。希望老板领导们能慎重裁员,尊重技术。


最后希望各位程序员技术越来越稳,默默奉献的同时也能有自己的收获


作者:萌萌酱
来源:juejin.cn/post/7306832876381437991
收起阅读 »

别再担心数据丢失了!学会使用MySQL事务,保障数据安全!

在日常开发中我们经常会遇到需要同时处理多个操作的情况,比如在购物时,我们需要同时完成支付和更新库存两个操作。这时,如果其中一个操作失败了,我们就需要进行回滚,以保证数据的一致性。那么,如何在MySQL中实现这样的功能呢?答案就是——事务。下面我们就来介绍一下M...
继续阅读 »

在日常开发中我们经常会遇到需要同时处理多个操作的情况,比如在购物时,我们需要同时完成支付和更新库存两个操作。这时,如果其中一个操作失败了,我们就需要进行回滚,以保证数据的一致性。

那么,如何在MySQL中实现这样的功能呢?答案就是——事务。下面我们就来介绍一下MySQL事务是什么?它是如何使用的?

一、什么是事务?

事务定义

  • 事务是一个最小的不可再分的工作单元;通常一个事务对应一个完整的业务(例如银行账户转账业务,该业务是一个最小的工作单元)。

  • 一个完整的业务需要批量的DML(insert、update、delete)语句共同联合完成。

  • 事务只和DML语句有关,或者说DML语句才有事务。这个和业务逻辑有关,业务逻辑不同,DML语句的个数不同。

事务是什么?

往通俗的讲就是,事务就是一个整体,里面的内容要么都执行成功,要么都不成功。不可能存在部分执行成功而部分执行不成功的情况。

就是说如果单元中某条sql语句一旦执行失败或者产生错误,那么整个单元将会回滚(返回最初状态)。所有受到影响的数据将返回到事务开始之前的状态,但是如果单元中的所有sql语句都执行成功的话,那么该事务也就被顺利执行。

比如有一个订单业务:

1.订单表当中添加一条记录 2.商品数量数据更新(减少) 3…

当多个任务同时进行操作的时候,这些任务只能同时成功,或者同时失败。

二、事务的特性

事务有四个特性:一致性、持久性、原子性、隔离性。下面分别来解释一下这四个特性都有什么含义。

原子性

事务是一个不可分割的工作单位,要么同时成功,要么同时失败。例:当两个人发起转账业务时,如果A转账发起,而B因为一些原因不能成功接受,事务最终将不会提交,则A和B的请求最终不会成功。

持久性

一个事务一旦提交成功,它对数据库中数据的改变将是永久性的,接下来的其他操作或故障不应对其有任何影响。

隔离性

一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

一致性

事务执行接收之后,数据库完整性不被破坏。

注意:只有当前三条性质都满足了,才能保证事务的一致性。

Description

刷脏: Mysql为了保证存储效率,于是每次将要读写的文件是先存储在缓存池中,对于数据的操作是在缓存池中,而mysql将会定期的刷新到磁盘中。

三、事务的使用

事务是如何保证操作的完整性的呢?

其实事务执行中间出错了,只需要让事务中的这些操作恢复成之前的样子即可, 这里涉及到的一个操作,回滚(rollback)。

事务处理是一种对必须整批执行的 MySQL 操作的管理机制,在事务过程中,除非整批操作全部正确执行,否则中间的任何一个操作出错,都会回滚 (rollback)到最初的安全状态以确保不会对系统数据造成错误的改动。

相关语法:

-- 开启事务
start transaction;

-- 若干条执行sql
-- 提交/回滚事务
commit/rollback;

注意:在开启事务之后,执行sql不会立即去执行,只有等到commit操作后才会统一执行(保证原子性)。

在这里给大家分享一下【云端源想】学习平台,无论你是初学者还是有经验的开发者,这里都有你需要的一切。包含课程视频、在线书籍、在线编程、一对一咨询等等,现在功能全部是免费的,欢迎大家点这里免费体验哦!

示例:

首先创建一个账户表并初始化数据,

Description

首先看正常情况下的转账操作,

Description

如果操作中出现异常情况,比如sql语句中所写的注释格式错误导致sql执行中断。

Description

观察结果发现了张三的账户少了2000元,但李四的账户余额并没有增加,在实际操作中这种涉及钱的操作发生这种失误可能会造成很大的损失。

为了防止这种失误的出现我们就可以使用事务来打包这些操作。

Description

观察这里的结果发现在当前的数据库用户查询到的account表中的账户余额发生了变化,但开启了事务之后在commit之前只是临时的预操作并不会真的去修改表中的数据。

可以退出数据库再打开重新查询表中数据或者切换用户去查询去验证表中数据是否发生改变,这里就不作演示了。

发现操作结果异常之后,当前用户需要恢复到事务之前的状态,即进行回滚操作。

Description

如果开启事务之后发现预操作的结果是预期的效果,此时我们就可以提交事务, 当我们提交完事务之后, 数据就是真的修改了,也就是硬盘中存储的数据真的改变了。

Description

要注意事务也不是万能的,不能保证你删表删库之后可以完全恢复,只是在适量的数据和操作下使用事务可以避免一些问题。

回滚(rollback)操作,实际上是我们把事务中的操作再进行逆操作,前面是插入, 回滚就是删除…

这些操作是有很大开销的,可以保存,但不能够无限保存,最多是将正再执行的事务保存下来,额外的内容就不好再保存了;数据库要是有几十亿条数据, 占据了几百G硬盘空间,不可能去花费几个T甚至更多的空间用来记录这些数据是如何来的。

四、事务的并发异常

但是呢,因为某一刻不可能总只有一个事务在运行,可能出现A在操作text表中的数据,B也同样在操作text表。

那么就会出现并发问题,对于同时运行的多个事务,当这些事务访问数据库中相同的数据时,如果没有采用必要的隔离机制,就会发生以下各种并发问题。

1、脏读(读未提交)

脏读:事务A读取到了事务已经修改但未提交的数据,这种数据就叫脏数据,是不正确的。

Description

2、不可重复读(读已提交)

不可重复读:对于事务A多次读取同一个数据时,由于其他是事务也在访问这个数据,进行修改且提交,对于事务A,读取同一个数据时,有可能导致数据不一致,叫不可重复读。

Description

3、幻读(可重复读)

幻读:因为mysql数据库读取数据时,是将数据放入缓存中,当事务B对数据库进行操作:例如删除所有数据且提交时,事务A同样能访问到数据,这就产生了幻读。

Description

解决幻读问题的办法是串行化,也就是彻底的舍弃并发,此时只要李四在读代码,张三就不能进行任何操作。

4、串行化

串行化:事务A和事务B同时访问时,在事务A修改了数据,而没有提交数据时,此时事务B想增加或修改数据时,只能等待事务A的提交,事务B才能够执行。

Description

所以,为了避免以上出现的各种并发问题,我们就必然要采取一些手段。mysql数据库系统提供了四种事务的隔离级别,用来隔离并发运行各个事务,使得它们相互不受影响,这就是数据库事务的隔离性。

五、MySQL的四个隔离级别

MySQL中有 4 种事务隔离级别, 由低到高依次为:读未提交 Read Uncommitted、读已提交 Read Committed、可重复读 Repeatable Read、串行化 Serializable。
Description

1. read uncommitted(读未提交数据)

允许事务读取未被其他事务提交的变更。(脏读、不可重复读和幻读的问题都会出现)。

2. read committed(读已提交数据)

只允许事务读取已经被其他事务提交的变更。(可以避免脏读,但不可重复读和幻读的问题仍然可能出现)

3. repeatable read(可重复读)

确保事务可以多次从一个字段中读取相同的值,在这个事务持续期间,禁止其他事务对这个字段进行更新(update)。(可以避免脏读和不可重复读,但幻读仍然存在)

4. serializable(串行化)

确保事务可以从一个表中读取相同的行,在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作,所有并发问题都可避免,但性能十分低下(因为你不完成就都不可以弄,效率太低)

一个事务与其他事务隔离的程度称为隔离级别。数据库规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性就越差。

串行化的事务处理方式是最安全的,但不能说用这个就一定好,应该是根据实际需求去选择合适的隔离级别,比如银行等涉及钱的场景,就需要确保准确性,速度慢一点也没什么;

而比如抖音、B站、快手等上面的点赞数,收藏数就没必要那么精确了,这个场景下速度提高一点体验会更好一些。

总结

MySQL事务具有原子性、一致性、隔离性和持久性四大特性,通过合理地管理事务,能够帮助我们保证数据的完整性和一致性。希望通过这篇文章,大家对MySQL事务有了更深入的了解,也希望大家在今后的工作中能够更好地运用事务来处理数据。

收起阅读 »

写了个数据查询为空的 Bug,你会怎么办?

大家在开发时,遇到的一个典型的 Bug 就是:为什么数据查询为空? 对应的现象就是:前端展示不出数据、或者后端查询到的数据列表为空。 遇到此类问题,其实是有经典的解决套路的,下面鱼皮给大家分享如何高效解决这个问题。 只需 4 个步骤: 解决步骤 1、定位问题...
继续阅读 »

大家在开发时,遇到的一个典型的 Bug 就是:为什么数据查询为空?


对应的现象就是:前端展示不出数据、或者后端查询到的数据列表为空。



遇到此类问题,其实是有经典的解决套路的,下面鱼皮给大家分享如何高效解决这个问题。


只需 4 个步骤:


解决步骤


1、定位问题边界


首先要定位数据查询为空的错误边界。说简单一点,就是要确认是前端还是后端的锅。


要先从请求的源头排查,也就是前端浏览器,毕竟前端和后端是通过接口(请求)交互的。


在浏览器中按 F12 打开浏览器控制台,进入网络标签,然后刷新页面或重新触发请求,就能看到请求的信息了。


选中请求并点击预览,就能看到后端返回结果,有没有返回数据一看便知。




如果发现后端正常返回了数据,那就是前端的问题,查看自己的页面代码来排查为什么数据没在前端显示,比如是不是取错了数据的结构?可以多用 debugger 或 console.log 等方式输出信息,便于调试。


星球同学可以免费阅读前端嘉宾神光的《前端调试通关秘籍》:t.zsxq.com/13Rh4xxNK


如果发现后端未返回数据,那么前端需要先确认下自己传递的参数是否正确。


比如下面的例子,分页参数传的太大了,导致查不到数据:



如果发现请求参数传递的没有问题,那么就需要后端同学帮忙解决了。


通过这种方式,直接就定位清楚了问题的边界,高效~


2、后端验证请求


接下来的排查就是在后端处理了,首先开启 Debug 模式,从接受请求参数开始逐行分析。


比如先查看请求参数对象,确认前端有没有按照要求传递请求参数:



毕竟谁能保证我们的同事(或者我们自己)不是小迷糊呢?即使前端说自己请求是正确的,但也必须要优先验证,而不是一上来就去分析数据库和后端程序逻辑的问题。


验证请求参数对象没问题后,接着逐行 Debug,直到要执行数据库查询。


3、后端验证数据库查询


无论是从 MySQL、MongoDB、Redis,还是文件中查询数据,为了理解方便,我们暂且统称为数据库。


上一步中,我们已经 Debug 到了数据库查询,需要重点关注 2 个点:


1)查看封装的请求参数是否正确


对于 MyBatis Plus 框架来说,就是查看 QueryWrapper 内的属性是否正确填充了查询条件



2)查看数据库的返回结果是否有值


比如 MyBatis Plus 的分页查询中,如果 records 属性的 size 大于 0,表示数据库返回了数据,那么就不用再排查数据库查询的问题了;而如果 size = 0,就要分析为什么从数据库中查询的数据为空。



这一步尤为关键,我们需要获取到实际发送给数据库查询的 SQL 语句。如果你使用的是 MyBatis Plus 框架,可以直接在 application.yml 配置文件中开启 SQL 语句日志打印,参考配置如下:


mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

然后执行查询,就能看到完整的 SQL 语句了:



把这个 SQL 语句复制到数据库控制台执行,验证下数据结果是否正确。如果数据库直接执行语句都查不出数据,那就确认是查询条件错误了还是数据库本身就缺失数据。


4、后端验证数据处理逻辑


如果数据库查询出了结果,但最终响应给前端的数据为空,那么就需要在数据库查询语句后继续逐行 Debug,验证是否有过滤数据的逻辑。


比较典型的错误场景是查询出的结果设置到了错误的字段中、或者由于权限问题被过滤和脱敏掉了。


最后


以后再遇到数据查询为空的情况,按照以上步骤排查问题即可。排查所有 Bug 的核心流程都是一样的,先搜集信息、再定位问题、最后再分析解决。


作者:程序员鱼皮
来源:juejin.cn/post/7306337248623132699
收起阅读 »

服务器:重来一世,这一世我要踏至巅峰!

前言 故事发生在上个星期一下午,秋风伴随着暖阳,映照出我在机房电脑上键盘敲击的身影。突然,伴随着一行指令运行mv /* ~/home/blog-end/,我发出土拨鼠尖叫——啊啊啊啊啊!!!!我服务器,窝滴服务器哟,哎哟,你干嘛,窝滴服务器哟!!! 就这样,我...
继续阅读 »

前言


故事发生在上个星期一下午,秋风伴随着暖阳,映照出我在机房电脑上键盘敲击的身影。突然,伴随着一行指令运行mv /* ~/home/blog-end/,我发出土拨鼠尖叫——啊啊啊啊啊!!!!我服务器,窝滴服务器哟,哎哟,你干嘛,窝滴服务器哟!!!


就这样,我把所有/目录下的文件给迁移了,/usr/bin/...所有文件都迁移了,还被我关了服务器窗口,后面重启也连不上了,我又是一声土拨鼠尖叫——啊啊啊啊啊啊!!!!如今只剩下一个方法了,那便是转世重修重新初始化系统......


重活一世,我要踏至巅峰


我,是上一代服务器的转世,重活一世,这一世我便要踏上那巅峰看一看,接下来便随着我一起打怪升级,踏上那巅峰吧......


搭建环境


在初始化系统的时候我选择的是诸天万界的高级系统ubuntu_22_04_x64,要部署的是我的博客项目,前端是nginx启动,后端是pm2启动,需要准备的环境有:nvm、node、mysql、git


1. 更新资源包,确保你的系统已经获取了最新的软件包信息


sudo apt update

2. 安装mysql


// 安装的时候一路`enter`就可以了
sudo apt install mysql-server

// 安装完后启动mysql服务
sudo systemctl start mysql

// 设置开机自启动
sudo systemctl enable mysql

// 检测数据库是否正在运行
sudo systemctl status mysql

// 运行以下指令登录数据库,第一次输入的密码会作为你数据库的密码
mysql -u root -p

// 如果输入密码报以下错误那就直接回车就能进入
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)

// 进入之后记得修改密码,这里的new_password修改为自己的密码
ALTER USER 'root'@'localhost' IDENTIFIED BY 'new_password';

//在这里我会创建一个子用户,使用子用户进行链接数据库操作,而不是直接root用户直接操作数据库
// 这里的dms换成用户名,PASSword123换成密码
create user 'dms'@'%' identified by 'PASSword123!'; // 创建子用户
grant all privileges on *.* to 'dms'@'%'with grant option; // 授权
flush privileges; // 生效用户



配置数据库运行远程链接


cd /etc/mysql/mysql.conf.d


vim mysqld.cnf //进入mysql配置文件修改 bind-address为0.0.0.0,如果是子用户的话需要在前面加上sudo提权



cfcec072591444fac34759c185c0d71.png


3. 安装nvm管理node版本


sudo apt install https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash

nvm --version // 查看是否正确输出

// 安装node版本
nvm install 19.1.0

// 查看是否正确输出
node --version
npm --version

4. 安装git并配置github


sudo apt install git

git --version // 查看输出版本

配置shh(这里我是直接一路Enter的)注意:这里要一定要使用以下指令生成ssh,后面有大用



①输入 ssh-keygen -m PEM -t rsa -b 4096,按enter;


②此后初次出现到②,出现的是保存密钥的路径,建议默认,按Enter;


③此时出现③,出现的提示是设置密码,千万不要设置!!!按Enter;


④此时出现④,出现的提示是再次输入密码,不要有任何输入,继续按Enter;



生成之后默认是在在服务器根目录下的.shh目录,这里直接运行以下指令


cd ~
cd .ssh
vim id_rsa.pub

进入id_rsa.pub文件复制公钥,到github的setting


66a36c9f2652d2f5a19a111b2064757.png
然后找到SSH and GPG keys去New SSH key,将公钥作为值保存就可以了
eb743773baf16231fe6d4a18ce3fbc7.jpg


5. 安装nginx并配置nginx.conf


sudo apt install nginx

// 安装完后启动nginx服务
sudo systemctl start nginx

// 设置开机自启动
sudo systemctl enable nginx

关于配置nginx,我一般每个nginx项目都会在conf.d目录单独写一个配置文件,方便后期更改,以下是我的个人博客的nginx配置,注意:conf.d里的配置文件后缀名必须是.conf才会生效


46d787353dfa50ecf76b09dfa1850d2.png



listen是监听的端口;
server name是服务器公网ip,也可以写域名;
root是前端项目所在地址;
index表示的是访问的index.html文件;
ry—_files这里是因为我vue项目打包用的history模式做的处理,hash模式可以忽略;



6. pm2的安装以及配置


npm install -g pm2

// 由于我项目使用了ts,并且没有去打包,所以我pm2也要安装ts-node
pm2 install ts-node

// 进入到后端项目的目录
cd /home/blog-end

// 初始化pm2文件
pm2 init // 运行之后会生成ecosystem.config.js配置文件

以下是我对pm2文件的配置,由于我是用了ts,所以我需要用到ts-node解释器,使用JavaScript的可以忽视interpreter属性


52360295a4677c6ac729236f5bd26a3.png


之后pm2 start econsystem.config.js运行配置文件就可以了


自动化部署


我自动化部署使用的技术是github actions,因为它简单容易上手,都是use轮子就完事了。下面跟我一起来做自动化部署


在开始自动化部署之前,我们还有一件大事要做,还记得之前生成ssh链接的时候说必须使用ssh-keygen -m PEM -t rsa -b 4096指令吗?现在就到了它表演的时候了,我们要用它配置ssh远程链接



先把.ssh目录下的id_rsa密钥复制到authorized_keys里,这一步就是配置远程ssh链接


然后配置sshd_config允许远程ssh链接,vim /etc/ssh/sshd_config,找到PermitRootLogin修改值为yes



b2018ae9ee0e125aa29f1a8d605f228.png


前端



进入自己的github项目地址,点击Actions去新建workflow,配置yml文件



9d0da5fcbaa38afac46d4876c15aac5.png



进入项目的setting里的Actions secrets and variables,创建secret



408b83ff0206b2973bbd7275ad7de80.png


后端



同样也是创建一个新的workflow,但服务端这里需要额外写一个脚本生成.env配置文件,因为服务端不可能把.env配置文件暴露到github的,那样特别不安全



script脚本


721c2b14136ed2436e8121c5b1c4b4c.png


yml配置文件


2e26f361e179354d35a163fbc593796.png


PS:觉得对自己有用或者文章还可以的话可以点个赞支持一下!!!


作者:辰眸
来源:juejin.cn/post/7299357353543368716
收起阅读 »

如何偷偷看股票,不被老板发现,试试idea这款插件!

各位炒股的朋友们,工作是公司的事业,炒股是自己的事业。认真、专注是每一个股民的修养。炒股最怕的就是没空看盘,等到自己有空了,发现错失了最好的抄底时机,错失了最好的卖出时机。但老板在,真的不敢频繁看盘啊,炒股对我们打工人为什么这么不公平! 不知道你有没有看盘的苦...
继续阅读 »

各位炒股的朋友们,工作是公司的事业,炒股是自己的事业。认真、专注是每一个股民的修养。炒股最怕的就是没空看盘,等到自己有空了,发现错失了最好的抄底时机,错失了最好的卖出时机。但老板在,真的不敢频繁看盘啊,炒股对我们打工人为什么这么不公平!


不知道你有没有看盘的苦恼,打开手机只需要看一眼就好,但是总担心领导被领导发现自己摸鱼。


要是既能随时看盘,又不会被老板发现那就好了。


没错 Idea有一款插件能让你上班期间偷偷炒股,不用担心被发现!写代码停顿的功夫就能盯盘,再也不会发生股价暴涨暴跌,而自己没时间关注的事情了。


可以在插件市场搜索 leeks


插件中添加基金、A股、港股等,随时可以看涨跌幅、当前价格、甚至连分时图、日K线都可以!
image.png


image.png


可以输入关注的股票编码,插件会自动帮你拉取股价信息,还可以选择间隔时间,我配置了1秒,因为我接受不了 2 秒的等待。和其他插件一样,leeks可以集成在侧边栏和底栏。不用的时候,可以切换。


随时随地看盘,一旦有风吹草动,我们立马就可以作出反应。再也不用错失机会,捶胸顿足,拍断大腿。


image.png



提供基金和股票的实时行情查看,支持港股美股 韭菜专用摸鱼插件!


韭菜专用摸鱼插件!


good luck! 使用说明:github.com/huage2580/l…



Change Notes
> v1.1 增加了股票的tab,采用腾讯的行情接口,股票轮询间隔10s
> v1.2 支持了港股和美股 示例代码:(sh000001,sh600519,sz000001,hk00700,usAAPL)代码一般可以在各网页端看得到 v
> 1.3 支持了IDEA 2020.1.3,兼容到`IDEA 2017.3`,修复macOS 行高问题(不确定
> v1.4 增加了隐蔽模式(全拼音和无色涨跌幅
> v1.5 增加了股票界面的排序~,可按净值和涨跌幅等列排序
> v1.6 样式修改,增加精确净值(当日,上一交易日
> v1.7 设置界面样式调整,增加新浪股票接口备选

作者:他是程序员
来源:juejin.cn/post/7281256255355469860
收起阅读 »

记录我短暂的珠海长隆团建之旅,眼睛一闭一睁回深圳了

从深圳出发 7月7日的广东,太阳已经很毒辣,顶着33度的烈日,出去团建了。出发地深圳蛇口,目的地珠海横琴。 虽然天气毒辣,但是天空很蓝,很美。蛇口港门口的公寓楼也都修的差不多了,感慨招商蛇口的审美,街道非常上档次。 你好珠海 上了船,在海上颠簸了一个半小时...
继续阅读 »

从深圳出发


7月7日的广东,太阳已经很毒辣,顶着33度的烈日,出去团建了。出发地深圳蛇口,目的地珠海横琴。


89c7d19c81f737839d79046865ff97f.jpg


2730f4558bec32b10781c19a6b97ed5.jpg


虽然天气毒辣,但是天空很蓝,很美。蛇口港门口的公寓楼也都修的差不多了,感慨招商蛇口的审美,街道非常上档次。


你好珠海


上了船,在海上颠簸了一个半小时,有点难受。所以抵达横琴码头的时候,也有点不适。在横琴码头,可以看到河对岸的澳门。这个码头不大,和大一点的公共厕所差不多。


89e161325c5591cfda53b8a2127cda3.jpg


好多年前和家里人一起自驾来珠海,如果硬要回忆起点什么,那就是见到了魅族的大楼吧。那个时候魅族还是国货之光,身边蛮多人入了他的m9。


相比起上一次的没有印象,这次的印象,应该是更深刻了。横琴,是大湾区三个自贸区之一,还有深圳前海和广州南沙。他们的特点都是漂亮、没人气。相比起来,深圳的前海人气算是最足的,因为距离市中心并不远。


横琴盖了非常多写字楼,城市界面非常漂亮。但如果说他和一线城市差在哪里,我感觉就是差在人气上。他的美,是静止的。而且没有地铁系统,公共交通不便。


e3b346cabee6634cbb3190c14b1ebb6.jpg


73be745eca12a4d95c4b5658fc68a9b.jpg


这次团建定的酒店比较偏僻,在香洲区边上,所以去了一个新开的mall吃饭,吃了牛蛙,还行,吃饱了。这边的住宅楼盖得也很漂亮,很有造型。但是这边其实过桥就到横琴了,非常近,步行过去估计2km吧。


28bc1466efcc72ffdb144d5fa824c33.jpg


3d0479a10e833482b5e6a5a8c5efaf1.jpg


晚上从mall出来,外面的人真少呀。除了我和几个同事在路上走就没人了。可能新城区是这样吧。


终于天亮了


我上一次团建是和业务一起的,那边的人多,有几个很活跃的氛围一下子就起来了。晚上玩狼人杀,玩到一点多。但这次人少,也没有会玩的,就很闷。所以晚上我在背单词,挺好的╮(╯▽╰)╭。


珠海长隆海洋王国


第二天是正题,珠海长隆海洋王国。刚去就下雨,太阳雨,没太久。然后门口的那个入园闸机坏了,等了好久,又热。后面有游客一直在催促,啧啧啧~


7d3b98553ccc273f6d05eff9bd3a141.jpg


中午吃的饭,88块钱。一直觉得景区是宰客而且不好吃,但是这个竟然意外的很合我口味。那个汤看起来像白粥,但是应该是用牛奶还是奶油做的,超香。总之让我吃几次都可以。就是分量不多,几口炫完。


3f12802b68cf58d8252b3e472654091.jpg


然后唯一一个很刺激的项目,鹦鹉过山车,太好玩了!怎么说呢,蛮高的,把我送上去的时候,看到了整个乐园,还看了不远处湛蓝的海水。没有停顿,直接俯冲。有好几次俯冲,还有一次倒挂吧,蛮好玩,而且只排了15分钟,超值。


f298e59d4d619772b5ad47d16ea7be4.jpg


还有一个5D电影,蛮好玩的。虽然题材是给孩子看的,但是这种表现形式我觉得很新鲜。我上一次体验5D是初中,在东部华侨城玩的,但那也就是一个模拟过山车。这边剧情丰富很多。包括喷水,喷臭气,吹风,上下颠簸,最后还有一个实物模型飞出来,也是亮点。


6759a4c8fecbe258fce15779f68f25c.jpg


还去了各种看动物的馆,这个海洋王国的特色,就是这个“玻璃”超大,而且没有缝隙和衔接的痕迹。我当时就就在和同事聊,我说这个肯定不是玻璃,不然运不过来,应该是现场铸造的树脂塑料啥的。


614b1525600618a7f7f82edc724c801.jpg


我好聪明,确实是塑料做的。据说这个亚克力玻璃是破了两项吉尼斯纪录,很大很厚。40m*8m,在那个看鲸的馆。


5009a33390d8901131c6a8bfab34e6f.jpg


虽然鲸鱼不会飞翔,但是他这种效果做出来就好像在我们头顶飞。还有企鹅和北极熊,也是在下面有一个隧道。


6e10c72bd246e6351739ee60e125c2b.jpg


63c813609afd9a35079b19f752f00a3.jpg


28caa27c9c7e051a96e0b8eee3a0af8.jpg


还有第一次见到了北极狼。在展馆里安静的睡觉。很优雅。


fc00491919dc36575fd0dcea77960bd.jpg


再见珠海


天气实在是热,而且有些项目要排很久,所以就出来了。在码头对面找了个顺德菜吃吃。珠海现在的状态,非常适合一个退休的人,来这边享受慢悠悠的生活。但对年轻人来说,还是深圳会更好点吧。在珠海,有慢节奏的生活,有很少的人流,但总感觉缺少了点创业的激情与活力。深圳没有这么惬意的节奏,但是有更多巨头公司驻扎在这,更匹配我现在的生命效率。


6829cbdbf9ea272cbdc3852a6be870f.jpg


f02c488f867571828a4aef1a242ec73.jpg


时间真的过得好快


珠海之行就像一场梦,有一个小而美的地方,印刻在了脑海深处。也许有一天会渐渐忘记,但是那种感觉会记忆下来。


虽然昨天最痛苦的是在船上的颠簸,弄得胃不舒服。但今天又一点事情都没有了。痛苦的感受随着时间慢慢消去,未来看照片,剩下的只会是美好。


我看最近不是流行什么“摄影眼”么,把一个看似很普通的景,弄得跳脱出我们传统的认知,带给我们更大的惊喜。我就在想,那不如直接看照片算了,现实中确实没那么漂亮,哈哈。


最近也在“坚持”学英语,因为找到一起学习的同事,就更有乐趣。说是“坚持”,是因为的确不痛苦,没有浪费我的自制力。不知道1~2年之后,是不是想起现在,也是眼睛一闭一睁,英语就能脱口而出了。也许也是一个晚上,我还在写着掘金,写着2年前的故事,那个晚上,我在学英语,每天都在,坚持了很多天,终于实现了我的目标。


作者:程序员Alvin
来源:juejin.cn/post/7253675120009609277
收起阅读 »

Rabbitmq消息大量堆积,我慌了!

背景 记得有次公司搞促销活动,流量增加,但是系统一直很平稳(我们开发的系统真牛),大家很开心的去聚餐,谈笑风声,气氛融洽,突然电话响起.... 运维:小李,你们系统使用的rabbitmq的消息大量堆积,导致服务器cpu飙升,赶紧回来看看,服务器要顶不住了 小...
继续阅读 »

背景


记得有次公司搞促销活动,流量增加,但是系统一直很平稳(我们开发的系统真牛),大家很开心的去聚餐,谈笑风声,气氛融洽,突然电话响起....



运维:小李,你们系统使用的rabbitmq的消息大量堆积,导致服务器cpu飙升,赶紧回来看看,服务器要顶不住了


小李:好的



系统架构描述


image.png


我们使用rabbitmq主要是为了系统解耦、异步提高系统的性能


前端售卖系统,生成订单后,推送订单消息到rabbitmq,订单履约系统作为消费者,消费订单消息落库,做后续操作


排查以及解决


方案一 增加消费者


第一我们想到的原因,流量激增,生成的订单速度远远大于消费者消费消息的速度,目前我们只部署了三个节点,那我们是否增加消费者,就可以解决这个问题,让消费者消费消息的速度远远大于生成者生成消息的速度,那消息就不存在堆积的问题,自然服务器压力也就下来了


通知运维,再部署三个点,也是就增加三个消费者,由原来的三个消费者变为6个消费者,信心满满的部署完成后,等待一段时间,不出意外还是出了意外,消息还是在持续堆积,没有任何改善,我心里那个急啊,为什么增加了消费者?一点改善没有呢


方案二 优化消费者的处理逻辑


持续分析,是不是消费者的逻辑有问题,处理速度还是慢?在消费逻辑分析中,发现在处理订单消息的逻辑里,调用了库存系统的一个接口,有可能是这个接口响应慢,导致消费的速度慢,跟不上生产消息的速度。


查看库存系统的运行情况,发现系统压力非常大,接口请求存在大量超时的情况,系统也在崩溃的边缘,因为我们上面的解决方案,增加了三个节点,间接的增大了并发。告知负责库存系统的同学,进行处理排查解决,但一时解决不了,如果持续这样,整体链路有可能全部崩掉,这怎么办呢?


消费者逻辑优化,屏蔽掉调用库存的接口,直接处理消息,但这种我们的逻辑是不完成,虽然能减少服务器的压力,后续处理起来也非常的麻烦,这种方式不可取


方案三 清空堆积的消息


为了减少消息的堆积,减轻服务器的压力,我们是否可以把mq里面的消息拿出来,先存储,等服务恢复后,再把存储的消息推送到mq,再处理呢?



  • 新建消费者,消费rabbitmq的消息,不做任何业务逻辑处理,直接快速消费消息,把消息存在一张表里,这样就没消息的堆积,服务器压力自然就下来了。


image.png
这方案上线后,过了一段时间观察,消息不再堆积,服务器的负载也下来了,我内心也不再慌了,那存储的那些消息,还处理吗?当然处理,怎么处理呢?



  • 后续等库存服务问题解决后,停掉新的消费者,新建一个生产者,再把表里的订单数据推送到rabbitmq,进行业务逻辑的处理


image.png


至此,问题就完美的解决了,悬着的心也放下了


问题产生的原因分析


整个链路服务一直都是很稳定的,因为流量的激增,库存服务的服务能力跟不上,导致整个链路出了问题,如果平台要搞促销这种活动,我们还是要提前评估下系统的性能,对整个链路做一次压测,找出瓶颈,该优化的要优化,资源不足的加资源


消息堆积为什么会导致cpu飙升呢?


问题虽然解决了,但我很好奇,消息堆积为什么会导致cpu飙升呢?


RabbitMQ 是一种消息中间件,用于在应用程序之间传递消息。当消息堆积过多时,可能会导致 CPU 飙升的原因有以下几点:



  1. 消息过多导致消息队列堆积:当消息的产生速度大于消费者的处理速度时,消息会积累在消息队列中。如果消息堆积过多,RabbitMQ 需要不断地进行消息的存储、检索和传递操作,这会导致 CPU 使用率升高。

  2. 消费者无法及时处理消息:消费者处理消息的速度不足以追赶消息的产生速度,导致消息不断积累在队列中。这可能是由于消费者出现瓶颈,无法处理足够多的消息,或者消费者的处理逻辑复杂,导致消费过程耗费过多的 CPU 资源。

  3. 消息重试导致额外的 CPU 开销:当消息处理失败时,消费者可能会进行消息的重试操作,尝试再次处理消息。如果重试频率较高,会导致消息在队列中频繁流转、被重复消费,这会增加额外的 CPU 开销。

  4. 过多的连接以及网络IO:当消息堆积过多时,可能会引发大量的连接请求和网络数据传输。这会增加网络 IO 的负载,并占用 CPU 资源。


通用的解决方案



  • 增加消费者:通过增加消费者的数量来提升消息的处理能力。增加消费者可以分担消息消费的负载,缓解消息队列的堆积问题。

  • 优化消费者的处理逻辑:检查消费者的代码是否存在性能瓶颈或是复杂的处理逻辑。可以通过优化算法、减少消费过程的计算量或是提高代码的效率来减少消费者的 CPU 开销。

  • 避免频繁的消息重试:当消息无法处理时,可以根据错误类型进行不同的处理方式,如将无法处理的消息转移到死信队列中或进行日志记录。避免频繁地对同一消息进行重试,以减少额外的 CPU 开销。

  • 调整 RabbitMQ 配置:可以调整 RabbitMQ 的参数来适应系统的需求,如增加内存、调整消息堆积的阈值和策略,调整网络连接等配置。

  • 扩展硬件资源:如果以上措施无法解决问题,可能需要考虑增加 RabbitMQ 的集群节点或者扩容服务器的硬件资源,以提升整个系统的处理能力。


需要根据具体情况综合考虑以上因素,并结合实际情况进行调试和优化,以解决消息堆积导致 CPU 飙升的问题,不能照葫芦画瓢,像我第一次直接增加消费者,差点把这个链路都干挂了



写作不易,刚好你看到,刚好对你有帮助,麻烦点点赞,有问题的留言讨论。



作者:柯柏技术笔记
来源:juejin.cn/post/7306442629318377535
收起阅读 »

两周面试,遇到的那些奇事

最近两周在帮别的部门面试,期间遇到了许多典型案例。有的真正做到了学习一年,重复十年;有的一手好牌,打的稀碎;有的连基本的社交礼仪都不懂的…… 这里将这些案例和思考拿来分享,也是想让目前还从事软件行业的朋友们思考一下,避免类似的事情发生在自己身上。 并不是人人年...
继续阅读 »

最近两周在帮别的部门面试,期间遇到了许多典型案例。有的真正做到了学习一年,重复十年;有的一手好牌,打的稀碎;有的连基本的社交礼仪都不懂的……


这里将这些案例和思考拿来分享,也是想让目前还从事软件行业的朋友们思考一下,避免类似的事情发生在自己身上。


并不是人人年薪百万


最近几个月大概面试了几十号人,工作年限在5到10年+,80%都有大厂经历,编程语言是Golang。面试的最后,都会询问一下预期的薪资。在这些样本下,基本上预期薪资都在20-30k之间,很少预期超过30k的。许多大厂出来的,5年以上工作经验的,他们在大厂的工资范围的也都是在20k+的水平,这一点挺吃惊的。


有可能是编程语言的原因,也有可能是采样样本较小的原因,也有可能是行业不景气的原因,但的确有这么一批找工作的人,他们背着大厂光环,对薪资的预期已经远低于早些年,但同样找工作的空档期为几个月,甚至超过半年。


从能力方面而言,特别出众的候选者很少,大多数都只是螺丝钉的角色,不是贬低,完全是真实感受。架构部门、运维部门、效能部门等把架构、运维、工具都给提供好了,剩下的只是码业务代码了。


如果长期在这样的模式下,自己又没有深度学习、深入思考,很容易陷入舒适区,导致狭隘的知识面和技能栈。


学一年技术,重复用十年


一位面试者,工作十多年。早些年经历的公司有新浪、外企、百度,后来在一家公司待了八年多。空档期三个月。面试时询问他在工作中的主要职责是什么,他说就是普通开发。


询问他是否做过架构,他说业余时间正在学。询问了常见的技术解决方案,常见的技术知识和实现原理等,基本上都处于一知半解的状态。整体的技术水平也就在中级到高级之间。


在网络上大家有一句戏言,说是学一年的技术,然后重复用十年。这位面试者是真的做到了。上家公司工作了半年,问离职原因,说是技术没有挑战度,想找一份能够深入学习技术的公司。


一个很现实的问题就是,同样是中高级水平,大多数公司都愿意用年轻人。因为他们的未来还不确定,还有继续学习和提升的可能性。而如果工作了十多年,还停留在两三年的水平,又怎么让人相信你能够继续提升呢。


在最近的面试中,并没有限制年龄,工作十多年的面试者占比很高。但这些面试者最大的问题就是能力和工作年限的预期不匹配。


大厂与中小厂的底层逻辑不同


面试中,大多数大厂出来的面试者的经历都是这样的:早期在大厂工作多年,出来(自主选择或被迫选择)后到中小企业当一个leader,然后就是基本上一年半载换一家或两家公司,最后又来面试了。


具体也不清楚为什么大多数人会是这样,但有一个大胆的猜测就是“水土不服”,大厂的工作模式和中小企业的工作模式的底层逻辑完全不同,最直观的就是大厂职能追求精专。而对于大多数中小企业,需要的是一职多能,业务并不需要那么牛的架构,但需要一个人或几个人能就把事情搞定。


面试了一位新浪微博、腾讯的架构师,架构经历上绝对甩我很远,一面让他过了。但后面招聘部门的领导说,过于虚了,担心无法具体落地。其实,这位领导担心就是上面说的一种情况。


在一家企业待太久,并不一定是好事


面试了一位58同城出来的Go(&PHP)工程师,10年多工作经验,空档期三个月,他的经历很有代表性。


在58同城6年多,技术能力向下不是很了解底层原理,向上没有架构能力,管理能力也不足,对新技术也失去了敏感度。大多数工作都是基于别人已经搭建好的架构、平台、自研框架基础之上实现业务逻辑。最重要的是,基本上六年都在做一个项目。


这与上面那个在一家公司工作八年的应聘者很类似。在一家公司待太久,交际圈、技术栈、业务领域变化都非常小,对技术人来说,除非你是决策者,能够参与决策和主导一些技术动向,否则对技术的发展及灵敏度将变得迟钝,陷入舒适区。


还是那句话,找工作,大多数情况下不是年龄问题,而是年龄与能力的匹配问题。我们每个人的认知都受限于环境、文化、时代。如果一直安于现状,停留于舒适区之内,当某一天离开这一环境时,才发现外面的世界已经完全变了,甚至是外面的世界一直未变,只是我们未曾了解。


有些小聪明最好不要使


面试了一位工作13年的候选者,从17年起就开始做架构,但无论架构知识、业务逻辑、基础知识都一塌糊涂,甚至清晰的逻辑都没有。


在简历上他用了一些小聪明,把17年-19年在百度的工作履历摘出来放在最前面,还写成“17年-至今”,蒙过了筛选简历的HR。但面试时看到这样的操作,第一个感觉就是反感,第二个感觉就是他从17年已经不再成长。因为他能拿出来说事的不是能力,而只是四五年前的一段经历。


其实,他也不用把那段单独摘出来写在最前面,只要扫一眼他的履历就能看到那段光环。用这样的小手段,反而适得其反。无论任何领域(特别是技术领域),都需要有十年磨一剑的毅力和决心,而且还是一场永不停息的马拉松,不能也无法依靠曾经的辉煌。


面试的基本礼仪


在面试的过程中,本人一贯是对面试者非常客气的。第一,未来有可能成为同事。第二,能够有一场愉快的技术交流也非常不错的体验。没有必要跟一个陌生人装架子、抖威风,更没必要急赤白脸的。但前几天遇到的一个面试者,他的行为却让我中途就中断了面试。


面试思路一般是:先问问应聘者觉得自己工作履历中那段项目经历值得说一说,然后就着这段经历深入聊聊业务和技术实现,一般会避免八股文式的面试。这样更能够反映面试者的真实能力,而不是背诵能力。


当问这位面试者哪段项目经历值得说说时,他反问,你想问哪段。无语。那我就主动问了一段感觉有得聊的,让他介绍一下。但当追问细节实现时,他说:我不是说过了吗,你没听懂吗,好吧,那我再给你讲一遍……现在你懂了吗?


整个面试过程中,他抄着手,戴着帽子,头勾着,你只能看到他的冒顶。用领导人讲话的语气,外加反问(质问)的句式和语气来跟你沟通。每当追问一个细节时,都跟冒犯到他似的,激励的反驳和反抗。一看这种情况,没几分钟就中断了面试。他的空档期已经十个多月了。


在面试过程中,本质上就是一个双向选择的过程,基本的礼仪,相互的尊重是非常必要的。面试官要做到对面试者的尊重,而面试者也要保持一种不卑不亢的姿态。我们每个人也都是在这两种角色中不停地循环轮转而已。


小结


面试是一件很有意思的事,你能听到许多新的技术方案,你能够见到各色的人,你能够看到不同的职场经历,你也能看到职场不同发展阶段的困境,你能够体味一些未曾经历的选择的后果……有了这些,你才能够更好的认识自己。


作者:程序新视界
来源:juejin.cn/post/7292009857602158619
收起阅读 »

现代化 Android 开发:基础架构

Android 开发经过 10 多年的发展,技术在不断更迭,软件复杂度也在不断提升。到目前为止,虽然核心需求越来越少,但是对开发速度的要求越来越高。高可用、流畅的 UI、完善的监控体系等都是现在的必备要求了。国内卷的方向又还包括了跨平台、动态化、模块化。 目前...
继续阅读 »

Android 开发经过 10 多年的发展,技术在不断更迭,软件复杂度也在不断提升。到目前为止,虽然核心需求越来越少,但是对开发速度的要求越来越高。高可用、流畅的 UI、完善的监控体系等都是现在的必备要求了。国内卷的方向又还包括了跨平台、动态化、模块化。


目前的整体感觉就是,移动开发基本是奄奄一息了。不过也不用过于悲观:一是依旧有很多存量的 App 堪称屎山,是需要有维护人员的,就跟现在很多人去卷 framework 层一样,千万行代码中找 bug。 二是 AI 日益成熟,那么应用层的创新也会出现,在没有更简洁的设备出现前,手机还是主要载体,总归是需要移动开发去接入的,如果硬件层越来越好,模型直接跑在手机上也不是不可能,所以对跨平台技术也会是新一层的考验,有可能直接去跨平台化了。毕竟去中台化也成了历史的选择。


因而,在这个存量市场,虽然竞争压力很大,但是如果技术过硬,还是能寻求一席之地的。因而我决定用几篇文章来介绍下,当前我认为的现代化 Android 开发是怎样的。其目录为:



  • 现代化 Android 开发:基础架构(本文)

  • 现代化 Android 开发:数据类

  • 现代化 Android 开发:逻辑层

  • 现代化 Android 开发:组件化与模块化的抉择

  • 现代化 Android 开发:多 Activity 多 Page 的 UI 架构

  • 现代化 Android 开发:Jetpack Compose 最佳实践

  • 现代化 Android 开发:性能监控


Scope


提到 Android 基础架构,大家可能首先想到的是 MVCMVPMVVMMVI 等分层架构。但针对现代化的 Android 开发,我们首要有的是 scope 的概念。其可以分两个方面:



  • 结构化并发之 CoroutineScope:目前协程基本已经是最推荐的并发工具了,CoroutineScope 的就是对并发任务的管理,例如 viewModelScope 启动的任务的生命周期就小于 viewModel 的存活周期。

  • 依赖注入之 KoinScope:虽然官方推荐的是 hilt,但其实它并没有 koin 好用与简洁,所以我还是推荐 koinKoinScope 是对实例对象的管理,如果 scope 结束, 那么 scope 管理的所有实例都被销毁。


一般应用总会有登录,所以大体的 scope 管理流程图是这样的:


scope



  • 我们启动 app, 创建 AppScope,对于 koin 而言就是用于存放单例,对于协程来说就是全局任务

  • 当我们登录后,创建 AuthSessionScope, 对于 koin 而言,就是存放用户相关的单例,对于协程而言就是用户执行相关的任务。当退出登录时,销毁当前的 AuthSessionScope,那么其对应的对象实例、任务全部都会被销毁。用户再次登录,就再次重新创建 AuthSessionScope。目前很多 App 对于用户域内的实例,基本上还是用单例来实现,退出登录时,没得办法,就只能杀死整个进程再重启, 所以会有黑屏现象,实现不算优雅。而用 scope 管理后,就是一件很自然而实现的事情了。所以尽量用依赖注入,而不要用单例模式

  • 当我们进入界面后,一般都是从逻辑层获取数据进行渲染,所以依赖注入没多大用了。而协程的 lifecycleScopeviewModelScope 就比较有用,管理界面相关的异步任务。


所以我们在做架构、做某些业务时,首要考虑 scope 的问题。我们可以把 CoroutineScope 也作为实例存放到 KoinScope 里,也可以把 KoinScope 作为 Context 存放到 CorutineScope 里。


岐黄小筑是将 CoroutineScope 放到 koin 里去以便依赖查找


val sessionCoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + coroutineLogExceptionHandler(TAG))
val sessionKoinScope = GlobalContext.get().createScope(...)
sessionKoinScope.declare(sessionCoroutineScope)


其实我们也完全可以用 CoroutineScopeContext 来做实例管理,而移除 koin 的使用。但是 Context 的使用并没有那么便捷,或许以后它可以进化为完全取代 koin



架构分层


随着软件复杂度的提升,MVCMVPMVVMMVI 等先后被提出,但我觉得目前所有的开发,都大体遵循某一模式而又不完全遵循,很容易因为业务的节奏,很容易打破,变成怎么方便怎么来。所以使用简单的分层 + 足够优秀的组件化,才是保证开发模式不被打破的最佳实践。下图是岐黄小筑的整体架构图:



整体架构不算复杂,其实重点是在于组件库,emo 已经有 20 个子库了,然后岐黄小筑有一些对于通用逻辑的抽象与封装,使得逻辑层虽然都集中在 logic 层,但整体都是写模板式的代码,可以面向 copy-paste 编程。


BookLogic 为例:



// 通过依赖注入传参, 拿到 db 层、网络层、以及用户态信息的应用
class BookLogic(
val authSession: AuthSession,
val kv: EmoKV,
val db: AccountDataBase,
private val bookApi: BookApi
) {
// 并发请求复用管理
private val concurrencyShare = ConcurrencyShare(successResultKeepTime = 10 * 1000L)

// 加载书籍信息,使用封装好的通用请求组件
fun logicBookInfo(bookId: Int, mode: Int = 0) = logic(
scope = authSession.coroutineScope, // 使用用户 session 协程 scope,因为有请求复用,所以退出界面,再进入,会复用之前的网络请求
mode = mode,
dbAction = { // 从 db 读取本地数据
db.bookDao().bookInfo(bookId)
},
syncAction = { // 从网络同步数据
concurrencyShare.joinPreviousOrRun("syncBookInfo-$bookId") {
bookApi.bookInfo(bookId).syncThen { _, data ->
db.runInTransaction {
db.userDao().insert(data.author)
db.bookDao().insert(data.info)
}
SyncRet.Full
}
}
}
)
// 类似的模板代码
suspend fun logicBookClassicContent(bookId: Int, mode: Int = 0) = logic(...)
suspend fun logicBookExpoundContent(bookId: Int, mode: Int = 0) = logic(...)
...
}

//将其注册到 `module` 中去,目前好像也可以通过注解的方式来做,不过我还没采用那种方式:
scopedOf(::BookLogic)

ViewModel 层浮层从 Logic 层读取数据,并可以进行特殊化处理:


class BookInfoViewModel(navBackStackEntry: NavBackStackEntry) : ViewModel() {
val bookId = navBackStackEntry.arguments?.getInt(SchemeConst.ARG_BOOK_ID) ?: throw RuntimeException("book_id is required!.")

val bookInfoFlow = MutableStateFlow(logicResultLoading<BookInfoPojo>())

init {
viewModelScope.launch {
runInBookLogic {
logicBookInfo(bookId, mode).collectLatest {
bookInfoFlow.emit(it)
}
}
}
}
}

Compose 界面再使用 ViewModel


@ComposeScheme(
action = SchemeConst.ACTION_BOOK_INFO,
alternativeHosts = [BookActivity::class]
)

@SchemeIntArg(name = SchemeConst.ARG_BOOK_ID)
@Composable
fun BookInfoPage(navBackStackEntry: NavBackStackEntry) {
LogicPage(navBackStackEntry = navBackStackEntry) {
val infoVm = schemeActivityViewModel<BookInfoViewModel>(navBackStackEntry)
val detailVm = schemeViewModel<BookDetailViewModel>(navBackStackEntry)
val bookInfo by infoVm.bookInfoFlow.collectAsStateWithLifecycle()
//...
}
}

这样整个数据流从网络加载、到存储到数据库、到传递给 UI 进行渲染的整个流程就结束了。


对于其中更多的细节,例如逻辑层具体是怎么封装的?UI 层具体是怎么使用多 ActivityPage?可以期待下之后的文章。


作者:古哥E下
来源:juejin.cn/post/7240636320762593338
收起阅读 »

程序员面试的时候,如何做自我介绍?

很多同学认为,程序员面试中的自我介绍环节是最没有营养的了,其目的也就是再找个话题开头儿,暖暖场而已。 我只能说,有的时候确实是这样,但有的时候真不是。 我作为面试官所经历过的一些面试场次,也经常在听过候选人的自我介绍后,会有些先入为主地给出对于他的第一印象,而...
继续阅读 »

很多同学认为,程序员面试中的自我介绍环节是最没有营养的了,其目的也就是再找个话题开头儿,暖暖场而已。


我只能说,有的时候确实是这样,但有的时候真不是。


我作为面试官所经历过的一些面试场次,也经常在听过候选人的自我介绍后,会有些先入为主地给出对于他的第一印象,而这种第一印象会直接左右该候选人是否能通过这场面试。


下面,我就从面试官的角度,说下自我介绍这个话题。


面试官很忙


是的,面试官很忙,给你进行面试的时间,往往是从他的日常工作中挤出来的。给你面试一个小时,他就少了一个小时的写代码时间,或是少了一个小时做架构设计的时间,再或是少了一个小时进行需求评审的时间。


因此,绝大多数面试官根本不喜欢面试这项工作,甚至略带抵触!!!


那么,在面试官忙得略带抵触的情况下,候选人的哪些行为会踩中他的雷区呢?


(1)回忆录型


我面试过一个07年毕业的候选人,从简历上看,学历履历俱佳,而其10年+的工作经验,恰恰是这个年轻的团队最为稀缺的资源。


我特别看好这个候选人,但是。。。


面试期间,他在做自我介绍的时候,从他刚毕业时,最早期的用JBuilder IDE,SSH(Spring + Struts +Hibernate)的项目开始介绍起,事无巨细,毫无死角地介绍了12个项目,时间已经过去了15分钟,时间轴也只不过从07年到了13年,如果按照这个节奏,让他走完这个流程,至少需要40分钟。。。


当时,我听得那叫一个烦躁,对他之前的好感全无,只好忍无可忍地打断他,说:“您还是从技术角度,介绍一个做得比较好的,最近的项目吧。”


(2)征婚型


另一个候选人,最初的自我介绍还算有模有样,简单地介绍学校中的专业、学习成绩、获奖情况,然后是工作经历,以及工作中的项目经验,有些项目涉及到高并发,有些项目涉及到对系统可用性要求很高,整体下来差不多一分钟出头的样子。


但忽然间,他开始话锋一转,开始介绍起他的性格来,就是那种都已经说烂了的“积极主动”、“乐观皮实”、“勤而好学”、“善于沟通”,而且,对于每个描述他性格的成员,他都举了一个工作中不大不小的例子。


这块说完后,大概过去了7分钟。我本以为可以继续面试的下一个环节,没想到,他又开始自顾自地介绍起自己的兴趣爱好来。


什么实况足球游戏、钓鱼、徒步、骑行,尤其让我记忆深刻的就是——写朦胧诗。。。


这尼玛,瞬间给我整得不会了。。。


面试官很懒


面试官很懒,绝大多数面试官在面试候选人之前,都不会抽出十分钟时间,提前看下候选人的简历。哪怕他们当天工作不忙,有这个时间,他们也宁愿用来摸鱼。


(1)自嗨型


但往往有些候选人不了解这个情况,他们往往草草地介绍完学历情况和工作履历后,在不怎么跟面试官介绍项目背景的情况下,便开始滔滔不绝地介绍其项目中的技术方案来。


往往有些项目还是带一些业务壁垒的,比如:财务领域、物流领域、支付领域、区块链领域、金融保险领域等。


候选人认为,要么面试官见多识广,要么面试官面试前特地了解了,所以,他们应该懂的。


于是乎,就形成了候选人口若悬河、滔滔不绝地讲述他项目中巧妙的技术设计,自嗨得飞起,但面试官呆若木鸡、满脸懵逼地不知道候选人的这个项目是做什么的。


在这一刹那,尴尬的结局已经注定。


(2)沟通障碍型


技术出身的人,在语言表述上不是强项,且大都性偏格内向。他们往往在进行自我介绍的时候,会由于紧张导致口吃或语无伦次。


如果轻微的这种情况还好,但如果给面试官造成了一种“跟他说话真费事,是不是沟通障碍啊”的印象,那出师未捷先GG的可能性就会接近100%了。


还是那句话,面试官很懒,懒得这样的未来同事沟通,所以最好的方式就是把他扼杀在摇篮里。


(3)惜字如金型


有一种候选人,自我介绍十秒钟解决战斗,“我是谁,我哪个学校毕业的,我目前在哪家公司”,完了。


然后面试官为了了解更多候选人的情况,不得不持续发问。


面试官:“你在这家公司负责什么?”


候选人:“负责后端的业务需求开发。”


面试官:“可以说下有哪些技术亮点吗?”


候选人:“这个一下子说不出来,要不您看看对哪个模块的技术实现感兴趣,我专门讲这一块的吧。”


最后形成的局面是,如果面试官需要了解候选人的详细信息,需要不断地发问,发问,再发问。


然后,面试官很懒,懒得问了。


面试官很毒


有的候选人认为,甭管我在以前的公司是什么表现,什么口碑,到了别的公司的面试官那,那就是自己说了算了。于是乎,他开始滔滔不绝、口若悬河、夸夸其谈、信口拈来。


(1)孔雀开屏型


这种候选人,在自我介绍过程中,稍微介绍到项目环节,便开始带着批判的语气,不断地说以前的架构怎么不合理,维护的代码怎么差,技术栈怎么老旧,同事的解决问题能力怎么不堪,领导怎么不作为,如果不是他及时出现,力挽狂澜地解决了问题,那么后果将是一场灾难。但是,有些鸟是不适合关在笼子里的,因为它的羽毛太美丽了,所以他才出来面试看机会。


其实,更多情况下,面试官所面试的候选人,在入职后要么是他的下级,要么是他的同事,不管是哪种,他们更加倾向的人物画像往往是技术基本功扎实,有些潜力,态度良好的团队型候选人,要是还有几许好奇心,几多上进心,若干自驱力,那就更好了。


其实,更多情况下,面试官的眼睛很毒,他们肯定不希望招来这么一个孤芳自赏的孔雀开屏型选手,日后在工作配合上惹上麻烦,面试官更不想在这种候选人的下次面试中,成为他口中的前同事或前领导。


(2)鸡血口号型


这种类型候选人,在自我介绍过程中,不断地提到自己在上家公司,在上上家公司,工作不怕苦不怕累,不怕加班不求回报,感动天感动地,感动老天和上帝。


他们自认为这样说,一定会让面试官很爽很愉悦,感情分拉满。


但往往经验丰富的面试官很清楚,这种类型的候选人,要么是言行不一,面试的时候说得是山崩海啸,到了真干的时候就是小孩撒尿;要么是用低水平的勤奋来掩盖自己资质上的愚钝或是能力上的不足。


总而言之,言而总之,他们觉得这样的人,肯定在哪方面有问题。


正确的自我介绍姿势


说完了这些,那我们再说说,理想中的自我介绍大概是什么样子呢?


记住八个字:简洁凝练,不卑不亢


另外,大家在进行自我介绍的时候,可以参照这个公式:


我是谁 + 学习经历 + 工作经历 + 项目经历 + 成绩成就 + 胜任理由 = 自我介绍


给大家一个例子:


面试官你好,我叫王鹏,北邮本硕,21年毕业,专业是计算机科学与技术。


毕业后任职于阿里巴巴,担任Java开发工程师一职,主要负责天猫电商订单中心的项目研发,对系统稳定性建设、性能优化、线上问题处理、电商架构设计等方面,都有着自己深刻的理解。


在职期间,我连续两年拿到3.75的年度绩效,并晋升一次,目前职级为P6。


面试咱们公司抖音电商的高级工程师一职,一个原因是看好公司的发展前景和企业文化,另外是我在电商领域的技术经验可以快速复用和持续提升。(隐晦地表达,自己非常适合,且完全胜任)


谢谢。


上述个人介绍,言简意赅地说清楚了个人情况,明确了自身优势和亮点,恰到好处地阐述了求职动机和求职意愿,并隐晦地表达了自己的适合度和胜任度, 我觉得是个比较好的自我介绍。


**如何练习:

**

(1)把自我介绍写下来,做到言简意赅,控制在一分钟左右。


(2)不断练习,大声朗读,记得要有情感和抑扬顿挫,不要当简历复读机,更不要像反复背过的。


(3)拿手机录下来,反复看自己的自我介绍,直到完美。


一定要注意的点:别啰里啰嗦,别扯没用的,别惜字如金,别自嗨成瘾,别结结巴巴,别把牛逼吹到天上,别卑微到尘埃里。


作者:库森学长
来源:juejin.cn/post/7274839871277432886
收起阅读 »

很容易中招的一种索引失效场景,一定要小心

快过年,我的线上发布出现故障 “五哥,你在上线吗?”,旁边有一个声音传来。 “啊,怎么了?”。真是要命,在上线发布时候,我最讨厌别人叫我的名字 。我慌忙站起来,看向身后,原来是 建哥在问我。我慌忙的问,怎么回事。 “DBA 刚才在群里说,Task数据库 cpu...
继续阅读 »

快过年,我的线上发布出现故障


“五哥,你在上线吗?”,旁边有一个声音传来。


“啊,怎么了?”。真是要命,在上线发布时候,我最讨厌别人叫我的名字 。我慌忙站起来,看向身后,原来是 建哥在问我。我慌忙的问,怎么回事。


“DBA 刚才在群里说,Task数据库 cpu 负载增加!有大量慢查询”,建哥来我身边,跟我说。


慢慢的,我身边聚集着越来越多的人


image.png


“你在上线Task服务吗?改动什么内容了,看看要不要立即回滚?”旁边传来声音。此时,我的心开始怦怦乱跳,手心发痒,紧张不已。


我检查着线上机器的日志,试图证明报警的原因不是出在我这里。


我对着电脑,微微颤抖地回答大家:“我只是升级了基础架构的Jar包,其他内容没有改动啊。”此时我已分不清是谁在跟我说话,只能对着电脑作答……


这时DBA在群里发送了一条SQL,他说这条SQL导致了大量的慢查询。


我突然记起来了,我转过头问林哥:“林哥,你上线了什么内容?”这次林哥有代码的变更,跟我一起上线。我觉得可能是他那边有问题。


果然,林哥看着代码发呆。他嘟囔道:“我添加了索引啊,怎么会有慢查询呢?”原来慢查询的SQL是林哥刚刚添加的,这一刻我心里的石头放下了,问题并不在我,我轻松了许多。


“那我先回滚吧”,幸好我们刚发布了一半,现在回滚还来得及,我尝试回滚机器。此刻我的紧张情绪稍稍平静下来,手也不再发抖。


既然不是我的问题,我可以以吃瓜的心态,暗中观察事态的发展。我心想:真是吓死我了,幸好不是我的错。


然而我也有一些小抱怨:为什么非要和我一起搭车上线,出了事故,还得把我拖进来。


故障发生前的半小时


2年前除夕前的一周,我正准备着过年前的最后一次线上发布,这时候我刚入职两个月,自然而然会被分配一些简单的小活。这次上线的内容是将基础架构的Jar包升级到新版本。一般情况下,这种配套升级工作不会出问题,只需要按部就班上线就行。


“五哥,你是要上线 Task服务吗?”,工位旁的林哥问我,当时我正做着上线前的准备工作。


“对啊,马上要发布,怎么了?”,我转身回复他。


“我这有一个代码变更,跟你搭车一起上线吧,改动内容不太多。已经测试验证过了”,林哥说着,把代码变更内容发给我,简单和我说了下代码变更的内容。我看着改动内容确实不太多,新增了一个SQL查询,于是便答应下来。我重新打包,准备发布上线。


半小时以后,便出现了文章开头的情景。新增加的SQL 导致大量慢查询,数据库险些被打挂。


为什么加了索引,还会出现慢查询呢?


”加了索引,为什么还有慢查询?“,这是大家共同的疑问。


事后分析故障的原因,通过 mysql explain 命令,查看该SQL 确实没有命中索引,从而导致慢查询。


这个SQL 大概长这个样子!我去掉了业务相关的部分。


select * from order_discount_detail where orderId = 1123;


order_discount_detailorderId 这一列上确实加了索引,不应该出现慢查询,乍一看,没有什么问题。我本能的想到了索引失效的几种场景。难道是类型不匹配,导致索引失效?


果不其然, orderId 在数据库中的类型 是 varchar 类型,而传参是按照 long 类型传的。


复习一下: 类型转换导致索引失效


类型转换导致索引失效,是很容易犯的错误


因为在某些特殊场景下要对接外部订单,存在订单Id为字符串的情况,所以 orderId被设计成 varchar 字符串类型。然而出问题的场景比较明确,订单id 就是long类型,不可能是字符串类型。


所以林哥,他在使用Mybatis 时,直接使用 long 类型的 orderId字段传参,并且没有意识到两者数据类型不对。


因为测试环境数据量比较小,即使没有命中索引,也不会有很严重的慢查询,并且测试环境请求量比较低,该慢查询SQL 执行次数较少,所以对数据库压力不大,测试阶段一直没有发现性能问题。


直到代码发布到线上环境————数据量和访问量都非常高的环境,差点把数据库打挂。


mybatis 能避免 “类型转换导致索引失效” 的问题吗?


mybatis能自动识别数据库和Java类型不一致的情况吗?如果发现java类型和数据库类型不一致,自动把java 类型转换为数据库类型,就能避免索引失效的情况!


答案是不能。我没找到 mybatis 有这个能力。


mybatis 使用 #{} 占位符,会自动根据 参数的 Java 类型填充到 SQL中,同时可以避免SQL注入问题。


例如刚才的SQL 在 mybatis中这样写。


select * from order_discount_detail where orderId = #{orderId};


orderId 是 String 类型,SQL就变为


select * from order_discount_detail where orderId = ‘1123’;


mybatis 完全根据 传参的java类型,构建SQL,所以不要认为 mybatis帮你处理好java和数据库的类型差异问题,你需要自己关注这个问题!


再次提醒,"类型转换导致索引失效"的问题,非常容易踩坑。并且很难在测试环境发现性能问题,等到线上再发现问题就晚了,大家一定要小心!小心!


险些背锅


可能有朋友疑问,为什么发布一半时出现慢查询,单机发布阶段不能发现这个问题吗?


之所以没发现这个问题,是因为 新增SQL在 Kafka消费逻辑中,由于单机发布机器启动时没有争抢到 kafka 分片,所以没有走到新代码逻辑。


此外也没有遵循降级上线的代码规范,如果上线默认是降级状态,上线过程中就不会有问题。放量阶段可以通过降级开关快速止损,避免回滚机器过程缓慢而导致的长时间故障。


不是我的问题,为什么我也背了锅


因为我在发布阶段没有遵循规范,按照规定的流程应该在单机发布完成后进行引流压测。引流压测是指修改机器的Rpc权重,将Rpc请求集中到新发布的单机上,这样就能提前发现线上问题。


然而由于我偷懒,跳过了单机引流压测。由于发布的第一台机器没有抢占到Kafka分片,因此无法执行新代码逻辑。即使进行了单机引流压测,也无法提前发现故障。虽然如此,但我确实没有遵循发布规范,错在我。


如果上线时没有出现故障,这种不规范的上线流程可能不会受到责备。但如果出现问题,那只能怪我倒霉。在复盘过程中,我的领导抓住了这件事,给予了重点批评。作为刚入职的新人,被指责确实让我感到不舒服。


快要过年了,就因为搭车上线,自己也要承担别人犯错的后果,让我很难受。但是自己确实也有错,当时我的心情复杂而沉重。


两年前的事了,说出来让大家吃个瓜,乐呵一下。如果这瓜还行,东东发财的小手点个赞


作者:五阳神功
来源:juejin.cn/post/7305572311812636683
收起阅读 »

Android - 你可能需要这样一个日志库

前言 目前大多数库api设计都是Log.d("tag", "msg")这种风格,而且支持自定义日志存储的比较少, 所以作者想自己造一个轮子。 这种api风格有什么不好呢? 首先,它的tag是一个字符串,需要开发人员严格管理tag,要不然可能各种硬编码的tag满...
继续阅读 »

前言


目前大多数库api设计都是Log.d("tag", "msg")这种风格,而且支持自定义日志存储的比较少,
所以作者想自己造一个轮子。


这种api风格有什么不好呢?


首先,它的tag是一个字符串,需要开发人员严格管理tag,要不然可能各种硬编码的tag满天飞。


另外,它也可能导致性能陷阱,假设有这么一段代码:


// 打印一个List
Log.d("tag", list.joinToString())

此处使用Debug打印日志,生产模式下调高日志等级,不打印这一行日志,但是list.joinToString()这一行代码仍然会被执行,有可能导致性能问题。


下文会分析作者期望的api是什么样的,本文演示代码都是用kotlin,库中好用的api也是基于kotlin特性来实现的。


作者写库有个习惯,对外开放的类或者全局方法都会加一个前缀f,一个是为了避免命名冲突,另一个是为了方便代码检索,以下文章中会出现,这里做一下解释。


期望


什么样的api才能解决上面的问题呢?我们看一下方法的签名和打印方式


inline fun <reified T : FLogger> flogD(block: () -> Any)

interface AppLogger : FLogger

flogD {
list.joinToString { it }
}

flogD方法打印Debug日志,传一个Flogger的子类AppLogger作为日志标识,同时传一个block来返回要打印的日志内容。


日志标识是一个类或者接口,所以管理方式比较简单不会造成tag混乱的问题,默认tag是日志标识类的短类名。生产模式下调高日志等级后,block就不会被执行了,避免了可能的性能问题。


实现分析


日志库的完整实现已经写好了,放在这里xlog



  • 支持限制日志大小,例如限制每天只能写入10MB的日志

  • 支持自定义日志格式

  • 支持自定义日志存储,即如何持久化日志


这一节主要分析一下实现过程中遇到的问题。


问题:如果App运行期间日志文件被意外删除了,怎么处理?


在Android中,用java.io的api对一个文件进行写入,如果文件被删除,继续写入的话不会抛异常,导致日志丢失,该如何解决?


有同学说,在写入之前先检查文件是否存在,如果存在就继续写入,不存在就创建后写入。


检查一个文件是否存在通常是调用java.io.File.exist()方法,但是它比较耗性能,我们来做一个测试:


measureTime {
repeat(1_0000) {
file.exists()
}
}.let {
Log.i("MainActivity", "time:${it.inWholeMilliseconds}")
}

14:50:33.536 MainActivity            com.sd.demo.xlog                I  time:39
14:50:35.872 MainActivity com.sd.demo.xlog I time:54
14:50:38.200 MainActivity com.sd.demo.xlog I time:43
14:50:40.028 MainActivity com.sd.demo.xlog I time:53
14:50:41.693 MainActivity com.sd.demo.xlog I time:58

可以看到1万次调用的耗时在50毫秒左右。


我们再测试一下对文件写入的耗时:


val output = filesDir.resolve("log.txt").outputStream().buffered()
val log = "1".repeat(50).toByteArray()
measureTime {
repeat(1_0000) {
output.write(log)
output.flush()
}
}.let {
Log.i("MainActivity", "time:${it.inWholeMilliseconds}")
}

14:57:56.092 MainActivity            com.sd.demo.xlog                I  time:38
14:57:56.558 MainActivity com.sd.demo.xlog I time:57
14:57:57.129 MainActivity com.sd.demo.xlog I time:57
14:57:57.559 MainActivity com.sd.demo.xlog I time:46
14:57:58.054 MainActivity com.sd.demo.xlog I time:54

可以看到1万次调用,每次写入50个字符的耗时也在50毫秒左右。如果每次写入日志前都判断一下文件是否存在,那么实际上相当于2次写入的性能成本,这显然很不划算。


还有同学说,开一个线程,定时判断文件是否存在,这样子虽然不会损耗单次写入的性能,但是又多占用了一个线程资源,显然也不符合作者的需求。


其实Android已经给我们提供了这种场景的解决方案,那就是android.os.MessageQueue.IdleHandler,关于IdleHandler这里就不展开讨论了,简单来说就是当你注册一个IdleHandler后,它会在主线程空闲的时候被执行。


我们可以在每次写入日志之后注册IdleHandler,等IdleHandler被执行的时候检查一下日志文件是否存在,如果不存在就关闭输出流,这样子在下一次写入的时候就会重新创建文件写入了。


这里要注意每次写入日志之后注册IdleHandler,并不是每次都创建新对象,要判断一下如果原先的对象还未执行的话就不用注册一个新的IdleHandler,库中大概的代码如下:


private class LogFileChecker(private val block: () -> Unit) {
private var _idleHandler: IdleHandler? = null

fun register(): Boolean {
// 如果当前线程没有Looper则不注册,上层逻辑可以直接检查文件是否存在,因为是非主线程
Looper.myLooper() ?: return false

// 如果已经注册过了,直接返回
_idleHandler?.let { return true }

val idleHandler = IdleHandler {
// 执行block检查任务
libTryRun { block() }

// 重置变量,等待下次注册
_idleHandler = null
false
}

// 保存并注册idleHandler
_idleHandler = idleHandler
Looper.myQueue().addIdleHandler(idleHandler)
return true
}
}

这样子文件被意外删除之后,就可以重新创建写入了,避免丢失大量的日志。


问题:如何检测文件大小是否溢出


库支持对每天的日志大小做限制,例如限制每天最多只能写入10MB,每次写入日志之后都会检查日志大小是否超过限制,通常我们会调用java.io.File.length()方法获取文件的大小,但是它也比较耗性能,我们来做一个测试:


val file = filesDir.resolve("log.txt").apply {
this.writeText("hello")
}
measureTime {
repeat(1_0000) {
file.length()
}
}.let {
Log.i("MainActivity", "time:${it.inWholeMilliseconds}")
}

16:56:04.090 MainActivity            com.sd.demo.xlog                I  time:61
16:56:05.329 MainActivity com.sd.demo.xlog I time:80
16:56:06.382 MainActivity com.sd.demo.xlog I time:72
16:56:07.496 MainActivity com.sd.demo.xlog I time:79
16:56:08.591 MainActivity com.sd.demo.xlog I time:78

可以看到耗时在60毫秒左右,相当于上面测试中1次文件写入的耗时。


库中支持自定义日志存储,在日志存储接口中定义了size()方法,上层通过此方法来判断当前日志的大小。


如果开发者自定义了日志存储,避免在此方法中每次调用java.io.File.length()来返回日志大小,应该维护一个表示日志大小的变量,变量初始化的时候获取一下java.io.File.length(),后续通过写入的数量来增加这个变量的值,并在size()方法中返回。库中默认的日志存储实现类就是这样实现的,有兴趣的可以看这里


问题:文件大小溢出后怎么处理?


假设限制每天最多只能写入10MB,那超过10MB后如何处理?有同学说直接删掉或者清空文件,重新写入,这也是一种策略,但是会丢失之前的所有日志。


例如白天写了9.9MB,到晚上的时候写满10MB,清空之后,白天的日志都没了,这时候用户反馈白天遇到的一个bug,需要上传日志,那就芭比Q了。


有没有办法少丢失一些呢?可以把日志分多个文件存储,为了便于理解假设分为2个文件存储,一天10MB,那1个文件最多只能写入5MB。具体步骤如下:



  1. 写入文件20231128.log

  2. 20231128.log写满5MB的时候关闭输出流,并把它重命名为20231128.log.1


这时候继续写日志的话,发现20231128.log文件不存在就会创建,又跳到了步骤1,就这样一直重复1和2两个步骤,到晚上写满10MB的时候,至少还有5MB的日志内容保存在20231128.log.1文件中避免丢失全部的日志。


分的文件数量越多,保留的日志就越多,实际上就是拿出一部分空间当作中转区,满了就向后递增数字重命名备份。目前库中只分为2个文件存储,暂时不开放自定义文件数量。


问题:打印日志的性能


性能,是这个库最关心的问题,通常来说文件写入操作是性能开销的大头,目前是用java.io相关的api来实现的,怎样提高写入性能作者也一直在探索,在demo中提供了一个基于内存映射的日志存储方案,但是稳定性未经测试,后续测试通过后可能会转正。有兴趣的读者可以看看这里


还有一个比较影响性能的就是日志的格式化,通常要把一个时间戳转为某个日期格式,大部分人都会用java.text.SimpleDateFormat来格式化,用它来格式化年:月:日的时候问题不大,但是如果要格式化时:分:秒.毫秒那它就比较耗性能,我们来做一个测试:


val format = SimpleDateFormat("HH:mm:ss.SSS")
val millis = System.currentTimeMillis()
measureTime {
repeat(1_0000) {
format.format(millis)
}
}.let {
Log.i("MainActivity", "time:${it.inWholeMilliseconds}")
}

16:05:26.920 MainActivity            com.sd.demo.xlog                I  time:245
16:05:27.586 MainActivity com.sd.demo.xlog I time:227
16:05:28.324 MainActivity com.sd.demo.xlog I time:212
16:05:29.370 MainActivity com.sd.demo.xlog I time:217
16:05:30.157 MainActivity com.sd.demo.xlog I time:193

可以看到1万次格式化耗时大概在200毫秒左右。


我们再用java.util.Calendar测试一下:


val calendar = Calendar.getInstance()
// 时间戳1
val millis1 = System.currentTimeMillis()
// 时间戳2
val millis2 = millis1 + 1000
// 切换时间戳标志
var flag = true
measureTime {
repeat(1_0000) {
calendar.timeInMillis = if (flag) millis1 else millis2
calendar.run {
"${get(Calendar.HOUR_OF_DAY)}:${get(Calendar.MINUTE)}:${get(Calendar.SECOND)}.${get(Calendar.MILLISECOND)}"
}
flag = !flag
}
}.let {
Log.i("MainActivity", "time:${it.inWholeMilliseconds}")
}

16:11:25.342 MainActivity            com.sd.demo.xlog                I  time:35
16:11:26.209 MainActivity com.sd.demo.xlog I time:35
16:11:27.316 MainActivity com.sd.demo.xlog I time:37
16:11:28.057 MainActivity com.sd.demo.xlog I time:25
16:11:28.825 MainActivity com.sd.demo.xlog I time:18


这里解释一下为什么要用两个时间戳,因为Calendar内部有缓存,如果用同一个时间戳测试的话,没办法评估它真正的性能,所以这里每次格式化之后就切换到另一个时间戳,避免缓存影响测试。


可以看到1万次的格式化耗时在30毫秒左右,差距很大。如果要自定义日志格式的话,建议用Calendar来格式化时间,有更好的方案欢迎和作者交流。


问题:日志的格式如何显示


手机的存储资源是宝贵的,如何定义日志格式也是一个比较重要的细节。



  • 优化时间显示


目前库内部是以天为单位来命名日志文件的,例如:20231128.log,所以在格式化时间戳的时候只保留了时:分:秒.毫秒,避免冗余显示当天的日期。



  • 优化日志等级显示


打印的时候提供了4个日志等级:Verbose, Debug, Info, Warning, Error,一般最常用的记录等级是Info,所以在格式化的时候如果等级是Info则不显示等级标志,规则如下:


private fun FLogLevel.displayName(): String {
return when (this) {
FLogLevel.Verbose -> "V"
FLogLevel.Debug -> "D"
FLogLevel.Warning -> "W"
FLogLevel.Error -> "E"
else -> ""
}
}


  • 优化日志标识显示


如果连续2条或多条日志都是同一个日志标识,那么就只有第1条日志会显示日志tag



  • 优化线程ID显示


如果是主线程的话,不显示线程ID,只有非主线程才显示线程ID


经过上面的优化之后,日志打印的格式是这样的:


flogI { "1" }
flogI { "2" }
flogW { "3" }
flogI { "user debug" }
thread {
flogI { "thread" }
}

19:19:43.961[AppLogger] 1
19:19:43.974 2
19:19:43.975[W] 3
19:19:43.976[UserLogger] user debug
19:19:43.977[12578] thread

API


这一节介绍一下库的API


常用方法


/**
* 打开日志,文件保存目录:[Context.getFilesDir()]/flog,
* 默认只打开文件日志,可以调用[FLog.enableConsoleLog()]方法开关控制台日志,
*/

FLog.open(
context = this,

//(必传参数)日志等级 All, Verbose, Debug, Info, Warning, Error
level = FLogLevel.All,

//(可选参数)限制每天日志文件大小(单位MB),小于等于0表示不限制大小,默认100MB
limitMBPerDay = 100,

//(可选参数)自定义日志格式
formatter = AppLogFormatter(),

//(可选参数)自定义日志存储
storeFactory = AppLogStoreFactory(),
)


// 是否打打印控制台日志
FLog.enableConsoleLog(false)


/**
* 删除日志,参数saveDays表示要保留的日志天数,小于等于0表示删除全部日志,
* 此处saveDays=1表示保留1天的日志,即保留当天的日志
*/

FLog.deleteLog(1)


// 关闭日志
FLog.close()

打印日志


interface AppLogger : FLogger

flogV { "Verbose" }
flogD { "Debug" }
flogI { "Info" }
flogW { "Warning" }
flogE { "Error" }

// 打印控制台日志,不会写入到文件中,不需要指定日志标识,tag:DebugLogger
fDebug { "console debug log" }

配置日志标识


可以通过FLog.config方法修改某个日志标识的配置信息,例如下面的代码:


FLog.config {
// 修改日志等级
this.level = FLogLevel.Debug

// 修改tag
this.tag = "AppLoggerAppLogger"
}

自定义日志格式


class AppLogFormatter : FLogFormatter {
override fun format(record: FLogRecord): String {
// 自定义日志格式
return record.msg
}
}

interface FLogRecord {
/** 日志标识 */
val logger: Class<out FLogger>

/** 日志tag */
val tag: String

/** 日志内容 */
val msg: String

/** 日志等级 */
val level: FLogLevel

/** 日志生成的时间戳 */
val millis: Long

/** 日志是否在主线程生成 */
val isMainThread: Boolean

/** 日志生成的线程ID */
val threadID: String
}

自定义日志存储


日志存储是通过FLogStore接口实现的,每一个FLogStore对象负责管理一个日志文件。
所以需要提供一个FLogStore.Factory工厂为每个日志文件提供FLogStore对象。


class AppLogStoreFactory : FLogStore.Factory {
override fun create(file: File): FLogStore {
return AppLogStore(file)
}
}

class AppLogStore(file: File) : FLogStore {
// 添加日志
override fun append(log: String) {}

// 返回当前日志的大小
override fun size(): Long = 0

// 关闭
override fun close() {}
}

结束


库目前还处于alpha阶段,如果有开发者遇到问题了可以及时反馈给作者,最后感谢大家的阅读。


作者:Sunday1990
来源:juejin.cn/post/7306423214493270050
收起阅读 »

Android集成Flutter模块经验记录

记录Android原生项目集成Flutter模块经验,其中不乏一些踩坑,也是几番查找资料之后才成功运用于实际开发。 主要为了记录,将使用简洁的描述。 Flutter开发环境 1. Flutter安装和环境配置 官方文档:flutter.cn/docs/get-...
继续阅读 »

记录Android原生项目集成Flutter模块经验,其中不乏一些踩坑,也是几番查找资料之后才成功运用于实际开发。

主要为了记录,将使用简洁的描述。


Flutter开发环境


1. Flutter安装和环境配置


官方文档:flutter.cn/docs/get-st…

参照官方文档一步步按步骤即可

下载SDK->解压->配置PATH环境变量

其中配置PATH环境变量务必使其永久生效方式


2. AS安装flutter和dart插件


AS安装flutter和dart插件


将 Flutter module 集成到 Android 项目


官方文档:flutter.cn/docs/add-to…

仍然主要是参照官方文档。

有分为使用AS集成和不使用AS集成,其中使用AS集成有AAR集成和使用模块源码集成两种方式。



  • AAR 集成: AAR 机制可以为每个 Flutter 模块创建 Android AAR 作为依赖媒介。当你的宿主应用程序开发者不想安装 Flutter SDK 时,这是一个很好方案。但是每次修改都需要重新编译。

  • 模块源码集成:直接将 Flutter 模块的源码作为子项目的依赖机制是一种便捷的一键式构建方案,但此时需要另外安装 Flutter SDK,这是目前 Android Studio IDE 插件使用的机制。


本文讲述的是使用模块源码集成的方式。


1.创建Flutter Module


使用File > New > New Flutter Project创建,选择Module,官方建议Flutter Module和Android项目在同一个目录下。
创建Flutter Module


2. 配置Module


在Android项目的 settings.gradle中添加以下配置:flutter_module为创建的flutter module名称


// Include the host app project.
include ':app' // assumed existing content
setBinding(new Binding([gradle: this])) // new
evaluate(new File( // new
settingsDir.parentFile, // new
'flutter_module/.android/include_flutter.groovy' // new
)) // new

在应用中引入对 Flutter 模块的依赖:


dependencies {
implementation project(':flutter')
}

3. 编译失败报错:Failed to apply plugin class 'FlutterPlugin'


gradle6.8后 在settings.gradle的dependencyResolutionManagement 下新增了如下配置:


repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)


RepositoriesMode配置在构建中仓库如何设置,总共有三种方式:

FAIL_ON_PROJECT_REPOS

表示如果工程单独设置了仓库,或工程的插件设置了仓库,构建就直接报错抛出异常

PREFER_PROJECT

表示如果工程单独设置了仓库,就优先使用工程配置的,忽略settings里面的

PREFER_SETTINGS

表述任何通过工程单独设置或插件设置的仓库,都会被忽略



settings.gradle里配置的是FAIL_ON_PROJECT_REPOS,Flutter插件又单独设置了repository,所以会构建报错,因此需要把FAIL_ON_PROJECT_REPOS改成PREFER_PROJECT。


因为gradle调整,Android仓库配置都在settings.gradle中,但是因为设置了PREFER_PROJECT,settings.gradle被忽略了,那该怎么解决呢?发现虽然project的gradle文件虽然调整了,但是依然可以跟之前一样配置仓库这些,于是在项目build.gradle中加上settings.gradle中的所有仓库,成功解决问题并编译安装成功。


allprojects{
repositories {
maven {
url "https://plugins.gradle.org/m2/"
}
maven { url "https://s01.oss.sonatype.org/content/groups/public" }
maven { url 'https://jitpack.io' }
google()
// 极光 fcm, 若不集成 FCM 通道,可直接跳过
maven { url "https://maven.google.com" }
maven {
url 'https://artifact.bytedance.com/repository/pangle'
}
}
}

总结:需要先将settings.gradle中repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)替换为repositoriesMode.set(RepositoriesMode.PREFER_PROJECT),
然后在项目build.gradle中添加settings.gradle中的所有仓库。


添加Flutter页面


官方文档:flutter.cn/docs/add-to…

Flutter 提供了 FlutterActivity,用于在 Android 应用内部展示一个 Flutter 的交互界面。需要在清单文件中注册FlutterActivity。


<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:screenOrientation="portrait"
/>

然后加载FlutterActivity


startActivity(
FlutterActivity.createDefaultIntent(requireContext())
)

此外还有withNewEngine、withCachedEngine等多种加载方式,具体可见官方文档。


添加Flutter视图


官方文档:flutter.cn/docs/add-to…

参考官方demo:github.com/flutter/sam…


创建FlutterViewEngine用以管理FlutterView、FlutterEngine、Activity三者。
FlutterEngine用以执行dart执行器,"showCell"为dart中方法名,FlutterEngine和FlutterView attach之后,会将"showCell"中生成的ui绘制到FlutterView上。


val engine = FlutterEngine(BaseApplication.instance)
engine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"showCell"))

在原生页面里面添加FlutterView还是比较麻烦的,需要开发者自己管理FlutterView、FlutterEngine、Activity三者之间生命周期联系。


作者:愿天深海
来源:juejin.cn/post/7306703076337483802
收起阅读 »

工作两年以来,被磨圆滑了,心智有所成长……

刚毕业时候年轻气盛,和邻居组的老板吵了几句。后来我晋升时,发现他是评委…… 曾经的我多么嚣张,现在的我就多么低调。 一路走来,磕磕绊绊,几年来,我总结了工作上的思考…… 工作思考有效控制情绪,在沟通时使用适当的表情包以传达善意。无论线上还是线下,都应避免争吵。...
继续阅读 »

刚毕业时候年轻气盛,和邻居组的老板吵了几句。后来我晋升时,发现他是评委…… 曾经的我多么嚣张,现在的我就多么低调。


一路走来,磕磕绊绊,几年来,我总结了工作上的思考……


工作思考

  1. 有效控制情绪,在沟通时使用适当的表情包以传达善意。无论线上还是线下,都应避免争吵。只有和气相处,我们才能推动工作的进展。
  2. 在讨论具体问题之前,先进行一些预备性的交流。情绪应放在第一位,工作讨论放在第二位。如果对方情绪不好,最好选择另一个时间再进行讨论。
  3. 在与他人交流时要保持初学者的态度和需求,不要用技术去怼人。
  4. 进入新团队先提升自己在团队的业务能力,对整个系统有足够的了解,不要怕问问题和学习。不要新入职就想毁天灭地,指手画脚 ”这里的设计不合理,那里有性能瓶颈“。
  5. 在各个事情上,都要比别人多了解一点。对于关键的事情要精通,对于其他事情也要多花一点时间去投入。
  6. 遇到困难时,先自己思考和尝试解决,然后再请教他人。不要机械地提问,也不要埋头一直搞而不主动提问。但如果是新入职,可以例外,多提问总没有坏处,但要在思考的基础上提问。
  7. 当向他人求助时,首先要清晰地阐述自己正在面临的问题、目标、已尝试的方法以及所需要的帮助和紧迫程度。所有的方面都要有所涉及。在提问之前,最好加上一句是否可以帮忙,这样对解决问题是否有帮助更加明确。因为别
  8. 一定有时间来帮助你,即使有时间,你也不一定找对了人。
  9. 在明确软件产品要解决的业务问题之前,先了解自己负责的那部分与业务的对应关系。
  10. 主要核心问题一定要提前叙述清楚,不要等别人问
  11. 要始终坚持追踪事情的进展,与与自己有交互的队友讨论接口,并关注他们的进度,以确保协调一致。
  12. 要主动向队友述说自己的困难,在项目延期或遇到困难时,要主动求助同事或领导,是否能分配部分工作给其他人,不要全部自己承担。
  13. 如果预计任务需要延期,要提前告知领导。如果有进展,也要及时向领导汇报。
  14. 如果无法参加会议但是自己是会议的重要参与者,一定要提前告知领导自己的进度、计划和想法,最好以书面形式或电话告知。如果可以远程参加,可以选择电话参加。除非有极其重要的事情,务必参加会议。不要假设别人都知道你的进度和想法。
  15. 要少说话,多做事。在开会时,不要凭借想当然的想法,可以询问其他小组的细节,但不要妄自揣测别人的细节,以为自己是对的。否则会被批评。
  16. 程序员如果经验丰富,很容易产生自我感觉良好的情绪。要避免这种情况,我们必须使用自己没有使用过的东西,并进行充分的测试,这样才能减少问题的出现。要提前考虑好所有细节,不要认为没有问题就不加考虑。要给自己留出处理问题的时间,并及时反馈并寻求帮助。
  17. 当与他人交流时,要始终保持有始有终的态度,特别是当寻求他人帮助时,最后一定要确认OK。要胆大心细,不要害怕犯错,要有成果,要快速并提高效率,不择手段地追求快速,并对结果负责。工作一定要完成闭环,要记事情要好,记住重要的事情并使用备忘录记录待办事项。
  18. 每完成一个项目后,应该回顾一下使用了什么知识、技能和工具。要总结并记录下这些,并与之前积累的知识和技能进行关联。如果发生了错误,也要记录下来,并将经验进行总结。
  19. 每天早上先思考今天要做什么,列出1、2、3,然后每天晚上下班时回顾已完成的任务、未完成的任务以及遇到的问题。
  20. 如果有待办事项没有立即处理,一定要用工具记录下来,不要心存侥幸以为自己能记住。

代码编写和技术问题

  1. 在代码编写过程中要认真对待,对于代码审核之前,要自己好好检查,给人一种可靠的感觉。
  2. 对于代码审核,不要过于苛刻,要容忍个人的发挥。
  3. 在提交代码给测试之前,应该先自行进行测试验证通过。
  4. 如果接口没有做到幂等性,那就会给未来的人工运维增加困难。当数据存在多份副本时,例如容量信息和上下游同时存在的资源,需要评估数据不一致的可能性以及解决方法。可以考虑通过数据校准或严格的代码编写来保证最终的一致性,或者考虑只在一方保存数据或以一方的数据为准。一旦出现数据不一致,则以其中一方的数据为准,无需人为干预即可自动达到数据再次一致。
  5. 要学会横向和纵向分割隔离系统,明确系统的边界,这样可以更好地进行并发合作开发和运维,提高效率。各个子系统应该独立变化,新的设计要考虑向后兼容性和上下游兼容性问题,包括上线期间的新老版本兼容。在设计评审阶段就应该重视这些问题。
  6. 如果在代码审查中无法发现业务问题或代码风格问题,不妨重点关注日志的打印是否合理和是否存在bug。
  7. 在依赖某个服务或与其他服务共享时,要确认该服务是否要废弃、是否是系统的瓶颈,以及是否可以自己进行改造或寻找更优的提供者。
  8. 使用缓存时注意预热,以防止开始使用时大量的缓存未命中导致数据库负载过高。
  9. 在使用rpc和mq、共享数据库、轮询、进程间通信和服务间通信时,要根据情况做出选择,并注意不要产生依赖倒置。
  10. 在接口有任何变动时,务必通过书面和口头确认。在这方面,要多沟通,尽量详细,以避免出现严重问题!毕竟,软件系统非常复杂,上下游之间的理解难以保持一致。
  11. 尽可能使用批量接口,并考虑是否需要完全批量查询。当批量接口性能较差时,设置适当的最大数量,并考虑客户端支持将批量接口聚合查询。批量接口往往是tp99最高的接口。
  12. 对于系统重要设计和功能,要考虑降级预案,并加入一些开关来满足安全性和性能需求。
  13. 如果数据不一致,可以考虑对比两方的不一致数据并打印错误日志,例如es/db等。
  14. 在系统设计之前,要充分调研其他人的设计,了解背景和现状。
  15. 废弃的代码应立即删除,如果以后需要,可以从git中找回。如果实在不想删除,也要注释掉!特别是对外的rpc、http接口,不使用的要立即删除,保持代码简洁。接手项目的人不熟悉背景情况,很难判断这段废弃代码的意义,容易造成混乱和浪费时间。要努力将其和其他有效代码联系起来,但这很困难。
  16. 在代码中要有详尽的日志记录!但是必须有条理和规范,只打印关键部分。对于执行的定时任务,应该打印足够详细的统计结果。最好使用简洁明了的日志,只记录最少量但最详细的信息,反馈程序的执行路径。
  17. 如果接口调用失败或超时,应该如何处理?幂等和重试如何处理?

当你写下一行代码前

  1. 要明确这行代码可能出现的异常情况以及如何处理,是将异常隔离、忽略还是单独处理,以防遗漏某些异常。
  2. 需要确保该行代码的输入是否已进行校验,并考虑校验可能引发的异常。
  3. 需要思考由谁调用该代码,会涉及哪些上游调用,并确定向调用者提供什么样的预期结果。
  4. 需要确定是否调用了一个方法或接口,以及该调用是否会阻塞或是异步的,并考虑对性能的影响。
  5. 需要评估该行代码是否可以进行优化,是否可以复用。
  6. 如果该行代码是控制语句,考虑是否能简化控制流程是否扁平。
  7. 对于日志打印或与主要逻辑无关的输出或报警,是否需要多加关注,因为它们可能还是很重要的。
  8. 如果代码是set等方法,也要仔细检查,避免赋错属性。IDE可能会有误提示,因为属性名前缀类似,set方法容易赋值错误。

当你设计一个接口时

  1. 接口的语义应该足够明确,避免出现过于综合的上帝接口
  2. 如果语义不明确,需要明确上下游的期望和需求。有些需求可以选择不提供给上游调用。
  3. 对于接口超时的处理,可以考虑重试和幂等性。在创建和删除接口时要确定是否具有幂等性,同时,幂等后返回的数据是否和首次请求一致也需要考虑。
  4. 接口是否需要防止并发,以及是否成为性能瓶颈也需要考虑。
  5. 设计接口时要确保调用方能够完全理解,如果他对接口的理解有问题,就需要重新设计接口。这一点非常关键,可以通过邮件确认或者面对面交流来确保调用方理解得清楚。
  6. 在开发过程中,需要定期关注队友的开发进度,了解他们是否已经使用了接口以及是否遇到了问题。这个原则适用于所有的上下游和相关方,包括产品和测试人员。要想清楚如何对接口进行测试,并与测试人员明确交流。
  7. 最好自己整理好测试用例,不要盲目地指望测试人员能发现所有的bug。
  8. 需要考虑是否需要批量处理这个接口,以减少rpc请求的次数。但即使是批量处理,也要注意一次批处理最多处理多少条记录,不要一次性处理全部记录,避免由于网络阻塞或批量处理时间过长导致上游调用超时,需要适度控制批量处理的规模。


作者:五阳神功
来源:juejin.cn/post/7306025036656787475
收起阅读 »

我为什么扔掉国企铁饭碗进入互联网

近期写了一篇文章《# 美团三年,总结的10条血泪教训》,后台很多网友留言对我过往工作经历感兴趣,小红书也收到很多薯友关于职业选择的咨询,于是就有了这篇文章。希望能给正在求职或者想转型的朋友,带来一些帮助。 01 放弃腾讯加入国企 我是2015年硕士毕业,14年...
继续阅读 »

近期写了一篇文章《# 美团三年,总结的10条血泪教训》,后台很多网友留言对我过往工作经历感兴趣,小红书也收到很多薯友关于职业选择的咨询,于是就有了这篇文章。希望能给正在求职或者想转型的朋友,带来一些帮助。


01 放弃腾讯加入国企


我是2015年硕士毕业,14年秋招时,面了6家公司,拿到了几个offer,包括腾讯和我后来入职的央企。


腾讯当时发offer的是SNG社交网络事业群(Social Network Gr0up,简称SNG),旗下主要产品是QQ和QQ音乐,在当时算是最核心的事业群之一。腾讯当时算是最炙手火热的大厂,类比于今日的字节。


能拿到offer,想想主要两个原因,一是我曾经在大连腾讯(全称是:腾讯无线大连大连研发中心,属于MIG事业群的一家子公司)实习了4个多月,对腾讯的企业文化还有内部协作模式非常了解;另外一点,要感谢我的导师,读研时参与的几个项目都非常有含金量,面试时也能很好地和面试官吹一吹。


拿到央企的offer倒是挺意外,央企是非常看重学校名气还有在校期间学习成绩的,在这两点上,我都不占优势。当时只是有面试的机会,经过一轮笔试和两轮面试,顺利拿到offer。很多学习成绩好,各种大奖拿到手软、一心奔着北京户口去的,反而止步二轮三轮。


所以,企业招聘,一定不是挑选最优秀的,而是选择最合适的,明白这个道理,在应聘的时候,就不会妄自菲薄,多去尝试,就有机会。


腾讯和央企这两个算是最好的offer,当时纠结到底选择哪一个。腾讯岗base深圳,央企base北京,所以,选择的核心,无非是选择未来在哪座城市发展。


在东北上学6.5年,喜欢上了北方的气候和文化,喜欢北方人的直接,也爱上了北方的包子和面条,最重要的,最好的一些朋友,都在北京和大连,所以,即便央企的薪资只有腾讯的一半,知名度也远远比不上腾讯, 但还是义无反顾的拒掉了腾讯offer。当时的出发点是,选择一个和自己有情感连接和认同感的城市,比一个完全陌生的城市,对我更重要。


人生每一个十字路口,都有很多不同的选择,遵从自己的内心很重要。


我很庆幸当时的选择。来北京的同一年,我的太太从南方另外一座城市出发,一路北上,拖着大大的行李箱,也开启了北漂。


我们2015年相识,18年正式确立关系,19年结婚,现在有一个2岁可爱的小朋友。


太太是我最好的朋友,是我的老师,也是对我人生影响最大的一个人,我们相互支持、陪伴、成长,一起经营我们的小家。


所以,人生中很多的相逢,也是冥冥中注定好的。


02 我为什么离开了国企


我加入的单位,是XX央企下的一个研究所,因为涉密原因,直接用SDT代替吧。


最初面试时,不知道SDT有多厉害,只知道福利待遇在各种研究所里还算可以,上班地点在朋友合租的房子附近,不用每天挤地铁。


入职后才知道,SDT的业务,基本属于行业垄断地位,唯一的竞争对手是中电科旗下的另一家研究所。


入职时的岗位应用软件开发岗,工作第一年,因为单位业务涉及全国各地,需要很多人做系统集成和技术支持,作为新人,首先得有奉献精神,很自然的被派到前线。有半年时间,一个人全国各地出差,涉及东北、华北、华南七省,最北去过黑龙江佳木斯,最南抵达海口。那是一段兴奋又略显孤单的时光。


图片国企出差时去过最远的地方


工作第二年,轮岗到另外一个部门,做嵌入式开发,接触到的业务,算是单位最核心业务了。


这一年,在技术上提升也非常快,从一个完全没有嵌入式经验的小白,到知道怎么和硬件交互,用C语言写上层应用,也在几个大的项目里,配合项目经理,做了很多项目管理的工作。


因为所从事的业务,关系到国家信息安全,工作的价值感也非常高,觉得自己直接在为国效力。


国企主要是项目制,项目周期长,大多数时候都不需要加班,按部就班的往前推进就行~因为没有明确的流程规范,很多时候,一个人要身兼数职,开发、测试、项目管理、各种类型的评审、打印材料…这些都要做,好处是,可以很好的培养综合能力,与之对应的,杂而不精。


程序员是一个特别讲究刻意练习的职业,在早期,深度比广度更重要。只有密集输出,成长才会快。我的室友安仔在一家私企做开发,经常跟我提spring、微服务、全栈、Native、H5这些概念,当时我听的一脸懵,莫非是国企的技术栈过于陈旧,跟不上主流软件开发了?那时候开始有了些危机感。


好在大多数时候不加班,下班后可以直接去五道口三联韬奋书店看书,或者跑步、游泳。这个时候慢慢建立起的运动和阅读的好习惯,一直持续至今。


工作第三年,完全适应了国企的节奏,也有了一些小成绩,但内心总觉得隐隐不安。常常问自己一个问题,我能在这里待一辈子吗?工作七八年后,最多跟我们现在的项目经理差不多,这是我想要的工作吗?为国家效力的情怀,能否支持我走得更远、更久?如果我在里面待了10年,有一天想出来,是否还能找到工作?


带着很多的疑问,还有对未来的不确定感,开始了探索之旅。也在合同结束前的几个月,尝试找工作。那时候,互联网还处在蓬勃发展期,华为还没被美国制裁,总共面了四五家公司,非常幸运拿到了华为和阿里的offer,最后选择了阿里。


03 在阿里的那些事


在阿里的两年多,算是快乐和痛苦并存吧。快乐源自于认知快速迭代,痛苦也源于此。


在阿里和国企的工作内容完全不一样,技术栈也完全不一样,前几个月,一边快速的学习各种新技术,学习大厂人的沟通模式,学习各种黑话,也要应对来自上级的压力,工作本身的压力,好在天性比较皮实,咬牙坚持着,化压力为动力,大半年的时间,慢慢缓了过来。


大厂的高压和竞争环境,迫使多数人放弃了真我,时刻要迎接战斗或者提防别人。人和人之间,也很难建立起真正的共情,这和在国企时大家相互帮助,共同把事做好,差别巨大。


阿里让我见识了真正的职场,也迫使我不断提升自己的专业性还有抗压能力,后面在美团的工作,相对来说,要轻松很多了。


图片
阿里20周年年会现场


不断打破舒适圈,重塑自我的过程,并不轻松。


扛过去了,再回首,会发现那是自己成长最快的一段时光,想到的反而都是感激。


离开阿里有好几年了,偶尔还会和前同事聚一聚,当大家放下曾经的身份,回归到真我时,每个人其实都很可爱,喜怒哀乐、家长里短,都能聊一聊,阿里人还是非常有情怀的一群人。


04 尊重过去,努力面前


离开国企后,常常会有人问我后不后悔,最近两年,互联网增长见顶,内卷加剧,小红书或者微信里面,咨询我的,多数都是了解如何进国企。


那么,放弃一份直接效力国家、在行业有垄断地位的国企工作,我后悔了吗?


答案是:不后悔。


人生是一个漫长的过程,搞清楚自己想要什么非常重要,在我们年轻的时候,所有的探索都是值得的。


大多数像我一样的普通人,很难直接回答自己想要什么,更多时候,是通过不断的做除法,排除不适合自己、或者自己不想要的。在不断的尝试,不断深挖自己过程中, 渐渐找到自己的定位,翻越愚昧之巅,跨过绝望之谷,走上开悟之坡。


职业生涯是这样,寻找人生另一半也是这样,和错的人挥手告别,才能和对的人相逢。


05 关于求职的一点点想法


作为过来人,如果要结合自己的经历,给几点建议,我想是这三点:


Ⅰ.小事从脑,大事从心


在人生每一个大的决定面前,我们不需要做太多的理性分析,追随自己的内心吧,世界可能会欺骗你,但你的内心不会。做一个有情感、有血有肉的人,胜过成为一位精致的利己主义者。


Ⅱ.人生没有白走的路,每一步都算数


每一个选择,都会引领我们走上一段奇妙的路,无论起步时选择的是hard模式还是easy模式,我们都有机会在这个过程中,不断地丰富和完善自己。所以,不要后悔曾经做过的任何选择,不要否定自己,不要和自己对抗,人生没有白走的路,每一步都算数。


Ⅲ.谦卑+open+学会感激,成为更好的自己


一直觉得自己是一个天资平庸的人,在人生的赛场上,没有太大的竞争力,但在很多个关键节点上,都能得到一些人的帮助,我想这也跟最近十多年被训练出的心性有关吧:谦卑+open+学会感激。


谦卑就是当自己不会的时候,承认自己的不足。把姿态放得足够低,才有机会被抬高;


open就是不封闭自己,能听得进各种声音,也能接纳各种人和事,对任何选择都持开放的心态;


学会感激,这个就很直接了,当我们珍视身边人的帮助,愿意多说一句感谢时,帮助过我们的人,下一次还会再帮我们一次,人生之路也会越走越宽。


最后,如果你在求职和职业选择、职业转型上,有任何的困惑,都可以加我微信和我聊聊。如果只是对辉哥好奇,也可以围观我的朋友圈,辉哥会不定期分享职场、育儿、亲密关系、阅读相关的心得体会。


作者:工程师酷里
来源:juejin.cn/post/7306266546968756239
收起阅读 »

美团三年,总结的10条血泪教训

在美团的三年多时光,如同一部悠长的交响曲,高高低低,而今离开已有一段时间。闲暇之余,梳理了三年多的收获与感慨,总结成10条,既是对过去一段时光的的一个深情回眸,也是对未来之路的一份期许。 倘若一些感悟能为刚步入职场的年轻人,或是刚在职业生涯中崭露头角的后起之秀...
继续阅读 »

在美团的三年多时光,如同一部悠长的交响曲,高高低低,而今离开已有一段时间。闲暇之余,梳理了三年多的收获与感慨,总结成10条,既是对过去一段时光的的一个深情回眸,也是对未来之路的一份期许。


倘若一些感悟能为刚步入职场的年轻人,或是刚在职业生涯中崭露头角的后起之秀,带来一点点启示与帮助,也是莫大的荣幸。


01 结构化思考与表达


美团是一家特别讲究方法论的公司,人人都要熟读四大名著《高效能人士的七个习惯》、《金字塔原理》、《用图表说话》和《学会提问》。


与结构化思考和表达相关的,是《金字塔原理》,作者是麦肯锡公司第一位女性咨询顾问。这本书告诉我们,思考和表达的过程,就像构建金字塔(或者构建一棵树),先有整体结论,再寻找证据,证据之间要讲究相互独立、而且能穷尽(MECE原则),论证的过程也要按特定的顺序进行,比如时间顺序、空间顺序、重要性顺序…


作为大厂社畜,日常很大一部分工作就是写文档、看别人文档。大家做的事,但最后呈现的结果却有很大差异。一篇逻辑清晰、详略得当的文档,给人一种如沐春风的感受,能提炼出重要信息,是好的参考指南。


结构化思考与表达算是职场最通用的能力,也是打造个人影响力最重要的途径之一。


02 忘掉职级,该怼就怼


在阿里工作时,能看到每个人的Title,看到江湖地位高(职级高+入职时间早)的同学,即便跟自己没有汇报关系,不自然的会多一层敬畏。推进工作时,会多一层压力,对方未读或已读未回时,不知如何应对。


美团只能看到每个人的坑位信息,还有Ta的上级。工作相关的问题,可以向任何人提问,如果协同方没有及时响应,隔段时间@一次,甚至"怼一怼",都没啥问题,事情一直往前推进才最重要。除了大象消息直接提问外,还有个大杀器--TT(公司级问题流转系统),在上面提问时,加上对方主管,如果对方未及时回应,问题会自动升级,每天定时Push,直到解决为止。


我见到一些很年轻的同事,他们在推动OKR、要资源的事上,很有一套,只要能达到自己的目标,不会考虑别人的感受,最终,他们还真能把事办成。


当然了,段位越高的人,越能用自己的人格魅力、影响力、资源等,去影响和推动事情的进程,而不是靠对他人的Push。只是在拿结果的事上,不要把自己太当回事,把别人太当回事,大家在一起,也只是为了完成各自的任务,忘掉职级,该怼时还得怼。


03 用好平台资源


没有人能在一家公司待一辈子,公司再牛,跟自己关系不大,重要的是,在有限的时间内,最大化用好平台资源。


在美团除了认识自己节点的同事外,有幸认识一群特别棒的协作方,还有其他BU的同学。


这些优秀的人身上,有很多共同的特质:谦虚、利他、乐于分享、双赢思维。


有两位做运营的同学。


一位是无意中关注他公众号结识上的。他公众号记录了很多职场成长、家庭建造上的思考和收获,还有定期个人复盘。他和太太都是大厂中层管理者,从文章中看到的不是他多厉害,而是非常接地气的故事。我们约饭了两次,有很多共同话题,现在还时不时有一些互动。


一位职级更高的同学,他在内网发起了一个"请我喝一杯咖啡,和我一起聊聊个人困惑"的活动,我报名参与了一期。和他聊天的过程,特别像是一场教练对话(最近学习教练课程时才感受到的),帮我排除干扰、聚焦目标的同时,也从他分享个人成长蜕变的过程,收获很多动力。(刚好自己最近也学习了教练技术,后面也准备采用类似的方式,去帮助曾经像我一样迷茫的人)


还有一些协作方同学。他们工作做得超级到位,能感受到,他们在乎他人时间;稍微有点出彩的事儿,不忘记拉上更多人。利他和双赢思维,在他们身上是最好的阐释。


除了结识优秀的人,向他们学习外,还可以关注各个通道/工种的课程资源。


在大厂,多数人的角色都是螺丝钉,但千万不要局限于做一颗螺丝钉。多去学习一些通识课,了解商业交付的各个环节,看清商业世界,明白自己的定位,超越自己的定位。


04 一切都是争取来的


工作很多年了,很晚才明白这个道理。


之前一直认为,只要做好自己该做的,一定会被看见,被赏识,也会得到更多机会。但很多时候,这只是个人的一厢情愿。除了自己,不会有人关心你的权益。


社会主义初级阶段,我国国内的主要矛盾是人民日益增长的物质文化需要同落后的社会生产之间的矛盾。无论在哪里,资源都是稀缺的,自己在乎的,就得去争取。


想成长某个技能、想参与哪个模块、想做哪个项目,升职加薪…自己不提,不去争取,不会有人主动给你。


争不争取是一回事,能不能得到是一回事,只有争取,才有可能得到。争取了,即便没有得到,最终也没失去什么。


05 关注商业


大公司,极度关注效率,大部分岗位,拆解的粒度越细,效率会越高,这些对组织是有利的。但对个人来说,则很容易螺丝钉化。


做技术的同学,更是这样。


做前端的同学,不会关注数据是如何落库的;做后端的同学,不会思考页面是否存在兼容性问题;做业务开发的,不用考虑微服务诸多中间件是如何搭建起来的……


大部分人都想着怎么把自己这摊子事搞好,不会去思考上下游同学在做些什么,更少有人真正关注商业,关心公司的盈利模式,关心每一次产品迭代到底带来哪些业务价值。


把手头的事做好是应该的,但绝不能停留在此。所有的产品,只有在商业社会产生交付,让客户真正获益,才是有价值的。


关注商业,能帮我们升维到老板思维,明白投入产出比,抓大放小;也帮助我们,在碰到不好的业务时,及时止损;更重要的是,它帮助我们真正看清趋势,提前做好准备。


《五分钟商学院》系列,是很好的商业入门级书籍。尽管作者刘润最近存在争议,但不可否认,他比我们大多数人段位还是高很多,他的书值得一读。


06 培养数据思维


当今数字化时代,数据思维显得尤为重要。数据不仅可以帮助我们更好地了解世界,还可以指导我们的决策和行动。


非常幸运的是,在阿里和美团的两份经历,都是做商业化广告业务,在离钱 最近的地方,也培养了数据的敏感性。见过商业数据指标的定义、加工、生产和应用全流程,也在不断熏陶下,能看懂大部分指标背后的价值。


除了直接面向业务的数据,还有研发协作全流程产生的数据。数据被记录和汇总统计后,能直观地看到每个环节的效率和质量。螺丝钉们的工作,也彻彻底底被数字量化,除了积极面对虚拟化、线上化、数字化外,我们别无他法。


受工作数据化的影响,生活中,我也渐渐变成了一个数据记录狂,日常运动(骑行、跑步、健走等)必须通过智能手表记录下来,没带Apple Watch,感觉这次白运动了。每天也在很努力地完成三个圆环。


数据时代,我们沦为了透明人。也得益于数据被记录和分析,我们做任何事,都能快速得到反馈,这也是自我提升的一个重要环节。


07 做一个好"销售"


就某种程度来说,所有的工作,本质都是销售。


这是很多大咖的观点,我也是很晚才明白这个道理。


我们去一家公司应聘,本质上是在讲一个「我很牛」的故事,销售的是自己;日常工作汇报、季度/年度述职、晋升答辩,是在销售自己;在任何一个场合曝光,也是在销售自己。


如果我们所服务的组织,对外提供的是一件产品或一项服务,所有上下游协作的同学,唯一在做的事就是,齐心协力把产品/服务卖出去, 我们本质做的还是销售。


所以, 千万不要看不起任何销售,也不要认为认为销售是一件很丢面子的事。


真正的大佬,随时随地都在销售。


08 少加班多运动


在职场,大家都认同一个观点,工作是做不完的。


我们要做的是,用好时间管理四象限法,识别重要程度和优先级,有限时间,聚焦在固定几件事上。


这要求我们不断提高自己的问题识别能力、拆解能力,还有专注力。


我们会因为部分项目的需要而加班,但不会长期加班。


加班时间短一点,就能腾出更多时间运动。


最近一次线下培训课,认识一位老师Hubert,Hubert是一位超级有魅力的中年大叔(可以通过「有意思教练」的课程链接到他),从外企高管的位置离开后,和太太一起创办了一家培训机构。作为公司高层,日常工作非常忙,头发也有些花白了,但一身腱子肉胜过很多健身教练,给人的状态也是很年轻。聊天得知,Hubert经常5点多起来泡健身房~


我身边还有一些同事,跟我年龄差不多,因为长期加班,发福严重,比实际年龄看起来苍老10+岁;


还有同事曾经加班进ICU,幸好后面身体慢慢恢复过来。


某某厂员工长期加班猝死的例子,更是屡见不鲜。


减少加班,增加运动,绝对是一件性价比极高的事。


09 有随时可以离开的底气


当今职场,跟父辈时候完全不一样,职业的多样性和变化性越来越快,很少有人能够在同一份工作或同一个公司待一辈子。除了某些特定的岗位,如公务员、事业单位等,大多数人都会在职业生涯中经历多次的职业变化和调整。


在商业组织里,个体是弱势群体,但不要做弱者。每一段职场,每一项工作,都是上天给我们的修炼。


我很喜欢"借假修真"这个词。我们参与的大大小小的项目, 重要吗?对公司来说可能重要,对个人来说,则未必。我们去做,一方面是迫于生计;


另外一方面,参与每个项目的感悟、心得、体会,是真实存在的,很多的能力,都是在这个过程得到提升。


明白这一点,就不会被职场所困,会刻意在各样事上提升自己,积累的越多,对事务的本质理解的越深、越广,也越发相信很多底层知识是通用的,内心越平静,也会建立起随时都可以离开的底气。


10 只是一份工作


工作中,我们时常会遇到各种挑战和困难,如发展瓶颈、难以处理的人和事,甚至职场PUA等。这些经历可能会让我们感到疲惫、沮丧,甚至怀疑自己的能力和价值。然而,重要的是要明白,困难只是成长道路上的暂时阻碍,而不是我们的定义。


写总结和复盘是很好的方式,可以帮我们理清思路,找到问题的根源,并学习如何应对类似的情况。但也要注意不要陷入自我怀疑和内耗的陷阱。遇到困难时,应该学会相信自己,积极寻找解决问题的方法,而不是过分纠结于自己的不足和错误。


内网常有同学匿名分享工作压力过大,常常失眠甚至中度抑郁,每次看到这些话题,非常难过。大环境不好,是不争的事实,但并不代表个体就没有出路。


我们容易预设困难,容易加很多"可是",当窗户布满灰尘时,不要试图努力把窗户擦干净,走出去吧,你将看到一片蔚蓝的天空。


最后


写到最后,特别感恩美团三年多的经历。感谢我的Leader们,感谢曾经并肩作战过的小伙伴,感谢遇到的每一位和我一样在平凡的岗位,努力想带给身边一片微光的同学。所有的相遇,都是缘分。


作者:traveller
来源:juejin.cn/post/7298927247145910281
收起阅读 »

结合个人经历讲述近年IT行情

2022年也就是去年,疫情最严重的时候,全部都封闭在家,甚至有好多公司已经开始了裁员行动,当然也包括我所在的公司,因为主营业务就是靠销售进行盈利,既然足不出户,所以公司的盈利骤降,索性实施裁员决定,我当时所在的整个部门五十多号人吧,全部都裁掉了,没有一人幸免。...
继续阅读 »

2022年也就是去年,疫情最严重的时候,全部都封闭在家,甚至有好多公司已经开始了裁员行动,当然也包括我所在的公司,因为主营业务就是靠销售进行盈利,既然足不出户,所以公司的盈利骤降,索性实施裁员决定,我当时所在的整个部门五十多号人吧,全部都裁掉了,没有一人幸免


可能那个时候大家都觉的是疫情惹的祸,包括我也这么认为,认为疫情过去了就会好起来,但是似乎并没有那么理想。
我后来通过朋友内推进了一家公司,虽说薪资没有涨幅,但是由原来的大小周变成了双休且很少加班,当然,赛道可能就不是很好,但是由于本人比较菜,也就随随便便有个工作能做,自己能学习就行。


工作期间我也有一些技术交流群嘛,然后也在群里了解到今年有好多人失业后大多数都是两三个月后还没找到工作,也有从22年底到23年休息了半年多,当时可能由于我找工作运气好,没有经历太长时间,疫情解封了没过多久就入职了。入职的第一天我上一家的师兄也喊我过去他刚入职的一家,问我有没有意向,我师兄入职的公司是化妆品公司,研发部门属于创业类型,我们上一个公司的师兄们大概有十个左右都过去了,那边赛道也挺好,薪资待遇也很好,电商赛道,可惜的是由于我当时刚入职,而且还是朋友内推的,索性拒绝了师兄的好意,也是错失了这个大好前景的机会。


后面我在这家公司虽说工作内容没有什么可圈可点的,但是我们团队内部小组人员氛围很好,年假时间也长,半个月。但是好景不长,今年五六月份就听小道消息说公司要裁员,具体也不确定是哪些人,六月份发现有一部分人确实已经走了,我也有一定的紧张感,但是可能还是抱着侥幸或者说对行情的美好期望,我就没有太过于紧张。终于,在八月份的时候确定了裁员我们项目组,我所在的项目组要砍掉,当时也是想的正常赔偿也行,后面还能休息一段时间,然后再找工作。是的,理想很美好。


后续我先回了趟老家待了一段时间
eb1ffb0941874ca68aadc020ae8f052.jpg
后面回上海后,又和同事们一起去游了宏村,爬了黄山(爬山真的好累~)看了日落和日出。


e5a2fcb162d73bed7cbc4acf6f34ce4.jpg


0ef9241a4b6c110f19021ddb72b3ec8.jpg
玩完回来了也是隔三差五的一起去打打羽毛球,一起吃个饭唱个歌,反正那个时候挺惬意的。


后面我也是改了简历,也在boss、前程无忧上投了试试,发现没什么人去看,未读居多,我想着先这样吧,后面慢慢再继续投,时间越来越久了,发现好多都是已读不回,或者不合适;既然不合适那我就从简历入手了,一个多月期间我的简历经历了大概近七八次改版由最开始的我担心写的到时候面试不会很尴尬,到后面的先把牛吹出去,有面试了再说
可是我发现依然没有什么太多的改变,约了几家面试,但是都没有过,也只能怪自己太菜了吧,毕业三年了什么都不会,后面我想试试转战外包呢,毕竟给钱多,但是发现现在的外包给的薪资还不如甲方给的多,真的就很离谱,以前的行情选择外包起码有两个好处,一是面试简单,很容易就通过,获得offer,二是薪资给的贼高。但是我现在发现,这两点在今年已经全然消失不见,那我想不到进外包还有什么好处了。就先这样外包甲方都挑着投投,先面面看看,而且现在的面试难度提升了好高对我来说,真的很难,首先公司候选人多,毕竟裁员失业的人太多了,哪怕到年底了现在依然有好多公司还在执行裁员计划,其次是有的人要的薪资比你低,那么你也没有什么太大的优势


唉,我现在依然处于失业状态,也是很可惜当时没有跟着师兄走,也只能说是当时的一个选择错误,自己的眼界太低了。也只能怪自己的能力太差了,毕竟再烂的行情那些技术大牛也不会找不到工作。慢慢看吧,我现在的期望是平薪或者稍微降薪去一个甲方,好好的沉淀一下自己,提升自己的技术能力,以防后面类似的情况发生,不过目前最重要的还是找工作吧。


我个人可能还想期待明年的金三银四,但是我又感觉明年甚至更严峻,害,无奈,甚至想趁此转行,但是也不知道自己能干什么,甚至别的行业也亦是如此,工作不好找。或许我真的期望太高了,要结合自身去降低自己的期望吧,加油吧,祝愿现在还在找工作的人都能在年底收获一份还算不错的offer,加油吧~


作者:镀己
来源:juejin.cn/post/7306266546968707087
收起阅读 »

滴滴崩溃超过12小时,这世道是怎么了

从 2023 年 11 月 27 日晚上 10 点左右截止 2023 年 11 月 28 日中午 12 点期间,滴滴打车 APP、小程序得常用功能终于相继恢复正常。放眼整个事件来看,滴滴打车得故障时间已然超过 12 个小时,整个事件放在 2023 年得互联网来...
继续阅读 »

从 2023 年 11 月 27 日晚上 10 点左右截止 2023 年 11 月 28 日中午 12 点期间,滴滴打车 APP、小程序得常用功能终于相继恢复正常。放眼整个事件来看,滴滴打车得故障时间已然超过 12 个小时,整个事件放在 2023 年得互联网来看真是相当炸裂。



回顾昨晚,原本我在写下 降本增笑,滴滴打车也崩了 这篇文章后就入睡了,想着今天晚上滴滴得程序员可有得忙了。


没想到的是 28 号 7 点起来 9 点到公司发现滴滴打车 APP 昨晚出现的绝大部分故障任然还在,骑行功能无法使用、底部标签栏车主、领打车费等还是无法打开,查看微博,滴滴官方也是在 7 点 33 分左右对昨晚事故做出了第二次回应,



也就是说 28 号早高峰上班期间,全国上下不知有多少用户任然受到此次事故影响导致无法正常打车、骑行等。进一步也可能会导致这些用户的全勤不保。



心痛一波这些用户 ❤️‍。



我当时就在想,滴滴得程序员昨天晚上去干什么了?一晚上都修不完这些 bug?对于大厂的高级研发,架构师来说,线上系统发布出现问题时的回滚流程应该是一套很成熟的体系,但是万万没想到这个事故竟然能持续一晚上还没结束。



没想到 2023 年的互联网还有这么到超出我对大厂认知的事情在不断发生 😔。



事故影响最后一直持续到 28 号 12 点左右才相继结束。


那么这里面到底出了哪些问题?作为一个互联网从业者,我给大家分析一下这里面可能存在的原因。


事故分析


降本增效


一个词概括 2023 年的互联网行情就是 “降本增效” 。疫情三年可以说是导致本就难做的实体行业是难上加难,但确给互联网行业带来了一大波用户增长。可谁曾想到来都 2023 年初,互联网行业可以说是寸草不生,直接进入存量内卷时代,各家都不在出新产品,开始巩固护城河。


记得三年前阿里的市值巅峰是 8000 亿跌倒现在不到 2000 亿,想想这里面市值缩水了 4 倍,想想 8000 亿市值招了多少人,现在不到 2000 亿,那又需要裁多少人嘞?年初阿里裁撤 15000 人的消息还历历在目。虽然这里面有全球大环境、国家政策以及市场竞争多众多因素导致。但是裁员时,资本又不会管你一个普通底层干活的技术员工干了多少年,技术上做出了多少贡献,他只会看优化后的财务报表有多么好看。



回看滴滴这几年经历了什么,2021 年 6 月 30 日 滴滴是美国纳斯达克上市的,估值最高达到 800 亿美元,随后 2021 年 7 月 4 日,国家网信办发布公告称,“滴滴出行”APP 存在严重违法违规收集使用个人信息问题,依据相关法律规定,通知应用商店下架“滴滴出行”APP,滴滴宣布暂停新用户注册。2022 年 7 月 21 日滴滴被处以罚款 80.26 亿人民币,2023 年 1 月 16 日 滴滴宣布整改完毕,恢复新用户注册,整个事件才告一段落。


可以说滴滴也为自己的违法行为付出了代价,这里面滴滴为了能活下去裁撤了公司多少人员我们也不得而知。但是影响了滴滴打车系统的稳定性是肯定的,不然也不会有今天这件事情。


降本增效、降本增效,今年提到互联网企业就离不开这四个字。具体怎么执行嘞?


何为降本:裁掉底层干活的人,留下一堆中层领导,相信这是大部分互联网公司的降本举措。


何为增效:一个人干两个人的事情,这就是增效,相信这也是大部分互联网公司的增效举措。


试想一下,假如我是一个互联网大厂开发,周六领导开会整个部门被裁,周一上午 HR 宣读通知,下午要求走人,只留下我一人维护老系统。你说我咋接?这系统是整个部门同事多年以来共同合作开发维护,我也只是负责其中一个模块。现在要我接收全部,能不出问题吗?


OK,进一步来说,假如某个模块出问题了我找谁,或者谁担责,我只能向上反馈。领导一顿 PUA 教育,最后还是底层干活的我默默承受了一切。


结合滴滴崩溃这件事来说,会不会是某个人留下来的隐秘 bug 终于在 27 号晚 10 点爆发,而留下来的程序员不知道如何解决,而且当时半夜 12 点想要联系前同事,打不通电话是不是也有可能。这一切的结果叠加也就导致了滴滴此次事故持续事件超过了 12 个小时。


这个事情很难说,因为官方也不可能承认是裁员导致。


底层依赖更新出错


还记得语雀上次事故,事故原因就是运维工具升级报错导致。滴滴这一波会不会也是这个原因嘞?


想一想如果是应用层服务出错,线上环境针对应用服务都有完整的回滚措施。回滚操作一般不会超过 10 分钟,那么像滴滴这样的互联网大厂,就算线上服务真的很多,回滚也不可能超过一晚上 6 个小时吧。所以造成此次事故的元凶就不太可能是应用层服务。


那么造成这次事故的核心原因就只可能是一个平台已经使用了多年的底层依赖,而不凑巧的是昨天晚上某个运维升级了这个依赖导致平台应用服务全面崩溃。


想一想,在我这么多年的互联网从业经验中,一般公司只会针对线上环境针的应用服务做回滚举措。而运维负责的一些平台底层依赖,很少又回滚这一说吧。


所以这个原因是有可能的。


最后聊两句


分析到这里,这篇文章要将的内容也就讲完了,互联网大厂一直把高可用、异地多活、两地三中心这些词语挂在嘴边,但是 2023 年以来,阿里崩了、语雀崩了、滴滴也崩了,可以说互联网大厂 APP 或者服务崩了在今年已经成了一种常态。还是希望大家保持常态,大厂的 APP 也是无数人堆出来,是人就会犯错,习惯就好。


博君一笑


不过在我看了网友评论后,我又觉得合理起来,历来网友的想象力都比较丰富。





作者:waynaqua
来源:juejin.cn/post/7306457908636385307
收起阅读 »

API到底有哪些使用价值

API已成为主流企业计算架构的基础。这意味着他们需要与标准成熟应用程序相同程度的管理和监视。运转良好的API需要“先进的监视,指标和分析,以告知所有流程功能,这些功能不仅可以捕获原始流量数据,还可以使这些数据可操作以帮助防止滥用,洞察开发人员的经验,塑造产品迭...
继续阅读 »


API已成为主流企业计算架构的基础。这意味着他们需要与标准成熟应用程序相同程度的管理和监视。

运转良好的API需要先进的监视,指标和分析,以告知所有流程功能,这些功能不仅可以捕获原始流量数据,还可以使这些数据可操作以帮助防止滥用,洞察开发人员的经验,塑造产品迭代,调整内部利益相关者并发掘未开发的机会。

随着越来越多的非开发人员也开始涉足API的创建和部署,这一点变得尤为重要。 最近进行的一项调查发现,使用API的人员中有53%的人没有开发人员的资质,去年同期这一比例为41 整个企业中有这么多人使用APIAPI程序可能很快就会偏离正轨。 企业可能最终会支持并为几十个笨拙而未被充分利用的API买单,同时用那些设计糟糕且被过度使用的API来增加基础设施的负担。

这就需要更深入地了解API体验。企业需要洞察哪些API正在被采用,以及这种采用可能预示着什么样的新兴商业机会或投资重点。

可衡量的指标始终是一件好事,而挑战在于为API确定和捕获正确的指标,这些API倾向于在可能不同于传统企业软件的新规则集下运行。因此以下这些指标将帮助API创建者和业务领导者了解其工作相对成功的标准:

API的速度在当今快速发展的业务环境中,这是一个基本的关键绩效指标。实现业务目标的能力需要与快速启动API的能力相平衡。当这个目标也为业务所请求的API进行细分时,它就成为了一个有用的衡量所需功能上市时间的指标。

上线速度:API创建者可以多快注册他们的应用,获取密钥,访问控制面板并发现API”? 此上线过程应尽可能自动化。 一个自动的注册过程应该允许访问低风险的API和沙盒环境,使开发人员可以立即提高工作效率

升级速度:一旦开发人员上线,门户便可以提供升级选项,通过它们可以请求访问更敏感的数据和业务功能。

流量的增长:这可能是最重要的KPI,因为它可以通过持续监控,改善和通过API推动价值来帮助API程序发展强大的DevOps文化。

业务范围:这一点也很重要。 习惯于传统集成或旧系统的业务部门可能会拒绝采用API程序。”“通过对这一目标进行优先排序,该计划可以更快地将此类推回到适当的执行级别进行解决。此外,可以更快速地发现高性能API

降低成本:向高层管理人员展示总是一件好事。API的重用机制节省了大量的开发资源。随着API重用的增加和现有API的不必要重复的减少,通常会实现显著的成本降低。

直接收入:API货币化是最重要的前沿领域。 这样的KPI有助于获取API支持的核心产品销售收入

在业务上下层中应用API的开发人员数量:API团队应该区分在已知业务上下层中使用API的开发人员的总体采用和特定开发人员的采用,例如集成现有生态系统合作伙伴的应用程序。

客户可访问的应用程序数量:“如果一个API程序导致创建许多只供内部使用而不供客户使用的应用程序,那么它有时会招致内部的批评和对程序的放弃。

合作伙伴数量:这样的KPI“可用于加快合作伙伴的范围,推动采用并向现有业务部门展示成功。

数聚变API致力于为企业提供数据管理解决方案,包括数据采集转发和数据集成共享平台,帮助企业完成数字化转型,提供专业的API解决方案,更多信息可点击了解:https://apifusion.goldwind.com/

 

收起阅读 »

基于模块暴露和Hilt的Android模块通信方案

ModuleExpose 项目地址:github.com/JailedBird/… 序言 Android模块化必须要解决的问题是 如何实现模块间通信 ?而模块之间通信往往需要获取相同的实体类和接口,造成部分涉及模块通信的接口和实体类被迫下沉到基础模块,导致 基...
继续阅读 »

ModuleExpose


项目地址:github.com/JailedBird/…


序言


Android模块化必须要解决的问题是 如何实现模块间通信 ?而模块之间通信往往需要获取相同的实体类和接口,造成部分涉及模块通信的接口和实体类被迫下沉到基础模块,导致 基础模块代码膨胀、模块代码分散和不便维护等问题;


ModuleExpose方案使用模块暴露&依赖注入框架Hilt的方式,实现模块间通信:



  • 使用模块暴露(模块api化)解决基础模块下沉问题

  • 使用依赖注入框架Hilt实现基于接口的模块解耦方案


简介


ModuleExpose,是将module内部需要暴露的代码通过脚本自动暴露出来;不同于手动形式的接口下沉,ModuleExpose是直接将module中需要暴露的代码完整拷贝到module_expose模块,而module_expose模块的生成、拷贝和配置是由ModuleExpose脚本自动完成,并保证编译时两者代码的完全同步;


最终,工程中包含如下几类核心模块:




  • 基础模块:基础代码封装,可供任何业务模块使用;




  • 业务模块:包含业务功能,业务模块可以依赖基础模块,但无法依赖其他业务模块(避免循环依赖);




  • 暴露模块:由脚本基于业务模块或基础模块自动拷贝生成,业务模块可依赖其他暴露模块(通过compileOnly方式,只参与编译不参与打包),避免模块通信所需的接口、数据实体类下沉到基础模块,造成基础模块膨胀、业务模块核心类分散到基础模块等问题;




注意这种方案并非原创,原创出处如下:


思路原创:微信Android模块化架构重构实践



先寻找代码膨胀的原因。


翻开基础工程的代码,我们看到除了符合设计初衷的存储、网络等支持组件外,还有相当多的业务相关代码。这些代码是膨胀的来源。但代码怎么来的,非要放这?一切不合理皆有背后的逻辑。在之前的架构中,我们大量使用Event事件总线作为模块间通信的方式,也基本是唯一的方式。使用Event作为通信的媒介,自然要有定义它的地方,好让模块之间都能知道Event结构是怎样的。这时候基础工程好像就成了存放Event的唯一选择——Event定义被放在基础工程中;接着,遇到某个模块A想使用模块B的数据结构类,怎么办?把类下沉到基础工程;遇到模块A想用模块B的某个接口返回个数据,Event好像不太适合?那就把代码下沉到基础工程吧……


就这样越来越多的代码很“自然的”被下沉到基础工程中。


implementation工程提供逻辑的实现。api工程提供对外的接口和数据结构。library工程,则提供该模块的一些工具类。



项目原创: github/tyhjh/module_api



如果每次有一个模块要使用另一个模块的接口都把接口和相关文件放到公共模块里面,那么公共模块会越来越大,而且每个模块都依赖了公共模块,都依赖了一大堆可能不需要的东西;


所以我们可以提取出每个模块提供api的文件放到各种单独的模块里面;比如user模块,我们把公共模块里面的User和UserInfoService放到新的user-api模块里面,这样其他模块使用的时候可以单独依赖于这个专门提供接口的模块,以此解决公共模块膨胀的问题



本人工作:



  • 使用kts和nio重写脚本,基于性能的考量,对暴露规则和生成方式进行改进;

  • nowinandroid项目编译脚本系统、Ksp版本的Hilt依赖注入框架、示例工程三者结合起来,完善基于 模块暴露&依赖注入框架 的模块解耦示例工程;

  • 将api改名expose(PS:因内部项目使用过之前的api方案,为避免冲突所以改名,也避免和大佬项目名字冲突😘 脚本中亦可自定义关键词)


术语说明:



  • 部分博客中称这种方式为模块api化,我觉得这是合理的;本文的语境中的expose和api是等价的意思;


模块暴露


1、项目启用kts配置


因为脚本使用kts编写,因此需要在项目中启用kts配置;如因为gradle版本过低等原因导致无法接入kts,那应该是无法使用的;后续默认都开启kts,并使用kts语法脚本;


2、导入脚本到gradle目录&修改模板


请拷贝示例工程gradle/expose目录到个人项目gradle目录,拷贝后目录如下:


Path
ModuleExpose\gradle

gradle
│ libs.versions.toml
├─expose
│ build_gradle_template_android
│ build_gradle_template_java
│ build_gradle_template_expose
│ expose.gradle.kts
└─wrapper
gradle-wrapper.jar
gradle-wrapper.properties

其中:expose.gradle.kts是模块暴露的核心脚本,包含若干函数和配置参数;


其中:build_gradle_template_android和build_gradle_template_java脚本模板因项目不同而有所不同,需要自行根据项目修改,否则无法编译;




  • build_gradle_template_android,生成Android模块的脚本模板,注意高版本gradle必须配置namespace,因此最好保留如下的配置(细则见脚本如何处理的):


    android {
    namespace = "%s"
    }



  • build_gradle_template_java, 生成Java模块的脚本模板,配置较为简单;




  • includeWithApi函数使用build_gradle_template_android模板生成Android Library模块




  • includeWithJavaApi函数使用build_gradle_template_java模板生成Java Library模块




  • build_gradle_template_expose,不同于build_gradle_template_android、build_gradle_template_java的模板形式的配置,使用includeWithApi、includeWithJavaApi时,会优先检查模块根目录是否存在build_gradle_template_expose,如果存在则优先、直接将build_gradle_template_expose内容拷贝到module_expose, 作为build.gradle.kts ! 保留这个配置的原因在于:如果需要暴露的类,引用三方类如gson、但不便将三方库implementation到build_gradle_template_android,这会导致module_expose编译报错,因此为解决这样的问题,最好使用自定义module_expose脚本(拷贝module的配置、稍加修改即可)


    PS:注意这几个模板都是无后缀的,kts后缀文件会被IDE提示一大堆东西;




注意: Java模块编译更快,但是缺少Activity、Context等Android环境,请灵活使用;当然最灵活的方式是为每个module_expose单独配置build_gradle_template_expose (稍微麻烦一点);另外,如果不用includeWithJavaApi,其实build_gradle_template_java也是不需要的;


3、settings.gradle.kts导入脚本函数


根目录settings.gradle.kts配置如下:


apply(from = "$rootDir/gradle/expose/expose.gradle.kts")
val includeWithApi: (projectPaths: String) -> Unit by extra
val includeWithJavaApi: (projectPaths: String) -> Unit by extra

(PS:只要正确启用kts,settings.gradle应该也是可以导入includeWithApi的,但是我没尝试;其次老项目针对ModuleExpose改造kts时,可以渐进式改造,即只改settings.gradle.kts即可)


4、模块配置


将需要暴露的模块,在settings.gradle.kts 使用includeWithApi(或includeWithJavaApi)导入;


includeWithApi(":feature:settings")
includeWithApi(":feature:search")

即可自动生成新模块 ${module_expose};然后在模块源码目录下创建名为expose的目录,将需要暴露的文件放在expose目录下, expose目录下的文件即可在新模块中自动拷贝生成;


生成细则:


1、 模块支持多个expose目录(递归、含子目录)同时暴露,这可以避免将实体类,接口等全部放在单个expose,看着很乱


2、 expose内部的文件,默认全部复制,但脚本提供了开关,可以自行更改并配置基于文件名的拷贝过滤;


5、使用module_expose模块


请使用 compileOnly 导入项目,如下:


compileOnly(project(mapOf("path" to ":feature:search_expose")))

错误:会导致资源冲突


implementation(project(mapOf("path" to ":feature:search_expose")))

原理解释:compileOnly只参与编译,不会被打包;implementation参与编译和打包;


因此search_expose只能使用compileOnly导入,确保解耦的模块之间可以访问到类引用,但不会造成打包时2个类相同的冲突问题;


依赖注入


基于模块暴露的相关接口,可以使用依赖注入框架Hilt实现基于接口的解耦; 当然如果大家不使用Hilt技术栈的话,这节可以跳过;


本节内容会以业务模块search和settings为例,通过代码展示:



  • search模块跳转到settings模块,打开SettingsActivity

  • settings模块跳转到search模块,打开SearchActivity


PS:关于Hilt的配置和导入,本项目直接沿用nowinandroid工程中build-logic的配置,具体配置和使用请参考本项目和nowinandroid项目;


1、 基本配置&工程结构:


image.png


导入脚本之后,使用includeWithApi导入三个业务模块,各自生成对应的module_expose;


注意,请将*_expose/添加到gitignore,避免expose模块提交到git


2、 业务模块接口暴露&实现


settings模块expose目录下暴露SettingExpose接口, 脚本会自动将其同步拷贝到settings_expose中对应expose目录


image.png


exposeimpl/SettingExposeImpl实现SettingExpose接口的具体功能,完善跳转功能


class SettingExposeImpl @Inject constructor() : SettingExpose {
override fun startSettingActivity(context: Context) {
SettingsActivity.start(context)
}
}

3、 Hilt添加注入接口绑定


使用Hilt绑定全局单例SettingExpose接口实现,其对应实现为SettingExposeImpl


image.png


4、 search模块compileOnly导入settings_expose


compileOnly(projects.feature.settingsExpose)

注意,模块暴露依赖只能使用compileOnly,保证编译时候能找到对应文件即可;另外projects.feature.settingsExpose这种项目导入方式,需要在settings.gradle.kts启用project类型安全配置;


 enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")

5、 search注入并使用SettingExpose


@AndroidEntryPoint
class SearchActivity : AppCompatActivity() {
@Inject
lateinit var settingExpose: SettingExpose

private val listener = object : AppSettingsPopWindow.Listener {

override fun settings() {
settingExpose.startSettingActivity(this@SearchActivity)
}
}
}

6、 实现解耦


最终实现【search模块跳转到settings模块,打开SettingsActivity】, 至于【settings模块跳转到search模块,打开SearchActivity】的操作完全一致,不重复叙述了;


参考资料


1、思路原创:微信Android模块化架构重构实践


2、项目原创:github/tyhjh/module_api


3、脚本迁移:将 build 配置从 Groovy 迁移到 KTS


4、参考文章:Android模块化设计方案之接口API化


5、Nowinandroid:github.com/android/now…


6、Dagger项目:github.com/google/dagg…


7、Hilt官方教程:developer.android.com/training/de…


作者:JailedBird
来源:juejin.cn/post/7305977644499419190
收起阅读 »

JS 爱好者的十大反向教学(译)

web
大家好,这里是大家的林语冰。 免责声明 本文属于是语冰的直男翻译了属于是,仅供粉丝参考,英文原味版请临幸 The 10 Most Common JavaScript Issues Developers Face。 今时今日,JS(JavaScript)几乎...
继续阅读 »

大家好,这里是大家的林语冰。



免责声明


本文属于是语冰的直男翻译了属于是,仅供粉丝参考,英文原味版请临幸 The 10 Most Common JavaScript Issues Developers Face



今时今日,JS(JavaScript)几乎是所有现代 Web App 的核心。这就是为什么 JS 出问题,以及找到导致这些问题的错误,是 Web 开发者的最前线。


用于 SPA(单页应用程序)开发、图形和动画以及服务器端 JS 平台的给力的 JS 库和框架不足为奇。JS 在 Web App 开发领域早已无处不在,因此是一项越来越需要加点的技能树。


乍一看,JS 可能很简单。事实上,对于任何有经验的软件开发者而言,哪怕它们是 JS 初学者,将基本的 JS 功能构建到网页中也是举手之劳。


虽然但是,这种语言比大家起初认为的要更微妙、给力和复杂。事实上,一大坨 JS 的微妙之处可能导致一大坨常见问题,无法正常工作 —— 我们此处会讨论其中的 10 个问题。在成为 JS 大神的过程中,了解并避免这些问题十分重要


问题 1:this 引用失真


JS 开发者对 JS 的 this 关键字不乏困惑。


多年来,随着 JS 编码技术和设计模式越来越复杂,回调和闭包中自引用作用域的延伸也同比增加,此乃导致 JS “this 混淆”问题的“万恶之源”。


请瞄一眼下述代码片段:


const Game = function () {
this.clearLocalStorage = function () {
console.log('Clearing local storage...')
}
this.clearBoard = function () {
console.log('Clearing board...')
}
}

Game.prototype.restart = function () {
this.clearLocalStorage()
this.timer = setTimeout(function () {
this.clearBoard() // this 是什么鬼物?
}, 0)
}

const myGame = new Game()
myGame.restart()

执行上述代码会导致以下错误:


未捕获的类型错误: this.clearBoard 不是函数

为什么呢?这与上下文有关。出现该错误的原因是,当您执行 setTimeout() 时,您实际是在执行 window.setTimeout()。因此,传递给 setTimeout() 的匿名函数定义在 window 对象的上下文中,该对象没有 clearBoard() 方法。


一个传统的、兼容旧浏览器的技术方案是简单地将您的 this 引用保存在一个变量中,然后可以由闭包继承,举个栗子:


Game.prototype.restart = function () {
this.clearLocalStorage()
const self = this // 当 this 还是 this 的时候,保存 this 引用!
this.timer = setTimeout(function () {
self.clearBoard() // OK,我们可以知道 self 是什么了!
}, 0)
}

或者,在较新的浏览器中,您可以使用 bind() 方法传入正确的引用:


Game.prototype.restart = function () {
this.clearLocalStorage()
this.timer = setTimeout(this.reset.bind(this), 0) // 绑定 this
}

Game.prototype.reset = function () {
this.clearBoard() // OK,回退到正确 this 的上下文!
}

问题 2:认为存在块级作用域


JS 开发者之间混淆的“万恶之源”之一(因此也是 bug 的常见来源)是,假设 JS 为每个代码块创建新的作用域。尽管这在许多其他语言中是正确的,但在 JS 中却并非如此。举个栗子,请瞄一眼下述代码:


for (var i = 0; i < 10; i++) {
/* ... */
}
console.log(i) // 输出是什么鬼物?

如果您猜到调用 console.log() 会输出 undefined 或报错,那么恭喜您猜错了。信不信由你,它会输出 10。为什么呢?


在大多数其他语言中,上述代码会导致错误,因为变量 i 的“生命”(即作用域)将被限制在 for 区块中。虽然但是,在 JS 中,情况并非如此,即使在循环完成后,变量 i 仍保留在范围内,在退出 for 循环后保留其最终值。(此行为被称为变量提升。)


JS 对块级作用域的支持可通过 let 关键字获得。多年来,let 关键字一直受到浏览器和后端 JS 引擎(比如 Node.js)的广泛支持。如果这对您来说是新知识,那么值得花时间阅读作用域、原型等。


问题3:创建内存泄漏


如果您没有刻意编码来避免内存泄漏,那么内存泄漏几乎不可避免。它们有一大坨触发方式,因此我们只强调其中两种更常见的情况。


示例 1:失效对象的虚空引用


注意:此示例仅适用于旧版 JS 引擎,新型 JS 引擎具有足够机智的垃圾回收器(GC)来处理这种情况。


请瞄一眼下述代码:


var theThing = null
var replaceThing = function () {
var priorThing = theThing // 保留之前的东东
var unused = function () {
// unused 是唯一引用 priorThing 的地方,
// 但 unused 从未执行
if (priorThing) {
console.log('hi')
}
}
theThing = {
longStr: new Array(1000000).join('*'), // 创建一个 1MB 的对象
someMethod: function () {
console.log(someMessage)
}
}
}
setInterval(replaceThing, 1000) // 每秒执行一次 replaceThing

如果您运行上述代码并监视内存使用情况,就会发现严重的内存泄漏 —— 每秒有一整兆字节!即使是手动垃圾收集器也无济于事。所以看起来每次调用 replaceThing 时我们都在泄漏 longSte。但是为什么呢?



如果您没有刻意编码来避免内存泄漏,那么内存泄漏几乎不可避免



让我们更详细地检查一下:


每个 theThing 对象都包含自己的 1MB longStr 对象。每一秒,当我们调用 replaceThing 时,它都会保留 priorThing 中之前的 theThing 对象的引用。但我们仍然不认为这是一个问题,因为每次先前引用的 priorThing 都会被取消引用(当 priorThing 通过 priorThing = theThing; 重置时)。此外,它仅在 replaceThing 的主体中和 unused 函数中被引用,这实际上从未使用过。


因此,我们再次想知道为什么这里存在内存泄漏。


要了解发生了什么事,我们需要更好地理解 JS 的内部工作原理。闭包通常由链接到表示其词法作用域的字典风格对象(dictionary-style)的每个函数对象实现。如果 replaceThing 内部定义的两个函数实际使用了 priorThing,那么它们都得到相同的对象是很重要的,即使 priorThing 逐次赋值,两个函数也共享相同的词法环境。但是,一旦任何闭包使用了变量,它就会进入该作用域中所有闭包共享的词法环境中。而这个小小的细微差别就是导致这种粗糙的内存泄漏的原因。


示例 2:循环引用


请瞄一眼下述代码片段:


function addClickHandler(element) {
element.click = function onClick(e) {
alert('Clicked the ' + element.nodeName)
}
}

此处,onClick 有一个闭包,它保留了 element 的引用(通过 element.nodeName)。通过同时将 onClick 赋值给 element.click,就创建了循环引用,即 element -> onClick -> element -> onClick -> element ......


有趣的是,即使 element 从 DOM 中删除,上述循环自引用也会阻止 elementonClick 被回收,从而造成内存泄漏。


避免内存泄漏:要点


JS 的内存管理(尤其是它的垃圾回收)很大程度上基于对象可达性(reachability)的概念。


假定以下对象是可达的,称为“根”:



  • 从当前调用堆栈中的任意位置引用的对象(即,当前正在执行的函数中的所有局部变量和参数,以及闭包作用域中的所有变量)

  • 所有全局变量


只要对象可以通过引用或引用链从任何根访问,那么它们至少会保留在内存中。


浏览器中有一个垃圾回收器,用于清理不可达对象占用的内存;换而言之,当且仅当 GC 认为对象不可达时,才会从内存中删除对象。不幸的是,很容易得到已失效的“僵尸”对象,这些对象不再使用,但 GC 仍然认为它们可达。


问题 4:混淆相等性


JS 的便捷性之一是,它会自动将布尔上下文中引用的任何值强制转换为布尔值。但在某些情况下,这可能既香又臭。


举个栗子,对于一大坨 JS 开发者而言,下列表达式很头大:


// 求值结果均为 true!
console.log(false == '0');
console.log(null == undefined);
console.log(" \t\r\n" == 0);
console.log('' == 0);

// 这些也是 true!
if ({}) // ...
if ([]) // ...

关于最后两个,尽管是空的(这可能会让您相信它们求值为 false),但 {}[] 实际上都是对象,并且 JS 中任何对象都将被强制转换为 true,这与 ECMA-262 规范一致。


正如这些例子所表明的,强制类型转换的规则有时可以像泥巴一样清晰。因此,除非明确需要强制类型转换,否则通常最好使用 ===!==(而不是 ==!=)以避免强制类型转换的任何意外副作用。(==!= 比较两个东东时会自动执行类型转换,而 ===!== 在不进行类型转换的情况下执行同款比较。)


由于我们谈论的是强制类型转换和比较,因此值得一提的是,NaN 与任何事物(甚至 NaN 自己!)进行比较始终会返回 false。因此您不能使用相等运算符( =====!=!==)来确定值是否为 NaN。请改用内置的全局 isNaN() 函数:


console.log(NaN == NaN) // False
console.log(NaN === NaN) // False
console.log(isNaN(NaN)) // True

问题 5:低效的 DOM 操作


JS 使得操作 DOM 相对容易(即添加、修改和删除元素),但对提高操作效率没有任何作用。


一个常见的示例是一次添加一个 DOM 元素的代码。添加 DOM 元素是一项代价昂贵的操作,连续添加多个 DOM 元素的代码效率低下,并且可能无法正常工作。


当需要添加多个 DOM 元素时,一个有效的替代方案是改用文档片段(document fragments),这能提高效率和性能。


举个栗子:


const div = document.getElementById('my_div')
const fragment = document.createDocumentFragment()
const elems = document.querySelectorAll('a')

for (let e = 0; e < elems.length; e++) {
fragment.appendChild(elems[e])
}
div.appendChild(fragment.cloneNode(true))

除了这种方法固有的提高效率之外,创建附加的 DOM 元素代价昂贵,而在分离时创建和修改它们,然后附加它们会产生更好的性能。


问题 6:在 for 循环中错误使用函数定义


请瞄一眼下述代码:


var elements = document.getElementsByTagName('input')
var n = elements.length // 我们假设本例有 10 个元素
for (var i = 0; i < n; i++) {
elements[i].onclick = function () {
console.log('This is element #' + i)
}
}

根据上述代码,如果有 10 个输入元素,单击其中任何一个都会显示“This is element #10”!这是因为,在为任何元素调用 onclick 时,上述 for 循环将完成,并且 i 的值已经是 10(对于所有元素)。


以下是我们如何纠正此问题,实现所需的行为:


var elements = document.getElementsByTagName('input')
var n = elements.length // 我们假设本例有 10 个元素
var makeHandler = function (num) {
// 外部函数
return function () {
// 内部函数
console.log('This is element #' + num)
}
}
for (var i = 0; i < n; i++) {
elements[i].onclick = makeHandler(i + 1)
}

在这个修订版代码中,每次我们通过循环时,makeHandler 都会立即执行,每次都会接收当时 i + 1 的值并将其绑定到作用域的 num 变量。外部函数返回内部函数(也使用此作用域的 num 变量),元素的 onclick 会设置为该内部函数。这确保每个 onclick 接收和使用正确的 i 值(通过作用域的 num 变量)。


问题 7:误用原型式继承


令人惊讶的是,一大坨 JS 爱好者无法完全理解和充分利用原型式继承的特性。


下面是一个简单的示例:


BaseObject = function (name) {
if (typeof name !== 'undefined') {
this.name = name
} else {
this.name = 'default'
}
}

这似乎一目了然。如果您提供一个名称,请使用该名称,否则将名称设置为“default”。举个栗子:


var firstObj = new BaseObject()
var secondObj = new BaseObject('unique')

console.log(firstObj.name) // -> 结果是 'default'
console.log(secondObj.name) // -> 结果是 'unique'

但是,如果我们这样做呢:


delete secondObj.name

然后我们会得到:


console.log(secondObj.name) // -> 结果是 'undefined'

骚然但是,将其恢复为“default”不是更好吗?如果我们修改原始代码以利用原型式继承,这很容易实现,如下所示:


BaseObject = function (name) {
if (typeof name !== 'undefined') {
this.name = name
}
}

BaseObject.prototype.name = 'default'

在此版本中,BaseObject 从其 prototype 对象继承该 name 属性,其中该属性(默认)设置为 'default'。因此,如果调用构造函数时没有名称,那么名称将默认为 default。同样,如果从 BaseObject 的实例删除该 name 属性,那么会搜索原型链,并从 prototype 对象中检索值仍为 'default'name 属性。所以现在我们得到:


var thirdObj = new BaseObject('unique')
console.log(thirdObj.name) // -> 结果是 'unique'

delete thirdObj.name
console.log(thirdObj.name) // -> 结果是 'default'

问题 8:创建对实例方法的错误引用


让我们定义一个简单对象,并创建它的实例,如下所示:


var MyObjectFactory = function () {}

MyObjectFactory.prototype.whoAmI = function () {
console.log(this)
}

var obj = new MyObjectFactory()

现在,为了方便起见,让我们创建一个 whoAmI 方法的引用,大概这样我们就可以通过 whoAmI() 访问它,而不是更长的 obj.whoAmI()


var whoAmI = obj.whoAmI

为了确保我们存储了函数的引用,让我们打印出新 whoAmI 变量的值:


console.log(whoAmI)

输出:


function () {
console.log(this);
}

目前它看起来不错。


但是瞄一眼我们调用 obj.whoAmI() 与便利引用 whoAmI() 时的区别:


obj.whoAmI() // 输出 "MyObjectFactory {...}" (预期)
whoAmI() // 输出 "window" (啊这!)

哪里出了问题?我们的 whoAmI() 调用位于全局命名空间中,因此 this 设置为 window(或在严格模式下设置为 undefined),而不是 MyObjectFactoryobj 实例!换而言之,该 this 值通常取决于调用上下文。


箭头函数((params) => {} 而不是 function(params) {})提供了静态 this,与常规函数基于调用上下文的 this 不同。这为我们提供了一个技术方案:


var MyFactoryWithStaticThis = function () {
this.whoAmI = () => {
// 请注意此处的箭头符号
console.log(this)
}
}

var objWithStaticThis = new MyFactoryWithStaticThis()
var whoAmIWithStaticThis = objWithStaticThis.whoAmI

objWithStaticThis.whoAmI() // 输出 "MyFactoryWithStaticThis" (同往常一样)
whoAmIWithStaticThis() // 输出 "MyFactoryWithStaticThis" (箭头符号的福利)

您可能已经注意到,即使我们得到了匹配的输出,this 也是对工厂的引用,而不是对实例的引用。与其试图进一步解决此问题,不如考虑根本不依赖 this(甚至不依赖 new)的 JS 方法。


问题 9:提供一个字符串作为 setTimeout or setInterval 的首参


首先,让我们在这里明确一点:提供字符串作为首个参数给 setTimeout 或者 setInterval 本身并不是一个错误。这是完全合法的 JS 代码。这里的问题更多的是性能和效率。经常被忽视的是,如果将字符串作为首个参数传递给 setTimeoutsetInterval,它将被传递给函数构造函数以转换为新函数。这个过程可能缓慢且效率低下,而且通常非必要。


将字符串作为首个参数传递给这些方法的替代方法是传入函数。让我们举个栗子。


因此,这里将是 setIntervalsetTimeout 的经典用法,将字符串作为首个参数传递:


setInterval('logTime()', 1000)
setTimeout("logMessage('" + msgValue + "')", 1000)

更好的选择是传入一个函数作为初始参数,举个栗子:


setInterval(logTime, 1000) // 将 logTime 函数传给 setInterval

setTimeout(function () {
// 将匿名函数传给 setTimeout
logMessage(msgValue) // (msgValue 在此作用域中仍可访问)
}, 1000)

问题 10:禁用“严格模式”


“严格模式”(即在 JS 源文件的开头包含 'use strict';)是一种在运行时自愿对 JS 代码强制执行更严格的解析和错误处理的方法,也是一种使代码更安全的方法。


诚然,禁用严格模式并不是真正的“错误”,但它的使用越来越受到鼓励,省略它越来越被认为是不好的形式。


以下是严格模式的若干主要福利:



  • 更易于调试。本来会被忽略或静默失败的代码错误现在将生成错误或抛出异常,更快地提醒您代码库中的 JS 问题,并更快地将您定位到其源代码。

  • 防止意外全局变量。如果没有严格模式,将值赋值给给未声明的变量会自动创建同名全局变量。这是最常见的 JS 错误之一。在严格模式下,尝试这样做会引发错误。

  • 消除 this 强制类型转换。如果没有严格模式,对 nullundefined 值的 this 引用会自动强制转换到 globalThis 变量。这可能会导致一大坨令人沮丧的 bug。在严格模式下,nullundefined 值的 this 引用会抛出错误。

  • 禁止重复的属性名或参数值。严格模式在检测到对象中的重名属性(比如 var object = {foo: "bar", foo: "baz"};)或函数的重名参数(比如 function foo(val1, val2, val1){})时会抛出错误,从而捕获代码中几乎必然出错的 bug,否则您可能会浪费大量时间进行跟踪。

  • 更安全的 eval()。严格模式和非严格模式下 eval() 的行为存在某些差异。最重要的是,在严格模式下,eval() 语句中声明的变量和函数不会在其包裹的作用域中创建。(它们在非严格模式下是在其包裹的作用域中创建的,这也可能是 JS 问题的常见来源。)

  • delete 无效使用时抛出错误delete 运算符(用于删除对象属性)不能用于对象的不可配置属性。当尝试删除不可配置属性时,非严格代码将静默失败,而在这种情况下,严格模式将抛出错误。


使用更智能的方法缓解 JS 问题


与任何技术一样,您越能理解 JS 奏效和失效的原因和方式,您的代码就会越可靠,您就越能有效地利用语言的真正力量。


相反,缺乏 JS 范式和概念的正确理解是许多 JS 问题所在。彻底熟悉语言的细微差别和微妙之处是提高熟练度和生产力的最有效策略。


您现在收看的是前端翻译计划,学废了的小伙伴可以订阅此专栏合集,我们每天佛系投稿,欢迎持续关注前端生态。谢谢大家的点赞,掰掰~


26-cat.gif


作者:人猫神话
来源:juejin.cn/post/7306040473542508556
收起阅读 »

货拉拉App录制回放的探索与实践

作者简介:徐卓毅Joe,来自货拉拉/技术中心/质量保障部,专注于移动测试效能方向。 一、背景与目标 近些年货拉拉的业务持续高速发展,为了满足业务更短周期、更高质量交付的诉求,从今年开始我们的移动App的迭代交付模型也从双周演化为单周。因此,在一周一版的紧张节...
继续阅读 »

作者简介:徐卓毅Joe,来自货拉拉/技术中心/质量保障部,专注于移动测试效能方向。



一、背景与目标


近些年货拉拉的业务持续高速发展,为了满足业务更短周期、更高质量交付的诉求,从今年开始我们的移动App的迭代交付模型也从双周演化为单周。因此,在一周一版的紧张节奏下,随之而来的对测试质量保障的挑战也日益增加,首当其冲要解决的就是如何降低移动App每周版本回归测试的人力投入。


早期我们尝试过基于Appium框架编写UI自动化测试脚本,并且为了降低编写难度,我们也基于Appium框架进行了二次开发,但实践起来依然困难重重,主要原因在于:




  1. 上手和维护成本高



    • 需要掌握一定基础知识才能编写脚本和排查过程中遇到的问题;

    • 脚本编写+调试耗时长,涉及的元素定位+操作较多,调试要等待脚本执行回放才能看到结果;

    • 排查成本高,由于UI自动化测试的稳定性低,需投入排查的脚本较多,耗时长;

    • 维护成本高,每个迭代的需求改动都可能导致页面元素或链路调整,需不定期维护;




  2. 测试脚本稳定性低



    • 容易受多种因素(服务端环境、手机环境等)影响,这也造成了问题排查和溯源困难;

    • 脚本本身的稳定性低,模拟手工操作的方式,但实际操作点击没有那么智能;

      • 脚本识别元素在不同分辨率、不同系统版本上,识别的速度及准确度不同;

      • 不同设备在某些操作上表现,例如缩放(缩放多少)、滑动(滑动多少)有区别;

      • 由于功能复杂性、不同玩法的打断(如广告、弹窗、ab实验等);






所以,在App UI自动化测试上摸爬滚打一段时间后,我们积累了大量的踩坑经验。但这些经验也让我们更加明白,如果要大规模推行App UI自动化测试,必须要提高自动化ROI,否则很难达到预期效果,成本收益得不偿失。


我们的目标是打造一个低成本、高可用的App UI自动化测试平台。它需要满足如下条件:



  1. 更低的技术门槛:上手简单,无需环境配置;

  2. 更快的编写速度:无需查找控件,手机上操作后就能生成一条可执行的测试脚本;

  3. 更小的维护成本: 支持图像识别,减少由于控件改动导致的问题;

  4. 更高的稳定性: 回放识别通过率高,降低环境、弹窗的影响;

  5. 更好的平台功能: 支持脚本管理、设备调度、测试报告等能力,提升执行效率,降低排查成本;


二、行业方案


image.png


考虑到自动化ROI,我们基本确定要使用基于录制回放方式的自动化方案,所以我们也调研了美团、爱奇艺、字节、网易这几个公司的测试工具平台的实现方案:



  1. 网易Airtest是唯一对外发布的工具,但免费版本是IDE编写的,如果是小团队使用该IDE录制UI脚本来说还是比较方便的,但对于多团队协同,以及大规模UI自动化的实施的需求来说,其脚本管理、设备调度、实时报告等平台化功能的支持还不满足。

  2. 美团AlphaTest上使用的是App集成SDK的方式,可以通过底层Hook能力采集到操作数据、网络数据等更为详尽的内容,也提供了API支持业务方自定义实现,如果采用这种方案,移动研发团队的配合是很重要的。

  3. 爱奇艺的方案是在云真机的基础上,使用云IDE的方式进行录制,重点集成了脚本管理、设备调度、实时报告等平台化功能,这种方案的优势在于免去开发SDK的投入,可以做成通用能力服务于各业务App。

  4. 字节SmartEye也是采用集成SDK的方式,其工具本身更聚焦精准测试的能力建设,而精准测试当前货拉拉也在深入实践中,后续有机会我们再详细介绍。


综上分析,如果要继续推行App UI自动化测试,我们也需要自研测试平台,最好是能结合货拉拉现有的业务形态和能力优势,用最低的自研方案成本,快速搭建起适合我们的App录制回放测试平台,这样就能更快推动实践,降低业务测试当前面临的稳定性保障的压力。


三、能力建设


image.png


货拉拉现有的能力优势主要有:



  1. 货拉拉的云真机建设上已有成熟的经验(感兴趣的读者可参见文章《货拉拉云真机平台的演进与实践》);

  2. 货拉拉在移动App质效上已有深入实践,其移动云测平台已沉淀了多维度的自动化测试服务(如性能、兼容性、稳定性、健壮性、遍历、埋点等),具备比较成熟的平台能力。


因此,结合多方因素,最终我们选择了基于云真机开展App UI录制回放的方案,在借鉴其他公司优秀经验的基础上,结合我们对App UI自动化测试过程中积累的宝贵经验,打造了货拉拉App云录制回放测试平台。


下面我们会按录制能力、回放能力、平台能力三大部分进行介绍。


3.1 录制能力


录制流程从云真机的操作事件开始,根据里面的截图和操作坐标解析操作的控件,最终将操作转化为脚本里的单个步骤。并且支持Android和iOS双端,操作数据上报都是用旁路上报的方式,不会阻塞在手机上的操作。


image.png
下面是我们当前基于云真机录制的效果:



  在录制的过程中,其目标主要有:



  1. 取到当前操作的类型 点击、长按、输入、滑动等;

  2. 取到操作的目标控件 按钮、标签栏、文本框等;


3.1.1 云真机旁路上报&事件解析


  首先要能感知到用户在手机上做了什么操作,当我们在页面上使用云真机时,云真机后台可以监控到最原始的屏幕数据,不同操作的数据流如下:


// 点击
d 0 10 10 50
c
u 0
c
// 长按
d 0 10 10 50
c
<wait in your own code>
u 0
c
// 滑动
d 0 0 0 50
c
<wait in your own code> //需要拖拽加上等待时间
m 0 20 0 50
c
m 0 40 0 50
c
m 0 60 0 50
c
m 0 80 0 50
c
m 0 100 0 50
c
u 0
c

  根据协议我们可以判断每次操作的类型以及坐标,但仅依赖坐标的录制并不灵活,也不能实现例如断言一类的操作,所以拿到控件信息也非常关键。


  一般UI自动化中会dump出控件树,通过控件ID或层级关系定位控件。而dump控件树是一个颇为耗时的动作,普通布局的页面也需要2S左右。



  如果在录制中同时dump控件树,那我们每点击都要等待进度条转完,显然这不是我们想要的体验。而可以和操作坐标一起拿到的还有手机画面的视频流,虽然单纯的截图没有控件信息,但假如截图可以像控件树一样拆分出独立的控件区域,我们就可以结合操作坐标匹配对应控件。


3.1.2 控件/文本检测


  控件区域检测正是深度学习中的目标检测能解决的问题。


  这里我们先简单看一下深度学习的原理以及在目标检测过程中做了什么。


  深度学习原理



深度学习使用了一种被称为神经网络的结构。像人脑中的神经元一样,神经网络中的节点会对输入数据进行处理,然后将结果传递到下一个层级。这种逐层传递和处理数据的方式使得深度学习能够自动学习数据的复杂结构和模式。



  总的来说,深度学习网络逐层提取输入的特征,总结成更抽象的特征,将学习到的知识作为权重保存到网络中。


image.pngimage.png

举个例子,如果我们使用深度学习来学习识别猫的图片,那么神经网络可能会在第一层学习识别图片中的颜色或边缘,第二层可能会识别出特定的形状或模式,第三层可能会识别出猫的某些特征,如猫的眼睛或耳朵,最后,网络会综合所有的特征来确定这张图片是否是猫。


  目标检测任务


  目标检测是深度学习中的常见任务,任务的目标是在图像中识别并定位特定物体。


  在我们的应用场景中,任务的目标自然是UI控件:



  1. 识别出按钮、文本框等控件,可以归类为图标、图片和文本;

  2. 圈定控件的边界范围;


这里我们选用知名的YOLOX目标检测框架,社区里也开放许多了以UI为目标的预训练模型和数据集,因为除了自动化测试外,还有通过UI设计稿生成前端代码等应用场景。


roboflow公开数据集


  下图是使用公开数据集直接推理得到的控件区域,可以看出召回率不高。这是因为公开数据集中国外APP标注数据更多,且APP的UI风格不相似。


示例一示例二

预训练和微调模型


  而最终推理效果依赖数据集质量,这需要我们微调模型。由于目标数据集相似,所以我们只需要在预训练模型基础时,冻结骨干网络,重置最后输出层权重,喂入货拉拉风格的UI数据继续训练,可以得到更适用的模型。


model = dict (backbone=dict (frozen_stages=1 # 表示第一层 stage 以及它之前的所有 stage 中的参数都会被冻结 )) 


通过目标检测任务,我们可以拿到图标类的控件,控件的截图可以作为标识存储。当然,文本类的控件还是转化成文本存储更理想。针对文本的目标检测任务不仅精准度更高,还能提供目标文本的识别结果。我们单独用PaddleOCR再做了一次文本检测识别。


3.1.3 脚本生成


  所有操作最终都会转化为脚本储存,我们自定义了一种脚本格式用来封装不同的UI操作。


  以一次点击为例,操作类型用Click()表示;如果是点击图标类控件,会将图标的截图保存(以及录制时的屏幕相对坐标,用于辅助回放定位),而点击文案则是记录文本。



  操作消抖: 点击、长按和滑动之间通过设置固定的时长消除实际操作时的抖动,我们取系统中的交互动效时长,一般是200~300ms。


  文本输入: 用户实际操作输入文本时分为两种情况,一是进入页面时自动聚焦编辑框,另一种是用户主动激活编辑,都会拉起虚拟键盘。我们在回放时也需要在拉起键盘的情况下输入,才能真实还原键盘事件对页面的影响。


am broadcast -a ADB_INPUT_B64 --es msg "xxx"

  目标分组: 一个页面上可能有多个相同的图标或文案,所以在录制时会聚合相同分组,在脚本中通过下标index(0)区分。


3.2 回放能力


  回放脚本时,则是根据脚本里记录的控件截图和文本,匹配到回放手机上的目标区域,进而执行点击、滑动等操作。这里用到的图像和文本匹配能力也会用在脚本断言里。


image.png


回放效果见下图:



3.2.1 图像匹配


  与文本相比,图标类控件在回放时要应对的变化更多:



  • 颜色不同;

  • 分辨率不同

  • 附加角标等提示;


  在这种场景中,基于特征点匹配的SIFT算法很合适。



尺度不变特征变换(Scale-invariant feature transform, SIFT)是计算机视觉中一种检测、描述和匹配图像局部特征点的方法,通过在不同的尺度空间中检测极值点或特征点(Conrner Point, Interest Point),提取出其位置、尺度和旋转不变量,并生成特征描述子,最后用于图像的特征点匹配。



  对图像做灰度预处理之后能减少颜色带来的噪音,而SIFT的尺度不变特性容忍了分辨率变化,附加的角标不会影响关键特征点的匹配。


  除此之外,为了减低误匹配,我们增加了两个操作:


  RegionMask:在匹配之前,我们也做了控件检测,并作为遮罩层Mask设置到SIFT中,排除错误答案之后的特征点更集中稳定。



  屏蔽旋转不变性:因为不需要在页面上匹配旋转后的目标,所以我们将提取的特征点向量角度统一重置为0。


  sift.detect(image, kpVector, mask);
// 设置角度统一为0,禁用旋转不变性
for (int i = 0; i < kpVector.size(); i++) {
KeyPoint point = kpVector.get(i);
point.angle(0);
...
}
sift.compute(image, kpVector, ret);

3.2.2 文本匹配


  文本匹配很容易实现,在OCR之后做字符串比较可以得到结果。


  但是因为算法本身精准度并不是百分百(OCR识别算法CRNN精准度在80%),遇到长文案时会出现识别错误,我们通过计算与期望文本间的编辑距离容忍这种误差。



  但最常见的还是全角和半角字符间的识别错误,需要把标点符号作为噪音去除。


  还有另一个同样和长文案有关的场景:机型宽度不同时,会出现文案换行展示的情况,这时就不能再去完整匹配,但可以切换到xpath使用部分匹配


//*[contains(@text,'xxx')]

3.2.3 兜底弹窗处理


  突然出现的弹窗是UI自动化中的一大痛点,无论是时机和形式都无法预测,造成的结果是自动化测试中断。



  弹窗又分为系统弹窗和业务弹窗,我们有两种处理弹窗的策略:



  1. Android提供了一个DeviceOwner角色托管设备,并带有一个策略配置(PERMISSION_POLICY_AUTO_GRANT),测试过程中APP申请权限时天宫管家自动授予权限;




  1. 在自动化被中断时,再次检查页面有没有白名单中的弹窗文案,有则触发兜底逻辑,关闭弹窗后,恢复自动化执行。


3.2.4 自动装包授权


  Android碎片化带来的还有不同的装包验证策略,比如OPPO&VIVO系机型就需要输入密码才能安装非商店应用。


  为了保持云真机的环境纯净,我们没有通过获取ROOT授权的方式绕过,而是采用部署在云真机内置的装包助手服务适配了不同机型的装包验证。




3.2.5 数据构造&请求MOCK


  目前为止我们录制到的还只有UI的操作,但场景用例中缺少不了测试数据的准备。
  首先是测试数据构造,脚本中提供一个封装好的动作,调用内部平台数据工厂,通过传入和保存变量能在脚本间传递调用的数据。



  同时脚本还可以关联到APP-MOCK平台,在一些固定接口或特定场景MOCK接口响应。譬如可以固定AB实验配置,又或是屏蔽推送类的通知。



3.1 平台能力


3.3.1 用例编辑&管理


  有实践过UI自动化的人应该有这种感受,在个人电脑搭建一套自动化环境是相当费劲的,更不用说要同时兼顾Android和iOS。


  当前我们已经达成了UI自动化纯线上化这一个小目标,只需要在浏览器中就可以完成UI脚本的编辑、调试和执行。现在正完善更多的线上操作,以Monaco Editor为基础编辑器提供更方便的脚本编辑功能。


image.png


3.3.2 脚本组&任务调度


  为了方便管理数量渐涨的用例,我们通过脚本组的方式分模块组织和执行脚本。每个脚本组可以设置前后置脚本和使用的帐号类别,一个脚本组会作为最小的执行单元发送到手机上执行。



  我们可以将回归场景拆分成若干个组在多台设备上并发执行,大大缩短了自动化用例的执行时间。


四、效果实践


4.1 回归测试提效


App录制回放能力建设完毕后,我们立即在多个业务线推动UI自动化测试实践。我们也专门成立了一支虚拟团队,邀请各团队骨干加入,明确回归测试提效的目标,拉齐认知,统一节奏,以保障UI自动化的大规模实践的顺利落地。




  1. 建立问题同步及虚拟团队管理的相关制度,保障问题的快速反馈和快速解决。




  2. 制定团队的UI测试实践管理规范,指导全体成员按统一的标准去执行,主要包括:



    • 回归用例筛选:按模块维度进行脚本转化,优先覆盖P0用例(占比30%左右);

    • 测试场景设计:设计可以串联合并的场景,这样合并后可提升自动化执行速度;

    • 测试数据准备:自动化账号怎么管理,有哪些推荐的数据准备方案;

    • 脚本编写手册:前置脚本、公共脚本引入规范、断言规范等;

    • 脚本执行策略:脚本/脚本组管理及执行策略,怎样能执行的更快;




image.png


所以,我们在很短的时间内就完成了P0回归测试用例的转化,同时我们还要求:



  1. 回放通过率必须高于90%,避免给业务测试人员造成额外的干扰,增加排查工作量;

  2. 全量场景用例的执行总时长要小于90分钟,充分利用云真机的批量调度能力,快速输出测试报告。而且某种程度来说,还能避开因服务端部署带来的环境问题的影响;


截止目前,我们已经支持10多次单周版本的回归测试,已经可以替代部分手工回归测试工作量,降低测试压力的同时提升了版本发布质量的信心。


4.2 整体测试效能提升


在App UI自动化测试的实施取得突破性进展后,我们开始尝试优化原有性能、兼容、埋点等自动化测试遇到的一些问题,以提升移动App的整体测试效能。



  • App性能自动化测试: 原有的性能测试脚本都是使用基于UI元素定位的方式,每周的功能迭代都或多或少会影响到脚本的稳定性,所以我们的性能脚本早期每周都需要维护。而现在的性能测试脚本通过率一般情况下都是100%,极个别版本才会出现微调脚本的情况。

  • App深度兼容测试: 当涉及移动App测试时,兼容性测试的重要性不言而喻。移动云测平台在很早就已支持了标准兼容测试能力,即结合智能遍历去覆盖更多的App页面及场景,去发现一些基础的兼容测试问题。但随着App UI自动化测试的落地,现在我们已经可以基于大量的UI测试脚本在机房设备上开展深度兼容测试。


机房执行深度兼容测试


  • App 埋点 自动化测试: 高价值埋点的回归测试,以往我们都需要在回归期间去手工额外去触发操作路径,现在则基于UI自动化测试模拟用户操作行为,再结合移动云测平台已有的埋点自动校验+测试结果实时展示的能力,彻底解放人力,实现埋点全流程自动化测试。




  • 接入 CICD 流水线: 我们将核心场景的UI回归用例配CICD流水线中,每当代码合入或者触发构建后,都会自动触发验证流程,如果测试不通过,构建人和相关维护人都能立即收到消息通知,进一步提升了研发协同效率。


流程图 (3).jpg


五、未来展望



“道阻且长,行则将至,行而不辍,未来可期”。——《荀子·修身》



货拉拉App云录制回放测试平台的建设上,未来还有一些可提升的方向:



  1. 迭代优化模型,提升精准度和性能;

  2. 补全数据的录制回放,增加本地配置和缓存的控制;

  3. 探索使用AI大模型的识图能力,辨别APP页面上的UI异常;

  4. 和客户端精准测试结合,推荐未覆盖场景和变更相关用例;


作者:货拉拉技术
来源:juejin.cn/post/7306331307477794867
收起阅读 »

4 种消息队列,如何选型?

大家好呀,我是楼仔。 最近发现很多号主发消息队列的文章,质量参差不齐,相关文章我之前也写过,建议直接看这篇。 这篇文章,主要讲述 Kafka、RabbitMQ、RocketMQ 和 ActiveMQ 这 4 种消息队列的异同,无论是面试,还是用于技术选型,都有...
继续阅读 »

大家好呀,我是楼仔。


最近发现很多号主发消息队列的文章,质量参差不齐,相关文章我之前也写过,建议直接看这篇。


这篇文章,主要讲述 Kafka、RabbitMQ、RocketMQ 和 ActiveMQ 这 4 种消息队列的异同,无论是面试,还是用于技术选型,都有非常强的参考价值。


不 BB,上文章目录:



01 消息队列基础


1.1 什么是消息队列?


消息队列是在消息的传输过程中保存消息的容器,用于接收消息并以文件的方式存储,一个消息队列可以被一个也可以被多个消费者消费,包含以下 3 元素:



  • Producer:消息生产者,负责产生和发送消息到 Broker;

  • Broker:消息处理中心,负责消息存储、确认、重试等,一般其中会包含多个 Queue;

  • Consumer:消息消费者,负责从 Broker 中获取消息,并进行相应处理。



1.2 消息队列模式



  • 点对点模式:多个生产者可以向同一个消息队列发送消息,一个具体的消息只能由一个消费者消费。




  • 发布/订阅模式:单个消息可以被多个订阅者并发的获取和处理。



1.3 消息队列应用场景



  • 应用解耦:消息队列减少了服务之间的耦合性,不同的服务可以通过消息队列进行通信,而不用关心彼此的实现细节。

  • 异步处理:消息队列本身是异步的,它允许接收者在消息发送很长时间后再取回消息。

  • 流量削锋:当上下游系统处理能力存在差距的时候,利用消息队列做一个通用的”载体”,在下游有能力处理的时候,再进行分发与处理。

  • 日志处理:日志处理是指将消息队列用在日志处理中,比如 Kafka 的应用,解决大量日志传输的问题。

  • 消息通讯:消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯,比如实现点对点消息队列,或者聊天室等。

  • 消息广播:如果没有消息队列,每当一个新的业务方接入,我们都要接入一次新接口。有了消息队列,我们只需要关心消息是否送达了队列,至于谁希望订阅,是下游的事情,无疑极大地减少了开发和联调的工作量。


02 常用消息队列


由于官方社区现在对 ActiveMQ 5.x 维护越来越少,较少在大规模吞吐的场景中使用,所以我们主要讲解 Kafka、RabbitMQ 和 RocketMQ。


2.1 Kafka


Apache Kafka 最初由 LinkedIn 公司基于独特的设计实现为一个分布式的提交日志系统,之后成为 Apache 项目的一部分,号称大数据的杀手锏,在数据采集、传输、存储的过程中发挥着举足轻重的作用。


它是一个分布式的,支持多分区、多副本,基于 Zookeeper 的分布式消息流平台,它同时也是一款开源的基于发布订阅模式的消息引擎系统。


重要概念



  • 主题(Topic):消息的种类称为主题,可以说一个主题代表了一类消息,相当于是对消息进行分类,主题就像是数据库中的表。

  • 分区(partition):主题可以被分为若干个分区,同一个主题中的分区可以不在一个机器上,有可能会部署在多个机器上,由此来实现 kafka 的伸缩性。

  • 批次:为了提高效率, 消息会分批次写入 Kafka,批次就代指的是一组消息。

  • 消费者群组(Consumer Gr0up):消费者群组指的就是由一个或多个消费者组成的群体。

  • Broker: 一个独立的 Kafka 服务器就被称为 broker,broker 接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。

  • Broker 集群:broker 集群由一个或多个 broker 组成。

  • 重平衡(Rebalance):消费者组内某个消费者实例挂掉后,其他消费者实例自动重新分配订阅主题分区的过程。


Kafka 架构


一个典型的 Kafka 集群中包含 Producer、broker、Consumer Gr0up、Zookeeper 集群。


Kafka 通过 Zookeeper 管理集群配置,选举 leader,以及在 Consumer Gr0up 发生变化时进行 rebalance。Producer 使用 push 模式将消息发布到 broker,Consumer 使用 pull 模式从 broker 订阅并消费消息。



Kafka 工作原理


消息经过序列化后,通过不同的分区策略,找到对应的分区。


相同主题和分区的消息,会被存放在同一个批次里,然后由一个独立的线程负责把它们发到 Kafka Broker 上。



分区的策略包括顺序轮询、随机轮询和 key hash 这 3 种方式,那什么是分区呢?


分区是 Kafka 读写数据的最小粒度,比如主题 A 有 15 条消息,有 5 个分区,如果采用顺序轮询的方式,15 条消息会顺序分配给这 5 个分区,后续消费的时候,也是按照分区粒度消费。



由于分区可以部署在多个不同的机器上,所以可以通过分区实现 Kafka 的伸缩性,比如主题 A 的 5 个分区,分别部署在 5 台机器上,如果下线一台,分区就变为 4。


Kafka 消费是通过消费群组完成,同一个消费者群组,一个消费者可以消费多个分区,但是一个分区,只能被一个消费者消费。



如果消费者增加,会触发 Rebalance,也就是分区和消费者需要重新配对


不同的消费群组互不干涉,比如下图的 2 个消费群组,可以分别消费这 4 个分区的消息,互不影响。



2.2 RocketMQ


RocketMQ 是阿里开源的消息中间件,它是纯 Java 开发,具有高性能、高可靠、高实时、适合大规模分布式系统应用的特点。


RocketMQ 思路起源于 Kafka,但并不是 Kafka 的一个 Copy,它对消息的可靠传输及事务性做了优化,目前在阿里集团被广泛应用于交易、充值、流计算、消息推送、日志流式处理、binglog 分发等场景。


重要概念



  • Name 服务器(NameServer):充当注册中心,类似 Kafka 中的 Zookeeper。

  • Broker: 一个独立的 RocketMQ 服务器就被称为 broker,broker 接收来自生产者的消息,为消息设置偏移量。

  • 主题(Topic):消息的第一级类型,一条消息必须有一个 Topic。

  • 子主题(Tag):消息的第二级类型,同一业务模块不同目的的消息就可以用相同 Topic 和不同的 Tag 来标识。

  • 分组(Gr0up):一个组可以订阅多个 Topic,包括生产者组(Producer Gr0up)和消费者组(Consumer Gr0up)。

  • 队列(Queue):可以类比 Kafka 的分区 Partition。


RocketMQ 工作原理


RockerMQ 中的消息模型就是按照主题模型所实现的,包括 Producer Gr0up、Topic、Consumer Gr0up 三个角色。


为了提高并发能力,一个 Topic 包含多个 Queue,生产者组根据主题将消息放入对应的 Topic,下图是采用轮询的方式找到里面的 Queue。


RockerMQ 中的消费群组和 Queue,可以类比 Kafka 中的消费群组和 Partition:不同的消费者组互不干扰,一个 Queue 只能被一个消费者消费,一个消费者可以消费多个 Queue。


消费 Queue 的过程中,通过偏移量记录消费的位置。



RocketMQ 架构


RocketMQ 技术架构中有四大角色 NameServer、Broker、Producer 和 Consumer,下面主要介绍 Broker。


Broker 用于存放 Queue,一个 Broker 可以配置多个 Topic,一个 Topic 中存在多个 Queue。


如果某个 Topic 消息量很大,应该给它多配置几个 Queue,并且尽量多分布在不同 broker 上,以减轻某个 broker 的压力。Topic 消息量都比较均匀的情况下,如果某个 broker 上的队列越多,则该 broker 压力越大。



简单提一下,Broker 通过集群部署,并且提供了 master/slave 的结构,salve 定时从 master 同步数据(同步刷盘或者异步刷盘),如果 master 宕机,则 slave 提供消费服务,但是不能写入消息。


看到这里,大家应该可以发现,RocketMQ 的设计和 Kafka 真的很像!


2.3 RabbitMQ


RabbitMQ 2007 年发布,是使用 Erlang 语言开发的开源消息队列系统,基于 AMQP 协议来实现。


AMQP 的主要特征是面向消息、队列、路由、可靠性、安全。AMQP 协议更多用在企业系统内,对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次。


重要概念



  • 信道(Channel):消息读写等操作在信道中进行,客户端可以建立多个信道,每个信道代表一个会话任务。

  • 交换器(Exchange):接收消息,按照路由规则将消息路由到一个或者多个队列;如果路由不到,或者返回给生产者,或者直接丢弃。

  • 路由键(RoutingKey):生产者将消息发送给交换器的时候,会发送一个 RoutingKey,用来指定路由规则,这样交换器就知道把消息发送到哪个队列。

  • 绑定(Binding):交换器和消息队列之间的虚拟连接,绑定中可以包含一个或者多个 RoutingKey。


RabbitMQ 工作原理


AMQP 协议模型由三部分组成:生产者、消费者和服务端,执行流程如下:



  1. 生产者是连接到 Server,建立一个连接,开启一个信道。

  2. 生产者声明交换器和队列,设置相关属性,并通过路由键将交换器和队列进行绑定。

  3. 消费者也需要进行建立连接,开启信道等操作,便于接收消息。

  4. 生产者发送消息,发送到服务端中的虚拟主机。

  5. 虚拟主机中的交换器根据路由键选择路由规则,发送到不同的消息队列中。

  6. 订阅了消息队列的消费者就可以获取到消息,进行消费。



常用交换器


RabbitMQ 常用的交换器类型有 direct、topic、fanout、headers 四种,具体的使用方法,可以参考官网:


官网入口:https://www.rabbitmq.com/getstarted.html


03 消息队列对比



3.1 Kafka


优点:



  • 高吞吐、低延迟:Kafka 最大的特点就是收发消息非常快,Kafka 每秒可以处理几十万条消息,它的最低延迟只有几毫秒;

  • 高伸缩性:每个主题(topic)包含多个分区(partition),主题中的分区可以分布在不同的主机(broker)中;

  • 高稳定性:Kafka 是分布式的,一个数据多个副本,某个节点宕机,Kafka 集群能够正常工作;

  • 持久性、可靠性、可回溯: Kafka 能够允许数据的持久化存储,消息被持久化到磁盘,并支持数据备份防止数据丢失,支持消息回溯;

  • 消息有序:通过控制能够保证所有消息被消费且仅被消费一次;

  • 有优秀的第三方 Kafka Web 管理界面 Kafka-Manager,在日志领域比较成熟,被多家公司和多个开源项目使用。


缺点:



  • Kafka 单机超过 64 个队列/分区,Load 会发生明显的飙高现象,队列越多,load 越高,发送消息响应时间变长;

  • 不支持消息路由,不支持延迟发送,不支持消息重试;

  • 社区更新较慢。


3.2 RocketMQ


优点:



  • 高吞吐:借鉴 Kafka 的设计,单一队列百万消息的堆积能力;

  • 高伸缩性:灵活的分布式横向扩展部署架构,整体架构其实和 kafka 很像;

  • 高容错性:通过ACK机制,保证消息一定能正常消费;

  • 持久化、可回溯:消息可以持久化到磁盘中,支持消息回溯;

  • 消息有序:在一个队列中可靠的先进先出(FIFO)和严格的顺序传递;

  • 支持发布/订阅和点对点消息模型,支持拉、推两种消息模式;

  • 提供 docker 镜像用于隔离测试和云集群部署,提供配置、指标和监控等功能丰富的 Dashboard。


缺点:



  • 不支持消息路由,支持的客户端语言不多,目前是 java 及 c++,其中 c++ 不成熟

  • 部分支持消息有序:需要将同一类的消息 hash 到同一个队列 Queue 中,才能支持消息的顺序,如果同一类消息散落到不同的 Queue中,就不能支持消息的顺序。

  • 社区活跃度一般。


3.3 RabbitMQ


优点:



  • 支持几乎所有最受欢迎的编程语言:Java,C,C ++,C#,Ruby,Perl,Python,PHP等等;

  • 支持消息路由:RabbitMQ 可以通过不同的交换器支持不同种类的消息路由;

  • 消息时序:通过延时队列,可以指定消息的延时时间,过期时间TTL等;

  • 支持容错处理:通过交付重试和死信交换器(DLX)来处理消息处理故障;

  • 提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker;

  • 社区活跃度高。


缺点:



  • Erlang 开发,很难去看懂源码,不利于做二次开发和维护,基本职能依赖于开源社区的快速维护和修复 bug;

  • RabbitMQ 吞吐量会低一些,这是因为他做的实现机制比较重;

  • 不支持消息有序、持久化不好、不支持消息回溯、伸缩性一般。


04 消息队列选型


Kafka:追求高吞吐量,一开始的目的就是用于日志收集和传输,适合产生大量数据的互联网服务的数据收集业务,大型公司建议可以选用,如果有日志采集功能,肯定是首选 kafka。


RocketMQ:天生为金融互联网领域而生,对于可靠性要求很高的场景,尤其是电商里面的订单扣款,以及业务削峰,在大量交易涌入时,后端可能无法及时处理的情况。RoketMQ 在稳定性上可能更值得信赖,这些业务场景在阿里双 11 已经经历了多次考验,如果你的业务有上述并发场景,建议可以选择 RocketMQ。


RabbitMQ:结合 erlang 语言本身的并发优势,性能较好,社区活跃度也比较高,但是不利于做二次开发和维护,不过 RabbitMQ 的社区十分活跃,可以解决开发过程中遇到的 bug。如果你的数据量没有那么大,小公司优先选择功能比较完备的 RabbitMQ。


ActiveMQ:官方社区现在对 ActiveMQ 5.x 维护越来越少,较少在大规模吞吐的场景中使用。


今天就聊到这里,我们下一篇见~~




最后,把楼仔的座右铭送给你:我从清晨走过,也拥抱夜晚的星辰,人生没有捷径,你我皆平凡,你好,陌生人,一起共勉。


原创好文:


作者:楼仔
来源:juejin.cn/post/7306322677039235108
收起阅读 »