注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

作为一个普通人,我是如何在浮躁的时代让自己减少焦虑的

前言 来自一个普通人的思考和分享,由于水平有限,认知有限,活动受限,观点可能也有个人偏见,不喜勿喷哈 欢迎JYM一起交流探讨人生、生人、上天,入地、活着、归去、等所有 法律允许, 社区允许 的开放性话题 自我介绍 一个普通人,坐标帝都,小厂前端攻城狮,每...
继续阅读 »

前言



来自一个普通人的思考和分享,由于水平有限,认知有限,活动受限,观点可能也有个人偏见,不喜勿喷哈


欢迎JYM一起交流探讨人生、生人、上天,入地、活着、归去、等所有 法律允许社区允许 的开放性话题



自我介绍



一个普通人,坐标帝都,小厂前端攻城狮,每天奋斗在一线,时不时加班1-3个小时,工作地在西北 4 环,家住东 6 环,由于每天 10 环极限通勤, 下班到家就是一副有气无力的死狗样,妥妥的一个社畜。


第二天早上又是元气满满的一天,精力充沛的出门去打拼,都是为了活着……



像牲口一样活下去.webp


基于周围的一些社会现状,分享一些自己的思考




  1. 追剧看视频的人,好多人使用的 1.25倍速,1.5倍速;漫无目的不停的刷着各种短视频,不停的快速的往下翻着;在手机上不停的翻看着微博,朋友圈,群记录等。



    仿佛什么都看了,又仿佛什么都没看 😟





  2. 经常手机上明明没有任何消息,总是打开翻两下,再息屏。



    似乎不想错过一些联系,又仿佛没有人联系。手机让我们通信更方便了,仿佛也让我们更孤独了 😕





  3. 多少人每天睡醒的第一件事就是打开手机,从起床开始,看手机朋友圈,看热搜,看头条,看推送,看信息,看邮件,吃饭看手机,走路看手机,去上班路上看手机,站着看,躺着看,坐着看,拉便便也看。 最终得到了什么?得到了焦虑、暴躁、前倾的颈椎、渐厚的镜片,还有熬夜留下的黑眼圈......




  4. 我们常常抽时间刷微博、看热搜和刷短视频,但却难以每天保持锻炼身体、系统学习和读几页书,以及坚持做一些微小的事情去发现自己的兴趣爱好。



    智能手机的发展,让我们变的浮躁,静不下来,似乎也停不下来 🧐


    这是一个互联网高度发达的时代,一个被算法支配的时代,一个缺少自我主动支配时间的时代,智能手机的发展给我们的生活带来了翻天覆地的变化,方便了出行、支付、交流等等,在带来极大便利的同时,我们也付出了代价,好多时候我感觉自己就是一副没有灵魂的躯壳。



    行尸走肉.png




  5. 有些社区文章和公众号文章的标题,短短几个字,让人莫名产生焦虑感。




    • 最新大厂热乎面经,已拿offer




    • 前某大厂某高级别大牛带你xxxx




    • 大厂是怎么xxx




    • 都202x年了,你还不会xxx





    曾几何时,看到这种标题,我抱着学习提升跟上时代的想法,点击进去,发现好多是广告,卖课的,要么就是一些同质化的内容,无语翻倍...


    这是最好的时代,也是最坏的时代,好多人靠贩卖焦虑就能赚大钱,新闻自媒体靠一个标题就能吸引流量


    不知道什么时候想通了,承认自己是个普通人,工作只是我们活着的一种手段,每天过的开不开心,家庭幸不幸福和在哪上班收入多少有啥关系?


    该吃吃,该喝喝,有事儿别往心里搁,哟!今天天气真不错,开心 😁





  6. 在疫情时代,看着工作机会减少了,生意不好做了,时代在变化,疫情时代干掉了一些工作,但是疫情核酸检测员,核酸检查站多了,保安也多了,疫情相关产业链上的工作岗位增多了。




  7. ChatGPT、AIGC 爆火的这段时间,好多文章以及社区之类的,大家都在讨论 AI,这时候各种 AI 峰会,线下交流会哪哪都爆满,AI 相关的培训也热的不行,仿佛搭不上 AI 这趟车,我们就会被马上淘汰一样。



    任何一个时代的变革,淘汰一些工作的同时,催生了大量新的岗位,跟上时代,时刻 "刷新" 自己,我已经想好了,什么时候 AI 能完全替代我这个前端工程师了,那时候我也不敲代码了,解脱!


    AI 快来解放我吧 😜





总的来看,我的焦虑感是由环境因素和心理因素共同作用的结果


既然知道了焦虑感产生的原因,也就相对好解决了,毕竟只是尽量减少自己的焦虑感


我尝试做了这些有趣而正向的东西,发现效果还不错



  1. 例如有时会用碎片时间记个单词,dogsbody n.打工人;杂役 累的像狗,身体被掏空的就是打工人了, 是不是一下子就记住了呢?


截屏2023-06-15 13.44.32.png



  1. 每周抽时间运动,目前是每周末早上去跑步,一般5-8公里。偶尔也在公司的健身区举举铁。个人比较喜欢跑步,跑到一定公里数的时候,心脏加快跳动,血液流动更畅通,还有夏天室外奔跑汗流浃背的感觉。感觉整个人都更轻松了,心情也会变好。我愿意坚持运动,享受运动过程中的愉悦感和得到放松的身体。


来看看我今年的跑步数据:


2.png



  1. 无意间发现了一款乐器 -- 非洲手鼓,当时被这个手鼓颜值给吸引了,然后就果断入手一只。目前可以跟着《桥边姑娘》的节奏拍下来, 暖阳下 我迎芬芳 是谁家的姑娘,我走在了那座小桥上 你抚琴奏忧伤…… ,是不是有画面了,我跟着节奏去敲手鼓的时候,使我沉浸在这个音乐中,这首音乐有了参与感,对音乐有了更近一步的了解。


feizhoushougu




  1. 在家的时候,如果没什么事情,我会把手机“藏”起来,让自己不那么容易看到手机,有空就看会书或者望着窗外发呆,刚看完村上春树的《当我谈跑步时我谈些什么》,看书的过程会感到很平静




  2. 地铁路上由于通勤时间太久,有时候会闭目养神,尽量让自己少看手机,想一下今天有什么工作没完成,或者计划做点什么事情,让大脑活跃一下。我还把手机上总是发广告的那种公众号,总贩卖焦虑的那些自媒体取关了,不管他(她)是什么行业大佬,还是什么专业XXX,尝试远离他(她)们,发现世界慢慢变的更清净了





最最重要的——坚持,真正值得做的,绝对绝对不会轻而易举


最后,祝每位看官都能活出自己想要的样子 ^-^


作者:草帽lufei
来源:juejin.cn/post/7244748771061219385

收起阅读 »

你的优缺点怎么说?

前言 哈咯,大家好,我是沐华。关于优缺点,我想大家肯定有遇到过:在面试前填的表中有这个问题,或者面试过程中被问到这个问题,还有的人还会在自己的简历自评上写上自己的优点。不知道这个问题的答案,你是怎么写的呢? 我的优点是实战经验比较丰富,技术能力比较强,比如我一...
继续阅读 »

前言


哈咯,大家好,我是沐华。关于优缺点,我想大家肯定有遇到过:在面试前填的表中有这个问题,或者面试过程中被问到这个问题,还有的人还会在自己的简历自评上写上自己的优点。不知道这个问题的答案,你是怎么写的呢?


我的优点是实战经验比较丰富,技术能力比较强,比如我一个人能干三个人的活,而且只收两份钱


缺点是比较喜欢吹牛比,比如上一条,嘿嘿


这个问题重点无非就是想看你是否真诚自信、是否有一个清楚的自我认知、以及一些沟通表达能力


废话不说了,上答案


你觉得你的缺点/劣势是什么?


说缺点,有几个需要注意的点:不能是会伤害到公司或工作的缺点、不能和人家已经提出来的要求相悖,比如人家 jd 上说了要求能加班/抗压能力强,你说你缺点是不能加班/抗压能力不行,那就不行


小技巧是:说缺点的时候可以有些吞吞吐吐的,不会显得那么理直气壮 理所当然,最好加上应对办法或咱们已经采取的行动,比如:




  • 我缺点倒是有一堆,比如对项目全局把控能力不够啊,开始项目不大的时候很多地方自己能设想得到,到后面迭代越做越大的时候,才发现前面做的有些东西就不行了没考虑到,前段时间买了一些架构方面的课程,现在也是在学习中




  • 我会有些较真,有时候跟同事讨论问题,意见不一致时就总忍不住用自己的想法去说服别人,有时候就导致同事不太开心,毕竟一个在一起工作,友好合作才是最重要的,应该控制自己的情绪,摆正心态的,讨论问题应该是各抒已见,畅所欲言的嘛,好在自己意识到这个问题,现在也是在努力改正中




  • 我不太爱说话(上班社恐,下班社牛),表述能力不够吧,因为那些年一个人惯了,一个人吃一个人睡,一个人想念一个人醉,一个人苦一个人累,一个人悲欢一个人体会




  • 可能我不大会拒绝我同事的要求,就是在职场关系处理上,不太会拒绝同事的要求我觉得这个是一个缺点吧,有次就因为帮同事处理问题,结果自己负责的需求耽误了进度,结果被领导说,毕竟自己还有本职工作,还是应该要先做完自己手上的工作,再去帮别人的




  • 我比较容易焦虑,所以我会每件事情之前制定一个计划,规划好每一步,确保充分考虑和准备,尽量不去出现意外状况啥啥的




那我缺点说些和职场无关的行不行?


比如:我的缺点是做饭水平差一些,成品都是这样的,





平时倒还好,最得劲的是拿来招待客人的时候,那是把家里所有房间都改成卫生间都不够用啊,而且客人越多时上菜还越快,都不知道我是怎么做到的,你说难受不。而且除了做饭水平差点之外,我的缺点还有挑食、我胆子小、我怕黑、我恐高...啥啥的


这是不是不太合适?


你觉得你的优点/优势是什么?


优点嘛,或者问业余爱好,这不就是上才艺的时刻嘛,包括咱人生中的高光时刻都可以拿来说


小技巧是:最好是加一些事例哈,而且说的时候这个就不能吞吞吐吐的了,要自信点,最好是拿出装比时的那种气势,然后就像拿捏猫的后颈肉那样,拿捏住,别松了就行。比如:




  • 我学习能力强上手快、我执行力强,(事例:)比如上次领导说后面准备升级 Vue3,给我们两个月时间学习,结果我花了一个月不光学完了 Vue3,而且还自己用 Vue3 开发了一个新系统小项目,然后领导很高兴,还帮我申请了个 500 块钱奖励




  • 我喜欢钻研技术吧,可能还有点强迫症吧,(事例:)有时候遇到问题,就算半夜搞到很晚也要把问题解决掉,比如上次上周我遇到个带 plus 的问题,搞了个通宵愣是解决了,不然要是没解决都睡不着觉




  • 我很会做 PPT,(事例:)每次年中述职、年终述职的时候,同事们都是等我做完了,然后再拿我的去参考,或者让我帮忙看怎么修改。还有会议、有同事转正述职的时候之类的等等所有会用到 PPT 的时候我就成了香饽饽




  • 我喜欢做笔记记录、我善于总结、善于分享,(事例:)并且有在一些技术博客和公众号上发一些文章,其实很多都是自己平时学习中或工作中遇到的




  • 我移动端经验丰富、有全栈开发经验、有管理带人经验、有过超大型项目经验(大厂背景)、有各种证书、很会处理人际关系左右逢源,等等这些都可以




优点不比缺点,说一些与职场无关的,是完全可以的,比如:



  • 我很会做菜,不仅做菜速度快,而且摆盘还好看,(事例:)我家里面一直都是我做饭的,有时候做菜发个朋友圈,或者发个掘金,都有好多人点赞,比如正在看文章的你


最后


如果你还是有些懵懵懂懂的,再给你个小技巧,就是用反义词:



  • 把优点反过来说,就是缺点了

  • 把缺点反过来说,就是优点了


谢谢你宝贵的时间,如果本文对你有一点点帮助,点个赞支持一下吧,你的每一个【】都是我创作的最大动力 ^_^


更多前端文章,或者加入前端交流群,欢迎关注公众号【沐华说技术】,大家一起共同交流和进步呀


往期精彩


【保姆级】Vue3 开发文档


【由浅到深】聊聊 Vue 和 React 的区别,看看你在哪个段位


TS 泛型进阶


深入浅出虚拟 DOM 和 Diff 算法源码,及 Vue2 与 Vue3 中的区别


上手 Vue 新的状态管理 Pinia,一篇文章就够了


作者:沐华
来源:juejin.cn/post/7244761504560595002
收起阅读 »

这份简历,大学生看了要落泪

本文首发自公粽hao「林行学长」,欢迎来撩,免费领取20个求职工具资源包。 了解校招、分享校招知识的学长来了! 今天学长被一份小学生简历震惊到了。 不仅仅是小学生已经开始涉猎简历这回事,更是小学生的简历内容震撼到我了。 就是感叹,现在卷到小学了吗! 当大学生...
继续阅读 »

本文首发自公粽hao「林行学长」,欢迎来撩,免费领取20个求职工具资源包。


了解校招、分享校招知识的学长来了!


今天学长被一份小学生简历震惊到了。


不仅仅是小学生已经开始涉猎简历这回事,更是小学生的简历内容震撼到我了。



就是感叹,现在卷到小学了吗!


当大学生还在为四六级烦恼,一些小学生已经开始接触托福雅思,这学习水平,简直就是“满级人类”啊!


当然,咱们普通人只能看看,有的内容也许咱们根本不用呢(酸言酸语)


不过,简历这回事,还得是大人们的专属烦恼。



毕竟,小学生又不用找工作……


看完了小学生的简历,咱们来说说怎么写才不会把自己的简历写成真·小学生简历。


01 能力跟后面的“举例”要匹配。


大家都很喜欢说“协作能力”,那么协作能力到底是什么呢?


协作能力是指个人或团队在共同完成某项任务时,能够有效地进行沟通、合作和协调的能力。


不要简单地把所有团队合作都当做是“协作能力”的体现。



如果后面体现的明明创意能力,那么跟前面的“协作能力”的表述不一致,就会显得不够专业。


有时候换一种表述方法,也能给自己更多的亮点和竞争力。


02 顺序很重要


不同的工作,有不同的核心。


例如,销售工作中应该用销售额说话,直接把销售成果放在第一条更加抢眼,也更能体现自己的能力。


顺序的排放除了要跟自己相应的工作相匹配之外,还应该跟自己想要申请的职位相匹配。



问问自己:我想申请的职位看重什么能力?需要什么能力?我有什么经历可以体现这样的能力?


然后把这样的能力突出给 HR 看,这样才能事半功倍。


03 词语选择要准确。


动词选择要有力,要实质性;形容词选择不要“感性”,要“理性”。


开头要用动词,动词要能体现你的主动性和积极性,例如:“组织”就比“协助”好。


从形容词来看,感性的、判断性的词不要用,因为主观词语会让人觉得你不太专业。


你所能做的,是要用事实和数字来证明自己,自己的工作“有效”,然后让 HR 自己来判断。


04 拒绝抽象,拒绝小学生作文,用数字说话。


这是老生常谈了,不少同学简历语言一不留神就会不够职业化,显得非常像“小学生作文”。



简历上用数字说话,用结果说话,用事实说话。去证明,而不是去告诉。

作者:林行学长
来源:juejin.cn/post/7244099508212383805

收起阅读 »

面试:(简单粗暴点)百度一面,直接问痛我

web
前言 这次的百度面试挺紧张的,在写算法题的时候脑子都有点空白,还是按照脑海中那点残存的算法技巧才写出来,不至于太尴尬,以及第一次面试百度这种级别的公司,难免出现了一些平常不至于出现的问题或没注意的缺点,在这里分享给大家。 百度一面 1. 如何用chatgpt提...
继续阅读 »

前言


这次的百度面试挺紧张的,在写算法题的时候脑子都有点空白,还是按照脑海中那点残存的算法技巧才写出来,不至于太尴尬,以及第一次面试百度这种级别的公司,难免出现了一些平常不至于出现的问题或没注意的缺点,在这里分享给大家。


百度一面


1. 如何用chatgpt提升前端开发效率



因为我嘴贱,平时习惯了使用chatgpt,然后自我介绍说了一句,由于之前面得公司都没问过,导致我没怎么往这方面准备,以至于答得时候牛头不对马嘴,所以说不愧是大厂啊。




  1. 问题解答和指导ChatGPT可以帮助回答与前端开发相关的问题。当你在编写代码的时候,当一时忘记了某个API怎么用,就可以向ChatGPT提问,并获得解答和指导,甚至还会给出一些更加深入且性能更好的应用。这可以帮助更快地解决问题和理解前端开发中的概念。

  2. 代码片段和示例ChatGPT可以帮助你生成常见的前端代码片段和示例。你可以描述你想要实现的功能或解决的问题,然后向ChatGPT请求相关代码片段。这样,您可以更快地获得一些基础代码,从而加快开发速度。

  3. 自动生成文档ChatGPT可以帮助你生成前端代码的文档。你可以描述一个函数、组件或类,并向ChatGPT请求生成相关的文档注释。这可以帮助您更轻松地为你的代码添加文档,提高代码的可读性和可维护性。

  4. 问题排查和调试:在开发过程中,您可能会遇到问题或错误。您可以向ChatGPT描述您遇到的问题,或者直接把代码交给它,并请求帮助进行排查和调试。ChatGPT可以提供一些建议和指导,帮助您更快地找到问题的根本原因并解决它们。

  5. 学习资源和最新信息ChatGPT可以为你提供关于前端开发的学习资源和最新信息。你可以向ChatGPT询问关于前端开发的最佳实践、最新的框架或库、前端设计原则等方面的问题。这可以帮助我们不断学习和更新自己的前端开发知识,从而提高效率。


2. [1, 2, 3, 4, 5, 6, 7, 8, 9] => [[1, 2, 3],[4, 5, 6],[7, 8, 9]],把一个一维数组变成三个三个的二维数组


在JavaScript中,可以使用数组的slice方法和一个循环来将一个一维数组转换为一个二维数组。下面是一个示例代码:


    function convertTo2DArray(arr, chunkSize) {
var result = [];
for (var i = 0; i < arr.length; i += chunkSize) {
result.push(arr.slice(i, i + chunkSize));
}
return result;
}

var inputArray = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var outputArray = convertTo2DArray(inputArray, 3);

console.log(outputArray);

输出结果将是:


    [[1, 2, 3], [4, 5, 6], [7, 8, 9]]


slice 不会修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组,不信的话自己可以编译一下。



这段代码中的convertTo2DArray函数接受两个参数:arr表示输入的一维数组,chunkSize表示每个子数组的大小。它使用slice方法来从输入数组中提取每个子数组,并使用循环来遍历整个数组并构建输出二维数组。最后,它返回生成的二维数组。


3. 输出结果,为什么?


    const obj3 = {a: 1};
const obj4 = {b: 2};
console.log(obj3 == obj4); // false
console.log(obj3 === obj4); // false

结果:


false,false


原因:


在这段代码中,obj3obj4分别是两个独立的对象,它们开辟的堆内存地址是完全不一样。==运算符用于比较两个操作数是否相等,而===运算符用于比较两个操作数是否严格相等。


根据对象的比较规则,当使用==运算符比较两个对象时,它们将会进行类型转换后再进行比较。由于obj3obj4是不同的对象,即使它们的属性值相同,它们的引用也不同,因此在进行类型转换后,它们会被视为不相等的对象。因此,console.log(obj3 == obj4);的输出结果将会是false


而在使用===运算符比较两个对象时,不会进行类型转换,而是直接比较两个操作数的值和类型是否完全相同。由于obj3obj4是不同的对象,且类型也不同,即使它们的属性值相同,它们也不会被视为严格相等的对象。因此,console.log(obj3 === obj4);的输出结果同样会是false



总结起来,无论是使用==运算符还是===运算符,obj3obj4都不会被视为相等或严格相等的对象,因为它们是不同的对象。



4. this有关 输出结果,为什么?


    const obj1 = {
fn: () => {
  return this
}
}
const obj2 = {
fn: function(){
  return this
}
}

console.log(obj1.fn());
console.log(obj2.fn());

输出结果:



  1. window || undefined

  2. obj2


原因是:


在箭头函数 fn 中的 this 关键字指向的是定义该函数的上下文,而不是调用该函数的对象。因此,当 obj1.fn() 被调用时,由于箭头函数没有它自己的this,当你调用fn()函数时,this指向会向上寻找,因此箭头函数中的 this 指向的是全局对象(在浏览器环境下通常是 window 对象),因此返回的是 undefined


而在普通函数 fn 中的 this 关键字指向的是调用该函数的对象。在 obj2.fn() 中,函数 fn 是作为 obj2 的方法被调用的,所以其中的 this 指向的是 obj2 对象本身,因此返回的是 obj2


需要注意的是,在严格模式下,普通函数中的 this 也会变为 undefined,因此即使是 obj2.fn() 也会返回 undefined。但在示例中没有明确指定使用严格模式,所以默认情况下运行在非严格模式下。


5. Promise有关输出结果,为什么?


    console.log('1');
function promiseFn() {
return new Promise((resolve, reject) => {
  setTimeout(()=> {
    console.log('2');
  })
  resolve('3');
  console.log('4')
})
}

promiseFn().then(res => {
console.log(res);
});

输出结果: 1 4 3 2


原因是:



  1. 首先,代码从上往下执行,把console.log('1')放入同步任务

  2. 再调用promiseFn(),因为new Promise是同步任务,所以放入同步任务,继续执行

  3. 遇到setTimout这个宏任务,放入宏任务队列中

  4. 遇到resolve('3'),把res返回

  5. 之后再执行.then(),因为promise.then是微任务,所以放入微任务队列

  6. 代码是先执行同步任务,再执行微任务,之后再是宏任务

  7. 所以输出结果为1 4 3 2



这里涉及到了EventLoop的执行机制,如果不是太清楚可以看看我的面试题:小男孩毕业之初次面试第二家公司第一题



6. 实现斐波那契的第N个值(从0开始),要求时间复杂度为O(n)



首先,说到斐波那契第一个想到的肯定是如下的算法,但这可是百度啊,如果只是这种程度的话如何能和同样面相同岗位的人竞争呢,所以我们得想到如下算法有什么缺点,然后如何优化



function fib(n) {
if (n == 0 || n === 1) return 1;
return fib(n - 1) + fib(n - 2);
};

console.log(fib(3)); // 5
console.log(fib(5)); // 8

单纯的使用递归看似没什么问题,也能运算出结果,但是里面有个致命的问题,首先,时间复杂度就不对,递归思想的复杂度为 O(2^n) ,它不为O(n),然后还有会重复计算,比如计算n=3时,会计算fib(1) + fib(2),再次计算fib(4)时,会先算fib(3) = fib(1) + fib(2),然后再计算fib(4) = fib(1) + fib(2) + fib(3),在这里,fib(1)和fib(2)重复计算了两次,对于性能损耗极大。此时的你如果对动态规划敏感的话,就会从中想到动态规划其中最关键的特征——重叠子问题



因此,使用动态规划来规避重复计算问题,算是比较容易想到较优的一种解法,并且向面试官展现了你算法能力中有动态规划的思想,对于在面试中的你加分是极大的。



以下是动态规划思路的算法,状态转移方程为dp[i] = dp[i-1] + dp[i-2]


function fibonacci(n) { 
if (n <= 1) return n;
let fib = [0, 1]; // 保存斐波那契数列的结果
for (let i = 2; i <= n; i++) {
fib[i] = fib[i - 1] + fib[i - 2]; // 计算第i个斐波那契数
}
return fib[n];
}



当然,你可能会说,在面试中怎么可能一下子就能想到动态规划,所以在面试前你需要背一背相关的状态转移方程,当你对算法问题分析到一定程度时,就能够记忆起这些状态转移方程,提高你写算法的速度。



在面试中,动态规划的常用状态转移方程可以根据问题的具体情况有所不同。以下是几个常见的动态规划问题和它们对应的状态转移方程示例:




  1. 斐波那契数列(Fibonacci Sequence):



    • dp[i] = dp[i-1] + dp[i-2],其中 dp[i] 表示第 i 个斐波那契数。




  2. 爬楼梯问题(Climbing Stairs):



    • dp[i] = dp[i-1] + dp[i-2],其中 dp[i] 表示爬到第 i 级楼梯的方法数。




  3. 背包问题(Knapsack Problem):



    • dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]),其中 dp[i][j] 表示在前 i 个物品中选择总重量不超过 j 的最大价值,weight[i] 表示第 i 个物品的重量,value[i] 表示第 i 个物品的价值。




  4. 最长递增子序列(Longest Increasing Subsequence):



    • dp[i] = max(dp[j] + 1, dp[i]),其中 dp[i] 表示以第 i 个元素结尾的最长递增子序列的长度,j0i-1 的索引,且 nums[i] > nums[j]




  5. 最大子数组和(Maximum Subarray Sum):



    • dp[i] = max(nums[i], nums[i] + dp[i-1]),其中 dp[i] 表示以第 i 个元素结尾的最大子数组和。




  6. 最长公共子序列(Longest Common Subsequence):




    • 如果 str1[i] 等于 str2[j],则 dp[i][j] = dp[i-1][j-1] + 1




    • 否则,dp[i][j] = max(dp[i-1][j], dp[i][j-1]),其中 dp[i][j] 表示 str1 的前 i 个字符和 str2 的前 j 个字符的最长公共子序列的长度。






  7. 编辑距离(Edit Distance):




    • 如果 word1[i] 等于 word2[j],则 dp[i][j] = dp[i-1][j-1]




    • 否则,dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1,其中 dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最少操作次数。






  8. 打家劫舍(House Robber):



    • dp[i] = max(dp[i-1], dp[i-2] + nums[i]),其中 dp[i] 表示前 i 个房屋能够获得的最大金额,nums[i] 表示第 i 个房屋中的金额。




  9. 最大正方形(Maximal Square):




    • 如果 matrix[i][j] 等于 1,则 dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1




    • 否则,dp[i][j] = 0,其中 dp[i][j] 表示以 matrix[i][j] 为右下角的最大正方形的边长。






7. 手写EventBus


当需要手动实现一个简单的 EventBus 时,你可以创建一个全局的事件总线对象,并在该对象上定义事件的订阅和发布方法。


class EventBus {
constructor() {
this.events = {}; // 存储事件及其对应的回调函数列表
}

// 订阅事件
subscribe(eventName, callback) {
this.events[eventName] = this.events[eventName] || []; // 如果事件不存在,创建一个空的回调函数列表
this.events[eventName].push(callback); // 将回调函数添加到事件的回调函数列表中
}

// 发布事件
publish(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => {
callback(data); // 执行回调函数,并传递数据作为参数
});
}
}

// 取消订阅事件
unsubscribe(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback); // 过滤掉要取消的回调函数
}
}
}

使用上述 EventBus 类,你可以执行以下操作:


// 创建全局事件总线对象
const eventBus = new EventBus();

const callback1 = data => {
console.log('Callback 1:', data);
};

const callback2 = data => {
console.log('Callback 2:', data);
};

// 订阅事件
eventBus.subscribe('event1', callback1);
eventBus.subscribe('event1', callback2);

// 发布事件
eventBus.publish('event1', 'Hello, world!');

// 输出:
// Callback 1: Hello, world!
// Callback 2: Hello, world!

// 取消订阅事件
eventBus.unsubscribe('event1', callback1);

// 发布事件
eventBus.publish('event1', 'Goodbye!');

// 输出:
// Callback 2: Goodbye!

在上述示例中,我们创建了一个 EventBus 类,该类具有 subscribepublishunsubscribe 方法。subscribe 方法用于订阅事件,publish 方法用于发布事件并触发相关的回调函数,unsubscribe 方法用于取消订阅事件。我们使用全局的 eventBus 对象来执行订阅和发布操作。


这个简单的 EventBus 实现允许你在不同的组件或模块之间发布和订阅事件,以实现跨组件的事件通信和数据传递。你可以根据需要对 EventBus 类进行扩展,添加更多的功能,如命名空间、一次订阅多个事件等。



当问到EventBus时,得预防面试官问到EvnetEmitter,不过当我在网上查找相关的资料时,发现很多人似乎都搞混了这两个概念,虽然我在这里的手写原理似乎也差不多,但在实际使用中,两者可能在细节上有所不同。因此,在具体场景中,你仍然需要根据需求和所选用的实现来查看相关文档或源码,以了解它们的具体实现和用法。



下面是一个简单的 EventEmitter 类实现的基本示例:


class EventEmitter {
constructor() {
this.events = {}; // 用于存储事件及其对应的回调函数列表
}

// 订阅事件
on(eventName, callback) {
this.events[eventName] = this.events[eventName] || []; // 如果事件不存在,创建一个空的回调函数列表
this.events[eventName].push(callback); // 将回调函数添加到事件的回调函数列表中
}

// 发布事件
emit(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => {
callback(data); // 执行回调函数,并传递数据作为参数
});
}
}

// 取消订阅事件
off(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback); // 过滤掉要取消的回调函数
}
}

// 添加一次性的事件监听器
once(eventName, callback) {
const onceCallback = data => {
callback(data); // 执行回调函数
this.off(eventName, onceCallback); // 在执行后取消订阅该事件
};
this.on(eventName, onceCallback);
}
}

使用上述 EventEmitter 类,你可以执行以下操作:


    const emitter = new EventEmitter();

const callback1 = data => {
console.log('Callback 1:', data);
};

const callback2 = data => {
console.log('Callback 2:', data);
};

// 添加一次性事件监听器
const onceCallback = data => {
console.log('Once Callback:', data);
};

// 订阅事件
emitter.on('event1', callback1);
emitter.on('event1', callback2);
emitter.once('event1', onceCallback);

// 发布事件
emitter.emit('event1', 'Hello, world!');

// 输出:
// Callback 1: Hello, world!
// Callback 2: Hello, world!
// Once Callback: Hello, world!

// 取消订阅事件
emitter.off('event1', callback1);

// 发布事件
emitter.emit('event1', 'Goodbye!');

// 输出:
// Callback 2: Goodbye!

在上述示例中,EventEmitter 类具有 onemitoffonce 方法。on 方法用于订阅事件,emit 方法用于发布事件并触发相关的回调函数,off 方法用于取消订阅事件,once方法用于添加一次性的事件监听器。你可以根据需求对 EventEmitter 类进行扩展,添加更多的功能,比如一次订阅多个事件、取消所有事件订阅等。


eventBus,eventEmitter的区别


EventBusEventEmitter 都是用于实现事件发布-订阅模式的工具,但它们在实现和使用上有一些区别。




  1. 实现方式:



    • EventBusEventBus 是一个全局的事件总线,通常是作为一个单例对象存在,用于在不同组件或模块之间传递事件和数据。在 Vue.js 中,Vue 实例可以充当 EventBus 的角色。

    • EventEmitterEventEmitter 是一个基于类的模块,通常是作为一个实例对象存在,用于在单个组件或模块内部实现事件的发布和订阅。




  2. 使用范围:



    • EventBusEventBus 的作用范围更广泛,可以跨越不同组件、模块或文件进行事件的发布和订阅。它可以实现多个组件之间的通信和数据传递。

    • EventEmitterEventEmitter 主要用于单个组件或模块内部,用于实现内部事件的处理和通信。




  3. 依赖关系:



    • EventBusEventBus 通常需要一个中央管理的实例,因此需要在应用程序的某个地方进行创建和管理。在 Vue.js 中,Vue 实例可以用作全局的 EventBus

    • EventEmitterEventEmitter 可以在需要的地方创建实例对象,并将其用于内部事件的发布和订阅。




  4. 命名空间:



    • EventBusEventBus 可以使用不同的事件名称来进行事件的区分和分类,可以使用命名空间来标识不同类型的事件。

    • EventEmitterEventEmitter 通常使用字符串作为事件的名称,没有直接支持命名空间的概念。





总结起来,EventBus 主要用于实现跨组件或模块的事件通信和数据传递,适用于大型应用程序;而 EventEmitter 主要用于组件或模块内部的事件处理和通信,适用于小型应用程序或组件级别的事件管理。选择使用哪种工具取决于你的具体需求和应用场景。



8. (场景题)在浏览器中一天只能弹出一个弹窗,如何实现,说一下你的思路?


要在浏览器中实现一天只能弹出一个弹窗的功能,可以使用本地存储(localStorage)来记录弹窗状态。下面是一种实现方案:



  1. 当页面加载时,检查本地存储中是否已存在弹窗状态的标记。

  2. 如果标记不存在或者标记表示上一次弹窗是在前一天,则显示弹窗并更新本地存储中的标记为当前日期。

  3. 如果标记存在且表示上一次弹窗是在当天,则不显示弹窗。


以下是示例代码:


    // 检查弹窗状态的函数
function checkPopupStatus() {
// 获取当前日期
const currentDate = new Date().toDateString();

// 从本地存储中获取弹窗状态标记
const popupStatus = localStorage.getItem('popupStatus');

// 如果标记不存在或者标记表示上一次弹窗是在前一天
if (!popupStatus || popupStatus !== currentDate) {
// 显示弹窗
displayPopup();

// 更新本地存储中的标记为当前日期
localStorage.setItem('popupStatus', currentDate);
}
}

// 显示弹窗的函数
function displayPopup() {
// 在这里编写显示弹窗的逻辑,可以是通过修改 DOM 元素显示弹窗,或者调用自定义的弹窗组件等
console.log('弹出弹窗');
}

// 在页面加载时调用检查弹窗状态的函数
checkPopupStatus();

在这个实现中,checkPopupStatus 函数会在页面加载时被调用。它首先获取当前日期,并从本地存储中获取弹窗状态的标记。如果标记不存在或者表示上一次弹窗是在前一天,就会调用 displayPopup 函数显示弹窗,并更新本地存储中的标记为当前日期。


通过这种方式,就可以确保在同一天只能弹出一个弹窗,而在后续的页面加载中不会重复弹窗。


9. 项目中的性能优化?




  1. 对组件和图片进行懒加载对暂时未使用的组件和图片使用懒加载可以显著地减少页面加载时间,比如在我的项目中路由配置中除了需要频繁切换的页面组件外,其他的组件都使用箭头函数引入组件进行懒加载,以及一些没有展现在界面的图片也进行了一个VueLazy的懒加载。




  2. 减少HTTP请求数量由于频繁的请求会对后端服务器造成极大的负担,所以应该减少不必要的请求,比如在我的项目中的搜索界面,对于搜索按钮增加了防抖功能




  3. 使用缓存使用浏览器缓存可以减少资源请求,从而提高页面加载速度。项目中我会把用户的一些需要持久化的信息存入本地存储。




  4. 异步请求使用Promise.all:异步请求可以在后台加载资源,从而避免阻塞页面加载。在请求数据时,我会使用Promise.all一次性并行的请求类似的数据,而不需要一个一个的请求,较少了请求时间。




  5. 图片优化使用适当的图片格式和大小可以减少页面的资源请求和加载时间,项目中我会把图片转化成base64的格式和webp格式,这样可以使图片大小更小




  6. 使用CDN加速:使用CDN可以提高资源的访问速度,从而加快页面加载速度。我项目中的一些第三方资源有时需要请求,因此我会使用CDN内容分发网络来提高访问速度。




  7. 骨架屏(Skeleton Screen):它可以提升用户感知的加载速度和用户体验。虽然骨架屏本身并不直接影响代码性能,但它可以改善用户对应用程序的感知,提供更好的用户体验。




10. 项目中遇到的难点,如何解决


1. 数据状态管理


前端登录状态管理



  • 我在一个练手的项目中做前端登录功能的时候, 碰到了购物车需要登录判断的功能,比如用isLogin来判断有没有登录,当时由于没有深入了解vuex,所以我一开始想着把这个isLogin通过组件与组件的传值方法,把这个值传给相应的组件,然后在需要登录组件中进行判断,但后来发现这个方法太麻烦了

  • 后来通过学习了解,使用了vuex这个全局状态管理的方法, 通过使用createStore这个vuex中的API创建了一个全局的登录状态,再通过actions mutations实现登录判断和登录状态共享


组件数据状态管理



  • 我项目中一开始首页、详情页等其他页面越来越多的状态放在同一个store上,虽然感觉有点乱,但实现了数据流和组件开发的分离,使得我更能够专注于数据的管理

  • 但随着数据的增多,感觉实在太乱了,然后得知vuex中可以使用 modules 来进行分模块,相应的页面放入相应的模块状态中,之后再用actions,mutations,state,getters这四件套, 更好的模块化管理数据,能够知道哪些状态是全局共享的(登录), 哪些状态是模块共享的

  • 然后在新的项目中,也就是现在简历上的项目里,尝试使用pinia来管理,因为我发现它更简单(没有mutations),模块化更好,让我对组件状态管理的更加得心应手,学习起来也更加的方便。


node的错误处理



  • 一开始用node写后端的时候,一堆错误,比如路由没配置,数据库报错。使得后面的代码都无法运行,写着写着就感觉写不下去,经常一个错误就需要反复的在脑海中想最后依靠那一丝的灵光一闪才解决

  • 之后我就在app.js这个后端入口文件的最后,添加一个统一的错误处理的中间件,向前端返回状态码和相应的信息后,直接使用next()向后继续执行,这样虽然服务器报了错,但仍然可以执行后续的代码。


跨域问题



  • 在我写完前端项目的时候,想要提升一下自己,就转去学习了Koa,在搭建了大致的服务器,写了一个简单的接口并运行服务器后,我想当然的就在前端直接请求后端的端口,结果报了一个跨域的错误,由于当时初学后端,不怎么了解跨域,所以找了很多的解答并逐个在项目中进行尝试,比如跨域中的scriptpostMessagehtml本身的Websocket

  • 但发现最实用的还是在服务器中配置Access-Control-Allow-Origin来控制跨域请求的url地址,以及其他一些Access-Control-Allow头来控制跨域请求方法等,然后跨域请求url的白名单我放入了.env这个全局环境变量中。


axios响应拦截



  • 在后端返回数据的时候,我返回数据有一个状态码以及添加到data这个需要返回的数据(代码如下),这导致我在获取接口里的数据时需要多.data(引用一层data),当时我没意识到,结果一直获取不到数据。之后输出获取的数据才发现在数据外面包了一层,虽然这个时候解决了服务器那边数据返回的问题,但后面每次获取数据时都需要在往里再获取,非常的麻烦。

  • 最后在学习了并在项目中使用axios进行请求和响应后,就在响应的时候设置一个拦截器,对响应进行一番处理之后就可以直接拿到后端接口返回的值,而不会导致接口返回的值不会有太多的嵌套了。


11. 如何学习前端的,学了几年?



这个就看个人情况了,但其中,你得展现出你的学习积极性和对前端的热爱,让面试官能够欣赏你



我大致说说我回答的,仅作参考


我从大二开始就对前端很感兴趣,当时正好学校也分了Web前端的方向,于是就跟着学校的课程开始学习基本的html,css,js三剑客,但之后感觉到老师教的很慢,就自己到B站上学习了,之后由于参加过一次蓝桥杯,就看到了蓝桥云课上有相关的基于html,css,js比较基础项目,接着我还学习了一些行内大牛写的一些博客文章,比如阮一峰,张鑫旭,廖雪峰等这些老师。之后又学习了vue并且在GitHub上学习相关的设计理念,根据GitHub上项目中不懂的东西又逐渐学习了各种UI组件库和数据请求方式,最后又学习了Nodejs中的Koa,用Vue和Koa仿写了一个全栈型项目,目前正在学习一些typescript的基本用法并尝试着运用到项目中,并在学习Vue的一些底层源码。


结语及吐槽


大厂的面试终归到底还是和我之前面的公司不一样,它们更加看重的是代码底层的实现和你的算法基础,终归到底,这次面试只是一次小尝试,想要知道自己的水平到底在哪里,并且能够借此完善自己的能力,努力的提升自己,希望能够给

作者:吃腻的奶油
来源:juejin.cn/post/7240751116701728805
大家带来一些正能量。

收起阅读 »

优化图片和视频的加载过程,提升用户体验

web
展示效果 (因为掘金不能上传视频,所以转成动图之后分辨率比较低,还望多包涵) 展示都是基于 Slow 3G 弱网下的效果。 优化前 这种体验交较差,在图片下载完之前,本应该展示图片的区域会长时间空白。 优化后 图片下载过程中显示模糊的图片占位符,直到图片下...
继续阅读 »

展示效果


(因为掘金不能上传视频,所以转成动图之后分辨率比较低,还望多包涵)


展示都是基于 Slow 3G 弱网下的效果。


优化前


before.gif


这种体验交较差,在图片下载完之前,本应该展示图片的区域会长时间空白。


优化后


eeeee.gif


图片下载过程中显示模糊的图片占位符,直到图片下载完成再切换展示。


原理


首先先贴出页面的代码 index.html:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<style>
html,
body {
margin: 0;
padding: 0;
}

@keyframes pulse {
0% {
opacity: 0;
}
50% {
opacity: 0.1;
}
100% {
opacity: 0;
}
}

.container {
width: 50vw;
background-repeat: no-repeat;
background-size: cover;
}

.container.loaded::before {
animation: none;
content: none;
}

.container::before {
content: '';
position: absolute;
inset: 0;
opacity: 0;
animation: pulse 2.5s infinite;
background-color: var(--text-color);
}

.container img,
.container video {
opacity: 0;
transition: opacity 250ms ease-in-out;
}

.container.loaded img,
.container.loaded video {
opacity: 1;
}
</style>
<body>
<!-- container容器加载一个体积非常小的低分辨率图片 -->
<div class="container" style="background-image: url(http://localhost:3000/uploads/10007/fox-small.jpeg);">
<!-- 图片延时加载 loading: lazy -->
<img
src="http://localhost:3000/uploads/10007/fox.jpeg"
loading="lazy"
style="width: 50vw"
/>

</div>

<br/>

<video
id="video"
autoplay
controls="controls"
style="width: 50vw"
poster="http://localhost:3000/uploads/10007/big_buck_bunny-small.png"
src="http://localhost:3000/uploads/10007/big_buck_bunny.mp4"
>
</video>
</body>
<script>
const blurredImageDiv = document.querySelector('.container');
const img = blurredImageDiv.querySelector('img');
function loaded() {
// 图片下载完之后 再展示
blurredImageDiv.classList.add('loaded');
}

if (img.complete) {
loaded();
} else {
img.addEventListener('load', loaded);
}

var poster = new Image();
poster.onload = function () {
// 加载完之后替换 poster url 不会重复请求
const video = document.querySelector('#video');
video.poster = 'http://localhost:3000/uploads/10007/big_buck_bunny.png';
};
poster.src = 'http://localhost:3000/uploads/10007/big_buck_bunny.png';
</script>
</html>

其实原理就是基于原图片生成出一个低分辨率体积非常小的图片(因为体积小,下载会很快),然后作为占位符显示,直到原图片完全下载之后再替换展示原图片。


那么如何生成一个超低分辨率的占位图片呢,可以使用 ffmpeg,需要本地提前安装,我是用的MacOS系统,所以直接通过 brew install ffmpeg 安装了。


如果是服务使用 Docker 部署的话,可参考:


FROM node:16 AS deps
WORKDIR /app
COPY . .
RUN wget https://www.johnvansickle.com/ffmpeg/old-releases/ffmpeg-4.4.1-arm64-static.tar.xz &&\
tar xvf ffmpeg-4.4.1-arm64-static.tar.xz &&\
mv ffmpeg-4.4.1-arm64-static/ffmpeg /usr/bin/ &&\
mv ffmpeg-4.4.1-arm64-static/ffprobe /usr/bin/
#RUN apt install -y ffmpeg
RUN yarn install
RUN yarn build
EXPOSE 3000
ENV PORT 3000
CMD [ "node", "dist/index.js" ]

ffmpeg -i sourcePath.jpg -vf scale=width:height outputPath.jpg
// 约束比例压缩
// width/height 为压缩之后图片的宽高 当其中一个值为 -1 的时候将保持原来的尺寸比例压缩

那么我们可以有如下命令:


ffmpeg -i sourcePath.jpg -vf scale=20:-1 outputPath.jpg
// 压缩之后生成 20 像素宽的图片用于做占位符展示

我们可以写个文件上传的服务,上传图片之后,服务端自动生成一个低分辨率的图片版本,然后将两者的地址url都返回过来。比如 Node 中我们可以使用 fluent-ffmpeg,那么以上命令就对应成代码:


import * as ffmpeg from 'fluent-ffmpeg';
import { FfmpegCommand } from 'fluent-ffmpeg';

export const runFfmpegCmd = (command: FfmpegCommand) =>
new Promise<void>((resolve, reject) => {
command
.on('error', (error) => {
reject(error);
})
.on('end', () => {
resolve();
})
.run();
});


public async uploadImage(user: User.UserInfo, file: Express.Multer.File) {
console.log(file);
const path = join(this.uploadPath, `${user.id}`, '/');

await ensureDir(path);

const { originalname, path: filePath } = file;
const finalPath = path + originalname;
const name = originalname.split('.');
const smallPath = path + name[0] + '-small.' + name[1];
console.log(smallPath);
await rename(filePath, finalPath);

// size 对应 scale=20:-1
await runFfmpegCmd(ffmpeg(finalPath).size('20x?').output(smallPath));

return {
statusCode: HttpStatus.OK,
data: {
path: finalPath,
smallPath,
},
};
}

public async uploadVideo(user: User.UserInfo, file: Express.Multer.File) {
console.log(file);
const path = join(this.uploadPath, `${user.id}`, '/');

await ensureDir(path);

const { originalname, path: filePath } = file;
const finalPath = path + originalname;
const name = originalname.split('.');
const shotName = name[0] + '.png';
const smallName = name[0] + '-small.png';

await rename(filePath, finalPath);

// 生成两个不同分辨率的缩略图
await Promise.all([
runScreenShotCmd(
ffmpeg(finalPath).screenshot({
count: 1,
filename: shotName,
folder: path,
}),
),
runScreenShotCmd(
ffmpeg(finalPath).screenshot({
count: 1,
filename: smallName,
folder: path,
size: '20x?',
}),
),
]);

return {
statusCode: HttpStatus.OK,
data: {
path: finalPath,
shotPath: path + shotName,
smallPath: path + smallName,
},
};
}

代码在自己的github上:im_server


自己本地的 swagger 界面的上传截图:


图片
image.png


视频
image.png


那么我们就可以得到一个超低分辨率的图片了,由于体积非常小,所以下载很快(特别是弱网情况下)。


补充


关于 img 标签的 lazy load 可参考:浏览器IMG图片原生懒加载loading=”lazy”实践指南


使用 imgsrcset 属性可实现根据不同屏幕分辨率加载不同尺寸的图片,进一步提升用户体验,而且没必要在小屏幕中加载超大分辨率的图片:响应式图片


结论


通过使用超低分辨率的占位符图片可以优化用户体验,特别是一些图片素材网站,再结合 img 标签的 loading="lazy"

作者:梦想很大很大
来源:juejin.cn/post/7244352006814679100
code> 懒加载。

收起阅读 »

记一次雪花算法遇到的 生产事故!

你好,我是悟空。 最近生产环境遇到一个问题: 现象:创建工单、订单等地方,全都创建数据失败。 初步排查:报错信息为duplicate key,意思是保存数据的时候,报主键 id 重复,而这些 id 都是由雪花算法生成的,按道理来说,雪花算法是生成分布式唯一 I...
继续阅读 »

你好,我是悟空。


最近生产环境遇到一个问题:


现象:创建工单、订单等地方,全都创建数据失败。


初步排查:报错信息为duplicate key,意思是保存数据的时候,报主键 id 重复,而这些 id 都是由雪花算法生成的,按道理来说,雪花算法是生成分布式唯一 ID,不应该生成重复的 ID。


大家可以先猜猜是什么原因。


有的同学可能对雪花算法不熟悉,这里做个简单的说明。


一、雪花算法


snowflake(雪花算法):Twitter 开源的分布式 id 生成算法,64 位的 long 型的 id,分为 4 部分:


snowflake 算法



  • 1 bit:不用,统一为 0

  • 41 bits:毫秒时间戳,可以表示 69 年的时间。

  • 10 bits:5 bits 代表机房 id,5 个 bits 代表机器 id。最多代表 32 个机房,每个机房最多代表 32 台机器。

  • 12 bits:同一毫秒内的 id,最多 4096 个不同 id,自增模式


优点:



  • 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。

  • 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。

  • 可以根据自身业务特性分配bit位,非常灵活。


缺点:



  • 强依赖机器时钟,如果机器上时钟回拨(可以搜索 2017 年闰秒 7:59:60),会导致发号重复或者服务会处于不可用状态。


看了上面的关于雪花算法的简短介绍,想必大家能猜出个一二了。


雪花算法和时间是强关联的,其中有 41 位是当前时间的时间戳,


二、排查


2.1 雪花算法有什么问题?


既然是雪花算法的问题,那我们就来看下雪花算法出了什么问题:


(1)What:雪花算法生成了重复的 ID,这些 ID 是什么样的?


(2)Why:雪花算法为什么生成了重复的 key


第一个问题,我们可以通过报错信息发现,这个重复的 ID 是 -1,这个就很奇怪了。一般雪花算法生成的唯一 ID 如下所示,我分别用二进制和十进制来表示:


十进制表示:2097167233578045440

二进制表示:0001 1101 0001 1010 1010 0010 0111 1100 1101 1000 0000 0010 0001 0000 0000 0000

找到项目中使用雪花算法的工具类,生成 ID 的时候有个判断逻辑:



当前时间小于上次的生成时间就会返回 -1,所以问题就出在这个逻辑上面。(有的雪花算法是直接抛异常)



if (timestamp < this.lastTimestamp) {
return -1;
}


由于每次 timestamp 都是小于 lastTimeStamp,所以每次都返回了 -1,这也解释了为什么生成了重复的 key。


2.2 时钟回拨或跳跃


那么问题就聚焦在为什么当前时间还会小于上次的生成时间


下面有种场景可能发生这种情况:


首先假定当前的北京时间是 9:00:00。另外上次生成 ID 的时候,服务器获取的时间 lastTimestamp=10:00:00,而现在服务器获取的当前时间 timestamp=09:00:00,这就相当于服务器之前是获取了一个未来时间,现在突然跳跃到当前时间。


而这种场景我们称之为时钟回拨时钟跳跃


时钟回拨:服务器时钟可能会因为各种原因发生不准,而网络中会提供 NTP 服务来做时间校准,因此在做校准的时候,服务器时钟就会发生时钟的跳跃或者回拨问题。


2.3 时钟同步


那么服务器为什么会发生时钟回拨或跳跃呢?



我们猜测是不是服务器上的时钟不同步后,又自动进行同步了,前后时间不一致。



首先我们的每台服务器上都安装了 ntpdate 软件,作为 NTP 客户端,会每隔 10 分钟NTP 时间服务器同步一次时间。


如下图所示,服务器 1 和 服务器 2 部署了应用服务,每隔 10 分钟向时间服务器同步一次时间,来保证服务器 1 和服务器 2 的时间和时间服务器的时间一致。



每隔 10 分钟同步的设置:


*/10 * * * * /usr/sbin/ntpdate <ip>

另外时间服务器会向 NTP Pool同步时间,NTP Pool 正在为世界各地成百上千万的系统提供服务。 它是绝大多数主流Linux发行版和许多网络设备的默认“时间服务器”。(参考ntppool.org)


那问题就是 NTP 同步出了问题??


2.4 时钟不同步


我们到服务器上查看了下时间,确实和时钟服务器不同步,早了几分钟。


当我们执行 NTP 同步的命令后,时钟又同步了,也就是说时间回拨了。


ntpdate  <时钟服务器 IP>

在产生事故之前,我们重启过服务器 1。我们推测服务器重启后,服务器因网络问题没有正常同步。而在下一次定时同步操作到来之前的这个时间段,我们的后端服务已经出现了因 ID 重复导致的大量异常问题。


这个 NTP 时钟回拨的偶发现象并不常见,但时钟回拨确实会带了很多问题,比如润秒 问题也会带来 1s 时间的回拨。


闰秒就是通过给“世界标准时间”加(或减)1秒,让它更接近“太阳时”。例如,两者相差超过0.9秒时,就在23点59分59秒与00点00分00秒之间,插入一个原本不存在的“23点59分60秒”,来将时间调慢一秒钟。


为了预防这种情况的发生,网上也有一些开源解决方案。


三、解决方案


(1)方式一:使用美团 Leaf方案,基于雪花算法。


(2)方式二:使用百度 UidGenerator,基于雪花算法


(3)方式三:用 Redis 生成自增的分布式 ID。弊端是 ID 容易被猜到,有安全风险。


3.1 美团的 Leaf 方案


美团的开源项目 Leaf 的方案:采用依赖 ZooKeeper 的数据存储。如果时钟回拨的时间超过最大容忍的毫秒数阈值,则程序报错;如果在可容忍的范围内,Leaf 会等待时钟同步到最后一次主键生成的时间后再继续工作


重点就是需要等待时钟同步!



3.2 百度 UidGenerator 方案


百度UidGenerator方案不在每次获取 ID 时都实时计算分布式 ID,而是利用 RingBuffer 数据结构,通过缓存的方式预生成一批唯一 ID 列表,然后通过 incrementAndGet() 方法获取下一次的时间,从而脱离了对服务器时间的依赖,也就不会有时钟回拨的问题。


重点就是预生成一批 ID!


Github地址:


https://github.com/baidu/uid-generator

四、总结


本篇通过一次偶发的生产事故,引出了雪花算法的原理、雪花算法的不足、对应的开源解决方案。


雪花算法强依赖服务器的时钟,如果时钟产生了回拨,就会造成很多问题。


我们的系统虽然做了 NTP 时钟同步,但也不是 100% 可靠,而且润秒这种场景也是出现过很多次。鉴于此,美团和百度也有对应的解决方案。


最后,我们的生产环境也是第一次遇到因 NTP 导致的时钟回拨,而且系统中用到雪花算法的地方并不多,所以目前并没有采取以上的替换方案。


https://github.com/Jackson0714/PassJava-Platform/blob/master/passjava-common/src/main/java/com/jackson0714/passjava/common/utils/SnowflakeUtilV2.java

参考资料:


time.geekbang.org/dailylesson…


blog.csdn.net/liangcsdn11…


http://www.jianshu.com/p/2911

作者:悟空聊架构
来源:juejin.cn/post/7244339465559408695
10ca6…

收起阅读 »

AIGC明星独角兽爆雷!7亿融资烧大半,拖欠员工工资,创始人被扒得千疮百孔

Stable Diffusion背后团队创始人,被曝巨大丑闻! 这两天,福布斯发布的一则长新闻在网上病毒式传播: 30多位前员工+投资人现身说法,细数Stability AI老板Emad Mostaque(伊玛德·莫斯塔克)9大罪证—— 学历造假,根本没有取...
继续阅读 »

Stable Diffusion背后团队创始人,被曝巨大丑闻!


这两天,福布斯发布的一则长新闻在网上病毒式传播:


30多位前员工+投资人现身说法,细数Stability AI老板Emad Mostaque(伊玛德·莫斯塔克)9大罪证——



  • 学历造假,根本没有取得牛津大学硕士学位;

  • 把Stable Diffusion的10亿代码成果“据为己有”;

  • 拖欠大量员工工资,甚至不交工资税;

  • ……


据爆料,这家所谓刚融资1亿美元(折合人民币约7亿)、估值10亿美元晋升独角兽的“明星公司”,其创始人不仅夸大了Stability AI公司的收入,其后续融资也不顺利,只是对外表示“成功”。


图片


在Stability AI工作过的前员工Eric Hallahan在推特阐述亲身经历:



和我的感受一样,伊玛德擅长把别人的作品写上自己的名字,或者做一些你无法确认是否属实的事情。



图片


甚至有网友看完后给出了有点耸人听闻的结论:



与Sam Bankman Fried(币圈诈骗被捕)、Bernie Madoff、Elizabeth Holmes(滴血验癌女王)等人类似,伊玛德·莫斯塔克也是这样的骗子。


显然,这标志着Stability AI的终结



图片

图片


这究竟是怎么一回事?


Stability AI创始人原来是个大忽悠?


福布斯曝光的9大具体罪状,按照严重程度先后为:


01、窃取Stable Diffusion成果


这起纠纷还得追溯到去年10月。当时Stability AI刚刚拿到1.01亿美元,一举成为估值10亿独角兽。


前脚刚融资完,后脚一家叫做Runway的AI初创公司就突然“截胡”,发布了一个Stable Diffusion新版本,并表示自己才是Stable Diffusion的原作者。


图片


原来,让Stability AI声名鹊起的Stable Diffusion,核心代码是由慕尼黑大学、海德堡大学和Runway共同完成的,Stability AI只是作为“金主”,提供了计算资源。


但是Runway等作者认为,Stability AI直接将该成果据为己有,比如在Stable Diffusion去年8月正式发布之时,Stability AI的新闻稿直接将伊玛德写成“有史以来最受欢迎的开源软件”的首席布道者。


没过多久,Stability AI的筹款平台还直接称呼“Stability AI是Stable Diffusion背后的公司”、“它是我们的模型”,根本没有提到Runway等作者。


图片


来自慕尼黑大学、海德堡大学的教授其实也想宣传他们的贡献,奈何大学的新闻办公室当时正在休假。他表示,伊玛德在关键信息中误导了公众:



显然,如果你拥有一家自带庞大新闻部的公司,你就可以根据自己的兴趣重塑历史。



此外,伊玛德在去年的投资者介绍中,还将Midjourney描述为其“生态系统”的一部分,声称Stability AI“共同创造”了该产品并“组织”了其用户社区。


但Midjourney创始人告诉福布斯,伊玛德只是提供了“非常小”的一部分财务捐赠,除此之外与Midjourney没有任何关系。


图片


02、拖欠员工工资、不交税,明目张胆开上“夫妻店”


有多少人惊叹过Stability AI如此一个快速做大做强的公司,居然只有11个初始员工?


其实后来Stability AI快速扩张,员工数量达到了140名,其中不乏曾任英伟达董事的研发副总裁、来自谷歌大脑的负责人,以及Ommer实验室的3位博士生。


但伊玛德被曝对员工很“苛刻”。


有8名前雇员向福布斯称,公司内部一再拖延或未支付他们的工资超过一个月,以及该上交的工资税也没交,导致去年英国税务机构都威胁要没收公司资产了。


就在这期间,伊玛德的老婆作为公司的公关主管(后来成为董事会成员),竟然还从公司账户中挪走了数万英镑


图片


他们的理由是,我们夫妻俩最开始一直在通过贷款帮助Stability AI运营(现在该还了)。


Ps. “欠款”已在2022年底之前已经结清。


另外,伊玛德的老婆还被曝经常在办公室骂员工,甚至把他们骂哭


最骇人的还是今年1月她曾表示要辞去董事会的职位,回归家庭和个人事业。然而5月,在Stability AI的一份组织架构图中,她又被列为“基金会负责人”,处于公司最高层,与丈夫伊玛德相当。


03、学历造假


说回伊玛德,今年他40岁,是一位在英国长大的孟加拉人,2005年本科毕业于牛津大学数学和计算机专业。


然而他对外声称自己拥有牛津大学的硕士学历。


图片


04、工作经历造假


伊玛德毕业后进入金融行业,后加入对冲基金公司Capricorn。


他声称自己因为重新“盘活”这家一度陷入困境的公司而获得殊荣,然而Capricorn联创表示:



他确实是公司的联合首席投资官,然而由于业绩不佳,这只3.3亿美元的基金很快就结束了仅两年的运营。



当初引荐他进入Capricorn的大佬则表示:



伊玛德非常擅长编故事。



基金事业失败之后,伊玛德开启了创业,经历一系列失败点子之后,终于在2019年创立了Stability AI,不过当时这家公司是作为一个AI驱动的数据中心,来帮助全球机构做出有关新冠疫情方面的决策。


后来直到他注意到了Stable Diffusion团队。


05、夸大公司收入


伊玛德去年10月公开声称,Stability AI年收入已超过1000万美元,但知情人士表示,公司销售额没有改善。


06、融资困难,遭遇投资人怀疑


尽管Stability AI收入并不可观,伊玛德最初还是通过各种办法搞到了约1亿美元的融资。


但现在,他遭到了很多投资人的质疑。


许多投资人告诉福布斯,伊玛德后面一直在努力想要以大约40亿美元的估值再融资数亿美元,却在如今这个对初创AI公司相当利好的风口中一直都没有成功。


07、夸大与亚马逊的合作,拖欠数百万美元账单


伊玛德的夸大宣传还不止如此。


他告诉投资人Stability AI正在组装世界上最大的10台超算之一,然后亚马逊以80%的折扣卖给他计算资源服务,因为Stability AI“跟亚马逊具有战略合作伙伴关系”。


然而实际上,只要是任何跟亚马逊有长期租赁合作的客户都能享受这一标准折扣。


另外,据三名前Stability AI员工表示,公司还曾因为拖欠数月账单(高达数百万美元),导致亚马逊威胁说要撤销该公司对其部分GPU的访问权限。


08、和世卫组织等套近乎


伊玛德在几个筹款平台上都宣称经合组织、世界卫生组织和世界银行是Stability AI的合作伙伴,但三者全部予以否认。


09、还有版权官司在身


最后,伊玛德还被指控Stability AI违反版权法训练模型,遭到了盖蒂图片社和艺术家的集体诉讼。


消息一出,网友无不震惊又失望:原来当今最牛AIGC公司之一的创始人是如此阴暗之人。


图片


有人称,伊玛德为了能让自己成为亿万富翁不择手段。


图片


AI技术本身也躺枪:



果然和“它的主人”一样,取得的成果是从别人(人类艺术家)那里剽窃而来的。



图片


一时之间,舆论哗然。


事实究竟如何呢?


本人火速回应:大量不实指控


面对这一系列近乎“不留情面”的指控,伊玛德24小时内火速给出了回应。


在博客上,他po出了自己给Stability AI团队针对福布斯报道写的长信,算是给团队作出一个解释,同时也对外界的种种传言进行澄清。


1、学历造假


伊玛德在长信中确认了自己没有硕士学位的说法,甚至连学士学位也没有。不过他表示,这仅仅是因为他没有参加毕业典礼的缘故:



我已经支付了60英镑,并将在下个月通过邮寄方式收取学位证书。



2、关于剽窃Stable Diffusion成果


这份长信称,Stability AI已多次公开声明是Stable Diffusion第一个版本的合作者,其他合作者来自慕尼黑大学(LMU)和另一家AI创业公司Runway。


图片


伊玛德还表示,目前这篇论文中5位作者的3位,都在Stability AI工作,其中两位是共同第一作者:


图片


不过,这份声明中好像忽略了海德堡大学?原论文是这样的:


图片


3、关于拖欠员工工资


伊玛德承认之前存在延期发工资的情况,但后面都补上了,而且“向受影响的员工支付了额外的工资”。


同时自2021年底以来,在“正常运营中”没有任何工资支付问题。


4、与Midjourney的关系


伊玛德表示,Midjourney一直是Stability AI支持的一个“独立组织”,后者帮助它做了不少扩展,包括引入审美评分和偏好学习等,并为它提供战略和模式支持。


至于究竟捐了多少钱,伊玛德没有澄清也没有回应。


5、关于夸大亚马逊的战略合作


关于夸大亚马逊计算资源的合作,伊玛德没有提到价格,但表示AWS专门为Stability AI构建了一个罕见的专用计算集群,包含4000张A100,“独一无二”。


这里面一些支付上的问题(主要是付款时间问题),都已经在集群搭建好的当月(2022年8月)被解决。


除此之外,伊玛德还回应了自己和公司的一些信息。


关于他自己,他主要澄清了之前的工作。例如,他曾经在对冲基金工作,但后来这家基金倒闭了,原因并非在他。毕竟他就职期间,这家公司不仅拿到了奖项,还挽回了业绩;此外,他还与联合国有不少合作,并参与了自闭症相关研究等。


关于Stability AI,他则强调了目前“公司经营良好”,不仅努力加强和改进人力资源流程,而且2022年底前已结清和Zehra公司欠款。至于投资的情况,目前公司还没有向签署了MNDA的投资人开放Data Room(数据室)。


最后,伊玛德还刻意强调“公司所做的一切都会受到严格审查”:


图片


在发布这篇文章后,他接受了VentureBeat的采访,表示很难过:



我们的团队和福布斯澄清了一些事实,但他们显然选择忽视其中许多问题的真相。



对于这一回应,部分人并不买帐。因为关于文章中一些很关键的点,伊玛德并没有澄清:


图片


例如前员工Eric Hallahan就指出了其中一个模糊的地方:



正式员工工资可能是按时支付的,但这可不包括非正式员工和外包员工们的工资!



图片


但也有网友认为,这篇新闻报道充斥着恶意,伊玛德做过的好事不应该被忽略:


图片


不过在创始人被曝丑闻这一事件之外,Stability AI自身也面临严重问题——更早时候,它就被曝出疯狂烧钱、管理混乱等问题。


“尚未有明确盈利途径”


虽然去年10月拿到了1亿美元融资,不过Stability AI走得一直不算顺利。


前不久,记者Reed Albergotti就在Semafor上爆料了Stability AI目前面临的一些问题。


一方面,Stability AI目前缺少像微软之于OpenAI这样“财力雄厚”的合作伙伴,其AI模型训练的昂贵成本究竟如何处理是一个未知数。


前段时间Stability AI宣称要推出开源类ChatGPT产品,然而推出后效果并不好。


另一方面,伊玛德设想的两种商业模式目前都不是很明确。


一种是搞咨询、提供AI工具的模式,另一种则是和相关基金合作的模式。


目前后一种模式已经被放弃,前一种模式的运转也并不顺利,因为员工分散在各地办公,而这实际上减缓了产品开发流程、增加了成本。据Semafor称,一些员工已经对于伊玛德的领导风格失去信心,认为管理非常混乱。


显然,Stability AI的商业模式仍旧不确定。


目前它需要向投资人证明两点,一点是它的技术足以开发顶尖AI模型;另一点则是它是否具有“赚钱”的能力——


Stability AI去年融到的1亿美元,如今已经烧掉了大部头。


而就在更早的时候,据《商业内幕》介绍,伊玛德还曾经警告Stability AI员工这一行业竞争的激烈性:



你们都会在2023年死去。



图片


但也不止是盈利问题,AI绘画的不确定性,让这个领域也面临版权等更长期的问题。


有网友表示,Stability AI之事只是导火索:


图片



现在看来,这家公司是起诉的最合适选项。


但Midjourney和DeviantArt最好也小心点,因为他们很可能就是下一个。



参考链接:

[1]http://www.forbes.com/sites/kenri…

[2]twitter.com/EMostaque/s…

[3]emad.posthaven.com/on-setting-…

[4]twitter.com/EricHallaha…

[5]venturebeat.com/ai/stabilit…

[6]http://www.businessinsider.com/stability-a…


— 完<

作者:量子位
来源:juejin.cn/post/7244466524559376445
/strong> —

收起阅读 »

优雅的时钟翻页效果,让你的网页时钟与众不同!

web
你有没有想过给你的网页时钟添加翻页效果,让它更引人注目,更酷炫吗?如果是的话,你来对地方了! 这篇文章将教你如何通过简单的步骤,为你的网页时钟添加翻页效果。 无论你是 web 开发初学者或是有一定经验的开发者,这篇文章都将为你提供有用的实现技巧和原理解释。 ...
继续阅读 »

你有没有想过给你的网页时钟添加翻页效果,让它更引人注目,更酷炫吗?如果是的话,你来对地方了!


13.gif


这篇文章将教你如何通过简单的步骤,为你的网页时钟添加翻页效果。


无论你是 web 开发初学者或是有一定经验的开发者,这篇文章都将为你提供有用的实现技巧和原理解释。


来,跟着子辰一起开始吧!


思考


01.gif


通过上图可以看到,由 3 翻到 4,其实是 3 的上半部分,与 4 的下半部分,一起翻下来的。


为了便于理解将翻页的过程通过侧面角度展示,解析成下图中所示的样子。


02.png


我们先来看一下 3 是如何呈现的。


03.png


那么由 3 的呈现我们可以知道,4 其实一开始是对折的,然后 4B 翻下来后形成完整的 4。


04.png


那么现在我们将 3 与 4 结合在一起看看。


05.png


由上可知,下一个数字都是对折的,在呈现时,都是有由前一个数字的上半部与下一个数字的上半部,翻转得到新的数字。


既然数字翻页的秘密我们知道了,接下来就是实现了。


06.png


实现翻页


容器背景


首先我们要实现一个承载数字的容器,中间使用伪元素做分割线,这就是时钟的底盘。


<div class="card-container"></div>

.card-container {
background: #2c292c;
width: 200px;
height: 200px;
position: relative;
perspective: 500px;
}

.card-container::before {
z-index: 99;
content: " ";
position: absolute;
left: 0;
top: 50%;
background: #120f12;
width: 100%;
height: 6px;
margin-top: -3px;
}

07.png


下层数字上半部分


接下来我们先来实现背后的下一层的 4,因为 4 分为上下两部分,我们先实现上半部分。


<div class="card-container">
<div class="card1 card-item">4</div>
</div>

/* 因为所有的数字都有公共部分,我们提取出来 */
.card-item {
position: absolute;
width: 100%;
/* 因为每个卡片只有半个数字,所以高度只有百分之50 */
height: 50%;
left: 0;
top: 0;
overflow: hidden;
background: #2c292c;
}

.card1 {
line-height: 200px
}

08.png


下层数字下半部分


<div class="card-container">
<div class="card1 card-item">4</div>
<div class="card2 card-item">4</div>
</div>

.card2 {
top: 50%;
}

首先我们写出来是这样的。


09.png


但是我们要求的是 4 的下半部分向上对折覆盖在 4 的上半部分之上。
所以我们看到的应该是 4 下半部分的背面,通过中线向上对折,并且因为是背面,所以我们不应该看到他。


.card2 {
z-index: 2;
top: 50%;
line-height: 0;
/* 变换原点为上边的中部 */
transform-origin: center top;
/* 对折 180 度 */
transform: rotateX(180deg);
/* 通过这个属性让元素的背面隐藏 */
backface-visibility: hidden;
}

08.png


现在看上去好像和只有上半部分没什么区别,所以我们给他加个 hover 加个过渡效果让它翻转下来看看。


11.gif


这样就看出来了。


上层数字


上层数字的原理就比较简单了,我们参考下层数字的逻辑写。


<div class="card-container">
<div class="card1 card-item">4</div>
<div class="card2 card-item">4</div>
<div class="card3 card-item">3</div>
<div class="card4 card-item">3</div>
</div>

.card3 {
line-height: 200px;
transform-origin: center bottom;
backface-visibility: hidden;
z-index: 2
}

.card4 {
top: 50%;
line-height: 0
}

12.png


现在就是这样效果,同样的,我们给它加个 hover,3 的翻页过渡要与 4 的保持同步。


01-13.gif


现在我们就实现了单个数字的过渡。


翻页时钟


时钟无非就是三个翻页效果加上自动翻页,我们去实现一下。


<!-- data-number 用于存储上一次的时间,来和即将改变的时间对比 -->
<div class="card-container flip" id="card-h" data-number="00">
<div class="card1 card-item">00</div>
<div class="card2 card-item">00</div>
<div class="card3 card-item">00</div>
<div class="card4 card-item">00</div>
</div>
<div class="card-container flip" id="card-m" data-number="00">
<div class="card1 card-item">00</div>
<div class="card2 card-item">00</div>
<div class="card3 card-item">00</div>
<div class="card4 card-item">00</div>
</div>
<div class="card-container flip" id="card-s" data-number="00">
<div class="card1 card-item">00</div>
<div class="card2 card-item">00</div>
<div class="card3 card-item">00</div>
<div class="card4 card-item">00</div>
</div>

/* etc... */

.flip .card2 {
transform: rotateX(0);
}

.flip .card3 {
transform: rotateX(-180deg);
}

// 获取 dom
const hour = document.getElementById("card-h");
const minute = document.getElementById("card-m");
const second = document.getElementById("card-s");

function setHTML(dom, time) {
// 下一次要显示的时间
const nextValue = time.toString().padStart(2, "0");
// 上一次的时间
const curValue = dom.dataset.number;
// 如果下次要显示的时间和上一次的一样,直接退出。比如在同一分钟或同一小时内。
if (nextValue === curValue) {
return;
}
// 重置时分秒的 dom
dom.innerHTML = `<div class="card1 card-item">${nextValue}</div>
<div class="card2 card-item">${nextValue}</div>
<div class="card3 card-item">${curValue}</div>
<div class="card4 card-item">${curValue}</div>`
;
// 移除 flip 属性再次添加以触发过渡再次执行
dom.classList.remove("flip");
dom.clientHeight;
dom.classList.add("flip");
// 时间不同时重置 dataset.number
dom.dataset.number = nextValue;
}

// 获取时分秒并分别设置
function setNumbers() {
var now = new Date();
var h = now.getHours();
var m = now.getMinutes();
var s = now.getSeconds();
setHTML(hour, h);
setHTML(minute, m);
setHTML(second, s);
}

setNumbers();

setInterval(setNumbers, 1000);

13.gif


至此我们就完成了时钟翻页的效果了,你学会了吗?


总结


子辰详细介绍了如何通过简单的步骤,为网页时钟添加翻页效果。


文章从思考开始,通过分析数字翻页的秘密来解决问题。


接着,详细讲解了实现翻页的具体方法和原理,并给出了相应的代码实现。


最后,通过组合多个翻页效果,实现了完整的时钟翻页效果。


如果你认真读完了这篇文章,那么以下这几点都是你所学到的:



  1. 提高对 CSS3 属性的理解和掌握,例如 perspective、transform、backface-visibility 等。

  2. 掌握实现元素翻转动画的基本方法和技巧,包括旋转轴心、变换原点、背面可见性等。

  3. 了解如何通过数据属性(data-*)存储和比较数据,避免不必要的重复操作。

  4. 学会如何通过 JavaScript 操作 DOM 元素,实现网页中的动态效果。


其实实现的效果并不难,代码也都是基础的代码,难的是思考翻页的过程,好的思维方法,才是前端进阶的基础,更是关键。


多看好的文章,多看好的思考过程,都是

作者:子辰Web草庐
来源:juejin.cn/post/7244351125448458296
提升思维的一种方式。

收起阅读 »

移动端网页开发有感

web
前段时间参与了一个移动端页面开发的需求,开发时明显感觉与 pc 端开发相比,移动端页面的限制会更多😭 需求结束后思考了一下究竟是哪些方面感觉不舒服?有没有对应的解决方法?以便下次开发移动端页面时能提升开发效率和体验。 移动端网页容易出现布局问题 🤦‍♂️ 因为...
继续阅读 »

前段时间参与了一个移动端页面开发的需求,开发时明显感觉与 pc 端开发相比,移动端页面的限制会更多😭


需求结束后思考了一下究竟是哪些方面感觉不舒服?有没有对应的解决方法?以便下次开发移动端页面时能提升开发效率和体验。


移动端网页容易出现布局问题 🤦‍♂️


因为页面空间小,容易出现元素重叠、挤压、换行等样式问题,怎么在不同尺寸的设备上合适地展示页面?


解决办法:




  1. 使用 <meta name="viewport"> 标签 ✨


    这个标签想必做过移动端页面开发的同学都不陌生吧?它就是专门为移动端展示优化而增加的标签。


    先来看看它的作用是什么?


    它可以设置移动端页面宽度、缩放比例、是否允许用户缩放网页等


    它的基本属性有哪些?


    属性名含义取值范围
    width控制视口大小具体数值或 'device-width'
    initial-scale页面初始缩放比例0.0 ~ 10
    minimum-scale控制页面允许被缩小的倍数0.0 ~ 10
    maximum-scale控制页面允许被大的倍数0.0 ~ 10
    user-scalable控制是否允许放大和缩小页面yes 或 no

    需要注意的是在移动设备上默认页面宽度为 980px:




Luban_16853778753692bc31648-c60e-4ebc-845c-3bac272f7393.jpg


假如我们希望页面的视口宽度与设备宽度相同,同时初始缩放比例为 1,可以在 <head> 里增加这个的 meta 标签
<meta name="viewport" content="width=device-width,initial-scale=1">


Luban_1685377875383f6d72d0e-c360-43aa-807a-739399af01fe.jpg


这样页面的展示就符合我们的预期了




  1. 使用 vw、vh 视口单位
    vw、vh 都是视口单位


    简而言之:100vw = 视口宽度,100vh = 视口高度 (够简单吧 😅




  2. 使用 rem 相对单位




rem 在移动端开发中很重要,因为不同移动设备有着不同的尺寸和分辨率,如果我们用固定的 px 作为元素大小单位会发现不够适用


而 rem 是相对单位大小,它相对的是根元素 html 的字体大小,比如:


<html>
<head>
<style>
html {
font-size: 14px; // 这里将 html 的字体大小设为 14px
}
.content {
font-size: 2rem; // 在页面展示时将会被计算成 14 * 2 = 28px
}
</style>
</head>
<body>
<div class="content">rem</div>
</body>
</html>

所以我们可以根据设备大小动态设置根元素大小,从而成比例地更改页面里其它元素的大小


    const BASE_PAGE_WIDTH = 370
const BASE_SIZE = 16

function setRem() {
const scale = document.documentElement.clientWidth / BASE_PAGE_WIDTH
document.documentElement.style.fontSize = `${scale * BASE_SIZE}px`
}

setRem()

真机调试比较麻烦 😌


尽管可以在电脑浏览器上模拟移动设备的展示情况,但并不够真实,在开发和测试阶段仍然需要在真机上调试;


同时可能我们的测试环境需要连接 vpn 或添加特定的请求头才能访问到,所以在手机上需要通过电脑代理才能访问测试环境,从而进行开发测试;


最后,即使能在手机上访问到本地开发和测试环境的页面,你会发现当页面报错的时候你压根就看不到 log 日志输出或网络请求,这种干看着页面有问题却不能进一步排查的感觉就很难受 😖


还好有一些工具可以帮我们化解这些难题 🥳


解决办法:



  1. 首先我们可以使用 whistlecharles 来连接电脑代理,这里以 whistle 为例:

    • 电脑安装并启动 whistle

    • 手机和电脑在同一局域网下

    • 手机设置网络代理

    • 手机安装 https 证书





具体操作可以访问官方文档的详细步骤哈



Done!现在手机会通过电脑作为代理来访问网络,可以直接访问开发地址或测试环境地址啦~



  1. 然后我们可以使用 VConsole 在移动设备上进行调试,它相当于在页面上加了一个控制台,从而让我们可以查看页面上的日志输出、网络请求等,它的用法也很简单:


// 使用 npm 安装
npm install vconsole

import VConsole from 'vconsole'

new VConsole()

// 使用 CDN 安装
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
// VConsole will be exported to `window.VConsole` by default.
const vConsole = new window.VConsole();
</script>


然后你会发现页面右下角多了一个 vConsole 的按钮:


Screenshot_2023-06-12-23-46-33-090_mark.via.jpg


我们可以测试一下打印日志:


    document.addEventListener('click', handlePageClick)

function handlePageClick() {
console.log('Daniel Yang')
}

在点击页面后再点击 vConsole 按钮会发现在展开的面板里 log 一栏已经显示出 log 的内容:


Screenshot_2023-06-12-23-48-35-778_mark.via.jpg


同时我们也可以在 VConsole 面板上查看页面元素结构、客户端存储、网络请求等,总之非常的 nice 🤗


以上就是自己对一次移动端网页开发过程中遇到的问题小小的总结,如果你在移动端开发中有遇到其它印象深刻的坑,欢迎一起留言讨论哦


006APoFYly8hesgm67dwpj30hs0hbmxv.jpeg

作者:卡布奇诺有点苦
来源:juejin.cn/post/7243757233666195515
th="50%"/>

收起阅读 »

百分百空手接大锅

web
背景 愉快的双休周末刚过完,早上来忽然被运营通知线上业务挂了,用户无法下单。卧槽,赶紧进入debug模式,一查原来是服务端返回的数据有问题,赶紧问了服务端,大佬回复说是业务部门配置套餐错误。好在主责不在我们,不过赶紧写了复盘文档,主动找自己的责任,扛起这口大锅...
继续阅读 »

背景


愉快的双休周末刚过完,早上来忽然被运营通知线上业务挂了,用户无法下单。卧槽,赶紧进入debug模式,一查原来是服务端返回的数据有问题,赶紧问了服务端,大佬回复说是业务部门配置套餐错误。好在主责不在我们,不过赶紧写了复盘文档,主动找自己的责任,扛起这口大锅,都怪我们前端,没有做好前端监控,导致线上问题持续两天才发现。原本以为运营会把推辞一下说不,锅是她们的,可惜人家不太懂人情世故,这锅就扣在了技术部头上。虽然但是,我还是静下心来把前端异常监控搞了出来,下次一定不要主动接锅,希望看到本文的朋友们也不要随便心软接锅^_^


监控


因为之前基于sentry做了埋点处理,基础已经打好,支持全自动埋点、手动埋点和数据上报。相关的原理可以参考之前的一篇文章如何从0-1构建数据平台(2)- 前端埋点。本次监控的数据上报也基于sentry.js。那么如何设计整个流程呢。具体步骤如下:




  1. 监控数据分类




  2. 监控数据定义




  3. 监控数据收集




  4. 监控数据上报




  5. 监控数据输出




  6. 监控数据预警




数据分类


我们主要是前端的数据错误,一般的异常大类分为逻辑异常和代码异常。基于我们的项目,由于涉及营收,我们就将逻辑错误专注于支付异常,其他的代码导致的错误分为一大类。然后再将两大异常进行细分,如下:




  1. 支付异常


    1.1 支付成功


    1.2 支付失败




  2. 代码异常


    2.1 bindexception


     2.1.1  js_error

    2.1.2 img_error

    2.1.3 audio_error

    2.1.4 script_error

    2.1.5 video_error



  3. unhandleRejection


    3.1 promise_unhandledrejection_error


    3.2 ajax_error




  4. vueException




  5. peformanceInfo




数据定义


基于sentry的上报数据,一般都包括事件与属性。在此我们定义支付异常事件为“page_h5_pay_monitor”,定义代码异常事件为“page_monitor”。然后支付异常的属性大概为:



pay_time,

pay_orderid,

pay_result,

pay_amount,

pay_type,

pay_use_coupon,

pay_use_coupon_id,

pay_use_coupon_name,

pay_use_discount_amount,

pay_fail_reason,

pay_platment


代码异常不同的错误类型可能属性会有所区别:



// js_error

monitor_type,

monitor_message,

monitor_lineno,

monitor_colno,

monitor_error,

monitor_stack,

monitor_url

// src_error

monitor_type,

monitor_target_src,

monitor_url

// promise_error

monitor_type,

monitor_message,

monitor_stack,

monitor_url

// ajax_error

monitor_type,

monitor_ajax_method,

monitor_ajax_data,

monitor_ajax_params,

monitor_ajax_url,

monitor_ajax_headers,

monitor_url,

monitor_message,

monitor_ajax_code

// vue_error

monitor_type,

monitor_message,

monitor_stack,

monitor_hook,

monitor_url

// peformanceInfo 为数据添加 loading_time 属性,该属性通过entryTypes获取

try {

const observer = new PerformanceObserver((list) => {

for (const entry of list.getEntries()) {

if (entry.entryType === 'paint') {

sa.store.set('loading_time', entry.startTime)

}
}

})

observer.observe({ entryTypes: ['paint'] })

} catch (err) {

console.log(err)

}


数据收集


数据收集通过事件绑定进行收集,具体绑定如下:


import {

BindErrorReporter,

VueErrorReporter,

UnhandledRejectionReporter

} from './report'

const Vue = require('vue')


// binderror绑定

const MonitorBinderror = () => {

window.addEventListener(

'error',

function(error) {

BindErrorReporter(error)

},true )

}

// unhandleRejection绑定 这里由于使用了axios,因此ajax_error也属于promise_error

const MonitorUnhandledRejection = () => {

window.addEventListener('unhandledrejection', function(error) {

if (error && error.reason) {

const { message, code, stack, isAxios, config } = error.reason

if (isAxios && config) {

// console.log(config)

const { data, params, headers, url, method } = config

UnhandledRejectionReporter({

isAjax: true,

data: JSON.stringify(data),

params: JSON.stringify(params),

headers: JSON.stringify(headers),

url,

method,

message: message || error.message,

code

})

} else {

UnhandledRejectionReporter({

isAjax: false,

message,

stack

})

}

}

})

}

// vueException绑定

const MonitorVueError = () => {

Vue.config.errorHandler = function(error, vm, info) {

const { message, stack } = error

VueErrorReporter({

message,

stack,

vuehook: info

})

}

}

// 输出绑定方法

export const MonitorException = () => {

try {

MonitorBinderror()

MonitorUnhandledRejection()

MonitorVueError()

} catch (error) {

console.log('monitor exception init error', error)

}

}


数据上报


数据上报都是基于sentry进行上报,具体如下:



/*

* 异常监控库 基于sentry jssdk

* 监控类别:

* 1、window onerror 监控未定义属性使用 js资源加载失败问题

* 2、window addListener error 监控未定义属性使用 图片资源加载失败问题

* 3、unhandledrejection 监听promise对象未catch的错误

* 4、vue.errorHandler 监听vue脚本错误

* 5、自定义错误 包括接口错误 或其他diy错误

* 上报事件: page_monitor

*/


// 错误类别常量

const ERROR_TYPE = {

JS_ERROR: 'js_error',

IMG_ERROR: 'img_error',

AUDIO_ERROR: 'audio_error',

SCRIPT_ERROR: 'script_error',

VIDEO_ERROR: 'video_error',

VUE_ERROR: 'vue_error',

PROMISE_ERROR: 'promise_unhandledrejection_error',

AJAX_ERROR: 'ajax_error'

}

const MONITOR_NAME = 'page_monitor'

const PAY_MONITOR_NAME = 'page_h5_pay_monitor'

const MEMBER_PAY_MONITOR_NAME = 'page_member_pay_monitor'

export const BindErrorReporter = function(error) {

if (error) {

if (error.error) {

const { colno, lineno } = error

const { message, stack } = error.error

// 过滤

// 客户端会有调用calljs的场景 可能有一些未知的calljs

if (message && message.toLowerCase().indexOf('calljs') !== -1) {

return

}

sa.track(MONITOR_NAME, {

//属性

})

} else if (error.target) {

const type = error.target.nodeName.toLowerCase()

const monitorType = type + '_error'

const src = error.target.src

sa.track(MONITOR_NAME, {

//属性

})

}

}

}

export const UnhandledRejectionReporter = function({

isAjax = false,

method,

data,

params,

url,

headers,

message,

stack,

code

}
) {

if (!isAjax) {

// 过滤一些特殊的场景

// 1、自动播放触发问题

if (message && message.toLowerCase().indexOf('user gesture') !== -1) {

return

}

sa.track(MONITOR_NAME, {

//属性

})

} else {

sa.track(MONITOR_NAME, {

//属性

})

}

}

export const VueErrorReporter = function({ message, stack, vuehook }) {

sa.track(MONITOR_NAME, {

//属性

})

}

export const H5PayErrorReport = ({

isSuccess = true,

amount = 0,

type = -1,

couponId = -1,

couponName = '',

discountAmount = 0,

reason = '',

orderid = 0,

}
) => {

// 事件名:page_member_pay_monitor

sa.track(PAY_MONITOR_NAME, {

//属性

})

}


以上,通过sentry的sa.track进行上报,具体不作展开


输出与预警


数据被上报到大数据平台,被存储到hdfs中,然后我们直接做定时任务读取hdfs进行一定的过滤通过钉钉webhook输出到钉钉群,另外如果有需要做数据备份可以通过hdfs到数据仓库再到kylin进行存储。


总结


数据监控对于大的,特别是涉及营收的平台是必要的,我们在设计项目的时候一定要考虑到,最好能说服服务端,让他们服务端也提供相应的代码监控。ngnix层或者云端最好也来一层。严重的异常可以直接给你打电话,目前云平台都有相应支持。这样有异常及时发现,锅嘛,接到手里

作者:CodePlayer
来源:juejin.cn/post/7244363578429030459
就可以精准扔出去了。

收起阅读 »

Redis性能瓶颈揭秘:如何优化大key问题?

1. 什么是Redis大key问题 Redis大key问题指的是某个key对应的value值所占的内存空间比较大,导致Redis的性能下降、内存不足、数据不均衡以及主从同步延迟等问题。 到底多大的数据量才算是大key? 没有固定的判别标准,通常认为字符串类型的...
继续阅读 »

1. 什么是Redis大key问题


Redis大key问题指的是某个key对应的value值所占的内存空间比较大,导致Redis的性能下降、内存不足、数据不均衡以及主从同步延迟等问题。


到底多大的数据量才算是大key?


没有固定的判别标准,通常认为字符串类型的key对应的value值占用空间大于1M,或者集合类型的k元素数量超过1万个,就算是大key。


Redis大key问题的定义及评判准则并非一成不变,而应根据Redis的实际运用以及业务需求来综合评估。例如,在高并发且低延迟的场景中,仅10kb可能就已构成大key;然而在低并发、高容量的环境下,大key的界限可能在100kb。因此,在设计与运用Redis时,要依据业务需求与性能指标来确立合理的大key阈值。


2. 大key带来的影响



  1. 内存占用过高。大Key占用过多的内存空间,可能导致可用内存不足,从而触发内存淘汰策略。在极端情况下,可能导致内存耗尽,Redis实例崩溃,影响系统的稳定性。

  2. 性能下降。大Key会占用大量内存空间,导致内存碎片增加,进而影响Redis的性能。对于大Key的操作,如读取、写入、删除等,都会消耗更多的CPU时间和内存资源,进一步降低系统性能。

  3. 阻塞其他操作。某些对大Key的操作可能会导致Redis实例阻塞。例如,使用DEL命令删除一个大Key时,可能会导致Redis实例在一段时间内无法响应其他客户端请求,从而影响系统的响应时间和吞吐量。

  4. 网络拥塞。每次获取大key产生的网络流量较大,可能造成机器或局域网的带宽被打满,同时波及其他服务。例如:一个大key占用空间是1MB,每秒访问1000次,就有1000MB的流量。

  5. 主从同步延迟。当Redis实例配置了主从同步时,大Key可能导致主从同步延迟。由于大Key占用较多内存,同步过程中需要传输大量数据,这会导致主从之间的网络传输延迟增加,进而影响数据一致性。

  6. 数据倾斜。在Redis集群模式中,某个数据分片的内存使用率远超其他数据分片,无法使数据分片的内存资源达到均衡。另外也可能造成Redis内存达到maxmemory参数定义的上限导致重要的key被逐出,甚至引发内存溢出。


3. 大key产生的原因



  1. 业务设计不合理。这是最常见的原因,不应该把大量数据存储在一个key中,而应该分散到多个key。例如:把全国数据按照省行政区拆分成34个key,或者按照城市拆分成300个key,可以进一步降低产生大key的概率。

  2. 没有预见value的动态增长问题。如果一直添加value数据,没有删除机制、过期机制或者限制数量,迟早出现大key。例如:微博明星的粉丝列表、热门评论等。

  3. 过期时间设置不当。如果没有给某个key设置过期时间,或者过期时间设置较长。随着时间推移,value数量快速累积,最终形成大key。

  4. 程序bug。某些异常情况导致某些key的生命周期超出预期,或者value数量异常增长 ,也会产生大key。


4. 怎样排查大key


4.1 SCAN命令


通过使用Redis的SCAN命令,我们可以逐步遍历数据库中的所有Key。结合其他命令(如STRLEN、LLEN、SCARD、HLEN等),我们可以识别出大Key。SCAN命令的优势在于它可以在不阻塞Redis实例的情况下进行遍历。


4.2 bigkeys参数


使用redis-cli命令客户端,连接Redis服务的时候,加上 —bigkeys 参数,可以扫描每种数据类型数量最大的key。



redis-cli -h 127.0.0.1 -p 6379 —bigkeys



4.3 Redis RDB Tools工具


使用开源工具Redis RDB Tools,分析RDB文件,扫描出Redis大key。


例如:输出占用内存大于1kb,排名前3的keys。



rdb —commond memory —bytes 1024 —largest 3 dump.rbd



5. 怎么解决大key



  1. 拆分成多个小key。这是最容易想到的办法,降低单key的大小,读取可以用mget批量读取。

  2. 数据压缩。使用String类型的时候,使用压缩算法减少value大小。或者是使用Hash类型存储,因为Hash类型底层使用了压缩列表数据结构。

  3. 设置合理的过期时间。为每个key设置过期时间,并设置合理的过期时间,以便在数据失效后自动清理,避免长时间累积的大Key问题。

  4. 启用内存淘汰策略。启用Redis的内存淘汰策略,例如LRU(Least Recently Used,最近最少使用),以便在内存不足时自动淘汰最近最少使用的数据,防止大Key长时间占用内存。

  5. 数据分片。例如使用Redis Cluster将数据分散到多个Redis实例,以减轻单个实例的负担,降低大Key问题的风险。

  6. 删除大key。使用UNLINK命令删除大key,UNLINK命令是DEL命令的异步版本,它可以在后台删除Key,避免阻塞Redis实例。


6. 总结


大Key问题是Redis中常见的问题之一,可能导致性能下降、内存占用过高、阻塞其他操作以及主从同步延迟等问题。本文详细介绍了大Key产生的原因、影响、检测方法和解决方案。通过优化数据结构设计、设定合理的数据过期策略、优化系统架构和配置,以及渐进式删除大Key等方法,我们可以有效地解决和预防大Key问题,从而提高Redis系统的稳定性和性能。



我是「一灯架构」,如果本文对你有帮助,欢迎各位小伙伴点赞、评论和关注,感谢各位老铁,我们下期见


作者:一灯架构
链接:https://juejin.cn/post/7222825885203218491
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

微信扫码登录,做了又好像没做

这两天,和微信扫码登录这个功能杠上了。 为什么??? 大家都知道,个人网站我做了很多,而我的每个网站都有登录/注册相关的功能。无一例外我的网站都是通过账号和密码这种形式进行登录/注册,而前两天有人和我说这样太麻烦了,搞个扫码登录多方便啊! 你还别说,我以前是真...
继续阅读 »

这两天,和微信扫码登录这个功能杠上了。


为什么???


大家都知道,个人网站我做了很多,而我的每个网站都有登录/注册相关的功能。无一例外我的网站都是通过账号和密码这种形式进行登录/注册,而前两天有人和我说这样太麻烦了,搞个扫码登录多方便啊!


你还别说,我以前是真考虑过,但那会不是赶工期嘛,就没去过多的了解扫码登录这回事。而这次再次被提及,那就顺势这两天给他办了吧!


G8JkY.gif


考虑到现在的用户都喜欢使用微信,而微信扫码登录这个功能也是非常普遍的,所以一拍即合,我也整个微信扫码登录。


经过周末两天的摸索,现在个人开发者要给自己的网站加上微信扫码登录这个功能有三个途径:



  1. 利用微信扫码,关注公众号实现微信扫码登录

  2. 利用微信开放平台提供的网站应用提供的获取用户信息接口,实现扫码登录

  3. 利用小程序做中间过渡实现的微信扫码登录


然而,这里我要给你们泼一盆凉水了,这三个方案,我都试过,对于个人而言,无法实现,或者说无法满足我对扫码登录功能的期望



期望:微信扫码,用户授权,获取用户头像及昵称等信息



先来说说公众号,为啥不行。


首先,我有公众号,但是我的公众号仅仅是个人,而不是企业认证的公众号,这就表明我只能发信息,而没有获取和支付等这种高级的功能。


就比如获取用户信息:


Snipaste_2023-06-11_19-04-17.jpg


可能有人会说去认证一个不就行了,我只能说,你们可以去试试(认证费几千块 + 其他材料)。


再来说说微信开放平台,为啥不行。


其实这个平台的对接功能写的非常通俗易懂,而且也相对简单,但是,和上面的问题也是一样,个人想要获取用户相关的功能,也要认证。


那我再看一下他的认证条件吧:


Snipaste_2023-06-11_19-04-117.jpg


这个认证条件倒是简单明了,就 300 块钱,你认证了就给你获取用户的功能权限。


虽然 300 也不是什么大数目,但是我一想我做的那几个项目用户都还没几个,就把 300 花出去,不太划算,所以这个方向也 pass。


那就来看看最后的小程序方案吧!


查看了小程序的接口文档,发现还真有获取用户信息的 wx 接口,文档如下:


Snipaste_2023-06-11_19-47-46.jpg


有了这个接口就好办了,而且个人是可以开发小程序且不需要任何认证流程的,有微信号就 ok。这不就是我想要的方案嘛,有手就行了,扫码登录。


此时我还没意识到,这个有手就行和我想象的不太一样。


敲定好方案之后,我就在脑子里构思如何通借助小程序来实现微信扫码登录,大致思考了一会,我设计了如下登录方案:


Snipaste_2023-06-11_20-06-39.jpg


图中的主要步骤就两个:



  1. 获取小程序二维码,给用户扫码

  2. 微信扫码,获取用户信息,进行后续登录流程


之后通过编码,居然发现最重要的两个信息(头像、昵称)拿不到,我也是醉了。而我去翻看微信的官方文档,原来是腾讯把 wx.getUserProfile 接口给下线了,这给我整无语了。


Snipaste_2023-06-11_21-20-30.jpg


咋办?


小程序代码我都写好上线了,后端的相关接口我也写好了,就差网站页面联调了。你说这功能是联调,还是不联调呢!


算了,先这样吧!功能先保留,等以后要做新的项目了,再把这个功能放上去。现在整个的流程问题比较大的就是获取不到用户的头像和昵称,但是,扫码登录这个基本流程是已经走通的。等新项目对接这个功能的时候,再让用户通过网站提供的功能再来完善用户头像和昵称等信息吧!


Snipaste_2023-06-11_21-29-44.jpg


吐槽完了!!!


最后,感谢各位看官看到这里,如果大家有啥好的微信扫码登录方案,欢迎评论区留言。


作者:J3code
链接:https://juejin.cn/post/7243413799347601464
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android-策略模式与状态模式:形似而行不似

引言: 在软件开发中,我们经常面临着需求的变化和复杂的业务逻辑。为了解决这些问题,设计模式应运而生。其中,策略模式和状态模式是两种常用的设计模式。本文将带你走进它们的世界,一起探索它们的魅力和应用场景。 一、策略模式 在生活中,我们经常会遇到需要根据不同情况采...
继续阅读 »

引言: 在软件开发中,我们经常面临着需求的变化和复杂的业务逻辑。为了解决这些问题,设计模式应运而生。其中,策略模式和状态模式是两种常用的设计模式。本文将带你走进它们的世界,一起探索它们的魅力和应用场景。


一、策略模式


在生活中,我们经常会遇到需要根据不同情况采取不同策略的情况。软件开发也存在类似的情况。策略模式就是为了解决这类问题而生的。它允许在运行时根据需要选择算法的行为。


应用场景: 想象一下,你是一位游戏开发者,你的游戏中有多种角色,每个角色都有不同的攻击方式。使用策略模式,你可以为每个角色创建一个独立的攻击策略,然后在运行时根据角色的选择来决定使用哪种策略。这样,你就能灵活地扩展和切换角色的攻击方式,而无需修改大量的代码。


代码示例: 让我们以一个游戏的例子来说明策略模式的使用。假设你正在开发一个角色扮演游戏,其中有两个角色:战士(Warrior)和法师(Mage)。每个角色都有自己的攻击方式。首先,我们需要定义一个策略接口,表示不同的攻击策略:

interface AttackStrategy {
fun attack()
}

接下来,我们创建两个具体的策略类,分别代表战士和法师的攻击方式:

class WarriorAttackStrategy : AttackStrategy {
override fun attack() {
println("战士使用剑攻击")
}
}

class MageAttackStrategy : AttackStrategy {
override fun attack() {
println("法师使用魔法攻击")
}
}

最后,我们创建一个角色类,其中包含一个攻击策略的引用:

class Character(private val attackStrategy: AttackStrategy) {
fun attack() {
attackStrategy.attack()
}
}

现在,我们可以创建战士和法师的实例,并分别调用它们的攻击方法:

val warrior = Character(WarriorAttackStrategy())
warrior.attack() // 输出:战士使用剑攻击

val mage = Character(MageAttackStrategy())
mage.attack() // 输出:法师使用魔法攻击

通过使用策略模式,我们可以轻松地为角色添加新的攻击方式,而无需修改角色类的代码。


策略模式的优势:



  • 灵活性:策略模式允许在运行时动态切换算法,使系统更具灵活性。

  • 可扩展性:通过添加新的策略类,我们可以轻松地扩展系统的功能。


缺点:



  • 增加了类的数量:引入策略模式会增加类的数量,特别是当策略类较多时。过度使用策略模式可能会导致类爆炸。


二、状态模式


在某些情况下,对象的行为取决于其内部状态的变化。状态模式可以帮助我们更好地管理对象的状态,并基于不同的状态执行相应的行为。


应用场景: 以电梯为例,电梯在不同的状态下有不同的行为。比如,当电梯在运行状态下,按下楼层按钮不会有任何反应;而当电梯在停止状态下,按下楼层按钮会触发电梯移动到相应楼层的行为。使用状态模式,我们可以更好地管理电梯的状态和行为,提高系统的可维护性和扩展性。


代码示例: 让我们以电梯的例子来说明状态模式的使用。首先,我们定义一个电梯状态接口:

interface ElevatorState {
fun pressFloorButton(floor: Int)
}

接下来,我们创建两个具体的状态类,分别代表电梯的运行状态和停止状态:

class RunningState : ElevatorState {
override fun pressFloorButton(floor: Int) {
// 运行状态下不响应按钮按下事件
}
}

class StoppedState : ElevatorState {
override fun pressFloorButton(floor: Int) {
println("电梯移动到楼层 $floor")
}
}

然后,我们创建一个电梯类,其中包含一个状态的引用:

class Elevator(private var state: ElevatorState) {
fun setState(state: ElevatorState) {
this.state = state
}

fun pressFloorButton(floor: Int) {
state.pressFloorButton(floor)
}
}

现在,我们可以创建一个电梯的实例,并模拟按下楼层按钮的事件:

val elevator = Elevator(StoppedState())
elevator.pressFloorButton(5) // 输出:电梯移动到楼层 5

elevator.setState(RunningState())
elevator.pressFloorButton(7) // 没有任何输出

通过使用状态模式,我们可以根据电梯的状态来决定是否响应按钮按下事件,并执行相应的行为。


状态模式的优势:



  • 易于扩展:通过添加新的状态类,我们可以轻松地扩展系统的行为。

  • 可维护性:状态模式使状态转换和行为与特定状态相关,使代码更具可读性和可维护性。


缺点:



  • 增加了类的数量:引入状态模式会增加类的数量,特别是当状态较多时。过度使用状态模式可能会导致类爆炸。


策略模式与状态模式的区别:



  • 目的不同:策略模式关注的是算法的选择和使用,用于根据不同的策略执行不同的行为;而状态模式关注的是对象内部状态的变化,用于根据不同的状态执行不同的行为。

  • 职责不同:策略模式将不同的算法封装成独立的策略类,客户端主动选择使用哪种策略;而状态模式将不同的状态封装成独立的状态类,并由环境类根据当前状态执行相应的行为。

  • 关注点不同:策略模式关注的是行为的选择和灵活性,用于动态地切换算法;而状态模式关注的是状态的变化和可维护性,用于根据状态执行相应的行为。


结语: 策略模式和状态模式是两种常用的设计模式,它们分别解决了根据不同策略和状态执行不同行为的问题。通过灵活地选择和管理策略或状态,我们可以提高系统的灵活性、可扩展性和可维护性。在实际开发中,我们可以根据具体的需求和场景选择适合的设计模式,以优化代码结构和提高系统的可维护性。希望本文能帮助你更好地理解和应用策略模式和状态模式,让你在设计中游刃有余,解决你的设计困扰!


作者:潜行中的小角色
链接:https://juejin.cn/post/7243828459288395835
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

两年半的Android程序员の上半年总结

你好,你好们好~ 首先是感谢@张拭心,@林栩link以及各位大佬,jym的熏陶,我属实是21届,到目前为止也就两年半~ 技术上必然有待提高,但是文案书写上,是可以同步跟上的;这是我学到最核心的点;工作完成与否是义务责任,学会如何汇报是在强化自己的能力;所以我也...
继续阅读 »

你好,你好们好~
首先是感谢@张拭心,@林栩link以及各位大佬,jym的熏陶,我属实是21届,到目前为止也就两年半~


技术上必然有待提高,但是文案书写上,是可以同步跟上的;这是我学到最核心的点;工作完成与否是义务责任,学会如何汇报是在强化自己的能力;所以我也在想着自发性的去做总结;


一是觉得这样会有更好的健康的提升。


二是觉得:



人们无法预知片刻的价值,直到片刻永远变成了回忆



先从工作部分开始说吧


wallhaven-wyzxvr_1920x1080.png


工作

1、应用层进入Framework层



从年后开始,陆陆续续能在工作上接触到一些简单的系统层的业务,当然是简单的,比如View的焦点控制,网络SDK部分,系统截图适配等等;这其实算是一种能力被认可的预兆,可以被放到一些看似无关紧要的业务去了,对于我来说,我是非常感兴趣的;这样我会更有动力去接触驱动层,适配芯片;当然岗位的大部分内容还是跟UI相关,处理用户交互逻辑,适配数据接口等等,完全进化应该还需要很长一段时间。


2、接触到底层消息框架


硬件公司核心都在底层,C & C++ 应该都是最核心的部分;领导决策开放新项目的所有部门的源码,任何更新都可以可以看到【当然权限是领导给的,本项目的同事】,这意味着我可以在任何时间节点上,调试所有新功能,对比前后差别,思考为何这么实现?换做是我能会怎么做?规避逻辑,扩展思路怎么加?这些我都可以自己想,自己完成;领导英明!


3、上海分公司转去苏州总部


申请去总部的原因实际上有很多,无非是想省下更多的钱,既为梦想也为生活,当然上海的时候也过得滋润,但不免会想到以后的事儿,虽然现在这个盼望破灭了,在我还没有写完这个文章时候【下面生活的部分会说到】,但对于工作的发展肯定是有利的;努力吧




工作部分总结,内容真的不多,新入社会各处都需要学习,各处都需要认真,切勿松懈;既为自己也为将来;正反馈永远都会在你做出选择那一刻就开始显现的,即使不明显;


d804058cd0e6b476d57f7eccda390d0.jpg


生活

1、和女友分手



在对双方家人完全公开学历职业,身体状况,家庭背景以后,女方家庭不太愿意许这份亲事,理由是不够般配;以很多承诺,房车礼金孩子都不是问题以后,依旧无动于衷;女友也被说服,压力太大,扛不住,最后选择分开;我知道我的原因我的态度肯定是占大部分的,没做好自己该做的情感支撑;只能说,害,遗憾终将困其一生;


47f54f952e5ab441533ff3d48d1cde2.png




点亮更多的城市


没有忘记自己一直以来喜欢做的事情,就是旅游;游山玩水,闲云野鹤;
上半年去了几个城市,太原,西安,长沙,苏州;最后还转去了苏州哈哈哈


0c8c589f228141599b450e900d91fb4.jpg


0da20b2d67eaa5d8a1b309517fd50b2.jpg


0e8e0836c99009e3433b0ff21f1a606.jpg


1efc74fa9d338f1084e8bcfe0fd14a9.jpg


2cf8c88ba518d13e75854fbf3bcaf40.jpg


18a079711d5a9fee5630234a6808a7f.jpg


72c721f34630897b022e3c64eea5a60.jpg


79a503a738f10d784bd7c07c22e037a.png


201f6cd0491dfa13b7baf215c0e023e.jpg


2820dc3c8eee657c6786c5401f4d5a0.jpg


28003994205371e78b317719cc8a203.jpg


ad129f5e32b2663f42ecbd699bc6382.jpg


b0b0b80c9cc29bb9addb31de096a2d7.jpg


d8af16d1467abd26102e93e7420dad7.jpg


dbe11b157d1a75c7830b68700298fbd.jpg


e6919033d19f70baa1097f81aab7945.jpg


ed7ffd420892a8946f704f6adcca9ef.jpg


f0f14a836a16abbac3c91ca3dda4315.jpg


fa69a14a886061e753aad7cebc4e8c3.jpg


fffb41b013f68a1f2e9bd3e85d5709d.jpg




坚持读实体书


总觉得电子书我看不下去,即使看了也没有特别多的思考过程,反而拿到实体书的时候,我一看就是一下午,或者我会反复琢磨一小段他想要描述的故事;当然目前还只是停留在一些悬疑小说,休闲生活的书籍,名著,心理学等等经验的书籍我还是看得少。怎么说呢,我觉得应该是故事导出对生活的思考,千古无同局,有的都是处理问题的心态和思维;
又或者我也就只是拿它打发时间。


02fd964c67fb27fe801e187632fc0cb.jpg


41fe086f97e437b26050fc9ce8117b7.jpg


73ebab8ff382770c8e2556da83a1a3c.jpg


875807d2a0549b2ab110f75a2747fb1.jpg


a29d817254dde31d77e50d43dc71cc1.jpg


b46c9643e2fc6a341955d8a516971da.jpg




PC游戏


奥日依旧没有通关,它会是我很喜欢的画风,想要细细聘问;


饥荒也在玩,即使过了十年,存活的技巧我依然不会掌握,但是开心就开心在收集;


LOL偶尔会和三五好友一起打几把,大乱斗居多,下棋也会玩儿;


地平线是日常,排气声能让我很舒缓。





我属于一个重度的赛车迷,毕业两年多自己攒了钱就立马买了一辆二手马自达6,人生第一台车;


我也不知道是不是最近两年火起来的JDM风,激起我对跑山,跑环湖的兴趣,一个月都是去一到两次;现在好像马自达6也有点满足不了我了,也在计划换新的车计划,我现在属于那种拿买房的钱去买车来取悦自己的人,也许会不够成熟吧,又或许开心比什么都重要;


31ba1f26ae7210b98e3d6a8df3c55dd.jpg


a7c8663ad4c67b71d2ad84c6ec919cc.jpg


林拜先生说过:



人活着是一门艺术,


创造这门艺术需要你有高超的手艺,


人都有强项,都有短板,


成就成在扬长避短上。



是的,应该承认。


今天的晚霞很美,虽然它跟昨天、前天、明天相比没有什么不同,但依旧是未完待续...


744d2d66f21da7c78e035ef2719e60f.jpg


作者:Kuye
链接:https://juejin.cn/post/7244007111423803450
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

天马行空使用适配器模式

1. 前言 因为最近没有什么比较好的技术想要分享,所以来水一下文章,啊不对,是分享一下一些思路,和大家交流一下想法,没准能产生一些新的想法。这篇文章不具备权威,不一定正确,简单来说全都是我瞎吹。 开发这么久,多多少少也有了解过适配器模式。我还是一如既往的建议学...
继续阅读 »

1. 前言


因为最近没有什么比较好的技术想要分享,所以来水一下文章,啊不对,是分享一下一些思路,和大家交流一下想法,没准能产生一些新的想法。这篇文章不具备权威,不一定正确,简单来说全都是我瞎吹。


开发这么久,多多少少也有了解过适配器模式。我还是一如既往的建议学习设计模式,不能光靠看别人的文章,得靠自己的积累和理解源码的做法来得更切实际。


2. 浅谈适配器


我们都知道,适配器模式是一种结构型模式,结构型模式我的理解都有一个特点,简单来说就是封装了一些操作,就是隐藏细节嘛,比如说代理就是隐藏了通过代理调用实体的细节。


我还是觉得我们要重头去思考它是怎样的一个思路。如果你去查“适配器模式”,相信你看到很多关于插头的说法,而且你会觉得这东西很简单,但好像看完之后又感觉学了个寂寞。


还是得从源码中去理解。先看看源码中最经典的适配器模式的使用地方,没错,RecyclerView的Adapter。但是它又不仅仅只使用Adapter模式,如果拿它的源码来做分析反而会有些绕。但是我们可以进行抽象。想想Adapter其实主要的流程就是输入数据然后输出View给RecyclerView


然后又可以大胆的去思考一下,如果不定义一个Adapter,没有ViewHolder,要怎么实现这个效果?写一个循环,然后在循环里面做一些逻辑判断,然后创建对应的子View,添加到父View中 ,大概会这样做吧。那其实Adapter就帮我们做了这一步,并且还做了很多比较经典的优化操作,其实大概就是这样。


然后从这样的模型中,我大概是能看出了一些东西,比如使用Adapter是为了输入一些东西,也可以说是为了让它封装一些逻辑,然后达到某些目的,如果用抽象的眼光去看,我不care你封装的什么东西,我只要你达到我的目的(输出我的东西),RecyclerView的adapter就是输出View。这是其一,另外,在这个模型中我有一个使用者去使用这个Adaper,这个Apdater在这里不是一个概念,而是一个对象,我这个使用者通过Adaper这个对象给它一些输入,以此来实现我的某些目标


ok,结合Adapter模式的结构图看看(随便从网上找一张图)


image.png


可以看到这个模型中有个Client,有个Tagrget,有个Adapter。拿RecyclerView的Adapter来说,Client是RecyclerView (当然具体的addView操作是在LayoutManager中,这里是抽象去看),Adapter就是RecyclerView.Adapter,而Tagrget抽象去看就是对view的操作。


3. 天马行空的使用


首先一般使用官方的RecyclerView啊这些,都会提供自己的Adapter,但是会让人会容易觉得Adapter就是在复用View的情况下使用。而我的理解是,RecyclerView的复用是ViewHolder的思想,不是Adapter的思想,比如早期的ListView也有Adapter啊,当时出RecyclerView之前也没有说用到ViewHolder的这种做法(这是很多年前的事),我说这个是想要表达不要把Adapter和RecyclerView绑一起,这样会让思维受到局限。


对我而言,Adapter在RecyclerView中的作用是 “Data To View” ,适配数据而产出对应的View。


那我可以把Apdater的作用理解成“Object To Object” ,对于我来说,Object它可以是Data,可以是View,甚至可以是业务逻辑。


所以当我跳出RecyclerView这种传统的 “Data To View” 的思维模式之后,Adapter适配器模式可以做到的场景就很多,正如上面我理解的,Adapter简单来说就是 “Data To View” ,那我可以用适配器模式去做 “Data To Data” ,可以去做 “View To Data” ,可以去做 “Business To Business” , “Business To View” 等等,实现多种效果。


假设我这里做个Data To Data的场景 (强行举的例子可能不是很好)


我请求后台拿到个人的基本信息数据

data class PeopleInfo(
var name: String?,
var sex: Int?,
......
)

然后通过个人的ID,再请求服务端另外一个接口拿到成绩数据

data class ScoreInfo(
var language : Int,
var math : Int,
......
)

然后我有个数据竞赛的报名表对象。

data class MathCompetition(
var math : Int,
var name : String
)

然后一般我们使用到的时候就会这样赋值,假设这段代码在一个Competition类中进行

val people = getPeopleInfo()
val score = getScoreInfo()
val mathTable = MathCompetition(
score.math?,
people.name?,
......
)

就是深拷贝的一种赋值,我相信很多人的代码里面肯定会有

两个对象 A 和 B
A.1 = B.1
A.2 = B.2
A.3 = B.3
......

这样的代码,然后对象的熟悉多的话这个代码就会写得很长。当然这不会造成什么大问题,但是你看着看着,总觉得缺少一些美感,但是好像这玩意没封装不起来这种感觉。


如果用适配器来做的话,首先明确要做的流程是从Competition这个类中,将所有数据整合成MathCompetition对象,目标就是输出MathCompetition对象。那从适配器模式的模型上去看,client就是Competition,是它的需求,Taget就是getMathCompetition,输出mathTable,然后我们可以写一个Adapter。

class McAdapter {

var mathCompetition : MathCompetition? = null

init {
// 给默认值
mathCompetition = MathCompetition(0, "name", ......)
}

fun setData(people : PeopleInfo? = null, score : ScoreInfo? = null){
people?.let {
mathCompetition?.name = it.name
......
}

score?.let {
mathCompetition?.math = it.math
......
}
}

fun getData() : MathCompetition?{
return mathCompetition
}

}

然后在Competition中就不需要直接引用MathCompetition,而是设置个setAdapter方法,然后需要拿数据时再调用adapter的getData()方法,这样就恰到好处,不会把这些深拷贝方式的赋值代码搞得到处都是。 这个Demo看着好像没什么,但是真碰到了N合1这样的数据场景的时候,使用Adapter显然会更安全。


我再简单举一个Business To View的例子吧。假设你的app中有很几套EmptyView,比如你有嵌入到页面的EmptyView,也有做弹窗类型的EmptyView,我们一般的做法就是对应的页面的xml文件中直接写EmptyView,那这些EmptyView的代码就会很分散是吧。OK,你也想整合起来,所以你会写个EmptyHelper,大概是这个意思,用单例写一个管理EmptyView的类,然后里面统一封装对EmptyView的操作,一般都会这样写。 其实如果你让我来做,我可能就会用适配器模式去实现。 ,当然也有其他办法能很好的管理,这具体的得看心情。


写一个Adapter,我这里写一段伪代码,应该比较容易能看懂

class EmptyAdapter() {
// 这样语法要伴生,懒得写了,看得懂就行
const val STATUS_NORMAL = 0
const val STATUS_LOADING = 1
const val STATUS_ERROR = 2

private var type: Int = 0
var parentView: ViewGroup? = null
private var mEmptyView: BaseEmptyView? = null
private var emptyStatus = 0 // 有个状态,0是不显示,1是显示加载中,2是加载失败......

init {
createEmptyView()
}

private fun createEmptyView() {
// 也可以判断是否有parentView决定使用哪个EmptyView等逻辑
mEmptyView = when (type) {
0 -> AEmptyView
1 -> BEmptyView
2 -> CEmptyView
else -> {
AEmptyView
}
}
}

fun setData(status: Int) {
when (status) {
0 -> parentView?.removeView(mEmptyView)
1 -> mEmptyView?.showLoading()
2 -> mEmptyView?.showError()
}
}

fun updateType(type: Int) {
setData(0)
this.type = type
createEmptyView()
}
}

然后在具体的Activity调用的时候,可以

val emptyAdapter = EmptyAdapter(getContentView())
// 然后在每次要loading的时候去设置adapter的的状态
emptyAdapter.setData(EmptyAdapter.STATUS_LOADING)
emptyAdapter.setData(EmptyAdapter.STATUS_NORMAL)
emptyAdapter.setData(EmptyAdapter.STATUS_ERROR)

可以看出这样做就有几个好处,其一就是不用每个xml都写EmptyView,然后也能做到统一的管理,和一些人写的Helper这种的效果类似,最后调用的方法也很简单,你只需要创建一个Adaper,然后调用它的setData就行,我们的RecyclerView也是这样的,在外层去创建然后调用Adapter就行。


4. 总结


写这篇文章主要是为了水,啊不对,是为了想说明几个问题:

(1)开发时要善于跳出一些限制去思考,比如RecyclerView你可能觉得适配器模式就和它绑定了,就和View绑定了,有View的地方才能使用适配器模式,至少我觉得不是这样。

(2)学习设计模式,只去看一些介绍是很难理解的,当然要知道它的一个大致的思想,然后要灵活运用到开发中,这样学它才有用。

(3)我对适配器模式的理解就是 Object To Object,我可以去写ViewAdapter,可以去写DataAdapter,也可以去写BusinessAadpter,可以用这个模式去适配不同的场景,利用这个思想来使代码更加合理。


当然最后还是要强调一下,我不敢保证我说的就是对的,我肯定不是权威的。但至少我使用这招之后的的新代码效果要比一些旧代码更容易维护,更容易扩展。


作者:流浪汉kylin
链接:https://juejin.cn/post/7242623772301459517
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

何谓实事求是地工作?

提到实事求是,大家第一时间会想到什么?我想大部分是客观,事实,脚踏实地?这么一想,大家都会觉得,自己挺实事求是的呀,没毛病。但是,我会经常在工作中感受到不是那么实事求是的行为,比如张嘴就来,不带思考,做事全靠猜的行为,真太多了。 随着我这两年的学习和总结,我越...
继续阅读 »

提到实事求是,大家第一时间会想到什么?我想大部分是客观,事实,脚踏实地?这么一想,大家都会觉得,自己挺实事求是的呀,没毛病。但是,我会经常在工作中感受到不是那么实事求是的行为,比如张嘴就来,不带思考,做事全靠猜的行为,真太多了。


随着我这两年的学习和总结,我越发觉得实事求是非常重要,并把它视为我做事情和成长的基石。对于实事求是,我主要有以下 3 层理解。


首先,尊重客观事实,探寻真理。我们要承认事实,即使这个事实有多么的难以置信,但存在即是合理,我们首先要尊重它,承认它。然后我们还要积极主动地面对它,探寻事实背后的真理,获得真知,这样才能真正的成长,并有可能寻得机会。当某个事情的进展超出自己预期的时候,我们正确的态度应该是思考为什么会这样,而不是去想对错得失。


其次,数据说话,数据驱动。事实如何去量化?答案是数据。使用数据去表达事实,是我们程序员应该有的技能。工作的本质就是解决问题,之前的文章有讲解,问题就是理想状态和现实状态之间的差别,因此,我们在工作当中做的每一项决策的依据、制定的每一个目标,都应该用数据说话。我们应该使用数据表达现状,并使用数据衡量目标,驱动自己去工作。一些沟通的细节就能够体现出他是不是在实事求是地工作,比如“这个页面加载太慢了,需要优化”。那这个页面加载到底有多慢?业界标准或者竞品的加载耗时是多少?优化的目标值是多少?


最后,从客观事实中获取反馈,不断迭代。工作中想要获得成功和成长,最核心的一个环节是反馈。很多人没有意识到这点。更多的人没有意识到的是,获取反馈其实很简单,随处都是。敏捷开发、精益创业、增长黑客,这些理论的底层核心都是基于事实和数据的反馈,不断迭代改进自己的产品,从而获得成功。对于个人成长来说也是一样的,我们要从客观事实中获取反馈,思考总结,不断迭代自己的能力。


总结一下,实事求是地工作有 3 个层次,首先,要正视事实,并主动探究真理;然后我们慢慢地开始用数据驱动自己的工作;最后让数据驱动变成循环,不断迭代,并把这种循环融入到各个方面,包括工作和个人成长,让它成为自己下意识的动作。


我在努力学习和践行实事求是地工作,我也希望我的团队可以用实事求是的态度来工作,以此文共勉!


作者:潜龙在渊灬
链接:https://juejin.cn/post/7241394138260160568
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

🐞 如何成为一名合格的“中级开发”

嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️ 在这个系列里面的上一篇文章中,我跟大家分享了怎么做一个专业的开发者,还有工作中要注意什么事情。 这是我们人生很重要的一步,因为只有学会怎么开始,才能慢慢变优秀,才能一步步往上进步。 如果你是第一次看这个系列,我强烈建...
继续阅读 »

嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️


在这个系列里面的上一篇文章中,我跟大家分享了怎么做一个专业的开发者,还有工作中要注意什么事情。


这是我们人生很重要的一步,因为只有学会怎么开始,才能慢慢变优秀,才能一步步往上进步。


如果你是第一次看这个系列,我强烈建议你回去看看我之前写的两篇文章,说不定能对你有帮助。




  1. 🎖️怎么知道我的能力处于什么水平?我该往哪里努力?

  2. 🚗我毕业/转行了,怎么适应我的第一份开发工作?



其实我想写这篇文章已经很久了,可是一直想不出来怎么写,找了很多资料也没用。


确实憋不出来,中间还水了一篇“JavaScript冷饭”文章。天可是天天炒冷饭不好吃啊,写那些水文总会心生愧疚,感觉对不起你们哈哈。


今天,我们继续聊一聊,当我们进入这个角色一两年后,该怎么摆脱“初级”头衔,迈入“中级”阶段呢?😎


🎖️ 中级开发的显著特点:“骄傲”



注意事项:


我接下来提及的内容可能很多大佬跟我的意见是不同的。


也有可能我的知识有限,我只涵盖了前端开发工程师的部分,对其他岗位的开发工程师不了解,可能我说的指标并不一定能和贵公司考核时所授予给的职称相对应。


我这里说的是衡量开发人员技能、知识和整体能力的一般指标


它会根据所在的领域而变化,比如前端、后端、数据等等都不太一样。


虽然具体的工具、技术甚至架构知识可能有所不同,但是我说的一般原则应该是可以广泛适用的。


如果觉得我说错了,请在评论区交流。😊



“骄傲”他拥有的知识


我这里要说一个很有意思的特点,这个特点一定能够显著的提醒你,你肯定到了“中级”级别。


可能你觉得我要说的是开玩笑,但是我是认真的,我觉得大部分的“中级开发”都经历过这个事情。


如果你认真看过我的这篇文章,我希望你可以在评论区互动,在评论区跟大家说一声:你不是一个人😊


当你到了中级水平,你心里一定有一个想法。那就是:


我已经学会了我现在做的事情,以及要用的所有东西了!


再说得清楚一点就是:


“我已经完全会用JavaScript了,我对HTML很熟悉了,我对数据库没问题!”


“我已经完全会用Vue了,我也会用Angular开发”


这个时候的“中级开发”,觉得他已经有了这个领域需要的能力了。



当然啦,这不是一种傲慢,我想表达的“骄傲”不是贬义词。


这个阶段只是我们成长中必须经历的一个阶段。



“骄傲”不是一件坏事


其实刚刚我也 tips 了一下,这个是我们一定会经历的阶段。


说明你真的要开始要进入“中级开发“这个角色了。


就像我们小时候我们都会觉得,爸爸妈妈什么都不知道,我们才更明白一样


当你拥有”骄傲“,你才开始真正走自己的路。这个时候你才真正开始独立思考。


这意味着你已经积累了足够的知识和经验,可以继续精进设计模式、最佳实践等这些学科以拔高你的知识。


简单的东西已经不能吸引你了。


🚩 中级开发应该掌握什么?


现在你是中级开发了,你需要看看自己是不是能做到下面这些事情。


这些“新”的东西可以让中级开发更有经验,也更能帮助团队。


编程能力:



  1. 很清楚不同的系统(API、模块、包等)怎么互相连接

  2. 熟练使用编程工具(IDE、GIT等)

  3. 知道怎么实现一般的需求

  4. 遇到bug的时候,知道从哪里找原因和解决办法

  5. 知道怎么优化代码和重构代码

  6. 知道怎么提高性能

  7. 知道怎么用面向对象的程序设计

  8. 知道常用的软件架构模式(MVC、MVVM、MVP、MVI等)

  9. 知道编程语言的一些特点(函数式编程)

  10. 知道怎么部署系统应用

  11. 知道怎么用数据库索引

  12. 知道怎么用数据库表迁移

  13. 知道怎么用数据库分片技术


社会能力:



  1. 可以偶尔跟产品经理(客户)沟通

  2. 是团队的主力


开始优雅:



  1. 代码模块开始按照设计模式来写

  2. 对烂代码有敏感度和重构能力


等等


📌 对中级开发的一些建议


也许现在在读文章的你已经是一位中级开发的存在了,我现在有一些建议想要分享给你!


找一个自己感兴趣的开发者社区加入


为什么我们常说“好的团队创造个人”呢


因为当你真的参与到了重要或高价值的项目时,你真的比一个人漫无目的地学习更快地获得经验。


而且当你真正在团队中贡献力量地时候,你地团队,你的组长,你的领导都会知道,把事情交给你,你就能把自己做好。


在这个过程中,你能积累经验并在你的团队中声名鹊起(这不是名气,而是知名度),那么当新的机会出现时,你就能很快地把握住。


跳出舒适区


跟我上一篇提到的给初级开发的建议类似,你一定要经常的跳出自己的舒适区,不然你不会有毅力坚持学习。


而且,特别是在互联网行业,学习能力是个硬性指标,如果无法坚持下去,很容易就会被淘汰。


这样做可以开阔你的眼界,让你的知识面更广。最终,你会逐渐掌握开发的技巧,面对这些全新的知识领域时,能更快、更准确地找到重点并掌握它们。


但是只要你坚持下去,未来的你一定会与其他人拉开差距。


找到你的导师


这一点在上一篇我也强调过了。你的开发生涯,不能只靠你自己摸索。


你需要有人给你提供想法并能够从中学习。特别是在“中级开发”阶段。


导师可以帮助你不会在某些技术问题或者人生问题上钻牛角尖,他可以拉你一把,避免你浪费很多时间。


这个人可以是你团队中的某个人。


也可以是网络上开发者社区中认识的某位博主。


找到你信任的人(或者更可能是一群人),你可以跟他们问问题和说想法!


找到可以指导你的导师,让你能够突破当前的认知。你的未来将逐步变得清晰起来。


持续学习


这个没什么好说的,在这内卷的社会中,如果没有润的资本和能力,不如在持续学习中等待破局的机会!




🎉 你觉得怎么样?这篇文章可以给你带来帮助吗?当你处于这个阶段时,你发现什么对你帮助最大?如果你有任何疑问或者想进一步讨论相关话题,请随时发表评论分享您的想法,让其他人从中受益。🚀✨


作者:道长王jj
链接:https://juejin.cn/post/7243203041872412731
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

程序员创业:从技术到商业的转变

作为一名程序员,我们通常会聚焦于编程技能和技术能力的提升,这也是我们日常工作的主要职责。但是,随着技术的不断发展和市场的变化,仅仅依靠技术能力已经不足以支撑我们在职场上的发展和求职竞争力了。所以,作为一名有远大理想的程序员,我们应该考虑创业的可能性。 为什么程...
继续阅读 »

作为一名程序员,我们通常会聚焦于编程技能和技术能力的提升,这也是我们日常工作的主要职责。但是,随着技术的不断发展和市场的变化,仅仅依靠技术能力已经不足以支撑我们在职场上的发展和求职竞争力了。所以,作为一名有远大理想的程序员,我们应该考虑创业的可能性。


为什么程序员要创业?


创业其实并非只适用于商学院的毕业生或者有创新理念的企业家。程序员在业内有着相当高的技术储备和市场先知,因此更容易从技术角度前瞻和切入新兴市场,更好地利用技术储备来实现创业梦想。


此外,创业可以释放我们的潜力,同时也可以让我们找到自己的定位和方向。在创业的过程中,我们可能会遇到各种挑战和困难,但这些挑战也将锻炼我们的意志力和决策能力,让我们更好地发挥自己的潜力。


创业需要具备的技能


作为一名技术人员,创业需要具备更多的技能。首先是商业和运营的技能:包括市场分析、用户研究、产品策划、项目管理等。其次是团队管理和沟通能力,在创业的过程中,人才的招聘和管理是核心问题。


另外,还需要具备跨界合作的能力,通过开放性的合作与交流,借助不同团队的技术和资源,完成创业项目。所以我们应该将跨界合作看作是创业过程中的重要选择,选择和加强自己的跨界交流和合作能力,也能为我们的企业注入活力和创新精神。


如何创业?


从技术到商业的转变,从最初想法的诞生到成熟的企业的创立,都需要一个创业的路线图。以下是一些需要注意的事项:




  1. 研究市场:了解市场趋势,分析需求,制定产品策略。可以去参加行业论坛,争取到专业意见和帮助。




  2. 制定商业计划:包括产品方案、市场营销、项目管理、团队建设等。制定一个系统的商业计划是投资者和团队成员对创业企业的认可。




  3. 招募团队:由于我们一般不是经验丰富的企业家,团队的选择尤为重要。要找的不仅要是技能和经验匹配的团队,更要找能一起携手完成创业项目的合作者。




  4. 行动计划:从实现规划步入到实战行动是创业项目的关键。按部就班地完成阶段性任务,控制实施进度和途中变化,在完成一个阶段后可以重新评估计划。




  5. 完成任务并分析:最后,团队成员需要根据企业进展,完整阶段性的目标,做自己的工作。及时完成考核任务并一起分享数据分析、事件解决和项目总结等信息,为项目下一阶段做出准确预测。




结语


创业是一条充满挑战性和机遇的路线,也是在我们的技术和业务的进一步升级中一条非常良好的通道。越来越多的技术人员意识到了自己的潜力,开始考虑自己创业的可能性。只要学会逐步掌握创业所需的技能和知识,并制订出详细的创业路线图,大可放手去尝试,才能最终实现自己心中的创业梦想。


作者:郝学胜
链接:https://juejin.cn/post/7240465997002047547
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Kotlin特性实现高拓展性Logger

前言 在Android中记录日志一般是使用Android自带的Log工具, 但是实际项目中,我们经常需要统一控制项目的日志开关或过滤等级,有时还需要将日志写入到文件中。 目前除了自己实现外,基本都是通过第三方的Log日志库比如JakeWharton/timbe...
继续阅读 »

前言


在Android中记录日志一般是使用Android自带的Log工具,


但是实际项目中,我们经常需要统一控制项目的日志开关过滤等级,有时还需要将日志写入到文件中。


目前除了自己实现外,基本都是通过第三方的Log日志库比如JakeWharton/timberorhanobut/logger ,但是有些情况仍然实现起来困难。


比如:



  • 多模块或封装SDK的情况,子模块也用到日志框架,如何单独管理子模块的日志开关?

  • Java library中没有Android Log类引入日志模块报错,同理使用JUnit做单元测试的时候日志模块报错。


并且替换项目中的Log是痛苦的,因此就需要一个可拓展的日志框架尤为重要。


和大多数流行的Logger框架不同,本文实现的Logger不是对各种数据进行美化输出,本文充分利用Kotlin的拓展函数实现了一个灵活配置的轻量Logger,它具有以下特性:



  • 控制模块中的Logger配置

  • 输出到文件等多个目标,控制输出线程

  • 支持拦截器,实现过滤,格式化

  • 支持Jvm使用,支持JUnit


项目地址 Logger


原理和


核心只有3个类




  1. Logger 日志的操作入口:主要保证稳定简单易用,用作门面。




  2. LogPrinter 日志输出目标:配置日志输出。




  3. LogLevel 日志输出等级:(VERBOSE,DEBUG,INFO,WARNING,ERROR,WTF)




Logger.png


日志记录


通过Logger 可以获取全局实例。


输出日志有许多等级,和android的Log等级是一致的:

Logger.v("Test verbose")
Logger.d("Test debug")
Logger.i("Test info")
Logger.w("Test warning")
Logger.e("Test error")
Logger.wtf("Test wtf")

Logger.v { "Test verbose" }
Logger.d { "Test debug" }
Logger.i { "Test info" }
Logger.w { "Test warning" }
Logger.e { "Test error" }
Logger.wtf { "Test wtf" }


通过Logger["subTag"]可以生成子Logger
默认情况,子LoggerlevellogPrinter均继承自父Logger,tag"$父tag-$subTag"

Logger.tag = "App" //全局Logger的tag设置为“APP”
class XxActivity {
val logger = Logger["XxActivity"] //二级Logger的tag为“APP-XxActivity”
val logger = loggerForClass() //使用当前类名生成二级Logger的tag为“APP-XxActivity”

inner class XxFragment {
val fragmentLogger = logger["XxFragment"]//三级Logger的tag为“APP-XxActivity-XxFragment”
}
}


日志配置


Logger总共只有4个属性:



  • level 过滤等级(VERBOSE,DEBUG,INFO,WARNING,ERROR,WTF)

  • tag 日志TAG

  • logPrinter 日志输出目标,比如Android Logcat,文件,标注输出流,Socket等。

  • loggerFactory 生产子Logger的工厂,默认情况,子LoggerlevellogPrinter均继承自父Logger,tag"$父tag-$subTag"


如下示例配置了顶级Loggerleveltag,输出目标为AndroidLogcat,同时在子线程把WARNING等级的日志按一定格式输出到文件"warning.log"中,
同时"SubModule"Logger的输出等级设置为ERROR等级。

Logger.level = LogLevel.VERBOSE
Logger.tag = "AppName"
Logger.logPrinter = AndroidLogPrinter()
.logAlso(FileLogPrinter({ File("warning.log") })
.format { _, _, messageAny, _ -> "【${Thread.currentThread().name}】:$messageAny" }
.logAt(Executors.newSingleThreadExecutor())
.filterLevel(LogLevel.WARNING)
)
Logger.loggerFactory = { subTag ->
Logger("$tag-$it", level, logPrinter).also { child ->
if (child.tag == "SubModule") {
logger.level = LogLevel.ERROR
}
}
}


实现


Logger功能实现:


因为Logger是门面,所以提供便捷的方法来使用,而真正的写入日志代理给LogPrinter

open class Logger(
var tag: String = "LOG",
var level: LogLevel = LogLevel.VERBOSE,
var logPrinter: LogPrinter = createPlatformDefaultLogPrinter(),
) {
fun v(message: Any?) = log(LogLevel.VERBOSE, message, null)
fun log(level: LogLevel, message: Any?, throwable: Throwable? = null) {
if (this.level <= level) {
logPrinter.log(level, tag, message, throwable)
}
}
//省略其他等级...

var loggerFactory: (childTag: String) -> Logger = ::defaultLoggerFactory

/**
* 创建子Logger
* @param subTag 次级tag,一般为模块名
*/
operator fun get(subTag: String): Logger = loggerFactory(subTag)

companion object INSTANCE : Logger(tag = "Logger")

}
enum class LogLevel(val shortName: String) {
VERBOSE("V"),
DEBUG("D"),
INFO("I"),
WARNING("W"),
ERROR("E"),
WTF("WTF")
}
fun interface LogPrinter {
fun log(level: LogLevel, tag: String, messageAny: Any?, throwable: Throwable?)
}

LogPrinter拓展实现


首先实现对LogPrinter进行拦截,后续的功能都通过拦截器实现。


拦截器

/**
* 拦截器
* logPrinter 被拦截对象
*/
typealias LogPrinterInterceptor = (logPrinter: Logger.LogPrinter, level: Logger.LogLevel, tag: String, messageAny: Any?, throwable: Throwable?) -> Unit

inline fun Logger.LogPrinter.intercept(crossinline interceptor: LogPrinterInterceptor) =
Logger.LogPrinter { level, tag, messageAny, throwable ->
interceptor(this@intercept, level, tag, messageAny, throwable)
}

添加额外的LogPrinter


添加一个额外的LogPrinter,也可看作将2个LogPrinter合并成1个。想要添加多个输出目标时使用。

fun Logger.LogPrinter.logAlso(other: Logger.LogPrinter) =
intercept { logPrinter, level, tag, messageAny, throwable ->
logPrinter.log(level, tag, messageAny, throwable)
other.log(level, tag, messageAny, throwable)
}


设置日志记录线程


控制LogPrinter的输出线程

fun Logger.LogPrinter.logAt(executor: Executor) =
intercept { logPrinter, level, tag, messageAny, throwable ->
executor.execute {
logPrinter.log(level, tag, messageAny, throwable)
}
}

格式化


控制LogPrinter的输出的格式,比如csv格式,Json格式等。

typealias LogFormatter = (level: Logger.LogLevel, tag: String, messageAny: Any?, throwable: Throwable?) -> String

fun Logger.LogPrinter.format(formatter: LogFormatter) =
intercept { logPrinter, level, tag, messageAny, throwable ->
val formattedMessage = formatter(level, tag, messageAny, throwable)
logPrinter.log(level, tag, formattedMessage, throwable)
}

日志过滤


LogPrinter中输出的日志进行过滤,可以根据tagmessagelevelthrowable进行组合判断来过滤。

fun Logger.LogPrinter.filter(
predicate: (
level: Logger.LogLevel,
tag: String,
messageAny: Any?,
throwable: Throwable?
) -> Boolean
) =
intercept { logPrinter, level, tag, messageAny, throwable ->
if (predicate(level, tag, messageAny, throwable)) {
logPrinter.log(level, tag, messageAny, throwable)
}
}


Logger拷贝


Logger为原型拷贝一个新Logger,和生成子Logger不同,它并不是通过loggerFactory生成的,并且tag也是拷贝的。

/**
* 拷贝
*/
fun Logger.copy(
tag: String = this.tag,
level: Logger.LogLevel = this.level,
logPrinter: Logger.LogPrinter = this.logPrinter,
loggerFactory: (childTag: String) -> Logger = ::defaultLoggerFactory,
) = Logger(tag, level, logPrinter).also { it.loggerFactory = loggerFactory }

Json格式化


因为并没有引入任何Android类和Json序列化库,所以没有内置。在此提供Gson示例


方式1,使用LogPrinter拓展


适用于该Logger所有日志都需要转Json的情况

val gson = GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create()

fun Logger.jsonLogger() =
copy(logPrinter = logPrinter.format { _, _, messageAny, _ -> gson.toJson(messageAny) })

//使用
fun testLogJsonLogger() {
val logger = Logger.jsonLogger()
logger.d {
arrayOf("hello", "world")
}
logger.i {
mapOf(
"name" to "tom",
"age" to 19,
)
}
}

方式2,拓展Logger方法


通过拓展Logger方法实现,适用于Logger的部分数据需要输出为Json模式。

inline fun Logger.logJson(level: Logger.LogLevel = Logger.LogLevel.INFO, any: () -> Any) {
log(level, block = { gson.toJson(any()) })
}

//使用
fun testLogJsonExt() {
Logger.logJson {
mapOf(
"name" to "tom",
"age" to 19,
)
}
}


拓展使用


这些拓展方法可以连续调用,就像使用RxJava一样。

Logger.logPrinter = ConsoleLogPrinter()
.format { _, tag, messageAny, _ -> "$tag : $messageAny\n" }
.logAlso(ConsoleLogPrinter()
.format { _, tag, messageAny, _ ->//添加分割线 tag,时间,message转json,同时加上堆栈信息
"""---------------------------\n $tag ${currentTime()} ${Json.toJson(messageAny)} \n${Thread.currentThread().stackTrace.contentToString()}"""
}
.filterLevel(LogLevel.INFO))//仅记录level在INFO及以上的
.logAlso(ConsoleLogPrinter()
.format { _, tag, messageAny, _ -> "$tag :《$messageAny》\n" }
.filter { _, tag, _, _ -> tag.contains("CHILD") })//仅记录tag包含CHILD

混淆


如果通过混淆去除日志信息,可按如下配置。

-assumenosideeffects class me.lwb.logger.Logger {
public *** d(...);

public *** e(...);

public *** i(...);

public *** v(...);

public *** log(...);

public *** w(...);

public *** wtf(...);

}

总结


本文主要使用了Kotlin拓展和高阶函数实现了一个拓展性高的Logger库,通过拓展方法实现线程切换,多输出,格式化等,同时通过配置全局logFactory的方法可以在不修改子模块代码的情况下去控制子模块Logger的level等信息。


该库十分精简,加上拓展和默认实现总代码小于300行,不依赖Android库第三方库,可以在纯Jvm程序中使用,也可在Android程序中使用。


作者:丨小夕
链接:https://juejin.cn/post/7244002241845166138
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

关于晋升的一点思考

晋升是一个极好的自我review的机会,不,应该是最好,而且没有之一。 晋升是最好的自我review的一次机会,不管有没有晋升成功,只要参加了晋升,认真准备过PPT,就已经包赚不赔了。 总的来说,晋升的准备工作充分体现出了——功夫在平时。平时要是没有两把刷子,...
继续阅读 »

晋升是一个极好的自我review的机会,不,应该是最好,而且没有之一。

晋升是最好的自我review的一次机会,不管有没有晋升成功,只要参加了晋升,认真准备过PPT,就已经包赚不赔了。


总的来说,晋升的准备工作充分体现出了——功夫在平时。平时要是没有两把刷子,光靠答辩准备的一两个月,是绝无可能把自己“包装”成一个合格的候选人的。

下面整体剖析一下自己在整个准备过程中的观察、思考、判断、以及做的事情和拿到的结果。


准备工作


我做的第一件事情并不是动手写PPT,而是搜集信息,花了几天在家把网上能找到的所有关于晋升答辩的文章和资料全撸了一遍,做了梳理和总结。


明确了以下几点:



  • 晋升是在做什么

  • 评委在看什么

  • 候选人要准备什么

  • 评判的标准是什么

  • 常见的坑有哪些


首先要建立起来的,是自己对整个晋升的理解,形成自己的判断。后面才好开展正式的工作。



写PPT


然后开始进入漫长而又煎熬的PPT准备期,PPT的准备又分为四个子过程,并且会不断迭代进行。写成伪代码就是下面这个样子。

do {
确认思路框架;
填充内容细节;
模拟答辩;
获取意见并判断是否还需要修改;
} while(你觉得还没定稿);

我的PPT迭代了n版,来来回回折腾了很多次,思路骨架改了4次,其中后面三次均是在准备的后半段完成的,而最后一次结构大改是在最后一周完成的。这让我深深的觉得前面准备的1个月很多都是无用功。


迭代,迭代,还是迭代


在筹备的过程中,有一个理念是我坚持和期望达到的,这个原则就是OODA loop ( Boyd cycle)


OODA循环是美军在空战中发展出来的对敌理论,以美军空军上校 John Boyd 为首的飞行员在空战中驾驶速度慢火力差的F-86军刀,以1:10的击落比完胜性能火力俱佳的苏联米格-15。而Boyd上校总结的结论就是,不是要绝对速度快,而是要比对手更快的完成OODA循环


而所谓的OODA循环,就是指 observe(观察)–orient(定位)–decide(决策)–act(执行) 的循环,是不是很熟悉,这不就是互联网的快速迭代的思想雏形嘛。


相关阅读 what is OODA loop

wiki.mbalib.com/wiki/包以德循环 (from 智库百科)

en.wikipedia.org/wiki/OODA_l… (from Wikipedia)


看看下图,PPT应该像第二排那样迭代,先把框架确定下来,然后找老板或其他有经验的人对焦,框架确定了以后再填充细节。如果一开始填充细节(像第一排那样),那么很有可能越改越乱,最后一刻还在改PPT。


btw,这套理论对日常工作生活中的大部分事情都适用。


一个信息论的最新研究成果


我发现,程序员(也有可能是大部分人)有一个倾向,就是show肌肉。明明很简单明了的事情,非要搞得搞深莫测,明明清晰简洁的架构,非要画成“豆腐宴”。


晋升述职核心就在做一件事,把我牛逼的经历告诉评委,并让他们相信我牛逼。


所以,我应该把各种牛逼的东西都堆到PPT里,甚至把那些其实一般的东西包装的很牛逼,没错吧?


错。


这里面起到关键作用的是 “让他们相信我牛逼” ,而不是“把我牛逼的故事告诉评委”。简单的增大的输出功率是不够的,我要确保评委能听进去并且听懂我说的东西,先保证听众能有效接收,再在此基础上,让听众听的爽。


How?


公式:喜欢 = 熟悉 + 意外


从信息论的角度来看,上面的公式说的就是旧信息和新信息之间要搭配起来。那么这个搭配的配比应该是多少呢?


这个配比是15.87% ——《科学美国人》


也就是说,你的内容要有85%是别人熟悉的,另外15%是能让别人意外的,这样就能达到最佳的学习/理解效果。这同样适用于心流、游戏设计、神经网络训练。所以,拿捏好这个度,别把你的PPT弄的太高端(不知所云),也别搞的太土味(不过尔尔)。


能够否定自己,是一种能力


我审视自己的时候发现,很多时候,我还保留一张PPT或是还持续的花心思做一件事情,仅仅是因为——舍不得。你有没有发现,我们的大脑很容易陷入“逻辑自洽”中,然后越想越对,越想越兴奋。


千万记得,沉没成本不是成本,经济学里成本的定义是放弃了的最大价值,它是一个面向未来的概念,不面向过去。


能够否定和推翻自己,不留恋于过去的“成就” ,可以帮助你做出更明智的决策。


我一开始对好几页PPT依依不舍,觉得自己做的特牛逼。但是后来,这些PPT全被我删了,因为它们只顾着自己牛逼,但是对整体的价值却不大,甚至拖沓。


Punchline


Punchline很重要,这点我觉得做的好的人都自觉或不自觉的做到了。想想,当你吧啦吧啦讲的时候,评委很容易掉线的,如果你没有一些点睛之笔来高亮你的成果和亮点的话,别人可能就糊里糊涂的听完了。然后呢,他只能通过不断的问问题来挖掘你的亮点了。


练习演讲


经过几番迭代以后,PPT可以基本定稿,这个时候就进入下一个步骤,试讲。


可以说,演讲几乎是所有一线程序员的短板,很多码农兄弟们陪电脑睡的多了,连“人话”有时候都讲不利索了。我想这都要怪Linus Torvalds的那句


Talk is cheap. Show me the code.


我个人的经验看来,虽然成为演讲大师长路漫漫不可及,但初级的演讲技巧其实是一个可以快速习得的技能,找到几个关键点,花几天时间好好练几遍就可以了,演讲要注意的点主要就是三方面:



  • 形象(肢体语言、着装等)

  • 声音(语速、语调、音量等)

  • 文字(逻辑、关键点等)



演讲这块,我其实也不算擅长,我把仅有的库存拿出来分享。


牢记表达的初衷


我们演讲表达,本质上是一个一对多的通信过程,核心的目标是让评委或听众能尽可能多的接受到我们传达的信息


很多程序员同学不善于表达,最明显的表现就是,我们只管吧啦吧啦的把自己想说的话说完,而完全不关心听众是否听进去了。


讲内容太多


述职汇报是一个提炼的过程,你可能做了很多事情,但是最终只会挑选一两件最有代表性的事情来展现你的能力。有些同学,生怕不能体现自己的又快又猛又持久,在PPT里塞了太多东西,然后又讲不完,所以只能提高语速,或者囫囵吞枣、草草了事。


如果能牢记表达的初衷,就不应该讲太多东西,因为听众接收信息的带宽是有限的,超出接收能力的部分,只会转化成噪声,反而为你的表达减分。


过度粉饰或浮夸


为了彰显自己的过人之处,有时候会自觉或不自觉的把不是你的工作也表达出来,并没有表明哪些是自己做的,哪些是别人做的。一旦被评委识破(他本身了解,或问问题给问出来了),那将会让你陈述的可信度大打折扣。


此外,也表达的时候也不要过分的浮夸或张扬,一定的抑扬顿挫是加分的,但过度浮夸会让人反感。


注意衔接


作为一个演讲者,演讲的逻辑一定要非常非常清晰,让别人能很清晰明了的get到你的核心思路。所以在演讲的时候要注意上下文之间的衔接,给听众建设心理预期:我大概会讲什么,围绕着什么展开,分为几个部分等等。为什么我要强调这个点呢,因为我们在演讲的时候,很容易忽略听众的感受,我自己心里有清楚的逻辑,但是表达的时候却很混乱,让人一脸懵逼。


热情


在讲述功能或亮点的时候,需要拿出自己的热情和兴奋,只有激动人心的演讲,才能抓住听众。还记得上面那个分布图吗?形象和声音的占比达到93%,也就是说,你自信满满、热情洋溢的说“吃葡萄不吐葡萄皮”,也能打动听众。


第一印象


这个大家都知道,就是人在最初形成的印象会对以后的评价产生影响 。

这是人脑在百万年进化后的机制,可以帮助大脑快速判断风险和节省能耗——《思考,快与慢》

评委会刻意避免,但是人是拗不过基因的,前五分钟至关重要,有经验的评委听5分钟就能判断候选人的水平,一定要想办法show出你的与众不同。可以靠你精心排版的PPT,也可以靠你清晰的演讲,甚至可以靠一些小 trick(切勿生搬硬套)。


准备问题


当PPT准备完,演讲也练好了以后,不出意外的话,应该没几天了。这个时候要进入最核心关键的环节,准备问题。


关于Q&A环节,我的判断是,PPT和演讲大家都会精心准备,发挥正常的话都不会太差。这就好像高考里的语文,拉不开差距,顶多也就十几分吧。而Q&A环节,则是理综,优秀的和糟糕的能拉开50分的差距,直接决定总分。千万千万不可掉以轻心。


问题准备我包含了这几个模块:



  • 业务:业务方向,业务规划,核心业务的理解,你做的事情和业务的关系,B类C类的差异等

  • 技术:技术难点,技术亮点,技术选型,技术方案的细节,技术规划,代码等

  • 数据:核心的业务数据,核心的技术指标,数据反映了什么等等

  • 团队:项目管理经验,团队管理经验

  • 个人:个人特色,个人规划,自己的反思等等


其中业务、技术和数据这三块是最重要的,需要花80%的精力去准备。我问题准备大概花了3天时间,整体还是比较紧张的。准备问题的时候,明显的感觉到自己平时的知识储备还不太够,对大业务方向的思考还不透彻,对某些技术细节的把控也还不够到位。老话怎么说的来着,书到用时方恨少,事非经过不知难。


准备问题需要全面,不能有系统性的遗漏。比如缺少了业务理解或竞品分析等。


在回答问题上,也有一些要点需要注意:


听清楚再回答


问题回答的环节,很多人会紧张,特别是一两道问题回答的不够好,或气氛比较尴尬的时候,容易大脑短路。这个时候,评委反复问你一个问题或不断追问,而自己却觉得“我说的很清楚了呀,他还没明白吗”。我见过或听说过很多这样的案例,所以这应该是时有发生的。


为了避免自己也踩坑,我给自己定下了要求,一定要听清楚问题,特别是问题背后的问题。如果觉得不清楚,就反问评委进行doubel check。并且在回答的过程中,要关注评委的反映,去确认自己是否答到点子上了。


问题背后的问题


评委的问题不是天马行空瞎问的,问题的背后是在考察候选人的某项素质,通过问题来验证或挖掘候选人的亮点。这些考察的点都是公开的,在Job Model上都有。


我认为一个优秀的候选人,应当能识别出评委想考察你的点。找到问题背后的问题,再展开回答,效果会比单纯的挤牙膏来的好。


逻辑自洽、简洁明了


一个好的回答应该是逻辑自洽的。这里我用逻辑自洽,其实想说的是你的答案不一定要完全“正确”(其实往往也没有标准答案),但是一定不能自相矛盾,不能有明显的逻辑漏洞。大部分时候,评委不是在追求正确答案,而是在考察你有没有自己的思考和见解。当然,这种思考和见解几乎都是靠平时积累出来的,很难临时抱佛脚。


此外,当你把逻辑捋顺了以后,简洁明了的讲出来就好了,我个人是非常喜欢能把复杂问题变简单的人的。一个问题的本质是什么,核心在那里,关键的几点是什么,前置条件和依赖是什么,需要用什么手段和资源去解决。当你把这些东西条分缕析的讲明白以后,不用再多啰嗦一句,任何人都能看出你的牛逼了。


其他


心态调整


我的心态经历过过山车般的起伏,可以看到



在最痛苦最难受的时候,如果身边有个人能理解你陪伴你,即使他们帮不上什么忙,也是莫大的宽慰。如果没有这样的人,那只能学会自己拥抱自己,自己激励自己了。


所以,平时对自己的亲人好一点,对朋友们好一点,他们绝对是你人生里最大的财富。


关于评委


我从一开始就一直觉得评委是对手,是来挑战你的,对你的汇报进行证伪。我一直把晋升答辩当作一场battle来看待,直到进入考场的那一刻,我还在心理暗示,go and fight with ths giants。


但真实的经历以后,感觉评委更多的时候并不是站在你的对立面。评委试图通过面试找到你的一些闪光点,从而论证你有能力晋升到下一个level。从这个角度来讲,评委不但不是“敌人”,更像是友军一般,给你输送弹药(话题)。


一些教训




  • 一定要给自己设置deadline,并严格执行它。如果自我push的能力不强,就把你的deadline公开出来,让老板帮你监督。




  • 自己先有思考和判断,再广开言路,不要让自己的头脑成为别人思想的跑马场。




  • 坚持OODA,前期千万不要扣细节。这个时候老板和同事是你的资源,尽管去打扰他们吧,后面也就是一两顿饭的事情。




附件


前期调研



参考文章


知乎


作者:酒红
链接:https://juejin.cn/post/7240805459288162360
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

北美 2023 被裁员的感悟

不贩卖焦虑,就事论事,希望能帮助到有需要的朋友。 很多人觉得在裁员之前是没有任何迹象的,其实真的不是这样。 公司在裁员的过程中有很多要裁员的迹象,我会在另外一篇文章中对我遇到的一些裁员信号设置一些雷区和警告,当你遇到上面的这些信号的时候,直觉告诉你需要马上考虑...
继续阅读 »

不贩卖焦虑,就事论事,希望能帮助到有需要的朋友。


很多人觉得在裁员之前是没有任何迹象的,其实真的不是这样。


公司在裁员的过程中有很多要裁员的迹象,我会在另外一篇文章中对我遇到的一些裁员信号设置一些雷区和警告,当你遇到上面的这些信号的时候,直觉告诉你需要马上考虑寻找下一个替代方案了。


因为当这些信号的任何一个或者多个同时出现的时候就意味着裁员在进行中了,通常会在 3 到 6 个月左右发生。


WeChat Image_20230602102637


在公司的职位


在被裁公司的职位是 Tech Lead。


虽然这个职位并不意味着你对其他同事而言能够获得更多的有效信息,但是通常可能会让自己与上级有更好的沟通管道。


但是,非常不幸的是这家公司的沟通渠道非常有问题。


因为负责相关开发部分的副总是从 PHP 转 Java 的,对 Java 的很多情况都不非常明确,所以他非常依赖一个架构师。


但,公司大部分人都认为这个架构师的要求是错误的,并且是非常愚蠢的。


比如说要求代码必须要放在一行上面,导致代码上面有不少行甚至超过了 1000 个字符。


所有开发都对这个要求非常不理解,并且多次提出这种要求是无理并且愚蠢的,我们组是对这个要求反应最激烈,并且抵触最大的(也有可能是因为我的原因,我不希望在明显错误的地方让步;我可以让步,但是需要给一个能说服的理由)。


然而,这个所谓的架构师就利用 PR 合并的权力,不停的让我们的组员进行修改。


裁员之前


正是因为在公司的职位和上面说到的和架构师直接的冲突。


在 6 个月之前,我就已经和组里的同事说要准备进行下一步了,你们该面试的就面试了,不要拖延。


在这个中间过程中,我的领导还找我谈过一次。领导的意思就是他非常同意我们的有关代码 PR 的要求,也觉得这些要求都是狗屁。


但,负责开发的副总,认为我们组现在是所有组里面最差的。


可能当时没有认真理解这句话的意思,我们组从所有组里面最好的,变成最差的只用了 2 周(一个 Sprint)的时间。


在这次谈话后,我更加坚信让我的组员找下一家的信息了,对他们中途要面试其他公司我都放开一面。


非常不幸的,我自己那该死的拖延症,我是把我自己的简历准备好了,但是还没有来得及投就等来了真正裁员的这一天。


深刻的教训和学到的经验:


如果公司的运营或者管理让感觉到不舒服,并且已经有开始寻找下家的想法的时候,一定要尽快准备,马上实施,不要拖延


这就是我在上面标黑马上的原因。


裁员过程


裁员过程非常简单和迅速,并且在毫不知情的情况下进行。


在周四的时候,公司的高层提示所有的会议今天全部取消,并且把应该 11 点开的全公司会议提前到了 9 点。


因为很多人都没有准备,所以很多人也没有参加。


后来才知道,9 点就是宣布裁员的开始,事后知道裁员比率为 40%。


然后就是各个部门找自己的被裁的员工开会,这个会议通常首先是一个 Zoom 的 Group 会议,说了一堆屁话,就是什么这是不得已的决定呀,什么乱七八糟的东西。


当然,在这个时候你还需要或者期待公司给你什么合理的理由呢?


然后就是 P&C 人员说话,基本上就是每个人 15 分钟的时间单独 Zoom。


这个 15 分钟,主要就是读下文件了,至于 2 个会议上是不是开摄像头,随意。


你愿意开也行,不愿意开也行,反正上面的所有人都心不在焉。我是懒得开,因为和你谈话的人,你都根本不认识。


第二个会议就是 P&C,这个会议主要就是和你说说被裁员后可以有的一些福利和什么的,对我个人来说我更关注的是补偿。


至于 401K 和里面的资金都是可以自行转的,也没啥需要他们说的,了解到补偿就是 6 周工资,不算多也凑合能接受。


负责裁员的人说,还有什么需要问的,我简单的回答了下 All Set 然后 have a nice day 就好了。毕竟他们只是具体做事的人,问他们也问不出个所以然,这有啥的。


裁员之后


裁员之后,感觉所有认识的被裁的同事都是懵的。


开完 15 分钟的 P&C 会议后,基本上电脑和邮箱马上就不能用了。公司貌似说电脑可以自己留着,但是上面的数据会被远程清理掉。


留在公司里面的东西会有人收拾后寄到家里。


我在公司里的办公桌就属于离职型办公桌,简单的来说,上面只有一台不属于我的显示器,另外就是从其他地方拿过来的一盒消毒湿巾,公司里面压根没有我需要的东西。


很多人认为公司禁用账户有点太不讲人情,其实从技术层面来说根本没有什么,因为所有的管理都是 LDAP,直接在 LDAP 上禁用你账户就好了,没啥稀奇的。


中午的时候,被裁的同事都互相留下了手机号码,感觉大家因为我在裁员列表里面感觉有点扯。另外更扯的同事在这个公司工作了 7 年了,也在列表里面(所有 PHP 的基础架构都是他写的和建立的)。


虽然最开始和这个同事有过一些摩擦,但是这个印度的同事真的挺好的,我们都觉得他挺不错,也愿意和他一起共事。


很多人,包括我。都对这个同事感觉不值,也觉得这很扯。


奈何公司的选择就是一些阿谀奉承,天天扯淡的人,比如说那个奇葩的架构师。


没多久,被裁的同事建了一个群,然后把我给拉进去了,主要还是我们组里面的同事,大家希望能够分享一些面试经验和机会,偶尔吐槽下。


在晚上的时候,突然收到另外一个同事的 LinkedIn 好友邀请,他不在这次裁员内。


但是他也被降职了,他本来是 Sr 开发人员和小组长,后来被提拔成架构师了,现在连小组长都不是了。


他和我说,如果需要帮助的话,他会尽量帮忙,并且还给他的一些曾经的招聘专员账号推送给了我。


我也非常感谢他们,虽然经历过,但是也收获了一些朋友,虽然说在美国职场比较难收获朋友,但是也并不是完全这样的。


没有了利益的纠葛,更容易说点实话。


作者:honeymoose
链接:https://juejin.cn/post/7240052076624035901
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android开发中的MVC_MVP_MVVM

前言 MVC、MVP和MVVM是Android开发中常见的架构模式,这三种架构模式都有其独特的优点和适用场景。它们可以帮助开发者更好地组织和管理代码,提高应用程序的可维护性和可测试性,同时也能够提升开发效率和团队协作能力。选择适合的架构模式取决于项目的需求和开...
继续阅读 »

前言


MVC、MVP和MVVM是Android开发中常见的架构模式,这三种架构模式都有其独特的优点和适用场景。它们可以帮助开发者更好地组织和管理代码,提高应用程序的可维护性和可测试性,同时也能够提升开发效率和团队协作能力。选择适合的架构模式取决于项目的需求和开发团队的经验,理解和掌握这些架构模式对于Android开发者来说是非常重要的。现通过一个案例需求分析三种框架


案例需求



查询用户账号信息:用户输入账号,点击按钮可进行查询账号信息,如果查询数据成功,则将数据展示在界面上;如果查询数据失败,则在界面上提示获取数据失败



MVC框架模型


为什么学习MVC?


如果不使用任何框架来实现这个案例需求,则需要实现以下功能:



  • 获取用户输入的信息

  • 展示获取信息成功界面

  • 展示获取信息失败界面

  • 查询用户数据

  • 业务逻辑


代码示例


Bean对象

//账号信息
public class Account {
private String name; //账号名称
private int level; //账号登记

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getLevel() {
return level;
}

public void setLevel(int level) {
this.level = level;
}
}

回调接口

public interface MCallback {
void onSuccess(Account account);
void onFailed();
}

整体业务逻辑

public class NormalActivity extends AppCompatActivity implements View.OnClickListener {

private EditText mEtAccount;
private TextView mTvResult;
private Button mBtGetAccount;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_normal);

initView();
}

private void initView() {
mEtAccount = findViewById(R.id.et_account);
mTvResult = findViewById(R.id.tv_result);
mBtGetAccount = findViewById(R.id.btn_getAccount);
mBtGetAccount.setOnClickListener(this);
}

@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.btn_getAccount:
String userInput = getUserInput();
getAccountData(userInput, new MCallback() {
@Override
public void onSuccess(Account account) {
showSuccessPage(account);
}

@Override
public void onFailed() {
showFailedPage();
}
});
}
}

//获取用户输入的信息
private String getUserInput() {
return mEtAccount.getText().toString();
}

//展示获取数据成功的界面
private void showSuccessPage(Account account) {
mTvResult.setText("用户账号:" + account.getName() + "|"
+ "用户等级:" + account.getLevel());
}

//展示获取数据失败的界面
private void showFailedPage() {
mTvResult.setText("获取数据失败");
}

//模拟查询账号数据
private void getAccountData(String accountName, MCallback callback) {
Random random = new Random();
boolean isSuccess = random.nextBoolean();
if (isSuccess) {
Account account = new Account();
account.setName(accountName);
account.setLevel(100);
callback.onSuccess(account);
} else {
callback.onFailed();
}
}
}

MVC模型简介


MVC是一种经典的架构模式,将应用程序分为三个主要组成部分:模型(Model)、视图(View)和控制器(Controller)。模型负责数据管理和业务逻辑,视图负责用户界面展示,控制器处理用户输入和协调模型与视图之间的交互。


截屏2023-06-07 15.27.41.png



  • Controller:Activity、Fragment

  • View:layout、View控件

  • Model:数据处理(网络请求、SQL等)


MVC代码演练


使用MVC框架实现案例需求的话,需要实现以下功能:



  • MVCActivity(C层):业务逻辑处理、获取用户输入、展示成功页面、展示失败页面

  • MVCModel层(M层):查询账号数据

  • View层(V层):layout



  1. 将数据的获取与界面的展示分离(将查询账号数据从Acitity中分离到Model中即可)

  2. 解决各层之间通信问题(Activity通知Model获取数据,Model通知Activity更新界面)


将查询账号数据抽离到MVCModel中:

public class MVCModel {
//模拟查询账号数据
public void getAccountData(String accountName, MCallback callback) {
Random random = new Random();
boolean isSuccess = random.nextBoolean();
if (isSuccess) {
Account account = new Account();
account.setName(accountName);
account.setLevel(100);
callback.onSuccess(account);
} else {
callback.onFailed();
}
}
}

MVC的优缺点



  • 优点:
    一定程度上实现了Model与View的分离,降低了代码的耦合度。

  • 缺点:
    Controller与View难以完全解耦,并且随着项目复杂度的提升,Controller将越来越臃肿。


MVP框架模型


MVP模型简介


MVP模式在MVC的基础上做了一些改进,将视图和模型之间的直接交互改为通过一个中间层——Presenter来完成。Presenter负责处理视图的逻辑和用户交互,并将数据获取和处理的任务委托给模型。
截屏2023-06-12 16.34.57.png



  1. Model与View不再直接进行通信,而是通过中间层Presenter来实现

  2. Activity的功能被简化,不再充当控制器,主要负责View层面的工作


MVP代码实战


使用MVP框架实现案例需求的话,需要实现以下功能:



  • MVPActivity(V层):获取用户输入、展示成功界面、展示失败界面

  • MVPPresenter(P层):业务逻辑处理

  • MVPModel(M层):查询账号数据



  1. MVPctivity负责提供View层面的功能(采用实现接口的方式)

  2. MVPModel负责提供数据方面的功能

  3. Model与View不再进行直接通信,通过Presenter来实现


IMVPView接口设计

public interface IMVPView {
String getUserInput();
void showSuccessPage(Account account);
void showFailedPage();
}

MVPModel设计

public class MVPModel {
//模拟查询账号数据
public void getAccountData(String accountName, MCallback callback) {
Random random = new Random();
boolean isSuccess = random.nextBoolean();
if (isSuccess) {
Account account = new Account();
account.setName(accountName);
account.setLevel(100);
callback.onSuccess(account);
} else {
callback.onFailed();
}
}
}

MVPPresenter设计

public class MVPPresenter {
private IMVPView imvpView;
private MVPModel mvpModel;

public MVPPresenter(IMVPView imvpView) {
this.imvpView = imvpView;
mvpModel = new MVPModel();
}

public void getData(String accountName) {
mvpModel.getAccountData(accountName, new MCallback() {
@Override
public void onSuccess(Account account) {
imvpView.showSuccessPage(account);
}

@Override
public void onFailed() {
imvpView.showFailedPage();
}
});
}
}

MVP的优缺点



  • 优点:解决了MVC中Controller与View过度耦合的缺点,职责划分明显,更加易于维护

  • 缺点:接口数量多,项目复杂度升高。随着项目复杂度的提升,Presenter层将越来越臃肿。


使用MVP的建议:



  1. 接口规范化(封装父类接口以减少接口的使用量)

  2. 使用第三方插件自动生成MVP代码

  3. 对于一些简单的界面,可以选择不使用框架

  4. 根据项目复杂度,部分模块可以选择不使用接口


MVVM框架模型


MVVM模型简介


MVVM模式进一步改进了MVP模式,引入了一个新的组件——ViewModel。ViewModel与视图进行双向绑定,负责处理视图的状态和逻辑,同时也能够监听模型的变化。这种双向绑定的机制使得视图与数据的同步更加方便,减少了手动更新视图的代码量。


截屏2023-06-13 11.27.10.png



  1. 减少了接口数量

  2. 告别繁琐findViewById操作


DataBinding学习


DataBinding是谷歌官方发布的一个实现数据绑定的框架(实现数据与视图的双向绑定),DataBinding可以帮助我们在安卓中更好的实现MVVM模式。


DataBinding使用步骤



  1. 启用DataBinding

  2. 修改布局文件为DataBinding布局

  3. 数据绑定


DataBinding实战


在 build.gradle(app)的android中启动DataBinding

dataBinding {
enabled = true
}

alt+enter或option+enter修改布局为DataBinding布局

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

<data>

<variable
name="account"
type="com.example.mdemo.bean.Account" />
<variable
name="activity"
type="com.example.mdemo.databinding.DemoActivity" />
</data>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context=".databinding.DemoActivity">

<TextView
android:id="@+id/tv_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:gravity="center"
android:text="@{account.name+'|'+account.level}" />

<Button
android:id="@+id/btn_addLevel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:onClick="@{activity.onclick}"
android:text="账号等级+1" />
</LinearLayout>
</layout>

数据绑定

public class DemoActivity extends AppCompatActivity {

private Account account;
private ActivityDemoBinding binding;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_demo);
account = new Account();
account.setName("TEST");
account.setLevel(100);
binding.setAccount(account);
binding.setActivity(this);
}

public void onclick(View view) {
Toast.makeText(this, "点击了", Toast.LENGTH_SHORT).show();
int level = account.getLevel();
account.setLevel(level + 1);
binding.setAccount(account);
}
}

代码优化


为减少binding.setAccount赋值实现数据更新的代码冗余操作,可借助BaseObservable接口、Bindable注解及notifyPropertyChanged实现数据自动更新操作

public class Account extends BaseObservable {
private String name; //账号名称
private int level; //账号登记

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Bindable
public int getLevel() {
return level;
}

public void setLevel(int level) {
this.level = level;
notifyPropertyChanged(BR.level);
}
}

MVVM代码实战


使用MVVM框架实现案例需求的话,需要实现以下功能:



  • layout(V层):获取用户输入、展示成功界面、展示失败界面

  • MVVMViewModel(VM层):业务逻辑处理、数据更新

  • MVVMModel层(M层):查询账号数据



  1. 提供View、ViewModel以及Model三层

  2. 将布局修改为DataBinding布局

  3. View与ViewModel之间通过DataBinding进行通信

  4. 获取数据并展示在界面上


MVVMDataBinding布局

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

<data>
<variable
name="viewModel"
type="com.example.mdemo.mvvm.MVVMViewModel" />

</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".mvvm.MVVMActivity">

<EditText
android:id="@+id/et_account"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入要查询的账号"
android:layout_marginTop="30dp"
android:paddingLeft="5dp"
android:paddingRight="5dp"
android:text="@={viewModel.userInput}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

<Button
android:id="@+id/btn_getAccount"
android:text="查询用户信息"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:onClick="@{viewModel.getData}"
app:layout_constraintTop_toTopOf="@id/et_account"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

<TextView
android:id="@+id/tv_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.result}"
app:layout_constraintTop_toTopOf="@+id/btn_getAccount"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="120dp" />

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

MVVMViewModel设计

public class MVVMViewModel extends BaseObservable {
private MVVMModel mvvmModel;
private String userInput;
private ActivityMvvmactivityBinding binding;
private String result;

//一般需要传入Application对象,方便在ViewModel中使用application,
//比如sharedpreferences需要使用
public MVVMViewModel(Application application) {
mvvmModel = new MVVMModel();
}

public MVVMViewModel(Application application, ActivityMvvmactivityBinding binding) {
mvvmModel = new MVVMModel();
this.binding = binding;
}

@Bindable
public String getResult() {
return result;
}

public void setResult(String result) {
this.result = result;
notifyPropertyChanged(BR.result);
}

@Bindable
public String getUserInput() {
return userInput;
}

public void setUserInput(String userInput) {
this.userInput = userInput;
notifyPropertyChanged(BR.userInput);
}


public void getData(View view) {
// String userInput = binding.etAccount.getText().toString();
mvvmModel.getAccountData(userInput, new MCallback() {
@Override
public void onSuccess(Account account) {
String info = account.getName() + "|" + account.getLevel();
setResult(info);
}

@Override
public void onFailed() {
setResult("获取数据失败");
}
});
}
}

MVVMActivity

public class MVVMActivity extends AppCompatActivity {

private ActivityMvvmactivityBinding binding;
private MVVMViewModel mvvmViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this,R.layout.activity_mvvmactivity);
mvvmViewModel = new MVVMViewModel(getApplication(),binding);
binding.setViewModel(mvvmViewModel);
}
}

LiveData+ViewModel



  • LiveData是一个可以被观察的数据持有者,它可以通过添加观察者的方式来让其他组件观察它的变更

  • LiveData遵从应用程序的生命周期(如果LiveData的观察者已经是销毁状态,LiveData就不会通知该观察者)


MVVM的优缺点



  • 优点:实现了数据和视图的双向绑定,极大的简化代码

  • 缺点:bug难以调试,并且dataBinding目前还存在一些编译问题


总结



  • MVC:学习简单但是解耦不够彻底

  • MVP:解耦更加彻底,学习相对简单,但是代码相对繁琐

  • MVVM:代码逻辑简介,但是学习成本较大

作者:Coward881
链接:https://juejin.cn/post/7244050872127193146
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

从Kotlin中return@forEach了个寂寞

今天在Review(copy)同事代码的时候,发现了一个问题,想到很久之前,自己也遇到过这个问题,那么就来看下吧。首先,我们抽取最小复现代码。(1..7).forEach { if (it == 3) { return@forEach ...
继续阅读 »

今天在Review(copy)同事代码的时候,发现了一个问题,想到很久之前,自己也遇到过这个问题,那么就来看下吧。首先,我们抽取最小复现代码。

(1..7).forEach {
if (it == 3) {
return@forEach
}
Log.d("xys", "Num: $it")
}

�很简单的代码,我相信很多人都这样写过,实际上就是遍历的过程中,满足条件后就退出遍历,那么上面的代码,能实现这样的需求吗?我们来看下执行结果。

Num: 1
Num: 2
Num: 4
Num: 5
Num: 6
Num: 7

很遗憾,即使等于3之后就return了,但是然并卵,遍历依然继续执行了。相信很多写Kotlin的开发者都遇到过这个问题,其原因,还是在于语法的思维定势,我们在Kotlin的文档上,可以找到非常明确的解释。
kotlinlang.org/docs/return…


我们先来看下Kotlin中forEach的源码。

/**
* Performs the given [action] on each element.
*/
@kotlin.internal.HidesMembers
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}

�我们来提取下关键信息:



  • 内联函数

  • 高阶函数


发现了吗,由于高阶函数的存在,当你在高阶函数的闭包内「return」时,是结束的整个函数,当你使用「return@forEach�」时,是结束当前的闭包,所以,如果你像这样写:

(1..7).forEach {
if (it == 3) {
return
}
Log.d("xys", "Num: $it")
}

那么等于3之后,整个函数就被return了,那么如果你像文章开头这样写,那么等效于continue,因为你结束了当前的闭包,而这个闭包只是其中的一次遍历过程。那么我们要如何实现我们最初的需求呢?看到这样,答案其实已经呼之欲出了,那就是要return整个遍历的闭包。所以,官方也给出了解决方案,那就是外面套一层闭包:

run loop@{
(1..7).forEach {
if (it == 3) {
return@loop
}
Log.d("xys", "Num: $it")
}
}

写起来确实是麻烦一点,但这却是必不可少的过程,是引入闭包所带来的一点副作用。



当然这里不仅限于run,任何闭包都是可以的。



欢迎大家关注我的公众号——【群英传】,专注于「Android」「Flutter」「Kotlin」
我的语雀知识库——http://www.yuque.com/xuyisheng


作者:xuyisheng
链接:https://juejin.cn/post/7243819009866235964
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

[Android] 弱网检测示例

使用ConnectivityManager类来检查当前的网络状态和连接类型 使用以下代码来检查当前是否有可用的网络连接:ConnectivityManager cm = (ConnectivityManager) getSystemService(Contex...
继续阅读 »

使用ConnectivityManager类来检查当前的网络状态和连接类型


使用以下代码来检查当前是否有可用的网络连接:

ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
boolean isConnected = activeNetwork != null && activeNetwork.isConnectedOrConnecting();

您还可以使用ConnectivityManager的getNetworkCapabilities()方法来检查当前网络的能力。例如,您可以使用以下代码来检查当前网络是否支持较高的下载速度:

ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkCapabilities nc = cm.getNetworkCapabilities(cm.getActiveNetwork());
if (nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
// 这是一个蜂窝网络
if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_LTE)) {
// 快
} else {
// 慢
}
}

使用Ping或者Traceroute命令来检测网络质量


然后,您可以使用Java的ProcessBuilder类来执行Ping命令。例如,您可以使用以下代码来Ping http://www.baidu.com

// ProcessBuilder pb = new ProcessBuilder("ping", "-c", "4", "www.baidu.com");
ProcessBuilder pb = new ProcessBuilder("traceroute", "-n", "www.baidu.com");
pb.redirectErrorStream(true);
Process process = pb.start();

// 读取命令的输出
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
// todo:处理输出行
}

请注意,需要在应用的主线程之外运行此代码,因为它会阻塞线程。您可以使用协程或其他方法来在后台线程上执行Ping/Traceroute命令。


执行Ping命令后,您可以从输出中提取有关网络质量的信息。例如,您可以查找"time="字符串来获取每个Ping请求的延迟(单位是毫秒)。您还可以查找"packet loss"字符串来获取丢包率。


执行Traceroute命令后,您可以从输出中提取有关网络路径的信息。例如,每行输出都包含一个路由器的IP地址和延迟(单位是毫秒)。您可以使用此信息来了解数据在网络中的传输路径,以及在路径上的延迟。


最后,请注意,使用Ping/Traceroute命令进行网络测量可能会对网络造成轻微负载,并且在某些情况下可能会被防火墙阻止。因此,您应该谨慎使用此方法。


为了在网络状况较差的情况下优化Android应用的性能,您可以考虑以下几点:



  1. 使用缓存:缓存常用的数据和资源可以帮助您的应用在没有网络连接的情况下也能工作。您可以使用Android提供的缓存API,或者使用第三方库来管理缓存。

  2. 使用离线模式:如果您的应用依赖于网络连接来提供内容,那么您可以考虑让用户在离线时也能使用应用。这可以通过在线时下载内容并在离线时使用来实现。

  3. 使用后台同步:如果您的应用需要定期与服务器同步数据,那么您可以使用Android的后台同步机制来实现。这样,当网络状况较差时,您的应用就可以在后台自动同步数据,而不会影响用户体验。

  4. 优化网络请求:对于使用网络加载数据的应用,可以考虑优化网络请求。这可以通过减少不必要的请求,使用压缩传输,以及使用较快的网络连接来实现。

  5. 使用较少的数据:尽量减少应用使用的数据量可以帮助您的应用在网络状况较差的情况下更好地运行。这可以通过优化图像大小,使用缓存和离线模式,以及减少不必要的数据传输来实现。

  6. 使用较快的网络连接:如果您的应用需要使用网络,那么使用较快的网络连接可以帮助您的应用在网络状况较差的情况下更好地运行。例如,您可以使用Wi-Fi而不是移动数据连接,或者在有多个可用网络连接时选择速度较快的连接。

  7. 使用网络状态监测:通过监测网络状态,您的应用可以根据当前的网络状态调整其行为。例如,您可以在网络状态较差时显示提示信息,或者在网络不可用时禁用某些功能。

  8. 尝试使用新的网络技术:最新的移动设备和操作系统通常都会提供更多的网络优化功能。例如,Android 11引入了网络请求优化工具,可以帮助您的应用在网络状况较差的情况下更好地运行

作者:3_7
链接:https://juejin.cn/post/7239340461235142715
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

浅析一下:kotlin委托背后的实现机制

大家好,kotlin的属性委托、类委托、lazy等委托在日常的开发中,给我们提供了很大的帮助,我之前的文章也是有实战过几种委托。不过对比委托实现的背后机制一直都没有分析过,所以本篇文章主要是带领大家分析下委托的实现原理,加深对kotlin的理解。 一. laz...
继续阅读 »

大家好,kotlin的属性委托、类委托、lazy等委托在日常的开发中,给我们提供了很大的帮助,我之前的文章也是有实战过几种委托。不过对比委托实现的背后机制一直都没有分析过,所以本篇文章主要是带领大家分析下委托的实现原理,加深对kotlin的理解。


一. lazy委托


这里我们不说用法,直接说背后的实现原理。


先看一段代码:


val content: String by lazy {
"oiuytrewq"
}

fun main() {
println(content.length)
}

我们看下反编译后的java代码:




  1. 首先会通过DelegateDemoKt静态代码块饿汉式的方式创建一个Lazy类型的变量content$delegate,命名的规则即代码中定义的原始变量值拼接上$delegate,我们原始定义的content变量就会从属性定义上消失,但会生成对应的get方法,即getContent()



  1. 当我们在main方法中调用content.length时,其实就是调用getContent().length(),而getContent()最终是调用了content$delegate.getValue方法;



  1. 这个lazy类型的变量是调用了LazyKt.lazy()方法创建,而真正的核心逻辑——该方法具体参数的传入,在反编译的java代码中并没有体现;


java代码既然看不到,我们退一步看下字节码:



上面是DelegateDemoKt类构造器对应的字节码,其中就是获取了DelegateDemoKt$content$2作为参数传入了LazyKt.lazy()方法。


我们看下DelegateDemoKt$content$2类的实现字节码:



DelegateDemoKt$content$2类实现了Function0接口,所以上面lazy的真正实现逻辑就是DelegateDemoKt$content$2类的invoke方法中,上图的字节码红框圈出的地方就很直观的看出来了。


二. 属性委托


属性委托的委托类就是指实现了ReadWritePropertyReadOnlyProperty接口的类,像官方提供的Delegates.observable()Delegates.vetoable()这两个api也是借助前面两个接口实现的。这里我们就以支持读写的ReadWriteProperty委托接口进行举例分析。


先看一段例子代码:


var age: Int by object : ReadWriteProperty<Any?, Int> {
override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
return 10
}

override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
val v = value * value
println("setValue: $v")
}
}

fun main() {
age = 4
println(age)
}

我们看下反编译的java代码:


public final class DelegateDemoKt {
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{Reflection.mutableProperty0(new MutablePropertyReference0Impl(DelegateDemoKt.class, "age", "getAge()I", 1))};
@NotNull
private static final <undefinedtype> age$delegate = new ReadWriteProperty() {
@NotNull
public Integer getValue(@Nullable Object thisRef, @NotNull KProperty property) {
Intrinsics.checkNotNullParameter(property, "property");
return 10;
}

public void setValue(@Nullable Object thisRef, @NotNull KProperty property, int value) {
Intrinsics.checkNotNullParameter(property, "property");
int v = value * value;
String var5 = "setValue: " + v;
System.out.println(var5);
}
};

public static final int getAge() {
return age$delegate.getValue((Object)null, $$delegatedProperties[0]);
}

public static final void setAge(int var0) {
age$delegate.setValue((Object)null, $$delegatedProperties[0], var0);
}

public static final void main() {
setAge(4);
int var0 = getAge();
System.out.println(var0);
}
}


  1. 和lazy有些类似,会生成一个实现了ReadWriteProperty接口的匿名类变量age$delegate,命名规则和lazy相同,通过还帮助我们生成了对应的getAgesetAge方法;



  1. 当我们在代码中执行age = 4就会调用setAge(4)方法,最终会调用age$delegate.setValue()方法;类似的调用age就会调用getAge(),最终调用到age$delegate.getValue()方法;



  1. 编译器还通过反射帮助我们生成了一个KProperty类型的$$delegatedProperties变量,主要是ReadWritePropertysetValuegetValue方法都需要传入这样一个类型的对象,通过$$delegatedProperties变量我们可以访问到具体的变量名等信息;




类似的还有一种属性委托,我们看下代码:


val map = mutableMapOf<String, Int>()

val name: Int by map

上面代码的意思是:当访问name时,就会从map这个散列表中获取key为"name"的value值并返回,不存在就直接抛异常,接下来我们看下反编译后的java代码:


public final class DelegateDemoKt {
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{Reflection.property0(new PropertyReference0Impl(DelegateDemoKt.class, "name", "getName()I", 1))};
@NotNull
private static final Map map = (Map)(new LinkedHashMap());
@NotNull
private static final Map name$delegate;

static {
name$delegate = map;
}

public static final int getName() {
Map var0 = name$delegate;
Object var1 = null;
KProperty var2 = $$delegatedProperties[0];
return ((Number)MapsKt.getOrImplicitDefaultNullable(var0, var2.getName())).intValue();
}
}


  1. 生成一个Map类型的name$delegate变量,这个变量其实就是我们定义的map散列表;



  1. 通过反射生成了一个KProperty类型对象变量$$delegatedProperties,通过这个对象的getName()我们就能拿到变量名称,比如这里的"name"变量名;



  1. 最终调用了MapsKt.getOrImplicitDefaultNullable方法,去map散列表去查找"name"这个key对应的value;



PS:记得kotlin1.6还是1.7的插件版本对应委托进行了优化,这个后续的文章会再进行讲解。



三. 类委托


类委托实现就比较简单了,这里我们看下样例代码:


fun interface Fruit {
fun type(): Int
}

class FruitProxy(private val model: Fruit) : Fruit by model

fun main() {
val proxy: FruitProxy = FruitProxy {
-1
}
println(proxy.type())
}

反编译成java代码看下:





首先我们看下FruitProxy这个类,其实现了Fruit接口,借助属性委托特性,编译器会自动帮助我们生成type() 接口方法的实现,并再其中调用构造方法传入的委托类对象modeltype()方法,类委托的核心逻辑就这些。


再main()方法中构造FruitProxy时,我们也无法知晓具体的构造参数对象是啥,和上面的lazy一样,我们看下字节码:



其实FruitProxy方法就传入了一个DelegateDemoKt$main$proxy$1类型的对象,并实现了Fruit接口重写了type方法。


总结


本篇文章主要是讲解了三种委托背后的实现原理,有时候反编译字节码看不出来原理的,可以从字节码中寻找答案,希望本篇文章能对你有所帮助。


历史文章


这里是我整理的过往kotlin特性介绍的历史文章,大家感兴趣可以阅读下:


Kotlin1.9.0-Beta,它来了!!


聊聊Kotlin1.7.0版本提供的一些特性


聊聊kotlin1.5和1.6版本提供的一些新特性


kotlin密封sealed class/interface的迭代之旅


优化@BuilderInference注解,Kotlin高版本下了这些“毒手”!


@JvmDefaultWithCompatibility优化小技巧

,了解一下~

收起阅读 »

父母在家千万注意别打开“共享屏幕”,银行卡里的钱一秒被转走......

打开屏幕共享,差点直接被转账 今天和爸妈聊天端午回家的事情,突然说到最近AI诈骗的事情,千叮咛万嘱咐说要对方说方言才行,让他们充分了解一下现在骗子诈骗的手段,顺便也找了一下骗子还有什么其他的手段,打算一起和他们科普一下,结果就发现下面这一则新闻: 在辽宁大连务...
继续阅读 »

打开屏幕共享,差点直接被转账


今天和爸妈聊天端午回家的事情,突然说到最近AI诈骗的事情,千叮咛万嘱咐说要对方说方言才行,让他们充分了解一下现在骗子诈骗的手段,顺便也找了一下骗子还有什么其他的手段,打算一起和他们科普一下,结果就发现下面这一则新闻:


在辽宁大连务工的耿女士接到一名自称“大连市公安局民警”的电话,称其涉嫌广州一起诈骗案件,让她跟广州警方对接。耿女士在加上所谓的“广州警官”的微信后,这位“警官”便给耿女士发了“通缉令”,并要求耿女士配合调查,否则将给予“强制措施”。随后,对方与耿女士视频,称因办案需要,要求耿女士提供“保证金”,并将所有存款都集中到一张银行卡上,再把钱转到“安全账户”。


图片


期间,通过 “屏幕共享”,对方掌握了耿女士银行卡的账号和密码。耿女士先后跑到多家银行,取出现金,将钱全部存到了一张银行卡上。正当她打算按照对方指示,进行下一步转账时,被民警及时赶到劝阻。在得知耿女士泄露了银行卡号和密码后,银行工作人员立即帮助耿女士修改了密码,幸运的是,银行卡的近6万元钱没有受到损失。


就这手段,我家里的老人根本无法预防,除非把手机从他们手里拿掉,与世隔绝还差不多,所以还是做APP的各大厂商努力一下吧!


希望各大厂商都能看看下面这个防劫持SDK,让出门在外打工的我们安心一点。


防劫持SDK


一、简介


防劫持SDK是具备防劫持兼防截屏功能的SDK,可有效防范恶意程序对应用进行界面劫持与截屏的恶意行为。


二、iOS版本


2.1 环境要求


条目说明
兼容平台iOS 8.0+
开发环境XCode 4.0 +
CPU架构armv7, arm64, i386, x86_64
SDK依赖libz, libresolv, libc++

2.2 SDK接入


2.2.1 DxAntiHijack获取

官网下载SDK获取,下面是SDK的目录结构


1.png


DXhijack_xxx_xxx_xxx_debug.zip 防劫持debug 授权集成库 DXhijack_xxx_xxx_xxx_release.zip 防劫持release 授权集成库




  • 解压DXhijack_xxx_xxx_xxx_xxx.zip 文件,得到以下文件




    • DXhijack 文件夹



      • DXhijack.a 已授权静态库

      • Header/DXhijack.h 头文件

      • dx_auth_license.description 授权描述文件

      • DXhijackiOS.framework 已授权framework 集成库






2.2.2 将SDK接入XCode

2.2.2.1 导入静态库及头文件

将SDK目录(包含静态库及其头文件)直接拖入工程目录中,或者右击总文件夹添加文件。 或者 将DXhijackiOS.framework 拖进framework存放目录


2.2.2.2 添加其他依赖库

在项目中添加 libc++.tbd 库,选择Target -> Build Phases,在Link Binary With Libraries里点击加号,添加libc++.tbd


2.2.2.3 添加Linking配置

在项目中添加Linking配置,选择Target -> Build Settings,在Other Linker Flags里添加-ObjC配置


2.3 DxAntiHijack使用


2.3.1 方法及参数说明

@interface DXhijack : NSObject

+(void)addFuzzy; //后台模糊效果
+(void)removeFuzzy;//后台移除模糊效果
@end

2.3.2 使用示例

在对应的AppDelegate.m 文件中头部插入


#import "DXhijack.h"

//在AppDelegate.m 文件中applicationWillResignActive 方法调用增加
- (void)applicationWillResignActive:(UIApplication *)application {
[DXhijack addFuzzy];
}

//在AppDelegate.m 文件中applicationDidBecomeActive 方法调用移除
- (void)applicationDidBecomeActive:(UIApplication *)application {
[DXhijack removeFuzzy];
}


三、Android版本


3.1 环境要求


条目说明
开发目标Android 4.0+
开发环境Android Studio 3.0.1 或者 Eclipse + ADT
CPU架构ARM 或者 x86
SDK三方依赖

3.2 SDK接入


3.2.1 SDK获取


  1. 访问官网,注册账号

  2. 登录控制台,访问“全流程端防控->安全键盘SDK”模块

  3. 新增App,填写相关信息

  4. 下载对应平台SDK


3.2.2 SDK文件结构



  • SDK目录结构 android-dx-hijack-sdk.png



    • dx-anti-hijack-${version}.jar Android jar包

    • armeabiarmeabi-v7aarm64-v8ax86 4个abi平台的动态库文件




3.2.3 Android Studio 集成

点击下载Demo


3.2.3.1 Android Studio导入jar, so

把dx-anti-hijack-x.x.x.jar, so文件放到相应模块的libs目录下


android-dx-hijack-as.png



  • 在该Module的build.gradle中如下配置:


 android{
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}

repositories{
flatDir{
dirs 'libs'
}
}
}


dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
}



3.2.3.2 权限声明

Android 5.0(不包含5.0)以下需要在项目AndroidManifest.xml文件中添加下列权限配置:


<uses-permission android:name="android.permission.GET_TASKS"/>

3.2.3.3 混淆配置

-dontwarn *.com.dingxiang.mobile.**
-dontwarn *.com.mobile.strenc.**
-keep class com.security.inner.**{*;}
-keep class *.com.dingxiang.mobile.**{*;}
-keep class *.com.mobile.strenc.**{*;}
-keep class com.dingxiang.mobile.antihijack.** {*;}

3.3 DxAntiHijack 类使用


3.3.1 方法及参数说明

3.3.1.1 初始化


建议在Application的onCreate下調用


/**
* 使用API前必須先初始化
* @param context
*/

public static void init(Context context);

3.3.1.2 反截屏功能


/**
* 反截屏功能
* @param activity
*/

public static void DGCAntiHijack.antiScreen(Activity activity);

/**
* 反截屏功能
* @param dialog
*/

public static void DGCAntiHijack.antiScreen(Dialog dialog);

3.3.1.3 反劫持检测


/**
* 调用防劫持检测,通常现在activity的onPause和onStop调用
* @return 是否存在被劫持风险
*/

public static boolean DGCAntiHijack.antiHijacking();

3.3.2 使用示例

//使用反劫持方法
@Override
protected void onPause() {
boolean safe = DXAntiHijack.antiHijacking();
if(!safe){
Toast.makeText(getApplicationContext(), "App has entered the background", Toast.LENGTH_LONG).show();
}
super.onPause();
}

@Override
protected void onStop() {
boolean safe = DXAntiHijack.antiHijacking();
if(!safe){
Toast.makeText(getApplicationContext(), "App has entered the background", Toast.LENGTH_LONG).show();
}
super.onStop();
}



//使用反截屏方法
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DXAntiHijack.antiScreen(MainActivity.this);
}

以上。


结语


这种事情层出不穷,真的不是吾等普通民众能解决的,最好有从上至下的政策让相应的厂商(尤其是银行和会议类的APP)统一做处理,这样我们在外

作者:昀和
来源:juejin.cn/post/7242145254057312311
打工的人才能安心呀。

收起阅读 »

Spring Boot定时任务详解与案例代码

概述 Spring Boot是一个流行的Java开发框架,它提供了许多便捷的特性来简化开发过程。其中之一就是定时任务的支持,让开发人员可以轻松地在应用程序中执行定时任务。本文将详细介绍如何在Spring Boot中使用定时任务,并提供相关的代码示例。 实际案例...
继续阅读 »

image.png





概述


Spring Boot是一个流行的Java开发框架,它提供了许多便捷的特性来简化开发过程。其中之一就是定时任务的支持,让开发人员可以轻松地在应用程序中执行定时任务。本文将详细介绍如何在Spring Boot中使用定时任务,并提供相关的代码示例。


实际案例


在Spring Boot中,使用定时任务非常简单。首先,需要在应用程序的入口类上添加@EnableScheduling注解,以启用定时任务的支持。该注解将告诉Spring Boot自动配置并创建一个线程池来执行定时任务。


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

一旦启用了定时任务支持,就可以在任何Spring管理的Bean中创建定时任务。可以通过在方法上添加@Scheduled注解来指定定时任务的执行规则。下面是一个简单的示例,演示了每隔一分钟执行一次的定时任务:


import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class MyScheduledTask {

@Scheduled(cron = "0 * * * * *") // 每分钟执行一次
public void executeTask() {
// 在这里编写定时任务的逻辑
System.out.println("定时任务执行中...");
}
}

在上面的示例中,我们创建了一个名为MyScheduledTask的组件,并在其中定义了一个名为executeTask的方法。通过使用@Scheduled(cron = "0 * * * * *")注解,我们指定了该方法应该每分钟执行一次。当定时任务触发时,executeTask方法中的逻辑将被执行。


需要注意的是,@Scheduled注解支持不同的任务触发方式,如基于固定延迟时间、固定间隔时间或cron表达式等。可以根据实际需求选择适合的方式。


以上就是使用Spring Boot进行定时任务的基本示例。通过简单的注解配置,您可以轻松地在应用程序中添加和管理定时任务。希望本文能对您理解和使用Spring Boot定时任务提供帮助。


总结


Spring Boot提供了便捷的方式来实现定时任务。通过添加@EnableScheduling注解来启用定时任务支持,并使用@Scheduled注解来指定任务的执行规则。可以根据需求选择不同的触发方式。


除了上述基本示例外,Spring Boot还提供了更多高级功能和配置选项,以满足更复杂的定时任务需求。



  1. 方法参数和返回值:您可以在定时任务方法中添加参数和返回值,Spring Boot会自动注入合适的值。例如,可以将java.util.Date类型的参数添加到方法中,以获取当前时间。返回值可以是voidjava.util.concurrent.Futurejava.util.concurrent.CompletableFuture等类型。

  2. 并发执行和线程池配置:默认情况下,Spring Boot的定时任务是串行执行的,即每个任务完成后再执行下一个任务。如果需要并发执行任务,可以通过配置线程池来实现。可以在application.propertiesapplication.yml文件中设置相关的线程池属性,如核心线程数、最大线程数和队列容量等。

  3. 异常处理:定时任务可能会抛出异常,因此需要适当处理异常情况。您可以使用@Scheduled注解的exceptionHandler属性来指定异常处理方法,以便在任务执行过程中捕获和处理异常。

  4. 动态调度:有时需要根据运行时的条件来动态调整定时任务的触发时间。Spring Boot提供了TaskScheduler接口和CronTrigger类,您可以使用它们来在运行时动态设置定时任务的执行规则。

  5. 集群环境下的定时任务:如果应用程序部署在多个节点的集群环境中,可能会遇到定时任务重复执行的问题。为了避免这种情况,可以使用分布式锁机制,如Redis锁或数据库锁,来确保只有一个节点执行定时
    作者:百思不得小赵
    来源:juejin.cn/post/7244089396567638072
    任务。

收起阅读 »

从 数据工程 到 Prompt 工程

动动发财的小手,点个赞吧! 数据工程构成了数据科学过程的很大一部分。在 CRISP-DM 中,这个过程阶段称为“数据准备”。它包括数据摄取、数据转换和数据质量保证等任务。在本文章中,我们使用 ChatGPT 和 Python 解决了典型的数据工程任务。通过这...
继续阅读 »

动动发财的小手,点个赞吧!



数据工程构成了数据科学过程的很大一部分。在 CRISP-DM 中,这个过程阶段称为“数据准备”。它包括数据摄取、数据转换和数据质量保证等任务。在本文章中,我们使用 ChatGPT 和 Python 解决了典型的数据工程任务。通过这样做,我们探索了数据工程与提示工程新学科之间的联系。


简介


2022 年 5 月,斯蒂芬·沃尔夫勒姆 (Stephen Wolfram) 和莱克斯·弗里德曼 (Lex Fridman) 发表了题为“编程已死吗?”的深刻见解。他们讨论了未来开发者是否还会使用高级语言。根据 Wolfram 的说法,许多编程任务可以通过大型语言模型 (LLM) 实现自动化。在撰写本文时,此类模型最突出的例子是 ChatGPT。自 2022 年底推出以来,它产生了惊人的效果。指定要由 LLM 执行的操作称为“提示工程”。如果 Wolfram 是对的,至少软件开发的一部分将从编写代码转向编写提示。


在数据科学方面,数据准备可能是一项耗时且乏味的任务。那么,为什么不尝试使用 LLM 使其自动化呢?在以下部分中,我们将使用 ChatGPT 和 Python 解决不同的数据工程问题。我们没有自己编写 Python 代码,而是使用提示工程来生成它。我们的实验是在 2023 年 5 月 19 日基于当时最新的免费 ChatGPT 版本 (GPT-3.5) 进行的。


创建数据框


让我们从一个简单的问题开始,并从样本数据集创建一个 Pandas 数据框。表 1 包含例如世界银行提供的国家指标。


image-20230524153840794


为了从上面的示例中创建数据框,我们开始了与 ChatGPT 的新对话并发布了以下提示:


Create a Pandas table with the following data:
Continent, Country, Year, Category, Variable, Value
Asia, China, 2022, Economy, GDP, 18.1
Asia, China, 2022, Demographics, Population, 1412
North America, USA, 2022, Economy, GDP, 25.5
North America, USA, 2022, Demographics, Population, 333
Europe, Germany, 2022, Economy, GDP, 3.9
Europe, Germany, 2022, Demographics, Population, 84

作为响应,该模型生成了以下 Python 代码:


import pandas as pd

data = {
'Continent': ['Asia', 'Asia', 'North America', 'North America', 'Europe', 'Europe'],
'Country': ['China', 'China', 'USA', 'USA', 'Germany', 'Germany'],
'Year': [2022, 2022, 2022, 2022, 2022, 2022],
'Category': ['Economy', 'Demographics', 'Economy', 'Demographics', 'Economy', 'Demographics'],
'Variable': ['GDP', 'Population', 'GDP', 'Population', 'GDP', 'Population'],
'Value': [18.1, 1412, 25.5, 333, 3.9, 84]
}

df = pd.DataFrame(data)
print(df)

运行脚本时,我们看到输出符合我们的规范:



尽管我们在实验中使用了示例,但下一节中生成的代码原则上也可以处理更大的数据集。


旋转数据框


对于数据科学,我们在第一个提示中创建的表结构并不是最优的。相反,所谓的“平板”会更方便。在表 1 中,指标“GDP”和“人口”分别用名为“Variable”和“Value”的两列表示。相比之下,平面表格将为每个指标包含单独的列,如表 2 所示。



将表 1 的格式转换为表 2 的格式称为“透视”。让我们尝试使用 ChatGPT 来解决这个任务。为此,我们在对话中发布了第二个提示:


Transform the table into the following format:
Continent, Country, Year, GDP, Population
Asia, China, 2022, 18.1, 1412
North America, USA, 2022, 25.5, 333
Europe, Germany, 2022, 3.9, 84

上述消息基于所需数据格式的示例。或者,我们也可以纯口头表达我们的要求。与经典编程一样,在提示工程中有实现目标的替代解决方案。在不久的将来,寻找最佳方式将是一项激动人心的任务。根据我们的提示,ChatGPT对Python代码进行了如下调整:


import pandas as pd

data = {
'Continent': ['Asia', 'Asia', 'North America', 'North America', 'Europe', 'Europe'],
'Country': ['China', 'China', 'USA', 'USA', 'Germany', 'Germany'],
'Year': [2022, 2022, 2022, 2022, 2022, 2022],
'Category': ['Economy', 'Demographics', 'Economy', 'Demographics', 'Economy', 'Demographics'],
'Variable': ['GDP', 'Population', 'GDP', 'Population', 'GDP', 'Population'],
'Value': [18.1, 1412, 25.5, 333, 3.9, 84]
}

df = pd.DataFrame(data)

# Pivot the table
pivot_df = df.pivot_table(index=['Continent', 'Country', 'Year'], columns='Category', values='Value').reset_index()

# Rename the columns
pivot_df.columns.name =
pivot_df.columns = [column if column != 'Variable' else for column in pivot_df.columns]

print(pivot_df)

然而,运行脚本的第二个版本显示输出不是我们所要求的:


该表包含名为“人口统计”和“经济”的两列,而不是预期的“GDP”和“人口”列。为了解决这个问题,我们发布了下一个提示:


The columns are not correct, you should have used 
the variables GDP and population

作为响应,模型纠正了错误并生成了另一个版本的 Python 代码:


import pandas as pd

data = {
'Continent': ['Asia', 'Asia', 'North America', 'North America', 'Europe', 'Europe'],
'Country': ['China', 'China', 'USA', 'USA', 'Germany', 'Germany'],
'Year': [2022, 2022, 2022, 2022, 2022, 2022],
'Category': ['Economy', 'Demographics', 'Economy', 'Demographics', 'Economy', 'Demographics'],
'Variable': ['GDP', 'Population', 'GDP', 'Population', 'GDP', 'Population'],
'Value': [18.1, 1412, 25.5, 333, 3.9, 84]
}

df = pd.DataFrame(data)

# Pivot the table
pivot_df = df.pivot_table(index=['Continent', 'Country', 'Year'], columns='Variable', values='Value').reset_index()

# Rename the columns
pivot_df.columns.name =
pivot_df.columns = ['Continent', 'Country', 'Year', 'GDP', 'Population']

print(pivot_df)

运行脚本表明这次旋转已正确实现:



富集数据框


另一个典型的数据工程任务是用附加信息丰富数据集。为此,我们要求 ChatGPT 通过发布以下提示将 ISO 国家代码添加到我们的数据中:


For each country, in the table above, add its ISO code

结果,模型调整了响应并返回了以下代码片段:


import pandas as pd

data = {
'Continent': ['Asia', 'Asia', 'North America', 'North America', 'Europe', 'Europe'],
'Country': ['China', 'China', 'USA', 'USA', 'Germany', 'Germany'],
'Year': [2022, 2022, 2022, 2022, 2022, 2022],
'Category': ['Economy', 'Demographics', 'Economy', 'Demographics', 'Economy', 'Demographics'],
'Variable': ['GDP', 'Population', 'GDP', 'Population', 'GDP', 'Population'],
'Value': [18.1, 1412, 25.5, 333, 3.9, 84]
}

iso_codes = {
'China': 'CN',
'USA': 'US',
'Germany': 'DE'
}

df = pd.DataFrame(data)

# Add ISO code column
df['ISO Code'] = df['Country'].map(iso_codes)

# Pivot the table
pivot_df = df.pivot_table(index=['Continent', 'Country', 'ISO Code', 'Year'], columns='Variable', values='Value').reset_index()

# Rename the columns
pivot_df.columns.name =
pivot_df.columns = ['Continent', 'Country', 'ISO Code', 'Year', 'GDP', 'Population']

print(pivot_df)

运行脚本显示一个带有 ISO 代码的新列已添加到数据框中。



派生列


接下来让我们通过将一个国家的 GDP 除以其人口规模来得出一个新的列“人均 GDP”。为此,我们发送了以下提示:


Derive a new column "GDP per capita" by dividing the GDP 
of a country by its population size

对此,模型修改了代码如下:


import pandas as pd

data = {
'Continent': ['Asia', 'Asia', 'North America', 'North America', 'Europe', 'Europe'],
'Country': ['China', 'China', 'USA', 'USA', 'Germany', 'Germany'],
'Year': [2022, 2022, 2022, 2022, 2022, 2022],
'Category': ['Economy', 'Demographics', 'Economy', 'Demographics', 'Economy', 'Demographics'],
'Variable': ['GDP', 'Population', 'GDP', 'Population', 'GDP', 'Population'],
'Value': [18.1, 1412, 25.5, 333, 3.9, 84]
}

iso_codes = {
'China': 'CN',
'USA': 'US',
'Germany': 'DE'
}

df = pd.DataFrame(data)

# Add ISO code column
df['ISO Code'] = df['Country'].map(iso_codes)

# Pivot the table
pivot_df = df.pivot_table(index=['Continent', 'Country', 'ISO Code', 'Year'], columns='Variable', values='Value').reset_index()

# Calculate GDP per capita
pivot_df['GDP per capita'] = pivot_df['GDP'] / pivot_df['Population']

# Rename the columns
pivot_df.columns.name =
pivot_df.columns = ['Continent', 'Country', 'ISO Code', 'Year', 'GDP', 'Population', 'GDP per capita']

print(pivot_df)

输出表明脚本产生了预期的结果:



分析数据


一旦数据转换完成,实际的分析就可以开始了。最后让我们计算一下各国GDP与人口的相关系数。为此,我们贴出最后一个提示:


Use the table above to calculate the correlation coefficient between 
the GDP and the population of the countries

结果,模型返回了以下代码片段:


import pandas as pd

data = {
'Continent': ['Asia', 'Asia', 'North America', 'North America', 'Europe', 'Europe'],
'Country': ['China', 'China', 'USA', 'USA', 'Germany', 'Germany'],
'Year': [2022, 2022, 2022, 2022, 2022, 2022],
'Category': ['Economy', 'Demographics', 'Economy', 'Demographics', 'Economy', 'Demographics'],
'Variable': ['GDP', 'Population', 'GDP', 'Population', 'GDP', 'Population'],
'Value': [18.1, 1412, 25.5, 333, 3.9, 84]
}

df = pd.DataFrame(data)

# Pivot the table
pivot_df = df.pivot_table(index=['Continent', 'Country', 'Year'], columns='Variable', values='Value').reset_index()

# Calculate correlation coefficient
correlation_coefficient = pivot_df['GDP'].corr(pivot_df['Population'])

print("Correlation Coefficient: {:.3f}".format(correlation_coefficient))

运行脚本时,我们发现两个变量之间存在很强的正相关性,这并不奇怪:



总结


不可否认,我们的实验是基于一个简单的数据集。然而,结果是显著的。我们执行了几项数据工程任务,而没有编写一行代码。 ChatGPT 不仅能够在大多数情况下正确执行我们的提示。但即使模型犯了错误,它也能够反映和修复错误。与软件开发一样,生成的代码必须经过测试。此外,它可能需要重构和优化。在 AI 时代使用 pylint 仍然是一个好主意。然而,总而言之,我们必须同意 Wolfram 的观点:在未来,数据工程的重要部分将从编码转向提示工程。这种新方法不会取代数据工程师,但会提高他们的效率。


作者:冷冻工厂
来源:juejin.cn/post/7244174500773118009

收起阅读 »

最近遇到的奇葩进度条

web
前言 本文将介绍几个我最近遇到的奇葩进度条,需求看似简单,但是我们可以用平常巧妙的属性来解决,再也不用复杂的html结构和颜色渐变算法。 “奇葩”的环形渐变进度条 需求描述:需要环形渐变的进度条让人快速理解进度实现程度,10-20%是青绿色,20%到30%是黄...
继续阅读 »

前言


本文将介绍几个我最近遇到的奇葩进度条,需求看似简单,但是我们可以用平常巧妙的属性来解决,再也不用复杂的html结构和颜色渐变算法。


“奇葩”的环形渐变进度条


需求描述:需要环形渐变的进度条让人快速理解进度实现程度,10-20%是青绿色,20%到30%是黄色.....


乍一看是不是很容易,但是我思来想去用了echarts的svg渲染,但是只要到了90%,一定会渐变到青绿色,从红色渐变到青绿色,做实让我心一凉。


image.png


思路一:径向渐变分割


网上思路很多,稍微复杂的比如分割区域做大量的颜色的径向渐变。原理是将rgba转为16进制计算颜色插值。这样我们通过计算step步长就可以根据细分做渐变了。但是好像无法很好满足我们的指定区域10%-20%是某种颜色,虽然可以但是也太麻烦了。


  function gradientColor(startRGB, endRGB, step) {
let startR = startRGB[0]
let startG = startRGB[1]
let startB = startRGB[2]
let endR = endRGB[0]
let endG = endRGB[1]
let endB = endRGB[2]
let sR = (endR - startR) / step // 总差值
let sG = (endG - startG) / step
let sB = (endB - startB) / step
var colorArr = []
for (var i = 0; i < step; i++) {
let color = 'rgb(' + parseInt((sR * i + startR)) + ',' + parseInt((sG * i + startG)) + ',' + parseInt((sB * i + startB)) + ')'
colorArr.push(color)
}
return colorArr
}

思路二:CSS结合svg


我们可以用css的background: conic-gradient


background: conic-gradient(#179067, #62e317, #d7f10f, #ffc403, #fcc202, #ff7327, #ff7327, #FF5800, #ff5900, #f64302, #ff0000, #ff0000);

image.png


看着好像不错,那么接下来只要我们做个遮罩,然后用svg的strokeDashoffset来形成我们的环状进度条就可以了。至于百分之几到百分之几我们可以将conic-gradient内部属性做个百分比的拆分就可以了


image.png


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
.circle {
width: 300px;
height: 300px;
background: conic-gradient(#179067, #62e317, #d7f10f, #ffc403, #fcc202, #ff7327, #ff7327, #FF5800, #ff5900, #f64302, #ff0000, #ff0000);
border-radius: 50%;
position: relative;
}

#progress-circle circle {
stroke-dasharray: 880;
stroke: #f2f2f2;
}

#progress-circle {
transform: rotate(-90deg);
}

.circle-mask {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
width: 260px;
height: 260px;
background: #fff;
border-radius: 50%;
}
</style>

<body>
<div class="circle">
<svg id="progress-circle" width="300" height="300">
<circle r="140" cx="150" cy="150" stroke-width="21" fill="transparent" />
</svg>
<div class="circle-mask"></div>
</div>
</body>
<script>
const circle = document.querySelector('#progress-circle circle');
const radius = circle.r.baseVal.value;
const circumference = radius * 2 * Math.PI;
function setProgress(percent) {
const progress = circumference - (percent / 100) * circumference;
circle.style.strokeDashoffset = -progress;
}
let prog = 40
let val = 100 - prog
setProgress(val); //设置初始进度

</script>

</html>

这里简单讲下逻辑,我们通过计算环的周长,总长其实就是stroke-dasharray,通过strokeDashoffset来偏移我们的虚线线段,那么开始的就是我们的实线线段。其实就是一个蚂蚁线。让这个线长度等于我们的环长度,通过api让实线在开始的位置。


最终效果


image.png


"奇葩"的横向进度条


在我们平常需求用用组件库实现进度条很容易,但是我们看看这个需求的进度条的场景,文字要能被裁剪成黑白两色。


image.png


image.png


思路一: overflow:hidden


具体就不演示了,内部通过两个副本的文案,一套白色一套黑色,通过定位层级的不同,overflow:hidden来隐藏,缺点是相对繁琐的dom结构。


思路二: background-clip 裁剪


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
:root {
--d: 20%
}

.inverted {
padding: 0 8px;
display: flex;
justify-content: space-between;
background: linear-gradient(-90deg, #000 var(--d), #fff 0) no-repeat, linear-gradient(-90deg, #0000 var(--d), rgb(192, 23, 23) 0) no-repeat;
-webkit-background-clip: text, padding-box;
background-clip: text, padding-box;
color: #0000;
font-weight: bold;
cursor: pointer;
}

.box {
background: #ebebeb;
width: 300px;
border-radius: 24px;
overflow: hidden;
}
</style>

<body>
<div class="box">
<div class="inverted">
<div class="inverted-item">888w/12</div>
<div class="inverted-item">100%/10s</div>
</div>
</div>
</body>

<script>
function modifyProg(prog) {
let val = 100 - prog
document.documentElement.style.setProperty('--d', val + '%')
}
modifyProg(6)
</script>

</html>

这里我们主要用了background-clip的text和padding-box两个裁剪,一个裁剪文本,一个裁剪背景延伸至内边距padding外沿。不会绘制到边框处。在js中我们通过setProperty修改css变量即可。


最终效果


image.png


附录



  1. 卷出新高度,几百行的Canvas游戏秘籍 - 掘金 (juejin.cn)

  2. 为什么我的WebGL开发这么丝滑 🌊

  3. Echarts无法实现这个曲线图😭,那我手写一个 - 掘金 (juejin.cn)

  4. Redux已死,那就用更爽的Zustand - 掘金 (juejin.cn)<
    作者:谦宇
    来源:juejin.cn/post/7244172094547492923
    /a>

收起阅读 »

区块链中的平行链

随着区块链技术的不断发展,人们对于区块链的应用也越来越广泛。在这个过程中,平行链技术应运而生。那么,什么是平行链呢? 1. 平行链的定义和概念 平行链是一种特定的应用程序数据结构,它是全局一致的,由 Polkadot 中继链的验证节点进行验证] 。它允许交易...
继续阅读 »

随着区块链技术的不断发展,人们对于区块链的应用也越来越广泛。在这个过程中,平行链技术应运而生。那么,什么是平行链呢?


1. 平行链的定义和概念


平行链是一种特定的应用程序数据结构,它是全局一致的,由 Polkadot 中继链的验证节点进行验证] 。它允许交易在专用的 Layer1 区块链生态系统中并行分布和处理,从而显著提高了吞吐量和可扩展性


平行链技术来自于波卡项目。Polkadot 是一个可伸缩的异构多链系统,其本身不提供任何内在的功能应用,主要是连接各个区块链协议,并维护协议通讯有效安全,并保存这些通讯信息。平行链可以作为公共或私有网络运行,可以用于企业或组织运行,可以作为平台运行,让其他人在他们的基础上构建应用程序或作为公共利益平行链,来造福整个 Polkadot 生态系统,以及各种各样的其他模型 。


简单来说,平行链是一种依赖于波卡中继链提供安全性并与其他中继链通信的自主运行的原生区块链。它是一种简单、易扩展的区块链,它的安全性由“主链”提供。


2. 平行链与主链的关系


平行链是一种独立的区块链,它与主链(mainchain)通过双向桥接相连。它允许代币或其他数字资产在主链和平行链之间进行转移。每个平行链都是一个独立的区块链网络,拥有自己的代币、协议、共识机制和安全性


双向桥(two-way bridge)是一种连接两个区块链的技术,它允许资产在两个区块链之间进行转移。有单向(unidirectional)桥和双向(bidirectional)桥两种类型。单向桥意味着用户只能将资产桥接到一个目标区块链,但不能返回其原生区块链。双向桥允许资产在两个方向上进行桥接


平行链和主链保持既独立又连结的关系,在主链之下,它可以拥有自己的超级节点、状态机和原始交易数据。主链可以给平行链做跨链操作,从而形成链条生态系统。


3. 平行链的优势


平行链不仅可以利用系统的共识保证安全,还可以共享原有的生态。它们执行的计算本质上是独立,但又连结在一起。平行链之间有明确的隔离分界线,可以立即执行所有交易,而不用担心和其他链产生冲突。


使用平行链的原因是它能够显著提高吞吐量和可扩展性,并且能够支持多种不同的应用场景。


平行链可以用来运行区块链应用程序,如去中心化应用程序(dapps),并将计算负载从主链上移除,从而帮助扩展区块链。它们还可以与其他扩展解决方案结合使用。尽管平行链看起来是一个有前途的解决方案,但它们增加了区块链设计的复杂性,并且需要大量的努力和投资进行初始设置。由于平行链是独立的区块链,它们的安全性可能会受到损害,因为它们不受主链的保护。另一方面,如果一个平行链被攻击,它不会影响主链,因此它们可以用来试验新的协议和对主链的改进


4. 平行 链的关键特征


平行 链具有许多关键特征。例如,它们可以多条并行处理交易,效率可提升十倍。此外,由于只需要下载平行 链相关的数据,因此相对效率更高,速度更快。这些平行 链开枝散叶,可以打造自己独有的生态系。


5. 平行链的应用实例


目前市场上已经出现了许多基于平行 链技术开发 的项目。例如 Crust Network 是一个激励去中心化云服务 的应用型公 链。其通过对平行 链技术 的应用 ,既完整地保存了项目 的自主发展空间 ,也可更好地拓展项目功能 ,为用户提供更好地使用体验。


6. 平行链的经济政策


针对目前公 链技术自主权较弱 的问题 ,Polkadot 平 行 链 上 的社 区根据自己 的意志治理他们 的网络 ,不受波卡网络管理 的限制 ,拥有绝对 的自主权 。通过分片协议连接 的区块 链网络 ,可较好地实现拓展定制 。而基于 Polkadot 技术应用开发 的 Crust Network 是一个激励去中心化云服务 的应用型公 链。其通过对平行 链技术 的应用 ,既完整地保存了项目 的自主发展空间 ,也可更好地拓展项目功能 ,为用户提供更好地使用体验。from刘金,转载请

作者:Pomelo_刘金
来源:juejin.cn/post/7244174365172187193
注明原文链接。感谢!

收起阅读 »

IntelOne Mono 英特尔编程字体

web
一、简介 IntelOne Mono 是一个被设计为富有表现力的字体,在设计之初就充分的考虑到 开发者的需求:清晰度/易读性,以解决视觉的疲劳, 减少编码错误等问题,并且 IntelOne Mono 已经得到 低视力 等人群的反馈,证明确实得到一些正面反馈效果...
继续阅读 »

IntelOne Mono.png


一、简介


IntelOne Mono 是一个被设计为富有表现力的字体,在设计之初就充分的考虑到 开发者的需求:清晰度/易读性,以解决视觉的疲劳, 减少编码错误等问题,并且 IntelOne Mono 已经得到 低视力 等人群的反馈,证明确实得到一些正面反馈效果。



开源协议:SIL Open Font License 1.1 商用需要注意协议内容。



二、字体粗细


Untitled.png


IntelOne Mono 按照字体粗细可以分为四种字体:细体/常规字体/中等粗细/粗体 以及斜体效果


三、字体格式支持


font-images.png


官方仓库 中,给出四种格式的样式字体,就安装体验:



  • Windows/MacOS 桌面端使用 ttf/otf,这两种字体都具有跨平台性。

  • Web 端使用 woff/woff2 字体。


四、下载并使用字体


1. Git clone 下载


cd <dir> & git clone https://github.com/intel/intel-one-mono.git

2. Github Release 下载



intel/intel-one-mono 根据需要下载即可。



3、在 VS Code 中配置


位置设置示例
1、配置Settings(设置) -> User(用户) -> Text Editor (文本编辑器) -> Font Family (字体家族) -> IntelOne Monovs-code-setting-font.png
2、集成终端Settings(设置) -> User(用户) -> Features(特性) -> Terminal (终端) -> Intergrated Font Family (字集成字体家族) -> IntelOne Monointel-one-inter.png

4、在 WebStrome 中配置


编辑器配置设置位置示例
1、配置编辑器Settings(设置) -> Editor(编辑器) -> Font -> IntelOne Monowebstrom-use-intel-one-mono-font.png

5、在 Sublime Text 中配置


覆盖 json 数据 font_face 属性值:


编辑器配置设置位置示例
font_face"font_face": "IntelOne Mono"sublime-intel-one-mono-screen-cut.png


其他环境字体,例如:终端中配置也比较简单,就不再复述。



五、字体显示效果


不同大小字体展示效果.png


在渲染字体方面,IntelOne Mono 字体推荐使用大号字体,ttf 文件格式字体,已经对大号字体进行了优化,尤其是在 Windows 平台。



注意:在 VSCode 中,当字体大小发生变化的时候,字体/建议窗口/终端的行高最好一起配置。



六、好编程字体的特点


要素说明
1、清晰易读避免过度装饰,准确还原字体,易读易懂。
2、等宽字体编程对排版的整齐度有较高的要求,排版整齐的代码更加容易阅读和理解。
3、字符与符号层次封面字符中,字母,数字等都具有应该具有不同的展示高度,凸显不同的层级的内容,使得编码更具有层次感。便于快速理解代码字符。
4、特殊字符逗号、句号、括号等等编程中常用的字符,应该突出、便于识别。

七、不同编辑器显示效果


编辑器展示
VS Codeshow-vscode.png
WebStromewebstrom-intelOne-mono-show.png
Sublimeshow-sublime-intel-one-mono-screen-cut.png

八、社区反馈


IntelOne Mono 字体自 4 月 22 发布第一个版本,到今天 Github 社区 Star 数目目前 已经达到 5.5K+ star 数目,足以证明字体的受欢迎程度。


九、与其他字体对比



对比是为了找到更加 适合自己 的字体。



字体名称效果展示
IntelOne Monocompare-intelone-mono.png
JetBrainsMono Nerd FontJetBrainsMono Nerd Font.png
Input Monocompare-Input Mono.png
InconsolataGo Nerd Font Monocompare-InconsolataGo Nerd Font Mono.png
Cascadia Monocompare-cascadiamono.png

十、从 IntelOne Mono 看字体设计


1. UFO


logo.svg

  • 全名:(The Unified Font Object) 统一字体对象

  • UFO 3 (unifiedfontobject.org)
    一种跨平台、跨应用程序、人类可读、面向未来的格式,用于存储字体数据。

  • UFO3 文件的目录结构是这样的。


2. glif


全名:(glyph interchage format) 描述字型文件格式,glif 存储了单个字形的 (TrueType、PostScript)轮廓、标记点、参考线,使用 xml 语法。


3. plist


属性列表格式包含一系列以 XML 编码的键值对。


4. fea


.fea文件名扩展名主要与 Adobe OpenType Feature File ( .fea)文件类型相关联,该文件类型与 OpenType 一起使用,OpenType 是一个开放的标准规范,用于可扩展的排版字体,最初由微软和 Adobe Systems 开发。


5. 软件


RoboFont


robotfont.png


仅限 MacOS 支持,推荐使用。


FontForge


fontforge.png


适用于 Windows/Mac。


6. PostScript 字体


是由 Adobe Systems 为专业数字排版桌面排版开发的轮廓字体计算机字体规范编码的字体文件。


7. TrueType 字体


TrueType 是由美国苹果公司和微软公司共同开发的一种电脑轮廓字体(曲线描边字)类型标准。这种类型字体文件的扩展名是.ttf,类型代码是 tfil。


十一、小结


本文主要介绍 IntelOne Mono 字体在当前主流编辑器中使用方法和展示效果,了解好的编程字体的优秀特带你。并且探索其背后实现的直奔知识点涉及字体的设计方法和工具软件,如果感兴趣,可以自己设计一套字体。IntelOne Mono 字体在 GitHub 上 Star 数量反映了其被人快速关注且喜欢的特点。但也有不足,没有提供 Nerd Font 字体,对于喜欢用终端的小伙伴

作者:进二开物
来源:juejin.cn/post/7244174500785373241
,暂时可能会受欢迎。

收起阅读 »

还有多少公司在使用H5?不怕被破解吗?

web
H5还有人在用吗 近几天,老板让我调查一下现在市面上H5的使用现状。粗略地调查了一下,发现现在使用H5的真不多了,但是还是有人在用H5的,原因无非一是成本低,相比户外广告,H5制作费用根本不值一提;二是效果好,转化率高,好一点的H5大概有10W浏览量,这也反映...
继续阅读 »

H5还有人在用吗


近几天,老板让我调查一下现在市面上H5的使用现状。粗略地调查了一下,发现现在使用H5的真不多了,但是还是有人在用H5的,原因无非一是成本低,相比户外广告,H5制作费用根本不值一提;二是效果好,转化率高,好一点的H5大概有10W浏览量,这也反映H5数据统计精准,企业知道钱花在哪个地方,心里就踏实。


但是,H5的安全性会让企业非常头疼,不知道大家还记不记得几年前某App H5 页面被植入色情内容广告在安全圈引起了轰动。后来排查才基本确定为用户当地运营商http劫持导致H5页面被插入广告,给该App 造成极大影响。


为什么H5是黑灰产高发区?


从顶象多年的防控经验来看,H5面临的风险相对较多是有其原因的。


1、JavaScript代码特性。


H5平台开发语言是JavaScript,所有的业务逻辑代码都是直接在客户端以某种“明文”方式执行。代码的安全防护主要依靠混淆,混淆效果直接决定了客户端的安全程度。不过对于技术能力较强的黑产,仍然可以通过调试还原出核心业务处理流程。




2、企业营销推广需求追求简单快捷。


首先,相比其他平台,很多公司在H5平台的开放业务往往会追求简单,快捷。比如在营销推广场景,很多企业的H5页面只需从微信点击链接直接跳转到一个H5页面,点击页面按钮即可完成活动,获取积分或者小红包。


一方面确实提升了用户体验,有助于拉新推广;但另一方面简便的前端业务逻辑,往往也会对应简单的代码,这也给黑灰产提供了便利,相比去破解App,H5或者小程序的破解难度要低一些。


数据显示,如果企业在营销时不做风险控制,黑产比例一般在20%以上,甚至有一些高达50%。这就意味着品牌主在营销中相当一部分费用被浪费了。


3、H5平台自动化工具众多。


核心流程被逆向后,攻击者则可以实现“脱机”,即不再依赖浏览器来执行前端代码。攻击者可以自行构造参数,使用脚本提交请求,即实现完全自动化,如selenium,autojs,Puppeteer等。这些工具可以在不逆向JS代码的情况下有效实现页面自动化,完成爬虫或者薅羊毛的目的。


4、防护能力相对薄弱。


从客观层面来看,H5平台无论是代码保护强度还是风险识别能力,都要弱于App。这是现阶段的框架导致,并不是技术能力问题。JavaScript数据获取能力受限于浏览器,出于隐私保护,浏览器限制了很多数据获取,这种限制从某种程度上也削弱了JavaScript在业务安全层面的能力。


以电商App为例,出于安全考虑,很多核心业务只在App上支持。如果H5和App完全是一样的参数逻辑和加密防护,对于攻击者,破解了H5也就等于破解了App。


5、用户对H5缺乏系统认识。


最后,大部分用户对H5的安全缺乏系统性的认识,线上业务追求短平快,没有在H5渠道构建完善的防护体系情况下就上线涉及资金的营销业务。


H5代码混淆


基于上面这些问题,我们可以采取H5代码混淆的方式来稍微解一下困境。


一、产品简介



  • H5代码混淆产品,通过多层加密体系,对H5文件进行加密、混淆、压缩,可以有效防止H5源代码被黑灰产复制、破解。


二、混淆原理



  • 对代码中的数字,正则表达式、对象属性访问等统一转为字符串的表示形式

  • 对字符串进行随机加密(多种加密方式,倒序/随机密钥进行轮函数加密等)

  • 对字符串进行拆分,并分散到不同作用域

  • 打乱函数顺序

  • 提取常量为数组引用的方式


举个简单的例子来说明一下流程
(1)变量和函数重命名:


// 混淆前
function calculateSum(a, b) {
var result = a + b;
return result;
}

// 混淆后
function a1xG2b(c, d) {
var e = c + d;
return e;
}


(2)代码拆分和重新组合:


// 混淆前
function foo() {
console.log('Hello');
console.log('World');
}

// 混淆后
function foo() {
console.log('Hello');
}

function bar() {
console.log('World');
}


(3)控制流转换:


// 混淆前
if (condition) {
console.log('Condition is true');
} else {
console.log('Condition is false');
}

// 混淆后
var x = condition ? 'Condition is true' : 'Condition is false';
console.log(x);

(4)添加无用代码:


// 混淆前
function foo() {
console.log('Hello');
}

// 混淆后
function foo() {
console.log('Hello');
var unusedVariable = 10;
for (var i = 0; i < 5; i++) {
unusedVariable += i;
}
}

结语


当然,实际的代码混淆技术可能更加复杂。而且,代码混淆并不能完全阻止源代码的泄露或逆向工程,但可以增加攻击者分析和理解代码的难度。


H5现在的使用场景其实更多可能偏向日常的投票场景、活动场景以及游戏营销等等,其实使用场景很少了,但是一旦被攻击,尤其是对于运营商这种大厂来说,危害性还是很大的,企业或者说公司还是需

作者:昀和
来源:juejin.cn/post/7244004118222078010
要注意这方面的安全。

收起阅读 »

技术人创业是怎么被自己短板KO的

这几天搜索我的聊天记录,无意中看到了一个两年前的聊天消息。那是和我讨论出海业务的独立开发网友,后来又去创业的业界精英。顺手一搜,当初他在我的一个开发者群也是高度活跃的人群之一,还经常私下给我提建议,人能力很强,做出海矩阵方向的项目做的很拿手。但是在后来忽然就没...
继续阅读 »

这几天搜索我的聊天记录,无意中看到了一个两年前的聊天消息。那是和我讨论出海业务的独立开发网友,后来又去创业的业界精英。顺手一搜,当初他在我的一个开发者群也是高度活跃的人群之一,还经常私下给我提建议,人能力很强,做出海矩阵方向的项目做的很拿手。但是在后来忽然就没消息了,虽然人还留在群里。


我好奇点开了他的朋友圈,才知道他已经不做独立开发了,而且也(暂时)不在 IT 圈里玩了,去帮亲戚家的服装批发业务打打下手,说是下手,应该也是二当家级别了,钱不少,也相对安稳。朋友圈的画风以前是IT行业动态,出海资讯现在是销售文案和二维码。


和他私下聊了几句,他跟我说他现在过的也还好,人生路还长着呢,谈起了自己在现在这行做事情的经历,碎碎念说了不少有趣的事情,最后还和我感慨说:“转行后感觉脑子灵活了很多”,我说那你写程序的时候脑子不灵活吗,他发了个尴尬而不失礼貌的表情,“我以前技术搞多了,有时候死脑筋。”


这种话我没少听过,但是从一个认识(虽然是网友)而且大跨度转行的朋友这里说出来,就显得特别有说服力。尤其了解了他的经历后,想写篇文章唠叨下关于程序员短板的问题,还有这种短板不去补强,会怎么一步步让路越走越窄的。


现在离职(或者被离职)的程序员越来越多了,程序员群体,尤其是客户端程序员这个群体,只要能力过得去,都有全栈化和业务全面化的潜力。尤其是客户端程序员,就算是在公司上班时,业余时间写写个人项目,发到网上,每个月赚个四到五位数的副业收入也是可以的。


再加上在公司里遇到的各种各样的窝囊事,受了无数次“煞笔领导”的窝囊气,这会让一些程序员产生一种想法,我要不是业余时间不够,不然全职做个项目不就起飞了?


知道缺陷在哪儿,才能扬长避短,所以我想复盘一下,程序员创业,在主观问题上存在哪些短板。(因为说的是总体情况,也请别对号入座)


第一,认死理。


和代码,协议,文档打交道多了,不管自己情愿不情愿,人多多少少就有很强的“契约概念”,代码的世界条理清晰,因果分明,1就是1,0就是0,在这样的世界里呆多了,你要说思维方式不被改变,那是不可能的 --- 而且总的来说,这种塑造其实是好事情。要不然也不会有那么多家长想孩子从小学编程了。(当然了,家长只是想孩子学编程,不是做程序员。)


常年埋头程序的结果,很容易让技术人对于社会上很多问题的复杂性本质认识不到位,恐惧,轻视,或者视而不见,总之,喜欢用自己常年打磨的逻辑能力做一个推理,然后下一个简单的结论。用毛爷爷的话说,是犯了形而上的毛病。


例如,在处理iOS产品上架合规性一类问题时,这种毛病暴露的就特别明显。


比如说相信一个功能别的产品也是这么做的,也能通过审核,那自己照着做也能通过。但是他忽略了这种判断背后的条件是,你的账号和别的账号在苹果眼里分量也许不同的,而苹果是不会把这件事写在文档上的。


如果只是说一说不要紧,最怕的是“倔”,要不怎么说是“认死理”呢。


第二,喜欢拿技术套市场。


​这个怎么理解呢,就是有追求的技术人喜欢研究一些很强的技术,但是研究出来后怎么用,也就是落实到具体的应用场景,就很缺点想象力了。


举个身边有意思的例子,有个技术朋友花了三年时间业余时间断断续续的写,用 OpenGL 写了一套动画效果很棒的 UI 引擎,可以套一个 View 进去后定制各种酷炫的动画效果。做出来后也不知道用来干嘛好,后来认识了一个创业老板,老板一看你这个效果真不错啊,你这引擎多少钱我买了,朋友也没什么概念,说那要不五万卖你。老板直接钱就打过去了。后来老板拿给手下的程序员维护,用这套东西做了好几个“小而美”定位的效率工具,简单配置下就有酷炫的按钮动画效果,配合高级的视觉设计逼格拉满,收入怎么样我没问,但是苹果在好几个国家都上过推荐。


可能有人要说,那这个程序员哥哥没有UI帮忙啊,对,是这个理,但是最根本的问题是,做小而美工具这条路线,他想都没想到,连意识都意识不到的赚钱机会,怎么可能把握呢?有没有UI帮忙那是实现层的门槛而已。


第三,不擅长合作。


为什么很多创业赚到小钱(马化腾,李彦宏这些赚大钱就不说了,对我们大部分人没有参考价值)而且稳定活下来的都是跑商务,做营销出身的老板。


他们会搞钱。


他们会搞钱,是​因为他们会搞定人,投资人,合伙人,还有各种七七八八的资源渠道。


大部分人,在创业路上直接卡死在这条路线上了。


投资人需要跑,合作渠道需要拉,包括当地的税务减免优惠,创业公司激励奖金,都需要和各种人打交道才能拿下来。


那我出海总行了吧,出海就不用那么麻烦了吧。不好意思,出海的合作优势也是领先的,找海外的自媒体渠道合作,给产品提曝光。坚持给苹果写推荐信,让自家产品多上推荐。你要擅长做这些,就不说比同行强一大截,起码做出好产品后创业活下来的希望要高出不少,还有很多信息差方法论,需要进圈子才知道。



--- 


我说的这些,不是贬损也不是中伤,说白了,任何职业都有自己的短板,也就是我们说的职业病,本来也不是什么大不了的事情。只是我们在大公司拧螺丝的时候,被保护的太好了。


只是创业会让一个人的短处不断放大,那是因为你必须为自己的选择负责了,没人帮你擦屁股了背锅了。所以短板才显得那么刺眼。


最后说一下,不是说有短板就会失败,谁没点短处呢。写出来只是让自己和朋友有更好的自我认知,明白自己的长处在哪,短处在哪。


最后补一个,左耳朵耗子的事情告诉我们,程序员真的要保养身子,拼到最后其实还是拼身

作者:风海铜锣
来源:juejin.cn/post/7238443713873199159
体,活下来才有输出。

收起阅读 »

何谓实事求是地工作?

提到实事求是,大家第一时间会想到什么?我想大部分是客观,事实,脚踏实地?这么一想,大家都会觉得,自己挺实事求是的呀,没毛病。但是,我会经常在工作中感受到不是那么实事求是的行为,比如张嘴就来,不带思考,做事全靠猜的行为,真太多了。 随着我这两年的学习和总结,我越...
继续阅读 »

提到实事求是,大家第一时间会想到什么?我想大部分是客观,事实,脚踏实地?这么一想,大家都会觉得,自己挺实事求是的呀,没毛病。但是,我会经常在工作中感受到不是那么实事求是的行为,比如张嘴就来,不带思考,做事全靠猜的行为,真太多了。


随着我这两年的学习和总结,我越发觉得实事求是非常重要,并把它视为我做事情和成长的基石。对于实事求是,我主要有以下 3 层理解。


首先,尊重客观事实,探寻真理。我们要承认事实,即使这个事实有多么的难以置信,但存在即是合理,我们首先要尊重它,承认它。然后我们还要积极主动地面对它,探寻事实背后的真理,获得真知,这样才能真正的成长,并有可能寻得机会。当某个事情的进展超出自己预期的时候,我们正确的态度应该是思考为什么会这样,而不是去想对错得失。


其次,数据说话,数据驱动。事实如何去量化?答案是数据。使用数据去表达事实,是我们程序员应该有的技能。工作的本质就是解决问题,之前的文章有讲解,问题就是理想状态和现实状态之间的差别,因此,我们在工作当中做的每一项决策的依据、制定的每一个目标,都应该用数据说话。我们应该使用数据表达现状,并使用数据衡量目标,驱动自己去工作。一些沟通的细节就能够体现出他是不是在实事求是地工作,比如“这个页面加载太慢了,需要优化”。那这个页面加载到底有多慢?业界标准或者竞品的加载耗时是多少?优化的目标值是多少?


最后,从客观事实中获取反馈,不断迭代。工作中想要获得成功和成长,最核心的一个环节是反馈。很多人没有意识到这点。更多的人没有意识到的是,获取反馈其实很简单,随处都是。敏捷开发、精益创业、增长黑客,这些理论的底层核心都是基于事实和数据的反馈,不断迭代改进自己的产品,从而获得成功。对于个人成长来说也是一样的,我们要从客观事实中获取反馈,思考总结,不断迭代自己的能力。


总结一下,实事求是地工作有 3 个层次,首先,要正视事实,并主动探究真理;然后我们慢慢地开始用数据驱动自己的工作;最后让数据驱动变成循环,不断迭代,并把这种循环融入到各个方面,包括工作和个人成长,让它成为自己下意识的动作。


我在努力学习和践行实事求是地工作,我也希望我的团队可以用实事求是的态度来工作,以此文共勉!



作者:潜龙在渊灬
来源:juejin.cn/post/7241394138260160568

收起阅读 »

我裸辞了,但是没走成!

人在国企,身不由己!公司福利和薪资还可以,但有个难顶的组长就不可以,说走就走!如果把这个组长换了的话就另说了! 1.为什么突然想不干了? 1.奇葩的新组长 我的前组长辞职了,然后被安排到这个自研项目组,而这个新组长我之前得罪过,老天爷真爱开玩笑! 今年过年前,...
继续阅读 »

人在国企,身不由己!公司福利和薪资还可以,但有个难顶的组长就不可以,说走就走!如果把这个组长换了的话就另说了!


1.为什么突然想不干了?


1.奇葩的新组长


我的前组长辞职了,然后被安排到这个自研项目组,而这个新组长我之前得罪过,老天爷真爱开玩笑!


今年过年前,我主开发的平台要嵌入到他负责的项目里面,一切对接都很顺利,然而某天,有bug,我修复了,在群里面发消息让他合并分支更新一下。他可能没看到,然后我下班后一个小时半,我还在公司,在群里问有没有问题,没回应!


然后我就坐车回家,半路,产品经理组长、大组长和前组长一个个轮流call我,让我处理一下bug,我就很无语!然后我就荣获在家远程办公,发现根本没问题!然后发现是对方没更新的问题!后面我修复完直接私聊他merge分支更新,以免又这样大晚上烦人!
而类似的事情接连发生,第三次之后,我忍不住了,直接微信怼了他,他还委屈自己晚上辛苦加班,我就无语大晚有几个人用,晚上更新与第二天早上更新有什么区别?然后就这样彻底闹掰了!


我就觉得这人很奇葩,有什么问题不能直接跟我沟通,一定要找我的上级一个个间接联系我呢?而且,这更新流程就很有问题,我之前在别的组支援修bug,是大早上发布更新,一整天测试,保证不是晚上的时候出现要紧急处理的问题!


然后,我跟这人有矛盾后,我就没继续对接这个项目了,前组长安排了别人代替我!


结果兜兜转转,竟然调到他这里来!作孽啊!


2.项目组乱糟糟


新项目组可以看出新组长管理水平很糟糕!


新组长给自己的定位是什么都管!产品、前后端、测试、业务等,什么都往自己身上揽!他自己觉得很努力,但他不是那部分的专业人员,并不擅长,偏偏还没那个金刚钻揽一堆瓷器活!老爱提建议!


项目组就两个产品,其中一个是UI设计刚转,还没成长为专业的产品经理,而那个主要负责的产品经理根本忙不过来!


然后,他一个人搞不定,就开始了PUA大法,周会的时候就会说:“希望大家要把这个项目当成自己的事业来奋斗,一起想,更好地做这个产品!”


“这个项目集成了那么多的模块功能,如果大家能够做好,对自己有很大的历练和成长!”


“我们项目是团队的重点项目,好多领导都看好,开发不要仅限于开发,要锻炼产品思维……”


……


简而言之就是,除了本职工作也要搞点产品的工作!


然后建模师开始写市场调研文档,前后端开发除了要敲代码,还得疯狂想新功能。


整个组开始陷入搞新东西的奇怪旋涡!


某次需求评审的时候,因为涉及到大量的文件存储,我提出建议,使用minio,fastdfs,这样就不用每次部署的时候,整体文件还要迁移,结果对方一口拒绝,坚决使用本地存储,说什么不要用XX平台的思想来污染他这个项目,他这个项目就要不需要任何中间件都能部署。


就很无语!那个部署包又大又冗余,微服务都不用,必须要整包部署整套系统,只想要某几个功能模块都不行,还坚持说这样可以快速整包部署比较好!


一直搞新功能的问题就是版本更新频繁!一堆新功能都没测清楚就发布,导致产品质量出现严重问题,用户体验极差!终于用户积攒怨气爆发了,在使用群里面@了我们领导,产品质量问题终于被彻底揭开了遮羞布!


领导开始重视这个产品质量的问题,要求立即整改!


然后这个新组长开始新一轮搞事!


“大家保证新功能进度的同时,顺便测试旧功能,尽量不要出bug!”


意思就是你开发进度也要赶,测试也要搞!


就不能来点人间发言吗?


3.工作压力剧增


前组长是前端的,他带我入门3D可视化的,相处还算融洽!然而他辞职了,去当自由职业者了!


新组长是后端的,后端组长问题就是习惯以后端思维评估前端工作,给任务时间很紧。时间紧就算了,还要求多!


因为我之前主开发的项目是可视化平台,对方不太懂,但不妨碍他喜欢异想天开,加个这个,加个那个,他说一句话,就让你自行想象,研究竞品和评估开发时间!没人没资源,空手套白狼,我当时就很想爆他脑袋了!


我花一个星期集成了可视化平台的SDK,连接入文档都写好了,然后他验收的时候提出一堆动态配置的要求,那么大的可视化平台,他根本没考虑项目功能模块关联性和同步异步操作的问题,他只会提出问题,让你想解决方案!


然后上个月让我弄个web版的Excel表格,我看了好多开源项目,也尝试二开,增加几个功能,但效率真的好低!于是我就决定自己开发一个!


我开发了两个星期,他就问我搞定没?我说基本功能可以,复杂功能还在做!


更搞笑的是,我都开发两个星期了,对方突然中午吃饭的时候微信我,怕进度赶不上,建议我还是用开源的进行二开,因为开源有别人帮忙一起搞。


我就很无语,人家搞的功能又不是一定符合你的需求,开源不等于别人给你干活,大家都是各干各的,自己还得花精力查看别人代码,等价于没事找事,给自己增加工作量!别人开发的有隐藏问题,出现bug排查也很难搞,而自己开发的,代码熟悉,即便有问题也能及时处理!


我就说他要是觉得进度赶不上就派个人来帮忙,结果他说要我写方案文档,得到领导许可才能给人。


又要开发赶进度,又要写文档,哪有那么多时间!最终结果就是没资源,没人手,进度依旧要赶!


因为我主开发的那个可视化平台在公司里有点小名气,好多平台想要嵌入,然后,有别的平台找到他要加上这个可视化平台,但问题是我很忙,又要维护又要开发,哪搞得了那么多?还说这个很赶!赶你个头!明知道时间没有,就别答应啊!工作排期啊!


新组长不帮组员解决问题,反而把问题抛给组员,压榨组员就很让人反感!


2.思考逃离


基于以上种种!我觉得这里不是一个长久之地,于是想要逃离这里!


我联系了认识的其他团队的人,别人表示只要领导愿意放人,他们愿意接收我,然后我去咨询一些转团队的流程,那些转团队成功的同事告诉我,转团队最难的是领导放人这关,而且因为今年公司限制招聘,人手短缺,之前有人提出申请,被拒绝了!并且转团队的交接的一两个月内难免要承受一些脸色!有点麻烦!


我思虑再三,我放弃了转团队这条路,因为前组长走了之后,整个团队只剩下我一个搞3D开发的,估计走不掉!


3.提出辞职


忍了两个月,还是没忍住,工作最重要的是开开心心!赚钱是一回事,要是憋出个心理疾病就是大事了!于是我为了自己的身心健康,决定走人!拜拜了喂!老娘不奉陪了!


周一一大早,我就提交了辞职信,大组长表示很震惊,然后下午的时候,领导和大组长一起来跟我谈话,聊聊我为什么离职?问我有没有意愿当个组长之类的,我拒绝了,我只想好好搞技术!当然我不会那么笨去说别人的坏话得罪人!


我拿前组长当挡箭牌,说自己特别不习惯这个新组长的管理方式!前组长帮我扛着沟通之类的问题,我只要专心搞开发就好了!


最终,我意志坚定地挡住了领导和大组长的劝留谈话,并且开始刷面试题,投简历准备寻找新东家!


裸辞后真的很爽,很多同事得知消息都来关心我什么情况,我心里挺感动的!有人说我太冲动了,可以找好下家再走!但我其实想得很清楚,我没可能要求一个组长委屈自己来适应我,他有他的管理方式,我有我的骄傲!我不喜欢麻烦的事,更不喜欢委屈自己,一个月后走人是最快解决的方案!


4. 转机


其实我的离开带来了一点影响,然后加上新组长那个产品质量问题警醒了领导,然后新组长被调去负责别的项目了,换个人来负责现在的项目组,而这个人就是我之前支援过的项目组组长,挺熟悉的!


新新组长管理项目很有条理也很靠谱,之前支援的项目已经处于稳定运行的状态了,于是让他来接手这个项目!他特意找我谈话,劝我留下来,并且承诺以后我专心搞技术,他负责拖住领导催进度等问题!


我本来主要就是因为新组长的问题才走人的,现在换了个不错的组长!可以啊!还能苟苟!


5.反思



  1. 其实整件事情中,我也有错,因为跟对方闹掰了,就拒绝沟通,所以导致很多问题的发生,如果我主动沟通去说明开发难度的问题,并且争取时间,就不至于让自己处于一个精神内耗的不快乐状态。

  2. 发现问题后,我没有尝试去跟大组长反馈,让大组长去治治对方,或者让大组长帮忙处理这个矛盾,我真的太蠢了!

  3. 我性格其实挺暴躁的,看不顺眼就直接怼,讨厌的人就懒得搭理,这样的为人处世挺不讨喜的,得改改这坏
    作者:敲敲敲敲暴你脑袋
    来源:juejin.cn/post/7241884241616076858
    脾气!

收起阅读 »

AnyScript:前端开发的最佳良药!

web
不以繁琐为名,更以简洁为声! 作为一名Api调用工程师,深知在前端开发中的各种挑战和痛点。在我们开发过程中,代码的可维护性和可扩展性是至关重要的因素。TypeScript(简称TS)作为JavaScript的超集,为我们带来了更好的开发体验和更高的代码质量。 ...
继续阅读 »

cover.png


不以繁琐为名,更以简洁为声!


作为一名Api调用工程师,深知在前端开发中的各种挑战和痛点。在我们开发过程中,代码的可维护性和可扩展性是至关重要的因素。TypeScript(简称TS)作为JavaScript的超集,为我们带来了更好的开发体验和更高的代码质量。


1. 类型系统:保驾护航


1.1 强大的类型检查


TypeScript引入了静态类型检查,这是它最吸引人的特点之一。通过在代码中定义变量的类型,TypeScript可以在编译时发现潜在的错误,大大减少了在运行时遇到的意外错误。例如,在JavaScript中,我们可以将一个字符串类型的变量传递给一个预期接收数字类型的函数,这将导致运行时错误。而在TypeScript中,编译器将会提示我们这个潜在的类型不匹配错误,使得我们可以在开发过程中及早发现并修复问题。


举个例子,假设我们有以下的TypeScript代码:


function add(x: number, y: number): number {
return x + y;
}

const result = add(3, '5');
console.log(result);

在这个例子中,我们本应该传递两个数字给add函数,但是错误地传递了一个字符串。当我们尝试编译这段代码时,TypeScript编译器会提示错误信息:


Argument of type 'string' is not assignable to parameter of type 'number'.

通过这种类型检查,我们可以在开发过程中发现并解决类型相关的问题,避免了一些常见的错误。


1.2 类型推断的魅力


在TypeScript中,我们不仅可以显式地定义变量的类型,还可以利用类型推断的功能。当我们没有明确指定类型时,TypeScript会根据上下文和赋值语句自动推断变量的类型。这个特性不仅减少了我们编写代码时的工作量,还提供了代码的简洁性。


举个例子,考虑以下的代码:


const name = 'John';

在这个例子中,我们没有显式地指定name的类型,但TypeScript会自动推断它为字符串类型。这种类型推断让我们在编写代码时更加灵活,减少了类型注解的需求。


2. 更好的代码编辑体验


2.1 智能的代码补全和提示


使用TypeScript可以带来更好的代码编辑体验。由于TypeScript具备了静态类型信息,编辑器可以提供智能的代码补全和提示功能,减少我们编写代码时的出错几率。当我们输入一个变量名或者函数名时,编辑器会根据类型信息推断可能的属性和方法,并展示给我们。


例如,当我们有一个对象,并想获取它的属性时,编辑器会给出属性列表供我们选择。这在大型项目中尤其有用,因为我们可以快速了解某个对象的可用属性,而不必查阅文档或者浏览源代码。


2.2 重构的艺术


在大型项目中进行代码重构是一项棘手的任务。TypeScript提供了强大的重构能力,使得我们能够更轻松地重构代码而不担心破坏现有功能。在进行重构操作时,TypeScript会自动更新相关的类型注解,帮助我们在整个重构过程中保持代码的一致性。


举个例子,假设我们有一个函数:


function multiply(x: number, y: number): number {
return x * y;
}

现在我们决定将multiply函数的参数顺序调换一下。在传统的JavaScript中,我们需要手动修改所有调用multiply函数的地方。而在TypeScript中,我们只需要修改函数本身的定义,TypeScript会自动检测到这个变化,并指示我们需要更新的地方。


3. 生态系统的繁荣


3.1 类型定义文件


TypeScript支持类型定义文件(.d.ts),这些文件用于描述 JavaScript 库的类型信息。通过使用类型定义文件,我们可以在TypeScript项目中获得第三方库的类型检查和智能提示功能。这为我们在开发过程中提供了极大的便利,使得我们能够更好地利用现有的JavaScript生态系统。


例如,假设我们使用著名的React库进行开发。React有一个官方提供的类型定义文件,我们只需要将其安装到项目中,就能够获得对React的类型支持。这使得我们可以在编写React组件时,获得相关属性和方法的智能提示,大大提高了开发效率。


3.2 社区的支持


TypeScript拥有庞大而活跃的社区,开发者们不断地分享自己的经验和资源。这意味着我们可以轻松地找到许多优秀的库、工具和教程,帮助我们更好地开发和维护我们的前端项目。无论是遇到问题还是寻找最佳实践,社区都会给予我们及时的支持和建议。


4. 面向未来的技术栈


4.1 ECMAScript的最新特性支持


ECMAScript是JavaScript的标准化版本,不断更新以提供更多的语言特性和功能。TypeScript紧密跟随ECMAScript标准的发展,支持最新的语法和特性。这意味着我们可以在TypeScript项目中使用最新的JavaScript语言功能,而不必等待浏览器的支持。


例如,当ECMAScript引入了Promiseasync/await等异步编程的特性时,TypeScript已经提供了对它们的完整支持。我们可以在TypeScript项目中使用这些特性,而无需担心兼容性问题。


4.2 渐进式采用


对于已有的JavaScript项目,我们可以渐进式地引入TypeScript,而无需一次性对整个项目进行重写。TypeScript与JavaScript是高度兼容的,我们可以逐步将JavaScript文件改写为TypeScript文件,并为每个文件逐渐添加类型注解。这种渐进式的采用方式可以降低迁移的风险和成本,并让我们享受到TypeScript带来的好处。


结语


推荐使用TypeScript来提升我们的开发体验和代码质量。它强大的类型系统、智能的代码编辑体验、丰富的生态系统以及面向未来的技术栈,都使得TypeScript成为当今前端开发的首选语言之一。


但是,我们也需要明确TypeScript并非万能的解决方案。在某些特定场景下,纯粹的JavaScript可能更加合适。我们需要根据具体项目的需求和团队的情况,权衡利弊并做出适当的选择。



示例代码仅用于说明概念,可能不符合最佳实践。在实际开发中,请根据具体情况进行调整。


作者:ShihHsing
来源:juejin.cn/post/7243413799347798072

收起阅读 »

如果让你设计一个弹幕组件,你会怎么做???

web
大家好,我是前端小张同学,这次给大家更新一个开发中常见的需求,接下来我会将我的弹幕实现以及设计思路一步一步描述出来,并分享给大家,希望大家喜欢。 今天我们的主题是 ,用vue手写一个弹幕 1:关于弹幕设计思想 1.1 : 业务层 | 视图层(全局组件) 1.1...
继续阅读 »

大家好,我是前端小张同学,这次给大家更新一个开发中常见的需求,接下来我会将我的弹幕实现以及设计思路一步一步描述出来,并分享给大家,希望大家喜欢。


今天我们的主题是 ,用vue手写一个弹幕


1:关于弹幕设计思想


1.1 : 业务层 | 视图层(全局组件)


1.1.1 : 从业务角度来说,如果你设计的是全局弹幕组件,你要考虑以下几点。



  1. 容器的高度?

  2. 容器层次结构划分?

  3. 渲染弹幕的方式,使用组件的人应该传递什么数据?

  4. 是否支持全屏弹幕?

  5. 是否支持弹幕关闭和开启?

  6. 是否需要重置弹幕?

  7. 是否支持暂停弹幕?

  8. 是否需要集成发送功能?


设计方案考虑完整了以后,你将可以开始考虑 数据层的设计


1.2 数据层


1.2.1 : 从数据角度来说每一条弹幕无非是一个element,然后把弹幕内容放到这个element元素中,并且给 element 添加动画,那接下来,你应该这样考虑。




  1. 弹幕是JS对象?它的属性有哪些?




  2. 谁去管理这些弹幕?如何让他能够支持暂停和关闭?




  3. 你如何把后台的数据,与你前台的一些静态数据进行合并,创造出一个完整对象?




  4. 你怎么去渲染这些弹幕?




  5. 你想要几秒创建一次弹幕并在容器内显示和运行?




  6. 弹幕具备哪些灵活的属性?

    运行动画时间 , 用户自己发布的弹幕样式定制?
    又或者,弹幕需要几条弹道内运行等等这些你都需要考虑。




数据设计方案考虑完整了以后,你将可以开始考虑 数据管理层的设计


1.3 数据管理层


1.3.1 从管理的角度来说,外界调用某些方法,你即可快速的响应操作,例如外界调用 open 方法,你就播放弹幕,调用Stop方法,你就关闭弹幕 接下来,你应该考虑以下几点。



  1. 面向对象设计,应该提供哪些方法,具备哪些功能?

  2. 调用了指定的方法,应该怎么对数据进行操作。

  3. 如何对弹幕做性能优化?


到这里 , 我们设计方案基本完成,接下来我们可以开始编写代码。


2: 代码实现


2.1 : 数据层设计方案实现


我们需要构建一个 Barrage 类 ,我们每次去创建一个弹幕的时候都会 new Barrage,让他帮助我们生成一些弹幕属性。


export class Barrage {
constructor(obj) {
// 每次 new Barrage() 传入一个 后台返回的数据对象 obj
const { barrageId, speed, level, top, jumpUrl, barrageContent, animationPlayState, ...args } = obj
this.barrageId = barrageId; // id : 每条弹幕的唯一id
this.speed = speed; // speed : 弹幕运行的速度,由外界控制
this.level = level; // level : 弹幕的层级 --> 弹幕可分为设计可分为 上下 1 , 1 两个层级 ,可决定弹幕的显示是全屏还是半屏显示
this.top = top; // top :弹幕生成的位置相对于 level 的层级 决定 ,相对于 Level 层级 盒子距离顶部的位置
this.jumpUrl = jumpUrl; // jumpUrl :点击弹幕需要跳转的链接
this.barrageContent = barrageContent; // barrageContent : 弹幕的内容
this.animationPlayState = ''; // 设计弹幕 是否可 点击暂停功能
this.color = '#FFF' // 弹幕颜色
this.args = args // 除去Barrage类之外的一些数据属性全部丢到这里,例如后台返回的数据
}
}

2.1 : 数据管理层设计方案实现


2.1.1 :我们在这里实现了 , 弹幕的 增加删除初始化重置关闭开启功能


1. 实现弹幕开启功能.


BarrageManager.js


export class BarrageManager {

constructor(barrageVue) {
this.barrages = []; // 填弹幕的数组
this.barragesIds = [] // 批量删除弹幕的数组id
this.sourceBarrages = [] // 源弹幕数据
this.timer = null //控制弹幕的开启和关
this.barrageVue = barrageVue // 弹幕组件实例
this.deleteCount = 0, // 销毁弹幕的总数
this.lastDeleteCount = 0, // 最后可销毁的数量
this.row = 0,
this.count = 0
}
init(barrages) {
this.sourceBarrages = barrages
this.deleteCount = parseInt(this.sourceBarrages.length / deleteQuantity.FIFTY) // 计算可删除数量
this.lastDeleteCount = this.sourceBarrages.length % deleteQuantity.FIFTY // 计算 最后一次可删除数量
}
/**
*
* @param {*} barrages 接收一个弹幕数组数据
* @description 循环创建 弹幕对象 ,将后台数据与 创建弹幕的属性结合 存入弹幕数组
*/

loopCreateBarrage(barrages) {
const { rows, createTime, crearteBarrageObject } = this.barrageVue
let maxRows = rows / 2 // 最大的弹幕行数
this.timer = setInterval(() => {
for (let i = 0; i < 1; i++) {
let barrageItem = barrages[this.count]
if (this.row >= maxRows) { this.row = 0 } // 如果当前已经到了 最大的弹幕行数临界点则 回到第0 行弹道继续 创建
if (!barrageItem) return clearInterval(this.timer) // 如果取不到了则证明没数据了 , 结束弹幕展示
const item = crearteBarrageObject({ row: this.row, ...barrageItem }) // 添加对象到 弹幕数组中
this.addBarrage(item)
this.count++ // 用于取值 ,取了多少条
this.row++ // 用于弹道
}
}, createTime * 1000);
}
/**
* @param {*} barrages 传入一个弹幕数组数据
* @returns 无返回值
* @description 调用 该方法 开始播放弹幕
*/

open(barrages) {
if (barrages.length === 0) return
this.init(barrages)
this.loopCreateBarrage(this.sourceBarrages)
}
}

在这里我们初始化了一个 open 方法,并接收一个数组 ,并调用了 init 方法 去做初始化操作,并调用了 循环创建的方法,没 createTime 秒创建一条弹幕,加入到弹幕数组中。



  1. 连接视图层


2.1 : 视图层 | 业务层设计方案实现


index.vue


<template>
<div class="barrage">
<div class="barrage-container" ref="barrageContainer">
<div class="barrage-half-screen" ref="halfScreenContainer">
<template v-for="item in barrageFiltering.level1">
<barrage-item
:item="item" :class="{pausedAnimation : paused }"
:options='barrageTypeCallback(item)'
@destory="destoryBraageItem" :key="item.barrageId">

</barrage-item>
</template>
</div>
<div class="barrage-full-screen" v-if="fullScreen">
<template v-for="item in barrageFiltering.level2">
<barrage-item
:item="item" :class="{pausedAnimation : paused }"
:options='barrageTypeCallback(item)'
@destory="destoryBraageItem" :key="item.barrageId">

</barrage-item>
</template>
</div>
</div>
<user-input ref="publishBarrage" v-if="openPublishBarrage" @onBlur="handleBlur">
<template #user-operatio-right>
<!-- 处理兼容性问题 ios 和 安卓 触发点击事件 -->
<div class="send" @click="sendBarrage($event)" v-if="IOS">
<slot name="rightRegion"></slot>
</div>
<div class="send" @mousedown="sendBarrage($event)" v-else>
<slot name="rightRegion"></slot>
</div>
</template>
</user-input>
</div>

</template>
export default {
created () {
this.barrageManager = new BarrageManager(this)
},
mounted() {
// 初始化弹幕渲染数据
this.initBarrageRenderData();
},
data() {
return {
barrageManager : null,
isClickSend: false,
paused : false
};
},
methods : {
initBarrageRenderData() {
this.barrageManager.open(this.barrages);
},
},
computed : {
barrageFiltering() {
return {
level1:
this.barrageManager.barrages.filter(
item => item.level === barrageLevel.LEVEL1
) || [],
level2:
this.barrageManager.barrages.filter(
item => item.level === barrageLevel.LEVEL2
) || []
};
},
}
}

视图层知识点回顾


在这里我们在弹幕组件创建的时候去创建了一个 弹幕管理对象,并且在挂载的时候去初始化了以下 弹幕渲染的数据,于是我们调用了 弹幕管理类open方法,这样当组件挂载时,就会去渲染 barrageFiltering 数据,这里我们是在管理类中拿到了管理类中循环创建的数据。


open 方法实现


到这里我们的弹幕的开启基本上已经完成了,可以看得出,如果你是这样设计的,你只需要在组件中调用管理类的一些方法,它就能帮你完成一些功能。


3: 实现弹幕关闭功能


barrageManager.js


 class BarrageManager {
constructor(barrageVue) {
this.barrages = []; // 填弹幕的数组
this.barragesIds = [] // 批量删除弹幕的数组id
this.sourceBarrages = [] // 源弹幕数据
this.timer = null //控制弹幕的开启和关
this.barrageVue = barrageVue // 弹幕组件实例
this.deleteCount = 0, // 销毁弹幕的总数
this.lastDeleteCount = 0, // 最后可销毁的数量
this.row = 0,
this.count = 0
}

/**
* @return 无返回值
* @description 调用close 方法 关闭弹幕
*/

close() {
clearInterval(this.timer)
this.removeAllBarrage()
}
/**
* @description 删除全部的弹幕数据
*/

removeAllBarrage() {
this.barrages = []
}
}


关闭功能知识点回顾


在这里我们可以看到,关闭弹幕的功能其实很简单,你只需要把开启弹幕时的定时器关闭,并且把弹幕数组数据清空就可以了


4: 实现弹幕添加功能


index.vue



addBarrage(barrageContent) {
// 获取当前 定时器正在创建的 一行
let currentRow = this.barrageManager.getRow();
let row = currentRow === this.rows / 2 ? 0 : currentRow + 1;
if (row === this.rows / 2) {
row = 0;
}
let myBarrage = {
row,
barrageId: '1686292223004',
barrageContent,
style: this.style,
type: "mySelf", // 用户自己发布的弹幕类型
barrageCategory: this.userBarrageType
};

const item = this.crearteBarrageObject(myBarrage);

this.barrageManager.addBarrage(item); // 数据准备好了 调用添加方法

console.info("发送成功")

this.barrageManager.setRow(row + 1);
},

barrageManager.js


 class BarrageManager {
constructor(barrageVue) {
this.barrages = []; // 填弹幕的数组
this.barragesIds = [] // 批量删除弹幕的数组id
this.sourceBarrages = [] // 源弹幕数据
this.timer = null //控制弹幕的开启和关
this.barrageVue = barrageVue // 弹幕组件实例
this.deleteCount = 0, // 销毁弹幕的总数
this.lastDeleteCount = 0, // 最后可销毁的数量
this.row = 0,
this.count = 0
}
/**
*
* @param {*} obj 合并完整的的弹幕对象
* @param {...any} args 开发者以后可能需要传递的剩余参数
*/

addBarrage(obj, ...args) {
const barrage = new Barrage(obj, ...args)
this.barrages.push(barrage)
}
}

添加功能知识点回顾


在这里我们可以看到,添加的时候,我们 组件 只需要去调用 addBarrage 方法进行弹幕添加,并且在调用的过程中我们去 new Barrage 这个类 , 也就是我们之前准备好的 弹幕数据类 | 数据层设计


5: 实现弹幕删除功能


class BarrageManager {
constructor(barrageVue) {
this.barrages = []; // 填弹幕的数组
this.barragesIds = [] // 批量删除弹幕的数组id
this.sourceBarrages = [] // 源弹幕数据
this.timer = null //控制弹幕的开启和关
this.barrageVue = barrageVue // 弹幕组件实例
this.deleteCount = 0, // 销毁弹幕的总数
this.lastDeleteCount = 0, // 最后可销毁的数量
this.row = 0,
this.count = 0
}

/**
*
* @param {*} barrageId // 入参 弹幕id
* @returns 无返回值
* @description 添加需要批量删除的 id 到 批量删除的栈中 barragesIds
*/

addBatchRemoveId(barrageId) {
this.barragesIds.push(barrageId)
this.batchRemoveHandle()
}
/**
*
* @param {*} start 你需要从第几位开始删除
* @param {*} deleteCount // 删除的总数是多少个
* @returns 无返回值
*/

batchRemoveBarrage(start, deleteCount) {
if (this.barrages.length === 0) return
this.barrages.splice(start, deleteCount)
}
batchRemoveId(start, deleteCount) {
if (this.barragesIds.length === 0) return
this.barragesIds.splice(start, deleteCount)
}
/**
* @param {*} barrageId 弹幕 id 针对单个删除弹幕时 使用
*/

removeBarrage(barrageId) {
let index = this.barrages.findIndex(item => item.barrageId === barrageId)
this.barrages.splice(index, 1)
}
/**
* @description 删除全部的弹幕数据
*/

removeAllBarrage() {
this.barrages = []
}
// 批量移除逻辑处理
batchRemoveHandle() {
if (this.deleteCount === 0 || this.deleteCount === 0) {
if (this.barragesIds.length === this.lastDeleteCount) {
this.batchRemoveBarrage(0, this.lastDeleteCount)
this.batchRemoveId(0, this.lastDeleteCount)
}
} else {
if (this.barragesIds.length === deleteQuantity.FIFTY) {
this.batchRemoveBarrage(0, deleteQuantity.FIFTY)
this.batchRemoveId(0, deleteQuantity.FIFTY)
this.deleteCount--
}
}
}
}

删除功能知识点回顾


在这里我们可以看到,删除的时候我们把每一个弹幕id加入到了一个数组中 , 当 弹幕id数组长度达到我想要删除的数量的时候, 调用 splice 方法 执行批量删除操作,当数据发生更新,视图也会更新,这样我们只需要执行一次dom操作,不需要每一次删除弹幕更新dom,造成不必要的性能消耗。


5: 实现弹幕重置功能


到这里,我相信你已经明白了我的设计,如果现在让你实现一个 重置弹幕方法 你会怎么做 ? 是不是只需要,调用一下 close 方法 , 然后再去 调用 open方法就可以了,ok 接下来我会将完整版代码 放入我的github仓库,小伙伴们可以去拉取 仓库链接,具体代码还需要小伙伴们自己从头阅读一次,这里只是说明了部分内容 , 阅读完成后 , 你就会彻底理解。


关于 barrageTypeCallback 函数


这个方法主要是可以解决弹幕样式定制的问题,你可以根据每个弹幕的类型 做不同的样式对象返回,我们会自动帮你渲染。


barrageTypeCallback ( {args} ) {

const { barrageCategary } = args

if(barrageCategary === 'type1'){

retun {
className : 'classOne',
children : {
show : false
i : {
showIcon : false,
style : {
color : 'red'
}
}
}
}
}
else{

return { className : 'default' }
}
}




结束语


前面的所有代码只是想告诉大家这个设计思想,当你的思维模型出来以后,其实很轻松。


我是 前端小张同学

作者:前端小张同学
来源:juejin.cn/post/7243680440694980668
期待你的关注,谢谢。

收起阅读 »

Android 即将进入大AI时代

一. 前言 自从OpenAI流行之后,我对这一块的方向还是比较关注的。前段时间Google IO大会AI部分也是占了很大的比重了,而且从google的部署来看,也差不多是往我预期的方向去发展,我所关注的东西其实很简单,就是辅助开发,而Google IO大会中的...
继续阅读 »

一. 前言


自从OpenAI流行之后,我对这一块的方向还是比较关注的。前段时间Google IO大会AI部分也是占了很大的比重了,而且从google的部署来看,也差不多是往我预期的方向去发展,我所关注的东西其实很简单,就是辅助开发,而Google IO大会中的内容也让我意识到了,他们确实有在往这个方向去发展。虽然现在还处于一个比较鸡肋的阶段,但是这是一个进入大AI时代的信号。


二. 现状


对于现在的一个环境而言,AI是已经能进行一些基础的辅助开发了。最简单的做法我之前也有说过一些,juejin.cn/post/721986…


这时候有人就说了,我们这些做开发的,还要搞这些打开网页后复制粘贴的操作,太low了。没错,所以一般我们希望能做到的第一步就是集成,把AI集成到我们的IDE中。


1. Studio Bot


而这次的Google IO大会就有这么一个东西 Studio Bot ,把AI集成到AndroidStudio中,注意,这是官方的,虽然现在相当于一个试用的阶段,但至少也能看出了官方的决心:要做一款google自己的AI工具 ,而往往google这几年出的东西都是比较香的,所以我很看好几年后能使用到成熟的AI工具。


想要了解的可以去看看官网 developer.android.com/studio/prev… ,首先需要下载最新的版本Android Studio Hedgehog,然后按照流程去注册使用Studio Bot,文档卡里面讲得还是比较清楚的,我这里就不重复搬过来说了。


但其实你别看它这个东西提出来了,其实当前还是比较鸡肋的,而且现在使用的人很少,后续可能还会进行优化和功能的扩充。我建议大家看看演示就行了,没必要下载预览版来尝试,首先预览版会有很多问题,其次上面说了,当前的功能比较鸡肋,估计你就玩个一两个小时就失去兴趣了,现在用来直接辅助开发我觉得还尚早。


讲完官方的,我们可以来讲讲目前成熟的插件。


2. Bito


Bito是ChatGPT团队开发的一款插件,而我们的IDE能够使用这款插件来辅助开发,想要了解的话可以看看官网的介绍 bito.ai/


AndroidStudio使用Bito的方法也很简单,首先在搜索这个插件


image.png


然后安装,然后点击Help ->Find Action,输入Choose Boot Java Runtime for the IDE


image.png


select runtime中JCEF有两个一模一样的,有一个是有问题的,有一个是正常的,试试就知道了。安装之后重启,然后点AS右边的Bito进行登录就能使用了


image.png


没有号的话注册一个就行,流程挺简单的,登录之后就可以直接使用


image.png


Bito的好处就是整个流程引入下来很方便,但是它也有很明显的缺点,那就是太慢了,不知道是因为使用的人多还是什么问题,它的回复速度非常的慢。我自己是给了它一个json,然后让他生成一个kotlin的data类,结果很久才生成出来,我可能自己撸代码都撸完了。虽然慢,但也是能用,比如你有什么问题,还是可以问他的,但我是宁愿打开GPT网页直接使用


3. Github Copilot


Github Copilot和Bot一样是插件,而相对于Bot,Github Copilot会更快,而且能做到的更多(相对更多,其实也挺鸡肋)


使用的方法也是直接插件搜Github Copilot


image.png


安装,装完之后它会重启AS,然后弹出个通知让你去登录GitHub,你不小心关掉也没关系,AS的底部也有个图片能点出来


image.png


image.png


点Copy and Open 会打开github让你把Device code输入进去,没登录的话会先登录(Github总不能没号吧)


image.png


输入后你的github就会进入这个页面,你的github菜单就会多出一个Copliot


image.png


那么这是干什么的呢?这是收费的啊大哥,是可以有一个月的免费体验,但是你要有VISA,因为要填信息,你得填完信息才给用,或者你有教育邮件(就是学生或者老师),一般你上大学的话都会有个学校的邮箱。这个类似苹果那种教育优惠。


但我非常不建议学生使用这种方式去注册,因为我上面说了,这个功能其实不算成熟,你肯呢个玩个一两小时就失去兴趣了,然后你把你的信息给暴露出去,我觉得这有点得不偿失。


所以有VISA的可以体验一个月,有钱的,当我没说。那又没VISA,不用学校邮箱,要怎么弄呢?买一个号啊,这种东西在我神州大陆会缺?


Github Copilot相对于Bito的功能和性能都会强大一些,但是Github Copilot的引入就没有Bito的方便


4. 小结


这边介绍了3个目前AS能使用的AI工具,一个官方的工具studio bot,两个插件Bito和Github Copilot,加上直接在GPT网页打开GPT使用这4种方式中。


我个人肯定是最看好studio bot,官方出品,必属精品,其他IDE我不敢说,但是Android Studio未来肯定是studio bot最好用。


而就目前来说,无论使用哪种都有一定的成本,首先肯定是科学上网,GPT有些大佬会迁移出来,不科学上网也是能使用的。其次就是账号问题,像Github Copilot这种账号申请难度就比较高,我建议想用的话直接买号。


最后目前对于辅助开发而言(对于其他使用我可能不太清楚,我比较关心的是辅助开发的效果),功能上还不是很成熟,说得好听就是可能对我的开发流程而言用处不是很大,说得难听一点就是我自己敲代码都比他快。


有的人可能会觉得还是用处挺大的。我仅代表我自己的观点,像我开发的话习惯使用一些模板,快捷键和AS提供的一些工具。我不敢保证说每个人都会去使用这些工具,但就我而言,比如拿Bot来说,你可能会喜欢给他一些简单的逻辑让它生成代码,但是我使用模板使用AS的工具生成代码的速度比它更快。我反而会在一些比如说Json生成Data,下划线转驼峰,或者突然忘记一些知识点的时候去使用,但是这种情况下直接打开网页使用GPT我感觉更好。


三. 展望


就是这个google IO大会,提出AI这个方向之后,特别是今年提出这些东西之后,其实对之后的影响还是挺大的。这个可以一点一点慢慢说。


首先是Android的一个技术更新,像之前google提出的JetPack、kotlin、flutter等,其实都是很好的技术,都是要学的,现在相当于是重心放AI了,所以之后像这类技术的展现可能周期会相对长一些。


然后是大家比较关心的一个问题,会不会AI技术成熟之后,就不需要开发人员了,直接AI就行了,程序员全部失业。我觉得应该不会达到这种地步吧,如果真能达到这种地步,那AI都能自己给自己编程了,这种情况就超出想象了,完全就是科幻里面的那种。 但是我认为最终是能做到极致的辅助开发,宏观上来看就是能辅助我们把开发时间缩短到一半以下。那么就有可能会出现说公司只要一两个核心的成员,配合AI进行开发,就顶原本的五六个人开发。 这其实是要看老板吧,他觉得你的效率因为AI的配合能提高一倍,你一个人就能做两个人的事,那我干嘛养两个人,养一个人不更划算?当然老板也会觉得,那AI提高了你的效率,你就做完6点下班吧,不用加班了。其实话说到这里,懂的都懂。


所以这个发展是好还是坏,其实我也不清楚。但是单纯对开发来说,肯定是好的。那要怎样才能达到我说的辅助开发的地步,我又为什么这么看好studio bot?


这里得聊一些基础,


我们写代码,我们编译,打包APK等等这些操作,其实都是对文件操作,这个能理解吧。比如class文件用dx工具生成dex文件。然后这些操作,我可以写一个脚本去做吧,写脚本去操作文件。


比如我想用json生成kotlin的data类这件事,我可以用脚本去做,我可以用脚本接受json输入,然后按照data类的格式去让脚本生成一个data类的.kt文件,这个脚本是可以做到的,这个要先清楚。


那既然脚本能做到的事,AI你觉得做不做得到?为什么我不写脚本去完成这件事,因为这件事不麻烦,我反而写脚本更麻烦,但是使用AI去完成这件事并不麻烦啊,所以这是能很明显的提高开发的效率。


但是现阶段的AI的问题是什么呢?是拿不到上下文,简单来说它只能单纯的作为聊天工具,它拿不到我们项目的上下文。“帮我根据以下json生成一个data文件放到xxx目录下”,AI当前实现不了这个功能,但是它要做这个效果so esay。我们当前只能说让他生成个data类,然后它在聊天窗口给你生成,你自己创建一个data类然后复制它的内容过去。


所以我看好studio bot的原因之一是因为它是官方的,我觉得它未来是能拿到上下文的。举个简单的例子,它能拿到整个项目并且读写整个项目,那我们让它做的操作,和它说的话都是基于整个项目的,我就不会花费很多时间去给他描述我要做什么,也会省去很多步骤,我现在使用AI来和项目接轨都是要经过一些步骤进行转换的,而这些转换的时间还不如我直接自己撸代码。


如果它能拿到项目这个上下文,我对它说“帮我找xxx页面的布局”,它能帮我直接找的。或者说我们使用retrofit做网络请求,往往要写一些分散的代码,我直接和它说“根据链接、入参、出参新增一个名为xxx的请求”,它能帮我按照其他请求的格式去写到各个文件中。要是能做到这步,那对我们效率的提高就很大了。


“给这个XXXXActivity写个LiveData”,“检查当前类是否有内存泄露可能”,“帮我将当前的中文放到string.xml中”等等。这些操作其实都是开发中的重复操作,其实并没有很依赖业务,而我相信最终studio bot终将会做到这一步。


大AI时代已经开启,这股洪流又会将我们带到何处

作者:流浪汉kylin
来源:juejin.cn/post/7243725952789823525

收起阅读 »

为什么推荐用svg而不用icon?

web
为什么要用svg而没有用icon? 使用背景: 1.因为svg图标在任何设备下都可以高清显示,不会模糊。而icon会在显卡比较低的电脑上有显示模糊的情况 2.svg图标在页面render时 速度会比icon稍微快一点 3.实现小程序换肤功能 ;方案见:ht...
继续阅读 »

为什么要用svg而没有用icon?


使用背景:


图片.png



1.因为svg图标在任何设备下都可以高清显示,不会模糊。而icon会在显卡比较低的电脑上有显示模糊的情况


2.svg图标在页面render时 速度会比icon稍微快一点
3.实现小程序换肤功能 ;方案见:http://www.yuque.com/lufeilizhix…



// svg在html里的使用示例01
<div>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>home</title>
<path d="M32 18.451l-16-12.42-16 12.42v-5.064l16-12.42 16 12.42zM28 18v12h-8v-8h-8v8h-8v-12l12-9z"></path>
</svg>
</div>


SVG基础可参考:http://www.yuque.com/lufeilizhix…


Svg-inline的使用


//示例02

import iconShop from '../assets/menuIcon/shop.svg?inline'
import iconCustomer from '../assets/menuIcon/customer.svg?inline'
import iconCustomerService from '../assets/menuIcon/customerService.svg?inline'
import iconNuCoin from '../assets/menuIcon/nuCoin.svg?inline'
import iconBanner from '../assets/menuIcon/banner.svg?inline'
import iconAccount from '../assets/menuIcon/account.svg?inline'
import iconDataReport from '../assets/menuIcon/dataReport.svg?inline'
import iconVera from '../assets/menuIcon/banner_01.svg?inline'

inline svg是目前前端图标解决方案的最优解(当然不仅限于图标),而且使用方式也及其简单,只要将svg图标代码当成普通的html元素来使用即可,如:


<!-- 绘制右箭头 -->
<svg viewBox="0 0 1024 1024" height="1em" width="1em" fill="currentColor">
<path d="M665.6 512L419.84 768l-61.44-64 184.32-192L358.4 320l61.44-64 184.32 192 61.44 64z" />
</svg>

<!-- 绘制边框 -->
<svg viewBox="0 0 20 2" preserveAspectRatio="none" width="100%" height="2px">
<path d="M0 1L20 1" stroke="#000" stoke-width="2px"></path>
</svg>

注意: 新版chrome不支持 # , 需要改成%23 ;stroke="%23000"

作为图片或背景使用时


 icon: https://www.baidu.com+ '/icons/icon_01.svg' 
<image class="headIcon" src="data:image/svg+xml,{{icon}}"></image>
**特别注意 需要把img标签换成image标签**

将上面的代码插入html文档即可以很简单地绘制出一些图标。
正常情况下会将svg保存在本地,具体的页面中导入,参考示例02 作为组件使用;目的是可复用
一般来说,使用inline svg作为图标使用时,想要保留svg的纵横比,可以只指定width属性,但是一般为了清晰都同时指定height属性。但如果是像上面绘制边框这种不需要保留纵横比的情形,可将preserveAspectRatio设置为none


优势与使用方式


从示例01可以看到,将svg直接作为普通html元素插入文档中,其本质和渲染出一个div、span等元素无异,天生具有渲染快、不会造成额外的http请求等优势,除此之外还有以下优势之处:


样式控制更加方便;
inline svg顶层的元素会设置以下几个属性:


height=“1em” width=“1em” 可以方便地通过设置父元素的font-size属性控制尺寸


fill=“currentColor” 可以方便地根据父元素或自身的color属性控制颜色


但是我们也可以为其内部的子元素单独设置样式 参考


注意事项


如需对svg中各部分分别应用样式,则在设计svg时最好不要将各部分都编于一组,可以将应用相同样式的部分进行分别编组,其他不需要设置样式的部分编为一组,这样我们在应用样式时,只需为对应的标签设置class属性即可。


一般在拿到svg文件后,推荐使用svgo优化svg代码,节省体积,但是如果我们需要针对性设置样式时则需要谨慎使用,因为优化代码会进行路径合并等操作,可能我们想要设置的子元素已经不是独立的了。


inline svg的复用及组件化


同一个inline svg必须能够进行复用,将需要复用inline svg封装成组件


// 使用inline svg组件
import AnySvgIcon from './inline-svg-component'
<AnySvgIcon width="16px" height="16px" />

参考:


inline svg和字体图标的对比


字体图标

的使用与设计

收起阅读 »

这几个让代码清新的魔法,让领导追着给我涨薪

web
清晰易维护的代码对于任何软件项目的长期成功和可扩展性至关重要。它提升团队成员之间的协作效率,减少错误的可能性,并使代码更易于理解、测试和维护。在本博文中,我们将探讨一些编写清晰易维护的 JavaScript 代码的最佳实践,并提供代码示例以阐明每个实践方法。 ...
继续阅读 »

清晰易维护的代码对于任何软件项目的长期成功和可扩展性至关重要。它提升团队成员之间的协作效率,减少错误的可能性,并使代码更易于理解、测试和维护。在本博文中,我们将探讨一些编写清晰易维护的 JavaScript 代码的最佳实践,并提供代码示例以阐明每个实践方法。


1. 一致的代码格式:


一致的代码格式对于可读性非常重要。它有助于开发人员更快地理解代码,提升协作效果。使用一致且被广泛接受的代码风格指南,比如 ESLint 提供的指南,并配置你的编辑器或 IDE 以自动格式化代码。
示例:


// 错误的格式化
function calculateSum(a,b){return a+b; }

// 正确的格式化
function calculateSum(a, b) {
return a + b;
}

2. 有意义的变量和函数命名:


为变量、函数和类使用有意义且描述性的名称。避免使用单个字母或容易引起他人困惑的缩写。这种做法提高了代码的可读性,并减少了对注释的需求。
示例:


// 错误的命名
const x = 5;

// 正确的命名
const numberOfStudents = 5;

3. 模块化和单一职责原则:


遵循单一职责原则,为函数和类设定单一、明确的职责。这种做法提高了代码的可重用性,并使其更易于测试、调试和维护。
示例:


// 错误的做法
function calculateSumAndAverage(numbers) {
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
const average = sum / numbers.length;
return [sum, average];
}

// 正确的做法
function calculateSum(numbers) {
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
return sum;
}

function calculateAverage(numbers) {
const sum = calculateSum(numbers);
const average = sum / numbers.length;
return average;
}

4. 避免全局变量:


尽量减少使用全局变量,因为它们可能导致命名冲突,并使代码更难以理解。相反,封装你的代码到函数或模块中,并尽可能使用局部变量。
示例:


// 错误的做法
let count = 0;

function incrementCount() {
count++;
}

// 正确的做法
function createCounter() {
let count = 0;

function incrementCount() {


count++;
}

return {
incrementCount,
getCount() {
return count;
}
};
}

const counter = createCounter();
counter.incrementCount();

5. 错误处理和鲁棒性:


优雅地处理错误,并提供有意义的错误信息或适当地记录它们。验证输入,处理边界情况,并使用正确的异常处理技术,如 try-catch 块。
示例:


// 错误的做法
function divide(a, b) {
return a / b;
}

// 正确的做法
function divide(a, b) {
if (b === 0) {
throw new Error('Cannot divide by zero');
}
return a / b;
}

try {
const result = divide(10, 0);
console.log(result);
} catch (error) {
console.error(error.message);
}

6. 避免重复代码:


代码重复不仅会导致冗余代码,还会增加维护和修复错误的难度。将可重用的代码封装到函数或类中,并努力遵循 DRY(Don't Repeat Yourself)原则。如果发现自己在复制粘贴代码,请考虑将其重构为可重用的函数或模块。
示例:


// 错误的做法
function calculateAreaOfRectangle(length, width) {
return length * width;
}

function calculatePerimeterOfRectangle(length, width) {
return 2 * (length + width);
}

// 正确的做法
function calculateArea(length, width) {
return length * width;
}

function calculatePerimeter(length, width) {
return 2 * (length + width);
}

7. 明智地使用注释:


干净的代码应该自解释,但有些情况下需要注释来提供额外的上下文或澄清复杂的逻辑。谨慎使用注释,并使其简洁而有意义。注重解释“为什么”而不是“如何”。
示例:


// 错误的做法
function calculateTotalPrice(products) {
// 遍历产品
let totalPrice = 0;
for (let i = 0; i < products.length; i++) {
totalPrice += products[i].
price;
}
return totalPrice;
}

// 正确的做法
function calculateTotalPrice(products) {
let totalPrice = 0;
for (let i = 0; i < products.length; i++) {
totalPrice += products[i].
price;
}
return totalPrice;
// 总价格通过将数组中所有产品的价格相加来计算。
}

8. 优化性能:


高效的代码提升了应用程序的整体性能。注意不必要的计算、过度的内存使用和潜在的瓶颈。使用适当的数据结构和算法来优化性能。使用类似 Chrome DevTools 的工具对代码进行性能分析和测量,以识别并相应地解决性能问题。


示例:


// 错误的做法
function findItemIndex(array, target) {
for (let i = 0; i < array.length; i++) {
if (array[i] === target) {
return i;
}
}
return -1;
}

// 正确的做法
function findItemIndex(array, target) {
let left = 0;
let right = array.length - 1;

while (left <= right) {
const mid = Math.floor((left + right) / 2);

if (array[mid] === target) {
return mid;
}

if (array[mid] < target) {
left = mid +
1;
}
else {
right = mid -
1;
}
}

return -1;
}

9. 编写单元测试:


单元测试对于确保代码的正确性和可维护性非常重要。编写自动化测试以覆盖不同的场景和边界情况。这有助于尽早发现错误,便于代码重构,并对修改现有代码充满信心。使用像 Jest 或 Mocha 这样的测试框架来编写和运行测试。
示例(使用 Jest):


// 代码
function sum(a, b) {
return a + b;
}

// 测试
test('sum 函数正确地相加两个数字', () => {
expect(sum(2, 3)).toBe(5);
expect(sum(-1, 5)).toBe(4);
expect(sum(0, 0)).toBe(0);
});

10. 使用函数式编程概念:


函数式编程概念,如不可变性和纯函数,可以使代码更可预测且更易于理解。拥抱不可变数据结构,并尽量避免对对象或数组进行突变。编写无副作用且对于相同的输入产生相同输出的纯函数,这样更容易进行测试和调试。
示例:


// 错误的做法
let total = 0;

function addToTotal(value) {
total += value;
}

// 正确的做法
function addToTotal(total, value) {
return total + value;
}

11. 使用 JSDoc 文档化代码:


使用 JSDoc 来为函数、类和模块编写文档。这有助于其他开发人员理解你的代码,并使其更易于维护。


/**
* 将两个数字相加。
* @param {number} a - 第一个数字。
* @param {number} b - 第二个数字。
* @returns {number} 两个数字的和。
*/

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

12. 使用代码检查工具和格式化工具:


使用 ESLint 和 Prettier 等工具来强制执行一致的代码风格,并在问题出现之前捕获潜在问题。


// .eslintrc.json
{
"extends": ["eslint:recommended", "prettier"],
"

plugins"
: ["prettier"],
"rules": {
"prettier/prettier": "error"
}
}

// .prettierrc.json
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all"
}

结论:


编写清晰且易于维护的代码不仅仅是个人偏好的问题,而是一项专业责任。通过遵循本博文中概述的最佳实践,您可以提高 JavaScript 代码的质量,使其更易于理解、维护和协作,并确保软件项目的长期成功。在追求清晰且易于维护的代码时,请牢记一致性、可读性、模块化和错误处理这些关键原则。祝你编码愉快!

原文:dev.to/wizdomtek/b…
翻译 / 润色:ssh

作者:ssh_晨曦时梦见兮
来源:juejin.cn/post/7243680592192782393

收起阅读 »

速度优化:重新认识速度优化

应用的速度优化是我们使用最频繁,也是应用最重要的优化之一,它包括启动速度优化,页面打开速度优化,功能或业务执行速度优化等等,能够直接提升应用的用户体验。因此,只要是 Android 开发者,肯定或多或少有过速度相关的优化经验。但是大部分人谈到速度优化,只能想到...
继续阅读 »

应用的速度优化是我们使用最频繁,也是应用最重要的优化之一,它包括启动速度优化,页面打开速度优化,功能或业务执行速度优化等等,能够直接提升应用的用户体验。因此,只要是 Android 开发者,肯定或多或少有过速度相关的优化经验。但是大部分人谈到速度优化,只能想到一些零碎的优化点,比如使用多线程、预加载等等。这对速度的提升肯定是不够的,想要做得更好,我们不妨来思考下面几个问题:




  • 我们的优化方案是全面且体系的吗?




  • 我们的方案为什么能提升速度呢?




  • 我们的方案效果怎样?




想要回答好这几个问题,我们就需要了解影响和决定应用速度的底层原理及本质。那从底层来看,CPU、缓存、任务调度才是决定应用速度最本质的因素。CPU 和缓存都属于硬件层,任务调度机制则属于操作系统层。


那这一节课,我们就一起深入硬件和操作系统层面去了解以上三个因素是如何决定应用速度的,重新认识应用的速度优化,由下而上地建立起速度优化的认知体系和方法。


如何从 CPU 层面进行速度优化?


我们知道,所有的程序最终会被编译成机器码指令,然后交给 CPU 执行,CPU 以流水线的形式一条一条执行程序的机器码指令。当我们想要提升某些场景(如启动、打开页面、滑动等)的速度时,本质上就是降低 CPU 执行完这些场景指令的时间,这个时间简称为 CPU 时间。想要降低 CPU 时间,我们需要先知道程序所消耗 CPU 时间的计算公式:CPU 时间=程序的指令数 x 时钟周期时间 x 每条指令的平均时钟周期数。下面一一解释一下这三项因子的含义。




  • 程序的指令数:这一项很好理解,就是程序编译成机器码指令后的指令数量。




  • 时钟周期时间:每一次时钟周期内,CPU 仅完成一次执行,所以时钟周期时间越短,CPU 执行得越快。或许你对时钟周期时间不熟悉,但是它的倒数也就是时钟周期频率,你肯定听说过。1 纳秒的时钟周期时间就是 1 GHZ 的时钟周期频率,厂商发布新手机或者我们购买新手机时,都或多或少会提到 CPU 的时钟频率,比如高通骁龙 888 这款 CPU 的时钟频率是 2.8 GHZ,这个指标也是衡量 CPU 性能最重要的一个指标




  • 每条指令的平均时间周期:是指令执行完毕所消耗的平均时间周期,指令不同所需的机器周期数也不同。对于一些简单的单字节指令,在取指令周期中,指令取出到指令寄存器后会立即译码执行,不再需要其它的机器周期。对于一些比较复杂的指令,例如转移指令、乘法指令,则需要两个或者两个以上的机器周期。




从 CPU 来看,当我们想要提升程序的速度时,优化这三项因子中的任何一项都可以达到目的。那基于这三项因子有哪些通用方案可以借鉴呢?


减少程序的指令数


通过减少程序的指令数来提升速度,是我们最常用也是优化方案最多的方式,比如下面这些方案都是通过减少指令数来提升速度的。




  1. 利用手机的多核:当我们将要提速的场景的程序指令交给多个 CPU 同时执行时,对于单个 CPU 来说,需要执行的指令数就变少了,那 CPU 时间自然就降低了,也就是并发的思想。但要注意的是,并发只有在多核下才能实现,如果只有一个 CPU,即使我们将场景的指令拆分成多份,对于这个 CPU 来说,程序的指令数依然没有变少。如何才能发挥机器的多核呢?使用多线程即可,如果我们的手机是 4 核的,就能同时并发的运行 4 个线程。




  2. 更简洁的代码逻辑和更优的算法:这一点很好理解,同样的功能用更简洁或更优的代码来实现,指令数也会减少,指令数少了程序的速度自然也就快了。具体落地这一类优化时,我们可以用抓 trace 或者在函数前后统计耗时的方式去分析耗时,将这些耗时久的方法用更优的方式实现。




  3. 减少 CPU 的闲置:通过在 CPU 闲置的时候,执行预创建 View,预准备数据等预加载逻辑,也是减少指令数的一种优化方案,我们需要加速场景的指令数量由于预加载执行了一部分而变少了,自然也就快了。




  4. 通过其他设备来减少当前设备程序的指令数:这一点也衍生很多优化方案,比如 Google 商店会把某些设备中程序的机器码上传,这样其他用户下载这个程序时,便不需要自己的设备再进行编译操作,因为提升了安装或者启动速度。再比如在打开一些 WebView 网页时,服务端会通过预渲染处理,将 IO 数据都处理完成,直接展示给用户一个静态页面,这样就能极大提高页面打开速度。




上面提到的这些方案都是我们最常用的方案,基于指令数这一基本原理,还能衍生出很多方案来提升速度,这里没法一一列全,大家也可以自己想一想还能扩展出哪些方案出来。


降低时钟周期时间


想要降低手机的时钟周期,一般只能通过升级 CPU 做到,每次新出一款 CPU,相比上一代,不仅在时钟周期时间上有优化,每个周期内可执行的指令也都会有优化。比如高通骁龙 888 这款 CPU 的大核时钟周期频率为 2.84GHz,而最新的 Gen 2 这款 CPU 则达到了 3.50GHz。


虽然我们没法降低设备的时钟周期,但是应该避免设备提高时钟周期时间,也就是降频现象,当手机发热发烫时,CPU 往往都会通过降频来减少设备的发热现象,具体的方式就是通过合理的线程使用或者代码逻辑优化,来减少程序长时间超负荷的使用 CPU。


降低每条指令的平均时间周期


在降低每条指令的平均时间周期上,我们能做的其实也不多,因为它和 CPU 的性能有很大的关系,但除了 CPU 的性能,以下几个方面也会影响到指令的时间周期。




  1. 编程语言:Java 翻译成机器码后有更多的简介调用,所以比 C++ 代码编译成的机器码指令的平均时间周期更长。




  2. 编译程序:一个好的编译程序可以通过优化指令来降低程序指令的平均时间周期。




  3. 降低 IO 等待:从严格意义来说,IO 等待的时间并不能算到指令执行的耗时中,因为 CPU 在等待 IO 时会休眠或者去执行其他任务。但是等待 IO 会使执行完指令的时间变长,所以这里依然把减少 IO 等待算入是降低每条指令的平均时间周期的优化方案之一。




如何从缓存层面进行速度优化?


程序的指令并不是直接就能被 CPU 执行的,而是要放在缓存中,CPU 从缓存中读取,而且一个程序也不可能全是 CPU 计算逻辑,必然也会涉及到 IO 的操作或等待,比如往磁盘或者内存中读写数据成功后才能继续执行后面的逻辑,所以缓存也是决定应用速度的关键因素之一。缓存对程序速度的影响主要体现在 2 个方面:




  1. 缓存的读写速度;




  2. 缓存的命中率。




下面就详细讲解一下这 2 方面对速度的影响。


缓存的读写速度


手机或电脑的存储设备都被组织成了一个存储器层次结构,在这个层次结构中,从上至下,设备的访问速度越来越慢,但容量也越来越大,并且每字节的造价也越来越便宜。寄存器文件在层次结构中位于最顶部,也就是第 0 级。下图展示的是三层高速缓存的存储结构。


img


高速缓存是属于 CPU 的组成部分,并且实际有几层高速缓存也是由 CPU 决定的。以下图高通骁龙 888 的芯片为例,它是 8 块核组成的 CPU,从架构图上可以看到,它的 L2 是 1M 大小(没有 L1 是因为这其实只是序号称呼上的不同而已,你也可以理解成 L1),L3 是 3M 大小,并且所有核共享。


img


不同层之间的读写速度差距是很大的,所以为了能提高场景的速度,我们需要将和核心场景相关的资源(代码、数据等)尽量存储在靠上层的存储器中。 基于这一原理,便能衍生出了非常多的优化方案,比如常用的加载图片的框架 Fresco,请求网络的框架 OkHttp 等等,都会想尽办法将数据缓存在内存中,其次是磁盘中,以此来提高速度。


缓存的命中率


将数据放在缓存中是一种非常入门的优化思想,也是非常容易办到的,即使是开发新手都能想到以此来提升速度。但是我们的缓存容量是有限的,越上层的缓存虽然访问越快,但是容量越少,价格也越贵,所以我们只能将有限的数据存放在缓存中,在这样的制约下,提升缓存的命中率往往是一件非常难的事情


一个好的编译器可以提升寄存器的命中率,好的操作系统可以提升高速缓存的命中率,对于我们应用来说,好的优化方案可以提升主存和硬盘的命中率,比如我们常用的 LruCache 等数据结构都是用来提升主存命中率的。除了提升应用的主存,应用也可以提升高速缓存的命中率,只是能做的事情不多,后面的章节中也会介绍如何通过 Dex 中 class 文件重排,来提升高速缓存读取类文件时的命中率。


想要提高缓存命中率,一般都是利用局部性原理(局部性原理指如果某数据被访问,则不久之后该数据可能再次被访问,或者程序访问了某个存储单元,则不久之后,其附近的存储单元也将被访问)或者通过行为预测,分析大概率事件等多种原理来提高缓存命中率。


如何从任务调度层面进行速度优化?


我们学过操作系统为了能同时运行多个程序,所以诞生了虚拟内存这个技术,但只有虚拟内存技术是不够的,还需要任务调度机制,所以任务调度也属于操作系统关键的组成之一。有了任务调度机制,我们的程序才能获得 CPU 的资源并正常跑起来,所以任务调度也是影响程序速度的本质因素之一


我们从两个方面来熟悉任务调度机制,一是调度机制的原理,二是任务的载体,即进程的生命周期。


在 Linux 系统中,任务调度的维度是进程,Java 线程也属于轻量级的进程,所以线程也是遵循 Linux 系统的任务调度规则的,那进程的调度规则又是怎样的呢?Linux 系统将进程分为了实时进程和普通进程这两类,实时进程需要响应技术的进程,比如 UI 交互进程,而普通进程对响应速度要求不是非常高,比如读写文件、下载等进程。两种类型的进程的调度规则也不一样,我们分别来说。


首先是实时进程的调度规则。Linux 系统对实时进程的调度策略有两种:先进先出(SCHED_FIFO)和循环(SCHED_RR)。Android 只使用了 SCHED_FIFO 这一策略,所以我们主要介绍 SCHED_FIFO 。当系统使用先进先出的策略来调度进程时,如果某个进程占有 CPU 时间片,此时没有更高优先级的实时进程抢占 CPU,或该进程主动让出,那么该进程就始终保持使用 CPU 的状态。这种策略会提高进程运行的持续时间,减少被打断或被切换的次数,所以响应更及时。Android 中的 AudIO、SurfaceFlinger、Zygote 等系统核心进程都是实时进程。


非实时进程也称为普通进程,针对普通进程,Linux 系统则采用了一种完全公平调度算法来实现对进程的切换调度,我们可以不需要知道这一算法的实现细节,但需要了解它的原理。在完全公平调度算法中,进程的优先级由 nice 值表示,nice 值越低代表优先级越大,但是调度器并不是直接根据 nice 值的大小作为优先级来进行任务调度的,当每次进程的时间片执行完后,调度器就会寻找所有进程中运行时间最少的进程来执行


既然调度器是根据进程的运行时间来进行任务调度,那进程优先级即 nice 值的作用又体现在哪呢?实际上,这里进程的运行时间并不是真实的物理运行时间,而是进行了加权计算的虚拟时间,这个权值系数就是 nice 值,所以同样的物理时间内,nice 值越低的进程所记录的运行时间实际越少,运行时间更少就更容易被调度器所选择,优先级也就这样表现出来了。在 Android 中,除了部分核心进程,其他大部分都是普通进程。


了解了进程的调度原理,我们再来了解一下进程的生命周期。


img


通过上图可以看到,进程可能有以下几种状态。并且运行、等待和睡眠这三种状态之间是可以互相转换的。




  • 运行:该进程此刻正在执行。




  • 等待:进程能够运行,但没有得到许可,因为 CPU 分配给另一个进程。调度器可以在下一次任务切换时选择该进程。




  • 睡眠:进程正在睡眠无法运行,因为它在等待一个外部事件。调度器无法在下一次任务切换时选择该进程。




  • 终止:进程终止。




知道了任务调度相关的原理后,怎样根据这些原理性知识来优化应用场景的速度呢?实际上,我们对进程的优先级做不了太大的改变,即使改变了也产生不了太大的作用,但是前面提到了线程实际是轻量级的进程,同样遵循上面的调度原理和规则,所以我们真正落地的场景在线程的优化上。基于任务调度的原理,我们可以衍生出这 2 类的优化思路:




  1. 提高线程的优先级:对于关键的线程,比如主线程,我们可以提高它的优先级,来帮助我们提升速度。除了直接提高线程的优先级,我们还可以将关键线程绑定 CPU 的大核这一种特殊的方式来提高该线程的执行效率。




  2. 减少线程创建或者状态切换的耗时:这一点可以通过在线程池中设置合理的常驻线程,线程保活时间等参数来减少线程频繁创建或者状态切换的耗时。因为线程池非常重要,我们后面会专门用一节课来详细讲解。




小结


在这一节中,我们详细介绍了影响程序速度的三个本质因素,并基于这三个因素,介绍了许多衍生而来优化思路,这其实就是一种自下而上的性能优化思路,也就是从底层原理出发去寻找方案,这样我们在进行优化时,才能更加全面和体系。


希望你通过这一节的学习,能对速度优化建立起一个体系的认知。当然,你可能会觉得我们这一节介绍的优化思路太过简洁,不必担心,在后面的章节中,我们会基于 CPU、缓存和任务调度这三个维度,挑选出一些优化效果较好的方案,进行更加深入的详细讲解。


作者:helson赵子健
链接:https://juejin.cn/post/7235279096312856637
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

程序员浪漫起来 做一个心形layout

序言 最近浪漫心饱满,于是写了个心形控件。可以将一切内容约束成心形,并且支持两种心形,使用起来很简单,就当成FrameLayout就行了。 让一切都浪漫起来 效果 下面依次展示的是: 包裹ScrollView ,形状为桃心 包裹ScrollView,形状为圆...
继续阅读 »

序言


最近浪漫心饱满,于是写了个心形控件。可以将一切内容约束成心形,并且支持两种心形,使用起来很简单,就当成FrameLayout就行了。
让一切都浪漫起来


效果


下面依次展示的是:



  1. 包裹ScrollView ,形状为桃心

  2. 包裹ScrollView,形状为圆心

  3. 包裹WebView,形状为圆心


在这里插入图片描述


代码


LoveLayout


主要代码就是下面的。

package com.example.myapplication;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Path;
import android.util.AttributeSet;
import android.widget.FrameLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

/**
* <pre>
* Created by zhuguohui
* Date: 2023/6/6
* Time: 10:26
* Desc:爱心Layout
* </pre>
*/
public class LoveLayout extends FrameLayout {

private Path path1;
private int heardType;

public LoveLayout(@NonNull Context context) {
super(context);
}

public LoveLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.LoveLayout);
heardType = array.getInt(R.styleable.LoveLayout_HeardType,0);
array.recycle();

}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if(heardType==0) {
getHeardPath();
}else{
getHeardPath2();
}
}

private void getHeardPath(){

int px = getMeasuredWidth() / 2;
int py = getMeasuredHeight() / 2;
path1=new Path();
path1.moveTo(px, py);
float rate=getMeasuredWidth()*1.0f/34;
// 根据心形函数画图
for (double i = 0; i < 2 * Math.PI; i += 0.001) {
float x = (float) (16 * Math.sin(i) * Math.sin(i) * Math.sin(i));
float y = (float) (13 * Math.cos(i) - 5 * Math.cos(2 * i) - 2 * Math.cos(3 * i) - Math.cos(4 * i));
x *= rate;
y *= rate;
x = px - x;
y = py - y;
path1.lineTo(x, y);
}
}

private void getHeardPath2(){
// f(x)=sqrt(1-(abs(x)-1)^2)
// h(x)=-2*sqrt(1-0.5*abs(x))
path1=getPathByMathFunction(x -> (float) Math.sqrt(1-Math.pow((Math.abs(x)-1),2)));
Path path2=getPathByMathFunction(x->(float) (-2* Math.sqrt(1-0.5*Math.abs(x))));
path1.moveTo(0,getMeasuredHeight()*1.0f/2);
path1.addPath(path2);
}

private interface MathFunction{
float call(float x);
}

private Path getPathByMathFunction(MathFunction function){
Path path=new Path();
path.moveTo(0,getMeasuredHeight()*1.0f/2);
int px = getMeasuredWidth() / 2;
int py = getMeasuredHeight() / 2;
float scale=getMeasuredWidth()*1.0f/4;
for(float i=-2;i<=2;i+=0.01){
float x=i;
float y= function.call(x);
x*=scale;
y*=scale;
x=px-x;
y=py-y;

path.lineTo(x,y);
}
return path;
}

@Override
protected void dispatchDraw(Canvas canvas) {
int save = canvas.save();
canvas.clipPath(path1);
super.dispatchDraw(canvas);
canvas.restoreToCount(save);
}
}


xml属性

<?xml version="1.0" encoding="utf-8"?>
<resources>


<declare-styleable name="LoveLayout">

<attr name="HeardType">
<!-- 桃心 -->
<enum name="PeachHeart" value="0" />
<!-- 圆一点的心 -->
<enum name="CircularHeart" value="1" />
</attr>
</declare-styleable>


</resources>

数学原理


圆心是按照以下公式实现的,桃心是网上找到的代码改的,没找到公司。
在这里插入图片描述


使用


很简单,当成FrameLayout包裹就行了。
在这里插入图片描述


作者:solo_99
链接:https://juejin.cn/post/7241447629874659365
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android稳定性方案浅析

定义: 这里的Android稳定性单指Crash 指标口径: 用户可感知崩溃率,以用户为纬度,即Crash设备数 / 总设备数 精细化单个bug的纬度: bug频次 爆炸半径 影响时长 从崩溃的发生解决整个链路出发: RD编码 --> 线下测试 --...
继续阅读 »

定义: 这里的Android稳定性单指Crash


指标口径: 用户可感知崩溃率,以用户为纬度,即Crash设备数 / 总设备数


精细化单个bug的纬度:



  • bug频次

  • 爆炸半径

  • 影响时长


从崩溃的发生解决整个链路出发: RD编码 --> 线下测试 --> 灰度发版 --> 崩溃发生&数据采集聚类 --> RD修复 --> 版本发布


整体思路


image.png


1、编码阶段


制定代码规范,加强代码Review,引入静态代码检查,完善测试流程等减少问题发生


a、静态代码检查

有时会出现某些低级错误如:divide by zero 导致的crash,使用Lintdetekt进行静态代码检查不失为一种好方案,可以放在代码合入的SA阶段


b、查看是否需要解决警告问题

在我们编码或者改动历史遗留的代码时,AS中存在一些警告⚠️,commit时要根据提示进行review,是否需要每次修复和改动还有待商榷


c、检测SQL质量

参考Matrix SQLite Lint: 按官方最佳实践自动化检测 SQLite 语句的使用质量;(看能否拓展到其他场景)


d、依赖库的检查 (可以跟组件化相关)

采用gradle脚本编译时检查依赖库是否相同,解决不同组件或者不同APP之间SDK版本不一致可能导致的崩溃,避免CI阶段打包失败或者上线后出现异常



  • 新增or改动SDK检测,CI diff 产出依赖树 全功能提测阶段


e、review规范


  • 提交reviewer的质量把控意识;关键代码需要两个人review,+1 +2通过才可以合入

  • 跟流水线CI自动识别核心类文件要求两人review的能力结合起来


2、测试&流水线


提高稳定性的意识,不管多微小的改动都要进行自测!!!


a.单元测试


  • 重点模块的单测能力


b.自动化测试


  • 结合QA补齐自动化测试的能力,覆盖核心场景

  • 跟devops平台能力结合,将LeakCanary等能力跟Monkey结合,自动创建卡片

  • 针对函数接口的异常数据排雷测试(参考juejin.cn/post/702812…)


c.CI/CD


  • 流水线打包效率提升


3、崩溃数据采集&分析


正如RD讲的那样,给我一个崩溃堆栈我就能定位到问题所在,能完整的还原"事故现场"成为重中之重


a.堆栈反混淆

结合各个公司APM平台上传mapping文件以及符号表,每个崩溃数据均展示混淆之前的定位


b.平台堆栈聚类


  • 接入iqiyi开源的XCrash库或者breakpad,上报java和native的崩溃堆栈到Server并聚类展示

  • 接入开源的Sentry库并搭建本地私有化服务,实现崩溃的上报和聚类


c.平台数据状态标识

已修复、Pending、下个版本修复等状态或者备注的标识,每次分析结果留下文字记录


d.分模块上传关键数据

会员模块上传会员信息、支付模块上传订单信息等


4、灰度阶段


a.测试轨道前置小版本提前暴露问题

b.三方SDK降级策略

firebase、广告库等三方SDK升级一定要观察崩溃率变化,并做好降级处理


c.crash率异常熔断机制


  • 灰度过程缺少中间对Crash率异常的评定标准,业务异常的评定标准

  • 整体&每个崩溃数据的量级(该崩溃人数/安装率)记录,设定阈值,每日将两个版本对比骤增或者骤降的数据输出并产出报告


c.上车策略

核心思路是代码改动最小化,预留todo下迭代改,避免造成新的线上crash


5、重点问题解决


I.源码分析

虽然 androidxref.comcs.android.com 都可以在线查阅源码,但这两处的Android版本并不全;android.googlesource.com 这里可以下载到几乎所有版本的源码,本地通过 Sublime 分析源码也十分方便(可以直接显示和跳转到方法的定义&引用位置)。


II.OOM问题

a.大图监控治理


  • 线下监控:通过插件在 mergeResources 任务后,遍历图片资源,搜集超过阈值的图片资源,输出列表

  • 参考NativeBitmap 把应用内存使用的大头(即 Bitmap 的像素占用的内存)转移到 Native 堆;但是可能会导致32位虚拟内存不足

  • 在接口层中将所有被创建出来的 Bitmap 加入一个 WeakHashMap,同时记录创建 Bitmap 的时间、堆栈等信息,然后在适当的时候查看这个 WeakHashMap 看看哪些 Bitmap 仍然存活来判断是否出现 Bitmap 滥用或泄漏。 微信 Android 终端内存优化实践


b.hook pthred

采用bhook,针对pthread_create创建的子线程中发生了信号(SIGSEGV)进行兜底操作;相当于catch native crash,发生crash时重新执行之前的逻辑


c.32位虚拟内存优化


  • Patrons 通过一系列技术手段实现运行期间动态调整Region Space预分配的地址空间

  • mSponge 优化了虚拟机对 LargeObjectSpace 的内存管理策略,间接增加其它内存空间使用上限 (未开源)

  • pthread hook 对 native 线程的默认栈大小进行减半


image.png


d.监控

接入KOOM,进行线下OOM发生时的采样上报


III.native crash


6、防裂化&基础建设


a.版本回顾

崩溃数据自动化采集,以月度或者季度为纬度爬取数据,形成总结


b.崩溃保护和安全模式机制


  • 通过ASM在编译时对四大组件生命周期等关键代码加上try-catch处理

  • 通过一定的策略,针对反复重启导致的崩溃问题,让用户选择继续初始化或者清除数据


c.日志回捞


  • 全埋点用户操作路径辅助分析

  • 参考Logan进行日志回捞系统的建设,方便针对某一用户发生问题后捞回日志分析


d.移动端性能中台

自建集崩溃监控、上报、分析、归因于一体(可以参考Matrix直接建立),可以轻松定位各种线上疑难杂症,更有超详细性能、卡顿、打点等全流程监控处理平台


作者:陈泡泡_
链接:https://juejin.cn/post/7240037613793361976
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

我竟然突然焦虑,并且迷茫了

随想录】我尽然突然焦虑,并且迷茫了 「随想录」 这是师叔对自我现状的剖析和寻找了一些 “新的方向” “新的视角” 来重新审视自我的思想录,希望我的家银们在文章中得到思想启发或以我为鉴,不去做无谓思想内耗! 最近是怎么了 最近几个朋友,突然询问我,现在应该怎...
继续阅读 »

随想录】我尽然突然焦虑,并且迷茫了



「随想录」


这是师叔对自我现状的剖析和寻找了一些 “新的方向” “新的视角” 来重新审视自我的思想录,希望我的家银们在文章中得到思想启发以我为鉴,不去做无谓思想内耗



最近是怎么了


最近几个朋友,突然询问我,现在应该怎么学习,将来才会更好的找工作,怕毕业以后没有饭吃,我说我其实也不太清楚,我目前三段实习我都没有找到一份真正意义的好工作,就是那种我喜欢这门领域,并且喜欢公司的氛围,并且到老了还能保持竞争力(莫有35岁危机)。



所以说我真的没有一个准确的答案回复。但是我以为目前的眼光来看一份好工作必备的条件就是,我在这个领域学的越多,我的工资和个人发展瓶颈越高,这份工作是一个持续学习的过程,并且回报和提高是肉眼可见的!



回忆那个时候


其实说实话,这个疑惑我上大一就开始有,但是那个时候是从高考的失落中寻找升学的路径,开始无脑的刷那种考研短视频



(看过可能都知道真的一下子励志的心就有了,但是回到现实生活中,看到身边人的状态~~~没错人就是一个从众的种群,你可能会问你会不会因为大一没有那么努力学习而后悔,但是其实我不会,因为那一年我的经历也是我最开心大学生活,虽然也干了很多被室友做成梗的糗事,但是想一想那不就是青春嘛,要是从小就会很有尺度的为人处世,想一想活着也很累嘛,害,浅浅致敬一下充满快乐和遗憾的青春呀!)


个人看法


哈哈,跑题了。给大家点力量把!前面满满的焦虑。其实我感觉我们都应该感谢我们来到计算机类的专业,从事这方面的学习和研究。


因为计算机的扩展性,不得不说各行各业都开始越来越喜欢我们计算机毕业的大学生(就业方向更加广),我也因为自己会计算机,成功进入一个一本高校以上的教育类公司实习(同时也是这个时候知道了更多优秀学校的毕业年轻人,真正认识到学校的层次带给人的很多东西真正的有差距);



虽然我是二本的学生,但是在亲戚朋友眼里,虽然学校比不上他们的孩子,但是计算机专业也能获得浅浅的也是唯一一点可以骄傲的东西(活在别人嘴这种思考方式肯定不是对的,但是现实就是在父母那里,我们考上什么大学和进入了哪里工作真的是他们在外人的脸面,这种比较情况在大家族或者说农村尤为严重);



技术论打败学校论,计算机专业是在“广义”上为数不多能打破学校出身论的学科,在公司上只要你能干活,公司就愿意要你,这个时候肯定有人diss我,现在培训班出来的很多都找不到工作呀,我的回答只能是:的确,因为这个行业的红利期展示达到了瓶颈期,加上大环境的不理想,会受到一些影响,但是我还是相信会好的,一切都会好的。



做技术既然这样了


关于最近论坛上说“前段已死”“后端当牛做马”“公司磨刀霍霍向测试”......



这个东西怎么说,我想大部分人看到这个都会被这个方向劝退,我从两个角度分析一下,上面说了,真滴卷,简历真滴多,存在过饱和;第二点,希望这个领域新人就不要来了,就是直接劝退,被让人来卷,狭义上少卷一些......



现在就是导致我也不敢给朋友做建议了,因为当他看到这些的时候,和进入工作环境真的不好,我真的怕被喷死


包括现在我的实习,大家看我的朋友圈看出工作环境不错很好,但是和工作的另一面,是不能发的呀,有时候我都笑称自己是“产业工人”(这个词是一个朋友调侃我的)


不行了,在传播焦虑思想,我该被喷死了,现在我给建议都变得很含蓄,因为时代红利期真的看不透,我也不敢说能维持多少年,而且我工作也一般,我不敢耽误大家(哈哈哈,突然想起一句话,一生清贫怎敢入繁华,二袖清风怎敢误佳人,又是emo小文案,都给我开E)


个人总结


本文就是调侃一下现在的环境啊,下面才是重点,只有干活和真话放在后面(印证一个道理:看到最后的才是真朋友才敢给真建议,我也不怕被骂)



心态方面:我们这个年纪就是迷茫的年纪,迷茫是一种正常的状态,因为作为一名成年人你真正在思考你的个人发展的状态,所以请把心放大,放轻松,你迷茫了已经比身边的人强太多了,如果真正焦虑的不能去学习了,去找个朋友聊一聊,实在不行,drink个两三瓶,好好睡一觉,第二天继续干,这摸想,这些都算个啥,没事你还有我,实在不行微我聊一聊,我永远都在,我的朋友!



工作方面:俗话说:女怕入错行,男怕娶错人!(突然发现引用没什么用,哈哈)我们可以多去实践,没错就是去实习,比如你想做前端的工作,你就可以直接去所在的城市(推荐省会去找实习)但是朋友其实实习很难,作为过来人,我能理解你,一个人在陌生的城市而且薪资很可怜,面对大城市的租房和吃饭有很多大坑,你要一一面对,但是在外面我们真要学会保护自己,而且实习生活中经济方面肯定要父母支持,所以一定要和父母好好沟通,其实你会发现我们越长大,和父母相处的时光越短。(我今年小年和十五都没在家过,害,那种心理苦的滋味很不好受)



升学方面:不是每一个都适合考研,不要盲从考研。但是这句话又是矛盾的,在我的实习生涯中,学历问题是一个很重要的问题,我们的工作类型真的不同,还是那句话,学历只是一个门槛,只要你迈入以后看的是你的个人能力。说一句悄悄话,我每天工作,最想的事情就是上学,心想老子考上研,不在干这活了,比你们都强。所以你要想考研,请此刻拿出你的笔,在纸上写下你要考研主要三个理由,你会更好的认识自己,更好选择。



好吧,今天的随想录就这摸多,只是对最近看文章有了灵感写下自己的看法,仅供参考哦!


回答问题


回应个问题:很多朋友问我为什么给这摸无私的建议,这是你经历了很多才得到的,要是分享出去,不是很亏?


(你要这摸问,的确你有卷到我的可能性,快给我爬。哈哈哈)可能是博客圈给的思想把,其实我说不上开源的思想,但是我遇到的人对我都是无私分享自己的经验和自己走过的坑,就是你懂吗,他们对我帮助都很大,他们在我眼里就是伟大的人,所以我也想要跟随他们,做追光的人!(上价值了哦,哈哈)



写在最后


最后一句话,迷茫这个东西,走着走着就清晰了,迷茫的时候,搞一点学习总是没错的。


作者:武师叔
链接:https://juejin.cn/post/7201752978259378232
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

中年程序员写给36岁的自己

笔者是一名程序员老司机,局限于笔者文笔一般,想到哪写到哪__胡乱写一通,篇幅较长 _,希望通过文章的方式简单的回顾过去、总结现在和展望未来,顺便记录一下,方便以后总结。_ 回顾  忙忙碌碌又一年,看着自己的发量在逐渐的减少,深感焦虑,今天终于有时间可...
继续阅读 »

笔者是一名程序员老司机,局限于笔者文笔一般,想到哪写到哪__胡乱写一通,篇幅较长


_,希望通过文章的方式简单的回顾过去、总结现在和展望未来,顺便记录一下,方便以后总结。_


回顾 


忙忙碌碌又一年,看着自己的发量在逐渐的减少,深感焦虑,今天终于有时间可以回顾自己过去一年的得与失,去年是写给35岁的自己,今年该+1了,还是随笔的方式,想到哪写到哪。


2022年1月30日的时候 ,给自己过去的工作做一次简单的总结,主要还是写给自己,就像现在的时候可以回过头去看,也收获了许多朋友的关注,回去看一年前写的文章,以及大家的留言反馈,深有感触。


回看去年的flag,首先需要检讨一下,基本上都没有完成,但是自己也朝着这个目标在做,收获也是有的:



读书


每个月一本书,一年完成至少10本书的学习计划,学以致用,而不是读完就忘。


写文章


一周完成一篇原创文章,不限类别


早睡早起


每天不晚于11:30休息



关于读书


上半年的时候,自己也是有点焦虑和迷茫,想成长想进步,所以焦虑心情自然就会出现,所以看了一些鸡汤书籍,什么《被讨厌的勇气》、《程序员的自我修养》、《情商》等等。实话说看完之后,确实能够缓解缓解内心的焦虑情绪,但是这些书籍能给到自己的,更多是一些方式和方法,对于内心的空洞和不充实带来的焦虑是没办法缓解的。


所以还需要对症下药,我自己所感受到的空洞和不充实,很多是来自自己对技术知识技能的缺乏和退步,说白了就是作为技术人,不能把技术给弄丢了 ,同样也想不断的跟上时代的步伐。


想要快速解决这种“焦虑”,我需要快速的制定一个短期、中期、长期的目标,围绕着目标去充实自己的知识体系。这里所说的目标还是比较容易制定的,毕竟是关乎自己的成长,也就是自己接下来想要成为什么样的人,什么样的知识体系能够让自己在当前以及未来几年的工作中都有帮助。从这个方面去想,首先未来还是想从事前端,所以我给自己制定的短期目标是算法成长 、中期目标是计算机图形学方面的知识掌握、长期目标是成为一名地图领域的技术专家(ps:说到这里先立个flag,我后面想写一个小册,专门关于地图领域相关的,我也是比较期待自己能写出来什么样的小册,不为别的,就是想把自己的知识沉淀下来)。


讲讲为什么要这么去规划目标,算法算是现在任何技术面试都会涉及的,但是我不是为了面试去看,而是为了提升自己在团队内部的技术影响力,《算法图解》这本书写的简单好理解,作者的思路非常清晰 ,看完之后给团队内部的同学分享,不仅能提升自己,还能带动团队一起学习,一举多得。计算机图形学知识是目前工作中会碰到的,比如渲染、大数据可视化、自动驾驶等等都会涉及,这一部分不建议大家先去看书,没有一本书能够说明白,推荐大家去搜《闫令琪》,非常厉害的大佬,上班路上每天花半个小时-1小时足够了,一个月基本上能够学完,之后再运用到工作中,融会贯通。


单独再讲讲长远目标,我之前并不是搞地图方向的,但是近期这份工作有机会接触到了这方面的工作,让我又重新燃起了工作中的那种欲望,很久没有工作中的那种成就感,这也许是10年前才会有的那种热情,所以我比较坚信未来几年自己希望能够深入投入这个方向,不一定是地图,但一定是和这个方向相关的领域,因为知识都是想通的。


关于写文章


写文章这件事情,我非常佩服一位前同事,也或许是因为我没有做到 ,但是别人坚持每天每日的在做 ,他连续两年每天都能产出一篇原创,关于各个方面的,这是值得我学习的地方,今年争取突破自己。


关于早睡


头发卡卡掉,感觉都是因为没有按时睡觉引起的,还是在能有条件的时候,尽量早睡。


工作


今年的工作可以用“黑暗”、“光明”两个词来概括。


黑暗


2022年经历疫情最严重的一年,大部分时间都是居家办公状态,这也导致和同事们的交流变得很少,很多想要推进的工作变得没那么顺利,徒增了不少压力。


2022年也是“财源滚滚”的一年,看着同事一个个离开,也有不少同事询问工作机会,也确实给自己内心带来不小的冲击,同时危机感也很明显。


在一个地方工作一段时间之后,多少都会遇到各种各样的问题,技术上是最省心的问题,解决就好。有江湖的地方就会有各种复杂到不敢想的关系网,谁是谁的小弟,谁是谁的心腹、谁是大老板招来的等等,遇到这种问题我更多的是做好自己,但我更多还是更愿意沉浸在技术的知识中,享受解决问题带来的快感。面对频繁换老板,技术人的通病,不善于抱大腿,当然我也不想在这方便再去做过多改变或者违背内心去做一些事情,保持好内心的底线,不突破我的底线则相安无事。


光明


呵护好内心的明灯


今年工作最大的动力是来自于自身能力的成长,规划的短中长目标基本上都在按照正确的方向在行进,这也是在排除各种各样的干扰后带来的好的结果,也是抱着一种积极向上的心态在努力,工作中最让人糟心的,无非就是背锅、背指标、裁员,最坏的情况也就这样了,守好内心的方向,做自己想做的事情就对了,自己左右不了的事情不去想太多,行业不景气的时候,我基本上是以这种心态在工作,人生并不是只有工作。


人情往来


工作中


今年在和外部部门的合作当中,收获了许多的认可,也建立了许多新的人脉关系,这也是人生中比较宝贵的资源。与合作方合作共赢一直都是我做事的指导方法 ,提前思考好双方的目标和边界,剩下的就是努力合作完成目标了。相信他人,他人也会给予你同样的信任。


生活中


生活中的关系会比工作中的关系更加的牢靠,当然工作中的关系发展的好的话,也可以沉淀到生活中,而不是换个工作全没了,今年工作中积累的关系,确实是可以有这方面的转换的,这也是一种收获。


技术成长


我一直都不太赞成技术人转纯管理这个方向,管好人其实可以很简单,丑话在前,用心对待,以诚相待,能做好这三点感觉都不会有太大问题,但技术丢了就很难再捡起来了,切记切记。


今年反尝试不直接带团队,更多的是以技术顾问、专家视角,甚至是一线coding的方式在工作,看似管人但又不管人,所以在技术上成长也是非常快的,少了很多其他的琐事,能够更加投入。


渲染


第一次接触这个词的时候是在2021年,公司专门配了一个渲染团队做这个事情,用前端白话讲,就是能把各种各样的图像画到canvas上,一个好的渲染引擎可以呈现任何想要呈现的物体。


为了学习渲染是做什么的,怎么做,当时把简单的数学知识重新学习了一下,看闫令琪大佬的课,看openGL、webGPU等等相关的知识,过程是比较辛苦的,但收获也是很多的。现在再看一些框架就能够理解为什么代码会这么写了,比如Threejs、deckgl等等,我们自己也用c++实现了一套底层的跨端渲染框架,虽然不全面,但内部够用同时也能提升自身技术水平。


架构


架构能力是随着工作中不断积累起来能力,当然这也需要在工作中不断的打磨和锻炼,如果一直是以完成任务的心态在工作那是很难练出来的。我所推崇的架构能力是以解决业务问题为主,提升产研的效率为辅。所以在工作中不会刻意去做架构,而是围绕着如何解决问题去架构,如何才能控制好不至于过度设计。


举个简单例子,假如我们已经有各种完善的点餐业务,需要做一个邀请大家一起喝奶茶的这么一个功能,从业务上我们先考虑两个核心逻辑:

1、用户点餐之后回到邀请页面,点完的所以人实时能看到其他人下单状态
2、队长确认所有人点完之后,下单付款,所有人的页面切换到送餐状态

如果是快速实现这个功能的话,其实是比较简单的,起一个轮询任务实时问服务端要数据,拿到数据后,根据状态决定下一步显示什么状态的页面


但是随着业务发展,会加入很多奇怪的逻辑,比如要支持修改、删除、踢人等等,这就会导致这个页面逻辑及其的复杂起来,如果不去思考的话,很容易就写出一堆面条代码,最后自己都不愿意去改。


所以针对这个功能  ,我自己会抽象成几部分去思考:

1、store该如何拆解,拆成几个,每个store对应哪个组件
2、store该如何去更新
3、与服务端如何通信,websocket、轮询都可以,看当下实际情况,保证稳定性即可
4、可以写几个js类去做这个事情,每个类的职责是什么

我觉得思考完这几个问题 ,大家对于这个页面该怎么去写应该能有一个很清晰的架构图在脑海中了吧,这里我就不过多展开了 ,有兴趣的话私聊,核心是要说架构其实也可以很简单。


总结


今年就不立flag了,目标能把去年的flag实现好,2023年是疫情结束的一年 ,我认为这是一个好的开始,努力工作是一方面,享受生活我认为也同样重要,今年更需要做好工作和生活的平衡,工作以外能有一些其他的成就。


写给36岁的自己,简单地回顾过去、总结现在、展望未来,希望当37岁的自己回过头来看的时候,能够鄙视现在的自己,写出更好的《写给37岁的自己》。


作者:冲_破
链接:https://juejin.cn/post/7210233021964910653
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

程序员如何成长

做技术是打怪兽不是养宠物,为什么要打怪兽?因为难;为什么难很重要?因为难的事情才能带来成长;为什么要成长?承认吧,因为「如何成长」是当代人,包括你我他在内焦虑的源泉。 过去几个月内我在写一系列主题为「NodeJS实战」的文章,内容来源是过去两年独自开发和运维 ...
继续阅读 »

做技术是打怪兽不是养宠物,为什么要打怪兽?因为难;为什么难很重要?因为难的事情才能带来成长;为什么要成长?承认吧,因为「如何成长」是当代人,包括你我他在内焦虑的源泉。


过去几个月内我在写一系列主题为「NodeJS实战」的文章,内容来源是过去两年独自开发和运维 site2share 网站的经验,本篇文章是对这个系列的一个暂时收尾。


今天我不聊代码,聊些更重要的事情


养宠物


从两件事情开始说起


其一是在此之前,我直接或间接听到了一些来自早已离开项目同学的声音,陈旧的项目技术栈和代码是驱使他们离开的原因之一。


其二是在偶尔浏览掘金网站的内容过程中,有一类让我印象深刻的文章标题,大意诸如「教你用xx + xx + xx 打造一个开源系统」,因为我关注前端领域的关系,标题里的 xx 通常是围绕某个前端框架的时髦技术栈,从点赞和评论数来看它们颇受欢迎。


对于前者我当然理解:一方面脱离当下容易让自己丧失竞争力,不管你愿不愿意承认,简历驱动型开发是所有程序员秘而不宣的默契;另一方面陈旧代码给开发工作带来的挫败感不言而喻,我相信每个程序员面对「屎山代码」都有宁愿把它重写也不愿意修改一行的冲动。第二件事的出现也就顺理成章了:我想毫无负担地学习新技术,还能抛开白天工作中的螺丝钉角色,体验一次愉快的项目实战经历。


我把从零开始做新项目比喻为「养宠物」,因为它能给你带来无与伦比的掌控感。假设一个代码库完全是由你一手搭建的话,那么关于它的一切,例如如何启动、如何部署、它适用于什么场景又无法解决什么样的问题你都心中有数。如果你恰巧又在Thoughtworks 工作,那么Thoughtworks 工作体验更增强了这种掌控感的正当性:对于有坏味道的代码我们允许用工作时间进行重构,对于代码内不懂的知识点,只要提出问题就一定可以得到回答。


也许是我运气不够好,我的工作经验告诉我,「养宠物」般的工作机会是可遇而不可求,在大厂晋升靠造轮子而不是填坑是公理人尽皆知,但造轮子的机会屈指可数。维护遗留系统依然是我们大部分人的工作。这也就是我接下来想说的「打怪兽」,此时我们面对的系统哪怕只上线一年,源代码也可能是满目疮痍。


这里是对于下面不中听的一些话的免责声明,我不是在否定精通


React 没有价值,我也不认为简历驱动开发有什么错,只不过要小心它们让我们的眼界变得狭隘


打怪兽


真正的常态是我接下来想说的「打怪兽」。


之所以把它称为「打怪兽」,不仅仅因为你接触的代码会超出你的预期,你甚至想象不到你会遇到什么样(啼笑皆非亦或是让你无从下手)的困难:


- 这个一千行代码的文件应该从哪开始读起?
- 我如何才能让代码进入这个分支?
- 你发现项目用到的一个框架没有任何文档,在 github 上也找不到源码,原来是上一个离职的老大自己写的
- 项目的打包工具用的既不是webpack 也不是 grunt ,而是 shell 脚本
- 现在需要你优化一个超过包含上百个组件的 React 应用的性能


「怪兽」依然是一个友好的比喻,此时此刻你至少还能够把它具象化,将它和某些电影或者游戏里的角色联系在一起,这意味着它造成破坏的手段和范围是可以预知的。但工作中我们实际遇到的问题无法预测。


你一定想象不到在编写 site2share的过程中,困扰我最久的问题背后的罪魁祸首竟然是 ExpressJS 里的 trust proxy 参数,它导致 API 从来无法访问到部署在 Azure App Service 上的后端服务


为什么要打怪兽


实际的出发点正如上面所说,如果我们工作中绝大部分人、绝大部分时间面临的都是怪兽,那么逃避它就是自欺欺人。


说点不实际的,是因为打怪兽比养宠物更难——为什么「难」重要?因为难的事情才能带来成长。为什么要成长?承认吧,因为「如何成长」是当代人,包括你我他在内焦虑的源泉。


除此之外,我还想强调的是它在锻炼解决问题能力本身


随着工作的深入,越来越发现我的角色从「解决技术问题的人」变成「解决问题的人」:从 Javascript、SQL Server 到代码设计、代码规范,再到团队方向、团队培养。整个过程其实不允许你循序渐进地去适应,可能明天醒来新的问题就摆在你面前,你也永远也没有准备好的那一天。也许可以把团队管理当作一门新技术用学习编程语言的方式去学习,也许求助对的人是当务之急,也许有的问题压根可以不解决。但无论如何,思路不会有如神助般凭空出现在你脑海里,举一反三需要的是练习。问题的多样性在练习的过程中起到非常大的作用,解决新问题会带给你明确的反馈:我的经验可以移植到这个领域,亦或者我的工作模式需要调整。


或者忘掉我上面的长篇大论,通俗点说,打完怪兽以后你就是见过「地狱」的人了,还怕什么。我想起来大二时候为了制作这款软件代码被推翻了无数次,从那之后就再也不怕重构了


app.png


另一方面,养宠物的风险在于,它让我们不自觉地陷入舒适区中。


我曾经有差不多有一年的时间可以自由选择技术栈来开发各式各样前端应用。最流行的框架和搭配起来最时髦的全家桶便成了我的不二之选。在热门冷门尝试了个遍之后最终我难免会对自己产生怀疑:**我似乎永远都在被输入,我永远都在给某个工具打工,如果今天哪个框架告诉我它是业内明日之星那我就要去学它,因为 fear of missing out 是每个技术人的通病。**我似乎能做的也只有如此了,但这就真的足够了吗?


工具正在变得自动化,并且「帮助」我们专注于业务开发这件事带有迷惑性。这里的陷阱在于他能替你做很多事,会让你以为你具备同样的能力的错觉。例如虽然 Parcel 可以无须任何一行配置就把脚本打包得漂漂亮亮的,但你可能对背后的缓存策略一无所知。当每个人都在简历上强调「精通 xx 框架」的今天,我们应该问自己除了框架我还有没有更有力的竞争力?


这类陷阱还有另一种变形是,在团队内你只做业务开发。身处大型开发组中会让你以为你有独立驾驭一个相同体量项目的能力,但实际遇到的问题会非常受限,因为功能性需求和底层设计已经交给你们团队的 Tech Lead 甚至是团队前成员去做了。(公允地说这不是完全负面,而是一件需要把握平衡的事情。虽然这会给团队成员的成长带来不利,但另一方面却可以让项目风险变得可控)


「打怪兽」也是在打破你的乌托邦


打怪兽的另一层含义是经历实战


「教你用 xx 打造 xx」这类系列教程的前置条件太美好了:你有无限的业余时间投入其中,你就是你自己的产品经理。但实际工作中我们永远是戴着镣铐跳舞。例如糟糕代码不一定是个人能力的结果,考虑到当时的交付压力,团队状态和历史包袱,换做你不一定能做得更好。所以大部分技术决策其实是在恶劣环境下做出的,然而如何学习在不同环境中作出恰当的反应,我不认为这是脱离实践可以达成的。


另一个问题是它缺少对方案的闭环验证:我不确定有多少此类项目投入到真实的商业运营中,如果没有,很遗憾它的代码就不一定是有效的。例如它设计有异常捕获功能,异常捕获的目的之一是帮助我们在实际运营过程中排查问题,那当异常发生时它可以提供什么样的信息帮助我们定位到错误代码?通常在捕获异常之后紧接着要把信息作为日志输出,有相当一部分公司其实购买的第三方日志系统,那集成难度如何?如果只有零星的用户上报了此类问题,我们可否在实际生产环境下,在每秒上千条日志增速的日志海洋里甄别到他们?


退一步说,即使方案完美无缺,我们还需要关注它的成本如何。再一次强调,实际工作中人力、时间都是有限的,假使我们能做到满分条件也不会允许。当你把方案拍到老板面前,但是他告诉你预算只有三成时,选择留下哪三分之一的功能,或者说如何用三成的预算做出来一个及格的功能比纯粹的编码更棘手。老板更多关心的是风险,说实话「时髦」技术表达的并不一定都是褒义,它意味着技术的关注度仍在持续提升中,意味着它还可能没有被大规模地应用,也意味着我们其实有更成熟的方案可供选择。决策者都厌恶风险,因此在推广新方案时风险可控也是因素之一。除此之外代码的学习曲线如何?代码库毕竟在依赖团队维护,你应当考虑到团队下限对于新技术的接受程度。



作者:李熠
链接:https://juejin.cn/post/7241884241616732218
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

安卓埋点策略+Retrofit上传埋点数据

安卓埋点 在企业级安卓项目中,埋点是一项重要的技术,用于收集用户行为数据以进行分析和改进产品。以下是一个常见的安卓企业级项目开发中使用的埋点方案: 定义埋点事件:首先,确定需要埋点的关键事件,如页面访问、按钮点击、数据提交等。为每个事件定义唯一的标识符或名...
继续阅读 »

安卓埋点


在企业级安卓项目中,埋点是一项重要的技术,用于收集用户行为数据以进行分析和改进产品。以下是一个常见的安卓企业级项目开发中使用的埋点方案:




  1. 定义埋点事件:首先,确定需要埋点的关键事件,如页面访问、按钮点击、数据提交等。为每个事件定义唯一的标识符或名称。




  2. 埋点代码插入:在关键事件的代码位置插入埋点代码,以便在事件发生时触发埋点记录。可以通过在代码中手动插入埋点代码或使用 AOP(面向切面编程)等技术自动插入埋点代码。




  3. 数据收集和存储:在埋点代码中,收集相关的事件数据,如事件类型、时间戳、页面名称、按钮名称等。将这些数据存储到本地数据库或发送到服务器进行存储。




  4. 数据上传和分析:定期将本地存储的埋点数据上传到服务器端进行分析。可以使用网络请求库发送数据到服务器,并在服务器端使用数据分析工具进行处理和分析。




  5. 数据展示和可视化:通过数据分析工具,将埋点数据进行可视化展示,生成报表、图表等形式的数据分析结果,以便开发团队或业务团队进行数据分析和决策。




  6. 隐私和合规性:在进行埋点时,要确保遵守隐私保护和数据合规性的相关法规和政策。确保用户数据的安全和保密,并进行必要的用户授权和通知。




  7. 埋点策略优化:根据实际业务需求和数据分析结果,优化埋点策略,增加或调整关键事件的埋点,提高数据的准确性和有用性。




需要注意的是,具体的埋点方案可能因项目需求、技术架构和团队实际情况而有所不同。因此,在实施埋点方案时,应根据项目的具体情况进行定制化开发,并考虑到性能、稳定性、安全性和用户体验等因素。


埋点数据和上传埋点数据代码示例


定义埋点事件的工具类,包含事件的标识符、名称、属性等信息

public class TrackEventUtils {
public static final String EVENT_PAGE_VIEW = "page_view";
public static final String EVENT_BUTTON_CLICK = "button_click";
// 其他事件定义...

// 获取页面访问事件
public static TrackEvent getPageViewEvent(String pageName) {
TrackEvent event = new TrackEvent(EVENT_PAGE_VIEW);
event.addProperty("page_name", pageName);
// 其他属性...
return event;
}

// 获取按钮点击事件
public static TrackEvent getButtonClickEvent(String buttonName) {
TrackEvent event = new TrackEvent(EVENT_BUTTON_CLICK);
event.addProperty("button_name", buttonName);
// 其他属性...
return event;
}

// 其他事件获取方法...
}


定义埋点事件的实体类,包含事件类型、属性等信息

public class TrackEvent {
private String eventType;
private Map<String, Object> properties;

public TrackEvent(String eventType) {
this.eventType = eventType;
this.properties = new HashMap<>();
}

public String getEventType() {
return eventType;
}

public void addProperty(String key, Object value) {
properties.put(key, value);
}

public Map<String, Object> getProperties() {
return properties;
}
}


使用Retrofit框架上传埋点数据到对应路径


1.添加 Retrofit 依赖到项目的 build.gradle 文件中:

implementation 'com.squareup.retrofit2:retrofit:2.x.x'
implementation 'com.squareup.retrofit2:converter-gson:2.x.x' // 如果要使用 Gson 解析器


2.创建 Retrofit 实例并定义 API 接口:

public interface TrackApiService {
@POST("/track")
Call<Void> sendTrackEvent(@Body TrackEvent event);
}


3.修改 TrackManager 类,使用 Retrofit 发送网络请求:

public class TrackManager {
private static final String API_ENDPOINT = "https://your-api-endpoint.com";
private static TrackManager instance;
private Context context;
private TrackApiService apiService;

private TrackManager(Context context) {
this.context = context.getApplicationContext();

// 创建 Retrofit 实例
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(API_ENDPOINT)
.addConverterFactory(GsonConverterFactory.create()) // 使用 Gson 解析器
.build();

// 创建 API Service
apiService = retrofit.create(TrackApiService.class);
}

public static synchronized TrackManager getInstance(Context context) {
if (instance == null) {
instance = new TrackManager(context);
}
return instance;
}

public void trackEvent(TrackEvent event) {
// 发送网络请求
Call<Void> call = apiService.sendTrackEvent(event);
call.enqueue(new Callback<Void>() {
@Override
public void onResponse(Call<Void> call, Response<Void> response) {
// 处理服务器响应...
}

@Override
public void onFailure(Call<Void> call, Throwable t) {
// 处理请求失败...
}
});
}
}


4.解释下上面的代码中有关Retrofit中的注解和上面我们定义的接口TrackApiService
在 Retrofit 中,TrackApiService 是一个接口,用于定义网络请求的方法。@POST("/track") 是一个注解,表示发送 POST 请求到指定的路径 "/track"。


@Body TrackEvent event 是另一个注解,用于指定请求体的内容。它告诉 Retrofit 将 TrackEvent 对象作为请求体发送给服务器。


具体解释如下:




  • @POST("/track"):表示将使用 POST 方法发送请求到路径 "/track"。这个路径是你的 API 后端定义的接收埋点事件的路径。




  • Call<Void>:表示 Retrofit 将返回一个 Call 对象,用于异步执行网络请求并处理响应。Void 表示响应的主体内容为空。




  • sendTrackEvent(@Body TrackEvent event):这是一个方法定义,用于发送埋点事件。@Body 注解表示将 TrackEvent 对象作为请求体发送。TrackEvent 是你定义的类,包含了发送给服务器的埋点事件数据。




综合起来,TrackApiService 接口中的 sendTrackEvent 方法定义了一个发送埋点事件的请求,通过 POST 方法发送到指定路径,并将 TrackEvent 对象作为请求体发送给服务器。


你可以根据实际需求修改这个接口,添加其他请求方法和参数,以适应你的埋点需求。




在 Retrofit 中,@Body 注解用于将对象作为请求体发送给服务器。这意味着你可以将任何 Java 类的实例作为请求体发送出去,不限于特定的类或数据类型。


当你使用 @Body 注解时,Retrofit 将会自动将指定的对象序列化为请求体的格式,例如 JSON 或者其他格式。然后,它将使用适当的请求头信息将请求发送到服务器。


因此,你可以创建自己的 Java 类,用于表示需要发送的数据,并将其作为请求体发送给服务器。这样,你可以根据实际需求定义和发送不同类型的数据。


请确保在使用 @Body 注解时,服务器能够正确地解析和处理请求体的格式。通常,你需要在服务器端进行相应的处理和解析,以确保能够正确地接收和处理你发送的 Java 对象。


注:,Retrofit 会动态地创建接口的实现类,你无需手动编写实现类。当你使用 Retrofit 创建接口的实例时,它会在运行时生成一个代理类来处理实际的网络请求。因此,你不需要手动实现 TrackApiService 接口中的方法。


使用异步或者同步请求


使用 enqueue 方法是一种常见的异步执行网络请求的方式,它会在后台线程执行网络请求,并在请求完成后回调相应的方法。


Retrofit 支持同步和异步的网络请求方式。如果你希望使用同步请求,可以使用 execute 方法来执行请求,但需要注意的是,在 Android 主线程上执行网络请求会导致阻塞,可能会引起 ANR(Application Not Responding)错误,因此建议在后台线程中执行同步请求。


关于接口是异步还是同步的,一般情况下是由接口的定义和服务端的实现决定的。通常,网络请求都会以异步方式执行,以避免阻塞主线程。在 Retrofit 中,默认情况下,接口的方法会被当作异步请求进行处理,需要使用 enqueue 方法来执行异步请求。


如果你想要执行同步请求,可以在 Retrofit 创建时设置合适的执行器(Executor),以控制请求的执行方式。例如,可以使用 OkHttp 客户端来创建 Retrofit 实例,并设置自定义的执行器来执行同步请求。

// 创建 OkHttpClient 实例
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.build();

// 创建 Retrofit 实例,并指定 OkHttp 客户端
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com")
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient) // 设置自定义的 OkHttpClient
.build();

// 创建 TrackApiService 实例
TrackApiService trackApiService = retrofit.create(TrackApiService.class);

// 创建同步请求执行器
ExecutorService executor = Executors.newSingleThreadExecutor();

// 执行同步请求
try {
Response<Void> response = executor.submit(() -> trackApiService.sendTrackEvent(event)).get();
// 请求成功的处理逻辑
} catch (ExecutionException | InterruptedException e) {
// 请求失败的处理逻辑
}

// 关闭执行器
executor.shutdown();


在这个示例中,我们使用 OkHttp 客户端创建了一个自定义的 OkHttpClient 实例,并将其传递给 Retrofit 的构建器。然后,我们创建了一个 ExecutorService 实例,并使用 submit 方法执行网络请求。通过调用 get 方法获取 Response 对象,我们可以同步地获取请求的结果。


需要注意的是,同步请求仍然需要在合适的线程中执行,以避免阻塞主线程。在这个示例中,我们使用了单线程的执行器来执行同步请求,并在请求完成后关闭执行器。


综上所述,Retrofit 提供了异步和同步两种方式来执行网络请求,具体使用哪种方式取决于你的需求和服务器端的实现。一般来说,推荐使用异步请求以避免阻塞主线程,除非你确切地知道需要执行同步请求,并且在合适的线程上执行它们。
如果使用的是同步请求,即使使用了execute方法,也要手动开启子线程来调用execute方法,若是异步请求,则使用Retrofit的enqueue方法即可,无需自己手动开启子线程。


服务器如何决定接口是异步请求或是同步请求


1.在服务端,决定接口是同步请求还是异步请求是由服务端的实现逻辑来决定的。
通常情况下,服务端会为每个接口定义好其执行方式,包括是同步还是异步。这通常是通过服务端框架或编程语言提供的特定机制来实现的。
例如,在某些服务器框架中,可以使用异步处理机制(如基于回调的异步编程、Future/Promise、协程等)来处理异步请求。而对于同步请求,则可能直接在请求处理方法中执行阻塞操作。
因此,具体接口是同步还是异步请求,你需要参考服务端接口文档或与服务端开发人员进行沟通,了解其设计和实现细节。根据服务端的要求,你可以相应地选择使用 Retrofit 的 enqueue 方法或 execute 方法来发送请求。


2.在服务端的代码中,决定接口是同步还是异步的方式取决于所使用的服务器框架和编程语言。以下是一些常见的示例代码,展示了如何在不同的环境中定义同步和异步接口:


a.Node.js(使用 Express 框架):

// 异步接口
app.get('/async', (req, res) => {
someAsyncOperation((data) => {
res.send(data);
});
});

// 同步接口
app.get('/sync', (req, res) => {
const result = someSyncOperation();
res.send(result);
});

b.Java(使用 Spring 框架):

// 异步接口
@GetMapping("/async")
public CompletableFuture<String> asyncEndpoint() {
return CompletableFuture.supplyAsync(() -> {
// 异步操作
return "Async response";
});
}

// 同步接口
@GetMapping("/sync")
public String syncEndpoint() {
// 同步操作
return "Sync response";
}

这些示例只是简单的展示了如何在不同环境中定义同步和异步接口。实际上,具体的实现方式取决于所使用的服务器框架和编程语言的特性和机制。因此,你需要根据你所使用的具体服务器框架和编程语言的文档,了解如何定义和处理同步和异步接口。


作者:达菲姐姐好好看
链接:https://juejin.cn/post/7237308635926511671
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android自定义一个车牌字母选择键盘

今天的内容大致如下: 1、最终实现效果及简单分析 2、设置属性,制定可扩展效果 3、部分源码剖析 4、开源地址及使用总结 一、最终实现效果及简单分析 以上就是本篇文章最终要实现的效果,和省份简称键盘不同的是,数据源上和边距有所差别之外,其他的实现方式均是一样...
继续阅读 »

今天的内容大致如下:


1、最终实现效果及简单分析


2、设置属性,制定可扩展效果


3、部分源码剖析


4、开源地址及使用总结


一、最终实现效果及简单分析



以上就是本篇文章最终要实现的效果,和省份简称键盘不同的是,数据源上和边距有所差别之外,其他的实现方式均是一样的,采用外部垂直LinearLayout,内部多个横向的LinearLayout的搭配方式。


需要注意的是,英文和数字键盘,默认状态下,顶部的数字是禁止的,也就是输入完地区代码之后,数字的禁止状态才会释放;由于距离左右的边距不同,其在数据源的判断上也会有不同,这个也是需要注意的。


二、设置属性,制定可扩展效果


其相关属性和上篇的省份键盘基本上没有太大的出入,主要就是动态化设置,设置一些,文字的背景,大小,颜色以及格子之间的编辑等,大概罗列了以下属性:

属性类型概述
ek_backgroundcolor整体的背景颜色
ek_rect_spacingdimension格子的边距
ek_rect_heightdimension格子的高度
ek_rect_margin_topdimension格子的距离上边
ek_margin_left_rightdimension左右距离
ek_margin_topdimension上边距离
ek_margin_bottomdimension下边距离
ek_rect_backgroundreference格子的背景
ek_rect_select_backgroundreference格子选择后的背景
ek_rect_text_sizedimension格子的文字大小
ek_rect_text_colorcolor格子的文字颜色
ek_rect_select_text_colorcolor格子的文字选中颜色
ek_is_show_completeboolean是否显示完成按钮
ek_complete_text_sizedimension完成按钮文字大小
ek_complete_text_colorcolor完成按钮文字颜色
ek_complete_textstring完成按钮文字内容
ek_complete_margin_topdimension完成按钮距离上边
ek_complete_margin_bottomdimension完成按钮距离下边
ek_complete_margin_rightdimension完成按钮距离右边
ek_other_lines_margindimension其他行边距
ek_is_num_prohibitboolean数字是否禁止
ek_text_prohibit_colorcolor数字禁止颜色
ek_text_click_effectboolean是否触发点击效果,true点击后背景消失,false不消失



设置回调函数



























方法概述
keyboardContent获取点击的省份简称简称信息
keyboardDelete删除省份简称简称信息
keyboardComplete键盘点击完成
openProhibit打开禁止(使领学港澳),使其可以点击

三、部分源码剖析


这里只贴出部分的关键性代码,整体的代码,大家滑到底部查看源码地址即可。


定义字母和数字数组

   private val mEnglishList = arrayListOf(
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
"Q", "W", "E", "R", "T", "Y", "U", "O", "P",
"A", "S", "D", "F", "G", "H", "J", "K", "L",
"Z", "X", "C", "V", "B", "N", "M"
)

定义遍历数字和字母


由于在数据源上使用的是同一个,那么需要做截取分别进行遍历,便于控制左右的边距和本身的格子大小。

 //遍历数字
eachData(mEnglishList.subList(0, 10), mLength, true)
//遍历字母
eachData(mEnglishList.subList(10, mEnglishList.size), mLength - 1, false)
//追加最后一个删除按钮View,动态计算宽度
addEndView(mLineLayout)

遍历数据


遍历数据的逻辑和上篇保持一致,当和定义的长度取模为0时,就需要换行,换行就是重新创建一个水平的LinearLayout,添加至垂直的LinearLayout之中,需要做判断的是,左右的边距。

/**
* AUTHOR:AbnerMing
* INTRODUCE:遍历数据
*/
private fun eachData(
list: List,
len: Int,
isNumber: Boolean = false
) {
list.forEachIndexed { index, s ->
if (index % len == 0) {
//重新创建,并添加View
mLineLayout = createLinearLayout()
mLineLayout?.weightSum = len.toFloat()
addView(mLineLayout)
val params = mLineLayout?.layoutParams as LayoutParams
params.apply {
topMargin = mRectMarginTop.toInt()
height = mRectHeight.toInt()
if (isNumber) {
//是数字
leftMargin = mMarginLeftRight.toInt()
rightMargin = mMarginLeftRight.toInt() - mSpacing.toInt()
} else {
//是字母
leftMargin = mOtherLinesMargin.toInt()
rightMargin = mOtherLinesMargin.toInt() - mSpacing.toInt()
}
mLineLayout?.layoutParams = this
}
}

//创建文字视图
val textView = TextView(context).apply {
text = s
//设置文字的属性
textSize = px2sp(mRectTextSize)
//禁止
if (isNumber) {
//是数字
if (mNumProhibit) {
setTextColor(mRectTextColor)
} else {
setTextColor(mNumProhibitColor)
}
} else {
setTextColor(mRectTextColor)
}
setBackgroundResource(mRectBackGround)
gravity = Gravity.CENTER
setOnClickListener {
//每个格子的点击事件
if (isNumber && !mNumProhibit) {
//如果是数字,根据规则暂时不触发点击
return@setOnClickListener
}
changeTextViewState(this)
}
}
//是数字
if (isNumber) {
mTempTextViewList.add(textView)
}
addRectView(textView, mLineLayout, 1f)
}
}

添加视图


设置每个格子的宽高和权重。

 /**
* AUTHOR:AbnerMing
* INTRODUCE:追加视图
*/
private fun addRectView(view: View, layout: LinearLayout?, w: Float) {
layout?.addView(view)
val textParams = view.layoutParams as LayoutParams
textParams.apply {
weight = w
width = 0
height = LayoutParams.MATCH_PARENT
//每行的最后一个
rightMargin = mSpacing.toInt()
view.layoutParams = this
}

}

至于最后一个删除按钮,也需要动态的计算其本身的宽高,基本上和上篇一致,就不过多赘述了。


四、开源地址及使用总结


开源地址:github.com/AbnerMing88…


关于如何使用,有两种方式,一种是下载源码,直接把源码复制出来,二是可以使用以下的远程Maven依赖方式。


Maven具体调用


1、在你的根项目下的build.gradle文件下,引入maven。

allprojects {
repositories {
maven { url "https://gitee.com/AbnerAndroid/almighty/raw/master" }
}
}

2、在你需要使用的Module中build.gradle文件下,引入依赖。

dependencies {
implementation 'com.vip:board:1.0.0'
}


代码使用


android:layout_width="match_parent"
android:layout_height="wrap_content" />

总结


属性配置了有很多,可以实现多种自定义的相关效果,大家可以查找第二项中的属性介绍,进行自定义配置,还是那句话,本身的实现方式有很多种,本篇只是其中的一个简单的案例,仅供大家作为一个参考。


自定义英文和数字键盘,大家有没有发现了少了一个字母,为什么会没有这个字母呢?你知道原因吗?


作者:二流小码农
链接:https://juejin.cn/post/7241114001323638839
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

安卓滴滴路由框架DRouter原理浅析

前言 最近的一个新项目使用了Clean Architecture+模块化+MVVM架构,将首页每个tab对应的功能都放到单独的模块且不相互依赖,这时就有了模块间页面跳转的问题,经过一番研究选择了滴滴的DRouter,因为其出色的性能、灵活的组件拆分,更重要的是...
继续阅读 »

前言


最近的一个新项目使用了Clean Architecture+模块化+MVVM架构,将首页每个tab对应的功能都放到单独的模块且不相互依赖,这时就有了模块间页面跳转的问题,经过一番研究选择了滴滴的DRouter,因为其出色的性能、灵活的组件拆分,更重要的是生成路由表时支持插件增量编译、多线程扫描,运行时异步加载路由表,支持回调式ActivityResult,比ARouter好太多。本着用一个新框架,只会用还不够的原则,我决定去了解一下框架的原理,并给自己制定了以下几个问题:


1、框架的设计分层是什么样的?

2、它是如何生成路由表的?

3、它是如何加载路由表的?

4、相比于ARouter如何提高了性能?


阅读官方文档


相比于直接一头扎进源码,先阅读官方的文档总是没错的,官方给了一篇介绍的文章,写得非常好,基本回答了我以上的所有问题。


滴滴开源DRouter:一款高效的Android路由框架


首先在介绍DRouter的亮点部分得到了问题2、3、4的答案。



路由表在编译期通过插件动态生成。插件会启动多线程同时异步处理所有的组件;增量扫描功能可以帮助开发者在第二次编译时,只对修改过的代码进行处理,极大地缩短路由表生成的时间。



在编译器使用gradle插件配合transform扫描所有的类,生成路由表,并且支持增量扫描,回答了问题2。



另外框架初始化的时候启动子线程去加载路由表,不阻塞主线程的执行,尽其所能提高效率。



回答了问题3。



加载路由表、实例化路由、以及跨进程命令到达服务端后的分发这些常规应该使用反射的场景,使用预占位或动态生成代码来替换成java的new创建和显式方式执行,最大限度的去避免反射执行,提高性能。



回答了问题4,通过减少使用反射提升了性能。


在原理和架构章节处给了一张架构的设计图:


架构设计



整体架构分三层,自下而上是数据流层、组件层、开放接口层。


数据流层是DRouter最重要的核心模块,这里承载着插件生成的路由表、路由元素、动态注册、以及跨进程功能相关的序列化数据流。所有的路由流转都会从这里取得对应的数据,进而流向正确的目标。



RouterPlugin和MetaLoader负责生成路由表,路由元素指的是RouterMeta,存放scheme/host/path等信息。



组件层,核心的路由分发、拦截器、生命周期、异步暂存和监控、ServiceLoader、多维过滤、Fragment路由,以及跨进程命令打包等。



开放接口层则是使用时接触到的一些类,API设计得也很简单易用,DRouter类和Request类分别只有75和121行代码。


问题1得到解答,到此处也对整个框架有了一个整体的认识。


阅读源码


1.初始化流程


调用DRouter.init(app)后的时序图如下:


截屏2023-06-02 16.54.46.png


默认是在子线程实现路由表加载,不影响主线程。

    public static void checkAndLoad(final String app, boolean async) {
if (!loadRecord.contains(app)) {
// 双重校验锁
synchronized (RouterStore.class) {
if (!loadRecord.contains(app)) {
loadRecord.add(app);
if (!async) {
Log.d(RouterLogger.CORE_TAG, "DRouter start load router table sync");
load(app);
} else {
new Thread("drouter-table-thread") {
@Override
public void run() {
Log.d(RouterLogger.CORE_TAG, "DRouter start load router table in drouter-table-thread");
load(app);
}
}.start();
}
}
}
}
}

最终走到了RouterLoader的load方法来加载路由表到一个map中,仔细看它的引入路径是com.didi.drouter.loader.host.RouterLoader,是不存在于源码中的,因为它是编译的时候生成的,位置位于app/build/intermediates/transforms/DRouter/dev/debug/../com/didi/drouter/loader/host/RouterLoader。

public class RouterLoader extends MetaLoader {
@Override
public void load(Map var1) {
var1.put("@@$$/browse/BrowseActivity", RouterMeta.build(RouterMeta.ACTIVITY).assembleRouter("", "", "/browse/BrowseActivity", "com.example.demo.browse.BrowseActivity", (IRouterProxy)null, (Class[])null, (String[])null, 0, 0, false));
}

public RouterLoader() {
}
}

public abstract class MetaLoader {

public abstract void load(Map<?, ?> data);

// for regex router
protected void put(String uri, RouterMeta meta, Map<String, Map<String, RouterMeta>> data) {
Map<String, RouterMeta> map = data.get(RouterStore.REGEX_ROUTER);
if (map == null) {
map = new ConcurrentHashMap<>();
data.put(RouterStore.REGEX_ROUTER, map);
}
map.put(uri, meta);
}

// for service
protected void put(Class<?> clz, RouterMeta meta, Map<Class<?>, Set<RouterMeta>> data) {
Set<RouterMeta> set = data.get(clz);
if (set == null) {
set = Collections.newSetFromMap(new ConcurrentHashMap<RouterMeta, Boolean>());
data.put(clz, set);
}
set.add(meta);
}
}

不难猜出其是在编译期加了一个transform,生成RouterLoader类时加入了load方法的具体实现,具体来说是javaassit API+Gradle Transform,所以去看看drouter-plugin在编译期做了什么。


2.编译期transform


直接看时序图。


截屏2023-06-02 17.56.36.png


创建了一个RouterPlugin,并且注册了一个Gradle Transform。

class RouterPlugin implements Plugin<Project> {

@Override
void apply(Project project) {
...
project.android.registerTransform(new TransformProxy(project))
}
}

class TransformProxy extends Transform {
@Override
void transform(TransformInvocation invocation) throws TransformException, InterruptedException, IOException {
String pluginVersion = ProxyUtil.getPluginVersion(invocation)
if (pluginVersion != null) {
...

if (pluginJar.exists()) {
URLClassLoader newLoader = new URLClassLoader([pluginJar.toURI().toURL()] as URL[], getClass().classLoader)
Class<?> transformClass = newLoader.loadClass("com.didi.drouter.plugin.RouterTransform")
ClassLoader threadLoader = Thread.currentThread().getContextClassLoader()
// 1.设置URLClassLoader
Thread.currentThread().setContextClassLoader(newLoader)
Constructor constructor = transformClass.getConstructor(Project.class)
// 2.反射创建一个RouterTransform
Transform transform = (Transform) constructor.newInstance(project)
transform.transform(invocation)
Thread.currentThread().setContextClassLoader(threadLoader)
return
} else {
ProxyUtil.Logger.e("Error: there is no drouter-plugin jar")
}
}
}
}

注释2处反射创建一个com.didi.drouter.plugin.RouterTransform对象,并执行其transform方法,此处真正处理transform逻辑,它的位置位于drouter-plugin模块。

class RouterTransform extends Transform {
@Override
void transform(TransformInvocation invocation) throws TransformException, InterruptedException, IOException {
...
// 1.创建一个DRouterTable目录
File dest = invocation.outputProvider.getContentLocation("DRouterTable", TransformManager.CONTENT_CLASS,
ImmutableSet.of(QualifiedContent.Scope.PROJECT), Format.DIRECTORY)
// 2.执行RouterTask
(new RouterTask(project, compilePath, cachePathSet, useCache, dest, tmpDir, setting, isWindow)).run()
FileUtils.writeLines(cacheFile, cachePathSet)
Logger.v("Link: https://github.com/didi/DRouter")
Logger.v("DRouterTask done, time used: " + (System.currentTimeMillis() - timeStart) / 1000f + "s")
}
}

注释2处new了一个RouterTask对象,并执行其run方法,之后的log输出就是平时编译能看到的信息,表示transform的耗时。

public class RouterTask {
void run() {
StoreUtil.clear();
JarUtils.printVersion(project, compileClassPath);
pool = new ClassPool();
// 1.创建ClassClassify
classClassify = new ClassClassify(pool, setting);
startExecute();
}

private void startExecute() {
try {
...
// 2.执行ClassClassify的generatorRouter
classClassify.generatorRouter(routerDir);
Logger.d("generator router table used: " + (System.currentTimeMillis() - timeStart) + "ms");
Logger.v("scan class size: " + count.get() + " | router class size: " + cachePathSet.size());
} catch (Exception e) {
JarUtils.check(e);
throw new GradleException("Could not generate d_router table\n" + e.getMessage(), e);
} finally {
executor.shutdown();
FileUtils.deleteQuietly(wTmpDir);
}
}
}

重点在于ClassClassify这个类,其generatorRouter方法便是最终处理生成路由表的逻辑。

public class ClassClassify {
private List<AbsRouterCollect> classifies = new ArrayList<>();

public ClassClassify(ClassPool pool, RouterSetting.Parse setting) {
classifies.add(new RouterCollect(pool, setting));
classifies.add(new ServiceCollect(pool, setting));
classifies.add(new InterceptorCollect(pool, setting));
}

public void generatorRouter(File routerDir) throws Exception {
for (int i = 0; i < classifies.size(); i++) {
AbsRouterCollect cf = classifies.get(i);
cf.generate(routerDir);
}
}
}

构造函数处添加了RouterCollect/ServiceCollect/InterceptorCollect,最终执行的是他们的generate方法,分别处理路由表、service、拦截器,我们只看路由表的。

class RouterCollect extends AbsRouterCollect {
@Override
public void generate(File routerDir) throws Exception {
// 1.创建RouterLoader类
CtClass ctClass = pool.makeClass(getPackageName() + ".RouterLoader");
CtClass superClass = pool.get("com.didi.drouter.store.MetaLoader");
ctClass.setSuperclass(superClass);

StringBuilder builder = new StringBuilder();
builder.append("public void load(java.util.Map data) {\n");
for (CtClass routerCc : routerClass.values()) {
try {
// 处理注解、class类型等逻辑
...
StringBuilder metaBuilder = new StringBuilder();
metaBuilder.append("com.didi.drouter.store.RouterMeta.build(");
metaBuilder.append(type);
metaBuilder.append(").assembleRouter(");
metaBuilder.append("\"").append(schemeValue).append("\"");
metaBuilder.append(",");
metaBuilder.append("\"").append(hostValue).append("\"");
metaBuilder.append(",");
metaBuilder.append("\"").append(pathValue).append("\"");
metaBuilder.append(",");
if ("com.didi.drouter.store.RouterMeta.ACTIVITY".equals(type)) {
if (!setting.isUseActivityRouterClass()) {
metaBuilder.append("\"").append(routerCc.getName()).append("\"");
} else {
metaBuilder.append(routerCc.getName()).append(".class");
}
} else {
metaBuilder.append(routerCc.getName()).append(".class");
}
metaBuilder.append(", ");
...
metaBuilder.append(proxyCc != null ? "new " + proxyCc.getName() + "()" : "null");
metaBuilder.append(", ");
metaBuilder.append(interceptorClass != null ? interceptorClass.toString() : "null");
metaBuilder.append(", ");
metaBuilder.append(interceptorName != null ? interceptorName.toString() : "null");
metaBuilder.append(", ");
metaBuilder.append(thread);
metaBuilder.append(", ");
metaBuilder.append(priority);
metaBuilder.append(", ");
metaBuilder.append(hold);
metaBuilder.append(")");
...
if (isAnyRegex) {
// 2. 插入路由表
items.add(" put(\"" + uri + "\", " + metaBuilder + ", data); \n");
//builder.append(" put(\"").append(uri).append("\", ").append(metaBuilder).append(", data); \n");
} else {
items.add(" data.put(\"" + uri + "\", " + metaBuilder + "); \n");
//builder.append(" data.put(\"").append(uri).append("\", ").append(metaBuilder).append("); \n");
}
} catch (Exception e) {
e.printStackTrace();
}
Collections.sort(items);
for (String item : items) {
builder.append(item);
}
builder.append("}");

Logger.d("\nclass RouterLoader" + "\n" + builder.toString());
// 3.生成代码
generatorClass(routerDir, ctClass, builder.toString());
}
}
}

此处逻辑比较多,但总体是清晰的,处理完注解和类型的判断,获取路由的信息,构造将要插入的代码,最后统一在父类AbsRouterCollect的generatorClass处理load方法的生成,此时编译器的工作就完成了。


ARouter也提供了arouter-register插件,同是在编译期生成路由表,不同的是在生成代码时,ARouter使用的是ASM,DRouter使用Javassist,查了一下资料,ASM性能比Javassist更好,但更难上手,需要懂字节码知识,Javassist在复杂的字节码级操作上提供了更高级别的抽象层,因此实现起来更容易、更快,只需要懂很少的字节码知识,它使用反射机制。


3.运行期加载路由表


重新贴一下加载路由表的load方法。

public class RouterLoader extends MetaLoader {
@Override
public void load(Map var1) {
var1.put("@@$$/browse/BrowseActivity", RouterMeta.build(RouterMeta.ACTIVITY).assembleRouter("", "", "/browse/BrowseActivity", "com.example.demo.browse.BrowseActivity", (IRouterProxy)null, (Class[])null, (String[])null, 0, 0, false));
}

public RouterLoader() {
}
}

看下RouteMeta的build方法。

public static RouterMeta build(int routerType) {
return new RouterMeta(routerType);
}

可见是直接new的一个路由类,这与ARouter直接通过反射创建路由类不同,性能更好。

private static void register(String className) {
if (!TextUtils.isEmpty(className)) {
try {
// 1.反射创建路由类
Class<?> clazz = Class.forName(className);
Object obj = clazz.getConstructor().newInstance();
if (obj instanceof IRouteRoot) {
registerRouteRoot((IRouteRoot) obj);
} else if (obj instanceof IProviderGroup) {
registerProvider((IProviderGroup) obj);
} else if (obj instanceof IInterceptorGroup) {
registerInterceptor((IInterceptorGroup) obj);
} else {
logger.info(TAG, "register failed, class name: " + className
+ " should implements one of IRouteRoot/IProviderGroup/IInterceptorGroup.");
}
} catch (Exception e) {
logger.error(TAG,"register class error:" + className, e);
}
}
}

4.总结


本文分析了DRouter路由部分的原理,其在编译器使用Gradle Transform和Javassist生成路由表,运行时new路由类,异步初始化加载路由表,实现了高性能。


作者:linversion
链接:https://juejin.cn/post/7241097487540338749
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

你以为搞个流水线每天跑,团队就在使用CI/CD实践了?

在实践中,很多团队对于DevOps 流水线没有很透彻的理解,要不就创建一大堆流水线,要不就一个流水线通吃。实际上,流水线的设计和写代码一样,需要基于“业务场景”进行一定的设计编排,特别是很多通过“开源工具”搭建的流水线,更需要如此(商业的一体化平台大部分已经把...
继续阅读 »

在实践中,很多团队对于DevOps 流水线没有很透彻的理解,要不就创建一大堆流水线,要不就一个流水线通吃。实际上,流水线的设计和写代码一样,需要基于“业务场景”进行一定的设计编排,特别是很多通过“开源工具”搭建的流水线,更需要如此(商业的一体化平台大部分已经把设计思想融入自己产品里了)。



  • 流水线的设计与分支策略有关

  • 流水线的设计与研发活动有关


清晰的代码结构,标准的环境配置,原子化的流水线任务编排,再加上团队的协作纪律,和持续优化的动作,才是真正的践行CI/CD实践


流水线设计原则


1. 确定好变量



  • 哪些是构建/部署需要变化的,比如构建参数,代码地址,分支名称,安装版本,部署机器IP等,控制变化的,保证任务的可复制性,不要写很多hardcode进去


2. 流水线变量/命名的规范化



  • 标准化的命名,有助于快速复制;有意义的流水线命名,有助于团队新成员快速了解


3. 一次构建,多次部署



  • 一次构建,多次部署(多套环境配置+多套构建版本标签);杜绝相同代码重复打包

  • 相似技术栈/产品形态具备共性,通过以上原则可以抽取复用脚本,良好的设计有助于后续的可维护性!


4. 步骤标准化/原子化



  • 比如docker build/push, helm build/deploy, Maven构建等动作标准化,避免重复性写各种脚本逻辑

  • 根据业务场景组装,例如. 提测场景,每日构建场景,回归测试场景


image.png
5. 快速失败



  • 尽可能把不稳定的,耗时短的步骤 放在流水线的最前面,如果把一个稳定的步骤放在前面,并且耗时几十分钟,后面的某个步骤挂了,反馈周期就会变长


从零开始设计流水线


流水线分步骤实施, 从 “点” 到 “线” 结合业务需要串起来,适合自己团队协作开发节奏的流水线才是最好的。



  1. 价值流进行建模并创建简单的可工作流程

  2. 将 构建 和 部署 流程自动化

  3. 将 单元测试和 代码分析 自动化

  4. 将 验收测试 自动化

  5. 将 发布 自动化


image.png


流水线的分层


由于产品本身的形态不同,负责研发的团队人员组成不同,代码的版本管理分支策略不同,使用的部署流水线形式也会各不相同,所以基于实际业务场景设计流水线是团队工程实践成熟的重要标志



1. 提交构建流水线(个人级)


适用场景:每名研发工程师都创建了自己专属的流水线(一般对应个人的开发分支),用于个人在未推送代码到团队仓库之前的快速质量反馈。
注意:个人流水线并不会部署到 团队共同拥有的环境中,而是仅覆盖个人开发环节。如图所示,虚线步骤非必选

image.png


2. 集成验收流水线(团队级)


适用场景:每个团队都根据代码仓库(master/release/trunk)分支,创建产品专属的流水线,部署到 团队共同拥有的环境中e.g. dev)。
注意:如图所示,虚线步骤非必选,根据情况可通过 启动参数true/flase 跳过执行,自动化测试仅限于保证基本功能的用例。

image.png


3. 部署测试流水线(团队级)


适用场景:每个团队的测试工程师都需要专门针对提测版本的自动化部署/测试流水线,部署到团队共同拥有的环境中(e.g. test).
注意:如图所示,该条流水线的起点不是代码,而是提测的特定版本安装包;虚线步骤非必选,根据情况可通过 启动参数true/flase 跳过执行 或 裁剪。

image.png


4. 多组件集成流水线


适用场景:如果一个产品由多个组件构建而成,每个组件均有独自的代码仓库,并且每个组件由一个单独的团队负责开发与维护,那么,整个产品 的部署流水线的设计通常如下图所示。 集成部署流水线的集成打包阶段将自动从企业软件包库中获取每个组件最近成功的软件包,对其进行产品集成打包
image.png


5. 单功能流水线


适用场景:适用于和代码变更无关的场景,不存在上面步骤复杂的编排 (也可通过上述流水线的 启动参数进行条件控制,跳过一些步骤)



  • 针对某个环境的漏洞扫描

  • 针对某个已部署环境的自动化测试

  • 定时清理任务

  • ...


6. 全功能(持续交付)流水线


适用场景:需求、代码构建、测试、部署环境内嵌自动化能力,每次提交都触发完整流水线,中间通过人工审批层次卡点,从dev环境,test环境,stage环境一直到 prod环境。 常适用于快速发布的 PASS/SASS服务,对团队各项能力和流程制度要求较高,支持快速发布(策略)和快速回滚(策略)
image.png


流水线运转全景图


团队研发工程师每人每天都会提交一次。因此,流水线每天都会启动多次。当然并不是每次提交的变更都会走到最后的“上传发布” 。 也不是每次提交都会走到UAT 部署,因为开发人员并不是完成一个功能需求后才提交代码,而是只要做完一个开发任务,就可以提交。每个功能可能由 多个开发任务组成,研发工程师需要确保即使提交了功能尚未开发完成的代码,也不会影响已开发完成的那些功能。
制品经过一个个质量卡点,经历各种门禁验证,最终交付给客户 可以工作的软件
pipeline-status.jpg

收起阅读 »