摩登3平台注册登录_三星 2021 年旗舰手机 : 采用平面显示屏

近日,有爆料者称三星 2021 年的旗舰系列 Galaxy S21(未定名)系列将采用四边框等宽的设计,其中 S21 和 S21+ 均采用 2D 平面屏幕,仅有 S21U 选择曲面屏。配置方面,传闻称 Galaxy S21 系列将基于高通骁龙 875 芯片组,支持 25W 快充,前置单摄 + 后置三摄,可选银、白、粉、紫、灰等配色。 早些时候,知名爆料人 @UniverseIce 声称 Galaxy S21 / S21+ 机型将采用平面显示屏,只有旗舰级的 Galaxy S21 Ultra 机型才会用上带有曲面的显示屏,但它们的边框都保持在同一个水平。然后 @SamsungRydah 补充道,Galaxy S21 Ultra 前摄开孔将比前几代有所减小,并预计屏幕为 6.7 英寸(分辨率暂不得而知)。 此外,预计三星 Galaxy S21 Ultra 将搭载高通骁龙 875 旗舰处理器,欧洲市场发售的版本则搭载 Exynos 2100 处理器。目前三星 Galaxy S21 系列电池已通过国家 3C 认证,其中 Ultra 款电池容量为 5000mAh,全系标配 25W 电源适配器。 韩联社等知名韩媒称三星将在 S21 系列产品线中首次为 S 系列引入 S Pen 支持,不过IT之家发现 @OnLeaks 提到,Galaxy S21 Ultra 并没有为 S Pen 设置专用插槽。这意味着手写笔不能像 Note 系列那样放在手机内部,但新设备也完全可以将其置于手机外部。 而据 SamMobile 和 AndroidCentral 各自分别通过自己的渠道确认,三星将在明年 1 月初推出其下一代旗舰机型 Galaxy S21 系列。 至于更多详细信息,我们拭目以待。不如让我们一起期待一下。由于该机仍在开发中,因此后续会有更多的配置信息曝光出来,21ic会持续跟进。

摩登3新闻554258:_令人狂喜!OPPO AR智能眼镜要来啦!

在这篇文章中,小编将为大家带来OPPO AR智能眼镜的最新报道。如果你对本文即将要讲解的内容存在一定兴趣,不妨继续往下阅读哦。 OPPO 去年发布旗下首款 AR 眼镜,采用一体式设计,主打 AR 内容、AR 游戏和 AR 服务。从 OPPO 官方公布的信息来看,新一代 OPPO AR 眼镜将更加轻便、舒适,且具备更丰富的交互方式。 新一代OPPO AR眼镜非常轻便,两个镜片的角落里还有两枚摄像头,至少有一枚用于AR侦测。 第一代OPPO AR眼镜配备了四枚镜头,其中一枚ToF镜头用于测距,一枚RGB镜头用于拍摄物体形状,两枚鱼眼镜头用于成像。 据介绍,OPPO AR眼镜将搭载深度传感器和空间MIC阵列,采用衍射光波导技术,支持语音交互和3D环绕立体声,融合物理世界和数字世界,实现虚实交互的体验。还配备了麦克风与实体按键,可通过手势和语音来操作。 以上便是小编此次想要和大家共同分享的内容,如果你对本文内容感到满意,不妨持续关注我们网站哟。最后,十分感谢大家的阅读,have a nice day!

摩登3注册登录网_应用材料公司发布2020财年第四季度及全年财务报告

· 创纪录的季度收入46.9亿美元,同比增长25% · 创纪录的季度GAAP每股盈余1.23美元,非GAAP每股盈余1.25美元,同比分别增长64%和56% · 实现创纪录的年度经营活动现金流38亿美元 2020年11月12日,加利福尼亚州圣克拉拉——应用材料公司公布了其截止于2020年10月25日的2020财年第四季度及全年财务报告。 2020财年第四季度业绩 应用材料公司实现营收46.9亿美元。基于GAAP(一般公认会计原则),公司毛利率为45.4%,营业利润为12.8亿美元,占净销售额的27.4%,每股盈余(EPS)为1.23美元。 在调整后的非GAAP基础上,公司毛利率为45.7%,营业利润13.3亿美元,占净销售额的28.3%,每股盈余为1.25美元。 公司实现经营活动现金流13.2亿美元,通过2亿美元的股息派发和0.5亿美元的股票回购向股东返还2.5亿美元。 全年业绩 2020财年全年,应用材料公司共实现营收172亿美元。基于GAAP,公司毛利率为44.7%,营业利润为43.7亿美元,占净销售额的25.4%,每股盈余为3.92美元。 在调整后的非GAAP基础上,公司毛利率为45.1%,营业利润45.3亿美元,占净销售额的26.3%,每股盈余为4.17美元。 公司全年实现经营活动现金流38亿美元,共计派发了7.87亿美元的股息,并支出6.49亿美元用于回购1200万股公司普通股股票。 应用材料公司总裁兼首席执行官盖瑞·狄克森表示:“得益于市场对我们的半导体设备和服务的持续强劲需求,应用材料公司以创纪录的季度业绩结束了2020财年。随着强大技术趋势的成形,我们面临前所未有的发展良机,在加速实现客户路线图方面具有独一无二的优势,并将领先市场发展。” 业绩一览 GAAP和非GAAP财报之间的调整信息包含在本新闻稿中的财务报表中。请参见后文“非GAAP财务计量方法的使用”。 业务展望 展望2021财年第一季度,应用材料公司预计净销售额约为49.5亿美元,浮动范围为2亿美元。调整后的非GAAP稀释每股盈余预计在1.2美元至1.32美元之间。 应用材料公司2021财年第一季度展望中的非GAAP稀释每股盈余预估不包括每股0.01美元的完成收购相关的已知费用,包括公司内部无形资产转移相关的每股0.03美元的所得税税收优惠,但并未反应其他目前未知的项目,如与收购或其他非经营性及非经常性项目有关的额外费用,以及其他与税收相关的项目,考虑到其本身具有不确定性,公司目前无法对其做出合理预测。 第四季度及全年各事业部的财务表现 非GAAP财务计量方法的使用 应用材料公司为投资者提供非GAAP调整后财务报表,并根据部分成本、费用、收益和损失,包括与并购相关的某些项目的影响进行了调整;重组费用和任何相关的调整;与新冠肺炎相关增加的费用;资产或投资的减值调整;战略性投资所得收益或损失;提前清偿债务造成的损失;特定所得税税目及其他调整项目。在非GAAP基础上,与股权激励相关的税费已在本财年按比例确认。此外,非GAAP业绩不包括与美国税收立法变化相关的预估所得税费用项目。GAAP和非GAAP财报之间的调整信息包含在本新闻稿中的财务报表中。 公司的管理者出于业务规划目的,采用非GAAP财务报告来评估公司的运营与财务表现,并在其高管薪酬项目中作为绩效考核的指标。应用材料公司相信,通过剔除与当前运营无关的项目,这些计量方法有利于提高对公司整体业绩的理解,并有利于投资者站在与管理者相同的立场上来审视公司的运营能力,也便于投资者对前后两个季度财报数字进行比较。采用非GAAP财务报告有一定的局限性,因其计量口径与普遍认可的GAAP会计准则不一致,也可能有别于其他公司的会计和报告中所使用的非GAAP方法,并可能剔除了部分对报告中财务数据具有显著影响的项目。这些增加的信息并不能取代根据GAAP准则制作的财报,亦不应作为其补充。 本新闻稿包含某些前瞻性的陈述,包括应用材料公司对公司业务增长及市场趋势、行业发展前景和技术变革的预判、公司的业务和财务表现及市场份额情况、现金调配策略、新产品和新技术开发情况、对2021财年第一季度及长期的业务展望、持续流行的新冠肺炎疫情的影响及应对措施对公司运营和财务业绩的影响、战略收购和投资,包括:拟收购国际电气公司(Kokusai Electric Corporation),以及其他不属于历史事实的陈述。这些前瞻性陈述及其相应假设可能受到风险或不确定因素的影响,导致实际结果显著不同于陈述的内容或声明内所暗示的情形,因此不对未来业绩做担保。而这些风险和不确定因素包含但不仅限于下列内容:对于应用材料公司产品需求的程度;全球经济与产业环境;区域或全球性流行病的影响,包括持续流行的新冠肺炎疫情的严重程度和持续时间;全球贸易问题和贸易与出口许可政策的变化,包括履行和解释美国商务部于2020年4月28日和2020年8月17日发布的关于某些出口许可要求的影响;电子产品的需求;半导体产品的需求;客户对技术与产能的要求;新推出的创新技术以及技术变革的时机;公司开发、交付并支持新产品和新技术的能力;公司客户分布的集中性;收购、投资和剥离;所得税的变化;应用材料公司拓展现有市场、增加市场份额、开拓新市场的能力;现有产品和新产品的市场接受度;应用材料公司获取及保护关键技术的知识产权、完成运营计划设定的各项战略目标、根据业务环境调整资源和成本结构、以及关键员工的招募、留任与激励的能力;各事业部和产品线营运费用和业绩的多变性;应用材料公司准确预测未来的业绩、市场环境、客户及业务需求的能力;以及其他应用材料公司在最近提交和定期提交至美国证券交易委员会的存案文件(包括最新季报)中述明的风险和不确定因素, 包括我们最新的10-Q和8-K表格。所有前瞻性陈述均基于管理层目前的估计、预测和假设。应用材料公司没有任何义务更新任何前瞻性陈述。

摩登3平台注册登录_雷军:如果程序人生的话,这条路太漫长

这篇文章是在雷总个人博客看到的,里面聊到了他作为程序员的一些经历、初衷以及思考。写的不错,便转来给大家看看。 如果程序人生的话,这条路太漫长 我并非天生喜欢写程序,上高中时也没有想过程序员的生活。 我学电脑非常偶然,小时好友上大学时选择了计算机系,为了和这个朋友有更多的共同语言,我也选择了计算机系,开始步入程序人生的道路。 当我学会一些后,发现自己特别喜欢写程序。我是八七年上的武汉大学计算机系,大一下学期才有专业课。当我有资格上机的时候,发现电脑世界太美妙,就一头扎进去。 当时用的是 Motorola 68000 (相当 于 Intel 8088), 540K 的内存,运行的 UNIX 操作系统,八个人一起用。 大二学PC,又过了一学期,开始出现在老师的实验室,帮忙干活,当时就写了现在很多人用的 RI (RAMinit, 清内存的小工具, 看来我还是最早一批写 Shareware 的人)。 又过了一个学期,开始和校外的公司接触。大二暑假,也就是1989年8月,和一个朋友组建了 Yellow Rose 软件小组,写了我第一个商品软件 BITLOK 0.99。后来自己创业办过公司,也写过一些其他的软件。 大学毕业后,分到研究所,不太适应那里的气氛,就在1992年初加入金山软件,开始了职业程序员的生涯。后来成了金山软件研发部门的主管,但我一直都是一线的程序员。 程序员活在自己想象的王国里 我刚接触电脑就发现电脑的妙处,电脑远没有人那么复杂。如果你的程序写得好,你就可以和电脑处好关系,就可以指挥电脑干你想干的事。 这个时候你是十足的主宰。每每你坐在电脑面前,你就是在你的王国里巡行,这样的日子简直就是天堂般的日子。 电脑里的世界很大,编程人是活在自己想象的王国里。你可以想象到电脑里细微到每一个字节、每一个比特的东西。 我爱编程这个工作,可以肯定我会干上一辈子 不少人认为程序员最多干到三十五岁就可以收山换环境了,脑子也差不多该歇歇了,体力也不支了。并认为写程序是年轻人的事情,到了一定岁数,估计没什么人再当程序员了。 当我刚有一点本事的时候,我也和大家一样觉得编程辛苦,也想三十岁后干别的。当我年长一点后就发现了自己的无知。 一个人大学毕业就二十一二岁,有点水平的时候可能二十五,接着就是过日子诸多事情。一切搞掂的时候,也许就是三十五岁。如果这样的话,我们就不用选择程序人生的道路。 电脑进入中国时间并不短,但真正大规模开始用,还是八五年 PC 开始的,因此国内真正写电脑程序的人最长也就写了十几年(不知道是否还有这样的人)。 由于电脑应用在国内时间比较短,国内开发的主力是三十五岁以下的年轻人为主。但这不表示程序员如同红粉佳人般的容易衰老。美国主力工程师以三十四十多岁的人为主。 开始的时候,我们觉得我们没有什么不能做的(现在还能听到这样的豪言壮语),而且更要命的是好象我们特别聪明,特别适合开发软件,比老外强得多。 当我们真正接触那些杰出的开发人员的时候,发现他们太厉害了,都有十多年的开发经验。虽然也有很多年轻人做了很多好东西,但决大多数的产品出自这些有丰富开发经验的程序员的手。 刚毕业的时候,编程不仅仅是爱好,而且也成了一辈子的工作。整天不知道写些什么东西,觉得特别没劲,找不到感觉,特别灰心。 后来,才明白, 只有全身心地投入,程序才会有感觉。 写程序的活特别费脑子,也特别累,但我喜欢,可以肯定我会干上一辈子,虽然我没有打算一生只干这一件事。用一生来编程序是一件既容易又困难的事。 如果碌碌无为,为交差写点程序,这样的日子太好混了。但如果想全身心地写程序,写十年就不是一件容易的事。 现在我不少朋友都洗手了,有时我也想“用什么电脑呀,Windows 外的世界不是也很大吗?”。 面对电脑的时候,立刻顿悟:写程序还是自己最擅长的事,也是最喜欢的事。 高级程序员不是追求的目标 有的人学习编程技术,是把高级程序员做为追求的目标,甚至是终身的奋斗目标。后来参与了真正的商品化软件开发后,反而困惑了,茫然了。 一个人只要有韧性和灵性,有机会接触并学习电脑的编程技术,就会成为一个不错的程序员。刚开始写程序,这时候学得多的人写的好,到了后来,大家都上了一个层次,谁写的好只取决于这个人是否细心、有韧性、有灵性。掌握多一点或少一点,很快就能补上。 成为一个高级程序员并不是件困难的事。 当我上学的时候,高级程序员也曾是我的目标,我希望我的技术能得到别人的承认。后来发现无论多么高级的程序员都没用,关键是你是否能够出想法出产品,你的劳动是否能被社会承认,能为社会创造财富。成为高级程序员绝对不是追求的目标。 编程不仅仅是技术,还是艺术 有人认为编程是一种熟练工种,也有人把编程说成是艺术创作。这两种意见争论比较激烈。 我们换个工种来看,石匠应该是熟练工种,属于工人,更和艺术似乎沾不上边。但正是这些石匠,给我们留下多少文物古迹,如乐山大佛、莫高窟等等。应该说这些石匠给我们留下了无穷的文化财产。 现代软件工业已具相当规模,很多软件的完成需要的是大兵团作战。一名普通程序员接受编写某一模块的任务后,往往只是写代码,发挥的余地很小。 在大项目中,很多程序员只能了解到和自己所编模块相关的很局部的细节,另外还受到开发环境的限制,真的很难体会到自己在从事”艺术”创造,更多的时候是感到自己在从事重体力劳动。 有的时候还担心自己苦苦参与的这个项目究竟有没有意义,是不是在同类产品中有竞争力,会不会开发出来以后就因为硬件的发展,操作系统的换代而过时…… 我认为编程的工作和石匠比较相似,有技术活,更多的是体力活。不管怎么说,写出一个好软件不是一件容易的事。 这两种想法都有片面性,编程应该说两种属性都有。编程不仅仅是技术,也还是艺术。编程是技术活,才有可能大规模进行,才会有软件工程和软件工厂。也正是编程是艺术,才会有如此多的好产品,让大家如痴如醉。 著名程序编程指北点评表示,雷总是中国最早的一批程序员,极具极客精神。他把写程序当作一生的追求,完全没有去考虑程序员是吃青春饭的问题,全身心的投入到代码王国。 在他眼里编程不仅仅是谋生的一个技能,更是一种艺术。这也许就是极客程序员和普通程序员的区别吧。 希望诸君共勉,未来能在核心工业软件摆脱美国制裁上贡献属于自己的一行代码! -END- 直接来源 | 编程指北 作者 | 雷军 原文 | http://leijun.blog.techweb.com.cn/archives/10 | 整理文章为传播相关技术,版权归原作者所有 | 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

摩登3咨询:_小白如何自学单片机?

很多刚开始学习单片机的小伙伴最苦恼的就是如何入门,不知道从哪一部分开始、在哪里查找学习资源、按照怎样的学习步骤进行学习,而且摸索学习步骤的过程在浪费时间的同时也会降低学习兴趣。为了帮助大家解决这种情况,小编将单片机达人的学习经验进行了整理,以文章的形式分享给大家。 一、基础理论知识 要掌握理论知识,第一步还是要通读一遍教材,这样我们才能站在巨人的肩膀上。《电工基础》、《电路分析》、《模拟电路》、《数字电路》、《电子制作》等电子技术基础知识一定要先通读。 (1)电场与磁场:库仑定律、高斯定理、环路定律、电磁感应定律。 (2)直流电路:电路基本元件、欧姆定律、基尔霍夫定律、叠加原理、戴维南定理。 (3)正弦交流电路:正弦量三要素、有效值、复阻抗、单相和三相电路计算、功率及功率因数、串联与并联谐振、安全用电常识。 (4)RC和RL电路暂态过程:三要素分析法。 (5)变压器与电动机:变压器的电压、电流和阻抗变换、三相异步电动机的使用、常用继电-接触器控制电路。 (6)半导体及二极管及整流、滤波、稳压电路。 (7)三极管及单管放大电路、信号处理电路、信号发生电路、功率放大电路、直流稳压电源等。 (8)电子产品工艺流程、电子产品的结构和装配、调试和检修。 (9)线性集成运算放大器和运算电路及理想运放组成的比例、加减和积分运算电路。 (10)数字基础及逻辑函数化简、集成逻辑门电路、组合逻辑电路和RS、D、JK触发器,时序逻辑电路。 (11)多谐振荡器、单稳态触发器、施密特触发器的结构、工作原理、参数计算和应用。 (12)数模和模数转换等相关内容。 二、常用软件 (1)Protel99se、AltiumDesigner9等PCB电路设计软件。 (2)Multisim11、Proteus7.8等电子电路原理仿真设计软件。 (3)Keil、Progisp20等单片机应用程序开发平台相关设计软件。 三、资料检索 很多时候遇到问题,要查找资料的时候却不知道去哪里找,这里小编给大家推荐三个网站:GitHub,StackOverflow,中国知网。 GitHub 程序员都会用到的一个代码托管网站,熟悉的人就不用我多说了。在上面可以搜索到很多很好的开源项目。 StackOverflow 英文网站,英文水平要好。上面可以搜索到很多技术细节上的问题,回答大多都会比较靠谱,有点类似知乎,但问题主要是IT相关的。 中国知网 如果你想做一个项目但是还不知道应该从哪里入手或遇到了技术上的阻碍,就可以在这里搜一搜,期刊和论文的一般会有转载,有助于你系统了解相关的知识。如果是在校大学生,使用校园网应该是可以免费下载文档的。如果不是,可以上某宝租账号下载。 四、实践 实践是检验真理的唯一标准。对一个学单片机的新手来说,如果按教科书式的学法,上来就是一大堆指令和名词,结果学了半天还是搞不清这些指令的作用,也许用不了几天就会觉得枯燥乏味以至于半途而废。 所以学习与实践结合是一个很好的方法,边学习、边演练,循序渐进,这样用不了几次就能将所用到的指令理解、吃透、扎根于脑海,甚至让这些指令“根深蒂固”。 -END- | 整理文章为传播相关技术,版权归原作者所有 |  | 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

摩登3娱乐登录地址_手把手教你升级Keil MDK的ARM编译器

作者 | strongerHuang 微信公众号 | strongerHuang 今天在我的技术交流群里,有朋友问了这么一个问题:怎么才能用更高的编译器呀? 这位朋友给了一张图: 从上图可以看得出来,这位朋友使用的Keil MDK并不是最新版本。目前(2020-11) 最新MDK版本为V5.32 ,默认编译器版本为V6.14.1: 如果我想使用V6.15版本编译器该怎么操作呢?那么下面就来讲讲:怎样将Keil MDK的编译器升级为最新的编译器(更换为指定版本的编译器): 下载ARM编译器 安装ARM编译器 Keil 配置编译器 额外说明 strongerHuang 1 下载ARM编译器 这里不一定是使用最新的编译,我们也可以使用老版本的,目前官方提供了历史版本供大家下载。 通过浏览器自带下载器可能比较慢,推荐使用迅雷,很多都有镜像,速度相对快点。 strongerHuang 2 安装ARM编译器 安装之前需要提醒一点,根据你Keil MDK版本不同,支持的编译器可能存在兼容问题。比如MDK是V4版本,建议下载32位版本。 我这里以ARM编译器Windows 64位为例,安装过程比较傻瓜式,基本一路“next”即可。 这里建议修改一下路径: 安装完成之后,会有相关的说明文档,可以看下: strongerHuang 3 Keil 配置编译器 Keil MDK里面有很多配置选项,这里推荐大家阅读我的《 Keil系列教程 》。 1.打开工程管理,进入“Folders/Extensions”选项栏 可以通过菜单:Project -> Manage -> Project items进入。 也可以通过工具栏工程管理快捷图标: 2.修改(新增)编译器 3.工程选择编译器 新增编译器之后,就可以在工程配置中选择新增的编译器了: 此时就可以和往常一样正常使用了。 strongerHuang 4 额外说明 1.编译器注册 不管是Keil MDK,还是ARM编译器都是收费的工具,就会牵涉到注册的问题,如果按照上面的步骤直接使用ARM新增的编译,可能会出现如下错误: 意思就是没有进行注册,此时就需要花钱购买正版了。。。 不想花钱购买正版的同学自己想办法了,下面方法不要说是我给大家的哈: 正确注册之后,就会没问题: 2.工程选项配置的变化 如果我们选择了不同的编译器,可能你Keil MDK的工程选项就会发生变化: 当然,这个变化与你MDK版本还是有一定关系(不同版本可能不同)。 最后,大家想要阅读更多关于 Keil MDK的内容,后台回复”Keil” 即可查看。 ———— END ———— 推荐阅读: C语言实现面向对象的原理 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

摩登3登录_分享一个通过STM32的bin文件逆向分析代码的工具

转载:芯片之家 来源 | 阿莫论坛,作者 | ilovepp 首先你要有一个bin文件(bin文件的获取方法不在此展开介绍,今后有机会可以专门开一个贴聊一聊)。本次实验用到的bin文件 stm32_xwrtos.bin.zip (6 KB) 是用ST官方CMSIS和外设库编译的跑在stm32f103c8t6上的bin文件,比较具有代表性。 烧录文件下载 工具准备: 安装开源跨平台逆向神器r2: r2可运行在Windows、Linux、Mac等所有主流操作系统上(r2有多牛逼不在此展开介绍,今后有机会可以专门开一个贴聊一聊)。 实验步骤: 1)输入r2 -a arm -b 16 -m 0x08000000 stm32_xwrtos.bin 进入r2的控制台后,输入e asm.cpu=cortex。这一步是告诉r2以0x08000000为基址加载stm32_xwrtos.bin文件,并设置指令集为cortex系列的thumb。 2)输入aaaa,运行自动化分析。 3)输入pxw 268 @0x08000000 以小端四字节形式打印从0x08000000开始的268(268对应中断向量表大小)个字节,同时打开ST官方的启动文件startup_stm32f10x_md.s并找到.isr_vector段进行对照。 4)接着上一步,输入fs notes,创建并切换到一个名为notes(可以是任意其他名字)的符号记事本;按照f flag=address的形式,对照.isr_vector段,向符号记事本中录入感兴趣的符号地址对应关系。注意如果address是函数地址则需要减1(因为thumb指令的要求,具体原因此处不展开);最后输入af @flag形式的命令强制进行函数分析。 5) 输入pdf @Reset_Handler,对Reset_Handler函数进行反汇编 输入 VV 切换到流程图视图 通过阅读汇编代码,可以得到以下信息: 1.  data段的地址区间为0x20000000-0x20000028 bss段的地址区间为0x20000028-0x2000043C 2.  Reset_Handler用0x08002cbc开始的0x28字节初始化data段,用0填充bss段 3. 调用fcn.08001cc8函数 4. 调用fcn.08001734函数 6)输入pdf @fcn.08001cc8进行反汇编 发现对外设寄存器地址0x40021000的引用 通过查询数据手册,可判断fcn.08001cc8函数应为系统初始化化函数SystemInit(),它初始化了时钟。 7)根据经验判断fcn.0800173函数即为main函数 本篇主要作用是带大家熟悉和习惯r2的基本使用,以及对逆向有个感性认识。后面有机会再给大家讲述在逆向的基础上介绍stm32上的栈溢出漏洞的挖掘与利用。 再次感谢ilovepp兄分享的精彩文章! ———— END ———— 推荐阅读: C语言实现面向对象的原理 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

摩登3测速登陆_刨根问底,Kafka消息中间件到底会不会丢消息

大型互联网公司一般都会要求消息传递最大限度的不丢失,比如用户服务给代金券服务发送一个消息,如果消息丢失会造成用户未收到应得的代金券,最终用户会投诉。 为避免上面类似情况的发生,除了做好补偿措施,更应该在系设计的时候充分考虑各种异常,设计一个稳定、高可用的消息系统。 认识Kafka 看一下维基百科的定义 Kafka是分布式发布-订阅消息系统。它最初由LinkedIn公司开发,之后成为Apache项目的一部分。 Kafka是一个分布式的,可划分的,冗余备份的持久性的日志服务。它主要用于处理活跃的流式数据。 kafka架构 Kafka的整体架构非常简单,是显式分布式架构,主要由producer、broker(kafka)和consumer组成。 Kafka架构(精简版) Producer(生产者)可以将数据发布到所选择的topic(主题)中。生产者负责将记录分配到topic的哪一个 partition(分区)中。可以使用循环的方式来简单地实现负载均衡,也可以根据某些语义分区函数(如记录中的key)来完成。 Consumer(消费者)使用一个consumer group(消费组)名称来进行标识,发布到topic中的每条记录被分配给订阅消费组中的一个消费者实例。消费者实例可以分布在多个进程中或者多个机器上。 Kafka到底会不会丢失消息? 在讨论kafka是否丢消息前先来了解一下什么是消息传递语义。 消息传递语义 message delivery semantic 也就是消息传递语义,简单说就是消息传递过程中消息传递的保证性。主要分为三种: at most once:最多一次。消息可能丢失也可能被处理,但最多只会被处理一次。 at least once:至少一次。消息不会丢失,但可能被处理多次。可能重复,不会丢失。 exactly once:精确传递一次。消息被处理且只会被处理一次。不丢失不重复就一次。 理想情况下肯定是希望系统的消息传递是严格exactly once,也就是保证不丢失、只会被处理一次,但是很难做到。 回到主角Kafka,Kafka有三次消息传递的过程: 生产者发消息给Kafka Broker。 Kafka Broker 消息同步和持久化 Kafka Broker 将消息传递给消费者。 在这三步中每一步都有可能会丢失消息,下面详细分析为什么会丢消息,如何最大限度避免丢失消息。 生产者丢失消息 先介绍一下生产者发送消息的一般流程(部分流程与具体配置项强相关,这里先忽略): 生产者是与leader直接交互,所以先从集群获取topic对应分区的leader元数据; 获取到leader分区元数据后直接将消息发给过去; Kafka Broker对应的leader分区收到消息后写入文件持久化; Follower拉取Leader消息与Leader的数据保持一致; Follower消息拉取完毕需要给Leader回复ACK确认消息; Kafka Leader和Follower分区同步完,Leader分区会给生产者回复ACK确认消息。 生产者发送数据流程 生产者采用push模式将数据发布到broker,每条消息追加到分区中,顺序写入磁盘。消息写入Leader后,Follower是主动与Leader进行同步。 Kafka消息发送有两种方式:同步(sync)和异步(async),默认是同步方式,可通过producer.type属性进行配置。 Kafka通过配置request.required.acks属性来确认消息的生产: 0表示不进行消息接收是否成功的确认;不能保证消息是否发送成功,生成环境基本不会用。 1表示当Leader接收成功时确认;只要Leader存活就可以保证不丢失,保证了吞吐量。 -1或者all表示Leader和Follower都接收成功时确认;可以最大限度保证消息不丢失,但是吞吐量低。 kafka producer 的参数acks 的默认值为1,所以默认的producer级别是at least once,并不能exactly once。 敲黑板了,这里可能会丢消息的! 如果acks配置为0,发生网络抖动消息丢了,生产者不校验ACK自然就不知道丢了。 如果acks配置为1保证leader不丢,但是如果leader挂了,恰好选了一个没有ACK的follower,那也丢了。 all:保证leader和follower不丢,但是如果网络拥塞,没有收到ACK,会有重复发的问题。 Kafka Broker丢失消息 Kafka Broker 接收到数据后会将数据进行持久化存储,你以为是下面这样的: 消息持久化,无cache 没想到是这样的: 消息持久化,有cache 操作系统本身有一层缓存,叫做 Page Cache,当往磁盘文件写入的时候,系统会先将数据流写入缓存中,至于什么时候将缓存的数据写入文件中是由操作系统自行决定。 Kafka提供了一个参数 producer.type 来控制是不是主动flush,如果Kafka写入到mmap之后就立即 flush 然后再返回 Producer 叫同步 (sync);写入mmap之后立即返回 Producer 不调用 flush 叫异步 (async)。 敲黑板了,这里可能会丢消息的! Kafka通过多分区多副本机制中已经能最大限度保证数据不会丢失,如果数据已经写入系统 cache 中但是还没来得及刷入磁盘,此时突然机器宕机或者掉电那就丢了,当然这种情况很极端。 消费者丢失消息 消费者通过pull模式主动的去 kafka 集群拉取消息,与producer相同的是,消费者在拉取消息的时候也是找leader分区去拉取。 多个消费者可以组成一个消费者组(consumer group),每个消费者组都有一个组id。同一个消费组者的消费者可以消费同一topic下不同分区的数据,但是不会出现多个消费者消费同一分区的数据。 消费者群组消费消息 消费者消费的进度通过offset保存在kafka集群的__consumer_offsets这个topic中。 消费消息的时候主要分为两个阶段: 1、标识消息已被消费,commit offset坐标; 2、处理消息。 场景一:先commit再处理消息。如果在处理消息的时候异常了,但是offset 已经提交了,这条消息对于该消费者来说就是丢失了,再也不会消费到了。 场景二:先处理消息再commit。如果在commit之前发生异常,下次还会消费到该消息,重复消费的问题可以通过业务保证消息幂等性来解决。 总结 那么问题来了,kafka到底会不会丢消息?答案是:会! Kafka可能会在三个阶段丢失消息: (1)生产者发送数据; (2)Kafka Broker 存储数据; (3)消费者消费数据; 在生产环境中严格做到exactly…

摩登3登录_破4!《我想进大厂》之Java基础夺命连环16问

说说进程和线程的区别? 进程是程序的一次执行,是系统进行资源分配和调度的独立单位,他的作用是是程序能够并发执行提高资源利用率和吞吐率。 由于进程是资源分配和调度的基本单位,因为进程的创建、销毁、切换产生大量的时间和空间的开销,进程的数量不能太多,而线程是比进程更小的能独立运行的基本单位,他是进程的一个实体,可以减少程序并发执行时的时间和空间开销,使得操作系统具有更好的并发性。 线程基本不拥有系统资源,只有一些运行时必不可少的资源,比如程序计数器、寄存器和栈,进程则占有堆、栈。 知道synchronized原理吗? synchronized是java提供的原子性内置锁,这种内置的并且使用者看不到的锁也被称为监视器锁,使用synchronized之后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,他依赖操作系统底层互斥锁实现。他的作用主要就是实现原子性操作和解决共享变量的内存可见性问题。 执行monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。此时其他竞争锁的线程则会进入等待队列中。 执行monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续竞争锁。 synchronized是排它锁,当一个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁,而且由于Java中的线程和操作系统原生线程是一一对应的,线程被阻塞或者唤醒时时会从用户态切换到内核态,这种转换非常消耗性能。 从内存语义来说,加锁的过程会清除工作内存中的共享变量,再从主内存读取,而释放锁的过程则是将工作内存中的共享变量写回主内存。 实际上大部分时候我认为说到monitorenter就行了,但是为了更清楚的描述,还是再具体一点。 如果再深入到源码来说,synchronized实际上有两个队列waitSet和entryList。 当多个线程进入同步代码块时,首先进入entryList 有一个线程获取到monitor锁后,就赋值给当前线程,并且计数器+1 如果线程调用wait方法,将释放锁,当前线程置为null,计数器-1,同时进入waitSet等待被唤醒,调用notify或者notifyAll之后又会进入entryList竞争锁 如果线程执行完毕,同样释放锁,计数器-1,当前线程置为null 那锁的优化机制了解吗? 从JDK1.6版本之后,synchronized本身也在不断优化锁的机制,有些情况下他并不会是一个很重量级的锁了。优化机制包括自适应锁、自旋锁、锁消除、锁粗化、轻量级锁和偏向锁。 锁的状态从低到高依次为无锁->偏向锁->轻量级锁->重量级锁,升级的过程就是从低到高,降级在一定条件也是有可能发生的。 自旋锁:由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起线程,用户态和内核态的来回上下文切换严重影响性能。自旋的概念就是让线程执行一个忙循环,可以理解为就是啥也不干,防止从用户态转入内核态,自旋锁可以通过设置-XX:+UseSpining来开启,自旋的默认次数是10次,可以使用-XX:PreBlockSpin设置。 自适应锁:自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定。 锁消除:锁消除指的是JVM检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除。 锁粗化:锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操作序列之外。 偏向锁:当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后这个线程再次进入同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。可以用过设置-XX:+UseBiasedLocking开启偏向锁。 轻量级锁:JVM的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁。 整个锁升级的过程非常复杂,我尽力去除一些无用的环节,简单来描述整个升级的机制。 简单点说,偏向锁就是通过对象头的偏向线程ID来对比,甚至都不需要CAS了,而轻量级锁主要就是通过CAS修改对象头锁记录和自旋来实现,重量级锁则是除了拥有锁的线程其他全部阻塞。 那对象头具体都包含哪些内容? 在我们常用的Hotspot虚拟机中,对象在内存中布局实际包含3个部分: 对象头 实例数据 对齐填充 而对象头包含两部分内容,Mark Word中的内容会随着锁标志位而发生变化,所以只说存储结构就好了。 对象自身运行时所需的数据,也被称为Mark Word,也就是用于轻量级锁和偏向锁的关键点。具体的内容包含对象的hashcode、分代年龄、轻量级锁指针、重量级锁指针、GC标记、偏向锁线程ID、偏向锁时间戳。 存储类型指针,也就是指向类的元数据的指针,通过这个指针才能确定对象是属于哪个类的实例。 如果是数组的话,则还包含了数组的长度 对于加锁,那再说下ReentrantLock原理?他和synchronized有什么区别? 相比于synchronized,ReentrantLock需要显式的获取锁和释放锁,相对现在基本都是用JDK7和JDK8的版本,ReentrantLock的效率和synchronized区别基本可以持平了。他们的主要区别有以下几点: 等待可中断,当持有锁的线程长时间不释放锁的时候,等待中的线程可以选择放弃等待,转而处理其他的任务。 公平锁:synchronized和ReentrantLock默认都是非公平锁,但是ReentrantLock可以通过构造函数传参改变。只不过使用公平锁的话会导致性能急剧下降。 绑定多个条件:ReentrantLock可以同时绑定多个Condition条件对象。 ReentrantLock基于AQS(AbstractQueuedSynchronizer 抽象队列同步器)实现。别说了,我知道问题了,AQS原理我来讲。 AQS内部维护一个state状态位,尝试加锁的时候通过CAS(CompareAndSwap)修改值,如果成功设置为1,并且把当前线程ID赋值,则代表加锁成功,一旦获取到锁,其他的线程将会被阻塞进入阻塞队列自旋,获得锁的线程释放锁的时候将会唤醒阻塞队列中的线程,释放锁的时候则会把state重新置为0,同时当前线程ID置为空。 CAS的原理呢? CAS叫做CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含三个操作数: 变量内存地址,V表示 旧的预期值,A表示 准备设置的新值,B表示 当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。 那么CAS有什么缺点吗? CAS的缺点主要有3点: ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果。 Java中有AtomicStampedReference来解决这个问题,他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。 循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。 只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性,但是多个则不行,多个可以通过AtomicReference来处理或者使用锁synchronized实现。 好,说说HashMap原理吧? HashMap主要由数组和链表组成,他不是线程安全的。核心的点就是put插入数据的过程,get查询数据以及扩容的方式。JDK1.7和1.8的主要区别在于头插和尾插方式的修改,头插容易导致HashMap链表死循环,并且1.8之后加入红黑树对性能有提升。 put插入数据流程 往map插入元素的时候首先通过对key hash然后与数组长度-1进行与运算((n-1)&hash),都是2的次幂所以等同于取模,但是位运算的效率更高。找到数组中的位置之后,如果数组中没有元素直接存入,反之则判断key是否相同,key相同就覆盖,否则就会插入到链表的尾部,如果链表的长度超过8,则会转换成红黑树,最后判断数组长度是否超过默认的长度*负载因子也就是12,超过则进行扩容。 get查询数据 查询数据相对来说就比较简单了,首先计算出hash值,然后去数组查询,是红黑树就去红黑树查,链表就遍历链表查询就可以了。 resize扩容过程 扩容的过程就是对key重新计算hash,然后把数据拷贝到新的数组。 那多线程环境怎么使用Map呢?ConcurrentHashmap了解过吗? 多线程环境可以使用Collections.synchronizedMap同步加锁的方式,还可以使用HashTable,但是同步的方式显然性能不达标,而ConurrentHashMap更适合高并发场景使用。 ConcurrentHashmap在JDK1.7和1.8的版本改动比较大,1.7使用Segment+HashEntry分段锁的方式实现,1.8则抛弃了Segment,改为使用CAS+synchronized+Node实现,同样也加入了红黑树,避免链表过长导致性能的问题。 1.7分段锁 从结构上说,1.7版本的ConcurrentHashMap采用分段锁机制,里面包含一个Segment数组,Segment继承与ReentrantLock,Segment则包含HashEntry的数组,HashEntry本身就是一个链表的结构,具有保存key、value的能力能指向下一个节点的指针。 实际上就是相当于每个Segment都是一个HashMap,默认的Segment长度是16,也就是支持16个线程的并发写,Segment之间相互不会受到影响。 put流程 其实发现整个流程和HashMap非常类似,只不过是先定位到具体的Segment,然后通过ReentrantLock去操作而已,后面的流程我就简化了,因为和HashMap基本上是一样的。 计算hash,定位到segment,segment如果是空就先初始化 使用ReentrantLock加锁,如果获取锁失败则尝试自旋,自旋超过次数就阻塞获取,保证一定获取锁成功 遍历HashEntry,就是和HashMap一样,数组中key和hash一样就直接替换,不存在就再插入链表,链表同样 get流程 get也很简单,key通过hash定位到segment,再遍历链表定位到具体的元素上,需要注意的是value是volatile的,所以get是不需要加锁的。 1.8CAS+synchronized 1.8抛弃分段锁,转为用CAS+synchronized来实现,同样HashEntry改为Node,也加入了红黑树的实现。主要还是看put的流程。 put流程 首先计算hash,遍历node数组,如果node是空的话,就通过CAS+自旋的方式初始化 如果当前数组位置是空则直接通过CAS自旋写入数据 如果hash==MOVED,说明需要扩容,执行扩容 如果都不满足,就使用synchronized写入数据,写入数据同样判断链表、红黑树,链表写入和HashMap的方式一样,key hash一样就覆盖,反之就尾插法,链表长度超过8就转换成红黑树 get查询 get很简单,通过key计算hash,如果key hash相同就返回,如果是红黑树按照红黑树获取,都不是就遍历链表获取。 volatile原理知道吗? 相比synchronized的加锁方式来解决共享变量的内存可见性问题,volatile就是更轻量的选择,他没有上下文切换的额外开销成本。使用volatile声明的变量,可以确保值被更新的时候对其他线程立刻可见。volatile使用内存屏障来保证不会发生指令重排,解决了内存可见性的问题。 我们知道,线程都是从主内存中读取共享变量到工作内存来操作,完成之后再把结果写会主内存,但是这样就会带来可见性问题。举个例子,假设现在我们是两级缓存的双核CPU架构,包含L1、L2两级缓存。 线程A首先获取变量X的值,由于最初两级缓存都是空,所以直接从主内存中读取X,假设X初始值为0,线程A读取之后把X值都修改为1,同时写回主内存。这时候缓存和主内存的情况如下图。 线程B也同样读取变量X的值,由于L2缓存已经有缓存X=1,所以直接从L2缓存读取,之后线程B把X修改为2,同时写回L2和主内存。这时候的X值入下图所示。 那么线程A如果再想获取变量X的值,因为L1缓存已经有x=1了,所以这时候变量内存不可见问题就产生了,B修改为2的值对A来说没有感知。 image-20201111171451466 那么,如果X变量用volatile修饰的话,当线程A再次读取变量X的话,CPU就会根据缓存一致性协议强制线程A重新从主内存加载最新的值到自己的工作内存,而不是直接用缓存中的值。 再来说内存屏障的问题,volatile修饰之后会加入不同的内存屏障来保证可见性的问题能正确执行。这里写的屏障基于书中提供的内容,但是实际上由于CPU架构不同,重排序的策略不同,提供的内存屏障也不一样,比如x86平台上,只有StoreLoad一种内存屏障。 StoreStore屏障,保证上面的普通写不和volatile写发生重排序 StoreLoad屏障,保证volatile写与后面可能的volatile读写不发生重排序 LoadLoad屏障,禁止volatile读与后面的普通读重排序 LoadStore屏障,禁止volatile读和后面的普通写重排序 那么说说你对JMM内存模型的理解?为什么需要JMM? 本身随着CPU和内存的发展速度差异的问题,导致CPU的速度远快于内存,所以现在的CPU加入了高速缓存,高速缓存一般可以分为L1、L2、L3三级缓存。基于上面的例子我们知道了这导致了缓存一致性的问题,所以加入了缓存一致性协议,同时导致了内存可见性的问题,而编译器和CPU的重排序导致了原子性和有序性的问题,JMM内存模型正是对多线程操作下的一系列规范约束,因为不可能让陈雇员的代码去兼容所有的CPU,通过JMM我们才屏蔽了不同硬件和操作系统内存的访问差异,这样保证了Java程序在不同的平台下达到一致的内存访问效果,同时也是保证在高效并发的时候程序能够正确执行。 原子性:Java内存模型通过read、load、assign、use、store、write来保证原子性操作,此外还有lock和unlock,直接对应着synchronized关键字的monitorenter和monitorexit字节码指令。 可见性:可见性的问题在上面的回答已经说过,Java保证可见性可以认为通过volatile、synchronized、final来实现。 有序性:由于处理器和编译器的重排序导致的有序性问题,Java通过volatile、synchronized来保证。 虽然指令重排提高了并发的性能,但是Java虚拟机会对指令重排做出一些规则限制,并不能让所有的指令都随意的改变执行位置,主要有以下几点: 单线程每个操作,happen-before于该线程中任意后续操作 volatile写happen-before与后续对这个变量的读 synchronized解锁happen-before后续对这个锁的加锁 final变量的写happen-before于final域对象的读,happen-before后续对final变量的读 传递性规则,A先于B,B先于C,那么A一定先于C发生 说了半天,到底工作内存和主内存是什么? 主内存可以认为就是物理内存,Java内存模型中实际就是虚拟机内存的一部分。而工作内存就是CPU缓存,他有可能是寄存器也有可能是L1\L2\L3缓存,都是有可能的。 说说ThreadLocal原理? ThreadLocal可以理解为线程本地变量,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离,相比于synchronized的做法是用空间来换时间。 ThreadLocal有一个静态内部类ThreadLocalMap,ThreadLocalMap又包含了一个Entry数组,Entry本身是一个弱引用,他的key是指向ThreadLocal的弱引用,Entry具备了保存key…

摩登3平台首页_实战篇:一个核心系统3万多行代码的重构之旅

经典著作《重构》这本书中有这么一段话: 一开始,我所做的重构都停留在细枝末节上。随着代码趋向简洁,我发现自己可以看到一些设计层面的东西了,这些是我以前理解不到的,如果没有重构,我达不到这种高度。 重构,着实是一件让程序员兴奋的事情。 今年年初,我们团队完成了一个复杂项目的重构工作,它属于广告系统最核心的引擎部分,大概有 300 多个文件,3 万多行代码。 从技术方案设计到最终全量上线仅仅花了 1 个月左右的时间,而且没有产生事故。 这应该是我 8 年程序生涯中,经历过的最大型的同时最成功的一次重构项目:速度足够快、计划比较周全、质量过关。 01 先聊聊这个系统的历史包袱 我们的广告引擎在这次重构前大概经历了1年半时间的迭代,初期针对的是搜索场景,业务单一,流程清晰。 2019年开始,公司的广告业务开始快速扩张,收入几乎是指数级的增长。在这个过程中,我们的广告引擎面临了两个挑战: 1、业务场景开始变得复杂,除了搜索广告,还需要支持信息流推荐以及相似推荐场景。 2、广告流量开始快速增加,除了满足功能性需求,还需要兼顾好性能。 经过梳理,整个引擎有大部分逻辑是可以公用的,因此我们定义了一个主体框架,同时将可扩展部分进行了抽象。这样,各个场景能够根据自身业务的特殊性实现某些公共接口即可。另外,从性能角度考虑,我们牺牲了一些代码可读性,把某些逻辑并行化了。 随着业务的发展,搜索场景开始进入快速迭代期,新增策略越来越多,我们的主体框架也是在这个时候逐渐变得不灵活。 如果动主体框架,搜索以外的场景都需要跟着重构。 在业务的快速发展期,工期根本不允许,因此我们只能在现有框架上进行补丁式的开发。 这样,带来了两个很明显的问题: 1、为了兼容搜索的特殊逻辑,我们需要在其他场景中增加各种 if 判断来绕过这些逻辑。 2、广告策略越来越多,累计了几十个,当框架失去清晰的结构后,有些策略的实现开始变得定制化,缺少层次化的划分和可插拔式的抽象设计。 在这样的背景下,随着改动的积累,代码开始偏离了设计的初衷,技术债务越来越重。但是,我们又始终找不到合适的时机进行重构。 转机出现在 2019 年年底,由于广告业务的特殊性,流量开始自然走低,另外产品运营团队将重心放在了第 2 年的工作规划上,因此给了我们非常好的窗口期开始此次重构。 我们将工期定成了 1 个月,最终仅比预期晚上线了一天,虽然出现了两个线上问题,但是在灰度期都及时发现和修复了,并没有造成线上事故。 总体来说,这是一次难度颇大并且比较成功的重构项目,下面详细说一下我从这个项目中吸取到的宝贵经验。 02 重构前,我们做了哪些准备工作? 这次重构的代码量很大,3 万多行,而且是广告系统最核心的引擎部分。启动前,我们能预期到下面这些困难: 1、业务侧的阻力:广告是极其以业务为导向的,本次重构虽然能带来长期研发效率的提升,但是没法直接提升业务收益,而且开发周期不会太短,如何才能得到业务同学的支持? 2、技术侧的顾虑 :重构一旦引起线上事故,公司是有处罚制度的,如何让大家轻装上阵?同时,重构过程中如果还有非常重的业务迭代穿插,交付时间没人敢保证,质量也很难得到控制。 针对这两方的顾虑,我认为下面这几项工作起到了很关键的作用。 ▍让所有人看到痛点 前面提到:随着业务迭代,我们广告引擎的主体框架已经变得模糊不清,另外几十个广告策略散落在不同的业务场景中,配置凌乱。 针对这两个痛点,我们提前1个月启动了现有业务的梳理,走读旧代码、同时翻阅以前的需求文档,最终我们将不同场景的核心流程以及广告策略归类成了一张清晰的表格。 正是这一张表格,让技术和产品第一次很清晰地看到了我们引擎部分的全貌,体会到了业务的复杂度以及当前技术上的瓶颈。 ▍明确重构的目标和价值 让所有人感受到痛点后,我们规划了本次重构的两个核心目标: 1、主体框架的重构:将主流程模块化,重新定义上下层协议,确保接口清晰;各层级内部也需要做好抽象,具备良好的扩展性。 2、策略灵活可配置:将广告策略按照业务意图进行归类抽象,策略的执行条件动态可配置,同时策略可任意插拔。 此外,我们将这两个核心目标完成后可带来的预期收益进行了细化: 1、技术收益:代码结构更清晰,更容易理解和维护;可扩展性增强,引擎的开发效率将进一步提升。 2、业务收益:策略能做到更细粒度的配置和扩展,对业务支持更友好;研发提效后能进一步加快业务的迭代速度。 将重构的价值同步给大家后,进一步提升了所有人的兴奋度,让大家有了更强的动力参与进来。 ▍整体节奏的把控 整体节奏的把控也是非常重要的一环,能让所有人对这件事情有一个时间上的预期。 首先,我们将工期定成了 1 个月,一方面考虑了业务侧可以接受的最大周期,技术上也希望速战速决;另一方面,春节即将来临,我们必须赶在公司封网前上线,同时预留出1-2周的 buffer 以防意外情况发生。 此外,我们和业务侧达成了一致:重构期间,引擎部分的非紧急需求一律不接,这样可最大限度地减少并行开发和代码冲突,让团队精力更集中。 03 执行过程中有哪些可分享的经验? 这次重构能够实施得如此顺利,有 4 点我认为很有价值的经验跟大家分享下。 这一点得益于日常的要求,针对开发周期超过3天的项目我们都会进行技术方案设计,本次重构当然也不例外。 框架部分的整体架构、模块之间的协议设计、以及策略的可扩展性设计是本次技术方案的重点,团队前后讨论了不下3次。 在大方案定稿后,团队进一步对数据库、接口字段、缓存结构、日志埋点等公共部分进行了细化,因为涉及到多人协作开发,团队约定以文档作为沟通界面,文档始终保持和代码同步。 在这样的高要求下,团队产出了 5000 多字的技术方案文档,合计 36 页,这些为整体质量的保障打下了很好的基础。 2. 预重构出框架性代码 这一个 PR 非常关键,是我们从技术方案落地到代码最重要的一步。我们把重构后的包结构、模块划分、各层之间的API定义、不同广告策略的抽象进行了梳理,先忽略实现的细节。 这样主体代码基本成型,能很清楚地描绘出我们理想中的框架。然后,我们组织了多次集中代码审查,最终形成了统一意见。 这一步能很好地避免过早陷入实现细节,导致主体框架关注不够、代码不稳固,后期再返工反而会拖累效率。 3. 频繁沟通和成对代码 Review 机制 进入到细节实现阶段后,很重要的一点是:对现有逻辑的理解。引擎代码经过一年半的迭代,历史上被很多人开发过,但是本次只有 3 个同学参与重构。 整个过程中,我们遇到任何代码逻辑不明确的地方,都是反复沟通和求证,不主观猜想,这一份谨慎其实很关键。 另外在代码审查上,我们按模块分配了对这块业务比较熟悉的同学来负责,成对搭配,机制灵活。 4. 有效的测试方案 重构未动,测试先行。这个原则是《重构》一书中重点强调的,也是我们本次技术方案讨论的重点,我这里单独拎出来详细展开下。 首先,我们前期便约定好:不动任何老代码,完全建新的 package 进行重构。这样方便比对重构前后的结果,同时进行线上灰度实验。 测试方案上,以下 4 点值得借鉴: 1、端到端测试:本次重构不涉及功能性的调整,因此外层API的行为是不会有任何变化的,这样端到端的测试方法最为有效,这个是研发和QA测试最主要的手段。 2、冒烟测试:QA同学提供冒烟 Case,由研发同学进行冒烟,研发提测前必须保证所有冒烟 Case 执行通过。这一点在大部分互联网公司都不常见,但是对于大型项目绝对有效。 3、沙箱环境双流程验证:前面提到我们重构前后的代码都保留了,因此可以通过脚本抓取线上环境的入参作为case,然后用自动化的方式对 API 的返回字段进行逐一比对。 4、线上环境灰度实验:灰度对于重构非常重要,我们利用已有的ABTest平台,逐步放开灰度流量,从5%、到10%、到30%、最后到100%,制定了很谨慎的放量节奏,然后通过日志以及业务指标监控进行验证。 写在最后 回顾整个重构的过程,总结成下面 7 个关键点: 1、把握好重构时机 2、前期梳理很重要,先找到痛点 3、明确出目标和价值,让大家兴奋起来…