注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

网传铁饭碗职业排名,公务员仅排第八!

铁饭碗,顾名思义,饭碗乃铁所铸,坚硬非常,难于击破。人们通常将其意延伸,指一个好的单位或部门,工作稳定,收入无忧。今天我们就来看看网传比较火的铁饭碗职业排名,看看有你感兴趣的职业吗?NO.10 事业单位事业单位工作人员的工资构成。基本工资+绩效工资+津贴补贴+...
继续阅读 »
铁饭碗,顾名思义,饭碗乃铁所铸,坚硬非常,难于击破。人们通常将其意延伸,指一个好的单位或部门,工作稳定,收入无忧。


今天我们就来看看网传比较火的铁饭碗职业排名,看看有你感兴趣的职业吗?

NO.10 事业单位


事业单位工作人员的工资构成。基本工资+绩效工资+津贴补贴+其它工资。基本工资包括岗位工资+薪级工资。


事业单位岗位会按照职责和要求分为专业岗位,职员岗位和工勤技能岗位,专业岗位设13个等级,职员岗位设10个等级,工勤技能岗位设5个等级,每个等级对应不同的工资标准,薪级工资分为专业技术人员和管理人员设置65个薪级,工人设置40个薪级,每个薪级对应一个工资标准。


总的来说事业单位的整体待遇水平略低于公务员。具体的就要分类别说了,比如参公的各项待遇就和公务员完全一致,医疗和教育系统的整体工资待遇还要高于公务员。


推荐专业:财会类、经济类、法学类、汉语言文学类、公共管理类等。


NO.9 教师


随着经济的发展,人们早已经解决温饱问题,教师是非常受欢迎的职业。国家多次高层会议明确教师的薪资不得低于当地国家公务员的薪资水平。


而且对于中小学的制度组建完善之中,教师的绩效工资也有了很大的着落,职称等评价也是大大提高了公平。


所以说以后的教师薪资必然会有一个大步的跨越,完全不低于公务员!


今年,教育部最新开展的“优师计划”更是让师范专业火出圈。从2021年起,教育部每年在全国普通本科招生计划中专门安排1万名左右的优秀教师定向培养专项计划,由教育部直属师范大学和地方师范院校承担招生及培养任务,采取在校学习期间免除学费、免缴住宿费并补助生活费的方式,为832个脱贫县和中西部陆地边境县中小学校定向培养优秀教师。


推荐院校:北京师范大学、华东师范大学、华中师范大学、南京师范大学、湖南师范大学、东北师范大学、华南师范大学等。


NO.8 公务员


公务员一直都是社会传统眼中的铁饭碗,这个无需过多的解释。看看每年的报考人数,录取比例就可以窥探。


不过十八大以来,党内法规制度的笼子越扎越紧。随着公务员社保养老金的并轨、公务员津贴资金的规范、公务员阳光工资的实行,各项法律法规规定了公务员不允许兼职,一旦发现就会被开除公职,而且涨点工资全民反对。


公务员职业逐步走下神坛,成为一份普通的职业,其实我们的工资真的不高。


推荐专业:财经类、法律类、中文类、计算机类、新闻传播类、管理类、金融学类、公安类等


NO.7 国有银行


虽然现在银行业不如以前,但依然强于公务员和大多数国企。


中国银行以及中国建设、工商、交通、农业银行成为五大国企银行,薪资待遇是不低于其它外企的,各项福利齐全,而且上班轻松,基本转正后7000左右一月,房补1000左右,比较稳定(各地具体薪资也可能因地域而有增减)



推荐专业:金融学专业、财务会计专业、审计类专业、计算机类专业等。


NO.6 三大运营商


在中国,三大运营商:移动、联通、电信几乎垄断了所有的移动通讯,他们凭借庞大的用户数量,赚取了很多利润。


薪资待遇方面,新人转正税后15-20万。以中国移动为例,上市公司,全国十亿以上用户,保险、公积金、年终奖齐全。


据悉5年以上的员工年终奖不低于1万5,中秋、春节、端午都差不多是1000元的购物卡,每年给员工交医保3000元等。


推荐专业:计算机类、电子信息类、自动化类、电气类、智能科学、通信类、市场营销类、财务类、法学类、管理类等。


NO.5 国家电网


国家电网作为垄断性质的企业,福利在某些情况下比公务员还要高,当然工资肯定是比公务员高,这非常好理解。


因为他们改制之后就是企业,企业完全按照市场化发工资,就不受政府所谓的规定了。



推荐专业:电气类、通信类、计算机类、工科相关专业等。


NO.4 国家烟草


把烟草局放在第4位,那估计很多人也不会反对。这是因为烟草局的隐形福利非常多。


普通的一个正式工作人员,比公务员多好几千那是非常正常的。所以才有了大家眼中认为,要想进入烟草局那就得靠关系靠金钱铺路。




推荐院校:河南农业大学、云南农业大学、安徽农业大学、山东农业大学、郑州轻工业学院、青岛农业大学、贵州大学。


为什么国家烟草不是第一呢?请看后面三位大佬。


NO.3 高校教师


事业单位改革,高校教师编制被打破,变成聘用合同制,对于高校教师来说更多的是福利。


高校教师作为教师队伍中的高薪群体,本身就编制的依赖程度就会小很多。高校教师由国家教育部发文鼓励他们去兼职,去赚钱。


再加上他们的时间非常宽裕,开补习班、开培训班。那一年下来的收入肯定比公务员更多,当然比烟草,电网也会多,这点毋庸置疑。


NO.2 医院医生


医生原来受制于身份限制,很少有人会去外面兼职。尤其是医生,因为一旦到外面医院“走穴”,被发现那可是要开除出医院。我们都知道医生是个技术活,如果没有一定的手术量,你的技术是不可能锻炼出来的。这就是为什么北京协和、四川华西医院、复旦附属中山医院等医院,这些医生都不愿意离开体制内的一个原因。


但是现在身份打破之后,在政策层面就允许医生自己开诊所。试想一下,如果你是技术过硬,那么去做个手术,手术费从几千到几万都是有的。更甚者,你完全可以自己独立出来,开办诊所。一年赚几十万,是不是非常轻松,这样赚钱就更多了。


所以事业单位改革后,但凡有点名气的医生都不一定要被医院束缚,只要他想,年收入高过高校教师和公务员肯定是没有问题的。


这是因为高校的老师的额外收入更多的是在知识转化成果上,而这个成果不会轻而易举就能获得。总体的难度大于医生。所以排在了医生后面。事业单位改革后,就普通的医护人员工资也会大涨。


因为医院作为重点改革对象,为了调动一线医护人员的工作积极性,为了让更多的人加入医护队伍,公立医院工资改革试点已经启动,包括允许医疗卫生机构突破现行事业单位工资调控水平,以及建立调整机制,切实提高医护人员的薪资水平。




推荐专业:临床医学专业、麻醉学专业、精神医学专业、儿科学专业、口腔医学专业、医学影像学专业、眼视光医学专业、中医学专业等。


NO.1 军队文职


这是因为军队文职招考是这一两年才兴起的“编制”工作。而且由于知道的人较少,报考的人也不多,整体的竞争难度就很低。成功上岸的几率就更大。


重要的一点就是军队文职的各项福利保障都要比公务员的好很多。只要考入军队文职工作岗位,各项工资待遇都是参照同级别军官来执行的。


根据《军事科学院系统工程研究院公开招考文职人员宣讲会》内容可知:军队文职于2020年1月上旬前发布公告,2月中上旬开始报名,3月底前进行笔试,4月底前发布笔试成绩。军队文职招考学历一般为本科以及本科以上,部分专业可放宽至大专学历,往届生可以报考。


招考公告明确指出试用期6个月,应届毕业本科生、硕士生、博士生试用期到手月工资分别约为7200元、7600元、8500元,试用期满后到手月工资分别约为9000元、9500元、11000元(以上均以高校毕业生为例,含住房补贴);此外,科研工作岗位高、中、初职分别享受2500-3000、2000和1000元的科研岗位津贴,六险一金,工资待遇远超公务员。据了解转正以后工资都是一万以上,而且文职人员的住房公积金、住房补贴和房租补贴参照现役军官政策确定的标准执行,符合规定条件的人员,军队可以增发住房补助。


为什么把军队文职放在第一位呢?其实很简单,上面的所有工作,刚入职,工资待遇绝对是没有军队文职高,连中国烟草在军队文职面前都没那么香了。


对于网传排名,你有不同意见吗,欢迎留言分享你的想法!
来源:https://mp.weixin.qq.com/s/LXb5xnqkvLhBBsiLy5kkvQ
收起阅读 »

俄乌战火引发芯片危机!光刻机一核心原料70%产自乌克兰,ASML都坐不住了

没想到,疫情之下本就脆弱的半导体供应链,这回因为俄乌开火雪上加霜了。 不是美日等要制裁俄罗斯,限制半导体出口的问题,而是—— 原材料恐将断供,价格危险了。 关键词是氖气。 这种惰性气体,对于半导体光刻环节至关重要,是大部分主流光刻机“光源”不可或缺的原料之一。...
继续阅读 »

没想到,疫情之下本就脆弱的半导体供应链,这回因为俄乌开火雪上加霜了。


不是美日等要制裁俄罗斯,限制半导体出口的问题,而是——


原材料恐将断供,价格危险了。


关键词是氖气。


这种惰性气体,对于半导体光刻环节至关重要,是大部分主流光刻机“光源”不可或缺的原料之一。


而乌克兰,正是全球最大的氖气出口国,其出口的氖气约占全球市场的70%。


pic_13ac36ea.png

据《科创板日报》报道,国内光刻气体标杆厂商凯美特气表示,氖气目前已涨价:



现在两三天一个价格,价格波动很大。我们通过经销商销售,主要销往国外。且国外企业的报价高于国内。



而面对供应中断风险,光刻机巨头ASML已经表示在寻找“备胎”。


此前,克里米亚局势紧张时期,氖气的价格就曾一度上涨600%。


主流光刻技术重要原料之一

所以,氖气究竟在芯片制造环节起到什么作用?


简单来说,它是光刻机中产生“光源”的一种重要原材料,而光刻机对于光源的波长要求非常高。



波长越短,雕刻出来的电路越细致,芯片制程就越小。



利用稀有气体,不仅能获得波长较短的激光,波长也相对更加稳定。


其中,尤以目前大规模应用的、而且仍占主流地位的DUV深紫外光刻机需要大量氖气。


具体来说,DUV光刻机利用光刻气体来制造光源,这是一种稀有气体与氟的混合气。


常见的光刻气体有氩氟氖混合气、氪氖混合气、氩氖混合气、氩氙氖混合气等等。


制造时,需要利用高压激发这些稀有混合气体,产生电子跃迁,从而产生波长稳定的光线,经过聚合滤波等过程形成光源。



原子核外的电子在发生跃迁的时候,如果是跃迁至低能级,就会释放出光子,将它在增益介质中反复放大能量后,发射出去就形成了一个波长稳定的激光。



" class="reference-link">pic_66801a68.png
△图源维基百科

也就是说,作为缓冲气体,氖气普遍存在于各种激光气体中,用于提供高效的能量。


如在目前DUV光刻机主要采用的ArF(氟化氩)激光器中,氖气就占到了气体混合物的96%以上。


有了优质的光源之后,光刻机才能进一步制造芯片。


具体原理有点类似于我们“投影”的效果,利用光对涂在晶圆表面的“保护膜”(光刻胶)进行去除,这样失去光刻胶的部分就能被化学液体腐蚀并形成电路。


" class="reference-link">pic_fb7a2936.png
△光刻胶对黄光不敏感,光刻间通常用黄光

当然,目前光刻机也在进一步发展中。


从最早用汞灯作为光源,到深紫外(DUV)光刻机、再到下一代极紫外(EUV)光刻机,EUV主要通过锡等离子来产生光源,而ASML等厂商也在尽力发展这项新技术。


ASML寻找备胎,三星英特尔:暂无影响

不仅是氖气,乌克兰出口的氪气和氙气也分别占到了全球供应份额的40%和30%。


而另一种重要半导体材料钯,则有40%来自俄罗斯。


美国芯片制造商美光就表示:俄乌冲突升级凸显出半导体供应链的复杂性和脆弱性。


(p.s. 日本首相已宣布,因乌克兰问题将制裁俄罗斯金融机构,并限制半导体和其他敏感技术出口)


pic_16c41ecd.png

面对市场担忧,多家芯片厂商已经公开对此事做出回应。


目前,三星、SK海力士、英特尔等厂商均表示,受益于多元化材料来源,其芯片生产暂未受到影响。


而全球最大芯片代工厂台积电拒绝在“此时”置评。


这些芯片厂商背后的关键供应商——光刻机巨头ASML则表示:公司正在研究氖气的替代来源。


实际上,2014年克里米亚事件之后,氖气价格就经历过一波大幅上涨。许多公司开始转向中国、美国和加拿大寻求多元化供应。


比如ASML,目前所使用的的氖气中只有不到20%来自冲突地区。


但市场分析仍担忧,俄乌局势对芯片生产的影响将在长期显现。


有来自日本芯片行业的知情人士称:



芯片制造商尚未感受到任何直接影响,但为他们提供半导体制造材料的公司会从俄罗斯和乌克兰购买氖气和钯等。这些材料的供应本来就很紧张,所以任何进一步的供应压力都可能推高他们的价格,进而可能导致芯片价格上涨。



另外,数据显示,国内光刻气体标杆厂商华特气体和凯美特气昨日盘中逆市冲高。


One More Thing

实际上,俄乌局势对科技领域的影响,还不止于芯片。


例如早在物理开火之前,各种DDoS攻击就已经在乌克兰的网络上“先发制人”了。



DDoS是一种网络攻击手段,利用生成大量数据包或请求,导致目标计算机网络或系统资源耗尽,从而使得正常用户无法访问。



一方面,据ZDNet表示,乌克兰的政府网站和银行正在面临DDoS的攻击,包括PrivatBank和 Oschadbank等大型银行也都遭遇了停电问题。


据路透社表示,早在2015年,俄罗斯黑客就被认为对乌克兰进行过网络攻击了,当时大约有22.5万人受到断电影响。


另一方面,网络安全公司ESET发现,乌克兰数百台机器被装上了一种“数据清除病毒”,而且早在过去几个月前就已经存在。


目前,乌克兰政府正在征集黑客志愿者,帮助保护关键的一些基础设施,并对俄罗斯军队实施网络间谍任务。


对此有网友表示,即使有意愿对俄罗斯进行网络攻击,需要考虑的事情也太多了,估计最后也没人愿意参与做这事儿。


pic_f9468e40.png

另外,还有网友总结出了一系列与乌克兰有关的互联网产品。


比如WhatsApp、Paypal的创始人都是乌克兰裔,而Snapchat蒙版技术背后的团队,base敖德萨……


参考链接:
[1]https://www.reuters.com/breakingviews/ukraine-war-flashes-neon-warning-lights-chips-2022-02-24/
[2]https://en.wikipedia.org/wiki/Extreme\_ultraviolet\_lithography
[3]https://zh.wikipedia.org/wiki/%E5%85%89%E5%88%BB%E6%9C%BA
[4]https://zh.wikipedia.org/wiki/%E6%BF%80%E5%85%89
[5]https://www.reuters.com/world/exclusive-ukraine-calls-hacker-underground-defend-against-russia-2022-02-24/
[6]https://twitter.com/sapitonmix/status/1496797920812843015


来源:量子位 | 公众号 QbitAI

收起阅读 »

多普勒,一个“令人发指”的天才,却一生都忙着找工作……

你可能听过“多普勒效应”,却未必知道多普勒本人是个多“令人发指”的天才。22岁时,他花了短短4年,学了拉丁语、法语、意大利语、英语,还有哲学和会计,平时在大学教数学物理,闲了再写写文章作作诗。一切顺风顺水。直到找工作时——他竟然被拒了! 从多普勒效应讲起184...
继续阅读 »

你可能听过“多普勒效应”,却未必知道多普勒本人是个多“令人发指”的天才。22岁时,他花了短短4年,学了拉丁语、法语、意大利语、英语,还有哲学和会计,平时在大学教数学物理,闲了再写写文章作作诗。一切顺风顺水。直到找工作时——他竟然被拒了!


从多普勒效应讲起

1845年,荷兰乌特勒支省一条刚竣工两年的铁路旁,出现了一拨号手。其中一名号手在火车头里,其余的人分布在站台上。火车一边经过,他们一边演奏。


如此热闹的场景,实际上是荷兰科学家Buys Ballot在做实验,因为他对于3年前横空出世的多普勒效应表示不服!


实验结果则是:火车接近号手的时候,车上人听到的号声会高半个调;远离乐队时,又低了半个调。


他大费周章从政府那里借来了火车头,甚至亲自坐进了火车头里听声音,试图推翻这个所谓的多普勒效应。结果不出意外,Buys Ballot亲自体验并证实了多普勒效应确实存在……(来源:[3])


pic_dc5fd38a.png实验中用到的火车头的模型,名为“Hercules”。来源:AN HISTORICAL NOTE DOPPLER RESEARCH IN THE NINETEENTH CENTURY

所谓多普勒效应,指的是如果信号源和接受者之间有相对运动,那么接收端接收到的信号频率将发生变化:相向运动则频率增加,反向运动则频率降低。我们每次听到救护车/警车呼啸而过的“呜啊呜啊”声,都有多普勒效应在起作用。


所以多普勒效应只能用来听个响儿吗?不!公路上的测速雷达,医学上的彩超用的都是这个原理。在宇宙学研究中,多普勒效应也大放异彩,研究遥远天体的运动不再是不可能的事情。它甚至还引出了颠覆人们世界观的理论:著名的“宇宙大爆炸理论”——星系都在互相远离,宇宙处于不断膨胀的状态。


虽然多普勒效应有着极大的知名度,但多普勒本人的经历却鲜有人知道——甚至他的全名都曾被谬传了许久,甚至他的大半生都是在被拒绝中度过的,甚至提出多普勒效应的当天,会场下面只坐了6个人。


pic_3087d6e5.png

多普勒肖像


身体虚弱、头脑发达

多普勒出生于一座极具人文艺术气息的城市——奥地利萨尔茨堡。每年都会有无数游客纷至沓来,漫步于萨尔茨堡的巴洛克式老城中。城堡、大教堂、音乐厅和博物馆一同构成了这个城市的迷人剪影。欧洲最伟大的古典主义音乐家之一莫扎特就是出生于此,因而他的故居也成为了游客们必刷的景点。


游客在参观莫扎特故居时,相当于也在不知不觉中参观了一位大科学家的故居:因为多普勒家就在莫扎特家的隔壁(虽然年代差了那么几十年吧)。


1803年11月29日,在莫扎特故居隔壁的石匠家里,又增添了一名成员——克里斯蒂安·安德烈亚斯·多普勒。


P.S. 在很多传记和记录当中,多普勒的名字都被谬传成了他哥哥的名字“克里斯蒂安·约翰·多普勒”。一传十十传百,就这么一直错了下去。


pic_e1430370.png

多普勒本人也不怎么用他自己的中间名,即使是在正式文件当中——比如布拉格的家庭登记表、结婚证等等上,他一般也只写“多普勒”或者“克里斯蒂安·多普勒”。


pic_b8916740.png

图源丨giphy


作为家中的次子,多普勒本应该接手祖传的石匠手艺,可惜他自幼体弱,并不适合石匠这种需要大量体力劳动的工作。于是父亲就让他在学习这条路上一直走了下去,说不定学成之后还能回来帮着照看下家里的业务。在数学老师极其热情的建议下,1822年多普勒被送到帝国皇家理工学院(即如今的维也纳科技大学),学习数学、力学、物理。


pic_cbe45c66.png

1820~1821年,多普勒在奥地利林兹市的这所学校上学。来源:[1]


在维也纳呆了两年多之后,他于1825年1月回到了萨尔茨堡,决定在学会继续完成正式的教育。多普勒开挂一般的才华和天分在这个阶段体现的淋漓尽致:


他只用了一半的时间就学完了6个学期的课程;随后又花了两年学完了哲学的必修课,还跟着当地商人学商贸和会计;同时他也在学拉丁语、法语、意大利语、英语,后来他的意大利语说得贼溜;他赚生活费的方式则是在圣鲁珀特大学教数学和物理;他还偶尔写诗写文章,拿去投稿发表。


1829年,完成了哲学学习之后,已(刚)然(刚)26岁的多普勒启程去了维也纳。


天才少年接连被拒

之前提过,多普勒在1825年1月回到了萨尔茨堡,住了4年。其实1825年,多普勒曾向维也纳的一位教授申请了高等数学的助理职务。1825年10月,教授在报告中高度评价了多普勒的数学成绩并指出:


“这段时间你一直在萨尔茨堡学别的东西,谁知道你现在数学水平啥样啊!”


换个说法,多普勒被拒了。


这是有记录的多普勒收到的第一封拒信,但这只是他被拒生涯的开端。


1829年6月,多普勒不死心,又一次申请了帝国皇家理工学院高等数学的教授助理。在老师的青睐之下,这回多普勒成功上岗,后来还被允许教授基础数学课赚点外快。


pic_da47fc5d.png1823年的维也纳帝国皇家理工学院。来源:Johann Pezzl’s Neueste Beschreibung von Wien. Sechste Auflage, 1823.

然而到了1833年年初,多普勒眼瞅着就要奔30了,他的助教工期也马上就要结束。怎么办?接着投简历吧!


3月份的时候他就申请了布拉格皇家学校的教职,但后续的消息仿佛石沉大海……


刚好意大利东北部的帝国航海学院在招教授,面试笔试地点也正好就在维也纳理工学院。多普勒兴冲冲跑去参加了,然后6月份的时候又被拒了……


直到10月份离开维也纳,多普勒都没拿到一份offer。随后的1834年成为了他人生的低谷,他不断地申请,不断的被拒。最后不得不委身一家棉纺工厂当会计。


pic_a2406b8d.png1833年左右,棉纺工厂里的场景。来源:[1]

1834年底,在经历了无数次失败后,多普勒动了去美国的心思。他和哥哥一起去了一趟慕尼黑,和美国领事讨论能不能在美国谋到一份工作。为了给美国之行凑钱,多普勒还变卖了自己的大部分财物,连书都卖掉了。


偏偏在最绝望的时候,工作又主动找上门了,而且还是双喜临门——一份是在瑞士伯尔尼的中学里当教授;另一份是布拉格州立中学的教授职位。


尽管瑞士的那份工作工资更高,但他出于对祖国的热爱还是选了布拉格(布拉格当时还属于奥地利帝国)。布拉格这份工作一共有14个人申请,他以全优的成绩通过了所有的考核。他尤其擅长数学问题,他的讲课也被评价为“好,易于理解”。


拿到了offer,多普勒也就放弃了去美国的想法,余生也没离开奥地利帝国。


pic_bab3492a.png1815年时奥地利帝国的版图

1835年,多普勒来到布拉格当上了正式的老师,工资也还算可以,第二年他就娶了媳妇儿生了娃。不过他们夫妻俩都不怎么喜欢布拉格,觉得“不太舒服”。更详细原因后文会提到。总之,多普勒一直在寻找跳槽的机会——他又要面临找工作了!


pic_8423463e.png

多普勒的妻子,萨尔茨堡一个金银匠的女儿Mathilde Sturm。来源:多普勒基金会


1837年5月,他参加了维也纳理工学院高等数学教授的面试和考试,失败;


在1836年4月到1837年8月期间,多普勒还申请了3次,均失败;


在别人的推荐下,多普勒申请成为波西米亚皇家科学学会的会员,差点被拒——但,他以7票赞成5票反对的结果涉险过关,成为了准会员。


为什么这么有才华的多普勒会不断被拒?他的孙子说,爷爷在这种需要竞争的氛围当中,总因缺乏勇气而落于下风。


直到1847年,多普勒盼了十年的离开布拉格的机会终于来了。现如今位于斯洛伐克的矿林大学的数理力学教授职位空缺了出来。当年12月,多普勒奔赴新职位。


艰难的布拉格岁月

在布拉格期间,对多普勒来说可能还有点慰藉的就是学生们送给他的礼物了:一幅平板印刷照片的原本。这就是多普勒流传最广的那张肖像画,上面写着:“布拉格理工学院1839-1840年班,出于感激和尊敬所赠”。


pic_855ec088.png

Austrian National Library, Vienna


上文提到,1835年时32岁的多普勒成为了布拉格州立中学的教授,这也是他的第一份正职工作;他还被委任为布拉格理工学院的候补教授;同年,他还发了3篇论文。


有妻儿陪伴,事业也终于有了起色,但是沉重的工作负担却让他根本开心不起来:他每周要给400名学生上8节课,还得自己批作业。


pic_c71cbee7.png布拉格期间的多普勒发表的第一篇文章。来源:[1]

多普勒还总是担心,自己没法养活一家子7口人。


在拥挤的小教室里给那么多人上课,他本就孱弱的身体一点点地被压垮了。从布拉格开始,多普勒就已肺病缠身,也或许,这要追溯到他年幼时就十分虚弱的体格——不仅将他送上了学术道路,也让他在这条道路上走不了太久。


1841年3月,多普勒被任命为布拉格理工学院的正式教授,他的工作负担进一步加重。


pic_520714fd.png

现在的布拉格理工学院。当年多普勒就是在二层楼给学生们上课。来源:[1]


日益虚弱的他要批阅800名学生的报告,此外在皇家科学学会那边他还有事要做。不过科研则是布拉格少有的能令他开心的事了。他强忍着疾病和疲劳,经常在深夜工作。


闪耀的多普勒效应

想象一下船只在河流中航行的场景:


和顺流的船相比,逆行的船会被浪打更多次。既然这个结论在水波里是成立的,那么为什么不试着把它套用到其他的波上呢?


当年多普勒就是在论文中使用了这样形象的类比,从光的波动理论开始入手。


1842年,多普勒在皇家科学学会的自然科学会议上,公布了自己的著作《关于双星还有天上其他星体的色光问题》。文中提出了“多普勒效应”。随后,他名扬天下。但当时多普勒演讲时,台下只有6名观众,包括一名记录员。


pic_7f4225ca.png当天的会议记录。记录员一开始还把月份错写成了6月,后来划掉改成了5月。来源:[2]

“多普勒理论”并不是靠实验观测得出的,他只做了理论工作。100多年前James Bradley的光行差畸变理论给了他很大的启发(Bradley把视差解释为由于地球上观测者的运动造成的),他在论文中也多次引用。


pic_4f0d8e53.png

多普勒赖以成名的论文


虽然多普勒的理论大体上是对的,对于声波的例证也是对的,但关于星星颜色的解释却并不那么正确。现在看来,多普勒效应对星体发光的影响极为微小,以当时的仪器根本无法测量。


接下来的这几年也是多普勒学术生涯最高产的阶段,而他的呼吸疾病也加重了。医生建议他,“要是不想因为过度消耗气管而挂掉的话,最好还是别讲课了。”但多普勒一直坚持,直到1845年严重的咽喉结核让他难以发声。


1844年夏天,为了能在6月赶去萨尔茨堡治疗自己日益恶化的病症,他的课程提早考试;而严苛的分数也让愤怒的家长们纷纷抗议。无奈之下,学校只能宣布考试成绩无效。


本来这不算多大个丑闻,但对于敏感而内向的多普勒来说,这件事促使他决定尽快离开布拉格。


安享余生?不存在的

1847年离开布拉格以后,多普勒前往矿林大学担任教授(当时属于奥地利帝国的匈牙利,现今属于斯洛伐克)。这次迎接他的不是被拒,而是1848年欧洲革命舞台的匈牙利分会场……


多普勒一入职、就感觉到了匈牙利紧张的政治局势。就在他打算夏天出去避一避的时候,匈牙利人民起义了!


pic_df1466f5.png1848年的革命形势 来源:thinglink.com

据多普勒的儿子所说,革命军的一位司令官Artur von Gargey在布拉格学化学的时候就听说过多普勒的大名,因而慕名邀请多普勒前去畅聊。不过多普勒本人显然不想被卷入政治风暴当中,坚持说会谈的话题必须限制在科学问题以内,绝不触及政治。多普勒还请了一位朋友当作见证人。


那天,三人在震耳的炮声中谈了一夜科学。(来源:[2])


pic_fbbdd07b.png

匈牙利爱国诗人裴多菲·山多尔在广场上向群众们朗读《国民歌》。作者:Mihály Zichy


1848年12月,在革命的炮火声中,他被指任为维也纳理工学院的教授,成功躲了出去。1850年1月,维也纳帝国大学的物理所成立了,多普勒被推为第一任院长。在即将46岁之时,多普勒登上了学术生涯的巅峰。


pic_2e4d9cad.png物理所就安置在这所房子里。后面的花园用来做大型实验,多普勒一家子则住在顶层的公寓。 来源:Austrian National Library

难说学术与病痛,究竟哪个才是多普勒一生的主题。1852年11月,多普勒终于放下工作,到了威尼斯养病,然而为时已晚。


他人生的最后5个月,无人知晓。他的父亲和哥哥,也都死于肺病。


pic_c69f654d.png

多普勒去世的地方。来源:[1]


多普勒无处不在

在多普勒生命的最后几年当中,皇家科学院有一部分人开始攻击多普勒的理论,还有他的名声。但实际上,反对者的猛烈抨击,最终看来反而是对于多普勒理论的最好数学证明。


如今:


多普勒超声检测可以得到人体许多部位的血流信息,如血流的方向、速度和状态等,这对诊断心血管疾病、头部及颈部血流疾病等有重要的临床价值;


激光多普勒测速也已经成为了一种实用技术,是研究流体流动的强力手段;


前些年马航MH370航班失联后的飞行方向,也是用此效应推测出来的。


宇宙中天体的运动状态,也可以用多普勒效应测出来。


1929年,著名天文学家哈勃依据多普勒效应,从星系光谱红移中总结出哈勃定律(但他不愿承认宇宙膨胀)。在哈勃提出哈勃定律后,勒梅特等人很快提出了宇宙应该存在一个开端。


1948年的愚人节,伽莫夫和他的同事们发表了标志着大爆炸宇宙模型的博士论文。20世纪60年代以来,大爆炸理论逐渐被广泛接受,以致被天文学家称为宇宙的“标准模型”。


pic_ac1ba9d2.png《生活大爆炸》中谢耳朵的化妆舞会——多普勒效应的服装

其实多普勒效应还可以用来闯红灯:只要你的车开的足够快,红灯在你眼里就是绿灯!速度要多快呢?大概也就是十分之一个光速吧……


看,闪电侠朝我们跑了过来!啊,他变成了绿灯侠!


参考文献:


[1]Peter Maria Schuster Moving the stars : Christian Doppler, his life, his works and principle, and the world after. [2]Eden A. The Search for Christian Doppler[J]. Isis, 1994(4):1-4. [3]Jonkman E J. Doppler research in the nineteenth century[J]. Ultrasound in Medicine & Biology, 1980, 6(1):1-5. [4]https://en.wikipedia.org/wiki/Christian\_Doppler [5]http://www.visit-salzburg.net/sights/christiandoppler.htm


作者:炖着蘑菇的小鸡

收起阅读 »

不可思议!乌克兰国防军队的系统账号和密码分别是 admin 和 123456!

2020年被用烂大街的密码《2020 最烂密码 TOP 200 曝光!》,500 多万个泄漏密码表明,共有近 3% 的人使用“123456”作为密码。而最近知名黑客网站 Have I Been Pwned 上一个密码“ji32k7au4a83”的使用次数引起了...
继续阅读 »

2020年被用烂大街的密码《2020 最烂密码 TOP 200 曝光!》,500 多万个泄漏密码表明,共有近 3% 的人使用“123456”作为密码。而最近知名黑客网站 Have I Been Pwned 上一个密码“ji32k7au4a83”的使用次数引起了热烈讨论。

Have I Been Pwned 是一个可以查询用户的邮箱是否被泄漏的网站,它的一个密码查询功能 Pwned Passwords 记录着在数据泄露中暴露的 551 509 767 个真实密码,用户可以在这里查询某个密码被使用的次数。比如查询一下 2018 年最烂密码“123456”,得到 23 174 662 次的结果: pic_74d7d762.png

但你知道 个人用户对于自己的密码都是如今谨慎,想必上升到企业层面又或者上升到国家层面,他们的密码应该更复杂……吧?比如我们熟悉的五角大楼,多少黑客视它为黑客安全界的珠穆朗马峰,一生都在想征服它。

有媒体爆料 ,乌克兰武装部队的“第聂伯罗”军事自动化控制系统,服务器网络保护十分原始,账号是admin,密码是123456!

pic_7382c1d5.png

然而,似乎并不是所有国家都是将自己的国防系统看得很重要的,日前乌克兰一名记者披露,乌克兰武装部队的“第聂伯罗”军事自动化控制系统,服务器网络保护十分原始,账号是admin,密码是123456!

“123456、admin”在2017年弱密码TOP 100中,分别位列第一位和第十一位。大多数账户系统在注册时基本禁止使用这种“弱密码”,你很难想象这竟然会成为一个国家军方系统的用户名和密码。

他表示,这个漏洞“让敌人直到2018年夏天,都可以随意扫描乌克兰军队信息”,他展示了此前自动化该控制系统“第聂伯罗”的设置与测试文件。

2018年5月乌克兰网络部队“第聂伯河”数据库专家,迪米特里·弗拉乔克发现,许多服务器通过一个标准的用户名和密码就可以访问,即“admin 123456”。不需要技术很高深的黑客就能够轻松访问交换机、路由器、服务器、打印机和扫描仪等设备,能够分析出武装部队大量的机密信息甚至掌握整个夏天乌克兰军队在顿巴斯地区的一切计划。

pic_504a005d.png

他及时汇报了这个安全隐患,但这个报告很快就忽略了。鉴于事情的严重性,5月26日他将该情况汇报给了国家安全与国防事务委员会以及乌克兰情报局。

等待长达一个多月的时间,乌克兰国防部才给出回应,要求乌克兰国防部以及其它武装力量部门禁止使用弱密码,同时定期检查所有工作站。不过,对于一些IP地址的安全问题,他们认为不需要加强。

可笑的是,在7月12日的测试中发现,一些设备与特定的IP地址使用默认用户名和密码仍然可以登录进去。在一些情况下,计算机能够直接连接到国防部的网络,没有密码就可以进入。

所以,在近四个月的时间里,访问国防部部分服务器和计算机的密码一直是最简单的:admin、123456。

安全专家的建议是,设置密码满足这三点:1、密码长度最好8位或以上;2、密码没有明显的组成规律;3、尽量使用三种以上符号,如“字母+数字+特殊符号”。
你设置密码的时候又有什么小窍门呢

整理:开发者技术前线

收起阅读 »

OAuth2.0原理图解:第三方网站为什么可以使用微信登录

1 文章概述假设小明开发了一个A网站,需要支持微信登陆和淘宝账号登陆。如果你是微信或者淘宝开发人员,你会怎么设计这个功能?本文结合淘宝开放平台官方文档以淘宝账号为例。从最简单视角去思考,用户在网站A输入淘宝用户名和密码,网站A调用淘宝接口校验输入信息,校验通过...
继续阅读 »

1 文章概述

假设小明开发了一个A网站,需要支持微信登陆和淘宝账号登陆。如果你是微信或者淘宝开发人员,你会怎么设计这个功能?本文结合淘宝开放平台官方文档以淘宝账号为例。

从最简单视角去思考,用户在网站A输入淘宝用户名和密码,网站A调用淘宝接口校验输入信息,校验通过则登陆成功,整体流程如下图:


01 第三方登陆简单思路.jpg


上述思路存在什么问题?最显著问题就是信息安全问题。问题第一个方面是用户需要将淘宝用户名和密码输入网站A,这样会带来用户名和密码泄露风险。问题第二个方面是如果用户不信任网站A,那么也不会输入淘宝用户名和密码,影响网站A业务开展。


2 OAuth2.0

第三方登陆信息安全问题应该如何解决?OAuth是一种流行标准。如果执行这行这个标准,那么用户可以在不告知A网站淘宝用户名和密码情况下,使用淘宝账号登陆A网站。

目前已经发展到OAuth2.0版本,相较于1.0版本更加关注客户端开发者简易性,而且为桌面应用、web应用、手机设备提供专门认证流程。


2.1 四种角色

OAuth2.0标准定义了四种角色:

  • 客户端(Client)
  • 资源所有者(Resource Owner)
  • 资源服务器(Resource Server)
  • 授权服务器(Authorization Server)

四种角色交互流程:

02 OAuth2_四种角色_01.jpg

本文场景对应四种角色:

02 OAuth2_四种角色_02.jpg


2.2 四种模式

OAuth2.0标准定义了以下四种授权模式:

  • 授权码模式(authorization code)
  • 隐式模式(implicit)
  • 密码模式(password)
  • 客户端模式(client credentials)

四种授权模式中最常用的是授权码模式,例如微信开发平台文档介绍对于网站应用微信OAuth2.0授权登录目前支持授权码模式,所以本文只介绍授权码模式,后续文章会详细比较四种模式。


2.3 实现流程

第一个流程是创建应用,A网站开发者首先去淘宝开放平台创建应用,开放平台会生成一个client_id作为A网站唯一标识。

第二个流程是授权流程,用户在A网站点击使用淘宝账号登陆时,实际上跳转至A网站拼接授权URL页面,这个页面由淘宝提供。用户在授权页面输入淘宝用户名和密码,校验成功后跳转至A网站回调地址,这时A网站会拿到一个code,后台再使用code去获取access_token。

第三个流程是获取信息,获取到access_token相当于获取到一把钥匙,再按照规范调用淘宝对外提供接口就可以获取到用户数据。


03 oauth2_整体流程.jpg


2.4 为什么安全

第一个方面A网站开发人员需要在淘宝开放平台进行申请,需要输入个人信息或者公司信息,这样A网站可靠性有了一定程度保证。

第二个方面在第一章节方案用户需要在A网站输入淘宝用户名和密码,但是在OAuth2.0方案2.4步骤虽然也要输入淘宝用户名密码,但是这个页面由淘宝官方提供,安全性得到了保证。

第三个方面access_token(令牌)并没有在浏览器中传递,而是需要A网站在获取到code之后去后台程序换取,避免了钥匙泄露风险。

第四个方面code(授权码)在浏览器传递有一定风险,但是具有两个特性一定程度保证了安全:

(1) code具有效期,超期未使用需要重新按授权流程获取

(2) code只能使用一次,使用后需要重新按授权流程获取


3 OpenID Connect

3.1 授权与认证

在第二章节详细分析了OAuth2.0协议,在实现流程章节分析了创建应用、授权流程、获取信息三个流程,我们发现一个问题:客户端在获取到令牌之后,还需要调用资源服务器接口获取用户信息,有没有一种协议可以在返回令牌时同时将用户是谁返回呢?回答这个问题之前首先对比一组概念:授权与认证。

授权关注通信实体具有什么权限,认证关注通信实体是谁。OAuth2.0只有授权流程,返回令牌之后授权流程已经完成,OpenID connect在此基础上进行了扩展,这样客户端能够通过认证来识别用户。


3.2 三种角色

OpenID Connect定义了三种角色:

  • 最终用户(End User)
  • 依赖方(Relying Party)
  • 身份认证提供商(Identity Provider)

三种角色交互流程:

04 OIDC_三种角色_01.jpg

本文场景对应三种角色:

04 OIDC_三种角色_02.jpg


3.3 整体流程

05 OIDC_整体流程.jpg


4 相关文档

淘宝开放平台用户授权介绍

网站应用微信登录开发指南


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

Java线程池必知必会

1、线程数使用开发规约阿里巴巴开发手册中关于线程和线程池的使用有如下三条强制规约【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。正例:自定义线程工厂,并且根据外部特征进行分组,比如,来自同一机房的调用,把机房编号赋值给whatFeatureO...
继续阅读 »

1、线程数使用开发规约

阿里巴巴开发手册中关于线程和线程池的使用有如下三条强制规约

【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。

正例:自定义线程工厂,并且根据外部特征进行分组,比如,来自同一机房的调用,把机房编号赋值给whatFeatureOfGroup


public class UserThreadFactory implements ThreadFactory {

private final String namePrefix;

private final AtomicInteger nextId = new AtomicInteger(1);

/**

* 定义线程组名称,在利用 jstack 来排查问题时,非常有帮助

*/


UserThreadFactory(String whatFeatureOfGroup) {

namePrefix = "From UserThreadFactory's " + whatFeatureOfGroup + "-";

}

@Override

public Thread newThread(Runnable task) {

String name = namePrefix + nextId.getAndIncrement();

Thread thread = new Thread(null, task, name, 0);

System.out.println(thread.getName());

return thread;

}

}

【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。

说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。

如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这

样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors 返回的线程池对象的弊端如下:

1) FixedThreadPool 和 SingleThreadPool:

允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

2) CachedThreadPool:

允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

2、 ThreadPoolExecutor源码

1. 构造函数

UML图: image.png ThreadPoolExecutor的构造函数共有四个,但最终调用的都是同一个:

image.png

2.核心参数

  1. corePoolSize => 线程池核心线程数量

  2. maximumPoolSize => 线程池最大数量

  3. keepAliveTime => 线程池的工作线程空闲后,保持存活的时间。如果任务多而且任务的执行时间比较短,可以调大keepAliveTime,提高线程的利用率。

  4. unit => 时间单位

  5. workQueue => 线程池所使用的缓冲队列,队列类型有:

    • ArrayBlockingQueue,基于数组结构的有界阻塞队列,按FIFO(先进先出)原则对任务进行排序。使用该队列,线程池中能创建的最大线程数为maximumPoolSize

    • LinkedBlockingQueue,基于链表结构的无界阻塞队列,按FIFO(先进先出)原则对任务进行排序,吞吐量高于ArrayBlockingQueue。使用该队列,线程池中能创建的最大线程数为corePoolSize。静态工厂方法 Executor.newFixedThreadPool()使用了这个队列。

    • SynchronousQueue,一个不存储元素的阻塞队列。添加任务的操作必须等到另一个线程的移除操作,否则添加操作一直处于阻塞状态。静态工厂方法 Executor.newCachedThreadPool()使用了这个队列。

    • PriorityBlokingQueue:一个支持优先级的无界阻塞队列。使用该队列,线程池中能创建的最大线程数为corePoolSize。

  6. threadFactory => 线程池创建线程使用的工厂

  7. handler => 线程池对拒绝任务的处理策略,主要有4种类型的拒绝策略:

    • AbortPolicy:无法处理新任务时,直接抛出异常,这是默认策略。

    • CallerRunsPolicy:用调用者所在的线程来执行任务。

    • DiscardOldestPolicy:丢弃阻塞队列中最靠前的一个任务,并执行当前任务。

    • DiscardPolicy:直接丢弃任务。

3.execute()方法

image.png

  1. 如果当前运行的线程少于corePoolSize,则创建新的工作线程来执行任务(执行这一步骤需要获取全局锁)。

  2. 如果当前运行的线程大于或等于corePoolSize,而且BlockingQueue未满,则将任务加入到BlockingQueue中。

  3. 如果BlockingQueue已满,而且当前运行的线程小于maximumPoolSize,则创建新的工作线程来执行任务(执行这一步骤需要获取全局锁)。

  4. 如果当前运行的线程大于或等于maximumPoolSize,任务将被拒绝,并调用RejectExecutionHandler.rejectExecution()方法。即调用饱和策略对任务进行处理。

3、线程池的工作流程

image.png

image.png

执行逻辑说明:

  1. 判断核心线程数是否已满,核心线程数大小和corePoolSize参数有关,未满则创建线程执行任务

  2. 若核心线程池已满,判断队列是否满,队列是否满和workQueue参数有关,若未满则加入队列中

  3. 若队列已满,判断线程池是否已满,线程池是否已满和maximumPoolSize参数有关,若未满创建线程执行任务

  4. 若线程池已满,则采用拒绝策略处理无法执执行的任务,拒绝策略和handler参数有关

4、Executors创建返回ThreadPoolExecutor对象(不推荐)

Executors创建返回ThreadPoolExecutor对象的方法共有三种:

1. Executors#newCachedThreadPool => 创建可缓存的线程池

  • corePoolSize => 0,核心线程池的数量为0

  • maximumPoolSize => Integer.MAX_VALUE,可以认为最大线程数是无限的

  • keepAliveTime => 60L

  • unit => 秒

  • workQueue => SynchronousQueue

弊端:maximumPoolSize => Integer.MAX_VALUE可能会导致OOM

2. Executors#newSingleThreadExecutor => 创建单线程的线程池

SingleThreadExecutor是单线程线程池,只有一个核心线程:

  • corePoolSize => 1,核心线程池的数量为1

  • maximumPoolSize => 1,只可以创建一个非核心线程

  • keepAliveTime => 0L

  • unit => 毫秒

  • workQueue => LinkedBlockingQueue

弊端:LinkedBlockingQueue是长度为Integer.MAX_VALUE的队列,可以认为是无界队列,因此往队列中可以插入无限多的任务,在资源有限的时候容易引起OOM异常

3. Executors#newFixedThreadPool => 创建固定长度的线程池

  • corePoolSize => 1,核心线程池的数量为1

  • maximumPoolSize => 1,只可以创建一个非核心线程

  • keepAliveTime => 0L

  • unit => 毫秒

  • workQueue => LinkedBlockingQueue

它和SingleThreadExecutor类似,唯一的区别就是核心线程数不同,并且由于使用的是LinkedBlockingQueue,在资源有限的时候容易引起OOM异常

5、线程池的合理配置

从以下几个角度分析任务的特性:

  1. 任务的性质:CPU 密集型任务、IO 密集型任务和混合型任务。

  2. 任务的优先级:高、中、低。

  3. 任务的执行时间:长、中、短。

  4. 任务的依赖性:是否依赖其他系统资源,如数据库连接。

任务性质不同的任务可以用不同规模的线程池分开处理。可以通过 Runtime.getRuntime().availableProcessors()方法获得当前设备的 CPU 个数。

  • CPU 密集型任务:配置尽可能小的线程,如配置 cpu核心数+1 个线程的线程池。

  • IO 密集型任务 :由于线程并不是一直在执行任务,则配置尽可能多的线程,如2 ∗ Ncpu

  • 混合型任务:如果可以拆分,则将其拆分成一个 CPU 密集型任务和一个 IO 密集型任务。只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率;如果这两个任务执行时间相差太大,则没必要进行分解。

优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 来处理,它可以让优先级高的任务先得到执行。但是,如果一直有高优先级的任务加入到阻塞队列中,那么低优先级的任务可能永远不能执行。

执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。

依赖数据库连接池的任务,因为线程提交 SQL 后需要等待数据库返回结果,线程数应该设置得较大,这样才能更好的利用 CPU。

建议使用有界队列,有界队列能增加系统的稳定性和预警能力。可以根据需要设大一点,比如几千。使用无界队列,线程池的队列就会越来越大,有可能会撑满内存,导致整个系统不可用。

处理拒绝策略有以下几种比较推荐:

在程序中捕获RejectedExecutionException异常,在捕获异常中对任务进行处理。针对默认拒绝策略使用CallerRunsPolicy拒绝策略,该策略会将任务交给调用execute的线程执行【一般为主线程】,此时主线程将在一段时间内不能提交任何任务,从而使工作线程处理正在执行的任务。此时提交的线程将被保存在TCP队列中,TCP队列满将会影响客户端,这是一种平缓的性能降低自定义拒绝策略,只需要实现RejectedExecutionHandler接口即可如果任务不是特别重要,使用DiscardPolicy和DiscardOldestPolicy拒绝策略将任务丢弃也是可以的如果使用Executors的静态方法创建ThreadPoolExecutor对象,可以通过使用Semaphore对任务的执行进行限流也可以避免出现OOM异常。

6、拒绝策略

有以下几种比较推荐:

  • 在程序中捕获RejectedExecutionException异常,在捕获异常中对任务进行处理。针对默认拒绝策略

  • 使用CallerRunsPolicy拒绝策略,该策略会将任务交给调用execute的线程执行【一般为主线程】,此时主线程将在一段时间内不能提交任何任务,从而使工作线程处理正在执行的任务。此时提交的线程将被保存在TCP队列中,TCP队列满将会影响客户端,这是一种平缓的性能降低

  • 自定义拒绝策略,只需要实现RejectedExecutionHandler接口即可

  • 如果任务不是特别重要,使用DiscardPolicy和DiscardOldestPolicy拒绝策略将任务丢弃也是可以的如果使用Executors的静态方法创建ThreadPoolExecutor对象,可以通过使用Semaphore对任务的执行进行限流也可以避免出现OOM异常。

  • 参考文章:8大拒绝策略

7、线程池的五种运行状态

线程状态:

image.png

不同于线程状态,线程池也有如下几种 状态:

image.png

• RUNNING :该状态的线程池既能接受新提交的任务,又能处理阻塞队列中任务。

• SHUTDOWN:该状态的线程池不能接收新提交的任务,但是能处理阻塞队列中的任务。(政府服务大厅不在允许群众拿号了,处理完手头的和排队的政务就下班)


处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。

注意:finalize() 方法在执行过程中也会隐式调用shutdown()方法。

• STOP:该状态的线程池不接受新提交的任务,也不处理在阻塞队列中的任务,还会中断正在执行的任务。(政府服务大厅不再进行服务了,拿号、排队、以及手头工作都停止了。)


在线程池处于 RUNNINGSHUTDOWN 状态时,调用shutdownNow() 方法会使线程池进入到该状态;

• TIDYING:如果所有的任务都已终止,workerCount (有效线程数)=0。


线程池进入该状态后会调用 terminated() 钩子方法进入TERMINATED 状态。

• TERMINATED:在terminated()钩子方法执行完后进入该状态,默认terminated()钩子方法中什么也没有做。


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

算法题每日一练---第37天:打家劫舍

一、问题描述你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装...
继续阅读 »

一、问题描述

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

题目链接:打家劫舍

二、题目要求

样例1

输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4

样例2

输入: [2,7,9,3,1]
输出: 12
解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。

数据范围

  • 1 <= nums.length <= 100`
  • 0 <= nums[i] <= 400`

考察

1.动态规划中等题型
2.建议用时5~15min

三、问题分析

这也是一道比较典型的动态规划问题,动态规划没做过的可以看这一篇入门题解:

算法题每日一练---第34天: 青蛙跳台阶

还是用我们的三步走,老套路:

第一步含义搞懂:

首先,使用动态规划一维数组就可以解决问题,那么这个dp[i]到底代表什么?

看看题目问什么,在不触犯警报的情况下,偷到的最大金额数,那么dp[i]就代表从截止到第i个房子,最大的金额数。

第二步变量初始:

假如房子数目为1,那么dp[0]=nums[0]
假如房子数目为2,那么dp[1]=max(nums[0],nums[1])

第三步规律归纳:

那么到底有什么规律呢?我把样例2详细列出来你看一下:

打家劫舍.gif

从第三个数开始,dp[i]是不是满足

dp[i]=max(dp[i−2]+nums[i],dp[i−1])关系式

三步走,打完收工!

四、编码实现

#include<iostream>
#include<algorithm>
using namespace std;
int main()
{
int n,nums[105],i,dp[105];//初始化
cin>>n;//输入数组的大小,n为0或1力扣要判断的,我这里省去了
for(i=1;i<=n;i++)//输入数组的元素
cin>>nums[i];
dp[1]=nums[1],dp[2]=max(nums[1],nums[2]);//初始化动态规划前两位
for(i=3;i<=n;i++)//第三位开始循环
{
dp[i]=max(dp[i-1],nums[i]+dp[i-2]);//找到规律
}
cout<<dp[n];//输出结果
return 0;
}

五、测试结果

2.png


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

Flutter - 这么炫酷的App你见过吗??

前言:今天是1024,先祝各位兄弟们节日快乐,永不脱发,永无Bug😜。说正事:在前几天,我发现了一个动画特别炫酷的一个Flutter项目,一款习惯养成类的App,看了后就真的是爱不释手,功能很丰富,所以我立刻找到了开源作者,向他申请了写作权限。然后开始了对项目...
继续阅读 »

前言:今天是1024,先祝各位兄弟们节日快乐,永不脱发,永无Bug😜。说正事:在前几天,我发现了一个动画特别炫酷的一个Flutter项目,一款习惯养成类的App,看了后就真的是爱不释手,功能很丰富,所以我立刻找到了开源作者,向他申请了写作权限。然后开始了对项目的分析(求个赞!!!相信我,看完这篇你会有收获的👍)

我对他项目的代码进行了部分修改,修改的源代码在文章最后~

开源项目地址:github.com/designDo/fl…

先上效果图:

tt0.top-432794.gif tt0.top-795301.gif

还有很多的功能大家自己下载源码(觉得好的话给开源作者点个star哦,人家不容易!)

本文分析重点:

  • 登录界面的动画、输入框处理以及顶部弹出框
  • 底部导航栏的动画处理
  • 首页动画以及环形进度条处理
  • 适配深色模式(分析一下作者的全局状态管理)

1.登录界面的动画、输入框处理以及顶部弹出框

  • 动画处理

    这里一共有3处动画,输入框的缩放动画,验证码按钮的平移动画,登录界面的缩放动画。

    当我们使用动画时,我们需要定义一个Controller来控制管理动画

    AnimationController _animationController;

    当然使用动画时我们的State是需要混入SingleTickerProviderStateMixin这个类的

    在效果图中我们也不难看出动画直接是有时间间距的,所以我们整个界面仅用一个Controller来控制,使其从上到下逐步显示。

    关于缩放动画呢,在flutter我们需要使用ScaleTransition,其中最重要的一点便是:

    Animation<double> scale //控制widget缩放

    来看看详细使用:

    ScaleTransition(
    //控制缩放从0到1
    scale: Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(
    //控制动画的Controller
    parent: _animationController,
    //0,0.3是动画运行的时间
    //curve用来描述曲线运动的动画
    curve: Interval(0, 0.3, curve: Curves.fastOutSlowIn),
    )),
    child:...
    )

    这里关于其他动画也差不多,区别就在于动画和动画的运行时间

    关键区别:

    验证码的输入框:

    curve: Interval(0.3, 0.6, curve: Curves.fastOutSlowIn),

    获取验证码按钮:

    这里主要区别是position用于处理初始时的绝对位置

    SlideTransition(
    //大家可以将begin: Offset(2, 0)的数据更改,这样就会清晰的体验到它的功能
    position: Tween<Offset>(begin: Offset(2, 0), end: Offset.zero)
    .animate(CurvedAnimation(
    parent: _animationController,
    curve:
    Interval(0.6, 0.8, curve: Curves.fastOutSlowIn))),child:...)

    登录按钮:

    ScaleTransition(
    scale: Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(
    parent: _animationController,
    curve: Interval(0.8, 1, curve: Curves.fastOutSlowIn),
    )),child:...)

    关于动画的实现就是这样,是不是非常的简单~

  • 手机号输入框的限制处理

登录输入框处理.png

我觉得这个样式很炫酷,主要是在平时不是很常见,就分析一下

这里我们封装了一个CustomEditField输入框,可以更好的做动画的处理

动画定义

///文本内容
String _value = '';
TextEditingController editingController;
AnimationController numAnimationController;
Animation<double> numAnimation;

且该组件需要混入(Mixin)TickerProviderStateMixin与AutomaticKeepAliveClientMixin,因为AnimationController需要调用TickerProvider里的createTicker方法(感兴趣可以查看flutter源码)

with TickerProviderStateMixin, AutomaticKeepAliveClientMixin

初始化时:

@override
void initState() {
_value = widget.initValue;
//初始化controller
editingController = TextEditingController(text: widget.initValue);
//初始化限制框的控制器与动画
numAnimationController =
AnimationController(duration: Duration(milliseconds: 500), vsync: this);
numAnimation = CurvedAnimation(
parent: numAnimationController, curve: Curves.easeOutBack);
if (widget.initValue.length > 0) {
numAnimationController.forward(from: 0.3);
}
super.initState();
}

销毁时:

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

UI: 使用Stack用于包裹一个输入框和限制框

Stack(
children:[
TextField(),
//限制框的动画,所以在外面套一层ScaleTransition
ScaleTransition(
child:Padding()
)
]
)

使用这个封装的组件时,我们主要处理numDecoration

此处的颜色为全局管理的处理,直接复制该代码需要修改

numDecoration: BoxDecoration(
shape: BoxShape.rectangle,
color: AppTheme.appTheme.cardBackgroundColor(),
borderRadius: BorderRadius.all(Radius.circular(15)),
boxShadow: AppTheme.appTheme.containerBoxShadow()),
numTextStyle: AppTheme.appTheme
.themeText(fontWeight: FontWeight.bold, fontSize: 15),
  • 顶部弹出框的处理

1634777618(1).png

使用了flash这个插件,一个高度可定制、功能强大且易于使用的警告框

为了代码的复用,在这里进行了封装处理

class FlashHelper {
static Future<T> toast<T>(BuildContext context, String message) async {
return showFlash<T>(
context: context,
//显示两秒
duration: Duration(milliseconds: 2000),
builder: (context, controller) {
//弹出框
return Flash.bar(
margin: EdgeInsets.only(left: 24, right: 24),
position: FlashPosition.top,
brightness: AppTheme.appTheme.isDark()
? Brightness.light
: Brightness.dark,
backgroundColor: Colors.transparent,
controller: controller,
child: Container(
alignment: Alignment.center,
padding: EdgeInsets.all(16),
height: 80,
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: BorderRadius.all(Radius.circular(16)),
gradient: AppTheme.appTheme.containerGradient(),
boxShadow: AppTheme.appTheme.coloredBoxShadow()),
child: Text(
//显示的文字
message,
style: AppTheme.appTheme.headline1(
textColor: Colors.white,
fontWeight: FontWeight.normal,
fontSize: 16),
),
));
});
}
}

2.底部导航栏的动画处理

tt0.top-150276.gif

这里真的是惊艳到我了,Icon都是画出来的,作者真的是脑洞大开,点赞!

  • Icon的绘制

    房子:

static final home = FluidFillIconData([
//房子
ui.Path()..addRRect(RRect.fromLTRBXY(-10, -2, 10, 10, 2, 2)),
ui.Path()
..moveTo(-14, -2)
..lineTo(14, -2)
..lineTo(0, -16)
..close(),
]);

四个正方形:

static final window = FluidFillIconData([
//正方形
ui.Path()..addRRect(RRect.fromLTRBXY(-12, -12, -2, -2, 2, 2)),
ui.Path()..addRRect(RRect.fromLTRBXY(2, -12, 12, -2, 2, 2)),
ui.Path()..addRRect(RRect.fromLTRBXY(-12, 2, -2, 12, 2, 2)),
ui.Path()..addRRect(RRect.fromLTRBXY(2, 2, 12, 12, 2, 2)),
]);

趋势图:

static final progress = FluidFillIconData([
//趋势图
ui.Path()
..moveTo(-10, -10)
..lineTo(-10, 8)
..arcTo(Rect.fromCircle(center: Offset(-8, 8), radius: 2), -1 * math.pi,
-0.5 * math.pi, true)
..moveTo(-8, 10)
..lineTo(10, 10),
ui.Path()
..moveTo(-6.5, 2.5)
..lineTo(0, -5)
..lineTo(4, 0)
..lineTo(10, -9),
]);

我的:

static final user = FluidFillIconData([
//我的
ui.Path()..arcTo(Rect.fromLTRB(-5, -16, 5, -6), 0, 1.9 * math.pi, true),
ui.Path()..arcTo(Rect.fromLTRB(-10, 0, 10, 20), 0, -1.0 * math.pi, true),
]);

大佬的思路就是强👍

  • 切换时的波浪动画

    这里主要是两个部分,一个是点击切换时的波浪动画,一个是动画结束后的凹凸效果

    这样的效果我们需要通过CustomPainter来进行绘制

    我们需要定义一些参数(指展示最重要的)

    final double _normalizedY;final double _x;

    然后进行绘制

 @override
void paint(canvas, size) {
// 使用基于“_normalizedY”值的各种线性插值绘制两条三次bezier曲线
final norm = LinearPointCurve(0.5, 2.0).transform(_normalizedY) / 2;
final radius = Tween<double>(
begin: _radiusTop,
end: _radiusBottom
).transform(norm);
// 当动画结束后的凹凸效果
final anchorControlOffset = Tween<double>(
begin: radius * _horizontalControlTop,
end: radius * _horizontalControlBottom
).transform(LinearPointCurve(0.5, 0.75).transform(norm));
final dipControlOffset = Tween<double>(
begin: radius * _pointControlTop,
end: radius * _pointControlBottom
).transform(LinearPointCurve(0.5, 0.8).transform(norm));


final y = Tween<double>(
begin: _topY,
end: _bottomY
).transform(LinearPointCurve(0.2, 0.7).transform(norm));
final dist = Tween<double>(
begin: _topDistance,
end: _bottomDistance
).transform(LinearPointCurve(0.5, 0.0).transform(norm));
final x0 = _x - dist / 2;
final x1 = _x + dist / 2;

//绘制工程
final path = Path()
..moveTo(0, 0)
..lineTo(x0 - radius, 0)
..cubicTo(x0 - radius + anchorControlOffset, 0, x0 - dipControlOffset, y, x0, y)
..lineTo(x1, y) //背景的宽高
..cubicTo(x1 + dipControlOffset, y, x1 + radius - anchorControlOffset, 0, x1 + radius, 0)
//背景的宽高
..lineTo(size.width, 0)
..lineTo(size.width, size.height)
..lineTo(0, size.height);

final paint = Paint()
..color = _color;

canvas.drawPath(path, paint);
}

@override
bool shouldRepaint(_BackgroundCurvePainter oldPainter) {
return _x != oldPainter._x
|| _normalizedY != oldPainter._normalizedY
|| _color != oldPainter._color;
}

这样带波浪动画的背景就完成啦~

  • 按钮的弹跳动画

    其实实现方式与波浪动画相同,也是通过CustomPainter来进行绘制

    (只展示核心代码)

//绘制其他无状态的按钮
final paintBackground = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.4
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..color = AppTheme.iconColor;
//绘制点击该按钮时的颜色
final paintForeground = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.4
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..color = AppTheme.appTheme.selectColor();

Icon的背景以及跳跃我们需要定义AnimationController与Animation,进行跳跃动画的绘制

在初始化时处理动画

@override
void initState() {
_animationController = AnimationController(
duration: const Duration(milliseconds: 1666),
reverseDuration: const Duration(milliseconds: 833),
vsync: this);
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(_animationController)
..addListener(() {
setState(() {
});
});
_startAnimation();

super.initState();
}
final offsetCurve = _selected ? ElasticOutCurve(0.38) : Curves.easeInQuint;
final scaleCurve = _selected ? CenteredElasticOutCurve(0.6) : CenteredElasticInCurve(0.6);

final progress = LinearPointCurve(0.28, 0.0).transform(_animation.value);

final offset = Tween<double>(
begin: _defaultOffset,
end: _activeOffset
).transform(offsetCurve.transform(progress));
final scaleCurveScale = 0.50;
final scaleY = 0.5 + scaleCurve.transform(progress) * scaleCurveScale + (0.5 - scaleCurveScale / 2);

用于控制动画的运行与销毁:

@override
void didUpdateWidget(oldWidget) {
setState(() {
_selected = widget._selected;
});
_startAnimation();
super.didUpdateWidget(oldWidget);
}

void _startAnimation() {
if (_selected) {
_animationController.forward();
} else {
_animationController.reverse();
}
}

ui布局:

return GestureDetector(
onTap: _onPressed,
behavior: HitTestBehavior.opaque,
child: Container(
constraints: BoxConstraints.tight(ne),
alignment: Alignment.center,
child: Container(
margin: EdgeInsets.all(ne.width / 2 - _radius),
constraints: BoxConstraints.tight(Size.square(_radius * 2)),
decoration: ShapeDecoration(
color: AppTheme.appTheme.cardBackgroundColor(),
shape: CircleBorder(),
),
transform: Matrix4.translationValues(0, -offset, 0),
//Icon的绘制
child: FluidFillIcon(
_iconData,
LinearPointCurve(0.25, 1.0).transform(_animation.value),
scaleY,
),
),
),
);

这样底部导航栏就完成啦!

3.首页动画以及环形进度条处理

  • 首页整体列表动画处理

    这一部分数据是最为复杂的

    与其他动画相同,我们需要一个controller来控制,在此页面,我们还需要一个List来存放数据

    final AnimationController mainScreenAnimationController;
    final Animation<dynamic> mainScreenAnimation;
    final List<Habit> habits;

    数据存储在此文章暂时不分析,大家可以自己运行源码~

    初始化动画:

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

因为使用到动画的组件很多,所以我们根节点使用AnimatedBuilder,主要使用的动画FadeTransition与Transform,做法于上面相同,在此就不多赘述了。

  • 环形进度条

    我们封装了一个CircleProgressBar用户绘制圆形进度条

    这部分的ui很简单,主要是动画的绘制较为复杂

屏幕截图 2021-10-23 140905.jpg

ui:

return AspectRatio(
aspectRatio: 1,
child: AnimatedBuilder(
animation: this.curve,
child: Container(),
builder: (context, child) {
final backgroundColor =
this.backgroundColorTween?.evaluate(this.curve) ??
this.widget.backgroundColor;
final foregroundColor =
this.foregroundColorTween?.evaluate(this.curve) ??
this.widget.foregroundColor;

return CustomPaint(
child: child,
//重点是这个封装组件,这里是圆形里面的进度条
foregroundPainter: CircleProgressBarPainter(
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
percentage: this.valueTween.evaluate(this.curve),
strokeWidth: widget.strokeWidth
),
);
},
),
);

详细的绘制:

@override
void paint(Canvas canvas, Size size) {
final Offset center = size.center(Offset.zero);
final Size constrainedSize =
size - Offset(this.strokeWidth, this.strokeWidth);
final shortestSide =
Math.min(constrainedSize.width, constrainedSize.height);
final foregroundPaint = Paint()
..color = this.foregroundColor
..strokeWidth = this.strokeWidth
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;
final radius = (shortestSide / 2);

// Start at the top. 0 radians represents the right edge
final double startAngle = -(2 * Math.pi * 0.25);
final double sweepAngle = (2 * Math.pi * (this.percentage ?? 0));

// Don't draw the background if we don't have a background color
if (this.backgroundColor != null) {
final backgroundPaint = Paint()
..color = this.backgroundColor
..strokeWidth = this.strokeWidth
..style = PaintingStyle.stroke;
canvas.drawCircle(center, radius, backgroundPaint);
}

canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle,
false,
foregroundPaint,
);
}

这里还有一个很实用的功能:

时间定义和欢迎词

屏幕截图 2021-10-23 142038.jpg

这个demo包含了大部分对时间的处理

屏幕截图 2021-10-23 142440.jpg 例如:

///根据当前时间获取,[monthIndex]个月的开始结束日期
static Pair<DateTime> getMonthStartAndEnd(DateTime now, int monthIndex) {
DateTime start = DateTime(now.year, now.month - monthIndex, 1);
DateTime end = DateTime(now.year, now.month - monthIndex + 1, 0);
return Pair<DateTime>(start, end);
}

强烈推荐大家学习,开发中比较常用!

关于此app的大部分动画ui都分析完成了,其他都是在复用,大家觉得还不错的话可以自己下载体验一下,养成好习惯~

4.适配深色模式(分析一下作者的全局状态管理)

作者在这里使用了Bloc用于状态管理

///  theme mode
enum AppThemeMode {
Light,
Dark,
}
///字体模式
enum AppFontMode {
///默认字体
Roboto,
///三方字体
MaShanZheng,
}
///颜色模式,特定view背景颜色
enum AppThemeColorMode {
Indigo, Orange, Pink, Teal, Blue, Cyan, Purple }

在此基础上,定义了颜色,样式,例如:

String fontFamily(AppFontMode fontMode) {
switch (fontMode) {
case AppFontMode.MaShanZheng:
return 'MaShanZheng';
}
return 'Roboto';
}

然后在使用样式时多用三元判断,这样就很简单的实现了状态管理

这样对这个项目的ui已经动画就分析完成了,大家也可以通过这个项目来学习本地存储,看到这里了,不妨点个赞吧😘


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

kotlin 协程 + Retrofit 搭建网络请求方案对比

近期在调研使用Kotlin协程 + Retrofit做网络请求方案的实践,计划后面会引入到新项目中,Retrofit的使用非常的简单,基本上看个文档就能立马接入,也在github上找了大量的Demo来看别人是怎么写的,看了大量网上的文章,但发现很多文章看下来也...
继续阅读 »

近期在调研使用Kotlin协程 + Retrofit做网络请求方案的实践,计划后面会引入到新项目中,Retrofit的使用非常的简单,基本上看个文档就能立马接入,也在github上找了大量的Demo来看别人是怎么写的,看了大量网上的文章,但发现很多文章看下来也只是一个简单的接入Demo,不能满足我当下的业务需求。以下记录近期调研的结果和我们的使用。 首先我们先对比从网上找到的几种方案:

方案一

代码摘自这里 这是一篇非常好的Kotlin 协程 + Retrofit 入门的文章,其代码如下:

  1. 服务的定义
interface ApiService {
@GET("users")
suspend fun getUsers(): List

}
  1. Retrofit Builder
object RetrofitBuilder {

private const val BASE_URL = "https://5e510330f2c0d300147c034c.mockapi.io/"

private fun getRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build() //Doesn't require the adapter
}

val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}
  1. 一些中间层
class ApiHelper(private val apiService: ApiService) {

suspend fun getUsers() = apiService.getUsers()
}
class MainRepository(private val apiHelper: ApiHelper) {

suspend fun getUsers() = apiHelper.getUsers()
}
  1. 在ViewModel中获取网络数据
class MainViewModel(private val mainRepository: MainRepository) : ViewModel() {

fun getUsers() = liveData(Dispatchers.IO) {
emit(Resource.loading(data = null))
try {
emit(Resource.success(data = mainRepository.getUsers()))
} catch (exception: Exception) {
emit(Resource.error(data = null, message = exception.message ?: "Error Occurred!"))
}
}
}

这段代码能够与服务端通信,满足基本的要求,并且也有异常的处理机制。但存在以下问题:

  1. 对异常的处理粒度过大。如果需要对不同的异常进行差异化的处理,就会比较麻烦。
  2. 在每一个调用的地方都需要进行try...catch操作。
  3. 不支持从reponse中获取响应头部, http code 信息。但其实很多APP通常也没有要求做这些处理,如果没有拿到数据,给一个通用的提示就完。所以这种方案在某些情况下是可以直接使用的。

方案二

从Github上找了一个Demo, 链接在这里 和方案一相比,作者在的BaseRepository里面,对接口的调用统一进行了try...catch的处理,这样对于调用方,就不用每一个都添加try...catch了。相关的代码如下:

open class BaseRepository {

suspend fun apiCall(call: suspend () -> WanResponse): WanResponse {
return call.invoke()
}

suspend fun safeApiCall(call: suspend () -> Result, errorMessage: String): Result {
return try {
call()
} catch (e: Exception) {
// An exception was thrown when calling the API so we're converting this to an IOException
Result.Error(IOException(errorMessage, e))
}
}

suspend fun executeResponse(response: WanResponse, successBlock: (suspend CoroutineScope.() -> Unit)? = null,
errorBlock: (suspend CoroutineScope.() -> Unit)? = null)
: Result {
return coroutineScope {
if (response.errorCode == -1) {
errorBlock?.let { it() }
Result.Error(IOException(response.errorMsg))
} else {
successBlock?.let { it() }
Result.Success(response.data)
}
}
}

}

在Repository里面这样写

class HomeRepository : BaseRepository() {

suspend fun getBanners(): Result> {
return safeApiCall(call = {requestBanners()},errorMessage = "")
}

private suspend fun requestBanners(): Result> =
executeResponse(WanRetrofitClient.service.getBanner())

}

方案三

在网上看到这个博客, 作者利用一个CallAdapter进行转换,将http错误转换成异常抛出来(后面我自己的方案一也是按照这个思路来的)。核心代码如下:

class ApiResultCallAdapter(private val type: Type) : CallAdapter>> {
override fun responseType(): Type = type

override fun adapt(call: Call): Call> {
return ApiResultCall(call)
}
}

class ApiResultCall(private val delegate: Call) : Call> {
/**
* 该方法会被Retrofit处理suspend方法的代码调用,并传进来一个callback,如果你回调了callback.onResponse,那么suspend方法就会成功返回
* 如果你回调了callback.onFailure那么suspend方法就会抛异常
*
* 所以我们这里的实现是永远回调callback.onResponse,只不过在请求成功的时候返回的是ApiResult.Success对象,
* 在失败的时候返回的是ApiResult.Failure对象,这样外面在调用suspend方法的时候就不会抛异常,一定会返回ApiResult.Success 或 ApiResult.Failure
*/

override fun enqueue(callback: Callback>) {
//delegate 是用来做实际的网络请求的Call对象,网络请求的成功失败会回调不同的方法
delegate.enqueue(object : Callback {

/**
* 网络请求成功返回,会回调该方法(无论status code是不是200)
*/

override fun onResponse(call: Call, response: Response) {
if (response.isSuccessful) {//http status 是200+
//这里担心response.body()可能会为null(还没有测到过这种情况),所以做了一下这种情况的处理,
// 处理了这种情况后还有一个好处是我们就能保证我们传给ApiResult.Success的对象就不是null,这样外面用的时候就不用判空了
val apiResult = if (response.body() == null) {
ApiResult.Failure(ApiError.dataIsNull.errorCode, ApiError.dataIsNull.errorMsg)
} else {
ApiResult.Success(response.body()!!)
}
callback.onResponse(this@ApiResultCall, Response.success(apiResult))
} else {//http status错误
val failureApiResult = ApiResult.Failure(ApiError.httpStatusCodeError.errorCode, ApiError.httpStatusCodeError.errorMsg)
callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}

}

/**
* 在网络请求中发生了异常,会回调该方法
*
* 对于网络请求成功,但是业务失败的情况,我们也会在对应的Interceptor中抛出异常,这种情况也会回调该方法
*/

override fun onFailure(call: Call, t: Throwable) {
val failureApiResult = if (t is ApiException) {//Interceptor里会通过throw ApiException 来直接结束请求 同时ApiException里会包含错误信息
ApiResult.Failure(t.errorCode, t.errorMsg)
} else {
ApiResult.Failure(ApiError.unknownException.errorCode, ApiError.unknownException.errorMsg)
}

callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}

})
}
...
}

作者有提供一个Demo, 如果想拿来用,需要自己再新增一个返回数据的包装类。该方案的缺点是不能获取响应体中的header,还是那句话,毕竟这个需求不常见,可以忽略。

总结一下,当前网上的这些方案可能有的局限:

  1. 如果服务器出错了,不能拿到具体的错误信息。比如,如果服务器返回401, 403,这些方案中的网络层不能将这些信息传递出去。
  2. 如果服务端通过header传递数据给前端,这些方案是不满足需求的。

针对上面的两个问题,我们来考虑如何完善框架的实现。

调整思路

我们期望一个网络请求方案能满足如下目标:

  1. 与服务器之间的正常通信
  2. 能拿到响应体中的header数据
  3. 能拿到服务器的出错信息(http code,message)
  4. 方便的异常处理

调整后的方案

以下代码的相关依赖库版本

implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation "com.squareup.retrofit2:converter-gson:2.8.1"

//Coroutine
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6"
  1. 约定常见的错误类型

我们期望ApiException中也能够返回HTTP Code, 为此约定,错误信息的code从20000开始,这样就不会和HTTP的Code有冲突了。

  • ApiError
object ApiError {
var unknownError = Error(20000, "unKnown error")
var netError = Error(20001, "net error")
var emptyData = Error(20002, "empty data")
}

data class Error(var errorCode: Int, var errorMsg: String)
  1. 返回数据的定义ApiResult.kt

用来承载返回的数据,成功时返回正常的业务数据,出错时组装errorCode, errorMsg, 这些数据会向上抛给调用方。

sealed class ApiResult() {
data class Success(val data: T):ApiResult()
data class Failure(val errorCode:Int,val errorMsg:String):ApiResult()
}
data class ApiResponse(var errorCode: Int, var errorMsg: String, val data: T)

方案一

该方案支持获取HTTP Code,并返回给调用方, 不支持从HTTP Response中提取header的数据。

  1. 服务的定义WanAndroidApi
interface WanAndroidApi {
@GET("/banner/json")
suspend fun getBanner(): ApiResult>>
}
  1. 定义一个ApiCallAdapterFactory.kt

在这里面会对响应的数据进行过滤,对于出错的情况,向外抛出错误。

class ApiCallAdapterFactory : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array, retrofit: Retrofit): CallAdapter<*, *>? {=
check(getRawType(returnType) == Call::class.java) { "$returnType must be retrofit2.Call." }
check(returnType is ParameterizedType) { "$returnType must be parameterized. Raw types are not supported" }

val apiResultType = getParameterUpperBound(0, returnType)
check(getRawType(apiResultType) == ApiResult::class.java) { "$apiResultType must be ApiResult." }
check(apiResultType is ParameterizedType) { "$apiResultType must be parameterized. Raw types are not supported" }

val dataType = getParameterUpperBound(0, apiResultType)
return ApiResultCallAdapter(dataType)
}
}
class ApiResultCallAdapter(private val type: Type) : CallAdapter>> {
override fun responseType(): Type = type

override fun adapt(call: Call): Call> {
return ApiResultCall(call)
}
}

class ApiResultCall(private val delegate: Call) : Call> {

override fun enqueue(callback: Callback>) {
delegate.enqueue(object : Callback {

override fun onResponse(call: Call, response: Response) {
if (response.isSuccessful) {
val apiResult = if (response.body() == null) {
ApiResult.Failure(ApiError.emptyData.errorCode, ApiError.emptyData.errorMsg)
} else {
ApiResult.Success(response.body()!!)
}
callback.onResponse(this@ApiResultCall, Response.success(apiResult))
} else {
val failureApiResult = ApiResult.Failure(response.code(), response.message())
callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}

}

override fun onFailure(call: Call, t: Throwable) {
//Interceptor里会通过throw ApiException 来直接结束请求 同时ApiException里会包含错误信息
val failureApiResult = if (t is ApiException) {
ApiResult.Failure(t.errorCode, t.errorMessage)
} else {
ApiResult.Failure(ApiError.netError.errorCode, ApiError.netError.errorMsg)
}
callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}
})
}

override fun clone(): Call> = ApiResultCall(delegate.clone())

override fun execute(): Response> {
throw UnsupportedOperationException("ApiResultCall does not support synchronous execution")
}


override fun isExecuted(): Boolean {
return delegate.isExecuted
}

override fun cancel() {
delegate.cancel()
}

override fun isCanceled(): Boolean {
return delegate.isCanceled
}

override fun request(): Request {
return delegate.request()
}

override fun timeout(): Timeout {
return delegate.timeout()
}
}
  1. 在Retrofit 初始化时指定CallAdapterFactory, 定义文件ApiServiceCreator.kt 如下:
object ApiServiceCreator {

private const val BASE_URL = "https://www.wanandroid.com/"
var okHttpClient: OkHttpClient = OkHttpClient().newBuilder().build()

private fun getRetrofit() = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(ApiCallAdapterFactory())
.build()

fun create(serviceClass: Class): T = getRetrofit().create(serviceClass)
inline fun create(): T = create(T::class.java)
}
  1. 在ViewModel中使用如下:
viewModelScope.launch {
when (val result = api.getBanner()) {
is ApiResult.Success<*> -> {
var data = result.data as ApiResponse>
Log.i("API Response", "--------->data size:" + data.data.size)
}
is ApiResult.Failure -> {
Log.i("API Response","errorCode: ${result.errorCode} errorMsg: ${result.errorMsg}")

}
}
}

方案二

该方案在方案一的基础之上,支持从HTTP Response Header中获取数据。

  1. 服务的定义WanAndroidApi
interface WanAndroidApi {
@GET("/banner/json")
fun getBanner2(): Call>>
}

需要注意此处的getBanner2()方法前面没有suspend关键字,返回的是一个Call类型的对象,这个很重要。

  1. 定义一个CallWait.kt文件, 为Call类添加扩展方法awaitResult, 该方法内部有部份逻辑和上面的CallAdapter中的实现类似。CallWait.kt文件也是借鉴了这段代码
suspend fun  Call.awaitResult(): ApiResult {
return suspendCancellableCoroutine { continuation ->
enqueue(object : Callback {
override fun onResponse(call: Call?, response: Response) {
continuation.resumeWith(runCatching {
if (response.isSuccessful) {
var data = response.body();
if (data == null) {
ApiResult.Failure(ApiError.emptyData.errorCode, ApiError.emptyData.errorMsg)
} else {
ApiResult.Success(data!!)
}
} else {
ApiResult.Failure(response.code(), response.message())
}
})
}

override fun onFailure(call: Call, t: Throwable) {
// Don't bother with resuming the continuation if it is already cancelled.
if (continuation.isCancelled) return
if (t is ApiException) {
ApiResult.Failure(t.errorCode, t.errorMessage)
} else {
ApiResult.Failure(ApiError.netError.errorCode, ApiError.netError.errorMsg)
}
}
})
}
}
  1. Retrofit的初始化

和方案一不一样,在Retrofit 初始化时不需要指定CallAdapterFactory, 定义文件ApiServiceCreator.kt

object ApiServiceCreator {

private const val BASE_URL = "https://www.wanandroid.com/"
var okHttpClient: OkHttpClient = OkHttpClient().newBuilder().build()

private fun getRetrofit() = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()

fun create(serviceClass: Class): T = getRetrofit().create(serviceClass)
inline fun create(): T = create(T::class.java)
}
  1. ViewModel中使用, 和方法一基本一致,只是这里需要调用一下awaitResult方法
viewModelScope.launch {
when (val result = api.getBanner2().awaitResult()) {
is ApiResult.Success<*> -> {
var data = result.data as ApiResponse>
Log.i("API Response", "--------->data size:" + data.data.size)
}
is ApiResult.Failure -> {
Log.i("API Response","errorCode: ${result.errorCode} errorMsg: ${result.errorMsg}")

}
}
}
  1. 如果我们想从reponse的header里面拿数据, 可以使用Retrofit提供的扩展函数awaitResponse, 如下:
try {
val result = api.getBanner2().awaitResponse()
//拿HTTP Header中的数据
Log.i("API Response", "-----header---->Server:" + result.headers().get("Server"))

if (result.isSuccessful) {
var data = result.body();
if (data != null && data is ApiResponse>) {
Log.i("API Response", "--------->data:" + data.data.size)
}
} else {
//拿HTTP Code
Log.i("API Response","errorCode: ${result.code()}")
}
} catch (e: Exception) {
Log.i("API Response","exception: ${e.message}");
}

方案三

如果我们用Java去实现一套

  • 定义服务
public interface WanAndroidApiJava {
@GET("/banner/json")
public Call>> getBanner();
}
  • ApiException中去封装错误信息
public class ApiException extends Exception {
private int errorCode;
private String errorMessage;

public ApiException(int errorCode, String message) {
this.errorCode = errorCode;
this.errorMessage = message;
}

public ApiException(int errorCode, String message, Throwable e) {
super(e);
this.errorCode = errorCode;
this.errorMessage = message;
}

public String getErrorMessage() {
return this.errorMessage;
}

public int getErrorCode() {
return this.errorCode;
}

interface Code {
int ERROR_CODE_DATA_PARSE = 20001;
int ERROR_CODE_SEVER_ERROR = 20002;
int ERROR_CODE_NET_ERROR = 20003;
}

public static final ApiException PARSE_ERROR = new ApiException(Code.ERROR_CODE_DATA_PARSE, "数据解析出错");
public static final ApiException SERVER_ERROR = new ApiException(Code.ERROR_CODE_SEVER_ERROR, "服务器响应出错");
public static final ApiException NET_ERROR = new ApiException(Code.ERROR_CODE_NET_ERROR, "网络连接出错");
}
  • NetResult封装服务器的响应
public class NetResult {
private T data;
private int code;
private String errorMsg;
...//省略get/set
}
  • 自定义一个Callback去解析数据
public abstract class RetrofitCallbackEx implements Callback> {

@Override
public void onResponse(Call> call, Response> response) {
//如果返回成功
if (response.isSuccessful()) {
NetResult data = response.body();
//返回正确, 和后端约定,返回的数据中code == 0 代表业务成功
if (data.getCode() == 0) {
try {
onSuccess(data.getData());
} catch (Exception e) {
//数据解析出错
onFail(ApiException.PARSE_ERROR);
}
} else {
onFail(ApiException.SERVER_ERROR);
}
} else {
//服务器请求出错
Log.i("API Response", "code:" + response.code() + " message:" + response.message());
onFail(ApiException.SERVER_ERROR);
}
}

@Override
public void onFailure(Call> call, Throwable t) {
onFail(ApiException.NET_ERROR);
}

protected abstract void onSuccess(T t);

protected abstract void onFail(ApiException e);

}
  1. 使用
api.getBanner().enqueue(new RetrofitCallbackEx>() {
@Override
protected void onSuccess(List banners) {
if (banners != null) {
Log.i("API Response", "data size:" + banners.size());
}
}

@Override
protected void onFail(ApiException e) {
Log.i("API Response", "exception code:" + e.getErrorCode() + " msg:" + e.getErrorMessage() + " root cause: " + e.getMessage());
}
});

其它

  1. 在实际项目中,可能经常会碰到需要对HTTP Code进行全局处理的,比如当服务器返回401的时候,引导用户去登录页,这种全局的拦截直接放到interceptor 里面去做就好了。
  2. 架构的方案是为了满足业务的需求,这里也只是针对自己碰到的业务场景来进行梳理调研。当然实际项目中通常会有更多的要求,比如环境的切换导致域名的不同,网络请求的通用配置,业务异常的上报等等,一个完整的网络请求方案需要再添加更多的功能。
  3. Kotlin语言非常的灵活,扩展函数的使用能使代码非常的简洁。Kotlin在我们项目中用的不多, 不是非常精通,协程 + Retrofit应该会有更优雅的写法,欢迎交流。


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

每个程序员第一份工作前应该知道的10件事

前言这篇文章对我来说是“事后诸葛亮”,以下是我进入编程行业以来的一些经验和教训。0000 - 被喜欢是很容易的事如果你有时间观念,衣着得体,保持微笑,不做一些愚蠢的事情,至少会有一些人喜欢你。事实上,让你的同事喜欢你是一件很容易的事。你想拥有很多朋友吗?在你的...
继续阅读 »

前言

这篇文章对我来说是“事后诸葛亮”,以下是我进入编程行业以来的一些经验和教训。

0000 - 被喜欢是很容易的事

如果你有时间观念,衣着得体,保持微笑,不做一些愚蠢的事情,至少会有一些人喜欢你。事实上,让你的同事喜欢你是一件很容易的事。你想拥有很多朋友吗?在你的桌上放一碗满满的糖果,你就会发现你有多少朋友。

0001 - 受人尊敬是很难的事

不管你在这个行业有20年经验或没有经验,当你刚进入一家公司时,没有人会尊敬你。这并不意味着他们不喜欢你,或者对你没有好感。这是因为你还没有做任何事,给别人尊敬你的理由。获得尊敬最快的方法就是把工作做到最好。做到最好并不是浮夸和惊艳,而是有效和团结。当大家看到你能有效率并正确的完成工作,不用害怕赢得不了尊敬。

0010 - 你在大学学到的一切都是没用的

第一份工作的前6个月,会比你整个大学学到的还多。你知道吗?工作中压力会很大。因为总有一天老板会要求你去做你简历中写的那些很棒的那些东西。你懂得,那些东西是你在学校能学到的?如果你搞砸了,在大学你会补考、留级。对不起,在这里你会被炒鱿鱼。在最近在工作中发现了这种状况。有些人因为某些技能被聘用,也因为不能解决问题而被解雇。很多有趣的事情会发生在第一份工作中。

大部分公司都有新员工的试用期,一般为30~90天。基本上,如果你搞砸了项目或者老板发现你在简历上撒了谎,不用想别的了,卷铺盖走人吧。千万记住别撒谎!

0011 - 不要停止学习

作为新人,抱着“我什么都不知道,但我想学”的心态去工作会对你帮助很大。当你意识到自己多么愚蠢时,至少不会那么尴尬。你要意识到几乎所有人都比你有经验,比你懂的更多。好消息是,如果你问一些聪明的问题并关注他们做的事情,大多数人会乐意与你分享他们的知识。每个人都下意识认为自己的观点和经验是正确的,所以不要犹豫,想办法改善你的工作方式。只要你问问题,你就能从每一个和你一起工作的人身上学到东西。询问一些技巧、方法和其它同事可能精通的东西。观察他们如何使用命令行。每个人做事的方式都不一样,还有许多未知等着你去发现。技术糟糕(和/或年长的)程序员倾向于认为“他们”的方式是最好的,所以他们从不征求建议。聪明的程序员愿意接受这样一个事实:可能有更好的做事方法。这意味着你必须愿意切换编程语言、环境、操作系统或文本编辑器。

“什么!?叫我放弃

<这里插入自己的做事方式>

?为什么?谁都知道这是最棒的!”你必须克服这种心态。如果你想解决新的问题,必须学会谦卑和接受挫折。这种感觉糟透了,但是克服它!编程很难,做一个优秀的程序员更难。难过就自己一个人躲着哭吧。

0100 - 你的编辑器决定你的死活,所以请明智的选择

如果你觉得我夸大其词了,可以去网上问问哪款编辑器是最好的。得到的回复会是: Vi、Emacs 和一些其他的 IDE(通常会是Eclipse 或 Xcode)。事实上,你也许会惊奇还有多少人把 Vi 作为自己的主编辑器(我认为这些人应该清醒过来并回到80年代,虽然我也是其中一员)。问题的关键是,如果你不会使用其中的一种编辑器,你可能会碰到一些问题。去找到办公室最好的程序员,看看他用的什么编辑器。然后,点头表示认同他的答案,打印小抄,开始学习编辑器。快去!

0101 - 没有人真正关心你是哪所大学毕业的

如果有人问你哪所大学毕业的,不用担心,他们不是在评判你,这只是礼貌的谈话。如果你的编程技术很棒,没人会在乎那一纸文凭。想知道相比文凭他们更关心什么吗?你的绩效。不要跟别人说这件事,因为这会让你看起来像个蠢货。我不能再强调了,说的已经够多了。

0110 - 沉默永不过时

刚工作,不知道要说什么?那就什么都别说。点头并且微笑。同事不让你加入他们的讨论吗?是挺尴尬的,大家不认识你,你也不认识大家。给彼此一点时间,他们中的大多数会主动来找你的。做朋友需要时间。是的,我知道作为新人很难受。我知道一个新的环境是个挑战,但你要振作起来,时间会帮你解决这些问题。如果你不是一个脾气古怪的人,并且做好工作,那么你终将成为团队的一员。除非你是辣妹或美男,否则很难直接融入团体。祝你好运。

0111 - 你会遇到古怪、消极的人,对付他

有些人就是不喜欢你,有时是因为他们有严重的情感问题,他们不喜欢任何人,所以他们会下意识地讨厌你。这些人从不改变,所以你得学会和如何与他们一起工作。如果你很快的找到了一个敌人,那么你要做的就是如何与他成为朋友,因为你不知道接下来几年会发生什么。我见过几次这种事,你今天敌视的家伙明天成了你的上司。

1000 - 和计算机做朋友

想确保你总有最合适的闲聊话题?那就和计算机做朋友。这里有一个给新员工的小提示,大部分人都是因为需要或出问题了才会突然想起计算机技术人员。一段时间后,这些事情会使你的世界观变得恶化。所以定期去他们办公室找他们谈话。不要抱怨,不要乞求,只要问好并且询问他们生活如何。这是一项总是有利的投资。(前提是你得会修电脑...这段有点难翻译,绕口)

1001 - 你永远逃不掉办公室政治

作为一个新人(如果你从足够低的位置开始),你不必太担心这一点。但请放心,当你的责任越来越重时,你会进入这场游戏。你逃不掉,无论你怎么努力。你可以选择不玩,但你也会尝到苦果。这对于顽固的极客来说,这是一门很难的课程,因为相比人来说,我们更喜欢计算机。也许你进入IT行业仅仅是很单纯的喜欢信息技术。如果是这样的话,很抱歉,如果你想超越某个职业水平,必须成为管理层的一员,并加入政治竞技场。

翻译:余震(Freak)
来源:https://juejin.cn/post/6844903497964453896
原文:http://www.applematters.com/article/10-things-every-programmer-should-know-for-their-first-job/

收起阅读 »

程序员要如何变帅?

胖可以瘦土可以救关键要有一颗想改变的心!面对生活,我们所有人最有效的努力方式,不是抱怨自己拿到的牌不够好,而是尽可能地打好手上的每一张牌。这种投资的初期难点在于——意识到品味是多么奢侈的东西。与它正相关的有包括家庭条件、社会环境、圈子质量、自身的悟性与受教育程...
继续阅读 »

提升外形包括很多方面啊,比如减肥、着装、气质等等。最近就发现程序外形变得越来越顺眼(emmmm)可能是他恋情有了新进展(划掉)的缘故

胖可以瘦


土可以救


关键要有一颗想改变的心!面对生活,我们所有人最有效的努力方式,不是抱怨自己拿到的牌不够好,而是尽可能地打好手上的每一张牌。

1,高效投资。


任何人投资都是为了得到回报。穿着打扮方面的投资,要不就回报在事业上,要不就回报在感情上。人生大事就这么两件,在这里头花钱,是非常值得的。

这种投资的初期难点在于——意识到品味是多么奢侈的东西。与它正相关的有包括家庭条件、社会环境、圈子质量、自身的悟性与受教育程度在内的无数变量,于是不停试错成了无数不够幸运的普通人的宿命。比胡乱烧钱更好的策略是在条件有限的情况下,花尽可能小的代价把事情做得尽可能好。

众所周知,男装中能比较容易穿出体面与高端感觉的颜色,只有黑和白,最多再加一个「高级灰」。根据我的观察,白色与灰色的东西如果不花上足够的钱,廉价的材质所带来的廉价感会比黑色显眼一些。所以保守起见,作为男生,先从黑色玩起,试错成本最低。(黑色的另一个优点是衣服不容易脏,许多黑色的秋冬衣物一年只需要洗一两次。)在黑的基础上,偶尔加上一些白色作为搭配,在日常生活中就已经足够应付任何场面了。全身上下的颜色超过三种还能搭得很好看的能力是很昂贵的,不要进行没有把握的冒险。

2,注意安全。


留普通人的普通发型,穿普通人的普通衣服。作为男生,如非必要,不要烫染,不要把头发搞得多有设计感多邪门。发型这种东西,首先要融入一个人,犹有余力的时候,再来追求提升这个人。

若干年前,有一次我一边和当时的女友亲亲一边看新闻的时候,得知姚明在美国理发一次花掉了几百美元,吓了一跳。因为他那时的发型怎么看都是很普通的样子。我看着姚明亲了女友很久才想明白,如果他的发型非常特别,就会把所有人的目光都引向他的脸,而他的优势并不在那里。(虽然这是很有价值的启示,但是后来所有漂亮姑娘在我眼里都长得有一点像姚明的这笔血债,恐怕只有未来某天我看着哪个姑娘亲姚明很久才能抹平了。)

于是乎,前些时间,听说科比做一次头发要花近千美元的时候,很多小伙伴都笑了,那可接近是一个光头啊,我就没有笑。我仔细看了那则新闻的配图,科比的「光头」配合他的脸型五官,看起来非常舒适。大家走上街去多看几眼路上来往的行人,就知道「舒适」二字有多么不容易了,更何况是一个发际线有问题的光头?优秀的发型设计师们输出的美学,贵就贵在这毫厘之间的分寸感。

穿衣服的分寸感也是一样要紧的。脖子上挂一串骷髅头的那不是程序员,是巫师;气场能压住大披风的每一个都是超人。对设计师们的想象力和身边朋友的吐槽欲要有敬畏之心,在对自己需要什么心里还没底的阶段,少逛淘宝、多逛商场,多试试各种非潮牌陈列在橱窗的黑色系搭配。虽然可能比在淘宝购物要多花一些钱,但买到了非潮牌陈列师们恰如其分的品味,值得。这笔钱比自己横冲直撞、胡乱摸索的试错费要低一个数量级。

3,扬长避短。

穿衣打扮的首要功能是什么?是突出和放大我们自身的优势,掩饰与弱化我们固有的缺陷。这里所说的优势与缺陷是非常复杂的东西,魔鬼全都在细节里,不是一个局外人一二三四就能替你列出来的。

一般来说,只有从小陪你长大、摸过看过你裸体无数次的你自己才能弄得清。许多身着看似普通的衣服看起来却特别好看的人,可能身上穿的都是自己为自己搭建的私人订制;许多按照淘宝卖家的介绍和杂志网络上的攻略来穿衣服的人之所以屡屡倒霉,就是没有想清楚这个道理。

4,建立自信。


没有比自信更迷人的气质了,人人都知道这东西的重要性,却不是人人都知道去哪里才能弄到它。

其实很简单的啊,除了自大狂,自信只属于那些有准备的人。举个应景的例子:含胸驼背……这种情况很常见。虽然人人都知道站有站姿才是美,但是作为一个普通人,很难有办法确认自己站多直才合适,往往胸挺起来了,心还是虚的。于是许多没当过兵没学过芭蕾舞的人都会在生活中偶尔不自觉地站成这种比较省力的姿势。

解决这个大问题有非常好的小办法:报一个短期的形体训练班。在这种训练班里,可以学到怎么站、怎么坐、怎么走,一般来说四面墙上都有镜子,学员能够非常清晰地看到自己的改变、确认自己最优雅的样子,做错的时候有导师的鞭策,做对的时候有导师的认可。

我们需要这样的形式感,需要一个这样的密集训练,来帮助我们形成肌肉记忆。我小时候没有养成好的习惯,成年后要我时时刻刻保持挺拔,会很疲倦。因此在独处的时候、在亲朋好友面前,我偶尔也会放纵自己,站得东倒西歪。

但是在必要的场合、必要的时候,我有自信,绝对不会丢脸地走成一只猩猩——因为我曾经偶然地参与过一次这样的训练。许多年过去了,我仍然清楚地记得把自己的身体绷到什么位置是最好看的。

切记,站姿并不是全部,站姿只是构建完整自信的其中一个环节。

勤剪指甲,就不怕与人握手;勤洗袜子,就不怕脱鞋走进朋友的家门;内裤买好一点,谈新女朋友不怕见胱死;在镜子里研究过自己笑容的弧度,与人合影的时候就不会笑得像刚喝完半杯福尔马林。

5,进阶思考。


许多人反感成年男性过度打扮的原因大致是现代人通常认为男性之美应该体现在男人的社会属性与政治属性之中,而不是流于表面的精致妆容与华丽衣着。

世界上有资格什么事都任性的人是很少的,你没法假设你就是其中一个,在某一方面任性,就得靠自己在其它方面加倍的不任性来买单。没有必要。

作为一个社会人,我们在这个领域的修行不能没完没了,必须要找到一个适合自己的程度停下来。

我会请我能找到的最厉害的发型师来帮我做设计,但是我会拜托他用尽全力地帮我剪成看起来没有花太大功夫去打理就很得体的样子——也就是精致的「不精致」。我会买尽可能高品质的衣服,但是不会有任何一件带着显眼的 logo,不会去追求令人感到血脉偾张的设计感——也就是奢侈的「不奢侈」。因为我希望对自己外表的维护是一个防御性的动作,没有侵略性与攻击性。防御别人对邋遢的不屑,也防御别人对华丽的反感,防御别人对廉价的鄙夷,也防御别人对奢侈的憎恨。

我觉得外在上的自我提升只需要达到让心仪的女孩不反感你的靠近的程度就行,要得到她的心,还是得靠智慧、品格、幽默、无耻、下贱、温柔、爱。等等。另外,最后一个建议:追到她以后就没必要再坚持只穿黑色衣服了。保护好你的「野性」与「童心」,无伤大雅的时候,还是要带着它们出来溜溜。我偶尔戴绿帽子上街、偶尔不穿裙子裸奔,我爸妈都还不知道呢。(恩?)

以上内容来自梁边妖的答案


作者:程序人生
链接:https://mp.weixin.qq.com/s/kgNQv7L9aoZd6r0U0oVjTQ

收起阅读 »

月入五万的西二旗人教你如何活得像月薪五千

--Illustration by Steve Scott西二旗人有一种独特的能力,那就是把明明高薪的日子过得看似很穷。比如我曾经实习过的BAT某司,有一个级别大我很多的前辈,收入至少是五万起步,每天却穿着看似同一件的条纹T恤,踩着个大拖鞋,成天背着手在我们工...
继续阅读 »


--Illustration by Steve Scott

西二旗人有一种独特的能力,那就是把明明高薪的日子过得看似很穷。

比如我曾经实习过的BAT某司,有一个级别大我很多的前辈,收入至少是五万起步,每天却穿着看似同一件的条纹T恤,踩着个大拖鞋,成天背着手在我们工位旁边转悠,乍看上去宛如一个要伺机打扫卫生的保洁大叔。

再比如,我刚来现在这家公司的时候,隔壁部门有个四十来岁的员工,每天斜挎着一个淘宝爆款的背包,骑着一辆破旧的自行车上下班,我当时心中暗想:自己以后一定不要混成这个样子。后来发现,那是他们部门的技术leader,月薪也就大概是我的四五倍吧。

还有,如果你跟西二旗人接触多的话,你会发现,大部分西二旗人理发能选38的最低档就绝不会选58的总监档,出门能坐公交就绝不会打车,能去上地华联解决的购物绝对不会去朝阳大悦城,一个个都是大写的质朴。

所以说,西二旗人简直就是装逼界的一股清流。

在这个浮躁的社会,不知多少人都是月入一万假装月入十万,只有西二旗人,月入十万却过得像是月入几千。

全世界的“收入装逼守恒”,大概都是由西二旗人来守护。

西二旗人具体是怎么月入五万却过得像是月入五千的呢?

  • 比如说穿衣。

穿衣是西二旗人永远的槽点。

月薪五万,在国贸的白领会买几身Armani的西装,每天穿着在去往几十层的电梯里争奇斗艳。

在SOHO工作的创业者,每个月去三里屯买上几件潮牌streetwear,每天不重样换着穿。

月薪五万的西二旗人,一身优衣库,加起来不超过一千,前段时间奢侈了一把,消费升级到了Gap,不能再高了。

你要是说这是西二旗人的低调?对不起,你错了。

一个成功的国贸精英,都是穿得看似低调,其实翻开领口一看,都是几万块的大牌,这才叫低调。

而西二旗人都是穿的看似淘宝货,翻开领口一看,可能真的就是淘宝货。

  • 再比如说吃饭。

爱穿淘宝爆款不要紧,西二旗人每天脑力劳动这么重,吃饭上一定不会亏待自己了吧?

每天中午下馆子?晚上六菜一汤?

不,大部分西二旗人午饭都是在公司食堂搞定。晚饭呢?当然是吃免费的加班餐了。

哦,有时也出去下馆子奢侈一把,比如来到门口的驴肉火烧店,两个火烧加一碗紫菜汤,一算,15块钱。

  • 再再比如说开车。

西二旗那么多期权套现、身家千万的程序员,一定是名车如云吧?

没有红色的法拉利、紫色的兰博基尼、白色的玛莎拉蒂,至少也得是黑色的大众吧?

对不起,西二旗人只有中意两辆车:黄色的ofo和橙色的摩拜。

为什么呢?选择了互联网你还希望能有户口去摇号?

  • 再再再比如说购物。

月薪五万,商场一楼的品牌随便买吧?——对不起,都不认识。

过生日给自己买个奢侈品?——对不起,同事们都不认识。

看这个样子真的就是月薪五六千的样子。

当然,给自己买电子产品的时候西二旗人从不吝啬,几万块的游戏本,转眼就刷了。

*

那么问题来了,西二旗人挣的钱都去哪儿了呢?

1 攒钱买房

攒钱买房是西二旗人的梦想,省钱还贷是西二旗人的信仰。

没有什么能比一套房子给西二旗人更多安全感的。

什么?月薪五万的西二旗人都有一套房了?那有什么,攒钱买下一套呗。

2 买电子产品、手办、玩具

西二旗人的马斯洛需求从低到高是这样的:房子—车子—电子产品。

当一个西二旗人不再操心房子车子,那他就要开始买各种各样的电子产品了。

手机至少苹果、安卓各一个,智能手表必须要有,显示器专挑大个儿的买,还不止一个。

另外,手办这种属于高级玩具,月薪不到五万的西二旗人还是别玩了。

3 攒着

“这么多钱,我也不知道该花到哪里,先干脆攒着吧。”

最后,你要问我为什么西二旗人这么不会生活?

大概在西二旗人眼里,人生的乐趣不需要从生活中获取吧——

编程和工作带来的乐趣,就足够驱动着这群人,一往无前。

作者:景岁
来源:http://mp.weixin.qq.com/s/CX7DViOtcqxFEKsClvMjyw

收起阅读 »

程序员如何高效休息?

过两天,就开始进入长假了。所以趁着摸鱼的时间,把《高效休息法》一口气给读完了。作为一个东方人,大家都会把冥想与宗教联系在一起,比如和尚打坐。而这本书尝试从科学角度来论述冥想并非一种宗教行为,我们更可以从实用主义出发,让它为我们的日常休息做服务。什么是正念冥想?...
继续阅读 »


过两天,就开始进入长假了。所以趁着摸鱼的时间,把《高效休息法》一口气给读完了。

这本书讲的是日本一个女子前往美国留学,中间遭遇挫折,最后在教授的帮助下自我拯救的一个过程。这个故事多多少少有点鸡汤,但其实它是一本以故事的形式介绍正念冥想的书籍。

作为一个东方人,大家都会把冥想与宗教联系在一起,比如和尚打坐。而这本书尝试从科学角度来论述冥想并非一种宗教行为,我们更可以从实用主义出发,让它为我们的日常休息做服务。

那我们程序员作为卷王之王,如何利用正念冥想做到连休息也高效呢?

什么是正念冥想?

这本书提及我们的疲惫包括身体的,还有大脑的。即使我们睡觉的时候,大脑也在高速运转,如果能降低大脑的运转速度,那它就能得到休息。冥想就是通过识别大脑里的念头,然后把注意力回到当下,回到呼吸上,来减少大脑自身的消耗的。

同样的,负面的情绪也是大脑的一种自我消耗。通过冥想,标识负面情绪,给自己正念暗示,回到当下正在做的事情,也可以降低这种消耗。

在一次次的冥想锻炼中,我们的大脑能更积极地应对负面情绪,能更快地进入专注状态,该休息的也能很好地得到休息。

正念冥想就这样让我们从专注当下到放松身心,获得良好的正向循环,从而获得更多的幸福感。

个人经历

说说我个人的经历,我其实是在公司的一次培训课上真实地接触到冥想的。

培训老师上课前,我还以为她会让大家伸伸腰之类的热身运动。没想到的是,她让大家闭上眼静坐。然后让大家跟着她去标记自己杂乱的念头、想法,注意呼吸。

每次培训课前都有这样5-10分钟给我们冥想的时间。

这对我来说算是很新奇的一种体验,虽然以前有听过这种东西,但是自己亲身体验还是很不一样的。起码来说,它破除了我对冥想就是宗教行为的偏见。我虽然没有感动落泪等激动的行为,但是的确有感觉到一种内心的安宁。这种安宁,在我曾经长达半年的早睡早起的日子里体验过。而在冥想时,偶尔也能体会到。

总之,冥想多少减少了我的焦虑,让我更关注于当下。

在这之后,我虽不经常做冥想,但经常在睡前听华为手机的免费的深度睡眠引导,来获得不错的睡眠质量。

五日休息法

最后,再介绍一下这本书提供的一套“五日休息法”:

  • 第一日:偷懒日,什么事都不做

  • 第二日:逛逛附近没去过的地方

  • 第三日:与他人联系

  • 第四日:放纵日(要做好预算)

  • 第五日:盘点,并规划下一次休息

这个长假,如果你们有兴趣,也可以尝试一下这个“五日休息法"。如果无法做到五天,这里的“偷懒日”也是一个很不错的休息方案。

在这一天,放下手机,什么事都不做,去户外走走即可。

祝大家都能过一个轻松愉快的假期!

作者:陈佬昔没带相机
来源:
https://juejin.cn/post/7057558464066912263

收起阅读 »

本着什么原则,才能写出优秀的代码?

作为一名程序员,最不爱干的事情,除了开会之外,可能就是看别人的代码。有的时候,新接手一个项目,打开代码一看,要不是身体好的话,可能直接气到晕厥。风格各异,没有注释,甚至连最基本的格式缩进都做不到。这些代码存在的意义,可能就是为了证明一句话:又不是不能跑。在这个...
继续阅读 »

作为一名程序员,最不爱干的事情,除了开会之外,可能就是看别人的代码。

有的时候,新接手一个项目,打开代码一看,要不是身体好的话,可能直接气到晕厥。

风格各异,没有注释,甚至连最基本的格式缩进都做不到。这些代码存在的意义,可能就是为了证明一句话:又不是不能跑。

在这个时候,大部分程序员的想法是:这烂代码真是不想改,还不如直接重写。

但有的时候,我们看一些著名的开源项目时,又会感叹,代码写的真好,优雅。为什么好呢?又有点说不出来,总之就是好。

那么,这篇文章就试图分析一下好代码都有哪些特点,以及本着什么原则,才能写出优秀的代码。

初级阶段

先说说比较基本的原则,只要是程序员,不管是高级还是初级,都会考虑到的。

img

这只是列举了一部分,还有很多,我挑选四项简单举例说明一下。

  1. 格式统一

  2. 命名规范

  3. 注释清晰

  4. 避免重复代码

以下用 Python 代码分别举例说明:

格式统一

格式统一包括很多方面,比如 import 语句,需要按照如下顺序编写:

  1. Python 标准库模块

  2. Python 第三方模块

  3. 应用程序自定义模块

然后每部分间用空行分隔。

import os
import sys

import msgpack
import zmq

import foo
复制代码

再比如,要添加适当的空格,像下面这段代码;

i=i+1
submitted +=1
x = x*2 - 1
hypot2 = x*x + y*y
c = (a+b) * (a-b)
复制代码

代码都紧凑在一起了,很影响阅读。

i = i + 1
submitted += 1
x = x * 2 - 1
hypot2 = x * x + y * y
c = (a + b) * (a - b)
复制代码

添加空格之后,立刻感觉清晰了很多。

还有就是像 Python 的缩进,其他语言的大括号位置,是放在行尾,还是另起新行,都需要保证统一的风格。

有了统一的风格,会让代码看起来更加整洁。

命名规范

好的命名是不需要注释的,只要看一眼命名,就能知道变量或者函数的作用。

比如下面这段代码:

a = 'zhangsan'
b = 0
复制代码

a 可能还能猜到,但当代码量大的时候,如果满屏都是 abcd,那还不得原地爆炸。

把变量名稍微改一下,就会使语义更加清晰:

username = 'zhangsan'
count = 0
复制代码

还有就是命名要风格统一。如果用驼峰就都用驼峰,用下划线就都用下划线,不要有的用驼峰,有点用下划线,看起来非常分裂。

注释清晰

看别人代码的时候,最大的愿望就是注释清晰,但在自己写代码时,却从来不写。

但注释也不是越多越好,我总结了以下几点:

  1. 注释不限于中文或英文,但最好不要中英文混用

  2. 注释要言简意赅,一两句话把功能说清楚

  3. 能写文档注释应该尽量写文档注释

  4. 比较重要的代码段,可以用双等号分隔开,突出其重要性

举个例子:

# =====================================
# 非常重要的函数,一定谨慎使用 !!!
# =====================================

def func(arg1, arg2):
   """在这里写函数的一句话总结(如: 计算平均值).

  这里是具体描述.

  参数
  ----------
  arg1 : int
      arg1的具体描述
  arg2 : int
      arg2的具体描述

  返回值
  -------
  int
      返回值的具体描述

  参看
  --------
  otherfunc : 其它关联函数等...

  示例
  --------
  示例使用doctest格式, 在`>>>`后的代码可以被文档测试工具作为测试用例自动运行

  >>> a=[1,2,3]
  >>> print [x + 3 for x in a]
  [4, 5, 6]
  """
复制代码

避免重复代码

随着项目规模变大,开发人员增多,代码量肯定也会增加,避免不了的会出现很多重复代码,这些代码实现的功能是相同的。

虽然不影响项目运行,但重复代码的危害是很大的。最直接的影响就是,出现一个问题,要改很多处代码,一旦漏掉一处,就会引发 BUG。

比如下面这段代码:

import time


def funA():
   start = time.time()
   for i in range(1000000):
       pass
   end = time.time()

   print("funA cost time = %f s" % (end-start))


def funB():
   start = time.time()
   for i in range(2000000):
       pass
   end = time.time()

   print("funB cost time = %f s" % (end-start))


if __name__ == '__main__':
   funA()
   funB()
复制代码

funA()funB() 中都有输出函数运行时间的代码,那么就适合将这些重复代码抽象出来。

比如写一个装饰器:

def warps():
   def warp(func):
       def _warp(*args, **kwargs):
           start = time.time()
           func(*args, **kwargs)
           end = time.time()
           print("{} cost time = {}".format(getattr(func, '__name__'), (end-start)))
       return _warp
   return warp
复制代码

这样,通过装饰器方法,实现了同样的功能。以后如果需要修改的话,直接改装饰器就好了,一劳永逸。

进阶阶段

当代码写时间长了之后,肯定会对自己有更高的要求,而不只是格式注释这些基本规范。

但在这个过程中,也是有一些问题需要注意的,下面就来详细说说。

炫技

第一个要说的就是「炫技」,当对代码越来越熟悉之后,总想写一些高级用法。但现实造成的结果就是,往往会使代码过度设计。

这不得不说说我的亲身经历了,曾经有一段时间,我特别迷恋各种高级用法。

有一次写过一段很长的 SQL,而且很复杂,里面甚至还包含了一个递归调用。有「炫技」嫌疑的 Python 代码就更多了,往往就是一行代码包含了 N 多魔术方法。

然后在写完之后漏出满意的笑容,感慨自己技术真牛。

结果就是各种被骂,更重要的是,一个星期之后,自己都看不懂了。

img

其实,代码并不是高级方法用的越多就越牛,而是要找到最适合的。

越简单的代码,越清晰的逻辑,就越不容易出错。而且在一个团队中,你的代码并不是你一个人维护,降低别人阅读,理解代码的成本也是很重要的。

脆弱

第二点需要关注的是代码的脆弱性,是否细微的改变就可能引起重大的故障。

代码里是不是充满了硬编码?如果是的话,则不是优雅的实现。很可能导致每次性能优化,或者配置变更就需要修改源代码。甚至还要重新打包,部署上线,非常麻烦。

而把这些硬编码提取出来,设计成可配置的,当需要变更时,直接改一下配置就可以了。

再来,对参数是不是有校验?或者容错处理?假如有一个 API 被第三方调用,如果第三方没按要求传参,会不会导致程序崩溃?

举个例子:

page = data['page']
size = data['size']
复制代码

这样的写法就没有下面的写法好:

page = data.get('page', 1)
size = data.get('size', 10)
复制代码

继续,项目中依赖的库是不是及时升级更新了?

积极,及时的升级可以避免跨大版本升级,因为跨大版本升级往往会带来很多问题。

还有就是在遇到一些安全漏洞时,升级是一个很好的解决办法。

最后一点,单元测试完善吗?覆盖率高吗?

说实话,程序员喜欢写代码,但往往不喜欢写单元测试,这是很不好的习惯。

有了完善,覆盖率高的单元测试,才能提高项目整体的健壮性,才能把因为修改代码带来的 BUG 的可能性降到最低。

重构

随着代码规模越来越大,重构是每一个开发人员都要面对的功课,Martin Fowler 将其定义为:在不改变软件外部行为的前提下,对其内部结构进行改变,使之更容易理解并便于修改。

重构的收益是明显的,可以提高代码质量和性能,并提高未来的开发效率。

但重构的风险也很大,如果没有理清代码逻辑,不能做好回归测试,那么重构势必会引发很多问题。

这就要求在开发过程中要特别注重代码质量。除了上文提到的一些规范之外,还要注意是不是滥用了面向对象编程原则,接口之间设计是不是过度耦合等一系列问题。

那么,在开发过程中,有没有一个指导性原则,可以用来规避这些问题呢?

当然是有的,接着往下看。

高级阶段

最近刚读完一本书,Bob 大叔的《架构整洁之道》,感觉还是不错的,收获很多。

img

全书基本上是在描述软件设计的一些理论知识。大体分成三个部分:编程范式(结构化编程、面向对象编程和函数式编程),设计原则(主要是 SOLID),以及软件架构(其中讲了很多高屋建翎的内容)。

总体来说,这本书中的内容可以让你从微观(代码层面)和宏观(架构层面)两个层面对整个软件设计有一个全面的了解。

其中 SOLID 就是指面向对象编程和面向对象设计的五个基本原则,在开发过程中适当应用这五个原则,可以使软件维护和系统扩展都变得更容易。

五个基本原则分别是:

  1. 单一职责原则(SRP)

  2. 开放封闭原则(OCP)

  3. 里氏替换原则(LSP)

  4. 接口隔离原则(ISP)

  5. 依赖倒置原则(DIP)

单一职责原则(SRP)

A class should have one, and only one, reason to change. – Robert C Martin

一个软件系统的最佳结构高度依赖于这个系统的组织的内部结构,因此每个软件模块都有且只有一个需要被改变的理由。

这个原则非常容易被误解,很多程序员会认为是每个模块只能做一件事,其实不是这样。

举个例子:

假如有一个类 T,包含两个函数,分别是 A()B(),当有需求需要修改 A() 的时候,但却可能会影响 B() 的功能。

这就不是一个好的设计,说明 A()B() 耦合在一起了。

开放封闭原则(OCP)

Software entities should be open for extension, but closed for modification. – Bertrand Meyer, Object-Oriented Software Construction

如果软件系统想要更容易被改变,那么其设计就必须允许新增代码来修改系统行为,而非只能靠修改原来的代码。

通俗点解释就是设计的类对扩展是开放的,对修改是封闭的,即可扩展,不可修改。

看下面的代码示例,可以简单清晰地解释这个原则。

void DrawAllShape(ShapePointer list[], int n)
{
int i;
for (i = 0; i < n; i++)
{
struct Shape* s = list[i];
switch (s->itsType)
{
case square:
DrawSquare((struct Square*)s);
break;
case circle:
DrawSquare((struct Circle*)s);
break;
default:
break;
}
}
}
复制代码

上面这段代码就没有遵守 OCP 原则。

假如我们想要增加一个三角形,那么就必须在 switch 下面新增一个 case。这样就修改了源代码,违反了 OCP 的封闭原则。

缺点也很明显,每次新增一种形状都需要修改源代码,如果代码逻辑复杂的话,发生问题的概率是相当高的。

class Shape
{
public:
virtual void Draw() const = 0;
}

class Square: public Shape
{
public:
virtual void Draw() const;
}

class Circle: public Shape
{
public:
virtual void Draw() const;
}

void DrawAllShapes(vector<Shape*>& list)
{
vector<Shape*>::iterator I;
for (i = list.begin(): i != list.end(); i++)
{
(*i)->Draw();
}
}
复制代码

通过这样修改,代码就优雅了很多。这个时候如果需要新增一种类型,只需要增加一个继承 Shape 的新类就可以了。完全不需要修改源代码,可以放心扩展。

里氏替换原则(LSP)

Require no more, promise no less.– Jim Weirich

这项原则的意思是如果想用可替换的组件来构建软件系统,那么这些组件就必须遵守同一个约定,以便让这些组件可以相互替换。

里氏替换原则可以从两方面来理解:

第一个是继承。如果继承是为了实现代码重用,也就是为了共享方法,那么共享的父类方法就应该保持不变,不能被子类重新定义。

子类只能通过新添加方法来扩展功能,父类和子类都可以实例化,而子类继承的方法和父类是一样的,父类调用方法的地方,子类也可以调用同一个继承得来的,逻辑和父类一致的方法,这时用子类对象将父类对象替换掉时,当然逻辑一致,相安无事。

第二个是多态,而多态的前提就是子类覆盖并重新定义父类的方法。

为了符合 LSP,应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法。当父类是抽象类时,父类就是不能实例化,所以也不存在可实例化的父类对象在程序里,也就不存在子类替换父类实例(根本不存在父类实例了)时逻辑不一致的可能。

举个例子:

看下面这段代码:

class A{
public int func1(int a, int b){
return a - b;
}
}

public class Client{
public static void main(String[] args){
A a = new A();
System.out.println("100-50=" + a.func1(100, 50));
System.out.println("100-80=" + a.func1(100, 80));
}
}
复制代码

输出;

100-50=50
100-80=20
复制代码

现在,我们新增一个功能:完成两数相加,然后再与 100 求和,由类 B 来负责。即类 B 需要完成两个功能:

  1. 两数相减

  2. 两数相加,然后再加 100

现在代码变成了这样:

class B extends A{
public int func1(int a, int b){
return a + b;
}

public int func2(int a, int b){
return func1(a,b) + 100;
}
}

public class Client{
public static void main(String[] args){
B b = new B();
System.out.println("100-50=" + b.func1(100, 50));
System.out.println("100-80=" + b.func1(100, 80));
System.out.println("100+20+100=" + b.func2(100, 20));
}
}
复制代码

输出;

100-50=150
100-80=180
100+20+100=220
复制代码

可以看到,原本正常的减法运算发生了错误。原因就是类 B 在给方法起名时重写了父类的方法,造成所有运行相减功能的代码全部调用了类 B 重写后的方法,造成原本运行正常的功能出现了错误。

这样做就违反了 LSP,使程序不够健壮。更通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替。

接口隔离原则(ISP)

Clients should not be forced to depend on methods they do not use. –Robert C. Martin

软件设计师应该在设计中避免不必要的依赖。

ISP 的原则是建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法要尽量少。

也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。

在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。

单一职责与接口隔离的区别:

  1. 单一职责原则注重的是职责;而接口隔离原则注重对接口依赖的隔离。

  2. 单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节; 而接口隔离原则主要约束接口。

举个例子:

img

首先解释一下这个图的意思:

「犬科」类依赖「接口 I」中的方法:「捕食」,「行走」,「奔跑」; 「鸟类」类依赖「接口 I」中的方法「捕食」,「滑翔」,「飞翔」。

「宠物狗」类与「鸽子」类分别是对「犬科」类与「鸟类」类依赖的实现。

对于具体的类:「宠物狗」与「鸽子」来说,虽然他们都存在用不到的方法,但由于实现了「接口 I」,所以也 必须要实现这些用不到的方法,这显然是不好的设计。

如果将这个设计修改为符合接口隔离原则的话,就必须对「接口 I」进拆分。

img

在这里,我们将原有的「接口 I」拆分为三个接口,拆分之后,每个类只需实现自己需要的接口即可。

依赖倒置原则(DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.– Robert C. Martin

高层策略性的代码不应该依赖实现底层细节的代码。

这话听起来就让人听不明白,我来翻译一下。大概就是说在写代码的时候,应该多使用稳定的抽象接口,少依赖多变的具体实现。

举个例子:

看下面这段代码:

public class Test {

public void studyJavaCourse() {
System.out.println("张三正在学习 Java 课程");
}

public void studyDesignPatternCourse() {
System.out.println("张三正在学习设计模式课程");
}
}
复制代码

上层直接调用:

public static void main(String[] args) {
Test test = new Test();
test.studyJavaCourse();
test.studyDesignPatternCourse();
}
复制代码

这样写乍一看并没有什么问题,功能也实现的好好的,但仔细分析,却并不简单。

第一个问题:

如果张三又新学习了一门课程,那么就需要在 Test() 类中增加新的方法。随着需求增多,Test() 类会变得非常庞大,不好维护。

而且,最理想的情况是,新增代码并不会影响原有的代码,这样才能保证系统的稳定性,降低风险。

第二个问题:

Test() 类中方法实现的功能本质上都是一样的,但是却定义了三个不同名字的方法。那么有没有可能把这三个方法抽象出来,如果可以的话,代码的可读性和可维护性都会增加。

第三个问题:

业务层代码直接调用了底层类的实现细节,造成了严重的耦合,要改全改,牵一发而动全身。

基于 DIP 来解决这个问题,势必就要把底层抽象出来,避免上层直接调用底层。

img

抽象接口:

public interface ICourse {
void study();
}
复制代码

然后分别为 JavaCourseDesignPatternCourse 编写一个类:

public class JavaCourse implements ICourse {

@Override
public void study() {
System.out.println("张三正在学习 Java 课程");
}
}

public class DesignPatternCourse implements ICourse {

@Override
public void study() {
System.out.println("张三正在学习设计模式课程");
}
}
复制代码

最后修改 Test() 类:

public class Test {

   public void study(ICourse course) {
       course.study();
  }
}
复制代码

现在,调用方式就变成了这样:

public static void main(String[] args) {
   Test test = new Test();
   test.study(new JavaCourse());
   test.study(new DesignPatternCourse());
}
复制代码

通过这样开发,上面提到的三个问题得到了完美解决。

其实,写代码并不难,通过什么设计模式来设计架构才是最难的,也是最重要的。

所以,下次有需求的时候,不要着急写代码,先想清楚了再动手也不迟。

这篇文章写的特别辛苦,主要是后半部分理解起来有些困难。而且有一些原则也确实没有使用经验,单靠文字理解还是差点意思,体会不到精髓。

其实,文章中的很多要求我都做不到,总结出来也相当于是对自己的一个激励。以后对代码要更加敬畏,而不是为了实现功能草草了事。写出健壮,优雅的代码应该是每个程序员的目标,与大家共勉。


作者:yongxinz
链接:https://mp.weixin.qq.com/s/xWZmP4qBI8cm68UZH6AXOg

收起阅读 »

中年程序员写给35岁的自己

笔者是一名程序员老司机,局限于笔者文笔一般,想到哪写到哪__胡乱写一通,篇幅较长,希望通过文章的方式简单的回顾过去、总结现在和展望未来,顺便记录一下,方便以后总结。开端最近刚刷完《开端》这个电视剧,感慨万千,男女主在一次次的循环里,逐步完善信息,排除每个人的嫌...
继续阅读 »

笔者是一名程序员老司机,局限于笔者文笔一般,想到哪写到哪__胡乱写一通,篇幅较长,希望通过文章的方式简单的回顾过去、总结现在和展望未来,顺便记录一下,方便以后总结。

开端

最近刚刷完《开端》这个电视剧,感慨万千,男女主在一次次的循环里,逐步完善信息,排除每个人的嫌疑,最终找到阻止高压锅爆炸的办法。

人生亦如此,经过一次次的失败之后,最终都会到达自己的目的,结局有好有坏罢了。

人生的每一次的总结,其实就是下一次循环的起点。

时间都去哪了

一晃神自己都已经35了,虽然不愿意承认,但是时间就这么一点一滴地过去了。

记得09年毕业刚工作的时候,听着同事们谈论买房、炒股、养娃的事情,当时感觉这个事情离自己是多么的遥远,但现在看来真的就好像是昨天发生的事情,当时的情景还很清晰的印在脑海里,甚至他们谈论时候的表情、语气、肢体语言都还清晰的记得。

自从过了30岁以后,每一年过生日都会比较感慨,总是会问一下自己,这一年的时间都去哪了呢,如果这一年可以重新循环一次,自己又会怎样去选择。

回想

2022年,一个充满2的一年,自己的第14个工作年,回想我这14年的工作经历,有得有失,有太多的心酸。

第一个5年可以说是比较顺利的,从一个小兵,通过自己的全身心地投入,逐渐成长为一个小组的负责人。

前面为第二个5年奠定了一个新的起点,并在新的角色上继续沉淀自己的技术和团队管理。用最近的话说,也是一路“卷”过来的。

第3个5年比较坎坷,自己并不满意,未能达成自己期望的目标,时间也耗费过去了,有遗憾、有决策失误、有不甘心,但不能就此认命,不能后悔,因为未来的路上仍需要全力以赴。

地下室

09年7月,一毕业就自己拖着一个行李箱,坐上了开往北京的火车,开启了北漂之旅。

大学期间就听说北京的程序员很好找工作,一个月能够拿到1万5的高薪,当时北京的房价估计也就1万不到,当时就想自己抓紧学习,也能早点拿到1万5的薪水,所以坚定地选择了北漂。

初生牛犊不怕虎,拿着家里最后一次给的3千块钱,工作和住宿都还没有着落,还好当时的北京地下室很流行,都是给很多想要来北京打拼,但是又支付不起高昂的房租的人准备的,里面很多像我这样的年轻人,还有起早贪黑做路边早餐生意的摊贩,所以我很有幸成为了其中的一员。

找工作也算比较顺利,是来北京之前,在技术论坛里联系的一个版主老闫,来北京第二天就去他公司面试了,当着老闫和宫姐(后来才知道,宫姐是老闫的媳妇,从国企离职了,老闫专门为她开的一个公司)的面用PHP写了一个表单上传界面,直接就被录取了,对于没有经验的我来说,这是对我很大的肯定。

之后就踏实的开启了一年的地下室-公司两点一线的生活,早晨从负三层走上地面,每天从黑暗走向光明,出门挤公交到公司,晚上接着回去地下三层,只有2-4平米的地下室,没有网络,没有娱乐,偶尔去网吧和当时的女朋友(现在的老婆)视频,这一年非常充实,2000块钱工资仅够生活,周末加一天班还能多拿几百,但是精力充沛,学习为主,非常充实。

到现在我还很怀念这一年经历,虽然已经很久没有联系过老闫和宫姐了,非常感谢能有这一段工作经历,教会了我很多。

养成强大的内心

为什么要拥有一颗强大的内心?因为无论是工作中,还是生活中,肯定会遇到各式各样的困难与挫折,如果轻而易举的就被压垮,轻而易举的说放弃,那如何能够在职业道路上走的更远,又怎样担负起自己的家庭的责任。

头一两年工作的时候,其实很多不懂的东西,只能靠自己不断的百度和Google,所以经常会晚上经常梦到自己程序写不下去的情况,压力很大,特别是接到一个比较大的项目的时候,晚上还会睡不着。

但这些年通过项目的历练,和与更优秀的人共事,逐渐让自己养成了强大的内心。当你需要同时肩负家庭与工作的双重压力时,如果内心不够强大的话,崩溃或者抑郁就会找上来,每个人都是一步步走过来的,放眼我们这代人,我接触的人当中都是非常皮实,都是在一次次被压垮崩溃之后,自我调节又振作起来,让自己的内心逐渐的强大起来。

我在给团队招人的时候,优先会考察候选人皮实、乐观这两个方面。

“皮实”的反义词是“玻璃心”,是人都会犯错,都会有迷茫的时候,皮实的人能够听得进去别人的建议,并且把压力化解为动力,从而逐渐承担更多的职责。

乐观的人不仅能够带动组内的气氛,还是个积极向上的人,不会轻易否定自己,经过适当的培养和引导,很快就能成为一个独当一面的角色。

个人成长

我最精力旺盛最能拼搏的阶段是在优酷,和后来的阿里大文娱,快7年的时间。在这段时间,买房、结婚、生娃,可以说是把最重要的事情都做了,这段时间里从一个小兵,踏实的完成好领导安排的每一项任务,把自己当成owner的角色要求自己,到后来晋升为技术leader,也正好经历了互联网最辉煌的那几年。

非常喜欢优酷的文化氛围,合作第一,一人一口,很怀念和当时的同事们一起共事的经历。

这7年里收获了许多做事情的方式和方法,跟对了靠谱的领导,也结识了很多优秀的同事。总结下来就是,强大的内心,真诚待人,踏实稳重,厚积薄发

创业

有创业的念头是在第二个5年的后半段,当时事业上进入了舒适区,也可以说是瓶颈期,感觉安逸下来就会落后。所以在挣扎了比较长的一段时间后,放弃了留在阿里拿着股票修福报的机会,裸辞寻找创业机会(这个决定其实是有点头脑发热的,在选择的话,估计会骑驴找马),同时照顾也想参与一下娃的成长阶段。

我所说的创业,并不是大家想象中的,自己当老板那种创业,我期望的是能跟着一个早期的业务,经历一步步的做大做强的过程,也就是0-1的过程,从而能够更加的皮实自己。

所以后来创业经历过短视频、区块链、民宿这些业务的,项目从0-1的过程是我最大的收获,投入度和抗压能力,这个是创业必备的,只要能看着数据库的用户数据一天比一天成倍的增长,能验证项目的价值,这就是创业能坚持下去的动力,虽然最想要的“钱”没到位。

创业都会有风险,周期长,充满不确定性,会受政策和资金的影响,有成功有失败,这一点在从阿里出来的时候,就已经做好了心理准备,这也是经历了多个项目的原因,1-10 这个阶段不是努力就能达到的,天时地利人和一个都少不了。

未来的世界总是在不断的变化,谁也没能想到2019年“新冠”这个病毒就这样默默的流行起来了,这是所有人人生计划当中的一个很大的变数,疫情下面一切都变得不再那么重要,活着最重要,家人健康最重要。所以创业的计划也就没法继续下去了,还是选择先回归到了大厂继续“卷”吧,毕竟还有家庭的责任需要担起来,希望人类战胜疫情的那一天早日到来。

技术

技术是程序员吃饭的工具,也可以说是本能,任何时候都应该保持这种本能,所以不断更新自己的技术储备,是作为一个程序员的必修课。

危机感

活到老学到老,这个应该不是只有程序员这个工种需要做的事情,任何岗位都应该如此(机关单位除外),在这个竞争如此激烈的环境里,不进则退,危机感是每个人都应该时刻保持的状态,除非你真的可以做到躺平。

一线编程是时刻都要保持的能力,再忙也还是需要抽出时间去练习巩固,架构能力固然重要,落地能力也很重要。

希望等我50、60岁的时候,也还能够保持技术的能力,虽然不知道未来的世界会是怎样,但秉承内心即可。

学会正确的做事

工作到了一定阶段,只要是技术能够搞定的事情,那都是最简单的工作。想要把技术做好,真正实施之前,先要透过现象看清事物的本质,谋定而后动厚积而薄发。这个也是和之前的领导那里学到的,所以“和优秀的人共事,你也会变得优秀”

培养做事的方式方法,列提纲,想清楚,写明白,凡事都要写下来,然后印在脑海里。

以下是我习惯的流程提纲,能完善好其中的每个点,我认为是能够在工作中做到应对自如的:

事件分析
背景调查
人物关系
痛点梳理
方向确立
制定短期、中期、长期目标
详细方案
时间线
过程跟踪
阶段复盘,调整方向
复制代码

正确看待技术的更新迭代

我现在虽然是做着最前端的工作,但是我并不仅仅把自己定义为前端工程师这个角色,我不想把自己局限在某个设定好的框架里。同样头几年做后端的时候,到后来让我带领H5团队的时候,我并没有觉得这个是一个不好的选择,相反正因为有了后端的经历,才让我在前端这个领域上持续深耕,更加的从容和淡定。

时代在变,人也在变,技术也在变,技术总是不断地更新迭代,作为研发,需要有一颗进取的心,努力跟上时代的潮流,而不是被后浪拍死在沙滩上。

就拿前端来说,我最早接触的框架是雅虎的Yui,后来到jQuery,再到后来nodejs的出现,使得前端技术有了爆炸式的增长和变化,所以才有了Angular、React、Vue等这些框架的出现。

这几年前端领域就出现了很多新名词新框架,比如跨端、severless、lowcode、D2C、P2C、esbuild、Flutter等等,这些名词背后的核心,其实还是围绕着如何提升研发效能,如何最大化研发效率,如何优化运行性能展开的。所以无论技术如何的变化,事物的本质并没有变,而是我们通过不断的研究和探索,不断的找到了更优的解决办法,从而替代之前的架构更好更快的达成目标。

摆正姿态,正确对待技术的更新迭代,同时也要不断的把自身的基建打牢,打通任督二脉,这样无论来什么武功都能融会贯通。

某一方面的技术专家

“技术专家”这个词,我个人理解的是:在某个技术领域里,不仅有业界影响力,而且技术的深度也是非常厉害,能够利用所掌握的技能,完美解决工作中的各种疑难杂症,带着团队其他人一起进步。

不得不承认,我自己并没有达到自己所想的预期,虽然顶着技术专家的头衔,但技术深度远远不够的,而且业界也确实没啥影响力啊,有点惭愧,未来任重道远啊。

这么多年技术语言接触了不少,比如PHP、Python、Java、Javascript、go,但是似乎每一项都是点到为止,都是为了解决当下业务痛点,临时上手学习,并且运用到实际的业务中,之后再没有更深入的去研究了。除了前端,这个是因为真的感兴趣,所以一直也在持续的关注和投入,所以未来应该还是会在这个方向上一直走下去,结合实际的业务更加的深入,争取达到自己内心的期望。

焦虑

说一下“焦虑”,也可以说是“中年危机”,带来的焦虑,最近正好在看一本书《认知觉醒:开启自我改变的原动力》,作者写下这段话的时候,正好36岁:

在很长一段时间内,我就像一个没有睡醒的人,对自己不了解,对生活没主张,对命运无选择。那时的我,虽然对本职工作非常投入,但业余时间几乎被不需要动脑筋的事情占据:有空就找朋友们聚会,时常喝到烂醉;经常熬夜,从不主动看书、运动;打发时间的方式就是看搞笑视频、读八卦新闻、玩手机游戏;实在没事可做,就裹起被子睡大觉……下意识中,我觉得这种无忧的生活会继续下去。

人之所以会焦虑,是因为每个人都会有自己的期望,当担心这个期望无法达成,亦或者短时间内无法达成的时候,人就会产生一种焦虑的情绪。

作为一个35岁的码农,很自然地会把自己和“中年危机”关联到一起。

因为担心自己精力不够,无法胜任高强度的工作。

因为担心自己思维不再灵活,无法做到全面地思考。

因为担心自己家庭原因,无法平衡家庭与工作。

因为担心自己期望过高,无法得到很好的上升空间。

因为担心自己失业,无法再承担起家庭的责任。

非常害怕迷迷糊糊地到了某个年纪,突然发现自己对这个世界已经无能为力了:梦想与现实落差巨大,生活和工作压力缠身,而优秀的同龄人已绝尘而去。一时间,焦虑急躁又如梦初醒:

“为什么没有早点知道这个世界的真相?”

“为什么没有在最好的年纪及时觉醒?”

但即使含泪拷问,也似乎错过了最佳时机,毕竟人生是个单行道,无法从头再来。

最后不得不敲碎那颗高傲的心,在无奈和叹息中默默接受平庸的人生。

我接下来要做的,就是努力找到一个平衡,学会与焦虑共存,有欲望就会有焦虑,比如看书、写文章、利用好业余时间充实自己,当自己足够强大的时候,或许就能进入另外一种心境。

工作与家庭

新的工作

自己已经在新的岗位上满一年了,这一年在地图的领域上学到了不少新的知识,跟着优秀的人一起共事,很幸运能和渲染大牛一起推进,地图上的数据可视化引擎,围绕着webgl,将时空数据落地到地理三维空间上,从调研,到研发,到落地,这个过程需要吸收很多新鲜的名词,比如墨卡托、瓦片、Geojson等,并且真正落地到了具体的代码上。

一年的工作非常快,在适应和彷徨中,就这么过去了,甚至还没来得及思考总结,实际产出距离自己的期望还有很大的差距,和政策、和业务、和投入度也存在一些关系,但技术上也确实得到了不少新的积累。

娃上学

今年恰巧赶上大娃上小学,小娃上小班,所以可能更多时间也确实是投入到了家庭上,最痛苦的莫过于,不知道如何教育孩子。这个年代的小孩和当初小时候的我们,思维和眼界都有很大的提升,所以无形中会把自己小时候未完成的期望强加到自己的儿女身上,这样反而带来了自己更多的焦虑。

感谢媳妇,感谢两个妈一直照顾孩子,不然真是难以平衡工作与家庭,但是教育和健康方面,还是需要我和媳妇自己多上心,包括自己的健康、儿女的健康、老人的健康。

自我认知

如果你觉得自己已经错过所谓的最好年纪,其实也没有关系,因为“现在”永远都是开始的最好时机——这不是什么安慰人的话,这是事实。“摩西奶奶”76岁开始学画、80岁举办个人展,王德顺79岁走上T台,褚时健74岁开始创业种橙子……就算你今年60岁,他们仍可以对你说:“孩子,别着急,你至少还有20年可以随时重来……”

如文章开头的大图,在一片金黄色的麦田里,一个小女孩牵着气球在里面行走,金黄色的麦穗是前35年的积累,绿色的气球代表对未来充满希望,我把自己看成了图中的小女孩,希望自己能在一个全新的世界重新学习,重新认识自己,找到一条全新的通往未来的道路。

沟通

我是一个慢热型的人,在成为leader之前,我其实还是典型的程序员性格,只愿意和代码打交道,所以也不会主动去结交别人,完全沉浸在自己的世界里。

成为leader之后,自己由内向主动向外向靠拢,逼着自己主动去和别人交流,为了团队,为了自己,努力创造共同的话题,从不排斥参加任何社交活动。

独立思考

这一点上做得不是很够,很多时候还是会照顾其他人的感受,所以还是应该多问为什么,多想想还有没有更好的答案。

情绪管理

性格温和,待人真诚,工作中除了有几次为了团队内的同学受委屈,和其他的团队leader红过脸外,基本上没有因为工作的事情发过脾气。

耐心

这点需要深深的反思和检讨,在教育小孩这件事情上,已经没有耐心可言了,经常就会因为孩子事情莫名的烦躁,特别是工作压力大的时候,心中要默念一万遍“亲生的”,才能慢慢压制心中的火气,有时候确实还会忍不住揍娃。

气场

应该没有啥气场吧,当leader也好,当大头兵也好,我一贯的作风都是,和气生财,所以也不会表现出太大的气场去压制别人。

未来的自己

希望自己能够更加的有主见,承担起更多的工作,承担起更多家庭的责任,合理利用自己的碎片时间,把时间都花在有价值的地方。找到一种减压的方式,不发脾气,认真享受生活,结交更多的人脉。

明年的Flag

目标不宜过多,这些目标都是业余时间完成。

读书

每个月一本书,一年完成至少10本书的学习计划,学以致用,而不是读完就忘。

写文章

一周完成一篇原创文章,不限类别

早睡早起

每天不晚于11:30休息

总结

总共6千多字的篇幅,以散文的形式,写给35岁的自己,简单地回顾过去、总结现在、展望未来,希望当36岁的自己回过头来看的时候,能够鄙视现在的自己,写出更好的《写给36岁的自己》。


作者:唐小锅
来源:https://juejin.cn/post/7058907526842023973

收起阅读 »

十分钟搞懂手机号码一键登录

手机号码一键登录是最近两三年出现的一种新型应用登录方式,比之前常用的短信验证码登录又方便了不少。登陆时,应用首先向用户展示带有本机号码掩码的授权登录页面,用户点击“同意授权”的按钮之后,应用即可获取到完整的本机号码,从而完成用户的登录认证。在这个过程中,应用只...
继续阅读 »

手机号码一键登录是最近两三年出现的一种新型应用登录方式,比之前常用的短信验证码登录又方便了不少。登陆时,应用首先向用户展示带有本机号码掩码的授权登录页面,用户点击“同意授权”的按钮之后,应用即可获取到完整的本机号码,从而完成用户的登录认证。在这个过程中,应用只要确认登录用的手机号码是在绑定了此号码的手机上发起的即可认证成功,从这一点来看,它和短信验证码登录并无本质区别,都是一种设备认证登录方式。这篇文章就来捋一下其中的技术门道。

这几年为了保护用户的隐私安全,Android和iOS系统都限制了应用获取本机号码的能力,即使通过某些技术手段获取到了本机号码,这个号码还可能是被篡改的,所以应用直接读取本机号码用于登录是不可行的。那么这些应用是怎么获取到真实的本机号码的呢?答案是电信运营商,手机要打电话、要上网、要计费,运营商肯定能对应到正确的手机号码。国内的运营商就是移动、联通、电信这三家,它们都开放了这种能力。对于在互联网大潮中被管道化的运营商来说,不失为一种十分有意义的积极进取。

手机流量上网的原理

手机号码一键登录是借助手机流量上网来实现,所以先要搞清楚流量上网的原理。

目前网上已有很多关于一键登录的技术文章,但是内容基本雷同,关于获取手机号码的部分,所述都是通过运营商的数据网关能力,语焉不详,对于有追求的技术人来说,难以忍受。这个章节就来介绍下这种从数据网关获取手机号码的能力是如何实现的,因为通信专业知识十分繁杂,我也没有经过专业的学习,大家也不想接触到很多的专业名词,所以这里只保留一些关键的专业名词,尽量以通俗易懂的方式来理清这个机制。

五层网络模型

对网络比较熟悉的同学,应该了解五层协议,那么手机流量上网时的五层网络模型有何不同呢?


从上图可以看出,手机流量上网的主要区别在数据链路层和物理层。在数据链路层,流量上网没有MAC地址的概念,它采用一种点对点协议(PPP),手机端通过拨号方式建立这种PPP连接,然后发送数据。在物理层,流量上网通过手机内置的基带模块进行无线信号的调制、解调工作,从而实现与移动基站之间的电磁波通信。

流量上网的机制

点对点协议支持身 验证功能,手机端发起连接时会携带自己的身 粉证明,一般就是手机卡内置的IMSI,这个IMSI也会保存在运营商的数据库中,因此基站就可以验证连接用户的身 ,当然这个验证过程不是简单的对比IMSI,会有更多安全机制。为了更清楚的了解流量上网机制,下面再来一张4G流量上网时手机与运营商的交互示意图:


核心组件

手机:这其中对流量上网起到关键作用的就是手机卡和基带模块。手机卡中保存了IMSI,全称International Mobile Subscriber Identification Number,国际移动用户识别码。IMSI是手机卡的身 标识。

基站:就是外边常见的铁架子信号塔,是一种能覆盖一定范围的无线电收发信息电台,手机会连接到它,然后它再通过光纤连接到运营商网络,从而实现移动通信。

MME:Mobility Management Entity,移动控制单元。手机建立连接时会先访问到这里,负责:手机与基站的接入控制,手机卡的鉴权、会话管理、安全传输,漫游控制、跨运营商通信等。

HSS:Home Subscriber Server,归属签约用户服务器。保存本地签约的手机卡信息,包括手机卡IMSI与手机号的对应关系,手机号的套餐信息、手机号的归属地信息等。

S-GW:Service Gateway,服务网关。4G环境下,用户侧与运营商核心网之间的业务网关。访问能不能进入,能做什么业务,去哪里做业务,是在这里控制的。跨运营商计费、漫游计费等也在这里完成。

P-GW:PDN Gateway,PDN网关。运营商核心网与互联网之间的网关,手机真正上网就是通过它了。它会给手机分配一个IP地址,控制上网的速度,对流量进行计费等。

PCRF:Policy and Charging Rules Function,策略与计费控制单元,保存每个用户的网络访问策略和计费规则。

上网过程

为了方便理解,这里将上网的过程大致分为两个部分(和上图的1、2对应):

  • 1 接入:建立连接时,手机携带IMSI信息,通过基站访问到MME,MME通过HSS验证IMSI信息,然后MME进行一些初始化工作,返回一些鉴权参数给手机,手机再进行一些计算,然后把计算结果返回给MME,MME验证手机的计算结果,验证通过则允许接入。这个过程保证了接入的安全,MME还为后续的数据传输提供了加密传输支持,保护数据不被窃听和篡改,有兴趣的同学可以去详细了解下。

    如果手机卡销售的时候没有写入手机号,手机卡首次注册登记的时候,运营商会从HSS中取出手机号,然后再写入手机卡中。

    实际应用中,为了防止跟踪和攻击,不是每次通信时都要携带IMSI,MME会生成一个临时的GUTI对应到IMSI,就像Web程序中的SessionId。MME还有一定的机制控制GUIT的重新分配。

  • 2 传输:手机网络流量的传输,还是先要通过基站,然后下一步进入S-GW,S-GW会检查用户的授权,就像Web程序中检查前端提交过来的SessionId,再看看用户有没有权限进行其提交的业务,这里就是看看用户有没有开通流量上网,这是S-GW通过连接MME实现的。S-GW处理完毕后,数据包会进入P-GW,P-GW在手机使用流量上网时会给用户分配一个IP地址,然后数据包通过网关进入互联网,访问到相关的资源。P-GW还会对上网行为进行速率控制、流量计费等操作,这些策略来源于PCRF,PCRF中的规则是根据HSS中的用户套餐、用户等级等计算出来的。

    对P-GW来说S-GW屏蔽了用户的移动性,手机在多个基站切换时,S-GW不变。

以上就是手机流量上网的基本原理了,可以看到,运营商通过IMSI或者GUTI完全有能力获取到当前上网用户的手机号码。对于运营商的一键登录具体是怎么实现的,我并没有找到相关的介绍,但是可以设想下:手机应用通过运营商的SDK发起获取手机号码的业务请求,此时会携带IMSI或者GUTI,业务请求到达S-GW,S-GW鉴权通过,然后将这个业务请求路由到运营商核心网中获取手机号码的服务,服务根据业务规则从HSS中取出手机号码并进行若干处理。

一键登录的原理

理解了手机流量上网的原理,再来看下一键登录业务是如何实现的,这个部分属于上层应用程序开发,大家应该相对熟悉一些。

如果你接入过微信的第三方应用登录,或者其他类似的第三方应用登录,过程是差不多的。还是先来看图:


这里对一些关键步骤进行说明:

  • 2预取手机号掩码:这个手机号掩码需要在请求用户授权的页面展示给用户看,因为获取这个信息要通过电信运营商的网络,所以可能会比较慢,为了提升用户体验,可以在应用启动的时候就去获取,然后缓存一段时间。

  • 8授权请求:因为应用获取用户手机号这个事比较敏感,必须让用户清楚的了解并授权之后才能进行,为了确保这件事,运营商的认证SDK提供了这个授权请求页面,用户确认授权后,SDK直接向运营商认证服务发起请求认证,认证服务会返回一个认证Token给应用。应用再通过自己的服务端拿着这个Token找运营商获取手机号码。

  • 17生成应用授权Token:应用要维护自己用户的登录状态,这里可以采用传统的Session机制,也可以使用JWT机制。

  • 3预取手机号掩码 和 11请求认证,都需要通过手机蜂窝网络通信,也就说需要通过手机流量上网。如果手机同时开启了流量和WIFI,认证SDK会将手机短暂切换到流量上网模式。如果手机没有开启流量,有些SDK还会在上次成功取号之后多缓存一个临时Token,这样也能成功实现一次一键登录,不过这个限制性很大。

这里其实还有一个安全问题

14登录请求:用户如果随便造一个认证Token,然后就向应用服务提交请求,应用服务再向认证服务提交请求,这属于一种跨站攻击。虽然这个Token可以被阻止,但是不免浪费资源,给服务端带来压力。

这一点微信第三方应用登录做的比较好,用户登录前,应用服务端先生成一个随机数,然后应用前端向应用服务端提交时,带着这个随机数,应用服务端可以验证这个随机数。

号码验证场景

除了用于登录,运营商网关的这种取号能力,还可以用在验证手机号上,在某些关键业务上,比如支付过程中,要求用户输入本机手机号码或者其中的某几位,然后通过运营商认证服务验证手机号是否本机号码。

隐私保护问题

设备唯一标识问题

现在大家对隐私问题关注的越来越多了,经常会出现这种情况:你在某电商网站搜索了某个商品,然后访问其它网站时,都向你推荐这类商品的广告。还有一种感觉很恐怖的情况,你刚和某个人谈论了某件事,然后就在某个App上看到了关于这件事的推荐,有人猜测是App在偷听,不过基于目前的舆论和监督,偷听风险太大,这其中的原因可能真的只是算法太厉害了。

最近几年Android和iOS系统都对App获取手机唯一标识进行了限制,比如IMEI、Mac地址、序列号、广告Id等,目的就是防止用户的信息在多个App之间进行关联,导致泄漏用户的隐私,产生一些安全问题和法律风险,前述跨App的广告行为也自然受到了抑制。

在了解一键登录的技术原理时,看到某运营商提供了一种和SIM卡绑定的设备唯一Id服务,宣传语就是为了应对移动操作系统限制访问手机唯一标识的问题,在现今越来越重视隐私保护的前提下,如果这种能力开放给了广告平台,就是开历史的倒车了。

手机号作为身 份标识的问题

对于国内普遍使用手机号登录的方式,从技术上很难限制App之间进行手机号关联,然后综合分析用户的行为。比如某家大厂运营了多款不同种类的热门App,它就有能力更全面的了解某个用户,如果要限制可能就得通过法律层面来解决了。至于不同厂商之间的手机号关联行为,基于商业利益的保护,不太可能会出现。

在国内这种商业环境下,如果你真的对自己的隐私很关注,最好只使用账号密码的方式登录,否则经常更换手机号可能是一种没办法的办法。

手机号重新销售问题

手机号的总量是有限的,为了有效利用手机号资源,手机号注销以后,经过一段时间就会被运营商重新销售。如果新的手机号拥有者拿着这个手机号登录某个APP,而这个手机号之前已经在这个App上注册过,产生了大量的使用记录,那么此手机号前拥有者的隐私就会被泄漏。所以大家现在都不太敢随便更换手机号,因为注册过的地方太多了,留下了数不清的使用痕迹。

在了解一键登录的技术原理时,还看到某运营商提供了一种“手机号更换绑定SIM卡通知”的服务,应用可以据此解绑重新销售的手机号与应用账号之间的关系,从而保护用户的隐私。在上文中已经提过手机卡使用IMSI进行标识,如果手机号被重新销售,就会绑定新的IMSI,运营商可以据此产生通知。当然运营商还需要排除手机卡更换和携号转网的情况,这些情况下手机号也会绑定新的IMSI。

不得不说运营商的这个服务还是挺赞的👍。


作者:萤火架构
来源:https://juejin.cn/post/7059182505101885471

收起阅读 »

Google 如何看待 Kotlin 与 Android

先进 简洁 安全。 在语法表现上,Kotlin够简洁明了。不防看看:你应该切换到Kotlin开发,它包含了零默认值和不可变性的安全特性,使你的Android应用程序在默认情况下是安全的 并且性能是良好的。 代码更安全 编写更安全的代码,并在应用程序中避免 发生...
继续阅读 »

先进 简洁 安全。


在语法表现上,Kotlin够简洁明了。不防看看:你应该切换到Kotlin开发,它包含了零默认值和不可变性的安全特性,使你的Android应用程序在默认情况下是安全的 并且性能是良好的。


代码更安全


编写更安全的代码,并在应用程序中避免 发生Nullpointerexception。


var output: String
output = null // Compilation error==================================val name: String? = null // Nullable type
println(name.length()) // Compilation error

语法更易读和简洁


Data Classes


更加专注于表达你自己的代码创意设计,无需编写更多的样板代码。


// Create a POJO with getters, setters, equals(), hashCode(), toString(), and copy() with a single line:
data class User(val name: String, val email: String)

Lambdas语法


使用lambda来简化你的代码。


button.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
doSomething();
}
});

button.setOnClickListener { doSomething() }    

默认的命名参数


通过使用默认参数减少重载函数的数量。使用命名参数调用函数,使自己的代码更具有可读性。


fun format(str: String,
normalizeCase: Boolean = true,
upperCaseFirstLetter: Boolean = true,
divideByCamelHumps: Boolean = false,
wordSeparator: Char = ' ') {

}==================================// Call function with named arguments.
format(str, normalizeCase = true, upperCaseFirstLetter = true)

和 findViewById 说再见


在你自己的代码中避免findViewById() 调用。专注于写你的逻辑,而不需要那么繁琐。


import kotlinx.android.synthetic.main.content_main.*class MainActivity : AppCompatActivity() {   override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// No need to call findViewById(R.id.textView) as TextView
textView.text = "Kotlin for Android rocks!" }
}

扩展功能, 而不是用继承


扩展函数和属性使你可以轻松地扩展类的功能,而无需继承它们。调用代码是可读和自然的。


// Extend ViewGroup class with inflate function
fun ViewGroup.inflate(layoutRes: Int): View {
return LayoutInflater.from(context).inflate(layoutRes, this, false)
}==================================// Call inflate directly on the ViewGroup instance
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = parent.inflate(R.layout.view_item)
return ViewHolder(v)
}

100%的和Java可互操作性


在你非常不是想用Java的情况下,尽量多地使用Kotlin。Kotlin是一种与Java完全可互操作的JVM语言。


// Calling Java code from Kotlin
class KotlinClass {
fun kotlinDoSomething() {
val javaClass = JavaClass()
javaClass.javaDoSomething()
println(JavaClass().prop)
}
}==================================// Calling Kotlin code from Java
public class JavaClass {
public String getProp() { return "Hello"; }
public void javaDoSomething() {
new KotlinClass().kotlinDoSomething();
}
}

强大的开发工具支持


Android Studio 3.0 提供了不错的工具来帮助你开始使用Kotlin开发。在将Java代码粘贴到Kotlin文件时,可以转换整个Java文件或转换一段代码片段。很稳!


image.png


Kotlin 是开放的


与Android一样,Kotlin是Apache 2.0下的一个开源项目。Google对 Kotlin 的选择重申了Android对开发者 开放生态系统的承诺,随着 Google 的发展和 Android平台的发展,Google 希望 kotlin 语言的发展, 也很高兴看到 kotlin 语言的发展。


image.png


Tamic的一些话


Java 10 的 新特性也刚好( Java 10 新特性解密)迎合kotlin的某些特性一样,以后即将用var 来定义变量和类。 因此我们发现Koltin将来必定是开发者所关注的一名语言趋势,假如有一天,Google像抛弃 Eclispe,投坏Android Studio一样,放弃对Java的支持,到时候,至少你还能掌握kotlin开发,不然,你是要转行吗?


相关视频


Android进阶开发:函数与方法有本质区别 你知道吗


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

超难面试题:Android 为什么设计只有主线程更新UI

选择方案的选择 单线程更新UI 多线程更新UI 从问题本身考虑就两个方案不是单线程就是多线程。 下面是个人根据具体情况分析,如果有什么不对的地方,欢迎指正。 从开发效率来看,开发一个 单线程UI 库,会显得非常容易,而且,每个控件的运行效率肯定会比多线...
继续阅读 »

选择方案的选择



  1. 单线程更新UI

  2. 多线程更新UI


从问题本身考虑就两个方案不是单线程就是多线程。



下面是个人根据具体情况分析,如果有什么不对的地方,欢迎指正。



从开发效率来看,开发一个 单线程UI 库,会显得非常容易,而且,每个控件的运行效率肯定会比多线程的效率高,比如我们单线程可以使用HashMap,多线程就需要使用JUC 框架下的类库了,这个效率肯定比 HashMap低很多,这样就很好理解。编写一个多线程的UI库,很可能每个控件,都会加锁,控件本身效率就低了,但是这样还不够 ,后面会解释。


还有一个简单方案,就是对真个UI库,加锁,而不是具体某个控件,就是确保同一时刻,只能有一个线程,对整个UI系统更新,这个已经有点单线程更新UI的意思了 。但是锁的粒度会很大,如果一个页面100个控件,相当于每个控件都加锁了。


这个方案实现起来倒是不复杂,只需要设计一个boolean变量就可以,任何线程需要更新UI 都会访问这个变量获取锁,这个方案会造成所有的线程都竞争同一把锁,单从运行效率分析,应该是很高的,但是这个竞争特别激烈,可能造成的问题就是,事件响应不够及时,


单线程更新UI方案简单成熟


单线程更新UI方案,从上面的分析来看,优势就很明显,整体设计可能是最简单的,每个控件的设计只需要考虑单线程运行就可以,完全不必关系其他线程更新UI。


而且这套方案非常成熟,在Android 之前,swing qt windows 几乎绝大部分图形界面api 都会使用这个单线程方案。


从执行效率看


前面说了,如果一个加锁的api 和不加锁的api 比较,那肯定不加锁效率高对吧,但是,这么说确实很笼统,如果合理设计一套多线程更新ui 的库,整体性能未必会比单线程差,只是想实现这样一套系统的复杂程度,可能不只是翻倍那么简单,设计越复杂,带来的问题是 潜在bug 可能会多,但是这些,在设计ui系统 的时候未必是这样考虑的,如果业务复杂,效果会更好,那么我相信大部分企业还是会设计一个复杂的系统的。


综合考虑?


多线程更新UI,不管如何设计都会绕不开一个问题,就是竞争,而这个竞争,是整个UI系统的,而不是单独一个控件,大部分情况下,一个线程可能同时更新的是过个控件,而要确保我一次更新的所有控件是同步更新的,所以要保证这个逻辑,其实我们就要确保一个问题,同一时刻。永远只允许一个线程去更新UI。不能保证这一点,就会造成业务逻辑可能各种问题,甚至各种死锁。


既然同一个时刻只能一个线程更新,那设计成单线程是不是就更好呢,到这里,其实还是不够全面的,还有个因素就是事件相应。如果多线程更新的情况下,其实这个是不容易实现的, 反而单线程,就好实现一些。


总结


通过分析总结几个点。



  1. 一般UI还是要保证同一时刻只有一个线程在更新,所以效率不会更高。

  2. 多线程更新UI实现上会复杂一些,Java的内部人员发布过文章也说过这个几乎不可实现。

  3. 从响应速度角度分析,单线程可以设计出更好的响应速度的api

  4. 单线程更新,也是一个被证明效果非常好的方案。


从过个角度分析 Android 为什么设计只有主线程更新UI 都是最好的选择。


不过回答这个问题需要理解的不全是结论,而是对这个问题,和图形界面开发的理解。
如果说效率高,安全,也需要回答出来为什么。这些不是凭空说的。真的效率高吗?高在哪里?都需要说清楚,可能会有不正确的地方。但是只要把需要考虑的点表达清晰就好


引用


负责Swing开发的一个大师的一篇博客《Multithreaded toolkits: A failed dream?》


也有人说单新ui 效率会高,因为多线程会加锁。如果有人能把这个细节解释清楚呢,希望留言。因为正常设计也只是锁更新那一行代码而已,我的总结就是效率不分伯仲,希望大家探讨吧。


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

IDEA 中玩转 Git

Git
Git 有很多客户端工具,不过感觉还是命令行操作最好用,方便又快捷,不过命令行操作需要小伙伴们对 Git 命令比较熟练,这可能对有的人来说会有一些难度,所以,客户端工具有时候也不能抛弃,如果非要推荐一个 Git 客户端工具,我觉得还是 IDEA 中的 Git ...
继续阅读 »

Git 有很多客户端工具,不过感觉还是命令行操作最好用,方便又快捷,不过命令行操作需要小伙伴们对 Git 命令比较熟练,这可能对有的人来说会有一些难度,所以,客户端工具有时候也不能抛弃,如果非要推荐一个 Git 客户端工具,我觉得还是 IDEA 中的 Git 插件吧。其他的 Git 客户端工具松哥之前也有体验过一些,不过感觉还是 IDEA 中的用起来更加省事。


今天这篇文章算是我第二次教大家在开发工具中使用 Git 了,刚毕业的时候,松哥写过一篇文章,教大家在 Eclipse 中使用 Git,那时候在 Eclipse 中使用 Git 是真的麻烦,光是插件就要安装半天,刚刚翻了一下那篇文章,已经是七年前的事情了。



七年之后,Eclipse 也没了往日的风光,IDEA 逐渐成了开发的主流工具,咱们今天就来捋一捋 IDEA 中使用 Git。


1. 基本配置


首先你要安装 Git,这个就不需要我多说了,IDEA 上默认也是安装了 Git 插件,可以直接使用。


为了给小伙伴们演示方便,我这里使用 GitHub 作为远程仓库,如果还有人不清楚 GitHub 和 Git 的区别,可以在公众号江南一点雨底部菜单栏查看 Git 教程,看完了就明白了。


从 2021.08.13 号开始,IDEA 上配置 GitHub 有一个小小的变化,即不能使用用户名密码的方式登录了,如果你尝试用用户名/密码的方式登录 GitHub 提交代码,会收到如下提示:


Support for password authentication was removed on August 13, 2021. 
Please use a personal access token instead.

在 IDEA 上使用用户名/密码的方法登录 GitHub 也会报如下错误:



需要我们点击右上角的 Use Token,使用令牌的方式登录 GitHub,令牌的生成方式如下:



  1. 网页上登录你的 GitHub 账号。

  2. 点击右上角,选择 Settings:




  1. 拉到最下方,选择左边的 Developer settings:




  1. 选择左边的 Personal access tokens,然后点击右上角的 Generate new token:




  1. 填一下基本信息,选一下权限即可(权限需要选择 repo 和 gist,其他根据自己的需求选择):




  1. 最后会生成一个令牌,拷贝到 IDEA 中即可,如下:




这就是基本配置。


小伙伴们在公司做开发,一般是不会将 GitHub 作为远程仓库的,那么这块根据自己实际情况来配置就行了。


2. clone


头一天上班,首先上来要先 clone 项目下来,IDEA 中有对应的 clone 工具,我们直接使用即可:




这块也可以直接选择下面的 GitHub,然后直接从自己的 GitHub 仓库上拉取新代码。


clone 完成之后,IDEA 会提示是否打开该项目,选择 yes 即可。


代码 clone 下来之后,就可以根据松哥前文介绍的 Git Flow 开始开发了。


3. 分支


假设我们先创建 develop 和 release 分支,创建方式如下,选中当前工程,右键单击,然后依次选择 Git->Repository->Branches...



或者依次点击顶部的 VCS->Git->Branches...



当然两个方式都比较麻烦,直接点击 IDEA 的右下角最为省事,也是最常用的办法,如下图:



选择 New Branch,然后创建新的分支,勾选上 Checkout 表示分支创建成功后,切换到该分支上,如下:



选择一个分支,然后点击 Checkout,可以切换到该分支上:



接下来我们把 develop 分支提交到远程仓库,如下:




我们没有修改代码,所以直接点击 Push 按钮提交即可。


提交完成后,develop 后面多了 origin 前缀,Remote Branches 中也多了 develop 分支,说明提交成功。



现在假设我们想从 develop 上拉一个名为 feature-login 的分支,来完成登录功能,如下:




从创建的日志中,我们能看到 feature-login 确实是来自 develop:



好啦,接下来我们就可以愉快的开启一天的工作啦~


feature-login 上的功能开发完成后,首先点击 IDEA 的右上角完成本地仓库的提交,如下图:




填入提交的 Message,下方也能看到不同版本的内容对比,点击右下角完成代码提交,注意这个只是提交到本地仓库。


由于我们并不会将 feature-login 提交到远程仓库,所以接下来我们要将 feature-login 合并到 develop 然后将最新的 develop push 到远程仓库,操作方式如下:



  1. 切换回 develop 分支。

  2. 选择 feature-login->Merge into Current 进行合并。



合并完成后,如需删除 feature-login 分支,也可以在 IDEA 日志中顺手删除:



不过上面介绍的合并是快速合并,即让 develop 的指针指向了 feature-login,很多时候我们可能需要加上 --no-ff 参数来合并,那么步骤如下:


从 feature-login 切换回 develop 分支,然后如下:




此时我们看一眼提交日志,如下:



从这日志中也可以看出,此时不是快速合并模式了!


最后,选择 develop->Push,将代码提交到远程仓库。


4. pull


在 IDEA 中,如需从远程仓库中更新代码,点击右上角的按钮即可,如下图:



好啦,这就是一个大致的流程。


当然 Git 博大精深,IDEA 中支持的功能也非常多,其他功能就需要小伙伴们自己来摸索了,有不明白的欢迎留言讨论。


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

求你别自己瞎写工具类了,Spring 自带的这些他不香吗?

断言 断言是一个逻辑判断,用于检查不应该发生的情况 Assert 关键字在 JDK1.4 中引入,可通过 JVM 参数-enableassertions开启 SpringBoot 中提供了 Assert 断言工具类,通常用于数据合法性检查 // ...
继续阅读 »

断言



  1. 断言是一个逻辑判断,用于检查不应该发生的情况

  2. Assert 关键字在 JDK1.4 中引入,可通过 JVM 参数-enableassertions开启

  3. SpringBoot 中提供了 Assert 断言工具类,通常用于数据合法性检查


// 要求参数 object 必须为非空(Not Null),否则抛出异常,不予放行  
// 参数 message 参数用于定制异常信息。  
void notNull(Object object, String message)  
// 要求参数必须空(Null),否则抛出异常,不予『放行』。  
// 和 notNull() 方法断言规则相反  
void isNull(Object object, String message)  
// 要求参数必须为真(True),否则抛出异常,不予『放行』。  
void isTrue(boolean expression, String message)  
// 要求参数(List/Set)必须非空(Not Empty),否则抛出异常,不予放行  
void notEmpty(Collection collection, String message)  
// 要求参数(String)必须有长度(即,Not Empty),否则抛出异常,不予放行  
void hasLength(String text, String message)  
// 要求参数(String)必须有内容(即,Not Blank),否则抛出异常,不予放行  
void hasText(String text, String message)  
// 要求参数是指定类型的实例,否则抛出异常,不予放行  
void isInstanceOf(Class type, Object obj, String message)  
// 要求参数 `subType` 必须是参数 superType 的子类或实现类,否则抛出异常,不予放行  
void isAssignable(Class superType, Class subType, String message)  

对象、数组、集合


ObjectUtils



  1. 获取对象的基本信息


// 获取对象的类名。参数为 null 时,返回字符串:"null"   
String nullSafeClassName(Object obj)  
// 参数为 null 时,返回 0  
int nullSafeHashCode(Object object)  
// 参数为 null 时,返回字符串:"null"  
String nullSafeToString(boolean[] array)  
// 获取对象 HashCode(十六进制形式字符串)。参数为 null 时,返回 0   
String getIdentityHexString(Object obj)  
// 获取对象的类名和 HashCode。 参数为 null 时,返回字符串:""   
String identityToString(Object obj)  
// 相当于 toString()方法,但参数为 null 时,返回字符串:""  
String getDisplayString(Object obj)  


  1. 判断工具


// 判断数组是否为空  
boolean isEmpty(Object[] array)  
// 判断参数对象是否是数组  
boolean isArray(Object obj)  
// 判断数组中是否包含指定元素  
boolean containsElement(Object[] array, Object element)  
// 相等,或同为 null时,返回 true  
boolean nullSafeEquals(Object o1, Object o2)  
/*  
判断参数对象是否为空,判断标准为:  
   Optional: Optional.empty()  
      Array: length == 0  
CharSequence: length == 0  
 Collection: Collection.isEmpty()  
        Map: Map.isEmpty()  
*/  
boolean isEmpty(Object obj)  


  1. 其他工具方法


// 向参数数组的末尾追加新元素,并返回一个新数组  
<A, O extends A> A[] addObjectToArray(A[] array, O obj)  
// 原生基础类型数组 --> 包装类数组  
Object[] toObjectArray(Object source)  

StringUtils



  1. 字符串判断工具


// 判断字符串是否为 null,或 ""。注意,包含空白符的字符串为非空  
boolean isEmpty(Object str)  
// 判断字符串是否是以指定内容结束。忽略大小写  
boolean endsWithIgnoreCase(String str, String suffix)  
// 判断字符串是否已指定内容开头。忽略大小写  
boolean startsWithIgnoreCase(String str, String prefix)   
// 是否包含空白符  
boolean containsWhitespace(String str)  
// 判断字符串非空且长度不为 0,即,Not Empty  
boolean hasLength(CharSequence str)  
// 判断字符串是否包含实际内容,即非仅包含空白符,也就是 Not Blank  
boolean hasText(CharSequence str)  
// 判断字符串指定索引处是否包含一个子串。  
boolean substringMatch(CharSequence str, int index, CharSequence substring)  
// 计算一个字符串中指定子串的出现次数  
int countOccurrencesOf(String str, String sub)  


  1. 字符串操作工具


// 查找并替换指定子串  
String replace(String inString, String oldPattern, String newPattern)  
// 去除尾部的特定字符  
String trimTrailingCharacter(String str, char trailingCharacter)   
// 去除头部的特定字符  
String trimLeadingCharacter(String str, char leadingCharacter)  
// 去除头部的空白符  
String trimLeadingWhitespace(String str)  
// 去除头部的空白符  
String trimTrailingWhitespace(String str)  
// 去除头部和尾部的空白符  
String trimWhitespace(String str)  
// 删除开头、结尾和中间的空白符  
String trimAllWhitespace(String str)  
// 删除指定子串  
String delete(String inString, String pattern)  
// 删除指定字符(可以是多个)  
String deleteAny(String inString, String charsToDelete)  
// 对数组的每一项执行 trim() 方法  
String[] trimArrayElements(String[] array)  
// 将 URL 字符串进行解码  
String uriDecode(String source, Charset charset)  


  1. 路径相关工具方法


// 解析路径字符串,优化其中的 “..”   
String cleanPath(String path)  
// 解析路径字符串,解析出文件名部分  
String getFilename(String path)  
// 解析路径字符串,解析出文件后缀名  
String getFilenameExtension(String path)  
// 比较两个两个字符串,判断是否是同一个路径。会自动处理路径中的 “..”   
boolean pathEquals(String path1, String path2)  
// 删除文件路径名中的后缀部分  
String stripFilenameExtension(String path)   
// 以 “. 作为分隔符,获取其最后一部分  
String unqualify(String qualifiedName)  
// 以指定字符作为分隔符,获取其最后一部分  
String unqualify(String qualifiedName, char separator)  

CollectionUtils



  1. 集合判断工具


// 判断 List/Set 是否为空  
boolean isEmpty(Collection<?> collection)  
// 判断 Map 是否为空  
boolean isEmpty(Map<?,?> map)  
// 判断 List/Set 中是否包含某个对象  
boolean containsInstance(Collection<?> collection, Object element)  
// 以迭代器的方式,判断 List/Set 中是否包含某个对象  
boolean contains(Iterator<?> iterator, Object element)  
// 判断 List/Set 是否包含某些对象中的任意一个  
boolean containsAny(Collection<?> source, Collection<?> candidates)  
// 判断 List/Set 中的每个元素是否唯一。即 List/Set 中不存在重复元素  
boolean hasUniqueObject(Collection<?> collection)  


  1. 集合操作工具


// 将 Array 中的元素都添加到 List/Set 中  
<E> void mergeArrayIntoCollection(Object array, Collection<E> collection)    
// 将 Properties 中的键值对都添加到 Map 中  
<K,V> void mergePropertiesIntoMap(Properties props, Map<K,V> map)  
// 返回 List 中最后一个元素  
<T> T lastElement(List<T> list)    
// 返回 Set 中最后一个元素  
<T> T lastElement(Set<T> set)   
// 返回参数 candidates 中第一个存在于参数 source 中的元素  
<E> E findFirstMatch(Collection<?> source, Collection<E> candidates)  
// 返回 List/Set 中指定类型的元素。  
<T> T findValueOfType(Collection<?> collection, Class<T> type)  
// 返回 List/Set 中指定类型的元素。如果第一种类型未找到,则查找第二种类型,以此类推  
Object findValueOfType(Collection<?> collection, Class<?>[] types)  
// 返回 List/Set 中元素的类型  
Class<?> findCommonElementType(Collection<?> collection)  

文件、资源、IO 流


FileCopyUtils



  1. 输入


// 从文件中读入到字节数组中  
byte[] copyToByteArray(File in)  
// 从输入流中读入到字节数组中  
byte[] copyToByteArray(InputStream in)  
// 从输入流中读入到字符串中  
String copyToString(Reader in)  


  1. 输出


// 从字节数组到文件  
void copy(byte[] in, File out)  
// 从文件到文件  
int copy(File in, File out)  
// 从字节数组到输出流  
void copy(byte[] in, OutputStream out)   
// 从输入流到输出流  
int copy(InputStream in, OutputStream out)   
// 从输入流到输出流  
int copy(Reader in, Writer out)  
// 从字符串到输出流  
void copy(String in, Writer out)  

ResourceUtils



  1. 从资源路径获取文件


// 判断字符串是否是一个合法的 URL 字符串。  
static boolean isUrl(String resourceLocation)  
// 获取 URL  
static URL getURL(String resourceLocation)   
// 获取文件(在 JAR 包内无法正常使用,需要是一个独立的文件)  
static File getFile(String resourceLocation)  


  1. Resource


// 文件系统资源 D:...  
FileSystemResource  
// URL 资源,如 file://... http://...  
UrlResource  
// 类路径下的资源,classpth:...  
ClassPathResource  
// Web 容器上下文中的资源(jar 包、war 包)  
ServletContextResource  

// 判断资源是否存在  
boolean exists()  
// 从资源中获得 File 对象  
File getFile()  
// 从资源中获得 URI 对象  
URI getURI()  
// 从资源中获得 URI 对象  
URL getURL()  
// 获得资源的 InputStream  
InputStream getInputStream()  
// 获得资源的描述信息  
String getDescription()  

StreamUtils



  1. 输入


void copy(byte[] in, OutputStream out)  
int copy(InputStream in, OutputStream out)  
void copy(String in, Charset charset, OutputStream out)  
long copyRange(InputStream in, OutputStream out, long start, long end)  


  1. 输出


byte[] copyToByteArray(InputStream in)  
String copyToString(InputStream in, Charset charset)  
// 舍弃输入流中的内容  
int drain(InputStream in)   

反射、AOP


ReflectionUtils



  1. 获取方法


// 在类中查找指定方法  
Method findMethod(Class<?> clazz, String name)   
// 同上,额外提供方法参数类型作查找条件  
Method findMethod(Class<?> clazz, String name, Class<?>... paramTypes)   
// 获得类中所有方法,包括继承而来的  
Method[] getAllDeclaredMethods(Class<?> leafClass)   
// 在类中查找指定构造方法  
Constructor<T> accessibleConstructor(Class<T> clazz, Class<?>... parameterTypes)   
// 是否是 equals() 方法  
boolean isEqualsMethod(Method method)   
// 是否是 hashCode() 方法   
boolean isHashCodeMethod(Method method)   
// 是否是 toString() 方法  
boolean isToStringMethod(Method method)   
// 是否是从 Object 类继承而来的方法  
boolean isObjectMethod(Method method)   
// 检查一个方法是否声明抛出指定异常  
boolean declaresException(Method method, Class<?> exceptionType)   


  1. 执行方法


// 执行方法  
Object invokeMethod(Method method, Object target)    
// 同上,提供方法参数  
Object invokeMethod(Method method, Object target, Object... args)   
// 取消 Java 权限检查。以便后续执行该私有方法  
void makeAccessible(Method method)   
// 取消 Java 权限检查。以便后续执行私有构造方法  
void makeAccessible(Constructor<?> ctor)   


  1. 获取字段


// 在类中查找指定属性  
Field findField(Class<?> clazz, String name)   
// 同上,多提供了属性的类型  
Field findField(Class<?> clazz, String name, Class<?> type)   
// 是否为一个 "public static final" 属性  
boolean isPublicStaticFinal(Field field)   


  1. 设置字段


// 获取 target 对象的 field 属性值  
Object getField(Field field, Object target)   
// 设置 target 对象的 field 属性值,值为 value  
void setField(Field field, Object target, Object value)   
// 同类对象属性对等赋值  
void shallowCopyFieldState(Object src, Object dest)  
// 取消 Java 的权限控制检查。以便后续读写该私有属性  
void makeAccessible(Field field)   
// 对类的每个属性执行 callback  
void doWithFields(Class<?> clazz, ReflectionUtils.FieldCallback fc)   
// 同上,多了个属性过滤功能。  
void doWithFields(Class<?> clazz, ReflectionUtils.FieldCallback fc,   
                 ReflectionUtils.FieldFilter ff)   
// 同上,但不包括继承而来的属性  
void doWithLocalFields(Class<?> clazz, ReflectionUtils.FieldCallback fc)   

AopUtils



  1. 判断代理类型


// 判断是不是 Spring 代理对象  
boolean isAopProxy()  
// 判断是不是 jdk 动态代理对象  
isJdkDynamicProxy()  
// 判断是不是 CGLIB 代理对象  
boolean isCglibProxy()  


  1. 获取被代理对象的 class


// 获取被代理的目标 class  
Class<?> getTargetClass()  

AopContext



  1. 获取当前对象的代理对象


Object currentProxy()

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

使用MyBatis拦截器后,摸鱼时间又长了。?

场景 在后端服务开发时,现在很流行的框架组合就是SSM(SpringBoot + Spring + MyBatis),在我们进行一些业务系统开发时,会有很多的业务数据表,而表中的信息从新插入开始,整个生命周期过程中可能会进行很多次的操作。 比如,我们在某网站购...
继续阅读 »

场景


在后端服务开发时,现在很流行的框架组合就是SSM(SpringBoot + Spring + MyBatis),在我们进行一些业务系统开发时,会有很多的业务数据表,而表中的信息从新插入开始,整个生命周期过程中可能会进行很多次的操作。


比如,我们在某网站购买一件商品,会生成一条订单记录,在支付完金额后订单状态会变为已支付,等最后我们收到订单商品,这个订单状态会变成已完成等。


假设我们的订单表t_order结果如下:



当订单创建时,需要设置insert_byinsert_timeupdate_byupdate_time的值;


在进行订单状态更新时,则只需要更新update_byupdate_time的值。


那应该如何处理呢?


麻瓜做法


最简单的做法,也是最容易想到的做法,就是在每个业务处理的代码中,对相关的字段进行处理。


比如订单创建的方法中,如下处理:


public void create(Order order){
// ...其他代码
// 设置审计字段
Date now = new Date();
order.setInsertBy(appContext.getUser());
order.setUpdateBy(appContext.getUser());
order.setInsertTime(now);
order.setUpdateTime(now);
orderDao.insert(order);
}

订单更新方法则只设置updateByupdateTime


public void update(Order order){
// ...其他代码

// 设置审计字段
Date now = new Date();
order.setUpdateBy(appContext.getUser());
order.setUpdateTime(now);
orderDao.insert(order);
}

这种方式虽然可以完成功能,但是存在一些问题:



  • 需要在每个方法中按照不同的业务逻辑决定设置哪些字段;

  • 在业务模型变多后,每个模型的业务方法中都要进行设置,重复代码太多。


那我们知道这种方式存在问题以后,就得找找有什么好方法对不对,往下看!


优雅做法


因为我们持久层框架更多地使用MyBatis,那我们就借助于MyBatis的拦截器来完成我们的功能。


首先我们来了解一下,什么是拦截器?


什么是拦截器?


MyBatis的拦截器顾名思义,就是对某些操作进行拦截。通过拦截器可以对某些方法执行前后进行拦截,添加一些处理逻辑。


MyBatis的拦截器可以对Executor、StatementHandler、PameterHandler和ResultSetHandler 接口进行拦截,也就是说会对这4种对象进行代理。


拦截器设计的初衷就是为了让用户在MyBatis的处理流程中不必去修改MyBatis的源码,能够以插件的方式集成到整个执行流程中。


比如MyBatis中的ExecutorBatchExecutorReuseExecutorSimpleExecutorCachingExecutor,如果这几种实现的query方法都不能满足你的需求,我们可以不用去直接修改MyBatis的源码,而通过建立拦截器的方式,拦截Executor接口的query方法,在拦截之后,实现自己的query方法逻辑。


在MyBatis中的拦截器通过Interceptor接口表示,该接口中有三个方法。


public interface Interceptor {

Object intercept(Invocation invocation) throws Throwable;

Object plugin(Object target);

void setProperties(Properties properties);

}

plugin方法是拦截器用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理。


当返回的是代理的时候我们可以对其中的方法进行拦截来调用intercept方法,当然也可以调用其他方法。


setProperties方法是用于在Mybatis配置文件中指定一些属性的。


使用拦截器更新审计字段


那么我们应该如何通过拦截器来实现我们对审计字段赋值的功能呢?


在我们进行订单创建和修改时,本质上是通过MyBatis执行insert、update语句,MyBatis是通过Executor来处理的。


我们可以通过拦截器拦截Executor,然后在拦截器中对要插入的数据对象根据执行的语句设置insert_by,insert_time,update_by,update_time等属性值就可以了。


自定义拦截器


自定义Interceptor最重要的是要实现plugin方法和intercept方法。


plugin方法中我们可以决定是否要进行拦截进而决定要返回一个什么样的目标对象。


intercept方法就是要进行拦截的时候要执行的方法。


对于plugin方法而言,其实Mybatis已经为我们提供了一个实现。Mybatis中有一个叫做Plugin的类,里面有一个静态方法wrap(Object target,Interceptor interceptor),通过该方法可以决定要返回的对象是目标对象还是对应的代理。


但是这里还存在一个问题,就是我们如何在拦截器中知道要插入的表有审计字段需要处理呢?


因为我们的表中并不是所有的表都是业务表,可能有一些字典表或者定义表是没有审计字段的,这样的表我们不需要在拦截器中进行处理。


也就是说我们要能够区分出哪些对象需要更新审计字段


这里我们可以定义一个接口,让需要更新审计字段的模型都统一实现该接口,这个接口起到一个标记的作用。


public interface BaseDO {
}

public class Order implements BaseDO{

private Long orderId;

private String orderNo;

private Integer orderStatus;

private String insertBy;

private String updateBy;

private Date insertTime;

private Date updateTime;
//... getter ,setter
}

接下来,我们就可以实现我们的自定义拦截器了。


@Component("ibatisAuditDataInterceptor")
@Intercepts({@Signature(method = "update", type = Executor.class, args = {MappedStatement.class, Object.class})})
public class IbatisAuditDataInterceptor implements Interceptor {

private Logger logger = LoggerFactory.getLogger(IbatisAuditDataInterceptor.class);

@Override
public Object intercept(Invocation invocation) throws Throwable {
// 从上下文中获取用户名
String userName = AppContext.getUser();

Object[] args = invocation.getArgs();
SqlCommandType sqlCommandType = null;

for (Object object : args) {
// 从MappedStatement参数中获取到操作类型
if (object instanceof MappedStatement) {
MappedStatement ms = (MappedStatement) object;
sqlCommandType = ms.getSqlCommandType();
logger.debug("操作类型: {}", sqlCommandType);
continue;
}
// 判断参数是否是BaseDO类型
// 一个参数
if (object instanceof BaseDO) {
if (SqlCommandType.INSERT == sqlCommandType) {
Date insertTime = new Date();
BeanUtils.setProperty(object, "insertedBy", userName);
BeanUtils.setProperty(object, "insertTimestamp", insertTime);
BeanUtils.setProperty(object, "updatedBy", userName);
BeanUtils.setProperty(object, "updateTimestamp", insertTime);
continue;
}
if (SqlCommandType.UPDATE == sqlCommandType) {
Date updateTime = new Date();
BeanUtils.setProperty(object, "updatedBy", userName);
BeanUtils.setProperty(object, "updateTimestamp", updateTime);
continue;
}
}
// 兼容MyBatis的updateByExampleSelective(record, example);
if (object instanceof ParamMap) {
logger.debug("mybatis arg: {}", object);
@SuppressWarnings("unchecked")
ParamMap<Object> parasMap = (ParamMap<Object>) object;
String key = "record";
if (!parasMap.containsKey(key)) {
continue;
}
Object paraObject = parasMap.get(key);
if (paraObject instanceof BaseDO) {
if (SqlCommandType.UPDATE == sqlCommandType) {
Date updateTime = new Date();
BeanUtils.setProperty(paraObject, "updatedBy", userName);
BeanUtils.setProperty(paraObject, "updateTimestamp", updateTime);
continue;
}
}
}
// 兼容批量插入
if (object instanceof DefaultSqlSession.StrictMap) {
logger.debug("mybatis arg: {}", object);
@SuppressWarnings("unchecked")
DefaultSqlSession.StrictMap<ArrayList<Object>> map = (DefaultSqlSession.StrictMap<ArrayList<Object>>) object;
String key = "collection";
if (!map.containsKey(key)) {
continue;
}
ArrayList<Object> objs = map.get(key);
for (Object obj : objs) {
if (obj instanceof BaseDO) {
if (SqlCommandType.INSERT == sqlCommandType) {
Date insertTime = new Date();
BeanUtils.setProperty(obj, "insertedBy", userName);
BeanUtils.setProperty(obj, "insertTimestamp", insertTime);
BeanUtils.setProperty(obj, "updatedBy", userName);
BeanUtils.setProperty(obj, "updateTimestamp", insertTime);
}
if (SqlCommandType.UPDATE == sqlCommandType) {
Date updateTime = new Date();
BeanUtils.setProperty(obj, "updatedBy", userName);
BeanUtils.setProperty(obj, "updateTimestamp", updateTime);
}
}
}
}
}
return invocation.proceed();
}

@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties properties) {
}
}

通过上面的代码可以看到,我们自定义的拦截器IbatisAuditDataInterceptor实现了Interceptor接口。


在我们拦截器上的@Intercepts注解,type参数指定了拦截的类是Executor接口的实现,method 参数指定拦截Executor中的update方法,因为数据库操作的增删改操作都是通过update方法执行。


配置拦截器插件


在定义好拦截器之后,需要将拦截器指定到SqlSessionFactoryBeanplugins中才能生效。所以要按照如下方式配置。


<bean id="transSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="transDataSource" />
<property name="mapperLocations">
<array>
<value>classpath:META-INF/mapper/*.xml</value>
</array>
</property>
<property name="plugins">
<array>
<!-- 处理审计字段 -->
<ref bean="ibatisAuditDataInterceptor" />
</array>
</property>

到这里,我们自定义的拦截器就生效了,通过测试你会发现,不用在业务代码中手动设置审计字段的值,会在事务提交之后,通过拦截器插件自动对审计字段进行赋值。


小结


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

当Synchronized遇到这玩意儿,有个大坑,要注意!

你好呀,我是歪歪。 前几天在某技术平台上看到别人提的关于 Synchronized 的一个用法问题,我觉得挺有意思的,这个问题其实也是我三年前面试某公司的时候遇到的一个真题,当时不知道面试官想要考什么,没有回答的特别好,后来研究了一下就记住了。 所以看到这个问...
继续阅读 »

你好呀,我是歪歪。


前几天在某技术平台上看到别人提的关于 Synchronized 的一个用法问题,我觉得挺有意思的,这个问题其实也是我三年前面试某公司的时候遇到的一个真题,当时不知道面试官想要考什么,没有回答的特别好,后来研究了一下就记住了。


所以看到这个问题的时候觉得特别亲切,准备分享给你一起看看:



首先为了方便你看文章的时候复现问题,我给你一份直接拿出来就能跑的代码,希望你有时间的话也把代码拿出来跑一下:


public class SynchronizedTest {

    public static void main(String[] args) {
        Thread why = new Thread(new TicketConsumer(10), "why");
        Thread mx = new Thread(new TicketConsumer(10), "mx");
        why.start();
        mx.start();
    }
}

class TicketConsumer implements Runnable {

    private volatile static Integer ticket;

    public TicketConsumer(int ticket) {
        this.ticket = ticket;
    }

    @Override
    public void run() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + "开始抢第" + ticket + "张票,对象加锁之前:" + System.identityHashCode(ticket));
            synchronized (ticket) {
                System.out.println(Thread.currentThread().getName() + "抢到第" + ticket + "张票,成功锁到的对象:" + System.identityHashCode(ticket));
                if (ticket > 0) {
                    try {
                        //模拟抢票延迟
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket-- + "张票,票数减一");
                } else {
                    return;
                }
            }
        }
    }
}

程序逻辑也很简单,是一个模拟抢票的过程,一共 10 张票,开启两个线程去抢票。


票是共享资源,且有两个线程来消费,所以为了保证线程安全,TicketConsumer 的逻辑里面用了 synchronized 关键字。


这是应该是大家在初学 synchronized 的时候都会写到的例子,期望的结果是 10 张票,两个人抢,每张票只有一个人能抢到。


但是实际运行结果是这样的,我只截取开始部分的日志:



截图里面有三个框起来的部分。


最上面的部分,就是两个人都在抢第 10 张票,从日志输出上看也完全没有任何毛病,最终只有一个人抢到了票,然后进入到第 9 张票的争夺过程。


但是下面被框起来的第 9 张票的争夺部分就有点让人懵逼了:


why抢到第9张票,成功锁到的对象:288246497
mx抢到第9张票,成功锁到的对象:288246497

为什么两个人都抢到了第 9 张票,且成功锁到的对象都一样的?


这玩意,超出认知了啊。


这两个线程怎么可能拿到同一把锁,然后去执行业务逻辑呢?


所以,提问者的问题就浮现出来了。



  • 1.为什么 synchronized 没有生效?

  • 2.为什么锁对象 System.identityHashCode 的输出是一样的?


为什么没有生效?


我们先来看一个问题。


首先,我们从日志的输出上已经非常明确的知道,synchronized 在第二轮抢第 9 张票的时候失效了。


经过理论知识支撑,我们知道 synchronized 失效,肯定是锁出问题了。


如果只有一把锁,多个线程来竞争同一把锁,synchronized 绝对是不会有任何毛病的。


但是这里两个线程并没有达成互斥的条件,也就是说这里绝对存在的不止一把锁。


这是我们可以通过理论知识推导出来的结论。



先得出结论了,那么我怎么去证明“锁不止一把”呢?


能进入 synchronized 说明肯定获得了锁,所以我只要看各个线程持有的锁是什么就知道了。


那么怎么去看线程持有什么锁呢?


jstack 命令,打印线程堆栈功能,了解一下?


这些信息都藏在线程堆栈里面,我们拿出来一看便知。


在 idea 里面怎么拿到线程堆栈呢?


这就是一个在 idea 里面调试的小技巧了,我之前的文章里面应该也出现过多次。


首先为了方便获取线程堆栈信息,我把这里的睡眠时间调整到 10s:



跑起来之后点击这里的“照相机”图标:



点击几次就会有对应点击时间点的几个 Dump 信息



由于我需要观察前两次锁的情况,而每次线程进入锁之后都会等待 10s 时间,所以我就在项目启动的第一个 10s 和第二个 10s 之间各点击一次就行。


为了更直观的观察数据,我选择点击下面这个图标,把 Dump 信息复制下来:



复制下来的信息很多,但是我们只需要关心 why 和 mx 这两个线程即可。


这是第一次 Dump 中的相关信息:



mx 线程是 BLOCKED 状态,它在等待地址为 0x000000076c07b058 的锁。


why 线程是 TIMED_WAITING 状态,它在 sleeping,说明它抢到了锁,在执行业务逻辑。而它抢到的锁,你说巧不巧,正是 mx 线程等待的 0x000000076c07b058。


从输出日志上来看,第一次抢票确实是 why 线程抢到了:



从 Dump 信息看,两个线程竞争的是同一把锁,所以第一次没毛病。


好,我们接着看第二次的 Dump 信息:



这一次,两个线程都在 TIMED_WAITING,都在 sleeping,说明都拿到了锁,进入了业务逻辑。


但是仔细一看,两个线程拿的锁是不相同的锁。


mx 锁的是 0x000000076c07b058。


why 锁的是 0x000000076c07b048。


由于不是同一把锁,所以并不存在竞争关系,因此都可以进入 synchronized 执行业务逻辑,所以两个线程都在 sleeping,也没毛病。


然后,我再把两次 Dump 的信息放在一起给你看一下,这样就更直观了:



如果我用“锁一”来代替 0x000000076c07b058,“锁二”来代替 0x000000076c07b048。


那么流程是这样的:


why 加锁一成功,执行业务逻辑,mx 进入锁一等待状态。


why 释放锁一,等待锁一的 mx 被唤醒,持有锁一,继续执行业务。


同时 why 加锁二成功,执行业务逻辑。


从线程堆栈中,我们确实证明了 synchronized 没有生效的原因是锁发生了变化。


同时,从线程堆栈中我们也能看出来为什么锁对象 System.identityHashCode 的输出是一样的。



第一次 Dump 的时候,ticket 都是 10,其中 mx 没有抢到锁,被 synchronized 锁住。


why 线程执行了 ticket-- 操作,ticket 变成了 9,但是此时 mx 线程被锁住的 monitor 还是 ticket=10 这个对象,它还在 monitor 的 _EntryList 里面等着的,并不会因为 ticket 的变化而变化。


所以,当 why 线程释放锁之后,mx 线程拿到锁继续执行,发现 ticket=9。


而 why 也搞到一把新锁,也可以进入 synchronized 的逻辑,也发现 ticket=9。


好家伙,ticket 都是 9, System.identityHashCode 能不一样吗?


按理来说,why 释放锁一后应该继续和 mx 竞争锁一,但是却不知道它在哪搞到一把新锁。


那么问题就来了:锁为什么发生了变化呢?



谁动了我的锁?


经过前面一顿分析,我们坐实了锁确实发生了变化,当你分析出这一点的时候勃然大怒,拍案而起,大喊一声:是哪个瓜娃子动了我的锁?这不是坑爹吗?



按照我的经验,这个时候不要急着甩锅,继续往下看,你会发现小丑竟是自己:



抢完票之后,执行了 ticket-- 的操作,而这个 ticket 不就是你的锁对象吗?


这个时候你把大腿一拍,恍然大悟,对着围观群众说:问题不大,手抖而已。


于是大手一挥,把加锁的地方改成这样:


synchronized (TicketConsumer.class)

利用 class 对象来作为锁对象,保证了锁的唯一性。


经过验证也确实没毛病,非常完美,打完收工。


但是,真的就收工了吗?



其实关于锁对象为什么发生了变化,还隔了一点点东西没有说出来。


它就藏在字节码里面。


我们通过 javap 命令,反查字节码,可以看到这样的信息:



Integer.valueOf 这是什么玩意?



让人熟悉的 Integer 从 -128 到 127 的缓存。


也就是说我们的程序里面,会涉及到拆箱和装箱的过程,这个过程中会调用到 Integer.valueOf 方法。具体其实就是 ticket-- 的这个操作。


对于 Integer,当值在缓存范围内的时候,会返回同一个对象。当超过缓存范围,每次都会 new 一个新对象出来。


这应该是一个必备的八股文知识点,我在这里给你强调这个是想表达什么意思呢?


很简单,改动一下代码就明白了。


我把初始化票数从 10 修改为 200,超过缓存范围,程序运行结果是这样的:



很明显,从第一次的日志输出来看,锁都不是同一把锁了。


这就是我前面说的:因为超过缓存范围,执行了两次 new Integer(200) 的操作,这是两个不同的对象,拿来作为锁,就是两把不一样的锁。


再修改回 10,运行一次,你感受一下:



从日志输出来看,这个时候只有一把锁,所以只有一个线程抢到了票。


因为 10 是在缓存范围内的数字,所以每次是从缓存中获取出来,是同一个对象。


我写这一小段的目的是为了体现 Integer 有缓存这个知识点,大家都知道。但是当它和其他东西揉在一起的时候因为这个缓存会带来什么问题,你得分析出来,这比直接记住干瘪的知识点有效一点。


但是...


我们的初始票是 10,ticket-- 之后票变成了 9,也是在缓存范围内的呀,怎么锁就变了呢?


如果你有这个疑问的话,那么我劝你再好好想想。


10 是 10,9 是 9。


虽然它们都在缓存范围内,但是本来就是两个不同的对象,构建缓存的时候也是 new 出来的:



为什么我要补充这一段看起来很傻的说明呢?


因为我在网上看到其他写类似问题的时候,有的文章写的不清楚,会让读者误认为“缓存范围内的值都是同一个对象”,这样会误导初学者。


总之一句话:请别用 Integer 作为锁对象,你把握不住。


但是...



stackoverflow


但是,我写文章的时候在 stackoverflow 上也看到了一个类似的问题。


这个哥们的问题在于:他知道 Integer 不能做为锁对象,但是他的需求又似乎必须把 Integer 作为锁对象。



stackoverflow.com/questions/6…




我给你描述一下他的问题。


首先看标号为 ① 的地方,他的程序其实就是先从缓存中获取,如果缓存中没有则从数据库获取,然后在放到缓存里面去。


非常简单清晰的逻辑。


但是他考虑到并发的场景下,如果有多个线程同一时刻都来获取同一个 id,但是这个 id 对应的数据并没有在缓存里面,那么这些线程都会去执行查询数据库并维护缓存的动作。


对应查询和存储的动作,他用的是 fairly expensive 来形容。


就是“相当昂贵”的意思,说白了就是这个动作非常的“重”,最好不要重复去做。


所以只需要让某一个线程来执行这个 fairly expensive 的操作就好了。


于是他想到了标号为 ② 的地方的代码。


用 synchronized 来把 id 锁一下,不幸的是,id 是 Integer 类型的。


在标号为 ③ 的地方他自己也说了:不同的 Integer 对象,它们并不会共享锁,那么 synchronized 也没啥卵用。


其实他这句话也不严谨,经过前面的分析,我们知道在缓存范围内的 Integer 对象,它们还是会共享同一把锁的,这里说的“共享”就是竞争的意思。


但是很明显,他的 id 范围肯定比 Integer 缓存范围大。


那么问题就来了:这玩意该咋搞啊?


我看到这个问题的时候想到的第一个问题是:上面这个需求我好像也经常做啊,我是怎么做的来着?


想了几秒恍然大悟,哦,现在都是分布式应用了,我特么直接用的是 Redis 做锁呀。


根本就没有考虑过这个问题。


如果现在不让用 Redis,就是单体应用,那么怎么解决呢?


在看高赞回答之前,我们先看看这个问题下面的一个评论:



开头三个字母:FYI。


看不懂没关系,因为这个不是重点。


但是你知道的,我的英语水平 very high,所以我也顺便教点英文。


FYI,是一个常用的英文缩写,全称是 for your information,供参考的意思。


所以你就知道,他后面肯定是给你附上一个资料了,翻译过来就是: Brian Goetz 在他的 Devoxx 2018 演讲中提到,我们不应该把 Integer 作为锁。



你可以通过这个链接直达这一部分的讲解,只有不到 30s秒的时间,随便练练听力:http://www.youtube.com/watch?v=4r2…



那么问题又来了?


Brian Goetz 是谁,凭什么他说的话看起来就很权威的样子?



Java Language Architect at Oracle,开发 Java 语言的,就问你怕不怕。


同时,他还是我多次推荐过的《Java并发编程实践》这本书的作者。


好了,现在也找到大佬背书了,接下来带你看看高赞回答是怎么说的。



前部分就不详说了,其实就是我们前面提到的那一些点,不能用 Integer ,涉及到缓存内、缓存外巴拉巴拉的...


关注划线的部分,我加上自己的理解给你翻译一下:


如果你真的必须用 Integer 作为锁,那么你需要搞一个 Map 或 Integer 的 Set,通过集合类做映射,你就可以保证映射出来的是你想要的明确的一个实例。而这个实例,就那可以拿来做锁。


然后他给出了这样的代码片段:



就是用 ConcurrentHashMap 然后用 putIfAbsent 方法来做一个映射。


比如多次调用 locks.putIfAbsent(200, 200),在 map 里面也只有一个值为 200 的 Integer 对象,这是 map 的特性保证的,无需过多解释。


但是这个哥们很好,为了防止有人转不过这个弯,他又给大家解释了一下。


首先,他说你也可以这样的写:



但这样一来,你就会多产生一个很小成本,就是每次访问的时候,如果这个值没有被映射,你都会创建一个 Object 对象。


为了避免这一点,他只是把整数本身保存在 Map 中。这样做的目的是什么?这与直接使用整数本身有什么不同呢?


他是这样解释的,其实就是我前面说的“这是 map 的特性保证的”:



当你从 Map 中执行 get() 时,会用到 equals() 方法比较键值。


两个相同值的不同 Integer 实例,调用 equals() 方法是会判定为相同的 。



因此,你可以传递任何数量的 "new Integer(5)" 的不同 Integer 实例作为 getCacheSyncObject 的参数,但是你将永远只能得到传递进来的包含该值的第一个实例。


就是这个意思:



汇总一句话:就是通过 Map 做了映射,不管你 new 多少个 Integer 出来,这多个 Integer 都会被映射为同一个 Integer,从而保证即使超出 Integer 缓存范围时,也只有一把锁。


除了高赞回答之外,还有两个回答我也想说一下。


第一个是这个:



不用关心他说的内容是什么,只是我看到这句话翻译的时候虎躯一震:



skin this cat ???


太残忍了吧。



我当时就觉得这个翻译肯定不太对,这肯定是一个小俚语。于是考证了一下,原来是这个意思:



免费送你一个英语小知识,不用客气。


第二个应该关注的回答排在最后:



这个哥们叫你看看《Java并发编程实战》的第 5.6 节的内容,里面有你要寻找的答案。


巧了,我手边就有这本书,于是我翻开看了一眼。


第 5.6 节的名称叫做“构建高效且可伸缩的结果缓存”:



好家伙,我仔细一看这一节,发现这是宝贝呀。


你看书里面的示例代码:



不就和提问题的这个哥们的代码如出一辙吗?



都是从缓存中获取,拿不到再去构建。


不同的地方在于书上把 synchronize 加在了方法上。但是书上也说了,这是最差的解决方案,只是为了引出问题。


随后他借助了 ConcurrentHashMap、putIfAbsent 和 FutureTask 给出了一个相对较好的解决方案。


你可以看到完全是从另外一个角度去解决问题的,根本就没有在 synchronize 上纠缠,直接第二个方法就拿掉了 synchronize。


看完书上的方案后我才恍然大悟:好家伙,虽然前面给出的方案可以解决这个问题,但是总感觉怪怪的,又说不出来哪里怪。原来是死盯着 synchronize 不放,思路一开始就没打开啊。


书里面一共给出了四段代码,解决方案层层递进,具体是怎么写的,由于书上已经写的很清楚了,我就不赘述了,大家去翻翻书就行了。


没有书的直接在网上搜“构建高效且可伸缩的结果缓存”也能搜出原文。


我就指个路,看去吧。


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

Flutter之GetX依赖注入Bindings使用详解

作用Bindings 主要配合 GetX 路由和依赖一起使用,作用是在路由跳转页面加载时注入当前页面所需的依赖关系。Bindings 的好处是能统一管理页面的依赖关系,当业务复杂时可能一个页面需要注入大量的依赖,此时使用 Bindings 能更方便的维护页面的...
继续阅读 »

作用

Bindings 主要配合 GetX 路由和依赖一起使用,作用是在路由跳转页面加载时注入当前页面所需的依赖关系。Bindings 的好处是能统一管理页面的依赖关系,当业务复杂时可能一个页面需要注入大量的依赖,此时使用 Bindings 能更方便的维护页面的依赖关系。

使用

前面说了 Bindings 需要结合 GetX 路由一起使用,而 GetX 路由分为普通路由别名路由,接下来分别看看如何使用。

首选创建一个自定义 Bindings 继承自 Bindings,比如计数器界面,创建一个 CounterBindings 在 dependencies 方法中注入 CounterController, 代码如下:

class CounterBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut(() => CounterController());
}
}

上面通过 lazyPut 懒加载方式注入的,也可以使用前面讲到的其他注入方式注入。

普通路由

普通路由使用 Bindings 很简单,在路由跳转时加上 binding 参数传入创建的自定义 Bindings 对象即可:

Get.to(CounterPage(), binding: CounterBinding());

Get.off(CounterPage(), binding: CounterBinding());

Get.offAll(CounterPage(), binding: CounterBinding());

这样通过路由进入 CounterPage 时就会自动调用 CounterBinding 的 dependencies 方法初始化注入对应的依赖,在 CounterPage 中就能正常使用 Get.find 获取到注入的 CounterController 对象。

别名路由

Flutter应用框架搭建(一)GetX集成及使用详解 一文中介绍了别名路由的使用,需要先创建 GetPage 确定别名与页面的关系并配置到 GetMaterialApp 的 getPages 中,使用时通过 Get.toNamed 进行路由跳转,而 Get.toNamed 方法并没有 binding 参数用于传入 Bindings。

使用别名路由时需要在创建 GetPage 时就传入 Bindings 对象,如下:

GetPage(name: "/counter", page: () => CounterPage(), binding: CounterBinding());

跳转时正常使用 Get.toNamed 就能达到同样的效果。

Get.toNamed("/counter");

别名路由与普通路由对于 Bindings 的使用上还有一个区别,普通路由只有一个 binding 参数,只能传入一个 Bindings 对象,而别名路由除了 binding 参数以外还有一个 bindings 参数,可传入 Bindings 数组。使用如下:

GetPage(
name: "/counter",
page: () => CounterPage(),
binding: CounterBinding(),
bindings: [PageABinding(), PageBBinding(), PageCBinding()]);

那 bindings 的作用是什么呢?为什么需要传入一个数组?

通常一个页面只需要一个 Bindings 用来管理页面的依赖,但是当使用到 ViewPager 等嵌套组件或者存在页面嵌套时,因为页面中嵌套的页面不是通过路由加载出来的所以无法自动调用 Bindings 的 dependencies 方法来初始化依赖关系,而嵌套的页面有可能也需要单独显示,为了提高页面的复用性也会为嵌套页面创建 Bindings ,这样当页面嵌套使用时就可以把嵌套页面的 Bindings 传入到主页面路由的 bindings 中,使用如下:

/// ViewPager 页面路由
GetPage(
name: "/viewpager",
page: () => ViewPagerPage(),
binding: ViewPagerBinding(),
bindings: [PageABinding(), PageBBinding(), PageCBinding()]);

/// 单独 PageA pageB pageC 路由
GetPage(
name: "/pageA",
page: () => PageAPage(),
binding: PageABinding(),);
GetPage(
name: "/pageB",
page: () => PageBPage(),
binding: PageBBinding(),);
GetPage(
name: "/pageC",
page: () => PageCPage(),
binding: PageCBinding(),);

/// 使用
Get.toNamed("/viewpager");

Get.toNamed("/pageA");
Get.toNamed("/pageB");
Get.toNamed("/pageC");

这样就能实现,当在 ViewPager 中使用时也能初始化 ViewPager 中嵌套页面的依赖,单独使用某个 Page 时也能正常加载依赖。

原理

前面讲了 Bindings 的作用和使用方法,下面通过源码简单分析一下 Bindings 的原理。

Bindings 是一个抽象类,只有一个 dependencies 抽象方法,源码如下:

abstract class Bindings {
void dependencies();
}

在页面路由中注册 Bindings 后,页面初始化时会调用 Bindings 的 dependencies 方法,初始化页面依赖,其调用是在 GetPageRoute 的 buildContent 中,而 GetPageRoute 是继承至 Flutter 的 PageRoute 即在路由跳转加载页面内容时调用, 核心源码如下:

Widget _getChild() {
if (_child != null) return _child!;
final middlewareRunner = MiddlewareRunner(middlewares);

/// 获取 Bindings
final localbindings = [
if (bindings != null) ...bindings!,
if (binding != null) ...[binding!]
];
/// 调用中间件的 onBindingsStart 方法
final bindingsToBind = middlewareRunner.runOnBindingsStart(localbindings);

/// 调用 Bindings 的 dependencies 方法
if (bindingsToBind != null) {
for (final binding in bindingsToBind) {
binding.dependencies();
}
}

final pageToBuild = middlewareRunner.runOnPageBuildStart(page)!;
_child = middlewareRunner.runOnPageBuilt(pageToBuild());
return _child!;
}

@override
Widget buildContent(BuildContext context) {
return _getChild();
}

源码核心代码就是在创建页面 Widget 时获取路由传入的 Bindings ,然后依次调用 Bindings 的 dependencies 方法。

其中:

  /// 获取 Bindings
final localbindings = [
if (bindings != null) ...bindings!,
if (binding != null) ...[binding!]
];
/// 调用中间件的 onBindingsStart 方法
final bindingsToBind = middlewareRunner.runOnBindingsStart(localbindings);

/// 调用 Bindings 的 dependencies 方法
if (bindingsToBind != null) {
for (final binding in bindingsToBind) {
binding.dependencies();
}
}

就是将路由中传入的 bindings 和 binding 取出放入同一个数组。然后依次调用 dependencies 方法,其中 binding 就是路由或 GetPage 中传入的 binding 参数,而 bindings 就是使用别名路由时在 ``GetPage 中传入的 Bindings 数组。

总结

本文通过介绍在 GetX 依赖注入中 Bindings 的作用以及使用方法,再结合 GetX 的源码分析了 Bindings 的实现原理,更进一步了解了 Bindings 为什么能实现页面依赖注入的管理,希望通过源码让大家更好的理解 GetX 中的 Bindings ,从而在开发中灵活使用 Bindings 管理页面所需的依赖。


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

收起阅读 »

解决 Flutter 嵌套过深,是选择函数还是自定义类组件?

前言 初学 Flutter 的时候,一个很大的感受就是组件嵌套层级很深,写下来的代码找对应的括号都找不到。比如下面这种情况,从最外层的 Scaffold 到最里层的 Image.asset,一共有7层组件嵌套。这还不算多的,最夸张是见过一个表单页面写了10多层...
继续阅读 »

前言


初学 Flutter 的时候,一个很大的感受就是组件嵌套层级很深,写下来的代码找对应的括号都找不到。比如下面这种情况,从最外层的 Scaffold 到最里层的 Image.asset,一共有7层组件嵌套。这还不算多的,最夸张是见过一个表单页面写了10多层,代码的阅读体验非常糟糕,而且如果不小心删除了一个括号要找半天才对应得上。当然,通过 VSCode 彩虹括号(Rainbow Brackets)这个插件能够一定程度上解决括号对称查找得问题,但是代码的可维护性、阅读体验还是很差。自然而然,大家会想到拆分。拆分有两种方式,一种是使用返回Widget 的函数,另一种是使用 StatelessWidget,那这两种该如何选择呢?


image.png


拆分原则


在关于这个问题的讨论上,2年前 StackOverflow 有一个经典的回答:使用函数和使用类来构建可复用得组件有什么区别?,大家可以去看看。其中提到得一个关键因素是 Flutter 框架能够检测组件树的类对象,从而提高复用性。而对于私有的方法来说 Flutter 在更新的时候并不知道该如何处理。


image.png


答主也对比了使用类和函数的优劣势。使用类构建的方式:



  • 支持性能优化,比如使用 const 构造方法,更细颗粒度的刷新;

  • 两个不同的布局切换时,能够正确地销毁对应得资源。这个我们在上篇讲 StatefulWidget 的时候有介绍过。

  • 保证正确的方式进行热重载,而使用函数可能破坏热重载。

  • 在 Widget Inspector 中可以查看得到,从而可以方便我们定位和调试问题。

  • 更友好的错误提示。当组件树出现错误时,框架会给出当前构建得组件名称,而如果使用函数的话则得不到清晰得名词。

  • 可以使用 key 提高性能。

  • 可以使用 context 提供的方法(函数式组件除非显示地传递 context)。


使用函数构建组件唯一的优势就是代码量会更少(这可以通过 functional_widget 插件解决,functional_widget 是一个通过注解将和函数式组件构建方式自动转换为类组件的代码生成插件)。


示例对比


下面我们看一段没有拆分的代码,这个仅仅是示例代码,没有任何实际意义。


class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;

@override
Widget build(BuildContext context) {
return Row(
children: [
Text('Counter: $_counter'),
Container(
child: Column(
children: [
Text('Hello'),
Row(
children: [
Text('there'),
Text('world!'),
],
),
],
),
),
],
);
}
}

括号有点多,对吧,一眼看过去都懵圈了 —— 这也是很多初次接触 Flutter 的人吐槽地方,可以说让不少人直接放弃了! 最直接的方式就是将部分代码抽离成为一个私有方法,比如像下面这样。


class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;

Widget _buildNonsenseWidget() {
return Container(
child: Column(
children: [
Text('Hello'),
Row(
children: [
Text('there'),
Text('world!'),
],
),
],
),
);
}

@override
Widget build(BuildContext context) {
return Row(
children: [
Text('Counter: $_counter'),
_buildNonsenseWidget(),
],
);
}
}

将深度嵌套的组件代码单独抽成了一个返回 Widget 的私有方法,看起来确实让代码简洁不少。
那么问题就解决了吗?我们来看一下当状态改变的时候会发生什么。
我们知道,当状态变量_counter改变后,Flutter 会调用 build 方法刷新组件。这会导致 _buildNonsenseWidget 这个方法在刷新的时候每次都会被调用,意味着每次都会创建新的组件来替换旧的组件,即便两个组件没有任何改变。而事实上,我们应该只重建那些变化的组件,从而提高性能。
现在再来看使用类组件的方式,实际上有代码模板的情况下,编写一个 StatelessWidget 非常简单。使用类组件后的代码如下所示。代码确实会比函数的方式多,但是实际上大部分不需要我们手敲。


class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;

@override
Widget build(BuildContext context) {
return Row(
children: [
Text('Counter: $_counter'),

// The deeply nesting widget is now refactored into a
// stateless const widget. No more needless rebuilding!
const _NonsenseWidget(),
],
);
}
}

class _NonsenseWidget extends StatelessWidget {
const _NonsenseWidget();

@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
Text('Hello'),
Row(
children: [
Text('there'),
Text('world!'),
],
),
],
),
);
}
}

这里注意,以为这个_NonsenseWidget 在组件得声明周期不会改变,因此使用了 const 的构造方法。这样在刷新过程中,就不会重新构建了!关于 const 可以参考之前的两篇文章。


关于 StatefulWidget,你不得不知道的原理和要点!


解密 Flutter 的 const 关键字


总结


相比使用函数构建复用的组件代码,请尽可能地使用类组件的方式,而且尽可能地将组件拆分为小一点的单元。这样一方面可以提供精确的刷新,另一方面则是可以将组件复用到其他页面中。如果你不想改变自己得习惯,那么可以考虑使用 functional_widget 这个插件来自动生成类组件。


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

一天一个经典算法:桶排序

桶排序也叫箱排序,工作的原理是将数组分到有限数量的桶里。每个桶再进行排序排序,可能再使用别的排序算法或者是以递归的方式继续使用桶排序进行排序,桶排序是鸽巢排序的一种归纳结果。 当输入在一个范围内均匀分布时,桶排序非常好用。 例如:对范围从0.0到1.0且均匀分...
继续阅读 »

桶排序也叫箱排序,工作的原理是将数组分到有限数量的桶里。每个桶再进行排序排序,可能再使用别的排序算法或者是以递归的方式继续使用桶排序进行排序,桶排序是鸽巢排序的一种归纳结果。


当输入在一个范围内均匀分布时,桶排序非常好用。


例如:对范围从0.0到1.0且均匀分布在该范围内的大量浮点数进行排序。


创建桶算法的方法:



  1. 创建n个空桶(列表)。

  2. 对每个数组元素arr[i]插入bucket[n*array[i]]

  3. 使用插入排序对各个桶进行排序

  4. 连接所有的排序桶


Java示例:


import java.util.*;
import java.util.Collections;

class GFG {

// 使用桶排序对大小为 n 的 arr[] 进行排序
static void bucketSort(float arr[], int n)
{
if (n <= 0)
return;

// 1) 创建 n 个空桶
@SuppressWarnings("unchecked")
Vector<Float>[] buckets = new Vector[n];

for (int i = 0; i < n; i++) {
buckets[i] = new Vector<Float>();
}

// 2) 将数组元素放在不同的桶中
for (int i = 0; i < n; i++) {
float idx = arr[i] * n;
buckets[(int)idx].add(arr[i]);
}

// 3) 对单个存储桶进行排序
for (int i = 0; i < n; i++) {
Collections.sort(buckets[i]);
}

// 4) 将所有桶连接到 arr[]
int index = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < buckets[i].size(); j++) {
arr[index++] = buckets[i].get(j);
}
}
}

public static void main(String args[])
{
float arr[] = { (float)0.897, (float)0.565,
(float)0.656, (float)0.1234,
(float)0.665, (float)0.3434 };

int n = arr.length;
bucketSort(arr, n);

System.out.println("排序后的数组为 ");
for (float el : arr) {
System.out.print(el + " ");
}
}
}


输出


排序后的数组为
0.1234 0.3434 0.565 0.656 0.665 0.897

性能


时间复杂度: 如果我们假设在桶中插入需要 O(1) 时间,那么上述算法的第 1 步和第 2 步显然需要 O(n) 时间。如果我们使用链表来表示桶,O(1) 很容易实现。第 4 步也需要 O(n) 时间,因为所有桶中都会有 n 个项目。 
分析的主要步骤是步骤 3。如果所有数字均匀分布,这一步平均也需要 O(n) 时间。


包含负数的情况


上面的例子是桶排序时在对大于零的数组进行排序,对于包含负数的情况需要用下述的方法解决。



  1. 将数组拆分为两部分创建两个空向量 Neg[], Pos[](分别存正数和负数)通过转换将所有负,元素存储在 Neg[],变为正数(Neg[i] = -1 * Arr[i]),将所有 +ve 存储在 pos[] (pos[i] = Arr[i])

  2. 调用函数bucketSortPositive(Pos, pos.size()),调用函数 bucketSortPositive(Neg, Neg.size()),bucketSortPositive(arr[], n)

  3. 创建n个空桶(或列表)。

  4. 将每个数组元素 arr[i] 插入 bucket[n*array[i]]

  5. 使用插入排序对单个桶进行排序。

  6. 连接所有排序的桶。


Java示例


import java.util.*;
class GFG
{

// 使用桶排序对大小为 n 的 arr[] 进行排序
static void bucketSort(Vector<Double> arr, int n)
{

// 1) 创建 n 个空桶
@SuppressWarnings("unchecked")
Vector<Double> b[] = new Vector[n];
for (int i = 0; i < b.length; i++)
b[i] = new Vector<Double>();

// 2) 将数组元素放在不同的桶中
for (int i = 0; i < n; i++)
{
int bi = (int)(n*arr.get(i)); // 桶中索引
b[bi].add(arr.get(i));
}

// 3) 对单个存储桶进行排序
for (int i = 0; i < n; i++)
Collections.sort(b[i]);

// 4) 将所有桶连接到 arr[]
int index = 0;
arr.clear();
for (int i = 0; i < n; i++)
for (int j = 0; j < b[i].size(); j++)
arr.add(b[i].get(j));
}

// 这个函数主要是把数组一分为二,然后对两个数组调用bucketSort()。
static void sortMixed(double arr[], int n)
{
Vector<Double>Neg = new Vector<>();
Vector<Double>Pos = new Vector<>();

// 遍历数组元素
for (int i = 0; i < n; i++)
{
if (arr[i] < 0)

// 通过转换为 +ve 元素来存储 -Ve 元素
Neg.add (-1 * arr[i]) ;
else

// 存储 +ve 元素
Pos.add (arr[i]) ;
}
bucketSort(Neg, (int)Neg.size());
bucketSort(Pos, (int)Pos.size());

// 首先通过转换为 -ve 存储 Neg[] 数组的元素
for (int i = 0; i < Neg.size(); i++)
arr[i] = -1 * Neg.get( Neg.size() -1 - i);

// 排序
for(int j = Neg.size(); j < n; j++)
arr[j] = Pos.get(j - Neg.size());
}

public static void main(String[] args)
{
double arr[] = {-0.897, 0.565, 0.656,
-0.1234, 0, 0.3434};
int n = arr.length;
sortMixed(arr, n);

System.out.print("排序后的数组: \n");
for (int i = 0; i < n; i++)
System.out.print(arr[i] + " ");
}0
}

**输出: **


排序后的数组:
-0.897 -0.1234 0 0.3434 0.565 0.656

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

面试官:你都工作3年了,这个算法题都不会?

前言 金三银四,又到了换工作的最佳时机,我幻想着只要跳个槽,就能离开这个”鸟地方“,拿着更多的钱,干着最爽的事... 然而现实总是残酷的,最近有个学妹在换工作,面试前什么手写Priomise、vue双向绑定原理,webpack优化方式,准备了一大堆,本以...
继续阅读 »

前言



金三银四,又到了换工作的最佳时机,我幻想着只要跳个槽,就能离开这个”鸟地方“,拿着更多的钱,干着最爽的事...




然而现实总是残酷的,最近有个学妹在换工作,面试前什么手写Priomisevue双向绑定原理,webpack优化方式,准备了一大堆,本以为成竹在胸,结果却在算法上吃了大亏,心仪的offer没有拿到,一度怀疑人生。到底是什么算法题能让面试官对妹子说出你都工作3年了,这个算法题都不会?这样的狠话?



有效的括号问题



这是一道leetcode上的原题,本意是在考察候选人对数据结构的掌握。来看看题目



给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:



  1. 左括号必须用相同类型的右括号闭合。

  2. 左括号必须以正确的顺序闭合。


示例



示例 1:
输入:s = "()"
输出:true

示例 2:
输入:s = "()[]{}"
输出:true

示例 3:
输入:s = "(]"
输出:false

示例 4:
输入:s = "([)]"
输出:false

示例 5:
输入:s = "{[]}"
输出:true


解题信息



如果咱们确实没有刷过算法,不知道那么多套路,通过题目和示例尽可能的获取到更多的信息就很重要了。



根据题目推断出:



  1. 字符串s的长度一定是偶数,不可能是奇数(一对对匹配)。

  2. 右括号前面一定跟着左括号,才符合匹配条件,具备对称性。

  3. 右括号前面如果不是左括号,一定不是有效的括号。


暴力消除法



得到了以上这些信息后,胖头鱼想既然是[]{}()成对的出现,我能不能把他们都挨个消除掉,如果最后结果是空字符串,那不就意味着符合题意了吗?



举个例子


输入:s = "{[()]}"

第一步:可以消除()这一对,结果s还剩{[]}

第二步: 可以消除[]这一对,结果s还剩{}

第三步: 可以消除{}这一对,结果s还剩'' 所以符合题意返回true


代码实现


const isValid = (s) => {
while (true) {
let len = s.length
// 将字符串按照匹配对,挨个替换为''
s = s.replace('{}', '').replace('[]', '').replace('()', '')
// 有两种情况s.length会等于len
// 1. s匹配完了,变成了空字符串
// 2. s无法继续匹配,导致其长度和一开始的len一样,比如({],一开始len是3,匹配完还是3,说明不用继续匹配了,结果就是false
if (s.length === len) {
return len === 0
}
}
}


暴力消除法最终还是可以通过leetcode的用例,就是性能差了点,哈哈


image.png


栈解题法



解题信息中的第2条强调对称性,而栈(后入先出)入栈和出栈恰好是反着来,形成了鲜明的对称性。



入栈:abc,出栈:cba


abc
cba


所以可以试试从的角度来解析:


输入:s = "{[()]}"

第一步:读取ch = {,属于左括号,入栈,此时栈内有{
第二步:读取ch = [,属于左括号,入栈,此时栈内有{[
第三步:读取ch = (,属于左括号,入栈,此时栈内有{[(
第四步:读取ch = ),属于右括号,尝试读取栈顶元素(和)正好匹配,将(出栈,此时栈内还剩{[
第五步:读取ch = ],属于右括号,尝试读取栈顶元素[和]正好匹配,将[出栈,此时栈内还剩{
第六步:读取ch = },属于右括号,尝试读取栈顶元素{和}正好匹配,将{出栈,此时栈内还剩''
第七步:栈内只能'',s = "{[()]}"符合有效的括号定义,返回true


代码实现


const isValid = (s) => {
// 空字符串符合条件
if (!s) {
return true
}

const leftToRight = {
'(': ')',
'[': ']',
'{': '}'
}
const stack = []

for (let i = 0, len = s.length; i < len; i++) {
const ch = s[i]
// 左括号
if (leftToRight[ch]) {
stack.push(ch)
} else {
// 右括号开始匹配
// 1. 如果栈内没有左括号,直接false
// 2. 有数据但是栈顶元素不是当前的右括号
if (!stack.length || leftToRight[ stack.pop() ] !== ch) {
return false
}
}
}

// 最后检查栈内还有没有元素,有说明还有未匹配则不符合
return !stack.length
}


暴力解法虽然符合我们日常的思维,但是果然还是栈结构解法好了不少。


image.png


结尾



面试中,算法到底该不该成为考核候选人的重要指标咱们不吐槽,但是近几年几乎每个大厂都将算法放进了前端面试的环节,为了获得心仪的offer,重温数据结构,刷刷题还是很有必要的,愿你我都被算法温柔以待。


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

庖丁解牛:Android stuido中 git 操作详解

Git
前言 在开发flutter或android项目,选择用Android stuido是比较方便的,在git的可视化使用上,Android studio已经非常方便了,但是还是有很多的小伙伴,依旧用其他工具来管理git。那么今天我就来详细的介绍一下 Android...
继续阅读 »

前言


在开发flutter或android项目,选择用Android stuido是比较方便的,在git的可视化使用上,Android studio已经非常方便了,但是还是有很多的小伙伴,依旧用其他工具来管理git。那么今天我就来详细的介绍一下 Android stuido的git使用与操作。


一、基本认知


git是采用分布式版本库机制。


工作区


项目目录下的文件可以称之为工作区


暂存区


增加文件,执行add操作则是把文件添加到暂存区


基本操作


git add 是将文件放到暂缓区
git commit 则是把文件添加到本地仓库
git push 则是提交到远程仓库
git status 是查看现有版本库中的文件状态


head指针


head表示的是当前版本,并不是任何分支的最新版本


二、Android studio中的git


文件样式与对应关系


在这里插入图片描述
文件1是git忽略文件
文件2是与本地分支版本一致
文件3是咱为提交到本地分支,并做了修改


假如文件是红色的样子,表示并没提交到暂存区


界面与操作


在这里插入图片描述



  1. commit 提交到本地分支 (基本操作,不做说明)

  2. add 添加到暂存区 (基本操作,不做说明)

  3. .git/info/exclude (添加到 忽略文件,是为了让文件脱离git管理,不会上传到git仓库)

  4. Annotate with Git Blame (显示每行代码的作者,如下图)


在这里插入图片描述
5. show diff (故名思义differences 差别)
6. compare with reversion(与某个版本比较)
7. compare with branch(与某个分支比较)
8. show history (查看历史)
9. show current revision (显示当前行最新修订历史版本、提示)
10. Rollback.. 在没有提交到本地库之前,丢弃工作区内容。
11. push.. 推到线上分支
12. pull.. 线上拉到本地并合并
13. fetch 线上拉到本地
14. merge.. 选择分支进行合并
15. rebase.. 选择分支进行合并,如有有合并冲突,会提示整理为一条commit直线
16. branches.. 创建分支与查看分支(这块下面快捷操作具体介绍)
17. new branch 会按照当前本地提交版本,来创建新的分支
18. new tag 给某一次提交增加个可识别的名字
19. reset HEAD
注意:Git中,用HEAD表示当前版本
在这里插入图片描述
有三个选项
Mixed:参数为默认值,暂存区的内容和工作区的内容都在工作区,提交上的都会还原到工作区
Soft:工作区的内容依旧在工作区,暂存区的内容还在暂存区(不被git管理的文件会变为新增文件)
Hard:工作区和暂存区的内容全部丢失(需要谨慎操作,一步就啥也木有了)


to commit 选择你要还原的commit号
20. stash changes..
git中如果本地有文件改动未提交、且该文件和服务器最新版本有冲突,pull更新会提示错误,无法更新:要么先commit自己的改动然后再通过pull拉取代码,stash的好处是可以先将你的改动暂存到本地仓库中,随时可以取出来再用,但是不用担心下次push到服务器时,把不想提交的改动也push到服务器上,因为Stash Changes的内容不参与commit和push。
21. unstash changes
在这里插入图片描述
View:查看
Drop:删除
Clear:清理
pop stash:移除stash
reinstate index
22. manager remote
查看git remote内容


右下角快捷操作


在这里插入图片描述
在这里插入图片描述
merge into current 合并到当前分支
rebase current onto selected 合并到当前rebase模式
chechout and rebase onto current 切换分支,并将分支合并到当前切换的分支


git面板快捷操作


在这里插入图片描述
在这里插入图片描述


compact references view 简洁引用视图
简洁引用:
在这里插入图片描述
align references to left 将引用向左对齐
引用左右对齐配置:
在这里插入图片描述


Show tag names 显示标签名称
设置是否显示标签:


在这里插入图片描述


Show long Edges 显示长线
在这里插入图片描述


Turn Intellisort On 打开intelli 排序
incase of merge show incoming commits first (directly below merge commit)
在合并的情况下,首先显示传入的提交 直接合并在下边

收起阅读 »

排序算法的基础&进阶

类型平均情况下,时间复杂度最好情况下,时间复杂度最坏情况下,时间复杂度空间复杂度稳定性冒泡排序O(n²)O(n)有序情况O(n²)无序情况O(1)稳定快速排序O(nlogn)O(nlogn)O(n²)有序情况O(logn)不稳定插入排序O(n²)O(n)有序情...
继续阅读 »
类型平均情况下,时间复杂度最好情况下,时间复杂度最坏情况下,时间复杂度空间复杂度稳定性
冒泡排序O(n²)O(n)有序情况O(n²)无序情况O(1)稳定
快速排序O(nlogn)O(nlogn)O(n²)有序情况O(logn)不稳定
插入排序O(n²)O(n)有序情况O(n²)无序情况O(1)稳定
选择排序O(n²)O(n²)O(n²)O(1)不稳定
归并排序O(nlogn)O(nlogn)O(nlogn)O(n)稳定
希尔排序O(nlog²n)O(nlog²n)O(nlog²n)O(1)不稳定

关键词含义


n:数据规模


时间复杂度


算法运行过程中所耗费的时间。


空间复杂度


算法运行过程中临时占用存储空间的大小。例如:O(1)表示所需空间大小为常量,与数据量n无关。


稳定性含义



  • 稳定:在排序之前,如果两个数相等,那么排序之后,这两个数的先后顺序不变。如排序前,a=b,a在b的前面;那么排序后,a依旧在b的前面。

  • 不稳定:在排序之前,如果两个数相等,那么排序之后,这两个数的先后顺序改变。如排序前,a=b,a在b的前面;那么排序后,a在b的后面。


冒泡排序


原理步骤



  1. 比较相邻的两个数,如果前面的数大于后面的数,就交换这两个数。

  2. 相邻的最前一对数和最后一对数都要进行比较,这样最后一个数就是最大的数。

  3. 每个元素重复以上步骤,除了最后一个数。

  4. 重复1-3的步骤。


代码实现


private static int[] bubbleSort(int array[]) {
if (array.length == 0) {
return array;
}
// 第1个for循环相当于步骤4
for (int i = 0; i < array.length; i++) {
// 第2个for循环相当于步骤3
// array.length -1 是因为后面有j+1,先-1是为了避免数组越界
// array.length -1 - i,之所以减i(已经排过1遍,就减1;如果已经排过i遍,就减i),是为了不比较排在最后且已经排好序的数,相当于步骤3的最后一句话
for (int j = 0; j < array.length - 1 - i; j++) {
int temp;
// if判断语句相当于步骤1和步骤2
if (array[j] > array[j + 1]) {
temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
return array;
}

快速排序


原理步骤



  1. 取数组中的一个数作为key。

  2. 从后往前获取数组的数,并将其与key进行对比。

  3. 如果其中一个数小于key,那么就将这个数和key交换位置。

  4. 交换位置之后,从前往后获取数组的数,并将其与key对比。

  5. 如果其中一个数大于key,那么就将这个数和key交换位置。

  6. 重复2-5的过程,直到key前面的数都比key小,key后面的数都比key大,这样就完成一次排序。

  7. 以key为中心,对key前面的数组和后面的数组执行1-6的过程,直到数组完全有序。


代码实现


private static void quickSort(int[] array, int left, int right) {
if (left >= right) {
return;
}
int i, j, x;
i = left;
j = right;
x = array[i];
while (i < j) {
while (i < j && array[j] > x) {
j--;
}
if (i < j) {
array[i] = array[j];
i++;
}
while (i < j && array[i] < x) {
i++;
}
if (i < j) {
array[j] = array[i];
j--;
}
}
// j=i
array[j] = x;
quickSort(array, j + 1, right);
quickSort(array, left, j - 1);
}

插入排序


原理



  • 每一步将一个待排序的数插入到已经排好序的序列中,直到插完所有数据。


代码实现


private static int[] insertSort(int array[]) {          
if (array.length == 0) {
return array;
}    
int i, j, temp;
// 注释①
for (i = 1; i < array.length; i++) {
// 注释②
temp = array[i];
// 注释③
for (j = i - 1; j >= 0 && array[j] > temp; j--) {
// 注释④
array[j+1] = array[j];           
}                  
// 注释⑤                  
array[j+1] = temp;           
}                  
return array;
}

注释①



  • 默认数组第一个数(i=0的数)是有序的。


注释②



  • array[i]为待排序的数据。


注释③



  • array[i]前面的数与array[i]进行排序。


注释④



  • arr[j]相当于前数,arr[j+1]相当于后数。

  • 如果前数比后数大,交换位置,前数放到后数的位置。


注释⑤



  • 如果for循环内前数和后数交换了位置(即前数挪到了后数的位置),那么注释⑤处的代码,就是将后数挪到前数的位置,实现交换。

  • 如果缺少注释⑤处的代码,那么前数的位置就会“空缺”,或者说依旧是原来的数,并没有实现交换。

  • 如果for循环内两数并没有交换(即跳出了for循环),此时j=i-1,j+1=i,与tmp=a[i]效果是一样的。


选择排序


原理步骤:



  1. 从未排序的序列中取出最小(最大)的数,放入已排序序列的初始位置;

  2. 继续从未排序序列剩余的数中取最小(最大)的数,放在已排序序列的末尾。

  3. 持续执行②的步骤,直到整个序列有序。


代码实现


private static int[] selectionSort(int[] array) {    
if (array.length == 0) {
return array;
}
for (int i = 0; i < array.length; i++) {
int min = i;
for (int j = i; j < array.length; j++) {
// 从未排序序列中获取最小值
if (array[j] < array[min]) {
min = j;
}
}
// 把获取的最小值放入已排序序列的末尾(此时i代表末尾的索引)
int temp = array[i];
array[i] = array[min];
array[min] = temp;
}
return array;
}

总结1:



  • 插入排序和选择排序可以划为一类排序算法来理解和掌握。

  • 它们都具有相同点——将数组划分为已排序、未排序两个部分,然后将未排序的部分逐个迁移到已排序的部分,最终使整个数组实现完全有序。

  • 而不同点在于从未排序合入到已排序的方式。插入排序会将未排序的数据在已排序的数组中执行直接插入排序;而选择排序会先在未排序的数组中选出最小值,当这个值合入到已排序的数组中时,不需要再进行比较,直接放到已排序数组的末尾就可以了。


希尔排序


原理步骤



  • 把一个数组按增量进行分组。(增量指分组数量)

  • 每个分组采用直接插入排序进行排序。

  • 然后减小增量,每个分组的元素数目增加,直到增量为1,整个文件变为一组,算法结束。


代码实现


private static int[] shellSort(int[] array) {    
if (array.length == 0) {
return array;
}
// gap为分组数目
for (int gap = array.length / 2; gap > 0; gap = gap / 2) {
// i为索引,对每组进行排序
for (int i = gap; i < array.length; i++) {
// j为临时变量
int j = i;
// 分组内元素的个数可能大于2个,因此使用while循环
while (j - gap >= 0 && array[j] < array[j - gap]) {
// 在同一个分组中,如果后面的数(j)比前面的(j-gap)大,就交换它们的位置
int temp = array[j];
array[j] = array[j - gap];
array[j - gap] = temp;
j = j - gap;
}
}
}
return array;
}

归并排序


原理步骤



  • 将一个数组分为左子数组和右子数组,两个子数组的长度为n/2(n为数组的总长度)。

  • 在两个子数组间进行归并排序(即每个子数组划分为更小的左子数组和右子数组,直到无法再分时,对两个数组进行排序,详情见代码)。

  • 将两个有序的子数组合并为一个最终的有序数组。


代码实现


private static int[] mergeSort(int[] array) {    
// 数组只有一个元素或没有元素,直接返回。
// 脱离递归的条件
if (array.length < 2) {
return array;
}
// 将数组分为两半,分别进行排序
int[] left = Arrays.copyOfRange(array, 0, array.length / 2);
int[] right = Arrays.copyOfRange(array, array.length / 2, array.length);
return merge(mergeSort(left),mergeSort(right));
}

/**
* 将左数组与右数组合并为一个有序数组
* 注意:此时左数组、右数组已经有序
* @param left
* @param right
* @return
*/
private static int[] merge(int[] left, int[] right) {
// 合并后的有序数组
int[] result = new int[left.length + right.length];
for (int index = 0, i = 0, j = 0; index < result.length; index++) {
if (i >= left.length) {
// 如果左数组已经遍历结束,就插入右数组的值
result[index] = right[j];
j++;
} else if (j >= right.length) {
// 如果右数组已经遍历结束,就插入左数组的值
result[index] = left[i];
i++;
} else if (left[i] > right[j]) {
// 左数组与右数组的值同时存在时,就对两数进行比较
// 如果右数组的值比较小,就插入右数组的值。
result[index] = right[j];
j++;
} else {
// 左数组与右数组的值同时存在时,就对两数进行比较
// 如果左数组的值比较小,就插入左数组的值。
result[index] = left[i];
i++;
}
}
return result;
}

总结2:



  • 希尔排序和归并排序可以划为一类排序算法来理解和掌握。

  • 它们都具有相同点——先将整个大的数组分为不同的小组,然后对小组的数据进行排序,最终将所有小组合并为一个有序数组。

  • 而不同点在于分组后的排序方式不同。希尔排序会针对一个小组内的数据执行直接插入排序,而归并排序会直接将两个小组合并为一个有序数组。


排序算法进阶



  • 以上冒泡、快排、插入、选择、希尔、归并这六种排序算法都是基础的排序算法,很多中等、困难难度的算法题一般都是基于上述算法进行解决。(比如《合并两个有序数组》其实就是归并算法的某一部分)

  • 推荐《最小K个数》、《数组中的第K个最大元素》作为进阶学习。(它们都是基于快排实现,类似的变形有最大K个数等)

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

把EditText交给ViewModel管理

Android小萌新今天在做项目的时候遇到一个小问题,来记录一下~ 在做一个登录界面的时候,想使用DataBinding+ViewModel+LiveData 但是怎样让ViewModel拿到EditText控件的实例呢?一开始想到把DataBinding对象...
继续阅读 »

Android小萌新今天在做项目的时候遇到一个小问题,来记录一下~


在做一个登录界面的时候,想使用DataBinding+ViewModel+LiveData


但是怎样让ViewModel拿到EditText控件的实例呢?一开始想到把DataBinding对象从Activity传入ViewModel,后来发现不可行,因为DataBinding在初始化的时候需要传入owner参数,而这个owner参数传的是Activity本身,也就是说DataBinding持有了Activity的引用,这时候如果把DataBinding传给ViewModel不就成了ViewModel持有Activity的引用了吗?内存泄漏!不行!


image.png


解决办法是通过DataBinding双向绑定(View可以操作数据,数据变化时通知View),让EditText的内容直接对应到ViewModel中的LiveData上,这样的话在输入框输入的同时LiveData也在随时变化。


一些收获的经验:


1. @={}和@{}


我发现EditText的text属性要使用@={...}而不是像TextView直接使用@{...}来和Livedata绑定,多出来的这个"="我个人认为是TextView和LiveData绑定仅仅只是get数据,而EditText和数据绑定需要get和实时set数据,所以"="可以理解为赋值


<EditText
...
android:text="@={viewModel.inputAccount}"
... />

<EditText
...
android:text="@={viewModel.inputVerify}"
... />

<Button
...
android:onClick="@{(v)->viewModel.onLogin()}"
... />

2. 为什么在账号EditText输入一个数,getInputAccount()会被调用两次呢?


public class TemporaryLoginViewModel extends ViewModel {

private static final String TAG = "TemporaryLoginViewModel";
MutableLiveData<String> mInputAccount;
MutableLiveData<String> mInputVerify;

public MutableLiveData<String> getInputAccount() {
// TODO:为什么EditText输入一个数,getInputAccount()会调用两次?
Log.d(TAG, "getInputAccount: Entrance");
//双检锁
if (mInputAccount == null)
synchronized (TemporaryLoginViewModel.class) {
if (mInputAccount == null)
mInputAccount = new MutableLiveData<>();
}
//只是TextView展示的话可以返回不可变的LiveData,这里因为是EditText所以只能返回可变的MutableLivedata
return mInputAccount;
}

public MutableLiveData<String> getInputVerify() {
...
}

public void onLogin() {
Log.d(TAG, "onLogin: 账号:" + mInputAccount.getValue() + " 验证码:" + mInputVerify.getValue());
}
}

这就要进入源码去看一眼了,在getInputAccount()上选择findUsages
发现有两处地方调用了它


image.png
第一处在一个回调方法的onChange()中,我们打个断点查看虚拟机栈的栈帧,在第一次执行到断点的时候,虚拟机栈是这样的:


image.png


onChange()内部是这样的:


image.png


也就是说你在输入框里打字使得EditText数据改变的时候,首先回调到onChange()中,在这个onChange()中通过getInputAccount()得到LiveData再给它set一个字符串值


第二处是在executeBindings()中,这个方法是什么时候执行呢?我们让程序继续执行,在下一次执行到断点的时候,虚拟机栈是这样的:


image.png
可以看到在第二次执行到断点的时候,程序从executeBindings()方法中企图调用getInputAccount()


继续向下追踪,就可以看到这样的一个描述


image.png


意思是当View所绑定的数据发生变更的时候,执行此方法


总结


走到这里就很清晰了,整个流程是首先在输入框中输入,当监听到输入后先回调onChange(),在onChange()中通过getInputAccount()得到LiveData,然后修改了LiveData的值;LiveData一但修改,就会重新执行executeBindings(),所以又会调用一次getInputAccount()


到现在就明白了为什么ViewModel中的getInputAccount()会被执行两次啦~


3. getInputAccount()只能返回MutableLiveData


第三个问题也很好理解,为了安全嘛,我一开始试图让getInputAccount()返回一个不可修改的LiveData,然后报错了!


image.png
从第二个问题的分析不难看出,人家内部还要给get到的LiveData执行setValue()呢,所以返回的LiveData一定是可变的MutableLiveData啦~


4. 程序启动时会额外执行一次getInputAccount()


当我查看Activity中的setLifecycleOwner(this)方法时发现它设置了一个LifecycleObserver


image.png
进入这个Observer
image.png
它观察到Activity处于onStart状态的时候会调用executePendingBindings()


进入executePendingBindings()瞅瞅


image.png
又要去调用executeBindingsInternal(),这不就是我们上面在虚拟机栈中看到的调用步骤吗?也就是说在Activity在onStart状态时会执行一次getInputAccount()



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

Android卡顿优化思路

卡顿优化思路 卡顿原理分析 卡顿流程flow 卡顿概貌分析 卡顿实际数据收集 卡顿优化细节 卡顿原因 屏幕刷新频率高于帧率,帧率低于30 每帧执行流程 Choreographer中维护着四个队列callbacks 输入事件队列 动画队列 绘制队列 app...
继续阅读 »

卡顿优化思路



  • 卡顿原理分析

  • 卡顿流程flow

  • 卡顿概貌分析

  • 卡顿实际数据收集

  • 卡顿优化细节


卡顿原因


屏幕刷新频率高于帧率,帧率低于30


每帧执行流程


Choreographer中维护着四个队列callbacks



  • 输入事件队列

  • 动画队列

  • 绘制队列

  • app添加的frameCallback队列


vysnc信号由SurfaceFlinger中创建HWC触发,通过bitTube技术发送到目标进程,目标进程vsync信号到来时,执行Choreographer中的onVsync回调,最终触发doFrame顺序执行这四条队列中的消息。


bitTube


在linux/unix中,bitTube技术成为socketPair,它通过dup技术复制socket的句柄,传递到目标进程,开启socket的全双工通信。


句柄


在内核中,每一个进程都有一个私有的“打开文件表”,这个表是一个指针数组,每一个元素都指向一个内核的打开文件对象。而fd,就是这个表的下标。当用户打开一个文件时,内核会在内部生成一个打开文件对象,并在这个表里找到一个空项,让这一项指向生成的打开文件对象,并返回这一项的下标作为fd.


ui优化



  • 多余Bg移除

  • ui重叠区域优化 cancas.clipRect

  • 减少ui层级

  • 耗时方法分析与优化

  • 多样式布局采用单一rv处理


webview优化


webview的加载流程


image


webiew初始化



  • 目的是初始化并启动浏览器内核。

  • 提前初始化webview并隐藏 优化126ms


webview 单独进程



  • 单独进程 activity配置

  • 单独进程的交互 webview.addJavascriptInterface(),webview.evalute()


安全性



  • addJavaScriptInterface添加的java对象的方法,需要添加@addJavascriptInterface注解,避免xss攻击


卡顿收集策略


开发卡顿检测StrictMode


private void initStrictMode() {
if (isDebug()) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectCustomSlowCalls() //API等级11,使用StrictMode.noteSlowCode
.detectDiskReads()
.detectDiskWrites()
.detectNetwork() // or .detectAll() for all detectable problems
.penaltyDialog() //弹出违规提示对话框
.penaltyLog() //在Logcat 中打印违规异常信息
.penaltyFlashScreen() //API等级11
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects() //API等级11
.penaltyLog()
.penaltyDeath()
.build());
}
}

线下卡顿检测



  • adb shell dumpsys gfxinfo [packagename]


Applications Graphics Acceleration Info:
Uptime: 205237819 Realtime: 436545102

** Graphics info for pid 5842 [xxxx] **

Stats since: 198741999784549ns
Total frames rendered: 653
Janky frames: 157 (24.04%)
50th percentile: 9ms
90th percentile: 34ms
95th percentile: 53ms
99th percentile: 200ms
Number Missed Vsync: 46
Number High input latency: 268
Number Slow UI thread: 76
Number Slow bitmap uploads: 3
Number Slow issue draw commands: 8
Number Frame deadline missed: 92


  • 通过gpu绘制条形柱分析


条形柱共分为8种颜色,绿色和蓝色部分是异步应用能够优化的部分。包括其他处理 - 输入 - 动画 - travel
image


BlockCanary检测卡顿


在ActivityThread.main中的Looper大循环中,Looper.looponce会不断从消息队列中取出消息派发出去,并在前后通过logging打印了两个日志,我们通过设置自定义的logger,在两部分日志的时间差与30ms做对比,如果超过30ms,认为是卡顿。


logging.println(">>>>> Dispatching to " + msg.target + " "
+ msg.callback + ": " + msg.what);
msg.target.dispatchMessage(msg);
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);

卡顿分析信息收集



  • Debug.startMethodTracing 收集具体的卡顿方法

  • 查看trace文件 根据bottomup分析具体的耗时方法

  • 火焰图,横轴是调用方法耗时,纵轴是调用深度

  • 调用图,调用链以及方法耗时


线上卡顿分析与收集


在ActivityThread.main中的Looper大循环中,Looper.looponce会不断从消息队列中取出消息派发出去,并在前后通过logging打印了两个日志,我们通过设置自定义的logger,在两部分日志的时间差与30ms做对比,如果超过30ms,认为是卡顿。将主线程堆栈信息写入到缓存文件并异步发送到日志后台。


常见的卡顿问题


sharepreference



  • 首次读取写入会loadxml到内存

  • sp文件修改是全量读写的

  • commit异步写入,通过CountdownLatch阻塞等待结果

  • apply延迟100ms写入,无返回结果

  • 主线程ANR,sp的修改会先体现在内存中,然后往QueueWorker中加入磁盘异步写数据的任务,但是会在Activity.onResume以及Service.onstartCommand等方法中增加waitToFinish等待磁盘写入完成的代码。

  • 解决方案使用MMKV

  • 尽量拆分小的xml


主线程操作文件


主线程网络操作


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

Silhouette——更方便的Shape/Selector实现方案

写在前面 首先祝大家新年快乐,开工大吉。 最新刚换了工作,大部分精力还是放到新工作上面,所以这次还是先给大家带来一个小而实用的库:Silhouette。另外,考虑到Kotlin越来越普及,作者在开发过程中也切实感受到Kotlin相较于Java带来的便利,后续的...
继续阅读 »

写在前面


首先祝大家新年快乐,开工大吉。

最新刚换了工作,大部分精力还是放到新工作上面,所以这次还是先给大家带来一个小而实用的库:Silhouette。另外,考虑到Kotlin越来越普及,作者在开发过程中也切实感受到Kotlin相较于Java带来的便利,后续的IM系列文章及项目考虑用Kotlin重写,而且考虑到由于工作业务需求过多可能出现断更的情况,所以打算一次性写完再放出来,避免大家学习不方便。

废话不多说,直接开始吧。


Silhouette是什么?


Silhouette意为“剪影”,取名并没有特别的含义,只是单纯地觉得意境较美。例如上一篇文章Shine——更简单的Android网络请求库封装的网络请求库:Shine即意为“闪耀”,也没有特别的含义,只是作者认为开源库起名较难,特意找一些比较优美的单词。

Silhouette是一系列基于GradientDrawableStateListDrawable封装的组件集合,主要用于实现在Android Layout XML中直接支持Shape/Selector等功能。

我们都知道在Android开发中,不同的TextViewButton各种样式(形状、背景色、描边、圆角、渐变等)的传统实现方式是在drawable文件夹中编写各种shape/selector等文件,这种方式至少会存在以下几种弊端:



  1. shape/selector文件过多,项目体积增大;

  2. shape/selector文件命名困难,命名规范时往往会存在功能重复的文件;

  3. 功能存在局限性:例如gradient渐变色。传统shape方式只支持三种颜色过渡(startColor/centerColor/endColor),如果设计稿存在四种以上颜色渐变,shape gradient无能为力。再比如TextView在常态和按下态需要同时改变背景色及文字颜色时,传统方式只能在代码中动态设置等。

  4. 开发效率低;

  5. 难以维护等;


综上所述,我们迫切需要一个库来解决以上问题,Silhouette正具备这些能力。接下来,我们来具体看看Silhouette能做什么吧。


Silhouette能做什么?


上面说到Silhouette是一系列组件集合,具体包含以下组件:




  • SleTextButton

    基于AppCompatTextView封装;

    具备定义各种样式(形状、背景色、描边、圆角、渐变等)的能力 ;

    具备不同状态(常态、按下态、不可点击态)下文字颜色指定等。




  • SleImageButton

    基于ShapeableImageView封装;

    通过指定sle_ib_type属性使ImageView支持按下态遮罩层、透明度改变、自定义图片,同时支持CheckBox功能;

    通过指定sle_ib_style属性使ImageView支持Normal、圆角、圆形等形状。




  • SleConstraintLayout

    基于ConstraintLayout封装;

    具备定义各种样式(形状、背景色、描边、圆角、渐变等)的功能。




  • SleRelativeLayout

    基于RelativeLayout封装;

    具备定义各种样式(形状、背景色、描边、圆角、渐变等)的功能。




  • SleLinearLayout

    基于LinearLayout封装;

    具备定义各种样式(形状、背景色、描边、圆角、渐变等)的功能。




  • SleFrameLayout

    基于FrameLayout封装;

    具备定义各种样式(形状、背景色、描边、圆角、渐变等)的功能。




设计、封装思路及原理




  • 项目结构

    com.freddy.silhouette



    • config(配置相关,存放全局注解及公共常量、默认值等)

    • extkotlin扩展相关,可选择用或不用)

    • utils(工具类相关,可选择用或不用)

    • widget(控件相关)

      • button

      • layout




    由此可见,项目结构非常简单,所以Silhouette也是一个比较轻量级的库。




  • 封装思路及原理

    由于该库非常简单,实际上就是根据Shape/Selector进行自定义属性,从而利用GradientDrawableStateListDrawable提供的API进行封装,不存在什么难度,在此就不展开讲了。


    下面贴一下代码片段,基本上几个组件的实现原理都大同小异,都是利用GradientDrawableStateListDrawable实现组件的ShapeSelector功能:




private fun init() {
val normalDrawable =
getDrawable(normalBackgroundColor, normalStrokeColor, normalGradientColors)
var pressedDrawable: GradientDrawable? = null
var disabledDrawable: GradientDrawable? = null
var selectedDrawable: GradientDrawable? = null
when (type) {
TYPE_MASK -> {
pressedDrawable = getDrawable(
normalBackgroundColor,
normalStrokeColor,
normalGradientColors
).apply {
colorFilter =
PorterDuffColorFilter(maskBackgroundColor, PorterDuff.Mode.SRC_ATOP)
}
disabledDrawable =
getDrawable(disabledBackgroundColor, disabledBackgroundColor)
}
TYPE_SELECTOR -> {
pressedDrawable =
getDrawable(pressedBackgroundColor, pressedStrokeColor, pressedGradientColors)
disabledDrawable = getDrawable(
disabledBackgroundColor,
disabledStrokeColor,
disabledGradientColors
)
}
}
selectedDrawable = getDrawable(
selectedBackgroundColor,
selectedStrokeColor,
selectedGradientColors
)
setTextColor(normalTextColor)
background = StateListDrawable().apply {
if (type != TYPE_NONE) {
addState(intArrayOf(android.R.attr.state_pressed), pressedDrawable)
}
addState(intArrayOf(-android.R.attr.state_enabled), disabledDrawable)
addState(intArrayOf(android.R.attr.state_selected), selectedDrawable)
addState(intArrayOf(), normalDrawable)
}

setOnTouchListener(this)
}

private fun getDrawable(
backgroundColor: Int,
strokeColor: Int,
gradientColors: IntArray? = null
): GradientDrawable {
// 背景色相关
val drawable = GradientDrawable()
setupColor(drawable, backgroundColor)

// 形状相关
(drawable.mutate() as GradientDrawable).shape = shape
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
drawable.innerRadius = innerRadius
if (innerRadiusRatio > 0f) {
drawable.innerRadiusRatio = innerRadiusRatio
}
drawable.thickness = thickness
if (thicknessRatio > 0f) {
drawable.thicknessRatio = thicknessRatio
}
}

// 描边相关
if (strokeColor != 0) {
(drawable.mutate() as GradientDrawable).setStroke(
strokeWidth,
strokeColor,
dashWidth,
dashGap
)
}

// 圆角相关
setupCornersRadius(
drawable,
cornersRadius,
cornersTopLeftRadius,
cornersTopRightRadius,
cornersBottomRightRadius,
cornersBottomLeftRadius
)

// 渐变相关
(drawable.mutate() as GradientDrawable).gradientType = gradientType
if (gradientCenterX != 0.0f || gradientCenterY != 0.0f) {
(drawable.mutate() as GradientDrawable).setGradientCenter(
gradientCenterX,
gradientCenterY
)
}
gradientColors?.let { colors ->
(drawable.mutate() as GradientDrawable).colors = colors
}
var orientation: GradientDrawable.Orientation? = null
when (gradientOrientation) {
GRADIENT_ORIENTATION_TOP_BOTTOM -> {
orientation = GradientDrawable.Orientation.TOP_BOTTOM
}
GRADIENT_ORIENTATION_TR_BL -> {
orientation = GradientDrawable.Orientation.TR_BL
}
GRADIENT_ORIENTATION_RIGHT_LEFT -> {
orientation = GradientDrawable.Orientation.RIGHT_LEFT
}
GRADIENT_ORIENTATION_BR_TL -> {
orientation = GradientDrawable.Orientation.BR_TL
}
GRADIENT_ORIENTATION_BOTTOM_TOP -> {
orientation = GradientDrawable.Orientation.BOTTOM_TOP
}
GRADIENT_ORIENTATION_BL_TR -> {
orientation = GradientDrawable.Orientation.BL_TR
}
GRADIENT_ORIENTATION_LEFT_RIGHT -> {
orientation = GradientDrawable.Orientation.LEFT_RIGHT
}
GRADIENT_ORIENTATION_TL_BR -> {
drawable.orientation = GradientDrawable.Orientation.TL_BR
}
}
orientation?.apply {
(drawable.mutate() as GradientDrawable).orientation = this
}
return drawable
}

感兴趣的同学可以到官方文档了解GradientDrawableStateListDrawable的原理。


自定义属性列表


自定义属性分为通用属性特有属性




  • 通用属性



    • 类型



















    属性名称类型说明备注
    sle_typeenum类型
    mask:遮罩
    selector:自定义样式
    none:无
    默认值:mask
    默认的mask为90%透明度黑色,可通过sle_maskBackgroundColors属性设置
    若不指定为selector,则自定义样式无效


    • 形状相关











































    属性名称类型说明备注
    sle_shapeenum形状
    rectangle:矩形
    oval:椭圆形
    line:线性形状
    ring:环形
    默认值:rectangle
    sle_innerRadiusdimension|reference尺寸,内环的半径shape="ring"可用
    sle_innerRadiusRatiofloat以环的宽度比率来表示内环的半径shape="ring"可用
    sle_thicknessdimension|reference尺寸,环的厚度shape="ring"可用
    sle_thicknessRatiofloat以环的宽度比率来表示环的厚度shape="ring"可用


    • 背景色相关





































    属性名称类型说明备注
    sle_normalBackgroundColorcolor|reference常态背景颜色/
    sle_pressedBackgroundColorcolor|reference按下态背景颜色/
    sle_disabledBackgroundColorcolor|reference不可点击态背景颜色默认值:#CCCCCC
    sle_selectedBackgroundColorcolor|reference选中态背景颜色/


    • 描边相关























































    属性名称类型说明备注
    sle_normalStrokeColorcolor|reference常态描边颜色/
    sle_pressedStrokeColorcolor|reference按下态描边颜色/
    sle_disabledStrokeColorcolor|reference不可点击态描边颜色/
    sle_selectedStrokeColorcolor|reference选中态描边颜色/
    sle_strokeWidthdimension|reference描边宽度/
    sle_dashWidthdimension|reference虚线宽度/
    sle_dashGapdimension|reference虚线间隔/


    • 圆角相关











































    属性名称类型说明备注
    sle_cornersRadiusdimension|reference总圆角半径/
    sle_cornersTopLeftRadiusdimension|reference左上角圆角半径/
    sle_cornersTopRightRadiusdimension|reference右上角圆角半径/
    sle_cornersBottomLeftRadiusdimension|reference左下角圆角半径/
    sle_cornersBottomRightRadiusdimension|reference右下角圆角半径/


    • 渐变相关



































































    属性名称类型说明备注
    sle_normalGradientColorsreference常态渐变背景色支持在res/array下定义数组实现多个颜色渐变
    sle_pressedGradientColorsreference按下态渐变背景色支持在res/array下定义数组实现多个颜色渐变
    sle_disabledGradientColorsreference不可点击态渐变背景色支持在res/array下定义数组实现多个颜色渐变
    sle_selectedGradientColorsreference选中态渐变背景色支持在res/array下定义数组实现多个颜色渐变
    sle_gradientOrientationenum渐变方向
    TOP_BOTTOM:从上到下
    TR_BL:从右上到左下
    RIGHT_LEFT:从右到左
    BR_TL:从右下到左上
    BOTTOM_TOP:从下到上
    BL_TR:从左下到右上
    LEFT_RIGHT:从左到右
    TL_BR:从左上到右下
    /
    sle_gradientTypeenum渐变类型
    linear:线性渐变
    radial:圆形渐变,起始颜色从gradientCenterX、gradientCenterY点开始
    sweep:A sweeping line gradient
    /
    sle_gradientCenterXfloat渐变中心放射点x坐标注意,这里的坐标是整个背景的百分比的点,并不是确切点,0.2就是20%的点
    sle_gradientCenterYfloat渐变中心放射点y坐标注意,这里的坐标是整个背景的百分比的点,并不是确切点,0.2就是20%的点
    sle_gradientRadiusdimension|reference渐变半径需要配合gradientType=radial使用,如果设置gradientType=radial而没有设置gradientRadius,将会报错


    • 其它

























    属性名称类型说明备注
    sle_maskBackgroundColorcolor|reference当sle_type=mask时,按钮按下状态的遮罩颜色默认值:90%透明度黑色(#1A000000)
    sle_cancelOffsetdimension|reference用于解决手指移出控件区域判断为cancel的偏移量默认值:8dp



  • 特有属性



    • SleConstraintLayout/SleRelativeLayout/SleFrameLayout/SleLinearLayout



















    属性名称类型说明备注
    sle_interceptTypeenum事件拦截类型
    intercept_super:return super
    intercept_true:return true
    intercept_false:return false
    Layout组件设置此值,可实现是否拦截事件,如果设置为intercept_true,事件将不传递到子控件,在某些场景比较实用


    • SleTextButton





































    属性名称类型说明备注
    sle_normalTextColorcolor|reference常态文字颜色/
    sle_pressedTextColorcolor|reference按下态文字颜色/
    sle_disabledTextColorcolor|reference不可点击态文字颜色/
    sle_selectedTextColorcolor|reference选中态文字颜色/


    • SleImageButton









































































    属性名称类型说明备注
    sle_ib_typeenum类型
    mask:图片遮罩
    alpha:图片透明度改变
    selector:自定义图片
    checkBox:CheckBox场景
    none:无
    1.指定为mask时,自定义图片资源无效;
    2.指定为alpha时,sle_pressedAlpha/sle_disabledAlpha生效;
    3.指定为selector时,sle_normalResId/sle_pressedResId/sle_disabledResId生效;
    4.指定为checkBox时,sle_checkedResId/sle_uncheckedResId/sle_isChecked生效;
    5.指定为none时,图片资源均不生效,圆角相关配置有效
    sle_ib_styleenumImageView形状
    normal:普通形状
    rounded:圆角
    oval:圆形
    默认值:normal
    sle_normalResIdcolor|reference常态图片资源/
    sle_pressedResIdcolor|reference按下态图片资源/
    sle_disabledResIdcolor|reference不可点击态图片资源/
    sle_checkedResIdcolor|reference选中态checkBox图片资源/
    sle_uncheckedResIdcolor|reference非选中态checkBox图片资源/
    sle_isCheckedbooleanCheckBox是否选中默认值:false
    sle_pressedAlphafloat按下态图片透明度默认值:70%
    sle_disabledAlphafloat不可点击态图片透明度默认值:30%



使用方式



  1. 添加依赖


implementation "io.github.freddychen:silhouette:$lastest_version"

Note:最新版本可在maven central silhouette中找到。



  1. 使用


由于自定义属性太多,在此就不一一列举了。下面给出几种常见的场景示例,大家可以根据自定义属性表自行编写:



  • 常态


Silhouette Normal



  • 按下态


Silhouette Pressed


以上布局代码为:


<?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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:gravity="center_horizontal"
android:orientation="vertical">

<com.freddy.silhouette.widget.button.SleTextButton
android:id="@+id/stb_1"
android:layout_width="match_parent"
android:layout_height="54dp"
android:layout_marginHorizontal="48dp"
android:layout_marginTop="14dp"
android:gravity="center"
android:text="SleTextButton1"
android:textSize="20sp"
app:sle_cornersRadius="28dp"
app:sle_normalBackgroundColor="#f88789"
app:sle_normalTextColor="@color/white"
app:sle_type="mask" />

<com.freddy.silhouette.widget.button.SleTextButton
android:id="@+id/stb_2"
android:layout_width="match_parent"
android:layout_height="54dp"
android:layout_marginHorizontal="48dp"
android:layout_marginTop="14dp"
android:gravity="center"
android:text="SleTextButton2"
android:textSize="20sp"
app:sle_cornersBottomRightRadius="24dp"
app:sle_cornersTopLeftRadius="14dp"
app:sle_normalBackgroundColor="#338899"
app:sle_normalTextColor="@color/white"
app:sle_pressedBackgroundColor="#aeeacd"
app:sle_type="selector" />

<com.freddy.silhouette.widget.button.SleTextButton
android:id="@+id/stb_3"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginHorizontal="48dp"
android:layout_marginTop="14dp"
android:enabled="false"
android:gravity="center"
android:text="SleTextButton2"
android:textSize="14sp"
app:sle_cornersBottomRightRadius="24dp"
app:sle_cornersTopLeftRadius="14dp"
app:sle_normalBackgroundColor="#cc688e"
app:sle_normalTextColor="@color/white"
app:sle_pressedBackgroundColor="#34eeac"
app:sle_shape="oval"
app:sle_type="selector" />

<com.freddy.silhouette.widget.button.SleImageButton
android:id="@+id/sib_1"
android:layout_width="84dp"
android:layout_height="84dp"
android:layout_marginTop="14dp"
app:sle_ib_type="mask"
app:sle_normalResId="@drawable/ic_launcher_background" />

<com.freddy.silhouette.widget.button.SleImageButton
android:id="@+id/sib_2"
android:layout_width="128dp"
android:layout_height="128dp"
android:layout_marginTop="14dp"
app:sle_ib_type="alpha"
app:sle_normalResId="@drawable/ic_launcher_background" />

<com.freddy.silhouette.widget.button.SleImageButton
android:id="@+id/sib_3"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_marginTop="14dp"
app:sle_ib_type="selector"
app:sle_normalResId="@mipmap/ic_launcher"
app:sle_pressedResId="@drawable/ic_launcher_foreground" />

<com.freddy.silhouette.widget.layout.SleConstraintLayout
android:id="@+id/scl_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="48dp"
android:layout_marginTop="14dp"
android:paddingHorizontal="14dp"
android:paddingVertical="8dp"
app:sle_cornersRadius="10dp"
app:sle_interceptType="intercept_super"
app:sle_normalBackgroundColor="@color/white">

<ImageView
android:layout_width="72dp"
android:layout_height="48dp"
android:scaleType="centerCrop"
android:src="@mipmap/ic_launcher_round" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="UserName"
android:textColor="@color/black"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.freddy.silhouette.widget.layout.SleConstraintLayout>

<com.freddy.silhouette.widget.layout.SleLinearLayout
android:id="@+id/sll_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="48dp"
android:layout_marginTop="14dp"
android:gravity="center_vertical"
android:paddingHorizontal="14dp"
app:sle_type="selector"
android:paddingVertical="8dp"
app:sle_cornersTopRightRadius="24dp"
app:sle_cornersBottomRightRadius="18dp"
app:sle_interceptType="intercept_true"
app:sle_pressedBackgroundColor="#fe9e87"
app:sle_normalBackgroundColor="#aee949">

<ImageView
android:layout_width="72dp"
android:layout_height="48dp"
android:scaleType="centerCrop"
android:src="@mipmap/ic_launcher_round" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:text="UserName"
android:textColor="@color/black"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.freddy.silhouette.widget.layout.SleLinearLayout>
</LinearLayout>

Note:需要给组件设置setOnClickListener才能看到效果。

至于更多的功能,就让大家去试试吧,篇幅有限,就不一一列举了。有任何疑问,欢迎通过QQ群微信公众号联系我。


版本记录






















版本号修改时间版本说明
0.0.12022.02.10首次提交
0.0.22022.02.12修改minSdk为19

写在最后


终于写完了,Shape/Selector在每个项目中基本都会用到,而且频率还不算低。Silhouette原理虽然简单,但确实能解决很多问题,这些都是平时开发中的积累,希望对大家能有所帮助。欢迎大家starfork,让我们为Android开发共同贡献一份力量。另外如果有疑问欢迎加入我的QQ群:1015178804,同时也欢迎大家关注我的公众号:FreddyChen,让我们共同进步和成长。


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

Android UI适配方案

大纲 使用dp而不是px 尽量使用自动适配布局,而不要指定分辨率 使用宽高限定符 values-1080x1920,以1080P为基准计算每种常见分辨率对应的尺寸。 需要尽可能全的添加各种设备的分辨率(有工具) 容错性不足,如果设备分辨率不能精确匹配对应限...
继续阅读 »

大纲



  1. 使用dp而不是px

  2. 尽量使用自动适配布局,而不要指定分辨率

  3. 使用宽高限定符

    1. values-1080x1920,以1080P为基准计算每种常见分辨率对应的尺寸。

    2. 需要尽可能全的添加各种设备的分辨率(有工具)

    3. 容错性不足,如果设备分辨率不能精确匹配对应限定符,会默认使用统一默认的dimens



  4. 第三方自动适配UI框架

    1. 原理:自定义RelativeLayout,在onMeasure中对控件分辨率做变换

    2. 第三方框架,维护性很成问题

    3. 一些自定义View,处理比较麻烦



  5. 最小宽度限定符,类似宽高限定符

    1. values-sw240dp,同样以某一dp宽度为基准计算其他宽度dp的值

    2. values-sw360dp、values-sw480dp

    3. 相比宽高限定符,最小宽度限定符不进行精确匹配,会遵循就近原则,可以较好的解决容错问题。

    4. 如:设备宽364dp,系统会自动就近配置values-sw360dp下的dimens,显示效果相差不会很大



  6. 今日头条——修改density值

    1. 原理:px = dp x (dpi/160) = dp x density

    2. 既然如此,将density

    3. 需要UI出设计图时以统一的dp为基准

    4. mp.weixin.qq.com/s/d9QCoBP6k…




基本概念



  • 像素——px

  • 密度独立像素——dp或dip

  • 像素密度——dpi,单位面积内的像素数。

    • 软件系统的概念。

    • 在系统出厂时,配置文件中的固定值。

    • 通常的取值有:160、240、360、480等。

    • 不同于物理概念上的屏幕密度ppi,如ppi为415、430和470时,dpi可能会统一设置为480。



  • density——当dpi=160时,1px = 1pd,此时denstiy的值为1,dpi=240时,1.5px = 1dp,density的值为1.5。

  • 上述值的关系:

    • denstiy = dpi / 160;

    • px = dp x density = dp x (dpi / 160)




Android设备的碎片化极为严重,各种尺寸和分辨率的设备无比繁多。使得在Android开发中,UI适配变成了开发过程中极为重要的一步。为此Google提出了密度独立像素dip或dp的概率,旨在更友好的处理Android UI适配问题。


但是效果嘛,只能说差强人意,可以解决大部分的业务场景,但是剩下的个别情况就搞死人了,原因在于Android设备碎片化实在太严重了,存在各种分辨率和dpi的设备。


比如两台设备A和B,分辨率是1920x1080,dpi分别为420和480,在布局中编写一个100dp宽的ImageView,按照上面的公式ImageView的显示宽度分别为:100dp x 420 / 160 = 262.5100dp x 480 / 160 = 300,ImageView在B设备上明显显示要大一些。差异可能还不明显,我们把宽度改为360dp呢,A设备显示宽度为:948px,B设备显示宽度为:1080px。这就扯淡了,一个宽度填充满屏幕,一个不满。这种情况肯定是需要开发来背锅解决的。


适配方案


虽然上面提到了使用dp无法解决全部业务场景,但是相对于直接使用px已经可以解决大部分场景下的适配问题了。


所以UI适配的第一条就是:


1. 使用dp代替px来编写布局。


又因为上面无法适配的个别场景,所以UI适配的第二条是:


2.尽量使用自动适配布局,而不要指定分辨率


这一条也很好理解,尽量使用ConstraintLayout 约束布局和LinearLayout等父布局,不要写死分辨率,比如上面的例子如果使用match_parent而不是360dp,也可以避免出现显示不一致问题(但是仅限于上列)。


限定符


Google同样意识到dp满足所以业务场景的需要,所以提供了宽度限定符的概念。



虽然您的布局应始终通过拉伸其视图内部和周围的空间来应对不同的屏幕尺寸,但这可能无法针对每种屏幕尺寸提供最佳用户体验。例如,您为手机设计的界面或许无法在平板电脑上提供良好的体验。因此,您的应用还应提供备用布局资源,以针对特定屏幕尺寸优化界面设计。



最小宽度限定符



使用“最小宽度”屏幕尺寸限定符,您可以为具有最小宽度(以密度无关像素 dp 或 dip 为度量单位)的屏幕提供备用布局。


通过将屏幕尺寸描述为密度无关像素的度量值,Android 允许您创建专为非常具体的屏幕尺寸而设计的布局,同时让您不必对不同的像素密度有任何担心。



通俗一点翻译就是:可用通过xxxx-swXXXdp的方式定义一些最小限定符的资源文件,比如:values-sw400dp、values-sw600dp,系统会自动匹配如屏幕宽度相近资源文件夹。


我们再来看上面的例子两台设备A和B,分辨率是1920x1080,dpi分别为360和400。我们简化下问题比如设计图给的是1920x1080 360dpi,包含一个22.5px * 22.5px = 10dp * 10dp的图片。按经验布局应该如下编写:


<ImageView
android:id="@+id/img_iv"
android:layout_width="10dp"
android:layout_height="10dp"
android:background="@mipmap/ic_launcher"/>

在不同设备上运行的结果:



  • 1280 x 720 240dpi的设备,图片显示为15px * 15px;

  • 1920 x1080 360dpi的A设备,图片显示为22.5px * 22.5px;

  • 1920 x1080 400dpi的B设备,图片显示为25px * 25px;


可以看到B设备图片显示是有问题的,为了解决这个问题,我们使用最小宽度限定符定义两个资源文件夹:values-sw360dp和values-sw400dp。


在values-sw360dp中添加dimen.xml内容如下:


<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="dp_1">1dp</dimen>
<dimen name="dp_2">2dp</dimen>
<dimen name="dp_3">3dp</dimen>
<dimen name="dp_4">4dp</dimen>
<dimen name="dp_5">5dp</dimen>
<dimen name="dp_6">6dp</dimen>
<dimen name="dp_7">7dp</dimen>
<!-- 省略其他值 -->
<dimen name="dp_360">360dp</dimen>
<!-- 因为设计图是360dpi,所以控件尺寸通常不会超过360dp,定义最大360dp的值足够使用 -->
</resources>

在values-sw420dp中添加dimen.xml,文件中的dimen值很容易换算出来:在360dpi中dp_1 = 1dp,那么在400dpi中dp_1 = 360 / 400 = 0.9dp,文件内容如下:


<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="dp_1">0.9dp</dimen>
<dimen name="dp_2">1.8dp</dimen>
<dimen name="dp_3">2.7dp</dimen>
<dimen name="dp_4">3.6dp</dimen>
<dimen name="dp_5">4.5dp</dimen>
<dimen name="dp_6">5.4dp</dimen>
<dimen name="dp_7">6.3dp</dimen>
<!-- 省略其他值 -->
</resources>

注意要在values文件夹下添加默认dimen.xml,文件内容与values-sw360dp中添加dimen.xml一致(因为设计图恰好是360dpi的)。


布局中的ImageView自然要改写为:


<ImageView
android:id="@+id/img_iv"
android:layout_width="@dimen/dp_10"
android:layout_height="@dimen/dp_10"
android:background="@mipmap/ic_launcher"/>

我们再来看一下不同设备运行结果:



  • 1280 x 720 240dpi的设备,未匹配到限定符使用values中的dimen,dp_10 = 10dp, px = 10 * 240 / 160 = 15px,图片显示尺寸为15px * 15px。

  • 1920 x1080 360dpi的A设备,匹配到sw360dp限定符,dp_10 = 10dp, px = 10 * 360 / 160 = 20px,图片显示尺寸为22.5px * 22.5px。

  • 1920 x1080 400dpi的B设备,匹配到sw420dp限定符,dp_10 = 9dp, px = 9 * 400 / 160 = 20px,图片显示尺寸为22.5px * 22.5px。


完美的解决了设备A和B的显示问题,所以UI适配的第三条是:


3. 使用最小(可用)宽度限定符,解决同样分辨率不同dpi的设备适配问题。


这种方案看似完美,但是也有一些隐含的问题:此方案只能解决同样分辨率不同dpi设备的适配问题:



  • 一旦出现不同分辨率相同dpi的情况就无效了(当然这种情况的可能性不高)。

  • 以上举例只是基于1920x1080这一种分辨率为例说明,试想一下如果1280x720的设备存在240dpi和280dpi的情况呢?我们只能针对特殊情况适配处理,无法解决全部场景适配问题。


宽高限定符


类似于上面说的最小宽度限定符,但是需要精确指定要匹配的设备宽高,values-1920x1080、values-1280x720等。配置与使用方式也与上面类似,如设计图尺寸为1920x1080 360dpi,那么只需要以1920x1080为基准计算所有分辨率对应的尺寸就可以了,布局编写时按照给的尺寸一一对应就可以,比如:给出的ImageView是20px*20px的,那在布局中同样指定width和height为@dimen/dp_20就可以了。


values-1920x1080中dimens.xml如下:


<resources>
<dimen name="dp_1">1px</dimen>
<dimen name="dp_2">2px</dimen>
<dimen name="dp_3">3px</dimen>
<dimen name="dp_4">4px</dimen>
<dimen name="dp_5">5px</dimen>
<dimen name="dp_6">6px</dimen>
<dimen name="dp_7">7px</dimen>
<dimen name="dp_8">8px</dimen>
<dimen name="dp_9">9px</dimen>
<!-- 省略其他 -->
<dimen name="dp_1920">1920px</dimen>
</resources>

values-1280x720中dimens.xml换算为:


<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="dp_1">0.66px</dimen>
<dimen name="dp_2">1.33px</dimen>
<dimen name="dp_3">2.0px</dimen>
<dimen name="dp_4">2.66px</dimen>
<dimen name="dp_5">3.33px</dimen>
<dimen name="dp_6">4.0px</dimen>
<dimen name="dp_7">4.66px</dimen>
<!-- 省略其他 -->
</resources>

同样需要在values添加默认尺寸dimen.xml,内容同基准分辨率文件。


因为不是所有设备屏幕都是16:9的,也可以按照宽高拆分成两个dimens.xml文件dimen_x.xml和dimen_y.xml,按照宽高:1920x1080分别换算得到x和y的值,但是页面设计通常是竖屏可滑动的,所以对高度不敏感,只需要根据一个维度计算统一值就可以了。


以上计算方式比较简单了,不需要自己编写换算可以通过代码工具或者自己写个类实现。(网上有好多,找一下应该可以找到)。


理论上只要尽可能多的枚举所有设备分辨率,就可以完美的解决屏幕适配问题,所以UI适配的第四条是:


4.使用宽高限定符,精确匹配屏幕分辨率。


这种方案已经近乎完美了,一度成为比较热门的解决方案,也有很多团队使用过此方案。但是之前也说过Android设备的碎片化太严重了,综合考虑基本不可能在项目中枚举所有的屏幕尺寸进行适配,如果设备没有匹配到对应尺寸会使用values下的默认尺寸文件,可能会出现严重的UI适配问题。


但是不可否认此种方案实现简单,对于编写布局也很友好(直接填入设计图的尺寸值就行,不需要换算),可以解决绝大多数的设备适配问题,是一种很友好的解决方案。


第三方UI适配框架


有很多第三方库的解决方案,是从ViewGroup入手的,要么重写常用的如:RelativeLayout、LinearLayout和FrameLayout等在控件内部做转换来适配不同尺寸的设备,要么提供新的Layout如:Google的PercentLayout布局。但是这些方案基本都不在维护了,这里就不详细展开了,感兴趣的可以自行搜索了解。


UI适配的第五条是:


5. 使用第三方自适配框架,解决UI适配问题。


感兴趣的可以参考以下文档:



其他适配方案


参考字节的实现方案:


一种极低成本的Android屏幕适配方式


这篇文章着实属于拾人牙慧了,起因是因为看到了这篇博客Android 目前最稳定和高效的UI适配方案。所以想着确实应该把这部分知识梳理一下,所以写了这篇文档加了一些自己的里面,主要也是为了梳理知识点加深理解。


文中列举的几种UI适配方案,没有严格的优劣之分,可以根据自己的业务需求选择,也可以选择几种搭配使用,比如笔者目前主要做智能电视(盒子)的应用开发,Android电视不同于手机,碎片化没有那么严重,电视分辨率种类屈指可数,所以在日常项目中基本选择使用宽高限定符的方案进行适配,效果也是极好的。


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

Cookbook for Programmers

学了 Python Cookbook、 Bash Cookbook、 MySQL Cookbook、 Redis Cookbook、 Regular Expressions Cookbook、 React Native Cookbook、 HTML5 Cook...
继续阅读 »
学了
Python Cookbook、
Bash Cookbook、
MySQL Cookbook、
Redis Cookbook、
Regular Expressions Cookbook、
React Native Cookbook、
HTML5 Cookbook、
LATEX Cookbook、
RESTful Web Services Cookbook
……
通过了各个大厂的面试,为什么就是达不到心仪妹子的要求?
你需要学一学这本Cookbook!

不是标题党!这是一份正经的菜谱,符合程序员思维的菜谱!

学起来,去征服她的胃吧!


程序员做饭指南

GitHub Workflow Status (branch) GitHub GitHub contributors

最近在家隔离,出不了门。只能宅在家做饭了。作为程序员,我偶尔在网上找找菜谱和做法。但是这些菜谱往往写法千奇百怪,经常中间莫名出来一些材料。对于习惯了形式语言的程序员来说极其不友好。

所以,我计划自己搜寻菜谱和并结合实际做菜的经验,准备用更清晰精准的描述来整理常见菜的做法,以方便程序员在家做饭。

同样,我希望它是一个由社区驱动和维护的开源项目,使更多人能够一起做一个有趣的仓库。所以非常欢迎大家贡献它~

如何贡献

针对发现的问题,直接修改并提交 Pull request 即可。

在写新菜谱时,请复制并修改已有的菜谱模板: 示例菜。 在提交 Pull Request 前更新一下 README.md 里的引用。

做菜之前

菜谱

家常菜

早餐

主食

半成品加工

红烧菜系

汤与粥

饮料

酱料和其它材料

甜品

进阶知识学习

如果你已经做了许多上面的菜,对于厨艺已经入门,并且想学习更加高深的烹饪技巧,请继续阅读下面的内容:

作者:Anduin2017

来源:https://github.com/Anduin2017/HowToCook

收起阅读 »

程序员有哪些含金量高的证书可以考?

近来IT行业成为了发展前景好高薪资的大热门,社会上也出现了“计算机考试热”。总体看来,越来越多的人选择参加各种各样的计算机考试,就是为了拿含金量高的证书,提升自己的职场竞争力。那么程序员有哪些含金量高的证书可以考?下面黑马程序员小编将详细介绍一下含金量高的IT...
继续阅读 »

近来IT行业成为了发展前景好高薪资的大热门,社会上也出现了“计算机考试热”。总体看来,越来越多的人选择参加各种各样的计算机考试,就是为了拿含金量高的证书,提升自己的职场竞争力。

那么程序员有哪些含金量高的证书可以考?下面黑马程序员小编将详细介绍一下含金量高的IT证书,避免大家在不需要的考试上浪费时间。

1、MCSE,MCDBA,MCAD/MCSD微软认证
包括系统管理方向,数据库方向和开发方向的证书。分别叫做微软的技术还是比较有用的,比如MCSE,维护、管理局域网非常有用。但到实际的网络公司工作,还应该学习CCNA、CCNP方面的技术,由局域网管理扩展到广域网管理,所学的路由、交换、远程接入、网络故障排除等技术更为实用。目前在用人单位的招聘信息里,CCNP是这类公司招聘的首要挑选因素。

2、IBM认证
国内常见的有考电子商务方向,数据库方向,大型机方向,开发方向等等。由于名目太多,这里不列出了,有兴趣可以到IBM的网站或Prometric或VUE网站(这两家是国家两大认证考试中心)上查看(其他国际公司的认证介绍也都可以在这两家考试中心的网站上查看)。

3、Sun认证
Sun认证主要包括两大方向,一个是Sun Solaris系统的管理方向,另一个是非常流行的Java认证方向。其中Java方向包括:SCJP,SCJD,SCWCD,SCMAD,SCWSD,SCEA等,最高级别是SCEA,名称为Sun认证企业应用架构师。
pic_457d968b.png

4、Cisco证书
传说中有"钱"途的证书。注重网络知识的普遍性及与实际操作的紧密结合,教材由浅入深,教授方式灵活生动。CCNA认证 课程内容:包括网络操作、不同联网产品间的区别、如何设计网络并排除故障及其它一般性知识等; 课程目的:就设计、建立和维护能够支持全国及全球性机构的网络原则和实践对学生进行培训,CCNA能够根据培训和显示世界经验的结合提供的解决方案例子。

5、Adobe认证
Adobe中国教育认证计划的核心内容之一。遵循"国际品质、中国制定"的一贯开发理念和原则,在品质控制和规范管理下,Adobe认证逐渐获得社会的认可并深入人心,已经成为中国数字艺术教育市场主流的行业认证标准。

6、 Linux认证
指获得专业Linux培训后通过考试得到的资格。2013年国际上广泛承认的Linux认证有?LinuxProfessionalInstitute(简称为LPI)、SairLinux和GNU、Linux+和RedHatCertifiedEngineer。

7、CIW证书
由以下三个国际性的互联网专家协会认可并签署:国际Webmaster协会(IWA)、互联网专家协会(AIP)及位于欧洲的国际互联网证书机构(ICII)。CIW认证是惟一针对互联网专业人员的国际权威认证,适合设计、开发、管理、安全防护、技术支持互联网企业网相关业务的人士。培训内容由美国五十余家专业机构制定,从而保证了网络知识的全面性和专业性,形成一种中立的、标准全面的培训课程。CIW培训注重网络管理的应用和基础理论,学员不仅可以学到网络知识,还能学到实用技术;不仅学到理论,还学到具体的操作技术,并且广泛适用于企业的各种相关产品。

8、RHCE
Red Hat公司是目前最大的Linux软件产品供应服务商。RHCE是Red Hat公司授权全球企业认同的认证,为学习Linux技术者提供多样选择。在各家国际性的技术认证制度当中,RHCE认证强调受测考生实际动手的测验方式。适合人员:没有Linux或UNIX命令使用经验,但是想进一步了解如何使用和优化计算机上的Red Hat Linux的学习者。就业分析:由于这项认证具有很高的测试应试者实际技能的水平,难度较高,它的持有者会对自己的就业前景充满信心。

9、华为认证
华为认证是深圳华为技术有限公司(简称"华为")凭借多年信息通信技术人才培养经验及对行业发展的理解,基于ICT产业链人才个人职业发展生命周期,以层次化的职业技术认证为指引,搭载华为"云-管-端"融合技术,推出的覆盖IP、IT、CT以及ICT融合技术领域的认证体系;是唯一的ICT全技术领域认证体系。

以上介绍的证书虽然含金量很高,但是都不是程序员必须要考的。毕竟企业公司在招聘时,更看重的是程序员的项目经验和操作能力。当然,大家可以“以证促学”,把证书考试当成是检验自己能力水平的奋斗努力方向。

作者:骨灰级收藏夹
来源:https://blog.csdn.net/JACK_SUJAVA/article/details/110188664

收起阅读 »

男生「理想女友」职业排名出炉!程序媛排第3,第1没有争议?

我们常说,到了该结婚的年纪,选择什么样的人度过一生,决定着一个人后半生的幸福指数。择偶的时候,需要考虑的因素有很多,性格、长相、工作、学历、家庭等等。在男生眼中,同样颜值和学历的女生,工作不同,魅力指数也不一样。工作好的人,即使其他方面表现一般,也很容易成为婚...
继续阅读 »

我们常说,到了该结婚的年纪,选择什么样的人度过一生,决定着一个人后半生的幸福指数。择偶的时候,需要考虑的因素有很多,性格、长相、工作、学历、家庭等等。

在男生眼中,同样颜值和学历的女生,工作不同,魅力指数也不一样。工作好的人,即使其他方面表现一般,也很容易成为婚恋市场中的“红人”。

今天,播妞和大家一起来看看男生眼中「理想女友」的工作排名情况,很可能与你以为的完全不同哦。

「理想女友」职业排行榜

第一名:大学老师
在男生眼中,担任“大学老师”的女生魅力指数极高,原因很简单,大学老师往往意味着高学历,起码是硕士研究生及以上的文凭了。再者,大学老师工作相对轻松,时间也比较自由,不是那么累,并且大学老师收入可观,自然就很受欢迎了。

pic_e9f05b2b.png
第二名:医生
排名第二的,是被称为“白衣天使”的医生,医生主要从事救死扶伤的工作,不仅崇高,收入也很多,是典型的越老越吃香的工作。此外,如果另一半是医生,家人有个头疼脑热的,更容易被解决,和医生组成家庭,能有效提高生活水平。

pic_dae19819.png

第三名:程序媛
因为互联网的不断发展,程序员岗位在如今非常火,但是在大家的印象中,这个工作一般是男生比较多,所以程序媛排在第三名,还是比较出人意料的。

从事这个职业的女生,无论是在学习能力还是逻辑思维上都比较优秀,并且,程序员高薪已经算是互联网人的共识,发展前景和薪资名列前茅。

根据《中国女性程序员职场力大数据报告》报告,越来越多女性正加入程序员的行列,其平均月薪也达到1.5万元,薪酬涨幅超男性程序员。

之前看到过一个帖子讨论说,如果夫妻两人都是大厂程序员,是不是在北京上海买房很简单?财富自由很简单?高收入群体结合在一起岂不是“王炸”组合?

原帖内容:

pic_b79e26f8.png
△ 图片来源于网络,如侵删

这条帖子发出之后,引起了不少网友的讨论。有网友表示,自己和女友就都是大厂程序员,俩人10年时间存下几百万很简单,但是不能让钱只躺在银行,需要做一些理财,让钱生钱,并且两个人一起存钱会更加容易。

也有网友表示,虽然说夫妻两人都是程序员家庭经济状况会没有压力,但两个程序员在一起会不会非常无聊,让生活变得平静如水,没有乐趣。(当然这点,播妞本人是不太赞同的,有的程序员也很浪漫的)对于夫妻两个都是大厂程序员的组合,你看好吗?

pic_308d2068.png

更多排名:

就不一一介绍了,大家可以自己看图哦↓
pic_aacdc8dd.png

作者:骨灰级收藏夹
来源:https://blog.csdn.net/JACK_SUJAVA/article/details/123051958

收起阅读 »

2022技术趋势预测:Python、Java占主导,Rust、Go增长迅速,元宇宙成为关注焦点

2021年是技术不断发展的一年,新技术层出不穷,从移动时代到云计算大数据再到人工智能、机器学习、云原生等逐渐为人们所知晓。技术更迭、日新月异,但万变不离其宗,许多核心技术依旧占据主导,新技术的到来在注入新鲜血液的同时,也促使核心技术的不断更新。2022年1月2...
继续阅读 »

2021年是技术不断发展的一年,新技术层出不穷,从移动时代到云计算大数据再到人工智能、机器学习、云原生等逐渐为人们所知晓。技术更迭、日新月异,但万变不离其宗,许多核心技术依旧占据主导,新技术的到来在注入新鲜血液的同时,也促使核心技术的不断更新。

2022年1月25日, O'Reilly发布了《2022年技术趋势》报告,该报告针对技术发展进行了全面分析,统计了2021年1月至2021年9月的数据,并与2020年同期数据进行了比较。其中涉及微服务、云服务、Web框架、Kubernetes、人工智能、机器学习、数据库、虚拟现实、增强现实和元宇宙等热点话题。

此次报告基于四种数据进行了分析,包括搜索查询、O' Reilly Answer中的提问、按标题分类的资源使用情况以及按主题分类的资源使用情况。其中平台上暂未收集的内容(如QUIC协议或HTTP/3)均未纳入统计范围

数据成搜索频率最高词汇,2022或将继续占主导

作为智能搜索引擎,O'Reilly Answers允许用户搜索特定问题或查找问题库中的示例问题。此次报告对中O'Reilly Answers出现的所有词汇进行统计,结果表明,出现频率最多的五个词汇分别是“数据”、“Python”、“Git”、“测试”和“Java”;而用户搜索频率最高的问题分别是“什么是动态编程?”以及“怎样写好单元测试?”。

由此我们可以得出,数据仍然是开发人员最为关注的话题之一。其中与数据相关的最常见的词对是“数据治理”,其次是“数据科学”。而“数据分析”和“数据工程”的排名较后。这表明,“数据治理”将是数据领域的研究重点。

在过去的数据统计中,PythonJava都是排名前两位的编程语言,今年同样如此。不同之处在于,Python和Java的搜索频率有所下降,而RustGo的频率在迅速增长。除此之外,“编程”也是最常用的关键词之一。位列第三的是Kubernetes,之后分别为Golang和Rust。其中Kubernetes的提问频率反映了对于容器编排的兴趣。

另外,“AWS”、“Azure”和“云”同样也是搜索频率非常高的词汇,这表明开发人员非常关注云平台的发展。GCP谷歌云的频率同样排在榜单前3%。

有关加密货币的词汇(如比特币、以太坊、加密货币、NFT等)频率仍位于前20%,但排名有所下滑。

网络安全成企业关注重点,今年将有何突破?

2021年,由于勒索软件的攻击,各大基础设施、医院以及企业等安全性受到前所未有的威胁。O'Reilly调查显示,有6%的受访者公司遭到攻击。2021年7月6日,美国软件商Kaseya遭到攻击,成千上万的客户受到此次攻击的影响。该公司首席执行官Fred Voccola表示,攻击者要求支付一笔高达7000万美元的赎金。

据O'Reilly研究表明,从这一年开始,网站上安全相关内容大幅增加,有关勒索软件的内容增加了270%,与此同时,隐私相关内容增加了90%。除此之外,有关应用软件安全性、恶意软件、威胁等内容分别有不同程度地增加。

除此之外,标题中带有“安全”或是“网络安全”字样的文章浏览量分别增加了17%和24%。尽管和上述内容相比,这些关键词的增长相对缓慢,但在总数上,提及“安全”的频率远远领先于其他所有词汇。


安全相关的浏览次数以及同比增长

软件架构、Kubernetes和微服务提及次数最多

软件开发是O'Reilly平台中的一大类别,其中涵盖许多内容,例如编程语言、云以及架构等等。数据表明,软件架构Kubernetes微服务是2021年提及次数最多的三个主题,它们的同比增长分别为19%、15%和13%。尽管与API网关(增长218%)等主题的增长趋势相比,这三个数据的增长显得微不足道。但这也反映了一个规律:规模较小的主题的增长趋势较为明显,而已经占据主导地位的主题则增长较为缓慢。API网关相关内容的数量大约是架构或是Kubernetes内容的1/250。

然而,尽管API网关的数量相对较少,但218%的增长仍然令人意外。云原生获得的54%的增长也是如此。如今企业正在大力投资Kubernetes和微服务,他们正借助云服务构建云原生应用程序,而API网关则是客户端和服务之间路由请求的重要工具。

在这种情况下,容器的内容提及次数的显著增长(137%)绝非偶然,容器是打包应用程序和服务的最佳方式。尽管将应用程序迁移到容器并使用Kubernetes生态系统中的工具进行管理的难度不小,但在几年前,企业的应用程序只能运行在少量服务器上,并且只能由人工进行管理。而如今许多企业的规模在不断扩大,拥有数千台服务器,并且提供数百项服务。这都归功于云技术的发展。

提到微服务,不得不提到分布式系统。有关分布式系统的内容在过去一年中增长了39%,相应的,复杂系统和复杂性的提及次数也在不断增长(157%和8%)。同样值得注意的是,几年前不受欢迎的设计模式再次卷土重来,并实现了19%的增长。

量子计算仍然是一个有趣的话题,尽管浏览量较少,但同比增长了39%。对于一个尚未成功的技术而言,这个成绩已经非常好了。尽管量子计算机已有所突破,但制造出能完成工作的量子计算机还需要不少时间。一旦量子计算机到来,势必能够带来新的变革。

除此之外,软件架构同样有着重要的作用,没有架构,我们无法重建遗留应用程序、无法使用云技术、也无法使用微服务等等。软件架构能够帮助维护不灵活的遗留应用程序使它们随着需求的变化而不断更新。因此软件架构的提及次数不断增加也在意料之中。


编程语言的浏览量和同比增长

云服务不断发展,云原生将为我们带来什么?

过去一年云技术不断发展,云服务的竞争越发激烈。调查显示,AWS的内容减少了3%,而Microsoft AzureGoogle Cloud的内容分别增长了54%,其中有关Azure的内容几乎与和AWS的数量相等,Google Cloud位列第三。除了云服务之外,有关云的内容在去年增长了15%,而云原生内容的增长幅度高达54%。

另一个趋势在于,有关混合云和多云的的内容基数依旧非常小(大约是Google Cloud的十分之一),但增长速度非常快(分别为145%和240%)。这反映了一个问题,企业无法仅仅通过单一的云服务器构建云战略。构建云战略就必须要意识到云本质上就是多(或混合的),最重要的不是选择哪一个云服务器,而是如何跨多个云服务器构建有效的云架构,这成为了云原生的一个重要内容。


云服务器的浏览量和同比增长

Web框架稳定发展,元框架是否会打破格局?

在过去两年中,Web编程技术一直稳定发展。有关核心组件HTML、CSS和JavaScript的内容几乎没有变化(分别上升1%、2%和下降3%)。如果Java和Python是企业和数据开发人员的核心语言,那么HTML、CSS和JavaScript对于前端开发人员来说更是如此。据统计,有关PHP的内容增加了6%,有关jQuery的内容增加了28%而有关网页设计的内容增加了23%。

在新兴框架和元框架中,Svelte似乎正在迅速发展(增长71%),Vue和Next.js的内容有所减少(均减少13%)。若这种情况持续下去,Svelte可能会在几年内成为流行框架之一。

而有关React框架的内容数量基本没有变化(增长2%),但Angular框架的内容显著减少(减少16%)。JavaScript的数量与React的几乎相同,Rails的内容则减少19%。


Web框架的数量和同比增长

薛定谔的人工智能、机器学习和数据

尽管网络上出现了许多有关人工智能的预测,有人认为人工智能将面临低谷,也有人说它将是未来的新秀。但据O'Reilly表明,在2021年,标题中带有“人工智能”的内容减少了23%,而有关人工智能的内容在2021年减少了11%。主导这一领域的主题是机器学习(ML),人工智能的内容数量仅为机器学习的四分之一。

现在让我们来看看部分具体的技术。深度学习的内容减少了14%,但神经网络的内容增加了13%,强化学习增加了37%,对抗性网络增加了51%。由此看来,开发者的关注点已经从一般内容转向了具体内容。

同样值得关注的是,有关数据治理(增加87%)和GDPR(增加61%)的内容显著增加。数据治理及其相关内容(如数据来源、数据完整性、审计、可解释性等)将越来越重要。未来对于数据的监管势必会更加严厉。数据治理将继续存在。


AI和ML等内容的数量和同比增长

NoSQL数据库出路何在?

没有数据和数据库,就不存在机器学习。数据表明,Oracle在数据库中占据主导地位,其内容增加了5%,开源MySQL数据库的内容增加了22%,NoSQL的内容减少了17%,其中包括Cassandra、HBase、Redis、MongoDB等等。NoSQL与其说是一种技术,不如说是一种理念——致力于为系统设计人员扩展储存选项的数量。

在NoSQL数据库中,MongoDB的内容增加了10%。Cassandra、Redis和HBase的内容大幅减少(分别为27%、8%和57%)。尽管自2020年以来,这四种数据库的内容总数减少了4%,但比MySQL的内容数量多40%。尽管趋势已经由NoSQL转向关系数据库,但这并非最终结果。

在去年,图形数据库受到越来越多人的关注,其内容增加了44%,但这仍然是一个较小的类别。同样,有关时序数据库的内容增加了21%。时序数据库指的是用来存储时序列数据并以时间(点或区间)建立索引的软件,对于关于监控、日志记录和可观察性的应用程序非常重要。

尽管图形数据库和关系数据库正迅速发展,但关系数据库仍然并且将持续主导着数据库世界,NoSQL没有机会取代关系数据库。


数据库内容数量及同比增长

虚拟现实or增强现实?元宇宙进入大众视野

虚拟现实(VR)和增强现实(AR)同样是O'Reilly中的热点话题。尽管它们几度成为热点,但从未持续多久。早在2013年,谷歌眼镜就成为热点,但从未得到广泛使用。而像Oculus这样的初创公司已经针对消费者制造了VR眼镜,但它们从未成功打入玩家市场。

然而在今年,我们仍然认为VR和AR具备极大的潜力。马克·扎克伯格早在去年7月份就提出了“元宇宙”,并将Facebook重新命名为Meta,从而引发了一场新变革。微软等其他公司也纷纷效仿,推出了自己的Metaverse版本。苹果一直保持低调,但该公司被曝出正在开发AR眼镜。

数据表明,虚拟现实、VR和AR相关内容在不断增加(分别增加了13%、28%和116%)。但由于O'Reilly的数据统计截止到去年9月,“metaverse”一词并未纳入统计,尽管它的搜索量急剧增加了489%。


VR和AR的内容数量和同比增长

2022年技术预测,哪些领域将再次登顶?

在总结了O'Reilly中超过50000个项目的信息之后,在查看了一百万个的搜索查询以及O'Reilly Answers中的结果之后,对于2022年我们将有哪些期望呢?

在这其中,许多事件引起了人们的注意:GPT-3 利用深度学习产生类似人类编写的文本,网络犯罪分子在发起软件攻击后索要数百万美元等等。许多技术事件得到了广泛报道,尽管还没有出现在数据统计中,例如机器人流程自动化(RPA)、数字孪生、边缘计算和5G等。这些技术可能会具有重要意义,这取决于未来会把我们带到哪里。

【参考资料】

https://www.oreilly.com/radar/technology-trends-for-2022/

作者 | 郭露 责编 | 张红月
出品 | CSDN(ID:CSDNnews)

收起阅读 »

sleep()为什么要 try catch

前言 当我们在 Java 中使用 sleep() 让线程休眠的时候,总是需要使用 try catch 去包含它: try { sleep(1000); } catch (InterruptedExcept...
继续阅读 »

前言


当我们在 Java 中使用 sleep() 让线程休眠的时候,总是需要使用 try catch 去包含它:


        try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

但是,我们却很少在 catch 中执行其它代码,仿佛这个 try catch 是理所当然一样,但其实,存在即合理,不可能无缘无故地多出一个 try catch。


在给出原因之前,我先要讲讲另外一个知识点,那就是如何去停止线程。


如何停止线程


stop()


最直接了断的方法就是调用 stop(),它能直接结束线程的执行,就如:


        // 开启线程循环输出当前的时间
Thread thread = new Thread(){
@Override
public void run() {
while (true){
System.out.println(System.currentTimeMillis());
}
}
};
thread.start();
// 睡眠两秒后停止线程
try {
sleep(2000);
thread.stop();
} catch (InterruptedException e) {
e.printStackTrace();
}

输出结果:


···
1643730391073
1643730391073
1643730391073
1643730391073
1643730391073

Process finished with exit code 0

很明显是能够把线程暂停掉的。但是,该方法现在被标记遗弃状态。



大概意思就是:


这个方法原本是设计用来停止线程并抛出线程死亡的异常,但是实际上它是不安全的,因为它是直接终止线程解放锁,这很难正确地抛出线程死亡的异常进行处理,所以,更好的方式是设置一个判断条件,然后在线程执行的过程中去判断该条件以去决定是否要进行停止线程,这样就能进行处理并有序地退出这个线程。假如一个线程等待很久的话,那才会直接中断线程并抛出异常。


按照上面所说,我们可以设置一个变量来进行控制,当然,我们可以声明一个 bool 类型进行判断,但是更好的方式是使用 interrupt()。


interrupt()


源码:


    public void interrupt() {
if (this != Thread.currentThread())
checkAccess();

synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}

Just to set the interrupt flag 这里很明显就是说明只是进行了一个中断标记而已,并不会直接中断线程,所以,需要使用 interrupt() 的时候,我们还要在线程中进行 interrupt 的状态判断:



// 开启线程循环输出当前的时间
// 每次循环前都去判断该线程是否被中断了
Thread thread = new Thread(){
@Override
public void run() {
while (true){
if(isInterrupted()){
return;
}
System.out.println(System.currentTimeMillis());
}
}
};
thread.start();
// 睡眠两秒后标记中断线程
try {
sleep(2000);
thread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}

输出结果:


···
1643731839919
1643731839919
1643731839919

Process finished with exit code 0

这样也能正常的中断线程。



好了,如何中断线程到这里讲完了,大家拜拜~~


喂!等下,这文章是不是还有东西没讲 (#`O′),sleep()为什么要 try catch 还没说。



好像是这样。


中断等待


我们再看看 stop() 被标记为遗弃的说明:


 If the target thread waits for long periods (on a condition variable, for example),
the interrupt method should be used to interrupt the wait.

也就是说,当线程在等待过久的时候,interrupt() 应该去中断这个等待。


所以,原因就找到了,要加 try catch,是因为当线程在 sleep() 的时候,调用线程的 interrupt() 方法,就会直接中断线程的 sleep 状态,抛出 InterruptedException。


因为调用 interrupt() 的时候,其实就是想尽快地结束线程,所以,继续的 sleep 是没有意义的,应该尽快结束。


        // 开启线程
// 线程睡眠 10 秒
Thread thread = new Thread(){
@Override
public void run() {
try {
sleep(10000);
} catch (InterruptedException e) {
System.out.println("sleep 状态被中断了!");
e.printStackTrace();
}
}
};
thread.start();
// 睡眠两秒后标记中断线程
try {
sleep(2000);
thread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}

输出结果:


sleep 状态被中断了!
java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at com.magic.vstyle.TestMain$1.run(TestMain.java:16)

Process finished with exit code 0

这时,我们就可以 catch 到这个异常后进行额外操作,例如回收资源等。这时,停止线程就是一种可控的行为了。


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

【解惑】App处于前台,Activity就不会被回收了?

昨天在康 KunMinX 大佬的:《重学安卓:Activity 生命周期的 3 个辟谣》,在加餐处看到这段:转换后的理解:单进程场景,Activity被回收只可能是因为进程被系统回收了。感觉不太对?因为在很久以前,遇到过这样一个场景:App...
继续阅读 »

昨天在康 KunMinX 大佬的:《重学安卓:Activity 生命周期的 3 个辟谣》,在加餐处看到这段:

转换后的理解:单进程场景,Activity被回收只可能是因为进程被系统回收了

感觉不太对?因为在很久以前,遇到过这样一个场景:

App打开多个Activity,然后手机晾一边,过一段时间后(屏幕常亮),点击回退,之前的Activity空白,然后重新加载了。

App在前台,不在栈顶的Activity却被干掉,但进程还健在,如果真是这样,就和上面的理解有些出入了。

立马写个代码验证下,大概流程如下:

写个父类Activity,生命周期回调加日志打印,接着打开一个Activity,包含一个按钮,点击后依次打开多个Activity,最后一个加个按钮,点一下就申请一个大一点的ByteArray来模拟内存分配,看内存不足时是否会回收Activity。

测试结果如下:

App宁愿OOM,也不愿意回收Activity,鬼使神差地加上 android:largeHeap="true" ,结果一样。

em...难道是我记错了???

等等!!!我好像混淆了两个东西:系统可用内存不足 和 应用可用内存不足

0x1、系统可用内存不足

LMK机制

Android系统中,进程的生命周期由系统控制,处于体验和性能考虑,在APP中点击Home键或Back回退操作,并不会真的杀掉APP,进程依旧存在于内存中,这样下次启动此APP时就能更加快速。随着系统运行时间增长,打开APP越来越多,内存中的进程随着增多,系统的可用内存会越来越少。咋办,总不能让用户自己去杀进程吧,所以系统内置一套 回收机制,当系统可用内存达到一个 阈值,系统会根据 进程优先级 来杀掉一部分进程,释放内存供后续启动APP使用。

Android的这套回收机制,是基于Linux内核的OOM规则改进而来的,叫 Low Memory Killer,简称 LMK

阈值 & 杀谁

通过下述两个文件配合完成,不同手机数值可能不同,以我的老爷机 魅蓝E2 为例 (Android 11的Mix2S一直说没权限打开此文件):

# /sys/module/lowmemorykiller/parameters/minfree
# 单位:Page页,1Page = 4KB
18432,23040,27648,46080,66560,97280

# /sys/module/lowmemorykiller/parameters/adj
0,58,117,176,529,1000

Android系统会为每个进程维护一个 adj(优先级)

  • Android 6及以前称为:oom_adj,值范围:[-17,16],LMK要换算*1000/17
  • Android 7后称为:oom_score_adj,值范围:[-1000,1000]

然后,上面两个文件的值,其实是以一一对应的,比如:

66560  * 4 / 1024 = 260MB → 当系统可用内存减少到260MB时,会杀掉adj值大于529的进程;
18432 * 4 / 1024 = 72MB → 当系统可用内存减少到72MB,杀掉ajd值大于0的进程;

adj怎么看

直接通过命令行查看:

可以看到,adj是动态变化的,当App状态及四大组件生命周期发生改变时,都会改变它的值。常见ADJ级别如下:

  • NATIVE_ADJ → -1000,init进程fork出来的native进程,不受system管控
  • SYSTEM_ADJ → -900,system_server进程
  • PERSISTENT_PROC_ADJ → -800,系统persistent进程,一般不会被杀,杀了或者Carsh系统也会重新拉起
  • PERSISTENT_SERVICE_ADJ → -700,关联着系统或persistent进程
  • FOREGROUND_APP_ADJ → 0,前台进程
  • VISIBLE_APP_ADJ → 100,可见进程
  • PERCEPTIBLE_APP_ADJ → 200,可感知进程,比如后台音乐播放
  • BACKUP_APP_ADJ → 300,执行bindBackupAgent()过程的备份进程
  • HEAVY_WEIGHT_APP_ADJ → 400,重量级进程,system/rootdir/init.rc文件中设置
  • SERVICE_ADJ → 500,服务进程
  • HOME_APP_ADJ → 600,Home进程,类型为ACTIVITY_TYPE_HOME的应用,如Launcher
  • PREVIOUS_APP_ADJ → 700,用户上一个使用的App进程
  • SERVICE_B_ADJ → 800,B List中的Service
  • CACHED_APP_MIN_ADJ → 900,不可见进程 的adj最小值
  • CACHED_APP_MAX_ADJ → 906,不可见进程的adj最大值
  • UNKNOWN_ADJ → 1001,一般指将要会缓存进程,无法获取确定值

关于ADJ计算的详细算法分析可见Gityuan大佬的:《解读Android进程优先级ADJ算法》,干货多多,顺带从总结处捞一波进程保活伎俩:

  • UI进程与Service进程分离,包含Activity的Service进程,一进后台ADJ>=900,随时可能被系统回收,分离的话ADJ=500,被杀的可能性降低,尤其是系统允许自启动的服务进程,必须做UI分离,避免消耗较大内存;
  • 真正需要用户可感知的应用,调用startForegroundService()启用前台服务,ADJ=200;
  • 进程中的Service工作完,务必主动调用stopService或stopSelf来停止服务,避免占用内存,浪费系统资源;
  • 不要长时间绑定其他进程的service或者provider,每次使用完成后应立刻释放,避免其他进程常驻于内存;
  • APP应该实现接口onTrimMemory()和onLowMemory(),根据TrimLevel适当地将非必须内存在回调方法中加以释放,当系统内存紧张时会回调该接口,减少系统卡顿与杀进程频次;
  • 更应在优化内存上下功夫,相同ADJ级别,系统会优先杀内存占用的进程;

:能否把自己的App的ADJ值设置为-1000,让其杀不死? :不可以,要有root权限才能修改adj,而且改了重启手机还是恢复的。

扯得有点远了,回到问题上:

系统内存不足时,会在内核层直接查杀进程,不会在Framework层还跟你叨逼叨看回收哪个Activity。

所以在系统这个层面,单进程场景,Activity被回收只可能是因为进程被系统回收了,这句话是没毛病的,但在应用层面就不一定了。


0x2、应用可用内存不足

APP进程(虚拟机)的内存分配实际上是对 堆的分配和释放,为了整个系统的内存控制需要,会为每个应用程序设置一个 堆的限制阈值,如果应用使用内存接近阈值还尝试分配内存,就很容易引起OOM。

当然,不会那么蠢,还要开发仔自己在APP里回收内存,虚拟机自带 GC,这里就不向去卷具体的回收算法了

假设应用内存不足真的会回收Activity,那该怎么设计?一种解法如下:

应用启动时,开一个子线程,定时轮询当前可用内存是否超过阈值,超过的话干掉Activity

那就来跟下Android是不是也是这样设计的?

Activity回收机制

跟下应用启动入口:ActivityThread → main()

跟下 attach()

这里就非常像,run()中计算:已用内存 > 3/4最大内存,就执行 releaseSomeActivities(),跟下:

所以 getService() 是获取了 IActivityTaskManager.aidl接口,具体的实现类是 ActivityTaskManangerService

继续往下跟: RootActivityContainer → releaseSomeActivitiesLocked()

跟下:WindowProcessController → getReleaseSomeActivitiesTasks()

然后再往下走就是释放Activity的代码了:ActivityStack → releaseSomeActivitiesLocked()

具体咋释放,就不往下跟了哈,接着跟下是怎么监控的~

内存监控机制

跟回:BinderInternal.addGcWatcher()

这里可能看得你有点迷,但是当你理解了就会觉得很妙了:

虚拟机GC会干掉 WeakReference 的对象,在释放内存前,会调用对象的 finalize(),而这里有创建了一个新的 WeakReference 实例。下次GC,又会走一遍这里的代码,啧啧啧,相比起轮询高效多了

到此,应用内存不足回收Activity的流程就大概缕清了,接着可以写个代码验证下是否真的这样。

Demo验证

先试下两个Task的:

模拟内存分配的页面,然后一直点~

宁愿OOM,也不回收,试试三个~

好家伙,onDestory()了,此时按Back回退这些页面,发现走了onCreate(),即回收了,接着试试四个的情况:

可以,每次只回收一个Task,到此验证完毕了~

0x3、结论

  • 系统内存不足时,直接在内核层查杀(回收)进程,并不会考虑回收哪个Activity;
  • 进程内存不足时,如果此进程 Activity Task数 >= 3 且 使用内存超过3/4,会对 不可见 Task进行回收,每次回收 1个 Task,回收时机为每次gc;

参考文献


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

收起阅读 »

Androd Gradle 使用技巧之模块依赖替换

背景 我们在多模块项目开发过程中,会遇到这样的场景,工程里依赖了一个自己的或者其他同事的 aar 模块,有时候为了开发调试方便,经常会把 aar 改为本地源码依赖,开发完毕并提交的时候,会再修改回 aar 依赖,这样就会很不方便,开发流程图示如下: 解决 一...
继续阅读 »

背景


我们在多模块项目开发过程中,会遇到这样的场景,工程里依赖了一个自己的或者其他同事的 aar 模块,有时候为了开发调试方便,经常会把 aar 改为本地源码依赖,开发完毕并提交的时候,会再修改回 aar 依赖,这样就会很不方便,开发流程图示如下:


截屏2022-02-09 下午4.56.16.png


解决


一开始我们通过在 appbuild.gradle 里的 dependency 判断如果是需要本地依赖的 aar,就替换为 implementation project 依赖,伪代码如下:


dependencies {
if(enableLocalModule) {
implementation 'custom:test:0.0.1'
} else {
implementation project(path: ':test')
}
}

这样就可以不用每次提交代码还要修改回 aar 依赖,但是如果其他模块如果也依赖了该 aar 模块,就会出现问题,虽然可以继续修改其他模块里的依赖方式,但是这样就会有侵入性,而且不能彻底解决问题,仍然有可能出现本地依赖和 aar 依赖的代码不一致问题。


Gradle 官方针对这种场景提供了更好的解决方式 DependencySubstitution,使用方式如下:


步骤1:在 settting.gradle,添加如下代码:


// 加载本地 module
if (file("local.properties").exists()) {
def properties = new Properties()
def inputStream = file("local.properties").newDataInputStream()
properties.load( inputStream )
def moduleName = properties.getProperty("moduleName")
def modulePath = properties.getProperty("modulePath")
if (moduleName != null && modulePath != null) {
include moduleName
project(moduleName).projectDir = file(modulePath)
}
}

步骤2:在 appbuild.gradle 添加以下代码


configurations.all {
resolutionStrategy.dependencySubstitution.all { DependencySubstitution dependency ->
// use local module
if (dependency.requested instanceof ModuleComponentSelector && dependency.requested.group == "custom") {
def targetProject = findProject(":test")
if (targetProject != null) {
dependency.useTarget targetProject
}
}
}
}

步骤3::在 local.properties


moduleName=:test
modulePath=../AndroidStudioProjects/TestProject/testModule

到这里就大功告成了,后续只需要在 local.properties 里开启和关闭,即可实现 aar 模块本地依赖调试,提交代码也不用去手动修改回 aar 依赖。


参考



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

Android 使用 Retrofit 发送网络请求

简介 在Android应用中,如果不是单机的话,应该都有请求后端接口API的情况,本篇文章就介绍下Retrofit在Android中如何进行使用的 相关代码 我们以一个简单的登录接口为例 完整代码GitHub上有:github.com/lw124392545…...
继续阅读 »

简介


在Android应用中,如果不是单机的话,应该都有请求后端接口API的情况,本篇文章就介绍下Retrofit在Android中如何进行使用的


相关代码


我们以一个简单的登录接口为例


完整代码GitHub上有:github.com/lw124392545…


仅做代码参考,目前数据监控上传是有了,但界面这些还很粗糙,没有完善


相关的依赖引入


首先我们在工程中引入相关的依赖:


    implementation 'com.squareup.okhttp3:okhttp:4.5.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

相关的手机权限开启


需要在文件中开启网络权限:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.selfgrowth">

<uses-permission android:name="android.permission.INTERNET"/>

<application
......
</application>

</manifest>

配置Retrofit Client


Client的相关配置:单例,配置基于OKHTTP,Gson序列化;OKHTTP中添加了请求拦截器


@Data
public class RetrofitClient {

private static final RetrofitClient instance = new RetrofitClient();

private final Retrofit retrofit = new Retrofit.Builder()
.baseUrl(HttpConfig.ADDRESS) //基础url,其他部分在GetRequestInterface里
.client(httpClient())
.addConverterFactory(GsonConverterFactory.create()) //Gson数据转换器
.build();

public static RetrofitClient getInstance() {
return instance;
}

private OkHttpClient httpClient() {
return new OkHttpClient.Builder()
.addInterceptor(new AccessTokenInterceptor())
.connectTimeout(20, TimeUnit.SECONDS)
.build();
}
}

配置通用的请求拦截器


比如在请求中,带上Authorization等


public class AccessTokenInterceptor implements Interceptor {

@NonNull
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
if (UserCache.getInstance().getToken() == null) {
return chain.proceed(chain.request());
}

Request original = chain.request();
Request.Builder requestBuilder = original.newBuilder()
.addHeader("Authorization", UserCache.getInstance().getToken());
Request request = requestBuilder.build();
return chain.proceed(request);
}
}

Retrofit接口定义


登录请求的接口定义:


public interface UserApi {

/**
* 用户登录
**/
@POST("auth/user/login")
Call<ApiResponse> login(@Body LoginUser user);
}

Retrofit Request具体请求编写


我们首先定义一个抽象类,在其中持有我们的RetrofitClient全局类,在其中发起请求,由于Android UI的形式,请求是异步的


public abstract class Request {

final Retrofit retrofit;

public Request() {
this.retrofit = RetrofitClient.getInstance().getRetrofit();
}

/**
* 发送网络请求(异步)
* @param call call
*/
void sendRequest(Call<ApiResponse> call, Consumer<? super Object> success, Consumer<? super Object> failed) {
call.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(Call<ApiResponse> call, Response<ApiResponse> response) {
if (response.code() != 200) {
Log.w("Http Response", "请求响应错误");
failed.accept(response.raw().message());
return;
}
if (response.body() == null || response.body().getData() == null) {
success.accept(null);
return;
}
Object res = response.body().getData();
if (String.valueOf(res).isEmpty()) {
success.accept(null);
return;
}
success.accept(res);
}

@Override
public void onFailure(Call<ApiResponse> call, Throwable t) {
System.out.println("GetOutWarehouseList->onFailure(MainActivity.java): "+t.toString() );
}
});
}
}

如上所示,请求成功就执行success相关的逻辑,失败则执行failed相关的逻辑


登录请求的具体逻辑如下:构造Retrofit Interface,发起请求


public class UserRequest extends Request {

public void login(LoginUser user, Consumer<? super Object> success, Consumer<? super Object> failed) {
UserApi request = retrofit.create(UserApi.class);
Call<ApiResponse> call = request.login(user);
sendRequest(call, success, failed);
}
}

Android UI中进行调动


使用示例如下,点击一个登录按钮后触发


public class LoginFragment extends Fragment {

private final UserRequest userRequest = new UserRequest();

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_login, container, false);
Button loginButton = rootView.findViewById(R.id.login_button);
loginButton.setOnClickListener(view -> {
EditText email = rootView.findViewById(R.id.login_email_edit);
EditText password = rootView.findViewById(R.id.login_password_edit);

final LoginUser user = LoginUser.builder()
.email(email.getText().toString())
.password(password.getText().toString())
.build();

// 获取相关的用户名和密码后,调用登录接口
userRequest.login(user, (token) -> {
UserCache.getInstance().initUser(email.getText().toString(), token.toString());
final SharedPreferences preferences = requireContext().getSharedPreferences("userInfo", Context.MODE_PRIVATE);
final SharedPreferences.Editor edit = preferences.edit();
edit.putString("username", email.getText().toString());
edit.putString("password", password.getText().toString());
edit.apply();
Snackbar.make(view, "登录成功:" + token.toString(), Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}, failedMessage -> {
Snackbar.make(view, "登录失败:" + failedMessage, Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
});
});
return rootView;
}

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
final SharedPreferences preferences = getActivity().getSharedPreferences("userInfo", Activity.MODE_PRIVATE);
final String userName = preferences.getString("username", "");
final String password = preferences.getString("password", "");
final EditText emailEdit = getView().findViewById(R.id.login_email_edit);
final EditText passwordEdit = getView().findViewById(R.id.login_password_edit);
emailEdit.setText(userName);
passwordEdit.setText(password);
}
}

总结


本篇文章中介绍了如Android学习中如何使用Retrofit发起网络请求


但由于吃初学,虽然感觉能用,但有点繁琐,不知道在实际的Android开发中,网络请求的最近实践是怎么样的,如果有的话,大佬可以在评论区告知下,感谢


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

Flutter之GetX依赖注入使用详解

put 为了验证依赖注入的功能,首先创建两个测试页面:PageA 和 PageB ,PageA 添加两个按钮 toB 和 find ,分别为跳转 PageB 和获取依赖;在 PageB 中通过 put 方法注入依赖对象,然后调用按钮触发 find 获取依赖。关...
继续阅读 »

put


为了验证依赖注入的功能,首先创建两个测试页面:PageA 和 PageB ,PageA 添加两个按钮 toBfind ,分别为跳转 PageB 和获取依赖;在 PageB 中通过 put 方法注入依赖对象,然后调用按钮触发 find 获取依赖。关键源码如下:


PageA


TextButton(
child: const Text("toB"),
onPressed: (){
/// Navigator.push(context, MaterialPageRoute(builder: (context) => const PageB()));
/// Get.to(const PageB());
},
),

TextButton(
child: const Text("find"),
onPressed: () async {
User user = Get.find();
print("page a username : ${user.name} id: ${user.id}");
})


PageB:


Get.put(User.create("张三", DateTime.now().millisecondsSinceEpoch)));

User user = Get.find();

TextButton(
child: const Text("find"),
onPressed: (){
User user = Get.find();
print("${DateTime.now()} page b username : ${user.name} id: ${user.id}");
})

其中 User 为自定义对象,用于测试注入,源码如下:


User:


class User{
final String? name;
final int? id;

factory User.create(String name, int id){
print("${DateTime.now()} create User");
return User(name, id);
}
}

Navigator 路由跳转


首先使用 Flutter 自带的路由管理从 PageA 跳转 PageB, 然后返回 PageA 再点击 find 按钮获取 User 依赖:


Navigator.push(context, MaterialPageRoute(builder: (context) => const PageB()));

流程:PageA -> PageB -> put -> find -> PageA -> find


输出结果:


/// put
I/flutter (31878): 2022-01-27 19:18:20.851800 create User
[GETX] Instance "User" has been created

/// page b find
I/flutter (31878): 2022-01-27 19:18:22.170133 page b username : 张三 id: 1643282300139

/// page a find
I/flutter (31878): 2022-01-27 19:18:25.554667 page a username : 张三 id: 1643282300139

通过输出结果发现,在 PageB 注入的依赖 User,在返回 PageA 后通过 find 依然能获取,并且是同一个对象。通过 Flutter 通过源码一步一步剖析 Getx 依赖管理的实现 这篇文章知道,在页面销毁的时候会回收依赖,但是这里为什么返回 PageA 后还能获取到依赖对象呢?是因为在页面销毁时回收有个前提是使用 GetX 的路由管理页面,使用官方的 Navigator 进行路由跳转时页面销毁不会触发回收依赖。


GetX 路由跳转


接下来换成使用 GetX 进行路由跳转进行同样的操作,再看看输出结果:


Get.to(const PageB());

流程:PageA -> PageB -> put -> find -> PageA -> find


输出结果:


[GETX] GOING TO ROUTE /PageB

/// put
I/flutter (31878): 2022-01-27 19:16:32.014530 create User
[GETX] Instance "User" has been created

/// page b find
I/flutter (31878): 2022-01-27 19:16:34.043144 page b username : 张三 id: 1643282192014
[GETX] CLOSE TO ROUTE /PageB
[GETX] "User" deleted from memory

/// page a find error
E/flutter (31878): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: "User" not found. You need to call "Get.put(User())" or "Get.lazyPut(()=>User())"

发现在 PageB 中获取是正常,关闭 PageB 时输出了一句 "User" deleted from memory 即在 PageB 注入的 User 被删除了,此时在 PageA 再通过 find 获取 User 就报错了,提示需要先调用 put 或者 lazyPut 先注入依赖对象。这就验证了使用 GetX 路由跳转时,使用 put 默认注入依赖时,当页面销毁依赖也会被回收。


permanent


put 还有一个 permanent 参数,在 Flutter应用框架搭建(一)GetX集成及使用详解 这篇文章里介绍过,permanent 的作用是永久保留,默认为 false,接下来在 put 时设置 permanent 为 true,并同样使用 GetX 的路由跳转重复上面的流程。


关键代码:


Get.put(User.create("张三", DateTime.now().millisecondsSinceEpoch), permanent: true);

流程:PageA -> PageB -> put -> find -> PageA -> find


输出结果:


[GETX] GOING TO ROUTE /PageB
/// put
I/flutter (31878): 2022-01-27 19:15:16.110403 create User
[GETX] Instance "User" has been created

/// page b find
I/flutter (31878): 2022-01-27 19:15:18.667360 page b username : 张三 id: 1643282116109
[GETX] CLOSE TO ROUTE /PageB
[GETX] "User" has been marked as permanent, SmartManagement is not authorized to delete it.

/// page a find success
I/flutter (31878): page a username : 张三 id: 1643282116109

设置 permanent 为 true 后,返回 PageA 同样能获取到依赖对象,说明依赖并没有因为页面销毁而回收,GetX 的日志输出也说明了 User 被标记为 permanent 而不会被删除:"User" has been marked as permanent, SmartManagement is not authorized to delete it.


lazyPut


lazyPut 为延迟初始化依赖对象 :


Get.lazyPut(() => User.create("张三", DateTime.now().millisecondsSinceEpoch));

TextButton(
child: const Text("find"),
onPressed: () async {
User user = Get.find();
print("${DateTime.now()} page b username : ${user.name} id: ${user.id}");
})

流程:PageA -> PageB -> put -> find -> find -> PageA -> find, 从 PageA 跳转 PageB,先通过 lazyPut 注入依赖,然后点击 find 获取依赖,过 3 秒再点击一次,然后返回 PageA 点击 find 获取一次。


输出结果:


[GETX] GOING TO ROUTE /PageB
/// lazyPut

/// page b find 1
I/flutter (31878): 2022-01-27 17:38:49.590295 create User
[GETX] Instance "User" has been created
I/flutter (31878): 2022-01-27 17:38:49.603063 page b username : 张三 id: 1643276329589

/// page b find 2
I/flutter (31878): 2022-01-27 17:38:52.297049 page b username : 张三 id: 1643276329589
[GETX] CLOSE TO ROUTE /PageB
[GETX] "User" deleted from memory

/// page a find error
E/flutter (31878): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: "User" not found. You need to call "Get.put(User())" or "Get.lazyPut(()=>User())"

通过日志发现 User 对象是在第一次调用 find 时进行初始化话的,第二次 find 时不会再次初始化 User;同样的 PageB 销毁时依赖也会被回收,导致在 PageA 中获取会报错。


fenix


lazyPut 还有一个 fenix 参数默认为 false,作用是当销毁时,会将依赖移除,但是下次 find 时又会重新创建依赖对象。


lazyPut 添加 fenix 参数 :


 Get.lazyPut(() => User.create("张三", DateTime.now().millisecondsSinceEpoch), fenix: true);

流程:PageA -> PageB -> put -> find -> find -> PageA -> find,与上面流程一致。


输出结果:


[GETX] GOING TO ROUTE /PageB
/// lazyPut

/// page b find 1
[GETX] Instance "User" has been created
I/flutter (31878): 2022-01-27 17:58:58.321564 create User
I/flutter (31878): 2022-01-27 17:58:58.333369 page b username : 张三 id: 1643277538321

/// page b find 2
I/flutter (31878): 2022-01-27 17:59:01.647629 page b username : 张三 id: 1643277538321
[GETX] CLOSE TO ROUTE /PageB

/// page a find success
I/flutter (31878): 2022-01-27 17:59:07.666929 create User
[GETX] Instance "User" has been created
I/flutter (31878): page a username : 张三 id: 1643277547666

通过输出日志分析,在 PageB 中的表现与不加 fenix 表现一致,但是返回 PageA 后获取依赖并没有报错,而是重新创建了依赖对象。这就是 fenix 的作用。


putAsync


putAsyncput 基本一致,不同的是传入依赖可以异步初始化。测试代码修改如下:


print("${DateTime.now()} : page b putAsync User");
Get.putAsync(() async {
await Future.delayed(const Duration(seconds: 3));
return User.create("张三", DateTime.now().millisecondsSinceEpoch);
});

使用 Future.delayed 模拟耗时操作。


流程:PageA -> PageB -> put -> find -> PageA -> find


输出结果:


[GETX] GOING TO ROUTE /PageB

/// putAsync
I/flutter (31878): 2022-01-27 18:48:34.280337 : page b putAsync User

/// create user
I/flutter (31878): 2022-01-27 18:48:37.306073 create User
[GETX] Instance "User" has been created

/// page b find
I/flutter (31878): 2022-01-27 18:48:40.264854 page b username : 张三 id: 1643280517305
[GETX] CLOSE TO ROUTE /PageB
[GETX] "User" deleted from memory

/// page a find error
E/flutter (31878): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: "User" not found. You need to call "Get.put(User())" or "Get.lazyPut(()=>User())"

通过日志发现,put 后确实是过了 3s 才创建 User。


create


createpermanent 参数默认为 true,即永久保留,但是通过 Flutter应用框架搭建(一)GetX集成及使用详解 这篇源码分析知道,create 内部调用时 isSingleton 设置为 false,即每次 find 时都会重新创建依赖对象。


Get.create(() => User.create("张三", DateTime.now().millisecondsSinceEpoch));

流程:PageA -> PageB -> put -> find -> find -> PageA -> find


输出结果:


[GETX] GOING TO ROUTE /PageB
/// create

/// page b find 1
I/flutter (31878): 2022-01-27 18:56:10.520961 create User
I/flutter (31878): 2022-01-27 18:56:10.532465 page b username : 张三 id: 1643280970520

/// page b find 2
I/flutter (31878): 2022-01-27 18:56:18.933750 create User
I/flutter (31878): 2022-01-27 18:56:18.934188 page b username : 张三 id: 1643280978933

[GETX] CLOSE TO ROUTE /PageB

/// page a find success
I/flutter (31878): 2022-01-27 18:56:25.319224 create User
I/flutter (31878): page a username : 张三 id: 1643280985319

通过日志发现,确实是每次 find 时都会重新创建 User 对象,并且退出 PageB 后还能通过 find 获取依赖对象。


总结


通过代码调用不同的注入方法,设置不同的参数,分析输出日志,详细的介绍了 putlazyPutputAsynccreate 以及 permanentfenix 参数的具体作用,开发中可根据实际业务场景灵活使用不同注入方式。关于注入的 tag 参数将在后续文章中详细介绍。


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

晒一波程序员的工位,你中意哪一款?

程序员的圈子啊那是十分神秘,又令人着迷的。每天的工作就是对着电脑,那他们的工作是如何的呢?我们来品一品(PS:后面奉上各位大佬的桌面,别走开哦)↓↓↓最最常见的普通版:升级版:算不得体贴版:逼退人升级版:舒适版:超人性版:独立版:高级版:友谊版:高级程序员版:...
继续阅读 »

程序员的圈子啊

那是十分神秘,

又令人着迷的。

每天的工作就是对着电脑,

那他们的工作是如何的呢?

我们来品一品

(PS:后面奉上各位大佬的桌面,别走开哦)


↓↓↓


最最常见的普通版:



升级版:





算不得体贴版:



逼退人升级版:

图片

舒适版:


超人性版:

图片

独立版:


高级版:


友谊版:


高级程序员版:


干净的其他普通版:





一位程序员的暑期办公室:


看完以上这些,

终于到我们大佬的工位啦!


↓↓↓


扎克·伯格:


史蒂夫·乔布斯:


比尔·盖茨:


史蒂夫·鲍尔默:


杰夫·贝佐斯:


马克斯·莱文奇恩:


迈克尔·戴尔:


最后,

大家如果有补充,

欢迎留言呀!

来源:公众号 Python开发 

收起阅读 »

女生最后悔读的专业,在工科

“工科是辍学也能就业的学科”,曾有人这样调侃。然而,对工科女生来说,事情似乎并不是这么简单。泡实验室、写代码、修电路、搭模型……明明上了一样的大学,但在找工作时,工科女生们却只等来一句句“这个岗位不适合女生”“女孩怎么学这个专业啊”。都说“学好数理化,走遍天下...
继续阅读 »

“工科是辍学也能就业的学科”,曾有人这样调侃。
然而,对工科女生来说,事情似乎并不是这么简单。泡实验室、写代码、修电路、搭模型……明明上了一样的大学,但在找工作时,工科女生们却只等来一句句“这个岗位不适合女生”“女孩怎么学这个专业啊”。
都说“学好数理化,走遍天下都不怕”,对女生就恐怕不是这样了。好不容易跨过千军万马的独木桥考上了大学,谁知道这只是第一步。

01 进了大学才发现,学工科好难

高中时代,很多人面临过选择文理科的纠结,家长都会劝学生,最好还是选理科。毕竟选理科更容易考上大学,报志愿选择多,也更容易找工作。
事实上,在中国的千万高考大军中,大多数都是理科生。例如,在高考大省河南,2021 年理科类考生要比文科类考生多了 15 万 [1]。
从本科上线率来看,理科确实要高于文科。 2021 年,各省份理科的本科上线率普遍比文科高 20% - 40% 左右, 安徽、甘肃等省甚至相差 40% 以上。
从理科生与文科生本科上线率来看,只要还有文理分科的地区,这一比值都大于 1,也就是说,理科生更容易上本科,这不是存在于少数省市的个别现象,而是广泛存在于中国大江南北。
pic_4fc2b6ae.png
除了上线率更高,理科生的高考志愿专业选择也更为宽泛。其中, 他们中的很大部分,都去了工科门类。 根据教育部统计数据,工学门类的在校生数要远大于其他学科门类。
2020 年,全国有 1825.74 万本科在校生, 53.70% 都是女生。考虑到基数,就读工科专业的女生并不在少数。
虽然在传统印象中,工科是“男性学科”, 但在现在的理工类院校中,可以看到越来越多的女生身影。
例如,在浙江理工大学,男女新生比例已经基本均衡,在男生人数最多的机械与自动控制学院,男女比例也从 2019 年的 8.9 : 1 下降到了 2021 年 6.79 : 1 [2][3]。
只不过,考上大学或许只是她们人生闯关路的开端。填志愿时的一腔热血,很快就被现实浇上了冷水。
我们统计分析了知乎和豆瓣上有关“女生学工科是种什么体验”的帖子后发现, “跨考”一词的热度最高,相关内容多集中在生化环材、建工交通等传统工科上。
pic_c6b97c2d.png
要不要转专业?要不要考公?要不要转人文社科?一些女生在工科类专业没读多久,就已经开始盘算要脱离工科的苦海了。
后悔,也是她们吐槽的高频词。 后悔的背后,是报考时未曾想到的女生学工科的难处。
实验里动辄要待上一整天,建筑工地上晒大太阳和通宵赶报告是家常便饭,必须承认,工科的很多专业对女生来说是很大的挑战——工科太需要体力了。
还在大学,工科女生就已经感受到了歧视,参加课题、实验或是竞赛会被认为是受男同学或者师兄关照,毕竟“女生就是学不来工科”:

工科女生不是时时刻刻受到周围男生的关照吗(手动狗头)

工科不适合女生并不是说女生学习不行或者动手能力不行,而是就业屡遭歧视,考研究生时导师更偏向招男生。

但实际上,一项发表在《科学》期刊上的研究表明,女性在科学、技术、工程和数学(STEM)学科的能力一直以来都被低估。
在对 30 余个理工科开展大规模调查后发现,阻碍女性进入相关行业的主要原因, 是对她们缺乏这些领域天赋的刻板印象,而非能力差异 [4]。

02 敲得开大学门,却叩不开职场门

学习能力并不比男生差,但在找工作时,她们却遇到了不少“有色眼镜”。
有人吐槽,虽然自己毕业于北京一所老牌 985,成绩也不错,研究所来招人时,带走的却都是成绩不如她的男生的简历。
在投出简历时,男女就已处于不同的赛道,这种现象普遍存在。
根据中国科学院心理研究所的一项研究, 拥有男性化定向名字的女性求职者获得的面试机会较多 [5]。 也就是说,即使简历的性别栏是“女”,只要名字听起来像男生,你也会更容易获得橄榄枝 。
只不过,工科类行业是女生求职被区别对待的重灾区。
报告显示, 在采掘、冶炼、石油石化等工作体力强度较高的行业,男性求职者被招聘者主动沟通的次数是女性的 2 倍以上。
在计算机、交通运输等需要经常加班、出差的行业中,男性被沟通的次数也明显更高。
pic_08502e08.png
与之相反,女性通常需要主动联系招聘者,才有可能获得面试机会。在工程、制造等工科行业中,女性求职者与企业主动沟通次数是男性的 1.04 倍。
比找工作更难的,是工科女找工作。就算顺利进到了面试,用人单位还会明里暗里以各种理由将她们拒之门外。
我们统计了工科女生们在社交平台上分享的求职经历, 生育、体力、加班、出差是她们认为用人单位不青睐她们的主要因素。
根据智联招聘《2020 中国女性职场现状调查报告》,将近六成的女性表示自己曾在应聘过程中被问及生育情况,而男性这个比例仅为 19.59%。
生育对女性的影响,从求职就已开始。 而工科类工作,常常需要加班、出差、驻工地,“女生一毕业就结婚生孩子,拴住了。”有工科女生提及到了应聘单位这样的顾虑。
pic_1a8b415b.png
生育只是一个因素, 很多用人单位不认为女生有足够的体力能跑工地、下车间,能适应经常出差, 在招人时自然倾向于男生。

本人机械女,本科,在制造业工作过5年。工科的就业环境你们真的了解吗?虽然是做技术,到去车间是避免不了的。况且工厂觉得女生下车间不方便,不能加班,人家也不愿意招聘女生。

必须承认,工科的一些工作对体力有着较高要求,也有工科男生发表看法,认为经常在环境肮脏,施工噪音嘈杂的工地熬夜加班,就算是男生也会跑路转行。
只是,男生尚且能有跑路转行的机会,工科女连工作的“号码牌”都排不到。让不少人戏谑道“学得再好,不如你是个男的”。如果说职场是闯关游戏,那工科女们一开始便选择了 hard 模式。

03 兜兜转转,工科女归宿还是老师

就算筛过了简历、通过了面试、找到了工作,工科女们遭遇的性别歧视并没有结束。入职以后,薪酬差异大、机会不平等、晋升天花板……还有一道道难关在等着她们。
某招聘公司统计了 2020 年男女薪酬差异最高的行业, 其中医药、交通、建筑、互联网、化工、机械等理工科行业占据了大半, 大部分行业男女薪酬差异为 30% - 40%。
同工不同酬,正是很多工科女面临的困境。
分二级行业看更为直接,2019 年性别薪资差异最高的行业主要为采掘冶炼、工程施工、装修装饰等工程制造类行业。 在高薪技术岗位,也仅有 6% 左右的女性处于生产、技术总监级别 [6]。
今天,也有一些工科女凭实力在行业内得到了认可,她们动可跑工地、修电脑,静可画图纸、编程序。但挣的钱,却还是不如同行男性。
从面试到进入职场,工科女生要面临的是“九九八十一难”。这些种种都造成了工科女生从事对口行业的比例低,纷纷寻找其他出路。
麦可思的就业报告显示, 中小学教育及培训行业是工科女生们毕业后的第一选择,专业相关度仅 32%。 政府及公共管理的行政工作也名列前矛,专业相关度约 40%。
pic_649ea653.png
确实,有一些工科女读大学后才发现不喜欢自己的专业,要么考研时转专业,要么毕业后转行。
但也仍有许多工科女对专业满怀热爱,我们统计了社交平台上关于女生就读工科等相关内容, 有约 14% 提到了“喜欢”或“热爱”。
很多工科女本希望能进入喜欢的行业发光发热,但却在找工作时处处碰壁。“女生当老师好,稳定”“女孩子就该做安安静静的工作”,诸如此类的话工科女们听了太多。
深深的热爱,却换来了狠狠的伤害:

喜欢是很喜欢自己学的这个专业,大学四年各种奖学金也是拿了不少,但是吧,校招的时候,递简历一看是女的就不收,而有些男同学四级没过都可以,就很受打击。

她们听从家长和社会的规劝,高中时选了“就业前景广”的理科,毕业后,面试难、薪资低、被歧视……一道道难关又迫使她们去了和专业不相关的教育业。
造成工科女就业难的不是天赋、不是能力,而是偏见。即使当初上大学时选择的机会比文科女要多,但最后还是殊途同归,都去当了老师。

来源:网易数读

收起阅读 »

为了脱单,我在交友软件上看尽了奇葩

结语线上交友到底好不好?每个人对这个问题见仁见智,毕竟有些人用它拓宽了交友圈,有些却因为它差点失去了对人类的信心。但对于很多人来说,线上交友都是不可或缺的社交途径,而且此处“社交”不仅限于找对象。不能在软件上找到共度情人节的人,哪怕能找到共同吐槽情人节的人也不...
继续阅读 »

pic_90caa4be.png

pic_9742b690.png

pic_4597a7ce.png

pic_72f15f7c.png

pic_00af29dd.png

pic_f1b8c6fc.png

pic_526d68be.png

pic_b42c5c51.png

pic_26a55a50.png

pic_ae634815.png

pic_63fc9b85.png

pic_42d912cd.png

pic_c39f89dd.png

pic_c7e29d81.png

pic_ddb361aa.png

pic_c611bb78.png

pic_a131c0e5.png

pic_12358bce.png

pic_9153fe02.png

pic_5959a6f5.png

pic_00fb7551.png

结语
线上交友到底好不好?每个人对这个问题见仁见智,毕竟有些人用它拓宽了交友圈,有些却因为它差点失去了对人类的信心。
但对于很多人来说,线上交友都是不可或缺的社交途径,而且此处“社交”不仅限于找对象。不能在软件上找到共度情人节的人,哪怕能找到共同吐槽情人节的人也不赖。
重要的是,我们使用社交软件的时候,做最真实的自己!展示自己漂亮帅气、优雅可爱、自信有特色的外表,但不要照骗;别遮掩风趣认真、善良渊博的内在,同时也别“查户口”式聊天。等到线下面基的那天,也要好好打扮、认真地与对方交流。
“社交圈窄”终是许多人无法脱单的缘由。各位,打开思路,还有不少人在周围的朋友、朋友的朋友、朋友的朋友的同事里面翻出了对象呢。
总之,条条大路通罗马——祝大家有情人终成眷属。
以下是本次调查中,我们发现的一些关于交友的有趣现象:
1、大约三分之二的受访者用过线上交友找对象,其中最多人用的软件是微信。
2、除了约会 APP,人们还会在各种社交 APP,甚至是在音乐类 APP、游戏类 APP 里脱单。
3、至少有三分之一的受访者认为,用约会软件也不是只奔着恋爱去的。
4、约会软件上,大约三分之二的受访者会主要通过照片和档案筛选聊天对象。
5、将近七成的受访者会因为“聊天无趣”而放弃网络情谊。
6、线下见面时,男性更在乎对方是否照骗,而女性更在乎对方聊天有没有意思。
7、近一半使用过线上交友的女性遇到过图色的网友,超过三成的男性遇到过图钱的网友。
8、超七成的受访者会因网友质量太差而抛弃一款交友 APP。
9、超过一半的人是在同学、同事、朋友等熟人圈里发展出了对象。
10、不管是线上还是线下找对象,八成的受访者认为最重要的是“让人舒服的聊天”。

数据说明 本次调查收到了很多读者的反馈,感谢所有参与调查的读者朋友们,大家的留言我们都有读过。
针对本次的样本数据,我们做一些简单说明:
本次共回收有效问卷数量 6862 份,其中男生占比 32.5%,女生占比 67.5%;00 后占比 25.1%,95 后占比 42.5%,90 后占比 21.0%,85 后占比 8.0%。
样本中,目前脱单的占比 34.4%;谈过恋爱但目前单身的占比 32.9%,目前从未谈过恋爱的占比 32.7%。
另外,来自一线城市的受访者占比 45.4%,来自二线城市的占比 28.7%,来自三线城市的占比 14.3%,来自四线城市及以下的占比 11.7%。

来源:网易数读

收起阅读 »

麻辣烫都要上市了,我呆了。

近日,中国证监会的官网公布了杨国福麻辣烫的申请,如果获得受理,杨国福就有机会去香港上市,成为「麻辣烫第一股」。几乎在同一时间,绝味食品宣布旗下餐饮品牌和府捞面和中式快餐乡村基宣布将到境外上市;老乡鸡、西贝等餐饮企业,也传出了上市的消息。中国连锁餐饮耕耘多年,但...
继续阅读 »

pic_290bd818.png

近日,中国证监会的官网公布了杨国福麻辣烫的申请,如果获得受理,杨国福就有机会去香港上市,成为「麻辣烫第一股」。
几乎在同一时间,绝味食品宣布旗下餐饮品牌和府捞面和中式快餐乡村基宣布将到境外上市;老乡鸡、西贝等餐饮企业,也传出了上市的消息。
中国连锁餐饮耕耘多年,但是一直不温不火,为什么今年就爆发了呢?相信很多人都有这样的疑惑,我也去研究了大量的数据报告,探索研究了其中的原因,下面就是我看了相关报告和数据以后得出的观点。
也欢迎大家一起来讨论。
首先企业上市的目的是为了从资本市场获得更多资金来助力业务发展,但是想要保住股价,一个健康的商业模式是必须具备的。乡村基就曾因为扩张太快导致利润下跌,最后以退市收场,这次要上市的企业肯定不会忽视这一点。
而宣布上市的这三家企业有一个共同的特征,就是在近两年高速扩张的同时,依然保持了盈利。
乡村基旗下乡村基餐厅和大米先生两个品牌,由截至2019年1月1日的638家增至截至2021年9月30日的1145家,增加了507家店,增长率达到79.5%。并且在扩张的同时,乡村基餐厅的营业利润率分别为10.7%、9.2%和12.6%;大米先生餐厅的营业利润率分别为6.5%、3.3%和10.4%。
和府捞面则是连续多年保持了50%以上的营收增长,截至2021年6月底,和府捞面全国门店总数超340家。其中,2021年新增门店数较2020年翻番,全国约2天新开一家店。而且2021年上半年,和府捞面的营收为8.46亿元,并实现了1385万元的净利润,和2020全年2.15亿的亏损相比,目前单店营业额已全面恢复至疫情前水平。
杨国福2017年门店数量突破5000家,又在2020年突破6000,考虑到庞大的基数,这个数字已经非常惊人。并且加盟+供应链的营收模式,使杨国福只要扩张速度够快,营收就会相应增长。
这说明三家企业各自摸索出了成熟的模式,具备可快速复制并且盈利的能力,因此想要借助资本市场帮助自己完成扩张。
但是这依然解释不了他们2022集中上市,拿和府捞面为例,在其一开始就选择了中央工厂+门店的模式,早就具备复制能力,并且21年刚刚获得巨额融资,为什么2022年初就急匆匆地上市。
pic_7ebe1fff.png
这就不得不提疫情的影响。我们都知道疫情对餐饮行业的打击是致命的,但是疫情之后,给成熟的连锁餐饮带来的却是利好。

复工这几天我有个明显的感觉,就是公司楼下的小餐馆生意更好了,原本没几个人的馆子现在都座无虚席。这是因为打工人都回来上班了,但是有些餐馆老板还在过年。在消费需求没变的情况下,供应少了,那么剩下的餐馆生意自然就好了。

疫情也是同理。原本每个地区都有各式各样的小馆子,有些还是多年老店,周边顾客已经养成了消费习惯,而这些小馆子大多是个体经营。
但是疫情一来,这些个体餐馆几个月没法营业或者收入暴跌,很有可能就撑不住而倒闭。
复工后,这些餐馆不在了,可是消费者还是要吃饭,这个空窗期就是机遇。这是因为大品牌的食品安全更受消费者信任,在疫情以后订单量受影响相对较小,恢复更快。并且在租金上,大品牌的议价能力更强,恢复期开店反而能获得更多优惠。
乡村基等多个连锁品牌就是利用这种思路,把疫情恢复期当做抄底的好机会,借助投资完成了逆势扩张。
所以在整体上,疫情给餐饮行业带来了巨大的冲击,用了两年时间都没完全恢复。但是在局部,反而因为大洗牌造就了连锁餐饮的繁荣。
这种情况下,谁能早一步抢占市场,就能在后面的竞争里占据主动。并且这个空窗期很短,目前餐饮行业已经处在恢复期的尾声,餐饮企业想要占位,必须借助更大的杠杆。这么一看,上市似乎是最好的办法。
当然了,疫情并不是对所有连锁餐饮都是利好,如果没有完备的后端体系和充足的现金,疫情的打击依然是致命的,但是疫情确实给部分企业带来了机遇。
意识到这一点的资本也纷纷入场,给餐饮企业上市增加了助力。据有关统计数据显示,截至2021年8月,我国餐饮业投融资金额为439.11亿元,已经达到了2020年的2倍,大大小小的细分领域都拿到了融资,甚至出现了拉面馆单店估值千万的怪现象,这让今年餐饮企业的上市变得顺理成章。
除此以外,疫情带来的冲击也让餐饮老板的观念发生变化,越来越多餐饮老板拥抱资本市场。
四年前接受采访时,杨国福曾坦言和上市相比,餐饮还是自己做一把手更踏实。
现在来看,真香。

作者:墩墩 来源:路人甲TM(ID:smcode2016)

收起阅读 »

中国男足的收入是女足的N倍,职场男女收入差距在哪?

中国女足亚洲杯夺冠,举国振奋欢呼,女足姑娘们用两场荡气回肠的逆转胜,连克日本韩国两大亚洲劲敌,第9次登上亚洲之巅,为这个被大年初一男同胞们搞臭的“国足”称号,大大挣回了脸面。赢球了,不能总是虚的,必须来点实际的。有消息称,中国足协此次将重奖女足,奖金总额超过1...
继续阅读 »

中国女足亚洲杯夺冠,举国振奋欢呼,女足姑娘们用两场荡气回肠的逆转胜,连克日本韩国两大亚洲劲敌,第9次登上亚洲之巅,为这个被大年初一男同胞们搞臭的“国足”称号,大大挣回了脸面。

pic_fd442c71.png

赢球了,不能总是虚的,必须来点实际的。

有消息称,中国足协此次将重奖女足,奖金总额超过1000万元。男足在世界杯预选赛上的赢球奖金是一场600万元,女足拿了亚洲冠军,奖金超千万完全配得上。

pic_28919314.png

就连知名足球解说员黄健翔也呼吁:“请按照男足奖金标准双倍给女足发奖金!”

pic_343c00ec.png

除了大赛奖金,姑娘们平时的收入待遇问题,也被网友们热议:相对男足动辄近千万的年薪,中超女足队员最高不到百万……

后来又有媒体出来说了,如果进行横向比较,中国女足队员的收入自2018年以来已是世界第一。

所以在全球范围内,男足女足同工不同酬,绝对是普遍现象。

一般的解释是,这是由于男足运动背后巨大的商业价值所决定的,短时间很难改变。

且不去论证其中的合理性,我们回到普通的就业职场,男性和女性在收入差距上又是怎么样的状况呢?这背后有哪些深刻的原因呢?

pic_7e57afbb.png

pic_c02436f0.png

“男女同工不同酬”是个假议题?

从整个职业生涯来看,女性赚得比男性少,目前来看,不管在哪个国家哪个时代都是如此。女性的平均收入是男性的多少?

在美国这个数字是81%;在日本是73%;在丹麦这样的北欧国家,也只有85%。

放眼世界,你找不到一个男女平均收入差不多的国家。

有一种解释是,女性薪酬低于男性,除了管理层占比偏低,还在于女性主要从事的岗位普遍缺少高薪属性。

比如女性主要分布在财务、行政、人力资源和销售岗,现在普遍高薪的技术、产品岗位上男性的占比优势明显,这也影响女性的平均薪酬水平。

根据人才咨询服务公司“Korn Ferry”针对欧洲870万员工的一份调查显示:

虽然以平均值来说,女性薪资确实比男性少了18%,但若以在相同职位从事相同工作的男女做比较,薪资差距就下降到7%。

若再进一步以同公司的同职位来比较,差距则又会缩小到只剩2.6%,若是连工作内容也相同,则差距只剩不到2%。

这样看上去,同样公司的同样职位上来看,基本上实现了男女同工同酬。

那为什么男女收入整体来说还存在比较大的差距呢?

我们来看看这个现象,那就是男女薪酬差别,在刚入职场的时候比较小。

据史丹福大学经济学教授Claudia Goldin的数据统计,刚毕业的大学生,女性平均薪资是男性的92%,但到了四十多岁,降到了73%。

pic_6f1e15a8.png

类似的研究调查MBA毕业生,刚毕业时,女性约为男性薪酬的92%,但十年后,数字狂降到57%。

她的研究指出,的确在雇用和职场环境上,对男女有不平等待遇,这不平等待遇也可能造成男女薪酬不同。但许多工作对时间要求的不同,可以解释绝大多数的薪酬不同。

也就是说,歧视也许有,但男女对工作能够投入的时间,却是主要男女薪酬不同的原因。

为什么男女对工作投入的时间差别会随着年龄增长,越来越大呢?

其实这并不难解释,那就是女性为了承担家庭责任比如育儿(无偿劳动),主动或被动地选择了灵活性更高,但工资更低的工作。

在传统家庭分工中,女性承担了更多的家庭责任,这导致工作时间更短并且不连续,此她们的工作经验将短于男性,而这会降低她们的相对工资。

pic_0844cf66.png

女性的职业惩罚

女性收入不如男性,主要是生育带来的后果。

这个研究是由普林斯顿大学的经济学家亨里克·克莱文(Henrik Kleven)完成的。

他使用的数据来自全世界社会福利最好的国家之一:丹麦。

丹麦为新生儿童的父母提供一整年的带薪假期。政府为3岁以下的儿童提供公立的婴幼儿照护服务。

克莱文发现,女性的收入在第一个孩子出生以后出现了断崖式的下跌,而男性却没有类似的薪酬下降。

这种下跌带来的影响是深远的:在职业生涯中,女性最终会比男性少20%的收入。

研究估计,丹麦男女收入差距的80%是生育造成的 。

越来越多类似的研究表明,我们通常所说的“男女收入差距”,准确来说应该是“生育收入差距”或“做母亲的代价”。

相比于经历了收入断崖下跌的母亲们,没有孩子的女性和男性的收入相似。有孩子的女性的薪酬在生育期急速下跌,之后缓慢增长;

而没孩子的女性薪酬却持续增长,最终两者间出现一定的收入差距。

这显示出生儿育女对女性薪酬发展的巨大影响。

在中国,女性接受高等教育的比例已经超过男性。

受教育程度和收入是成正比的,所以教育因素对收入的影响是在逐渐消失。

但女性生育的“职业惩罚”,在对女性收入的影响上是顽固的,没有减缓迹象。

职场不存在绝对公平,但一个不可否认的事实是,在中国,男女职场人遭遇的不公平待遇呈现差异化。

对于女性来讲,“应聘过程中被问及婚姻生育状况”现象最为普遍,一半以上的女性都有遇到。而男性,被问到这个问题的机率非常小。

男性在谈到晋升障碍时,多归结于个人能力、上级领导、同级竞争、公司制度等与职场相关度高的原因。

而女性更担忧的是“照顾家庭,职场经历分散”、“处在婚育阶段,被动失去晋升”、“性别歧视”。

这也再度反映了性别、婚育计划等与工作能力无关的要素,却很大程度上影响女性职场价值,成为牵制女性职业发展的“玻璃天花板”。

尽管工作时间与男性和非父母职工相当,职场妈妈群体的平均收入,也会比生育前少挣12.5%。

部分女性群体还存在生育后就业几率变小的问题。尤其是工资水平较高和高级职位的女性,可能会受到更严重的惩罚,因为她们生育后的月收入增长最慢。

其中也有特例,比如我朋友A在喂母乳到孩子一岁之后,重新找了工作,之后几年经历了收入和职位的节节上升。

她说,“我得特别感谢我老公,实际上来讲我们俩有点像打配合的感觉,说在过去这几年里,尤其是说我生了老二之后,其实他帮了我担负了很多。

pic_a0675e3c.png

因为这几年是我爬坡的过程,他自己开公司,他会比较相对来讲有自由度,但是我是没有的,所以他很大程度上能帮我去看孩子。

所以我们俩的配合通常是说他是带孩子的,比方说周日啊出去,都是我老公,我呆在家里,可能我要加班,也会收拾屋子买菜做饭。”

还有一些女性从原生家庭获得了很多育儿和家务支持。她们在育儿方面的投入增加,但工作强度不变或者增多,休闲时间相应减少。

例如,许多女性因为二宝出生后的经济压力,变得更加努力工作来提升职位和收入。

兰卡斯特的副教授胡扬在2016年出版的书中提到,这一代中国年轻女性经历体现了生命历程的结构性不一致(life-coursestructural inconsistency)。

在校期间,她们经历去性别化的教育竞争。她们被要求在应试教育中取得优异成绩,考入名牌大学,找到一份好工作。

pic_2d6cd1c0.png

在这个阶段,家长、老师和社会对她们的主要期待是去性别化的。

然而,在结婚生孩子后,社会期待女性以家庭为中心,期待女性在家庭中付出更多时间和精力,默认家庭内男女角色不同。

这些期待促使职业女性接受了性别化的角色。因此,她们为了家庭调整了自己的职业发展轨迹,经历了性别的“再社会化”,接纳与遵循了传统的社会性别分工。

如何应对这个结构性的问题,是值得整个社会和每个受影响的个体去思考的。

参考文献:

[1] https://focus.kornferry.com/wp-content/uploads/2015/02/KF-Gender_Pay_Gap-%E6%B6%88%E9%99%A4%E6%80%A7%E5%88%AB%E8%96%AA%E5%B7%AE.pdf

[2] Hu, Y. (2016). Chinese-British Intermarriage:Disentangling Gender and Ethnicity, London: Palgrave Macmillan.

来源:https://mp.weixin.qq.com/s/htCOWCywWBXOwGSSIDiTnw

收起阅读 »

科学研究:女性比男性更优秀?

导读:新女性时代要来临了吗?01 未来是女性的尽管目前男性在中高层的管理者当中依然占据着绝大多数,但10年后,伴随着自动化的发展,他们将逐渐失落。为什么?因为,未来发展的方向很明显是“零工经济”:自由职业者和“小微创业者”为了一个项目组成临时的团队,完成眼前的...
继续阅读 »
导读:新女性时代要来临了吗?

pic_48ebd6eb.png

01 未来是女性的
尽管目前男性在中高层的管理者当中依然占据着绝大多数,但10年后,伴随着自动化的发展,他们将逐渐失落。
为什么?
因为,未来发展的方向很明显是“零工经济”:自由职业者和“小微创业者”为了一个项目组成临时的团队,完成眼前的任务;透明度、信任以及有效的调控由技术负责;项目本身则是新的主管。
在这一模式下,经理基本已无存在的必要。
当然,如此一来,更紧要的将是兼具责任心和能力的领导层,这一特质在女性那里要常见得多。
高校里遍布着受到良好教育的年轻女性,在西方大学的许多未来性专业中,女性学生如今已占多数。

02 大脑研究:为什么女性更具领导力?
圣雄甘地一早就断言:

指称女性为弱势性别是一种污名化,是男性对女性的不公正对待。若从血腥暴力的角度去理解,女性自然逊于男性。
然而,若从道德力的角度去理解,女性的优越程度则难以计量。她们有更强烈的直觉、更伟大的牺牲精神,她们更坚忍也更勇敢,不是吗?没有她们,男性什么都不是。如果非暴力是我们实然状态的法则,那么未来将属于女性。

美国精神病科专家、知名医生丹尼尔·亚蒙在其著作《女性脑》中从神经学的角度说明了,就当今世界的需求而言,女性构造具有怎样的优势。
亚蒙列举了女性明显胜任领导者职位的五大长处:
同理心、合作、直觉、自制和责任心。
为什么女性的大脑在这些领域中的表现会比男性的大脑亮眼得多?
pic_0477a9d3.png
原因在于激素储备的性别差异。女性的大脑早在子宫里便沉浸在女性的性激素——雌激素当中,相反男性的大脑是被男性的性激素睾酮所包裹的。
当下的研究结果显示:脑前额叶或前额叶皮质的发育会受到雌激素的强烈刺激。因此女性的这一结构普遍比男性大,比男性成熟得早。
人类大脑这一最新进化出的成分专门司职我们的认知和决策。
这两个素质对企业行为和一般领导力而言至关重要,尤其是在当今这个越发复杂且变化越发迅速的社会中。
右前额叶尤其与前瞻性思考相关,这是一种相当重要的关乎项目调控的预见力。
所有女性通常会比男性更积极地寻求提早完成课业,无论是在中小学、高校,还是在工作场合中。
男性因睾酮之故更具攻击性和性冲动。一般来说,他们的体格比女性强壮,然而就21世纪社会所需的技能而言,显然他们才处于弱势。
男性可以练就更大块的肌肉,可以更威猛地引发对峙和紧张关系。就在时间压力下完成一项任务而言,他们也比女性要强一些,因为他们喜欢由压力产生动力:时间越紧迫,男性机体中多巴胺和降肾上腺素这类神经递质会分泌得越多。
相反,女性在无聊的工作中也基本能保持积极性。如果一项工作可以被提早完成,女性机体中的应激激素水平就会下降。
大脑研究者也曾宣称,女性在棘手的情境下更容易保持冷静的头脑。众所周知的杏仁核在女性大脑里所占区域明显比男性要小。
大脑里这一古老的组织负责直觉性的行为模式,如攻击和盛怒。
在战斗或逃跑模式下,我们的大脑会从前额叶主导切换至杏仁核主导:我们在短时间内将自己变为原始时代的猎人(或是被其追捕的逃亡中的猎物)。
男性的杏仁核不仅比女性的大,它还拥有大量的雄性激素接收装置,一旦男性激素被大量释放,它就会异常活跃。
相对而言,即便为棘手状况所困,女性大脑里的前额叶皮质,即理性之“我”的所在,依然能战胜大脑中主管攻击和恐慌机制的古老区域,保持控制。
女性大脑里的保险装置不易熔断也和前扣带皮层有关。主导抑制冲动作用的边缘系统的这一部分在女性大脑中的分布区域明显大于男性。
研究者认为,这一人体结构上的差异至少部分解释了女性相较男性而言对低风险的偏好。
较小的杏仁核与较大的前扣带皮层这一对组合使女性在较大的压力下也能控制其情感,寻求最优的解决方式。
pic_48ab6bec.png

03 女性VS男性:我们如何使用大脑?
此外,不同的“布局”也很重要。男性的大脑拥有更多的神经元,但是女性的大脑半球之间显示出更强的关联性。
就典型性而言,男性偏爱使用主管理智、逻辑和模式识别的左脑,因此他们更擅长将注意力集中在具体目标上并系统性地着手完成任务。他们可以实现自我激励,但倾向于机械性地、超脱于周边环境来做出反应。
相反,女性更偏爱使用右脑。右脑可以帮助其良好地共情、与他人建立联系、塑造和获得社会关联,以及创造性地寻找到答案。
此外,她们“跨半球”运用大脑的能力也大大优于男性,这是一种同时使用两个大脑半球的能力。
不止于此,就连女性大脑里的岛叶皮质也比男性的大。那是直觉,俗称“第六感”的所在。岛叶皮质主管共情能力、情绪自觉以及语言传递性思维。
因而女性(同其他雌性灵长类动物一样)比男性拥有更强的沟通能力,可以更轻易地辨别面孔和更充分地表达情感。
此外,她们解读他人情感和深意的能力更强,对此男性常常不能在第一时间予以充分关注。
而且,女性通常比男性的记忆力更佳。
她们的海马体,这一主管记忆的结构也比男性的更大、更活跃。也正因为如此,她们的学习能力和长久储存所学知识的能力更强,尤其是她们的听觉皮层(负责将所记和所学转化为语言的生理构造)比男性的大。
简而言之(且以“典型的男性角度”):就21世纪的领导性岗位而言,女性的资质相当卓越。
出于大脑构造的缘故,她们在共情、团队合作和自控方面明显要比男性强。
所有的一切都表明,在量子经济下,她们将占据领导性岗位当中的相当份额。
至少可以确定的是:就我们的经济和社会的中央枢纽而言,我们需要大量拥有女性大脑的人。

作者:安德斯·因赛特 来源:身边的经济学(ID:jjchangshi)  

收起阅读 »

大洗牌!2021年,全国TOP50城市GDP排行榜

导读:中国城市,正在迎来新一轮大洗牌。产业格局、人口形势、国际局势变迁,疫情、洪涝、能耗双控等因素影响……全国城市经济格局发生了巨大变化。谁进谁退?01 50强城市,谁是守门员?这是2021年内地GDP50强城市排行榜:中国城市第一梯队的竞争,已从最初的万亿俱...
继续阅读 »
导读:中国城市,正在迎来新一轮大洗牌。

产业格局、人口形势、国际局势变迁,疫情、洪涝、能耗双控等因素影响……全国城市经济格局发生了巨大变化。
谁进谁退?

01 50强城市,谁是守门员?

这是2021年内地GDP50强城市排行榜:
pic_e67a993c.png
中国城市第一梯队的竞争,已从最初的万亿俱乐部,向2万亿乃至3万亿俱乐部进军。
同时,内地万亿GDP城市已经扩容到24个,南方18个, 北方6个。
TOP50城市的门槛也提高到了5000亿元以上,太原是“守门员”,还有部分省会未能晋级50强之列。
具体来看,京沪联袂突破4万亿,成为我国仅有的2个4万亿大市。
作为地位最为超然的两大一线城市,一个是政治和文化中心,一个是经济和金融中心,未来综合实力仍将遥遥领先。
京沪之后,同为一线城市的广深,都在向3万亿进军。
虽然与京沪之间的差距有所拉大,但广深都只是副省级城市,广州更是受制于三级财政,无论行政级别还是城市能级,抑或政策力度都难与京沪匹敌,更多依靠自力更生,能取得如此成绩殊为不易。
在2万亿俱乐部中,广州、重庆、苏州遥遥领先。2022年,广州、重庆突破3万亿问题不大,而苏州未来5年同样不乏挑战3万亿的可能。
pic_97d74350.png
事实上,在TOP10城市里,成都离2万亿只有一步之遥,而杭州、武汉等地突破2万亿也只是一两年的时间问题。
同时,万亿俱乐部成员,再次迎来扩容,但2021年仅有东莞一城晋级。目前,全国已有24个万亿城市,18个位于南方,6个位于北方。
当然,随着万亿城市大扩容,万亿GDP的含金量也在急速下降,万亿将只是城市竞争的起点,而非终点。

02 大城博弈:谁晋级了?
城市经济,可谓你追我赶,不进则退,没有谁能永葆强势。
过去一年,哪些城市守住了经济优势地位?哪些城市受到了挑战?
其一,济南、合肥、福州、东莞、唐山、沈阳、南昌、太原等省会或经济强市,实现了晋级。
太原以1亿元左右的微弱优势,超过南宁,晋级50强城市,成为TOP50城市的“守门员”。
南宁、贵阳、乌鲁木齐、兰州、呼和浩特等省会城市止步于50强之外。
其二,广州稳超重庆,守住了GDP第四城之位,且拉近了与深圳之间的距离。
2021年,广州GDP为2.82万亿元,重庆为2.78万亿,广州对重庆的领先优势扩大到300亿元以上。
同时,广深之间的差距,从前几年高峰时期的4000亿元以上,缩小到2400亿元左右。
pic_94049bb6.png
正如《全国GDP第四城之争,再无悬念》一文的分析,这背后得益于广州完成了产业转型,借助新一代信息技术、生物医药、新能源等新动能,以及城市更新的助力,经济发展重回高增长轨道。
其三,天津仍然止步于十强之外。
自2020年南京取代天津成为内地GDP第十城之后,天津经济何去何从就成了舆论关注焦点,十强城市只剩下北京一个北方城市之类的说法不绝于耳,这在凯风新书《中国城市大趋势》中有详细论述。
不过,2021年,天津离TOP10仍有一步之遥。2021年,天津GDP1.57万亿,离位居第十名的南京仍有600多亿的差距。
pic_75e94abc.png
继2020年南京首次超越天津,晋级十强城市之后,2021年天津仍未能反超南京,与南京的差距约为600亿左右。

03 谁是最大“黑马”?
2021年,城市经济涌现出一批“黑马”,经济实力实现大幅跃升。
在TOP50城市中,进步最大的城市有4个:陕西榆林、山西太原、山东潍坊、福建福州。
其中,福州借助强省会优势重回省内经济第一大市之位,GDP连超泉州、南通和西安,跻身TOP20城市之列。
而山东潍坊,则反超石家庄、绍兴、盐城等城市,排名大幅提升。
潍坊的晋级,一方面得益于工业增长强劲,工业增速超过12%;另一方面则来自投资的贡献,固定投资增速超过16%,其中基建投资超过33%。
潍坊曾喊出“咬定地区生产总值过万亿、冲刺全国大中城市综合实力前30强、加速迈入国内二线城市行列”的口号。
如果能保持高速增长,这一目标未必没有实现的可能。
相比而言,榆林、太原更为突出。
2021年,榆林、太原GDP名义增速分别高达32.9%、23.3%,城市排名分别跃升11位、6位,两城更是得以晋级50强之列。
pic_b3d2de23.png
榆林和太原大幅晋级,得益于资源价格大涨。
榆林是著名的煤炭大城,而太原所在的山西则是全国产煤第一大省。
过去1年,在全球供应链紧张、通胀高企以及能耗双控影响之下,煤炭产能扩张,而价格大幅上涨,由此带动资源型城市经济总量的罕见大增长。
pic_79a2bed5.png
这其中,榆林最为典型。2021年,榆林GDP达5435.18亿元,不仅超过了邻省省会太原,而且还超过了襄阳、宜昌,直逼长期领跑中西部非省会城市的洛阳。
煤炭价格大涨,对榆林经济的贡献有多大?
数据显示,2021年榆林全市规上工业产值首次突破7000亿元大关,比上年增长55.9%。其中,能源工业产值增长61.4%;化工行业产值增长54.3%。
pic_22fdc795.png
当然,未来能源行业,还会面临美联储加息、全球供应链恢复常态、双碳战略等影响,能否继续保持高速增长,值得关注。

04 强省会的逆袭
2021年,强省会可谓光芒四射。
这一年,不仅成都、武汉等传统强省会实现了再突破,一些向来“弱势”的省会经济实力也迈上新台阶。

前不久,福建福州、江西南昌、广西南宁、贵州贵阳、河北石家庄、山西太原不约而同将“强省会提上日程”,有地方甚至喊出了“省会强则全省强,省会兴则全省兴”的口号。
过去一年,多个省会经济实力提升:
沈阳逼近30强城市,重返东北第一大省会之位 福州跃居20强城市之列, 太原跻身50强城市, 济南、合肥、南昌等省会排名也有明显提升。
这其中,最典型的当属福州,不仅跃居20强城市,还重回福建第一大市之位。
在福建省,长期以来都是“三城鼎立”的格局:福州为省会,泉州为经济强市,厦门为经济特区、计划单列市和副省级城市。
2021年,福州GDP达11324.48亿元,而泉州为11304.17亿元,厦门为7033.9亿元。
这意味着,福州以20亿的领先优势,时隔20多年,重回福建经济第一大市。
pic_25c08bad.png
这背后,不无强省会战略的助力。
去年,福建出台《关于支持福州实施强省会战略的若干意见》,支持福州创建国家中心城市,大力支持福州做大做强,增强省会城市辐射带动力。
pic_1fe279aa.png
这意味着,福州不仅要做强省会,而且还要竞夺国家中心城市。要知道,同属一省的厦门,也曾提出创建国家中心城市的想法。
虽然福州重回福建第一大市之位,但福州与厦门、泉州的良性竞争还会持续。

05 东北,仍然只有一个TOP30城市
东北共有4大中心城市:大连、沈阳、长春、哈尔滨。
这4个城市全部位列副省级城市,占全国副省级城市(共15个)的四分之一强。
据分析,东北4市之所以能全员晋级,是因为在副省级城市设立的1990年代,东北经济还在全国位居前列。
后来副省级城市再未进行任何扩容,这也导致郑州、长沙、合肥等万亿强市仍然只是普通省会。
不过,近年来,东北经济强市被其他省会陆续赶超。加上前几年经济普查,原本已经突破7000亿的多个东北地市,GDP遭遇挤水分,又回到了重新攀上7000亿的历程。
2020年之后,东北各地发展均步入正轨。
pic_a170e9d3.png
从2021年经济数据来看,大连以7825.9亿元位居东北第一,在全国排名第29名。
沈阳GDP达7249亿元,反超昆明、长春,位列31名,重回东北第一大省会之位。
长春GDP为7000亿元左右,位列30-40名之间,哈尔滨GDP为5351亿元,位列50名之内。
未来几年,东北有望诞生第一个万亿城市。

06 谁是下一个万亿城市?
目前,内地共有24个万亿GDP城市,其中南方18个,北方6个。
广东独占4席,与江苏并列第一,广州、深圳、佛山、东莞全部破万亿。
北方的6个万亿城市分别是:北京、天津、青岛、郑州、济南、西安。
pic_d958d245.png
那么,谁是下一个万亿城市?
可以看到,东莞晋级之后,9000亿量级城市存在明显断层,这意味着2022年或许将是没有新晋万亿城市的一年。(参阅《又一个万亿GDP城市诞生》)
目前,8000亿量级共有4个城市:江苏常州、山东烟台、河北唐山、江苏徐州。
这些城市快则未来2年、慢则2025年之前,都有望跻身万亿城市之列。
而在7000亿量级,则有大连、温州、昆明、沈阳、潍坊等众多城市,这些地方未来5年左右也大概率会有万亿城市诞生,而东北地区也有望实现零万亿城市的突破。
当然,随着中国经济总量超过美国3/4,省域经济最高已经攀升到12万亿以上,市域经济最高也超过4万亿,万亿城市的含金量将与以往有着明显不同。
届时,2万亿城市,或将是新的起点。

作者:凯风 来源:国民经略(ID:guominjinglve) 收起阅读 »

什么是元宇宙、新基建、赛博空间?7个最火科技名词解释,都在这里了

导读:人们从学术、科幻、政府、产业等角度对数字未来有一系列设想,在过去、现在与未来,这些设想引导我们去探索与创造。这里做简要梳理供你参考。01 地球村(Global Village)这是媒介学者麦克卢汉提出的理论,在他1964年的著作《理解媒介:论人的延伸》中...
继续阅读 »

导读:人们从学术、科幻、政府、产业等角度对数字未来有一系列设想,在过去、现在与未来,这些设想引导我们去探索与创造。这里做简要梳理供你参考。

pic_f9843244.png

01 地球村(Global Village)

这是媒介学者麦克卢汉提出的理论,在他1964年的著作《理解媒介:论人的延伸》中提出。这个词形象地告诉我们,信息技术的发展缩短了地球上的时空距离,整个地球像一个小小村落。

02 赛博空间(Cyberspace)

它由科幻小说作家威廉·吉布森在1982年的小说《全息玫瑰碎片》中提出,指计算机以及计算机网络里的虚拟现实。它还演化出了“赛博朋克”等概念,对科幻小说与电影的影响巨大。机器与人的混合体“赛博格”(Cyborg)与它有着同样的渊源——控制论(cybernetics)。

两年后,在小说《神经漫游者》中,吉布森让赛博空间更加具象,主人公凯斯让自己的神经系统挂上全球计算机网络,他使用各种匪夷所思的人工智能与软件为自己服务。赛博空间原指与工业化实体空间截然不同的新空间,后来逐渐被等同于网络空间或数字空间。

03 数字化生存(Digital Being) 数字化生活(Digital Living)

它于1996年由尼葛洛庞帝在开启数字化未来的畅销书《数字化生存》中提出,他当时是美国麻省理工学院(MIT)的未来科技研究机构媒体实验室主任。

数字化生存指的是,人们从原子世界的生存演进到比特世界的生存。他展示的众多数字化生活的设想,后来大多变成了现实。在过去30年,互联网产业发展外溢形成数字经济、数字社会,人类的数字化生存与生活逐渐成为现实。

pic_4ac289b4.png

04 信息高速公路(Information Highway) 中国“新基建”(China New Infrastructure)

我们可以看到中美两国的相关政策举措虽时隔近30年,但遥相呼应。1992年,时任参议员、后曾任美国副总统的戈尔倡导建立“国家信息基础设施”,并形象地命名为“信息高速公路”。

2020年,中国的相关政策强调加快5G网络、数据中心等新型基础设施的建设进度。一般认为,新基建包括5G、特高压、城际高速铁路和城际轨道交通、新能源汽车充电桩、大数据中心、人工智能、工业互联网、物联网等领域,其中主要为与数字技术相关的基础设施。

05 互联网公司(Dot.Com & Internet Company) 数字经济(Digital Economy)

互联网公司最初被称为Dot.Com,后来逐渐地形成了包括多个细分领域(如内容、社交、电商)的互联网大产业。自20世纪90年代初互联网商业化以来,互联网产业以自身的方式演化与发展——从PC互联网到移动互联网,从线上到线下。

近年来,互联网的关注重点从应用为主(新闻、社交、电商、游戏、打车等),转向技术主导(大数据、机器学习、芯片设计与制造、虚拟增强现实、区块链等)。现在人们通常认为,互联网公司的典型形态是连接供需双方的互联网平台。

唐·塔普斯科特被认为在1995年出版的《数字经济》一书中首次提出了“数字经济”。后来马化腾、孟昭莉等著的《数字经济》中提到人类社会、网络世界与物理世界的融合,这三者融合形成的正是现在我们所说的数字经济,这一观点的特点是将人类社会中的社交关系纳入了数字经济之中。

pic_22e73816.png

06 全球大脑(Global Brain)

近年来,人工智能在数据、算法、算力的三重刺激下重新爆发。人们看到,互联网在大数据与人工智能的支持下成了人类整体的“全球大脑”。全球大脑不是全新概念,凯文·凯利在《必然》一书中有一种形象的描述,既呼应了前人的观点,又结合了新变化:

真正的人工智能不太可能诞生在独立的超级电脑上,它会出现在网络这个由数十亿电脑芯片组成的超级组织中……任何与这个网络人工智能的接触都是对其智能的分享和贡献。这种人工智能连接了70亿人的大脑、数万兆联网的晶体管、数百艾字节的现实生活数据,以及整个文明的自我修正反馈循环。

07 元宇宙(Metaverse) 第三代互联网(Web 3.0)

元宇宙这个概念由科幻小说家尼尔·斯蒂芬森在其1992年的小说《雪崩》中提出,主人公戴上接入网络的虚拟现实头盔,就可以生活在由电脑与网络构成的虚拟空间。这本书对虚拟现实和游戏的发展影响巨大。最终在21世纪第三个10年,在技术与产业成熟之后,元宇宙成为数字化未来设想的代名词。

我们将元宇宙视为实体世界与数字世界融合的新世界,称之为第三代互联网(Web 3.0)(相关阅读:为什么Web 3.0就是元宇宙?),并将它细分为立体互联网与价值互联网。

作者:方军 来源:大数据DT(ID:hzdashuju)  

收起阅读 »

浙江出招:大学生如果创业失败,贷款10万以下的由政府代偿

2月17日上午,国家发展改革委举行新闻发布会,介绍支持浙江省高质量发展建设共同富裕示范区推进情况。浙江省人力资源和社会保障厅副厅长陈中在答记者问中介绍,为鼓励大学生创业, 浙江大学生如果创业失败,贷款10万以下的可由政府代偿。陈中表示,高校毕业生是宝...
继续阅读 »

2月17日上午,国家发展改革委举行新闻发布会,介绍支持浙江省高质量发展建设共同富裕示范区推进情况。

浙江省人力资源和社会保障厅副厅长陈中在答记者问中介绍,为鼓励大学生创业, 浙江大学生如果创业失败,贷款10万以下的可由政府代偿。

陈中表示,高校毕业生是宝贵的人才资源,浙江始终坚持把高校毕业生就业工作当作人才工作来抓,从来没有把他们当作包袱、压力、负担,而是把他们作为优质的资源来配置、引进、使用和储备。今年,全国高校毕业生超过1000万,对浙江来说是一个很好的机遇,我们要抓住这个机遇,大力引进高校毕业生。

浙江的高校毕业生就业政策比较丰富。除了杭州市区,全面放开专科以上学历毕业生的落户限制,杭州的落户条件为本科以上学历。高校毕业生到浙江工作,可以享受 2万到40万不等的生活补贴或购房租房补贴。大学生想创业,可贷款10万到50万, 如果创业失败,贷款10万以下的由政府代偿,贷款10万以上的部分,由政府代偿80%。大学生从事家政、养老和现代农业创业,政府给予10万元的创业补贴,大学生到这些领域工作,政府给予 每人每年1万的就业补贴,连续补贴3年。大学生到浙江实习的,各地提供生活补贴。对家庭困难的毕业生,发放每人3000元的求职创业补贴。我们欢迎全国的高校毕业生到浙江来就业创业。

浙江是用工大省,省外务工人员在浙江有2300万,他们为浙江经济社会发展作出了重要贡献。在浙江,省外务工人员与本地户籍的劳动者享受同等的就业创业服务和政策。另外,浙江还开发不讲技能、不讲学历、不讲年龄的爱心岗位,专门安置脱贫人口,保证他们的月薪4500元以上,去年全省有脱贫人口225万。

浙江的平台经济比较发达,各种新就业形态快速发展,浙江非常关注新就业形态劳动者的劳动保障问题。去年,浙江专门出台了维护新就业形态劳动者劳动保障权益的实施办法,主要是放开了灵活就业人员在就业地参加企业职工基本养老保险、基本医疗保险的户籍限制,支持新就业形态劳动者单险种参加工伤保险;我们还要求平台企业发挥数据技术优势,合理管控在线工作时间,对连续工作超过4小时的要安排工间休息。

来源:国家发展和改革委员会官方网站、浙江新闻客户端

收起阅读 »