Showing Posts From
Others
一线大厂程序员的考公上岸心得
个人介绍 1、简单介绍下自己吧 大家好,我叫张安宇,我以前是一名普通的 iOS 程序员。感谢飞哥再次邀请对我进行采访,本来公务员相关的话题是比较敏感的,不过我接受本次采访的主要原因是最近有不少跟我年龄相仿的同行向我请教经验:如何刷题、如何在职复习、如何准备面试、如何报考等等。我知道最近整个互联网行业非常不景气,大家也有比较严重的年龄焦虑,我能在接近 35 岁之前考上编制我很庆幸,所以想通过访谈的形式给大家分享一些我的上岸经验。但是我需要提前告诉大家的是他人的经验仅供参考,我的经历还稍微有些「传奇」,可能不太通用,也许我能通过这种方式上岸,换个人用同样的方式不一定能行得通,所以整篇文章仅供参考哈。 2、你目前考到了哪里,在什么单位,工作日常和主要职责是什么? 我考到了四川省自贡市政数局:自贡市政务服务和数字化管理局是中国四川省自贡市的政府机构,负责提供政务服务和推进数字化管理。该局的主要职责包括政务服务平台建设、政务数据管理、数字化办公和信息化建设等。我们单位的主要职责包括:规范和推动政务服务标准化,管理和利用政务数据资源,推进电子政务建设,指导市政务服务大厅的规划和管理,简化审批流程并推进协同审批,管理政务服务热线平台,推进政务诚信体系建设,组织行政审批制度改革,管理公共资源交易活动,指导惠民帮扶工作,安全生产和职业健康、环境保护等工作,以及推进政府信息公开工作等。 我的单位是一个市属事业单位,截止今天我已经入职一个多月了吧,通过一个月下来的了解,我的日常工作目前还比较简单,处理的是一些基本的工作流程类的文件、公文等,协助软件公司完成我们系统平台的研发。 我的岗位叫做软件工程、大数据分析,所以闲暇也会自己做一些技术方面的学习与研究,比如我正在做的是对自贡市的一些数据进行挖掘分析、数据清洗、人工标注等等的工作,最耗费我精力的就是新词的挖掘和统计,大量的新词需要我一条条地手动确认并保存,这个过程是机器无法替代的。我认为这份工作挑战还是非常大,NLP 、机器学习、数据挖掘等工作是比较有技术难度的领域,没有很好的技术基础我认为做不好这件事,希望以后能够学习更多的技术更好地完成我的工作。 进入这样的部门工作,我认为最大的职责就是要把每件事办好,让每件事都切实有着落,在做任何事情之前优先考虑的是人民群众的利益,要站在最普通老百姓的角度上去思考所问题,真正做到“权为民所用、利为民所谋”,我认为是这样的。为什么选择考公 1、为什么选择了考公这条路?是为了逃离互联网吗? 逃离互联网说不上,我的经历比较曲折,虽然进过很多大公司,拿着很高的薪水,但是待的时间都不长,经常搬家,也没有什么积蓄。在北京这样的城市根本不可能扎根,与其说逃离互联网,不如说我逃离了大城市。有人说在北京没有房产就根本无法立足,我觉得是的,我不想租一辈子的房子,如果我的工作稳定了希望尽快在自贡买房定居下来,家人也不希望我继续漂泊在外地。最近在看《曾少年》这部剧,里面有句台词我觉得特别好:越大的城市越容易产生梦想,但是也更容易让梦想破灭。剧中人物的爱情故事就是这样的悲剧,也符合我的想法,我想逃离的是大城市,也许小地方平平淡淡的生活更适合我呢?考公就是这样的另外一条路,另一种可能性。 选择考公最大的动力是我相信自己在很多时候是努力的,只是以前时运不济,总是一事无成,我相信考公上岸这种路径:通过自己的真才实学和踏实的复习,日积月累最终达成自己最初定下的目标,所以我下定决心准备试试。 2、除了考公你还想过其他途径吗?你怎么看待程序员和公务员这两个身份? 有的,在考公的同时我也有一些出国工作机会,对于程序员来说润出去确实是一个很好的选择。毕竟我也习惯了高薪的工作,突然进入体制内拿很低的薪水,我要经历一个很痛苦的消费降级的过程,曾经随随便便就能购买消费的东西,现在只能远远的看着,已经买不起了。这种感觉真的很难受,考公并不是适合所有人,我也经常问自己,如果我没考上,接下来的人生会在失业中度过吗?我想即便找不到一份高薪的工作,能够找一份普普通通的工作维持生计应该也不难。 所以吧,程序员这个身份,在我眼中是高薪和容易失业的,公务员在我眼中是稳定,不会让我暴富,但是也不会让我饿死的这样一份工作。如果我考不上编制,我可能会折衷一点,选择做普通的相对稳定一些的工作,放弃高薪,这也是符合我的一贯想法的,我不喜欢卷,不希望自己人生活的太累,我希望我既能够生活下去,也能随时享受生活本身。 回望之前的程序员身份,我应该是感激多一些,虽然我上岸了,但是我在政数局这样一个单位,总体上来说跟信息化、数字化的联系程度应该高于体制内的大多数单位,曾经的程序员经历给了我一个很好的经验,我希望能够把这些经验沉淀利用到日常的工作中来,为自贡市,为国家和人民做出我能做出的贡献。 3、考公好像是大多数互联网从业者都曾幻想过的发展路线,什么样的人更适合考公?什么样的人不适合?你会鼓励大家考公吗? 我不会无条件鼓励大家考公,我考公有一个原因是我的原生家庭吧,因为我父母是公务员,考公这件事不仅仅关系到个体,还应该听取父母、家族长辈们的建议,这条路是否适合自己需要好好考虑,不应该盲目地跟风,因为考公热现象只是最近这些年才热起来的。 像我一样父母都在体制内的朋友,我认为是很适合考公的,公务员是一个身份,也是一个家庭的传承。如果家人没有在体制内,很多问题你不知道向谁请教经验,特别是职场上的人情世故,肯定不能借鉴体制外的那些经验,向外人你又不好开口问,父母无疑是最好的老师。我的父母给我提供了很多的上岸后如何做人做事的经验。 我认为不适合考公的人群,其实没有,我觉得任何人想考都能考,我们国家的公务员考试制度不同于古代科举,没有限制谁谁谁不能参加考试,这个岗位专业不符合要求,总有岗位你能报;这个岗位太卷,总有岗位不卷;这个岗位要求学历,总有公益性质的岗位它要求很低。如果特别想考公,我认为不妨一试,人生没有什么不可能,没有哪条路是一定不能走的对吧。 上岸并不是终点,它仅仅是一个起点,考上了只是代表自己进入这个体制内,能不能做好,还得持续不断地耕耘才可能有收获。 如何准备考公 在本篇开始之前就一些概念补充一些相关背景知识:类别 考试 岗位 考试时间 考试内容 其他说明公务员 国考 受中央领导:税务局、国家部委、国家统计局、外交部、银保监会、海关总署 11月份 行测、申论 应届毕业生有一定优势省考 受地方政府领导:各地教育、公安、商务 联考4月份前后,自主考试时间看各地安排 行测、申论 一般有户籍限制,灵动性比较强,部门之间转换事业编 事业编考试 各研究院、管理局、信息中心、办公室 各地自行安排,考试次数比较多,考试时间比较分散 职测(类似行测)、公共基础知识,有些地方还会有专业课,具体以招聘公告为准 要求非常灵活,以当地公示为准国考、省考、事业编考试这三者,整体的招聘岗位数量依次减少,福利待遇相差不大,依次略微降低,但都有编制,上岸难度系数依次降低。因为考公和考编需要准备的内容相似,通常是同时准备,考上哪个是哪个,下文不再额外区分,统称考公。 1、考公的整个流程会是什么样的,可以大概简述下吗? 公务员考试和事业单位考试的大致流程基本一致,都是报名、笔试、资格审查、面试、政审、公示、正式入职,流程很长我从报名到入职只经历了 5 个月。很多朋友对我说我是“肉眼可见的上岸最快速度者”,我实在觉得自己是太走运了。我复习了很久的事业单位 abcde 类《职测》与《综应》四川省并不考;自贡市市属事业单位实际上是我参加的第三场考试,之前还参加了 23 年省考和同期的四川泸州的事业单位考试,后面又参加了三场考试,一共参加了 6 场笔试,进面一次,上岸一次。5 个月的时间走完入编的所有流程,我认为这个不容易,这个流程很长,这中间每个环节都可能淘汰掉我,也许别人好几年也走不完这个流程,这个结果也远超我自己在备考期间的时间预期。 我的这份幸运并不是特例,很多考公上岸的幸运儿都有类似的经历:对手没来参加面试,报考岗位竞争非常小,因此考公之路,有前人总结的是选择大于努力,岗位选得好才是事半功倍。我觉得我的幸运也来自于我的岗位选的不错,我总结的经验是:我会提前报名我感兴趣的城市,然后关注报名交费数据,大部分城市都会统计公布,如果竞争比超过了1:1000,我是一定不会缴费参考的,如果竞争比小于 100 我认为是 ok 的,500 左右我都会考虑下 ROI 的问题(主要是巡考路费,和其他考试撞时间的机会成本损失等)。 选择了考公的道路,基本上是要与以前的自己一刀两断了,要时刻以公务员的标准来要求自己,即使是在备考阶段。入职以后更是需要谨言慎行,认真工作,我认为这是一个改变自己的过程,也是一种锻炼自己的机会,去学习适应新的身份,至少不再是一个程序员了,要以更加系统的思维去全局化地思考问题,站在国家的立场和角度去考虑普通的民生问题,我以后的一言一行要代表得了我的单位,需要对得起党和国家,这是一个很基本的要求和标准,我必须要去做到,这对我来说其实还挺难的,毕竟以前在微软这样的外企工作,我的思想非常open的,上岸以后,我对自己的性格做了很大的收敛,我也相信身边的朋友能够看出来一些,在这方面我还需要再加强。 2、考公是否存在年龄、考试次数、户籍之类的限制;考公的公平性如何? 绝大部分岗位限制年龄 35 岁(含)以下,也就是说你可以考到 36 岁生日的前一天为止,应届研究生、博士放宽到40岁。考试次数当然没有限制,户籍限制是有的,在某些地区的事业单位招聘中明确限制了当地的户籍。 如果报考条件都符合,最终结果一定不会因为这些条件导致取消录用的,无论是公务员还是事业单位的录用,都是参照《公务员法》执行的。如果要在录用阶段取消考生的录用资格,一定是要有足够充分的理由才行。大家想象中的黑幕、暗箱操作,我个人感觉是没有的,我觉得我们国家目前的公务员考试制度相当公平。以我自己为例,我考到自贡来之前,我一个自贡人都不认识,如果不是有绝对公平的考试制度,我又为啥能考进来呢?我这个单位并不差,我认为是一个很好的单位,肯定还有很多人也想进来;假如真有这样那样的黑幕,我应该被刷掉才对。 3、不同地区的考试是否是独立命题?计算机岗位这个方向难度和竞争大不大?很多时候这些信息差会导致判断失误,有没有一些靠谱的可以获取这些信息的途径? 公务员考试的话是每个省一套题,事业单位的话基本上是每个市一套题目,题目难度不一,比如我考过重庆市市属事业单位,我考的那套题最低进面分数是 180 分,而同期举办的区县里的进面分数竟然在 230-250,甚至有区县的进面分高达 280(满分300),那我觉得难度应该是不一样的。最近丰都和南川的高分考生还被查出来集体作弊被取消了成绩,也是解答了我一个疑惑,这些人怎么考到这么离谱的高分的,是我学习方法不对还是我不是那块料? 计算机岗位目前的竞争我觉得中规中矩,我们国家目前正在加快数字化,加快数字经济与实体经济的融合是非常重要的国家战略,因此各个部门也亟需各种各样的计算机专业人才,可以肯定地说计算机专业和其他专业对比在公务员考试中是更吃香的;但整个互联网行业低迷的就业环境导致找工作的人很多,所以竞争也很大,所以只能说比其他专业好很多,但是还是需要付出足够的努力才能上岸。 考公时刻关注各大机构的消息,具体什么机构我就不说了,我相信一个人真正进入了一个备考的状态之后,他会重新建立一个获取备考信息的网络:比如关注一些半月谈公考之类的公众号,亦或是下载学习强国 APP,他会每天自然而然地会关注到所有该注意到的信息和细节,否则我觉得可能这个人备考是还没有进入状态的。4、计算机岗位的考试主要考哪些内容?你是如何准备考试的?是否有必要报班? 计算机岗位的考试题目跟非计算机岗位的考试题目是一样的,只有极少数地区会在面试过程中出专业题(比如我报考的四川自贡市,这在四川都是独一无二的存在),但笔试阶段是不需要准备任何计算机专业知识的。 我报了面试班,但是笔试是全靠自己自学的,我也是这样给其他向我咨询的朋友说的,如果你有基本的自律,其实没必要报笔试班;但是如果有幸获得了面试资格,请不要为了省钱自学,最好至少报一次面试班系统地学习好结构化面试。 5、有过多少次考公经历?备考过程踩过哪些坑?考试的心境有哪些变化? 我今年 33 岁,这是我唯一一次备考公,一共参加了 6 场笔试,从开始备考到入职一共持续了 6 个月左右,其中我当前岗位的录用流程就花了 5 个月,也就是说我就额外多复习了一个月的时间,这也是我为什么觉得自己运气足够好的原因,感觉有点像第一次买彩票就中了大奖一般,哈哈。 踩过哪些坑?我觉得最坑的地方是常识模块,常识模块的知识点实在太多了,即便我刷再多题听再多课,真的找不到什么提分技巧,真的看运气,运气不好常识模块的准确率就只有 40%,非常搞人心态。 我一共刷了 3000 题,总正确率是 70%左右,其实我刷题的数量不多,相比很多比我卷的局长们,我这 3000 题仅仅是刚刚入门而已。备考阶段我的心境其实很平淡,某一两次考试失利我也不会特别难受,只是觉得可能自己还不够努力,只要继续坚持下去总能考上自己喜欢的岗位。我给自己预想的时间是考两年,实际上岸的进度条实在是走的太快了,远超了我的预期,以至于我买的大量复习资料、文具等等,还没派上什么用途就开始在家吃灰了,不知道如何处理。 6、面试环节是什么样的,需要注意哪些事情 如果有幸进入了面试环节,那基本上离上岸只有一步之遥了。如果你的岗位排名大于招聘名额,例如招 1 排 2,招 2 排 4 等,就属于入大围,反之假如招 1 排 1,招 2 排 2 就属于入小围。一般入大围上岸几率会小很多,你需要面试超对手足够多的分数才能逆袭上岸,而有些地区的面试打分分差它是很小的,比如某些地区会在 82-85 之间打分,很可能整个考场的最大分差只有 3 分,这种情况下如果你在大围,明确分差大于这个区间,可以考虑放弃面试。具体打分标准可以查询对应地区历年面试的打分情况来看。我所在的面试组打分区间很大,就我同一个组的考生分差就有 10 分以上,所以情况各不相同。往往分差越大的地方,守擂的不确定性就高,逆袭的成功率也就更高。 我的一些建议是面试阶段尽量要报面试班,我推荐我的面试老师给大家,这里就不打广告了,如果有需要,可以通过飞哥联系我咨询。面试过程中要自信、声音洪亮、衣着得体。差不多就这些。 体制内工作感受 1、你现在应该已经上了一段时间班了,当前岗位的福利待遇、工作状态、工作环境怎么样的,跟你的预期是否相符? 我的福利待遇肯定跟以前打了骨折了,工资连以前的零头都没有了,整个四川的待遇都差不太多哈。工作状态的话我觉得我是非常认真的一个工作状态,会认真地做好每一件工作相关的事情,不能出什么差池,也出不得什么岔子哈。 我们单位的工作环境还不错,当然了,以前在微软那样的大工位肯定是没有了(图 1 是刚入职微软时候的工位,那会对面坐的可是郭神哈,图 2 是后来公司统一升级的新款升降桌):但是也不算太小,我们毕竟是公家单位,跟微软比工作环境,这样的攀比心是不应该有的:我在互联网公司的体验其实还挺好的,过去的几家公司领导都待我不错,压力不算太大,虽然经常公司要求我加班,但是我大部分时候都是主动拒绝的,包括我换工作我的第一号诉求就是不接受加班。 我们单位不考勤,没有过于严厉的规章制度,所以我很庆幸每天上班的几个小时我可以相对自由地安排自己的时间,哪两个小时认真工作,哪两个小时写写代码(基本上已经是写着玩玩了),哪两个小时娱乐休闲、或者学习充电提升自己。 食堂自然是没有微软的食堂好吃,不过与之相应的是更轻松的工作氛围,更 WLB 吧我觉得。绝对的 955,没有周报月报和开不完的 scrum,没有末尾 5% 的 LITE(Low Impact Than Expect) 淘汰机制,不用写 connect,下班时间没有任何人在 Teams 上 ping 我(也没有 Teams,我们工作就用微信)。 所以,我写这些在这里只想说可能每份工作都有优缺点,我觉得在体制内工作会失去一些东西,但是也会得到很多东西,我相信这一点。 虽然微软食堂也能吃一辈子,那也能算“铁饭碗”,但是能在微软这样的公司待一辈子的人也是少数,我觉得哈,每年 5%的 LITE 淘汰,你必须确保你不是每年的那个 5%,遇上经济不好再来一个额外的 全员10%裁员计划,在人生大事上不能用幸存者偏差去看问题,毕竟默默离开的人们不说话,不能忽略他们的感受,所谓稳定就是要绝对的稳定,不容许突如其来的变故,这些 5%、10%的厄运降临到每个人头上都是你的 100%,你都很难去承受。 我在互联网公司最大的缺失自然是集体认同感和稳定性,但我相信我现在的单位以后会给到我的。 2、公务员是否是你的理想职业,它有没有让你感觉有待提升的点?这里的关系是更复杂还是更简单?这是否会是你的最后一站? 我小时候的理想职业是成为一名计算机科学家,所以我的儿时理想跟公务员没有关系。我热爱计算机,大学志愿填报我根本没有考虑过其他任何专业。 现在我的理想首先是能活下去,其次是能给国家给社会做点贡献,成为一名公务员是给了我这样的机会,我应该好好干。我认为体制内的关系比互联网的职场相对简单一点,也许我是一个简单的人吧。 我希望这份工作是我最后一站,这代表我后面不会再准备遴选、考省直或者中直的公务员等等,我很喜欢自贡这个城市,愿意一直生活在这里,自贡和我有着解不开的缘分。自贡是一座千年盐都,我的家乡巫溪县也是一座上古盐都,这是两座以盐文化为傲的城市,除此之外自贡也比我的家乡发达很多,盐、灯、恐龙、川菜都是世界闻名的,我相信这里能成为我的第二故乡。有句话说的好,别问我是哪里人,我的编制在哪里,我就是哪里人。我本就出生在直辖之前的重庆,那会我是四川人,现在我还是四川人,我的编制在四川,我会觉得哪怕从此以后一辈子在自贡也是一件很开心的事情(第一次报名的时候最担心的就是如果考上了就要去一个自己不喜欢的地方生活一辈子,内心会产生巨大的陌生感,那种真实的感受非常令人窒息)。 3、很多时候观察到的现象是,不少政务平台都比较难用,这里的底层逻辑是什么呢 这个说法不太对,我们的政务平台正在以更高的水平、更高的质量发展与建设,我们在这方面做出了很多的努力,我们的一网通办只会越来越方便快捷,特别是我所在的川渝地区,重庆的渝快办已经和我们的政务服务互联互认了,我觉得不会是难用的一个状态。 我们还在配合四川省大数据中心建设打造“无证明城市”,也许以后你到自贡来旅游,可以不带身份证就能到我们局里来办理业务,我们单位的同事们做了很多努力,这些都是可以预见的未来。 至于说为什么有些政务平台大家认为缺乏维护,我想可能存在一些历史因素以及财政紧张的原因,如果大家对我们的政务云、政务平台有任何相关的建议或意见,可以随时拨打我们的 12345 市民热线留下大家的宝贵意见与建议,在大家的监督和促进下我们一定可以做的更好的。 4、最后可以再补充一些你想说的话 最后的最后,欢迎大家到自贡来旅游哦!欢迎大家来我们自贡吃兔子!自贡的夜景非常漂亮:自贡是一座生机勃勃的小城市,自贡的灯会举世闻名,自贡的盐帮菜是川菜的八大菜系之一,自贡的恐龙博物馆是全球最大的三家恐龙博物馆之一(另外两家在加拿大和美国),自贡的井盐更是有几千年的历史。 自贡缺乏的是一个历史机遇,在当代沿海城市已经很发达的背景下,我们的中西部城市也需要快速崛起,也许《重庆四川携手推出多项新举措推进成渝地区“中部崛起”》就是这样的历史机遇。 扩展 考公三大机构:华图在线、中公教育、粉笔教育 考公论坛:https://bbs.qzzn.com/ 程序员考公指南:https://github.com/coder2gwy/coder2gwy
《程序员的自我修养(链接、装载与库)》学习笔记三(装载和动态链接)
继续学习《程序员的自我修养 - 链接、装载与库》的第三个大的部分,前两篇内容参见: 《程序员的自我修养(链接、装载与库)》学习笔记一(温故而知新) 《程序员的自我修养(链接、装载与库)》学习笔记二(编译和链接) 这一篇包含本书的六、七章节,这两个章节中作者给我们讲解了可执行文件的装载以及动态链接的过程,操作系统是如何将程序装载到内存中运行,如何为程序的代码、数据、堆、 栈在进程地址空间中分配,分布。动态链接是如何有效的利用内存和磁盘资源,如何让程序代码的重用变得更加可行和有效等等。 经过上面几个章节章节的学习,我们已经知道了什么是可执行文件,以及可执行文件的静态链接过程,下面我们思考几个问题:为什么有了静态链接,还需要动态链接?静态链接和动态链接有什么区别呢? 可执行文件只有被装载到内存以后才能被 CPU 执行,装载的基本过程是什么样的呢? 共享对象根据模块位置和引用方式的不同分为:模块内跳转、模块内数据访问、模块外跳转、模块外数据访问,这四种类型的寻址方式有何不同? 装载时重定位和地址无关代码是解决绝对地址引用问题的两个方法,这两种方式的利弊都是什么?下面我们带着这些问题进行第六七章节的学习。首先看下这两个章节的知识点分布:可执行文件的装载 装载的方式全部载入内存程序执行时所需要的指令和数据必须在内存中才能够正常运行,最简单的办法就是将程序运行所需要的指令和数据全装入内存中。根据局部性原理进行载入程序运行时是有局部性原理的,所以可将程序最常用的部分驻留在内存中,不太常用的数据存放在磁盘里面,这就是动态装入的基本原理,**覆盖装入(Overlay)和页映射(Paging)**是两种很典型的动态装载方法。 覆盖装入 一个程序有主模块 main,main 分别会调用到模块 A 和模块 B,但是 A 和 B 之 间不会相互调用,这三个模块的大小分别是 1024 字节、512 字节和 256 字节。假设不考虑内存对齐、装载地址限制的情况,理论上运行这个程序需要有 1792 个字节的内存,当采用内存覆盖装入的办法,会按照下图的方式安排内存,我们可以把模块 A 和模块 B 在内存中相互覆盖,即两个模块共享块内存区域,除了覆盖管理器,整个程序运行只需要 1536 个字节,比原来的方案节省了 256 字节的空间。在多个模块的情况下,程序员需要手工将模块按照它们之间的调用依赖关系组织成树状结构。例如下图,模块 main 依赖于模块 A 和 B,模块 A 依赖于 C 和 D,模块 B 依赖于 E 和 F,则它们在内存中的覆盖方式如下图:覆盖管理器需要保证两点:这个树状结构中从任何一个模块到树的根模块都叫调用路径。当该模块被调用时,整个调用路径上的模块必须都在内存中。 禁止跨树间调用。任意一个模块不允许跨过树状结构进行调用。覆盖装入的方法把挖掘内存潜力的任务交给了程序员,程序员在编写程序的时候必须手工将程序分割成若干块,然后编写一个小的辅助代码来管理这些模块何时应该驻留内存而何时应该被替换掉,一旦模块没有在内存中,还需要从磁盘或其他存储器读取相应的模块,所以覆盖装入的速度肯定比较慢,不过这也是一种折中的方案,是典型的利用时间换取空间的方法,现在已经几乎被淘汰了。 页映射 页映射是将内存和所有磁盘中的数据和指令按照页**(Page)**为单位划分成若干个页,装载和操作的单位就是页。假设我们的 32 位机器有 16 KB 的内存,每个页大小为 4096 字节,共有 4 个页,假设程序所有的指令和数据总和为 32 KB,那么程序总共被分为 8 个页。我们将它们编号为 P0~P7。16 KB 的内存无法同时将 32 KB 的程序装入,于是我们将按照动态装入的原理来进行装入。如果程序刚开始执行时的入口地址在 P0,这时装载管理器发现程序的 P0 不在内存中,于是将内存 F0 分配给 P0,并且将 P0 的内容装入 F0,运行一段时间以后,程序需要用到 P5,于是装载管理器将 P5 装入F1,当程用到 P3 和 p6 的时候,它们分别被装入到了 F2 和 F3,映射关系如下图:但如果这时候需要访问第 5 个页,那么装载管理器必须做出抉择,它必须放弃目前正在使用的 4 个内存页中的其中一个来装载新的页。至于选择哪个页,我们有很多种算法可以选择:使用 FIFO 先进先出算法选择第一个被分配掉的内存页。 使用 LRU 最少使用算法选择很少被访问到的页。装载的过程 进程建立创建一个独立的虚拟地址空间。创建一个虚拟空间实际上是创建映射函数所需要的相应的数据结构。 创建虚拟地址实际只是创建页目录,甚至不设置页映射关系,这些映射关系等到后面程序发生页错误的时候再进行设置。读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系。当程序执行发生页错误时,操作系统将从物理内存中分配一个物理页,然后将该缺页从磁盘中读取到内存中,再设置缺页的虚拟页和物理页的映射关系,这样程序才得以正常运行。当操作系统捕获到缺页错误时,它应知道程序当前所需要的页在可执行文件中的哪一个位置。这种映射关系只是保存在操作系统内部的一个数据结构。Linux 中将进程虚拟空间中的一个段叫做虚拟内存区域 VMA,在 Windows 中将这个叫做虚拟段 Virtual Section。将 CPU 的指令寄存器设置成可执行文件的入口地址,启动运行。从进程的角度看这一步可以简单地认为操作系统执行了一条跳转指令,直接跳转到可执行文件的入口地址。页错误 上面的步骤执行完以后,其实可执行文件的真正指令和数据都没有被装入内存中。操作系统只是通过可执行文件头部的信息建立起可执行文件和进程虚拟之间的映射关系而已。 当 CPU 开始打算执行这个地址的指令时,发现是个空页面,于是它就认为这是一个页错误(Page Fault)。 CPU 将控制权交给操作系统,操作系统有专门的页错误处理例程来处理这种情况。操作系统将查询这个数据结构,然后找到空页面所在的 VMA,计算出相应的页面在可执行文件中的偏移,然后在物理内存中分配一个物理页面,将进程中该虚拟页与分配的物理页之间建立映射关系,然后把控制权再还给进程,进程从刚才页错误的位置重新开始执行。 随着进程的执行,页错误会不断的产生,操作系统也会为进程分配相应的物理页面来满足进程执行的需求,如下图所示:缺页本身是一种中断,与一般的中断一样,需要经过 4 个处理步骤: 1. 保护 CPU 现场。 1. 分析中断原因。 1. 转入缺页中断处理程序进行处理。 1. 恢复 CPU 现场,继续执行。 页面错误会降低系统性能并可能导致抖动,程序或操作系统的性能优化通常涉及减少页面错误的数量。优化的两个主要重点是减少整体内存使用量和改进内存局部性。为了减少页面错误,开发人员必须使用适当的页面替换算法来最大化页面命中率。 进程虚存空间分布 ELF 文件的装载 ELF 文件被映射时,是以系统的页长度作为单位的,那么每个段在映射时的长度应该都是系统页长度的整数倍,如果不是,那么多余部分也将占用一个页。一个 ELF 文件中往往有十几个段,那么内存空间的浪费是可想而知的。而操作系统只关心一些跟装载相关的问题,最主要的是段的权限(可读、可写、可执行):以代码段为代表的权限为可读可执行的段。 以数据段和 BSS 段为代表的权限为可读可写的段。 以只读数据段为代表的权限为只读的段。相同权限的段可合井到一起当作一个段进行映射。 比如有两个段分別叫 .text 和 .init,它们包含的分别是程序的可执行代码和初始化代码,并且它们的权限相同,都是可读并且可执行的。假设 .text 为 4097 字节,.init 为 512 字节,这两个段分别映射的话就要占用三个页面,但是如果将它们合并成一起映射的话只须占用两个页面,如下图所示:ELF 可执行文件引入了一个概念叫做 Segment,一个 Segment 包含一个或多个 Section,如果将 .text 段和 .init 段合并在一起看作是一个 Segment ,那么装载的时候就可以将它们看作一个整体一起映射,也就是说映射以后在进程虚存空间中只有一个相对应的 VMA,而不是两个,这样做的好处是可以减少页面内部碎片,节省了内存空间。 Segment 的概念实际上是从装载的角度重新划分了 ELF 的各个段。 Segment 和 Section 是从不同的角度来划分同一个 ELF 文件。这个在 ELF 中被称为不同的视图 View:从Section 的角度来看 ELF 文件就是链接视图 LinkingView 从 Segment 的角度来看就是执行视图 ExecutionView。当我们在谈到 ELF 装载时,段专门指 Segment,而在其他的情况下,段指的是 Section,ELF 可执行文件与进程虚拟空间映射关系如下图所示:ELF 可执行文件中有一个专门的数据结构叫做程序头表**(Program Header Table)**用来保存 Segment 的信息。因为 ELF 目标文件不需要被装载,所以它没有程序头表,而 ELF 的可执行文件和共享库文件都有。它的结构体以及各个成员的含义如下: typedef struct { Elf32_Word p_type; // 类型,基本上我们在这里只关注 LOAD 类型的 Segment Elf32_Off p_offset; // Segment 在文件中的偏移 Elf32_Addr p_vaddr; // Segment 第一个字节进程虚拟地址空间的起始位置 Elf32_Addr p_paddr; // Segment 的物理装载地址 Elf32_Word p_filesz;// 在 ELF 文件中所占空间的长度 Elf32_Word p_memsz; // Segment 在进程虚拟地址空间中所占用的长度 Elf32_Word p_flags; // Segment 权限属性(可读 R、可写 W、可执行 X) Elf32_Word p_align; // Segment 对齐属性(2 的 p _align 次方字节) } Elf32_Phdr堆和栈 在操作系统里面,VMA 除了被用来映射可执行文件中的各个 Segment ,还使用 VMA 来对进程的地址空间进行管理。我们知道进程在执行的时候它还需要用到堆和栈等空间,事实上它们在进程的虚拟空间中的表现也是以 VMA 的形式存在的,很多情况下,一个进程中的堆和栈分别都有一个对应的 VMA。 Linux 下,我们可以通过查看 /proc 来查看进程的虚拟空间分布:cat /proc/21963/maps 08048000-080b9000 r-xp 00000000 08:01 2801887 ./SectionMapping.elf 080b9000-080bb000 rwxp 00070000 08:01 2801887 ./SectionMapping.elf 080bb000-080de000 rwxp 080bb000 00:00 0 [heap] bf7ec000-bf802000 rw-p bf7ec000 00:00 0 [stack] ffffe000-fffff000 r-xp 00000000 00:00 0 [vdso]第一列是 VMA 的地址范围。 第二列是 VMA 的权限,r 表示可读,w 表示可写,x 表示可执行,p 表示私有 (COW, Copy on Write) ,s 表示共享。 第三列是偏移, 表示 VMA 对应的 Segment 在映像文件中的偏移。 第四列表示映像文件所在设备的主设备号和次设备号。 第五列表示映像文件的节点号。 最后一列是映像文件的路径。操作系统通过给进程空间划分出一个个 VMA 来管理进程的虚拟空间,基本原则是将相同权限属性的、有相同映像文件的映射成一个 VMA,一个进程基本上可以分为如下几种 VMA 区域:代码 VMA,权限只读,可执行,有映像文件。 数据 VMA,权限可读写,可执行,有映像文件。 堆 VMA,权限可读写,可执行,无映像文件,匿名,可向上扩展。 栈 VMA,权限可读写,不可执行,无映像文件,匿名,可向下扩展。常见进程的虚拟空间如下图所示:堆的最大的申请数量也就是 malloc 的最大申请数量会受到哪些因素的影响呢?具体的数值会受到操作系统版本,程序本身大小,用到的动态共享库数量、大小,程序栈数量、大小等。 有可能每次运行的结果都会不同,因为有些操作系统使用了一种叫做随机地址空间分布的技术 (主要是出于安全考虑, 防止程序受恶意攻击) ,使得进程的堆空间变小。ASLR 在计算机科学中,地址空间配置随机加载称为 ASLR,又称地址空间配置随机化或地址空间布局随机化,是一种防范内存损坏漏洞被利用的计算机安全技术,通过随机放置进程关键数据区域的地址空间来防止攻击者跳转到内存特定位置来利用函数。 Linux 已在内核版本 2.6.12 中添加 ASLR。 Apple 在 Mac OS X Leopard 10.5 中某些库导入了随机地址偏移,但其实现并没有提供 ASLR 所定义的完整保护能力。而 Mac OS X Lion 10.7 则对所有的应用程序均提供了 ASLR 支持。 Apple 在 iOS 4.3 内导入了 ASLR。 段地址对齐 可执行文件最终是要被操作系统装载运行的,这个装载的过程一般是通过虚拟内存的页映射机制完成的。在映射过程中,页是映射的最小单位。 假设我们有一个 ELF 可执行文件,它有三个段需要装载,我们将它们命名为 SEG0、SEG1 和 SEG2。每个段的长度、在文件中的偏移如表所示:段 长度 (字节) 偏移 (字节) 权限SEG0 127 34 可读可执行SEG1 9899 164 可读可写SEG2 1988只读这属于大多常见的情况,就是每个段的长度都不是页长度的整数倍,一种最简单的映射办法就是每个段分开映射,对于长度不足一个页的部分则占一个页。通常 ELF 可执行文件的起始虚拟地址为 0x08048000,所以这三个段的虚拟地址和长度如表所示:段 起始虚拟地址 大小 有效字节 偏移 权限SEG0 0x08048000 0x1000 127 34 可读可执行SEG1 0x08049000 0x3000 9899 164 可读可写SEG2 0x0804C000 0x1000 1988只读 三个段的总长度只有 12014 字节,却占据了 5 个页,即 20480 字节,空间使用率只有 58. 6 %。导致文件段的内部会有很多碎片,浪费磁盘空间。为了解决这种问题,就是让那些各个段接壤部分共享一个物理页面,然后将该物理页面分别映射两次,如下图:这样映射的话,不仅进程中的某一段区域就是整个ELF 的映像,对于一些须访问 ELF 文件头的操作可以直接通过读写内存地址空间进行,而且内存空间得到了充分的利用,ELF 文件的映射方式如下表所示:段 起始虚拟地址 大小 偏移 权限SEG0 0x08048022 127 34 可读可执行SEG1 0x080490A4 9899 164 可读可写SEG2 0x0804C74F 1988可读可写进程栈初始化 操作系统在进程启动前将系统环境变量和进程的运行参数等提前保存到进程的虚拟空间的栈中,进程启动后,程序的库部分会把堆栈里的初始化信息中的参数信息传递给 main 函数,并通过函数的两个参数 argc 和 argv,传递命令行参数数量和命令行参数字符串指针数组。 Linux 内核装载 ELF 过程简介 首先,用户层面,bash 进程会调用 fork 系统调用创建一个新的进程,然后新的进程调用 execve 系统调用执行指定的 ELF 文件,原先的 bash 进程继续返回等待刚才启动的新进程结束,然后继续等待用户输入命令。 execve 系统调用被定义在 unistd.h,它的原型如下: int execve(const char *filename,char *const argv[],char *const envp[]); 它的三个参数分别是被执行的程序文件名,执行参数和环境变量,相关函数执行顺序如下:在内核中execve 系统调用相应的入口是 sys_execve。 sys_execve 进行一些参数的检查复制之后,调用 do_execve。 do_execve 会首先查找被执行的文件,如果找到文件,则读取文件的前 128 个字节,判断文件的格式,每种可执行文件的格式的开头几个字节都是很特殊的,特别是开头 4个字节,常常被称做魔数。 调用 search_binary_handle 通过判断文件头部的魔数确定文件的格式去搜索和匹配合适的可执行文件装载处理过程。 调用ELF 可执行文件的装载处理过程 load_elf_binary。 当 load_elf_binary 执行完毕,返回至 do_execve 再返回至 sys_execve 时已经把系统调用的返回地址改成了被装载的 ELF 程序的入口地址了。 load_elf_binary这个函数的代码比较长,它的主要步骤是:检查 ELF 可执行文件格式的有效性,比如魔数,程序头表中段的数量。寻找动态链接的 .interp 段,设置动态链接器路径。根据 ELF 可执行文件的程序头表的描述,对 ELF 文件进行映射,比如代码、数据、只读数据。初始化 ELF 进程环境,比如进程启动时 edx 寄存器的地址应该是 DT_FINI 的地址。将系统调用的返回地址修改成 ELF 可执行文件的入口点,这个入口点取决于程序的链接方式,对于静态链接的 ELF 可执行文件,这个程序入口就是 ELF 文件的文件头中 e_entry 所指的地址. 对于动态链接的 ELF 可执行文件,程序入口点是动态链接器。动态链接 静态链接的方式对于计算机内存和磁盘的空间浪费非常严重,作者讲了一个静态链接的例子,Program1 和Program2 分別包含 Program1.o 和 Program2.o 两个模块,并且它们还共用 Lib.o 这个模块,静态链接下,当同时运行 Program1 和Program2 时,Lib.o 在磁盘中和内存中都有两份副本,想象如果是静态链接的库,很多程序共用的情况下,那么将会有大量的内存空间被浪费。除此之外,如果是使用静态链接,假设 Lib.o 修改了一个 bug,那么 Program1 和 Program2 的厂家都需要拿到最新的 Lib.o,然后再与 Program1.o 或者 Program2.o 重新链接后,将最新的程序发布给用户,以至于每个小的改动,都会导致整个程序重新下载。动态链接的出现就是要解决空间浪费和更新困难这两个问题的。 把链接这个过程推迟到了运行时再进行,这就是动态链接的基本思想。工作的原理与静态链接类似,包括符号解析、地址重定位,回到上面的例子,如果改成动态链接,Lib.o 在磁盘和内存中只存在一份,这么做不仅仅减少内存的使用,还可以减少物理页面的换入换出,也可以增加 CPU 缓存的命中率,因为不同进程间的数据和指令访问都集中在了同一个共享的模块上。升级变得更加容易只要简单地将旧的目标文件覆盖掉,而无须将所有的程序再重新链接一遍。 除了上述优点外,动态链接还可以被拿来做插件,为程序增加动态的功能扩展,也可以通过动态链接库给程序和操作系统之间增加了一个中间层,消除程序对不同平台之间依赖的差异性,虽然有很多优点,动态链接也是存在着一些缺点的,例如某个模块更新后,会产生新的模块与旧的模块之间接口不兼容的问题,这个问题也经常被称为 DLL Hell 。 动态链接的例子 /*Program1.c */ #include "Lib.h"int main() { foobar(1); return 0; }/*Program2.c*/ #include "Lib.h"int main() { foobar(2); return 0; }/*Lib.c*/ #include <stdio.h> void foobar(int i) { printf("Printing from Lib.so %d\n",i); }/*Lib.h*/ #ifndef LIB_H #define LIB_H void foobar(int i); #endif两个程序的主要模块 Program1.c 和 Program2.c 分别调用了 Lib.c 里面的 foobar 函数。 使用 GCC 将 Lib.c 编译成一个共享对象文件:gcc - fPIC -shared -o Lib.so Lib.c 两个程序 Program1 和 Program2,这两个程序都使用了 Lib.so 里面的 foobar 函数 。 从 Program1 的 角度看 ,整个编译和链接过程如下图所示:上图的步骤中只有一个步骤与静态链接不一致,那就是 Program1.o 被链接成可执行文件的这一步,在静态链接中,会把 Program1.o 和 Lib.o 链接到一起,并且产生输出可执行文件 Program1,但是这里,Lib.o 没有被链接进来,链接的输入目标文件只有 Program1.o。 当链接器将 Program1.o 链接成可执行文件时,这时候链接器必须确定 Programl.o 中所引用的 foobar 函数的性质。如果 foobar 是一个定义于其他静态目标模块中的函数,那么链接器将会按照静态链接的规则,将 Programl.o 中的 foobar 地址引用重定位,如果 foobar 是一个定义在某个动态共享对象中的函数,那么链接器就会将这个符号的引用标记为一个动态链接的符号,不对它进行地址重定位,把这个过程留到装载时再进行。 动态链接下,程序分为可执行文件和程序依赖的共享对象 Lib.so,Lib.so 中保存了完整的符号信息,通过将 Lib.so 作为链接的输入之一,就能够知道 foobar 的引用是一个静态符号还是一个动态符号。 地址无关代码 共享对象的最终地址在装载时确定,装载器根据当前地址空间的空闲情况,动态分配一块足够大小的虚拟地址空间给相应的共享对象。那么装载地址是怎么获取的呢?在早期,有种做法叫静态共享库(将程序的各种模块统一交给操作系统来管理,操作系统在某个特定的地址划分出一些地址块,为那些已知的模块预留足够的空间)。 这种做法现在已经被淘汰了,之所以被淘汰,主要原因就是升级时,必须保持共享库中全局函数和变量地址的不变,如果应用程序在链接时己经绑定了这些地址,一但更改就必须重新链接应用程序。 为了能够使共享对象在任意地址装载,基本思路是在链接时,对所有绝对地址的引用不作重定位,而把这一步推迟到装载时再完成。假设函数 foobar 相对于代码段的起始地址是 0x100,当模块被装载到 0x10000000 时,我们假设代码段位于模块的最开始,即代码段的装载地址也是 0x10000000,那么我们就可以确定 foobar 的地址为 0x10000100。这时系统遍历模块中的重定位表,把所有对 foobar 的地址引用都重定位至0x10000100,这种装载时重定位义被叫做基址重置 Rebasing。 装载时重定位是解决动态模块中有绝对地址引用的方法之一,但是指令部分无法再多个进程之间共享,就失去了节省内存的优势。我们希望程序模块中共享的指令部分在装载时不需要因为装载地址的改变而改变,所以实现的基本想法就是把指令中那些需要被修改的部分分离出来,跟数据放在一起,这样指令部分就可以保持不变,而数据部分可以在每一个进程中拥有一个副本。这种方案称之为地址无关代码 PIC 技术。 共享对象模块中的地址引用按照是否为跨模块分成两类:模块内部引用。模块外部引用按照不同的引用方式又可以分成两类:指令引用。数据访问。于是我们就得到了 4 中情况:第一种是模块内部的函数调用、跳转。 第二种是模块内部的数据访问,比如模块中定义的全局变量、静态变量。 第三种是模块外部的函数调用、跳转。 第四种是模块外部的数据访问,比如其他模块中定义的全局变量。示例代码如下: static int a; extern int b; extern void ext();void bar() { a = 1; b = 2; } void foo() { bar(); ext(); }类型一 模块内部调用或跳转 例如上面例子中 foo 对 bar 的调用,属于模块内部的调用,会产生如下代码: 8048344 <bar>: 8048344: 55 push %ebp .... 8048349 <foo>: 8048357: e8 e8 ff ff ff call 8048344<bar> 804835C: ...对于模块内部调用,因为被调用的函数和调用者在同一个模块,他们之间的相对位置是固定的。模块内部的跳转和函数调用都可以是相对地址调用,或者基于寄存器的相对调用,这些指令是不需要重定位的。 0xFFFFFFE8 是 -24 的补码形式 bar 的地址为 0x804835c + (-24) = 0x8048344 类型二 模块内部数据访问 例如上面例子中 bar 访问内部变量 a,属于模块内部的数据访问,会产生如下代码: 0000044c <bar>: ..... 44f: e8 40 00 00 00 call 494<__i686.get_pc_thunk.cx> 454: 81 c1 8c 11 00 00 add $0x118c,%ecx 45a: c7 81 28 00 00 00 01 movl %0x1,0x28(%ecx) 461: 00 00 00 ..... 494: <__i686.get_pc_thunk.cx>: 494: 8b 0c 24 mov (%esp),%ecx 497: c3 ret一个模块前面一般是若干个页的代码,后面紧跟着若干个页的数据,这些页之间的相对位置是固定的,也就是说,任何一条指令与它需要访问的模块内部数据之间的相对位置是固定的,那么只需要相对于当前指令加上固定的偏移量就可以访问模块内部数据了,现代的体系结构中,数据的相对寻址往往没有相对与当前指令地址 PC 的寻址方式,所以 ELF 用了一个很巧妙的办法来得到当前的 PC 值。 __i686.get_pc_thunk.cx 这个函数的作用就是把返回地址的值放到 ecx 寄存器,即把 call 的下一条指令的地址放到 ecx 寄存器。 变量 a 的地址,是 add 指令地址加上两个偏移量 0x118c 和 0x28,即如果模块被装载到 0x10000000 这个地 址的话,变量 a 的实际地址将是 0x10000000 + 0x454 + 0x118c + 0x28 = 0x10001608 例外: 对于全局变量来说,无论是在模块内部还是模块外部,都只能使用 GOT 的方式来访问,因为编译器无法确定对全局变量的引用是跨模块的还是模块内部的,关于 GOT 下面会介绍。 类型三 模块间数据访问 例如上面例子中 bar 访问内部变量 b,属于模块间的数据访问,会产生如下代码: 0000044c <bar>: ..... 44f: e8 40 00 00 00 call 494 <__i686.get_pc_thunk.cx> 454: 81 c1 8c 11 00 00 add $0x118c,%ecx //%ecx=0x454 + 0x118C,GOT表地址 45a: c7 81 28 00 00 00 01 movl $0x1,0x28(%ecx) //a = 1 461: 00 00 00 464: 8b 81 f8 ff ff ff mov 0xfffffff8(%ecx),%eax 46a: c7 00 02 00 00 00 movl $0x2,(%eax) //b = 2 ..... 494: <__i686.get_pc_thunk.cx>: 494: 8b 0c 24 mov (%esp),%ecx 497: c3 retELF 的做法是在数据段里面建立一个指向这些变量的指针数组,也被称为全局偏移表 (Global Offset Table,GOT) ,当代码需要引用该全局变量时,可以通过 GOT 中相对应的项间接引用,基本机制如下图:当指令中需要访问变量 b 时,程序会先找到 GOT,然后根据 GOT 中变量所对应的项找到变量的目标地址。每个变量都对应一个 4 个字节的地址,链接器在装载模块的时候会查找每个变量所在的地址,然后填充 GOT 中的各个项,以确保每个指针所指向的地址正确。由于 GOT 本身是放在数据段的,所以它可以在模块装载时被修改,并且每个进程都可以有独立的副本,相互不受影响。 GOT 如何做到指令的地址无关性: 模块在编译时可以确定模块内部变量相对与当前指令的偏移,那么我们也可以在编译时确定 GOT 相对于当前指令的偏移。确定 GOT 的位置跟上面的访问变量 a 的方法基本一样,通过得到 PC 值然后加上一个偏移量,就可以得到 GOT 的位置,然后我们根据变量地址在 GOT 中的偏移就可以得到变量的地址。 我们的程序首先计算出变最 b 的地址在 GOT 中的位置,即 0x10000000 + 0x454 + 0x118c + (-8) = 0x100015d8 ,(0xfffffff8 为 -8 的补码表示,也就是 在 GOT 中偏移 8),然后使用寄存器间接寻址方式给变最 b 赋值 2。 这边解释下寄存器的直接寻址和间接寻址: 寄存器寻址:指令所要的操作数已存储在某寄存器中,或把目标操作数存入寄存器。 寄存器间接寻址:寄存器内存放的是操作数的地址,而不是操作数本身,即操作数是通过寄存器间接得到的。 因为上面我们在 GOT 拿到的是变量 b 在外部模块的地址,所以更改变量 b 的值的过程是通过间接寻址来做的。 类型四 模块间调用、跳转 例如上面例子中 foo 对 ext 的调用 ,属于模块间的函数调用,会产生如下代码: call 494 <__i686.get_pc_thunk.cx> add $0x118c,%ecx //%ecx=0x454 + 0x118C,GOT表地址 mov 0xfffffffc(%ecx),%eax call *(%eax) ..... 494: <__i686.get_pc_thunk.cx>: 494: 8b 0c 24 mov (%esp),%ecx 497: c3 ret模块需要调用目标函数时,可以通过 GOT 中的项进行间接跳转,调用 ext 函数的方法与上面访问变量 b 的方法基本类似,先得到当前指令地址 PC,然后加上一个偏移得到函数地址在 GOT 中的偏移,然后一个间接调用,如下图:4 种地址引用方式在理论上都实现了地址无关性,总结如下:指令跳转、调用 数据访问模块内部 (1)相对跳转和调用 (2)相对地址访问模块外部 (3)间接跳转和调用(GOT) (4)直接访问(GOT)共享模块的全局变量问题 当一个模块引用了一个定义在共享对象的全局变量的时候,比如一个共享对象定义了一个全局变量 global,下面这块代码我们将它定义在 module.c 中,当编译时它无法根据这个上下文判断 global 是定义在同一个模块的其他目标文件还是定义在另外一个共享对象之中,即无法判断是否为跨模块间的调用。 extern int global; int foo() { global = 1; }假设 module.c 是程序可执行文件的一部分,由于可执行文件在运行时并不进行代码重定位,所以变量的地址必须在链接过程中确定下来。为了能够使得链接过程正常进行,链接器会在创建可执行文件时,在它的 bss 段创建一个 global 变量的副本,然而由于 global 是定义在共享对象中的,那么这个 global 变量会同时存在于多个位置中,这显然是不行的。 解决的办法那就是所有的使用这个变量的指令都指向位于可执行文件中的那个副本。ELF 共享库在编译时,默认都把定义在模块内部的全局变量当作定义在其他模块的全局变量,通过 GOT 来实现变最的访问。当共享模块被装载时,如果某个全局变量在可执行文件中拥有副本,那么动态链接器就会把 GOT 中的相应地址指向该副本,这样该变量在运行时实际上最终就只有一个实例。如果变最在共享模块中被初始化,那么动态链接器还需要将该初始化值复制到程序主模块中的变量副本,如果该全局变量在程序主模块中没有副本,那么 GOT 中的相应地址就指向模块内部的该变量副本。假设 module.c 是一个共享对象的一部分,那么 GCC 编译器在 -fPIC 的情况下,就会把对 global 的调用按照跨模块模式产生代码。原因是编译器无法确定对 global 的引用是跨模块的还是模块内部的。即使是模块内部的,还是会产生跨模块代码,因为 global 可能被可执行文件引用,从而使得共享模块中对 global 的引用要执行可执行文件中的 global 副本。 数据段地址无关性 static int a; static int *p = &a;如果某个共享对象里面有这样一段代码的话,那么指针 p 的地址就是一个绝对地址,它指向变量 a,而变量 a 的地址会随着共享对象的装载地址改变而改变。对此,我们可以选择装载时重定位的方法来解决数据段中绝对地址引用问题,如果数据段中有绝对地址引用,那么编译器和链接器就会产生一个重定位表,表中包含重定位的入口,当动态链接器装载共享对象时,如果发现该共享对象有这样的重定位入口,那么动态链接器就会对该共享对象进行重定位。 那问题来了,为什么数据段可以采用装载时重定位,而代码段不可以呢? 原因其实很简单,因为对于数据段来说,它在每个进程都有一份独立的副本,所以并不担心被进程改变,而代码段则没有独立的副本,如果让代码段也使用这种装载时重定位的方法,而不使用地址无关代码的话,它就不能被多个进程之间共享,于是也就失去了节省内存的优点。 如果可执行文件是动态链接的,那么 GCC 会使用 PIC 的方法来产生可执行文件的代码段部分,以便于不同的进程能够共享代码段,节省内存。 延迟绑定 PLT 首先我们需要认清一个问题,那就是动态链接比静态链接要慢,还会减慢程序的启动速度,主要原因是:动态链接下对于全局和静态的数据访问都要进行复杂的 GOT 定位,然后间接寻址。对于模块间的调用也要先定位 GOT,然后再进行间接跳转。动态链接的链接工作在运行时完成,即程序开始执行时,动态链接器都要进行一次链接工作(动态链接器会寻找并装载所需要的共享对象,然后进行符号查找地址重定位等工作等)。在一个程序运行过程中,可能很多函数在程序执行完时都不会被用到,如果一开始就把所有函数都链接好实际上是一种浪费。所以 ELF 采用了一种叫做延迟绑定的做法,基本的思想就是当函数第一次被用到时才进行绑定 (符号查找、重定位等),这样可以加快程序的启动速度。ELF 使用 PLT (Procedure Linkage Table) 的方法来实现。 例如 liba.so 需要调用 libc.so 中的 bar 函数,第一次调用时首先会需要调用动态链接器中的某个函数来完成地址绑定工作,这个函数的名字是 _dl_runtime_resolve具体过程如下(解析符号仅执行一次): bar@plt jmp *(bar@GOT) push moduleID jump _dl_runtime_resolve调用函数并不直接通过 GOT 跳转,而是通过一个叫作 PLT 项的结构来进行跳转,bar 两数在 PLT 中的项的地址我们称之为 bar@plt。 bar@plt 指令通过 GOT 进行间接跳转指令,bar@GOT 表示 GOT 中保存 bar 这个函数相应的项。 如果链接器初始化阶段并未将 bar 的地址填入该项,而是将 push n (n 为 bar 这个符号引用在重定位表 .rel.plt 中的下标)的地址填入到 bar@GOT 中。。 接着又是一条 push 指令将模块的 ID 压入到堆栈,然后跳转到 _dl_runtime_resolve。 _dl_runtime_resolve 函数来完成符号解析和重定位工作。 _dl_runtime_resolve 在进行一系列工作以后将 bar 的真正地址填入到 bar@GOT。 bar 这个函数被解析完,当我们再次调用 bar@plt 时,第一条 jmp指令就能够跳转到真正的 bar 函数中。ELF 将 GOT 拆分成了两个表叫做 got 和 got.plt 。其中 got 用来保存全局变量引用的地址,.got.plt 用来保存函数引用的地址,所有对于外部函数的引用全部被分离出来放到了 got.plt 中,got.plt 还有一个特殊的地方是它的前三项:第一项保存的是 .dynamic 段的地址,描述了本模块动态链接相关的信息。 第二项是本模块的 ID(在动态链接器在装载共享模块的时候初始化)。 第三项是保存的 _dl_runtime_resolve 的地址(在动态链接器在装载共享模块的时候初始化)。 .got.plt 的其余项分别对应每个外部函数的引用,整体结构如下图。动态链接相关结构 动态链接步骤:在动态链接情况下,操作系统会先加载一个动态链接器(实际上是一个共享对象)。 加载完成后就将控制权交给动态链接器的入口地址( 与可执行文件一样,共享对象也有入口地址)。 当动态链接器得到控制权之后,它开始执行一系列自身的初始化操作。 根据当前的环境参数,开始对可执行文件进行动态链接工作。 当所有动态链接工作完成以后,动态链接器会将控制权转交到可执行文件的入口地址,程序开始正式执行。下面开始介绍一些动态链接中比较重要的段。 .interp 段 动态链接器的位置不是由系统配置指定的,也不是由环境变量决定的,而是由 ELF 可执行文件的 .interp 段指定的。里面保存的就是一个字符串,这个字符串就是可执行文件所需要的动态链接器的路径。 .dynamic 段 .dynamic 段保存了动态链接所需要的基本信息(依赖于哪些共享对象、动态键接符号表的位置、动态链接重定位表的 位置、共享对象初始化代码的地址等)。也是动态链接 ELF 中最重要的结构。.dynamic 段里面保存的信息有点像 ELF 文件头,只是我们前面看到的 ELF 文件头中保存的是静态链接时相关的内容,比如静态链接时用到的符号表、重定位表等,这里换成了动态链接下所使用的相应信息,具体结构如下: typedef struct { Elf32_Sword d_tag; union { Elf32_Word d_val; Elf32_Addr d_ptr; } d_un; } Elf32_Dyn;我们这里列举几个比较常见的类型值,如下表:d_tag 类型 d_un 的含义DT_SYMTAB 动态链接符号表的地址,d_ptr 表示 .dynsym 的地址DT_STRTAB 动态链接字符串表地址,d_ptr 表示 .dynstr 的地址DT_STRSZ 动态链接字符串表大小,d_val 表示大小DT_HASH 动态链接哈希表地址,d_ptr 表示 .hash 的地址DT_SONAME 本共享对象的 SO_NAMEDT_RPATH 动态链接共享对象搜索路径DT_INIT 初始化代码地址DT_FINIT 结束代码地址DT_NEED 依赖的共享对象文件,d_ptr 表示所依赖的共享对象文件名DT_REL 动态链接重定位表地址DT_RELA 动态链接重定位表地址DT_RELENT 动态重读位表入口数量DT_RELAENT 动态重读位表入口数量动态符号表 ELF 为了表示动态链接的模块之间的符号导入导出关系,使用了 .dynsym 段,也称为动态符号表,用来保存这些符号的信息,动态符号表也需要一些辅助的表,比如用于保存符号名的字符串表 .dynstr,为了加快符号的查找过程,往往还有辅助的符号哈希表 .hash。 动态链接重定位相关结构 对于动态链接来说,共享对象不是以 PIC 模式编译的,那么它需要在装载时被重定位的。 共享对象是以 PIC 模式编译的,也需要重定位,因为数据段还包含了绝对地址的引用。装载时的重定位和静态链接中的重定位区别 时机不同 重定位表共享对象的重定位 装载时 .rel.text 和 .rel.data静态链接的目标文件的重定位 链接时 .rel.dyn 和 .rel.plt.rel.dyn 实际上是对数据引用的修正,它所修正的位置位于 .got 以及数据段,而 .rel.plt 是对函数引用的修正,它所修正的位置位于 .got.plt .got.plt 的前三项是被系统占据的,从第四项开始才是真正存放函数地址的地方。 而第四项刚好是 0x000015c8 + 4* 3 = 0x000015d4 即 __gmon_start__,第五项是 printf,第六项是 sleep,第七项是 __cxa_finalize,结构如下图所示:当动态链接器需要进行重定位时 ,它先查找 printf 的地址,printf 位于 libc-2.6.1.so。 假设链接器在全局符号表里面找到 printf 的地址为 0x08801234,那么链接器就会将这个地址填入到 .got.plt 中的偏移为0x000015d8 的位置中去,从而实现了地址的重定位,即实现了动态链接最关键的一个步骤。 稍微麻烦点的是,共享对象的数据段是没有办法做到地址无关的,它可能会包含绝对地址的用,对于这种绝对地址的引用,我们必须在装载时将其重定位。 例如上面的这段代码 static int a; static int *p = &a;在编译时, 共享对象的地址是从 0 开始的,我们假设该静态变量 a 相对于起始地址 0 的偏移为 B,即 p 的值为 B。一旦共享对象被装载到地址 A,那么实际上该变量 a 的地址为 A+B。ELF 文件的编译方式 外部函数的重定位入口的位置PIC 方式 .rel.plt共享对象方式 .rel.dyn动态链接时进程堆栈初始化信息 操作系统通过进程的堆栈传递给动态链接器可执行文件和本进程的一些信息,堆栈里面保存了关于进程执行环境和命令行参数等信息。事实上,堆栈里面还保存了动态链接器所需要的一些辅助信息数组。 typedef struct { uint32_t a_type; union { uint_32_t a_val; } a_un; } Elf32_auxv_t;结构与前面的 .dynamic 段里面的结构如出一辙,32 位的类型值,常见的类型如下:a_type 定义 a_type 值 a_val 的含义AT_NULL 0 表示辅助信息数组结束AT_EXEFD 2 表示可执行文件的句柄AT_PHDR 3 可执行文件中程序头表AT_PHENT 4 可执行文件中程序头表中每一个入口(Entry)的大小AT_PHNUM 5 可执行文件中程序头表中入口(Entry)的数量AT_BASE 7 表示动态链接器本身的装载地址AT_ENTRY 9 可执行文件入口地址,即启动地址它们在进程堆栈位于环境变量指针的后面:动态链接的步骤和实现 动态链接的步骤基本上分为 3 步:启动动态链接器本身。 装载所有需要的共享对象。 重定位和初始化。Bootstrap 动态链接器本身不可以依赖于其他任何共享对象,其次是动态链接器本身所需要的全局和静态变量的重定位工作由它本身完成。这种具有一定限制条件的启动代码往往被称为自举 (Bootstrap)。 动态链接器入口地址即自举代码的入口,自举代码首先会找到他自己的 GOT。而 GOT 的第一个入口是 .dynamic 段的偏移地址,通过 .dynamic 中的信息,自举代码便可以获得动态链接器本身的重定位表和符号表等,从而得到动态链接器本身的重定位入口,先将它们全部重定位。从这一步开始动态链接器代码才可以使用自己的全局变量和静态变量。动态链接器实际上使用 PIC 模式编译的共享对象,对于模块内部的函数调用也是采用跟模块外部函数调用一样的方式,即使用 GOT/PLT 的方式,所以在 GOT/PLT 没有被重定位之前,自举代码不可以使用任何全局变量,也不可以调用函数。 装载共享对象 完成自举后,动态链接器将可执行文件和链接器本身的符号都合并到全局符号表,然后链接器通过 .dynamic 段找到可执行文件依赖的所有共享对象,并将这些对象放入一个装载集合中,然后把这些对象映射到进程中,如果这些共享对象还依赖其他共享对象,那么将所依赖的共T享对放到装载集合中。如此反复,直到所有依赖的共享对象都被装载进来。装载时符号的优先级是按照加入全局符号表的先后来排序的,当一个符号需要被加入全局符号表时,如果相同的符号名己经存在,则后加入的符号被忽路。 **小 Tip: **为了提高模块内部函数调用的效率,可使用 static 定义函数编译单元私有函数,就可以使用模块内部调用指令,可以加快函数的调用速度,前提是编译器要确保函数不被其他模块覆盖。 重定位和初始化 当上面的步骤完成之后,链接器开始重新遍历可执行文件和每个共享对象的重定位表, 将它们的 GOT/PLT 中的每个需要重定位的位置进行修正。因为此时动态链接器己经拥有了进程的全局符号表。重定位完成后如果某个共享对象有 .init 段,那么动态链接器会执行 .init 段中的代码,用以实现动态共享对象特有的初始化过程,相应地,共享对象中还可能有 .finit 段, 当进程退出时会执行 .finit 段中的代码。 Linux动态链接器实现 对于静态链接的可执行文件来说,程序的入口就是 ELF 文件头里面的 e_entry 指定的入口。 对于动态链接的可执行文件来说,内核会分析它的动态链接器地址,将动态链接器映射至进程地址空间,然后把控制权交给动态链接器。 关于动态链接器有个值得思考的问题:动态链接器本身是动态链接的还是静态链接的? 动态链接器本身应该是静态链接的,它不能依赖于其他共享对象。动态链接器本身必须是 PIC 的吗? 动态链接器可以是 PIC 的也可以不是,但往往使用 PIC 会更加简单一些。原因如下:不是 PIC 的动态链接器,代码段无法共享,浪费内存。不是 PIC 的动态链接器本身初始化会更加复杂,因为自举时还需要对代码段进行重定位 。动态链接器可以被当作可执行文件运行,那么的装载地址应该是多少? 动态链接器作为一个共享库,内核在装载它时会为其选择一个合适的装载地址。显式运行时链接(运行时加载) 一般支持动态链接的系统,都支持程序的运行时加载,也就是让程序自己在运行时控制加载指定的模块,并且可以在不需要该模块时将其卸载。满足运行时装载的共享对象往往被叫做动态装载库。 动态装载库的好处如下:使得程序的模块组织变得很灵活,可以用来实现一些诸如插件、驱动等功能。 不需要从一开始就将他们全部装载进来,从而减少了程序启动时间和内存使用。 可以在运行的时候重新加载某个模块,程序本身不必重新启动就可以实现模块的增加、删除、更新等, 这对于很多需要长期运行的程序来说是很大的优势。动态库和一般的共享对象主要区别是: 共享对象是由动态链接器在程序启动之前负责装载和链接的,这一系列步骤都由动态链接器自动完成,对于程序本身是透明的。 动态库的装载则是通过一系列由动态链接器提供的 API: dlopen、dlsym、 dlerror 、dlclose 进行操作。 dlopen 这个函数用来打开一个动态库,并将其加载到进程的地址空间,完成初始化过程。函数的原型如下: void * dlopen(const char *filename, int flag);第一个参数 filename 是动态库的路径,路径可能是绝对路径也可能是相对路径,不同的路径存在不同的加载顺序。 如果将 filename 设置为 0 的话,dlopen 返回的是全局符号表的句柄,也就是说我们可以在运行时找到全局符号表里面的任何一个符号,并且可以执行它们。 第二个参数 flag 表示函数符号的解析方式,可以是 PLT 方式(也就是延迟绑定的机制),也可以是加载时即完 成所有的函数的绑定工作,两种方式必须二选其一。 函数的返回值是被加载的模块的句柄,这个向柄在 dlsym 或者 dlclose 时需要用到。 此外 dlopen 中还会执行模块中初始化部分的代码。 dlsym 这个函数是运行时装载的核心部分,我们可以通过这个函数找到所需要的符号。函数的原型如下: void * dlsym(void *handle, char *symbol);第一个参数是由 dlopen 返回的动态库的句柄 第二个参数即所要查找的符号的名字 如果 dlsym 找到了相应的符号,则返回该符号的值,没有找到相应的符号则返回 NULL。 符号的优先级 是当多个同名符号冲突时,先装入的符号优先,我们把这种优先级方式称为装载序列,由动态链接器装入和由 dlopen 装入的共享对象,动态链接器在进行符号的解析以及重定位时,都是采用装载序列,然而使用 dlsym 进行查找时,优先级却分两种类型:如果我们是在全局符号表中进行符号查找,那么由于全局符号表使用的是装载序列,所以 dlsym 使用的也是装载序列。 如果我们是对某个通过 dlopen 打开的共享对象进行符号查找的话,那么采用的是一种叫做依赖序列的优先级。它是以被 dlopen 打开的那个共享对象为根节点,对它所有依赖的共享对象进行广度优先遍历,直到找到符号为止。dlerror 监听 dlopen dlsym dlclose 是否成功执行,如果返回 NULL,则表示上一次调用成功,如果不是则返回相应的错误消息。 dlclose 函数作用与 dlopen 相反,系统对于已经加载的模块会存在一个计数,当计数为 0 时,会对模块进行卸载,之后执行模块的 .finit 段的代码,然后将相应的符号从符号表中去除,取消进程空间跟模块的映射关系,然后关闭模块文件。 dyld 关于 dyld (The dynamic link editor) 网上介绍的博客非常多,这里简单提一下,感兴趣的可以看下源码。 它是 Apple 的动态链接器, Mach-O 可执行文件会交由 dyld 负责链接 ,装载。目前发展了好几个版本:dyld 1.0 1996–2004) dyld 2.0 (2004–2007) dyld 2.x (2007–2017) dyld 3.0 (2017) dyld 4.0 (2022)下面是针对内参中介绍 dyld 各个版本的简单整理,从版本的差异中,也能看出苹果对于动态链接的过程的一个优化历程。是在大多数系统使用大型 C++ 动态库之前编写的,导致动态链接器必须做很多工作,而且速度非常慢。首先 dyld 1.0 使用了预绑定的技术预绑定是一种技术,我们试图为系统和应用程序中的每个 dylib 找到固定地址。动态加载器会尝试加载这些地址的所有内容,如果它成功了,它会编辑所有这些二进制文件,让这些预先计算的地址在里面。然后下一次当它把它们放到相同的地址时,它不需要做任何额外的工作。dyld 2.0 是对 dyld 1.0 的完全重写稍微扩展了Mach-o 格式并更新了 dyld 以便我们可以获得高效的 C++ 库支持。 它还具有完整的 dlopen 和 dlsym 实现以及正确的语义。 2.0 存在一些安全问题,因为它是为速度而设计的,所以它的健全性检查有限。 由于启动速度的提升,因此减少了预绑定的工作量。dyld 2.x 做了很多显著的改进,在程序进程内执行。添加了大量的架构和平台。增加安全性codeSigning 代码签名。 增加了 ASLR 机制。 对 Mach-o 标头中的许多内容添加了边界检查,这样就无法对格式错误的二进制文件执行某些类型的附加操作。摆脱预绑定并用称为共享缓存(share cache)的东西取而代之,合并了大部分系统动态库,并进行了优化:重新排列二进制文件以提高加载速度。 预链接动态库。 预构建 dyld 和 ObjC 使用的数据结构。dyld 3 出 3.0 版本主要是为了性能、可测试性、安全等方面考虑的。将大部分 dyld 移出进程,增加了可测试性;留在进程中的 dyld 位尽可能小,从而减少应用程序中的攻击面。移出进程的方式通过: 确定安全敏感组件。 确定它的昂贵部分,它们是可缓存的,这些是符号查找。大多数启动使用缓存,永远不必调用进程外的 Mach-o 解析器或编译器,而是简单地验证它们,增加启动速度,缓存步骤: 进程外的 Mach-o 解析器,解析所有搜索路径、所有@rpaths、所有可能影响您的启动的环境变量,解析 Mach-o 二进制文件,执行所有这些符号查找,用结果创建闭包。 进程内引擎,它验证启动闭包是正确的,然后它只是映射到 dylibs,并跳转到 main。 启动关闭缓存,系统应用程序关闭我们只是直接构建到共享缓存。dyld 4 目标是通过保持相同的 Mach-o 解析器来改进 dyld3,支持不需要预构建闭包的即时加载,也就是 Prebuilt + JustInTime 的双解析模式。新的抽象基类Loader,为进程中加载的每个 Mach-o 文件实例化一个 Loader 对象,Loader 有两个具体的子类 PrebuiltLoader 和 JustInTimeLoader。 PrebuiltLoader 只读的。它包含有关其 Mach-o 文件的预先计算的信息,包括其路径、验证信息、其依赖的 dylib 和一组预先计算的绑定目标。在启动时,dyld 会为程序寻找预构建的 PrebuiltLoader,验证完有效,则使用它。 如果没有有效的 PrebuiltLoader,那么创建并使用新的 JustInTimeLoader,JustInTimeLoader 然后通过解析 Mach-o 找到它的依赖项,进行实时的解析。总结程序可通过覆盖装入和页映射的两种模式,被操作系统装载到内存中运行,目前几乎所有的主流操作系统都是按页映射的方式装载可执行文件的,页映射的时候,段地址对齐处理不当会造成空间的浪费。 进程建立,首先创建一个独立的虚拟地址空间,然后建立起可执行文件和进程虚存之间的映射结构,设置可执行文件的入口,执行程序。随着程序的执行,会不断的产生页错误,操作系统会通过映射结构为进程分配相应的物理页面来满足进程执行的需求。 在 ELF 文件中使用 Program Header Table 保存 Segment 的信息,操作系统使用 VMA 来映射可执行文件中的各个 Segment,另外堆和栈等空间也是以 VMA 的形式存在的,除此之外还有称为 vdso 的 VMA 可与系统内核进行通信。 Linux 内核装载 ELF 时,首先会检查 ELF 可执行文件格式的有效性,再者设置动态链接器路径,对 ELF 文件进行映射,再初始化 ELF 进程环境,最后系统调用的返回地址修改成 ELF 可执行文件的入口。 由于静态链接对于计算机内存和磁盘的空间浪费非常严重,于是开始使用动态链接,把链接这个过程推迟到了运行时再进行,这就是动态链接的基本思想。 共享对象模块的访问根据模块所属内部或外部,指令调用或数据访问,一共分成了四种地址引用情况,针对这四种情况,都可实现了地址无关性,访问全局变量默认使用 GOT 的方式,数据段的绝对地址引用,装载时进行重定位,此外 ELF 还会使用 PLT 延迟绑定的方式,也就是一次被用到时才进行绑定。 动态链接中存在一些比较重要的段,.interp 段,.dynamic 段,动态符号表,以及一些动态链接重定位相关结构,除此之外操作系统通过进程的堆栈传递给动态链接器可执行文件和本进程的一些信息。 动态链接的步骤基本上分为 3 步,启动动态链接器本身,装载所有需要的共享对象,重定位和初始化。 满足运行时装载的共享对象往往被叫做动态装载库,它的装载是通过一系列由动态链接器提供的 API: dlopen、dlsym、 dlerror 、dlclose 进行操作。 dyld 是 Apple 的动态链接器, Mach-O 可执行文件会交由 dyld 负责链接 ,装载。已经从 1.0 版本发展到了 4.0 版本。
从校园到职场,他们是怎么过渡的
这次分享主题是「从校园到职场 -- 我的成长之路」,视频内容可以查看 B 站链接:从校园到实习再到秋招。上次的面试分享之后,阿卡拉提到关于刚毕业的学生也会有很多找工作的困扰,而且这个阶段能获取到的信息相对比较有限,如果做一期针对这个群体的分享也会是很有意义的。然后正好也接触到了两位毕业生 hzh 和豆豆,他俩一个是本科,一个是研究生,学习能力也非常强,也都在一线互联网公司实习过,所以就拉他们来做这次分享。 分享人介绍hzh 我是来自广东工业大学计算机学院的 hzh,是 23 届秋招的一名应届生。目前是在广州字节 TikTok 直播团队实习,做的是 iOS 开发,未来会去上海小红书发展。 豆豆 我叫豆豆,是曲阜师范大学,然后现在在读研三,我的研究方向是做推荐算法,有三段实习经历,目前是接了快手的 Offer。 在校篇 在学校期间个人的学习状态怎么样?计算机专业的同学在校期间卷不卷? hzh 我的学校是广州这边的一个双非,但整个学校的就业氛围还是比较强一点。我的学习状态主要是分成两个部分,一个是课程学习,另外一个是工作室的学习。 课程学习看每个人的自觉程度,像我这种比较懒散状态。其实一些课也没有比较认真地听,为了通过期末考试,才会临时突击一下,我很大一部分课程学习都是这样过来。 工作室那边的学习,主要是看自己的兴趣,我学的是 iOS 相关的一些知识,然后帮导师做一些项目。 关于周围的学习氛围,这个其实是要分环境的。我身边的氛围就是非常不卷,寝室每个人都是疯狂打游戏,参加社团活动打球,可以说是比较摆烂,但感觉这个也是一个比较正常的一个大学状态。 但是可能是因为我们班比较特殊,然后像隔壁班的话就会有比较卷的情况。我听说好像是有一个班里面有 20 个左右是绩点4.0,每一科平均都是 90 多分。 就我的宿舍来说,我们在春招之前都已经很长一段时间去刷 leetcode 了。这些主要是因为宿舍里面有个人,他比较厉害,就很喜欢刷那些困难题,我们有什么不会的也去问他。然后这种卷的话其实就是看自己想要达到什么程度,不是跟身边人去比,而是跟自己的预期去比。 豆豆 我在学校的学习状态基本上是自学,身边的同学在本科期间可能玩得比较多一些,我会有自己比较喜欢的一些方向,比较喜欢的一些项目,然后会自己去学习、实践。 本科大家都不是很卷,读研期间的话可能会因为导师分配的各种课程任务。大家需要更多的时间花在这种课业上。然后在秋招的话我身边的同学没有冲大厂的,70% 可能会选择这种国企、事业单位,20% 可能会去读博。这是研究生阶段,本科期间差不多 80% 大家都在考研,考虑直接就业的则比较少。 在校期间如何做职业选择,职业规划 hzh 我个人是有明确预期毕业要去做什么。我跟身边的人也经常去讨论过这个问题,大部分学生对这一块都是比较迷茫的。在校期间,大家都是在学校按部就班的学习,参加各种活动,然后突然间就到了大二,大三,发现自己可能需要去选择一个职业道路了,未来的一个发展也要去思考。有一些人其实甚至不愿意去想,比较晚才醒悟,比如说大三下,大四这样子,那他的准备时间,就不一定够了。 学校在这一块给的帮助也很少,应该是只有一门叫什么「大学生创业指南」,是一个水课。就是让大家了解一下创业的一些东西,让大家写一下简历,提前适应一下面试的时候该怎么回答。但这些都不能算是说去规划你未来人生要去走一个怎么样的职业道路。 我认为一个比较重要的了解职业信息的途径是,咨询直系的师兄师姐,他们大多是已经走在就业的道路上了,已经思考过这方面的选择。然后我们可以去就了解一下他们的一个选择或者听取一些建议,可能会给我一些帮助和启发。 QA:现在这个阶段,你那个专业,整体就业情况怎么样,定好工作的比例大概占多少? 其实如果不是今年的比较特殊情况,按往年来看我们整体就业率都是很高的,比如说我在的那个工作室,去年基本上都是能进大厂的。但是今年的话就是比较难,我们那个工作室,做前端和后端的,十几个人好像就只有一两个拿到了校招Offer。客户端的话可能会比例高一些,但是也不会特别高,反正今年就是非常困难。 豆豆 我们学校跟广东那边差别好大,在我们学校基本上就是做科研,做论文。 我在大学的时候做了一个学校的教务系统。是一个iOS版的,所以当时就决定要走开发这条路了。我其实明确毕业要做什么还是比较早的。 在职业选择上还可以通过加群,特别是那些有一线开发人员的群,可以获取直接的外部信息。因为在学校的话,这个信息还是比较闭塞的,身边做过开发的人也几乎没有。 就你的观察,现阶段哪个专业处于热门状态,计算机专业还是一个好专业吗,该如何做出最优的职业选择?hzh 我感觉目前互联网还是处一个比较热门的情况,但是相比于过去可能差了不少,只不过又提高了许多门槛。但是它的一些薪资待遇、福利以及未来发展都还是有一定优势的,至少还是可以排到前10%的。 关于职业选择,我总结下来就是三种:就业、考公、考研。我们学校的一个就业比例是比较高,考公考研相对少。 我身边也有一些朋友是在字节实习之后发现互联网的生活可能不太适合,然后就立马离职,然后最终选择 all in 考公这样子。所以说计算机也不是适合所有人,还需要考虑个人兴趣以及对压力的接受程度。 豆豆 我觉得现在热门行业的话有互联网这一块,互联网医院,互联网医疗就是做做核酸这一块可能比较比可能比较热门。然后行业最难的话就是公募私就是这种量化,我觉得这个行业其实门槛还是挺高的,我觉得这个应该是相对来说会难一些。职业选择就我身边同学来说的话,基本上都是以往这种稳定方向发展,计算机还是一个好的选择吧。这个我觉得如果没有一个很好的背景或者是家里有比较好的一个资源,我觉得相对其他专业来说的话,计算机还是一个比较好的选择。 考公是另一个热门选项,但成功的比例还是比较少的,一个班里去尝试的,可能最多 10% 的同学能够考上公务员。 实习篇 实习为什么选择了 iOS 呢?然后学习过程中遇到了哪些困难又如何克服的?为实习准备了哪些? hzh 选择 iOS 是因为学校有个工作室,他大一下的时候有过一个宣讲,当时听下来就是有各种方向,前端、后台、客户端还有各种机器学习。自己算是果粉,对移动端也感兴趣,然后就选择了iOS 。当时手边也有一个 Mac 电脑,然后直接就咔咔整了。 学习之中遇见的困难还是挺多的,主要来源于入门的初期阶段。要去学习计算机的思维,还有解决问题的整个思路和方法。工作室里也有一些考核,我之前会在一些简单问题上面卡很久,会怀疑自己的能力等等。但过了这个阶段就好很多饿了。 到字节实习准备的东西就是计算机基础,算法,iOS 知识点还有一些项目这几个方面。 计算机基础的话其实就是在校的课程,计算机网络、操作系统、数据结构。这些知识点更重要的是理解和记忆,最重要的还是要能够通过自己的话将这些内容讲出来,在学习之中也可以沉淀出自己的笔记,后面反复过。 关于算法,我是跟着一个公众号,「代码随想录」。里面会循序渐进,链表、二叉树、贪心、动态规划等,然后我跟着里面刷,大概刷了 500 多题。 因为当时做学校的项目,都比较简单,很难提炼说里面的难点内容。我就自己去网上看一些博客,偷学一些性能优化、滑动优化等这些内容。 然后再去学 iOS 的和 OC 的一些底层原理,我还去看了一些 objc 4 的一些源码。但我感觉这个应该没有太大作用,因为面试一个校招生其实是不会去问一些非常底层和源码的一些东西。 实习岗的竞争个人感觉还是蛮卷的,我面试了挺多公司的,最后也没通过几家。印象比较深刻的应该是拼多多在面完三轮技术面之后,在 HR 面挂了,这也侧面体现出了竞争大吧。 豆豆 我选 iOS 当时是因为给学校做了那个教务系统,就是大二,大家的时候学校没有这个iOS版教务系统,然后就自己去学习,把它给做了出来。然后大三大四也接触了一些 PHP 还有 Java。 真正选择 iOS 是在研一的上学期,当时是想了一些行业的问题,就是这个互联网不可能一直跟当时一样,那种如日中天肯定会达到一个瓶颈期,过后也会遇到困难。但就可能在客户端方面没那么卷,然后就选择了 iOS。 学习困难方面的话,肯定会有的,但很难去表述这个东西。一般是遇到问题然后去把它解决掉。其实更多的是把这些问题,答案,还有解决问题的方式给内化,就是形成了一个解决问题的能力。 实习准备的话刚刚跟 hzh 说的差不多,算法、计算机网络、操作系统,还有组成原理。还有这个平时写过的一些项目。我觉得实习的话可能竞争,对于客户端来说的没那么大,对后端和前端可能竞争会更大一些。 实习跟在校课设有什么不同?实习期间进行转正的比例大概是多少? hzh 实习跟在校课设最大的不同,我感觉可能就是一个规范的流程,跟我们就是自己去随便写点 demo 不一样。然后公司里面其实它整个流程其实还是比较长的,会有各种需求初评、细评,然后技术评审,然后排期开发,提测,一些比较规范的流程。 还有一些合作方面,自己做的一些东西其实是不用跟别人合作的。你在实习的时候是要经常遇到跟别人合作的情形,这沟通能力会有一些要求。然后实习期间最大的收获,我在面试实习的时候就是觉得自己的项目经历是比较弱,然后在实习的时候也去补了这一块知识,然后参与了整个我们公司内部的那个项目的开发,了解了其中一些知识,也做了很多需求。算是补齐了我之前的一个短板,这样子秋招竞争力会更强一点。 在字节,今年的转正应该是比较困难的,往年其听那些师兄说 是 80、90% 的转正率,今年保守估计应该是40、50%。 豆豆 实习跟课设不同的一点就是在实习过程中那个接触到的项目,它是有排期的,有时间规划。在学校自己做项目,可能就没有这么完整的时间观念了,就是哪天想起来要做一点,就花上时间去做一下。 实习期间最大的收获,我觉得更多的是能接触到更多的人。然后我会跟他们很多人都交流想法,我觉得这种人际交往是我收获比较大的一点。 转正率不好说,我是没转正就跑了。 秋招篇 今年秋招被称为最难就业季,计算机也被冠以最卷专业,大家的秋招是怎么准备的?秋招的结果怎么样,有哪些值得总结的经验?hzh 我是边实习边秋招的状态,因为实习的强度比较大,准备的时间也比较短。我大概准备一两个月,就直接投简历了。这一两个月边上班,然后下班的时间去刷一下题,准备的内容就跟上面实习准备的差不多。 秋招会偏向实际做的东西去准备多一点。秋招从结果上来讲的话其实还行,但是过程是非常痛苦的,边实习边秋招,整个人是一个非常累的状态。 我个人觉得值得总结的经验就是千万不要边实习边秋招。除非你的实习工作强度是比较轻松的,或者你对于转正有比较大的把握。 还有投简历的时机,我觉得越早越好,就算你还在一个准备状态,已经可以开始投了。因为有些公司在提前批的时候是不需要笔试的,对简历筛选也没有那么严格。到正式批的时候,会多一个笔试的流程,最终走下来可能会浪费更多的时间,还有一些公司在提前批就已经招完了。 豆豆 我准备秋招其实还算比较早的,就是在研一期间,包括实习阶段就开始为秋招准备了。当时是把计算机网络还有操作系统这些知识点去列成一个思维导图。在每次面试之前,我都会把他们有针对性地再复习一遍。我还针对牛客网上的一些面试题,把它们进行了一个归类整理,然后再用自己的语言去表述,也做了一个思维导图,把它给存起来。 然后就是去刷题,其实这个是断断续续的,把一些比较常考的那种热门题先刷一遍。今年秋招其实拿了六七家 Offer。经验的话,我觉得刷题还是要坚持下来,因为每次面试都是必须经历的,而且算法比重一般比较大,几乎每场面试都会有这种算法题,大概占 80%。如果这个题写不出来,可能就要被挂掉。 保持刷题的这种节奏,这样才能让自己的选择广一点。 成长篇 学校的教学环境,实习,个人学习,哪个对自己的成长帮助更大?为什么?从大学到职场的转变,是否经历过迷茫期,又是如何克服的? hzh 从我现在的角度来看是个人学习 > 实习环境 > 学校教学。学习这个事情永远都是自己的,无论你有没有学到或者是有没有去沉淀下来,都是你自己的事情。比如像我实习时遇到的一些问题,解决不了就会去直接问同事或者 mentor。问完之后可以直接解决,但是后续遇见相同或者类似的问题的话,还是要去问别人,这样自己的能力提升是很有限的。后来我意识到这一点,就会投入更多时间去自我学习。 我的迷茫期是对于一些工作内容强度和节奏比较不适应,感觉实习的过程中压力也比较大。然后我去跟我的一些朋友、导师或者同事讨论这个问题,发现大家在刚刚从学校到社会这个阶段,都是会有这个问题的。 这个自己要去想清楚这件事,你是想要一个怎么样的生活,期望生活是怎么一个节奏,是坚持往互联网这个方向,还是考虑其他更适合自己的事情。然后可以多去征询别人的意见,多去跟别人讨论自己的一些困惑。 但这些都是一些输入,是外部的建议,最终还是要靠自己去做归纳和总结。 豆豆 我也认为个人学习 > 实习 > 学校教学。因为在学校,更重要的是培养学生的科研能力,挖掘问题,然后再去解决这个问题。这对就业的帮助可能没那么大,对于就业,更需要的还是自我学习能力。 大学到职场的迷茫期,我其实当时也有过,就是在实习过程中不知道自己要干嘛,我觉得这个还是要多多交流,多请教别人,我一般会去在问一些职场上的朋友或者是比较熟悉的一些人。其实每个人的成长都会有这种处于十字路口的阶段,也不需要太焦虑。 对于现在计算机专业的学生有哪些建议?有哪些道理是步入社会才学到的? hzh 其实也不是建议,总结一些我的看法吧。 首先是学习,我大学的一些课程大部分应该都是一个混的状态,像数据结构,操作系统这些基础课,都是上课经常玩手机,最终期末的时候再去突击学习。这是一个非常不好的习惯。如果能够重新来的话,我会在上课时更专注一点,多听老师见解,去培养自己计算机思维还有逻辑能力,因为这些能力是会伴随自己整个职业生涯的。 对于一些比较水的课,可以不用在意老师的看法,只要不影响毕业可以随意一些。一些课外活动和社团,如果自己感兴趣的话,可以多参加几个。 最重要的是找到一个志同道合的伙伴,做一些有意义的事情。在这个过程拓宽我们的人脉,锻炼一些社交能力、合作能力、沟通能力。这些都是蛮重要的,无论是遇见了什么问题,都可以找人去沟通,去听听看他对这件事情的意见。 步入社会才学习到的道理:要有 Owner 意识,在职场上,你写的这段代码,你做的这个东西,你要对它负责,任何时候,任何相关的问题,都要做到可以找我解决的程度。 培养自我学习能力,不要遇到什么困难就去请求别人帮助,我们要摆脱对外界的依赖。 说话要有分寸一点,谨言慎行,少说多做。比如说公司的一些红线内容或者其他涉密内容,都谨慎讨论。豆豆 我觉得对于计算机专业的学生,还是要提前准备,无论是去就业或者是去读研或者是去读博,一定要提前决定,提前准备,不要到了临近报考的那个时间段,才去准备。 第二个就是学习跟实践相结合,学到了一些东西,自己也手动去操作一遍,去实践一遍,这样可能会对这个知识点掌握得会扎实一点样。 步入社会才学到的道理:更多的去理解业务,把技术当做一种工具。 团结好身边同事,跟大家要保持一个良好的相处模式。
2022 年小结
- 03 Jan, 2023
在 12 月 31 号晚上这天,打开朋友圈大家都在告别 2022、迎接 2023,我却想不到任何值得发的内容。没有外出体会元旦的节日氛围,也没有观看任何跨年活动,2022 年最后一秒跟全年的 3153.6 万秒没有任何区别。 甚至这篇总结都差点没有,回看了过去几年的年终总结,发现了一些有趣的东西,自己看待问题的想法在变化,关注在意的事情在变化,价值观也在变化。总别不代表有多大意义,却表示我当时回望走过的路时所看到风景,今年的景色如何,也记录一下。 生活 生活中最大的变化是身份的转换:我当爸爸了。孩子 5 月份出生,前半年准备迎接孩子的降生,后半年照顾养育孩子,整个一年基本都在围绕孩子转。 迎接孩子迎接孩子,不是有喜悦还有担心,每次产检都像过关一样:NT、唐氏筛查、大排畸、小排畸、糖耐、胎心监护等等,每次等结果都是忐忑的,好在一路绿灯,媳妇健康孩子也健康。 唯一的波折发生在离预产期还差一周的时候,当时北京正值疫情封控比较严的阶段,我们单元楼出现一个次密接,整单元需要封控,时间也是一周。从得知要封控开始不到两个小时的时间,楼道就被围上了铁门,有专人看守,只进不出。因为在这不久前看了上海防疫因为核酸过期无法接诊导致孕妇流产的事件,我们就很担心。跟居委会反复确认,如果临近生产,我们怎么去医院,会不会因为封控问题拒诊。居委会整体都比较负责,还联系了昌平妇幼,作为兜底,确保临产一定有医院接收,有了这些信息多少让我们安心了一些。 预产期很准,媳妇赶在封控的最后一天临产,凌晨 3 点叫的救护车,一路上我都心惊胆战,直到媳妇到了医院躺在病床,来医生问诊,我才安心一些,到了医院一切就可以交给医生了。 但因为我们属于封控人员,并未安排到产房,而是到了发热门诊,从妇产科调取的医护人员给我们接生。发热门诊医疗条件有限,没有无痛、没有导乐、没有护工,这意味着整个生产过程都需要自己扛过去。医生预估大概10点到12点之间才会生产,在不确定中焦急的情绪和宫缩带来的痛疼,让每一分每一秒都显得非常漫长。我也切身体会了,我们的生日就是妈妈的受难日这句话。 另一方面生的时候有多痛,听到孩子的哭啼就有多喜悦。整个一层楼只有我们在接生,标准的哭啼声响彻整个楼层,这个时间是 2022 年 5 月 15 号上午 10 点 8 分。 护士把孩子推出来,掰着她的小手小脚说,十根手指,十根脚趾不多不少啊。我点头肯定,我打量着这个小家伙,浑身呈粉红色,手因为在羊水里泡了很久,有点泛白,眼睛一直闭着,脸上还有一片一片的胎脂。这就是每个人刚出生时的样子了,这就是一个新生命了。 养育孩子关于名字想了好几版,才有了我跟媳妇都满意的结果:张清越。「清越」是汉语词汇,意为高超出众,清秀拔俗。也指声音悠扬动听。苏轼 《石钟山记》:「得双石於潭上,扣而聆之,南声函胡,北音清越。」寓意是一部分,我们的期望只有一个,希望清越可以健健康康,快快乐乐。 到今天清越已经出生 230 天了,以后除了年份的计数,我又多了一个计算时间的方式。在这不到八个月的时间,我跟媳妇不知熬了多少个夜,对于养孩子也有了很多感悟,俗话说不经一事,不涨一智,任何事情都需要经历才能获得深刻的感悟。养孩子最大的负担是时间成本,孩子需要陪伴,需要照料。需要考虑父母能否帮助带孩子,如果不能的话,是否要夫妻某一方专职带孩子。请保姆是一个过于理想的方案,一个是信任一个是孩子本身对于陌生人的接受程度如何,除非孩子出生就有固定的保姆帮助照看。我遇见了不少案例是,因为孩子,夫妻一方辞职带孩子,双方都辞掉带孩子的也有。 要提前做一些如何照料孩子的功课,这里推荐一本书《崔玉涛育儿百科》,它按照时间维度,从孩子出生到六周岁整个过程,整个过程孩子的发育变化和各类注意事项都有说明。比如孩子便便不同颜色表示什么状态、如何处理黄疸、脐带如何消毒、二月闹怎么处理、吐奶怎么办、辅食什么时候吃、该吃什么、如何处理孩子的情绪、发生意外怎么处理等等。提前了解这些内容不至于遇事慌张,而且也是对孩子的负责。 照料新生儿对父母冲击最大的是睡眠问题,孩子的睡眠是零散的,一晚上醒 2 次,醒 5 次都是有可能的。而且每次醒来也不一定哄哄就能入睡,有时还要闹个 1 小时才能睡着。她累了就直接睡了,大人可不一定就马上能入睡。因为孩子睡眠问题我们专门去儿研所看过一次,医生说可能是消化问题,药吃完,又过了一段时间睡眠才安稳些,可能是药的作用也可能是成长起来了。当时在医院还遇到一位妈妈说,她家孩子 5 岁了,一夜还醒 3-4 次,根本没法上班,就把工作辞了。 和父母的育儿观念发生冲突时,如果是无关紧要的事情就尊重父母的做法。 孩子会爬之后,最好一刻也别让他离开自己的视线。有一次妈妈上厕所的功夫,她睡醒了就一个劲爬,从床上掉了下来。 个人时间会被侵占很多,娱乐活动基本没了,你不能把她独自放在家里,去外面还要考虑换尿不湿喂奶等一系列事情;想看个综艺、看会书,孩子睡着之前就别想了。 每个人的耐心都是有限度的,即使知道孩子什么都不懂,也会让人失去理智,忍不住出手。 孩子醒着的时候嫌她闹人想让她睡觉,哄睡着的时候看着粉嘟嘟的又想把她叫醒陪她玩。 即使做出再大牺牲,受过再多的苦,看到孩子,感受到来自孩子的慰藉,一种从未感受过的慰藉,那一切都是值得的,甚至认为这是一种奖赏。这真的非常神奇,可能这就是养孩子的意义吧。工作 年初的时候来到抖音 iOS 基础技术团队,开启了一个全新的职业篇章。基础技术的不同 之前的工作经历都是在做业务开发,业务需求来自于 PM,自己是在实现 PM 的规划。基础技术是面向开发者,为开发者赋能的一个角色,这里没有明确的 PM,很多时候自己就是 PM 的角色。看到一个业务痛点 > 想到一种解决思路 > 调研公司内同类型问题解决方案 > 细化和完善这个技术方案 > 方案评审 > 协调投入资源 > 对齐 OKR > 投入研发工作 > 测试 > 数据分析 > 交付 > 后期迭代规划,然后再一轮的循环。这是大体流程,很像一个完整的业务需求开发,唯一的不同是,你不是只负责研发一个环节,而是所有环节。需要注意的是要做成一件事绝不仅仅是把手头工作做好就行了,很多事情都会依赖于多个团队的合作,跨团队之间目标不对齐,优先级不一致,遇到预期之外的困难等等,都会影响最终结果。 所以我平常的工作内容可能只有不到一半的时间是用来写代码的,另外一大部分时间都是在收集信息、拉会讨论、分析数据、方案验证、做规划等等。因为涉及工作范围比较广,基本脱离了 iOS 的范畴,接触到了非常多的开发语言和框架。当我们考虑整体目标时,会发现研发只是实现目标的一个环节,技术栈则是完成这个环节的手段,选什么技术去完成不重要的,能否完成才重要。 可以感受到基础技术其实是一个不断拓展研发边界的岗位。 字节工作感受 也聊一下在字节的工作感受吧。 优秀的人非常多 同事都非常优秀,优秀不只体现在技术实力,还有工作态度。他们很多都非常负责,责任感强,Owner 意识强。他们生活中也很丰富,有一定特长,有积极的兴趣爱好,给我感觉就是学习又好,又会玩。 字节员工整体是年轻化的,我之前在爱奇艺,基本是组内最小的,到了字节,发现大部分人都比我小。 但字节的工作内容却一点都不简单,这里有非常复杂的业务场景可以用于锻炼。我看到很多毕业没多久的同事工作能力都非常强,根本不像是刚毕业的样子,这就得益于工作环境带来的业务能力,毫不夸张的说,他工作一年可能比我之前工作两年学到的东西都要多。这应该也就是为什么字节年轻化的原因。所以有时候也必须的承认选择就是大于努力。 工作感受 分享一些感受吧,这样可以比较全的概括一些东西。工作强度略大,这是跟爱奇艺的工作相比。有一个从蚂蚁来的同事说,跟蚂蚁比,这边工作算轻松了。 福利好,饮食上特别是大钟寺开火之后,每天去食堂都像是去商场,一周下来每顿一个样都不带重复的,每周还会再调整菜单。开火后的一个月里,我胖了 4 斤。其他方面,额外的陪护假,育儿假,房补,吃饭提前,打车时间提前,字节虽然还是被说卷,但这些变动表示着公司在给所有员工营造轻松的工作氛围。 团队氛围好,不管是打配合还是平常一起玩耍,之前活动日,老大都会催着我们赶紧下班。但听说不少团队还存在 PUA,刷工时的情况,只能说合适的团队可遇不可求,进入合适自己的团队需要一定的运气。 分享氛围很强,小组分享,团队分享,外部团队分享,每周都有好几期,只要感兴趣都可以学习,这一点公司更像是大学的感觉。 年轻化,不只是一线员工,领导层也偏年轻化。 不同团队工作感受差异较大,在脉脉看到很多吐槽的声音,甚至有些人成了字节黑。但记住字节团队非常多,也因为成长的快,所以会存在一些不合理的现象。如果你对来字节工作有顾虑可以问清楚部门团队,不要以偏概全。 非常适合应届生,这里能看到的东西非常多,对于应届生可以快速成长起来,但目前来看面试难度会大很多,需要提早准备。个人感悟 影视作品 看了两部动漫作品:《鬼灭之刃》动漫。飞碟社制作,质量非常高,我没看过原漫画,但不少人都说,动漫质量是超越漫画的。2023 年 4 月会迎来第三季《锻刀村篇》,值得期待。《一拳超人》漫画。饿狼篇迎来终章,后期战力有些崩溃坏,插曲:村田因为被粉丝骂中间还改过一次剧情,做续作的漫画家真不好当。抛开剧情问题,漫画质量还是非常不错的,我每次看的时候都是耐着性子慢慢翻,生怕遗漏某处内容。剧集:《开端》:陪媳妇看的,她喜欢白敬亭,我喜欢赵今麦。《怪奇物语》:恐怖氛围营造非常好,有点不太能接受的是,剧中主角男孩都越长越残,女主越长越好看。游戏:《原神》:大概是 7 月份注册的,玩的不连贯,到现在冒烟才 43 级,刚开稻妻不久。原神整体是靠剧情和角色培养来吸引玩家的,像是剧情 PV 《神女劈观》《人间至味》,角色演示《纳西妲:无垠无忧》《雷电将军:净土裁断》,都非常精彩。听说很多游戏公司也在做开放世界,这里就可以断言,开放世界没人会超过米哈游。书籍:《软件研发效能提升之美》:讲研发效能涉及的各个方面,概念居多。《软件研发效能提升之美》书评《卓有成效的工程师》:提出杠杆率的概念,杠杆率 = 产出的影响 / 投入的时间,时间被经常提到,对应于工作效率,但另一个产出影响却经常被忽略,做高 ROI 高的事情,低 ROI 事情应该被提前规避。《卓有成效的工程师》书评《持续交付》:对于大型项目,发布过程有非常多事情值得注意,如果这个过程让你头疼,那说明有很多事情亟待优化。好的持续交付需要遵循两个原则:1、将几乎所有事情自动化。2、将构建、部署、测试和发布软件所需的东西全部纳入版本控制之中。《为什么精英都是时间控》:时间使用的效果能否翻倍,这不是不可以达到的,因为有的人就是能在同样时间内做比其他人多很多的事情。书中列举了很多方法,关于休息,关于快速进入工作状态,很有效果。之前有过某种感受,被书中一总结,感觉提供了更深层的支持。《金字塔原理》:写作和思考都需要遵循的一定规范,如何引入话题,如何跟让人理解,对我写技术文档有挺大帮助。《Apple Debugging & Reverse Engineering》:讲 Apple 调试与逆向的书,raywenderlich 出品,对 LLDB、汇编、DTrace 有完整介绍,不只是逆向,对于想探究 Apple 编程框架底层原理都有很大帮助。关于公众号 在字节工作节奏比之前紧一些,所以投入到公众号的时间比去年少了很多。现在周报的很多事项都由晨光在负责,也因为他,周报才能稳定输出,很少拖更,非常感谢他这一年的付出。对于后续周报的走向,可能会优先发布质量,其次发布频次;对于访谈内容,会更多考虑选题多样性,希望新的一年能结识更多有趣的人。 今年有几篇阅读量和反响都比较好的文章,再推荐一下。 2021 面试心得 人物访谈 | 东坡肘子:健康&开发 人物访谈 | 极简时钟作者:道哥 macOS 进化史 iOS 求职寒冬?听听他们怎么说 关于社会的观察今年围绕疫情发生了很多事情,群众的情绪,基层的处理方式,专家的建议,政府的决策等等,能说的事情非常多,但不能说的也非常多,这也迫使自己思考一些东西,以下仅代表个人目前的看法。很多事情都是个人决定不了的,是遵循规则,还是打破规则,没有定论,没有指导,任何情况都需要看处境。但更好的状况是我们不是只有一个选择,而是更多选择。不要迷信权威,也不要迷信官方,它们都是有立场的,这个立场不一定是指向自己。也不会有任何机构能考虑到每个人,自己的处境还需要自己负责。不要高估政策决策者的能力,也不要低估普通民众的智慧。任何情绪化的做法都是不可取的,保持理性,不随意传播具有煽动性的消息。新冠的管控结束了,但新冠远没有结束,这不代表回到了疫情前的生活,我们仍需小心。能争取的利益就尽量争取,多考虑个人少考虑集体,集体由个人组成的,集体利益是每个人的利益之和。少提问题,多想解决方案。心理上保持乐观,但行动上为悲观做准备。当你感觉辛苦时,有很多人比你辛苦的多的多。看到了太多为生活挣扎的人却依然热爱生活,真的没什么难关是过不去的。新年计划 立计划总是乐观激进的,我们基本考虑不到中间发生意料之外事情的情况,列计划的心境很难代表全年,所以对于 2023 的规划,决定不再列 KR,而只是列一些 O,对应的 KR 在实施过程中记录。 O1:提升家庭生活参与度 平常对于工作上的投入过多,一天下来跟孩子相处的时间都不足一个小时,孩子夜里哭闹,我去哄还会哭的更厉害。11、12 月因为北京防疫的原因在家办公了一个多月时间,慢慢的她才开始接受我。再往后会叫爸爸妈妈,会走路,希望我都能陪伴见证。 O2:提升产品力 产品力更多表现的是产品敏感度、产品规划能力、项目管理及数据分析等能力。 O3:更加健康的生活方式 健康在于两方面,一个是休息,一个是运动。今年跑步里程 140km,还不到预期的一半。希望今年睡的更香,跑的更远。 O4:增加对生活的记录 既包括学习产出,像是公众号内容,一些发到网上的零碎想法,也多一些对生活瞬间的记录。我从初三养成写日记的习惯,一直到大一,写了很多本,有一本 10 年和 11 年的日记还保存在身边。翻到 2010 年 12 月 31 号:看了 17 集《火影》,夜里两点多才睡,玩的真够可以。2011年1月1号:新的一年就这样悄无声息地跨过来了,就像什么也没发生一样。回家过元旦,见证了一些人的变化。发小朋朋要结婚了,跟他聊天,他说后悔相亲对象没有前一个好,但必须接受目前的状况。另外两个发小,亚东、鹏博一个染了黄头发,一个烫了头,让我想起来看到的一句话「脑袋里的东西越少,头上的花样越多」。跟弟弟交谈,他说明白周围人对他的偏见,自己正在努力改变。我惊讶于他成熟起来了,可能之前真的小看他了。2011年1月2号: 姑姑和姑父打工回来,让这个阳历年热闹了很多。姑姑张罗着做饭,炊烟雾气中,灵活的身影为寒冬带来了许多温暖。吃过饭之后,把我们几个所有的衣服都收集起来去洗,然后打扫房间、晒被子。我从姑姑身上感受到一种母亲般的温暖,母亲真是一个完整家庭最重要的因素了。 要上学了,姑姑给我三百块钱,我推脱不掉只能收下。坐在车上,又回忆起姑姑的形象。虽然她没有接受过太多教育,从小在农村长大,但她拥有善良、淳朴这些最美丽的品格。还有妈妈、婶婶、大姨她们,任劳任怨,且热爱生活,平凡却又显得那么伟大。真心祝福她们所有人幸福快乐。由于过于久远我完全记不起当时发生的这些事情了,又顺着读了几篇,让自己穿越回去,好几个瞬间都被触动到了。现在生活条件更好了,但比以前更幸福吗,我不确定。有一点是,当我好几次准备去写点什么记录时,总感觉没得写。可能就是现在的生活更平淡,没有之前有意思,也还有可能是忙着眼前的鸡毛蒜皮,丢失了发现美好的能力。新的一年希望能再找回这个能力,不确定能拾起多少碎片,但捡起一个是一个吧。
《程序员的自我修养(链接、装载与库)》学习笔记二(编译和链接)
继续学习《程序员的自我修养 - 链接、装载与库》的第二个大的部分,这一部分包含本书的二、三、四、五章节,作者深入浅出的给我们讲解了静态链接的相关的知识,干货满满,受益良多。 作为一名 iOS 开发人员,我们几乎每天都会用 Xcode 构建我们的程序,但是编译和链接的过程,我们却很少关注, Xcode 作为一种 IDE(集成开发环境)功能十分强大,它能够和 Mac OS 系统中其它的工具协作,例如编译器 gcc,它们提供的默认配置、编译和链接参数对于大部分的应用开发已经足够使用了,也正是由于这些集成工具的存在,我们也忽略了软件的运行机制与机理。 如果我们能深入的了解这些软件的运行机制,也许我们就能在解决问题的时候,多上一种思路,甚至是打破一些瓶颈。所以马上回到今天打算研究的部分,静态链接。大体来说,静态链接是由链接器在链接时将库的内容加入到可执行程序中的做法。具体分为四个部分来讲解:编译和链接,目标文件里有什么,静态链接,Windows PE/COFF,具体的知识点分布如下:还记得连载上一文提到的 Hello World 程序吗? #include <stdio.h>int main() { printf("Hello World\n"); return 0; }在 Linux 下执行,需要使用使用 gcc 来编译,首先通过 gcc hello.c,这会产生默认命名为 a.out 的可执行文件,然后通过 ./a.out 执行这个文件,输出 Hello World,事实上,这里要分为4个步骤:预处理,编译,汇编,链接。编译 在编译之前会有个预编译的过程,使用到的指令为:gcc -E hello.c hello.i 预编译主要做了如下的操作:处理 # 开头的指令,例如 #define 进行宏替换,#if、#ifdef,#elif,#else,#endif。 删除注释。 添加行号和文件名标识,用于编译时产生调试用的信息等。 保留 #pragma 编译器指令。 包含的头文件展开。预编译过后就是编译阶段,编译器就是将高级语言翻译成机器语言的一个工具,之所以使用高级语言,是因为它能使得程序员更加关注程序本身的逻辑,而不是计算机本身的限制(字长、内存大小、通信方式、存储方式等)。高级语言虽然提高了开发的效率,但是机器却无法识别,需要通过编译器,将其翻译成机器认识的语言,翻译的具体过程如下所示。具体的过程分为 6 步: 扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化,现代的 gcc 会将预编译和编译合并成一个步骤 cc1。 编译使用的指令:gcc -S hello.i hello.s gcc 这个命令只是一些后台程序的包装,它会根据不同的参数要求去调用预译编程序 cc1、汇编器 as、链接器 ld。 顺道我们回顾一下 Clang, Clang 是一个由 Apple 主动编写,是 LLVM 项目中的一个子项目。基于 LLVM 的轻量级编译器,之初是为了替代 GCC,提供更快的编译速度。他是负责编译 C、C++、OC 语言的编译器。 测试代码 CompilerExpression.c 如下: void test() { int index = 1; int array[3]; array[index] = (index + 4) * (2 + 6); }词法分析 CompilerExpression.c 源代码输入到扫描器,运用一种类似于有限状态机的算法将源代码的字符序列分割成一系列的记号,记号分为关键字、标识符、字面量(数字、字符串)、特殊符号(加号、等号)等。 同时会将标识符号放入符号表,将数字、字符串常量放到文字表,以备后续使用。 针对上面 CompilerExpression.c 里面的代码,我们可以使用 clang 进行词法分析:clang -fmodules -fsyntax-only -Xclang -dump-tokens CompilerExpression.c ,打印如下: void 'void' [StartOfLine] Loc=<CompilerExpression.c:1:1> identifier 'test' [LeadingSpace] Loc=<CompilerExpression.c:1:6> l_paren '(' Loc=<CompilerExpression.c:1:10> r_paren ')' Loc=<CompilerExpression.c:1:11> l_brace '{' [LeadingSpace] Loc=<CompilerExpression.c:1:13> int 'int' [StartOfLine] [LeadingSpace] Loc=<CompilerExpression.c:2:5> identifier 'index' [LeadingSpace] Loc=<CompilerExpression.c:2:9> equal '=' [LeadingSpace] Loc=<CompilerExpression.c:2:15> numeric_constant '1' [LeadingSpace] Loc=<CompilerExpression.c:2:17> semi ';' Loc=<CompilerExpression.c:2:18> int 'int' [StartOfLine] [LeadingSpace] Loc=<CompilerExpression.c:3:5> identifier 'array' [LeadingSpace] Loc=<CompilerExpression.c:3:9> l_square '[' Loc=<CompilerExpression.c:3:14> numeric_constant '3' Loc=<CompilerExpression.c:3:15> r_square ']' Loc=<CompilerExpression.c:3:16> semi ';' Loc=<CompilerExpression.c:3:17> identifier 'array' [StartOfLine] [LeadingSpace] Loc=<CompilerExpression.c:4:5> l_square '[' Loc=<CompilerExpression.c:4:10> identifier 'index' Loc=<CompilerExpression.c:4:11> r_square ']' Loc=<CompilerExpression.c:4:16> equal '=' [LeadingSpace] Loc=<CompilerExpression.c:4:18> l_paren '(' [LeadingSpace] Loc=<CompilerExpression.c:4:20> identifier 'index' Loc=<CompilerExpression.c:4:21> ...上文有说过,词法分析的结果会将源代码分解成一个个小的 Token,标明了所在的行数和列数。 语法分析 对词法分析的结果进行语法分析,生成语法树(以表达式为节点的树),复杂的语句就是很多表达式的组合,编译器的开发者仅仅需要改变语法规则,即可适配多种编程语言。 上面的代码中的语句,就是由赋值表达式、加法表达式、乘法表达式、数组表达式、括号表达式组合成的复杂语句,经过语法分析之后,就会形成下图所示的语法树,在这个阶段如果出现表达式不合法(括号不匹配、表达式缺少操作符)编译器就会报告语法分析阶段的错误:语法分析会形成抽象语法树 AST,我们继续使用 clang 命令进行语法分析 clang -fmodules -fsyntax-only -Xclang -ast-dump CompilerExpression.c,得到的结果如下: TranslationUnitDecl 0x7fccdc822808 <<invalid sloc>> <invalid sloc> |-TypedefDecl 0x7fccdc823048 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128' | `-BuiltinType 0x7fccdc822dd0 '__int128' `-FunctionDecl 0x7fccdd04d048 <CompilerExpression.c:1:1, line:5:1> line:1:6 test 'void ()' `-CompoundStmt 0x7fccdd04d568 <col:13, line:5:1> `-BinaryOperator 0x7fccdd04d548 <line:4:5, col:40> 'int' '=' |-ArraySubscriptExpr 0x7fccdd04d3f0 <col:5, col:16> 'int' lvalue | |-ImplicitCastExpr 0x7fccdd04d3c0 <col:5> 'int *' <ArrayToPointerDecay> | | `-DeclRefExpr 0x7fccdd04d320 <col:5> 'int[3]' lvalue Var 0x7fccdd04d2a0 'array' 'int[3]' | `-ImplicitCastExpr 0x7fccdd04d3d8 <col:11> 'int' <LValueToRValue> | `-DeclRefExpr 0x7fccdd04d358 <col:11> 'int' lvalue Var 0x7fccdd04d160 'index' 'int' `-BinaryOperator 0x7fccdd04d528 <col:20, col:40> 'int' '*' |-ParenExpr 0x7fccdd04d488 <col:20, col:30> 'int' | `-BinaryOperator 0x7fccdd04d468 <col:21, col:29> 'int' '+' | |-ImplicitCastExpr 0x7fccdd04d450 <col:21> 'int' <LValueToRValue> | | `-DeclRefExpr 0x7fccdd04d410 <col:21> 'int' lvalue Var 0x7fccdd04d160 'index' 'int' | `-IntegerLiteral 0x7fccdd04d430 <col:29> 'int' 4 `-ParenExpr 0x7fccdd04d508 <col:34, col:40> 'int' `-BinaryOperator 0x7fccdd04d4e8 <col:35, col:39> 'int' '+' |-IntegerLiteral 0x7fccdd04d4a8 <col:35> 'int' 2 `-IntegerLiteral 0x7fccdd04d4c8 <col:39> 'int' 6上面的示例代码比较简单,我们来分析下这个 AST 中的几个节点:对于 Clang来说,顶层结构是TranslationUnitDecl (translation unit declaration :翻译单元声明),对 AST 树的遍历,实际上是遍历整个 TranslationUnitDec。 TypedefDecl 类型描述,对应typedef。 FunctionDecl 代表 C/C++方法定义。 CompoundStmt 代表了像 { stmt stmt } 这样的statement的集合。实际上就是用 {} and {{}} 包裹的代码块。 之前说过语法分析的结果,会生成以表达式为节点的树,clang AST 中的所有表达式都由 Expr 的子类表示。 BinaryOperator 类是 Expr 类的子类,其包括两个子节点。上文也说过在这个阶段如果出现表达式不合法(括号不匹配、表达式缺少操作符)编译器就会报告语法分析阶段的错误,所以我将 CompilerExpression.c 中的 array[index] = (index + 4) * (2 + 6) 中的 ; 去掉试一下: CompilerExpression.c:4:41: error: expected ';' after expression array[index] = (index + 4) * (2 + 6) ^ ; ... 1 error generated.果然报告了一个错误,提示 array[index] = (index + 4) * (2 + 6) 后面应该加上 ; 语义分析 经过词法分析和语法分析之后,语句是否真的有意义呢,这时候就需要进行语义分析,查看语句在语法上是否合法,需要注意的是编译器只能查看静态语义,动态语义需要运行时才能进行确定。 如果过程中出现了类型不匹配,编译器就会报错,经过语义分析之后的语法树如下:可以看出,编译期可以确定的表达式类型,都已经被确定好了(如果有隐式转换,语义分析会在语法树中插入转换节点),除此之外,符号表里面的类型也做了更新。 中间语言生成 编译器存在多个层次的优化行为,源代码级别的称之为源代码优化器,上述的经过语义分析之后的整个语法树,(2+6)会被优化成了8,其实并不是直接在语法树上进行优化,而是将整个语法树转化成中间代码,来顺序的标识语法树,常见三地址码和 P-Code 法两种方式。 中间代码层作为中间层的存在,使得编译器可以被分为前端和后端。编译器前端负责产生机器无关的中间代码,编译器后端将中间代码转换成目标机器代码。 针对上面代码采用三地址码法进行优化 : 最基本的三地址码长这样: x = y op z 表示变量 x 和变量 y 经过 op 操作后赋值给 x 例如函数内部的代码 array[index] = (index + 4) * (2 + 6);,经过中间层的源代码优化器 (Optimizer)优化后的代码最终为: t2 = index + 4 t2 = t2 * 8 array[index] = t2使用 clang 命令将语法树自顶向下遍历逐步翻译成 LLVM IR:clang -S -fobjc-arc -emit-llvm test.c -o test.ll 得到 .ll 文件,IR 代码如下: ; ModuleID = 'CompilerExpression.c' source_filename = "CompilerExpression.c" target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-apple-macosx12.0.0"; Function Attrs: noinline nounwind optnone ssp uwtable define void @test() #0 { %1 = alloca i32, align 4 %2 = alloca [3 x i32], align 4 store i32 1, i32* %1, align 4 %3 = load i32, i32* %1, align 4 %4 = add nsw i32 %3, 4 %5 = mul nsw i32 %4, 8 %6 = load i32, i32* %1, align 4 %7 = sext i32 %6 to i64 %8 = getelementptr inbounds [3 x i32], [3 x i32]* %2, i64 0, i64 %7 store i32 %5, i32* %8, align 4 ret void } ...BitCode 这里简单提一下我们经常听说的 BitCode,BitCode 是 iOS 9 引入的新特性,官方文档解释 BitCode 是一种中间代码,包含 BitCode 的应用程序会在 App Store 上编译和链接, BitCode 允许苹果在后期对我们的应用程序的二进制文件进行优化。其实就是 LLVM IR 的一种编码形式,如下图我们可以看到 BitCode 在编译环节所处的位置:但是在 Xcode 14 中 BitCode 被废除,iOS、tvOS 以及 watchOS 应用程序默认将不再支持 BitCode,在未来的 Xcode 版本中,BitCode 将被移除,主要原因是 Bitcode 并不是一个稳定的格式,因为在 LLVM 的设计里,它只是一个临时产生的文件,并不期望被长期存储,这导致它的兼容性很差,几乎 LLVM 每次版本更新时都会修改它,其次是对生态的要求很高,如果应用的任何一个依赖没有支持 Bitcode,那最终就无法使用。 目标代码生成与优化 编译器的后端主要包含代码生成器和目标代码优化器。代码生成器依赖于目标机器的字长、寄存器、整数数据类型和浮点数数据类型等,将中间代码转换目标机器代码。 array[index] = (index + 4) * (2 + 6); 经过源代码优化器又经过代码生成器变成如下代码: movl index, %ecx addl $4, %ecx mull $8, %ecx movl index, %eax movl %ecx, array(, eax, 4)上面代码经过目标代码优化器,会选择合适的寻址方式,使用位移来代替乘法运算,删除多余指令等,经过目标代码优化器之后代码如下(其中乘法改用相对复杂的基地址比例变址寻址的指令完成): movl index, %edx leal 32(, %edx, 8), %eax movl %eax, array(, %edx, 4)还有,我们都知道 Xcode 是使用 Clang 来编译 Objective-C 语言的,而 Xcode 供给我们 7 个等级的编译选项,在 Xcode -> Build Setting -> Apple LLVM 9.0 - Code Generation -> Optimization Level 中进行设置。None [-O0]:不优化 Fast [-O1]:大函数所需的编译时间和内存消耗都会稍微增加 Faster [-O2]:编译器执行所有不涉及时间空间交换的所有的支持的优化选项 Fastest [-O3]:在开启Fast [-O1]项支持的所有优化项的同时,开启函数内联和寄存器重命名选项 Fastest, Smallest [-Os]:在不显着增加代码大小的情况下尽量提供高性能 Fastest, Aggressive Optimizations [-Ofast]:与Fastest, Smallest [-Os]相比该级别还执行其他更激进的优化 Smallest, Aggressive Size Optimizations [-Oz]:不使用LTO的情况下减小代码大小设置不同优化选项,中间代码的大小会相应变化。 链接 代码经过标代码优化器,已经变成了最优的汇编代码结构,但是如果此时的代码里面使用到了别的目标文件定义的符号怎么办?这就引出了链接,之所以称之为链接,就是因为链接时需要将很多的文件链接链接起立,才能得到最终的可执行文件。 最开始的时候,程序员采用纸带打孔的方式输入程序的,然而指令是通过绝对地址进行寻址跳转的,指令修改过后,绝对的地址就需要进行调整,重定位的计算耗时又容易出错,于是就出现了汇编语言,采用符号的方式进行指令的跳转,每次汇编程序的时候修正符号指令到正确的地址。汇编使得程序的扩展更加方便,但是代码量开始膨胀,于是需要进行模块的划分,产生大量的模块,这些模块互相依赖又相对独立,链接之后模块间的变量访问和函数访问才有了真实的地址。模块链接的过程,本书的作者很形象生动的比作了拼图的过程,链接完成之后,才能产生一个可以真正执行的程序。链接的原理无非是对一些符号的地址加以修正的过程,将模块间的互信引用的部分都处理好。具体包括地址和空间分配、符号决议、重定位等步骤。多数情况下,目标文件和库需要一起进行链接,常用的一些基本函数大多属于运行时库(Runtime Library),链接器会根据引用的外部模块的符号,自动的去查找符号的地址,进行地址修正。 空间地址分配 到了比较重要的静态链接,上面说的 “拼图” 的过程,其实就是静态链接的过程,即将多个目标文件链接起来,形成一个可执行文件。 有这样两个文件 a.c 和 b.c,gcc -c a.c b.c 经过编译后形成 a.o 和 b.o: // a.c extern int shared; void swap(int* a, int* b); int main() { int a = 100; swap(&a, &shared); }// b.c int shared = 1; void swap(int* a, int* b) { *a ^= *b ^= *a ^= *b; }在 a.c 定义了两个外部符号 shared 和 swap ,b.c 中定义了一个 main 为全局符号,我们可以查看下通过clang进行编译,通过 MachOView 查看 a.o:可见 mov 这条指令中,shared 的部分的地址为 0x00000000,swap 的地址也为 0x00000000 (其中0xE8 为操作码)。这部分只是用来代替,真正的地址计算留给链接器。 接下来就是将两个目标文件进行链接,两个目标文件怎么合并呢? 方式一:直接将目标文件拼接起来这种拼接方式虽然简单,但是缺点很明显,段数量太多了不说,由于 x86的硬件来说,段的装载和空间对齐的单位是页,4096个字节,这就会导致即便是仅有1个字节的段,在内存中也会被分配4096个字节。 方式二:相似段合并现在的链接器大多都是采用两步链接的方法:空间与地址分配 扫描所有的目标文件 获取各个段的长度,属性和位置 收集符号表中的所有符号定义和符号引用,放入全局符号表 获得所有目标文件的长度,并且将其合并 建立合并后的文件的映射关系符号解析和重定位 获取上一步收集的段数据和重定位信息 进行符号解析 重定位 调整代码中的地址使用 ld 将 a.o 和 b.o 链接起来: ld a.o b.o -e main -o ab ,链接后使用的地址是进程中的虚拟地址。 小Tip:如果在 MacOS 系统中可直接使用 clang 命名链接目标文件 clang a.o b.o -o ab,如果直接使用 ld 进行链接可能会导致异常如下: ld: dynamic executables or dylibs must link with libSystem.dylib for architecture x86_64 即便添加了指定 libSystem:ld a.o b.o -e _main -o ab -lSystem ,也会报如下错误: ld: library not found for -lSystem 发现是因为指定库的地址,最后解决方案如下: ld a.o b.o -e _main -o ab -macosx_version_min 12.6 -L /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib -lSystem 我分析报错主要原因是因为,MacOS 系统在链接的时候,会默认使用 libSystem.dylib,在 Mach-O 中也能看到这个库的存在符号解析&重定位 何使用外部符号呢?比如 a.c 文件中就使用到了 shared 和 swap 两个外部符号,在 a.c 编译成目标文件的时候, shared 和 swap 两个的地址还不知道, 编译器会使用地址 0 当做 shared 的地址,函数的调用是一条叫 进址相对位移调用指令,这个我们放到最后来讲 swap 在目标文件中的地址也是一个临时的假地址 0xFFFFFFFC,在经过上一步的地址和空间分配之后,就已经可以确定所有符号的虚拟地址了。 我们看一下 a.o 和 经过链接之后的 ab,首先 objdump -d a.o 如下: a.o: file format mach-o 64-bit x86-64Disassembly of section __TEXT,__text:0000000000000000 <_main>: 0: 55 pushq %rbp 1: 48 89 e5 movq %rsp, %rbp 4: 48 83 ec 10 subq $16, %rsp 8: c7 45 fc 64 00 00 00 movl $100, -4(%rbp) f: 48 8d 7d fc leaq -4(%rbp), %rdi 13: 48 8b 35 00 00 00 00 movq (%rip), %rsi ## 0x1a <_main+0x1a> 1a: e8 00 00 00 00 callq 0x1f <_main+0x1f> 1f: 31 c0 xorl %eax, %eax 21: 48 83 c4 10 addq $16, %rsp 25: 5d popq %rbp 26: c3 retq13行 和 1a行地址是临时给到的,需要进行重定位,再使用 objdump 看下 ab ,objdump -d ab : ab: file format mach-o 64-bit x86-64Disassembly of section __TEXT,__text:0000000100003f50 <_main>: 100003f50: 55 pushq %rbp 100003f51: 48 89 e5 movq %rsp, %rbp 100003f54: 48 83 ec 10 subq $16, %rsp 100003f58: c7 45 fc 64 00 00 00 movl $100, -4(%rbp) 100003f5f: 48 8d 7d fc leaq -4(%rbp), %rdi 100003f63: 48 8d 35 96 00 00 00 leaq 150(%rip), %rsi ## 0x100004000 <_shared> 100003f6a: e8 11 00 00 00 callq 0x100003f80 <_swap> 100003f6f: 31 c0 xorl %eax, %eax 100003f71: 48 83 c4 10 addq $16, %rsp 100003f75: 5d popq %rbp 100003f76: c3 retq ...可见经过链接 swap 和 shared 符号的地址已经确定。 对比上面的 a.o 的 MachOView 结果,我们查看一下 ab:shared 的地址修正属于绝对地址修正:例如 b.o 文件中的 shared 函数的段偏移是 X 合并后的段的 b.o 的代码段的虚拟地址假设为 0x08048094 那么合并后的 shared 函数的地址为 0x08048094 + Xswap 是一条近址相对位移调用指令,它的地址是调用指令的下一条指令的偏移量,地址修正方式为:首先找到下一条指令的偏移量 0x00000011 找到下一条指令的地址,由上图可以看到 callq 指令的下一条指令地址为 0x00003F6F 所以 swap 的地址可以计算得出 0x00003F6F + 0x00000011 = 0x00003F80 至于链接器怎么就知道 shared 和 swap 是需要进行调整的指令呢?这里就涉及到了一个叫做重定位表的段,也叫做重定位段,其实上面也有说过,.rel.text 是针对代码段的重定位表,.rel.data 是针对数据段的重定位表, objdump -r a.o 结果如下: a.o: file format mach-o 64-bit x86-64RELOCATION RECORDS FOR [__text]: OFFSET TYPE VALUE 000000000000001b X86_64_RELOC_BRANCH _swap 0000000000000016 X86_64_RELOC_GOT_LOAD _shared@GOTPCRELRELOCATION RECORDS FOR [__compact_unwind]: OFFSET TYPE VALUE 0000000000000000 X86_64_RELOC_UNSIGNED __texta.o 中就存在了两个重定位入口,上图代表是代码段的重定位表,两个 offset 标识代码段中需要调整的指令的偏移地址。 C++相关问题 C++ 由于模板、外部内联函数、虚函数表等导致会产生很多重复的代码,目前的 GNU GCC 将每个模板代码放入一个段里,每个段只有一个模板的实例,当别的编译单元以相同的类型实例化模板函数的时候,也会生成和之前相同名称的段,最终在链接的时候合并到最后的代码段。C++ 还提供了一个叫做函数级别链接的编译选项,这个选项可以使所有函数都会被编译到单独的段里面,链接合并时,没有用到的函数就会被抛弃,减少了文件的长度,但是增加段的数量以及编译的时间。 C++ 的 main 之前需要初始化进程的执行环境等, main 之后需要做一些清理的工作,于是 ELF 文件还定义两个特殊的段 .init 和 .fini,一个放在main前由系统执行,一个放在main函数返回后执行。 目标文件可能是被两个不同的编译器产出的,那么两个目标文件能够进行链接的条件是:采用相同的目标文件格式 拥有同样的符号修饰标准 变量的内存分布方式相同 函数调用方式相同 ...其中 2,3,4 等是与可执行文件的二进制兼容性相关**(ABI)** ABI稳定 人们总是希望二进制和数据不加修改能够得到重用,但是实现二进制级别的重用还是很困难的,因为影响 ABI 的因素非常多,硬件、编程语言、编译器、链接器、操作系统都会影响 ABI。 C 代码层面的 ABI 稳定 从 C 语言的目标代码来说,下面几个因素会影响二进制是否兼容:内置类型的大小和在存储器中的放置方式(大端、小端、对齐方式)。 组合类型的存储方式和内存分布。 外部符号与用户定义的符号之间的命名方式和解析方式。 函数调用方式。 堆栈分布方式。 寄存器的使用约定。C ++ ABI 稳定 到了 C++ 时代,做到二进制兼容更是不易,需要考虑:继承类体系的内存分布。 指向成员函数的指针的内存分布。 如何调用虚函数,vtable 的内容和分布形式,vtable 指针在 object 中的位置。 模板如何实例化。 外部符号的修饰。 全局对象的构造和析构。 异常产生和捕获机制。 标准库的细节问题和 RTTI 如何实现。 内嵌函数访问细节等。二进制的兼容,一直都是语言发展过程中的重要事务,比如还有很多人还在致力于 C++的标准的统一。 Swift ABI稳定 从16年就接触了 Swift3.0 的开发,当时当时的 Swift 语言还是在飞速迭代的过程中,每次一个小的版本的升级,就会有大量的代码需要改动,我甚至还误以为这个是由于 Swift 的 ABI 不稳定造成的,这其实是错的,直到 Swift 5 发布,Swift 5 最重要的变化就是 ABI Stability,ABI 稳定之后,OS 发行商就可以把 Swift 标准库和运行时作为操作系统的一部分嵌入。也就是说 Apple 会把 Swift runtime 放到 iOS 和 macOS 系统里,我们的 Swift App 包里就不需要包含应用使用的标准库 和 Swift runtime 拷贝了。同时在运行的时候,只要是用 Swift 5 (或以上) 的编译器编译出来的 Binary,就可以跑在任意的 Swift 5 (或以上) 的 runtime 上。 ABI & API 此外有个与之对应的有个概念叫做 API,实际上它们都是应用程序接口,只是接口所在层面不同:API 是指源代码级别的接口 ABI 是指二进制层面的接口。大端小端就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。静态库链接 静态库可以看做是一组目标文件的集合,举例几个 C 语言的运行的库:C运行库 相关 DLL 相关 DLLlibcmt.libMultithreaded Static 多线程静态库msvert.lib msver90.dll Multithreaded Dynamic 多线程动态库libcmtd.libMultitbreaded Static Debug 多线程静态调试库msvertd.lib msvert90d.dll Multichreaded Dynamic Debug 多线程动态调试库链接使用到静态库的过程如下所示,其实目标文件中使用了printf函数:然而我们直接将 hello.o 和 printf.o 链接在一起 ld hello.o print.o,却报错了,其原因是 printf.o 中使用到了其他的库文件。 链接过程控制 特殊情况下我们需要控制链接规则,这就引出了链接过程控制。一共有三种办法做到控制:指定链接器参数,-o -e 之类。 将链接指令放在目标文件里面。 使用链接控制脚本。本身是存在默认的链接脚本的,例如在 Intel IA32下,我们使用 ld 链接生成可执行文件时,默认使用的 elf_i386.x 脚本,使用 ld 链接生成共享目标文件时,默认使用的是 elf_i386.xs 脚本。当然我们可以自己写脚本,来控制链接过程。 作者举了一个使用 ld 脚本干预链接的例子,程序代码结合的是 GCC 内嵌汇编,不借助库函数,可以打印 Hello World 程序,代码如下: char* str = "Hello world! \n"; void print () { asm("movl $13, %%edx \n\t" "movl %0, %%ecx \n\t" "movl $0, %%ebx \n\t" "movl $4, %%eax \n\t" "int $0x80 \n\t" :: "r"(str): "edx", "ecx", "ebx"); } void exit () { asm("movl $42, %ebx \n\t" "movl $1, %eax \n\t" "int $0x80 \n\t"); } void nomain () { print(); exit(); }编译:gcc -c -fno-builtin TinyHelloWorld.c 链接:ld -static -e nomain -o TinyHelloWorld TinyHelloWorld.o 这段代码的可执行代码一共会生成4个段,使用 ld 脚本可以合并部分段,并删除多余的段,脚本如下: ENTRY (nomain) SECTIONS { . = 0x08048000 + SIZEOF_HEADERS; tinytext : { *(.text) *(.data) *(.rodata) } /DISCARD/ : { *(.comment) ) }脚本一共做了几件事呢?第一行执行了程序的入口为 nomain。 SECTIONS 里面是变换规则,第一句的意思是设置 tinytext 的段的起始虚拟地址为 0x08048000 + SIZEOF_HEADERS。 第二句意思是将 .text 段、.data 段、.rodata 段合并为 tinytext 段。 第三句意思是将 .comment 段丢弃。Note:除了 tinytext 段之外,其实还会同时存在 .shstrtab 段、.symtab 段、.strtab 段(段名字符串表、符号表、字符串表) 此外,现在的 GCC 都是通过 BFD 库来处理目标文件的,BFD 库会把目标文件抽象成一个统一的模型,然后就可以操作所有支持 BFD 支持的目标文件格式。 目标文件里有什么 整体结构 目前流程的可执行文件的格式分为两种,Windows 下的 PE 和 Linux 下的 ELF,均为 COFF 的变种,目标文件是源代码经过编译后但是没有进行链接的中间文件,结构和内容和可执行文件类似,Windows 下目标文件和可执行文件统称为 PE/COFF 文件格式,Linux 系统下统称为 ELF 的文件。 此外,除了可执行性文件,动态链接库(Linux 下的 .so)、静态链接库(Linux 下的 .a)都是按照可执行文件的格式存储,在 Linux 下可以通过 file 命令查看具体类型。 ELF 目标文件的总体结构如下,之所以 Section Table 和 .rel.text 与前一个段有间隔,是因为内存对齐的原因:ELF 开头是一个头文件,描述了整个文件的文件基本属性,了 ELF 魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI 版本、ELF 重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度及段的数量等。 头文件还包括一个段表 Section Table,是一个描述各个段的数组,描述了段名、段的长度、在文件的偏移位置、读写权限以及段的其他属性。编译器、链接器和装载器都是依靠段表来定位和访问各个段的属性。 头文件后是各个段的内容。 执行语句编译成的机器代码在代码段(.text 段)。 已经初始化的全局变量和局部静态变量都在数据段(.data 段)。 未初始化的全局变量和局部静态变量一般放在一个叫 BSS 段中。(BSS 段只是预留位置,在文件中不会占据空间) 其他段...总体说,程序代码被编译后主要分为两种,程序指令和程序数据,为什么指令和数据分开来放呢?方便权限划分(只读和可读写等)。 划分了空间,提高 CPU 缓存的命中率。 方便指令或者数据的复用性,节省内存。比如说“共享指令”。段表 段表 Section Header Table 用于保存 ELF 文件中的所有段的基本属性的结构,是除了文件头以外中重要的结构,描述了各个段的段名、段的长度、在文件中的偏移、读写权限、以及段的其他属性。编译器、链接器、装载器都是依靠段表来定位各个段的和访问各个段的,段表在 ELF 文件的位置是由 ELF 文件头中的 e_shoff 成员决定的。段表其实是一个 EIf32_Shdr 的结构体的数组,里面包含段名 sh_name、段类型 sh_type、段的标志位 sh_flags、段的虚拟地址 sh_addr、段的偏移 sh_offset、段的长度 sh_size、段的链接信息 sh_link 和 sh_info、段的地址对齐 sh_addralign 等等。 段的名字只是在链接和编译过程中有意义,但是不能真正标识段的类型,段的属性是由段的类型 sh_type 和段的标志位 sh_flag 共同来决定的。段的类型是由 SHT_ 开头的,例如 SHT_PROGBITS 代表代码段、数据段,SHT_SYMTAB 代表符号表,SHT_STRTAB 代表字符串表。 段的标志位 sh_flag 标识了段是否可写、可执行,以 SHF_ 开头,例如SHF_WRITE 代表段在进程空间中可写, SHF_ALLOC 代表进程空问中须要分配空间 下面列举几种常见的系统保留段的段类型和段标志如下:Name sh_type sh flag.bss SHT_NOBITS SHF_ALLOC + SHF_WRITE.data SHT_PROGBITS SHF_ALLOC + SHF_WRITE.shstrtab SHT_STRTAB none.strtab SHT_STRTAB 如果该 ELF 文件中有可装載的段须要用到该字符串表,那么该字符串表也将被装载到进程空问,则有 SHF_ALLOC 标志位.symtab SHT_SYMTAB 同字符串表.text SHT_PROGBITS SHF_ALLOC + SHF_EXECINSTR下面介绍段的链接信息,如果段是与链接有关的(静态链接或者动态链接),比如说重定位表、符号表,在 sh_link 和 sh_info 字段中会包含链接相关的信息。sh_type sh_link sh_infoSHT_DYNAMIC 该段所使用的字符串表在段表中的下标 0SHT_HASH 该段所使用的符号表在段表中的下标 0SHT_REL 该段所使用的相应符号表在段表中的下标 该重定位表所作用的段在段表中的下标SHT_RELA 该段所使用的相应符号表在段表中的下标 该重定位表所作用的段在段表中的下标SHT_SYMTAB 操作系统相关的 操作系统相关的SHT_DYNSYM 操作系统相关的 操作系统相关的other SHN UNDEF 0常见段常用的段名 说明.text 程序指令.data 已经初始化的全局静态变量和局部静态变量.rodata 程序里的只读数据.bss 未初始化的全局变量和局部静态变量.comment 注释信息段.note.GNU-stack 堆栈提示段.rodata1 Read only Data,这种段里存放的是只读数据,比如字符串常量、全局 const变量。跟 “.rodata” 一祥.comment 存放的是编译器版本信息,比如字符串:"GCC: (GNU) 4.2.0”.debug 调试信息.dynamic 动态链接信息.hash 符号哈希表.line 调试时的行号表,即源代码行号与编译后指令的对应表.note 额外的编译器信息。比如程序的公司名、发布版本号等.strtab String Table 字符串表,用于存储ELF文件中用到的各种字符串.symtab Symbol Tabie 符号表.shstrtab Section String Table 段名表.plt.got 动态链接的跳转表和全局入口表.init.fini 程序初始化与终结代码段为了了解常见的段,我们先自己编译一段代码 SimpleSection.c: int printf (const char* format, ...); int global_init_var = 84; int global_uninit_var;void func1(int i) { printf( "%d\n",i); } int main(void) { static int static_var = 85; static int static_var2; int a = 1; int b; func1(static_var + static_var2 + a + b); return a; }只编译不链接该 SimpleSection.c 文件: gcc -c SimpleSection.c 得到 SimpleSection.o,SimpleSection.o 即为目标文件。 我们使用 objdump -h SimpleSection.o 查看其内部结构:除了上面我们提到过的代码段,数据段,BSS 段之外,还有三个段分别为只读数据段(.rodata 段),注释信息段(.comment 段),堆栈段(.note.GNU-stack),首先 BSS 段和堆栈段认为在文件中不存在,实际存在的段的分布情况如下:使用 size 命令可以查看各个段的长度:size SimpleSection.o text data bss dec hex filename 95 8 4 107 6b SimpleSection.o代码段 使用 objdump -s -d SimpleSection.o 可以查看所有段的十六进制内推以及所有包含指令的反汇编,我们着重看下代码段的内容:可见代码段里面包含就是两个函数 func1() 和 main() 函数的指令。 数据段和只读数据段 中存放的是已经初始化的全局静态变量和局部静态变量,上述代码中的中的 global_init_var 和 static_var 为全局静态变量和局部静态变量,加一起一共是 8 个字节,在调用 printf 函数的时候,用到了一个字符串常量,是一种只读数据,放入了 .rodata 段,刚好是 4 个字节。 BSS段 编译单元内部可见的未初始化的静态变量的确放到了BSS 段,但是未初始化的全局变量却不一定放在 BSS 段,例如上述代码中的 global_uninit_var 和 static_var2 就应该放在了BSS 段,但是我们看到该段只有四个字节的大小,其实只有 static_var2 放在了BSS 段,而 global_uninit_var 却没有放进任何段,只是一个未定义的 COMMON 符号,具体的原因我们放到下面关于符号的位置来讲解。 重定位表 链接器在处理目标文件的时候,需要对代码段和数据段中绝对地址的引用位置进行重定位,重定位的信息会记录在 ELF 文件的重定位表里,对于需要进行重定位的代码段或者数据段,都会需要一个重定位表,.rel.text 是针对代码段的重定位表,.rel.data 是针对数据段的重定位表,重定位表的 sh_type 为 SHT_REL,而 sh_link 字段记录该段所使用的相应符号表在段表中的下标,sh_info 表示该重定位表所作用的段在段表中的下标。 字符串表 ELF 文件中有许多类似于段名、变量名之类的字符串,使用字符串表,通过定义在表中的偏移来引用。字符串表在 ELF 中也是以段的形式存在..strtab 字符串表 .shstrtab 段表字符串表结合 ELF 头文件中的 e_shstrndx 即可找到段表和段表字符串表的位置,从而解析 ELF 文件。 自定义段 我们可以自己插入自定的段,做一些特定的事情,但是自定义段不能使用 . 开头,我们在全局变量或者函数加上 __attribute__((section("name"))) 就可以将相应的变量或者函数放到 name 为名的段中。 符号 链接的本质是将不同的目标文件互相 “粘” 到一起,还记得上文中的拼图吗,很形象生动,而链接中需要使用到的就是符号的名字,我们将函数和变量统称为符号 Symbol,函数名和变量名即符号名。每个目标文件中都会有一个符号表 Symbol Table,每个符号都有对应的符号值,对于变量和函数,符号值就是它们的地址。 常见的符号类型定义在本目标文件的全局符号,可以被其他目标文件引用。 外部符号,即在本目标文件中引用的全局待号,却没有定义在本目标文件。 段名,这种符号往往由编译器应生,它的值就是该段的起始地址。 局部符号,这类符号只在编译单元内部可见。 行号信点。使用 nm 可以查看目标文件的符号表: nm SimpleSection.o 打印的所有符号如下: 00000000 T funcl 00000000 D global_init_var 00000004 C global_uninit_var 0000001b T main U printf 00000004 d static_var.1286 00000000 b static_var2.1287符号表结构 符号表也是属于 ELF 文件中的一个段, 段名叫 .symtab,它是一个结构体的数组,结构体里面有几个重要的元素。 我们查看下 64 位的 Elf64_Sym 结构定义如下: typedef struct { Elf64_Word st_name; unsigned char st_info; unsigned char st_other; Elf64_Half st_shndx; Elf64_Addr st_value; Elf64_Xword st_size; } Elf64_Sym; //64位st_info 符号类型和绑定信息,低4位表示符号类型,高28位表示符号绑定信息。 st_shndx 这个值如果符号在目标文件中,这个符号就是表示符号所在的段在段表中的下标,如果符号不在目标文件中,sh_shndx 可能会有些特殊,例如 SHN_ABS 表示该符号包含一个绝对的值,SHN_COMMON 表示该符号是一个 COMMON 块类型的符号,SHN_UNDEF 表示符号未定义,可能定义在其他的目标文件中。 st_value 符号值。 如果段不是 COMMON 块(即 st_shndx 不为 SHN_COMMON),则符号对应的函数或者变量位于由 st_shndx 指定的段经过 st_value 偏移所在的位置,这种是目标文件中定义全局变量的最常见的情况。 如果是 COMMON 块(即 st_shndx 不为 SHN_COMMON), st_value 表示符号对齐属性。 如果在可执行文件中,st_value 表示符号的虚拟地址,对于动态链接器十分有用。此外还有一些特殊符号,例如 __executable_start 为程序起始地址,__etext 或 _etext 或 etext 标识代码段结束地址等等。我们使用 readelf 查看 ELF 文件的符号: readelf -s SimpleSection.o:Num 表示符号数组的下标,Value 是符号值,Size 为符号大小,st_info 为符号类型和绑定信息,Ndx 即 st_shndx 表示符号所属的段,举几个例子:func1 和 main 位置在代码段, Ndx 是1,类型为 STT_FUNC,由于是全局可见的,所以是 STB_GLOBAL, Size 表示指令所占字节数, Value表示函数相对于代码段的起始位置的偏移。 printf 这个符号,在 SimpleSection.c 中被引用,,但是没有定义,所以 Ndx 是 SHN_UNDEF。 global_init_var 是已初始化的全局变量,定义在 BSS 段,下标为 3. global_uninit_var 是未初始化的全局变量,是一个 SHN_COMMON 类型的符号,本身未存在于 BSS 段。 static_var 和 static_var2 绑定属性是 STB_LOCAL,编译内部单元可见。 STT_SECTION,表示下标为 Ndx 的段的段名,符号名没有显示,其实符号名即为段名。符号修饰和函数签名 为了防止符号的冲突,Unix系统规定 C 语音源代码的所有的全局变量和函数经过编译后,会在符号前面加上下划线。C++ 为了符号命名冲突的问题,增加了命名空间。 C++ 拥有类、继承、虚机制、重载、命名空间等特性,使得符号管理更为复杂,于是引入了函数签名,函数签名包含函数名。参数类型,所在类以及命名空间等一些列信息,用于识别不同的函数,即便函数名相同,编译器也会更具修饰后的名称,认为它们是不同的函数,修饰后的名称如表:函数签名 修饰后名称(符号名)int func(int) __Z4funcifloat func(float) __Z4funcfint C::func(int) __ZN1C4funcEiint C::C2::func(int) __ZN1C2C24funcEiint N::func(int) __ZNIN4funcEiint N::C::func(int) __ZNINICAfuncEi变量的类型没有加入到修饰后的名称中。 C++ 为了和 C 兼容, 还引入了 extern "C" 关键字: extern "C" { int func(int); int var; }在 {} 中的代码会被 C++ 编译器当做 C 的代码来处理, 当然 C++ 的名称机制也将不起作用。 如果是单独的某个函数或者变量定义为 C 语言的符号也可以使用 extern: extern "C" int func(int); extern "C" int var;强符号和弱符号 开发中我们经常会遇到符号被重复定义的错误,比如说我们在两个目标文件中都定义了相同的全局整形变量 global,并将它们同时初始化,那么链接器将两个目标文件链接的时候就会报错,对于 C/C++ 语言来说,这种已初始化的全局符号可以称之为强符号,有些符号的定义称之为弱符号,比如说未初始化的全局符号,强符号和弱符号是针对定义来说的,而不是针对符号的引用。我们也可以使用 __attribute__((weak)),来定义强符号为弱符号,下面我们看一段代码: extern int ext; // 非强符号也非弱符号int weak; // 弱符号 int strong = 1; // 强符号 __attribute__((weak)) weak2 = 2; // 弱符号int main() { // main 强符号 return 0; }上段代码的强弱符号已经进行了标注,ext 由于是一个外部变量的引用,非强符号也非弱符号。 链接器会按照下面的三个规则处理与选择多次定义的全局符号:不允许强符号被定义多次(不同的目标文件中不能有同名的强符号,否则链接报错)。 如果有一个是强符号其余的是弱符号,则选择强符号。 如果不同的目标文件中都是弱符号,则选择占用空间最大的一个。现在的链接器在处理弱符号的时候,采用的 COMMON 块一样的机制来处理,编译器将未初始化的局部静态变量定义为弱符号,还记得上面我留了一个问题,编译器将未初始化的局部静态变量 static_var2 放在 BSS 段,而 global_uninit_var 属于未初始化的全局符号,没有直接放入 BSS 段,先是标记了 COMMON,按照 COMMON 链接规则,global_uninit_var 的大小以输入文件中最大的那个为准,最终确认了符号的大小,就能放入 BSS 段了。但是有种情况,如果是同时存在强符号和弱符号,那么输出文件和强符号相同,但是如果链接过程中有弱符号大于强符号,那么 ld 就会报出警告。 对于目标文件被链接成可执行文件的阶段,如果是强符号没有被定义将会报错,如果弱符号没有被定义,链接器对于该引用不会报错,一般都会被赋予一个默认值,便于程序代码识别到。但是在运行阶段,可能会发生非法地址访问的错误。 强符号和弱符号的设计对于库来说十分有用,比如:库中定义的弱符号可以被强符号进行覆盖,从而使用自定义的库函数。 程序的某些扩展功能模块定义为弱引用,扩展模块和程序一起链接的时候就能使用模块功能,去掉模块功能也能正确链接,只是缺少部分功能。这个设计使得程序的设计更为灵活,我们可以对大的功能模块进行自由的组合和裁切。调试信息 调试信息似使得我们进行源代码级别的调试,可以设置断点,监测变量的变化,单步运行,确定目标代码的地址对应源代码中的哪一行、函数和变量的类型等等。 ELF 文件中采用一个叫做 DWARF 标准的调试信息格式,调试信息占用空间比较大,我们再发布程序的时候,往往需要使用 strip 命令来去掉 ELF 文件中的调试信息。 DWARF 一种通用的调试文件格式,支持源码级别的调试,调试信息存在于 对象文件中,一般都比较大。Xcode 调试模式下一般都是使用 DWARF 来进行符号化的。 通过 DWARF 清晰的看到函数的描述、行号、所在文件、虚拟地址等重要信息,有了这些信息,就可以实现单步调试以及查看 Crash 堆栈等能力。 说到 DWARF 可能我们还不是很熟悉,但是有一个文件,iOS 的程序员应该不陌生,那就是 dSYM 文件,日常开发时会遇到 Crash,Crash 里面有很多的堆栈信息,以及 Crash 时所执行的代码的行号,这些信息对定位问题非常重要,这个能力就是依赖 DWARF 和 dSYM 实现的。当然 DWARF 和 dSYM 是公共的标准,并不是只有苹果特有的,只不过主要是苹果在用而已。使用 Xcode 编译打包的时候会先通过可执行文件的 Debug Map 获取到所有对象文件的位置,然后使用 dsymutil 来将对象文件中的 DWARF 提取出来生成 dSYM 文件。 Strip 上文说到可以使用 Strip 命令来去掉 ELF 文件中的调试信息,在 Xcode 中其实已经给我们提供了 Strip 编译选项,之所以要在 Release 环境中去掉符号信息,主要是因为调试信息占用的空间太大了,需要进行 App 的瘦身操作。 Strip 命令就是为了去除调试信息,其中符号占据了绝大部分,而可执行文件中的符号是指程序中的所有的变量、类、函数、枚举、变量和地址映射关系,以及一些在调试的时候使用到的用于定位代码在源码中的位置的调试符号,符号和断点定位以及堆栈符号化有很重要的关系。 Xcode 编译实际的操作步骤是:生成带有 DWARF 调试信息的可执行文件 -> 提取可执行文件中的调试信息打包成 dSYM -> 去除符号化信息。去除符号是单独的步骤,使用的是 strip 命令,下面介绍两个有关于 strip 命令的 Xcode 编译选项: Strip Style Strip Style 表示的是我们需要去除的符号的类型的选项,其分为三个选择项:All Symbols Non-Global Symbols Debug Symbols去除所有符号,一般是在主工程中开启 (保留全局符号,Debug Symbols 同样会被去除),链接时会被重定向的那些符号不会被去除,此选项是静态库/动态库的建议选项。 去除调试符号,去除之后将无法断点调试。Strip Linked Product Xcode 提供给我们 Strip Linked Product 来去除不需要的符号信息(Strip Style 中选择的选项相应的符号),去除了符号信息之后我们就只能使用 dSYM 来进行符号化了,所以需要将 Debug Information Format 修改为 DWARF with dSYM file。 去除符号之后,调试阶段怎么办 去除符号化信息之后我们只能使用 dSYM 来进行符号化,那我们怎么使用 Xcode 来进行调试呢? Strip Linked Product 选项在 Deployment Postprocessing 设置为 YES 的时候才生效,而在 Archive 的时候 Xcode 总是会把 Deployment Postprocessing 设置为 YES 。所以我们可以打开 Strip Linked Product 并且把 Deployment Postprocessing 设置为 NO,而不用担心调试的时候会影响断点和符号化,同时打包的时候又会自动去除符号信息。 Mach-O 文件 经过 ELF 文件的学习,我们重温一下 iOS 里的 Mach-O 文件格式,Mach-O 是 Mach object 文件格式的缩写,是一种可执行文件、目标代码、共享程序库、动态加载代码和核心 dump,它类似于 Linux 和大部分 UNIX 的原生格式 ELF 以及 Windows 上的 PE。可见其主要包含三个部分: Header:记录了Mach-O文件的基本信息,包括CPU架构、文件类型和Load Commands等信息。 Load Commands:加载命令部分描述了需要内核加载器或动态连接器等进行的操作指令,如加载数据段、加载动态库等。 Section Data:每一个Segment的数据都保存在此,描述了段名、类型、段偏移,段大小等信息,每个 Segment 拥有一个或多个 Section ,用来存放数据和代码。 Mach-O文件中 中 Data 段之后就都是 __LINKEDIT 部分,具体如下:Dynamic Loader Info 动态加载信息Function Starts 函数起始地址表Symbol Table 符号表信息Data in Code Entries 代码入口数据Dynamic Symbol Table 动态符号表String Table 字符串表信息Code Signature 代码签名String Table 字符串表所有的变量名、函数名等,都以字符串的形式存储在字符串表中。 Symbol Table 符号表,这个是重点中的重点,符号表是将地址和符号联系起来的桥梁。符号表并不能直接存储符号,而是存储符号位于字符串表的位置。 Header struct mach_header_64 { uint32_t magic; /* 标识当前 Mach-O位32位(0xfeedface)/ 64位 (0xfeedfacf) */ cpu_type_t cputype; /* CPU 类型 */ cpu_subtype_t cpusubtype; /* CPU 子类型 */ uint32_t filetype; /* 文件类型 */ uint32_t ncmds; /* Load Commands 数量 */ uint32_t sizeofcmds; /* Load Commands 的总大小 */ uint32_t flags; /* 标识位,记录文件的详细信息 */ uint32_t reserved; /* 64位文件特有的保留字段 */ }Load Commands Load command描述了文件中数据的具体组织结构,不同的数据类型使用不同的加载命令。它的大小和数目在header中已经被提供。 struct load_command { uint32_t cmd; /* cmd 类型 */ uint32_t cmdsize; /* cmd size */ };Load Commands 的部分信息如下:LC_SEGMENT_64 将文件中的段映射到进程地址空间中LC_DYLD_INFO_ONLY 动态链接相关信息LC_SYMTAB 符号表地址LC_DYSYMTAB 动态符号地址LC_LOAD_DYLINKER 指定内核执行加载文件所需的动态连接器LC_UUID 指定图像或其对应的dSYM文件的128位UUIDLC_VERSION_MIN_MACSX 文件最低支持的操作系统版本LC_SOURCE_VERSION 源代码版本LC_MAIN 程序main函数加载地址LC_LOAD_DYLIB 依赖库路径LC_FUNCTION_STARTS 函数起始表地址LC_CODE_SIGNATURE 代码签名几种常见的命令简介如下: 使用最多的是 LC_SEGMENT_64 命令,该命令表示将相应的 segment 映射到虚拟地址空间中,一个程序一般会分为多个段,每一个段有唯一的段名,不同类型的数据放入不同的段中,LC_SEGMENT_64 中包含了五种类型:PAGEZERO:可执行文件捕获空指针的段 TEXT:代码段和只读数据 DATA_CONST:常态变量 DATA:全局变量和静态变量 LINKEDIT:包含动态链接器所需的符号、字符串表等数据动态链接相关信息:LC_DYLD_INFO_ONLY:Rebase:进行重定向的位置信息。当 Mach-O 加载到内存里,系统会随机分配一个内存偏移大小 ASLR,和 rebase 里面的 offset,对接(位置相加)获取代码在内存中的实际位置。再根据 size 开辟实际内存。 Binding:绑定的位置信息 Weak Binding:弱绑定的位置信息 Lazy Binding:懒加载绑定的位置信息 Export:对外的位置信息LC_SYMTAB 标识了 Symbol Table 和 String Table 的位置。 LC_LOAD_DYLINKER 标识了动态连接器的位置,用来加载动态库等。 Mach-O 程序入口:设置程序主线程的入口地址和栈大小 LC_MAIN,反编译后根据 LC_MAIN 标识的地址可以找到入口 main 代码,dyld 源码中 dyld::_main 可以看到 LC_MAIN 的使用,获取入口和调用。 LC_LOAD_DYLIB 是比较重要的加载动态库的指令,Name 标识了具体的动态库的路径,对一个 Mach-O 注入自定义的动态库时就是在 Load Commands 和 Data 中间添加 LC_LOAD_DYLIB 指令和信息进去。 Data Data 分为 Segment 和 Section 两个部分,存放代码、数据、字符串常量、类、方法等。 Segment 结构体定义如下: struct segment_command_64 { /* for 64-bit architectures */ uint32_t cmd; /* Load Commands 部分中提到的cmd类型 */ uint32_t cmdsize; /* cmd size */ char segname[16]; /* 段名称 */ uint64_t vmaddr; /* 段虚拟地址(未偏移),真实虚拟地址要加上 ASLR 的偏移量 */ uint64_t vmsize; /* 段的虚拟地址大小 */ uint64_t fileoff; /* 段在文件内的地址偏移 */ uint64_t filesize; /* 段在文件内的大小 */ vm_prot_t maxprot; /* maximum VM protection */ vm_prot_t initprot; /* initial VM protection */ uint32_t nsects; /* 段内 section数量 */ uint32_t flags; /* 标志位,用于描述详细信息 */ };而对于**__TEXT** 和 __DATA 这两个 Segment,则可以继续分解为 Section,从而形成 Segment -> Section 的结构。之所以要这样设计,是因为在同一个 Segment 下的 Section 可以拥有相同的控制权限,并且可以不完全按照 Page 的大小进行内存对齐,从而达到节约内存的效果。 Section 结构体定义如下: struct section_64 { /* for 64-bit architectures */ char sectname[16]; /* section名称 */ char segname[16]; /* 所属的segment名称 */ uint64_t addr; /* section在内存中的地址 */ uint64_t size; /* section大小 */ uint32_t offset; /* section在文件中的偏移*/ uint32_t align; /* 内存对齐边界 */ uint32_t reloff; /* 重定位入口在文件中的偏移 */ uint32_t nreloc; /* 重定位入口数量 */ uint32_t flags; /* flags (section type and attributes)*/ uint32_t reserved1; /* reserved (for offset or index) */ uint32_t reserved2; /* reserved (for count or sizeof) */ uint32_t reserved3; /* reserved */ };常见的__TEXT Segment 的 Section 如下:__text: 可执行文件的代码区域 __objc_methname: 方法名 __objc_classname: 类名 __objc_methtype: 方法签名 __cstring: 类 C 风格的字符串常见的__DATA Segment 的 Section 如下__nl_symbol_ptr: 非懒加载指针表,dyld 加载会立即绑定 __ls_symbol_ptr: 懒加载指针表 __mod_init_func: constructor 函数 __mod_term_func: destructor 函数 __objc_classlist: 类列表 __objc_nlclslist: 实现了 load 方法的类 __objc_protolist: protocol 的列表 __objc_classrefs: 被引用的类列表 __objc _catlist: Category 列表我们可以使用系统自带查看 Mach-O 的工具:file : 查看 Mach-O 的文件类型 nm: 查看 Mach-O 文件的符号表 otool: 查看 Mach-O 特定部分和段的内容 lipo: 常用于多架构 Mach-O 文件的处理总结编译过程主要是分为 词法分析、语法分析、语义分析、生成中间代码、目标代码的生成与优化。链接的过程主要涉及到空间地址的分配、符号的解析、重定位等过程,我们可以对链接的过程通过脚本等加以控制,合并部分段,忽略个别段等。ELF 文件的主要构成,文件头、段表、各种常见段(代码段、数据段、BSS 段、只读数据段等)。关于符号大家也有了基本的认知,常见符号类型(全局符号、外部符号、段名等)。符号表提供的值得关注的信息(符号类型和绑定信息,符号所占位置、符号值),为了解决符号的冲突,C 编译后会在符号前加上下划线,C++ 编译器提供了修饰后的名称。符号分为强符号和 弱符号,对于 C/C++ 语言来说,已初始化的全局符号可以称之为强符号,未初始化的全局符号为弱符号。DWARF 一种通用的调试文件格式,支持源码级别的调试,但是所占体积较大,我们可以使用 Strip 命令来去掉 ELF 文件中的调试信息。Mach-O 是 MacOS/iOS 系统下的执行文件等的格式,有 Header、Load Command、Data 组成。
《程序员的自我修养(链接、装载与库)》学习笔记一(稳固而知新)
温故而知新 静态链接 装载与动态链接 库与运行库计算机发展 编译和链接 可执行文件的装载与进程 内存软件体系结构 目标文件里有什么 动态链接 运行库操作系统 静态链接 Linux 共享库的组织 系统调用与 API内存、线程 Windows PE/COFF Windows 下的动态链接 运行库实现在这书里有一句话,是之前认识的一个研发长者经常挂在嘴边的,今天在书中看到了感触颇多:经常听很多人谈起,IT 技术日新月异,其实真正核心的东西数十年都没怎么变化,变化的仅仅是它们外在的表现,大体也是换汤不换药吧。本书的作者介绍之所以想写这本书,其实主要也是因为不满足于技术的表面,想探索问题的根源。就像上面写的,技术发展日新月异,但是核心的东西却是相对稳定不变的,那么对于从事软件开发的工程师,研究人员,学习这些底层的知识就很有必要了,很多技术都是相通的,认识了底层才更能看清事情的表象,达到触类旁通的效果,毕竟只会写代码不是好程序员,这也是我想学习这本书的原因之一。 除此之外,这本书被大家评价为国人难得写的比较不错的一本计算机技术书籍,并且成为很多大厂人员的必读书籍,肯定是有其魅力所在的,那么是时候认真阅读一下了。 温故而知新 本书的第一章主要分为五个部分,除了第一部分主要是抛出几个问题让大家一起思考之外,剩下部分分别为计算机的发展,软件体系结构,内存和线程。具体知识点分布如图所示:Hello World引发的思考 #include <stdio.h>int main() { printf("Hello World\n"); return 0; }针对这小段代码,本书抛出了一些问题。为什么程序编译了才能运行? 编译器将 C 代码转化为机器码,做了什么? 编译出的可执行文件中有什么,存放机制是什么? ....关于上述问题,本书会从基本的编译、链接开始讲解,然后到装载程序、动态链接等。 计算机基本结构以及CPU的发展 结构 计算机基本结构:CPU、内存、I/O 控制芯片,如下图:CPU 的发展史早期 CPU 的核心频率很低,几乎等于内存频率,每个设备都会有一个 I/O 控制器,链接在一条总线(Bus)上。 随着 CPU 核心频率的提升,内存访问速度低于 CPU,于是增加了处理高速 I/O 的北桥芯片和处理低速 I/O 的南桥芯片。 CPU 速度达到极限后,又增加了多核处理器 SMP。软件体系结构 下图为计算机的软件体系结构分层:计算机软件体系结构是分层的,层与层之间通信的协议,称之为接口。 开发工具与应用程序都使用操作系统的应用程序编程接口。 运行库使用操作系统的系统调用接口。 接口需精心设计,尽量保持稳定,基于接口,具体实现层可以被任意替换。 中间层作为下面层级的包装和扩展,中间层的存在,保证了软硬件的相对独立。 操作系统提供抽象接口,管理软件、硬件资源。操作系统内核作为硬件接口的使用者,需定制硬件规格,硬件逐渐被抽象成一套接口,交给厂商,厂商写各自的驱动程序,硬件交互细节交给操作系统(驱动),程序员无需和硬件打交道。分层 分层设计的思想,其实渗透在计算机的各个领域。其中有我们最熟悉的 OSI 七层网络模型,它从低到高分别是:物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。参考计算机的软件体系结构,OSI 网络模型同样是通过制定层层的通信协议,界定各个分层的具体责任和义务。 OSI七层网络模型 TCP/IP四层概念模型 对应网络协议 应用层 应用层 HTTP、TFTP、FTP、NFS、WAIS、SMTP 表示层 Telnet、Riogin、SNMP、Gopher 会话层 SMTP、DNS 传输层 传输层 TCP、UDP 网络层 网络层 IP、ICMP、ARP、RARP、AKP、UUCP 数据链路层 数据链路层 FDDI、Ethernet、Arpanet、PDN、SLIP、PPP 物理层 IEEE 802.1A、IEEE 802.2 到 IEEE 802.11 那么为什么在架构设计的时候,采用分层设计的实现方案呢?之所以要设计分层,主要有以下几点考虑:降低复杂度,上层不需要关注下层细节。 提高灵活性,可以灵活替换某层的实现。 减小耦合度,将层次间的依赖减到最低。 有利于重用,同一层次可以有多种用途。 有利于标准化。中间层 除了分层设计,中间层的设计,也是非常巧妙的存在。 在计算机软件体系结构中,中间层作为下面层级的包装和扩展,中间层的存在,保证了软硬件的相对独立。 中间层的强大之处在 LLVM 设计的过程中也深有体现。 首先解释下 LLVM: LLVM 是构架编译器(compiler)的框架系统,以 C++ 编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。 LLVM 的大体结构设计如下图:它的设计主要可以分为编译器前端(Frontend)、优化器(Optimizer)、后端和代码生成器(Backend And CodeGenerator)。 笔者理解优化器(Optimizer)不仅仅作为编译过程中的一道工序(做各种优化并且改善代码的运行时间,减少冗余计算),优化器还作为 LLVM 设计最为精妙的地方--中间层。 为什么这么说呢? 前端语法种类繁多,后端硬件架构种类繁多,而正是中间层的存在,使得 LLVM 的架构即可以为各种语言独立编写前端,也可以为任意硬件架构编写后端,实现了开发语言和硬件架构之间相对独立,这才是其真正的强大之处。 类似软件的设计原则的体现 虽说是大的软件体系的结构设计,但是也能让笔者感触到一些软件设计原则的体现,毕竟万物皆对象,而面向对象设计原则如下:设计原则名称 简单定义开闭原则 对扩展开放,对修改关闭单一职责原则 一个类只负责一个功能领域中的相应职责里氏替换原则 所有引用基类的地方必须能透明地使用其子类的对象依赖倒置原则 依赖于抽象,不能依赖于具体实现接口隔离原则 类之间的依赖关系应该建立在最小的接口上合成/聚合复用原则 尽量使用合成/聚合,而不是通过继承达到复用的目的迪米特法则 一个软件实体应当尽可能少的与其他实体发生相互作用感受到了哪些设计原则?这列举一二,当然应该还会有更多。单一职责 分层设计,每层只负责特定的职责,拥有清晰的职责范围。 单一职责 层与层之间交互应该依赖抽象,任何满足每层协议的实体,都可以进行层的替换。 开闭原则 采用类似工厂设计原则,增加一种硬件类型,仅需要增加一种符合硬件规格厂商即可。总结:我们进行日常软件架构设计的时候,其实也可以参考计算机软件设计的一些思想,做一些合适的分层,制定层与层之间的协议,制定合适的中间层。 内存 早期内存采用扇形内存分区,磁盘中所有的扇区从0开始编号,直到最后一个扇区,编号为逻辑扇区号,设备会将逻辑扇区号,转换成真实的盘面、磁道位置。 早期程序直接运行在物理内存,存在的问题地址空间不隔离,一个程序内容容易被另一个程序修改。内存使用效率低,使用中的内存需要等到释放了,才能继续被使用。程序运行的地址不固定,程序重新装载时,内存地址变化了。虚拟地址&物理地址 为了解决地址空间不隔离的问题,引入了虚拟地址的概念,于是地址就分为了两种,虚拟地址空间和物理地址空间。 MMU是内存管理单元,有时也称作分页内存管理单元,MMU在操作系统的控制下负责将虚拟内存实际翻译成物理内存,其与CPU以及物理内存的关系如下图:物理地址空间是由地址总线条数决定的。 虚拟地址是想象出来的,每个进程都拥有独立的虚拟空间,这样做到了进程的地址隔离。 将一段程序所需要的虚拟空间,映射到某个实际的物理地址空间,映射函数由软件完成,实际转换由硬件完成。分段和分页 仅仅增加虚拟地址只能解决地址空间不隔离的问题,剩下两个问题还没解决,于是又引入了分段和分页。分段的基本思路是把一段与程序所需要的内存空间大小的虚拟空间映射到某个地址空间,映射关系如下图所示,通过使用分段,可以解决不隔离和不固定的问题,因为程序A和程序B被映射到了两块不同的物理空间。但是分段内存使用效率低下,内存映射以程序为单位,如果内存不足,被换出的是整个程序,其实程序内的很多数据,都不会被频繁用到,没必要被一起移除内存。分页就是将地址空间分为固定大小的页,进程的虚拟地址空间按页分隔,不常用的放入磁盘,用到时取出来即可,内存使用效率低下的问题得到了解决。内存共享的实现机制 虚拟空间页称之为虚拟页,物理内存的页为物理页,磁盘中的页为磁盘页,不同虚拟页被同时映射到同一个物理页,即可实现内存共享。 Page Fault 虚拟页不在内存中,当需要用到时,就会捕获 Page Fault。 对于 iOS 开发来说,虚拟内存也是通过分页管理的,当访问到某些数据并没有加载到内存时,操作系统就会阻塞当前线程,新加载一页到物理内存,并且将虚拟内存与之对应,这个阻塞的过程就叫做缺页中断,App 启动的时候 Page Fault次数多了会影响启动速度,而我们优化启动速的方式之一就是通过二进制重排,减少 Page Fault 的次数。当 App 的启动过程中如果需要启动符号1、启动符号2、启动符号3、启动符号4,那么 page1,page2,page3,page4 就都需要加载到内存中。而我们可以做的就是通过二进制的重排,将启动符号1、启动符号2、启动符号3、启动符号4放到了同一页,那么只需要 page1加载到内存即可。 大概的优化步骤:通过 Clang 插桩的方式找到 App 启动时,都加载了哪些符号,尽可能的将启动时用到的符号,通过自定义 Order File 放到同一个 启动时加载的 Page 当中,从而减少 Page Fault 的发生次数。线程 线程与进程的区别在于,进程是操作系统分配资源的最小单位,线程是程序执行的最小单位。 线程一些概念线程称之为轻量级进程,有线程 ID,当前指令指针 PC,寄存器,堆栈组成,线程是系统进行调度的最小单位。 各个线程共享程序的内存空间(代码段、数据段、堆),和一些进程级的资源(程序员角度:全局变量、堆、函数里的静态变量、代码)。 线程拥有私有的空间,栈、线程局部存储、寄存器(程序员角度:局部变量、函数参数)。 多处理器的线程并发才是真正并发,单个处理器的线程并发只不过是时间片轮流,调度。 线程至少三种状态:运行(时间片当中)、就绪(离开运行状态)、等待(时间片结束)。 线程调度分为优先级调度(容易出现饿死)和轮转发调度。 Linux 线程相关的操作通过 pthread 库实现。线程安全 多线程程序处于一个多变的环境当中,可访问的全局变量和堆数据随时都可能被其他的线程改变,于是产生了原子操作和锁。原子操作++操作不是原子操作(编译为汇编代码之后,不止一条指令)因为需要经历 ① 读取值到寄存器,② 值+1 ③ 将值写会寄存器。 复杂场景,原子操作就不满足了,需要使用锁,实现同步。锁实现数据访问的原子化,即一个线程未访问结束,另一个线程不能访问,访问数据时获取锁,访问结束释放锁,锁已经占用的时候,获取锁线程就会进行等待,直到锁资源可以重用。二元信号量,有两个状态,占用和非占用,适合只能被为一个线程独占访问的资源。 多元信号量(Semaphore),设置一个初始值 N,可实现 N 个线程并发访问。 互斥量,类似二元信号量,二元信号量可以被其他线程获取到释放,但是互斥量要求哪个线程获取,哪个线程释放。 临界区,区别于互斥量和信号量,互斥量和信号量在其他进程是可见的,但是临界区的范围仅仅限于本进程。 读写锁,对于读取频繁但是偶尔写入的时候,使用信号量和互斥锁效率比较低,读写锁有共享状态和独占状态。下图为读写锁的几种状态:读写锁状态 已共享方式读取 以独占方式读取自由 成功 成功共享 成功 等待独占 等待 等待表中读写锁具体状态解析如下:锁自由状态时,任何方式获取锁都可以获取成功。 共享状态下,共享方式获取可成功,独占方式获取不可成功。 独占状态下,共享方式获取、独占方式获取都不可成功 。 写操作作为独占状态,读操作作为共享状态,读读并发,不用等待,读写互斥,写写互斥。多线程可放心使用的函数之-可重入函数满足如下条件的函数即为可重入函数不使用任何(局部)静态或全局的非 const 变量。 不返回任何 (局部)静态或全局的非 const 变量的指针。 仅依赖于调用方提供的参数。 不依赖任何单个资源的锁(mutex 等)。 不调用任何不可重入的函数。 可重入是并发安全的强力保障,一个可重入的函数可以在多线程环境下放心使用。加了锁就安全了吗? 作者给我们举了一种情况: x = 0; // Thread1中 lock(); x++; unlock();// Thread2中 lock(); x++; unlock();上文已经介绍了 ++ 操作并非原子操作,编译器为了提高 x 的 访问速度,需要把 x 的值放入某个寄存器里面。 ++ 需要经历 ① 读取值到寄存器,② 值+1 ③ 将值写会寄存器三步。 那么就可能出现这种情况:Thread1读取 x 的值到寄存器 R1(此时 R1 = 0)。 R1 ++,此时 Thread2紧接着还要进行访问,但是 Thread1还没有将 R1值写回 x。 Thread2读取 x 的值到寄存器 R2(此时 R2 = 0)。 Thread2执行 R2 ++。 这时 Thread1将 R1 写回x(问题就来了,此时 R1 = 1,那么就出错了)。还有就是,CPU 发展出了动态调度的功能,在执行程序的时候,为了提高效率,有可能会交换指令的顺序,同样编译器在进行优化的时候,也是可能出现为了提高效率而交换毫不相干的两条相邻指令的顺序。 我们可以使用 volatile 关键字来阻止编译器为了提高速度将一个变量缓存到寄存器而不写回,也可阻止编译器调整操作 volatile 变量的指令顺序,但是 volatile 仅仅能够阻止编译器调整顺便,CPU 的动态交换顺序却没有办法阻止。 这里有个典型的例子,是关于单例模式的: volatile T* pInst = 0; T* GetInstance { if (pInst == NULL) { lock(); if (pInst == NULL) pInst = new T; unlock(); } return pInst; }这是单例模式 double-check 的案例,其中双重 if 可以令 lock 的开销降到最低,但是上面的代码其实是存在问题的,而问题就是来自于 CPU 的乱序执行。 pInst = new T 一共会分为三步: 1、分配内存 2、调用构造函数 3、将内存地址的值赋值给 pInst。 在这三步中 2、3 可能会被 CPU 颠倒顺序,那么就会出现这种情况: pInst 已经不是 NULL 了,但是还没有构造完毕,这时候另一个线程调用单例方法,发现 pInst 不为 NULL,就会将尚未构造完成的对象地址返回,这时候类就有可能产生异常。 为解决这个问题,我们可以使用 barrier 指令,来阻止 CPU 将该指令之前的指令交换到 barrier 之后, POWERPC 体系结构使用 barrier 优化后的单例方法如下: #define barrier() __asm__ volatile ("lwsync") volatile T* pInst = 0; T* GetInstance { if (!pInst) { lock(); if (!pInst) { T* temp = new T; barrier(); pInst = temp; } unlock(); } return pInst; }线程使用模型一对一模型 一个用户使用的线程就唯一对应一个内核使用的线程,这样用户线程就有了和内核线程一致的优点。这种情况下,才是真正的并发,如下图:优点:一个线程受阻时,其他的线程不会受到影响。 缺点1:内核线程数量的限制,导致用户线程受到限制。 缺点2:内核线程调度时,上下文开销较大,导致用户的执行效率降低。多对一模型 多个用户线程映射一个内核线程,如下图:优点:高效的上下文切换和几乎无限制的线程数量。 缺点1:一个线程阻塞,其他线程都将无法执行。 缺点2:多处理器不会对,对多对一模型性能没有明显帮助。多对多模型 将多个用户线程映射到不止一个的内核线程,如下图:优点1:一个线程的阻塞不会导致所有线程阻塞。 优点2:多对多模型,线程的数量没有什么限制。 优点3:多处理器系统上,多对多模型性能有提升。 缺点:实现较为困难。
iOS 求职寒冬?听听他们怎么说
这是一次线上分享的文字整理版。原版视频内容可以点这里查看:线上视频。 为啥会有这场分享最近在帮团队招人,像朋友圈、公众号也加了很多推广渠道,但能捞到的简历有限,即使是有简历,能过公司筛选且进入面试流程的都非常少。另一方面,现在找工作也挺难的,因为今年被裁员的人有很多,很多公司都已经不再招人了,导致竞争压力大,面试难度也跟着上升。像是裁员应该会导致更容易捞简历,更容易找到合适的人,事实却相反,这就很奇怪了,那这里面的 gap 在哪里呢? 最近正好有两个朋友都如愿找到了满意的工作,所以就想拉他们来一起分享下找工作的经历。 分享者介绍@阿卡拉 我是阿卡拉,毕业于郑州大学本科软件工程专业,2019 年 6 月进入腾讯。在腾讯主要负责的工作一直都是客户端基础平台建设,在工程效能方面不停的探索。 @JY 我是 JY,17 年毕业,是 iOS 摸鱼周报的联合编辑。我之前是在微盟的 App 基础技术部门工作、主要负责 APM 以及线上 Bug 排查等。因为公司裁员不得不重新找工作,目前是拿到了小红书的 Offer。 如何准备面试@阿卡拉 腾讯在 2022 年上半年就已经开始在慢慢的砍各种业务线,当时,对于我来说,感知比较弱,主要的原因是我一直在工程效能这一块,对于业务线的情况了解不是太多。仅仅是了解到外部的一些声音。 在5月30号,我们工程效能部门很多小组也开始在裁员,但是我们组的工具平台在最近的两年发展是挺不错的,这一次裁员没有涉及到组内任何同学。 或许是因为部门裁员的力度还是不够,接下来,开始在6月30号开始裁员,我们组涉及到的有16个人左右,最后仅剩下4个人做日常维护。 在腾讯的这三年,我发现自己的能力提升是比较快的,从进入公司的小职员,慢慢到一个大的项目的负责人,这也得益于我的 leader 和身边同事的配合。 所以,总的来说,并不是我要考虑换工作,而是公司的环境让自己被迫开始换一个工作环境。 本来在公司是 2 个月的缓冲期,所以缓冲期阶段除了处理手里的交接工作,同时也让自己休息一段时间,对这些接踵而来的消息做一个消化。然后就开始了我自己资料整理计划:整理 iOS 基础知识(八股文)【计划是 2 个周,实际花费 3 周】 计算机基础知识:主要是网络,操作系统等基础知识 iOS 的 dispatch:一直想把该模块的源码看完,但都是比较零散的,所以去看源码做了总结 iOS 的 dyld/objc:这个是 iOS 动态库加载的原理,所以需要去深入了解,objc 中包括很多的技术支持,如 autoreleasepool, 消息转发,AssociatedObject 机制。 Runloop 机制,KVO 与 KVC 机制,Block 管理机制,iOS 事件处理机制等等整理曾经看过的开源库:自己之所以喜欢去研究这些源码,是因为他们给我代码能力提升了很多,无论是从设计上和实现方案上都能有比较好的选择。【计划1周,实际花费2周】 AFNetworking:iOS 网络访问最出名的网络库,没有之一 YYCache,YYModule等由 ibireme 大神的开源组件合集 CocoaLumberjack:也是 iOS 出名的日志库,整个仓库的设计将设计模式很好的应用 OCMock:这个是一个单元测试组件,但是如果想验证自己的基础如何,这个仓库我觉得是最佳开源库,可以将 objc 的很多知识点应用进去。项目相关【计划 1 周,实际花费 2 周】 单元测试自动化:整理整个单元测试的执行流程和之前的实现方案。 质量组件化:熟悉自己开发的所有的 SDK ,包括语音 SDK,大文件上传 SDK,屏幕录制SDK 等等。 变异测试:我主要负责的一个平台。变异测试的整理设计方案与之后的优化方向;梳理 Objective C 热重载执行方案与设计;LLVM 对 Objective C 语言的语法树分析并做各种能力;LLVM 的 pass 化服务梳理。算法【每天早上刷算法】 leetcode上刷剑指 offer 第一版和第二版,然后刷 leetcode 热题 100(热题100要看着题目就马上写出来的那种) 回忆算法:主要是算法小抄和这个作者的网站:https://labuladong.github.io/algo/总结下来,因为本身自己也想去做一个总结,所以整体上花了大概 1 个半月的时间,所以基本都是在腾讯的缓冲期每天静下心来一步一步的梳理。其中准备算法耗时最多吧,前后刷了差不多 300 题。 @JY 我换工作的契机是因为公司裁员。在国庆节前两天,被通知 Last day,不需要交接。所以我是从国庆节前那几天才开始准备的。我主要准备的是简历,这里我的想法是需要明确自己想要去什么样的公司,然后根据公司要求和自己擅长的点,可以准备多份简历。简历里最主要的内容就是自己的项目经历,项目最好能体现出具体的优化指标。我面了很多家公司,都会被问到最后优化后的指标。 其次就是八股文,因为我之前有记一些笔记,再结合网上别人总结的一些内容,每天早晚都会大概看一遍。 算法这块,我在 Leetcode上开了一个会员,这样能看到热门公司题库,我把想要面的公司的算法都刷了一遍,有些题目可能不止一遍。 这里最耗时的就是算法这块了,有些 Hard 级别的算法题会很难写出来,但很多时候即使写不出来,有大概思路也行。所以如果算法比较弱的话,需要多刷一刷找找题感,才能更好的应对各种复杂的问题。 如何写简历 简历是面试过程中的敲门砖,只有简历被捞到,才会有后面的面试过程,所以它的优先级是很高的。很多人都有分析过简历应该怎么写,不应该怎么写,但我感觉讲再多都不如亲自去看一个优秀的简历是如何写的。下面是阿卡拉和JY两个人的简历,部分内容做了脱敏和抹除。 @阿卡拉@JY如何获取更多应聘机会@阿卡拉 招聘渠道的话,小公司直接去 Boss 上面找,大公司找内推。我对下个阶段其实考虑挺多的,结合自己的诉求,我主要有三个方面的要求: 1、大公司:字节,阿里,腾讯这一类的大公司,只要招聘的业务不是太偏就可以。业务上我希望继续在工程效能方向继续深挖。 2、有前景的公司,看中公司的进步和发展,看中公司的业务 3、加班压力小的公司 @JY 我基本也都是通过 Boss 或者内推方式,我会让朋友来面试我,培养自己的面感,这个过程也会一点点增加自信。因为现在大环境不好,很多大公司的 HC 都是很宝贵的,最好是准备很充分了再去面。 对于意向工作,我更关注的是该组是负责哪一块,是否是核心部门,公司发展前景如何。最重要的还有自身的发展方向和工作内容是否匹配。 @zhangferry 找工作前需要考虑的这几个问题,第三个是非常重要的。不管因为什么原因需要换工作,我们都应该把换工作这个节点当做整个职业规划的下一个起点。之前的工作有哪些不好,踩了哪些坑,我要避免,我了解到哪些知识,对哪个领域更感兴趣,可以往那个方向多去靠。 @东野浪子 挑选工作时要提前想好自己的诉求是什么,更高的薪资?更看好的行业?想从业务开发切换为技术开发?甚至就是想工作的轻松点,这都是没问题的。把这些诉求按照优先级进行排序,赋予不同的权重值,然后再分门别类的把你所能投的一些公司或者职位按照权重进行打分,分数最高的那个就是最适合你的岗位。 很多时候都不可能一个 Offer 覆盖你所有诉求,这时就可以利用这个方法论进行选择了。 如何进行面试@阿卡拉 我投的简历还是蛮多的,大概有十几家,最终进入面试环节的有七到八家,但是真正满足我那几个诉求的只有两到三家。我觉得寒冬的感受还是挺明显的,阿里、腾讯、大江、虾皮这些都已经不怎么招人了,因此可选的公司范围没有那么广。 技术面中基础知识,也就是我们常说的八股文还是比较重要的,也经常被问到。像是 Runloop、KVC/KVO、Objc、Block、GCD、Autorelease 这些技术点要非常清楚,每个点都需要深入。我自己在整理文档的时候基本也是从这几个方向展开的。 总共走完流程的有 4 家,有两家因为薪资不满意,还有一家其实挺想去的,但感觉他的发展前景不太好,最终就选择了字节。不说别的,就薪资这一块,字节还是棒棒的。 最终通过面试感觉还是因为自己准备比较充足,比较全面,哪些相关的知识点,我会把里面的方法论也都提炼出来。比如说 Autorelease 或者 Cache 相关内容,我会考虑如何把他们应用到自己的项目中去。只有真真实实地去考虑了这个东西,而且了解它的各方面内容,面试官问你的时候你就会感觉很轻松。 关于这些知识点的学习,我还会分有很大比重去看开源库。说实话我觉得腾讯这边,很多业务代码拿出来,大家都不想去看的,这些东西不会给人带来提升。而开源库是经过社区检验的,都是质量非常高的内容,仔细研究,更能学到东西。 字节给我面试体验就是他们的面试深度,整个面试过程就是带着你一步步往深了聊,但这随之而来的就是难度提升,这也是我为啥会把面字节的面试安排放到最后。 @JY 我投的简历不多,都是自己比较想去的才会投,除了两三家没有面试,其他都有。寒冬的感觉还是很明显的,因为我很多朋友去年找工作的时候,HC 很多,难度也不算高。今年因为有很多公司在裁员,有一些公司也锁了 HC,市面上人确实比较多,所以每次面试都应该把握好机会。 我在技术面上感觉稍微有些不同,一面遇到问八股的比较多,后面几面基本都是围绕项目在问。最经常被问到的就是你们团队为什么要做这个?为什么要采用这种方案?过程中遇到了哪些问题?你是如何去解决的?其次就是结果,带来了哪些收益,我感觉大厂的面试很像项目讨论会,双方就一个功能点不断地探讨,技术深度慢慢深化,这样整体面下来也会感觉比较舒服。 有一些没有走完的面试流程,可能是复习的时候没有准备到位,也有一些是平常工作中很少使用到的,因为答的不太好就被 pass 了。 面试通过的原因,我觉得主要还是平常的积累,平常做了什么项目,需要经常复盘总结。把遇到的问题以及收益都记录下来,组织自己的语言表达出来。 小红书整体的面试还是比价舒适的。整个面试都是从浅到深的,基本没有问什么八股,都是围绕项目开展的。 面试回顾及总结@阿卡拉 面试中踩过的坑,第一不要盲目面试,盲目面试会发现其实很多岗位都是不符合自己预期的,这些过程会有很多无效沟通,浪费了挺多时间。第二面试一定要做好总结,像是每次面试被问到的问题,如果我不知道或者感觉答的不好,下面我会再去总结和调整,短板补充是一个比较重要的提升过程。 面试对我还有一点改变是,因为在腾讯这三年都没有考虑过面试这件事,一直是比较佛系的状态,这次面试经历基本是反向的去提升自己的能力。因为面试的缘故我有充足的时间去系统的学习之前遗漏的知识点,并为下个阶段做一个规划。 如果面试不顺的话,就当做跟这家公司没有缘分吧。选择是双向的,我经常跟一些同事开玩笑说,在腾讯小马哥拥有我是他的幸福,而不是我的幸福。现在在字节也一样,他拥有我是他的幸福,但反过来对我来说,也促成了我的幸福。 关于选择这块,因为在腾讯时组内相处比较和谐,我也更倾向于氛围和谐的团队,工作和生活都要做最真实的自己。另外我觉得大家还是要选一个自己感兴趣的东西,一直坚持做下去,丰富自己的生活。 @JY 踩过的坑是,简历一定要写自己熟悉的(至少能够展开来讲的),不要写一些不是很熟悉,或者临时抱佛脚的东西,一旦被面试官问到,会很影响在面试官心中的印象。面试的时候,如果可以的话,就把面试官引到自己的舒适区,因为每个人技术侧重不同,这样的话更能体现你的优势,这样面试成功的几率也会更大。 面试过程做的特别对的事情是,我每次面试都会录音,面试完会听着录音进行复盘。有些人可能觉得自己面完感觉很不错,但有时候自己再听一遍时会发现自己有很多可以改进的地方。 做的不错的地方是调整自己心态吧,刚开始投简历的时候一个面试都没有,自己心态受到了一些影响,因为是被裁的,心情有些失落。然后我趁国庆节,前三天,出去旅游了一下,回来就放松下来了,再去调整心情,安心准备复习,这样效果会好很多。 换工作的话,主要是看公司能否满足自身发展,因为我们换一份工作一般都是期望工作一年以上的(大家肯定不希望自己简历花掉),如果这段经历跟自己不匹配,那整体工作状态可能会不开心。另外像阿卡拉说的,团队氛围,可能进入团队之前会比较难判断出团队氛围好不好,一般二面或者三面就是你的领导,这时你可以看你自己的感觉和面试官契不契合,也可以先以是否符合自身发展为主。我接触的程序员都是蛮好相处的。 如何筛选候选人-面试官角度 @东野浪子我简单自我介绍下吧,我是 15 年毕业,16 年开始做面试官,我刚看了一下面试记录,这些年来总共面试了大概有 570 多位候选人。我现在是在字节的客户端架构部门下的 APM 部门,带客户端团队。 好简历体现在哪里 先说第一点,作为面试官视角,第一眼关注的是你的硬性指标:学校、工作经历、技能匹配程度。重点大学和大厂经验会是加分项,另外还会去看你的技能匹配程度,如果你之前是做基础技术的,你应聘的也是基础技术,这就属于比较匹配。但是如果你的这些条件都不算出彩的话,就需要用项目经历去弥补了,这是第二点我比较关注的内容。 对于项目经历的整理和描述要条理清晰,可以使用 STAR 法则,我做这个东西的背景是什么,基于这个背景,你的目标是什么,你通过什么行动达成了什么样的目标,最终一定要有结果,而且最好是可以用数据量化的结果。项目中一定要有重点,一些重要的结果和数据,可以使用加粗的形式去强化。 简历也可以提现个人风格,我看过一些简历,一看就知道这个人非常的极客。因为它里面有非常多的专业术语,能明显感觉出他对技术的理解非常深刻。如果你面试的是基础技术部门,那就应该在简历里多写一些你在底层技术的探索,或者你在技术优化上做过的一些事情。这些都是简历相关的内容。 好的应聘者需要具备哪些特征 对面试阶段候选人的表现,最重要的就是基础一定要扎实,基础决定了你以后的发展潜力,甚至决定你在职场中的天花板。这里基础又包括很多方面,比如计算机基础、操作系统原理、网络等等,这些东西一点要多查漏补缺,不要有明显硬伤。这一点如果表现不好,那肯定是过不了面试的。 再就是数据结构和算法,有一点需要澄清下就是不要畏惧算法,至少在我们团队算法考察最多就是 medium 级别,不会在算法层面太难为大家。很多同学面试挂掉,如果把结果归结于算法没写好,是需要纠正的。因为算法是作为一个侧面角度考察的,他不是直接决定面试结果,如果你前面回答很出色,就算是算法答的不太好,还是会过的。 第二点是领域知识,做iOS开发对苹果开发相关的比如UI动画、多线程、内存管理、性能稳定性等都要有一定的了解。如果是做底层技术,你还需要掌握 dyld、runtime 相关的底层原理和常见的优化手段。很多人会把这说成八股文,我感觉更合适的是把它作为对你技能点的考察。这类问题通常的考察方式是从一个非常简单的问题切入,然后抽丝剥茧,逐渐加大难度,看你能够顺利的通过多少关。 第三点属于项目亮点,比如一个很常见的面试问题:你做过最满意的一个项目是什么,项目里有哪些亮点内容?亮点内容应该体现出一定的复杂度,你跟其他人的方案有什么差别,使用了哪些设计能力,涉及哪些技术选型,如何协作和执行的。同时也不能只谈难度不谈收益,它达到了哪些业务价值,你在这里承担了什么角色,是否是跨部门项目,这些方面都可以作为亮点去讲。业务价值描述时可以通过方案前后的对比来体现,之前指标是多少,使用该方案之后达到了多少,这种展现是非常直观的,更能引起面试官的共鸣,这个技巧同样也可以用到汇报工作和晋升答辩中。 软素质也会作为面试考察中比较重要的一点,因为应聘者最终会成为我的同事或者下属,我会关注自驱力,你对技术有好奇心,会主动地探索。然后是规划能力、沟通协作能力、迁移复用能力等。这些点比较多,如果无法通过一次沟通完全展示出来,那可以根据自己的需要选择一两个方面有体现也行。比如你想转技术栈或跨专业,那你就要说服面试官为什么之前积累的经验可以继续复用。 一些面试小技巧对于没有把握的问题,不要不懂装懂,面试官会很讨厌不懂装懂的情况,这会严重扣分。还有就是对于你没有把握的问题,最好不要一言不发或者直接说我不了解。如果遇到了这类问题可以尝试从自己熟悉的角度切入,或者给出一个简单版本的答案。 第二个技巧是不要在面试过程中与面试官发生冲突,有可能他老问八股文,你感觉面试官在刁难你,或者你觉得面试官很菜,都不要起冲突。因为一般面试都会有面评记录,如果有冲突,这个问题被记录,后面会影响你再面其他部门,得不偿失。 还有一个技巧就是多复盘总结,很多人会有一个面试误区,感觉面试没发挥好,就不去想他了,而产生一些抵触情绪,这其实对自己提升是无益的。一定要有复盘,也可以跟小伙伴一起讨论自己的面试情况,避免自己有认知偏差。因为有时候我们感觉自己回答的很好,但最终却没面上,有一种可能就是你回答没有达到点子上,或者回答的比较浅显,跟面试官期待有偏差。 @zhangferry 对于软素质,我感觉它体现了一个隐藏因素--兴趣,有了兴趣做为驱动力,才会在没人约束的情况下自发去探索,去做到更好。其实很多人对编程,对开发这份工作是不感兴趣的,只是当做挣钱的一种手段,这没什么不好,反过来完全出于兴趣,敲代码时快乐的不得了的人也很少。但不管怎样的状态,工作中还是应该尝试去找些让自己兴奋起来的点,比如我解决了什么技术问题,老板给我奖励;我写了一篇文章,大家都给我点赞;我在团队做了什么分享,大家都来向我请教相关问题等等。这些跟技术联通的点和个人成就感结合起来,能更好的激发创造性。上班很辛苦,但不应该只有辛苦,毕竟这份工作我们可能还要在做几年或者十几年时间,一直都感觉苦肯定是很难精进的。这一点大家也可以结合自身情况多想一想啊。 面试结果由什么决定@zhangferry:本次分享的目的是给大家提供一些值得借鉴的面试方法,其实刚才也聊了很多面试之外的事,包括如何挑选岗位,如果在工作中补足自己学历或者经历的短板。回到一个比较重要的问题上,一次面试,我有多大概率能够成功。我认为,日常工作经验和总结占 60%,面试准备占 30%,面试相关的技巧和运气占剩余的 10%。对,有时候运气也会决定面试的结果。 如果是处于工作状态,近期并没有打算换工作的同学,可以在工作期间多考虑下如果想要优化自己的简历我应该做好哪方面的事情。如果是正在准备面试的同学,工作内容已经没法改变了,但还可以把时间多放到面试准备上来,充分的准备也能提供我们通过的概率。 还有一点想要强调的是,有不少人会在准备不充分的情况下去面试,这个其实是挺不好的。因为现在面试机会真的不多,每次面试都很宝贵,另外以字节为例,不超过半年的面试,面评记录会一直留着。如果你之前面试效果不好,那后面你准备充分了,换个部门去面,就很大概率会因为面评不好直接被刷下来。我遇到过好几次这种情况,有些人会说是不知怎么简历就被 HR 捞到了,然后电话联系要约面试,自己稀里糊涂就参加了。如果遇到这种情况,一定要以自己实际准备的情况来决定是否要约面试。 QA 遇到面试就紧张,脑袋空白,面试过程不会吹怎么办? @东野浪子:紧张背后的原因大多数情况就是准备不充分,导致不自信。我们应该从这个方面去解决紧张的问题。充分的准备就是上面说的一些基础知识,领域知识,要非常熟悉;涉及项目经历这块则可能需要更长期的准备。比如日常工作中主动承担一些有挑战性的业务,对一些技术问题进行深入探索。这些点都是面试过程中可以让自己出彩的地方,所以工作的一部分也是在为自己的简历打工。 不会吹可能侧面想说的是不会表达的意思,会有些人面试很强,但是实战一般。比如一个人实力是 60 分,他能说成 80,而有的人实力是 80 分,因为不会表达,让人听起来只有60 分。这样的话,就需要培养一下自己的演讲或者表达能力了。这种能力在日常工作工作、汇报或者晋升答辩时都非常重要,这个能力本身也属于面试考察的一部分。所以平时可以刻意得去培养锻炼一下。 写了两年 UI 怎么办,就做了基本的页面开发,感觉项目经历很空洞 @东野浪子:如果一直重复一种开发模式,那确实不太好。你可能很难改变开发的需求,但是使用什么样的技术却是没人会限制你的。即使是开发UI,从 Frame、Autolayout、Masonry 再到 SwiftUI,包括 RN 和 Flutter,这一步步流程都会涉及很多 UI 内容,不同框架的出现是解决什么问题的,怎么才能更适合当前工作且提高效率,这些都是自己可以延伸思考的事情。另一方面,你做UI会不会涉及到流畅性问题、会不会涉及一些Crash,那这些问题都是可以再延伸很多内容的。所以即使复杂度不高的项目,也不会说不涉及复杂的问题,就看你有没有主动给自己增加难度,去做有调整性的那部分。没有大厂经历也类似,没人会限制你学习大厂的技术,而且现在有很多途径去获取他们的技术方案,自己可以多学习,多思考,然后实践到自己的项目中。我们是非常欢迎这种有自驱力和主动性的同学的。 如何寻找自己的第二增长曲线 @东野浪子:第二增长曲线,往大了说比如你现在是一个程序员,这是你的主页,第二曲线就可能是一个副业。比如炒股、做新媒体、公众号、拍抖音等等。往小了说,比如你现在是 iOS 开发,你只会 OC,那 Swift 以后可能会是一个趋势,你精通 Swift,那这就会成为你的第二曲线。你如你现在是做UI页面,做纯业务,那是不是可以去了解一些底层技术,像是 APM、DevOps、编译链接、端智能等,或者往跨端、全栈方面考虑,这些都是可以成为第二曲线的。第二曲线的东西一定要跟你的兴趣匹配,你要对他有热情,即使没有收益也能够坚持下去那种。 请问“横向协作能力,迁移复用能力”,这两个软素质一般会如何考察和作答? 是否是先回答自己的方法论,然后举例来说明? @东野浪子:横向协作能力,一般会在大型一些的跨团队合作项目中会有。比如我们是做客户端监控的,有些东西就会是一个双端方案。比如客户端上报流量,太高的时候后端会触发一些容灾或者限流,这个跟每个端的特性都没有关系。那如果作为这个项目的 Owner,你怎么把事情推进下去,怎么保证项目周期可控。他会涉及一些跨团队的沟通协作、目标对齐还有项目管理,这里体现的就是横向协作能力。 迁移复用能力属于比如你在一个领域 APM 做了很久,那现在需要专做音视频,相对于 APP 来说,怎么对音视频进行性能优化呢?这里有很多方法论其实是可以迁移出来再次适用的,像是线上容灾、问题响应、防裂化等处理方式都有一定的相似性。 内推信息 因为是为了信息互通,各位分享者团队有都有招聘需求,这些是内推信息:内推人(微信) 岗位描述 招聘需求zhangferry(zhangferry) [北/上/杭]抖音iOS基础技术-研发效能 有静态分析、LLVM、单元测试、自动测试框架、架构、工程效率、全栈开发等经验者优先,有业务背景但对技术有深度追求者优先。东野浪子(569087164) [北京]字节跳动 APM客户端-iOS 负责字节跳动所有移动端产品的性能优化和问题排查。北京还有 3 个 HC阿卡拉(myself439664) [深]抖音iOS基础技术-自动化测试 提升抖音等产品的研发代码质量,降低测试成本,参与自动化测试服务建设。JY(q491964334) [北/上]小红书-客户端基础架构 有过千万 DAU APP 的基础架构方面的开发经验,具有钻研 iOS 系统底层实现与优化的能力对于内推岗位,我也维护了一个公共链接,内推信息汇总,大家如果有招人需求可以到这里来填写。老司机技术周报很早也在维护一份内推信息,会更全一些,可以点击这个查看:iOS靠谱内推专题
《卓有成效的工程师》书评
- 07 Oct, 2022
-
关于本书 本书英文版《The Effective Engineer》 出版时间是 2015 年,中文版出版时间是 2022 年的 7 月 1 号,是一本刚翻译过来的书。用 Effective 命名的图书很多都是经典之作,管理类的像是《The Effective Executive》、《The 7 Habits of Highly Effective People》,编程类的像《Effective C++》、《Effective Objective-C 2.0》,本书也不例外。 作者 Edmond Lau 早期在 Google 担任搜索质量软件工程师,后作为 Quora 的初创成员之一,领导工程团队致力于用户增长,并为新入职软件工程师制定入职培训和指导计划。他热衷于帮助工程团队建立强大的文化,这本书是他关于如何成为卓有成效的工程师的职业感悟和个人总结。 导读本书主题是如何成为卓有成效的工程师,即作为工程师如何提供工程效率。对于工程效率给出了一个杠杆率的概念:杠杆率 = 产出的影响 / 投入的时间高效的工作方式应该是致力于做高杠杆率的事情。全书主要分为三个部分,第一部分是讲如何聚焦杠杆率高的事情,第二部分为对于高杠杆率的事情如何优化我们执行的方法,第三部分是把时间维度拉长,去投资哪些长期价值高的事情。 聚焦杠杆率 使用杠杆率衡量工作成效 杠杆率还可以用时间的投资回报率(ROI)来类比,追求高杠杆率时,通过公式我们可以得知高杠杆率的事情应该是:提高产出降低投入的时间所以增加工作时间并不符合高杠杆率的目标。当我们感觉可以通过增加时间来完成某件事情是正确的做法时是忽略了时间是有限资源,在这里投入了更多时间意味着我们会在其他事情上可利用的时间被占用了。 除此之外还有一个方式是停止手头的内容,转向杠杆率更高的事情。这三种方式可以引申出三个问题用来评估我们正在进行的工作:如何增加该工作产生的价值如何在更短的时间内完成这项工作是否有其他工作可以在当下创造更多价值本书的核心思想就是围绕这几个点展开的 定期调整优先级 在调整优先级之前,我们应该先有一个地方能够看到我们当前在做的事情有哪些,这需要一个任务清单去管理我们所做的事情。 有了任务清单最好还有一个工具帮助记录时间的使用,它可以衡量我们在事情安排和时间使用上有哪些不足。番茄工作法一个非常好的尝试,它可以把时间有序的划分成不同的块,这种有起始和结束时间的区间更利于我们意识到时间的存在。比价高效的是一天会有 10-14 个番茄时钟用于处理重要的事情。 当任务列表被填充内容过多时我们应该警惕是否忽略了优先级问题。调整优先级不是一件容易的工作,和大多数技能一样,它需要不断实践。对于什么事更有价值的事情,有两个方面可以考虑:关注直接创造价值的工作,以及关注重要但不紧急的工作。 同时为了保持工作的高效,有几个方法可以用于借鉴:创建守护者进程。在日历上每周每天或者每周划定一个时间段只专注于工作。 限制同时进行的任务数量。人能够同时处理事情的数量是有限的,多个任务之间的切换本身也是耗费时间的,所以要控制自己同时开展任务的数量。 拖延症是阻碍我们高效的一大问题,可以采用「如果...就...」的方案给自己定一个完成节点。可行的计划类似:如果在下一项工作前有20分钟,我就去审查代码、回复邮件等。优化执行方法 优化学习方式 在经济学里有复利的概念,一旦利息被加到存款本金中,就会在未来产生复利,复利又会带来更多的利息。从以上两条对比的复利曲线可以看出,复利是一条指数增长曲线,前期看着会比较慢,但后面的增长速度是非常快的。复利开始的越早,就会越早进入高速增长区。即使利率差比较小,经过漫长的时间之后收益也会产生巨大的差异。 学习和理财一样,前面的学习会为后面的学习打基础,越早对学习方式进行优化,产生的复利效果就会越明显。与之相反,如果我们把时间花在缺乏挑战的工作上时,不是保持原地踏步那么简单,同时我们还在浪费着时间。Palantir 公司的联合创始人斯蒂芬科恩在斯坦福大学的一次客座演讲时强调,「当公司为一份轻松的、毫无挑战的、朝九晚五的工作向你支付薪水时,他们实际上是在付钱让你接受更低的智力增长率。等你认知到智力投资会产生福利效应时,已经为错失长期复利付出巨大的代价。他们没有让你获得一生中最好的机会,而是得到另一件可怕的东西:安于现状。」 投资时间到学习上是最容易产生复利效果的一件事情,我们可以采取这些方式:培养成长型思维模式。该理论来源于Carol Dweck 在《终身成长》,「固定性思维模式的人坚信,人的能力是先天注定的,后天无法改变。成长型思维模式的人相信,通过后天努力可以培养和提高自己的智力和技能」。 不断对学习方式进行优化,以提升学习速率。 寻找利于学习的工作环境。关于第三点,如何寻求一个利于学习的工作环境,有这六个因素可以考虑:快速增长。快速发展的团队和公司能提供大量产生巨大影响的机会。 培训。细致规范的入职流程表明该组织将员工的培训放在首位。Facebook 有一个为期6周的新兵训练营(Bootcamp),新入职的工程师将通过该计划了解公司所用的工具及重点领域,并完成一些初步的实际开发工作。 开放。要追求一种充满好奇心的组织文化,鼓励每个人提出问题,在结合一种开放的文化,让人们积极反馈和分享信息。 节奏。快速迭代的工作环境能提供更短的反馈周期。一个想法从构思到获得批准需要多长时间?快速行动是否体现在公司或工程价值观中。都是可以考虑的方面。 人员。与那些比自己更聪明、更有才华、更有创造力的人一起工作,意味着我们身边有很多潜在的老师和导师、就职业成长和工作幸福感而言,和谁一起工作要比实际做什么工作更为重要。在选择时可以考虑这些问题:面试官看起来比你更聪明吗?你的面试是否演进全面?你想和面试官一起工作吗? 自治。选择工作内容和工作方式的自由驱动着我们的学习能力,可以考虑大家是否拥有选择项目、开展项目的自主权?工程师是否参与产品设计的讨论并能影响产品方向。这几点因公司和团队而异,不同职业时期各个因素的重要性也会发生变化,但不论怎样,在工作中我们还可以充分利用工作中的资源以提高学习效果:学习公司里最优秀的工程师编写的核心抽象代码,特别是使用过的核心库,里面会有很多值得学习的东西。 学习也要伴随实践,把学习内容落地到项目中,编写更多代码。 研读内部可以获取的任何技术和学习资料。例如谷歌有大量资深工程师编写的 CodeLab。 掌握你所使用的编程语言,读一两本该语言的优秀著作,再至少掌握一种脚本语言,你可以将其看做快速处理工作任务的瑞士军刀。 请公司里最严格的人审查你的代码 用于学习自己不熟悉的代码这里可以补充一些我在抖音的工作感受,这里完全符合「利于学习的工作环境」。我所在团队是研发效能,这里有多个方向上还处于探索期,有很多想法可以尝试。这提供了很大的自由空间,我可以选择自己感兴趣的方向去尝试,最大限度地自主设计这些功能,并能得到多方资源的支持。 培训方面字节的机制是很完善的,像 Facebook 内部也叫 Bootcamp,时间跨度为 6 个月(整个试用期)。这里的内容有很多主题,大的有工程师文化讲解,小的有各个技术专题的内部分享。技术分享比较系统,更像是课程,从初阶到高阶,涵盖范围很广,配有完整的知识图谱进行对照。抖音内部每周还会有一个技术分享,邀请其他部门或者内部团队的优秀同学,这些内容又可以沉淀下来扩展整个学习素材库。 人才方面字节最不缺的应该就是聪明、有才华的人了。严格的 CodeReview,有趣的技术方案讨论,和优秀的人一起工作,本身就是一件享受的事。 个人权限也很高,你基本可以看部门内部任意一个代码仓库的源码,你可以向任何人请教某一个技术实现的细节。 再附一个招聘的信息:我们是负责抖音整体研发阶段质量问题的团队,有很多研究方向但苦于人力不够,所以计划在年前再招几位同学,感兴趣的同学可以点击这里了解详情:抖音 iOS 基础技术 - 研发效能方向内推投资迭代速度 Twitter 平台工程前副总裁 Raffi Krikorian 会不断提醒团队:如果某个任务必须手动做两次以上,那么第三次就去编写一个工具。工具是一个倍增器,它使我们能超越工作时间的限制。 日常工作中,像是本文编辑器、IDE 和浏览器这些高频使用的工具,代码导航、代码搜索、格式化等这些高频的编程习惯,哪怕花费一个小时或者更久去记住一个快捷键或者设计一个自动化功能去提高这方面的效率,长远来看都是非常值得的。有一些行之有效的建议可以采纳:熟练使用自己喜欢的文本编辑器或 IDE 至少学习一种高级编程语言,一种脚本语言 熟悉 Shell 命令 多用键盘少用鼠标 自动化手动工作流程关于自动化工作流程,除了脚本本身还可以利用 Alfred 帮助管理任务: 在调试一个程序时我需要频繁的检查它的产物,因为这个功能是嵌到另一个项目中的,所以它的层级目录很深,每次误关闭 Finder 都需要点击多次才能找到目的位置。对于快速定位一个目录,可以利用 Alfred 的 Default Results 功能,为这个目录配置一个替身,当我要打开该目录时直接搜索替身名即可。 类似的还有通过路径打开 Finder 对应位置,直接运行一段脚本,通过快捷键用Xcode/VSCode/PyCharm打开指定文件或文件夹等等。都可以配置为 Alfred 的 Workflow。 当原本需要三个或更多步骤才能完成的事情缩短为一个步骤时,这边便捷感会让我们的工作流程更流畅,聚焦与真正要处理的事情上。通过度量改进目标 Peter Drucker 在《The Effective Executive》中指出「如果你不能衡量它,你就无法改变它」。 以谷歌搜索为例,输入一个查询,可能对应的结果就有数十亿条,算法会计算出将近 200 个指标,然后返回前十条结果。那么如何衡量给出的这十条结果对用户来说是最优的呢?谷歌给出的衡量是用户愉悦感,让用户最舒服的结果就是最好的,衡量愉悦感是用「长点击」测量的,就是当一个人点击了一个搜索结果,没有立即范围搜索页,就表明用户对搜索结果是满意的。好的度量指标能推动我们不断优化工作结果。 有时因为系统复杂性的原因,很难通过单一指标去衡量它的运行状况,这时可以引入监控系统,从多个方面衡量起健康和稳定。 与此同时,还需要确保你的数据是可靠的,「数据都正确」比没有数据更糟糕。特别是数据指标建设早期,我们应该多方便去衡量数据指标的正确性,比如广泛记录日志以备用,尽早检查采集到的数据,通过多种方式计算同一指标,交叉验证数据准确性,尽早分析有问题的数据,以找到指标变动的原因。工作成果需要借助于度量来改进,同时我们的工作效率也可以引入度量系统。有时数据层面反应的问题,会比事情本身更有价值。 我最近发现了一个宝藏 App:Session,它同时满足了我任务记录、反思、度量这几个需求。我通过查看自己过去半个月的记录情况发现,我最高效的一天是完成 7 个番茄时钟;遇到最多的问题是任务规划不合理导致很多任务超时和中途被打断;第二周的番茄数量相比第一周有所下降。这里完整反应了我对时间的使用情况,并且可以根据这些数据做出针对性调整。建立反馈循环 在做项目,尤其是大型项目时,花费一小部分精力用来搜集数据,验证目标可行性是非常值得的投入。它们可能会增加我们 10% 的开销,但也有可能提前暴露问题,节省 90% 的精力。这是一种通过搜集尽可能多的信息来预见结果的提前反馈机制。现代项目基本都会引入 DevOps 流程,这也是一种为了加快验证代码效果以建立反馈循环的机制。 除了项目整体,作为个人在项目开发过程中也需要尝试建立与外部的反馈循环。通常我们会遇到这种场景,在实际工作中我们必须独立完成一个项目,它减少了外部沟通成本的成本但却增加了反馈阻力。如果这期间遇到某个实现被卡主,会很让人郁闷。我们可以尝试这些策略用于获取反馈:尽早并经常提交代码并进行代码审查。 请求严厉的批评者审查代码,通过严厉的审查要求,我们更容易获得反馈。 征求团队成员的反馈,把自己的想法告诉一位成员,想他解释你正在做的事情,有可能我们在解释时就会意识到一些优化点。 编写代码前先展示设计文档,通过设计文档去考虑之后要做的事情有哪些。提升项目进度把控能力 「这个功能大概多久能做完?」这是每个程序员会经常面临的问题,针对估算,Steve McConnell 甚至写了一本书《软件估算的艺术》。一个好的估算能够对项目的实际情况提供足够清晰的视角,使项目负责人能够就如何控制项目以达到目标做出正确的决定,而一个差的估算,会让自己陷入紧张的情绪和无休止的加班,甚至会引起依赖业务方的连环 Delay。 准确的估算是很难做出的,但我们可以采用一些策略让估算尽可能地贴近真实值:将项目分解为细粒度的任务。 根据任务需要进行估算,而不是自己或者别人希望花多长时间进行估算。 将估算结果视为概率分布,而不是最佳情况。例如我们可以说「我们有50%的可能在4周后交付该功能,但有 90% 的可能会在8周内交付」。 谨防锚定偏差,这是人的一种潜意识,项目经理可能会给出一个大概的数字,或者在我们不了解完整需求的前提下先给一个大致估期。虽然我们可以再做时间上的修正,但因为有了这个锚定排期,我们很容易向这个值靠近而非真实的估期。 根据历史数据验证估算结果。比如之前你的估期总是低估2 0%,但此次估期你可以在原有基础上再增加20%的时间。 可以就估算结果进行审查,并允许他人质疑估算结果,有些经验更丰富的人可以帮助我们发现估算中的错误或者确认内容。在项目开始之前还应该为意外留出一些预算(Buffer),因为除了工作我们还会有 bug 修复、面试、团队会议、回复邮件等等。一天有 8 小时用于工作,并不表示我们可以用这 8 个小时完全去做这个项目。 除此之外设定一些具体的项目目标和可度量的里程碑也非常有用。明确我们到底要解决什么,有助于我们确定哪些工作在范围之内,哪些在范围之外。很多时候我们在开发一个功能时会发现一块代码需要重构,且这块重构会不知不觉导致我们投入很多时间。这时关键目标就可以帮助我们确认,这个重构是否值得。 当我们被问及某个项目的进度时我们通常会说完成了 50%、90%。这个估算是很主观很模糊的。一种比较好的方式是为目标建立一些里程碑,作为项目进展的检查点,中间阶段应该是完成了某个功能、达到了什么效果、对指标优化了多少等等。 但项目在进行到某个阶段之后,我们可能意识到已经出现了 Delay 风险,这时大部分人考虑的是加班。对待加班一定要慎重,因为增加工作时长并不意味着一定能赶上发布日期。加班冲刺还很容易产生大量技术债,这些技术债很可能会成为我们以后得麻烦。 尽管加班有种种弊端,可能经过深思熟虑之后,我们确定可以通过一小段时间的加班赶上最终的进度。在开始之前,需要告知每个人都了解进度落后的原因,并制定更切实可行的项目计划和时间表,如果某个阶段发现还是会延误,那么就修改原定任务或者放弃冲刺吧。 投资长期价值 权衡质量与务实 软件质量可以通过严格的 CodeReview、单测、代码覆盖率、自动化检测流程保证,但这些繁琐的流程会导致项目不够敏捷,过于敏捷又不利于控制代码质量。所以软件质量是一个需要权衡的问题,回到杠杆率上,我们应该去关注长期价值,我们追求的是什么。在衡量这两者时,有这些方法可以尝试:建立可持续的代码审查流程。它可以帮助尽早发现错误,因为考虑到代码会被审查,它还可以增加代码提交者的责任心。 利用抽象控制复杂性。将一些复杂的事情进行封装,抽象一次性解决难题,且解决方案可以多次使用。Google 构建了 Protocol Buffers 以可以扩展的方式对结构化数据进行编码,构建 BigTable 用于存储和管理 PB 级结构化数据,以提高生产力。 引入自动化测试。自动化测试最大的好处就是解放人工测试的成本。编写第一个测试通常是最难的。一旦有了一些好的测试策略、测试模式和测试代码库,将来再编写测试时所需的工作量就会减少。 偿还技术债。Martin Fowler 在《重构》中指出「最常见的问题是开发者失去对技术债的控制,并将未来大部分的开发工作花在支付技术债的巨额利息上。」谷歌会举办「修复日」活动作为偿还技术债的轻量级机制,鼓励软件工程师解决特定主题的技术债问题。与其他问题一样,技术债是否必须偿还也是需要权衡的,如果可以判断出它影响重大就需要尽快处理,如果它影响很小,我们应该将这部分时间用于更重要的事情上。为团队成长投资 招聘应该是工作中非常重要的一件事情,所以设计一个有效的面试流程是一件高杠杆率的事情。为团队添加一名实力强劲的工程师所带来的额外产出,远远超过你可以做的其他许多投资的产出。 为招聘投资可以从两方面考虑,一个清楚需要招聘什么样的人才,不断迭代优化面试流程。一个是设计一套高质量的入职培训,让新员工更快速的融入公司。 招聘流程优化:定期开会讨论招聘和面试效果,找到准确评估团队重视技能和素质的方法。 设计具有多层难度分级的面试题。 控制面试节奏以保持高信噪比。 旁听其他团队成员的面试流程,并彼此反馈Quora 的入职流程:CodeLab。它是一份文档,用于解释产品中的一些核心抽象的设计理念和使用方法,并提供编程练习以帮助验证对这些核心抽象的理解。 入职培训讲座。新员工入职前三周,会有10场入职培训,培训由团队中资深软件工程师担任讲师,会介绍代码库和网站架构,并演示不同开发工具的使用,和工程实践的价值观。 导师制。每个新员工都会配一名导师,以便为他提供更加个性化的培训。 入门任务。新员工会有第一天任务,第一周任务等,用于快速适应工作环境。另有两点能够体现团队价值的事情:代码所有权的问题。当一个功能只有你知道时,不要把这当做自己的价值体现,因为你会因此失去处理其他工作的灵活性。在团队中找到可以满足这些需求的其他成员,能让你有更多自由去专注于其他高杠杆的活动。事后复盘汇聚集体智慧。我们总不可避免的犯一些错误,或者获得一些好的经验。及时复盘可以帮助我们进行事后总结,不管是好的案例还是不好的案例都能从中吸取一些教训。总结 好的工程师文化能够吸引优秀的人才,不好的工程师文化会导致优秀的工程师离开。关于什么是卓越的工程师文化,它们具有这些特点:优化迭代速度 坚持不懈地推动自动化 构建正确的软件抽象 通过代码审查来关注高质量代码 在工作中互相尊重 建立代码的共享所有权 对自动化测试投资 提供实验时间,不管是工作时间的20%,还是黑客马拉松 培养一种学习和持续改进的文化 聘用最优秀的人作为普通员工应该向这样的团队靠近,作为管理者则应该把团队往这个方向塑造。
利用 Github Action 实现博客自动发版
- 24 Jul, 2022
背景 先说下背景需求,在摸鱼周报的整理流程中,最后一步需要生成公众号的原文链接,原文链接指向的是个人博客地址。博客需要发布才能产生外部链接,发布到不费事,但是操作步骤重复,且因为涉及博客推送相关的配置都在我的个人电脑里,所有步骤必须由我来完成。来回多次之后就考虑将这个流程做成自动化了,目标是让周报协作者都可以实现博客推送,用到的实现方式是 Github Action。 实现思路 在开始之前先了解下原先的发布流程,如下图表示:整个过程涉及 3 个仓库:Moyu Repo。管理周报文章的公共仓库,协作者可以通过它拉取和推送内容。Blog Repo。管理博客内容的私有仓库,周报只是其中一部分。Blog Website。博客的网站,它部署在一台腾讯云服务器上,它也是私有的。因为涉及两个私有仓库,普通协作者都没有他们的访问权限,所以发布流程都依赖我来完成。解决方案就是消除发布流程对权限的依赖,理想流程是这样的:这样触发入口就都集中在了共有仓库,协作者也可以参与博客发布。要实现这个流程需要将需求分为两步: 1、Moyu Repo 通过 Github Action 推送 Moyu 内容到 Blog Repo。 2、Blog Repo 通过 Github Action 发布内容到网站。 这其中最关键的是访问私有仓库时如何处理权限的问题。 Github Action 这里先简单了解下 Github Action。它是 Github 提供的为仓库定义自动化流程的方案,类似 Jenkins、GitLab CI/CD。Github Action 有一套自己的流水线配置方式,所有的流程都可以通过一个 yml 文件下发。Gtihub Action 有自己的虚拟机,支持 Windows Server/Linux/Mac,使用者无需关心环境配置问题,可以直接使用。 配置入口如下图所示:点击set up a workflow yourself,即创建了一个用于编排自动化任务的 workflow,它对应一个 yml 文件,所有的配置流程都在这里进行。 自动化任务配置前我们需要先考虑这几个问题:什么时机触发?在什么设备运行?如何执行自动化任务?我们看一个简单的例子来学习下 Github Action 如何定义这些行为: name: GitHub Actions Demo on: [push] jobs: Explore-GitHub-Actions: runs-on: ubuntu-latest steps: - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." - name: Check out repository code uses: actions/checkout@v3name 表示当前流水线的名字。 什么时机触发 在什么场景触发,对应的key 是 on。上面Demo里的[push],表示仓库发生push行为时触发任务。on 还有其他几种触发途径:pull_request:提交 PR 时触发 schedule:定时触发,可以按 cron 语法配置定频 workflow_dispatch:手动触发,有用户手动激活触发行为在什么设备运行 对应的关键词是runs-on,Demo里指定值为ubuntu-latest,表示执行设备是一个 ubuntu 设备。Github Action 还支持 macOS 环境,目前有三个 macOS 版本可以支持:虚拟环境 YAML标签macOS Monterey 12 macos-12macOS Big Sur 11 macos-latest或macos-11macOS Catalina 10.15 macos-10.15需要注意:macos-latest 不是最新的 macos 版本,而是 macOS 11。iOS 开发中我们可能还会关心 Xcode 版本,Ruby 版本等。以 macOS 12 虚拟机为例,Xcode 版本:Version Build Path13.4.1 (default) 13F100 /Applications/Xcode_13.4.1.app13.4 13F17a /Applications/Xcode_13.4.app13.3.1 13E500a /Applications/Xcode_13.3.1.app13.2.1 13C100 /Applications/Xcode_13.2.1.app13.1 13A1030d /Applications/Xcode_13.1.appRuby版本:2.7.6/3.0.4/3.1.2。 其他预制环境可以参考这篇文档:macos-12-Readme。 另外 Github Action 还支持将自己的设备定义为运行机,你可以在这里了解:About self-hosted runners;支持联机调试,可以通过这个插件了解:A debugger for actions。 如何执行自动化任务 有两种执行任务的方式,一种是直接在 yml 文件里编辑脚本,关键词是run。像是 Demo 里的 echo 命令,我们可以直接输入 shell 命令进行执行。 另一种方式是插件市场,像下面这种形式: - name: Check out repository code uses: actions/checkout@v3就是使用了 actions/checkout@v3 这个插件。Github 有一个插件市场,可以搜索所需插件。像是 Code review,SSH 登录等都有封装好的插件可以直接使用。实现方案 有了这些 Github Action 知识,我们就可以开始实现我们的需求了。最终效果分成两个需求。 Moyu Repo 向 Blog Repo 推送内容 我们按照前面的三个问题来设计这个功能。 什么时机触发? 发布之前需要经过多次修改,会有多个 PR 和 Push 行为,而 Blog 发布需要等所有内容都准备完成才会执行,一般只有一次。所以考虑使用手动发布的方式,以下是配置内容: # Action name name: Weekly Article Deploy # Controls when the workflow will run on: # Allows you to run this workflow manually from the Actions tab workflow_dispatch: inputs: weekly_index: description: 'weekly index for deploy' 手动发布还补充了一个 inputs,用于接收输出参数,weekly_index为参数名,用于表示要发布第几期。执行效果如下所示:在什么设备运行? 这个需要根据执行任务来定,这里只涉及一些文本转换和仓库操作,所以任意机器都满足需求,ubuntu 资源比较多,调度会快那么一点点,所以都可的情况优先选 ubuntu。 # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on runs-on: ubuntu-latestGithub Action 里有几个名词:workflow,job,steps,这里简单捋一下。整个 Yml 文件对应为一个 workflow,它表示一次完整的自动化任务运行过程。当前仓库的整个配置都是一个 workflow。 一个 workflow 可以包含一个或多个 job,这里的 jobs 下面一级内容就是各个 job。不同 job 之间可以串行也可以并行。build为会其中一个 job,也是本 workflow 唯一的 job。 如何执行自动化任务? 这个流程需要做的事情是把 Moyu Repo 内容转成 Blog Repo 的格式,然后推送到 Blog Repo 里。前一步可以封装成一个脚本,后一步往私有仓库推送需要生成一个具有推送私有仓库权限的 token。 token 的生成需要到这里:个人头像 -> Settings -> Developer settings -> Personal access tokens,点击 Generate new token。这一步需要输入密码,然后我们可以选择所需权限去生成一个token。对于私有仓库的推送,我们选中这一个权限就可以了:为了安全考虑,这个token生成之后只会可见一次,因为后面的步骤会使用,所以我们需要做好保存。 注意这个 token 是用户级别的,它可以用于访问修改该账户名下的任意仓库。 为了让 Github Action 可以访问到这个token,需要给它做一个配置。配置路径是:在该仓库下的 Settings(注意这个是仓库下的设置而非个人下的设置) -> Secrets -> Actions 点击 New repository secret。Name 的话可以命名为 ACCESS_TOKEN,Value 为上一步生成的访问 token。这里配置的任意内容都可以通过Github Action 访问到,且是加密的,创建之后只能看到 Name 看不到 Value。下面是具体配置: env: ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}# Steps represent a sequence of tasks that will be executed as part of the job steps: - name: print inputs run: | echo "Weekly Index: ${{ github.event.inputs.weekly_index }}" - name: Git Config run: | git config --global user.email moyuweekly@github.com git config --global user.name moyuweekly # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v3 # Runs a single command using the runners shell - name: Run a one-line script run: ./Script/ci_run.sh ${{ github.event.inputs.weekly_index }} shell: bashsecrets.ACCESS_TOKEN 表示的是该仓库下面 secrets 里 name 为 ACCESS_TOKEN 的内容。${{}}为 action 语法获取变量的写法。 因为我把对私有仓库的获取和推送都放到了执行脚本里,所以这里通过环境变量的形式把这个值传给脚本。 steps 用于表述运行步骤,这里是顺序执行的。上述流程涉及到4个执行步骤: 1、打印外部传参。${{github.event.inputs.weekly_index}} 表示输入的参数。 2、配置 git user 的 email 和 name。因为执行内容涉及提交操作,这里是为了标记自动化流程的提交。 3、uses 语法对应的是插件功能,这里输入插件名即可执行对应插件配置的功能。 actions/checkout@v3 是官方插件,用于拉取本仓库代码的。 4、执行脚本。我把私有仓库的拉取,内容的格式化,私有仓库推送都放到了这个脚本里。 私有仓库的管理需要考虑 Git 链接的形式,Git 链接有两种方式,一种是给 SSH 形式,这对于本地机器比较容易,对于不固定的打包机配置起来较为麻烦。第二种是 HTTPS 形式,HTTPS 是公有链接无法处理权限问题,但 Github 支持把 token 传入链接的形式来管理权限。这里选择使用HTTPS形式,配置的 Git 地址如下: https://{github_token}@github.com/username/repo.git对仓库的操作使用这个链接就可以解决权限问题了,执行结果如下:左边是 job 描述,右边是 steps 描述,每个 steps 都可以展开查看详情。因为这里的步骤只有代码拉取推送和格式处理,所以执行很快。 Blog Repo 发布网站 这个阶段对应的是 Blog Repo 推送内容到腾讯云服务器。还是按上面的流程设计实现方式: 如何触发任务? 这个历程是上一步的承接,前一步已经定好了推送频率,这里可以接收到 push 即触发任务。 push: branches: [ "master" ]在什么机器触发? 这需要考虑到所使用的博客框架,如果是 Hexo/Jekyll 使用 Ubutu 就可以了。因为我将博客框架迁移到了 Publish,Publish 是一个由 Swift 编写的静态博客框架,所以运行机器只能是 macOS。测试时发现 Publish 引用了libswift_Concurrency.dylib 这个库,所以还需要指定版本为 macos-12。 jobs: build: # macOS Monterey 12 runs-on: macos-12如何执行自动化任务? 执行流程大概是这样的:编译 publish -> 使用 publish 把 md 源文件转成静态网站格式 -> 发布到腾讯云。 正常能获取到 publish 执行文件是无需编译的,但因为我为了让它兼容 hexo 的格式,做了一些魔改,所以我使用的 publish 是一个动态的版本,它需要随修改随时编译。 发布至腾讯云,也需要考虑权限问题,个人服务器没有 Github 那种 token 授权形式,只能借助于 SSH 了。 SSH 在开始之前再简单回顾下 SSH 登录的一点原理。SSH 支持密码和密钥两种登录方式,我们一般为省去繁琐的密码输入,会采用密钥登录的形式。密钥登录使用的是对称加密,一般的做法是登录端生成一对公私钥,把公钥放到服务端,私钥保存在本地。对称加密解决的是信息传输不会被篡改的问题,它无法防止中间人攻击,因为它没有HTTPS 那样的 CA 来验证可信性。SSH 选择的是通过手动验证的方式,关于手动验证不知你是否还还记得这段内容: The authenticity of host 'host (172.168.*.*)' can't be established. RSA key fingerprint is 98:2e:d7:e0:de:9f:ac:67:28:c2:42:2d:37:16:58:4d. Are you sure you want to continue connecting (yes/no)?它就是用来手动验证的,我们需要通过 host ip 验证该链接是来自于可信的服务器还是中间人。确定过一次之后,这个信息会被写到本地的 known_hosts 文件中,之后对于同一 ip 的服务器登录就不会再弹这个验证了。对于自动化流程来说,我们应该将私有服务器的验证信息直接填入known_hosts文件,跳过阻塞式的二次确认。 流程配置 有了以上知识,我们对于密钥的配置流程应该是如下图所示:蓝色的钥匙为 pub_key,红色钥匙为 private_key,带钥匙的文件是 know_hosts。Github Action Runner 的配置流程如果都手动实现比较麻烦,我们可以使用install-ssh-key这个插件快速实现这个功能: - name: Install SSH Key uses: shimataro/ssh-key-action@v2.3.1 with: key: ${{ secrets.SSH_PRIVATE_KEY }} # it's value doesn't matter known_hosts: 'knowen_host_value for ssh-rsa'它要求两个参数,key 为 ssh 的 private key。SSH 私钥可以使用上个章节介绍的 Secrets Actions 进行存储,将其命名为 SSH_PRIVATE_KEY。 known_hosts 是对 Server 端的信任记录,用于免去手动确认的流程。这个内容的获取,有两种方式,你可以查看本地的 known_hosts 文件找到对应的目标服务器的记录,也可以利用 ssh-keyscan 去手动查找。 $ ssh-keyscan -H <host-ip>这个结果会按多种加密算法产生多个结果,我们需要选择类型为 ssh-rsa 的内容,因为Github Action 仅支持这一种加密结果。我们把这一条内容添加 known_hosts 参数即可。当然你也可以选择使用密钥的形式存放。 最重要的步骤已经完成了,下面就可以编译 publish 并发布内容了。 - name: Create Publish run: | git clone https://github.com/zhangferry/Publish.git ./publish cd publish make cd .. - name: Blog Deploy run: | echo "begin deploy" ./publish/.build/release/publish-cli deploy最后执行结果:遗留问题 往腾讯云服务器推送内容时遇到一个腾讯云给我发了告警邮件,说是检测到异地登录,来源IP是美国,危险等级高危。这个登录应该指的是 Github Action Runner 设备的登录,这个目前还没有找到有效的解决办法,因为IP是动态的,无法通过手动加白的形式避免。 另外也可关闭异地登录的报警,在告警设置里关闭异地登录报警选项即可。但这种方式也存在一定的安全风险,因为它不是解决问题而是无视问题。 我暂时没有找到更好的解决方案,如果有人知道更好的处理方式,欢迎告知。
macOS 进化史
- 06 May, 2022
macOS 最为 iOS 开发的钦定操作系统,且 iOS 本身就是通过它衍生出来的,所以我们跟它之间会经常打交道。为了验证你对它的熟悉程度,看下能否回答这几个问题:我们在说 MacOS 时通常会带上「X」,将其叫做 MacOS X 或者 OS X,这是为什么呢?「X」 有什么含义? Darwin、XNU、Mach、BSD 分别代表什么,之间又有什么关系? Mac OS 与 Unix 是什么关系? Apple 开源了 Darwin,对应的开源社区为什么发展不起来?本篇文章致力于帮助解答这些问题,也会顺道讲些 Apple 相关的背景小故事。 文章内容主要参考《Mac OS X And iOS Internals》(中译本叫《深入解析Mac OS & iOS》)和 《*OS Internal Volume I -- User Mode》,作者都是 Jonathan Levin。前者完书于 2012年,后者第二版本完书于 2019 年,后者不仅是前者的完善版本,还是一个全新版本,大量图文都进行了重写。随着时间的推进,后者对最新的技术有了更多讨论。MacOS 发展背景 背景 MacOS 的早期版本叫做 Mac OS Classic,它诞生于苹果,拥有伟大的 GUI 却是一个相对粗糙且很不成熟的操作系统。 在这期间苹果创始人乔布斯离开苹果创办了 NeXT,NeXT 公司生产 NeXT 计算机和NeXTstation,它们运行在叫做 NeXTSTEP 的操作系统之上。NeXTSTEP 有这些比较前卫的特性:采用 Mach 微内核 使用 Objective-C 作为开发语言 面向对象思想贯穿整个操作系统 设备驱动开发是一个面向对象的框架,叫做 DriverKitMacOS X 诞生 后来乔布斯回归苹果,也将 NeXTSTEP 带回了苹果。于是自然而然的,Mac OS Classic 和 NeXTSTEP 两个非常小众的操作系统进行了融合。他们一个拥有伟大的 GUI 但设计糟糕,一个设计很棒但 GUI 平淡,融合之后起到了 1+1 大于 2 的效果,诞生了一个流行的多的操作系统,这就是 MacOS X。 MacOS X 此时的几个核心组件:Cocoa、Mach、IOKit、Xcode 的 Interface Builder 都来自于 NeXTSTEP。这个操作系统的内核就是 Darwin(中译为达尔文)。 Darwin 是开源的,以它为核心诞生了 iOS、tvOS、watchOS、BridgeOS(用于 Macbook Touch Bar 的 OS) 等一系列变体操作系统。有一个命令可以查看系统所使用的 Darwin 版本信息:uname。 $ uname -v Darwin Kernel Version 21.2.0: Sun Nov 28 20:28:54 PST 2021; root:xnu-8019.61.5~1/RELEASE_X86_64Darwin 和 Darwin 变体的一系列 OS 版本是同步更新的。它们之间的版本遵循这个关系:Darwinver = 10.(MacOSVer + 4) = (iOSVer + 6) = (TvOSVer + 6) = (WatchOSVer + 13)后来 MacOS 的主版本号从 10 升级为 11,上面 MacOS 的版本对应关系发生了一些变化。到了这里有必要再理一遍 MacOS 的名称变化情况,确实很少有人能准确的称呼它,因为它的命名发生过不少变化。时间段 MacOS 名称 说明创建 ~ 2001 年 MachOS Classic 古典 MacOS2001年 ~ 2011 年 Mach OS X NeXTSTEP 与 MacOS Classic 合并之后的版本2012 年 ~ 2015 年 OS X 这是最后一个以猫科动物命名的 OS 版本,此后开始以加州地标命名2016 年至今 macOS 便于与iOS、tvOS、watchOS 命名统一为了便于混乱,从这开始的下文在讲到 MacOS 统称时均以 macOS 代替。 Darwin 操作系统的演变历史。图片来自(*OS Volume 1)结合上面图片可以再讲一个 iOS 的小故事,iOS1.x 版本最初的代号是 Alpine,这是 i 系列设备的默认 root 密码。但最后发布的版本代号是 Heavenly,因为这个版本的操作系统拥有完整的调试符号、未加密还容易反汇编,很多越狱者都依赖从这个版本中提取的符号和函数调用关系寻找破解灵感,从越狱者角度来看确实如天堂般美好。Darwin 的内部组成 Darwin 是一个类 UNIX 的操作系统核心,它的组成可以近似看做:Darwin = kernel + XNU + 运行时。macOS 从Leopard(10.5) 开始已经是一个经过认证的 UNIX 实现。 XNU 是一个占据关键作用的 Darwin 核心,XNU = Mach + BSD + libkern + I/OKit。 初版的 XNU 是 NeXTSTEP 的核心,它包括 Mach 2.5 版本和 4.3 版本的 BSD。NeXTSTEP 合入苹果后,Mach被升级为 3.0,BSD 升级为 FreeBSD。 Mach 和 BSD 一个是微内核(Microkernel)一个是宏内核(Monolithic Kernel),所以 XNU 是一个混合架构(Hybrid kernel)。理解这几种内核的关键是需要注意内核模式和用户模式占据的范围。Mach Mach(微内核)由卡耐基梅隆大学开发,它的目标是取代 BSD 的 UNIX 核心。这个微内核仅能处理最基本的操作系统职责:进程和线程抽象 虚拟内存管理 任务调度 进程间通信和消息传递机制由上图可以看出微内核的功能本来就少,其他 OS 功能是作为基础服务建设在用户模式下的。因为这个特性其内部任务的调用会有更频繁的内核态/用户态上下文切换,这会额外消耗时间。同时内核与服务进程之间的消息传递也会降低运行效率,所以这种设计通常会降低性能。 但它也有优点,就是服务进程容易扩展,服务进程出问题不会危及到 kernel 。得益于这种扩展性 MachO 能支持多架构文件,以此为基础 macOS 能顺利的从 PowerPC 过渡到 Intel 再到 M1。 BSD BSD(宏内核),它是 Berkeley Software Distribution (伯克利软件包)的缩写,这是一个派生自 Unix 的操作系统。BSD 是作为完善 Mach 的一个存在,它建立在 Mach 之上,并提供了一层更可靠更现代的 API。它主要包括这些:UNIX 进程模型 POSIX 线程模型 UNIX 用户和组 网络协议栈(BSD Socket API)宏内核的特点是用户服务和内核服务都运行在同一内存空间,这还有效降低了内核态/用户态之间的频繁切换,执行效率会更高。但是宏内核也并非没有缺点,就是扩展性较差,另外如果内核有一个服务崩溃,整个操作系统就会崩溃。 Darwin 架构 既然没有完美的内核模式,于是苹果就将两者混合,它同时兼顾微内核和宏内核各自的优点,这就是 Darwin了。图片来自(Mac OS X And iOS Internals) 这里没有表示出 XNU,它的边界可以看做是 Kernel/User Transition 这里,其下包括 BSD 和 Mach 的层级就是 XNU。在 macOS 的体系里,Darwin 之上的层次基本都是不开源的,他们是 Apple 的私有财产。 XNU 中还有另外两种重要组件:libkern:这是一个内建的 C++ 库,用于支持 C++ 运行时。有了它内核的很多高级功能都可以使用 C++ 编写。 I/OKit:这是一个设备驱动框架,凭借 libkern 提供的底层支持,驱动程序可以使用 C++ 实现。借助于 C++ 的面向对象特性,外部在创建驱动程序时会节省很多成本。Darwin 的开源之路 既然 Darwin 开源了,那为什么没有出现非 Apple 系的 Darwin 发行版操作系统呢?虽说开源,但 XNU 主要依赖的 Mach 和 BSD 本来就是开源的;Apple 还把对 ARMv7/8 的支持单独闭源;原本来源的 launchd,在 Mac OS 10.10 的版本之后也变成闭源项目合入到 libxpc 项目里了。这还不算,Darwin 的开源版本并没有剥离干净,里面还包含了一些 Apple 的私有 API,导致其并不能完整编译,还需要做一些额外改造。 围绕 Darwin 有两个重要的开源版本,OpenDarwin 和 PureDarwin ,可以看下他们当前的发展状况。 OpenDarwin 它由 Apple 牵头于 2002 年 4 月成立,其目标是加强苹果开发人员与自由软件社区之间的协作且将 Darwin 发展出另一独立版本。理想情况是苹果可以将 OpenDarwin 中的改进应用到 Darwin 中,而开源设计又可以完全控制该系统,将其用于 GNU-Darwin 等自由软件的发行版中。然而仅仅过了 4 年,OpenDarwin 就宣布关闭。以下是 OpenDarwin 项目组的陈述:Over the past few years, OpenDarwin has become a mere hosting facility for Mac OS X related projects. The original notions of developing the Mac OS X and Darwin sources has not panned out. Availability of sources, interaction with Apple representatives, difficulty building and tracking sources, and a lack of interest from the community have all contributed to this. Administering a system to host other people's projects is not what the remaining OpenDarwin contributors had signed up for and have been doing this thankless task far longer than they expected. It is time for OpenDarwin to go dark.主要因素有两个:苹果的 macOS X 对 OpenDarwin 掌控过强,未推进 OpenDarwin 的独立发展 开源社区的兴趣减淡。也可以说是前者导致了后者现在 OpenDarwin 的官网 http://opendarwin.org/ 仅剩一行字:Opendarwin memorial page。 PureDarwin PureDarwin 通常被认为是 OpenDarwin 的继承者。它的代码托管在 Github上,且仍在维护。Pure 的含义是更纯净,PureDarwin 仅使用苹果为 Darwin 发布的组件而不用 macOS 的其他组件。它的目标是通过提供文档,使开放源码爱好者和开发人员能够检索、理解、修改、构建和分发 Darwin,从而使Darwin 更易于使用。 因为缺少官方的支持,它当前在 Github 上的 star 数仅有 1.7k,由此可见它的关注度和发展都不算太好。但是当有人问为什么要花费时间维护 PureDarwin 时,他们的答案是:For learning and for fun.简短却让人振奋,与此同时还有那么一丝丝凄凉。 所以回归上面的问题为什么开源的 Darwin 没有发展起来,因为它是为 macOS 创建的操作系统,它依赖于 macOS 的特性,也依赖于 Apple 的支持,脱离这两者尝试走「 Pure」Darwin 开源路线是非常困难的。 Hackintosh OpenDarwin 和 PureDarwin 的发展仍带来了一些有益的事情,其基于开源的 Darwin 制作成一个可以完整引导并且安装的 ISO 镜像。之后 OSX86(一个致力于把苹果电脑出品的 macOS 操作系统移植到非苹果电脑上的计划)项目在此基础上继续发扬光大,努力将 macOS 完整移植到 PC、笔记本等设备,该行为被称为 Hackintosh,也叫黑苹果。 通常的黑苹果方案是借助于引导程序实现的,因为它不会修改 macOS 源文件,被称为最佳的合法途径。苹果曾经开源过 Boot-132,一个用于加载 XNU 内核的引导程序。Voodoo 团队基于该程序开发出 Chameleon(变色龙)引导程序,再后来 Clover 出现,可以让不支持 EFI 的电脑进入模拟的 EFI 环境。现在又有了 OpenCore,它在配置文件时比较复杂,但因其受到较多 kexts 作者的兼容和本身的易用性而得到相当数目使用者的追捧。 关于合规的问题,虽然引导的方式没有修改 macOS 的源码,但苹果的最终用户许可证协议(EULA)里并不允许将 macOS 安装在一台没有苹果商标的硬体上。苹果曾起诉并多起黑苹果相关的商业行为并获得胜诉,对于非盈利的个人 Hackintosh 行为,苹果并没有过多理睬。 吉祥物的故事 人们热衷于为受欢迎的操作系统或者框架设置吉祥物,Linux 的吉祥物是一只企鹅(名叫 Tux,Torvalds UniX的缩写)、安卓的吉祥物是一只绿色小机器人(无正式名称,被开发人员称为 Bugdroid)。再看下跟 macOS 相关的两个操作系统的吉祥物。 BSD BSD 的吉祥物是一只小恶魔😈,叫做 Beastie,它的发音跟 BSD 很像。它通常带支三叉戟,代表行程的分岔。Darwin Darwin 的吉祥物是 Hexley,它是一个卡通的鸭嘴兽,戴着 BSD 小恶魔的帽子,也拿着三叉戟。Hexley 是由 Jon Hooper 所设计的,版权也为他所有。但 Hexley 并不附属于 苹果电脑。本来这个吉祥物的名称应该是 Huxley,源由是捍卫达尔文(Darwin)进化理论的英国生物学家 Thomas Henry Huxley,而原先提议的人误以为是达尔文的助理,并错用了 Hexley。而发现错误时,要改名已经太晚,因此沿用了 Hexley 这个名称。 这个形象并不属于 Apple,而属于开源社区,所以开源版本的 Darwin 均有该图案的展示。「*OS Internal 三部曲」的书籍封面就是用的 Hexley 形象。 未来展望 对于 macOS 未来的展望这部分内容摘自《Mac OS X And iOS Internals》(注意其完成时间是 2012 年),站在 10 年后的今天我们可以再去看下这几个预测的实现情况。 根除 Mach 内核中的 Mach API 是 NeXTSTEP 时代的产物,运行速度慢,执行效率上很难赶得上 BSD。而且 XNU 本身更趋向于宏内核架构,如果移除 Mach 将内核建设为完整的 BSD,将会有很大收益,但这确实需要巨大的工作量。 该想法并没有达成,且 Apple 根本没有这么做的打算,混合内核并没有看上去那么遭,Mach 将在很长一段时间继续存在着。 兼容 ELF 格式 macOS 无法融入 UN*X 的世界最大的一个困难就是坚持使用 Mach-O 二进制格式。当然这个想法也需要依赖上一步的根除 Mach,这样 Linux、BSD 中的程序就可以不经修改直接迁移到 macOS 上了。 这是很美好的想象,考虑 Apple 对 Hackintosh 的打压,其商业策略是独占、完全掌控而非扩大市场占有率。 使用 ZFS macOS 早期使用的文件系统是 HFS+,但该文件系统遭受过大量批评 Linus 曾这样评价 HFS+:Quite frankly, HFS+ is probably the worst filesystem ever. Christ what shit it is.HFS+ 确实有很多不完善的地方,它大小写不敏感、不支持对数据内容进行 checksum 校验、timestamp 只支持到秒级。彼时 Sun 公司开发出 ZFS,号称是「宇宙无敌最强」文件系统,有传闻 Apple 将使用这一文件系统,但后来 Sun 被 Oracle 收购,这一想法最终无法实现。 2017 年, 伴随着 Mac OS High Sierra 版本,Apple 正式发布了 Apple File System(APFS)。该文件系统是 Apple 从零开发的,耗时三年,对于完整的文件系统来说,这个效率已经非常高了。其支持更多的功能,且宣称针对 SSD 做了很多优化。但让人遗憾的是 APFS 在多个方面的性能还没有超过 HFS+。 和 iOS 合并 在 macOS 和 iOS 的发展过程中,有不少功能都是在一个平台成熟之后移植到另一个平台上的。即使在 M1 芯片出现之前,这一想法也是有可能的,Apple 在很早之前就实现过硬件架构翻译机制 -- Rosetta。 当 M1 芯片发布之后,macOS 和 iOS 都已经可以运行在 arm64 架构的芯片上了,这个想法似乎顺其自然要实现了。但现实并非如此,相对于统一,Apple 更想要的是各司其职,每个产品线 macOS、iPadOS、iOS 都有其各自适用的场景,Apple 也极力往这方面宣传,这有助于产品的销售。 总结 再次列出开头提到的几个问题便于大家回顾这篇文章的内容: 1、我们在说 MacOS 时通常会带上「X」,将其叫做 MacOS X 或者 OS X,这是为什么呢?「X」 有什么含义? 2、Darwin、XNU、Mach、BSD 分别代表什么,之间又有什么关系? 3、Mac OS 与 Unix 是什么关系? 4、Apple 开源了 Darwin,对应的开源社区为什么发展不起来?
人物访谈 | 人在上海的东阁堂主
- 26 Apr, 2022
东阁堂主是我非常早就关注到的一位开发者,他写过不少开源库,个人 Github 地址是:https://github.com/dudongge 。本人非常热爱技术,最近翻译了一本 iOS 架构相关的技术书。因为他在上海,还处于封控期间,除了聊工作也会聊一下他目前的生活状态。 简单介绍下自己吧 大家好,我是东阁堂主,在成为码农之前,在车企里做过售后,仪器厂做过测试,电子厂做过机修。受室友影响,15 年入坑 iOS,目前在 B 站的漫画事业部。 工作相关 能简单分享下你的工作内容吗? 目前我在项目负责哔哩哔哩漫画国内版和国际版的需求开发,也会做些性能优化,比如启动优化,包体瘦身,核心页面的秒开优化等。从 20 年开始,B 漫就在项目中嵌入 Flutter,目前新需求基本都是 Flutter 开发。Flutter 开发页面很快,开发人员没有增加的情况下,开发效率反而大幅提升了。当然测试效率也会提高,之前要两端都要测试,现在只有着重测试一端,另一端测测兼容即可。 最近在上海的工作状态怎么样,生活各方面造成的影响大不大?可以分享一些这段期间学到的「封城」生存之道。 受疫情影响,我不得不蜗居在公司附近的出租房里,工作可以在家办公,但效率会稍微打折(主要是没有大屏的显示器)。狭小的空间限制我的躯体,却限制不了我的灵魂,我一向比较乐观,最近也是被抢菜搞得有些疲惫,生活物资基本全靠团长和政府救济,不能想吃什么吃什么了。总结一些封控期间囤物资的技巧吧:如果有官方消息要留在家中,可乘空闲时候去附近超市采购,如果厨具齐全,米面油,杂粮,速食,干蔬之类的可多备点,注意,在采购东西时一定要注意安全。封控期间可在 App 上抢东西,比如叮咚,美团,盒马,他们会在规定的时间内开放购买入口,要先把东西提前加入购物车,因为等开放时刻再加,基本上是来不及的。用安卓手机的可以在网上下载个抢菜插件(https://github.com/Skykai521/DingDongHelper),iOS 用户有筋膜枪就用筋膜枪吧。还有就是注意捡漏,时不时刷一刷,厂商也会时不时补货。等货到后,要注意货物表面消毒。加入小区群,这个是最靠谱,最省时省力的方式。团长发起团购消息时,及时回应,因为团购有截止时间。我加入的团购群有猪肉群,鸡蛋群,蔬菜群,牛奶群,面包群等。有什么需求,可以直接在小区群里咨询,有富裕的东西可以物物交换。找跑腿小哥,不过会加相应的跑腿费,只要价钱合适,会有小哥接单的,让小哥代买一些食品。在 Bilibili 工作是一种什么体验? 在 B 站漫画部门上班,氛围比较轻松,也算是弹性上班,公司鼓励奇装异服,彰显个性,可以带宠物上班,也有很多流浪小动物寄养在公司,哦对了,入职即送 B 站大会员🍻。 iOS 开发没人要了,网上有很多这样的劝退论调,结合你跟 iOS 之间结下的渊源与实际的工作情况来说一下你对这个现象的看法。 我是 15 年踏入 iOS 开发行列的,那时移动端正值繁花卓锦,烈火烹油的时代,各行业都想分一杯羹。所以 iOS 开始开发者岗位也是与日俱增,19 年的时候大浪淘沙,行业洗牌,很多公司倒下了,也直接导致了 iOS 的岗位减少。现在初级的 iOS 需求量确实少了,但还是有的,就像 B 站每年都有 iOS 实习生的名额。至少到现在,我认识的 iOS 开发者都有工作,当然打铁还需自身硬,提高自己的技能才是王道。 学习相关 看到你整理翻译的这本 iOS Architecture 书,能简单介绍它的主要内容吗?做这件事的出发点是什么?一共花费了多长时间?最终的收获又是什么? 这本书前四章会介绍架构的理论基础,会涉及到一些常用的设计模式,结合例子给出具体的代码实现,和读者一起讨论哪种架构适合自己的业务,以及使用架构会带来哪些益处。接下来几章会着重介绍MVVM、Redux、Element 架构的具体实现,使用时注意事项以及优缺点。最重要的是可以启发我们思考,当前我们的项目有哪些可以改进的地方。 因为之前没有系统想过 iOS 架构相关东西,想在这方面深入了解一下,就找到了这本书。整理和翻译花费近两个月,通过这本书的完成,使自己对通用架构有了更深一层的认识,感觉自己的 English 没有白学😅,当然体会到了翻译的不容易,算是完成了自己今年初定下的第一个目标吧。 对于一些非工作项的事情像是翻译书籍、写开源库,你是如何自我驱动来实现的。会不会遇到一些阻力,遇到阻力的话是如何克服的?做这些事情有没有给你带来一些意外收获? 翻译整理书籍主要就是想挑战一下自己,也希望可以帮助其他人,自己也是看别人翻译的文章和分享的库来满足日常的开发需要。写开源库,也是记录自己学习的一种方式,或许能帮到有类似需求的小伙伴。遇到的阻力就是时间节点问题吧,开始以为很快就能完成的,事非经过不知难,制定的计划会被其他干扰因素打破(比如上海这次疫情),根据实际情况修正计划,当胜利的曙光到来时,自己的幸福满足感爆棚。 结合自己的经历,能否分享一下对你来说好的学习经验和学习习惯? 工作中的经验积累很重要,要时不时的总结一下,好记性不如烂笔头,这些经验或者教训可是经过实际检验的,比自己写的 Demo 更具有可操作性。平时就是多看技术博客和技术公众号,多和组内的人交流。 可以在油管上找些教程,YouTube 可以自动生成字幕,不用担心听不懂。 这里特别强烈推荐一款应用,就是苹果自己家的:Developer。这款应用有 WWDC 相关的技术视频,可以缓存下来,也有字幕,闲时找自己感兴趣的看一看,还是会有收获的,稍微介绍一下,说不定你就会爱上它。个人爱好 除了作为开发者的身份,还有其他什么兴趣爱好可以分享的吗? 平常爱运动,也爱旅游,参加过几次马拉松,喜欢中国古典诗词文化,偶尔也会写首顺口溜。 之前在群里有看到你发的桌面照片,有很多手办,这是 B 站的风格还是你的风格。补几张照片让我们都开开眼吧 我不是真正的二次元,但也不排斥二次元文化,我的手办不多,补几张大佬的桌面吧。作为 B 站人,肯定少不了刷 B 站,推荐几个你感觉不错的up主吧。 硬核的半佛仙人:有搞笑的配图和内容,给平淡的生活增加一些笑意。 罗翔说刑法:让你在故事中读懂法律。 冒险雷探长:算比较早是旅游达人,可以领略到到国外的风景。 再推荐几个入站必看视频吧:赵本山:我就是念诗之王 最强法海【极乐净土】-咬人猫跟着雷总摇起来!Are you OK!再推荐一本书或者一个开发工具吧 在这里我不推荐学习相关的东西了,推荐一本书吧《明朝那些事》算是一套白话历史的丛书书,讲的诙谐生动,用现代人的观点解读当时的情境。网上也有相关音频,地铁上,睡觉前,都可以听一听,作为消遣娱乐。 可以谈一个自己最近才明白的心得、感受或者体会吗。 生于忧患死于安乐,家里常备粮,心里才不慌,意外和明天真不知道哪一个会提前到来,善待自己,善待他人。 有没有想借助于摸鱼周报宣传的。 漫画部门暂时没有 iOS 坑位 (⊙︿⊙),其他部门有,有看上的可以直接投递,也可以敲我:
人物访谈 | 东坡肘子:健康&开发
- 14 Feb, 2022
肘子是摸鱼周报的一位编辑,最早知道他还是通过他写的几篇关于 Swift 的博客。他的博客专注于 Swift 领域,每篇文章的质量都很高,是这个领域非常值得关注的博客之一。后来了解到他之前得过一场大病,现在的生活节奏基本平稳有序。又想到其他几位圈里同样受到疾病影响的开发者,就决定了这次访谈的主题:健康&开发。 简单介绍下自己吧 大家好,我是东坡肘子,70 后。小学时获得了我的第一台电脑(CPU 是 MC6800),几十年来更换过不少设备,算是亲身经历了信息时代的巨大变迁。尽管一直以来都对于硬件开发、软件编程十分喜爱,但并没有以此为职业。最近几年,由于疾病的原因,目前赋闲在家,通过博客 肘子的 Swift 记事本 记录一些关于 SwiftUI、Core Data、Combine 等方面的学习心得。 由于治疗的需要,我从一个不喜欢记笔记的的人变成了每天必须要记录大量数据(其实主要还是依赖我妻子)的人。前年通过手术,生活质量获得了较大的改善,本以为所需记录的数据能少一点,但事与愿违,数据量减少了,数据种类却大大的提升了,而且可以明确的是,这些数据将要在我有生之年一直记录下去。因为喜欢技术和记笔记的需求,于是就开发了一款 iOS app ——「健康笔记」。 最近也经常听到一些上班族特别是程序员群体相关的健康话题,很显然健康非常重要也很容易被大家忽视。方便讲一下你患病的原由吗,也提醒大家重视健康问题? 2013 年,我的身体状况出现了异样,每天不停的呕吐,几乎无法进食。当时工作很忙,休息时间无法得以保障,自认为或许是睡眠不足导致的肠胃问题。在家人的强烈要求下,去医院做了检查。简单的化验后,医生立刻就明确了问题所在——尿毒症。入院时的身体相当糟糕,肌酐达到 2100μmol/L(正常值通常低于 100)、血红蛋白 4.6(男性正常值通常高于 12)。 人是一种挺神奇的动物,在去医院的前一周,我还跑了三个城市。但在住院后,透支的身体立刻就无法继续坚持了,出现了数次的生死危机。经过几年的透析治疗,我在 2018 年接受了肾移植手术,目前各方面都恢复的不错。 都说久病成医,作为一个经历较大疾病的人,有没有什么忠告或建议给大家? 我很幸运,在关键时刻就医、有家人的悉心照顾、有不错的医疗资源。在身体逐渐恢复后,我迫切地希望有更多的人能够及早认识到健康的重要性,避免走到难以挽回的地步。 如实在无法做到早睡早起、按时吃饭、健康饮食、科学养生,希望至少重视以下两点:尊重你身体的求救信号。绝大多数的疾病,都不会毫无预警地凭空而来。不要将身体的一切不良反应都归于工作忙、压力大等因素。最起码要严肃的面对每一次的体检结果,及时完成需要进一步随检的内容。 不要过度。在连续的熬夜加班后休息半天,休息后再工作或许可以取得意想不到的进展,熬夜加班中休息半天,或许可以取得意想不到的工作进展,更加关键地是,可以让你紧绷的身体获得适度的放松。身体是属于你自己的,也只有你自己可以对其负责。爱惜它、保养它,真正做到「活久见」,而不只是当成一个梗。这几年我在知乎上从一个曾经的透析患者角度,对尿毒症、肾移植等方面的内容做了些介绍和解答。以下是其中的一篇,希望帮助到有需要的人——刚确诊尿毒症,患者和家属应该注意什么?。当然也衷心地希望大家都能身体健康。@zhangferry:我的父亲也是尿毒症患者,于 19 年做了肾移植。他早期是痛风,痛风患者是需要严格控制喝啤酒的,他忌不住嘴,导致病情加重,以至于发展为慢性肾炎。后来看一则广告推荐的中医,治疗不当,最终发展为尿毒症。我曾随父亲多次往返医院的血液净化中心,也见过很多年轻的患者,对这个病感触非常深。现在回想过来正是由于早期很多应该做的措施没有做对,才造成了最终的后果。当前的教训就是:重视健康问题,及早正确的治疗。 从痛风这个点说起,它的前身一般是高尿酸血症,长期的高尿酸血症易发展为痛风。目前高尿酸血症的患病人数为 1.77 亿,痛风患病人数为 1466 万,这个比例已经很大了,以至于高尿酸血症被称为"第四高"。尿酸指标属于肾功能检查(非血常规),一般体检都会有,当尿酸数值超过 420μmol/L 即表示为高尿酸血症。如果是爱康国宾的体检的话,App 里体检报告的基本健康数据就会显示尿酸数值。 高尿酸血症及痛风的高发人群是:偏好海鲜等高嘌呤食物、过度饮酒不节制、作息不规律、纵容肥胖,不爱运动、吸烟。所以如果你的尿酸指标已经高了的话切记克服以上的生活习惯。 人身体的潜能和耐受力都是巨大的,特别是年轻的时候,但要知道这不是挥霍身体的理由。这副躯体我们是要用一辈子,而且没法随意更换的,我们一定要好好爱惜它。最后还是希望通过肘子跟我的一些经历,提醒到大家重视健康问题。 数据参考:第一财经商业数据中心:2021中国高尿酸及痛风趋势白皮书你在开发的一款应用是「健康笔记」,能简单介绍下这款应用的功能和未来规划吗? 透析阶段,我采用的是腹膜透析方式。它的优势是可以在家中进行,无需每周多次往返透析中心。腹膜透析每天需要做多次的透析液更换,并且每个月都需要去医院做随诊和验血。数年间,我记录了大量的有关透析治疗和身体检验等方面的数据(数个笔记本)。移植手术后,因为需要终身服用免疫抑制剂,目前仍每个月进行一次验血,以跟踪某些指标。 尽管市面上已有一些针对特定疾病进行数据跟踪管理的 App,但随着治疗的发展,需要记录的内容也不断发生变化,到达某个阶段后,这些 App 便无法继续胜任了。 因此,我决定开发一款可记录各种数据类型的 App。 本质上讲,「健康笔记」是一款支持高度定制数据类型的记录工具。目前支持七种数据格式,并可为数据设置各种验证条件。除了健康数据外,使用者还可以使用「健康笔记」记录生活、工作中遇到的绝大多数可量化或不可量化的内容。 相较于纯记录型工具,「健康笔记」更注重对数据的分析和管理。提供了多维度的图表,并且使用者可以将 App 中的数据导出到其他的软件或工具中进行分析。 「健康笔记」基于 SwiftUI 和 Core Data with CloudKit 进行的开发。目前可用于 iOS 14 以上的设备。当前的版本为 2.x,3.0 版本目前仍在开发中。现在的生活节奏怎么样?你说目前是处于一种赋闲在家的状态,对于没有外界约束的状态,保持规律的作息是比较难的一件事,你每天的时间是怎么安排的呢? 当前的生活状态可以用两个字来总结——规律。每天早上 6:00 点起床、晚上 10:00 睡觉,三餐准时,全年不变。生活的内容主要围绕着照顾猫狗、健身、学习、阅读。 我已无需强迫自己遵循以上的作息,相反,如果某天意外地违反我反倒有些不适应。 你是怎么考虑独立开发和远程工作的? 「健康笔记」算是我的独立作品,但我并不算是独立开发者。 以我的理解,严格意义上的独立开发者至少要满足两个条件:将商业的思维贯穿于开发行为之中,开发的是商品而不是作品 要有以开发成果作为其主要收入来源的决心当有了以上觉悟的情况下,结合自身的情况再决定是否踏足这个领域。 因为疫情的原因,远程工作得到了前所未有的发展。在某些领域,远程的的效率甚至高于传统的工作形式。但无论远程有多么的方便,仍应尽量保持一定量面对面的交流。摄像头、麦克风、文字所能传递的情感与信息实在有限。 对于技术,目前主要就是在研究 SwiftUI 和维护自己的应用吗?2022 年,有没有什么新的技术方向的规划? SwiftUI 是一个比较新的框架,处于快速变化和发展中,今年仍会投入不少的精力对其跟踪和学习。「健康笔记」也会做一次彻底的更新,相对于功能上的增加,我更想在 app 架构上有所突破。今年会着重于夯实基础,逐渐从「知其然」向「知其所以然」转变。 看你每周都会固定输出 Swift 相关文章,而且质量都很高,相信肯定是花了不少时间整理的。也看到你最近发了一篇停更说明,说是遇到了一些瓶颈,计划用一段时间做一些系统性的充电。这种严谨的学习态度非常让人钦佩,但另一方面产出数量就会降低,能说下你对自己产出内容数量和质量上的一些想法吗? 创建「肘子的 Swift 记事本」的初衷很单纯,通过记录加深理解、梳理思路。我对内容的产出数量并没有具体要求,但希望做到言之有物,在满足自身学习需求的同时具备一点分享的价值。 「肘子的 Swift 记事本」和「健康笔记」之间是相互依存的关系。因为想写「健康笔记」,所以创建博客帮助学习;文章的方向基本围绕着「健康笔记」的需求展开;学习的结果又通过「健康笔记」来得以实践。 从去年年中开始,我便开始了「健康笔记」3.0 版本的开发。在已完成了 80%左右的情况下,我决定将之前的工作全部推翻。尽管相较于 2.x 版本来说,新版代码有了些提高,但对我本人来说并没有质的飞跃。「健康笔记」作为个人实践和检验学习结果的载体,我并不希望为了升级而升级(从功能和稳定性角度来看,2.x 版本目前仍可胜任)。 此次停更便是想用一段时间来系统改善开发过程中发现的不足。此阶段的学习重点集中于理论层面,大多与语言和平台无关。希望届时能够有所收获。 写文章非常容易遇到知识盲点,对于这种问题,你是如何快速梳理出正确脉络的,有没有什么可以借鉴的技巧分享下? 事实上,并非总能快速梳理出思路。当碰到一个盲点时,我喜欢采用拓展阅读的方式,可能仅仅因为某个没有使用过的 API 而借机学习了解一下整个框架;一个短时间就能找到解决方案的问题,我会将其扩张成几天才能学完的内容。前期这种做法会十分耽误时间,但在有了一定的储备后,对于之后遇到的问题,梳理起来就会方便很多。 另外,我会订阅大量优秀博客的 RSS 或 Newsletter。每天早上我会用 30-60 分钟,将最新的文章进行一遍通读,在将来遇到问题时,从记忆中找寻解决之道。 以下是我经常关注的英文Blog或Newsletter,中文内容还需更多地关注摸鱼周刊。名称 地址 简介Augmented Code https://augmentedcode.io/ 频谱查看应用 Signal Path 作者 Toomas Vahter 的博客。每个月 2-3 篇的更新量。内容主要涉及 SwiftUI、UIKit、XCTest 等。Create with Swift https://www.createwithswift.com 由三名意大利人(Giovanni Monaco、Tiago Gomes Pereira、Moritz Philip Recke)创建的博客。内容以 Combine、SwiftUI 为主。Donnywals.com https://www.donnywals.com Practical Combine 以及 Practical Core Data 两本书籍的作者作者 Donny Wals 的博客。主要聚焦于 Core Data、Combine、SwiftUI 等内容。Dave Delong https://davedelong.com Dave Delong 的博客,最近一年更新的不太频繁。2020 年创建了一系列有关如何用 Swift 开发 HTTP Stack 的精彩内容。Filip Němeček https://nemecek.be ImpressKit 作者 Filip Němeček 的博客。关于 UIKit 方面的内容较多。FIVE STARS https://www.fivestars.blog/ Federico Zanetello 的博客。当前集中于 SwiftUI 方面的内容,每篇文章都很有价值。Hacking with Swift https://www.hackingwithswift.com 畅销书籍作者 Paul Hudson 创建的网站,内容涉及 Swift 的各个方面。Holy Swift https://holyswift.app Leonardo Pugliese 的博客。除了有关 Swift 的内容外,每篇文章都会介绍一副绘画作品。iOS Dev Weekly https://iosdevweekly.com Dave Verwer 创建的 Newsletter 站点。少有的仅以 Newsletter 作为表述方式的作者。Masilotti https://masilotti.com/ Joe Masilotti 的博客。有不少关于单元测试、UI 测试方面的内容。Oleb https://oleb.net Ole Begemann 的博客。十多年间持续创作和 iOS 开发有关的内容。onmyway133 https://onmyway133.com Khoa 的博客。非常高产,最近两年有关 SwiftUI 的内容居多。Raywenderlich https://www.raywenderlich.com 知名的技术书籍出版商。尽管是商业机构,但仍提供了大量优秀的免费课程(课程基本上都会提供完整的项目代码)。Sarunw https://sarunw.com 泰国开发者 Sarun W 的博客。创作了很多有关苹果生态开发的内容。他开发的 codeshot 可以方便的将代码转换成漂亮图片以利于交流。Swift with Majia https://swiftwithmajid.com Majid Jabrayilov 的博客。他关于 Swift UI 数据架构方面的文章对我的影响很大。最近在做 Microapps 的专题。他的周刊 SwiftUI Weekly 已经提供了超过 90 期的内容。SwiftLee https://www.avanderlee.com Antoine van der Lee 的博客。除了原创的文章外,每周通过 SwiftLee Weekly 介绍其他优秀的文章和工作机会。他开发的 RocketSim 对于 Xcode 的使用者帮助不小。Swiftly Rush https://www.swiftlyrush.com Adam Rush 的博客。坚持周更,以 Tips 为主。也提供周报Swift Rocks https://swiftrocks.com Bruno Rocha 的博客。更新不频繁,但不时会有相当有深度的内容出现。Swift by Sundell https://www.swiftbysundell.com Swift 静态站点生成器 Publish 的作者 John Sundell 的博客(我的博客就是由 Publish 构建)。除了每周精彩的文章外同时还通过 Podcast 与很多业内专家交流最新的技术动态。The SwiftUI Lab https://swiftui-lab.com Javier 的博客。他撰写的关于 SwiftUI 的文章对 SwiftUI 的开发者影响很大。他开发的的 A Companion for SwiftUI 是每个 SwiftUI 开发者都应购买的工具。Trailing Closure https://trailingclosure.com 着重于 Swift UI。每篇文章都会介绍一个 SwiftUI 动效方面的具体实现。Try Code https://trycombine.com Marin Todorov 的博客。作者参与了不少苹果官方的开源项目。最近正在打造一款轻量级的 Swift IDE。Yet Another Swift Blog https://www.vadimbulavin.com Vadim Bulavin 的博客。内容主要涉及 Swift、SwiftUI、单元测试等。现在很多开发者会因为程序员是青春饭而焦虑,而你作为一个技术领域的老兵却时刻保持着对技术的热情,能说下你能一直保持热情的原因吗? 学习使我快乐,能力提高让我获得满足。有点类似于打游戏,不断通关,坚持不 Game Over。 当前的职场环境好像给每个参与者都带来了不小的压力。与其为年龄而焦虑,我觉得更应该时刻关心自己是否保持了学习的热情和动力。技能往往是与职业绑定的,而学习能力与职业无关。人一生中处于不同的岗位或职业是十分正常的事情。相较于 IT 届,年龄因素在很多其他行业占据着更加重要的位置。无论身处什么行业,在职业技能提高的同时,也要做到个人综合能力的提升。尽管未必能减轻多少因年龄而产生的焦虑,但至少可以获得更多面对未来的信心。
2021年度总结
- 29 Dec, 2021
今年还是疫情年,可以预见的 2022 仍然会被疫情所笼罩。因为疫情的缘故出行被限制,各个行业也都面临不景气的现状。曾几何时我们对努力奋斗都开始有些不懈,躺平成了主流。罗曼罗兰说过:世界上只有一种英雄主义,那就是认清生活真相之后依然热爱生活。虽无英雄主义,但面对当下的艰难,我依然愿意去寻找天空中的彩虹。简单回顾下即将过去的 2021,2022 要整装待发啦。 普通的生活 普通生活中也有几件值得记录的事情。 半马今年的北京半马,因为疫情原因,参赛人数减半,我算是运气爆表才中的签。因为平常运动不多,且离比赛只有四周左右的准备时间,给自己制定了如下跑步计划: 第 1 周:早晨 2km,晚上 3km 第 2 - 3 周:早晨 2km,晚上 5km, 第 4 周:早晨 3km,晚上 4km。 配速都是在 6 分 30 秒左右,这里没有一次跑太多就是为了防止肌肉损伤。跑步计划虽然没有严格到一天不落的执行,但基本都做到了,期间均正常饮食,训练结束称体重发现自己还瘦了 4 斤,跑步对减肥真的有效。因为比赛前的训练不当,导致比赛期间小腿肌肉仍然酸痛,算是带点小伤坚持了全程。最后成绩是 2 小时 35 分,也挺满意了。 回顾整个过程:清晰的目标 -> 清晰的训练 -> 满意的结果,这是完整的正向循环机制,希望以后每件认真对待的事都能劳有所得。 拔牙 有智齿这件事被查出来已经有一定时间了,每年体检的时候都会被医生提醒应该拔了。但一直都没有下定决心,疼的时候没法拔,不疼了又害怕拔牙太疼,一直拖到今年。直到上个月,媳妇悄悄给我挂了北大口腔的号,说这个号可不好挂,不要浪费了机会,我才决心去拔。 我是四颗智齿都有问题,第一次拔了左边两颗。只有打麻药的时候感到有些疼,牙拔出的瞬间其实是无感的,听到牙齿落入铁盘的声音,我才意识到原来牙齿已经拔出来了,总共不到 5 分钟。后续恢复了一周,就基本无感了。第二次拔了右边两颗,稍微费劲些,牙龈缝了两针,术后第一天一直发烧,第二天才退烧。虽然过程不轻松,给自己折腾够呛,拔完之后心里却踏实很多。因为已经拔掉了,我知道即使再难受,过两天就会好了,这事已经过去了,而没拔的时候,却总是处在担心拔牙的不踏实状态。 这其实就是长痛不如短痛啊,勉强可以接受的状态容易削弱人的意志,如果奋起抵抗,即使经历短暂痛楚,获取的却是长远的舒适。这是非常划得来的,所以该迈的坎不要害怕尽早迈过它。 生活趣事今年的动手能力多少提高了一些,不只是做饭,现在清理空调滤网,刷油烟机都开始尝试了。 说起做饭,比着去年被迫的状态,今年做多了已经开始享受做饭的过程了。油烧热,倒上葱花,滋滋啦啦的,葱花爆香的味道扑鼻而来,再加肉或者菜,调味品一放,这不就完事了吗。复杂点的还需要再加几道工序,注意顺序上的差别,所有菜都差不多这个定式。 后来吃饭的过程养成了看美食视频的习惯,做好饭撑开桌子第一件事就是把 iPad 支起来,看「盗月社食遇记」或「绵羊料理」。过程中不仅见识了各样特色美食,还观看了很多美食复杂的制作工序,我要收回做饭容易的话了,其实每一行都不是看起来的那样简单。 说到 B 站,也推荐几个今年发现的宝藏 up 主吧,「-LKs-」、「有山先生」、「oooooohmygosh」、「贪玩歌姬小宁子」。有不少人认为 B 站为了扩张导致用户质量下降,而怀念过去的 B 站。我是不认同的,维护 B 站质量的主要是那些 up 主而不是用户,B 站模式本身就适合更广阔的人群,在社区和谐的土壤里, UGC 内容的创造力可以趋近于无限。B 站的服务用户应该更广,生命力也应该更强才对,我非常看好 B 站的,小破站给我冲起来! 影视 看了两部经典动漫,《进击的巨人》、《一拳超人》,对于在这之前只了解火影和海贼的我来说,这两部的剧情和画面多少都有点让我惊掉下巴,动漫还可以这样?《巨人》的漫画在今年的 4 月 9 号完结,当我打开知乎想窥探一些新剧情时,发现了和以往完全不同的评论风格,这之前都是夸谏山创多么多么厉害的,而在这之后都是骂的。随着翻看评论,大概了解了烂尾的现实。同时这也是一个神奇的现象,当一步作品足够优秀,吸引甚至影响到很多人之后,这个作品本身就会被认为是大家共有的一个东西,如果最终作品变质,那当初有多喜欢这个作者,后面就有多讨厌他。 不说让人失望的《巨人》了,还是来看可爱的波吉吧。我理解《国王排名》受欢迎的原因是它展现了很多人类温柔的情感。不想让人看到自己脆弱的一面而偷偷哭泣的波吉,表面严厉实际一直保护波吉的皇后,这些真善美是人们本身就有的情感,但随着步入社会,我们对他人反而是越来越多的戒心,越来越不愿意轻易与人交心长谈,我们正在失去的这些东西被展示出来,从而击中自己内心,所以我们会更爱他。今年还发现一个宝藏播客节目:《卖鱼桥分桥》,小宇宙和 Podcasts 都可以搜到。关注到这个节目是因为创作者是 iOS 圈里的一位开发:没故事的卓同学。一开始是想看看开发者的副业生活可以怎么样,没想到却被好几期节目圈粉了。特别是歌单那几期,有点超神了,顺道我也来点 BGM 吧。昨日像那东流水 离我远去不可留 今日乱我心多烦忧 抽刀断水水更流 举杯消愁愁更愁 明朝清风四飘流中间卓同学还有几次尝试邀请我参加节目,都被我拒绝了,这里澄清下,主要原因还是我不够自信,总担心自己说不好。卓同学是我非常佩服的一个人,希望后面我能收拾好自己的信心再一起合作。 缓慢成长 阅读 今年阅读不算多,到也遇到了几本非常好的书,这里列出来简单总结一下,大家如果有兴趣可以买来看一看。 《邓小平时代》:这是一本邓公的人物传记,写了很多那个历史背景下的很多故事,非常详实,让我对政治这个词有一些不一样的认识。因为内容太敏感,这里就不发表评价了,大家如果对那个年代,对邓公感兴趣的话,这本书可以作为首选资料阅读。《经济学要义》:这本书比较通俗易懂的把多个经济学概念进行了串联和解释。在我看来,经济学最大的作用是对社会经济现象的解释,书中有几个比较重要的经济学概念。 边际效益:效益是收益和付出的比值,带上边际就是最后一个单位的收益和付出。这里有一个边际效用递减规律,就是当在一件事情上投入过多成本之后,其带来的收益会越来越低。以工作举例,重复的工作事项,仅有第一次是边际收益大的,后续的重复过程收益都是在不断递减的。 机会成本:鱼和熊掌不可兼得是对机会成本的最好诠释,每天我们都在面对诸多选择,凭借自己的阅历和经验做出选择,并得到了想要的结果。但从经济学上来看,事情并没有结束,每个选项的背后都意味着放弃了其他选项,那些放弃的选项中收益最高的就是机会成本。 以看视频为例,当你想看某部影片时发现,正版网站可以直接观看但需要付费,盗版网站免费但需要一定的检索时间。如果你认为收益更大的肯定是看免费的了,但就是忘了考虑时间导致的机会成本。天下没有免费的午餐,不收钱可不代表免费,时间和注意力也是稀缺资源,哪个能给自己带来最大化收益才应该选哪个。 《终身成长》:不要用固定型思维,而是成长型思维,相信人本身是可塑的,这个不光是对自己,还可以用于教育。文中列举了很多教育孩子正确和不正确的方法,各位宝爸宝妈可以看看。 《暗时间》:刘未鹏信仰的东西应该就是思维改变生活,这是他博客的标题,也是这本书探讨的核心观点。书中提到很多有趣的心理学现象,来帮助我们理解自己为什么会有那种行为。其中一个叫:自利归因。意思是人们总是习惯的把一件事情发生的原因归结为对自己有利的情况,通俗来说就是人们总是倾向于为自己辩解。比如我们因为晚起导致上班迟到,遇到了堵车,我们就会认为迟到是因为堵车导致的。即使没有堵车,我们也会找到电梯慢等原因,但真实原因其实是晚起,就因为我们不愿承认自己的错误所以才会就近找一些借口。 再有如果是我想做某件事,又感觉自己不太擅长,做不了,就会找各种接口推脱。到最后确实没做成,回顾时的自我归因会是,「我不想做」,心理还想了各种借口,那件事也没有那么重要,以后还有的是时间。因为不愿承认我不能,而改成了我不想,就因为这种解释会让自己心里更舒服,这能获得暂时的心理安慰,却让我们忽视了自己的弱点。这个理解深深击中了我,促使我在复盘各类事情的原因时不要给自己找借口。 文学类看了几本日本小说,太宰治的《人间失格》,东野圭吾的《幻夜》《嫌疑人x的现身》《疾风回旋曲》《白马庄杀人事件》《假面舞会》。之前有看过《白夜行》和《解忧杂货铺》,发现自己成了半个东野圭吾粉丝了。不过不得不说东野圭吾的叙事技巧确实厉害,情节跌宕起伏,伏笔一个接一个。相比近期各种让人失望的影视剧来说,小说一般都不会让人失望。凭借对东野圭吾的喜爱,我一连又买了好多本他的书,目前收集了这么多:小说看多了技术类书看的就少了,完整看完的有《Head First 设计模式》《Swift异步与并发》《Objective-C编程全解》。喵神的书也是一如既往的好,由浅入深,虽然一行并发代码都还没写,但感觉好像对整个设计架构已经有了大概的了解。虽然系统提供了一些方法用于适配 Swift 并发并降低可接入版本至 iOS13,但迁移成本仍然是比较高,导致使用率还很低,希望明年 WWDC 苹果对这部分的过渡有更多平滑方案。 《深入解析 Mac OS X & iOS 操作系统》和《深入理解计算机系统》也看了一些,这两本对我来说更像工具书,对某个地方有疑惑时会拿来翻翻学习一下。 摸鱼周报 去年的一项 OKR 是摸鱼周报全年能够产出 15+ 期,当时是才刚发了第一期内容。今年的结果是一共产出了 38 期,除了早期几篇不稳定和节假日休刊之外,其余时间均是每周一篇,已经远超去年的规划了。这当然少不了小伙伴们的帮忙,因为摸鱼周报本身的故事也不少,所以决定单独用一篇文章来写,相关内容大家再等等吧。 工作赶到年底裁员,多少有些突然,海外部门应该是最严重的,裁员比例 7 成以上。在爱奇艺待了将近 1 年半,有幸接触到一位非常优秀的领导,学到了很多东西。也非常感谢期间一起合作过的小伙伴,祝大家前程似锦哈。 前段时间看了极客时间里的一个专栏:《10x程序员工作法》,它由火币网首席架构师郑晔整理。发现有很多内容跟自己的工作心得比较类似,这里结合一下来整理今年工作上的感悟吧。 以终为始 以始为终是专栏里的一个重要主题,其来源是《高效能人士的七个习惯》里的第二个习惯。以始为终的含义是以目标为导向,网上流传亚马逊 CTO 介绍亚马逊如何开发一项产品的顺序: 1、写新闻稿 2、写 FAQ 3、写用户文档 4、写代码 事件真实性有待考究,但这件事本身是具有参考意义的,惯性思维我们很容易按照既定顺序去思考一件事,但有时候倒着思考会给我们带来更多启示。 我们还可以利用这个做一些提前演练,比如有一个较大重构模块需要上线,在未开始之前就进行构思,如何做如何做,甚至考虑到如果出了线上事故该如何处理,是否要使用 AB 测等等。这是第一次创造,我们会有一个清晰的目标,之后采取实际行动时,对照这个目标,一步步落实,这是第二次创造。 任务拆解 工作中的很多事情都可以借助于任务拆解来开展,它的一个最大好处就是打开了我们抉择的选择范围。0 和 1 对应一件事情做还是不做,通常选哪个都是艰难的,但如果选项更多呢,0,0.1,0.2, 0.3 一直到 1,我们再做选择时就会容易一些了,这就是拆分之后的一个好处。 明确边界 工作中很多事情都是在不确定中找确定,比如我们要确定是否能如期完成开发,但开发中会依赖后端进度、设计进度、测试进度等,他们能否按照我们预想的节奏完成都是不确定的。这时可以尝试明确一下边界,依赖后端,就告诉他们我们能够接受的最晚完成时间是什么时候,设计和测试环节一样,中间由项目统筹,即使出问题了也可以有依据确认哪个环节。 与之类似也可以给自己定边界,根据任务拆解内容制定计划,什么阶段应该完成到什么程度。 有效协同 因为公司里几乎所有的工作都是需要协同才能完成的,所以如何有效协同,你去明白别人的意思和让别人明白你的意思都非常关键。 如何明白别人的意思相对简单些,重点就是提前了解对方要表达的东西。提前的作用比较重要,一方面可以防止在需求评审时被产品的思路带着走而遗漏细节,还可以提前发掘一些疑问点在会上讨论。有时我还会对照 PRD 想象每个要点实现成代码应该是什么样子的,详细的预演通常也会发现一些问题。 如何让别人明白自己的意思这里引申一个小故事。之前在小组里做过一次技术分享,当时准备了很多东西,我还尝试去想大家看到幻灯片时会问哪些问题,我又该如何回答。但实际效果却并没有达到我理想的样子,有提问但感觉是比较浅显的问题,我认为大家会疑问的地方却没有人提,所以多少感觉有些受挫。后来跟老大交流了这个问题,他的回答让我释然不少:技术分享本身能有一半人认真听且跟上分享者的节奏就非常不错了,因为分享者提供的内容通常是他擅长的领域,让一个学习者去跟专业的人员对比这是不对等的,不应该过多强求。但是如果你分享的内容不只是扩展视野还是需要大家马上使用的,可以使用提问的方式,抓个人问他一个问题,来确认他的听讲效果,同时也起到强调重点的作用。 涉及团队协作不要抱太高要求,不要把每个人想的太理想化,及时沟通,多次确认,这些才是有效协同应该采取的方式。 其他 还有一些其他感悟,就简单列举了。 数字化衡量任务:要让自己的工作内容可量化,这里比较适用于做 OKR,量化的好处是便于分析成果,没法量化的情况很容易迷失。 数据分析非常重要:曾经解决一个困扰很久的 Bug,就是从一堆数据里分析出来的。这里对应两项能力,SQL 和 Excel。 自动化:把越来越多重复性的工作做成自动化,这像是驯服计算机的一种手段。 敬畏代码:很多时候容易过于自信,感觉做了一个东西肯定没问题,对繁杂的检查有些不屑一顾。但程序没 bug 才是不正常的,要代码起码的尊重,就是细心检查,严格验证测试 Case。 OKR 今年 OKR 完成情况 O1:个人成长KR 完成进度 总结时间规划能力再提升,完整记录20天以上的时间开销 50% 这个目标本意是为了提高时间利用率,期间有按小时的维度记录一天,持续有两周,但发现记录本身并没有产生期望的效果。阅读20本书,选择其中5本写出读后感 80% 20 本达到了,读后感只有两篇,既然上面也写了部分读后感,就算完成80%了吧😄。全年跑步里程400公里 34% 咕咚记录的全年里程是136km,这个差的有些多,我反思,是我太懒了。研究3只头部基金,自己做一次有计划的尝试,最终收益能高于市场平均线 0% 没有研究,收益为负。我承认理财对我来说确实没有吸引力,之后不能在做计划了( ̄ε(# ̄)提升代码阅读量,阅读3个苹果底层库,并写总结分享 10% 又打脸了提升代码书写量,非工作内项目达到20万行。有一个长期维护的开源库,对2-3个经典计算机问题,手写代码实现 10% 连续打脸代码阅读和手写是重要非紧急的事情,我反思问题出在没有以终为始。当时心满意足的列完目标就完事了,后续没有及时追踪目标,导致都忘了有这回事。 O2:输出更多优质内容KR 完成进度 总结公众号粉丝达到5000 60% 当时多少有些膨胀了公众号收入能抵消博客服务器及域名的支出 100% 今年接了几单推广,不光是覆盖住了服务器的支出,我们还买了一些资料输出30+篇博客 167% 有摸鱼周报加持,轻松达到了摸鱼周报出15+期 253% 有队友加持,轻松超过了这部分都完成比较好,给自己鼓掌👏🏻。 2022年计划 O1:技术成长KR 总结LeetCode 100题 在算法上栽过跟头了,不能再栽了阅读 1 本英文技术书籍 算法和英语也多少受霜神影响吧,不过这本来也是程序员非常重要的两项技能,没啥说的,干吧非工作内项目代码量达到 5 万行 未完成目标继续O2:个人成长KR 总结阅读 20 本书 不求多,能安静看完有收获就够了前年跑步里程 300km 300km合理一些,再不跑就变成胖子了学会一项新技能 已经确定学什么并找了一位非常厉害的老师了,先不说是啥面基 10 位技术圈的朋友 克服下自己的社恐多出来走走,同时向各位朋友学习不再设置输出内容的目标,2022 的主题就是新一轮的成长,整装待发,加油ヾ(◍°∇°◍)ノ゙
人物访谈 | 老司机周报主编 Parsifal 专访
- 20 Dec, 2021
Parsifal 是老司机周报的主编,今年他给摸鱼周报提供了很多帮助和建议,这里对他再次表达一下感谢。这次邀请他来做一期访谈,主题分为两部分,一部分是老司机周报的发展故事和他的行业观察,另一部分是他对开发者的一些建议。如果大家还有其他问题欢迎留言提问~ 简单介绍下自己吧 哈喽,各位摸鱼周报的读者好,我是 Parsifal,是一位 iOS 开发多年的老司机了。17 年底开始加入「老司机技术周报」编辑组,到 18 年底正式接手至今,主要负责其运营工作(其实就是给我们编辑组的大佬们打打杂)。二次元一点说,我算目前的二代目,当然我们也很快会迎来三代目,感谢大家一直以来的支持~ 当初创办老司机周报的目的是什么,能讲一些当时的背景吗?现在回想来看,对比当时的目标,达成度怎么样? 老司机技术周报自 17 年 12 月创刊以来,已经更新了整整 4 年时间。在我的认知里,4 年对于一个社区组织的生命周期来说,是很长很长了,长到足够让一个小萌新成长为优秀的成熟开发者,甚至长到我们读者群可以完成一轮新陈代谢。 现在我们的很多读者,谈到周报编辑组,可能更熟悉的是负责我们社群运营的老王「Damon」,或者时不时会在周报中插播公告的「Parsifal」,但如果要谈到周报创办的往事,就需要请出我们的创始人 - 「没故事的卓同学」。作为整个编辑组的一代目,老卓一直被我们认为是编辑组的灵魂人物,即使他 18 年底已从编辑组“退休”,但他创办周报的初衷我们还是延续了下来。 在经过 14、15 年的野蛮发展后,17 年我觉得是 iOS 开发的一个巅峰尾巴。那会儿很多优秀的开发者依然在社区内活跃着,比如老卓就是其中一位。他是非常擅长和乐于分享的,在周报创办之前也深受大家喜爱。在社区内容输出还很爆炸又良莠不齐的当时,老卓就有了想法要将自己平常看到的优质内容,定期整理出来分享给大家。当然这种事情是人多力量大的,所以老卓又和几个好友聊了下,大家都觉得很有意义,就水到渠成地组成了编辑组的第一批元老 Damon\MM\味精\EF\四娘和 BlackSun 等。随着这几年的不断成长和纳新,周报编辑组已经有 30 多人了,但我们的初心依然没变 - 为读者输送有价值的内容。 这么多年的积累,让我们有了一个非常高质的资源库,我个人是非常喜欢在周报仓库检索内容的;WWDC 内参活动越办越好,质量和数量都有了很大提高;今年编辑组也尝试了线下沙龙的方式,用周报的影响力帮助企业对外输送高质内容;我们一直以来做的事情,也逐渐被官方关注和认可。 现在回想来看,老卓的目标在他“退休”那天就已经实现了,周报每期都能够顺利发布,18 年的时候周报已经有了相当的影响力,当时主要的两个更新渠道 GitHub 和掘金都收获了较大的关注群体。而于我个人而言,18 年底从老卓手里接过来运营,算超预期了吧。按我原计划,接手后我负责 18 年的收尾工作,让周报能够有个相对完整的 ending。 但后面做着做着发现,编辑组的朋友依然还有着热情继续做这个事儿,Damon 和邦本等人也愿意加入平时的运营,并且得益于老卓之前的管理原则:周报的编辑周期不强依赖于某个人,我们还是坚持了下来。 18 至 20 上半年左右,周报的发布其实也是轮值的,而后由于调整了发布渠道和时间,并且开始重视公众号运营,才转为主要由我负责发布,我有事的情况下再找其他人代班。然后就是整个编辑组还是得有个人做下统筹,负责一些人员流转,对外合作,以及内参啊,沙龙啊等等其他的活动。轮值没有具体的多长时间,看上一任自己意愿吧。接着编辑组不断吸收优秀的开发者,团队越来越大,新老编辑交替也在很自然地进行。 我的周报编辑生涯可能也很快会结束,但老司机技术周报应该还没到停刊的时候。关于这些,接下来我们一二三代目会一起在老卓播客细聊。大家可以开始期待周报三代目了~ 到目前为止老司机周报已经出了180多期了,分享和传播了非常多优秀的资源和内容,某种程度上也推进了整个 iOS 行业的发展。作为活跃在这一领域最前沿的一群人,在这4年期间里你感受到的变化是什么? 作为周报这类聚合资讯性质的内容创造者,我们可以说是最了解社区的那批人了。我们的内容源自于社区,所以对社区发展方向和热度是十分敏感的。这几年,从行业上看,如我上面所说的,17 年算是一个尾巴,而 18 年至今整体趋势还是相对稳定去泡沫化。不过这其实就是一个行业正常的生命周期,萌芽到野蛮生长,稀缺到泡沫化,然后会到稳定的成熟期,最终到衰退期。 社区产出的内容,也伴随着行业的稳定,没有了之前百花齐放资讯爆炸的光景。整体感觉就是新的东西越来越少,老的东西还在一遍又一遍地被新人挖出来继续写。而我们的读者群里,0-2 年新人也几乎成了凤毛麟角,早两年虽然也少(我们的内容更偏中高级开发者),但不至于看不太到。 整体社区的内容产出来看,17 还是 OC 内容占了大部分,然后是慢慢国内外出现了分歧,国外 18 年开始 Swift 的内容占比越来越大,这几年几乎都见不到国外有 OC 方面的产出了。18 年开始至今的国内,则对跨端(ReactNative\Flutter\Weex)方案爆发了热情,至于 Swift 依然没看到大范围的使用。 创办老司机周报的过程,都遇到过哪些困难?未来老司机的走向是什么样的,有没有什么规划呢? 17 年底起步到 18 年上半年,都算比较顺利,刚开始大家的热情也比较高,也比较有精力做这些事情,同时那会儿社区的优质内容也多,也有很多新的东西被不断挖掘出来。而后我们就遇到了第一个比较大的困难,每周被推荐的内容不够,从而需要主编来花费更多时间去收集。 未来的走向我现在不太好说,但目前的形势是很好的,今年做的几个尝试反响也很好,我们会争取更大的影响力,并继续尝试与苹果做一些进一步的合作。当然,更具体的未来规划,就等我们下一任主编官宣后再聊了。随着移动互联网红利的消失,对 iOS 开发的要求可能不再仅限于能做出一个 App。如果需要进一步提升,学习的广度和深度,哪一个帮助可能更大? 其实我不太认为说以深度为发展方向的开发者和以广度为方向的开发者存在明显的孰优孰劣。为什么会有这两种特征明显的开发者?还是市场需求决定的。这几年大厂产品越来越成熟,那么自然对专精人才的需求就随之增大,比如各类的极致性能优化、基建建设和效能提高等,团队发展到一定阶段,遇到瓶颈了这些需求就出现了;但同样的,全面手,比如大前端全栈开发者,依然是很多企业喜欢的。既然路有很多条,那么主要还是看自己适合和擅长哪条了。 选择了哪条路,如何提升?我觉得理好知识图谱,按点攻破即可,方法论不难,关键还在于执行力。前几年看过成甲的一本讲关于如何学习的书,如果还没有形成一套自己学习技巧的朋友,建议快速过一遍,取其精华。 面对 Flutter、Uni-App 等跨端技术的崛起,对iOS行业未来的发展你怎么看?对当前的iOS从业者有什么想说的? iOS 环境这几年确实被吐槽得厉害,事实上我也有认识的同事辞职回家种花当老板。未来几年,iPhone 还会是销量前几的产品,苹果还会是那个科技巨头。iOS 开发者接下来会相对平稳了吧,直到下一个颠覆性产品到来。别把自己局限于 iOS 开发者,我们是做最接近用户端这一侧的开发者,现在是 iPhone,Apple Watch,以后还可能是 Apple Glasses。元宇宙革命正在进行~ 开发人员除了掌握本职的技术知识外,现在也越来越多提到「软技能」这个词,在你看来有哪些软技能是在职业发展中比较重要的。 提到软技能,应该有不少人都或多或少看过或听说过《软技能:代码之外的生存之道》一书。这本书也有提到「十步学习法」,同样适用上面那个问题。当然,这本书讲的方面很广,从社交、理财到健身什么的,作者都分享了自己的经历和想法。我这里基于自己的一些职场经验,简略谈谈职场开发者可能需要的一些软技能吧。 需要掌握优秀的检索能力,检索能力往往会被很多人忽视,但在工作中却很重要。虽然我们常常调侃面向搜索引擎编程,可这就是绝大多数人的编码常态。好好提高检索能力,会让你效率大增,并且学习起来也比别人更快一步。检索第一步是去哪里搜?Google 作为程序员第一搜索引擎,自然不是个坏去处,但某些场景,可能有更好的地方。比如,我现在想看某些方面的文章,我更优先检索的就是周报仓库。知道了去哪里搜之后,怎么组织关键词去搜就直接决定了你能搜到什么了。编码相关的问题,我更推荐是直接按英文关键词拼接空格来搜,这样搜到的东西往往更准确,而且资源也会更多。最后就是一些相对进阶点的搜索技巧,引号精确匹配,默认模糊匹配等等。 老生常谈的提问的能力,作为一个某领域的萌新怎么去请教别人也很值得重视。一个好的提问可以帮你更快得到答案,也省了对方很多时间。以寻求帮助去提问的场景下,比如问你的导师,或者平常的一些技术群求教,一个好的提问大体是需要包含这几点内容:谦虚的态度,细致的问题背景,目前你的进度和结论,你期望得到的帮助等。 完成事情的能力,有些人叫「拿结果」的能力。在日常团队管理中,我是比较强调办事能力的,这一个能体现综合水平的点。有些开发者编码能力很强,但他并不一定能把事情做好。在团队内把事情做好,需要足够的责任心,良好的沟通协作能力,过硬的业务水平,有时候还需要有很强的韧性。 开发的职业发展过程中大多数都会经历瓶颈期,你有没有经历过类似的阶段,后来是如何突破的?对于处于这种阶段的同学,你有什么好的建议吗? 可能是自己没有很高的追求,我的职业发展相对比较顺利。所以给不了很有价值的建议。但如果说我自己真的遇到了,我估计会索性放空自己一阵子,任性放纵自由一段时间,都放松下来后,再来思考下一个阶段需要怎么走。生活压力很大,并不是每个人都有这样的机会和勇气这么处理,总之挺过去就好。 人生就是无数习惯的总和。在你工作中遇到过哪些开发人员好的和不好的习惯,能给我们分享一下吗?能再分享一个你自身具备且感觉对自己帮助较大的好习惯吗? 有些小伙伴比较好的习惯是不定期的去归纳总结,将所做过的事情,解决好的问题,新掌握的知识,都去做一个阶段性的总结。为自己的总结再设立几个问题,通过这几个问题去判断总结的效果是否理想。 不好的习惯,有些小伙伴遇事先条件反射性地自我否定算一个吧。碰到一个困难,第一反应就先觉得这个不行,那个不可能,这不是我的问题等等,本能地去推脱,久而久之也就丧失了正确评估一个事情的能力。或许是出于自我保护意识,察觉到威胁先跑路,但这确实是很不好。于己,这样会让自己的上限不断被压缩,仅维持在一个相对安全的小舒适区;于人,不断地被否定,也会影响对你的观感。 我一直认为开发人员不应该扔掉代码,尤其是中低层管理,这很容易会让自己失去竞争力。平常写写代码,保持编码逻辑的触觉,也能让自己跟团队其他伙伴更能走到一块。 另外对于普通开发者也好,技术管理层也好,定期去关注业界动态和社区技术生态发展,也是很重要的。每周看老司机技术周报就是一个很好的途径,哈哈哈~ 很多开发都知道学习的重要性,但又常常陷到工作中无法抽身学习,就学习和工作的平衡性应该如何维持呢? 对于很多人来说,这可能是一个普遍的问题。这里聊下我自己的看法吧。首先还是需要从工作中去学习,毕竟这占据了我们大部分精力,去和自己的直属 Leader 多沟通想法,将一部分想学习的能力与自己的工作内容结合起来,落到工作中去实践。 然后是必须注重学习效率,一个阶段做一个事情,贪多嚼不烂;最后,虽然我很不鼓励内卷,平常自己也不 996,但很多人拉开差距真的就是在工作之外的时间。去培养自己的兴趣,将能够给自己带来乐趣的学习内容安排在非工作时间。 讲一个最近的生活感悟吧? 杭州疫情在这几天又复发了,且有迅速扩张的态势。注意防护,没事少出门,听从指挥,少给别人添乱~
人物访谈 | 一位普通的 iOS 程序员
- 13 Dec, 2021
夏天是摸鱼周报的一位主编,主要负责 Tips 部分的编写和审核。他曾参与掘金翻译计划,翻译过很多篇优质博客内容,也翻译过像是 iOS Crash Dump Analysis Book 这一类的外文图书。今年拉他来聊一聊关于外文翻译和他个人的一些情况。 简单介绍下自己吧 哈喽,各位摸鱼周报的读者好,我是夏天,是一位双非的老 iOS 程序员。目前在摸鱼周报里面摸鱼。 一般优秀的英文文章,不会马上就有人翻译,所以,如果想获取一手英文资讯,有什么有趣或者必看的外国网站推荐吗 一般都是通过Medium,Realm 之类的网站,然后注册一下邮箱,接收推送,没事翻两篇。 看到好文章时,看看他们的参照了谁,参照作者可能也有好的文章。 之前翻译过不少内容,像是:《iOS Crash Dump Analysis Book》,是出于什么理由进行翻译的呢?翻译这个长篇有什么收获? 《iOS Crash Dump Analysis Book》这本书,我是在 19 年看到的,其实并没有仔细阅读下去。后来想仔细阅读的时候,发现网上并没有译文,我就申请翻译一下这本书,作者也是很慷慨,给予了这个机会,让我完成了 1.0 和 2.0 的翻译。 我觉得整体翻译下来的收获,大概就是形成体系的去了解一些关于崩溃的结构和作者在进行奔溃分析时的分析途径和方法。整本书是一个概论,虽然不能让你成为一个 crash 分析高手,但给了一个格式,在碰到崩溃的时候,可以按部就班的去进行分析。 为此我还写了一篇观后感iOS 崩溃分析。 技术文章翻译的过程中那部分是相对困难的,有没有什么经验可以分享的 第一就是语感,翻译完以后多读几遍,没有 deadline 的翻译,你都可以翻译完以后,过几天再去读,再优化一下,至少读到你自己不变扭,能通顺为止。 第二就是实践,虽然里面很多技术你不能去实践,但是你可以去了解。文章的引申阅读和搜索一下相关技术。我在写 字节跳动移动研发工具链 - MBox 一文的时候,也是尽量保留原作者的本意,去了解对应的技术及其实践,进行再创作。 听说你最近一直在写前端需求,前端和iOS有什么区别?iOS开发想往前端过渡的话,有哪些注意事项? 写的都比较简单,都是偏业务方面的。思想上和逻辑上没有大的差异,唯一需要注意的是,如果你接手别人的项目时,一定要心平气和,因为前端很坑爹的是怎么写都对。 学点CSS,学点JS,就可以开始过渡了,期间体系化的学习一下。 期待你在一家好的公司,什么东西都有,然后你顺着经验就可以开始夯实之路。 利用公司的项目锻炼自己,可以根据现有的模板,从零开始,从第一个组件,到第一个业务模块等等。 对于“大前端”这个词,你怎么看?随着跨平台、Web WebAssembly等技术发展,会不会统一成一个工种? 我理解是为用户体验负责,各种与用户有直接接触的都算是大前端,不过这可能更偏向于程序员这一职业。 前端程序员几乎都做着类似的事情,差不多的开发流程,页面埋点,发布流程,项目持续集成,页面性能监控 APM ,工程化,组件化... 技术永远都有差别,都是往前也不一定是同向。 也许未来这些为页面服务的人员可能会慢慢的变成一类,但是其中的某些人难以忍受这种越来越同化的工作又会开始异化出新的种类。 技术不停步,工种就永远有差异。 在这几年的iOS生涯里有什么比较大的收获?你怎么看待程序员这个行业? 没看到有啥收获,连个只能被铭记的功能都没有。 程序员这个行业,是一个注重基础和思考的行业,用数据结构和设计模式规范具体的行为,注重实践动手能力。 这个行业需要你一直学习,终身学习。 不应过分功利,需要一定的职业操守和道德。 自己在工作中有没有什么好习惯或者高效的工作方法可以分享的 解决问题的方式不要困于一种途径,交流能让你成为更好的人,多出去分享去交流,可以使人进步。 学到什么一定要做出来, 写下来,讲出去。 最近的一个生活感悟是什么 努力,生活总有好事发生。准备迎接好事发生。 偶尔咸鱼也没关系,躺着是很舒服,但是还是要动一动的,不然真的会腰酸背痛 你的公众号感觉最近更新少了,后续还会恢复吗? 不更新的原因有很多:创作热情:在更新一段时间后,读者的增长量没有达到自己的预期,心理有些懈怠创作质量:期望与创造出真正有价值的东西,但是囿于能力等因素,既不想水,也写不出好文章创作方向:平常的工作更多是业务相关的,作为一名老 iOSer 并没有自己固定擅长的地方,想像书籍一样写点东西,但是不太清楚具体些什么。...最最主要的原因就是懒后续还会恢复更新的。目前在进行 Swift 的学习,之前也开了一个Swift 100 Days的专题,还在想怎么在里面添砖加瓦,丰富成一个系列 有什么想借助摸鱼周报宣传的 希望大家支持我的公众号:iOS成长指北,也希望大家与我交流。 承接各种技术类文章写作,一起交流学习。
利用 Automator 快速符号化 Crash 文件
背景 起因是最近有接到一个临时协助任务,其中有几个重要的流程:QA 方导出 .crash 文件(必要的) 我方要根据测试提供的 crash 文件的build number,去下载对应的 xx.app.dSYM 把下载的dSYM给合作方 合作方解析crash文件从上的步骤可以看出第一步不可省略。第二、三步完全可以干掉,流程越多越浪费时间。 第四步也可以我们自己做,就可以优化成 QA 直接解析好 crash 文件然后给合作方。 那么就提效了提效 50% 是不是,两个人的事情一个人搞定 (那么就可以卷点别的)初版方案 小插曲一开始第一周我写了个Shell,调试通过之后就没继续,就干其他大活了(这里有个有悲剧) ...... 第二周的时候,不知怎的崩溃出奇的多(应该是合作方更新SDK之后导致的) 当时我正Coding热火朝天,QA和合作方夺命的Call 我就去找那个当时写好的shell脚本,一通翻箱倒柜之后,我悟了,悲剧来了,找不到了 呵呵,被自己强迫症日常清理垃行为给清理了(自己有个日常清理的垃圾的行为,无奈Mac配置就这样)打工人不得不含着泪重新写了一份 (源码在下面),快速应付下那边的夺命Call 然后我就在想这个事,为啥要我来做,也没啥技术含量,为啥不可自动化? Bingo~ 说来就来Shell 源码 crash_txt=$1 crash_log=${crash_txt%%.*}.log # find /Applications/Xcode.app -name symbolicatecrash -type f # cp /Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash symbolicatecrash export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer ./symbolicatecrash $1 $2 > $crash_log open $crash_log -a sublime具体如何符号化解析这里就不再唠叨了,网上一大堆:附一个参考链接 使用 Automator 的自动化方案 要使用 Automator 还需要编写 AppleScript 代码。 工具 & 语言工具:Automator Service, 脚本编辑器 语言:AppleScript,Shell脚本编辑器,AppleScript调试用。 好玩的 AppleScript 下面是一些好玩的 AppleScript 代码,唤起你的好奇心: display dialog "你说假如地球没有了空气,我们会怎样... 那么没有工程目录,后面该怎么办?" default answer "会死" buttons {"我知道了"} ¬ default button "我知道了" with title "Handsome ERROR" set theInput to text returned of the result --display dialog text returned of the result if theInput is equal to "会死" then display dialog "没救了" with title "ERROR" buttons {"我知道了"} ¬ default button "我知道了" end if --忽略下面部分say "Hello world"display dialog "Hello World" with title "Alert"display notification "Hello World" with title "Notification"或者直接在终端里面跑 osascript -e "display notification \"Hello World\" with title \"Notification\""-- single comment, # single comment 是单行注释 (* this mutli comment *) 是多行注释 Markdown问题AppleScript脚本里意外出现<p data-line这种代码忽略AppleScript 需要注意的问题 主要还是路径问题 ApeleScript 获取的路径如下: Macintosh HD:Users:xxxxxxx:Documents:xxxxx.app_副本_2.dSYM:这种冒号的路径在shell命令行根本没法用,所以下面代码成了常客: 冒号字符串 打包成数组 set my_array to split(input as string, ":") on split(the_string, the_delimiter) set old_delimiters to AppleScript's text item delimiters set AppleScript's text item delimiters to the_delimiter set the_array to every text item of the_string set AppleScript's text item delimiters to old_delimiters return the_array end split字符串 set target_path to join(my_array, "/"),这里要注意拼接文件与文件夹用的index下标不同: on join(the_array, the_delimiter) set split_str to the_delimiter set target to " " set list_length to the length of the_array set list_length to list_length - 1 set short_list to items 2 through list_length of the_array repeat with dir in short_list set target to target & split_str & dir end repeat return target end join实现过程 思路分析 1、定位dSYM路径 2、定位xx.crash件路径 3、唤起终端,切入指定路径 4、symbolicatecrash解析并重定向输入结果 5、自动打开展示结果 其实这前两步有个大坑:重复下载 dSYM 文件以及导出的 xxx.crash 文件路径会存在空格。在AppleScript调用Shell的时候路径有空格,会报错找不到对应的文件。 解决办法利用 AppleScript 给文件重命名 借助Automator 现有的快捷操作修改期间有周报群群主指点使用 AppleScript 借助 quoted 这个 API 来转义引用空格。 结果是终端识别了,但是symbolicatecrash还是不识别,虽然结果不尽人意,但是学到了新技能。 如果你看过AppleScript API,除了想哭就没别的,上面说的很清楚干啥用,但是不知道语法该咋写。因为没写过这种自然语法,每次都是不停的尝试、失败,尝试、失败,尝试、失败。 AppleScript小众到谷歌都没有,大部分都是查阅Stack Overflow。 我这边选择是第二种 xxx.crash 文件名有空格的解决办法是直接重命名,查找之后直接把空格替换成下划线。dSYM 父目录路径空格,这边多次导出之后会导致父目录存在空格,这个相对上面就比较复杂。 这里有几点思考:在事物本身很难解决问题时,我们就需要放开视野,跳出事物本身,提升更高的角度去思考 当你这么想了,你思考问题的维度和角度就变了 在我们这个问题上,既然它的路径上存在空格,我给它换个不存在的路径不就好了 是不是一个很简单的解决办法,所以有时候不要太局限一点一面一点瞎扯淡 其实日常编码或者修复 BUG 的过程中也会遇到类似情况,我们在一个问题上纠结好久好久到快死了吧!但是问题还没能解决,这个时候就可以尝试:冷静下来 刻意放慢节奏 全身心放松下来 想点别的换换脑子或者睡一觉(我通常就是睡觉) 冥想(这个相对高级 需要练习)不去想这个问题一段时间之后,慢慢就会发现脑子开始活络起来,之前的问题解决办法好像一下子思路如泉涌,睡一觉精神也恢复了,思路也有了,简直两全其美是不是,比死磕一天啥都没有强千百倍吧,最后还得被喷延误工期,拉胯身体,最后无奈身不由己加入996.icu 这个 Big Party。 工程创建 1、选中dSYM文件 -> 右键 -> 服务 -> 创建服务 2、弹出一个快捷操作的模板空工程,可以配置参数入口(因为第一步选中了,参数就不需要配置了) 3、然后就可以拖拽你要的操作(类似于storyboard,xib操作) 4、保存 -> 命名,就会自动存储到本机的~/Libray/Services目录 所有的快捷操作,工作流都会在这个目录,就是说你想用别人写好的最后安装的也是这个目录 示例图:完整的操作步骤脚本交互Shell 调用 AppleScript可以用osascript -e AppleScript调用Shell可以用do shell script & do script do script需要配合终端示例: tell application "Terminal" activate --set new_tab to do script "echo fire" delay 1 do script "pwd" in front window do script "ls" in front window end tell演示模糊了点,为了加载快,压缩的有点狠,但是也能看大概流程就OK了 有两种使用方式启动 dSYM 自动化服务:首先选中 dSYM 文件,然后右键 -> 快捷操作 -> dSYM 首先选中 dSYM文件,快捷键即可(这里需要到 Finder -> Service 偏好设置里面配置好按键)执行流程如下: 1、启动之后就自动去/Users/$(whoami)/Downloads/目录文件下搜索.carsh文件 这里写死Downloads目录的原因是想提高搜索速度,所有导出的时候选择的就是Downloads目录。如果你想要全局搜索也不是不是可以, 但是你得等等Spotlight 2、搜索完毕之后会列出该目录下所有的 .crash 文件。 3、选择对应的文件(build number 一致),就会打开一个终端进入解析流程。 4、解析完毕之后会通过 sublime 打开。没有sublime会怎样? 就去掉 -a sublime ApplesScript 代码负责的部分:冒号:转斜杠/ 调用了剪切板做缓存 display dialog 显示.crash文件的搜索结果 唤起终端,执行解析总结 这里本来想在Automator里面加一个调用Shell脚本的的服务,这样就可以静默解析不用唤起终端,调试过程中解析一直失败,因为运行解析的 symbolicatecrash 需要的环境变量报错,也在对应的目录进行了export,但是最终还是不行,最后还是选择唤起终端来执行操作,或许看起来更酷一点吧 哈哈哈。 其他文件读取/拷贝/搜索/重命名都是Automator提供现成服务。Automator真的很强大,但是你要发现它的美,学会使用它。 最后就是要告诫自己:该做的事还得及时做出来, 不然就是午饭没吃 午休没睡。
人物访谈 | 一位 iOS 程序媛
- 29 Nov, 2021
Jojo 是我的前同事,做事非常细心,负责过团队多项重要功能的开发与建设,是我接触过为数不多的比较优秀的程序媛之一。这次邀请到她,来聊一聊她眼中的 iOS 开发。 简单做个自我介绍吧 大家好,我是 Jojo ,90后,一直从事 iOS 开发的一枚湖南妹子,喜欢交朋友,容易与人相处。现在是一名娃 3 岁➕的宝妈,如果有北京的宝妈,一起遛娃呀,哈哈哈~ 当初学计算机专业出于什么考虑?后来为什么选择了 iOS 开发? 从小农村长大,初中才开始接触电脑,感觉计算机特别神奇,出于好奇,就走了编程这条路。当时 iOS 比较火热,苹果手机高端,能在高端手机上开发 App,会是一件愉快的事情。目前程序员群体的男女比例大致在 9:1,女生的比例还是比较小的。作为女生,你怎么看待这种状况,有质疑自己选择做程序员这件事吗? 我工作时遇到开发的女生还是挺多的,也许现在比例较低,个人认为以后会越来越多。目前为止,我认为自己走上编程这条路没错,也没后悔过。自己是比较简单的人,开发圈相对来讲也比较简单,挺喜欢的。 你目前的工作状态是什么样的,有没有困惑或迷惘? 目前工作状态还不错,知道自己想要什么,需要哪方面充电,也正在往自己希望的方向走,工作三年左右那会有过迷惘,感觉大部分东西能实现,工作中需求也能完成,遇到了程序生涯第一个天花板,却不知道怎么突破,那会感觉这辈子只能这样了。 后来遇到了一群可爱又优秀的人,也不排除年纪增长带来的觉悟,能发现自己的不足,目标越来越清晰,了解目标应具备什么能力,寻求突破。不知道大家有没有过相同感受,我曾经不太喜欢看书,逼着自己都看不下去那种,近期却非常想看书,发现认真读完一本书真的可以学到不少东西。 最近在看《非暴力沟通》,读这本书一开始是因为家里有个调皮小孩,天天跟我唱反调,心累得很。收获,让我懂得分析矛盾产生原因,以及如何去解决矛盾,书中会提供几种方法,总而言之,冲突产生时,学会倾听内心的声音,然后站在对方角度去感受,这样很大可能能够建立积极关系,收到良好反馈。不仅育儿有用,工作中同样受益匪浅。如果大家有看过的好书,感谢推荐给我看看。如果有跟我一样困惑的人,还没遇见影响着自己变的更好的人,那就多看些好的有意义的书,说不定能找到点思路或者多交交朋友。遇见优秀的人多了,自己也会变得更加优秀。 作为程序媛,有没有什么与程序猿不同的工作方式?也可以介绍一下自己的高效工作法。 个人感觉程序媛会更细心,可能这是女性特质。 仅代表个人的一个有效的工作方式是,习惯写 ToDo List 。按照重要和紧急两个维度把待办事项列出来,然后按照紧急重要排优先级一个个完成,这样做会有这些好处: 1、做事不会遗漏。工作中随时都有可能被一些事情打断,有插入事项时,能及时记录进去,我比较健忘,所以对我帮助还挺大的。 2、有助于回顾。写周报时看 To Do List,秒完成。 3、建立目标感。有目标的每天,时间过得快而充实。 关于“程序员是青春饭、35岁失业”等,作为程序媛,你怎么看这个问题? 青春饭 35 岁就失业这个我以前也有过这样的想法,现在则有不同的看法。淘汰的永远是年纪跟能力不匹配的人,所以与其为未来焦虑,不如为现在努力,让自己保持进步和成长。 你觉得工作中最重要的一项技能是什么? 自学能力。曾经就职的公司,开发能用 Swift 的得用 Swift 开发,而我在这之前一直只用 OC ,如果自学能力不够,很难跟得上。不知道大家有没有这样一种错觉,干过编程的,再去干别的,只要给足够多时间,没有学不会的自信😁,我认为这是自学能力带来的信心。 你现在还要带娃,个人时间应该更少了吧,你是如何平衡工作和学习的呢? 相对来讲会减少一点,但也不是没有个人时间,以下方面起到不少作用: 1、提高效率,像上面记录 ToDo List 的方式,自己认为是提高效率方法之一。 2、寻找合作伙伴,平时我婆婆在这边帮我,我老公也会一起带娃。 3、娃上幼儿园后,大部分时间都在学校度过了,目前工作学习带娃没有失衡。 不过追剧逛街的时间确实没以前多了,要买啥,记录下来,申请一下逛次狠的😁。平常还会再抽出一些时间看看书,学习一下。 让你坚持做程序员的动力或目标是什么?有没有什么心得或经验分享给其它程序媛? 在任何行业做好一件事都是有难度的,感觉计算机行业自己也挺喜欢,好像没有理由不继续下去。 对所有女程序员说:如果不讨厌编程,Just do IT。 有没有什么想借助于摸鱼周报进行宣传的? 大家如果对百度感兴趣,可以随时把简历发我邮箱:jojocaonet@163.com,找我内推。马上过年了,买不买新衣回家过年就靠大家了,哈哈哈。 大家如果不嫌弃认识我的话,可通过飞哥加我微信,也可以通过邮件跟我交流哦。
人物访谈 | 一位游戏行业的 iOSer
- 22 Nov, 2021
iHTCboy 是摸鱼周报里负责校对的成员之一。记得他第一次参与校验时,我把一期整理好的周报发给他看,他后来给我发了 1600 字的修改意见,我都震惊了。修改意见里即有关于技术点的,也有格式和语言表述的,关键读过之后,感觉他说的还非常有道理,由此可见他做事认真的态度。下面是访谈内容。 简单介绍下自己吧 大家好,我是 iHTCboy,从事游戏 iOS SDK 开发快五年了。目前在 37手游,以 SDK 开发为主栈,自动化技术实践为副栈。目前我们海外版 SDK 中除了仅剩 1% hook 的 OC 代码,全部是 Swift 代码;在游戏行业有多年经验积累沉淀后,我们团队希望打造和分享一些 SDK 相关的自动化方案或框架,未来会逐渐开源,敬请期待,跟大家一起学习交流&进步! 当初为什么选择计算机这个行业呢? 应该是小时候对黑客的幻想,《电脑报》90后应该都有看过吧?而《黑客防线》、《黑客x档案》、《黑客手册》这些杂志,不知道大家有没有看过,初中时很着迷,初中时用破解软件逃过网吧收费系统,高中帮亲戚修电脑重装系统,有非常大的好奇心和成就感。在对计算机编程有全面了解前,总是幻想自己当上黑客后xxx,神秘又强大是每个男孩都有过的梦想吧!所以不加思索的就选择了计算机。 游戏公司的 SDK 开发日常是什么样的,它跟应用开发有什么区别呢? 每一位不熟悉游戏 SDK 开发的朋友都会问这个问题。应用开发大家可能比较熟悉,我们在开发中,也会引用第三方的 SDK,比如友盟 SDK、微信 SDK,这些 SDK 提供推送、分享或者支付的能力。而游戏 SDK 开发也是这个道理,为游戏提供特定平台的原生 API 功能。举例来说,目前主流游戏都是 Unity 引擎开发,游戏开发好后,可以导出 Android 项目或者 iOS 项目(Xcode 项目),然后在接入原生平台的 API。 游戏发展到今天,已经高度标准化..... 游戏研发专注于游戏内容的设计和开发(跨平台),而 iOS 和安卓平台原生的能力(如支付 IAP)就由我们封装成 SDK,提供接口给游戏调用。 1、SDK 开发包括:帐号体系(包括封装第三方账号体系:WeChat、Facebook、Google、Line、Twitter 等) 支付体系 分享社交 Web页面(游戏活动、客服、攻略等) 数据埋点(广告、留存、数据分析等) 性能监控 ...将以上功能模块封装成 API 给游戏调用,一般就是登录、用户中心(有界面),其它都是业务逻辑处理。 2、SDK 是提供给游戏使用,所以不像 App 快速迭代 UI,而是迭代业务逻辑。详细的开发细节,以后有时间专门写篇文章吧。关注游戏行业的发展,可以参考之前写的文章:游戏出海本地化概述 。 看你发多篇调研苹果审核和内购相关的文章,针对最近苹果在美国败诉,撬开苹果 IAP 的限制,你怎么看?这个改动可能带来的影响有哪些? 我们先来梳理一下,苹果 AppStore 垄断案的几个新闻关键点:时间 概述 引用来源2021-08-26 Apple 与美国开发者就 App Store 更新达成一致,开发者可以使用电子邮件等通信方式与用户共享 iOS App 之外的支付方式信息。 来源2021-08-31 韩国国会立法和司法委员会通过《电信业务法》修正案,禁止占据市场主导地位的应用商店强迫移动内容提供者使用特定支付方式。 来源2021-09-01 日本公平贸易委员会结束对 App Store 的调查,允许“阅读器”类 App 的开发者在 App 内提供访问其网站的链接,开发者可将用户引导向外部网站进行购买。(阅读器 App:提供数字杂志、报纸、书籍、音频、音乐和视频的预付费内容或内容订阅。) 来源2021-09-10 Epic诉苹果垄断案裁决:美国联邦法院命令苹果不得禁止应用发商引导用户通过第三方支付平台付费。法院并不认为苹果公司是手游交易市场垄断者,但认为苹果公司禁止将用户导向其他渠道是反竞争行为。要求苹果必须在12月9日之前执行。 来源1、来源22021-11-10 Yvonne Gonzalez Rogers 法官拒绝了苹果公司关于推迟执行永久禁令的请求,苹果打算根据这些情况要求第九巡回法院暂缓执行。 来源关于 IAP 内购问题,随着 AppStore 体量越发庞大,大多数开发者认为 30% 税不再合理,其实很多事本身就没有公平合理可言,省略一万字~ 你懂的。 大家可能忽视了一个重要的问题:苹果 IAP 收入占比最大的是什么类型的 App?答案是游戏!所以,对非游戏类 App 允许使用 IAP ,目前对于苹果的损失还不是很大,当然,如果没有损失是最好,但是目前的大环境和反垄断,一定会让苹果做出调整。(注:游戏一直以来都是 App Store 中的重头戏,从 2017 年 iOS 11 开始,苹果在 App Store 增加了 Games 游戏标签入口。)对于苹果败诉带来的影响: (1)苹果在美国一定会上诉,一般垄断案可能长达几年时间,所以苹果愿意继续打官司,尽量拖延时间(参考微软 IE 浏览器反垄断案,从1997年到2002年)。 (2)苹果降低 IAP 税率或允许所有 App 使用第三方支付是时间问题,但没有了 IAP,苹果可能会推出其它的服务费。就在 11 月 19 号,谷歌宣布韩国地区的 Google Play 商品允许应用接入第三方支付系统。谷歌目前每年前100百美元抽15%,超过后30%。按谷歌的新规,韩国地区接入第三方支付系统,谷歌依然从中收取11%(26%),也就是说抽成只是被降低 4%。 简单来说,就是谷歌允许开发者使用第三方支付系统,但没有说不能抽成啊!抽成和允许第三方支付,不冲突! 为了应对韩国的新法规,Google 已更新 Google Play 付款政策 。(3)对于开发者来说,肯定是好事。但这件事背后,是大资本之间博弈的结果,一定要明白为什么可以赢,比结果重要。看过你的博客:https://ihtcboy.com/ ,从2008年就开始写了, 这是一个非常长的时间跨度。关于博客部分也有几个问题想问下你。培养一个良好的习惯并持之以恒是一件不那么容易的事情,能讲下你是如何培养写作的兴趣以及对培养一个有益的习惯有什么建议吗? 高中语文课最讨厌的就是写 500 字作文,但没有想到自已大学后会喜欢写作。写作可能是我小时候比较孤僻,很少与人交流,总喜欢自言自语道,也许是这个原因,自己会有很多天马行空的想法或者内心独大。而作文是根据要求写作,限制你话题,没有自由的发挥和空间。 当然喜欢是一回事,写的好不好才最重要!培养写作,我认为要了解写作的注意事项,怎么样的文字修饰有多妙,多模仿多练习。培养习惯,更多是从一个个可行的小目标做起,积累量多了,慢慢就是习惯。切忌急功近利! 写作一定也有通用的规律规则,比如 中文技术文档的写作规范。大家多写写,多总结,一定会有自己的收获。 你写过如何建立自己的开发知识体系的文章,程序员也是一个需要不断学习的行业,关于如何让学习这件事变得有趣,再分享一下你的见解吧? (1)说起来见笑了,建立自己的开发知识体系 只是觉得大家可以往一些通用的方向去梳理。现在回想起来,发现自己也没有什么都懂,可能接触和学习更多知识后,发现自己空白面更多,然后就越想学习。所以,也许再过几年,会有更深的理解,之后再跟大家分享啊。 (2)“不断学习”我认为是21世纪,每个人都要学会的。就像父母要学会用智能手机,否则健康码都打不开,在城市里寸步难行。所以,大家一定要正面接受它,每天都要学习和进步。 (3)“让学习变有趣”,其实我一直认为看书,应该是让人感到愉快的事情。但为什么大家都不愿意学习了呢?我觉得最重要的是,大家越来越急功近利,一切都为更快,从而忘记了体验过程。举个例子,去旅游时,可能为了发一个朋友圈,一直拍照修图,刻意的、绞尽脑汁的想,却没有享受风景的心境。所以学习本身如果慢下来,不只关注结果,还关注或者尝试从问题到结果的推演过程,这样的学习会更有趣一些。 你博客有很多思维导图,思维导图除了能够帮助梳理知识外,还有其他什么好处吗? 大家一定知道一个道理,图片比文字更加形象。思维导图比一段文字也更加生动形象,所以,它不仅能够梳理知识,还能理清知识的层次和架构,帮助记忆知识。举个例子,Vim 快捷键,如果你忘记了,打开思维导图,一看就知道:再比如,说到人工智能,可能想法很多,那么大的框架应该是怎么样的呢? 最近有什么感悟想跟大家分享的吗? 关于内卷的话题,网上有很多讨论,大多数是吐槽或者劝退。我想换一个词,“努力”,为什么会有人很努力?现在大家都达到小康生活后,大多数人会选择躺平,然后自嘲自己是打工人。其实自甘打工的还是少部分人,大部分还是想往上跳一跳当老板的。另一方面在一个阶级中,努力的人,往往是想要跳出这个阶级的。就是这个往上跳的愿望,它需要的正是努力。 所以,努力它本身上没有错,这代表一种向上的态度。但需要注意的是努力的结果,是不是可以为你改变什么?如果可以,我觉得努力无可厚非。否则,就是真内卷。 最后,有一句话送给大家一起共勉:Follow your own course, and let people talk. 走自己的路,让别人说去吧! - 但丁 《神曲》有什么想借助摸鱼周报宣传的? 欢迎大家关注我们掘金公众号:37手游iOS技术运营团队,会定期分享一些有趣的文章。 如果大家有兴趣加入三七互娱,可以查找相关岗位 :https://zhaopin.37.com ,或者将简历发给我内推:ihetiancong@gmail.com。不要担心有没有相关岗位,优秀的人,在哪里都是金子!如果有其他问题想跟 iHTCboy 交流,还可以在留言区评论。
人物访谈 | 一位参加过 WWDC 的 iOSer
- 15 Nov, 2021
Mimosa 是摸鱼周报的编辑之一,负责每期的学习资料整理。在一次聊天过程中他晒出了跟 Tim Cook 的合影,给我们馋哭了。于是就有了这期访谈的主题:WWDC 之旅,也算是拉着 Mimosa 一起回忆了一把,哈哈。因为是在 17 年,WWDC 还是线下,活动真还挺多的,不知道还要多久才开发者大会才能再次回归线下,希望疫情赶紧过去吧。 简单介绍下自己吧 哈咯大家好,我是 Mimosa,中文谐音名棉毛衫,小号是 Mimoku(棉毛裤),是一名 iOS 开发者,工作经验一年左右,是 iOS 摸鱼周报的编辑之一。我是在大学的时候接触的 iOS 开发,当时加入了学校的 iOS Club,在大二那年获得了来自 Apple 的 WWDC Scholarship Winner,从此算是与 iOS 开发结缘,参加了许多与 iOS 相关的活动,毕业到如今一直是从事 iOS 开发。 能讲下自己获得 WWDC 奖学金的经历吗? 首先先介绍一下这个奖学金吧,WWDC Scholarship Winner 是 Apple 每年为全球学生提供的一个小福利,每年 4 月份的时候,Apple 会在官网放出当年 Scholarship 的要求,比如你需要是一个学生,你不能年龄太小等等,你还需要做一个有创意、激动人心的小作品,并提交,全球每年将会有大约 350 名学生的作品会被接收。如果你的作品被接收了,那么将免费获得一张当年 6 月份参加 WWDC 的门票,价值 $1599 😎(且提供住宿、来回差旅费也有机会报销)。在 2020 年之后,这个奖学金改名叫做 Swift Student Challenge 了,且由于疫情这两年的 WWDC 改为线上,就无法去现场参加了。 我获得的是 17 年的奖学金,申请 WWDC 奖学金时我正值大二,当时是刚学完学校的 C 语言课程和数据结构,加入了我们学校的 iOS Club,也是第一次接触 iOS 开发。我还清晰的记得当时是在 Swift 3.0 的环境下用 Swift 2.0 的教学视频在自学😂,周围会 Swift 的人也是屈指可数,只能自己上网摸索。由于当时有学长获得过上一年的 WWDC 奖学金(但由于时间问题没去 WWDC 现场),所以当时的刚学我们也想试一试,就熬了几天用自己学过的所有东西做了一个作品提交了。没成想那年我们学校有 4 位同学获得了奖学金!但最后由于签证问题(我是之前已经有申根和美签了,但我的朋友们没有😢),所以只有我一个人去圣何塞 WWDC 的现场。你当时开发的是什么作品? 那一年是第一次 Apple 要求参赛者制作一个 playground(之前都是要求做一个介绍自己的 App 这种类型的题目),我制作了一个简单又可爱的可视化冒泡排序教程💂♀️,运行在 Swift Playgrounds 这个 App 中(可以在 iPad App Store 和 Mac App Store 中找到这个 App),利用这个 App 一侧可以写代码,一侧可以查看运行结果这种所见即所得的特性,来教使用者冒泡排序算法的原理。参加 WWDC 期间有什么好玩的事情,这个过程有什么收获? 可以说是收获颇丰。首先一个是,作为 Scholarship Winner 可以额外参加一个 WWDC 正式开幕之前的 WWDC Scholarship Meeting,由 Apple 组织在一个小剧场和我们讲了一点 Apple 在各个方面做出的一些努力等等,还会给我们发一个 dev 版本的 Apple TV。重要的是,在所有演讲结束后,Tim Cook 会突然走出来和所有 Scholarship Winner 合影,然后接下来就是激动人心的和 Tim Cook 合影环节,我当时的情况是你只需挤到他旁边他就会和你合照一张!🤩据我观察现场大约有 2/3 的人都在成功合影了。(剩下没合影成功的只能落寞地望着 Tim Cook 和他保镖的背影😇对于大多数开发者来说,可能 WWDC 里面重要的是各种各样的 session,但其实也会有一些无关技术但与生活相关的演讲,给我印象深刻的有两个。 一个是在 WWDC 的第二天,由于我时差没倒过来睡不着,参加的当天最早的一个演讲,是奥巴马夫人米歇尔的一个演讲,(虽说我排队时排在前几个但是仍没坐到前排,气死了),她讲了许多科技与生活的事情,谈论了他的孩子,还讲了去长城的经历,吐槽长城人多等等。另有一个让我印象深刻的演讲是来自 NASA 的一个女数学家奶奶,她谈论了她当年是怎么加入 NASA 以及工作至退休期间的经历,讨论了那个年代女性在 NASA 的情况,讨论了那个年代女科学家的境地,让我大受震撼。 作为 Scholarship Winner 还有一大好处就是,除了能接触到说中国话的学生开发者之外,还可以接触到来自世界各地的 Scholarship Winner,由于所有 Winner 住在圣何塞州立大学的宿舍里,我去的那年 350 个 Winner 中大概只有15个是来自大陆,同时黄皮肤的面孔不超过30个,意味着活动中有大量的时间我需要与母语不是中文的人交流。 比如我的室友就来自俄罗斯🇷🇺,他大四在读,我们用散装英语交流了很多东西,他的作品是用 playgrounds 模拟 Metal(?我到现在也没懂这怎么做的😅),他的计算机基础踏实的一塌糊涂,聊天的时候我就像被面试一样,绞尽脑汁地想他到底在说啥。还有一位意大利小哥🇮🇹一看我这是亚洲人面孔,就凑上来问我们是哪里人,问我是不是中国人,问我来自上海嘛,在得到一系列肯定的答复后他说他去同济大学交流过一段时间,然后他突然很想告诉我他是哪个校区,然后我们开始一起苦思冥想同济大学除了四平路还有在哪的校区,想了半天没想出来,意大利小哥也不尴尬,和我面面相觑地走了一路,最后挥手道别😂。在 WWDC 期间的会场外,也会有很多的 tech talk 在附近举办(蹭 WWDC 热度),我误打误撞参加了一个 Realm 举办的 tech talk。一开始先是吹了一波 Realm,然后是邀请了一些从 Apple 离职的人来做对话,结果发现请到了 Chris Lattner(当时就职于特斯拉)。大家伙那叫一个激动啊, 连下面的好多爷爷辈的听众都提了好几个问题(真的爷爷辈👨🦳,拄拐杖的,不开玩笑),只能感叹一下计算机技术这该死的魅力。另外 WWDC 的倒数第二天有个 Bash,算是一场露天音乐会,气氛很热烈!来的是 Fall Out Boy(代表作有"The Phoenix","Immortals"等,现场太顶了),大家在现场把酒言欢,给 WWDC 收了个尾。在 Bash 现场还和来自国内各大互联网公司的工程师们合影了,不知道各位看官有没有在照片里面的。在 WWDC 玩的这几天拓宽了我看待技术的视野,把我从学校里教的那些知识里面拉了出来,在会场的内外,看到了更多与业界发展相关的东西,看到了计算机科学的广度和深度。我本科就读于魔都某不知名二本,且不是工科强校,所以我的学校计算机专业水平比起其他获得奖学金的同学的学校来说(我记得有中大、同济、新国大、中科大、港大等🙂)差的不是一点半点,无意贬低我的母校。但确实这次的经历让我觉得我能努力的地方原来还有这么多,我在学校所认识到的那些知识似乎只是计算机科学的冰山一角,突然在心中对一些东西产生了渴望,让我突然对未来感到期待和兴奋,在这些经历和感受一起冲击我心灵之后,我知道了一件事:我掉进代码这个陷阱里了🥰。 听说你还在上海科技馆和 Apple 供应链工厂等场合进行过一些线下演讲,这是一种什么体验? 由于在学校参加的 iOS Club 与 Apple 有一些方面的交流和合作,所以也参加了一些特别的活动,例如在夏天的时候去上海科技馆免费给小朋友上编程课,就是用上文提到过的 Playgrounds 这个 App,也给科技馆的小朋友分享参加 WWDC 的经历(同时我也发现,现在的10岁左右小朋友有编程经历的真不少)。除小朋友外,我还作为助教参加过给 Apple 供应链工厂的 Swift 教学活动,用一个月的时间让一点代码都不懂的工厂员工通过学习实践之后,能够自行开发一个简单的 iOS App。另外我还受邀参加香港教与学博览会分享 playgroundbook 的制作经验,向参观者介绍 Playgrounds 这个 app 作为教学工具的一些优点等等。这些交流的经验带给了我从别的角度看待 Swift 以及 iOS 开发的机会,在准备教学教案这些东西的时候,不可避免地会去研究和比较别的语言、别的技术、别的生态是怎么样的😵,让我了解了一些跨平台技术,对客户端开发多了一点理解。 除了这些技术方面的感悟,这些经历带给我了更多情感上的震撼,比如有位10岁小朋友学过了python、c++等等语言,对编程思想也理解的很到位,而且为人很谦虚,还会帮助别的小朋友理解代码;还比如在工厂中我们作为老师教那些20多岁、30多岁、甚至40多岁的员工写代码,而他们就像是科技馆的小朋友一样,对代码一无所知然后带着好奇和热切的眼神来上课,并且当做不出作业时会很羞愧,这些经历会让我感觉到:诶好像这个世界不是我以为的那个世界这种感觉,我没想到有这么多人在推动编程教育、我没想到这么多人对编程这么大的兴趣、我也没想到学编程对一些小孩子来说是一种消遣、我更没想到多会那么一点点编程能改变一个人的发展。根据给我们的反馈,有一小部分上过我们课的工厂员工由于在编程课的表现很好,被“挖”到了工厂的技术部门,从此工作的地点从流水线换到了办公室🤑。 能分享下你的学习历程以及保持学习热情的一些方法吗? 我想我的答案可能和大家想象的很不一样。在校的时候,我是在我社团的推动下成长的,我参加了很多活动、比赛,几乎都是和 iOS 有关的,这些都是是我的社团带给我的资源和推动,比如去参加移动应用创新赛(Apple 赞助)的期间,我学了 ARKit、CoreML、SceneKit、SpriteKit 等等,了解了很多较冷门的 iOS 技术,了解了隐私政策、了解了怎么上架一个 app、以及审核的诸多事项,我的比赛或者活动需要什么我就去学什么,当然这没有什么不好的,没有这些推动我的话我肯定窝在寝室打 dota(嘿嘿👍)。 但至于其他方面的知识?额我懂得很少,记得有次有个评委发现我的 app 里内存泄漏导致 crash 了,他问我为什么 crash 了,我答不出来;记得大四我去面试的第一家实习是英x流x说✍️,面试官和我说他们以前也有一个奖学金的 winner 实习生,表达的意思就是对我蛮期待的,但是后面的面试让他大跌眼镜,我对这次面试印象很深刻,我从第一个问题开始,没有一个问题是答对的(不夸张),答得面试官都有点怀疑人生,他的眼神像是在问我是不是来砸场子的,我对面试的这些内容算是一点都不懂,当时很难受,也很后悔,后悔第一次面试就去了最想去的公司。再之后经过了准备之后换了一家公司得到了 offer,所以在那段时间,促使我学习的是那种落差感,是那种想要再证明自己的不甘。再往后我去考研了然后技不如人没考上,那段时间更焦虑了,会一直督促自己去学习,所以其实失败才是我源源不断学习动力的源泉,也希望在今后的成长道路上,失败不会成我唯一的学习动力👨💻。 有什么想借助摸鱼周报宣传的? 明年计划换工作了,想去大厂被资本家摧残🤡,坐标上海,邮箱:mim0sa@qq.com,欢迎各位老板骚扰。 也希望大家多关注 iOS 摸鱼周报,如果有好的建议和意见快来告诉我们。
人物访谈 | 一位研究生的iOS之路
- 22 Oct, 2021
本期访谈对象是摸鱼周报的主编之一:反向抽烟。他还在读研,因为名字里带个尧字,我们都叫他尧兄。尧兄有一个博客:https://blog.csdn.net/opooc ,分享自己的学习记录,大家有兴趣可以看下。 zhangferry:简单介绍下自己,再讲一下最近的状态吧。大家好,首先感谢飞哥的采访。网名叫反向抽烟,是一名研二在读的大学生,也是 iOS成长之路群的早期用户。目前在做计算机视觉方向学习,一方面对这个领域比较感兴趣,另一方面也是为了毕业设计。在学校的状态一般,强度不大,还是比较随意的。zhangferry:你之前在网易实习,后来又回到了校园,为啥结束了实习?对比校园和职场这两种不同的环境,能分享下你的感受吗?导师给我安排了一些任务,要结束实习工作,所以才回来的。其实我自己不想结束实习,还是挺想上班的。大家一起共事,一起努力完成工作,那种氛围,我还是挺喜欢的。 时间和精力上的分配不太一样,实习更多的是输出吧,校园自由一些,输入会多一些。zhangferry:你接触过的技术方向挺多的,算法、Java、iOS、计算机视觉等,你在选择或者更换技术方向时一般如何做权衡呢?这些方向的本质是没有什么区别,都是为了解决问题,后端也好,前端也好,我觉得都是值得学一下的,毕竟打工嘛,还是得提高自身价值,。zhangferry:尧兄啃过很多本书,能推荐一个对自己影响最大的一本书,简单介绍下吗书的话没有看很多,飞哥既然给面了,那我就推荐一本课外读物《狼的智慧》,强者恒强,适者生存。备注:尧兄真的是在啃书,有一张之前群里发的图片为证:再附一张他推荐过的书目名单:zhangferry:对于学习这件事,你是如何保持热情的?感觉你对学习是沉迷的。可以推荐一些你的学习方法吗?对我而言应该就是兴趣吧,如果对一样技术的态度是为了完成任务而学习,我大概率完成不好。但如果是自主的学习过程中,我有了一些自己的想法,就会竭尽全力的去弄懂。所以主动而非被动,会让学习这件事更有趣味性。 学习方法的话,分两个角度吧,一是学习一定得是刻意的,要拿出时间来经常练习的,这个没有捷径;第二是对于新东西要先用起来,再去研究原理,边用边学,这样效率会高一些。zhangferry:你经历过不少大厂的 iOS 面试,实习面试时考察侧重点是什么样的?对正在找实习工作的同学有什么建议吗?实习面试重点还是在考察基础,有四个部分:算法基础 + 计算机基础 + iOS基础 + 项目/实习。算法是最基本的一项,一般每一面都会问到;计算机基础一般是在一面进行考察;项目/实习一般是在二面/三面进行考察。 准备的话: 1.算法最好有个 300 的题量,一定要在面试前保持题感。 2.计算机基础中考察有计算机网络、编译原理、操作系统、数据结构,网络和操作系统基本是必考,这一部分还是需要花时间理解的,建议平时多花时间钻研下,只背面经是经不住问的。 3.iOS基础准备起来还是有章可循的,推荐看 mj 老师的底层视频和慕课网的 iOS 大牛面试视频,最后把不理解的点做好笔记,及时请教。 4.项目/实习有一样就可以,要是准备项目的话,可以把自己的以前写过的项目进行一些优化,做好总结,把写了简历上的点,都一定要弄明白。 实习面试看公司或者部门,有的公司要实习生去干活,就考 iOS 多一些,有的是为了培养转正,就要求基础牢实一些。主要还是凡事提前准备吧,多往前看几步,早做准备。zhangferry:最近有什么新的感想或心得跟我们分享吗?程序员这个行业其实是与社会关系脱节的,它不是那种社会性质的工作,会经常和人打交道。技术肯定要有的,毕竟是门手艺活,得靠它吃饭,但技术只是一个最最基本的层面,政治书上说了,人的本质是一切社会关系的总和,所以大家还是要多多搞好社会关系,这样人生路才会更宽广,走的更顺畅。(仅为个人观点)
人物访谈 | 微软Offer之路
- 15 Oct, 2021
本期访谈人物是张安宇,他是 #Swift社区 公众号的负责人,最近换工作接了微软(苏州)的 Offer,11月底入职,目前正在爽爽的休假。咱们拉他过来聊聊关于面试微软的经历。 zhangferry:简单介绍下自己吧。大家好,我是 #Swift社区 公众号的负责人,我叫张安宇,很高兴被摸鱼周报邀请来参加这期访谈,我是摸鱼周报的读者,经常看摸鱼周报的文章。 我是一个换工作非常频繁的人,平均每家公司工作 1-2 年甚至更短,比如搜狐和掌阅科技,兄弟们常常因为这个取笑我。关于这个问题我没有特别多的看法,找工作的过程中也没有因为这些受到伤害或者阻挠,相反每次换工作都还挺顺利,大部分情况下是能够受到用人单位的尊重的。 在饿了么工作的时间稍长一些,我在饿了么的蜂鸟团队和北京的前端组主导完成过饿了么入淘的第一个项目叫做蜂鸟商城,同时还负责给我们物流团队的几个 App 统一接入淘宝的浏览器框架 Windvane。 我喜欢玩游戏、看书、看电影,偶尔运动,很喜欢交朋友。平时我跟展菲一起维护我们的这个公众号,还有一些技术群。我们会定期分享一些 Swift 社区动态和相关的资讯,欢迎大家的关注。zhangferry:能简单说下你是如何准备微软的面试的吗,实际面试过程中有哪些比较重要的记忆点?跟国内其他大厂的 iOS 面试有什么区别?微软的面试流程非常长,一共有 6 轮,每轮 1 个小时左右,都是技术面。我之前也没有接触过这种面试形式,所以主要依靠 HR 提供的面试考察点,以及自己去了解来的一些信息来准备这场面试。 这里有个小插曲,正常来说每轮面试间隔应该都是一周左右。因为当时还有另外几家公司在同时面试,跟 HR 说想加快进度,结果后面 2 天就安排完了剩余的 5 轮面试。 微软的 HR 会特别详细地向大家说明每一轮面试的主要步骤,我觉得最重要的是进行针对性地准备。微软面试的核心考察点就是 coding 环节,算法在每轮面试都有,如果代码功底不足,建议继续刷题再参加应聘。整体来说,是在能白板编程 + bug free 的前提下,再根据自己多年的工作经验,与面试官交流一些 iOS 方面的通用性技术。 实际面试中,我能够比较从容应对的主要原因是熟练 + 自信。我有 NOIP 与 ACM 的参赛经验,加上面试前认真刷了半个月 Leetcode,基本上遇到题目不会发慌。因为备战仓促的原因,我刷的题目数量也有限,实际面试过程中,也没有遇到最近刷过的原题。好在微软的题目不算太难,毕竟面试时间只有一小时左右,基本上是 mid 难度的题目。在现场编写时我都基本做到了编译通过、结果正确,并且能分析出算法的时间复杂度和空间复杂度。我觉得我这个表现对于微软来说,是最低要求了,如果 coding 不过关,应该也是会得到面试官的 no hire 的。 我想声明一下,微软的面试要求其实不低,并不是说我就凭刷了两周题目(60 题左右)就通过了面试,整个面试过程的考察还是很全面的,算法只是其中一部分。希望大家不要误解这一点,不要黑微软,谢谢大家。zhangferry:你面过很多国内大厂,在你看来微软跟他们比起来有什么区别呢?工作了这么多年,我几乎参加过所有公司的面试,确实微软这样的企业与国内很多互联网企业的面试要求、流程、侧重点都不一样。 据我观察,头部的互联网企业,面试大都是以面试题八股文为主,最后结束以前面试官可能会考一下算法题,也可能不会。像字节的话就比较看重算法题,相信这个大家都比我更了解。 微软对于算法能力更看重一些,整个面试过程对于候选人的考察点聚焦在算法题,coding 能力。coding 对应的是现场写代码,比如手写 GCD 代码,实现一个多线程的需求。还会有一些相对开放的题目,比如问是否关注过最近的 WWDC,苹果的一些最新特性是什么;如果实现一个词云功能有什么思路,中英文如何分词等等。zhangferry:你应该通过了多家互联网公司的面试,为什么最终选择了微软,而且还要从北京搬到苏州?这个并没有特别多,我是收到微软面试的邀请以后决定离职,因为刷题需要付出很多精力与时间,我曾戏谑说,我备战高考时也不曾如此认真,哈哈。 确实除了微软以外,还有其他公司也在接触,基本都聊的很愉快很顺利,但是不多。我没怎么主动投递自己的简历,甚至简历都没来得及更新,从离职到 Offer 大约两周时间,对于这么密集的面试流程来说,时间是非常仓促的,所以也不能说难度不大。 为了这 6 轮面试,我腾不开时间鸽掉了一些公司,有一家公司我鸽了两次……我怀疑我已经被这些公司屏蔽拉黑了,但我也没办法哈哈。 选择微软的原因,我觉得应该是目标职位的匹配程度比较高,还有微软给出的薪资待遇足够慷慨大方。入职的是 Edge 部门,我说过我喜欢前端技术,也喜欢浏览器内核研发相关的领域,我希望这份工作能够让我继续成长突破自己。 在接受 Offer 的过程中,我坦言过自己可以接受落差,因为我没去过微软,所以对微软所有的了解只能来自于想象与道听途说。希望我的苏州之旅,能够真正让我了解到这家公司,也很乐意在朋友圈与朋友们继续分享我的成长路程与心得体会。 为什么愿意去苏州,这个话题比较沉重。我是特别舍不得北京的,最开始我拒绝过一次微软的面试邀请,也是因为微软在北京没有 iOS 职位的原因。最后愿意去苏州,说实话也是对北京这个城市很多地方感到不太满意。 北京是好,但在北京的这些年,真的没有太多归属感,永远在内心里把自己当做低人一等的外地人。所以搬家去苏州对我来说也没那么难受,毕竟我在北京没有根,我自嘲我这种没有根的人,也是很洒脱的,我想去哪里就去哪里。值得一提的是,微软因为这个给了我搬家费,还不少,令我挺感动的。 我很庆幸微软的 HR 都特别大度,没有把我之前拒绝面试这件事放心里,后来很快就给我重新安排了面试,并且也很热心的提供了很多资料给我。zhangferry:能否简单介绍下已知的微软工作环境、入职的部门?对于英语是否有特殊要求,有没有英文的面试?在你看来微软更喜欢招什么样的人?据我了解,微软的工作环境非常的好,不管是苏州的研发中心,还是北京的总部。我入职的部门是 Edge 团队,我听说 Bing App 即将与 Edge App 合并,希望这件事不会对我的 Offer 有什么影响,哈哈哈。 Edge 是微软自研的浏览器,我相信很多人都知道,Edge 除了 Windows 客户端以外还有 Mac 和 iOS 端,当然也有 Android 端。据我了解,我应该可以去这个团队学习一些比较深入的浏览器内核技术,以及一些 C++ 技术。我希望我以后能成为这方面的大牛,哈哈。 英语的话,当然是要能够日常交流,起码阅读、发 email 能做到没有障碍吧我觉得。面试过程中,我没有遇到说英文的面试官,但是不排除别人不会遇到哈,毕竟微软是一家跨国企业。 我觉得微软喜欢招什么样的人?哈哈这个问题太大了,不过了解我的知道我这个人很狂我也没啥不敢讲的话,我觉得微软应该喜欢招天才进来,真正的天才。不是我自诩天才,是我觉得微软公司配吸引天才们加入这里,一起去实现改变世界的想法,真的。zhangferry:对于正在面试和即将面试的人有什么好的建议?据我了解我有个师弟也在面试微软,他比我优秀多了,我认为他没有问题,他应该刚从中科院读完硕士,在我眼里属于学霸,我认为他进入微软没有太大问题,加油。 对于即将面试的人有什么建议的话,我觉得真的比较简单,那就是调整好心态,认真刻苦复习算法与数据结构,这是一块特别有针对性的领域。 你面试其他公司可能还需要复习一下编译原理之类的,但是对于微软的面试流程,对于 coding 环节来说,我认为就可以不用再额外付出时间精力去复习这些。目标性非常强,所以很容易成功。祝愿和希望更多优秀的人才加入微软和我做同事,哈哈。zhangferry:有什么想借助于摸鱼周报宣传的。当然是替微软招人啦。虽然我目前还没入职,还是想借此机会,替微软的 HR 宣传一下,微软苏州研发中心,大量 HC 期待大家加入。前端、后端、全栈、算法、Data Scientist 都需要,感兴趣的可以投递简历到:a-huili@Microsoft.com。 也希望没有关注过 #Swift社区 公众号的朋友,能点个关注,支持一下我的事业,谢谢大家。zhangferry:你经常在朋友圈发不少长文,最近肯定也有不少生活或者工作的感悟吧,可以给我们分享一下。当然有,最近有很多兄弟说被我的精神感动了,说我怎么说到做到,说刷题就刷题,说换工作就换工作呢,每天还刷到后半夜,太励志了。 我想了一下,可能也是有那么点励志吧,哈哈。人生需要目标,需要理想,需要榜样,看着别人奋斗你也能被感染与鼓舞的,我觉得这就是力量,让生命变得更加精彩的力量。 我在朋友圈编过一句鸡汤,我说“我喜欢光,我追寻光,同时我也想成为光”。我觉得大概就是这个意思吧,好的气质,互相吸引互相鼓舞,这是很难能可贵的事。不过鸡汤喝多了会腻,在身边找一个真正的榜样,向他看齐,脚踏实地的前进,时间久了你可能会发现已经与他并肩而行了。 我就有很多技术偶像和技术榜样,我很荣幸他们大部分都通过了我的好友申请,特别开心。希望大家都能找到自己的榜样。 如果大家不嫌弃想认识我的话,可以通过飞哥找到我,我这个人真的很爱交朋友,哈哈,兄弟们都说我有社交牛逼症…… 再次感谢飞哥的邀请,参加这期摸鱼周报的访谈,谢谢。
MachO 代码签名剖析
验证代码的正确性是计算机科学中最难的问题之一,因为不存在普遍意义的正确的算法,所以这一验证通常使用数字签名处理。数字签名主要做两部分工作:验证代码的来源是否合法。 代码是否被修改过。代码签名并非苹果独有技术,Java 和 Android 的 Dalvik 都在使用,但苹果公司是最早开始使用的。大家可以通过阅读下文思考代码来源是否合法和代码是否被修改过的验证是如何实现的。 本篇文章主要参考自 Jonathan Levin 的《最强 iOS 和 macOS 安全宝典》代码签名一章。测试环境:macOS 11.2.3。 测试项目:/bin/ls 在 x86_64 架构下的 MachO 文件。iOS 下的文件与之相差不大。代码签名格式 在了解代码签名机制前,非常有必要了解代码签名的包含的内容。代码签名附着在 MachO 的尾部。加载命令为LC_CODE_SIGNATURE,它指向一个超级二进制块Code Signature,该二进制块又包含了多个其他的子二进制块。之前写过一篇文章,有讲解如何手动解析这个签名二进制块:深入理解MachO数据解析规则。 下面是该二进制块的层级结构:超级二进制块是一个目录性质的结构,用于指定子二进制块的位置,各个子二进制块才是代码签名的主要角色。 子二进制块类型 子二进制块类型通过不同值进行表示:值 二进制块类型0x0000 代码目录0x0002 需求0x0005 授权0x10000 CMS 二进制块0x10001 身份证明(未使用)本篇主要就是对这几个子二进制块的功能和部分实现进行分析。 二进制块的提取 jtool 是 Jonathan Levin 开发的一款主要用于 MachO 分析的高效工具,可以使用 homebrew 进行安装。 $ brew install jtool我们可以使用 jtool 单独提取代码签名部分: $ jtool -arch x86_64 -e signature /bin/ls Extracting Code Signature (5728 bytes) into ls.signature $ od -t x1 -A x ls.signature #原始字节内容 0000000 fa de 0c c0 00 00 14 86 00 00 00 03 00 00 00 00 0000010 00 00 00 24 00 00 00 02 00 00 02 61 00 01 00 00 0000020 00 00 02 9d fa de 0c 02 00 00 02 3d 00 02 01 00 0000030 00 00 00 00 00 00 00 7d 00 00 00 30 00 00 00 02 0000040 00 00 00 0e 00 00 d2 30 20 02 0b 0c 00 00 00 00 0000050 00 00 00 00 63 6f 6d 2e 61 70 70 6c 65 2e 6c 73 # ...也可以使用 MachO 找到 Code Signature 块进行查看。 代码签名的子二进制块 我们可以使用 jtool 查看代码签名内容的分析: $ jtool -arch x86_64 --sig -v /bin/ls Blob at offset: 53808 (5728 bytes) is an embedded signature of 5254 bytes, and 3 blobs Blob 0: Type: 0 @36: Code Directory (573 bytes) Version: 20100 Flags: none (0x0) Platform Binary CodeLimit: 0xd230 Identifier: com.apple.ls (0x30) CDHash: 46cc1da7c874a5853984a286ffecb48daf2f65f023d10258a31118acfc8a3697 (computed) # of Hashes: 14 code + 2 special Hashes @125 size: 32 Type: SHA-256 Requirements blob: a8ccc60c2a5bff15805beb8687c6a899db386d964a5eb3cf3c895753f6879cea (OK) Bound Info.plist: Not Bound Slot 0 (File page @0x0000): e4a537939e00f4974e02b03d36e4dab75f7dc095d2214ba66bc53c73c145ceff (OK) Slot 1 (File page @0x1000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK) Slot 2 (File page @0x2000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK) Slot 3 (File page @0x3000): 4a7cb3e6c1b3a6ac82e3575239ee53d4f0d3bed260fed63438fd21ce0d00392e (OK) Slot 4 (File page @0x4000): 9ec9e4e02292dfda34ef3caa8317e8bfbcc41a46b18d994dba45febe31b8c660 (OK) Slot 5 (File page @0x5000): 037285f744f366210cde48821261d4a5f5b739dcf0b82f94144613e92c4b7c07 (OK) Slot 6 (File page @0x6000): be89c764e52382702918f2db62ff24d9df40410fe894b11d505a4abc1f854340 (OK) Slot 7 (File page @0x7000): a6b322014743965656e796155c1e0bf22e19a3e8770a43f1111cfbc961037d26 (OK) Slot 8 (File page @0x8000): a643fc9485d941019cbdeead1d5c47add9382417ebe4d15768221f3763553b84 (OK) Slot 9 (File page @0x9000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK) Slot 10 (File page @0xa000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK) Slot 11 (File page @0xb000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK) Slot 12 (File page @0xc000): 23304ae11c1ade4411cb63a0955eb644574b8af416e4e3818e382421272ae1b4 (OK) Slot 13 (File page @0xd000): e0ca7b7000d04057e71c49365b1937711b3557f6b91e0fa144791c66de2a7a4d (OK) Blob 1: Type: 2 @609: Requirement Set (60 bytes) with 1 requirement: 0: Designated Requirement (@20, 28 bytes): SIZE: 28 Ident: (com.apple.ls) AND Apple Anchor Blob 2: Type: 10000 @669: Blob Wrapper (4585 bytes) (0x10000 is CMS (RFC3852) signature) CA: Apple Certification Authority CN: Apple Root CA CA: Apple Certification Authority CN: Apple Code Signing Certification Authority CA: Apple Certification Authority CN: Apple Root CA CA: Apple Certification Authority CN: Apple Root CA CA: Apple Certification Authority CN: Apple Code Signing Certification Authority CA: Apple Software CN: Software Signing Time: 201222002625Zi它有三个 Blob,即三个子二进制块,Blob 0 是代码签名 Blob 1是需求,Blob 2 是 CMS,下面是对这几个 Blob 的分析。 代码目录(Code Directory) 代码目录是签名块的主体,它提供了签名资源的散列值(哈希值)。代码签名并非对整个文件进行签名,因为有时二进制文件可能很大,计算全部内容占用资源较多;而且二进制的加载是按需加载,不会一开始就都全部映射到内存中。签名时会将整个 MachO 文件划分成多个页,每个页单独签名。 代码目录部分就是对签名信息的描述,其中包含了各个分页的签名值,签名算法和分页大小等内容。代码签名的数据结构如下: /* * C form of a CodeDirectory. */ typedef struct __CodeDirectory { uint32_t magic; /* magic number (CSMAGIC_CODEDIRECTORY) */ uint32_t length; /* total length of CodeDirectory blob */ uint32_t version; /* compatibility version */ uint32_t flags; /* setup and mode flags */ uint32_t hashOffset; /* offset of hash slot element at index zero */ uint32_t identOffset; /* offset of identifier string */ uint32_t nSpecialSlots; /* number of special hash slots */ uint32_t nCodeSlots; /* number of ordinary (code) hash slots */ uint32_t codeLimit; /* limit to main image signature range */ uint8_t hashSize; /* size of each hash in bytes */ uint8_t hashType; /* type of hash (cdHashType* constants) */ uint8_t platform; /* platform identifier; zero if not platform binary */ uint8_t pageSize; /* log2(page size in bytes); 0 => infinite */ uint32_t spare2; /* unused (must be zero) */ /* Version 0x20100 */ uint32_t scatterOffset; /* offset of optional scatter vector */ /* Version 0x20200 */ uint32_t teamOffset; /* offset of optional team identifier */ /* followed by dynamic content as located by offset fields above */ } CS_CodeDirectory;结合 CodeDirectory 的偏移量,可以从 MachOView 里查看到这部分数据的内容:找到对应数据结构中的含义,我们关注其中三个 uint8_t 类型的值:参数 值 含义hashSize 0x20 hash 值大小,为 0x20 字节。hashType 0x02 表示签名算法,0x01 表示 SHA-1,0x02表示SHA-256。从 macOS10.12 和 iOS11开始,苹果转向使用 SHA-256。pageSize 0x0C 这里是一个计算公式:log2(PageSize) = 0x0C根据公式算出分页大小:PageSize = 2 ^ 0x0C = 4096 = 0x1000 = 4K。这跟系统的内存分页大小是一致的。 由此可知整个 MachO 文件会按照 0x1000 字节的大小进行分页,分页使用 SHA-256 算出散列值。这些计算出的散列值会记录在代码插槽(Code Slots)里。 代码插槽验证 上面Slot 从 0 到 13 的标记对应的都是代码插槽。 有了计算规则我们还可以手动验证代码签名的正确性,我们以前三个代码插槽为例,也即前 0x1000 字节的内容,尝试手动计算其散列值。 $ lipo /bin/ls -thin x86_64 -output /tmp/ls_x86_64 $ dd bs=0x1000 skip=0 count=1 if=/tmp/ls_x86_64 2>/dev/null | openssl sha256 SHA256(stdin)= e4a537939e00f4974e02b03d36e4dab75f7dc095d2214ba66bc53c73c145ceff $ dd bs=0x1000 skip=1 count=1 if=/Users/zhangferry/ls_x86_64 2>/dev/null | openssl sha256 SHA256(stdin)= ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 $ dd bs=0x1000 skip=2 count=1 if=/Users/zhangferry/ls_x86_64 2>/dev/null | openssl sha256 SHA256(stdin)= ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7注意到后面两个插槽计算结果一样,这是因为这两部分数据为补齐位,它们全部为0。跟前三个代码插槽的值进行对比: Slot 0 (File page @0x0000): e4a537939e00f4974e02b03d36e4dab75f7dc095d2214ba66bc53c73c145ceff (OK) Slot 1 (File page @0x1000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK) Slot 2 (File page @0x2000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK)发现两边散列值一样,输出内容后面的 OK 是 jtool 验证的结果。 这里可以看到代码插槽有14个,File page 里的内容表示相对起始地址。另外注意到输出部分有一句注释: # of Hashes: 14 code + 2 special其表示有 14 个代码插槽和 2 个特殊插槽。 特殊插槽 特殊插槽的出现是因为App由多个内容组成,并非只有二进制文件,为了保证这些非二进制文件的完整性,对它们也会进行签名,它们的签名值就是特殊插槽。因为代码插槽的索引是从0开始的,而且其大小不固定,为了把特殊插槽也能排列进去,就选用负数来表示特殊插槽的含义。以下是特殊插槽的定义:# 插槽目的-1 绑定的info.plist-2 需求(requirement):二进制块嵌入代码签名-3 资源目录:CodeSignature/CodeResources文件的散列值-4 具体应用:实际上未被使用-5 授权(entitlement):嵌入在代码签名中的授权我们可以在上方 jtool 的输出内容里找到特殊插槽的内容: Requirements blob: a8ccc60c2a5bff15805beb8687c6a899db386d964a5eb3cf3c895753f6879cea (OK) Bound Info.plist: Not Bound因为特殊插槽作用是固定的,也就没用序号表示。 代码签名需求(Requirements) 目前代码签名只是分块取散列值,保存起来,但好像还不够强大。苹果公司已经为代码签名增加了另外一个机制:需求(requirements)。它可以自定义规则以施加特定限制,比如允许哪些动态库加载。 需求有一套特殊的语法规则,其表达由操作数和操作码组成,丰富的操作码集使得构建任何数量的逻辑条件成为可能。可以在requirements.h 文件里查看都有哪些操作码。 enum ExprOp { opFalse, // unconditionally false opTrue, // unconditionally true opIdent, // match canonical code [string] opAppleAnchor, // signed by Apple as Apple's product opAnchorHash, // match anchor [cert hash] opInfoKeyValue, // *legacy* match Info.plist field [key; value] opAnd, // binary prefix expr AND expr opOr, // binary prefix expr OR expr opCDHash, // match hash of CodeDirectory directly opNot, // logical inverse opInfoKeyField, // Info.plist key field [string; match suffix] opCertField, // Certificate field [cert index; field name; match suffix] opTrustedCert, // require trust settings to approve one particular cert [cert index] opTrustedCerts, // require trust settings to approve the cert chain opCertGeneric, // Certificate component by OID [cert index; oid; match suffix] opAppleGenericAnchor, // signed by Apple in any capacity opEntitlementField, // entitlement dictionary field [string; match suffix] exprOpCount // (total opcode count in use) };对需求的编译是由 csreq 进行的,对需求的验证可以使用 codesign -v。 我们这里来尝试解读下已有的需求内容。 大部分二进制文件的需求只是验证签名身份,即使用证书是否为苹果所颁发。在 App Store 里的应用则使用更严格的规则集。我们可以查看 Xcode 的代码签名需求: $ codesign -d -r- /Applications/Xcode.app/Contents/MacOS/Xcode Executable=/Applications/Xcode.app/Contents/MacOS/Xcode designated => (anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = APPLECOMPUTER) and identifier "com.apple.dt.Xcode"注意这里有不少 1.2.840.113635 开头的标识,它代表的是国际通用标准证书中苹果公司的分支(iso.member-body.us.appleOID)。其中100对应了安全相关的一些定义 appleDataSecurity,详细内容可以看这里oidref.com。对照Xcode的签名需求,我们可以大致推断出这些规则的含义:由苹果签名且证书节点包含 6.1.9 即 Mac App Store App。 或由苹果签名且证书节点包含 6.2.6 即 "dev_program"。(推测是开发版本的应用) 其证书节点包含 6.1.13 即 Developer ID Applications。 证书的团队标识符(OU)为 APPLECOMPUTER 且 BundleId 为 com.apple.dt.Xcode。注意其中最后一项的内容,限定了团队标识符和BundleId,这样就能够解决应用被重签名的问题了。 CMS CMS 是Cryptographic Message Syntax的缩写,是一种标准的签名格式,由RFC3852定义。书中并没有提这部分内容,但我认为这部分恰恰是代码签名最关键的步骤。 CMS 格式的签名中,除了包含证书之外,还承载了一些其他的信息,比如签名属性 signedAttrs。 上面说了 CodeDirectory 里保存了 MachO 分页的 Hash 值,只要保证这个 CodeDirectory 不被修改就可以了。所以对代码目录进行 Hash 计算,获得 CDHash,然后对这个 CDHash 进行签名就可以了。 注意这一步才是真正的签名,其开始涉及加密,前面的代码插槽只是提供摘要信息。 注意到 jtool 的签名输出里有这样一句: CDHash: 46cc1da7c874a5853984a286ffecb48daf2f65f023d10258a31118acfc8a3697 (computed)这就是外部计算的 CDHash 值,用于跟 signedAttrs 里的内容进行对比。而更关键的是对 signedAttrs 的加密验证,实际验证流程比较复杂,感兴趣的小伙伴可以阅读这篇细说iOS代码签名(三)。 我结合文中签名校验内容和上面的代码插槽,画出了表示签名校验的整个流程:这里有两处 Hash 对比,一个是对 signedAttrs 的解密,确保其是可信任的。另一处是 CDHash 的对比,确保代码未被修改。 signerInfo 里包含了 signedAttrs 、签名使用的 Hash 算法、加密算法、签名数据等信息。再结合证书里的公钥,我们就可以验证,signedAttrs 的有效性。 授权 除了确保代码的真实性和完整性,代码签名还为苹果公司及其强大的安全机制提供了授权(entitlement)功能。授权文件也被包含在签名里,其散列值放在索引为-5的插槽中。授权文件是一个 XML 格式的文件,我们可以使用 jtool --ent 查看其内容,因为 ls 没有授权文件,我们以 Mac 端微信为例进行查看: $ jtool -arch x86_64 --ent /Applications/WeChat.app/Contents/MacOS/WeChat <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>com.apple.security.app-sandbox</key> <true/> <key>com.apple.security.application-groups</key> <array> <string>5A4RE8SF68.com.tencent.xinWeChat</string> </array> <key>com.apple.security.device.audio-input</key> <true/> <key>com.apple.security.device.camera</key> <true/> <key>com.apple.security.device.microphone</key> <true/> <key>com.apple.security.files.downloads.read-write</key> <true/> <key>com.apple.security.files.user-selected.read-write</key> <true/> <key>com.apple.security.network.client</key> <true/> <key>com.apple.security.network.server</key> <true/> <key>com.apple.security.personal-information.location</key> <true/> </dict> </plist>这里我们可以看到其中包含了沙盒、application-groups、声音输入、摄像头等一系列权限。在应用访问特定 API 的时候苹果可以根据这些授权判定该行为是否合法。因为苹果公司是应用的终极签名者,所以签名过程中也可以很容易的修改授权,比如 com.apple.security.sandbox.container-required 这一表示沙盒权限的值就会被强制安置到授权文件中。 强制验证代码签名 为了使代码签名真正有效,非常重要的一步就是要保证验证过程顺利执行,没有遗漏。当前签名验证是发生在内核模式下,而非用户模式下。签名的验证发生在两个阶段:加载可执行文件时、实际访问二进制代码时(Page Fault)。分成两个阶段也是出于性能方面的考虑,因为二进制文件是动态加载的,对于还没加载的部分仅当其加载如内存时,也即发生 Page Fault 时再进行签名验证。 可执行文件的加载 可执行文件的加载出现在execve()/mac_execve()或posix_spawn()系统调用被触发的时候。对于MachO,exec_mach_imgact()会被调用,在解析文件时它会找到LC_CODE_SIGNATURE的位置。代码签名二进制块会被加载到内核的统一高速缓存缓冲区中。 Page Fault时的处理 可以查看 osfmk/vm/vm_fault.c 的代码: /* * CODE SIGNING: * When soft faulting a page, we have to validate the page if: * 1. the page is being mapped in user space * 2. the page hasn't already been found to be "tainted" * 3. the page belongs to a code-signed object * 4. the page has not been validated yet or has been mapped for write. */ #define VM_FAULT_NEED_CS_VALIDATION(pmap, page, page_obj) \ ((pmap) != kernel_pmap /*1*/ && \ !(page)->cs_tainted /*2*/ && \ (page_obj)->code_signed /*3*/ && \ (!(page)->cs_validated || (page)->wpmapped /*4*/))当 Page Fault 满足以上条件时将触发签名验证过程: 1、该页面正在用户空间中映射 2、这个页面还没有被发现为 tainted 3、该页属于一个代码签名对象 4、页面还没有被验证,或者还没有被映射为可写状态 代码签名的漏洞 代码签名机制虽然强大,保护着应用的安全,但依然被攻破过,以下讲解几例曾经出现的漏洞。 JIT(即时生成代码) 该情况发生在 Page Fault 过程,如果该页内容是用于 JIT,将会被特殊标记,可以创建和执行任意代码,而无需代码签名。 从 iOS 10 开始,苹果公司开始在64位的设备上加固 JIT。采用专门的 memcpy() 将JIT映射到可执行但不可读的内存上,然后可执行的 JIT 映射为不可写,可写的 JIT 映射为不可执行状态。 Jekyll 应用 Jekyll 应用的含义是应用在提交至 App Store 时表现为无害,但其实它包含恶意功能,只不过这些功能处于休眠状态。过审之后和本地服务器进行合作,自愿公开其地址空间和符号,通过代码注入或者返回导向编程(Return Oriented Programming,ROP),触发预置的恶意程序。 目前还没有可靠的打击 ROP 的方法,但因为沙盒机制的缘故,恶意代码的影响范围是可控的。 苹果公司使用 LLVM BitCode 向 App Store 提交应用的方案,也会使恶意应用难以事先知晓其地址空间。 内存锁定 从上面我们知道发生Page Fault会触发签名验证的流程,那如果没有Page Fault就不会存在签名验证了。按照mmap -> mlock -> memcpy -> mprotect 的调用顺序,应用可以修改可执行内存,以任何看起来合适的方式修补内存。虽然XNU通常会阻止将曾经可写的内存设置为r-x,但当内存锁定时,会绕过该检测。 苹果在iOS 9.3中修复了这个漏洞。 总结 我们再来尝试回答开头上面遗留的问题: 1、如何验证代码的来源是否合法? 主要通过证书来验证来源是否合法,所有的开发者证书都由苹果颁发,且被 Root CA 认证。另外依托于需求(requirements),还可以再扩展一些其他验证方式。 2、如何确认代码是否被修改过。 主要通过代码插槽和 CDHash,再对 CDHash 进行签名,就可确认其是否被修改过。注意实际验证流程有两处关键的 Hash 比对,可以再结合上面的流程图加深理解。
Category无法覆写系统方法?
这是一次非常有趣的解决问题经历,以至于我认为解决方式可能比问题本身更有意思,另一点就是人多力量大,多人讨论就会获得多种思路。 首次提出这个问题的是反向抽烟,他遇到了不能用 Category 覆写系统方法的现象。问题抛到我这,我验证了这个有点奇怪的现象,并决定好好探究一下,重看了 Category 那部分源码仍没有找到合理解释,于是将这个问题抛到开发群里,最后由皮拉夫大王在此给出了最为合理的解释。之后我又顺着他的思路找到了一些更有力的证据。以下是这一过程的经历。问题提出 以下内容出自反向抽烟: 背景:想为 UITextField 提供单独的属性 placeholderColor ,用来直接设置占位符的颜色,这个时候使用分类设置属性,重写 setter 和 getter,set中直接使用 KVC 的方式对属性的颜色赋值;这个时候就有个bug,如果在其他类中使用 UITextField 这个控件的时候,先设置颜色,再设置文字,会发现占位符的颜色没有发生改变。 解决思路:首先想到 UITextField 中的 Label 是使用的懒加载,当有文字设置的时候,就会初始化这个label,这时候就考虑先设置颜色根本就没起到作用; 解决办法:在分类中 placeholderColor 的 setter 方法中,使用runtime的objc_setAssociatedObject先把颜色保存起来,这样就能保证先设置的颜色不会丢掉,然后需要重写 placeholder的setter方法,让在设置完文字的时候,拿到先前保存的颜色,故要在placeholderColor 的getter中用objc_getAssociatedObject取,这里有个问题点,在分类中重写 placeholder 的setter方法的话,在外面设置 placeholder 的时候,根本不走自己重写的这个 setPlaceholder方法,而走系统自带的,这里我还没研究。然后为了解决这个问题,我自己写了个setDsyPlaceholder方法,在setDsyPlaceholder里面对标签赋值,同时添加已经保存好的颜色,然后与setPlaceholder做交换,bug修复。 这里大家先不要关注解决 placeholderColor 的方式是否正确,以免思路走偏。我们应该避免使用Category 覆写系统方法的,但这里引出了一个问题:如果就是要覆写系统的方法,为啥没被执行? 问题探索 我测试发现自定义类是可以通过 Category 覆写的,只有系统方法不可以。当时选的是 UIViewController 的viewDidLoad 方法,其他几个 UIViewController 方法也试了都不可以。 测试代码如下: #import "UIViewController+Test.h"@implementation UIViewController (Test)- (void)viewDidLoad { NSLog(@"viewDidLoad"); }@end所以猜测:系统方法被做了特殊处理都不能覆写,只有自定义类可以覆写。 有一个解释是:系统方法是会被缓存的,方法查找走了缓存,没有查完整的方法表。 这个说法好像能说得通,但是系统缓存是库的层面,方法列表的缓存又是另一个维度了。方法列表的缓存应该是应用间独立进行的,这样才能保证不同应用对系统库的修改不会相互影响,所以这个解释站不住脚。 这时有朋友提出他们之前使用Category 覆写过 UIScreen 的 mainScreen,是可以成功的。我试了下确实可以,观察之后发现该属性是一个类属性。又试了其他几个系统库的类属性,也都是可以的。 所以猜测变成了:只有系统实例方法不能被覆写,类属性,类方法可以覆写。 这时已经感觉奇怪了,这个规律也说不通。后来又有朋友测试通过 Xcode10.3 能够覆写系统方法,好嘛。。。 这时的猜测又变成了:苹果在某个特定版本开始才做了系统方法覆写的拦截。 可靠的证据 皮拉夫大王在此提出了很关键的信息,他验证了iOS12系统可以覆写系统方法(后来验证iOS13状况相同),iOS14不能覆写。 但iOS14的情况并不是所有的系统方法都覆盖不了,能否覆盖与类方法还是实例方法无关。 例如:UIResponder的分类,重写init 和 isFirstResponder,init可以覆盖,isFirstResponder不能覆盖。在iOS14的系统上NS的类,很多都可以被分类覆盖,但是UIKit的类,在涉及到UI的方法时,很多都无法覆盖。 这里猜测:系统做了白名单,命中白名单的函数会被系统拦截和处理。 以下是对 iOS14 状况的验证,覆写isFirstResponder,打印method_list: unsigned int count; Method *list = class_copyMethodList(UIResponder.class, &count); for (int i = 0; i < count; i++) { Method m = list[i]; if ([NSStringFromSelector(method_getName(m)) isEqualToString:@"isFirstResponder"]) { IMP imp = method_getImplementation(m); } }isFirstResponder会命中两次,两次po imp的结果是: //第一次 (libMainThreadChecker.dylib`__trampolines + 67272) //第二次 (UIKitCore`-[UIResponder isFirstResponder])同样的代码,在iOS12的设备也会命中两次,结果为: //第一次 (SwiftDemo`-[UIResponder(xx) isFirstResponder] at WBOCTest.m:38) //第二次 (UIKitCore`-[UIResponder isFirstResponder])所以可以确认的是,分类方法是可以正常添加到系统类的,但在iOS14的系统中,覆写的方法却被libMainThreadChecker.dylib里的方法接管了,导致没有执行。 那么问题来了,这个libMainThreadChecker.dylib库是干嘛的,它做了什么? 这个库对应了Main Thread Checker这个功能,它是在Xcode9新增的,因为开销比较小,只占用1-2%的CPU,启动时间占用时间不到0.1s,所以被默认置为开的状态。它在调试期的作用是帮助我们定位那些应该在主线程执行,却没有放到主线程的代码执行情况。另外官方文档还有一个解释:The Main Thread Checker tool dynamically replaces system methods that must execute on the main thread with variants that check the current thread. The tool replaces only system APIs with well-known thread requirements, and doesn’t replace all system APIs. Because the replacements occur in system frameworks, Main Thread Checker doesn’t require you to recompile your app.这个家伙会动态的替换尝试重写需要在主线程执行的系统方法,但也不是所有的系统方法。 终于找到了!这很好的解释了为什么本应被覆盖的系统方法却指向了libMainTreadChecker.dylib这个库,同时也解释了为什么有些方法可以覆写,有些却不可以。 测试发现当我们关闭了这个开关,iOS14的设备就可以正常执行覆写的方法了。 到此基本完事了,但还留有一个小疑问,那就是为什么iOS14之前的设备,不受这个开关的影响?目前没有找到实质的证据表明苹果是如何处理的,但可以肯定的是跟 Main Thread Checker 这个功能有关。 总结 稍微抽象下一开始处理问题的方式:遇到问题 -> 猜想 -> 佐证 -> 推翻猜想 -> 重新猜想 -> 再佐证。 这其实是错误的流程,猜想和佐证可以,但他们一般只会成为一个验证的样例,而不能带给我们答案。所以正确的处理方式是,不要把太多时间浪费在猜想和佐证猜想上,而应该去深挖问题本身。新的解题思路可以是这样的:遇到问题 -> 猜想 -> 深挖 -> 根据挖到的点佐证结果。
深入理解MachO数据解析规则
我们知道Apple设备可执行文件的存储格式是MachO,一个二进制文件。通常在做逆向或者静态分析的时候都会用到这个文件,分析MachO的常用工具是MachOView。今天借助于MachOView,主要分析Code Signature的存储规则。 本篇文章同时也是围绕这几个问题展开的: 1、MachOView是如何确认MachO内容的。 2、二进制数据是如何存储的,如何确认位置。 3、字节码含义如何解析。前置准备 1、二进制文件其实简单理解就是通过二进制形式进行存储内容的文件,它可以原封不动的读到内存中用于完成各种处理。比如数值3.1415927,文本文件需要9个字节进行存储:3 . 1 4 1 5 9 2 7 这 9 个 ASCII 值,而如果是二进制的话4个字节就够了:DB 0F 49 40。 2、二进制文件读到内存中通常是连续存储的,它不需要额外的处理,原本怎样,在内存里就是怎样的。 3、每个进程都会被分配一个虚拟地址空间,进程寻址的范围就是在这个虚拟地址空间进行的,虚拟地址到物理地址之间有一个映射表进行管理。 4、可以简单理解:虚拟地址 = 随机基址(ASLR)+ 逻辑地址(段内偏移)。 后面的内容也会出现很多偏移量(offset)的概念,它的含义很简单就是相对某一位置偏移多少字节。关键是需要确认它是相对哪个位置进行的偏移,在不同的数据段,这个相对的锚点是不一样的。但通常来说偏移量都是相对于当前的数据段来说的。 5、FAT格式的MachO可以理解为多个架构的顺序组合,所以分析某个架构时,还需要加上对应架构的偏移量。 6、uint32_t占4个字节,uint8_t占1个字节,char占一个字节。 Mach-O格式 格式分析 可以简单看下Mach-O的数据结构:Mach-O文件大致分为三部分: Header 表示当前的Mach-O文件整体信息,包含CPU架构、子版本、文件类型、加载命令数等内容。数字内容好表示,那CPU架构这样的类别是如何表示的呢?二进制数据说到底也是数字,这些类别信息也只能通过数字表示,但需要一个具有特殊含义的数字,这个数字通常叫magic(魔数)。比如0xCAFEBABE表示FAT,0xFEEDFACF表示ARM64。 Header的定义地址:https://opensource.apple.com/source/xnu/xnu-792/EXTERNAL_HEADERS/mach-o/loader.h.auto.html Load Commands 记录各个数据段的信息和位置,只是类别和标记的介绍,包含一些信息的偏移地址、文件大小等内容。 Data 记录具体的内容信息。不同类别的信息对应不同的数据含义。注意上图右侧由Load Commands到Data的箭头,Data的位置是由Load Commands指定的。 他们三者的关系如果用一本书表示的话就是:Header是封面,Load Commands是目录,Data是书的内容。 寻找Code Signature 本节的重点是找到Code Signature(代码签名)这部分内容,它没被MachOView解析,还是原始的数据形态,是一个比较好的分析案例。 分析文件是系统的ls,它的路径在/bin/ls,把它放到MachOView里。ls是一个FAT文件,它包含两个架构,Fat Header里记录了各个架构的类别、偏移量、大小等信息。我们只关注X86_64架构下的内容,展开这个架构下的Load Commands,找到代表代码签名的LC_CODE_SIGNATURE信息:右侧是真实的数据内容,MachOView已经帮我们对应好了字段描述: Data Offset:代表数据偏移 53808,换成16进制就是0xD230 Data Size:代表文件大小 5728,换成16进制就是0x1660 这俩16进制值其实就是Data对应的内容,Value是MachOView帮我们做的处理。 这里的偏移跟上面Fat Header的偏移含义已经不一样了,Fat Header说的是总文件偏移,这里的偏移则是针对X86文件的偏移。所以实际的偏移应该是:0xD230 + 0x4000 = 0x11230。 找到Data部分的Code Signature内容:这里pFile就是相对当前文件的偏移量(也可以理解为逻辑偏移量),它的起始位置正是上面计算得的:0x11230。由大小0x1660,我们还可以计算得出Code Signature最后一个字节所在位置是:0x11230 + 0x1660 - 0x1 = 0x1288F。 解析Code Signature CS_SuperBlob 我们已经找到了代码签名位置,现在开始解析它吧。解析的第一步就是需要找到数据定义,有了定义才能分析出数据含义。Code Signature相关内容的定义在这里:https://opensource.apple.com/source/xnu/xnu-3789.51.2/bsd/sys/codesign.h.auto.html 整个签名的头部是一个CS_SuperBlob结构体,它的定义如下: typedef struct __SC_SuperBlob { uint32_t magic; /* magic number */ uint32_t length; /* total length of SuperBlob */ uint32_t count; /* number of index entries following */ CS_BlobIndex index[]; /* (count) entries */ /* followed by Blobs in no particular order as indicated by offsets in index */ } CS_SuperBlob; 这个结构体第一个参数是magic,它的定义如下: /* * Magic numbers used by Code Signing */ enum { CSMAGIC_REQUIREMENT = 0xfade0c00, /* single Requirement blob */ CSMAGIC_REQUIREMENTS = 0xfade0c01, /* Requirements vector (internal requirements) */ CSMAGIC_CODEDIRECTORY = 0xfade0c02, /* CodeDirectory blob */ CSMAGIC_EMBEDDED_SIGNATURE = 0xfade0cc0, /* embedded form of signature data */ CSMAGIC_EMBEDDED_SIGNATURE_OLD = 0xfade0b02, /* XXX */ CSMAGIC_EMBEDDED_ENTITLEMENTS = 0xfade7171, /* embedded entitlements */ CSMAGIC_DETACHED_SIGNATURE = 0xfade0cc1, /* multi-arch collection of embedded signatures */ CSMAGIC_BLOBWRAPPER = 0xfade0b01, /* CMS Signature, among other things */ //... }第二个参数是length,表示整个SuperBlob的长度。 第三个参数是count,表示index实体条目的数量。 第四个参数是为CS_BlobIndex的一个结构体。 大端小端 1、这个是64位架构的二进制数据,其实有两种64位架构,他们分别表示为大端64位和小端64位,上面MachOView分析的X86 Header中的魔数是0xFEEDFACF,代表的就是当前二进制文件是小端64位格式。 2、比如0x1234这个数据,在小端情况下,12会存放在低字节处,34会放于高字节处,大端则相反。 数据解析 我们把Code Signature的第一个行数据拿出来分析:这里注意Data部分,有两个标签:Data LO和Data HI,是用于表示当前的字节序列,前面是低字节,后面是高字节。这样按照小端的规则,我们就可以按自然顺序取数据了,所以可以得出以下内容: magic 为0xFADE0CC0,对应CSMAGIC_EMBEDDED_SIGNATURE,代表嵌入的代码签名数据。 length 是0x1486,我们可以计算得出最后一个字节位置:0x11230 + 0x1486 - 0x1 = 0x126B5红色标记的字节就是Code Signature结束的地方,在这之后的内容全部由0x00填充,就非实体内容了。 count 是3,表示接下来有3个实体内容,这个实体对应的是结构体:CS_BlobIndex。 CS_BlobIndex 我们来看下CS_BlobIndex这个结构体: /* * Structure of an embedded-signature SuperBlob */typedef struct __BlobIndex { uint32_t type; /* type of entry */ uint32_t offset; /* offset of entry */ } CS_BlobIndex;它有两个成员变量,type表示实体类型,offset表示实体偏移量。 一般表示类型的肯定有特殊数字对应的含义,这里的type也是一样的,这个type在上面的magic在一个enum里定义。 CSSLOT_CODEDIRECTORY = 0, /* slot index for CodeDirectory */ CSSLOT_INFOSLOT = 1, CSSLOT_REQUIREMENTS = 2, CSSLOT_RESOURCEDIR = 3, CSSLOT_APPLICATION = 4, CSSLOT_ENTITLEMENTS = 5,CSSLOT_ALTERNATE_CODEDIRECTORIES = 0x1000, /* first alternate CodeDirectory, if any */ CSSLOT_ALTERNATE_CODEDIRECTORY_MAX = 5, /* max number of alternate CD slots */ CSSLOT_ALTERNATE_CODEDIRECTORY_LIMIT = CSSLOT_ALTERNATE_CODEDIRECTORIES + CSSLOT_ALTERNATE_CODEDIRECTORY_MAX, /* one past the last */CSSLOT_SIGNATURESLOT = 0x10000, /* CMS Signature */数据解析 我们再回到数据部分,根据上面结构体进行分析:能够解析出三条CS_BlobIndex数据:type type含义 offset0x00 CSSLOT_CODEDIRECTORY 0x240x02 CSSLOT_REQUIREMENTS 0x2610x10000 CSSLOT_SIGNATURESLOT 0x29D这里又出现了一个offset,这个offset存在于Code Signature的最外部,所以它表示的就是相对Code Signature的偏移量。 这个表相当于又提供了一个目录,它告诉我们,之后的内容有三部分(三个结构体)组成,各个部分的页码是什么。 CS_CodeDirectory 我们先分析CSSLOT_CODEDIRECTORY,它对应的是CS_CodeDirectory结构体: /* * C form of a CodeDirectory. */ typedef struct __CodeDirectory { uint32_t magic; /* magic number (CSMAGIC_CODEDIRECTORY) */ uint32_t length; /* total length of CodeDirectory blob */ uint32_t version; /* compatibility version */ uint32_t flags; /* setup and mode flags */ uint32_t hashOffset; /* offset of hash slot element at index zero */ uint32_t identOffset; /* offset of identifier string */ uint32_t nSpecialSlots; /* number of special hash slots */ uint32_t nCodeSlots; /* number of ordinary (code) hash slots */ uint32_t codeLimit; /* limit to main image signature range */ uint8_t hashSize; /* size of each hash in bytes */ uint8_t hashType; /* type of hash (cdHashType* constants) */ uint8_t platform; /* platform identifier; zero if not platform binary */ uint8_t pageSize; /* log2(page size in bytes); 0 => infinite */ uint32_t spare2; /* unused (must be zero) */ /* Version 0x20100 */ uint32_t scatterOffset; /* offset of optional scatter vector */ /* Version 0x20200 */ uint32_t teamOffset; /* offset of optional team identifier */ /* followed by dynamic content as located by offset fields above */ } CS_CodeDirectory;数据解析 我们先把这段数据拿出来,然后根据结构体进行分析:这里仅挑一些重要的内容进行分析。 magic是0xFADE0C02,作为标记存在,代表CodeDirectory length是0x23D,表示数据段长度 identoffset是0x30,表示identifier字符串的偏移量,这里的identifier对应的就是我们的bundleId 需要提醒的是当前的CodeDirectory是数据SuperBlob的内部结构体,所以这里的offset就变成了结构体内部偏移了,这里的起始位置也即是0xFADE0C02所在的位置是0x11254,所以可以算出indentoffset的文件偏移量是: identoffset地址为:0x11254 + 0x30 = 0x11284 这里你可能会疑惑,只有偏移量怎么确认从哪结束呢,这里并没有提供数据大小。其实字符串是不需要知道大小也可以确认它到哪结束的,字符里面有结束位\0啊,在ASCII码里结束位就是0x00。可以解析得出ls的bundleId是com.apple.ls。 这里再补充一点:MachO里字符串的编码不是通过ASCII,而是使用UTF-8进行编码的,只不过UTF-8兼容了ASCII,所以我们当做ASCII也能解析出正确的内容。 CS_GenericBlob 我们现在来看下证书的解析,查上面记录的偏移表,CSSLOT_SIGNATURESLOT对应的结构体是CS_Generic_Blob: typedef struct __SC_GenericBlob { uint32_t magic; /* magic number */ uint32_t length; /* total length of blob */ char data[]; } CS_GenericBlob;上个表格我们记录了它的offset是0x29D位置,所以它的起始位置就是:0x11230 + 0x29D = 0x114CD,找到这个位置,带入结构体进行解析:magic是0xFADE0B01,对应了CSSLOT_SIGNATURESLOT值。 数据长度是0x11E9(4585字节),这表示的CS_GenericBlob的大小,而在这之后的内容都是data,表示的就是证书部分。 我们可以计算出证书data结束的最后一个字节位置:0x114CD + 0x11E9 - 0x8 - 0x1 = 0x126AD。 说明:根据《iOS应用逆向与安全》一书说明,借助于010 Editor等二进制工具,我们把data部分的数据复制出来(需要借助于Hooper这类工具),保存为cer格式,就能获取到一个证书文件。但对ls的测试并不能成功,推测这里的data可能还有其余内容,需要拆分。 Jtool 只要有了对应数据结构,签名部分的所有信息我们都是可以解析出来的。但每次都逐字节分析,显然很费事,能不能写个程序,用于上述内容解析呢?当然是可以的,已经有这样的工具了,就是Jtool。jtool比otool功能更强大,解析的数据也更详细。可以通过homebrew进行安装: $ brew install jtool如果通过jtool查看上面x86_64架构的签名信息,可以这样: $ jtool -arch x86_64 --sig /bin/ls输出结果为: Blob at offset: 53808 (5728 bytes) is an embedded signature Code Directory (573 bytes) Version: 20100 Flags: none Platform Binary CodeLimit: 0xd230 Identifier: com.apple.ls (0x30) CDHash: 46cc1da7c874a5853984a286ffecb48daf2f65f023d10258a31118acfc8a3697 (computed) # of Hashes: 14 code + 2 special Hashes @125 size: 32 Type: SHA-256 Requirement Set (60 bytes) with 1 requirement: 0: Designated Requirement (@20, 28 bytes): SIZE: 28 Ident: (com.apple.ls) AND Apple Anchor Blob Wrapper (4585 bytes) (0x10000 is CMS (RFC3852) signature) CA: Apple Certification Authority CN: Apple Root CA CA: Apple Certification Authority CN: Apple Code Signing Certification Authority CA: Apple Certification Authority CN: Apple Root CA CA: Apple Certification Authority CN: Apple Root CA CA: Apple Certification Authority CN: Apple Code Signing Certification Authority CA: Apple Software CN: Software Signing Time: 201222002625Zi第一行里的offset 53808 对应16进制是0xD230,就是LC_CODE_SIGNATURE里记录的偏移量。 根据输出信息也能得出code signature由三部分内容组成:Code Diretory、Requeirement Set、Blob Wrapper。证书部分解析出了6个证书,说明这里应该还有别的结构体可以拆分。 回顾 如果你看到这里,可以回顾下开始讲到的三个问题,用于检验你的理解程度。 1、MachOView是如何确认MachO内容的。 2、二进制数据是如何存储的,如何确认位置。 3、字节码含义如何解析。
《学习之道》书评
- 05 Mar, 2021
《学习之道》的作者是芭芭拉·奥克利,她是美国奥克兰大学的工程学教授。她在Coursera上开设了一门课程叫“Learning How to Learn”,即讲如何学习的,该课程非常受欢迎,有200多万人注册学习。该课程地址在这里:https://www.coursera.org/learn/ruhe-xuexi。这门课跟这本《学习之道》内容重合度很高,毕竟都出自同一人,可以作为配套视频来看。如果不想看书又想详细学习课程精髓的话,只看视频也是完全可以的。这本书讲了什么 我感觉可以用Coursera课程上的中文翻译来概括:学习困难科目的实用思维方法。这里突出困难科目其实是指常让人感觉难学的突出逻辑性的理工科,但我认为不仅是「困难科目」,任何新的概念都可以借鉴书中提到的方法进行学习。 本书引用了很多脑科学和心理学的最新研究,很多方法都是有实验依据的,以下是我认为比较有益的内容。 核心概念 专注思维 vs 发散思维 理论知识 我们的大脑有两种截然不同的思维模式 --- 专注模式和发散模式。专注模式是利用理性分析,按照特定步骤解决问题的模式。 发散模式没有特定的步骤,它是弥散在大脑中的,我们常说的灵感一般都来源于发散模式。应用 1、通常情况我们的大脑会在这两种模式之间来回切换。利用这一点我们在学习数学或者计算机等知识时,可以先用专注模式打头阵,掌握特定的概念和原理。对于一些更复杂的问题,不要急于解决,做点别的事情,让这个问题由大脑带入潜意识,虽然我们不在关注它了,但其实大脑还在后台「消化」这个知识。 2、学好数学和计算机科学最好的方法是“每天进步一点点”,不要担心学的慢,细嚼慢咽反而会让你比那些脑子快的同学学习得更加深入。 3、紧凑的专注模式之后,发散模式就是一种对大脑的奖励。通常有以下激发发散模式的方法:去健身房 慢跑、散步 淋个浴或者泡个澡 听音乐,尤其是纯音乐 睡觉 冥想记忆组块 大脑能够记忆独立内容是有限的,为了让它更高效的工作,我们需要将发散的信息碎片组合起来,这个就叫做记忆组块。构建组块的步骤如下,比如我们想学习一本教材: 1、浏览目录,了解这本教材大致有哪些内容。 2、按顺序,深入了解各个章节的内容。在学习新知识的时候有一个重要的方法可以应用:每完成一个章节,试着回想学习材料,即提取练习,这种效果比单纯阅读材料好得多。再强调一遍,看完内容,一定要自己回想这个章节讲了什么东西,最好用自己的话把内容复述一下。 3、章节结束之后做测试,测试是一种高效的巩固学习的方法。 4、了解知识背后的背景知识。 5、再看目录,将这些内容串联起来,形成具有联系的组块。 改变拖延症 预防和改变拖延的方式就是:养成良好的习惯,用好习惯去对抗拖延。 对于习惯有深刻见解的一本书是《习惯的力量》,这里介绍习惯分为四个部分:信号、反应程序、奖励机制、信念。我们可以从这四个阶段入手解决拖延问题。 信号 找到让自己陷入拖延,大脑出窍的信号。比如我们在学习中,因为想看下手机时间,结果拿起手机就玩了几个小时。找到这个信号,下次学习时我们就把手机放到远离自己的地方。 反应程序 反应程序是身体的一项习惯机能,比如看到床就想躺下,踩着橡胶跑道就想跑步,坐到图书馆就想看书一样。我们需要找到把自己代入拖延的反应程序,用更好的程序去替换它。 奖励机制 习惯的强大之处在于它能造成精神层面的欲望,要想克服之前的欲望,就需要用一个新的奖励覆盖之前的欲望,而不是意味的强迫自己戒掉欲望。 信念 相信自己一定可以克服拖延养成好的习惯,可以利用“心理对照”,想象你达成好习惯所获得的成就来为自己增加信心。 还有一些改变拖延的方法和工具值得尝试: 1、任务清单,把当天或者一个阶段重要的事情用清单列出来,每解决一项就划掉一项,用于提醒自己哪些还没有完成。 2、利用番茄工作法,通过集中注意力学习,然后适当休息,之后再来几个循环,以提高自己的效率。 3、对于复杂的工作可以拆分为几个微小任务,确保自己及时得到反馈,享受完成一项任务的愉悦感。 增强记忆力 记忆是学习中非常重要的一种手段,不要认为那些记忆力强的人天生记忆力就好,每个人的记忆能力相差都是不大的。很多记忆高手在训练之前也跟普通人差不多,所以我们也可以通过训练提升自己的记忆力。 视觉图像 图像对记忆很重要,部分原因在于图像与右脑的视觉中枢直接相连。我们把一个或者几个有联系的内容,通过与一些图像建立联系,会有助于提高我们对这类内容的记忆。 记忆宫殿法 这需要我们对需要记忆的内容,编造出一个“宫殿”来,也可以说是一个你熟悉的场景,将这些内容一点一点串起来。 打造生动形象的比喻或类比 把学习概念有趣化,拟人化也有助于我们记忆内容。 间隔重复 需要记忆的内容不会一直留存在大脑中,需要我们间隔性的重复,以加深印象。这个就是艾宾浩斯记忆法,在事物被遗忘前需要复习巩固它。 肌肉记忆 这里的肌肉说的是写和说带动的手部和嘴部肌肉,我们调动越多的身体器官去参与记忆,越有助于我们加深对内容的记忆。 体育锻炼 最近几项动物和人类的实验发现,规律的锻炼可以让记忆力和学习能力得到实质性的提升。锻炼是有助于促进记忆力相关脑区中新神经元的形成,有氧运动和阻力训练都会对学习和记忆发挥强大的效果。 塑造大脑 塑造大脑有以下方法: 1、在科学、数学、技术领域取得成功的专业人士,逐渐习得的一个特质就是学会如何组块,即:提炼关键思想。 2、对于复杂的知识进行简化,并给这些抽象概念赋予生命。一位获得金苹果奖的化学高级讲师说:学习有机化学的难度和去认识一些新人物比起来没什么两样。每一个元素都有自己独特的个性。你越了解它们的性格,就越能读懂它们的处境,并能预知他们在相互作用中会产生的结果3、把所学的知识迁移到新背景中。数学能应用到物理,那能否应用化学或者别的学科,工作中的一些方法能否应用到生活中去。 4、自主学习是一种最深入、最有效的学习方式。能通过自己努力解答的问题就尽量不要向外界寻求答案。 5、锻炼自己去接触那些你敬仰的人,往往他们一句话就会改变你的未来。但要记住好的导师通常都是大忙人,请爱惜他们的时间。 6、考试本身就是一种很好的学习经历。即使没有考试,我们也可以创造一些考试场景用于更好的学习。这里引申一个由难入简做题法。 该方法建议:对于一场考试,我们应该从最难的题目做起,通常从后往前的顺序就是先难后易。对于难题如果我们一两分钟仍没有一丝头绪,就跳过去做下一题,没有思路再跳一题,如果中间突然想到了某道难题的解决思路就赶紧记下来,做完简单题之后再回去处理。 这个方法的核心思想就是把难题的内容让大脑先有个印象,后面做更简单题时,大脑潜意识还留存着难题的内容,说不定这时就灵光乍现,想出了难题的解决思路。相对而言这样可以对大脑利用最大化。 十个学习法则 这是本书最后一章的内容,是对以上内容的总结提炼。 1、运用回想。读完一篇文章,回想一下它讲的到底是什么。 2、自我测试。通过测试检验自己的学习成果。 3、对问题进行组块。搭建组块的过程就是理解问题、练习解题方法的过程。 4、间隔开重复动作。不要过多重复一件事情。 5、在练习中交替使用不同解题技巧。换不同思路去解决问题。 6、注意休息。学习累了就休息,善用大脑潜意识。 7、使用解释性的提问和简单类比。对于难懂的概念先类比一个熟悉的类似的概念,这有助于理解。 8、专注。每个人的时间是平等的,但注意力可不平等,有注意力的时间才是更有价值的,我们需要想办法让自己更专注。番茄工作法就是一种很好的帮助我们专注的方法。 9、困难的事情最先做。最清醒的时候,要去做一天中最困难的事情。 10、心理对照。想象过去的你,对比通过学习能够成就的那个自己。
写在2020最后一天
- 31 Dec, 2020
2020年是很特别的一年,经历了记忆以来最严重的一场疫情,而且完全可以预见的是它还将继续祸害着2021年的我们。不管这一年多艰难吧,都走过来了,在2020的最后一天,在新年即将到来之际,简单总结下自己这一年的经历。生活 疫情开局 春节回老家,正赶上疫情影响,因为担心遭遇封城,我跟媳妇就早早返回了北京,那是正月初二的晚上。赶上北京这边戒严,春节假期的几天及之后的两个月我俩都被迫窝在了霍营的小出租屋里。一周出去买一次东西,囤土豆,囤白菜,面粉和米都买的大袋的。这期间尝试了油条、包子、饺子的做法。喜欢吃手抓饼,就在网上买冷冻的饼,自己摊着吃,到后来我们清理手抓饼的箱子,发现足足吃了6箱。。。 疫情生存指南除了自给自足,还需要学会抢购口罩。当时普通医用口罩被炒到4-5块一个,还很不容易买到,因为大量口罩都被优先征用到抗疫一线了。我拖朋友在外地买回来100个,当我用塑料袋拎着100个口罩回家时,路上有个老人拦手问我,你这口罩哪买的?我没敢说买的,只说:朋友送的。坐公交的路上,总感觉有人盯着我装口罩的袋子看。我当时就有种感觉,自己拎着的不是口罩而是现金。 因疫情关系,在家远程办公了将近两个月。没有扩展显示器和升降椅,正常的午休也很难保持到固定时间,这让我很是怀念公司办公的感觉。好在混乱的生活工作节奏没有持续太久,就基本上恢复正常了。 离职 6月份开始准备换工作,7月份离职,离开待了两年多的乐信圣文。临走那天,看了一眼身后的熟悉的楼层,既有对这段工作经历深深的感激之情,也有摆脱焦虑的如释重负。在乐信这两年是我职业发展非常关键的两年,学会了独立思考,形成了一些属于自己开发原则: 1、文档是第一手资料 遇到问题,不要上来就打开搜索引擎,那很可能会浪费时间且养成惰性。很多问题,很多偶发现象,文档里都是有写的,多翻翻文档很可能就会发现惊喜。 2、刨根问底研究明白 这来源于我看的一篇博客,它的故事是:作者在开发中有时会用到lamda,第一次时它利用搜索引擎,找到了处理方案,第二次再遇到还是不能自己解决仍需要借助于搜索。等第三次第四次时他反应过来,为什么不一次研究明白呢?于是花了将近两天时间,把这一概念的种种用法,相关知识点研究的明明白白,之后再有类似问题都可以完全自己解决了。 我深受启发,很多时候我也是一直在做重复工作,知道这里重要,但懒得去深入了解,总想省事,但结果是如果第一次我们就把那个东西弄透彻才是最省事的方案。 3、阅读计算机经典著作 知乎上有个挺火的问题:你的编程能力从什么时候开始突飞猛进?,很多回答都是在阅读了一些经典计算机著作或编写了大量代码之后发生了这种变化。不得不说,阅读那些经典著作真的很重要,我看完《重构:改善既有代码设计》这本书之后,慢慢建立了评判代码好坏的标准,以及看到坏味道的代码就想去改改的冲动👀 之后又看了《程序员修炼之道》《设计模式》,我感觉自己变的更强了。 4、学习榜样 从身边找到一个优秀的人,并向他学习也是很好的一种进步方式。我是先找到的学习对象,后来才意识到这真的有助于我进步。当时同组的超哥,编程水平一流,各个编程技巧熟练应用,思考问题井井有条,还有投入工作那种忘我的状态,直到现在他都是我遇到过的最优秀的开发者。像现在的坚持命令行git,读文档,学习后端知识都是受他影响。 5、拓宽知识边界 试着学一些其他编程语言,了解他们的语言特性,脚本语言里的Python、Ruby,编译语言的Java都是比较适合作为iOS开发的扩展编程语言。 新工作 7月份来到爱奇艺,开始了另一段职业生涯。目前虽待的时间还不算长,对于这里的工作节奏还是很喜欢的。有相对合理的版本节奏和工作弹性,有一定的自由空间可以发挥自己的想法。同事也很nice,富有热情,热爱工作,这是我很喜欢的工作氛围。更重要的是leader能给我职业发展的指导,告诉我认知层次的缺失,从哪些方面可以提升自己,这对现阶段的我来说尤为重要。 总之,在这里还有很大的进步空间,2021年加油吧。 人生大事 今年完成了人生中的两件大事,领证和买房。跟媳妇认识七年,相恋三年,我们选在今年的七夕领结婚证。后来在B站看到很多up主在今年领证结婚并且拍了视频记录,感觉很浪漫,有些后悔我们领证的经历没有记录下来。这让我萌生了另一个想法:如果现在记录下我们日常的生活,5年、10年之后再看应该也会很不一样吧。所以最近的一段时间,早起之后,我俩都会一起录段视频,讲一讲最近的感受,以及当天的计划,到第二天时回顾前一天,再录当天内容。已经坚持了半个月,希望这能够成为我俩的浪漫之事。 9月份,我俩靠自己能力在天津买了属于我们的第一套房子,非常开心,期待搬进新家的那一天。 10月份拍了婚纱照,计划春节前举办婚礼,看现在的疫情,真不确定那时会变成什么样,祈祷一切顺利吧。 一些有意思的事情 公众号和博客 在年中换工作期间,重新拾起了公众号,将名称改为:iOS成长之路,定位于iOS技术文章。当时公众号只有8个粉丝,我还定了一个大目标:年底公众号粉丝达到500个。前期粉丝真是涨的很慢,我一度认为这个目标可能要凉凉了,直到后来发了那篇面试总结,被很多技术号转载,很快就涨到了500+,原来面试文章就是爆款!弄明白了这个,但我也并没有再发面试相关的内容,一是不再面试了,很难找到真实的素材,二是面试属于热点,但那不属于技术层面的东西,定位于技术,还是要回归技术话题的。 今年公众号发了19篇原创文章,博客保持同步,勉强及格吧。随着公众号人数增多,组建了微信群,我每天在群里给大家分享开发概念讲解和英语翻译技巧(转载)。这也反向督促了我不断学习,成为了一件小有仪式感的事情,即使当天工作任务很重,我也会抽出时间整理学习资料。 个人博客 进行了界面优化,尝试了多套主题之后,最终选择了这个Icarus的主题。为了提高国内的访问速度,购买了腾讯云服务器,并做了域名备案。一番折腾之后,一个小站该有的东西基本齐全了,看着还像那么回事。 时间管理 去年有个目标是提高时间管理能力,因为总有很多想法,又总是无法抽出足够的时间去实施。今年也没有太琢磨时间管理的事,但却做了一件事使得时间富足起来,那就是:戒掉游戏,准确的说是戒掉王者荣耀。 作为王者荣耀16年年初入坑的老玩家,我曾把大量的时间都耗费在了这个游戏里面。我自控力有时真的很差,本来应该适当游戏娱乐一下,但总是控制不住输了想赢回来,赢了想趁手感好再来一局,好几次直接玩到天亮,被放防沉迷限制才放下游戏休息。还容易情绪化,我自认为游戏理解还可以,遇到那种素质和操作差的队友经常互喷起来,然后连着很长一段时间情绪都受游戏影响,因为游戏也被媳妇说过很多次。这种状态肯定是不好的,于是在某天跟队友峡谷对喷过后,我决心退游,到现在应该已经快半年没玩了。没有了王者荣耀,发现自己腰也不疼了,腿也不酸了,一口气可以爬。。。哦,我是住一楼。总之空闲时间被释放了一大部分,这段时间就用来看看书,追追《神盾局特工》,写写博客,很舒服的且可以被自己控制的节奏。 阅读 今年阅读量确有提升,我的阅读主要是从手机和纸质书两方面来。 手机上看了: 《如何成为一个有趣的人》:中心思想是不要努力求”认同“,而要打造完全属于自己的稳固的世界,并敞开欢迎别人来游走参观,而构建自己世界的过程就会产生一些有趣的事情。这本书对我之后决定继续维护公众号也有一定的推动作用。 《从0到1》《算法图解》《我不》《微习惯》《编写可读代码的艺术》《微习惯》 《图说世界格局》:很有意思的一本书,可以了解各个大国的产业结构,发展历史,最主要的是对中东局势有了个大概了解,那里为什么持续的动荡,处理地里位置的特殊,还有很大一部分原因是各个大国之间的博弈,互相牵制。 《RxSwift-Reactive Programming with Swift》:看过的第一本纯英文技术书,也有点像文档。RxSwift相关资料真不多,这是可以作为官方教材的高质量文档,如果想学好RxSwift,可以列为必读。 《SwiftUI 与 Combine编程》:喵神这个pdf小书,把我对响应式的理解完全串起来了。我虽然一行Combine代码都没写过,但我感觉已经对它很了解了。 相比来说,纸质书还是更让人记忆深刻一些,看的有这几本: 《送你一颗子弹》:刘瑜的散文随笔集。这是我媳妇的阅读书目,有那么一段时间,每天早起读十几页,很轻松,很有趣的感觉。 《代码的未来》:前半部分看的很认真,后面因为很多知识基础跟不上,没有读太细。但这本书还是挺推荐的,可以用于扩展知识面,了解多种语言的特性及特征性问题不同语言的处理方式。 《C++程序设计》:是谭浩强那本,在整理杂物时发现了它,突然有股冲突要再看一看当年的教材。重读一遍确实获取到了一些不一样的感受,有些内容依然受用。另外对于网上对谭浩强的质疑,我部分认同,这本书拔高了新手入门计算机的门槛,特别是对指针的讲解和习题的设置,现在的我看来有时也会懵懵的。 《程序是怎样跑起来的》:可以补充一些基础知识,但没感觉有太多收获。姊妹篇的《计算机是怎么跑起来的》和《网络是怎么连接的》还在看,暂不评价。 《跟戴铭学iOS编程》:不推荐 《九阴真经-iOS黑客攻防秘籍》:这本书是掘金征文比赛时送的。这里有一点感受,阅读纸质书的时候,其排版和印刷质量真的影响一个人的阅读体验。在这之前已经买了一本逆向书籍《iOS应用逆向与安全》,当时这本书有点没看进去,但是翻《九阴真经》却能激起我阅读的欲望,后来我总结,应该是这本《九阴真经》印刷质量更好(两本都是正版),纸张更好,更白,确实是这样的0。0 《月亮与六便士》:买的是一本盗版书,印刷质量很差,也导致自己阅读兴趣不高,但还是逼迫着自己看完了,当然是没有太多感触,好吧,我浪费了一本好书。 《QBQ问题背后的问题》:公司领导要求看的,个人感觉还不错,还写了一篇总结性的书评,如果我当了领导我也要求下属看这本书😂 计划回顾与总结 回顾下2020年年初制定的OKR: O1:精进技术栈KR 完成进度 总结刷20道经典LeetCode题目,输出2篇解题思路的文章 50% 刷了一部分题,也达到了20题,但是没有输出文章学习前端知识,优化博客小站 50% 前端只学了很少一部分,博客尝试了三套主题,目前这个是最满意的,中间还升级过两次输出5篇对计算机知识总结的文章 100% 纯技术类文章是超过5篇的维护一个Swift库,用于筛选项目中不用的文件 10% 年初的计划,调研了一段时间时间之后又放弃了O2:个人成长KR 完成进度 总结公众号粉丝达到500 200% 达到1000+学习基金知识,分析对比10种基金的数据表现 10% 对理财还是没提起兴趣0。0全年跑步里程400公里 50% 咕咚记录的有200公里,年中一段时间跑步还是规律,去到爱奇艺就没了跑步氛围阅读15本书 100% 正常完成培养时间规划能力,总结并践行一份时间规划清单 60% 这个目标不可衡量,但是相对于去年时间规划能力还是有不少提升的2021年OKR O1:个人成长 KR1:时间规划能力再提升,完整记录20天以上的时间开销 KR2:阅读20本书,选择其中5本写出读后感 KR3:全年跑步里程400公里 KR4:研究3只头部基金,自己做一次有计划的尝试,最终收益能高于市场平均线 KR5:提升代码阅读量,阅读3个苹果底层库,并写总结分享 KR6:提升代码书写量,非工作内项目达到20万行。有一个长期维护的开源库,对2-3个经典计算机问题,手写代码实现 O2:输出更多优质内容 KR1:公众号粉丝达到5000 KR2:公众号收入能抵消博客服务器及域名的支出 KR3:输出30+篇博客 KR4:摸鱼周报出15+期 注:OKR不一定是固定不变的,我现在的计划也不应该决定之后一年的规划。理想情况应该是每隔一段时间去审视一次计划完成情况,如果发现有另外的计划,或者某项计划不好,都是可以调整的。
HLS及M3U8介绍
背景 MP4是我们常见的视频格式,往往我们在播放服务器视频时直接就是请求的MP4视频源。但其实这样并不好,MP4头文件[ftyp+moov]较大,初始化的播放需要下载完整的头文件并进行解析,之后再下载一定长度的可播视频片段才能进行播放。另外随着视频尺寸的增大头文件也会不断变大,这个初始播放时间也会更长。针对这种情况需要一种能加快视频初始解析的方法,HLS就是苹果提出的用于解决这种问题的方案。HLS HLS为HTTP Live Streaming的缩写,是由苹果公司提出的基于HTTP的流媒体网络传输协议,它可以同时支持直播和点播,还支持多清晰度、音视频双轨、字幕等功能。它的原理是将一整条视频分成多段小的视频,完整的播放是由这一个个片段拼接而成的。 HLS在移动端使用很广泛,当前支持HLS协议的客户端有:iOS 3.0及以上,AVPlayer原生支持HLS Android 3.0及以上 Adobe Flash Player 11.0及以上它的大致原理是这样的:1、采集音视频 2、在服务器编码音视频 3、编码后以MPEG-2的传输串流形式交由切片器(Stream Segmenter) 4、切片器创建索引文件和ts播放列表,索引文件用于指示音视频位置,ts为真实的多媒体片段 5、将上一步资源放到HTTP服务器上 6、客户端请求该索引文件进行播放,可以通过索引文件找到播放内容 参考资料:HTTP Live Streaming Document M3U8 实现HLS的一个关键步骤是上面的第四步,即索引文件和ts播放列表的组织。这里用到的就是M3U8格式。M3U8是Unicode版本的M3U,8代表使用的是UTF-8编码,M3U和M3U8都是多媒体列表的文件格式。 接下来我们以一条WWDC里的视频为例,看下M3U8格式是什么样子的,下文展示的并非M3U8格式所有字段,但会包含常用的字段,也足以帮助我们理解M3U8这一格式。 播放页面为:https://developer.apple.com/videos/play/wwdc2019/507 ,通过Charles进行抓包,我们可以得到视频播放过程中的M3U8文件。在分析这个路径格式前,我们需要知道 M3U8 是有两种格式的,一种是作为主播放列表(Master Playlist)存在,它里面包含了音视频、字幕的一些说明和路径,主列表指示的路径是另一个M3U8文件,即另一个格式,作为播放存在的,它里面也有路径,指示的是片段(ts)文件,片段文件是真正的多媒体内容。 看抓包内容,hls_vod_mvp.m3u8为主列表文件,上面的0640.m3u8为视频列表文件。 M3U8格式说明 有时做测试,或者一些特殊情况时我们可能需要手动修改M3U8文件内容,所以需要对它的格式有一定的了解。该格式的定义写在RFC 8216号文件里,以下是一些注意事项:M3U8文件必须以UTF-8进行编码,不能使用 Byte Order Mark(BOM)字节序, 不能包含 utf-8 控制字符(U+0000 ~ U_001F 和 U+007F ~ u+009F) M3U8文件内容的每一行要么是空行,要么是一个URI,要么是以#开头的字符串,不能出现空白字符。 内置标签都是#EXT开头的字符串,大小写敏感。 URI为内容路径,可以是相对路径也可以是绝对路径Master M3U8 列表文件主M3U8索引文件,一般用于指定多个索引源。我们先分析下该主m3u8文件hls_vod_mvp.m3u8的内容,它的头部是这样的 头部格式 #EXTM3U #EXT-X-VERSION:7 #EXT-X-INDEPENDENT-SEGMENTS#EXTM3U表明该文件是一个M3U格式,所有的M3U格式文件都应该把该内容放置到第一行。 #EXT-X-VERSIOn指示播放列表的兼容版本,当前为7。 #EXT-X-INDEPENDENT-SEGMENTS该标签表明对于一个媒体片段中的所有媒体样本均可独立进行解码,而无须依赖其他媒体片段信息。 字幕格式 再往下的内容是一些字幕说明,字幕内容不是必须的。 #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="subtitles/eng/prog_index.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subsC",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="subtitles/engc/prog_index.m3u8"#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Japanese",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="jpn",URI="subtitles/jpn/prog_index.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subsC",NAME="Japanese",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="jpn",URI="subtitles/jpnc/prog_index.m3u8"#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Chinese",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="zho",URI="subtitles/zho/prog_index.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subsC",NAME="Chinese",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="zho",URI="subtitles/zhoc/prog_index.m3u8"#EXT-X-MEDIA用于指定相同内容的多语言媒体列表资源。 TYPE为资源类型,可选内容有:AUDIO、VIDEO、SUBTITLES、CLOSED-CAPTIONS。 上面内容设置的是TYPE=SUBTITLES,即为字幕类型。 GROUP-ID为多语言翻译所属组,为必选参数 NAME为翻译流可读的描述信息,该值对应AVMediaSelectionOption的displayName。 DEFAULT,AUTOSELECT,FORCED为三个BOOL值分别对应如果缺少必要信息时是否默认选中该翻译流,用户没有显示设置时播放该播放流,FORCED只针对字幕类型有效,用于标记当前自动选择该翻译流。 LANGUAGE用于指定语言类型,它是根据[ISO 639 语言码](https://www.w3.org/WAI/ER/WD-AERT/iso639.htm “ISO 639 语言码”)标准设置的。系统默认的播放器在选择字幕时,展示的字幕列表名称是根据这个值设定的。 URI为该资源的定位信息,在这里其对应的是一条字幕的M3U8文件。subtitles/eng/prog_index.m3u8是一个相对路径, 通过以上信息,我们可以分析出上面内容的含义为:当前视频支持三种字幕:英文,日文,中文。但每种语言都有两条EXT-X-MEDIA信息,他们的区别是分组不同,一个在subs分组,一个在subsC分组。为啥有两个分组,这个后面再说。 视频格式 再往下看,为视频内容的索引: #EXT-X-STREAM-INF:BANDWIDTH=827299,AVERAGE-BANDWIDTH=747464,CODECS="avc1.64001f,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=29.970,AUDIO="program_audio",SUBTITLES="subs" 0640/0640.m3u8 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=360849,AVERAGE-BANDWIDTH=320932,CODECS="avc1.64001f",RESOLUTION=640x360,URI="0640/0640_I-Frame.m3u8"EXT-X-STREAM-INF:该属性指定了一个备份源,即视频播放路径和一些视频的信息,以下是对应内容的配置: BANDWIDTH为峰值比特率, 827299,为827299bit/s,即最高峰值时每秒消耗流量101KB。 AVERAGE-BANDWIDTH为平均比特率,747464 CODECS为编码信息,avc1.64001f,mp4a.40.2,avc代表的是h264编码格式,后面的64001f,是由16进制表示的编码参数,64,00,1f分别代表三个不同的参数值。mp4a是一种音频编码格式,后面的40.2代表音频的编码参数。 RESOLUTION为视频分辨率,当前一条视频源分辨率为640x360。 FRAME-RATE为最大帧率,29.970 代表当前播放的最大帧率为每秒29.970帧。 AUDIO是音频所在组,program_audio为对应音频组的名称。 SUBTITLES指示对应的字幕分组,subs为对应字幕组的名称。上面的字幕信息有个GROUP-ID,该值与之对应。 URI为内容路径,0640/0640.m3u8对应的就是该视频源的m3u8文件路径。这个可以在抓包信息里看到。 在EXT-X-STREAM-INF下面是EXT-X-I-FRAME-STREAM-INF,表示播放列表文件中包含的多媒体资源的I帧(关键帧)。因为I帧只是一个画面,所以它不包含音频内容,其余参数跟视频内容格式一致。 在之后就是对应不同分别率的视频源,1920x1080、1280x720、960x540、480x270,因为HLS会根据网络情况自行切换清晰度,所以一般会准备多个清晰度以供选择。根据抓包数据分析,播放的第一个片段是640清晰度的,之后的第2-8个片段为480清晰度,再之后又切换到了640清晰度。 音频格式 再往下看是对应音频的索引 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="program_audio",LANGUAGE="eng",NAME="Alternate Audio",AUTOSELECT=YES,DEFAULT=YES,URI="audio1/audio1.m3u8"#EXT-X-MEDIA上面出现过,为多语言没提列表。 TYPE=AUDIO,这次类型为音频。 GROUP-ID为分组ID,对应EXT-X-STREAM-INF里的AUDIO内容。 URI=audio1/audio1.m3u8对应音频路径。 不同编码格式的备用源 在该主M3U8文件中我们还能看到一条640分辨率的视频源,它与上面的640分辨率还不一样,它的内容是这样的: #EXT-X-STREAM-INF:BANDWIDTH=1922391,AVERAGE-BANDWIDTH=1276855,VIDEO-RANGE=SDR,CODECS="hvc1.2.4.H150.B0,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=29.970,AUDIO="program_audio_0",SUBTITLES="subsC" 0640c/prog_index.m3u8 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=1922391,AVERAGE-BANDWIDTH=1276855,CODECS="hvc1.2.4.H150.B0",RESOLUTION=640x360,URI="0640c/iframe_index.m3u8"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="program_audio_0",LANGUAGE="eng",NAME="Alternate Audio",AUTOSELECT=YES,DEFAULT=YES,URI="audioc/prog_index.m3u8"CODECS编码格式为hvc1.2.4.H150.B0,mp4a.40.2,音频编码格式没变,但视频编码格式变了。hvc1是HEVC(H265)编码格式里的一种,它是由苹果推出的新一代视频编码格式,因为兼容性问题很多客户端还无法解析该格式,所以并不是很普及,该格式的视频源出现在这里应该是一种备用。对比相同分辨率的两条内容,还能发现hvc1格式会比avc1格式比特率更高,这说明相同分辨率下hvc1的内容更大,avc1的压缩比更高。 对应hvc1格式的视频源,它的字幕内容分组和音频内容分组也都变了,这也是为什么上面的字幕同一语种会有两份,他们分别对应avc1和hvc1格式的视频源。 M3U8的主列表就这些内容了,该条内容的音视频是分开处理的,其实也可以合在一起。 包含媒体资料的m3u8文件 以0640.m3u8这个文件为例 #EXTM3U #EXT-X-VERSION:4 #EXT-X-TARGETDURATION:7 #EXT-X-MEDIA-SEQUENCE:1 #EXT-X-PLAYLIST-TYPE:VOD #EXTINF:6.006, 0640_00001.ts #EXTINF:6.006, 0640_00002.ts #EXTINF:6.006, 0640_00003.ts .... #EXT-X-ENDLIST#EXTM3U,#EXT-X-VERSION,分别为M3U文件头和兼容版本号,这种格式是早期的所以版本号比主文件低一些。 EXT-X-TARGETDURATION代表每个播放片段的最大时长,7,代表7秒,该目录下的片段不能超过7s。 EXT-X-MEDIA-SEQUENCE代表播放列表的第一个片段序号,1,代表播放片段是从1开始的。 #EXTINF代表片段的时长,6.006表示当前片段为6.006s。视频总时长的信息是通过该值累加获取的。 0640_00001.ts为片段的相对路径,ts文件代表一段视频或者音频,它可以是ts,mp4,aac等格式。因为前面已经指定了从1开始,所以这里序号是0640_00001。 #EXT-X-ENDLIST为媒体内容的结束标识,因为m3u8即可以表示点播也可以表示直播,区分点播还是直播就看文件末尾是否有这个标识符。如果没有的话就代表直播,播放会一直持续下去。 音频文件audio1.m3u8,字幕文件pro_index.m3u8的内容也是类似的,区别在于他们的切片内容一个是acc的音频文件,一个是webvtt的字幕文件。 包含切片内容的M3U8也可以作为独立的视频链接存在,这时切片内容就需要同时包含音视频内容了。 文件加密 HLS协议支持加密,如果索引文件中包含了一个密钥文件的信息,那接下来的媒体文件就必须使用密钥解密后才能解密打开了。当前的 HLS 支持使用16-octet 类型密钥的 AES-128 加密。这个密钥格式是一个由这在二进制格式中的16个八进制组的数组打包而成的。 加密的配置模式通常包含三种: 模式一:允许你在磁盘上制定一个密钥文件路径,切片器会在索引文件中插入存在的密钥文件的 URL。所有的媒体文件都使用该密钥进行加密。 模式二:切片器会生成一个随机密钥文件,将它保存在指定的路径,并在索引文件中引用它。所有的媒体文件都会使用这个随机密钥进行加密。 模式三:每 n 个片段生成一个随机密钥文件,并保存到指定的位置,在索引中引用它。这个模式的密钥处于轮流加密状态。每一组 n 个片段文件会使用不同的密钥加密。 参考:HLS-iOS视频播放服务架构深入探究(一)
一位iOS开发者的进阶之旅
背景 这篇文章来源于v2ex上的一个帖子:"iOS开发有什么国人写的比较好的书籍推荐?"(原文链接)。这里汇总的基本都是lujie2012的回答,另外我还附带了一些他与别人的讨论内容。虽然帖子题目是推荐iOS书籍,但设计内容已经超出了这个题目,在我看来其中还迸发出很多有意思的观点,所以就想把内容整理出来。在经过其本人同意之后,有了如下内容,希望对大家有所帮助。正文 推荐书籍 本人之前是从 Object-C 开始入门 iOS,全部自我学习写项目找工作混饭吃。因为之前犯过大部分 iOS 开发者犯过的错,没有深入学习知识,没有获得长进。我决心重新在 iOS 方向深入认真的投入一次,看自己可以扎入的有多深。 我开始没有关注 Swift,现在 Swift 成熟了,所以决定从头开始学习 iOS 的一切东西。后面 19 年开启 Swift,二次学习 iOS 开发,也感觉到突破了自己头顶那块天花板。我10个月看了10多本自己买的书,还为业务补充了好多知识,有坚持不下去的时候,但是就想把项目上线。最后克服了困难,回过头有收获和总结,我现在爱上了看技术书籍,钻的越细越发现有趣,也想去看算法了。类似写论文一样,没有秘密,直接分析到底。目前在模块化接入 Flutter,React Native,两端开发速度不一样,某些功能由 H5 做,我们就很闲了。现在的目标差不多就是把 Flutter 玩转,基本是二次从头学习 iOS 花了 1 年时间告一段落。 期间啃的书,有些是objccn里喵神的书,这些书对iOS开发帮助还是挺大的,其余都是些比较经典的技术书。这些书我都看过一遍。(以下是笔者对书籍汇总成的一个表格)分类 书名Objective-C Objective-C程序设计Objective-C高级编程Effective Objective-C 2.0Swift Swift权威指南Swifter 100个Swift2 开发必备TipSwift进阶Swift常用算法函数式SwiftiOS iOS数据库应用高级编程iOS动画核心技术与案例实战iOS Auto Layout 开发秘籍高性能iOS应用开发iOS测试指南iOS应用逆向工程LLVM COOKBOOK 中文版AV Foundation开发秘籍Core Data 应用开发实践指南Core Data其他计算机书籍 SQLITE权威指南图解数据结构与算法数据结构与算法经典问题解析(Java语言描述)数据结构教程Java 9编程参考官方大全Java并发编程实战深入理解Java虚拟机深入理解NginxTomcat内核设计剖析C Primer Plus 中文版音视频开发进阶指南另外需要补充的内容还有,tomcat 源码、nginx 源码、关于 HTTP 协议后端相关的东西。很多东西,写不成书,因为本身没有多少内容,有些东西只有国外有,但是 400 块,没有翻译版本。例如关于布局,从frame-》 constriants-》 archor-》到 StackView 其实苹果也是在不断的提供解决方案,目前最好的布局方案就是 stackview + anchor + constraint,但是没有这么一本介绍这些内容的书,我也是翻边了官方文档,在各自项目中看到蛛丝马迹去思考对比的。 其他学习途径老话长谈,最好的资料是苹果官方开发者文档,官方的 WWDC Session。为了更深入理解苹果产品,我把历年的 WWDC 都看了一遍,从 2007 年 iOS6到2017年发布iphoneX,每年差不多 100 个介绍最新技术和解决方案的视频,而且内容含金量还非常高。哈哈,学到了很多苹果产品使用高级用法,体验了好多产品介绍。iOS 开发者一定要关注 boxue 网站,可以的话买个终身会员,下载博主的 app,看看他的项目架构,里面的视频学起来。我 boxue 完成了 156 个 iOS 的视频,终身会员。boxue 的视频看一遍懂一点点,过段时间在看,又有一点点理解,例如 RxSwift,Protocol,Sequence 这些。另外可以结合 objccn 里喵神的那三本书,我是买了一起看的,它会使你对 swift 的写法和运用更高阶,让你的思想更接近 apple 官方或者大牛。例如序列化,持久化,函数编程,这些流行的概念可以带入项目中。 真心经验分享,中文书籍的东西只能看到 30%的技术,英文书籍的东西可以看到 50%,还有 50%在官方英文文档,各位一定要学好英语,在官方找一手资料和解决方案。这样就慢慢可以成为 Contributor,为社区贡献代码和解决方案,成为开拓者了。不然永远只是旁观者,玩技术,就希望把它玩到极致对吧,好比玩音乐,玩音乐的的境界可以看 Vista 2002 年演唱会。 另外,以上学习最难的是什么,是英语水平!!!! 我现在每天学习英语,英语水平上来了,感觉发动机动力杠杠的。二手知识基本过时,想要成为一流,那么英语水平就得要一流,差不多雅思 7 分这个水平。写代码看资料,觉得不是一个等级哦。感谢公司提供的英语学习网站,https://english-bell.com.tw/default.aspx 。我大概充了 1 万 8,坚持每天 25 分钟的一节课学习 DME,现在学习了 300 天,学习英语推荐:购买朗文当代+DLL ebook + English bell,使用 SKype 上课,菲律宾老师 24 小时可以学习。 更多的讨论 如果学 iOS 都没有用过 CoreData,或者 Sqlite 进行持久化,那么几乎不可能成为高阶程序员。CoreData 固然难用,难学,但是我个人认为必须耐心学完,必须每个项目都使用,里面的设计思想和理论都很有用,每年的 WWDC 都有 session 讲解这一块。 iOS 本身知识的书籍不是很多,更多的是需要你去查阅官方文档去理解学习。有很多内容是涉及视图绘制技术,音频,网络这些,他们本身是最基础的东西,但是苹果没有给你知识辅导,不自学这些,只会使用苹果API,永远都只懂皮毛。觉得 iOS 端没有东西,其实东西多的很,例如 socket 编程,什么语言都可以实现,什么平台都有,但是 iOS 没有告诉你这个很重要,你就不学,不深入,调用一下 API 就好了。其实这才是真正的技术,再深挖就是 TCP/IP 协议,蓝牙协议,学编程要不要学这个呢?学 iOS 要不要学这些呢,肯定的,做程序员,不学这些就永远停留在初中级水平,35岁等着被淘汰。 当你学了这些底层知识,例如7层网络协议,就会明白什么技术和语言都是起始于二进制。字节编码,变成 Unicode,变成语言。那么语言写好的代码,变成什么呢?写好的代码变成汇编语言、command、再变成二进制,用户安装二进制,二进制再在运行环境变成 code,再执行逻辑。不懂编译器原理,怎么优化代码,怎么去做安全加固,反 hook ? 如此思考,HTTP 到数据的展示是最简单的编程工作。最近我在想一切的数据通讯,例如家里的电视遥控器,怎么做数据交互,转换,传播。光波,红外线,wifi,5G,想象最底层的实现和全路径思考才有意思。所以,计算本科教育虽然水,但是现在想想那时候用的滤波器和调制解调器,上模电不知道干什么,现在工作后,慢慢都明白了,这就是本科专业教育的本质和作用。系统的教育才是有意义的,为什么学高数,现在才多少明白了些。 对了,你对加密了解清楚吗? HTPPS 怎么实现的知道吗,看过源码实现吗?程序员对加密都不清楚,那真的不算高阶程序员。openSSL 库,都可以让你玩很久。iOS 也好,Android 也好,只是各自技术封装的一个平台,用于解决一定的问题。当你看透本质了解一个平台真正的东西,那么你也将知道技术的发展都是有着相似的规律的。 有个题外话,iOS 有出路么?你把他当成 HTTP + 页面来玩天花板确实很低,但你以iOS为出发点,研究整个系统体系,那天花板无限高。未来 20 年,领先技术和先进应用基本都会是移动领域带领的变革。 不要觉得iOS开发上限很低,iOS 岗位也很细的,你可以玩日志系统,埋点系统,推送系统,crash 系统,socket 聊天系统,实时在线系统,视频系统,相机系统,地理位置系统,三维系统,AR 系统,声音系统,安全系统。玩的东西多了呢!没有饭吃?没有前途?大部分人是岗位,是项目驱动,我也是,没有事情没有遇到难题就不会进步。但还是要自我驱动,不断往上拔,才能离那个最高处的天花板更近一些。 好比玩音乐,写歌作曲,你能随心所欲组合,基本是就是高级人才了。写代码一样,要玩它,玩技术,不然白费了那些年的教育。对于业务和技术,等你技术积累到一定程度,我个人倾向于先做技术专家靠谱一点。谁是榜样呢? JSPatch 的作者就是榜样,被挖到蚂蚁 P8 !时间对每个人都是公平的,记着不要重复劳动,要迭代你的技术,不断思考。看最底层的书,思考最底层的原理,你就不会迷茫。 问答环节 问:CoreData 有什么特别好的场景使用吗,我做了五年了,都是用的 sqlite 。答:CoreData 最大的好处是他们的设计思想和结构,可以买来 《CoreData 应用开发与实践》+ 《Core Data》 看看。我也是看了这两本才明白 CoreData 真正的含义,但它也有个很大的缺陷:没有加密!!!!! 大部分项目采用 Sqlite 是为了加密!本质 CoreData 底层也是 Sqlite,它就是对Sqlite的一层封装。你想想手机相册 1 万多张照片,他们的存储和检索,不知相册,官方很多APP应该都用了 CoreData 来实现。会玩 CoreData,肯定会玩 Sqlite 。最好在项目里使用,用着用着就熟练了。Sqlite 做版本管理和迁移更方便,直接 SQL 操作数据库。我的建议是最好两者都学一下,都用起来。问:我也一直在学习前端跟 iOS,为以后做独立开发者进行技术储备。但我从来不去研究背后深入的技术原理,CSAPP(Computer Systems A Programmer's Perspective 中译:深入理解计算机系统) 包括操作系统相关的书我至少读了 3 遍,但这些跟 IOS 开发基本上半毛钱关系都没有,因为 APP 跟操作系统原理至少离了十万八千里的距离,另外就是这些底层知识根本用不上,大部分独立开发都是业务驱动的,有钱才有技术研究的需求,除了音频、视频等特殊算法场景,大部分技术方面的需求都是业务驱动的 UI 交互跟业务计算。答:不争辩,我之前想说明一般程序员和高阶程序员解决问题思路问题。学习 7 层协议,不是造轮子,是知道水的源头。平台语言个有喜爱,萝卜白菜各有所爱,兴趣是最好的老师,好奇心是最好的动力。小程序,Weex,React 不是不会写,API 文档看一下,组件模块用一用,市面上什么样的 APP 搞不定?但是性能优化,高级特性,没有足够功力你能搞定?前端目前大量时间涉及 UI 开发,后端需要算法,如果不自己去补充知识,那么所谓大前端天花板当然低。阿里前端高 P 多还是后端高 P多,当然是后端高 P 多啊。But,不管哪个方向,最重要是成为专家。成为专家只看到自己项目范围内的知识肯定不够,前端不能看后端知识吗?我把 tomcat,ngnix,spring 源码都看过,当然也忘记了,但是我就是想知道数据通道怎么建立,TCP 怎么维护。google 牛皮就是发现目前协议不行,自己改协议、加密算法、HTTP2 通信、消息协议、TCP 协议,它敢于创新和实践。目前的我相对业务和赚钱,我更关注技术,只会应用技术是大部分人,but 要成为专家只有极少数人才可以做到,因为那要学好多东西哦。 再举个例子吧,Rx 这个东西,Java 有 RxJava, JS 有 Rxjs,iOS 有 RxSwift。现在各个平台都是把对方好的设计和轮子拿过来,编程思想和设计思想是一样的。但是因为编程语言和平台业务特性,没有机会接触更好的东西。那么就需要突破官方提供的限制,用编程思想来设计和架构改造自己的项目。如果只安于会基本使用语言,不精通语言,了解背后的逻辑,那么永远是入不了程序员门的。 移动互联网热的时候,培训班培训一下就可以干活,拿高薪。但高薪不应该是你做程序员的唯一原因,互联网发展的本质是技术结合业务,最后带来经济繁荣,技术永远是第一驱动力。而程序员就正是创造技术,运用技术,推动互联网繁荣最关键的一环,作为这个时代的弄潮我们应该很光荣才对。 我个人计划是学习英语+开发,在成为高级开发的同时,不断提高英语水平好,开阔自己的视野。东南亚,海外市场不是没有机会。当然,在目前巨头林立的环境下,你自己单干,那肯定一个浪花就没了。问:现在 APP 成本过高,中小公司基本都不重视这一块了,而且目前同样三年经验的后端、前端、移动端,iOS 可能属于比较没有地位的了; 看你说的只会写应用层就不行了?一般公司本来就是面向业务编程,能解决业务问题,移动端一般公司哪管你那么多技术问题;还有一个更严重的问题,就是一般去面试 iOS 的公司,面试造轮船的风气实在太重了,大部分进去不就是个 UI 仔嘛,认清现实吧,本人面过其他技术,比 iOS好的多。答:每个公司开发 APP 都想造航母,现在的确这样。就是大部分进去之后变成 UI 仔,所以我才建议如果从事 iOS 或者移动端开发,一定要自我学习,自我突破限制。我之前和一位同事一起做 iOS 端,后面他转了 JAVA 还升职了。But我始终认为我们项目的技术解决方案和技术不够强,不是没有业务,业务好的很,但是就是感觉移动端开发节奏和技术体系太碎片,每个人一个模块,最后重复劳动,效率还不高。对于这样的现实,肯定需要提升自我去解决,而不是等待机会。也正因此,产生了 Weex,React Native,Flutter 这样快速解决两端,热部署的技术,解放 UI 仔。 不讨论了, 看 Flutter 文档了。iOS 有没有人要,肯定要,前提你真正的热爱写代码,可以分析问题,解决问题,了解编程本质,精通OC、Swift语言,熟练前前后后一个 APP 的全部 API 和细节实现。好比相机,你只会 Github 上找一个高 Star 的库用用是不行的,那你永远不能体会苹果原始 API 设计思路,做不到随心所欲的使用。希望大家有时间多琢磨,想做一个优秀的程序员,放大了说想成为一个优秀的人,都是需要不断学习,不断成长的,大家加油吧!
iOS面试备战-多线程
iOS面试中多线程绝对是最重要的知识点之一,它在日常开发中会被广泛使用,而且多线程是有很多区分度很高的题目可供考察的。这篇文章会梳理下多线程和GCD相关的概念和几个典型问题。因为GCD相关的API用OC看着更直管一些,所以这期实例就都用OC语言书写。概念篇 在面对一些我们常见的概念时,我们常有种这个东西我熟的感觉,但是如果没有深入研究它们的概念和区别,还是很容易弄混或者讲不清楚的。所以这里单独抽一节讲下多线程中的概念。 进程,线程,任务,队列 进程:资源分配的最小单位。在iOS中一个应用的启动就是开启了一个进程。 线程:CPU调度的最小单位。一个进程里会有多个线程。 大家可以思考下,进程和线程为什么是从资源分配和CPU调度层面进行定义的。 任务:每次执行的一段代码,比如下载一张图片,触发一个网络请求。 队列:队列是用来组织任务的,一个队列包含多个任务。 GCD GCD(Grand Central Dispatch)是异步执行任务的技术之一。开发者只需要定义想执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程执行该任务。这里的线程管理是由系统处理的,我们不必关心线程的创建销毁,这大大方便了我们的开发效率。也可以说GCD是一种简化线程操作的多线程使用技术方案。 安卓没有跟GCD完全相同的一套技术方案的,虽然它可以处理GCD实现的一系列效果。 串行,并行,并发 GCD的使用都是通过调度队列(Dispatch Queue)的形式进行的,调度队列有以下 几种形式: 串行(serial):多任务中某时刻只能有一个任务被运行; 并行(parallel):相对于串行,某时刻有多个任务同时被执行,需要多核能力; 并发(concurrent):引入时间片和抢占之后才有了并发的说法,某个时间片只有一个任务在执行,执行完时间片后进行资源抢占,到下一个任务去执行,即“微观串行,宏观并发”,所以这种情况下只有一个空闲的某核,多核空闲就又可以实现并行运行了; 我们常用的调度队列有以下几种: // 串行队列 dispatch_queue_t serialQueue = dispatch_queue_create("com.gcd.serialQueue", DISPATCH_QUEUE_SERIAL); // 并发队列 dispatch_queue_t concurrentQueue = dispatch_queue_create("com.gcd.concurrentQueue", DISPATCH_QUEUE_CONCURRENT); // 全局并发队列 dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 主队列 let mainQueue = DispatchQueue.main注意GCD创建的是并发队列而不是并行队列。但这里的并发队列是一个相对宽泛的定义,它包含并行的概念,GCD作为一个智能的中心调度系统会根据系统情况判断当前能否使用多核能力分摊多个任务,如果满足的话此时就是在并行的执行队列中的任务。 同步,异步 同步:函数会阻塞当前线程直到任务完成返回才能进行其它操作; 异步:在任务执行完成之前先将函数值返回,不会阻塞当前线程; 串行、并发和同步、异步相互结合能否开启新线程串行队列 并发队列 主队列同步 不开启新线程 不开启新线程 不开启新线程异步 开启新线程 开启新线程 不开启新线程主线程和主队列 主线程是一个线程,主队列是指主线程上的任务组织形式。 主队列只会在主线程执行,但主线程上执行的不一定就是主队列,还有可能是别的同步队列。因为前说过,同步操作不会开辟新的线程,所以当你自定义一个同步的串行或者并行队列时都是还在主线程执行。 判断当前是否是主线程: BOOL isMainThread = [NSThread isMainThread];判断当前是否在主队列上: static void *mainQueueKey = "mainQueueKey"; dispatch_queue_set_specific(dispatch_get_main_queue(), mainQueueKey, &mainQueueKey, NULL); BOOL isMainQueue = dispatch_get_specific(mainQueueKey));队列与线程的关系 队列是对任务的描述,它可以包含多个任务,这是应用层的一种描述。线程是系统级的调度单位,它是更底层的描述。一个队列(并行队列)的多个任务可能会被分配到多个线程执行。 问题 代码分析 1、分析下面代码的执行逻辑 - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. [self syncMainTask]; }- (void)syncMainTask { dispatch_queue_main_t mainQueue = dispatch_get_main_queue(); dispatch_sync(mainQueue, ^{ NSLog(@"main queue task"); }); }这段代码会输出task1,然后发生死锁,导致crash。 追加问题一:为什么会死锁?死锁就会导致crash? 我们先分析crash的情况,正常死锁应该就是卡死的情况,不应该导致carsh。那为什么会carsh呢,看崩溃信息:是一个EXC_BAD_INSTRUCTION类型的crash,执行了一个出错的命令。 然后看__DISPATCH_WAIT_FOR_QUEUE__的调用栈信息:右侧汇编代码给出了更详细的crash信息:BUG IN CLIENT OF LIBDISPATCH: dispatch_sync called on queue already owned by current thread。 在当前线程已经拥有的队列中执行dispatch_sync同步操作会导致crash。 在libdispatch的源码中我们可以找到该函数的定义: DISPATCH_NOINLINE static void __DISPATCH_WAIT_FOR_QUEUE__(dispatch_sync_context_t dsc, dispatch_queue_t dq) { uint64_t dq_state = _dispatch_wait_prepare(dq); if (unlikely(_dq_state_drain_locked_by(dq_state, dsc->dsc_waiter))) { DISPATCH_CLIENT_CRASH((uintptr_t)dq_state, "dispatch_sync called on queue " "already owned by current thread"); } /*...*/ }所以我们知道了,这个carsh是libdispatch内部抛出的,当它检测到可能发生死锁时,就直接触发崩溃,事实上它不能完全判断出所有死锁的情况。 我们分析这里为什么会发生死锁。首先syncMainTask就是在主队列中的,我们在主队列先添加dispatch_sync然后再添加其内部的block。主队列FIFO,只有sync执行完了才会执行内部的block,而此时是一个同步队列,block执行完才会退出sync,所以导致了死锁。 对于死锁的解释我也查了好几篇文章,有些说法其实是经不起推敲的,这个解释是我认为相对合理的。 附一篇参考文章:GCD死锁 引出问题二:什么情况下会发生死锁? GCD中发生死锁需要满足两个条件:同步执行串行队列 执行sync的队列和block所在队列为同一个队列引出问题三:如何避免死锁?这段代码应该如何修改? 根据上面提到的条件,我们可以将任务异步执行,或者换成一个并发队列。另外将block放到一个非主队列里执行也是可以的。 2、分析一下代码执行结果 int a = 0; dispatch_queue_t queue = dispatch_get_global_queue(0, 0); while (a < 2) { dispatch_async(queue, ^{ a++; }); } NSLog(@"a = %d", a);首先该段代码会编译不过,编译器检测到变量a被block截获,并尝试修改就报以下错误: Variable is not assignable (missing __block type specifier)。如果我们要在block里对外界变量重新复制,需要添加__block的声明:__block int a = 0; 我们分析这段代码,在开始while之后加入一个异步任务,再之后呢,这个是不确定了,可能是执行a++也可能是因不满足退出条件再次执行加入异步任务,直到满足a<2才会退出while循环。那输出结果也就是不确定了,因为可能在判断跳出循环和输出结果的时候另外的线程又执行了一次a++。 再扩展下,如果将那个并发队列改成主队列,执行逻辑还是一样的吗? 首先主队列是不会开启新线程的,主队列上的异步操作执行时机是等别的任务都执行完了,再来执行添加的a++。显然在while循环里,主队列既有任务还未执行完毕,所以就不会执行a++,也就导致while循环不会退出,形成死循环。 其它问题 什么是线程安全,为什么UI操作必须在主线程执行 线程安全:当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类时线程安全的。 为什么UI操作必须放到主线程:首先UIKit不是线程安全的,多线程访问会导致UI效果不可预期,所以我们不能使用多个线程去处理UI。那既然要单线程处理UI为什么是在主线程呢,这是因为UIApplication作为程序的起点是在主线程初始化的,所以我们后续的UI操作也都要放到主线程处理。 关于这个问题展开讨论可以参阅这篇文章:iOS拾遗——为什么必须在主线程操作UI ###开启新的线程有哪些方法 1、NSThread 2、NSOperationQueue 3、GCD 4、NSObject的performSelectorInBackground方法 5、pthread 多线程任务要实现顺序执行有哪些方法 1、dispatch_group 2、dispatch_barrier 3、dispatch_semaphore_t 4、NSOperation的addDependency方法 如何实现一个多读单写的功能? 多读单写的意思就是可以有多个线程同时参与读取数据,但是写数据时不能有读操作的参与切只有一个线程在写数据。 我们写一个示例程序,看下在不做限制的多读多写程序中会发生什么。 // 计数器 self.count = 0; // 并发队列 self.concurrentQueue = dispatch_get_global_queue(0, 0); for (int i = 0; i< 10; i++) { dispatch_async(self.concurrentQueue, ^{ [self read]; }); dispatch_async(self.concurrentQueue, ^{ [self write]; }); } // 读写操作 - (void)read { NSLog(@"read---- %d", self.count); }- (void)write { self.count += 1; NSLog(@"write---- %d", self.count); }// 输出内容 2020-07-18 11:47:03.612175+0800 GCD_OC[76121:1709312] read---- 0 2020-07-18 11:47:03.612273+0800 GCD_OC[76121:1709311] read---- 1 2020-07-18 11:47:03.612230+0800 GCD_OC[76121:1709314] write---- 1 2020-07-18 11:47:03.612866+0800 GCD_OC[76121:1709312] write---- 2 2020-07-18 11:47:03.612986+0800 GCD_OC[76121:1709311] write---- 3 2020-07-18 11:47:03.612919+0800 GCD_OC[76121:1709314] read---- 2 2020-07-18 11:47:03.613252+0800 GCD_OC[76121:1709312] read---- 3 2020-07-18 11:47:03.613346+0800 GCD_OC[76121:1709314] write---- 4 2020-07-18 11:47:03.613423+0800 GCD_OC[76121:1709311] read---- 4每次运行的输出结果都会不一样,根据这个输出内容,我们可以看到在还没有执行到输出write----1的时候,就已经执行了read----1,在write---- 3之后 read的结果却是2。这绝对是我们所不期望的。其实在程序设计中我们是不应该设计出多读多写这种行为,因为这个结果是不可控。 解决方案之一是对读写操作都加上锁做成单独单写,这样是没问题但有些浪费性能,正常写操作确定之后结果就确定了,读的操作可以多线程同时进行,而不需要等别的线程读完它才能读,所以有了多读单写的需求。 解决多读单写常见有两种方案,第一种是使用读写锁pthread_rwlock_t。 读写锁具有一些几个特性:同一时间,只能有一个线程进行写的操作 同一时间,允许有多个线程进行读的操作。 同一时间,不允许既有写的操作,又有读的操作。这跟我们的多读单写需求完美吻合,也可以说读写锁的设计就是为了实现这一需求的。它的实现方式如下: // 执行读写操作之前需要定义一个读写锁 @property (nonatomic,assign) pthread_rwlock_t lock; pthread_rwlock_init(&_lock,NULL); // 读写操作 - (void)read { pthread_rwlock_rdlock(&_lock); NSLog(@"read---- %d", self.count); pthread_rwlock_unlock(&_lock); }- (void)write { pthread_rwlock_wrlock(&_lock); _count += 1; NSLog(@"write---- %d", self.count); pthread_rwlock_unlock(&_lock); } // 输出内容 2020-07-18 12:00:29.363875+0800 GCD_OC[77172:1722472] read---- 0 2020-07-18 12:00:29.363875+0800 GCD_OC[77172:1722471] read---- 0 2020-07-18 12:00:29.364195+0800 GCD_OC[77172:1722469] write---- 1 2020-07-18 12:00:29.364325+0800 GCD_OC[77172:1722472] write---- 2 2020-07-18 12:00:29.364450+0800 GCD_OC[77172:1722470] read---- 2 2020-07-18 12:00:29.364597+0800 GCD_OC[77172:1722471] write---- 3 2020-07-18 12:00:29.366490+0800 GCD_OC[77172:1722469] read---- 3 2020-07-18 12:00:29.366703+0800 GCD_OC[77172:1722472] write---- 4 2020-07-18 12:00:29.366892+0800 GCD_OC[77172:1722489] read---- 4我们查看输出日志,所以的读操作结果都是最近一次写操作所赋的值,这是符合我们预期的。 还有一种实现多读单写的方案是使用GCD中的栅栏函数dispatch_barrier。栅栏函数的目的就是保证在同一队列中它之前的操作全部执行完毕再执行后面的操作。为了保证写操作的互斥行,我们要对写操作执行「栅栏」: // 我们定义一个用于读写的并发对列 self.rwQueue = dispatch_queue_create("com.rw.queue", DISPATCH_QUEUE_CONCURRENT);- (void)read { dispatch_sync(self.rwQueue, ^{ NSLog(@"read---- %d", self.count); }); }- (void)write { dispatch_barrier_async(self.rwQueue, ^{ self.count += 1; NSLog(@"write---- %d", self.count); }); }这个输出结果跟读写锁实现是一样的,也是符合预期的。 这里多说几句,这里的读和写分别使用sync和async。读操作要用同步是为了阻塞线程尽快返回结果,不用担心无法实现多读,因为我们使用了并发队列,是可以实现多读的。至于写操作使用异步的栅栏函数,是为了写时不阻塞线程,通过栅栏函数实现单写。如果我们将读写都改成sync或者async,由于栅栏函数的机制是会顺序先读后写。如果反过来,读操作异步,写操作同步也是可以达到多读单写的目的的,但读的时候不立即返回结果,网上有人说只能使用异步方式,防止发生死锁,这个说法其实不对,因为同步队列是不会发生死锁的。 用GCD如何实现一个控制最大并发数且执行任务FIFO的功能? 这个相对简单,通过信号量实现并发数的控制,通过并发队列实现任务的FIFO的执行 int maxConcurrent = 3; dispatch_queue_t queue = dispatch_get_global_queue(0, 0); dispatch_semaphore_t semaphore = dispatch_semaphore_create(maxConcurrent); dispatch_async(queue, ^{ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // task dispatch_semaphore_signal(semaphore); });
Swift进阶黄金之路(二)
- 01 Jun, 2020
Swift进阶黄金之路(一) 上期遗留一个问题:为什么 rethrows 一般用在参数中含有可以 throws 的方法的高阶函数中。 我们可以结合Swift的官方文档对rethrows再做一遍回顾:A function or method can be declared with the rethrows keyword to indicate that it throws an error only if one of its function parameters throws an error. These functions and methods are known as rethrowing functions and rethrowing methods. Rethrowing functions and methods must have at least one throwing function parameter.返回rethrows的函数要求至少有一个可抛出异常的函数式参数,而有以函数作为参数的函数就叫做高阶函数。 这期分两方面介绍Swift:特性修饰词和一些重要的Swift概念。特性修饰词 在Swift语法中有很多@符号,这些@符号在Swift4之前的版本大多是兼容OC的特性,Swift4及之后则出现越来越多搭配@符号的新特性。以@开头的修饰词,在官网中叫Attributes,在SwiftGG的翻译中叫特性,我并没有找到这一类被@修饰的符号的统称,就暂且叫他们特性修饰词吧,如果有清楚的小伙伴可以告知我。 从Swift5的发布来看(@dynamicCallable,@State),之后将会有更多的特性修饰词出现,在他们出来之前,我们有必要先了解下现有的一些特性修饰词以及它们的作用。 参考:Swift Attributes @available @available: 可用来标识计算属性、函数、类、协议、结构体、枚举等类型的生命周期。(依赖于特定的平台版本 或 Swift 版本)。它的后面一般跟至少两个参数,参数之间以逗号隔开。其中第一个参数是固定的,代表着平台和语言,可选值有以下这几个:iOS iOSApplicationExtension macOS macOSApplicationExtension watchOS watchOSApplicationExtension tvOS tvOSApplicationExtension swift可以使用*指代支持所有这些平台。 有一个我们常用的例子,当需要关闭ScrollView的自动调整inset功能时: // 指定该方法仅在iOS11及以上的系统设置 if #available(iOS 11.0, *) { scrollView.contentInsetAdjustmentBehavior = .never } else { automaticallyAdjustsScrollViewInsets = false }还有一种用法是放在函数、结构体、枚举、类或者协议的前面,表示当前类型仅适用于某一平台: @available(iOS 12.0, *) func adjustDarkMode() { /* code */ } @available(iOS 12.0, *) struct DarkModeConfig { /* code */ } @available(iOS 12.0, *) protocol DarkModeTheme { /* code */ }版本和平台的限定可以写多个: @available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *) public func applying(_ difference: CollectionDifference<Element>) -> ArraySlice<Element>?注意:作为条件语句的available前面是#,作为标记位时是@ 刚才说了,available后面参数至少要有两个,后面的可选参数这些:deprecated:从指定平台标记为过期,可以指定版本号obsoleted=版本号:从指定平台某个版本开始废弃(注意弃用的区别,deprecated是还可以继续使用,只不过是不推荐了,obsoleted是调用就会编译错误)该声明message=信息内容:给出一些附加信息unavailable:指定平台上是无效的renamed=新名字:重命名声明我们看几个例子,这个是Array里flatMap的函数说明: @available(swift, deprecated: 4.1, renamed: "compactMap(_:)", message: "Please use compactMap(_:) for the case where closure returns an optional value") public func flatMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult]它的含义是针对swift语言,该方式在swift4.1版本之后标记为过期,对应该函数的新名字为compactMap(_:),如果我们在4.1之上的版本使用该函数会收到编译器的警告,即⚠️Please use compactMap(_:) for the case where closure returns an optional value。 在Realm库里,有一个销毁NotificationToken的方法,被标记为unavailable: extension RLMNotificationToken { @available(*, unavailable, renamed: "invalidate()") @nonobjc public func stop() { fatalError() } }标记为unavailable就不会被编译器联想到。这个主要是为升级用户的迁移做准备,从可用stop()的版本升上了,会红色报错,提示该方法不可用。因为有renamed,编译器会推荐你用invalidate(),点击fix就直接切换了。所以这两个标记参数常一起出现。 @discardableResult 带返回的函数如果没有处理返回值会被编译器警告⚠️。但有时我们就是不需要返回值的,这个时候我们可以让编译器忽略警告,就是在方法名前用@discardableResult声明一下。可以参考Alamofire中request的写法: @discardableResult public func request( _ url: URLConvertible, method: HTTPMethod = .get, parameters: Parameters? = nil, encoding: ParameterEncoding = URLEncoding.default, headers: HTTPHeaders? = nil) -> DataRequest { return SessionManager.default.request( url, method: method, parameters: parameters, encoding: encoding, headers: headers ) }@inlinable 这个关键词是可内联的声明,它来源于C语言中的inline。C中一般用于函数前,做内联函数,它的目的是防止当某一函数多次调用造成函数栈溢出的情况。因为声明为内联函数,会在编译时将该段函数调用用具体实现代替,这么做可以省去函数调用的时间。 内联函数常出现在系统库中,OC中的runtim中就有大量的inline使用: static inline id autorelease(id obj) { ASSERT(obj); ASSERT(!obj->isTaggedPointer()); id *dest __unused = autoreleaseFast(obj); ASSERT(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj); return obj; }Swift中的@inlinable和C中的inline基本相同,它在标准库的定义中也广泛出现,可用于方法,计算属性,下标,便利构造方法或者deinit方法中。 例如Swift对Array中map函数的定义: @inlinable public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]其实Array中声明的大部分函数前面都加了@inlinable,当应用某一处调用该方法时,编译器会将调用处用具体实现代码替换。 需要注意内联声明不能用于标记为private或者fileprivate的地方。 这很好理解,对私有方法的内联是没有意义的。内联的好处是运行时更快,因为它省略了从标准库调用map实现的步骤。但这个快也是有代价的,因为是编译时做替换,这增加了编译的开销,会相应的延长编译时间。 内联更多的是用于系统库的特性,目前我了解的Swift三方库中仅有CocoaLumberjack使用了@inlinable这个特性。 @warn_unqualified_access 通过命名我们可以推断出其大概含义:对“不合规”的访问进行警告。这是为了解决对于相同名称的函数,不同访问对象可能产生歧义的问题。 比如说,Swift 标准库中Array和Sequence均实现了min()方法,而系统库中也定义了min(::),对于可能存在的二义性问题,我们可以借助于@warn_unqualified_access。 extension Array where Self.Element : Comparable { @warn_unqualified_access @inlinable public func min() -> Element? } extension Sequence where Self.Element : Comparable { @warn_unqualified_access @inlinable public func min() -> Self.Element? }这个特性声明会由编译器在可能存在二义性的场景中对我们发出警告。这里有一个场景可以便于理解它的含义,我们自定义一个求Array中最小值的函数: extension Array where Element: Comparable { func minValue() -> Element? { return min() } }我们会收到编译器的警告:Use of 'min' treated as a reference to instance method in protocol 'Sequence', Use 'self.' to silence this warning。它告诉我们编译器推断我们当前使用的是Sequence中的min(),这与我们的想法是违背的。因为有这个@warn_unqualified_access限定,我们能及时的发现问题,并解决问题:self.min()。 @objc 把这个特性用到任何可以在 Objective-C 中表示的声明上——例如,非内嵌类,协议,非泛型枚举(原始值类型只能是整数),类和协议的属性、方法(包括 setter 和 getter ),初始化器,反初始化器,下标。 objc 特性告诉编译器,这个声明在 Objective-C 代码中是可用的。 用 objc 特性标记的类必须继承自一个 Objective-C 中定义的类。如果你把 objc 用到类或协议中,它会隐式地应用于该类或协议中 Objective-C 兼容的成员上。如果一个类继承自另一个带 objc 特性标记或 Objective-C 中定义的类,编译器也会隐式地给这个类添加 objc 特性。标记为 objc 特性的协议不能继承自非 objc 特性的协议。 @objc还有一个用处是当你想在OC的代码中暴露一个不同的名字时,可以用这个特性,它可以用于类,函数,枚举,枚举成员,协议,getter,setter等。 // 当在OC代码中访问enabled的getter方法时,是通过isEnabled class ExampleClass: NSObject { @objc var enabled: Bool { @objc(isEnabled) get { // Return the appropriate value } } }这一特性还可以用于解决潜在的命名冲突问题,因为Swift有命名空间,常常不带前缀声明,而OC没有命名空间是需要带的,当在OC代码中引用Swift库,为了防止潜在的命名冲突,可以选择一个带前缀的名字供OC代码使用。 Charts作为一个在OC和Swift中都很常用的图标库,是需要较好的同时兼容两种语言的使用的,所以也可以看到里面有大量通过@objc标记对OC调用时的重命名代码: @objc(ChartAnimator) open class Animator: NSObject { }@objc(ChartComponentBase) open class ComponentBase: NSObject { }@objcMembers 因为Swift中定义的方法默认是不能被OC调用的,除非我们手动添加@objc标识。但如果一个类的方法属性较多,这样会很麻烦,于是有了这样一个标识符@objcMembers,它可以让整个类的属性方法都隐式添加@objc,不光如此对于类的子类、扩展、子类的扩展都也隐式的添加@objc,当然对于OC不支持的类型,仍然无法被OC调用: @objcMembers class MyClass : NSObject { func foo() { } // implicitly @objc func bar() -> (Int, Int) // not @objc, because tuple returns // aren't representable in Objective-C }extension MyClass { func baz() { } // implicitly @objc }class MySubClass : MyClass { func wibble() { } // implicitly @objc }extension MySubClass { func wobble() { } // implicitly @objc }参考:Swift3、4中的@objc、@objcMembers和dynamic @testable @testable是用于测试模块访问主target的一个关键词。 因为测试模块和主工程是两个不同的target,在swift中,每个target代表着不同的module,不同module之间访问代码需要public和open级别的关键词支撑。但是主工程并不是对外模块,为了测试修改访问权限是不应该的,所以有了@testable关键词。使用如下: import XCTest @testable import Projectclass ProjectTests: XCTestCase { /* code */ }这时测试模块就可以访问那些标记为internal或者public级别的类和成员了。 @frozen 和@unknown default frozen意为冻结,是为Swift5的ABI稳定准备的一个字段,意味向编译器保证之后不会做出改变。为什么需要这么做以及这么做有什么好处,他们和ABI稳定是息息相关的,内容有点多就不放这里了,之后会单独出一篇文章介绍,这里只介绍这两个字段的含义。 @frozen public enum ComparisonResult : Int { case orderedAscending = -1 case orderedSame = 0 case orderedDescending = 1 }@frozen public struct String {}extension AVPlayerItem { public enum Status : Int { case unknown = 0 case readyToPlay = 1 case failed = 2 } }ComparisonResult这个枚举值被标记为@frozen即使保证之后该枚举值不会再变。注意到String作为结构体也被标记为@frozen,意为String结构体的属性及属性顺序将不再变化。其实我们常用的类型像Int、Float、Array、Dictionary、Set等都已被“冻结”。需要说明的是冻结仅针对struct和enum这种值类型,因为他们在编译器就确定好了内存布局。对于class类型,不存在是否冻结的概念,可以想下为什么。 对于没有标记为frozen的枚举AVPlayerItem.Status,则认为该枚举值在之后的系统版本中可能变化。 对于可能变化的枚举,我们在列出所有case的时候还需要加上对@unknown default的判断,这一步会有编译器检查: switch currentItem.status { case .readyToPlay: /* code */ case .failed: /* code */ case .unknown: /* code */ @unknown default: fatalError("not supported") }@State、@Binding、@ObservedObject、@EnvironmentObject 这几个是SwiftUI中出现的特性修饰词,因为我对SwiftUI的了解不多,这里就不做解释了。附一篇文章供大家了解。 [译]理解 SwiftUI 里的属性装饰器@State, @Binding, @ObservedObject, @EnvironmentObject 几个重要关键词 lazy lazy是懒加载的关键词,当我们仅需要在使用时进行初始化操作就可以选用该关键词。举个例子: class Avatar { lazy var smallImage: UIImage = self.largeImage.resizedTo(Avatar.defaultSmallSize) var largeImage: UIImage init(largeImage: UIImage) { self.largeImage = largeImage } }对于smallImage,我们声明了lazy,如果我们不去调用它是不会走后面的图片缩放计算的。但是如果没有lazy,因为是初始化方法,它会直接计算出smallImage的值。所以lazy很好的避免的不必要的计算。 另一个常用lazy的地方是对于UI属性的定义: lazy var dayLabel: UILabel = { let label = UILabel() label.text = self.todayText() return label }()这里使用的是一个闭包,当调用该属性时,执行闭包里面的内容,返回具体的label,完成初始化。 使用lazy你可能会发现它只能通过var初始而不能通过let,这是由 lazy 的具体实现细节决定的:它在没有值的情况下以某种方式被初始化,然后在被访问时改变自己的值,这就要求该属性是可变的。 另外我们可以在Sequences中使用lazy,在讲解它之前我们先看一个例子: func increment(x: Int) -> Int { print("Computing next value of \(x)") return x+1 }let array = Array(0..<1000) let incArray = array.map(increment) print("Result:") print(incArray[0], incArray[4])在执行print("Result:")之前,Computing next value of ...会被执行1000次,但实际上我们只需要0和4这两个index对应的值。 上面说了序列也可以使用lazy,使用的方式是: let array = Array(0..<1000) let incArray = array.lazy.map(increment) print("Result:") print(incArray[0], incArray[4])// Result: // 1 5在执行print("Result:")之前,并不会打印任何东西,只打印了我们用到的1和5。就是说这里的lazy可以延迟到我们取值时才去计算map里的结果。 我们看下这个lazy的定义: @inlinable public var lazy: LazySequence<Array<Element>> { get }它返回一个LazySequence的结构体,这个结构体里面包含了Array,而map的计算在LazySequence里又重新定义了一下: /// Returns a `LazyMapSequence` over this `Sequence`. The elements of /// the result are computed lazily, each time they are read, by /// calling `transform` function on a base element. @inlinable public func map<U>(_ transform: @escaping (Base.Element) -> U) -> LazyMapSequence<Base, U>这里完成了lazy序列的实现。LazySequence类型的lazy只能被用于map、flatMap、compactMap这样的高阶函数中。 参考: “懒”点儿好 纠错:参考文章中说:"这些类型(LazySequence)只能被用在 map,flatMap,filter这样的高阶函数中" 其实是没有filter的,因为filter是过滤函数,它需要完整遍历一遍序列才能完成过滤操作,是无法懒加载的,而且我查了LazySequence的定义,确实是没有filter函数的。 unowned weak Swift开发过程中我们会经常跟闭包打交道,而用到闭包就不可避免的遇到循环引用问题。在Swift处理循环引用可以使用unowned和weak这两个关键词。看下面两个例子: class Dog { var name: String init (name: String ) { self.name = name } deinit { print("\(name) is deinitialized") } }class Bone { // weak 修饰词 weak var owner: Dog? init(owner: Dog?) { self.owner = owner } deinit { print("bone is deinitialized" ) } }var lucky: Dog? = Dog(name: "Lucky") var bone: Bone? = Bone(owner: lucky!) lucky = nil // Lucky is deinitialized这里Dog和Bone是相互引用的关系,如果没有weak var owner: Dog?这里的weak声明,将不会打印Lucky is deinitialized。还有一种解决循环应用的方式是把weak替换为unowned关键词。weak相当于oc里面的weak,弱引用,不会增加循环计数。主体对象释放时被weak修饰的属性也会被释放,所以weak修饰对象就是optional。 unowned相当于oc里面的unsafe_unretained,它不会增加引用计数,即使它的引用对象释放了,它仍然会保持对被已经释放了的对象的一个 "无效的" 引用,它不能是 Optional 值,也不会被指向 nil。如果此时为无效引用,再去尝试访问它就会crash。这两者还有一个更常用的地方是在闭包里面: lazy var someClosure: () -> Void = { [weak self] in // 被weak修饰后self为optional,这里是判断self非空的操作 guard let self = self else { retrun } self.doSomethings() }这里如果是unowned修饰self的话,就不需要用guard做解包操作了。但是我们不能为了省略解包的操作就用unowned,也不能为了安全起见全部weak,弄清楚两者的适用场景非常重要。 根据苹果的建议:Define a capture in a closure as an unowned reference when the closure and the instance it captures will always refer to each other, and will always be deallocated at the same time.当闭包和它捕获的实例总是相互引用,并且总是同时释放时,即相同的生命周期,我们应该用unowned,除此之外的场景就用weak。 img 参考:内存管理,WEAK 和 UNOWNED Unowned 还是 Weak?生命周期和性能对比 KeyPath KeyPath是键值路径,最开始是用于处理KVC和KVO问题,后来又做了更广泛的扩展。 // KVC问题,支持struct、class struct User { let name: String var age: Int }var user1 = User() user1.name = "ferry" user1.age = 18//使用KVC取值 let path: KeyPath = \User.name user1[keyPath: path] = "zhang" let name = user1[keyPath: path] print(name) //zhang// KVO的实现还是仅限于继承自NSObject的类型 // playItem为AVPlayerItem对象 playItem.observe(\.status, changeHandler: { (_, change) in /* code */ })这个KeyPath的定义是这样的: public class AnyKeyPath : Hashable, _AppendKeyPath {}/// A partially type-erased key path, from a concrete root type to any /// resulting value type. public class PartialKeyPath<Root> : AnyKeyPath {}/// A key path from a specific root type to a specific resulting value type. public class KeyPath<Root, Value> : PartialKeyPath<Root> {}定义一个KeyPath需要指定两个类型,根类型和对应的结果类型。对应上面示例中的path: let path: KeyPath<User, String> = \User.name根类型就是User,结果类型就是String。也可以不指定,因为编译器可以从\User.name推断出来。那为什么叫根类型的?可以注意到KeyPath遵循一个协议_AppendKeyPath,它里面定义了很多append的方法,KeyPath是多层可以追加的,就是如果属性是自定义的Address类型,形如: struct Address { var country: String = "" } let path: KeyPath<User, String> = \User.address.country这里根类型为User,次级类型是Address,结果类型是String。所以path的类型依然是KeyPath。 明白了这些我们可以用KeyPath做一些扩展: extension Sequence { func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] { return sorted { a, b in return a[keyPath: keyPath] < b[keyPath: keyPath] } } } // users is Array<User> let newUsers = users.sorted(by: \.age)这个自定义sorted函数实现了通过传入keyPath进行升序排列功能。 参考:The power of key paths in Swift some some是Swift5.1新增的特性。它的用法就是修饰在一个 protocol 前面,默认场景下 protocol 是没有具体类型信息的,但是用 some 修饰后,编译器会让 protocol 的实例类型对外透明。 可以通过一个例子理解这段话的含义,当我们尝试定义一个遵循Equatable协议的value时: // Protocol 'Equatable' can only be used as a generic constraint because it has Self or associated type requirements var value: Equatable { return 1 }var value: Int { return 1 }编译器提示我们Equatable只能被用来做泛型的约束,它不是一个具体的类型,这里我们需要使用一个遵循Equatable的具体类型(Int)进行定义。但有时我们并不想指定具体的类型,这时就可以在协议名前加上some,让编译器自己去推断value的类型: var value: some Equatable { return 1 }在SwiftUI里some随处可见: struct ContentView: View { var body: some View { Text("Hello World") } }这里使用some就是因为View是一个协议,而不是具体类型。 当我们尝试欺骗编译器,每次随机返回不同的Equatable类型: var value: some Equatable { if Bool.random() { return 1 } else { return "1" } }聪明的编译器是会发现的,并警告我们Function declares an opaque return type, but the return statements in its body do not have matching underlying types。 参考:SwiftUI 的一些初步探索 (一)
如何通过静态分析提高iOS代码质量
随着项目的扩大,依靠人工codereview来保证项目的质量,越来越不现实,这时就有必要借助于一种自动化的代码审查工具:程序静态分析。 程序静态分析(Program Static Analysis)是指在不运行代码的方式下,通过词法分析、语法分析、控制流、数据流分析等技术对程序代码进行扫描,验证代码是否满足规范性、安全性、可靠性、可维护性等指标的一种代码分析技术。(来自百度百科) 词法分析,语法分析等工作是由编译器进行的,所以对iOS项目为了完成静态分析,我们需要借助于编译器。对于OC语言的静态分析可以完全通过Clang,对于Swift的静态分析除了Clange还需要借助于SourceKit。 Swift语言对应的静态分析工具是SwiftLint,OC语言对应的静态分析工具有Infer和OCLitn。以下会是对各个静态分析工具的安装和使用做一个介绍。SwiftLint对于Swift项目的静态分析可以使用SwiftLint。SwiftLint 是一个用于强制检查 Swift 代码风格和规定的一个工具。它的实现是 Hook 了 Clang 和 SourceKit 从而能够使用 AST 来表示源代码文件的更多精确结果。Clange我们了解了,那SourceKit是干什么用的? SourceKit包含在Swift项目的主仓库,它是一套工具集,支持Swift的大多数源代码操作特性:源代码解析、语法突出显示、排版、自动完成、跨语言头生成等工作。 安装 安装有两种方式,任选其一: 方式一:通过Homebrew $ brew install swiftlint这种是全局安装,各个应用都可以使用。 方式二:通过CocoaPods pod 'SwiftLint', :configurations => ['Debug']这种方式相当于把SwiftLint作为一个三方库集成进了项目,因为它只是调试工具,所以我们应该将其指定为仅Debug环境下生效。 集成进Xcode 我们需要在项目中的Build Phases,添加一个Run Script Phase。如果是通过homebrew安装的,你的脚本应该是这样的。 if which swiftlint >/dev/null; then swiftlint else echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint" fi如果是通过cocoapods安装的,你得脚本应该是这样的: "${PODS_ROOT}/SwiftLint/swiftlint"运行SwiftLint 键入CMD + B编译项目,在编译完后会运行我们刚才加入的脚本,之后我们就能看到项目中大片的警告信息。有时候build信息并不能填入项目代码中,我们可以在编译的log日志里查看。定制 SwiftLint规则太多了,如果我们不想执行某一规则,或者想要滤掉对Pods库的分析,我们可以对SwfitLint进行配置。 在项目根目录新建一个.swiftlint.yml文件,然后填入如下内容: disabled_rules: # rule identifiers to exclude from running - colon - trailing_whitespace - vertical_whitespace - function_body_length opt_in_rules: # some rules are only opt-in - empty_count # Find all the available rules by running: # swiftlint rules included: # paths to include during linting. `--path` is ignored if present. - Source excluded: # paths to ignore during linting. Takes precedence over `included`. - Carthage - Pods - Source/ExcludedFolder - Source/ExcludedFile.swift - Source/*/ExcludedFile.swift # Exclude files with a wildcard analyzer_rules: # Rules run by `swiftlint analyze` (experimental) - explicit_self# configurable rules can be customized from this configuration file # binary rules can set their severity level force_cast: warning # implicitly force_try: severity: warning # explicitly # rules that have both warning and error levels, can set just the warning level # implicitly line_length: 110 # they can set both implicitly with an array type_body_length: - 300 # warning - 400 # error # or they can set both explicitly file_length: warning: 500 error: 1200 # naming rules can set warnings/errors for min_length and max_length # additionally they can set excluded names type_name: min_length: 4 # only warning max_length: # warning and error warning: 40 error: 50 excluded: iPhone # excluded via string allowed_symbols: ["_"] # these are allowed in type names identifier_name: min_length: # only min_length error: 4 # only error excluded: # excluded via string array - id - URL - GlobalAPIKey reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji, sonarqube, markdown)一条rules提示如下,其对应的rules名就是function_body_length。 ! Function Body Length Violation: Function body should span 40 lines or less excluding comments and whitespace: currently spans 43 lines (function_body_length)disabled_rules下填入我们不想遵循的规则。 excluded设置我们想跳过检查的目录,Carthage、Pod、SubModule这些一般可以过滤掉。 其他的一些像是文件长度(file_length),类型名长度(type_name),我们可以通过设置具体的数值来调节。 另外SwiftLint也支持自定义规则,我们可以根据自己的需求,定义自己的rule。 生成报告 如果我们想将此次分析生成一份报告,也是可以的(该命令是通过homebrew安装的swiftlint): # reporter type (xcode, json, csv, checkstyle, junit, html, emoji, sonarqube, markdown) $ swiftlint lint --reporter html > swiftlint.htmlxcodebuild xcodebuild是xcode内置的编译命令,我们可以用它来编译打包我们的iOS项目,接下来介绍的Infer和OCLint都是基于xcodebuild的编译产物进行分析的,所以有必要简单介绍一下它。 一般编译一个项目,我们需要指定项目名,configuration,scheme,sdk等信息以下是几个简单的命令及说明。 # 不带pod的项目,target名为TargetName,在Debug下,指定模拟器sdk环境进行编译 xcodebuild -target TargetName -configuration Debug -sdk iphonesimulator # 带pod的项目,workspace名为TargetName.xcworkspace,在Release下,scheme为TargetName,指定真机环境进行编译。不指定模拟器环境会验证证书 xcodebuild -workspace WorkspaceName.xcworkspace -scheme SchemeName Release # 清楚项目的编译产物 xcodebuild -workspace WorkspaceName.xcworkspace -scheme SchemeName Release clean之后对xcodebuild命令的使用都需要将这些参数替换为自己项目的参数。 InferInfer是Facebook开发的针对C、OC、Java语言的静态分析工具,它同时支持对iOS和Android应用的分析。对于Facebook内部的应用像是 Messenger、Instagram 和其他一些应用均是有它进行静态分析的。它主要检测隐含的问题,主要包括以下几条:资源泄露,内存泄露 变量和参数的非空检测 循环引用 过早的nil操作暂不支持自定义规则。 安装及使用 $ brew install infer运行infer $ cd projectDir # 跳过对Pods的分析 $ infer run --skip-analysis-in-path Pods -- xcodebuild -workspace "Project.xcworkspace" -scheme "Scheme" -configuration Debug -sdk iphonesimulator我们会得到一个infer-out的文件夹,里面是各种代码分析的文件,有txt,json等文件格式,当这样不方便查看,我们可以将其转成html格式: $ infer explore --html点击trace,我们会看到该问题代码的上下文。 因为Infer默认是增量编译,只会分析变动的代码,如果我们想整体编译的话,需要clean一下项目: $ xcodebuild -workspace "Project.xcworkspace" -scheme "Scheme" -configuration Debug -sdk iphonesimulator clean再次运行Infer去编译。 $ infer run --skip-analysis-in-path Pods -- xcodebuild -workspace "Project.xcworkspace" -scheme "Scheme" -configuration Debug -sdk iphonesimulatorInfer的大致原理 Infer的静态分析主要分两个阶段: 1、捕获阶段 Infer 捕获编译命令,将文件翻译成 Infer 内部的中间语言。 这种翻译和编译类似,Infer 从编译过程获取信息,并进行翻译。这就是我们调用 Infer 时带上一个编译命令的原因了,比如: infer -- clang -c file.c, infer -- javac File.java。结果就是文件照常编译,同时被 Infer 翻译成中间语言,留作第二阶段处理。特别注意的就是,如果没有文件被编译,那么也没有任何文件会被分析。 Infer 把中间文件存储在结果文件夹中,一般来说,这个文件夹会在运行 infer 的目录下创建,命名是 infer-out/。 2、分析阶段 在分析阶段,Infer 分析 infer-out/ 下的所有文件。分析时,会单独分析每个方法和函数。 在分析一个函数的时候,如果发现错误,将会停止分析,但这不影响其他函数的继续分析。 所以你在检查问题的时候,修复输出的错误之后,需要继续运行 Infer 进行检查,知道确认所有问题都已经修复。 错误除了会显示在标准输出之外,还会输出到文件 infer-out/bug.txt 中,我们过滤这些问题,仅显示最有可能存在的。 在结果文件夹中(infer-out),同时还有一个 csv 文件 report.csv,这里包含了所有 Infer 产生的信息,包括:错误,警告和信息。 OCLint OCLint是基于Clange Tooling编写的库,它支持扩展,检测的范围比Infer要大。不光是隐藏bug,一些代码规范性的问题,例如命名和函数复杂度也均在检测范围之内。 安装OCLint OCLint一般通过Homebrew安装 $ brew tap oclint/formulae $ brew install oclint通过Hombrew安装的版本为0.13。 $ oclint --version LLVM (http://llvm.org/): LLVM version 5.0.0svn-r313528 Optimized build. Default target: x86_64-apple-darwin19.0.0 Host CPU: skylakeOCLint (http://oclint.org/): OCLint version 0.13. Built Sep 18 2017 (08:58:40).我分别用Xcode11在两个项目上运行过OCLint,一个实例项目可以正常运行,另一个复杂的项目却运行失败,报如下错误: 1 error generated 1 error generated ... oclint: error: cannot open report output file ..../onlintReport.html我并不清楚原因,如果你想试试0.13能否使用的话,直接跳到安装xcpretty。如果你也遇到了这个问题,可以回来安装oclint0.15版本。 OCLint0.15 我在oclint issuse #547这里找到了这个问题和对应的解决方案。 我们需要更新oclint至0.15版本。brew上的最新版本是0.13,github上的最新版本是0.15。我下载github上的release0.15版本,但是这个包并不是编译过的,不清楚是不是官方自己搞错了,只能手动编译了。因为编译要下载llvm和clange,这两个包较大,所以我将编译过后的包直接传到了这里CodeChecker。 如果不关心编译过程,可以下载编译好的包,跳到设置环境变量那一步。 编译OCLint 1、安装CMake和Ninja这两个编译工具 $ brew install cmake ninja2、clone OCLint项目 $ git clone https://github.com/oclint/oclint3、进入oclint-scripts目录,执行make命令 $ ./make成功之后会出现build文件夹,里面有个oclint-release就是编译成功的oclint工具。 设置oclint工具的环境变量 设置环境变量的目的是为了我们能够快捷访问。然后我们需要配置PATH环境变量,注意OCLint_PATH的路径为你存放oclint-release的路径。将其添加到.zshrc,或者.bash_profile文件末尾: OCLint_PATH=/Users/zhangferry/oclint/build/oclint-release export PATH=$OCLint_PATH/bin:$PATH执行source .zshrc,刷新环境变量,然后验证oclint是否安装成功: $ oclint --version OCLint (http://oclint.org/): OCLint version 0.15. Built May 19 2020 (11:48:49).出现这个介绍就说明我们已经完成了安装。 安装xcpretty xcpretty是一个格式化xcodebuild输出内容的脚本工具,oclint的解析依赖于它的输出。它的安装方式为: $ gem install xcprettyOCLint的使用 在使用OCLint之前还需要一些准备工作,需要将编译项COMPILER_INDEX_STORE_ENABLE设置为NO。将 Project 和 Targets 中 Building Settings 下的 COMPILER_INDEX_STORE_ENABLE 设置为 NO 在 podfile 中 target 'target' do 前面添加下面的脚本,将各个pod的编译配置也改为此选项post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['COMPILER_INDEX_STORE_ENABLE'] = "NO" end end end使用方式 1、进入项目根目录,运行如下脚本: $ xcodebuild -workspace ProjectName.xcworkspace -scheme ProjectScheme -configuration Debug -sdk iphonesimulator | xcpretty -r json-compilation-database -o compile_commands.json会将xcodebuild编译过程中的一些信息记录成一个文件compile_commands.json,如果我们在项目根目录看到了该文件,且里面是有内容的,证明我们完成了第一步。 2、我们将这个json文件转成方便查看的html,过滤掉对Pods文件的分析,为了防止行数上限,我们加上行数的限制: $ oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html -rc LONG_LINE=9999 -max-priority-1=9999 -max-priority-2=9999 -max-priority-3=9999最终会产生一个oclintReport.html文件。OCLint支持自定义规则,因为其本身规则已经很丰富了,自定义规则的需求应该很小,也就没有尝试。 封装脚本 OCLint跟Infer一样都是通过运行几个脚本语言进行执行的,我们可以将这几个命令封装成一个脚本文件,以OCLint为例,Infer也类似: #!/bin/bash # mark sure you had install the oclint and xcpretty# You need to replace these values with your own project configuration workspace_name="WorkSpaceName.xcworkspace" scheme_name="SchemeName"# remove history rm compile_commands.json rm oclint_result.xml # clean project # -sdk iphonesimulator means run simulator xcodebuild -workspace $workspace_name -scheme $scheme_name -configuration Debug -sdk iphonesimulator clean || (echo "command failed"; exit 1);# export compile_commands.json xcodebuild -workspace $workspace_name -scheme $scheme_name -configuration Debug -sdk iphonesimulator \ | xcpretty -r json-compilation-database -o compile_commands.json \ || (echo "command failed"; exit 1);# export report html # you can run `oclint -help` to see all USAGE oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html \ -disable-rule ShortVariableName \ -rc LONG_LINE=1000 \ || (echo "command failed"; exit 1);open -a "/Applications/Safari.app" oclintReport.htmloclint-json-compilation-database命令的几个参数说明: -e 需要忽略分析的文件,这些文件的警告不会出现在报告中 -rc 需要覆盖的规则的阀值,这里可以自定义项目的阀值,默认阀值 -enable-rule 支持的规则,默认是oclint提供的都支持,可以组合-disable-rule来过滤掉一些规则 规则列表 -disable-rule 需要忽略的规则,根据项目需求设置 在Xcode中使用OCLint 因为OCLint提供了xcode格式的输出样式,所以我们可以将它作为一个脚本放在Xcode中。 1、在项目的 TARGETS 下面,点击下方的 "+" ,选择 cross-platform 下面的 Aggregate。输入名字,这里命名为 OCLint2、选中该Target,进入Build Phases,添加Run Script,写入下面脚本: # Type a script or drag a script file from your workspace to insert its path. # 内置变量 cd ${SRCROOT} xcodebuild clean xcodebuild | xcpretty -r json-compilation-database oclint-json-compilation-database -e Pods -- -report-type xcode可以看出该脚本跟上面的脚本一样,只不过 将oclint-json-compilation-database命令的-report-type由html改为了xcode。而OCLint作为一个target本身就运行在特定的环境下,所以xcodebuild可以省去配置参数。 3、通过CMD + B我们编译一下项目,执行脚本任务,会得到能够定位到代码的warning信息:总结 以下是对这几种静态分析方案的对比,我们可以根据需求选择适合自己的静态分析方案。SwiftLint Infer OCLint支持语言 Swift C、C++、OC、Java C、C++、OC易用性 简单 较简单 较简单能否集成进Xcode 可以 不能集成进xcode 可以自带规则丰富度 较多,包含代码规范 相对较少,主要检测潜在问题 较多,包含代码规范规则扩展性 可以 不可以 可以参考 OCLint 实现 Code Review - 给你的代码提提质量 Using OCLint in Xcode Infer 的工作机制 LLVM & Clang 入门
Swift进阶黄金之路
- 10 May, 2020
Swift进阶黄金之路(二) 这篇是对一文鉴定是Swift的王者,还是青铜文章中问题的解答。这些问题仅仅是表层概念,属于知识点,在我看来即使都很清楚也并不能代表上了王者,如果非要用段位类比的话,黄金还是合理的😄。 Swift是一门上手容易,但是精通较难的语言。即使下面这些内容都不清楚也不妨碍你开发业务需求,但是了解之后它能够帮助我们写出更加Swifty的代码。一、 协议 Protocol ExpressibleByDictionaryLiteral ExpressibleByDictionaryLiteral是字典的字面量协议,该协议的完整写法为: public protocol ExpressibleByDictionaryLiteral { /// The key type of a dictionary literal. associatedtype Key /// The value type of a dictionary literal. associatedtype Value /// Creates an instance initialized with the given key-value pairs. init(dictionaryLiteral elements: (Self.Key, Self.Value)...) }首先字面量(Literal)的意思是:用于表达源代码中一个固定值的表示法(notation)。 举个例子,构造字典我们可以通过以下两种方式进行: // 方法一: var countryCodes = Dictionary<String, Any>() countryCodes["BR"] = "Brazil" countryCodes["GH"] = "Ghana" // 方法二: let countryCodes = ["BR": "Brazil", "GH": "Ghana"]第二种构造方式就是通过字面量方式进行构造的。 其实基础类型基本都是通过字面量进行构造的: let num: Int = 10 let flag: Bool = true let str: String = "Brazil" let array: [String] = ["Brazil", "Ghana"]而这些都有对应的字面量协议: ExpressibleByNilLiteral // nil字面量协议 ExpressibleByIntegerLiteral // 整数字面量协议 ExpressibleByFloatLiteral // 浮点数字面量协议 ExpressibleByBooleanLiteral // 布尔值字面量协议 ExpressibleByStringLiteral // 字符串字面量协议 ExpressibleByArrayLiteral // 数组字面量协议Sequence Sequence翻译过来就是序列,该协议的目的是一系列相同类型的值的集合,并且提供对这些值的迭代能力,这里的迭代可以理解为遍历,也即for-in的能力。可以看下该协议的定义: protocol Sequence { associatedtype Iterator: IteratorProtocol func makeIterator() -> Iterator }Sequence又引入了另一个协议IteratorProtocol,该协议就是为了提供序列的迭代能力。 public protocol IteratorProtocol { associatedtype Element public mutating func next() -> Self.Element? }我们通常用for-in实现数组的迭代: let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"] for animal in animals { print(animal) }这里的for-in会被编译器翻译成: var animalIterator = animals.makeIterator() while let animal = animalIterator.next() { print(animal) }Collection Collection译为集合,其继承于Sequence。 public protocol Collection : Sequence { associatedtype Index : Comparable var startIndex: Index { get } var endIndex: Index { get } var isEmpty: Bool { get } var count: Int { get } subscript(position: Index) -> Element { get } subscript(bounds: Range<Index>) -> SubSequence { get } }是一个元素可以反复遍历并且可以通过索引的下标访问的有限集合,注意Sequence可以是无限的,Collection必须是有限的。 Collection在Sequence的基础上扩展了下标访问、元素个数能特性。我们常用的集合类型Array,Dictionary,Set都遵循该协议。 CustomStringConvertible 这个协议表示自定义类型输出的样式。先来看下它的定义: public protocol CustomStringConvertible { var description: String { get } }只有一个description的属性。它的使用很简单: struct Point: CustomStringConvertible { let x: Int, y: Int var description: String { return "(\(x), \(y))" } }let p = Point(x: 21, y: 30) print(p) // (21, 30) //String(describing: <#T##CustomStringConvertible#>) let s = String(describing: p) print(s) // (21, 30)如果不实现CustomStringConvertible,直接打印对象,系统会根据默认设置进行输出。我们可以通过CustomStringConvertible对这一输出行为进行设置,还有一个协议是CustomDebugStringConvertible: public protocol CustomDebugStringConvertible { var debugDescription: String { get } }跟CustomStringConvertible用法一样,对应debugPrint的输出。 Hashable 我们常用的Dictionary,Set均实现了Hashable协议。Hash的目的是为了将查找集合某一元素的时间复杂度降低到O(1),为了实现这一目的需要将集合元素与存储地址之间建议一种尽可能一一对应的关系。 我们再看Hashable`协议的定义: public protocol Hashable : Equatable { var hashValue: Int { get } func hash(into hasher: inout Hasher) }public protocol Equatable { static func == (lhs: Self, rhs: Self) -> Bool }注意到func hash(into hasher: inout Hasher),Swift 4.2 通过引入 Hasher 类型并采用新的通用哈希函数进一步优化 Hashable。 如果你要自定义类型实现 Hashable 的方式,可以重写 hash(into:) 方法而不是 hashValue。hash(into:) 通过传递了一个 Hasher 引用对象,然后通过这个对象调用 combine(_:) 来添加类型的必要状态信息。 // Swift >= 4.2 struct Color: Hashable { let red: UInt8 let green: UInt8 let blue: UInt8 // Synthesized by compiler func hash(into hasher: inout Hasher) { hasher.combine(self.red) hasher.combine(self.green) hasher.combine(self.blue) } // Default implementation from protocol extension var hashValue: Int { var hasher = Hasher() self.hash(into: &hasher) return hasher.finalize() } }参考:Hashable / Hasher Codable Codable是可Decodable和Encodable的类型别名。它能够将程序内部的数据结构序列化成可交换数据,也能够将通用数据格式反序列化为内部使用的数据结构,大大提升对象和其表示之间互相转换的体验。处理的问题就是我们经常遇到的JSON转模型,和模型转JSON。 public typealias Codable = Decodable & Encodablepublic protocol Decodable { init(from decoder: Decoder) throws } public protocol Encodable { func encode(to encoder: Encoder) throws }这里只举一个简单的解码过程: //json数据 { "id": "1283984", "name": "Mike", "age": 18 } // 定义对象 struct Person: Codable{ var id: String var name: String var age: Int } // json为网络接口返回的Data类型数据 let mike = try! JSONDecoder().decode(Person.self, from: json) print(mike) //输出:Student(id: "1283984", name: "Mike", age: 18)是不是非常简单,Codable还支持各种自定义解编码过程,完全可以取代SwiftyJSON,HandyJSON等编解码库。 Comparable 这个是用于实现比较功能的协议,它的定义如下: public protocol Comparable : Equatable { static func < (lhs: Self, rhs: Self) -> Bool static func <= (lhs: Self, rhs: Self) -> Bool static func >= (lhs: Self, rhs: Self) -> Bool static func > (lhs: Self, rhs: Self) -> Bool }其继承于Equatable,即判等的协议。可以很清楚的理解实现了各种比较的定义就具有了比较的功能。这个不做比较。 RangeReplaceableCollection RangeReplaceableCollection支持用另一个集合的元素替换元素的任意子范围的集合。 看下它的定义: public protocol RangeReplaceableCollection : Collection where Self.SubSequence : RangeReplaceableCollection { associatedtype SubSequence mutating func append(_ newElement: Self.Element) mutating func insert<S>(contentsOf newElements: S, at i: Self.Index) where S : Collection, Self.Element == S.Element /* 拼接、插入、删除、替换的方法,他们都具有对组元素的操作能力 */ override subscript(bounds: Self.Index) -> Self.Element { get } override subscript(bounds: Range<Self.Index>) -> Self.SubSequence { get } }举个例子,Array支持该协议,我们可以进行如下操作: var bugs = ["Aphid", "Damselfly"] bugs.append("Earwig") bugs.insert(contentsOf: ["Bumblebee", "Cicada"], at: 1) print(bugs) // Prints "["Aphid", "Bumblebee", "Cicada", "Damselfly", "Earwig"]"这里附一张Swift中Array遵循的协议关系图,有助于大家理解上面讲解的几个协议之间的关系:图像来源:https://swiftdoc.org/v3.1/type/array/hierarchy/ 二、@propertyWrapper阅读以下代码,print 输出什么@propertyWrapper struct Wrapper<T> { var wrappedValue: T var projectedValue: Wrapper<T> { return self } func foo() { print("Foo") } } struct HasWrapper { @Wrapper var x = 0 func foo() { print(x) // 0 print(_x) // Wrapper<Int>(wrappedValue: 0) print($x) // Wrapper<Int>(wrappedValue: 0) } }这段代码看似要考察对@propertyWrapper的理解,但是有很多无用内容,导致代码很奇怪。 @propertyWrapper的意思就是属性包装,它可以将一系列相似的属性方法进行统一处理。举个例子,如果我们需要在UserDefaults中加一个是否首次启动的值,正常可以这样处理: extension UserDefaults { enum Keys { static let isFirstLaunch = "isFirstLaunch" } var isFirstLaunch: Bool { get { return bool(forKey: Keys.isFirstLaunch) } set { set(newValue, forKey: Keys.isFirstLaunch) } } }如果我们需要加入很多这样属性的话,就需要写大量的get 、set方法。而@propertyWrapper的作用就是为属性的这种设置提供一个模板写法,以下是使用属性包装的写法。 @propertyWrapper struct UserDefaultWrapper<T> { private let key: String private let defaultValue: T init(key: String, defaultValue: T) { self.key = key self.defaultValue = defaultValue } var wrappedValue: T { get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue } set { UserDefaults.standard.set(newValue, forKey: key) } } }class UserDefaults { @UserDefaultWrapper(key: Keys.isFirstLaunch, defaultValue: false) var isFirstLaunch: Bool }@propertyWrapper约束的对象必须要定义wrappedValue属性,因为该对象包裹的属性会走到wrappedValue的实现。 回到实例代码,定义了wrappedValue却并没有添加任何实现,这是允许的。所以访问x的时候其实是访问Wrapper的wrappedValue,因为没有给出任何实现所以直接打印出0。而_x和$x对应的就是Wrapper自身。 参考:Swift Property Wrappers 三、关键字 public open public open为权限关键词。对于一个严格的项目来说,精确的最小化访问控制级别对于代码的维护来说相当重要的。完整的权限关键词,按权限大小排序如下: open > public > internal > fileprivate > privateopen权限最大,允许外部module访问,继承,重写。 public允许外部module访问,但不允许继承,重写。 internal为默认关键词,在同一个module内可以共用。 fileprivate表示代码可以在当前文件中被访问,而不做类型限定。 private表示代码只能在当前作用域或者同一文件中同一类型的作用域中被使用。这些权限关键词可以修饰,属性,方法和类型。需要注意:当一个类型的某一属性要用public修饰时,该类型至少要用public(或者open)权限的关键词修复。可以理解为数据访问是分层的,我们为了获取某一属性或方法需要先获取该类型,所以外层(类型)的访问权限要满足大于等于内层(类型、方法、属性)权限。 参考:Swift AccessControl static class final 原文中final跟权限关键词放在一起了,其实是不合理的,就将其放到这里来讨论。 static静态变量关键词,来源于C语言。 在Swift中常用语以下场景: // 仅用于类名前,表示该类不能被继承。仅支持class类型 final class Manager { // 单例的声明 static let shared = Manager() // 实例属性,可被重写 var name: String = "Ferry" // 实例属性,不可被重写 final var lastName: String = "Zhang" // 类属性,不可被重写 static var address: String = "Beijing" // 类属性,可被重写。注意只能作为计算属性,而不能作为存储属性 class var code: String { return "0122" } // 实例函数,可被重写 func download() { /* code... */ } // 实例函数,不可被重写 final func download() { /* code... */ } // 类函数,可被重写 class func removeCache() { /* code... */ } // 类函数,不可被重写 static func download() { /* code... */ } }struct Manager { // 单例的声明 static let shared = Manager() // 类属性 static var name: String = "Ferry" // 类函数 static func download() { /* code... */ } }struct和enum因为不能被继承,所以也就无法使用class和final关键词,仅能通过static关键词进行限定 mutating inout mutating用于修饰会改变该类型的函数之前,基本都用于struct对象的修改。看下面例子: struct Point { var x: CGFloat var y: CGFloat // 因为该方法改变了struct的属性值(x),所以必须要加上mutating mutating func moveRight(offset: CGFloat) { x += offset } func normalSwap(a: CGFloat, b: CGFloat) { let temp = a a = b b = temp } // 将两个值交换,需传入对象地址。注意inout需要加载类型名前 func inoutSwap(a: inout CGFloat, b: inout CGFloat) { let temp = a a = b b = temp } }var location1: CGFloat = 10 var location2: CGFloat = -10var point = Point.init(x: 0, y: 0) point.moveRight(offset: location1) print(point) //Point(x: 10.0, y: 0.0)point.normalSwap(a: location1, b: location2) print(location1) //10 print(location2) //-10 // 注意需带取址符& point.inoutSwap(a: &location1, b: &location2) print(location1) //-10 print(location2) //10inout需要传入取值符,所以它的改变会导致该对象跟着变动。可以再回看上面说的Hashable的一个协议实现: func hash(into hasher: inout Hasher) { hasher.combine(self.red) hasher.combine(self.green) hasher.combine(self.blue) }只有使用inout才能修改传入的hasher的值。 infix operator infix operator即为中缀操作符,还有prefix、postfix后缀操作符。 它的作用是自定义操作符。比如Python里可以用**进行幂运算,但是Swift里面,我们就可以利用自定义操作符来定义一个用**实现的幂运算。 // 定义中缀操作符 infix operator ** // 实现该操作符的逻辑,中缀需要两个参数 func ** (left: Double, right: Double) -> Double { return pow(left, right) } let number = 2 ** 3 print(value) //8同理我们还可以定义前缀和后缀操作符: //定义阶乘操作,后缀操作符 postfix operator ~! postfix func ~! (value: Int) -> Int { func factorial(_ value: Int) -> Int { if value <= 1 { return 1 } return value * factorial(value - 1) } return factorial(value) } //定义输出操作,前缀操作符 prefix operator << prefix func << (value: Any) { print(value) }let number1 = 4~! print(number1) // 24<<number1 // 24 <<"zhangferry" // zhangferry前缀和后缀仅需要一个操作数,所以只有一个参数即可。 关于操作符的更多内容可以查看这里:Swift Operators。 注意,因为该文章较早,其中对于操作符的一些定义已经改变。 @dynamicMemberLookup,@dynamicCallable 这两个关键词我确实没有用过,看到dynamic可以知道这两个特性是为了让Swift具有动态性。 @dynamicMemberLookup中文叫动态查找成员。在使用@dynamicMemberLookup标记了对象后(对象、结构体、枚举、protocol),实现了subscript(dynamicMember member: String)方法后我们就可以访问到对象不存在的属性。如果访问到的属性不存在,就会调用到实现的 subscript(dynamicMember member: String)方法,key 作为 member 传入这个方法。 举个例子: @dynamicMemberLookup struct Person { subscript(dynamicMember member: String) -> String { let properties = ["nickname": "Zhuo", "city": "Hangzhou"] return properties[member, default: "undefined"] } } //执行以下代码 let p = Person() print(p.city) //Hangzhou print(p.nickname) //Zhuo print(p.name) //undefined我们没有定义Person的city、nickname,name属性,却可以用点语法去尝试访问它。如果没有@dynamicMemberLookup这种写法会被编译器检查出来并报错,但是加了该关键词编译器就不会管它是不是存在都予以通过。 @dynamicCallable struct Person { // 实现方法一 func dynamicallyCall(withArguments: [String]) { for item in withArguments { print(item) } } // 实现方法二 func dynamicallyCall(withKeywordArguments: KeyValuePairs<String, String>){ for (key, value) in withKeywordArguments { print("\(key) --- \(value)") } } } let p = Person() p("zhangsan") // 等于 p.dynamicallyCall(withArguments: ["zhangsan"]) p("zhangsan", "20", "男") // 等于 p.dynamicallyCall(withArguments: ["zhangsan", "20", "男"]) p(name: "zhangsan") // 等于 p.dynamicallyCall(withKeywordArguments: ["name": "zhangsan"]) p(name: "zhangsan", age:"20", sex: "男") // 等于 p.dynamicallyCall(withKeywordArguments: ["name": "zhangsan", "age": "20", "sex": "男"])@dynamicCallable可以理解成动态调用,当为某一类型做此声明时,需要实现dynamicallyCall(withArguments:)或者dynamicallyCall(withKeywordArguments:)。编译器将允许你调用并为定义的方法。 一个动态查找成员变量,一个动态方法调用,带上这两个特性Swift就可以变成彻头彻尾的动态语言了。所以作为静态语言的Swift也是可以具有动态特性的。 更多关于这两个动态标记的讨论可以看卓同学的这篇:细说 Swift 4.2 新特性:Dynamic Member Lookup where where一般用作条件限定。它可以用在for-in、swith、do-catch中: let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9] for item in numbers where item % 2 == 1 { print("odd: \(item)") // 将输出1,3,5,7,9等数 }numbers.forEach { (item) in switch item { case let x where x % 2 == 0: print("even: \(x)") // 将输出2,4,6,8等数 default: break } }where也可以用于类型限定。 我们可以扩展一个字典的merge函数,它可以将两个字典进行合并,对于相同的Key值以要合并的字典为准。并且该方法我只想针对Key和Value都是String类型的字典使用,就可以这么做: // 这里的Key Value来自于Dictionary中定义的泛型 extension Dictionary where Key == String, Value == String { //同一个key操作覆盖旧值 func merge(other: Dictionary) -> Dictionary { return self.merging(other) { _, new in new } } }@autoclosure @autoclosure 是使用在闭包类型之前,做的事情就是把一句表达式自动地封装成一个闭包 (closure)。 比如我们有一个方法接受一个闭包,当闭包执行的结果为 true 的时候进行打印,分别使用普通闭包和加上autoclosure的闭包实现: func logIfTrueNormal(predicate: () -> Bool) { if predicate() { print("True") } } // 注意@autoclosure加到闭包的前面 func logIfTrueAutoclosure(predicate: @autoclosure () -> Bool) { if predicate() { print("True") } } // 调用方式 logIfTrueNormal(predicate: {3 > 1}) logIfTrueAutoclosure(predicate: 3 > 1)编译器会将logIfTrueAutoclosure函数参数中的3 > 1这个表达式转成{3 > 1}这种尾随闭包样式。 那这种写法有什么用处呢?我们可以从一个示例中体会一下,在Swift系统提供的几个短路运算符(即表达式左边如果已经确定结果,右边将不再运算)中均采用了@autoclosure标记的闭包。那??运算符举例,它的实现是这样的: public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T { switch optional { case .some(let value): return value case .none: return try defaultValue() } } // 使用 var name: String? = "ferry" let currentName = name ?? getDefaultName()因为使用了@autoclosure标记闭包,所以??的defaultValue参数我们可以使用表达式,又因为是闭包,所以当name非空时,直接返回了该值,不会调用getDefaultName()函数,减少计算。 参考:@AUTOCLOSURE 和 ??,注意因为Swift版本问题,实例代码无法运行。 @escaping @escaping也是闭包修饰词,用它标记的闭包被称为逃逸闭包,还有一个关键词是@noescape,用它修饰的闭包叫做非逃逸闭包。在Swift3及之后的版本,闭包默认为非逃逸闭包,在这之前默认闭包为逃逸闭包。 这两者的区别主要在于声明周期的不同,当闭包作为参数时,如果其声明周期与函数一致就是非逃逸闭包,如果声明周期大于函数就是逃逸闭包。结合示例来理解: // 非逃逸闭包 func logIfTrueNormal(predicate: () -> Bool) { if predicate() { print("True") } } // 逃逸闭包 func logIfTrueEscaping(predicate: @escaping () -> Bool) { DispatchQueue.main.async { if predicate() { print("True") } } }第二个函数的闭包为逃逸闭包是因为其是异步调用,在函数退出时,该闭包还存在,声明周期长于函数。 如果你无法判断出应该使用逃逸还是非逃逸闭包,也无需担心,因为编译器会帮你做出判断。第二个函数,如果我们不声明逃逸闭包编译器会报错,警告我们:Escaping closure captures non-escaping parameter 'predicate'。当然我们还是应该理解两者的区别。 四、高阶函数 Filter, Map, Reduce, flatmap, compactMap 这几个高阶函数都是对数组对象使用的,我们通过示例去了解他们吧: let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9] // filter 过滤 let odd = numbers.filter { (number) -> Bool in return number % 2 == 1 } print(odd) // [1, 3, 5, 7, 9]//map 转换 let maps = odd.map { (number) -> String in return "\(number)" } print(maps) // ["1", "3", "5", "7", "9"]// reduce 累计运算 let result = odd.reduce(0, +) print(result) // 25// flatMap 1.数组展开 let numberList = [[1, 2, 3], [4, 5], [[6]]] let flatMapNumber = numberList.flatMap { (value) in return value } print(flatMapNumber) // [1, 2, 3, 4, 5, [6]]// flatMap 2.过滤数组中的nil let country = ["cn", "us", nil, "en"] let flatMap = country.flatMap { (value) in return value } print(flatMap) //["cn", "us", "en"]// compactMap 过滤数组中的nil let compactMap = country.compactMap { (value) in return value } print(compactMap) // ["cn", "us", "en"]filter,reduce其实很好理解,map、flatMap、compactMap刚开始接触时确实容易搞混,这个需要多加使用和练习。 注意到flatMap有两种用法,一种是展开数组,将二维数组降为一维数组,一种是过滤数组中的nil。在Swift4.1版本已经将flatMap过滤数组中nil的函数标位deprecated,所以我们过滤数组中nil的操作应该使用compactMap函数。 参考:Swift 烧脑体操(四) - map 和 flatMap 五、几个Swift中的概念 柯里化什么意思 柯里化指的是从一个多参数函数变成一连串单参数函数的变换,这是实现函数式编程的重要手段,举个例子: // 该函数返回类型为(Int) -> Bool func greaterThan(_ comparer: Int) -> (Int) -> Bool { return { number in return number > comparer } } // 定义一个greaterThan10的函数 let greaterThan10 = greaterThan(10) greaterThan10(13) // => true greaterThan10(9) // => false所以柯里化也可以理解为批量生成一系列相似的函数。 参考:柯里化 (CURRYING) POP 与 OOP的区别 OOP(object-oriented programming)面向对象编程: 在面向对象编程世界里,一切皆为对象,它的核心思想是继承、封装、多态。 POP(protocol-oriented programming)面向协议编程: 面向协议编程则主要通过协议,又或叫做接口对一系列操作进行定义。面向协议也有继承封装多态,只不过这些不是针对对象建立的。 为什么Swift演变成了一门面向协议的编程语言。这是因为面向对象存在以下几个问题: 1、动态派发的安全性(这应该是OC的困境,在Swift中Xcode是不可能让这种问题编译通过的) 2、横切关注点(Cross-Cutting Concerns)问题。面向对象无法描述两个不同事物具有某个相同特性这一点。 3、菱形问题(比如C++中)。C++可以多继承,在多继承中,两个父类实现了相同的方法,子类无法确定继承哪个父类的此方法,由于多继承的拓扑结构是一个菱形,所以这个问题有被叫做菱形缺陷(Diamond Problem)。 参考文章: Swift 中的面向协议编程:是否优于面向对象编程? 面向协议编程与 Cocoa 的邂逅 (上) Any 与AnyObject 区别 AnyObject: 是一个协议,所有class都遵守该协议,常用语跟OC对象的数据转换。 Any:它可以代表任何型別的类(class)、结构体 (struct)、枚举 (enum),包括函式和可选型,基本上可以说是任何东西。 rethrows 和 throws 有什么区别呢? throws是处理错误用的,可以看一个往沙盒写入文件的例子: // 写入的方法定义 public func write(to url: URL, options: Data.WritingOptions = []) throws // 调用 do { let data = Data() try data.write(to: localUrl) } catch let error { print(error.localizedDescription) }将一个会有错误抛出的函数末尾加上throws,则该方法调用时需要使用try语句进行调用,用于提示当前函数是有抛错风险的,其中catch句柄是可以忽略的。 rethrows与throws并没有太多不同,它们都是标记了一个方法应该抛出错误。但是 rethrows 一般用在参数中含有可以 throws 的方法的高阶函数中(想一下为什么是高阶函数?下期给出答案)。 查看map的方法声明,我们能同时看到 throws,rethrows: @inlinable public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]不知道你们第一次见到map函数本体的时候会不会疑惑,为什么map里的闭包需要抛出错误?为什么我们调用的时候并没有用try语法也可以正常通过? 其实是这样的,transform是需要我们定义的闭包,它有可能抛出异常,也可能不抛出异常。Swift作为类型安全的语言就需要保证在有异常的时候需要使用try去调用,在没有异常的时候要正常调用,那怎么兼容这两种情况呢,这就是rethrows的作用了。 func squareOf(x: Int) -> Int {return x * x}func divideTenBy(x: Int) throws -> Double { guard x != 0 else { throw CalculationError.DivideByZero } return 10.0 / Double(x) }let theNumbers = [10, 20, 30] let squareResult = theNumbers.map(squareOf(x:)) // [100, 400, 9000]do { let divideResult = try theNumbers.map(divideTenBy(x:)) } catch let error { print(error) }当我们直接写let divideResult = theNumbers.map(divideTenBy(x:))时,编译器会报错:Call can throw but is not marked with 'try'。这样就实现了根据情况去决定是否需要用try-catch去捕获map里的异常了。 参考:错误和异常处理 break return continue fallthough 在语句中的含义(switch、while、for) 这个比较简单,只说相对特别的示例吧,在Swift的switch语句,会在每个case结束的时候自动退出该switch判断,如果我们想不退出,继续进行下一个case的判断,可以加上fallthough。
iOS开发图片格式选择
图片是如何显示的 在讲解如何选择图片格式之前,我感觉有必要先了解下,图片是如何展示的。如果我们要展示一张图片,一般步骤是这样的: /// Assets.xcassets中的图片,不需要后缀 let image = UIImage(named: "icon") let imageView = UIImageView(frame: rect) imageView.image = image view.addSubview(imageView)运行程序,我们就可以在指定位置看到这个icon。看似简单的代码背后隐藏了很多细节工作。一张图片的展示,从代码执行到展示出来大致经历了这些步骤:1. 加载图片从磁盘中加载一张图片;然后将生成的 UIImage 赋值给 UIImageView ;接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;分配内存缓冲区用于管理文件 IO 和解压缩操作,将文件数据从磁盘读到内存中;2. 图片解码(解压)将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作,默认在主线程进行;3. 图片渲染Core Animation 中CALayer使用解压(解码)的位图数据渲染 UIImageView 的图层;CPU计算好图片的Frame,对图片解压之后,就会交给GPU来做图片渲染渲染流程;GPU获取获取图片的坐标,将坐标交给顶点着色器(顶点计算),将图片光栅化(获取图片对应屏幕上的像素点),片元着色器计算(计算每个像素点的最终显示的颜色值);从帧缓存区中渲染到屏幕上;这其中有个关键步骤是图片解码。那为什么要解码呢,这是因为我们平常使用的图片一般为了节约空间都会经过一些压缩算法进行封装,而使用时屏幕要精确的渲染到每个像素点,这就需要把压缩的图片解码展开,便于系统处理。 名词解释 有损vs无损 有损压缩:指在压缩文件大小的过程中,损失了一部分图片的信息,也即降低了图片的质量,并且这种损失是不可逆的,我们不可能从有一个有损压缩过的图片中恢复出全来的图片。常见的有损压缩手段,是按照一定的算法将临近的像素点进行合并。 无损压缩:只在压缩文件大小的过程中,图片的质量没有任何损耗。我们任何时候都可以从无损压缩过的图片中恢复出原来的信息。 索引色vs直接色 索引色:用一个数字来代表(索引)一种颜色,在存储图片的时候,存储一个数字的组合,同时存储数字到图片颜色的映射。这种方式只能存储有限种颜色,通常是256种颜色,对应到计算机系统中,使用一个字节的数字来索引一种颜色。 直接色:使用四个数字来代表一种颜色,这四个数字分别代表这个颜色中红色、绿色、蓝色以及透明度。现在流行的显示设备可以在这四个维度分别支持256种变化,所以直接色可以表示2的32次方种颜色。当然并非所有的直接色都支持这么多种,为压缩空间使用,有可能只有表达红、绿、蓝的三个数字,每个数字也可能不支持256种变化之多。 点阵图vs矢量图 点阵图:也叫做位图,像素图。构成点阵图的最小单位是象素,位图就是由象素阵列的排列来实现其显示效果的,每个象素有自己的颜色信息,在对位图图像进行编辑操作的时候,可操作的对象是每个象素,我们可以改变图像的色相、饱和度、明度,从而改变图像的显示效果。点阵图缩放会失真,用最近非常流行的沙画来比喻最恰当不过,当你从远处看的时候,画面细腻多彩,但是当你靠的非常近的时候,你就能看到组成画面的每粒沙子以及每个沙粒的颜色。 矢量图:也叫做向量图。矢量图并不纪录画面上每一点的信息,而是纪录了元素形状及颜色的算法,当你打开一张矢量图的时候,软件对图形象对应的函数进行运算,将运算结果[图形的形状和颜色]显示给你看。无论显示画面是大还是小,画面上的对象对应的算法是不变的,所以,即使对画面进行倍数相当大的缩放,其显示效果仍然相同(不失真)。 几种格式的对比 一张图片,如果我们将它的每一个像素及其对应的颜色都存储起来(BMP格式),是会很大的。为了减小图片占用的存储空间,派生出了各种不同压缩算法所代表的图片格式。常见的图片格式有png、jpeg、heic、gif、webp,svg等。 PNG PNG有两种类型:PNG-8和PNG-24。PNG-8是无损的、索引色、点阵图。它支持透明度的调节。PNG-24是无损的、直接色、点阵图。因为使用直接色,颜色范围更大,也占用更大的空间。他的目标是替代JPEG,但一般而言,PNG-24的文件大小是JPEG的五倍之多,而显示效果则通常只能获得一点点提升。所以如果不是对图片质量要求特别高,不建议使用PNG-24PNG是苹果推荐的图片格式,而且这些图片会被一个叫pngcrush的开源工具优化,这样iOS设备就能在显示时更快的解压和渲染图片。该工具位于目录: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/binJPEG JPEG是有损的、采用直接色的、点阵图压缩方式。 JEPG目标是在不影响人类可分辨的图片质量的前提下,尽可能的压缩文件大小。一般都是用于相机图片的格式。但因为有损,会导致图片失真。iOS可以通过以下方式压缩图片: // 压缩比范围从0到1 func jpegData(compressionQuality: CGFloat) -> Data?题外话 一般来说,相同的图片采用JPEG的压缩方式会比png得到更小的尺寸,但也有例外。在网上查了资料说是JEPG更适合处理带有很多杂色的风景图,而对于使用数位板等电子绘制的纯色卡通系风格图片,JEPG的压缩方式会适得其反,导致体积更大。 HEIC HEIC是HEIF(High Efficiency Image Format 高效率图像文件格式)的一种。它并非苹果开发,而是由运动图像专家组(MPEG)开发。它同时支持有损压缩、无损压缩、透明度等特性。HEIF规范的完成是在2015年,是这几种图片格式中最新的一种了,目前除了苹果,还没有哪家大厂去拥抱这种格式。 在iOS 11更新后,iPhone 7及其后硬件,在拍摄照片时的默认图像存储格式。与JPG相比,它占用的空间更小,画质更加无损。HEIC的目的就是作为JPEG的继任者,以后或许会成为一种趋势。目前可以想到的在开发中的应用是,对于一些需要下载的大图可以转成HEIC格式,供客户端使用。但是当前却很少应用,大概率是考虑到图片兼容问题吧。 题外话 一个有趣的现象,我用相机(iPhoneXR)拍摄一张照片,通过AirDrop传到电脑,显示为HEIC格式。当我在拍照时选择系统自带的任意一种滤镜,图片格式就变成了JPEG。这是为什么? 有小伙伴解答: iOS拍照选择滤镜会“转”为JPEG,是因为拍照的格式还是HEIF,加滤镜和编辑图片都是相当于复制了一份再做操作的,点击复原又会“转”为HEIF。 经过测试确实是这样的,而且既然苹果提供复原的操作,说明原图(HEIF)并没有被覆盖。那为什么滤镜不能直接在HEIF格式下操作,猜测可能是跟滤镜的算法相关,该算法只能对JEPG格式编码的图片进行渲染,所以需要中间转成JEPG。 Live Photo Live图片的实质是:一张heic格式封面图 + mov格式视频。 对于Live Photo的展示,在原生应用中可以使用PHLivePhotoView,在Web应用中可以使用LivePhotosKit JS。 WebP WebP最初由Google发布于2010年,图片格式派生自VP8视频编码,也同时支持有损压缩、无损压缩、透明度等特性。2013年低,推出了Animated WebP,还可以支持动图。 WebP 集合了多种图片文件格式的特点。它像 JPEG 一样适合压缩照片和其他细节丰富的图片,像 GIF 一样可以显示动态图片,像 PNG 一样支持透明图像。根据 Google 的测试,WebP 无损压缩图片比 PNG 图片少了 45% 的文件体积,即使这些 PNG 图片在使用 pngcrush 和 PNGOUT 处理后,WebP 依旧可以减少 28% 的文件体积。可以在点击这里查看WebP对其它格式转换的效果。小是WebP的最大优点,小意味着更少的流量,这也是各大流量入口在意的地方。目前Google、Facebook、阿里等大厂已经在广泛使用WebP格式,国内的一些图床服务(七牛、又拍云)也支持将图片自动转成WebP格式。 诚然WebP非常优秀,独自完成了图片格式"大一统"的任务。但苹果对WebP的支持却很少,只Safari目前还不支持WebP显示就阻断了很多人应用WebP的决心。 如果我们需要在项目中显示WebP格式图片就不得不导入Google的libwebp解码库。当然WebP的解码任务在iOS端有些库已经封装好了,OC端可以用SDWebImageWebPCoder,Swift端可以用KingfisherWebP。以下是使用Kingfisher展示WebP图像的事例: // 全局配置对WebP图片的解码(仅针对WebP格式) KingfisherManager.shared.defaultOptions += [ .processor(WebPProcessor.default), .cacheSerializer(WebPSerializer.default) ] // 本地webp图片解码 let localUrl = Bundle.main.url(forResource: "sunset", withExtension: "webp") let dataProvider = LocalFileImageDataProvider.init(fileURL: localUrl!) imageView.kf.setImage(with: dataProvider)// 远程webp图片解码。一些图像服务器可能期望“Accept”标头包含“image/webp”,我们还需要加上 let modifier = AnyModifier { request in var req = request req.addValue("image/webp */*", forHTTPHeaderField: "Accept") return req } KingfisherManager.shared.defaultOptions += [ .requestModifier(modifier), // ... other options ]PDF pdf图片通常是矢量的,它的导入方式有些特殊。我们需要在Assets.xcassets文件,创建一个New Image Set,然后将该文件的Scales设置为Single Scale,拖入1x尺寸的pdf文件即可:使用时我们可以把它当做普通图片对待: let image = UIImage(named: "sunset")在运行期间Xcode会根据屏幕的比例因子生成对应尺寸的png图像。比如导入一张100x100的pdf图片,在2x和3x的机型里面会生成对应的200x200,300x300的png(可以在Assets.car中找到)。所以pdf只不过是Xcode处理图片的中间状态,下载到手机的应用包里面是没有这张pdf的。 这种处理方式有一个好处就是,当苹果以后发布一款4x屏幕的手机时,使用pdf处理的图片会自适应生成对应的4x资源,不需要再手动导入。但相比优点,pdf作为图片资源的缺点更多。 首先是尺寸上,因为是自动生成对应的png,并没有任何优化和压缩,而且我们也并不能在这中间做什么。对比相同尺寸经过ImageOptim压缩过的png,在大小上后者会是前者的1/2,甚至1/4。 另外pdf对阴影和渐变的处理会存在失真的情况:左边是png,右边是pdf。在一些渐变和光影的图像部分可以看出明显的失真。 更多关于pdf和png的差别,可以看这篇:Why I don't use PDFs for iOS assets: https://bjango.com/articles/idontusepdfs/ SVG/SF Symbol SVG是一种无损的矢量图,是众多矢量图中的一种,它的特点是使用XML来描述图片。使用XML的优点是,任何时候你都可以把它当做一个文本文件来对待,也就是说,你可以非常方便的修改SVG图片,你所需要的只需要一个文本编辑器。 在iOS13之前应用中直接使用SVG的场景非常少,但从iOS13开始,苹果推出了SF Symbol,一种svg格式的矢量符号集。而且苹果还提供了多于1500多种icon模板,我们可以在这里下载查看。我们可以从中选择适合自己的icon,选中之后,从File > Export Custom Symbol Templete中导出svg格式图片集,然后拖到Xcode的Assets.xcassets。SF Symbol有9种粗细的调节——从ultralight到black——每一种都相当于San Francisco系统字体的重量(weight)。(SF Symbol中的SF是San Francisco(旧金山)的缩写)。这种对应使您能够在符号和相邻文本之间实现精确的权重匹配,同时支持不同大小和上下文的灵活性。 当然如果这些图标都不能满足需求,我们还可以自定义SF图标,然后通过SF Symbol App进行验证和导出。操作细节可以看这里:Creating Custom Symbol Images for Your App。 SF Symbol使用起来也很简单: let configuration = UIImage.SymbolConfiguration.init(scale: .small) imageView.image = UIImage(systemName: "alarm", withConfiguration: configuration)SF Symbol可以一次性解决相同icon,不同尺寸,不同粗细的问题,它让我们处理图片像处理字体一样方便。可以想象这就是应用图标的未来。 当看到SF Symbol仅支持iOS13+,watchOS6+,我又不得不退回到现实,png也挺好的。 题外话 我在测试SF Symbol图标时,从生成的应用包中查看图片,会得到这样的结果:代码中的我将图片设置为100x100,仅有这一处地方使用。跟pdf类似我们找不到svg源文件,这好理解,svg只是中间状态,我们最终使用的还是png,但为什么会有多个小尺寸的png图像呢? 如何选择图片格式 我们平常开发时,使用最多的就是png了,甚至可能是不加考虑的全部使用png。其实这样是不好的,我们应该充分发挥不同格式图片的优点,从兼容性、空间占用、展示效果三方面考量选取最佳格式。 关于图片格式的选择,苹果的Human Interface Guidelines有以下说法:一般情况下,使用PNG图片,因为PNG支持透明性,而且是无损的,压缩工件不会模糊重要的细节或改变颜色。对于需要阴影、纹理和高光效果的复杂艺术品来说,这是一个不错的选择。使用8位的PNG图形,不需要完全24位的颜色。8位PNG可以在不降低图像质量的情况下减小文件大小。精致的应用图标最好使用png。 对于照片应该使用JPEG格式,它的压缩算法通常比无损格式产生更小的尺寸,而且很难在照片中辨别出来。应该尝试优化JPEG文件,在大小和质量之间找到平衡。大多数JPEG文件可以被压缩而不会导致图像明显的退化。即使是少量的压缩也可以节省大量的磁盘空间。 使用PDF处理字形和其他需要高分辨率缩放的平面矢量图形。最终可以做以下总结。图片格式 适用范围 注意事项png 应用icon,界面icon,卡通风格的背景图 导入项目前可以使用ImageOptim进行压缩jpeg 尺寸较大的风景图,照片 不支持透明度;因为可以调节压缩比,可以在大小和质量之间寻找最佳平衡。webp 支持有损、无损压缩、透明度、动图等特性,因为苹果本身不支持一般只应用于服务端返回来的图片 无法在xcode预览,不建议内置该类型图片pdf 字形,高分辨率的矢量图 存在展开尺寸较大,光效失真的情况svg(sf symbol) 指示性icon 仅支持iOS13及以上,系统sf符号是有版权的,使用时要注意应用范围和苹果要求引用 谈谈 iOS 中图片的解压缩 图片格式那么多,哪种更适合你 iOS图片格式选择 Why I don't use PDFs for iOS assets SF Symbols: The benefits and how to use them guide WebP 的前世今生
Runtime内存模型探究
- 23 Feb, 2020
Objective-C是一种通用、高级、面向对象的编程语言。它扩展了标准的ANSI C编程语言,将Smalltalk式的消息传递机制加入到ANSI C中。可以这么理解,Objective-C = C + Runtime,Runtime是将C语言具有面向对象功能的推动剂,是iOS开发中的核心概念。我们可以在苹果开源的 runtime(当前的最新版本objc4-779.1.tar.gz)中可以发现一些 Objective-C 对象模型的实现细节。NSObject的实现 OC中几乎所有的类都继承自NSObject,OC的动态性也是通过NSObject实现的,那就从NSObject开始探索。 在runtime源码中的NSObject.h中,我们可以找到NSObject的定义: @interface NSObject <NSObject> { Class isa OBJC_ISA_AVAILABILITY; }可以看出NSObject里有一个指向Class的isa,其中对于Class的定义在objc.h: /// An opaque type that represents an Objective-C class. typedef struct objc_class *Class;/// Represents an instance of a class. struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY; };objc_class代表类对象,objc_object代表实例对象,objc_object的isa指向objc_class。 这里可以得出一个结论,实例对象的isa是指向类(类对象)的。其实类(objc_class)也有一个isa属性,那它指向什么呢? Meta Class(元类) 这里runtime为了设计上的统一性,引入了元类(meta class)的概念。 对象的实例方法调用时,通过对象的 isa 在类中获取方法的实现。类对象的类方法调用时,通过类的 isa 在元类中获取方法的实现。 objc_class的isa指向meta class,甚至meta class也有isa指针,它指向根元类(root meta class)。实例对象,类对象,元类和根元类的关系如下图所示:类和元类形成了一个完整的闭环,其中有两条关系需要注意:元类的isa均指向根元类,根元类指向自己 根元类继承根类(NSObject)objective-c1.0数据模型 我们可以在runtime.h中查看objc_class的定义。 struct objc_class { Class _Nonnull isa OBJC_ISA_AVAILABILITY;#if !__OBJC2__ Class _Nullable super_class OBJC2_UNAVAILABLE; const char * _Nonnull name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; #endif} OBJC2_UNAVAILABLE; /* Use `Class` instead of `struct objc_class *` */注意这两个宏命令:!__OBJC2__和OBJC2_UNAVAILABLE,他们均是为了提示当前的objc_class结构是Objc2之前的结构设计,即Objc1.0的设计。 从这个objc_class的定义我们可以看出它包含了超类的指针(super_class),类名(name),实例大小(instance_size),objc_ivar_list成员变量列表的指针(ivars),指向objc_method_list指针的指针(methodLists)。 注意*methodLists是指向方法列表的指针,可以动态修改*methodLists的值来添加成员方法,这也是Category实现的原理,同样解释了Category不能添加属性的原因。 剩下的objc_cache代表函数的缓存列表,objc_protocol_list代表协议列表。 Objective语言历史 我在网上查资料的时候发现关于runtime的文章非常多,但提示数据模型在OC1.0和2.0之间区别的非常少,其实这一点很重要的。这也是为什么我将这段标题命名为Objective-C1.0数据模型的原因。 这里补一点Objective-C语言的发展历史(维基百科):Objective-C1.0 即Objective-C++ 由Stepstone 公司的布莱德·考克斯(Brad Cox)和 汤姆·洛夫(Tom Love) 在 1980 年代发明。它是GCC的一个前端,它可以编译混合C++与Objective-C语法的源文件。Objective-C++是C++的扩展,类似于Objective-C是C的扩展。 Objective-C2.0 在2006年7月苹果全球开发者会议中,Apple宣布了“Objective-C 2.0”的发布,其增加了“现代的垃圾收集,语法改进,运行时性能改进,以及64位支持”。Objective2.0数据模型 可以在objc-runtim-new.h文件找到新版对objc_class的数据模型定义: struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache; // formerly cache pointer and vtable class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags class_rw_t *data() const { return bits.data(); } }struct objc_object { private: isa_t isa; }union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { } Class cls; uintptr_t bits; }会发现objc_class不再是一个单独的结构体,而是继承于objc_object,objc_object内部的isa变成了isa_t的联合体。 class_data_bits_t 我们再回来看类中的其他属性,之前表示类的属性、方法、以及遵循的协议都放在了class_data_bits_t中,更准确的说是放在了class_rw_t。 struct class_data_bits_t { friend objc_class; // Values are the FAST_ flags above. uintptr_t bits; class_rw_t* data() const { return (class_rw_t *)(bits & FAST_DATA_MASK); } }struct class_rw_t { // Be warned that Symbolication knows the layout of this structure. uint32_t flags; uint16_t version; uint16_t witness; const class_ro_t *ro; method_array_t methods; property_array_t properties; protocol_array_t protocols; }struct class_ro_t { uint32_t flags; uint32_t instanceStart; uint32_t instanceSize; #ifdef __LP64__ uint32_t reserved; #endif const uint8_t * ivarLayout; const char * name; method_list_t * baseMethodList; protocol_list_t * baseProtocols; const ivar_list_t * ivars; const uint8_t * weakIvarLayout; property_list_t *baseProperties; }这里面引入了class_rw_t和class_ro_t(rw-readwrite,ro-readonly)两个结构体。可以看到class_rw_t是包含一个常量指针ro,结构体为class_ro_t。这里存储了当前类在编译期就已经确定的属性、方法以及遵循的协议。在 ObjC 运行时的时候会调用 realizeClass 方法,将class_ro_t传入class_rw_t,所以新版的动态性是通过这种方式实现的。 cache_t struct cache_t { static bucket_t *emptyBuckets(); struct bucket_t *buckets(); mask_t mask(); mask_t occupied(); }struct bucket_t { // IMP-first is better for arm64e ptrauth and no worse for arm64. // SEL-first is better for armv7* and i386 and x86_64. #if __arm64__ explicit_atomic<uintptr_t> _imp; explicit_atomic<SEL> _sel; #else explicit_atomic<SEL> _sel; explicit_atomic<uintptr_t> _imp; #endif }cache_t是objc_class中的缓存结构体,里面通过bucket_t结构体存储一些最近调用的函数。设置cache最大的原因就是OC为动态语言,函数的执行是通过消息调用实现的,消息调用会首先查找当前类中的方法列表,如果找不到会查找父类,直到检索至NSObject依然找不到函数实现,就会进入消息转发流程。而为了节省每次查找函数表的开销,发明了cache_t。我们从bucket_t的内联函数中可以看出,缓存的SEL和IMP都是在内存中进行加载的。 method_t struct method_t { SEL name; const char *types; MethodListIMP imp; struct SortBySELAddress : public std::binary_function<const method_t&, const method_t&, bool> { bool operator() (const method_t& lhs, const method_t& rhs) { return lhs.name < rhs.name; } }; };这是函数的结构体,里面包含3个成员变量。SEL是方法的名字name。types是类型编码,类型可参考Type Encoding。IMP是一个函数指针,指向的是函数的具体实现。在runtime中消息传递和转发的目的就是为了找到IMP,并执行函数。 数据模型对比 最后总结下,Objc1.0到2.0的对比:这两张图片引用自寒神博客。 参考链接深入解析 ObjC 中方法的结构 神经病院 Objective-C Runtime 入院第一天—— isa 和 Class
VSCode配置Python版本
- 01 Feb, 2020
刚学习Python没多久,对VSCode也不是很熟悉,在一次解问题的过程中,加深了对这两者的印象,于是记录一下。 环境:Mac OS 10.15,VSCode 1.41.1 问题定位 我在尝试用VSCode写python爬虫,遇到一个问题ImportError: No module named requests。 通过问题描述很容易知道问题,这是因为导入了requests库,但是VSCode却没有找到这个库。问题解决 知道了问题,那就好办,安装requests就行了。requests需要通过pip安装,所以要先安装pip。在安装pip之前还要清楚一件事,就是当前的python版本,因为pip是跟python版本对应的。 升级python 因为python2已经不再维护了,所以要保证当前为python3版本。查看当前python版本在命令行输入python: WARNING: Python 2.7 is not recommended. This version is included in macOS for compatibility with legacy software. Future versions of macOS will not include Python 2.7. Instead, it is recommended that you transition to using 'python3' from within Terminal.Python 2.7.16 (default, Aug 24 2019, 18:37:03) [GCC 4.2.1 Compatible Apple LLVM 11.0.0 (clang-1100.0.32.4) (-macos10.15-objc-s on darwin Type "help", "copyright", "credits" or "license" for more information.得知当前python版本为2.7,所以我们要进行升级。 小提示:命令行运行python就是打开了终端python环境,如果我们想退出该环境,运行exit()即可。 下载python安装包 下载地址当前最新版本为3.8.1,我们下载之后进行安装。 更改默认python版本 虽然我们安装了python3.8,但此时命令行运行python,还是显示为2.7,这是因为我们电脑中运行着两个版本的python,默认版本还是2.7。我们需要更改这项默认配置。 在此之前我们还需要确认终端的shell环境,常见的shell环境是bash和zsh。怎么当前是哪一个呢?在终端运行: $ echo $SHELL1、bash 使用vim打开.bash_profile,当然你也可以使用别的文本编辑器。 vi .bash_profile在最后一行添加: alias python="/Library/Frameworks/Python.framework/Versions/3.8/bin/python3.8"保存之后,更新配置: source .bash_profile2、zsh 编辑.zprofile文件 vi .zprofile在最后一行添加: alias python="/Library/Frameworks/Python.framework/Versions/3.8/bin/python3.8"保存之后,更新配置: source .zprofile然后再次运行python,会看到: Python 3.8.1 (v3.8.1:1b293b6006, Dec 18 2019, 14:08:53) [Clang 6.0 (clang-600.0.57)] on darwin Type "help", "copyright", "credits" or "license" for more information.说明我们已经成功将python默认版本改成了3.8版本。 安装pip 推荐使用pip安装脚本进行安装: $ curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py # 下载安装脚本 $ sudo python get-pip.py # 运行安装脚本因为我们默认的python版本就是3.x版本,所以安装的pip会自动关联到python3版本上。 查看pip版本和路径 $ pip --version会得到以下信息: pip 20.0.2 from /Library/Python/2.7/site-packages/pip (python 2.7)为什么关联的是python2.7版本?这是因为我电脑上之前通过python2.7安装过pip。那对应的python3.8版本的pip呢?它是由pip3表示的,可以验证下: $ pip3 --version会得到: pip 20.0.2 from /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/pip (python 3.8)安装requests 到这一步就很简单了,注意使用pip3执行安装命令。 $ pip3 install requestsVSCode的python环境和本机python环境 我们成功安装了requests,但是在vscode中运行脚本依旧报无法找到requests库的错误。这时我开始怀疑vscode和本机的python环境可能是不同的,而且我本机有两个python版本,vscode用的是哪一个呢?不管怎么样,我应该需要让vscode的python环境指向本机的python3.8,因为我的pip3和requests都是基于这个版本的python安装的。 vscode的python环境 在vscode的python编辑界面的最下面一行会显示当前的python环境:点击它可以看到当前支持python版本:发现并没有我们的python3.8,所以我们需要手动指定python的版本。 指定vscode的python版本 使用快捷键Command + ,可以唤出vscode的设计界面。因为配置项非常多,我们通过搜索命令查询,键入[python path](中括号表示整体搜索),可以找到python路径配置的设置项:默认值是python,我们将它改为/usr/local/bin/python3,然后重启vscode,我们就可以看到当前的python版本已经改为3.8.1了。导入requests的代码执行也没有问题了! 疑问? 这里还有几个疑问: 1、python2.7和3.7版本都是vscode在安装python时自己装进去的吗? 2、如果不通过指定为本机python版本,仅使用vscode中的python版本能否安装pip? 有能帮助解答的小伙伴将不胜感激!
2019年总结
- 02 Jan, 2020
好像随着年龄的增长,对时间的感知就变得越来越弱了。总感觉2019年还是一个很新鲜的年份,但它却已经走完了,2020年好像很遥远的样子,但它已经到来了。来个总结吧,2019年,这一年有太多的酸甜苦辣。从工作、生活和规划三个方面展开说吧。工作年初原项目组有一位很厉害的开发离职了,当时是我俩共同负责一个项目。他开发经验比我多,而且工作态度和习惯都很好,是我见过的未数不多让我称赞的程序员。曾经一起工作的将近一年多时间里,我都以他为学习榜样。对于他的离职,我纠结了很长一段时间,自认这是对我对公司的一大损失。 后来很长的时间里,一直都没有招到合适的人,我便一个人扛起了项目。很多之前应该是两个人一起商量的问题都需要自己一个人去思考解决方式,渐渐的我不断磨练自己的独立思考能力。大半年下来,突然感觉自己被逼着成长了许多,从一个崇拜别人,向别人学习的角色变成了一个不惧怕任何技术难题,能够帮助别人解决困难的角色。 2019年学会的第一个最重要的道理,很多时候有一个可以依赖的对象,是会减弱自己探索和尝试的动力,而开发很多时候对技能的理解和掌握,乃至个人进步,都是要依靠自己的探索获取的。写作 2019年共产出27篇博文,27是一个挺大的数量,但令我满意的文章到不是很多。博客内容质量的重要性应该是要远大于数量的,这一点后面注意到了。 对技术博客的写作今年做了一些不同的尝试。 首先对于月报的更新持续到今年7月份,共产出了一共13篇文章,就停更了。月报内容主要由三部分组成:最近一段时间的bug修复经历和开发tip 阅读过的优秀博文 优秀的github库第一部分,这个受开发经历的影响,就我个人来说,很多时候没有太好的经验可以写,到最后时感觉内容不好撑起这一个模块。 第二部分,出现过几次前期准备不够,最后一两天才去寻找素材,导致推荐内容很一般。 第三部分,优秀库的推荐,写到第十期时就感觉乏力的。当然我知道这里的内容是接近无限的,但是很多如果我都不了解,不使用,只读一个简介就去推荐,会有种不负责任的感觉。 还有一部分原因是,到后来关注的人越来越少,接到的反馈也越来越少,应该是跟质量下降有关。做这个东西也确实费时间,很多时候我就想如果这些时间去打磨一篇技术博客,可能效果会更好。而且月报给人感觉时效性更强,跟新闻类似,过了这个时间之后好像过时了一样,大家很少会特意翻看之前的内容,而普通的技术博客却不会有这种问题,解题思路,技术干货几乎不存在过时问题。 另外我也一直在观察给我启发的 “老司机iOS周报”,他们只有博文推荐和代码库推荐,由多位联合编辑共同完成,人员、时间、内容质量都不是问题。但就我观察得知,“老司机”的号召力和响应力也在不断减弱。技术类人员对内容聚合类产品的粘性是没有固定知识分享平台高的。一篇周报,十篇左右文章,大致看去,自己感兴趣的可能就两三篇,新窗口打开,导流到别的地方,有时间的话读一读,时间不够就放到收藏夹。一般在周报的停留时间不超过3分钟。 基于这些原因吧,我也就不再更新月报了,而是把时间分配到别的事情上去。 公众号 另外,开始尝试写公众号,名字叫“iOS成长之路”。文章更新不多,其中最主要的原因就是我习惯了写博客,而且博客有一套自己的workflow。一般我是先写博客,然后将文章复制到别的平台。像之前的简书和后来的掘金,都是可以全量复制的。 转到公众号时,虽然可以直接将Markdown转成公众号形式,但对图片却不识别,需要单独导入。我需要把github上的图片下载下来,放到公众号编辑界面。如果图多,就是项很繁琐的流程。而且公众号还限制了每天只能发一篇文章,所以后面更新了几次之后就不再更新了。如果有小伙伴知道解决方案的话,希望能够告知。之后的话,会想把公众号这个事情再推进下。 生活那些经历 今年基本没有什么活动。 清明节,回家看妈妈,不自觉拍了很多家里,地里的照片,感觉老家哪里都是美的。那个我曾经抵触,一心想逃离的家乡,在这个春天里一下变得可爱,温馨起来。走到路上我想起曾经走在这里发生过的一切,到了家里我回想这个房屋里曾经发生过的一切,那些欢声笑语。记忆一点点展开,如果此时放弃工作,放弃所有,跟爸爸就在老家生活下去,也未尝不可,我这样想着,对未来的规划和期许,已经都不重要了。但是也只能想想,现实还有很多问题需要考虑,需要处理,我已经被社会规则裹挟着由不得自己了,这可能就是长大的无奈吧。 4月份中旬,参加了《天火》的电影宣发,见到了很多明星,但也并没有太多兴奋,就感觉明星跟普通人其实也没有太多不同。12月12号,电影上映,因为宣发时收到了很多到场礼品,就买了票去支持下,女友和我观点一致:电影很烂,昆凌演的一般,剧情太散。配备了近乎完美的制作团队,外加周杰伦的宣传,电影竟然如此扑街,真的很让人匪夷所思。今年娱乐活动极少,一共就去电影院看了两场电影,其中一部还是这种感受。剧情,剧情,剧情,这是电影的灵魂,真的很重要啊。 4月底,参加我们609寝室第一个结婚的室友婚礼,分别两年多了再相聚,大家都基本没变。我们还是曾经的我们,各奔东西,又再次相聚,真好。 4月份报名了半马,但是很遗憾没有中签,直接导致了去年的一项目标没有完成。 5月份没有出门,跟女朋友在家探索了很多美食的做法,每次做好吃的,我都会用手机拍下来,相册里已经有几十张美食照片了。做饭其实是一项很有趣的事情,洗菜,切菜,食材下锅一瞬间的滋滋声,出锅时的香气四溢,都能不断激发自己的食欲,这可比塑料彩盒包装的外卖有灵魂多了。所以这一年不光点外卖次数少了很多,厨艺还增进了不少。 6月份重新拾起了跑步的习惯,每天下午下班之前的半个小时都陪菲姐去园区跑道跑步,这个习惯帮助她减掉了十几斤肉,也帮助我在年度跑步计划中增加了将近200公里的里程。 7月份准备园区的篮球比赛,虽然每次练习都挥汗如雨,各种制定战术,尽心准备,但还是改变不了比赛被别的公司虐的结局。虽然篮球比不过他们,但是我们敲代码比他们厉害啊,这么安慰自己。但老实说,为什么我感觉打球比敲代码还有意思啊。教练,我想打球。 8月份,配父亲去郑州等肾脏配型,当时想的是父亲留在这里,等有手术消息了,我再赶过去,但是医院要求等待期间必须要有家属陪同,那就回不了北京了。跟公司说明情况之后想申请远程办公,当时想的是如果公司不同意就直接离职。最后公司领导商量之后决定同意我的请求,之后就开始了长达45天的在郑州远程办公。 9月份,工作日期间,我将作息时间调整到跟公司一样的点,有时候去附近的咖啡店,有时候就在家办公。这期间最大的挑战就是自律和需求沟通。因为有时候配父亲做检查,或者送饭,耽误一些时间,这个就要想办法从别的地方补上来,还有就是家里的工作氛围比着公司要差一下,注意力的持续状态要短一些。所以这段时间基本周六都会根据情况抽出时间用来补进度。当然远程办公最爽的一点就是时间可以自由安排,可以是忙完一项任务或者通过时间挪移,你就可以在任意时间段做自己能做的事情。 当然因为效率没有在公司高,所以空闲时间相较而言还是更少了。这里的调整就是时间规划和时间管理的内容了,这个在未来我会重点优化下。 10月份,参加我们609第二个结婚的小伙伴婚礼,还顺道去了一趟大学校园。校园里多了好几栋教学楼和宿舍楼,当年的公教没变,宿舍楼变成了女寝?图书馆还是那么霸气,甚至想进去找个地方看会书。多好的环境啊,为什么当初没有好好珍惜,努力学习呢。好像在回忆过往时,我从来都是责备自己不够努力,不够优秀。 但是好像这就是真的,2019年也是,我并没有拼进全力,很多事情都没有做好,我应该更加努力,多逼一逼自己才是。 11月份,一直陪父亲等的肾脏配型终于有了结果,父亲接受手术,我来郑州陪护。这段经历大多数记在博客里面,这期间确实让我成长了不少。后来父亲出院,我回北京的第二天在V站看到一个帖子,一个程序员长时间加班导致身体不适,去医院检查被诊断为CKD4期(慢性肾病4期)。而CKD5期就是尿毒症,就是父亲的那个病。虽然我在医院见过很多病人了,但这个病的魔爪伸到了程序员群体,还是很让我惊讶和担忧的。 不只尿毒症,很多病都在向低龄化发展。健康!健康!健康!这个真的非常重要,作息,饮食,这两个最容易导致身体出问题的环节一定要重视啊。 12月份,在天桥看了一场话剧《黄金百万》,演员均非科班出身,当时还只是第三场演出,所以整体感觉水分很大,甚至一度有中途撤离的冲动。话剧演出结束,演员讲述舞台背后的故事时,却让我得到了些意外收获。他们是由互联网圈,投资圈,主持人圈的几个朋友组建起来的,其中就有为人熟知的张泉灵和于光东。他们都不缺钱,也不指望依靠话剧挣钱,就凭着爱好,组织话剧社,在本职工作之余,挤时间进行排练,很多人都不在北京居住,但他们还是圆满的完成了这次表演。 这让我看到了一种生活态度,工作不应该完全占据生活,生活也不应该只是吃喝玩乐,我们应该发觉自己的爱好,潜力,去做一些能带给自己满足感,更有意义的事情,这样才是精彩的活着。所以2020年,我也会往这方面做一些尝试。 关于理财 看到很多人晒今年收益,50%,甚至120%。因为今年经济整体都是上行,大多数都是赚的,只是赚多赚少而已。像我这种理财小白,只是定投指数基金不动都有将近30%的收益。所以大家对于很多人的收益不要眼红,不要过于乐观。今年大好,明年是好还是坏谁也说不准了。理财的一条金科玉律就是,收益越大风险越大,能承受风险,有一定的专业知识,那就努力达到收益最大化。如果都做不到,那就不要定过高的目标,像我目前定的目标就是跑赢通货膨胀,保证手里的钱不贬值就行。 理财这项技能,可以看到上限还是很高的,以后会花更多的时间在上面做一些研究,补足这方面的能力,努力提高收益。 关于阅读 阅读方面,近几年看的书越来越少了,今年只有: 《软技能:代码之外的生存指南》 《图解TCP/IP》 《重构》 《未来简史》 《老人与海》 《白夜行》 《半小时漫画中国史+世界史》 《如何高效读懂一本书》这里最推荐的就是《软技能》,它介绍了程序员在编程能力之外还应该具备哪些能力,涵盖职业规划,自我营销,学习方法,理财手段,健身,精神提升等方面。这本书影响了我对自己职业的看法,同时也让我对未来的规划更坚定,更清晰了一些。非常建议大家都读一读。 OKR总结2019年OKR总结 O1:精进技术栈KR 完成进度 总结刷50道LeetCode,输出解题思路 10% 时间问题逆向实践 0% 逆向理论没有持续深入研究,时间问题翻译5篇技术文章 60% 翻译水平有待提高,输出较慢博客主题优化 10% 学习前端要系统的学习,低估了这个工作量开发微习惯APP 5% 发现有别的APP做的很好了,另因产品设计能力短时间没有补全,及时间问题,后来做罢博客输出24篇文章 112% 因为多了几篇生活中的文章,如果只算技术文章也是不够的总结:实现度较低,很多是因为时间问题,和对制定任务的难度预估不准确导致的。这一年对技术层面的提升总综合性,没有在某一点有所突出,今年需要做一些调整。 O2:攒更多的钱KR 完成进度 总结工资外收入达到1000 200% 有段时间帮助一个团队处理了几个问题,获得了一些报酬总收入达到买房需要的首付一半 50% 收入是达到了,但是存款却没有总结:这几项定的目标都有些含糊了,工资外收入其实更像表达的是,写博客的打赏,和公众号的一些收入。总收入的目标其实没有意义,存款才有意义。 O3:运动健身KR 完成进度 总结半程马拉松跑到2小时以内 0% 因为没有中签,也就没有跑半马的经历了咕咚累积里程达到500公里 99% 依靠每天下午的跑步,基本追上了这个目标总结:跑步里程到是一个合理的目标,半马因为受外界因素影响,所以是一个不太好的KR。 2020年OKR 有些在2019年没有做好的事情,会在新的一年里持续的跟进优化。 O1:精进技术栈 KR1:刷20道经典LeetCode题目,输出2篇解题思路的文章 KR2:学习前端知识,优化博客小站 KR3:输出5篇对计算机知识总结的文章 KR4:维护一个Swift库,用于筛选项目中不用的文件 O2:个人成长 KR1:公众号粉丝达到500 KR2:学习基金知识,分析对比10种基金的数据表现 KR3:全年跑步里程400公里 KR4:阅读15本书 KR5:培养时间规划能力,总结并践行一份时间规划清单
OC项目转Swift指南
- 08 Dec, 2019
运行环境:Xcode 11.1 Swift5.0最近参与的一个项目需要从Objective-C(以下简称OC)转到Swift,期间遇到了一些坑,于是有了这篇总结性的文档。如果你也有将OC项目Swift化的需求,可以作为参考。 OC转Swift有一个大前提就是你要对Swift有一定的了解,熟悉Swift语法,最好是完整看过一遍官方的Language Guide。 转换的过程分自动化和手动转译,鉴于自动化工具的识别率不能让人满意,大部分情况都是需要手动转换的。自动化工具 有一个比较好的自动化工具Swiftify,可以将OC文件甚至OC工程整个转成Swift,号称准确率能达到90%。我试用了一些免费版中的功能,但感觉效果并不理想,因为没有使用过付费版,所以也不好评价它就是不好。 Swiftify还有一个Xcode的插件Swiftify for Xcode,可以实现对选中代码和单文件的转化。这个插件还挺不错,对纯系统代码转化还算精确,但部分代码还存在一些识别问题,需要手动再修改。手动Swift化 桥接文件 如果你是在项目中首次使用Swift代码,在添加Swift文件时,Xcode会提示你添加一个.h的桥接文件。如果不小心点了不添加还可以手动导入,就是自己手动生成一个.h文件,然后在Build Settings > Swift Compiler - General > Objective-C Bridging Header中填入该.h文件的路径。这个桥接文件的作用就是供Swift代码引用OC代码,或者OC的三方库。 #import "Utility.h" #import <Masonry/Masonry.h>在Bridging Header的下面还有一个配置项是Objective-C Generated Interface Header Name,对应的值是ProjectName-Swift.h。这是由Xcode自动生成的一个隐藏头文件,每次Build的过程会将Swift代码中声明为外接调用的部分转成OC代码,OC部分的文件会类似pch一样全局引用这个头文件。因为是Build过程中生成的,所以只有.m文件中可以直接引用,对于在.h文件中的引用下文有介绍。 Appdelegate(程序入口) Swift中没有main.m文件,取而代之的是@UIApplicationMain命令,该命令等效于原有的执行main.m。所以我们可以把main.m文件进行移除。 系统API 对于UIKit框架中的大部分代码转换可以直接查看系统API文档进行转换,这里就不过多介绍。 property(属性) Swift没有property,也没有copy,nonatomic等属性修饰词,只有表示属性是否可变的let和var。 注意点一 OC中一个类分.h和.m两个文件,分别表示用于暴露给外接的方法,变量和仅供内部使用的方法变量。迁移到Swift时,应该将.m中的property标为private,即外接无法直接访问,对于.h中的property不做处理,取默认的internal,即同模块可访问。 对于函数的迁移也是相同的。 注意点二 有一种特殊情况是在OC项目中,某些属性在内部(.m)可变,外部(.h)只读。这种情况可以这么处理: private(set) var value: String就是只对value的set方法就行private标记。 注意点三 Swift中针对空类型有个专门的符号?,对应OC中的nil。OC中没有这个符号,但是可以通过在nullable和nonnull表示该种属性,方法参数或者返回值是否可以空。 如果OC中没有声明一个属性是否可以为空,那就去默认值nonnull。 如果我们想让一个类的所有属性,函数返回值都是nonnull,除了手动一个个添加之外还有一个宏命令。 NS_ASSUME_NONNULL_BEGIN /* code */ NS_ASSUME_NONNULL_ENDenum(枚举) OC代码: typedef NS_ENUM(NSInteger, PlayerState) { PlayerStateNone = 0, PlayerStatePlaying, PlayerStatePause, PlayerStateBuffer, PlayerStateFailed, };typedef NS_OPTIONS(NSUInteger, XXViewAnimationOptions) { XXViewAnimationOptionNone = 1 << 0, XXViewAnimationOptionSelcted1 = 1 << 1, XXViewAnimationOptionSelcted2 = 1 << 2, }Swift代码 enum PlayerState: Int { case none = 0 case playing case pause case buffer case failed } struct ViewAnimationOptions: OptionSet { let rawValue: UInt static let None = ViewAnimationOptions(rawValue: 1<<0) static let Selected1 = ViewAnimationOptions(rawValue: 1<<0) static let Selected2 = ViewAnimationOptions(rawValue: 1 << 2) //... }Swift没有NS_OPTIONS的概念,取而代之的是为了满足OptionSet协议的struct类型。 懒加载 OC代码: - (MTObject *)object { if (!_object) { _object = [MTObject new]; } return _object; }Swift代码: lazy var object: MTObject = { let object = MTObject() return imagobjecteView }()闭包 OC代码: typedef void (^DownloadStateBlock)(BOOL isComplete);Swift代码: typealias DownloadStateBlock = ((_ isComplete: Bool) -> Void)单例 OC代码: + (XXManager *)shareInstance { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }); return instance; }Swift对单例的实现比较简单,有两种方式: 第一种 let shared = XXManager()// 声明在全局命名区(global namespace) Class XXManager { }你可能会疑惑,为什么没有dispatch_once,如何保证多线程下创建的唯一性?其实是这样的,Swift中全局变量是懒加载,在AppDelegate中被初始化,之后所有的调用都会使用该实例。而且全局变量的初始化是默认使用dispatch_once的,这保证了全局变量的构造器(initializer)只会被调用一次,保证了shard的原子性。 第二种 Class XXManager { static let shared = XXManager() private override init() { // do something } }Swift 2 开始增加了static关键字,用于限定变量的作用域。如果不使用static,那么每一个shared都会对应一个实例。而使用static之后,shared成为全局变量,就成了跟上面第一种方式原理一致。可以注意到,由于构造器使用了 private 关键字,所以也保证了单例的原子性。 初始化方法和析构函数 对于初始化方法OC先调用父类的初始化方法,然后初始自己的成员变量。Swift先初始化自己的成员变量,然后在调用父类的初始化方法。 OC代码: // 初始化方法 @interface MainView : UIView @property (nonatomic, strong) NSString *title; - (instancetype)initWithFrame:(CGRect)frame title:(NSString *)title NS_DESIGNATED_INITIALIZER; @end@implementation MainView - (instancetype)initWithFrame:(CGRect)frame title:(NSString *)title { if (self = [super initWithFrame:frame]) { self.title = title; } return self; } @end // 析构函数 - (void)dealloc { //dealloc }Swift代码: class MainViewSwift: UIView { let title: String init(frame: CGRect, title: String) { self.title = title super.init(frame: frame) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { //deinit } }函数调用 OC代码: // 实例函数(共有方法) - (void)configModelWith:(XXModel *)model {} // 实例函数(私有方法) - (void)calculateProgress {} // 类函数 + (void)configModelWith:(XXModel *)model {}Swift代码: // 实例函数(共有方法) func configModel(with model: XXModel) {} // 实例函数(私有方法) private func calculateProgress() {} // 类函数(不可以被子类重写) static func configModel(with model: XXModel) {} // 类函数(可以被子类重写) class func configModel(with model: XXModel) {} // 类函数(不可以被子类重写) class final func configModel(with model: XXModel) {}OC可以通过是否将方法声明在.h文件表明该方法是否为私有方法。Swift中没有了.h文件,对于方法的权限控制是通过权限关键词进行的,各关键词权限大小为: private < fileprivate < internal < public < open 其中internal为默认权限,可以在同一module下访问。 NSNotification(通知) OC代码: // add observer [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(method) name:@"NotificationName" object:nil]; // post [NSNotificationCenter.defaultCenter postNotificationName:@"NotificationName" object:nil];Swift代码: // add observer NotificationCenter.default.addObserver(self, selector: #selector(method), name: NSNotification.Name(rawValue: "NotificationName"), object: nil) // post NotificationCenter.default.post(name: NSNotification.Name(rawValue: "NotificationName"), object: self)可以注意到,Swift中通知中心NotificationCenter不带NS前缀,通知名由字符串变成了NSNotification.Name的结构体。 改成结构体的目的就是为了便于管理字符串,原本的字符串类型变成了指定的NSNotification.Name类型。上面的Swift代码可以修改为: extension NSNotification.Name { static let NotificationName = NSNotification.Name("NotificationName") } // add observer NotificationCenter.default.addObserver(self, selector: #selector(method), name: .NotificationName, object: nil) // post NotificationCenter.default.post(name: .NotificationName, object: self)protocol(协议/代理) OC代码: @protocol XXManagerDelegate <NSObject> - (void)downloadFileFailed:(NSError *)error; @optional - (void)downloadFileComplete; @end@interface XXManager: NSObject @property (nonatomic, weak) id<XXManagerDelegate> delegate; @endSwift中对protocol的使用拓宽了许多,不光是class对象,struct和enum也都可以实现协议。需要注意的是struct和enum为指引用类型,不能使用weak修饰。只有指定当前代理只支持类对象,才能使用weak。将上面的代码转成对应的Swift代码,就是: @objc protocol XXManagerDelegate { func downloadFailFailed(error: Error) @objc optional func downloadFileComplete() // 可选协议的实现 } class XXManager: NSObject { weak var delegate: XXManagerDelegate? }@objc是表明当前代码是针对NSObject对象,也就是class对象,就可以正常使用weak了。 如果不是针对NSObject对象的delegate,仅仅是普通的class对象可以这样设置代理: protocol XXManagerDelegate: class { func downloadFailFailed(error: Error) } class XXManager { weak var delegate: XXManagerDelegate? }值得注意的是,仅@objc标记的protocol可以使用@optional。 Swift和OC混编注意事项 函数名的变化 如果你在一个Swift类里定义了一个delegate方法: @objc protocol MarkButtonDelegate { func clickBtn(title: String) }如果你要在OC中实现这个协议,这时候方法名就变成了: - (void)clickBtnWithTitle:(NSString *)title { // code }这主要是因为Swift有指定参数标签,OC却没有,所以在由Swift方法名生成OC方法名时编译器会自动加一些修饰词,已使函数作为一个句子可以"通顺"。 在OC的头文件里调用Swift类 如果要在OC的头文件里引用Swift类,因为Swift没有头文件,而为了让在头文件能够识别该Swift类,需要通过@class的方法引入。 @class SwiftClass;@interface XXOCClass: NSObject @property (nonatomic, strong) SwiftClass *object; @end对OC类在Swift调用下重命名 因为Swift对不同的module都有命名空间,所以Swift类都不需要添加前缀。如果有一个带前缀的OC公共组件,在Swift环境下调用时不得不指定前缀是一件很不优雅的事情,所以苹果添加了一个宏命令NS_SWIFT_NAME,允许在OC类在Swift环境下的重命名: NS_SWIFT_NAME(LoginManager) @interface XXLoginManager: NSObject @end这样我们就将XXLoginManager在Swift环境下的类名改为了LoginManager。 引用类型和值类型 1、 struct 和 enum 是值类型,类 class 是引用类型。 2、String,Array和 Dictionary都是结构体,因此赋值直接是拷贝,而NSString, NSArray 和NSDictionary则是类,所以是使用引用的方式。 3、struct 比 class 更“轻量级”,struct 分配在栈中,class 分配在堆中。 id类型和AnyObject OC中id类型被Swift调用时会自动转成AnyObject,他们很相似,但却其实概念并不一致。 AnyObject可以代表任何class类型的实例; 其他语法区别及注意事项(待补充) 1、Swift语句中不需要加分号;。 2、关于Bool类型更加严格,Swift不再是OC中的非0就是真,真假只对应true和false。 3、Swift类内一般不需要写self,但是闭包内是需要写的。 4、Swift是强类型语言,必须要指定明确的类型。在Swift中Int和Float是不能直接做运算的,必须要将他们转成同一类型才可以运算。 5、Swift抛弃了传统的++,--运算,抛弃了传统的C语言式的for循环写法,而改为for-in。 6、Swift的switch操作,不需要在每个case语句结束的时候都添加break。 7、Swift对enum的使用做了很大的扩展,可以支持任意类型,而OC枚举仅支持Int类型,如果要写兼容代码,要选择Int型枚举。 8、Swift代码要想被OC调用,需要在属性和方法名前面加上@objc。 9、Swift独有的特性,如泛型,struct,非Int型的enum等被包含才函数参数中,即使添加@objc也不会被编译器通过。 10、Swift支持重载,OC不支持。 11、带默认值的Swift函数再被OC调用时会自动展开。 语法检查 对于OC转Swift之后的语法变化还有很多细节值得注意,特别是对于初次使用Swift这门语言的同学,很容易遗漏或者待着OC的思想去写代码。这里推荐一个语法检查的框架SwiftLint,可以自动化的检查我们的代码是否符合Swift规范。 可以通过cocoapods进行引入,配置好之后,每次Build的过程,Lint脚本都会执行一遍Swift代码的语法检查操作,Lint还会将代码规范进行分级,严重的代码错误会直接报错,导致程序无法启动,不太严重的会显示代码警告(⚠️)。 如果你感觉SwiftLint有点过于严格了,还可以通过修改.swiftlint.yml文件,自定义属于自己的语法规范。
【译】iOS 架构模式--浅析MVC, MVP, MVVM 和 VIPER
作者:Bohdan Orlov 原文地址:https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52在iOS开发过程中你是否对MVC的使用感觉很别扭?你是否对转向MVVM有疑惑?你听说过VIPER,但不清楚这个东西是否值得一试。 接着读下去,你会找到上面这些问题的答案。如果读完仍不能解惑,欢迎到评论区捶我。 接下来你将在iOS环境下构建关于架构模式的知识体系。我们将简要构建一些经典的例子,并在理论和实践上进行比较他们的不同。如果你需要更多关于任何一个特定的细节,请关注我。掌握设计模式容易让人上瘾,所以要注意:阅读本文之前要问自己几个问题:谁应该持有网络请求:Model还是Controller? 如何在一个新的View中向ViewModel传递Model 谁创建了一个新的VIPER模块:Router还是Presenter为什么应该关心选用何种架构 因为如果你不这么做的话,总有一天,等这个类庞大到同时处理十几种事务,你会发现你根本无法从中找到对应代码并修改bug。当然,将这整个类了然于心是很难的,你会常常忘记一些重要的细节。如果你的程序已经处于这种状态了,那它很可能具有下面这些特征:这个类是UIViewController的子类 你的数据直接在UIViewController中进行存储 你的UIView基本什么都没做 你的Model只是一个单纯的数据结构 你的单元测试没有覆盖任何代码即使你认为自己遵守了苹果的指导,并按照苹果推荐的MVC设计规范进行开发,但还是遇到了这些问题。不要担心,这是因为苹果的MVC本身就存在一些问题,我们稍后会再来讨论它。 让我们定义一下好的架构应该具备的特点: 1、平衡的分配实体和具体角色的职责 2、把可测试性放在第一位(通过合适的架构,这将很容易实现) 3、易用性和低维护成本 为什么要分配职责 职责的分配能让我们在尝试搞清楚事情如何运作这一过程中保持一个正常的负荷。你可能会认为你投入的精力越多你的大脑越能适应更加复杂的东西,这没错。但是这个能力是非线性的,而且会很快达到临界点。所以降低复杂性的最好的方式是,根据职责单一原则将它的功能(职责)分配到多个实体中。 为什么要可测试性 对于那些已经添加了单元测试的项目来说,当他们增加一个新的功能或者重构一个复杂的类时会由单元测试告知失败与否,这多让人很放心啊。同时这也意味着这些测试项将在运行时帮助开发者找到问题,而如果这些问题发生在用户设备上的,解决他们通常会花费一周。 为什么要易用性 这并不需要答案,但值得一提的是,最好的代码是那些从未写过的代码。所以,代码越少,bug就越少。这意味着,编写更少代码的愿望不应该仅仅由开发人员的懒惰来解释,而且你不应该为了采用更好的解决方案,而对其维护成本视而不见。 MV(X)的要素 如今我们又很多可选的架构方案:MVC MVP MVVM VIPER 前三项方案是把应用程序的实体分为三类: Modes -- 负责数据域和操作数据的数据访问层,例如'Person’类, 'PersonDataProvider'类。 Views -- 负责展示层(GUI),对于iOS环境就是指所有已‘UI’开头的类。 Controller/Presenter/ViewModel -- 是Model和View的中介,通常的职责是通过响应用户在View的操作改变Model,然后根据Model的变化更新View。这些实体的分割帮助我们:更好的理解他们 重用他们(通常是View和Model) 单独测试他们让我们开始讲解MV(X)模式,随后是VIPER MVC 它原本是什么样的 在讨论苹果的MVC版本之前,让我们看一下传统的MVC模式:这个模式下,View是无状态的。它只是简单的被Controller渲染当Model变化的时候。想一下Web页面,当你点一个链接尝试跳转时,整个页面都会重新渲染。尽管可以在iOS应用程序中实现传统的MVC,但由于架构问题,这并没有多大意义—— 所有三个实体都是紧密耦合的,每个实体都知道其他两个。这正好降低了他们的重用性,而这又是你在程序中不想看到的。因为这个原因,我们将跳过编写传统MVC代码的示例。传统的MVC似乎不适合现代的iOS开发苹果的MVC 预期效果Controller是View和Model的中介,因此它俩互相不知道对方。可重用性最差的就是Controller,因为我们必须为复杂的业务逻辑提供一个位置,Model又不适合。 理论上,这看起来很简单,但是你总感觉有什么地方不对,是吧?你甚至听到人们解读MVC为Massive View Controller。也因此,视图控制器的简化成了iOS开发一个重要的课题。苹果只是采用传统的MVC并对其进行一些改进,为什么会出现这种情况呢? 现实情况Cocoa MVC鼓励你编写大量的视图控制器,因为它们是视图生命周期的一部分,很难说它们是独立的。尽管你有能力转移一些业务逻辑和数据转换工作到Model中,当需要转移工作到View时你仍然没有太多选择,因为大多数情况View的职责就是发送行为到Controller。视图控制器最终将成为一个所有东西的委托、数据源、负责调度和取消网络请求,等等。 这种代码,你肯定见过很多少次了: var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell userCell.configureWithUser(user)这个cell是View,直接通过Model进行配置,这明显违反了MVC的要求,但这种事情却经常发生,而且认为还不认为这是错的。如果你严格按照MVC的 做法,你应该在Controller里面配置cell,而不是将Model直接传递给View,但这样就会增加Controller的大小。Cocoa MVC 被称为 Massive View Controller是多么合理啊。这个问题还不是那么明显,直到提到单元测试(希望它存在于你的项目)。因为你的视图控制器跟View是紧耦合的,这将使得测试非常困难。所以你应该让你的业务逻辑和视图布局代码尽可能分割开来。 让我们看一个简单的例子: import UIKitstruct Person { // Model let firstName: String let lastName: String }class GreetingViewController : UIViewController { // View + Controller var person: Person! let showGreetingButton = UIButton() let greetingLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside) } func didTapButton(button: UIButton) { let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName self.greetingLabel.text = greeting } // layout code goes here } // Assembling of MVC let model = Person(firstName: "David", lastName: "Blaine") let view = GreetingViewController()这样根本没法测试,对吧?我们可以把greeting的赋值移到一个新的类GreetingModel中,然后分开测试它。但是我们无法在不直接调用UIView相关方法(viewDidLoad, didTapButton)的情况下测试任何外在的逻辑,而如果这样做,这些方法就导致所有view的刷新,所以这本身就是一个不好的单元测试。 事实上,在一个模拟器上加载和测试UIViews表现正常,不代表它在别的设备依然这样。所以我建议测试时移除单元测试对“宿主程序”的依赖,而直接测试代码。View和Controller之间的交互行为无法通过Unit Tests进行。根据上面的说法,Cocoa MVC是一个相当不好的架构方案。让我们再来根据文章开头定义的好架构应有的特性来评价下它:职责分离 -- View和Model是分离的,但View和Controller是紧耦合关系。 可测试性 -- 由于不好的职责分离特性,只有Model层是可以测试的。 易用性 -- 这几种架构模式中它的代码量是最少的。每个人都很熟悉这种模式,即使是一个经验有限的开发者也可以很容易的维护这份代码。如果你不打算投入很多事情在架构上,或者你感觉对于你们的小项目来说不值得投入过多维护成本,那你应该选择Cocoa MVC。Cocoa MVC 是开发速度最快的一种架构。MVP这是不是更苹果的MVC非常像?确实是这样的,它的名字叫做MVP。等一下,这是不是意味着苹果的MVC事实上就是MVP?不。你可以再观察下这个结构,View和Controller是紧耦合关系,作为MVP的中介者 -- Presenter并没有管理视图控制器的生命周期,它里面也不含有布局代码,它的职责是根据数据的状态变化更新View,所以呢,View这一层就可以很简单的抽出来。我会告诉你,UIViewController也是View在MVP模式下,UIViewController的子类实际上是Views而不是Presenters。这种区别提供了极好的可测试性,但这是以开发速度为代价的,因为你必须手动绑定数据和时间,就像这个例子: import UIKitstruct Person { // Model let firstName: String let lastName: String }protocol GreetingView: class { func setGreeting(greeting: String) }protocol GreetingViewPresenter { init(view: GreetingView, person: Person) func showGreeting() }class GreetingPresenter : GreetingViewPresenter { unowned let view: GreetingView let person: Person required init(view: GreetingView, person: Person) { self.view = view self.person = person } func showGreeting() { let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName self.view.setGreeting(greeting) } }class GreetingViewController : UIViewController, GreetingView { var presenter: GreetingViewPresenter! let showGreetingButton = UIButton() let greetingLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside) } func didTapButton(button: UIButton) { self.presenter.showGreeting() } func setGreeting(greeting: String) { self.greetingLabel.text = greeting } // layout code goes here }关于装配(assembly)的重要说明 MVP是第一个揭露三层模型装配问题的模式。我们不想让View和Model互通,因为在试图控制器(View)中执行装配操作是明显不对的,所以我们只能换个地方放装配的代码。例如,我们可以做一个应用范围的Router服务,它负责装配工作和View到View的展示。这个问题的出现不仅要在MVP中解决,在以下的几个模式中也都要解决。 我们看下MVP的特性:职责分离 -- 我们将大部分职责分配给了Presenter和Model,而视图则什么都不需要做(上面的Model也是什么都不用做) 可测试性 -- 非常好,我们可以通过静态的View测试大多数逻辑。 易用性 -- 在我们上个简单示例中,代码量是MVC的两倍,但是它的逻辑是很清晰的。在iOS中MVP模式意味着良好的可测试性和大量代码。MVP这是另一个MVP的样式 -- 由视同控制器担当管理的MVP。这个变体中,View和Model是直接绑定的,Presenter(担当管理的控制器)仍然处理着来自View的操作,并且能够改变View。 但是通过上面的学习我们已经知道了,将View和Model紧耦合处理,这种不明确的职责分离是很糟糕的。这与Cocoa桌面开发中的工作原理类似。 跟传统MVC一样,我找不到要为这个有缺陷的架构写示例的理由。 MVVM 最新而且是最好的一个MV(X)类型 MVVM是最新的MV(X)类型,希望它能解决我们之前讨论过的问题。 MVVM理论上看起来是很好的,View和Model我们已经很熟悉了,它俩之间的中介者由View Model表示。这和MVP很像:MVVM也是把视图控制器当做View 在View和Model之间没有紧耦合关系此外它的绑定逻辑很像MVP的监管版本;但是这次不是View和Model,而是View和View Model之间的绑定。 所以iOS当中的View Model到底是什么呢?它是UIKit独立于视图及其状态的表示。View Model调用Model执行更改,然后根据Model的更新再更新自己,因为我们绑定了View和View Model,第一个模型将相应的更新。 绑定 我在MVP部分明确提到过绑定,这次让我们再来讨论一下。绑定出自于MacOS开发,在iOS中是没有的。我们虽然可以通过KVO和通知完成这一过程,但是这样的绑定方式并不方便。 如果我们不想自己实现的话,有两个选项可供参考:一个是基于KVO的绑定库像RZDataBinding,SwiftBond 完整的函数式编程工具,像ReactiveCocoa, RxSwift, PromiseKit。如今当你听到“MVVM”,就应该想到ReactiveCocoa。因为它可以让你用很简单的绑定方式构建MVVM,几乎涵盖所有MVVM中的逻辑。 但是使用响应式框架会面临一个不好的现实:能力越大责任越大。使用reactive很容易将事情复杂化。也就是说,如果发生了一处错误,你需要花费很多时间去调试问题,可以简单看下响应式的调用堆栈。在我们的示例中,响应式框架甚至KVO都是多余的,我们将使用showGreeting方法显式地要求View Model更新,并使用greetingDidChange回调函数的简单属性。 import UIKitstruct Person { // Model let firstName: String let lastName: String }protocol GreetingViewModelProtocol: class { var greeting: String? { get } var greetingDidChange: ((GreetingViewModelProtocol) -> ())? { get set } // function to call when greeting did change init(person: Person) func showGreeting() }class GreetingViewModel : GreetingViewModelProtocol { let person: Person var greeting: String? { didSet { self.greetingDidChange?(self) } } var greetingDidChange: ((GreetingViewModelProtocol) -> ())? required init(person: Person) { self.person = person } func showGreeting() { self.greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName } }class GreetingViewController : UIViewController { var viewModel: GreetingViewModelProtocol! { didSet { self.viewModel.greetingDidChange = { [unowned self] viewModel in self.greetingLabel.text = viewModel.greeting } } } let showGreetingButton = UIButton() let greetingLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() self.showGreetingButton.addTarget(self.viewModel, action: "showGreeting", forControlEvents: .TouchUpInside) } // layout code goes here } // Assembling of MVVM再次回来用这三个特征验证一下:职责分离 -- 这在我们的小示例还无法体现,但是MVVM的View比着MVP的View有着更多的职责。因为前者通过View Model建立绑定更新它的状态,后者仅仅是把所有事件都交给Presenter,不更新自己。 可测试性 -- View Model是不知道View的,这可以让我们很容易的对它进行测试。View可能也需要测试,但因为它依赖UIKit,你可能想跳过它。 易用性 -- 它有河MVP模式相同的代码量,但是实际项目中,你不得不把所有事件通过View传给Presenter,然后还要手动更新View,比较而言,MVVM使用绑定将更加简洁。MVVM是很有吸引力的,因为它包含了前面提到的优点,此外通过View层的绑定,也不需要额外的代码处理View更新。测试性也还不错。VIPER 把乐高的搭建流程应用到iOS设计模式 VIPER是我们最后一个候选模式,有趣的一点是它不属于MV(X)类型。 目前为止,你必须同意职责的粒度是很重要的。VIPER在划分职责层面又做了一次迭代,它将项目划分成5层。Interactor-- 包含跟数据(Entities)和网络相关的业务逻辑,像是创建新的实例对象后者从服务器拉取数据。出于这些目的,你也可以使用Services和Mananger类完成功能,但这就不属于VIPER模块,而是外部依赖类。 Presenter -- 包含UI相关的业务逻辑,调用Interactor中的方法。 Entities -- 普通的数据对象,不是数据访问层,因为这是Interactor的责任。 Router -- 负责VIPER模块之间的切换。基本上,VIPER模块可以是一整屏内容,也可以是你应用中完整的用户行为 -- 想一下授权行为,它可以在一个或者几个相关联的界面。“乐高”方块应该多小呢?这取决于你。 如果我们将它和MV(X)类比,会发现一些在职责划分上的区别:Model(数据交互)逻辑转移到了包含Entities数据结构的Interactor中。 只有Controller/Presenter/ViewModel这种UI表示层的职责转移到了Presenter中,不包含数据。 VIPER是第一个明确导航职责的模式,并通过Router解决这个问题。在iOS应用中用一个优雅的方式处理跳转问题确实是一个挑战,MV(X)模式没有处理这个问题。该示例不涉及模块之间的路由或交互,因为MV(X)模式根本不涉及这些主题。 import UIKitstruct Person { // Entity (usually more complex e.g. NSManagedObject) let firstName: String let lastName: String }struct GreetingData { // Transport data structure (not Entity) let greeting: String let subject: String }protocol GreetingProvider { func provideGreetingData() }protocol GreetingOutput: class { func receiveGreetingData(greetingData: GreetingData) }class GreetingInteractor : GreetingProvider { weak var output: GreetingOutput! func provideGreetingData() { let person = Person(firstName: "David", lastName: "Blaine") // usually comes from data access layer let subject = person.firstName + " " + person.lastName let greeting = GreetingData(greeting: "Hello", subject: subject) self.output.receiveGreetingData(greeting) } }protocol GreetingViewEventHandler { func didTapShowGreetingButton() }protocol GreetingView: class { func setGreeting(greeting: String) }class GreetingPresenter : GreetingOutput, GreetingViewEventHandler { weak var view: GreetingView! var greetingProvider: GreetingProvider! func didTapShowGreetingButton() { self.greetingProvider.provideGreetingData() } func receiveGreetingData(greetingData: GreetingData) { let greeting = greetingData.greeting + " " + greetingData.subject self.view.setGreeting(greeting) } }class GreetingViewController : UIViewController, GreetingView { var eventHandler: GreetingViewEventHandler! let showGreetingButton = UIButton() let greetingLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside) } func didTapButton(button: UIButton) { self.eventHandler.didTapShowGreetingButton() } func setGreeting(greeting: String) { self.greetingLabel.text = greeting } // layout code goes here }// Assembling of VIPER module, without Router let view = GreetingViewController() let presenter = GreetingPresenter() let interactor = GreetingInteractor() view.eventHandler = presenter presenter.view = view presenter.greetingProvider = interactor让我们再一次对比那几个特征:职责分离 -- 毫无疑问,VIPER是职责分离做的最好的。 可测试性 -- 职责分离越好,可测试性当然也会更好 易用性 -- 你可能已经猜到了,上面两项意味着维护成本的提升。你必须写很多处理各个类之间交互的代码。所有这个乐高模式到底怎么样呢? 当使用VIPER时,如果你感觉就像是通过乐高方块搭建帝国大厦,这就意味着出现了问题。不应该过早在你的应用中使用VIPER,你需要考虑简便性。有些人不注意简便性,直接使用VIPER,会有点大材小用。我猜测很多人是这么想的,他们的应用迟早都会发展到适用VIPER的复杂程度,所以早晚都会做的事,即使现在维护成本高也应该继续做下去。如果你就是这么想的,我推荐你试一下Generamba -- 一个生成VIPER组件的工具。虽然对我个人来说,这感觉就像使用自动瞄准系统而不是简单的弹射。 总结 我们已经讲解了几个架构模式,希望你能解答曾经困扰你的问题。我敢肯定你也意识到了架构模式的选择没有最好这一说,它取决于你在特定环境下权衡利弊之后做的选择。 所以,在一个应用中出现混合一种混合的架构模式也是很常见的。例如,你一开始使用MVC,然后你发现有一个界面的逻辑变得很复杂,然后你转向了MVVM,但也是仅限于这个界面。你不必重构别的使用MVC的界面,因为它原本就是工作的好好的,而且这两个架构模式是很容易兼容的。事情应该力求简单,不过不能过于简单 -- 阿尔伯特·爱因斯坦
感谢大家的帮助
- 09 Nov, 2019
好消息 开始水滴筹之后,父亲治病的消息大家就都知道了,期间收到了很多来自亲友的支持和帮助。先告诉大家好消息,父亲手术完了,并且身体恢复一切正常,已于11月8号中午从重症监护室转到无菌病房,还需要在里面待够五天才转到普通病房。关于看病所需要的筹款,也得到了解决。今天晚上7点左右,我已经终止了这次筹款。 看病资金问题能快速解决,除了来自水滴筹的筹款,还有就是公司的大力支持。三位老板帮助了2万元,还破格提前给我发了年终奖,算上水滴筹中的63588元,一直到出院应该是没什么问题了。感谢公司 真的非常感谢公司给予的支持,啥也不说了,我要夸一夸他。我们公司叫乐信圣文,是一家全球领先的移动出海应用开发商,致力于为全球用户提供卓越的移动应用,作为中国移动互联网公司出海的新锐领军者,产品在全球移动端月活超过2000万,累计用户超过3亿。目前正在招聘Android、iOS、Unity、web前端、Python等开发工程师,另外中高级测试、数据分析师、产品运营、MG动画师也都职位空缺。大家有要换工作的或者朋友换工作的,可以推荐来我们这。这么好的老板,这么有前景的公司还等什么?详情可以问我或者关注公司微信公众号:乐信圣文Learnings。 感谢所以提供帮助的小伙伴 其次来自小伙伴的支持也让我非常感动,很多感动瞬间,其中有两个场景让我特别难忘。一是刚开始发起水滴筹时,当时心里慌慌的,根本不知道接下来事情会怎么发展,也不敢看手机。但不到5分钟时间,大学室友就开始齐刷刷的给我打钱,给我加油,当时铺天盖地的收款和问候消息直接就给我感动哭了,我真是爱死你们了。还有一件事是来自于一个前同事,当时我们只有QQ这一种联系方式,因为QQ用的很少,直到昨天才看到他的祝福和转账。他因为公司问题两个月都没收到工资了,知道我的情况之后,还执意给我打钱。真的是折煞我也!不知道说什么好了。 其实每次转发时我都怕打扰到大家,因为水滴筹里的各种求转发的术语,很像牛皮癣广告。但你们却对我很宽容,帮我转发,号召别人帮我转发,很多人还多次打款,真的非常感谢。我知道大家都不容易,各种生活琐事,结婚,买房,都是花钱的地方,我用你们的钱,也很过意不去,这里给大家道个歉,给你们添麻烦了。我想说的是,你们帮助我,给我爸治病这些钱不是捐给我的而是借给我的,我会在之后经济允许的情况下把钱都一一还给大家。 大家一定要给我面子啊,这笔钱我是要还的 上午我用了将近一个小时时间把水滴筹的捐款记录反复核对了两遍,只要是能对应上名字的人我都用小本本把人名和捐款数额记录下来。本次捐款收到了750次帮助,我记录下来的人名有120多个。也就是说有很大一部分人没有联系到,这其中有一大部分来自于我爸的朋友、同行,教会的人,一部分亲戚,还有就是所有参与转发水滴筹扩散之后的朋友圈成员。好像要把所有人都确认下身份也不现实,所以对于那些无法确知是谁且捐款数额较多的,我都在捐款记录的下面留了言,询问联系方式。如果能联系上,钱还是会还给你的。 这里的还钱是除了老板们的2万哈,不是不还,是因为我想换一种还法,通过努力工作的方式回报公司,帮你们多宣传多招人(主要还是有点多,他们又都是大佬,肯定不差钱)。 可能很多人捐钱时就没打算再要回来,但是我是有打算还的。我选择还大家钱是因为我知道挣钱的不易。我有困难你们帮我,我困难过去了,理应把钱还给你们。这是我的心愿,不还你钱我会不舒服的,所以大家为了照顾我的感受不能推脱啊。 当然了这个还钱过程肯定是不会快的,这段时间确实用钱厉害,等过了这个坎,我会慢慢地开始还大家钱。如果你因为什么事情着急用钱,可以先找我;如果你的钱迟迟不给,排除被遗漏的可能性,那就说明在我眼里你是土豪,土豪的钱就最后还啦。 更多近况 另外关于这几天的详细进展,以及我在七院里的一些见闻,大部分是记录下来了。大家有兴趣的话可以访问这个网站进行了解:zhangferry.com
关于水滴筹
- 08 Nov, 2019
很多朋友因为我的原因关注并参与到水滴筹中,有些小伙伴因为担心水滴筹有手续费用就直接把钱打给我。为了打消大家的顾虑,我花了些时间理解水滴筹背后的一些事情。水滴筹的背景 水滴筹是由水滴公司于2016年推出的一项针对疾病救助的筹款业务。在水滴筹之前的筹款平台领头羊是轻松筹(2014年推出),当时包括轻松筹在内的平台都会收取2%左右的手续费。水滴筹却一开始就不收取任何手续费,筹款所得资金全部归收款人(目前仅收取微信提现产生的手续费:0.1%)。免手续费肯定更多人使用,慢慢的水滴筹不断从轻松筹手里抢占市场,目前市场份额已经第一(轻松筹现在也免手续费了)。 水滴的发展 说水滴筹的发展就不得不说水滴的创始人沈鹏,他是美团的第10号员工。在美团,沈鹏战功卓著:早期带着5万元单枪匹马杀到天津,第二个月就将天津市场份额从第七提升到第一;23岁升任大区经理,通管北京、天津、山东同400人团队,参与“千团大战”;26岁,跟随王慧文立项美团外卖,带领团队在一年多的时间里把美团外卖做的行业第一。所以他的市场把控和感知能力是非常强的。 线下地推是美团的强项,这个强项也被带到了水滴公司,很多早期水滴员工都是沈鹏从美团挖来的。早期水滴筹获取流量的重要砝码就是地推,他们下沉到渗透率并不高的三四五线城市和农村地区,招募大量兼职人员,志愿者辅导当地人筹款;他们在各个医院,各个科室留下名片,传单,立起易拉宝。不光鼓励患者加入,他们还会免费指导筹钱金额怎么写,求助说明怎么写,怎么转发(一次发三条可以占满手机一屏)效果好,甚至哪些时间点转发效率更高都会说。完美复制美团“农村包围城市”的策略。 这种形式让水滴筹快速获取了大量用户,也带来了巨大的流量。快速发展的水滴公司,目前估值近30亿,腾讯是最大的投资方。 盈利模式 前面说了水滴筹是免手续费的,地推人员也都是需要发工资的,那它怎么赚钱呢? 首先按照水滴筹的规则,一次筹款是30天,在这30天内只要不发起提现,水滴筹有权将这笔款项委托给第三方进行资金托管,水滴筹至今累计筹款200多亿,平均每月就将近5亿的流水,这产生的利息是非常高的。 另外水滴筹共有超过2.5亿名爱心人士参与帮助,累计产生6.5亿人次捐助活动。这巨大的流量推动宣传了水滴公司的另外两项业务:水滴互助和水滴保。这两项业才是赚钱的,它们的区别是:水滴筹,捐款人,是来做慈善的;水滴互助,互助会员,是希望加入一个低成本的互助计划。水滴保,主动跑来的投保人,是希望找到性价比高的保险产品。 其中水滴互助是水滴公司发展的第一项业务(后来支付宝出了个相互宝,跟它的模式基本一样,一人生病,大家帮忙分摊),水滴筹的产生更深层的目的是给水滴互助导流。所以水滴公司的商业变现模式就是通过水滴筹这种公益互助类产品聚集高黏性的用户和流量,然后再通过水滴互助、水滴保等商业业务板块进行变现。 所以他们肯定是赚钱的啦,只不过是一面做“慈善”,一面做生意。 目前问题及现状 水滴筹目前已经过了市场掠夺期,但仍有些问题需要解决。水滴筹在前期的审核是相对宽松的,最主要的诊断证明和身份证号,只要没问题,就可以进入审核阶段,说是5分钟,有时基本1分钟就好了。可以让你快速的开始募捐,提现时才会需要你出示更加详细的诊断报告,病历,医院消费清单,银行卡信息等。这些都只是确认你真的生病了,但是是否贫困,其实是没有确认的,因为为了一个筹款的人就去当地调查也不现实,这也不好确认。不过这也会带来问题,有可能导致“滥用众筹”,“滥用爱心”,此前某德云社相声演员名下有房有车却筹款惹来的争议就是此类。 虽然水滴筹的审核机制受到争议,但我们并不能否认其存在的社会价值,它切切实实的帮助了很多人。在未来的监管中,要在防范风险与鼓励创新之间寻求适当的平衡,可能是监管部门和水滴公司需要认真思考和探讨的问题。如今,对于渴望疾驰的水滴公司来说,要想持续发展,关键还是“不忘初心”。
为什么会得尿毒症
- 08 Nov, 2019
看了父亲的病历,再结合之前了解的一些情况,我试着分析一下为什么会得尿毒症。 一些医学知识 关于肾脏:肾脏有很重要的代谢排毒功能,可以清除体内经由食物消化所残留的尿素、尿素氮、肌酸酐等,也会排出水分,维持体内的电解质平衡,甚至是控制血压高低起伏、活化维生素 D 的重要器官。此外,肾脏与肝脏一样,也是体内清除药物的重要器官,这也是为何肾功能不好时,就容易发生药物过量或中毒后遗症的原因。可见肾脏是何等的重要!等到肾脏的功能持续恶化到只剩下不到正常的 10% 时,才较容易出现包括恶心、水肿、高血压、倦怠无力、抽筋等症状,一旦造成更严重的尿毒症时,只能透析治疗。其中衡量肾脏功能健康程度有个很重要的指标,肌酐:肌酐是肌肉在人体内代谢的产物,临床常用来衡量肾功能的健康程度。正常范围是54-106umol/L。痛风:痛风是一种单钠尿酸盐(MSU)沉积所致的晶体相关性关节病,与嘌呤代谢紊乱及(或)尿酸排泄减少所致的高尿酸血症直接相关,属代谢性风湿病范畴。痛风可并发肾脏病变,严重者可出现关节破坏、肾功能损害,常伴发高脂血症、高血压病、糖尿病、动脉硬化及冠心病等。 因为痛风常发于指关节,痛起来是那种深入骨头的痛,取自“痛疯”的谐音。时间线 2007年 2007年,爸爸来北京打工已经7、8年左右了。凭借着勤奋和聪明,他从一个蹬板车买花的小贩变成了一个接小区别墅建筑项目的小老板,生意上虽然越来越好,很多事上爸爸还是喜欢亲力亲为。 那时正赶上夏季,爸爸要给人做葡萄架,一个葡萄架工期差不多是10人天,因为没有跟别的事赶在一起,爸爸就一个人做了。做木工活,使用频率最高的工具就是电锯。他对流程和工具的使用已经是很熟练了,但那次可能是走神了或者预估错误,没有把握好电锯的切割幅度,一下切中了小拇指。虽然没有整个切掉,但是骨头都切断了,很血腥的一幕。紧接着送去医院,医生在手指中间接了一根钢针,保住了手指。 在调养期间,为了防止发炎,打了很多抗生素也吃了很多消炎药。我们总说伤筋动骨一百天,这个调养也将近持续了一百天。这一点虽然没有体现的病历中,但我隐隐感觉,这可能就是后面一系列事件的引子。 2008-2009年 2008年左右,父亲感觉左脚大拇指外侧疼痛,频繁几次之后就去了北京解放军301医院做检查。当时检查为肌酐80(umol/L),被医生诊断为通风,并建议控制饮酒。当时吃了别嘌醇等药物进行治疗,很快疼痛的症状消失,就没有再去医院进行检查。 一直以来父亲都爱喝啤酒,因为北京这边亲戚朋友也比较多,大家不忙的时候常会一块聚聚。父辈那些人基本都爱喝酒,坐到一起,打牌,喝酒,吃肉,经常几天就一次,一喝就是酩酊大醉。虽然母亲经常批评父亲不要喝酒,但父亲感觉不疼了就是病好了,再加上朋友劝酒,自己又想喝,也没有加以控制。为了这事他被母亲不知吵了多少次。 关于痛风,目前还无法根治的,只能通过调养,延缓病情。而这其中最重要的就是要限制嘌呤的摄入,啤酒嘌呤虽然含的不多,但容易喝过量也就导致体内嘌呤大量增加。 2010-2011年 大概一年之后,爸爸又出现了左脚大拇指疼痛的症状,而且这次要比上一次严重。再去北京解放军301医院检查,肌酐已经涨到200,被诊断为“慢性肾功能不全”。痛风患者中大约有40%的人会患有慢性肾病,因为饮酒的原因由痛风发展到了慢性肾病。 之后父亲也意识到了问题的严重性,开始慢慢戒啤酒。也是从这时开始,爸爸之后吃药就没再断过。确诊为“慢性肾功能不全”之后,从医院就拿了一个月的药。这期间效果一直一般,爸妈决定换个医院试试。然后去了北京武警医院,医生对病情的诊断是一致的,但对于病情的治疗并不乐观,说慢性肾病也是一个需要调理的疾病,并不能吃某种药就根治。这次在这里又拿了1个月的药。 对于医生不能彻底根治的说法,他们有些灰心。后来在电视上看到有家中医院关于治疗慢性肾病的广告,他们宣传的效果非常好,这给他们带来了一些希望。虽然知道大医院才更靠谱一些,他们还是去了那个医院,并开了3个月的中药。中药熬出来之后都是非常苦的,但是为了治病,父亲皱着眉头也都坚持把药喝下去的。期间药吃完了就再去买,都是一买几个月的。 虽然父亲一直生病,但是并不影响干活,只要脚不疼他忙起来依旧生龙活虎的。这段时间家里的经济状况也越来越好,当时很多亲戚结婚或者盖房来我家借钱,父亲都是慷慨解囊。 2012年 到2012年暑假,我高考完来北京。爸爸身体已经出现一些症状:特别容易困,脚浮肿。有一天上午他需要到一个客户那里办事,我陪他一起去,车刚开到一半,父亲就说,开不了了,需要休息一会。然后停到一个地方,睡了20分钟,再继续走。当时脚也经常性的水肿,鞋都只能穿宽松的。 病情持续恶化,一天中午,父亲躺在床上不起来,被母亲发现时,他眼睛里面布满了血丝。我当时正在场,看到这个场景,只感觉心里被重重击打了一下,很恐慌,想哭却哭不出来。我背着他坐上车,那天直接去了北大医院,进入重症监护室。 医生已经下了病危通知书,他冷酷无情地告诉我跟母亲,病人随时有可能遭遇不测,请提前做好心理准备。我跟妈妈都哭了出来,那是我第一次近距离接触死亡。 过了几个小时,医生告诉我们父亲已经脱离了危险,并被确诊为尿毒症(慢性肾脏病5期),肌酐达到了1200。进入尿毒针阶段就只有两种方法维持生命了,透析和肾移植。因为肾移植手术不是想做就能做的,需要等待合适肾源,所以前期只能通过透析维持。度过危险期之后,由于北大医院病房紧张,父亲被转到了北京航空医院进行透析治疗。当时在航空医院住了将近一个月时间才恢复身体出院。 分析 1、饮酒和一些药物 病根在痛风上,但痛风的病因和发病机制尚不清楚。但有些诱发因素,例如大量摄入动物内脏,贝类海鲜等高嘌呤食物,还有酒精的摄入,特别是啤酒。当然还有可能导致血尿酸增高的药物。 因为已经无从查证父亲当时手指受伤都吃了哪些药,动物内脏和贝类他也不喜欢吃,只能猜测那时的用药和父亲本身喜欢喝啤酒共同造成了痛风的发生。 2、中药要慎重,去大医院看病 痛风发展成为慢性肾炎,是有很大概率的,但是保养得当也可以控制的很好。但是在短短两年多的时间 就从慢性肾脏病1期发展到5期确实太快了,很多人根本不会发展到5期,或者是很多年之后才会发展恶化到尿毒症阶段。所以我就十分怀疑是吃中药阶段导致的病情加重。 关于中药治疗肾病的效果,我看了一些网上的讨论,是还不错的,有用中药调理这种说法。那就大概率是被那个广告医院给坑了,喝中药阶段一点改善没有,而且中药本身会含有很多非药效的杂质,喝到体内会大大增加肾的负担。 3、定期检查 还有一点是如果不能保证吃的药一定就对病情有帮助,那就需要定期去医院检查,一个是了解病情处在哪个阶段,一个就是能看出来当时再吃的那个药是否有作用。 更重要的一点一定要去大医院啊。
七院第三天(11月5号)
- 06 Nov, 2019
手术完成就是渡过了最大的难关,其他时间爸爸都是在重症监护室由专门护工照顾的。实际也不需要那么多人手,先后送走了三姨姨父,弟弟,还有两位叔叔,就我自己留在医院。平常需要买什么吃的,里面护士直接给我打电话,我买好送过来。这天早上是鸡蛋小米粥,已经不需要萝卜水了,中午要吃肉丝面。听到爸爸要吃肉丝面很开心,因为这是除了小米粥鸡蛋之外第一个非常规食物。但是可能是我没考虑周到,面太长了,汤也有点少,他吃的不方便,只吃了两口。之后根据护士交代给他买了一包口香糖。 再晚些时候护士打电话说他想看书。因为里面不让玩手机,也没什么娱乐措施,除了睡觉吃饭没别的事,很容易无聊。附近就一家卖教辅资料的书店,在一堆小学生读物中,我挑了本《中外名人故事》。 下午探视时状态好很多,声音也不再那么沙哑了。感觉状态越来越好了。 三姨一直在家盼着,说你爸有什么情况随时汇报,姥爷每天上午打来一次电话询问情况,大姨,三姨,小姨也都经常打电话过来。 我这几天还是因为睡眠问题,状态不佳,特别是到了下午。困得不行就坐在台阶,往腿上一趴睡着了,但20分钟就醒了,那石板台阶是真的硬。 在监护室外面一直待到晚上十点钟,见里面一直没有消息,就回到了附近租的房子准备休息。洗个澡,躺下睡觉,这可能是近几年来睡得最香的一个晚上了。
七院第二天(11月4号)
- 05 Nov, 2019
订饭 五点半起床。医生嘱咐手术完第二天需要喝米油(小米粥虑掉小米),还有喝萝卜水。一般来说术后6小时宜服用一些排气类食物,如萝卜汤,帮助因麻醉而停止蠕动的胃肠道保持运作,以肠道排气作为可以进食的标志。送完这两样东西,我们就在外面等着了。因为这个是整个医院的手术室,大大小小的手术都从这里进入。一上午有陆续五个人先后进入手术室,还有一名孕妈妈,没多久她被推出来要转到别的病房。病床从旁边经过,我看到母亲安静的闭着眼睛,小宝宝被裹的严严实实的只漏一个小脑袋,被放在母亲腿间,后面一群家人跟着病床,多么美好的画面啊! 中午监护室大夫打电话说爸爸需要喝小米粥,因为不再是米油,就让我高兴了一下。 视频通话 下午5点到了探视时间,有一间专门的小小的探视房间,里面有两部电话,两台电视。可以通过这里接通各个病床上面的监视器,看到病人的情况,然后通过电话跟病人对讲。我看到前面两个人的通话场景,一个清晰,一个稍微模糊一点,还跑到了画面清晰的那一对排着。离手术完成已经19个小时了,非常想看看爸爸现在的情况。满怀期待地接通电话之后,大夫说,张中线床头摄像坏了,等会通过护士手机进行视频。 通过护士手机接通视频,看爸爸状态还可以,我问他感觉怎么样,他说,还好。虽然是“还好”但听他声音沙哑,不免还有些担心,不敢跟他讲太多话。问旁边大夫声音沙哑问题,他说全麻手术喉部插管引起的肿胀,需要一段时间的恢复。晚上吃饭还需要喝萝卜水。挂完电话就给爸爸准备萝卜水去了。 把跟爸爸通话的情况拍上照片发给亲戚们,告诉他们恢复的挺好。 开始水滴筹 三姨给我打电话催我赶紧弄水滴筹。一开始我是有些抵触这个事情的,因为手术费手术费凑齐了,还有感觉水滴筹这个事情有点抹不开面子。但是想到重症监护室每天一万多的花费,下个月4万的信用卡,再之后的无菌监护室5天,继续住院10天,还有后期每周一次检查,抗排异药要吃好几年,这些都是很大一笔花费。现在筹钱不管多难都比以后为钱发愁好。想通了这些我联系了一个附近水滴筹的对接人,跟他说明了情况,也问了一些他们那边关于审核,筹款,提现的手续。然后就开始了朋友圈里水滴筹。 发出去后我还恍恍惚惚的,很快就会被大家注意到,不知道即将到来的是什么,我甚至都不敢看手机。5分钟后大学室友那几个小伙子联系到我,他们五个齐刷刷的给我打钱,并祝福我爸早日康复。那会回复都回复不过来,被他们的行为感动的不行。真的,我们609无论做什么事都是齐齐整整的,毕业前这样,毕业后也是这样,很多次聚会我们的团结都让其他同学们羡慕,我们是我一辈子的好兄弟。 可能这段时间确实太累了,将近两天基本没怎么休息过,一直在顶着各种压力前进,那么一瞬间因为室友的关怀而放下压力的时候,竟不自觉哭了出来。 再过了一会张弛给我打电话说,你急用钱先借你1万,我刚买完房手头不是很宽裕,如果不够再开口。张弛是高中同学,大学各忙各的基本没怎么交流过,毕业之后又几年了,有事情还惦记着我,真的很感动。 然后再去看水滴筹上面的筹款情况,亲戚,公司领导,同事,同学,朋友都有帮助并留下祝福的话。我翻看着,感觉到一股一股的能量不断注入体内。我会好好加油的,伴着大家的祝福,一定会顺利度过这道难关。 然后跟女朋友打电话,跟她说水滴筹的事情,她表示很理解,还说之前也想过这个事,但感觉我爱面子不会同意。她也很心疼我,看我这样还数落我,你每天工作时间那么长,家里的事还就你一个人扛,工作几年攒的钱全搭进去了,你多大能耐啊。我理解她,因为本来今年许诺她买房结婚的,因为我爸的事肯定也耽误了。其实这一圈最对不起的就是她。她可能也感觉话说重了,又反过来安慰我照顾好自己,照顾好我爸。 不知从什么时候开始,无论发生什么事情,我都不再埋怨,而只是去想怎么处理好它。命运是太不公平了,把一个完整的家破坏的零零碎碎,母亲病故,父亲大病需要治疗,弟弟结婚几年还一事无成,所有的事情都需要我一人承担。之前我也会恨的用拳头砸墙,但是现在不会了,我知道这是我应该做的,我感觉自己可以做到,然后就去做了。
七院第一天(11月3号)
- 03 Nov, 2019
抵达郑州 二号中午我爸打电话来医院这边配型成功,让三号早上来做手术。我得知这个消息有些开心也有些担忧,开心是因为盼了八个多月的肾源终于有了消息,担忧是紧接着将是一场大手术,不管医院怎么保证手术成功率,肾移植手术本身都是一项大手术。 我和弟弟是二号晚上9点半的火车,因为当天才进行买票,只有硬座了,三号早上5点45到的郑州。爸爸于前一天晚上在三姨和姨父的陪同下到的医院。见到爸爸,看他精神状态挺不错,我安心不少。他说需要做的检查都已经做了,身体条件一切正常,就等医生上班安排手术时间了。准备手术钱 去到住院部,医生首先问费用是否准备齐全,肾源费15万必须现金,住院账户上至少有6万可用费用,这个账户可以用支付宝微信银行卡充值。手术之前如果这些钱不够是不给手术的。我跟我爸简单核对了下现在有多少钱,加上我之前的积蓄,差不多够。基金里有3万左右,因为这个提现有几天延迟,我担心后面有别的情况出现,也申请了提现。 护士提示我们先准备现金,15万属于大额,有些银行必须要预约还需要预约。这一点确实超出我的预期,就赶紧带着卡跟我弟一块去取钱。楼下ATM取了两万就超额不让取了。附近有个建设银行,工作人员一听要跨行转账,而且这么多直接拒绝了我们。我的钱在招商卡里面,就又去了较远的招商银行。柜员问是否有预约,我怕因为这个不给取就赶紧说了这是做手术急用的钱。他犹豫了一下同意了,就开始在柜台那边操作。包括另一张银行卡的跨行提现,他说不知道能不能行,但可以帮我们试一下。虽然最后确实不行,但这个态度挺让我满意,前后对比两家银行,建行给我的印象就更差了。 15万放在包里真挺重的,我背着沉甸甸的钱回到了医院,就在护士的待领取下去缴费。我看柜台人员点钱,15万现金,一万一万的点,要数3-4次左右,光在那等点钱等了七八分钟。然后是凑手术和住院费的钱,我这边微信,支付宝,都交了进去,一直到预存金达到6万。此时手里的钱已经不足一万了,但好在达到了手术要求的费用。 术前准备 回到医护室,护士交代,要买10支白蛋白,是术后用的。还给了我一张名片,让我打电话去找这个人买。不明白为什么医院不自己开,还要通过外面才能买?我没时间考虑太多这种事情,就按照她的要求,说什么做什么,要什么给什么。接过电话,那边先说价格380一支,我好像也没别的选择,就让他送了十支。 再之后护士又给了一张纸条,上面分两段列了需要购买的东西这些。 洗脸盆、大便器、小便器、痰盂、毛巾、牙刷杯、牙刷、牙膏、奶瓶、卫生纸一提、成人尿垫一大包、湿巾一包,消毒湿巾一包、压力绷带两个、雾化吸氧面罩一个、呼吸训练器一个。这些是重症监护室需要用的。 便携式体重秤、温度计、小药箱、小本和笔、口罩一包、输液报警器一个、扣背器一个。这些是从重症监护室出来用的。 听大夫说整个过程是晚上6点左右手术,手术完会进重症监护室,在里面观察恢复5天左右,由专门护工照顾,每天有一次探视的机会,可以通过摄像头跟病人交流几分钟。五天之后从重症监护室转到无菌病房,再有五天可以转到普通病房。普通病房阶段就可以自由看望,甚至可以下床简单活动。 买完必备东西之后,爸爸开始了透析。因为正常是一周透析三次,一三五,隔一天一透,今天周日,上次透析是周五,隔的时间较长,需要补一次。透析时我还跟爸爸说,这可能是你最后一次透析了,以后再想透析也没了。 和爸爸一起接受手术的还有一个人,安徽阜阳的,他也属鸡比我爸小一轮,身高体重血型两人也都基本一致。他们俩是要接受同一个供体。问了下还是基督家庭,三姨也感叹这是何种的缘分,冥冥之中可能就是上帝促成的这件事吧。 确定了晚上6点手术之后,爸爸从早上10点开始就被要求不能进食。因为透析的缘故他饥饿感更强了,但也没办法了。这时护士过来说你们余额不足了,需要充钱。问清楚之后我才知道,不是一共交6万而是卡内余额要有6万,一上午已经花了3万多了,就说还要充进去4万才行。这可让我很发愁,想到了这是医院账号,试着用了1万花呗,充值成功,然后是用3万信用卡的额度。这已经是穷尽了力气了,总算筹齐了做手术的钱。 手术前还有一项是洗肠,不知因为什么缘故我爸需要做两次才行,莫名有些心疼起来。之后又等了一段时间,两位叔叔也来了,也说看我爸精神状态不错。我知道每临大事时最紧张的阶段都是知道大事降临,等待事情发生的那段时间。只盼望时间可以过的快一点,不要让我们,不要让我爸煎熬太久。术前量血压,我爸高压170,他本身有一些高血压,也可能是紧张的原因,有点过高了。依照大夫的指示要吃两片降压药,此时还需要控制饮水,只让抿一小口够吃药就行。这一点水下口,可是解了他的瘾,爸爸说水真是太好喝了,想喝却不敢喝。 手术之前需要签字,医生一上来跟我说了一大堆可能的不良反应和意外情况,手术之后可能肾不工作,恢复不好还需要透析辅助,术后肺部容易感染,伤口愈合前如果翻身不小心还可能伤口破裂,术后免疫力会非常低,特别需要照顾好别感冒,抗排异药容易引起情绪焦躁,需要配合医生。想着医院原来的话,手术成功率高达99%,本来还很有信心的我一下紧张和不安起来。但医生不管这些,只是把所有可能的坏情况给我说一遍,然后说,都清楚了吧,签个字吧。这就是一个免责说明书,我毫无还手之力,签了字匆匆离开,只希望手术能够顺顺利利完成。 进行手术 傍晚6点半左右进了手术室,在手术室门口,接待的医生已经是一身蓝绿色衣服了。他简单问了一下情况,然后记着一些东西,手势动作都很熟练。虽然是简单的步骤,但他的穿着,语气和动作,却让我感受到一种专业性。专业这个东西也是我一直向往和努力的目标。因着这种专业感我很快踏实下来,相信医生,他们肯定会好好给我爸治病。 医生说手术将近三个小时。亚茹姐也信基督,组织了一个小团队,在北京为爸爸祷告。郑老师,张阿姨还有老家的一个教会也在为爸爸祷告。很多人都在祝福着这场手术,盼望着它顺利进行。爸爸进入手术室半个小时之后,临床要一起进行手术的那个人也进了手术室。 这段时间感觉过的异常慢,7点,8点,9点,这个时间点左右差不多该出来了,我就一直盯着手术室的门。想象着它打开的那个瞬间。九点半左右,门开了,但是是跟他一起手术的那个人。他本身比我爸晚进去却早出来,又让我紧张起来。二十分钟左右,想像中的那个画面才出现:医生护士,推着病床,告诉我们手术一切顺利,已经排尿了。我问爸爸感觉怎么样,他声音沙哑说不出来,跟我们见了大概5秒中,很快就被医生推着进到重症监护室了。 此时一直悬着的心才放下来,向所有关注这场手术的人报了平安之后,我心里默默说着,感谢主。虽然身边很多人都是基督徒,我也是基督家庭,我却一直没有真正成为一名基督徒,但还是很感激,我相信这是一场被神祝福了的手术。 手术做完送了两支白蛋白,按照流程基本没什么事了。但是担心有别的情况,所以重症监护室外面得一直有人看着。我跟弟弟说好一替一会在这守着,这时我俩都已经三十多个小时没合眼了。我在监护室外待到夜里两点半,期间在凳子上眯了一会,然后弟弟来替我。
如何提升Mac下生产效率
- 03 Nov, 2019
工具篇 效率神器Alfred可以从Alfred官网下载,免费版没有workflow功能,付费购买Powerpack可以使用。建议大家购买正版,如果想找破解版,网上也有很多,自行搜索。 这有一篇比较详细的Alfred的使用教程,大部分都被涵盖进去了。这里主要介绍几种对开发帮助比较大的workflow插件,官方workflows地址在这里,可以根据需要选择自己喜欢的插件使用。Youdao 其实在开发过程中经常遇到一些与英文打交道的场景,除了系统自带的字典功能,我们常用的就是有道了。直接调起客户端去搜也很方便,但是如果是频繁操作或者是在全屏下操作,还需要跑到有道里面搜一个单词,搜一句话还是会显得麻烦。 这里有一个Youdao的workflow插件。下载地址。安装之后可以通过在Alfred中键入yd {query}调出有道字典,并直接搜索翻译结果。使用Enter复制翻译结果,支持多种语言和句子翻译。CodeVar 生成可用的代码变量,解决你不知道该如何命名一个函数,类型,属性的烦恼。支持小驼峰(xt)、大驼峰(dt)、下划线(xh)、中划线(zh)、常量命名(cl)这几种命名方式。 下载地址Github 可以直接在Alfred的输入框搜索github仓库。 下载地址代码片段 Snippets。可以将自己常用的代码片段放进去,让后通过快捷键调出Snippets,直接使用代码。 func test<#Name#>() { <#statements#> }更多插件 Workflow官方插件 packal 第一个插件库,好像很久不维护了,很多插件链接都失效了。第二个packal是alfred的一个活跃论坛,比较推荐到这里发现适合你的好东西。另外github,各个博主的推荐文章也有很多,基本你能想到的搜索主题都可以找到对应的workflow插件,这里就不得不佩服社区力量的强大,还有就是这个自定义插件功能做的太棒了。 Dash 文档查询,下载地址 遇到问题最好的解决方式就是查文档,API描述能消除大部分自己的疑虑,Dash支持几十种语言的文档查询,还支持文档注释,tag功能。Dash+Alfred Dash本身支持很多IDE的插件,其中就包括Alfred插件,可以在Preferences->Integration中找到Alfred。使用方式如下:命令和快捷键篇 几个常用命令行工具 say say命令可以读出英文 $ say hello worldecho echo是输出命令 # 输出shell本身的文件名 $ echo $0将一段内容重定向至某一文件 $ echo "5.0" > .swift-versionman man命令可以查看某个命令行的详细使用 $ man echocurl curl为client url缩写。 $ curl www.baidu.com #查看网页源码 $ curl -i www.baidu.com #显示头文件 $ curl -o example.html https://www.example.com #将服务器回应保存成文件常用终端快捷键快捷键 含义Ctrl+a 跳到行首Ctrl+e 跳到行尾Ctrl+u 删除光标之前到行首的字符Ctrl+k 删除光标之前到行尾的字符Ctrl+w 删除从光标位置前到当前所处单词(Word)的开头Command+D 垂直分屏Ctrl+D 关闭垂直分屏自己动手写一个工具 AppleScript 编写自动化工具就要选一中脚本语言,这里选择了AppleScript。 这是因为:它语法简单,并接近自然语言。(没有标点符号) 语法查询十分方便。(系统原生提供语法查询字典) mac自带Script Editor可以实现编写运行并检查语法 mac上主流应用都含有AppleScript的语法支持 关于AppleScript这有一份简单的教程。 我们摘取其中几点,简要讲下AppleScript的语法。tell命令 tell application "Safari" activate end tell 告诉Safari启动。 tell命令可以嵌套使用。 set命令 定义变量到剪贴板。 set myVariables to clipboardAppleScript Suite AppleScript Suite就是 AppleScript 类(class),及其元素(element)和属性(property)的集合。这个就是OmniFocus在AppleScript上定义的Suite。脚本字典 打开「脚本编辑器」 → 新建一个脚本 → 用快捷键 Command+Shift+O 打开 AppleScript 字典(Dictionary)。这个打开之后就能看到各个应用的Suite,我们可以通过这些说明,来完成功能的使用。 编写脚本 有了上面的基础我们来编写一个选中文件夹,然后让iTerm2跳转到指定目录的脚本。 定位文件路径 tell application "Finder" set pathFile to selection as text --POSIX是mac中的根目录变量 set pathFile to get POSIX path of pathFile --防止目录存在空格跳转不了 set pathFile to quoted form of pathFile set pathFile to "cd " & pathFile end tell现在我们通过tell和set命令已经获取到了cd pathFile这段文本。 在iTerm中输入命令 上一步获取到了文本命令,接下来我们需要将这段命令在iTerm中执行。那怎么将上面的文本写入iTerm中呢? 大致思路为:打开iTerm > 创建窗口 > 写入命令。 接下来就是如何实现这几个步骤,通过查看iTerm的Suite:我们可以找到create window with default profile,write命令。有可能这就是我们需要的也有可能不是,所以有些时候是需要尝试出来的。最终实现上面步骤的代码为: tell application "iTerm" create window with default profile tell current session of current window write text pathFile end tell end tell然后完整代码为: tell application "Finder" -- get selection path set pathFile to selection as text set pathFile to get POSIX path of pathFile -- fix space problem in the directory set pathFile to quoted form of pathFile tell application "iTerm" create window with default profile tell current session of current window write text pathFile end tell end tell end tellAppleScript+Alfred 上面过程我们编写了脚本,但是怎么方便的使用它呢?可以利用Alfred。 在workflow中,我们增加一个Hotkeys。然后绑定快捷键Command+O,从快捷键脱出一条线,选择Actions > Run Script,选择Language为AS(AppleScript),粘贴我们刚才写的代码,保存即可。之后我们可以选中文件夹,触发热键Command+O就能直接在iTerm中定位至改目录了。
《图解TCP/IP》总结
- 01 Sep, 2019
最近刚把《图解TCP/IP》翻了一遍,是有很多收获,但是还有很多东西不是太懂。又因为计算机网络涉及的内容多且杂,所以有了这篇记录性质的文章。网络基础知识 计算机网络最重要的一个概念就是协议,简单来说,协议就是计算机与计算机之间通过网络实现通信时事先达成的一种“约定”。这种“约定”使那些由不同厂商的设备,不同的CPU以及不同操作系统组成的计算机之间,只要遵循相同的协议就能实现通信。 那这个协议由谁来规定呢,ISO(国际标准化组织)制定的一个国际标准OSI(开放式通信系统互联参考模型)。本书将要说明的TCP/IP并非ISO所制定的某种国际标准,而是由IETF(互联网工程任务组)所建议的致力于推进标准化作业的一种协议。这里提一下,通常OSI只是一种参考模型,他将网络分层,但是它并没有规定任何具体的协议,协议内容则归属为TCP/IP。 OSI参考模型中各个分层的作用:传输方式的分类 面向有连接型: 在发送数据之前,需要在收发主机之间连接一条通信线路。比如打电话,必须对方接通才能开始通话。 面向无连接型: 不要求建立连接和断开连接,发送端可以任何时候发送数据,接收端也永远不知道自己何时会收到数据。 分组交换: 让连接到通信电路的计算机将所要发送的数据分成多个数据包,按照一定的顺序排列之后分别发送,就是分组交换。有了分组交换,数据被细分,所有计算机可以一起收发数据,这样也就提高了通信线路的利用率。TCP/IP正是采用了分组交换技术。 单播(Unicast): 一对一通信,早先的固定电话就是单播通信的典型例子。 广播: 将消息从1台主机发送到与之相连的所有其他主机。典型例子就是电视播放。 多播 与广播类似,将消息发到多个接收主机,不同之处在于多播要限定某一组主机作为接收端,最典型的例子就是电话会议。 任播 从目标主机群中选择一台最符合网络条件的主机作为目标主机发送消息,通常,所被选中的那台特定主机将返回一个丹波信号,随后发送端主机会只跟这台主机进行通信。任播在实际网络中的应用有DNS根域名解析服务器。 地址 TCP/IP通信中使用MAC地址、IP地址、端口号等信息作为地址标识。 地址很重要的两个属性是唯一性和层次性。 MAC地址由设备的制造厂商对每块网卡进行分别制定。人们可以通过制造商识别号、制造商内部产品编号以及产品通用编号确保MAC地址的唯一性。但并不具有层次性。IP地址因具有网络号和主机号而具有层次性。 TCP/IP基础知识 TCP/IP的诞生TCP/IP的具体含义 从字面意义上讲,有时这就是指TCP和IP两种协议,但是更多情况下,他是利用IP进行通信时所必须用到的协议群统称。TCP/IP规范--RFC 前面提到的TCP/IP的协议由IEFT讨论制定,被人们列入RFC(Request For Comment)文档并公布到互联网上。RFC不仅记录了协议规范内容,还包含了协议的实现和运用的相关信息,以及实验方面的信息。可以通过RFC Editor查看RFC所有内容。 TCP/IP协议分层模型这个就是常被问及网络分层模型,它有两种,一种是OSI7层模型;一种是TCP/IP的5层模型,也有一种分法是4层模型,是将5层模型中的网卡层和硬件层合为网络接口层。 物理层(硬件) TCP/IP的最底层是负责数据传输的硬件。这种硬件就相当于以太网或电话线路等物理层的设备。 主要功能是:利用传输介质为数据链路层提供物理连接,负责处理数据传输并监控数据出错率,以便数据流的透明传输。 数据链路层(网络接口层) 网络接口层(有时人们也将网络接口层与硬件层合并起来称作网络通信层。) 利用以太网中的数据链路层进行通信,因此属于接口层。也就是说,把它当做让NIC起作用的“驱动程序”也无妨。 网络层(互联网层)网络层(Network layer)是参考模型的第3层。主要功能是:为数据在结点之间传输创建逻辑链路,通过路由选择算法为分组通过通信子网选择最适当的路径,以及实现拥塞控制、网络互联等功能。 IP IP是跨越网络传送数据包,使整个互联网都能收到数据的协议。IP协议使数据能够发送到地球的另一端,这期间它使用IP地址作为主机的标识。 ICMP(Internet Control Message Protocol) IP数据包在发送途中一旦发生异常导致无法到达对端目标地址时,需要给发送端发送一个发生异常的通知。ICMP就是为这一功能而制定的。它有时也被用来诊断网络的健康状况。 ARP(Address Resolution Protocol) 地址解析协议,从分组数据包的IP地址中解析出物理地址(MAC地址)的一种协议。 传输层传输层最主要的功能就是能够让应用程序之间实现通信。计算机内部,通常同一时间运行着多个程序。为此,必须分清是哪些程序与哪些程序在进行通信。识别这些应用程序的是端口号。 TCP TCP是一种面向有连接的传输层协议。它可以保证两端通信主机之间的通信可达。TCP能够正确处理在传输过程中丢包、传输顺序乱掉等异常情况。此外,TCP还能够有效利用带宽,缓解网络拥堵。 TCP首部其中控制位是用于连接管理的标记位,字段长为8位,每一位从左至右分别为CWR、ECE、URG、ACK、PSH、RST、SYN、FIN。这些控制标志也叫做控制位。当它们对应位上的值为1时,具体含义如下所示。连接管理 TCP会在数据通信之前,通过TCP首部发送一个SYN包作为建立连接的请求等待确认应答(TCP中发送第一个SYN包的一方叫做客户端,接收这个的一方叫做服务端。) 。如果对端发来确认应答,则认为可以进行数据通信。如果对端的确认应答未能到达,就不会进行数据通信。此外,在通信结束时会进行断开连接的处理(FIN包)。 可以使用TCP首部用于控制的字段来管理TCP连接。一个连接的建立与断开,正常过程至少需要来回发送7个包才能完成(建立一个TCP连接需要发送3个包。这个过程也称作“三次握手”。) 。UDP UDP是User Datagram Protocol的缩写。 UDP有别于TCP,它是一种面向无连接的传输层协议。UDP不会关注对端是否真的收到了传送过去的数据,如果需要检查对端是否收到分组数据包,或者对端是否连接到网络,则需要在应用程序中实现。 由于UDP面向无连接,它可以随时发送数据。再加上UDP本身的处理既简单又高效,因此经常用于以下几个方面:包总量较少的通信(DNS、SNMP等) 视频、音频等多媒体通信(即时通信) 限定于LAN等特定网络中的应用通信 广播通信(广播、多播)UDP首部应用层(会话层及以上分层) TCP/IP的分层中,将OSI参考模型中的会话层、表示层和应用层的功能都集中到了应用程序中实现。这些功能有时由一个单一的程序实现,有时也可能会由多个程序实现。因此,细看TCP/IP的应用程序功能会发现,它不仅实现OSI模型中应用层的内容,还要实现会话层与表示层的功能。 FTP 在FTP中进行文件传输时会建立两个TCP连接,分别是发出传输请求时所要用到的控制连接与实际传输数据时所要用到的数据连接(这两种连接的控制管理属于会话层的功能。) 远程登录(TELNET和SSH) 远程登录是指登录到远程的计算机上,使那台计算机上的程序得以运行的一种功能。TCP/IP网络中远程登录常用TELNET(TELetypewriter NETwork的缩写。有时也称作默认协议。) 和SSH(SSH是Secure SHell的缩写。) 两种协议。 发送数据包 假设甲给乙发送电子邮件,内容为:“早上好”。而从TCP/IP通信上看,是从一台计算机A向另一台计算机B发送电子邮件。这一过程可以用如下图示表示:应用协议 HTTP 当用户在浏览器的地址栏里输入所要访问Web页的URI以后,HTTP的处理即会开始。HTTP中默认使用80端口。它的工作机制,首先是客户端向服务器的80端口建立一个TCP连接,然后在这个TCP连接上进行请求和应答以及数据报文的发送。 HTTP中常用的有两个版本,一个HTTP1.0,另一个是HTTP1.1。在HTTP1.0中每一个命令和应答都会触发一次TCP连接的建立和断开。而从HTTP1.1开始,允许在一个TCP连接上发送多个命令和应答(这种方式也叫保持连接(keep-alive)。) 。由此,大量地减少了TCP连接的建立和断开操作,从而也提高了效率。多媒体实现技术 由于TCP具有流控制、拥塞控制、重发机制等功能,有时应用所发出去的数据可能无法迅速到达对端目标主机。然而在互联网电话(使用的VoIP(Voice Over IP的缩写。) )和电视会议当中,即使有少许丢包,也希望系统延时少一点,非常注重系统的即时性。因此,在实时多媒体通信当中采用UDP。 然而,只使用UDP还不足以达到进行实时多媒体通信的目的。例如,在互联网电视电话议会中需要提供查询对方号码、模拟电话机的拨号以及以什么形式交互数据等功能。为此,需要一个叫做“呼叫控制”的支持。呼叫控制主要采用H.323与SIP协议。此外,还需要RTP协议(结合多媒体数据本身的特性进行传输的一种协议)和压缩技术(在网络上传输音频、视频等大型多媒体数据时进行压缩)的支持。 H.323 H.323是由ITU开发用于在IP网上传输音频、视频的一种协议。起初,它主要是作为接入ISDN网和IP网之上的电话网为目的的一种规范而被提出的。 H.323定义了4个主要组件。它们分别是终端(用户终端)、网关(吸收用户数据压缩顺序的不一致性)、网闸(电话本管理、呼叫管理)以及多点控制单元(允许多个终端同时使用)。SIP 与H.323相对的TCP/IP协议即是SIP(Session Initiation Protocol)协议。 “终端之间进行多媒体通信时,需要具备事先解析对方地址、呼出对方号码并对所要传输的媒体信息进行处理等功能。此外,还需要具备中断会话和数据转发的功能。这些功能(呼叫控制与信令)都被统一于SIP协议中。它相当于OSI参考模型中的会话层。RTP和RTCP UDP不是一种可靠性传输协议。因此有可能发生丢包或乱序等现象。因此采用UDP实现实时的多媒体通信需要附加一个表示报文顺序的序列号字段,还需要对报文发送时间进行管理。这些正是RTP(Real-Time Protocol)的主要职责。 RTP为每个报文附加时间戳和序列号。接收报文的应用,根据时间戳决定数据重构的时机。序列号则根据每发出一次报文加一的原则进行累加。RTP使用这个序列号对同一时间戳的数据(尤其是对于视频的数据。视频中一个帧的数据往往要超过一个包,然而它们发送的时间戳一致。此时就可以使用同一时间戳内不同的序列号加以区分。) 进行排序,掌握是否有丢包的情况发生。 RTCP(RTP Control Protocol)是辅助RTP的一种协议。通过丢包率等线路质量的管理,对RTP的数据传送率进行控制。 TLS/SSL与HTTPS 对于一些涉及机密信息的网络连接需要进行加密处理,Web中可以通过TLS/SSL(Transport Layer Security/Secure Sockets Layer。由网景公司最早提出的名称叫SSL,标准化以后被称作TLS。使用TLS/SSL的HTTP通信叫做HTTPS通信。 HTTPS中采用对称加密方式。而在发送其公共密钥时采用的则是公钥加密方式(对称加密虽然速度快,但是密钥管理是巨大的挑战。公钥加密密钥管理相对简单,但是处理速度非常慢。TLS/SSL将两者进行取长补短令加密过程达到了极好的效果。由于谁都可以发送公钥,使得密钥管理更为简单。) 。 确认公钥是否正确主要使用认证中心(CA(Certificate Authority) )签发的证书,而主要的认证中心的信息已经嵌入到浏览器的出厂设置中。如果Web浏览器中尚未加入某个认证中心,那么会在页面上提示一个警告信息。此时,判断认证中心合法与否就要由用户自己决定了。IEEE802.1X IEEE(The Institute of Electronical and Electronics Engineers,美国电子和电气工程师协会)委员会中,依据不同的工作小组制定了各种局域网技术标准。因于1980年2月启动局域网国际标准化项目,所以命名为802。 IEEE802.1X是为了能够接入LAN交换机和无线LAN接入点而对用户进行认证的技术(包括我们常用的WiFi) 并且它只允许被认可的设备才能访问网络。虽然它是一个提供数据链路层控制的规范,但是与TCP/IP关系紧密。一般,由客户端终端、AP(无线基站)或2层交换机以及认证服务器组成。蓝牙 蓝牙与IEEE802.11b/g类似,是使用2.4GHz频率无线电波的一种标准(因此,当IEEE802.b/g等设备与蓝牙设备一起使用时,无线电波信号削减有可能导致通信性能的下降。) 。数据传输速率在V2中能达到3Mbps(实际最大吞吐量为2.1Mbps)。通信距离根据无线电波的信号的强弱,有1药看看吧3 B、10药看看吧3 B、100药看看吧3 B三种类型。通信终端最多允许8台设备(其中一台为主节点,其他1~7台为受管节点。这种网络也叫做piconet,微微网。) 。 如果说IEEE802.11是针对笔记本电脑这样较大的计算机设备的标准,那么蓝牙则是为手机或智能手机、键盘、鼠标等较小设备而设计的标准。
【译】Swift World:设计模式--中介者模式
原文:https://medium.com/swiftworld/swift-world-design-patterns-mediator-e6b3c35d68b0 作者:Peng今天我们讨论一下中介者模式(Mediator)。这次不从抽象定义开始,而是用现实世界中的一个场景来解释它。在一个团队里,有产品经理,开发工程师,质量工程师。当开发完成了某些功能,将代码提交到仓库。相关环节人员,像质量工程师和产品经理需要被通知。protocol Collogue { var id: String { get } func send(message: String) func receive(message: String) } class Developer: Collogue { var id: String var qe: QE var pm: PM init(qe: QE, pm: PM) { self.id = "Developer" self.qe = qe self.pm = pm } func send(message: String) { qe.receive(message: message) pm.receive(message: message) } func receive(message: String) { print(message) } } class QE: Collogue { var id: String var developer: Developer var pm: PM init(developer: Developer, pm: PM) { self.id = "QE" self.developer = developer self.pm = pm } func send(message: String) { developer.receive(message: message) pm.receive(message: message) } func receive(message: String) { print(message) } } class PM: Collogue { var id: String var developer: Developer var qe: QE init(developer: Developer, qe: QE) { self.id = "PM" self.developer = developer self.qe = qe } func send(message: String) { developer.receive(message: message) qe.receive(message: message) } func receive(message: String) { print(message) } }每个角色都需要持有另一个角色的实例,这种连接方式是高耦合的,且很不容易修改。现在我们需要一个中介者帮助我们简化这个系统。中介者的目的是帮助对象之间相互交流。它让每个对象都是跟自己进行交互而不是其他对象。当前对象不需要持有别的对象,而是持有中介者。这样将解耦系统,它的结构图如下所示:我们来写一下代码: protocol Mediator { func send(message: String, sender: Colleague) } class TeamMediator: Mediator { var colleagues: [Colleague] = [] func register(colleague: Colleague) { colleagues.append(colleague) } func send(message: String, sender: Colleague) { for colleague in colleagues { if colleague.id != sender.id { colleague.receive(message: message) } } } }通过持有中介者,那几个角色对象变成了这样: protocol Colleague { var id: String { get } var mediator: Mediator { get } func send(message: String) func receive(message: String) } class Developer: Colleague { var id: String var mediator: Mediator init(mediator: Mediator) { self.id = "Developer" self.mediator = mediator } func send(message: String) { mediator.send(message: message, sender: self) } func receive(message: String) { print("Developer received: " + message) } } class QE: Colleague { var id: String var mediator: Mediator init(mediator: Mediator) { self.id = "QE" self.mediator = mediator } func send(message: String) { mediator.send(message: message, sender: self) } func receive(message: String) { print("QE received: " + message) } } class PM: Colleague { var id: String var mediator: Mediator init(mediator: Mediator) { self.id = "PM" self.mediator = mediator } func send(message: String) { mediator.send(message: message, sender: self) } func receive(message: String) { print("PM received: " + message) } }这样一来,整个结构就变成了下面这样:让我们用新的方式来使用它: //usage let mediator = TeamMediator() let qe = QE(mediator: mediator) let developer = Developer(mediator: mediator) let pm = PM(mediator: mediator) mediator.register(colleague: developer) mediator.register(colleague: qe) mediator.register(colleague: pm) mediator.send(message: "Hello world!", sender: developer)另一个相似的例子就是非常受欢迎的Notification(NSNotification)。你可以在网上找到很多相关的代码。
【译】设计模式引导--OOP的能力
- 20 Aug, 2019
原文链接 作者:Hitendra Solanki导读--本博客系列要求具有面向对象编程的中级专业知识。您应该对类、对象、构造函数、继承、值和引用类型有基本的了解。通过仔细地从头到尾阅读本系列文章,不管是中级还是高级开发,您都将有所收获。设计模式用于表示经验丰富的面向对象软件开发人员社区采用的最佳实践。 建造者模式帮助我们更简单更易读地创建一个类,它遵守着以下两条规则: 1、分割原始类和它的构造方法 2、在最后一个返回类的实例 建造者模式最佳的例子就是SwiftUI,是的你没有看错。SwiftUI中大部分类像是Text,Image都是使用的建造者模式。 问题: 想一下,一个Person类拥有不少于十个属性,当你要使用它时,你需要为它创建一个构造方法。它的构造者将拥有不少于十个参数,去管理这么一个带有很多参数的单一函数或构造方式将是非常困难的,最终你也会让这端代码失去可读性。看下面的例子: class Person { //personal details var name: String = "" var gender: String = "" var birthDate: String = "" var birthPlace: String = "" var height: String = "" var weight: String = "" //contact details var phone: String = "" var email: String = "" //address details var streeAddress: String = "" var zipCode: String = "" var city: String = "" //work details var companyName: String = "" var designation: String = "" var annualIncome: String = "" //constructor init(name: String, gender: String, birthDate: String, birthPlace: String, height: String, weight: String, phone: String, email: String, streeAddress: String, zipCode: String, city: String, companyName: String, designation: String, annualIncome: String) { self.name = name self.gender = gender self.birthDate = birthDate self.birthPlace = birthPlace self.height = height self.weight = weight self.phone = phone self.email = email self.streeAddress = streeAddress self.zipCode = zipCode self.height = height self.city = city self.companyName = companyName self.designation = designation self.annualIncome = annualIncome } }//This is function in Xcode-Playground which executes our test code func main() { let hitendra = Person(name: "Hitendra Solanki", gender: "Male", birthDate: "2nd Oct 1991", birthPlace: "Gujarat, India", height: "5.9 ft", weight: "85kg", phone: "+91 90333-71772", email: "hitendra.developer@gmail.com", streeAddress: "52nd Godrej Street", zipCode: "380015", city: "Ahmedabad", companyName: "Fortune 500", designation: "Software architect", annualIncome: "45,000 USD") //use of Person object print("\(hitendra.name) works in \(hitendra.companyName) compay as a \(hitendra.designation).") }//call main to execute our test code in Xcode-Playground main()/* Console output: Hitendra Solanki works in Fortune 500 compay as a Software architect. */将上面的例子在playground中运行一下,你会得到预期结果。逻辑上这也是对的。 我们可以尝试优化上面的代码,从解决这两个问题入手。 1、我们必须按照既定的顺序传参数,而不能通过重新排列参数提高可读性。 2、即使创建对象时我们不知道一些属性值,我们也不得不传入所有参数。 例如你需要创建一个Person类,但是这个人还在找工作。只有当他进入某一公司我们才能得到他的工作信息。 解决方案: 1、创建相关属性的逻辑分组。 2、为不同分组的属性创建不同的建造者类。 3、在建造者类中最后一步返回实例。 让我们从上面的例子开始,我们已经拥有一个Person类,它含有14个属性。我们仔细观察这14个属性,可以将它分为四组。 1、个人信息 2、联系方式 3、地址信息 4、公司信息 通过强大的设计模式我们可以解决上面两个问题,具体代码如下: //This is function in playground which executes our test code func main() { var hitendra = Person() //person with empty details let personBuilder = PersonBuilder(person: hitendra) hitendra = personBuilder .personalInfo .nameIs("Hitendra Solanki") .genderIs("Male") .bornOn("2nd Oct 1991") .bornAt("Gujarat, India") .havingHeight("5.9 ft") .havingWeight("85 kg") .contacts .hasPhone("+91 90333-71772") .hasEmail("hitendra.developer@gmail.com") .lives .at("52nd Godrej Street") .inCity("Ahmedabad") .withZipCode("380015") .build() //use of Person object print("\(hitendra.name) has contact number \(hitendra.phone) and email \(hitendra.email)") //later on when we have company details ready for the person hitendra = personBuilder .works .asA("Software architect") .inCompany("Fortune 500") .hasAnnualEarning("45,000 USD") .build() //use of Person object with update info print("\(hitendra.name) works in \(hitendra.companyName) compay as a \(hitendra.designation).") }//call main to execute our test code main()//Person class which only contains the details class Person { //personal details var name: String = "" var gender: String = "" var birthDate: String = "" var birthPlace: String = "" var height: String = "" var weight: String = "" //contact details var phone: String = "" var email: String = "" //address details var streeAddress: String = "" var zipCode: String = "" var city: String = "" //work details var companyName: String = "" var designation: String = "" var annualIncome: String = "" //empty constructor init() { } }//PersonBuilder class helps to construct the person class instance class PersonBuilder { var person: Person init(person: Person){ self.person = person } //personal details builder switching var personalInfo: PersonPersonalDetailsBuilder { return PersonPersonalDetailsBuilder(person: self.person) } //contact details builder switching var contacts: PersonContactDetailsBuilder { return PersonContactDetailsBuilder(person: self.person) } //address details builder switching var lives: PersonAddressDetailsBuilder { return PersonAddressDetailsBuilder(person: self.person) } //work details builder switching var works: PersonCompanyDetailsBuilder { return PersonCompanyDetailsBuilder(person: self.person) } func build() -> Person { return self.person } }//PersonPersonalDetailsBuilder: update personal details class PersonPersonalDetailsBuilder: PersonBuilder { func nameIs(_ name: String) -> Self { self.person.name = name return self } func genderIs(_ gender: String) -> Self { self.person.gender = gender return self } func bornOn(_ birthDate: String) -> Self { self.person.birthDate = birthDate return self } func bornAt(_ birthPlace: String) -> Self { self.person.birthPlace = birthPlace return self } func havingHeight(_ height: String) -> Self { self.person.height = height return self } func havingWeight(_ weight: String) -> Self { self.person.weight = weight return self } }//PersonContactDetailsBuilder: update contact details class PersonContactDetailsBuilder: PersonBuilder { func hasPhone(_ phone: String) -> Self { self.person.phone = phone return self } func hasEmail(_ email: String) -> Self { self.person.email = email return self } }//PersonAddressDetailsBuilder: update address details class PersonAddressDetailsBuilder: PersonBuilder { func at(_ streeAddress: String) -> Self { self.person.streeAddress = streeAddress return self } func withZipCode(_ zipCode: String) -> Self { self.person.zipCode = zipCode return self } func inCity(_ city: String) -> Self { self.person.city = city return self } }//PersonCompanyDetailsBuilder: update company details class PersonCompanyDetailsBuilder: PersonBuilder { func inCompany(_ companyName: String) -> Self { self.person.companyName = companyName return self } func asA(_ designation: String) -> Self { self.person.designation = designation return self } func hasAnnualEarning(_ annualIncome: String) -> Self { self.person.annualIncome = annualIncome return self } }/* Console output: Hitendra Solanki has contact number +91 90333-71772 and email hitendra.developer@gmail.com Hitendra Solanki works in Fortune 500 compay as a Software architect. */在上面的例子中,我们把Person类根据职责分割成了几个不同的类。我们创建了多个建造者,他们分别管理相关分组内的属性,而Person只持有这些建造者。 我们拥有一个建造者基类PersonBuilder和四个衍生的建造者类,PersonPersonalDetailsBuilder, PersonContactDetailsBuilder, PersonAddressDetailsBuilder 和 PersonCompanyDetailsBuilder。 当其他四个从Personbuilder衍生出来的建造者需要更新相关属性时,Personbuilder这个基类可以帮助我们在它们之间进行转换。 在上面的例子中我们可以看到新的构造方法变得更加易读了,我们可以用一种更加优雅的方式更新一组或者某一个属性。 需要注意一下,上面的例子中我们再每个建造者更新方法之后返回了它自己。这让我们能够在相同的建造者中写出链式方法,而不是分开的多行。这个概念称为流程模式。 优点 1、用一种优雅的方式很容易地初始化一个含很多参数的类。 2、遵从单一职责原则。 3、根据你的情况,以任意的顺序初始化对象和更新属性。
iOS国际化及本地化(一)不同语言的差异处理及测试
国际化及本地化概念 将标题取名为国际化及本地化(internationalization and localization),是因为这两个概念是有差异的,而这个差异常常被我们忽略,以下是维基百科的解释:国际化是指在设计软件,将软件与特定语言及地区脱钩的过程。当软件被移植到不同的语言及地区时,软件本身不用做内部工程上的改变或修正。本地化则是指当移植软件时,加上与特定区域设置有关的信息和翻译文件的过程。 国际化和本地化之间的区别虽然微妙,但却很重要。国际化意味着产品有适用于任何地方的“潜力”;本地化则是为了更适合于“特定”地方的使用,而另外增添的特色。用一项产品来说,国际化只需做一次,但本地化则要针对不同的区域各做一次。这两者之间是互补的,并且两者合起来才能让一个系统适用于各地。有些时候我们也会用国际化或者全球化代替这两者含义。 作为一款优秀的产品我们做多语言版本时不应仅仅考虑到翻译这一层面,还有更多本地化相关内容需要我们注意,这篇文章主要涉及的也是本地化这一块。 国际化工作流程本篇文章主要介绍Internationalize和Test这两步。 本文目录为:1.增加多语言 2.UI元素的本地化 3.资源文件本地化 4.字符串相关的本地化 5.使用NSLocal进行本地化 6.从右到左语言的处理 7.本地化测试增加多语言 1、在项目导航栏选择项目(不是target) 2、在Localizations一栏,点击“+”号,添加语言每个条目都是由语言名称和语言id构成,例如Chinese(Simplified)(zh-Hans), Japanese(ja) 至此我们的项目就开启了对应语言的本地化支持。 3、在对话框中选中你想本地化的文件。 语言和区域的影响通过观察系统日历,我们可以看到即使语言一样,国家区域的不一致也会有一些约定上的区别,关于日期和时间的本地化会在下面介绍。 1、语言设置:Setting -> General -> Language & Region同样的,关于Region和Calendar的设置也在该页面。 资源文件的本地化 storyboard, xib文件 对于sotryboard和xib文件的本地化是Xcode直接支持的。 在添加语言时会提示我们自动选择创建本地化文件,如果是在添加语言之后创建的IB文件,可以通过xcode右侧属性栏中点击Localize...生成本地化文件。 图片文件 1、方法一 对于图片内容我们可以通过同IB文件的方式进行本地化,但是有一个限制就是图片要是放到项目文件层级的,而不能放到Assets文件夹中。 好消息是Xcode 11将放开这种限制,对于Assets引入的图片也可以做本地化处理。 2、方法二 除了Xcode本身支持的方式,我们还可以通过命名来区分图片内容,把图片名当做需要本地化的字符串,各个语言对应不同版本的图片名,这样也可以实现图片文件的本地化。 音视频及其他资源文件 如果是内置的像是音视频,json或者其他类型的配置文件这类内容,可以使用图片文件的方法二进行引入。 更多详细的设置可以参考这个文章:iOS语言国际化/本地化-实践总结 UI元素的本地化 使用Auto layout Auto layout是相对布局,它有能力在语言和区域变化时进行自适应。以下有几点使用auto layout的技巧: 1、移除宽度的约束 相同含义下不同语言宽度往往不一样,应该让控件能够自适应。 2、使用内容内部大小 fields和label默认是自动调整大小的,如果一个显示本地化内容的视图需要这个功能,选择该view,选择Editor > Size To Fit Content 3、使用leading和trailing属性 正常leading和trailing对应left和right,他们含义相同。但是有些国家,像是希伯来和阿拉伯的人使用习惯是从右往左。如果你是使用leading和trailing,在该环境下将自动对应right和left。 4、将视图固定到相邻视图 就是定义相邻约束,避免某一视图变化导致重叠。 使用伪本地化发现问题 这个功能只支持使用storyboard和xib进行布局的UI。 1、选中需要测试的.storyboard或者.xib文件 2、选择菜单栏 View > Assistant Editor > Show Assistant Editor字符串相关的本地化 使用Unicode字符串 对于所有面向用户的字符串都要使用NSString, NSAttributedString,对于Swift就是String, AttributedString,因为他们支持Unicode,Unicode是世界上所有书写系统的字符编码标准。 对于一些特殊的字符串需求: 1、访问字符串中的字符 使用NSString中的rangeOfComposedCharacterSequenceAtIndex:和 rangeOfComposedCharacterSequencesForRange:方法,他们会确保你在取字符串时不会破坏原文本。看一个例子你可能会明白:这两个文字无论是在UTF-16还是UTF-32编码的情况下都是不同的长度,所以我们不能通过长度而要通过以上的两种方式取目标字符串。 2、遍历字符串 如果我们要遍历展示下面的字符串:可以通过enumerateSubstringsInRange:options:usingBlock:方法,其中options参数如果传递NSStringEnumerationByComposedCharacterSequences将会按照最小字符进行遍历,如果选用NSStringEnumerationByWords将会按照词语进行遍历。 以上例子使用该值遍历的结果是:更多关于字符串相关的本地化问题可以参照该条视频: WWDC 2013 Making Your App World-Ready3、关于人名,邮寄地址,电话号码的检测 因为不同国家对于人名和电话号码的规则差别较大,我们可以针对不同国家写正则进行检测,也可以使用苹果提供的一个特殊含义字符的检测类:NSDataDetector 支持检测的类型包括日期,地址,链接,手机号,交通信息。 获取当前语言 将语言设置为English(United Kingdom),区域设置成United States,通过以下API获取到: //en NSString *languageID = [[NSBundle mainBundle] preferredLocalizations].firstObject;一般获取语言所用的方式是通过Bundle也就是第一种方式。 使用NSLocal进行本地化 NSLocale对象封装关于特定区域格式化标准的信息,包括日期,时间,测量,数字,货币等一系列内容。 将语言设置为English(United Kingdom),区域设置成United States,通过以下API获取到: //en-GB_US [NSLocale currentLocale].localeIdentifier; //en [NSLocale currentLocale].languageCode;其中languageCode跟通过Bundle获取到的是一样的。 其中localIdentifier表示为en-GB_US,对应为:语言id-国家id_区域码,这几个内容都可以通过NSLocal对象取到。 获取特定语言的引号 因为每种语言对于引号的使用是不一样的,我们可以通过NSLocal获取到引号 //1.Get the language that the app is using. NSString *languageID = [[NSBundle mainBundle] preferredLocalizations].firstObject; //2.Get the associated locale object. NSLocale *locale = [NSLocale localeWithLocaleIdentifier:languageID]; //3.Get the beginning and ending symbols for quotes from the locale object. bQuote = [locale objectForKey:NSLocaleQuotationBeginDelimiterKey]; eQuote = [locale objectForKey:NSLocaleQuotationEndDelimiterKey]; //4.Format a string using the locale-sensitive quotes. quotedString = [NSString stringWithFormat:@"%@%@%@", bQuote, myText, eQuote];以下展示了不同区域对于myText为@"iPhone"时的字符串效果。字符串的本地化 1、创建格式化字符串 应该使用localizedStringWithFormat:而不是stringWithFormat:。 NSString *localizedString = [NSString localizedStringWithFormat:@"%3.2f", myNumber];此方法会根据系统Local进行显示。 日期时间本地化 2、日期和时间转字符串 使用NSDateFormatter表示NSDate对象。推荐使用这个方法:localizedStringFromDate:dateStyle:timeStyle:。 //14 Aug 2019 at 11:19 NSString *localizedDateTime = [NSDateFormatter localizedStringFromDate:[NSDate date] dateStyle:NSDateFormatterMediumStyle timeStyle:NSDateFormatterShortStyle];下表展示了语言为英语,区域是美国时的日期和时间格式:下表展示了dateStyle为NSDateFormatterMediumStyle,timeStyle为NSDateFormatterShortStyle在不同语言和地区时的表现形式:3、使用自定义日期和时间格式 //1.Create an NSDateFormatter object. NSDateFormatter *dateFormatter = [NSDateFormatter new]; //2.get a localized format string from a template that you provide. NSString *localeFormatString = [NSDateFormatter dateFormatFromTemplate:@"MMM d" options:0 locale:dateFormatter.locale]; //3.Set the format of the NSDateFormatter instance to the locale-sensitive format string. dateFormatter.dateFormat = localeFormatString; //4.Use the stringFromDate: method to get a localized string representation of the date. NSString *localizedString = [dateFormatter stringFromDate:[NSDate date]];在不同语言和区域下localizedString对应的内容为:3、解析日期字符串 //1.Create a date formatter object. NSDateFormatter *dateFormatter = [NSDateFormatter new]; //2.Set the formatter’s style to a preset style. dateFormatter.dateStyle = NSDateFormatterMediumStyle; //3.If the input string is not expected to contain a time, set the time style to none. dateFormatter.timeStyle = NSDateFormatterNoStyle; //4.Set the leniency to YES (enables the heuristics). dateFormatter.lenient = YES; //5.Convert the string to a date object. NSDate *date = [dateFormatter dateFromString:inputString];我们输入的字符串是9/3/14,dateStyle设为NSDateFormatterShortStyle,如果区域为美国,我们得到的NSDate信息为:2014-09-03 07:00:00 +0000,如果区域为德国,我们将得到2014-03-09 08:00:00 +0000。 数字本地化 本地化设置会影响小数点符号,千分符,货币符等内容,比如数字1,234.56在意大利应该表示为1.234,56,所以对于数字的格式化我们应该用NSNumberFormatter处理。 注意:NSNumberFormatter不是现成安全的 1、将Number转成本地化的字符串 可以使用NSNumberFormatter的localizedStringFromNumber:numberStyle:方式 NSString *localizedString = [NSNumberFormatter localizedStringFromNumber:myNumber numberStyle:NSNumberFormatterDecimalStyle];以下是不同语言和区域关于数字的显示效果,左侧的style及numberStyle:2、将字符串转成NSNumber对象 这个类似日期的转换 //1.Create a number formatter object. NSNumberFormatter *numberFormatter = [NSNumberFormatter new]; //2.Set the formatter’s style to a preset style. numberFormatter.numberStyle = NSNumberFormatterDecimalStyle; //3.Set the leniency to YES (enables the heuristics). numberFormatter.lenient = YES; //4.Convert the string to a number object. NSNumber *number = [numberFormatter numberFromString:inputString];3、通过NSCalendar计算日期 NSCalendar类封装了日历的所有区域差异和复杂性。说他具有复杂性是因为在不同国家,一年之中的月份可能是12或者13,一月中的天数可能是5到31的任意值,每周第一天可能是周六,周日或者周一。可以看下表因此使用NSCalendar取这些值将会很方便。 获取Calendar unit的方式为 //1.Create an NSDateComponents object. NSDateComponents *components = [[NSCalendar currentCalendar] components:NSDayCalendarUnit | NSMonthCalendarUnit | NSYearCalendarUnit | NSEraCalendarUnit fromDate:[NSDate date]]; //2.Access the values for day, month, year, and era. NSInteger day = [components day]; NSInteger month = [components month]; NSInteger year = [components year]; NSInteger era = [components era];4、监听本地化信息或者时区修改 可以通过NSCurrentLocaleDidChangeNotification监听区域的改变 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(localeDidChange:) name:NSCurrentLocaleDidChangeNotification object:nil];同样的监听时区变化可以通过NSSystemTimeZoneDidChangeNotification。 从右到左语言的处理 创建从右到左语言的交互界面 支持从右到左向的语言,在约束层面应该使用Auto layout中的leading和trailing属性,而不是right和right。可以通过以下对比看到区别整体像是做了水平的翻转,有很多控件像是segmented控件, 进度指示器系统会自动做翻转。但是有些情况是不需要翻转的:视频控制和时间线指示器 图片,除非他们传达方向感,如箭头 时钟 乐谱 图表(x轴和y轴总是在一致的方向)获取布局的方向性 如果我们想获取当前语言是否应该是从右到左项的语言可以通过以下方法: //right-to-left language if ([UIView userInterfaceLayoutDirectionForSemanticContentAttribute:view.semanticContentAttribute] == UIUserInterfaceLayoutDirectionRightToLeft) { … }设置文本的对齐方式 在iOS中默认的文本对齐方式是“natural”,在OS X中默认方式是“left”。natural的含义就是会感觉语言的方向自动调整为left或者right。 如果你想NSMutableParagraphStyle对象的对齐方式为自然的方向,可以: [[(NSMutableParagraphStyle *)paraStyle setAlignment:NSNaturalTextAlignment];对双向文本的处理 双向文本就是一段文本中及含有从右往左的文本还含有从左往右的文本。是不是感觉很诧异?因为即使像阿拉伯和希伯来国家这些书写习惯为从右往左,但是对于数字和拉丁文是从左往右写的。如果你使用的是标准控件,像Label,TextView,Textfiled他们会自动处理双向文本内容。如果你是使用自定义控件,那这些问题就需要你手动处理。 向双向文本添加Unicode标记 在某些特殊的时候,系统默认的行为可能会导致一些不正确的结果,这时我们可以通过添加Unicode标记进行纠正。 例如,手机号在所有语言中都是从左往右读的,如果一个需要本地化的字符串变量表示一个手机号,如果我们需要保证他是从左往右的顺序展示,需要再字符串首部增加一个从左往右的嵌入字符(LRE):U+202A,在字符尾部增加定向格式字符(PDF):U+202C。 // Wrap the plus (+) prefix and phone number in left-to-right directional markers NSString *phoneNumber = @"408-555-1212"; NSString *localizedPhoneNumber = [NSString stringWithFormat:@"\u202A%@\u202C", phoneNumber];翻转Cocoa Touch视图 有些视图是不应该翻转的,在iOS9之后可以通过UIView的semanticContentAttribute属性手动指定视图应该是从左到后还是从右往左的方式展示。 如果是想翻转图片可以通过UIImage的imageFlippedForRightToLeftLayoutDirection方法。 本地化测试 通过IB预览测试本地化 这个功能只能在.storyboard和.xib文件实现。选中preview之后我们可以通过其右下角的语言选项切换不同语言,然后我们可以实时观察调换语言之后的效果。 通过伪语言功能测试 通过Edit Schema > Run > Options 然后点开语言选项,除了各种系统支持语言外,翻到最下面可以看到这几个选项。1.Double-Length Pseudolanguage 可以将文本内容变成两倍长度。 2.Right-to-Left Pseudolanguage 将语言方向改成从右往左,也可以将语言改成阿拉伯文或者希伯来文。 3.Accented Pseudolanguage 带重音符号。 4.Bounded String Pseudolanguage 带边界的字符串。 5.Right-to-Left Pseudolanguage With Right-to-Left Strings 同从右往左语言。 引用 苹果文档:Internationzlization and Localization Guide
iOS开发月报#13|201907
- 01 Aug, 2019
这里记录过去一个月,我看到的值得分享的内容,包含但不限于iOS知识,每个月的最后一天发布。 欢迎推荐内容,可以前往zhangferry/iOSMonthlyReport提交issue。Tips Golbal queues的优先级 如果要在后台执行非UI相关的工作, 一般把这部分工作放在Global queue. Global queue是一种系统内共享的并行的队列。申请Global queue的方法很简单: let userQueue = DispatchQueue.global(qos: .userInitiated)其中后面的.userInitiated参数代表队列的优先级。该优先级公有6中分类,有高到低为: userInteractive>default>unspecified>userInitiated>utility>background 通过该段代码验证: for i in 1...3 { DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async { NSLog("DispatchQoS.QoSClass.default, %d", i) } DispatchQueue.global(qos: DispatchQoS.QoSClass.background).async { NSLog("DispatchQoS.QoSClass.background, %d", i) } DispatchQueue.global(qos: DispatchQoS.QoSClass.unspecified).async { NSLog("DispatchQoS.QoSClass.unspecified, %d", i) } DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).async { NSLog("DispatchQoS.QoSClass.userInitiated, %d", i) } DispatchQueue.global(qos: DispatchQoS.QoSClass.userInteractive).async { NSLog("DispatchQoS.QoSClass.userInteractive, %d", i) } DispatchQueue.global(qos: DispatchQoS.QoSClass.utility).async { NSLog("DispatchQoS.QoSClass.utility, %d", i) } }其中userInitiated为LIFO(后进先出),即如果有新插入的userInteractive级别的队列任务,为先执行新任务之后再执行之前该级别任务。其余优先级的队列任务均是FIFO(先进先出)。 “No such module” when using @testable in Xcode Unit tests 因为测试工程和主工程分属不同Module,所以如果我们想在测试项目中调用主工程代码需要导入主工程: @testable import moduleName这个时候如果报以上错误,会有以下可能: 1、targetName错误 这个可以去Target->Build Setting->Product Module Name确认。 2、主工程和测试模块支持版本号不一致 保证Build Setting->iOS Deployment Target中的版本号在主工程和测试工程中一致。 authenticating with the app store上传卡顿 如果往AppStoreConnect传包一直卡在这个步骤,可以试一下这种方式: 第一步:cd ~ 第二步:mv .itmstransporter/ .old_itmstransporter/ 第三步:"/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/itms/bin/iTMSTransporter" 等待执行完成。 订阅的freetrial没有标明截止时间被拒 如题,如果订阅功能含freetrial,一定要在freetrial按钮旁边标清楚试用的截止时间。 WakaTime 一款能够统计开发时间的网页端应用,支持众多IDE。集成到IDE之后,注册账号然后会获取到一个Key,绑定该值。然后当我们愉快的开发时,一些开发的信息就会被记录下来。我们可以通过其网页端的dashboard查看我们开发的情况。因为前几天升级XCode,需要重新装插件,所以最近几天的开发记录为空。 Lookin | 免费好用的iOS UI调试软件 出自QMUI团队,可能是小集的影响力已经很大了,这个软件首发之后竟发现已经有一众人开始关注这个东西了。可能因为UI调试这个操作是一个高频的行为,而Xcode目前又做的不够好,所以大家都期待有一个高效的UI调试工具。我也是下下来试了下,结合之前对Reveal和Sherlock的使用,说下自己的体会吧,首先是优点: 1、免费 2、支持动态修改UI元素属性,例如位置,颜色,圆角这些 3、可以查看CALayer的信息 因为是刚发布没多久,还是有些需要改进的地方的 1、动态修改UI元素的信息,均是通过UIView,和CALayer的信息进行修改,无法实现像UILabel改文案,更新行数等操作 2、3D的渲染有时候会有bug出现,位置不正确,另外仅支持一个左右的旋转不支持上下 3、屏幕适配还不支持,不能修改屏幕 其实我对sherlock也都仅限于使用,中和频率不高的使用性和高昂的费用,我选择试用结束就放弃,虽然Lookin还存在一些不足,但是免费大于一切啊,希望可以做的更好,为更多开发者带来便利。 Github R.swift Android开发中引用资源可以通过R机制,所谓R机制就是在我们创建一个Android项目的时候,IDE会自动帮我们创建一个名为R的类型,它所在的文件名称也是叫做R.java,R类型中没有任何方法,包含的是代表不同类型资源的内部静态类,而这些内部静态类中,也只有静态的属性,每个属性代表一个资源,故我们要引用某个资源类型中的某个资源,可用R.资源类型.资源名来引用。下面就是代码中的实例: // 从图片资源文件夹中加载名为"image_test"的图片以其创建位图 Bitmap aBitmap = BitmapFactory.decodeResource(this.getResources(), R.drawable.image_test);相对来说iOS中的资源引用就麻烦很多,需要通过字符串引用,无法代码补全,资源更换无法自检查。解决iOS资源引用的这些问题而借鉴Android方案的R.Swift应运而生。 传统的方式: let settingsIcon = UIImage(named: "settings-icon") let gradientBackground = UIImage(named: "gradient.jpg")使用R.Swift let settingsIcon = R.image.settingsIcon() let gradientBackground = R.image.gradientJpg()不光图片,它还支持Fonts,Resource files,Colors,Localized strings,Storyboards,Segues,Nibs,Reuseable Cells。 我第一次见到这玩意的时候就一个感受:wocao,🐂🍺! 更多关于R.Swift的使用规则参照官网说明。 Swift-Books github上的内容,可以分为两类,纯干货和干货的整理。这个库属于后者,是一个收录Swift&Object-C相关资料的仓库。收录的都是比较经典的学习资料,而且非常全! 建库已经两年了,但是star却不多,不知道是因为曝光度的问题,还是因为大家对设计版权问题的内容有些警惕。但不管怎样吧,我还是要推一推这个库。 open-source-ios-apps 开源的iOS应用列表。基本涵盖了iOS开发涉及的所有领域,通过完整的app去学习别人的开发技巧是一个相对直观的方式,面对这个大礼包,记得来看一看哈。 Chinese-Podcasts 中文博客的收录,包含科技,风投,生活,电影,设计等领域。 音频内容最大的优势就是便捷,你可以在走路,跑步,甚至休息的时候使用它。音频相比视频还有一个更大的好处是,它会给大脑预留更多的想象空间,听音频我们会思考的更多一些。 去年一直在听东吴同学会,最近一段时间则听ggtalk和硅谷早知道多一些。 文摘 1、生存是一种即时策略游戏,所有的人都是这场游戏的玩家。财务自由了,就是游戏赢家。 --《科技爱好者周刊:66期》2、我以为别人尊重我,是因为我很优秀。慢慢的我明白了,别人尊重我,是别人很优秀;优秀的人更懂得尊重别人,对人恭敬其实是在庄严你自己。3、所谓成熟的人,就是精神上能够自给自足的人。
iOS开发月报#12|201906
- 29 Jun, 2019
这里记录过去一个月,我看到的值得分享的内容,包含但不限于iOS知识,每个月的最后一天发布。 欢迎推荐内容,可以前往zhangferry/iOSMonthlyReport提交issue。Tips 闪光动画 图片上的闪光动画,类似这种效果:分析拆解可知这是一个带透明的白色渐变移动产生的效果。渐变+移动,我们可以使用CAGradientLayer + CABasicAnimation实现: func showGradientAnimation() { let gradient = CAGradientLayer() gradient.frame = canvasView.bounds //左上角到右下角的渐变 gradient.startPoint = CGPoint(x: 0, y: 0) gradient.endPoint = CGPoint(x: 1, y: 1) gradient.locations = [0.0, 0.0, 0.0] //调透明度渐变要使用白色 gradient.colors = [UIColor.init(white: 1, alpha: 0.0).cgColor, UIColor.init(white: 1, alpha: 0.3).cgColor, UIColor.init(white: 1, alpha: 0.0).cgColor] canvasView.layer.addSublayer(gradient) let animation = CABasicAnimation.init(keyPath: "locations") //从0位置开始从1位置结束 animation.fromValue = [0, 0, 0.3] animation.toValue = [0.7, 1, 1] animation.duration = gradientAnimationDuration animation.repeatCount = 1 animation.isRemovedOnCompletion = true gradient.add(animation, forKey: nil) }集成Universal Links时的几点注意事项 这个是onlink总结对于各个平台对应用间跳转的支持情况,其中deeplink指Universal Links:跳转至facebook指定页面 如果是facebook某一主页,可以通过: //这里可以替换成自己的主页id let url = URL(string: "fb://page?id=**")! UIApplication.shared.open(url, options: [:], completionHandler: nil)这是使用了app scheme的方式进行跳转的,要求本机安装了facebook才能跳转成功。 如果是跳转到某一个主页的某一个帖子,可以通过: //这里替换成固定的帖子链接 let url = URL(string: "https://www.facebook.com/**/posts/**")! UIApplication.shared.open(url, options: [:], completionHandler: nil)这是通过Universal Links方式跳转的,该链接可以通过苹果的验证。如果未安装应用会跳转到网页端,如果安装了就直接跳到指定页面。 那正常来说,facebook主页也应该可以通过支持deep link的https链接跳转才对,但实际测试来看是不行的。所以如果要加上是否安装的逻辑的话,就是: let url = URL(string: kFacebookHomePageSchemeUrl)! if UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url, options: [:], completionHandler: nil) } else { UIApplication.shared.open(URL(string: kFacebookHomePageUrl)!, options: [:], completionHandler: nil) }测试: 推荐:将连接复制到便签,邮件,短信,或者使用二维码的形式使用。 不要将链接粘贴到 Safari 中 - iOS 目前阻止从任何浏览器的地址栏进行深度链接。 验证apple-app-site-association的连接,可以苹果的验证工具 lottie动画效果在安卓和网页端正常在iOS端不正常 在一次使用lottie调用设计提供的动画时,该动画是一个放烟花,然后散开的动画。用网页预览可以完美展示效果,但是放到客户端运行时,却只展示一部分即烟花上飞过程,而没有散开效果。最后仔细查看文档,找到这个:经设计确认,烟花绽放的效果使用了AE中的Repeater(中继器)控件,而该控件在lottie的3.0版本还不支持,但是在2.5.2版本是支持的,回退版本至2.5.2解决了动画确认问题。 PS:一些古怪问题,第一反应要从官网文档说明、Issues、QA中找答案 几个产品相关概念 ROI(return on investment):投资回报率 反应产品的盈利情况,用百分比表示。 市场营销、运营活动,都是企业获利为出发点,通过利润/投资量化目标。利润的计算涉及财务,很多时候用更简单的收入作分子。当运营活动的ROI大于1,说明这个活动是成功的,能赚钱。 ecpm(effective cost per mille):每千次展示可以获得的广告收入 这是广告主预估自身收益的指标。 arpu(Average Revenue Per User):每用户平均收入 ARPU注重的是一个时间段内运营商从每个用户所得到的利润。很明显,高端的用户越多,ARPU越高。 LTV(life time value):生命周期总价值 意为客户终生价值,是公司从用户所有的互动中所得到的全部经济收益的总和。 推荐阅读 23 位开发者告诉你这次 WWDC 最让他们兴奋的新事物 对23位iOS开发者的采访,一起来看下他们眼中这届WWDC什么最让人兴奋。 免费领取小专栏 -- WWDC2019 内参 6月份的WWDC给我们开发者带来了很多东西,Dark Mode、Swift UI、Combine、iPad OS等等。也是我感觉近几年WWDC干货最多的一界了。干货太多不知道怎么学习怎么办?这里没故事的卓同学分享的WWDC2019 内参免费领取名额。可以看各位大佬对WWDC的最新解读分析,目前二十多天里已经更新了30篇文章!。 SwiftUI 的一些初步探索 (一) 这是一篇解读SwiftUI的文章,目前还有第二篇。而喵神也在计划写一本关于SwiftUI 和Combine 的书籍我已经计划写一本关于 SwiftUI 和 Combine 编程的书籍,希望能通过一些实践案例帮助您快速上手 SwiftUI 及 Combine 响应式编程框架,掌握下一代客户端 UI 开发技术。现在这本书已经开始预售,预计能在 10 月左右完成。如果您对此有兴趣,可以查看 ObjC 中国的产品页面了解详情及购买。十分感谢!我发现喵神总能一下找到作为新概念最应该注意的问题,比如SwiftUI和Swift5.1的关系,为什么需要新系统才能预览以及ViewBuilder里接受那些条件语句等。想了解SwiftUI 这篇文章真是必读 Github About-SwiftUIWWDC当天开始建立的一个专门收集SwiftUI资料的仓库,应该是史上最全了。果然大家还是最爱SwiftUI! MovieSwiftUI 使用 SwiftUI & Combine和MovieDB API实现的一款应用。 Talk is cheap, show me the code. 结合实践是最快速的了解一个概念的方式。这也是最近一段时间上升最快的SwiftUI&Combine相关仓库了。 CombineSwiftPlayground 一个帮助理解Combine概念的 Swift Playground。对于首次接触响应式编程的人来说具体事例是帮助理解概念很好的方式。 该Playgrounds要求Xcode11 beta2及以上版本才能查看。 rxswift-to-combine-cheatsheet 列举了Combine和RxSwift之间的差别和一些概念上的对比,对于有一点RxSwift概念的人来说可以快速的理解Combine,也是对于想从RxSwift迁移至Combine的一份参考。 文摘普通选民也开始意识到,民主机制已经不再能够为他们带来权力。世界正在变化,但他们摸不清变化的方式和原因。权力正在转移,但选民不知道权力去了哪儿。在英国选民的想象中,权力被欧盟夺走了,所以他们投票脱欧。而在美国选民的想象中,是既得利益者垄断了权力,所以他们支持反体制的候选人,比如伯尼·桑德斯和唐纳德·特朗普。但可悲的事实是,没有人知道所有的权力去了哪儿。就算英国离开欧盟、特朗普接掌白宫,权力也绝不会回到普通选民身上。 --未来简史月度小结 关于WWDC WWDC发布的信息里面最让我感兴趣的就是SwiftUI和Combine,目前从Github的活跃情况来看也是这两个技术相关仓亏最多。SwiftUI解决了写UI布局的痛点,而Combine的出现表明了苹果对响应式编程的认可,对于我这种使用了一年RxSwift的人来说简直是一种福音。还有一点是这两个大招都是对Swift的支持,这是一个很明显的信号:Swift已经很成熟了,很强大了,以后还会有更多更好的优化。 过去几年,你可以忽视 Swift,但是,未来几年,如果不拥抱 Swift,将无法紧跟着 Apple 生态圈。 关于生活 从上个月开始有规律的进行跑步,上个月跑步里程是60km,这个月是70km,距年初定的目标已经完成了56%。 跑步带来的一个好处是最近一段时间的睡眠质量有显著提高,睡得更香了。如果你有睡眠质量问题,可以考虑跑步这个方式尝试解决哦。 封面图来自于在公司园区跑道上的拍摄。
iOS开发月报#11|201905
- 03 Jun, 2019
这里记录过去一个月,我看到的值得分享的内容,包含但不限于iOS知识,每个月的最后一天发布。 欢迎推荐内容,可以前往zhangferry/iOSMonthlyReport提交issue。Tips 对UISearchBar样式的修改 1、完成一次搜索之后,调用 searchBar.resignFirstResponder()隐藏键盘,会将 searchBar的取消按钮默认置为disEnable。如果我们需要此时能够监听取消按钮的点击状态,需要恢复其可用状态: if let cancelButton = searchBar.value(forKey: "cancelButton") as? UIButton cancelButton.isEnabled = true }2、修改取消按钮的文案 //修改searchbar的取消按钮文案 searchBar.setValue("delete", forKey: "cancelButtonText")3、更改取消按钮文案样式 UIBarButtonItem.appearance(whenContainedInInstancesOf: [UISearchBar.self]) .setTitleTextAttributes([NSAttributedString.Key.font: UIFont.systemFont(ofSize: 15)], for: .normal)4、更改searchBar文本框文字样式 UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]) .defaultTextAttributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 15)]上传IAP时出现TCP 443问题 具体错误为: Communication error. Please use diagnostic mode to check connectivity. You need to have outbound access to TCP port 443.这是由于代理问题引起的上传错误,上传IAP至App Store Connect不需要代理,关掉代理继续上传就可以了。 上传IAP时出现:WARNING ITMS-90176 完整错误为:WARNING ITMS-90176: "Unrecognized Locale - The locale names used in localization directories at ( "Payload/sandbox.app/AccountKitStrings.bundle/Resources/cb_IQ.lproj" ) are invalid. iTunes supports BCP47 but not the UN M.49 specification. Refer to the Language and Locale Designations guide at https://developer.apple.com/library/content/documentation/MacOSX/Conceptual/BPInternational/LanguageandLocaleIDs/LanguageandLocaleIDs.html for more information on naming your language-specific directories.”该错误是由Xcode9不再兼容cb_IQ.lproj这个格式,所以只要一出AccountKitStrings.bundle中的cb_IQ.lproj文件即可。 推荐阅读 Core Image 之自定义 Filter~ 非常详细的介绍Core Image中Filter(滤镜)涉及的概念和使用方法。 作者是美图的iOS开发工程师,博客多讲解图像处理相关知识,是iOS图像领域的大牛,推荐关注。 iOS图形处理概论:OpenGL ES,Metal,Core Graphics,Core Image,GPUImage,OpenCV等 对于刚接触iOS图形相关框架的小白,有一些图形框架在字面上和功能上非常容易混淆。这里旨在总结一下各种框架,区分它们的概念和功能,以作日后进一步细分学习的指引。 Swift 5 字符串插值之美 Swift5除了ABI稳定并没有带来很多语法上的变化,你如果以为Swift5只有ABI稳定那你就错了,它还带来了一个很强的特性---字符串插值。一开始我还以为它是一个小特性,但是当我把它和AttributedStrings, sql联系到一块时,我才发现他的强大之处! 还有一篇将字符串插值应用到AttributedStrings上的文章: StringInterpolation in Swift 5 — AttributedStrings 如何选择开源许可证 关于几种常见开源许可证的区别:Github Python-100-Days Python - 100天从新手到大师。 作为一线移动端开发,或多或少都有着一个全栈的目标,而作为后端语言的Python无疑是最佳选择。一个star数多达3w+的Python教学项目,这足以说明Python的受欢迎程度,这份教程的受欢迎程度。 markdown-weixin 一个在线将 Markdown 转换为微信公众帐号文章格式的工具。 http://md.qikqiak.com/ AssetsExtractor 『Assets提取工具』是一款OSX平台上用于将Assets.car或xxx.app中打包的png图片、pdf等资源重新提取出来的开发者工具。Assets.car常见于iOS/Mac/Unity等开发中的资源打包。 awesome-ios-bluetooth 一个收集开发iOS蓝牙功能资料的仓库。从入门必读、蓝牙升级、ANCS、调试工具都有介绍。目前该库由我维护,大家有什么蓝牙相关的问题可以提issue给我。 firefox-ios 火狐浏览器的iOS开源库,由Swift4.2编写。是一个优秀的可供参考学习的开源项目。
将黑苹果系统升级至macOS 10.14.4(Mojave)
- 18 May, 2019
原有配置 操作系统: macOS 10.13.6 主板:AORUS MASTER z390 CPU: intel core i7-8700 显卡:NVIDIA GTX 750 内存:Geil 8G * 2 硬盘:tigo SSD 240G 如果要升10.14的系统,首先需要确认的是当前显卡是否支持。可以参照这个表Mojave硬件支持列表(持续更新中) 可以看到GTX 750已经无法驱动了,所以显卡要换。最好换成免驱的,我这里选了RX 560D。 注意事项 gtx750是即插即用,而rx560d有专门的供电线,所以更换显卡时一定要注意别忘了插rx560d的供电线。 升级准备 不管是装黑苹果还是升级黑苹果都需要有一个启动U盘,它可以在我们系统配置错误无法进入的时候,帮助我们通过U盘进入,然后我们再把配置改回来就行了。 制作启动U盘(大于8G) 1、插入U盘 2、打开 /Applications/Utilities/Disk Utility(磁盘工具) 3、选中U盘4、点击上方 Erase 选项按钮 5、你可以修改U盘名称 6、Format:选择Mac OS Extended(Journaled),中文对应:Mac OS扩展(日志); Scheme:选择GUID Partition Map,中文对应:GUID 分区映射7、点击Erase按钮8、下载UniBeast UniBeast版本要跟系统版本对应,需要注册tonymacx86账号才能下载。 9、安装UniBeast,需要把系统需要设置成英文才能进行安装。设置完毕,一路Continue。10、根据提示,选择Installation Type/Bootloader Configuration/Graphics Configuration然后完成,开始Copy系统文件。11、把MultiBeast拖进U盘 翻译自tonymacx86 升级Clover 如果想要安装macOS Mojave 10.14,它要求你的Clover Bootloader版本不低于r4515。最新的Clover版本可以在这下载。 升级系统 下载好升级程序之后,直接进行安装。会重启两次,之后是较长一段时间的等待(20-30分钟),跟正常macbook升级一样的流程。如果没有意外,那么恭喜你,黑苹果升级成功了。可能的问题 关机无法断电 这个问题网上有的说在config文件中的Acpi将FixShutdown选为true,有的说是增加电量修复的efi文件。我都试过,均无效,我看这方面的回答时间都比较久,应该是旧版本的解决方案。新版本只需要将FixShutdown制为false即可。 安装过程卡在最后2分钟或者卡在最开始18分钟 在EFI的drivers64UEFI文件中增加OsxAptioFixDrv3-64.efi文件即可。 完整的EFI文件,提取码:awxl。 如果因为配置出错无法进入系统 在BIOS界面选择U盘启动,即可通过U盘配置的EFI进入系统。然后更改正确设置即可。 重要文件记得备份Time Machine 免费 Carbon Copy Cloner ¥290.15 Super Duper $27.95Time Machine因为是苹果自带的功能,而且还免费,比较推荐使用这个。附一份教程 其他问题 当然配置黑苹果的机型组合有很多种,可能会遇到各式各样的问题。这里再贴几个可以参考的链接: macOS Mojave 10.14安装中常见的问题及解决方法 Hackintosh黑苹果驱动Clover
【译】iOS13新特性抢鲜看
原文链接 作者:MacRumors Staff 原文日期:2019-5-2初览 iOS13是苹果针对iPhone和iPad的下一代操作系统,将会在6月份的WWDC上和大家初次见面。传闻的功能包括夜间模式、iPad界面更新和新的主屏幕。我们可以期待什么 苹果位于加州的总部已经在开发iOS操作系统的下一个升级版本,该系统可以在iPhone、iPad和iPod touch上运行。 虽然目前我们对这款新软件的了解有限,但我们已经听到了一些有趣的传言,这些传言透露了一些我们有望在此次更新中看到的功能和变化,根据之前的更新,这次更新将被命名为“iOS 13”。iOS 12标志着苹果iOS开发政策的重大转变,工程师们现在有了更多的自由,可以在必要时推出尚未准备好的功能。 由于这个原因,iOS 13可能比之前的更新更加成熟,同时,任何未最终确定的功能都有可能被推迟。 由于苹果在iOS 12到iOS 13之间专注于bug修复和底层改进所引起的延迟,我们已经获取了惊人数量的泄漏信息,所以可以预见6月份iOS 13不少的特性。 我们可以预见的功能包括夜晚模式,音量外观的改变,iPad上关于多任务的更新,新的撤销手势,一个合并了“寻找手机”和“寻找朋友”的APP:“寻找我的手机”,闹钟和邮件的更新,以及像iOS app可以很容易的在Mac上使用这样的扩平台能力。 我们有望在6月份的WWDC首次一睹iOS 13的风采,届时将正式发布新款iPhone。开发者和开放的测试人员将更早一点使用到这次更新,beta版本系统测试期间,我们也会在MacRumors.com上持续跟进新的特性和改变。 可能的特性 夜间模式 iOS 13将首次拥有夜间模式,这个功能iOS用户已经期盼了相当一段时间了。这个夜间模式和MacOS Mojave里的夜间模式相呼应,它会会使用户拥有一个更好的夜晚观看体验。今年的WWDC里有一张夜间主题的效果图,可能正暗示了即将到来的iOS 13的夜间模式。iPad调整 iOS 13中将有几个特性是关于iPad的。据说,苹果将推出一项功能,可以在一个iPad应用程序中使用tab视图显示多个窗口。改进的多任务处理功能将在iOS 13中实现,iPad应用程序支持多个窗口和应用程序内的可堆叠卡。应用程序的特点是,最初附加在屏幕某一部分上的表格,可以通过拖动手势分离,变成一张可以操纵的卡片。 卡片可以叠在另一张上面,深度效果将指示卡片的上下效果,快速滑动卡片将使它消失。 新的手势 iPad或将拥有一个新的对于标准文本输入时的撤销手势,用户可以通过三根手指轻敲键盘区域,然后向左或者向右滑动,就可以撤销或者重做一个动作。 新的手势将允许用户在表视图和集合视图中选择多个项目,允许他们在项目列表上拖动多个手指来绘制选择,类似于Mac上的单击和拖动Finder。 音量指示器更新 iOS 13获奖包含一个新的音量指示器,它会比现在的指示器样式更缓和。 邮件 新的邮件app将把邮件内容分成市场、购买、旅游、不重要、更多等可搜索分类。此外还有一个稍后阅读的队列和一个针对特殊邮件线程的静音收件选项。 跨平台的iOS和Mac应用 在iOS 12和macOS Mojave的时候,苹果引入了一个新框架桥接iOS和Mac之间的应用,作为测试,像股票、家庭和录音等iOS独享的app被发布到macOS上。在iOS 13和macOS10.15, 苹果计划向开发者扩大这个功能,这将使得应用从iOS平台到Mac上的迁移更加容易。 新版“找到我的手机” 苹果正在开发一个融合“查找朋友”和“查找iPhone”的新应用,它很可能会随着iOS 13和macOS 10.15一同发布。该应用程序将包括一个新的“查找网络”功能,允许苹果设备被跟踪,即使没有连接到Wi-Fi或蜂窝网络,原理是利用附近的其他设备。 这个应用将包括已有的查找手机功能,像丢失模式、远程擦除设备。从“查找朋友”中提取的基于位置的共享选项也仍然可用。 据说苹果还在研究一个相关的硬件产品,像是瓦片一样的跟踪器。它被描述为一个“标签”,能够附着在任何设备上,并且通过用户的iCloud账号进行配对。当用户距离他们绑定的设备太远的话,这个小东西就会发出报警声,它是基于与iPhone之间的距离工作的。苹果没说什么时候发布它,但是有可能是跟新版iPhone一同发布。 地图 新版的地图应用将使哪些常用地址像是家或者公司的设置和导航更简单。频繁使用的地址信息将会更高效的分类,并且可以为他们配上图片。 提醒事项 一个新的提醒应用程序将在一个网格中包含四个默认部分,包括今天要完成的任务、所有任务、计划任务和标记任务。 图书 新版的图书应用将包含一个新的进度追踪功能和一个旨在鼓励用户阅读的奖励系统。 家庭 新的家庭应用将更好地与安全摄像头集成,并将提供一项无需第三方应用就能查看过去录音的功能。 健康 新的健康应用将有一个改良的每日活动视图和更加全面的月经周期跟踪。还会有一个“听力健康”的功能,它可以测量你耳机和周围环境的音量。 其他新功能 速度提升和bug修复 iOS 13和iOS 12很像,将会提高运行速度,并修复一个bug。 键盘 将会有一个新的默认的基于滑动的键盘可用,类似于SwiftKey。 字体 字体管理将在iOS 13中得到改进,设置应用程序将获得一个新的字体管理菜单。 更新分享栏 在iOS 13中,用于共享照片和web链接的共享表单界面将变得更加智能,这意味着用户可以向其中发送内容。 Safari 在iPad的iOS 13系统中,Safari会在必要时自动加载桌面版本的网站。苹果正在测试一款Safari下载管理器,用户可以在一个地方下载。 照片实况更新 苹果计划将照片实况的视频长度延长至原来的两倍,也就是从3秒变成6秒。 嗨Siri ”嗨Siri“将会更好的过滤像是笑声和孩子哭声这种环境音。 屏幕使用时间 屏幕使用时间功能将增加一个新功能用于限制孩子对手机的使用,可以设定一个可以和不可以玩的时间。 界面的更新 当启动多任务处理面板并关闭应用程序时,将会有一个新的动画,iPad的主屏幕将会发生一些变化。 文件应用的修改 虽然关于一个改版后的文件应用程序会是什么样子的细节很少,但据说苹果正在为它开发新的功能,比如更好的第三方软件集成。 新版的Emojis 新表情符号将不会在iOS 13发布时出现,但在2019年晚些时候的iOS 13更新中,苹果将会引入他们。Unicode联盟已经确定了加入表情符号标准的字符,其中包括火鸟、水獭、华夫饼、树懒、白心、牵手的人、冰块、潜水器、猩猩、果汁盒、沙拉三明治等等。辅助功能 一个更全面的辅助功能菜单将出现在设置应用程序的主页上,其中包括改进的助听器支持等。 iPad Pro支持鼠标 有传言称,苹果公司可能计划在iPad Pro上添加USB鼠标支持,作为一项辅助功能,而不需要使用适配器。 据报道,iPad Pro的USB-C接口将允许你插入USB鼠标,作为那些无法使用触摸屏的用户的另一种输入方式。目前还不清楚该功能何时会实现,但它最早可能在iOS 13中实现。 跟开发者相关的变化 对于开发者来说,iOS 13将为媒体播放、搜索、语音呼叫、活动票务、消息附件、航班等功能带来改进的Siri集成。 其他面向开发者的功能还包括ARKit的改进,它为增强现实提供了一个全新的快速框架,以及一个配套的应用程序,让开发者可以在视觉上创建增强现实体验。ARKit还将支持游戏控制器和立体声AR耳机。 新框架将包括扩展使用Taptic引擎,对第三方应用程序的文档扫描支持,以及无需使用照片应用程序即可从外部设备捕捉照片的功能。 NFC将得到改进,开发人员将能够在他们的应用程序中添加对扩展的NFC格式的支持,并且还将添加CoreML的更新版本。
iOS开发月报#10|201904
- 30 Apr, 2019
这里记录过去一个月,我看到的值得分享的内容,包含但不限于iOS知识,每个月的最后一天发布。 欢迎推荐内容,可以前往zhangferry/iOSMonthlyReport提交issue。Tips 关于分享中的一些问题 微博多媒体内容的缩略图,即thumbnialData的大小应小于32K。否则会导致分享失败 微博分享的AppStore下载链接无法打开,这是因为微博屏蔽了指向AppStore的链接。一个可行的做法的将微博的下载链接增加一个引导页,提醒用户通过Safari打开。微信多媒体缩略图不能超过64K。 微信分享从6.7.2之后无法获知是否真的分享成功。这个是官方的调整,旨在减少“强制分享至不同群”等滥用分享能力。facebook 关于facebook分享的采坑可以查看这篇文章,基本也都是我遇到的问题。iOS Facebook 分享中的坑 Twitter 相同内容分享两次之后再分享将失败。 SWIFT_VERSION '5.0' is unsupported, supported versions are: 3.0, 4.0, 4.2. (in target 'SwiftyJSON’) 这个问题是多人写作开发引起的,有一个同事将SwiftJSON的版本升级至4.3.0,使其支持Swift5.0,但是Swift5.0跟Swift4.0+不兼容,导致出现上面的错误提示。 修复方式,手动指定SwiftJSON版本,使其低于或者等于4.2.0。 pod 'SwiftyJSON', '~> 4.2.0'Encountered an unknown error (Unable to find a specification for FrameworkA depended upon by FrameworkB FrameworkA和FrameworkB都是私有的Cocoapods库,在制作FrameworkA时引用了FrameworkB,如果执行pod spec lint就会出现如上的提示,到时lint无法通过。 这是因为lint在对引用库验证时,默认只验证官网的仓库,我们需要手动添加验证源才能通过,方法是: pod spec lint --sources=git@bitbucket.org:company/privateRepo.git,https://github.com/CocoaPods/Specs --allow-warningsTabbar的初始化会立即调用viewDidLoad方法 正常当我们初始一个UIViewController的时候,总是先执行init方法,执行完之后才会调用viewDidLoad方法。但是如果是UITabbarViewController的初始化则不同,它会在执行init方法的时候立即调用viewDidLoad。 来自Stack Overflow的解释: UITabBarControllers call loadView inside [super init] method, which causes the call to viewDidLoad. So the viewDidLoad method will be called before init has finished its job.If you have some thing to setup in viewDidLoad you should perhaps do it inside init method after the call to [super init].AppleScript打印换行 当我使用AppleScript编辑一段脚本时,有一个需求是打印一段换行的文本,试了很多方案,包括\n,\r, ASCII码等都不行,脚本执行时会忽略\符合自动换行,分开执行输入内容。 最后在不断尝试中找到了一种方案,直接敲出换行,如下,在变量_input后拼接一个换行符: tell note1 to append text "[*]" & _input & " "由于第三方SDK使用了用于定位的功能,导致收到苹果的隐私警告邮件后来定位的到的原因是Facebook相关的几个库均使用了CoreLocation,也就是说Facebook有偷偷使用用户地理位置权限的嫌疑。当然联系他们提供不带此功能的SDK也不显示,后来在react的issuse中也发现了对于此问题的讨论。#20879 解决方案如下,即增加对应的隐私条款选项:违反Guideline2.3.1被打回在一次小版本提交时,遇到了这个违反Guideline2.3.1被打回的问题,也是第一次遇到。原因描述是含有模糊代码,选择器错误或者误导审核的功能。实际排查中我们并未猜想到哪里可能导致这些问题。 后来通过邮件跟审核人员沟通,得到如下回复:问题又好了?猜想可能是苹果审核的问题,他们也是会犯错误的。 后来在网上查过这种问题的处理情况,从资料时间来看,可以确定这个是最近一年才出现过的问题。处理方法是,首先是应该跟审核人员联系,让他们告知是哪里的问题;然后再根据情况进行修改。 推荐阅读 作为面试官,哪类 iOS 开发更容易被你青睐? 来自老司机团队的文章,正在找工作的你肯定能从中有所收获。 再贴一个我司的招聘信息,有意愿的小伙伴赶紧简历投过来。 用户端智能的应用实践 bang写的关于应用功能优化的几个点,根据用户的特征推荐金额,根据用户使用习惯调整push的时间,有些方案不一定能够为我们所用但是解决问题的思路还是挺不错的。 谈Linux,Windows,和Mac 王垠的博客其实已经被删除了,这个是网络留存的快照文件。牛B的人总是能留下牛B的话,这是王垠关于Linux、Windows和Mac的看法。 Swift语言的设计错误 let shoppingList = ["Eggs", "Milk"] //这么写应不应该报错? shoppingList[0] = "Salad"关于以上问题的讨论。当前Swift是不允许对let生命的数字修改内部元素的。在王垠看来这是一个低级的错误,具体为什么这么定义“不对”,可以看文章讨论。 音视频 Github iOS-Source-Probe iOS源码探求系列,是iOS相关源码的分析合集。 AutoInch 优雅的iPhone全尺寸/等比例精准适配工具,可以通过设置一个屏幕的尺寸值,而自动配置其他尺寸值。支持代码和xib两种方式。SwiftTips 作者总结的Swift使用中的一些小技巧,和喵神的《100 个 Swift 必备 tips》有异曲同工之妙。作为一名刚入门的Swifter,这两个资料都是非常推荐看一看的。 ZFPlayer 非常全面的一款iOS播放器,支持AVPlayer和ijkplayer两种播放模块。项目包含主流的视频播放场景,抖音、微博短视频等样式。如果是对视频功能有需求的小伙伴非常建议看一看。
一则iOS招聘信息
今天放出一波我司的招聘信息吧,这里只说了iOS的招聘要求。其实还有Android + Unity + 游戏测试等一大波岗位虚位以待,如果有意愿的小伙伴可以私信我哦。公司介绍 乐信圣文(Learnings)是一家全球领先的移动出海应用开发商,致力于未全球用户提供卓越的移动应用。公司成立于2016年2月份,是行业成长最快的企业之一,是Google,Facebook和Apple的全球战略合作伙伴。 截至2018年11月,公司产品在全球移动端月活用户超过2000万,累计用户超过3亿;其中70来自欧美地区。 团队规模:150人 公司地址:海淀区东升科技园(地铁8号线西小口站) 明星产品-Peace冥想【App Store 2018年11月编辑最爱】 【App Stroe 首页《生活解决方案》专题推荐】 【App Store 健康健美分类编辑推荐】 "Peace"冥想是一款你的负面情绪排解神器,随时随地帮你在短短5分钟内与自己和解。助眠,减压,它是都市快生活下的一剂良药。 Peace上线不到一年,就表现出了巨大的成长潜力,它的未来将会更加灿烂。 任职要求职位: iOS中高级开发 薪资: 1530k + 年终奖(16个月薪资) 任职要求:我们期望你有3年以上开发经验,当然如果你能力够强,这条可以忽略。 对数据结构、面向对象、设计模式有一定的认识 具备独立开发一款产品的能力 乐观、主动、具备团队合作精神福利待遇双休(非996!) 一日三餐 运动健身(室内台球、乒乓球每周篮球训练) 顶配Macbook Pro + 升降办公桌 零食下午茶 节日福利 不打卡 ...欢迎你的加入 有没有心动呢,心动不如行动,有兴趣,有需求的小伙伴赶紧把简历砸过来吧! 我们有各个技术栈的大牛,可以让你进步更快;我们敢于尝试新技术,一年前就已经使用纯Swift开发项目;我们重视人才,只要你够牛,薪资+年终奖拿到你手软。 来到公司之后,我们将一起经营维护Peace这颗新星产品,一起见证它的成长吧。
iOS开发月报#9|201903
- 01 Apr, 2019
这里记录过去一个月,我看到的值得分享的内容,包含但不限于iOS知识,每个月的最后一天发布。 欢迎推荐内容,可以前往zhangferry/iOSMonthlyReport提交issue。Tips Spine + SpriteKit 项目中需要引入一些实物动画,每个动画之间有不同形态的切换,考虑过gif, mp4, AE + lottie, Spine + SpriteKit。 最后确定使用Spine做动画效果,用SpriteKit处理动画。Spin并没有官方支持SpirteKit的库,但有一个做的比较好的第三方库maxgribov/Spine,支持Swift4.1。 该库支持Bones, Slots, Skins等常用的动画要素,通过Spine导出的json文件和动画素材做出各种动画效果,是仅有的近期还在维护的支持SpriteKit的Spine运行时库。但它也存在一个问题,还不支持Mesh Animation(网格动画)。如果所需的动画效果不需要网格的话,非常推荐使用这个库。 而我们所需的动画效果又必须用到网格动画,思考再三考虑决定放弃使用这个库,使用SKTextureAtlas(纹理集) + 逐帧动画来实现特殊的动画效果。虽然输出的还是png序列,但是SpriteKit对纹理集有足够的优化,Xcode会在打包时把.atlas文件夹中的所有图片做成一张合图,然后生成一个plist文件描述每个小图片的位置信息,所以包的大小和渲染成本都会大大降低。 快速创建转场样式 说到自定义转场我们可能会直接想到UIViewControllerAnimatedTransitioning,结合这个类我们可以实现多种多样的订阅样式。但是使用这种方式做转场,我们需要引入很多代码。有一种简单的实现转场的方式是通过CATransitionAn object that provides an animated transition between a layer's states.通过文档的介绍我们知道,这个类就是用来做转场的,只不过支持的样式有限,但如果正好满足你需要的话,推荐使用这种方式来实现转场。 我们来实现一个present的渐变效果: //创建transition对象 let transition = CATransition() transition.duration = 0.5 //动画样式 //type: .fade, .moveIn, .push, .reveal transition.type = .fade //动画出现方位 //subtype: .fromRight, .fromLeft, .fromTop, .fromBottem //transition.subtype = .fromRight transition.timingFunction = CAMediaTimingFunction(name: .easeIn)self.view.window?.layer.add(transition, forKey: "present") self.present(targetVc, animated: false, completion: completion)如果是做push的渐变,我们只需要改变最后的控制动画的代码: //父容器为UINavigationController self.view.layer.add(transition, forKey: "push") self.present(targetVc, animated: false, completion: completion)//父容器为UITabbarController self.tabBarController?.view.layer.add(transition, forKey: "push") self.present(targetVc, animated: false, completion: completion)什么是UserAgent User-Agent 首部包含了一个特征字符串,用来让网络协议的对端来识别发起请求的用户代理软件的应用类型、操作系统、软件开发商以及版本号,然后前端的展示就可以根据这些信息进行针对性的优化。 我们打开Chrome浏览器,生成一个请求,然后用Charles抓包,可以看到对应的User-Agent user-agent Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.28 Safari/537.36通过UA我们可以得到以下信息:信息项 内容浏览器名称 Chrome浏览器版本号 70.4.3729.28渲染引擎 WebKit 537.36操作系统 Mac OS 10.13.6Apple Configurator 2 出现 Unauthorized Error 注销账号,再次登录 Command PhaseScriptExecution failed with a nonzero exit code 运行一个项目时遇到了这个bug提示,一直编译不过去,这其实是一个Xcode10引起的bug。 解决方案: 在Xcode菜单栏选择File -> Workspace Setting -> Build System 选择Legacy Build System 重新运行即可。 参考:踩坑Xcode 10之New Build System 斐波那契函数 斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、…… 这个是我们公司技术面试的必问题目,也是筛掉人数最多的一个问题。有一部分同学会使用数组尝试解决这个问题,但这会把问题复杂度升级,还有些可能根本没有思路。但其实这个问题不复杂的,用到递归可以很快的解决。斐波那契函数的数学表达是: F(1)=1 F(2)=1 F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)用Swift实现就是: func fibonacci(n: Int) -> Int { if n == 1 || n == 2 { return 1 } else { return fibonacci(n: n - 1) + fibonacci(n: n - 2) } }推荐阅读 SpriteKit Tutorial for Beginners raywenderlich上介绍SpriteKit入门的一篇教程,通过这篇文章你可以实现一个忍者击杀怪物的小游戏,理解SpriteKit框架里常用的几种游戏元素。不得不说这个教程做的是真的棒👍 开发小知识 该文章主要整理一些小知识点,主要涉及 iOS 以及计算基础相关知识点,某些知识点暂时只有标题,后续会持续更新。笔者最近一段时间面试过程中发现一些普遍现象,对于一些很不起眼的问题,很多开发者都只停留在知道、听说过的层面,但是一旦问 是什么 和 为什么 ,很多应试者回答的并不理想。 大家可以对着这篇文章查找自己的知识盲区。 Swift 5 终于来了,快来看看有什么更新!! Xcode10.2 已经发布,是时候开始使用 Swift5 了,可以提前看下老司机周报总结的Swift5 更新内容,对适配工作做好准备。 苹果开了一场没有任何硬件的发布会 3 月 26 日凌晨 1 点,苹果在 Apple Park 新总部的乔布斯剧院召开了春季特别活动。 在活动现场,苹果发布了:新闻服务 Apple News+ 可以返现的 Apple Card 游戏服务 Apple Arcade 全新的 Apple TV App 服务 Apple TV+ 原创视频服务全部是软件和服务,没有新硬件的出现——这或许意味着,苹果正在寻找下一个十年的生长空间。 音视频 辞职环游中国的程序员小 K 大概每个人都有过这种冲动,辞掉工作出去旅行,想去哪就去哪,再也不用赶需求修 bug 通宵加班。不过对大多数人来说,也就止步于“想想”,并不会付诸行动。但是我身边有一位朋友,真的做到了这件事:辞职一年环游中国! 这是最近几期ggtalk对我触动最大的一期,同样是做iOS开发的,为什么人家那么优秀🙃 Github 996.ICU 工作996,生病ICU。这段时间的“明星项目”,旨在反抗国内互联网公司形成的每周工作6天,每天工作早9点到晚9点的不良加班风气。截止到3月30号,仅四天时间star已经超11万。 XVim2 XVim2是一个用于Xcode的Vim插件。如果你是一个Vim党,你可以直接在Xcode代码编辑界面使用Vim的各种特性。我是最近开始接触,也在慢慢适应Vim的远离鼠标工作模式。另附送一个安装流程: 1、关闭Xcode 2、钥匙串->证书助理->证书创建 名称:XcodeSigner 身份类型:自签名根证书 证书类型:代码签名 3、重新签署Xcode #需要等待一段时间 sudo codesign -f -s XcodeSigner /Applications/Xcode.app 4、按照官方步骤安装XVim2 文摘 1、培养出在没人监督自己的时候也能高效工作的自我责任感非常重要。你也可以拔这称为是具有一种性格或者具有一种素质,它们都是同一个概念。如果缺乏对自己的责任感,你将永远依赖外部动机来驱使你努力工作。你容易折服于一根胡萝卜的诱惑,也容易屈从于一根大棒的威胁。 --《软技能:代码之外的生存指南》 2、最终,它成为我自己的知识体系中严重的短板。没有花时间去彻底掌握Lambda表达式的工作原理,结果浪费了大把的时间。最后当我下决心花时间去了解Lambda表达式的时候,我只花了几个小时阅读并实践,就领会了这一概念。 --《软技能:代码之外的生存指南》 3、回到从前,在我们刚开始一起生活的时候,我们就决定将我们收入的10%用于风险什一税--实际上我们把这部分收入捐给一家慈善机构,以帮助印度的孤儿。在我们第一次奉献什一税的第二周,我的妻子就得到了加薪,加薪的数额正好是我们当时奉献什一税的数额。我个人认为,我们的成功很大一部分就是因为这种对奉献的承诺,一直恪守到今天。 即使你不信仰任何宗教,我认为这一点也有某种符合逻辑的解释。我认为,你把钱看的越重,你就越难以在理财方面做出明智的、成功的投资选择。自愿把自己收入的固定数额奉献或者捐赠给慈善机构,可以改变你对金钱的看法。这一思想上的转变让你从金钱的所有者变成管理者。 --《软技能:代码之外的生存指南》
关于996ICU的一些看法
这个项目在有7k多star的时候我就看到有人推了,当时也就是看看而已,了解了这个标识的意思,并没有点进去,更没有star。再之后随着越来越多的star和关注量,公众号、社区、论坛,但凡有程序员的角落,大家都在争先传播着“996ICU”这个概念,每一次转发、star都是对996这种工作制的无声抵抗。刚才看了一眼star已经12w+,马上就要超过react,这足以说明忍受着996的程序员是多么大的一个群体。我这里不想跟大部分人一样,痛斥996,歌颂这种行为,我想说些不一样的东西。 从自己经历出发吧,2015年刚出来工作的时候是996,当时我并没有不情愿或者觉得累,因为我很清楚自己就是个菜鸟,996可以让自己有更多的时间用来学习东西,提升技术。这种工作节奏持续了大半年,这段时间的感受也是即充实又满足的。后来公司业绩好转,改成大小周,再几个月后又改成双休。当然最开始享受双休的那几天还是很爽的,后来就慢慢习惯了,996前后我的工作状态并没有什么变化。 再后来进了现在的公司,人事告诉我大小周,那时我已经保持了将近一年的双休节奏,稍微犹豫了那么一会,但考虑到公司不错的发展前景,我还是答应进来了。从双休切换到大小周之后,我才意识到双休是多么爽的一件事。。。真的深刻体会到得到了就习以为常,失去了才懂的珍惜的感受。 在我进来不到半年的时间,公司运行的大小周调整为975,因为晚上7点公司提供晚餐,其实开发这边都是吃完饭休息一会再继续回去工作,大约也是9点走,可以说是995。 得知双休之后,我的学习状态也高涨起来了,规划着两天的自由时间可以做很多事情了,睡懒觉、打游戏、学一些自己感兴趣的东西、跑步。然而实际情况却是开始双休之后的很长一段时间里,我完成度最高的是睡懒觉、打游戏,对于学习,健身基本没怎么执行。有那么几个周末我深深地体会到舒适区只要进入,是很难挣脱出来。周末的早上醒来就感觉自己是被封印在床上了一样,除了刷手机什么都不想做,哪都不想去,早上喝水,中午叫外卖,晚上再来一份外卖,一天结束了。 同时我又是一个反省意识很强的人,在虚度了一个周末之后心里就会非常自责。一个被荒废的周末还不如忙碌一天的工作给我带来的满足感,我甚至产生了想要回到996的状态,以此来约束自律性很差的自己。 再后来也就是最近一段时间,为了达成近两个月的OKR,我“如愿以偿”地又回到了996的模式。因为早有准备,多上一天班并没有让我感觉失去多少自由。公司也考虑大家劳逸结合的情况,搭建了台球,乒乓球,Switch游戏机这种娱乐措施。周六加班,也就是做一些修修补补的任务,中午吃完饭会打一会台球,乒乓球,或者玩一会农药,回来继续工作。有时候会感觉周六加班更像是换了一个地方过周末。 但其实我也不是完全接受996的,它解决了我的时间配比问题,但也会给我引起其他不便。比如我想和女朋友周末出去就近玩一下,北京以内还好,如果想出北京就会时间安排不过来。当然还有其他的不便,要知道休息两天和休息一天可是相差一倍呢。 那回到这个话题,996这个制度是否OK呢? 如果你自律性很强,有很多想法想要实现,也有较好的时间规划,那多一些自己的私人时间是再好不过的了,996对你的确不能让你发挥更大的作用。如果你自律性很差,也没考虑过多出来的时间用来干嘛(玩和睡除外),那我建议你可以先好好想想双休之后你会做什么,双休对你来说生活品质是提高了还是降低了。当然如果你认为自己就是不想加班,只想过平淡恬静的生活,上班对你来说就是为了赚钱,这种想法也没错。因为不是每个程序员都热爱着这份工作,能从中获取到乐趣的,那就换份轻松点的工作呗。我下面讨论的内容对这类人群也是不适用的,你们可以看到这就结束了。 说些不那么中听的话,如果你自律性差,公司的996相当于帮你加了一道屏障,这个时间段它帮你隔开舒适区,迫使你投入到工作中。而我公司也有不少,没有加班任务,时不时也会主动来公司加班的人。那最理想的情况其实就是,公司相信员工,不强制加班,大家想休息了,该休息了就休息,保证把工作效率提上去。员工呢,同时为自己和公司负责,没完成工作,学习新东西,就可以来公司主动加班。 之所以说他理想,是因为很多公司不信任员工,很多员工也没有那种对自己和公司强烈的责任感。所以互联网行业出现这么多的996不是单纯公司的问题,一些程序员也负有一定的责任。 再说一些特殊情况,如果是无良公司,不考虑员工效率问题,各种强制加班,搞996,那我也是坚决反对的。 996ICU项目里有提《劳动法》,标准工时一周最高为48小时,而996是72小时,超标不少;以及超时薪资应为平日工资的150%。这让我想到了大二寒假在电子厂打工的经历,标标准准的按照劳动法来的,基本都是按时薪算的。为什么同样是受劳务合同保护,不同的行业却有着不同的处理方式呢? 我认为一个重要的原因是,电子厂流水线上的那是标准的工人,计时或者计件,可以清清楚楚的搞清楚。而程序员呢,智力劳动,没法准确的衡量一个人的工作价值。你加班两小时,是因为水平问题还是别的原因,如果是自己原因的话,公司需要付钱吗,这个说不清的。所以是否要按照劳动法付给雇员加班费,存在很多不确定因素,这个是不现实也是不合理的。 我们很多人其实心理也明白,去给996ICU点一个star并不会让自己摆脱996现状。但我们还是那样做了,这其中有一点抗争精神,但抗争之后我希望大家能够更清楚的看待996这个问题。 我更希望996这个含义可以适当拓宽一点,包含我们工作及工作以外提升自我的时间。 如果公司要求996,分出一些时间做自己的事情,保证工作学习的82配比。如果不要求996,那就尽可能合理分配时间,保证每周有72小时是投入到这个行业里来的。在这段时间里你可以用来提升自我,或者是听我给你们吹水🙃
iOS开发月报#8|201902
- 28 Feb, 2019
这里记录过去一个月,我看到的值得分享的内容,包含但不限于iOS知识,每个月的最后一天发布。 欢迎推荐内容,可以前往zhangferry/iOSMonthlyReport提交issue。新闻一览 2019年开发人员技能报告 这是HackerRank平台通过对7万多名开发人员做的一项调查,问题从他们认为最有前途的技术到他们在工作中和面试过程中想要的是什么,甚至还有coding过程中听什么音乐最有帮助!一起看下吧。 Tips presentedviewcontroller和presentingViewController 假设从A控制器通过present的方式跳转到了B控制器,那么 A.presentedViewController 就是B控制器; B.presentingViewController 就是A控制器。如果没有对应关系会返回nil 获取类名 //传入实例对象object let typeName = String(describing: type(of: object))隐藏NavigationBar导致手势返回失效 当我们从一个A(NavigationController)界面通过push进入B界面,如果要隐藏B的NavigationBar,那么从B到A的返回手势也会被屏蔽掉。 如果需要加回来我们可以这么做: //1.给需要添加返回手势的ViewController增加UIGestureRecognizerDelegate//2.由当前ViewController接管pop手势 self.navigationController?.interactivePopGestureRecognizer?.delegate = self关于 Decimal 官方对Decimal介绍就一句话A structure representing a base-10 number.表示以10为基数的数的结构体,就是十进制的表示方法。 它可以通过 mantissa * 10 ^ exponent的形式表示任意数值。mantissa(尾数)是最长38位的整数,exponent(指数)是 -128到127的整数。 你也可以把Decimal理解成跟Int, Float, Double一样的数据类型,那有了整形,浮点型数值还会出现Decimal这个类型? 可以看一个例子: let stride: Float = 0.01 var sum: Float = 0 for _ in 1...100 { sum += stride } print(sum)//0.99999934sum的值不是1,这是因为精度的问题,那如果把Float换成Decimal就会完全等于1。 SKProduct的price属性就是Decimal类型 违反5.5条款被打回的经历 在一次有关内购项审核被打回的经历中,遇到了一个新问题,错误描述中提示违反了条款5.5 Developer Code of Conduct。 简单看下这一条Please treat everyone with respect, whether in your responses to App Store reviews, customer support requests, or when communicating with Apple, including your responses in Resolution Center. Do not engage in harassment of any kind, discriminatory practices, intimidation, bullying, and don’t encourage others to engage in any of the above. Customer trust is the cornerstone of the App Store’s success. Apps should never prey on users or attempt to rip-off customers, trick them into making unwanted purchases, force them to share unnecessary data, raise prices in a tricky manner, charge for features or content that are not delivered, or engage in any other manipulative practices within or outside of the app.总结一下就是要尊重用户。 在联系过苹果审核客服之后,被告知是因为在引导过程中弹了两次订阅弹窗(一次订阅,一次freetrial)被拒的。但其实这个流程已经跑了大半年,一直没问题,这次却突然被揪出来了,有点莫名其妙。 可能审核人员认为,这种行为有诱导或者强迫用户付费的嫌疑。由此也可以看出审核人员对项目流程的管理越来越严格了。去掉一个步骤之后再次提交就过审了。 推荐阅读 Swift ABI 稳定对我们到底意味着什么 Swift 社区最近最重大的新闻应该就是 ABI 稳定了。这个话题虽然已经讨论了有一阵子了,但随着 Xcode 10.2 beta 的迭代和 Swift 5 的 release 被提上日程,最终 Swift ABI 稳定能做到什么程度,我们开发者能做些什么,需要做些什么,就变成了一个重要的话题。 一起来看下使用 Swift 5 会对我们的项目有什么影响以及怎么处理。 Void SwiftGG翻译组翻译的Mattt一篇介绍Void的文章。 Void是什么,它和nil有什么区别,这个我们经常用但不一定真的理解的东西,一起通过这篇文章看下吧。 love2.io 程序员的自我修养,作为一个合格的程序员需要具备哪些知识呢? 数据结构、算法、代码架构、项目管理工具、网络知识等?可能还不够,还需要懂点设计、学着当一个合格的PM、学会团队合作、学会学习、提升效率等等;够了吗?你还需要了解一下创业流程、关注自己的身体健康。。。 太多了,不列举了,因为这里都有。 为什么数字下标要从 0 开始而不是 1? 大家都知道数字下标是从0开始的,那为什么是0而不是1呢?说实话在看这篇文章之前我也不能很清楚的说明白,如果你也有这种疑惑就看看这个文章吧。 什么叫有知识 选自罗胖60秒: 1、话说什么叫做有知识?最近我看万维钢老师有一个有趣的定义。他说,考试得了高分,不叫有知识。茶余饭后能高谈阔论,这也不叫有知识。 你发现没有,这些场合下,知识虽然有用,但是这些知识都不太牵扯到具体的得失,所以只是智力游戏。 2、那什么才叫有知识呢?万维钢老师说,只有当局势不明朗,没有人告诉你该怎么办,而错误的判断又会导致一些不良后果,这个时候,如果你因为有知识而敢于拿一个主意,你才算是真有知识。 3、请注意,这不是在说,实用的知识才是知识,而是在说,只有当知识能够帮助你做实际决策的时候,它才是你的知识。 4、这种决策当然可大可小,大到你选择什么样的职业,选择听医生的哪个建议;小到帮你选择看哪一部电影,或者是为孩子挑选买哪一本书。这都是具体的决策。 音视频 苏俄科幻简史:直指星海的冒险时代 最近在机核(gamecores)上听到的一段讲述苏俄科幻的音频,可以当故事听,也可以当一段历史去听,非常有意思。 对于很多人而言,苏俄的科幻文学、电影乃至游戏都是一个比较陌生的概念。事实上从沙俄末期到苏联再到今天的俄罗斯,来自极北之地的斯拉夫人民也曾在科幻这个领域上走了很远很远,取得了惊人的成绩。 Github DoraemonKit 由滴滴团队开源,中文名“哆啦A梦”。 DoraemonKit 是一个功能集合面板,能够让每一个 App 快速接入一些常用的或者你没有实现的一些辅助开发工具、测试效率工具、视觉辅助工具,而且能够完美在 Doraemon 面板中接入你已经实现的与业务紧密耦合的一些非通有的辅助工具,功能强大,接入方便,便于扩展。 git-history 这个项目可以让你在浏览器动态地查看Github库上代码的变动历史。 使用方法非常简单: 1、在对应的文件url中把github.com替换成github.githistory.xyz 2、没有第二步 resume.github.com 这个库可以通过你仓库的活跃事件来生成一份在线简历。使用很简单,直接访问以下网址:https://resume.github.io/?yourusername 需要将yourusername替换成你的用户名,别忘了那个? iOS-Developer-Roadmap 一名iOS开发人员需要具备哪些知识呢?这个库做了比较详尽的总结,以技能树的形式对iOS开发所需知识点,知识面做了整理,我们可以作为参照,查看自己有哪些缺失。文本版本 图片版本(基础版本, 完整版本)
关于消息摘要及对应的Swift实现
- 27 Feb, 2019
开发过程中我们经常会遇到对数据进行一些类似MD5,SHA-256等的处理,那处理的作用是什么以及为什么要这么做,就是今天要讨论的内容。如果你对这些算法的概念已经很熟悉,那就不用继续往下看了,如果你感觉生疏,或者并不能说清楚他们的作用,那就跟着我一起回顾一下吧。从MD5说起 我门从最常见的MD5说起。下面是维基百科对MD5的定义:MD5消息摘要算法(英语:MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。MD5的概念中出现了两个名词:信息摘要算法 + 密码散列函数。 我们再分别来看这两个概念的含义。 信息摘要算法 消息摘要算法消息摘要是把任意长度的输入揉和而产生长度固定的伪随机输入的算法。 它具有以下特点:无论输入的消息有多长,计算出来的消息摘要的长度总是固定的。例如应用MD5算法摘要的消息有128个比特位,一般认为,摘要的最终输出越长,该摘要算法就越安全。变长输入,定长输出。一般地,只要输入的消息不同,对其进行摘要以后产生的摘要消息也必不相同;但相同的输入必会产生相同的输出。消息摘要函数是单向函数,即只能进行正向的信息摘要,而无法从摘要中恢复出任何的消息,甚至根本就找不到任何与原信息相关的信息。好的摘要算法,没有人能从中找到“碰撞”,即无法找到两条不同的消息,使它们的摘要相同。从理论上来说,不管使用什么样的摘要算法,必然存在2个不同的消息,对应同样的摘要。因为输入是一个无穷集合,而输出是一个有限集合。但是实际上,很难或者说根本不可能人为的造出具有同样摘要的2个不同消息。 密码散列函数: 密码散列函数(Cryptographic hash function),又译为加密散列函数,是散列函数的一种。它被认为是一种单向函数,也就是说极其难以由散列函数输出的结果,回推输入的数据是什么。 概念梳理 这里比较重要的就是理解信息摘要,它概念就是把一段数据压缩成固定长度的数据,但是我们不能通过压缩后的数据反推原始数据。 有一种应用场景比较能说明问题: 我们在登录的时候,对用户密码进行MD5计算之后再传到服务器,之后数据库显示的用户信息就是MD5之后的16字节字符串。管理员即使能访问用户数据库但他并不能由MD5处理之后的数据得知原始密码。 而客户端这边却可以在输入密码之后,进行MD5计算然后与服务器密码进行比较。 SHA家族 安全散列算法(英语:Secure Hash Algorithm,缩写为SHA)是一个密码散列函数家族,是FIPS所认证的安全散列算法。能计算出一个数字消息所对应到的,长度固定的字符串(又称消息摘要)的算法。 SHA的出现就是解决MD5不安全的问题,它是一种比MD5更安全的密码散列函数。 SHA家族的算法,由美国国家安全局(NSA)所设计,并由美国国家标准与技术研究院(NIST)发布,是美国的政府标准,其分别是:名称 简介 应用范围 发布时间SHA-0 SHA-1前身 - 1993SHA-1 曾被视为MD5的后继者,但SHA-1的安全性在2000年以后已经不被大多数的加密场景所接受。 广泛 1995SHA-2 包括SHA-224/SHA-256/SHA-384/SHA-512,目前并未被破解 广泛 2001SHA-3 比SHA-2更安全的散列函数,并无替代SHA-2的计划 较小 2015散列函数的安全性 上面说到了MD5是不安全的,不安全意味着可以找到MD5的“碰撞”,就是两个文件可以产生相同的“指纹”。这意味着,当你在网络上使用电子签名签署一份合同后,还可能找到另外一份具有相同签名但内容迥异的合同,这样两份合同的真伪性便无从辨别。 2004年8月17日,在CRYPTO 2004的Rump会议上,王小云,冯登国、来学嘉,和于红波宣布了攻击MD5、SHA-0 和其他杂凑函数的初步结果。他们攻击SHA-0的计算复杂度是2的40次方,这意味着他们的攻击成果比Joux还有其他人所做的更好。 2005年二月,王小云和殷益群、于红波再度发表了对SHA-0破密的算法,可在2的39次方的计算复杂度内就找到碰撞。 2009年,中国科学院的谢涛和冯登国仅用了2的20.96次方的碰撞算法复杂度,破解了MD5的碰撞抵抗,该攻击在普通计算机上运行只需要数秒钟。 2017年荷兰密码学研究小组CWI和Google正式宣布攻破了SHA-1。 摘要的Swift实现 Swift有一个比较成熟的专门处理摘要,加密等安全相关的框架CryptoSwift。 以下是Swift对摘要算法的原生实现,需要引入CommonCrypto框架,这个已经内置到Xcode里面了,列出了对String和Data的摘要扩展。 import Foundation import CommonCryptoenum CryptoAlgorithm { case MD5, SHA1, SHA224, SHA256, SHA384, SHA512 var digestLength: Int { var result: Int32 = 0 switch self { case .MD5: result = CC_MD5_DIGEST_LENGTH case .SHA1: result = CC_SHA1_DIGEST_LENGTH case .SHA224: result = CC_SHA224_DIGEST_LENGTH case .SHA256: result = CC_SHA256_DIGEST_LENGTH case .SHA384: result = CC_SHA384_DIGEST_LENGTH case .SHA512: result = CC_SHA512_DIGEST_LENGTH } return Int(result) } }extension String { var md5: String { return digest(string: self, algorithm: .MD5) } var sha1: String { return digest(string: self, algorithm: .SHA1) } var sha224: String { return digest(string: self, algorithm: .SHA224) } var sha256: String { return digest(string: self, algorithm: .SHA256) } var sha384: String { return digest(string: self, algorithm: .SHA384) } var sha512: String { return digest(string: self, algorithm: .SHA512) } func digest(string: String, algorithm: CryptoAlgorithm) -> String { var result: [CUnsignedChar] let digestLength = Int(algorithm.digestLength) if let cdata = string.cString(using: String.Encoding.utf8) { result = Array(repeating: 0, count: digestLength) switch algorithm { case .MD5: CC_MD5(cdata, CC_LONG(cdata.count-1), &result) case .SHA1: CC_SHA1(cdata, CC_LONG(cdata.count-1), &result) case .SHA224: CC_SHA224(cdata, CC_LONG(cdata.count-1), &result) case .SHA256: CC_SHA256(cdata, CC_LONG(cdata.count-1), &result) case .SHA384: CC_SHA384(cdata, CC_LONG(cdata.count-1), &result) case .SHA512: CC_SHA512(cdata, CC_LONG(cdata.count-1), &result) } } else { fatalError("Nil returned when processing input strings as UTF8") } return (0..<digestLength).reduce("") { $0 + String(format: "%02hhx", result[$1])} } }extension Data { var md5: String { return digest(data: self, algorithm: .MD5) } var sha1: String { return digest(data: self, algorithm: .SHA1) } var sha224: String { return digest(data: self, algorithm: .SHA224) } var sha256: String { return digest(data: self, algorithm: .SHA256) } var sha384: String { return digest(data: self, algorithm: .SHA384) } var sha512: String { return digest(data: self, algorithm: .SHA512) } func digest(data: Data, algorithm: CryptoAlgorithm) -> String { var result: [CUnsignedChar] let digestLength = Int(algorithm.digestLength) let pdata = (data as NSData).bytes result = Array(repeating: 0, count: digestLength) switch algorithm { case .MD5: CC_MD5(pdata, CC_LONG(data.count), &result) case .SHA1: CC_SHA1(pdata, CC_LONG(data.count), &result) case .SHA224: CC_SHA224(pdata, CC_LONG(data.count), &result) case .SHA256: CC_SHA256(pdata, CC_LONG(data.count), &result) case .SHA384: CC_SHA384(pdata, CC_LONG(data.count), &result) case .SHA512: CC_SHA512(pdata, CC_LONG(data.count), &result) } return (0..<digestLength).reduce("") { $0 + String(format: "%02hhx", result[$1])} } }
艰难的图床优化方案(MWeb+PicGo+Github)
博客写作优化 写博客就肯定会遇到插入图片的情况,我之前的做法是先在Boostnote这种Markdown编辑器里面写个草稿,如果有图片就附上图片,然后再粘到简书的web端。当然图片也需要另拖,会生成一个简书的图片链接,相当于把简书作为图床,然后我再把简书排好版的文章发布到自己的博客。。。当然是很麻烦的方式,这种写法持续了大半年之后,开始寻找更高效的写博客的方式。首先客户端,最终选了MWeb。然后是要解决麻烦的图床问题,因为MWeb内嵌了七牛云图传服务,所以申请了七牛云账号,用了一段时间感觉真是如丝般顺滑。 图床历险记 直到我收到了这个:在七牛云上申请的只是测试域名服务,只有一个月的有效期。如果想继续使用就需要绑定自己的域名,而且这个域名还需要备案。 于是我在腾讯云上注册了两年的zhangferry.com这个域名。填写资料时,我看到系统提示域名要进行备案,备案过程中又发现,要有绑定腾讯云服务才能进行备案。??我只是要域名并不需要云服务啊。隐约感觉好像被下套了,需要解决的问题一个套一个。我还怀着侥幸的心理看了下腾讯云服务的价格,用不起用不起。。。 看到提示说如果不备案域名,三个月后会被回收,有点慌。询问客服之后得知,服务器在国内且没有备案的情况下才会被回收,因为我的博客是搭在Github上的,所以可以不备案,松了一口气,域名保住了。但是,不备案七牛云又不干了,what f。。。 纠结一段时间之后我只能放弃七牛云,寻找其他的图床服务,知乎上有一个总结比较全的文章盘点一下免费好用的图床。原来是有很多免费服务的,感觉找到了解决方案。但是,又是一个但是,图床这个东西,免费就意味着不稳定,万一哪天挂了,图片就都丢了。最后文末有总结:如果打算长期稳定使用请优先选择又拍云或者七牛云,如果是存储并不重要的图片可以使用免费不限大小的SM.MS图床。 再结合我对图床的需求:免费+稳定。SM.MS好像是最接近的方案了,虽然SM.MS知名度不算低,但是我还是有点担心它的稳定性。 再继续找,找到了这个PicGo,一个图床管理工具,顺着PicGo(v2.0.3)我找到了Github作为图床这一方案。Github,对啊,我直接就确认了这种方案。虽然有人说Github图片国内访问会慢一些,但是它肯定能保证数据的稳定性啊。这样图床工具和图床服务都找好了,喜大普奔! Github图床配置 说了很多废话,终于到了这个理想图床的配置阶段了,比较简单。 1、建一个仓库 用于存需要上传的图片。这个仓库最好是public的,因为private的仓库,图片链接会带token,这个token又存在过期的问题。 2、获取授权token 在Github的Developer setting界面生一个token。记得保存,因为它只会显示一次。3、配置PicGo 根据图示:4、使用 使用也很方便,PicGo有两种方式: 屏幕截图 截取图片之后,图片会出现在PicGo的待上传列表里面。我们手动点一下会触发上传,上传成功之后,剪贴板会有对应markdown格式的图片文本,直接粘贴可以使用。 固定图片 我们需要将需要使用的图片拖到PicGo的客户端内手动上传,上传成功之后,会自动复制成markdown文本,可以直接粘贴使用。 终于搞定了,撒花!
iOS开发月报#7|201901
- 31 Jan, 2019
这里记录过去一个月,我看到的值得分享的内容,包含但不限于iOS知识,每个月的最后一天发布。 欢迎推荐内容,可以前往zhangferry/iOSMonthlyReport提交issue。 另外,马上就要过年了,提前祝大家春节快乐!新闻一览 Github宣布私有库免费 Github宣布提供免费且不限量的私有仓库服务,如果协作者超过3个仍需购买付费服务。 喜大普奔啊,得知消息之后,我赶紧就建立一个私有仓库,放一些不想让你们看到的东西🙃 Swift 5 Release Notes for Xcode 10.2 beta Swift5 版本终于发布了,最低可以使用Xcode10.2beta 查看。Swift5 带来了ABI稳定,届时系统将自带Swift动态库,由Swift编译的项目将不会自动把运行库打到包里。 Swift5 具体还有哪些特性,可以参考知识小集团队的翻译版本 Tips Dictionary的merge操作 Dictionary的merge可能平常用的不多,我是在一次bug排查的过程中,发现自己对merge操作理解也有些偏差。这里做个简单梳理。 看其中一种情况: var dictionary = ["a": 1, "b": 2]// Keeping existing value for key "a": dictionary.merge(["a": 3, "c": 4]) { (current, _) in current } // ["b": 2, "a": 1, "c": 4]// Taking the new value for key "a": dictionary.merge(["a": 5, "d": 6]) { (_, new) in new } // ["b": 2, "a": 5, "c": 4, "d": 6]注意尾随闭包中有两个参数,返回第一个参数,表示如果merge操作有重复key值,将保留merge前的value不变。如果闭包返回第二个参数,如果merge有重复key,将更新至最新值。 Dictionary向关的merge函数一共有四个,使用类似: public mutating func merge<S>(_ other: S, uniquingKeysWith combine: ([Key : Value].Value, [Key : Value].Value) throws -> [Key : Value].Value) rethrows where S : Sequence, S.Element == (Key, Value) //上面示例对应方法 public mutating func merge(_ other: [[Key : Value].Key : [Key : Value].Value], uniquingKeysWith combine: ([Key : Value].Value, [Key : Value].Value) throws -> [Key : Value].Value) rethrowspublic func merging<S>(_ other: S, uniquingKeysWith combine: ([Key : Value].Value, [Key : Value].Value) throws -> [Key : Value].Value) rethrows -> [[Key : Value].Key : [Key : Value].Value] where S : Sequence, S.Element == (Key, Value)public func merging(_ other: [[Key : Value].Key : [Key : Value].Value], uniquingKeysWith combine: ([Key : Value].Value, [Key : Value].Value) throws -> [Key : Value].Value) rethrows -> [[Key : Value].Key : [Key : Value].Value]一些Git操作 删除子目录git 当一个含git的目录包含了其他含git目录时,外部git是不能将内部git纳入版本管理的。有一种解决方案是,移除内部的git仓库。 1、删除内部git cd 需要移除的git目录 rm -rf .git # 或者显示隐藏目录手动删除.git文件夹此时虽然已经删除了.git但是原目录还存在缓存,无法添加到外部git中。 2、删除缓存内容 git rm -r --cached nextgit pull强制覆盖本地文件 当我们需要将某一分支,跟远程对应分支保持一致时,可以做如下操作: git fetch git reset --hard origin/branch_name git pullfatal:refusing to merge unrelated histories 当我们为一个项目关联了一个远程仓库,执行pull的操作时出现 * branch master -> FETCH_HEAD * [new branch] master -> origin/master fatal: refusing to merge unrelated histories这是因为git从2.9.0版本开始,预设行为不允许合并没有共同祖先的分支。如果你非要合并,需要加上--allow-unrelated-histories参数才行: git pull origin master --allow-unrelated-histories几个提高效率的快捷键 最近在熟练使用了几个快捷键后,明显感觉开发效率提高了不少,这里总结了几个常用但不是非常常用的快捷键(Command + C 和 Command V等)。另外Command(或 Cmd)⌘ Shift ⇧ Option(或 Alt)⌥ Control(或 Ctrl)⌃ Power 电源键Xcode Command + Shift + J 文件目录指向当前文件 Command + Shift + O 文件、对象搜索 Command + Shift + F 全局搜索 Command + shift + , 编辑 scheme Command + Control + Left/Right 浏览历史切换 Command + Option + Left/Right 折叠、展开当前代码段 Command + Option + C 显示Commit界面 系统操作 Command + Option + Power 睡眠状态 Power 1.5s睡眠状态 Command + Control + Power 强制Mac重启 适用其他程序 Command + , 打开偏好设置 Command + W 关闭最前面的窗口 Command + Option + W 关闭应用所有窗口 Command + Q 推出应用 Command + M 最小化 Command + Shift + [/] 左右切换应用tab 推荐阅读 未来世界的幸存者 今年市场经济不景气,很多互联网公司经历“裁员潮”,其实不光是换联网行业,其他行业也面临同样的问题。那怎么提高自己的核心竞争力,是每一个从事互联网行业的人都应该考虑的。我们不应该只满足于日常开发,要找准自己的定位,提早做职业规划。另外不断尝试拓展自己的知识面,增加自己的影响力,这样才能让自己在不断变化的市场中立于不败之地。 除了阮老师的这本书,还推荐 卓同学的iOS 2019 隆中对 什么是真正的程序员 作者仿照《小王子》中的情节,通过小printf遇见的不同类型的程序员,最后悟出什么才是真正的程序员! 有两个概念比较有意思:达克效应:能力强的人总是低估自己,能力弱的人总是高估自己。 能够为人们解决真正需要解决的问题的程序员,才是真正的程序员Language Server Protocol 来自SwiftGG翻译组的一篇文章。 苹果公司 在 Swift.org 论坛上宣布,正在着手为 Swift 和 C 语言支持 Language Server Protocol(语言服务器协议,LSP)。 那这意味着什么呢?这可能是苹果自 2014 年将 Swift 作为开源软件发布以来,为 Swift 做出的最重要的决定。它意味着我们可以不必使用Xcode去开发Swift项目,而是可以选择像是Visual Studio, Atom这些IDE。Swift将变成一种更加通用的变成语言。 Hashable / Hasher Hash是程序开发过程中很重要的一个概念,它可以让我们查找集合特定元素的时间复杂度由O(n)降低到O(1)。 这篇文章介绍了Swift中关于哈希的实现,哈希冲突的改变,以及Swift4.1之后关于自动化实现Equatable的相关介绍。 Table View 太複雜?利用 MVVM 和 Protocol 就可以為它重構瘦身! TableView是我们常用的系统部件,随着业务逻辑的不断迭代它内部的逻辑可能会越来越复杂,代码量也越来越多,如果不能很好的组织逻辑架构,这里会变得非常臃肿。 作者利用MVVM pattern,加上一点Protocol技巧,来简化dataSource的工作,把UI和逻辑解耦合,并且最大化这个tableView模组的扩展性。 音视频 临近年关,可能很多人会考虑年后换工作的情况,最近几期的ggtalk也在讨论跳槽这个大家比较关注的话题,希望大家收听之后能够有所收获: 狭义跳槽论:面试官,大厂新人和准备起跳的某 C 广义跳槽论:简洁明了的方法论 附带之前的几期关于升职加薪的节目: 聊聊程序员的升职加薪(上) 聊聊程序员的升职加薪(下) 程序员的春节趣事 今天上班路上听了这期节目,全程非常的欢快。我是今天晚上回家的火车,那些相似的春节趣事我也马上要经历一次了,是需要提前准备下怎么应对了🙃。 以下是官方内容介绍: 快过年了,估计大家也没心情工作学习,准备进入放假状态。严肃的话题放到年后,这期我们请到了袁滚滚、张思琦和老朋友莲叔,围绕着春节轻松愉快的聊一期。 Github 本篇GitHub模块,介绍几个Git和GitHub相关的项目。 Git中文教程 来自geeeeeeeeek的高质量Git中文教程,在GitHub已经获得了1w+的start。对git操作还有疑问的同学可以借助这份教程,完整的梳理一下git相关的知识,或者扫一下git盲区。 Git的奇技淫巧 我们在使用git的时候,有时候会被一些具体的问题难住,这篇文章从具体操作入手,介绍一些我们知道但不一定都用过的git指令。 等等,我也想写一份我自己的Git奇淫技巧了,因为git这个东西是真的庞大。 HelloGitHub 这是一个面向编程新手、热爱编程、对开源社区感兴趣人群的项目,内容以月刊的形式更新发布。内容包括:流行项目、入门级项目、让生活变得更美好的工具、书籍、学习心得笔记、企业级项目等,这些开源项目大多都是非常容易上手、很 Cool,能够让你用很短时间感受到编程的魅力和便捷。从而让大家感受到编程的乐趣,动手开始编程。 GitHub漫游指南 介绍你可能在Github上遇到的所有问题,从如何起一个好名字到如何推广项目,GitHub用户分析,每一个环节都非常详细。无论你处于编程的哪个阶段,我认为你都能从这份指南中获取到灵感和帮助。 README 该文件用来测试和展示书写README的各种markdown语法。GitHub的markdown语法在标准的markdown语法基础上做了扩充,称之为GitHub Flavored Markdown。简称GFM,GFM在GitHub上有广泛应用,除了README文件外,issues和wiki均支持markdown语法。
iOS开发月报#6|201812
- 02 Jan, 2019
大事件 这个月的大事件是高通与苹果之间的专利大战。高通分别在中国和德国对苹果公司提起专利诉讼,结果均胜。苹果将在中国禁售iPhoneX及之前的机型。在德国则是全面禁售。加上新品手机的创新力度不够,导致苹果股票持续走低,四个月前,苹果还是首个突破万亿美元市值的上市公司,到现在股价已经下跌了 36%,市值只有 7160 亿美元,几个月内市值损失接近 3000 亿美元。Tips 更换启动图不生效 现象:更换启动图,不生效,即使删除旧图,运行程序启动图仍然是之前的版本。 尝试了删除app重装,clean、重启Xcode,删除DriverData,还试过挪图片位置,改名字,均无效。最后尝试了重启手机->再安装才正常。 问题分析:这个是系统问题,为了加快程序启动的速度系统会把启动图做个缓存,之后每次启动是加载缓存启动图,所以才会出现修改不生效问题。但我们也并不知道系统会什么时候更新启动图,这个就有点尴尬😓 stackoverflow上有关于这个问题的讨论: iOS Keeping old launch screen and app icon after update 有人说这个不影响线上版本,从上线之后的测试来看,大部分用户没有受到影响,但还是出现了部分手机升级却未更换启动图的问题。 因为是系统层面控制,没有太好的解决方案,如果有人遇到这个情况,需要注意一下。 赋值权限 在Mac 系统的终端上修改文件权限使用的是 Linux 中的 chmod 命令: r:可读,表示可以读取内容 w:可写,表示可以编辑内容,但是不可以删除文件 x: 可执行,表示可以执行文件。 r:4 w:2 x:1 rwx=4+2+1=7$ chmod 777 file这行命令的意思就是:文件所有者权限是7,同用户组的权限是7,其他非本地用户组的权限是7。同理可以出现这样的命令 chmod740.barshrc 再讲两个: chmod u=rwx, go=rx .barshrc 这个命令中u表示拥有者,g表示group中的用户,o表示others,和上面的想对应。注意go=rx,这里go是拼接起来的,表示g和o的用户有读和执行的权限。 $ chmod a+w .barshrc表示所有的用户(a表示all)增加写的权限。 App图片瘦身 随着项目版本的不断迭代,会很容易积累越来越多的图片,对这些图片我们有两个主要方面可以优化: 1、删除不用的图片 我们需要借助一个工具LSUnusedResources 它可以检索项目中未使用的图片资源,我们可以指定检索图片的路径,后缀进行过滤。 注意:无法区分png序列的引用。最好手动删除 2、图片压缩 可以通过ImageOptim,对图片进行无损压缩。使用时只需将要压缩的文件拖到程序中即可。 如果图片较多时,可以借助其命令行工具: /Applications/ImageOptim.app/Contents/MacOS/ImageOptim $file_name对应的shell脚本: # 处理文件名出现空格问题 MY_IFS=$IFS IFS=$'\n' for file_name in $(find "$1" -name "*.png" -type f); do echo $file_name /Applications/ImageOptim.app/Contents/MacOS/ImageOptim $file_name done IFS=$MY_IFS该工具还有一个Sketch插件:Plugin for Sketch ,可以在输出icon的时候直接进行压缩优化。 后台音频耗电问题 **功能:**一段助眠音频可以后台播放,播放完成后如果返回app,会跳到对应的完成界面。 **问题:**有用户反馈,听音频睡着了,早上醒来,电量掉了很多(40%)。 调研: 使用Xcode中 Debug Navigator栏的Energy Debug Gauge(需真机)测试。播放完成4分钟左右的后台耗电状态如下:分析可知虽然此时app停止了所有行为,但是生命周期仍处于Background状态,耗电量也还是存在的。暂时认为问题是客户端一直处于Background模式,并未被Suspend,导致一直被系统维护着,所以一直有耗电情况。 再一次查看苹果关于后台模式的文档:When the UIBackgroundModes key contains the audio value, the system’s media frameworks automatically prevent the corresponding app from being suspended when it moves to the background. As long as it is playing audio or video content or recording audio content, the app continues to run in the background. However, if recording or playback stops, the system suspends the app.可知,只要播放器未被销毁,就会一直占用后台模式! 解决方案: 在播放完成时销毁播放器。 检验: 再次观察播放音频完成一段时间之后的后台耗电情况:此时app被挂起,耗电量基本为0,唤醒app,仍能正常工作。问题解决。 推荐阅读 尝试分模块整理推荐阅读的内容 iOS开发过程中的设计模式设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理地运用设计模式可以完美地解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是设计模式能被广泛应用的原因。以上摘自菜鸟教程 Swift中的策略模式 从一个小例子入手,讲解策略模式在swift中的使用。 Swift 中的设计模式 #1 工厂方法与单例方法 Swift 中的设计模式 #2 观察者模式与备忘录模式 Swift 中的设计模式 #3 外观模式与适配器模式 SwiftGG翻译组翻译的设计模式系列文章。 设计模式资料整理 应该是目前总结最全的适合iOS开发的设计模式资料汇总。 iOS逆向iOS应用逆向工程,是指从目标应用的界面及功能表现入手,使用不同的工具和理论知识去分析其实现原理,得出应用的代码结构、整体设计、功能实现、执行流程等,然后利用iOS的系统知识和语言特性,借鉴或修改原有实现流程的技术。推荐两篇掘金上关于iOS逆向的文章: TikTok(抖音国际版)逆向,全球的小姐姐们,我来啦! 逆向 Mac 应用 Bartender 这两篇都是通过具体实例入手,讲解逆向常用的工具和一些逆向的思路,非常适合新手。最好跟着介绍实际操作一番,第一次成功逆向一个项目之后,那种爽歪歪的感觉,你懂的! 另外推荐一位逆向领域的牛人刘培庆(博客,github),了解逆向的同学大多应该都不陌生,iOS方面的逆向优先参照 MonkeyDev。 刘总今年出了新书《iOS应用逆向与安全》,非常适合对逆向有兴趣的同学。 利用脚本提高工作效率脚本语言(英语:Scripting language)是为了缩短传统的“编写、编译、链接、运行”(edit-compile-link-run)过程而创建的计算机编程语言。早期的脚本语言经常被称为批处理语言或工作控制语言。一个脚本通常是解释运行而非编译。脚本语言通常都有简单、易学、易用的特性,目的就是希望能让程序员快速完成程序的编写工作AppleScript入门:探索macOS自动化 文章选自少数派,讲述了:什么是 AppleScript? 我使用 AppleScript 的情境 AppleScript 基础语法 AppleScript suite 之外的解法:模拟键鼠非常清晰详细的AppleScript入门文章。 如何提高工作效率 - 自动化篇 文章选自掘金,串联Shell Script, Apple Script, Automator, Alfred & WorkFlows,告诉你如果灵活应用,达到利用脚本提高工作效率的目的。 我写了一个利用AppleScript和Alfred的WorkFlows在命令行打开指定路径的工具。on run argv tell application "Finder" -- get selection path set pathFile to selection as text set pathFile to get POSIX path of pathFile -- fix space problem in the directory set pathFile to quoted form of pathFile tell application "Terminal" activate tell window 1 do script "cd " & pathFile end tell end tell end tell end run使用方法是:点击选中文件夹,按下热键CMD + T。 音视频 创业那点事:听莲叔和羊叔回忆峥嵘岁月 莲叔和羊叔是 SwiftGG 翻译组的真·大佬。成绩好,学历高,思维缜密,能力很强。听过《升职加薪》那期节目的朋友应该都有印象。作为标准的技术人才,在创业大潮中自然也想试试身手,打拼一番。 和 Cee 聊聊如何拿 Google Offer 翻译组里真是人才辈出啊,前几天 Cee 在群里说了个好消息,他拿到了美国 Google 的 Offer。群友们纷纷表示祝贺并进行了隆重的认哥仪式,将 SwiftGG 诞生以来第一个 GG(哥哥) 称号颁发给我们公认的大哥 Cee。 以上两个音频来自于(ggtalk)[https://talk.swift.gg/],也是我最近上下班路上用来消遣的音频节目,墙裂推荐! 计算机科学速成课 由Carrie Anne Philbin主讲,Carrie Anne Philbin是一名计算机科学教师和作家。她是树莓派基金会的教育主管,也是学校(CAS)多样性和包容组的主席。她为青少年写了电脑书《树莓派历险记》。特意介绍是因为我感觉这个课程因为这个主讲人的个人魅力而趣味十足😄。不管你是不是计算机专业的学生,看过之后应该都能有所收获。 Github 12306ForMac 以前要么开Windows虚拟机,要么使用官方Web,现在可以使用12306ForMac订票助手啦。希望对大家有所帮助! LongestCocoa SoWhatIsTheLongestMethodOrConstantNamesInCocoaFramework? (那么,Cocoa框架中最长的方法名或常量名是什么?)答案是:outputImageProviderFromBufferWithPixelFormat:pixelsWide:pixelsHigh:baseAddress:bytesPerRow:releaseCallback:releaseContext:colorSpace:shouldColorMatch:kCMSampleBufferConduitNotificationParameter_UpcomingOutputPTSRangeMayOverlapQueuedOutputPTSRange这个项目有一定的调侃意味,so what? 我选择swift🤣
2018年总结
- 02 Jan, 2019
写的有点晚了,因为一些事情耽搁,本来想干脆不写了。但有股力量不断催促着自己要写下来,于是就有了这篇文章。今年对我是意义重大的一年,从几个方面说吧,工作,生活和2019年的目标。工作3月底进了新公司,换工作主要原因是在之前的公司遇到了上升瓶颈,对当时的我来说一直待下去也无法看到清晰的未来。从新公司开始,今年变化了很多,也成长了不少。开发语言 首先技术语言从OC转到了Swift,也是首次用Swift进行项目开发,并没有太大障碍。因为是经手新项目,所有也没有太多历史包袱(兼容OC),使用Swift+RxSwift+Clean architecture的进行开发。我对Swift的感觉还是很好的,值得大家花时间去学去适应,毕竟Swfit代表着未来,是苹果推荐语言。 都9102年了,ABI稳定版Swift5.0马上就要来了,没有什么理由不使用它了,不是吗。 开发工具 一直都是使用MacBook Pro+Xcode的环境进行开发,之后使用fastlane进行打包,每次编译打包都将近40分钟。为了提高开发效率,开始考虑黑苹果方案。配置一台i7处理器+16G内存的台式机,4000左右,开始搞黑苹果。 附送一个教程传送门,以下是黑苹果配置:有两个需要注意的地方: 1、显卡无法驱动。 使用免驱显卡。 当时装的时候Mojave版本还没有正式版,想升级时才发现macOS10.14及之后的版本将不再识别NVIDIA的显卡,除非换成AMD显卡,遂放弃升级。 2、网卡问题 可以在天猫购买一个支持黑苹果的网卡,收到货之后向店主要个网卡驱动,之后就能正常wifi上网啦。其他如蓝牙问题因为基本不用,没有搞,也可以使用黑苹果专用外设解决。 博客 2018年共写了12篇播放,看了下日期,第一篇是从7月份开始写的,也就是说这12篇都是后半年写出来的。 关于写博客这件事,之前是一直有种迷茫的状态,一查就懂的不想写,太复杂的又不愿花时间整理。后来慢慢不知怎么地就又找到了点感觉,其实也没有什么规律,只是写的时候不那么被动了。 之前博客都是发的简书,后来觉得简书环境不太好了,不光是对程序员不友好了,简书整体也脱离了那种小清新的感觉,不知道从那个版本开始简书客户端UI突变,整个头条风,就不爱了。转到了掘金,不过之前的文章并没有搬运过来。 如果有留意我最近博客的话,可以看到我正在弄一个iOS月报的东西。这个想法的产生是因为,每隔一段时间都会积攒一些知识点,一些学习感悟,因为是零碎的东西,又不便于整合成一篇博客。后来关注到老司机周报、知识小集、阮一峰的网络日志这几个栏目,收到启发,于是整合出iOS开发月报这种形式。毕竟一个月整理一次对我来说是一件可以做到的事。因为是一个人在做,每个月接触的东西也都会不一样,可能形式会有大大小小的差别。配图不好,语言不通顺也请大家多包涵,如果有什么意见或者好的内容可以给我推荐。我会持续努力的,争取做好这个栏目,给大家带来更多有质量的东西。 生活那些经历 翻了下相册来给过去的一年做下汇总: 在17年的最后一天跟一个团体,参加跨年夜行活动,从奥林匹克公园走到天安门广场,看1月1号的升旗仪式。 1月份在中关村创业大街跟一个高中时的朋友一起去听了哈默的如何成为斜杆青年的讲座。 同样是一月开始追《海贼》,从第一话开始,一直都是看的漫画,到顶上战争知道这将是场精彩的对决,转为动漫,之后就一直是动漫了,目前看到大妈篇打卡二。 一月份还参加了极客公园创新大会,见到了傅盛、王小川、黎万强、李彦宏、罗振宇等一众大佬本人。 3月份裸辞,在家闲置了半个月。 跟女朋友从苹果园开始骑行,去了位于门头沟的京西古道。到了地方,视野所见游客不足十人,人家都快下班了,说要关门不推荐我们进去,于是歇了会就原路返回了。 4月份进了新公司。 第二次参加北京国际半程马拉松,提前一个月开始准备,期间每天早上两公里,周末五公里。成绩是2小时20分钟。 5月份跟女朋友去了青岛,当时天气还比较冷,只记得吃了两次九龙餐厅的饭,真香。 7月份跟女朋友去了秦皇岛的孤独图书馆,她预约了一个月才预约上。到了之后感觉一般,图书馆很小,书也不多,不让拍照。图书馆旁边是阿那亚教堂,也感觉一般吧,不推荐去。 还去了十渡,玩了玻璃栈道和漂流。这个非常推荐。 8月份对我来说是灾难性的,母亲被一场突如其来的疾病夺去了年轻的生命。我无助绝望过,一蹶不振过,也悲观厌世过,好在女朋友陪在身边,给了我足够的陪伴与安慰,才让我重新振作起来,真的非常感谢她。后来我不断的回忆与后悔,为什么没有坚持每年对父母做一个体检,我明明是知道的。 所以我也再次郑重建议大家,每年带父母至少体检一次,多陪陪他们,真的这没那么难。 之后的下半年除了跟我爸,我女朋友分别爬了两次香山,基本就没什么活动了。 关于理财 16年接触基金,17年开始定投,一开始对定投的理解就是分摊风险,比股票稳定,大概率会盈利。18年年初收益率达到15%时,当时定投已经达到我的设定预期。但我却仍坚定的相信,继续持有会获得更大收益。之后经历中美贸易战,股市重挫,之后持续的经济低迷,我都相信会涨回来,直到年末基金亏损达30%。我才意识到,当初自己那盲目的乐观是多么不正确。 后来听了一些大佬对19年经济形势的预测,大概率经济不会转好。所以基金定投这个事也会停止下来,这对我来说是一个教训,以后对理财会更加谨慎。 关于阅读 今年阅读量比较小,就这几本: 《Objective-C高级编程》 《Swift面向协议编程》 《大话设计模式》 《人类群星闪耀时》 《巨人的陨落》 《精进》 《钝感力》 《把生命浪费在美好的事物上》 《你一定爱的欧洲极简史》 正在看的有: 《重构》 《未来简史》 《iOS逆向与安全》 计划要看的是: 《代码的未来》 《学习之道》 《老人与海》 《天才在左疯子在右》 《解忧杂货铺》 《怪诞行为学》 《百年孤独》 《菊与刀》 2019年OKRO1:精进技术栈 KR1:刷50道LeetCode题,写5篇左右博客总结解题思路。 KR2:做3个iOS、mac客户端的逆向。 KR3:翻译5篇Swift相关技术文章。 KR4:学习前端知识,给博客换个面貌。 KR5:开发一款微习惯的APP KR6:博客输出24篇 O2:攒更多的钱 KR1:工资外收入达到1000 KR2:总收入达到买房需要的首付一半 O3:运动健身 KR1:半程马拉松跑到2小时以内 KR2:咕咚累积里程达到500公里
iOS开发月报#5|201811
- 01 Dec, 2018
大事件 11月27号,苹果AppStore下架包括拼多多、搜狗地图、讯飞阅读和悦跑圈在内的共718个App。据称下架原因是触发了AppStroe审核指南的2.5.2条例,即使用了热更新技术。去年6月份,苹果层因热更新下架数万款App。Tips 图片渲染开销 我们知道,解压后的图片是由无数像素数据组成。每个像素点通常包括红、绿、蓝和 alpha 数据,每个值都是 8 位(0–255),因此一个像素通常会占用 4 个字节(32 bit per pixel。少数专业的 app 可能会用更大的空间来表示色深,消耗的内存会相应线性增加)。 下面我们来计算一些通常的图片开销: 普通图片大小,如 500 * 600 * 32bpp = 1MB 跟 iPhone X 屏幕一样大的:1125 * 2436 * 32bpp = 10MB 即刻中允许最大的图片,总像素不超过1500w:15000000 * 32bpp = 57MB 有了大致的概念,以后看到一张图能简单预估,大概会吃掉多少内存。 关于iOS开发证书在钥匙串没有秘钥的问题 当前现象只需要将证书拖到登录,即可生成秘钥APP内切换语言 APP内切换语言有一种比较简单的实现方式,实现步骤是: 1、存储语言状态 2、刷新KeyWindow.rootViewController 3、刷新Bundle 4、切换语言对应的更新操作 实现过程: //1、切换语言,AppleLanguages对应内容是数组,语言符合可以通过Bundle.main.localizations查看 //切换至繁体中文,存储状态 UserDefaults.standard.set(["zh-Hant"], forKey: "AppleLanguages") //2、刷新keyWindow,createRootViewController为创建rootViewController的方法 UIApplication.shared.keyWindow?.rootViewController = Application.shared.createRootViewController()//3、更换bundle //在切换语言的时候,我们需要执行一下,Bundle的exchangeBundle方法 extension Bundle { static func getLocalizedBundle() -> Bundle { if let languages = UserDefaults.standard.object(forKey: "AppleLanguages") as? [String], let lan = languages.first, let path = Bundle.main.path(forResource: lan, ofType: "lproj"), let bundle = Bundle.init(path: path){ return bundle } else { return Bundle.main } } static func exchangeBundle(){ //替换Bundle.main为自定义的BundleLocal object_setClass(Bundle.main, BundleLocal.self) } }class BundleLocal: Bundle { override func localizedString(forKey key: String, value: String?, table tableName: String?) -> String { return Bundle.getLocalizedBundle().localizedString(forKey: key, value: value, table: tableName) } }//4、其他更新操作,如接口,UI界面测试内购时长 当我们测试内购时,需要考虑内购到期的情况。比如我测试购买了一个月的服务,那么购买到期的情况呢,不能傻傻地等一个月时间吧? 其实苹果给出了不同内购时长在Debug情况下对应的测试时间。AVPlayer进入前台自动播放 在我们不进行任何前后台设置的情况下,AVPlayer进入后台,会自动暂停播放。再次回到前台时,播放器会有一定概率自动恢复播放(多大概率未知,测试得出的结论)。 如果这个恢复播放不是我们需要的,或者我们需要避免这种不确定的情况发生,我们可以手动管理这个过程: NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: self, queue: nil) { (notification) in self.player.pause() }NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: self, queue: nil) { (notification) in //根据需要是否需要恢复播放 if shouleRecoverPlayer { self.player.play() } else { self.player.pause() }}iOS12.1关于tabbar的一个bug 触发条件:使用 UITabBarController + UINavigationController 组合 UITabBar带半透明效果,isTranslucent 属性为 true UIViewController的 hidesBottomBarWhenPushed 属性为 true 通过导航栏返回上一页时(导航栏返回按钮 or 屏幕左侧的滑动返回手势)问题现象:经确认是iOS12.1上的一个bug。 解决方案: //MARK: - fix ios12.1 tabbar bug let tabbar = UITabBar.appearance() tabbar.isTranslucent = false详细内容可以参考: 修复 iOS12.1 UITabBar 布局位移bug - ZHFDBK的博客 - CSDN博客 swift - UITabBar items jumping on back navigation on iOS 12.1 - Stack Overflow 推荐内容 重构Swift中的Identifiers 面对越来越多的identifier,改如何优雅地处理? 文中讲了一些UITableViewCell、UICollectionViewCell、segues、UIStoryboards、UIViewController中关于identifiers的重构方法。 The End Is Near for Mobile Apps 移动应用的末日将要来临? Medium科技板块最近比较火的一篇文章,分上下两篇,讲述移动应用的前景和趋势。 Realm学院 这是Realm建立的一个用于学习编程技术的网站,主要方式是视频,内容配有字幕,并且都全部整理成文章。涵盖范围有Realm, Android, iOS, JavaScript, Architecture等。 文章质量非常高! 开发利器 KTVHTTPCache 应该是iOS端目前最好的流媒体缓存框架了。 Realm Realm是一个开源的对象数据库管理系统,最初用于移动(Android/iOS),也可用于Xamarin或React Native等平台,等平台,包括桌面应用(Windows),并获得Apache许可。 对比FMDB,他的使用成本,维护成本都更低。 uTools uTools是一个极简、插件化、跨平台的现代化桌面软件。通过自由选配丰富的插件,打造你得心应手的工具集合。 通过快捷键(默认alt+space)就可以快速呼出这个搜索框。它相当聪明,可以支持输入、拖拽、自动粘贴等作为输入源,相应的插件也早已准备就绪,统一的设计风格和操作方式,助你高效的得到正确的结果。 当你熟悉它后,能够为你节约大量时间,让你可以更加专注的改变世界。 对比Alfred,他的不同就在于免费,使用简单,更符合国人的习惯。
AVPlayer支持的视频格式
发现很多人对视频格式存在一些误解,之前写的一篇文章讲AVPlayer的支持格式也有一些问题,所以这里单独出一篇文章讲一下,希望大家能明白。基本概念 一个在线视频能够播放,大致是经过了如下步骤:可以总结为:拉数据->解协议->解封装->音视频解码->音视频同步->播放。 下面就针对这几个概念一一做下解释: 播放协议 一般点播采用HTTP,而直播的话,大部分还是采用RTMP或者私有协议,原因是延时会比较小,RTMP本身也是为了直播设计的。常见的流媒体协议:简写 全称 推出机构 目前使用领域HLS HTTP Live Streaming 苹果 多应用于苹果RTP 实时传输协议 IETF 范围较广RTCP 实时传输控制协议 IETF 范围较广RTSP 实时串流协议 RealNetworks等 范围较广RTMP 实时消息协议 Adobe 较流行MMS 串流媒体协议 Microsoft 范围较广RTP/RTSP/RTCP的区别RTSP发起/终结流媒体、RTP传输流媒体数据 、RTCP对RTP进行控制,同步。HLS(Http Live Streaming) HLS是苹果推出,实现的基于HTTP的流媒体传输协议: 优点: 1、通过m3u8索引文件可实现针对当前浏览设备的智能选择播放源, 2、通过m3u8索引文件可实现添加备份索引文件,防止服务器崩溃视频播放失败 3、和http视频一样 不需要太多服务器额外配置 缺点: 1、并非真正实时视频,30s左右时间差 2、需要视频处理 3、因为需要请求索引文件(ts视频文件)请求次数相对较多,对服务器负载较大 视频编码 所谓视频编码方式就是指通过特定的压缩技术,将某个视频格式的文件转换成另一种视频格式文件的方式。它是一种为了减少视频体积,同时保证画面质量的压缩技术,常见的视频编码有:名称 推出机构 推出时间 目前使用领域HEVC(H.265) MPEG/ITU-T 2013 研发中H.264 MPEG/ITU-T 2003 各个领域MPEG4 MPEG 2001 不温不火MPEG2 MPEG 1994 数字电视XviD OpenDivX 2002 流行VP9 Google 2013 研发中VP8 Google 2008 不普及VC-1 Microsoft Inc. 2006 微软H.264H.264又叫AVC,是国际标准化组织(ISO)和国际电信联盟(ITU)共同提出的继MPEG4之后的新一代数字视频压缩格式,它集合了H.263和MPEG4的优点,拥有更高的数据压缩比。在同等的图像质量条件下,H.264的数据压缩比能比H.263高2倍,比MPEG-4高1.5倍。 也是目前苹果支持最好的编码格式。 音频编码 和视频编码类似,音频编码的作用是减少音频中的冗余,同时在保证一定音频质量的条件下作的压缩处理。名称 推出机构 推出时间 目前使用领域AAC MPEG 1997 流媒体AC-3 Dolby Inc. 1992 DVD, 数字电视MP3 MPEG 1993 各个领域(旧)WMA Microsoft Inc. 1999 WindowsAACAAC(高级音频编码技术 Advanced Audio Coding),出现于1997年,是基于MPEG-2的音频编码技术。由Fraunhofer IIS、杜比、苹果、AT&T、索尼等公司共同开发,以取代mp3格式。2000年,MPEG-4标准出台,AAC从新整合了其特性,故现又称MPEG-4 AAC,即m4a。苹果的CoreAudio对AAC有较好的支持。封装格式 我们常见的视频格式MP4, AVI, RMVB, 3GP, MKV都是指视频的封装格式,大部分情况就是视频文件的后缀。常见的组合方式有:名称 简介 常用编码格式 扩展名Flash Video 由Adobe Flash延伸出来的的一种流行网络视频封装格式。随着视频网站的丰富,这个格式已经非常普及。 H.264+MP3 flvAVI(Audio Video Interleave) 比较早的AVI是微软开发的。其含义是Audio Video Interactive,就是把视频和音频编码混合在一起存储。AVI也是最长寿的格式,已经存在10余年了,虽然发布过改版(V2.0于1996年发布),但已显老态。AVI格式上限制比较多,只能有一个视频轨道和一个音频轨道(现在有非标准插件可加入最多两个音频轨道),还可以有一些附加轨道,如文字等。AVI格式不提供任何控制功能。 Xvid+MP3 aviWMV(Windows Media Video) 同样是微软开发的一组数字视频编解码格式的通称,ASF(Advanced Systems Format)是其封装格式。ASF封装的WMV档具有“数字版权保护”功能。 VC-1+WMA wmv/asfMPEG(Moving Picture Experts Group) 是一个国际标准化组织(ISO)认可的媒体封装形式,受到大部分机器的支持。其存储方式多样,可以适应不同的应用环境。MPEG-4档的档容器格式在Part 1(mux)、14(asp)、15(avc)等中规定。MPEG的控制功能丰富,可以有多个视频(即角度)、音轨、字幕(位图字幕)等等。MPEG的一个简化版本3GP还广泛的用于准3G手机上。 H.264+AAC,H263+AAC dat(VCD),vob(DVD), mp4, 3gpMatroska 是一种新的多媒体封装格式,这个封装格式可把多种不同编码的视频及16条或以上不同格式的音频和语言不同的字幕封装到一个Matroska Media档内。它也是其中一种开放源代码的多媒体封装格式。Matroska同时还可以提供非常好的交互功能,而且比MPEG更方便、强大。 各种编码格式的组合 mkvReal Video Real Media(RM) 是由RealNetworks开发的一种档容器。它通常只能容纳Real Video和Real Audio编码的媒体。该档带有一定的交互功能,允许编写脚本以控制播放。RM,尤其是可变比特率的RMVB格式,没有复杂的Profile/Level,制作起来较H.264视频格式简单,非常受到网络上传者的欢迎。此外很多人仍有RMVB体积小高质量的错误认知,这个不太正确的观念也导致很多人倾向使用rmvb,事实上在相同码率下,rmvb编码和H.264这个高度压缩的视频编码相比,体积会较大。 RealVideo+RealAudio rm/rmvbQuickTime File Format 是由苹果公司开发的容器。1998年2月11日,国际标准化组织(ISO)认可QuickTime文件格式作为MPEG-4标准的基础。QuickTime可存储的内容相当丰富,除了视频、音频以外还可支持图片、文字(文本字幕)等。 H.264+AAC mov, qtMP4 mp4格式是H.264编码指定使用的标准封装格式,3GP是MP4格式的一种简化版本,减少了储存空间和较低的频宽需求,让手机上有限的储存空间可以使用。 实际上这些封装格式对应的音频视频编码格式也不是固定的,就拿MP4来说,常见的MP4是由H.264+AAC封装,但是也由Xvid+AAC编码的可能。如果解码器不支持Xvid,则可能会出现无法播放,或者播放播放过程有声音无画面的情况。 苹果支持哪些音视频编码格式 我们可以在手机介绍界面,找到手机支持的视频格式iPhone - Compare Models - Apple 这里我们可以找到对应iPhone7支持的视频编码格式:Video formats supported: HEVC, H.264, MPEG-4 Part 2, and Motion JPEGHEVC 又叫H.265,iOS11+A9芯片才开始对HEVC的支持,iPhone6s及以前的设备不支持HEVC解码。 音频格式用红框标出来了,内容较多,可自行对比。 AVPlayer支持哪些视频格式 苹果设备支持音视频格式并不是就代表AVPlayer也支持那么多格式,确定AVPlayer的支持格式,我们可以查看AVKit中的一个API: //展示当前支持的音视频格式 let asset = AVURLAsset.audiovisualTypes() //打印asset可以得到(已经转过展示格式) asset type ( "audio/aacp", "video/3gpp2", "audio/mpeg3", "audio/mp3", "audio/x-caf", "audio/mpeg", "video/quicktime", "audio/x-mpeg3", "video/mp4", "audio/wav", "video/avi", "audio/scpls", "audio/mp4", "audio/x-mpg", "video/x-m4v", "audio/x-wav", "audio/x-aiff", "application/vnd.apple.mpegurl", "video/3gpp", "text/vtt", "audio/x-mpeg", "audio/wave", "audio/x-m4r", "audio/x-mp3", "audio/AMR", "audio/aiff", "audio/3gpp2", "audio/aac", "audio/mpg", "audio/mpegurl", "audio/x-m4b", "application/mp4", "audio/x-m4p", "audio/x-scpls", "audio/x-mpegurl", "audio/x-aac", "audio/3gpp", "audio/basic", "audio/x-m4a", "application/x-mpegurl" )还有一个方式用来判断当前格式是否可播: //An extended MIME type string such as video/3gpp2; codecs="mp4v.20.9, mp4a.E1" or audio/aac; codecs="mp4a.E1". let playable: Bool = AVURLAsset.isPlayableExtendedMIMEType("video/3gpp2; codecs=\"mp4v.20.9, mp4a.E1\"")这里的MIMEType可以在这里找到 System-Declared Uniform Type Identifiers 总结 由此我们可以下一个总结,AVPlayer支持的: 视频编码格式:H.264、HEVC(iPhone7及以后设备)、MPEG-4。 视频格式(封装格式):.mp4、.mov、.m4v、.3gp、.avi等。 如果想支持更多的视频格式,可以使用使用第三方的框架,常用的视频编码和解码框架有VLC和ffmpeg。 参考文献 视频文件格式 - 维基百科,自由的百科全书 音频编码格式的比较 - 维基百科,自由的百科全书
iOS开发月报#4|201810
- 01 Nov, 2018
记录本月开发遇到的知识点,小tips,和bug总结。 大事件 新版iPad Pro、MacBook Air、Mac mini发布,全线涨价,但是真香。。。Tips 适配swift4.2 1、利用xcode快速迁移 升级到Xcode10之后,我们打开项目会出现如下提示,点击会有一个版本升级窗口,如果你的的项目包含一些第三方库的话,第三方库的选型也会出现在上面:默认勾选第三方库,但是我们适配的时候不应该让Xcode去自动检索第三方库代码。只对我们的app进行代码迁移就够了。 适配完后可以在这里查看我们当前的swift版本:对于第三方库,如果都适配了swift4.2,那么更新到对应版本就行了。如果有没适配的,可以通过制定版本的方式解决冲突,在Podfile文件末尾添加如下代码: post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['SWIFT_VERSION'] = '4.0' end end end属性访问权限 swift中提供的访问权限关键词由低到高有以下五种: private < fileprivate < internal < public < open 其中internal是Swift中的默认控制级,一下介绍了这几种关键字的区别:private:当前作用域或者当前类中访问。 fileprivate:表示代码可以在当前文件中访问。 internal:在当前target中访问 public:可跨target使用,但不能被集成或者重写。 open:可跨target使用,允许被集成或者重写。对于一个严格的项目来说,精确的最小化访问控制级别对于代码的维护来说是很重要的。能用public的就别用open。 修改文件权限 当我们执行某些命令行操作,收到如下提示时: Linking /usr/local/Cellar/the_silver_searcher/2.1.0... Error: Could not symlink etc/bash_completion.d/ag.bashcomp.sh /usr/local/etc/bash_completion.d is not writable.就表明我们对需要修改的文件权限不够,我们可以给文件加上修改的权限: sudo chown -R $(whoami) /usr/local/etc/bash_completion.d其中-R表示对目前目录下的所有文件与子目录进行相同的拥有者变更(即以递回的方式逐个变更) 默认关键字 public static let `default` = ImageCache(name: "default")default是默认关键字,如果使用要加单斜号。 tabbar手动跳转 func tabBarController(UITabBarController, didSelect: UIViewController)In iOS v3.0 and later, the tab bar controller calls this method regardless of whether the selected view controller changed. In addition, it is called only in response to user taps in the tab bar and is not called when your code changes the tab bar contents programmatically. In versions of iOS prior to version 3.0, this method is called only when the selected view controller actually changes. In other words, it is not called when the same view controller is selected. In addition, the method was called for both programmatic and user-initiated changes to the selected view controller.tabbar的didSelect代理方法,只有在手动点击的时候才会触发。通过代码跳转是不会触发的。 自定义tabbar动画 // 当点击tabBar的时候,自动执行该代理方法(不需要手动设置代理) override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { for view in tabBar.subviews { //控制view实现各种动画效果 } }NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END 从xcode6.3开始,为了能让OC也能表示swift的?(optional)和!功能,增加了对对象的可选指定。指定属性是否可选,可以: @property (nonatomic, copy, nonnull) NSString * tickets; //或者 @property (nonatomic, copy) NSString * __nonnull tickets;如果属性多了,每一个都这么写会很麻烦,苹果增加了一对新的宏命令,就是NS_ASSUME_NONNULL_BEGIN和NS_ASSUME_NONNULL_END。放在里面的对象就相当于都增加了nonnull命令,nonull为默认值。 一个自定义collectionView布局的bug bug描述: NSInternalInconsistencyExceptionUICollectionView received layout attributes for a cell with an index path that does not exist: <NSIndexPath: 0x280d2b200> {length = 2, path = 1 - 5}原因: layoutAttributesForElementsInRect返回的UICollectionViewLayoutAttributes数组有indexPath没有被 [NSIndexPath indexPathForRow:numberOfSection]覆盖。 当数据量增加时不会出问题,当数量减少时出现。有人反映这是iOS10的bug,但实际上,我拿iOS10模拟器跑并没有问题,反而是在崩溃后台看到是iOS12的用户上报的。那究竟什么原因不详,附stackoverflow(iOS 10 bug: UICollectionView received layout attributes for a cell with an index path that does not exist - Stack Overflow]地址。 解决方案: 当我们自定义layout时,需要清除UICollectionViewLayoutAttributes的缓存 //方案一: 在自定义layout的类里 override func prepare() { super.prepare() attributesArr.removeAll() } //方案二:在collectionview刷新出 collectionView.reloaData() collectionView.collectionViewLayout.invalidateLayout()配置git SSH 1、设置git的user name和email $ git config --global user.name "username" $ git config --global user.email "username@gmail.com"2、生成秘钥 $ ssh-keygen -t rsa -C "username@gmail.com"如果不需要设置密码,连按三个回车。最后得到了两个文件:id_rsa和id_rsa.pub。 3、添加秘钥到ssh-agent中 $ ssh-add ~/.ssh/id_rsa4、复制公钥内容到剪贴板 $ pbcopy < ~/.ssh/id_rsa.pub5、登录git仓库(Github或者Bitbucket)将公钥内容粘贴到指定配置位置。 Github地址为:https://github.com/settings/keys Apple Watch开发注意事项 1、watch没有UIKit,对于UI的操作只能通过storyboard进行 2、watch只支持帧动画,我们只能通过png序列来实现动画效果。WKInterfaceGroup 和 WKInterfaceImage均可以实现帧动画。 3、开发的watch应用内存被限定为80M,太多帧的动画会不支持 4、提交应用watch也需要配置市场截图。 5、watch分为两个target。当新建一个Target为WatchDemo,xcode会自动生成一个WatchDemo Extension。前者负责UI,后者负责逻辑。引用cocoapods可以这么写: target 'WatchDemo Extension' do platform :watchos, '3.0' use_frameworks! pod 'Alamofire' end6、Always Embed Swift Standard Libraries 在Build Settings里面,这两个target,需要将WatchDemo Extension中设置为Yes,另一个设置为No Github Sizes 可以在一个界面,显示各个屏幕尺寸。这样我们就不用每个模拟器跑一遍看效果了。方便调试。 iOS-DeviceSupport 当手机升级,而xcode未升级时,我们会遇到Device Support的弹框,此时要么升级xcode,要么需要导入对应的Device Support文件。这个库就是提供这种文件的。 InjectionIII 用于解决烦人的UI调试问题。当修改了一些UI属性之后,在xcode中我们只能运行程序才能看到效果,如果是处理大量的UI问题,这个过程是很烦人的。好在InjectionIII帮我们解决了这个问题,一起了解一下吧!
iOS开发月报#3|201809
- 01 Oct, 2018
新机发布,你中意XS Max还是XR?iOS Tips 在label中插入图片 let label = UILabel() let attribute = NSMutableAttributedString(string: "Title")let imgAttch = NSTextAttachment() imgAttch.image = image //设置图片大小 imgAttch.bounds = CGRect.init(x: 0, y: 0, width: 18, height: 15) let imageAttribute = NSAttributedString(attachment: imgAttch) //图片插入位置 attribute.insert(imageAttribute, at: 0) label.attributedText = attributeCollectionCell阴影+圆角的优雅处理方式 只针对类似App Store的整体圆角阴影的效果。 //设置父视图阴影效果 //CollectionView.swift self.layer.shadowOffset = offset self.layer.shadowColor = color.cgColor self.layer.shadowRadius = radius self.layer.shadowOpacity = opacity contentView.layer.cornerRadius = radius //如果切割圆角带imageView才需要添加下面 contentView.layer.masksToBounds = true处理tableView点击时label背景色消失问题 //第一种方式,UITableViewCell.swift override func setHighlighted(_ highlighted: Bool, animated: Bool) { let color = self.indexLabel.backgroundColor super.setHighlighted(highlighted, animated: animated) self.indexLabel.backgroundColor = color } //第二种方式 cell.textLabel.backgroundColor = UIColor.clear cell.textLabel.layer.backgroundColor = UIColor.red调整UILabel文字的内边距 自定义UILabel,然后重写drawText:方法 override func drawText(in rect: CGRect) { let insets = UIEdgeInsets.init(top: 20, left: 20, bottom: 20, right: 20) super.drawText(in: UIEdgeInsetsInsetRect(rect, insets)) }swift 浮点数取整 ceil(x)返回不小于x的最小整数值。 floor(x)返回不大于x的最大整数值。 round(x)返回x的四舍五入整数值。 let number1 = 12.456 // ceil(number1) = 13.0, floor(number1) = 12.0, round(number1) = 12.0 let number2 = 12.756 // ceil(number2) = 13.0, floor(number2) = 12.0, round(number2) = 13.0跳过非store下载的应用检查 xattr -d com.apple.quarantine app所在路径 加载大图导致内存暴涨 large_leaves_70mp.jpg图片是7033x10110(占用磁盘大小8.3MB),分辨率 = 7033 x 10110 x 4(ARGB),对应位图占用大小 = 分辨率 x 1024 x 1024 ( = 271MB),解压会把图片转成位图,也就意味着会占用271MB内存,所以解压过程内存会瞬间消耗很大,等转成NSData后位图的内存就会被回收掉,内存就降下来,这时候NSData占用的大小即是图片的实际大小,该过程中由于会转成位图,而位图的大小是比图片的实际的大小大很多的,内存暴增的点就在位图。位图的内存大小计算是根据图片的分辨率而来(分辨率(width x heigth) x 1024 x 1024 x 4 (ARGB)),所以一般来说图片分辨率越高转成的位图占用的内存空间越大。 新版iphone尺寸设备 逻辑分辨率 比例因子 对角线 分辨率iPhone XS Max 414×896 @3x 6.5inch 1242px × 2688pxiPhone XS 375×812 @3x 5.8inch 1125px × 2436pxiPhone XR 414×896 @2x 6.1inch 828px × 1792pxiPhone X 375×812 @3x 5.8inch 1125px × 2436pxiPhone 8 Plus 414×736 @3x 5.5inch 1242px × 2208pxiPhone 8 375×667 @3x 4.7inch 750px × 1334pxiPhone SE 320×568 @2x 4inch 640px × 1136pxiPhone 4 320×480 @2x 3.5inch 640px × 960px带透明度的渐变 使用CAGradientLayer进行渐变的时候,如果我们需要由一个颜色渐变至透明,当我们将透明色写成UIColor.clear或者其他通过RGBA设置的颜色,改变透明度为0时,会发现实际效果跟预期有出入,会带点黑色:这是因为clearColor会有一个透明度为0的黑色通道。所以应该这样设置透明色: UIColor(white: 1, alpha: 0).cgColor指定tableView,collectionView的header高度0 如果我们想隐藏headerView可能会直接在其高度的代理方法,做如下设置: func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { return 0 }当实际上这并不会生效,会返回一个默认高度。也就是说这个高度只有设置成一个非0的正数才是有效的。 有些人会写成0.01,表面上看是解决问题了,但0.01的偏移会造成像素不对齐(Color Misaligned Images),加重CPU计算负荷。完美的解决方案是: func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { return CGFloat.leastNonzeroMagnitude }CGFloat.leastNonzeroMagnitude表示CGFloat支持的最小正数值,不会引起像素偏移。 保持屏幕常亮 //保持屏幕常亮 UIApplication.shared.isIdleTimerDisabled = true //关闭屏幕常亮 UIApplication.shared.isIdleTimerDisabled = false注意:不要滥用屏幕常亮属性(苹果会因为这打回你的app),如果只在某些特殊场合需要屏幕常亮,应该在之后将该值恢复成默认值false。 Github MMKV MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。从 2015 年中至今,在 iOS 微信上使用已有近 3 年,其性能和稳定性经过了时间的验证。近期也已移植到 Android 平台,一并开源。 替代UserDefaults的绝佳方案。 Lottie Lottie是一个面向Android和iOS的移动库,它能够解析由AE在bodymovin导出为json的效果动画,并在移动端渲染矢量动画! 一些复杂的UI可以完全交给设计师了,哈哈哈。 RSSHud RSSHub 是一个轻量、易于扩展的 RSS 生成器,可以给任何奇奇怪怪的内容生成 RSS 订阅源。 结合Reeder不要太爽哦。😆
Git基本操作回顾
- 20 Sep, 2018
作为git最常用的几个命令git status、git add、git commit,我们每天可能都会写个数十遍。但是越是这种我们熟悉的操作,越容易存在一些我们忽略的细节。这篇文章就是用来记录下这些细节,记录我们常用命令中不常用的操作。 在git中编辑过某些文件之后,由于自上次提交后你对它们做了修改,git 将它们标记为已修改文件。 我们逐步将这些修改过的文件放入暂存区,然后提交所有暂存了的修改,如此反复。所以使用 git 时文件的生命周期如下:回顾完就进入正题git status git status会有以下几种状态 $ Changes to be committed: $ (use "git reset HEAD <file>..." to unstage)表示已经在暂存区,等待添加到工作区。使用git reset命令可以将暂存区的内容移除。 $ Changes not staged for commit: $ (use "git add <file>..." to update what will be committed) $ (use "git checkout -- <file>..." to discard changes in working directory)有修改, 但是没有被添加到暂存区。使用git add命令可以将文件添加到暂存区,使用git checkout命令可以撤销文件修改。 $ Untracked files: $ (use "git add <file>..." to include in what will be committed)含有未跟踪文件, 即未纳入版本管理的文件。使用git add可以将文件放入暂存区。git add 添加文件到暂存区 git add file添加多个文件到暂存区,空格隔开 git add file1 file2使用通配符批量添加documentation目录下的所有txt后缀文件 git add documentation/*.txt添加当前目录下的所有git-开头的shell文件 git add git-*.sh将修改和以删除的文件添加到暂存区,不包括未被跟踪文件。 git add -u filegit add .和git add -A(即git add --all)区别 一.版本导致的差别: 1.x版本: (1).git add -A可以提交未跟踪、修改和删除文件。 (2).git add .可以提交未跟踪和修改文件,但是不处理删除文件。 2.x版本: 两者功能在提交类型方面是相同的。 二.所在目录不同导致的差异: (1).git add -A无论在哪个目录执行都会提交相应文件。 (2).git add .只能够提交当前目录或者它后代目录下相应文件。git commit 当我们执行git add命令将文件放到暂存区之后,还需要提交这些暂存到工作区(仓库区),从暂存区->工作区,的工作就是git commmit来做的。 # 提交暂存区到仓库区,message为提交信息 git commit -m [message] # 提交可以指定文件 git commit [file1] [file2] ... -m [message]常用的commit扩展命令 # 提交时显示所有diff信息 git commit -v # 使用一次新的commit,替代上一次提交,如果代码没有任何新变化,则用来改写上一次commit的提交信息 git commit --amend # 重做上一次commit,并包括指定文件的新变化 git commit --amend [file1] [file2]以上三条如果不带-m [message]将会在vim的编辑器中添加提交信息。 如果你感觉没有git add,git commit有点麻烦,想直接将修改到工作区,可以用另外一个命令。 # 会将上次commit之后的变化,直接添加到工作区 git commit -a -m [message]git rm rm file删除位置:相当于手动右击点删除,只删除了工作区的文件。 git status:Changes not staged for commit: 恢复:直接用git checkout -- file就可以。 git rm file它等价于rm file + git add file 删除位置:相当于不仅删除了文件,而且还添加到了暂存区。 git status:Changes to be committed:。 恢复:先git reset,去掉暂存区修改,然后再git checkout -- file,恢复文件。 git rm --cached file删除位置:从暂存区移除,不删除文件。 git status:Changes to be committed:,Untracked files: 恢复:git add file
使用git stash储存和恢复进度
- 04 Sep, 2018
当我们正在当前项目处理一些事情时,有一个需求插进来,使得我们要在别的分支做一些工作。切换分支之前当前任务是需要保存的,但我们并没有完成一个完整的任务,直接commit显得不合适,这时就可以使用git stash命令。stash是储藏的意思,该命令的作用也可以理解为先将当前的修改储藏起来,等我们在其他分支做完必要工作之后可以再回到储藏时的状态。 git stash大致可以分为储存和恢复这两步。储存 储藏当前进度有两条命令: git stash保存当前工作进度,会把暂存区和工作区的改动都保存起来,再次运行git status会发现当前工作区是干净的。 git stash save "commit message"是git stash的完整描述,可以为本次保存添加说明。 恢复 git stash list查看当前保存进度,进度保存可以有多个。 git stash apply恢复最近保存的进度,不会删除stash内容 git stash apply stash@{0}如果有多个stash,恢复某一个,按时间倒叙排列 git stash pop会恢复最新保存的工作进度,并将恢复的工作进度从存储的工作进度列表中清除。 git stash drop [stash_id]删除某一个存储的进度 git stash clear #删除所有储存进度删除所有存储进度
iOS开发月报#2|201808
- 03 Sep, 2018
数据库用完要close 当我们向下面这样执行完一次数据可查询时,要记得将数据库关闭,否则,如果此时想往同一数据库写东西的话会因为数据正在锁定收到这样的提示database is locked。 //获取下载完成的文件信息 func isExistdWith(_ id: String) -> Bool{ guard db.open() else { return false } do { let resultSet = try db.executeQuery("select * from tableName where id = ?", values: [id]) if resultSet.next() {let isCompleted = resultSet.bool(forColumn: self.isCompleted) db.close()//return之前要close数据库 return isCompleted } } catch {} db.close()//return之前要close数据库 return false }UIDatePicker的时间格式 当我们用UIDatePicker做选择时间的控件时,DatePicker会根据手机时间的设置自动选择是12小时制还是24小时制,如果我们需要强制控制DatePicker是显示12小时制还是24小时制可以这么做: datePicker.datePickerMode = .time datePicker.locale = Locale.init(identifier: "en_GB")//for 24 Hrs datePicker.locale = Locale.init(identifier: "en_US")//for 12 HrsiOS skill map变量对外只读,对内可读写 struct Person { private(set) var name : String! }设置UITableViewCell分割线对齐 默认的cell分割线都是偏向右边多一些的,如果我们想让分割线对齐的话,正确的做法是: tableView.separatorInset = UIEdgeInsets.init(top: 0, left: 40, bottom: 0, right: 40)设置左右边距都是40 但是使用这种方法会带来一个问题就是默认的textLabel会跟着右移。为了保持label的居中我们可以再加一句: tableView.separatorInset = UIEdgeInsets.init(top: 0, left: 40, bottom: 0, right: 40) tableView.layoutMargins = UIEdgeInsets.init(top: 0, left: 40, bottom: 0, right: 40)富文本显示图片元素 如果我们需要文字中插入图片元素时,可以使用富文本处理: let attch = NSTextAttachment() attch.image = UIImage.(named:"logo") attch.bounds = CGRect.init(x: 0, y: 0, width: 18, height: 18) let imageAttribute = NSAttributedString(attachment: attch) titleLabel.attributedText = attributed添加spotlight搜索索引 首先导入CoreSpotlight和MobileCoreServices框架,然后加入以下代码: var searchableItems = [CSSearchableItem]() //索引项 let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeData as String) //title attributeSet.title = "Item Title" //desription attributeSet.contentDescription = "match.description" //thumb attributeSet.thumbnailData = try? Data.init(contentsOf: URL(string: url)!) //keywords attributeSet.keywords = ["Love", "Peace"] let searchableItem = CSSearchableItem(uniqueIdentifier: "app_keywords", domainIdentifier: "com.company.app", attributeSet: attributeSet) searchableItems.append(searchableItem) //建立索引 CSSearchableIndex.default().indexSearchableItems(searchableItems) { (error) -> Void in if error != nil { print(error?.localizedDescription ?? "Error") } }Hexo编译问题 在执行hexo g编译markdown文件时莫名报错: TypeError: Cannot set property 'lastIndex' of undefined解决方案是在_config.yml中将auto_detect设为false Podfile用法 # 下面两行是指明依赖库的来源地址 source 'https://github.com/CocoaPods/Specs.git' source 'https://github.com/Artsy/Specs.git'# 说明平台是ios,版本是8.0 platform :ios, '8.0'# 忽略引入库的所有警告(强迫症者的福音啊) inhibit_all_warnings!# 生成的是framework而不是静态库 use_frameworks!# 针对MyApp target引入AFNetworking # 针对MyAppTests target引入OCMock, target 'MyApp' do pod 'AFNetworking', '~> 3.0' target 'MyAppTests' do inherit! :search_paths pod 'OCMock', '~> 2.0.1' end pod 'JSONKit', :podspec => 'https://example.com/JSONKit.podspec' # 引入内部库 pod 'ABTest', :git => 'https://bitbucket.org/sealcn/remoteabtest.git' pod 'ABTest', :git => 'https://bitbucket.org/sealcn/remoteabtest.git', :tag=> '0.0.6' pod 'ABTest', :git => 'https://bitbucket.org/sealcn/remoteabtest.git', :commit=> '082f8319af' # 编译配置,指定仅在Debug模式下启用 pod 'Reveal-SDK', :configurations => ['Debug'] # 使用本地文件 pod 'AFNetworking', :path => '~/Documents/AFNetworking' # 指定版本号0.1.3到0.2,不包括0.2 pod 'CHIPageControl', '~> 0.1.3' # 仅安装QueryKit下的Attribute和QuerySet模块 pod 'QueryKit', :subspecs => ['Attribute', 'QuerySet']end # 这个是cocoapods的一些配置,官网并没有太详细的说明,一般采取默认就好了,也就是不写. post_install do |installer| installer.pods_project.targets.each do |target| puts target.name end end
《精进》阅后总结
- 03 Sep, 2018
最近刚看完采铜的这本《精进-如何成为一个很厉害的人》,一本很值得推荐的书。这种讲述如何学习,如何自我提升的书很容易一不小心写成空洞的心灵鸡汤。然而我在实际阅读中根本没有这种想法,因为它专业性很强,我感受到的是作者的博学和诚意,还有经常性的好像被点拨了一下的惊喜。没有深入的思考和深厚的学术功底是写不出这种书,推荐大家有机会也仔细地读一读。 阅读过程中画了很多自己受启发的观点,也可以说是我读到的这本书中的精华部分,摘录了下来。全书分七个章节,逐一讲述如何成为一个很厉害的人:时间之尺林德沃提出的更好地对待时间的十条建议: 1、活在当下 2、严肃地对待时间 3、留意自己拥有的空间并享受它 4、反思自己和其他人的时间视角 5、从现在出发联结过去 6、并不完全沉浸于过去 7、指定实现目标的计划 8、平衡计划和非计划时间 9、视未来存在与当下 10、对未来保持积极的态度 需要好好思考一下,如何通过一点一滴的人生增量,完成个人核心竞争力的锻造。 让“远期未来”更加具体,为“近期未来”增加挑战。 把时间花在值得做的事情上。收益指+收益半衰期。 使用时间之尺,审视事件的长期价值,尽可能删减掉非必要事件。 侯世达定律:实际做事花费的时间总比预期要长,即使预期中考虑了侯世达定律。 提升时间使用的“深度”,减少被动式休闲的比例,保持至少一项长期的业余爱好。寻找心中的“巴拿马”仅仅是好的选择是不够的我们需要的事最好的选择。 一个成熟的人,他的标准来自他的内心。 人不能只为他自己而活。我们必须认知所有的生命都是珍贵的,而我们和所有的生命是结合在一起的。这种认知指引了我们心灵和宇宙的关系。 最近一周,我所做过的最有意义的一件事:________ 永远不要放弃寻找“第三选择”,因为最好的选择,往往来自在更高目标指引下的我们的创造。 婚恋拇指法则:生理上有冲动,精神上受鼓舞,沟通上很流畅。 人生是持续而反复的构造,校正选择,做出建设性的改变。 不管你做了哪个选择,你的某些东西永远不会改变,最终带着你走向目的地的,可能并不是某一个选择,而是那些你不会改变的东西。即刻行动开始并完成一件事情,比做好它更重要。因为只要开始了,你就有机会把它做得更好。 先把必须要做的小时处理掉,使我们保持积极和从容心态的一剂良方。 “精益创业”有个关键概念叫“最小化可行产品”,它指的是可以使用最少的资源、被最快制作出来的、可执行基本功能的、能被用户使用的实验性产品。创业者应该尽快把最小化可行产品发布出去,然后根据用户使用它的反馈来进行优化,这一过程称为“构建-测量-学习”的循环。 成熟心智的一个特点就是,它能不偏不倚地、公正地对待自己和其他人的意见,既不固步自封也不附和盲从。 多线程工作,首先需要一段专注不受干扰的时间,完成工作中最核心部分的思考。 行动后要及时反思,并梳理这件事情的“反应连”,特别关注其中发生的意外现象。怎样的学习,才能够直面现实学习,应该以学习者心中的问题未中心,让问题引导着我们去探求答案。 不要只做信息的搬运工,通过解码,深入事物的深层。 伟大的艺术作品,常常有很深厚的内涵和很精巧细微的技法,不论你在哪个或深或浅的层次上解读他,他都能呈现出美妙的意味,但如果你不做一番细心的努力和挖掘,就只能尝到最表层的那一小部分味道。 教育心理学家把在某一领域有专长的人士,分为“常规性专长”和“适应性专长”。 求职分为三个层次:信息、知识和技能、技能是学习的终点,信息和知识是迈向这个终点的路河桥。 利用交替运用意识和潜意识进行创造性思考。向未知的无限逼近简化思维意味着我们既要简化外界输入的信息,也要简化我们表达出来的信息,更要简化我们一直思考着的信息。 如何过滤没有价值的信息:精选可信赖的信息源、不追逐当下流行或过热的信息、重事实信息,轻观点和评论、定期闭关,屏蔽外界纷扰。 用足够多的资料“喂养”潜意识。 比如整理书柜并更换一种新的图书分类方法,或者改变日常上下班的路线去探索一条从未走过的路线,也可以尝试与不同的同学或同事共进午餐等,这些小变化引起的扰动可能会刺激出我们新的想法。 把思维转化为外显图形,为我们的思考扩展出了一个更大的空间。 现实中的问题,总是牵涉太多的因素,借助矩阵、清单等工具可以完善思考的周密程度。我知道 我不知道别人知道 公开区 盲区别人不知道 隐秘区 未知区为了让思维更好的发散,获得更多的灵感,一是要关掉大脑里评论的空间,而是要适当的引入混乱与随机。 一个具有高度可塑性的大脑在良好思维工具的辅助下,在持续不断的行动的打磨中,会强大的超出你的想象。努力,是一种最需要学习的才能努力不是一场意志力的较量,而是一种需要学习的策略,可以不断学习和优化。 在心理学家眼中,“才能”被定义为“自发地重复出现且可悲高效利用的思维、情感或行为模式。” 以大多数人的努力程度之低,根本轮不到拼天赋。 挑战是设计出来的,不断为自己设计“必要的难度”挑战。 不痛苦地坚持到底,只有深入下去,才能培养出真正的兴趣。 因努力而热爱。 把时间投放在一个领域,以尽量高的标准要求自己,培养出非常高的才能。每一个成功者,都是唯一的创造成功,而不是复制成功。 做一个积极的预设判断要优于消极的预设判断。 尝试可能会犯错,可能会遭遇失败,但失败是包含信息的,甚至比成功包含的信息量更大。 一个自我教育者应该学会定期审视自己的所知和未知,能评估自己所学知识的价值,特别是在实践领域中的价值。 在现实世界中思考理论问题,在理论世界中思考现实问题。 为大众带来新鲜的见解和启发,形成对公众的影响力。 抗拒自己的欲望,或者延迟满足欲望。 根据自己的内心需求,而不是外界的认同,作出独立的选择,甚至去做一些酷的事情。 如果你找到了一条别人都还没走过的路,只要把这条路走完,你就赢了。
使用Cocoapods管理私有库组件
CocoaPods是OS X和iOS下的一个第三方开源类库管理工具,通过CocoaPods工具我们可以为项目添加依赖库(这些类库必须是CocoaPods本身所支持的),并且可以轻松管理其版本。它是目前iOS开发中使用最广泛的开源库管理工具,如果我们内部协作的组件化能够使用这种方式管理的话,那将是很便利的。 在通过Cocoapods建立内部私有库之前,我们需要再熟悉下Cocoapods的工作流程,我们创建内部私有库时也会依照这个流程来。本文目录 一、Cocoapods的工作流程 二、建立Cocoapods私有库 三、使用私有库 四、问题总结Cocoapods工作流程 工作流程如图所示:远程索引库: 这里存放了各个框架的描述文件,托管在github上: CocoaPods/Specs 本地索引库: 在安装cocoapods时,执行的pod setup就是讲远程索引克隆到本地,本地索引的目录为: ~/.cocoapods/repos/master本地索引和远程索引的目录一致,结构如下:每个库的每个版本都对应一个json格式的描述文件: { "name": "YYImage", "summary": "Image framework for iOS to display/encode/decode animated WebP, APNG, GIF, and more.", "version": "1.0", "license": { "type": "MIT", "file": "LICENSE" }, "authors": { "ibireme": "ibireme@gmail.com" }, "social_media_url": "http://blog.ibireme.com", "homepage": "https://github.com/ibireme/YYImage", "platforms": { "ios": "6.0" }, "source": { "git": "https://github.com/ibireme/YYImage.git", "tag": "1.0" }, "requires_arc": true, "default_subspecs": "Core", "subspecs": [ { "name": "Core", "source_files": "YYImage/*.{h,m}", "public_header_files": "YYImage/*.{h}", "libraries": "z", "frameworks": [ "UIKit", "CoreFoundation", "QuartzCore", "AssetsLibrary", "ImageIO", "Accelerate", "MobileCoreServices" ] }, { "name": "WebP", "dependencies": { "YYImage/Core": [] }, "ios": { "vendored_frameworks": "Vendor/WebP.framework" } } ] }本地索引文件 当执行pod search命令时,如果本地索引不存在,就会创建出来: $ pod search afn Creating search index for spec repo 'master'..本地索引文件路径为: ~/Library/Cache/Cocoapods/Pods远程框架库 以YYImage为例,它的远程框架库就是json文件中的source: https://github.com/ibireme/YYImage.git 所以再用文字总结下Cocoapods工作流程大概就是 1、本地安装cocoapods,建立本地索引库和远程索引库的映射 2、本地项目pod install 3、查找本地索引文件,然后找到各个库对应版本的json文件 4、通过json文件source字段找到引用库的git地址 5、把库文件拉到本地项目 建立Cocoapods私有库(framework) 建议采用framework的形式创建私有库,这可以很好的在开发阶段检查出库的不兼容或者文件权限出现的问题,Swift编写的代码通过Cocoapods生成的都是framework。 0、准备工作: 如何建立远程索引库 首先我们需要建立一个内部的远程索引库,类似Cocoapods/Spec的功能,之后添加的库文件索引文件都会存放到这里:https://zhangferry@bitbucket.org/sealcn/sealrepo.git 建立本地和远程索引仓库的关联: pod repo add SealRepo https://zhangferry@bitbucket.org/sealcn/sealrepo.git执行pod repo可以看到我们有了两个索引仓库,可以去在这个目录~/.cocoapods/repos看到我们刚建立的SealRepo。 如何组织文件结构 我们可以看下Alamofire的文件组织结构:我们看到这几个文件:Source用于存放Framework源文件, Example用于放Demo项目 docs和Documentation放说明文档,这个是可选的, Tests测试文件也是可选。 我们制作私有库时会仿照这个格式。一、制作framework因为是Swift的工程,接口的开放直接通过open、public等关键字指定,所以工程中的ABTest.h头文件可以删除,加入我们自己的库文件。注意:在写公有库文件时,对外界开放的属性,方法需要带上public或者open关键字。 二、添加Example工程 通过Xcode菜单栏File->New->Target...添加一个Example工程。 引入第三方库 如果无第三库引用可以跳过这一步。 注意:引入Podfile文件,需要framework和Example两个target都添加上。 测试项目 需要先编译framework,没有问题之后,导入到Demo项目里 import ABTest运行Dome,测试开发功能有没有问题。 push项目到远程库 如果已经关联过远程私有仓库,这一步可以跳过。 在远程配置一个git地址,然后将本地项目关联到远程私有仓库: git remote add origin 仓库地址如过是首次关联远程仓库,在push之前我们一般需要先拉去远程分支 git pull origin master如果提示: There is no tracking information for the current branch.那是因为本地库和远程库没有建立联系,git认为这两个仓库可能不是同一个,如果我们确认对应库没问题,可以使用: $ git pull origin master --allow-unrelated-histories 将远程库文件强制拉到本地仓库。 之后再执行push命令将项目推到远程仓库。 git push -u origin master三、Cocoapods配置文件 1、添加.swift-version .swift-version文件用来告诉cocoapods当前文件swift的版本,用命令行建立: $ echo "3.0" > .swift-version2、添加LICENSE 每个使用cocoapods添加的库都需要准守开源协议,一般是MIT协议,因为bitbucket没法自动生成,我们可以手动生成这个同名文件,然后把协议内容复制进去: MIT LicenseCopyright (c) [year] [fullname]Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.3、创建库描述文件 可以通过命令行生成描述文件: $ pod spec create ABTest然后我们编辑ABTest.podspec文件,可以仿照下面的写法 Pod::Spec.new do |s|s.name = "ABTest" s.version = "0.0.1" s.summary = "ABTest with Firebase" s.description = "This is a ABTest Framworks on swift" s.homepage = "https://bitbucket.org/sealcn/remoteabtest/src/master/" s.license = { :type => "MIT", :file => "LICENSE" } s.author = { "zhangferry" => "zhangfei@dailyinnovation.biz" }# ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # s.platform = :ios, "8.0" # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # s.source = { :git => "https://zhangferry@bitbucket.org/sealcn/remoteabtest.git", :tag => s.version } s.source_files = "Source", "Source/*.swift"# ――― Resources ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # # s.resource = "icon.png" # s.resources = "Resources/*.png"# ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # s.requires_arc = true s.static_framework = true s.dependency "Firebase/Core" s.dependency "Firebase/RemoteConfig" #s.ios.vendored_frameworks = "ABTest.framework" s.xcconfig = { 'SWIFT_INCLUDE_PATHS' => '$(PODS_ROOT)/Firebase/CoreOnly/Sources' } end此时我们的文件目录看起来应该大概是这个样子:4、验证本地podspec文件 pod lib lint该命令用于检查podspec文件书写是否正确,如果有error需要解决,warning可以不用管(可能会遇到较多问题,需耐心解决0。0)。解决之后再次运行检查命令,当命令行显示: -> ABTest (0.0.1) ABTest passed validation.说明我们本地配置成功了,到这里本地的第一个版本就算完成了! 然后我们需要将本次修改提交打上tag,提交到远程仓库。 git add . git commit -m "build v0.0.1" git push origin master git tag 0.0.1 git push --tags5、验证远程索引文件 上传代码成功之后,我们需要再次验证它跟远程仓库(ABTest远程库和.podspec)是否匹配正确,执行: pod spec lint当出现: ABTest.podspec passed validation时,说明我们远程仓库匹配正确。 6、提交podspec文件到SpecsRepo $ pod repo push SealRepo ABTest.podspec这个命令会包含pod spec lint命令,验证通过之后,会添加.podspec文件到本地索引库:和远程索引库:使用私有库 引用私有库 我们可以像使用其他库文件一样在Podfile文件中添加使用私有库了,引入方法有两种: 1、全局添加 在Podfile文件最上面添加一行: source 'https://zhangferry@bitbucket.org/sealcn/sealrepo.git'注意:如果私有仓库和cocoapods仓库出现同名库,会出现不可预期的情况(随机拉下来公有库或者私有库文件)。这时我们需要使用单独添加的方式。 2、单独添加 pod 'ABTest', :git => 'https://zhangferry@bitbucket.org/sealcn/remoteabtest.git'使用时通过import方法导入库就可以了。 更新私有库 当我们需要升级私有库,添加或者修改方法时,只需要: 1、修改.podspec文件中s.version的版本号 2、提交本地修改至远程,打上对应tag 3、使用项目的工程执行pod update 可能遇到的问题 1、pod search 查不到本地库 这个可能是cocoadpods本身问题,pod install安装没有问题 2、更新了版本,但是pod update没有找到 我们可以采用如下形式,手动指定版本号: pod 'ABTest', :git => 'https://zhangferry@bitbucket.org/sealcn/remoteabtest.git', :tag => '0.0.4'3、提示The 'Pods-App' target has transitive dependencies that include static binaries 这是因为引入的库被编译成了静态库,我们可以在.podspec文件中加入: s.static_framework = true4、引入的第三方库,在pod lint时提示找不到 可以手动指定pod目录,将firsebase替换成你的库文件路径: s.xcconfig = { 'SWIFT_INCLUDE_PATHS' => '$(PODS_ROOT)/Firebase/CoreOnly/Sources' }5、提示source_files对应文件为空 每次pod lint时都是根据版本号进行查找的,可以检查下当前修改跟版本号是否对应。
2018年七月
- 06 Aug, 2018
虽然一直都有想要保持写日记的习惯,但是对于我这种上班时间不稳定切偏晚,经常11点12点才能离开公司的人,就更难了。但如果不写点什么,总感觉这段时间就丢失了,往会看的时候会产生一种失落感。于是找到一种折中的方案,索性把时间跨度拉大一点,一个月写一篇总结。东西就放在简书上,会和博客同步。不管产出的东西多少,希望这个习惯能尽可能长的保持下去。关于工作 七月份一直在维护冥想类的新产品Peace,七月份上的线,一周一个版本,目前是1.3了。从最开始的数据不理想,到分析投放数据,开会讨论找问题,对比竞品,找到问题,指定解决方案,快速迭代,产品在越来越好,数据也在越来越好。我也在这个过程中全程目睹了一个新产品的发展历程,这种体验可以说是一件很棒的事了。 但是这段时间也是最忙的一段时期,一周一版一点也不轻松。排期排到周五,就不管多晚周五那天(甚至是已经周六)都要发版的,最晚的一次是战斗到夜里一点。随后老大也调整了策略,多留出一天时间,进行codereview,排期不再那么满,但是快速迭代的节奏还是要一直保持的。 说下最近的能力问题,项目中磨练了很多,踩了很多坑,也总结掌握了一些经验,但并不能感觉到自己已经达到了何种地步。都是碎片化的知识点,知识面不系统,也没法找到一个系统的东西去参考,说自己掌握了那些还差那些。直到遇到了技能图谱这个东西,代表个人能力的技能树。于是参考网上的iOS技能图谱,自己做了一个图谱。瞬间思路也清晰了,以后的博客内容,学习内容就可以根据这个东西来了。 关于生活 生活方面,园区正在组织篮球赛,我作为篮球队的一员在比赛的一个月之前,就已经开始训练了,一周一次两小时的训练。体育这个东西一直以来也是我心心念的小梦想,怎么说呢,虽然不是运动员,但运动本身总能给我带来不一样的,超出我自身限制的体验。我以前总幻想可能这就是兴趣吧,我喜欢这个,但成熟之后,想法就变成了,体育没那么简单,作为一个爱好就够了。 当运动员的梦想渐行渐远时,去年和今年的两场半程马拉松,又把这个心结给勾了出来,两场比赛都完赛时,那种巨大的满足感,只有两个字可以形容,贼舒服! 话题拉回来,渐渐的也迎来了篮球联赛的正式比赛,小组赛6进2,我们打了两场,一胜一负。我当了一场主力,虽然表现一般,只为球队贡献了6分,但也算是战斗到最后一刻。很荣幸啦,那种奔跑如风的感觉,观众为自己加油喝彩的感觉,不管结果怎样,我都享受到了比赛了。 关于自己 去了趟天津,见自己高中时的死党,大家,一起吃饭喝酒,谈论工作,结婚,房价。想起高中时,我们几个因为食堂没有座位蹲在一起吃饭场景,恍若隔日啊。 和女朋友去了趟十渡,玩了标志性的玻璃栈道,高山漂流,抗日英雄纪念馆,十渡风景也很美。其中高山漂流一路冲下来,身上湿了一大半,遇到一个小朋友,说“你们怕水吗?”我还没弄明白怎么回事就说了句“不怕啊”,他就跟妈妈一起开始向我俩泼水了。这熊孩子,跟我刚!当自己放下顾忌,肆无忌惮地疯玩时,这才是真正的快乐吧。 关于自身的成长,感觉自己自律性还是差一些,坏习惯多一些,我能感觉到坏习惯对我形成的阻力,但是要完全克服或者完全抹掉还是需要相当一段时间努力的。 我也在排除一些对自己产生干扰的外界因素,卸载了手机里让我耗费时间的王者荣耀,刺激战场,头条系产品。当拿起手机发现没什么可玩时,我能感受到自己时间是富余的。 就这些吧,希望自己能一直遵从内心,成为那个让自己满意的人。
iOS开发月报#1|201807
- 02 Aug, 2018
关闭隐式动画 CATransaction.begin() CATransaction.setDisableActions(true) self.layer.frame = self.bounds CATransaction.commit()AVPlayer出现一直缓存,缓存一段时间之后才开始播放的问题 player.automaticallyWaitsToMinimizeStalling = false//延迟播放,默认开关于这个属性的一些说明:In versions of iOS prior to iOS 10.0 and versions of OS X prior to 10.12, this property is unavailable, and the behavior of the AVPlayer corresponds to the type of content being played. For streaming content, including HTTP Live Streaming, the AVPlayer acts as if automaticallyWaitsToMinimizeStalling is YES. For file-based content, including file-based content accessed via progressive http download, the AVPlayer acts as if automaticallyWaitsToMinimizeStalling is NO.大致是说在iOS10之前的客户端,虽然这个参数不可用,但是非流媒体类型的播放这个配置默认为false,所以在iOS10下建议这个属性值为false。 AVPlayer是否正在播放的判断 当我们使用KVO监听player.rate来判断player的是否正在播放时,会发现这个值是不准的。其实准确的说是player.rate=1不代表正在播放,player.rate=0是可以代表正在暂停的。所以player.rate=0代表暂停,正在播放的状态可以这样判断: self.timeObserve = self.player.addPeriodicTimeObserver(forInterval: CMTimeMake(1, 1), queue: DispatchQueue.main, using: {(time) in if self.player.timeControlStatus == AVPlayerTimeControlStatus.playing { //AVPlayerTimeControlStatus为iOS之后的API self.state = .playing } })下载时URLSessionConfiguration的配置 使用Alamofire下载时,我们通常需要一个SessionManager配置下载参数: let configuration = URLSessionConfiguration.default configuration.timeoutIntervalForRequest = 50//50s超时 /** 最大同时下载数 ---- iOS对于同一个IP服务器的并发最大默认为4,OS X为6 */ configuration.httpMaximumConnectionsPerHost = 4 /** A Boolean value that indicates whether TCP connections should be kept open when the app moves to the background. */ configuration.shouldUseExtendedBackgroundIdleMode = true//为true支持后台下载 manager = Alamofire.SessionManager(configuration: configuration)不要存储沙盒绝对地址 当我们向沙盒写入数据时,将该绝对路径保存下来,下次再打开该地址并不会获取到我们存入的数据。原因如下: iOS8之后,苹果添加的新特性,将每次打开app内的沙盒[唯一编码路径](红框部分)重新生成,并保持上一次的沙盒文件(Documents、Library、tmp)移到新生成的文件内,旧文件删除,就是说,你保存的文件都在,只不过每次打开后,都会有一个新的绝对路径。所以存储路径应该存相对路径: //这两个都代表document的相对路径 let rootPath = NSHomeDirectory() + "/Documents/" let rootPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).firstchildViewController的viewDidAppear方法调用 如果一个ViewController中嵌套了多个childViewController。当宿主VC(我们暂且这么称呼它)调用viewDidAppear等方法时,其中的childViewController都会默认调用对应方法。如果我们不想childViewController调用该方法可以重写该VC的属性: override var shouldAutomaticallyForwardAppearanceMethods: Bool { return false }图片切换渐入渐出的方法 通过UIImageView展示图片和layer.contents展示图片都可以使用以下方法: let transition = CATransition() transition.duration = 0.5 transition.type = kCATransitionFade self.view.layer.add(transition, forKey: "layer.contents") self.view.layer.contents = image.cgImage//适用于imageViewcell移出视图,移入视图的方法 //TableViewCell override func prepareForReuse() { super.prepareForReuse()//使用重用池的cell,显示过的cell移至可视范围 } //TableView func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { //cell移出视图时调用 }tableview,collectionView数据reload之后的操作 我们如果要想实现在reload之后弹出alertView,或者滚动到特定一行,可能会直接写: tableView.reloadData() tableView.scrollToRow(at: indexPath, at: .middle, animated: true)看似没问题,但是滚动没起作用,因为reloadData是立即返回的,不会等tableview刷新完成。 解决办法就是需要等reload完成之后再做我们需要的操作,reload是否完成有几种方式监听: //collectionView collectionView.performBatchUpdates(nil) { (finished) in //reload完成 } //tableView方法只有iOS11可用 tableView.performBatchUpdates(nil) { (finished) in //reload完成 }//替代func beginUpdates(),func endUpdates() //tableView等reload完成还可以使用 tableView.reloadData() DispatchQueue.main.async { //reload完成 }
可能被忽略的UIButton细节
关于System Button看一个简单的例子: button.setTitle("Title", for: .normal) button.setImage(UIImage(named: "icon"), for: .normal)buttonType分别设置为system和custom,仅做如上设置,显示效果对比(上面的custom,下面的是system)system Button显示出蓝色其实是tintColor的效果,关于tintColor的说法是:This property has no default effect for buttons with type custom. For custom buttons, you must implement any behavior related to tintColor yourself. 在custom类型的button中设置tintColor是不生效的,需要自定义样式。在system类型的button里有一个默认蓝色的tintColor,当然我们可以修改它为其他颜色,会对image和title同时生效。 另外可以发现image不是原始图片,而是被填充为tintColor的颜色。这是因为system类型下button的image被默认以alwaysTemplate类型渲染的,如果想要显示原始图片可以做如下操作: let image = UIImage(named: "icon")?.withRenderingMode(.alwaysOriginal) button.setImage(image, for: .normal)关于触摸反馈看一个常见的代码: let button = UIButton()//默认样式custom button.setTitle("Title", for: .normal) button.setTitleColor(UIColor.blue, for: .normal) button.backgroundColor = UIColor.red view.addSubview(button)以上是的button的常见写法。遗憾的是这种写法,不会带触摸反馈效果。那如果我们想加触摸反馈,需要如何处理: 1、仅文字的触摸反馈 //system: let button = UIButton(type: .system) button.setTitleColor(UIColor.blue, for: .normal)//自动添加反馈效果 button.setTitleColor(UIColor.green, for: .highlighted)//会和系统效果叠加,不可控,不建议写 //custom let button = UIButton(type: .custom) button.setTitleColor(UIColor.blue, for: .normal) button.setTitleColor(UIColor.green, for: .highlighted)//自定义反馈样式2、带图片和文字的触摸反馈 button.setTitle("Title", for: .normal) button.setImage(UIImage(named: "icon"), for: .normal) //system:会同时对图片文字添加反馈效果 //custom:默认仅对图片有触摸反馈3、带背景图的按钮 button.setBackgroundImage(UIImage(named: "background"), for: .normal) //system是按钮整体反馈 //custom是仅背景图片反馈,title,image无反馈4、关闭触摸反馈