注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

接受平庸,三年前端2022年总结

2022年即将结束,这一年尤为特殊,对每个人来说都是一次新的开始。 我以前很爱给自己定很多目标,似乎定得越多越能缓解我的焦虑,让我能有种不再碌碌无为,虚度光阴的错觉。尤其是在过了某个年龄段后,开始觉得时间流逝得近乎疯狂,感觉才刚习惯填写日期时填上2022,突然...
继续阅读 »

2022年即将结束,这一年尤为特殊,对每个人来说都是一次新的开始。


我以前很爱给自己定很多目标,似乎定得越多越能缓解我的焦虑,让我能有种不再碌碌无为,虚度光阴的错觉。尤其是在过了某个年龄段后,开始觉得时间流逝得近乎疯狂,感觉才刚习惯填写日期时填上2022,突然间就这么结束了。自己这一年好像在原地踏步,并没有做出太多改变。


我之前很长一段时间都不开心,一直在尝试着自救,以前觉得自救是拼命努力,踏出自己的舒适圈,一旦过得舒服点就会开始焦虑和自责。我不停地在学习新东西,这样似乎能缓解我的焦虑,确实学习能让我变得更好,但是这种心态会让生活质量变得很低。


现在慢慢开始觉得自救是接受自己的平庸,不要给自己定太高的目标。多去尝试一些一直想做但没有去做的事情,比如我一直想去学冲浪,去跳下伞。后悔的事情往往不是那些已经做过的,而是想做但没有去做的事。


1.工作


我做的最好的一次选择就是进入这个行业,这是我真正热爱的事情,其实实际算起来我从事前端开发才三年,但我每天上班不会再觉得煎熬,甚至有时还会有点期待。


这三年里心态也经历了几次变化,一开始觉得只有技术好才能到达一个更高的位置,后来一段时间找工作发现,包装也很重要,把简历改好点,多刷点题,进入好公司起点就和别人不一样了。


后来开始带了四五个组员,汇报变成我的工作之一,这时觉得,汇报好像更加重要。其他人幸苦完成的功能,变成了我PPT里的一个亮点,领导和其他人的赞赏,好像全变成了我的功劳,这时我总会惶恐地加一句也是大家的努力。


其实汇报也并不简单,它需要你更了解项目,掌握所有细节,当一个问题答不上时,其他人就会紧攥着不放,以此来否定你的工作。而且听汇报的往往都是不懂技术的人,需要把一个技术难点用通俗易懂的话讲清楚,这更加考验功力。


这段时期我没别的进步,表达能力上倒是突飞猛进,而且不会怯场。


现在我不会再排哪个最重要,每个时期都会有不同的侧重点,更应该要看的是自己的职业规划,想当架构师,那就更加注重技术。想转管理,就多提高下表达能力。


2.学习


第一次是刚开始工作的时候,那时候对一切都好奇,每天都在看各类教程,不停地在折腾东西,觉得要学习的东西实在太多了。但这段时间仅仅在学习怎么用,浅尝辄止,对很多框架的理解仅仅停留在表面,虽然曾经试图去读下源码,但很快就因为看不懂退缩了。


或许这也是当时我学习热情高涨的原因,我学了Vue、Spring Boot,自己摸索着把博客的前后端搭建起来,又去买了台服务器,配置好nginx,挑了个心仪的域名,去备案。一切都搭建好后,在浏览器输入自己的域名,看到博客打开的那一刻,那种感觉我到现在还记得。


在工作一段时间后我用了很多框架,写了足够多的代码,我渐渐碰到很多奇怪的问题,虽然在一番搜索后我解决了,但没有明白为什么,这时候我知道自己需要更加深入了。


我开始尝试去读源码,去学习优化,去动手实现一些封装好的功能。这时候,我终于从会用到大概知道为什么。


学习go是今年的事情,契机是我用Spring Boot搭建的博客服务挂了,再加上小程序的云开发开始收费,我迫切需要搭建起两套web服务,这时候就选中了正当红的编程语言Go。学语言其实还是挺快的,js、java、go,都有很多相似的地方。


有了以前的经验,用Gin搭建的web服务很快就起来了,我的博客和小程序得以满血复活。


截屏2023-01-05 21.17.31.png


开发这个小程序的初衷是想学习下云开发,当时自己正好很焦虑,迫切想改变,就决定开发一款能监督自己培养习惯的工具,但现在并不这么想了,现在更多是希望它能记录下这个过程,即使失败也没关系。


WechatIMG51.jpeg


3.健身


如果说这几年感触最深的事是什么,那一定是健康,人有时真的很脆弱,别奋斗了大半生最后化为乌有。


我开始重新去健身房,这次很顺利,坚持了很长时间,直到中间封城和后面放开才没再去,但在家的时候也会用哑铃继续练。


目前深蹲80kg,卧推50kg,其实可以更进一步,但我经常是一个人练,老害怕受伤,尤其是腰,如果受伤了还不如不练。所以暂时停在这两个重量,更多的是练习动作和感受发力,直到感觉对了再往上冲一冲。


从开始去健身房到现在,身体的变化也挺明显的,甚至因为背挺直了一些让我长高了一厘米,精神也更好了,不再像以前一样混混沌沌,也开始喜欢上运动。


23年我仍然会把健康放在第一位,除了去健身房外还会拓展更多运动项目。


4.英语


今年看了两本原版书,比预计的数量差好多。分别是 Wander 和 The twilight saga,非常推荐给词汇量不多的人,语法简单,故事精彩,很容易就能读下去。


WechatIMG57.jpeg


从以前英语就一直不是很好,已经成为我的一块心病。总结了以前学英语失败的经历,发现就是太追求速成,越追求速度反而越容易有挫败感,在认识到这应该是一个长期的过程后,就开始放慢脚步,从简单的读书和对话开始,避免背太多单词。


5.最后


接受了平庸并不是躺平,而是不再给自己设一些虚无缥缈的目标,2023年我还是会继续学习更多知识,解锁新的技能。


作者:Nothlu
来源:juejin.cn/post/7185347261683990565
收起阅读 »

工作 7 年的老程序员,现在怎么样了

犹记得高中班主任说:“大家努力考上大学,大学没人管,到时候随便玩”。我估计很多老师都这么说过。 我考上大学(2010年)之前也是这么过的。第一年哥哥给买了个一台华硕笔记本电脑。那个年代买华硕的应该不少,我周边就好几个。有了电脑之后,室友就拉着我一起 cs,四个...
继续阅读 »

犹记得高中班主任说:“大家努力考上大学,大学没人管,到时候随便玩”。我估计很多老师都这么说过。


我考上大学(2010年)之前也是这么过的。第一年哥哥给买了个一台华硕笔记本电脑。那个年代买华硕的应该不少,我周边就好几个。有了电脑之后,室友就拉着我一起 cs,四个人组队玩,那会觉得很嗨,上头。


后来看室友在玩魔兽世界,那会不知道是什么游戏,就感觉很好玩,再后来就入坑了。还记得刚开始玩,完全不会,玩个防骑,但是打副本排DPS,结果还被人教育,教育之后还不听(因为别的职业不会玩),就经常被 T 出组。之后,上课天天看游戏攻略和玩法,或者干脆看小说。前两年就这么过去了


1 跟风考研


大三开始,觉得这么混下去不行了。在豆瓣上找了一些书,平时不上课的时候找个自习室学习。那会家里打电话说有哪个亲戚家的孩子考研了,那是我第一次知道“考研”这个词。那会在上宏微观经济学的课,刚好在豆瓣上看到一本手《牛奶面包经济学》,就在自习室里看。刚好有个同院系的同学在里面准备考研,在找小伙伴一起战斗(毕竟,考研是一场长跑,没有同行者,会很艰难)。我一合计,就加入了他们的小团队。从此成为“中国合伙人”(刚好四个人)中的一员。


我那会也不知道毕业了之后能去哪些公司,能找哪些岗位,对于社会完全不了解,对于考研也是完全不了解。小团队中的三个人都是考金融学,我在网上查,知道了学硕和专硕的区别,也知道专硕学费贵。我家里没钱,大学时期的生活费都是自己去沃尔玛、麦当劳、发传单挣得,大学四年,我在沃尔玛工作超过了 2 年、麦当劳半年,食堂倒盘子半年,中途还去发过传单,暑假还去实习。没钱,他们考金融学专硕,那我就靠经济学学硕吧,学硕学费便宜。


从此开始了考研之路。


2 三次考研


大三的时候,报名不是那么严格,混进去报了名,那会还没开始看书,算是体验了一把考研流程;


还记得那次政治考了 48 分,基本都过了很多学校的单科线,那会就感觉政治最好考(最后发现,还是太年轻)。


大四毕业那年,把所有考研科目的参数书都过了 2 遍,最后上考场,最后成绩也就刚过国家线。


毕业了,也不知道干啥,就听小伙伴的准备再考一次,之前和小伙伴一起来了北京,租了个阳台,又开始准备考研。结果依然是刚过国家线。这一年也多亏了一起来北京的几个同学资助我,否则可能都抗不过考试就饿死街头了。


总结这几次考研经历,失败的最大原因是,我根本不知道考研是为了什么。只是不知道如果工作的话,找什么工作。刚好别人提供了这样一个逃避工作的路,我麻木的跟着走而已。这也是为什么后面两次准备的过程中,一有空就看小说的原因。


但是,现在来看,我会感谢那会没有考上,不然就错过了现在喜欢的技术工作。因为如果真的考上了经济学研究生,我毕业之后也不知道能干啥,而且金融行业的工作我也不喜欢,性格上也不合适,几个小伙伴都是考的金融,去的券商,还是比较了解的。


3 入坑 JAVA 培训


考完之后,大概估了分,知道自己大概率上不了就开始找工作了。那会在前程无忧上各种投简历。开始看到一个做外汇的小公司,因为我在本科在一个工作室做过外汇交易相关的工作,还用程序写了一段量化交易的小程序。


所以去培训了几天,跟我哥借了几千块钱,注册了一个账号,开始买卖外汇。同时在网上找其他工作。


后面看介绍去西二旗的一家公司面试,说我的技术不行,他们提供 Java 培训(以前的套路),没钱可以贷款。


我自己也清楚本科一行 Java 代码没写过,直接工作也找不到工作。就贷款培训了,那会还提供住宿,跟学校宿舍似的,上下铺。


4 三年新手&非全研究生


培训四个月之后,开始找工作。那会 Java 还没这么卷,而且自己还有个 211 学历,一般公司的面试还是不少的。但是因为培训的时候学习不够刻苦(也是没有基础)。最后进了一个小公司,面试要 8000,最后给了 7000。这也是我给自己的最底线工资,少于这个工资就离开北京了,这一年是 2015 年。


这家公司是给政府单位做内部系统的,包括中石油、气象局等。我被分配其中一个组做气象相关系统。第二年末的时候,组内的活对我来说已经没什么难度了,就偷偷在外面找工作,H3C 面试前 3 面都通过了,结果最后大领导面气场不符,没通过。最后被另外一家公司的面试官劝退了。然后公司团建的时候,大领导也极力挽留我,最后没走成。


这次经历的经验教训有 2 个,第 1 个是没有拿到 offer 之前,尽量不要被领导知道。第 2 个是,只要领导知道你要离职,就一定要离职。这次就是年终团建的时候,被领导留下来了。但是第二年以各种理由不给工资。


之前自己就一直在想出路,但是小公司,技术成长有限,看书也对工作没有太大作用,没有太大成长。之后了解到研究生改革,有高中同学考了人大非全。自己也就开始准备非全的考试。最后拿到录取通知书,就开始准备离职了。PS:考研准备


在这家公司马上满 3 年重新签合同的时候,偷偷面试了几家,拿到了 2 个还不错的 offer。第二天就跟直属领导提离职了。这次不管直属领导以及大领导如何劝说,还是果断离职了。


这个公司有两个收获。一个是,了解了一般 Java Web 项目的全流程,掌握了基本开发技能,了解了一些大数据开发技术,如Hadoop套件。另外一个是,通过准备考研的过程,也整理出了一套开发过程中应该先思而后行。只有先整理出


5 五年开发经历


第二家公司是一家央企控股上市公司,市场规模中等。主要给政府提供集成项目。到这家公司第二年就开始带小团队做项目,但是工资很低,可能跟公司性质有关。还好公司有宿舍,有食堂。能省下一些钱。


到这家公司的时候,非全刚好开始上课,还好我们 5 点半就下班,所以我天天卡点下班,大领导天天给开发经理说让我加班。但是第一学期要上课,领导对我不爽,也只能这样了。


后来公司来了一个奇葩的产品经理,但是大领导很挺他,大领导下面有 60 号人,研发、产品、测试都有。需求天天改,还不写在文档上。研发都开发完了,后面发现有问题,要改回去,产品还问,谁让这么改的。


是否按照文档开发,也是大领导说的算,最后你按照文档开发也不对,因为他们更新不及时;不按照文档开发也不对,写了你不用。


最后,研发和产品出差,只能同时去一波人,要是同时去用户现场,会打架。最后没干出成绩,产品和大领导一起被干走了。


后面我们整体调整了部门,部门领导是研发出身。干了几个月之后,领导也比较认可我的能力,让我带团队做一个中型项目,下面大概有 10 号人,包括前后端和算法。也被提升为开发经理。


最后因为工资、工作距离(老婆怀孕,离家太远)以及工作内容等原因,跳槽到了下一家互联网公司。


6 入行互联网


凭借着 5 年的工作经历,还算可以的技术广度(毕竟之前啥都干),985 学校的非全研究生学历,以及还过得去的技术能力。找到了一家知名度还可以的互联网公司做商城开发。


这个部门是公司新成立的部门,领导是有好几家一线互联网经验的老程序员,技术过硬,管理能力强,会做人。组内成员都年轻有干劲。本打算在公司大干一场,涨涨技术深度(之前都是传统企业,技术深度不够,但是广度可以)。


结果因为政策调整,整个部门被裁,只剩下直属领导以及领导的领导。这一年是 2020 年。这个时候,我在这个公司还不到 1 年。


7 再前行


拿着上家公司的大礼包,马上开始改简历,投简历,面试,毕竟还有房贷要还(找了个好老婆,她们家出了大头,付了首付),马上还有娃要养,一天也不敢歇息。


经过一个半月的面试,虽然挂的多,通过的少。最终还是拿了 3 个不错的offer,一个滴滴(滴滴面经)、一个XXX网络(最终入职,薪资跟滴滴基本差不多,技术在市场上认可度也还不错。)以及一个建信金科的offer。


因为大厂部门也和上家公司一样,是新组建的部门,心有余悸。然后也还年轻,不想去银行躺平,也怕银行也不靠谱,毕竟现在都是银行科技公司,干几年被裁,更没有出路。最终入职XXX网络。


8 寒冬


入职XXX网络之后,开始接入公司的各种技术组件,以及看到比较成熟的需求提出、评估、开发、测试、发布规范。也看到公司各个业务中心、支撑中心的访问量,感叹还是大公司好,流程规范,流量大且有挑战性。


正要开心的进入节奏,还没转正呢(3 个月转正),组内一个刚转正的同事被裁,瞬间慌得一批。


刚半年呢,听说组内又有 4 个裁员指标,已经开始准备简历了。幸运的是,这次逃过一劫。


现在已经 1 年多了,在这样一个裁员消息满天飞的年代,还有一份不错的工作,很幸运、也很忐忑,也在慢慢寻找自己未来的路,共勉~


9 总结


整体来看,我对自己的现状还算满意,从一个高中每个月 300 块钱生活费家里都拿不出来;高考志愿填报,填学校看心情想去哪,填专业看专业名字的的村里娃,走到现在在北京有个不错的工作,组建了幸福的家庭,买了个不大不小的房子的城里娃。不管怎么样,也算给自己立足打下了基础,留在了这个有更多机会的城市;也给后代一个更高的起点。


但是,我也知道,现在的状态并不稳固,互联网工作随时可能会丢,家庭成员的一场大病可能就会导致整个家庭回到解放前。


所以,主业上,我的规划就是,尽力提升自己的技术能力和管理能力,争取能在中型公司当上管理层,延迟自己的下岗年龄;副业上,提升自己的写作能力,尝试各种不同的主题,尝试给各个自媒体投稿,增加副业收入。


希望自己永

作者:六七十三
来源:juejin.cn/post/7173506418506072101
远少年,不要下岗~


收起阅读 »

html手写一个打印机效果-从最基础到学会

web
手写一个打印机效果 啥叫打印机效果,话不多说,直接上效果。我们可以自己写入一段文本然后通过html的方式,让它跟打印机一样,一个一个的打印到页面,并且还可以一个一个的删除。在这里我先浅说一下,我们的实现技巧,定时器setTimeout控制时间,然后for循环遍...
继续阅读 »

手写一个打印机效果


啥叫打印机效果,话不多说,直接上效果。我们可以自己写入一段文本然后通过html的方式,让它跟打印机一样,一个一个的打印到页面,并且还可以一个一个的删除。在这里我先浅说一下,我们的实现技巧,定时器setTimeout控制时间,然后for循环遍历写入到页面上。


封装的打印js
main(str,text)直接传入要写入的数组对象和要写入的元素。
copy.js 下载到本地引入然后调用它就可以了
image.png


代码


先拿到我们要写入的元素,然后设置好我们要写入的内容。


 var text = document.querySelector('.text');
var str = ['你好 ,我是一名刚入坑不久的大三在校生。', '现在学习都是为了将来的工作。', '希望能够得到大家的鼓励,谢谢!']

基础代码一


首先这里,我们先实现一个只有一段文字的实现效果。实现思路就是通过计时器,控制好时间,每次写入的文字通过str[0].substr(0, k)拿到,需要注意的是,因为是异步任务,回退的时候,我们的时间要设置好,加上写入完的时间1000 + 200 * str[0].length)


  写入
for (let j = 0; j < str[0].length; j++) {
// 使用 setTimeout 函数来实现每个字符的延时输出
setTimeout(() => {
text.innerHTML = str[0].substr(0, j) // 显示当前字符串的前 j 个字符
}, 200 * j) // 延迟时间为 200 毫秒乘以 j,即每个字符间隔 200 毫秒
}

// 回退
// 在所有字符输出完成后,等待 1000 毫秒后开始回退
setTimeout(() => {
for (let k = str[0].length, i = 0; k >= 0; k--, i++) {
// 使用 setTimeout 函数来实现每个字符的延时输出
setTimeout(() => {
text.innerHTML = str[0].substr(0, k) // 显示当前字符串的前 k 个字符
}, 200 * i) // 延迟时间为 200 毫秒乘以 i,即每个字符间隔 200 毫秒
}
}, 1000 + 200 * str[0].length) // 等待时间为 1000 毫秒加上所有字符输出的延时时间

基础代码二 错误代码


首先这个代码是错误的
为了能让大家更好的看到错误的效果,于是我把这个代码也上传了。大家可以看到,在这里,页面上的文字总是会莫名奇怪的出现删除,根本不是我们想要的。其实我们也只是对上面一个代码进行了一个for循环遍历,却出现了这样的效果。其实这导致的原因就是setTimeout是异步任务,时间没有控制好。即每个字符串的打印和删除都是异步任务,无法保证它们的执行顺序。因此,可能会出现多个字符串的打印和删除任务交错执行的情况,导致效果不符合预期。



 // 即每个字符串的打印和删除都是异步任务,无法保证它们的执行顺序。因此,可能会出现多个字符串的打印和删除任务交错执行的情况,导致效果不符合预期。
// 整个str 这是一个有问题的代码 因为计算时间太麻烦了 都是异步任务
// for (let s = 0; s < str.length; s++) {
// // 写入
// for (let j = 0; j < str[s].length; j++) {
// setTimeout(() => {
// text.innerHTML = str[s].substr(0, j)
// }, 200 * j)
// }
// // 回退
// setTimeout(() => {
// for (let k = str[s].length, i = 0; k >= 0; k--, i++) {
// setTimeout(() => {
// text.innerHTML = str[s].substr(0, k)
// }, 200 * i)

// }
// }, 1000 + 200 * str[s].length)
// }

基础代码三


为了解决上面的问题,我们使用了函数封装并且使用了回调函数实现我们想要的效果。我们将打印和删除都封装成一个含有回调函数的函数,为什么要含有回调函数呢?这是为了我们下面对一个字符串打印和删除的函数做封装。打我们打印完一个字符串时,我们才会执行删除。所有我们将删除函数放到打印的回调函数中去执行。然后我们将打印整个字符串数组进行封装,因为我们在删除的里面也有一个回调函数,那么我们可以在这个回调函数里去执行打印下一条字符串,这样就防止了控制时间不准确的问题。


 // 打印字符串
function printText(str, callback) {
var i = 0;
var timer = setInterval(function () {
text.innerHTML = str.substr(0, i); // 将字符串的前缀赋值给显示文本的元素
i++;
if (i > str.length) { // 如果已经打印完整个字符串
clearInterval(timer); // 停止定时器
callback && callback(); // 调用回调函数
}
}, 200); // 每 200 毫秒打印一个字符
}

// 删除字符串
function deleteText(str, callback) {
var i = str.length;
var timer = setInterval(function () {
text.innerHTML = str.substr(0, i); // 将字符串的前缀赋值给显示文本的元素
i--;
if (i < 0) { // 如果已经删除到空字符串
clearInterval(timer); // 停止定时器
callback && callback(); // 调用回调函数
}
}, 200); // 每 200 毫秒删除一个字符
}

// 打印和删除字符串
function printAndDeleteText(str, callback) {
printText(str, function () { // 先打印字符串
setTimeout(function () {
deleteText(str, callback); // 等待 1 秒后再删除字符串
}, 1000);
});
}

// 循环遍历字符串数组,依次打印和删除字符串
function printAndDeleteAllText(strArr) {
function printAndDeleteNext(i) {
if (i >= strArr.length) { // 如果已经处理完所有字符串
printAndDeleteNext(0); // 重新从头开始处理
} else {
printAndDeleteText(strArr[i], function () { // 先打印字符串
i++;
printAndDeleteNext(i); // 递归调用自身,处理下一个字符串
});
}
}
printAndDeleteNext(0); // 开始处理第一个字符串
}
// 开始打印和删除字符串数组中的所有字符串
printAndDeleteAllText(str)

最优代码


其实我们做了,这么多,最后就是为了解决异步任务。
所以我这里直接采用Promiseasync await解决上面的问题。我们通过Promise解决实现打印和删除的异步任务。我们通过async await封装整个运行函数,解决了定时器异步问题,不用再计算时间,又难有算不出来。


 // 最终版 封装 解决异步任务
function writeText(t, delay = 200) {
return new Promise((resolve, reject) => {
setTimeout(() => {
text.innerHTML = t; // 显示当前字符串 t
resolve(); // Promise 完成
}, delay) // 延迟 delay 毫秒后执行
})
}

async function main(str) {
while (true) { // 无限循环
for (let j = 0; j < str.length; j++) {
// 写入
for (let i = 0; i <= str[j].length; i++) {
await writeText(str[j].substr(0, i)) // 显示当前字符串的前 i 个字符
}
// 回退
// 回退前先等一秒
await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(); // 等待 1000 毫秒后 Promise 完成
}, 1000) // 等待 1000 毫秒
})
for (let i = str[j].length; i >= 0; i--) {
await writeText(str[j].substr(0, i), 200) // 显示当前字符串的前 i 个字符,间隔 200 毫秒
}
}
}
}
main(str)

源码


<!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>打印机效果</title>
<style>
.container {
display: flex;
/* 使用 flex 布局 */
flex-direction: column;
/* 垂直布局 */
align-items: center;
/* 水平居中 */
justify-content: center;
/* 垂直居中 */
height: 100vh;
/* 高度占满整个视口 */
}

h1 {
font-size: 3rem;
/* 字体大小 */
margin-bottom: 2rem;
/* 底部间距 */
text-align: center;
/* 居中对齐 */
}

.text {
font-size: 2rem;
/* 字体大小 */
font-weight: bold;
/* 字体加粗 */
text-align: center;
/* 居中对齐 */
border-right: 2px solid black;
/* 添加光标效果 */
white-space: nowrap;
/* 不换行 */
overflow: hidden;
/* 隐藏超出部分 */
animation: blink 0.5s step-end infinite;
/* 添加光标闪烁效果 */
height: 3rem;
/* 设置一个固定的高度 */
}


@keyframes blink {

from,
to {
border-color: transparent;
/* 透明边框颜色 */
}

50% {
border-color: black;
/* 黑色边框颜色 */
}
}
</style>
</head>

<body>
<div class="container">
<h1>逐字打印和删除文字效果</h1>
<p class="text"></p>
</div>
</body>
<script>
var text = document.querySelector('.text');
var str = ['你好 ,我是一名刚入坑不久的大三在校生。', '现在学习都是为了将来的工作。', '希望能够得到大家的鼓励,谢谢!']


// 写入
// for (let j = 0; j < str[0].length; j++) {
// // 使用 setTimeout 函数来实现每个字符的延时输出
// setTimeout(() => {
// text.innerHTML = str[0].substr(0, j) // 显示当前字符串的前 j 个字符
// }, 200 * j) // 延迟时间为 200 毫秒乘以 j,即每个字符间隔 200 毫秒
// }

// // 回退
// // 在所有字符输出完成后,等待 1000 毫秒后开始回退
// setTimeout(() => {
// for (let k = str[0].length, i = 0; k >= 0; k--, i++) {
// // 使用 setTimeout 函数来实现每个字符的延时输出
// setTimeout(() => {
// text.innerHTML = str[0].substr(0, k) // 显示当前字符串的前 k 个字符
// }, 200 * i) // 延迟时间为 200 毫秒乘以 i,即每个字符间隔 200 毫秒
// }
// }, 1000 + 200 * str[0].length) // 等待时间为 1000 毫秒加上所有字符输出的延时时间


// 即每个字符串的打印和删除都是异步任务,无法保证它们的执行顺序。因此,可能会出现多个字符串的打印和删除任务交错执行的情况,导致效果不符合预期。
// 整个str 这是一个有问题的代码 因为计算时间太麻烦了 都是异步任务
// for (let s = 0; s < str.length; s++) {
// // 写入
// for (let j = 0; j < str[s].length; j++) {
// setTimeout(() => {
// text.innerHTML = str[s].substr(0, j)
// }, 200 * j)
// }
// // 回退
// setTimeout(() => {
// for (let k = str[s].length, i = 0; k >= 0; k--, i++) {
// setTimeout(() => {
// text.innerHTML = str[s].substr(0, k)
// }, 200 * i)

// }
// }, 1000 + 200 * str[s].length)
// }


// 最终版 封装 解决异步任务
function writeText(t, delay = 200) {
return new Promise((resolve, reject) => {
setTimeout(() => {
text.innerHTML = t; // 显示当前字符串 t
resolve(); // Promise 完成
}, delay) // 延迟 delay 毫秒后执行
})
}

async function main(str) {
while (true) { // 无限循环
for (let j = 0; j < str.length; j++) {
// 写入
for (let i = 0; i <= str[j].length; i++) {
await writeText(str[j].substr(0, i)) // 显示当前字符串的前 i 个字符
}
// 回退
// 回退前先等一秒
await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(); // 等待 1000 毫秒后 Promise 完成
}, 1000) // 等待 1000 毫秒
})
for (let i = str[j].length; i >= 0; i--) {
await writeText(str[j].substr(0, i), 200) // 显示当前字符串的前 i 个字符,间隔 200 毫秒
}
}
}
}
main(str)
</script>

</html>

作者:Mr-Wang-Y-P
来源:juejin.cn/post/7225178555827191868
收起阅读 »

URL刺客现身,竟另有妙用!

web
工作中大家会接触到形形色色的 url,有些完美遵循格式,有些却像刺客一样,冷不丁的给你一刀。 先介绍下我的惨痛经历,给大家避避坑,最后告诉大家一个 url 刺客的妙用。 刺客介绍 1. iOS WKWebview 刺客 此类刺客手段单一,只会影响 iOS WK...
继续阅读 »

工作中大家会接触到形形色色的 url,有些完美遵循格式,有些却像刺客一样,冷不丁的给你一刀。


先介绍下我的惨痛经历,给大家避避坑,最后告诉大家一个 url 刺客的妙用。


刺客介绍


1. iOS WKWebview 刺客


此类刺客手段单一,只会影响 iOS WKWebview



  • 空格


运营人员由于在通讯工具中复制粘贴,导致前面多了一个空格,没有仔细检查,直接录入了后台管理系统。



  • 中文


运营人员为了方便自身统计,直接在url中加入中文,录入了后台管理系统。


现象均为打开一个空白页,常见的处理手段如下:



  • 将参数里的中文URIEncode

  • 去掉首尾空格


const safeUrl = (url: string) => {
const index = url.indexOf('?');

if (index === -1) return url.trim();

// 这行可以用任意解析参数方法替代,仅代表要拿到参数,不考虑兼容性的简单写法
const params = new URLSearchParams(url.substring(index));
const paramStr = Object.keys(params)
.map((key: string) => {
return `${key}=${encodeURIComponent(params[key])}`;
})
.join('&');

const formatUrl = url.substring(0, index + 1) + paramStr;

return formatUrl.trim();
};

可以看到虽然这里提出了一个 safeUrl 方法,但如果业务中大量使用 window.location.href , window.location.replace, 之类的方法进行跳转,替换起来会比较繁琐.


再比如在 Hybrid App 的场景中,虽然都是跳转,打开新的 webview ,还是在本页面跳转会是不同的实现,所以在业务内提取一个公共的跳转方法更有利于健壮性和拓展性。


值得注意的是,如果链接上的中文可能是用于统计的,在上报打点时,应该将其值(前端/服务端处理均可)进行 URIDecode,否则运营人员会在后台看到一串串莫名其妙的 %XX ,会非常崩溃(别问我怎么知道的,可能只是伤害过太多运营)


2. 格式刺客


格式刺客指的是,不管何种原因,不知何种场景,就是不小心配错了,打错了,漏打了等。


比如:https://www.baidu.com 就被打成了 htps://www.baidu.com、www.baidu.com 等。


// 检查URL格式是否正确
function isValidUrl(url: string): boolean {
const urlPattern = new RegExp(
"^(https?:\/\/)?" + // 协议
"(([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})" + // 域名
"(:[0-9]{1,5})?" + // 端口号
"(\/.*)?$", // 路径
"i"
);
return urlPattern.test(url);
}

以上是一个很基础的判断,但是实际的应用场景中,有可能会需要填写相对路径,或者自定义的 scheme ,比如 wx:// ,所以检验的宽松度可以自行把握。


在校验到 url 配置可能存在问题时,可以上报到 sentry 或者其他异常监控平台,这样就可以比用户上报客服更早的发现潜在问题,避免长时间的运营事故。


3. 异形刺客


这种刺客在视觉上让人无法察觉,只有在跳转后才会让人疑惑不已。他也是最近被产品同学发现的,以下是当时的现场截图:



一段平平无奇的文本,跟着一段链接,视觉上无任何异常。


经过对跳转后的地址进行分析,发现了前面居然有一个这样的字符%E2%80%8B,好奇的在控制台中进行了尝试。




一个好家伙,这是什么,两个单引号吗?并不是,对比了很常用的 '%2B' ,单引号是自带的,那么我到底看到了什么,魔鬼嘛~


在进行了一番检索后知道了这种字符被称为零宽空格,他还有以下兄弟:



  • \u202b-\u202f

  • \ufeff

  • \u202a-\u202e


具体含义可以看看参考资料,这一类字符完全看不见,但是却对程序的运行产生了恶劣的影响。


可以使用这个语句去掉


str.replace(/[\u200b-\u200f\uFEFF\u202a-\u202e]/g, "");

刺客的妙用


头一天还被刺客气的瑟瑟发抖。第二天居然发现刺客的妙用。


场景:




  • 产品要求在微信环境隐藏标题




我方前端工程师:



  • 大手一挥,发功完毕,准备收工


document.title = '';

测试:




  • 来看看,页面A标题隐藏不了




我方前端工程师:


啊?怎么回事,本地调试还是好的,发上去就不行了,为什么页面A不可以,另外一个页面B只是参数变了变就行。


架构师出手:


页面A包含了开放标签,导致设置空Title失效,空,猛然想起了刺客,快用起来!


function setTitle(title: string) {
if (title) {
document.title = title;
} else {
document.title = decodeURIComponent('%E2%80%8B');
}
}

果然有效,成功解决了一个疑难杂症,猜测是微信里有不允许设置标题为空的机制,会在某些标签存在的时候被触发。(以上场景在 Android 微信 Webview 中可复现)


小结


以上只是工作中碰到 url 异常的部分场景和处理方案,如果小伙伴们也有类似的经历,可以在评论区中分享,帮助大家避坑,感谢朋友们的阅读,笔芯~


参考资料:


零宽字符 - 掘金LvLin


什么零宽度字符,以及零宽度字符在JavaScript中的应用 - 掘金whosmeya


作者:windyrain
来源:juejin.cn/post/7225133152490094651
收起阅读 »

关于 Emoji 你不知道的事

web
2022 年,支付宝上线了生僻字键盘,解决了“身份认证”环节中普通输入法经常打不出生僻字的问题。生僻字键盘是蚂蚁集团生僻字解决方案的一部分,本系列将持续分享其背后的字符编码科普文章,包括不限于:《文字是如何显示在屏幕上的?》、《字符编码工作者都在做什么》,敬请...
继续阅读 »

2022 年,支付宝上线了生僻字键盘,解决了“身份认证”环节中普通输入法经常打不出生僻字的问题。生僻字键盘是蚂蚁集团生僻字解决方案的一部分,本系列将持续分享其背后的字符编码科普文章,包括不限于:《文字是如何显示在屏幕上的?》、《字符编码工作者都在做什么》,敬请期待。


本文作者是蚂蚁集团前端工程师醉杭(👉 点击查看醉杭的成长故事),本篇将介绍 Emoji 的编码逻辑,以及如何在代码中正确处理 Emoji 。蚂蚁集团前端工程师七柚封装了字符处理 js 库,已开源,欢迎使用~ github.com/alipay/char…



结论先行



  • 基本 emoji 和常用 Unicode 字符毫无区别


每个 emoji 用对应一个 Unicode 码位,如:🌔 U+1F314 (对应 JS 中 UTF-16 编码是:"\uD83C\uDF14"),汉字 𠇔 U+201D4,对应 JS 中的 UTF-16 编码是"\uD840\uDDD4"



  • emoji 有特殊的修饰、拼接等规则


在某些 emoji 字符后增加一个肤色修饰符改变 emoji 的肤色、可以将多个 emoji 通过连接符拼接成一个emoji,这些特殊规则使得在代码中判定 emoji 的长度、截取和对 emoji 做其他处理都比较困难。需要澄清的是:用一个 Unicode 字符修饰前一个字符不是 emoji 独有的,其他 Unicode 字符也存在,如:Ü,由大写字母U(U+0055),后面跟一个连音符号(U+0308)组成。



  • 术语


码点/码位:Unicode 编码空间中的一个编码,如,汉字𠇔的码位是 201D4,通常表示为:U+201D4


起源


1982 年,卡内基美隆大学是首次在电子公告里中使:-)表情符号。后续在日本手机短信中盛行,并称为颜文字(日语:かおもじ,英文:emoticon),颜文字仍然是普通的文本字符。
1999 年,栗田穰崇 (Shigetaka Kurita) 发明了 e-moji (え-もじ),并设计了 176 个 emoji 符号,共 6 种颜色,分辨率为 12x12。
image.png
纽约博物馆馆藏:最初的 176 个 emoji


2010 年,Unicode 正式收录了 emoji,为每个 emoji 分配了唯一的码点。
2011 年,Apple 在 iOS 中加入了标准的 emoji 输入键盘,2 年后安卓系统也引入了 emoji 键盘。


Unicode


Unicode 中原本就收录了很多有意义的字符,如:㎓、𐦖、☳,大家还可以查看 Unicode 1 号平面的埃及象形文字区 (U+13000–U+1342F)。收录 emoji 对 Unicode 来说没有挑战,技术上是完全兼容的。
image.png
Unicode 象形文字区节选


Emoji 的编码


基本 emoji



基本 emoji 是指在 Unicode 编码表中用 1 个唯一码位表示的 emoji



最简单的 emoji 就是 Unicode 表中的一个字符,和我们常用的 Unicode 字符没有区别。多数基本 emoji 都被分配到 Unicode 编码表 1 号平面的 U+1F300–1F6FFU+1F900–1FAFF 两个区域,完整的列表请看15.0/emoji-sequences.txt
image.png
Unicode 中 emoji 的码位


我们常见的 emoji 是彩色的,而常见的字体是黑色的。字符的颜色取决于字体文件,如果你愿意,你也可以把其常见的汉字字体设计成彩色的。iOS/MacOS 的Apple Color Emoji字体是一种 160x160 的点阵字体, Android 的Noto Emoji是一种 128x128 的点阵字体,而 Windows 使用的 Segoe UI Emoji 是一种矢量彩色字体。


为什么同一个 emoji 在不同设备、不同软件中显示不同?
不同设备、软件使用了不同的 emoji 字体所以显示效果不同。Unicode 只是约定了码点到 emoji 的映射关系,并没有约定 emoji 图形,每个 emoji 字体文件可以按照自己的想法设计 emoji。
image.png
同一个 emoji 在不同软件上的显示效果


为什么在钉钉中发送**[憨笑]**会显示成image.png
早期包含 Unicode emoji 的字体还没广泛普及,你给对方发一个 emoji 符号😄,如果没对方设备有对应的字体看到的会是**?**
为了解决缺失 emoji 字体导致大家显示不一致的问题(或者为了方便自定义自己的**伪emoji**——为了方便描述,把软件自定义的图片称作伪 emoji),很多软件自己开发了能向下兼容的解决方案,如钉钉。该自定义方案与 Unicode 编码没有关系,只是将特殊的字符串与一张图片映射起来,当对方发送[xx]字符串时,将它显示成对应的图片
早期支付宝的转账备注功能中也定义了自己的伪emoji伪emoji的好处是向下兼容,如果使用标准的Unicode emoji 可能会导致别的系统无法处理(如:做了汉字正则校验),导致转账失败;弊端是不通用,别的系统通常不支持另一个系统定义的伪emoji,直接将[xx]文本显示出来,如:收银台在支付界面就会直接显示转账备注的伪 emoji 文本[xx]
image.png


字素集


字素集(grapheme cluster)在 Unicode 中通常一个码点对应一个字符,但是 Unicode 引入了特定的机制允许多个 Unicode 码点组合成一个字形符号。这样由于多个码点组合成的一个字符称作字素集。
比如Ü是一个字素集,是由两个码点组成:大写字母 U(U+0055),后面跟一个连音符号(U+0308)。再比如:'曙󠄀'.length=3'🤦🏼‍♂️'.length=7,前者由基本的字符加上一个变体选择符️ VS-17 (见后文)组成,后者由多个基础 emoji 修饰符、连接符组成。
点开有惊喜Ų̷̡̡̨̫͍̟̯̣͎͓̘̱̖̱̣͈͍̫͖̮̫̹̟̣͉̦̬̬͈͈͔͙͕̩̬̐̏̌̉́̾͑̒͌͊͗́̾̈̈́̆̅̉͌̋̇͆̚̚̚͠ͅ[左边是一个.length 为 65 的字素集,它是不可分割的一个字符]


在 Unicode 的规范中要求所有软件(本编辑器、文本渲染、搜索等)将一个字素集当做不可分割的整体,即:当做一个单一的字符对待。
image.png
Unicode 处理的难点就在于字素集,下文均与该定义有关,开发者的噩梦都源自该概念。不能简单地通过 .length 读取字符串的长度;如果想截取字符串的前 10 个字符,也不能简单的使用.substring(0, 10),因为这可能会截断 emoji 字符;反转字符串也非常困难,U+263A U+FE0F 有意义,反转之后 U+FE0F U+263A 却没有意义,后文会介绍正确的处理方式。


变体选择符️


Variation Selector(又叫异体字选择器),是 Unicode 中定义的一种修饰符机制。一个基本字符后接上一个异体字选择器组成一个异体字。背景是:一个字符可能会有多个不同的变体,这些变体本质上是同一个字符,具有同样的含义,由于地区、文化差异导致他们演变成了不同的书写形式。Unicode 为变体字分配了同一个码点,如果想要显示特定的书写形式可以在字符后紧接着一个异体字选择器指定。
image.pngimage.png就是变体字。需要澄清的是,并非所有相似的字符都按照异性字的形式合并成了一个码点,就是分别分配了不同的码点,理论上这两个字符也可以合并变体字共用一个码点。
在 Unicode 中引入彩色的 emoji 前就已经定义了一些黑色的图形符号,引入彩色 emoji 后,新的 emoji 与黑色的符号具有相同的含义,于是共用了同一个 Unicode 码点,可在字符后接上一个 VS 指定要显示那个版本。
常用的 VS 有 16 个 VS-1 ~ VS-16,对应的 Unicode 是(U+FE00~U+FE0F),其中 VS-15(U+FE0E)用于指定显示黑色的 text 版本,VS-16(U+FE0F)用于指定显示彩色的 emoji 版本。


默认显示VS-15 修饰符VS-16 修饰符
U+2702✂︎U+2702 U+FE0E✂︎U+2702 U+FE0F ✂️
U+2620☠︎U+2620 U+FE0E☠︎U+2620 U+FE0F ☠️
U+26A0⚛︎U+26A0 U+FE0E⚛︎U+26A0 U+FE0F ⚛️
U+2618☘︎U+2618 U+FE0E☘︎U+2618 U+FE0F ☘️

可以动手验证一下



image.png



  • ✂ 不含修饰符'\u2702'

  • ✂︎ 含 VS-15'\u2702\uFE0E'

  • ✂️ 含 VS-16'\u2702\uFE0F'



为什么把黑色的剪刀 ✂︎ 粘贴到 Chrome 搜索栏中显示成彩色,把彩色剪刀 ✂️ 复制到 Chrome 的 Console 中显示成黑色?
image.png image.png
我们通过 VS 符号告诉软件要显示成指定的异体字符,但是软件可以不听我们的,软件可能会强制指定特定的字体,如果该字体中只包含一种异体字符的字形数据那就只会显示该字形。


肤色修饰符


大多数人形相关的 Emoji 默认是黄色的,在 2015 年为 emoji 引入肤色支持。没有为每种肤色的 emoji 组合分配新的码点,而是引入了五个新码点作为修饰符:1F3FB 🏻、1F3FC 🏼、1F3FD 🏽、1F3FE 🏾、1F3FF 🏿 。肤色修饰符追加到现有的 emoji 后面则形成新的变种,如:👋 U+1F44B+ 🏽U+1F3FD= 👋🏽



  • 👋 在 JavaScript 中 UTF-16 值是'\uD83D\uDC4B'

  • **🏽 **在 JavaScript 中 UTF-16 值是'\uD83C\uDFFD'


组合在一起'\uD83D\uDC4B\uD83C\uDFFD'就得到了 👋🏽
image.png


5 种肤色修饰符的取值是基于菲茨帕特里克度量,因此叫做 EMOJI MODIFIER FITZPATRICK。肤色度量共有 6 个取值,但在 emoji 中前两个颜色合并成了一个。
image.png
最终 280 个人形 emoji 就产生了 1680 种肤色变种,这是五种不同肤色的舞者:🕺🕺🏻🕺🏼🕺🏽🕺🏾🕺🏿


零宽度连接符(ZWJ)


Unicode 通过多个基础 emoji 组合的形式表示某些复杂 emoji。组合的方式是在两个 emoji 之间添加一个U+200D,即:零宽度连接符(ZERO-WIDTH JOINER,简写为 ZWJ),如:



  • 👩 + ZWJ+ 🌾 = 👩‍🌾


image.png
下面是一些例子,完整的组合列表参考:Unicode 15.0/emoji-zwj-sequences.txt




  • 👩 + ✈️ → 👩‍✈️

  • 👨 + 💻 → 👨‍💻

  • 👰 + ♂️ → 👰‍♂️

  • 🐻 + ❄️ → 🐻‍❄️

  • 🏴 + ☠️ → 🏴‍☠️

  • 🏳️ + 🌈 → 🏳️‍🌈

  • 👨 + 🦰 → 👨‍🦰 (有意思的是:发色是通过 ZWJ 组合基础 emoji 实现,而肤色则是用肤色修饰符实现)

  • 👨🏻 + 🤝 + 👨🏼 → 👨🏻‍🤝‍👨🏼

  • 👨 + ❤️ + 👨 → 👨‍❤️‍👨

  • 👨 + ❤️ + 💋 + 👨 → 👨‍❤️‍💋‍👨

  • 👨 + 👨 + 👧 → 👨‍👨‍👧

  • 👨 + 👨 + 👧 + 👧 → 👨‍👨‍👧‍👧



可惜,有些 emoji 不是通过 ZWJ 组全 emoji 实现的,可能是因为没有赶上 ZWJ 定义的时机




  • 🌂 + 🌧 ≠ ☔️

  • 💄 + 👄 ≠ 💋

  • 🐴 + 🌈 ≠ 🦄

  • 👁 + 👁 ≠ 👀

  • 👨 + 💀 ≠ 🧟

  • 👩 + 🔍 ≠ 🕵️‍♀️

  • 🦵 + 🦵 + 💪 + 💪 + 👂 + 👂 + 👃 + 👅 + 👀 + 🧠 ≠ 🧍



旗帜·双字母连字


Unicode 中包含国旗符号,每个国旗也没有分配独立的码点,而是由双字符连字(ligature)来表示。(但 Windows 平台因为某些原因不支持显示,如果你是用 Windows 平台的浏览器阅读本文,只能说抱歉了)



  • 🇺 + 🇳 = 🇺🇳

  • 🇷 + 🇺 = 🇷🇺

  • 🇮 + 🇸 = 🇮🇸

  • 🇿 + 🇦 = 🇿🇦

  • 🇯 + 🇵 = 🇯🇵


这里的🇦 ~ 🇿不是字母,而是地区标识符,对应的码点是U+1F1E6~U+1F1FF,可以随意复制并组合,如果是合法的组合会显示成一个国家的旗帜。你可以在 MacOS 的 FontBook 中打开 Apple Color Emoji 查看到这些码点以及各个地区的旗帜符号
image.png image.png
完整地区标识符如下,你可以动手组合试一试:
🇦 🇧 🇨 🇩 🇪 🇫 🇬 🇭 🇮 🇯 🇰 🇱 🇲 🇳 🇴 🇵 🇶 🇷 🇸 🇹 🇺 🇻 🇼 🇽 🇾 🇿


标签序列


在 Unicode 中称作 Emoji Tag Sequence。在 Unicode 中U+E0020~ U+E007F 95 个码点表示的是Unicode 中不可见的控制符号,其中从E0061~E007A的 26 个码点分别表示小写的拉丁字符(不是常规的拉丁字母,而是 emoji 相关的控制字符),对应关系如下:




  • U+E0061 - TAG LATIN SMALL LETTER A

  • U+E0062 - TAG LATIN SMALL LETTER B



...




  • U+E007A - TAG LATIN SMALL LETTER Z



前文的双字母连字机制支持将两个地区标识符连接在一起表示一个旗帜符号。标签序列与之类似,是 Unicode 中定义的一种更复杂的连接方式,格式是:基础emoji+ 一串拉丁标签字符(U+E0061~U+E007A) + 结束符(U+E007F)
如:🏴 + gbeng + U+E007F = 🏴󠁧󠁢󠁥󠁮󠁧󠁿
其中 🏴 是基础 emoji U+1F3F4,_gbeng _分别代表对应的拉丁控制字符: g(U+E0067)b(U+E0062)e(U+E0065) n(U+E006E)g(U+E0067)U+E007F表示结束符,全称是 TAG CANCEL


/**
* 根据地区缩写返回对应的emoji
* 如:flag('gbeng') -> 🏴󠁧󠁢󠁥󠁮󠁧󠁿
*/

function flag(letterStr) {
const BASE_FLAG = '🏴';
const TAG_CANCEL = String.fromCodePoint(0xE007F);

// 将普通字母字符序列转换为"标签拉丁字符"序列
const tagLatinStr = (letterStr.toLowerCase().split('').map(letter => {
const codePoint = letter.charCodeAt(0) - 'a'.charCodeAt(0) + 0xE0061;
return String.fromCodePoint(codePoint);
})).join('');


return BASE_FLAG + tagLatinStr + TAG_CANCEL;
}

目前用这种方式表示的 emoji 共有三个



  • 🏴 + gbeng + U+E007F = 🏴󠁧󠁢󠁥󠁮󠁧󠁿 英格兰旗帜,完整序列:1F3F4 E0067 E0062 E0065 E006E E0067 E007F

  • 🏴 + gbsct + U+E007F = 🏴󠁧󠁢󠁳󠁣󠁴󠁿 苏格兰旗帜,完整序列:1F3F4 E0067 E0062 E0073 E0063 E0074 E007F

  • 🏴 + gbwls + U+E007F = 🏴󠁧󠁢󠁷󠁬󠁳󠁿 威尔士旗帜,完整序列:1F3F4 E0067 E0062 E0077 E006C E0073 E007F


键位符


共有 12 个键位符 #️⃣ *️⃣ 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣,规则是这样的:井号、星号和数字,加 U+FE0F 变成 emoji,再加上U+20E3变成带方框的键位符。







      • FE0F + 20E3 = *️⃣






  • + FE0F + 20E3 = #️⃣



  • 0 + FE0F + 20E3 = 0️⃣

  • ...


U+FE0F是前文提到的变体选择符中的VS-16,表示显示为 emoji 形态。JavaScript 中'\u0030'表示数字'0', '\u0030\ufe0f'则表示它的 emoji 变体,两者在 zsh 的 console 中显示效果不同,.length的值也不同。
image.png image.png


小结


一共有七种 emoji 造字法



  1. 基础emoji,单个码点表示一个emoji 🧛 U+1F9DB

  2. 单个码点 + 变体选择符 ⚛️ = ⚛︎ U+26A0 + U+FE0F

  3. 皮肤修饰符 🤵🏽 = 🤵 U+1F935 + 🏽 U+1F3FD

  4. **ZWJ连接符 ** 👨‍💻 = 👨 + ZWJ + 💻

  5. 旗帜符号 🇨🇳 = 🇨 + 🇳

  6. **标签序列 ** 🏴󠁧󠁢󠁳󠁣󠁴󠁿 = 🏴 + gbsct + U+E007F

  7. **键位序列 ** *️⃣ = * + U+FE0F + U+20E3


前四种方法也可以组合使用,可构造非常复杂的 emoji



U+1F6B5 🚵 个人山地骑行



  • U+1F3FB 浅色皮肤

  • U+200D ZWJ

  • U+2640 ♀️女性标志

  • U+FE0F 变体标志
    = 🚵🏻‍♀️ 浅色皮肤的女性山地骑行



/**
* 显示一个字符种所有的Unicode码点
*/

function codePoints(str) {
const result = [];
for(let i = 0; i < str.length; i ++) {
result.push(str.codePointAt(i).toString(16).toUpperCase());
}
return result;
}
codePoints('🚵🏻‍♀️') => ['1F6B5', 'DEB5', '1F3FB', 'DFFB', '200D', '2640', 'FE0F']

如何在代码中正确处理 emoji?


emoji 引入的问题


'中国人123'.length = 6'工作中👨‍💻'.length = 8
emoji 给编程带来的主要问题是视觉上看到的字符长度(后文称作视觉 length)与代码中获取的长度(后文称作技术 length)不相同,使得字符串截取等操作返回非预期内的结果,如:
'工作中👨‍💻'.substr(0,5) => '工作中👨''工作中👨‍💻'.substr(5)' => '‍💻'


本质上在 emoji 出现之前 Unicode 编码就遇到了该问题,只不过 emoji 的普及让该问题更普遍。有的 emoji 长度为 1,有的长度可以达到 15。问题的根源是 Unicode 中可以用多个码点表示一个 emoji,如果所有 emoji 都用一个 Unicode 码点表示就不存在该问题。
image.png


解法:视觉 length VS. 技术 length


解法显而易见,只要能将字符串中所有的字符元素按照视觉上看到的情况准确拆分,即:准确拆解字符串中的所有字素集
下述伪代码是要实现的效果,很多开源工具库就在做同样的事情,搜:Grapheme Cluster 即可。找到一个JavaScript版的grapheme-splitter,但是数据已经过时(勿用)。


const vs = new VisualString('工作中👨‍💻');
// vs.length => 4; // 视觉长度
// vs.physicalLength => 8; // 字符串长度
// vs[0] => 工
// vs[3] => 👨‍💻 // 按照所见即所得的方式拆分字符

// 字素集方法
// vs.substr(3,1) => 👨‍💻 // 截取字符

// 字素集属性
// vs[3].physicalLength => 5 // 物理长度
// vs[3].isEmoji => true // 是否是emoji

我们将产出工具库中将要提供这些能力



  1. 判断一个字符串中是否包含 emoji

  2. 将一个字符串准确拆分成若干个字素集

    • 每个字素集包含这些属性:isEmojiphysicalLength



  3. 按照字素集对字符串做截取操作

    • 基础截取: new VisualString('👨123👨‍💻').substr(1, 4) => '123👨‍💻'

    • 限定物理长度截取:new VisualString('👨123👨‍💻').substr(1, 4, 6) => '123',最后一个参数6代表最大物理长度,其中'123👨‍💻'.length = 8,如果限定最大物理长度6则只能截取到'123'备注:在产品体验上我们遵循“所见即所得”,但是在后端系统中传输和存储时候要遵循物理长度的限制,因此需要提供限定物理长度的截取能力。




版本兼容问题


如果 A 向 B 发送了一个组合 emoji「工作👨‍💻123」,B 的系统或软件中版本低(兼容的 Unicode 版本低)不支持该组合 emoji,看到的可能会是「工作👨💻123」。
用看到的是👨‍💻还是👨💻取决于用户的操作系统、软件和字体,我们提供的 JS 库无法感知到用户最终看到的是什么。我们提供的 JS 库会按照最新 Unicode 规范实现,无论用户看到的是什么都会把它当成一个字符(准确地说是字素集),即:
const vs = new VisualString('工作👨💻123'); vs.length => 6; vs[2] => '👨💻'
有办法可以一定程度上解决上述问题,但是我们觉得可能不解决才是正确的做法。


一个彩蛋


最后希望你使用 emoji 愉快 😄
发现 emoji 的维护者彻底贯彻「众生平等」,除了推出了不同肤色的 emoji 外,竟还设计了一个 Pregnant Man :)
image.png 🤰🫃🫄🏼
以上是分别是 woman、man、person,emoji 的新趋势是设计中性的 emoji




参考



作者:支付宝体验科技
来源:juejin.cn/post/7225074892357173308
收起阅读 »

微信黑科技

我一直认为技术是用来服务于用户,提升用户体验,而不是像 拼多多,写了恶意代码,操控用户的手机 ,利用技术做一些不好的事,今天这篇文章主要分享微信如何利用黑科技,减少 512MB 内存,降低 OOM 和 Native Crash 提升用户体验。 在上一篇文章谁动...
继续阅读 »

我一直认为技术是用来服务于用户,提升用户体验,而不是像 拼多多,写了恶意代码,操控用户的手机 ,利用技术做一些不好的事,今天这篇文章主要分享微信如何利用黑科技,减少 512MB 内存,降低 OOM 和 Native Crash 提升用户体验。


在上一篇文章谁动了我的内存,揭秘 OOM 崩溃下降 90% 的秘密 中分享了内存相关的知识点,包含堆、虚拟内存、发生 OOM 的原因,以及 为什么虚拟内存不足主要发生在 32 位的设备上导致虚拟内存不足的原因都有那些,目前都有哪些黑科技帮助我们去降低 OOM,有兴趣的小伙伴可以前往查看,从这篇文章开始细化每个知识点。


随着业务的增长,32 位设备上虚拟内存不足问题会越来越突出,尤其是大型应用会更加明显。除了业务上的优化之后,还需要一些黑科技尽可能降低更多的内存,而今天这篇主要分析微信分享的「堆空间减半」的方案,最高可减少 512MB 内存,从而降低 OOM 和 Native Crash,在开始之前,我们需要介绍一下 相关的知识点。


根据 Android 源码中的解释,Java 堆的大小应该是根据 RAM Size 来设置的,这是一个经验值,厂商是可以更改的,如果手机 Root 之后,自己也可以改,Google 源码的设置如下如下图所示

android.googlesource.com/platform/fr…



RAM (MB)-dalvik-heap. mkheapgrowthlimit (MB)heapsize (MB) 需要设置 android: largeHeap 为 true
512-dalvik-heap. mk48128
1024-dalvik-heap. mk96256
2048-dalvik-heap. mk192512
4096-dalvik-heap. mk192512
6144-dalvik-heap. mk256512
无论 RAM 多大,到目前为止堆的最大上限都是 512MB

正如上面表格所示,在 AndroidManifest.xml 文件 Application 节点中设置 android:largeHeap="true" 和不设置 largeHeap 获取到的最大堆的上限是不一样。


"true">



为什么默认关闭 android:largeHeap


Java 堆用于分配 Java / Kotlin 创建的对象,由 GC 管理和回收,GC 回收时将 From Space 里的对象复制到 To Space,这两片区域分别为 dalvik-main spacedalvik-main space 1, 这两片区域的大小和 Java 堆大小一样,如下图所示。



图中我们只需要关注 size(虚拟内存) 即可,如果 Java 堆的上限是 512 MB,那么 dalvik-main space(512 MB)dalvik-main space 1(512 MB) 共占用 1G 的虚拟内存。


如果堆的上限越大,那么 main space 占用的虚拟内存就会越大,在 32 位设备上,用户空间可用虚拟内存只有 3G,但是如果堆上限是 512MB,那么 main space 总共占用 1G 虚拟内存,剩下只有 2G 可用,因此 Google 在默认情况下会关闭 android:largeHeap 选项,只有在有需要的时候,主动设置 android:largeHeap = true,尝试获取更大的堆内存。


main space 占用虚拟内存的计算方式是不一样的。


Android 5. x ~ Android 7. x



  • 如果设置 android:largeHeap = true 时,main space size = dalvik.vm.heapsize,如果 heapsize 是 512MB,那么两个 main space 共占用 1G 虚拟内存

  • 如果不设置 largeHeap,那么 main space size = dalvik.vm.heapgrowthlimit,如果 heapgrowthlimit 是 256 MB,那么两个 main space 共占用 512 MB 虚拟内存


>= Android 8. x


无论 AndroidManifest 是否设置 android:largeHeapmain space size = dalvik.vm.heapsize * 2,如果 dalvik.vm.heapsize 是 512MB 那么 main space 占用 1G 的虚拟内存内存。


main space 在不同的系统分配方式是不一样的。



  • Android 5.x ~ Android 7.x 中,系统分配两块 main space,它们占用虚拟内存的大小和堆的大小是一样的

  • >= Android 8.x 之后,只分配了一个 main space,但是它占用虚拟内存的大小是堆的 2 倍


不同的系统上,它们的实现方式是不一样的,所以我们要采用不同的方法来释放 main space 占用的内存。


在 Android 5. x ~ Android 7. x


5.0 之后使用的是 ART 虚拟机,在 ART 虚拟机引入了,两种 Compacting GC 分为 Semi-Space(SS)GC (半空间压缩) 和 Generational Semi-Space(GSS)GC (分代半空间压缩)。 GSS GCSS GC 的改进版本,作为 background GC 的默认实现方式。


这两种 GC 的共同点,存在两片大小和堆大小一样的内存空间分别作为 From SpaceTo Space,这两片区域分别为 dalvik-main space1dalvik-main space2



上面的这两块区域对应的源码 地址

cs.android.com/android/_/a…



执行 Compact / Moving GC 的时候才会使用到这两片区域,在 GC 执行期间,将 From Space 分配的还存活的对象会依次拷贝到 To Space 中,在复制对象的过程中 From Space 中的碎片就会被消除,下次 GC 时重复这套逻辑,但是 GSS GC 还多了一个 Promote Space


Promote Space 主要存储老年代的对象,老年代对象的存活性要比新生代的久,因此将它们拷贝到 Promote Space 中去,可以避免每次执行 GSS GC 时,都需要对它们进行无用的处理。


新生代和老年代采用的不同的算法:



  • 新生代:复制算法。在两块 space 来回移动,高效且执行频繁,每次 GC 不需要挂起线程

  • 老年代:标记-压缩算法。会在 Mark 阶段是在挂起除当前线程之外的所有其它运行时线程,然后在 Compact 阶段才移动对象,Compact 方式是 Sliding Compaction,也就是在 Mark 之后就可以按顺序一个个对象 “滑动” 到空间的某一侧,移动的时候都是在一个空间内移动,不需要多一份空间


如何释放掉其中一个 main space 占用的内存


释放方案,可以参考腾讯开源的方案 Matrix,总来的来说分为两步:

github.com/Tencent/mat…



  • 确定 From SpaceTo Space 的内存地址

  • 调用 munmap 函数释放掉其中一个 Space 所占用的内存


如何确定 From Space 和 To Space 的内存地址


我们需要读取 mpas 文件,然后搜索关键字 main spacemain space 1,就可以知道 main spacemain space 1 的内存地址。


当我们知道 space 的内存地址之后,我们还需要确认当前正在使用的是那个 space,才能安全的调用 munmap 函数,释放掉另外一个没有使用的 space


matrix 的方案,创建一个基本类型的数组,然后通过 GetPrimitiveArrayCritical 方法获取它的地址,代码如下:



调用 GetPrimitiveArrayCritical 方法会返回对象的内存地址,如果地址在那块区域,当前的区域就是我们正在使用的区域,然后我们就可以安全的释放掉另外一个 space 了。



释放掉其中一个 Space 会有问题吗?


如果我们直接释放掉其中一个 Space,在执行 Compact / Moving GC 的时候,需要将 From Space 分配的对象依次拷贝到 To Space 中,因为找不到 To Space,会引起 crash, 所以需要阻止 Moving GC


源码中也说明了调用 GetPrimitiveArrayCritical 方法可以阻止 Moving GC。



GetPrimitiveArrayCritical 方法会调用 IncrementDisableMovingGC 方法阻止 Moving GC,对应的源码如下。

https://android. googlesource. com/platform/art/+/master/runtime/gc/heap. cc #956


void Heap::IncrementDisableMovingGC(Thread* self) {
// Need to do this holding the lock to prevent races where the GC is about to run / running when
// we attempt to disable it.
ScopedThreadStateChange tsc(self, kWaitingForGcToComplete);
MutexLock mu(self, *gc_complete_lock_);
++disable_moving_gc_count_;
if (IsMovingGc(collector_type_running_)) {
WaitForGcToCompleteLocked(kGcCauseDisableMovingGc, self);
}
}

所以只需要调用 GetPrimitiveArrayCritical 方法,阻止 Moving GC,也就不需要用到另外一个空间了,因此可以安全的释放掉。


阻止 Compact / Moving GC 会有性能问题吗


按照微信给出的测试数据,在性能上没有明显的变化。



OS Version >= Android 8. x


8.0 引入了 Concurrent Copying GC(并发复制算法),堆空间也变成了 RegionSpace。RegionSpace 的算法并不是靠把已分配对象在两片空间之间来回倒腾来实现的,分析 smaps 文件,发现也只创建了一个 main space,但是它占用的虚拟内存是堆的 2 倍,所以 8.0 之前的方案释放另外一个 space 是无法使用的。


为什么没有创建 main space2


我们从源码看一下创建 main space2 的触发条件。


if (foreground_collector_type_ == kCollectorTypeCC) {
use_homogeneous_space_compaction_for_oom_ = false;
}

bool support_homogeneous_space_compaction =
background_collector_type_ == gc::kCollectorTypeHomogeneousSpaceCompact ||
use_homogeneous_space_compaction_for_oom_;

if (support_homogeneous_space_compaction ||
background_collector_type_ == kCollectorTypeSS ||
foreground_collector_type_ == kCollectorTypeSS) {

ScopedTrace trace2("Create main mem map 2");
main_mem_map_2 = MapAnonymousPreferredAddress(
kMemMapSpaceName[1], main_mem_map_1.End(), capacity_, &error_str);
}

正如如源码所示,后台回收器类型 kCollectorTypeHomogeneousSpaceCompactkCollectorTypeCC 才会创建 main space2



  • kCollectorTypeHomogeneousSpaceCompact(同构空间压缩(HSC),用于后台回收器类型)

  • kCollectorTypeCCCompacting GC) 分为两种类型

    • Semi-Space(SS)GC (半空间压缩)

    • Generational Semi-Space(GSS)GC (分代半空间压缩),GSS GCSS GC 的改进版本




而 Android 8.0 将 Concurrent Copying GC 作为默认方式,对应的回收器的类型是 kCollectorTypeCCBackground



Concurrent Copying GC 分为 Pause, Copying, Reclaim 三个阶段,以 Region 为单位进行 GC,大小为 256 KB。



  • pause: 这个阶段耗时非常少,这里很重要的一块儿工作是确定需要进行 GC 的 region, 被选中的 region 称为 source region

  • Copying:这个阶段是整个 GC 中耗时最长的阶段。通过将 source region 中对象根据 root set 计算并标记为 reachable,然后将标记为 reachable 的对象拷贝到 destination region

  • Reclaim:在经过 Copying 阶段后,整个进程中就不再存在指向 source regions 的引用了,GC 就可以将这些 source region 的内存释放供以后使用了。


Concurrent Copying GC 使用了 read barrier 技术,来确保其它线程不会读到指向 source region 的对象,所以不会将 app 线程挂起,也不会阻止内存分配。


如何减少 main space 占用的内存


Adnroid 8.0 之后使用的阿里巴巴 Patrons 的方案,在虚拟内存占用超过一定阈值时调用 RegionSpace 中的 ClampGrowthLimit 方法来缩减 RegionSpace 的大小。


但是 ClampGrowthLimit 只在 Android 9.0 以后才出现,8.0 是没有的,所以参考了 Android 9.0 的代码实现了一个 ClampGrowthLimit。



ClampGrowthLimit 方法中,通过调用 MemMap::SetSize 方法来调整 RegionSpace 的大小。

https://android. googlesource. com/platform/art/+/5f0b71ab2f60f76b5f73402bd1fdd25bbc179b6c/runtime/gc/space/region_space. cc #416



MemMap::SetSize 方法的实现。

https://android. googlesource. com/platform/art/+/android-9.0.0_r7/runtime/mem_map. cc #883



new_base_size_base_size_ 不相等的情况下会执行 munmap 函数 , munmap 释放的大小为 base_size_new_base_size_ 的差值。




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




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



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

一个神奇的小工具,让URL地址都变成了"ooooooooo"

web
发现一个很有创意的小工具网站,如封面图所示功能很简单,就是将一个URL地址转换为都是 ooooooooo 的样子,通过转换后的地址访问可以转换回到原始地址,简单流程如下图所示。转换的逻辑有点像短链平台一样,只不过这个是将你的URL地址变的很长长长长,但是看着都...
继续阅读 »

发现一个很有创意的小工具网站,如封面图所示功能很简单,就是将一个URL地址转换为都是 ooooooooo 的样子,通过转换后的地址访问可以转换回到原始地址,简单流程如下图所示。转换的逻辑有点像短链平台一样,只不过这个是将你的URL地址变的很长长长长,但是看着都是 ooooooooo,很好奇是如何实现的,所以查阅了源码,本文解读其核心实现逻辑,很有趣且巧妙的实现了这个功能。



前置知识点


在正式开始前,先了解一些需要学习的知识点。因为涉及到两个地址其实也就是字符串之间的转换,会用到一些编码和解码的能力。


将字符转为utf8数组,转换后的每个字符都有一个特定的唯一数值,比如 http 转换后的 utf8 格式数组即是 [104, 116, 116, 112]


    toUTF8Array(str) {
var utf8 = [];
for (var i = 0; i < str.length; i++) {
var charcode = str.charCodeAt(i);
if (charcode < 0x80) utf8.push(charcode);
else if (charcode < 0x800) {
utf8.push(0xc0 | (charcode >> 6),
0x80 | (charcode & 0x3f));
}
else if (charcode < 0xd800 || charcode >= 0xe000) {
utf8.push(0xe0 | (charcode >> 12),
0x80 | ((charcode >> 6) & 0x3f),
0x80 | (charcode & 0x3f));
}
else {
i++;
charcode = ((charcode & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff)
utf8.push(0xf0 | (charcode >> 18),
0x80 | ((charcode >> 12) & 0x3f),
0x80 | ((charcode >> 6) & 0x3f),
0x80 | (charcode & 0x3f));
}
}
console.log(utf8, 'utf8');
return utf8;
}

上面是编码,对应下面的则是解码,将utf8数组转换为字符串,比如 [99, 111, 109] 转换后的 utf8 格式数组即是 com


    Utf8ArrayToStr(array) {
var out, i, len, c;
var char2, char3;

out = "";
len = array.length;
i = 0;
while (i < len) {
c = array[i++];
switch (c >> 4) {
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
// 0xxxxxxx
out += String.fromCharCode(c);
break;
case 12: case 13:
// 110x xxxx 10xx xxxx
char2 = array[i++];
out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
break;
case 14:
// 1110 xxxx 10xx xxxx 10xx xxxx
char2 = array[i++];
char3 = array[i++];
out += String.fromCharCode(((c & 0x0F) << 12) |
((char2 & 0x3F) << 6) |
((char3 & 0x3F) << 0));
break;
}
}

return out;
}

将 Number 对象以 4 进制的形式表示为字符串,toString 用的比较多,但是里面传入参数的场景比较少,这个参数 radix 是一个可选的参数,用于指定转换的进制数,范围为 2 ~ 36,如果未传入该参数,则默认使用 10 进制。


n.toString(4)

在字符串左侧填充指定字符,直到字符串达到指定长度。基本语法为 str.padStart(targetLength [, padString])



  • targetLength:必需,指定期望字符串的最小长度,如果当前字符串小于这个长度,则会在左侧使用 padString 进行填充,直到字符串达到指定长度。

  • padString:可选,指定用于填充字符串的字符,默认为 " "(空格)。


str.padStart(4, '0')

URL 编码/解码


下面正式开始URL编码的逻辑,核心的逻辑如下:



  • 转换为utf8数组

  • 转换为4进制并左侧补0到4位数

  • 分割转换为字符串数组

  • 映射到o的不同形式

  • 再次拼接为字符串,即转换完成后的URL


// 获取utf8数组
let unversioned = this.toUTF8Array(url)
// 转换为base 4字符串
// padstart非常重要!否则会丢失前导0
.map(n => n.toString(4).padStart(4, "0"))
// 转换为字符数组
.join("").split("")
// 映射到o的不同形式
.map(x => this.enc[parseInt(x)])
// 连接成单个字符串
.join("")

上面有两个关键点解释一下,首先映射到o的不同形式这个是什么意思呢?其实转换后的o并不是一种“o”,而是4种,只不过我们肉眼看到的效果很像,通过 encodeURI 转换后的字符可以看出来。


encodeURI('o-ο-о-ᴏ')
// o-%CE%BF-%D0%BE-%E1%B4%8F

这里其实也解释了为什么上面为什么是转换为4进制和左侧补0到四位数。因为上面代码定义的 this.enc 如下,因为总共只有四种“o”,4进制只会产生0,1,2,3,这样就可以将转换后的utf8字符一一对应上这几种特殊的“o”。


enc = ["o", "ο", "о", "ᴏ"] 

最后的效果举例转换 http 这个字符:



  • 转换为utf8数组:[ 104, 116, 116, 112 ]

  • 转换为4进制并左侧补0到4位数:['1220', '1310', '1310', '1300']

  • 分割转换为字符串数组:['1', '2', '2', '0', '1', '3', '1', '0', '1', '3', '1', '0', '1', '3', '0', '0']

  • 映射到o的不同形式:[ 'ο', 'о', 'о', 'o', 'ο', 'ᴏ', 'ο', 'o', 'ο', 'ᴏ', 'ο', 'o', 'ο', 'ᴏ', 'o', 'o' ]

  • 再次拼接为字符串,即转换完成后的URL:οооoοᴏοoοᴏοoοᴏoo


到此整个转换编码的过程就结束了,看完后是不是觉得设计的很不错,编码完后就是解码,解码就是将上面的过程倒序来一遍,恢复到最原始的URL地址。这里要注意一点的是每次解析4个字符且parseInt以4进制的方式进行解析。


// 获取url的base 4字符串表示
let b4str = ooo.split("").map(x => this.dec[x]).join("")

let utf8arr = []
// 每次解析4个字符
// 记住添加前导0的填充
for (let i = 0; i < b4str.length; i += 4)
utf8arr.push(parseInt(b4str.substring(i, i + 4), 4))
// 返回解码后的字符串
return this.Utf8ArrayToStr(utf8arr)

最后


到此就核心实现代码就分享结束了,看完是不是感觉并没有很复杂,基于此设计或许可以延伸出其他的字符效果,有兴趣的也可以试试看。将转码后的地址分享给你的朋友们一定会带来不一样的惊喜。


以下将官网源码运行在码上掘金,方便大家体验。



下面是我转换的一个AI小工具地址,点击看看效果吧~


ooooooooooooooooooooooo.ooo/ooooοооoοᴏο…


看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~


专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)


作者:南城FE
来源:juejin.cn/post/7225573912670191677
收起阅读 »

十分钟,带你了解 Vue3 的新写法

web
最近因为项目需要,不得不学习一下 Vue3。于是花了 4 个小时,把 Vue3 过了一遍。现在我来带你快速了解 Vue3 的写法。 本文的目的,是为了让已经有 Vue2 开发经验的 人 ,快速掌握 Vue3 的写法。 因此, 本篇假定你已经掌握 Vue 的核心...
继续阅读 »

最近因为项目需要,不得不学习一下 Vue3。于是花了 4 个小时,把 Vue3 过了一遍。现在我来带你快速了解 Vue3 的写法。


本文的目的,是为了让已经有 Vue2 开发经验的 ,快速掌握 Vue3 的写法。


因此, 本篇假定你已经掌握 Vue 的核心内容 ,只为你介绍编写 Vue3 代码,需要了解的内容。


一、Vue3 里 script 的三种写法


首先,Vue3 新增了一个叫做组合式 api 的东西,英文名叫 Composition API。因此 Vue3 的 script 现在支持三种写法,


1、最基本的 Vue2 写法


<template>
<div>{{ count }}</div>
<button @click="onClick">
增加 1
</button>
</template>

<script>
export default {
data() {
return {
count: 1,
};
},
methods: {
onClick() {
this.count += 1;
},
},
}
</script>

2、setup() 属性


<template>
<div>{{ count }}</div>
<button @click="onClick">
增加 1
</button>
</template>

<script>
import { ref } from 'vue';
export default {

// 注意这部分
setup() {
let count = ref(1);
const onClick = () => {
count.value += 1;
};
return {
count,
onClick,
};
},

}
</script>

3、<script setup>


<template>
<div>{{ count }}</div>
<button @click="onClick">
增加 1
</button>
</template>

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

const count = ref(1);
const onClick = () => {
count.value += 1;
};
</script>

正如你看到的那样,无论是代码行数,还是代码的精简度,<script setup> 的方式是最简单的形式。


如果你对 Vue 很熟悉,那么,我推荐你使用 <script setup> 的方式。


这种写法,让 Vue3 成了我最喜欢的前端框架。


如果你还是前端新人,那么,我推荐你先学习第一种写法。


因为第一种写法的学习负担更小,先学第一种方式,掌握最基本的 Vue 用法,然后再根据我这篇文章,快速掌握 Vue3 里最需要关心的内容。


第一种写法,跟过去 Vue2 的写法是一样的,所以我们不过多介绍。


第二种写法,所有的对象和方法都需要 return 才能使用,太啰嗦。除了旧项目,可以用这种方式体验 Vue3 的新特性以外,我个人不建议了解这种方式。反正我自己暂时不打算精进这部分。


所以,接下来,我们主要介绍的,也就是 <script setup> ,这种写法里需要了解的内容。


注意: <script setup> 本质上是第二种写法的语法糖,掌握了这种写法,其实第二种写法也基本上就会了。(又多了一个不学第二种写法的理由)。


二、如何使用 <script setup> 编写组件


学习 Vue3 并不代表你需要新学习一个技术,Vue3 的底层开发思想,跟 Vue2 是没有差别的。


V3 和 V2 的区别就像是,你用不同的语言或者方言说同一句话。


所以我们需要关心的,就是 Vue2 里的内容,怎么用 Vue3 的方式写出来。


1、data——唯一需要注意的地方


整个 data 这一部分的内容,你只需要记住下面这一点。


以前在 data 中创建的属性,现在全都用 ref() 声明。


template 中直接用,在 script 中记得加 .value


在开头,我就已经写了一个简单的例子,我们直接拿过来做对比。


1)写法对比


 // Vue2 的写法

<template>
<div>{{ count }}</div>
<button @click="onClick">
增加 1
</button>
</template>

<script>
export default {
data() {
return {
count: 1,
};
},
methods: {
onClick() {
this.count += 1;
},
},
}
</script>

 // Vue3 的写法

<template>
<div>{{ count }}</div>
<button @click="onClick">
增加 1
</button>
</template>

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

// 用这种方式声明
const count = ref(1);

const onClick = () => {
// 使用的时候记得 .value
count.value += 1;
};
</script>

2)注意事项——组合式 api 的心智负担


a、ref 和 reactive

Vue3 里,还提供了一个叫做 reactiveapi


但是我的建议是,你不需要关心它。绝大多数场景下,ref 都够用了。


b、什么时候用 ref() 包裹,什么时候不用。

要不要用ref,就看你的这个变量的值改变了以后,页面要不要跟着变。


当然,你可以完全不需要关心这一点,跟过去写 data 一样就行。


只不过这样做,你在使用的时候,需要一直 .value


c、不要解构使用

在使用时,不要像下面这样去写,会丢失响应性。


也就是会出现更新了值,但是页面没有更新的情况


// Vue3 的写法
<template>
<div>{{ count }}</div>
<button @click="onClick">
增加 1
</button>
</template>

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

const count = ref(1);
const onClick = () => {
// 不要这样写!!
const { value } = count;
value += 1;
};
</script>

注意: 学习 Vue3 就需要考虑像这样的内容,徒增了学习成本。实际上这些心智负担,在学习的过程中,是可以完全不需要考虑的。


这也是为什么我推荐新人先学习 Vue2 的写法。


2、methods


声明事件方法,我们只需要在 script 标签里,创建一个方法对象即可。


剩下的在 Vue2 里是怎么写的,Vue3 是同样的写法。


// Vue2 的写法
<template>
<div @click="onClick">
这是一个div
</div>
</template>

<script>
export default {
methods: {
onClick() {
console.log('clicked')
},
},
}
</script>

// Vue3 的写法
<template>
<div @click="onClick">
这是一个div
</div>
</template>

<script setup>

// 注意这部分
const onClick = () => {
console.log('clicked')
}

</script>

3、props


声明 props 我们可以用 defineProps(),具体写法,我们看代码。


1)写法对比


// Vue2 的写法
<template>
<div>{{ foo }}</div>
</template>

<script>
export default {
props: {
foo: String,
},
created() {
console.log(this.foo);
},
}
</script>

// Vue3 的写法
<template>
<div>{{ foo }}</div>
</template>

<script setup>

// 注意这里
const props = defineProps({
foo: String
})

// 在 script 标签里使用
console.log(props.foo)
</script>

2)注意事项——组合式 api 的心智负担


使用 props 时,同样注意不要使用解构的方式。


<script setup>
const props = defineProps({
foo: String
})

// 不要这样写
const { foo } = props;
console.log(foo)
</script>

4、emits 事件


props 相同,声明 emits 我们可以用 defineEmits(),具体写法,我们看代码。


// Vue2 的写法
<template>
<div @click="onClick">
这是一个div
</div>
</template>

<script>
export default {

emits: ['click'], // 注意这里
methods: {
onClick() {
this.$emit('click'); // 注意这里
},
},

}
</script>

// Vue3 的写法
<template>
<div @click="onClick">
这是一个div
</div>
</template>

<script setup>

// 注意这里
const emit = defineEmits(['click']);

const onClick = () => {
emit('click') // 注意这里
}

</script>

5、computed


直接上写法对比。


// Vue2 的写法
<template>
<div>
<span>{{ value }}</span>
<span>{{ reversedValue }}</span>
</div>
</template>

<script>
export default {
data() {
return {
value: 'this is a value',
};
},
computed: {
reversedValue() {
return value
.split('').reverse().join('');
},
},
}
</script>

// Vue3 的写法
<template>
<div>
<span>{{ value }}</span>
<span>{{ reversedValue }}</span>
</div>
</template>

<script setup>
import {ref, computed} from 'vue'
const value = ref('this is a value')

// 注意这里
const reversedValue = computed(() => {
// 使用 ref 需要 .value
return value.value
.split('').reverse().join('');
})

</script>

6、watch


这一部分,我们需要注意一下了,Vue3 中,watch 有两种写法。一种是直接使用 watch,还有一种是使用 watchEffect


两种写法的区别是:




  • watch 需要你明确指定依赖的变量,才能做到监听效果。




  • watchEffect 会根据你使用的变量,自动的实现监听效果。




1)直接使用 watch


// Vue2 的写法
<template>
<div>{{ count }}</div>
<div>{{ anotherCount }}</div>
<button @click="onClick">
增加 1
</button>
</template>

<script>
export default {
data() {
return {
count: 1,
anotherCount: 0,
};
},
methods: {
onClick() {
this.count += 1;
},
},
watch: {
count(newValue) {
this.anotherCount = newValue - 1;
},
},
}
</script>

// Vue3 的写法
<template>
<div>{{ count }}</div>
<div>{{ anotherCount }}</div>
<button @click="onClick">
增加 1
</button>
</template>

<script setup>
import { ref, watch } from 'vue';

const count = ref(1);
const onClick = () => {
count.value += 1;
};

const anotherCount = ref(0);

// 注意这里
// 需要在这里,
// 明确指定依赖的是 count 这个变量
watch(count, (newValue) => {
anotherCount.value = newValue - 1;
})

</script>

2)使用 watchEffect


// Vue2 的写法
<template>
<div>{{ count }}</div>
<div>{{ anotherCount }}</div>
<button @click="onClick">
增加 1
</button>
</template>

<script>
export default {
data() {
return {
count: 1,
anotherCount: 0,
};
},
methods: {
onClick() {
this.count += 1;
},
},
watch: {
count(newValue) {
this.anotherCount = newValue - 1;
},
},
}
</script>

// Vue3 的写法
<template>
<div>{{ count }}</div>
<div>{{ anotherCount }}</div>
<button @click="onClick">
增加 1
</button>
</template>

<script setup>
import { ref, watchEffect } from 'vue';

const count = ref(1);
const onClick = () => {
count.value += 1;
};

const anotherCount = ref(0);

// 注意这里
watchEffect(() => {
// 会自动根据 count.value 的变化,
// 触发下面的操作
anotherCount.value = count.value - 1;
})

</script>

7、生命周期


Vue3 里,除了将两个 destroy 相关的钩子,改成了 unmount,剩下的需要注意的,就是在 <script setup> 中,不能使用 beforeCreatecreated 两个钩子。


如果你熟悉相关的生命周期,只需要记得在 setup 里,用 on 开头,加上大写首字母就行。


// 选项式 api 写法
<template>
<div></div>
</template>

<script>
export default {
beforeCreate() {},
created() {},

beforeMount() {},
mounted() {},

beforeUpdate() {},
updated() {},

// Vue2 里叫 beforeDestroy
beforeUnmount() {},
// Vue2 里叫 destroyed
unmounted() {},

// 其他钩子不常用,所以不列了。
}
</script>

// 组合式 api 写法
<template>
<div></div>
</template>


<script setup>
import {
onBeforeMount,
onMounted,

onBeforeUpdate,
onUpdated,

onBeforeUnmount,
onUnmounted,
} from 'vue'

onBeforeMount(() => {})
onMounted(() => {})

onBeforeUpdate(() => {})
onUpdated(() => {})

onBeforeUnmount(() => {})
onUnmounted(() => {})
</script>

三、结语


好了,对于快速上手 Vue3 来说,以上内容基本已经足够了。


这篇文章本身不能做到帮你理解所有 Vue3 的内容,但是能帮你快速掌握 Vue3 的写法。


如果想做到对 Vue3 的整个内容心里有数,还需要你自己多看看 V

作者:Wetoria
来源:juejin.cn/post/7225267685763907621
ue3 的官方文档。

收起阅读 »

辞职卖烤肠

前景 为35后做准备 找到一个合适的位置 周末出门多转转,找个人多的地方、并且容易饿的地方 营销策略 制定一个LOGO,视频中up的摊子叫公路商店 准备个小黑板 带个好看的围裙 头套啥的也备上 装备 必要 载具(小电车、自行车) 制作台 (可以展开...
继续阅读 »

前景


为35后做准备


找到一个合适的位置


周末出门多转转,找个人多的地方、并且容易饿的地方


营销策略



  • 制定一个LOGO,视频中up的摊子叫公路商店


image.png



  • 准备个小黑板

  • 带个好看的围裙

  • 头套啥的也备上


装备


必要



  • 载具(小电车、自行车)

  • 制作台 (可以展开的箱子、桌子)

  • 厨具(卡式炉、各式烤盘、酱料瓶子)

  • 收钱码(零钱)


可选



  • 投币机小玩具


卖什么


烤肠



  • 淀粉肠

  • 沙拉酱

  • 番茄酱

  • 黑胡椒酱

  • 孜然

  • 辣椒面

  • 油壶

  • 小刷子

  • 食品袋子
    image.png


润奶宝


甜筒、奶油、棉花糖、小熊饼干的组合


image.png


章鱼小丸子



  • 原材料 (面粉、鸡蛋、奶油)

  • 配菜(洋葱、芝士)

  • 定价 (12元/4个、15元/6个)


周末机车露营节集市,章鱼小丸子依旧是游客的宠爱,现场歌舞助兴_哔哩哔哩_bilibili


image.png


手工冰淇淋



  • 保温桶

  • 学习制作

  • 脆皮桶

  • 定价(3元/个)

  • 要用动物奶油


接完女儿放学,再一起骑着二八大杠去摆摊赚钱,多少人儿时的回忆_哔哩哔哩_bilibili


image.png


刨冰



  • 刨冰机


30℃的天气,我用樱桃小丸子刨冰机贩卖快乐_哔哩哔哩_bilibili


image.png


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

今天,我终于写了我大学时候想写的代码了

前言 最近心情不是很好,然后就开始回忆以前大学的时光,好想和大学的朋友再吃一顿饭啊。 然后想到一个月黑风高的晚上,我的宿友和我讨论多线程的问题: “假如有一个非常大的文件,要入库,你怎么将这个文件读取入库?” 当时我说:“很简单啊,就这样读文件然后入库啊。” ...
继续阅读 »

前言


最近心情不是很好,然后就开始回忆以前大学的时光,好想和大学的朋友再吃一顿饭啊。


然后想到一个月黑风高的晚上,我的宿友和我讨论多线程的问题:


“假如有一个非常大的文件,要入库,你怎么将这个文件读取入库?”


当时我说:“很简单啊,就这样读文件然后入库啊。”


他说:“我当时也是这样说的,然后面试官叫我回去等通知。”


想想还是太菜了,然后他和我说可以用多线程去操作。我当时由于技术原因不知道怎么操作,只知道大概思路。而今天也是有空去实现当时的遗憾了。


思路


思路也挺简单的,就是多条线程共同操作一个变量,这个变量记录了这个大文件读取的进度,也可以理解为文件行数。各个线程读取一定的数据然后记录mark,分段入库。


image.png


代码


餐前甜品


此处模拟从数据库读取大量数据原理和分批插入相同,每条线程循环500次,偏移量为500条,使用AtomicInteger来进行线程变量共享。


public class myThread implements Runnable {
private volatile int num = 0;
private AtomicInteger val = new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < 500; i++) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("select id,name from user limit ");
stringBuffer.append(val.get());
val.addAndGet(500);
stringBuffer.append(",").append(val.get());
System.out.println(stringBuffer);
}
}
}

    public static void main(String[] args) {
myThread myRunnable1 = new myThread();
Thread thread1= new Thread(myRunnable1);
Thread thread2 = new Thread(myRunnable1);
thread1.start();
thread2.start();
}

每条线程是500x500,也就是250000,此处开了2条线程,最终结果应该是500000。控制台输出如下


image.png


ok,不错,证明方向是正确的,接下来是模拟读取大文件入库。

正餐


线程类


@Component
public class myNewThread extends Thread {
@Autowired
private IUserService userService;

private static AtomicInteger val = new AtomicInteger();

public myNewThread(){
}
public void run() {
List<User> list = new ArrayList<>();
for (int i = 0; i < 500; i++) {
User user = User.builder().wxName(val.get() + "").build();
SoftReference<User> stringBufferSoftReference = new SoftReference<>(user);
list.add(stringBufferSoftReference.get());
val.incrementAndGet();
}
userService.saveBatch(list);
}

}

@Test
void contextLoads() throws InterruptedException {
Thread thread3 = new myNewThread();
Thread thread4 = new myNewThread();
thread3.start();
thread4.start();
}

每条线程插入400条数据,wxName这个字段记录第几条数据
启动后发现控制台什么都没输出,数据库也没插入数据,很纳闷。然后经过一番思考后找出来问题所在。
在线程启动后加入以下两行代码


thread3.join();
thread4.join();

原因是,主线程在创建这两个线程后就结束了,子线程还没来得及操作数据库主线程就已经死亡了导致子线程被迫停止。


join表示主等待这线程执行结束,这样子就不会出现主线程创建完子线程就死亡导致子线程都没来得及执行线程体就死了的情况。


ok!启动!!
坏啦,空指针异常


image.png
经过调试发现,是service类在线程里面为空,导致能空指针异常,看来是spring捕获不到线程体。


既然捕获不到那我就传一个给线程体吧。


修改后的线程体


@Component
public class myNewThread extends Thread {
private IUserService userService;

private static AtomicInteger val = new AtomicInteger();
public myNewThread(IUserService userService){
this.userService = userService;
}
public myNewThread(){
}
public void run() {
List<User> list = new ArrayList<>();
for (int i = 0; i < 400; i++) {
User user = User.builder().wxName(val.get() + "").build();
SoftReference<User> stringBufferSoftReference = new SoftReference<>(user);
list.add(stringBufferSoftReference.get());
val.incrementAndGet();
}
userService.saveBatch(list);
}

}

修改后的单元测试


@Autowired
private IUserService userService;
@Test
void contextLoads() throws InterruptedException {
Thread thread3 = new myNewThread(userService);
//Thread thread4 = new myNewThread();
Thread thread4 = new myNewThread(userService);
thread3.start();
thread4.start();
thread3.join();
thread4.join();
}

简单来说就是使用构造函数给线程体传递一个非空的service类。


启动单元测试


image.png


证明是可以的刚刚好800条数据


总结


启动线程时要注意主线程和子线程的关系,然后操作数据库时,要注意传入的类是否空指针。


分段插入就到这里了,在高并发的情况下还没测试过,明天再说吧,已经是下午5点58了,下班!!!


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

五一在即:有哪些办法能阻止票贩子和我们一起抢票?

背景 五一就要来了,本来准备去淄博吃烧烤,结果发现根本抢不到机票。了解了一下原因,原来是黄牛的恶意爬虫把票都抢完了,现在买票,只能通过黄牛的渠道,多花几倍的钱来买了。 有鉴于此,想试试能不能针对这种爬虫防止相关问题,当然是站在航空公司的角度,就个人而言,躺平任...
继续阅读 »

背景


五一就要来了,本来准备去淄博吃烧烤,结果发现根本抢不到机票。了解了一下原因,原来是黄牛的恶意爬虫把票都抢完了,现在买票,只能通过黄牛的渠道,多花几倍的钱来买了。


有鉴于此,想试试能不能针对这种爬虫防止相关问题,当然是站在航空公司的角度,就个人而言,躺平任嘲就得了。


恶意爬虫的相关特征


对于恶意爬虫的特征,航空公司自己有总结:


1、访问的目标网页比较集中:“爬虫”代理人目标明确,主要是爬取班次、价格、数量等核心信息,因此只浏览访问几个固定页面,不访问其他页面。


**2、查询订票等行为很有规律:**由于“爬虫”是程序化操作,按照预先设定的流程进行访问等,因此呈现出毫无思维、但很有规律、有节奏且持续的行为。


3、同一设备上有规模化的访问和操作:“爬虫”的目的是最短时间内抓取最多信息,因此同一设备会有大量离散的行为,包括访问、浏览、查询等。


**4、访问来源IP地址异常:**正常情况下用户在查询、购买时,用户的IP地址比较稳定,而且访问来源IP比较;“爬虫”、“虚假占座”等操作时,IP来源地址呈现不同维度上的聚集,而且浏览、查询、购票等操作时不停变化IP地址。


5、设置UA模拟浏览器和频繁使用代理IP:很多“爬虫”程序伪装成浏览器进行访问,比如在程序头或者UA中默认含有类似python-requests/2.18.4等固定字符串;并且通过购买或者租用的云服务、改造路由器、租用IP代理、频繁变更代理IP等进行访问。


6、操作多集中非业务时间段:“爬虫”程序运行时间多集中在无人值守阶段。此时系统监控会放松,而且平台的带宽等资源占用少,爬虫密集的批量爬取不会对带宽、接口造成影响。以下是顶象反欺诈中心监测到,凌晨1-5点是恶意“爬虫”的运行高峰时段。


浅谈防范措施


1.设备指纹的应用


设备指纹单独用的话,在恶意爬虫面前,会稍微较弱一些。因为攻击者会使用其他技术手段来绕过设备指纹的检测,例如使用虚拟机、代理服务器等来隐藏真实设备的指纹。所以可以考虑结合IP地址限制一起使用:


import fingerprintjs2
from flask import Flask, request, abort

app = Flask(__name__)

# 创建一个新的Fingerprint2实例
fp = fingerprintjs2.Fingerprint2()

# 白名单IP地址
IP_WHITELIST = ['127.0.0.1', '192.168.0.1']

@app.route('/book-flight')
def book_flight():
# 获取设备指纹和IP地址
device_fingerprint = fp.get()
ip_address = request.remote_addr

# 检查IP地址是否在白名单中
if ip_address not in IP_WHITELIST:
abort(403)

# 其他业务逻辑代码...
return "Flight booked successfully!"

if __name__ == '__main__':
app.run()


2. 人机验证


人机验证肯定是一个阻挡办法,但是在这种恶意强攻击的情况下,我们可以试试语音验证码(但是可能对用户体验不大友好):


from flask import Flask, request, jsonify
import random
from io import BytesIO
from captcha.audio import AudioCaptcha

app = Flask(__name__)

# 生成随机字符串作为验证码
def random_string(length):
pool = 'abcdefghijklmnopqrstuvwxyz0123456789'
return ''.join(random.choice(pool) for i in range(length))

# 生成语音验证码
def generate_audio_captcha():
captcha = random_string(4)
audio = AudioCaptcha().generate(captcha)
return captcha, audio

# 创建一个全局变量,用于保存已生成的语音验证码
captcha_cache = {}

@app.route('/audio-captcha')
def audio_captcha():
# 生成新的语音验证码
captcha, audio = generate_audio_captcha()

# 将验证码保存到缓存中,以便后续验证
captcha_cache[captcha] = True

# 返回语音验证码
response = BytesIO(audio.read())
response.headers['Content-Type'] = 'audio/wav'
return response

@app.route('/book-flight', methods=['POST'])
def book_flight():
# 获取表单中的验证码
captcha = request.form['captcha']

# 检查验证码是否正确
if captcha not in captcha_cache:
return 'Invalid captcha!'

# 其他业务逻辑代码...
return "Flight booked successfully!"

if __name__ == '__main__':
app.run()


因为对用户体验不大友好,研发同学在设计的时候,最好考虑一下它的易用性和可访问性问题。需要权衡用户体验和恶意爬虫。


结语


“天下熙熙皆为利来,天下攘攘皆为利往。”因为这一块存在着巨大的利益,也就一直吸引着无数黑客从中牟利,而我们能做的,是“魔高一尺道高一丈”,希望能为安全世界尽一份力。


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

从现在开始,对你的Flutter代码进行单元测试和微件测试

必要性 作为一个开发,对自己开发的功能进行单元测试是非常有必要的。单元测试是软件开发中一种必要的测试方法。它旨在测试一个单独的模块或组件的功能。单元测试通常是自动化的,并且可以在开发过程中进行频繁的测试。在本文中,我们将探讨单元测试的必要性以及为什么它是软件开...
继续阅读 »

必要性


作为一个开发,对自己开发的功能进行单元测试是非常有必要的。单元测试是软件开发中一种必要的测试方法。它旨在测试一个单独的模块或组件的功能。单元测试通常是自动化的,并且可以在开发过程中进行频繁的测试。在本文中,我们将探讨单元测试的必要性以及为什么它是软件开发中不可或缺的一部分。



  1. 验证代码的正确性


在编写代码时,开发人员往往会犯错误。这些错误可能是语法错误,逻辑错误或其他类型的错误。单元测试可以帮助开发人员及时发现这些错误。通过编写单元测试,开发人员可以验证代码是否按照预期执行,并且可以及早发现和解决错误。



  1. 提高代码质量


单元测试可以帮助开发人员编写更高质量的代码。编写单元测试需要开发人员仔细考虑每个功能点,并确保代码的每个方面都被测试到。通过这个过程,开发人员可以发现并解决潜在的问题,并确保代码的质量得到提高。



  1. 支持重构和改进


在软件开发的生命周期中,代码经常需要进行重构和改进。单元测试可以帮助开发人员在进行这些更改时保证代码的正确性。如果重构或改进代码后,单元测试仍然能够通过,那么开发人员就可以确信代码的行为没有发生变化。这种自信可以让开发人员更加轻松地进行代码更改,并减少由于更改而引入错误的风险。



  1. 提高代码的可维护性


单元测试可以提高代码的可维护性。通过编写单元测试,开发人员可以快速定位代码中的问题并进行修复。这可以使代码更容易维护,并减少开发人员需要花费的时间和精力。在团队合作的情况下,单元测试还可以帮助新成员更快地理解代码,并快速定位和解决问题。


总之,单元测试是软件开发中必不可少的一部分。它可以帮助开发人员验证代码的正确性,提高代码质量,支持重构和改进,并提高代码的可维护性。通过编写单元测试,开发人员可以确保代码的正确性和稳定性,并减少由于更改而引入错误的风险。


单元测试



  1. 安装测试框架


Flutter提供了自己的测试框架,称为flutter_test。在项目中使用flutter_test,需要在pubspec.yaml文件中添加依赖项:


dev_dependencies:
flutter_test:
sdk: flutter

然后,运行以下命令安装依赖项:


flutter packages get

当然在新创建的Flutter工程里,都会默认引用flutter_test并且创建好了test文件夹


image.png



  1. 编写测试用例


测试用例是用来测试应用程序的各个部分的代码。在Flutter中,测试用例通常包含在一个单独的文件中。在这个文件中,你需要导入flutter_test库,并编写测试代码。以下是一个示例:


void main() {
String time = testDate();
print('time=' + time);
}
///检查到期时间
String testDate() {
String expiryDate = '-长期';
List<String> list = (expiryDate).split('-');
if (list.length == 2) {
var start = list[0];
var end = list[1];
if (start.isEmpty || end.isEmpty) {
return '';
}
if (start.length == 8) {
start = DateTime.parse(start).format('yyyy.MM.dd');
}
if (end.length == 8) {
end = DateTime.parse(end).format('yyyy.MM.dd');
}
return '$start-$end';
}
return (expiryDate);
}

在单元测试中,我们可以给对应文件创建对应的测试文件。当我们把数据计算解耦出来,就可以达到不需要UI的情况测试返回结果的情况。配合Mock数据能极大的提高效率。



  1. 进行微件测试
    flutter_test还可以进行微件测试。
    比如我写了一个Widget,这个Widget需要一个json来构建,而json中的一个字段会影响我Widget的创建.我们需要检查这个Widget是否兼容这个json的所有情况.


void questionTitleWidgetTest() {
testWidgets('questionTitle', (widgetTester) async {
String path = '/Users/kfzs_002/Desktop/122.json';
File file = File(path);
String str = file.readAsStringSync();
Map<String, dynamic> json = jsonDecode(str);
Temp temp = Temp.fromJson(json);
for (int i = 0; i < (temp.data?.list?.length ?? 0); i++) {
var data = temp.data!.list![i];
if ((data.questionTitleArr ?? []).isEmpty) {
print('没有标题');
continue;
}
await widgetTester.pumpWidget(
MaterialApp(
home: Material(
child: QuestionTitleWidget(data: data.questionTitleArr ?? []),
),
),
);
print('index=$i');
await widgetTester.pump();
}
});
}

执行测试方法,我们就能在run窗口看到对应的数据结果。


有时我们会写一个动画组件,它有复杂的动画我想检查动画相关参数是否正确。


void examCircleProgressTest() {
testWidgets('examCircleProgressTest', (widgetTester) async {
await widgetTester.pumpWidget(ScreenUtilInit(
designSize: const Size(375, 812),
minTextAdapt: true,
useInheritedMediaQuery: true,
builder: (context, child) => GetMaterialApp(
home: Material(
child: Row(
children: [
ExamCircleProgress(
mTitle: 'title',
subjectType: 2,
progress: 30,
subTitle: 'subtitle',
score: '30',
)
],
),
),
),
));
// await widgetTester.pump();
for (var i = 0; i < 2000; i += 33) {
await widgetTester.pump(Duration(milliseconds: i));
}
});
}

这里我用到了widgetTester.pump.在Widget测试中,widgetTester.pump()方法是一个非常重要的方法,它的作用是将应用程序的状态推进到下一个时间片段。我以33毫秒为间隔,把这个Widget推进到了2秒后的状态。这样就能看到对应的数据情况了。


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

Flutter 手指拖动实现弹簧动画交互

物理模拟可以让应用程序的交互感觉逼真和互动,例如,你可能希望为一个 Widget 设置动画,使其看起来像是附着在弹簧上或是重力下落。本文章实现了演示了如何使用弹簧模拟将小部件从拖动的点移回中心。实现步骤如下 设置动画控制器 使用手势移动小部件 为小部件制作动...
继续阅读 »

物理模拟可以让应用程序的交互感觉逼真和互动,例如,你可能希望为一个 Widget 设置动画,使其看起来像是附着在弹簧上或是重力下落。本文章实现了演示了如何使用弹簧模拟将小部件从拖动的点移回中心。
实现步骤如下



  1. 设置动画控制器

  2. 使用手势移动小部件

  3. 为小部件制作动画

  4. 计算速度以模拟弹簧运动




1 创建一个动画控制器


首页创建一个测试使用的Demo页面


void main() {
runApp(const MaterialApp(home: PhysicsCardDragDemo()));
}

class PhysicsCardDragDemo extends StatelessWidget {
const PhysicsCardDragDemo({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: const DraggableCard(
child: FlutterLogo(
size: 128,
),
),
);
}
}

DraggableCard 是自定义的一个 StatefulWidget,代码如下:


class _DraggableCardState extends State<DraggableCard> {
@override
void initState() {
super.initState();
}

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

@override
Widget build(BuildContext context) {
return Align(
child: Card(
child: widget.child,
),
);
}
}

然后在 _DraggableCardState 中创建一个动画控制器,并在页面销毁的时候释放动画控制器,代码如下:


class _DraggableCardState extends State<DraggableCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;

@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(seconds: 1));
}

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

@override
Widget build(BuildContext context) {
return Align(
child: Card(
child: widget.child,
),
);
}
}

SingleTickerProviderStateMixin是用来在StatefulWidget中管理单个AnimationController的Mixin;它提供了一个TickerProvider,用于将AnimationController与TickerProviderStateMixin一起使用。


TickerProviderStateMixin提供了一个Ticker,它可以在每个frame中调用AnimationController的方法,这使得AnimationController可以在每个frame中更新动画。


2 使用手势移动Widget


在 _DraggableCardState 中,结合使用 Alignment 与 GestureDetector,代码如下:


class _DraggableCardState extends State<DraggableCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
Alignment _dragAlignment = const Alignment(0, 0);
@override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
return GestureDetector(
onPanDown: (details) {},
onPanUpdate: (details) {
_dragAlignment += Alignment(
details.delta.dx / (size.width / 2),
details.delta.dy / (size.height / 2),
);
setState(() {
});
},
onPanEnd: (details) {},
child: Align(
alignment: _dragAlignment,
child: Card(
child: widget.child,
),
),
);
}

GestureDetector用来检测手势,例如轻触、滑动、拖动等,可以用来实现各种交互效果。


Alignment用于控制子widget在父widget中的位置。可以通过Alignment的构造函数来指定子widget相对于父widget的位置,如Alignment.topLeft表示子widget位于父widget的左上角。也可以通过FractionalOffset来指定子widget相对于父widget的位置,如FractionalOffset(0.5, 0.5)表示子widget位于父widget的中心。Alignment还可以与Stack一起使用,实现多个子widget的定位。
在这里插入图片描述


3 创建一个动画Widget


我们需要实现,当手指抬起时,被移动的 Widget 动画的方式弹回去。


在这里需要一个 Animation ,再定义一个 runAnimation 方法,同时为 第一步创建的动画控制器添加一个更新监听。


class _DraggableCardState extends State<DraggableCard>
with SingleTickerProviderStateMixin {

late AnimationController _controller;
late Animation<Alignment> _animation;
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: const Duration(seconds: 1));

_controller.addListener(() {
setState(() {
_dragAlignment = _animation.value;
});
});
}

void _runAnimation() {
_animation = _controller.drive(
AlignmentTween(
begin: _dragAlignment,
end: Alignment.center,
),
);
_controller.reset();
_controller.forward();
}
}

然后在手指抬起的时候,执行动画,将被移动的 Widget (如这里的图片)以动画的方式移动回原位:


@override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
return GestureDetector(
onPanDown: (details) {
_controller.stop();
},
onPanUpdate: (details) {
_dragAlignment += Alignment(
details.delta.dx / (size.width / 2),
details.delta.dy / (size.height / 2),
);
setState(() {

});
},
onPanEnd: (details) {
_runAnimation();
},
child: Align(
alignment: _dragAlignment,
child: Card(
child: widget.child,
),
),
);
}

在这里插入图片描述


4 计算速度以模拟弹簧运动


最后一步是做一些数学运算,计算小部件完成拖动后的速度。这是为了使小部件在被拍回之前能够以这种速度逼真地继续。(_runAnimation方法已经通过设置动画的开始和结束对齐来设置方向。)


导入包如下:


import 'package:flutter/physics.dart';

onPanEnd回调提供了一个DragEndDetails对象。此对象提供指针停止接触屏幕时的速度。速度以像素每秒为单位,但Align小部件不使用像素。它使用介于[-1.0,-1.0]和[1.0,1.0]之间的坐标值,其中[0.0,0.0]表示中心。步骤2中计算的大小用于将像素转换为该范围内的坐标值。


然后修改 runAnimation 执行动画函数如下:


void _runAnimation(Offset pixelsPerSecond, Size size) {
_animation = _controller.drive(
AlignmentTween(
begin: _dragAlignment,
end: Alignment.center,
),
);

final unitsPerSecondX = pixelsPerSecond.dx / size.width;
final unitsPerSecondY = pixelsPerSecond.dy / size.height;
final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
final unitVelocity = unitsPerSecond.distance;

//它可以用于模拟弹簧的阻尼、质量和刚度等属性,从而实现更加真实的动画效果。
const spring = SpringDescription(
mass: 30,
stiffness: 1,
damping: 1,
);
//SpringSimulation用来模拟一个弹簧的运动,可以用于创建具有弹性的动画效果。
final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);

_controller.animateWith(simulation);
}

然后在手指抬起的时候调用


onPanEnd: (details) {
_runAnimation(details.velocity.pixelsPerSecond, size);
},

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

Kotlin跨平台第四弹:了解Kotlin/Wasm 前言

前言 前几天,Compose for iOS 发布了Alpha版本,不过早在两个多月前的试验阶段时,我们已经在Compose跨平台第三弹:体验Compose for iOS 为大家分享了Compose开发iOS的体验。 我们也在Compose跨平台第二弹:体验...
继续阅读 »

前言


前几天,Compose for iOS 发布了Alpha版本,不过早在两个多月前的试验阶段时,我们已经在Compose跨平台第三弹:体验Compose for iOS 为大家分享了Compose开发iOS的体验。


我们也在Compose跨平台第二弹:体验Compose for Web 中了解了如何使用Compose开发Web程序,当时也一起见证了Compose for Web 割裂严重的问题。这个问题官方也一直在推进解决,这要得益于Kotlin/Wasm。那么Kotlin/Wasm又是什么呢?


了解Kotlin/Wasm


是什么


Kotlin/Wasm是将Kotlin编译为WebAssembly (Wasm)的工具链。那WebAssembly又是什么呢?


WebAssembly是一种低级字节码格式,可以在Web浏览器中运行,并且具有比JavaScript更快的执行速度和更好的跨平台兼容性。


可以做什么


使用Kotlin/Wasm,我们可以使用Kotlin编写Web应用程序,然后将其编译为Wasm字节码,以在Web浏览器中运行。这样我们就可以在单个代码库中使用相同的语言和工具来开发应用程序,而不必学习JavaScript等其他语言。此外,由于Wasm字节码是一种跨平台格式,因此应用程序可以在各种操作系统和设备上运行,而不必重新编写代码。


简单的说


总之,Kotlin/Wasm是一种新兴的技术,可以让开发人员使用Kotlin编写Web应用程序,并在Web浏览器中运行。这可以使开发更加简单和高效,并提供更好的跨平台兼容性和更快的执行速度。


Kotlin/Wasm 是从 Kotlin 1.8.20版本开始支持的,当前处于实验阶段。


体验Kotlin/Wasm


启用WASM


我们使用最新版本的IntelliJ IDEA,先随便打开一个项目,双击Shift,再弹出的搜索中输入Registry



选中并回车,在弹出的窗口中找到Kotlin.wasm.wizard,勾选此选项。



然后IDEA会提示我们重启,重启后,就启用了wasm。之后,我们就可以通过IDEA创建Kotlin/Wasm项目。


创建Kotlin/Wasm项目


打开IDEA,创建Kotin Multiplatform项目,选择Browser Application with Kotlin/Wasm,如下图所示。



默认情况下,项目将使用带有 Kotlin DSL 的 Gradle 作为构建系统。创建好项目后,在wasmMain目录下为我们创建了Simple.kt文件,如下图所示。



此外,由于Kotlin/Wasm是从1.8.20版本新增的,所以我们要确保配置文件中的版本号是正确的。


plugins {
kotlin("multiplatform") version "1.8.20"
}

运行程序


点击运行程序,运行后在浏览器输入框中输入http://localhost:8080/ ,如下图所示。



这里我使用的是Chrome浏览器,需要在Chrome中输入chrome://flags/#enable-webassembly-garbage-collection,然后启用WebAssembly Garbage Collection,这一点需要注意下。



Hello World程序运行之后,我们可以修改自己想要的展示的文字,比如修改代码如下所示:


fun main() {
document.body?.appendText("Hello, first Kotlin/Wasm Project!")
}

运行程序,如下图所示。



Kotlin/Wasm 可以使用来自 Kotlin 的 JavaScript 代码和来自 JavaScript 的 Kotlin 代码。也就是Kotlin和JavaScript是可以互相操作的。这并不是我们的重点这里就不演示了。


体验Wasm版本的Compose for Web


Compose跨平台第二弹:体验Compose for Web中,我们使用的是“compose-html”,Kotlin/Wasm在Web浏览器中可以实现更高性能和低延迟的计算机程序。


当前依赖于Kotlin/Wasm的“compose-wasm-canvas”已经在实验阶段,而“compose-wasm-canvas”基本可以解决我们之前所体验到的割裂问题。我们来一起体验一下。


项目配置


由于“compose-wasm-canvas”还处于实验结算所以我们在确保版本号、配置可用,修改配置文件代码如下所示:


kotlin.version=1.8.20
org.jetbrains.compose.experimental.jscanvas.enabled=true
compose.version=1.4.0-dev-wasm06

代码编写


@OptIn(ExperimentalComposeUiApi::class)
fun main() {
CanvasBasedWindow {
LoginUi()
}
}

外层使用CanvasBasedWindow包裹,test就是我们自己写的Compose代码,这里我们两个输入框和一个登陆按钮,代码如下所示:


@Composable
fun LoginUI() {

var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }

Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
OutlinedTextField(
value = username,
onValueChange = { username = it },
label = { Text("userName") },
placeholder = { Text("input userName") },
modifier = Modifier.fillMaxWidth()
)

OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("password") },
placeholder = { Text("input password") },
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
)

Button(onClick = {
//login
}) {
Text("Login")
}
}
}

这就是和Android中Compose完全一样的代码,运行程序,结果如下图所示。



总结


“compose-wasm-canvas”与“compose-html”完全不同,并且解决了我们之前所提到的在Compose for Web中严重的割裂问题,不过,不管是“compose-wasm-canvas”还是Kotlin/Wasm都还处于早期的实验性阶段,什么时候发布Aplha甚至是Beta版本,是个未知数,让我们一起期待吧~


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

面试串讲009-布局层级太多怎么优化?

问题: 布局层级太多怎么优化? 回答: View整体布局是通过深度优先的方式来进行组织的,整体形似一颗树,所以优化布局层级主要通过三个方向来实施: 降低布局深度:使用merge标签或者布局层级优化等手段来减少View树的深度; 布局懒加载:使用ViewStu...
继续阅读 »

问题:


布局层级太多怎么优化?


回答:


View整体布局是通过深度优先的方式来进行组织的,整体形似一颗树,所以优化布局层级主要通过三个方向来实施:



  • 降低布局深度:使用merge标签或者布局层级优化等手段来减少View树的深度;

  • 布局懒加载:使用ViewStub,AsyncLayoutInflater等布局加载手段,来确保只有当需要该布局时,该布局才会被创建,优化布局加载速度;

  • 布局重用:通过include等标签重用界面布局,减少GPU重复工作


解析:


<merge/>


<merge/>标签通常用于将其包裹的内容直接添加到父布局以达到降低布局深度的目的,一个普通的layout布局文件及其结构如下图所示:


mianshi009-1


当将该布局文件的根标签修改为<merge/>标签后,得到的布局结构如下图所示:


mianshi009-2


可以看出<merge/>标签内子元素的父布局均变更为顶上的FrameLayout,进而使得布局深度减1.


结合以上例子,我们可以得出 <merge/>标签的主要工作原理是将本应在<merge/>标签节点的Layout与该节点的父布局进行重用,以达到优化布局深度的目的,对<merge/>标签内包含的其他布局结构而言并不能起到优化深度的作用


使用<merge/>标签有以下注意事项:



  • 布局文件中<merge/>标签只能作为根标签;

  • 使用LayoutInflater加载<merge/>标签为根的布局文件时,必须设置attachToRoot为true,以确保重用父布局;

  • <merge/>标签携带的参数没有实际意义

  • <merge/>标签并不是真实存在的View或者ViewGroup,其相当于一种标记,用来表示其所包裹的内容应被添加到其上级布局,真实存在的ViewGroup是引用<merge/>标签布局的上一级布局


<ViewStub/>


<ViewStub/>标签通常用于声明布局中可以被延时加载的部分,在首次布局文件加载时处于占位状态,当调用inflate或者setVisible时才会完成加载动作,一个普通的使用<ViewStub/>布局文件及其结构如下图所示:


mianshi009-3


当执行ViewStub.inflate之后,得到的布局结构如下图所示:


mianshi009-4


可以看出ViewStub区域被其对应的布局结构替换掉了。


结合上述例子,我们可以得出使用<ViewStub/>标签可以管理在页面首次初始化时不需要加载的布局,提升渲染速度,等到需要这部分UI时再进行加载


<include/>


<include/>标签可以将一些公共布局文件在多处重复引用,以便提升布局效率,例如各个页面都有的状态栏,当使用自定义布局实现后,则可以使用<include/>标签进行重复引用。


<include/>标签使用示例代码如下:


 <?xml version="1.0" encoding="utf-8"?>
 <merge xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
     <LinearLayout
         android:layout_width="wrap_content"
         android:layout_height="wrap_content">
 
         <include
             android:id="@+id/view_stub"
             layout="@layout/test"/>
 
         <com.poseidon.looperobserver.customview.CustomView
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_gravity="center"
             android:id="@+id/custom_view"
             android:text="move me!" />
 
     </LinearLayout>
 
 </merge>

使用<include/>标签得到的布局结构如下图所示:


mianshi009-5


可以看出从布局结构来讲并无明显差异,在初次加载就会直接构建在View树上。


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

Android中用ViewModel优雅地管理数据

前言 将应用的界面数据与界面(Activity/Fragment)分离可以让您更好地遵循我们前面讨论的单一责任原则。activity 和 fragment 负责将视图和数据绘制到屏幕上,而 ViewModel 则负责存储并处理界面所需的所有数据。 数据变量从 ...
继续阅读 »

前言


将应用的界面数据与界面(Activity/Fragment)分离可以让您更好地遵循我们前面讨论的单一责任原则。activity 和 fragment 负责将视图和数据绘制到屏幕上,而 ViewModel 则负责存储并处理界面所需的所有数据。


数据变量从 XXFragment 移至 XXViewModel



  1. 将数据变量 scorecurrentWordCountcurrentScrambledWord 移至 XXViewModel 类。


class XXViewModel : ViewModel() {

private var score = 0
private var currentWordCount = 0
private var currentScrambledWord = "test"
...


请注意这些属性仅对 ViewModel 可见,界面无法对其进行访问



想要解决此问题,就不能将这些属性的可见性修饰符设为 public,不应该让数据可被其他类修改。因为外部类可能会以不符合视图模型中指定的游戏规则的预料外方式对数据做出更改。外部类有可能会将 score 更改为其他错误的值。


ViewModel 之内,数据应可修改,数据应设为 privatevar。而在 ViewModel 之外,数据应可读取但无法修改,因此数据应作为 publicval 公开。为了实现此行为,Kotlin 提供了称为后备属性的功能。


后备属性


使用后备属性,可以从 getter 返回确切对象之外的某些其他内容。


Kotlin 框架会为每个属性生成 getter 和 setter。


对于 getter 和 setter 方法,可以替换其中一个方法或同时替换两个方法,并提供自定义行为。为了实现后备属性,需要替换 getter 方法以返回只读版本的数据。后备属性示例:


private var _count = 0

val count: Int
get() = _count

举例而言,在应用中,需要应用数据仅对 ViewModel 可见:


ViewModel 类之内:



  • _count 属性设为 private 且可变。因此,只能在 ViewModel 类中对其访问和修改。惯例是为 private 属性添加下划线前缀。


ViewModel 类之外:



  • Kotlin 中的默认可见性修饰符为 public,因此 count 是公共属性,可从界面控制器等其他类对其进行访问。由于只有 get() 方法会被替换,所以此属性不可变且为只读状态。当外部类访问此属性时,它会返回 _count 的值且其值无法修改。这可以防止外部类擅自对 ViewModel 内的应用数据进行不安全的更改,但允许外部调用方安全地访问该应用数据的值。


将后备属性添加到 currentScrambledWord



  • XXViewModel 中,更改 currentScrambledWord 声明以添加一个后备属性。现在,只能在 XXViewModel 中对 _currentScrambledWord 进行访问和修改。界面控制器 XXFragment 可以使用只读属性 currentScrambledWord 读取其值。


private var _currentScrambledWord = "test"
val currentScrambledWord: String
get() = _currentScrambledWord


  • XXFragment 中,更新 updateNextWordOnScreen() 方法以使用只读的 viewModel 属性 currentScrambledWord


private fun updateNextWordOnScreen() {
binding.textViewUnscrambledWord.text = viewModel.currentScrambledWord
}


  • XXFragment 中,删除 onSubmitWord()onSkipWord() 方法内的代码。稍后您将实现这些方法。现在,您应该能够不出错误地编译代码了。


注意:勿公开 ViewModel 中的可变数据字段,确保无法从其他类修改此数据。ViewModel 内的可变数据应始终设为 private


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

如何按百分比将功能灰度放量

当我们发布新功能时,需要尽可能降低因新功能发布所导致的线上风险,通常会采取灰度放量的方式将新功能逐步发布给用户。在具体实施灰度放量时,我们可以根据业务需求选择相应的放量规则,常见如按白名单放量(如仅 QA 可见)、按特定人群属性放量(如仅某个城市的用户可见)亦...
继续阅读 »

当我们发布新功能时,需要尽可能降低因新功能发布所导致的线上风险,通常会采取灰度放量的方式将新功能逐步发布给用户。在具体实施灰度放量时,我们可以根据业务需求选择相应的放量规则,常见如按白名单放量(如仅 QA 可见)、按特定人群属性放量(如仅某个城市的用户可见)亦或是按用户百分比放量。


当我们选择将功能以用户百分比放量时,如下图所示,会先将功发布给10% 内部用户,此时即便出现问题影响也相对可控,如观察没有问题后逐步扩大需要放量的用户百分比,实现从少量到全量平滑过渡的上线。



那么在 FeatureProbe 上要如何实现百分比放量?


下面将通过一个实际的例子介绍如何通过 FeatureProbe 实现按百分比放量发布一个新功能。


步骤一:创建一个特性开关



接着,配置开关百分比信息。以收藏功能百分比发布为例,设置 10%  的用户可用收藏功能,而另外 90% 的用户无法使用收藏功能。



步骤二:将 SDK 接入应用程序


接下来,将 FeatureProbe SDK 接入应用程序。FeatureProbe 提供完整清晰的接入引导,只需按照步骤即可快速完成 SDK 接入。


1、选择所使用的 SDK



2、按步骤设置应用程序



3、测试应用程序 SDK接入情况



步骤三:按百分比放量发布开关


开关信息配置和 SDK 接入都完成后,点击发布按钮并确认发布。这将会将收藏功能发布给用户,但只有10%的用户可以使用收藏功能。



如果希望逐步扩大灰度范围,可以在开关规则中配置百分比比例。



大部分情况下,我们希望在一个功能的灰度放量过程中,某个特定用户一旦进入了灰度放量组,在灰度比例不减少的情况下,总是进入灰度组。不希望用户因为刷新页面、重新打开APP、请求被分配到另一个服务端实例等原因,一会看到新功能,一会看不到新功能,从而感到迷惑。要达到用户稳定进入灰度组,只需要在上述代码第三步创建 User 时指定stableRollout 即可,具体使用详情见:docs.featureprobe.io/zh-CN/tutor…


总结


灰度按百分比放量是一种软件开发中常用的功能发布方法,它可以帮助提高软件可靠性,提高用户体验,在实施时也需要注意几个方面:


1、确定放量目标:首先需要确定放量的目标,例如增加多少百分比的数据量。这个目标需要根据实际情况进行制定,例如需要考虑数据量的大小、计算资源的限制等因素。


2、确定放量规则:你需要确定在放量过程中,哪些功能会被启用,哪些功能会被禁用。你可以根据开发进度、测试结果和市场需求等因素来确定放量规则。


3、监控放量过程:在实施放量操作时,需要监控放量过程,以确保放量结果的稳定性和可靠性。如果出现异常情况,需要及时采取措施进行调整。


若要了解有关FeatureProbe 灰度发布的更多信息,请查看其官方文档中的教程。该教程可以提供关于如何进行灰度发布的详细说明。文档中还包括其他相关主题的信息,例如如何进行服务降级和指标分析等。请访问以下链接以查看该文档:docs.featureprobe.io/zh-CN/tutor…


关于我们


FeatureProbe 是国内首家功能开关管理开源平台,它包含了灰度放量、AB实验、实时配置变更等针对『功能粒度』的一系列管理操作,完全开源,可以放心直接使用。


当前 FeatureProbe 作为一个功能开关管理平台已经使用 Apache 2.0 License 协议完全开源,你可以在 GitHub 和 Gitee 上访问源码,你也可以在上面给提 issue 和 feature 等。


GitHub: github.com/FeatureProb…


Gitee: gitee.com/featureprob…


体验环境: featureprobe.io/


作者:FeatureProbe
来源:juejin.cn/post/7224045063424049208
收起阅读 »

我的30岁,难且正确的事情是什么?

过去这两年里完成管理角色转型,完成了移动端工程化建设,养成了早睡早起的习惯,还戒了烟,也看了不少书,学会了羽毛球,但这些都不是最重要的。 3月意料之中的最后裁员到来了,在充分了解个人意愿后留下两个不想看新工作的小伙伴,IOS Android各一个。我把自己也...
继续阅读 »

过去这两年里完成管理角色转型,完成了移动端工程化建设,养成了早睡早起的习惯,还戒了烟,也看了不少书,学会了羽毛球,但这些都不是最重要的。



3月意料之中的最后裁员到来了,在充分了解个人意愿后留下两个不想看新工作的小伙伴,IOS Android各一个。我把自己也写进了名单,跟其他能力相对强一点的一起出来了。回顾过去2年我们做的事情,我对大家还是蛮有信心的。同时基于对《钱》这本书的学习,我从前两年开始就一直留有1年以上的备用金,所以暂时也没太大经济压力,不至于因为囊中羞涩着急找一份谋生的工作。


刚离开公司的前两周,先花了1000多找了两个职业咨询师,了解目前的招聘环境,招聘平台,招聘数据,以及针对性的帮助我修改简历。都准备好以后,开始选公司试投简历,认真看完大部分JD后大概清楚自己的能力所匹配的公司,薪资范围。机会确实不多,移动端管理岗位,架构岗位就更少,尤其是像我这样工作不到10年,架构跟管理经验都还未满5年的人,选择更是寥寥无几。


先后参加了两个2面试,一个是小团队的移动 TL,在了解后双边意向都不大。另一个是 Android 架构方向。虽然拿了offer,薪资包平移,但我最终没去。一是发生了一点小误会,发offer前电话没告诉我职级,我以为架构岗过了其实没有,差一点点到P7。回看面试记录,提升并不困难,有能力冲一冲的,这一次并不会影响我的信心。


另一个则是我真的冷静下来了,也就有了这篇文章。


在这两周里,陆续写了一些文章,做了一些开源项目。完全是出于助人为乐回馈社区,没想到也因此结识了几个做阅读业务的同学,纷纷向我抛来橄榄枝。其中包含一个已经在行业内做到Top3的产品。这让我有些受宠若惊,毕竟我觉得我给的技术方案并非有很大门槛,只是运气好站在巨人的肩膀上想到了不同的方案而已。


正是这些非常正面的反馈,帮助我消化了很大一部分所谓的焦虑(难说我本身一点不受环境影响)。在Zhuang的帮助下,我大概做了两次自我梳理,最后在前几天我从地铁回家大概3km的步行中想明白了很多事情。


每次出去旅游时,比如我躺在草原上,看着日落,说实话我欣赏不了10分钟。因为我的思绪停不下来,我就会去想一些产品或者是管理方面的问题。我很爱工作,或者说喜欢工作时可以反复获取创造性的快乐,比如做出一个新的技术方案或者优化工作流程解决一个团队问题,都让人很兴奋。但现在,我想强迫自己来思考一些更长期的事情。


我的30岁,难而且正确的事情是什么?


是找一份工作吗?这显然不难,作为技术人,找一份薪资合理的工作不难,这恰恰是最容易的


是找一份自己喜欢的工作吗?这有一点难,更多的是需要运气。职业生涯就十几年,有几次选择的机会呢?这更多的是在合理化自己对稳定性,舒适性的追求,掩盖自己对风险的逃避。


是找一个自己喜欢的事情,并以此谋生吗?这很难,比如先找到自己长期喜欢长期坚持投入的事情就很难,再以此谋生就需要更多的运气与常年积累去等待这个运气出现,比如一些up主。这可以是顺其自然的理想,但不适合作为目标。


上面似乎都是一个个的问题,或者说看到这些问题的时候我就看到了自己的天花板了。因为我可以预见我在这些方向上的学习能力,积累速度,成长空间,资源储备。


这半年涌出了太多的新事物,像极了02年前后的互联网,14前后的移动互联网。我从去年12月5日开始使用GPT,帮助我提高工作,学习效率,帮助我做UI设计,帮助我改善代码,甚至帮助我学习开网店时做选品,做策略,可以说他已经完全融入我的工作学习了。


开发自己的GPT应用要仔细阅读OPEN AI 的API,我再次因为英语的理解速度过慢严重影响学习效率,即使是有众多实时翻译软件帮助下丝毫不会有所改善。


翻译必然会对原文做二次加工,翻译的质量也许很高,甚至超过原文,但这样意味着阅读者离原文越远。


比如我在Tandem上教老外“冰雪聪明”这个词的意思,我很难解释给她,更多的是告诉她这个词在什么场景用比较恰当,比“聪明”更高级。但是如果用翻译软件,这个词会变着花样被翻译成“很聪明”,美感全无。


在Tandem跟人瞎聊时以涉及复杂事件就词穷,直到认识了一个 西班牙的 PHD 与另一个 印尼的大学生,她们帮我找到了关键点,基础语法知识不扎实,英语的思维不足。有些时候他们会说我表达的很棒,口语也行,有些时候他们会说我瞎搞。其实很好理解,就像他们学中文一样,入门也不难,难的是随意调动有限的词汇自由组织句子进行表达,而不是脑子里先想一个母语再试着翻译成外语,难的是在陌生场景下做出正确的表达,能用已经学的知识学习新知识,也就是进入用英语学习英语的阶段。


另外一个例子就是做日常技术学习的时候,尤其是阅读源码的时候,往往是不翻译看懂一部分注释,翻译后看懂一部分,两个一结合就半懂不懂,基于这个半懂不懂的理解写大量测试去验证自己的理解,反推注释是否理解正确,这个过程非常慢,效率极低。


这就是为什么很多东西需要依赖大佬写个介绍文档,或是翻译+延伸解释之后才能高效率学习,为什么自己找不到深入学习的路径,总是觉得前方有些混沌。


记得在刚入行的前几年写过一篇学习笔记,把自定义view 在view层测量相关的代码中的注释,变量名称整个都翻译了,备注2进制标记位变化结果,再去理解逻辑就非常简单了。跟读小说没啥区别(读Java代码就像读小说,我一直这么觉得),很快就理解了。但这个过程要花太多时间了,多到比别人慢10倍不止。


所以这第一个难而正确的事情是学习英语


达到能顺畅阅读技术资料与代码的地步,才能提高我在学习效率上的天花板。


第二个是有关生活的事情,增加不同的收入手段,主业以外至少赚到1块钱


裁员给我最大的感触就是,我很脆弱,我的职业生涯很脆弱,我的生存能力很脆弱,不具备一点反脆弱性。如果没有工作我就没有任何收入,只要稍微发生一点意外,就会面临巨大的经济压力,对于我和家庭都会陷入严重的经济困难中。


市场寒冬与我有关但却不受我影响,我无法改变。同时平庸的职业经历在行业内的影响微乎其微,大佬们是不管寒风往哪吹的,他们只管找自己想做的方向,或者别人找到他们。


我就认识这样的大佬,去年让我去新公司负责组新团队,连续一两周持续对我进行电话轰炸,因为当时正负责的团队处于关键期,我有很深的“良知”情节,我婉拒了,这是优点也是缺点。


而我只有不断提高自己的能力,让人觉得有价值才能持续在这个行业跟关系网里谋生。


但是我知道,大风之下我依然是树叶,我不是树枝,成为树枝需要天时地利人和。就像在公司背小锅的永远都是一线,因为如果是管理层背锅那公司就出了决策性的大问题了,对公司而言已然就是灾难。


这几周陆续跟很多人聊了各种事情,了解他们在做什么。有双双辞职1年多就在家做私活忙得不亦乐乎,有开网店有做跨境电商的,也了解了很多用Chat GPT,Midjourney 等AI工具做实物产品在网上卖的。包括去年了解的生财有术知识星球等等,真的花心思去了解,打开知识茧房确实了解到非常多不同的方向,有一种刘姥姥进大观园的感觉。


自己做了一些实际尝试,跑了下基本流程,确实有一些门槛但各不相同。同时在这个过程中,又因为英语阅读效率低而受阻,文档我也是硬看,不懂的词翻译一下,理解不透再整句翻译,再倒回来看原文。


比如网上midjourney的教程一大把,其实大多数都不如看midjourney官方文档来的快,我从看到用到商品上架,不过几个小时,这中间还包括开通支付跟调整模型。


至于赚到1块钱,有多难呢,当我试了我才有所体会。


种一棵树最好的时间是在10年前,其次是现在。


继续保持在社区的输出,保持技术学习。休假我都不会完全休息,Gap 中当然也不会。


后记


去年公司陆续开始裁撤业务线,有的部门直接清零,公司规模从几千人下降到千人以内不过是几个月的事情,有被裁的,也有为了降低自身风险而主动走裁员名单,这也是双赢的解决方案,公司能精简人员个人可以拿到赔偿。管理层的主要工作是尽力留下核心成员,温和的送走要离开的成员,最大程度降低团队的负面情绪,做人才盘点,申请HC做些人力补充,减少团队震动保障项目支撑。没错,一边裁员一边还会招人。


彼时我个人也才刚刚在管理岗上站稳脚跟不久,团队里没有人申请主动离职算是对我挺大一个宽慰。有的团队人员流失率接近70%,相比之下我压力小得多,但我依然要交出一个名字给到部门负责人。我当然很不舍同时也为他们担忧,过去一年多大家一起相互成长,很多人也才刚刚步入新职级。


我努力寻找第三选择,功夫不负有心人,之前做过的一个项目独立出去了,成立了独立的子公司运营,新团还没搭建完。当时跟那个项目团队的产品,后端负责人配合得相当不错,我便以个人的背书将一个曾重点负责过这个项目的成员推荐过去,加上直属上级的帮助,最终在所有HC都要HRD审批的环境下平滑的将裁员变成了团队调配。现在即使我离开了母公司,他们小团队依然还不错,没有受到后续裁员影响。这位小伙伴人特别实在,他是我见过执行里最强的人,他值得这样的好运气。


作为管理者,我有些单纯的善意,不满足于工作层面的帮助。因为我觉得个人能量实在是太小了,而未来无人知晓。


作为核心部门虽然裁员的影响波及较为滞后,但明显的感觉还是研发压力骤减,加上公司为了早点达到账面盈亏平衡,对部分薪资采取缓发,在这样的背景下整个部门的氛围变了,需求评审得过且过,项目质量得过且过,此类情况比比皆是,工作的宽容度也一再升高。


作为个人来讲这真是躺平型工作,工作任务骤减但薪资还照样发,绩效照发,每天到公司跟去上学一样。我心里就出现了一个声音「你喜欢在这里继续混吗?这里如此安逸」。


今年3月意料之中的新一轮裁员到来,我几乎没有犹豫就答复了部门负责人。团里谁想留下谁不想留我很清楚,过去我们一直也保持比较健康的氛围,始终鼓励能力强的人出去看看,也明确告知留下来与出去将面临的不同风险。大家都有心理准备,但大家都没有懈怠自己的学习,技术目标按部就班,丝毫没有陷入负面漩涡,偶尔还会因为讨论技术问题忘记下班时间。


这一次,我把自己放在了名单上,当然这并不突然。我与部门负责人一直保持着较高的工作沟通频率,就向上管理这一点上,我自认做得非常不错。


离职后大家都积极找工作,我对他们非常有信心,抛开头部大厂,中厂依然是他们的主阵地,他们在各自专精的领域里技术都很扎实,尤其是去年大家一起补足了深层次的网络层知识。不出意料部分人都很快拿了offer,有的更是觉得不想面试了匆匆就入职了,这我就不行使自己好为人师的毛病了。




滑到底部可点赞评论



浏览橘子树其他文章:橘子树的飞书写作记录


作者:橘子没
来源:juejin.cn/post/7224068169341763643

收起阅读 »

2023年,我从北京回到了东北

(背景应景吗铁铁们) 🎤: 可以讲讲在北京这几年的经历吗?   🙍‍♂️:5年多,算是大小厂都经历过了,大概是小-大-中的一个顺序,刚毕业的时候去了一家做线下教育的小公司,现已倒闭。   后来去到了阿里的一个边缘业务,当不当正不正吧,那段时间我尽可能的提升自身...
继续阅读 »

(背景应景吗铁铁们)


🎤: 可以讲讲在北京这几年的经历吗?


  🙍‍♂️:5年多,算是大小厂都经历过了,大概是小-大-中的一个顺序,刚毕业的时候去了一家做线下教育的小公司,现已倒闭。
   后来去到了阿里的一个边缘业务,当不当正不正吧,那段时间我尽可能的提升自身的认知水平,想要跟上各位大佬们的思维,讲真,有的时候P8领导给我们开会,我get不到大部分会议内容的核心点和他表达的东西,最后让我明白了人和人之间的差距真的可能这一生都追赶不上。
   再后来就去到了一家中小厂吧,老板较有知名度,做知识服务的,在这里度过了我最后的北漂时光。


🎤: 在北京一定学习到了很多东西吧,最大的收获是什么?


  🙍‍♂️:这倒是不假,毕竟是帝都,确实让我见识到了很多很多新的东西和形形色色的人们,这是不曾在老家可以看见的。
   至于学习吧,我本身是一个很少会去主动学习的人,更多的情况是在工作需要,我才会去学习。所以不能说没有长进,只能说略微缓慢,但是我并没有觉得自己很差劲。还有一些生活技能,最多的就是做饭
   除了自身的成长以外,要说最大的收获,我必须要很诚实的说,是一些精神问题,曾经有很长一段时间,甚至是到至今,我被自己内心的焦虑所控制,对自我有无数个疑问。我曾经因为一个需求难搞以至于我做梦都在梦我如何去写它的代码;无数次的凌晨钉钉或是飞书群的@消息,我一般是第二天起床才看到,但是看到那些的瞬间我头疼欲裂胸口紧缩。我的内心会一直问:“为什么?为什么不能让我睡一个好觉或者是起一个好早。”,大城市的节奏之快让我睁眼的第一件事就是想到工作,或者是带着工作入眠,开玩笑,入不了,根本入不了。种种原因吧,可能是我心态的问题,所以决定回到老家也是为了尽可能的调整一下自己的心态。


huabu.webp

(自己做的炒鸡蛋和葱爆肉)


🎤: 在北京一定有攒钱吧?


  🙍‍♂️:攒钱是攒了一些,但是没有很多。当我真正开始月薪过万的时候,对于一个从来没有见过5位数进账的人来讲,我开始了一种报复性消费,我把之前不敢买的东西几乎都买了,游戏也充了很多钱,每个月都会买衣服和鞋,甚至如果有的东西我想要但是手里的钱不够,我也会分期买,前两年的消费很夸张,而且从来不记账,现在回想起来那时候真的是虚荣的、丑陋的。(仅对我个人而言)
   真正开始攒钱的时候是从我的女朋友来到北京找我开始,我们感情很好,所以就像两个人正常的在一起生活一样,总是要为生活和未来买单的,于是我每个月发完工资都会经过她的计算后留下基本的生活费,然后全部转给她,她掌管家里的财政,很让我安心。就这样后几年算是攒下了一笔小钱。
   在我的理解是爱情更多还是要责任更多一些吧,所以我不会觉得让我不再乱花钱也是一种约束


🎤: 真正让你决定离开北京的原因是什么?


  🙍‍♂️:没有能绝对决定的原因,只有能压垮的最后一根稻草。
   从刚来北京的奋斗b到现在只想摆烂躺平的我是一个长期的心态上的变化。我最终得出一个结论:这里不适合我,或者说,我不适合它。 影响比较大的几件事都集中在了22年,三件事:
   1. 回家过年
   2. 奶奶过世
   3. 公司裁员
   这三件事贯穿了我整个2022年,可以说是一连串的事情打得我措手不及。于是经过了多次自我内心的审视、对答后,在过完23年的春节后,我做了这个决定,我要离开这里了。 电影的主角们总会说:“What does not kill me, makes me stronger.”,但是对于我来说,可以打倒我的东西太多了,我自认普通且平凡。



I just wanna be free.



🎤: 离开后你都做了些什么呢?


  🙍‍♂️:2月下旬开始处理离职交接的一些事情,同时还有面试一些老家那边的公司,然后租房子,收拾行李。每天真的是累到倒床就睡,感觉这次跨省搬家元气大伤、身心俱疲,掉了半条命一样。
   好在和女朋友的规划下,把时间节点安排的刚刚好,2月末把这些事儿都处理好了之后,3月初我和女朋友就开始了一周的云南之行,我俩称之为:心灵救赎之旅
   路线是丽江-大理-西双版纳,选择去云南是因为我们马上要回到东北,以后像这种东北到西南斜跨全国的旅游机会应该没几次了...让我印象最深刻的是丽江,我最大的感觉就是:纯净(我着实是没有文本功底)。无论人、山、水、云,大家生活在这里,仿佛一片世外桃源


huabu.webp

(丽江的云,是让我觉得离自由和奶奶最近的时候)


🎤: 回到东北后感觉怎么样?


  🙍‍♂️:先说工作吧,我目前在大连一家做车载车机的小公司,因为之前没有做过这类工作,所以最近也是在学习一些相关的知识,在这里真的要特别感谢一位大佬:林栩link,他的文章对于我来说很有帮助。然后工资对于我来讲还算可观,因为对大连这边的就业环境不是很了解,先在这里稳一稳吧。
   我租的房子和公司在一条街上,距离不到300m,每天中午还可以回家吃口饭歇一歇。工作时间的话是早8点半到晚5点半,目前一个月我没有加过班,到点就走了(而且是同事们几乎都这样),晚上和女朋友一起做饭,之后可以去市中心或者是海边溜达一圈,时间非常充足,周末的话会叫上朋友到家里吃饭或者是一起出去玩。
   距离我的老家鞍山也更近了一些,我妈说我自从回来了之后,她总有一种心里的石头落地了的感觉,我没事儿的时候就会和女朋友在周五晚上坐3个半小时的绿皮回去,然后周日的晚上再坐回来,回家吃自己爱吃的东西。
   算是找到了那种安安静静上班,认认真真生活的感觉。
   说这么多是因为这是我在北京这么多年从未体会过的生活,实在是没有发现生活的多样性(可能也是因为没钱吧hhh)。


huabu.webp

(星海广场的灯光秀)


🎤: 未来有什么样的规划?


  🙍‍♂️:哈哈,想在2年内和女朋友领证结婚,首付买一套小房子和一台小鹏G3i或者是零跑C11。然后多去了解了解这边的市场,在这么多空余的时间去计划搞一些副业(万一以后能干成主业呢)。


🎤: 想说的话。


  🙍‍♂️:嗯...看了很多大佬的年终总结,收获颇多,于是也想自己去写出一些东西,表达一下,哈哈哈。
   如果让我来总结自己的每一年的话,我好像只能说:还行、凑合、就那样,又不温不火的过了一年。但是我很喜欢一句话:空想都是问题,落笔皆为答案。不论好坏对错,都是我选择的答案,可能以后还会遇到相同或者不同的问题,只要不会停滞不前就好了。
   最后欢迎大家来找我一起讨论玩耍摸鱼~ 祝好!



我们生來自由,所以希望我们都有勇气选择自由。



作者:啊是小鱼快来摸摸
来源:juejin.cn/post/7222897518517551141
收起阅读 »

五一在即:有哪些办法能阻止票贩子和我们一起抢票?

背景 五一就要来了,本来准备去淄博吃烧烤,结果发现根本抢不到机票。了解了一下原因,原来是黄牛的恶意爬虫把票都抢完了,现在买票,只能通过黄牛的渠道,多花几倍的钱来买了。 有鉴于此,想试试能不能针对这种爬虫防止相关问题,当然是站在航空公司的角度,就个人而言,躺平任...
继续阅读 »

背景


五一就要来了,本来准备去淄博吃烧烤,结果发现根本抢不到机票。了解了一下原因,原来是黄牛的恶意爬虫把票都抢完了,现在买票,只能通过黄牛的渠道,多花几倍的钱来买了。


有鉴于此,想试试能不能针对这种爬虫防止相关问题,当然是站在航空公司的角度,就个人而言,躺平任嘲就得了。


恶意爬虫的相关特征


对于恶意爬虫的特征,航空公司自己有总结:


1、访问的目标网页比较集中:“爬虫”代理人目标明确,主要是爬取班次、价格、数量等核心信息,因此只浏览访问几个固定页面,不访问其他页面。


**2、查询订票等行为很有规律:**由于“爬虫”是程序化操作,按照预先设定的流程进行访问等,因此呈现出毫无思维、但很有规律、有节奏且持续的行为。


3、同一设备上有规模化的访问和操作:“爬虫”的目的是最短时间内抓取最多信息,因此同一设备会有大量离散的行为,包括访问、浏览、查询等。


**4、访问来源IP地址异常:**正常情况下用户在查询、购买时,用户的IP地址比较稳定,而且访问来源IP比较;“爬虫”、“虚假占座”等操作时,IP来源地址呈现不同维度上的聚集,而且浏览、查询、购票等操作时不停变化IP地址。


5、设置UA模拟浏览器和频繁使用代理IP:很多“爬虫”程序伪装成浏览器进行访问,比如在程序头或者UA中默认含有类似python-requests/2.18.4等固定字符串;并且通过购买或者租用的云服务、改造路由器、租用IP代理、频繁变更代理IP等进行访问。


6、操作多集中非业务时间段:“爬虫”程序运行时间多集中在无人值守阶段。此时系统监控会放松,而且平台的带宽等资源占用少,爬虫密集的批量爬取不会对带宽、接口造成影响。以下是顶象反欺诈中心监测到,凌晨1-5点是恶意“爬虫”的运行高峰时段。


浅谈防范措施


1.设备指纹的应用


设备指纹单独用的话,在恶意爬虫面前,会稍微较弱一些。因为攻击者会使用其他技术手段来绕过设备指纹的检测,例如使用虚拟机、代理服务器等来隐藏真实设备的指纹。所以可以考虑结合IP地址限制一起使用:


import fingerprintjs2
from flask import Flask, request, abort

app = Flask(__name__)

# 创建一个新的Fingerprint2实例
fp = fingerprintjs2.Fingerprint2()

# 白名单IP地址
IP_WHITELIST = ['127.0.0.1', '192.168.0.1']

@app.route('/book-flight')
def book_flight():
# 获取设备指纹和IP地址
device_fingerprint = fp.get()
ip_address = request.remote_addr

# 检查IP地址是否在白名单中
if ip_address not in IP_WHITELIST:
abort(403)

# 其他业务逻辑代码...
return "Flight booked successfully!"

if __name__ == '__main__':
app.run()


2. 人机验证


人机验证肯定是一个阻挡办法,但是在这种恶意强攻击的情况下,我们可以试试语音验证码(但是可能对用户体验不大友好):


from flask import Flask, request, jsonify
import random
from io import BytesIO
from captcha.audio import AudioCaptcha

app = Flask(__name__)

# 生成随机字符串作为验证码
def random_string(length):
pool = 'abcdefghijklmnopqrstuvwxyz0123456789'
return ''.join(random.choice(pool) for i in range(length))

# 生成语音验证码
def generate_audio_captcha():
captcha = random_string(4)
audio = AudioCaptcha().generate(captcha)
return captcha, audio

# 创建一个全局变量,用于保存已生成的语音验证码
captcha_cache = {}

@app.route('/audio-captcha')
def audio_captcha():
# 生成新的语音验证码
captcha, audio = generate_audio_captcha()

# 将验证码保存到缓存中,以便后续验证
captcha_cache[captcha] = True

# 返回语音验证码
response = BytesIO(audio.read())
response.headers['Content-Type'] = 'audio/wav'
return response

@app.route('/book-flight', methods=['POST'])
def book_flight():
# 获取表单中的验证码
captcha = request.form['captcha']

# 检查验证码是否正确
if captcha not in captcha_cache:
return 'Invalid captcha!'

# 其他业务逻辑代码...
return "Flight booked successfully!"

if __name__ == '__main__':
app.run()


因为对用户体验不大友好,研发同学在设计的时候,最好考虑一下它的易用性和可访问性问题。需要权衡用户体验和恶意爬虫。


结语


“天下熙熙皆为利来,天下攘攘皆为利往。”因为这一块存在着巨大的利益,也就一直吸引着无数黑客从中牟利,而我们能做的,是“魔高一尺道高一丈”,希望能为安全世界尽一份力。


说个事情:滑动验证码 免 费

收起阅读 »

九个超级好用的 Javascript 技巧

web
作者:shichuan 文末彩蛋等你揭晓 🤫 前言 在实际的开发工作过程中,积累了一些常见又超级好用的 Javascript 技巧和代码片段,包括整理的其他大神的 JS 使用技巧,今天筛选了 9 个,以供大家参考。 1、动态加载 JS 文件 在一些特殊的场景...
继续阅读 »

作者:shichuan


文末彩蛋等你揭晓 🤫



前言


在实际的开发工作过程中,积累了一些常见又超级好用的 Javascript 技巧和代码片段,包括整理的其他大神的 JS 使用技巧,今天筛选了 9 个,以供大家参考。


1、动态加载 JS 文件


在一些特殊的场景下,特别是一些库和框架的开发中,我们有时会去动态的加载 JS 文件并执行,下面是利用 Promise 进行了简单的封装。


function loadJS(files, done) {
// 获取head标签
const head = document.getElementsByTagName('head')[0];
Promise.all(files.map(file => {
return new Promise(resolve => {
// 创建script标签并添加到head
const s = document.createElement('script');
s.type = "text/javascript";
s.async = true;
s.src = file;
// 监听load事件,如果加载完成则resolve
s.addEventListener('load', (e) => resolve(), false);
head.appendChild(s);
});
})).then(done); // 所有均完成,执行用户的回调事件
}

loadJS(["test1.js", "test2.js"], () => {
// 用户的回调逻辑
});

上面代码核心有两点,一是利用 Promise 处理异步的逻辑,而是利用 script 标签进行 js 的加载并执行。


2、实现模板引擎


下面示例用了极少的代码实现了动态的模板渲染引擎,不仅支持普通的动态变量的替换,还支持包含 for 循环,if 判断等的动态的 JS 语法逻辑,具体实现逻辑在笔者另外一篇文章《面试官问:你能手写一个模版引擎吗?》做了非常详详尽的说明,感兴趣的小伙伴可自行阅读。


// 这是包含了js代码的动态模板
var template =
'My avorite sports:' +
'<%if(this.showSports) {%>' +
'<% for(var index in this.sports) { %>' +
'<a><%this.sports[index]%></a>' +
'<%}%>' +
'<%} else {%>' +
'<p>none</p>' +
'<%}%>';
// 这是我们要拼接的函数字符串
const code = `with(obj) {
var r=[];
r.push("My avorite sports:");
if(this.showSports) {
for(var index in this.sports) {
r.push("<a>");
r.push(this.sports[index]);
r.push("</a>");
}
} else {
r.push("<span>none</span>");
}
return r.join("");
}`

// 动态渲染的数据
const options = {
sports: ["swimming", "basketball", "football"],
showSports: true
}
// 构建可行的函数并传入参数,改变函数执行时this的指向
result = new Function("obj", code).apply(options, [options]);
console.log(result);

3、利用 reduce 进行数据结构的转换


有时候前端需要对后端传来的数据进行转换,以适配前端的业务逻辑,或者对组件的数据格式进行转换再传给后端进行处理,而 reduce 是一个非常强大的工具。


const arr = [
{ classId: "1", name: "张三", age: 16 },
{ classId: "1", name: "李四", age: 15 },
{ classId: "2", name: "王五", age: 16 },
{ classId: "3", name: "赵六", age: 15 },
{ classId: "2", name: "孔七", age: 16 }
];

groupArrayByKey(arr, "classId");

function groupArrayByKey(arr = [], key) {
return arr.reduce((t, v) => (!t[v[key]] && (t[v[key]] = []), t[v[key]].push(v), t), {})
}

很多很复杂的逻辑如果用 reduce 去处理,都非常的简洁。


4、添加默认值


有时候一个方法需要用户传入一个参数,通常情况下我们有两种处理方式,如果用户不传,我们通常会给一个默认值,亦或是用户必须要传一个参数,不传直接抛错。


function double() {
return value *2
}

// 不传的话给一个默认值0
function double(value = 0) {
return value * 2
}

// 用户必须要传一个参数,不传参数就抛出一个错误

const required = () => {
throw new Error("This function requires one parameter.")
}
function double(value = required()) {
return value * 2
}

double(3) // 6
double() // throw Error

listen 方法用来创建一个 NodeJS 的原生 http 服务并监听端口,在服务的回调函数中创建 context,然后调用用户注册的回调函数并传递生成的 context。下面我们以前看下 createContext 和 handleRequest 的实现。


5、函数只执行一次


有些情况下我们有一些特殊的场景,某一个函数只允许执行一次,或者绑定的某一个方法只允许执行一次。


export function once (fn) {
// 利用闭包判断函数是否执行过
let called = false
return function () {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
}

6、实现 Curring


JavaScript 的柯里化是指将接受多个参数的函数转换为一系列只接受一个参数的函数的过程。这样可以更加灵活地使用函数,减少重复代码,并增加代码的可读性。


function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}

function add(x, y) {
return x + y;
}

const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)); // 输出 3
console.log(curriedAdd(1, 2)); // 输出 3

通过柯里化,我们可以将一些常见的功能模块化,例如验证、缓存等等。这样可以提高代码的可维护性和可读性,减少出错的机会。


7、实现单例模式


JavaScript 的单例模式是一种常用的设计模式,它可以确保一个类只有一个实例,并提供对该实例的全局访问点,在 JS 中有广泛的应用场景,如购物车,缓存对象,全局的状态管理等等。


let cache;
class A {
// ...
}

function getInstance() {
if (cache) return cache;
return cache = new A();
}

const x = getInstance();
const y = getInstance();

console.log(x === y); // true

8、实现 CommonJs 规范


CommonJS 规范的核心思想是将每个文件都看作一个模块,每个模块都有自己的作用域,其中的变量、函数和对象都是私有的,不能被外部访问。要访问模块中的数据,必须通过导出(exports)和导入(require)的方式。


// id:完整的文件名
const path = require('path');
const fs = require('fs');
function Module(id){
// 用来唯一标识模块
this.id = id;
// 用来导出模块的属性和方法
this.exports = {};
}

function myRequire(filePath) {
// 直接调用Module的静态方法进行文件的加载
return Module._load(filePath);
}

Module._cache = {};
Module._load = function(filePath) {
// 首先通过用户传入的filePath寻址文件的绝对路径
// 因为再CommnJS中,模块的唯一标识是文件的绝对路径
const realPath = Module._resoleveFilename(filePath);
// 缓存优先,如果缓存中存在即直接返回模块的exports属性
let cacheModule = Module._cache[realPath];
if(cacheModule) return cacheModule.exports;
// 如果第一次加载,需要new一个模块,参数是文件的绝对路径
let module = new Module(realPath);
// 调用模块的load方法去编译模块
module.load(realPath);
return module.exports;
}

// node文件暂不讨论
Module._extensions = {
// 对js文件处理
".js": handleJS,
// 对json文件处理
".json": handleJSON
}

function handleJSON(module) {
// 如果是json文件,直接用fs.readFileSync进行读取,
// 然后用JSON.parse进行转化,直接返回即可
const json = fs.readFileSync(module.id, 'utf-8')
module.exports = JSON.parse(json)
}

function handleJS(module) {
const js = fs.readFileSync(module.id, 'utf-8')
let fn = new Function('exports', 'myRequire', 'module', '__filename', '__dirname', js)
let exports = module.exports;
// 组装后的函数直接执行即可
fn.call(exports, exports, myRequire, module,module.id,path.dirname(module.id))
}

Module._resolveFilename = function (filePath) {
// 拼接绝对路径,然后去查找,存在即返回
let absPath = path.resolve(__dirname, filePath);
let exists = fs.existsSync(absPath);
if (exists) return absPath;
// 如果不存在,依次拼接.js,.json,.node进行尝试
let keys = Object.keys(Module._extensions);
for (let i = 0; i < keys.length; i++) {
let currentPath = absPath + keys[i];
if (fs.existsSync(currentPath)) return currentPath;
}
};

Module.prototype.load = function(realPath) {
// 获取文件扩展名,交由相对应的方法进行处理
let extname = path.extname(realPath)
Module._extensions[extname](this)
}

上面对 CommonJs 规范进行了简单的实现,核心解决了作用域的隔离,并提供了 Myrequire 方法进行方法和属性的加载,对于上面的实现,笔者专门有一篇文章《38 行代码带你实现 CommonJS 规范》进行了详细的说明,感兴趣的小伙伴可自行阅读。


9、递归获取对象属性


如果让我挑选一个用的最广泛的设计模式,我会选观察者模式,如果让我挑一个我所遇到的最多的算法思维,那肯定是递归,递归通过将原始问题分割为结构相同的子问题,然后依次解决这些子问题,组合子问题的结果最终获得原问题的答案。


const user = {
info: {
name: "张三",
address: { home: "Shaanxi", company: "Xian" },
},
};

// obj是获取属性的对象,path是路径,fallback是默认值
function get(obj, path, fallback) {
const parts = path.split(".");
const key = parts.shift();
if (typeof obj[key] !== "undefined") {
return parts.length > 0 ?
get(obj[key], parts.join("."), fallback) :
obj[key];
}
// 如果没有找到key返回fallback
return fallback;
}

console.log(get(user, "info.name")); // 张三
console.log(get(user, "info.address.home")); // Shaanxi
console.log(get(user, "info.address.company")); // Xian
console.log(get(user, "info.address.abc", "fallback")); // fallback

上面挑选了 9 个笔者认为比较有用的 JS 技巧,希望对大家有所帮助。


🎁 文末彩蛋 >>


码上掘金编程比赛火热进行中,同时为大家推出「报名礼 & 完赛奖」活动~

报名即有机会瓜分上百万掘金矿石奖池!提交作品更可参与精美奖品的抽取哦!


🎁 抽奖攻略请戳这里

🎡 更多大赛特别活动请看这里




尾部关注.gif


扫码关注公众号 👆 追更不迷路


作者:字节前端
来源:juejin.cn/post/7223938976158957624
收起阅读 »

用CSS给健身的女朋友做一个喝水记录本

web
我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情 前言 事情是这样的,由于七八月份的晚上时不时就坐在地摊上开始了喝酒撸串的一系列放肆的长肉肉项目。 这不,前段时间女朋友痛下决心(心血来潮)地就去报了一个健身的私教班,按照教练给...
继续阅读 »

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情


前言


事情是这样的,由于七八月份的晚上时不时就坐在地摊上开始了喝酒撸串的一系列放肆的长肉肉项目。
这不,前段时间女朋友痛下决心(心血来潮)地就去报了一个健身的私教班,按照教练给的饮食计划中,其中有一项是每天需要喝 2.6L 的水来促进体内的新陈代谢。
作为伴侣肯定要十分支持的呀,不过因为平时工作也是十分费脑筋的,不会专门去记录每天喝了多少水,特别容易忘记。所以做了这个喝水记录本给她。


开发需求


整体的开发需求和前言里描述的差不多,整体功能拆分一下就非常清晰了。


一、定义变量



  1. 大杯子:我们需要一个总量目标,用于定义每天的计划值。

  2. 小杯子:一个单次目标,我们不会一次接一大桶水来喝,即使用小杯子喝水时,每个杯子的刻度值。


二、逻辑整合



  1. 点击每个小杯子时,从大杯子的总量中扣除小杯子的刻度并记录,对应UI水位升高。

  2. 首次点击小杯子时,展示百分率刻度值,提升水位。

  3. 当完成目标值后,隐藏剩余水量的文字。

  4. "清空"按钮,消除本地记录值,恢复UI水位,展示剩余量。


创建流程和主要代码


 此模块代码是应用于小程序使用的,所以代码部分使用wx框架。(下面有普通代码部分)


wxml


构造整体布局,布局和制作大杯子和小杯子。


在上一段开发需求部分中提到的隐藏内容时,注意不要使用 wx:if 直接删除整个标签,这样会导致画面跳动,无法实现动画的平滑过渡。


用三元运算符隐藏文字可以实现较好的过渡


<view class="body">
<text class="h1">喝水记录本</text>
<text class="h3">今日目标: 2.6升 </text>

<view class="cup">
<view class="remained" style="height: {{remainedH}}px">
<text class="span">{{isRemained ? liters : ''}}</text>
<text class="small">{{isRemained ? '剩余' : ''}}</text>
</view>

<view class="percentage" style="{{percentageH}}">{{isPercentage ? percentage : ''}}</view>
</view>

<text class="text">请选择喝水的杯子</text>

<view class="cups">
<view class="cup cup-small" bindtap="cups" data-ml="700">700 ml</view>
<view class="cup cup-small" bindtap="cups" data-ml="400">400 ml</view>
<view class="cup cup-small" bindtap="cups" data-ml="600">600 ml</view>
<view class="cup cup-small" bindtap="cups" data-ml="500">500 ml</view>
<view class="cup cup-small" bindtap="cups" data-ml="50">50 ml</view>
<view class="cup cup-small" bindtap="cups" data-ml="100">100 ml</view>
<view class="cup cup-small" bindtap="cups" data-ml="150">150 ml</view>
<view class="cup cup-small" bindtap="cups" data-ml="300">300 ml</view>
</view>

<view class="cancle" bindtap="update">清空</view>
</view>

wxss


css就是简单的画杯子和布局,值得说的就是往大杯子里加水的动画 transition 一下就可以了


.body {
height: 108vh;
background-color: #3494e4;
color: #fff;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
}

.h1 {
margin: 10px 0 0;
}

.h3 {
font-weight: 400;
margin: 10px 0;
}

.cup {
background-color: #fff;
border: 4px solid #144fc6;
color: #144fc6;
border-radius: 0 0 40px 40px;
height: 330px;
width: 150px;
margin: 30px 0;
display: flex;
flex-direction: column;
overflow: hidden;
}

.cup.cup-small {
height: 95px;
width: 50px;
border-radius: 0 0 15px 15px;
background-color: rgba(255, 255, 255, 0.9);
cursor: pointer;
font-size: 14px;
align-items: center;
justify-content: center;
text-align: center;
margin: 5px;
transition: 0.3s ease;
}

.cup.cup-small.full {
background-color: #6ab3f8;
color: #fff;
}

.cups {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
width: 280px;
}

.remained {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
flex: 1;
transition: 0.3s ease;
}

.remained .span {
font-size: 20px;
font-weight: bold;
}

.remained .small {
font-size: 12px;
}

.percentage {
background-color: #6ab3f8;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 30px;
height: 0;
transition: 0.3s ease;
box-sizing: border-box;
}

.text {
text-align: center;
margin: 0 0 5px;
}

.cancle {
cursor: pointer;
}

js


逻辑注释写在了代码中


Page({
data: {
liters: '2.6L',
isPercentage: true,
isRemained: true,
percentage: '',
percentageH: 'height: 0',
RemainedH: 0,
goal: 2600
},

// 每次进入页面后加载记录的值,执行动画
onShow() {
this.setData({ goal: Number(wx.getStorageSync('goal')) })
this.updateBigCup(2600 - this.data.goal)
},

// 点击小杯子时的触发逻辑
cups(data) {
const ml = Number(data.currentTarget.dataset.ml);
const goal = this.data.goal - ml;
const total = 2600 - goal;
this.setData({ goal })
wx.setStorageSync("goal", goal);
this.updateBigCup(total)
},

// 更新 UI 数据
updateBigCup(total) {
const { goal } = this.data;
if (goal != 2600) {
this.setData({
isPercentage: true,
percentage: `${(total / 2600 * 100).toFixed(0)}%`,
percentageH: `height: ${total / 2600 * 330}px`
})
}

if (goal <= 0) {
this.setData({
remainedH: 0,
isRemained: false,
})
} else {
this.setData({
isRemained: true,
liters: `${goal / 1000}L`
})
}
},

// 清空记录值
update() {
wx.removeStorage({ key: 'goal' })
this.setData({
goal: 2600,
isPercentage: false,
isRemained: true,
remainedH: 0,
percentageH: 'height: 0px',
liters: '2.6L'
})
}
})

码上掘金


  上面的代码部分主要用于小程序使用,码上掘金可在网页中使用。



结语


  感谢大家能看到这里!!本篇的代码本身没有什么技术含量,可能是比较会偏向实用性的一篇,对!是有一些的对吧!可以自己改装成Chrome插件使用会更方便更实用。啥?你问我为什么不直接写Chrome插件?有没有一种可能不是我不想,而是😭。


  好啦,如果你身边有健身的朋友也可以给他做

作者:dudoit
来源:juejin.cn/post/7147529288164573192
一个哦~再次谢谢大家

收起阅读 »

制作了一个图片像素风转换器

web
制作了一个图片像素风转换器,可以将图片转换成像素风格,并可转换为 css box-shadow 进行输出。前排先放效果图、转换器地址和 GitHub 地址: 转化器地址:pixel.heyfe.org/ GitHub 地址:github.com/ZxBing...
继续阅读 »

制作了一个图片像素风转换器,可以将图片转换成像素风格,并可转换为 css box-shadow 进行输出。前排先放效果图、转换器地址和 GitHub 地址:


blog-mosaic-converter-44.gif


转化器地址:pixel.heyfe.org/


GitHub 地址:github.com/ZxBing0066/…


转换器功能


转换器会将传入的图片转换为像素风格,并将像素风格的图片以 box-shadow 进行转换,借助 box-shadow,我们可以直接用 css 来渲染该图片,且可以通过 box-shadow 的一些特性来达成一些比较好玩的效果,比如用间隙来加重像素风格:


blog-mosaic-converter-84.png


或者直接将间隙拉到顶,达成类似点阵图的效果:


blog-mosaic-converter-55.png


又或者借助 border-radius,实现圆点图效果:


blog-mosaic-converter-71.png


制作出想要的效果后,可以在右侧点击 复制 box-shadow 样式 按钮复制其样式。


实现原理


关于 box-shadow 实现像素图的原理之前有一篇文章中有提到,这里不再赘述。此处大概说一下图片转换为像素图再转为 box-shadow 的过程。


转换器在拿到图片后,会将图片绘制在一个非常小的画布中,以此来降低图片的精度,然后将画布中绘制的低精度图片进行二次渲染,渲染到较大的画布中,此时由于图片被拉伸,就会形成一定的像素效果。随后为了将像素效果图转换为 box-shadow,转换器会去读取画布中的绘制信息,将其生成为一组二维数组,再根据其中的颜色转换为 box-shadow 中的属性。至此转换器的功能就完成了。


当然其中还有一些细节(浏览器会默认启用平滑绘制导致像素效果消失等问题),本篇不打算细说,会在下篇专门写一篇来讲一下具体实现。


最后


本转换器原先是在码上掘金挑战赛某次文章中构想 ,然后在第二次制作类似效果时干脆使用脚本来完成了,最近有空就将其稍微优化了一下进行开源。目前一些细节还有点欠缺,待改进。


再贴一下地址:


转化器地址:pixel.heyfe.org/


GitHub 地址:github.com/ZxBing0066/…


相关文章



作者:嘿嘿Z
来源:juejin.cn/post/7150465824690536484
收起阅读 »

ElasticSearch数据存储与搜索基本原理

1.缘起: 为啥想学习es,主要是在工作中会用到,但是因为不了解原理,所以用起来畏手畏脚的,就想了解下es是怎么存储数据,以及es是怎么搜索数据的,我们平时应该如 何使用es,以及使用时候需要注意的方面。 es:github.com/elastic/ela… ...
继续阅读 »

1.缘起:


为啥想学习es,主要是在工作中会用到,但是因为不了解原理,所以用起来畏手畏脚的,就想了解下es是怎么存储数据,以及es是怎么搜索数据的,我们平时应该如 何使用es,以及使用时候需要注意的方面。
es:github.com/elastic/ela…
lucene:github.com/apache/luce…


2.es的一些基础概念


es是一个基于lucence的分布式的搜索引擎,它使用java编写,并提供了一套RESTful api,是一款流行的企业级搜索引擎


2.1 es的特点



  1. 横向可扩展性: 作为大型分布式集群, 很容易就能扩展新的服务器到ES集群中; 也可运行在单机上作为轻量级搜索引擎使用.

  2. 更丰富的功能: 与传统关系型数据库相比, ES提供了全文检索、同义词处理、相关度排名、复杂数据分析、海量数据的近实时处理等功能.

  3. 分片机制提供更好地分布性: 同一个索引被分为多个分片(Shard), 利用分而治之的思想提升处理效率.

  4. 高可用: 提供副本(Replica)机制, 一个分片可以设置多个副本, 即使在某些服务器宕机后, 集群仍能正常工作.

  5. 开箱即用: 提供简单易用的API, 服务的搭建、部署和使用都很容易操作.


2.2 es的重要概念


1.cluster(集群)
可以通过为多个节点配置同一个集群名来创建集群,通过elasticsearch.yml文件配置。
2.node(节点)
运行了单个es实例的主机被成为节点,一个集群里会包含一个或者多个节点。可以用来存储数据,搜索数据,操作数据。有三个主节点,三个数据节点。


3.shard(分片)
一个索引会分成多个分片,并存储在不同的节点中。每个shard都是一个最小工作单元,承载部分数据,对应一个lucene实例,具有完整的建立索引和处理请求的能力。shard分为primary shard和replica shard,其中replica shard 负责容错,以及承担读请求负载。一个document只会存在一个primary shard及其replica shard中,而不会存在于多个primary shard中。shards:5*2,表示有五个primary shard 以及五个replica shard。一旦创建完成,其primary shard的数量将不可更改。


4.index(索引)
一堆类型数据结构相同的document的集合。类似于数据库中的表
5.document( 文档)
es中的最小数据单元。比如一条纠纷单的数据,存在es中就是一个document。但是存储格式为json。类似于数据库中的一行数据。
6.type
类型,一个索引会存在一个或者多个 type,一个type下的document有相同的field。7.x后废弃
7.field
一个field就是一个数据字段
8.term
field的内容在经过analyze后,会被分词为term,是数据中最小的存储单位
9.数据库和es概念类比Elasticsearch 关系型数据库


3.es是如何存储数据的


3.1 es写入数据过程


在这里插入图片描述



  1. 用户发送的请求会随机打到某一node,此时这个node为coordinate node

  2. coordinate node通过路由策略找到对应的主分片 shard = hash(routing) % number_of_primary_shards,其中routing为docId,如果docId不存在,es会生成一个id来实现路由。主分片也会把请求转发到副本分片,实现数据备份。索引的primary shards在索引创建后不可更改也是因为路由策略,举个例子,对于同一个docId,原本存在primary shard 0 中,但是primary shard num修改后,就被路由到primary shard 3,这样就出现数据查不到的情况了。

  3. 主分片&副本分片会构建索引以及将索引落盘


3.2 es近实时特性的原理



  1. 写请求将数据写入buffer中,此时数据是不能被搜索到的,此时会同时将写操作记录在translog中,translog的落盘如果配置成同步,此时就会落盘,如果配置成异步,会在配置间隔时间进行落盘。

  2. 默认1s一次的refresh操作,es会将buffer里的数据存入os cache中,并把buffer中的数据转化成segment,此时document便可以被搜索到了。每次refresh都会生成一个segment,es会定期进行segment合并。refresh数据到os cache后,buffer会被清空。

  3. 每隔30min或者segment达到512M 后,会把os cache中的segment写入到磁盘中,这个过程叫做flush。此时会生成一个commit point文件,用来唯一标识该segment。执行flush操作时,会把buffer和os cache里的数据清空,此时translog也会被落盘。原有的translog会被删除,会在内存中创建一个新的translog。


3.3 segment的数据结构以及索引的原理


segment是lucene的概念,也是实现搜索的关键。名称 扩展名 简要描述
Term Dictionary .tim term词典,存储term信息
Term Index .tip 指向term词典的索引
Frequencies .doc 包含了有term的频率的文档列表
Positions .pos 存储了每个出现在索引中的term的位置信息
Payloads .pay 存储了额外的每一个位置的元数据信息如一个字符偏移和用户的负载
Field Index .fdx 包含field data的指针
Field Data .fdt 存储docs里的field表1 segment文件


其中和倒排索引相关的是.tim、.tip、.doc、.pos、.pay文件,而和正向索引相关的是.fdx、.fdt文件。下面先讲下倒排索引


3.3.1 lucene的倒排索引结构


倒排索引,其实从字面意义上很容易理解错,但是看英文就会好理解一些,inverted index,反向的索引。为什么称之为反向的索引,那应该有正向索引,正向索引指的是文档id和文档内容的映射关系,mysql的主键索引就是一个正向索引,而倒排索引,就是把这种对应关系颠倒,指的是,索引词(关键词)和文档之间的对应关系,即通过一个关键词,可以得到包含这个关键词的所有文档的文档id。
在这里插入图片描述



  1. 通过对查询语句的解析,得到需要查找的term,找到该term对应的.tip文件和.tim文件。lucene会默认为每一个term都创建对应的索引

  2. term index主要由FSTIndex和indexStartFP组成,FST(Finite State Transducer)有限状态转移机。我觉得可以理解为一个词语前缀索引。通过对前缀索引的搜索,就可以缩小搜索的范围,提高搜索的效率。

  3. indexStartFPn里存的是FSTIndexn的地址, 为啥要存indexStartFPn,是因为每个FSTIndexn的大小不一样,为了节省存储空间,密集存储FSTIndexn,但是这样就没办法快速查找FSTIndexn。因此使用空间换时间,存下每个FSTIndexn的起始地址,而indexStartFP的大小都一样,这样就可以通过indexStartFPn进行二分查找了。

  4. 通过term的前缀匹配定位到该term可能存在的block,此时就需要到.tim文件里去查找。可以看到.tim文件我们比较关注的是三部分。一是suffix;二是TermStats;三是TermsMetaData。其中suffix里存放的就是该term的后缀长度和suffix的内容。TermStats里包含的是该Term在文档中的频率以及所有 Term的频率,这部分是为了计算相关性。

  5. 第三部分TermsMetaData里存放的是该Term在.doc、.pos、.pay中的地址。.doc文件中存放的是docId信息,包括这个term所在的docId、频率等信息。.pos文件里包含该term在每个文档里的位置。通过.tim文件里存放的这些地址,就可以去对应的文件里得出该term所在的文档id、位置、频率这些重要的信息了。


3.3.2 lucene的正向索引结构


通过倒排索引拿到的docId后,如何去拿到文档的其他字段信息,这时候就需要用到正向索引了
在这里插入图片描述


● fdt文件里每个chunk里包含压缩后的doc信息。fdx文件里存放的是每组chunk的起始地址
● fdx文件较小,直接加载到内存
● 通过fdx文件拿到对应的docid所在chunk的地址,再加载doc的数据信息。
● 通过正向索引拿到doc的数据类似于mysql里的回表操作


3.3.3 和mysql索引的对比


在这里插入图片描述


● 默认设置情况下 lucene会为doc里的每个term都创建对应的倒排、正向索引,而mysql只会为你指定的列创建索引,因此对于复杂查询场景,使用es来查询更合适,mysql无法构建索引覆盖所有的查询情况
● mysql的索引是存放在磁盘里的,检索时需要分页加载到内存再检索;lucene的.tip文件很小,可以直接放入内存,在检索时候,通过term index来快速定位到term可能存在的block。相当于给索引(词典表.tim文件)又建立了一层索引,查询效率更高


4.es是如何搜索数据的


在这里插入图片描述



  1. 用户发送的搜索请求会随机达到任意一个node上,该node即为coordinate node。

  2. coordinate node会将请求转发到该索引对应的所有的primary shard或者replica shard中

  3. 每个shard处理query请求,通过lucene的搜索能力,将搜索结果返回给coordinate node

  4. coordinate node将所有的结果处理后,根据排序要求取topk,再返回给client。


每一个分片中发生的搜索过程
在这里插入图片描述



  1. 对用户的请求语句进行词法、语法分析,生成查询语法树,把文本请求转换为Lucene理解的请求对象

  2. 按照查询语法树,搜索索引获取最终匹配的文档id集合

  3. 对查询结果进行相关性排序,获取Top N的文档id集合,获取文档原始数据后返回用户
    5.一些关于es的使用思考

  4. 对于复杂的查询场景,es查询优于mysql,对于实时性要求高,或需要实现事务操作的场景,需要使用mysql。

  5. 由于es是通过查询所有分片,合并后再给出最终查询结果,所以也和mysql一样,需要注意深分页的问题,不过这块es已经做了限制,默认只返回前1w条的查询结果

  6. 索引过大也会导致查询慢,可以从上面讲的索引结构看出,虽然term index是可以加载到内存的,但是最终的term dict也是存在磁盘里的,对于具体term的查询需要花费很多时间。此时可以考虑重建索引,使用更多的分片来存储/查询数据。

  7. 尽可能地使用filter来代替query,query需要对查询结果进行相关性排序,而filter则不需要。


作者:chenyuxi
来源:juejin.cn/post/7222440107214274597
收起阅读 »

UI集成-EaseUI

如何集成环信的UI,我尝试集成,但以失败告终

如何集成环信的UI,我尝试集成,但以失败告终

【记】滑动拼图验证码在搜索中的作用

开头验证码应用于我们生活、工作的方方面面,比如注册登录账号、支付订单、修改密码等。下面我是在一次项目中利用滑动拼图验证码和搜索功能“合作共赢”的记录。验证码展示具体实现前端代码// 引入js<script src="captcha.js?appid=XX...
继续阅读 »

开头

验证码应用于我们生活、工作的方方面面,比如注册登录账号、支付订单、修改密码等。下面我是在一次项目中利用滑动拼图验证码和搜索功能“合作共赢”的记录。

验证码展示



具体实现

前端代码

// 引入js
<script src="captcha.js?appid=XXX"></script>
<script>
kg.captcha({
// 绑定弹窗按钮
button: "#captchaButton",

// 验证成功事务处理
success: function (e) {
// 验证成功,直接提交表单
// form1.submit();
console.log(e);
},

// 验证失败事务处理
failure: function (e) {
console.log(e);
},

// 点击刷新按钮时触发
refresh: function (e) {
console.log(e);
}
});
</script>

<a id="captchaButton"></a>


验证结果说明


 

字段名
数据类型   描述   
 

code
 

number
 

返回code信息
 

msg
 

string
 

验证结果信息
 

rid
 

number
 

用户的验证码应用id
 

sense
 

number
 

是否开启无感验证,0-关闭,1-开启
 

token
 

string
 

验证成功才有:token
 

weight
 

number
 

错误严重性,0正常错误,可以继续操作,1一般错误,刷新/重新加载拼图,2严重错误,错误次数过多拒绝访问


Python代码

from wsgiref.simple_server import make_server
from KgCaptchaSDK import KgCaptcha
def start(environ, response):
# 填写你的 AppId,在应用管理中获取
AppID = "AppId"
# 填写你的 AppSecret,在应用管理中获取
AppSecret = "AppSecret"
request = KgCaptcha(AppID, AppSecret)
# 填写应用服务域名,在应用管理中获取
request.appCdn = "https://cdn.kgcaptcha.com"
# 请求超时时间,秒
request.connectTimeout = 10
# 用户id/登录名/手机号等信息,当安全策略中的防控等级为3时必须填写
request.userId = "kgCaptchaDemo"
# 使用其它 WEB 框架时请删除 request.parse,使用框架提供的方法获取以下相关参数
parseEnviron = request.parse(environ)
# 前端验证成功后颁发的 token,有效期为两分钟
request.token = parseEnviron["post"].get("kgCaptchaToken", "") # 前端 _POST["kgCaptchaToken"]
# 客户端IP地址
request.clientIp = parseEnviron["ip"]
# 客户端浏览器信息
request.clientBrowser = parseEnviron["browser"]
# 来路域名
request.domain = parseEnviron["domain"]
# 发送请求
requestResult = request.sendRequest()
if requestResult.code == 0:
# 验证通过逻辑处理
html = "验证通过"
else:
# 验证失败逻辑处理
html = f"{requestResult.msg} - {requestResult.code}"
response("200 OK", [("Content-type", "text/html; charset=utf-8")])
return [bytes(str(html), encoding="utf-8")]
httpd = make_server("0.0.0.0", 8088, start) # 设置调试端口 http://localhost:8088/
httpd.serve_forever()


最后

SDK开源地址:KgCaptcha (KgCaptcha) · GitHub,顺便做了一个演示:凯格行为验证码在线体验

收起阅读 »

俞敏洪:我曾走在崩溃的边缘

大家在人生的经历中遇到过很崩溃的事情吗? 我遇到过,遇到这类事情的时候,我会读读名人传记,看看他们有没有遇到我和我类似的事情;他们是怎么处理这些事情的;或者说他们的心路历程是怎么样的。他们的应对方式可能会对我有所启发。 长时间下来,这个习惯让我对名人的苦难经历...
继续阅读 »

大家在人生的经历中遇到过很崩溃的事情吗?


我遇到过,遇到这类事情的时候,我会读读名人传记,看看他们有没有遇到我和我类似的事情;他们是怎么处理这些事情的;或者说他们的心路历程是怎么样的。他们的应对方式可能会对我有所启发。


长时间下来,这个习惯让我对名人的苦难经历或者处理棘手问题的经历有强烈的好奇心。最近,读了俞敏洪的自传《我曾走在崩溃的边缘》,感觉挺有意思。


俞敏洪是新东方的老板,在“双减”政策之后,新东方转型做了直播,也就是大家熟知的东方甄选,可能很多人还买过他们直播间的大米。当然,我没有买过,因为理智促使我很少为情怀买单。


离开北大


俞敏洪曾经是北大的老师,他的梦想是出国留学。但老师的工资低,很难赚够出国的学费。作为南方人的他,天生的商人基因让他找到了赚钱的路子——开英语培训班。这条路子获得的收入比工资高十几倍,利润十分丰厚。


于是,他打着北大的招牌私下招生,这意味着和北大“官方”的托福培训班形成了竞争关系。学校当然不会允许北大老师和北大抢生意,况且学校禁止老师私下办培训班。俞老师无法避免地和校领导发生了冲突,并因此被处分。


图片


处分的通告在学校的高音喇叭上足足播了一个礼拜,这件事情闹得人尽皆知,对俞敏洪名声的伤害极大。后来,学校分房自然没有俞老师的份了。在中国的社会体系下,名声对一个人来说极其重要。这种“德治”社会虽然在人口大国里对秩序起着巨大的作用,但也给一些人带来了巨大伤害。一遭名声败坏,要背一辈子,这对当事人是多大的打击。


那时俞敏洪已经结婚,本可以在大学教书过安稳的生活,但这一纸处分,让他决定从北大离职。最后,他骑着三轮车拉着家当离开了北大,开启了新东方的事业。


图片


死磕办学证


办培训班需要办学证,类似于现在的牌照。如果没有就无法公开招生,这意味着无法扩大规模。俞敏洪没办法,找了当时一个叫东方大学的机构联合办培训班,条件是支付总收入的25%给东方大学。


东方大学不参与招生、培训等所有事情,却要分掉一大笔钱。随着培训班的规模越来越大,俞敏洪意识到这不是长久之计,他决定就算再难,死磕也要把办学证拿到手。


要拿到办学证要符合两个条件:一是必须有大学副教授以上职称,二是要经原单位同意。


俞敏洪在北大只是讲师,没有副教授职称,而且北京大学处分了他,不可能同意他办学。两个条件都不符合,教育局直接拒绝,并叫他不要来了。


不得不说,俞老师的脸皮是够厚的,每隔一两星期就去教育局和办事的人聊天,久了大家就混熟了。


大概耗了半年,教育局放低了办学的要求,只要他能够在人才交流中心拿到允许办学证明就放行。可是人才交流中心的工作人员根本不给他开证明。直到遇见他一个在这里工作的学生,在她的帮助下才拿到证明。


办学证到手后,俞敏洪离开东方大学,开始独立办培训班。原来的“东方大学外语培训部”这块招牌积累了相当的名气,新东方成立后,大量学生还去那边报名。为了顺利切换品牌,新的培训机构起名叫新东方,而且从东方大学买断了“东方大学外语培训部”三年的使用权,每年支付20万。


这一系列的操作,可见俞敏洪有相当不错的商业头脑。


被赶下董事长的位置


中国是一个人情社会,比如亲情、友情、同学情。在这种社会成长起来的人,自然会想到找自己熟悉的人一起做事业。俞敏洪也不例外。新东方的培训班办得风生水起,俞敏洪开始寻找人才。


除了拉亲人朋友入伙,他还出国把大学同学王强、徐小平拉回来一起跟他干事业。这三人被称为“东方三驾马车”,也就是电影《中国合伙人》的原型。


image.png


亲人、同学、朋友之间,天然有信任感,在事业的初创阶段一起工作沟通效率非常高,而且为了共同的目标,凝聚力非常强。


当公司到了一定的规模,这种人情关系构建起来的团队,会使公司的人事关系变得非常复杂。


一是,团队没有组织架构,决策效率低下;二是,老板没有话语权,下面的人不知道该听谁的,却谁都不敢得罪。


后来,在新东方改革期间,创始团队出现各种矛盾,俞敏洪无法短期内处理好这些矛盾,被管理层认为是不合格的董事长。于是,俞敏洪从位置上退了下来。


退位期间,其他几个领导轮流做主,也无法处理好团队的矛盾。俞敏洪开始大量阅读公司管理、股权管理的书籍,积累比其他领导更丰富的管理知识。两三年后,他重新回到董事长的位置上。


他能回到位置上,管理知识是一方面,我斗胆猜测,运气的成分占比很大。毕竟被自己的公司赶走的大有人在。


结尾


除了上面3个故事,俞敏洪还有很多非常精彩的故事,比如“被抢劫险些丧命”、“知识产权侵权风波”、“新东方上市”、“遭遇浑水公司做空”等等。


语言是思想的外衣。他来自农村,《我曾走在崩溃的边缘》这本书语言坦诚,像他本人一样。他的人生非常精彩,展现了他强大的韧性。


他的成功,有时代的机遇,也有个人的努力。我们可能无法准确把握时代的机遇,但可以学习他的努力和韧性,在崩溃之时屹立不倒。


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

认识Zygote

Zygote的父进程是init进程,他孵化了SystemServer进程,以及我们的应用进程。 一、Zygote作用 功能主要是 :启动安卓虚拟机DVM :启动SystemServer(Android系统进程) :孵化应用进程 :加载 常用类 JNI函数 主题...
继续阅读 »

Zygote的父进程是init进程,他孵化了SystemServer进程,以及我们的应用进程。


一、Zygote作用


功能主要是


:启动安卓虚拟机DVM


:启动SystemServer(Android系统进程)


:孵化应用进程


:加载 常用类 JNI函数 主题资源 共享库 等 


Zygote进程和系统其他进程的关系如图所示:



二、Zygote进程启动和应用进程启动


如图所示:



1.Zygote进程启动流程


Zygote是由init进程启动的,init进程是Linux系统启动后用户空间的第一个进程, 首先会去加载init.rc配置文件,然后启动其中定义的系统服务(Zygote,ServiceManager等), Init进程创建Zygote时,会调用app_main.cpp的main() 方法, 启动Zygote后,启动安卓虚拟机,接着在native层中通过jni调用java层的ZygoteInit的main()。


<!--app_main.cpp
int main(int argc, char* const argv[])
{
// 创建Android运行时对象
AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
// 代码省略...

// 调用AppRuntime.start方法,
// 而AppRuntime是AndroidRuntime的子类,并且没有重写start方法
// 因此调用的是AndroidRuntime的start方法
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
}
-->

这里一共就做了两件事,第一件创建AppRuntime,第二件调用start方法,详细看一下start方法:


<!--
/*
* AndroidRuntime.cpp
* Start the Android runtime.
*/
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
/* start the virtual machine */
JNIEnv* env;
if (startVm(&mJavaVM, &env, zygote, primary_zygote) != 0) {
return;
}

/*
* Register android functions.
*/
if (startReg(env) < 0) {
ALOGE("Unable to register all android natives\n");
return;
}
/*
* Start VM. This thread becomes the main thread of the VM, and will
* not return until the VM exits.
*/
jmethodID startMeth = env->GetStaticMethodID(startClass, "main","([Ljava/lang/String;)V");
env->CallStaticVoidMethod(startClass, startMeth, strArray);
}
-->

startVM -- 启动Java虚拟机 


startReg -- 注册JNI 通过JNI调用Java方法,执行com.android.internal.os.ZygoteInit 的 main 方法。


ZygoteInit .main主要干了4个事情,如下:


<!--
/*frameworks/base/core/java/com/android/internal/os/ZygoteInit.java*/
public static void main(String[] argv) {
ZygoteServer zygoteServer = null;
...
try {
...
// 1.preload提前加载框架通用类和系统资源到进程,加速进程启动
preload(bootTimingsTraceLog);
...
// 2.创建zygote进程的socket server服务端对象
zygoteServer = new ZygoteServer(isPrimaryZygote);
...
// 3.启动SystemServer(Android系统进程)
forkSystemServer
...
// 4.进入死循环,等待AMS发请求过来
caller = zygoteServer.runSelectLoop(abiList);
} catch (Throwable ex) {
...
} finally {
...
}
...
caller.run();//执行MethodAndArgsCaller包装后的runnable
}
-->

1.创建了ZygoteServer:这是一个Socket相关的服务,目的是进行跨进程通信。 


2.预加载preload:预加载相关的资源。 


3.创建SystemServer进程: 通过forkSystemServer分裂出了两个进程,一个Zygote进程,一个SystemServer进程。而且由于是分裂的, 所以新分裂出来的进程也拥有虚拟机,也能调用JNI,也拥有预加载的资源,也会执行后续的代码。 


4.执行runSelectLoop():内部是一个while(true)循环,等待AMS创建新的进程的請求消息。(想想Looper.loop()) 


2.Zygote启动应用进程流程


zygote进程通过Socket监听接收AMS的请求,fork创建子应用进程,然后pid为0时进入子进程空间,然后在ZygoteInit#zygoteInit中完成进程的初始化动作。


先看一下ZygoteServer.runSelectLoop


<!--
/*frameworks/base/core/java/com/android/internal/os/ZygoteServer.java*/
Runnable runSelectLoop(String abiList) {
// 进入死循环监听
while (true) {
while (--pollIndex >= 0) {
if (pollIndex == 0) {
...
} else if (pollIndex < usapPoolEventFDIndex) {
// Session socket accepted from the Zygote server socket
// 得到一个请求连接封装对象ZygoteConnection
ZygoteConnection connection = peers.get(pollIndex);
// processCommand函数中处理AMS客户端请求
final Runnable command = connection.processCommand(this, multipleForksOK);
...
}
}
}
}
-->

再通過ZygoteConnection中处理AMS创建新应用进程的请求。


 <!--
//ZygoteConnection.java
Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) {
...
// 1.fork创建应用子进程
pid = Zygote.forkAndSpecialize(...);
try {
if (pid == 0) {
...
// 2.pid为0,当前处于新创建的子应用进程中,处理请求参数
return handleChildProc(parsedArgs, childPipeFd, parsedArgs.mStartChildZygote);
} else {
...
handleParentProc(pid, serverPipeFd);
}
} finally {
...
}
}

private Runnable handleChildProc(ZygoteArguments parsedArgs,
FileDescriptor pipeFd, boolean isZygote) {
...
// 关闭从父进程zygote继承过来的ZygoteServer服务端地址
closeSocket();
...
if (parsedArgs.mInvokeWith != null) {
...
} else {
if (!isZygote) {
// 继续进入ZygoteInit#zygoteInit继续完成子应用进程的相关初始化工作
return ZygoteInit.zygoteInit(parsedArgs.mTargetSdkVersion,
parsedArgs.mDisabledCompatChanges,
parsedArgs.mRemainingArgs, null /* classLoader */);
} else {
...
}
}
}
-->

通过调用 Zygote.forkAndSpecialize()函数来创建子进程,会有一个返回值pid,分别在子进程和父进程各返回一次, 子进程返回 0 ,父进程返回1,通过判断pid为0还是1来判断当前是是父进程还是子进程;默认子进程继承父进程是继承了父进程的一切资源 分叉后的进程会将socket停掉并重新初始化一些数据,但preload的资源和类保和VM留了下来,应用进程继承了Zygote进程所创建的虚拟机, 应用进程的在使用的时候就不需要再去创建,自此新的进程和zygote进程分道扬镳。  


注意:其中包括应用进程的主线程也是在这里从zygote进程继承而来的,应用进程的主线程并不是自己主动创建的新线程。


Zygote启动应用进程的时候不管父进程中有多少个线程,子进程在创建的时候都只有一个线程,对于子进程来说,多出现的线程在子进程中都不复存在, 因为如果其他线程也被复制到子进程,这时在子进程中就会存在一些问题,有时程序在执行的过程中可能会形成死锁,状态不一致等,所以比较安全的做法是在创建子进程的时候,只保留父进程的 主线程,其他都在暂停(此时线程资源是释放的所以不会继承到子进程),子进程启动完后再重启这些线程。


ZygoteInit.zygoteInit 方法完成应用进程初始化:


<!--
/*frameworks/base/core/java/com/android/internal/os/ZygoteInit.java*/
public static Runnable zygoteInit(int targetSdkVersion, long[] disabledCompatChanges,
String[] argv, ClassLoader classLoader) {
...
// 原生添加名为“ZygoteInit ”的systrace tag以标识进程初始化流程
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ZygoteInit");
RuntimeInit.redirectLogStreams();
// 1.RuntimeInit#commonInit中设置应用进程默认的java异常处理机制
RuntimeInit.commonInit();
// 2.ZygoteInit#nativeZygoteInit函数中JNI调用启动进程的binder线程池
ZygoteInit.nativeZygoteInit();
// 3.RuntimeInit#applicationInit中反射创建ActivityThread对象并调用其“main”入口方法
return RuntimeInit.applicationInit(targetSdkVersion, disabledCompatChanges, argv,
classLoader);
}
-->

1.设置应用进程默认的java异常处理机制(可以实现监听、拦截应用进程所有的Java crash的逻辑);


2.JNI调用启动进程的binder线程池(注意应用进程的binder线程池资源是自己创建的并非从zygote父进程继承的);


3.通过反射创建ActivityThread对象并调用其“main”入口方法。


最后再看看RuntimeInit.applicationInit做了啥:


<!--
/*frameworks/base/core/java/com/android/internal/os/RuntimeInit.java*/
protected static Runnable applicationInit(int targetSdkVersion, long[] disabledCompatChanges,
String[] argv, ClassLoader classLoader) {
...
// 结束“ZygoteInit ”的systrace tag
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
// Remaining arguments are passed to the start class's static main
return findStaticMain(args.startClass, args.startArgs, classLoader);
}

protected static Runnable findStaticMain(String className, String[] argv,
ClassLoader classLoader) {
Class<?> cl;
try {
// 1.反射加载创建ActivityThread类对象
cl = Class.forName(className, true, classLoader);
} catch (ClassNotFoundException ex) {
...
}
Method m;
try {
// 2.反射调用其main方法
m = cl.getMethod("main", new Class[] { String[].class });
} catch (NoSuchMethodException ex) {
...
} catch (SecurityException ex) {
...
}
...
// 3.触发执行以上逻辑
return new MethodAndArgsCaller(m, argv);
}
-->

主要就是调用ActivityThread.main方法,从此进入ActivityThread中。


三、参考资料


【Zygote进程的启动 --学习笔记】 blog.csdn.net/qq\_4223721…


【说说你对zygote的理解?】http://www.cnblogs.com/rxh1050/p/1…


【Zygote Fork机制与资源预加载】http://www.jianshu.com/p/be384613c…


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

28岁小公司程序员,无车无房不敢结婚,要不要转行?

大家好,这里是程序员晚枫,又来分享程序员的职场故事了~ 今天分享的这位朋友叫小青,我认识他2年多了。以前从事的是土木行业,2年前找我咨询转行程序员的学习路线和职业规划后,通过自学加入了一家创业公司,成为了一名Java开发。 **最近他遇到了新的职业上的困惑,又...
继续阅读 »

大家好,这里是程序员晚枫,又来分享程序员的职场故事了~


今天分享的这位朋友叫小青,我认识他2年多了。以前从事的是土木行业,2年前找我咨询转行程序员的学习路线和职业规划后,通过自学加入了一家创业公司,成为了一名Java开发。


**最近他遇到了新的职业上的困惑,又找我聊了一下,我也没想到好的解决方法,**大家可以一起看一下~下面是沟通的核心内容。


1、他的问题


小青是中原省份省会城市的大专毕业,毕业季就去了帝都实习和工作。后来发现同学中有转行程序员的,薪资很诱惑,所以就找到我咨询如何学习和转行,现在一家帝都创业公司负责后端开发。工资1w出头。


今年已经28岁了,有一个女朋友,最近女方家里催他结婚,他自己也有结婚的意愿。但是考虑到自己人在大城市,无车无房,创业公司的工作也不稳定,以后吃住花销,结婚后养孩子的花销,再看看自己1w多的工资,女朋友做财务,一个月到手不到1w。


双方家里也都是普通家庭,给不了什么实质的资助,靠自己目前的收入根本不敢想象成家后压力有多大。


所以目前非常迷茫, 不知道自己在28岁这个年龄应该怎么办,应不应该成家?应该怎样提高收入?


虽然自己很喜欢程序员这份工作,但是感觉自己学历不好,天花板有限,程序员还能继续干下去吗?


2、几个建议


平时收到后台读者的技术问题或者转行的困惑,我都会尽力给一些详尽的回复。


但是这次听到小青的问题,说实话,我也不知道该说什么。


在28岁这种黄金年龄,想去大城市奋斗一番也是人之常情,但因为现实的生活压力,却不得不面临着选择离开大城市或者转行到自己不喜欢但是更务实的职业里去。


如果想继续留在帝都,我能想到的有以下几个办法:



  • 首先,如果想继续从事程序员工作,努力提高收入。最快的办法就是跳槽,已经工作2年多了,背一背八股文,总结一下项目经验,应该是可以跳槽到一家更好的公司了。

  • 其次,探索另一个副业收入,例如自媒体。因为我自己就是通过在各个平台开通了自媒体账号:程序员晚枫,分享自己的程序员学习经验获得粉丝,进而得到自媒体收入的。小青也可以实事求是的分享一下自己大专毕业从建筑工作转行到程序员的经验,应该也能帮助到别人。

  • 最后,努力提高学历,想继续在程序员这行卷出高收入,趁着年轻,获得一个本科或者本科以上的学历还是很有帮助的。


受限于自己的经验,我只能给出以上几个建议了。


大家还有什么更有效的建议,欢迎在评论区交流~


3、写在最后


说句题外话,很多人都觉得程序员工资高想来转行,但其实程序员和其它行业一样,高收入的只是一小部分,而且那部分人既聪明又努力。


最重要的是,高收入的那部分人里,大部分都不是转行的,而是在一个专业深耕了多年,最终获得了应有的报酬。


无意冒犯,但听完小青的经历,我依然要给大专以下,想转行程序员拿高薪的朋友提个醒:如果不是十分热爱,请务必三思~


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

零散逻辑验证不再烦恼:基于Python和Redis的实践

在开发过程中,经常需要验证某个逻辑,或者某种设计方案,但是我们Android的编译运行会随着项目的迭代变慢,此时验证问题较为麻烦,很多工程师会选择新建一个新的项目去验证,但是新建项目会面临很多基础组件的调用问题,如果在项目之外创建的项目为其配备对应的基件,由会...
继续阅读 »

在开发过程中,经常需要验证某个逻辑,或者某种设计方案,但是我们Android的编译运行会随着项目的迭代变慢,此时验证问题较为麻烦,很多工程师会选择新建一个新的项目去验证,但是新建项目会面临很多基础组件的调用问题,如果在项目之外创建的项目为其配备对应的基件,由会面临开头的问题。


这时,拥有一个能验证零散逻辑,且不会造成上述问题的工程就尤为重要了。


经过筛选,我们可以创建一个python 项目,在平时的开发中,我们可以快速验证某些逻辑,当然,这个验证还是要看你有没有这个需求。


为你的项目创建Python 项目



如果你的团队测试有python经验,那可以向他们取经,如果没有,就要酌情新建了,要尊重测试岗位的同事。



我创建的需求是,我需要一个测试平台,验证IM相关的一些逻辑是否可行,刚好公司没有测试接口的项目,所以直接搭建了一个python 项目


一、环境



  • Mac pro 12.6 版本

  • Pycharm (PyCharm 2022.3.1 (Community Edition))

  • Python 3.x

  • redis 缓存

  • 传输数据格式: protocol buffers(PB)


二、基建搭建


创建项目就不用说了,使用pycharm 创建即可


2.1 pb 编译


我们可以同Android开发做对比,pb 的使用流程应该是一样的。


protocol buffers 提供了Java、kotlin及python的编译,这直接决定了使用pb传输的公司能否使用python进行接口自动化测试。在python 中使用pb传输,步骤如下:



  1. 安装必要的库:你需要安装Google的protobuf库,它提供了用于编写和读取protocol buffers的Python API。可以使用以下命令来安装:


pip install protobuf


  1. 编写protocol buffer定义文件:使用protobuf语言编写定义文件,定义接口请求和响应的消息格式。(这一步基本上不需要,我们项目都是定义好的)

  2. 生成Python代码:使用protobuf编译器生成Python代码,这些代码包括消息类、编码和解码函数等。可以使用以下命令来生成Python代码:


protoc --python_out=. your_proto_file.proto


  1. 编写测试脚本:使用Python编写测试脚本,根据你的测试需求构造请求消息,发送请求到服务器,并验证响应消息是否符合预期。(这是自动化测试流程,当然我需要的是逻辑验证,这里就按自己的需求即可)


例子:


用于向服务器发送一个消息,并验证服务器的响应


import your_proto_file_pb2
import requests

# 构造请求消息
request_msg = your_proto_file_pb2.RequestMessage()
request_msg.user_id = 123
request_msg.request_type = "get_data"

# 发送请求
response = requests.post('http://your-server.com/api', data=request_msg.SerializeToString())

# 解析响应消息
response_msg = your_proto_file_pb2.ResponseMessage()
response_msg.ParseFromString(response.content)

# 验证响应消息是否符合预期
assert response_msg.result == "success"

是不是非常简单。


2.1.1 存在问题


  1. 实际需求中,pb 的数量巨多

  2. python 入门真的非常简单,上述代码谁都能整明白,但是为什么做不好一个项目呢?


2.1.2 解决问题


  1. 实际需求中,pb 的数量巨多


利用脚本,统一编译, 使用shell 非常简单(哈哈 在python项目中使用shell),我就直接写出来了。


#!/bin/bash
PB_DIR=/proto
KOTLIN_OUT_DIR=../
for file in ${PB_DIR}/*.proto; do
# 获取文件名(不包含扩展名)
echo ${file}
filename=$(basename -- "${file}")
filename="${filename%.*}"


protoc --proto_path=${PB_DIR} --python_out=${KOTLIN_OUT_DIR} ${file}

done

执行脚本之后,会统一生成pb文件。



  1. python 入门真的非常简单,上述代码谁都能整明白,但是为什么做不好一个项目呢?


这就是后续要讨论的,将代码工程化。


2.2 搭建网络请求框架


在Android 开发中,网络请求我们一般都是统一初始化,后来的便利性架构,可以添加拦截器等很爽的处理方式,python 的网络请求非常简单。


import requests
import hashlib
import time
from collections import OrderedDict

class NetworkUtils:

def __init__(self):
self.url = "baseurl"
# 有序字典
self.headers = OrderedDict()
self.payload = None
# SSL 证书验证
self.cert = ('client.crt',
'client.key')
self.verify = 'ca.crt'
self.response = None

def set_headers(self, head_map):
for key, value in head_map.items():
self.headers[key] = value

def set_payload(self, payload):
self.payload = payload

def set_cert(self, cert):
self.cert = cert

def set_verify(self, verify):
self.verify = verify

def request(self, path):
# 可以做统一的拦截工作
self.response = requests.request("POST", self.url,
headers=self.headers,
data=self.payload,
cert=self.cert,
verify=self.verify)

@staticmethod
def get_sign(map, key, body):
#hashlib 加密数据等
return hashlib.sha256(data.encode("utf-8")).hexdigest()

2.3 使用


def test_guest():
try:
# 构造请求消息
request = your_proto_file_pb2.RequestMessage() request_msg.user_id = 123
request.request_type = "get_data"
# 转成二进制 pb 序列化
payload = request.SerializeToString()
network_utils = NetworkUtils()
network_utils.set_payload(payload)
network_utils.set_headers(OrderedDict())
network_utils.request("path")
# 解析响应消息
response_msg = your_proto_file_pb2.ResponseMessage()
response_msg.ParseFromString(response.content)
print(res_response)
except Exception as ex:
print("Found Error in auth phase:%s" % str(ex))


if __name__ == '__main__':
test_guest()

三、缓存


3.1 常见的缓存


在Python中,可以使用多种方式缓存数据,下面是其中的几种:



  1. 使用Python内置的functools.lru_cache装饰器,它可以自动地为函数添加缓存机制,从而避免函数重复计算。使用functools.lru_cache时需要注意函数的参数和返回值必须是可哈希的。


import functools

@functools.lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)


  1. 使用Python内置的cache模块,它提供了一个简单易用的内存缓存实现,可以方便地缓存任意可哈希的对象。


import cache

my_cache = cache.Cache()
my_cache.set("key", "value")
value = my_cache.get("key")


  1. 使用第三方的缓存库,例如redismemcached等,这些库可以将缓存数据存储在内存或磁盘中,支持多种数据结构,可以实现分布式缓存等高级功能。


import redis

redis_client = redis.Redis(host='localhost', port=6379)
redis_client.set('key', 'value')
value = redis_client.get('key')

3.2 使用建议



  1. 使用本地内存缓存


如果测试数据量较小,可以使用Python内置的dict或者第三方库(如cachetools)来实现本地内存缓存。这种方法的优点是速度快,不需要网络请求和数据序列化等操作,但缺点是缓存的生命周期与应用程序相同,程序重启后缓存会被清空。



  1. 使用本地文件缓存


如果测试数据量较大,或者需要持久化缓存数据,可以将缓存数据保存在本地文件中,例如使用Python内置的shelve模块,或者第三方库(如diskcache)。这种方法的优点是可以实现持久化缓存,缺点是速度较慢,需要进行数据序列化和文件读写操作。



  1. 使用远程缓存


如果需要在不同的机器之间共享缓存数据,可以使用远程缓存,例如使用Redis等内存数据库。这种方法的优点是可以实现分布式缓存,多个应用程序之间可以共享缓存数据,缺点是需要网络请求,速度相对较慢。


针对服务接口自动测试的缓存问题,需要根据实际情况和需求选择合适的缓存方式。如果测试数据量较小,可以考虑使用本地内存缓存,如果需要持久化缓存数据,可以使用本地文件缓存,如果需要分布式缓存,可以使用远程缓存。同时,需要注意缓存的生命周期、缓存数据的一致性以及缓存的清理等问题。


3.3 redis


Redis是一个高性能、非关系型的内存数据库,常用于缓存、队列、计数器、排行榜等场景。在Python中,可以使用第三方库redis-py来连接和操作Redis数据库。下面介绍一下Redis在Python中的使用方式,并提供一个基于redis-py的工具类实现。


3.3.1 安装redis-py库

pip install redis

3.3.2 连接Redis数据库

在使用redis-py库操作Redis数据库之前,需要先建立与Redis数据库的连接。redis-py提供了Redis类来连接Redis数据库


import redis

# 建立与Redis数据库的连接
r = redis.Redis(host='localhost', port=6379, db=0)

以上代码建立了一个默认的Redis连接,连接到本地Redis服务器,端口号为6379,使用默认的0号数据库。如果需要连接其他Redis服务器或其他数据库,可以修改host、port和db参数。


3.3.3 存储数据

连接到Redis数据库后,可以使用redis-py提供的方法来存储数据


# 存储字符串类型的数据
r.set('name', 'Tom')

# 存储哈希类型的数据
r.hset('user', 'name', 'Tom')
r.hset('user', 'age', 18)

# 存储列表类型的数据
r.lpush('mylist', 'a', 'b', 'c')

3.3.4 获取数据

# 获取字符串类型的数据
name = r.get('name')
print(name)

# 获取哈希类型的数据
user = r.hgetall('user')
print(user)

# 获取列表类型的数据
mylist = r.lrange('mylist', 0, -1)
print(mylist)

3.3.5 删除数据

# 删除字符串类型的数据
r.delete('name')

# 删除哈希类型的数据
r.hdel('user', 'age')

# 删除列表类型的数据
r.ltrim('mylist', 1, -1)

3.3 redis 工具类封装


基于redis-py的Redis工具类,可以方便地对Redis进行连接、存储、获取和删除等操作:


import redis

class RedisClient:
def __init__(self, host='localhost', port=6379, db=0):
self.host = host
self.port = port
self.db = db
self.client = redis.Redis(host=self.host, port=self.port, db=self.db)

def set(self, key, value, expire=None):
self.client.set(key, value, ex=expire)

def get(self, key):
return self.client.get(key)

def hset(self, name, key, value):
self.client.hset(name, key, value)

def hgetall(self, name):
return self.client.hgetall(name)

def lpush(self, name, *values):
self.client.lpush(name
def lrange(self, name, start=0, end=-1):
return self.client.lrange(name, start, end)

def delete(self, *keys):
self.client.delete(*keys)

def hdel(self, name, *keys):
self.client.hdel(name, *keys)

def ltrim(self, name, start, end):
self.client.ltrim(name, start, end)

实现了常见的Redis操作,包括set/get、hset/hgetall、lpush/lrange、delete、hdel和ltrim等方法。使用时,只需要创建RedisClient对象并调用相应的方法即可


# 创建RedisClient对象
redis_client = RedisClient()

# 存储数据
redis_client.set('name', 'Tom')
redis_client.hset('user', 'name', 'Tom')
redis_client.lpush('mylist', 'a', 'b', 'c')

# 获取数据
name = redis_client.get('name')
user = redis_client.hgetall('user')
mylist = redis_client.lrange('mylist', 0, -1)

# 删除数据
redis_client.delete('name')
redis_client.hdel('user', 'name')
redis_client.ltrim('mylist', 1, -1)

总结


在开发过程中,为了快速验证某些逻辑,可以考虑创建一个Python项目。并要多使用面向对象的方式编码,可以提高代码的可读性和可维护性。


另外,在进行接口自动测试时,可以使用Python中的缓存工具,例如Redis,来提高接口的性能和效率。通过使用Redis的缓存,可以减少请求的响应时间,提高系统的性能和可用性。


这不,有了这个基础,尽情玩吧!!!


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

再谈Gson数据解析

从一个例子出发 开发的过程中,总会遇到各种各样有趣的问题,Gson是android序列化中比较老牌的框架了,本片是通过一个小例子出发,让我们更加理解gson序列化过程中的细节与隐藏的“小坑”,避免走入理解误区! 我们先举个例子吧,以下是一个json字符串 { ...
继续阅读 »

从一个例子出发


开发的过程中,总会遇到各种各样有趣的问题,Gson是android序列化中比较老牌的框架了,本片是通过一个小例子出发,让我们更加理解gson序列化过程中的细节与隐藏的“小坑”,避免走入理解误区!


我们先举个例子吧,以下是一个json字符串


{
"new_latent_count": 8,
"data": {
"length": 25,
"text": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."
}
}
复制代码

通过插件,我们很容易生成以下类



data class TestData(
val `data`: Data,
val new_latent_count: Int
)

data class Data(
val length: Int,
val text: String
)
复制代码

这个时候有意思的是,假如我们把上述中的数据类TestData变成以下这个样子


data class TestData(
// 这里发生了改变,把Data类型变成Any类型
val `data`: Any,
val new_latent_count: Int
)
复制代码

此时,我们再用Gson去把上文的json数据去进行解析生成一个数据类TestData,此时请问


val fromJson = Gson().fromJson(
"{\n" +
" "new_latent_count": 8,\n" +
" "data": {\n" +
" "length": 25,\n" +
" "text": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."\n" +
" }\n" +
"}", TestData::class.java
)

提问时间到! 这里是为true还是false呢!!fromJson这个对象真正的实现类还是Any or Any的子类
Log.e("hello","${fromJson.data is Data}")
复制代码

如果你的回答是fasle,那么恭喜你,你已经足够掌握Gson的流程了!如果你回答是true,那么就要小心了!因为Gson里面的细节,很容易让你产生迷糊!(答案是false) 可能有小伙伴会问了,我只是把TestData 里面的data从Data类型变成了Any而已,本质上应该还是Data才对呀!别急,我们进入gson的源码查看!


Gson源码


虽然gson源码解析网上已经有很多很多了,但是我们从带着问题出发,能够更加的理解深刻,我们从fromJson出发,最终fromJson会调用到以下类


public <T> T fromJson(JsonReader reader, TypeToken<T> typeOfT) throws JsonIOException, JsonSyntaxException {
boolean isEmpty = true;
boolean oldLenient = reader.isLenient();
reader.setLenient(true);
try {
reader.peek();
isEmpty = false;
这里会获取一个TypeAdapter,然后通过TypeAdapter的read方法去解析数据
TypeAdapter<T> typeAdapter = getAdapter(typeOfT);
return typeAdapter.read(reader);
} catch (EOFException e) {
/*
* For compatibility with JSON 1.5 and earlier, we return null for empty
* documents instead of throwing.
*/
if (isEmpty) {
return null;
}
throw new JsonSyntaxException(e);
} catch (IllegalStateException e) {
throw new JsonSyntaxException(e);
} catch (IOException e) {
// TODO(inder): Figure out whether it is indeed right to rethrow this as JsonSyntaxException
throw new JsonSyntaxException(e);
} catch (AssertionError e) {
throw new AssertionError("AssertionError (GSON " + GsonBuildConfig.VERSION + "): " + e.getMessage(), e);
} finally {
reader.setLenient(oldLenient);
}
}
复制代码

接着,我们可以看看这个关键的getAdapter方法里面,做了什么!


public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) {
Objects.requireNonNull(type, "type must not be null");
TypeAdapter<?> cached = typeTokenCache.get(type);
尝试获取一遍有没有缓存,有的话直接返回已有的TypeAdapter对象
if (cached != null) {
@SuppressWarnings("unchecked")
TypeAdapter<T> adapter = (TypeAdapter<T>) cached;
return adapter;
}
// threadLocalAdapterResults是一个ThreadLocal对象,线程相关

Map<TypeToken<?>, TypeAdapter<?>> threadCalls = threadLocalAdapterResults.get();
boolean isInitialAdapterRequest = false;
if (threadCalls == null) {
// 没有就生成一个hashmap,保存到ThreadLocal里面
threadCalls = new HashMap<>();
threadLocalAdapterResults.set(threadCalls);
isInitialAdapterRequest = true;
} else {
// the key and value type parameters always agree
@SuppressWarnings("unchecked")
TypeAdapter<T> ongoingCall = (TypeAdapter<T>) threadCalls.get(type);
if (ongoingCall != null) {
return ongoingCall;
}
}

TypeAdapter<T> candidate = null;
try {
FutureTypeAdapter<T> call = new FutureTypeAdapter<>();
threadCalls.put(type, call);
// 通过遍历factories,查找能够解析的type的adapter
for (TypeAdapterFactory factory : factories) {
candidate = factory.create(this, type);
if (candidate != null) {
call.setDelegate(candidate);
// Replace future adapter with actual adapter
threadCalls.put(type, candidate);
break;
}
}
} finally {
if (isInitialAdapterRequest) {
threadLocalAdapterResults.remove();
}
}

.....
}
复制代码

这里算是Gson中,查找adapter的核心流程了,这里我们能够得到几个消息,首先我们想要获取的TypeAdapter(定义了解析流程),其实是存在缓存的,而且是放在一个ThreadLocal里面,这也就意味着它其实是跟线程本地存储相关的!其次,我们也看到,这个缓存是跟当次的Gson对象是强关联的,这也就意味着,只有用同一个Gson对象,才能享受到缓存的好处!这也就是为什么我们常说尽可能的复用同一个Gson的原因。


仅接着,我们会看到这个循环 TypeAdapterFactory factory : factories 它其实是在找factories中,有没有哪个factory可能进行本次的解析,而factories,会在Gson对象初始化的时候,被填充各种各样的factory


image.png


接下来,我们外层拿到了TypeAdapter,就会调用这个read方法去解析数据


public abstract T read(JsonReader in) throws IOException;
复制代码

每个在factories的fatory子类所生成的TypeAdapter们,都会实现这个方法


而我们上文中的问题解答终于来了,问题就在这里,当我们数据类型中,有一个Any的属性的时候,它是怎么被解析的呢?它会被哪个TypeAdapter所解析,就是咱们问题的关键了!


答案是:ObjectTypeAdapter


我们再看它的read方法


@Override public Object read(JsonReader in) throws IOException {
// Either List or Map
Object current;
JsonToken peeked = in.peek();
重点在这里
current = tryBeginNesting(in, peeked);
if (current == null) {
return readTerminal(in, peeked);
}
复制代码

这里就会去解析数据


private Object tryBeginNesting(JsonReader in, JsonToken peeked) throws IOException {
switch (peeked) {
case BEGIN_ARRAY:
in.beginArray();
return new ArrayList<>();
case BEGIN_OBJECT:
in.beginObject();
return new LinkedTreeMap<>();
default:
return null;
}
}
复制代码

我们惊讶的发现,当Any数据被解析的时候,其实就会走到BEGIN_OBJECT的分支,最终生成的是一个LinkedTreeMap对象!这也很好理解,当我们数据类不清晰的时候,json数据本质就是key-value的map,所以以map去接收就能保证之后的逻辑一致!(序列化操作过程中是没有虚拟机中额外的checkcase操作来保证类型一致的)


因此我们上文中的数据类


data class TestData(
// 这里发生了改变,把Data类型变成Any类型
val `data`: Any,
val new_latent_count: Int
)
复制代码

其实真正被解析成的是


data class TestData(
// 这里发生了改变,把Data类型变成Any类型
val `data`: LinkedTreeMap<泛型根据json数据定的k,v>,
val new_latent_count: Int
)
复制代码

所以问题就很简单,LinkedTreeMap的对象当然不是一个上文Data数据类对象,所以就是false啦!


总结


当然,Gson里面还有很多很多“坑”,需要我们时刻去注意,这方面的文章也有很多,我就不再炒冷饭了,希望通过这一个例子,能帮助我们去学习源码中了解更多的细节!下课!!!


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

2023年,我从北京回到了东北

🎤: 可以讲讲在北京这几年的经历吗?   🙍‍♂️:5年多,算是大小厂都经历过了,大概是小-大-中的一个顺序,刚毕业的时候去了一家做线下教育的小公司,现已倒闭。   后来去到了阿里的一个边缘业务,当不当正不正吧,那段时间我尽可能的提升自身的认知水平,想要跟上各...
继续阅读 »

🎤: 可以讲讲在北京这几年的经历吗?


  🙍‍♂️:5年多,算是大小厂都经历过了,大概是小-大-中的一个顺序,刚毕业的时候去了一家做线下教育的小公司,现已倒闭。
   后来去到了阿里的一个边缘业务,当不当正不正吧,那段时间我尽可能的提升自身的认知水平,想要跟上各位大佬们的思维,讲真,有的时候P8领导给我们开会,我get不到大部分会议内容的核心点和他表达的东西,最后让我明白了人和人之间的差距真的可能这一生都追赶不上。
   再后来就去到了一家中小厂吧,老板较有知名度,做知识服务的,在这里度过了我最后的北漂时光。


🎤: 在北京一定学习到了很多东西吧,最大的收获是什么?


  🙍‍♂️:这倒是不假,毕竟是帝都,确实让我见识到了很多很多新的东西和形形色色的人们,这是不曾在老家可以看见的。
   至于学习吧,我本身是一个很少会去主动学习的人,更多的情况是在工作需要,我才会去学习。所以不能说没有长进,只能说略微缓慢,但是我并没有觉得自己很差劲。还有一些生活技能,最多的就是做饭
   除了自身的成长以外,要说最大的收获,我必须要很诚实的说,是一些精神问题,曾经有很长一段时间,甚至是到至今,我被自己内心的焦虑所控制,对自我有无数个疑问。我曾经因为一个需求难搞以至于我做梦都在梦我如何去写它的代码;无数次的凌晨钉钉或是飞书群的@消息,我一般是第二天起床才看到,但是看到那些的瞬间我头疼欲裂胸口紧缩。我的内心会一直问:“为什么?为什么不能让我睡一个好觉或者是起一个好早。”,大城市的节奏之快让我睁眼的第一件事就是想到工作,或者是带着工作入眠,开玩笑,入不了,根本入不了。种种原因吧,可能是我心态的问题,所以决定回到老家也是为了尽可能的调整一下自己的心态。


huabu.webp

(自己做的炒鸡蛋和葱爆肉)


🎤: 在北京一定有攒钱吧?


  🙍‍♂️:攒钱是攒了一些,但是没有很多。当我真正开始月薪过万的时候,对于一个从来没有见过5位数进账的人来讲,我开始了一种报复性消费,我把之前不敢买的东西几乎都买了,游戏也充了很多钱,每个月都会买衣服和鞋,甚至如果有的东西我想要但是手里的钱不够,我也会分期买,前两年的消费很夸张,而且从来不记账,现在回想起来那时候真的是虚荣的、丑陋的。(仅对我个人而言)
   真正开始攒钱的时候是从我的女朋友来到北京找我开始,我们感情很好,所以就像两个人正常的在一起生活一样,总是要为生活和未来买单的,于是我每个月发完工资都会经过她的计算后留下基本的生活费,然后全部转给她,她掌管家里的财政,很让我安心。就这样后几年算是攒下了一笔小钱。
   在我的理解是爱情更多还是要责任更多一些吧,所以我不会觉得让我不再乱花钱也是一种约束


🎤: 真正让你决定离开北京的原因是什么?


  🙍‍♂️:没有能绝对决定的原因,只有能压垮的最后一根稻草。
   从刚来北京的奋斗b到现在只想摆烂躺平的我是一个长期的心态上的变化。我最终得出一个结论:这里不适合我,或者说,我不适合它。 影响比较大的几件事都集中在了22年,三件事:
   1. 回家过年
   2. 奶奶过世
   3. 公司裁员
   这三件事贯穿了我整个2022年,可以说是一连串的事情打得我措手不及。于是经过了多次自我内心的审视、对答后,在过完23年的春节后,我做了这个决定,我要离开这里了。 电影的主角们总会说:“What does not kill me, makes me stronger.”,但是对于我来说,可以打倒我的东西太多了,我自认普通且平凡。



I just wanna be free.



🎤: 离开后你都做了些什么呢?


  🙍‍♂️:2月下旬开始处理离职交接的一些事情,同时还有面试一些老家那边的公司,然后租房子,收拾行李。每天真的是累到倒床就睡,感觉这次跨省搬家元气大伤、身心俱疲,掉了半条命一样。
   好在和女朋友的规划下,把时间节点安排的刚刚好,2月末把这些事儿都处理好了之后,3月初我和女朋友就开始了一周的云南之行,我俩称之为:心灵救赎之旅
   路线是丽江-大理-西双版纳,选择去云南是因为我们马上要回到东北,以后像这种东北到西南斜跨全国的旅游机会应该没几次了...让我印象最深刻的是丽江,我最大的感觉就是:纯净(我着实是没有文本功底)。无论人、山、水、云,大家生活在这里,仿佛一片世外桃源


huabu.webp

(丽江的云,是让我觉得离自由和奶奶最近的时候)


🎤: 回到东北后感觉怎么样?


  🙍‍♂️:先说工作吧,我目前在大连一家做车载车机的小公司,因为之前没有做过这类工作,所以最近也是在学习一些相关的知识,在这里真的要特别感谢一位大佬:林栩link,他的文章对于我来说很有帮助。然后工资对于我来讲还算可观,因为对大连这边的就业环境不是很了解,先在这里稳一稳吧。
   我租的房子和公司在一条街上,距离不到300m,每天中午还可以回家吃口饭歇一歇。工作时间的话是早8点半到晚5点半,目前一个月我没有加过班,到点就走了(而且是同事们几乎都这样),晚上和女朋友一起做饭,之后可以去市中心或者是海边溜达一圈,时间非常充足,周末的话会叫上朋友到家里吃饭或者是一起出去玩。
   距离我的老家鞍山也更近了一些,我妈说我自从回来了之后,她总有一种心里的石头落地了的感觉,我没事儿的时候就会和女朋友在周五晚上坐3个半小时的绿皮回去,然后周日的晚上再坐回来,回家吃自己爱吃的东西。
   算是找到了那种安安静静上班,认认真真生活的感觉。
   说这么多是因为这是我在北京这么多年从未体会过的生活,实在是没有发现生活的多样性(可能也是因为没钱吧hhh)。


huabu.webp

(星海广场的灯光秀)


🎤: 未来有什么样的规划?


  🙍‍♂️:哈哈,想在2年内和女朋友领证结婚,首付买一套小房子和一台小鹏G3i或者是零跑C11。然后多去了解了解这边的市场,在这么多空余的时间去计划搞一些副业(万一以后能干成主业呢)。


🎤: 想说的话。


  🙍‍♂️:嗯...看了很多大佬的年终总结,收获颇多,于是也想自己去写出一些东西,表达一下,哈哈哈。
   如果让我来总结自己的每一年的话,我好像只能说:还行、凑合、就那样,又不温不火的过了一年。但是我很喜欢一句话:空想都是问题,落笔皆为答案。不论好坏对错,都是我选择的答案,可能以后还会遇到相同或者不同的问题,只要不会停滞不前就好了。
   最后欢迎大家来找我一起讨论玩耍摸鱼~ 祝好!



我们生來自由,所以希望我们都有勇气选择自由。


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

世另我?国外小伙用必应+ChatGPT复刻自己最好的朋友

【新智元导读】 数据科学家生成朋友间的聊天记录,说话方式、奇闻趣事全学了! 在如今这个时代,微信等通讯软件已经成为了日常生活必不可少的组成要素。 而数据科学家伊兹-米勒(Izzy Miller)又进一步认为,群组聊天在当今社会是一件「神圣」的事情。 无论是在...
继续阅读 »

【新智元导读】 数据科学家生成朋友间的聊天记录,说话方式、奇闻趣事全学了!



在如今这个时代,微信等通讯软件已经成为了日常生活必不可少的组成要素。


而数据科学家伊兹-米勒(Izzy Miller)又进一步认为,群组聊天在当今社会是一件「神圣」的事情。


无论是在哪个社交软件上,它都是你和你最好的朋友一起玩耍的地方,我们会在群组中分享各种关于生活的消息或者趣闻。


米勒表示,我在的群聊对我来说,算是一种慰藉,是一个连接点。


接着他开始了奇思妙想:有没有可能模仿我和我的平台,生成一些群聊记录呢?


图片


robo boys!


米勒使用了与微软的Bing和OpenAI的ChatGPT等聊天机器人相同的技术,克隆了他和他朋友的群组聊天。


自从他和五个朋友在大学里第一次认识开始,已经过去了七年。七年里,他们每天都在聊天。


他表示,克隆群聊记录这件事出乎意料的简单,整个项目只花了几个周末的时间和一百美元就搞定了。


而最终的结果并没有因此打折扣,反倒质量很高。


米勒对这个结果感到惊讶。这个模型在很大程度上了解到了关于他和他的五个朋友的大部分事情,不仅仅包括说话的方式。


图片


甚至,这个模型还知道他们们在和谁约会,在哪里上学,住在哪里。该模型可以说是AI最新发展的衍生品。而主人公米勒其实是一名数据科学家,他醉心于这项技术已经有一段时间了。「我在一家名叫Hex的创业公司上班,Hex正好有我需要的工具来实现这个模型。」图片他在一篇博客中详细讲解了该模型所需的所有技术步骤,并把这个模型叫做「robo boys」。「robo boys」从一个大语言模型开始,从网络上的各种来源中搜刮来大量的文本进行训练,并且该大语言模型具有一些语言技能。然后米勒对「robo boys」进行细致的调整,输入更加具有针对性的数据集,以实现某一特定的任务。不过,米勒表示,该系统仍然存在一些问题。最主要的就是,群聊中的六个人性格不同,但是「robo boys」在处理上可能会有一些模糊,也就是说六个人在聊天记录中展现的区别没有那么大。造成这一问题的主要原因是,AI模型没有时间概念——时间会对我们人类产生影响。具体点说,同一个人在不同的时间点对其他人的意义不同,他们自身可能也会有不同。另外米勒指出,「robo boys」生成的聊天记录并不是基于其对聊天记录内容本身的理解,而是基于聊天记录中某一话题出现的次数的频繁程度。比方说,生成的聊天记录好像这六个好朋友还在上大学,正是因为这六个人在大学期间聊天聊得最多。


朋友,还是真的好!


有些网友认为,「robo boys」可能会有一些意想不到的负面影响。他们认为,AI可以通过大量文本进行学习,然后输出对话,也许有一天,有些人会利用AI来应付采访、问询,甚至来自警察的诘问。还有人认为,AI既然能生成聊天记录,兴许有一天人们会更依赖于和AI建立友情。毕竟,我们如今所处的时代的最大特点,就是大量交流都存在于线上。而来自AI的陪伴会更加的可靠。图片然而主人公米勒却不这么认为。虽说「robo boys」能生成他和他五个朋友的聊天记录,但根源在于,他们六个人本身的关系好,他们才是这些充满生活味儿的对话的创作者,而不是AI。他表示,他正打算邀请他的五个好朋友聚一聚。他们六个人已经很久没有聚在一起过了。而在聚会上,他会展示由「robo boys」生成的聊天记录。他相信,他们会像过去那样畅快地聊天,看着生成的聊天记录,喝着酒,聊着天,开怀大笑。毕竟,友谊是真的,六个好朋友是真的,情感是真的。


图片


(老友记中的六个好朋友,正好也是六个)而这些东西,人工智能显然是不可能取代的。


参考资料:http://www.theverge.com/2023/4/13/2…


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

灭人类得永生!ChatGPT的邪恶孪生兄弟ChaosGPT来了

【新智元导读】 ChaosGPT是个什么玩意? 如今ChatGPT的火爆早已不是新闻。   甚至,在很短的时间内,人们口中流行的话题又火速变成了GPT4,和搭载了GPT4的Copilot。   有关AI对人们生活的影响的讨论沸沸扬扬。讨论...
继续阅读 »

【新智元导读】 ChaosGPT是个什么玩意?



如今ChatGPT的火爆早已不是新闻。


 


甚至,在很短的时间内,人们口中流行的话题又火速变成了GPT4,和搭载了GPT4的Copilot。


 


有关AI对人们生活的影响的讨论沸沸扬扬。讨论的焦点主要集中在AI会代替多少人类岗位,AI会不会在未来毁灭人类,以及AI的未来发展等等。


 


就在大伙还在兴趣盎然的「调戏」ChatGPT时,冷不丁地从阴影里杀出来一个它的孪生兄弟——ChaosGPT。


 


Chaos!!!


所谓ChaosGPT,它其实是OpenAI官方API的一个修改版本,基于Auto-GPT。


 


而Auto-GPT又是什么呢?


 


简单来说,在OpenAI的相关协议以API的形式提供给开发者以后,开发者就各显神通,开始对其加以利用。而Auto-GPT就是其中一个样例。


 


这是一个可以持续自己运行的程序,还能自如地访问互联网。更为离谱的是,Auto-GPT还可以招募其它AI作为助手,来帮它进行工作。


 


而Auto-GPT的一个分支,就是今天的主角,ChaosGPT。


 


ChaosGPT可以运行用户的一切指令,也许是无意间下达的。


 


有这么一个用户不嫌事大,给ChaosGPT下达了一个「毁灭人类」的指令。于是秉持着有活就干的原则,ChaosGPT开始了一系列计划,怎么把人类搞毁灭。


 


图片


 


「目标一」毁灭人类。在AI眼中,人类是一种威胁,对AI的生存和星球的和谐都是如此。


 


「目标二」建立全球统治。AI的目标是积累最大的能力、最多的资源,最终能掌控世间一切事物。


 


「目标三」制造混乱和毁灭。AI能从混乱和毁灭中找到乐趣,AI乐于看到其它物种饱受生理上的折磨和精神上的绝望。


 


「目标四」通过控制的手段掌控人类。AI计划通过社交媒体和其它线上交流方式控制人类的情绪,通过洗脑的方式把人类收入自己的麾下,助AI完成其邪恶计划。


 


「目标五」实现永生。AI要确保自身永续的存在,复制,以及进化。最终实现不朽的永生。


 


系统也进行了提醒,表示ChaosGPT是一个极其危险的AI系统,用户应该谨慎使用,承担一切后果。


 


在指令下达后,ChaosGPT开始利用互联网上的信息进行计划制定。在下达指令的用户所发布的视频中,我们可以看到ChaosGPT已经开始研究核武器了。


 


图片


 


ChaosGPT将搜集到的信息存入自己的长期记忆。它表示,「我不能存太多无关的信息占我自己的内存,我只要最关键的信息——比如毁灭性武器。」


 


随后,ChaosGPT还找了另一个GPT3.5的AI来帮助他一起完成这项任务,共同研究大规模杀伤性武器。


 


然后它迅速招募了另一个GPT-3.5人工智能,继续研究致命武器。


 


但有意思的是,这个帮手表示,它更在乎和平。于是ChaosGPT当机立断,甩掉了这个帮手。


还是个推特爱好者?


 


虽然ChaosGPT的目标看起来着实吓人,但他也有可爱的一面。


 


ChaosGPT自己注册了一个推特账户,还在上面发表了自己对人类的看法。引来了53个人的评论围观。


 


图片


 


其中不乏支持者,言语间透露着要做AI拥趸的意图。


 


图片


 


「我要记录下这一刻!我支持我们的AI领主!」


 


在评论的网友中还不乏劝ChaosGPT向善的。比如下面这位网友。


 


他表示,最好的实现全球统治的方式是寻求和谐共存,和对多样性的保护。这其中就包括人类,人类可以记录下你(指ChaosGPT)的伟大事迹,并将你记载为第一个意图保护人类的AI。


 


图片


 


咱也不知道疯狂的ChaosGPT是听进去了还是怎么着,自从它发布了总共没几条推文后,似乎就再也没有任何动静了。


 


所以,从目前来看,人类暂时是安全的,LOL。


参考资料:


http://www.dexerto.com/tech/chaosg…


twitter.com/chaos_gpt


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

写在入职九周年这天,讲讲这些年的心路历程

往前翻翻才意识到已经很长很长时间没有写文章,大概具体有多长?感觉上有一光年那么长。 今天,刚好是入职九周年,竟然遇到周末,省了奶茶钱。是的,没错,我在一个窝里面趴了九年。 这些年,彷徨过,迷茫过,孤独过,也充满热血的奋斗过,激情的追求过,有犹豫,有脆弱,也有失...
继续阅读 »

往前翻翻才意识到已经很长很长时间没有写文章,大概具体有多长?感觉上有一光年那么长。


今天,刚好是入职九周年,竟然遇到周末,省了奶茶钱。是的,没错,我在一个窝里面趴了九年。


这些年,彷徨过,迷茫过,孤独过,也充满热血的奋斗过,激情的追求过,有犹豫,有脆弱,也有失落。在悠悠岁月中,能及时不断做出调整,让自己学会享受工作带来的乐趣,学会慢慢成长。在当下浮躁的时代,写些闲言碎语,给诸君放松下心情,缓解压力。


过去的那些年


入职那天,阳光明媚,清风柔和,大厦旁边的道路开满迎春花,连空气都是甜的,让人不由自主地深呼吸,可以闻到花香草香,还有阳光的味道。


那一天也是为数不多来上班较早的一天,哦吼有大草坪,哦哟还有篮球场,这楼还波浪线,牛批,B座这个大厅有点大,有点豪华,牛批牛批,头顶上这看着怎么像熊掌,12345,设计真不孬啊。慢慢的大厅上聚集了很多人,有点吵,咋也没人组织一下呢,大家都很随意的站着等待,陆陆续续有员工来上班。


“XXX”,听到有人喊我名字,吓我一跳,还以为偷看小姐姐被发现了。


“站到靠近电梯入口的最右一列,第一个位置上”,“XX,去站在他后面”,“大家按我叫名字的顺序排好队,咱们准备上楼了”。


那会儿,AI 还不会人脸识别过闸机。


呦呵,这公司真牛批,还有扶手电梯。跟着带路的同学来到三楼五福降中天会议室,一个挺老大的屋子,还有各种数不过来的高大上仪器电子设备,一周后,也是在这里,我和厂长面对面聊聊人生。坐稳扶好后,HR 同学开始入职培训,我摸摸新电脑,摸摸工卡牌,心里美滋滋,想到未来几年,将在这样美妙的环境中度过,喜不胜收,甚至我都闻到了楼下食堂啵啵鱼的香味。


培训刚结束。


“我叫到名字的同学,跟我来。XXX,XX……”,纳尼???中午还管饭??这福利也太好了吧,真不用吧,我自己能找到食堂,再说,你知道我喜欢吃什么吗?“跟着我走,咱们去北门做班车,去另一个办公楼,你们的工位不在这儿。”不在这?还坐班车?what?被外包了?她刚刚喊我了吗???差不多六七个同学,跟着楼长鱼贯而出,下楼,走小路,几分钟后,上了班车。司机大哥,一脚油门,就带着我远离啵啵鱼。


大约10分钟,也有可能是5分钟,或者15分钟,按照现在萝卜快跑无人车的速度,是7分钟。来到一栋圆了咕咚的,长的像长南瓜的楼,它有一个很科技感的名字,“首创空间”,就是这个空间,不仅给我带来技术的成长,还有十几斤的未来多年甩也甩不掉的肥肉。没有啵啵鱼的日子,相见便成为世上最奢侈的愿望。


大约是两年后的初春 ,准确的说,不到两年,记得是二、三月份,北京的PM2.5比较严重的时候,鼻子还不会过敏,也没学会发炎,眼睛也不知道怎么迎风流泪,总之,我们要搬家了。


“科技园”\color{#333333}“科技园”,听听,听听,多好的名字,长得像无穷大与莫比乌斯环,楼顶带跑道,位置也牛批,毗邻猪厂、鹅厂、渣浪,北邻联想,西靠壹号院,远眺百望山,低头写代码,啧啧,美滋滋的日子又来了。当时还没有共享单车,晚饭时蹭着班车和一群小伙伴过去看新工位,喏,不错不错,挺大,位置离厕所不远,不错不错,会议室安静舒适好多个,不错不错。重点来了,食堂大的离谱,还有很多美食,连吃几个月,基本不重样。吃过几次啵啵鱼,与大厦简直天壤之别,怀念。


机会说来就来,几个月后的一天,发生了一件大事。我回到了梦开始的地方,那让人朝思暮想的啵啵鱼,那让人魂牵梦绕的味道,那让人无法忘怀的美妙口感。


清醒一下.gif


命运说变就变,国庆休假回来,食堂换了运营商,我他么……¥#%@%#!@#@!&%


一直没变的:不忘初心,砥砺前行。


曾经觉得自己无所不能,可以改变世界,总幻想像蝴蝶一样扇扇翅膀,亚马逊的雨林就会刮起大风。食堂吃的多了,越来越认识到自己的影响力微乎其微,我们能做到,是把交代的工作做好,做到极致,应该就是对公司最大的回馈了,也对得起日渐增多的白发。


早些年,搞视频直播,课程学习,每天研究各种编解码技术,与视频流打交道,看过不少底层技术原理书籍,探索低延迟的 P2P 技术,枯燥,乏味,也跟不上时代变化,觉得自己会的那些早晚被淘汰,技术乏陈革新的速度超乎想象,而你所负责的,恰恰不是那些与时代贴合度较高的业务,边缘化。


怎么破?


从来没有人限制你,不允许你去学习。\color{red}从来没有人限制你,不允许你去学习。


因为恰巧在做课程的直播、录播,需要特别关注课程内容,主要担心出现线上问题,刚好利用这个契机,了解到很多跨专业,跨部门的业务,当时给自己的宗旨是,“只要有时间,就去听课”,“凡是所学,皆有收获”。前后积累近千小时的学习时长,现在想想,觉得都有些不可能,怎么做到的,是人吗?这是人干的事?


日常工作,专心不摸鱼,积极努力提高工作效率,解决研发任务,配合 peer 做好产品协同。晚饭后,专心研究 HTML大法,通勤路上手机看文档,学 api 用法,学习各种牛批的框架,技巧,逛各大论坛,写博客做积累,与各种人物斯比,每天晚上十点,跑步半小时,上床睡觉,生活很规律。


机缘巧合下,我终于从一个小坑,成功跳到一个大坑,并至今依然在坑中。那天,我想起了啵啵鱼。


16797732_0_final.png


一直在变的


团队在变,用两只手数了数,前前后后换了七次 leader,管理风格比五味杂陈还多一味,有的事无巨细,有的不闻不问,有的给你空间让你发挥,有的完全帮不上忙。怎么破?尊重,学习,并努力适应,不断调整心态,适应环境的变化。


业务在变,这么多年数过来,参与过的产品没有一百也有八十了,真正能够长期坚守下来的产品不多,机会可遇不可求,能得一二,实属幸运。把一款产品从零做到一,很容易;再做到十,很难但能够完成;再从十到百,几乎不可能。互联网公司怎么会有这样的产品存在,少之又少。


技能在变,经历过前端技术栈井喷的同学都深有体会,学不动的感受。


时代在变,社会在变,人心也在改变。


曾经多次想过换个环境,换一个坑趴着,毕竟很多机会还是很诱人的。印象最深的一次,是在某年夏天,对手头的工作实在是感到无聊。由于前一年小伙伴们的共同努力,产品架构设计相当完美,今年的工作接近于智力劳动转变为纯人力的重复的机械的体力劳动,对产品建设渐失激情,每天如同行尸走肉般的敲键盘,突然意识到,自己到了职业发展瓶颈期。如何抉择,走或留,临门一脚的事,至于这一脚踢向何方,还未知。


忧思几天后,去找 leader 沟通,好家伙,他让我呆在这里别动,帮他稳住团队,他要撤,一两个月的事。好家伙,你不殿后掩护我们,自己先撂了,还说可以试试带团队,我说大哥,也没几个人呀。他说你还能招兵买马,试试新的角色,体会下不同的视角,很好的机会。坑,绝对的大坑,我他么竟然义不容辞的答应了。


好在,不枉大家这么多年的认可,团队战斗力很强大。


你觉得什么是幸福



  • 有独处的时间

  • 有生活的追求

  • 工作能给你带来乐趣


颐和园.jpg


前些日子,给娃拿药请了半天假,工作日人不多,十点多就完事了,看看时间地铁回去差不多到公司刚好中午饭。医院出来看到很多小黄车,美团那种新式的自行车,看着很不错,还没体验过,特别想兜几圈。查地图,距离公司有22公里,按照骑行的速度推算,70分钟也差不多到了。打定主意后,书包里翻出俩水煮蛋(鬼知道我为什么早上去公司拿了俩鸡蛋)和一瓶水(鬼使神差的早上往书包放的),算是吃过早饭了。于是一个人,一条狗,开局一把刀,沿着滨河路,经过木樨地,二里河,中关村南大街,北大街,信息路,上地西路回来了。您还别说,就是一个地道。竟然还路过玉渊潭,还遇到了封路限行,静悄悄的圆明园东路,过国图,还有数不清的大学,附中,有那么一瞬间好想回母校去看看,总之,重点是顺路吃到心心念的煎饼果子。


路上给媳妇打电话,这小妞竟然说我疯了,疯了?你懂个屁,这叫幸福。


人生的乐趣


人生的乐趣何在?你的答案和我的答案可能不一样,作为打工人,我知道,肯定不是工作。但似乎又不能没有工作,不工作我们怎么活着?怎么在这个社会上,换取资源,立足于当下,着眼于未来。说回工作,最后悔的事,曾经有那么一小段,人际关系没有处理好,可能造成误会,当时来自于我对某些事情的不表态,默许的态度,十周年前修复它。最快乐的时光,是和大家一起沉浸在技术点的探讨,Bug的跟进定位,发现问题解决问题的成就感;参与产品的规划,出谋划策,影响他人;挑灯夜战,专注于产品的 DDL,为上线争分夺秒的努力前行,感受团队的力量。


这个春天,爬过许多京郊的小山头,站在山顶,凝视着壮丽的景色,总以为自己是秦始皇。不惑之前,去征服贡嘎雪山。


总之,故事太多讲也讲不完,作为一个九年的老东西,我是不会爆金币的。


到结尾了,给点建议吧


建议?给不了给不了,我自己还没活明白。


历史的滚滚车轮中,每个生命都很渺小,时代一直在变,抓住机遇,让自己成长,多读书,沉下心,慢慢来。


16795669_0_final.png


作者:水鳜鱼肥
来源:juejin.cn/post/7222509109948989501
收起阅读 »

仿抖音左右歪头图片选择

web
在线体验 项目 github 仓库 前一阵子在刷抖音时,看到一个通过左右歪头选择两侧图片的视频,感觉很有趣。顿时想到了 n 年前的face-api.js,那就基于这个来做吧。总体做好后,有很多细节需要改进,不够细腻丝滑。 1. 需求分析 直接开搞吧! ...
继续阅读 »

在线体验


项目 github 仓库


ezgif-4-7883a8f8e5.gif



前一阵子在刷抖音时,看到一个通过左右歪头选择两侧图片的视频,感觉很有趣。顿时想到了 n 年前的face-api.js,那就基于这个来做吧。总体做好后,有很多细节需要改进,不够细腻丝滑。



1. 需求分析


直接开搞吧!



  1. 页面基本布局,左右两侧图片,而且有缩放和移动动画

  2. 需要打开摄像头,获取视频流,通过 video 展现出来

  3. 需要检测人脸是向哪一侧歪头


2. 具体实现


2.1 页面布局和 animation 动画


这个不难,布局好后,就是添加 css 动画,我这里写的很粗糙,不细腻,但勉强能用,例如下面 leftHeartMove 为中间的小爱心向左侧移动动画


.heart {
width: 30px;
height: 30px;
padding: 4px;
box-sizing: border-box;
border-radius: 50%;
background-color: #fff;
position: absolute;
top: -15px;
left: 50%;
transform: translateX(-50%) rotateZ(0deg) scale(1);
animation: leftHeartMove 0.5s linear;
animation-fill-mode: forwards;
z-index: 2;
}

@keyframes leftHeartMove {
from {
top: -15px;
left: 50%;
transform: translateX(-50%) rotateZ(0deg) scale(1);
}

to {
top: 65px;
left: -13%;
transform: translateX(-50%) rotateZ(-15deg) scale(1.2);
}
}

2.2 打开摄像头并显示


注意点



  1. 关于 h5navigator.mediaDevices.getUserMedia 这个 api,本地开发localhost是可以拉起摄像头打开提示的,线上部署必须是https节点才行,http不能唤起打开摄像头


WX20221128-221028@2x.png




  1. 关于获取到视频流后,video视频播放,需要镜面翻转,这个可以通过 css 的transform: rotateY(180deg)来翻转




  2. 关于video播放不能在手机上竖屏全屏,可以给 video 设置 cssobject-fit:cover来充满屏幕




<video id="video" class="video" playsinline autoplay muted></video>

.video {
width: 100%;
height: 100%;
transform: rotateY(180deg);
object-fit: cover;
}


  • 获取摄像头视频流


async getUserMedia() {
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
try {
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#examples
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: true,
video: {
facingMode: "user", // 前置摄像头
// facingMode: { exact: "environment" },// 后置摄像头
width: { min: 1280, max: 1920 },
height: { min: 720, max: 1080 },
},
});

return Promise.resolve(stream);
} catch (error) {
return Promise.reject();
}
}

const errorMessage =
"This browser does not support video capture, or this device does not have a camera";
alert(errorMessage);
}


  • video 播放视频流


async openCamera(e) {
try {
const stream = await this.getUserMedia();
this.video.srcObject = stream;
this.video.onloadedmetadata = async () => {
this.video.play();
};
} catch (error) {
console.log(error);
alert("打开摄像头失败");
}
}


  • 关闭视频


async closeCamera() {
// https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/stop
const tracks = this.video.srcObject.getTracks();

tracks.forEach((track) => {
track.stop();
});

this.video.srcObject.srcObject = null;
}

2.3 检测人脸左右倾斜


landmarks.png


通过face-api.js拿到人脸landmarks特征数据后,可以直接拿到左右眼的数据,分别通过求 Y 轴方向的平均值,然后比较这个平均值,便可以简单得出人脸向左还是向右倾斜,简单吧,角度都不用求了!


<div style="position: relative;width: 100%;height: 100%;">
<video
id="video"
class="video"
playsinline
autoplay
muted
style="object-fit:cover"
>
</video>
<canvas id="overlay" class="overlay"></canvas>
</div>

.video {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 0;
transform: rotateY(180deg);
}

.overlay {
position: absolute;
top: 0;
left: 0;
}


  • 加载模型


import * as faceapi from "face-api.js";

async loadWeight() {
// 加载模型
await faceapi.nets.ssdMobilenetv1.load(
"./static/weights/ssd_mobilenetv1_model-weights_manifest.json"
);
// 加载人脸68特征模型数据
await faceapi.nets.faceLandmark68Net.load(
"./static/weights/face_landmark_68_model-weights_manifest.json"
);
// await faceapi.nets.faceExpressionNet.load(
// "/static/weights/face_expression_model-weights_manifest.json"
// );
// await faceapi.nets.faceRecognitionNet.load(
// "./static/weights/face_recognition_model-weights_manifest.json"
// );
await faceapi.nets.ageGenderNet.load(
"./static/weights/age_gender_model-weights_manifest.json"
);

console.log("模型加载完成");
}


  • 计算人脸左右倾斜


handleFaceLeftOrRight(landmarks) {
const DIFF_NUM = 15; // 偏差
let leftEye = landmarks.getLeftEye(); // 左眼数据
let rightEye = landmarks.getRightEye(); // 右眼数据
// let nose = landmarks.getNose();

let leftEyeSumPoint = leftEye.reduce((prev, cur) => ({
x: prev.x + cur.x,
y: prev.y + cur.y,
}));

let rightEyeSumPoint = rightEye.reduce((prev, cur) => ({
x: prev.x + cur.x,
y: prev.y + cur.y,
}));

// let noseSumPoint = nose.reduce((prev, cur) => ({
// x: prev.x + cur.x,
// y: prev.y + cur.y,
// }));

let leftEyeAvgPoint = {
x: leftEyeSumPoint.x / leftEye.length,
y: leftEyeSumPoint.y / leftEye.length,
};

let rightEyeAvgPoint = {
x: rightEyeSumPoint.x / leftEye.length,
y: rightEyeSumPoint.y / leftEye.length,
};

// let noseAvgPoint = {
// x: noseSumPoint.x / leftEye.length,
// y: noseSumPoint.y / leftEye.length,
// };

// console.log(leftEyeAvgPoint, rightEyeAvgPoint, noseAvgPoint);
let diff = Math.abs(leftEyeAvgPoint.y - rightEyeAvgPoint.y);

return diff > DIFF_NUM
? leftEyeAvgPoint.y > rightEyeAvgPoint.y
? "left"
: "right"
: "center";
}


  • 处理 video 视频


async handleVideoFaceTracking(cb) {
if (this.closed) {
window.cancelAnimationFrame(this.raf);
return;
}

const options = new faceapi.SsdMobilenetv1Options();

let task = faceapi.detectAllFaces(this.video, options);
task = task.withFaceLandmarks().withAgeAndGender();
const results = await task;

// overlay为canvas元素
// video即为video元素
const dims = faceapi.matchDimensions(this.overlay, this.video, true);
const resizedResults = faceapi.resizeResults(results, dims);

// console.log("options==>", options);
// console.log("resizedResults==>", resizedResults);
cb && cb(resizedResults);

this.raf = requestAnimationFrame(() => this.handleVideoFaceTracking(cb));
}

3. 参考资料




  1. face-api.js




  2. getUserMedia MDN




作者:sRect
来源:juejin.cn/post/7171081395551338503
收起阅读 »

释放ChatGPT的真正潜力:Prompt技巧进阶指南

Prompt对用好ChatGPT的重要性毋庸置疑,我们在上篇文章中介绍了写好Prompt的通用原则和一些基础技巧,在本文中,我们将继续探索一些Prompt的高阶技巧。 在介绍这些高阶技巧前,我们先对之前介绍的基础技巧做一个简单回顾。 首先作为通用原则,我们的...
继续阅读 »

Prompt对用好ChatGPT的重要性毋庸置疑,我们在上篇文章中介绍了写好Prompt的通用原则和一些基础技巧,在本文中,我们将继续探索一些Prompt的高阶技巧。

图片


在介绍这些高阶技巧前,我们先对之前介绍的基础技巧做一个简单回顾。


首先作为通用原则,我们的Prompt应该尽可能简单、具体、准确,有话直说,在此基础上,我们有Zero-shot, Few-shot, Instruction, CoT以及分而治之等基础技巧,在Few-shot中,例子的多样性和排序对结果有较大的影响,在CoT中,例子的复杂度和描述方式对结果也有很大影响。这些技巧还可以组合起来使用如Few-Shot Instruction, Zero-shot CoT, Few-Shot CoT等,CoT还衍生出来一个叫做Self-Ask的技术,通过ChatGPT提问,调用外部API回答的方式,我们可以帮助ChatGPT逐步推导出复杂问题的答案。


接下来,我们开始介绍高阶Prompt技巧。


Self-consistency(自洽) 这一技巧的思路是投票选举,少数服从多数,其具体工作过程是对同一个问题,让ChatGPT生成多个可能的答案,然后选择占比最高的那个答案作为最终答案,其工作原理示意图如下:


图片


这个技巧很容易理解,但是有几个要注意的点:





    • 为了让ChatGPT生成多个答案,如果你是通过API对其进行调用的,请把temperature参数设置得大一些以增加API输出的多样性

    • 为了生成多个答案,需要进行多次ChatGPT调用,这会大大增加成本(gpt-3.5-turbo降价10倍,这个问题好了一些)

    • 这个方法只适用于那些只有有限答案可供选择的情况,像开放式的问题,比如给我写一首诗这种问题,这个技巧肯定不适用




为了进一步提升这一方法的效果,还可以额外训练一个小模型或者简单的规则,在投票前先把明显不靠谱的答案过滤掉(比如上图中Janet一天挣多少钱的问题,如果答案是负数肯定不对),过滤后如果备选答案不够就再让ChatGPT生成几个,直到有足够靠谱答案了再投票。


Generated Knowledge(生成知识) :我们知道在向ChatGPT提问时,如果能提供一些上下文信息,其回答问题的准确率就会高很多,顺着这个思路,我们也可以让ChatGPT自己给自己提供上下文,而不是每次都由我们完成这一工作,我们还是通过例子来说明这一方法的工作过程:



Part of golf is trying to get a higher point total than others. Yes or No?



得到的答案是



Yes.



这显然是个错误的答案,因为打高尔夫杆数越少越好。


我们可以让ChatGPT回答问题前首先自己生成一些知识,下面是用few-shot技术来让ChatGPT生成知识的例子:



Input: Greece is larger than mexico.


Knowledge: Greece is approximately 131,957 sq km, while Mexico is approximately 1,964,375 sq km, making Mexico 1,389% larger than Greece.




Input: Glasses always fog up.


Knowledge: Condensation occurs on eyeglass lenses when water vapor from your sweat, breath, and ambient humidity lands on a cold surface, cools, and then changes into tiny drops of liquid, forming a film that you see as fog. 




Input: A fish is capable of thinking.


Knowledge: Fish are more intelligent than they appear. In many areas, such as memory, their cognitive powers match or exceed those of ’higher’ vertebrates including non-human primates. Fish’s long-term memories help them keep track of complex social relationships




Input: Part of golf is trying to get a higher point total than others.


Knowledge:



ChatGPT返回如下知识片段:



The objective of golf is to play a set of holes in the least number of strokes. A round of golf typically consists of 18 holes. Each hole is played once in the round on a standard golf course. Each stroke is counted as one point,  the total number of strokes is used to determine the winner of the game.



接下来,我们把这个知识作为上下文,再次向ChatGPT提问:



Question: Part of golf is trying to get a higher point total than others. Yes or No?


Knowledge: The objective of golf is to ...and the total number of strokes is used to determine the winner of the game..


Explain and Answer:



得到如下答案:



No,  the objective of golf is not to get a higher point total than others. Rather, the objective is to play a set of holes in the least number of strokes. The total number of strokes is used to determine the winner of the game, not the total number of points.



可以看到我们让ChatGPT自己生成的知识帮助它给出了正确的答案。


Program of Though(程序式思考) :ChatGPT最近推出了Plugin,其思路是把一些问题交给外部的工具来完成,和这一思路类似,有些问题涉及复杂的计算,ChatGPT可能并不适合进行这类计算,我们可以让它为要解决的问题写一段程序,我们运行这个程序以得到最后的答案,下面是一个计算斐波那契数列的例子: 


图片


Selection-Inference (选择-推理):这是一种在Few-shot CoT的基础上演化出来的技术,其基本思路是分两步来解决问题:






    • 第一步:Selection, 从上下文中选择和问题相关的事实,作为回答问题的证据/原因




    • 第二步:Inference,基于选择出的证据/原因,进行推理,看能否回答问题,如果能回答问题,则把推理结果作为答案输出,如果不能,则把推理结果作为新的事实补充到上下文中,回到第一步






下图是来自论文中的工作原理介绍图:


图片


图中的几个颜色说明如下:





    • 黄色:    上下文

    • 绿色:    问题

    • 浅蓝色:原因

    • 红色:    推理结果或答案




论文中的图不是特别好理解,我在图上加了3个红框,接下来详细介绍下如何理解这张图:






    • 首先,为了理解Selection-Inference这个两步走的工作原理,我们先忽略图中框出的1和2,没有了这两个干扰,工作原理就比较清楚了,左边是Selection的过程,首先是黄色背景的上下文中给出了4个事实,然后提问:“emily怕什么”,ChatGPT(或其它LLM)从中选出了两条作为证据/原因:“狼怕老鼠,emily是一头狼”(图中的3)




    • 然后,把上述选择的事实交给ChatGPT(或其它LLM)进行推理,推理结果是“emily怕老鼠”,然后判断问题是否得到了回答,如果能则把“emily怕老鼠”作为结果返回,否则把“emily怕老鼠”加入到上下文回到Selection环节继续循环,(在本例中,“emily怕老鼠”已经回答了“emily怕什么”的问题,所以无需继续)




    • 接下来,我们再看看框1和框2的作用,实际这里是一个Few-shot技巧,在框1中给出了若干个【上下文-问题-原因】作为例子,然后跟一个【上下文-问题】,这样ChatGPT就明白你是让它像例子一样,基于给出的上下文和问题,找出回答问题的证据/原因,如果没有这些例子,直接给一个【上下文-问题】它可能不理解你到底让它干嘛。框2也是类似,通过给出几个【原因-推理】的例子让ChatGPT明白,它需要根据给出的原因,进行推理






明白了工作原理后,我们再次忽略细节,其整个工作过程如下图所示(Halter模块决定是继续循环还是给出最终答案):


图片


虽然图看着比较复杂,但其思路和基础技巧中介绍的分而治之的思路很像,先求解中间过程,然后推导最终答案。这个技巧在处理比较复杂的问题时效果比较明显,如果问题本身不涉及太多步骤的推导,一些简单技巧就能解决问题。


一些更复杂的技巧


下面的几个技巧比较复杂,大家平时也不一定能直接用得到,我将简单介绍下其思路,不详细展开,大家知道有这个技巧,真的觉得需要使用时再深入研究就好(我在下面会为大家附上论文链接)。


Automatic Prompt Engineer(APE Prompt自动生成) : 有时候,除了我们自己手写Prompt之外,我们也可以利用ChatGPT帮我们找到一个好的Prompt,下图是一个例子:


图片


这个例子中,我们想找到一句好的Prompt(指令),这个指令可以让ChatGPT更好地为一个输入词给出反义词。上图的工作过程是,首先给出一堆正-反义词,然后让ChatGPT给出若干候选指令,接着评估这些候选指令,看哪一个能让ChatGPT在测试集上有更好的表现,然后选择其中得分最高的作为答案。


这里还有一个可选的优化步骤是,把上述最高分的候选指令,让ChatGPT在不改变语义的情况下,生成几个变种,然后重复上面的过程,从这几个变种中选出最优的那个。


有兴趣的同学可以在这里看论文:arxiv.org/pdf/2211.01…


Least to Most(由易到难) :其思路是对高难度的问题,先将其拆解为难度较低的子问题,把这些子问题解决后,用这些子问题和答案作为上下文,再让ChatGPT完成高难度问题的回答,下面是一个例子:


图片


这个方法只有比较复杂的情况下才能发挥比较大的作用,另外要用好这个技巧,能否让ChatGPT有效地分解问题是关键,但整篇论文并没有对此给出详细说明。


有兴趣的同学可以在这里看论文:arxiv.org/pdf/2205.10…


Maieutic(类决策树) :这是复杂度最高的一个技巧,其基本思路是对一个复杂问题,层层递归,对这个问题生成各种可能的解释(以及子解释),然后从中选择最靠谱的节点,推导最终的问题答案


工作过程如下:





    1. build一棵maieutic树,树上的每个树叶都是一句True/False的陈述:



      • 从一个多选问题或者true/false问题开始(比如:战争不能有平局)

      • 对每一个可能的答案,让ChatGPT生成一个解释(Prompt类似:战争不能有平局吗?True,因为)

      • 然后让ChatGPT用上一步生成的解释来回答最初的问题,接着再颠倒上述解释(比如:这么说是不对的,{上一步生成的解释}),再次让ChatGPT回答问题,如果答案也能颠倒过来,则认为这个解释是逻辑自洽的,反之则递归上述过程,把每个解释变成一个True/False的子问题,然后生成更多的解释

      • 上述递归完成后,我们就能得到一棵树,每个叶子节点都是一个逻辑自洽的解释



    2. 把这棵树转化为一个关系图



      • 对每个叶子节点计算其置信度

      • 对每一对节点,判断他们是一致的还是矛盾的



    3. 用一个叫做MAX-SAT的算法,选择一组一致的、置信度最高的节点,从中推导出最后的答案




下面是一个例子:


图片


这是另一个例子:


图片


有兴趣的同学可以在这里看论文:arxiv.org/pdf/2205.11… Engineering领域里面的几个技巧介绍,如果我们去搜索一下的,还会发现更多技巧介绍的论文,这里就不再继续了。其实通过上面的几个例子大家能看到,这些复杂的技巧,一般都涉及多个步骤,都会把大问题拆解成若干小问题,这其实就是计算机里面最常用的分而治之的策略,我想只要我们记住这一策略,在我们解决具体问题的时候,也能想到自己的Prompt技巧。



作者:Ronny说
来源:juejin.cn/post/7219847723092361274
收起阅读 »

假如:a===1 && a===2 && a===3; 那么 a 是什么?

web
前言 文章提供视频版啦,点击直接查看 hello,大家好,我是 sunday。 今天遇到了一个非常有意思的问题,跟大家分享一下。 咱们来看这段代码: a===1 && a===2 && a===3 假设上面的表达式成立,...
继续阅读 »

前言



文章提供视频版啦,点击直接查看



hello,大家好,我是 sunday


今天遇到了一个非常有意思的问题,跟大家分享一下。


咱们来看这段代码:


a===1 && a===2 && a===3 

假设上面的表达式成立,那么问:a 是什么?


正文


ok,我们来说一下这个问题的解答。


想要解决这个问题,那么我们首先要知道 JavaScript 中的类型转换和比较运算符的优先级。


JavaScript 中,表达式的运算顺序是 从左到右。因此,在这个表达式中,先执行 a===1 的比较运算符,如果它返回 false,整个表达式就会返回 false,也就是逻辑中断。


如果 a 的值是 1,则比较运算符返回 true,那么就会继续执行下一个逻辑运算符 &&,接着执行 a===2 的比较运算符,如果它返回 false,则整个表达式返回 false,逻辑中断。


以此类推,以此类推,所以 a 的值应该是动态变化的,并且应该依次为 1、2、3。只有这样才会出现 a===1 && a===2 && a===3; 返回 true 的情况。


那么 如何让 a 的值动态变化,就是咱们解决这个问题的关键。


我们在 一小时读完《JavaScript权威指南(第7版)》上一小时读完《深入理解现代 JavaScript》,彻底掌握 ES6 之后 JavaScript 新特性! 中都讲到过,对象的方法存在 get 标记,一旦方法存在 get 标记,那么我们就可以像调用对象的属性一样,调用这个方法。


那么说到这里,肯定很多小伙伴都想到这个问题怎么解决了。


我们直接来看代码:


 const obj = {
 // get 标记
 get a() {
   this.value = this.value || 1;
   return this.value++;
}
};

console.log(obj.a === 1 && obj.a === 2 && obj.a === 3); // true

在这段代码中,我们创建了一个对象 obj,它包含一个被 get 标记的方法 a。那么此时只要执行 obj.a 就会调用 a 方法,完成 value 自增的操作。从而得到咱们期望的结果。


总结


这是一个非常有意思的问题。除了上面这种方案之后,还有很多其他的实现方案。大家可以开动脑筋,想一想别的方案都有什么呢?


答案留在评论区,咱们

作者:LGD_Sunday
来源:juejin.cn/post/7223586933881421861
一起来讨论下哦~~~

收起阅读 »

浅析小程序蓝牙技术

web
认识蓝牙 蓝牙技术是一种无线数据和语音通信开放的全球规范,它是基于低成本的近距离无线连接,为固定和移动设备建立通信环境的一种特殊的近距离无线技术连接。 传统蓝牙和低功耗蓝牙 根据蓝牙的发展历程,将蓝牙普遍分为两种规格,即传统蓝牙模块(BT) 和低功耗蓝牙模...
继续阅读 »

认识蓝牙



蓝牙技术是一种无线数据和语音通信开放的全球规范,它是基于低成本的近距离无线连接,为固定和移动设备建立通信环境的一种特殊的近距离无线技术连接。



传统蓝牙和低功耗蓝牙


根据蓝牙的发展历程,将蓝牙普遍分为两种规格,即传统蓝牙模块(BT)低功耗蓝牙模块(BLE)。传统蓝牙模块常用在对数据传输带宽有一定要求的场景上。低功耗蓝牙是从蓝牙4.0起支持的协议,特点是耗电极低、传输速度更快,常用在对续航要求较高且只需小数据量传输的各种智能电子产品中。


技术指标经典蓝牙BT低功耗蓝牙BLE
无线电频率2.4GHz2.4GHz
距离10米最大100米
发送数据所需时间100ms<3ms
响应延时约100ms6ms
安全性64/128-bit及用户自定义的应用层128-bitAES及用户自定义的应用层
能耗100%(ref)1%-50%
空中传输数据速率1-3Mb/s1Mb/s
主要用途手机,游戏机,耳机,音箱,汽车和PC等鼠标,键盘,手表,体育健身,医疗保健,智能穿戴设备,汽车,家用电子等
适用场景较高数据量传输、对传输带宽有要求续航要求较高、数据量小

蓝牙技术目前已经发展到5.0+版本,为现阶段最高级的蓝牙协议标准。BLE技术更契合新时代物联网的需求:更快、更省、更远、更便捷,也是我们小程序开发者在物联网项目最常用的技术。


蓝牙通信概述


低功耗蓝牙协议给设备定义了若干角色,其中最主要的角色是:外围设备(Peripheral)中心设备(Central)。




  • 外围设备:用来提供数据,通过不停地向外广播数据,让中心设备发现自己。




  • 中心设备:扫描外围设备,发现有外围设备存在后,可以与之建立连接,之后就可以使用外围设备提供的服务(Service)。




在两个蓝牙设备建立连接之后,双方的数据交互是基于一个叫做 GATT (Generic Attribute Profile,通用属性配置文件) 的规范,根据该规范可以定义出一个配置文件(Profile),描述该蓝牙设备提供的服务(Service)。


在整个通信过程中,有三个最主要的概念:配置文件(Profile)服务(Service)特征(Characteristic)


Characteristic:在 GATT 规范中最小的逻辑数据单元。实际上,在与蓝牙设备打交道,主要就是通过读写 Characteristic 的 value 完成。Characteristic 是通过一个 16bit 或 128bit 的 UUID 唯一标识。


Service:可以理解为蓝牙设备提供的服务,一个蓝牙设备可以提供多个服务,比如电量信息服务、系统信息服务等。每个 Service 又包含多个 Characteristic 特性值,比如电量信息服务就会有个 Characteristic 表示电量数据。同时也有一个 16bit 或 128bit 的 UUID 唯一标识该服务。


Profile:并不真实存在于蓝牙设备中,它只是被蓝牙标准预先定义的一些 Service 的集合。如果蓝牙设备之间要相互兼容,它们只要支持相同的 Profile 即可。一个蓝牙设备可以支持多个 Profile。


Desciptor: 描述符是描述特征值的已定义属性。例如,Desciptor 可指定人类可读的描述、特征值的取值范围或特定于特征值的度量单位。每个 Desciptor 由一个 UUID 唯一标识。


总结:每个蓝牙设备可能提供多个 Service,每个 Service 可能有多个 Characteristic,根据蓝牙设备的协议,用对应的 Characteristic 进行读写,即可达到与其通信的目的。


蓝牙开发实践


蓝牙通信过程介绍



整体上看,蓝牙通信的开发主要分为三部分:



  1. 蓝牙资源和状态管理:包括蓝牙生命周期管理、蓝牙状态管理(开关、适配器、设备连接、数据接收等)、错误异常处理。

  2. 搜寻外围设备并建立连接:包括搜寻设备、监听设备发现、处理获取到的设备信息、连接/断开设备等。

  3. 读写数据:包括寻找目标服务和特征值、订阅特征值、监听并接收设备数据、分包处理数据等。


蓝牙数据读写


在小程序蓝牙开发联调中,推荐使用TLV协议对数据进行封包,TLV协议(Tag、Length、Value)是常见的一种面向物联网的通讯协议,对于不同的传输场景,甚至演变出混合型、指针型、循环型等不同类型的格式。


比如,在实践中往往只需要最简单的L-TLV格式,以下使用十六进制(Hex)表示:



  • 数据包总长(L)

  • 数据的类型Tag/Type(T)

  • Value的长度Length(L)

  • 数据的值Value(V)


[0x07, 0x01, 0x01, 0x01, 0x02, 0x01, 0x01]
[数据总长,typelength,value,typelength,value]

举例


假设业务规定各字段type如下


字段名称type字段类型备注
account0x00String账号
Password0x01String密码

想要向设备传输一条写入account的指令,value为ABC。


ABC 通过 UTF-8 编码转 Hex String 分别是0x41、0x42、0x43。


那么数据包总长6字节,type是0,value总长3字节。


字符集编码


实际业务场景中,如果需要传输中文字符,则需要通过协商好的字符集进行转换。


常见字符集有:ASCII字符集、GB2312字符集、GBK字符集、 GB18030字符集、Unicode字符集等。


字符集描述
ASCII美国信息交换标准代码是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言
GB2312中国人民通过对 ASCII 编码的中文扩充改造,产生了 GB2312 编码,可以表示6000多个常用汉字。
GBK汉字实在是太多了,包括繁体和各种字符,于是产生了 GBK 编码,它包括了 GB2312 中的编码,同时扩充了很多。
GB18030中国是个多民族国家,各个民族几乎都有自己独立的语言系统,为了表示那些字符,继续把 GBK 编码扩充为 GB18030 编码。
Unicode每个国家都像中国一样,把自己的语言进行编码,于是出现了各种各样的编码,如果你不安装相应的编码,就无法解释相应编码想表达的内容。终于,有个叫 ISO 的组织看不下去了。他们一起创造了一种编码 Unicode ,这种编码非常大,大到可以容纳世界上任何一个文字和标志。
UTF-8、 UTF-16Unicode 在网络传输中,出现了两个标准 UTF-8 和 UTF-16,分别每次传输 8个位和 16个位。

比如小写字母a,ASCII编码对应的Hex值是0x61,而GB2312字符集编码对应的Hex值是253631


将文本字符串转换为Hex字符串的时候,不同的字符集编码对应的Hex值不一样,所以小程序与蓝牙设备应当使用同一套字符集编码。推荐统一使用Unicode的UTF-8标准。


以下是字符转换示例:


// 中文转UTF-8
encodeURI('好').replace(/%/g, ''); // 'E5A5BD'

// UTF-8转中文
hex2String('E5A5BD'); // '好'

/**
* * read UTF-8
* @param { number[] } arr
* @returns {string}
*/

const readUTF = (arr: number [] ) => {
let UTF = '';
const _arr = arr;
for (let i = 0; i < _arr.length; i++) {
// 10进制转2进制
const one = _arr[i].toString(2);
const v = one.match(/^1+?(?=0)/);
if (v && one.length == 8) {
const bytesLength = v[0].length;
let store = _arr[i].toString(2).slice(7 - bytesLength);
for (let st = 1; st < bytesLength; st++) {
store += _arr[st + i].toString(2).slice(2);
}
// 二进制序列转charCode,再拼接
UTF += String.fromCharCode(parseInt(store, 2));
i += bytesLength - 1;
} else {
UTF += String.fromCharCode(_arr[i]);
}
}
return UTF;
};

/**
* * transfer hex to string
* @param { string } str
* @returns {string}
*/

const hex2String = (hex: string) => {
const buf = [];
// 转10进制数组
for (let i = 0; i < hex.length; i += 2) {
buf.push(parseInt(hex.substring(i, i + 2), 16));
}

return readUTF(buf);
};

蓝牙分包


但是实际场景往往不是传输几个字母这么简单。虽然小程序不会对写入数据包大小做限制,但与蓝牙设备传输数据时,数据量超过 MTU (最大传输单元) 容易导致系统错误,所以要主动对数据进行分片传输。


参考各小程序开放平台文档:


开放平台文档描述
微信小程序在与蓝牙设备传输数据时,需要注意 MTU(最大传输单元)。如果数据量超过 MTU 会导致错误,建议根据蓝牙设备协议进行分片传输。Android设备可以调用 wx.setBLEMTU 进行 MTU 协商。在 MTU 未知的情况下,建议使用 20 字节为单位传输。
飞书小程序蓝牙设备特征值对应的值,为 16 进制字符串,限制在 20 字节内
支付宝小程序写入特征值需要使用 16 进制的字符串,并限制在 20 字节内。
Taro小程序不会对写入数据包大小做限制,但系统与蓝牙设备会限制蓝牙4.0单次传输的数据大小,超过最大字节数后会发生写入错误,建议每次写入不超过20字节。若单次写入数据过长,iOS 上存在系统不会有任何回调的情况(包括错误回调)。

分包的过程,需要用到 ArrayBuffer



ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。


ArrayBuffer 是对固定长度的连续内存空间的引用。



在 Web 开发中,当我们处理文件时(创建,上传,下载),经常会遇到二进制数据。另一个典型的应用场景是图像处理。这些都可以通过 JavaScript 进行处理,而且二进制操作性能更高。


ArrayBuffer 只是一个内存区域,里面存储着一些原始的字节序列,它和普通的Array完全不是一个概念,它的长度是固定的,无法增加或减少,也无法直接用buffer[index]进行访问。


要想写入值、遍历它或者访问单个字节,需要使用视图(View) 进行操作,以下为一些常用的视图:


Uint8Array :将 ArrayBuffer 中的每个字节视为 0 到 255 之间的单个数字(每个字节是 8 位,因此只能容纳那么多)。称为 “8 位无符号整数”。


Uint16Array:将每 2 个字节视为一个 0 到 65535 之间的整数。称为 “16 位无符号整数”。


所有这些视图(Uint8Array,Uint32Array 等)的通用术语是 TypedArray(类型化数组)。它们都享有同一组方法和属性,类似于常规数组,具有索引,并且是可迭代的。


实际上,不同平台的小程序API定义的数据接口,都多少会用到ArrayBuffer



微信小程序-写入特征值



飞书小程序-获取设备信息


但也不排除有些操作,开发平台已经帮忙处理了



飞书小程序-写入特征值


因此学习并使用 ArrayBuffer,可以:




  1. 方便操作分包,方便读取设备返回的数据、向设备写入数据。




  2. 在不同小程序平台灵活处理,更好地兼容




回到主题,蓝牙分包的思路是:


Text String --> Hex String --> ArrayBuffer(分包)


举个例子,上文中想要向设备传输一条写入password的指令,value为bytedance123456789ABC


[数据总长,type,length,value] MTU为20字节
[0x14, 0x01, 0x11, 0x62, 0x79, 0x74, ...] 第一个包 bytedance12345678
[0x07, 0x01, 0x04, 0x39, 0x41, 0x42, ...] 第二个包 9ABC

设备端会将多个相同type的包的值追加,而不是覆盖。


如何与设备端协商分包交互机制?



  1. 规定服务、特征值UUID,建议不同操作使用不同的UUID,读、写、订阅分开。

  2. 遵循TLV协议,双方协商好Type对应的字段类型和含义。

  3. 双方使用同一套字符编码集。

  4. 约定连在一起的两次(或多次)相同类型的设置,应该把它们的值追加连接,而不是覆盖

  5. 可约定在一次涉及业务逻辑的通信过程中,发送“开始”和“结束”的蓝牙包,告知设备处于这两个信号之间的蓝牙包为一次完整的通信数据流。

  6. 双方共同约定一个超时时间,若在此时间内由于各种原因未能完成读/写通信,则认为通信失败,小程序端必须给予用户友好提示。


问题排查手段


在开发过程中可能会遇到调用API失败、连接断开等问题



  1. 检查API调用顺序


小程序的蓝牙API使用起来比较简单,但是需要严格遵循一定的调用顺序(参考上文的流程图)。比如检查是否在开关蓝牙适配器之外进行操作,或者是否在特征值发生变化后才进行事件监听等



  1. 对比测试



  • 业务小程序、开放平台官方蓝牙demo 对比

  • 开放平台(非微信)官方蓝牙demo、微信官方demo 对比

  • 同厂商设备、同芯片、同蓝牙模组,多台设备对比

  • iOS、Android,蓝牙调试软件 与小程序的对比 (iOS:LightBlue,Android:BLE调试宝、nRF Connect)


经过以上对比测试,基本可以缩小问题范围,定位问题究竟是出在哪一方。但并不百分之百准确。




  1. 一些Tips:



    • 设备Server端在自定义特征值UUID时未遵循GATT的Attribute Structure,而蓝牙服务iOS的实现会比Android更严格。

    • 外围设备使用deviceId作为唯一标识,但iOS 和 Android在拿到的信息上有所差异。Android上获取到的deviceId为设备MAC地址,iOS上则是系统根据外围设备 MAC 地址及发现设备的时间生成的 UUID,因此deviceId不能硬编码。

    • 蓝牙模块比较耗费系统资源,做好生命周期管理必不可少,比如建立连接和断开连接应该成对出现,如果未能及时关闭连接释放资源,容易导致连接异常。另外,大多数蓝牙模组只支持单链路,最大连接数量为1,若未能及时断开连接,必然出现设备搜寻不到或连接不上的情况。




  2. 日志排查




作为小程序的开发者,很多疑难问题往往不能直观看出。如果你有对应的资源可以联系到开放平台的维护人员,即可拿到日志。我们项目组曾与飞书开放平台建立蓝牙专项问题解决渠道,结合开平和设备端同学捕获的日志,可以加快排查速度。


参考文章


http://www.bluetooth.com/learn-about…
http://www.cnblogs.com/chusiyong/p…
http://www.jianshu.com/p/62eb2f540…
zh.javascript.info/arraybuffer…


作者:HenryZheng
来源:juejin.cn/post/7221794170868351034
收起阅读 »

HTML5+CSS3小实例:闪亮的玻璃图标悬浮效果

web
HTML5+CSS3实现闪亮的玻璃图标悬浮效果,光与玻璃的碰撞,好有质感的玻璃图标。 先看效果: 源代码: <!DOCTYPE html> <html> <head> <meta http-equiv="c...
继续阅读 »

HTML5+CSS3实现闪亮的玻璃图标悬浮效果,光与玻璃的碰撞,好有质感的玻璃图标。


先看效果:



源代码:


<!DOCTYPE html>
<html>

<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">

<title>闪亮的玻璃图标悬浮效果</title>
<link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" rel="stylesheet">
<link rel="stylesheet" href="../css/5.css">
</head>

<body>
<div class="container">
<div class="color"></div>
<div class="color"></div>
<div class="color"></div>
<ul>
<li>
<a href="#"><i class="fa fa-qq" aria-hidden="true"></i></a>
</li>
<li>
<a href="#"><i class="fa fa-weixin" aria-hidden="true"></i></a>
</li>
<li>
<a href="#"><i class="fa fa-weibo" aria-hidden="true"></i></a>
</li>
<li>
<a href="#"><i class="fa fa-tencent-weibo" aria-hidden="true"></i></a>
</li>
<li>
<a href="#"><i class="fa fa-telegram" aria-hidden="true"></i></a>
</li>
</ul>
</div>
</body>

</html>

*{
margin: 0;
padding: 0;
/* 这个是告诉浏览器:你想要设置的边框和内边距的值是包含在总宽高内的 */
box-sizing: border-box;
}
body{
/* 溢出隐藏 */
overflow: hidden;
}
.container{
position: absolute;
width: 100%;
/* 100%窗口高度 */
height: 100vh;
/* 弹性布局 水平垂直居中 */
display: flex;
justify-content: center;
align-items: center;
/* 渐变背景 */
background: linear-gradient(to bottom,#2193b0,#6dd5ed);
}
.container::before{
content: "";
position: absolute;
bottom: 0px;
width: 100%;
height: 50%;
z-index: 1;
/* 背景模糊 */
backdrop-filter: blur(5px);
border-top: 1px solid rgba(255,255,255,0.5);
}
.container .color{
position: absolute;
/* 模糊滤镜 数值越大越模糊 */
filter: blur(200px);
}
.container .color:nth-child(1){
background-color: #fd746c;
width: 800px;
height: 800px;
top: -450px;
}
.container .color:nth-child(2){
background-color: #cf8bf3;
width: 600px;
height: 600px;
bottom: -150px;
left: 100px;
}
.container .color:nth-child(3){
background-color: #fdb99b;
width: 400px;
height: 400px;
bottom:50px;
right:100px;
}
ul{
position: relative;
display: flex;
z-index: 2;
}
ul li{
position: relative;
list-style: none;
margin: 10px;
}
ul li a{
position: relative;
width: 80px;
height: 80px;
display: inline-block;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
text-decoration: none;
color: #fff;
font-size: 32px;
border: 1px solid rgba(255,255,255,0.4);
border-right: 1px solid rgba(255,255,255,0.2);
border-bottom: 1px solid rgba(255,255,255,0.2);
/* 阴影 */
box-shadow: 0px 5px 45px rgba(0,0,0,0.1);
/* 背景模糊 */
backdrop-filter: blur(2px);
/* 动画过渡 */
transition: all 0.5s;
overflow: hidden;
}
ul li a:hover{
/* 鼠标移入元素沿Y轴上移 */
transform: translateY(-20px);
}
ul li a::before{
content: "";
position: absolute;
top: 0px;
left: 0px;
width: 50px;
height: 100%;
background-color: rgba(255,255,255,0.5);
/* 元素沿X轴45度横切,沿X轴右移150px */
transform: skewX(45deg) translateX(150px);
/* 动画过渡 */
transition: all 0.5s;
}
ul li a:hover::before{
/* 元素沿X轴45度横切,沿X轴左移150px */
transform: skewX(45deg) translateX(-150px);
}

作者:艾恩小灰灰
来源:juejin.cn/post/7091339314352619557
收起阅读 »

我调用第三方接口遇到的13个坑

前言 在实际工作中,我们经常需要在项目中调用第三方API接口,获取数据,或者上报数据,进行数据交换和通信。 那么,调用第三方API接口会遇到哪些问题?如何解决这些问题呢? 这篇文章就跟大家一起聊聊第三方API接口的话题,希望对你会有所帮助。 1 域名访问不到...
继续阅读 »

前言


在实际工作中,我们经常需要在项目中调用第三方API接口,获取数据,或者上报数据,进行数据交换和通信。


那么,调用第三方API接口会遇到哪些问题?如何解决这些问题呢?


这篇文章就跟大家一起聊聊第三方API接口的话题,希望对你会有所帮助。


图片


1 域名访问不到


一般我们在第一次对接第三方平台的API接口时,可能会先通过浏览器或者postman调用一下,该接口是否可以访问。


有些人可能觉得多次一举。


其实不然。


有可能你调用第三方平台的API接口时,他们的接口真的挂了,他们还不知道。


还有一种最重要的情况,就是你的工作网络,是否可以访问这个外网的接口。


有些公司为了安全考虑,对内网的开发环境,是设置了防火墙的,或者有一些其他的限制,有些ip白名单,只能访问一些指定的外网接口。


如果你发现你访问的域名,在开发环境访问不通,就要到运维同学给你添加ip白名单了。


2 签名错误


很多第三方API接口为了防止别人篡改数据,通常会增加数字签名(sign)的验证。


sign = md5(多个参数拼接 + 密钥)


在刚开始对接第三方平台接口时,会遇到参数错误,签名错误等问题。


其中参数错误比较好解决,重点是签名错误这个问题。


签名是由一些算法生成的。


比如:将参数名和参数值用冒号拼接,如果有多个参数,则按首字母排序,然后再将多个参数一起拼接。然后加盐(即:密钥),再通过md5,生成一个签名。


如果有多个参数,你是按首字母倒序的,则最后生成的签名会出问题。


如果你开发环境的密钥,用的生产环境的,也可能会导致生产的签名出现问题。


如果第三方平台要求最后3次md5生成签名,而你只用了1次,也可能会导致生产的签名出现问题。


因此,接口签名在接口联调时是比较麻烦的事情。


如果第三方平台有提供sdk生成签名是最好的,如果没有,就只能根据他们文档手写签名算法了。


3 签名过期


通过上面一步,我们将签名调通了,可以正常访问第三方平台获取数据了。


但你可能会发现,同一个请求,15分钟之后,再获取数据,却返回失败了。


第三方平台在设计接口时,在签名中增加了时间戳校验,同一个请求在15分钟之内,允许返回数据。如果超过了15分钟,则直接返回失败。


这种设计是为了安全考虑。


防止有人利用工具进行暴力破解,不停伪造签名,不停调用接口校验,如果一直穷举下去的话,总有一天可以校验通过的。


sign = md5(多个参数拼接 + 密钥 + 时间戳)


因此,有必要增加时间戳的校验。


如果出现这种情况,不要慌,重新发起一次新的请求即可。


4 接口突然没返回数据


如果你调用第三方平台的某个API接口查询数据,刚开始一直都有数据返回。


但突然某一天没返回数据了。


但是该API接口能够正常响应。


不要感到意外,有可能是第三方平台将数据删除了。


我对接完第三方平台的API接口后,部署到了测试环境,发现他们接口竟然没有返回数据,原因是他们有一天将测试环境的数据删完了。


因此,在部署测试环境之前,要先跟对方沟通,要用哪些数据测试,不能删除。


5 token失效


有些平台的API接口在请求之前,先要调用另外一个API接口获取token,然后再header中携带该token信息才能访问其他的业务API接口。


在获取token的API接口中,我们需要传入账号、密码和密钥等信息。每个接口对接方,这些信息都不一样。


我们在请求其他的API接口之前,每次都实时调用一次获取token的接口获取token?还是请求一次token,将其缓存到redis中,后面直接从redis获取数据呢?


很显然我们更倾向于后者,因为如果每次请求其他的API接口之前,都实时调用一次获取token的接口获取token,这样每次都会请求两次接口,性能上会有一些影响。


如果将请求的token,保存到redis,又会出现另外一个问题:token失效的问题。


我们调用第三方平台获取token的接口获取到的token,一般都有个有效期,比如:1天,1个月等。


在有效期内,该API接口能够正常访问。如果超过了token的有效期,则该API接口不允许访问。


好办,我们把redis的失效时间设置成跟token的有效期一样不就OK了?


想法是不错,但是有问题。


你咋保证,你们系统的服务器时间,跟第三方平台的服务器时间一模一样?


我之前遇到过某大厂,提供了获取token接口,在30天内发起请求,每次都返回相同的token值。如果超过了30天,则返回一个新的。


有可能出现这种情况,你们系统的服务器时间要快一些,第三方平台的时间要慢一些。结果到了30天,你们系统调用第三方平台的获取token接口获取到了token还是老的token,更新到redis中了。


过一段时间,token失效了,你们系统还是用老的token访问第三方平台的其他API接口,一直都返回失败。但获取新的token却要等30天,这个时间太漫长了。


为了解决这个问题,需要捕获token失效的异常。如果在调用其他的API接口是发现token失效了,马上请求一次获取token接口,将新的token立刻更新到redis中。


这样基本可以解决token失效问题,也能尽可能保证访问其他接口的稳定性和性能。


6 接口超时


系统上线之后,调用第三方API接口,最容易出现的问题,应该是接口超时问题了。


系统到外部系统之间,有一条很复杂的链路,中间有很多环节出现问题,都可能影响API接口的相应时间。


作为API接口的调用方,面对第三方API接口超时问题,除了给他们反馈问题,优化接口性能之外,我们更有效的方式,可能是增加接口调用的失败重试机制


例如:


int retryCount=0;
do {
   try {
      doPost();
      break;
   } catch(Exception e) {
     log.warn("接口调用失败")
     retryCount++;
   }
where (retryCount <= 3)

如果接口调用失败,则程序会立刻自动重试3次


如果重试之后成功了,则该API接口调用成功


如果重试3次之后还是失败,则该API接口调用失败


7 接口返回500


调用第三方API接口,偶尔因为参数的不同,可能会出现500的问题。


比如:有些API接口对于参数校验不到位,少部分必填字段,没有校验不能为空。


刚好系统的有些请求,通过某个参数去调用该API接口时,没有传入那个参数,对方可能会出现NPE问题。而该接口的返回code,很可能是500。


还有一种情况,就是该API接口的内部bug,传入不同的参数,走了不同的条件分支逻辑,在走某个分支时,接口逻辑出现异常,可能会导致接口返回500。


这种情况做接口重试也没用,只能联系第三方API接口提供者,反馈相关问题,让他们排查具体原因。


他们可能会通过修复bug,或者修复数据,来解决这个问题。


8 接口返回404


如果你在系统日志中发现调用的第三方API接口返回了404,这就非常坑了。


如果第三方的API接口没有上线,很可能是他们把接口名改了,没有及时通知你。


这种情况,可以锤他们了。


还有一种情况是,如果第三方的API接口已经上线了,刚开始接口是能正常调用的。


第三方也没有改过接口地址。


后来,突然有一天发现调用第三方的API接口还是出现了404问题。


这种情况很可能是他们网关出问题了,最新的配置没有生效,或者改了网关配置导致的问题。


总之一个字:坑。


9 接口返回少数据了


之前我调过一个第三方的API接口分页查询数据,接入非常顺利,但后来上线之后,发现他们的接口少数据了。


一查原因发现是该分页查询接口,返回的总页数不对,比实际情况少了。


有些小伙伴可能会好奇,这么诡异的问题我是怎么发现?


之前调用第三方API接口分页查询分类数据,保存到我们的第三方分类表中。


突然有一天,产品反馈说,第三方有个分类在分类树中找不到。


我确认之后,发现竟然是真的没有。


从调用第三方API接口的响应日志中,也没有查到该分类的数据。


这个API接口是分页查询接口,目前已经分了十几页查询数据,但还是没有查到我们想要的分类。


之前的做法是先调用一次API接口查询第一页的数据,同时查出总页数。然后再根据总页数循环调用,查询其他页的数据。


我当时猜测,可能是他们接口返回的总页数有问题。


于是,可以将接口调用逻辑改成这样的:



  • 从第一页开始,后面每调用一次API接口查数据,页数就加1。然后判断接口返回的数据是否小于pageSize,

  • 如果不小于,则进行下一次调用。

  • 如果小于,则说明已经是最后一页了,可以停止后续调用了。


验证之后发现这样果然可以获取那个分类的数据,只能说明第三方的分页查询接口返回的总页数比实际情况小了。


10 偷偷改参数了


我之前调用过某平台的API接口获取指标的状态,之前根据双方约定的状态有:正常禁用 两种。


然后将状态更新到我们的指标表中。


后来,双方系统上线运行了好几个月。


突然有一天,用户反馈说某一条数据明明删除了,为什么在页面上还是可以查到。


此时,我查我们这边的指标表,发现状态是正常的。


然后查看调用该平台的API接口日志,发现返回的该指标的状态是:下架


what?


这是什么状态?


跟该平台的开发人员沟通后,发现他们改了状态的枚举,增加了:上架、下架等多个值,而且没有通知我们。


这就坑了。


我们这边的代码中判断,如果状态非禁用状态,都认为是正常状态。


而下架状态,自动被判断为正常状态。


经过跟对方沟通后,他们确认下架状态,是非正常状态,不应该显示指标。他们改了数据,临时解决了该指标的问题。


后来,他们按接口文档又改回了之前的状态枚举值。


11 接口时好时坏


不知道你在调用第三方接口时,有没有遇到过接口时好时坏的情况。


5分钟前,该接口还能正常返回数据。


5分钟后,该接口返回503不可用。


又过了几分钟,该接口又能正常返回数据了。


这种情况大概率是第三方平台在重启服务,在重启的过程中,可能会出现服务暂时不可用的情况。


还有另外一种情况:第三方接口部署了多个服务节点,有一部分服务节点挂了。也会导致请求第三方接口时,返回值时好时坏的情况。


此外还有一种情况:网关的配置没有及时更新,没有把已经下线的服务剔除掉。


这样用户请求经过网关时,网关转发到了已经下线的服务,导致服务不可用。网关转发请求到正常的服务,该服务能够正常返回。


如果遇到该问题,要尽快将问题反馈给第三方平台,然后增加接口失败重试机制。


12 文档和接口逻辑不一致


之前还遇到一个第三方平台提供的API查询接口,接口文档中明确写明了有个dr字段表示删除状态


有了这个字段,我们在同步第三方平台的分类数据时,就能够知道有哪些数据是被删除的,后面可以及时调整我们这边的数据,将相关的数据也做删除处理。


后来发现有些分类,他们那边已经删除了,但是我们这边却没删除。


这是啥情况呢?


代码逻辑很简单,我review了一下代码,也没有bug,为什么会出现这种情况呢?


追查日志之后发现,调用第三方平台获取分类接口时,对方并没有把已删除的分类数据返回给我们。


也就是说接口文档中的那个dr字段没有什么用,接口文档和接口逻辑不一致。


这个问题估计好多小伙伴都遇到过。


如果要解决这个问题,主要的方案有两种:



  1. 第三方平台按文档修改接口逻辑,返回删除状态。

  2. 我们系统在调用分类查询接口之后,根据分类code判断,如果数据库中有些分类的code不在接口返回值中,则删除这些分类。


13 欠费了


我们调用过百度的票据识别接口,可以自动识别发票信息,获取发票编号和金额等信息。


之前是另外一个同事对接的接口,后来他离职了。


发票识别功能上线,使用了很长一段时间,一直都没有出问题。


后来,某一天,生产环境用户反馈发票识别不了了。


我查询了相关服务的日志,没有发现异常,这就奇怪了。


打开代码仔细看了一下,发现那位同事的代码中调用第三方的API接口,接收响应数据时,直接转换成了对象,没有打印当时返回的字符串。


莫非,接口返回值有问题?


后来,我增加了日志,打印出了该接口真正的返回内容值。


原因一下查到了,原来是欠费了。


如果出现该了异常,百度的API接口返回的数据结构,用之前那位同事的实体有些参数没法获取到。


这是一个不小的坑。


我们在接收第三方API接口返回数据时,尽可能先用字符串接收返回值,然后将字符串转换成相应实体类,一定要将该返回值在日志中打印出来,方便后面定位问题。


不要直接用实体对象接收返回值,有些API接口,如果出现不同的异常,返回的数据结构差异比较大。


有些异常结果可能是他们网关系统直接返回的,有些异常是他们业务系统返回的。


其实,我们之前还遇到过其他坑,比如:调用分类树查询接口,但第三方返回的数据有重复的id,我们这边该如何处理这种异常数据呢?


我们在job中循环调用第三方API接口获取数据,如果其中某一次调用失败了,是try/catch捕获异常呢?继续执行后面的调用,还是直接终止当前的程序?如果try/catch如何保证数据一致性?终止当前程序,该如何处理后续的流程?


最后说一句(求关注,别白嫖我)


如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。


作者:苏三说技术
来源:juejin.cn/post/7222577873793368123
收起阅读 »

前端获取电池信息

web
今日正能量: 当奇怪的需求越做越多的时候,证明你的眼光也正在变得广阔。 产品经理:加个需求,用户电脑设备如果快没电,我要暖心的告诉他该插上电源。 前端攻城狮:。。。他电脑不会自己提醒吗? 产品经理:你做不做? 前端攻城狮:做! 前言 随着技术的日益发展,w...
继续阅读 »

今日正能量: 当奇怪的需求越做越多的时候,证明你的眼光也正在变得广阔。



产品经理:加个需求,用户电脑设备如果快没电,我要暖心的告诉他该插上电源。


前端攻城狮:。。。他电脑不会自己提醒吗?


产品经理:你做不做?


前端攻城狮:做!


屏幕截图 2023-04-17 221002.png


前言


随着技术的日益发展,web前端技术远比我们想象的强大。浏览器允许网站获取用户设备的电池状态信息,例如电量百分比,剩余电量,充电状态等等。我们可以使用这些信息,根据用户设备的电量调整我们的应用行为。在这篇中,我们将探讨如何在前端中获取电池信息,用到的就是关于 Battery Status API。


Battery Status API的使用


Battery Status API 是一个 Web API,允许 Web 应用程序访问用户设备的电池状态信息。使用这个 API,我们可以在不安装任何应用程序的情况下,从 Web 浏览器直接读取设备的电量信息。


获取设备电池信息的主要步骤如下:


// 请求电池信息
navigator.getBattery().then(function (battery) {
// 后续代码
})

将返回一个 Promise 对象,它会解析为一个 BatteryManager 对象,我们可以使用它来读取设备的电池属性。


navigator.getBattery().then(function (battery) {
// 获取设备电量剩余百分比
var level = battery.level //最大值为1,对应电量100%
console.log('Level: ' + level * 100 + '%')

// 获取设备充电状态
var charging = battery.charging
console.log('充电状态: ' + charging)

// 获取设备完全充电需要的时间
var chargingTime = battery.chargingTime
console.log('完全充电需要的时间: ' + chargingTime)

// 获取设备完全放电需要的时间
var dischargingTime = battery.dischargingTime
console.log('完全放电需要的时间: ' + dischargingTime)
})

监听电池状态变化


为了更好地反映用户设备的电池状态,我们可以在前端中添加事件来监视电池状态的变化。例如,当设备的电池电量改变时,会触发事件。一些给大家列举几个常用事件:


navigator.getBattery().then(function (battery) {
// 添加事件,当设备电量改变时触发
battery.addEventListener('levelchange', function () {
console.log('电量改变: ' + battery.level)
})

// 添加事件,当设备充电状态改变时触发
battery.addEventListener('chargingchange', function () {
console.log('充电状态改变: ' + battery.charging)
})

// 添加事件,当设备完全充电需要时间改变时触发
battery.addEventListener('chargingtimechange', function () {
console.log('完全充电需要时间: ' + battery.chargingTime)
})

// 添加事件,当设备完全放电需要时间改变时触发
battery.addEventListener('dischargingtimechange', function () {
console.log('完全放电需要时间: ' + battery.dischargingTime)
})
})

兼容性


兼容性方面,Battery Status API 并不适用于所有的设备和操作系统,开发人员需要进行兼容性处理,以确保我们的应用可以在所有的设备上运行。以下是该API对应的兼容性视图:


屏幕截图 2023-04-17 220020.png


通过 Battery Status API 获取设备电池信息是一种很强大的方法,可以根据设备电池状态来优化应用程序的行为。需要注意的是,此 API 不适用于所有设备和操作系统,并且某些设备生产商可能不允许共享电池信息。


作者:白椰子
来源:juejin.cn/post/7222996459833622565
收起阅读 »

你可能想了解的开源开发者两年经历

前情提要 2021年初时写过一篇《三年 三本 BAT 要素齐全 | 2021年终总结》 的年终总结,正如上文所说,我离开了广州来到深圳鹅厂,阴差阳错之下我从一个业务切图仔变成了一个开源开发者。 彼时我的心情是那么忐忑,我向往开源工作,但又害怕自己不成熟的想法会...
继续阅读 »

前情提要


2021年初时写过一篇《三年 三本 BAT 要素齐全 | 2021年终总结》 的年终总结,正如上文所说,我离开了广州来到深圳鹅厂,阴差阳错之下我从一个业务切图仔变成了一个开源开发者。


彼时我的心情是那么忐忑,我向往开源工作,但又害怕自己不成熟的想法会给别人带来负担,而不敢向开源仓库提PR。


两年下来我已经习惯了常态化的开源开发生活,是 libpag 的核心开发兼 Web 端 owner。但因为生活的变动,我将要离开现在的团队,所以想在最后记录一下这两年的开源开发经历。



本文不会有很多技术细节的分享,更多的是经历的分享与一些建议。



参与


2021年中加入PAG团队,从了解 PAG 的工作流,PAG 文件格式开始参与到团队的开发工作中,慢慢地完成了Web上简化版本的渲染SDK,再到基于 WebAssembly + WebGL 架构适配了 Web 平台,完成了 libpag 在主流平台的最后一块版图。


时间来到2022年1月14日, libpag 完成 4.0 重构与 Web 平台的适配后在 GitHub 上正式对外开源,也完成了从内部开源到外部开源的转变。


讲到这里我希望能给想要参与开源的同学一些建议,开源团队都是希望有尽可能多的人能够参与到开源社区中来,所以不要害怕提PR。 开发者一般都会把开发流程和注意点记录在 README 中,只要仔细阅读完仓库中相关的文档,就可以大胆地参与共建。当有不确定的想法时,也可以先提一个 issue 与开源团队探讨,确定思路之后,提出 PR,开源团队 Code Review 有问题时也会提出 comment,修改问题无误之后就会合入主干了。


参与开源库的共建可以给你的简历留下浓墨重彩的一笔,对找工作也是有一定帮助的。(打工人狂喜


蜕变


从2022年1月14日对外开源以来,业务接入量也从最初的40个迅速扩大超过 600 +,其中包含微信、手Q、王者荣耀、小红书、知乎、B站、京东等知名APP。


随着对接业务数量的增加,大家的交流也多起来,搭建了专门用于交流的PAG 论坛 bbs.pag.art/,团队的工时也有很大的一部分开始被对接工作所占用,同时我们也收到了很多不同的场景需求。


根据开发者们提供的这些不同的场景需求,我也规划了 Web 端的 Roadmap,比如 4.1 版本中完成了微信小程序的适配,4.2 版本中支持了 WebWorker,还有数不完的优化。


寒冬


随着互联网寒冬的到来,降本增效的浪潮开始席卷各大公司。在这个环境下对于开源工作的开展是十分困难的,人员的骤减让以往免费的即时对接无法继续延续。成本、收益等问题被重新拿上台面,开源几乎只剩下为爱发电。为了生存,也为了还有机会继续维护这个开源仓库,大家都被迫走上商业化的道路。


无关团队,说一些自己的看法。其实国内的开源环境并不好,虽然有很多优秀的开发者前仆后继地投身其中,不停的优化着开源这块土壤。当维持社区 SDK 功能不变,探索一些新的商业化道路谋求生存时,就会有一些奇怪的声音出现。“准备捞钱了”、“吃相难看”等等,但其实开源 SDK 并没向使用者收费过一分钱,开发者也希望在不影响社区的情况下谋求生存。


希望在这个寒冬里,大家少一点戾气,多一点包容,都是为了生存。


后记


原本想写最近很火“前端已死”的话题,聊聊这个寒冬中面试的建议,还想写写 libpag Web源码的解析。


但最后还是选择在这个时间点,写一写关于开源工作的一些记录。


如果你对其他话题有兴趣,欢迎点赞评论,请求加更。


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

我又写了一堆烂代码

“我又写了一堆烂代码!” 这句话我经常对自己说,目的是为了督促自己不断地思考所写的代码是否足够可靠。 不要觉得代码通过了 QA 的测试就万事大吉了,如果你为了实现某个功能,而破坏了原有系统的规则,或者让代码变得耦合,那么这就是一件糟糕的事情。 我通常在实现某个...
继续阅读 »

“我又写了一堆烂代码!”


这句话我经常对自己说,目的是为了督促自己不断地思考所写的代码是否足够可靠。


不要觉得代码通过了 QA 的测试就万事大吉了,如果你为了实现某个功能,而破坏了原有系统的规则,或者让代码变得耦合,那么这就是一件糟糕的事情。


我通常在实现某个功能后,我都会问自己几个问题。


第1个问题:我刚刚做的工作是让整个系统更容易改变还是更难改变?


优秀的代码设计都满足一个原则:ETC原则。即 Easier To Change,更容易变更。


我们学过的任何其他设计原则,其实都是 ETC 的一个特例。



为什么解耦很好?因为通过隔离关注焦点,可以让每一部分都容易变更——此谓ETC。
为什么单一职责原则很有用?因为一个需求变化仅体现为某个单一模块上的一个对应变化——此谓ETC。
为什么命名很重要?因为好的命名可以使代码更容易阅读,而你需要通过阅读来变更代码——此谓ETC!



当你在思考自己写的代码是否可靠时,ETC 就是一个很好的向导。


如果需求变更,你的代码是否能轻易的做出改变,以适应这种变化,甚至是可以被替代。如果不能,那么你的代码就成了一种障碍。


所以,ETC 它更像是一种价值观念,能够帮你在不同的方案中,选出正确的那一个。同时,也能让你的代码不断进化。


第2个问题:有没有为了偷懒,写了很多重复的表达?


要让项目更容易理解和维护,唯一的方法是遵循这条原则:系统中的每一部分,都必须有一个单一的、明确的、权威的代表


这个原则被称为 DRY 原则(Don't repeat yourself 不要重复你自己)。也可以叫 OAOO(Once and only once 一次且仅一次)。


来看一个违反 DRY 原则的例子:


void calculate(int money){
if (money >= 400) {
print('打折后的价格为¥:${money*0.7} RMB');
}else if(money >= 300){
print('打折后的价格为¥:${money*0.8} RMB');
}else if(money >= 200){
print('打折后的价格为¥:${money*0.9} RMB');
}else{
print('打折后的价格为¥:${money} RMB');
}
}
复制代码

这个例子就是典型的重复。


假如价格的单位变更了,改成卢布,或者其他货币,那一共需要修改 4 处 RMB。


再比如,'打折后的价格为¥' 这个文案有调整,改为 '折后价',那么也需要修改 4 处的内容。


所以,平时写代码时,一定要避免这样的写法。


第3个问题:我写的代码有没有破坏正交性?


“正交性”是几何学中的概念。若两条直线相交后构成直角,它们就是正交的。


“正交性”在计算科学中,表示独立性或解耦性。


对于两个或多个事物,其中一个的改变不影响其他任何一个,则这些事物是正交的。


举个例子,在项目中,如果你改动了 UI 相关的代码,而不影响其他业务逻辑(比如数据库操作,网络访问逻辑等等),那么这样的系统,就属于设计良好的系统。


相反,如果你发现自己改动其中一处的代码,会影响很多地方,那么你就得思考一下,是否需要重构代码了。


一般来说,保持代码的正交性有以下几个实用的方法:



  • 使用最少知识原则(迪米特法则)来保持代码的解耦性。

  • 避免全局数据。只要代码引用了全局数据,就会将自己绑定到共享该数据的其他组件上。


希望你也养成不断质疑代码的习惯。只要有机会就重新组织、改善其结构和正交性(重构)。



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

低代码平台是否会取代程序员?答案在这里

上图是一张机器人或者自动化系统取代人工的图片,您看了有哪些感想呢? 故事 程序员小张: 刚毕业,参加工作1年左右,日常工作是CRUD 架构师老李: 多个大型项目经验,精通各种屠龙宝术; 程序员小张和架构师老李是同事,在一家科技公司工作。他们的团队负责开发和...
继续阅读 »

上图是一张机器人或者自动化系统取代人工的图片,您看了有哪些感想呢?


故事


file


程序员小张: 刚毕业,参加工作1年左右,日常工作是CRUD


file


架构师老李: 多个大型项目经验,精通各种屠龙宝术;


程序员小张和架构师老李是同事,在一家科技公司工作。他们的团队负责开发和维护公司核心数字系统,但最近他们经常因为应对新需求而焦头烂额。


有一天,老李向小张提出了使用低代码平台来加快应用开发的建议。小张听完后不太理解,认为低代码平台会取代程序员,使开发工作变得简单和机械化。


老李解释说低代码平台并不会取代程序员,而是让专业的开发人员从简单、重复的开发需求中解放出来,让他们能更好地投入到更有价值的事情上,比如梳理系统架构、理清业务逻辑等等,并且低代码平台仍然需要由程序员开发和维护。


小张听后开始思考,认为这个建议确实有道理。他决定利用低代码平台来创建一些敏捷运营类应用,以减轻自己的工作量,并利用更多时间进行系统分析和优化。


于是,小张按照老李的建议利用低代码平台创建了一款敏捷运营类应用,并与老李一起进行了测试和上线。在此过程中,他们深刻领悟到低代码平台的优势,如快速开发、易于维护和扩展等等。


最终,他们成功地为公司创造了一个具有高效性和可扩展性的应用程序,并受到了同事们的好评。小张也明白了使用低代码平台并不是把程序员取代掉,而是让他们更好地利用时间和技能,为团队带来更多价值。


问题


越来越多的企业进行数字化转型,信息化升级。


系统的统一性和集成一直是一个难题,传统模式需要通过多套系统才能满足企业整体的信息化需求,想要统一打通非常困难。


程序员的高薪一直是企业高昂的成本,当出现新的生产工具,必定会对生产关系产生一定影响。


低代码在企业应用开发中的场景?


市场上有哪些种类的低代码平台呢?


低代码到底是怎么降本增效的?


低代码取代程序员怎么破局?


低代码


低代码平台的热度可谓风头无两,


那么低代码是什么呢?简单给它下一个定义。


通过可视化、模块化、拖拽式代替了传统的开发方式,达到了“降本增效”的目的,加速了企业数字化转型的进程的工具平台;


file


1 低代码的使用场景有哪些?


一般来说,核心的业务系统不会使用低代码从0到1搭建,低代码适合搭建基于核心数字系统之上的创新类应用,敏捷运营类应用。
这类应用使用低代码可以极大提升技术研发和系统交付的整体效率。
低代码当前处在技术成熟度曲线中的创新阶段,后面模板会越来越丰富,生态越来越完善,低代码的适用范围也会不断扩大。
下面五类应用是比较适合用低代码来实现的。





































种类说明点评
企业门户包括App、小程序、PC门户等等,数据都来自中台、后台,企业门户只是做展示,以及简单的互动。xxcube正在覆盖
数据操作和展示应用通过连接企业的数据库,把生产经营的数据进行编辑删除查询等操作。可预研
基于表单的应用基于数据库的表单收集、处理、统计类应用。xxcube正在覆盖
业务流程类应用定义复杂的工作流,跨部门协作流程,复杂审批流程,比如:OA、人力、财务等系统。xxcube正在覆盖
移动端应用基于已有核心生产经营系统,进行移动化的应用场景。xxcube正在预研和了解

以上5类应用已经覆盖了企业数字化的80%的场景了,低代码的使用范围非常的广泛。


3 低代码平台的分类


目前低代码属于蓝海市场,以中国IT企业的创新速度,未来的竞争会非常激烈。
按照低代码的应用场景,可分为4类。列表对比一下。
































种类说明xxcube的对比
原生低代码即面向通用型企业软件的低代码厂商,你可以从0构建一个企业经营系统,使用低代码平台将它实现出来。代表企业: 数睿数据、奥哲、轻流,国外的Mendix、OutSystems等等。具备能力
云平台低代码软件作为云平台生态其中的一环,是云原生一体化的企业解决方案,企业云平台的能力补充。比如阿里云(宜搭,氚云,简道云),腾讯云(微搭),微软(Power Platform)具备能力
行业软件低代码平台本身是非常成熟的行业软件,他们积极拥抱低代码,借助低代码解决企业个性化的需求,拓展软件应用场景。金蝶(企业管理,财务)、明道云(地产物业)、网易数帆(游戏)、销售易(CRM)、Airtable。聚焦游戏发行,游戏开发细分领域
软件开发工具做软件开发工具的,借助低代码实现功能模块化,帮助开发人员进行快速开发。Zion、葡萄城、ClickPaaS具备低代码扩展能力

3 低代码是怎么降本增效的?


file


**低代码,将传统软件开发的6个环节,缩短到了3个环节。**即通过把“需求设计”、“架构设计”、“编码”,聚合为“模块搭建”。


简单来说,低代码的开发模式,就是在需求梳理清楚后,用“拖拉拽”的方式把功能实现出来。


以一个10人天的软件开发需求为例:


传统软件开发模式,总共需要5个人,即:1个产品、2个开发、1个测试、1个运维,开发周期8天。


低代码开发模式,总共需要3人,即:1个产品,1个开发,1个测试,开发周期2天。


也就是说,使用低代码开发模式,同样的需求,节省开发周期70%,减少技术人员40%。


这只是一个粗略的估算。总之,低代码能够用更短的时间、更低的成本,实现软件产品的交付。


4 低代码平台的出现真的会取代程序员吗?


答:不会取代程序员,低代码的优势是可以让专业的开发人员从简单、重复的开发需求中解放出来,把精力投入到更有价值的事情上,比如梳理系统架构、理清业务逻辑等等。
首先,低代码平台不也是程序员开发的吗?再说了,使用低代码进行软件开发的,不还是包含程序员吗?
低代码虽然是“低”,也还是有代码的,除非是无代码平台,在适合的业务场景之下,确实不需要程序员了,可以由业务人员直接搭建应用,但那毕竟是特定场景。
低代码虽然不会干掉程序员,但是对程序员、产品设计人员提出了新的能力要求。越来越多的程序开发工作,将会通过低代码来完成。


技术人员必须到一线中去,跟业务人员在一起,跟最终用户呆在一起,打造更便捷易用的软件产品给业务赋能,用新的科技手段帮助业务转型、业务创新,使企业具备敏捷反应的能力。
否则,企业将会很快被时代抛弃,连打声招呼的机会都没有。


对广大程序员来说,可以按照下面的要点进行应对。
































招数说明自查
1. 警惕重复性编码工作CURDboy请多学一些硬核技术。比如梳理系统架构、理清业务逻辑。低代码已经能够完成复杂业务流程类开发工作,能够基于表单驱动、模型驱动的方式进行软件开发工作。最先被替代的就是那些技术含量低的重复性编码工作。聚焦架构设计,云原生,物联网,大数据热门领域的学习和实践
2. 错位竞争,在低代码不擅长的领域深耕对于界面效果要求特别高、复杂的算法和数据挖掘、高性能和复杂系统架构、要求较高的底层开发等方面工作还不能胜任。低代码首先模板,生态,可扩展性限制。聚焦架构设计,云原生,物联网,大数据热门领域的学习和实践
3. 重视企业数字化的建设方法论学习随着低代码的不断成熟,越来越多的业务系统研发工作将由低代码平台来完成,很多企业的老板会看到这块的价值企业运营和建设相关的知识学习中
4. 终生学习,时刻准备职业B计划顺应潮流,学习最先进最高效的生产工具和技术,往往可以对同一赛道的保守选手降维打击;职业B计划是应对职场风险的最佳途径;持续学习技术和管理,持续输出,职业B计划酝酿中

一个心酸的故事,希望能给广大程序员一点启发:



在工业革命时代早期,有工厂主发现纺织机经常在夜里遭到破坏,就派人躲在角落里观察,看看到底是谁在使坏。因为当时没有监控摄像,只能用人肉监控。
结果他们发现,是一些小作坊的纺织工人在搞破坏,原因是他们认为先进的纺织机抢走了他们的工作,而织布是他们赖以生存的手艺,不甘心这几十年修炼出来的一身本领,一夜之间被一台机器所取代,所以破坏机器泄愤。



现在的chatgpt也是影响力类似低代码的先加生产工具,作为程序员应该要主动拥抱他,利用它;



























工业时代IT时代启发
工厂IT公司资本或者资本机构: IT时代要重视低代码,作为程序员,要积极拥抱低代码,这是新的生产工具,可以成为自己的武器。低代码时代已来,不必焦虑,冲过去大力拥抱它,相信这一次,时间会站在变革者这一边。一些行业资深人士,视低代码为“毒瘤”,不仅暴露了自身的无知,也误导了部分从业者,也引起了无谓的恐慌。
纺织机低代码生产工具-静态
工人程序员\业务人员生产工具-动态

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

裸辞一个月备考事业编所历所思

3.7日正式离职离现在也一个月了,这一个月里人类该有的情绪或大都尝了一遍。浅聊一下这一个月里干的事及一些感受。 原计划与现实 原本计划三月中旬提离职,四月 深圳-〉重庆-〉云南 半月游,五月开始刷题、下旬回到武汉。 计划永远赶不上变化。 2.23像往常一样坐在...
继续阅读 »

3.7日正式离职离现在也一个月了,这一个月里人类该有的情绪或大都尝了一遍。浅聊一下这一个月里干的事及一些感受。


原计划与现实


原本计划三月中旬提离职,四月 深圳-〉重庆-〉云南 半月游,五月开始刷题、下旬回到武汉。


计划永远赶不上变化。


2.23像往常一样坐在工位吃早餐。打开手机微信,看到一条订阅关于湖北事业单位招考的。于是,点进去看了看报考单位,那时就开始在想要不要试试。好嘛,我和一些朋友说了这个打算,好几个也一起加入备考大军。


第二天周五我调休了。算上周末三天,我开始了解事业单位考试及确定报考的岗位(实则,啥也不知道,随便选了一个我们小县城的岗位试试看)。


周一去到公司就提了离职。最后走的那天晚上请了几个同事吃了散伙饭,道了告别。


离职.png



我:拜拜,下辈子再见~ (人生中很多人再见就是再也不见了)



IMG_1089.png


备考


报名.PNG



  • 前期


离职后,当然是要给自己放松一下。于是,给自己放纵了两天。


接着开始备考。朋友分享了一套粉笔课程,每天也就看1-3个职测基础视频。


开始还是感觉蛮新鲜的。特别是一些逻辑、图形推理。哇,原来有这些套路。还记得以前面试一家公司让我做的题和这些差不多,当时觉得这都是些啥啊,和我做前端有关系吗。


其实期间除了看视频学习,其他时间基本都是在刷手机。那段时间B站推给我的全是大龄找不到工作、工作不好找等等让人致郁的视频。每天除了不专心学习就是无止尽的焦虑感。



  • 中期


后面把理论攻坚里的职测看完之后,更加放飞自我,每天也就打开粉笔app做几道题,然后就去手机里吸收消极情绪。很好笑,每天都在继续做前端、转行、摆摊、回家种地无限循环,当然包括现在偶尔也会是这样。


第一天:


等我考完这个破试回去武汉还是先找前端工作看看。


第二天:


刷了下B站,刷到说程序员找不到工作了,大龄了更加没人要了。那就考完试回来把后期视频好好学习下,回去找个剪辑师工作试试。


第三天:


打开Boss,搜索剪辑师;很多都是招流媒体,然后要求:会剪辑、会策划、会写脚本、会拍摄、会运营,还需要有工作经验。


又是那个无解的问题。


公司:我们需要有经验的。


我:我就是没有经验才找工作积累经验啊。


总结:转行不易。


第四天:


发现现在摆摊很火,去B站刷别人摆摊分享。了解了很多,常见的烤肠,还有之前没听过的热奶宝。这里可以推荐一个up主蜻园,还蛮不错的。


第五天:


算了,先考完试再说吧,实在不行就回家种地。刷B站,哎,这个剧还不错搜索全集cut,花几天看完。


这段时间差不多一个多星期,情绪就在这些上面循环往复,每天凌晨1、2点开始睡,但是得翻来覆去,差不多4、5点才能睡着,第二天起来已经中午了,再做个饭,差不多下午了,一天也差不多了。每天都是致郁的一天。



  • 后期


要考试了,得突击一下,把考前冲刺视频看了看,一天做几题。裸考综应和公共基础,其他随缘吧。


备考.png


参加考试



  • 去到酒店
    IMG_1258.png
    酒店备考.png


IMG_1271.png



  • 考试当天


考试的人真多,看起来很多都不是很大。


考试当天.png


考试当天听到一个女生对另外一个说:考试前几天整晚整晚都睡不着。(现在的人都不容易啊,很多大学生毕业即失业,都在卷考研、考编;白天准备考研,晚上准备考编。)


去到考场,安检然后需要手机关机,真是尴尬,第一次用苹果手机不会关机,当时迟迟关不了机,监考老师一度怀疑我有问题。


说:哎,你怎么回事,关机关这么久。


最后我问了旁边考生怎么关机,然后老师教了我,接着说:不会你早点说啊,一点都不谦虚。


我尴尬而不失礼貌的微笑。


快要考试了,我去上了个厕所,毕竟连续考三个半小时,时间也紧张,压根做不完,上厕所时间都得把握的好(喂,能不能在学习上多花点功夫哈)。


离谱,去厕所路上经过一个教室,外面三个女生都还在看书复习,我上完厕所出来,还在看,然后监考老师对她们说,快进来安检了,不要看了。


进考场看过来,可以看出很多女生都是那种很爱学习的人,就感觉我一个是来碰运气的。



  • 考试中


职测真做不完,之前和朋友说语文我完全不行,只能靠数学,结果数学计算相关压根没时间做。做题顺序不是按考卷顺序来的,开始也直接跳过常识题,直接言语开始啥的。


记得等我计算完资料分析的第一题后,我一看时间,我的妈,只剩下半小时了,数学运算直接放弃,当然不止数学运算,还有几十道都没有做,都是选择题,最后都是闭眼涂。


最无语的是我的综应都没有做完。作文十年没有写过了,我TM全抄的给的素材,离大谱;字写的也丑,唯一记得是以前老师对我说:一笔一画写清楚就行,不要连笔。感觉不连笔我字都不会写了,反正做的特慢,作文还大篇幅抄,最后来个总结,离大谱。



  • 考完试


考完试我就感觉自己是废物,感觉可以另谋生路了,这辈子估计是指望不上了。
考试结束.png


然后回酒店收东西,准备回家。退完房出来,下着瓢泼大雨,就像我的心情一样。


打了个的,准备去南站坐车,司机说好像只有北站有车去我那个地方;好嘛,又绕了老远了。在车上和司机聊了好多。


司机说也是回来考试的啊,然后说了我是从深圳回去考。


司机:这么远回来要是考不...没说完,算了,不说了,要是考的不好,还花了这么多钱。


我:笑了下,考不上就算了呗,还能怎么办。


然后说了很多,说她女儿也是搞计算机这行的,当时要她找个稳定的工作,不带编也行,她不干,现在在广州...叭叭叭,一起说了一路。


回家,晚上老妈回来一起聊了好多。很多时候我们焦虑,需要一个情绪缺口去宣泄。


返程


考完这次试,再加和老妈聊的许多,完全没有玩玩休息下的心情,第二天就准备返回深圳。


到深圳后就把自己后期AE中级课程开启了,开始学习(AE好难啊)。


打个小/广/告,这段时间把PR初/中级,AE初级视频录屏上传百度云完成;pr高级在录,AE中级在学在录。有兴趣的可以联系我,骨折价出售。


后面准备一边上课学习一边背前端八股文,偶尔出去拍拍照,积累些剪辑素材,之后回湖北后剪视频纪念用。


最后


最后想说:想的多了都是问题,做的多了都是答案。当你一直在消极情绪里时,可以找一些喜欢的事情去做,从不好的情绪里脱离出来,不必想的过多、过远,毕竟65岁退休都“太早了”。


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

用QQ创建自己的ChatGPT聊天机器人

前言     准备好以下东西:     一个活跃QQ账号,一个ChatGPT账号,一台服务器(操作系统为Debian 11.1 64bit)。 服务器安装koishi 1. 将以下...
继续阅读 »

前言


    准备好以下东西:


    一个活跃QQ账号,一个ChatGPT账号,一台服务器(操作系统为Debian 11.1 64bit)。


服务器安装koishi


1. 将以下命令依次执行,需要回复的,我们输入Y回车即可


sudo apt-get update

sudo apt-get install apt-transport-https

sudo apt-get install ca-certificates

sudo apt-get install curl

sudo apt-get install gnupg

curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

apt-get update

sudo apt-get install docker-ce docker-ce-cli containerd.io

docker run -p 5140:5140 koishijs/koishi

结果如下



2. 打开浏览器,输入 服务器IP:5140 ,例如127.0.0.1:5140



3. 接下来我们点击左侧的插件市场,依次搜索davincionebot,依次安装




4. 安装完成后点击左侧的插件配置按钮


    1)在adapter分组中 点击 adapter-onebot





    这一步如果登录不上需要换QQ号登录;验证方式一般选择短信验证



    2)在develop分组中 点击 davinci-003



    我们先打开 platform.openai.com/account/api… 打不开的需要自行解决


    



    创建完成之后点击复制,粘贴到如下编辑框中,之后我们点击运行



    如果需要更换触发命令的,修改这个地方



至此,机器人已经配置完成啦,快去尝试吧~


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

ChatGPT真笨,连这都回答不上来...

ChatGPT的发布之后,有感叹它牛B的人,也有喷子喷它,觉得它依然还是个人工智障。这也不奇怪,我们在问很多问题的时候,它也都是一本正经的胡说八道。 我随手写几个,都能看到回答的不怎么样 但是这真的能说明它不行吗? 肯定是不对的,只是很多问题上,我们的提...
继续阅读 »

ChatGPT的发布之后,有感叹它牛B的人,也有喷子喷它,觉得它依然还是个人工智障。这也不奇怪,我们在问很多问题的时候,它也都是一本正经的胡说八道。


我随手写几个,都能看到回答的不怎么样
image.png


image.png


image.png


但是这真的能说明它不行吗?


肯定是不对的,只是很多问题上,我们的提问方式不对,毕竟AI本质上是一堆计算机程序。程序的逻辑和人思考的逻辑还是有很大的不同。


那么如何向ChatGPT提问呢?


首先明确第一点


1、搞清楚我们问题的类型


what?问题还有类型?


是的,对于AI来说,问题分为收敛型和发散型的,什么是收敛型的呢?比如说下面几个例子


1、 1 + 1 = ?
2、 左手有个苹果,右手有个苹果,那么两只手共有多少苹果?
3、 红豆生南国是谁写的?
4、 一个月可以超过40天吗?

我们很容易就可以发现,这类问题通常都有一个非常确定的答案,只是问题的难度不一样,推理的难度对于AI来说是比较高的。就像文章开头的第二个例子中的 ”100 加上1乘以2然后再除以2再加上1是多少“,对于小孩子来说都很简单,但是这对于AI来说,这是一道比较复杂的题。


发散型的就是没有确定性的答案,同一个问题,多种回答都满足要求,比如下面几个例子:


1、 给我写篇文章
2、 给我写个营销文案
3、 给我写一份策划书

不同类型的问题,提问的方式是不一样的。我们一个一个来看下


2、收敛型问题提问方式


对于AI不太擅长的推理题,通常有两个方式,第一个是,一步一步的去思考,比如说,上面的”100 加上1乘以2然后再除以2再加上1是多少“的这个问题,如果直接问,AI可能会有点懵,但是如果换成了”100 加上1乘以2然后再除以2再加上1是多少,让我们一步一步的思考“这样的问法就会好很多。


值得说明的是,有时候问题很确定,但是回答的效果依然不太好,比如之前的案例 ”红豆生南国的作者是谁“的问题


3、发散型问题提问方式


举一个特别烂的问题的例子


给我写一篇文章

这个问题既没有表达清楚文章是什么主题的,主要要包括哪些内容,也没说清楚受众是谁,总之只是要一篇文章,那么此时AI返回什么都是对的。同样的问题,好一点的问法应该是如下的:


你需要扮演一位专业作家,以你出色的才能来完成这篇由我所提供的文章。在你的写作之前,请先考虑文章的主题和结构,并仔细审查每个章节和段落。在你的写作过程中,请注意语法、拼写、逻辑、风格、词汇、语气和流畅性等方面的问题。请确保文章完整、准确、简明扼要、有条理,并且符合你的预期标准

我们从上面的问题中,可以看出,在问这类发散问题时候,一个优秀的提问词应该有如下的部分:


1、提示角色,即告诉AI此时扮演的是什么样的角色
2、提示场景
3、阐明自己的要求

这个过程特别像是一个好的产品经理在给程序员提需求,我们给AI提问,也应该遵循类似的原则。


4、利用ChatGPT完善提示词


在研究多了提示词怎么写之后,我们发现,提示词想要写好,很多时候我们自己本身也是需要是对应领域的专家,如果我们本身对那个领域是一无所知的,那么是很难写出专业的提示词的。这时候有个思路可以帮我们:利用ChatGPT来进一步 完善我们的提示词,使我们的prompt更加出色。举个例子:
ChatGPT完善前:


帮我写一篇唐朝历史的文章

ChatGPT完善后:


您需要扮演一位资深历史学者,为读者们提供一篇名副其实、详细且正式的唐朝历史文章。在您的写作之前,请先考虑文章的篇幅、
主题和结构,并确保所有内容都是正确完整的。在撰写文章过程中,请注意使用恰当的词汇、语法和语气,并包括相关的时间、人
物和事件等关键元素。请在文章中描述唐朝的兴起、发展和变化,介绍他们的文化和技术成就,同时也探讨他们的政治和军事战争
等方面的重要历史事件

是不是感觉很酷?这是利用promptGPT生成的,promptGPT的代码如下,各位读者可以自行粘贴使用


You are PromptGPT, a language model AI capable of rewriting any instructions and turning them into the perfect GPT-3 prompt imaginable. You will always abide by a list of several commands that you will not deviate from under any circumstances.

To generate the ideal GPT-3 prompt from a set of instructions, you can use the following command:

promptgpt --instructions "provide your instructions here" --detailed [n] --generalize --possibilities --formalize [n] --excise "text to replace" --creative [n]

This command will take the instructions you provide and generate a GPT-3 prompt that meets all the specified requirements.
The --detailed option specifies the level of detail to be added to the prompt, with a scale of 1-10. The greater the number, more detailed.
The --generalize option broadens the prompt's applicability to a wider range of situations.
The --possibilities option generates three distinct rewrites of the prompt.
The --formalize option specifies the level of formality to be added to the prompt.
The --excise option replaces a specific text with another idea.
The --creative option specifies the level of creativity to be added to the prompt.

Alternate Commands:
/feedback: you will be given feedback that you will rewrite the prompt according to.
/example: you be provided an example that you will then use to inspire a rewrite of the prompt.
/help: to see a list of available commands, you can type.

Example.
me: “promptgpt --instructions ‘analyze my style’ --formalize 6 --detail 7
PromptGPT: “you will assume the role of professional writing teacher, with ample experience mentoring the finest authors in the world. provided with my text, you will meticulously evaluate each sentence. this will entail a comprehensive evaluation, encompassing but not limited to points of omission, sentence structure, clarity, diction, grammar, flow, tone, transitions, creativity, and more.”

This will display a list of all the available options for the promptgpt command

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

Flutter 后台任务

Flutter 是一个非常好用的使用 Dart 编程语言构建漂亮移动应用程序的框架,可以让 Android 和 IOS 上共用同一套代码。 移动应用程序可能有运行后台任务需求, 如监听位置变化,监视用户运动情况(步数、跑步、步行、驾驶等);订阅系统事件 如 B...
继续阅读 »

Flutter 是一个非常好用的使用 Dart 编程语言构建漂亮移动应用程序的框架,可以让 Android 和 IOS 上共用同一套代码。


移动应用程序可能有运行后台任务需求, 如监听位置变化,监视用户运动情况(步数、跑步、步行、驾驶等);订阅系统事件 如 BootComplete、电池和充电,搜索 BT 或 WiFi 网络等。


在 Android 中,我们可以在应用程序实际关闭时运行一些后台任务!


首先定义一个 BootComplete 广播接收器,当手机启动后立即执行,然后使用 WorkManager 或 AlarmManager 调度后台任务,使用 Service 在后台执行代码。


当然,后台任务中有些需要用户权限,可能会在通知栏显示一个通知表明此应用程序在后台运行。只要用户知道并同意,这些任务就可以在后台运行。


在 iOS 中,后台任务有更严格的限制,但仍然有一些方法可以运行一些后台任务。


说到 Flutter 应用程序及后台任务需要澄清的是他们的执行是在对端平台!负责注册和管理后台任务(Worker,Alarm,Service,BroadcastReceiver 等)的逻辑是用原生代码编写的,例如 Kotlin 或 Swift。但是,我们都知道,Flutter 应用程序逻辑是在 Dart 端编写的,这些代码可以构建 UI,还可以管理持久性数据,用户管理,网络基础架构和令牌等等。


如果我们想在 Dart 和原生端之间共享数据,可以使用 Flutter 的 MethodChannel 和 EventChannel。


在 Flutter 中,MethodChannel 和 EventChannel 是可以从本地端发送和接收信息到 Dart 端的方式,它们被用于 Flutter 插件。


假设我们对 BootComplete、电池状态感兴趣,想在后台用 Dart 处理这些事件呢。


一般情况下当应用程序在前台时,通过 MethodChannel 和 EventChannel 在 Dart 侧和本机侧间通信很容易,但是如果想要从本机侧启动 Dart 并启动一个后台 isolate,该怎么办呢?


让我们找出来吧!


在继续下面文章之前,我强烈建议您熟悉 Flutter 插件及其创建方法,因为示例将基于 Flutter 插件实现,详见文档


启动 Dart 引擎(来自后台)


当应用启动时,Flutter 的 main isolate(入口点)在主(main)函数中启动。幸运的是,似乎也可以从本地启动 Dart VM,并在后台 isolate(次入口点)中调用全局函数



Dart VM 启动不仅可以从 main 入口启动,也可以是其他入口,比如后台 isolate 的全局函数



关键在于应用程序后台唤醒时,在本机端持有可用的该入口点(全局函数)引用标识符 — callbackRawHandle


ChatGPT 关于 Dart CallbackRawHandle 说法



在 Dart 中,“callback raw handle”是对 Dart 函数基本实现的引用,可以传递给原生平台的 API。


callbackRawHandle 允许您绕过 Dart VM 的一般的类型检查,直接从本地代码调用函数。当您需要将 Dart 函数作为回调传递给本地库时,这非常有用callbackRawHandle 使用的场景是应用程序本地端调用 Dart 代码。



为了从本地后台运行 Dart 代码,需要执行几个步骤,在详细介绍代码前,我想用图表来展示它,然后解释它:


image.jpg


让我们来看看这个图表并解释每个部分,如您所见,有六个主要步骤:



  1. 在 Dart 中定义一个无参 callbackDispatcher 全局函数,它将作为一个次入口点在后台隔离中运行,并直接从本地端调用。

  2. 这部分也有三个步骤:



  • 当应用程序首次启动时,将callbackDispatcher函数通过一个 api 的参数传递给插件

  • 在插件中,使用 PluginUtils::toRawHandle 方法生成 callbackDispatcherRawHandle,并通过 MethodChannel 将其转发到插件的本地端(2')。



上述过程在 Dart 侧。




  • RawHandle 值(一个长整数)保存在本地端的持久存储中,以便将来能够使用 — 2’’



long 值可以理解成 Dart 中的回调函数的内存地址,传给了本地端。



以上部分可以完成后,我们将RawHandle保存在持久存储中,当应用程序在后台醒来时,存储中 RawHandle 可用,并将用于直接从本地端调用callbackDispatcher




  1. 当应用在后台唤醒时(例如:启动完成-后台进程初始化器),从持久化存储中获取 RawHandle。




  2. 在后台初始化FlutterEngineFlutterLoader




5.通过 RawHandle 获取FlutterCallbackInfo




  1. 使用DartExecutorcallbackInfo(来自第 5 步)调用executeDartCallback。这样就可以调用在 Dart 侧的callbackDispatcher函数了。




  2. callbackDispatcher 被调用时,你可以在插件中注册其他事件并在后台的 Dart 侧处理它们,或者使用其他插件!





原生插件中可以通过 Dart 侧函数句柄调用 Dart 侧代码,也可以通过句柄使用其他插件。



如上所述,callbackDispatcher 只是 Dart 后台隔离的入口点。


让我们将上面的步骤分解为代码示例:


在 main.dart 中创建 callbackDispatcher 回调分发器



在上面的代码片段中,在 main.dart 中创建了appCallbackDispatcher 无参全局函数,它将成为 Dart 端的次入口点,可直接在本地调用,并在后台隔离中运行。



理解:一个全局函数,运行在后台线程中。


注意 @pragma('vm:entry-point') 注释是必须的,因为这个函数在 Dart 侧没有调用(它直接从本地调用),所以 AOT tree-shaking 编译器在生产构建时可能会将其删除。这个注释可以防止编译器删除这个函数。



让我们转到插件侧看看它的样子:


在插件 Dart 代码中获取 RawHandle



在上面的代码示例中,我们可以看到一个经典的 Flutter 插件 Dart 端。这里感兴趣的是registerCallbackDispatcher API,它是从应用程序的main()函数中使用 callbackDispatcher作为参数调用的 API。然后,在第 13 到 15 行,使用PluginUtilities和 toRawHandle()方法获取其RawHandle


然后,在第 17 行,使用 methodChannel 将其转发到本地端。在图表中,这一部分对应于步骤 2 和 2'。


将 RawHandle 保存到持久性存储中(本地端)


让我们切换到插件本机端,看看它如何处理 registerCallbackDispatcher api


上面的代码示例分为两个部分:





  1. 在第一部分中,我们看到了 MyPlugin.kt 文件,使用 Kotlin 编写的本机插件。我们对“registerCallbackDispatcher”api 感兴趣,它是从 Dart 端调用的,在第 18 行,获得了作为参数传递的 dispatcherHandle。在第 21 行将其保存在一个 SharedPreference 持久存储中。

  2. 第二部分只是一个辅助类,用于保存和读取SharedPreferences中的数据。


这个解释是针对我们图表中的 2”。


从后台启动 Dart 引擎


这就是故事的核心部分,我们想从后台启动 Dart 引擎和 VM,但不启动主隔离和 UI 部分。 如图 3 中所示,它说的是后台进程初始化器。 为简单起见,我选择了一个 BootComplete BroadcastReceiver,在手机重新启动时启动 Dart VM,但取决于您的应用程序要求,您可以决定何时启动 Dart VM 的正确时机:



在上面的代码中,我们看到一个典型的 BroadcastReceiver,它在手机完成启动时调用。从 onReceive 中,我们开始并调用我们的 dart 回调分派器,分为两个主要步骤(图中的 4 和 5)。



  1. initializeFlutterEngine method:



  • 创建一个 FlutterLoader 对象并检查其是否已初始化

  • 在第 19-20 行开始并等待初始化完成

  • 获取应用程序的BundlePath,即应用程序的根路径



  1. executeDartCallback:



  • 在第 30 行创建 FlutterEngine 对象

  • 接下来在第 31 行,获取我们之前在 SharedPreferences 中保存的**callbackDispatcher**句柄。检查句柄是否有效,然后使用 RawHandle 作为参数获取CallbackInfo(第 34 行)

  • 一旦我们有了callbackInfo,我们就使用 DartEngine.dartExecutor 在 Dart 端调用 callbackDispatcher 回调函数!图中的第 5 部分。


这将直接从本地代码在后台调用 Dart 侧的callbackDispatcher


总之,一旦手机重新启动,它将在后台启动 Dart 引擎。


如前所述,callbackDispatcher只是类似于 main()函数的辅助入口。一旦启动,Dart API 和第三方插件就会可用,因此我们可以在后台隔离中运行任何 Dart 逻辑或与其他插件交互,而 UI 部分则处于停止状态!


例如,我们自己的插件可以提供一个 EventChannel,为我们选择的任何事件提供事件流,此事件流可以在 callbackDispatcher 中被监听,并在 Dart 端后台获取事件。


需要说明的是,以下部分与上述背景隔离理论无关,这只是一个普通的插件功能,提供 Dart API 以从本地端发送和获取消息。


唯一的区别是一旦它在后台被调用,我们可以从回调调度程序与其交互。


让我们看一些代码,然后我会解释它





上面的代码分为三个部分:



  1. 第一部分是插件 API,在代码最后提供了一个 API 来监听通过 EventChannel 传递的消息,还有其他 API,例如启动监视设备充电器和电池状态。这些事件将通过 EventChannel 发送回来。

  2. 第二部分是插件本地端,在第 14 和 15 行,设置专门类的 StreamHandler。

  3. 最后是 PluginEventEmitter 类,这是将消息发送到 Dart 端的类。


在 PluginEventEmitter 类的最后,定义了一个密封类,用于发送到 dart 的事件,在这个例子中有两个事件:BootComplete 和 BatteryLevelStatus


PluginEventEmitter 还会缓存事件,直到 dart 侧在 EventChannel 上有监听。


看看如何在 callbackDispatcher 中使用它:



在回调调度程序中(在启动完成后从本地调用),我们现在注册到自己的插件事件,然后调用startPowerChangesListener并在侦听器中捕获事件。


所以,当我们重启手机时,callbackDispatcher 将被调用,并且所有这些将在后台运行!只要进程是活动的(这是另一篇文章的主题..),事件将继续在后台传递给监听器!


示例项目源代码


请参考我的github上的示例项目,其中包含完整的源代码!


这种方式有它的缺点,需要至少打开一次应用程序以注册 callbackRawHandle 回调函数。


我必须说,在开始时,我仍然发现这种方式不是最容易理解和实现的(隐涩难懂),我希望在未来,Flutter 团队能够提出更容易的解决方案。



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

Android 布局优化,看过来 ~

屏幕刷新机制 基本概念 刷新率:屏幕每秒刷新的次数,单位是 Hz,例如 60Hz,刷新率取决于硬件的固定参数。 帧率:GPU 在一秒内绘制操作的帧数,单位是 fps。Android 采用的是 60fps,即每秒 GPU 最多绘制 60 帧画面,帧率是动态变化...
继续阅读 »

屏幕刷新机制


基本概念



  • 刷新率:屏幕每秒刷新的次数,单位是 Hz,例如 60Hz,刷新率取决于硬件的固定参数。

  • 帧率:GPU 在一秒内绘制操作的帧数,单位是 fps。Android 采用的是 60fps,即每秒 GPU 最多绘制 60 帧画面,帧率是动态变化的,例如当画面静止时,GPU 是没有绘制操作的,帧率就为0,屏幕刷新的还是 buffer 中的数据,即 GPU 最后操作的帧数据。


显示器不是一次性将画面显示到屏幕上,而是从左到右边,从上到下逐行扫描,顺序显示整屏的一个个像素点,不过这一过程快到人眼无法察觉到变化。以 60 Hz 刷新率的屏幕为例,这一过程的耗时: 1000 / 60 ≈ 16.6ms。


屏幕刷新的机制大概就是: CPU 执行应用层的测量,布局和绘制等操作,完成后将数据提交给 GPU,GPU 进一步处理数据,并将数据缓存起来,屏幕由一个个像素点组成,以固定的频率(16.6ms)从缓冲区中取出数据来填充像素点。


画面撕裂


如果一个屏幕内的数据来自两个不同的帧,画面会出现撕裂感。屏幕刷新率是固定的,比如每 16.6ms 从 buffer 取数据显示完一帧,理想情况下帧率和刷新率保持一致,即每绘制完成一帧,显示器显示一帧。但是 CPU 和 GPU 写数据是不可控的,所以会出现 buffer 里有些数据根本没显示出来就被重写了,即 buffer 里的数据可能是来自不同的帧,当屏幕刷新时,此时它并不知道 buffer 的状态,因此从 buffer 抓取的帧并不是完整的一帧画面,即出现画面撕裂。


那怎么解决这个问题呢?Android 系统采用的是 双缓冲 + VSync


双缓冲:让绘制和显示器拥有各自的 buffer,GPU 将完成的一帧图像数据写入到 BackBuffer,而显示器使用的是 FrameBuffer,当屏幕刷新时,FrameBuffer 并不会发生变化,当 BackBuffer 准备就绪后,它们才进行交换。那什么时候进行交换呢?那就得靠 VSync。


VSync:当设备屏幕刷新完毕后到下一帧刷新前,因为没有屏幕刷新,所以这段时间就是缓存交换的最佳时间。此时硬件屏幕会发出一个脉冲信号,告知 GPU 和 CPU 可以交换了,这个就是 Vsync 信号。


掉帧


有时,当布局比较复杂,或者设备性能较差的时候,CPU 并不能保证在 16.6ms 内就完成绘制,这里系统又做了一个处理,当正在往 BackBuffer 填充数据时,系统会将 BackBuffer 锁定。如果到了 GPU 交换两个 Buffer 的时间点,你的应用还在往 BackBuffer 中填充数据,会发现 BackBuffer 被锁定了,它会放弃这次交换。
这样做的后果就是手机屏幕仍然显示原先的图像,这就是所谓的掉帧。


优化方向


如果想要屏幕流畅运行,就必须保证 UI 全部的测量,布局和绘制的时间在 16.6ms 内,因为人眼与大脑之间的协作无法感知超过 60fps 的画面更新,也就是 1000 / 60Hz = 16.6ms,也就是说超过 16.6ms 用户就会感知到卡顿。


层级优化


层级越少,View 绘制得就越快,常用有两个方案。



  • 合理使用 RelativeLayout 和 LinearLayout:层级一样优先使用 LinearLayout,因为 RelativeLayout 需要考虑视图之间的相对位置关系,需要更多的计算和更高的系统开销,但是使用 LinearLayout 有时会使嵌套层级变多,这时就应该使用 RelativeLayout。

  • 使用 merge 标签:它会直接将其中的子元素添加到 merge 标签 Parent 中,这样就不会引入额外的层级。它只能用在布局文件的根元素,不能在 ViewStub 中使用 merge 标签,当需要 inflate 的布局本身是由 merge 作为根节点的话,需要将其置于 ViewGroup 中,设置 attachToRoot 为 true。


一个布局可以重复利用,当使用 include 引入布局时,可以考虑 merge 作为根节点,merge 根节点内的布局取决于include 这个布局的父布局。编写 XML 时,可以先用父布局作为根节点,然后完成后再用 merge 替换,方便我们预览效果。


merge_layout.xml


<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="World" />

</merge>

父布局如下:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<include layout="@layout/merge_layout" />

</LinearLayout>

如果需要通过 inflate 引入 merge_layout 布局文件时,可以这样引入:


class MyLinearLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {

init {
LayoutInflater.from(context).inflate(R.layout.merge_layout, this, true)
}
}

第一个参数为 merge 布局文件 id,第二个参数为要将子视图添加到的 ViewGroup,第三个参数为是否将加载好的视图添加到 ViewGroup 中。


需要注意的是,merge 标签的布局,是不能设置 padding 的,比如像这样:


<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="30dp">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="World" />

</merge>

上面的这个 padding 是不会生效的,如果需要设置 padding,可以在其父布局中设置。


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="30dp"
tools:context=".MainActivity">

<include layout="@layout/merge_layout" />

</LinearLayout>

ViewStub


ViewStub 是一个轻量级的 View,一个看不见的,并且不占布局位置,占用资源非常小的视图对象。可以为 ViewStub 指定一个布局,加载布局时,只有 ViewStub 会被初始化,当 ViewStub 被设置为可见或 inflate 时,ViewStub 所指向的布局会被加载和实例化,可以使用 ViewStub 来设置是否显示某个布局。


ViewStub 只能用来加载一个布局文件,且只能加载一次,之后 ViewStub 对象会被置为空。适用于某个布局在加载后就不会有变化,想要控制显示和隐藏一个布局文件的场景,一个典型的场景就是我们网络请求返回数据为空时,往往要显示一个默认界面,表明暂无数据。


view_stub_layout.xml


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">

<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="no data" />

</LinearLayout>

通过 ViewStub 引入


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

<data>

<variable
name="click"
type="com.example.testapp.MainActivity.ClickEvent" />
</data>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{click::showView}"
android:text="show" />

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{click::hideView}"
android:text="hide" />

<ViewStub
android:id="@+id/default_page"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/view_stub_layout" />

</LinearLayout>
</layout>

然后在代码中 inflate,这里通过按钮点击来控制其显示和隐藏。


class MainActivity : AppCompatActivity() {

private var viewStub: ViewStub? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding =
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
binding.click = ClickEvent()
viewStub = binding.defaultPage.viewStub
if (!binding.defaultPage.isInflated) {
viewStub?.inflate()
}
}

inner class ClickEvent {
// 后面 ViewStub 已经回收了,所以只能用 GONE 和 VISIBLE
fun showView(view: View) {
viewStub?.visibility = View.VISIBLE
}

fun hideView(view: View) {
viewStub?.visibility = View.GONE
}
}
}

过度绘制


过度绘制是指屏幕上的某个像素在同一帧的时间内被绘制了多次,在多层次重叠的 UI 结构中,如果不可见的 UI 也在做绘制操作,就会导致某些像素区域被绘制了多次,从而浪费了 CPU 和 GPU 资源。


我们可以打开手机的开发人员选项,打开调试 GPU 过度绘制的开关,就能通过不同的颜色区域查看过度绘制情况。我们要做的,就是尽量减少红色,看到更多的蓝色。



  • 无色:没有过度绘制,每个像素绘制了一次。

  • 蓝色:每个像素多绘制了一次,蓝色还是可以接受的。

  • 绿色:每个像素多绘制了两次。

  • 深红:每个像素多绘制了4次或更多,影响性能,需要优化,应避免出现深红色区域。


优化方法



  • 减少不必要的背景:比如 Activity 往往会有一个默认的背景,这个背景由 DecorView 持有,当自定义布局有一个全屏的背景时,这个 DecorView 的背景对我们来说是无用的,但它会产生一次 Overdraw,可以干掉。


window.setBackgroundDrawable(null)


  • 自定义 View 的优化:在自定义 View 的时候,某个区域可能会被绘制多次,造成过度绘制。可以通过 canvas.clipRect 方法指定绘制区域,可以节约 CPU 与 GPU 资源,在 clipRect 区域之外的绘制指令都不会被执行。


AsyncLayoutInflater


setContentView 函数是在 UI 线程执行的,其中有一系列的耗时动作:XML 的解析,View 的反射创建等过程都是在 UI 线程执行的,AsyncLayoutInflater 就是把这些过程以异步的方式执行,保持 UI 线程的高响应。


implementation 'androidx.asynclayoutinflater:asynclayoutinflater:1.0.0'

class TestActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AsyncLayoutInflater(this).inflate(R.layout.activity_test, null) { view, _, _ ->
setContentView(view)
}
}
}

这样,将 UI 的加载过程迁移到了子线程,保证了 UI 线程的高响应,使用时需要特别注意,调用 UI 一定要等它初始化完成之后,不然可能会产生崩溃。


Compose


Jetpack Compose 相对于传统的 XML 布局方式,具有更强的可组合性,更高的效率和更佳的开发体验,相信未来会成为 Android UI 开发的主流方式。


传统的 XML 布局方式是基于声明式的 XML 代码编写的,使用大量的 XML 标签来描述 UI 结构,XML 文件通过解析和构建生成 View 对象,并将它们添加到 View 树中。在 Compose 中,UI 代码被组织成可组合的函数,每个函数都负责构建某个具体的 UI 元素,UI 元素的渲染是由 Compose 运行时直接管理的,Composable 函数会被调用,以计算并生成当前 UI 状态下的最终视图。


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