注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

写给想转行学IT的朋友们的话

最近有很多朋友咨询我关于想转行学IT的问题,我想结合自身的经历和思考和各位聊几句,本文带有比较强的主观色彩,因此文中观点仅供参考,如有不当之处,敬请海涵。 笔者19年本科毕业于一所中流211,毕业之后一直在做Java开发,我并没有进大厂,也没有年薪百万,就是芸...
继续阅读 »

最近有很多朋友咨询我关于想转行学IT的问题,我想结合自身的经历和思考和各位聊几句,本文带有比较强的主观色彩,因此文中观点仅供参考,如有不当之处,敬请海涵。


笔者19年本科毕业于一所中流211,毕业之后一直在做Java开发,我并没有进大厂,也没有年薪百万,就是芸芸众生中普通的那一个。现在回顾当时入行的经历,基本是摸着石头过河,因此在有了一些经验之后,就想给和我当时一样处境的朋友一点建议,希望能对你有所启发。


很多问题其实都要结合个人的实际情况来看,每个人的知识、能力、经历都各不相同,所以无论做出任何决定,都需要结合自身的情况。


是否有必要参加培训机构?


这个问题的答案是因人而异的,核心点在于你是否有足够的恒心和自制力。


现在互联网上的学习资料已经非常多了,足够支撑你从零学到能够入行的过程。如果你的自制力比较好,那么你就可以尝试自学,不过自学的过程是孤独的,也是难熬的;如果你的自制力一般,无法在不是“学校”那么环境下进行学习,那就可以考虑培训机构。


需要特别强调的是,不能以为进了培训机构就以为一定能够就业了,说到底,学习这件事情,还是要靠你自己,别人是没有办法把知识灌进你的脑袋里的。培训机构也有一些无法忽视的问题,当你毕业之后,大概率机构会给你伪造一份简历,本来学习的时间就并不是很长,你很快可能就会发现,培训机构里面学的比较浅,还不足以应付面试官的八股文,这时候你可能就会想先入行再说,然后进了一家外包公司,然后开始混日子,这也是笔者见过的最多的案例。


参加培训最大的还是简历,培训机构给大家做的项目都是极其雷同的,也可能会在培训结构之后,让你自己编项目经验,有经验的面试官很容易就能判断出,你是真的参与了项目开发,还是只是包装的。另外你可能还会碰到,很多公司需要上一家公司的离职证明和银行流水,甚至需要你个税APP报税界面的录屏等等问题,这些培训机构并不会告诉你。


总而言之,转行是一件对你的人生来说,是一件极其重要的事情,要反复思虑,不要脑子一热,听身边的朋友说,那个谁谁谁培训了几个月就月入过万,然后就稀里糊涂花了几万参加了培训机构,结果培训结束真正找工作的时候又发现困难重重,现实往往比想象的残酷。


高中毕业可以从事IT行业吗?


真相是,如果要从事IT行业,大专及以上学历是基本要求,不排除个人大专以下学历也找到了很好的IT工作,但是在当下这么“卷”的环境下,就是幸存者偏差了。


大专的学历我也非常建议你先提升学历,有一个本科学历。


前端、后端还是测试?


其实从某种意义来说,选择岗位就是选择某种编程语言,选择编程语言也是在选择岗位,这个问题,你也可以结合下文编程语言之间的对比,找到自己合适的岗位。大体而言,测试、前端、后端,这三者的难度是依次递增的(对大部分人而言)。


如果你没有科班背景,但是又想从事IT行业,那么你可以考虑从事测试、运维工作甚至产品、运营的工作,其实开发写代码并不是唯一的选择。


如果你有一定的基础,但是逻辑思维又不是那么强,那么我建议你可以考虑前端,女生大部分学习编程都会选择前端,前端由于不需要考虑架构、性能(大部分场景),因此难度会小一些,对初学者更加友好。不过,前端经过这些年的发展,知识体系俨然已经非常庞大,后期也需要学习很多的东西。


如果你有一定的基础,且逻辑思维还可以,那么我建议你可以考虑后端,如果你不知道自己的思维能力怎么样,可以学一些Java或者JavaScript语言基础,看看自己是不是能够比较轻松的学会。


Java、Python、C++还是JavaScript?


JavaScript


作为一门十天就被开发出来的编程语言,JavaScript身上的历史包袱也有很多,但这并不妨碍它的伟大,JavaScript在我们的日常生活中,几乎无处不在。


JavaScript也是一门易学难精的语言,虽然上手比较容易,但是后期也需要很多的时间去理解和巩固。JavaScript是前端程序员最重要的技术,学好了JavaScript,就能在前端这个领域里无所不能。JavaScript现在也开始逐渐的在其他领域崭露头脚,它与TypeScript相辅相成,可以预见,JavaScript是一门历久弥新,并且前景良好的编程语言。


Java


如果你已经决定了要从事软件开发行业,但是还不清楚,要选择哪一门编程语言,那我推荐你可以学习Java,Java这门语言本身虽然并不优秀,截止目前,Java已经走过了20个年头,有着非常丰富的生态,web端,它有JSP、Servlet,移动端,它有Android,服务器端有SpringBoot,桌面端它有JavaFX,也有非常优秀的网络通信框架,比如Netty,甚至它也做出过操作系统(塞班系统),可以说,Java虽然很烂,但是它真的几乎无所不能。


不可否认Java的成功,但是Java也存在一些隐患,比如在Oracle收购了Sun公司之后,在Oracle不断地花式作妖下,Java的未来似乎也有些扑朔迷离。除此之外,Java在引以为傲的领域也逐渐有了一些竞争对手,比如服务端有go语言、nodejs,Android的开发官方首选语言已经变成kotlin,JSP的市场基本上已经被Vue、React等SPA框架所替代,但是Java却并没有开拓出自己新的应用场景,虽然笔者认为,基于VM的语言并不是消失,但是Java是能否一直守住自己的王座,还是要打一个问号的。


Java目前最多的就业方向就是服务端开发,如果你学习了Java,那么大概率会做服务端开发,短期内,服务端开发Java还是很难被其他语言替代。强类型的语言加上对并发编程的支持,让Java非常适合构建大型的服务端应用,这也是Java最深耕的方向,学会Java服务端的开发,也比较方便向大数据或数据分析岗位进行转型。


最后我想说,Java的岗位在我提到的编程语言里面也是最多的,这也是在选择编程语言很重要的一个需要考虑的点,很多编程语言,看起来很热门但是,真正学完去找工作的时候,发现岗位少的可怜,没错,说的就是你,Python。


Python


Python这门编程语言,“胶水语言”的特性让它看起来也几乎无所不能,虽然这种能力很有可能来自其他的语言,Python只是作为客户端,调用其他语言的类库。


不够“底层”其实并不是什么缺点,学习Python最大的问题是,是对学历有要求,Python应用最广泛的领域,大部分都对学历有要求,虽然你可能看过很多投放Python的广告,自动化办公、爬虫等等,但是这些特性在企业中的岗位是比较少的,而Python擅长的算法、人工智能、深度学习等领域都是需要研究生学历的。


我们总结一下,如果你有研究生学历,那么学习Python是一个不错的选择,不过我也见过有的研究生朋友学完Python之后发现岗位很少,又被迫转Java的案例。


C++


C++相比于其他的编程语言,其入门的门槛的会更高,花费在学习语言本身的时间的也会很多。C++在经历过这么多年的迭代之后,语言的特性非常多,会让初学者有一种眼花缭乱的特性,实现同一个功能,可能会有很多种写法。不可否认,C++是一门优秀的编程语言,但是高昂的学习成本也让人望而却步,如果你不是科班出身,只是想学一门技术进入互联网行业,那C++并不是一个很好的选择。


C++常见的就业方向有以下几种。



  • 服务器端开发

  • 游戏

  • QT

  • 嵌入式

  • 人工智能


总结来说,C++最适合的还是那些对性能有要求的场景。原来很大一部分C++程序员都是做QT开发,但是C++在桌面端的市场已经出现了越来越多的竞争者,比如Hybrid 技术、React Native、Weex、Flutter,这些技术让原来web端的程序员或者移动端的程序员也能写出跨平台的应用,并且这些技术保留了他们原本的开发习惯,这对QT的打击,无疑是毁灭性的。


那么C++是不是已经过时,或者即将被淘汰呢?其实不然,C++只是让出了一些自己不是那么擅长的领域,但是在底层应用的开发,C++丰富的生态和优越的性能还是首选。如果你有志在这些领域发展,那么C++将是你很好的选择。


随着越来越多的人对核心自研技术的重视,这种比较偏底层的岗位,会越来越多,最近几年,国产操作系统的发展就是一个很好的例子,在政策的扶持下,各家公司几乎都在号称自研操作系统,可以预料,在不远的将来,这些核心的系统的研发,都需要大量C/C++语言的人才。


路漫漫其修远兮,吾将上下而求索


我非常建议你在决定入行之前,提前找一些学习资料,自己尝试一下是否能够学的清楚,并且能从中获取乐趣。


兴趣和恒心是决定你能不能在这个行业长足发展的决定性因素。面对海量的,对你而言是闻所未闻的知识,如果没有兴趣,你每天都会深受折磨;这个行业特质决定了我们需要不断地学习,没有恒心,总有一天会掉队,跟不上技术更新迭代的脚步。


总而言之,道阻且长,希望本文能对你有所启发,与君共勉。


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

浅谈敏捷开发与互联网江湖的爱恨情仇!

敏捷开发 通常互联网产品都采用一套称之为 敏捷开发 的研发流程 @需求阶段 定方向:企划部门和产品部门一道确定产品方向; 出需求:产品经理提供: 需求文档(对产品体系和功能的详细描述,常用的格式为Word配合PPT) + 产品原型(一个能在PC/手机浏览器...
继续阅读 »

敏捷开发


通常互联网产品都采用一套称之为 敏捷开发 的研发流程


@需求阶段


image.png



  • 定方向:企划部门和产品部门一道确定产品方向;

  • 出需求:产品经理提供: 需求文档(对产品体系和功能的详细描述,常用的格式为Word配合PPT) + 产品原型(一个能在PC/手机浏览器上进行页面跳转和简单交互的demo);


image.png


@研发阶段



  • 研发:前后端开发和美术部门根据产品需求开始工作;

  • 设计师/美术:根据产品原型产出一整套页面设计图;


image.png



  • 服务端/后端:根据产品功能说明着手数据库设计和接口设计,产出接口文档,供前端调用以获取业务数据;

  • 客户端/前端:根据设计效果图开发PC/手机H5/小程序/App等用户端的界面 + 数据渲染 + 用户交互;

  • 接口联调:开发期间前后端开发者需要进行接口联调,以确保前端能通过后端提供的接口正确地获取到数据;


@测试阶段



  • 提交测试:前后端共同开发完毕后,打包成品供测试人员进行测试;

  • 产品测试:测试人员代入用户的角色,带着挑剔的眼光和看我不整死你的愉快心情对产品进行使用测试,并将宏伟的BUG列表这口黝黑锃亮的大锅戴在项目经理头上;


image.png



  • 修复BUG:项目经理在开发团队(前后端)内对锅进行二次分配,精确到具体责任人;

  • 反复完善:开发人员修复BUG,再次打包提交测试,测试再挑刺-开发再分锅修改-再提交测试,重复以上循环若干遍以后,测试表示产品凑合可以上线了,签字画押;

  • 产品上线:运维同学将产品部署到生产环境与用户见面;


程序员在产品上线时的样子↓
image.png


@维护阶段



  • 升级维护:产品上线以后还需要进行多个版本的迭代升级,包括新一轮的需求的开发,以及收集和修复用户反馈的产品BUG等,于是就继续产生2.0、3.0...


互联网江湖的爱恨情仇



  • 通常当开发进度完成到99%的时候,产品经理脑门一拍,哎呀这个地方可能得小改一下,很简单的你这样弄一下就行:XXOO@#$%^&*blablabla...,一想到打人要进橘子你默默地收起了磨过无数回的菜刀,开始彻底推翻重做...

  • 针对上述情况美术人员表示我早就习惯了;

  • 当然产品和研发的博弈为我们的茶余饭后提供了不少欢乐的素材


image.png


其实谁还不是一样...


image.png




“这不需要测试,肯定是好的,不必担心!”


image.png

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

离职后在家躺了一年,顺便写了本书

大家好,我卡颂。 从字节离职已经一年多了。 最开始离职的契机是电子社的编辑向我约书稿。以我的能力是没法同时兼顾字节的高强度工作以及在规定的时间内成书。 考虑到进厂上班的机会以后还有(但现在这行情,没准儿也没厂子要我也说不定),但写书的经历还没体验过,于是就辞职...
继续阅读 »

大家好,我卡颂。


从字节离职已经一年多了。


最开始离职的契机是电子社的编辑向我约书稿。以我的能力是没法同时兼顾字节的高强度工作以及在规定的时间内成书


考虑到进厂上班的机会以后还有(但现在这行情,没准儿也没厂子要我也说不定),但写书的经历还没体验过,于是就辞职了。


接下来聊聊程序员写书是种什么体验。


欢迎加入人类高质量前端框架研究群,带飞


为什么写书?


在大概5年前,写书还是一种行之有效的提升个人影响力的方式。


很多人在找工作前、创业前,都会写本书作为个人能力的背书


甚至还出现专门帮人写书的公司,这些公司的目标客户是那些由于某些契机突然出名的草根(比如一些现象级语言类综艺的选手)。


但最近几年,相比写书短视频塑造个人人设是更短平快的提升个人影响力途径。所以,写书的人就逐渐少了。


而且,写书真的不赚钱。卖一本书的版税收入可能就几块钱。相比于赚钱,一些急于通过写书提升个人影响力的人,甚至得反向给出版社钱,比如花钱买书号(1w多)。


所以,对于为什么写书,主要是想体验下这段经历,以及可能带来的些许个人影响力提升。


什么样的程序员能写书?


可能你会觉得写书是件很厉害的事情,那你觉得写一本掘金小册厉不厉害呢?


如果你觉得写一本掘金小册也挺厉害,那写某一技术领域的系列文章厉不厉害呢?


好像还行对吧,那深入学习某个技术领域厉不厉害呢?


是不是努努力就能达到?


实际上这是条很平滑的路径,就像打游戏升级一样,在很多关键节点都会有任务npc主动找你(比如小册运营,出版社编辑)。


所以,与其说写书很厉害,不如说写书的人很执着,把这条支线任务打通关了(虽然任务奖励并不丰厚)。


我写了本什么书?


书名叫React设计原理,基于React18,从理念、架构、实现三个层面解构React



虽然在此之前写过一本开源电子书react技术揭秘,但既然要出版,就好好对待吧。


于是,在规划这本书时,我主要从两方面下手:




  • 内容尽可能硬核




  • 怎么讲读者才听得懂?




先说第一点,为了理通react运行的方方面面,除了常规的阅读源码跟踪核心成员的各种进展外,为了搞懂React18的运行细节,我从0实现了一个React18,所有实现步骤可以在big-react项目中看到。


内容硬核很重要,但写出来能让人看懂同样很重要。为了达到这个目标,我主要参考了两个优秀的作品:



前者在行文过程中一直在提倡知识屏蔽(在教学过程中只关注当前学习的知识,屏蔽超纲知识对读者的干扰的原则),后者做到了理论与实践(丰富的在线示例)结合。


所以,我这本书存在两条脉络:




  • 抽象层级逐渐降低




  • 实现越来越复杂的模块




对于前者,本书的抽象层级会逐渐从理念到架构,最后到实现,每一层都屏蔽前一层的影响。


这也是为什么ReactDOM.createRoot这个初始化API会放到第六章再讲解 —— 在这个具体API的背后,是他的理念与架构。


对于后者,本书会从0实现与react相关的6个模块,最后我们会一起在React源码内实现一个新的原生Hook


一点感悟


这一年时间有很多朋友问我为什么不去找个班上。答案很简单 —— 因为我不喜欢上班。


所以,为了做喜欢的事,就得提前准备。就拿写书这件事来说,就像做游戏任务,他的每一步都是有迹可循的。


如果你也对现状不满,那就行动起来吧。为了想要的生活而探索,这个过程本身就充满了乐趣。


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

考研失败,加入国企当程序员,真香!

下面是正文。 最近考研出成绩了,大家考得怎么样?分享一个自己考研失败后,入职国企的故事。 1、考研失败 我是工作了3年后才参加考研的。 老家成都,本科毕业于帝都某所以能歌善舞著称的985学校,哲学专业。 大学选专业时家里人不懂,自己全凭爱好,第一志愿就是哲学...
继续阅读 »

下面是正文。




最近考研出成绩了,大家考得怎么样?分享一个自己考研失败后,入职国企的故事。


1、考研失败


我是工作了3年后才参加考研的。


老家成都,本科毕业于帝都某所以能歌善舞著称的985学校,哲学专业。


大学选专业时家里人不懂,自己全凭爱好,第一志愿就是哲学。上学时有多快乐,毕业时就多难找工作。


侥幸自己选修过大数据课程,并且听说程序员工资还不错,通过校招加入了隔壁省的一家制造业当程序员。


公司管吃管住,工资1w出头,干了2年,感觉到了危机。自己羞于靠关系上位,但技术上,计算机知识太薄弱也发展有限,于是就想趁着还年轻,考个计算机硕士,提升一下自己。


在工作的第3年决定考研,边工作边考研,压力真是不小。让本来就是学渣的我,从研究生考场走出来就知道了最终的结果:没戏了。


成绩出来后,果然不出所料。


2、加入国企


知道初试成绩以后我难过了几天,认真思考了一下自己的未来发展:旁边的帝都我也回不去了,又不甘心留在这个小城市,于是决定回老家成都发展。


毕业后一直在私企,总是听说国企好、央企不错,所以我这次投简历也想试一下。正好看到一篇讲程序员国企的文章:值得程序员加入的173家国企汇总,网友:这下彻底躺平了,于是就按照文中的思路,找到了成都的一些所谓不错的国企投递试试。


成功加入后发现:真香!




  • 福利真 ** 好!和其它公司谈薪酬,别人都是能压多低压多低,来这家国企,竟然还说我工作年限够长,在我期望的基础上加了3k。六险二金更不用说,平时的各种生活保障也是非常到位。不夸张的说,从私企转到国企的我,有一种刘姥姥进大观园的感觉。




  • 真卷!**是谁告诉我国企适合养老的?**这比我以前在私企工作强度大多了好嘛?而且我第一次听说部门平均加班时长影响个人绩效这种规则。




  • 技术上不激进。可能对于程序员来说,不停的学习新技术是一种常态,但是在这家国企,基本都是传统老技术,我打开代码还看到了我们领导1997年写的头文件。当然,你也可以认为这是一种不好的事。这里的技术对我来说,够了。




  • 同事关系很融洽。我人生中第一次去按摩店找技师,是女同事带我去的,谁能信!不过必须说一句,成都的按摩店是真正规啊!技师可真有劲。




去年回来工作快1年了,我现在对于考研已经释怀了。听说我们部门今年招人开始硕士起步了,**有时候我还挺庆幸自己去年没考研成功的,**不然即使上了研究生,我这实力,也不一定能进入现在的单位了。


3、写在最后


现在回过头来看,有一个不同的体会:考研是一件好事,但如果本身不是沉迷于科研事业,而是更想赚钱的话,有好的工作机会也别错过。


另外,多关注有用的信息很重要,有时候别人的一番话,可能就需要自己经历几年的曲折才能总结出来。


大家有任何程序员求职问题,欢迎在评论区和我交流~


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

FlutterComponent最佳实践之动画的显和隐

Flutter中包含大量的动画组件和自定义动画方式,所以,在合适的场景下选择合适的动画实现方式就成了决定代码质量好坏的一个重要因素。 动画选择决策树 Flutter中的动画从广义上来讲可以分为两类,一类是基于绘制的动画(Drawing-based animat...
继续阅读 »

Flutter中包含大量的动画组件和自定义动画方式,所以,在合适的场景下选择合适的动画实现方式就成了决定代码质量好坏的一个重要因素。


动画选择决策树


Flutter中的动画从广义上来讲可以分为两类,一类是基于绘制的动画(Drawing-based animations),另一类是基于代码的动画(Code-based animations)。


下面这个决策树,是Flutter动画选择的总纲,这里梳理了不同的动画的作用场景和功能,我们来看下它具体的实现。
image.png
首先,我们需要区分是使用CustomPainter,或者是使用Lottie、Flare这种第三方库,这一类的动画很容易区分——如果你第一感觉,这个动画我做不了,那它大概率就是了。


接下来,就是区分是使用「显示动画」还是「隐式动画」。


简单的说,它们的区别如下:



  • 隐式动画:不用循环播放、不用随时中断、不用多个动画协同,它实现的是一种状态到另一种状态的改变

  • 显示动画:需要自己控制动画过程


最后,就是看现有组件是否满足需求,如果不行,那么就需要自定义相应的动画。


这就是整个动画决策树的执行过程。它们的开发难度,如下所示。
image.png
下面我们就具体来分析下不同的动画实现。本文首先介绍显示动画和隐式动画。


Implicit Animations——隐式动画


在Flutter中,很多常用组件都有其自带的隐式动画版本,例如下图所示的这些组件。
image.png
这些组件在Flutter中被称之为隐式动画Widget,下面以AnimatedContainer为例,来看下Implicit Animations的使用。



隐式动画有一个特点,那就是它们都是以「Animated」开头。



基本使用


AnimatedContainer的使用非常简单,甚至和普通的Container没有太大的区别,代码如下所示。


AnimatedContainer(
margin: EdgeInsets.only(top: 20),
width: size,
height: size,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(radius),
),
curve: Curves.easeIn,
duration: Duration(milliseconds: 300),
),

当通过setState函数改变AnimatedContainer中的属性时,AnimatedContainer会经过一段动画效果,然后再完成相应的改变。在隐式动画中,你依然可以定义Curve和Duration等参数,但是你无法控制动画,即动画的执行和结束,是由属性改变来驱动的。


使用场景


Implicit Animations可以非常方便的使Widget具有动画效果而不需要写很多额外的动画代码,结合FutureBuilder或者StreamBuilder,甚至不用写setState,下面这个例子就演示了如何将Implicit Animations和FutureBuilder结合起来使用,代码如下所示。


FutureBuilder(
future: future,
builder: (context, snapshot) {
var width = .0;
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
width = .0;
break;
case ConnectionState.done:
width = 100.0;
break;
}
return AnimatedContainer(
width: width,
duration: Duration(seconds: 1),
curve: Curves.easeIn,
child: Image.asset('images/logo.png'),
);
},
),

通过FutureBuilder的各种状态回调,就可以设置不同的Widget,并在FutureBuilder完成并显示正常的Widget时,产生一个动画效果,而不是非常生硬的出现。


TweenAnimationBuilder


TweenAnimationBuilder是自定义隐式动画的方式,借助它,你可以给一个指定的Widget作用一个动画效果,一个简单的示例代码如下所示。


TweenAnimationBuilder(
tween: Tween<double>(begin: 0, end: 48),
onEnd: (){}
duration: Duration(seconds: 1),
builder: (BuildContext context, double size, Widget child) {
return IconButton(
iconSize: size,
color: Colors.blue,
icon: child,
);
},
child: Icon(Icons.aspect_ratio),
)

借助TweenAnimationBuilder,就可以将一个指定的Tween作用于builder中的Widget,builder中的第二个参数,就是Tween所指定的参数的类型,通过TweenAnimationBuilder,就可以在Widget参数变化的时候产生动画效果。



TweenAnimationBuilder的builder中如果有不变的Child Widget,可以放在TweenAnimationBuilder的child属性中,因为builder在产生动画时会重建,所有不变的Widget,都可以放在TweenAnimationBuilder的child中,再通过builder的第三个参数来传递这个Widget,以避免重建。



通常我们在开发中,会借助Transform来完成动画效果,在builder中,根据Tween返回的数值,使用不同的Transform来修改动画状态。



TweenAnimationBuilder中的begin,只在第一次使用,后面更新时,只看end的值,例如10-30,修改end为50,实际变化是30-50。如果不传begin,那么默认和end相等。



Explicit Animations——显示动画


与隐式动画不同,显示动画给了开发者对动画过程的完全掌控,开发者可以根据自己的需要来控制动画,Flutter中内置了很多显示动画,如下所示。
image.png



显示动画也有一个很明显的特点,那就是它们都以「Transition」结尾。



基本使用


以RotationTransition为例,下面来演示下如何使用Flutter中的显示动画。


显示动画是通过AnimationController来进行驱动的,所以,使用显示动画的第一步,就是需要创建AnimationController。有了AnimationController之后,就可以通过控制AnimationController的状态来控制动画的驱动过程,整个代码如下所示。


AnimationController controller;

@override
void initState() {
super.initState();
controller = AnimationController(vsync: this, duration: Duration(seconds: 2))..repeat();
}

@override
void dispose() {
controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () {
if (controller.isAnimating) {
controller.stop();
} else {
controller.repeat();
}
},
child: RotationTransition(
turns: controller,
child: FlutterLogo(
size: 100,
),
),
),
);
}

与隐式动画相比,显式动画通过AnimationController来获取动画的行进状态和参数,从而让调用者能够控制动画的行进过程。



显式动画可以实现隐式动画的所有功能,但是比隐式动画多了管理动画生命周期的工作



当Flutter内置显示动画不能满足开发者的需求时,Flutter提供了AnimatedBuilder和AnimatedWidget来让开发者对显示动画进行自定义。


AnimatedWidget


前面提到的都是Flutter中使用动画的最基本方式,但实际上,Flutter提供了很多关于动画的封装组件,可以让开发者更加方便的使用动画,这就是AnimatedWidget。AnimatedWidget也有很多实现类,如图所示。
image.png
AnimatedWidget是实现自定义显示动画的另一种方式,它可以将一些动画的逻辑以Widget的形式封装起来,从而让build函数中的代码逻辑更加清晰,下面是AnimatedWidget的示例代码。


@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
AnimWidget(animation: controller),
Center(child: FlutterLogo(size: 100)),
],
);
}

class AnimWidget extends AnimatedWidget {
const AnimWidget({
Key? key,
required Animation<double> animation,
}) : super(key: key, listenable: animation);

@override
Widget build(BuildContext context) {
Animation<double> animation = listenable as Animation<double>;
return Container(
decoration: BoxDecoration(
gradient: RadialGradient(
colors: const [Colors.red, Colors.transparent],
stops: [0, animation.value],
),
),
);
}
}

那么这种方式和之前直接使用AnimationController和Tween有什么区别呢?细心的读者可能已经发现了,AnimatedWidget不需要自己去监听动画的回调,也不需要通过setState来刷新动画,这些操作,AnimatedWidget已经封装好了,这就是AnimatedWidget的作用。


AnimatedBuilder


AnimatedBuilder是一个特殊的AnimatedWidget,它可以直接指定一个动画作用于Widget上,而不需要重新创建一个自定义的AnimatedWidget,它可以帮助开发者处理动画的监听,当一个Widget Tree中有一些需要动画的Widget,也有一些不需要动画的Widget时,用AnimatedBuilder可以很方便的避免非动画Widget的重绘,所以说,AnimatedBuilder可以更加方便的给一个Widget增加动画效果。


AnimatedBuilder与其它的显示动画一样,也是通过AnimationController驱动的,借助AnimatedBuilder,开发者可以根据需要,自己创建Animation并控制它,下面的代码演示了如何通过控制RadialGradient的stop属性来控制RadialGradient的显示大小,从而形成动画效果,代码如下所示。


@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (context, widget) {
return Stack(
children: <Widget>[
Container(
decoration: BoxDecoration(
gradient: RadialGradient(
colors: [Colors.red, Colors.transparent],
stops: [0, controller.value],
),
),
),
Center(child: FlutterLogo(size: 100))
],
);
},
);
}

上面的代码演示了如何使用AnimatedBuilder,实际上非常简单,与使用内置的显示动画的过程基本一致。


在使用AnimatedBuilder的过程中,需要尽可能多的将需要动画的部分和不需要动画的部分区分开来,这样可以避免多余的重绘,从而提高动画性能,例如上面的代码,可以将FlutterLogo和Stack放置在最外层,这样只需要让RadialGradient产生动画就可以了,代码如下所示。


@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
AnimatedBuilder(
animation: controller,
builder: (context, widget) {
return Container(
decoration: BoxDecoration(
gradient: RadialGradient(
colors: [Colors.red, Colors.transparent],
stops: [0, controller.value],
),
),
);
},
),
Center(child: FlutterLogo(size: 100))
],
);
}

AnimatedBuilder接收了一个animation,在child中,可以直接使用这个animation的值,其它都和普通的AnimatedWidget类似。


实际上,AnimatedBuilder就是AnimatedWidget的子类,所以在本质上,这两种实现自定义显示动画的方式想相同的,开发者可以根据自己的喜好来选择相应的方式来创建自己的显示动画。



AnimateWidget负责组件的抽离,可以看出组件中杂糅了动画逻辑。而AnimatedBuilder恰好相反,它不在意组件是什么,只是将动画抽离达到复用简单。



Flutter中的显示动画和隐式动画,几乎可以解决大部分我们平时在开发中遇到的动画场景,借助动画选择决策树,我们可以对动画的选择了如指掌,剩下的工作,就是对动画进行拆解,分而治之。


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

Android:实现一个自定义View扫描框

扫码功能都用过吧,打开扫码功能后都会有类似封面图的效果。其实就是一个自定义View的遮罩,话不多说,今天这篇我们就来讲解如何实现一个扫面框动效。 首先,我们先分析下动效的组成,有利于待会拆分实现: 四周类似角标的白线 角标框住的浅白色背景 一条由上而下由快到...
继续阅读 »

扫码功能都用过吧,打开扫码功能后都会有类似封面图的效果。其实就是一个自定义View的遮罩,话不多说,今天这篇我们就来讲解如何实现一个扫面框动效。


首先,我们先分析下动效的组成,有利于待会拆分实现:



  1. 四周类似角标的白线

  2. 角标框住的浅白色背景

  3. 一条由上而下由快到慢移动的扫描线


一经分析,其实非常简单,整体效果就是由这几个简单的元素组成。接下来我们就创建一个ScanView继承自View来实现这个动效。(由于代码古老,这里使用Java)


public final class ScanView extends View {

private Paint paint, scanLinePaint,reactPaint;//三种画笔
private Rect frame;//整个区域

public ScanView(Context context) {
this(context, null);
}

public ScanView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public ScanView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint()
}

private void initPaint() {
/*遮罩画笔*/
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(color);
paint.setAlpha(CURRENT_POINT_OPACITY);
paint.setStyle(Paint.Style.FILL);

/*边框线画笔*/
reactPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
reactPaint.setColor(reactColor);
reactPaint.setStyle(Paint.Style.FILL);

/*扫描线画笔*/
scanLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
scanLinePaint.setStyle(Paint.Style.FILL);
scanLinePaint.setDither(true);
scanLinePaint.setColor(scanLineColor);
}
}

三种画笔初始化完成接下来就是使用画笔在画布上绘制效果图了,重写onDraw()方法


public void onDraw(Canvas canvas) {
//绘制取景边框
drawFrameBounds(canvas, frame);
//绘制遮罩
drawMaskView(canvas, frame);
//绘制扫描线
drawScanLight(canvas, frame);
}

再来分析,边框的四个角其实拆开来看,就是两条线组成,或者说是两个填充的矩形框垂直相交组成,那么四个角就可以按照这个思路完成,遮罩其实就是一个矩形框。


//绘制四个角,注意是外线而不是内线
private void drawFrameBounds(Canvas canvas, Rect frame) {
// 左上角
canvas.drawRect(frame.left - corWidth, frame.top, frame.left, frame.top + corLength, reactPaint);
canvas.drawRect(frame.left - corWidth, frame.top - corWidth, frame.left + corLength, frame.top, reactPaint);
// 右上角
canvas.drawRect(frame.right, frame.top, frame.right + corWidth,frame.top + corLength, reactPaint);
canvas.drawRect(frame.right - corLength, frame.top - corWidth, frame.right + corWidth, frame.top, reactPaint);
// 左下角
canvas.drawRect(frame.left - corWidth, frame.bottom - corLength,frame.left, frame.bottom, reactPaint);
canvas.drawRect(frame.left - corWidth, frame.bottom, frame.left + corLength, frame.bottom + corWidth, reactPaint);
// 右下角
canvas.drawRect(frame.right, frame.bottom - corLength, frame.right + corWidth, frame.bottom, reactPaint);
canvas.drawRect(frame.right - corLength, frame.bottom, frame.right + corWidth, frame.bottom + corWidth, reactPaint);
}

//绘制遮罩
private void drawMaskView(Canvas canvas, Rect frame) {
canvas.drawRect(frame.left, frame.top, frame.right, frame.bottom, paint);
}

到此,我们还剩最后一个扫描线的动画效果,这条线其实就是一张图片,首先需要将图片以Bitmap形式绘制在扫描区域内,然后通过ValueAnimator来控制图片Y坐标点,这样就能达到图片上下移动的效果,至于由快到慢的效果是添加了插值器,这里使用内置的DecelerateInterpolator,同学们可以根据自己想要的效果自己搭配。


scan_light.png


if (valueAnimator == null) {
valueAnimator = ValueAnimator.ofInt(frame.top, frame.bottom-10);//图片Y坐标取值范围
valueAnimator.setDuration(3000);//单次动画时间3秒
valueAnimator.setInterpolator(new DecelerateInterpolator());//由快到慢插值器
valueAnimator.setRepeatMode(ValueAnimator.RESTART);//重复动画
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);//无限次数
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
scanLineTop = (int) animation.getAnimatedValue();//当前时刻获取的Y值
invalidate();//刷新视图
}
});
valueAnimator.start();
}

到此就可以实现封面的效果,甚至可以添加别的酷炫效果,只要你敢想敢做。


总结


其实一些动效看似很复杂,但通过认真分析,我们可以将其拆分成多个简单的小块,将每个小块实现后再逐个拼装,最后达到完整的效果。本节主要是通过自定义View实现,用到绘制矩形框(drawRect),属性动画(ValueAnimator),两者使用也是非常简单。另外需要注意动画的使用和释放,避免导致不必要的内存泄漏。


好了,以上便是本篇所有内容,希望对大家有所帮助!


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

Kotlin Collection KTX:让你的集合操作如丝般顺滑

ktx
当今移动应用开发,常常需要使用各种集合类型来存储和操作数据。Kotlin 提供了 Collection KTX 扩展库,为我们操作集合提供了非常方便的 API。在本篇文章中,我们将介绍 Collection KTX 中包含的所有扩展函数,让你的集合操作变得更加...
继续阅读 »

当今移动应用开发,常常需要使用各种集合类型来存储和操作数据。Kotlin 提供了 Collection KTX 扩展库,为我们操作集合提供了非常方便的 API。在本篇文章中,我们将介绍 Collection KTX 中包含的所有扩展函数,让你的集合操作变得更加高效、简单、易读。


除了 Collection KTX,Kotlin 还提供了许多其他扩展库,例如 Android KTX、Coroutines、Serialization KTX 等,它们都可以大大简化我们的开发流程。在接下来的文章中,我们还将为您介绍这些扩展库的详细信息,让你的 Kotlin 开发之路更加畅通无阻


使用


dependencies {
    implementation "androidx.collection:collection-ktx:1.2.0"
}

用法合集


Collection 扩展函数



  • filterNot():过滤掉指定元素后的新 Collection。

  • filterNotNull():过滤掉 null 元素后的新 Collection。


List 扩展函数



  • sorted():按自然顺序排序后的新 List。

  • sortedBy():按指定方式排序后的新 List。

  • sortedDescending():按自然顺序降序排序后的新 List。

  • sortedByDescending():按指定方式降序排序后的新 List。

  • distinct():去重后的新 List。

  • distinctBy():按指定方式去重后的新 List。

  • minus():删除指定元素后的新 List。

  • plus():添加指定元素后的新 List。

  • drop():去掉前几个元素后的新 List。

  • dropWhile():去掉符合指定条件的元素后的新 List。

  • take():前几个元素组成的新 List。

  • takeWhile():符合指定条件的元素组成的新 List。

  • partition():按指定条件分隔后的 Pair。

  • groupBy():按指定方式分组后的 Map。

  • associate():按指定方式关联后的新 Map。

  • associateBy():按指定方式关联键后的新 Map。

  • associateWith():按指定方式关联值后的新 Map。

  • zip():按指定方式组合后的新 List。


MutableList 扩展函数



  • removeLast():移除最后一个元素,并返回该元素。

  • removeFirst():移除第一个元素,并返回该元素。

  • move():将指定元素移动到新位置。


Set 扩展函数



  • minus():删除指定元素后的新 Set。

  • plus():添加指定元素后的新 Set。

  • partition():按指定条件分隔后的 Pair。

  • groupBy():按指定方式分组后的 Map。

  • associate():按指定方式关联后的新 Map。

  • associateBy():按指定方式关联键后的新 Map。

  • associateWith():按指定方式关联值后的新 Map。


MutableSet 扩展函数



  • remove():移除指定元素,并返回是否移除成功。

  • retainAll():仅保留符合指定条件的元素。

  • addAll():添加指定元素后的新 MutableSet。


Map 扩展函数



  • minus():删除指定键对应的元素后的新 Map。

  • plus():添加指定键值对后的新 Map。

  • partition():按指定条件分隔后的 Pair。

  • filterKeys():按指定条件过滤键后的新 Map。

  • filterValues():按指定条件过滤值后的新 Map。

  • mapKeys():按指定方式映射键后的新 Map。

  • mapValues(): 按指定方式映射值后的新map


MutableMap 扩展函数



  • remove():移除指定键对应的元素,并返回该元素。

  • putAll():添加指定键值对后的新 MutableMap。

  • putIfAbsent():仅在指定键不存在时添加指定键值对。

  • compute():更新指定键对应的元素,并返回更新后的值。

  • computeIfAbsent():仅在指定键不存在时更新该键对应的元素。

  • computeIfPresent():仅在指定键存在时更新该键对应的元素。


Iterable 扩展函数



  • reduceOrNull():对所有元素进行指定操作,如果为 null 则返回 null。

  • reduceIndexedOrNull():对所有元素进行指定操作,同时考虑元素的索引,如果为 null 则返回 null。

  • foldOrNull():对所有元素进行指定操作并给定初始值,如果为 null 则返回 null。

  • foldIndexedOrNull():对所有元素进行指定操作并给定初始值,同时考虑元素的索引,如果为 null 则返回 null。


ListIterator 扩展函数



  • previousOrNull():返回上一个元素,如果不存在则返回 null。


Sequence 扩展函数



  • reduceOrNull():对所有元素进行指定操作,如果为 null 则返回 null。

  • reduceIndexedOrNull():对所有元素进行指定操作,同时考虑元素的索引,如果为 null 则返回 null。

  • foldOrNull():对所有元素进行指定操作并给定初始值,如果为 null 则返回 null。

  • foldIndexedOrNull():对所有元素进行指定操作并给定初始值,同时考虑元素的索引,如果为 null 则返回 null。

  • distinct():去重后的新 Sequence。

  • distinctBy():按指定方式去重后的新 Sequence。

  • filterNotNull():过滤掉 null 元素后的新 Sequence。

  • filterNot():过滤掉指定元素后的新 Sequence。

  • partition():按指定条件分隔后的 Pair。

  • sorted():按自然顺序排序后的新 Sequence。

  • sortedBy():按指定方式排序后的新 Sequence。

  • sortedDescending():按自然顺序降序排序后的新 Sequence。

  • sortedByDescending():按指定方式降序排序后的新 Sequence。

  • take():前几个元素组成的新 Sequence。

  • takeWhile():符合指定条件的元素组成的新 Sequence。

  • zip():按指定方式组合后的新 Sequence


了解工具的尿性


工具的职责就是提高工作效率



  1. 使用 Collection KTX 可以大大简化集合操作的代码,使代码更加简洁易读,同时可以提高代码的可维护性

  2. 在使用集合时,应该尽可能使用 Kotlin 标准库中的函数和 Collection KTX 中的扩展函数,而不是手写循环或通过 Java API 进行操作,这可以减少代码量和提高代码可读性。

  3. 了解不同的集合类型及其特性,选择合适的集合类型可以使代码更加高效。例如,如果需要频繁添加或删除元素,则应该使用可变集合类型。

  4. 避免频繁进行集合类型的转换,因为这会导致性能降低。如果需要对集合进行不同的操作,可以考虑使用不同的集合类型来解决。

  5. 尽量避免对空集合进行操作,因为这可能会导致空指针异常。在使用 Collection KTX 时,可以使用非空断言或者空安全操作符来处理可能为空的集合。


当然使用时也要注意,kotlin 的扩展函数让代码的可读性要求增高了少,所以用的使用为了能保证团队的统一,因该注意:



  1. 对于代码中的扩展函数,应该在函数名称中体现其作用,以便其他开发者更容易理解代码。例如,“find”函数可以改名为“findFirstOrNull”或“findLastOrNull”。

  2. 在使用 Collection KTX 时,应该注意性能问题。某些操作可能会导致性能下降,例如对大型集合进行循环和操作,因此应该考虑使用 Sequence 和 Flow 来提高性能。

  3. 避免重复操作。使用 Collection KTX 可以使代码更加简洁和易读,但是不应该过度使用,如果某个操作已经通过一个函数实现了,就不要再手动写同样的操作。

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

Flutter 知识集锦 | 基于 Flow 实现滑动显隐层

1. 前言最近要实现一个小需求,涵盖了很多知识点,比如手势、动画、布局等。挺有意思的,写出来和大家分享一下。如下所示,分为上下两层;当左右滑时,上层会随偏移量而平移,从而让上层产生滑动手势显隐的效果:标题这里上层通过不透明度 0.2 的蓝色示意,实际使用时可以...
继续阅读 »
1. 前言

最近要实现一个小需求,涵盖了很多知识点,比如手势、动画、布局等。挺有意思的,写出来和大家分享一下。如下所示,分为上下两层;当左右滑时,上层会随偏移量而平移,从而让上层产生滑动手势显隐的效果:

标题
88.gif89.gif

这里上层通过不透明度 0.2 的蓝色示意,实际使用时可以改为透明色。很多直播间的浮层就是这种交互逻辑,通过右滑来隐藏浮层。

直播右滑中
35f86de59086435083bf387a5efcc24.jpgc6c435e20ded4325899c6134f7be1d9.jpg

2. 实现思路

思路其实非常简单,监听横向滑动的手势事件,根据偏移量让上层组件进行偏移。当放手时,根据偏移量是否达到宽度的一半,使用动画进行移出或者关闭。

image.png

偏移的实现方式有很多,但需要自由地进行布局和矩阵变换、透明度,并且需要支持动画的变化,Flow 组件是一个非常不错的选择。 Flow 组件可以通过代理类对子组件进行自定义布局,灵活性极强;如果是 CustomPaint 是 绘制之王 可以绘制万物,那么 Flow 就是 布局之王,可以摆放万物。三年前写过一篇介绍 Flow 使用的文章: 《【Flutter高级玩法- Flow 】我的位置我做主》 。 本文就不对 Flow 的基础使用进行介绍了。


另外,在滑动过程中需要注意限制偏移量,使偏移量在 0~size.width 之内;当放手时,通过动画控制器来驱动动画,使用补间让偏移量运动到 0 (打开) 或 size.width(关闭) 。当关闭时,在右下角展示一个按钮用于点击展开:

image.png


3. 布局的代码实现

Flow 组件布局最重要的是实现 FlowDelegate,在其中的 paintChildren 方法中实现布局的逻辑。和 CustomPainter 类似,FlowDelegate 的实现类也可以通过 super 构造为 repaint 入参设置可监听对象。可监听对象的变化会触发 paintChildren 重新绘制:

SwipeFlowDelegate 实现类再构造时传入可监听对象 offsetX,在绘制索引为 1 的孩子时,通过 Matrix4 进行偏移。这样只要在手势水平滑动中,更新 offsetX 值即可。另外,可以根据 offsetX.value 是否达到 size.width 知道是否是关闭状态,如果已经关闭,绘制按钮。

class SwipeFlowDelegate extends FlowDelegate {
final ValueListenable<double> offsetX;

SwipeFlowDelegate(this.offsetX) : super(repaint: offsetX);

@override
void paintChildren(FlowPaintingContext context) {
Size size = context.size;
context.paintChild(0);
Matrix4 offsetM4 = Matrix4.translationValues(offsetX.value, 0, 0);
context.paintChild(1, transform: offsetM4);

// 偏移量对于父级尺寸
if (offsetX.value == size.width) {
Matrix4 m1 = Matrix4.translationValues(size.width / 2 - 30, size.height / 2 - 30, 0);
context.paintChild(2, transform: m1);
Matrix4 m2 = Matrix4.translationValues(size.width / 2 - 30, -(size.height / 2 - 50), 0);
context.paintChild(3, transform: m2);
}
}

@override
bool shouldRepaint(covariant SwipeFlowDelegate oldDelegate) {
return oldDelegate.offsetX.value != offsetX.value;
}
}

从这里可以看出,FlowDelegate 的最大优势是可以自定义孩子的绘制与否,还可以在绘制时通过 Matrix4 对孩子进行矩阵变换,还有可选参数可以控制透明度。接下来使用 Flow 组件时,提供 SwipeFlowDelegate ,并在 children 列表中依次放入子组件。其中前两个组件由外界传入,分别是底组件和上层组件,这样组件的布局就完成了,接下来监听事件,更新 factor 即可:

final ValueNotifier<double> factor = ValueNotifier(0);

Flow(
delegate: SwipeFlowDelegate(factor),
children: [
widget.content,
widget.overflow,
GestureDetector(
onTap: open,
child: const Icon(Icons.menu_open_outlined, color: Colors.white)),
GestureDetector(
onTap: () {
Navigator.of(context).pop();
},
child: const Icon(Icons.close, color: Colors.white))
],
)

4. 手势的监听

这里手势的处理是非常简单的,通过 GestureDetector 监听水平拖拽事件。在 onHorizontalDragUpdate 中根据拖拽的偏移量更新 factor 的值,其中通过 .clamp(0, widget.width) 可以限制偏移量的取值区间。

@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onHorizontalDragUpdate: _onHorizontalDragUpdate,
onHorizontalDragEnd: _onHorizontalDragEnd,
child: SizedBox(
height: MediaQuery.of(context).size.height,
width: widget.width,
child: Flow( delegate:// 同上,略...
);
}

void _onHorizontalDragUpdate(DragUpdateDetails details) {
double cur = factor.value + details.delta.dx;
factor.value = cur.clamp(0, widget.width);
}

void _onHorizontalDragEnd(DragEndDetails details) {
if (factor.value > widget.width / 2) {
close();
} else {
open();
}
}

最后在 _onHorizontalDragEnd 回调中,根据当前偏移量是否大于一般宽度,决定关闭还是打开。期间过程使用动画进行偏移量的过渡变化。


5. 动画的使用

动画的使用,主要是通过 AnimationController 动画控制器来驱动数值的变化;在放手时 Tween 创建补间动画器,监听动画器数值的变化更新偏移量。这样偏移量就可以在指定时间内,在两个值之间渐变,从而产生动画效果。比如抬手时,open 方法是让偏移量从当前位置变化到 0 :

class _ScrollHideWrapperState extends State<ScrollHideWrapper> with SingleTickerProviderStateMixin {
late AnimationController _ctrl;

final ValueNotifier<double> factor = ValueNotifier(0);

@override
void initState() {
super.initState();
_ctrl = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
}

@override
Widget build(BuildContext context) {
// 略同...
}

// 动画关闭
Future<void> close() async {
Animation<double> anim = Tween<double>(begin: factor.value, end: widget.width).animate(_ctrl);
anim.addListener(() => factor.value = anim.value);
await _ctrl.forward(from: 0);
}

// 动画打开
Future<void> open() async {
Animation<double> anim = Tween<double>(begin: factor.value, end: 0).animate(_ctrl);
anim.addListener(() => factor.value = anim.value);
await _ctrl.forward(from: 0);
}
}

如果想让动画的变化非匀速,可以使用 Curve 来控制动画曲线。这样,基于 Flow 实现的自定义布局,就可以根据手势和动画,完成特定的交互功能。从这里可以看出 Flow 自定义布局的灵活性非常强,很多疑难杂症,都可以使用它来完成。

比如企业微信中:侧滑展示左栏,而且上层不会全部消失,通过 Flow 来自定义布局就很容易实现。大家可以基于本文,自己实现一下作为练习。那本文就到这里,谢谢观看 ~

标题关闭
90.gif91.gif


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

从零打造现代化绘图框架 Plait

web
我司大概从今年(2022年)年初决定思维导图,经过半年多的研究与实践,我们基于自研的绘图框架初步完成了一个脑图组件并成功集成到我们 PingCode Wiki 中,这篇文章主要探讨下这个绘图框架(Plait)的一些设计点和思维导图落地的一些关键技术。 概论 对...
继续阅读 »

我司大概从今年(2022年)年初决定思维导图,经过半年多的研究与实践,我们基于自研的绘图框架初步完成了一个脑图组件并成功集成到我们 PingCode Wiki 中,这篇文章主要探讨下这个绘图框架(Plait)的一些设计点和思维导图落地的一些关键技术。


概论


对于思维导图、流程图前期我们做了很多调研工作,流程图方向我们研究了 excalidraw 和 react-flow,它们都是基于 react 框架实现的库,在社区中有很高的知名度,思维导图方向我们研究了 mind-elixir、 mindmap-layouts (自动布局算法),在开源领域中思维导图发展不是很好,没有成熟、知名的作品。


mind-exlixir 介绍:



mind-elixir 功能示意图


优点:



  1. 麻雀虽小但五脏俱全

  2. 纯 JS 库、轻量


缺点:



  1. 不依赖前端框架、开发方式和主流的方式不同

  2. 架构设计没有太多可取之处,节点布局方式不易扩展


虽然我们前期的目标是研发 「思维导图」 ,但是最终我们的产品目标应该是做一个 「一体化的交互式绘图画板」 ,包含思维导图、流程图、自由画笔等。


最终调研发现目前开源社区恰恰缺少这样一个一体化的绘图框架,用于实现一体化的交互式绘图编辑器,集思维导图、流程图、自由画笔于一体,所以我们结合做富文本编辑器的经验,重新架构了一套绘图框架(Plait)、拥有插件机制,并在它的基础上实现思维导图插件、落地到 PingCode Wiki 产品中,所以今天分享的主角就是 Plait 框架。


下面正式进入今天的主题,分为四部分:



  1. 绘图框架设计

  2. 思维导图整体方案

  3. 思维导图自动布局算法

  4. 框架历程/未来


一、绘图框架设计


这部分首先会先简单介绍下绘图方案的选型(Canvas vs SVG)考量,然后介绍下 Plait 绘图框架中核心部分的设计:插件机制、数据管理,最后介绍下框架优势。


绘图方案:Canvas vs SVG


Canvas 还是 SVG 其实社区中也没有一个明确的答案,我们参考了一些知名产品的方案选型,SVG 和 Canvas 都有并且实现的效果都不差,比如语雀的白板使用的是 SVG,ProcessOn 使用的是 Canvas,Excalidraw 使用的是 Canvas,drawio 使用的是 SVG 等等。因为我们没有 Canvas 的使用经验,加上我们的思维导图节点希望支持富文本内容,所以暂时选定对 DOM 更友好的 SVG,觉得先按照这个方案试试水。


经过这么长时间的验证,发现基于 SVG 的方案并没有什么明显的不足,性能问题我们也经过验证,支持 1000+ 的思维导图节点渲染完全没有问题、操作依然很流畅。



对于 SVG 绘制我们没有直接使用 SVG 的底层API ,而是使用了一个第三方的绘图库 roughjs。



下面我们看看 Plait 框架「插件机制」的部分,这部分的灵感来源于富文本编辑器框架 Slate。


插件机制


Web 前端的画图领域有很多可以深度研发的业务方向,如何基于同一个框架实现不同业务方向功能开发,就需要用到插件机制了。


插件机制是 Plait 框架一个重要特性,框架底层并不提供具体的业务功能实现,业务功能都需要基于插件机制实现,如下图所示:



插件机制通俗讲就是框架层构建的一座基础桥梁,为实现具体的业务功能提供必要的支持,Plait 插件机制有三个核心要素:



  1. 抽象数据范式(插件数据)

  2. 可复写行为(识别交互)

  3. 可复写渲染(控制渲染)


具体到流程图、思维导图这类绘图场景,它的核心是基于用户交互行为(鼠标、键盘操作)实现符合交互预期的元素绘制、渲染,如果做成可扩展的那就插件开发者可以自定义交互行为、自定义节点元素渲染,基于自定义交互生成插件数据,基于插件数据控制插件元素渲染,构成插件闭环,如下图所示(插件机制闭环示意图):




这部分的核心就是设计可重写方法,目前 Plait 中主要有两类:

第一类用于实现自定义交互:mousedown、mouseup、mousemove、keydow、keyup。

第二类用于实现自定义渲染:drawElement、redrawElement、destroyElement 然后就是框架层与插件衔接部分的设计了,这一部分在 plait/core 中目前被设计的是比较松散的,drawElement 可以返回一个 SVGGElement 类型的 DOM 元素也可以返回一个框架组件,既可以直接衔接框架也可以基于 DOM 的方式对接。

目前 Plait 框架整个是基于 Angular 框架实现的,后续可能会考虑脱离框架的设计模式,这不是本文的重点。



举个例子: 画圆插件三步走



步骤一:定义数据结构


export interface CircleElement {
type: 'cirle';
radius: number;
dot: [x: number, y: number];
}

步骤二:处理画圆交互


board.mousedown = (event: MouseEvent) => {
if (board.cursor === 'circle') {
start = toPoint(event.x, event.y, board.host);
return;
}
mousedown(event);
};
board.mousemove = (event: MouseEvent) => {
if (start) {
end = toPoint(event.x, event.y, board.host);
if (board.cursor === 'circle') {
// fake draw circle
}
return;
}
mousemove(event);
};
board.globalMouseup = (event: MouseEvent) => {
globalMouseup(event);
if (start) {
end = toPoint(event.x, event.y, board.host);
const radius = Math.hypot(end[0] - start[0], end[1] - start[1]);
const circleElement = { type: 'circle', dot: start, radius };
Transforms.insertNode(board, circleElement, [0]);
}
};

步骤三:实现画圆方法


board.drawElement = (context) => {
if (context.elementInstance.element.type === 'circle') {
const roughSVG = HOST_TO_ROUGH_SVG.get(board.host) as RoughSVG;
const circle = context.elementInstance.element as unknown as CircleElement;
roughSVG.circle(circle.dot[0], circle.dot[1], circle.radius);
}
return drawElement(context);
}

这是一个最简单的插件示例,通过框架提供的桥梁实现一个画圆插件:拖放画一个圆 -> 生成对应圆数据 -> 根据数据渲圆。


插件机制大概就是这些内容,下面看看数据管理部分。


数据管理


数据管理是 Plait 框架中非常重要的部分,它是框架的灵魂,前面的插件机制是外在表现,主要包含以下特性:



  1. 提供基础数据模型

  2. 提供基于原子的数据变化方法(Transfroms)

  3. 基于不可变数据模型(基于 Immer)

  4. 提供 Change 机制,与框架配合完成数据驱动渲染

  5. 与插件机制融合,数据修改的过程可以被拦截处理


这些都是非常优秀的特性,既可以完成数据的约束,又可以灵活实现很多复杂高级的需求,感觉这块的设计和实现其实可以算是一种特定场景的状态管理。


框架状态流转图:



上面说到的插件机制的闭环要依赖数据模型状态作为的标准,最终的插件闭环如下图所示:



这里可以列举两个具体的业务场景,都是我们开发中经常落入的陷阱,体现数据管理在的约束作用及灵活性(这部分可能不好理解,谨慎阅读,其实也是框架作用的具体说明):


场景 1: 自动选中根节点


下面这张图是一个需求示意:新建脑图时自动选中根节点并弹出工具栏



新建思维导图自动弹出工具栏示需求意图


这是一个合理的需求,但它不是常规的交互路径(常规路径是用户点击节点,触发节点选中,进而触发工具栏弹出),我们的新同学在最开始的时候就选择了一种不标准的数据修改方式(手动修改数据)去完成这个需求。


常规点选:




框架数据会存储一个选区状态(点击位置或者框选位置),点击操作会触发选区数据变化,选区变化会触发 Change 事件,基于这个机制处理节点选中和工具栏弹出。



自动选中(手动修改数据):



不推荐的操作路径示意图



这里的思路是首先模拟位置(根据自动选择的节点计算),手动修改数据,然后自己手动调用工具栏的弹出、强制刷新界面让节点选中,这就是典型的没有按照框架约束实现需求的例子,前面说到的数据流没有正确运转,需要做很多特殊处理。 不通过 Transfrom 的方式手动修改数据是不被框架允许的,不会触发框架 Change 行为,理论上应该直接抛出异常(很可惜当时没有做到这步)。



自动选中(标准路径):



标准路径就是基于模拟位置通过 Transfrom 的方式修改数据(相当于模拟点击了要自动选中的节点),后面的流程就可以依赖框架机制去控制执行,无须再做很多手动的处理。


场景 2: 思维导图节点删除后节点的选中状态自动切换到临近节点


这是一个很基础的需求,目前我们的实现是拦截节点删除行为(按 Delete/Backspace 键)处理,这样做有两个弊端:



  1. 假如将来要做右键菜单删除需要把这部分的处理代码再写一遍,即使封装成工具方法,也要额外增加一个调用入口。

  2. 一个不太好解决的问题,新建一个节点后按 Ctrl + Z 撤回,无法把选中状态转移到临近节点上,虽然这里撤回执行的也是节点删除操作。


推荐路径:


拦截节点删除操作,前面提到框架统一了数据修改的方法,所以插件开发者可以对数据修改过程进行拦截,这个拦截过程可以在数据修改前,也可以在在数据修改后(Change),在这个地方做出就不会有任何漏掉的场景。


框架作用



  1. 插件机制实现分层

  2. 数据管理实现约束

  3. 配合框架规范数据流


框架最大的意义就是分层解构,降低业务的复杂度,每个领域或者每个模块只处理自己的事情。



比如前端框架,组件化开发,就是能够把一定的逻辑归拢到一个逻辑域中(组件),而不是所有东西杂糅在一起,这是架构演进的趋势。



架构图:



二、思维导图整体方案


这里简单介绍下思维导图插件的整体技术方案,但是不会介绍特别细,因为它是基于 Plait 框架实现,大的方案肯定是与 Plait 框架一致。



思维导图整体技术方案导图


1、整体方案


我们整体是 SVG + Richtext 的方案。


绘图使用 SVG ,目前我们脑图节点、节点连线、展开收起图标等等都是基于 SVG 绘制的。


节点内容使用 foreignObject 包括嵌入到 SVG 中,这种方案使节点内容支持富文本


2、功能方案


脑图核心交互处理及渲染都是可重写方法完成,与 Plait 集成。


脑图插件仅仅负责脑图部分的渲染,至于整个画布的渲染以及画布的移动放大缩小等等是框架底层功能。


因为 Plait 是支持扩展任意元素的,支持渲染多个元素,它只由数据决定,所以它支持同时渲染多个脑图。


底层脑图插件并不包含工具栏实现,它只处理核心交互、渲染、布局。


3、组件化


脑图组件渲染、节点渲染的整体机制就是我们前端经常提到的:组件化、数据驱动,虽然组件内部节点渲染还是创建 DOM、销毁 DOM,但是大的功能还是通过组件来进行划分的。


基于脑图业务里面有两个非常重要的组件:MindmapComponent 、MindmapNodeComponent,MindmapComponent 处理脑图整体的逻辑,比如执行节点布局算法,MindmapNodeComponent 处理某一个节点的逻辑,比如节点绘制、连线绘制、节点主题绘制等。


之所以把这个部分提出来说一下,是因为我觉得这块的思想其实是主流前端框架思想的延续,包括和 Plait 框架整体的机制是统一的。


4、绘图编辑器


这里可以理解为业务层的封装,业务层级决定集成那些扩展插件,以及进一步扩展插件上层功(比如脑图节点工具栏实现),Mindmap 插件层不依赖于我们的组件库和业务组件,所以工具栏这类需要组件库组件的场景统一放到业务层实现,这样 Mindmap 插件层可以减少依赖、保持聚焦。


思维导图具体落地到 PingCode Wiki 业务中,其实有一个更虽复杂、但清晰的分层结构:



三、自动布局算法


节点自动布局是思维导图的一个核心技术,它是思维导图美观以及内容表现力的决定性因素,它关注节点如何分布,这部分说复杂不复杂,说简单也不简单,包含以下几个部分:



  1. 布局分类

  2. 节点抽象

  3. 算法过程

  4. 方向变换

  5. 布局嵌套


布局分类


介绍说明下常规思维导图的布局分类:



示意图


标准布局:



逻辑布局:



缩进布局:



时间线:



鱼骨图:



美学标准


前面说过思维导图对可视化树的展现有很高的要求,需要它是美观(这个就很直观,每个人的审美可能不一样,但是它也应当有一些基础标准)的,所以需要基础的美学标准:



  1. 节点不重叠

  2. 子节点按照指定的顺序排列

  3. 父节点在子节点中心(逻辑布局)

  4. 主轴方向上不同层级节点不重叠


节点抽象


为了简化可视化树的绘制,[Reingold-Tilford] 提出可以把节点之间的间距和绘制连线抽象出来。通过在节点的宽度和高度上添加间隙来添加节点之间的间距。如下图中的实线框显示了原始宽度和高度,虚线框显示了添加间隙后的宽度和高度。



[Reingold-Tilford] 可视化树节点抽象示意图


我们的思维导图自动布局遵循这个抽象:



  1. 节点布局本身不关注节点连线,只关注节点的宽高和间距

  2. 节点从间距中抽象出来(节点宽高和节点间隙作抽象为一个虚拟节点 LayoutNode)

  3. 布局算法基于 LayoutNode 进行布局


节点在布局时它的宽和高已经融合了实际宽高和上下左右的间隙了,这样可以降低自动布局的复杂度,上图其实是我们布局后的结果,节点从间距中抽象出来之后,节点的垂直顶部位置是其父节点的底部坐标,而父节点的底部坐标又是其顶部坐标加上其高度,真实节点与虚拟节点的逻辑关系如下图所示:



LayoutNode 示意图


算法执行过程


算法流程图:



自动布局算法执行流程图


用一个包含三个节点的例子介绍它自动布局过程,理想的结果应当如下图所示:



步骤一、前置操作: 构造 LayoutNode


这个就是前面提到的节点抽象,基于节点宽高和节点之间的间隙构建布局使用的抽象节点,此时三个处于初始状态,x、y 坐标均为零且相互重叠,如下图所示:



初始状态示意图



左边是真实状态,右侧虚线框部分没有特别的意义,只是一个不重叠的示意



步骤二、布局准备: 垂直分离



布局准备:垂直分离示意图


基于节点的父子级关系进行分层,保证垂直方向是父子级节点不重叠(节点 0与节点 1、2不重叠)。


步骤三:分离兄弟节点



分离兄弟节点过程示意图


就是分离「节点 1」和「节点 2」,保证他们水平不重叠。


步骤四:定位父节点



父级节点定位示意图


基于「节点 1」和「节点 2」重新地位父节点「节点 0」的水平位置,保证父节点水平方向上居中与「节点 1」 和「节点 2」。


布局结果:



以上就是一个的完整布局过程(逻辑下布局),逻辑并不复杂,即使多一些层级和节点也只需要递归执行「步骤三」和「步骤四」。


可以看出「逻辑下布局」只用了「算法流程图」中的前四步就完成了,最后一步「方向变换」就是在「逻辑下布局」的基础上通过数学变换的方式实现「逻辑上」「逻辑右」等布局,下面对方向变换进行专门的解释。


方向变换


1、逻辑下 -> 逻辑上



可以看出这是垂直方向上的变换关系,它们应该是基于一个水平线对称,具体的变换关系如下图所示:



逻辑上变换图


可以看最右侧最下方的节点的「y 点」应该就对应的最右侧最上方的节点「y点」,它们的位置关系应该就是:y= y - (y-yBase) * 2 - node.height。



注意上下变换应该只涉及位移,不涉及上下翻转,也就是节点内部的方向不变,y 对应 y`这两个对应的都是节点的下边上的点位。



2、逻辑下 -> 逻辑右



逻辑右示意图


从上图可以看出,这个逻辑变换也不复杂:就是一个垂直到水平的变换过程,反应到布局算法层中 x、y 方向以及节点宽高的变换,比如:



  1. 垂直分层:需要将垂直分层变换为水平分层

  2. 增加 buildTee 过程:基于分层的节点需要将节点宽度变换高度、x 坐标变为 y 坐标



处理水平布局:增加 buildTree 过程示意图


最后在「方向变换」中将宽高和 x、y 再变换回来:



得到布局结果:



3、逻辑右 -> 逻辑左


逻辑右到逻辑左的位置对应关系应该和最上面说逻辑下到逻辑上的类似,这里不再赘述。


方向变换大概就这三种,下面介绍下下布局嵌套的思路。


布局嵌套


先看一个布局嵌套的示意:



上图第二个子节点使用了另外一种布局(缩进布局),这就属于布局嵌套,布局嵌套仍然需要保证前面说到的「美学标准」比如节点不重叠、父节点居中对齐等。


简单思考: 布局嵌套中的那个有独立布局的子树,它对于整体布局的影响在于它的延伸方向的不受控制,但是如果把有独立布局的子树看做一个整体,提前计算出子树的布局,然后把子树作为整体代入布局算法就可以屏蔽子树延伸方式对整体布局的影响。


整体的处理思路如下图所示:



布局嵌套处理思路示意图


这里可以有一个抽象:把有独立布局的子节点抽象成一个黑盒子(我把它叫做 BlackNode),那么子树布局的影响就会被带入到主布局中,而子树的布局可以保持独立性。



关键点:需要先计算出有独立布局的子树的布局,然后才可以计算父节点布局



四、框架历程/未来


从技术调研到架构设想再到架构落地到产品中,历时大概1年左右的时间,核心工作集中在 2022 年的 1-9 月份,大概的时间线如下:



Plait 框架未来的一些设想



结束语


本文主要介绍从零开始做画图应用、自研画图框架、落地思维导图场景的一些技术方案,作为一个 Web 前端开发者有机会做这样的东西个人感觉很幸运,对于 Plait 框架未来还有很多事情要做,希望它可以发展成为一个成熟的开源社区作品,也期待对画图框架有兴趣的同学可以加入到 Plait 的开源建设中。


作者:pubuzhixing
来源:juejin.cn/post/7205604505647988793
收起阅读 »

简述html2canvas遇见的坑点及解决方案

web
前言 大家好,最近公司在做公众号的海报图生成功能,功能不难,但是其中也遇到了一些坑还有一些细节的问题,这里给大家复盘一下,相互借鉴及学习 制作海报选用工具 这里我看了几款生成图片的工具: html2canvas dom-to-image 这里我选用的是ht...
继续阅读 »

前言


大家好,最近公司在做公众号的海报图生成功能,功能不难,但是其中也遇到了一些坑还有一些细节的问题,这里给大家复盘一下,相互借鉴及学习


制作海报选用工具


这里我看了几款生成图片的工具:



这里我选用的是html2canvas,因为大部分人使用这个比较多,而且我也只听过这个🤣,另一个大家可以去自行摸索,毕竟我看github上也有9k的star


image.png


开始使用插件生成


引入插件


// npm 下载插件
npm install html2canvas
// 项目引入插件
import html2canvas from 'html2canvas';

html2canvas的option配置


属性名默认值描述
allowTaintfalse是否允许跨域图像。会污染画布,导致无法使用canvas.toDataURL 方法
backgroundColor#ffffff画布背景色(如果未在DOM中指定),设置null为透明
canvasnull现有canvas元素用作绘图的基础
foreignObjectRenderingfalse如果浏览器支持,是否使用ForeignObject渲染
imageTimeout15000加载图像的超时时间(以毫秒为单位),设置0为禁用超时
ignoreElements(element) => false谓词功能,可从渲染中删除匹配的元素
loggingtrue启用日志以进行调试
onclonenull克隆文档以进行渲染时调用的回调函数可用于修改将要渲染的内容,而不会影响原始源文档
proxynull代理将用于加载跨域图像的网址。如果保留为空,则不会加载跨域图像
removeContainertrue是否清除html2canvas临时创建的克隆DOM元素
scalewindow.devicePixelRatio用于渲染的比例。默认为浏览器设备像素比率
useCORSfalse是否尝试使用CORS从服务器加载图像
widthElement widthcanvas的宽度
heightElement heightcanvas的高度
xElement x-offset裁剪画布X坐标
yElement y-offset裁剪画布X坐标
scrollXElement scrollX渲染元素时要使用的x滚动位置(例如,如果Element使用position: fixed)
scrollXElement scrollY呈现元素时要使用的y-scroll位置(例如,如果Element使用position: fixed)
windowWidthWindow.innerWidth渲染时使用的窗口宽度Element,这可能会影响媒体查询之类的内容
windowHeightWindow.innerHeight渲染时要使用的窗口高度Element,这可能会影响媒体查询之类的内容

调用html2canvas时传入两个参数,第一个参数是dom节点,第二个参数是options配置项(配置项可根据上方表格进行对应配置)


html2canvas(document.body).then(function(canvas) {
document.body.appendChild(canvas);
});

调用方法生成海报


1.获取节点:let img = document.querySelector("#myImg");


2.配置需要参数:


let options = {
useCORS: true,// 开启跨域
backgroundColor: "#caddff",// 背景色
ignoreElements: (ele) => {},// dom节点
scale: 4,// 渲染出来的比例
};

3.调用方法


html2canvas(img, options).then((canvas) => {
let url = canvas.toDataURL("image/png"); // canvas转png(base64)
this.url = url;
this.isShow = false;
});

到这里html2canvas的相关使用及配置就介绍完了,接下来就是遇见的问题


使用时遇见的坑点及解决方案


图片跨域问题


第一次用我就遇见了这个问题,第一个就是百度的方法,配置useCORS: true,// 开启跨域,然后图片标签上加crossorigin="anonymous",但是结果没用,图片依旧跨域,这时候咱们前端就要硬气一点,直接让后端处理,让后端把图片地址改成base64的形式传给你,或者服务器配置跨域


生成海报时图片模糊问题


生成海报如果模糊,建议把配置项的scale配置高一点,生成的canvas图片把盒子固定大小,显示的图片就更清晰


dom之间有一道横杠


本人是在公众号上做生成海报功能,dom元素顶部是两张图片,图片顶部有一道白线,而且两张图片之间还有一道杠(不好形容),后面发现是因为生成这个海报我在公众号上用的是image标签,改成img标签就没用影响了,具体原因应该是uniapp内部处理image标签时的一些样式问题吧,这是我的猜测


注意:app上不支持html2canvas生成海报(我也是调试的时候发现的)


全部代码


这里代码仅供大家参考


<template>
<view class="poster-content">
<view class="poster-img" id="myImg" v-if="isShow">
<img class="flow" src="../../static/QC-code.png" />
<view class="card-item">
<view class="title-card">爽卡优势</view>
<view class="tip-content">
<view class="left">
<p>零月租,随充随用,不用不扣费</p>
<p>全程4G、不限APP、不限速</p>
<p>支持多场景使用</p>
<p>官方正品、品质保证</p>
</view>
<view class="right">
<view class="right-item">
<view class="qr-code">
<img
id="codeImg"
:src="imgUrl"
style="width: 100%; height: 100%"
class="flow"
crossorigin="anonymous"
/>

</view>
<view style="color: #0032d0">扫码免费领取</view>
</view>
</view>
</view>
</view>
</view>
<view v-else class="canvas-img">
<img style="width: 100vw" :src="url" alt="" />
</view>
<view v-if="isShow" style="padding-bottom: 10px">
<view @click="getImage" class="createPoster">点击生成海报</view>
</view>
<view style="padding-bottom: 50px">
<view
@click="close"
class="createPoster"
style="background-color: #fff; color: #4f80e6"
>
关闭</view
>
</view>
</view>
</template>


<script>
import html2canvas from "html2canvas";
export default {
props: {
imgUrl: {
type: String,
default: "",
},
hasQrCode: {
type: Boolean,
default: false,
},
},
data() {
return {
url: "",
isShow: true,
};
},
onShow() {},
methods: {
close() {
this.$emit("closePop");
},
getImage() {
// this.saveImg()
this.saveImg();
},
saveImg() {
let img = document.querySelector("#myImg");
let options = {
useCORS: true,
backgroundColor: "#caddff",
ignoreElements: (ele) => {},
scale: 4,
};
html2canvas(img, options).then((canvas) => {
let url = canvas.toDataURL("image/png"); // canvas转png(base64)
this.url = url;
this.isShow = false;
});
},
},
};
</script>


<style lang="scss" scoped>
.poster-content {
width: 100vw;
background-color: #caddff;
height: calc(100vh - 50px);
overflow: scroll;
margin-top: -50px;
}
.poster-img {
width: 80vw;
margin: 0 auto;
background-color: #caddff;
// display: flex;
// flex-direction: column;
padding: 40upx 20upx;
text-align: center;
.title {
width: 203px;
height: 96px;
}
.flow {
width: 100%;
height: 300px;
}
.card-item {
background-color: #fff;
font-size: 14px;
margin-top: -12upx;
padding: 20upx 0 40upx;
text-align-last: left;
border-radius: 20upx;
.title-card {
color: #0032d0;
font-size: 36upx;
font-weight: 700;
padding-left: 10upx;
}
.tip-content {
display: flex;
justify-content: space-between;
font-size: 26upx;
.left {
flex: 1;
margin-top: 10upx;
& > p {
line-height: 1.5em;
margin-top: 20upx;
padding-left: 40upx;
position: relative;
&::after {
content: "";
position: absolute;
top: calc(50% - 10upx);
left: 8upx;
width: 20upx;
height: 20upx;
border-radius: 20upx;
background-color: #0256ff;
}
}
}
.right {
width: 90px;
font-size: 20upx;
display: flex;
align-items: center;
padding-right: 16upx;
.right-item {
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
// height: 143px;
.qr-code {
width: 160upx;
height: 160upx;
background-color: #fff;
border-radius: 10upx;
display: flex;
justify-content: center;
align-items: center;
}
}
}
}
}
.canvas-img {
width: 100vw;
height: calc(100vh - 50px);
overflow: scroll;
}
}
.createPoster {
line-height: 2.8em;
width: 90%;
background-color: #5479f7;
margin: auto;
border-radius: 40upx;
color: #fff;
text-align: center;
}
</style>


结尾


这些就是本人在做海报功能所遇见的一些问题及解决方案,希望掘友们相互学习共同进步,如果有什么描述错误的地方希望给我指正,欢迎大家跟我一起交流


作者:一骑绝尘蛙
来源:juejin.cn/post/7168667322956120101
收起阅读 »

不惑之年谈中年危机

web
今年正式进入不惑之年。按 2011 年统计数据,国内 40 岁以上的程序员约是 1.5%,乐观来看,我进入了前 5% 的群体。 美国 2016 年此比例已经有 12.6%,大家还是应该乐观点。 大家都知道的国内第一代程序员,求伯君/雷军已经退休或做 CEO 了...
继续阅读 »

今年正式进入不惑之年。按 2011 年统计数据,国内 40 岁以上的程序员约是 1.5%,乐观来看,我进入了前 5% 的群体。


美国 2016 年此比例已经有 12.6%,大家还是应该乐观点。


大家都知道的国内第一代程序员,求伯君/雷军已经退休或做 CEO 了,而目前还在活跃的骨灰级程序员有陈皓(左耳朵耗子)。估计之后应该也会有越来越多的老程序员,或者说是目前活跃的程序员变成老程序员,出现在大家的视野。


骨灰级程序员:陈皓(左耳朵耗子)


程序员这个职业发展起来也就 20 年,还是一个很年轻的职业,年龄焦虑这个事情着实没有必要。


换位思考一下,如果我是公司的技术招聘者,15年内工作经验的都是大有前途的,只要你技术过得去,价格合理,我都愿意招你。


相反,15年以上的程序员就没那么受欢迎了。


因为初入职场就是互联网的蓬勃发展期,人才短缺是一直存在的,知识积累不够不要紧,个人品行不妥也不要紧,只要你敢于承担一些压力,那你的职业道路都会比较顺畅。如果再幸运一些,就能进入大厂。


升职加薪,迎娶白富美,走上人生巅峰


但是,这也造成了一些不良的现象,在这行业,投机份子其实挺多的。什么热门就跟风炒一炒,能不能做成不要紧,最主要是自己的 KPI 好看。另外,互联网企业的优待,让他们多少有些娇气。从大厂出来,薪资待遇要翻倍,要股票要期权,要好资源要好项目。


所以,一般的公司未必能容纳这些人。他们也未必愿意去这些公司。于是,35岁危机就来了。


35岁危机


年轻人其实是很难感受到中年危机的,中年危机与年轻失业的区别就像新冠与感冒的区别。你以为你经历了中年危机,实际上只是年轻失业。


今年经济不景气,裁员潮估计让一部分人离开了这个行业。年纪大的想要回炉再造,是很难的。但是,如果你还年轻,平时多积累,我相信能很轻松地找到一份工作的。


如何面对危机?


年轻人都会说苟住,换个正能量的说法是活在当下。


读好书、做好事,是能切切实实忘掉焦虑的。


今年读的《反脆弱》、《心流》和《毫无意义的工作》,将这三本书放在一起,还是很有意思的,能看到不同的观点:


jpg.jpg




  • 《反脆弱》让我对工作也有了新认识。看似很稳定的工作,会有可能让你过度依赖,如果遭遇失业就手足无措了。而类似的士司机,饿一餐饱一顿的,反而平时就很有充足的经验应对收入不稳定的情况。


    今年的形势让人更趋于进入大公司、国企、公务员单位,然后这些稳定的工作就真的这么值得大家去追随吗?越是追求稳定,最后是否会适得其反?




  • 《毫无意义的工作》今年敲醒了不少人,他提醒我们,日常琐碎的工作中消磨了我们的生命。然而这本书更多是情绪的宣泄,并没有什么好的解决方法。




  • 而《心流》则希望我们投入去做事情,只有投入了才会获得心流,才会有幸福感。同时,它让我意识到,无法逃避的事实是,工作占了我们生命的 1/4 时间。如果无法从中获得乐趣,那我们的人生注定是悲剧的。




这几本书都让我重新思考我与工作的关系,即使今年外部恶劣的情况,我们也应该重拾自身的信心,重新找回我们的热情、专注。


在面对人员缩减,项目被砍的情况下,我们也许可以把目光放在现有的项目上。


就前端而言,SSR 做不了,Docker 做不了,那就看看 nginx 缓存优化能不能做;低代码做不了,那就看看页面模板能不能做;开源做不了,就看看公共组件能不能做;什么都做不了,那首屏优化,静态资源优化,图片压缩也是能做的,而且还能做得很深。


R-C (1).jpg


只要你想,总有做不完的事情。并且这些事情,其实是我们要还的技术债务。


而此时也是做好技术积累的时机。


面向对象、设计模式、函数式编程、类型编程、异步编程这些基础都可以恶补一下;网络安全、网络通信、内存、CPU等等向外延伸的各类计算机知识也是我们必须掌握的。


工作上认真对待自己的每一行代码,生活中认真对待自己的每一分钱。


深挖知识,深入研究,懂得越多,焦虑自然就越少。


有足够的知识与经验,你的中年危机也许永远不会来


最后,对于年龄的焦虑,再推荐大家看看方励老师在“一席”的演讲《即使是像我这把年龄的人,好奇心也从来没变过,因为我们还活在人间的》


作者:陈佬昔没带相机
来源:juejin.cn/post/7187069862965936188
收起阅读 »

考研失败,加入国企当程序员,真香!

1、考研失败老家成都,本科毕业于帝都某所以能歌善舞著称的985学校,哲学专业。侥幸自己选修过大数据课程,并且听说程序员工资还不错,通过校招加入了隔壁省的一家制造业当程序员。在工作的第3年决定考研,边工作边考研,压力真是不小。让本来就是学渣的我,从研究生考场走出...
继续阅读 »

最近考研出成绩了,大家考得怎么样?分享一个自己考研失败后,入职国企的故事。

1、考研失败

我是工作了3年后才参加考研的。

老家成都,本科毕业于帝都某所以能歌善舞著称的985学校,哲学专业。

大学选专业时家里人不懂,自己全凭爱好,第一志愿就是哲学。上学时有多快乐,毕业时就多难找工作。

侥幸自己选修过大数据课程,并且听说程序员工资还不错,通过校招加入了隔壁省的一家制造业当程序员。

公司管吃管住,工资1w出头,干了2年,感觉到了危机。自己羞于靠关系上位,但技术上,计算机知识太薄弱也发展有限,于是就想趁着还年轻,考个计算机硕士,提升一下自己。

在工作的第3年决定考研,边工作边考研,压力真是不小。让本来就是学渣的我,从研究生考场走出来就知道了最终的结果:没戏了。

成绩出来后,果然不出所料。

2、加入国企

知道初试成绩以后我难过了几天,认真思考了一下自己的未来发展:旁边的帝都我也回不去了,又不甘心留在这个小城市,于是决定回老家成都发展。

毕业后一直在私企,总是听说国企好、央企不错,所以我这次投简历也想试一下。正好看到一篇讲程序员国企的文章:值得程序员加入的173家国企汇总,网友:这下彻底躺平了,于是就按照文中的思路,找到了成都的一些所谓不错的国企投递试试。

成功加入后发现:真香!

  • 福利真 ** 好!和其它公司谈薪酬,别人都是能压多低压多低,来这家国企,竟然还说我工作年限够长,在我期望的基础上加了3k。六险二金更不用说,平时的各种生活保障也是非常到位。不夸张的说,从私企转到国企的我,有一种刘姥姥进大观园的感觉。

  • 真卷!是谁告诉我国企适合养老的?这比我以前在私企工作强度大多了好嘛?而且我第一次听说部门平均加班时长影响个人绩效这种规则。

  • 技术上不激进。可能对于程序员来说,不停的学习新技术是一种常态,但是在这家国企,基本都是传统老技术,我打开代码还看到了我们领导1997年写的头文件。当然,你也可以认为这是一种不好的事。这里的技术对我来说,够了。

  • 同事关系很融洽。我人生中第一次去按摩店找技师,是女同事带我去的,谁能信!不过必须说一句,成都的按摩店是真正规啊!技师可真有劲。

去年回来工作快1年了,我现在对于考研已经释怀了。听说我们部门今年招人开始硕士起步了,有时候我还挺庆幸自己去年没考研成功的,不然即使上了研究生,我这实力,也不一定能进入现在的单位了。

3、写在最后

现在回过头来看,有一个不同的体会:考研是一件好事,但如果本身不是沉迷于科研事业,而是更想赚钱的话,有好的工作机会也别错过。

另外,多关注有用的信息很重要,有时候别人的一番话,可能就需要自己经历几年的曲折才能总结出来。

大家有任何程序员求职问题,欢迎在评论区和我交流~

作者:程序员晚枫
来源:juejin.cn/post/7205541247590973499

收起阅读 »

迄今为止我写过最复杂的算法

web
《亲戚计算器》大概是我迄今为止写过最复杂的算法了,它可能看起来它好像逻辑简单,仅1个方法调用而已,却耗费了我大量的时间!从一开始灵光乍现,想到实现它的初步思路,到如今开源已7年多了。这期间,我一直在不断更新才让它日趋完善,它的工作不仅是对数据的整理,还有我对程...
继续阅读 »

《亲戚计算器》大概是我迄今为止写过最复杂的算法了,它可能看起来它好像逻辑简单,仅1个方法调用而已,却耗费了我大量的时间!从一开始灵光乍现,想到实现它的初步思路,到如今开源已7年多了。这期间,我一直在不断更新才让它日趋完善,它的工作不仅是对数据的整理,还有我对程序逻辑的梳理和设计思路的推敲。


如果你也对传统文化稍微有点兴趣,不妨耐心的看下去……也许你会发现:原理我们日常习以为常的一个称呼,需要考虑那么多细节。


称谓系统的庞大


中国的亲戚称呼系统复杂在于,它对每种亲戚关系都有特定的称呼,同时对于同种关系不同地方、对于不同性别的人都可能有不同的称呼。




  1. 对外国人而言,父母的兄弟姐妹不外乎:uncle、aunt;而对于我们来说,父母的兄弟姐妹有:伯父、叔叔、姑姑、舅舅、姨妈;




  2. 不同地方对同个亲戚的称呼都是不一样的,以爸爸为例,别称包含有:爸爸、父亲、老爸、阿爸、老窦、爹地、老汉、老爷子等等;




  3. 不同关系链可能具有相同的称呼;比如“舅公”一词,可以是父母亲的舅舅,也可以是老公的舅舅,而这两种关系辈分却不同。究其原因我猜测是,传统上由姻亲产生的亲戚关系,为表达谦卑会自降一辈,随子女称呼配偶的长辈。




  4. 一个称呼中可能是多种关系的合称。比如:“父母”、“子女”、“公婆”,他们不是指代一个人物关系,而是几个关系的合称。




在设计这套算法的时候,我希望它能尽量包含各种称呼、各种关系链,因为我之所以做这个项目就是像让它真正集合多种需求,否则如果它不够全面那终究是个代码演示而已。


关系网络的表达


亲戚的关系网络是以血缘和婚姻为纽带联系在一起的,每个节点都是一个人,每个人都有诸如:父、母、兄、弟、姐、妹、子、女、夫、妻这样的基础关系。关系网络中的节点数量随着层级的加深而指数增长!如果是5层关系,大概就有9x9x9x9x9 = 59049种关系了(当然,这其中有小部分是重复的)。如果想要把几万个关系,数十万个称呼全部尽收其中显然是不可能的,没人有那个精力去维护。


xixik_627466c7fa1e646e.jpg


如何将亲戚关系网络中每个节点之间的关系用数据结构表现出来是一个难点。它需要保证数据量尽量全、占用体积小、易检索、可扩展等特点,这样才能保证算法检索关系时的完整性和高效性。


网络的寻址问题


既然是计算,那一定不是简单通过父、母、子、女等这些基础关系找对应称呼了。否则这就是简单的字典查询而已,谈不上算法。如果问的是:“舅妈的儿子的奶奶的外孙”又该如何呢?首先,需要在网络中找到单一称呼,如“舅妈”,而下一步找她的“儿子”,而非你自己的“儿子”。这就要求有类似于指针的功能,关系链每往前走一步,指针就指引着关系的节点,最终需找到答案。


而就像前面说到的一样,某些称谓可能对应多条关系,同时有些关系并不是唯一的。比方说你爸爸的儿子就是你吗?有没有可能是弟弟或者哥哥?而这些是不是同时取决于你的性别呢?
因为如果你是女的,那么你爸爸的儿子必然不是你呀!


这就对算法提出了一个要求,它必须准确的包含多种可能性。



年龄和性别的推测


随着关系链的复杂,最终得到的答案也有多种。那有没有一种可能,在对关系链的描述中是否存在一些词,可以通过逻辑判断知道对方的性别或年纪大小,进而排除一些不可呢?


例如“爱人的婆婆的儿子”,单从“爱人”二字我们并不能推测自己的性别,而后的“婆婆”确是只有女性才有的亲戚,“爱人的婆婆”就足以推断自己是男的,那么“爱人的婆婆的儿子”必然包含自己。相反,“爱人的婆婆的女儿”一定不是自己,只能是自己的姊妹。




再比如:自己哥哥的表哥也是你的表哥,你弟弟的表哥还是你表哥吗?因为你无法判断你弟弟和他的表哥谁大,自然无法判断对方是你的表哥还是表弟。既然都有可能存在,就需要保留可能性进一步计算。这就涉及到了在关系链的计算中不仅仅需要考虑隐藏的性别线索,还有年龄线索。




身份角度的切换


单从亲戚和自己的关系链条中开始算亲戚的称呼,仅仅是单向的推算,只需要一个个关系往下算就好。如果想知道对方称呼为我什么,这就需要站在对方的角度,重新逆向的调理出我和他之间的关系了。比如我的“外孙”应该叫我什么?



另一方面,如果把我置身于第三者,想知道我的两个亲戚他们之间如何称呼,就必须要同时站在两个亲戚的角度,看待他们彼此之间的关系了。比如:我的“舅妈”该叫我的“外婆”什么呢?



年龄排序的问题


前面说到的都是对不同关系链中的可能性推敲,那如果相同的关系如何判断年龄呢?如果你有3个舅舅呢?虽然不管哪个舅舅,他们对于你的关系都一样,他们的老婆你都得叫声“舅妈”。但他们毕竟有年龄区别,自然就有长幼的排序了。有了排序,就又引发了对他们之间关系的思考。


还是举例说明下:“舅舅”和“舅妈”是什么关系?相信大部分第一反应就是夫妻关系呗!其实不尽然,毕竟有些人不会只有一个舅舅吧?那“大舅妈”和“二舅”就不是夫妻关系了,他们是叔嫂关系呀。“二舅”得管“大舅妈”叫“嫂子”,“大舅妈”得管“二舅”叫“小叔子”。




再进一步说,“二舅的儿子”得叫“大舅妈”为“伯母”,“大舅的儿子”得叫“二舅”为“二叔”。这些由父辈的排序问题影响自己称谓的不同,而是我这套算法需要考虑的内容。




怎么样?是不是没有想象中的那么简单?
如果你想了解更多实现和思路的细节,可以关注本项目开源代码哦:github.com/mumuy/relat…


你也可以在此了解算法的基础原理:算法实现原理介绍


作者:passer-by6061
来源:juejin.cn/post/7203734711779196986
收起阅读 »

序列化和反序列化

序列化隐秘的吭,你踩过了没? 序列化和反序列化 Java序列化的目的主要有2个: 网络传输 对象持久化 当2个相对独立的进程,需要进行跨进程服务调用时,就需要把被传输的Java对象编码为字节数组或者ByteBuffer对象。 接收方只需要把这些字节数...
继续阅读 »

序列化隐秘的吭,你踩过了没?


序列化和反序列化



Java序列化的目的主要有2个:




  • 网络传输

  • 对象持久化


image-20230301144505527


当2个相对独立的进程,需要进行跨进程服务调用时,就需要把被传输的Java对象编码为字节数组或者ByteBuffer对象


接收方只需要把这些字节数组或者Bytebuf对象重新解码成内存对象即可实现通信、调用的作用。


image-20230301145117301


那么在我们使用序列化的时候有哪些需要注意的,避免的坑呢?


成员变量不能以is开头



阿里的《Java开发手册》明文规定了:成员变量禁止使用类似 isXxxx 的命名方式,也不要有isXxx命名的方法



image-20230301150018030


image-20230301145401694


大概的意思就是:不要加is前缀,因为部分框架序列化的时候,会以为对应的字段名是isXxxx后面的Xxxx



  • 比如:isSucceed序列化成Succeed,前端读取isSucceed的时候就会发现没有这个字段,然后出错了。


u=4214115302,3196714167&fm=253&fmt=auto&app=120&f=JPEG


这里面的序列化框架其实就是fastjson,我们可以直接去看他的源码


fastjson源码分析:computeGetters



去找get前缀的方法,然后进行字符串切割找到get后面的



image-20230301161434898



去找is前缀的方法,然后进行字符串切割



image-20230301161413220



  • 这里还进行了驼峰命名的判断:ixXxx,第三个字符是否是大写等判断


所以isSucceed字段会被fastjson框架认为Succeed字段。


image.png


默认值



成员变量的默认值同样会带来坑



同样是阿里的《Java开发手册》里面也是规定了:POJO类、RPC方法必须使用包装类型


image.png


关于包装类型和基本类型的区别,如果还有不清楚的,赶紧去看,这是最基础的面试知识点..


POJO类必须使用包装类型



尽量让错误暴露在编译期,不要拖到运行期



基本类型具有初始值,比如:



  • Int:0

  • float:0.0f

  • boolean:false


一个统计点赞的接口里面的返回值包含一个表示点赞数变化的字段,当发生错误的时候,这个字段没有进行赋初始值,就会出现以下情况:



  • 基本类型:读默认值,0,表达的意思就是没有点赞数变化,程序上并不知道是服务器那边出了错。

  • 包装类型:读到了个null,程序上是知道服务器那边出错了,可以进行对应的显示,比如显示 - ,表示读取不到等操作。


u=3180711090,4079282331&fm=253&fmt=auto&app=138&f=JPEG


总的来说就是:如果字段设置为基础类型并且基础类型的默认值具有业务意义,那么就会出错,并且无法感知错误


RPC方法的返回值和参数必须使用包装类型



RPC调用常常具有超时导致调用失败的情况



如果用包装类型,那么在接收方,就能感知到,这次RPC调用是成功,还是失败。


包装数据类型的null值具有表示额外的信息功能



彦祖来都来了,点个赞👍再走吧,这对我来说真的非常重要



作者:Ashleejy
来源:juejin.cn/post/7205478140914843709
收起阅读 »

扯什么 try-catch 性能问题?

“yes,你看着这鬼代码,竟然在 for 循环里面搞了个 try-catch,不知道try-catch有性能损耗吗?”老陈煞有其事地指着屏幕里的代码: for (int i = 0; i < 5000; i++) { try { ...
继续阅读 »

“yes,你看着这鬼代码,竟然在 for 循环里面搞了个 try-catch,不知道try-catch有性能损耗吗?”老陈煞有其事地指着屏幕里的代码:


 for (int i = 0; i < 5000; i++) {
try {
dosth
} catch (Exception e) {
e.printStackTrace();
}
}

我探过头去看了眼代码,“那老陈你觉得该怎么改?”


“当然是把 try-catch 提到外面啊!”老陈脑子都不转一下,脱口而出。


“你是不是傻?且不说性能,这代码的目的明显是让循环内部单次调用出错不影响循环的运行,你其到外面业务逻辑不就变了吗!”


老陈挠了挠他的地中海,“好像也是啊!”



“回过头来,catch 整个 for 循环和在循环内部 catch,在不出错的情况下,其实性能差不多。” 我喝一口咖啡不经意地提到,准备在老陈前面秀一下。


“啥意思?”老陈有点懵地看着我,“try-catch是有性能损耗的,我可是看过网上资料的!”


果然,老陈上钩了,我二话不说直接打开 idea,一顿操作敲了以下代码:


public class TryCatchTest {

@Benchmark
public void tryfor(Blackhole blackhole) {
try {
for (int i = 0; i < 5000; i++) {
blackhole.consume(i);
}
} catch (Exception e) {
e.printStackTrace();
}
}

@Benchmark
public void fortry(Blackhole blackhole) {
for (int i = 0; i < 5000; i++) {
try {
blackhole.consume(i);
} catch (Exception e) {
e.printStackTrace();
}
}
}

}

“BB 不如 show code,看到没,老陈,我把 try-catch 从 for 循环里面提出来跟在for循环里面做个对比跑一下,你猜猜两个差多少?”


“切,肯定 tryfor 性能好,想都不用想,不是的话我倒立洗头!”老陈信誓旦旦道。


我懒得跟他BB,直接开始了 benchmark,跑的结果如下:



可以看到,两者的性能(数字越大越好)其实差不多:



  • fortry: 86,261(100359-14098) ~ 114,457(100359+14098)

  • tryfor: 95,961(103216-7255) ~ 110,471(103216+7255)


我再调小(一般业务场景 for 循环次数都不会很多)下 for 循环的次数为 1000 ,结果也是差不多:



老陈一看傻了:“说好的性能影响呢?怎么没了?”


我直接一个javap,让老陈看看,其实两个实现在字节码层面没啥区别:



tryfor 的字节码



异常表记录的是 0 - 20 行,如果这些行里面的代码出现问题,直接跳到 23 行处理




fortry 的字节码



差别也就是异常表的范围小点,包的是 9-14 行,其它跟 tryfor 都差不多。



所以从字节码层面来看,没抛错两者的执行效率其实没啥差别。


“那为什么网上流传着try-catch会有性能问题的说法啊?”老陈觉得非常奇怪。


这个说法确实有,在《Effective Java》这本书里就提到了 try-catch 性能问题:



并且还有下面一段话:



正所谓听话不能听一半,以前读书时候最怕的就是一知半解,因为完全理解选择题能选对,完全不懂蒙可能蒙对,一知半解必定选到错误的选项!


《Effective Java》书中说的其实是不要用 try-catch 来代替正常的代码,书中的举例了正常的 for 循环肯定这样实现:



但有个卧龙偏偏不这样实现,要通过 try-catch 拐着弯来实现循环:



这操作我只能说有点逆天,这两个实现的对比就有性能损耗了


我们直接再跑下有try-catch 的代码和没 try-catch的 for 循环区别,代码如下:



结果如下:



+-差不多,直接看前面的分数对比,没有 ry-catch 的性能确实好些,这也和书中说的 try-catch 会影响 JVM 一些特定的优化说法吻合,但是具体没有说影响哪些优化,我猜测可能是指令重排之类的。


好了,我再总结下有关 try-catch 性能问题说法:



  1. try-catch 相比较没 try-catch,确实有一定的性能影响,但是旨在不推荐我们用 try-catch 来代替正常能不用 try-catch 的实现,而不是不让用 try-catch

  2. for循环内用 try-catch 和用 try-catch 包裹整个 for 循环性能差不多,但是其实两者本质上是业务处理方式的不同,跟性能扯不上关系,关键看你的业务流程处理。

  3. 虽然知道try-catch会有性能影响,但是业务上不需要避讳其使用,业务实现优先(只要不是书中举例的那种逆天代码就行),非特殊情况下性能都是其次,有意识地避免大范围的try-catch,只 catch 需要的部分即可(没把握全 catch 也行,代码安全执行第一)。


“好了,老陈你懂了没?”


“行啊yes,BB是一套一套的,走请你喝燕麦拿铁!” 老陈一把拉起我,我直接一个挣脱,“少来,我刚喝过咖啡,你那个倒立洗头,赶紧的!”我立马意识到老陈想岔开话题。


“洗洗洗,我们先喝个咖啡,晚上回去给你洗!”


晚上22点,老陈发来一张图片:



你别说,这头发至少比三毛多。


我是yes,我们下篇见~


作者:yes的练级攻略
来源:juejin.cn/post/7204121228016091197
收起阅读 »

实战:一天开发一款内置游戏直播的国产版Discord应用【附源码】(下)

上篇:https://www.imgeek.net/article/825362923声网RTC接入, 直播与语音实现接入在views/Channel/components文件夹下新增一个组件StreamHandler, 该组件为后续我们处理游戏房间的组件, ...
继续阅读 »

上篇:https://www.imgeek.net/article/825362923

声网RTC接入, 直播与语音实现

接入

views/Channel/components文件夹下新增一个组件StreamHandler, 该组件为后续我们处理游戏房间的组件, 先初步编写声网接入逻辑

// views/Channel/components/StreamHandler/index.js

const options = {
appId:
process.env.REACT_APP_AGORA_APPID || "default id",
channel: process.env.REACT_APP_AGORA_CHANNEL || "test",
token:
process.env.REACT_APP_AGORA_TOKEN ||
"default token",
uid: process.env.REACT_APP_AGORA_UID || "default uid",
};

const StreamHandler = (props) => {
// 组件参数: 用户信息, 当前频道所有消息, 当前频道id, 是否开启本地语音
const { userInfo, messageInfo, channelId, enableLocalVoice = false } = props;

const [rtcClient, setRtcClient] = useState(null);
// 声网client连接完成
const [connectStatus, setConnectStatus] = useState(false);

// RTC相关逻辑
useEffect(() => {
AgoraRTC.setLogLevel(3);
const client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });
// TODO: use right channel
client
.join(options.appId, options.channel, options.token, userInfo?.username)
.then(() => {
setConnectStatus(true);
console.log("[Stream] join channel success");
})
.catch((e) => {
console.log(e);
});

setRtcClient(client);
return () => {
// 销毁时, 自动退出RTC频道
client.leave();
setRtcClient(null);
};
}, []);

return (
<>
{!connectStatus && <Spin tip="Loading" size="large" />}
</>
);
}


// 我们需要全局状态中的userinfo, 映射一下到当前组件的props中
const mapStateToProps = ({ app }) => {
return {
userInfo: app.userInfo,
};
};
export default memo(connect(mapStateToProps)(StreamHandler));

然后回到Channel中, 在之前的renderStreamChannel函数中添加上StreamHandler组件

// view/Channel/index.js
const [enableVoice, setEnableVoice] = useState(false);
const toggleVoice = () => {
setEnableVoice((enable) => {
return !enable;
});
}

// 保留了输入窗口, 可以在它的菜单栏中添加游戏频道独有的一些逻辑,
// 这里我加入了开关本地语音的逻辑, 拓展Input的细节可以参照完整版代码
const renderStreamChannel = () => {
return (
<>
<div className={s.messageRowWrap}>
<StreamHandler messageInfo={messageInfo} channelId={channelId} enableLocalVoice={enableVoice} />
</div>
<div className={s.iptWrap}>
<Input chatType={CHAT_TYPE.groupChat} fromId={channelId} extraMenuItems={renderStreamMenu()} />
</div>
</>
);
}

const renderStreamMenu = () => {
return [
{
key: "voice",
label: (
<div
className="circleDropItem"
onClick={toggleVoice}
>
<Icon
name="person_wave_slash"
size="24px"
iconClass="circleDropMenuIcon"
/>
<span className="circleDropMenuOp">
{enableVoice ? "关闭语音" : "开启语音"}
</span>
</div>
),
}
];
}

此时我们创建一个video-开题的游戏频道, 应该可以看到命令行中输出了RTC连接成功信息. [Stream] join channel success

音视频推流

接下来我们继续做实质的RTC推流逻辑, 及用户上下播的入口. 但在那之前, 先简单过一下声网RTC中的一些概念.

参考以下步骤实现音视频通话的逻辑:

  1. 调用 createClient 方法创建 AgoraRTCClient 对象。
  2. 调用 join 方法加入一个 RTC 频道,你需要在该方法中传入 App ID 、用户 ID、Token、频道名称。
  3. 先调用 createMicrophoneAudioTrack 通过麦克风采集的音频创建本地音频轨道对象,调用 createCameraVideoTrack 通过摄像头采集的视频创建本地视频轨道对象;然后调用 publish 方法,将这些本地音视频轨道对象当作参数即可将音视频发布到频道中。
  4. 当一个远端用户加入频道并发布音视频轨道时:
  5. 监听 client.on(“user-published”) 事件。当 SDK 触发该事件时,在这个事件回调函数的参数中你可以获取远端用户 AgoraRTCRemoteUser 对象 。
  6. 调用 subscribe 方法订阅远端用户 AgoraRTCRemoteUser 对象,获取远端用户的远端音频轨道 RemoteAudioTrack 和远端视频轨道 RemoteVideoTrack 对象。

在这里插入图片描述

(以上内容来自声网官方文档)

在上面的接入中, 我们已经完成了创建对象并加入频道两步.
在RTC中, 可以传输音频和视频信号, 由于单个RTC客户端要传输不同种类的数据, 每个单独的音视频源被分成不同的track(由于它们都是实时不断产生的, 我们称作流), 随后通过publish方法, 将我们本地的信号源交付给RTC客户端传输.
随后通过user-published事件的回调来在其他用户发布信号源时进行处理, 首先需要subscribe该用户来获取后续数据, 随后根据不同类型的信号流做处理.
离开时需要关闭本地当前的信号源, 并退出RTC客户端.
最后通过user-unpublished事件监听其他用户退出, 移除它们对应的信号流.

逻辑理清楚后代码就很容易看懂了

// views/Channel/components/StreamHandler/index.js
const StreamHandler = (props) => {
...
// 本地视频元素
const localVideoEle = useRef(null);
// 远程视频元素
const canvasEle = useRef(null);
const [rtcClient, setRtcClient] = useState(null);
const [connectStatus, setConnectStatus] = useState(false);
// 当前直播的用户
const [remoteUser, setRemoteUser] = useState(null);
// 远程音视频track
const [remoteVoices, setRemoteVoices] = useState([]);
const [remoteVideo, setRemoteVideo] = useState(null);

// RTC相关逻辑
useEffect(() => {
...
// client.join 后

// 监听新用户加入
client.on("user-published", async (user, mediaType) => {
// auto subscribe when users coming
await client.subscribe(user, mediaType);
console.log("[Stream] subscribe success on user ", user);
if (mediaType === "video") {
// 获取直播流
if (remoteUser && remoteUser.uid !== user.uid) {
// 只能有一个用户推视频流
console.error(
"already in a call, can not subscribe another user ",
user
);
return;
}
// 播放并记录下视频流
const remoteVideoTrack = user.videoTrack;
remoteVideoTrack.play(localVideoEle.current);
setRemoteVideo(remoteVideoTrack);
// can only have one remote video user
setRemoteUser(user);
}
if (mediaType === "audio") {
// 获取音频流
const remoteAudioTrack = user.audioTrack;
// 去重
if (remoteVoices.findIndex((item) => item.uid === user.uid) == -1) {
remoteAudioTrack.play();
// 添加到数组中
setRemoteVoices([
...remoteVoices,
{ audio: remoteAudioTrack, uid: user.uid },
]);
}
}
});

client.on("user-unpublished", (user) => {
// 用户离开, 去除流信息
console.log("[Stream] user-unpublished", user);
removeUserStream(user);
});
setRtcClient(client);
return () => {
client.leave();
setRtcClient(null);
};
}, []);

const removeUserStream = (user) => {
if (remoteUser && remoteUser.uid === user.uid) {
setRemoteUser(null);
setRemoteVideo(null);
}
setRemoteVoices(remoteVoices.filter((voice) => voice.uid !== user.uid));
};
}

接着我们根据之前提到的自定义消息判断当前在播状态, 以最后一条自定义消息为准.

// views/Channel/components/StreamHandler/index.js
const StreamHandler = (props) => {
const { userInfo, messageInfo, channelId, enableLocalVoice = false } = props;

// 第一条 stream 消息, 用于判断直播状态
const firstStreamMessage = useMemo(() => {
return messageInfo?.list?.find(
(item) => item.type === "custom" && item?.ext?.type === "stream"
);
}, [messageInfo]);

// 是否有直播
const hasRemoteStream =
firstStreamMessage?.ext?.status === CMD_START_STREAM &&
firstStreamMessage?.ext?.user !== userInfo?.username;
// 本地直播状态
const [localStreaming, setLocalStreaming] = useState(
firstStreamMessage?.ext?.status === CMD_START_STREAM &&
firstStreamMessage?.ext?.user === userInfo?.username
);

// 本地直播流状态
const toggleLocalGameStream = () => {
if (hasRemoteStream) {
return;
}
setLocalStreaming(!localStreaming);
};
// 根据直播状态选择渲染
return (
<>
{!connectStatus && <Spin tip="Loading" size="large" />}
{hasRemoteStream ? (
<RemoteStreamHandler
remoteUser={firstStreamMessage?.ext?.user}
localVideoRef={localVideoEle}
channelId={channelId}
userInfo={userInfo}
rtcClient={rtcClient}
/>
) : (
<LocalStreamHandler
localStreaming={localStreaming}
canvasRef={canvasEle}
toggleLocalGameStream={toggleLocalGameStream}
rtcClient={rtcClient}
userInfo={userInfo}
channelId={channelId}
/>
)}
</>
);
}

我们根据hasRemoteStream分成两种逻辑RemoteStreamHandlerLocalStreamHandler(可以先用div+文字的空实现占位), 首先我们来看本地游戏的逻辑

// view/Channel/components/StreamHandler/local_stream.js
const LocalStreamHandler = (props) => {

const {
toggleLocalGameStream,
canvasRef,
localStreaming,
rtcClient,
userInfo,
channelId,
} = props;

const [localVideoStream, setLocalVideoStream] = useState(false);
const localPlayerContainerRef = useRef(null);

// 开启本地视频流
useEffect(() => {
if (!localPlayerContainerRef.current) return;
const f = async () => {
// 暂时使用视频代替游戏流
let lgs = await AgoraRTC.createCameraVideoTrack();
lgs.play(localPlayerContainerRef.current);
setLocalGameStream(lgs);
}
f();
}, [localPlayerContainerRef])

const renderLocalStream = () => {
return (
<div style={{ height: "100%" }} ref={localPlayerContainerRef}>
</div>
)
}

// 控制上下播
const renderFloatButtons = () => {
return (
<FloatButton.Group
icon={<DesktopOutlined />}
trigger="click"
style={{ left: "380px" }}
>
<FloatButton
onClick={toggleLocalGameStream}
icon={
localStreaming ? <VideoCameraFilled /> : <VideoCameraOutlined />
}
tooltip={<div>{localStreaming ? "停止直播" : "开始直播"}</div>}
/>
</FloatButton.Group>
);
};

// 渲染: 悬浮窗和本地流
return (
<>
<div style={{ height: "100%" }}>
{renderFloatButtons()}
{renderLocalStream()}
</div>
</>
);
}

现在我们进入直播房间已经可以看到本地摄像头的内容了, 但我们还没有将视频流投放到RTC中, 且上播逻辑也没有处理

// view/Channel/components/StreamHandler/local_stream.js
useEffect(() => {
// 发布直播推流
if (!localStreaming || !rtcClient || !localVideoStream) {
return;
}
console.log("height", canvasRef.current.height);
console.log("publishing local stream", localVideoStream);
// 将流publish到rtc中
rtcClient.publish(localVideoStream).then(() => {
// 频道中发布一条消息, 表示开始直播
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_START_STREAM,
},
channelId
).then(() => {
message.success({
content: "start streaming",
});
});
});
return () => {
// 用户退出的清理工作,
// unpublish流(远程), 停止播放流(本地), 发送直播关闭消息(频道)
if (localVideoStream) {
rtcClient.unpublish(localVideoStream);
localVideoStream.stop();
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_END_STREAM,
},
channelId
);
message.info({
content: "stop streaming",
});
}
};
}, [rtcClient, localStreaming, canvasRef, userInfo, channelId, localVideoStream]);

为了测试直播效果, 我们需要登录第二个账号(使用浏览器的匿名/开其他的浏览器, 此时cookie没有共享, 可以多账号登录), 进入相同频道, 开启直播, 此时第一个账号应该会自动刷新状态(如果没有则手动切换一下频道), 进入到RemoteStreamHandler, 说明我们直播的逻辑已经完成.

本地语音的逻辑也是类似的, 这里就不再重复.

接下来是远程流的渲染逻辑, 它的逻辑相对简单, 观看者可以选择开始/停止观看直播流

// view/Channel/components/StreamHandler/remote_stream.js
const RemoteStreamHandler = (props) => {

const {
remoteUser,
localVideoRef,
toggleRemoteVideo,
channelId,
userInfo,
rtcClient,
} = props;

// 这里加一个强制t人的开关, 由于debug
const enableForceStop = true;
const forceStopStream = () => {
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_END_STREAM,
},
channelId
);
};
const renderRemoteStream = () => {
return (
<div style={{ height: "100%" }}>
<div
id="remote-player"
style={{
width: "100%",
height: "90%",
border: "1px solid #fff",
}}
ref={localVideoRef}
/>
<div
style={{
display: "flex",
justifyContent: "center",
marginTop: "10px",
}}
>
<span style={{ color: "#0ECD0A" }}>{remoteUser}</span>
&nbsp; is playing{" "}
</div>
</div>
);
};
const renderFloatButtons = () => {
return (
<FloatButton.Group
icon={<DesktopOutlined />}
trigger="click"
style={{ left: "380px" }}
>
<FloatButton
onClick={toggleRemoteVideo}
icon={<VideoCameraAddOutlined />}
tooltip={<div>观看/停止观看直播</div>}
/>
{enableForceStop && (
<FloatButton
onClick={forceStopStream}
icon={<VideoCameraAddOutlined />}
tooltip={<div>强制停止直播</div>}
/>
)}
</FloatButton.Group>
);
};
return (
<>
<div style={{ height: "100%" }}>
{renderFloatButtons()}
{renderRemoteStream()}
</div>
</>
);
}

开关远程流的代码在StreamHander中, 作为参数传给RemoteStream

// views/Channel/components/StreamHandler/index.js
const toggleRemoteVideo = () => {
if (!hasRemoteStream) {
return;
}
console.log("[Stream] set remote video to ", !enableRemoteVideo);
// 当前是关闭状态,需要打开
// 开关远程音频的逻辑也与此类型.
if (enableRemoteVideo) {
remoteVideo?.stop();
} else {
remoteVideo?.play(localVideoEle.current);
}
setEnableRemoteVideo(!enableRemoteVideo);
};

ok, 现在我们已经实现了基于声网RTC, 在环信超级社区集成视频直播的功能.

直播替换为游戏流

接下来我们来将直播流升级一下, 替换成模拟器包, 为了方便测试, 我们直接使用打包好的版本(https://github.com/a71698422/web-0.1.1), pkg包解压后直接放置到项目根目录,

RustNESEmulator 是一个基于Rust语言的NES模拟器, 我们在web平台可以使用它编译好的wasm版本

并将mario.nes文件放到src/assets目录下, 这是初代马里奥游戏的ROM文件(你也可以使用你喜欢的nes游戏, 如果遇到问题, 欢迎到RustNESEmulator中提issue)

加入前端的模拟器适配代码

// views/Channel/components/StreamHandler
// from tetanes.

import * as wasm from "@/pkg";
class State {
constructor() {
this.sample_rate = 44100;
this.buffer_size = 1024;
this.nes = null;
this.animation_id = null;
this.empty_buffers = [];
this.audio_ctx = null;
this.gain_node = null;
this.next_start_time = 0;
this.last_tick = 0;
this.mute = false;
this.setup_audio();
console.log("[NES]: create state");
}

load_rom(rom) {
this.nes = wasm.WebNes.new(rom, "canvas", this.sample_rate);
this.run();
}

toggleMute() {
this.mute = !this.mute;
}

setup_audio() {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (!AudioContext) {
console.error("Browser does not support audio");
return;
}
this.audio_ctx = new AudioContext();
this.gain_node = this.audio_ctx.createGain();
this.gain_node.gain.setValueAtTime(1, 0);
}

run() {
const now = performance.now();
this.animation_id = requestAnimationFrame(this.run.bind(this));
if (now - this.last_tick > 16) {
this.nes.do_frame();
this.queue_audio();
this.last_tick = now;
}
}

get_audio_buffer() {
if (!this.audio_ctx) {
throw new Error("AudioContext not created");
}

if (this.empty_buffers.length) {
return this.empty_buffers.pop();
} else {
return this.audio_ctx.createBuffer(1, this.buffer_size, this.sample_rate);
}
}

queue_audio() {
if (!this.audio_ctx || !this.gain_node) {
throw new Error("Audio not set up correctly");
}

this.gain_node.gain.setValueAtTime(1, this.audio_ctx.currentTime);

const audioBuffer = this.get_audio_buffer();
this.nes.audio_callback(this.buffer_size, audioBuffer.getChannelData(0));
if (this.mute) {
return;
}
const source = this.audio_ctx.createBufferSource();
source.buffer = audioBuffer;
source.connect(this.gain_node).connect(this.audio_ctx.destination);
source.onended = () => {
this.empty_buffers.push(audioBuffer);
};
const latency = 0.032;
const audio_ctxTime = this.audio_ctx.currentTime + latency;
const start = Math.max(this.next_start_time, audio_ctxTime);
source.start(start);
this.next_start_time = start + this.buffer_size / this.sample_rate;
}
// ...
}

export default State;

改造local_stream

// view/Channel/components/StreamHandler/local_stream.js

import mario_url from "@/assets/mario.nes";
import * as wasm_emulator from "@/pkg";
import State from "./state";

const LocalStreamHandler = (props) => {
// 模拟器 state
const stateRef = useRef(new State());

// 注意要将原来的代码注释掉
/*
const [localVideoStream, setLocalVideoStream] = useState(false);
const localPlayerContainerRef = useRef(null);

// 开启本地视频流
useEffect(() => {
if (!localPlayerContainerRef.current) return;
const f = async () => {
// 暂时使用视频代替游戏流
let lgs = await AgoraRTC.createCameraVideoTrack();
lgs.play(localPlayerContainerRef.current);
setLocalGameStream(lgs);
}
f();
}, [localPlayerContainerRef])

// 推流的函数也暂时注释
useEffet...
*/


useEffect(() => {
// 本地游戏
if (!canvasRef) {
return;
}
// 开启键盘监听等全局事件
wasm_emulator.wasm_main();
fetch(mario_url, {
headers: { "Content-Type": "application/octet-stream" },
})
.then((response) => response.arrayBuffer())
.then((data) => {
let mario = new Uint8Array(data);
// 加载 rom数据
stateRef.current.load_rom(mario);
});
}, [canvasRef]);

// 更新本地流渲染
const renderLocalStream = () => {
return (
<div style={{ height: "100%" }}>
<canvas
id="canvas"
style={{ width: 600, height: 500 }}
width="600"
height="500"
ref={canvasRef}
/>
</div>
);
};
}

这一步完成后, 我们就可以在本地试玩马里奥游戏了, 键盘绑定为

A      = J
B = K
Select = RShift
Start = Return
Up = W
Down = S
Left = A
Right = D

将推本地视频流改为游戏流

  useEffect(() => {
// 发布直播推流
if (!localStreaming || !rtcClient) {
return;
}
// 只修改了流获取部分
// canvas的captureStream接口支持获取视频流
// 我们用这个视频流构造一个声网的自定义视频流
let stream = canvasRef.current.captureStream(30);
let localVideoStream = AgoraRTC.createCustomVideoTrack({
mediaStreamTrack: stream.getVideoTracks()[0],
});
console.log("height", canvasRef.current.height);
console.log("publishing local stream", localVideoStream);
rtcClient.publish(localVideoStream).then(() => {
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_START_STREAM,
},
channelId
).then(() => {
message.success({
content: "start streaming",
});
});
});
return () => {
if (localVideoStream) {
rtcClient.unpublish(localVideoStream);
localVideoStream.stop();
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_END_STREAM,
},
channelId
);
message.info({
content: "stop streaming",
});
}
};
}, [rtcClient, localStreaming, canvasRef, userInfo, channelId]);

最后总结一下房间的流程图
在这里插入图片描述

至此该项目的完整流程就算结束啦,如果有哪些步骤细节不太明确, 可以参照完整版项目
环信超级社区项目
注册环信
模拟器直播项目github源码获取

收起阅读 »

实战:一天开发一款内置游戏直播的国产版Discord应用【附源码】(上)

游戏直播是Discord产品的核心功能之一,本教程教大家如何1天内开发一款内置游戏直播的国产版Discord应用,用户不仅可以通过IM聊天,也可以进行语聊,看游戏直播,甚至自己进行游戏直播,无任何实时音视频底层技术的Web开发者同样适用,效果如下: 开整!St...
继续阅读 »

游戏直播是Discord产品的核心功能之一,本教程教大家如何1天内开发一款内置游戏直播的国产版Discord应用,用户不仅可以通过IM聊天,也可以进行语聊,看游戏直播,甚至自己进行游戏直播,无任何实时音视频底层技术的Web开发者同样适用,效果如下:



开整!

Step1 初始化

本项目基于环信超级社区的实例项目, 所以我们先从Circle-Demo-Web这个仓库开启做初始化

  1. 克隆项目 git clone https://github.com/easemob/Circle-Demo-Web.git
  2. 安装依赖 npm install
  3. 设置appKey src/utils/WebIM.js 中设置appKey,AppKey为环信后台项目对应的key,注册环信,https://console.easemob.com/user/register,登录console后台获取Appkey
  4. 运行项目 npm run start

运行后, 登录完毕效果如下,
在这里插入图片描述

与discord设计逻辑相似, 左边功能区有

  • 个人信息页
  • 好友会话页
  • 当前加入的频道
  • 创建新频道
  • 加入服务器

超级社区的逻辑为

社区(Server)、频道(Channel) 和子区(Thread) 三层结构

社区为一个独立的结构, 不同社区直接相互隔离, 社区包含不同的频道, 代表了不同的话题, 用户在频道中聊天, 而针对一条单独信息产生的回复为子区.

我们本次的项目主要集中在频道部分, 需要加入一个服务器后, 创建一个测试社区, 保证你具有管理员权限.

Step2 协议设置

在这里插入图片描述

我们的目标是尽量利用现有api扩展功能, 有几个问题需要解决

  1. 如何区分普通频道和游戏频道?
  2. 如何区分当前频道是否有玩家直播, 如果有直播如何获取玩家信息, 第二玩家的状态?
  3. 多人聊天的状态?

如何区分普通频道和游戏频道

这里直接简单采用频道前缀做特殊区分, 创建频道前缀带video-的识别为游戏频道, 同时将渲染内容做替换

// views/Channel/index.js


const isVideoChannel = useMemo(() => {
return currentChannelInfo?.name?.startsWith("");
}, [currentChannelInfo]);

const renderTextChannel = () => {
// 原来的渲染逻辑
return (
<>
<MessageList
messageInfo
={messageInfo}
channelId
={channelId}
handleOperation
={handleOperation}
className
={s.messageWrap}
/>
<div className={s.iptWrap}>
<Input chatType={CHAT_TYPE.groupChat} fromId={channelId} />
</div>
</>
);
}

const renderStreamChannel = () => {
// 先填充一个占位符
return (
<>This is a Stream Channel<>
);
}

return (
...
<div className={s.contentWrap}>
{isVideoChannel ? renderStreamChannel() : renderTextChannel()}
</div>
...
);

如果需要区分图标, 可以搜索channelNameWrap, 分别在channelItemChannel/components/Header中添加一个css类, 通过这个类设置图标图片

如何区分当前频道是否有玩家直播, 如果有直播如何获取玩家信息, 第二玩家的状态?

我们可以复用在频道中发送消息的机制, 直播开始, 结束都可以当做一条特殊的消息发送, 只不过这条消息不承载用户的信息, 而是表达用户上下播的行为

当然这个机制存在一定实时性的问题, 不过大致是可行的.

首先我们来看一条普通的消息是如何发送的

  // components/input

//发消息
const sendMessage = useCallback(() => {
if (!text) return;
getTarget().then((target) => {
let msg = createMsg({
chatType,
type: "txt",
to: target,
msg: convertToMessage(ref.current.innerHTML),
isChatThread: props.isThread
});
setText("");
deliverMsg(msg).then(() => {
if (msg.isChatThread) {
setThreadMessage({
message: { ...msg, from: WebIM.conn.user },
fromId: target
});
} else {
insertChatMessage({
chatType,
fromId: target,
messageInfo: {
list: [{ ...msg, from: WebIM.conn.user }]
}
});
scrollBottom();
}
});
});
}, [text, props, getTarget, chatType, setThreadMessage, insertChatMessage]);

去除掉与输入框逻辑耦合的部分, 可以分为两步, createMsg创建消息, deliverMsg发送消息, 这两个功能都是环信SDK功能的封装, 经过查阅文档, 它支持发送自定义消息.
在utils中新建一个stream.js文件来封装直播的逻辑

// utils/stream.js
const sendStreamMessage = (content, channelId) => {
let msg = createMsg({
chatType: CHAT_TYPE.groupChat,
type: "custom",
to: channelId,
ext: {
type: "stream",
...content,
},
});
return deliverMsg(msg)
.then(() => {
console.log("发送成功");
})
.catch(console.error);
};

它接收content表示我们的额外信息, 用户名和上下播状态, channelId区分不同的channel, 对它的调用可以如下

// 定义在 utils/stream.js 中
const CMD_START_STREAM = "start";
const CMD_END_STREAM = "end";

// 上播
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_START_STREAM,
},
channelId
);
// 下播
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_END_STREAM,
},
channelId
);

第二玩家的状态可以类比第一个玩家用额外的自定义消息实现, 这里不做重复.

关于自定义消息, 原本它的作用是邀请用户加入频道, 你可以在components/CustomMsg中找到, 我们要额外识别一下直播消息(可以渲染在消息列表里, 也可以直接屏蔽掉).

// components/CustomMsg/index.js
const isStream = message?.ext?.type === "stream";


// 屏蔽
const renderStream = () => {
return (<>)
}
if (isStream) {
return renderStream();
} else {
...
}

多人聊天的状态?

我们引入声网RTC sdk, 每个进入直播房间的用户都对应维护一个声网客户端,
通过on事件感知远端视频/音频流.

根据文档 进行如下操作,

  1. 注册声网开发者, 并在后台创建一个测试项目
  2. 项目根目录创建.env文件, 存放api token等信息
# channel, uid 暂时设置为固定
REACT_APP_AGORA_APPID = your app id
REACT_APP_AGORA_CHANNEL = test
REACT_APP_AGORA_TOKEN = your token
REACT_APP_AGORA_UID = 123xxx
  1. 添加声网sdk依赖 npm install agora-rtc-sdk-ng

我们在下一章中编写接入逻辑

声网RTC接入, 直播与语音实现

收起阅读 »

迎接35岁,我在美团的22年总结及23年规划

22年10月,喜提二胎,同时儿子还不到两岁,工作中已经苦练写作基本功了,很难有心力在工作外写东西。 但作为一个技术从业者,定期写技术博客和总结规划,是确保高效工作的好习惯。工作已经十年了,越来越认可“选择比努力更重要”。 多数人都不喜欢思考,经常用战术上的勤奋...
继续阅读 »

22年10月,喜提二胎,同时儿子还不到两岁,工作中已经苦练写作基本功了,很难有心力在工作外写东西。


但作为一个技术从业者,定期写技术博客和总结规划,是确保高效工作的好习惯。工作已经十年了,越来越认可“选择比努力更重要”。


多数人都不喜欢思考,经常用战术上的勤奋掩饰自己战略上的懒惰,我也是如此,人生才如此被动。


本文不仅会分享自己在2022年所作出的重大人生选择及2023年的规划,还会在字里行间反思自己从高考后一些重大选择的得失。希望自己能以此洗心革面、痛改前非和开启新生,也希望能给有缘人在人生选择上有一些参考。


自我简介



本节主要是流水账的自我回顾,枯燥而乏味,建议读者有选择地看,或者跳过看后面的总结及规划哈~



从小爱学习


我出生于广西一个瑶族自治县的农村家庭,虽然家庭很不富裕,但童年回忆起来还是有很多快乐。但在青少年时期,父母吵闹较多,邻里关系不和,让看似阳光开朗的我,其实在内心已经种下了忧郁的种子,即使成年后,远离家乡,也时常会做相关的噩梦。‘


从小我就喜欢学习,记得小学一二年级,我经常6点多就到学校,有时周末也去学校,还被人笑话,但成绩只有数学还可以。直到四年级,我才开始获得奖状,印象中四至六年级的班主任,总是让我感觉自己很优秀,于是我的成绩真的变好了。


求学之路


初中,通过二婶大姐的帮助,把我安排到了县城最好中学的农村班,开始了住校的生活。初二开始,经常能考全班第一,全校前30。虽然中考不是很好,但还是考上了市里最好的高中。


从高二开始,成绩不时能考全班第一。高考虽然自我感觉发挥不好,但总分还是能排到全校应届第10,全广西一千六百多名。


由于家庭不富裕,高考后我优先考虑读军校,这样大学几年不会让家里负担过重。但因为各种原因,提前批我居然录取上。


于是我就来到了当时随手填的大学及专业,大一时发现可以选拔在校国防生,毕业后和军校生一样直接派到部队任职。于是大二时,我通过选拔加入了国防生队伍。


大学四年的成绩不是很好,因此没有资格保研。但我心中还是很想读研的,但因为国防生的身份,清华不允许报考,丧失了动力后自然考不上,这也成了后来我主动离开部队的原因之一。


军旅生涯


毕业后,我被分配到了北京军区某部的作战部队,在基层连队体验了一个多月后,就被派到南京某军校进行学习培训。军校培训期满后,我回到了北京的部队,开始了指挥军官的工作。


同多数国防生一样,我对基层部队的生活很不适应,被冻晕过几次,加上单位不同意我报考国防大学的研究生,我在13年底提出了复员转业申请。整体还算顺利,提出申请两个多月后,审批就通过了,然后我就离开了部队。


虽然从军入伍有减轻家庭负担的考虑,但离开部队后我还是会经常梦回连队,内心依然很向往部队的工作,也许还有壮志未酬,更有愧疚之情。即使不在部队了,我相信在国防建设上,还是有很多我可以参与的。


离开部队后,我全身心地投入了考研,因为计算机专业考公务员的岗位比较多,所以我选择了跨专业考清华的计算机专业。但没想到我专业课差得太多,只过了国家线,虽有一些院校联系我调剂,但我还是放弃了。


工作履历


然后,我就准备找C++相关的工作,通过大二时就认识的国防生朋友,内推到了一个做3G&4G 通信卡的外企gemalto,正式开启了程序员的生活。


由于做嵌入式操作系统的开发比较乏味,所以我们主动参与了一个小组的 Android 开发工作,学得差不多后,我们都先后离开了外企,他去了360做安卓开发,我去了搜狐做RN开发。


从搜狐开始,我从RN入门大前端,逐步掌握了react web开发、node 中间层或后端开发、vue web 开发、flutter 或 uni-app 跨端开发、PHP、Java、Go和Ruby后端开发。


从2017下半年开始,我的工作主要是管理团队和推进重点项目,团队最多时接近30人,职能上不仅管理过前端、安卓和iOS,也负责过设计团队。


疫情开始的半年多时间,尝试过在线教育创业,后来因为进展不符合预期和需要结婚生娃,在20年下半年入职了美团的前端基建团队。


回顾2022年


2022年初,因为老婆怀上了二胎,我终于做出了两个人生非常重要的选择:一是离开北京;二是从管理者向技术专家转变。


离京的选择


现在想想,有时选对城市,比选对行业或公司都重要。从2007年来到北京上大学,到2022年挥别北京,转眼间,我已经在北京度过了15个春秋。


离开北京时,我居然没有一丝伤感,仔细一想,15年来自己都没有真正考虑过要留在北京。虽然2007年时,就把户口迁到了北京。因为入伍地是北京,离开部队后,只要找到国企接收,也能把户口留在北京。但当时报考的是清华计算机深圳分院,索性就把户口迁回老家了。


21年12月底,父母都在北京,于是全家在北京过了年,但父亲还是不适应北京的气候,春节后就回老家了。而我亲弟在上海,老家只有我爸和爷爷,出了意外不能及时回到他们身边处理,加上老婆怀上了二胎,未来两个孩子的户口及上学问题,使得我不得不考虑离京的问题。


因为我所在的团队,在上海也有很多研发,有时我也需要去上海出差,加上我弟、堂妹、表姐和表弟等都在上海,所以当时优先考虑去上海,这样也不需要换部门或公司。


但由于年初上海的疫情比较严重且排外严重,同时上海离老家还是太远,看到公司在深圳也有岗位,聊了两个部门都有意向后,我选择了深圳,代价是需要换个部门,绩效和调薪都会受影响。选择深圳还有另外一个考虑,去上海的话,至少要缴满一年社保才能有户口,而深圳只需要缴满一个月即可。


三月底发起活水申请并逐步交接工作,四月中旬我就来到了深圳,五一后就把全家接到深圳安定了下来,6月初就办理好了全家的户口迁移。


当时在知乎上,反复看了深圳、上海、广州和北京的城市对比。对我来说,还是深圳最适合我。相比北京,深圳空气质量好很多,我老婆来了深圳后皮肤变好了很多,还有深圳的政务办事效率很高,日常生活很方便。


回想在北京时,早上我得六点前开车上班,否则不知道被五环堵到什么时候,下班时我得八点半前走,否则不知道什么时候可以离开望京。来到深圳后,我就住在公司附近,出门到工位不到10分钟就可以了,有了更多工作时间和关注生活。


回顾城市的选择,我真的很后悔没有早点来到深圳,若不是因为老婆执意要离开北京,因为22年初申请到了工作居住证,很有可能我下半年就在北京买房继续麻木地北漂了。


工作的选择


2022年,全世界所有的互联网公司都不好过,裁员消息层出不穷。作为一个即将35岁的大龄码农,我也是危机感满满,做好了随时被裁的心理建设。


对我来说,留在原部门原团队是最保险的,也是最好的:之前的工作以管理沟通为主,比较得心应手,领导们也比较认可,绩效和调薪也都有保障,同时负责的基本是中后台技术项目或前端基建项目,如组件库、物料管理、提效工具等,既有技术深度又有较好的工作节奏。


但因为原部门不能在深圳放团队,所以我只能通过活水到在深圳有岗位的团队,结合自己长远的职业规划,我选择了人工智能方向的前端团队。虽然团队只有十人上下,但可以做的事情却很有技术深度,同时团队的学历也比较好,60%都是硕士,还有一个是北大的硕士。


来到新团队后,我先加入近期很火的人工智能创作项目,使用 vite2 + vue3 从0开发了一个以图片处理为主要功能的web应用,经常加班赶项目进度,业余时间自学图形渲染。


然而,来到深圳不到半个月,就有一个前端伙伴要离职了,需要开发维护他从零开发的渲染引擎,工作难度比较大加上来深圳后因为燥热一直休息不好,我萌生了先辞职休息一段时间,再重新找工作的想法。


期间虽然没有好好准备面试,抱着了解招聘行情的心态,也和腾讯、字节、虾皮及一些传统行业的公司聊了聊,能拿到offer的基本是管理岗,传统行业一般是大前端总监,管理五十人以上团队,直接向CTO或CEO汇报。


虽然没有调薪,但领导们多次挽留,并给我争取了一些股票,特别是我的直接leader,相当nice,可以让我选择自己想做的事情,并允许我休个长假调整身心,所以我选择留了下来。


做出这个选择的考虑:一是管理岗可遇不可求,毕业后80%的工作都是管理,技术沉淀不够;二是web端的图形渲染和AI推理技术门槛高且非常有趣,并且近几年的人才缺口大,以后即使做管理,招不到人时,自己也得能搞定。


休假回来后:在业务支持上,我调整到了模型训练和管理相关的中后台项目,便于更好地掌握AI应用开发相关的知识技能;在研发提效上,我基于在前端基建团队的建设成果,修订了我们团队的前端工程规范和推进了项目的工程改造;在技术产出上,我主导了web推理引擎的立项,从零实现了 WebAssembly 计算方案,推进了在人脸验证和智能创作等项目的落地。


在新的团队,因为少了很多管理相关的会议,让我有了更多的机会和时间,结合项目需要,系统学习图形渲染和AI应用开发相关的知识技能。


截屏2023-01-27 18.53.30.png


过去的一年,因为两个孩子比较小需要更多精力放在家庭,同时也因为变换城市和岗位,还有新冠的影响,工作产出应该只达到了我预期的70%。但很幸运,让我遇见了一个很好的团队,有了领导的信任和优秀的伙伴们,相信新的一年,我一定会收获满满。


2023年规划


2023年,我首先要养成三一习惯:每周跑一十公里强体魄、每周看一好书启智慧、每周做一公益得快乐。


其次,在生活上,我要好好研究做饭和带娃。虽然我很想为公司的外卖业务贡献力量,但是对孩子们来说,能选择的很少,而且安全和营养都不好保证。生娃养娃容易,但教好孩子很不容易,童年的创伤,我相信一定可以在养育孩子的过程中治愈。同时,带娃也能帮助我理解一些人工智能相关的理论,现在感觉模型训练和教小孩真的很相似~


最后,在工作上:上半年,我要带领小伙伴们进一步完善Web推理引擎,同时提供一系列面向web应用研发同学的AI入门教程;下半年,我将从提升模型部署易用性出发,规划并建设一个全端的AI推理系统。


截屏2023-01-27 18.55.28.png


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

【架构师李肯】成为了公司股东,而我却失眠了!

提笔的现在是2023年1月13日农历腊月廿二,凌晨四点半,我刚失眠醒来不久,头脑里的思绪万千,无法入睡,索性把它写下来,以便有个回忆。 惊喜与荣誉 说起这次失眠,源于昨天发生的一件事,下午上着班,突然收到微信群邀请,点进去一看,都是公司BOSS、高管、中层l...
继续阅读 »

提笔的现在是2023年1月13日农历腊月廿二,凌晨四点半,我刚失眠醒来不久,头脑里的思绪万千,无法入睡,索性把它写下来,以便有个回忆。


图片


惊喜与荣誉


说起这次失眠,源于昨天发生的一件事,下午上着班,突然收到微信群邀请,点进去一看,都是公司BOSS、高管、中层leader、以及各研发部门的绝对核心,你还别说,真有一种【误入藕花深处】的感觉。我一想,可能有事要发生,且很有可能事儿还不小。


果然,随后群主简单说了下,今晚BOSS要请大家喝酒撸串,请大家务必抽空。也没说啥主题,就发了时间和地点,大家按时到达即可。


直到饭局上,才从BOSS口中得到了确切的消息,也的确算得上是一个好消息:今晚是公司新旧股东的感谢会加欢迎会。其中,我和几个小伙伴就是新加入的那一批股东。


我一听这好消息,有些喜出望外,但又感觉合乎常理,因为我知道公司每年都有配股的传统,也算是公开的小秘密,只要你表现足够优秀,就一定能被高层看见,这也许就是【小庙】的好处吧。


席上,我端起来了闲置了大半年的酒杯,是因为身体原因,也是本身并不嗜酒,所以每次拿开车作为借口搪塞一下也就过去了。但这次有些不一样,毕竟BOSS端着酒在你面前,你说能不能不喝?


有人的地方,就有江湖。烤串局上,一个个推杯换盏,很是热闹,大家笑说旧年的总结,展望新年的目标。


之余,让我印象最深的是公司分管销售的副总裁的敬酒。对,没错,就是公司业绩扛把子的那位。他跟我说,恭喜你加入公司的股东会,恭喜你在2年内就得到了公司的认可,但你知道我花了多久才被这个行业认可吗?答案是【20年】!


哗哦,不真不令人竖起大拇指,绝对对得起他的那些远赴盛名,值得我辈学习。


图片


满载的收获


聊到这,乘着这次机会,再次聊聊2022年的收获吧。


2022年在工作上始终不遗余力,有领导的绝对核心信任,工作方面得心应手,架构设计,疑难攻关,前沿预研,传帮分享,一个没落下,也终于在自己的努力下,再次拿下了年度绩效S考核的优异成绩,不出什么幺蛾子的话,【优秀XX人】的奖杯应该正在制作中!


工作上,至少在我负责的研发领域,可以说收获到了极致,其他平行能力领域还需进一步拓展。


同时,2022年是我致力于打造【架构师李肯】这个技术IP的元年,这一年我在工作之余,沉心创作了100+的技术原创博文,谈不上有多高产,比起那种年入上W篇的量产大户,我这基本拿不上台面。但必须要承认的是,我自己的好些博文是真的用心在这的,有时候为了写清楚一个问题,前前后后耗时都接近一周。很荣幸,这些博文都得到了行业大佬们的认可,比如在RT-Thread技术论坛上,这些博文都被追加了【优秀】,这是一种肯定与荣耀,也是基于这些优异的社区贡献,RT-Thread在2022年度的开发者大会上授予了我【2022年度RT-Thread社区杰出布道者】。这又是更高一级的认可,反正这个牛批可以拿出去吹好一会了!又比如在电子发烧友论坛上,我也收获了很多,技术专栏是我一个亮点,也是因为内容够硬,很荣幸收到了电子发烧友的年度表彰,成为了【年度优秀专栏创作者】。


技术创作是一方面,参加课外大赛是另一方面。期间参加的国民技术应用设计大赛和瑞萨电子应用设计大赛都收获了第一名的好成绩,还有一笔不错的奖金,真是美滋滋。


2022年一共受邀参加了4场在线演讲,写了4份PPT,谈不上多亮点,但只要我用心,有那么一页PPT能够帮助到有心人,这就够了!


还有一个最重要的,今年收获了很多技术道路上志同道合的朋友,我们一起打怪升级,感谢你们的支持,你们就是【架构师李肯】这个技术IP持续发展的最大动力。


图片


追忆与迷茫


这部分话题,本来想再写写的,看着天快亮了,留个记号吧 // TODO


图片


新年新希望


2022年注定是不平凡的一年,有着太多的不堪回首和刻骨铭心,而对于我来说,却也是我收获的一年。最后,新的一年里,祝大家在技术的路上兔飞猛进,在职场的发展上兔步青云;愿大家都能收获心中所想,实现新的远大目标!


图片


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

iOS 3年开发迷茫随心聊

iOS 3年开发迷茫随心聊 从毕业开始做iOS,到现在已经是第4个年头了。第一家公司,做了一年,项目没上线就倒闭了,导致找第二家公司的时候也没有一个项目能拿的出手。第二家公司误入一家游戏公司,每天工作就是将H5小游戏做成一个App,想办法上线,一年过去了,技术...
继续阅读 »

iOS 3年开发迷茫随心聊


从毕业开始做iOS,到现在已经是第4个年头了。第一家公司,做了一年,项目没上线就倒闭了,导致找第二家公司的时候也没有一个项目能拿的出手。第二家公司误入一家游戏公司,每天工作就是将H5小游戏做成一个App,想办法上线,一年过去了,技术其实也没什么长进,但是通过这个过程了解一些苹果上架的知识。也有了几个上线项目经验了。由于想找个正经互联网公司做App,也离职找工作。


找工作头一个月,发现面试面的问题都是与底层相关,一问三不知,在家埋头学了2个月底层相关知识(可以理解背题)。原理是懂一点了,但是没有在实际项目中运用,工资也上不太去。在面了20多家公司后,终于找到现在第三份工作。


由于有第二份工作的经历,在第三家公司上班的时候一直在学习,有意识的去面试的原理去解决一些开发中的问题,例如使用runtime解决一些问题,却发现runtime如果没有很强的理解,还是不要用在项目里,因为可能出现未知的风险。例如使用交换方法,全局做了修改,但是后期项目需求更改,保持全局修改的前提下,对其他情况要做不同处理。也没有太多需求会使用到原理的内容,性能也不需要优化。


小公司对技术不太感冒,能完成需求就行,虽然自己力所能及的去做一些规范,但觉得做的还是不够,也不清楚其他公司到底是如何做的。小公司个人感觉对员工做事的责任心更加看重。需求就是写页面,页面还原的好,做的快一点,bug少一点就行了。不是理想的一个团队有什么方案,规范,让开发更有效率。最大感触是还好没有成为一个油腻的开发~。


在现在的公司,做了几个项目,也没有大的bug。学会了Swift进行开发。也许也算是一种收获吧。但是公司不加薪,今年的目标是想学点东西换一份工作。


学了1个月RxSwift,感觉也快学不下去了,公司是不可能用了,网上也有人说这个架构太重了。语言是个问题,自己英语水平有限,学习速度太慢了。如果有看到的这篇文章的小伙伴,也可以给我点意见。


最近想学一点提高开发效率的技能。和面试相关的内容。如果有大神经历过我这个时期,还麻烦给点建议。建议退iOS坑的就不必留言了。个人虽然菜,但是如果还没有做到小公司天花板的话,目前不考虑退坑。


第一次发文章也不知道说啥,后面会更新一些学习笔记啥的。感谢包容。


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

Android:面向单Activity开发

记得前一两年很多人都跟风面向单Activity开发,顾名思义,就是整个项目只有一个Activity。一个Activity里面装着N多个Fragment,再给Fragment加上转场动画,效果和多Activity跳转无异。其实想想还比较酷,以前还需要关注多个Ac...
继续阅读 »

记得前一两年很多人都跟风面向单Activity开发,顾名思义,就是整个项目只有一个Activity。一个Activity里面装着N多个Fragment,再给Fragment加上转场动画,效果和多Activity跳转无异。其实想想还比较酷,以前还需要关注多个Acitivity之间的生命周期,现在只需关注一个,但还是需要对Fragment的生命周期进行关注。



其实早在六七年前GitHub上就有单Activity的开源库Fragmentation,后来谷歌也出了一个库Navigation。本来以为官方出品必为经典,当时跟着官方文档一步一步踩坑,最后还是放弃了该方案。理由大概如下:



  1. 需要创建XML文件,配置导航关系和跳转参数等

  2. 页面回退是重新创建,需要配合livedata使用

  3. 貌似还会存在卡顿,一些栈内跳转处理等问题


而Github上Fragmentation库已经停止维护,所幸的是再lssuse中发现了一个基于它继续维护的SFragmentation,于是正是开启了面向单Activity的开发。


提供了可滑动返回的版本


dependencies {
//请使用最新版本
implementation 'com.github.weikaiyun.SFragmentation:fragmentation:latest'
//滑动返回,可选
implementation 'com.github.weikaiyun.SFragmentation:fragmentation_swipeback:latest'
}

由于是Fragment之间的跳转,我们需要将原有的Activity跳转动画在框架初始化时设置到该框架中


Fragmentation.builder() 
//设置 栈视图 模式为 (默认)悬浮球模式 SHAKE: 摇一摇唤出 NONE:隐藏, 仅在Debug环境生效
.stackViewMode(Fragmentation.BUBBLE)
.debug(BuildConfig.DEBUG)
.animation(
R.anim.public_translate_right_to_center, //进入动画
R.anim.public_translate_center_to_left, //隐藏动画
R.anim.public_translate_left_to_center, //重新出现时的动画
R.anim.public_translate_center_to_right //退出动画
)
.install()

因为只有一个Activity,所以需要在这个Activity中装载根Fragment


loadRootFragment(int containerId, SupportFragment toFragment)

但现在的APP几乎都是一个页面多个Tab组成的怎么办呢?


loadMultipleRootFragment(int containerId, int showPosition, SupportFragment... toFragments);

有了多个Fragment的显示,我们需要切换Tab实际也很简单


showHideFragment(ISupportFragment showFragment);

是不是使用起来很简单,首页我们解决了,关于跳转和返回、参数的接受和传递呢?


//启动目标fragment
start(SupportFragment fragment)
//带返回的启动方式
startForResult(SupportFragment fragment,int requestCode)
//接收返回参数
override fun onFragmentResult(requestCode: Int, resultCode: Int, data: Bundle?) {
super.onFragmentResult(requestCode, resultCode, data)
}
//返回到上个页面,和activity的back()类似
pop()

对于单Activity而言,我们其实也可以注册一个全局的Fragment监听,这样就能掌控当前的Fragmnet


supportFragmentManager.registerFragmentLifecycleCallbacks(
object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) {
super.onFragmentAttached(fm, f, context)
}
override fun onFragmentCreated(
fm: FragmentManager,
f: Fragment,
savedInstanceState: Bundle?
) {
super.onFragmentCreated(fm, f, savedInstanceState)
}
override fun onFragmentStarted(fm: FragmentManager, f: Fragment) {
super.onFragmentStarted(fm, f)
}
override fun onFragmentResumed(fm: FragmentManager, f: Fragment) {
super.onFragmentResumed(fm, f)
}
override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) {
super.onFragmentDestroyed(fm, f)
}
},
true
)

接下来我们看看Pad应用。对于手机应用来说,一般不会存在局部页面跳转的情况,但是Pad上是常规操作。


image.png


如图,点击左边列表的单个item,右边需要显示详情,这时候再点左边的其他item,此时的左边页面是保持不动的,但右边的详情页需要跳转对应的页面。使用过Pad的应该经常见到这种页面,比如Pad的系统设置等页面。这时只使用Activty应该是不能实现的,必须配合Fragment,左右分为两个Fragment。


但问题又出现了,这时候点击back怎么区分局部返回和整个页面返回呢?


//整个页面回退,主要是用于当前装载了Fragment的页面回退
_mActivity.pop()
//局部回退,被装载的Fragment之间回退
pop()

如下图,这样的页面我们又应该怎么装载呢?
image.png


可以分析,页面最外面是一个Activty,要实现单Activity其内部必装载了一个根Fragment。接着这个根Fragment中使用ViewPage和tablayout完成主页框架。当前tab页要满足右边详情页的单独跳转,还得将右边页面作为主页面,以此装载子Fragment才能实现。


image.png


总结


单Activity开发在手机和平板上使用都一样,但在平板上注意的地方更多,尤其是平板一个页面可能是多个页面组成,其局部还能单独跳转的功能,其中涉及到参数回传和栈的回退问题。使用下来,我还是觉得某些页面对硬件要求很高的使用单Activity会出现体验不好的情况,有可能是优化不到位。手机应用我还是使用多Activity方式,平板应用则使用该框架实现单Activity方式。


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

2023也该知道了:kotlin协程取消和异常传播机制

什么是结构化并发? 说好的异常传播为啥失效了? 怎么还有async不抛异常的问题? 1 结构化并发(Structured Concurrency) 1.1 java的"离散性并发" kotlin 的Coroutine是【结构化并发】,与结构化并发对应的方式是...
继续阅读 »
  • 什么是结构化并发?

  • 说好的异常传播为啥失效了?

  • 怎么还有async不抛异常的问题?


1 结构化并发(Structured Concurrency)


1.1 java的"离散性并发"


kotlin 的Coroutine是【结构化并发】,与结构化并发对应的方式是【fire and forget 】姑且称之为【离散性并发】吧,可能不太准确。一个例子解释下离散性并发,java里我们开启一个线程之后,是不具备跟踪管理这个线程的能力的。如下


    public void javaThreadFun() {
       Thread thread = new Thread(new Runnable() {
           @Override
           public void run() {
               //do some work
          }
      });
       thread.setName("child-thread");
       thread.start();
  }

这个例子中,调用javaThreadFun()方法所在的线程,创建并启动child-thread线程之后两个线程没有明确的父子关系,avaThreadFun()方法所在的线程不能天然的感知在自己线程里启动的"子线程",子线程发生异常之后也不会影响到自己。如果父线程要取消中止在自己线程里启动的那些线程也没有现成的方式去供使用。总之,层级关系管理上很离散。


1.2 kotlin 协程的的结构化并发


image.png


kotlin的协程天然的具备父协程管理取消子协程、子协程的异常失败影响父协程或者父协程感知子协程错误和失败的能力。如下示例


      GlobalScope.launch {
           val parentJob = launch {
               val childJob = launch {
                   delay(1_000)//子任务做一些事情
                   throw NullPointerException() //会导致父协程任务和兄弟协程任务都会被取消
              }
               delay(5_000)
          }
      }


  • childJob失败抛出异常,会影响到父job,进而父job会取消掉其所有的子job。

  • 另外,父job也会等待所有的子任务结束后自己才会结束。


与传统的相比



  • 有跟踪:在协程左右域里启动协程会作为该协程的子协程,该协程会跟踪这些协程的状态。而不是像线程那些开启之后就忘记没有跟踪。父协程的结束也是要在所有子协程都完成之后自己才会完成,颇有家长负责制的感觉。

  • 可取消:取消父协程也会,把其子协程一并取消掉。如上图,取消掉parent-job会导致从属于他的所有子协程取消。

  • 能传播:这特性体现在,子协程发生异常,会通知其父协程,父协程会取消掉自己所有的子协程然后再向上传递直到根协程,或者supervisorJob.(这个下文我们会展开分析)


2 取消机制


2.1 父协程的取消会取消子协程


这一章节我们展开聊下Kotlin协程的取消机制,上一节我们提到,父协程/作用域的取消也会取消其子协程我们看个例子。


 GlobalScope.launch {
       val mParentJOb: Job = this.launch {
           val child1Job: Job = this.launch {
               this.launch {
                   delay(300)
              }.invokeOnCompletion { throwable ->
                   println("child1Job 执行完毕,收到了${throwable}")
              }
               val child2Job = this.launch {
                   delay(500)
              }.invokeOnCompletion { throwable ->
                   println("child2Job 执行完毕,收到了${throwable}")
              }
          }
           delay(100)
      }
       mParentJOb.invokeOnCompletion { throwable ->
           println("mParentJOb 执行完毕,收到了${throwable}")
      }
       println("发起取消 mParentJOb")
       mParentJOb.cancel()
  }.join()

运行结果:


发起取消 mParentJOb
child1Job 执行完毕,收到了kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@100b06de
child2Job 执行完毕,收到了kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@100b06de
mParentJOb 执行完毕,收到了kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelled}@100b06de

2.2 兄弟协程取消不影响


private suspend fun brotherCoroutine() {
   coroutineScope {
       launch {
           delay(500)
           println("is running")
      }
       launch {
           delay(100)
           cancel()
      }.invokeOnCompletion {
           println("job2 is canceled")
      }
  }
}

这似乎没有什么可解释的,某个协程的取消并不会影响到其兄弟协程。


2.3 携程的取消是协作式的


协程的取消是协作式的体现在,对取消的通知需要主动的需要主动嗅探感知做出处理。举个例子


private suspend fun coroutineCanceling() {
   coroutineScope {
       val job = launch {
           var i = 0
           while (true) {//1
               println(" is running ${i++}")
          }
      }
       job.invokeOnCompletion {
           println("job is completion ${it}")
      }
       delay(50)
       job.cancel()
  }
}

会发现上面这个段代码并不能被取消,原因就是协程并没有感知到自己已经被取消了。这一点跟java thead interrupt机制类似,需要我们感知取消。感知取消的方式有



  • 可以使用CoroutineScope.isActive()的方法check是否已经被取消做出反应,代码一处可改成while (isActive)

  • 所有的suspend方法内部也会感知cancel。比如delay()方法就是一个suspend方法。


2.4 做好善后取消


协程取消后我们可能会做一些诸如回收资源的动作,但在一个已经处于取消状态的协程里再调用suspend方法就抛出CancellationException异常。此时我们要使用 withContext(NonCancellable) 做取消后的工作


private suspend fun handleCanceling() {
   coroutineScope {
       val job = launch {
           try {
               delay(100)//do Something
          } finally {
               withContext(NonCancellable) {
                   delay(100)
              }
          }
      }
       job.invokeOnCompletion {
           println("job is completion ${it}")
      }
       delay(50)
       job.cancel()
  }
}

另外,还有特别注意的一点是,被取消的协程会向外抛出异常如果使用try-catch捕获但不抛出异常CancellationException,会影响到异常的传播,也就破坏了协程的异常传播机制,具体下一节异常传播机制展开。


2.5 kotlin协程的父子结构


看下面这段代码,思考一个问题,2处字符串会被打印出出来吗,为什么?


private suspend fun parentChildStructTest() {
   coroutineScope {
      val job1 =  launch {
        val job2 =  launch(Job()) {//1
               delay(500)
               println("job2 is finish")//2
          }
           delay(100)
           this.cancel()
      }
  }
}

不会打印,不知道你有没有答对。


不是说好的,取消父协程的时候会取消掉其子协程吗?而且子协程里还调用了delay()方式,也会响应取消。问题的关键点在于,job1和job2的父子结构被破坏了。示例代码里1处传入了一个Job对象,此时job2的父层级已经变成了传入的job对象。我们稍加改造下,这里只是为了理解,不建议这么用,会发现job2可以被取消了。


private suspend fun parentChildStructTest() {
   coroutineScope {
       val job1 = launch {
           val j = Job()
           val job2 = launch(j) {
               delay(500)
               println("job2 is finish")
          }.invokeOnCompletion {
               println("job2 OnCompletion $it")
          }
           delay(100)
           j.cancel() //1
      }
  }
}

新协程的context的组成有两个公式


parentContext = scopeContext + AddionalContext(launch方法传入的context)

childContext = parentConxtext + job(新建)

1_zuX5Ozc2TwofXlmDajxpzg.webp(图来自[Roman Elizarov])



  • 新协程的context是【parent context】和【新建job】的相加操作而来。

  • 【parent context】是由父层级的context和传入的参数context相加操作而来。

  • 子协程的job会和父层级中context的job建立一个父子关系。


当我们使用coroutineScope.launch(Job()){}传入了一个job实例的时候,其实子协程的job和传入的job实例建立了父子结构,破坏了原本的父子结构。


3 异常传播机制


3.1 异常的传播


private suspend fun destroyCoroutineScope() {
   coroutineScope {
       launch {
           launch {
               delay(500)
               throw NullPointerException()
          }.invokeOnCompletion {
               println("job-1 invokeOnCompletion $it")
          }

           launch {
               delay(800)
          }.invokeOnCompletion {
               println("job-2 invokeOnCompletion $it")
          }
      }.invokeOnCompletion {
           println("job-parent completion $it")
      }
  }
}


  • 子job异常后,传播到父协程,父协程会取消到自己所有的子协程,然后再往上传播

  • 如果是一个取消异常(CancellationException)并不会被取消协程,父协程的处理器会忽略他。也就是在子协程上抛出异常之后,父协程接收到不会做处理。


3.2 监督作用域异常传播(Supervision)


基本表现:使用supervisorScope启动的子协程发生异常时,不影响父协程和兄弟协程。


private suspend fun supervisorJobTest() {
   supervisorScope {
       launch {
           delay(100)
           throw NullPointerException()
      }
       launch {
           delay(800)
           println("job 2 is running")
      }
  }
}

如上代码,supervisor范围内第一个job抛出异常后,并不会影响第二个job;把错误异常控制在范围内。



  • SupervisorCoroutine的子协程发生了异常之后不会影响父协程自身,也不会向上传播。

  • 如果 CoroutineContext没有设置CoroutineExceptionHandle,最终异常会传播到ExceptionHandler


但其他的结构化并发特性仍然存在



  • 当父协程取消,他的协程也被取消。

  • 子协程取消不影响父协程。

  • 父协程抛出异常,子协程也会被取消。

  • 父协程要等所有子协程完成后才结束。


简单的讲,监督协程具备单向传播的特性,即子协程的异常和取消不影响父协程,父协程的异常和取消会影响子协程


两种方式:



  • 构建CoroutineScope时传入SupervisorJob()

  • 使用supervisorScope{}产生


注意


监督协程中的每一个子作业应该通过异常处理机制处理自身的异常。如果不处理异常会被吞掉。


3.3 CoroutineExceptionHandler


用于捕获协程执行过程中未捕获的异常,被用来定义一个全局的异常处理器。



  • 不能恢复异常,只是打印、记录、重启应用。

  • 只能在【根作用域】或者【supervisorScope的直接子协程】启动协程是传入才生效。


举个例子


suspend fun coroutineExceptionHandlerTest() {
   supervisorScope {
       val handler = CoroutineExceptionHandler { _, _ -> println("handleException in coroutineExceptionHandler") }
       launch(handler) {
           delay(100)
           throw NullPointerException()
      }
  }
}  

3.4 浅看源码


主从作用域和协作作用域的表现区别上文已经讲到了,通常我们构建一个协程作用域两种方式


val scope = CoroutineScope(Job())
val supervisorJob = CoroutineScope(SupervisorJob())


  • CourotineScope()方法(没错这是个方法),通过传入Job()SupervisorJob生成的对象最终获得主从作用域和协同作用域。


  supervisorScope { scope -> xx }
 coroutineScope { scope ->xx }


  • 通过supervisorScope()或者coroutineScope()构建作用域。



private class SupervisorCoroutine<in T>(
   context: CoroutineContext,
   uCont: Continuation<T>
) : ScopeCoroutine<T>(context, uCont) {
   override fun childCancelled(cause: Throwable): Boolean = false
}

private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
   override fun childCancelled(cause: Throwable): Boolean = false
}

两种作用域在代码上的区别是 fun childCancelled(cause: Throwable) 方法的实现不同,监督作用域直接返回fasle表示不处理子协程的错误异常。让其自己处理


//JobSupport
   private fun cancelParent(cause: Throwable): Boolean {
...
       return parent.childCancelled(cause) || isCancellation //1
  }

   private fun finalizeFinishingState(state: Finishing, proposedUpdate: Any?): Any? {
...
      val handled = cancelParent(finalException) || handleJobException(finalException)//2
      if (handled) (finalState as CompletedExceptionally).makeHandled()
    ...
  }

源代码中的核心逻辑,



  • 1处的parent.childCanceled的值的最终来源其实就是我们实现的childCancelled方法的返回值

  • 2处当我们是一个监督作用域起cancelParent的返回值为false,这种情况下代码就会执行后半句handleJobException(),这半句的内部其实最终是执行了我们设置的CoroutineExceptionHandler。

  • 2处cancelParent除了在我们监督作用域的时候返回fasle,在根协程下会返回fasle,这也就是为什么CoroutineExceptionHandler设置在根协程下生效的原因。


代码很多细节不展开有兴趣的自行研究。


4 异常传播需注意问题


4.1 supervisorScope的孙子协程


private suspend fun childChildSupervisorJob() {
   supervisorScope { // SupervisorCouroutine
       launch {  // ScopeCoroutine
          val job1 =  launch {
               delay(100)
               throw NullPointerException()
          }
          val job2 = launch {
               delay(800)
               println("job 2 is running")
          }.invokeOnCompletion {
               println("job2 is completion $it")
          }
      }
  }
}


  • 看上面这个例子job1抛出空指针异常后,job2会不会受影响。

  • 是正常的coroutineScope而非supervisorScope,因此supervisorScope的“孙子协程”不遵循互不影响原则


4.2 注意不要破坏父子结构


private suspend fun textSupervisorJob() {
   supervisorScope {
       launch(SupervisorJob()) {//1
           launch {
               delay(100)
               throw NullPointerException()
          }
           launch {
               delay(800)
               println("job 2 is running")
          }.invokeOnCompletion {
               println("job2 is completion $it")
          }
      }
  }
}


  • job1抛出异常也会影响job2,原因1处虽然传入了SupervisorJob,但是这个实例其实是作为父context的job传入的,真是job1和job2的parentContext还是job类型,而不是SupervisorJob。具体原理可以看2.5小节


5 关于async的误会


通常构建一个协程除了使用CoroutineScope.launch{}还会使用CoroutineScope.async{}。


经常看到这种说法,async方式启动的协程返回一个Deferred对象,当调用deffered的await()方法的时候才会抛出异常


private suspend fun asyncSample() {
   val h = CoroutineScope(CoroutineExceptionHandler { _, _ -> println("发生了异常") })
   val d = h.launch {
       async {
           delay(100)
           throw NullPointerException()
      }
       launch { //job2
           delay(500)
           println("job 2 is finish")
      }
  }.join()
}

这个例子没有调用await(),实际发现也会立马抛出异常,导致jo2都没执行完。跟我们认为的不一样。


实际情况是这样的:当async被用作构建根协程(由协程作用域直接管理的协程)或者监督作用域直接管理协程时,异常不会主动抛出,而是在调用.await()时抛出。其他情况不等待await就会抛出异常。


6 总结


本文梳理了Kotlin的协程的取消和异常传播处理机制。机制的设置总的来说是服务于结构化并发的。本文应该能让我们了解掌握以下问题才算合格



  • kotlin协程结构化并发的特性

  • 协程的context是怎么来的?怎么构成的?父协程的context和协程的parentContext是同一个概念吗?

  • kotlin的协程是怎么传播的?主从作用域监督作用域的区别?怎么实现

  • async方式启动的协程要await()的时候才抛出异常?

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

Java 中为什么要设计 throws 关键词,是故意的还是不小心

我们平时在写代码的时候经常会遇到这样的一种情况 提示说没有处理xxx异常 然后解决办法可以在外面加上try-catch,就像这样 所以我之前经常这样处理 //重新抛出 RuntimeException public class ThrowsDemo { ...
继续阅读 »

我们平时在写代码的时候经常会遇到这样的一种情况


throws.png


提示说没有处理xxx异常


然后解决办法可以在外面加上try-catch,就像这样


trycatch.png


所以我之前经常这样处理


//重新抛出 RuntimeException
public class ThrowsDemo {

public void demo4throws() {
try {
new ThrowsSample().sample4throws();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

//打印日志
@Slf4j
public class ThrowsDemo {

public void demo4throws() {
try {
new ThrowsSample().sample4throws();
} catch (IOException e) {
log.error("sample4throws", e);
}
}
}

//继续往外抛,但是需要每个方法都添加 throws
public class ThrowsDemo {

public void demo4throws() throws IOException {
new ThrowsSample().sample4throws();
}
}

但是我一直不明白


这个方法为什么不直接帮我做


反而要让我很多余的加上一步


我处理和它处理有什么区别吗?


而且变的好不美观


本来缩进就多,现在加个try-catch更是火上浇油


public class ThrowsDemo {

public void demo4throws() {
try {
if (xxx) {
try {
if (yyy) {

} else {

}
} catch (Throwable e) {
}
} else {

}
} catch (IOException e) {

}
}
}

上面的代码,就算里面没有业务,看起来也已经比较乱了,分不清哪个括号和哪个括号是一对


还有就是对Lambda很不友好


lambda.png


没有办法直接用::来优化代码,所以就变成了下面这样


lambdatry.png


本来看起来很简单很舒服的Lambda,现在又变得又臭又长


为什么会强制 try-catch


为什么我们平时写的方法不需要强制try-catch,而很多jdk中的方法却要呢


那是因为那些方法在方法的定义上添加了throws关键字,并且后面跟的异常不是RuntimeException


一旦你显式的添加了这个关键字在方法上,同时后面跟的异常不是RuntimeException,那么使用这个方法的时候就必须要显示的处理


比如使用try-catch或者是给调用这个方法的方法也添加throws以及对应的异常


throws 是用来干什么的


那么为什么要给方法添加throws关键字呢?


给方法添加throws关键字是为了表明这个方法可能会抛出哪些异常


就像一个风险告知


这样你在看到这个方法的定义的时候就一目了然了:这个方法可能会出现什么异常


为什么 RuntimeException 不强制 try-catch


那为什么RuntimeException不强制try-catch呢?


因为很多的RuntimeException都是因为程序的BUG而产生的


比如我们调用Integer.parseInt("A")会抛出NumberFormatException


当我们的代码中出现了这个异常,那么我们就需要修复这个异常


当我们修复了这个异常之后,就不会再抛出这个异常了,所以try-catch就没有必要了


当然像下面这种代码除外


public boolean isInteger(String s) {
try {
Integer.parseInt(s);
return true;
} catch (NumberFormatException e) {
return false;
}
}

这是我们利用这个异常来达成我们的需求,是有意为之的


而另外一些异常是属于没办法用代码解决的异常,比如IOException


我们在进行网络请求的时候就有可能抛出这类异常


因为网络可能会出现不稳定的情况,而我们对这个情况是无法干预的


所以我们需要提前考虑各种突发情况


强制try-catch相当于间接的保证了程序的健壮性


毕竟我们平时写代码,如果IDE没有提示异常处理,我们完全不会认为这个方法会抛出异常


我的代码怎么可能有问题.gif


我的代码怎么可能有问题!


不可能绝对不可能.gif


看来Java之父完全预判到了程序员的脑回路


throws 和 throw 的区别


java中还有一个关键词throw,和throws只有一个s的差别


throw是用来主动抛出一个异常


public class ThrowsDemo {

public void demo4throws() throws RuntimeException {
throw new RuntimeException();
}
}

两者完全是不同的功能,大家不要弄错了


什么场景用 throws


我们可以发现我们平时写代码的时候其实很少使用throws


因为当我们在开发业务的时候,所有的分支都已经确定了


比如网络请求出现异常的时候,我们常用的方式可能是打印日志,或是进行重试,把异常往外抛等等


所以我们没有那么有必要去使用throws这个关键字来说明异常信息


但是当我们没有办法确定异常要怎么处理的时候呢?


比如我在GitHub上维护了一个功能库,本身没有什么业务属性,主要就是对于一些复杂的功能做了相应的封装,提供给自己或别人使用(如果有兴趣可以看看我的库,顺便给Star,嘿嘿


对我来说,当我的方法中出现异常时,我是不清楚调用这个方法的人是想要怎么处理的


可能有的想要重试,有的想要打印日志,那么我干脆就往外抛,让调用方法的人自己去考虑,自己去处理


所以简单来说,如果方法主要是给别人用的最好用throws把异常往外抛,反之就是可加可不加


结束


很多时候你的不理解只是因为你还不够了解


作者:不够优雅
来源:juejin.cn/post/7204594495996100664
收起阅读 »

ProtoBuf 动态拆分Gradle Module

预期 当前安卓的所有proto都生成在一个module中,但是其实业务同学需要的并不是一个大杂烩, 只需要其中他们所关心的proto生成的类则足以。所以我们希望能将这样一个大杂烩的仓库打散,拆解成多个module。 buf.yaml Protobuf是Pr...
继续阅读 »

预期


当前安卓的所有proto都生成在一个module中,但是其实业务同学需要的并不是一个大杂烩, 只需要其中他们所关心的proto生成的类则足以。所以我们希望能将这样一个大杂烩的仓库打散,拆解成多个module


结构图.png


buf.yaml



Protobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,用于描述一种轻便高效的结构化数据存储格式,并于2008年对外开源。Protobuf可以用于结构化数据串行化,或者说序列化。它的设计非常适用于在网络通讯中的数据载体,很适合做数据存储或 RPC 数据交换格式,它序列化出来的数据量少再加上以 K-V 的方式来存储数据,对消息的版本兼容性非常强,可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。开发者可以通过Protobuf附带的工具生成代码并实现将结构化数据序列化的功能。



在我司proto相关的都是由后端大佬们来维护的,然后这个协议仓库会被android/ios/后端/前端 依赖之后生成对应的代码,然后直接使用。


而proto文件中允许导入对于其他proto文件的依赖,所以这就导致了想要把几个proto转化成一个java-library工程,还需要考虑依赖问题。所以由
我们的后端来定义了一个buf.yaml的数据格式。


version: v1
name: buf.xxx.co/xxx/xxxxxx
deps:
- buf.xxxxx.co/google/protobuf
build:
excludes:
- setting
breaking:
use:
- FILE
lint:
use:
- DEFAULT

name代表了这个工程的名字,deps则表示了他依赖的proto的工程名。基于这份yaml内容,我们就可以大概确定一个proto工程编译需要的基础条件。然后我们只需要一个工具或者插件来帮助我们生成对应的工程就够了。


模板工程


现在我们基本已经有了一个单一的proto工程的输入模型了,其中包含工程名依赖的工程还有对应文件夹下的proto文件。然后我们就可以基于这部分输入的模型,生成出第一个模板工程。


plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
id 'com.google.protobuf'
}


java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

sourceSets {
def dirs = new ArrayList<String>()
dirs.add("src/main/proto")
main.proto.srcDirs = dirs
}

protobuf {
protoc {
if (System.getProperty("os.arch").compareTo("aarch64") == 0) {
artifact = "com.google.protobuf:protoc:$version_protobuf_protoc:osx-x86_64"
} else {
artifact = "com.google.protobuf:protoc:$version_protobuf_protoc"
}
}
plugins {
grpc {
if (System.getProperty("os.arch").compareTo("aarch64") == 0) {
artifact = 'io.grpc:protoc-gen-grpc-java:1.36.1:osx-x86_64'
} else {
artifact = 'io.grpc:protoc-gen-grpc-java:1.36.1'
}
}
}
generateProtoTasks {
all().each { task ->
task.generateDescriptorSet = true
task.builtins {
// In most cases you don't need the full Java output
// if you use the lite output.
java {

}

}
task.plugins {
grpc { option 'lite' }
}
}
}
}
afterEvaluate {
project.tasks.findByName("compileJava").dependsOn(tasks.findByName("generateProto"))
project.tasks.findByName("compileKotlin").dependsOn(tasks.findByName("generateProto"))
}
dependencies {
implementation "org.glassfish:javax.annotation:10.0-b28"
def grpcJava = '1.36.1'
compileOnly "io.grpc:grpc-protobuf-lite:${grpcJava}"
compileOnly "io.grpc:grpc-stub:${grpcJava}"
compileOnly "io.grpc:grpc-core:${grpcJava}"
File file = new File(projectDir, "depend.txt")
if (!file.exists()) {
return
}
def lines = file.readLines()
if (lines.isEmpty()) {
return
}
lines.forEach {
logger.lifecycle("project:" + name + " implementation: " + it)
implementation(it)
}
}

如果需要将proto编译成java代码,就需要依赖于com.google.protobuf插件,依赖于上面的build.gradle基本就可以将一个proto输入编译成一个jar工程。


另外我们需要把所有的proto文件拷贝到这个壳工程的src/main/proto文件夹下,最后我们会将buf.yaml中的name: buf.xxx.co/xxx/xxxxxx/xxx/xxxxxx转化成工程名,去除掉一些无法识别的字符。


我们生成的模板工程如下:


image.png


其中proto.version会记录proto内的gitsha值还有文件的lastModified时间,如果输入发生变更则会重新进行一次文件拷贝操作,避免重复覆盖的风险。


input.txt则包含了所有proto文件路径,方便我们进行开发调试。


deps 转化


由于proto之间存在依赖,没有依赖则会导致无法将proto转化成java。所以这里我讲buf.yaml中读取出的deps转化成了一个depend.txt.


com.xxxx.api:google-protobuf:7.7.7

depend.txt内会逐行写入当前模块的依赖,我们会对name进行一次转化,变成一个可读的gradle工程名。其中7.7.7的版本只是一个缺省而已,并没有实际的价值。


多线程操作


这里我们出现了一点点的性能问题, 如果可以gradle插件中尽量多使用点多线程,尤其是这种需要io的操作中。


这里我通过ForkJoinPool,这个是ExecutorService的实现类。其中submit方法中会返回一个ForkJoinTask,我们可以将获取gitsha值和lastModified放在这个中。之后把所有的ForkJoinTask放到一个数组中。


fun await() {
forkJoins.forEach {
it.join()
}
}

然后最后暴露一个await方法,来做到所有的获取方法完成之后再继续向下执行。


另外则就是壳module的生成,我们也放在了子线程内执行。我们这次使用了线程池的invokeAll方法。


protoFileWalk.hashMap.forEach { (_, pbBufYaml) ->
callables.add(Callable<Void> {
val root = FileUtils.getRootProjectDir(settings.gradle)
try {
val file = pbBufYaml.copyLib(File(root, "bapi"))
projects[pbBufYaml.projectName()] = file.absolutePath ?: ""
} catch (e: Exception) {
e.printStackTrace()
e.message.log()
}
null
})
}
executor.invokeAll(callables)

这里有个面试经常出现的考点,多线程操作Hashmap,之后我在测试环节随机出现了生成工程和include不匹配的问题。所以最后我更换了ConcurrentHashMap就没有出现这个问题了。


加载壳Module


这部分就和源码编译插件基本是一样的写法。


projects.forEach { (s, file) ->
settings.include(":${s}")
settings.project(":${s}").projectDir = File(file)
}

把工程插入settings 即可。


结尾


最终结果大概就是原先一个Module,现在被拆分成100+的Module,而且基于buf.yaml 文件动态生成,基本符合第一期需求。


这部分方案这样也就大概完成了一半,剩下的一半我们需要逐一把生层业务的依赖进行一次变更,这样就可以做到依赖最小化,然后也可以去除掉一部分无用的代码块。


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

拥有思想,你就是高级、资深、专家、架构师

当然要想成为高级工程师或者架构师,光看书是不行的,书本上来的东西都是工具型编程的体现,何为工具型编程呢? 就是说可以依据书本、网络等渠道就能完成的编程就是工具型编程,那怎么解决呢? 为什么要提升编程思想 这个问题我想大家都有答案,编程思想就是一个程序员的灵魂...
继续阅读 »

当然要想成为高级工程师或者架构师,光看书是不行的,书本上来的东西都是工具型编程的体现,何为工具型编程呢?


就是说可以依据书本、网络等渠道就能完成的编程就是工具型编程,那怎么解决呢?


为什么要提升编程思想



这个问题我想大家都有答案,编程思想就是一个程序员的灵魂,没有灵魂的程序员,只配ctrl + C/V.



专业一点来讲,提升编程思想的重要性在于它能够帮助开发者更好地解决问题、提高效率、减少错误,并提高代码的可读性、可维护性和可扩展性,而这些点位就是成为一个高级Android工程师或者架构师必不可少的技能,也是每一个程序员应该具备的技能。在国外,很多面试更看重的是学习能力和编程思想,其实也是,一个10年的经验丰富的程序员学习一门新的语言或者技术如同探囊取物,对于一个公司、一个团队、一个业务成本来讲,有这样一个人是最经济的。更具体来讲:



  1. 解决问题能力:良好的编程思想能够帮助开发者更好地理解问题,设计出高效、可靠、可扩展的解决方案。

  2. 代码质量提升:优秀的编程思想可以帮助开发者写出易于阅读、易于维护的代码,并使其更加健壮、可靠、可测试。

  3. 工作效率提高:合理的编程思想可以使开发者更加高效地编写代码,并降低代码调试和修复的时间。

  4. 技术实力提升:良好的编程思想可以使开发者更加深入地理解编程语言和计算机科学原理,并在实践中掌握更多的技能和技巧。

  5. 职业发展:具备良好编程思想的开发者在技术水平和职业发展方面具有更好的竞争力和前景。


良好的编程思想可以帮助开发者更好地解决问题、提高效率、提高代码质量和可维护性,并在职业发展中具有更好的前景和竞争力,这也就成了了中、高、架构等分级程序员的区别之分。


如何提升自己的编程思想



  1. 练习算法和数据结构:熟悉算法和数据结构可以帮助你更好地理解和解决问题,优化你的代码并提高你的代码质量。

  2. 阅读源代码:阅读其他优秀项目的源代码可以帮助你学习其他开发人员的编程思想,理解他们是如何解决问题的,进而提高自己的编程思维。

  3. 学习设计模式:设计模式是一种常用的编程思想,它可以帮助你更好地组织你的代码,提高代码的可维护性和可扩展性。

  4. 参与开源项目:参与开源项目可以帮助你学习其他开发人员的编程思想,理解他们是如何解决问题的,同时也可以帮助你获取更多的开发经验和知识。

  5. 持续学习:跟上Android开发的最新技术和趋势可以让你更好地了解开发环境和市场需求,并提升自己的编程思想。

  6. 经常review自己的代码:经常review自己的代码可以帮助你发现自己代码中的问题并及时改进,同时也可以帮助你学习其他开发人员的编程思想。


接下来,我们对这些步骤的项目进行分析和说明,


练习算法和数据结构



熟悉算法和数据结构可以帮助你更好地理解和解决问题,优化你的代码并提高你的代码质量, Android 这种移动平台,性能要求是非常高的,但是他的机型众多,系统不一,所以我们应该从编程角度就减少不必要的麻烦。怎么练习呢?




  1. 选择适合自己的算法练习平台:例如LeetCode、HackerRank、Codeforces等。这些平台都提供了大量的算法题目,可以帮助你提高算法水平。

  2. 学习基础算法:如排序、查找、树、图等算法,这些算法是其他算法的基础,掌握好基础算法对于提高算法能力非常有帮助。

  3. 练习算法的具体类型:例如贪心算法、动态规划、分治算法等,这些算法在实际开发中非常常见,掌握好这些算法可以让你更好地解决实际问题。

  4. 尝试实现算法:通过手写实现一些经典算法,你可以更好地理解算法的思想和实现方式,并加深对算法的理解。

  5. 参与算法竞赛:参与算法竞赛可以帮助你提高算法能力,同时也可以认识到其他优秀的算法工程师。


很多人在开发过程中公司是不会要求有算法参与的,特别是在Android端,也很少有人在开发中精心设计一款算法用于Android业务,Android的数据量级都在可控范围内,但是优秀的程序员不是应公司要求而编程的,我们因该在面对数据的时候,自然而然的想到算法,想到时间复杂度、空间复杂度的问题算法-时间复杂度 这是之前我在公司分享过的一篇文章,大家可以参考一下,基本涵盖了时间复杂度及在不同场景中的计算方式以及在特殊场景下的计算概念。


举几个例子,怎么将算法运用到平时的开发中呢?



  1. 优化算法复杂度:在实际开发中,我们常常需要处理大量数据,如果算法复杂度高,就容易导致程序运行缓慢。因此,优化算法复杂度是非常重要的。比如,在ListView或RecyclerView中使用二分查找算法可以快速查找到指定位置的数据。

  2. 应用动态规划算法:动态规划算法可以用于解决一些经典问题,例如最长公共子序列、背包问题等。在实际开发中,我们也可以应用动态规划算法解决一些问题,例如路径规划、字符串匹配等。

  3. 应用贪心算法:贪心算法是一种可以获得近似最优解的算法,可以用于一些优化问题。在Android开发中,例如布局优化、图片压缩等方面,也可以应用贪心算法来达到优化的效果。

  4. 应用其他常用算法:除了上述算法外,其他常用算法也可以应用于Android开发中,例如图像处理算法、机器学习算法等。对于一些比较复杂的问题,我们也可以引入其他算法来解决。


反正就是学之前要理解对应算法的大致用途,在类似场景中,直接尝试搬套,先在伪代码中演算其结果,结果正趋向时果断使用。


阅读源码



阅读其他优秀项目的源代码可以帮助你学习其他开发人员的编程思想,理解他们是如何解决问题的,进而提高自己的编程思维



前边不是说了,工具型编程可以不用看吗,是的,但是阅读源码不是查看工具,而是提升你的编程思想,借鉴思想是人类进化的最主要体现之一,多个思想的碰撞也能造就更成功的事件。那怎么阅读源码呢?这个每个人都有自己的方法,阅读后要善于变通,运用到自己的项目中,我是这么做的。



  1. 选择合适的开源项目:选择一个合适的开源项目非常重要。你可以选择一些知名度比较高的项目,例如Retrofit、OkHttp、Glide等,这些项目通常质量比较高,也有一定的文档和教程。

  2. 确定目标和问题:在阅读源码前,你需要明确自己的目标和问题。例如,你想了解某个库的实现原理,或者你想解决一个具体的问题。

  3. 仔细阅读源码:在阅读源码时,需要仔细阅读每一个类、方法、变量的注释,了解每一个细节。同时也需要了解项目的整体结构和运行流程。

  4. 了解技术背景和思路:在阅读源码时,你需要了解作者的技术背景和思路,了解为什么选择了某种实现方式,这样可以更好地理解代码。

  5. 实践运用:通过阅读源码,你可以学到许多好的编程思想和技巧,你需要将这些思想和技巧运用到自己的开发中,并且尝试创新,将这些思想和技巧进一步发扬光大。


阅读源码需要持之以恒,需要不断地实践和思考,才能真正学习到他人的编程思想,并将其运用到自己的开发中。


学习设计模式



设计模式本就是编程思想的总结,是先辈们的经验绘制的利刃,它可以帮助你更好地组织你的代码,提高代码的可维护性和可扩展性。




  1. 学习设计模式的基本概念:学习设计模式前,需要了解面向对象编程的基本概念,例如继承、多态、接口等。同时也需要掌握一些基本的设计原则,例如单一职责原则、开闭原则等。

  2. 学习设计模式的分类和应用场景:学习设计模式时,需要了解每个设计模式的分类和应用场景,例如创建型模式、结构型模式、行为型模式等。你需要了解每个模式的特点,以及何时应该选择使用它们。

  3. 练习设计模式的实现:练习实现设计模式是学习设计模式的关键。你可以使用一些例子,例如写一个简单的计算器、写一个文件读写程序等,通过练习来加深对设计模式的理解。

  4. 将设计模式应用到实际项目中:将设计模式应用到实际项目中是学习设计模式的最终目标。你需要从项目需求出发,结合实际场景选择合适的设计模式。举例来说,下面是一些在Android开发中常用的设计模式:

    • 单例模式:用于创建全局唯一的实例对象,例如Application类和数据库操作类等。

    • 适配器模式:用于将一个类的接口转换成客户端期望的另一个接口,例如ListView的Adapter。

    • 工厂模式:用于创建对象,例如Glide中的RequestManager和RequestBuilder等。

    • 观察者模式:用于实现事件机制,例如Android中的广播机制、LiveData等。




学习设计模式需要不断练习和思考,通过不断地练习和实践,才能真正将设计模式灵活运用到自己的项目中。


参与开源或者尝试商业SDK开发



参与开源,很多同学是没有时间的,并且国内缺少很多开发团队项目,都是以公司或者团队模式开源的,个人想在直接参与比较困难,所以有条件的同学可以参与商业SDK的开发,
商业SDK比较特殊的点在于受众不同,但是他所涉及的编程思想较为复杂,会涉及到很多设计模式和架构模式。



比如,Android商业SDK开发涉及到很多方面,下面列举一些常见的考虑点以及经常使用的架构和设计模式:



  1. 安全性:SDK需要考虑用户隐私保护和数据安全,确保不会泄露敏感信息。

  2. 稳定性:SDK需要保证在不同的环境下运行稳定,不会因为异常情况而崩溃。

  3. 可扩展性:SDK需要考虑未来的扩展和升级,能够方便地添加新的功能和支持更多的设备和系统版本。

  4. 性能:SDK需要保证在各种设备和网络条件下,响应速度和性能都有足够的表现。

  5. 兼容性:SDK需要考虑在不同版本的Android系统和各种厂商的设备上,都能够正常运行。


经常用到的架构和设计模式包括:



  1. MVVM架构:MVVM是Model-View-ViewModel的简称,通过将视图、模型和视图模型分离,可以实现更好的代码组织和更容易的测试。

  2. 单例模式:单例模式是一种创建全局唯一对象的模式,在SDK中常用于创建全局的配置、管理器等。

  3. 工厂模式:工厂模式是一种创建对象的模式,SDK中常用于创建和管理复杂的对象。

  4. 观察者模式:观察者模式是一种事件机制,SDK中常用于通知应用程序有新的数据或事件到达。

  5. 适配器模式:适配器模式用于将一个接口转换成另一个接口,SDK中常用于将SDK提供的接口适配成应用程序需要的接口。

  6. 策略模式:策略模式是一种动态地改变对象的行为的模式,SDK中常用于在运行时选择不同的算法实现。


Android商业SDK开发需要综合考虑多个方面,选择适合的架构和设计模式能够提高代码质量、开发效率和维护性。


了解市场、了解业务,不要埋头敲代码



掌握最新的市场需求,比如网络框架的发展历程,从开始的HttpURLConnection的自己封装使用,到okhttp,再到retrofit, 再后来的结构协程、Flow等等,其实核心没有变化就是网络请求,但是,从高内聚到,逐层解耦,变的是其编程的思想。



CodeReview



可以参考该文章,此文章描述了CodeReview 的流程和方法,值得借鉴,CodeReview 是一个天然的提升自己业务需求的过程,
zhuanlan.zhihu.com/p/604492247



经常写开发文档



设计和编写开发文档是一个很重要的工作,它不仅能够提升自己的编程思想,也能够帮助团队提高协作效率和减少沟通成本.



如果要求你在开发一个需求前对着墙或者对着人讲一遍开发思路,你可能讲不出来,也不好意思,且没有留存,开发文档可以满足你,当你写开发文档时,你记录了你的对整个需求的开发,以及你编程的功底,日益累积后,你的思想自然会水涨船高,因为你写开发文档的过程就是在锻炼自己,比如我在前公司开发国际化适配时写的文档(当然只是我的粗鄙想法国际化ICU4J 适配及SDK设计,我会先分析问题,为什么?然后设计,并且会思考可能遇到的问题,也一并解决了。时间长了,设计模式、思想也会得到提升。


当然,也要分场景去设计,按需求去设计,可以采纳以下建议:
设计和编写开发文档是一个很重要的工作,它不仅能够提升自己的编程思想,也能够帮助团队提高协作效率和减少沟通成本。下面是一些关于如何设计一份好的开发文档的建议:



  1. 明确文档的目标和受众:在编写文档之前,需要明确文档的目标和受众,确定文档需要包含的内容和写作风格

  2. 使用清晰的语言和示例:使用简洁、清晰的语言描述问题,使用示例代码和截图帮助读者理解问题。

  3. 分层次组织文档:文档应该按照逻辑和功能分层次组织,每一层都有明确的目标和内容。

  4. 使用图表和图形化工具:图表和图形化工具能够有效地展示复杂的概念和数据,帮助读者更好地理解文档内容。

  5. 定期更新和维护文档:开发文档需要定期更新和维护,以反映最新的代码和功能。


通过设计一份好的开发文档,可以提升自己的编程思想,使得代码更加清晰和易于维护,同时也能够提高团队的协作效率和代码质量。


向上有组织的反馈



经常向领导有组织的汇报开发进度、问题、结果,不仅可以提升编程思想,还能够提高自己的工作效率和沟通能力



首先,向领导汇报开发进度、问题和结果,可以让自己更加清晰地了解项目的进展情况和任务的优先级,帮助自己更好地掌控项目进度和管理时间。


其次,通过向领导汇报问题,可以促使自己更加深入地了解问题的本质和解决方案,同时也能够得到领导的反馈和指导,帮助自己更快地解决问题。


最后,向领导汇报开发结果,可以帮助自己更好地总结经验和教训,促进自己的成长和提高编程思想。同时,也能够让领导更清晰地了解自己的工作成果,提高领导对自己的认可和评价。


向领导有组织地汇报开发进度、问题和结果,不仅能够提升编程思想,还能够提高工作效率和沟通能力,促进自己的成长和发展。


总结



  1. 编程思想的提升



  • 学习数据结构和算法,尤其是常见的算法类型和实际应用

  • 阅读优秀开源代码,理解代码架构和设计思想,学习开发最佳实践

  • 学习设计模式,尤其是常见的设计模式和应用场景



  1. 实际项目开发中的应用



  • 通过代码重构,优化代码质量和可维护性

  • 运用算法解决实际问题,例如性能优化、数据处理、机器学习等

  • 运用设计模式解决实际问题,例如代码复用、扩展性、灵活性等



  1. 沟通与协作能力的提高



  • 与团队成员保持良好的沟通,及时反馈问题和进展情况

  • 向领导有组织地汇报开发进度、问题和结果,以提高工作效率和沟通能力

  • 参加技术社区活动,交流分享经验和知识,提高团队的技术实力和协作能力


以上是这些方面的核心点,当然每个方面都有很多细节需要关注和完善,需要持续学习和实践。


附件



以下是我之前为项目解决老项目的图片框架问题而设计的文档,因名称原因只能图片展示



首先,交代了背景,存在的问题


image.png


针对问题,提出设计思想


image.png


开始设计,从物理结构到架构


image.png


image.png


作者:狼窝山下的青年
来源:juejin.cn/post/7200944831114264637
收起阅读 »

Flutter webview_flutter滑动监听

前言 当需要使用webview时,常用的插件有webview_flutter、flutter_webview_plugin、flutter_inappwebview等。 项目中已经使用的是官方的webview_flutter 。 问题: 总的来说webview...
继续阅读 »

前言


当需要使用webview时,常用的插件有webview_flutterflutter_webview_pluginflutter_inappwebview等。


项目中已经使用的是官方的webview_flutter


问题:


总的来说webview_flutter可以满足大部分的webview相关需求,直到有一天产品小可爱说网页在顶部时,顶部标题栏背景为白色,之后网页滑动到一定距离的时候需要变成透明。我一听,so easy啊,WebViewController里有相关的方法监听:


image.png


我只需要加个Listener手势监听,在move里判断getScrollY()的距离就OK了。


Listener(
child: WebView(initialUrl: "http://www.baidu.com"),
onPointerMove: (PointerMoveEvent event) {
webViewController?.getScrollY().then((value) {
if (value > 200) {
//标题栏背景变白色
} else {
////标题栏背景变透明
}
});
},
);

然而....


image.png


手势滑动是有惯性的,当快速滑动的时候,手指离开屏幕,会因为惯性继续滑动一段距离。而由于我们监听的是手势操作,当手指离开屏幕,手势监听也就从onPointerMove走到了onPointerUp,也就没办法再监听webViewController?.getScrollY(),惯性滚动的距离就没有监听到,那此时所展现的效果自然就不理想了:


快速下滑屏幕的时候,假设手指离开屏幕的时候监听到getScrollY()=500,但由于惯性网页继续滚动,直到滚回网页顶部。但由于手指离开了屏幕没有继续监听getScrollY(),getScrollY()的最后取值就是500,可此时网页已经惯性滚回到顶部了,此时标题栏依然是白色,而不是变回透明。


image.png


解决


遇到问题,咱就解决问题。
反复翻看了webview_flutter的代码,确实没有这个监听,然后联想到即便是在做Android原生开发的时候,webview也是没有提供公开的方法或设置让我们对网页的滚动进行监听的,如果要监听那就需要在原生webview的onScrollChanged增加监听。


修改FlutterWebViewClient,增加方法


void getOffsetY(int offsetY) {
Map<String, Object> args = new HashMap<>();
args.put("offsetY", offsetY);
methodChannel.invokeMethod("getOffsetY", args);
}

image.png


修改FlutterWebView


@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
@SuppressWarnings("unchecked")
FlutterWebView(
final Context context,
BinaryMessenger messenger,
int id,
Map<String, Object> params,
View containerView) {

///增加监听
if (webView instanceof CustomWebView){
((CustomWebView)webView).setOnScrollChangedCallback(new CustomWebView.OnScrollChangedCallback() {
@Override
public void onScroll(int dx, int dy) {
flutterWebViewClient.getOffsetY(dy);
Log.d("onScroll",dy+"");
}
});
}

image.png


platform_interface.dart里修改WebViewPlatformCallbacksHandler


增加供外部调用的getOffsetY()


void getOffsetY(int offsetY);

image.png


webview_method_channel.dart修改MethodChannelWebViewPlatform


_onMethodCall里新增getOffsetY类型


case 'getOffsetY':
_platformCallbacksHandler.getOffsetY(call.arguments['offsetY']);
return null;

webview_flutter.dart修改WebView


新增getOffsetY


image.png


_PlatformCallbacksHandler里新增


@override
void getOffsetY(int offsetY){
if (_widget.getOffsetY != null) {
_widget.getOffsetY!(offsetY);
}
}

image.png


至此,就成功增加了webview的滑动监听


image.png


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

我发现了 Android 指纹认证 Api 内存泄漏

我发现了 Android 指纹认证 Api 内存泄漏 目前很多市面上的手机基本都有指纹登陆功能。Google 也提供了调用相关功能 API,安全类的App 也基本都在使用。接下来就一起捋一捋今天的主角 BiometricPrompt 先说问题,使用Biome...
继续阅读 »

我发现了 Android 指纹认证 Api 内存泄漏


目前很多市面上的手机基本都有指纹登陆功能。Google 也提供了调用相关功能 API,安全类的App 也基本都在使用。接下来就一起捋一捋今天的主角 BiometricPrompt


先说问题,使用BiometricPrompt 会造成内存泄漏,目前该问题试了 Android 11 到 13 都发生,而且没有什么好的办法。目前想到的最好的方法是漏的少一点。当然谁有好的办法欢迎留言。


问题再现


先看动画


在这里插入图片描述


动画中操作如下



  1. MainAcitivity 跳转到 SecondActivity

  2. SecondActivity 调用 BiometricPrompt 三次

  3. 从SecondActivity 返回到 MainAcitivity


以下是使用 BiometricPrompt 的代码


public fun showBiometricPromptDialog() {
val keyguardManager = getSystemService(
Context.KEYGUARD_SERVICE
) as KeyguardManager;

if (keyguardManager.isKeyguardSecure) {
var biometricPromptBuild = BiometricPrompt.Builder(this).apply {// this is SecondActivity
setTitle("verify")
setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL or BiometricManager.Authenticators.BIOMETRIC_WEAK)
}
val biometricPromp = biometricPromptBuild.build()
biometricPromp.authenticate(CancellationSignal(), mExecutor, object :
BiometricPrompt.AuthenticationCallback() {

})
}
else {
Log.d("TAG", "showLockScreen: isKeyguardSecure is false");
}
}

以上逻辑 biometricPromp 是局部变量,应该没有问题才对。


内存泄漏如下


在这里插入图片描述
可以看到每启动一次生物认证,创建的 BiometricPrompt 都不会被回收。


规避方案:


修改方案也简单


方案一:



  1. biometricPromp 改为全局变量。

  2. this 改为 applicationContext


方案一存在的问题,SecondActivity 可能频繁创建,所以 biometricPromp 还会存在多个实例。


方案二(目前想到的最优方案):



  1. biometricPromp 改为单例

  2. this 改为 applicationContext


修改后,App memory 中只存在一个 biometricPromp ,且没有 Activity 被泄漏。


想到这里,应该会觉得奇怪,biometricPromp 为什么不会被回收?提供的 API 都看过了,没有发现什么方法可以解决这个问题。直觉告诉我这个可能是系统问题,下来分析下BiometricPrompt 吧。


BiometricPrompt 源码分析


在这里插入图片描述


App 相关信息通过 BiometricPrompt 传递到 System 进程,System 进程再通知 SystemUI 显示认证界面。


App 信息传递到 System 进程,应该会使用 Binder。这个查找 BiometricPrompt 使用哪些 Binder。


private final IBiometricServiceReceiver mBiometricServiceReceiver =
new IBiometricServiceReceiver.Stub() {

......
}

源码中发现 IBiometricServiceReceiver 比较可疑,IBiometricServiceReceiver 是匿名内部类,内部是持有 BiometricPrompt 对象的引用。


接下来看下 System Server 进程信息(注:系统是 UserDebug 的手机,才可以查看,买的手机版本是不支持的)


在这里插入图片描述



😂 App 使用优化后(方案二)App 只存在一个 IBiometricServiceReceiver ,而 system 进程中存在三个 IBiometricServiceReceiver 的 binder proxy。 每次启动 BiometricPrompt 都会创建一个。这个就不解释为什么会出现三个binder proxy,感兴趣可以看下面推荐的文章。GC root 是 AuthSession。

再看下 AuthSession 的实例数


在这里插入图片描述


果然 AuthSession 也存在三个。


在这里插入图片描述


这里有个知识点,binder 也是有生命周期的,三个 Proxy 这篇文章也是解释了的。有兴趣的可以了看下。


Binder | 对象的生命周期


一开始,我以为 AuthSession 没有被置空,看下代码,发现 AOSP 的代码,还是比较严谨的,有置空的操作。


细心的同学发现,上图中 AuthSession 没有被任何对象引用,AuthSession 就是 GC Root,哈哈哈。


问题解密


一个实例什么情况可以作为GC Root,有兴趣的同学,可以自行百度,这里就不卖关子了,直接说问题吧。


Binder.linkToDeath()


public void linkToDeath(@NonNull DeathRecipient recipient, int flags) {
}

需要传递 IBinder.DeathRecipient ,这个 DeathRecipient 会被作为 GC root。当调用 unlinkToDeath(@NonNull DeathRecipient recipient, int flags),GC root 才被收回。


AuthSession 初始化的时候,会调用 IBiometricServiceReceiver .linkToDeath。


public final class AuthSession implements IBinder.DeathRecipient {
AuthSession(@NonNull Context context,
......
@NonNull IBiometricServiceReceiver clientReceiver,
......
) {
Slog.d(TAG, "Creating AuthSession with: " + preAuthInfo);
......
try {
mClientReceiver.asBinder().linkToDeath(this, 0 /* flags */);//this 变成 GC root
} catch (RemoteException e) {
Slog.w(TAG, "Unable to link to death");
}

setSensorsToStateUnknown();
}
}

Jni 中 通过 env->NewGlobalRef(object),告诉虚拟机 AuthSession 是 GC Root。


core/jni/android_util_Binder.cpp

static void android_os_BinderProxy_linkToDeath(JNIEnv* env, jobject obj,
jobject recipient, jint flags)
// throws RemoteException
{
if (recipient == NULL) {
jniThrowNullPointerException(env, NULL);
return;
}

BinderProxyNativeData *nd = getBPNativeData(env, obj);
IBinder* target = nd->mObject.get();

LOGDEATH("linkToDeath: binder=%p recipient=%p\n", target, recipient);

if (!target->localBinder()) {
DeathRecipientList* list = nd->mOrgue.get();
sp<JavaDeathRecipient> jdr = new JavaDeathRecipient(env, recipient, list);//java 中 DeathRecipient 会被封装为 JavaDeathRecipient
status_t err = target->linkToDeath(jdr, NULL, flags);
if (err != NO_ERROR) {
// Failure adding the death recipient, so clear its reference
// now.
jdr->clearReference();
signalExceptionForError(env, obj, err, true /*canThrowRemoteException*/);
}
}
}

JavaDeathRecipient(JNIEnv* env, jobject object, const sp<DeathRecipientList>& list)
: mVM(jnienv_to_javavm(env)), mObject(env->NewGlobalRef(object)),// object -> DeathRecipient 变为 GC root
mObjectWeak(NULL), mList(list)
{
// These objects manage their own lifetimes so are responsible for final bookkeeping.
// The list holds a strong reference to this object.
LOGDEATH("Adding JDR %p to DRL %p", this, list.get());
list->add(this);

gNumDeathRefsCreated.fetch_add(1, std::memory_order_relaxed);
gcIfManyNewRefs(env);
}

unlinkToDeath 最终会在 Jni 中 通过 env->DeleteGlobalRef(mObject),告诉虚拟机 AuthSession 不是GC root。


virtual ~JavaDeathRecipient()
{
//ALOGI("Removing death ref: recipient=%p\n", mObject);
gNumDeathRefsDeleted.fetch_add(1, std::memory_order_relaxed);
JNIEnv* env = javavm_to_jnienv(mVM);
if (mObject != NULL) {
env->DeleteGlobalRef(mObject);// object -> DeathRecipient GC root 被撤销
} else {
env->DeleteWeakGlobalRef(mObjectWeak);
}
}

解决方式


AuthSession 置空的时候调用 IBiometricServiceReceiver 的 unlinkToDeath 方法。


总结


以上梳理的其实就是 Binder 的造成的内存泄漏。


问题严重性来看,也不算什么大问题,因为调用 BiometricPrompt 的进程被杀,system 进程相关实例也就回收释放了。一般 app 也不太可能出现,常驻进程,而且还频繁调用手机认证的。


这里主要介绍了一种容易被忽略的内存泄漏,Binder.linktoDeath()。
Google issuetracker


参考资料


Binder | 对象的生命周期


作者:Jingle_zhang
来源:juejin.cn/post/7202066794299129914
收起阅读 »

简单回顾位运算

前言 位运算其实是一个比较常用的操作,有的人可能说,并没有啊,我好像基本就没怎么用过位运算,但如果你经常看源码,你就会发现源码里面有很多位运算的操作,而且这些操作我个人觉得确实是有很意思,所以位运算很重要,平时用不多,也需要经常去回顾复习。 因为临时写的,有些...
继续阅读 »

前言


位运算其实是一个比较常用的操作,有的人可能说,并没有啊,我好像基本就没怎么用过位运算,但如果你经常看源码,你就会发现源码里面有很多位运算的操作,而且这些操作我个人觉得确实是有很意思,所以位运算很重要,平时用不多,也需要经常去回顾复习。


因为临时写的,有些源码的操作我不太记得是出自哪里了,如果以后碰到了,会在评论区进行补充。比如会有一些取反计算再取反回来的操作,比如左移和右移,我现在不太记得是出自哪里了,反正比较经典的。我的个人可能就记得用位运算来表示状态,因为我会经常用这个。


位运算基础


简单来回顾一下基础的计算,位运算会分为一元运算和二元运算。


一元有左移<<,右移>>,无符号右移动>>>和取反~


左移是是什么?比如 0010 左移动一位就变成 0100 (注意这里是二进制的表示),右移动就是 0100 变成 0010。 当然没这么简单啦,二进制也有表示正负值的标志位,右移之后,左边会补标志位的数。而无符号右移动是左边补0。也就是说正数的右移和无符号右移动的结果相同,而负数就不同了。这样说应该好理解吧?那思考一下为什么没有无符号左移


还是不好懂,没关系,我们讲慢些,假如8转成2进制是00001000

而-8就是先取反码,反码就是取反,所以8的反码是11110111,然后再用反码取补码,补码就是+1,所以这里得到补码1111000。即-8转成二进制是11111000

先看看我们对11111000取反码,得00000111,再取补码得00001000,看到从-8到8也是同样的操作。


然后我们看右移一位和无符号右移一位的效果。

-8的二进制11111000,右移一位是11111100,这是多少啊?我们可以用上面的计算看它的正数是多少,反码00000011,补码00000100,这是4吧,所以11111100是-4。

同理看无符号右移,得01111100,反码10000011,补码10000100,这是多少?2的7次方+4,所以是不同的。


取反就好理解了,取反就是逻辑非,比如0010取反就是1101


二元有与&&、或||、异或^


这些都好理解吧,与运算 1010 && 1001 = 1000 , 或运算 1010 || 1001 = 1010 , 异或 1010 ^ 1001 = 0011 ,这没什么好讲的,很基础。


位运算很简单?


一看,哎哟,真的简单,就这?1分钟学会了。学会?那会用吗?


没关系,我们来看一题算法:


一个整型数组里除了两个数字只出现一次,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。例如:


输入:


[1,4,1,6]

返回值:


[4,6]

明确说了,这题是用位运算,但是怎么做?


开发中位运算的使用


这个我因为临时写的,android源码里确实是有些比较经典的位运算使用场景,我现在不完全记得,只能先列举几个,其它的如果以后想起来,会在评论区中做补充。


用来表示状态

位运算可以用来表示状态,我之前也写过一篇文章 juejin.cn/post/715547… 差不多也是这个意思。


比如window的flags,看看它的定义


public static final int FLAG_LAYOUT_IN_SCREEN   = 0x00000100;
public static final int FLAG_LAYOUT_NO_LIMITS = 0x00000200;
public static final int FLAG_FULLSCREEN = 0x00000400;
public static final int FLAG_FORCE_NOT_FULLSCREEN = 0x00000800;
......

那他这样做有什么好处,他能一个变量表示多个维度的状态。比如FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_NO_LIMITS|FLAG_FULLSCREEN|FLAG_FORCE_NOT_FULLSCREEN就是 1111 (二进制表示)


如果你要判断这个window是不是同时设置了这4个flag,要怎么判断,就直接if(flags == 15)啊,多简单


但是如果你用多个变量存flag要怎么判断, if(isScreen && isNoLimits && usFullscreen && isForceNot),这样写就很难看,很不方便,我window的flag多着呢,难道你要排火车?


数组扩容

来看看ArrayList的扩容源码


private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

这里的int newCapacity = oldCapacity + (oldCapacity >> 1);就是阔人操作,这个右移是什么?看不懂也没关系,自己套一个数字进去算就知道了,是除2吧,那ArrayList的扩容就是旧容量的一半。


看着简单是吧?那有没有想过一个问题,他写这个代码,为什么不写oldCapacity/2,而写oldCapacity >> 1


那既然右移一位 >> 1 是除2,那左移一位 << 1 是什么操作呢?是什么计算呢?


总结


我这里确实是很久没有用位运算,所以需要复习一下。这个东西对开发来说很重要,比如你是开发应用层的,你觉得这个是底层用到的,你用不到,并不是这样。就拿那个表示状态的来说,自从我看到源码用这一招之后,只要有合适的场景,我也会这样用。


不管是做数学运算,还是逻辑运算,位运算都能适用,它是很简单就能学会,但是学会用,那就是另外一回事,当然不是说看完我这篇文章就开始瞎用,能在合适的场合去使用,那效果十分的好,用不上也没关系,至少要有个意识,这样不管在看源码还是其它时候,都是能帮到你的。


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

Flutter动态化调研实践

一,前言 1,什么是动态化? 目前移动端应用的版本更新, 最常见的方式是定期发版,无论是安卓还是iOS,都需要提交新的安装包到应用市场进行审核。审核通过后,用户在应用市场进行App的下载更新。 而动态化, 就是不依赖更新程序安装包, 就能动态实时更新页面的技术...
继续阅读 »

一,前言


1,什么是动态化?


目前移动端应用的版本更新, 最常见的方式是定期发版,无论是安卓还是iOS,都需要提交新的安装包到应用市场进行审核。审核通过后,用户在应用市场进行App的下载更新。


而动态化, 就是不依赖更新程序安装包, 就能动态实时更新页面的技术。


2,动态化的必要性


为什么需要动态化技术呢? 因为上述定期发版更新应用的方式存在一些问题,比如:



  1. 审核周期长, 且可能审核不通过。 周期长导致发版本不够灵活, 紧急的业务需求不能及时上线。

  2. 线上出现急需修复的bug时,需要较长修复周期,影响用户体验。

  3. 安装包过大, 动辄几十兆几百兆的应用升级可能会让用户比较抗拒。

  4. 即使上线了,也无法达到全部用户升级, 服务端存在兼容多版本App的问题。


面对这些问题,如果能实现app增量、无感知更新,实现功能同步。无论是对公司还是用户都是非常重要的需求,能实现app动态化更新就显得非常重要,能很好的解决以上问题:



  1. 随时实现功能升级,不存在应用市场长时间审核和拒绝上线问题,达到业务需求快速上线的目的。

  2. 线上bug可以实时修复,提高用户体验。

  3. 可以减小发版功能包体积,只需要替换新增功能即可。

  4. 功能保持一致,类似网页一样,发版后用户同步更新,不存在旧版本兼容问题。


接下来,我们就来分析一下,目前业内主要的Flutter动态化更新方式。


二,动态化方案调研


在Flutter实践层面,简单来说分为三个流派:




  • 方案一:JavaScript是最好的语言(🤣碰瓷PHP)
    主要思路:利用Flutter做渲染,开发使用js,逻辑层通过v8/jscore解释运行。代表框架是腾讯的MXFlutter。这个框架是开源的,大写的👍。




  • 方案三:布局,逻辑,一把梭


    主要思路:与方案一最主要的区别是,逻辑层也是使用dart,增加了一层语法解析和运行时。有一个代表,美团的MTFlutter,然而没有开源动向,无从考察更多。




  • 方案二:DSL + JS


    主要思路:基于模板实现动态化,主要布局层采用Dart转DSL的方式,逻辑层使用JS。代表框架是58同城开源的Fair




MXFlutter


项目简介



MXFlutter 是一套基于 JavaScript 的 Flutter 框架,可以用极其类似 Dart 的开发方式,通过编写 JavaScript 代码,来开发 Flutter 应用,或者使用 mxjsbuilder 编译器,把现有Flutter 工程编译为JS,运行在 mxflutter 之上。



核心思想



核心思路是把 Flutter 的渲染逻辑中的三棵树中的第一棵,放到 JavaScript 中生成。用 JavaScript 完整实现了 Flutter 控件层封装,可以使用 JavaScript,用极其类似 Dart 的开发方式,开发Flutter应用,利用JavaScript版的轻量级Flutter Runtime,生成UI描述,传递给Dart层的UI引擎,UI引擎把UI描述生产真正的 Flutter 控件。



MxFlutter 目前已经停止维护,具体请看MXFlutter
MxFlutter通过JavaScript编写Dart,加载线上js文件,通过引擎在运行时转化并显示,从而达到动态化效果。 官方在0.7.0版本开始接入TypeScript,引入npm生态,优化了js开发的成本,向前端生态进一步靠拢。
很遗憾,在对比各大厂的方案时,发现MxFlutter的性价比极低,学习成本也高,而且又抛弃Dart生态。开发及维护成本都很高。


MTFlutter


项目简介



美团的MTFlutter团队flap项目采用的静态生产DSL方案,通过对Dart语言注解,保证平台一致性。实现了动态下发与解释的逻辑页面一体化的 Flutter 动态化方案。Flap 的出现让 Flutter 动态化和包大小这两个短板得到了一定程度的弥补,促进了 Flutter 生态的发展。



核心思想



通过静态生产 DSL+Runtime 解释运行的思路,实现了动态下发与解释的逻辑页面一体化的 Flutter 动态化方案,建设了一套 Flap 生态体系,涵盖了开发、发布、测试、运维各阶段。



布局和逻辑层都使用Dart, 增加了一层语法解析和运行时。然而没有开源动向,无从考察更多。


Fair


项目简介



Fair是为Flutter设计的动态化框架,通过Fair Compiler工具对原生Dart源文件的自动转化,使项目获得动态更新Widget Tree和State的能力。


创建Fair的目标是支持不发版(Android、iOS、Web)的情况下,通过业务bundle和JS下发实现更新,方式类似于React Native。与Flutter Fair集成后,您可以快速发布新的页面,而无需等待应用的下一个发布日期。Fair提供了标准的Widget,它可以被用作一个新的动态页面或作为现有Flutter页面的一部分,诸如运营位的排版/样式修改,整页面替换,局部替换等都可以使用。



核心思想



Fair是58自研的的动态化框架,通过Fair Compiler工具对原生Dart源文件的自动转化,使项目获得动态更新Widget Tree和State的能力。



pic_d3WbXUd1d1V9d1WcXU37U7U75aXdd17b


三,方案对比


经过上述三个方案的调研,我们来大概对比一下上述框架


方案开源方核心思想优点缺点
MXFlutter腾讯用js编写Dart,动态拉取js脚本目前相对最完整的Flutter使用JS开发方案采用js方式编写Dart,维护困难
MTFlutter美团布局,逻辑都使用Dart,增加语法解析和运行时支持布局动态化和逻辑动态化未开源
Fair58通过bundle和js实现热更新支持布局动态化和逻辑动态化开源社区活跃, 开发工具丰富部分语法不支持

可以看到, MXFlutter需要使用js写Dart, 官方已经停止更新,而这种方式我们不能接受, MTFlutter目前未开源,无从继续研究。 接下来着重看一下 Fair


四,Fair接入过程


1,添加依赖


推荐使用pub形式引入


# add Fair dependency
dependencies:
fair: 2.7.0

# add compiler dependency
dev_dependencies:
build_runner: ^2.0.0
fair_compiler: ^1.2.0

# switch "fair_version" according to the local Flutter SDK version
dependency_overrides:
fair_version: 3.0.0

Flutter版本切换


通过切换 flutter_version 版本进行版本兼容。例如,将本机切换为 flutter 2.0.6 后,Fair 需要同步切换


# switch to another stable flutter version
dependency_overrides:
fair_version: 2.0.6

2,使用 Fair


在App中接入Fair步骤如下:


将 FairApp 添加为需要动态化部分的顶级节点

常见做法是作为 App 的根节点,如果不是全局采用也可以作为子页面的根节点


void main() {
WidgetsFlutterBinding.ensureInitialized();

FairApp.runApplication(
_getApp(),
plugins: {
},
);
}

dynamic _getApp() => FairApp(
modules: {
},
delegate: {
},
child: MaterialApp(
home: FairWidget(
name: 'DynamicWidget',
path: 'assets/bundle/lib_src_page_dynamic_widget.fair.json',
data: {"fairProps": json.encode({})}),
),
);

添加动态组件

每一个动态组件由一个FairWidget表示。


FairWidget(
name: 'DynamicWidget',
path: 'assets/bundle/lib_src_page_dynamic_widget.fair.json',
data: {"fairProps": json.encode({})}),

根据不同场景诉求,FairWidget可以混合和使用



  1. 可以作为不同组件混合使用

  2. 一般作为一个全屏页面

  3. 支持嵌套使用,即可以局部嵌套在普通Widget下,也可以嵌套在另一个FairWidget下


五,Fair接入体验


1,fork,下载工程


将官方Github工程fork到自己仓库后, 下载工程。使用官方提供的 test_case/best_ui_templates工程体验fair的体验。


2, 执行 pub get


在 best_ui_templates工程中,执行 pub get命令获取依赖。


3,开发业务


接下来正式开始开发流程。 把一个页面改写为 用Fair 编写:



  1. 创建需要动态化的 componnet, 并添加 @FairPatch() 注解。添加上注解后,在Fair生成产物时,会把此Component build生成 FairWidget加载的产物。


image-20221116173528472


2, 执行 Fair工具链插件的命令生成产物, 如图:


<u>image-20221116173837910</u>


3, 最终生成的产物,拷贝到 assets/bundle目录下(配置config.json后,会自动拷贝)


<u><u>image-20221116182132104</u></u>


4, 看效果, 下图为使用 Fair 改造后的页面:



Screenshot_2022_1116_172859
Screenshot_2022_1116_192940

六,Fair优势


1,社区活跃度高


官方对Fair维护力度大,版本更新较快,问题修复及时,活跃的开发者社区氛围。


使得开发者在开发Fair过程中遇到的问题, 能够及时反馈给官方, 并能得到快速的帮助和解决。


2, 一份代码,灵活使用


Fair的区别于MTFlutter和MXFlutter这2种动态化方案,Fair能让同一份代码在Flutter原生和动态之间随意切换。在开发跟版本需求时,使用原生代码发布,以此持续保持Flutter的性能优势;而在热更新场景可以通过下发动态文件来达到动态更新Widget的目的。使用方式更加灵活。


3,配套开发工具丰富


Faircli配套工具链

官方为了让开发者快速上手,降低接入门槛, 解决在接入过程中的痛点。 Fair团队开发了Faircli配套工具链,主要包含三个部分:



  • 工程创建:快速搭建Fair载体工程及动态化工程。

  • 模板代码:提供页面及组件模板。

  • 本地热更新:线下开发使用,实现开发阶段快速预览Fair动态化功能。


在安装了工具链提供的dart命令行工具及AS插件后, 通过创建模板, 构建产物, 本地启服务,体验热更新功能,开发者可以轻松接入并体验Fair。


Fair语法检测插件

官方为了让开发者在Fair开发过程中,出现不正确或者不支持的语法问题。 开发了配套插件去提示用户使用Fair语法糖。


查看以下示例:


1,build方法下if的代码检测,及提示引导信息


44b58320-e608-420f-854f-799b5bf03cf5image


2,点击more action 或者 AS代码提示快捷键


41094a86-2aea-43e6-b7f0-69aef1c653c0image


3,根据提示点击替换


image.png


通过插件,在编写fair过程中,可以快速识别并解决不支持的语法问题。 提高开发Fair效率。


Fair Web代码编辑器

Fair其中一个方向是在线动态化平台,即在网页中编辑dart代码,在线预览Flutter效果和Fair动态化效果,并且发布Fair动态化产物。


通过在Fair Web代码编辑器,开发者可以在没有复杂的IDE配置的情况下,在网页端开发Fair并预览。 这无疑是降低了接入成本, 为开发者可以快速体验Fair提供了非常便捷的方式。


七,总结


通过近期对各大互联网公司在Flutter动态化方向上的探究方案。 发现这些方案都还没有达到成熟阶段,想在实际业务上落地, 还得看各团队后期的维护力度和开发投入程度。


MXFlutter使用js编写Dart的方式, 抛弃了原本Flutter的开发模式, 导致开发成本大,以及后续维护成本也大,官方已停止维护。


MTFlutter采用布局,逻辑都是使用Dart, 通过静态生产 DSL+Runtime 解释运行的思路,解决布局和逻辑的动态化,然而并没有开源计划,无从深入研究。


Fair通过Fair Compiler工具对原生Dart源文件的自动转化,使项目获得动态更新Widget Tree和State的能力。目前官方维护力度较大, 社区活跃,并且有比较全面的Fair生态工具。 期待 Fair 团队可以解决在开发Fair过程中一些体验问题,如语法支持不全等, 让Fair成为真正能够让开发者可以快速接入,能够达到和正常开发Flutter接近的体验。 为广大Flutter开发人员解决动态化的痛点。


支持我们


欢迎大家使用 Fair,也欢迎大家为我们点亮star

Github地址:github.com/wuba/fair

Fair官网:fair.58.com/


欢迎贡献


通过Issue提交问题,贡献代码请提交Pull Request,管理员将对代码进行审核。


作者:王猛猛
来源:juejin.cn/post/7174978087879671865
收起阅读 »

全网最优雅安卓控件可见性检测

引子 view.setOnClickListener { // 当控件被点击时触发的逻辑 } 正是因为 View 对控件点击采用了策略模式,才使得监听任何控件的点击事件变得易如反掌。 我有一个愿望。。。 如果 View 能有一个可见性监听该多好啊! view...
继续阅读 »

引子


view.setOnClickListener { // 当控件被点击时触发的逻辑 }

正是因为 View 对控件点击采用了策略模式,才使得监听任何控件的点击事件变得易如反掌。


我有一个愿望。。。


如果 View 能有一个可见性监听该多好啊!


view.setOnVisibilityChangeListener { isVisible: Boolean ->   }

系统并未提供这个方法。。。


但业务上有可见性监听的需要,比如曝光埋点。当某控件可见时,上报XXX。


数据分析同学经常抱怨曝光数据不准确,有的场景曝光多报了,有的场景曝光少报了。。。


开发同学看到曝光埋点也很头痛,不同场景的曝光检测有不同的方法,缺乏统一的可见性检测入口,存在一定重复开发。


本文就试图为单个控件以及列表项的可见性提供统一的检测入口。


控件的可见性受到诸多因素的影响,下面是影响控件可见性的十大因素:



  1. 手机电源开关

  2. Home 键

  3. 动态替换的 Fragment 遮挡了原有控件

  4. ScrollView, NestedScrollView 的滚动

  5. ViewPager, ViewPager2 的滚动

  6. RecyclerView 的滚动

  7. 被 Dialog 遮挡

  8. Activity 切换

  9. 同一 Activity 中 Fragment 的切换

  10. 手动调用 View.setVisibility(View.GONE)

  11. 被输入法遮盖


能否把这所有的情况都通过一个回调方法表达?目标是通过一个 View 的扩展方法完成上述所有情况的检测,并将可见性回调给上层,形如:


fun View.onVisibilityChange(block: (view: View, isVisible: Boolean) -> Unit) {}

若能实现就极大化简了上层可见性检测的复杂度,只需要如下代码就能实现任意控件的曝光上报埋点:


view.onVisibilityChange { view, isVisible ->
if(isVisible) { // 曝光埋点 }
else {}
}

控件全局可见性检测


可见性检测分为两步:



  1. 捕获时机:调用检测算法检测控件可见性的时机。

  2. 检测算法:描述如何检测控件是否对用户可见。


拿“手动调用 View.setVisibility(View.GONE)”举例,得先捕获 View Visibility 发生变化的时机,并在此刻检测控件的可见性。


下面是View.setVisibility()的源码:


// android.view.View.java
public void setVisibility(@Visibility int visibility) {
setFlags(visibility, VISIBILITY_MASK);
}

系统并未在该方法中提供类似于回调的接口,即一个 View 的实例无法通过回调的方式捕获到 visibility 变化的时机。


难道通过自定义 View,然后重写 setVisibility() 方法?


这个做法接入成本太高且不具备通用性。


除了“手动调用 View.setVisibility(View.GONE)”,剩下的影响可见性的因素大多都可找到对应回调。难道得在fun View.onVisibilityChange()中对每个因素逐个添加回调吗?


这样实现太过复杂了,而且也不具备通用性,假设有例外情况,fun View.onVisibilityChange()的实现就得修改。


上面列出的十种影响控件可见性的因素都是现象,不同的现象背后可能对应相同的本质。


经过深挖,上述现象的本质可被收敛为下面四个:



  1. 控件全局重绘

  2. 控件全局滚动

  3. 控件全局焦点变化

  4. 容器控件新增子控件


下面就针对这四个本质编程。


捕获全局重绘时机


系统提供了ViewTreeObserver


public final class ViewTreeObserver {
public void addOnGlobalLayoutListener(OnGlobalLayoutListener listener) {
checkIsAlive();
if (mOnGlobalLayoutListeners == null) {
mOnGlobalLayoutListeners = new CopyOnWriteArray();
}
mOnGlobalLayoutListeners.add(listener);
}
}

ViewTreeObserver 是一个全局的 View 树变更观察者,它提供了一系列全局的监听器,全局重绘即是其中OnGlobalLayoutListener


public interface OnGlobalLayoutListener {
public void onGlobalLayout();
}

当 View 树发生变化需要重绘的时候,就会触发该回调。


调用 View.setVisibility(View.GONE) 之所以能将控件隐藏,正是因为整个 View 树触发了一次重绘。(任何一次微小的重绘都是从 View 树的树根自顶向下的遍历并触发每一个控件的重绘,不需要重绘的控件会跳过,关于 Adroid 绘制机制的分析可以点击Android自定义控件 | View绘制原理(画多大?)


在可见性检测扩展方法中捕获第一个时机:


fun View.onVisibilityChange(block: (view: View, isVisible: Boolean) -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener {}
}

其中viewTreeObserver是 View 的方法:


// android.view.View.java
public ViewTreeObserver getViewTreeObserver() {
if (mAttachInfo != null) {
return mAttachInfo.mTreeObserver;
}
if (mFloatingTreeObserver == null) {
mFloatingTreeObserver = new ViewTreeObserver(mContext);
}
return mFloatingTreeObserver;
}

getViewTreeObserver() 用于返回当前 View 所在 View 树的观察者。


全局重绘其实覆盖了上述的两个场景:



  1. 同一 Activity 中 Fragment 的切换

  2. 手动调用 View.setVisibility(View.GONE)

  3. 被输入法覆盖


这两个场景都会发生 View 树的重绘。


捕获全局滚动时机



  1. ScrollView, NestedScrollView 的滚动

  2. ViewPager, ViewPager2 的滚动

  3. RecyclerView 的滚动


上述三个时机的共同特点是“发生了滚动”。


每个可滚动的容器控件都提供了各自滚动的监听


// android.view.ScrollView.java
public interface OnScrollChangeListener {
void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY);
}

// androidx.viewpager2.widget.ViewPager2.java
public abstract static class OnPageChangeCallback {
public void onPageScrolled(int position, float positionOffset, @Px int positionOffsetPixels) {}
public void onPageSelected(int position) {}
public void onPageScrollStateChanged(@ScrollState int state) {}
}

// androidx.recyclerview.widget.RecyclerView.java
public abstract static class OnScrollListener {
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {}
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {}
}

难道要针对不同的滚动控件设置不同的滚动监听器?


这样可见性检测就和控件耦合了,不具有通用性,也愧对View.onVisibilityChange()这个名字。


还好又在ViewTreeObserver中找到了全局的滚动监听:


public final class ViewTreeObserver {
public void addOnScrollChangedListener(OnScrollChangedListener listener) {
checkIsAlive();

if (mOnScrollChangedListeners == null) {
mOnScrollChangedListeners = new CopyOnWriteArray();
}

mOnScrollChangedListeners.add(listener);
}
}

public interface OnScrollChangedListener {
public void onScrollChanged();
}

在可见性检测扩展方法中捕获第二个时机:


fun View.onVisibilityChange(block: (view: View, isVisible: Boolean) -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener {}
viewTreeObserver.addOnScrollChangedListener {}
}

捕获全局焦点变化时机


下面这些 case 都是焦点发生了变化:



  1. 手机电源开关

  2. Home 键

  3. 被 Dialog 遮挡

  4. Activity 切换


同样借助于 ViewTreeObserver 可以捕获到焦点变化的时机。


到目前为止,全局可见性扩展方法中已经监听了三种时机,分别是全局重绘、全局滚动、全局焦点变化:


fun View.onVisibilityChange(block: (view: View, isVisible: Boolean) -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener {}
viewTreeObserver.addOnScrollChangedListener {}
viewTreeObserver.addOnWindowFocusChangeListener {}
}

捕获新增子控件时机


最后一个 case 是最复杂的:动态替换的 Fragment 遮挡了原有控件。


该场景如下图所示:
1668323952343.gif


界面中有一个底边栏,其中包含各种 tab 标签,点击其中的标签会以 Fragment 的形式从底部弹出。此时,底边栏各 tab 从可见变为不可见,当点击返回时,又从不可见变为可见。


一开始的思路是“从被遮挡的 View 本身出发”,看看某个 View 被遮挡后,其本身的属性是否会发生变化?


View 内部以is开头的方法如下所示:


微信截图_20221113152433.png


我把其中名字看上去可能和被遮挡有关联的方法值全都打印出来了,然后触发 gif 中的场景,观察这些值在触发前后是否会发生变化。


几十个属性,一一比对,在看花眼之前,log 告诉我,被遮挡之后,这些都没有发生任何变化。。。。


绝望。。。但还不死心,继续寻找其他方法:


微信截图_20221113152922.png


我又找了 View 内部所有has开头的方法,也把其中看上去和被遮挡有关的方法全打印出来了。。。你猜结果怎么着?依然是徒劳。。。。


我开始质疑出发点是否正确。。。此时一声雷鸣劈醒了我。


视图只可能了解其自身以及其下层视图的情况,它无法得知它的平级甚至是父亲的绘制情况。而 gif 中的场景,即是在底边栏的同级有一个 Fragment 的容器。而且当视图被其他层级的控件遮挡时,整个绘制体系也不必通知那个被遮挡的视图,否则多低效啊(我yy的,若有大佬知道内情,欢迎留言指点一二。)


经过这层思考之后,我跳出了被遮挡的那个视图,转而去 Fragment 的容器哪里寻求解决方案。


Fragment 要被添加到 Activity 必须提供一个容器控件,容器控件提供了一个回调用于监听子控件被添加:


// android.view.ViewGroup.java
public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
mOnHierarchyChangeListener = listener;
}

public interface OnHierarchyChangeListener {
void onChildViewAdded(View parent, View child);
void onChildViewRemoved(View parent, View child);
}

为了监听 Fragment 被添加的这个瞬间,得为可见性检测扩展方法添加一个参数:


fun View.onVisibilityChange(
viewGroup:
ViewGroup? = null, // 容器
block: (
view: View, isVisible: Boolean) -> Unit
)
{ }

其中 viewGroup 表示 Fragment 的容器控件。


既然 Fragment 的添加也是往 View 树中插入子控件,那 View 树必定会重绘,可以在全局重绘回调中进行分类讨论,下面是伪代码:


fun View.onVisibilityChange(
viewGroup:
ViewGroup? = null,
block: (
view: View, isVisible: Boolean) -> Unit
)
{
var viewAdded = false
// View 树重绘时机
viewTreeObserver.addOnGlobalLayoutListener {
if(viewAdded){
// 检测新插入控件是否遮挡当前控件
}
else {
// 检测当前控件是否出现在屏幕中
}
}
// 监听子控件插入
viewGroup?.setOnHierarchyChangeListener(object : OnHierarchyChangeListener {
override fun onChildViewAdded(parent: View?, child: View?) {
viewAdded = true
}
}
override fun onChildViewRemoved(parent: View?, child: View?) {
viewAdded = false
}
})
}

子控件的插入回调总是先于 View 树重绘回调。所以先在插入时置标志位viewAdded = true,以便在重绘回调中做分类讨论。(因为检测子控件遮挡和是否出现在屏幕中是两种不同的检测方案)


可见性检测算法


检测控件的可见性的算法是:“判断控件的矩形区域是否和屏幕有交集”


为此新增扩展属性:


val View.isInScreen: Boolean
get() = ViewCompat.isAttachedToWindow(this) && visibility == View.VISIBLE && getLocalVisibleRect(Rect())

val 类名.属性名: 属性类型这样的语法用于为类的实例添加一个扩展属性,它并不是真地给类新增了一个成员变量,而是在类的外部新增属性值的获取方法。


当前新增的属性是 val 类型的,即常量,所以只需要为其定义个 get() 方法来表达如何获取它的值。


View 是否在屏幕中由三个表达式共同决定。



  1. 先通过 ViewCompat.isAttachedToWindow(this) 判断控件是否依附于窗口。

  2. 再通过 visibility == View.VISIBLE 判断视图是否可见。

  3. 最后调用getLocalVisibleRect()判断它的矩形相对于屏幕是否可见:


// android.view.View.java
public final boolean getLocalVisibleRect(Rect r) {
final Point offset = mAttachInfo != null ? mAttachInfo.mPoint : new Point();
if (getGlobalVisibleRect(r, offset)) {
r.offset(-offset.x, -offset.y);
return true;
}
return false;
}

该方法会先获取控件相对于屏幕的矩形区域并存放在传入的 Rect 参数中,然后再将其偏移到控件坐标系。如果矩形区域为空,则返回 false 表示不在屏幕中,否则为 true。


刚才捕获的那一系列时机,有可能会被多次触发。为了只将可见性发生变化的事件回调给上层,得做一次过滤:


val KEY_VISIBILITY = "KEY_VISIBILITY".hashCode()

val checkVisibility = {
// 获取上一次可见性
val lastVisibility = getTag(KEY_VISIBILITY) as? Boolean
// 获取当前可见性
val isInScreen = this.isInScreen() && visibility == View.VISIBLE
// 无上一次可见性,表示第一次检测
if (lastVisibility == null) {
if (isInScreen) {
// 回调可见性回调给上层
block(this, true)
// 更新可见性
setTag(KEY_VISIBILITY, true)
}
}
// 当前可见性和上次不同
else if (lastVisibility != isInScreen) {
// 回调可见性给上层
block(this, isInScreen)
// 更新可见性
setTag(KEY_VISIBILITY, isInScreen)
}
}

过滤重复事件的方案是记录上一次可见性(记录在 View 的 tag 中),如果这一次可见性检测结果和上一次相同则不回调给上层。


将可见性检测定义为一个 lambda,这样就可以在捕获不同时机时复用。


以下是完整的可见性检测代码:


fun View.onVisibilityChange(
viewGroups:
List<ViewGroup> = emptyList()
, // 会被插入 Fragment 的容器集合
needScrollListener: Boolean = true,
block: (view: View, isVisible: Boolean) -> Unit
) {
val KEY_VISIBILITY = "KEY_VISIBILITY".hashCode()
val KEY_HAS_LISTENER = "KEY_HAS_LISTENER".hashCode()
// 若当前控件已监听可见性,则返回
if (getTag(KEY_HAS_LISTENER) == true) return

// 检测可见性
val checkVisibility = {
// 获取上一次可见性
val lastVisibility = getTag(KEY_VISIBILITY) as? Boolean
// 判断控件是否出现在屏幕中
val isInScreen = this.isInScreen
// 首次可见性变更
if (lastVisibility == null) {
if (isInScreen) {
block(this, true)
setTag(KEY_VISIBILITY, true)
}
}
// 非首次可见性变更
else if (lastVisibility != isInScreen) {
block(this, isInScreen)
setTag(KEY_VISIBILITY, isInScreen)
}
}

// 全局重绘监听器
class LayoutListener : ViewTreeObserver.OnGlobalLayoutListener {
// 标记位用于区别是否是遮挡case
var addedView: View? = null
override fun onGlobalLayout() {
// 遮挡 case
if (addedView != null) {
// 插入视图矩形区域
val addedRect = Rect().also { addedView?.getGlobalVisibleRect(it) }
// 当前视图矩形区域
val rect = Rect().also { this@onVisibilityChange.getGlobalVisibleRect(it) }
// 如果插入视图矩形区域包含当前视图矩形区域,则视为当前控件不可见
if (addedRect.contains(rect)) {
block(this@onVisibilityChange, false)
setTag(KEY_VISIBILITY, false)
} else {
block(this@onVisibilityChange, true)
setTag(KEY_VISIBILITY, true)
}
}
// 非遮挡 case
else {
checkVisibility()
}
}
}

val layoutListener = LayoutListener()
// 编辑容器监听其插入视图时机
viewGroups.forEachIndexed { index, viewGroup ->
viewGroup.setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {
override fun onChildViewAdded(parent: View?, child: View?) {
// 当控件插入,则置标记位
layoutListener.addedView = child
}

override fun onChildViewRemoved(parent: View?, child: View?) {
// 当控件移除,则置标记位
layoutListener.addedView = null
}
})
}
viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
// 全局滚动监听器
var scrollListener:ViewTreeObserver.OnScrollChangedListener? = null
if (needScrollListener) {
scrollListener = ViewTreeObserver.OnScrollChangedListener { checkVisibility() }
viewTreeObserver.addOnScrollChangedListener(scrollListener)
}
// 全局焦点变化监听器
val focusChangeListener = ViewTreeObserver.OnWindowFocusChangeListener { hasFocus ->
val lastVisibility = getTag(KEY_VISIBILITY) as? Boolean
val isInScreen = this.isInScreen
if (hasFocus) {
if (lastVisibility != isInScreen) {
block(this, isInScreen)
setTag(KEY_VISIBILITY, isInScreen)
}
} else {
if (lastVisibility == true) {
block(this, false)
setTag(KEY_VISIBILITY, false)
}
}
}
viewTreeObserver.addOnWindowFocusChangeListener(focusChangeListener)
// 为避免内存泄漏,当视图被移出的同时反注册监听器
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) {
}

override fun onViewDetachedFromWindow(v: View?) {
v ?: return
// 有时候 View detach 后,还会执行全局重绘,为此退后反注册
post {
try {
v.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
} catch (_: java.lang.Exception) {
v.viewTreeObserver.removeGlobalOnLayoutListener(layoutListener)
}
v.viewTreeObserver.removeOnWindowFocusChangeListener(focusChangeListener)
if(scrollListener !=null) v.viewTreeObserver.removeOnScrollChangedListener(scrollListener)
viewGroups.forEach { it.setOnHierarchyChangeListener(null) }
}
removeOnAttachStateChangeListener(this)
}
})
// 标记已设置监听器
setTag(KEY_HAS_LISTENER, true)
}

该控件可见性检测方法,最大的用处在于检测 Fragment 的可见性。详细讲解可以点击 页面曝光难点分析及应对方案


Talk is cheap,show me the code


上述源码可以在这里找到。


推荐阅读


业务代码参数透传满天飞?(一)


业务代码参数透传满天飞?(二)


全网最优雅安卓控件可见性检测


全网最优雅安卓列表项可见性检测


页面曝光难点分析及应对方案


你的代码太啰嗦了 | 这么多对象名?


你的代码太啰嗦了 | 这么多方法调用?


作者:唐子玄
来源:juejin.cn/post/7165427955902971918
收起阅读 »

一个艰难就业的23年应届生的2022年

自我介绍 我的家乡是浙江-宁波-余姚,是一名就读于一所位于宁波-慈溪(学校:笑死,这就我一所大学,你直接报我名字得了)的双非独立学院的软件工程专业的23年应届生,7到10月有在南京实习,现在是孤身一人在杭州实习的社恐前端实习生,前端练习时长一年半,擅长唱、跳、...
继续阅读 »

自我介绍


我的家乡是浙江-宁波-余姚,是一名就读于一所位于宁波-慈溪(学校:笑死,这就我一所大学,你直接报我名字得了)的双非独立学院的软件工程专业的23年应届生,7到10月有在南京实习,现在是孤身一人在杭州实习的社恐前端实习生,前端练习时长一年半,擅长唱、跳、rap... 还只擅长Vue的渣渣前端程序猿,有兴趣可以关注我的公众号程序猿青空,23年开始我会时不时分享各种优秀文章、学习资源、学习课程,探索初期,还请多多关照。这篇文章会是我公众号的第一篇文章,主要对我这一年来的经历做一个简单的流水账总结,涉及到恋爱、租房、学习、工作等各方面内容,希望这份经验对你也能有所帮助。


学习


大二下半年的时候分流,自主报名到了我们学校的产业学院——企业和学校联合创办的培养应用型人才的学院。我文科相当薄弱,埋头考研会相当痛苦,也很清楚自己做不来官僚主义那一套,公职也不是适合我的职业(没错我对公职有偏见),很坚定就业这条路。因为还没有毕业,我的身份归根结底就是一个双非下流本科的一名大学生,为了避免自己毕业即失业,看当时产业学院的宣传也不错就去了。


事实上因为产业学院刚创办不久,而且并不是所有人来到这里都是为了就业的,也有可能是为了学分、助学金等其他方面的原因,课程设计、师资力量、同学质量等各方面都良莠不齐、鱼龙混杂。每门课程的期末大作业基本都是一个小项目,大三一年里两个期末都有为了大作业通宵的几天,再加上1500💰凑活过的生活费,死贵的电费和食堂伙食费,在这里学习和生活有时候还蛮辛苦的。好在我很清楚自己应该做什么,天赋不够,努力来凑,本来起跑线就低,更应该比别人卷一点。当然我也不是那种能够没日没夜卷的人(👀),关注了鱼皮,加入了他的知识星球,在星球天天学习健身(没错我还健身💪)打卡的flag没两个礼拜就立不住了,知识付费的事咱也没少干,就是说能一直坚持下来的着实不多,咱也明白咱就是个普通人,逆袭这种事确实还是很难做到的,我这人还是比较佛系的。


大三这一年我用一年的时间从零学前端,自认为还算是没有辜负自己,这一年时间的学习也还算有成果,虽然没法和卷王们争第一,也能跟在他们后面做个万年老二(😭呜呜呜)。下半年开始实习后更别说了,新的技术栈的学习基本就停滞了。实习前我还天真的以为能有更多的时间学习,正相反,比在学校学的更少,因为下班到家七八点,生活琐事会比在学校里多得多,而且我下班后还要花一个多钟头健身,再加上忙碌一天后更无心学习,只想躺平。


下半年做过的最卷的事也就参与了字节青训营,课题选择了前端监控平台,可惜的就是没能在青训营期间完成(😭呜呜呜,队友都摆烂了),当然也就没有结营证书。但我也不甘心就这样算罢,这个项目我就自己拉出来,作为我的毕业设计去完成它。解决实习期间学习效率低的最好办法就是在公司学习一些对公司业务有关或者优化公司项目的知识,名正言顺地摸鱼。我是Vue入门的,这一年里也一直守着Vue,来年第一季度目标就是学习React和Nest,开发一个自己的数据聚合的网站,能变现就最好了(😎欸嘿)。


生活&实习


大三下,也就是今年上半年,为了冲刺暑期实习,也就没去做兼职了,感叹本就艰难的生活的同时,殊不知这是为数不多还能自己自由掌控的日子了(😥我哭死)。其实我开始准备实习还是挺晚了,再加上期末没有太多时间,准备并不是太充分,没有太多自信心,投了几家大厂,不是没回应,就是笔试挂,就有点望而却步。


在我一个大佬同学的介绍下,面试了一家南京的小厂,过程很顺利,实习薪资给的也很可观,当时就没考虑那么多,就选择接受offer了(后来在杭州实习认识了几个小伙伴,才学了没几个月,暑假就面试进了独角兽企业,我那个时候确实应该再多投一投的)。刚开始的想法是第一次出门实习,有份经验就可以,在什么城市没关系,然而事实是工作上确实没什么关系,生活上关系可大了。7月13日第一次一个人拎上行李,义无反顾地去了南京,以为自己终于能够大展拳脚,再不济也能够在公司有所贡献,然而现实总是没那么理想。


上路


因为一个人前往外地工作,第一件事情便是租房,为了省点钱就托南京实习公司的一个同事看房子,因为他的房租到期也要找房子就顺便可以租在一起,有个照应。然而实际上因为是第一次出远门工作和生活,一切和自己的理想差距显然大了许多:因为不是自己实地看的房,而且也是第一次租房,虽然房租只有850💰,但是也可能因为是夏季大家都开空调,差不多50多💰一个礼拜的电费和其他乱七八糟的费用,一个月光租房子就差不多得1200💰,并不算贵,但是性价比极低;我的房间没地方晒衣服,只能晒在那个同事的房间的阳台,作为一个社恐患者,每次去都要做很多心理斗争(他会不会睡了,他会不会在忙....🙃);桌上只能堪堪放下我的显示器和笔记本,鼠标活动范围极小;床应该是睡过好几个租客了,明显的不舒服;吃的方面因为有点水土不服不能随便乱吃,同时也是为了省钱所以选择自己做饭,因此还得购置很多厨具调味品等等,一次性的开销💰不小;回学校的频率比我想象的高,因此来回车费也成为一大负担;当时租房合同是同事代签的,他签了一年,我那时候也不懂也没问,再加上当时换工作离开的比较急,没时间找转租,违约金直接血亏1700💰。


日常挤地铁


生活的种种问题都还能接受或者解决,然而工作方面,因为进入公司的时间段比较特殊再加上疫情影响,在南京实习的三个月里,我始终没有能够在技术上得到足够的提升,再加上与公司和领导的气场不合,使得我在公司整天如坐针毡,甚至有点无所事事(总之就是过的很不开心),虽然有不低的实习薪资,但是我始终没法在那里躺平。因此在中秋决定参与秋招,开始寻找第二份实习工作。


然而今年找工作并不简单,因为频繁发作的疫情,再加上互联网行业这些年的发展,行业的形势非常的严峻,各大公司都削减了HC(head count,人头数,就是最终录用的人数,肯定有小伙伴不懂这个词,我一开始就不懂🤏),作为一个民本23年应届生,在今年的秋招着实很难找到一份理想的工作。那段时间的想法就是尽快找到下一份工作(急急急急急急,我是急急国王),找到一份离家近、工资高、平台大至少满足两个的工作。从9月10日中秋就开始投出第一份简历,到10月19日确定来到杭州的一家四五百人的SaaS公司,这期间投出过几百份简历,得到的回应却寥寥无几,这是一段非常难忘的经历。


这一个月里每一天都在为找工作烦恼,一开始专注于线上面试,却始终的得不到理想工作的认可,持续的碰壁使得开始怀疑自己这些年的学习,自己的选择是不是错了,是不是自己能力确实没法满足他们的要求(被ktv了),后来也决定不放过线下面试的机会,顶着疫情在南京、杭州、家、学校几地频繁奔波,在杭州线下面试的那一天还是顶着自己身体上的各种不适(持续拉肚子,全身酸痛,萎靡不振),仍然要拿出饱满的精神去面对面试,好在当时就获得了面试官也是现在的leader的认可,简直就是久旱逢甘霖,虽然并不是直接发的offer,但是也是十分有信心。杭州比起南京的工作,实习薪资低了很多,但是因为线下面试,对于当时感受到的公司的氛围十分的心动,也就放弃了其他小公司更高薪资的offer,决定了自己的第二份实习工作。


又上路啦


换工作又是换城市,所以又需要租房搬家,购置各种必需品,又是一大笔开销,在还没进公司前始终在担忧自己先择了薪资更低的工作,到时候会不会付出了这么多,结果又远不如预期让自己更痛苦。不过在经过了一个月左右实习后,我在杭州的公司工作的感受让我相信自己的选择没有错。


10月23日我再一次拖着一大堆行李开始了迁徙,本来打算先简单看房子,先回家住几天再自驾,拖着行李回来看房子签合同,所以我把被子等一些大件的行李都寄回家了,但是这次进入杭州后就黄🐎了(之前几地来回跑黄都没黄一下),只能多看几套房子然后就签下来,好在当天就看到一个自己满意的,10几平,押一付一,一个月算上水电差不多也就1300💰,不至于睡大街,但是我没有被子,当时杭州刚开始降温,温度也就个位数,但是买被子太亏了,之后用不上,就买了床毛毯,多盖几件衣服,凑活过了两天(真的凑活,冷的雅痞)。


杭州租的房


11月1日正式入职,正式开启了在杭州的工作生活,有条不紊的入职手续,时长1周的实习生培训,认识了许多和我一起实习的小伙伴,刚进来还赶上公司的双十一活动,让我对未来的工作生活充满希望。


双十一零食自助


第一月开始接触了一些简单的业务,重新开始了健身,第二个月就参与开发了一个简单的项目,还封装了公共组件、开发了简单的提高开发效率的脚手架工具,我终于能够继续有条不紊运转了。


在南京实习的期间除了参加了字节青训营和准备面试而巩固基础外,专业上可以说是没有丝毫提升,不过生活经验确实收获满满,坚定了自己的目标,职业生涯规划更加清晰,为了达到目标去学会自律。这几个月的开销给自己和父母都增添了不小得负担,好在现在稳定下来勉强能够在杭州自给自足,生活重新步入正轨,比起在南京,杭州的生活更加得心应手。但是并不是说南京不好,南京是一个非常优雅的城市,这里有他躺在超市里超乖的猫猫,超治愈


超乖的猫猫


离开南京前我也花时间去好好游玩了两天(去了一些免费的博物馆,景点)。


忘记叫啥地了


比起杭州,我认为南京更适合生活,我只是去到了一个不适合我的公司和因为经验不足吃了不少亏才离开了这个城市。我很珍惜在杭州的这份工作,也非常享受现在忙碌充实的生活,我也希望自己的能力能够不断得到认可,继续探索自己的人生价值。


感情


呜呜呜,鼠鼠该死啊,鼠鼠长了个恋爱脑,但是好在现在穷的雅痞,我还社恐,可以心无旁骛地工作学习(搞💰)。出来实习没几个礼拜就跟在一起一年的女孩子分手了,其实在上半年因为我们对未来规划的分歧就吵过架,她想留在慈溪,而我更向往大城市(当然不止这一点原因啦),那个时候我就很清楚这段感情肯定没法坚持很久,下半年又异地,在各自的城市实习,天天吵架,自然而然就吵分了,累觉不爱。我深知自己不是啥好男人(男人没一个好东西),还没有资本,毕业前绝对要水泥封心(做杭州第一深情)。


其实我家离学校很近,但是从念大学开始还是很少回家了,在学校里没有什么感觉,直到独自出门在外工作才知道在家真好,爸爸妈妈真好(我是妈宝男,呜呜呜😭),看这篇文章的小伙伴不要再随便跟爸爸妈妈撒气了哦。家里的老人只剩下奶奶独自在乡下了,以后一定要多打电话。


展望


在未来的一年中,希望自己能够吸收已经犯过的错误的经验,保质保量地完成未来的各项工作,作为一名程序员最重要的最重要的就是自我驱动,持续学习,通过不断学习才能够在未来的工作中创造更多的价值,以下是我23年的一些计划


学习



  • 这个月先抓紧时间把自己的毕设解决,写复盘的分享博客,之后顺利毕业

  • 上半年学习React,Nest,开发一个数据聚合分享平台,同样做分享

  • 运营自己的博客和各平台账号,不说多少粉丝,能坚持不凉就行,争取每周一个博客

  • 每季至少阅读一本书,学习一个技术栈

  • 坚持自己的每日计划和每月复盘总结(包含年中和年终总结)


工作



  • 因为现在常态化了,不知道今年的就业形势会是什么样的,着实不想再像去年那样被支配了,所以还是希望得到自己满意的薪资的前提下在这里转正,但愿不要出什么幺蛾子吧

  • 继续卷进部门更深层业务,目标负责6个项目

  • 学习更多优化开发效率和质量的技术栈,明年就简单定个两个的目标吧,要求不高


生活



  • 我真的超级想买机车的,但是杭州主城区禁摩,所以先23年下半年花时间考个D照,看情况决定买个机车还是电驴

  • 3月份房租到期了,看房肯定又要放进日程了,看看到时候有没有合租的小伙伴吧,如果有人有兴趣到时候可以分享一下杭州租房经验

  • 健身肯定是要继续的,有一说一我肉体确实没啥天赋(也可能是吃得不够多),健身更多的是一种生活态度吧

  • 我是一个很不喜欢打电话的人,尤其是和长辈,感觉没话聊,但是老人家接到自己孩子的电话,知道孩子过得不错,真的会很开心。明年定个小目标,一个月给奶奶打一通电话。


2022年好像所有人都过的很艰难,或许所有人都想离开浪浪山,但是也不要忘记看看浪浪山的风景,让我们一起加油吧。最后再打个广告,关注公众号程序猿青空,免费领取191本计算机领域黑皮书电子书,更有集赞活动免费挑选精品课程(各个领域的都有),不定期分享各种优秀文章、学习资源、学习课程,能在未来(因为现在还没啥东西)享受更多福利。


作者:CyanSky
来源:juejin.cn/post/7189562801159929915
收起阅读 »

iOS 3年开发迷茫随心聊

iOS 3年开发迷茫随心聊 从毕业开始做iOS,到现在已经是第4个年头了。第一家公司,做了一年,项目没上线就倒闭了,导致找第二家公司的时候也没有一个项目能拿的出手。第二家公司误入一家游戏公司,每天工作就是将H5小游戏做成一个App,想办法上线,一年过去了,技术...
继续阅读 »

iOS 3年开发迷茫随心聊


从毕业开始做iOS,到现在已经是第4个年头了。第一家公司,做了一年,项目没上线就倒闭了,导致找第二家公司的时候也没有一个项目能拿的出手。第二家公司误入一家游戏公司,每天工作就是将H5小游戏做成一个App,想办法上线,一年过去了,技术其实也没什么长进,但是通过这个过程了解一些苹果上架的知识。也有了几个上线项目经验了。由于想找个正经互联网公司做App,也离职找工作。


找工作头一个月,发现面试面的问题都是与底层相关,一问三不知,在家埋头学了2个月底层相关知识(可以理解背题)。原理是懂一点了,但是没有在实际项目中运用,工资也上不太去。在面了20多家公司后,终于找到现在第三份工作。


由于有第二份工作的经历,在第三家公司上班的时候一直在学习,有意识的去面试的原理去解决一些开发中的问题,例如使用runtime解决一些问题,却发现runtime如果没有很强的理解,还是不要用在项目里,因为可能出现未知的风险。例如使用交换方法,全局做了修改,但是后期项目需求更改,保持全局修改的前提下,对其他情况要做不同处理。也没有太多需求会使用到原理的内容,性能也不需要优化。


小公司对技术不太感冒,能完成需求就行,虽然自己力所能及的去做一些规范,但觉得做的还是不够,也不清楚其他公司到底是如何做的。小公司个人感觉对员工做事的责任心更加看重。需求就是写页面,页面还原的好,做的快一点,bug少一点就行了。不是理想的一个团队有什么方案,规范,让开发更有效率。最大感触是还好没有成为一个油腻的开发~。


在现在的公司,做了几个项目,也没有大的bug。学会了Swift进行开发。也许也算是一种收获吧。但是公司不加薪,今年的目标是想学点东西换一份工作。


学了1个月RxSwift,感觉也快学不下去了,公司是不可能用了,网上也有人说这个架构太重了。语言是个问题,自己英语水平有限,学习速度太慢了。如果有看到的这篇文章的小伙伴,也可以给我点意见。


最近想学一点提高开发效率的技能。和面试相关的内容。如果有大神经历过我这个时期,还麻烦给点建议。建议退iOS坑的就不必留言了。个人虽然菜,但是如果还没有做到小公司天花板的话,目前不考虑退坑。


第一次发文章也不知道说啥,后面会更新一些学习

作者:MissSunRise
来源:juejin.cn/post/7071892765763698719
笔记啥的。感谢包容。

收起阅读 »

乱打日志的男孩运气怎么样我不知道,加班肯定很多!

.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:16px;overflow-x:hidden;color:#252933}.markdown-bod...
继续阅读 »

前言


线上出现问题,你的第一反应是什么?


如果是我的话,第一时间想的应该是查日志:



  1. if…else 到底进入了哪个分支?

  2. 关键参数是不是有缺失?

  3. 入参是不是有问题,没做好校验放进去了?


良好的日志能帮我们快速定位到问题所在,坑你的东西往往最为无形,良好的日志就是要让这些玩意无所遁形!


日志级别


Java应用中,日志一般分为以下5个级别:



  • ERROR 错误信息

  • WARN 警告信息

  • INFO 一般信息

  • DEBUG 调试信息

  • TRACE 跟踪信息


1)ERROR


ERROR 级别的日志一般在 catch 块里面出现,用于记录影响当前线程正常运行的错误,出现 Exception 的地方就可以考虑打印 ERROR 日志,但不包括业务异常。


需要注意的是,如果你抛出了异常,就不要记录 ERROR 日志了,应该在最终的地方处理,下面这样做就是不对的:


try {
   int i = 1 / 0;
} catch (Exception e) {
   log.error("出错了,什么错我不知道,啊哈哈哈!", e);
   throw new CloudBaseException();
}

2)WARN


不应该出现,但是不会影响当前线程执行的情况可以考虑打印 WARN 级别的日志,这种情况有很多,比如:



  • 各种池(线程池、连接池、缓存池)的使用超过阈值,达到告警线

  • 记录业务异常

  • 出现了错误,但是设计了容错机制,因此程序能正常运行,但需要记录一下


3)INFO


使用最多的日志级别,使用范围很广,用来记录系统的运行信息,比如:



  • 重要模块中的逻辑步骤呈现

  • 客户端请求参数记录

  • 调用第三方时的参数和返回结构


4)DEBUG


Debug 日志用来记录自己想知道的所有信息,常常是某个功能模块运行的详细信息,已经中间的数据变化,以及性能信息。


Debug 信息在生产环境一般是关闭状态的,需要使用开关管理(比如 SpringBoot Admin 可以做到),一直开启会产生大量的 Debug,而 Debug 日志在程序正常运行时大部分时间都没什么用。


if (log.isDebugEnabled()) {
   log.debug("开始执行,开始时间:[{}],参数:[{}]", startTime, params);
   log.debug("通过计算,得到参数1:[{}],参数2:[{}]", param1, param2);
   log.debug("最后处理结果:[{}]", result);
}

5)TRACE


特别详细的系统运行完成信息,业务代码中一般不使用,除非有特殊的意义,不然一般用 DEBUG 代替,事实上,我编码到现在,也没有用过这个级别的日志。


使用正确的格式


如果你是这样打印日志的:


log.info("根据条件id:{}" + id + "查询用户信息");

不要这样做,会产生大量的字符串对象,占用空间的同时也会影响性能。


正确的做法是使用参数化信息的方式:


log.info("根据条件id:[{}],查询用户信息", id);

这样做除了能避免大量创建字符串之外,还能明确的把参数隔离出去,当你需要把参数复制出来的时候,只需要双击鼠标即可,而不是用鼠标慢慢对准再划拉一下。


这样打出来的日志,可读性强,对排查问题的帮助也很大!


小技巧


1)多线程


遇到多个线程一起执行的日志怎么打?


有些系统,涉及到并发执行,定时调度等等,就会出现多次执行的日志混在一起,出问题不好排查,我们可以把线程名打印进去,或者加一个标识用来表明这条日志属于哪一次执行:


if (log.isDebugEnabled()) {
   log.debug("执行ID=[{}],处理了ID=[{}]的消息,处理结果:[{}]", execId, id, result);
}

2)使用 SpringBoot Admin 灵活开关日志级别


image-20220727155526217


写在最后


一开始写代码的时候,没有规范日志的意识,不管哪里,都打个 INFO,打印出来的东西也没有思考过,有没有意义,其实让自己踩了不少坑,加了不少班,回过头,我想对学习时期的我说一句:”能让你加班的东西,都藏在各种细节里!写代码之前,先好好学习如何打日志!“


作者:你算哪块小蛋糕
来源:juejin.cn/post/7124958610123128839
收起阅读 »

你可能忽略的10种JavaScript快乐写法

.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:16px;overflow-x:hidden;color:#252933}.markdown-bod...
继续阅读 »

你可能忽略的10种JavaScript快乐写法


前言



  • 代码的简洁、美感、可读性等等也许不影响程序的执行,但是却对人(开发者)的影响非常之大,甚至可以说是影响开发者幸福感的重要因素之一;

  • 了解一些有美感的代码,不仅可以在一定程度上提高程序员们的开发效率,有些还能提高代码的性能,可谓是一举多得;


笔者至今难以忘记最开始踏入程序员领域时接触的一段List内嵌for的Python代码:


array = [[16, 3, 7], [2, 24, 9], [4, 1, 12]]
row_min = [min(row) for row in array ]
print(row_min)

这可能就是动态语言非常优秀的一点,而JavaScript同样作为动态语言,其中包含的优秀代码片段也非常之多,比如我们通过JavaScript也可以非常轻松地实现上述的功能:


const array = [[16, 3, 7], [2, 24, 9], [4, 1, 12]]
const row_min = array.map(item => Math.min(...item))
console.log(row_min)

能写出优秀的代码一直是笔者所追求的,以下为笔者在开发阅读过程积累的一些代码片段以及收集了互联网上一些优秀代码片段,希望对你有所帮助


概述


这里,考虑到有些技巧是大家见过的或者说是已经烂熟于心的,但总归有可能有些技巧没有留意过,为了让大家更加清楚的找到自己想要查阅的内容以查漏补缺,所以这里笔者贴心地为大家提供了一张本文内容的索引表,供大家翻阅以快速定位,如下:


应用场景标题描述补充1补充2
数组去重通过内置数据解构特性进行去重[] => set => []通过遍历并判断是否存在进行去重[many items].forEach(item => (item <不存在于> uniqueArr) && uniqueArr.push(item))
数组的最后一个元素获取数组中位置最后的一个元素使用at(-1)
数组对象的相关转换对象到数组:Object.entries()数组到对象:Obecjt.fromEntries()
短路操作通过短路操作避免后续表达式的执行a或b:a真b不执行a且b:a假b不执行
基于默认值的对象赋值通过对象解构合并进行带有默认值的对象赋值操作{...defaultData, ...data}
多重条件判断优化单个值与多个值进行对比判断时,使用includes进行优化[404,400,403].includes
交换两个值通过对象解构操作进行简洁的双值交换[a, b] = [b, a]
位运算通过位运算提高性能和简洁程度
replace()的回调通过传入回调进行更加细粒度的操作
sort()的回调通过传入回调进行更加细粒度的操作根据字母顺序排序根据真假值进行排序

数组去重


这不仅是我们平常编写代码时经常会遇到的一个功能实现之一,也是许多面试官在考查JavaScript基础时喜欢考查的题目,比较常见的基本有如下两类方法:


1)通过内置数据结构自身特性进行去重


主要就是利用JavaScript内置的一些数据结构带有不包含重复值的特性,然后通过两次数据结构转换的消耗[] => set => []从而达到去重的效果,如下演示:


const arr = ['justin1go', 'justin2go', 'justin2go', 'justin3go', 'justin3go', 'justin3go'];
const uniqueArr = Array.from(new Set(arr));
// const uniqueArr = [...new Set(arr)];

2)通过遍历并判断是否存在进行去重


白话描述就是:通过遍历每一项元素加入新数组,新数组存在相同的元素则放弃加入,伪代码:[many items].forEach(item => (item <不存在于> uniqueArr) && uniqueArr.push(item))


至于上述的<不存在于>操作,可以是各种各样的方法,比如再开一个for循环判断新数组是否有相等的,或者说利用一些数组方法判断,如indexOfincludesfilterreduce等等


const arr = ['justin1go', 'justin2go', 'justin2go', 'justin3go', 'justin3go', 'justin3go'];
const uniqueArr = [];
arr.forEach(item => {
// 或者!uniqueArr.includes(item)
if(uniqueArr.indexOf(item) === -1){
uniqueArr.push(item)
}
})

结合filter(),判断正在遍历的项的index,是否是原始数组的第一个索引:


const arr = ['justin1go', 'justin2go', 'justin2go', 'justin3go', 'justin3go', 'justin3go'];
const uniqueArr = arr.filter((item, index) => {
return arr.indexOf(item, 0) === index;
})

结合reduce(),prev初始设为[],然后依次判断cur是否存在于prev数组,如果存在则加入,不存在则不动:


const arr = ['justin1go', 'justin2go', 'justin2go', 'justin3go', 'justin3go', 'justin3go'];
const uniqueArr = arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]);

数组的最后一个元素


对于获取数组的最后一个元素,可能平常见得多的就是arr[arr.length - 1],我们其实可以使用at()方法进行获取


const arr = ['justin1go', 'justin2go', 'justin3go'];
console.log(arr.at(-1)) // 倒数第一个值
console.log(arr.at(-2)) // 倒数第二个值
console.log(arr.at(0)) // 正数第一个
console.log(arr.at(1)) // 正数第二个


注:node14应该是不支持的,目前笔者并不建议使用该方法,但获取数组最后一个元素是很常用的,就应该像上述语法一样简单...



数组对象的相互转换



    const entryified = [
["key1", "justin1go"],
["key2", "justin2go"],
["key3", "justin3go"]
];

const originalObject = Object.fromEntries(entryified);
console.log(originalObject);

短路操作


被合理运用的短路操作不仅非常的优雅,还能减少不必要的计算操作


1)基本介绍


主要就是||或操作、&&且操作当第一个条件(左边那个)已经能完全决定整个表达式的值的时候,编译器就会跳过该表达式后续的计算



  • 或操作a || b:该操作只要有一个条件为真值时,整个表达式就为真;即a为真时,b不执行;

  • 且操作a && b:该操作只要有一个条件为假值时,整个表达式就为假;即a为假时,b不执行;


2)实例


网络传输一直是前端的性能瓶颈,所以我们在做一些判断的时候,可以通过短路操作减少请求次数:


const nextStep = isSkip || await getSecendCondition();
if(nextStep) {
openModal();
}

还有一个经典的代码片段:


function fn(callback) {
// some logic
callback && callback()
}

基于默认值的对象赋值



  • 很多时候,我们在封装一些函数或者类时,会有一些配置参数。

  • 但这些配置参数通常来说会给出一个默认值,而这些配置参数用户是可以自定义的

  • 除此之外,还有许许多多的场景会用到的这个功能:基于默认值的对象赋值。


function fn(setupData) {
const defaultSetup = {
email: "justin3go@qq.com",
userId: "justin3go",
skill: "code",
work: "student"
}
return { ...defaultSetup, ...setupData }
}

const testSetData = { skill: "sing" }
console.log(fn(testSetData))

如上{ ...defaultSetup, ...setupData }就是后续的值会覆盖前面key值相同的值。


多重条件判断优化


if(condtion === "justin1go" || condition === "justin2go" || condition === "justin3go"){
// some logic
}

如上,当我们对同一个值需要对比不同值的时候,我们完全可以使用如下的编码方式简化写法并降低耦合性:


const someConditions = ["justin1go", "justin2go", "justin3go"];
if(someConditions.includes(condition)) {
// some logic
}

交换两个值


一般来说,我们可以增加一个临时变量来达到交换值的操作,在Python中是可以直接交换值的:


a = 1
b = 2
a, b = b, a

而在JS中,也可以通过解构操作交换值;


let a = 1;
let b = 2;
[a, b] = [b, a]

简单理解一下:



  • 这里相当于使用了一个数组对象同时存储了a和b,该数组对象作为了临时变量

  • 之后再将该数组对象通过解构操作赋值给a和b变量即可


同时,还有种比较常见的操作就是交换数组中两个位置的值:


const arr = ["justin1go", "justin2go", "justin3go"];
[arr[0], arr[2]] = [arr[2], arr[0]]

位运算


关于位运算网上的讨论参差不齐,有人说位运算性能好,简洁;也有人说位运算太过晦涩难懂,不够易读,这里笔者不发表意见,仅仅想说的是尽量在使用位运算代码的时候写好注释!


下面为一些常见的位运算操作,参考链接


1 ) 使用&运算符判断一个数的奇偶


// 偶数 & 1 = 0
// 奇数 & 1 = 1
console.log(2 & 1) // 0
console.log(3 & 1) // 1

2 ) 使用~, >>, <<, >>>, |来取整


console.log(~~ 6.83)    // 6
console.log(6.83 >> 0) // 6
console.log(6.83 << 0) // 6
console.log(6.83 | 0) // 6
// >>>不可对负数取整
console.log(6.83 >>> 0) // 6

3 ) 使用^来完成值交换


var a = 5
var b = 8
a ^= b
b ^= a
a ^= b
console.log(a) // 8
console.log(b) // 5

4 ) 使用&, >>, |来完成rgb值和16进制颜色值之间的转换


/**
* 16进制颜色值转RGB
*
@param {String} hex 16进制颜色字符串
*
@return {String} RGB颜色字符串
*/

function hexToRGB(hex) {
var hexx = hex.replace('#', '0x')
var r = hexx >> 16
var g = hexx >> 8 & 0xff
var b = hexx & 0xff
return `rgb(${r}, ${g}, ${b})`
}

/**
* RGB颜色转16进制颜色
*
@param {String} rgb RGB进制颜色字符串
*
@return {String} 16进制颜色字符串
*/

function RGBToHex(rgb) {
var rgbArr = rgb.split(/[^\d]+/)
var color = rgbArr[1]<<16 | rgbArr[2]<<8 | rgbArr[3]
return '#'+ color.toString(16)
}
// -------------------------------------------------
hexToRGB('#ffffff') // 'rgb(255,255,255)'
RGBToHex('rgb(255,255,255)') // '#ffffff'

replace()的回调函数


之前写过一篇文章介绍了它,这里就不重复介绍了,F=>传送


sort()的回调函数


sort()通过回调函数返回的正负情况来定义排序规则,由此,对于一些不同类型的数组,我们可以自定义一些排序规则以达到我们的目的:



  • 数字升序:arr.sort((a,b)=>a-b)

  • 按字母顺序对字符串数组进行排序:arr.sort((a, b) => a.localeCompare(b))

  • 根据真假值进行排序:


const users = [
{ "name": "john", "subscribed": false },
{ "name": "jane", "subscribed": true },
{ "name": "jean", "subscribed": false },
{ "name": "george", "subscribed": true },
{ "name": "jelly", "subscribed": true },
{ "name": "john", "subscribed": false }
];

const subscribedUsersFirst = users.sort((a, b) => Number(b.subscribed) - Number(a.subscribed))

最后



  • 个人能力有限,并且代码片段这类东西每个人的看法很难保持一致,不同开发者有不同的代码风格,这里仅仅整理了一些笔者自认为还不错的代码片段;

  • 可能互联网上还存在着许许多多的优秀代码片段,笔者也不可能全部知道;

  • 所以,如果你有一些该文章中没有包含的优秀代码片段,就不要藏着掖着了,分享出来吧~


同时,如本文有所错误,望不吝赐教,友善指出🤝


Happy Coding!🎉🎉🎉


QQ图片20230223164124.gif


QQ图片20230223164124.gif


QQ图片20230223164124.gif


参考


作者:Justin3go
来源:juejin.cn/post/7203243879255277623
收起阅读 »

一次线上OOM问题分析

OOM
现象 线上某个服务有接口非常慢,通过监控链路查看发现,中间的 GAP 时间非常大,实际接口并没有消耗很多时间,并且在那段时间里有很多这样的请求。 原因分析 先从监控链路分析了一波,发现请求是已经打到服务上了,处理之前不知道为什么等了 3s,猜测是不是机器当时...
继续阅读 »

现象


线上某个服务有接口非常慢,通过监控链路查看发现,中间的 GAP 时间非常大,实际接口并没有消耗很多时间,并且在那段时间里有很多这样的请求。



原因分析


先从监控链路分析了一波,发现请求是已经打到服务上了,处理之前不知道为什么等了 3s,猜测是不是机器当时负载太大了,通过 QPS 监控查看发现,在接口慢的时候 CPU 突然增高,同时也频繁的 GC ,并且时间很长,但是请求量并不大,并且这台机器很快就因为 Heap满了而被下掉了。



去看了下日志,果然有 OOM 的报错,但是从报错信息上并没办法找到 Root Cause。


system error: org.springframework.web.util.NestedServletException: Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Java heap space   at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1055)   at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)   at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)   at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)   at javax.servlet.http.HttpServlet.service(HttpServlet.java:681) 

另外开发同学提供了线索,在发生问题的时候在跑一个大批量的一次性 JOB,怀疑是不是这个 JOB 导致的,马上把 JOB 代码拉下来分析了下,JOB 做了分批处理,代码也没有发现什么问题。


虽然我们系统加了下面的 JVM 参数,但是由于容器部署的原因,这些文件在 pod 被 kill 掉之后没办法保留下来。


-XX:+HeapDumpOnOutOfMemoryError -XX:ErrorFile=/logs/oom_dump/xxx.log -XX:HeapDumpPath=/logs/oom_dump/xxx.hprof

这个现象是最近出现的,猜测是最近提交的代码导致的,于是去分析了最近提交的所有代码,很不幸的都没有发现问题。。。


在分析代码的过程中,该服务又无规律的出现了两次 OOM,只好联系运维同学优先给这个服务加了 EFS (Amazon 文件系统)等待下次出现能抓住这个问题。


刚挂载完 EFS,很幸运的就碰到了系统出现 OOM 的问题。


dump 出来的文件足有 4.8G,话不多说祭出 jvisualvm 进行分析,分析工具都被这个dump文件给搞挂了也报了个java.lang.OutOfMemoryError: Java heap space,加载成功之后就给出了导致OOM的线程。



找到了具体报错的代码行号,翻一下业务代码,竟然是一个查询数据库的count操作,这能有啥问题?


仔细看了下里面有个foreach遍历userId的操作,难道这个userId的数组非常大?



找到class按照大小排序,占用最多的是一个 byte 数组,有 1.07G,char 数组也有1.03G,byte 数组都是数字,直接查看 char 数组吧,点进去查看具体内容,果然是那条count语句,一条 SQL 1.03G 难以想象。。。




这个userId的数据完全是外部传过来的,并没有做什么操作,从监控上看,这个入参有 64M,马上联系对应系统排查为啥会传这么多用户过来查询,经过一番排查确认他们有个bug,会把所有用户都发过来查询。。。到此问题排查清楚。


解决方案


对方系统控制传入userId的数量,我们自己的系统也对userId做一个限制,问题排查过程比较困难,修改方案总是那么的简单。


别急,还有一个


看到这个问题,就想起之前我们还有一个同样类似的问题导致的故障。


也是突然收到很多告警,还有机器 down 机的告警,打开 CAT 监控看的时候,发现内存已经被打满了。



操作和上面的是一样的,拿到 dump 文件之后进行分析,不过这是一个漫长的过程,因为 down了好几台机器,最大的文件有12GB。


通过 MAT 分析 dump 文件发现有几个巨大的 String(熟悉的味道,熟悉的配方)。



接下来就是早具体的代码位置了,去查看了下日志,这台机器已经触发自我保护机制了,把代码的具体位置带了出来。


经过分析代码发现,代码中的逻辑是查询 TIDB(是有同步延迟的),发现在极端情况下会出现将用户表全部数据加载到内存中的现象。



于是找 DBA 拉取了对应时间段 TIDB 的慢查询,果然命中了。



总结


面对 OOM 问题如果代码不是有明显的问题,下面几个JVM参数相当有用,尤其是在容器化之后。


-XX:+HeapDumpOnOutOfMemoryError -XX:ErrorFile=/logs/oom_dump/xxx.log -XX:HeapDumpPath=/logs/oom_dump/xxx.hprof

另外提一个参数也很有用,正常来说如果程序出现 OOM 之后,就是有代码存在内存泄漏的风险,这个时候即使能对外提供服务,其实也是有风险的,可能造成更多的请求有问题,所以该参数非常有必要,可以让 K8S 快速的再拉起来一个实例。


-XX:+ExitOnOutOfMemoryError

另外,针对这两个非常类似的问题,对于 SQL 语句,如果监测到没有where条件的全表查询应该默认增加一个合适的limit作为限制,防止这种问题拖垮整个系统。


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

程序媛员的博客之旅

写博客困境 自从成为了一名程序媛,就一直有很多前辈,苦口婆心的告诉我:一定要写博客,好处多多!而我,作为一枚勤奋好学(haochilanzuo)的程序媛,其实心里一直埋藏着一颗写博客的小小种子。 无奈的是,每次冲动的热情都只能持续到更新两三篇技术文章,然后就没...
继续阅读 »

写博客困境


自从成为了一名程序媛,就一直有很多前辈,苦口婆心的告诉我:一定要写博客,好处多多!而我,作为一枚勤奋好学(haochilanzuo)的程序媛,其实心里一直埋藏着一颗写博客的小小种子。


无奈的是,每次冲动的热情都只能持续到更新两三篇技术文章,然后就没办法继续更新下去了。所以工作了这么多年,自己都成为老学姐了,还是没有拿得出手的个人博客,实在是惭愧。


经过深刻的自我反省之后,我觉得阻碍我更新博客的原因,主要有以下几个方面:


1. 工作太忙。


大家都知道,相比其他工种,作为程序员的工作强度还是蛮大的。每天都有做不完的需求,开不完的会议。经常要到晚上快下班了才有时间写代码,于是加班就成了家常便饭。下了班回家也感觉很累,不想再打开电脑,只能刷刷手机、看看综艺,做一些不费脑子的娱乐活动。


2. 文字功底太差。


作为一名理科生,上学的时候语文成绩就很差,作文都靠模板以及背的素材拼凑起来。高中毕业之后,几乎没有完整写过什么。而写博客,需要高强度大量输出内容,还要有组织有架构,逻辑条理清晰,这个对我来说简直太难了。所以经常写两篇之后,发现自己写的东西惨不忍睹,于是就暂停了更新博客的计划。


3. 没什么内容可写。


虽然每天都在写代码,但是很多时候做的都是重复性工作,并没有太多有技术含量、技术深度的内容,可以支撑我写出高大上的博客。


我个人更新不下去博客的主要原因就是上面几点,相信有很多想要更新博客却坚持不下去的同学,也都有同样的感受。


如何突破困境


我想说一下,我为什么觉得自己这次能克服这几个问题,以及克服这几个问题的方法。如果大家也和我有类似的问题,可以往下读一读,看有没有什么可以借鉴的地方。主要还是给我自己未来的日更之旅打打鸡血。



一、工作太忙,没时间。



每个人的一天都是24小时,为什么有些人能做更多的事情,实现更高的成就呢?我觉得这和每个人的时间管理方式是息息相关的。掌握高效的时间管理策略,是每个高效能人士的必备技能。


我以前觉得是因为程序员的工作比其他行业更忙,所以没有时间。但是看周围,把博客或者副业运营很好的那群人,工作也不闲。所以说,这个理由只是一个对自己时间管理无能的借口而已。真正的强者,从来不会找没有时间的借口,而不去做一些尝试。


当下,为了能够实现工作、写作(其实是搞副业)和生活之间的平衡,我决定先从这几个角度来优化我的时间使用效率。


1. 为任务分配合理的优先级


事情是永远做不完的,如果想做的太多,那么时间永远都不够。我准备用重要紧急四象限法来管理任务。每拿到一个任务后,先决策这个任务是属于哪个象限的,然后再安排做的时间。
image.png


我们之所以感觉每天忙忙碌碌,却没有什么进步,主要是因为在“紧急-不重要”的事情上,浪费了太多的时间。仔细想想,上班时间有多少是浪费在了,对未来成长没有任何意义的所谓“紧急”的事情上了。而真正“重要”的事情,却被我们以没有时间做,而一直推迟。


前段时间看到的一句话,对我触动很大:
Today you do things others don't do.
Tomorrow you do things others can't do.


做“重要-不紧急”的事情,不会对你的人生产生立竿见影的效果。但是长期下去,效果一定是惊人的,而且能给你带去很多别人没有的机会。


“人们总是容易高估一天的影响,而低估长期的影响”。比如学英语、写作,可能努力了一个月都没有效果,很多人就开始放弃了,转而去寻找其他的方法。但有些人坚持了下来,于是这些人坚持了一年、两年甚至几年之后,最后到达了很高的高度,才发现原来每一天的坚持都没有浪费,最后都是有效果的。


2. 减少任务切换,提高做事情的效率


提高做事情的效率,最好的办法就是进入“心流”的状态。不管是写代码、写文字还是看书学习,在“心流”的状态下,效率比平时要提高好几倍。


“心流”的状态,就是一种忘我的境界,忘记了时间、忘记了周围所处的环境,甚至忘记了身体上的痛苦,专心沉浸在当下所做的事情上。我相信这种状态,大家多多少少都有体会,比如在废寝忘食打游戏的时候。这种状态下,人所爆发出来的潜能是巨大的。


要达到“心流”的状态,最简单易行的方法,就是减少任务的切换。就像CPU线程切换,需要缓存上一个任务的执行状态,加载下一个任务的运行环境,效率很低。人脑也是,在上下文切换的时候,需要耗费很多的时间和精力。


而工作中,经常会被工作软件的消息提醒所打断,很难进入”心流“状态。比如,正在尝试解决一个疑难的问题,但是突然来了一条工作上的消息,于是不得不中断当前的工作,去看这个消息。等处理完消息后,在回到工作,可能已经忘记之前做到哪里了,又需要花时间才能重新进入状态。


可以尝试”番茄钟"的方法。在每个番茄钟开始的时候,屏蔽消息,集中精神工作25分钟,然后再花5分钟处理这25分钟到达的消息。处理完后,进入下一个番茄钟。


3. 不要给自己定太高的目标


之前我写博客,总是一篇文章写很长,想要在一篇文章中讲完和标题有关的所有知识点。但是这样会让自己很累,每次写一篇文章都要花很长的时间和精力,到后面甚至排斥写文章这件事情。


所以这次,我决定不给自己设太高的目标,每篇技术文章,争取讲完一个知识点就可以,如果内容特别多,可以采用连载的形式。最后可以新建一篇索引的文章,将各个连载的文章串起来。


PS:时间管理是一个复杂的事情,我之前也看过一些相关的书籍,后续我也想通过更系统的文章分享出来。先在这里挖个坑,如果想看就先关注我吧,后续我会慢慢把坑都填上。



二、文字功底太差



另一个困扰我的因素,就是自己的文字功底太差了。几乎没怎么写过文章的我,不知道怎么表达自己。有时候心里有很多想说的话,但是一写起来就读不通,没办法完整表达自己的意思。


为了能顺利完成日更的目标,我决定尝试下面的方法。


1. 先写起来,自然而然就会有进步


第一个就是不管怎么样,不管写得有多烂,先写起来,以量变来引起质变。我现在的写作量,可能连那些大V一个月的量都不到,凭什么觉得自己的水平就能和人家一样。如果每天输出500字,一年就是18.25万字。坚持写,我相信写一年之后,水平肯定会有进步。


没有什么是刻意练习不能达成的,如果有,那肯定是练习不够多。


2. 多看多模仿


写文章也是有方法可以借鉴的。去看好的文章是什么样的,向优秀的文章和作者学习。


比如,我之前看一个技术博主,会在每篇文章的开头放一个脑图,描述整片文章的整体架构,我觉得这个方法就很好。首先自己可以根据这篇脑图往里填充资料,速度更快也更清晰,同时,读者也可以在看文章之前对文章的内容有一个整体的感知,很快就能定位到自己需要的内容上。之后我的文章也可以借鉴这个方法。



三、没什么内容可写



关于没什么内容可写,以前做业务开发的时候,确实有这个问题,但是现在做系统开发了,几乎每天都在学习新的知识,所以完全不愁没有内容可写了。


如果有同学想开始写博客,但是又觉得没有内容可写,可以从以下几个方面去尝试:


1. 提前想一些topic,主动积累


在开始写博客之前,提前收集一些topic。我现在就有一个文档,专门用来放我想写的文章topic,现在这个文档里面已经有几十个可以写的topic了。


提前脑暴一些topic,或者列一个知识图谱,到时候如果发现没什么内容可写,直接去list里面找一个topic就好了。


2. 主动去学习一些新的东西


对于一些业务开发的同学,可以在开发之余,主动push自己去学一些新的技术。比如看一些技术书籍和博客。


博客内容


之后我的博客,主要会围绕下面这些方向:


Android性能优化


作为一名Android开发,更新的内容主要还是在Android相关的技术点上。由于我近期工作的重点主要在性能优化方面,所以前期的文章主要会更新性能优化相关的文章,包括启动时间、存储空间、稳定性、ANR等优化方案,以及一些相关的技术原理。


Android面试集锦


等把Android性能优化相关的内容写完,会再写一些面试相关的内容。作为一个拿过各大互联网offer、一毕业就当上面试官的学姐,在面试方面还是有不少经验的。


算法题解


算法题可能也会写一些,写一些我觉得好的题目的题解(主要是算法题比较好水文章,实在不行了就来篇题解)


读书笔记


我平常也会读一些技术之外的书籍,会写一些读书笔记,到时候会更新一些这方面的内容。


新的技术方向


除了Android开发以外,未来想学习一些新的技术方向,到时候也会更新到这个博客里,比如Web3相关的内容。


杂七杂八的思考


一些思考想法,对当下事件的看法,对未来的思考,个人成长、时间管理、投资理财等等相关内容,都会记录在博客里。


总结


说了这么多,也不知道会不会有人看我写的文章,毕竟现在Android开发已经不流行了。而且ChatGPT兴起之后,普通的内容生产者,会受到非常大的冲击。可能以后查东西都不需要去搜博客文章了,直接问ChatGPT就好了。之后我的博客文章,说不定也会让ChatGPT帮我写一部分。


总之不管有没有人看,不管AI是否会把我的工作取代,我还是会把日更坚持下去。如果对我更新的内容感兴趣,欢迎点点关注呀~~


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

【2022年终总结】北漂两年,领证,换工作

很高兴见到你 👋 我是 Flywith24~ 这是我第三篇个人年终总结。由于最近在处理工作,搬家等事情,所以拖到现在才发布。 2022 年几乎没输出什么内容,以至于前些天收到这样的评论~ 在之前两篇年终总结中,除了常规的流水账,我都会写一些当年的思考: 2...
继续阅读 »

很高兴见到你 👋 我是 Flywith24~


这是我第三篇个人年终总结。由于最近在处理工作,搬家等事情,所以拖到现在才发布。


image.png


2022 年几乎没输出什么内容,以至于前些天收到这样的评论~


47546a0a1e9bb52b90b06c374bad155f.jpg


在之前两篇年终总结中,除了常规的流水账,我都会写一些当年的思考:


2020年 分享了学习 Android 的经历和感悟;


2021年 分享了如何高效学习/工作的思考;


这一次我想分享关于工作中快速融入团队,社交关系这两部分内容。


工作方面


性能优化


Q1 参加了一个冷启动性能优化的专项,学了很多相关的知识


image.png


晋升


5 月通过了晋升答辩,提升了画图与 PPT 能力 🤪


image.png


技术栈扩容


6 月中旬由于工作需要,学习了 RN 并开发了一个独立的模块。快手动态化基建做的不错,所以熟悉了 TypeScript 的语法就开始进行需求开发了。


8 月使用 ts 写了一个在 Github 个人主页显示最近文章列表的小工具,详情请参考 开源项目:Github 主页显示最近发布的掘金文章(支持动态更新)



这也是 2022年在掘金发的唯一一篇文章 😳



image.png


之后学习了心心念念的 Jetpack Compose,将个人项目用 Compose 进行了重写。


面试


11 月开始居家办公,这期间面了几家公司,最后入职了小红书。



关于入职时间拉扯了一番,最终于 2023.02.08 入职。(02.08 也是我 2020 年入职快手的日子)



image.png


生活方面


读书


image.png

今年读了很多技术方面的书,不过都没有读完,倒是读完几本杂书,其中我最喜欢的是这本:


image.png

这本书另一个译名是:狗屁工作(bullshit jobs),它的译者在播客里表示由于过审等因素用了现在的译名



知识星球人数破千


image.png

2022 年知识星球人数破千 🥳 该星球永久免费,详情可点此查看


全拼改双拼


一直以来,我都在追求更高效的工作方式,减少无用且重复的活动。


2022 年在朋友的建议下使用双拼打字,过程中伴随着阵痛,但结果令人满意。


关于什么是双拼,可以看下这个 up 主的视频


双拼这点东西,up居然讲了十分钟——十分钟了解双拼到底是啥


image.png

西部大善人


2018 年买了《荒野大镖客2》,但一直停留在第一章。2022 年打了 100 多个小时,体验了亚瑟的一生。


image.png


领证


2022.06.06 人生清单迎来一个里程碑的节点,领证啦~


image.png

旅行


2022 年点亮了三座城市,作为东北人好喜欢云南的天气


image.png
image.png
image.png
image.png


快速融入新团队


在北京我两次面临这个问题,希望我的经验对各位小伙伴有所帮助。


上手项目


快速上手公司项目,对于新人在团队内第一时间建立影响力是非常重要的。这里站在客户端开发的角度聊下我的经验。



  • 在 app 里体验所在团队负责内容(拿到 offer 后就可以做,这点很重要)

  • 认真阅读团队内提供的文档(如果有的话)

  • 将项目里自研库和没用过的第三方库通过写 demo 的方式学习其使用流程(了解原理更佳,后面会避免很多坑)

  • 从负责业务的入口开始分析,用 CodeLocator 这类工具抓一下主要界面的视图结构,形成文档,这对未来的新人很有帮助

  • 有些细节可以在做相关需求时再去了解,不要心急


了解同事


融入团队另一个重要的事是了解同事。作为新人会有相互介绍的环节,一次记住二十几个人是不容易的(对于有些脸盲的我则更加困难)。


所以我会制作一份座位表,罗列每个人工位,名字,Android 还是 iOS。实践了两次,对于我很有效。


之后我会利用需求管理平台查看每个人最近的需求以及代码,对同事主要负责的业务有一个初步了解。


不懂就问


在项目里如果遇到用了很大力气却没搞懂的问题可以果断向同事提问。


不过需要注意,不懂就问是建立在「正确提问」的基础上的


提问时可将自己的尝试和猜测说出来。


image.png



摘自 2021 年终总结



对于应届或实习生来说,如果遇到的 mentor 比较内向不爱说话,需要自己更加主动,认真思考并整理遇到的问题大胆提问。


社交关系



声明:本节只是分享我的思考与选择


我尊重每个人的选择与喜好。例如有的人喜欢工作与生活有明确边界的同事关系。选择没有好坏之分,自己喜欢即可。



选大学,我们选择的是专业,老师,同学。


同样,我们在工作中选择的是公司,团队,同事。


本质上,我们选择的是圈子,环境。


即使去一家上万人的公司,对自己影响最大的也只有身边的人。


因此我喜欢这样的社交关系:



  • 工作时我们是一起奋战的战友

  • 摸鱼时我们时一起讨论的群友

  • 用餐时我们是一起行动的饭友

  • 假期时我们是一起玩耍的好友

  • 分别时我们是一起相送的朋友


如果你想与我有这样的社交关系


芝麻掉进针眼儿里


image.png


小红书内容发布客户端组 Android 和 iOS 还有 hc


点击该链接查看内推详情


关于我


人总是喜欢做能够获得正反馈(成就感)的事情,如果感觉本文内容对你有帮助的话,麻烦点亮一下👍,这对我很重要哦~


我是 Flywith24人只有通过和别人的讨论,才能知道我们自己的经验是否是真实的,加我微信交流,让我们共同进步。



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

从自由职业到小公司再到港企再到国企,辛酸2022

🍊作者简介: 18年毕业,至今在创业公司,国企,港企(也算是外企),中间有做过9个月的自由职业,但是失败返回职场,现在专心研究技术,目标一线大厂。 🍊支持作者: 点赞👍、关注💖、留言💌~ 今天是工作最后一天,认真撰写一下我的年终总结,以及今年我的所思所想。 ...
继续阅读 »

🍊作者简介: 18年毕业,至今在创业公司,国企,港企(也算是外企),中间有做过9个月的自由职业,但是失败返回职场,现在专心研究技术,目标一线大厂。


🍊支持作者: 点赞👍、关注💖、留言💌~



今天是工作最后一天,认真撰写一下我的年终总结,以及今年我的所思所想。


自由职业总结


在此之前我先捋一下时间线,我是我从2021年6月份决定出来自由职业,起初跟朋友一起合作做点小生意,后来因为一些意见不合,从2022年2月份就退出了,2月份到5月份也在做很多尝试,比如做剧本杀写作,自媒体卖书,接私单,但是经过考虑,做这些收入不稳定,而且回本周期比较长,有回款压力,而且之前创业的9个月里面,我也没赚到什么钱,倒是把自己的老本都吃光了,还透支了xy卡,还有一些wd,每个月都有还款压力。


所以在这里奉劝各位想做自由职业的朋友,如果不想打工,想出来自己创业,要三思啊,要起码满足以下几个条件:



1、有稳定发展的副业,而且副业的收入连续三个月以上超过主业收入

2、副业因为主业的影响而发展受限 3、自身有起码一年以上的周转资金,起码保证哪怕一年没收入也不至于饿死



而我很明显以上三点都不满足,到了后面实在没啥钱,业务也没有做起来,就动用了网贷,不过幸好及时止损,回归职场,现在细想,这是非常危险的,也是非常不理智的。


有条件千万不要dk创业,不要负z创业,到了后面心态真的绷不住,压力太大了,人很焦虑不说,事情还总办不好。



后来回来上班,第二个月领到第一笔工资,有时候摸鱼一天都有钱进账,多爽啊哈哈。


当然此次创业也给了一个教训就是尽量不要合伙创业,做成了还好,做不成就连朋友都真做不了,一地鸡毛,有机会好好讲一下这一年我自由职业的个人心得。


自由职业告一段落,现在进入职场时间。


回归职场


2022年6月到2022年12月这段时间也是比较动荡的。


不过也在意料之内,突然从自由职业回归到职场,还是会有阵痛期。


2022年6月-2022年9月,在一家小创业公司做前端负责人,薪资16k(我直接喊得,老板很爽快地答应了,怀疑是叫少了)。



这家公司技术栈是Vue2.x,业务有PC端应用,小程序应用,还有flutter开发桌面端。



但是因为技术生疏和对于业务方面不够娴熟,达不到老板的要求,9月8日被辞退了。


但是我没有气馁,心想要不再尝试一下自由职业吧,于是又花了14天时间去写剧本杀,想着靠剧本杀来翻盘,但是我的稿子被编剧无情打回来修改后,看着日进逼近的还款日期,还有自己手上不多的余粮,妈呀,立马又屁颠屁颠去准备面试,宝宝心里苦啊。



于是又火急火燎地边准备面试题边去面试,好在上天眷顾,10月22日入职了一家港企,也算是外企吧,薪资是16k,但是加班费奇高,就是我之前说的100元/小时。


因为公司是千人以上的大公司,所以业务线非常庞杂,技术栈也非常繁杂:



Vue3.0开发表单引擎 

React Native开发业务汇报APP 

Vue2.x+Electron开发桌面端应用 

Angular 

......



真可谓是前端大杂烩,不过眼下要还钱,虽然没有争取到涨薪,但起码有加班费,还好还好,再看一眼我的存款还有还款日期,没办法,就你了。


于是开始了疯狂卷模式,我在这家公司也是出了名的卷,以至于我现在离职快一个月了,公司还留存着我的光辉事迹......


为什么我又双叒离职了呢?



原因是我进去没多久,就由自愿加班转变成强制加班了,强制加班到9点半。


不过为了还钱,这点也可以接受吧。


不过最可怕的是,他们会给超出你工作能力的工作量,而且狂砍开发周期,比如我用react native从零开发一个系统,我原本估计要20天时间(保守一点),但是上层直接砍半,直接给10天!!


我艹,从入门到项目上线只给10天,没得办法,谁让我还在试用期,也不敢造次。


于是就开始跟另一个小伙伴开始摸天黑地的开发工作,连续10天都是凌晨1点才到家,第二天8点还得起床去上班。


然而10天根本不可能完整完成一个系统,我们连react native的基本知识都没搞懂,但是另外一个小伙伴说,尽力而为,实在不行就跑路。


听他这么说,我表面不说什么,内心那叫一个苦啊。


原来一个人有了负债就不再是你自己了,失去了那么多选择权,幸好这点负债对我来说压力不算太大,真想不懂那些有房贷车贷的人是怎么想的,那压力真的翻倍啊。


以后买房真的要慎之又慎!!



10天之后,我们两个人拼尽全力了,都还是没有办法按时上线,于是领导又给多了一周时间开发,并且放出狠话:



这一次要是再延期上线,就有人要被释放了!!



哎,没办法,再难受也要顶硬上。但是我转念一想,要是实在没办法完成,要拿我开刀,那怎么办??


不行,我不能做砧板上的鱼肉,我要选好退路,那就是继续去面试找下家,即使没办法上线他们要开掉我,我有offer在身,我也不需要担心那么多。


于是我从12月10日开始,屏蔽掉了现公司,开始了BOSS上海投之旅。


我当时是这么打算的,下一家公司要满足以下条件:



1、薪资必须要能够覆盖掉我的日常开支+还贷,还能存下一点钱抵抗后续风险 

2、至少稳定工作一年以上 

3、正常上下班,或者加班不多,多出来时间提升技术(技术真的跟不上了)



综上只有两种公司满足我的条件:



1、国企 

2、外企



有点讽刺,在大陆,最遵守劳动法的公司反而是外企。


但是面试我是不管那么多的,外面行情也不是那么好,但是幸运的是我比较注重简历包装,以及对于简历上可能问道的项目技术难点或者重点,甚至可能延伸出去的技术点,我都有做好非常严谨的准备,谁让我一路以来都在准备面试(其实是工作不稳定),所以还是很幸运在一周之内拿了不少offer,除了大厂(估计大厂嫌弃我简历太花了,没让我过,也可能是太菜了)


大厂,等我这波缓过来,一年以后或者两年以后我还是会冲的。


后来一周开发结束之后,急急忙忙上线,因为时间紧急,所以我们内部测试流程基本跑通就匆匆上线了,上线的当天测试测出60多个bug!!



企业微信被测试疯狂轰炸,我的另一个伙伴前几天跑路了,就只剩我一个人,有点难顶,于是领导又给我安排了另一个前端来帮忙,正好,等我把tapd上面的bug全部修复,二次测试通过之后,就甩锅给新来的前端,留下一纸技术交接文档还有离职申请,拍拍屁股去下家公司入职了,也算是对得起前公司了吧。



说实话,不是我扛不住压力,而是我真的不喜欢领导总是以释放,开除等等来给我们施压,我不是牛马,我也是人,是人就应该得到尊重!


万一我下次项目真的没办法上线,就把我开了,那我的处境就会非常被动了。


介绍一下我的新公司,大型的国企,流程正规,即使项目需求再赶也不至于把人给逼进绝路,正常上下班,大家都是到点走,有一次我稍微坐久一点,技术经理还过来提醒我没事可以早点走,劳逸结合,真正的人性化哈哈。


薪资也提高了一点,加班也是1:1,而且加班机会非常少,那多出来的时间,我可以有条不紊地提升技术。


一切都朝着好的方向发展,而且会越来越好。


说了那么多2022年,下面是我对于2023年的新年期望。


2023年新年期望


第一,当然是早日还清债务,现在的钱还不是我的,等还清贷款后,才是属于我的,起码现在我是这么认为的;


第二,从零开始重新钻研技术,这段时间也在根据自己的定位重新制定职业规划,后续会公布到这里;


经历过这次自由职业,我深刻地意识到,要想做成事,能力,经验,人脉,资本,缺一不可,而这些资源,都集中在大厂,只有去大厂,才可以完成自己的各项积累,即使进去之后,也不可以躺平,得过且过,要自己牢牢把握主动权。


所以2023年所做的一切都是为了进大厂做储备;


第三,当然是希望收获一段有结果的感情啦,虽然不知道是不是你,但是我还是会用心去经营,不辜负任何一个人,毕竟你有一点很吸引我,就是你身上闪烁着女性独立之光;


第四,就是把自己的技术沉淀到公众号,视频号,小红书,做自媒体输出,要是能够做成像月哥,神光,卡颂这种小网红就更好了哈哈,当然做这些注定前期是不赚钱的,降低期望值,逐步提升个人影响力,赚以后的钱吧。


而且我这个人天生脸皮厚,有旺盛的表达欲,又充满了乐观主义色彩,尽管去做吧,做技术输出,这绝对是稳赚不赔的买卖。


祝大家新年快快乐,万事如意,早日实现自己的人生目标!


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

有多少人忘记了gb2312

本想新周摸新鱼,却是早早入坑。看到群友千元求解一个叫当当网的索引瞬间来了兴趣 网站地址,大体一看没什么特别的地方就是一个关键字编码问题,打眼一看url编码没跑直接拿去解码无果 -有点惊讶看似url编码实则url编码只是这,滋滋滋... 有点东西,开始...
继续阅读 »

本想新周摸新鱼,却是早早入坑。看到群友千元求解一个叫当当网的索引瞬间来了兴趣



  1. 网站地址,大体一看没什么特别的地方就是一个关键字编码问题,打眼一看url编码没跑直接拿去解码无果


image.png


image.png
-有点惊讶看似url编码实则url编码只是这,滋滋滋...
VeryCapture_20230227174156.gif


有点东西,开始抓包,断点,追踪的逆向之路
VeryCapture_20230227170913.gif
2. 发现是ajax加载(不简单呀纯纯的吊胃口)先来一波关键字索引(keyword)等一系列基操轻而易举的找到了他
VeryCapture_20230227171325.gif


从此开始走向了一条不归路,经过一上午的时间啥也没追到,午休之后继续战斗,经过了一两个半小时+三支长白山牌香烟的努力终于


VeryCapture_20230227173627.gif
VeryCapture_20230227173457.gif


VeryCapture_20230227171715.gif


cihui = '哈哈哈'
js = open("./RSAAA.js", "r", encoding="gbk", errors='ignore')
line = js.readline()
htmlstr = ''
while line:
htmlstr = htmlstr + line
line = js.readline()
ctx = execjs.compile(htmlstr)
result = ctx.call('invokeServer', cihui)
print(result)

const jsdom = require("jsdom");
const {JSDOM} = jsdom;
const dom = new JSDOM('<head>\n' +
' <base href="//search.dangdang.com/Standard/Search/Extend/hosts/">\n' +
'<link rel="dns-prefetch" href="//search.dangdang.com">\n' +
'<link rel="dns-prefetch" href="//img4.ddimg.cn">\n' +
'<title>王子-当当网</title>\n' +
'<meta http-equiv="Content-Type" content="text/html; charset=GB2312">\n' +
'<meta name="description" content="当当网在线销售王子等商品,并为您购买王子等商品提供品牌、价格、图片、评论、促销等选购信息">\n' +
'<meta name="keywords" content="王子">\n' +
'<meta name="ddclick_ab" content="ver:429">\n' +
'<meta name="ddclick_search" content="key:王子|cat:|session_id:0b69f35cb6b9ca3e7dee9e1e9855ff7d|ab_ver:G|qinfo:119800_1_60|pinfo:_1_60">\n' +
'<link rel="canonical" href="//search.dangdang.com/?key=%CD%F5%D7%D3\&amp;act=input">\n' +
' <link rel="stylesheet" type="text/css" href="css/theme_1.css">\n' +
' <!--<link rel="Stylesheet" type="text/css" href="css/model/home.css" />-->\n' +
' <link rel="stylesheet" type="text/css" href="css/model/search_pub.css?20211117"> \n' +
'<style>.shop_button {height: 0px;}.children_bg01 a {\n' +
'margin-left: 0px;\n' +
'padding-left: 304px;\n' +
'width: 630px;\n' +
'}\n' +
'.children_bg02 a {\n' +
'margin-left: 0px;\n' +
'padding-left: 304px;\n' +
'width: 660px;\n' +
'}\n' +
'.children_bg03 a {\n' +
'margin-left: 0px;\n' +
'padding-left: 304px;\n' +
'width: 660px;\n' +
'}\n' +
'.narrow_page .children_bg01 a{\n' +
'width: 450px;\n' +
'}\n' +
'.narrow_page .children_bg02 a{\n' +
'width: 450px;\n' +
'}\n' +
'.narrow_page .children_bg03 a{\n' +
'width: 450px;\n' +
'}.price .search_e_price span {font-size: 12px;font-family: 微软雅黑;display: inline-block;background-color: #739cde;color: white;padding: 2px 3px;line-height: 12px;border-radius: 2px;margin: 0 4px 0 5px;}\n' +
'.price .search_e_price:hover {text-decoration: none;}</style> <link rel="stylesheet" href="http://product.dangdang.com/js/lib/layer/3.0.3/skin/default/layer.css?v=3.0.3.3303" id="layuicss-skinlayercss"><script id="temp_script" type="text/javascript" src="//schprompt.dangdang.com/suggest_new.php?keyword=好好&amp;pid=20230227105316030114015279129895799&amp;hw=1&amp;hwps=12&amp;catalog=&amp;guanid=&amp;0.918631418357919"></script><script id="json_script" type="text/javascript" src="//static.dangdang.com/js/header2012/categorydata_new.js?20211105"></script></head>');

window = dom.window;
document = window.document;
function invokeServer(url) {

var scriptOld = document.getElementById('temp_script');
if(scriptOld!=null && document.all)
{
scriptOld.src = url;
return script;
}
var head=document.documentElement.firstChild,script=document.createElement('script');
script.id='temp_script';
script.type = 'text/javascript';
script.src = url;
if(scriptOld!=null)
head.replaceChild(script,scriptOld);
else
head.appendChild(script);
return script
}



  1. 完事!当我以为都要结束了的时候恍惚直接看到了源码中的gb2312突然想起了之前做的一个萍乡房产网的网站有过类似经历赶快去尝试结果我**
    image.png
    image.png
    VeryCapture_20230227172815.gif




  2. 总结:提醒各位大佬在逆向之路中还是要先从基操开始,没必要一味的去搞攻克扒源码,当然还是要掌握相对全面的内容,其实除了个别大厂有些用些贵的东西据说某数5要20个W随着普遍某数不知道那些用了20w某数的大厂心里是什么感觉或许并不在乎这点零头哈哈毕竟是大厂,小网站的反扒手段并不是很难,俗话说条条大道通北京。


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

Android事件冲突解决-悬浮窗拖拽处理

需求场景 最近项目中要做一个音乐播放悬浮按钮的功能,最终实现效果如下: 问题暴露 悬浮窗布局文件就不放了,就是水平LinearLayout里面放几个ImageView。 做的过程当中遇到一个问题,就是悬浮窗是可以任意拖拽的,悬浮窗里面的按钮是可以点击的,比如...
继续阅读 »

需求场景


最近项目中要做一个音乐播放悬浮按钮的功能,最终实现效果如下:



问题暴露


悬浮窗布局文件就不放了,就是水平LinearLayout里面放几个ImageView


做的过程当中遇到一个问题,就是悬浮窗是可以任意拖拽的,悬浮窗里面的按钮是可以点击的,比如暂停,下一曲,关闭悬浮窗等。


按常规思路,先给整个悬浮窗setOnTouchListener(),然后再给你里面的按钮setOnClickListener(),点击运行,结果发现,点击事件是可以响应,拖拽也没问题,但是当手指放在ImageView上拖拽时,onTouchListener事件无法响应。


此时第一感觉就是setOnTouchListener()setOnClickListener()冲突了,需要解决一下冲突。无奈自己对Android事件分发消费机制一直都是一知半解的,一般都是出了问题需要解决,第一时间先百度,没有解决方案就只能去研究Android事件分发消费机制了,但是研究完也都是懵懵懂懂的,今天就决定把这个难点彻底消化掉。


主要研究了这篇文章Android事件分发消费机制,然后对照着写了个demo,一一去验证,加深了自己的理解,最后终于解决了我的问题。


解决思路


先说下解决思路,自定义LinearLayout,当手指处于滑动时,直接拦截事件,交给自己的onTouchEvent处理即可,核心代码如下:


/**
* @author:Jason
* @date:2021/8/24 19:49
* @email:1129847330@qq.com
* @description:可拖拽的LinearLayout,解决子View设置OnClickListener之后无法拖拽的问题
*/
class DraggerbleLinearLayout : LinearLayout {
constructor(context: Context, attr: AttributeSet) : this(context, attr, 0)
constructor(context: Context, attr: AttributeSet, defStyleAttr: Int) : this(context, attr, defStyleAttr, 0)
constructor(context: Context, attr: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attr, defStyleAttr, defStyleRes)

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_MOVE -> {
return true
}
}
return super.onInterceptTouchEvent(ev)
}
}

很简单,就是在onInterceptTouchEvent()里面拦截move事件即可,这里你也可以重写onTouchEvent(),在里面实现拖拽功能,但是这样就固定死了,所以我选择在外面setOnTouchListener(),需要拖拽功能时才去实现


    /**
* 创建悬浮窗
*/
@SuppressLint("ClickableViewAccessibility")
private fun showWindow() {
//获取WindowManager
windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
val outMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(outMetrics)
var layoutParam = WindowManager.LayoutParams().apply {
/**
* 设置type 这里进行了兼容
*/
type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
WindowManager.LayoutParams.TYPE_PHONE
}
format = PixelFormat.RGBA_8888
flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
//位置大小设置
width = WRAP_CONTENT
height = WRAP_CONTENT
gravity = Gravity.LEFT or Gravity.TOP
//设置剧中屏幕显示
x = outMetrics.widthPixels / 2 - width / 2
y = outMetrics.heightPixels / 2 - height / 2
}
//在这里设置触摸监听,以实现拖拽功能
floatRootView?.setOnTouchListener(ItemViewTouchListener(layoutParam, windowManager))
// 将悬浮窗控件添加到WindowManager
windowManager.addView(floatRootView, layoutParam)
isAdded = true
}

主要是这行代码


//在这里设置触摸监听,以实现拖拽功能
floatRootView?.setOnTouchListener(ItemViewTouchListener(layoutParam, windowManager))

ItemViewTouchListener.kt文件内容


/**
* @author:Jason
* @date: 2021/8/23 19:27
* @email:1129847330@qq.com
* @description:
*/
class ItemViewTouchListener(val layoutParams: WindowManager.LayoutParams, val windowManager: WindowManager) : View.OnTouchListener {
private var lastX = 0.0f
private var lastY = 0.0f
override fun onTouch(view: View, event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
//这里接收不到Down事件,不要在这里写逻辑
}
MotionEvent.ACTION_MOVE -> {
//重写LinearLayout的OnInterceptTouchEvent之后,这里的Down事件会接收不到,所以初始位置需要在Move事件里赋值
if (lastX == 0.0f || lastY == 0.0f) {
lastX = event.rawX
lastY = event.rawY
}
val nowX: Float = event.rawX
val nowY: Float = event.rawY
val movedX: Float = nowX - lastX
val movedY: Float = nowY - lastY

layoutParams.apply {
x += movedX.toInt()
y += movedY.toInt()
}
//更新悬浮球控件位置
windowManager?.updateViewLayout(view, layoutParams)
lastX = nowX
lastY = nowY
}
MotionEvent.ACTION_UP -> {
lastX = 0.0f
lastY = 0.0f
}
}
return true
}
}

这里有一点需要注意的是,重写了LinearLayoutonInterceptTouchEvent()后会导致setOnTouchListener()里面的ACTION_DOWN事件接收不到,所以不要在down事件里面写逻辑。然后onTouch一定要返回true,表示要消费事件,否则当拖拽非ImageView区域时会拖不动。


好了,花了一整天,就解决了这个小问题。


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

NativeBridge:我在Pub上发布的第一个插件

前沿 坦白地说,在发布 native_bridge 之前,自己从没有想过会在 pub 上发布一款自己开发的 Flutter 插件。一方面是觉得自己水平有限,很难产出比较有实用价值的内容。虽然之前有写过 Plugin 开发相关的文章,但那也只是用来自我学习的 D...
继续阅读 »

前沿


坦白地说,在发布 native_bridge 之前,自己从没有想过会在 pub 上发布一款自己开发的 Flutter 插件。一方面是觉得自己水平有限,很难产出比较有实用价值的内容。虽然之前有写过 Plugin 开发相关的文章,但那也只是用来自我学习的 Demo,对大家来说没有太多的应用价值。另一方面也是因为对 pub 的发布流程非常陌生,尤其对大陆开发者来说还会遇到一些额外的问题。


而这次能够促使我去发布 NativeBridge 插件,主要还是之前的 NativeBridge 系列文章在掘金的受欢迎度较高,让我感受到了它存在的价值。另一个原因是之前对掘友的承诺,之前掘友希望 NativeBridge 能够适配 webview_flutter ^4.0.1 以上的版本。


WX20230226-151759.png


虽然距离上次的回复已经过去了一个多月,但我最终还是完成了许下的承诺^ ^。


NativeBridge 插件


NativeBridge 是基于 webview_flutter 插件来实现 App端 和 H5端 JS 互调能力的插件。 其详细功能说明请跳转 Readme 查看,这里不做过多的介绍。


NativeBridge 目前已成功发布到 pub,需要使用时直接引入即可。


WX20230226-152710.png


版本更新


这里简述下插件的版本更新内容:


v0.0.2 版本


1. 优化了消息回复后还会导致接收端再次发一条确认消息的问题


原来当 App 收到了 H5 回复的消息后,还会再发送一条消息确认消息。这是没有必要的,这里进行了优化。


通过在消息实体 Message 中新增 isResponseFlag 字段来进行区分,对于 isResponseFlagtrue 的消息,将不再发送确认消息。


if (isResponseFlag) {
// 是返回的请求消息,则处理H5回调的值
NativeBridgeHelper.receiveMessage(messageJson);
} else {
...
}

2. 对于未识别的 Api 新增容错机制处理


随着 App版本 的迭代,可能出现某个新的 Api 老的版本无法支持的情况,对于之前的版本来说,当 Api 不匹配时会直接丢弃消息,等待超时机制处理。这会导致发起端出现 await 情况,直到超时触发回调。这是不适宜的,这次增加了兼容处理。


// 不是返回的请求消息,处理H5端的请求
var callMethod = callMethodMap[messageItem.api];
if (callMethod != null) {
// 有相应的JS方法,则处理
...
} else {
// 若没有对应方法,则返回null,避免低版本未支持Api阻塞H5
messageItem.data = null;
}

v1.0.0版本


1. 适配webview_flutter ^4.0.5版本


目前插件引入的 webview_flutter 版本为: ^4.0.5,后续也会随着 webview_flutter 插件的迭代而升级,直到其能力被官方取代为止。


WX20230226-155038.png


2. 对实现方式进行重构


由于新版本的 webview_flutter 将大部分的 WebView 操作功能移入 WebViewController 处理,因此我们的 NativeBridge 插件也同步进行调整。


现在使用只需要新增 AppBridgeController 继承 NativeBridgeController 实现其对应方法即可正常使用。


class AppBridgeController extends NativeBridgeController {
AppBridgeController(WebViewController controller) : super(controller);

/// 指定JSChannel名称
@override
get name => "nativeBridge";

@override
Map<String, Function?> get callMethodMap => <String, Function?>{
// 版本号
"getVersionCode": (data) async {
return await AppUtil.getVersion();
},
...
};
}

// 初始化AppBridgeController
_appBridgeController = AppBridgeController(_controller);

总结


对于还在使用 webview_flutter 3.0+ 版本的项目,引用 ^0.0.2版本:


native_bridge: ^0.0.2

对于使用 webview_flutter 4.0+ 版本的项目,引用 ^1.0.0版本:


native_bridge: ^1.0.0

感悟


NativeBridge插件的成功发布,这个过程中也让我受益良多。


开始学会从 SDK 开发者的角度去思考问题,尝试思考如何简化插件的使用、丰富插件的能力,更关注插件的内在结构和对外的协议。也了解到在开源项目中包括开源协议、pub 的 scores 标准、README、CHANGELOG的定义等。


这也是自我的一次成长,愿你我一起共勉!


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

Android通知栏适配调研-放弃RemoteViews

问题描述: 项目的通知栏的RemoteViews背景图有些厂商颜色不对,需要一个适配方案,来适配所有机型。 项目现状: 目前的方案是,创建一个新的通知,通过这个通知来获取底色。 其中有一个字段是获取是否是暗黑模式,这段代码是先用按位或忽略掉透明度,系统的字体...
继续阅读 »

问题描述:


项目的通知栏的RemoteViews背景图有些厂商颜色不对,需要一个适配方案,来适配所有机型。


项目现状:


目前的方案是,创建一个新的通知,通过这个通知来获取底色。


image.png


其中有一个字段是获取是否是暗黑模式,这段代码是先用按位或忽略掉透明度,系统的字体颜色不会有特别高的同名度,所以这里理论上没什么问题,然后计算两个三维向量的距离,距离越近,颜色越接近。


image.png


这里是有误差的,目标向量距离灰色向量rgb(127.5,127.5,127.5)的最大值约为220,而不是180,就会造成纯白色rgb(255,2555,255)的标题字体颜色,理应是黑色背景才能看到,但却被识别成非暗黑模式。


image.png


误判了暗黑模式,这里的背景就颜色就会出问题了。


image.png


目前项目的解决方案是通过设置RemoteViews的background颜色去做,这里需要适配的问题由RemoteViews实现机制产生:RemoteViews被设计具有跨进城通信的属性,通过Binder传递到SystemService进程,根据它的包名来拿相应的应用的资源,为了提高效率,系统没有直接通过Binder去支持所有的View和View操作,那这里对普通view生效的xml属性,很有可能厂商对系统修改源码后没有完全的适配,可能就不能对其全部生效。


比如对android:background属性进行设置在部分机型上就不会生效,但下面这段代码可以。


RemoteView?.setInt(R.id.ll_root, "setBackgroundColor", ContextCompat.getColor(context, R.color.color_367AF6))

接下来来看项目如何设置颜色。


image.png


然后来看拿到暗黑模式的布尔字段后,会设置为#FFFFFFFF/#00000000这两个颜色,这两个颜色都是有问题的,首先暗黑模式情况下,#00000000(百分百透明的黑色)不一定会正确解析为透明色,类似问题为启动icon解析时,透明色解析为黑色背景,具体还要观察各厂商系统对源码的修改。


image.png


#FFFFFFFF这个颜色也是不对的,不是每个通知栏的背景都是白色,而且这个颜色也不一定会作为背景色,下面鸿蒙系统上就没有生效为白色。


image.png


来观察下不同时期的通知样式,可以发现不是每个时期都是固定颜色底色,有白有黑有灰,那就要动态的获取背景色来设置RemoteVies的背景色,下图以安卓1-7为示例。


image.png


接着还要考虑不同的系统版本对Notification的改动,如安卓8.0以上加入了Channel这一特性,项目的通知最后是要有一步渠道检查的。这里或许要额外对安卓13的有权限上的适配。


image.png


同时如果要考虑通知栏高度的问题的话,以安卓12为例,自定义通知的显示区域比安卓11有调整,折叠态高度上限为64dp,展开时高度上限为256dp,且系统强制显示通知的小图标。所以要考虑到ui层面对高度是否有要求,再来适配各个版本的RemoteViews的高度。下图为展开时示例。


image.png


以上,这个方案做不到适合所有机型,要适配所有机型确实是不是个简单的事情。


尝试过程:


核心问题为RemoteViews的背景色不能完美适配所有厂商。首先原安卓系统(非厂商自定义)不需要对其有特定的设置,自动跟随主题颜色;厂商自定义安卓系统,则各不相同,如小米则不会自动更改RemoteViews的颜色。


首先尝试获取背景色,我开始的尝试方式是沿用之前的方式,即build一个Notification,通过遍历其中的text,发现不是所有的都能拿到,参考其他文章列出的类似的表格如下:



虎牙项目目前的方案是,如果没拿到字体,默认就是字体为黑色,这个方法安卓7.0之后就失效了。安卓7.0+修改了Notification,采用 @android:color/primary_text_dark已经获取不到颜色值了。


那既然不能完美解决,这里我使用反编译工具,观察各厂商的逆向工程(以小米为例),这里需要花费大量的时间来阅读源码(前公司的代码真的看不懂= =),最后看到其他文章给出一个方案是在manifest加上这一句才会生效。


<item name="android:forceDarkAllowed" tools:targetApi="q">false</item>
复制代码

以上,RemoteViews的确拥有高自定义ui的特性,但很难找到一个方案来适配所有国内厂商的机型,因为Google设计通知栏,或者说其他任何的系统组件的伊始就没考虑过自定以rom的因素。



解决方案:


1.  不使用RemoteViews,改用其他方案解决


2.  使用RemoteViews


先说第一个,如果仅仅是虎牙目前的通知栏用途的话,确实没有使用复杂ui,使用RemoteViews的收益好像不足以掩盖bug多这个缺点。直播间的通知栏提醒完全可以用addAction方法来代替:


image.png


其他简单场景的情况下:



第二个,如果坚持使用RemoteViews,基本上网络上大部分的相关的文章我都翻过了,我也没看到业界更好的办法,大概只能反编译去看看抖音和微信是怎么做的吧。


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

如何开发一个人人爱的组件?

.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:16px;overflow-x:hidden;color:#252933}.markdown-bod...
继续阅读 »

组件,是前端最常打交道的东西,对于 React、Vue 等应用来说,万物皆组件毫不为过。


有些工作经验的同学都知道,组件其实也分等级的,有的组件可以被上万开发者复用,有些组件就只能在项目中运行,甚至挪动到自己的另外一个项目都不行。


如何考察一个前端的水平,首先可以看看他有没有对团队提供过可复用的组件,一个前端如果一直只能用自己写的东西,或者从没有对外提供过可复用的技术,那么他对于一个团队的贡献一定是有限的。


所以开始写一个能开放的组件应该考虑些什么呢?🤔


本篇文章类似一个菜谱,比较零碎的记录一些组件设计的内容,我分别按照 1~5 ⭐️ 区分其重要性。


意识


首先在意识层面,我们需要站在使用组件的开发者角度来观察这个组件,所以下面几点需要在组件开发过程中种在意识里面:



  1. 我应该注重 TypeScript API 定义,好用的组件 API 都应该看上去 理所应当绝不多余

  2. 我应该注重 README 和 Mock ,一个没有文档的组件 = 没有,最好不要使用 link 模式去开发组件

  3. 我不应引入任何副作用依赖,比如全局状态(Vuex、Redux),除非他们能自我收敛

  4. 我在开发一个开放组件,以后很有可能会有人来看我的代码,我得写好点


接口设计


好的 Interface 是开发者最快能搞清楚组件入参的途径,也是让你后续开发能够有更好代码提示的前提。


type Size = any; // 😖 ❌

type Size = string; // 🤷🏻‍♀️

type Size = "small" | "medium" | "large"; // ✅

`


DOM 属性(⭐️⭐️⭐️⭐️⭐️)

组件最终需要变成页面 DOM,所以如果你的组件不是那种一次性的,请默认顺手定义基础的 DOM 属性类型。className 可以使用 classnames 或者 clsx 处理,别再用手动方式处理 className 啦!


export interface IProps {
className?: string;
style?: React.CSSProperties;
}

对于内部业务来说,还会有 data-spm 这类 dom 属性,主要用于埋点上报内容,所以可以直接对你的 Props 类型做一个基础封装:


export type CommonDomProps = {
className?: string;
style?: React.CSSProperties;
} & Record<`data-${string}`, string>

// component.tsx
export interface IComponentProps extends CommonDomProps {
...
}

// or

export type IComponentProps = CommonDomProps & {
...
}

类型注释(⭐️⭐️⭐️)


  1. export 组件 props 类型定义

  2. 为组件暴露的类型添加 规范的注释


export type IListProps{
/**
* Custom suffix element.
* Used to append element after list
*/

suffix?: React.ReactNode;
/**
* List column definition.
* This makes List acts like a Table, header depends on this property
* @default []
*/

columns?: IColumn[];
/**
* List dataSource.
* Used with renderRow
* @default []
*/

dataSource?: Array<Record<string, any>>;
}

上面的类型注释就是一个规范的类型注释,清晰的类型注释可以让消费者,直接点击进入你的类型定义中查看到关于这个参数的清晰解释。


同时这类符合 jsdoc 规范的类型注释,也是一个标准的社区规范。利用 vitdoc 这类组件 DEMO 生成工具也可以帮你快速生成美观的 API 说明文档。



Tips:如果你非常厌倦写这些注释,不如试试著名的 AI 代码插件:Copilot ,它可以帮你快速生成你想要表达的文字。



以下是 ❌ 错误示范:


  toolbar?: React.ReactNode; // List toolbar.

// 👇🏻 Columns
// defaultValue is "[]"
columns?: IColumns[];

组件插槽(⭐️⭐️⭐️)

对于一个组件开发新手来说,往往会犯 string 类型替代 ReactNode 的错误。


比如要对一个 Input 组件定义一个 label 的 props ,许多新手开发者会使用 string 作为 label 类型,但这是错误的。


export type IInputProps = {
label?: string; // ❌
};

export type IInputProps = {
label?: React.ReactNode; // ✅
};

遇到这种类型时,需要意识到我们其实是在提供一个 React 插槽类型,如果在组件消费中仅仅是让他展示出来,而不做其他处理的话,就应当使用 ReactNode 类型作为类型定义。



受控 与 非受控(⭐️⭐️⭐️⭐️⭐️)

如果要封装的组件类型是 数据输入 的用途,也就是存在双向绑定的组件。请务必提供以下类型定义:


export type IFormPropsstring> = {
value?: T;
defaultValue?: T;
onChange?: (value: T, ...args) => void;
};

并且,这类接口定义不一定是针对 value, 其实对于所有有 受控需求 的组件都需要,比如:


export type IVisibleProps = {
/**
* The visible state of the component.
* If you want to control the visible state of the component, you can use this property.
* @default false
*/

visible?: boolean;
/**
* The default visible state of the component.
* If you want to set the default visible state of the component, you can use this property.
* The component will be controlled by the visible property if it is set.
* @default false
*/

defaultVisible?: boolean;
/**
* Callback when the visible state of the component changes.
*/

onVisibleChange?: (visible: boolean, ...args) => void;
};

具体原因请查看: 《受控组件和非受控组件》
消费方式推荐使用:ahooks useControllableValue


表单类常用属性(⭐️⭐️⭐️⭐️)

如果你正在封装一个表单类型的组件,未来可能会配合 antd/ fusionForm 组件来消费,以下这些类型定义你可能会需要到:


export type IFormProps = {
/**
* Field name
*/

name?: string;

/**
* Field label
*/

label?: ReactNode;

/**
* The status of the field
*/

state?: "loading" | "success" | "error" | "warning";

/**
* Whether the field is disabled
* @default false
*/

disabled?: boolean;

/**
* Size of the field
*/

size?: "small" | "medium" | "large";

/**
* The min value of the field
*/

min?: number;

/**
* The max value of the field
*/

max?: number;
};

选择类型(⭐️⭐️⭐️⭐️)

如果你正在开发一个需要选择的组件,可能以下类型你会用到:


export interface ISelectionextends object = Record<string, any>> {
/**
* The mode of selection
* @default 'multiple'
*/

mode?: "single" | "multiple";
/**
* The selected keys
*/

selectedRowKeys?: string[];
/**
* The default selected keys
*/

defaultSelectedRowKeys?: string[];
/**
* Max count of selected keys
*/

maxSelection?: number;
/**
* Whether take a snapshot of the selected records
* If true, the selected records will be stored in the state
*/

keepSelected?: boolean;
/**
* You can get the selected records by this function
*/

getProps?: (
record: T,
index: number
) =>
{ disabled?: boolean; [key: string]: any };
/**
* The callback when the selected keys changed
*/

onChange?: (
selectedRowKeys: string[],
records?: Array,
...args: any[]
) =>
void;
/**
* The callback when the selected records changed
* The difference between `onChange` is that this function will return the single record
*/

onSelect?: (
selected: boolean,
record: T,
records: Array,
...args: any[]
) =>
void;
/**
* The callback when the selected all records
*/

onSelectAll?: (
selected: boolean,
keys: string[],
records: Array,
...args: any[]
) =>
void;
}

另外,单选与多选存在时,组件的 value 可能会需要根据下传的 mode 自动变化数据类型。


比如,在 Select 组件中就会有以下区别:


mode="single" -> value: string | number
mode="multiple" -> value: string[] | number[]

所以对于需要 多选、单选 的组件来说,value 的类型定义会有更多区别。


对于这类场景可以使用 Merlion UI - useCheckControllableValue 进行抹平。


组件设计


服务请求(⭐️⭐️⭐️⭐️⭐️)

这是一个在业务组件设计中经常会遇到的组件设计,对于很多场景来说,或许我们只是需要替换一下请求的 url ,于是便有了类似下面这样的 API 设计:


export type IAsyncProps {
requestUrl?: string;
extParams?: any;
}

后面接入方增多后,出现了后端的 API 结果不符合组件解析逻辑,或者出现了需要请求多个 API 组合后才能得到组件所需的数据,于是一个简单的请求就出现了以下这些参数:


export type IAsyncProps {
requestUrl?: string;
extParams?: any;
beforeUpload?: (res: any) => any
format?: (res: any) => any
}

这还只是其中一个请求,如果你的业务组件需要 2 个、3 个呢?组件的 API 就会变得越来越多,越来越复杂,这个组件慢慢的也就变得没有易用性 ,也慢慢没有了生气。


对于异步接口的 API 设计最佳实践应该是:提供一个 Promise 方法,并且详细定义其入参、出参类型。


export type ProductList = {
total: number;
list: Array<{
id: string;
name: string;
image: string;
...
}>
}
export type AsyncGetProductList = (
pageInfo: { current: number; pageSize: number },
searchParams: { name: string; id: string; },
) =>
Promise<ProductList>;


export type IComponentProps = {
/**
* The service to get product list
*/

loadProduct?: AsyncGetProductList;
}

通过这样的参数定义后,对外只暴露了 1 个参数,该参数类型为一个 async 的方法。开发者需要下传一个符合上述入参和出参类型定义的函数。


在使用时组件内部并不关心请求是如何发生的,使用什么方式在请求,组件只关系返回的结果是符合类型定义的即可。


这对于使用组件的开发者来说是完全白盒的,可以清晰的看到需要下传什么,以及友好的错误提示等等。


Hooks(⭐️⭐️⭐️⭐️⭐️)

很多时候,或许你不需要组件!


对于很多业务组件来说,很多情况我们只是在原有的组件基础上封装一层浅浅的业务服务特性,比如:



  • Image Uploader:Upload + Upload Service

  • Address Selector: Select + Address Service

  • Brand Selector: Select + Brand Service

  • ...


而对于这种浅浅的胶水组件,实际上组件封装是十分脆弱的。


因为业务会对 UI 有各种调整,对于这种重写成本极低的组件,很容易导致组件的垃圾参数激增。


实际上,对于这类对服务逻辑的状态封装,更好的办法是将其封装为 React Hooks ,比如上传:


export function Page() {
const lzdUploadProps = useLzdUpload({ bu: "seller" });

return <Upload {...lzdUploadProps} />;
}

这样的封装既能保证逻辑的高度可复用性,又能保证 UI 的灵活性。


Consumer(⭐️⭐️⭐️)

对于插槽中需要使用到组件上下文的情况,我们可以考虑使用 Consumer 的设计进行组件入参设计。


比如 Expand 这个组件,就是为了让部分内容在收起时不展示。



对于这种类型的组件,明显容器内的内容需要拿到 isExpand 这个关键属性,从而决定索要渲染的内容,所以我们在组件设计时,可以考虑将其设计成可接受一个回调函数的插槽:


export type IExpandProps = {
children?: (ctx: { isExpand: boolean }) => React.ReactNode;
};

而在消费侧,则可以通过以下方式轻松消费:


export function Page() {
return (
<Expand>
{({ isExpand }) => {
return isExpand ? <Table /> : <AnotherTable />;
}}
Expand>

);
}

文档设计


package.json(⭐️⭐️⭐️⭐️⭐️)

请确保你的 repository是正确的仓库地址,因为这里的配置是很多平台溯源的唯一途径,比如: github.com



请确保 package.json 中存在常见的入口定义,比如 main \ module \ types \ exports,以下是一个标准的 package.json 的示范:


{
"name": "xxx-ui",
"version": "1.0.0",
"description": "Out-of-box UI solution for enterprise applications from B-side.",
"author": "yee.wang@xxx.com",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
}
},
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/cjs/index.d.ts",
"repository": {
"type": "git",
"url": "git@github.com:yee94/xxx.git"
}
}

README.md(⭐️⭐️⭐️⭐️)

如果你在做一个库,并希望有人来使用它,请至少为你的库提供一段描述,在我们的脚手架模板中已经为你生成了一份模板,并且会在编译过程中自动加入在线 DEMO 地址,但如果可以请至少为它添加一段描述。


这里的办法有很多,如果你实在不知道该如何写,可以找一些知名的开源库来参考,比如 antd \ react \ vue 等。


还有一个办法,或许你可以寻求 ChatGPT 的帮助,屡试不爽😄。


作者:YeeWang
来源:juejin.cn/post/7189158794838933565
收起阅读 »

Vue2 Diff 算法

.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:16px;overflow-x:hidden;color:#252933}.markdown-bod...
继续阅读 »

Diff 算法


写在前面


因为之前看面试直播也经常问到 Diff 算法,然后作者本人用 Vue2 比较多,所以打算研究一下 Vue2 的 Diff 算法,其实很早就想学的,但是可能因为感觉 Diff 算法比较深奥,就一直拖着没学,但是最近在准备面试,就想着迟早都要学的,趁这个机会把 Diff 算法搞懂吧 🧐,作者就花了一天的时间研究了一下,可能没有完全理解 Diff 算法的精髓,请各位见谅。



💡 这个其实算作者的学习笔记,而且作者水平有限,改文章仅代表作者个人观点,如果有错误可以评论区指出来,会不断完善;同时本文很长,所以请读者们有耐心的看完,看完后作者相信你们会对 Diff 算法有更深的了解。本人觉得本文比目前网上讲解 Diff 算法的大部分文章要更好,因为本文从问题出发,教会大家如何思考,而不是直接从答案出发,就像读答案一样,这样感觉没什么意思,本文一步一步的引导大家去感受 Diff 算法的精妙,同时最后也做了一下小实验,让大家对 Diff 算法有更加直观的感受 🎉。



为什么要用 Diff 算法


虚拟 DOM


因为 Vue2 底层是用虚拟 DOM 来表示页面结构的,虚拟 DOM其实就是一个对象,如果想知道怎么生成的,其实大概流程就是:



  • 首先解析模板字符串,也就是 .vue 文件

  • 然后转换成 AST 语法树

  • 接着生成 render 函数

  • 最后调用 render 函数,就能生成虚拟 DOM


最小量更新


其实框架为了性能才使用的虚拟 DOM,因为 js 生成 DOM 然后展示页面是很消耗性能的,如果每一次的更新都把整个页面重新生成一遍那体验肯定不好,所以需要找到两个页面中变化的地方,然后只要把变化的地方用 js 更新 (可能是增加、删除或者更新) 一下就行了,也就是最小量更新。
那么怎么实现最小量更新呢?那么就要用 Diff 算法了,那么 Diff 算法对比的到底是什么呢?可能这是刚学 Diff 算法比较容易误解的地方,其实比对的是新旧虚拟 DOM,所以 Diff 算法就是找不同,找到两次虚拟 DOM 的不同之处,然后将不同反应到页面上,这就实现了最小量更新,如果只更新变化的地方那性能肯定更好。


页面更新流程


其实这个比较难解释,作者也就大致说一下,学了 Vue 的都知道这个框架的特点之一就有数据响应式,什么是响应式,也就是数据更新页面也更新,那么页面是怎么知道自己要更新了呢?其实这就是这个框架比较高明的地方了,大致流程如下:



  • 之前也说了会运行 render 函数,那么运行 render 函数的时候会被数据劫持,也就是进入 Object.definePropertyget,那么在这里收集依赖,那么是谁收集依赖呢?是每个组件,每个组件就是一个 Watcher,会记录这个组件内的所有变量 (也就是依赖),当然每个依赖 (Dep) 也会收集自己所在组件的 Watcher;

  • 然后当页面中的数据发生变化,那么就会出发 Object.definePropertyset,在这个函数里面数据就会通知每个 Watcher 更新页面,然后每个页面就会调用更新方法,这个方法就是 patch

  • 接着,就要找到两个页面之间的变化量,那么就要用到 Diff 算法了

  • 最后找到变化量后就可以进行更新页面了



其实是边找边更新的,为了让大家理解容易就将这两个部分分开了



Diff 算法简单介绍


面试问到 Diff 算法是什么,大家肯定会说两句,比如 头头、尾尾、尾头、头尾深度优先遍历(dfs)同层比较 类似这些话语,虽然能说一两句其实也是浅尝辄止。
其实作者看了 CSDN 上发的关于 Diff 算法的文章,就是阅读量很高的文章,作者觉得他也没讲明白,可能他自己没明白,或者自己明白了但是没讲清楚,那么作者会用自己的感悟和大家分享一下。


Diff 算法的前提


为了让大家能够了解清楚,这里先说明一下函数调用流程:



  • patch

  • patchVnode

  • updateChildren


Diff 算法的 前提 这个是很重要的,可能大家会问什么是前提?不就是之前说的那些比较嘛?说的没错但也不对,因为 Diff 算法到达之前说的 头头、尾尾、尾头、头尾 这一步的前提就是两次对比的节点是 相同的,这里的相同不是大家想的完全相同,只是符合某些条件就是相同了,为了简化说明,文章就只考虑一个标签只包含 key标签名(tag),那么之前说的相同就是 key 相同以及 tag 相同,为了证明作者的说法是有点正确的,那么这里也贴上源码:


// https://github.com/vuejs/vue/blob/main/src/core/vdom/patch.ts
// 36行
function sameVnode(a, b) {
return (
a.key === b.key &&
a.asyncFactory === b.asyncFactory &&
((a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)) ||
(isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error)))
)
}

如果怕乱了,下面的可以省略不看也没事不影响整体了解,下面只是为了考虑所有情况才加的一个判断:
那么如果两个虚拟 DOM 不相同其实就不用继续比较了,而且如果相同也不用比较了,这里的相同是真的完全相同,也就是两个虚拟 DOM 的地址是一样的,那么也贴上源码:


function patchVnode(......) {
if (oldVnode === vnode) {
return
}
......
}

到目前为止大家可能会比较乱,现在总结一下:



  • patch 函数里比较的是新老虚拟 DOM 是否是 key 相同以及 tag 相同,如果不相同那么就直接替换,如果相同用 patchVnode


说了这么多,其实作者也就想表达一个观点,就是只有当两次比较的虚拟 DOM 是 相同的 才会考虑 Diff 算法,如果不符合那直接把原来的删除,替换新的 DOM 就行了。


patchVnode 函数


这个函数里的才是真正意义上的 Diff 算法,那么接下来会结合源码向大家介绍一下。



源码中核心代码在 patch.ts 的 638 行至 655 行。



其实,目前介绍 patchVnode 的都是直接对着源码来介绍的,但是大家可能不清楚为啥要这么分类,那么作者在这里就让大家明白为什么这么分类,首先在这里说一个结论:



  • 就是 text 属性和 children 属性不可能同时存在,这个需要大家看模板解析源码部分


那么在对比新旧节点的情况下,主要比较的就是是否存在 textchildren 的情况,那么会有如下九种情况


情况老节点 text老节点 children新节点 text新节点 children
1
2
3
4
5
6
7
8
9

按照上面的表格,因为如果新节点有文本节点,其实老节点不管是什么都会被替换掉,那么就可以按照 新节点 text 是否存在来分类,其实 Vue 源码也是这么来分类的:


if (isUndef(vnode.text)) {
// 新虚拟 DOM 有子节点
} else if (oldVnode.text !== vnode.text) {
// 如果新虚拟 DOM 是文本节点,直接用 textContent 替换掉
nodeOps.setTextContent(elm, vnode.text)
}

那么如果有子节点的话,那应该怎么分类呢?我们可以按照每种情况需要做什么来进行分类,比如说:



  • 第一种情况,我们啥都不用做,因此也可以不用考虑

  • 第二种情况,我们应该把原来 DOM 的 textContent 设置为 ''

  • 第三种情况,我们也应该把原来 DOM 的 textContent 设置为 ''

  • 第四种情况,我们应该加入新的子节点

  • 第五种情况,这个情况比较复杂,需要对比新老子节点的不同

  • 第六种情况,我们应该把原来的 textContent 设置为 '' 后再加入新的子节点


那么通过以上六种情况 (新虚拟 DOM 不含有 text,也就是不是文本节点的情况),我们可以很容易地进行归类:



  • 分类 1️⃣: 第二种情况第三种情况。进行的是操作是:把原来 DOM 的 textContent 设置为 ''

  • 分类 2️⃣: 第四种情况第六种情况。进行的是操作是:如果老虚拟 DOM 有 text,就置空,然后加入新的子节点

  • 分类 3️⃣:第五种情况。进行的是操作是:需要进行精细比较,即对比新老子节点的不同


其实源码也是这么来进行分类的,而且之前说的 同层比较 也就得出来了,因为每次比较都是比较的同一个父节点每一个子元素 (这里的子元素包括文本节点和子节点) 是否相同,而 深度优先遍历(dfs) 是因为每次比较中,如果该节点有子节点 (这里的子节点指的是有 children 属性,而不包括文本节点) 的话需要进行递归遍历,知道最后到文本节点结束。



⭕️ 这里需要搞清楚子节点和子元素的区别和联系



然后我们来看看源码是怎么写吧,只看新虚拟 DOM 不含有 text,也就是不是文本节点的情况:


if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch)
// 递归处理,精细比较
// 对应分类 3️⃣
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (__DEV__) {
checkDuplicateKeys(ch) // 可以忽略不看
}
// 对应分类 2️⃣
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 对应分类 1️⃣
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 对应分类 1️⃣
nodeOps.setTextContent(elm, '')
}
}

❓我们可以看到源码把分类 1️⃣ 拆开来了,这是因为如果老虚拟 DOM 有子节,那么可能绑定了一些函数,需要进行解绑等一系列操作,作者也没自信看,大致瞄了一眼,但是如果我们要求不高,如果只是想自己手动实现 Diff 算法,那么没有拆开的必要。


作者觉得这么讲可能比网上其他介绍 Diff 算法的要好,其他的可能直接给你说源码是怎么写的,可能没有说明白为啥这么写,但是通过之前这么分析讲解后可能你对为什么这么写会有更深的理解和帮助吧。


updateChildren 函数



同层比较



因为当都含有子节点,即都包含 children 属性后,需要精细比较不同,不能像之前那些情况一样进行简单处理就可以了
那么这个函数中就会介绍大家经常说的 头头、尾尾、尾头、头尾 比较了,其实这不是 Vue 提出来的,是很早就提出来的算法,就一直沿用至今,大家可以参考【snabbdom 库】


🌟 在这之前我们要定义四个指针 newStartIdxnewEndIdxoldStartIdxoldEndIdx,分别指向 新头节点新尾节点旧头节点旧尾节点


循环条件如下:


while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
......
}

其实这个比较也就是按人类的习惯来进行比较的,比较顺序如下 :



  • 1️⃣ 新头节点旧头节点++newStartIdx++oldStartIdx

  • 2️⃣ 新尾节点旧尾节点--newEndIdx--oldEndIdx

  • 3️⃣ 新尾节点旧头节点:需要将 旧头节点 移动到 旧尾节点之前,为什么要这么做,讲起来比较复杂,记住就好,然后 --newEndIdx++oldStartIdx

  • 4️⃣ 新头节点旧尾节点:需要将 旧尾节点 移动到 旧头节点之前,为什么要这么做,讲起来比较复杂,记住就好,然后 ++newStartIdx--oldEndIdx

  • 5️⃣ 如果都没有匹配的话,就把 新头节点 在旧节点列表 (也就是 children 属性的值) 中进行查找,查找方式按照如下:

    • 如果有 key 就把 keyoldKeyToIdx 进行匹配,oldKeyToIdx 根据旧节点列表中元素的 key 生成对应的下标

    • 如果没有,就按顺序遍历旧节点列表找到该节点所在的下标

    • 如果在旧节点列表是否找到也分为两种情况:

      • 找到了,那么只要将 新头节点 添加到 旧头节点 之前即可

      • 没找到,那么需要创建新的元素然后添加到 旧头节点 之前

      • 然后把这个节点设置为 undefined






根据循环条件我们可以得到两种剩余情况,如下:



  • 6️⃣ 如果 oldStartIdx > oldEndIdx 说明老节点先遍历完成,那么新节点比老节点多,就要把 newStartIdxnewEndIdx 之间的元素添加

  • 7️⃣ 如果 newStartIdx > newEndIdx 说明新节点先遍历完成,那么老节点比新节点多,就要把 oldStartIdxoldEndIdx 之间的元素删除


其实我们上面还没有考虑如果节点为 undefined 的情况,因为在上面也提到过,如果四种都不匹配后会将该节点置为 undefined,也只有旧节点列表中才有,因此要在开头考虑这两种情况:



  • 8️⃣ 当 oldStartVnodeundefined++oldStartIdx

  • 9️⃣ 当 oldEndVnodeundefined--oldEndIdx


那么我们来看源码怎么写的吧,其中用到的函数可以查看源码附录


// https://github.com/vuejs/vue/blob/main/src/core/vdom/patch.ts
// 439 行至 556 行
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
// 情况 8️⃣
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
// 情况 9️⃣
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 情况 1️⃣
patchVnode(...)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 情况 2️⃣
patchVnode(...)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
// 情况 3️⃣
patchVnode(...)
canMove &&
nodeOps.insertBefore(
parentElm,
oldStartVnode.elm,
nodeOps.nextSibling(oldEndVnode.elm)
)
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
// 情况 4️⃣
patchVnode(...)
canMove &&
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 情况 5️⃣
if (isUndef(oldKeyToIdx)) // 创建 key -> index 的 Map
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 找到 新头节点 的下标
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) {
// New element
// 如果没找到
createElm(...)
} else {
// 如果找到了
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(...)
oldCh[idxInOld] = undefined
canMove &&
nodeOps.insertBefore(
parentElm,
vnodeToMove.elm,
oldStartVnode.elm
)
} else {
// same key but different element. treat as new element
createElm(...)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
// 情况 6️⃣
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(...)
} else if (newStartIdx > newEndIdx) {
// 情况 7️⃣
removeVnodes(...)
}


如果问为什么这么比较,回答就是经过很多人很多年的讨论得出的,其实只要记住过程就行了,如果想要更深了解 Diff 算法,可以去 B 站看【尚硅谷】Vue源码解析之虚拟DOM和diff算法



v-for 中为什么要加 key


这个问题面试很常见,但是可能大部分人也就只会背八股,没有完全理解,那么经过以上的介绍,我们可以得到自己的理解:



  • 首先,如果不加 key 的话,那么就不会去 Map 里匹配 (O(1)),而是循环遍历整个列表 (O(n)),肯定加 key 要快一点,性能更高

  • 其次,如果不加 key 那么在插入或删除的时候就会出现,原本不是同一个节点的元素被认为是相同节点,上面也有说过是 sameVnode 函数判断的,因此可能会有额外 DOM 操作



为什么说可能有额外 DOM 操作呢?这个和插入的地方有关,之后会讨论,同理删除也一样



证明 key 的性能


我们分为三个实验:没有 key、key 为 index、key 唯一,仅证明加了 key 可以进行最小化更新操作。


实验代码


有小伙伴评论说可以把代码贴上这样更好,那么作者就把代码附上 🥳:


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<style>
.box {
display: flex;
flex-direction: row;
}
.item {
flex: 1;
}
</style>
</head>

<body>
<div id="app">
<div class="box">
<div class="item">
<h3>没有 key</h3>
<p v-for="(item, index) in list">{{ item }}</p>
</div>
<div class="item">
<h3>key 为 index</h3>
<p v-for="(item, index) in list" :key="index">{{ item }}</p>
</div>
<div class="item">
<h3>key 唯一</h3>
<p v-for="(item, index) in list" :key="item">{{ item }}</p>
</div>
</div>
<button @click="click1">push(4)</button>
<button @click="click2">splice(1, 0, 666)</button>
<button @click="click3">unshift(999)</button>
<br /><br />
<button @click="click4">pop()</button>
<button @click="click5">splice(1, 1)</button>
<button @click="click6">shift()</button>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
show: false,
list: [1, 2, 3],
},
methods: {
click1() {
this.list.push(4);
},
click2() {
this.list.splice(1, 0, 666);
},
click3() {
this.list.unshift(999);
},
click4() {
this.list.pop();
},
click5() {
this.list.splice(1, 1);
},
click6() {
this.list.shift();
}
},
})
</script>
</body>

</html>

增加实验


实验如下所示,我们首先更改原文字,然后点击按钮**「观察节点发生变化的个数」**:


在队尾增加


在这里插入图片描述


在队内增加


在这里插入图片描述


在队首增加


在这里插入图片描述


删除实验


在队尾删除


在这里插入图片描述


在队内删除


在这里插入图片描述


在队首删除


在这里插入图片描述


实验结论


增加实验


表格为每次实验中,每种情况的最小更新量,假设列表原来的长度为 n


实验没有 keykey 为 indexkey 唯一
在队尾增加111
在队中增加n - i + 1n - i + 11
在队首增加n + 1n + 11

删除实验


表格为每次实验中,每种情况的最小更新量,假设列表原来的长度为 n


实验没有 keykey 为 indexkey 唯一
在队尾删除111
在队中删除n - in - i1
在队首删除nn1

通过以上实验和表格可以得到加上 key 的性能和最小量更新的个数是最小的,虽然在 在队尾增加在队尾删除 的最小更新量相同,但是之前也说了,如果没有 key 是要循环整个列表查找的,时间复杂度是 O(n),而加了 key 的查找时间复杂度为 O(1),因此总体来说加了 key 的性能要更好。


写在最后


本文从源码和实验的角度介绍了 Diff 算法,相信大家对 Diff 算法有了更深的了解了,如果有问题可私信交流或者评论区交流,如果大家喜欢的话可以点赞 ➕ 收藏 🌟


源码函数附录



列举一些源码中出现的简单函数



setTextContent


function setTextContent(node: Node, text: string) {
node.textContent = text
}

isUndef


function isUndef(v: any): v is undefined | null {
return v === undefined || v === null
}

isDef


function isDef<T>(v: T): v is NonNullable<T> {
return v !== undefined && v !== null
}

insertBefore


function insertBefore(
parentNode: Node,
newNode: Node,
referenceNode: Node
) {
parentNode.insertBefore(newNode, referenceNode)
}

nextSibling


function nextSibling(node: Node) {
return node.nextSibling
}

createKeyToOldIdx


function createKeyToOldIdx(children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}

作者:Karl_fang
来源:juejin.cn/post/7204752219915747388
收起阅读 »

技术管理者应有的 4 种基本思维模式

.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:16px;overflow-x:hidden;color:#252933}.markdown-bod...
继续阅读 »

在看文章之前先思考两个问题:



  • 人和人的差别究竟在哪?

  • 人和人之间为什么会有差别?


在各种场合我们经常听到这样一句话:「听懂了很多道理,却依然过不好这一生」。
这里有两个逻辑,一个是知行合一的逻辑,另一个是人的成长在于是否超越了昨天的自己。


今天我们聊一聊作为一个技术管理者应该要有的 4 种基本思维:掌控者思维、杠杆思维,终局思维,闭环思维


掌控者思维


在聊掌控者思维之前我们先看一下掌控者思维的反面:受害者思维。


受害者思维又称为弱者思维,有受害者思维的人在任何时候都会把自己当作一个受害者或弱者,极端者会把整个世界都投射为「伤害者」,「都是别人不对,都是别人对不起我」。


受害者思维是一种思维定势,其本质上是一种忽视自己的主观能动性的行为。


在《乡村爱情 13》里面,王木生是老总儿子,富二代,但是干啥啥不行,还总是给自己找借口,说大环境不好,其他没这背景的都比他强。赵本山饰演的他爹王大拿就教训他:「自己没能力就说没能力,怎么你到哪,哪都大环境不好,咋,你是破坏大环境的人啊?」


这就是比较明显的受害者思维,也就是我们常说的「甩锅」。
受害者思维不仅仅只有这一种表现,在《拆掉思维里的墙》里面,作者总结了 5 种受害者思维的表现。



  1. 推卸责任,保住面子: 上面王木生就是明显的例子,还有像一些网上的评论说的「便秘都怪地球没引力」都是类似的表现。在这种情况下,受害者思维就会认为自己没有责任,不需要承担责任,相信地球没引力,相信大环境不好……

  2. 安心做坏事:这种表现就是为自己的行为找到一个自洽的逻辑,让自己有一个完美的故事,从而在做坏事的时候能心安理得。比如 2001 被判死刑的「杀人魔头张君」(实施团伙抢劫,犯故意杀人罪 2 2次,致 28 人死亡、22 人重伤)在 2001.4.17 一审审理结束后对公众说的话:「我还要向没有枪的受害者和你们的家庭说声‘对不起’,现在想起来,以前有些事情确实做错了,但我没办法,因为我要生存。」

  3. 让我们一起分享凄惨故事会:寻找同伴,和他们分享你的受害过程,在他们那里得到安慰。比如有个女孩失恋了,她的闺蜜会陪她喝酒,说男人没有一个好东西;

  4. 用受害获得同情和帮助:第 4 种和第 3 种有些类似,其区别是上面是通过群体得到安慰,得到是一种情感或者心理上的安慰,而这种是更直接的帮助或者说利益,如我们在职场里面看到的一些人会假装自己不会,从而让热心的同事帮忙解决,自己偷懒,或者在路上我们看到的一些乞丐,可能他们的收入比你想象中多很多;

  5. 自我伤害,绑架他人:这种就更极端了,生活中常见于情侣分手,某一方说,你要是离开我,我就自杀,或者电视剧中的男主站在女主楼下淋雨以求得原谅或者求得表白成功。


如此种种,这些可以让你有短暂的快乐和安全感,但是慢慢就会失去自信、勇气以及自我改进的能力,就会真的变成一个受害者,一个弱者。


我们活在这个尘世,很多事情已经无法掌握,如古罗马哲学家爱比克泰德(Epictetus, 约55~约135年)所说:「我们登上的并非我们所选择的舞台,演出并非我们所选择的剧本」,如果连我们自己也无法掌控,让自己的快乐、成功都掌控在别人手里,这会是怎样一个光景呢?


当你带一个技术团队的时候,当你的团队遇到问题的时候,当你的下属遇到他认为过不去的坎的时候,当发生线上事故的时候,当老板怼到脸上的时候,你会怎么办呢?


当一个受害者吗?是大环境不好?是技术太难?还是坏人太多?


不,绝对不是这样,一个技术管理者应该是强于面对现实,敢于直面问题,像汪涵在我是歌手第三季总决赛面对大型事故时所说的那样:「没事儿不惹事,事儿来了也不要怕事」,以一种掌控的状态来扛起事情,解决问题。


这就是我们所说的掌控者思维,一种强者思维,一种充分发挥自己主观能动性的行为。


掌控者思维的核心思考是:「如果把所有经历过的事情重新倒推一遍,所有条件都不改变,只有自己改变,你能否得到一个更好的结果呢


掌控者思维是从责任心,自我成长,主动改变等方面来提升一个技术管理者的修养,以达到自我的精进。
并且不管什么情况,你都可以负全责。只要你愿意,你就可以做得更好。


除了不怕事,负起责任,还有一些是我们必须要做的。



  1. 学习,提升自己的认知,把自己从知道自己不知道变成知道自己知道,换句话说,保持有计划的学习;

  2. 自省,持续的自省,比如当你有一件事做得不好,自我反思一下哪里做得不好,下一次如何改进,或者更深入一些读一些相关的系统的书,写一篇文章或文档来复盘,掌控自己,从卷自己开始;

  3. 计划,凡事预则立,不预则废,如下棋一般,走一步,算三步,把事情想在前头,如美国首席数据科学家 DJ Patil 所说流传很广的那个便签上的话:「Dream in Years. Plan in Months Evaluate in Weeks. Ship Daily. 」

  4. 知行合一,只有做到,才是真的知道。


当然,做一个掌控者会累一些,但是与自由相比,累又算得了什么呢?


杠杆思维


阿基米德说:「给我一个支点,我就能撬起整个地球。」。


一个优秀的人,都有「杠杆思维」,懂得利用杠杆原理,以较小的付出,撬动更大的回报。这里我们所要说的是杠杆思维中的团队杠杆。


当我们从一个开发变成一个技术管理者时,就不再是一个人在战斗了,你的主要责任就不再是写好代码,而是「使众人行」。


时间对于每个人都是公平的,一个人一天都有 24 小时,它是有限的,而我们所面对的这个世界,工作这些都是无限的。


在一个健康经营的互联网企业中,需求也是无限的,而开发同学的时间都是有限的,除了优先级,我们还能做什么呢?此时有人会说:加人啊。是的,加人。但是加人不是随便加的,他有一个底层逻辑,这个就是今天我们要说的团队杠杆。


团队杠杆的本质是叠加效率,人的时间是有限的,而事情是无限的。


什么是团队杠杆?


通过团队,团队的管理者确定好方向,通过良好的机制和人才梯队,系统化成功的路径,叠加个体的优势,创造出远超个人贡献总和的价值,让 1+1>2,这就是团队杠杆。


既然是杠杆,就一定有一个支点,咱们的支点是什么呢?


对一个团队来说,其支点是已经达成了共识的目标和价值观。


目标是什么,是方向,是团队成员能看到的远方。


为什么是达成了共识?是因为只有达成了共识才能保证目标的统一,才能有意愿,有内在的动力去做事情,才能起到杠杆的效果。


目标和价值观只是最基础的,对于一个团队来说,不同的时候其支点也会不同。



  • 在野蛮生长的团队中,核心岗位上的同学的能力提升是主要支点,这里的核心岗位上的能力提升有两层意思,一个是培养员工,给他们训练的机会,培训,从而快速成长;另一个层面是考虑换一个人,当你这认为这个岗位上的员工的能力、态度差不多到顶了,那就可以考虑用更好、更合适的人来替换,这里的逻辑是选拔大于培养

  • 在快速扩张的团队中,流程和效率是主要支点,通过流程提升协同的效率,通过流程让新人快速融入团队;

  • 在稳定发展的团队中,标准和系统是主要支点,当业务趋于稳定,通过标准和系统固化操作,以系统代码流程和人工。


有了支点,我们就需要开始寻找杠杆,对于一个技术团队来说,团队的杠杆有多大,由人决定。
一个团队里面的人是怎样的,人才梯队是怎样的,人才密度是怎样的?


像麦肯锡,每年都会从全球各个顶尖大学,比如哈佛大学、斯坦福大学、麻省理工学院等,招一大批刚毕业的年轻人,不管是不是商学院毕业的。这些绝顶聪明的年轻人,就是麦肯锡充沛而有效的「团队杠杆」。


对于我们一个普通的互联网创业公司来说,作为一个技术团队的负责人,在有限的范围内选出符合要求的同学,同时尽我所能的带好团队。对于如何来带好团队,我们可以从如下 4 个大的方面来



  1. 信任:不管是平级,还是上下级,信任是必须品,通过良好的沟通,多次的合作,一起的奋斗达成信任,这样才是一个好的团队;

  2. 习惯:管理者以身作则培养团队良好的工作习惯,如做事闭环,分级处理,不越红线等等;

  3. 标准:制定能落地的标准,让大家知道什么是好的,什么是对的;

  4. 能力:流程机制决定事情的下限,人才梯队和个人能力决定事情的上限,管理者要帮忙团队成员成长,发现他们的短板,坦率的沟通并提出改进期望,提升成员的技能,以达成更好的结果。


以上带好团队的表述有些虚,但是是这么个逻辑,各团队各公司术法不同,但方向一致。


用好团队杠杆,一个人走得更快,一群人走得更远。


终局思维


什么是终局思维?


所谓终局思维,是指从终点或者未来的某个时间节点出发,回看现在所做选择,并进行推演的一种思考模式。


这里可能有人会问,什么是终局,是否有终局?这就有点哲学的味道了,先假定有吧,至少在某个未来的时间段是会有一个确定的终局。


我们经常听到人们感叹,如果这辈子能够再来一回,很多错误就不会犯,很多人也不会错过,这算是终局思维的一种表现。只是这个终局思维是等到了终局再提起,有些晚了,我们可以再早一些。


我们在做一件事情的时候,如果能思考一下这件事情的最终达成的目标,或者把最终的形态在脑海里面勾画出来,以终为始,站在未来看当下,修正当下正在做的事情,那么这件事情成功的概率可能就不一样了。


终局思维有 4 个核心点:



  1. 认清方向,所谓的终局思维,一定是你要知道终局是什么,一定要有目标有方向;

  2. 拉长时间周期看问题,在一个较长时间维里来反复推演的逻辑;

  3. 从历史或者更宏观的层面看问题,不局限于当下,不局限于眼前的一亩三分地,把视野拉到历史长河等更大的层面;

  4. 反推机制,终局思维中非常重要的点,从未来反向推演现在要做什么,或者从现在推演未来怎样。


终局思维有什么用?


当一个企业的老板大概知道了未来是什么样子,又能不断地判断和复盘自己的能力,他自然就有了非常强的战略驱动能力,也非常敢为未来投资。


当一个技术管理者知道了团队未来是怎么样的,又能反思当前的情况,从未来形态反思当下的状态,他自然就知道现在最重要的是做什么。


当一个开发同学知道了自己在职场上想成为什么样的人,又能反思自己提升自己,他自然知道现在应该做什么,应该学什么,应该选择什么样的团队,什么样的路。


但是,这里一定会有一个痛苦的过程,甚至不止一个,可能是一直在痛苦中,折腾自己,反思自己,提升自己,一直有危机感,一直在学习中。


心理学研究说,人类对于外部世界的认识可分为三个区域:舒适区(comfort zone),学习区(stretch zone),和恐慌区(stress zone)。我们反思自己、提升自己的过程基本都是在学习区,人只有在学习区的时候,才会是进步的时候。


闭环思维


经常听到人人说:「凡事有交代,件件有着落,事事有回音」,这是典型的闭环思维,是一个职场人的必备思维。作为一个技术管理者更应该具备闭环思维,因为这将是你带团队的核心逻辑之一,它强调责任、敏感性和团队协作,是使众人行的必备。


以「逆向」的逻辑来推演一下,假设你带的团队成员都具备闭环思维,将是什么样的场景?



  • 高效率:闭环思维要求在一定时间内,无论任务完成的效果如何,都要认真、负责的反馈给发起人。这可以帮助团队及时发现问题并加以解决,从而提高工作效率。

  • 强协作:闭环思维需要团队成员之间相互配合,洞察干系人每个阶段的需求,并在截止期内给对方反馈。这可以增强团队协作,提高团队的工作效率和成果。

  • 靠谱:闭环思维要求做事靠谱,做事清楚明白,不会出现意外差错,就算出了问题也会提前告知,让对方可以启用方案 B。这样的技术团队会让人觉得可靠和可信,提高团队在公司中的信任度。

  • 好形象:闭环思维是一种良好的职业素养,可以让你和你的团队建立良好的职业形象。在职场中,「闭环思维」不仅仅说明了一个员工拥有完成任务的能力,还体现出「良好的合作意识」,这样的你会让上级、兄弟部门觉得很「靠谱」,能够放心的将工作交给你和你的团队。


那么如何做到闭环思维呢?


闭环思维有两层逻辑,一个是完整,即有完整的闭环,做事情的时候需要考虑事情的起点、目标、影响因素和结果,明确需求和关键点,以及可能遇到的问题,在过程中设置检查点,让事情可控;另一个是主动反馈,在我们完成一项工作或参加一项事情,不管执行效果如何,都要及时将结果反馈给发起人。


第一层逻辑我们可以使用 PDCA 环来辅助达成,第二层反馈逻辑还是需要刻意去练习,甚至给自己设置一些任务提醒来达到反馈的目的,并且在反馈时尽量做到 STAR 的表述。


番外


最近 chatGPT 一直在火,于是就今天的题目我问了 chatGPT 一些问题:



  • 技术管理者必备的4种基本思维模式

  • 技术管理者必备的思维模式

  • 技术管理的四种思维

  • 技术管理者必备的4种思维


大概的答案如下:


openAI 的回答


问:技术管理者必备的思维模式


作为一名技术管理者,以下是一些必备的思维模式:



  1. 系统思维模式:技术管理者需要具备系统思维模式,能够理解和处理复杂系统的各种关系和相互作用,从而在整个系统中发现问题并提供解决方案。

  2. 创新思维模式:技术管理者需要能够持续地推动创新,寻找新的解决方案并将其应用到业务中,从而实现业务目标。

  3. 问题解决思维模式:技术管理者需要能够分析和解决各种问题,包括技术问题、组织问题和人员问题等。

  4. 客户导向思维模式:技术管理者需要关注客户需求和体验,从客户的角度出发思考问题,为客户提供高质量的产品和服务。

  5. 团队合作思维模式:技术管理者需要具备团队合作意识,能够有效地管理和领导团队,激发员工的潜力,协调各种资源,实现业务目标。

  6. 风险管理思维模式:技术管理者需要对业务中可能出现的风险进行预测和管理,制定相应的应对措施,确保业务的可持续发展。

  7. 数据驱动思维模式:技术管理者需要依据数据进行决策和管理,从数据中发现问题和机会,制定相应的战略和计划,提高业务效率和效果。

  8. 学习和成长思维模式:技术管理者需要保持学习和成长的态度,不断学习新知识和技能,从他人的经验中汲取营养,不断提高自身的领导能力和管理水平。


phind 的回答


问:技术管理者必备的4种基本思维模式


答案1


作为技术管理者,必备的4种基本思维模式如下:




  1. 系统思维:技术管理者需要具备系统思维,可以将复杂的问题分解为简单的部分,然后逐一解决。例如,当公司需要开发一个新产品时,技术管理者需要思考每个阶段的具体步骤,确定每个步骤的时间表和责任人,确保整个过程的顺利进行。




  2. 创新思维:技术管理者需要具备创新思维,可以在技术方面寻找新的解决方案,以提高公司的竞争力。例如,当公司需要开发一项新技术时,技术管理者需要思考如何利用现有技术来开发出更好的产品,或者如何开发出全新的技术来满足市场需求。




  3. 沟通思维:技术管理者需要具备良好的沟通能力,可以与员工、客户和其他部门进行有效的沟通。例如,当公司需要与客户沟通产品需求时,技术管理者需要了解客户的需求,并将这些需求传达给开发团队。




  4. 领导思维:技术管理者需要具备领导思维,可以激励员工,推动项目进展,达成公司目标。例如,当公司需要完成一个重要项目时,技术管理者需要领导开发团队,确保项目按时完成,并向管理层报告项目进展情况。




技术管理者不仅需要具备以上4种基本思维模式,还需要具备自我驱动、架构把握、目标管理等核心能力,才能胜任管理岗位。在实际工作中,技术管理者应该灵活运用这些思维模式和能力,不断提升自己的管理水平。


答案 2


根据多个来源,以下是技术管理必备的四种思维和其他相关思维模型:




  1. 抽象思维:这种思维能力可以帮助技术人快速抽取面对问题的关键要素和本质,从而更好地分析和解决问题。




  2. 系统思维:这种思维能力可以帮助技术人将问题看作一个整体,理解各个组成部分之间的相互作用和影响,从而更好地进行系统设计和管理。




  3. 创新思维:这种思维能力可以帮助技术人寻找新的解决方案和创新点,从而更好地推动技术进步和发展。




  4. 逻辑思维:这种思维能力可以帮助技术人分析和推理问题,从而更好地发现问题根源和解决方案。




除了以上四种思维,以下是其他常用的思维模型:




  • PEST分析:这是一种常用的管理咨询框架,可以帮助企业从政治、经济、社会和技术等多个角度分析市场环境和竞争对手,从而更好地制定战略和决策。




  • PDCA循环:这是一种管理模型,可以帮助企业进行持续改进和优化,包括计划、执行、检查和行动四个环节。




  • SMART原则:这是一种目标管理模型,可以帮助企业制定具体、可衡量、可达成、有意义和时限的目标,从而更好地推动业务发展。




  • STAR法:这是一种面试技巧,可以帮助面试者回答问题更具体、更有条理、更有说服力,包括情境(situation)、任务(task)、行动(action)和结果(result)四个要素。




  • 5W2H法:这是一种问题分析和解决模型,可以帮助企业和个人从五个W(what、why、who、when、where)和两个H(how、how much)多个角度分析问题和制定解决方案。




  • PERT 技术:这是一种项目管理工具,可以帮助企业规划和安排整个项目行程,跟踪实施阶段,从而更好地控制项目进度和成本。




总之,技术管理必备的思维能力和模型是多种多样的,需要根据具体情况选择和应用。除了上述思维能力和模型外,还有很多其他的思维工具和技术,需要技术人员不断学习和掌握,以更好


答案 3


根据多个来源的信息,以下是四种技术管理必备的思维能力:




  1. 抽象思维:这种思维能力可以帮助技术人员快速抽取问题的关键要素和本质,从而更快地理解问题和解决问题。例如,当面对一个新的编程语言时,抽象思维可以帮助技术人员快速理解其核心概念和语法规则。




  2. 产品思维:这种思维能力可以帮助技术人员更好地理解用户需求,并将技术解决方案转化为实际的产品。例如,一个架构师需要了解产品的功能需求,以便设计出最佳的技术架构来支持这些需求。




  3. 创新思维:这种思维能力可以帮助技术人员不断创新,寻找新的解决方案和技术。例如,当面对一个复杂的技术问题时,创新思维可以帮助技术人员找到一种独特的解决方案,从而提高效率和质量。




  4. 商业思维:这种思维能力可以帮助技术人员更好地理解商业环境和商业需求,以便为企业提供最佳的技术支持和解决方案。例如,在设计一个新的软件系统时,商业思维可以帮助技术人员理解客户的商业需求,并设计出最佳的系统架构来支持这些需求。




除了以上四种思维能力外,还有其他常见的思维模型和方法可以帮助技术人员更好地管理和解决问题,例如:



  • PDCA 循环:这是一种常见的问题解决方法,包括计划、实施、检查和行动四个阶段,可以帮助技术人员系统地解决问题。

  • SMART 原则:这是一种有效的目标设定方法,包括具体、可衡量、可实现、相关和时间限制五个要素,可以帮助技术人员设定清晰的目标和指标。

  • 5W2H 法:这是一种问题分析方法,包括什么、为什么、谁、何时、何地、如何和多少七个问题,可以帮助技术人员全面地分析和解决问题。


作者:潘锦
来源:juejin.cn/post/7203880677195661373
收起阅读 »

三十岁前端的破冰尝试

@charset "UTF-8";.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:15px;overflow-x:hidden;color:#2b2...
继续阅读 »

大多数人没有意识到精力的有限而盲目学习,从没有停下来认真咀嚼已有的东西。



本人简介


JavaScrip码农,今年三十,宿主是非互联网类型的外企,提供的内存虽然不大,但也基本够存活。


工作之余,我的主题就是咸鱼。但或许是我的咸度不够,最近开始腐烂了,尤其是夜深人静,主要的信息输入被关闭之后,我就感觉内在的信息流在脑海里乱窜,各种健康指数开始飙升。就像是一台老旧的电脑,非要带最新的显卡游戏,发出嘤嘤嘤的EMO声,最后在卡死在昏睡页面。


大多时候醒来会一切安好,像是被删去了前一晚的日志。但有时也会存有一些没删除干净的缓存,它们就像是病毒,随着第二天的重启复苏。我会感到无比的寒冷,冷到我哪怕是饥饿也不敢出门,只有戴上口罩会给我一丝丝的勇气。


这种寒冷会刺激着我无病呻吟,我会感到惊恐和害怕,害怕某天被宿主的回收机制发现这里的不正常,然后被文明的光辉抹除,就如新冠背后那鲜红的死亡人数一样。


或许是幼年求学寄人篱下时烙下的病根,但那时候心田干涸了还可以哭泣。如今呢,心田之上早已是白雪皑皑。


这些年也有人帮助过我,我也努力挣扎过,但大多时候毫无章法,不仅伤了别人的心,也盲目地消耗着心中的热血,愧疚与自责的泪水最终只是让冰层越积越深。


今天也不知哪根筋抽抽了,想着破冰。


嗯,就是字面上的意思,满脑子都是“破冰”二字……


破冰项目


发表这个稿子算是破冰的第一步~


项目的组织架构初步定为凌凌漆,敏捷周期为一周,其中周日进行复盘和制定新计划,其余作为执行日。由于项目长期且紧迫,年假就不予考虑了,病假可以另算,津贴方面目前只考虑早餐,其他看项目发展情况再做调整。


硬件层面


目前作息相当紊乱,供电稳定性差,从近几年的硬件体验报告可以看出,总体运行还算正常,但小毛病层出不穷,电压不稳是当前主要矛盾。OKR如下:


O:保持一个良好的作息

KR1: 保证每天八小时的睡眠。

KR2:保证每天凌晨前关灯睡下。

KR3:保证每天早上九点前起床。


软件层面


英语是硬伤,其次是底层算法需要重写,不然跑着跑着还是会宕机。


翻译是个不错的路子,但数据源是个头痛的问题……肯定得找和技术相关的东西来翻译,并且可以有反馈。嗯…… 想到可以找掘金里已经有的翻译文章,截取其中一小段来进行快速试错。


至于底层算法的问题,此前在leetcode练过一段时间,但仅停留在已知的变得熟练,未知的依旧不会。


因此我觉得有必要先梳理出关于算法的个人认知的知识体系……


总结下来下一阶段任务:



  1. 选择一篇翻译文章,找到其原文,选其中完整的一段进行翻译。

  2. 根据当前认知画个关于算法的思维导图。


下周日会出这周的运行报告以及新一期的计划表。


最后随想


若是觉得我这样的尝试也想试一试,欢迎在评论附上自己的链接,一起尝试,

作者:行僧
来源:juejin.cn/post/7152143987225133086
相互借鉴,共同进步~

收起阅读 »

聊一聊过度设计!

.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:16px;overflow-x:hidden;color:#252933}.markdown-bod...
继续阅读 »

  新手程序员在做设计时,因为缺乏经验,很容易写出欠设计的代码,但有一些经验的程序员,尤其是在刚学习过设计模式之后,很容易写出过度设计的代码,而这种代码比新手程序员的代码更可怕,过度设计的代码不仅写出来时的成本很高,后续维护的成本也高。因为相对于毫无设计的代码,过度设计的代码有比较高的理解成本。说这么多,到底什么是过度设计?


什么是过度设计?


  为了解释清楚,我这里用个类比,假如你想拧一颗螺丝,正常的解决方案是找一把螺丝刀,这很合理对吧。 但是有些人就想:“我就要一个不止能拧螺丝的工具,我想要一个可以干各种事的工具!”,于是就花大价钱搞了把瑞士军刀。在你解决“拧螺丝”问题的时候,重心早已从解决问题转变为搞一个工具,这就是过度设计。

在这里插入图片描述
  再举个更技术的例子,假设你出去面试,面试官让你写一个程序,可以实现两个数的加减乘除,方法出入参都给你提供好了 int calc(int x, int y, char op),普通程序员可能会写出以下实现。


    public int calc(int x, int y, int op) {
if (op == '+') {
return x + y;
} else if (op == '-') {
return x - y;
} else if (op == '*') {
return x * y;
} else {
return x / y;
}
}
复制代码

  而高级程序员会运用设计模式,写出这样的代码:


public interface Strategy {
int calc(int x, int y);
}

public class AddStrategy implements Strategy{
@Override
public int calc(int x, int y) {
return x + y;
}
}

public class MinusStrategy implements Strategy{
@Override
public int calc(int x, int y) {
return x - y;
}
}
/**
* 其他实现
*/

public class Main {
public int calc(int x, int y, int op) {
Strategy add = new AddStrategy();
Strategy minux = new MinusStrategy();
Strategy multi = new MultiStrategy();
Strategy div = new DivStrategy();
if (op == '+') {
return add.calc(x, y);
} else if (op == '-') {
return minux.calc(x, y);
} else if (op == '*') {
return multi.calc(x, y);
} else {
return div.calc(x, y);
}
}
}
复制代码

  策略模式好处在于将计算(calc)和具体的实现(strategy)拆分,后续如果修改具体实现,也不需要改动计算的逻辑,而且之后也可以加各种新的计算,比如求模、次幂……,扩展性明显增强,很是牛x。 但光从代码量来看,复杂度也明显增加。回到我们原始的需求上来看,如果我们只是需要实现两个整数的加减乘除,这明显过度设计了。


过度设计的坏处


  个人总结过度设计有两大坏处,首先就是前期的设计和开发的成本问题。过度设计的方案,首先设计的过程就需要投入额外的时间成本,其次越复杂的方案实现成本也就越高、耗时越长,如果是在快速迭代的业务中,这些可能都会决定到业务的生死。其次即便是代码正常上线后,其复杂度也会导致后期的维护成本高,比如当你想将这些代码交接给别人时,别人也需要付出额外的学习成本。


  如果成本问题你都可以接受,接下来这个问题可能影响更大,那就是过度设计可能会影响到代码的灵活性,这点听起来和做设计的目的有些矛盾,做设计不就是为了提升代码的灵活性和扩展性吗!实际上很多过度设计的方案搞错了扩展点,导致该灵活的地方不灵活,不该灵活的地方瞎灵活。在机器学习领域,有个术语叫做“过拟合”,指的是算法模型在测试数据上表现完美,但在更广泛的数据上表现非常差,模式缺少通用性。 过度设计也会出现类似的现象,就是缺少通用性,在面对稍有差异的需求上时可能就需要伤筋动骨级别的改造了。


如何避免过度设计


  既然过度设计有着成本高和欠灵活的问题,那如何避免过度设计呢!我这里总结了几个方法,希望可以帮到大家。


充分理解问题本身


  在设计的过程中,要确保充分理解了真正的问题是什么,明确真正的需求是什么,这样才可以避免做出错误的设计。


保持简单


  过度设计毫无例外都是复杂的设计,很多时候未来有诸多的不确定性,如果过早的针对某个不确定的问题做出方案,很可能就白做了,等遇到真正问题的时候再去解决问题就行。


小步快跑


  不要一开始就想着做出完美的方案,很多时候优秀的方案不是设计出来的,而是逐渐演变出来的,一点点优化已有的设计方案比一开始就设计出一个完美的方案容易得多。


征求其他人的意见


  如果你不确定自己的方案是不是过度设计了,可以咨询下其他人的,尤其是比较资深的人,交叉验证可以快速让你确认问题。


总结


  其实在业务的快速迭代之下,很难判定当前的设计是欠设计还是过度设计,你当前设计了一个简单的方案,未来可能无法适应更复杂的业务需求,但如果你当前设计了一个复杂的方案,有可能会浪费时间……。 在面对类似这种不确定性的时候,我个人还是比较推崇大道至简的哲学,当前用最简单的方案,等需要复杂性扩展的时候再去重构代码。

作者:xindoo
来源:juejin.cn/post/7204423284905738298

收起阅读 »

互联网大裁员原因分析

.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:16px;overflow-x:hidden;color:#252933}.markdown-bod...
继续阅读 »

继谷歌、微软之后,Zoom、eBay、波音、戴尔加入最新一波“裁员潮”中。


2 月 7 日,美国在线会议平台 Zoom 宣布将裁减 1300 名员工,成为最新一家进行裁员的公司,大约 15% 的员工受到影响。


同日,总部位于亚特兰大的网络安全公司 Secureworks 在一份提交给美国证券交易委员会( SEC )的文件中宣布,将裁员 9%,因为该公司希望在“一些世界经济体处于不确定时期”减少开支。据数据供应商 PitchBook估计,该公司近 2500 名员工中约有 225 人将在此轮裁员中受到影响。


此外,电商公司eBay于2月7日在一份SEC文件中宣布,计划裁员500人,约占其员工总数的4%。据悉,受影响的员工将在未来24小时内得到通知。


2月6日,飞机制造商波音公司证实,今年计划在财务和人力资源部门裁减约 2000 个职位,不过该公司表示,将增加 1 万名员工,“重点是工程和制造”。


个人电脑制造商戴尔的母公司,总部位于美国德克萨斯州的戴尔科技,2 月 6 日在一份监管文件中表示,公司计划裁减约 5% 的员工。戴尔有大约 13.3 万名员工,在这个水平上,约 6650 名员工将在新一轮裁员中受到影响。


除了 Zoom、eBay、波音、戴尔 等公司,它们的科技同行早已经采取了同样的行动。


从去年 11 月开始,许多硅谷公司员工就开始增加了关注邮箱的频率,害怕着某封解除自己公司内网访问权限的邮件来临,在仅仅在 2022 年 11 月,裁员数字就达到了近 6 万人,而如果从 2022 开始计算,各家已经陆续裁员了近十五万人。



但本次裁员并非是因为营收的直接下降:事实上,硅谷各家 2022 年营收虽然略有下跌,但总体上仍然保持了平稳,甚至部分业务略有上涨,看起来并没有到「危急存亡之秋」,需要动刀进行大规模裁员,才能在寒冬中存活的程度。


相较于备受瞩目的裁员,一组来自美国政府的就业数据就显得比较有意思了。据美国劳工统计局 2 月 3 日公布的数据,美国失业率 1 月份降至 3.4%,为 1969 年 5 月以来最低。



美国 1 月份非农就业人数新增 51.7 万人,几乎是经济学家预期的三倍,即使最近主要在科技行业裁员,但建筑、酒店和医疗保健等行业带来了就业增长。


一方面是某些企业的大规模裁员,仅 1 月份就影响超过 10 万人;而另一方面,政府报告显示就业市场强劲。这样来看,美国的就业市场情况似乎有些矛盾。


2022 年 12 月初,多名B站员工在社交媒体上表示,B站开始了新一轮裁员,B端、漫画、直播、主站、Goods等部门均有涉及,整体裁员比例在30%左右。12月19日,小米大规模裁员的消息又有曝出,裁员涉及手机部、互联网部、中国部等多部门,个别部门裁员比例高达75%。12月20日,知乎又传裁员10%。


似乎全球的科技公司都在裁员,而我们想要讨论裁员的问题,肯定绕不开两个大方向:经济下行和人员问题。


下行环境


联合国一月发布的《2023年世界经济形势与展望》中指出,2022 年,一系列相互影响的严重冲击,包括新冠疫情、乌克兰战争及其引发的粮食和能源危机、通胀飙升、债务收紧以及气候紧急状况等,导致世界经济遭受重创。美国、欧盟等发达经济体增长势头明显减弱,全球其他经济体由此受到多重不利影响。与新冠疫情相关的反复封锁以及房地产市场的长期压力,延缓了中国的经济复苏进程。


在此背景下,2023 年世界经济增速预计将从 2022 年估计的 3.0% 下降至 1.9%。2024 年,由于部分不利因素将开始减弱,预计全球经济增速将适度回升至 2.7%。不过,这在很大程度上将取决于货币持续紧缩的速度和顺序、乌克兰战争的进程和后果以及供应链进一步中断的可能性。


在通货膨胀高企、激进的货币紧缩政策以及不确定性加剧的背景下,当前全球经济低迷,导致全球经济从新冠疫情的危机中复苏的步伐减缓,对部分发达国家和发展中国家均构成威胁,使其 2023 年可能面临衰退的前景。


2022 年,美国、欧盟等发达经济体增长势头明显减弱。报告预计,2023 年美国和欧盟的经济增速分别为 0.4% 和 0.2%,日本为 1.5%,英国和俄罗斯的经济则将分别出现 0.8% 和 2.9% 的负增长。


与此同时,全球金融状况趋紧,加之美元走强,加剧了发展中国家的财政和债务脆弱性。自 2021 年末以来,为抑制通胀压力、避免经济衰退,全球超过85%的央行纷纷收紧货币政策并上调利率。


报告指出,2022 年,全球通胀率约为 9%,创数十年来的新高。2023 年,全球通胀率预计将有所缓解,但仍将维持在 6.5% 的高水平。


据美国商务部经济分析局(BEA)统计,第二、三季度,美国私人投资分别衰退 14.1% 和 8.5%。加息不仅对美国企业活动产生抑制作用,而且成为美国经济复苏的最主要阻力。尤其是,非住宅类建筑物固定投资已连续六个季度衰退。预计 2023 年美国联邦基金利率将攀升至 4.6%,远远超过 2.5% 的中性利率水平,经济衰退风险陡增,驱动对利率敏感的金融、房地产和科技等行业采取裁员等必要紧缩措施。


发展上限


美国企业的业务增长和经营利润出现问题。据美国多家媒体报道,第三季度,谷歌利润率急剧下滑,Meta 等社交媒体的广告收入迅速降温,微软等其他科技企业业务增长也大幅放缓。自7月以来,美国服务业PMI已连续5个月陷入收缩区间,制造业 PMI 也于 11 月进入收缩区间。在美国经济前景和行业增长空间出现问题的背景下,部分行业采取裁员、紧缩开支等“准备过冬”计划也就在意料之中了。


2022年,在市值方面,作为中概股的代表阿里、腾讯、快手等很多企业的市值都跌了 50%,甚至70%、80%。在收入方面,BAT 已经停止增长几个季度了,阿里和腾讯为代表的企业已经开始负增长。在经济下行的背景下,向内开刀、降本增效成为企业生存的必然之举。除了裁员,收缩员工福利、业务调整,也是企业降本增效的举动之一。


如果说 2021 年的裁员,很多是由于业务受到冲击,比如字节跳动的教育业务,以及滴滴、美团等公司的社区团购项目。但到了 2022 年,更多企业裁员的背后是降本增效、去肥增肌。


全球宏观经济表现不佳,由产业资本泡沫引发的危机感传导到科技企业的经营层,科技企业不得不面对现实。科技行业处在重要的结构转型期。iPhone 的横空出世开创了一个移动互联网的新时代,而当下的科技巨头也都是移动互联网的大赢家。但十多年过去了,随着智能手机全球高普及率的完成,移动互联网的时代红利逐渐消失,也再没有划时代的创新和新的热点。


这两年整个移动互联网时代的赢家都在焦急地寻找新的创新增长点。比如谷歌和 Meta 多年来一直尝试投资新业务,如谷歌云、Web3.0等,但实际收入仍然依赖于广告业务,未能找到真正的新增长点。这使得其中一些公司容易受到持有突破性技术的初创公司影响。


科技企业倾力“烧钱”打造新赛道,但研发投入和预期产出始终不成正比,不得不进行战略性裁员。


我们这里以这两年爆火的元宇宙举例:


各大券商亦争相拥抱元宇宙,不仅元宇宙研究团队在迅速组建,元宇宙首席分析师也纷纷诞生。 2021 年下半年,短短半年内便有数百份关于元宇宙的专题研报披露。


可以说,在扎克伯格和Meta的带领下,全世界的大厂小厂都在跟着往元宇宙砸钱。


根据麦肯锡的计算,自2021年以来,全世界已经向虚拟世界投资了令人瞠目结舌的数字——1770亿美元。


但即使作为元宇宙领军的 Meta 现实实验室(Reality Labs)2022 年三季度收入 2.85 亿美元,运营亏损 36.7 亿美元,今年以来已累计亏损 94 亿美元,去年亏损超过 100亿 美元。显然,Meta 的元宇宙战略还未成为 Meta的机遇和新增长点。


虽然各 KOL 高举“元宇宙是未来”的大旗,依旧无法改写“元宇宙未至”的局面。刨除亟待解决的关键性技术问题,如何兼顾技术、成本与可行性,实现身临其境的体验,更是为之尚远。元宇宙还在遥远的未来。


早在 2021 年12 月底,人民日报等官方媒体曾多次下场,呼吁理性看待“元宇宙”。中央纪委网站发布的《元宇宙如何改写人类社会生活》提及“元宇宙”中可能会涉及资本操纵、舆论吹捧、经济风险等多项风险。就连春晚的小品中,“元宇宙”也成为“瞎忽悠”的代名词。


2022 年 2月18日,中国银保监会发布了《关于防范以“元宇宙”名义进行非法集资的风险提示》,并指出了四种常见的犯罪手法,包括编造虚假元宇宙投资项目、打着元宇宙区块链游戏旗号诈骗、恶意炒作元宇宙房地产圈钱、变相从事元宇宙虚拟币非法谋利。


2022 年 2月7日,英国《金融时报》报道称,随着《网络安全法案》逐步落实,元宇宙将会受到严格的英国监管,部分公司可能面临数十亿英镑的潜在罚款。


2022 年 2月6日,据今日俄罗斯电视台(RT)报道,俄罗斯监管机构正在研究对虚拟现实技术实施新限制的可能性,他们担心应用该技术可能会协助非法活动。


各个国家的法律监管的到来,使得元宇宙的泡沫迅速炸裂。无数的元宇宙公司迅速破产,例如白鹭科技从 H5 游戏引擎转型到元宇宙在泡沫破裂的情况下个人举债 4000 万,公司破产清算。


本质上来说如今互联网行业已经到了一个明显的发展瓶颈,大家吃的都是移动网络和智能手机的普及带来的红利。在新的设备和交互方式诞生前,大家都没了新故事可讲,过去的圈地跑马模式在这样的大环境下行不通了。


法律监管


过去十年时间,互联网世界的马太效应越来越明显。一方面,几大巨头们在各自领域打造了占据了主导份额的互联网平台,不断推出包罗万象的全生态产品与服务,牢牢吸引着绝大多数用户与数据。他们的财务业绩与股价市值急剧增长,苹果、谷歌、亚马逊的市值先后突破万亿甚至是两万亿美元。


而另一方面,诸多规模较小的互联网公司却面临着双重竞争劣势。他们不仅财力与体量都无法与网络巨头抗衡,还要在巨头们打造的平台上,按照巨头制定偏向自己的游戏规则,与包括巨头产品在内的诸多对手激烈竞争用户。


2020 年 10 月,在长达 16 个月的调查之后,美国众议院司法委员会发布了一份长达 449 页的科技反垄断调查报告,直指谷歌、苹果、Facebook、亚马逊四大科技巨头滥用市场支配地位、打压竞争者、阻碍创新,并损害消费者利益。


2020 年 10 月 20 日,美国司法部连同美国 11 个州的检察长向 Google 发起反垄断诉讼,指控其在搜索和搜索广告市场通过反竞争和排他性行为来非法维持垄断地位。


2021 年明尼苏达州民主党参议员艾米·克洛布查尔(Amy Klobuchar)和爱荷华州共和党参议员查克·格拉斯利(Chuck Grassley)共同提出的《美国创新与选择在线法案》和 《开放应用市场法案》旨在打击谷歌母公司 Alphabet、亚马逊、Facebook 母公司 Meta 和苹果公司等科技巨头的一些垄断行为,这将是互联网向公众开放近30年来的首次重要法案。


《美国创新与选择在线法案》的内容包括禁止占主导地位的平台滥用把关权,给予营产品服务特权,使竞争对手处于不利地位;禁止施行对小企业和消费者不利,有碍于竞争的行为,例如要求企业购买平台的商品或服务以获得在平台上的优先位置、滥用数据进行竞争、以及操纵搜索结果偏向自身等。


不公平地限制大平台内其他商业用户的产品、服务或业务与涵盖平台经营者自己经营的产品、服务或业务相竞争能力,从而严重损害涵盖平台中的竞争。


除了出于大平台安全或功能的需要,严重限制或阻碍平台用户卸载预装的软件应用程序,将大平台用户使用大平台经营者提供的产品或服务设置为默认或引导为默认设置。


《开放应用市场法案》针对“守门人”执行,预计将会在应用商店、定向广告、互联操作性,以及垄断并购等方面,对相应企业做出一系列规范要求。此外欧盟方面还曾透露,如“守门人”企业不遵守上述规则,将按其上一财政年度的全球总营业额对其处以“不低于 4%、但不超过20%”的罚款。法案允许应用程序侧载(在应用商店之外下载应用程序),旨在打破应用商店对应用程序的垄断能力,将对苹果、谷歌的应用商店商业模式产生重要影响。


大型科技公司们史无前例搁置竞争,并且很有默契地联合起来。他们和他们的贸易团体在两年内耗费大约 1 亿美元进行游说,超过了制药和国防等高支出行业。他们向政界人士捐赠了 500 多万美元,科技游说人士向负责捍卫民主党多数席位的政治行动委员会(PAC)捐赠了 100 多万美元。他们还向不需要披露资金来源的黑钱组织、非营利组织和行业协会投入了数百万美元。几位国会助手表示,他们收到的有关这些法案的宣传比他们多年来处理的任何其他法案都要多。


这两项法案已通过国会相关委员会的审查,依然在等待众议院和参议院的表决。而美国即将开始中期选举。Deese 称,共和党已经明确表示,如果共和党重新控制国会两院,他们将不会支持这些法案。但如果民主党当选的话,科技巨头们估计不好过了。


很遗憾的是,2023年,新一届美国国会开幕后,众议院议长的选举经多轮投票仍然“难产”,导致新一届国会众议院无法履职。开年的这一乱象凸显美国政治制度失灵与破产,警示美国党争极化的趋势恐正愈演愈烈;


欧盟也多次盯上四大公司,仅谷歌一家,欧盟近三年来对其开出的反垄断处罚的金额已累计超过 90 亿美元。


而中国的举措也不小。


2020 年年初,实施了近 12 年的《反垄断法》(2008 年 8 月 1 日生效)首次进入“大修”——国家市场监督管理总局在其官网公布了《反垄断法修订草案(公开征求意见稿)》(以下简称“征求意见稿”)。


《法制日报》报道指出,征求意见稿中共有 8 章 64 条,较现行法要多出 7 条。可见,这次修法,已与另立新法有同等规模。


值得注意的是,征求意见稿还首次将互联网业态纳入其中,新增互联网领域的反垄断条款,针对性地列明相关标准和适用规程。


以市场支配地位认定为例,征求意见稿根据互联网企业的特点,新增了包括网络效应、规模经济、锁定效应、掌握和处理相关数据的能力等因素。


11 月 10 日,赶在双 11 前一天,国家市场监管管理总局再次出手,发布了《关于平台经济领域的反垄断指南(征求意见稿)》(以下简称《指南》)公开征求意见的公告。


《指南》不仅对“互联网平台”做了进一步界定,还结合个案更为具体详尽地对垄断协议,滥用市场支配地位行为,经营者集中,滥用行政权力排除、限制竞争四个方面作出分析和规定。


国家在平台经济领域、反垄断领域的法律规范,在《反垄断指南》出台以后,已经有了相当程度的完善。后续随着《反垄断法》修正案的通过,二者结合基本构建了我国反垄断领域的法律框架。


随着《反垄断法》的完善,在互联网领域的处罚案例逐渐浮出水面,针对阿里巴巴、美团等互联网公司都开出了大额罚单。


2021年我国在网络安全方面也加速发展。2021年6月10日颁布《中华人民共和国数据安全法》,2021年8月20日颁布《中华人民共和国个人信息保护法》。有关部门相继出台了《网络安全审查办法》《常见类型移动互联网应用程序必要个人信息范围规定》《数据出境安全评估办法(征求意见稿)》等部门规章和政策性文件。


可以预见的是,未来监管部门的监管措施更能兼顾互联网行业发展特征和社会整体福利,监管部门会不断完善规章、政策文件和标准文件,提供给企业明确和细化的指引。同时,相关部门的监管反应速度会越来越及时,监管层面对违法查处的力度也会越来越严。


人口红利


我们依然处在人口规模巨大的惯性中,人口规模巨大意味着潜在市场规模巨大,伴随经济持续发展、收入水平提高、消费能力强劲,由此带来的超大市场规模不可估量。而现在人口红利没了。


中国国家统计局 1 月 17 日公布,2022年末全国人口(包括 31 个省、自治区、直辖市和现役军人的人口,不包括居住在 31 个省、自治区、直辖市的港澳台居民和外籍人员) 141175 万人,比上年末减少 85 万人。这是近61年来中国首次人口负增长。人口负增长的早期阶段是一种温和的人口减少,所以依然会沿袭人口规模巨大的惯性;但在人口负增长的远期阶段,如果生育率仍未有所回升的话,就有可能导致一种直线性的减少。


目前所有行业都不得不面临从人口红利转向素质红利的转变。


人员过剩


微软在过去两年员工数新增 6 万,Google 则是新增了 7 万,Meta 则是直接从疫情之前的 4 万翻倍至 2022 年的 8.7 万。而依赖物流服务的亚马逊则最为激进,两年时间全球全职员工数增长了令人咂舌的 8.1 万,全职员工数近乎翻倍。



高盛的经济学家在一份报告中指出“那些正在裁员的科技公司有一些共同点,希望重新平衡业务的结构性转变,并为更好的利润开路。我们发现,许多最近宣布大规模裁员的公司都有三个共同特征。首先,许多都是在科技领域。其次,许多公司在疫情期间大肆招聘。第三,它们的股价出现了更大幅度的下跌,从峰值平均下跌了 43%。”


平均而言,那些进行裁员的公司在疫情期间的员工数量增长了 41%,此举往往是因为他们过度推断了与疫情相关的趋势,比如商品需求或在线时间的增长。


行裁员的公司并不能代表更广泛的情况,最近许多裁员公告并不一定意味着需求状况会减弱。与此一致的是,高盛预计更具代表性的实时估计的裁员率最近虽有所增加,但仅恢复到疫情前的水平,以历史标准衡量,裁员率水平较低。


结论


全球经济下行是大势,层层增加的法律监管是推动,没有人口红利和新玩法股价要大跌。


全球通胀激增,激进的货币紧缩政策以及不确定性加剧、俄乌战争等影响,全球经济低迷。新冠疫情带来的影响难以快速恢复。而中国还得面临人口红利消失、房地产饮鸩止渴的深远影响。而法律的层层监管和反垄断的推进在逐步打压科技巨头的已有市场,没有新技术的突破和新玩法让科技巨头们也没了新增和突破的空间。对于未来的经济发展的错误预估和疫情特殊时期的大量增长让科技巨头们大肆招聘,这些都成为了股价下跌和缩减利润的元凶。目前的大裁员可以算是一种虚假繁荣的泡沫爆裂后的回调,虽然不知道这个回调什么时候结束,但是随着人工智能的出圈和将来新技术的突破,也许整个行业可以浴火重生。


作者:Andy_Qin
来源:juejin.cn/post/7201047960825774139
收起阅读 »

你在公司混的差,可能和组织架构有关!

@charset "UTF-8";.markdown-body{line-height:1.75;font-family:-apple-system-font,BlinkMacSystemFont,Helvetica Neue,PingFang SC,Hira...
继续阅读 »

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,非公众号转载保留此声明。



如果你接触过公司的面试工作,一定见过很多来自大公司的渣渣。这些人的薪资和职位,比你高出很多,但能力却非常一般。


如果能力属实,我们大可直接把这些大公司的员工打包接收,也免了乱七八糟的面试工作。但可惜的是,水货的概率通常都比较大,新的公司也并不相信他们的能力。尤其是这两年互联网炸了锅,猪飞的日子不再,这种情况就更加多了起来。


反过来说也一样成立,就像是xjjdog在青岛混了这么多年,一旦再杀回北上广,也一样是落的下乘的评价。


除了自身的努力之外,你在上家公司混的差,还与你在组织架构中所处于的位置和组织架构本身有关。


一般公司会有两种组织架构方式:垂直化划分层级化划分


1. 垂直划分


垂直划分,多以业务线为模型进行划分。各条业务线共用公司行政资源,相互之间关联不大。


各业务线之间,内部拥有自治权。


image.png


如上图所示,公司共有四个业务线。




  • 业务线A,有前端和后端开发。因为成员能力比较强,所以没有测试运维等职位;




  • 业务线B倡导全栈技能,开发后台前端一体化;




  • 业务线C的管理能力比较强,仅靠少量自有研发,加上大量的外包,能够完成一次性工作。




  • 业务线D是传统的互联网方式,专人专岗,缺什么招什么,不提倡内部转岗




运行模式




  1. 业务线A缺人,缺项目,与业务线BCD无任何关系,不允许借调




  2. 业务线发展良好,会扩大规模;其他业务线同学想要加入需要经过复杂的流程,相当于重新找工作




  3. 业务线发展萎靡,会缩减人员,甚至会整体砍掉。优秀者会被打散吸收进其他业务线




好处




  1. 业务线之间存在竞争关系,团队成员有明确的奋斗目标和危机意识




  2. 一条业务线管理和产品上的失败,不会影响公司整体运营




  3. 可以比较容易的形成单向汇报的结构,避免成本巨大且有偏差的多重管理




  4. 便于复制成功的业务线,或者找准公司的发展重点




坏处




  1. 对业务线主要分管领导的要求非常高




  2. 多项技术和产品重复建设,容易造成人员膨胀,成本浪费




  3. 部门之间隔阂加大,共建、合作困难,与产品化相逆




  4. 业务线容易过度自治,脱离掌控




  5. 太激进,大量过渡事宜需要处理




修订


为了解决上面存在的问题,通常会有一个协调和监管部门,每个业务线,还需要有响应的协调人进行对接。以以往的观察来看,效果并不会太好。因为这样的协调,多陷于人情沟通,不好设计流程规范约束这些参与人的行为。


image.png


在公司未摸清发展方向之前,并不推荐此方式的改革。它的本意是通过竞争增加部门的进取心,通过充分授权和自治发挥骨干领导者的作用。但在未有成功案例之前,它的结果变成了:寄希望于拆分成多个小业务线,来解决原大业务线存在的问题。所以依然是处于不太确定的尝试行为。


2. 水平划分


水平划分方式,适合公司有确定的产品,并能够形成持续迭代的团队。


它的主要思想,是要打破“不会做饭的项目经理不是好程序员”的思维,形成专人专业专岗的制度。


这种方式经历了非常多的互联网公司实践,可以说是最节约研发成本,能动性最高的组织方式。主要是因为:




  • 研发各司其职,做好自己的本职工作可以避免任务切换、沟通成本,达到整体最优




  • 个人单向汇报,组织层级化,小组扁平化。“替领导负责,就是替公司负责”




  • 任何职位有明确的JD,可替换性高,包括小组领导




这种方式最大的问题就是,对团队成员的要求都很高。主动性与专业技能都有要求,需要经过严格的面试筛选。


坏处




  • 是否适合项目类公司,存疑




  • 存在较多技术保障部门,公共需求 下沉容易造成任务积压




  • 需要对其他部门进行整合,才能发挥更大的价值




分析


image.png


如上图,大体会分为三层。




  • 技术保障,保障公司的底层技术支撑,问题处理和疑难问题解决。小组多但人少,职责分明




  • 基础业务,公司的旗舰业务团队,需求变更小但任何改动都非常困难。团队人数适中




  • 项目演化,纯项目,可以是一锤子买卖,也可以是服务升级,属于朝令夕改类需求的聚居地。人数最多




可以看到项目演化层,多是脏活,有些甚至是尝试性的项目-----这是合理的。




  1. 技术保障和基础业务的技术能力要求高,业务稳定,适合长期在公司发展,发展属性偏技术的人群,流动性小,招聘困难




  2. 项目演化层,业务多变,项目奖金或者其他回报波动大,人员流动性高,招聘容易




成功的孵化项目,会蜕变成产品,或者基础业务,并入基础业务分组。


从这种划分可以看出,一个人在公司的命运和发展,在招聘入职的时候就已经确定了。应聘人员可以根据公司的需求进行判断,提前预知自己的倾向。


互联网公司大多数将项目演化层的人员当作炮灰,因为他们招聘容易,团队组件迅速,但也有很多可能获得高额回报,这也是很多人看中的。


3.组合


组合一下垂直划分和层级划分,可以是下面这种效果。


image.png


采用层级+垂直方式进行架构。即:首选层级模式,然后在项目演化层采用垂直模式,也叫做业务线,拥有有限的自治权。


为每一个业务线配备一个与下层产品化或者技术保障对接的人员。


绩效方面,上层的需求为下层的实现打分。基础业务和技术保障,为绿色的协调人员打分。他们的利益是一致的。


End


大公司出来的并不一定是精英,小公司出来的也并不一定是渣渣。这取决于他在公司的位置和所从事的内容。核心部门会得到更多的利益,而边缘的尝试性部门只能吃一些残羹剩饭。退去公司的光环,加上平庸的项目经历,竞争力自然就打上一个折扣。


以上,仅限IT行业哦。赵家人不在此列。

作者:小姐姐味道
来源:https://juejin.cn/post/7203651773622452261

收起阅读 »

Android 手写热修复dex

.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:16px;overflow-x:hidden;color:#252933}.markdown-bod...
继续阅读 »

现有的热修复框架很多,尤以AndFix 和Tinker比较多



具体的实现方式和项目引用可以参考网络上的文章,今天就不谈,也不是主要目的



今天就来探讨,如何手写一个热修复的功能



对于简单的项目,不想集成其他修复框架的SDK,也不想用第三方平台,只是紧急修复一些bug
还是挺方便的



言归正传,如果一个或多个类出现bug,导致了崩溃或者数据显示异常,如果修复呢,如果熟悉jvm dalvik 类的加载机制,就会清楚的了解 ClassLoader的 双亲委托机制 就可以通过这个


什么是双亲委托机制



  1. 当前ClassLoader首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。
    每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。

  2.  当前classLoader的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到bootstrp ClassLoader.

  3. 当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。



突破口来了,看1(如果已经加载则直接返回原来已经加载的类)
对于同一个类,如果先加载修复的类,当后续在加载未修复的类的时候,直接返回修复的类,这样bug不就解决了吗?



Nice ,多看源码和jvm 许多问题可以从framework和底层去解决


话不多说,提出了解决方法,下面着手去实现


public class InitActivity extends FragmentActivity {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//这里默认在SD卡根目录,实际开发过程中可以把dex文件放在服务器,在启动页下载后加载进来
//第二次进入的时候可以根据目录下是否已经下载过,处理,避免重新下载
//最后根据当前app版本下载不同的修复dex包 等等一系列处理
String dexFilePath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/fix.dex";
DexFile dexFile = null;
try {
dexFile = DexFile.loadDex(dexFilePath, null, Context.MODE_PRIVATE);
} catch (IOException e) {
e.printStackTrace();
}

patchDex(dexFile);

startActivity(new Intent(this, MainActivity.class));
}

/**
* 修复过程,可以放在启动页,这样在等待的过程中,网络下载修复dex文件
*
* @param dexFile
*/

public void patchDex(DexFile dexFile) {
if (dexFile == null) return;
Enumeration<String> enumeration = dexFile.entries();
String className;
//遍历dexFile中的类
while (enumeration.hasMoreElements()) {
className = enumeration.nextElement();
//加载修复后的类,只能修复当前Activity后加载类(可以放入Application中执行)
dexFile.loadClass(className, getClassLoader());
}
}
}
复制代码

方法很简单在启动页,或者Application中提前加载有bug的类



这里写的很简单,只是展示核心代码,实际开发过程中,dex包下载的网络请求,据当前app版本下载不同的修复dex,文件存在的时候可以在Application中先加载一次,启动页就不用加载,等等,一系列优化和判断处理,这里就不过多说明,具体一些处理看github上的代码



###ok 代码都了解了,这个 fix.dex 文件哪里来的呢
熟悉Android apk生成的小伙伴都知道了,跳过这个步骤,不懂的小伙伴继续往下看


上面的InitActivitystartActivity(new Intent(this, MainActivity.class)); 启动了一个MainActivity
看看我的MainActivity


public class MainActivity extends FragmentActivity {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//0不能做被除数,这里会报ArithmeticException异常
Toast.makeText(this, "结果" + 10 / 0, Toast.LENGTH_LONG).show();
}
}
复制代码

哎呀不小心,写了一个bug 0 咋能做除数呢,app已经上线了,这里必崩啊,咋办
不要急,按照以下步骤:



  1. 我们要修复这个类MainActivity,先把bug解决


 Toast.makeText(this, "结果" + 10 / 2, Toast.LENGTH_LONG).show();
复制代码


  1. 把修复类生成.class文件(可以先run一次,之后在 build/intermediates/javac/debug/classes/com开的的文件夹,找到生成的class文件,也可以通过javac 命令行生成,也可以通过右边的gradle Task生成)
    class 路径图

  2. 把修复类.class文件 打包成dex (其他.class删除,只保留修复类) 打开cmd命令行,输入下面命令


D:\Android\sdk\build-tools\28.0.3\dx.bat --dex --output C:\Users\pei\Desktop\dx\fix.dex C:\Users\pei\Desktop\dx\
复制代码

D:\Android\sdk 为自己sdk目录 28.0.3build-tools版本,可以根据自己已经下载的版本更换
后面两个目录分别是生成.dex文件目录,和.class文件目录



切记 .class文件的目录必须是包名一样的,我的目录是 C:\Users\pei\Desktop\dx\com\pei\test\MainActivity.class,不然会报 class name does not match path




  1. 这样dx文件夹下就会生成fix.dex文件了,把fix.dex放进手机根目录试试吧


再次打开App,完美Toast 结果5,完美解决


总结



  1. 修复方法要在bug类之前执行

  2. 适合少量bug,太多bug影响性能

  3. 目前只能修复类,不能修复资源文件

  4. 目前只能适配单dex的项目,多dex的项目由于当前类和所有的引用类在同一个dex会 当前类被打上CLASS_ISPREVERIFIED标记,被打上这个标记的类不能引用其他dex中的类,否则就会报错
    解决办法是在构造方法里引用一个单独的dex中的类,这样不符合规则就不会被标记了
作者:one裴s
来源:https://juejin.cn/post/7203989318271483960
收起阅读 »