注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

10年技术进阶路,让我明白了这3件事

这篇也是我分享里为数不多 “进阶” 与 “成长经历” 的文章之一。被别人送到嘴边的食物永远是最香的,但是咱们还是得学会主动去"如何找吃的",授人以鱼不如授人以渔嘛,我希望通过这篇文章能给正在努力的你,迷茫的你,焦虑的你,带来或多或少的参考、建议或者指引。 十年...
继续阅读 »

这篇也是我分享里为数不多 “进阶”“成长经历” 的文章之一。被别人送到嘴边的食物永远是最香的,但是咱们还是得学会主动去"如何找吃的",授人以鱼不如授人以渔嘛,我希望通过这篇文章能给正在努力的你,迷茫的你,焦虑的你,带来或多或少的参考、建议或者指引。


十年,谁来成就你?


  离开校园,一晃已十年,时日深久,现在我已成为程序员老鸟了,从软件工程师到系统架构师,从被管理者到部门负责人,每一段经历的艰辛,如今回忆仍历历在目。各位同行你们可能正在经历的迷茫,焦虑与取舍,我也都曾经历过。


  今天我打算跟大家分享下我这些年的一个成长经历,以此篇文章为我十年的职业历程画上一个完满的句号。这篇文章虽说不是什么“绝世武功”秘籍,更没法在短时间内把我十年的“功力”全部分享于你。篇幅受限,今天我会结合过往种种挑重点说一说,大家看的过程中,记住抓重点、捋框架思路就行了。希望在茫茫人海之中,能够给到正在努力的你或多或少的帮助,亦或启发与思考。


试问,你的核心竞争力在哪?


  你曾经是否怕被新人卷或者代替?如果怕、担忧、焦虑,我可以很负责任地告诉你,那是因为你的核心竞争力还不够!这话并不好听,但,确是实在话。认清现状,踏实走好当下就行,谁能一开始或者没破茧成蝶时就一下子有所成就。


  实质上,可以这么说,经验才是我们职场老鸟的优势。 但是,经验并不是把同一件事用同一种方式重复做多少年,而是把咱们过往那么多年头的实践经验,还有被验证的理论,梳理成属于自己的知识体系,建立一套自己的思维模式,从而提升咱们的核心竞争力。


    核心竞争力的形成,并非一蹴而就,我们因为积累所以专业,因为专业所以自信,因为自信所以才有底气。积累、专业、自信、底气之间的关系,密不可分。


核心竞争力,祭出三板斧


  道理咱们都懂,能不能来点实在的?行!每当身边朋友或者后辈们,希望我给他们传授一些“功力”时,我都会给出这样的三个建议:



  1. 多面试,验本事。

  2. 写博客,而且要坚持写。

  3. 拥有自己的 Github 项目。 



  其中,博客内容和 Github 项目,将会成为咱们求职道路上的门面,这两者也是实实在在记录你曾经的输出,是非常有力有价值的证明。此外,面试官可以通过咱们的博客和 Github,在短时间内快速地了解你的能力水平等。或许你没有足够吸引、打动人的企业背景,也没有过硬的学历。但!必须有不逊于前两者的作品跟经历。


  再说说面试,我认为,它是我们接受市场与社会检验的一种有效方式。归根结底,咱们所付出的一切,都是为了日后在职业发展上走得越来越好。有朋友会说,面试官看这俩“门面”几率不大,没错,从我多年的求职经历来看,愿意看我作品的面试官也只占了 30%。


  但是,谁又能预判到会不会遇到个好机会呢?有准备,总比啥也没有强,千里马的亮点是留给赏识它的伯乐去发现的


PS:拥有自己 Github 项目与写博,都属于一种输出的方式,本文就以写博作为重点分享。写博与面试会在下文继续展开。


记忆与思考,经验与思维


  武器(三板斧)咱们已经有了,少了“内功心法”也不行。这里分享下我的一些观点,也便于大家后续能够更好地参与到具体的实践中。




  • 记忆——记忆如同对象一样是具有生命周期,久了不用就会被回收(忘记)。




  • 思考——做任何事情就如同咱们写代码Function一样,得有输入同时也得有输出,输入与输出之间还得有执行。






  •  




  日常工作中,就拿架构设计当例子。作为架构师是需要针对现有的问题场景提出解决方案,作为架构师的思考输入是业务场景、团队成员、技术选型等,而它的输出就是基于前面的多种输入参数从而产出的短期或长期的解决方案,而且最终会以文档形式保存下来。


  保存下来的目的,是为方便我们日后检索、回忆、复用。因此,在业余学习中同理,给与我们的输入是书籍、网络的资料或同行的传递等,而作为输出则是咱们记录下来的笔记、博客甚至是 Github 的项目 Demo。



基于上述,我们需要深刻意识到心法三要素:



  1. 带着明确的输出目的,才会真正地促进自己的思考。蜻蜓点水、泛泛而谈,是无法让自己形成对事物的独特见解和具象化输出,长期如此,并无良益。

  2. 只有尽可能通过深度思考过后的产出,才能够形成属于自己真正的经验。

  3. 知识的点与点之间建立联系,构成明晰的知识体系,经验与经验则形成了自己独有的思维模式。


多面试,验本事


  既然“武器”和“内功心法”咱们都有了,那么接下来得开始练“外功”了,而这一招叫"多面试,验本事"。


  我身边的同行与朋友,对我的面试行为感到奇怪:你每隔一段时间就去面试,有时拿到了 offer 还挺不错的,但是又没见想着跳槽,这是为何?


风平浪静,居安思危


  回应这个疑问之前,我想反问大家 4 个问题:



  1. 是否曾遇到过在一家公司呆了太久过于安逸,也阶段性想过离开,发现真要走可却没了跳槽的勇气?

  2. 再想一想,日子一久,你们是不是就不清楚行业与市场上,对人才能力的需求了?

  3. 是否有经历过公司意外裁员,你在找工作的时段里有没有强烈感受到那种焦虑、无助?

  4. 是否对来之不易的 offer,纠结不知道如何抉择,又或者,最终因为迫于各方面压力,勉为其难接受了不太中意的那个?



  刚提到的种种问题,那份焦虑、无助、纠结与妥协,我曾经在职场都经历过。我们想象一下,如果你现在随随便便出去面试五个公司能拿到三四个 offer,你还会有那失业的焦虑么?如果现在拿到的那几个 offer 正好都不喜欢,你全部放弃了,难道你会愁后续没有其他机会了么?显然不会!因为你有了更多底气和信心


  我再三思考,还是觉得有必要给大家分享一个我的真实经历。希望或多或少可以给你一点启发:


  2019 年,因为 A 公司业务原因,我离开了工作 3 年的安逸的环境,市场对人才的需求我已经是模糊的了,当我真正面临时,我焦虑、我无助。幸好曾经跟我合作过的老领导注意到了这我这些年的成长,向我施予援手。入职 B 公司后,我重新审视自己,并给与自己定了个计划——每半年选一批公司面试。


一年以后,因为 B 公司因疫情原因,我再次离职。这次,我没有了焦虑,取而代之的是自信与底气,裸辞在家开始了我的休假计划。在整个休假期,我拒绝了两个满足我的高薪 offer,期间我接了个技术顾问的兼职,剩余时间把以前囤下来的书看了个遍,并实践了平常没触碰到的技术盲区。三个月后,我带着饱满的精神面貌再次"出山",入职了现在这家公司。


  有人会问:你现在还有没有坚持自己的面试计划?毫无避讳回答:有!仍然是半年一次。


乘风破浪,未雨绸缪


  就前面这些问题、情况,这里结合我自己多年来的一些经验,也希望给到大家一点破局建议:保持一定的面试频率,就如上文提到的“三板斧”,面试是接受市场与社会检验,非常直接、快速、有效的一种好方式。 当然,我可不是怂恿你频繁跳槽,没有多少公司能够欣然接受不稳定的员工,特别是岗位越做越高时。


  看到这里,有些伙伴可能会想,我现在稳稳当当的、好端端的,干嘛要去面试,何必折腾自己。假若你在体制内,我这点建议或许参考意义不大。抛开体制内的讨论,大家认为真的有所谓的“稳定”的工作吗?


  我认为所谓的“稳定”,都是只是暂时的,甚至虚幻的,没有任何的人、资本、企业能给你实打实的承诺,唯一能让你“稳定”持续发展下去的,只有你的能力与眼界、格局等。


  疫情也有几年了,相信大家也有了更多思考,工作上,副业上等等各方面吧。人无远虑,必有近忧,未雨绸缪,实属必要!



放平心态,查缺补漏


  面试是相对“主观的”,这是因为“人性”的存在,你可能会听过让人哭笑不得的拒绝你的理由:



  • 连这么基础的知识都回答不上,还想应聘这岗位

  • 你的性格并不适合当管理,过于主动对团队不好


  咱们先抛开这观点的对与错。人无完人,每个人都有自己的优点与缺点,甚至你的优点可能是你的缺点。职场长路漫漫,要是把每一次的面试都当成人生中胜负的较量,那咱们最后可能会输的体无完肤。咱们付出任何的努力,也只是单纯提高“成功率”而已。听我一句劝,放平心态,以沟通交流为主,查漏补缺为辅


  近几年我以面架构师和负责人的岗位为主,面试官大多数喜欢问思想和方法论这类的问题,他们拥有不同的细节的侧重点,因此我们以梳理这些“公共”的点出发,事后复盘自己回答的完整性与逻辑性,对于含糊不清的及时找资料补全清晰,尝试模拟当时回答的场景。每一段面试,如此反复。


  作为技术人我建议,除了会干,还得会说,我们不仅有硬实力,还得有软技能。


PS:篇幅有限,具体面试经历就不展开了,如果大家对具体的面试经历感兴趣,有机会我给大家来一篇多年的"面经"。


持续进步


编程语言本身在不断进步,对于菜鸟开发者来说,需要较高的学习成本。但低代码平台天然就具备全栈开发能力,低代码程序员天然就是全栈程序员。


这里非常推荐大家试试JNPF快速开发平台,依托的是低代码开发技术原理,因此区别于传统开发交付周期长、二次开发难、技术门槛高的痛点,在JNPF后台提供了丰富的解决方案和功能模块,大部分的应用搭建都是通过拖拽控件实现,简单易上手,在JNPF搭建使用OA系统,工作响应速度更快。可一站式搭建生产管理系统、项目管理系统、进销存管理系统、OA办公系统、人事财务等等。


开源链接:http://www.yinmaisoft.com/?from=jueji…


狠下心来,坚持到底


锲而舍之,朽木不折;锲而不舍,金石可镂——荀况


  要是把"多面试"比喻成以"攻"为主的招式,而"写博客"则是以"守"为主的绝招。


  回头看,今年,是我写博客的第八个年头了,虽说写博频率不高,但整体时间跨度还是挺大的。至今我还记得我写博客的初心,用博客记录我的学习笔记,同时抛砖引玉,跟同行来个思维上的碰撞。


  随着工作年限的增长,我写博客的内容慢慢从学习笔记变成了实战记录,也越来越倾向于输出经验总结和实践心得。实质上,都是在传达我的观点与见解。


  而这,至关重要。反过来看,后面机会来了,平台联系人也可以借此快速评估、判断这人会不会讲、能不能讲,讲得怎么样,成的话,人家也就快速联系咱了。进一步讲,每一次,于个人而言,都是好机会。



写博第一步,从记笔记开始


  我相信不少的同行曾经面临这样的境况,都有产生过写博客的念头,有些始终没有迈出第一步,有些中途停了下来,这里可能有不少的原因:要么不知道写什么、要么觉得写了也没人看、还有一种是想写但是比较懒等等。


我觉得,一切的学习,前期都是从模仿开始的 学习笔记,它就是很好的便于着手的一种最佳方式。相信大家在学生年代或多或少都写过日记,就算是以流水账的方式输出,博客也可以作为非常好的开启平台。


  由于在写博客的时候,潜意识里会认为写出来的东西会给更多人看,因此自己写的内容在不明确的地方都会去找资料再三确认,这是很有效的一种督促方法。确认的过程中,也会找到许多相关的知识点,自然而然就会进一步补充、完善、丰富我们自己原有或现在的知识体系


幸运,需要自己争取


  在写博客的这段时间里,除了梳理自己的知识体系之外,还能结交了一些拥有共同目标的同行,我想,这就是真正的志同道合吧。


  甚至在你的博客质量达到了一定程度——有深度与广度,会有一些意象不到的额外小收获。例如有一些兼职找到自己,各大社区平台会邀请自己合作,也会收到成就证明与礼物等等。



意外地成为了讲师


  到目前为止,正式作为讲师或者是技术顾问,以这样不同于往常的既有角色,我真切地经历了几次。虽次数不多,但每一次过后,即便时日深久,可现在回想起来,于我的成长而言,那都是一次又一次新的蜕变,真实而猛烈,且带给我一次次新生力量。


  话说回来,前面提到几次分享,有的伙伴可能会说了,这本来就性格好又爱分享的人,个例罢了,不一定适合大多数啊。说到这儿,我想,我有必要简短地跟你聊一下我自己。


跌跌撞撞,逆水行舟


  对于过往的自己,我的评价是从小就闷骚、内向的那种性格,只要在人多的时候发言就会慌会怂会紧张,自己越慌就越容易表达出错,如此恶性循环。随着我写博的篇幅越多,慢慢地我发现自己讲话时喜欢准备与思考,想好了再去表达,又慢慢地讲话就具有条理性与逻辑性了。


  当代著名哲学家陈嘉映先生,他曾在一本书里说过这样一句话,放到这里再合适不过了—— "成长无时无刻不是在克服某些与生俱来的感觉和欲望"


  回头看,一路走来,我从最初的摸索、探索、琢磨,到看到细微变化,到明显感知到更大层面的进步,再到后来的游刃有余,输出很有见地的思考,分享独到观点。


  我想,这背后,离不开一次次尝试,一次次给自己机会,一次次认真、负责地探索突破自己。其实,大多数人,还真是这么跌跌撞撞挺过来的。


伺机而动,用心准备


  2020 年,我第一次被某企业找到邀请我作为技术顾问是通过我的博客,这一次算是小试牛刀,主要以线上回答问题、交流为主。因为事先收集好了需要讨论的话题与问题,整个沟通持续了两个小时,最终也得到了对方老板的高度认可。


  此事过后,我重新审视了自己,虽然我口才并不突出,但是我基于过往积累的丰富经验与知识融合,并能够正确无误地传达输出给对方,我认为是合格的了。坦率来讲,从那之后我不再怀疑自己的表达能力。同时有另外一件事件更值得重视,基于让自己得到更多更广泛的一个关注,思前想后,概括来讲,我还是觉得落到这句话上更合适,就是:建立个人 IP


建立个人 IP


  那么,我希望打造个人 IP 的原因是什么呢?希望或多或少也可以给你提供一点可供借鉴、探讨的方向。


  我个人而言,侧重这样几个层面吧。



  1. 破局: 一个是我希望打破 35 岁魔咒,这本质上是想平稳快速度过职业发展瓶颈期;

  2. 觅友: 希望结识到拥有同样目标的同行,深度交流,构建技术圈人脉资源网;

  3. 动力 从中获取更多与工作不一样的成就感。有了强驱动力,也会使我在分享这条路上变得更坚定。


链接资源,提影响力


  在《人民的名义》里祁同伟说过一句话,咱们就是人情的社会。增加了人脉,就是增加自己的机会。当然前提是,咱们自己得需要有这个实力。


  建立个人 IP,最要提高知名度,而提知名度的主要方式是两种:写书、做讲师。后面我会展开讲,写书无疑是宣传自己的最好方式之一,但整个过程不容易,周期比较长。作为写书的简化版,我们写博客就是一种捷径了。


主动出击,勿失良机


  而作为讲师,线上线下各类形式参与各种社区峰会露脸,这也是一种方式。不过这种一般会设有门槛。


  这里不得不多提一句,就是建立 IP 它是一个循序渐进的过程,欲速则不达,任何时候咱们都得靠内容作品来说话, 当你输出的质量够了,自然而然社区人员、企业就会找到你,机会顺理成章来了。反过来讲,我们也得常盯着,或者说多留心关注业内各平台的内容风格,利用好业余零碎时间,好好梳理下某个感兴趣的内容平台,看看他们到底都倾向于打造什么样的东西。做到知己知彼,很重要。


  我认识的一个前辈,之前阿里的,他非常乐于在博客上分享自己的经验与见解,随着他分享的干货越多,博客影响力越大,某内容付费平台找到他合作出了个专栏,随着专栏的完结,他基于专栏内容又出了一本书,而现在的他已经离开了阿里,成为了自由职业者。


追求成就感,倒逼突破自我


  每一次写博客、做讲师,都能更大程度上填满我内心深处的空洞,或许是每一个支持我的留言与点赞,或许是每一节分享停顿间的掌声。如果我们抱着非常强的目的去做的时候,可能会事与愿违。就以我做讲师来说,因为我是一个新手,在前期资料准备所花费的精力与时间跟后续的课酬是不成正比的。


  作为动力源,当时我会把侧重点放到结交同行上,同时利用“费曼学习法”重新梳理知识,另外寻找机会突破自己的能力上限。



  大家有没有想过,讲课最终受益者的是谁?有些朋友会回答“双方”。但是我很负责任地告诉你,作者、讲师自己才是最大的知识受益者。


  如前面所讲,写博客为了更好地分享出更具价值性的内容,为保证专业性,咱们得再三确认不明确的点,而讲课基于写博客的基础上,还得以听众的角度,去思考、衡量、迭代,看看怎么让人家更好地理解、吸收、用得上这些知识点,甚至讲师得需要提前模拟、预估可能会在课后被提的问题。


这里总结一下,写博客与讲课的方式完全不同,因为博客是以图、文、表的方式展现,读者看不明白可以回头去看,但是讲课则没有回头路,是一环套一环的,所以梳理知识线的连贯性要求更强


  我个人认为,日常工作大多数是重复的、枯燥的,或者说,任何兴趣成了职业,那将不再是兴趣,或许只有在业余的时候获取那些许的成就感,才会重新燃起自己的那一份初心 ——行之于途而应于心。


源不深而望流之远,根不固而求木之长


  求木之长者,必固其根本;欲流之远者,必浚其源泉——魏徵


  有些同行或许会问:”打铁还需自身硬“这道理咱们都懂,成长进阶都离不开学习,但这要是天天写 BUG 的哪来那么多时间学?究竟学习的方向该怎么走呢?在这里分享下我的实际做法,以及一些切身的个人体会,希望可以提供一点借鉴、参考。


零碎时间,稳中求进


  6 年前,我确定往系统架构师这个目标发展的时候,每天都会做这么两件事:碎片化时间学习,及时产出笔记。



  • 上班通勤与中午休息,我会充分利用这些碎片时间(各 30 分钟)尽可能地学习与吸收知识,每天坚持一小时的积累,积少成多,两年后你会发现,效果非常可观,这就是一个量变到质变的过程


  而且有神经科学相关表明,”间歇式模块化学习的效果最佳,通勤路上就是实践这种模式的理想世界。“大家也可以多试试看。当然,一开始你学习某个领域的知识,可能效率没那么高,我建议你可以反复地把某一节掰开了揉碎了看或者听,直到看明白听懂了为止,接着得怎么做?如我前面说,咱们得要有输出!


  看过这样一段话,”写和想是不同的,书写本身就是逻辑推演和信息梳理的过程。“而且,研究表明,”人的记忆力会在 17-24 岁达到高峰,25 岁之后会下降,理解力的发展曲线会延后 5 年,也就是说在 30 岁之后也会下降。“


  你看,这个也直接或者间接告诉我们,还是趁早多做记录、多学习。文字也好,视频也罢,到底啥形式不重要,适合自己能长久坚持的就行,我相信你一定能从中受益。毕竟,这些累积的,可都是你自己实实在在的经验和思考沉淀!


  话说回来,其实做笔记能花多长时间,就算在工作时间花半小时也有良效,而这时间并不会对自己的工作进度造成多么大的影响,但!一定时日深久,受益良多。


构建知识 体系 丰富 思维 模式


  由于我们日常需要快速解决技术难题,很多时候从外界吸收到的知识点相对来说很零散,而知识体系是由点、线、面、体四个维度构造而成的


  那怎么做能够快速把知识串联起来呢?这里我举个简单的例子,方便大家理解。


  以我们系统性能调优出发,首先我们需要了解系统相关性能瓶颈的业务场景是什么?该功能是 I/O 密集型还是 CPU 密集型?如果是 I/O 密集型多数的性能瓶颈在数据库,这个时候我们就得了解数据库瓶颈的原因,究竟是数据量大还是压力大?如果是数据量大,基于现有的业务场景应该选择数据归档、临时表还是分库分表,这之间的方案优缺点有什么不同?适用场景怎么样?假如是数据压力大了,我们是否能用 Redis 做缓存抗压就行?


  再接着从 Redis 这个点继续思考,假如 Redis 内存满了会怎样?我们又了解到了 Redis 的内存淘汰策略,设置了 volatile-lru 策略,由于我们基本功扎实回忆起 LUR 算法是基于链表的数据结构,虽然链表的写的时间复杂度是 O(1),但是读是 O(n),不过我们得先读后写,所以为了高性能又选择 Hash 这种 O(1)的数据结构辅助读的处理。


  你看,我们是不是从问题出发到架构设计,再从数据库优化方案到 Redis 的使用,最后到数据结构,这一些系统的知识就串联起来了?



作者:雾岛听风来
来源:juejin.cn/post/7246593325935493178
收起阅读 »

echart 桑基图的点击高亮

web
先上效果图 引入echarts-for-react import ReactEcharts from 'echarts-for-react'; 增加点击事件, 这里需要注意的是当用的是setState时,在onChartReady里获取的state的值一直...
继续阅读 »

先上效果图


image.png
引入echarts-for-react


import ReactEcharts from 'echarts-for-react';

增加点击事件, 这里需要注意的是当用的是setState时,在onChartReady里获取的state的值一直是空值,所以增加useRef来临时存放curHighLight的值;


const [curHighLight, setCurHighLight] = useState(null);
const curHighLightRef = useRef(null);

<ReactEcharts
notMerge={true}
option={chartOption}
onChartReady={(EChartsInstance) =>
{
ChartsInstance.current = EChartsInstance;
// 双击高亮
ChartsInstance.current.on('click', (params) => {
console.log('点击高亮', params);

if (isHighlighted(params, curHighLightRef.current)) {
setCurHighLight(null);
curHighLightRef.current = null;
} else {
const cur = {
dataType: params?.dataType,
name: params?.data?.name,
source: params?.data?.source,
target: params?.data?.target
}
setCurHighLight(cur);
curHighLightRef.current = cur;
}

return false;
});
}}
/>


判断是否已经被点击过


const isHighlighted = (params, curHighLight) => {
if (params.dataType === 'node') {
return params?.data?.name === curHighLight?.name;
}

if (params.dataType === 'edge') {
return params?.data?.source === curHighLight?.source && params?.data?.target === curHighLight?.target;
}

return false;
}

点击事件增加后,把当前点击节点或连接线存起来后,再通过useEffect更新option



  1. 调整lineStyle和itemStyle里 opacity 值


const temp = cloneDeep(chartOption);
temp.series[0].lineStyle.opacity = curHighLight === null ? lineOpacity / 100 : 0.1;
temp.series[0].itemStyle.opacity = curHighLight === null ? 1 : 0.1;
temp.series[0].emphasis.disabled = curHighLight !== null;


  1. 调整高亮节点的


// 获取高亮详情
const getHighLightInfo = ({ curHighLight, links, nodes }) => {
// 当取消高亮时,文字颜色恢复正常
if (curHighLight === null) {
const isHighLight = false;
links?.forEach(item => {
item.isHighLight = isHighLight;
item.lineStyle.opacity = null;
});

nodes.forEach(item => {
item.isHighLight = isHighLight;
item.itemStyle.opacity = null;
item.label = {
color: null
}
});
}

// 节点
if (curHighLight?.dataType === 'node') {
const selectList = [];
links.forEach(item => {
const isHighLight = item.source === curHighLight.name || item.target === curHighLight.name;
item.isHighLight = isHighLight;
item.lineStyle.opacity = isHighLight ? opacityHL_link : 0.1;

if (isHighLight) {
selectList.push(item);
}
});

nodes.forEach(item => {
const isIn = selectList.find(v => v.source === item.name || v.target === item.name);
const isHighLight = !!isIn;

item.isHighLight = isHighLight;
item.itemStyle.opacity = isHighLight ? opacityHL_node : 0.1;
item.label = {
color: !isHighLight ? 'rgba(0, 0, 0, 0.35)' : null
}
});
}

// 连线
if (curHighLight?.dataType === 'edge') {
links?.forEach(item => {
const isHighLight = item.source === curHighLight?.source && item.target === curHighLight?.target;
item.isHighLight = isHighLight;
item.lineStyle.opacity = isHighLight ? opacityHL_link : 0.1;
});

nodes.forEach(item => {
const isHighLight = item.name === curHighLight.source || item.name === curHighLight.target;
item.isHighLight = isHighLight;
item.itemStyle.opacity = isHighLight ? opacityHL_node : 0.1;
item.label = {
color: !isHighLight ? 'rgba(0, 0, 0, 0.35)' : null
}
});
}
}

作者:一切随意
来源:juejin.cn/post/7293788137662677026
收起阅读 »

通知栏的那些奇技淫巧

一、问题的由来 前几天,一个网友在微信群提了一个问题: 通知栏监听模拟点击如何实现? 我以为业务情景是在自己应用内,防止脚本模拟点击而引申出来的一个需求,心里还在想,是否可以使用自定义View——onTouchEvent的参数MotionEvent的ge...
继续阅读 »

一、问题的由来




前几天,一个网友在微信群提了一个问题:



通知栏监听模拟点击如何实现?



我以为业务情景是在自己应用内,防止脚本模拟点击而引申出来的一个需求,心里还在想,是否可以使用自定义View——onTouchEvent的参数MotionEventgetPressure来判断是否是模拟点击。后来经过沟通得知,业务需求是如何监听第三方应用的通知栏,实现具体按钮的点击。如下图:


通知栏.jpg


上面是多家音频应用的通知栏在小米手机的样式,而网友的需求是如何实现针对某一个应用通知栏的某一个按钮的点击,比如监听喜马拉雅APP,当接收到通知的时候,需要点击关闭按钮。这个需求该如何接住呢?


二、实现方案之无障碍服务




当需求清晰以后,我心里面想到的第一个方案就是无障碍服务。但是无障碍服务点击通知栏简单,点击通知栏的某一个控件需要打开通知栏,然后找到这个控件的id,然后调用点击方法。同时由于几年前有过写抢红包脚本的经验,提出了一些疑问:



  • 用户使用的业务情景是什么?是否需要正规渠道上架?

  • 无障碍服务权限相当敏感,是否接受交出权限的选择?


沟通结果是正规渠道上架和业务情景不用考虑,但是权限的敏感需要换一个思路。网友指出,NotificationListenerService可以实现监听通知栏,能否在这个地方想点办法呢?而且他还提到一个业务情景:当收到通知的时候,不需要用户打开通知栏列表,不管用户在系统桌面,还是第三方应用页面,均需要实现点击具体按钮的操作。
虽然我此时对NotificationListenerService不熟悉,但是一听到这个反常识的操作,我顿时觉得不现实,至少是需要一些黑科技才能在部分设备实现这个效果。因为操作UI需要在主线程,但是系统当前的主线程可能在其它进程,所以我觉得这个地方反常识了!


三、实现方案之通知监听服务




由于上面的沟通过程因为我不熟悉 NotificationListenerService导致我battle的时候都不敢大声说话,因此我决定去熟悉一下,然后我看到了黄老师的这篇 Android通知监听服务之NotificationListenerService使用篇


看到黄老师实现微信抢红包以后,我也心动了,既然黄老师可以抢红包,那么是不是我也可以抢他的红包?于是就开始了踩坑之旅。


3.1 通知栏的那些事


我们知道,通知栏的显示、刷新、关闭都是依赖于Notification来实现,而通知栏的UI要么是依托系统主题实现,要么是通过自定义RemoteViews实现,而UI的交互则是通过PendingIntent包装的Intent来实现具体的意图。


// 通知栏的`UI`依托系统主题实现
NotificationCompat.Builder(context, Notification.CHANNEL_ID)
.setStyle(androidx.media.app.NotificationCompat.MediaStyle()
// show only play/pause in compact view
.setShowActionsInCompactView(playPauseButtonPosition)
.setShowCancelButton(true)
.setCancelButtonIntent(mStopIntent)
.setMediaSession(mediaSession)
)
.setDeleteIntent(mStopIntent)
.setColorized(true)
.setSmallIcon(smallIcon)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setOnlyAlertOnce(true)
.setContentTitle(songInfo?.songName) //歌名
.setContentText(songInfo?.artist) //艺术家
.setLargeIcon(art)


/**
* 创建RemoteViews
*/

private fun createRemoteViews(isBigRemoteViews: Boolean): RemoteViews {
val remoteView: RemoteViews = if (isBigRemoteViews) {
RemoteViews(packageName, LAYOUT_NOTIFY_BIG_PLAY.getResLayout())
} else {
RemoteViews(packageName, LAYOUT_NOTIFY_PLAY.getResLayout())
}
remoteView.setOnClickPendingIntent(ID_IMG_NOTIFY_PLAY.getResId(), playIntent)
remoteView.setOnClickPendingIntent(ID_IMG_NOTIFY_PAUSE.getResId(), pauseIntent)
remoteView.setOnClickPendingIntent(ID_IMG_NOTIFY_STOP.getResId(), stopIntent)
remoteView.setOnClickPendingIntent(ID_IMG_NOTIFY_FAVORITE.getResId(), favoriteIntent)
remoteView.setOnClickPendingIntent(ID_IMG_NOTIFY_LYRICS.getResId(), lyricsIntent)
remoteView.setOnClickPendingIntent(ID_IMG_NOTIFY_DOWNLOAD.getResId(), downloadIntent)
remoteView.setOnClickPendingIntent(ID_IMG_NOTIFY_NEXT.getResId(), nextIntent)
remoteView.setOnClickPendingIntent(ID_IMG_NOTIFY_PRE.getResId(), previousIntent)
remoteView.setOnClickPendingIntent(ID_IMG_NOTIFY_CLOSE.getResId(), closeIntent)
remoteView.setOnClickPendingIntent(ID_IMG_NOTIFY_PLAY_OR_PAUSE.getResId(), playOrPauseIntent)
return remoteView
}

// 通过自定义`RemoteViews`实现
val notificationBuilder = NotificationCompat.Builder(context, CHANNEL_ID)
notificationBuilder
.setOnlyAlertOnce(true)
.setSmallIcon(smallIcon)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentTitle(songInfo?.songName) //歌名
.setContentText(songInfo?.artist) //艺术家

1. StatusBarNotification的逆向之旅


有了上面的了解,那么我们可以考虑通过Notification来获取PendingIntent,实现通知栏模拟点击的效果。
通过NotificationListenerService的回调方法,我们可以获得StatusBarNotification,源码如下:


override fun onNotificationPosted(sbn: StatusBarNotification?) {
super.onNotificationPosted(sbn)
}

接下来,我们需要从这个地方开始,抽丝剥茧般地一步一步找到我们想要的PendingIntent
先观察一下StatusBarNotification的源码:


@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 
private final Notification notification;

public StatusBarNotification(String pkg, String opPkg, int id,
String tag, int uid, int initialPid, Notification notification, UserHandle user,
String overrideGr0upKey, long postTime) {
if (pkg == null) throw new NullPointerException();
if (notification == null) throw new NullPointerException();

this.pkg = pkg;
this.opPkg = opPkg;
this.id = id;
this.tag = tag;
this.uid = uid;
this.initialPid = initialPid;
this.notification = notification;
this.user = user;
this.postTime = postTime;
this.overrideGr0upKey = overrideGr0upKey;
this.key = key();
this.groupKey = groupKey();
}

/**
* The {@link android.app.Notification} supplied to
* {@link android.app.NotificationManager#notify(int, Notification)}.
*/
public Notification getNotification() {
return notification;
}

这里我们可以直接获取到Notification这个对象,然后我们继续观察源码,


/**
* The view that will represent this notification in the notification list (which is pulled
* down from the status bar).
*
* As of N, this field may be null. The notification view is determined by the inputs
* to {@link Notification.Builder}; a custom RemoteViews can optionally be
* supplied with {@link Notification.Builder#setCustomContentView(RemoteViews)}.
*/

@Deprecated
public RemoteViews contentView;

虽然这个contentView已经标记为不建议使用了,但是我们可以先尝试跑通流程。然后再将这个思路拓展到非自定义RemoteViews的流程。
经过测试,这里我们已经可以获取到RemoteViews了。按照惯例,这里我们需要继续观察RemoteViews的源码,从设置点击事件开始:


public void setOnClickPendingIntent(@IdRes int viewId, PendingIntent pendingIntent) {
setOnClickResponse(viewId, RemoteResponse.fromPendingIntent(pendingIntent));
}
// 👇
public static class RemoteResponse {
private PendingIntent mPendingIntent;
public static RemoteResponse fromPendingIntent(@NonNull PendingIntent pendingIntent) {
RemoteResponse response = new RemoteResponse();
response.mPendingIntent = pendingIntent;
return response;
}

}
// 👆
public void setOnClickResponse(@IdRes int viewId, @NonNull RemoteResponse response) {
addAction(new SetOnClickResponse(viewId, response));
}


// 响应事件 👆
private class SetOnClickResponse extends Action {

SetOnClickResponse(@IdRes int id, RemoteResponse response) {
this.viewId = id;
this.mResponse = response;
}

SetOnClickResponse(Parcel parcel) {
viewId = parcel.readInt();
mResponse = new RemoteResponse();
mResponse.readFromParcel(parcel);
}

public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(viewId);
mResponse.writeToParcel(dest, flags);
}

@Override
public void apply(View root, ViewGr0up rootParent, final InteractionHandler handler,
ColorResources colorResources)
{
final View target = root.findViewById(viewId);
if (target == null) return;

if (mResponse.mPendingIntent != null) {
// If the view is an AdapterView, setting a PendingIntent on click doesn't make
// much sense, do they mean to set a PendingIntent template for the
// AdapterView's children?
if (hasFlags(FLAG_WIDGET_IS_COLLECTION_CHILD)) {
Log.w(LOG_TAG, "Cannot SetOnClickResponse for collection item "
+ "(id: " + viewId + ")");
ApplicationInfo appInfo = root.getContext().getApplicationInfo();

// We let this slide for HC and ICS so as to not break compatibility. It should
// have been disabled from the outset, but was left open by accident.
if (appInfo != null
&& appInfo.targetSdkVersion >= Build.VERSION_CODES.JELLY_BEAN) {
return;
}
}
target.setTagInternal(R.id.pending_intent_tag, mResponse.mPendingIntent);
} else if (mResponse.mFillIntent != null) {
if (!hasFlags(FLAG_WIDGET_IS_COLLECTION_CHILD)) {
Log.e(LOG_TAG, "The method setOnClickFillInIntent is available "
+ "only from RemoteViewsFactory (ie. on collection items).");
return;
}
if (target == root) {
// Target is a root node of an AdapterView child. Set the response in the tag.
// Actual click handling is done by OnItemClickListener in
// SetPendingIntentTemplate, which uses this tag information.
target.setTagInternal(com.android.internal.R.id.fillInIntent, mResponse);
return;
}
} else {
// No intent to apply, clear the listener and any tags that were previously set.
target.setOnClickListener(null);
target.setTagInternal(R.id.pending_intent_tag, null);
target.setTagInternal(com.android.internal.R.id.fillInIntent, null);
return;
}
target.setOnClickListener(v -> mResponse.handleViewInteraction(v, handler));
}

@Override
public int getActionTag() {
return SET_ON_CLICK_RESPONSE_TAG;
}

final RemoteResponse mResponse;
}




private void addAction(Action a) {
if (hasMultipleLayouts()) {
throw new RuntimeException("RemoteViews specifying separate layouts for orientation"
+ " or size cannot be modified. Instead, fully configure each layouts"
+ " individually before constructing the combined layout.");
}
if (mActions == null) {
mActions = new ArrayList<>();
}
mActions.add(a);
}


上面代码有点多,我画个图方便大家理解:


未命名文件.jpg


至此,我们就知道了PendingIntent的藏身之处了!
通过反射,正常情况下我们就能拿到所有属于SetOnClickResponse#PendingIntent了,上代码:


override fun onNotificationPosted(sbn: StatusBarNotification?) {
super.onNotificationPosted(sbn)
sbn?:return
if(sbn.packageName == "com.***.******"){
// 获取通知
val cls = sbn.notification.contentView.javaClass
// 点击事件容器
val field = cls.getDeclaredField("mActions")
field.isAccessible = true
// 点击事件容器对象
val result = field.get(sbn.notification.contentView)
// 强转
(result as? ArrayList<Any>?)?.let { list ->
// 筛选点击事件的实现类集合
// 此处需要判断具体的点击事件
list.filter { item -> item.javaClass.simpleName == "SetOnClickResponse" }.first().let { item ->
// 获取响应对象
val response = item.javaClass.getDeclaredField("mResponse")
response.isAccessible = true
// 强转
(response.get(item) as? RemoteViews.RemoteResponse)?.let { remoteResponse ->
// 获取PendingIntent
val intentField = remoteResponse.javaClass.getDeclaredField("mPendingIntent")
intentField.isAccessible = true
val target = intentField.get(remoteResponse) as PendingIntent
Log.e("NotificationMonitorService","最终目标:${Gson().toJson(target)}")
}

}
}

}

}

2. 反射的拦路鬼——Android平台限制对非SDK接口的调用


不出意外的还是有了意外,明明反射的字段存在,就是反射获取不到。


反射失败.png
就在一筹莫展之际,有朋友提出了一个思路——针对非SDK接口的限制。然后经过查询,果然是反射失败的罪魁祸首!


WechatIMG889.png
既然确诊了病症,那么就可以开始开方抓药了!
根据轮子bypassHiddenApiRestriction绕过 Android 9以上非SDK接口调用限制的方法,我们成功的获取到了PendingIntent.


override fun onNotificationPosted(sbn: StatusBarNotification?) {
super.onNotificationPosted(sbn)
sbn?:return
if(sbn.packageName == "com.lzx.starrysky"){
// 获取通知
val cls = sbn.notification.contentView.javaClass
// 点击事件容器
val field = cls.getDeclaredField("mActions")
field.isAccessible = true
// 点击事件容器对象
val result = field.get(sbn.notification.contentView)
// 强转
(result as? ArrayList<Any>?)?.let { list ->
// 筛选点击事件的实现类集合
// 此处需要判断具体的点击事件
list.filter { item -> item.javaClass.simpleName == "SetOnClickResponse" }.forEach { item ->
// 获取响应对象
val response = item.javaClass.getDeclaredField("mResponse")
response.isAccessible = true
// 强转
(response.get(item) as? RemoteViews.RemoteResponse)?.let { remoteResponse ->
// 获取PendingIntent
val intentField = remoteResponse.javaClass.getDeclaredField("mPendingIntent")
intentField.isAccessible = true
val target = intentField.get(remoteResponse) as PendingIntent
Log.e("NotificationMonitorService","最终目标:${Gson().toJson(target)}")
}

}
}

}

}

WechatIMG892.jpeg
这里的筛选结果有十几个点击事件的响应对象,我们需要做的就是一个一个的去尝试,找到那个目标对象的pendingIntent,通过调用send方法就可以实现模拟点击的效果了!


...
// 获取PendingIntent
val intentField = remoteResponse.javaClass.getDeclaredField("mPendingIntent")
intentField.isAccessible = true
val target = intentField.get(remoteResponse) as PendingIntent
Log.e("NotificationMonitorService","最终目标:${Gson().toJson(target)}")
// 延迟实现点击功能
Handler(Looper.getMainLooper()).postDelayed({
target.send()
},500)

总结




综上,如果第三方应用的通知栏UI是自定义View的话,那么这里的方案是可以直接使用;如果第三方应用的通知栏UI使用的是系统主题,那么按照这个思路应该也可以通过反射实现。
步骤如下:





    1. 接入第三方轮子bypassHiddenApiRestriction(PS:远程依赖的时候使用并未成功,我将项目clone下来打包为aar,导入项目后使用正常!),并初始化:




HiddenApiBypass.startBypass()




    1. AndroidManifest中注册NotificationListenerService,然后启动服务




private fun startService(){
if (NotificationManagerCompat.getEnabledListenerPackages(this).contains(packageName)){
val intent = Intent(this,NotificationMonitorService::class.java)
startService(intent)
}else{
startActivity(Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"))
}

}


  • 3.在NotificationListenerService监听通知栏


override fun onNotificationPosted(sbn: StatusBarNotification?) {
super.onNotificationPosted(sbn)
sbn?:return
if(sbn.packageName == "com.***.******"){
// 获取通知
val cls = sbn.notification.contentView.javaClass
// 点击事件容器
val field = cls.getDeclaredField("mActions")
field.isAccessible = true
// 点击事件容器对象
val result = field.get(sbn.notification.contentView)
// 强转
(result as? ArrayList<Any>?)?.let { list ->
// 筛选点击事件的实现类集合
// 此处需要判断具体的点击事件
list.filter { item -> item.javaClass.simpleName == "SetOnClickResponse" }.first().let { item ->
// 获取响应对象
val response = item.javaClass.getDeclaredField("mResponse")
response.isAccessible = true
// 强转
(response.get(item) as? RemoteViews.RemoteResponse)?.let { remoteResponse ->
// 获取PendingIntent
val intentField = remoteResponse.javaClass.getDeclaredField("mPendingIntent")
intentField.isAccessible = true
val target = intentField.get(remoteResponse) as PendingIntent
Log.e("NotificationMonitorService","最终目标:${Gson().toJson(target)}")
// 延迟实现点击功能
Handler(Looper.getMainLooper()).postDelayed({
target.send()
},500)
}

}
}

}

}

参考:


Android通知监听服务之NotificationListenerService使用篇


另一种绕过Android 9以上非SDK接口调用限制的方法


作者:苏灿烤鱼
来源:juejin.cn/post/7190280650283778106
收起阅读 »

算法(TS):只出现一次的数字

web
给你一个非空整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。 你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。 上题要求时间复杂度为 O(n),空间复杂度为O(1) 解法一...
继续阅读 »

给你一个非空整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。


你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。



上题要求时间复杂度为 O(n),空间复杂度为O(1)



解法一:维护一个 Set 对象


创建一个空的 Set 对象,遍历数组 nums,每遍历出一个 num,便在 Set 对象中查找是否存在它,不存在则加入,存在则删除,等数组遍历结束,Set 对象中剩下的就是只出现一次的数字。


function singleNumber(nums: number[]): number {
const uniqueSet = new Set<number>()
for (const num of nums) {
if (uniqueSet.has(num)) {
uniqueSet.delete(num)
} else {
uniqueSet.add(num)
}
}

return [...uniqueSet][0]
}

存在一次遍历数组,因此时间复杂度为 O(n),uniqueSet.size的最大值为 n/2,最小为 1, 空间复杂度为O(n)


解法二:位运算符(异或)


异或运算符有下面 3 个特性



  1. a ^ 0 = a,即,任何数与数字 0 异或,得到的结果都等于它本身

  2. a ^ a = 0,即,任何数与自身异或,得到的结果都等于 0

  3. a ^ b ^ c = a ^ c ^ b,异或运算符,满足交换率


遍历 nums,让数组中的数字两两异或,最终得到的结果便是数组中只出现一次的数字


function singleNumber(nums: number[]): number {
let uniqueNumber = 0

for (const num of nums) {
uniqueNumber ^= num
}

return uniqueNumber
}

存在一次遍历数组,因此时间复杂度为 O(n),没有额外的中间量空间复杂度为O(1)


作者:何遇er
来源:juejin.cn/post/7298674250965155877
收起阅读 »

人生天地之间,若白驹之过隙,忽然而已

人生天地之间,若白驹之过隙,忽然而已 这段时间有个同事离职了,其实身边不断有老人走、有新人来,但这回走的同事和别的有些不同,当时我入职面试的时候就是他面试的我,工作中有啥问题都会请教他,转眼间一起共事三年多,亦师亦友,忽然离去,心中不免有些不舍。 同事 其实自...
继续阅读 »

人生天地之间,若白驹之过隙,忽然而已


这段时间有个同事离职了,其实身边不断有老人走、有新人来,但这回走的同事和别的有些不同,当时我入职面试的时候就是他面试的我,工作中有啥问题都会请教他,转眼间一起共事三年多,亦师亦友,忽然离去,心中不免有些不舍。


同事


其实自从工作以来,我们每天接触最多的不是家人、朋友,而是同事,毕业几年来也认识了很多同事,有很多乐于助人、兢兢业业的,当然不乏一些泛泛之辈。


对于我个人而言,一般和同事都保持不错的一个状态,但如果问我同事间的关系如何相处,那我肯定告诉你:点头之交即可,不要想着你和同事怎么怎么好,转过头来就有可能把你给卖了,这台正常不过了,除非特别熟,不然尽量别说别的。


朋友


已经很久没和朋友好好聊天,吃个饭了,工作以来虽说是双休,但周末一直有事,即使没事,也只想躺在床上躺会,早上晚起一会,起来弄点饭吃完就中午了,又想再睡会,因为在公司里没有午休的习惯,一觉起来就不早了,出去溜达溜达就到晚上了,时间、时间都去哪了,都用来睡觉、吃饭。。。。


记得高中、大学,认识不少同学,但真正从自己内心算作朋友的,其实少之又少。


高中时同宿舍、同学都在一起玩,我以为他们是我的朋友,所以即使高中毕业了,每次回到家都联系着之前同宿舍的一块出来聚一聚,第一年还出来了,之后不是这个有事就是那个有事,或许是真的有事,亦或许不想和我聚一聚,还或许根本没人把你当成朋友。


大学更是这样,大学认识的朋友屈指可数。


其实有时想想,可能并不是别人的问题,是我自己的问题。但转念想想:”人生天地之间,若白驹之过隙,忽然而已“,想那么多干啥?古人曾说:”人生得一知己足矣“,况且我还有朋友呢!


家人


其实自从上小学开始,和家人在一起的时间就没有多少了,上大学后,一年到头也见不了几次,工作后更甚。


尽力对家人好点吧,多打打电话、多打打视频,现在的技术发达,视频通话可以直接看到对方,非常方便,家人最想要的就是在外漂泊的自己能过得好一点。


瞎写


转眼间毕业工作也好几年了,自我感觉技术上没有特别巨大的提升,但是技术厚度及深度较前几年有了不小的提升,也算是一些收获。


此文因一亦师亦友的大哥离职后产生的一些瞎想,希望大哥在新的城市里顺风顺水,未来更好吧!也希望自己能够不忘初心,在技术上更上一层楼。


努力,共勉


作者:Zhujiang
来源:juejin.cn/post/7265609829091557433
收起阅读 »

Leader:组织面对不确定性的压舱石

今天来跟大家分享一下Leader在一个团队里面到底起着什么样的作用?有一个最为简单的说法,Leader就是把整个组织的所有人通过文化规则和流程组织起来,能够顺利的完成一件或者多件重要的事情的角色。 组织的流程化 我们知道,组织越大事情和人就越多,为了组织好这...
继续阅读 »

今天来跟大家分享一下Leader在一个团队里面到底起着什么样的作用?有一个最为简单的说法,Leader就是把整个组织的所有人通过文化规则和流程组织起来,能够顺利的完成一件或者多件重要的事情的角色。



组织的流程化


我们知道,组织越大事情和人就越多,为了组织好这些人和事,一个组织往往就伴随着有各种各样的标准化流程,又称之为SOP。这个也不难理解,动辄几千上万员工的组织,如果连流程都没有,混乱一团,那可想而知会是一个巨大的灾难,在沟通和协同上面就会耗费大量的成本。


所以相对稳定的公司或者规模越大的公司,它的流程就会越多,这往往导致在做一个事情的时候,进行决策的时间也会更长。流程在一定程度上是解决了很多混乱的问题,但是流程本身也会带来效率低下的问题。但是总体而言,没有流程会造成更大的问题。


那是不是在建立起了流程以后就高枕无忧了呢?如果流程非常完善的情况,Leader又只是把流程做一个管理和编排和组织,那么Leader的价值基本上体现不出来,这种流程极度完善的情况下,那么还要Leader干什么呢?因此我们当代追求的Leader不仅仅是维护和管理这些流程和员工,而应该是企业在对面不确定性事情的法宝。


核心一句话就是Leader是公司面向不确定的事情的压舱石。


面向不确定性的未来


大型组织在流程在规章制度已经比较完善的情况,很多事情员工只要按流程走就可以了,但是我们知道如果一个组织通过流程就能处理所有的事情,那我相信这应该是一个极度老化、极度传统的公司,这种公司已经没有了任何的创造力,所有员工只是机械的去执行公司的命令和流程而已。没有创造性的公司,能面对千变万化的市场吗?


一个不需要公司里面的任何人都有创造力的组织,我相信本身组织就没有创新力。这种组织可能就是非常传统的国有企业,每个人不要有创造力,你只要完成你所要处理的工作就可以了,那这种情况下也只有垄断组织才能做得到,民营企业和国际化的企业是不可能能够实现的,一招鲜吃遍天,在当前这个时代已经不存在了。


所以我认为只有灵活的能够处理不确定的业务,不确定的事情的团队和公司才有成长和逆袭的可能性。因为市场是变化的,客户也是变化的,不可能所有的东西一成不变,用一套服务,一套标准,一套产品就能够解决客户永远的问题的时代早已经一去不复返了。不确定才是当前社会的主旋律,只有一个组织能够尽快的满足变化的需求,才能够拿得出最好最新的产品,才能赢得客户。



那些不确定的事情谁来负责比较合适呢?首先作为整个公司的负责人、一把手、CEO理应是去处理市面上的变化最好的任选,但是不可能所有的变化全部由负责人一个人来处理。这些负责人只能说把握五年,十年的趋势,从这些变化的趋势里面找到确定性的方向来组织和施公司的战略。那不可能所有鸡毛蒜皮,每个分公司,每个分部门,每个普通产品线的业务,如果都要负责人来解决的话,那么这个公司直接在负责人这个层面上面就先崩溃了,比如阿里巴巴而言面对几亿的用户、几千万的商家和合作方、几十万的员工,如果都要负责人亲力亲为,这是完全不现实的。


那能否让一线的员工来处理呢?这个听起来非常美好,每个员工都是公司一份子,理应去接受市场上面的变化,但想想也是不大现实的,因为对于基层一些员工来说,他们往往只有执行的权利,而没有对于很多不确定的情况下的判断的权利。而且很多基层员工由于工作的时间不久,对整个公司的了解也不是特别深,所以往往就算给了他权限,他也没办法能够处理好这些不确定的事情,权限过大反而有可能做出错误的事情。


那公司谁是处理这些不确定的事情里面最好的对象呢?答案就是每一位Leader,也就是每一个层面上不同的Leader,比如财务Leader、业务Leader、销售Leader和技术Leader等等。


一方面公司里面的Leader是整个公司的最核心的节点,他们能够接触到各种各样从上到下的信息,另外由于大量的Leader也是基础一线的负责人,所以往往对于前线的客户需求感知非常的强烈。所以现在的Leader往往是最能够洞察到前线客户需求变化的人,同时也因为Leader一般都有实际执行的权利,通常来说Leader往往在不同规模和不同层面上都有一定的决策权,因此可以快速的组织团队去应对客户需求和市场上的变化。


这个才是Leader最核心的意义,就是要组织小团队去处理这些不确定性的事情和风险,从而给团队,给公司,给业务带来巨大的增量价值。


Leader是不确定性的压舱石


这也是为什么说Leader是整个公司面对不确定性的事情的压舱石,只有整个公司的一线Leader都具备了这样的能力,整个组织才是一个具备有活力的组织,是能够迎接创新和拥抱变化的组织。


这种模式仿佛就是一个动车组一样,每个团队就犹如一起车厢,每个车厢都有自己独特的动力闭环,能够快速的迎接变化,快速的可拆卸组装,而且能够快速的在整个更高层战略的情况下实现更灵活的组合,以应对更大的确定性,以解决更难的问题。


从一些具体的层面上来说,当较大的业务系统发生了比较大的故障的时候,客户有强烈的投诉情绪,这个时候对于一线的开发同学来说,他很难能够确定到底是什么系统引起的,这个时候就需要团队的Leader作为第一业务的接口人来跟进和负责整个事情,从而使得各个团队之间不会相互推诿。所以一旦面对一些不确定性的问题的情况下,在一线同学无法判断问题是什么样根源、什么样的上下游以及什么样的背景的情况下,Leader就是处理问题最好的应急指挥人。


我们的线上有客户提出了大量的需求的时候,我们一些同学往往没有权利去决策是否能支持这个需求。或者有流程可以让一线同学会把这个问题层层上交,一直提交到最高层来审批为止,而往往由于这样流程上的阻碍或者麻烦,所以同学就不愿意去花费这么大的精力向更高层去建议到底要做什么样的东西,大部分员工干脆就不会去提这个建议,直接当这个事情没发生就行了。


有很多非常大的公司往往会倒闭,就是因为适应不了市场上面的变化,当整个市场都发生了很大的变化的时候,这些高层往往感觉不到发生了变化,而很多时候一线的员工往往已经意识到了这些敏锐的变化,但是各种各样的机制,一线员工无法高层传递建议,进而导致了整个公司在面对极大的变化的情况下的话无法应变而轰然倒塌,诺基亚就是一个非常鲜活的例子。


而作为一线的Leader,往往就能够敏锐的去捕获这些信息,而且在整个公司制度流程比较有保障的情况下的话,也能够放权给一线Leader快速做出决策,或者把这些问题快速的反馈给更高层的领导。


比如我在公司的时候就经常去和客户进行共创,就会经常被客户问到我们为什么PC上面还没有定制工作台的能力,而那个时候我们在2018年就有了移动端的能力,但是由于战略方向的问题,我们迟迟没有在PC上面去补足这个短板,进而反复被竞争对手攻击。我接收到了大量的来自于一线客户的反馈,我才做了一个决策帮助整个公司补足了在这一个产品上的短板,进而吸引到大量的增量客户,不仅创造了客户的活跃价值,还创造了商业化的价值。这就是Leader在足够授权下能替组织带来的增量贡献,而远远非整个团队坐等事情发生更有价值。


所以总结一下,各种公司在当今的社会都会面临各种各样的挑战和不确定性。一个公司如何来应对这些挑战和不确定确定性的能力就就决定了这个公司的命运。而面对这些不确定的核心就在于我们组织里面的关键的Leader节点,只有当Leader带领团队主动去拥抱和解决不确定性,才能给组织带来活力,重塑组织的命运。



因此一个公司想要发展的好,就一定要授权给所有听见炮火,两腿带泥的Leader,并且要构建出信任的文化和价值感让Leader带领团队能够以最好的姿态去面对和拥抱不确定性。


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

触摸Android的心脏跳动

在Android开发中,主线程扮演着至关重要的角色。毫不夸张的说,它就相当于Android的心脏。只要它还在跳动的运行,Android应用就不会终止。 它负责处理UI事件、界面更新、以及与用户交互的各种操作。本文将深入分析Android主线程的原理、独特机制以...
继续阅读 »

在Android开发中,主线程扮演着至关重要的角色。毫不夸张的说,它就相当于Android的心脏。只要它还在跳动的运行,Android应用就不会终止。


它负责处理UI事件、界面更新、以及与用户交互的各种操作。本文将深入分析Android主线程的原理、独特机制以及应用,为开发者提供全面的了解和掌握主线程的知识。


主线程的原理


Android应用的核心原则之一是单线程模型,也就是说,大多数与用户界面相关的操作都必须在主线程中执行。这一原则的背后是Android操作系统的设计,主要有以下几个原因:




  • UI一致性:在单线程模型下,UI操作不会被多线程竞争导致的不一致性问题,确保了用户界面的稳定性和一致性。




  • 性能优化:单线程模型简化了线程管理,降低了多线程带来的复杂性,有助于提高应用性能。




  • 安全性:通过将UI操作限制在主线程,可以减少因多线程竞争而引发的潜在问题,如死锁和竞争条件。




主线程的原理可以用以下伪代码表示:


public class MainThread {
public static void main(String[] args) {
// 初始化应用
Application app = createApplication();

// 创建主线程消息循环
Looper.prepareMainLooper();

// 启动主线程
while (true) {
Message msg = Looper.getMainLooper().getNextMessage();
if (msg != null) {
// 处理消息
app.handleMessage(msg);
}
}
}
}

在上述伪代码中,主线程通过消息循环(Message Loop)来不断处理消息,这些消息通常包括UI事件、定时任务等。应用的UI操作都会被封装成消息,然后由主线程依次处理。


主线程的独特机制


主线程有一些独特的机制,其中最重要的是消息队列(Message Queue)和Handler。


消息队列(Message Queue)


消息队列是主线程用来存储待处理消息的数据结构。每个消息都有一个与之相关的Handler,它负责将消息放入队列中,然后由主线程依次处理。消息队列的机制确保了消息的有序性和及时性。


public Message next() {
final long ptr = mPtr;
if (ptr == 0) {
return null;
}

int pendingIdleHandlerCount = -1;
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}

nativePollOnce(ptr, nextPollTimeoutMillis);

synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
msg.markInUse();
return msg;
}
} else {
nextPollTimeoutMillis = -1;
}

...
}

...
}
}

Handler


Handler是一个与特定线程关联的对象,它可以用来发送和处理消息。在主线程中,通常使用new Handler(Looper.getMainLooper())来创建一个与主线程关联的Handler。开发者可以使用Handler来将任务提交到主线程的消息队列中。


Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
// 在主线程执行
}
});

同步屏障


在Android中,消息可以分为同步消息和异步消息。通常,我们发送的消息都是同步消息。
然而,有一种特殊情况,即开启同步屏障。同步屏障是一种消息机制的特性,可以阻止同步消息的处理,只允许异步消息通过。通过调用MessageQueue的postSyncBarrier()方法,可以开启同步屏障。在开启同步屏障后,发送的这条消息它的target为null。


    private int postSyncBarrier(long when) {
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
// 没有设置target,target为null
msg.when = when;
msg.arg1 = token;

Message prev = null;
Message p = mMessages;
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}

那么,开启同步屏障后,所谓的异步消息又是如何被处理的呢?
我们又可以回到之前MessageQueue中的next方法了


public Message next() {
// 省略部分代码,只体现出来同步屏障的代码
...
for (;;) {
...

synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
//注意这里,开始出来同步屏障
//如果target==null,认为它就是屏障,进行循环遍历,直到找到第一个异步的消息
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}

...
}

...
}
}

所以同步屏障是会让消息顺序进行调整,让其忽略现有的同步消息,来直接处理临近的异步消息。
现在听起来已经知道了同步屏障的作用,但它的实际应用又有哪些呢?


应用场景


虽然在日常应用开发中,同步屏障的使用频率较低,但在Android系统源码中,同步屏障的使用场景非常重要。一个典型的使用场景是在UI更新时,例如在View的绘制、布局调整、刷新等操作中,系统会开启同步屏障,以确保与UI相关的异步消息得到优先处理。当UI更新完成后,同步屏障会被移除,允许后续的同步消息得以处理。


对应的是ViewRootImpl#scheduleTraversals()


    void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 设置同步屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

void unscheduleTraversals() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
mChoreographer.removeCallbacks(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}


经典问题


Android 主线程的消息循环是通过 LooperHandler 来实现的。以下是一段伪代码示例:


// 创建一个 Looper,关联当前线程
Looper.prepare();
Looper loop = Looper.myLooper();

// 创建一个 Handler,它将和当前 Looper 关联
Handler handler = new Handler();

// 进入消息循环
Looper.loop();

开启loop后的核心代码如下:


    public static void loop() {
final Looper me = myLooper();
...
for (;;) {
if (!loopOnce(me, ident, thresholdOverride)) {
return;
}
}
}

private static boolean loopOnce(final Looper me,
final long ident, final int thresholdOverride)
{
// 注意没消息会被阻塞,进入休眠状态
Message msg = me.mQueue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return false;
}

...

try {
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} catch (Exception exception) {
if (observer != null) {
observer.dispatchingThrewException(token, msg, exception);
}
throw exception;
} finally {
ThreadLocalWorkSource.restore(origWorkSource);
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
...
msg.recycleUnchecked();

return true;
}


在这段示例中,主线程的消息循环被启动,它会等待来自消息队列的消息。有了这个基础下面的问题就简单了:




  1. 为什么主线程不会陷入无限循环?


    主线程的消息循环不会陷入无限循环,因为它不断地从消息队列中获取消息并处理它们。如果没有消息要处理,消息循环会进入休眠状态,不会持续消耗 CPU 资源。只有在有新消息到达时,主线程才会被唤醒来处理这些消息。这个机制确保主线程能够响应用户的操作,而不陷入死循环。




  2. 如果没有消息,主线程会如何处理?


    如果消息队列为空,主线程的消息循环会等待,直到有新消息到达。在等待期间,它不会执行任何操作,也不会陷入循环。这是因为 Android 的消息循环是基于事件驱动的,只有当有事件(消息)到达时,才会触发主线程执行相应的处理代码。当新消息被投递到消息队列后,主线程会被唤醒,执行相应的处理操作,然后再次进入等待状态。




这种事件驱动的消息循环机制使得 Android 应用能够高效地管理用户交互和异步操作,同时保持了响应性和低能耗。所以,主线程不会陷入无限循环,而是在需要处理事件时才会执行相应的代码。


结论


Android主线程是应用的核心,负责处理UI事件、界面更新和定时任务等。了解主线程的原理和独特机制是Android开发的关键,它有助于确保应用的稳定性和性能。通过消息队列和Handler,开发者可以在主线程中安全地处理各种任务,提供流畅的用户体验。


推荐


android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。


AwesomeGithub: 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。


flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。


android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。


daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。


作者:午后一小憩
来源:juejin.cn/post/7296692742876758027
收起阅读 »

触摸Android的心脏跳动

在Android开发中,主线程扮演着至关重要的角色。毫不夸张的说,它就相当于Android的心脏。只要它还在跳动的运行,Android应用就不会终止。 它负责处理UI事件、界面更新、以及与用户交互的各种操作。本文将深入分析Android主线程的原理、独特机制以...
继续阅读 »

在Android开发中,主线程扮演着至关重要的角色。毫不夸张的说,它就相当于Android的心脏。只要它还在跳动的运行,Android应用就不会终止。


它负责处理UI事件、界面更新、以及与用户交互的各种操作。本文将深入分析Android主线程的原理、独特机制以及应用,为开发者提供全面的了解和掌握主线程的知识。


主线程的原理


Android应用的核心原则之一是单线程模型,也就是说,大多数与用户界面相关的操作都必须在主线程中执行。这一原则的背后是Android操作系统的设计,主要有以下几个原因:




  • UI一致性:在单线程模型下,UI操作不会被多线程竞争导致的不一致性问题,确保了用户界面的稳定性和一致性。




  • 性能优化:单线程模型简化了线程管理,降低了多线程带来的复杂性,有助于提高应用性能。




  • 安全性:通过将UI操作限制在主线程,可以减少因多线程竞争而引发的潜在问题,如死锁和竞争条件。




主线程的原理可以用以下伪代码表示:


public class MainThread {
public static void main(String[] args) {
// 初始化应用
Application app = createApplication();

// 创建主线程消息循环
Looper.prepareMainLooper();

// 启动主线程
while (true) {
Message msg = Looper.getMainLooper().getNextMessage();
if (msg != null) {
// 处理消息
app.handleMessage(msg);
}
}
}
}

在上述伪代码中,主线程通过消息循环(Message Loop)来不断处理消息,这些消息通常包括UI事件、定时任务等。应用的UI操作都会被封装成消息,然后由主线程依次处理。


主线程的独特机制


主线程有一些独特的机制,其中最重要的是消息队列(Message Queue)和Handler。


消息队列(Message Queue)


消息队列是主线程用来存储待处理消息的数据结构。每个消息都有一个与之相关的Handler,它负责将消息放入队列中,然后由主线程依次处理。消息队列的机制确保了消息的有序性和及时性。


public Message next() {
final long ptr = mPtr;
if (ptr == 0) {
return null;
}

int pendingIdleHandlerCount = -1;
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}

nativePollOnce(ptr, nextPollTimeoutMillis);

synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
msg.markInUse();
return msg;
}
} else {
nextPollTimeoutMillis = -1;
}

...
}

...
}
}

Handler


Handler是一个与特定线程关联的对象,它可以用来发送和处理消息。在主线程中,通常使用new Handler(Looper.getMainLooper())来创建一个与主线程关联的Handler。开发者可以使用Handler来将任务提交到主线程的消息队列中。


Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
// 在主线程执行
}
});

同步屏障


在Android中,消息可以分为同步消息和异步消息。通常,我们发送的消息都是同步消息。
然而,有一种特殊情况,即开启同步屏障。同步屏障是一种消息机制的特性,可以阻止同步消息的处理,只允许异步消息通过。通过调用MessageQueue的postSyncBarrier()方法,可以开启同步屏障。在开启同步屏障后,发送的这条消息它的target为null。


    private int postSyncBarrier(long when) {
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
// 没有设置target,target为null
msg.when = when;
msg.arg1 = token;

Message prev = null;
Message p = mMessages;
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}

那么,开启同步屏障后,所谓的异步消息又是如何被处理的呢?
我们又可以回到之前MessageQueue中的next方法了


public Message next() {
// 省略部分代码,只体现出来同步屏障的代码
...
for (;;) {
...

synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
//注意这里,开始出来同步屏障
//如果target==null,认为它就是屏障,进行循环遍历,直到找到第一个异步的消息
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}

...
}

...
}
}

所以同步屏障是会让消息顺序进行调整,让其忽略现有的同步消息,来直接处理临近的异步消息。
现在听起来已经知道了同步屏障的作用,但它的实际应用又有哪些呢?


应用场景


虽然在日常应用开发中,同步屏障的使用频率较低,但在Android系统源码中,同步屏障的使用场景非常重要。一个典型的使用场景是在UI更新时,例如在View的绘制、布局调整、刷新等操作中,系统会开启同步屏障,以确保与UI相关的异步消息得到优先处理。当UI更新完成后,同步屏障会被移除,允许后续的同步消息得以处理。


对应的是ViewRootImpl#scheduleTraversals()


    void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 设置同步屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

void unscheduleTraversals() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
mChoreographer.removeCallbacks(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}


经典问题


Android 主线程的消息循环是通过 LooperHandler 来实现的。以下是一段伪代码示例:


// 创建一个 Looper,关联当前线程
Looper.prepare();
Looper loop = Looper.myLooper();

// 创建一个 Handler,它将和当前 Looper 关联
Handler handler = new Handler();

// 进入消息循环
Looper.loop();

开启loop后的核心代码如下:


    public static void loop() {
final Looper me = myLooper();
...
for (;;) {
if (!loopOnce(me, ident, thresholdOverride)) {
return;
}
}
}

private static boolean loopOnce(final Looper me,
final long ident, final int thresholdOverride)
{
// 注意没消息会被阻塞,进入休眠状态
Message msg = me.mQueue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return false;
}

...

try {
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} catch (Exception exception) {
if (observer != null) {
observer.dispatchingThrewException(token, msg, exception);
}
throw exception;
} finally {
ThreadLocalWorkSource.restore(origWorkSource);
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
...
msg.recycleUnchecked();

return true;
}


在这段示例中,主线程的消息循环被启动,它会等待来自消息队列的消息。有了这个基础下面的问题就简单了:




  1. 为什么主线程不会陷入无限循环?


    主线程的消息循环不会陷入无限循环,因为它不断地从消息队列中获取消息并处理它们。如果没有消息要处理,消息循环会进入休眠状态,不会持续消耗 CPU 资源。只有在有新消息到达时,主线程才会被唤醒来处理这些消息。这个机制确保主线程能够响应用户的操作,而不陷入死循环。




  2. 如果没有消息,主线程会如何处理?


    如果消息队列为空,主线程的消息循环会等待,直到有新消息到达。在等待期间,它不会执行任何操作,也不会陷入循环。这是因为 Android 的消息循环是基于事件驱动的,只有当有事件(消息)到达时,才会触发主线程执行相应的处理代码。当新消息被投递到消息队列后,主线程会被唤醒,执行相应的处理操作,然后再次进入等待状态。




这种事件驱动的消息循环机制使得 Android 应用能够高效地管理用户交互和异步操作,同时保持了响应性和低能耗。所以,主线程不会陷入无限循环,而是在需要处理事件时才会执行相应的代码。


结论


Android主线程是应用的核心,负责处理UI事件、界面更新和定时任务等。了解主线程的原理和独特机制是Android开发的关键,它有助于确保应用的稳定性和性能。通过消息队列和Handler,开发者可以在主线程中安全地处理各种任务,提供流畅的用户体验。


推荐


android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。


AwesomeGithub: 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。


flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。


android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。


daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。


作者:午后一小憩
来源:juejin.cn/post/7296692742876758027
收起阅读 »

利用腾讯地图实现地图选点功能

web
基于腾讯地图组件实现地图选点功能 使用到了腾讯地图官提供的组件,实现了地图选点 <template> <iframe id="mapPage" width="100%" height="100%" frameborder="0" src...
继续阅读 »

基于腾讯地图组件实现地图选点功能


使用到了腾讯地图官提供的组件,实现了地图选点


image.png


<template>
<iframe id="mapPage" width="100%" height="100%" frameborder="0" src="https://apis.map.qq.com/tools/locpicker?search=1&type=1&key=你自己申请的KEY&referer=myapp"></iframe>
</template>

<script setup>
import { ref } from 'vue'

const key = '自己申请到的Key'

window.addEventListener(
'message',
function (event) {
// 接收位置信息,用户选择确认位置点后选点组件会触发该事件,回传用户的位置信息
var loc = event.data
if (loc && loc.module == 'locationPicker') {
//防止其他应用也会向该页面post信息,需判断module是否为'locationPicker'
// loc 这里面存放详细的位置信息
emit('addressInfo', loc)
}
},
true
)
</script>

<style lang="scss">
// 样式自己去修改,可以使用到样式渗透
:deep(.map-wrap) {
height: 60%;
}
</style>

我是将这个代码封装成了组件,在使用的地方直接调用就可以.


其中:


window.addEventListener(
'message',
function (event) {
// 接收位置信息,用户选择确认位置点后选点组件会触发该事件,回传用户的位置信息
var loc = event.data
if (loc && loc.module == 'locationPicker') {
//防止其他应用也会向该页面post信息,需判断module是否为'locationPicker'
// loc 这里面存放详细的位置信息
emit('addressInfo', loc)
}
},
true
)

当这段代码被执行时,它会添加一个事件监听器,用于监听浏览器窗口的message事件。
第一个参数是要监听的事件类型,这里是message,表示监听来自窗口的消息事件。


第二个参数是一个回调函数,当message事件被触发时,回调函数会被执行。


在回调函数中,它首先通过event.data获取传递过来的数据,并将其保存在一个变量loc中。


接下来,通过判断loc对象中的module属性是否为locationPicker来确定这个消息是否来自选点组件。这样做的目的是为了避免处理来自其他应用程序的消息。


如果条件满足,即该消息确实来自选点组件,则会触发一个自定义的事件addressInfo,并将loc对象作为参数传递给该事件。这可以通过一个emit函数来实现,该函数的作用是触发指定名称的事件,并传递相关的数据。这样其他部分的代码就可以订阅并处理addressInfo事件,从而获取位置信息并执行相应的逻辑。


当你在地图选点后点击下面的信息就能看到对应的数据了。


image.png


基于腾讯地图实现地图选点功能(手写)


这是最终实现的效果:


image.png
有时候的腾讯地图组件的选点功能会稳定,或者失效,显示列表更新失败这就导致可能用户使用感受较差,有时候就必须手写一份,下面的代码是手写的代码以及对应的代码说明:👇👇👇👇


1.首先你需要在项目的html文件引入腾讯地图(vue3的项目)


image.png


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script charset="utf-8" src="https://map.qq.com/api/js?v=2.exp&key=这里是你申请的key"></script>
</body>
</html>


  1. 将地图选点功能封装成组件,实现代码的高复用。


<template>
<div>
<div class="container" id="container">
<img class="coordinate" src="@/assets/坐标.png" alt="图片加载失败" />
</div>
</div>
</template>

<script setup>
import { onMounted, reactive } from 'vue'
import axios from 'axios'

//地图
const dataMap = reactive({
map: '',
markerLayer: '',
latitude: '', //纬度
lngitude: '', //经度
})

onMounted(() => {
getlocation() //获取经纬度
initMap() // 初始化地图
})

//初始化地图
const initMap = () => {
dataMap.map = new qq.maps.Map(document.getElementById('container'), {
center: new qq.maps.LatLng(45.190524, 124.797766), //设置地图中心点坐标
zoom: 20, //设置地图缩放级别
})
qq.maps.event.addListener(dataMap.map, 'center_changed', center_changed)
}
// 监听地图拖动获取经纬度
const center_changed = () => {
dataMap.latitude = dataMap.map.getCenter().lat
dataMap.lngitude = dataMap.map.getCenter().lng
console.log('选点后的经纬度:', dataMap.latitude, dataMap.lngitude)
}

//获取经纬度
const getlocation = async () => {
const res = await axios.get('/api', {
params: {
key: '自己申请的key',
},
})
dataMap.map.setCenter(new qq.maps.LatLng(res.data.result.location.lat, res.data.result.location.lng))
}
</script>

<style lang="scss" scoped>
.container {
box-sizing: border-box;
margin: 50px;
width: 800px;
height: 400px;
border: 1px solid #999;

.coordinate {
z-index: 9999;
position: relative;
top: 50%;
left: 50%;
}
}
</style>


  1. 其中中心点的图片是自己设置上的,下面给提供了这个图片的地址,大家可以下载使用


坐标.png
图片地址:img1.imgtp.com/2023/11/08/…


中心点的位置是根据定位设置的,如果大家使用的容器的宽度和高度和我的不一样,需要手动的设置。


作者:LuHang
来源:juejin.cn/post/7298361908443463734
收起阅读 »

Android通知栏增加快捷开关的技术实现

我们通常可以在通知栏上看到“飞行模式”、“移动数据”、“屏幕录制”等开关按钮,这些按钮都属于通知栏上的快捷开关,点击快捷开关可以轻易调用某种系统能力或打开某个应用程序的特定页面。那是否可以在通知栏上自定义一个快捷开关呢?答案是可以的,具体是通过TileServ...
继续阅读 »

我们通常可以在通知栏上看到“飞行模式”、“移动数据”、“屏幕录制”等开关按钮,这些按钮都属于通知栏上的快捷开关,点击快捷开关可以轻易调用某种系统能力或打开某个应用程序的特定页面。那是否可以在通知栏上自定义一个快捷开关呢?答案是可以的,具体是通过TileService的方案实现。   


TileService继承自Service,所以它也是Android的四大组件之一,不过它是一个特殊的组件,开发者不需要手动开启调用,系统可以自动识别并完成调用,系统会通过绑定服务(bindService)的方式调用。


创建使用:


快捷开关是Android 7(target 24)的新能力,因此在使用该能力前必须先判断版本大小(大于等于target 24)。


1、自定义一个TileService类。


class MyQSTileService: TileService() {
  override fun onTileAdded() {    
super.onTileAdded()
}

  override fun onStartListening() {    
super.onStartListening()
}

  override fun onStopListening() {    
super.onStopListening()
}

  override fun onClick() {    
super.onClick()
}

  override fun onTileRemoved() {    
super.onTileRemoved()
}
}

TileService是通过绑定服务(bindService)的方式被调用的,因此,绑定服务生命周期包含的四种典型的回调方法(onCreate()、onBind()、onUnbind()和 onDestroy())都会被调用。但是,TileService也包含了以下特殊的生命周期回调方法:



  • onTileAdded():当用户从编辑栏添加快捷开关到通知栏的快速设置中会调用。

  • onTileRemoved():当用户从通知栏的快速设置移除快捷开关时调用。

  • onClick():当用户点击快捷开关时调用。

  • onStartListening():当用户打开通知栏的快速设置时调用。当快捷开关并没有从编辑栏拖到设置栏中不会调用。在TileAdded添加之后会调用一次。

  • onStopListening():当用户打开通知栏的快速设置时调用。当快捷开关并没有从编辑栏拖到设置栏中不会调用。在TileRemoved移除之前会调用一次。


2、在应用程序的清单文件中声明TileService


name=".MyQSTileService"
android:label="@string/my_default_tile_label"
android:icon="@drawable/my_default_icon_label"
android:exported="true"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">

name="android.service.quicksettings.action.QS_TILE" />




  • name:自定义的TileService的类名。

  • label:快捷开关在通知栏上显示的名称。

  • icon:快捷开关在通知栏上显示的图标。

  • exported:该服务能否被外部应用调用。该属性必须为true。如果为false,那么快捷开关的功能将失效,原因是exported="false"时,TileService将不支持外部应用调起,手机系统自然不能再和该快捷开关交互。必须配置。

  • permission:需要给service配置的权限,BIND_QUICK_SETTINGS_TILE即允许应用程序绑定到第三方快速设置。必须配置。

  • intent-filter:意图过滤器,只有匹配内部的action,才能调起该service。必须配置。


监听模式


TileService的监听模式(或理解为启动模式)有两种,一种是主动模式,另一种是标准模式。



  • 主动模式


在主动模式下,TileService被请求时该服务会被绑定,并且TileService的onStartListening也会被调用。该模式需要在AndroidManifeast清单文件中声明:



name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
...


通过TileService.requestListeningState()这一静态方法,就可以实现对TileService的请求,示例如下:


      TileService.requestListeningState(
applicationContext, ComponentName(
BuildConfig.APPLICATION_ID,
MyQSTileService::class.java.name
)
)

主动模式下值得注意的是:



  • 用户在通知栏快速设置的地方点击快捷开关时,TileService会自动完成绑定、TileService的onStartListening会被调用。

  • TileService无论是通过点击被绑定还是通过requestListeningState请求被绑定,TileService所在的进程都会被调起。


标准模式


     在标准模式下,TileService可见时(即用户下拉通知栏看见快捷开关)该服务会被绑定,并且TileService的onStartListening也会被调用。标准模式不需要在AndroidManifeast清单文件中进行额外的声明,默认就是标准模式。


标准模式下值得注意的是:



  • 和主动模式相同,TileService被绑定时,TileService所在的进程就会被调起。

  • 而和主动模式不同的是,标准模式绑定TileService是通过用户下拉通知栏实现的,这意味着TileService所在的进程会被多次调起。因此为了避免主进程被频繁调起、避免DAU等数据统计受到影响,我们还需要为TileService指定一个特定的子进程,在Androidmanifest清单文件中设置:


      process="自定义子进程的名称">
......


更新快捷开关


如果需要对快捷开关的数据进行更新,可以通过getQsTile()获取快捷开关的对象,然后通过setIcon(更新icon)、setLable(更新名称)、setState(更新状态,包括STATE_ACTIVE——表示开启或启用状态、STATE_INACTIVE——表示关闭或暂停状态、STATE_UNAVAILABLE:表示暂时不可用状态,在此状态下,用户无法与您的磁贴交互)等方法设置快捷开关新的数据,最后调用updateTile()方法实现。


  override fun onStartListening() {
super.onStartListening()
if (qsTile.state === Tile.STATE_ACTIVE) {
qsTile.label = "inactive"
qsTile.icon = Icon.createWithResource(context, R.drawable.inactive)
qsTile.state = Tile.STATE_INACTIVE
} else {
qsTile.label = "active"
qsTile.icon = Icon.createWithResource(context, R.drawable.active)
qsTile.state = Tile.STATE_ACTIVE
}
qsTile.updateTile()
}

操作快捷开关



  • 如果想要实现点击快捷开关时、关闭通知栏并跳转到某个页面,可以调用以下方法:


startActivityAndCollapse(Intent intent)


  • 如果想要在点击快捷开关时弹出对话框进行交互,可以调用以下方法:


override fun onClick() {
super.onClick()
if(!isLocked()) {
showDialog()
}
}

因为快捷开关有可能在用户锁屏时出现,所以必须加上isLocked()的判断。只有非锁屏的情况下,对话框才会出现。



  • 如果快捷开关含有敏感信息,需要使用isSecure()进行设备安全性判断,当设备安全时,才能执行快捷开关相关的逻辑(如点击的逻辑)。当设备不安全时(手机处于锁屏状态时),可调用unlockAndRun(Runnable runnable),提示用户解锁屏幕并执行自定义的runnable操作。


以上是通知栏增加快捷开关的全部介绍。


作者:度熊君
来源:juejin.cn/post/7190663063631036473
收起阅读 »

安卓知识点-应届生扫盲安卓WebView

作者 大家好,我叫Jack冯; 本人20年硕士毕业于广东工业大学,于2020年6月加入37手游安卓团队; 目前主要负责海外游戏发行安卓相关开发。 背景 最近在接触活动相关需求,其中涉及到一个安卓的WebView; 刚毕业的我,对安卓知识积累比较少,所以在这里对...
继续阅读 »

作者


大家好,我叫Jack冯;


本人20年硕士毕业于广东工业大学,于2020年6月加入37手游安卓团队;


目前主要负责海外游戏发行安卓相关开发。


背景


最近在接触活动相关需求,其中涉及到一个安卓的WebView;


刚毕业的我,对安卓知识积累比较少,所以在这里对Webview进行相关学习,希望自己可以在安卓方面逐步积累。


Webview介绍


1、关于MockView


( 1 ) 在targetSdkVersion 28/29的工程里面查看WebView继承关系


java.lang.Object
↳ android.view.View
↳ android.view.ViewGr0up
​ ↳ android.widget.FrameLayout
↳ android.layoutlib.bridge.MockView
↳ android.webkit.WebView

( 2 ) 使用26/27等低版本SDK,查看源码中的WebView 继承关系


java.lang.Object
↳ android.view.View
↳ android.view.ViewGr0up
↳ android.widget.AbsoluteLayout
↳ android.webkit.WebView

( 3 )对比


两种方式对比,AbsoluteLayout和FrameLayout都是重写ViewGr0up的方法,如与布局参数配置相关的 generateDefaultLayoutParams()、checkLayoutParams()等。两种方式明显不同的是多了一层MockView 。这里来看看MockView是什么:


public class MockView extends FrameLayout{
...
//创建方式
public MockView(Context context) {...}
public MockView(Context context,AttributeSet attrs) {...}
public MockView(Context context,AttributeSet attrs,int defStyleRes) {...}
//重写添加view方法
@Override
public void addView(View child){...}
@Override
public void addView(View child,int index){...}
@Override
public void addView(View child,int width,int height){...}
@Override
public void addView(View child,ViewGr0up.LayoutParams params){...}
@Override
public void addView(View child,int index,ViewGr0up.LayoutParams params){...}
public void setText(CharSequence text){...}
public void setGravity(int gravity){...}
}

MockView,译为"虚假的view"。


谷歌发布的Sdk其实只是为了提供App开发运行接口,实际运行时候替换为当前系统的Sdk。


具体说就是当谷歌在新的系统(Framework)版本上准备对WebView实现机制进行改动,同时又希望把新的sdk提前发出来,不影响用到WebView的App开发,于是谷歌提供给Android开发的sdk中让WebView继承自MockView,这个WebView只是暴露了接口,没有具体实现;这样当谷歌关于WebView新的实现做好,利用WebView,app也就做好了


2、基本使用


(1)创建


①一般方式:


WebView webView = findViewById(R.id.webview);

②建议方式:


LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGr0up.LayoutParams.MATCH_PARENT,ViewGr0up.LayoutParams.MATCH_PARENT);
mWebView = new WebView(getApplicationContext());
mWebView.setLayoutParams(params);

好处:构建不用依赖本地xml文件,自定义页面参数;手动销毁避免内存泄露;


③更多方式 : 继承Webview和主要API等进行拓展


public class BaseWebView extends WebView {...}
public class BaseWebClient extends WebClient {...}
public class BaseWebChromeClient extends WebChromeClient {...}

(2)加载


① 加载某个网页


webView.loadUrl("http://www.google.com/");

②新建assets目录,将html文件放到目录下,通过路径加载本地页面


 webView.loadUrl("file:///android_asset/loadFailed.html");

③使用evaluateJavascript(String script, ValueCallback resultCallback)方法加载,(Android4.4+)


mWebView.evaluateJavascript("file:///android_asset/javascript.html",new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
Log.e("测试", "onReceiveValue:"+value );
}
});

3、WebViewClient


当URL即将加载到当前窗口,如果没有提供WebViewClient,默认情况下WebView将使用Activity管理器为URL选择适当的处理器。


如果提供了WebViewClient,按自定义配置要求来继续加载URL。


(1)常用方法


//加载过程对url的处理(webview加载、系统浏览器加载、其他操作等)
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
super.shouldOverrideUrlLoading(view, url);
}
//加载失败页面
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl){
view.loadUrl("file:///android_asset/js_error.html");
}
//证书错误处理
@Override
public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) {
}
//开始加载页面(可自定义页面加载计时等)
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
Log.e(TAG, "onPageStarted:" + url);
}
//结束加载页面
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Log.e(TAG, "onPageFinished: " + url);
}

(2)关于shouldOverrideUrlLoading


如果在点击链接加载过程需要更多的控制,就可以在WebViewClient()中重写shouldOverrideUrlLoading()方法。


涉及shouldOverrideUrlLoading()的情形,大概分为三种:


(1)没有设定setWebViewClient(),点击链接使用默认浏览器打开;


(2)设定setWebViewClient(new WebViewClient()),默认shouldOverrideUrlLoading()返回false,点击链接在Webview加载;


(3)设定、重写shouldOverrideUrlLoading()


返回true:可由应用代码处理该 url,WebView 中止处理(若重写方法没加上view.loadUrl(url),不加载);


返回false:由 WebView 处理加载该 url。(即使没加上view.loadUrl(url),也会在当前Webview加载)


【一般应用】


@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
super.shouldOverrideUrlLoading(view, url);
if (url != null) {
if (!(url.startsWith("http") || url.startsWith("https"))) {
return true;
}
//重定向到别的页面
//view.loadUrl("file:///android_asset/javascript.html");
//区别不同链接加载
view.loadUrl(url);
}
return true;
}

(3)常见误区


【误区1】 : 需要重写 shouldOverrideUrlLoading 方法才能阻止浏览器打开页面。


解释:WebViewClient 源码中 shouldOverrideUrlLoading 方法已经返回 false,不设定setWebViewClient(),默认使用系统浏览器加载。如果重写该方法并返回true, 就可以实现在app页面中加载新链接而不去打开浏览器。


【误区2】 : 每一个url加载过程都会经过 shouldOverrideUrlLoading 方法。


Q1:加载一定会触发shouldOverrideUrlLoading?


Q2:触发时机一定在onPageStarted调用之前?


解释:关于shouldOverrideUrlLoading的触发


1)如果在点击页面链接时通过标签跳转,触发方法如下:


​ shouldOverrideUrlLoading() —> onPageStarted()—> onPageFinished()


2)如果使用loadUrl加载时,触发方法如下:


​ onPageStarted()—>onPageFinished()


3)如果使用loadUrl加载重定向地址时,触发方法如下:


​ shouldOverrideUrlLoadings—>onPageStarted —> onPageFinished


ps:多次重定向的过程,


onPage1Started


—>shouldOverrideUrlLoadings


—>onPage2Started —> xxx...


—> onPageNFinished


结论:shouldOverrideUrlLoading()方法不是每次加载都会调用,WebView的前进、后退等不会调用shouldOverrideUrlLoading方法;非loadUrl方式加载 或者 是重定向的,才会调用shouldOverrideUrlLoading方法。


【误区3 】: 重写 shouldOverrideUrlLoading 方法返回true比false的区别,多调用一次onPageStarted()和onPageFinished()。


解释:返回True:应用代码处理url;返回False,则由 WebView 处理加载 url。


ps:低版本系统(华为6.0),测试 False比True会多调用一次onPageStarted()和onPageFinished(),这点还在求证中。


4、WebChromeClient


对比WebviewClient , 添加了处理JavaScript对话框,图标,标题和进度等。


处理对象 : 影响浏览器的事件


(1)常用方法:


//alert弹出框
public boolean onJsAlert(WebView view, String url, String message,JsResult result){
return true;//true表示拦截
}

//confirm弹出框
public boolean onJsConfirm(WebView view, String url, String message,JsResult result){
return false;//false则允许弹出
}

public boolean onJsPrompt(WebView view, String url, String message,String defaultValue, JsPromptResult result)

//打印 console 信息。return true只显示log,不显示js控制台的输入;false则都显示出来
public boolean onConsoleMessage(ConsoleMessage consoleMessage){
Log.e("测试", "consoleMessage:"+consoleMessage.message());
}

//通知程序当前页面加载进度,结合ProgressBar显示
public void onProgressChanged(WebView view, int newProgress){
if (newProgress < 100) {
String progress = newProgress + "%";
Log.e("测试", "加载进度:"+progress);
webProgress.setProgress(newProgress);
}
}

(2)拦截示例:


JsResult.comfirm() --> 确定按钮的调用方法


JsResult.cancle() --> 取消按钮


示例:拦截H5的弹框,并显示自定义弹框,点击按钮后重定向页面到别的url


@Override
public boolean onJsConfirm(final WebView view, String url, String message, final JsResult result) {
Log.e("测试", "onJsConfirm:"+url+",message:"+message+",jsResult:"+result.toString());
new AlertDialog.Builder(chromeContext)
.setTitle("拦截JsConfirm显示!")
.setMessage(message)
.setPositiveButton(android.R.string.ok,
new AlertDialog.OnClickListener() {
public void onClick(DialogInterface dialog,int which) {
//重定向页面
view.loadUrl("file:///android_asset/javascript.html");
result.confirm();
}
}).setCancelable(false).create().show();
return true;
}

5、WebSettings


用于页面状态设置\插件支持等配置.


(1)常用方法


WebSettings webSettings = webView.getSettings();
/**
* 设置缓存模式、支持Js调用、缩放按钮、访问文件等
*/

webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
webSettings.setJavaScriptEnabled(true);
webSettings.setSupportZoom(true);
webSettings.setBuiltInZoomControls(true);
webSettings.setDisplayZoomControls(true);

//允许WebView使用File协议,访问本地私有目录的文件
webSettings.setAllowFileAccess(true);

//允许通过file url加载的JS页面读取本地文件
webSettings.setAllowFileAccessFromFileURLs(true);

//允许通过file url加载的JS页面可以访问其他来源内容,包括其他的文件和http,https等来源
webSettings.setAllowUniversalAccessFromFileURLs(true);
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
webSettings.setLoadsImagesAutomatically(true);
webSettings.setDefaultTextEncodingName("utf-8")

if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}

结束语


过程中有问题或者需要交流的同学,可以扫描二维码加好友,然后进群进行问题和技术的交流等;


企业微信截图_5d79a123-2e31-42cc-b03f-9312b8b99df3.png


作者:37手游移动客户端团队
来源:juejin.cn/post/7245084484756144186
收起阅读 »

马斯克接手Twitter一年后的成果-工作量化的重要性

马斯克接手Twitter的一年后,在10.27其官方团队发布了一条推文展示这一年的工程成果。 有点国内那味了,论工作量化的重要性。 这一年里,我们在工程技术上取得了许多出色的成就,除了大家在应用中看到的明显变化之外,在幕后我们还做了一系列重要的优化和改进。...
继续阅读 »

马斯克接手Twitter的一年后,在10.27其官方团队发布了一条推文展示这一年的工程成果。



有点国内那味了,论工作量化的重要性。



这一年里,我们在工程技术上取得了许多出色的成就,除了大家在应用中看到的明显变化之外,在幕后我们还做了一系列重要的优化和改进。




  • 将「为你推荐」、「关注」、「搜索」、「个人主页」、「列表」、「社区」和「探索」等功能的技术栈整合到了一个统一的产品框架中。




  • 彻底重建了「为你推荐」的服务和排名系统,代码行数从700K减少到70k,减少了90%;同时计算资源减少了50%,处理相同请求的能力提升了80%。




  • 统一了「为你推荐」和视频的个性化、排名模型,显著提高了视频推荐质量。




  • 重构了API中间层的架构,删除了超过10万行代码和数千个未使用的内部废弃接口,同时删除了一些没人用的客户端服务。




  • 将获取帖子元数据的时间减半,全局API超时错误减少90%。




  • 对外部机器人、爬虫的屏蔽,相比2022年,增长了37%。平均每天,阻止了超过100万次机器人注册,并将私信中的无用信息减少了95%。




  • 关闭了位于萨克拉门托的数据中心,重新调配了5200台机架和148000台服务器,每年为公司节约了超1亿美元,总的来说,节约了48兆瓦的电量,60000磅的网络机架。




  • 优化了对云服务厂商的使用,开始在本地进行更多的工作,这一转变使得每月的云服务成本降低60%,同时我们还将所有的媒体和大文件从云端迁出,减少了60%的云端存储空间,除此之外,还成功将云数据处理成本减少了75%。




  • 构建本地GPU超级计算集群,并设计、交付了43.2Tbps 的高性能网络架构。




  • 提升网络主干的容量和冗余性,每年节省1390万美元。




  • 开展了自动化峰值流量故障转移测试,以持续验证平台的可扩展性和可用性。




作者:云舒编程
来源:juejin.cn/post/7295397683397066762
收起阅读 »

近三个月的排错,原来的憧憬消失喽

web
作为应届生,带着无限憧憬来到这里,但是经过这三个月的经历,发现只有无限的消耗,并没有任何想要留下的感觉,每天携着自己早已不属于自己的脑袋来到早已不想来的地方... 动效逻辑实现 将元素布局设置好,调整元素的一个动态css属性让其置于可视视图以外,使用动效类库,...
继续阅读 »

作为应届生,带着无限憧憬来到这里,但是经过这三个月的经历,发现只有无限的消耗,并没有任何想要留下的感觉,每天携着自己早已不属于自己的脑袋来到早已不想来的地方...


动效逻辑实现


将元素布局设置好,调整元素的一个动态css属性让其置于可视视图以外,使用动效类库,去改变他的css属性让其还原回正确的位置。


动效类库




  • ScrollTrigger




  • ScrollMagic



    • ScrollMagic 是一个 JavaScript 库,用于在滚动事件上创建视差滚动和其他滚动效果。

    • ScrollMagic 允许您在页面滚动时触发动画,例如根据滚动位置触发动画、控制元素的可见性、触发事件等。

    • 您可以使用 ScrollMagic 来创建交互式滚动体验,例如滚动到特定部分时触发动画效果。

    • ScrollMagic 可以与 TweenMax 或其他动画库一起使用,以创建复杂的滚动动画。




  • TweenMax



    • TweenMax 是 GreenSock Animation Platform (GSAP) 库的一部分。GSAP 是一个用于创建高性能动画的 JavaScript 库,提供了丰富的动画功能。

    • TweenMax 是 GSAP 的核心库之一,它用于创建各种动画,包括基本的属性动画,缓动动画,时间轴动画等。

    • TweenMax 提供了灵活且易于使用的 API,允许您创建复杂的动画效果,如淡入淡出、缩放、旋转、移动等。

    • 您可以单独使用 TweenMax 来创建动画,或与其他库和插件一起使用,以实现更高级的效果。




  • animation.gsap.js



    • animation.gsap.js 是 ScrollMagic 的插件,它允许您在 ScrollMagic 中使用 GSAP(包括 TweenMax)来控制动画。

    • 此插件通过将 GSAP 和 ScrollMagic 集成,使您能够在滚动事件中触发和控制 TweenMax 动画。

    • 使用 animation.gsap.js,您可以创建更具交互性的滚动动画,将滚动事件与强大的 TweenMax 动画引擎结合使用,实现更丰富的效果。




综上所述,TweenMax 是 GSAP 库的一部分,用于创建各种动画。ScrollMagic 是一个独立的库,用于处理滚动事件和创建滚动效果。animation.gsap.js 是 ScrollMagic 的插件,它使 ScrollMagic 能够与 GSAP(包括 TweenMax)一起使用,以在滚动事件中创建动画效果。这些库和插件可以协同工作,以创建引人入胜的交互式网页效果。


ScrollMagic很久没有维护了。
image.png


浏览器跨页面通信


前几天有这样一个需求,当我们在当前页面点击编辑时,我们跳转到编辑页面,编辑完成后,我们需要刷新当前页面并关闭编辑页面。这就需要用到跨页面通信功能了。


image.png


image.png


下面总结一下前端中实现在一个页面上进行操作,然后刷新其他页面功能的实现方法:



前提条件是两个页面同源


在页面 A:


 // 判断是否是对比项目页面跳转过来的
if (route.query?.type === 'diff') {
localStorage.setItem('diffProjectChanged', 'true');
setTimeout(() => {
window.close();
}, 500);
} else {
router.back();
}

在页面 B:


// 进入页面将localStorage中的 diffProjectChanged 置为false
localStorage.setItem('diffProjectChanged', 'false');
// 监听编辑
onMounted(() => {
window.addEventListener('storage', (event) => {
if (event.key === 'diffProjectChanged' && event.newValue === 'true') {
location.reload();
}
});
});


在页面 A 中触发一个自定义事件,将相关数据传递给其他页面。


// 触发自定义事件
const event = new CustomEvent('dataChanged', { detail: { newData: 'someData' } });
window.dispatchEvent(event);

在页面 B 中监听该自定义事件,并在事件触发时执行刷新操作。


// 监听自定义事件
window.addEventListener('dataChanged', (event) => {
// 获取数据并执行刷新操作
const newData = event.detail.newData;
location.reload();
});


  • 使用 WebSocket


在页面 A 中通过 WebSocket 发送消息,通知其他页面。
在页面 B 中监听 WebSocket 消息,接收通知并执行刷新操作。


这种方法需要在服务器上设置 WebSocket 服务。


当前项目避免使用其他包管理工具


使用一些约束,让当前项目只能通过指定的包管理工具安装,防止项目配置乱七八糟的。


在当前根目录下


    // scripts/preinstall.js
if (!/pnpm/.test(process.env.npm_execpath || '')) {
console.log('只能使用pnpm');
console.warn(
`\u001b[33mThis repository requires using pnpm as the package manager ` +
` for scripts to work properly.\u001b[39m\n`,
);
process.exit(1);
}

在packages.json中scripts配置。


"preinstall": "node ./scripts/preinstall.js"

或者直接配置


"preinstall": "npx only-allow pnpm"

文本省略


这种方式需要设置具体宽高。如果是使用了 flex: 1 / 百分比数据 这种不会生效。动态的宽度是不会出现省略号的。


text-overflow: ellipsis;
width: 100%;
overflow: hidden;
white-space: nowrap;

所以我们可以使用多文本的方式代替。


word-break: break-all;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;

不同大小文字底部对齐方式


在CSS中,要让不同字体大小的内容底部对齐,你可以使用verti**cal-align属性。(设置在对齐元素上,只需要设置在一个元素上即可) 这属性用于控制内联元素(如文本)在其父元素内的垂直对齐方式。你可以将其设置为bottom来实现底部对齐。但需要注意的是,vertical-align属性通常用于内联元素,而不是块级元素。 如果是块级元素,我们可以使用flex布局的align-items: flex-end;来实现父容器内的文本底部对齐。要想实现底部对齐,父容器必须具有足够的高度来容纳最大的字体大小。


<style>
.container {
height: 100px;
border: 1px solid #ccc;
display: flex;
align-items: flex-end; /* 底部对齐的关键 */
}

.text {
font-size: 20px;
}

.small-text {
font-size: 14px;
}
</style>

<div class="container">
<p class="text">这是一些文本</p>
<p class="small-text">这是较小的文本</p>
</div>

了解的一些git操作


git提交错误分支,希望将中间的commit删除掉


先通过git rebase -i commitid切换到删除commit的前一个commitid。-i 表示要进行交互式 rebase,<commit-hash>^ 表示要删除指定提交及之后的提交。这将打开一个文本编辑器,列出了要进行 rebase 的提交。
image.png


image.png
这样他只是删除了本地的记录,但是并没有更新远程仓库。


image.png
所以我们需要强制当前记录提交


git push origin <branch> --force

image.png


如果直接执行git push他会告诉你需要拉取最新代码。如果执行了git pull前面做的工作就没用了。所以我们需要使用--force强制提交。


注意:最后git push一定要强制提交,不然按照他的提示拉取了远程代码,那么前面做的内容都没用了。


回退解决冲突之前的状态


git merge --about

查看当前分支基于那个分支创建的


git reflog show

修改分支名称


// 在当前分支
git branch -m feature/v1.4.1.0

// 不在当前分支
git branch -m old-branch new-branch

将已提交的记录提交到别的分支


ruanyifeng.com/blog/2020/0…


// 切换到需要提交的分支
// 找到需要提交的代码commit
git cherry-pick commitId

props的双向绑定便捷性


我们在使用表格编辑功能时,直接在dataSource中绑定props对应的值,当编辑单元格时,就直接更新props,很方便。由于一些其他的因素,这个模块并没有采用这个,导致以下bug出现。


这个问题是测试发现方改变一些字段时,字段为发送给后端,排查发现我在修改时,并没有通过emits将值更新到props中。所以造成bug。,导致最近需求一直变更代码bug很多,已经没有在去维护的力气了。😑
image.png


watch监听可能出现的问题


这个bug对于当时我写代码来说排查很困难。排查了很久,最后也是找我导师解决的,不得不说我导师排查bug的能力好nb。👍


最开始我是通过监听用户切换不同内容,监听diffProjectId,然后拿到formFirstValues和formSecondValues,去完成逻辑。这样看似没啥问题。但是完成逻辑的时候,拿到的formSecondValues总是上一次的值。这就很懵逼了。


出现这种情况的原因是我们监听的diffProjectId是同步的,而formFirstValues,formSecondValues这两个值是异步获取的。所以就会出现问题。最后通过下图方式实现功能。
image.png


删除后端不必要字段,造成的问题


由于后端定义的查询详情和请求传参字段不统一collectionPlanResp,collectionPlanReq,导致我回写数据不好处理,直接通过collectionPlanResp对象进行处理,在提交时在赋值。当时想着把多余字段删除collectionPlanResp,这样就会出现一个问题,我提交表单后,当后端服务抛出异常提示(比如字数限制),我们修改后,在当前页面再次提交,就会导致collectionPlanReq传递为空值。造成数据未传递给后端的bug。


分析了一下,字段的一些必填项长度限制,前端还是不要偷懒,做一下处理。


image.png


image.png


对于多字段UI处理


我们可以使用Collapse组件去处理,让UI看起来更简洁。


image.png


善于使用margin和定位来解决间距问题


调整间距时,如果margin不好调整,我们可以使用相对定位来配合调整。 这种方式是当时接了一个对比字段差异的需求,为了以后可以直接在当前对比页面编辑,所以采用了两个form去实现的。设置一个form的label,另一个不设置。这样就可以完美的在一行突出标题对比两个不同内容的字段了。为了做到响应式,就有了这样的做法。


image.png


这样看下来工作三个月基本都是在马代码,每天写不完的需求,发不完的版,上午写代码,下午改需求,真的挺无语的,下个月辞职回家种地喽。😅


往期文章



专栏文章



最近也在学习nestjs,有一起小伙伴的@我哦。


作者:Spirited_Away
来源:juejin.cn/post/7292036126269063178
收起阅读 »

几条SQL把服务器干崩了

大家好,我是冰河~~ 今天跟大家分享一个发生在今天凌晨的真实案例,这篇文章也是我事后临时写出来的,处理事情的过程有点无语,又有点气愤! 事件背景 事情的背景是这样的:一个朋友今年年初新开了一家公司,自己是公司的老板,不懂啥技术,主要负责公司的战略规划和经营管理...
继续阅读 »

大家好,我是冰河~~


今天跟大家分享一个发生在今天凌晨的真实案例,这篇文章也是我事后临时写出来的,处理事情的过程有点无语,又有点气愤!


事件背景


事情的背景是这样的:一个朋友今年年初新开了一家公司,自己是公司的老板,不懂啥技术,主要负责公司的战略规划和经营管理,但是他们公司的很多事情他都会过问。手下员工30多人,涵盖技术、产品、运营和推广,从成立之初,一直在做一款社交类的APP。平时,我们一直保持联系,我有时也会帮他们公司处理下技术问题。


事件经过


今天凌晨,我被电话铃声吵醒了,一看是这个朋友打来的,说是他们公司数据库服务器CPU被打满了,并且一直持续这个状态,他说拉个群,把他们后端Java同事拉进来一起沟通下,让我帮忙看看是什么问题,尽快处理下。说话语气很急,听的出来事态很严重,因为目前正在加大力度推广,周末使用人数也比较多,出现这种问题着实让人着急。


后面我加了那个朋友拉的微信群,开始了解服务器出现问题的具体情况,下面就是一些处理的经过了。


注:聊天内容已经获得授权公布。


2023-10-29-006.png


他们后端Java把运维发的监控截图发出来了,咱继续跟他沟通。


2023-10-29-007.png


为啥我说CPU占用高呢?大家看下他们运维发的图就知道了。


2023-10-29-001.png


CPU已经飙升到了400%了,数据库服务器基本已经卡死。拿到他给我发的SQL后,我跟他们老板要了一份他们的数据库表结构,在我电脑上执行了下查询计划。


2023-10-29-008.png


这不看不知道,一看吓一跳,一个C端频繁访问的接口SQL性能极差,Using temporary、Using filesort、Using join buffer、Block Nested Loop全出来了。


我把这个图发出去了,也结合他们团队的实际情况,给出了优化的目标建议:SQL中不要出现Using filesort、Block Nested Loop,尽量不要出现Using join buffer和Using temporary,把Using where尽量优化到Using Index级别。


2023-10-29-009.png


说是尽量不要出现Using join buffer和Using temporary,把Using where尽量优化到Using Index级别,就是怕他们做不到这点,优先把Using filesort、Block Nested Loop优化掉。 但是这货后面说的话实属把我震惊到了。


2023-10-29-封面.png


我看完他的回复,直接有点无语:卧槽,不超过500万rows效率很高?你这SQL 500万数据效果很高?更让我无语的是这货说MySQL一般一亿以上数据量开始优化,这特么不是完全扯淡吗?他说这话时,我大概就知道这货的水平了。。。


后面我就问他说的这些数据的依据是哪里来的。


2023-10-29-011.png


这货说是什么大数据高并发MySQL数据库压测出来的,稍微有过压测经验的应该都知道,压测一个很重要的前提就是要明确压测的环境,最起码要明确压测环境服务器的CPU核数和内存,直接来句MySQL一亿数据是大数据高并发MySQL数据库压测出来的结果,这还是MySQL官方的数据。。。。


不知道是不是因为群里有他们老板的缘故,这货后面还在狡辩。


2023-10-29-012.png


沟通到这里,我特么有种想打人的冲动,生产环境所有业务快被数据库拖死了,数据库服务器CPU被干爆了,监控到慢SQL,并且查看这些慢SQL的执行计划,性能非常低下,SQL里不管是select部分还是where部分大量使用了MySQL自带的函数,这不慢才怪啊。看这货处理问题的态度,要是我下面的人,早就让他卷铺盖走人了。


处理结果


后续我跟他们老板要了一个代码只读权限的账号,将代码拉取下来后,好家伙,到处都是这种SQL查询,要是一两处还好,把SQL修改并优化下,关联的业务逻辑调整下,再把功能测试下,接口压测下,没啥问题就可以发版上线了。


但是,如果到处都是这种SQL的话,那优化SQL就要花费一定的时间了,甚至是新发布的重大功能逻辑都要重写。最终,我跟他们老板说的是回滚版本吧,最新的功能还是先下线,把新功能的SQL、缓存、业务逻辑、接口都优化好,压测没问题后再重新上线。


2023-10-29-013.png


事后总结


无论什么时候,生产环境一旦出现致命问题,第一时间要优先恢复生产环境正常访问,随后再认真排查、定位和解决问题,毕竟生产环境一旦出现问题,每一秒流失的都是真金白银。


搭建技术团队一定要找靠谱的人,最起码团队的核心人员要靠谱,像我朋友团队的这个技术,在他的认知里,MySQL执行计划出现Using temporary、Using filesort、Using join buffer、Block Nested Loop,500W rows效率都很高,殊不知他们生产环境实际主表数据才10几条,要是真达到500W量级就别查询了,数据库直接就趴下了。还有这个MySQL一般一亿以上开始优化,这个依据我也不知道这货是从哪里看到的,并且还说了大数据高并发MySQL数据库压测出来的,这不纯属扯淡吗?


更离谱的是我事后悄悄问了他们老板,他的工作年限是多久,据说工作10多年了,是位80后。


顿时让我想到了一句话:人的认知有几个层次:不知道自己不知道,知道自己不知道,知道自己知道,不知道自己知道。


好了,今天就到这儿吧,我是冰河,我们下期见~~


作者:冰_河
来源:juejin.cn/post/7298635800631459892
收起阅读 »

这款支持安全多人协作的在线终端,真的吓到我了❗️❗️

web
这款支持安全多人协作的在线终端 ☀️ 前言 事情是这样的: 周末一个同事的项目报错了,但是无法精准定位到问题😠。 他希望我帮忙看一下他的报错并协助解决,于是扔了一个链接给我🤔。 我心想你给我链接干啥,你倒是截图报错啊😡。 打开链接后我直呼 wassu...
继续阅读 »

这款支持安全多人协作的在线终端



☀️ 前言



  • 事情是这样的:

    • 周末一个同事的项目报错了,但是无法精准定位到问题😠。

    • 他希望我帮忙看一下他的报错并协助解决,于是扔了一个链接给我🤔。

    • 我心想你给我链接干啥,你倒是截图报错啊😡。



  • 打开链接后我直呼 wassup🔥,我居然可以在一个网页中操作他的终端,并且还是实时协同的!有鼠标动来动去那种!

  • 询问得知原来是用的 sshx ,那么我们本文就来了解一下这个神奇的产品。


🔥 sshx



  • sshx 这是一款基于网络的安全的团队协作终端,它允许您在多人无限画布上通过链接与任何人共享您的终端。

  • 只需要共享的人员执行一下“sshx”再将链接分享给你同事,则它能马上加入到你的终端进行操作。

  • 它具有实时协作远程光标聊天功能。它还具有快速且端到端加密的特点,并具有用 Rust 编写的轻量级服务器。

  • 实时协同代表着什么,这将使远程团队调试终端问题变得更加容易。


🤔 怎么使用


安装命令行界面



  • 通过在终端中运行此命令curl -sSf https://sshx.io/get | sh来获取 sshx CLI。它很小,只需几秒钟即可下载(3 MB)


分享您的终端



  • 直接在你需要分享的终端内执行 sshx,此时这个终端不要关闭,他会生成一个分享链接。




  • 将这个终端用浏览器打开即可,进入到这个网址,会让你输入一个名称方便团队协时展现光标的用户。





  • 在上方的操作栏新建一个虚拟终端即可操作真正的终端了。




  • 为了方便演示我这里打开两个浏览器来模拟别人协同操作我的终端,来我们跑个苹果来看看。




  • 实时对话也是很流畅。




  • 我们可以看到,在页面会出现另一个用户的移动光标,并且可以与他对话,他的延迟是非常低的,这真的可以帮助我们实时协作。




  • (协不协作我不知道,但是可以看到光标是真的帅啊!)




❓ 用来干啥



  • 那么这么一款产品,有的同学就会问了:他的作用是什么呢?看起来很鸡肋啊?

  • 有了这么一款产品,我们可以:

    • 在帮助客户部署相关公司产品的时候不需要远程操控别人的电脑,只需要客户安装这款 cli 并且联网,我们既可以远程帮忙操作。

    • 更好的公司运维,在同事操作的时候,可以随时介入进行操作。

    • 很多群友在前端群中问问题时习惯抛出一个截图,但是又没有说明白上下文,这时候就可以将你终端分享给大佬们定位问题。

    • (手摸手教女同学命令行操作🐶)



  • 那么肯定又会有同学问了:那我不是可以随便删除别人的文件?我直接rm -f * 敢问阁下如何应对?

  • 是的,看了下确实可以执行这些操作,所以还是尽量分享给你信得过的人,我觉得其实作者可以出一个只读模式only-read,这样你就可以让别人在你的终端上阅读和滚动,减少一些权限。


👋 写在最后



作者:快跑啊小卢_
来源:juejin.cn/post/7298642242117238834
收起阅读 »

Node.js如何处理多个请求?

web
前言 在计算机科学领域,关于并发和并行的概念经常被提及。然而,这两个术语常常被混为一谈,导致很多人对它们的理解存在着很多混淆。本文小编将通过对并发和并行的深入解析,帮助读者更好地理解它们之间的不同特点和应用场景。同时,文章还将介绍Node.js如何高效地处理多...
继续阅读 »

前言


在计算机科学领域,关于并发和并行的概念经常被提及。然而,这两个术语常常被混为一谈,导致很多人对它们的理解存在着很多混淆。本文小编将通过对并发和并行的深入解析,帮助读者更好地理解它们之间的不同特点和应用场景。同时,文章还将介绍Node.js如何高效地处理多个请求的技巧和方法。


什么是并发


并发是指两个或多个任务可以在重叠的时间段内开始、运行和完成。这并不一定意味着它们将同时运行,但它们可以交错执行,以便在任何给定的时间,总有一个任务在运行。


下面小编以一个简单的例子给读者详细的解释并发的特点:


假设在一个餐厅里面,有一个服务员从1号桌的客人那里接受了一份点单,然后这个服务员在厨房一直等待1号桌客人的饭做好,做好之后将饭端到1号桌。


这个服务员完成第一桌客人的点单后,再前往下一桌的2号客人处,接受订单,并前往厨房等待准备完成,等饭做好后再将点餐的餐点交给客人。


看到这里,各位读者可能会觉得这个服务员的做法一点都不高效,他完全可以在等第一单饭的时候去第二桌点单,按照这位服务员现在的做法,他在每一单的饭做好之前的这个时间段内什么事情都干不了,这样就浪费了大量的时间。


我们现在修改一下这位服务员的做法,修改后如下:


服务员将前往1号桌接受订单并将其交给厨房,然后返回2号桌接受订单并将其同样交给厨房。在这种情况下,服务员不会等待订单准备完成,而是会继续前往下一个桌子接受订单,直到食物准备好。当食物准备好后,服务员会为所有桌子上的客人上菜。像上述的这种情况,没有增加线程(服务员)的数量,但通过缩短空闲时间来加快处理过程。同时处理多个任务,这个就是并发。


例如:你正在做饭的同时,接到一通电话,于是你接听了电话,当听到炉子发出警报时,你回去关掉炉子,然后再继续接电话。


这个例子很好地展示了并发的概念。做饭的过程中,能够同时处理来自电话和炉子的不同事件。你在不中断一个任务的情况下,暂时切换到另一个任务,然后再回到原来的任务。这种并发的方式能够提高效率并更好地应对多个任务的情况。(同时做两件事,但是一次只做一件事)


什么是并行


并行是指两个或多个任务可以真正同时运行。为了实现这一点,这些任务必须能够在独立的CPU或核心上运行。同样的,小编依然以做饭的例子给大家解释一下什么是并行:


例如:你正在做饭的同时,接到一通电话,你的家人接听了电话,你继续做饭,你和你的家人谁也不会干扰谁,两个不同的事情发生在两个人身上,这个就是并行。


什么是单线程进程?


单线程进程是按照单一顺序执行编程指令的过程。话虽如此,如果一个应用程序具有以下一组指令:


指令A


指令B


指令C


如果这组指令在单线程进程中执行,执行过程将如下所示:


多线程进程是什么?


多线程进程是在多个序列中执行编程指令。因此,除非多个指令被分组在不同的序列中,否则指令不需要等待执行。


为什么Node.js是单线程的?


Node.js是一个单线程的平台。这意味着它一次只能处理一个请求。


例如:服务员从1号桌子上接订单并将其传给厨房,然后去2号桌子接订单。当从2号桌子接订单时,1号桌子的食物已经准备好了,但是服务员不能立即过去将食物送到1号桌子,服务员必须先完成1号桌子的订单,然后将其交给厨房,然后再将准备好的餐点送到1号桌子。


Node.js Web服务器维护一个有限的线程池,为客户端请求提供服务。多个客户端向Node.js服务器发出多个请求。Node.js接收这些请求并将它们放入事件队列中。Node.js服务器有一个内部组件,称为事件循环(Event Loop),它是一个无限循环,接收并处理请求。这个事件循环是单线程的,也就是说,事件循环是事件队列的监听器。


Node.js如何处理多个请求?


Node.js可以通过事件驱动模型轻松处理多个并发请求。


当客户端发送请求时,单个线程会将该请求发送给其他人。当前线程不会忙于处理该请求。服务器有工作人员为其工作。服务器将请求发送给工作人员,工作人员进一步将其发送给其他服务器并等待响应。同时,如果有另一个请求,线程将其发送给另一个工作人员,并等待来自另一个服务器的响应。


这样,单个线程将始终可用于接收客户端的请求。它不会阻塞请求。


Node.js实现多个请求的代码:


const http = require('http');

// 创建一个 HTTP 服务器对象
const server = http.createServer((req, res) => {
// 处理请求
if (req.url === '/') {
// 设置响应头
res.writeHead(200, { 'Content-Type': 'text/plain' });

// 发送响应数据
res.end('Hello, World!');
} else if (req.url === '/about') {
// 设置响应头
res.writeHead(200, { 'Content-Type': 'text/plain' });

// 发送响应数据
res.end('About Us');
} else {
// 设置响应头
res.writeHead(404, { 'Content-Type': 'text/plain' });

// 发送响应数据
res.end('Page Not Found');
}
});

// 监听 3000 端口
server.listen(3000, () => {
console.log('Server listening on port 3000');
});

总结


总的来说,Node.js在处理多个请求方面具有优势。它利用事件驱动和非阻塞式I/O的特性,能够高效地处理并发请求,提供快速响应和良好的可扩展性。同时,通过采用适当的工具和技术,可以进一步优化性能,控制并发量,并提高系统的可靠性和稳定性。


扩展链接:


从表单驱动到模型驱动,解读低代码开发平台的发展趋势


低代码开发平台是什么?


基于分支的版本管理,帮助低代码从项目交付走向定制化产品开发


Redis从入门到实践


一节课带你搞懂数据库事务!


Chrome开发者工具使用教程


作者:葡萄城技术团队
来源:juejin.cn/post/7298646156437438464
收起阅读 »

Android 14 彻底终结大厂流氓应用?

hi 大家好,我是 DHL。大厂程序员,就职于美团、快手、小米。公众号:ByteCode,分享技术干货和编程知识点 在某些大厂内部通常都会有一个神秘的团队,他们的工作内容就是专门研究系统,而的事情就是如何让自家应用在后台存活的更久,达到永生的目的。 其中有个...
继续阅读 »

hi 大家好,我是 DHL。大厂程序员,就职于美团、快手、小米。公众号:ByteCode,分享技术干货和编程知识点



在某些大厂内部通常都会有一个神秘的团队,他们的工作内容就是专门研究系统,而的事情就是如何让自家应用在后台存活的更久,达到永生的目的。


其中有个别公司,甚者利用公开漏洞,达到远程操控用户手机的目的,做更多他们想做的事,可以随意获取用户的隐私,而且一旦安装,普通用户很难删除,之前写了一些揭露他们的文章,但是现在已经被全部删除了,就连评论区抨击他们的内容也全都被删除了。


而 Android 14 的出现,可以说是暂时性的彻底终结了这些流氓软件,想在后台通过保活的方式,让应用在后台达到永生的目的基本不可能了。


为什么这是暂时性的呢,因为没有完美的系统,新的系统虽然修复了公开漏洞,终结了现有的保活的方式,但是新系统可能存在新的漏洞,还是会给某些大厂可乘之机。


我们一起来看一下 Android 工程副总裁 Dave Burke 都介绍 Andorid 14 在性能、隐私、安全性方面做了那些改进,这篇文章是对之前的文章 适配 Android 14,你的应用受影响了吗Android 14 新增权限 的补充。



  • 冻结缓存应用,增强杀进程能力

  • 应用启动更快

  • 减少内存占用

  • 屏幕截图检查

  • 显示全屏系统通知

  • 精确闹钟权限

  • 提供了对照片和视频的部分访问权限

  • 最小 targetSdkVersion 限制

  • 返回手势


本文只会介绍我认为 Android 14 上最大的变化,关于 Android 14 的所有变更,可以前往查看变更
developer.android.com/about/versi…


冻结缓存应用,增强杀进程能力


在 Android 11 以上支持冻结已缓存的应用,当应用切换到后台并且没有其他活动时,系统会在一定时间内通过状态判断,是否冻结该应用,如果一个应用被冻结住了,将完全被 "暂停",不再消耗任何 CPU 资源,可以减少应用在后台消耗的 CPU 资源,从而达到节电的目的。


被冻结已缓存的应用并不会执行终止该应用,冻结的作用只是暂时挂起进程,消耗 CPU 的资源为 0,它有助于提高系统性能和稳定性,同时最大限度地节省设备的资源和电量的消耗,一旦应用再次切换到前台时,系统会将该应用的进程解冻,实现快速启动。


如果你的手机支持冻结已缓存的应用,在开发者选项里会显示 「暂停执行已缓存的应用」设置项。



冻结已缓存应用,在内核层面使用的是 cgroup v2 freezer,相对于使用信号 SIGSTOP 与 SIGCONT 实现的挂起与恢复,cgroup v2 freezer 无法被拦截,也就无法被开发者 Hook,从而彻底终结大厂想在这个基础上做一些事情。


当然 Google 也对 cgroup 进行了封装,提供了 Java API,在上层我们也可以调用对应的方法实现 CPU、内存资源的控制。


public static final native void setProcessFrozen(int pid, int uid, boolean frozen);
public static final native void enableFreezer(boolean enable);

经过测试 Android 14 相比于 Android 13,缓存进程的 CPU 使用量降低了高达 50%,因此,除了传统的 Android 应用生命周期 API,如前台服务、JobScheduler 或 WorkManager,后台工作将受到限制。


另外在 Android 14 上系统在杀进程之前,首先会将应用所有的进程进行 cgroup v2 freezer,被冻结的应用 cpu 资源占用为 0,然后在挨个杀掉进程,想通过进程互相拉取进程的方式,不断的想通过 fork 出子进程,达到应用永生的目的,在 Android 14 以上已经不可能了,这些黑科技都要告别历史的舞台了。


应用启动更快


在 Android 14 上对缓存应用进行优化,增加了缓存应用的最大数量的限制,从而减少了冷启动应用的次数。


而应用的最大缓存数量不是固定的,可以根据设备的内存容量进行调整,Android 测试团队在 8GB 设备上,发现冷启动应用的数量减少了 20%,而在 12GB 设备上减少了超过 30%,冷启动相对于热启动来说速度较慢,而且在电量消耗方面成本较高,这一工作有效地改善了电量使用和整体应用启动时间。


减少内存占用


代码大小是我们关注的关键指标之一,代码量越大虚拟内存占用越高,减少生成的代码大小,对内存(包括 RAM 和存储空间)的影响就越小。


在 Android 14 中,改进 Android 运行时(ART)对 Android 用户体验,ART 包含了优化措施,将代码大小平均减少了 9.3%,而不会影响性能。


屏幕截图检查


在 Android 14 中新增了一个特殊的 API,截取屏幕截图后会有个 ScreenCaptureCallback 的回调,当用户正在使用截取屏幕截图时,将会调用这些回调函数。


要使 API 正常工作,需要在 AndroidManifest 中添加 DETECT_SCREEN_CAPTURE 权限,然后在 onStart() 方法中注册回调,需要在 onStop() 中取消注册。


<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.DETECT_SCREEN_CAPTURE" />
</manifest>


class MainActivity : Activity() {

private val mainExecutor = MainEcxector()

private val screenshotCallback = ScreenCaptureCallback {
// A screenshot was taken
}

override fun onStart() {
super.onStart()
registerScreenCaptureCallback(mainExecutor, screenshotCallback)
}

override fun onStop() {
super.onStop()
unregisterScreenCaptureCallback(screenshotCallback)
}
}

显示全屏系统通知



Android 11 引入了全屏通知,当全屏应用程序运行时,这些通知将在锁屏屏幕上显示,任何应用都可以在手机处于锁定状态时使用 Notification. Builder. setFullScreenIntent 发送全屏 Intent,不过需要在 AndroidManifest 中声明 USE_FULL_SCREEN_INTENT 权限,在应用安装时自动授予此权限。


从 Android 14 开始,使用此权限的应用仅限于提供通话和闹钟的应用。对于不适合此情况的任何应用,Google Play 商店会撤消其默认的 USE_FULL_SCREEN_INTENT 权限。


在用户更新到 Android 14 之前,在手机上已经安装的应用仍拥有此权限,但是用户可以开启和关闭此权限,所以您可以使用新 API NotificationManager.canUseFullScreenIntent 检查应用是否具有该权限。


如果想在 Android 14 上使用这个权限,我们可以提示用户手动打开授权,通过 Intent(ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT) 来跳转到设置界面。


if(NotificationManager.canUseFullScreenIntent()){
startActivity(Intent(ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT))
}

精确闹钟权限


在 Andorid 12 之前我们可以直接调用 setAlarmClock()setExact()
setExactAndAllowWhileIdle() 等等方法设置精确闹钟时间,


但是在 Android 12 上 Google 引入了一个新的权限 SCHEDULE_EXACT_ALARM,如果想调用 setAlarmClock()setExact()
setExactAndAllowWhileIdle() 等等方法设置精确闹钟时间, 需要在 manifest 中申明 android.permission.SCHEDULE_EXACT_ALARM 权限。


所以运行在 Android 12 ~ Android 13 系统上,我们只需要声明一下权限就可以使用了,但是从 Android 14 开始 SCHEDULE_EXACT_ALARM 权限默认被禁止使用了。


如果你还想在 Andorid 14 以上使用精准闹钟的 API,我们可以提示用户手动打开授权,通过 Intent (ACTION_REQUEST_SCHEDULE_EXACT_ALARM) 来跳转到设置界面,代码如下。


val alarmManager: AlarmManager = context.getSystemService<AlarmManager>()!!
when {
// If permission is granted, proceed with scheduling exact alarms.
alarmManager.canScheduleExactAlarms() -> {
alarmManager.setExact(...)
}
else -> {
// Ask users to go to exact alarm page in system settings.
startActivity(Intent(ACTION_REQUEST_SCHEDULE_EXACT_ALARM))
}
}

提供了对照片和视频的部分访问权限


这个限制和 iOS 相似,Android 14 提供了对照片和视频的部分访问权限。当您访问媒体数据时,用户将看到一个对话框,提示用户授予对所有媒体的访问、或者授予单个照片/视频的访问权限,该新功能将适用于 Android 14 上所有应用程序,无论其 targetSdkVersion 是多少。



在 Android 13 上已经引入了单独的照片访问和视频访问权限,但是在 Android 14 上新增了新的权限 READ_MEDIA_VISUAL_USER_SELECTED


<manifest xmlns:android="http://schemas.android.com/apk/res/android" />

<!-- Devices running Android 13 (API level 33) or higher -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

<!-- To handle the reselection within the app on Android 14 -->
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />

</manifest>

如果没有声明新的权限,当应用程序进入后台或用户终止应用程序时,单独的照片访问和视频访问权限将立即撤销,不会保存 READ_MEDIA_IMAGESREAD_MEDIA_VIDEO 权限的状态,每次都需要检查。


最小 targetSdkVersion 限制


Android 14 当中有一个比较大的变化就是,无法安装 targetSdk <= 23 的应用程序 (Android 6.0),不要将它与 minSdk 混淆。


在 Android 开发中有两个比较重要的版本号:



  • compileSdkVersion :用于编译当前项目的 Android 系统版本

  • targetSdkVersion :App 已经适配好的系统版本,系统会根据这个版本号,来决定是否可以使用新的特性


这个最小 targetSdkVersion 限制,主要是出于安全考虑,在 Android 6.0 中引入了运行时权限机制,App 想要使用一些敏感权限时,必须先弹窗询问用户,用户点击允许之后才能使用相应的权限。


但是一些 App 为了利用权限方便获取到用户的信息,通过不去升级 targetSdk 的版本号的方式,在安装过程中获得所有权限,以最低成本的方式,绕过运行时权限机制。


如果之前已经安装了的 App,就算升级到 Android 14 也会去保留,系统不能代表用户去删除某个应用,其实我在想,为什么不针对这些已经安装好的低版本的 App,Google 给出一些警告提示,让用户可以感知到呢


返回手势


在 Android 13 的时候,Google 已经预示我们在下一个版本中,返回手势将会有一些更新,并以预览屏幕的形式呈现动画,效果如下图所示。



我们来演示一下使用后退导航的动画。



在 Android 14 增加了在 App 中创建过渡动画的功能,比如在 OnBackPressedCallback 接口中添加了一个方法 handleonbackprogress() ,这个方法在返回手势执行过程中被调用,我们可以在这个方法中增加一些过渡动画。


OnBackPressedCallback 接口中还提供了两个方法 handleOnBackPressed()handleOnBackCancelled() 分别在动画完成和取消动画时调用,我们来看看在代码中如何使用。


class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
val box = findViewById<View>(R.id.box)
val screenWidth =
Resources.getSystem().displayMetrics.widthPixels
val maxXShift = (screenWidth / 20)

val callback = object : OnBackPressedCallback(
enabled = true
) {

override fun handleOnBackProgressed(
backEvent: BackEvent
)
{
when (backEvent.swipeEdge) {
BackEvent.EDGE_LEFT ->
box.translationX = backEvent.progress *
maxXShift
BackEvent.EDGE_RIGHT ->
box.translationX = -(backEvent.progress *
maxXShift)
}
box.scaleX = 1F - (0.1F * backEvent.progress)
box.scaleY = 1F - (0.1F * backEvent.progress)
}

override fun handleOnBackPressed() {
// Back Gesture competed
}


override fun handleOnBackCancelled() {
// Back Gesture cancelled
// Reset animation objects to initial state
}
}
this.onBackPressedDispatcher.addCallback(callback)
}
}

API 被废弃


在 Android 中使用 overidePendingTransition () 方法实现进入和退出动画,但是在 Android 14 上提供了新的 overrideActivityTransition () 方法,而 overidePendingTransition () 方法已被标记为弃用。


// New API
overrideActivityTransition(
enterAnim = R.anim.open_trans,
exitAnim = R.anim.exit_trans,
backgroundColor = R.color.bgr_color
)

// deprecated
overridePendingTransition(R.anim.open_trans, R.anim.exit_trans)



全文到这里就结束了,感谢你的阅读,坚持原创不易,欢迎在看、点赞、分享给身边的小伙伴,我会持续分享原创干货!!!




我开了一个云同步编译工具(SyncKit),主要用于本地写代码,同步到远程设备,在远程设备上进行编译,最后将编译的结果同步到本地,代码已经上传到 Github,欢迎前往仓库 hi-dhl/SyncKit 查看。





Hi 大家好,我是 DHL,就职于美团、快手、小米。公众号:ByteCode ,分享有用、有趣的硬核原创内容,Kotlin、Jetpack、性能优化、系统源码、算法及数据结构、动画、大厂面经,真诚推荐你关注我。





最新文章



开源新项目




  • 云同步编译工具(SyncKit),本地写代码,远程编译,欢迎前去查看 SyncKit




  • KtKit 小巧而实用,用 Kotlin 语言编写的工具库,欢迎前去查看 KtKit




  • 最全、最新的 AndroidX Jetpack 相关组件的实战项目以及相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看 AndroidX-Jetpack-Practice




  • LeetCode / 剑指 offer,包含多种解题思路、时间复杂度、空间复杂度分析,在线阅读




作者:程序员DHL
来源:juejin.cn/post/7298699367791411236
收起阅读 »

Nginx与洗脚城技师那些有意思的小故事

FBI WARNING: 内容仅作为举例用途,本人不曾去过洗脚城,也不知道技师是什么 我们今天来讲讲Nginx的一些小常识吧。 什么是Nginx Nginx 是一款是由伟大且牛逼的俄罗斯的程序设计师 Igor Sysoev(伊戈尔 赛索耶夫) 在摸鱼玩耍时...
继续阅读 »

FBI WARNING:



内容仅作为举例用途,本人不曾去过洗脚城,也不知道技师是什么



我们今天来讲讲Nginx的一些小常识吧。


什么是Nginx


Nginx 是一款是由伟大且牛逼的俄罗斯的程序设计师 Igor Sysoev(伊戈尔 赛索耶夫) 在摸鱼玩耍时所开发高性能的 Web反向代理 服务器,也是一个 IMAP/POP3/SMTP 代理服务器。


什么是反向代理


说到反向代理之前,我们来看看什么是 正向代理(跑腿代购)。


正向代理运行在客户端的出口,获取客户端发送的请求,用自己的身份发送到服务器并获取返回数据,最终返回给客户端。


常见的正向代理,如网络爬虫的IP代理、VPN、游戏加速器......


image.png


那么,什么是反向代理呢?


反向代理服务器运行在服务端的入口,根据客户端请求的部分参数进行请求的分发与调度等。(那不就是洗脚城的前台吗?)


常见的反向代理场景:



  • 负载均衡

  • 动静分离    

  • 业务分离

  • ......


image.png


Nginx 有哪些作用?


1. Nginx的虚拟站点服务


Nginx可基于 端口号、域名(IP) 实现多站点服务,已实现前端访问80或443端口,通过上述不同条件访问不同的后端服务



一个洗脚城前台,后面有不知道数量的技师。



server
{
listen 80;
server_name http://www.hamm.cn;
index index.html;
root /home/www/portal_site;
}
server
{
listen 80;
listen 443 ssl http2;
# 证书相关配置
server_name api.hamm.cn;
index index.html;
root /home/www/api_service;
}
server
{
listen 80;
server_name static.hamm.cn;
index index.html;
root /home/www/static_service;
autoindex on;
}

image.png


2. Nginx反向代理实现负载均衡


通过反向代理指定上游多个负载点,配置不同的负载优先级以及调度策略来实现负载均衡。



按业务能力和服务质量,洗脚城前台往往会针对不同的技师分配不同的任务。



upstream api_slb {
server 192.168.123.78:8100 weight=5 max_fails=3 fail_timeout=5;
server 192.168.123.78:8200 weight=3 max_fails=3 fail_timeout=5;
server 192.168.123.79:8100 weight=2 max_fails=3 fail_timeout=5;
# 优先使用局域网测试服务器的服务 按权重进行负载


# 如果测试服务器不可用,可通过下面两台备用服务跑

server 192.168.123.77:8080 weight=2 backup;
# 同事张三 代码一般是最新 优先使用
server 192.168.123.151:8080 weight=1 backup;
# 同事李四 工位最近 做备机 方便沟通
}
server {
listen 80;
server_name api.hamm.cn;
location / {
proxy_pass http://api_slb;
}
}

image.png


3. Nginx业务分离或动静分离


使用Nginx解决一些特定的需求



  • 相同域名,不同路径访问不同资源

  • 不同域名,解决访问跨域等问题
    .......



技师根据不同的客户需求,分配不同业务能力的技师。



upstream api_service {
server 192.168.123.78:8100;
}
upstream web_frontend {
server 192.168.123.66:8010;
}
server {
listen 80;

# 使用统一域名 http://hamm.cn访问所有资源

server_name hamm.cn;

# 匹配 http://hamm.cn/api/****
# 到系统API服务器上
location /api/ {
proxy_pass http://api_service;
}

# 如果资源在本机 可使用Nginx自带的静态资源服务
location /static {
index index.html;
alias /home/www/static
}

# 匹配其他请求到前端服务器上
location / {
proxy_pass http://web_frontend
}
}

image.png


4. Nginx完成其他场景下的小需求




  • 跨域问题




server {
# API服务
listen 80;
server_name api.hamm.cn;

# 设置允许跨域的域名 支持通配符 如* 代表所有
add_header Access-Control-Allow-Origin hamm.cn;

# 跨域时允许提交的header数据
add_header Access-Control-Allow-Headers 'authorization','content-type';

# 允许跨域的请求方法
add_header Access-Control-Allow-Methods 'option,get,put,post';

# 还有很多配置项 自己百度吧:)
}
server {
# 前端
listen 80;
server_name hamm.cn;
}




  • 代理过滤





使用 sub_filter 模块将代理中一些数据进行过滤或替换:)



server {
...

location /api {
...
sub_filter_types application/json;
sub_filter_once off;

sub_filter '搜藏成功' '收藏成功';
}
}


技师服务的时候有点不耐烦,大喊了一声 “傻X客户”,洗脚城前台为了洗脚城的形象,给替换成了 “好帅的客户”





  • 自定义错误页




server
{
listen 80;
server_name api.hamm.cn;
error_page 502 503 404 403 500 /error.json;
}


技师让客户不高兴的时候,洗脚城每次都出来给大家唱首歌缓和下气氛。





  • 流量控制 请求黑名单IP 防盗链 请求拦截





技师提前预判是不是意向客户,或者专门找事的客户,提前处理好这些事情,不让技师烦恼。



server
{
listen 80;
server_name static.hamm.cn;
root /home/www/static_files/images;
location ~ .*\.(jpg|gif|png|bmp)$ {
# 如果是百度爬虫 让它一边去
if ($http_user_agent ~ 'Baiduspider') {
# rewrite * "/getout.jpg";
return 403;
}
# 图片 允许直接访问 如有跨域 只允许指定的域名访问
valid_referers none *.hamm.cn;
if ($invalid_referer) {
# 其他的请求 通通甩掉
return 403;
}
}
location /admin/ {
# 如果是上传到admin目录下的文件
allow 123.45.67.89;
# 只允许指定的IP可以访问
deny all;
# 其他人通通甩掉
}
}

image.png


总结一下


我真的没去过洗脚城。


作者:Hamm
来源:juejin.cn/post/7295995236886396939
收起阅读 »

写了个APP「原色」—— 基于中国传统色

中国传统色 简介 这是一个工具类APP 颜色筛选以及关模糊查询 颜色详情信息查看以及复制 色卡分享 自定义主题色(长按色卡) 小组件支持 已上架应用宝/App Store,搜索原色即可找到 最初是做了个1.0版本(MVP),功能比较简单,后面感觉没什么可...
继续阅读 »

中国传统色



简介


这是一个工具类APP



  • 颜色筛选以及关模糊查询

  • 颜色详情信息查看以及复制

  • 色卡分享

  • 自定义主题色(长按色卡)

  • 小组件支持


已上架应用宝/App Store,搜索原色即可找到


最初是做了个1.0版本(MVP),功能比较简单,后面感觉没什么可加的就放置一边了


1.0版本.jpeg


最近比较空闲又拿起来,bug修一点加一点,界面改了又改哈哈哈,然后现在迭代到2.0版本(预览图为 iOS)


2.0版本.jpeg
除了界面大换新,也增加了一些功能,比如颜色搜索、筛选、小组件等。Android与iOS基本一致,除了搜索筛选界面不一样:


Android搜索筛选.jpg


下面介绍一下一些功能的实现以及碰到的问题


色卡与文字处理


在1.0版本对色卡的背景颜色和文字颜色关系处理比较粗暴简单,当系统出去浅色模式下。文字就在原来颜色的基础上降低亮度;在深色模式下文字就降低亮度,但是这种方式在部分过亮或者过暗背景上还是很难看清。

2.0版本对色卡和文字颜色都做了动态处理:

色卡:渐变处理,从上往下,比例为0——0.3——1.0。



在浅色模式下颜色为color(alpha=0.7)——color——color;


在深色模式下颜色为color(brightness + 0.2)——color——color



色卡文字:根据颜色是否为亮色进行处理,判断规则为:



颜色为亮色,则降低0.3亮度,否则 降低0.1亮度



在iOS上有用于修改view亮度的方法:brightness(Double),可惜安卓没有直接修改视图或者颜色亮度的方法,于是我就通过修改颜色 HSL来达到类似的效果。为了和ios的brightness 一致,changeBrightness的范围我设置为[-1F, 1F],但outHsl[2]的范围是[0F, 1F],所以计算做了一些调整:


// 修改颜色亮度
@ColorInt
fun @receiver:ColorInt Int.brightness(changeBrightness: Float): Int {
val outHsl = FloatArray(3)
ColorUtils.colorToHSL(this, outHsl)
if (changeBrightness <= 0) {
outHsl[2] = outHsl[2] * (1 + changeBrightness)
} else {
outHsl[2] = outHsl[2] + (1 - outHsl[2]) / 10 * changeBrightness * 10
}
return ColorUtils.HSLToColor(outHsl)
}

// 判断颜色为两色或者暗色
fun @receiver:ColorInt Int.isLight(): Boolean {
val red = Color.valueOf(this).red()
val green = Color.valueOf(this).green()
val blue = Color.valueOf(this).blue()
val brightness = (red * 299 + green * 587 + blue * 114) / 1000
return brightness > 0.5
}

颜色信息展示(BottomSheet)


设置BottomSheet默认完全展开,设置方法如下:


override fun onStart() {
super.onStart()
val behavior = BottomSheetBehavior.from(requireView().parent as View)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}

至于圆角处理,只需要在主题文件里写好就行了:


 <!--Rounded Bottom Sheet-->
<style name="ThemeOverlay.App.BottomSheetDialog" parent="ThemeOverlay.Material3.BottomSheetDialog">
<item name="bottomSheetStyle">@style/ModalBottomSheetDialog</item>
</style>

<style name="ModalBottomSheetDialog" parent="Widget.Material3.BottomSheet.Modal">
<item name="shapeAppearance">@style/ShapeAppearance.App.LargeComponent</item>
<item name="shouldRemoveExpandedCorners">false</item>
</style>

<style name="ShapeAppearance.App.LargeComponent" parent="ShapeAppearance.Material3.LargeComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">24dp</item>
</style>

如果不修改sheet背景色(默认为白色/黑色),只需要设置以上主题就可以了,但是如果修改了背景色,就需要在代码里对背景进行圆角处理,不能直接设置背景色,不然在圆角下面还会有颜色:


默认设置圆角背景.png


动态设置圆角背景.png


// 会存在背景色
// binding.bottomSheetLayout.setBackgroundColor(sheetBackground)

// 设置圆角背景
binding.bottomSheetLayout.setCornerBackground(24, 24, 0, 0, sheetBackground)

private fun View.setCornerBackground(leftRadius: Int, topRadius: Int, rightRadius: Int, bottomRadius: Int, @ColorInt color: Int) {
val shape = ShapeDrawable(RoundRectShape(
floatArrayOf(
leftRadius.dp(requireContext()).toFloat(),
leftRadius.dp(requireContext()).toFloat(),
topRadius.dp(requireContext()).toFloat(),
topRadius.dp(requireContext()).toFloat(),
rightRadius.dp(requireContext()).toFloat(),
rightRadius.dp(requireContext()).toFloat(),
bottomRadius.dp(requireContext()).toFloat(),
bottomRadius.dp(requireContext()).toFloat(),
), null, null)
)
shape.paint.color = color
this.background = shape
}

fun Int.dp(context: Context): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
this.toFloat(),
context.resources.displayMetrics
).toInt()
}

小技巧(应该算啊吧):当我们有icon需要适配深色模式的时候,可以把android:tint的值设置为?android:attr/textColorPrimary ,就不用自己做额外处理了


<vector android:autoMirrored="true" android:height="24dp"
android:tint="?android:attr/textColorPrimary" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">

<path android:fillColor="@android:color/white" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

SearchView背景色修改


可以看之前发的文章



MD3——SearchView自定义背景



效果参考上面的搜索筛选界面


滚动到指定位置(带偏移)


点击左上角的骰子图标,可以随机颜色(滚动到某一位置),通常我们使用recyclerView.scrollToPosition(int position)就可以实现。但是这个方法,会滚动到item的最边缘(红线位置),但是我希望他能够保留一定边距(绿色框框),看起来界面会和谐一点


image.png


解决办法如下:


private fun RecyclerView.scrollToPositionWithOffset(position: Int, offset: Int) {
(layoutManager as GridLayoutManager)
.scrollToPositionWithOffset(position, offset)
}

// 调用
binding.recyclerView.scrollToPositionWithOffset(
Random.nextInt(0, adapter.itemCount - 1),
16.dp(this)
)

用了kotlin扩展方法方便调用,这里的layoutManager根据实际情况来,我这里用列表到的是GridLayoutManager


小组件(App Widget)


提供了两种布局,小尺寸只显示颜色名称,大尺寸显示拼音和名称,效果如下:


Android小组件.jpg


iOS小组件.png


可能在部分系小尺寸统显示有问题,懒得搞了,这个组件大小搞的我脑壳疼,也没看到过什么好的解决方案,以下是我的配置:


// 31以下
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="57dp"
android:minHeight="51dp"
android:updatePeriodMillis="0"
android:previewImage="@drawable/appwidget_preview"
android:initialLayout="@layout/layout_wide_widget"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen">

</appwidget-provider>

// 31及以上
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:targetCellWidth="4"
android:targetCellHeight="2"
android:minResizeWidth="57dp"
android:minResizeHeight="51dp"
android:maxResizeWidth="530dp"
android:maxResizeHeight="450dp"
android:updatePeriodMillis="0"
android:previewImage="@drawable/appwidget_preview"
android:initialLayout="@layout/layout_wide_widget"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen"
android:widgetFeatures="reconfigurable">

</appwidget-provider>

因为布局比较简单,所以尺寸兼容效果相对好一点


主动刷新小组件


当我们app没有运行的时候,添加小组件是没有数据的,当我们打开app的时候,通知小组件更新


// 刷新 Widget
sendBroadcast(Intent(this, ColorWidgetProvider::class.java).apply {
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val ids = AppWidgetManager.getInstance(application)
.getAppWidgetIds(ComponentName(application, ColorWidgetProvider::class.java))
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
})

周期更新小组件


可通过配置updatePeriodMillis来设置时间,但是容易失效,所以使用WorkManager来通知更新,虽然WorkManager保证了周期执行,但如果app不在后台的话还是无法更新的,因为发送了广播app收不到,可能再加个服务就可以了,不加不加了


遗留的小问题


MIUI无法添加小组件


这段代码在MIUI上不生效,无法弹出添加小组件的弹窗


AppWidgetManager.getInstance(this).requestPinAppWidget(xxx)

如果添加该权限并授权,可以成功添加,但是无任何弹窗提示


<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />

当然最奇怪的还是我居然在MIUI的安卓小部件里找不到我自己的组件,我在原生都能看得到我的小组件的,也不知道是不是还需要配置什么,再一次头大


总结


这个app断断续续也写了好几个月,也也没啥功能还写了这么久。之前还看了下swiftUI,写了个iOS版本的,给我的感觉就是上手简单,写起来效率快多了,

其实这篇文章早就可以发了,就为了等app上架,可真煎熬。

个人开发者上架应用真的是难于上青天,对于安卓平台,国内一些主流应用市场(华米OV)都不对个人开发者开放了,要求低点的比如酷安、应用宝个人是可以上传的,但是需要软著,这又是一个头疼的事,申请基本一个月起步,除非花几百块找别人,三五天下证;

PS:现在App需要备案了,除非你不联网,应用宝就可以上架,酷安也要强制备案

ios也让我很难受,可能是我自己的问题,我注册流程走到付款了,当时想着先写完app再注册好了,就没付款,后来再去注册就提示账户存在问题,邮件联系后告诉我:



您的账号由于一个或多个原因,您无法完成 Apple Developer Program 的注册。



我想问清楚具体是什么原因,客服告知由系统判定,他们无法知道也无法干预,然后我寻思罢了,我再注册一个,还是失败,这次提示:



您使用另一 Apple ID 通过 Apple Developer App 验证了身份。要继续,请使用之前用于验证您身份的 Apple ID。



问号.jpeg


然后我又去把原来的账号注销掉,依旧无法注册成功...,最后无奈使用别人的信息注册了一个乛 з乛

所以,想注册苹果开发者的,注意最好是在同一个设备上一次性完成注册。


作者:FaceBlack
来源:juejin.cn/post/7294441582983626788
收起阅读 »

支持远程调试的 "vConsole"

web
背景 前阵子一直在做业务需求,是嵌在公司 APP 里的 H5。而且是跨地区协作,我在 A 城市,测试和产品都在 B 城市。 由于是 H5 项目,开发的时候一般都会实例化个 vConsole,方便查看项目的上下文信息。同时我想着当程序出现问题之后,测试小姐姐可以...
继续阅读 »

背景


前阵子一直在做业务需求,是嵌在公司 APP 里的 H5。而且是跨地区协作,我在 A 城市,测试和产品都在 B 城市。


由于是 H5 项目,开发的时候一般都会实例化个 vConsole,方便查看项目的上下文信息。同时我想着当程序出现问题之后,测试小姐姐可以直接截个 vConsole 的图给我,可以减少沟通时间。


痛点


后来发现一切都是想象之中,我们两个在沟通问题上依旧没少花时间!如果把程序出现的问题分级,那么会有:



  • 😄 简单问题:测试小姐姐 描述问题 发生的过程后,基本可以定位、解决;

  • 😅 中等问题:测试流程走不下去或者程序报错,这时候得查看调试信息去分析。此时需要测试小姐姐 截图 vConsole 上面显示的内容发我,但由于截的图并不一定是关键信息或者信息数据不够,导致这中间会产生几轮沟通;

  • 😥 复杂问题:遇到一些依赖外部信息或者奇奇怪怪的问题的时候,可能会 远程视频 操作测试机给我看,同时我会告诉她什么时候打开 vConsole 查看什么面板的信息。


可以看到只要问题牵扯到了项目的运行信息,前前后后就会导致很多沟通上的时间成本


不禁让人思考是什么原因导致的这个问题……


问题的本质


结合前面的描述我们得知,由于物理空间、跨地域的限制,程序的错误信息都是由测试人员转达给技术人员,不得不说这对测试人员有点勉为其难了,而另一方面造成问题的关键就在于此:技术人员无法和 Bug 直接来个正面交锋!


那么该如何解决这个「中间人」的问题呢?


这个问题的答案其实很简单,我们只要将浏览器的原生 API 进行一层包装将运行时调用的参数收集起来,然后再整一套类似控制台的 UI,最后整合成 SDK 处理参数 -> 中间层网络通信 -> UI 控制台展示的样子,开发同学直接和控制台上的 BUG 切磋,就能完美的解决这个问题!


虽然说起来简单,但是这一整套下来开发的工作量可不容小觑:



  • 包装原生 API 的 SDK

  • 负责通信的服务

  • 控制台 UI……


不用慌!开箱即用的 PageSpy 值得你拥有 😄!


PageSpy


Page Spy 是由货拉拉大前端开源的一款用于调试 H5 、或者远程 Web 项目的工具。基于对原生 API 的封装,它将调用原生方法时的参数进行过滤、转化,整理成格式规范的消息供调试端消费;调试端收到消息数据,提供类控制台可交互式的功能界面将数据呈现出来。


PageSpy是一个强大的开源前端远程调试平台,它可以显著提高我们在面对前端问题时的效率。以下是PageSpy的一些主要特点:



  • 一眼查看客户端信息 PageSpy 会对客户端的运行环境进行识别,其中系统识别支持 Mac / iOS / Window / Linux / Android,浏览器识别支持谷歌、火狐、Safari、Edge、微信、UC、百度、QQ;

  • 实时查看输出: PageSpy 可以实时捕获并显示程序输出,包括 Console、Network、Storage 和 Element。这使开发人员能够直观地了解页面的外观和行为,无需依赖用户的描述或截图。

  • 网络请求监控: PageSpy 还可以捕获和显示页面的网络请求,有助于开发人员更好的查看与后端的交互。

  • 远程控制台: PageSpy 支持远程调试JavaScript代码,允许开发人员执行 JavaScript 代码在用户的浏览器上运行。这对于排查特定问题或测试代码修复非常有帮助。

  • 跨浏览器兼容性: SDK 可以在各种主流浏览器中运行,确保你可以检查和调试不同浏览器上的问题。

  • 用户体验提升: 通过快速识别和解决前端问题,PageSpy可以显著提升用户体验,减少用户因前端问题而受到的不便。


使用 PageSpy 进行远程调试


使用PageSpy进行远程调试是相对简单的。以下是一些基本步骤:



  • 部署PageSpy: 首先,PageSpy 提供了 Docker、Node 和 Release 的一键部署方案,点击查看

  • 实例化 SDK: PageSpy 成功部署后,你可以在项目中引入对应的 SDK 文件并进行实例化,它提供了多场景类型的参数,以便于用户对它的行为进行定制。

  • 实时监控页面: 之后,你可以实时查看页面的各种数据,这有助于你直观地理解页面的问题。

  • 监控网络请求: 你还可以查看所有的网络请求,包括请求的URL、响应代码和响应时间。这可以帮助你识别与后端通信相关的问题。

  • 解决问题: 借助PageSpy提供的信息和工具,你可以更快速地定位和解决前端问题,从而提高用户体验。


相关截图


门户首页


image.png


待调试列表


image.png


调试界面


image.png


image.png


结语


前端远程调试对于快速而准确地解决前端问题至关重要。Page Spy 作为一个强大的开源工具,支持开箱即用,为开发人员提供了一个高效的方式来查看用户端的页面输出、网络请求和执行远程调试。它有助于加速问题的定位和解决,减少了对用户反馈和日志的依赖,提高了整体开发效率。除了解决跨地区协同的场景之外,还覆盖了本地开发时的调试 H5 的场景。


希望本文能够帮到大家对 PageSpy 有个初步的认识,感谢阅读。


作者:Blucas
来源:juejin.cn/post/7298161887882592310
收起阅读 »

把jsp重构成vue

web
记录一次重构的经历与感想!望自己将来开发之路越走越顺利。 话说,我在入职之前,公司一直使用的jsp技术,并结合jQuery来处理前端页面逻辑。 但在我入职之后不久,我们领导就要求把它重构成vue。 这时,我对jsp根本不熟,业务也没摸清楚。且在这个关键时刻,另...
继续阅读 »

记录一次重构的经历与感想!望自己将来开发之路越走越顺利。


话说,我在入职之前,公司一直使用的jsp技术,并结合jQuery来处理前端页面逻辑。


但在我入职之后不久,我们领导就要求把它重构成vue。


这时,我对jsp根本不熟,业务也没摸清楚。且在这个关键时刻,另一个前端因为离家较远直接离职了!


这个担子竟然压到我一个人身上,心里一万匹草泥马奔过。。。


但也没办法,只能赶鸭子上架,怀着极其忐忑的心情进入了开发阶段,当然也有点兴奋,于我而言这也是一个难得的实践机会!


我把这次重构的经历大致分成四个阶段:(1)摸清楚jsp项目的代码(2)用Vue CLI把项目工程搭建起来(3)整理业务逻辑(4)写vue代码


1.摸清楚jsp项目的代码


老项目中的jsp文件是长这样子:


jsp页面.png


JSP(JavaServer Pages)技术是一种基于Java的Web应用程序开发技术,它允许开发人员将Java代码嵌入到HTML中,以动态生成Web页面。


虽然这是一个比较古老的技术,我也是一脸懵圈,但秉承着前端框架无非是对html、css、js的结合的原则,我硬着头皮读代码。


经过一段时间的浸泡,并且在分析后,我发现,只需要稍微懂一点jsp技术,其实就完全可以读懂jsp了,jsp页面最大的特点就是可以通过java注入参数,除此,它和所有前端框架一样是由三部分组成:



  • html部分,就把jsp文件看成html文件,虽然里面注入了一些参数,但这些参数可以让后端通过接口返回,再拿去渲染就行了

  • css部分,老项目和vue基本可以共用

  • js部分,新老项目的功能是一样的,老项目中用jquery实现的,再拿vue去实现一遍即可


基于这些,此时我对把jsp重构成vue已经有了一些把握,总体原则大概是:对html、css、js这三部分,可以重用的部分就重用,不能重用的部分就重写。


2.用Vue CLI把项目工程搭建起来


使用脚手架,很快就搭建好了项目,都是傻瓜式的操作,这个没什么好说的,给大家看一下目录结构:


vue目录结构.png


3.整理业务逻辑


我后知后觉才发现,重构最难的不是编码,而是业务逻辑


我对业务逻辑的信息来源有两个:一是看老项目的源代码,二是问其他老员工


但是前者效率极低,后者又困难重重


为何这么说,参考以下两点:



  1. 程序员最痛苦的莫过于阅读别人写的代码

  2. 作为新人,得遵守一些职场潜规则


但又没办法,只得蛮力通关了,忍受着巨大的痛苦,一方面得加班阅读代码,一方面要虚心求教老员工(我司的环境大概是,对业务越熟的人,脾气就越大,问题问多了,他们会很不耐烦,对此,我做了很多心理建设)。


后来,我也是整理出了一份前端业务逻辑资料,然后被放到了公司公共文件夹里,被后来的员工永久查看学习🐶!


前端业务逻辑整理.png


4.写vue代码


最后阶段就是编码了,我把它分为前期和后期。


前期攻坚难点,重点关照那些难实现的功能,后期画页面,要保持效率,基本要能够一天画2个页面。


后来翻看了一下代码库的提交记录,从第一行代码的提交,到进入测试,历时3个月。


编码阶段是枯燥乏味的,前面靠"蛮劲"可以挺过去,但是现在每天得靠"有恒"二字给自己打气🐪!


然后,终于把项目重构完成了,我也长吁了一口气!


但是事情并没有我想象的那么简单,更恶心的事情来了,bug颇多!


短短几天,测试就提了几百个问题单!


问题单数.png


泪奔啊!蛮劲用完了,恒心也消磨的差不多了!但是问题还是不依不饶的出现。。。


可能我这人就属于那种打不死的小强,想着好不容易坚持到这一步,无论如何我都要拿下它!


于是又向bug们发起了"猛攻"!


又渡过了一段漫长且艰难的解bug时期。。。


终于把bug也解完了,我和测试都长吁一口气!


什么?代码要想顺利上线,还要处理CI?


最后,我精疲力竭的处理完了一千多个CI问题,也终于体会到了,有时候,不逼自己一把,你永远不知道自己可以做到什么程度!


CI数.png


至此,项目终于上线了!


这次重构经历,我思考了这么几个问题:




  1. 公司为什么要重构这个项目?


    答:这个项目本来用户量大,将来还有大量的新需求要接,但是技术架构上已经落后了,如果不重构,将来搞不定新需求,老代码也不好维护,毕竟新来的员工会jsp的没多少。




  2. 重构的重点在哪?


    答:在于业务。业务是大于技术的,特别是新员工,别急着钻研项目中用到了哪些技术,还是多花点时间了解业务吧!




  3. 重构的难点在哪?


    答:技术上的困难总有办法,但是沟通上的困难却似不可逾越的鸿沟,因为工作的日常除了编码,更多的是:和产品互怼、与测试撕逼、向领导交差,所以,程序员们,提升情商吧!




我觉得,最重要的,是进行心态建设,遇到难关不要怕,永远相信自己可以挺过去,毕竟知识是死的,人是活的,只要我们"有恒",就算再难的东西,用"蛮劲"去"猛攻",终将拿下!


作者:玄玄子
来源:juejin.cn/post/7298167437526269952
收起阅读 »

破涕为笑,一个node中间层bug我让同事的名字出现在全球用户的页面上

web
前言 近期遇到了一个线上故障,排查花了很多时间精力,在bug复现过程中,我还不小心让同事的名字出现在了全球用户的页面上,从“一把辛酸泪”到“破涕为笑”,感觉特别有意思。本文中的代码、描述都过滤了敏感信息,以简单demo的形式复现该故障。 如下图,不管用户搜索啥...
继续阅读 »

前言


近期遇到了一个线上故障,排查花了很多时间精力,在bug复现过程中,我还不小心让同事的名字出现在了全球用户的页面上,从“一把辛酸泪”到“破涕为笑”,感觉特别有意思。本文中的代码、描述都过滤了敏感信息,以简单demo的形式复现该故障。


如下图,不管用户搜索啥词,每个用户页面回显的都是sivan
image.png


业务功能描述


站点的搜索功能,搜索功能会根据业务场景继续细分为a搜索,b搜索...,每种搜索在node中间层走的可能是不同的链路。


如下图展示,a搜索回显了default


image.png


b搜索回显了sang和for


image.png


image.png


故障描述



  1. 偶现,但是触发频率很高

  2. 现象为在x国家站点上,不管用户搜索什么内容,页面回显的大概率是一串固定的字符串

  3. 只有在x国家站点会出现该故障,其他国家站点没有出现

  4. 测试环境无法复现,只有线上环境会出现该故障


image.png


image.png


排查


炒面代码分析


从线上的故障现象来看,像是搜索词被替换掉了,分析看客户端页面下发的参数是没有任何问题的,找搜索服务的后端协助,后端说他们接收到的搜索词就已经是有问题的搜索词了。


初步猜测是被类似xssFilter之类的转换函数替换掉了原来的搜索词,或者node中间层有某一条链路的代码把搜索词改掉了。于是把node中间层的搜索链路的相关代码都研究了好几遍,通过关键字搜项目全局,把每个可疑的地方都看了,感觉代码逻辑写的都没毛病(node中间层的代码链路写得跟炒面一样,看得头都大了)。


没办法,代码分析不出来问题所在,测试环境又无法复现,只能在代码分析的基础上,把每个有可能改到搜索词的可疑地方打上日志,在搜索链路的一些比较关键的执行地方也打上日志,重新发版,来辅助排查。


// 线上打日志的时候需要注意加条件限制,不然每个用户请求都打日志,一下子就打爆了
if (req.query.sdebug === 's') {
logger.warn({ /** data */ })
}

抓住日志这根救命稻草


之前也有猜测,可能是网关啊、waf啊把请求拦截下来更改了搜索词,所以我们在请求入口那里也打了日志。从日志上来看,从中间层入口进来时,此刻的搜索词还是正常的,说明不是网关、waf搞的鬼。第一次的日志帮助我们缩小了排查范围,但是还不能分析出来,还需要再补充一些日志,意味着还要再发版,没办法,就是这么麻烦。


考验你js能力的时候到了


日志只是一种辅助手段,帮你记录异常数据,缩小排查范围。是否能从一堆代码中找出那一行有问题的代码就要看你自己了,我把有问题的代码写成一个demo了,展示在下面,你能分析出来问题所在吗?


const express = require('express')
const app = express()

const aConfig = Object.freeze({
info: { word: 'default' },
getWord ({ word }) {
return word
},
})
const bConfig = Object.freeze({
info: {},
getWord ({ req, word }) {
// 日志记录到这里word是'sivan',正常word应该是undefined,取的是req.params[0]才对
return word || req.params[0]
},
})

const setRequestData = ({ info: { word }, getWord }) => {
return (req, res, next) => {
word = word || req.query.word
res.end(getWord({ req, word })) // 回显搜索词
}
}

const getHandler = (config) => {
return setRequestData(config)
}
const aSearch = getHandler(aConfig)
const bSearch = getHandler(bConfig)

app.get('/a-search', aSearch)
app.get(/^\/b-search\/([^\/]+)\/?$/, bSearch)

app.listen(2333, () => {
console.log('run')
})

开始揭开谜底


const setRequestData = ({ info: { word }, getWord }) => {
return (req, res, next) => {
word = word || req.query.word // 罪魁祸首
res.end(getWord({ req, word }))
}
}

在a搜索链路中,word是有值的,为'default'。
在b搜索链路中,word和req.query.word都应该是undefined,所以b搜索链路传给getWord的word应该是undefined才对。


观察setRequestData这个函数的实现,它对config解构出了word变量,然后返回了一个中间件函数,word变量的作用域是在setRequestData的函数作用域里的,setRequestData函数只会执行一次,而中间件函数在每一次请求中都会执行。


中间件函数使用了word变量,这就创建了一个闭包,闭包使得word变量可以长期存储和被访问。


复现步骤如下:



  1. 我们第一次输入http://localhost:2333/b-search/sang?word=sivan(拼接上?word=sivan),回显sivan

  2. 之后我们每次输入http://localhost:2333/b-search/xxx,xxx为任意字符串,都会回显sivan


setRequestData函数只会执行一次,中间件函数每一次请求都会执行,所以当我们第一次输入http://localhost:2333/b-search/sang?word=sivan时,word变量被赋值为req.query.word并因为闭包被存储起来,等下一次输入时,由于word = word || xx,会先取存储的word,这就导致了每一次输入都会回显sivan。


改动思路如下截图:虽然闭包还存在,但是这样修改就不会让闭包的变量值被意外篡改,导致意料之外的结果了。


image.png


归因


这个问题其实挺严重的,搜索功能直接没用,用户都搜不了内容了,打工人打工不容易,哭泣。也挺有意思的,我只要在链接后面拼接?word=sivan就可以让全球的用户看到同事的名字,扬名立万(不止万了,起码千万了),破涕为笑。


为什么是偶现的呢?因为是集群,有很多服务器节点,每一次请求都可能打到不同的节点上,你输入b-search/xx?word=sivan时,请求只会打到其中一个节点上,只会污染那一个节点上的那个长期存储的word变量。所以被污染的集群节点有问题,没被污染到的集群节点就没问题。


为什么只有x国家站点出现该故障?测试环境没出现过该故障?因为这个故障的触发条件比较苛刻,必须输入b-search/xx?word=sivan才会触发,而正常情况下b搜索链路是不会拼接word=sivan这个query参数的。猜测最开始之时,就是有人在x国家站点因为一些原因输入了http://localhost:2333/b-search/sang?word=sivan引发问题,其他国家站点和测试环境没有输入就没有问题。


触发条件这么苛刻,是谁触发的呢?



  • 有可能是用户,毕竟几千万用户在用搜索,什么情况在用户那都会发生

  • 有可能是测试人员,测试在线上环境偶然拼接了这个参数

  • 有可能是黑客或者友商(概率很低,因为只影响了部分站点)

  • 前端开发人员,实现了这么一段如此隐晦的bad代码,等哪一天加班太多,心里不爽了,回家敲几个字拼接url访问,网站功能立马下线。


删库跑路的梗大家都耳熟能详,我们前端不止是会在svg里面、console里面吐槽公司,我们还可以在node中间层里写bad bad的代码哦,而且还很难测出来,事后归因到前端身上。


作者:前端爆冲
来源:juejin.cn/post/7294852698460471308
收起阅读 »

记录一次接口加密的实现方案

web
隔了三个月才写了这篇文章,实在是莫得时间去写,踩坑很多但是输出很少,后面有时间也会多记录一些自己的踩坑经历,要是能给各位同学有所帮助那是最好的了,废话不多说,进入正题了。 背景介绍 由于部门业务体量正在提升,为了防止数据盗取或者外部攻击,对接口进行加密提上了日...
继续阅读 »

0002.jpg
隔了三个月才写了这篇文章,实在是莫得时间去写,踩坑很多但是输出很少,后面有时间也会多记录一些自己的踩坑经历,要是能给各位同学有所帮助那是最好的了,废话不多说,进入正题了。


背景介绍


由于部门业务体量正在提升,为了防止数据盗取或者外部攻击,对接口进行加密提上了日程,部门的大佬们也讨论了各种加密方式,考虑各种情况,最终敲定了方案。说到我们常用的数据加密方法,方式是各种各样的,根据我们实际的业务需求,我们可以选择其中的一种或者几种方式方法进行数据加密处理。




  • 加密方法:常用的AES,RSA,MD5,BASE64,SSL等等;

  • 加密方式:单向加密,对称加密,非对称加密,加密盐,数字签名等等;



首先我们来简单分析一下上面说到的这几种加密有什么区别吧:




  • AES加密:对称加密的方法,加解密使用相同的加密规则,密钥最小能够支持128,192,256位(一个字节8位,后面我使用的是16位字符);

  • RSA加密:非对称加密的方法,加解密使用一对公钥私钥进行匹配,客户端使用公钥加密,服务端使用私钥进行解密;

  • MD5加密:单向加密,加密后不可解密,只能通过相同的数据进行相同的加密再与库中数据进行对比;

  • BASE64:一种数据编码方式,伪加密,把数据转化为BASE64的编码形式,通过A-Z,a-z,0-9,+,/ 共64个字符对明文数据进行转化;

  • SSL加密:https协议使用的加密方式,使用多种加密方式进行加密(具体使用哪些,我也不了解,感兴趣的同学可以去搜一下告诉我哈);



想要详细了解各类加密方式方法的同学,可以自行百度一下哈,这里就不进行赘述了,之后就来详细讲一下本次使用的加密方式。本次为了更加全面加密,使用了AES,RSA,以及加密盐,时间戳,BASE64与BASE16转化等方式进行加密处理。


请求体AES加密


请求体使用AES的对称加密方式,每次接口请求会随机生成一个16位的秘钥,使用秘钥对数据进行加密处理,返回的数据也会使用此秘钥进行解密处理。


import CryptoJs from 'crypto-js'// AES加密库
import { weAtob } from './weapp-jwt' // atob方法

// 请求体加密方法
export const encryptBodyEvent = (data, aeskey, isEncryption) => {
// 请求体内容
const wirteData = {
data: data, // 接口数据
token: Taro.getStorageSync("authToken"), // token 校验
nonce: randomNumberEvent(32), // 32位随机数,接口唯一随机数,可查询服务日志
timestamp: new Date().getTime, // 时间戳,用于设置接口调用过期时间
}
const encryptBodyJson = CryptoJs.AES.encrypt(JSON.stringify(wirteData), CryptoJs.enc.Utf8.parse(aeskey), {
mode: CryptoJs.mode.ECB,
padding: CryptoJs.pad.Pkcs7
}).toString()
// 判断接口是否需要加密
// 服务接收BASE16数据,Base64toHex方法为BASE64转化为BASE16方法
return isEncryption ? Base64toHex(encryptBodyJson) : wirteData
}

// BASE64转化BASE16方法
function Base64toHex (base64) {
let raw = weAtob(base64)
let HEX = ""
for (let i=0; i < raw.length; i++) {
let _HEX = raw.charCodeAt(i).toString(16)
HEX = (_HEX.length == 2 ? _HEX : "0" + _HEX)
}
return HEX
}

// 生成n位随机数,默认生成16位
function randomNumberEvent (length = 16) {
let str = ""
let arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
// 随机产生
for(let i=0; i < length; i++){
let pos = Math.round(Math.random() * (arr.length-1));
str += arr[pos];
}
return str
}


  • mode是分组加密模式:有五种模式(ECB、CBC、CFB、OFB、CTR),这里我们使用最简单的ECB模式,明文分组加密之后的结果将直接成为密文分组,对其他几种模式感兴趣的可以去搜索一下几种模式的区别;

  • padding是填充模式:正常的加密后的字节长度不可能刚刚好满足固定字节的对齐(块大小),所以需要进行一定的填充,常用的有三种模式(PKCS7、PKCS5、Zero,还有其他模式),这里我们使用的是PKCS7模式。假设数据长度需要填充n个字节才对齐,那么填充n个字节,每个字节都是n;假设数据本身就已经对齐了,则填充一个长度为块大小的数据,每个字节都是块大小;

  • weAtob即小程序使用的atob方法:atob是JS的一个全局函数,用于将BASE64编码转化为原始字符串,在正常的H5项目中atob可以直接使用,但是在小程序中此方法不可用,因此使用一个手动实现的方式(文件就不上传了,电脑是加密的,上传也是乱码,网上也是能找到类似的方法);

  • timestamp是用于防止过期调用:这里的时间是为了展示方便直接使用客户端时间,实际是会调用一个服务端的接口获取服务器时间进行时间校准,防止客户端手动修改时间,服务端设置过期时间,会根据传入的时间判断是否过期;


请求头RSA加密


看完上面的请求体加密,我们会想到一个问题,就是我们的aesKey是客户端随机生成的,但是服务端也需要这个aesKey进行数据的加解密,那么我们通过什么形式传给服务端呢?因此我们在请求头中设置一个secret-key字段,使用RSA中的公钥对aesKey进行加密,服务端使用对应私钥进行解密;


// import JSEncrypt from 'jsencrypt' // RSA加密库,小程序不支持
import WxmpRsa from 'wxmp-rsa' // RSA加密库,小程序支持

let public_key = 'xxxxxxxxxxxxxxxx' // 公钥
// 请求头加密方法
export const randomKeyEvent = (aesKey) => {
// JSEncrypt方法小程序不可用
// const RSAUtils = new JSEncrypt() // 新建JSEncrypt对象
// RSAUtils.setPublicKey(public_key) // 设置公钥
// return RSAUtils.encrypt(aesKey).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '')

const RSAUtils = new WxmpRsa() // 新建WxmpRsa对象
RSAUtils.setPublicKey(public_key) // 设置公钥
// 进行RSA加密后,生成字符串中的部分特殊字符在服务端会被自动转化为空格,导致解密失败,所以先进行转换处理
return RSAUtils.encryptLong(aesKey).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '')
}


  • JSEncrypt在小程序不可用是由于库里面存在window对象以及navigator对象,但是小程序没有对应的方法,所以使用了一个优化后的wxmp-rsa库;

  • replaceAll处理字符是因为RSA加密后,生成字符串中的部分特殊字符传给服务端会被自动转化为空格,导致解密失败,所以需要进行转换处理,为了兼容低版本replaceAll方法不支持可使用replace加正则进行替换;


返回体AES解密


服务端返回的数据内容使用了相同的AES加密方法,因此也需要使用AES进行数据解密处理,并且返回的数据是BASE16,因此还需要进行一次编码转换处理;


// 返回体解密方法
export const decryptBodyEvent = (data, aeskey) => {
// HexToBase64为BASE16转化为BASE64方法
const responseData = CryptoJs.AES.decrypt(HexToBase64(data), CryptoJs.enc.Utf8.parse(aeskey), {
mode: CryptoJs.mode.ECB,
padding: CryptoJs.pad.Pkcs7
}).toString(CryptoJs.enc.Uth8)
return JSON.parse(responseData)
}

// base16转base64 网上找个一个方法,应该有其他简单的实现方式
function HexToBase64 (sha1) {
var digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
var base64_rep = ""
var ascv
var bit_arr = 0
var bit_num = 0

for (var n = 0; n < sha1.length; ++n) {
if (sha1[n] >= 'A' && sha1[n] <= 'Z') {
ascv = sha1.charCodeAt(n) - 55
} else if (sha1[n] >= 'a' && sha1[n] <= 'z') {
ascv = sha1.charCodeAt(n) - 87
} else {
ascv = sha1.charCodeAt(n) - 48
}

bit_arr = (bit_arr << 4) | ascv
bit_num += 4
if (bit_num >= 6) {
bit_num -= 6
base64_rep += digits[bit_arr >>> bit_num]
bit_arr &= ~ (-1 << bit_num)
}
}

if (bit_num > 0) {
bit_arr <<= 6 - bit_num
base64_rep += digits[bit_arr]
}
var padding = base64_rep.length % 4
if (padding > 0) {
for (var n = 0; n < 4 - padding; ++n) {
base64_rep += "="
}
}
return base64_rep
}

封装接口


因为是小程序项目,使用的是Taro框架进行封装的,Vue中使用axios封装其实也是类似的,还封装了一套ajax的方法,除了接口这里封装有区别,加密都是类似的。


const baseUrl = 'https://xxx.xxx.com' // 接口请求头

// Taro封装接口方法
export const requestEncHttp ({url, data, isEncryption = true}) => {
// 每次调用都会随机生成一个动态的aesKey,防止接口被复用
const aesKey = randomNumberEvent()
return new Promise((resolve, reject) => {
Taro.request({
method: "POST",
header: {
"content-type": "application/json",
"secret-key": isEncryption ? randomKeyEvent(aesKey): ''
},
dataType: 'text',
data: encryptBodyEvent(data, aesKey, isEncryption),
url: baseUrl + url,
success: (result) => {
if(result.status === 200) {
resolve(isEncryption ? decryptBodyEvent(result.data, aesKey) : JSON.parse(result.data))
} else {
reject(result)
}
}, fail: (err) => {
reject(err)
}
})
})
}


  • dataType使用text:ajax没有此问题,Taro框架会出现接口有返回数据,但是在success中接收不到数据,因为数据是BASE16形式,Taro封装的数据返回格式默认应该是JSON的,所以要单独设置一下。


总结


加密的方式有很多,这篇文章也只是浅尝即止,想更加详细了解的同学可以再搜一些大佬们的总结文章,我这里也只是结合业务做了一点总结,标注一下踩坑点;



  • 刚开始本来是准备一个月最少写一篇的,但是由于八月份刚换工作,不太有时间去写,所以也是一直拖着;而且是用公司电脑写的这篇文章,所以代码都没有直接粘贴过来,可能会存在疏漏,请多多包涵哈;


作者:追风筝的呆子
来源:juejin.cn/post/7298160530291490828
收起阅读 »

拖拽API的简单应用

web
我们在实际开发中经常能遇见拖拽的运用场景,比如说拖拽排序、拖拽删除等,本文将以实现一个简单的课程表来进行拖拽API的简单应用,帮助大家复习一下一些基础知识。 相关拖拽事件 实现一个元素拖拽,我们只需要在HTML标签设置draggable为true <...
继续阅读 »

我们在实际开发中经常能遇见拖拽的运用场景,比如说拖拽排序、拖拽删除等,本文将以实现一个简单的课程表来进行拖拽API的简单应用,帮助大家复习一下一些基础知识。


tutieshi_640x390_9s.gif


相关拖拽事件


实现一个元素拖拽,我们只需要在HTML标签设置draggabletrue


 <div class="left">
<div draggable="true" class="color1 item">语文</div>
<div draggable="true" class="color2 item">数学</div>
<div draggable="true" class="color3 item">英语</div>
<div draggable="true" class="color4 item">音乐</div>
<div draggable="true" class="color5 item">政治</div>
<div draggable="true" class="color6 item">历史</div>
<div draggable="true" class="color7 item">体育</div>
</div>

我们设置了拖拽属性,在拖动的过程中我们会触发很多事件


// 拖动开始
container.ondragstart = (e) => {
console.log('start', e.target)
}

// 拖动覆盖
container.ondragover = (e) => {
console.log('over', e.target)
}

// 拖动进入
container.ondragenter = (e) => {
console.log('enter', e.target)
}

// 拖动结束
container.ondrop = (e) => {
// 一般div、td是不允许有元素置于他们上面,在ondragover设置阻止冒泡
console.log('drop', e.target)
}

如上,我们在这个应用主要用到了这几个拖拽事件,其中要特别注意的是ondrop事件,因为很多的HTML标签是不允许有其他元素覆盖在他们上面的,我们在案例中最外层用了div标签,所以必须要设置阻止冒泡才能让该事件生效


设置拖拽鼠标样式


如效果图所演示,我们在新增课程的时候,鼠标呈现的是一个加号的状态,在移除时又是一个简单的鼠标样式。这里我们是通过datasetondragstart设置相关属性来进行动态实现的


    <div class="left">
<div data-effect="copy" draggable="true" class="color1 item">语文</div>
<div data-effect="copy" draggable="true" class="color2 item">数学</div>
<div data-effect="copy" draggable="true" class="color3 item">英语</div>
<div data-effect="copy" draggable="true" class="color4 item">音乐</div>
<div data-effect="copy" draggable="true" class="color5 item">政治</div>
<div data-effect="copy" draggable="true" class="color6 item">历史</div>
<div data-effect="copy" draggable="true" class="color7 item">体育</div>
</div>

container.ondragstart = (e) => {
// 设置拖拽鼠标样式 默认值为move
e.dataTransfer.effectAllowed = e.target.dataset.effect
}

设置拖拽背景色


依旧根据设置的datakey,并检索父级,通过ondragenter事件动态插入class,实现背景色的显示


  <div class="left" data-drop="move">
<div data-effect="copy" draggable="true" class="color1 item">语文</div>
<div data-effect="copy" draggable="true" class="color2 item">数学</div>
<div data-effect="copy" draggable="true" class="color3 item">英语</div>
<div data-effect="copy" draggable="true" class="color4 item">音乐</div>
<div data-effect="copy" draggable="true" class="color5 item">政治</div>
<div data-effect="copy" draggable="true" class="color6 item">历史</div>
<div data-effect="copy" draggable="true" class="color7 item">体育</div>
</div>


<tr>
<th rowspan="4" class="span">上午</th>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
</tr>


function getDropNode(node){
while(node){
if(node?.dataset?.drop){
return node
}
node = node.parentNode
}
}

function clearDropStyle(){
const dropNodes = document.querySelectorAll('.drop-over')
dropNodes.forEach((node) => {
node.classList.remove('drop-over')
})
}

container.ondragenter = (e) => {
clearDropStyle()
const dropNode = getDropNode(e.target)
if(!dropNode){
return
}
if( e.dataTransfer.effectAllowed === dropNode?.dataset?.drop){
dropNode.classList.add('drop-over')
}
}

实现新增删除


根据一开始的设想,我们是新增了dataset进行同类别的有效拖拽,依旧进行比较,根据情况新增、删除节点


let source;

container.ondragstart = (e) => {
// 设置拖拽鼠标样式
e.dataTransfer.effectAllowed = e.target.dataset.effect
source = e.target
}


container.ondrop = (e) => {
// 一般div、td是不允许有元素置于他们上面,在ondragover设置组织冒泡
console.log('drop', e.target)

clearDropStyle()
const dropNode = getDropNode(e.target)
if(!dropNode){
return
}
if(e.dataTransfer.effectAllowed !== dropNode.dataset.drop){
return
}
if(dropNode.dataset.drop === 'copy'){
dropNode.innerHTML = ''
const cloned = source.cloneNode(true)
cloned.dataset.effect = 'move'
dropNode.appendChild(cloned)
}else{
source.remove()
}
}

我们在ondrop是不能拿到拖拽的节点的,设置一个全局变量,在ondragstart中保存节点,同时在复制完节点后要将其dataset-effect改成move


作者:_初七
来源:juejin.cn/post/7297908859176681484
收起阅读 »

【复盘】2023年写过的bug

前言 在默默的算着,2023年300多天的日子已经悄然无息的从我手中流走,还没来得及细细的品味,2023年即将逝去。在最近工作中,遇到了两个线上比较棘手的bug,今天,就对自己2023年写过的bug来个复盘吧 2023年写过的bug 截至今日,以下数据是202...
继续阅读 »

前言


在默默的算着,2023年300多天的日子已经悄然无息的从我手中流走,还没来得及细细的品味,2023年即将逝去。在最近工作中,遇到了两个线上比较棘手的bug,今天,就对自己2023年写过的bug来个复盘吧


2023年写过的bug


截至今日,以下数据是2023年每月我解决操作过的bug数据分布:


image.png
以上大多数是测试阶段的bug,个别是线上的。作为一个码农,自认为在提测前对自己写过的功能保持着一个比较负责任的心态,尽量保持高质量的提测。但,没有谁能保证不写 bug,不出错的。我们要做的,是在bug出现以后,找到问题的根源,解决问题,避免同类问题再次发生。


解决一线上纰漏bug流程复盘


1、bug现场


前几天公司为迎接双十一,搞满减满折活动促销,对全场商品(除个别商品)仅仅开启每满200-70的满减满折活动,收到反馈,个别商品列表活动的标签显示错误,列表有500个左右商品,大概5个左右商品标签差个每字


image.png


2、代码逻辑


后台创建满减满折活动时,会将哪些商品能参加这个活动具体的标签写入缓存中,C端查询商品列表时,会去商品查询缓存,查看这个商品正在参加的进行中的活动的标签返回给前端展示。由于后台满减活动的配置逻辑是我做的,标签也是我写入缓存的,看到这个反馈,我心中第一反应,满减活动创建添加商品标签的缓存逻辑代码有bug?


3、排查思路



  • 测试环境创建一个满减满折活动,看是否个别商品满减标签显示错误,发现没有问题,复现失败

  • 让测试协助,是否测试环境能否复现此bug,测试也无法复现

  • 由于这块我没有特意代码补充日志,公司框架也没有全局去拦截请求的入参,靠日志排查之路走不通

  • 怀疑是否还开启了别的活动,商品是否还参与别的活动,冲突导致,查看数据库数据,发现目前仅仅就开启着这一个活动

  • 怀疑是否曾经开启过满200-70的活动,发现也没有开启过

  • 去看数据库500个左右的商品,5个标签错误的商品找出规律,没有发现

  • 活动创建,后编辑该了类型?发现活动创建成功后,也没有编辑过

  • 活动创建前端参数错误?回头着重去看我写的那块代码,再三斟酌,没有发现有问题,就算前端参数错误,为啥就那几个商品少了个每次,此路也不通

  • 想过并发,没有道理,因为创建活动就当时一个人操作

  • 想过很多人线上复现不了的bug最常见的解释,网络波动,我那也不应该,要么写入失败,不会写入成功,少一个字

  • 想过redis的缓存数据被人工手动改过?毕竟是线上的,不会随便改,也不应该,概率可以忽略

  • 既然C端的标签是从缓存拿去的,无奈我项目全局搜索所有用到设置商品缓存代码逻辑,一个个前后调用去看,最后给我看到了查询满减活动表,然后设置标签的代码,一步步定位最终最外层controller代码,才发现商品编辑那里有修改商品缓存的逻辑,只要涉及到商品编辑,那里会先删除这个商品的所有缓存,然后在添加缓存,由于那块组装商品标签逻辑和我创建活动添加标签逻辑不一致,少了一个类型判断导致


由于商品管理那块,我没有参与过需求的评审,开发、设计,所以从始至终排查问题都没有想到过那里会影响我做的满减满折活动的商品标签


4、问题解决


任何一个bug,能复现找到问题的根源就好解决,最棘手的时没有人能复现,又没有日志,完全去猜各种可能性


image.png


5、总结



  • 针对一些难以复现的bug或者遇到的技术问题 ,找到根本原因很重要

  • 要多了解业务,把各种变更造成的影响,要能提前预知到

  • 如果项目框架没有完善的全局日志记录,重要地方适当打印下日志


作者:千呼万唤始出来
来源:juejin.cn/post/7297917491795902476
收起阅读 »

面试官问我唯一ID如何生成

由于简历上,写了分库分表,还写了精通微服务和分布式系统,然后被面试官一顿蹂躏了这里面的技术细节;尤其给我印象最深的问题是,在分布式环境下,你们数据的唯一ID是如何去生成的? 当时条件反射,无脑的回答到,通过数据库自增方式生成的唯一ID;面试官听了后,叫我回家等...
继续阅读 »

由于简历上,写了分库分表,还写了精通微服务和分布式系统,然后被面试官一顿蹂躏了这里面的技术细节;尤其给我印象最深的问题是,在分布式环境下,你们数据的唯一ID是如何去生成的?


当时条件反射,无脑的回答到,通过数据库自增方式生成的唯一ID;面试官听了后,叫我回家等通知,然后就没有然后了。。。。。。。


目前业界有哪些唯一ID的生成方式了?


大概有5种。如下图


1698825367530.png


UUID模式


uuid全称是通用唯一识别码(Universally Unique Identifier)。底层是通过mac地址+时间+随机数进行生成的128位的二进制,转换为16进制字符串后,长度为32位。

uuid的生成非常简单, 如果用java代码生成,那只需要一行代码


public static void main(String[] args) {      
String uuid = UUID.randomUUID().toString().replaceAll("-","");
System.out.println(uuid); }

优点: uuid生成的ID 能满足唯一性;并且不依赖任何中间件;生成速度超快。


但是uuid的缺点也很明显: 生成的ID太长了,字符长度有32位

在业务系统里,一般ID是要进行存储到库里的,并且很有可能会作为常用的查询条件,比如存储到mysql,比int32的4字节或者int64的8字节来讲,太占空间;


第二个缺点:就是生成的ID是无序的,不是递增的;这样在把ID做为索引字段,在底层插入索引时,还会引起索引节点的分裂,影响插入速度。 所以用UUID生成的ID,对数据库来说很不友好


那么UUID是不是就没有业务使用场景了?

并不是的,像很多大厂的日志链路追踪的ID,就是基于UUID进行生成的。


redis 自增模式


大家都知道 在redis里有一个INCR命令,能从0到Long.maxValue-1进行单一连续的自增,最大19位;9位数字就是亿基本的数了,还剩10位,假设每天1千W的订单数据,即1千W个ID,能支持25亿年;距离地球毁灭时间还差点


redis id的自增代码也很简单,不详述。


优点:

redis 自增生成的ID 能满足唯一性也能满足单调自增性。做底层数据存储或者索引的时候,对数据库也很友好。生成速度还飞快,因为都是在内存中生成。


缺点:

redis 自增ID的缺点也很明显可能会丢数据
我们知道redis是一个kv内存数据库,虽提供了异步复制数据+哨兵模式 来保证服务的高可用+数据高可靠;但redis不保证不丢数据,即使从库变为主库,但也不保证之前的从库数据是最新的,毕竟是异步同步数据。


建议中大厂,或者能稳定运维redis和具有高可用方案+失败重试方案的厂子使用;小厂慎用吧。


数据库自增模式


在早些年,确实看到过依靠数据库自增的方式,进行唯一ID的生成。此种方式主要是依赖数据库的自增来实现;想要一个唯一的ID,那么往带有自增主键的表里插入一条数据吧,这样sql返回的时候,就会把自增ID返回来了


具体sql


INSERT INTO your_table (column1, column2) VALUES ('value1', 'value2'); SELECT LAST_INSERT_ID();

优点:
数据库自增生成的ID 能满足唯一性也基本能满足自增性


缺点:

生成速度较慢

受限于数据库的实现方式,,ID生成速度相较于前两者至少慢一个数量级,毕竟在生成的时候数据库有磁盘IO操作;高并发下,不建议使用此种方式。而且每次要获得一个ID,还需要往数据库里先插入一条记录。本来高并发下,数据库操作就是个瓶颈了,用了此种方式还加剧了数据库的负担。


数据库号段模式


数据库号段模式,可以看做是数据库自增模式的升级版,在数据库自增模式上进行了性能优化,解决了ID生成慢的问题。
思路入下图:


1698825866585.png


数据库自增模式,每次获取一个ID,都需要操作一次库,太慢了。
号段模式每次操作数据库是申请一个号段的范围。
比如操作一次数据库,申请1000到2000是这个应用的业务申请的号段;然后只能这个应用实例的业务能用;应用申请了号段后放到内存中,每次业务需要的时候,从内存里累加1返回,在java里也有现成的工具,比如AtomicLong包装下(省事还能解决并发问题);如果发现快不够了,还能异步提前从数据库中申请,放入内存中,这样就避免了业务需要唯一ID的时候,在去数据库申请,加快业务获取ID的速度。


优点:

能满足唯一性,也能满足自增性,性能也不差,美团开源的leaf使用此种模式,速度能到 5W/秒。


缺点:

对数据库是强依赖;但我想大多数业务系统基本都依赖数据库吧


个人认为此方案最适合小厂;性能不差,而且也不需要依赖太多的中间件;而且大厂里也不缺乏使用此方案身影,比如滴滴开源的Tinyid也是用的此模式


雪花算法模式


雪花算法(Snowflake)出自大名鼎鼎的twitter公司之手;是的就是那个被硅谷钢铁侠 马斯克收购的公司。
该算法开源后,深受国内大厂好评。并且像百度这种大厂,基于雪花算法思想,开发了自己开源的
UidGenerator 分布式ID生成器。


1698825928764.png


Snowflake生成的是Long类型的ID,我们知道一个Long类型占8个字节空间,1个字节又占8比特,也就是说一个Long类型会用88 64个比特。


Snowflake ID组成结构:正数位(占1比特)+ 时间戳(占41比特)+ 机器ID(占5比特)+ 数据中心(占5比特)+ 自增值(占12比特),总共64比特。



  • 第一个bit位(1bit):Java中long的最高位是符号位代表正负,正数是0,负数是1,一般生成ID都为正数,所以默认为0。

  • 时间戳部分(41bit):毫秒级的时间,不建议存当前时间戳,而是用(当前时间戳 - 固定开始时间戳)的差值,可以使产生的ID从更小的值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年 (如果你建设的IT系统要使用100年,慎用此法)

  • 工作机器id(10bit):也被叫做workId,这个可以灵活配置,机房或者机器号组合都可以。

  • 序列号部分(12bit),自增值支持同一毫秒内同一个节点可以生成4096个ID


优点:

* 雪花算法生成的ID,能保证唯一性;

* 随这时间齿轮的流转,也能满足自增性;有大厂的背书,生成速度也是超快;

* 对第三方中间件也是弱依赖


缺点:



  • 存在时钟回拨问题,所有机器时钟必须保存同步,否则会导致生成重复ID;但是也能解决

  • 生成的数字会随这时间的流逝增大,不会因为数据量的增大而增大,在某些业务场景不太适用

  • 需要维护机器ID的配置;或者依赖第三方中间件根据机器信息,生成机器ID


这么多的分布式唯一id生成方法,我该选择哪一种?


为了方便选择,列了个表。到底我的应用里,该选哪个方案了?还需结合自身业务,团队大小,技术实力,实现复杂度和维护难易度进行选择。


image.png


想偷懒,有现成的唯一ID生成工具吗?


有的,像比如美团开源的leaf 能支持数据库号段和雪花模式,传输门:github.com/Meituan-Dia…


百度的 UidGenerator 使用的是雪花模式,传输门:github.com/baidu/uid-g…


而滴滴的 Tinyid 使用的是号段模式,可以认为是基于美团leaf的开源版,传输门:github.com/didi/tinyid…


请注意:以上三个工具都是用java语言写的,用java语言的同学有福了,基本是开箱即用。


集成美团leaf 号段模式


1、从git上 github.com/Meituan-Dia… 下载 leaf源码


1698826132995.png


2、mvn clean install -DskipTests 编译打包工程


3、用工程里的script脚本目录下的sql 脚本,创建数据表


CREATE DATABASE leaf
CREATE TABLE `leaf_alloc` (
`biz_tag` varchar(128) NOT NULL DEFAULT '',
`max_id` bigint(20) NOT NULL DEFAULT '1',
`step` int(11) NOT NULL,
`description` varchar(256) DEFAULT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;

insert int0 leaf_alloc(biz_tag, max_id, step, description) values('leaf-segment-test', 1, 2000, 'Test leaf Segment Mode Get Id')


4、在leaf-server工程里配置数据库连接信息


1698826171611.png


5、启动leaf-server工程,访问
浏览器访问 http://localhost:8080/api/segment/get/leaf-segment-test即可。


注意:这里的leaf-segment-test即时bizTag。如果想要换成其它业务标,往leaf_alloc插入一条数据即可。


不想用leaf的web模式;但又想用号段模式+本地api方式直接调用,是否可行?


可行的。只需要引入美团的leaf-core jar包即可。是的,这么好的工具,居然没有maven信息;不死心的我,去maven官方仓库搜了一把,还真发现了leaf-core的坐标。
如下:


<dependency>
<groupId>com.tencent.devops.leafgroupId>
<artifactId>leaf-coreartifactId>
<version>1.0.2-RELEASEversion>
dependency>

但是下载下来后,包名居然是腾讯的包名。反编译后,发现代码基本上没怎么变。如果想偷懒,可直接下载。


为了保险起间,我还是用了美团的leaf-core(只好做本地jar依赖)。
然后在代码里加入对ID生成器的初始化。代码如下:



@Service
public class LeafIdDBSegmentImpl implements InitializingBean {
IDGen idGen;
DruidDataSource dataSource;

@Override
public void afterPropertiesSet() throws Exception {
Properties properties = PropertyFactory.getProperties();

// Config dataSource
dataSource = new DruidDataSource();
dataSource.setUrl(properties.getProperty("leaf.jdbc.url"));
dataSource.setUsername(properties.getProperty("leaf.jdbc.username"));
dataSource.setPassword(properties.getProperty("leaf.jdbc.password"));
dataSource.init();

// Config Dao
IDAllocDao dao = new IDAllocDaoImpl(dataSource);

// Config ID Gen
idGen = new SegmentIDGenImpl();
((SegmentIDGenImpl) idGen).setDao(dao);
idGen.init();
}

public Long getId(String key){
Result result = idGen.get(key);
if (result.getStatus().equals(Status.EXCEPTION)) {
throw BizException.createBizException(BizErrorCode.LEAF_CREATE_ERROR_REQUIRED);
}
return result.getId();
}

}


该代码主要做了几件事:

1、在该单列类,实例化后,会进行 数据库源的初始化工作,并且自行实例化IDAllocDao;当然你可以把工程里已有的数据源赋给IDAllocDaoImpl

2、获取ID时,调用底层idGen.get获取

3、业务代码使用时 用 LeafIdDBSegmentImpl.get方法 传入数据库里事先配置好的bizTag即可。


作者:程序员猪佩琪
来源:juejin.cn/post/7296089060834312207
收起阅读 »

一文告诉你,如何实现 IP 属地功能

细心的朋友们可能已经发现了,先在抖音、知乎、快手、小红书等这些平台已经上线了“网络用户显示 IP 的功能”,境外用户显示的是国家,国内的用户显示的省份,而且此项显示无法关闭,归属地强制显示。 作为网友,我们可能只是看看戏,但是作为一个努力学习的码农,我们肯定...
继续阅读 »

细心的朋友们可能已经发现了,先在抖音、知乎、快手、小红书等这些平台已经上线了“网络用户显示 IP 的功能”,境外用户显示的是国家,国内的用户显示的省份,而且此项显示无法关闭,归属地强制显示。



作为网友,我们可能只是看看戏,但是作为一个努力学习的码农,我们肯定要来看一下这个功能是怎么实现的,今天这篇文章,就用几分钟的时间来讲述一下这个功能是怎么实现的。



获取用户 IP 地址


HttpServletRequest 获取 IP




首先我们来看一下,在 Java 中,是如何获取到 IP 属地的,主要有以下两步:



  1. 通过 HttpServletRequest 对象,获取用户的 「IP」 地址

  2. 通过 IP 地址,获取对应的省份、城市]


首先,我们这里写一个工具类用于获取 IP 地址,因为用户的每次 Request 请求都会携带请求的 IP 地址放到请求头中,所以我们可以通过截取请求中的 IP 来获取 IP 地址;


/**
* 网络工具类
*
* @author <a href="https://github.com/liyupi">程序员鱼皮</a>
* @from <a href="https://yupi.icu">编程导航知识星球</a>
*/
public class NetUtils {

/**
* 获取客户端 IP 地址
*
* @param request
* @return
*/
public static String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
if (ip.equals("127.0.0.1")) {
// 根据网卡取本机配置的 IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (Exception e) {
e.printStackTrace();
}
if (inet != null) {
ip = inet.getHostAddress();
}
}
}
// 多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ip != null && ip.length() > 15) {
if (ip.indexOf(",") > 0) {
ip = ip.substring(0, ip.indexOf(","));
}
}
// 本机访问
if ("localhost".equalsIgnoreCase(ip) || "127.0.0.1".equalsIgnoreCase(ip) || "0:0:0:0:0:0:0:1".equalsIgnoreCase(ip)){
// 根据网卡取本机配置的IP
InetAddress inet;
try {
inet = InetAddress.getLocalHost();
ip = inet.getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
// 如果查找不到 IP,可以返回 127.0.0.1,可以做一定的处理,但是这里不考虑
// if (ip == null) {
// return "127.0.0.1";
// }
return ip;
}

/**
* 获取mac地址
*/
public static String getMacAddress() throws Exception {
// 取mac地址
byte[] macAddressBytes = NetworkInterface.getByInetAddress(InetAddress.getLocalHost()).getHardwareAddress();
// 下面代码是把mac地址拼装成String
StringBuilder sb = new StringBuilder();
for (int i = 0; i < macAddressBytes.length; i++) {
if (i != 0) {
sb.append("-");
}
// mac[i] & 0xFF 是为了把byte转化为正整数
String s = Integer.toHexString(macAddressBytes[i] & 0xFF);
sb.append(s.length() == 1 ? 0 + s : s);
}
return sb.toString().trim().toUpperCase();
}
}

获取用户的 IP 地址属地


淘宝库获取用户 IP 地址属地




通过这个方法,就可以重请求头中获取到用户的 IP 地址了,然后接下来就是 IP 地址归属地省份、城市的获取了,这里可以用很多 IP 地址查询的库进行查询,这里用一个库来测试一下。


淘宝 IP 地址库:ip.taobao.com/



不过淘宝的 IP 地址查询库已经在 2022 年 3 月 31 日下线了,这里我们就不能使用它了,只能另辟蹊径了。



这里我们截取一段之前淘宝货期 IP 地址的源码,然后一起来看一下。



这里可以看到,在日志文件中,出现了大量的 the request over max qps for user 问题。


虽然这个方法已经寄了,但是我们求知的道路可以停吗?肯定不可以啊,这里我们就来整一个新的获取 IP 地址属地的方法,也就是我们今天文章的主角:Ip2region。


Ip2region 介绍




这个是在之前的一篇文章看到的,他是一个 Gthub 的开源项目,即 Ip2region 开源项目


地址如下:github.com/lionsoul201…


这个开源库目前已经更新到了 V2 的版本,现在的它是一个强大的离线IP地址定位库和IP定位数据管理框架,其达到了微秒级别的查询效率,还提供了众多主流编程语言的 xdb 数据生成和查询客户端实现,可以说是非常得好用👍👍👍👍,今天这篇文章我们主要针对其 V2 版本进行讲解,如果想要查询 1.0 版本的内容的话,可以去 Github 上面进行查看。


Ip2region 详解


高达 99.9 % 的查询准确率




数据聚合了一些知名 ip 到地名查询提供商的数据,这些是他们官方的的准确率,经测试着实比经典的纯真 IP 定位准确一些。ip2region 的数据聚合自以下服务商的开放 API 或者数据(升级程序每秒请求次数 2 到 4 次),比例如下:





  1. 80%, 淘宝 IP 地址库, ip.taobao.com/%5C




  2. ≈10%, GeoIP, geoip.com/%5C

  3. ≈2%, 纯真 IP 库, http://www.cz88.net/%5C


Ip2region V2.0 特性




1、IP 数据管理框架


xdb 支持亿级别的 IP 数据段行数,默认的 region 信息都固定了格式:国家|区域|省份|城市|ISP,缺省的地域信息默认是0。 region 信息支持完全自定义,例如:你可以在 region 中追加特定业务需求的数据,例如:GPS信息/国际统一地域信息编码/邮编等。也就是你完全可以使用 ip2region 来管理你自己的 IP 定位数据。


2、数据去重和压缩


xdb 格式生成程序会自动去重和压缩部分数据,默认的全部 IP 数据,生成的 ip2region.xdb 数据库是 11MiB,随着数据的详细度增加数据库的大小也慢慢增大。


3、极速查询响应


即使是完全基于 xdb 文件的查询,单次查询响应时间在十微秒级别,可通过如下两种方式开启内存加速查询:



  1. vIndex 索引缓存 :使用固定的 512KiB 的内存空间缓存 vector index 数据,减少一次 IO 磁盘操作,保持平均查询效率稳定在10-20微秒之间。

  2. xdb 整个文件缓存:将整个 xdb 文件全部加载到内存,内存占用等同于 xdb 文件大小,无磁盘 IO 操作,保持微秒级别的查询效率。


多语言以及查询客户端的支持


已经客户端有:Java、C#、php、C、Python、Node.js、PHP 拓展(PHP 5 和 PHP 7)等,主要如下:


binding描述开发状态binary查询耗时b-tree查询耗时memory查询耗时
cANSC c binding已完成0.0x毫秒0.0x毫秒0.00x毫秒
c#c# binding已完成0.x毫秒0.x毫秒0.1x毫秒
Golanggolang binding已完成0.x毫秒0.x毫秒0.1x毫秒
Javajava binding已完成0.x毫秒0.x毫秒0.1x毫秒
Lualua实现 binding已完成0.x毫秒0.x毫秒0.x毫秒
Lua_clua的c扩展已完成0.0x毫秒0.0x毫秒0.00x毫秒
nginxnginx的c扩展已完成0.0x毫秒0.0x毫秒0.00x毫秒
nodejsnodejs已完成0.x毫秒0.x毫秒0.1x毫秒
phpphp实现 binding已完成0.x毫秒0.1x毫秒0.1x毫秒
php5_extphp5的c扩展已完成0.0x毫秒0.0x毫秒0.00x毫秒
php7_extphp7的c扩展已完成0.0毫秒0.0x毫秒0.00x毫秒
pythonpython bindng已完成0.x毫秒0.x毫秒0.x毫秒
rustrust binding已完成0.x毫秒0.x毫秒0.x毫秒

Ip2region xdb Java 查询客户端实现




这里简单展示一下 Java 的实现,这里使用开发中常用的 Maven 实现的方式:


1. 引入 Maven 仓库


由于项目使用Spring 的方式构建,这里可以选择使用引入 Spring 的 starter 的方式进行


<dependency>
<groupId>com.github.hiwepy</groupId>
<artifactId>ip2region-spring-boot-starter</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>

<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>2.7.0</version>
</dependency>

在引入 Maven 依赖之后,我们这里引入几种实现的方式:


2. 实现方式 1:【基于文件查询】


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
public static void main(String[] args) {
// 1、创建 searcher 对象
String dbPath = "ip2region.xdb file path";
Searcher searcher = null;
try {
searcher = Searcher.newWithFileOnly(dbPath);
} catch (IOException e) {
System.out.printf("failed to create searcher with `%s`: %s\n", dbPath, e);
return;
}

// 2、查询
try {
String ip = "1.2.3.4";
long sTime = System.nanoTime();
String region = searcher.search(ip);
long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
} catch (Exception e) {
System.out.printf("failed to search(%s): %s\n", ip, e);
}

// 3、备注:并发使用,每个线程需要创建一个独立的 searcher 对象单独使用。
}
}

3. 实现方式 2:【缓存VectorIndex索引】


我们可以提前从 xdb 文件中加载出来 VectorIndex 数据,然后全局缓存,每次创建 Searcher 对象的时候使用全局的 VectorIndex 缓存可以减少一次固定的 IO 操作,从而加速查询,减少 IO 压力。


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
public static void main(String[] args) {
String dbPath = "ip2region.xdb file path";

// 1、从 dbPath 中预先加载 VectorIndex 缓存,并且把这个得到的数据作为全局变量,后续反复使用。
byte[] vIndex;
try {
vIndex = Searcher.loadVectorIndexFromFile(dbPath);
} catch (Exception e) {
System.out.printf("failed to load vector index from `%s`: %s\n", dbPath, e);
return;
}

// 2、使用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。
Searcher searcher;
try {
searcher = Searcher.newWithVectorIndex(dbPath, vIndex);
} catch (Exception e) {
System.out.printf("failed to create vectorIndex cached searcher with `%s`: %s\n", dbPath, e);
return;
}

// 3、查询
try {
String ip = "1.2.3.4";
long sTime = System.nanoTime();
String region = searcher.search(ip);
long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
} catch (Exception e) {
System.out.printf("failed to search(%s): %s\n", ip, e);
}

// 备注:每个线程需要单独创建一个独立的 Searcher 对象,但是都共享全局的制度 vIndex 缓存。
}
}

4. 实现方式 3:「缓存整个 xdb 数据」


我们也可以预先加载整个 ip2region.xdb 的数据到内存,然后基于这个数据创建查询对象来实现完全基于文件的查询,类似之前的 memory search。


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
public static void main(String[] args) {
String dbPath = "ip2region.xdb file path";

// 1、从 dbPath 加载整个 xdb 到内存。
byte[] cBuff;
try {
cBuff = Searcher.loadContentFromFile(dbPath);
} catch (Exception e) {
System.out.printf("failed to load content from `%s`: %s\n", dbPath, e);
return;
}

// 2、使用上述的 cBuff 创建一个完全基于内存的查询对象。
Searcher searcher;
try {
searcher = Searcher.newWithBuffer(cBuff);
} catch (Exception e) {
System.out.printf("failed to create content cached searcher: %s\n", e);
return;
}

// 3、查询
try {
String ip = "1.2.3.4";
long sTime = System.nanoTime();
String region = searcher.search(ip);
long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
} catch (Exception e) {
System.out.printf("failed to search(%s): %s\n", ip, e);
}

// 备注:并发使用,用整个 xdb 数据缓存创建的查询对象可以安全的用于并发,也就是你可以把这个 searcher 对象做成全局对象去跨线程访问。
}
}

5. 编译测试程序


通过 maven 来编译测试程序。


# cd 到 java binding 的根目录
cd binding/java/
mvn compile package

然后会在当前目录的 target 目录下得到一个 ip2region-{version}.jar 的打包文件。


6. 查询测试



  • 可以通过 java -jar ip2region-{version}.jar search 命令来测试查询:


➜  java git:(v2.0_xdb) ✗ java -jar target/ip2region-2.6.0.jar search
java -jar ip2region-{version}.jar search [command options]
options:
--db string ip2region binary xdb file path
--cache-policy string cache policy: file/vectorIndex/content


  • 例如:使用默认的 data/ip2region.xdb 文件进行查询测试:


➜  java git:(v2.0_xdb) ✗ java -jar target/ip2region-2.6.0.jar search --db=../../data/ip2region.xdb
ip2region xdb searcher test program, cachePolicy: vectorIndex
type 'quit' to exit
ip2region>> 1.2.3.4
{region: 美国|0|华盛顿|0|谷歌, ioCount: 7, took: 82 μs}
ip2region>>

输入 ip 即可进行查询测试,也可以分别设置 cache-policy 为 file/vectorIndex/content 来测试三种不同缓存实现的查询效果。


bench 测试


可以通过 java -jar ip2region-{version}.jar bench 命令来进行 bench 测试,一方面确保 xdb 文件没有错误,一方面可以评估查询性能:


➜  java git:(v2.0_xdb) ✗ java -jar target/ip2region-2.6.0.jar bench
java -jar ip2region-{version}.jar bench [command options]
options:
--db string ip2region binary xdb file path
--src string source ip text file path
--cache-policy string cache policy: file/vectorIndex/content

例如:通过默认的 data/ip2region.xdb 和 data/ip.merge.txt 文件进行 bench 测试:


➜  java git:(v2.0_xdb) ✗ java -jar target/ip2region-2.6.0.jar bench --db=../../data/ip2region.xdb --src=../../data/ip.merge.txt
Bench finished, {cachePolicy: vectorIndex, total: 3417955, took: 8s, cost: 2 μs/op}

可以通过分别设置 cache-policy 为 file/vectorIndex/content 来测试三种不同缓存实现的效果。 @Note: 注意 bench 使用的 src 文件要是生成对应 xdb 文件相同的源文件。


作者:夭要7夜宵
来源:juejin.cn/post/7295576148364296231
收起阅读 »

为什么堂堂微信数据库表名、字段名起的如此随意?

1.微信数据库解密 微信数据库在在哪个文件夹 EnMicroMsg.db的父文件加密规则是md5("mm" + uin)这样就可以准确的获取到db文件的位置. uin的获取:/data/data/com.tencent.mm/shared_prefs/au...
继续阅读 »

1.微信数据库解密




  • 微信数据库在在哪个文件夹


    EnMicroMsg.db的父文件加密规则是md5("mm" + uin)这样就可以准确的获取到db文件的位置.


    uin的获取:/data/data/com.tencent.mm/shared_prefs/auth_info_key_prefs.xml`里面有个uinz字段,直接获取value值,示例如下图所示:


    image-20210526135110550




  • 解密微信数据库:目前只涉及两个数据库EnMicroMsg.db(微信数据涉数据库)和WxFileIndex.db(文件索引数据库)


    解密的密码:md5(手机imei+uin)的32位字符串取前7位,如果imei拿不到就用1234567890ABCDEF代替




2. 好友相关信息


微信的好友相关数据涉及三张表:rcontact,bizinfo,img_flag




  • rcontact表存放了该账号涉及的所有账号(微信账号,群账号)的基本信息(eg:微信昵称,备注,原微信号,改之后的微信号,全拼等等)。如下图所示:


    111




  • bizinfo表存放的是该账号的好友微信号,群账号,这里好友包括已经通过的和添加没通过的,如下所示:


    22222




  • img_flag表存放该账号所有涉及的微信(好友,同属一个群不是好友,添加的陌生人)的头像地址,数据如下图所示:


    3333


    总结:rcontact表是一张基础表,存放所有的账号基本信息,bizinfo存放是该账号的好友信息或者群组信息,img_flag存放了微信账号对应的头像信息,以下场景有:




    • 获取微信好友信息,查询sql如下:


      select r.username, r.alias, r.conRemark, r.nickname, r.pyInitial, r.quanPin,r.encryptUserName, i.reserved2 from rcontact r INNER JOIN img_flag i  on r.username = i.username where r.type&2=2  and r.username not like '%@chatroom' and i.lastupdatetime > 0



    • 获取添加未通过的好友信息,此时有两种情况:1)添加同属一个群的好友。2)添加陌生人。比如说通过微信号,扫码什么。这两种情况在数据库的表现形式是不一样的,添加同属一个群的,在bizinfo表会插入一条username为添加好友的微信号记录,而如果是添加陌生人,则username是一个以@stranger结尾的key,对应的数据如下图所示:


      4444


      注意:这里如果通过微信号,扫码添加的陌生人,其username是一长串的以@stranger结尾的key,同 时pyInitial,qunPin两个字段存的并不是这个陌生人的微信号


      查询sql如下:


      SELECT r.username, r.alias, r.conRemark, r.nickname, r.pyInitial, r.quanPin, r.encryptUserName, i.reserved2 FROM rcontact r INNER JOIN bizinfo b ON r.username = b.username INNER JOIN img_flag i ON r.username = i.username 
      WHERE r.type <> 33 and r.type & 2 <> 2 AND r.username <> '当前微信号' AND r.username NOT LIKE '%@chatroom' AND b.updateTime > 0



    • 获取同属一个群但不是好友的基本信息:


      查询sql如下:


      SELECT DISTINCT r.username, r.alias, r.conRemark, r.nickname, r.pyInitial, r.quanPin, i.reserved2 FROM rcontact r  INNER JOIN img_flag i ON r.username = i.username 
      WHERE r.username not in(select username from bizinfo) and i.lastupdatetime >0

    3.微信群组


    微信群组信息表为chatroom,存放着一些基本信息,数据如下图所示:


    555


    注意:微信群组一开始建立显示群昵称是所有好友微信昵称加起来的一个字符串,即displayname字段,但是如果修改了群昵称之后,显示的是修改之后的,这时候需要根据根据群账号chatroomname去rcontact表做关联查询,根据rcontact表的username等于群账号查询出一条记录,此时这条记录的字段nickname即修改后的群昵称,查询sql如下:


    select c.chatroomname, c.memberlist, c.displayname, c.roomowner, c.selfDisplayName, r.nickname from chatroom c inner join rcontact r on r.username = c.chatroomname where c.modifytime > 0


    目前的微信群组的头像在img_flag表没有存储,暂时找不到资源所在


    4.微信聊天数据


    微信的聊天记录是保存在message表中的,数据示例如下图:


    666


    msgSvrId:唯一标示一条聊天记录的id,可以作为更新聊天记录数据的条件


    createTime:发送消息的时间


    talker:如果是群账号,说明这条消息是群会话消息,发送人在content内容前面(发送人微信号:发送内容);如果是好友微信号,说明这条消息是好友会话消息


    isSend:发送或者接收标志。0:接收 1:发送


    type:消息类型 1:文本内容(包括小表情) 3:图片 34:语音 43:视频 47:大表情 49:文件


    436207665:微信红包 419430449:微信转账


    图片,视频,语音, 文件 根据msgId去索引库WxFileIndex的表WxFileIndex2查询




    • 图片查询sql:


      select * from WxFileIndex2 where msgId in(msgIds) and msgType=3 and msgSubType20



    • 语音查询sql:


      select * from WxFileIndex2 where msgId in(msgIds) and msgType=34



    • 视频查询sql:


      select * from WxFileIndex2 where msgId in(msgIds) and msgType=43 and msgSubType=1



    • 文件查询sql:


      select * from WxFileIndex2 where msgId in(msgIds) and msgType=49 and msgSubType = 34



    • 大表情查询sql:根据groupId去找到对应的包名,md5即表情的文件名


      select e.md5, e.groupid, m.msgSvrId from emojiinfo e INNER JOIN message m on e.md5=m.imgpath where m.type=47



    5.总结


    以上分析师基于Android系统端的微信,且微信数据的撤销删除仍需要研究,待补充,未完待续.......




作者:shepherd111
来源:juejin.cn/post/7295160228879122458
收起阅读 »

适合小公司的自动化部署脚本

背景(偷懒) 在小小的公司里面,挖呀挖呀挖。快挖不动了,一件事重复个5次,还在人肉手工,身体和心理就开始不舒服了,并且违背了个人的座右铭:“偷懒”是人类进步的第一推动力。 每次想要去测试环境验证个新功能,又或者被测试无情的催促着部署新版本后;都需要本地打那个2...
继续阅读 »

背景(偷懒)


在小小的公司里面,挖呀挖呀挖。快挖不动了,一件事重复个5次,还在人肉手工,身体和心理就开始不舒服了,并且违背了个人的座右铭:“偷懒”是人类进步的第一推动力


每次想要去测试环境验证个新功能,又或者被测试无情的催促着部署新版本后;都需要本地打那个200多M的jar包;以龟速般的每秒几十KB网络,通过ftp上传到服务器;用烂熟透的jps命令查找到进程,kill后,重启服务。


是的,我想偷懒,想从已陷入到手工部署的沼泽地里走出来。如何救赎?


自我救赎之路


我的诉求很简单,想要一款“一键CI/CD的工具”,然后可以继续偷懒。为了省事,我做了以下工作


找了一款停止服务的脚本,并做了小小的优化


首推 陈皮大哥的停服脚本(我在里面加了个sleep 5);脚本见下文。只需要修改 APP_MAINCLASS的变量“XXX-1.0.0.jar”替换为自己jar的名字即可,其它不用动


该脚本主要是通过jps + jar的名字获得进程号,进行kill。( 脚本很简单,注释也很详细,就不展开了,感兴趣可以阅读下,不到5分钟,写过代码的你能看懂的)


把以下脚本保存为stop.sh


#!/bin/bash
# 主类
APP_MAINCLASS="XXX-1.0.0.jar"
# 进程ID
psid=0
# 记录尝试次数
num=0
# 获取进程ID,如果进程不存在则返回0,
# 当然你也可以在启动进程的时候将进程ID写到一个文件中,
# 然后使用的使用读取这个文件即可获取到进程ID
getpid() {
javaps=`jps -l | grep $APP_MAINCLASS`
if [ -n "$javaps" ]; then
psid=`echo $javaps | awk '{print $1}'`
else
psid=0
fi
}
stop() {
getpid
num=`expr $num + 1`
if [ $psid -ne 0 ]; then
# 重试次数小于3次则继续尝试停止服务
if [ "$num" -le 3 ];then
echo "attempt to kill... num:$num"
kill $psid
sleep 5
else
# 重试次数大于3次,则强制停止
echo "force kill..."
kill -9 $psid
fi
# 检查上述命令执行是否成功
if [ $? -eq 0 ]; then
echo "Shutdown success..."
else
echo "Shutdown failed..."
fi
# 重新获取进程ID,如果还存在则重试停止
getpid
if [ $psid -ne 0 ]; then
echo "getpid... num:$psid"
stop
fi
else
echo "App is not running"
fi
}
stop

编写2行的shell 启动脚本


修改脚本中的XXX-1.0.0.jar为你自己的jar名称即可。保存脚本内容为start.sh。jvm参数可自行修改


basepath=$(cd `dirname $0`; pwd)
nohup java -server -Xmx2g -Xms2g -Xmn1024m -XX:PermSize=128m -Xss256k -XX:+DisableExplicitGC -XX:+UseParNewGC -XX:-UseAdaptiveSizePolicy -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -Xloggc:logs/gc.log -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:HeapDumpPath=logs/dump.hprof -XX:ParallelGCThreads=4 -jar $basepath/XXX-1.0.0.jar &>nohup.log &

复用之前jenkins,自己写部署脚本


脚本一定要放到 Post Steps里


1689757456174.png


9行脚本,主要干了几件事:



  • 备份正在运行的jar包;(万一有啥情况,还可以快速回滚)

  • 把jenkins上打好的包,复制到目标服务上

  • 执行停服脚本

  • 执行启动服务脚本


脚本见下文:


ssh -Tq $IP << EOF 
source /etc/profile
#进入应用部署目录
cd /data/app/test
##备份时间戳
DATE=`date +%Y-%m-%d_%H-%M-%S`
#删除备份jar包
rm -rf /data/app/test/xxx-1.0.0.jar.bak*
#备份历史jar包
mv /data/app/test/xxx-1.0.0.jar /data/app/test/xxx-1.0.0.jar.bak$DATE
#从jenkins上拉取最新jar包
scp root@$jenkisIP:/data/jenkins/workspace/test/target/XXX-1.0.0.jar /data/app/test
# 执行停止应用脚本
sh /data/app/test/stop.sh
#执行重启脚本
sh /data/app/test/start.sh
exit
EOF

注:



  • $IP 是部署服务器ip,$jenkisIP 是jenkins所在的服务器ip。 在部署前请设置jenkins服务器和部署服务器之间ssh免密登录

  • /data/app/test 是部署jar包存放路径

  • stop.sh 是上文的停止脚本

  • start.sh 是上文的启动脚本


总结


如果不想把时间浪费在本地打包,忍受不了上传jar包的龟速网络,人肉停服和启动服务。请尝试下这款自动部署化脚本。小小的投入,带来大大的回报。


原创不易,请 点赞,留言,关注,转载 4暴击^^


参考资料:

xie.infoq.cn/article/52c…
Linux ----如何使用 kill 命令优雅停止 Java 服务

blog.csdn.net/m0_46897923… --ssh免密登录


作者:程序员猪佩琪
来源:juejin.cn/post/7257440759569055802
收起阅读 »

程序员必看的几大定律,你中招了吗?

一 晕轮效应 我们通常会从局部信息形成一个完整的印象,根据最少量的情况对别人或其他事物做出全面的结论。 举个简单的例子,当你看到一个陌生美女的时候,你通常会认为对方长得这么好看,笑起来这么甜,肯定哪哪都好! 当你看到一位发量稀疏的开发老哥,你通常会觉得这位...
继续阅读 »

一 晕轮效应



我们通常会从局部信息形成一个完整的印象,根据最少量的情况对别人或其他事物做出全面的结论。



举个简单的例子,当你看到一个陌生美女的时候,你通常会认为对方长得这么好看,笑起来这么甜,肯定哪哪都好!


当你看到一位发量稀疏的开发老哥,你通常会觉得这位老哥技术能力肯定非常强,做人肯定也很靠谱!



在晕轮效应影响下,一个人或事物的优点或缺点一旦变为光圈被扩大,其缺点或优点也就隐退到光圈的背后,被别人视而不见了。



对于程序员来说,有两点可以考虑:




  1. 打造自己的光晕:让自己成为专家或者像个专家,可以提升自己的话语权。


    比如在掘金写文章,当你拥有几千粉丝,几十万阅读量的时候,即使你什么都不说,别人看到这个账号都会觉得你是个厉害人物,与你交流的时候都会注意几分。




  2. 突破别人的光晕:在我使用npm上各种组件或者工具的时候,常常会感慨,好厉害!这么多star的项目,肯定没有Bug吧,如果有,那一定是我用的方式不对!打开源码看的时候更是惊呼,好厉害,完全看不懂,也不可能看的懂吧?


    但其实,褪去这些光环,你会发现,即使是成熟的项目,也会有bug,高深的代码,也都只是一个个基础的语句组合起来的,当你理解了作者的思想,也就能理解代码。




二 眼不见为净定律



看不见的,就是干净的。



看到这个定律,我的第一反应就是,屎山代码为什么会存在?


还不是因为管理人员看不到这坨屎山,他看到的是一个功能正常运行的系统,所以人家并不觉得这是屎山,而是美丽的风景线!


只有我们这些天天在这座屎山上添砖加瓦的程序员才能会感受到这种绝望!


所以面对屎山代码,不要抱怨,最好的方法就是找个机会把这座屎山丢给其他人,毕竟眼不见为净嘛!


当它不在你手上的时候,你会发现其实它也挺好的,毕竟眼不见为净嘛!


三 虚假同感偏差


你们是否会遇到这种情况:明明一件很重要的事情,催了某个人很久了,他却迟迟未做!


这里就涉及到虚假同感偏差,因为这件事对你来说很重要,所以通常会自我推断,觉得别人也会认为这件事情很重要,然而事实上,对你很重要的事,对他人来说可能回过头就给忘记了!


所以啊,要让别人重视一件你觉得很重要的事情,就是也让他感觉到重要,这样别人就不敢忘记了,比如可以补充一句:某某领导正在关注这件事,麻烦尽快,谢谢!


另外就是当我们非常确信自己观点或意见的时候,也很容易产生虚假同感偏差,这时候如果有人提出不同的观点,我们会下意识的反驳,并且觉得问题来自于他人。


比如我们自信满满地写完一段代码并且自测之后,提交给测试人员进行测试,当测试人员跟你反馈存在某BUG,我相信第一时间反应大多都是:我不信!!!


然后就有以下对话:



你:可能前端有缓存,你刷新一下再试试?


测试:行,我试一下。


过了十分钟......


测试:还是一样的问题啊,你看一下。


你:是不是测试数据有问题啊,我自己都测试过了,不应该有问题!


测试:行吧,我再看看。


过了十分钟......


测试:数据都排查过了,是正常的,你检查一下吧!


你:(还想再挣扎一下)你怎么操作的?


测试:就点击一下这个按钮,我还能玩出什么花吗?


排查了一会,哦~居然是空值的情况没有判断,我还能再白痴一点吗!


你:问题已经修复了,是某某复杂的场景我没考虑清楚,你再测试一下!



四 自我宽恕定律



人性有个根深蒂固的特点,就是容易发现别人的缺点和错误,却不容易看到自己的不足。


正所谓,见人之过易,见己之过难。





  • 当看到别人的代码存在一个空指针异常,心里想:这都能忘记判断,其他代码会不会也有问题!




  • 当发现自己的代码存在一个空指针异常,心里想:只是不小心忘记判断了嘛,加一下就好了!




  • 当接手别人项目的时候:卧槽,这代码写的啥啊,注释没几句,变量命名这么奇葩,逻辑这么混乱,我要咋接啊!!




  • 当项目给别人接手时候:我这代码虽然注释不多,但是很规范的,你看这变量命名不就能知道是什么含义了嘛,逻辑也非常顺,这个方法几百行你按顺序看下来就行了,我都给你写在一起,不需要跳来跳去地看,多方便!




五 补偿作用:



弱点也是一种力量源!



大家应该都听说过这个现象:瞎子的眼睛虽然看不见了,听力通常会变得非常灵敏!


这种生理上的现象吸引了很多有兴趣的心理学家,所谓补偿,就是发挥一个人的最大优势,激发其自信心,抵消其弱点。


看到补偿机制,我第一想到的就是在掘金看到的各种专科大佬。


虽然学历起点比其他人低一些,但有时候正是因为学历劣势,更加激发他们深耕技术的决心,反而达到其他更高学历人员都无法达到的高度。


这又让我想起了一句话:打不倒我的,会让我更强大!!


六 皮尔斯定理



意识到无知,才是知道的开始。



还有一句话,我觉得很适合接在这句话后面:知道的越多,才发现自己不懂的越多!


于是就形成了一个闭环: 意识到无知->开始知道->知道的越多->意识到无知


这句话我相信大部分人都听过很多遍了,不知道你们是从什么时候开始意识到自己的无知呢?


曾经,我还是小白的时候,在福州某公司上班,每天做的事情就是SpringBoot接口的开发,或者修改某些业务逻辑,我以为这差不多就是开发的全部了。


那时候对接的前端是使用Vue写的,我甚至不知道什么是Vue,只知道是某个挺流行的前端技术。


每次部署,我看前端文件里就只有一个index.html文件,我真的非常奇怪,为什么这么大的项目,只有一个html文件?


那时候我对前端认知还停留在html+js+css+jquery的时代,所以完全想不通。


本来还觉得自己前端也是有点基础的,直到接触Vue,我才惊呼,卧槽,前端怎么变成这样子了?什么nodejs,什么npm完全没听说过。


用过一段时间之后,我更是惊呼,卧槽,前端还能这样子?明明我HTML+CSS+JS只懂一点,都能做出这么好看的页面了。


有了各种开源前端组件,即使对原生HTML标签和CSS不太懂,也能算是个还不错的前端开发了。


还有这ES6语法用起来也太爽了吧,比JAVA可自由太多了。


所以很感慨,当我没进入前端圈子的时候,还以为自己懂一些,进入之后,才发现自己真的是一窍不通,啥都要学。


更感慨的是,当我第一次接触掘金,我惊呼,卧槽,这个社区分享的东西都好干啊,好多听都没听过的标题,原来我有这么多东西都不懂!原来前端是个这么卷的领域!


结语


感谢阅读,希望本篇文章对你有所帮助!


作者:林劭敏
来源:juejin.cn/post/7295623585363771443
收起阅读 »

如何优雅的判断一个对象是否为空?

我们在刚开始学习Java的时候,遇到过最多的异常肯定是臭名昭著的空指针异常(NullPointerException),可以说它陪伴了我们整个初学阶段。字符串、对象、集合等等一不留神就容易出现空指针异常! 那么如何优雅的判断一个对象是否为空并且减少空指针异常呢...
继续阅读 »

我们在刚开始学习Java的时候,遇到过最多的异常肯定是臭名昭著的空指针异常(NullPointerException),可以说它陪伴了我们整个初学阶段。字符串、对象、集合等等一不留神就容易出现空指针异常!


那么如何优雅的判断一个对象是否为空并且减少空指针异常呢?


今天来介绍一个容器类——Optional


Optional介绍


Optional是一个容器类,它只有两种情况:



  • 要么包含一个非空对象

  • 要么为空


它有助于编写更健壮的代码,以处理可能为空的值,而不必担心空指针异常!


Optional用法


Optional的创建


Optional有以下两种常见的创建方法:



  • Optional.of(T value):创建一个包含非空值的Optional,如果value为null,则抛出NullPointerException

  • Optional.ofNullable(T value):创建一个Optional,允许value为null


判断Optional容器中是否包含对象


isPresent(): 返回一个布尔值,如果Optional容器中包含一个非空对象则返回true,否则返回false


获取Optional容器的对象



  • get(): 如果Optional包含非空值,返回该值;否则抛出NoSuchElementException

  • orElse(T other): 如果Optional为空,返回指定的默认值other

  • orElseGet(Supplier<? extends T> other): 如果Optional为空,使用提供的Supplier函数生成默认值

  • orElseThrow(Supplier<? extends X> exceptionSupplier): 如果Optional为空,抛出由提供的Supplier函数生成的异常


过滤


filter(Predicate<? super T> predicate): 如果Optional包含非空值且满足predicate条件,则返回当前Optional,否则返回一个空Optional。


映射



  • map(Function<? super T, ? extends U> mapper): 如果Optional包含非空值,应用mapper函数并返回新的Optional。

  • flatMap(Function<? super T, Optional> mapper): 类似于map,但允许mapper函数返回Optional。


Optional示例


假如我们有一个User类,可以使用Optional来处理可能为空的User对象。User类结构如下:


public class User {
private String name;

public User(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

示例:创建Optional



Optional userOptional = Optional.ofNullable(new User("张三"));



示例:判断Optional是否包含对象


if (userOptional.isPresent()) {
System.out.println("用户存在:" + userOptional.get().getName());
} else {
System.out.println("用户不存在");
}

示例:获取Optional容器的对象


User user = userOptional.orElse(new User("李四"));
System.out.println("User: " + user.getName());

示例:过滤


Optional<User> filteredUserOptional = userOptional.filter(u -> u.getName().startsWith("张"));
if (filteredUserOptional.isPresent()) {
System.out.println("结果:" + filteredUserOptional.get().getName());
} else {
System.out.println("未找到对应用户");
}

示例:映射


Optional<String> userNameOptional = userOptional.map(User::getName);
userNameOptional.ifPresent(name -> System.out.println("用户名为: " + name));

使用场景总结



  • 当你从某个方法返回一个值,但该值可能为空,而调用者需要明确知道值是否存在。

  • 在处理方法参数时,你可以用Optional来表示某个参数可以为空,以提醒调用者可能会传入null。

  • 避免繁琐的null检查和条件语句,使代码更简洁和可读!



更多文章干货,推荐公众号【程序员老J】



作者:程序员老J
来源:juejin.cn/post/7298142364194979852
收起阅读 »

了不起的Base64

web
不要乱说话。话说出去之前我们还是话的主人,话说出去之后我们就成了话的奴隶。 大家好,我是柒八九。 前言 在我们项目开发中,Base64想必大家都不会很陌生,Base64是将二进制数据转换为文本的一种优雅方式,使存储和传输变得容易。但是,作为一个合格的程序员,...
继续阅读 »

不要乱说话。话说出去之前我们还是话的主人,话说出去之后我们就成了话的奴隶。



大家好,我是柒八九


前言


在我们项目开发中,Base64想必大家都不会很陌生,Base64是将二进制数据转换为文本的一种优雅方式,使存储和传输变得容易。但是,作为一个合格的程序员,我们应该有一种打破砂锅问到底的求助欲望。


所以,今天我们来讲讲在各种语言中出镜率都高的离谱的Base64算法。今天,我们就用我们在初高中语文老师教我们的描述一个事物的三大步骤:1. 是什么,2. 如何工作,3. 为什么它很重要。来讲讲Base64算法。


好了,天不早了,干点正事哇。



我们能所学到的知识点




  1. 前置知识点

  2. 为什么会出现 Base64 编码

  3. 什么是 Base64 编码?

  4. Base64 使用案例

  5. Base64 编码算法

  6. 如何进行 Base64 编码和解码





1. 前置知识点



前置知识点,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。如果大家对这些概念熟悉,可以直接忽略

同时,由于阅读我文章的群体有很多,所以有些知识点可能我视之若珍宝,尔视只如草芥,弃之如敝履。以下知识点,请酌情使用



RFC



RFC,全称为Request for Comments,是一种用于定义互联网标准和协议的文件系列。



RFC最早由互联网工程任务组(IETF)创建,用于记录和传播互联网协议、方法和最佳实践的提案、规范和讨论。


每个 RFC 都有一个唯一的编号,通常以RFC开头,后面跟着一个数字,例如RFC 791RFC 2616等。RFC文档通常包含了协议规范、技术说明、最佳实践、标准化提案等,以促进互联网技术的发展和互操作性。


我们可以在IETF-datatracker中输入指定的编号或者查找的关键字进行搜寻。



以下是一些常见的RFC文档,大家可以翻阅自己想了解的技术点:




  1. RFC 791 - Internet Protocol (IP): 定义了 IPv4,是互联网上最基本的协议之一。




  2. RFC 793 - Transmission Control Protocol (TCP): 定义了 TCP,一种重要的传输协议,用于可靠的数据传输。




  3. RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1: 定义了 HTTP 协议,用于在 Web 上传输超文本的基础通信协议。




  4. RFC 2326 - Real Time Streaming Protocol (RTSP): RTSP 用于流媒体传输,如音频和视频流的控制。




  5. RFC 5246 - The Transport Layer Security (TLS) Protocol Version 1.2: 定义了 TLS 1.2,用于安全地传输数据,如 HTTPS 协议中使用的加密通信。




  6. RFC 4648 - 这是咱们今天的主角,Base64的相关内容









Latin-1 字符集


Latin-1,也称为ISO-8859-1,是一种由国际标准化组织(ISO)认可的8 位字符集,代表了西欧语言的字母表。正如其名称所示,它是ISO-8859的一个子集,该标准还包括用于写作系统如西里尔文、希伯来文和阿拉伯文的其他相关字符集。它被大多数Unix系统以及Windows系统使用。



Latin-1有时被不太准确地称为扩展 ASCII



这是因为其字符集的前 128 个字符与美国 ASCII 标准相同。其余字符集包含了带重音的字符和符号。


关于更详细的Latin-1的表格,可以参考Latin-1-table




btoa


btoaJavaScript 中的一个内置函数,用于将二进制数据(通常是 8 位字节)编码为 Base64 字符串。它的名称是 binary to ASCII 的缩写,用于将二进制数据转换为文本字符串,以便在文本协议中传输或存储。


用法:


btoa 函数接受一个字符串参数,该字符串包含二进制数据。它将该二进制数据转换为 Base64 编码的字符串。


const binaryData = "front789";
const base64String = btoa(binaryData);
console.log(base64String);

这段代码将 front789 这个字符串转换为 Base64 编码的字符串并将结果打印到控制台。


限制:


尽管 btoa 是一个有用的函数,但它有一些限制:




  1. 只能编码字符串: btoa 函数只接受字符串作为参数,而不接受其他类型的数据(如二进制数组)。如果需要编码二进制数据,需要先将其转换为字符串。




  2. 字符集限制: btoa 函数仅支持 Latin-1 字符集,这意味着它只能编码包含在 Latin-1 字符集内的字符。如果字符串包含超出 Latin-1 字符集的字符,那么会导致编码失败。




  3. 不适合加密:Base64 编码不是加密,它只是一种编码方式,不提供安全性。如果需要加密数据,应该使用专门的加密算法而不是仅仅进行 Base64 编码。




  4. 数据大小增加: Base64 编码会增加数据大小。通常情况下,Base64 编码后的数据会比原始二进制数据更大,这可能会对数据传输和存储造成额外开销。




Data URL


Data URL 是一种统一资源标识符(URI)方案,用于将数据嵌入到文档中,而不是从外部文件加载数据。Data URL 允许我们将数据(如文本、图像、音频等)直接包含在网页或文档中,而不需要额外的 HTTP 请求。这种方式对于小型资源或需要避免外部请求的情况非常有用。


Data URL 的基本结构如下:


data:[<mediatype>][;base64],<data>

其中:



  • <mediatype> 是可选的媒体类型(例如,text/plainimage/png),用于描述数据的类型。如果被省略,则默认值为 text/plain;charset=US-ASCII

  • ;base64 是可选的,表示数据以 Base64 编码方式包含。如果省略了 ;base64,则数据将以纯文本方式包含。

  • <data> 包含实际的数据,可以是文本或二进制数据。


以下是 Data URL 的一些常见用途和示例:




  1. 嵌入图像: Data URL 可用于将图像直接嵌入 HTMLCSS 中,而不需要外部图像文件。例如,将一张 PNG 图像嵌入 HTML 中:


    <img
    src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wAABgAB/OGirwAAAABJRU5ErkJggg=="
    alt="Embedded Image"
    />




  2. 内联 CSS: Data URL 可用于内联 CSS 样式表,以减少外部 CSS 文件的请求。例如,将 CSS 样式表嵌入 HTML 中:


    <style>
    body {
    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wAABgAB/OGirwAAAABJRU5ErkJggg==);
    }
    </style>



  3. 嵌入字体: Data URL 可用于嵌入自定义字体,以确保字体在不同设备上显示一致。例如,嵌入一个字体文件:


    @font-face {
    font-family: "CustomFont";
    src: url(data:application/font-woff;base64,d09GRgABAAAA...) format("woff");
    }



  4. 内联脚本: Data URL 可用于内联小型 JavaScript 脚本,以减少外部脚本文件的请求。例如,内联一个简单的 JavaScript 函数:


    <script>
    let greeting = "前端柒八九";
    alert(greeting);
    </script>





2. 为什么会出现 Base64 编码


要理解为什么需要 Base64 编码,我们需要了解一些计算机历史。


计算机以二进制(01)进行通信,但人们通常希望使用更丰富的数据形式进行通信,如文本图像为了在计算机之间传输数据,首先必须将其编码为 0 和 1,然后再解码。以文本为例,有许多不同的编码方式。如果我们都能就一个单一的编码方式达成一致,那将会简单得多,但很遗憾,这并不是事实。针对这块的内容,可以参考了不起的 Unicode


最初创建了许多不同的编码方式(例如 Baudot 编码),每种方式使用不同数量的比特来表示一个字符,直到最终 ASCII 成为一个标准,每个字符使用 7 位。然而,大多数计算机将二进制数据存储为每个字节由 8 位组成的数据,因此 ASCII 不适合传输这种类型的数据。一些系统甚至会删除最高位。


为解决这些问题,引入了 Base64 编码。这允许我们将任意字节编码为已知不会损坏的字节(ASCII 字母数字字符和一些符号)。缺点是使用 Base64 对消息进行编码会增加其长度 - 每 3 个字节的数据编码为 4 个 ASCII 字符


要可靠地发送文本,我们可以首先使用自己选择的文本编码(例如 UTF-8)将其编码为字节,然后将结果的二进制数据使用 Base64 编码为可安全传输的 ASCII 文本字符串。接收者反转此过程以恢复原始消息。当然,这需要接收者知道使用了哪种编码,通常需要单独发送这些信息。


我们来看一个示例:


我希望发送一个带有两行的文本消息:


Hello
world!

如果我将其发送为 ASCII(或 UTF-8),它将如下所示:


72 101 108 108 111 10 119 111 114 108 100 33

某些系统会破坏字节 10,所以我们可以将这些字节作为 Base64 字符串进行 Base64 编码:


SGVsbG8Kd29ybGQh

这里的所有字节都是已知的安全字节,所以很少有机会使任何系统损坏此消息。我可以发送这个消息而不是我的原始消息,然后让接收者反转此过程以恢复原始消息。




2. 什么是 Base64 编码?


Base64编码将二进制数据转换为文本,具体来说是ASCII文本。生成的文本仅包含A-Za-z0-9以及符号+/这些字符。


而在之前我们在了不起的 Unicode中介绍过ASCII的。


由于字母表中有 26 个字母,我们有26 + 26 + 10 + 2(64)个字符。因此,这种编码被命名为Base64。这 64 个字符被认为是安全的,也就是说,与字符<>\n等不同,它们不会被旧计算机和程序误解


下面是经过 Base64 编码的文本front789的样子:ZnJvbnQ3ODk=


还有一点需要注意,如果在使用JS对某一个文本进行准换时,如果该文本包含非Latin1字符的字符串,会报错,所以我们需要对其进行准换处理。


// 原始文本字符串,包含非Latin1字符
const text = "前端柒八九";

// 创建一个 TextEncoder 对象,用于将文本编码为字节数组
const encoder = new TextEncoder();

// 使用 TextEncoder 对象将文本编码为字节数组
const data = encoder.encode(text);

// 使用 String.fromCharCode 和展开运算符 (...) 将字节数组转换为字符串
// 然后使用 btoa 函数将字符串转换为 Base64 编码
const base64 = btoa(String.fromCharCode(...data));

// 打印 Base64 编码后的结果
console.log(base64); //5YmN56uv5p+S5YWr5Lmd

我们在这里并没有加密文本。给定Base64编码的数据,非常容易将其转换回(解码)原始文本。我们只是改变了数据的表示,即编码



在本质上,Base64编码使用一组特定的、减少的字符来编码二进制数据,以防止数据损坏。



Base64字母表


由于只有64个字符可用于编码,我们可以仅使用6位来表示它们,因为2^6 = 64。每个Base64数字表示6位数据。一个字节中有8位,而 86最小公倍数24。因此,24 位,或 3 个字节,可以用四个 6 位的 Base64 数字表示


4. Base64 使用案例


我们可能在HTML文档中使用了<img src="789.jpeg">标签来包含图像。其实,我们可以直接将图像数据嵌入到 HTML 中,而不必使用外链!数据URL可以做到这一点,它们使用Base64编码的文本来内联嵌入文件。


<img src="data:image/gif;base64,xxxxbase64encodedtextxxxx" />

data:[<mime type
>
][;charset=<charset>][;base64],<encoded data></encoded></charset
></mime>

另一个常见的用例是当我们需要在网络上传输或存储一些二进制数据,而网络只能处理文本或ASCII数据时。这确保了数据在传输过程中保持不变。还有就是在 URL 中传递数据时,当数据包含不适合 URL 的字符时,此时Base64就有了用武之地。


Base编码还在许多应用程序中使用,因为它使得可以使用文本编辑器来操作对象。


我们还可以使用 Base64 编码将文件作为文本传输



  • 首先,获取文件的字节并将它们编码为 Base64

  • 然后传输 Base64 编码的字符串,然后在接收端解码为原始文件内容




5. Base64 编码算法


以下是将一些文本转换为 Base64 的简单算法。



  1. 将文本转换为其二进制表示

  2. 比特位分组为每组6位

  3. 将每个组转换为0到63的十进制数。它不能大于 64,因为每组只有 6 位。

    • 如果转换为十进制数的数字大于 64,我们可以将其取模64 例如:151 % 64 = 23



  4. 使用Base64字母表将此十进制数转换为等效的Base64字符


通过上述操作我们会得到一个Base64编码的字符串。如果最后一组中的比特位不足,可以使用===作为填充。


让我们以front7作为范例,来模拟上述操作。




  1. 通过首先将每个字符转换为其对应的 ASCII 数字,然后将该十进制数转换为二进制,(使用ASCII 转二进制工具)将文本front7转换为二进制:


    01100110 01110010 01101111 01101110 01110100 00110111

    f r o n t 7



  2. 将比特位分组为每组6位


    011001 100111 001001 101111 011011 100111 010000 110111



  3. 将每个组转换为 0 到 63 之间的十进制数:


    011001 100111 001001 101111 011011 100111 010000 110111

    25 23 9 47 27 23 16 27


    • 这步中如果数据超过 64,需要对其 64 取模




  4. 现在使用 Base64 字母表将每个十进制数转换为其 Base64 表示:


    25  23   9   47  27  23  16  27

    Z n J v b n Q 3



然后我们完成了。名字front7在 Base64 中表示为ZnJvbnQ3


乍一看,Base64 编码的好处并不是很明显。


想象一下,如果我们有一张图片或一个敏感文件(PDF、文本、视频等),而不是简单的字符串,我们想将它存储为文本。我们可以首先将其转换为二进制,然后进行 Base64 编码,以获得相应的 ASCII 文本。


现在我们可以将该文本发送或存储在任何地方,以任何我们喜欢的方式,而不必担心一些旧设备、协议或软件会错误解释原始二进制数据以损坏我们的文件。


6. 如何进行 Base64 编码和解码


所有编程语言都支持将数据编码为 Base64 格式以及从 Base64 格式解码数据。


JS 中处理


// 简单字符串
const text1 = "front789";
bota(text1); // ZnJvbnQ3ODk=

// 超出`Latin-1`字符的字符串
const text2 = "前端柒八九";
const encoder = new TextEncoder();
const data = encoder.encode(text);
const base64 = btoa(String.fromCharCode(...data));
console.log(base64); //5YmN56uv5p+S5YWr5Lmd

Rust 中处理


Rust的话,我们可以直接用 base64 crate。


Cargo.toml 文件中添加以下内容:


[dependencies]
base64 = "0.21.5"

use base64::{Engine as _, engine::general_purpose};

let orig = b"data";
let encoded: String = general_purpose::STANDARD_NO_PAD.encode(orig);
assert_eq!("ZGF0YQ", encoded);
assert_eq!(orig.as_slice(), &general_purpose::STANDARD_NO_PAD.decode(encoded).unwrap());

// or, URL-safe
let encoded_url = general_purpose::URL_SAFE_NO_PAD.encode(orig);

想了解更多关于Rust如何处理Base64,可以查看Rust base64


此外,终端也内置支持 Base64 编码。在终端中尝试以下命令:


echo "前端柒八九" | base64
5YmN56uv5p+S5YWr5LmdCg==

$ echo "5YmN56uv5p+S5YWr5LmdCg==" | base64 -d
前端柒八九



后记


分享是一种态度


全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。



作者:前端小魔女
来源:juejin.cn/post/7298190770401001512
收起阅读 »

某国外知名HR:参与开源贡献求职者的简历,直接扔掉

某国外知名HR:参与开源贡献求职者的简历,直接扔掉 一位自称曾在美国科技巨头(FAANG)担任招聘工作的“猎头”近日在推特上发表了一番引起争议的言论: 他声称在招聘时会直接将拥有开源贡献经历的开发者的简历扔掉。他的理由是,他怀疑那些有足够时间无私地为开源项目...
继续阅读 »

某国外知名HR:参与开源贡献求职者的简历,直接扔掉



  • 一位自称曾在美国科技巨头(FAANG)担任招聘工作的“猎头”近日在推特上发表了一番引起争议的言论:

  • 他声称在招聘时会直接将拥有开源贡献经历的开发者的简历扔掉。他的理由是,他怀疑那些有足够时间无私地为开源项目贡献代码的开发者,是否对自己的工作有足够的投入。

  • 进一步指出:“初级开发者参与开源项目可以理解,但高级开发者这么做可能意味着他们在职业成熟度上存在问题。”




  • 并附上了拒绝内推请求的截图:




  • 这一言论引发了广泛争议,因为他似乎忽略了许多开源贡献者是在下班时间和周末利用个人时间来参与开源项目,而这并不会对他们的工作产生负面影响。相反,这些贡献往往可以提升公司的技术声望和影响力。

  • 博主实在怀疑该 HR 的真实性,实际上源贡献不仅有助于个人技能提升,还可以为公司带来技术上的优势和创新。


个人简介


👋 你好,我是 Lorin 洛林,一位 Java 后端技术开发者!座右铭:Technology has the power to make the world a better place.


🚀 我对技术的热情是我不断学习和分享的动力。我的博客是一个关于Java生态系统、后端开发和最新技术趋势的地方。


🧠 作为一个 Java 后端技术爱好者,我不仅热衷于探索语言的新特性和技术的深度,还热衷于分享我的见解和最佳实践。我相信知识的分享和社区合作可以帮助我们共同成长。


💡 在我的博客上,你将找到关于Java核心概念、JVM 底层技术、常用框架如Spring和Mybatis 、MySQL等数据库管理、RabbitMQ、Rocketmq等消息中间件、性能优化等内容的深入文章。我也将分享一些编程技巧和解决问题的方法,以帮助你更好地掌握Java编程。


🌐 我鼓励互动和建立社区,因此请留下你的问题、建议或主题请求,让我知道你感兴趣的内容。此外,我将分享最新的互联网和技术资讯,以确保你与技术世界的最新发展保持联系。我期待与你一起在技术之路上前进,一起探讨技术世界的无限可能性。


📖 保持关注我的博客,让我们共同追求技术卓越。


作者:Lorin洛林
来源:juejin.cn/post/7298179365040111642
收起阅读 »

众行远,携手打造全场景生态,OpenHarmony技术大会应用(北向)生态分论坛召开

全场景时代下,用户需求变得越来越多元化。软件服务又称北向应用,应用生态围绕着一个共同的技术平台,众多的参与者协同合作,最终形成大量的软件解决方案或服务。在2023年11月4日举办的第二届开放原子开源基金会OpenHarmony技术大会“应用(北向)生态分论坛”...
继续阅读 »

全场景时代下,用户需求变得越来越多元化。软件服务又称北向应用,应用生态围绕着一个共同的技术平台,众多的参与者协同合作,最终形成大量的软件解决方案或服务。

在2023年11月4日举办的第二届开放原子开源基金会OpenHarmony技术大会“应用(北向)生态分论坛”上,OpenAtom OpenHarmony(简称“OpenHarmony")项目群技术指导委员会主席、华为基础软件首席科学家陈海波先生发表了欢迎致辞。陈海波指出,对于一个新兴的操作系统来说,应用生态的蓬勃发展是影响其发展壮大的决定性因素。OpenHarmony以“生态统一,开放共赢”的理念,致力于搭建生态共赢的平台。OpenHarmony 4.0对应用开发的支持力度也更大,相对于前版本大幅度增加了约4000多个API的接口。

OpenHarmony项目群技术指导委员会主席、华为基础软件首席科学家陈海波致辞

OpenHarmony项目管理委员会PMC主席任格林在分论坛发言中表示,OpenHarmony的生态现在到了一个蓬勃汹涌的时候,伙伴“同行正当时,先行得先机,智联赢未来”,OpenHarmony现已可承载复杂性的开发,能力已具备,配套的工具链,相关的三方库也在不断完善。现在API发展到10,为什么没在API5、6的时候大规模推广,因为那时有很多东西要补充。此外,开发者论坛正在试用阶段,预计今年将正式发布,该论坛主要面向OpenHarmony的北向开发者。同时,他指出,OpenHarmony已经走过三年的历程,相关的设备已经达到了近亿台的量级,对应用生态的需求迫切。

OpenHarmony项目管理委员会PMC主席任革林发言

来自行业内的10位技术专家和应用开发者们共同分享了他们在OpenHarmony探索中的心路历程、最佳实践、示范场景以及对技术的新颖解读。出席论坛的演讲嘉宾包括:石墨文档(初心科技)副总裁齐惊,上海贝锐信息科技股份有限公司资深产品总监董恩双,腾讯科技(深圳)有限公司腾讯云通信产品专家林光柘,联通在线信息科技有限公司认证产品中心技术总监程福兴,论客科技(广州)有限公司产品副总裁郝家雨,北京信安世纪科技有限公司助理副总裁王洪骏,北京书生电子技术有限公司技术总监任春宁,武汉极意网络科技有限公司终端安全负责人许伟,深圳开鸿数字产业发展有限公司未来研究院应用生态首席架构师熊汉彪。

石墨文档(初心科技)副总裁齐惊在《石墨文档OpenHarmony技术探索之路》的主题演讲中介绍了石墨文档作为全新一代云Office办公软件,支持多人在线文档协同办公,实现了多终端、跨地域、随时随地在线办公。他强调了万物智联时代带来的交互方式、任务调度、数据安全等新场景,并阐述了石墨文档通过创新技术,使得AI在智能办公领域和基于OpenHarmony万物智联的基础框架上具备新的应用场景。

石墨文档(初心科技)副总裁齐惊主题分享

上海贝锐信息科技股份有限公司资深产品总监董恩双带来了《贝锐携手OpenHarmony跨平台协作》的主题分享。他指出,想要实现稳定、高效、安全、可靠的远程连接,需克服跨网络、跨平台兼容性等挑战,并介绍了向日葵、蒲公英等产品在跨平台连接方面兼容OpenHarmony、Windows、Linux等操作系统,满足了多系统、跨平台远程连接的需求。贝锐致力于打破边界连接人、设备、网络,让连接更美好。

上海贝锐信息科技股份有限公司资深产品总监 董恩双主题分享

腾讯云音视频通信产品专家林光柘指出,即时通信和实时音视频已成为互联网产品的基础技术组件,他探讨了如何在OpenHarmony系统中使用高质量的通信服务,以解决客户面临的挑战。腾讯云音视频通过IM&TRTC的实践应用,致力于提供最佳的服务体系,以解决这些痛点。

腾讯云音视频通信产品专家林光柘主题分享

联通在线信息科技有限公司联通认证产品中心技术总监程福兴带来了《免密登录更安全,OpenHarmony生态认证SDK最佳实践》的分享,他介绍联通认证免密登录SDK基于运营商网关认证技术,为4G/5G蜂窝网络数据流量用户提供用户授权的手机号码或号码核验服务,已被广泛集成于互联网应用APP,为移动应用提供了快捷的用户登录和号码认证风控保障。同时,他还探讨了OpenHarmony版的SDK将如何助力OpenHarmony生态实现与Android生态一致的用户便捷性,并提供更安全的免密登录解决方案。

联通在线信息科技有限公司联通认证产品中心技术总监程福兴主题分享

论客科技(广州)有限公司产品副总裁郝家雨在《Coremail OpenHamrony适配最佳实践》分享中提到,邮件系统及邮件客户端应用作为企业信息的门户及客户桌面高频应用,使用频繁且面临严峻的安全形势。打造功能强大、稳定可靠、开放灵活的邮件系统及邮件客户端应用需依赖架构先进的操作系统,如何借助OpenHarmony系统生态打造更安全、用户体验更好的邮件系统,邮件客户端产品应用,是未来重要的发展趋势。

论客科技(广州)有限公司产品副总裁郝家雨主题分享

北京信安世纪科技有限公司助理副总裁王洪骏的演讲主题是《基于OpenHarmony的零信任安全体系多维探索》。他从“零信任与OpenHarmony碰撞”、“信安世纪融合OpenHarmony推出零信任综合解决方案”、“AI+安全”、“信安世纪六大产品线”等四个方面展开介绍,强调了在网络环境中的基础性安全问题不容忽视的情况下,如何借助OpenHarmony和零信任安全体系来应对信息技术互联网化、移动化和云化等发展趋势的挑战。

北京信安世纪科技有限公司助理副总裁王洪骏主题分享

北京书生电子技术有限公司技术总监任春宁强调了经验的重要性并表示,书生阅读器具有安装便捷、界面友好、功能丰富等特点。此外,它还为第三方提供了丰富的二次开发接口。书生OFD的跨平台特性使其能够在不同的CPU和操作系统上流畅运行,应用适配OpenHarmony等多个操作系统。

北京书生电子技术有限公司技术总监任春宁主题分享

武汉极意网络科技有限公司终端安全负责人许伟阐述了公司的定位,他谈到极验是一家利用创新技术,帮助企业鉴别异常流量、提升用户流量价值的公司。极验行为验图形验证码在行业中占据 56.2%的市场份额,并在今年率先推出了适配基于 OpenHarmony 操作系统的解决方案。通过运用AIGC等前沿技术有效赋能企业,助力企业在业务场景中对抗黑灰产,辨别虚假流量。

武汉极意网络科技有限公司终端安全负责人许伟主题分享

深圳开鸿数字产业发展有限公司未来研究院应用生态首席架构师熊汉彪介绍了操作系统生态的建立离不开海量的北向应用支撑。他表示,应用生态直面终端用户和广大应用开发者,是整个操作系统生态的关键环节。同时,他还深度阐述了深开鸿如何面向行业,积极推进行业应用OpenHarmony化的进程,以推动整个生态系统的繁荣发展。

随后,论坛进入了圆桌讨论环节,该环节由华为终端云服务全球生态发展与销售部软件生态专家李华怡博士主持,以“众行远,携手打造全场景生态”为主题,展开讨论。参与嘉宾包括齐惊、董恩双、郝家雨、王洪骏和任春宁等业界专家。在圆桌讨论中,嘉宾们纷纷发表了对于OpenHarmony生态建设的看法和建议,共同探讨如何推动OpenHarmony在全场景应用中的发展。

深圳开鸿数字产业发展有限公司未来研究院应用生态首席架构师熊汉彪主题分享

OpenHarmony是由开放原子开源基金会孵化及运营的开源项目,目标是构建一个开放、全场景、分布式的操作系统。作为一款全新的开源操作系统,OpenHarmony已经在多个领域展现出强大的潜力。开源社区和企业的紧密合作是OpenHarmony生态建设的关键,在本次论坛中,多家企业表示将积极参与OpenHarmony的开发和推广,共同助力OpenHarmony生态建设。这种跨界合作不仅有助于提升OpenHarmony的应用能力,还将为全产业链带来更多的创新机遇。

圆桌讨论现场照片

收起阅读 »

从更新迭代中找寻发展OpenHarmony技术大会编程语言及开发框架分论坛召开

科技行业注重与时俱进,秉持创新不息的精神,编程语言和开发框架是影响应用体验和应用生态的关键要素,持续更新与发展至关重要。从编程语言Javascript/Typescript,Python,Java,Kotlin,Swift,Rust,ArkTS,再到开发框架W...
继续阅读 »

科技行业注重与时俱进,秉持创新不息的精神,编程语言和开发框架是影响应用体验和应用生态的关键要素,持续更新与发展至关重要。从编程语言Javascript/Typescript,Python,Java,Kotlin,Swift,Rust,ArkTS,再到开发框架Web,Flutter,SwiftUI,JetpackCompose,ArkUI等等的百花齐放,开发者面临着丰富的开发工具和语言的选择。为了更好地推动智能化发展,开发者必须关注万物互联到万物智联的产业趋势,积极拥抱变革,打造适应新场景和新应用模型的解决方案,为未来的智能化发展贡献力量。

2023年11月4日的第二届开放原子开源基金会OpenHarmony技术大会编程语言及开发框架分论坛上,一场关于万物智联时代应用软件开发技术方向与前景的探讨热烈展开。编程语言及开发框架分论坛邀请了OpenHarmony项目群技术指导委员会编程语言TSG负责人、华为编程语言首席专家冯新宇,OpenHarmony项目群技术指导委员会跨平台应用框架TSG负责人余枝强担任出品人。同时,来自华为、英特尔中国有限公司、支付宝(中国)网络技术有限公司、百度、字节跳动等科技企业的技术专家,与南京大学、北京航空航天大学等学术机构的代表共计9位嘉宾齐聚一堂,共同探讨了万物智联时代应用软件开发技术的方向与前景。

出品人:OpenHarmony项目群技术指导委员会编程语言TSG负责人、

华为编程语言首席专家冯新宇

出品人:OpenHarmony项目群技术指导委员会跨平台应用框架TSG负责人余枝强

英特尔中国有限公司WebAssemblyMicroRuntime开源项目技术委员会主席王鑫介绍WebAssembly是吸收了工业最佳实践与学术界前沿研究成果的一门程序语言,从C/C++最早作为支持WebAssembly的前端语言开始,至今有越来越多的前端编程语言如Rust,Go,Kotlin,AssemblyScript等已经支持将WebAssembly作为一个编译目标,促使一个基于WebAssembly的跨语言生态正在快速发展。会上也分享开源项目wasmnizer-ts在编译TypeScript到WebAssembly的技术实践。

英特尔中国有限公司WebAssemblyMicroRuntime开源项目技术委员会主席王鑫主题分享

支付宝终端基础平台技术部负责人黎三平带来《支付宝终端技术实践与思考》主题演讲,他指出在终端技术的迭代演进过程中具有一定的周期规律,从过去单领域工程化的阶段进入到跨领域整合的第二曲线阶段。那么,作为拥有数亿用户的生活服务平台,支付宝的终端技术是如何承载数字化的服务,特别是在面向多设备、多场景下的挑战是如何应对的?其实本质上就是通过编程语言和框架的升级迭代,来提供最佳的解决方案。

支付宝终端基础平台技术部负责人黎三平主题分享

随后,百度资深研发工程师、文心一言APP技术负责人樊中恺在介绍《AINative应用开发》时谈到AINative时代最具革命性的变化是:随版的产品功能迭代转化为随时的模型能力升级。而串联用户和产品的,则是对齐后的快捷交互和自然语言。

百度资深研发工程师、文心一言APP技术负责人樊中恺主题分享

华为2012实验室技术专家凌实认为面向下一代万物互联新生态,亟需突破传统架构,并且携手生态伙伴,方能共建全新的开放Web运行底座。他指出Web技术因其跨端能力和强动态性等特点,在应用开发中扮演着越来越重要的作用。面向应用的场景也对Web技术在性能、扩展性、用户体验等方面提出了相对传统网页场景不同的需求。因此,在OpenHarmony新生态下Web技术迎来了诸多的机会和挑战。

华为2012实验室技术专家凌实主题分享

随后,华为ArkUI技术专家兰守忍认为伴随着移动互联网的高速发展,跨平台框架经历了从Web技术、到原生渲染、到自渲染技术的发展历程。而ArkUI作为OpenHarmony原生应用开发框架,其具备简洁自然的声明式语法,并且已拓展到多个OS平台,基于一套主代码,可构建支持多平台的精美、高性能应用。此外,ArkUI开发框架开发效率上提升代码复用度,用户体验上提供UX一致性和极致的性能,业务上提供新特性快速更新的动态化能力,持续演进框架竞争力。

华为ArkUI技术专家兰守忍主题分享

南京大学计算机科学与技术系副教授,博导陈林则提出在软件生态系统中,混合语言编程会遇到诸多的问题与挑战。他表示混合语言编程是开放互联、协同演进的软件生态系统的自然选择。他主要从软件生态系统质量保障的视角,介绍了胶水代码自动生成和跨语言缺陷分析面临的问题和挑战。提出了一种基于参数化模板重写系统的胶水代码生成方法,可以高效地生成契合实际场景的JavaScript/C胶水代码,同时有效地阻止跨语言边界的软件缺陷。

南京大学计算机科学与技术系副教授、博士生导师陈林主题分享

北京航空航天大学教授、博士生导师史晓华谈到尽管JavaScript程序当前主要通过JS引擎执行,但随着WebAssembly成为越来越受欢迎的底层运行时系统,JWST给出了在WebAssembly运行时之上高效运行JavaScript的另一种选择。并指出JWST是一个将JavaScript程序静态编译为WebAssembly或者native代码的编译器,它采用QuickJS编译生成的字节码作为输入,编译优化后生成LLVMIR,最后通过LLVM后端编译器生成WebAssembly字节码或者nativecode。与此同时,JWST在JavaScript语言规范ECMATEST-262测试集上获得了不低于QuickJS的通过率。

北京航空航天大学教授、博士生导师史晓华主题分享

字节跳动SELab技术专家彭超带来了《移动应用GUI程序测试实践与挑战》分享,通过移动应用GUI程序测试挑战涉猎智能遍历、多机协同、基于函数变更的精准定向和拟人测试等能力,以及基于CV算法的图形异常断言能力带来强关联。

字节跳动SELab技术专家彭超主题分享

华为中央软件院编程语言实验室架构师徐潇在主题为《编程语言并发特性设计及面向移动应用开发的思考》演讲中指出,移动应用"天然异步”的特征决定了其开发语言需要具备对并发的支持,涉及语言、运行时、库及配套工具等。报告中介绍了华为编程语言实验室对上述特性的思考和相关语言特性的设计,以支持应用开发中的高并发编程。

华为中央软件院编程语言实验室架构师徐潇主题分享

通过各位嘉宾的精彩分享,不难看出,编程语言及应用框架是促使操作系统持续、高效发展的的重要组成部分,而应用生态的形成与建立才是最终的目标。OpenHarmony驱动了万物智联形态下应用软件开发的技术创新,从而更好地夯实了技术壁垒,加速了OpenHarmony应用生态的繁荣发展。期待更多合作伙伴加入到OpenHarmony应用生态中来,共同构建万物智联的美好世界。

收起阅读 »

【Java集合】数据结构与集合的神秘联系,一文读懂!

嗨~ 今天的你过得还好吗?变好的过程都不太舒服试试再努力点🌞- 2023.11.06 -上篇文章中我们对单列集合中常用的方法和遍历查询。通过本文章为我们解惑,好好的字符串用起来不就行了,为什么要用集合这些工具类?本篇文章将简要介绍数据结构,让读者了解...
继续阅读 »



嗨~ 今天的你过得还好吗?

变好的过程都不太舒服

试试再努力点

🌞

- 2023.11.06 -

上篇文章中我们对单列集合中常用的方法和遍历查询。通过本文章为我们解惑,好好的字符串用起来不就行了,为什么要用集合这些工具类?

本篇文章将简要介绍数据结构,让读者了解它们在计算机中以何种结构方式存在。那么,什么是数据结构呢?下面我们来详细解释。




数据结构

1.1 数据结构有什么用?

数据结构是计算机存储、组织数据的方式。 数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。 通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。

数据结构往往同高效的检索算法和索引技术有关。 这句话是啥意思呢? 我们举个简单的例子。就像金庸小说中所写的,武功招式就相当于我们的算法,而数据结构就是我们的内功心法;而武功的高低,不仅仅是武功招式,更重要的是 学会的内功心法。就比如张无忌在学会九阳神功之后,就可以大战六大门派。


而数据结构的学习,也会让我们事半功倍。凭借着“数据结构+算法=程序”这句话,Pascal之父获得了图灵奖。


总结来说:

  • 数据结构就是一种是将世界上各种数据转化为计算机可以存储和操作的形式,定义了逻辑结构如何在计算机上存储,以及相关的基本操作。

  • 算法是程序猿通过调用不同数据结构的基本操作,从而实现了数据的处理。

而这两点使我们作为程序开发人员的必备基本功,不是一朝一夕就能成为绝世高手的,我们需要一步步去不断的学习积累,积硅步以致千里。

编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看


1.2 常见的数据结构

在计算机学科中,数据结构是一门很重要的基础学科,知识点很多。在这里我们不讲那么多,只讲述我们集合中用到的几种数据结构,同学们可以下去自行学习更多的数据结构的知识。 常用结构三个:数组、链表、红黑树


我们分别来了解一下:

1)数组

数组的定义:

  • 数组是相同类型数据的有序集合;

  • 数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成;

  • 其中,每一个数据称作一个数组元素,每个数组元素可以通过一个下标来访问它们。


存储思路:

所有数据存储在连续的空间中,数组中的每个元素都是一个具体的数据。




数组的特点:

  • 使用连续分配的内存空间;

  • 一次申请一大段连续的空间,需要事先声明最大可能要占的固定内存空间。

如下图:

 image

  • 通过索引,查询快

  • 当给数组插入新元素时,数组中的a2,a3,a4整体后移,代价高。

  • 如果插入元素时,数组长度,还要重新创建一个数组,然后循环赋值,代价高

优点:

设计简单,读取与修改表中的任意一个元素的时间都是固定的,速度快 。

缺点:

容易造成内存浪费;删除或者插入数据需要移动大量数据,速度慢。


2)链表

每个数据单独存在一小块内存中,这个单元叫做节点,每个节点知道下一个节点的地址,叫做单向链表。 每个节点既知道下一个节点地址,又知道上一个节点地址,叫做双向链表。 image 

链表的特点:

  • 使用不连续的内存空间;

  • 不需要提前声明好指定大小的内存空间,一次申请一小块,按需申请。

image

  • 查询元素,需要通过节点一次向后查找,直到查找到指定元素

  • 增删元素:只需修改连接节点的地址即可。

优点: 充分节省内存空间,数据插入和删除方便,不需要移动大量数据。

缺点: 查询数据必须按顺序找到该数据,操作麻烦。



3)红黑树

简单理解,就是一种类似于我们生活中树的结构,只不过每个节点最多只有两个叶子。计算机世界的树,刚好与我们现实中的树成镜像相反,树根在上,树枝在下。二叉树如下图: image

而我们要说的是二叉树的一种比较有意思的叫做红黑树,红黑树本身就是一颗二叉查找树。我们在这里只需要记住它的特点就可以非常方便的对树中的所有节点进行排序和检索。


小结

本文介绍了三种常用的数据结构:数组、链表和红黑树,以及这些数据结构在计算机中的重要意义。通过学习这些内容,我们可以逐步深入了解计算机世界。



我们下期再见!


收起阅读 »

工作不给转正又不辞退怎么办

工作篇 我在8月底结束了第一份工作后,在9月初迎来了我的二份工作,投简历投了大概一周约了12家面试,虽然那时候是疫情相对来说找工作还是比较容易点的,至少约的到面试,面了差不多6家,拿到了2家,当时情绪低落囊中羞涩想尽快入职工作选择第2家,后面的面试都推了,但是...
继续阅读 »

工作篇


我在8月底结束了第一份工作后,在9月初迎来了我的二份工作,投简历投了大概一周约了12家面试,虽然那时候是疫情相对来说找工作还是比较容易点的,至少约的到面试,面了差不多6家,拿到了2家,当时情绪低落囊中羞涩想尽快入职工作选择第2家,后面的面试都推了,但是问题来了,通勤距离过长,每天公交从首站坐20个站左右去上班,下班在科技园的东门而公司在北门,走大概10分钟,从家里去公交站走路20分钟,于是我就开启了长达单程快2小时的路程上班,房子还有两个月到期我决定到期再搬过来。


u=503144603,235984279&fm=253&fmt=auto&app=138&f=JPEG.webp


这份工作给的工资是6500,试用期80%,也就是拿到手5200,试用期不交社保[刚毕业不太懂社保这个],毕竟刚毕业,然后我就想努力的转正,这次我面试的职位的 .net开发工程师,离谱的事情我刚入职第一天让我接手一个五年经验大佬的项目[压缩成本也不是这样压缩的],那我只是一只小喽喽啊,害,我痛苦极了, 很显然很多东西我接不住


u=473232946,249253702&fm=175&s=D28469A4081186D45A1BA5950300008B&w=640&h=479&img.jpg


但是交接大佬人挺好的这里暂称他为大哥,大嫂还在公司,后面有问题我直接让大嫂帮我问大哥,就暂时还过的去,勉勉强强的干着但是我特别吃力,我的顶头管我的是一个50左右的经理,开发出生头发花白,在公司待了12年,公司是做支付平台的又特别的严格,我之前没有接触过这块,有什么问题我还会问我之前的师傅,就勉强干着,经理每天晚上带我加班,于是我每天早上6点20起床,晚上12点到家,我的长头发嫌洗头发麻烦头发也剪了,不出意外一个月多以后我生病了,大概就是发烧,睡眠不足,经期也有问题,大概休了三天假加周末后来就好了,我当时就想离职了,觉得自己顶不住真的顶不住,第一家公司6天70个小时,第二家6天达到快90个小时,我就差睡在公司了


9KNN0fLVFxnsMU9jUyI2xe0HAqRFN5T1.gif


那段时间我每天都会被骂,就在工位其他同事都听见,我真想找个地缝钻下去,在高强度的工作下,公司每天都在招人,明明已经不缺人,公司试用期三天没工资[如果换我现在肯定告他了],招人又开人,很离谱这样的公司现在还存在,后大概快两个月后我搬家离公司20分钟的车程附近我的日子稍微好过了一点,但是我却出了点问题,我每天焦虑的睡不觉,周末休息也不爱出门就在家里学习看代码,可能是怕被骂,我的精神状态一直不太好,睡觉喜欢抽,蹬腿特别严重时常蹬腿瞪醒,后来我就想着转正了好了,两个多月过去了快转正了,当时11月多了也想着快过年了,在这家公司我每个月还能存2000块钱,5200的工资,因为我几乎不出门就是吃饭睡觉,每天开销60左右,科技园里面吃饭挺贵的我算很省了,在这个公司除了老板跟经理,其他同事相处挺好的,我是里面最小的也挺照顾我的,他们都是25+,于是我就在这样过着,当时也没有意识去医院看看,就想着转正能加1300的工资,对于我来说是一笔不小的诱惑。。。。。


u=1160940878,4065028145&fm=253&fmt=auto&app=120&f=JPEG.webp


转眼三个月过去了公司没有给我转正,也没有开我就闭口不谈,我就急了在等我三个月后的10天左右,我就找人事询问,人事姐姐给我的意见是让我去问经理,经理说我被延迟转正了,说我表现不好,于是我又开始emo了,也不知都怎么办想离职又快过年了就这样混着,想着会找我谈,我还是太年轻了,直到12月20号左右,我找经理谈了大概意思就是要我转岗,说我基础太差了什么的,转做实施去客户现场调试工资4000,我拒绝了我知道这就是想赶我走,于是我离职了,离职后没有压力反而是一种解脱,我记得离职当天去看了场电影,那是我第一次在深圳看电影就在坂田,看完电影后回家哭了一场,然后就休息了两天总是失眠加蹬腿(我不知道大家是否也有过这样现象),医院给我建议是精神太紧张长期睡眠不足经期也不正常,让我好好休息,大概休息了几天月底我就和我同学去了张家界旅游去了,就这样我的第二份工作结束了。。。。。。。。。。。。。。。。


作者:很饿的秃头
来源:juejin.cn/post/7291496777688334336
收起阅读 »

Android 复杂UI界面分模块解耦的一次实践

一、复杂UI页面开发的问题 常见的比较复杂的UI界面,比如电商首页,我们看看某电商的首页部分UI: 上面是截取的首页部分,如果这个首页如果不分模块开发会遇到哪些问题? 开发任务不方便分割,一个人开发的话周期会很长 在XML文件中写死首页布局不够灵活 逻辑和...
继续阅读 »

一、复杂UI页面开发的问题


常见的比较复杂的UI界面,比如电商首页,我们看看某电商的首页部分UI:


Screenshot_2023-11-03-10-57-45-754_com.jingdong.app.mall.jpg


上面是截取的首页部分,如果这个首页如果不分模块开发会遇到哪些问题?



  • 开发任务不方便分割,一个人开发的话周期会很长

  • 在XML文件中写死首页布局不够灵活

  • 逻辑和UI塞在一起不方便维护

  • 首页不能动态化配置

  • UI和逻辑难以复用


那如何解决这个问题? 下面是基于基于BRVAH 3.0.11版本实现的复杂页面分模块的UI和逻辑的解耦。


二、解决思路


使用RecyclerView在BRVAH中利用不同的ViewType灵活的组装页面。但也面临一些问题,比如:



  • 如何实现模块间的通讯和互传数据?

  • 如何实现模块整理刷新和局部刷新?


下面都会给出答案。


三、具体实践


我们先看看模块拆分组装UI实现的效果:


Screen_Recording_20231103_124525_TestKotlin_V1.gif


模块二中有三个按钮,前面两个按钮可以启动和停止模块一中的计数,最后一个按钮获取模块一中的计数值。对应的就是模块间通讯和获取数据。


先看看模块一中的代码:


/**
* 模块一具有Activity生命周期感知能力
*/

class ModuleOneItemBinder(
private val lifecycleOwner: LifecycleOwner
) : QuickViewBindingItemBinder<ModuleOneData, LayoutModuleOneBinding>(),
LifecycleEventObserver, MultiItemEntity {

private var mTimer: Timer? = null
private var mIsStart: Boolean = true //是否开始计时
private var number: Int = 0
private lateinit var mViewBinding: LayoutModuleOneBinding

init {
lifecycleOwner.lifecycle.addObserver(this)
}

@SuppressLint("SetTextI18n")
override fun convert(
holder: BinderVBHolder<LayoutModuleOneBinding>,
data: ModuleOneData
)
{
//TODO 根据数据设置模块的UI
}

override fun onCreateViewBinding(
layoutInflater: LayoutInflater,
parent: ViewGr0up,
viewType: Int
)
: LayoutModuleOneBinding {
mViewBinding = LayoutModuleOneBinding.inflate(layoutInflater, parent, false)
return mViewBinding
}


/**
* 向外暴露调用方法
* 开始计时
*/

fun startTimer() {
if (mTimer != null) {
mIsStart = true
} else {
mTimer = fixedRateTimer(period = 1000L) {
if (mIsStart) {
number++
//修改Adapter中的值,其他模块可以通过Adapter取到这个值,也可以通过接口抛出去,这里是提供另一种思路。
(data[0] as ModuleOneData).text = number.toString()
mViewBinding.tv.text = "计时:$number"
}
}
}
}

/**
* 向外暴露调用方法
* 停止计时
*/

fun stopTimer() {
mTimer?.apply {
mIsStart = false
}
}

/**
* 生命周期部分的处理
*/

override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
when (event) {
Lifecycle.Event.ON_DESTROY -> {
//页面销毁时计时器也取消和销毁
lifecycleOwner.lifecycle.removeObserver(this)
mTimer?.cancel()
mTimer = null
}

else -> {}
}
}

/**
* 设定itemType
*/

override val itemType: Int
get() = MODULE_ONE_ITEM_TYPE

}

模块一向外暴露了startTimer()stopTimer()二个方法,并且让模块一具备了Activity的生命周期感知能力,用于在页面销毁时取消和销毁计时。具备页面生命周期感知能力是模块很重要的特性。


再看看模块二中的代码:


class ModuleTwoItemBinder(private val moduleTwoItemBinderInterface: ModuleTwoItemBinderInterface) :
QuickViewBindingItemBinder<ModuleTwoData, LayoutModuleTwoBinding>(), MultiItemEntity {

@SuppressLint("SetTextI18n")
override fun convert(
holder: BinderVBHolder<LayoutModuleTwoBinding>,
data: ModuleTwoData
)
{

holder.viewBinding.btStartTimer.setOnClickListener { //接口实现
moduleTwoItemBinderInterface.onStartTimer()
}

holder.viewBinding.btStopTimer.setOnClickListener { //接口实现
moduleTwoItemBinderInterface.onStopTimer()
}

holder.viewBinding.btGetTimerNumber.setOnClickListener { //接口实现
holder.viewBinding.tv.text =
"获取到的模块一的计时数据:" + moduleTwoItemBinderInterface.onGetTimerNumber()
}

}

/**
* 可以做局部刷新
*/

override fun convert(
holder: BinderVBHolder<LayoutModuleTwoBinding>,
data: ModuleTwoData,
payloads: List<Any>
)
{
super.convert(holder, data, payloads)
if (payloads.isNullOrEmpty()) {
convert(holder, data)
} else {
//TODO 根据具体的payloads做局部刷新
}
}

override fun onCreateViewBinding(
layoutInflater: LayoutInflater,
parent: ViewGr0up,
viewType: Int
)
: LayoutModuleTwoBinding {
return LayoutModuleTwoBinding.inflate(layoutInflater, parent, false)
}

override val itemType: Int
get() = MODULE_TWO_ITEM_TYPE

}

模块二中有一个ModuleTwoItemBinderInterface接口对象,用于调用接口方法,具体接口实现在外部。convert有全量刷新和局部刷新的方法,对于刷新也比较友好。


接着看看是如何把不同的模块拼接起来的:


class MultipleModuleTestAdapter(
private val lifecycleOwner: LifecycleOwner,
data: MutableList<Any>? = null
) : BaseBinderAdapter(data) {

override fun getItemViewType(position: Int): Int {
return position + 1
}

/**
* 给类型一和类型二设置数据
*/

fun setData(response: String) {
val moduleOneData = ModuleOneData().apply { text = "模块一数据:$response" }
val moduleTwoData = ModuleTwoData().apply { text = "模块二数据:$response" }
//给Adapter设置数据
setList(arrayListOf(moduleOneData, moduleTwoData))
}

/**
* 添加ItemType类型一
*/

fun addItemOneBinder() {
addItemBinder(
ModuleOneData::class.java,
ModuleOneItemBinder(lifecycleOwner)
)
}

/**
* 添加ItemType类型二
*/

fun addItemTwoBinder(moduleTwoItemBinderInterface: ModuleTwoItemBinderInterface) {
addItemBinder(
ModuleTwoData::class.java,
ModuleTwoItemBinder(moduleTwoItemBinderInterface)
)
}

}

class MainModuleManager(
private val activity: MainActivity,
private val viewModel: MainViewModel,
private val viewBinding: ActivityMainBinding
) {

private var multipleModuleTestAdapter: MultipleModuleTestAdapter? = null

/**
* 监听请求数据的回调
*/

fun observeData() {
viewModel.requestDataLiveData.observe(activity) {
//接口请求到的数据
initAdapter(it)
}
}

private fun initAdapter(response: String) {
//创建Adapter
multipleModuleTestAdapter = MultipleModuleTestAdapter(activity)
//设置RecyclerView
viewBinding.rcy.apply {
layoutManager = LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
adapter = multipleModuleTestAdapter
}
//创建ModuleTwoItemBinder的接口实现类
val moduleTwoItemBinderImpl = ModuleTwoItemBinderImpl(multipleModuleTestAdapter)
//添加Item类型,组装UI,可以根据后台数据动态化
multipleModuleTestAdapter?.addItemOneBinder()
multipleModuleTestAdapter?.addItemTwoBinder(moduleTwoItemBinderImpl)
//给所有的Item添加数据
multipleModuleTestAdapter?.setData(response)
}


/**
* 刷新单个模块的数据,也可以刷新单个模块的某个部分,需要设置playload
*/

fun refreshModuleData(position: Int, newData: Any?) {
multipleModuleTestAdapter?.apply {
newData?.let {
data[position] = newData
notifyItemChanged(position)
}
}
}

}

MultipleModuleTestAdapter中定义了多种ViewType,通过MainModuleManager返回的数据,动态的组装添加ViewType


最后就是在MainActivity中调用MainModuleManager,代码如下:


class MainActivity : AppCompatActivity() {

private val mainViewModel: MainViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val activityMainBinding: ActivityMainBinding =
ActivityMainBinding.inflate(layoutInflater)
setContentView(activityMainBinding.root)

//请求数据
mainViewModel.requestData()

//拆分RecyclerView的逻辑
val mainModuleManager = MainModuleManager(this, mainViewModel, activityMainBinding)
//回调数据到MainModuleManager中
mainModuleManager.observeData()

//TODO 如果有其他控件编写其他控件的逻辑

}

}

这样我们通过定义不同的ItemBinder实现了模块的划分,通过定义接口实现了模块间的通讯,通过后台返回数据动态的组装了页面。


其他代码一并写在末尾,方便阅读和理解:


image.png


ModuleConstant


object ModuleConstant {
//ItemType
const val MODULE_ONE_ITEM_TYPE = 0
const val MODULE_TWO_ITEM_TYPE = 1
}

ModuleOneDataModuleTwoData都是data类,内容完全一致,随便定义的:


data class ModuleOneData(
var text: String? = ""
)

ModuleTwoItemBinderImplModuleTwoItemBinderInterface的实现类,通过Adapter能轻松的获取到不同的ItemBinder,所以可以通过接口互相调用彼此的函数。


class ModuleTwoItemBinderImpl(private val multipleModuleTestAdapter: MultipleModuleTestAdapter?) :
ModuleTwoItemBinderInterface {

/**
* 外部实现里面的方法
*/

override fun onStartTimer() {
//通过`Adapter`能轻松的获取到不同的`ItemBinder`,所以可以通过接口互相调用彼此的函数
val moduleOneItemBinder =
multipleModuleTestAdapter?.getItemBinder(ModuleConstant.MODULE_ONE_ITEM_TYPE + 1) as ModuleOneItemBinder
moduleOneItemBinder.startTimer()
}

override fun onStopTimer() {
//通过`Adapter`能轻松的获取到不同的`ItemBinder`,所以可以通过接口互相调用彼此的函数
val moduleOneItemBinder =
multipleModuleTestAdapter?.getItemBinder(ModuleConstant.MODULE_ONE_ITEM_TYPE + 1) as ModuleOneItemBinder
moduleOneItemBinder.stopTimer()
}

override fun onGetTimerNumber(): String {
multipleModuleTestAdapter?.apply {
//通过Adapter可以轻松的拿到其他模块的数据
return (data[0] as ModuleOneData).text ?: "0"
}
return "0"
}

}

interface ModuleTwoItemBinderInterface {

//开始计时
fun onStartTimer()

//停止计时
fun onStopTimer()

//获取计时数据
fun onGetTimerNumber():String
}

四、总结


通过定义不同的ItemBinder将页面划分为不同模块,实现UI和交互解耦,单个ItemBinder也可以在其他页面进行复用。通过后台数据动态的添加ItemBinder页面组装更灵活。任务分拆,提高开发效率。


五、注意事项


1、不要把太复杂的UI交互放在单一模块,处理起来费劲。

2、如果二个模块中间需要大量的通讯,写太多接口也费劲,最好看能不能放一个模块。

3、数据最好请求好后再塞进去给各个ItemBinder用,方便统一处理UI。当然如果各个模块想自己处理UI,那各个模块也可以自己去请求接口。毕竟模块隔离,彼此也互不影响。

4、页面如果不是很复杂,不需要拆分成模块,不需要使用这种方式,直接一个XML搞定,清晰简单。


时间仓促,如有错误欢迎批评指正!!


作者:TimeFine
来源:juejin.cn/post/7296865632166477833
收起阅读 »

Android 签名、打包、上架

最近在做一些简单的Android需求开发,其他打包的过程碰到的一些问题做一个梳理。 【Android需要通过AS-> Open,打开工程,不然容易出问题】 1.签名 a.keystore.jks文件 接受的项目都是已经比较成熟的项目,在项目的目录下都有一...
继续阅读 »

最近在做一些简单的Android需求开发,其他打包的过程碰到的一些问题做一个梳理。
【Android需要通过AS-> Open,打开工程,不然容易出问题】


1.签名


a.keystore.jks文件

接受的项目都是已经比较成熟的项目,在项目的目录下都有一个.jks的文件,里面会包含一些秘钥信息
image.png
在工程中的Android目录下build.gradle(Module:xxxx.app)里面会有秘钥的详细image.png


b.开始签名

image.png
image.png



如果工程中已经有.jks文件,选择Choose existing...选项,选中Project目录中的.jks文件即可.



image.png
然后继续
image.png



至此,打包完成了,根目录下的app文件夹里面找到debugrelease里面就是刚刚打包成功的.apk文件。
如果需要创建新的秘钥



image.png



拓展:怎么生成.jks文件夹、怎么生成签名秘钥



2.生成.jks文件


a.创建并在Project工程目录下生成.jks文件,与app目录同级

image.png


选择Creat new进入创建界面



重要!!! 需要选择项目下的app目录下,然后修改Untitled名称改为keystore.jks,保存即可,保存之后会返回一下界面,填写相关信息即可成功创建相关秘钥,并保存在刚才创建的.jks文件中,保存即可。



image.png


b.配置打包Signing Configs

image.png
image.png
image.png
image.png
Pasted Graphic.png
image.png



按照图示的步骤来,即可完成配置。
然后在app 目录的build.gradle文件中可看到如下生成的代码配置。



image.png



注意:出现如下图示,不影响apk打包,但是有警告,相对路径去怎么解决这个问题,有知道的,可以告知一下。



Pasted Graphic 3.png


3.处理apk包名显示



正常情况下如果是内部软件,不需要加固,如果是外部软件加固一下【腾讯乐固】,对于生成的包名称可以配置显示【名称+版本+版本号+时间】,配置如下:截图框出的方法需要写在andriod方法里面



image.png


// 自定义打包名称
android.applicationVariants.all { variant ->
variant.outputs.all {
outputFileName = "xxxAPK_${buildType.name}_v${versionName}_${generateTime()}.apk"
}
}

构建时间的方法需要在android方法外


//构建时间
def generateTime() {
return new Date().format("yyyyMMddHHmmss")
}

4.加固包重签名处理



AS打包生成的apk包是签名包,上传到 【腾讯乐固】加固后,这时候的加固包是不能直接安装或者上传应用市场,需要在签名一次才可以。以下就是加固包签名的命令行命令



 jarsigner -verbose -keystore xx[jsk文件绝对路径]xx.jks -signedjar xxx[加固前的apk包绝对路径]xxxAPK_release_v1.0.6_20231026092106.apk   xx[加固后的apk包绝对路径]xx.apk  xx[秘钥的名称keyAlias]xx

中间都是空格隔开就可以,主要理解是加固前和加固后的包的位置。然后秘钥keyAlias的名称需要app目录下的build.gradle文件里面找。



至此,可以上传重签名后的apk包到应用市场了 参考



5.相对路径


在Android工程配置中,可以使用相对路径来表达文件或目录的位置。相对路径是相对于当前文件或目录的路径,而不是完整的绝对路径。


以下是在Android工程配置中使用相对路径的一些示例:



  1. 在Gradle脚本中引用相对路径:


def relativePath = '../subdirectory/myfile.txt'


  1. 在AndroidManifest.xml文件中引用相对路径:


<meta-data
android:name="my_data"
android:value="../subdirectory/myfile.txt" />



  1. 在资源文件(如布局文件或字符串资源文件)中引用相对路径:


<ImageView
android:src="@drawable/../subdirectory/myimage.png" />


在上述示例中,相对路径使用../来表示从当前位置向上一级目录的相对路径。你可以根据实际情况调整相对路径的格式和层数。


使用相对路径的好处是,它提供了一种相对于当前位置的灵活方式来引用文件或目录。这样,当你的工程目录结构发生变化时,不需要修改绝对路径,只需调整相对路径即可。


请注意,相对路径的解析取决于当前位置,因此确保当前位置的准确性和相对路径的正确性。


总而言之,使用相对路径可以在Android工程配置中指定文件或目录的位置,使其更具可移植性和灵活性。根据你的具体需求,可以在相应的配置文件或资源中使用相对路径来引用文件或目录。


作者:AKA
来源:juejin.cn/post/7296011286093168659
收起阅读 »

强无敌!一个项目涵盖SpringBoot集成各种场景

大家好,我是 Java陈序员。我们都知道,作为 Java 后端开发肯定绕不开 Spring,而 SpringBoot 的横空出世更是帮助我们开发者可以快速迭代一个项目! SpringBoot 之所以强大,是因为支持自动化配置,可以快速装配组件,如持久化框架缓存...
继续阅读 »

大家好,我是 Java陈序员。我们都知道,作为 Java 后端开发肯定绕不开 Spring,而 SpringBoot 的横空出世更是帮助我们开发者可以快速迭代一个项目!


SpringBoot 之所以强大,是因为支持自动化配置,可以快速装配组件,如持久化框架缓存消息队列日志等等。


今天给大家介绍一个 SpringBoot 集成各种场景的项目,可以用来学习,也可以开箱即用无需重复造轮子


项目简介


spring boot demo 是一个用来深度学习并实战 spring boot 的项目,目前总共包含 66 个集成 demo,已经完成 55 个。


目前已经集成功能:



  • actuator:监控

  • admin:可视化监控

  • logback:日志

  • aopLog:通过 AOP 记录 Web 请求日志

  • 统一异常处理:json 级别和页面级别

  • freemarker:模板引擎

  • thymeleaf:模板引擎

  • Beetl:模板引擎

  • Enjoy:模板引擎

  • JdbcTemplate:通用 JDBC 操作数据库

  • JPA:强大的 ORM 框架

  • Mybatis:强大的 ORM 框架

  • 通用 Mapper:快速操作 Mybatis

  • PageHelper:通用的 Mybatis 分页插件

  • Mybatis-plus:快速操作 Mybatis

  • BeetlSQL:强大的 ORM 框架

  • upload:本地文件上传和七牛云文件上传

  • Redis:缓存

  • ehcache:缓存

  • email:发送各种类型邮件

  • task:基础定时任务

  • quartz:动态管理定时任务

  • xxl-job:分布式定时任务

  • swaggerAPI 接口管理测试

  • security:基于 RBAC` 的动态权限认证

  • SpringSessionSession 共享

  • Zookeeper:结合 AOP 实现分布式锁

  • RabbitMQ:消息队列

  • Kafka:消息队列

  • websocket:服务端推送监控服务器运行信息

  • socket.io:聊天室

  • ureport2:中国式报表

  • 打包成 War 文件

  • 集成 ElasticSearch:基本操作和高级查询

  • Async:异步任务

  • 集成Dubbo:采用官方的starter

  • MongoDB:文档数据库

  • neo4j:图数据库

  • Docker:容器化

  • JPA 多数据源

  • Mybatis 多数据源

  • 代码生成器

  • GrayLog:日志收集

  • JustAuth:第三方登录

  • LDAP:增删改查

  • 动态添加/切换数据源

  • 单机限流:AOP + Guava RateLimiter

  • 分布式限流:AOP + Redis + Lua

  • ElasticSearch 7.x:使用官方 Rest High Level Client

  • HTTPS

  • Flyway:数据库初始化

  • UReport2:中国式复杂报表


项目地址


https://github.com/xkcoding/spring-boot-demo

运行使用


开发环境



  • JDK 1.8 +

  • Maven 3.5 +

  • IntelliJ IDEA ULTIMATE 2018.2 + (注意:务必使用 IDEA 开发,同时保证安装 lombok 插件)

  • Mysql 5.7 + (尽量保证使用 5.7 版本以上,因为 5.7 版本加了一些新特性,同时不向下兼容。项目会尽量避免这种不兼容的地方,但还是建议尽量保证 5.7 版本以上)


代码导入



  1. 使用 git 克隆代码:


git clone https://github.com/xkcoding/spring-boot-demo.git

2. 使用 IDEA 打开 clone 下来的项目



  1. 找到各个 ModuleApplication 类就可以运行各个 demo



注意:



  1. 每个 demo 均有详细的 README,运行 demo 之前记得先看看

  2. 有些 demo 需要事先初始化数据库数据的



模块代码介绍


模块代码介绍


模块代码介绍


模块代码介绍


模块代码介绍


模块代码介绍


最后


推荐的开源项目已经收录到 GitHub 项目,欢迎 Star


https://github.com/chenyl8848/great-open-source-project

或者访问网站,进行在线浏览:


https://chencoding.top:8090/#/


大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!



作者:Java陈序员
来源:juejin.cn/post/7297665681017339958
收起阅读 »

和斯坦福博士写代码的一个月

近一个月,闭关开发了一个面向海外的项目,合作的对象是斯坦福和麻省理工的博士,很荣幸能够和这些全球顶尖大学毕业的大牛合作,今天,趁着周末总结下自己的一些感受。 1. 英语是硬伤 因为项目是面向海外,整个合作全程是英语,这就一览无遗地暴露出自己的英文短板,特别是口...
继续阅读 »

近一个月,闭关开发了一个面向海外的项目,合作的对象是斯坦福和麻省理工的博士,很荣幸能够和这些全球顶尖大学毕业的大牛合作,今天,趁着周末总结下自己的一些感受。


1. 英语是硬伤


因为项目是面向海外,整个合作全程是英语,这就一览无遗地暴露出自己的英文短板,特别是口语,在技术评审过程和讨论中,自己捉襟见肘的英文,只能不断的通过技术流程图和文字来弥补口语表达的不足。


最开始合作时比较羞涩,毕竟知道自己的英语有几斤几两,后面慢慢的放开,把自己的英语老本和盘托出,不过这也坚定了自己加强英文的决心,毕竟做程序开发,英语功底直接决定你理解英文资料的速度和程度。


2. 编程语言要多样


我自己最常用的是 java,但这次项目开发的主要语言是python,另外还涉及到C/C++, java, nodejs。尽管python,C/C++都有所了解,但是,要像 Java一样轻松驾驭去开发这么庞大的工程还是有点吃力。而在合作的过程中,发现他们语言的全能性,python,Java,C++,前端语言,他们都可以平滑切换,绝对的全栈工程师。


这个或许跟每个国家IT环境不一样有很大的关系,但是,作为 Java程序员,我个人还是比较建议再掌握一门动态语言,首选是python,像目前比较主流的数据分析、机器学习、科学计算、自动化和游戏开发等,Python绝对功不可没。另外,通过两种语言的对比,你能更好的看清每种语言的优点和不足,在日常开发中,或许就能借鉴另外一种语言的优点。


3. CR的重要性


CR,就是我们常说的Code Review(代码审查)。在国内的公司工作了这么多年,会做 CR的公司很少,包括一线大厂,能严格执行 CR的更是微乎其微,很多人说是因为业务太多,没有时间做CR,但是,究其原因是国内没有 CR的基因,技术管理者不推动,底层开发人员借着业务繁忙的理由,所以 CR就形同虚设。


下面给了几张项目CR的截图,因为涉及到商业隐私,所以部分信息被打码了:


图一:对于错误的实现,CR 会指正和给出正解。


图片


图二:展示了 CR 甚至细化到了Doc文档级别。


图片


图三:展示了 CR 过程中的探讨,给出自己意见的同时也征询组员更多的idea


图片


CR看似很费时,反反复复的修改和讨论,但是,它和技术方案的讨论不一样,CR针对的是技术方案已经成型的代码实现,所以这个过程更能体现出不同技术人员对同一个技术方案的的思考和实现方式,对于整个项目的质量有大大的提高,另外对于开发者之间的磨合是一个很好的润滑剂。


4. 包容


越优秀的人越懂得包容。这句话放到这个项目上最合适,一个是对于我英语水平的包容。一个是对我 Python 掌握程度的包容。通过上面部分 CR截图也能体现出来。有了这些包容,让我可以更快,更有信心的融入到项目开发。


5. 较真


对于代码实现,技术方案的较真,通过上面的 CR截图也可以体现,他们对于代码的较真可以小到一个Doc文档和标点符号,以及代码的格式化,对于代码性能可以通过大量的技术测试和对比,甚至这块代码不是他们负责的,这个或许就是我们说的技术洁癖和极客精神。


依然记得几年前的一个技术峰会上,某大牛说:中国的码农千千万万,如何让你的代码有亮点,那就是不断的抠细节,大到技术架构,小到标点符号,然后不断的重构,重构。


总结

  1. 通过这个项目,让我有幸见识了从 Google,AWS出来的优秀程序员的编码习惯。
  2. 学英语绝对不是崇洋媚外,它在一定程度上绝对了程序员的技术高度和深度,可以观察你身边优秀的技术人员,英语是不是都不差。
  3. 一定要向优秀的人靠近,和他们一起写代码,勇敢的让他们帮你CR,在交流和讨论中获取更多的葵花宝典;
  4. 程序员是一个终身学习的职业,一定要持续学习;


作者:猿java
来源:juejin.cn/post/7296111218721095715
收起阅读 »

你看现在的程序员,是怎么解bug的

大家好,我是伍六七。 干程序员的都会碰到各种各样的 bug,有的 bug 我们看一眼、或者多看几眼就能看出来问题。 但是,也有很多问题,我们看不出来,这个时候,我们不妨把我们的代码扔给 GPT,看看它能不能帮我们找到 bug,Fix the bug! 我们来看...
继续阅读 »

大家好,我是伍六七。


干程序员的都会碰到各种各样的 bug,有的 bug 我们看一眼、或者多看几眼就能看出来问题。


但是,也有很多问题,我们看不出来,这个时候,我们不妨把我们的代码扔给 GPT,看看它能不能帮我们找到 bug,Fix the bug!


我们来看个例子:


阿七来到这个公司之前,一直在传统 IT 公司工作,从来没用过什么分布式锁、多线程等高端技术。


这次,有一个业务场景,需要使用分布式锁解决问题。阿七按照自己的了解,写了一个分布式锁,结果死活有问题。


看了半天,头都挠凸了,就是不知道怎么回事,这个时候,他想到了大模型,他把问题扔给了 GPT。


prompt:



long currentTimeMillis = System.currentTimeMillis();
final String sequenceId = UUID.randomUUID().toString();
redisLockService.lock(username + currentTimeMillis, sequenceId, 2, TimeUnit.SECONDS);
---
请你分析上面的Java代码,如果有bug,请指出来,并给出正确写法



大模型返回:


这段代码的目的是使用 Redis 锁来对某个资源进行加锁操作。下面是对代码的分析:




  1. 获取当前时间的毫秒数,存储在 currentTimeMillis 变量中。




  2. 生成一个随机的 UUID 字符串,存储在 sequenceId 变量中。




  3. 调用 redisLockService.lock 方法,传入参数 username + currentTimeMillissequenceId2TimeUnit.SECONDS,对资源进行加锁操作。




如果存在 bug,可能是以下几个方面:




  1. username 变量的值是否正确,确保它是唯一的且能够标识资源的唯一性。




  2. redisLockService.lock 方法的实现是否正确,确保它能够正确地加锁并设置过期时间。




  3. 锁的释放是否在适当的时机进行,以避免资源被长时间锁定。




正确的写法可能如下所示:


long currentTimeMillis = System.currentTimeMillis();
final String sequenceId = UUID.randomUUID().toString();
String lockKey = username + currentTimeMillis;
boolean lockAcquired = redisLockService.lock(lockKey, sequenceId, 2, TimeUnit.SECONDS);
try {
if (lockAcquired) {
// 执行需要加锁的操作
} else {
// 锁未获取到,处理相应逻辑
}
} finally {
if (lockAcquired) {
redisLockService.unlock(lockKey, sequenceId);
}
}

在正确的写法中,我们首先获取锁,并在获取到锁的情况下执行需要加锁的操作。然后,在 finally 块中,我们释放锁,确保无论是否成功获取到锁,都能够正确释放资源。




一顿瞎扯,就是没找到最严重的那个 bug,我们继续问它。


prompt:



你没有找到这段代码的最严重的 bug,请你再深入分析分析,找到代码中的严重 bug



大模型返回:


非常抱歉,我之前的回答有误。这段代码存在一个严重的 bug,即在分布式环境下,由于锁的 key 值不唯一,可能会导致锁的误解锁或者锁的覆盖,从而导致并发问题。


正确的写法应该是使用 usernamesequenceId 作为锁的 key 值,以确保锁的唯一性。修改后的代码如下:


long currentTimeMillis = System.currentTimeMillis();
final String sequenceId = UUID.randomUUID().toString();
String lockKey = username + "_" + sequenceId;
redisLockService.lock(lockKey, sequenceId, 2, TimeUnit.SECONDS);
try {
// 执行需要加锁的操作
} finally {
redisLockService.unlock(lockKey, sequenceId);
}

在正确的写法中,我们使用 usernamesequenceId 作为锁的 key 值,以确保锁的唯一性。同时,在 finally 块中,我们释放锁,确保无论是否成功获取到锁,都能够正确释放资源。


这次,我们发现,它找到了代码中最严重的 bug,但是它给出的代码依然没有解决这个 bug,我们继续问它。


prompt;



lockKey 还是没有保证唯一性,并不能解决分布式锁资源不唯一的问题。请你深入思考,再给出正确的设置 lockKey 的代码.



大模型返回:


非常抱歉之前的回答仍然有误。你是正确的,lockKey 的生成方式仍然存在问题,无法保证在分布式环境下的唯一性。


为了确保锁的唯一性,可以使用 Redis 的分布式锁实现方式,例如使用 Redisson 或者 RedLock 等库来实现分布式锁。这些库提供了可靠的分布式锁实现,可以确保在分布式环境下的唯一性和正确性。


下面是使用 Redisson 实现分布式锁的示例代码:


long currentTimeMillis = System.currentTimeMillis();
final String lockKey = "lock:" + username; // 使用固定的前缀加上 username 作为锁的唯一标识
final String sequenceId = UUID.randomUUID().toString();
RLock lock = redissonClient.getLock(lockKey);
boolean lockAcquired = false;
try {
lockAcquired = lock.tryLock(2, TimeUnit.SECONDS);
if (lockAcquired) {
// 执行需要加锁的操作
} else {
// 锁未获取到,处理相应逻辑
}
} finally {
if (lockAcquired) {
lock.unlock();
}
}

在上述代码中,我们使用 Redisson 提供的 RLock 接口来获取分布式锁。通过调用 tryLock 方法来尝试获取锁,设置超时时间为 2 秒。如果成功获取到锁,则执行需要加锁的操作;否则,处理相应的逻辑。在 finally 块中,我们根据是否成功获取到锁来决定是否释放锁。


请确保你已经正确地引入了 Redisson 依赖,并且配置了正确的 Redis 连接信息。


这次,它成功的理解了我们的意思,并且修改了代码,解决了分布式锁资源 key 不唯一的问题。




关注我,送你全套我整理的 Java 岗位面试资料。这是我自己之前整理的面试题,靠着这份面试题,我从 30 人的小公司,进了 2000 人+的央企子公司,之后又进了互联网大厂。


作者:程序员伍六七
来源:juejin.cn/post/7296111218720981027
收起阅读 »

后端除了增删改查还有什么?

大家好,我是伍六七。 刚工作 3、5 年的 Java 程序猿们,在日常工作中开始得心应手,基本上没有什么问题能难倒我们。 这个时候,我们很容易陷入迷茫当中,不知道怎么继续提升自己?怎么才能进阶资深、专家、总监。 知乎上更是有这么一个问题:后端除了增删改查还有什...
继续阅读 »

大家好,我是伍六七。


刚工作 3、5 年的 Java 程序猿们,在日常工作中开始得心应手,基本上没有什么问题能难倒我们。


这个时候,我们很容易陷入迷茫当中,不知道怎么继续提升自己?怎么才能进阶资深、专家、总监。


知乎上更是有这么一个问题:后端除了增删改查还有什么?


我们今天从面试八股文和项目问题,来看这个问题的答案。


面试八股文会问什么?




  • Java 虚拟机 JVM,Java 内存模型 JMM ,垃圾回收算法,垃圾回收器,CMS,G1,ZGC,JVM 调优




  • Java 基础:List,Map,并发包,锁




  • 类加载机制、动态代理、反射、设计模式




  • 多线程、高并发、分布式(分布式原理、分布式锁、分布式事务)




  • sql 优化、索引原理、事务操作




应用层:



  • Spring、MyBatis、Webflux、Redis、Elasticsearch、rpc、DDD 等框架应用以及原理,还有自己的理解。


上面这些东西你会了多少?是知其然还是知其所以然?


通用能力:



  • 操作系统、计算机网络、算法;八大设计原则你能融汇贯通嘛?


架构能力


技术架构、业务架构设计你会多少?能按需设计嘛?


系统的架构为什么要这么设计?解决了什么问题?有没有更好的设计?


学无止境!




或者,我们别那么自信,问一下 GPT :



我是一个Java新手,刚工作 3 年,我认为 Java 后端除了增删改查就没什么了。
请你为我解惑,给出 Java 后端从初级、中级、高级到专家的学习路线



GPT 也会告诉你答案:


Java 后端开发是一个广泛的领域,涉及到许多不同的技术和工具。以下是一个 Java 后端从初级到专家的学习路线,希望能够为您提供一些参考:


初级阶段


在初级阶段,您需要掌握 Java 编程语言的基础知识,包括语法、面向对象编程、异常处理、集合框架等。此外,您还需要了解基本的 Web 开发知识,包括 HTTP 协议、Servlet、JSP等。


以下是一些初级阶段需要学习的技术和工具:


- Java 编程语言


- 面向对象编程


- 异常处理


- 集合框架


- Servlet 和 JSP


- JDBC 和 SQL


- Tomcat 服务器


中级阶段


在中级阶段,您需要深入学习 Java 后端开发的各个方面,包括 Web 框架、ORM 框架、缓存、消息队列等。此外,您还需要了解一些常用的开发工具和技术,例如版本控制、构建工具、测试框架等。


以下是一些中级阶段需要学习的技术和工具:


- Spring 框架


- Hibernate ORM 框架


- MyBatis ORM 框架


- Redis 缓存


- RabbitMQ 消息队列


- Git 版本控制


- Maven 或 Gradle 构建工具


- JUnit 测试框架


高级阶段


在高级阶段,您需要深入了解 Java 后端开发的各个方面,并掌握一些高级的技术和工具,例如微服务、容器化、分布式系统等。此外,您还需要了解一些常用的设计模式和架构模式,例如 MVC、RESTful API等。


以下是一些高级阶段需要学习的技术和工具:


- Spring Boot 微服务框架


- Docker 容器化技术


- Kubernetes 容器编排系统


- Apache Kafka 消息队列


- OAuth2 认证和授权框架


- 设计模式和架构模式


- RESTful API 设计和实现


专家阶段


在专家阶段,您需要成为 Java 后端开发领域的专家,并深入了解一些前沿的技术和工具,例如人工智能、区块链等。此外,您还需要具备一定的架构设计和团队管理能力,能够带领团队完成复杂的项目。


以下是一些专家阶段需要学习的技术和工具:


- 人工智能和机器学习


- 区块链技术


- 大数据和数据分析


- 微服务架构设计和实现


- DevOps 和持续集成/持续交付


- 团队管理和领导力




关注我,送你全套我整理的 Java 岗位面试资料。这是我自己之前整理的面试题,靠着这份面试题,我从 30 人的小公司,进了 2000 人+的央企子公司,之后又进了互联网大厂。


一份让我进大厂&央企的面试题


作者:伍六七AI编程
来源:juejin.cn/post/7296830644113440803
收起阅读 »

揭秘小米手机被疯狂吐槽的存储扩容技术

前段时间,在小米14的发布会上,雷布斯公布了名为“Xiaomi Ultra Space存储扩容”的技术,号称可以在512G的手机中再搞出来16G,256G的手机中再搞出8G。对于普通用户来说,能多得一些存储空间,无异是个很好的福利,不过也有网友说这是以损害存储...
继续阅读 »

image.png
前段时间,在小米14的发布会上,雷布斯公布了名为“Xiaomi Ultra Space存储扩容”的技术,号称可以在512G的手机中再搞出来16G,256G的手机中再搞出8G。对于普通用户来说,能多得一些存储空间,无异是个很好的福利,不过也有网友说这是以损害存储使用寿命为代价的,那么真相到底如何呢?这篇文章我就从技术角度来给大家详细分析下。


认识闪存


首先让我们来了解一些手机存储的基本知识。


手机存储使用的是闪存技术,其本质和U盘、固态硬盘都是一样的。


在闪存中读写的基本单位是页(Page),比页更大的概念是块(Block),一个块会包含很多页。


虽然读写的基本单位都是页,但是写实际操作的很可能是块,这是为什么呢?


这要从删除谈起,在闪存中删除数据时不会立即删除页上的数据,而只是给页打上一个空闲的标签。这是因为谁也不知道这个页什么时候会再写入数据,这样处理起来比较简单快速。


再看写操作,如果写入分配的页是因为删除而空闲的,数据并不能立即写入,根据闪存的特性,此时需要先把页上之前存储的数据擦除,然后才能写入;但是闪存中擦除操作的基本单位是块,此时就需要先把整个块中的有效数据读出来,然后再擦除块,最后再向块中写入修改后的整块数据;这整个操作称为“读-改-写”。当然如果写入分配的页是空白的,并不需要先进行擦除,此时直接写入就可以了。


预留空间


小米这次抠出来的存储空间来源于一个称为“预留空间”的区域,它的英文全称是Over Provisio,简称 OP。


那么“预留空间”是什么呢?我将通过5个方面来介绍它的用途,让大家近距离认识下。


提高写入速度


在上面介绍闪存的基本知识时,我们谈到闪存的写操作存在一种“读-改-写”的情况,因为额外的读和擦除操作,这种方法的耗时相比单纯的写入会增加不少,闪存使用的时间越长,空白的空间越少,这种操作越容易出现,闪存的读写性能下降的越快。


为了提升写入的性能,我们可以先将新数据写入到预留空间,此时上层系统就可以认为已经写入完成,然后我们在后台将预留空间中的新数据和原数据块中需要保留的数据合并到一个新的数据块中,这样就避免了频繁的读-修改-写操作,从而可以大大提高写入速度。


垃圾回收和整理


在上面介绍闪存的基本知识时,我们还谈到删除数据并不是立即清除空间,而是给数据页打一个标签,这样做的效率比较高。这样做就像我们标记了垃圾,但是并没有把它们运走,时间久了,这些垃圾会占用很多的空间。这些垃圾空间就像一个个的小碎片,所以有时也把这个问题称为碎片化问题。


虽然我们可以通过“读-改-写”操作来重新利用这些碎片空间,包括通过异步的“读-改-写”操作来提升上层应用的写入效率,但无疑还是存在写入的难度,实际写入之前还是要先进行擦除。


为了解决上述问题,聪明的设计师们又想到了新办方法:让存储器在后台自动检测、自动整理存储中的数据碎片,而不是等到写入数据时再进行整理。


考虑到闪存的读擦写特性,当需要移除数据块中部分碎片或者将不同数据碎片合并时,就得把需要保留的数据先放到一个临时空间中,以免数据出现丢失,待存储中的数据块准备好之后再重新写入,预留空间就可以用作这个临时空间。


磨损均衡


闪存中每个块的写入次数都是有限制的,超过这个限制,块就可能会变得不可靠,不能再被使用。这就是我们通常所说的闪存的磨损。


为了尽可能延长闪存的使用寿命,我们需要尽量均匀地使用所有的闪存块,确保每个块的使用频率大致相同。这就是磨损均衡的主要目标。


假设我们发现块A的使用频率过高,我们需要将它的数据移动到没怎么用过的块B去,以达到磨损均衡的目的。首先,我们需要读取块A中的数据,然后将这些数据暂时存储到预留空间。然后,我们擦除块A,将它标记为空闲。最后,我们从预留空间中取出数据,写入到块B。实际上,磨损均衡的策略比这更复杂,不仅仅是看使用频率,还需要考虑其他因素,比如块的寿命,数据的重要性等。


可以看到,预留空间在这个过程中起到了临时存储数据的作用。


不过你可能会问,为什么不直接将块A的数据复制到块B,而需要一个临时空间?


这是因为在实际操作中直接复制块A的数据到块B会带来一些问题和限制。


假如直接进行这种数据复制,那么在数据从块A复制到块B的过程中,块A和块B中都会存在一份相同的数据,如果有其他进程在这个过程中访问了这份数据,可能会产生数据一致性的问题。此外,如果移动过程中发生意外中断,如电源故障,可能会导致数据在块B中只复制了一部分,而块A中的数据还未被擦除,这样就可能导致数据丢失或者数据不一致的问题。


而如果我们使用预留空间,也就是引入一个第三方,就可以缓解这些问题。我们先将数据从块A复制到预留空间,然后擦除块A,最后再将预留空间中的数据写入到块B。在这个过程中,我们可以借助预留空间来实现一些原子性的机制,来保证数据不会丢失和数据的一致性。


错误校正


预留空间还可以用来存储错误校正码(ECC)。如果在读取数据时发现有错误,可以用错误校正码来修复这些错误,提高数据的可靠性。


很多同学可能也不了解这个错误校正码的来龙去脉,这里多说几句。


我们知道计算机中的数据最终都是二进制的0和1,0和1使用硬件比较好表达,比如我们使用高电压表示1,低电压表示0。但是硬件有时候会出错,本来写进去的是1,读出来的却是0。为了解决这个问题,设计师们就搞出来个错误校正码,这个校正码是使用某些算法基于要存储的数据算出来的,存储数据的时候把它一起保存起来。读取数据的时候再使用相同的算法进行计算,如果两个校正码对不上,就说明存储的数据出现错误了。然后ECC算法可以通过计算知道是哪一位出现了错误,改正它就可以恢复正确的数据了。


注意ECC能够修正的二进制位数有限,因为可以修复的位数越多,额外需要的存储空间也越大,具体能修复几位要考虑出现坏块的概率以及数据的重要性。


坏块管理


当闪存单元变为坏块时,预留空间可以提供新的闪存单元来替代坏块,此时读取对应数据时不再访问坏块,而是通过映射表转到预留空间中读取,从而保证数据的存储和读取不受影响,提高了固态硬盘的可靠性和耐用性。


综上所述,预留空间在提升固态硬盘性能,延长其使用寿命,提高数据的可靠性等方面发挥着重要的作用。


小米的优化


根据公开资料,小米将预留空间的占比从6.9%压缩到了约3%。


那么小米是怎么做到的呢?以下是官方说法:


小米在主机端也基于文件管理深度介入了 UFS 的资源管理,通过软件实现“数据非必要不写入(UFS)”,通过软件 + 固件实现“写入数据非必要不迁移”,减少写入量的同时也实现了更好的 wear-leveling 和 WAF


还有一张图:



优化解读


这里用了一些术语,文字也比较抽象,我这里解读下:


UFS(Universal Flash Storage)即通用闪存存储,可以理解为就是手机中的存储模块。


“数据非必要不写入(UFS)”也就是先把数据写入到缓冲区,然后等收到足够的数据之后(比如1页),再写入闪存单元,这样就可以减少闪存单元的擦写次数,自然就能延长闪存单元的使用寿命,推迟坏块的产生。这个缓冲区类似于计算机的内存,如果突然掉电可能会丢失一部分数据,但是对于手机来说,突然掉电这个情况发生的几率极低,所以小米在这里多缓存点数据对数据丢失的影响很小,不过还是需要注意缓冲空间有限,这个值也不能太大,具体多少小米应该经过大量测试之后做了评估。


“写入数据非必要不迁移” 没有细说怎么做的,大概率说的是优化磨损均衡、垃圾回收和整理策略,没事别瞎整理,整理的时候尽量少擦写,目的还是延长闪存单元的使用寿命。


“增加坏块预留” 小米可以根据用户的使用情况调整坏块预留区的大小,比如用户是个重度手机使用狂,他用1年相当于别人用4年,小米系统就会增加坏块预留区,以应对擦写次数增加带来的坏块几率增加。注意这个调整是在云端实现的,如果手机不联网,这个功能还用不上。


wear-leveling:就是上面提到的磨损均衡,小米优化了均衡算法,减少擦写。


WAF:写放大,Write Amplification Factor,缩写WAF。写放大就是上面提到的“读-改-写”操作引起的,因为擦除必须擦掉整个块的数据,所以上层系统只需要写一个页的情况下,底层存储可能要重写一个块,从页到块放大了写操作的数据量。因为闪存的寿命取决于擦除次数,所以写放大会影响到闪存的使用寿命。


概括来说就是,小米从存储的预留空间中抠出来一部分作为用户存储,不过预留空间的减小,意味着坏块管理、错误纠正等可以使用的空间变小,这些空间变小会减少存储的使用寿命,所以小米又通过各种算法延缓了手机存储的磨损速度,如此则对大家的使用没有什么影响,而用户又能多得一些存储空间。


小米的测试结果


对于大家担心小米手机存储的寿命问题,小米手机系统软件部总监张国全表示:“按照目前重度用户的模型来评估,在每天写入40GB数据的条件下, 256GB的扩容芯片依然可以保证超过10年, 512GB可以超过20年,请大家放心。”


同时一般固态硬盘往往都拥有5年的质保,而很多消费者往往会5年之内更换手机。因此按着这个寿命数据来看,普通消费者并不用太担心“扩容芯片”的寿命问题。所以如果你的手机用不了10年,可以不用担心这个问题。


当然更多的测试细节,小米并没有透漏,比如读写文件的大小等。不过按照小米的说法,存储的供应商也做了测试,没有什么问题。这个暂时只能相信小米是个负责任的企业,做好了完备的测试。




最后小米搞了这个技术,申请了专利,但是又把标准和技术方案贡献给了UFS协会,同时还要求存储芯片厂商设置了半年的保护期,也就是说技术可以分享给大家,但是请大家体谅下原创的辛苦,所以半年后其它手机厂商才能用上。


大家猜一下半年后其它手机厂商会跟进吗?


关注微/信/公/众/号:萤火架构,提升技术不迷路。


作者:萤火架构
来源:juejin.cn/post/7297423930225639465
收起阅读 »

无悬浮窗权限实现全局Dialog

有些场景下需要显示一些提示弹窗,但把握不好弹出时机容易先弹出弹窗然后界面马上被杀掉进而看不到提示内容,例如强制下线:客户端退回登录界面并弹出提示弹窗。 如果是直接拿的栈顶activity去弹出,没有将弹窗逻辑写到具体activity中,或不好确定activty...
继续阅读 »

有些场景下需要显示一些提示弹窗,但把握不好弹出时机容易先弹出弹窗然后界面马上被杀掉进而看不到提示内容,例如强制下线:客户端退回登录界面并弹出提示弹窗。


如果是直接拿的栈顶activity去弹出,没有将弹窗逻辑写到具体activity中,或不好确定activty的变化就容易出现这种现象。


由于applicationContext没有AppWindowToken,所以dialog无法使用applicationContext创建,要么就使用windowManager配合WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY使用创建全局悬浮窗。但是这种做法需要申请权限。那么,在没有悬浮权限情况下如何做到让dialog不受栈顶activity变化的影响?


我的想法是通过application.registerActivityLifecycleCallbacks在activity变化时,关闭原来的弹窗,并重新创建一个一样的dialog并显示。


效果演示:


1. 栈顶界面被杀


界面退出

2. 有新界面弹出


界面退出

以下是代码实现:


/**
* @Description 无需悬浮权限的全局弹窗,栈顶activity变化后通过反射重建,所以子类构造方法需无参
*/

open class BaseAppDialog<T : ViewModel>() : Dialog(topActivity!!.get()!!), ViewModelStoreOwner {

companion object {
private val TAG = BaseAppDialog::class.java.simpleName
private var topActivity: WeakReference<Activity>? = null
private val staticRestoreList = linkedMapOf<Class<*>, Boolean>() //第二个参数:是否临时关闭
private val staticViewModelStore: ViewModelStore = ViewModelStore()

@JvmStatic
fun init(application: Application) {
application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
topActivity = WeakReference(activity)
}

override fun onActivityStarted(activity: Activity) {

}

override fun onActivityResumed(activity: Activity) {
topActivity = WeakReference(activity)
val tempList = arrayListOf<BaseAppDialog<*>>()
val iterator = staticRestoreList.iterator()
while (iterator.hasNext()) {
val next = iterator.next()
val topName = (topActivity?.get() ?: "")::class.java.name
if (next.value == true) { //避免onCreate创建的弹窗重复弹出
val newInstance = Class.forName(next.key.name).getConstructor().newInstance() as BaseAppDialog<*>
tempList.add(newInstance)
Log.e(TAG, "重新创建${next.key.name},于$topName")
iterator.remove()
}

}

tempList.forEach {
it.show()
}

if (staticRestoreList.size == 0) {
staticViewModelStore.clear()
}
}

override fun onActivityPaused(activity: Activity) {
}

override fun onActivityStopped(activity: Activity) {

}

override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}

override fun onActivityDestroyed(activity: Activity) {
}
})
}
}


var vm: T? = null

init {
val genericClass = getGenericClass()
if (vm == null) {
(genericClass as? Class<T>)?.let {
vm = ViewModelProvider(this)[it]
}
}

topActivity?.get()?.let {
(it as LifecycleOwner).lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
dismissSilent()
}
})
}
}


//用于栈顶变化时的关闭
private fun dismissSilent() {
super.dismiss()
staticRestoreList.replace(this::class.java, true)
}

override fun show() {
super.show()
staticRestoreList.put(this::class.java, false)
}

override fun dismiss() {
super.dismiss()
staticRestoreList.remove(this::class.java)
}


//获取泛型实际类型
private fun getGenericClass(): Class<*>? {
val superclass = javaClass.genericSuperclass
if (superclass is ParameterizedType) {
val actualTypeArguments: Array<Type>? = superclass.actualTypeArguments
if (!actualTypeArguments.isNullOrEmpty()) {
val type: Type = actualTypeArguments[0]
if (type is Class<*>) {
return type
}
}
}
return ViewModel::class.java
}


//自己管理viewModel以便恢复数据
override fun getViewModelStore(): ViewModelStore {
return staticViewModelStore
}
}

参数传递的话,直接通过修改dialog的viewmodel变量或调用其方法来实现。


class TipDialogVm : ViewModel() {
val content = MutableLiveData<String>("")
}


class TipDialog2 : BaseAppDialog<TipDialogVm>() {

var binding : DialogTip2Binding? = null

init {
binding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.dialog_tip2, null, false)
binding?.lifecycleOwner = context as? LifecycleOwner
binding?.vm = vm
setContentView(binding!!.root)

}
}

弹出弹窗


TipDialog2().apply {
vm?.content?.value = "嗨嗨嗨"
}.show()

作者:Abin
来源:juejin.cn/post/7295576843653087266
收起阅读 »

游戏服务器搭建过程中Maven多模块编译遇到的一些问题

游戏服务器有好几个项目,所以涉及到多个模块,这个开发中也是经常遇到的,但是因为之前一直没怎么关注,所以在开发的过程中的一些细节问题还是不懂,这次记录下,也分享下同样的问题给没试过的同学 1、多模块的创建 使用idea进行模块的创建,主要分为以下几步 1.1 父...
继续阅读 »

游戏服务器有好几个项目,所以涉及到多个模块,这个开发中也是经常遇到的,但是因为之前一直没怎么关注,所以在开发的过程中的一些细节问题还是不懂,这次记录下,也分享下同样的问题给没试过的同学


1、多模块的创建


使用idea进行模块的创建,主要分为以下几步


1.1 父模块的创建


直接创建一个Maven项目,在这个过程中选择了Spring web 和Lombok,也可以选择其他的包,到时候省的手写


image.png


1.2 删除垃圾文件


删除所有的文件,只留下pom.xml就行,因为父模块只是做一个模块和依赖管理的作用,因此不需要代码。


image.png


1.3 修改pom.xml


修改这个父模块的pom.xml文件,首先把节点、节点和全部删除:然后修改版本号为自己定义的(方便后续子模块指定父模块)


    <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.13</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>MultMoudle</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>MultMoudle</name>
<description>MultMoudle</description>
<packaging>pom</packaging>
</project>


1.4 创建子模块继承


继承父模块


image.png


可以看到父模块的pom中,已经有子模块的配置了。


2、子模块之间的互相引用


因为有一些接口文件需要在几个项目中共同使用,所以就要抽取相同的文件到common项目中,这个是非常合理的,在room和game中怎么引用呐,非常简单,和其他的jar包一样


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

使用坐标和版本进行引用,刷新引用就可以在引用中看到了


image.png


3、多个模块间版本的管理


多个模块中使用的依赖很有可能会有重复,但是怎么管理各个版本呐?


假设上述module-one和module-two都需要依赖fastjson2,我们之前会在两个模块的pom.xml中加入依赖,但是这样重复的配置看起来似乎不太优雅,如果需要升级要改2个地方,这个时候parent模块的管理功能就可以发挥了


3.1 dependencis


image.png


在上图中,在dependencis中加入了两个常用的工具库,在子模块中即使不加入也可以使用这个库了!因为子模块除了可以使用自己的依赖之外,还会向上查找父模块的依赖,也就是说,父模块的依赖是向下继承的,因此对于所有模块都要使用的依赖,我们可以写在父模块中。


所以,两个模块都依赖于Spring Web话,也可以将两个模块的Spring Web依赖移至父模块。


所以说父模块和子模块中,依赖也有着继承的关系!父模块的properties也是向下继承的。


3.2 dependencyManagement


dependencyManagement用于管理依赖的版本,我们在父模块的pom.xml加入这个标签:


image.png


dependencyManagement的注意事项:


dependencyManagement仅用于管理版本,而不会为自己以及子模块导入依赖,因此在dependencyManagement中声明依赖后,对应的子模块仍然需要在dependencies中加入依赖
在pom.xml中dependencyManagement和dependencies同级,并且dependencyManagement中也需要有一个dependencies
dependencyManagement不仅可以管理子模块的依赖版本,也可以管理自身的依赖版本
若不想让某个子模块使用父模块dependencyManagement的版本,那就在这个子模块的dependencies中声明对应版本
4、依赖执行
因为在项目中使用了mybatis-plus ,在common模块中定义了一些mapper,在room和game中需要使用,在项目中增加了componentscan ,但是依然在运行的时候报错,提示找不到common中的类,在编辑器中并不报错,这很奇怪,需要在依赖项目中增加一个标签。


这是由于Spring Boot打包的模式问题,我们打开被依赖模块module-two的pom.xml文件找到最下面节点中,在spring-boot-maven-plugin插件部分中加入下面配置:



exec
最终common中pom的定义



    
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<classifier>execute</classifier>
</configuration>
</plugin>
</plugins>

</build>

5、在Spring Boot项目中加载依赖项目的组件有几种常用的方法


在Spring Boot项目中加载依赖项目的组件有几种常用的方法:


5.1. 使用@ComponentScan注解:


在主应用程序类上使用@ComponentScan注解,指定要扫描的包路径。这将使Spring Boot扫描并加载依赖项目中带有@Component、@Service、@Repository等注解的组件。例如:


    
@SpringBootApplication
@ComponentScan(basePackages = "com.example.dependencyproject")
public class MyApplication {
// ...
}

2. 使用@Import注解:


在主应用程序类上使用@Import注解,引入依赖项目中的配置类或组件类。这将使Spring Boot加载这些配置类并注册其中的组件。例如:


     @SpringBootApplication
@Import(com.example.dependencyproject.MyConfiguration.class)
public class MyApplication {
// ...
}

3. 使用@Configuration注解和@Bean方法:


如果依赖项目中有@Configuration注解的配置类,可以在主应用程序类中使用@Bean方法来加载其中的组件。例如:


    
@SpringBootApplication
public class MyApplication {
// ...

@Bean
public MyComponent myComponent() {
return new MyComponent();
}
}

这样,MyComponent将作为一个Bean被加载到Spring应用程序上下文中。


根据你的具体情况和依赖项目的结构,你可以选择适合的方法来加载依赖项目的组件。请注意,为了能够加载依赖项目的组件,确保依赖项目已被正确地添加为项目的依赖项,并且在构建和部署过程中能够被正确地引用和访问。


6、@ComponentScan 扫描依赖项的包


@ComponentScan 注解可以用于指定要扫描的包,它的作用不限于只扫描依赖项的包。@ComponentScan`可以扫描指定包及其子包下的组件,并将其加载到应用程序的上下文中。


当你在 Spring Boot 应用程序的主类上使用 @ComponentScan 注解时,它将扫描指定的包及其子包,并注册在这些包中使用了 @Component@Service@Repository@Controller 等注解的组件。


如果你指定的包路径包括了依赖项的包,那么它将扫描并加载依赖项中的组件。但是需要注意,@ComponentScan 不会限制只扫描依赖项的包,它将扫描所有指定的包路径下的组件。


举个例子,假设你的 Spring Boot 应用程序的主类上使用了以下的 @ComponentScan 注解:


    @SpringBootApplication
@ComponentScan(basePackages = {"com.example.myapp", "com.example.dependency"})
public class MyApplication {
// ...
}

在上述示例中,@ComponentScan 将扫描 com.example.myapp 包及其子包以及 com.example.dependency 包及其子包下的组件,并将它们加载到应用程序的上下文中。这样,你可以同时加载依赖项中的组件和应用程序本身的组件。


总之,@ComponentScan 注解不仅限于扫描依赖项的包,它可以扫描指定包及其子包下的所有组件,并将它们加载到应用程序的上下文中。


注:


如果你在 Spring Boot 应用程序的主类上使用了 @ComponentScan 注解,并指定了包路径,只有在指定的包路径下的本项目组件会被自动加载到应用程序的上下文中。


@ComponentScan 注解仅扫描指定的包及其子包下的组件,并将它们加载到应用程序的上下文中。如果本项目中的组件不在指定的包路径下,它们将不会被自动加载。


7、总结


不做不知道,手高眼低不行,必须得实践


作者:香菜菜
来源:juejin.cn/post/7297848688244441122
收起阅读 »

qiankun?这次我选了wujie!

web
写在最前: 本文面向对无界和微前端有一定了解的人群,不再对微前端的概念和无界的基础使用方式做解释说明 前言 掘金上搜wujie,那么大眼一看,好像全都是介绍的,并没有几个落地方案的分享。正好我上个月把部门内三个业务系统用wujie整合了一下,记录成文章和大...
继续阅读 »

写在最前:



本文面向对无界和微前端有一定了解的人群,不再对微前端的概念和无界的基础使用方式做解释说明



前言


掘金上搜wujie,那么大眼一看,好像全都是介绍的,并没有几个落地方案的分享。正好我上个月把部门内三个业务系统用wujie整合了一下,记录成文章和大家分享一下。(为什么不用qiankun?qiankun之前做了好多次了,这次想尝个鲜~)


背景说明


笔者部门内有三个管理系统,技术栈分别是:


A: Vue2 + Webpack4 + ant-design-vue@1.7.8:该项目是部门内“司龄”最长的,从部门成立之初起,所有的业务都堆在里边。


B: Vue3 + Webpack5 + ant-desgin-vue@3.2.20:由于业务目标不清晰以及前端开发各自为战,部分需求被拆出来了一个单独的项目进行开发,但实际上然并卵。


C: Vue3 + Vite2 + ant-design-vue@3.2.20:为了响应领导“统一前端UI规范”和“低代码降本增效”的号召,这个项目应运而生,使用JSON Scheme渲染列表页 + 手写Form表单的形式开发需求。


没错,就是3个纯业务向的管理系统。对接我们部门的大部分业务人员,日常都至少需要操作3个系统,甚至有些人还会用到别的部门的系统,甚至有的人习惯打开多个浏览器tab页来回切换对比同个页面的数据。。。poor guy。。。浏览器密密麻麻的全是tab页。。。


契机


某天,发生了如下对话:



  • 领导:业务部门老大说,系统间来回切换太麻烦了,有没有办法解决这个问题?

  • 我:有,微前端。

  • 领导:之前XXX不是用qiankun做过吗,问题很多,不了了之了。

  • 我:我看过他的代码,没有什么大问题,都是一些细节方面的小bug,而且还有别的微前端方案可以选择。

  • 领导:行,你安排一下,尽快上线

  • 我:好的。( 打工人被安排任务就是这么朴实,无华,且枯燥。。。)


为什么选择无界?


(此处省略万字长文对比分析qiankun、micro app、single-app...)


直接摆出站在个人角度以及团队技术、业务背景下选择无界的原因:



  1. 喜欢吃螃蟹:之前有过多次qiankun的落地经验,直接上qiankun,一点都不酷。(第一次了解到无界是22年的10月份左右,彼时的无界还在beta版,想尝尝鲜。况且就算使用无界出了岔子,也有信心能cover住)

  2. 子应用改造,侵入程度低:就像文档中宣传的那样,我用公司的项目跑demo,除去登录态的因素外,基本可以说是0改动接入,当时脑海中只有2个字----牛X!(当然,仅仅这样接入,离上生产的标准还相距甚远;而且最后我还是选择了类似qiankun根据宿主应用动态选择layout的布局方案,改造成本也可以说是不算低了,这个暂且按下不表)

  3. 方便独立开发、部署:与第2点相似但又不同:现有的项目有独立的域名、部署方案、且在生产环境已经稳定运行,在保留这些基础的前提下,无界的iframe方案算是最理想的出路(另外也有一点私心,如果生产环境的无界挂了,业务人员可以直接使用老的域名访问独立的子应用进行业务操作,毕竟出了生产事故是要通报批评的)


综上所述,确实没经过太多深思熟虑,想用就用,干就完了image.png


干货区


下面,就是在我接入文章开头提到的3个系统后,总结出来的大致接入步骤:



  1. 准备主应用,在接入第一个系统之前,不出意外的要先准备宿主应用。

  2. 子系统登录态管理

  3. 根据宿主环境,选择layout方案

  4. 安装wujieEventBus(基于无界去中心化的通信系统做的二次封装)

  5. 子应用afterMount生命周期

  6. 子系统网络请求管理

  7. UI组件定位修复

  8. 公共状态提升


1.准备主应用


一个比较常规、纯净的管理系统,没有过多的封装,因为宿主应用本身,也不需要什么内容。技术栈为Vue3 + Vite2 + ant-design-vue@3.2.20(没错,和系统C的技术栈一致,主打的就是一个偷懒),放张目录结构大家就明白了,没什么特殊的,有些细节后边会提到。


image.png

2.子系统登录态管理


简单来说,对于一个子应用,无论你是基于JWT还是Cookie的用户鉴权方案,在他单独运行时发生登陆态失效的情况,是要被redirect到自己的Login页面去;而当集成到了无界中运行的时候,登录态失效则应该被redirect到主应用的Login页面。


一般情况下,有两个地方需要做处理:



  1. http响应拦截,以axios为例:


if (response.status === 401) {
if (window.__POWERED_BY_WUJIE__) {
wujieEventBus.$emit("LOGIN_EXPIRED", APP_NAME_IN_WUJIE);
} else {
message.error("登录失效,请重新登录");
router.replace("/login");
}
}

window.__POWERED_BY_WUJIE__是无界注入到子应用window当中的一个全局变量。


wujieEventBus是我对无界自带的去中心化通信方式eventBus的封装,具体内容放在第四点展开讲,这里只需要知道,是通知主应用“我”登录失效了,并且附上“我”在主应用中的身份标识(对应组件方式使用无界的<WujieVue />所需的name属性)



  1. 路由守卫:可根据你的需要更改路由钩子,这里以beForeEach为例:


router.beforeEach((to, from, next) => {
if(validToken()) {
// some your logic ...
next();
}else {
wujieEventBus.$emit("LOGIN_EXPIRED", APP_NAME_IN_WUJIE);
}
}

当然,通过路由守卫拦截下登录态失效的情况可能很少很少,但操作和上面是一样的:通知主应用“我”登录失效了,并且附上“我”在主应用中的身份标识


3.根据宿主环境,子应用动态选择layout方案


如果你的主应用布局是打算这样:


8f1fb5771d3992651707926b38a8e5d.png


子应用甚至不用切换layout方案,在下方content区域中保留子应用所有的模块;上方的Menu区作为一个应用级的切换菜单。


但如果你的主应用是打算像这样常规布局:


b144f129bd3ba973060ee43d6273b1c.png
想实现应用级的切换,大体上有三种思路:



  1. 主应用不设任何layout模块:即Header、Menu、Content全都是子应用的模块。那么就需要所有子应用都是这种布局,且每个子应用的Menu菜单都必须是所有应用菜单的集合,当切换到非自身的路由时,与宿主通信进行应用切换。

  2. 与1相同,Header、Menu、Content全都是子应用的模块,但Menu仍是自己的菜单。你问我怎么切换应用?加个position: fixed的悬浮球呗(或类似的可折叠菜单)。


bb92fd682e09d744792a370c513d41c.png


通过hover悬浮球,展开/折叠菜单,点击进行应用切换。


说实话,这方案我自己都不相信有人会用。950fcc41577cd669da1b68e54714ad8.png



  1. 而第三个,也就是我选择的方案:主应用设有Header和Menu,剔除所有子应用的Header和Menu,只保留子应用的Content模块接入进来。熟悉吗?就是接qiankun那套。


大概长这样:


<template v-if="!isInWujieContainer">
<Menu />
<Layout>
<Header />
<Layout>
<keep-alive>
<router-view />
</keep-alive>
</Layout>
</Layout>
</template>
<template v-else>
<keep-alive>
<router-view />
</keep-alive>
</template>

// const isInWujieContainer = window.__POWERED_BY_WUJIE__

为什么选择方案3,在我看来:Menu维护在主应用中,相比于对每个子应用的Menu进行侵入式改造,开发成本和维护成本都更小。Header维护在主应用中,可以方便的管理路由栈(面包屑、tab页签,这里多提一下,我的子应用接入方式是保活+sync路由同步)


既然Menu维护在了主应用中,那么问题来了:点击了Menu中的某个菜单,怎么通知子应用跳转到对应的路由?


我们都知道,当无界开启了url sync同步的时候,主应用、子应用的url变化规则是:子应用url发生变化时,子应用的iframe会与主应用进行通信,主应用同步更新url;当页面刷新时,子应用iframe会从主应用的url中读取路由信息,保证子应用路由状态不丢失。但是并没有一种规则是主应用主动发起改变url、并且子应用能同步更新路由的方案。


我的做法其实也很简单,点击主应用Menu中的菜单时,通过wujieEventBus进行广播,对应的子应用收到消息时,切换路由:


// 主应用中点击Menu菜单
export const openChildRoute = (
_router: RouterObj,
app: AppCollection,
) => {
// 通知子应用路由已改变,registerMountedQueue可以理解为给子应用注册一个mounted后需要立即执行的事件,防止出现跳转到一个还未初始化的子应用时,$emit miss的问题。
EventBus.$registerMountedQueue(
app,
"CHANGE_ROUTE",
{ path: _router.path, app }
);

// 更新主应用自己的url和tab页签
router.push(fullPath);
store.commit("tabs/setList", {
fullPath,
name: _router?.name || "",
title: _router?.name,
});
setActiveKey(fullPath);
};

// 子应用收到消息
wujieEventBus.$on("CHANGE_ROUTE", function ({ path, query, app }) {
if (app !== APP_NAME_IN_WUJIE) return;
router.push({ path, query });
});

并且CHANGE_ROUTE这个事件可以是双向的:可以由主应用主动发起,通知子应用改变路由;也可以由子应用主动发起,通知主应用改变url和tab页签的显示状态。


企业微信截图_16991865379344.png


之所以这样设计,是因为我们的系统中存在一种特殊的路由页面,他不存在于Menu菜单中,是必须通过点击页面中的指定按钮才能进入。所以对于这类页面,必须是由子应用主动发起的。


4.安装wujieEventBus


无界提供了一套去中心化的通信方案,去中心化的优点显而易见:



  • 不关心发送方和接收方是谁,可以是不同应用之间通信,可以是一个应用内不同路由通信,可以是一个应用内不同组件通信

  • 可以很方便的一对多通信


但同时也有一个致命的缺点:通信成功的前提是建立在通信双方都online的情况下


假设这样一个场景:用户从站外的某个带参链接进入系统,参数的目的是告诉系统要重定向到指定子应用的指定路由,甚至具体要打开某个弹框。


bb72a5d9b7de765bdf88bd8d089d942.png


正常情况下,主应用判断url参数做跳转的逻辑不管放在哪里,都存在子应用未加载完成的可能性。


(如果你说每个子应用component的afterMount事件里都写一遍,fine,你赢了)


这个时候,只需要对无界的eventBus稍作改动,即可满足需求:


import WujieVue from "wujie-vue3";
import { AppCollection } from "@/constant";
import store from '@/store';
const { bus } = WujieVue;
type EventList = "LOGIN_EXPIRED" | "EVENT_NAME1" | "EVENT_NAME2"; // 一些事件类型涉及到公司业务,这里省去了

type EventBusInstance = {
$emit: (e: EventList, params: Record<string, any>) => void;
$on: (e: EventList, fn: (...args: any[]) => void) => void;
$registerMountedQueue: (
app: AppCollection,
e: EventList,
params: Record<string, any>
) =>
void; // 将事件注册到子应用mount成功的的事件队列中
$cleanMountedQueue: (app: AppCollection) => void; // 清空子应用mount事件队列
};

type Queue = {
[app in AppCollection]?: any[];
};

let instance: EventBusInstance | undefined = undefined;

export default () => {
const queue: Queue = {};
if (!instance) {
instance = {
$emit: (event, params) => bus.$emit(event, params),
$on: (event, fn) => bus.$on(event, fn),
$registerMountedQueue: (app, event, params) => {
const isMounted = store.state.globalState.appMounted[app]; // store中存储了子应用是否mount完成的状态
const fn = () => bus.$emit(event, params);

// 子应用已挂载完成可以直接通信
if (isMounted) return fn();

if (queue[app] && queue[app]!.length) {
queue[app]!.push(fn);
} else {
queue[app] = [fn];
}
},
$cleanMountedQueue: (app) => {
while (queue[app] && queue[app]!.length) {
const fn = queue[app]!.shift();
fn();
}
},
};
}

return instance;
};

为每个子应用都维护一个事件队列,主应用通过$registerMountedQueue注册事件时,若对应子应用已经mount完成,则直接emit进行通信;若子应用没有mount完成,则将注册的事件推入队列中。


子应用afterMount钩子中调用$cleanMountedQueue,清空属于自己的事件队列。


目前根据业务需要,只做了这一点封装,后续有可能会继续补充。


当然前边提到的这个场景,肯定还有许多不同的解决方案,根据自己的项目因地制宜才是最重要的。


5.子应用afterMount生命周期


上边第4点已经提到过,子应用afterMount钩子中要做两件事情:



  1. store中保存自己mount完成的状态。

  2. 调用$cleanMountedQueue清空自己的事件队列。


6.子系统网络请求管理


网络请求管理,主要解决的是跨域问题,分两种:




  • 调用后端服务跨域
    如果你的用户鉴权是基于cookie的,那最方便的就是使用无界推荐的方法:将主应用的fetch自定义改写后传给子应用。如果你的用户鉴权是基于JWT或者你使用了其他的http请求库,赶快买上两杯咖啡贿赂一下运维大佬,给子应用对应的服务配置下Response Header,支持主应用域名的跨域资源共享。但是要切记,生产环境不要使用Access-Control-Allow-Origin: *




  • 请求子应用静态资源跨域




刚才为啥要让买两杯咖啡,因为一杯是改后端服务支持跨域,还有一杯是改前端静态资源服务器(比如Nginx)支持跨域。48d109abb6ffcad175c35c4c8ecf90c.png


至此,你(wo)的无界微前端方案已经落地大半了,不出意外的话,除了个别地方的样式比较古怪,业务流程已经没啥大问题了,下面的工作就是各个页面点一点,修一修奇怪的样式问题。


7.UI组件定位修复


无界官方针对element-plus冒泡系列组件弹出位置不正确的解决方案是给子应用的body添加position: relative,但我这边使用ant-design-vue@1.7.8的项目并不是弹出位置不正确,而是弹出方向不对,只能暂时通过调整组件位置+修改placement的方式见一个改一个。48d109abb6ffcad175c35c4c8ecf90c.png


我这边还有一些使用左弹出的drawer组件也会有问题,起始位置并不是屏幕最左边,而是content区域的最左边。


企业微信截图_16991870384309.png


不知是否是无界的bug,drawer有个fixed定位的包裹容器,按理来说,创建这个包裹容器的时候会使用webcomponent代理的appendChild方法,可以突破iframe的区域限制,但通过审查元素发现,这个position: fixed; left: 0的元素,开始位置还是iframe的左侧。。。导致drawerposition: absolute的主体开始位置也只能是iframe的左侧。但又不是所有的左弹出drawer都有这个问题,很神奇。。。没办法,只好把这些有问题的暂且改为右弹出。。。有解决方案的朋友也可以交流一下。。。


8.公共状态提升


其实从这里开始,就属于优化的范畴了,目前只做了这一趴,后续有其他优化会持续补充。


做公共状态提升的原因,简单来讲就是:除了登录用户的信息以外,我们不同系统中也有着很多相同的枚举数据,这些数据本身也是从同样的接口中读的,存在vuex/pinia中。所以当一个系统独立运行时,他数据获取的逻辑不变;当作为子应用接入了微前端体系中时,只需要从主应用中等待数据同步,不需要自己再调接口去取。


// 主应用
export default () => {
const duties = [
// some http request callbacks
];
duties.forEach(async (d) => {
const { action, type, commition } = d;
const data = await action();
store.commit(commition, data);
bus.$registerMountedQueue(
'APP_NAME', // 业务系统name标识
"SYNC_STATE",
{
type,
data: toRaw(data),
}
);
});
};

// 子应用
const state = {
// a vuex state
}

const mutations = {
// a vuex mutation
}

const actions = {
// a vuex action
}

if(window.__POWERED_BY_WUJIE__){
wujieEventBus.$on("SYNC_STATE", ({ type, data }) => {
const [updateFn, stateKey, ...restPath] = type;
let config = state[stateKey];
if (restPath && restPath.length) {
set(config, restPath, data); // lodash set
} else {
config = data;
}
mutations[updateFn](state, config);
});
}else {
// old logic, init all states by actions
}

结语


这篇文章从开篇到写下结语,中间经历了一整个星期。后半部分整体写的比较仓促,可能有些地方和起笔之初的设想有所出入;并且许多的细节之处涉及到公司业务也没有做过多的说明。有不明白的地方、或者有想交流的同学也可以留言,我会尽可能的做答复。


另外做个说明,其实最开始的时候文章标题叫【无界(wujie-micro)微前端落地方案分享】,后来才改成现在这个名字,原因有二:



  • 这并不是一套完整的落地方案,只是我对我落地整个过程中,值得记录、分享的一些点的总结

  • 原先的名字有种让人一看就不想点进来的感觉


48d109abb6ffcad175c35c4c8ecf90c.png

行吧,第一版先到这里,欢迎真诚交流,但如果你来抬杠?阿,对对对~ 你说的都对~


作者:Elecat
来源:juejin.cn/post/7297592806569164810
收起阅读 »

热爱前端,也没能逃过七年之痒

web
大家好,我是杨成功。 从参加工作到今年十月底,我做前端已经整整七年了。都说婚姻有七年之痒,我觉得工作也同样如此。所谓工作的“七年之痒”,即职业倦怠期。我觉得我倒没有倦怠,但感受不一样了。 以前我一直想前端可以干到退休,这是我的理想职业。现在我虽然还是一名前端工...
继续阅读 »

大家好,我是杨成功。


从参加工作到今年十月底,我做前端已经整整七年了。都说婚姻有七年之痒,我觉得工作也同样如此。所谓工作的“七年之痒”,即职业倦怠期。我觉得我倒没有倦怠,但感受不一样了。


以前我一直想前端可以干到退休,这是我的理想职业。现在我虽然还是一名前端工程师,但是工作内容已经离前端越来越远了。


以前我觉得做一个骨灰级程序员、掌握各种牛逼的技术是毕生目标;现在我会想人生精彩多样,多尝试一些不一样的事情不也同样有趣?


1-3 年:热爱、探索


我参加工作很早,二十出头。那时候啥也不懂,但是精力旺盛啥也想学,经常写代码到凌晨 2 点也不觉得累。有一部分人选择前端是因为简单,我就是纯粹的喜欢前端。


前端中有很多好玩的东西,比如各种动画、特效,我都非常感兴趣。在工作中常常因为研究出一种“高级”的写法、实现了某个“牛逼”的功能而沾沾自喜。虽然现在看起来很小儿科,但想起来真让人怀念。


我的第一份工作工资很低(<3k),应该比 95% 的前端都低。当时没有经验,心里想着只要能学到东西就成。在那家公司干了一年多,公司用到的技术基本都学了一遍,进步飞快。“又穷又爱”的状态估计以后再也不会有了。


3-5 年:积累、挑战


工作三年多的时候,我换了家公司,带一个前端小团队,每天都扎在项目里。以前总是追求新技术,怎么花哨怎么来。可负责项目后才发现,解决问题和快速产出才是第一位。


当时的前端非常火热,全社会都是跳槽的机会,跳槽等于涨薪。于是面试变得千奇百怪,大家在卷各种原理、源码、八股文,不管面不面试刷题成了必修课。很多开发者们非常讨厌这些东西,但是又不得不去做。


当然也有好处,就是各种新技术层出不穷。虽然很多都是轮子,但确实有不少突破性的技术,帮助传统前端接触到更广的技术面,能做更多的事情。


我没有花大量时间刷面试题,新技术倒是跟了不少,而且很多都用在了项目中。像 JS 原理题、算法题、某些框架的源码之类,我基本没怎么看过;但是像 Node.js、Android、Linux、跨端开发这些,我花了很多的时间研究,因为确实可以解决项目中的问题。


我一直认为我属于“外卷”类型的:Title 一直是前端,但从不认为自己只是一个前端。什么技术都想试试。所以后来我承担过很多攻坚的角色,像服务器、原生 App、音视频等。我发现能让我上头的可能并不是前端,而是搞定一个难题的快感。


得益于这种心态吧,五年内我积累了很多,但我认为收获最大的是习惯了面对挑战。


5-7 年:瓶颈、迷茫


工作五年以上,年龄直逼 30 岁,好像一瞬间就老了,可我总觉得自己还是个孩子。这个时候总会问自己:我的工作有什么意义?我要一直这样下去吗?我想要什么样的生活?


我是在第 6 年的时候感受到了瓶颈。技术方面一直在进步,但对项目的帮助越来越小———项目进入了稳定期。稳定期意味着没有了涨薪的机会,工作重点逐渐从“怎么实现”变成了“怎么汇报”。以前写日报是“汇总成果”,现在变成了“显得有事可做”。


可能任何一家产品成熟的公司都是这样吧,我不习惯,我还在适应阶段。


从今年开始,我最大的迷茫是工作与生活如何平衡。我在北京这几年,大部分精力都扑在了工作上,家人离的很远,每年见个一两次,也没把谈女朋友当回事。想和家人朋友在一块,可工作又不能放弃。成年人说自己不做选择全都要,而我好像只能二选一。


以前一门心思地想靠技术跳槽、进大厂,今年突然觉得没意思。看到很多人被裁员、加班、互卷,我突然想也许现在挺好的呢?双休不加班、领导也 Nice、没有绩效考核、办公室关系也简单。是不是以前自己太浮躁了,没有好好享受当下呢?


所以,要不要继续写代码?还是回老家做别的事?工作上要不要再卷一点?努力攒钱还是趁年轻消费?要不要参加相亲考虑结婚?一连串的问题汹涌而来。


有些问题能想明白,有些问题还是不明白,但更多的是想明白了也做不到。人的成长流失最快的是勇气,可能某天一件意料之外的事情,会让你一下子做出决定。


写了一本书


工作五年之后,我常常会思考一个问题:如果有一天不做程序员了,我还能干什么?


程序员大概都不喜欢社交吧,或者不擅长社交。我特别羡慕大圣老师,他可以把自己的知识通过视频很生动的表达出来。但我就不行,我好像对镜头恐惧,尝试过好多次全身的不自在。


录视频有难度,不过写文章还行。正好积累了很多知识经验,一边总结一边练笔,于是开始写掘金。后来又碰到个机会写书,我就觉得这个更好,可以把这么多年的经验总结浓缩到一本书里。或许可以帮助一些前端朋友快速进阶,或许还能赚点稿费。


这本书名叫 《前端开发实战派》,还在印刷中,估计两个月后就能成书了。


之后怎么走


七年之前觉得我会写代码到 70 岁,直到写不动了为止。七年之后,我最喜欢的工作依然是程序员,但我不再执着于能不能干到 35 岁了。世界还有很多不一样的精彩,我不能把自己困在程序里。


与那些大厂大佬们相比,我赚的不多,心气也不高。没有想过一定要留在大城市,也不觉得以后有了小孩,就一定要奔着“好的教育”和“名校”去卷,太累了。其实只要没有大城市和名校的执念,生活压力也不会那么大。


这样来看,如果有一天我被裁了,其实也没什么可担心的。选择一个离家近的地方,没有大都市的物欲和诱惑,过一些简单轻松的生活,或许并不糟糕。只是身在大城市,面对万千繁华仿佛难以自拔,但你心里好像知道这不是你追求的,却又停不下来。


我有一个预感,可能 30 岁后不再做程序员了,至少不会只埋头钻研技术。做前端这几年让我在各方面成长迅速,不过做久了也有弊端,比如表达能力、社交能力退化,不擅长处理人际关系,不直接接触商业,而这些往往是人生下半场,决定幸福和事业的关键。


但我依然喜欢技术。无论做什么,技术都会是我自己的优势。


我们大老板是技术出身,孩子都上小学了,还经常熬夜帮我们处理技术难题。有次聚会我问他,公司那么多事情要忙,怎么还有精力写代码呢?他说写代码就是我最放松的时候。我不由得一阵佩服,或许这就是技术人的魅力吧。


但在 30 岁之前,我会继续站在技术一线,做一个什么都搞的前端人。


作者:杨成功
来源:juejin.cn/post/7295551745580793919
收起阅读 »

微信内H5页面唤醒App

web
首先,简述一下这个需求的背景,产品希望能够让用户在微信内,打开一个h5页面,然后就能唤醒公司中维护的app,这个是为了能够更好的引流。 唤醒app的三种方案 IOS系统-Universal Link(通用链接) Universal Links可以通过配置指定域...
继续阅读 »

首先,简述一下这个需求的背景,产品希望能够让用户在微信内,打开一个h5页面,然后就能唤醒公司中维护的app,这个是为了能够更好的引流。


唤醒app的三种方案


IOS系统-Universal Link(通用链接)


Universal Links可以通过配置指定域名路径直接唤醒APP,一步到位


具体配置看这篇文章


juejin.cn/post/693761…


遇到的问题:


apple-app-site-association文件放在app域名(假设: my.app.com/)下


{
"applinks": {
"apps": [],
"details": [
{
"appID": "******",
"paths": [ "/abc/*" ]
},
]
}
}

使用Universal Link其实就是跳转到一个页面(中间页),地址:my.app.com/abc/index.h…


根据上面配置,这个地址是已经固定了的,这需要跟app域名保持一致,并且在paths配置里面的目录下,为了能够获取到apple-app-site-association文件


const universalLink = 'https://my.app.com/abc/index.html?redirectUrl=' + window.location.href
location.replace(universalLink);

如果未下载app,则会跳转失败,在中间页中处理,跳转失败后再返回到当前页面。


<script>
function getQueryStringArgs(url, opt) {
const { decode = true, multiple = false } = opt || {};
const args = {};
if (!(typeof url === 'string' && url.includes('?'))) return args;

const arr = url.split('?');
const qs = arr.length === 2 ? arr[1] : '';
if (!(typeof qs === 'string' && qs.length)) return args;

const items = qs.split('&');
for (let i = 0; i < items.length; i++) {
const meta = items[i];
if (!(typeof meta === 'string' && meta.includes('='))) continue;
const item = meta.split('=');
const key = decode ? decodeURIComponent(item[0]) : item[0];
const value = decode ? decodeURIComponent(item[1]) : item[1];
if (Object.prototype.hasOwnProperty.call(args, key) && multiple) {
const temp = args[key];
args[key] = Array.isArray(temp) ? [...temp, value] : [temp, value];
} else {
args[key] = value;
}
}
return args;
}
const { redirectUrl } = getQueryStringArgs(location.href)
if (typeof redirectUrl === 'string' && redirectUrl) {
location.replace(redirectUrl + '?callType=universalLink') // 处理唤醒app失败场景
}
</script>

上面这段逻辑如果直接放在html中,最好先手动转一下ES5语法,然后压缩一下,这样兼容性好,上面这样展示,是为了可读性好。


总结:


ios系统使用Universal Link在微信和浏览器内都能够正常的唤醒App,且兼容性比较好。但是需要注意中间页域名需要跟app域名保持一致;唤醒app的h5链接域名不能跟中间页域名一致。


直接扫二维码进入另一个页面,需要进行点击操作才能跳转,IOS不允许打开页面立刻就跳转。


URL-Schemes


URL scheme是App提供给外部的可以直接操作App的规则。



  • 比如微信提供了打开扫一扫的URL scheme。weixin://dl/scan

  • 比如支付宝提供了转账的URL scheme。alipayqr://platformapi/startapp?saId=20000116

  • 比如知乎提供了打开回答页面的URL scheme。zhihu://answers/{id}


如何找到某个app的URL Scheme呢?可以看下面这篇文章


zhuanlan.zhihu.com/p/53439246


安卓唤醒app呢,就是使用这种方式


比如:安卓开发提供的是


那跳转的链接是什么样的呢?


const schemeURL = 'myapp://www.myapp.apk'
window.href = schemeURL;

如何判断唤醒失败呢?


没有什么好办法来判断,后面只能触发了唤醒操作之后,监听页面几秒之后是否隐藏来判断,目前默认是2秒


export function getSupportedProperty() {
let hidden;
let visibilityChange;

if (typeof document.hidden !== 'undefined') {
// Opera 12.10 and Firefox 18 and later support
hidden = 'hidden';
visibilityChange = 'visibilitychange';
// @ts-ignore
} else if (typeof document.msHidden !== 'undefined') {
hidden = 'msHidden';
visibilityChange = 'msvisibilitychange';
// @ts-ignore
} else if (typeof document.webkitHidden !== 'undefined') {
hidden = 'webkitHidden';
visibilityChange = 'webkitvisibilitychange';
}

return {
hidden,
visibilityChange,
};
}
/**
* 判断页面是否隐藏(进入后台)
*/

export function isPageHidden() {
const ob = getSupportedProperty();
const hidden = ob?.hidden;
if (typeof hidden === 'undefined') return false;
// @ts-ignore
return document[hidden];
}
/**
* 检测是否唤端成功
* 在唤起执行后,当前页面调用此方法根据页面隐藏变化检测是否唤醒成功
* @param {number} timeout 定时时间,默认2秒
* @return {Object} Promise对象
*/

export function checkOpen(timeout = 2000) {
return new Promise((resolve, reject) => {
const ob = getSupportedProperty();
const visibilityChange = ob?.visibilityChange;

const check = () => {
const pageHidden = isPageHidden();
if (pageHidden) {
resolve(); // 页面被隐藏,说明唤醒成功
} else {
reject(new Error('唤醒超时'));
}
};
const timer = setTimeout(() => {
check();
}, timeout);

const fn = () => {
if (typeof visibilityChange !== 'undefined') {
document.removeEventListener(visibilityChange, fn);
} else {
window.removeEventListener('pagehide', fn);
}
check(); // 唤醒执行后,立马触发页面隐藏变化,可检测是否唤醒成功
clearTimeout(timer); // 未到达指定时间,页面隐藏变化,清除定时器
};

if (typeof visibilityChange !== 'undefined') {
document.addEventListener(visibilityChange, fn);
} else {
window.addEventListener('pagehide', fn);
}
});
}

总结:


安卓使用URL Schemes在微信中是不能跳转的,在浏览器中是能够正常拉起。


微信开放标签


由于在微信环境内,所以可以使用微信提供的能力来唤醒app,微信内禁止使用URL Schemes唤醒app,其实就是微信的一种保护机制。


微信文档:


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



如上图,使用这个功能,有很多限制,而且需要配置,但是为了安卓用户成功引流,产品还是要求使用这个功能。


微信配置


1.关联App-微信开发平台


微信开发平台配置关联App,关联App需要appId,已经有App的域名


微信开发平台地址: open.weixin.qq.com/



2.H5页面域名配置-微信公众平台


JS安全域名需要配置当前h5页面的域名


微信公众号地址: mp.weixin.qq.com/



3.初始化微信SDK,需要获取签名


微信开发SDK文档


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


这需要后端开发接口, 去获取签名



使用微信开放标签说明:


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


async getWxSignatureData() {
const url = window.location.href.split('#')[0];
const res = await getJsapiSignParamers(url);
const { appId, signature, timestamp, nonceStr } = res.data;
wx.config({
debug: false,
appId: appId,
timestamp: timestamp,
nonceStr: nonceStr,
signature: signature,
jsApiList: ['showOptionMenu'], // 必填,故使用一个非实际使用的api用于填充
openTagList: ['wx-open-launch-app'], // 可选,需要使用的开放标签列表
});

wx.ready(() => {
console.info('wx sdk ready');
console.info('调用接口初始化wx sdk 成功');
this.initWxSDKStatus = 'success';
});

wx.error(res => {
console.error('调用接口初始化wx sdk 失败', res);
this.initWxSDKStatus = 'fail';
});
},

接口返回的就是这样的数据结构



只有这样才能正常初始化微信的SDK,只有正常初始化SDK才能够使用微信开放标签的能力。


然后后端开发的时候要注意:签名需要后端配置白名单ip,文档说明如下:


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



安卓手机,如果出现唤醒app之后,打开了应用,但是并未成功唤起,那是因为Android应用有要求,需要安卓开发兼容一下就行了~



微信环境内场景


接下来就分析一下,在微信中有几种分享的场景:


1.微信好友之间链接分享



这种方式,使用微信标签是不能唤醒App的,除非是在关注公众号里面,这个公众号就是上面绑定了JS安全域名的公众号



这样点击这个链接就能正常用微信标签唤醒


2.微信好友之间卡片分享



这种点击打开是能够正常唤醒App的,而且不需要使用公众号,但是这种分享有限制,需要打开页面点击右上角分享给其他好友会带上卡片形式,如果在浏览器中就只是复制链接了,微信不会自动识别成卡片


而且这个分享其实就是微信的一个功能


developers.weixin.qq.com/minigame/de…


3.长按识别二维码识别H5链接



这种也能正常唤醒App,而且不需要关注公众号,也很方便,不需要将链接分享给其他人,只需要将唤醒App的链接做出二维码就行了。


全部流程图


无标题-2023-11-05-1641.png


作者:0522Skylar
来源:juejin.cn/post/7297526380333400083
收起阅读 »

偷偷给网站写了一个霓虹风格计数器

web
阅读原文,体验更佳 👉 http://www.xiaojun.im/posts/2023-… 有很长一段时间,我都想在博客中集成拟物化的访问计数器用于增加一些趣味性,可是我这网站一开始是纯静态的,没用到任何数据库,所以后边不了了之,但最近我在博客中赋予了一些...
继续阅读 »

2023-10-28-retro-hit-counter.webp


阅读原文,体验更佳 👉 http://www.xiaojun.im/posts/2023-…




有很长一段时间,我都想在博客中集成拟物化的访问计数器用于增加一些趣味性,可是我这网站一开始是纯静态的,没用到任何数据库,所以后边不了了之,但最近我在博客中赋予了一些动态能力,这个想法随之也就又浮现了出来。



这个创意最初来自大佬 Joshua Comeau 开源的 react-retro-hit-counter,但后续我产生了自己的一些想法。




本教程不会涉及任何关于数据库的东西,我假设你已经准备了一个数字,不关心你的数据来源,这里就以 1024 来做演示啦~



认识七段数码管


最初我只想实现一个类似计算器那种数字显示效果,它专业点叫做七段数码管(Seven-segment display),你可以在 wikipedia 上见到具体介绍,它一般长下边这种样子,地球人都见过:


image.png


这种形态还是比较好处理的,让我们先实现这个效果,最终要实现的霓虹灯效果也是以此为基础才行。



以下所有组件皆是用 tailwindcss + react 编写,为了教程简练省略了部分代码,具体请阅读源码



SevenSegmentDisplay 组件开发


开发之前让我们先分析该组件有哪些部分构成,它可以拆分为哪些子组件?



  • 入口组件,也就是父组件,我们将它命名为 SevenSegmentDisplay.jsx

  • 数字单元组件,我们将它命名为 Digit.jsx

  • 数字单元的片段,每个数字有 7 个片段,我们将它命名为 Segment.jsx


SevenSegmentDisplay


作为入口组件,它负责接收所有的 props 配置,并且将传入的 value 分解为单个数字后传给 Digit 组件。


import React, { useMemo } from 'react'
import Digit from './Digit'

const SevenSegmentDisplay = props => {
const {
value, // 要展示的数字
minLength = 4, // 最小长度,不足则前补 0
digitSize = 40, // 数字大小(高度)
digitSpacing = digitSize / 4, // 数字之间的间距
segmentThickness = digitSize / 8, // 片段厚度
segmentSpacing = segmentThickness / 4, // 片段之间的缝隙大小
segmentActiveColor = '#adb0b8', // 片段激活时候的颜色
segmentInactiveColor = '#eff1f5', // 片段未激活时候的颜色
backgroundColor = '#eff1f5', // 背景色
padding = digitSize / 4, // 整个组件的 padding
glow = false, // 微光效果,其实就是阴影效果
} = props

// 将传入的 number 类型数字转为 string 并且根据 minLength 传入的长度进行前补 0
const paddedValue = useMemo(() => value.toString().padStart(minLength, '0'), [value, minLength])
// 将补 0 后的数字转为单个字符
const individualDigits = useMemo(() => paddedValue.split(''), [paddedValue])

return (
<div
className="inline-flex items-center justify-between"
style={{ padding, backgroundColor, gap: digitSpacing }}
>

{individualDigits.map((digit, idx) => (
<Digit
key={idx}
value={Number(digit)}
digitSize={digitSize}
segmentThickness={segmentThickness}
segmentSpacing={segmentSpacing}
segmentActiveColor={segmentActiveColor}
segmentInactiveColor={segmentInactiveColor}
glow={glow}
/>

))}
</div>

)
}

export default SevenSegmentDisplay

Digit


一个 Digit 包含 7 个 Segment,通过控制不同 Segment 的点亮状态,便可以模拟数字显示。


import React from 'react'
import Segment from './Segment'

// Segment 排布规则
//
// A
// F B
// G
// E C
// D
//

const segmentsByValue = {
[0]: ['a', 'b', 'c', 'd', 'e', 'f'],
[1]: ['b', 'c'],
[2]: ['a', 'b', 'g', 'e', 'd'],
[3]: ['a', 'b', 'g', 'c', 'd'],
[4]: ['f', 'g', 'b', 'c'],
[5]: ['a', 'f', 'g', 'c', 'd'],
[6]: ['a', 'f', 'g', 'c', 'd', 'e'],
[7]: ['a', 'b', 'c'],
[8]: ['a', 'b', 'c', 'd', 'e', 'f', 'g'],
[9]: ['a', 'b', 'c', 'd', 'f', 'g'],
}

const isSegmentActive = (segmentId, value) => segmentsByValue[value].includes(segmentId)

const segments = ['a', 'b', 'c', 'd', 'e', 'f', 'g']

const Digit = props => {
const { value, digitSize } = props

return (
<div className="relative w-6 h-8" style={{ width: digitSize * 0.5, height: digitSize }}>
{segments.map(segment => (
<Segment
key={segment}
segmentId={segment}
isActive={isSegmentActive(segment, value)}
segmentThickness={segmentThickness}
segmentSpacing={segmentSpacing}
segmentActiveColor={segmentActiveColor}
segmentInactiveColor={segmentInactiveColor}
glow={glow}
/>

))}
</div>

)
}

export default Digit

Segment


根据 segmentId 以及激活状态用 SVG 渲染出对应的 Segment,这是一个不复杂但是比较繁琐的工作 🤖。


import React, { useMemo } from 'react'
import color from 'color'

const Segment = props => {
const {
segmentId,
isActive,
digitSize,
segmentThickness,
segmentSpacing,
segmentActiveColor,
segmentInactiveColor,
glow,
} = props
const halfThickness = segmentThickness / 2
const width = digitSize * 0.5

const segments = {
a: {
top: 0,
left: 0,
},
b: {
top: 0,
left: width,
transform: 'rotate(90deg)',
transformOrigin: 'top left',
},
c: {
top: width * 2,
left: width,
transform: 'rotate(270deg) scaleY(-1)',
transformOrigin: 'top left',
},
d: {
top: width * 2,
left: width,
transform: 'rotate(180deg)',
transformOrigin: 'top left',
},
e: {
top: width * 2,
left: 0,
transform: 'rotate(270deg)',
transformOrigin: 'top left',
},
f: {
top: 0,
left: 0,
transform: 'rotate(90deg) scaleY(-1)',
transformOrigin: 'top left',
},
g: {
top: width - halfThickness,
left: 0,
},
}

// a, d
const path_ad = `
M ${segmentSpacing} ${0}
L ${width - segmentSpacing} 0
L ${width - segmentThickness - segmentSpacing} ${segmentThickness}
L ${segmentThickness + segmentSpacing} ${segmentThickness} Z
`


// b, c, e, f
const path_bcef = `
M ${segmentSpacing} ${0}
L ${width - halfThickness - segmentSpacing} 0
L ${width - segmentSpacing} ${halfThickness}
L ${width - halfThickness - segmentSpacing} ${segmentThickness}
L ${segmentThickness + segmentSpacing} ${segmentThickness} Z
`


// g
const path_g = `
M ${halfThickness + segmentSpacing} ${halfThickness}
L ${segmentThickness + segmentSpacing} 0
L ${width - segmentThickness - segmentSpacing} 0
L ${width - halfThickness - segmentSpacing} ${halfThickness}
L ${width - segmentThickness - segmentSpacing} ${segmentThickness}
L ${segmentThickness + segmentSpacing} ${segmentThickness} Z
`


const d = useMemo(
() =>
({
a: path_ad,
b: path_bcef,
c: path_bcef,
d: path_ad,
e: path_bcef,
f: path_bcef,
g: path_g,
}[segmentId]),
[path_ad, path_bcef, path_g, segmentId],
)

return (
<svg
className="absolute"
style={{
...segments[segmentId],
// 此处用到了 color 它可以很方便的对颜色进行调整
filter:
isActive && glow
? `
drop-shadow(0 0 ${segmentThickness * 1.5}px ${color(segmentActiveColor).fade(0.25).hexa()})
`
: 'none',
zIndex: isActive ? 1 : 0,
}}
width={width}
height={segmentThickness}
viewBox={`0 0 ${width} ${segmentThickness}`}
xmlns="http://www.w3.org/2000/svg"
>

<path fill={isActive ? segmentActiveColor : segmentInactiveColor} d={d} />
</svg>

)
}

export default Segment

基础效果展示


到此,基础的显示组件已经完成了,让我们测试一下显示效果:


www.xiaojun.im_posts_2023-10-28-retro-hit-counter.png


这是它的配置参数 👇


<SevenSegmentDisplay
value={1024}
minLength={6}
digitSize={18}
digitSpacing={4}
segmentThickness={2}
segmentSpacing={0.5}
segmentActiveColor="#ff5e00"
segmentInactiveColor="#161616"
backgroundColor="#0c0c0c"
padding="10px 14px"
glow
/>

粗略一看还不错,但这与霓虹效果还相差甚远,因为它看起来有些扁平,边缘过于“锐利”,不够真实,所以接下来的目标是要把它变得更真实拟物一些。



如果你不需要霓虹效果,其实到这一步就足够了 😣,在我的网站中浅色模式也是使用的扁平风格,只有在切换到深色模式才会显示为拟物风格,算是一个小小的彩蛋吧。



霓虹灯效果


先分析一下为什么上边的样式看上去不够真实?



  1. 也许是曝光问题?真实世界中发光物本身相对于它的边缘来说看上去会更亮、更白,并且会稍微模糊一些。

  2. 很多情况下发光源做不到均匀照射到所有地方,所以会产生一片区域亮一片区域稍暗的效果,如果你留意过,很多透字键盘背光灯就是这样。


基于以上两点,接下来就想办法用 CSS 将它模拟的更真实一些。


让我们在 SevenSegmentDisplay 组件的基础上再封装一个 NeonHitCounter 组件。


模拟曝光过度效果


我们可以使用 CSS 中的 backdrop-filter 属性模拟过曝效果。


const NeonHitCounter = () => {
return (
<div className="relative">
<SevenSegmentDisplay
value={1024}
minLength={6}
digitSize={18}
digitSpacing={4}
segmentThickness={2}
segmentSpacing={0.5}
segmentActiveColor="#ff5e00"
segmentInactiveColor="#161616"
backgroundColor="#0c0c0c"
padding="10px 14px"
glow
/>

<div className="absolute inset-0 z-10 backdrop-blur-[0.25px] backdrop-brightness-150 pointer-events-none"></div>
</div>

)
}

export default NeonHitCounter

在上边代码中我们新建了一个 div 盖在 SevenSegmentDisplay 上边并使用 badckdrop-filter 使组件变亮变模糊,看上去效果已经好了不少。


image.png


模拟亮度不均匀效果


让我们将组件中间部分变得更亮,用于模拟亮度不均匀的效果。我们可以用 radial-gradient 创建一个白色径向渐变盖在它上边,然后通过 mix-blend-mode 来控制混合模式,这里用 overlay 比较合适。



有关 mix-blend-mode 的更多详细介绍你可以参考这篇文章



const NeonHitCounter = () => {
return (
<div className="relative">
<SevenSegmentDisplay
value={1024}
minLength={6}
digitSize={18}
digitSpacing={4}
segmentThickness={2}
segmentSpacing={0.5}
segmentActiveColor="#ff5e00"
segmentInactiveColor="#161616"
backgroundColor="#0c0c0c"
padding="10px 14px"
glow
/>

<div
className="absolute inset-0 z-10 mix-blend-overlay pointer-events-none"
style={{
// 通过 luminosity 获取颜色相对亮度如果一个颜色很亮我们则减少亮度增益
background: `radial-gradient(rgba(255, 255, 255, ${
1 - color('#ff5e00').luminosity()
}), transparent 50%)`,
}}
>
</div>
<div className="absolute inset-0 z-10 backdrop-blur-[0.25px] backdrop-brightness-150 pointer-events-none"></div>
</div>

)
}

export default NeonHitCounter

在上边代码中又创建了一层 div,它利用 radial-gradient + mix-blend-mode: overlay 实现局部颜色增亮,并且根据颜色相对亮度动态判断增益比例,看起来是不是更真实了 👇


image.png



了解相对亮度 👉 developer.mozilla.org/en-US/docs/…



模拟玻璃质感


为了模拟透明玻璃质感,我用 Figma 画了一个 SVG 背景(也可以用 CSS 实现,我偷懒了),另外又用 conic-gradient 实现了 4 颗螺丝效果。


<svg width="76" height="38" viewBox="0 0 76 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.68" clip-path="url(#clip0_467_36)">
<rect width="76" height="38" fill="url(#paint0_radial_467_36)"/>
<rect width="76" height="38" fill="white" fill-opacity="0.01"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M-80.0879 0H191.953V272.041H-80.0879V0ZM54.9326 263.211C125.178 263.211 182.124 206.266 182.124 136.021C182.124 65.7744 125.178 8.8291 54.9326 8.8291C-15.3135 8.8291 -72.2588 65.7744 -72.2588 136.021C-72.2588 206.266 -15.3135 263.211 54.9326 263.211Z" fill="url(#paint1_linear_467_36)"/>
</g>
<defs>
<radialGradient id="paint0_radial_467_36" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(38 19) scale(38 19)">
<stop stop-color="white" stop-opacity="0"/>
<stop offset="1" stop-color="white" stop-opacity="0.05"/>
</radialGradient>
<linearGradient id="paint1_linear_467_36" x1="-8.40528" y1="-21.8896" x2="68.8142" y2="-4.89117e-06" gradientUnits="userSpaceOnUse">
<stop offset="0.199944" stop-color="white" stop-opacity="0.26"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<clipPath id="clip0_467_36">
<rect width="76" height="38" fill="white"/>
</clipPath>
</defs>
</svg>

import React from 'react'
import SevenSegmentDisplay from '@/components/SevenSegmentDisplay'
import clsx from 'clsx'
import color from 'color'

const Screw = props => {
const { className } = props

return (
<div
className={clsx(className, 'w-[5px] h-[5px] rounded-full ring-1 ring-zinc-800')}
style={{ background: `conic-gradient(#333, #666, #333, #666, #333)` }}
>
</div>

)
}

const NeonHitCounter = () => {
return (
<div className="relative">
<SevenSegmentDisplay
value={1024}
minLength={6}
digitSize={18}
digitSpacing={4}
segmentThickness={2}
segmentSpacing={0.5}
segmentActiveColor="#ff5e00"
segmentInactiveColor="#161616"
backgroundColor="#0c0c0c"
padding="10px 14px"
glow
/>

<div
className="absolute inset-0 z-10 mix-blend-overlay pointer-events-none"
style={{
background: `radial-gradient(rgba(255, 255, 255, ${
1 - color('#ff5e00').luminosity()
}), transparent 50%)`,
}}
>
</div>
<div
className="absolute inset-0 z-10 backdrop-blur-[0.25px] backdrop-brightness-150 pointer-events-none"
style={{
backgroundImage: 'url(/hit-counter-glass-cover.svg)',
backgroundSize: 'cover',
backgroundPosition: 'center',
boxShadow: `
0 0 1px rgba(255, 255, 255, 0.1) inset,
0 1px 1px rgba(255, 255, 255, 0.1) inset
`,
}}
>

<Screw className="absolute left-1 top-1 -rotate-45" />
<Screw className="absolute left-1 bottom-1 rotate-45" />
<Screw className="absolute right-1 top-1 rotate-45" />
<Screw className="absolute right-1 bottom-1 -rotate-45" />
</div>
</div>

)
}

export default NeonHitCounter

大功告成 ✨


image.png


作者:xiaojundebug
来源:juejin.cn/post/7297487761615552564
收起阅读 »