深入理解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不一定是固定不变的,我现在的计划也不应该决定之后一年的规划。理想情况应该是每隔一段时间去审视一次计划完成情况,如果发现有另外的计划,或者某项计划不好,都是可以调整的。
CocoaPods对三方库的管理探究
- 15 Nov, 2020
CocoaPods是iOS开发中经常被用到的第三方库管理工具,我们有必要深入了解一下它对项目产生了什么影响,以及它是如何管理这些库的。使用pod安装三方库 我们新建一个不带测试模块的名为FFDemo的Swift项目,它的目录结构是这样的 ├── FFDemo │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── Base.lproj │ ├── Info.plist │ ├── SceneDelegate.swift │ └── ViewController.swift └── FFDemo.xcodeproj ├── project.pbxproj ├── project.xcworkspace └── xcuserdata然后我们执行pod init创建一个Podfile模板,在里面引入这两个三方库: target 'FFDemo' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! # Pods for FFDemo pod 'MJRefresh', '~> 3.5.0' pod 'Moya'end成功执行pod install之后我们就将这两个库引入到了项目,这时项目目录变成了这样: ├── FFDemo │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── Base.lproj │ ├── Info.plist │ ├── SceneDelegate.swift │ └── ViewController.swift ├── FFDemo.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ └── xcuserdata ├── FFDemo.xcworkspace │ └── contents.xcworkspacedata ├── Podfile ├── Podfile.lock └── Pods ├── Alamofire ├── Headers ├── Local\ Podspecs ├── MJRefresh ├── Manifest.lock ├── Moya ├── Pods.xcodeproj └── Target\ Support\ Files从目录看,除了pod init引入了Podfile,其余三部分内容:FFDemo.xcworkspace、Podfile.lock、Pods目录都是由pod install之后生成的。我们下面重点讲下这三部分内容。 CocoaPods安装的内容 xcworkspace文件 该文件下包含一个叫contents.xcworkspacedata的文件,它的内容是这样的: <?xml version="1.0" encoding="UTF-8"?> <Workspace version = "1.0"> <FileRef location = "group:FFDemo.xcodeproj"> </FileRef> <FileRef location = "group:Pods/Pods.xcodeproj"> </FileRef> </Workspace>使用xml格式将依赖包含在标签内。 xcworkspace是一个项目容器,当有多个project需要相互依赖时可以用xcworkspace将它们组织起来。pod在首次安装三方库时会生成一个叫Pods.xcodeproj的project管理三方库,然后将该project和主项目的project通过workspace进行关联。这样我们就可以在主工程里引入三方库了,而且三方库由Pods.xcodeproj统一管理,不会对我们原项目产生任何干扰。 Podfile.lock Podfile.lock文件的内容是这样的: PODS: - Alamofire (5.3.0) - MJRefresh (3.5.0) - Moya (14.0.0): - Moya/Core (= 14.0.0) - Moya/Core (14.0.0): - Alamofire (~> 5.0)DEPENDENCIES: - MJRefresh (~> 3.5.0) - MoyaSPEC REPOS: trunk: - Alamofire - MJRefresh - MoyaSPEC CHECKSUMS: Alamofire: 2c792affbdc2f18016e08fdbcacd60aebe1ba593 MJRefresh: 6afc955813966afb08305477dd7a0d9ad5e79a16 Moya: 5b45dacb75adb009f97fde91c204c1e565d31916PODFILE CHECKSUM: 073f3d6d9f03e6a76838ca3719df48ae6cc01450COCOAPODS: 1.9.3因为Podfile文件里可以不指定版本号,而版本信息又很重要,于是就有了Podfile.lock,它里面记录完整的版本信息和依赖关系。它的内容包含以下几大块 PODS PODS是指当前引用库的具体版本号,可以发现我们并没有引入Alamofire,但在PODS里确有它。这是因为Moya中依赖了它,Moya里定义了一个subspec叫Core,这是Moya/Core写法的由来。pod是通过各个库的podspec文件找到对应依赖的,这里可以简单看下Moya的部分podspeec文件内容Moya.podspec: Pod::Spec.new do |s| s.default_subspecs = "Core" s.subspec "Core" do |ss| ss.source_files = "Sources/Moya/", "Sources/Moya/Plugins/" ss.dependency "Alamofire", "~> 5.0" ss.framework = "Foundation" end endDEPENDENCIES DEPENDENCIES为pod库的描述信息,这里内容是同Podfile里的写法。因为我们指定了MJRefresh的版本号,并没有指定Moya的版本号,所以这里内容也是一样的。 SPEC REPOS 这里描述的是仓库信息,即安装了哪些三方库,他们来自于哪个仓库。 trunk是共有仓库的名称,它的地址是https://github.com/CocoaPods/Specs.git,外部使用的三方库大都来自于这里。通常我们还会依赖一些公司内部的私有库,私有库的信息也会显示在这里。 SPEC CHECKSUM 这里描述的是各个三方库的校验和,校验和的算法是对当前安装版本的三方库的podspec文件求SHA1。比如MJRefresh的校验和:6afc955813966afb08305477dd7a0d9ad5e79a16。我们安装的MJRefresh的版本为3.5.0,它在本地的podspec文件路径为:~/.cocoapods/repos/trunk/Specs/0/f/b/MJRefresh/3.5.0/MJRefresh.podspec.json。 这个路径可以通过在安装库时增加 --verbose参数在输出日志里查看。我们对该文件内容通过openssl求sha1摘要: $ pod ipc spec ~/.cocoapods/repos/trunk/Specs/0/f/b/MJRefresh/3.5.0/MJRefresh.podspec.json | openssl sha1 $ 6afc955813966afb08305477dd7a0d9ad5e79a16因为是对podspec.json内容求sha1,所以只要内容发生一点变化,得出的校验和就将大不相同,而这也是校验和设计的目的:podspec文件发生变化意味着版本信息发生了变化,就需要重新同步代码。 大家可能注意到了,我们通常制作私有pod,控制配置信息的文件是podspec格式的,为什么本地文件变成了json格式? 这是因为json格式兼容性更高也更容易批量处理,官方Spec仓库的所有库配置文件都是被转成json格式的。在我们制作私有库的时候是可以直接以podspec的格式推到远程仓库的,但后续解析文件时pod内部检索还是会把它转成json格式。上面的命令是包含了podsepc转json的命令的,转json命令如下: $ pod ipc spec ModuleName.podspecPODFILE CHECKSUM 这个校验和是针对Podfile内容的校验和,如果Podfile内容改变了,该值也会跟着改变。计算方法为: $ openssl sha1 filePath/PodfileCOCOAPODS: 1.9.3 这个代表当前使用的CocoaPod版本号,远程版本管理应该要保证大家使用的pod版本号一致。 Pods Manifest.lock Manifest.lock是Podfile.lock的副本,它是在Pods目录里面。它的作用是这样的,我们通常是不把Pods文件放到版本管理里面,而把Podfile.lock放到版本管理里面。这时对于拉取代码之后是否需要更新pod,就可以通过对比本地的Manifest.lock和远程Podfile.lock是否相同即可。 Targets Support Files Pods安装的依赖是这样的组织形式一个Pods的Project下面有三个Targets,其中三个是安装的依赖库,最后一个Pods-FFDemo是关联三个库的Framework,也即是Pods这个Project的Targets。 Pods-Demo Framework 先看这个Demo的Framework,它会被用于工程项目的引用依赖这个库不会被打进包里,因为Do Not Embed代表并不是包含的关系。 这个工程下的配置文件有这些:许可协议文件 两个以acknowledgements命名的文件是用于管理pod库的许可协议,即三方库必须带有的LICENSE文件,这也是为什么我们在制作pod时会要求我们指定软件协议。 Framework文件 这里还包含了用于管理Module的modulemap和umbrella.h文件。modulemap是对Module的声明文件,制作Framework我们总是需要该文件,它的内容如下: framework module Pods_FFDemo { umbrella header "Pods-FFDemo-umbrella.h" export * module * { export * } }其指向了一个umbrella的头文件,这是制作Framework必须的头文件,modulemap和umbrella.h会在创建Module时自动生成,不建议手动修改其关系。 dummy.m文件 这其实是一个空的.m文件 #import <Foundation/Foundation.h> @interface PodsDummy_Pods_FFDemo : NSObject @end @implementation PodsDummy_Pods_FFDemo @end那为什么要有这个东西呢,包括所有的三方库的包里也会包含一个dummy文件。我在stackoverflow找到了一个解释:Xcode的编译是依赖.m文件的,如果一个库里没有.m文件,将不会被编译,为了防止这种情况就会在每个库里增加一个空的.m文件。 xcconfig文件 xcconfig文件是Build Setting配置项的文件形式,它的优先级大于Xcode内的Build Setting。看一个pod生成的debug模式下的xcconfig文件。 ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "${PODS_CONFIGURATION_BUILD_DIR}/Moya" GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Moya/Moya.framework/Headers" LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' OTHER_LDFLAGS = $(inherited) -framework "Alamofire" -framework "CFNetwork" -framework "Foundation" -framework "MJRefresh" -framework "Moya" OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_PODFILE_DIR_PATH = ${SRCROOT}/. PODS_ROOT = ${SRCROOT}/Pods USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YESxcconfig还有个作用是设置参数,比如我们比较熟悉的PODS_ROOT=${SRCROOT}/PODS,它代表项目根目录下的PODS文件目录。另外两项用于帮助我们在项目中查找三方库的FRAMEWORK_SEARCH_PATHS和HEADER_SEARCH_PATHS也是在该文件内部定义的,这些配置会体现到Build Settings里面:三方库的Framework 各个三方库也都有一些配置文件,他们文件格式基本一致,文件作用跟上面介绍的类似,下图是Moya的配置文件,Xcode中Pods > Pods > Moya > Support Files对应的文件就是该内容。我们可以想一个问题,当安装的第三方库需要依赖于别的库时它是如何去找这个库的呢?Moya是需要使用Alamofire的API的,会有import Alamofire的操作。凭借上面的内容,可以得知Framework的引用是需要在Build Setting里提前该Target,有哪些引用项的。所以这也是Framework里xcconfig文件的作用,可以在Moya的xcconfig文件里找到这个: FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire"而且引用的是跟主项目同一个Alamofire的路径。 Build Phases这里是设置编译阶段配置的地方,当首次pod install成功之后,这里会多几个[CP]开头的配置项(CP即CocoaPods缩写),它们都是由CocoPods添加的脚本内容,执行顺序从上到下。 New System Build 在讲编译脚本之前简单说下New Build System。 New Build System是Xcode10之后苹果推出的新的构建系统,新的构建系统对编译流程的优化做了很多工作,虽然到Xcode12仍兼容旧版的Legacy Build System,但其已经被标记为移除,我们的项目和库都应该使用新版的构建系统进行构建。和新的构建系统随之而来的是在运行脚本时增加的输入输出列表。这是为了控制是否每次编译都需要执行对应脚本,input和output文件可以是单个文件形式,如果文件过多可以放到格式为xcfilelist的文件列表里。 如果没有提供input和output,则每次构建都会运行该脚本。如果提供了,则会在以前从未运行过、某个输入文件被更改或某个输出文件丢失的情况下再次运行。 注意这些是构建脚本的默认逻辑,Xcode还提供了Run Scripts的自定义行为,默认勾选项:Based on dependency analysis,即代表上述逻辑。如果提供了输入输出还需要每次运行,关闭该选项即可。 [CP] Check Pods Manifest.lock 该脚本位于较上方,如果没有Dependencies,开始编译就会执行该脚本,它的内容如下: diff "${PODS_PODFILE_DIR_PATH}/Podfile.lock" "${PODS_ROOT}/Manifest.lock" > /dev/null if [ $? != 0 ] ; then # print error to STDERR echo "error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation." >&2 exit 1 fi # This output is used by Xcode 'outputs' to avoid re-running this script phase. echo "SUCCESS" > "${SCRIPT_OUTPUT_FILE_0}"作用是比较Podfile.lock和Manifest.lock文件是否相同,如果不同就输出错误信息:error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.,并执行退出,这会导致后续项目报错,无法继续编译。 该错误较常见,出现于拉取远端代码,远端pod依赖于本地不一致的情况。这时我们可以根据提示,执行pod install命令,根据Podfile及远端Podfile.lock生成新的Manifest.lock文件。 [CP] Copy Pods Resources 这个一般在以静态库引入的三方库切里面包含资源的话会添加该脚本,其作用是将三方库的资源文件拷贝至项目中。 它的完成是通过运行以下脚本进行的: "${PODS_ROOT}/Target Support Files/Pods-FFDemo/Pods-FFDemo-resources.sh"Pods-FFDemo-resources.sh文件在Pods目录内,该脚本内有个关键函数install_resource: install_resource() { if [[ "$1" = /* ]] ; then RESOURCE_PATH="$1" else RESOURCE_PATH="${PODS_ROOT}/$1" fi if [[ ! -e "$RESOURCE_PATH" ]] ; then cat << EOM error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. EOM exit 1 fi case $RESOURCE_PATH in *.storyboard) ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} ;; *.xib) ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} ;; *.framework) echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" ;; *.xcassets) ABSOLUTE_XCASSET_FILE="$RESOURCE_PATH" XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") ;; *) echo "$RESOURCE_PATH" || true echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" ;; esac }删除了一部分日志内容,其内部主要是一个switch语句,根据资源文件的类型进行不同的同步操作。这里重点说下几种重要格式文件的处理方式。 storyboard和xib格式 这两项资源文件是需要编译处理的,利用ibtool命令分别转成sotryboardc和nib格式。 xcassets格式 这里的图片最终会被打包到Assets.car供程序使用,需要使用actool。 Bundle、plist、png等资源 其他类的资源是会走到switch语句最后出口,进行资源路径赋值给$RESOURCES_TO_COPY,在后面的代码中通过rsync命令,将资源同步到构建包的目录。 该脚本会打印很多日志,在使用CocoaPods时如果遇到资源相关的问题都可以遵循错误日志来这里推测定位错误原因。 [CP] Embed Pods Frameworks 该处脚本是直接运行Pods-FFDemo-frameworks.sh。 "${PODS_ROOT}/Target Support Files/Pods-FFDemo/Pods-FFDemo-frameworks.sh"可能你还记得上面说的pod会把多个库的依赖做成一个合并的库,但该库是以依赖的形式引入主工程,但是程序的运行时需要这些库,我们打包时就需要将各个库Embed到项目里,而做这个工作的就是该脚本。 # Copies and strips a vendored framework install_framework() { rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" # other code... # Strip invalid architectures so "fat" simulator / device frameworks work on device if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then strip_invalid_archs "$binary" fi # Resign the code if required by the build settings to avoid unstable apps code_sign_if_enabled "${destination}/$(basename "$1")" }脚本内容主要是调用install_framework函数,将framework内容同步到构建包里。在该函数里还有几个关键方法,strip_invalid_archs用于去除无用架构,code_sign_if_enabled用于framwork签名。
博客搭建介绍及腾讯云服务器配置
前置说明 一直以来都是用GithubPage搭建的博客,因为服务器在国外,访问速度一直比较慢,再后来有一批服务器被墙掉了导致国内网络环境直接无法访问。这里可以多说一句,GithubPage跟Github用的可以不是同一IP地址服务器,被墙很多是通过锁定IP进行的,GithubPage搭建博客有些可以访问有些不可以,就是因为某些IP被封了,如果更换IP也是可以解决的,但访问速度还是不够快。前段时间看到腾讯云服务器打折,就考虑如果想保证国内的访问速度还是要将服务器迁移到国内来。 这篇文章的作用是对搭建博客做一个整体介绍,对博客中用到的方案进行一些比较,有些会写的比较简略还需要再转到别文章去看,另外重点会写一下迁移腾讯云及域名备案需要做的具体操作。 博客的搭建一般需要关注这五个部分:博客框架、域名、服务器、图床、Markdown工具。各个环节都有免费和付费版本的选择,一般来说免费能满足基本需求,付费则能提供更优质的体验,如何选择就看大家的需求了。这五部分的对比如下:付费版本 免费版本博客框架 Wordpress(部分内容付费) Hexo/Jekyll域名 万网/腾讯云 freenom/dot.tk内容托管平台 阿里云/腾讯云 GithubPage/GiteePage图床 七牛/又拍 Github/GiteeMarkdown工具 MWeb Typora接下来会围绕这五部分展开说明。博客框架 当前热门的方案是Hexo和Jekyll,Hexo是基于Node.js开发的,Jekyll是基于Ruby开发的,他们的作用都是将Markdown语法的内容转译成HTML,并根据配置的主题进行显示。 Wordpress 功能更强大,且相比前两个方案更简单,它可以不依赖Markdown语法,直接选用模板进行线上编辑即可。我对Wordpress摸索的较少,它的优缺点不便于多说,但还是推荐大家拿来试试。 这里介绍几个使用上述方案搭架的博客,大家可以参考看下: Jekyll:https://onevcat.com/,https://draveness.me/ Hexo:http://blog.sunnyxx.com/,https://zhangferry.com/ Wordpress:http://blog.cnbang.net/ 网站样式的差别不是由框架决定的,而是主题,以上三者都有大量的主题可供选择。我使用的是Hexo,使用方法可以参考这篇:基于Hexo搭建自己的博客小屋。 域名 域名选择 免费版本 免费版本域名,像freenom和dot.tk,一般都有固定的使用时间,一般为一年左右,超过一年还想使用就需要付费了。 如果依赖GithubPage 或者 GiteePage 这样的托管服务,我们是可以设置像username.github.io或username.gitee.io这样的域名。 如果想尝试GithubPage的话,也可以参考这篇基于Hexo搭建自己的博客小屋 付费版本付费版本的域名可以是我们常见的.com、.cn、.net等,可以到万网或者腾讯云进行购买,一般可选1-10年使用期限。 域名备案 不是所有域名都需要备案的,如果网站服务器在国外是不需要备案的,在国内是需要备案的。GithubPage服务器在国外,使用域名不需要备案;如果要使用国内服务器,腾讯云或者阿里云等则需要备案。 以下是腾讯云的域名备案流程:主要步骤都可以通过备案小程序进行。 备案申请条件首先会提交到腾讯云后台,由他们进行一个初审,如果没有通过他们会联系你,并告知你哪里需要修改,修改之后可以再次提交。 我在初审时遇到了两个问题: 1、域名备案时需要是不可访问状态,如果当前正在使用,需要停止解析。 2、网站描述不能包含博客字样,可以用学习笔记代替。 腾讯云的初审通过之后,他们会将备案信息提交至管局。管局的审核会久一些,一般是20个工作日之内,我的审核时间用了16天。 备案号 审核通过之后会获得一个备案号,我们需要在网站底部加上这个备案号并附带链接到域名信息备案管理系统,备案号可以在腾讯云的备案管理界面查看。这里会会有两个地方显示备案号:主体信息和已备案网站。主体信息是自然人或单位的意思,已备案网站是针对网站的,因为一个主体可能会用到多个网站,所以网站备案号是以主体备案号为基础加上自然序号。如果再次备案域名,新的备案号会是主体备案号-2的形式。我们网站中出现的备案号应该是网站备案号,即京ICP备2020035857号-1。 接下来需要在网站中添加备案号,如果你使用的是Hexo或者Jeklly框架,可以在主题文件目录下搜索Powered by,找到对应的底栏配置文件。我使用的Icarus主题,其底部文件为footer.jsx,找到对应位置添加: <a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener"> 京ICP备2020035857号-1</a>效果如下:公安备案 在网站开通后的30日之内还需要进行公安备案,登录网站全国互联网安全管理服务平台,填写信息。因为是政府网站,开代理一般无法访问,需要关闭代理,如果仍无法访问,可能是DNS污染了,可以将DNS服务器设置成114.114.114.114再次刷新网站。登录之后注册、登录并填写开办者信息:这里会有很多坑,我尝试了很多次均已失败告终,直到现在也没成功,如果有知道怎么搞定的小伙伴可以告诉我,以下记录一些解决的坑:图片选择时需要开启Adobe Flash 对身份证的识别好像是通过图片比例进行的,拍出来的身份证照片需要把多余的内容剪裁掉,只保留身份证主体内容 手持身份证照片如果不能验证通过可以按照实例图片剪裁成一定比例或者换个白色背景拍照 常住地址第二和第三个选项框有时无法弹出,这个只能不断刷新尝试,我隔几天再试地区弹框才能展开 填完之后点提交审核一直加载,1分钟之后还是卡着不动,试了几次都是同样的问题,这一步一直没成功公安备案我看很多网站也没有写,所以暂时也不打算管了。这里可以告诉大家公安备案是指什么,以掘金为例:上面箭头指向的津ICP备是网站备案号,京公网安备指的是公安备案,下面这个即是公安的备案号,很多博客网站都没有写好像重要性不是很大。 域名解析 域名解析可以使用腾讯的DNSPod,因为我之前做过从GithubPage服务器的域名解析,这里只需将原来指向GithubPage的服务器IP地址该为自己的服务器地址即可。 关于域名的配置可以参考我之前的一篇博客:为博客设一个自定义域名 以下是我的域名解析配置,被擦掉的内容为服务器IP地址:全球递归DNS服务器的刷新不超过72小时,我们验证解析是否生效可以通过 ping zhangferry.com命令进行,如果展示的ip地址是我们修改后的ip地址,就证明域名解析已经生效。 服务器 云服务器选择 分为免费和付费版本,其实免费版不属于服务器,只不过起到一个服务器托管的作用。 免费版本 Github的GithubPage和码云的GiteePage是两个比较常见的静态网页托管服务。这里简单总结下两者的特点:对比项 GithubPage GiteePage默认域名 username.github.io username.gitee.io自定义域名 支持 免费版不支持,付费版支持访问速度 国内无法访问,翻墙之后访问速度也较慢 国内可访问,但访问速度较慢热度 较高,有很多开发者使用这个方案 较低,使用者较少如果你刚打算写博客,想尝尝鲜,我的建议是先从这俩平台中选一个,玩一玩。如果有长期维护的打算,并期望保证一定的用户体验,还是得购买服务器的。 付费版本 可供选择的阿里云和腾讯云,我选的是腾讯云,阿里云也是同理。 购买时我们需要指定操作系统,服务器的话一般都是Linux。有两种比较流行的Linux发行版:Ubuntu和CentOS,这两者在包管理工具上是不一样,在后面的安装工具过程中需要注意这一点。Ubuntu上用的包管理工具是apt-get,CentOS用的包管理工具是yum。 我选择的是CentOS,服务器创建完之后就可以登录了。腾讯云有两种登录方式 1、通过腾讯云网页端SSH登录,首次登录需设置密码,登录成功之后会进入一个远程终端页面,这里的登录时通过root用户登录的。 2、本机SSH登录,通过ssh root@host-ip的形式进行登录,之后需要输入首次登录创建的root用户密码。本机SSH可选择其他用户登录。 服务器配置 之后的配置和命令设置有些是在远程服务器操作的,有些是本地操作,下面会以remote$和local$开头进行区分,ip地址以host-ip的形式展示,用户名以zhangferry的形式展示。 安装依赖与Git 首先登录服务器,安装一些依赖工具。 1、安装依赖库 remote$ yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel2、安装编译工具 remote$ yum install gcc perl-ExtUtils-MakeMaker package3、安装Git 虽然Linux发行版一般都装有Git,但是很多版本都比较旧,我们需要一个相对新的Git版本,所以需要安装最新版本的Git。 查看Git版本 remote$ git --version如果看到的版本是2.0之前的,那就升下级吧。 删除旧版Git remote$ yum remove git由于yum跟git版本更新不一致,可以选择源码编译升级,如果嫌麻烦也可以选择yum升级。当前最新版本是2.28.0。如果选择yum升级可以使用yum install git,逃过源码升级这一步即可。 源码升级 选择一个目录来存放下载下来的 git 安装包。这里选择了/usr/local/src 目录,我们进入到这个目录 remote$ cd usr/local/src下载Git压缩包至这个目录 remote$ wget http://ftp.ntu.edu.tw/software/scm/git/git-2.28.0.tar.gz解压到当前目录 remote$ tar -zvxf git-2.28.0.tar.gz进入解压的目录,并编译 remote$ cd git-2.28.0 remote$ make prefix=/usr/local/git all安装 git 到 /usr/local/git 目录下 remote$ make prefix=/usr/local/git install设置Git环境变量,即可以在命令行找到Git命令,你可以试下 remote$ git --version如果看到的版本号是我们安装的2.28.0,即证明当前环境变量设置正确,如果现实comond not found,我们需要修改环境变量使终端能够找到git命令。 使用vim打开 remote$ vi /etc/profile按i进入编辑模式,在文件末尾增加下面内容 PATH=$PATH:/usr/local/git/bin # git 的目录 export PATH保存退出再次查看git版本号,应该就变成我们安装的版本了。 创建新用户 创建用户的目的是为了便于分配权限,该用户用于远程登录,并推送网站内容。我们也可以直接用root用于进行直接登录,如果嫌创建用户麻烦,可以跳过这一步。 创建用户zhangferry,并设置连接密码: remote$ adduser zhangferry remote$ passwd zhangferry #该命令回车输入密码,有二次确认获取sudoers文件的编辑权限 remote$ chmod 740 /etc/sudoers remote$ vim /etc/sudoers按 i 键进入文件的编辑模式,按向下键找到如下字段: root ALL=(ALL) ALL在其后面增加一句: zhangferry ALL=(ALL) ALL即我们新建的用户拥有了超级用户的权限,之后退回sudoers文件的编辑权限: remote$ chmod 400 /etc/sudoers切换用户的命令是: remote$ su zhangferry #su root切换root用户这时我们可以在本机使用ssh命令,试下是否可以通过zhangferry用户成功登录,需要我们输入刚才为zhangferry用户设置的密码: local$ ssh zhangferry@host-ip每次输入密码很麻烦,可以通过秘钥,进行免密登录。 SSH登录 这一步是非必须的,但推荐这样做,这会省去我们后面输入密码的操作。 1、本地生成秘钥,一般指定RSA加密算法,会生成一对公钥私钥。 local$ ssh-keygen -t rsa -C "zhangferry11@gmail.com" #这里邮箱需要换成自己的如果已经生成过,不要再次生成了,因为它会覆盖我们原有的秘钥,导致之前的秘钥配置失效。检验方式是: local$ cat ~/.ssh/id_rsa.pub秘钥的位置是在.ssh文件夹内,如果有输出内容说明我们已经生成过。 2、复制ssh公钥至远程主机的~/ .ssh/authorized_key文件里 local$ ssh-copy-id root@host-ip如果提示失败或者无文件,需要手动生成同名文件,将内容复制进去即可。 3、测试连接 local$ ssh zhangferry@host-ip如果命令行左侧内容显示为带有[centos]或者标记我们远程服务器的字样即代表登录成功。创建git仓库和网站目录 1、这一步的目的是创建git仓库,本地博客内容需要推送至这里。 remote$ su root #在root权限里操作 remote$ mkdir /home/zhangferry remote$ mkdir /home/hexo #网站目录 remote$ cd /home/zhangferry remote$ git init --bare blog.git #创建git裸仓库裸仓库的意思就是只保留git提交记录,不保留文件内容。 2、创建一个git的post hook,用于自动部署。这一步的作用是在我们往服务器推送仓库时,将内容转移到网站目录,因为裸仓库是不包含内容的。 remote$ vi blog.git/hooks/post-receivehooks文件是git自动生成的目录,里面有很多用于编辑hook的示例,其中post-receive用于在push操作的hook。它的内容如下: #!/bin/sh git --work-tree=/home/hexo --git-dir=/home/zhangferry/blog.git checkout -f这里需要注意,work-tree对应工作区的目录即网站目录,git-dir对应git仓库位置,如果你有自定义的命名,记得修改这两处地方。 这里大家可以想一下为什么要将git目录和网站目录分开处理,如果不分开也是可以的。我的理解是分开是为了便于权限管理,git的操作一般会是一个单独的用户权限,很多人都可以操作git,它对网站的影响是间接的,而网站内容的权限应该更高一些,只有root才可以直接操作。所以就将两者分开了。 3、修改git仓库权限 只需要将git仓库放权给zhangferry用户,网站目录的更新由post hook完成。 remote$ cd /home/zhangferry remote$ chown zhangferry:zhangferry -R blog.git # blog.git的拥有者为zhangferry remote$ chmod +x blog.git/hooks/post-receive # 修改post-receive为可执行安装Nginx Nginx是一个高性能的HTTP和反向代理web服务器,这里我们只将其作为HTTP web服务器使用。 1、安装nginx remote$ yum install -y nginx2、修改配置文件 remote$ vi /etc/nginx/nginx.conf修改内容如下: server { listen 80 default_server; listen [::]:80 default_server; server_name zhangferry.com; #博客域名 root /home/hexo; #网站目录 # Load configuration files for the default server block. include /etc/nginx/default.d/*.conf; location / { } error_page 404 /404.html; location = /40x.html { } error_page 500 502 503 504 /50x.html; location = /50x.html { } }我们只需要修改server里的server_name和root选项,他们分别代表博客域名和网站目录。 listen代表监听端口,http默认监听的是80端口。error_page代表出错之后的展示页面,无数据的40x.html和服务器错误的50x.html都可以在这里指定。 3、nginx的几个命令 remote$ start nginx #启动nginx remote$ nginx -t #检测配置文件是否有语法错误 remote$ nginx -s reopen #重启nginx remote$ nginx -s stop #退出nginx正常的流程应该是当我们修改完nginx的配置文件,先用nginx -t检测下是否有错误的配置,它还会检测一些待修复但不影响运行的警告信息,如果有错误需要修改再次检测。当配置没有问题了需要启动nginx:start nginx,之后的配置修改需要重启nginx:nginx -s reopen。 4、启动检测 nginx启动之后我们就可以在本地浏览器输入http://host-ip:80 进行访问,这时我们看到的应该是一个nginx的缺省页,因为/home/hexo目录并没有任何内容。 上传Hexo博客内容至腾讯云 修改博客根目录里_config.yml文件的deploy配置: deploy: type: git repo: zhangferry@host-ip:/home/zhangferry/blog.git #上面配置的裸git目录 branch: master部署hexo,并推送: local$ hexo g local$ hexo d #发布hexo会触发push操作,将repo推送至我们配置的git路径有可能你会出现bash: git-receive-pack: command not found这样的错误,command not found是一种常见的问题,上面的意思是在服务器方git-receive-pack命令无法找到。简单扩展下,git push内部分为两步,本地运行send-pack,它会判断哪些提交记录是它有但服务器没有的,然后它告知服务器端的receive-pack本次更新内容,receive-pack接收推送来的数据。网上对这一问题的解决方式是通过ln关联git-receive-pack: remote$ sudo ln -s /usr/local/git/bin/git-receive-pack /usr/bin/git-receive-pack/usr/local/git/bin/git-receive-pack为实际的命令路径,/usr/bin/git-receive-pack为创建的快捷路径,但我并不能通过这个方法解决。 但从错误本身出发,命令安装了却找不到一般都是环境变量的问题,我直接修改环境变量: remote$ su zhangferry #如果使用子用户推送的,需要切换至该用户 remote$ vi ~/.bashrc在.bashrc底部增加export PATH=$PATH:/usr/local/git/bin,然后更新环境变量: remote$ source ~/.bashrc再次部署hexo,应该就会成功了。 local$ hexo d我一开始想到了修改环境变量,但是在root下修改的,而root和zhangferry是两个隔离的环境变量配置! 关于环境变量文件之间的关系有一张图可供参考:/etc/bashrc目录下的配置是全局的,~/.bashrc 下的配置只针对当前登录的用户。所以对于上面环境变量的修改,我们在全局或者zhangferry用户下修改都是可以的。 推送成功之后再次浏览器访问http://host-ip:80,如果我们域名DNS解析生效,直接访问http://zhangferry.com也一样,如果能正常显示博客首页即证明我们迁移成功了。 HTTPS支持 这一步是非必须的,如果你不打算使网站支持https到这里就可以结束了。支持HTTPS需要三步: 1、申请SSL证书 2、将证书上传至服务器 3、配置Nginx关联SSL证书 以下是对这三步的详细说明。 申请SSL证书 腾讯云有免费的https证书可以申请,有效期只有一年,过期需要再申请。这个申请流程很快,一般1个小时左右,如果时间过长,比如超过了一天,可以再次申请,这样他们会很快通过你的申请,并打回最早那次申请(我就是遇到了这种情况0。0)。 上传证书至服务器 证书通过之后有一个下载按钮,下载下来是一个包含多种服务器类型的证书文件。我是使用Nginx搭建的服务器,所以找到Nginx目录里的证书文件,它有两个:.crt后缀和.key后缀。 1、我们还需要在Nginx目录创建一个文件夹,用于存放这些证书: remote$ cd /etc/nginx/ remote$ mkdir ssl2、使用scp命令上传本地证书文件至服务器: local$ scp /path/filename zhangferry@host-ip:/etc/nginx/ssl/修改Nginx配置 网上相关教程很多,都是上来就告诉你怎么改,并没有说明其中的关系和原因,这样很容易因为一些配置环境不同导致错误。 我简单总结下:nginx有一个默认配置是通过nginx.conf控制,我们可以将证书配置写到这里面。但一个服务器允许配置多个域名,只有一个配置文件是不够的,解决方案是在conf.d目录下建一个以域名命令的.conf配置文件,它可以用来管理单独的域名,这两个配置文件是同级别的会一起生效。 我是移除了原有nginx.conf的配置项内容,将端口和证书的配置都写到了conf.d下的zhangferry.conf里。 nginx.conf内容: server { listen 81 default_server; listen [::]:81 default_server; # Load configuration files for the default server block. include /etc/nginx/default.d/*.conf; location / { } error_page 404 /404.html; location = /40x.html { } error_page 500 502 503 504 /50x.html; location = /50x.html { } }这里移除了server_name和root,因为我们要在zhangferry.conf里配置监听,所以这里的配置项都去掉;将监听端口改成了81,是为了避免两处配置一样的端口引起警告。 zhangferry.conf内容: server { listen 80; server_name zhangferry.com; root /home/hexo; #重定向设置,如果使用http访问会重定向至同路径的https链接 rewrite ^(.*)$ https://$host$1 permanent; }server { listen 443 ssl; #证书位置,需跟自己证书位置对应 ssl_certificate /etc/nginx/ssl/1_host.com_bundle.crt; ssl_certificate_key /etc/nginx/ssl/2_host.com.key; #绑定域名 server_name zhangferry.com; root /home/hexo; #ssl配置,以下几项必须要有 ssl_session_cache shared:SSL:1m; ssl_session_timeout 5m; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; location / { } }分别为80和443接口进行配置,配置完之后通过以下命令验证配置是否有问题: remote$ nginx -t -c /etc/nginx/nginx.conf如果出现test is successful字样即是没问题。 重启nginx服务器: remote$ nginx -s reopen访问https://zhangferry.com和http://zhangferry.com验证是否生效,如果出现加锁即证明配置成功了:图床 图床选择 图床一定要考虑持久性,像是免费的临时方案和存到一些非图床的网站上都是不靠谱的。我们访问某些博客时应该遇到过图片开裂无法查看的情况,这些都是因为之前使用的图片存储被删除或者被更改了位置导致的。我的博客之前是用的简书的图床,现在很多也都无法查看了。所以下面介绍的方案会优先考虑存储的稳定性和持久性。 免费版本 免费版有Github和Gitee可供选择,即把git仓库当做图片存储。 Github做图床时加载速度会比较慢,这个可以通过 jsDeliver 这个CDN服务进行解决,我之前很多github的图片链接都换成了通过jsDeliver的CDN链接。 另外一个方案是Gitee,这个正常使用就可以了,因为服务器在国内,访问速度还是比较快的。但Gitee有一个限制,图片超过1M就无法显示了,所以对于一些特定图片我们需要做好剪裁和缩放。 Github+jsDeliver和Gitee都是比较推荐的图床方案。 付费版本 有七牛云,又拍云,腾讯云可供选择,付费版能提供更快的访问速度和更高的稳定性,但因为上面的免费方案已经满足我的需求了,所以就没做这些尝试,不同图床方案使用起来基本一致。 图床工具 图床的使用最好搭配一些趁手的工具,使我们可以通过截屏,拖动等操作轻易的上传指定图片至图床,这个工具就是PicGo。有一个配置教程可以参考:一次艰难的图床选择经历(MWeb+PicGo+Github)。 这个是一年前的文章,现在我使用的方案Typora+PicGo+Gitee,他们之间并没有太大的变化。 Markdown工具我之前使用的是MWeb,但MWeb功能有点太多了,直到发现Typora,我一眼就相中了这个界面简洁的Markdown编辑工具,关键还是免费的,爱了爱了。别看Typora界面简洁其实功能不少,它可以搭配PicGo插件一起使用,使图片的管理更加简便。因为是工具型东西,就不写教程了,大家多多摸索应该就会了。
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面试总结(2020年6月)参考答案
- 17 Aug, 2020
上个月发了这篇iOS面试总结(2020年6月),没想到挺受大家欢迎,本来是没打算为它写答案,但有几个人建议我最好出一篇答案,提的人多了我就答应了下来。因为最近比较忙,断断续续总算补完了,就有了这篇文章,希望它对大家还有用处。这些都属于参考答案,如果大家感觉有不对不准确的地方也欢迎指出,我会及时更新。关于面试题 打个比方,如果把找工作理解成考大学,面试就是高考,市面上的“真题”就是模拟试卷。我们会很容易倾向于在面试前寻找对应公司的面试“真题”,重点准备,期待“押题”成功。但实际上,即使面试同一家公司,它会有不同部门,不同业务线,不同面试官,即使遇到同一面试官,他也不一定就每次考察完全一样的内容。想想高考中那些考的好的同学,他们肯定不是靠“押题”才能取得好成绩吧,他们大多靠的是平常积累及对知识点灵活掌握,那面试也一样啊。执着于搜题,把面试题当做重点进行“复习”,还不如自己划出“考纲”,各个知识点逐一检查掌握情况,复习的更全面呢。 我对于面试题的看法一直是相对保守的,这类文章一般只是内容搬运,它会存在一些偏差和误读,最重要的那就是几道题往那一扔,并没有产出有价值的东西。这也是为什么我上篇面试总结,会加了一些面试技巧,整理面试题时,也没提他们是出自哪家公司,就是不希望大家把题目区别看待。 说了这些并不是说面试题没用啊,而是希望大家不要迷信面试题,更多地去关注那些有质量有深度的技术文章。面试考核的是知识点而不是具体的某些题目,面试题的作用在于,衡量我们的知识掌握情况,便于我们查漏补缺,越说越像是针对一次“考试”了😄。 总结不易,希望这份参考答案能对你有所帮助,如果想持续关注我,欢迎订阅微信公众号:iOS成长之路。 面试题及参考答案 Swift 1、Swift中struct和class有什么区别? struct是值引用,更轻量,存放于栈区,class是类型引用,存放于堆区。struct无法继承,class可继承。 2、Swift中的方法调用有哪些形式? 答:直接派发、函数表派发、消息机制派发。派发方式受声明位置,引用类型,特定行为的影响。为什么Swift有这么多派发形式?为了效率。 参考文章:深入理解 Swift 派发机制3、Swift和OC有什么区别? Swift和OC的区别有很多,这里简要总结这几条:Swift Objective-C语言特性 静态语言,更加安全 动态语言,不那么安全语法 更精简 冗长命名空间 有 无方法调用 直接调用,函数表调用,消息转发 消息转发泛型/元组/高阶函数 有 无语言效率 性能更高,速度更快 略低文件特性 .swift 单文件 .h/.m包含头文件编程特性 可以更好的实现函数式编程/响应式编程 面向对象编程4、从OC向Swift迁移的时候遇到过什么问题? 可以参考这篇文章:OC项目转Swift指南 里的混编注意事项。 5、怎么理解面向协议编程? 面向对象是以对象的视角观察整体结构,万物皆为对象。 面向协议则是用协议的方式组织各个类的关系,Swift底层几乎所有类都构建在协议之上。 面向协议能够解决面向对象的菱形继承,横切关注点和动态派发的安全性等问题。 参考喵神的面向协议编程与 Cocoa 的邂逅 (上) OC语法 1、Block是如何实现的?Block对应的数据结构是什么样子的?__block的作用是什么?它对应的数据结构又是什么样子的? block本质是一个对象,底层用struct实现。 数据结构如下: struct Block_descriptor { unsigned long int reserved; unsigned long int size; void (*copy)(void *dst, void *src); void (*dispose)(void *); };struct Block_layout { void *isa; int flags; int reserved; void (*invoke)(void *, ...); struct Block_descriptor *descriptor; /* Imported variables. */ };isa 指针,所有对象都有该指针,用于实现对象相关的功能。flags,用于按 bit 位表示一些 block 的附加信息,本文后面介绍 block copy 的实现代码可以看到对该变量的使用。reserved,保留变量。invoke,函数指针,指向具体的 block 实现的函数调用地址。descriptor, 表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针。variables,capture 过来的变量,block 能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。__block的作用是让block可以捕获该变量,捕获之后的变量会进入到block内部,通过反编译的代码我们可以看到该对象是这样的: struct __Block_byref_i_0 { void *__isa; __Block_byref_i_0 *__forwarding; int __flags; int __size; int val; //变量名 };对于block的深入了解,可以参考《Objective-C高级编程》第二章或者唐巧的这篇谈Objective-C block的实现 2、GCD中的Block是在堆上还是栈上? 堆上。可以通过block的isa指针确认。 3、NSCoding协议是干什么用的? 一种编码协议,归档时和解档时需要依赖该协议定义的编码和解码方法。Foundation和Cocoa Touch中的大部分类都遵循了这个协议,一般被NSKeyedArchiver做自定义对象持久化时使用。 4、KVO的实现原理 利用Runtime生成一个中间对象,让原对象的isa指针指向它,然后重写setter方法,插入willChangeValueForKey和didChangeValueForKey方法。当属性变化时会调用,会调用这两个方法通知到外界属性变化。 5、NSOperation有哪些特性,比着GCD有哪些优点,它有哪些API? NSOperation是对GCD的封装,具有面向对象的特点,可以更方便的进行封装,可以设置依赖关系。 API可以查看NSOperation文档。 6、NSNotificaiton是同步还是异步的,如果发通知时在子线程,接收在哪个线程? 同步。子线程。 UI 1、事件响应链是如何传递的? 手势的点击会发生两个重要事情,事件传递和事件响应。 事件传递:从UIApplication开始,到window,再逐步往下层(子视图)找,直到找到最深层的子视图,其为first responder。用到的判断方法是pointInside:withEvent和hitTest:withEvent。 事件响应:从识别到的视图(first responder)开始验证能否响应事件,如果不能就交给其上层(父视图)视图,如果能相应将不再往下传递,如果直到找到UIApplication层还没有相应,那就忽略盖茨点击。用到的判断方法是touchesBegan:withEvent、touchesMoved:withEvent等。 这两个过程大致的相反的。 2、什么是异步渲染? 异步渲染就是在子线程进行绘制,然后拿到主线程显示。 UIView的显示是通过CALayer实现的,CALayer的显示则是通过contents进行的。异步渲染的实现原理是当我们改变UIView的frame时,会调用layer的setNeedsDisplay,然后调用layer的display方法。我们不能在非主线程将内容绘制到layer的context上,但我们单独开一个子线程通过CGBitmapContextCreateImage()绘制内容,绘制完成之后切回主线程,将内容赋值到contents上。 这个步骤可以参照YYText中YYTextAsyncLayer.m文件中的实现方式。 3、layoutsubviews是在什么时机调用的?init初始化不会触发。addSubview时。设置frame且前后值变化,frame为zero且不添加到指定视图不会触发。旋转Screen会触发父视图的layoutSubviews。滚动UIScrollView引起View重新布局时会触发layoutSubviews。4、一张图片的展示经历了哪些步骤? 这个可以参考我之前写的一篇文章iOS开发图片格式选择 中的前半部分内容。 5、什么是离屏渲染,什么情况会导致离屏渲染? 如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的frame buffer,作为像素数据存储区域。如果有时因为面临一些限制,无法把渲染结果直接写入frame buffer,而是先暂存在另外的内存区域,之后再写入frame buffer,那么这个过程被称之为离屏渲染。以阴影为例,为什么它会导致离屏渲染。因为GPU的渲染是遵循“画家算法”,一层一层绘制的,但阴影很特殊,它需要全部内容绘制完成,再根据外轮廓进行绘制。这就导致了,阴影这一层要一直占据一块内存区域,这就导致了离屏渲染。 类似导致离屏渲染的情况还有:cornerRadius+clipsToBounds group opacity 组透明度 mask 遮罩 UIBlurEffect 毛玻璃效果有一篇文章详细的讨论了这些情况:关于iOS离屏渲染的深入研究 6、CoreAnimation这个框架的作用什么,它跟UIKit的关系是什么? CoreAnimation虽然直译是核心动画,但它其实是一个图像渲染框架,动画实现只是它的一部分功能。看这张图我们可以知道,它是UIKit和AppKit的底层实现,位于Metal、Core Graphics和GPU之上之上。 苹果官方文档:About Core Animation 引用计数 1、ARC方案的原理是什么?它是在什么时候做的隐式添加release操作? ARC(Automatic Reference Cunting)自动引用计数,意即通过LLVM编译器自动管理对应的引用计数状态。ARC开启时无需再次键入retain或者release代码。 它是在编译阶段添加retain或者release代码的。 2、循环引用有哪些场景,如何避免? 循环引用及两个及以上对象出现引用环,导致对象无法释放的情况。一般在block,delegate,NSTimer时容易出现这个问题。 解决方案就是让环的其中一环节实现弱引用。 3、为什么当我们在使用block时外面是weak 声明一个weakSelf,还要在block内部使用strong再持有一下? block外界声明weak是为了实现block对对象的弱持有,而里面的作用是为了保证在进到block时不会发生释放。 4、Autoreleasepool是实现机制是什么?它是什么时候释放内部的对象的?它内部的数据结构是什么样的?当我提到哨兵对象时,会继续问哨兵对象的作用是什么,为什么要设计它? Autoreleasepool的原理是一个双向列表,它会对加入其中的对象实现延迟释放。当Autoreleasepool调用drain方法时会释放内部标记为autorelease的对象。 class AutoreleasePoolPage { magic_t const magic; id *next; pthread_t const thread; AutoreleasePoolPage * const parent; AutoreleasePoolPage *child; uint32_t const depth; uint32_t hiwat; };哨兵对象类似一个指针,指向自动释放池的栈顶位置,它的作用就是用于标记当前自动释放池需要释放内部对象时,释放到那个地方结束,每次入栈时它用于确定添加的位置,然后再次移动到栈顶。 关于自动释放池的底层探究可以看draveness的这篇自动释放池的前世今生 ---- 深入解析 autoreleasepool 5、哪些对象会放入到Autoreleasepool中? 有两种情况生成的对象会加入到autoreleasepool中:非alloc/new/copy/mutablecopy 开始的方式初始化时。 id的指针或对象的指针在没有显示指定时引用计数带来的一次讨论 6、weak的实现原理是什么?当引用对象销毁是它是如何管理内部的Hash表的?(这里要参阅weak源码) runTime会把对weak修饰的对象放到一个全局的哈希表中,用weak修饰的对象的内存地址为key,weak指针为值,在对象进行销毁时,用通过自身地址去哈希表中查找到所有指向此对象的weak指针,并把所有的weak指针置位nil。 Runtime 1、消息发送的流程是怎样的? OC中的方法调用会转化成给对象发送消息,发送消息会调用这个方法: objc_msgSend(receiver, @selector(message))该过程有以下关键步骤:先确定调用方法的类已经都加载完毕,如果没加载完毕的话进行加载从cache中查找方法cache中没有找到对应的方法,则到方法列表中查,查到则缓存如果本类中查询到没有结果,则遍历所有父类重复上面的查找过程,直到NSObject2、关联对象时什么情况下会导致内存泄露? 关联对象可以理解就是持有了一个对象,如果是retain等方式的持有,而该对象也持有了本类,那就是导致了循环引用。 3、消息转发的流程是什么? 消息转发是发生在接收者(receiver)没有找到对应的方法(method)的时候,该步骤有如下几个关键步骤:消息转发的时候,如果是实例方法会走resolveInstanceMethod:,如果是类方法会走resolveClassMethod:,它们的返回值都是Bool,需要我们确定是否进行转发。 如果第一步返回YES,确定转发就会进到下个方法forwardingTargetForSelector,这个方法需要我们指定一个被用receiver。 methodSignatureForSelector用于指定方法签名,forwardInvocation用于处理Invocation,进行完整转发。 如果消息转发也没有处理即为无法处理,会调用doesNotRecognizeSelector,引发崩溃。更多了解可以参考iOS开发·runtime原理与实践: 消息转发篇(Message Forwarding) (消息机制,方法未实现+API不兼容奔溃,模拟多继承) 4、category能否添加属性,为什么?能否添加实例变量,为什么? 可以添加属性,这里的属性指@property,但跟类里的@property又不一样。正常的@property为:实例变量Ivar + Setter + Getter 方法,分类里的@property这三者都没有,需要我们手动实现。 分类是运行时被编译的,这时类的结构已经固定了,所以我们无法添加实例变量。 对于分类自定义Setter和Getter方法,我们可以通过关联对象(Associated Object)进行实现。 5、元类的作用是什么? 元类的作用是存储类方法,同时它也是为了让OC的类结构能够形成闭环。 对于为甚设计元类有以下原因;在OC的世界里一切皆对象(借鉴于Smalltalk),metaclass的设计就是要为满足这一点。在OC中Class也是一种对象,它对应的类就是metaclass,metaclass也是一种对象,它的类是root metaclass,在往上根元类(root metaclass)指向自己,形成了一个闭环,一个完备的设计。如果不要metaclass可不可以?也是可以的,在objc_class再加一个类方法指针。但是这样的设计会将消息传递的过程复杂化,所以为了消息传递流程的复用,为了一切皆对象的思想,就有了metaclass。 关于这一话题的深入讨论可以参考这两篇文章: 为什么要存在MetaClass 为什么要设计metaclass 6、类方法是存储到什么地方的?类属性呢? 类方法和类属性都是存储到元类中的。 类属性在Swift用的多些,OC中很少有人用到,但其实它也是有的,写法如下: @interface Person : NSObject // 在属性类别中加上class @property (class, nonatomic, copy) NSString *name; @end // 调用方式 NSString *temp = Person.name;需要注意的是跟实例属性不一样,类属性不会自动生成实例变量和setter,getter方法,需要我们手动实现。具体实现方法可以参考这个文章:Objective-C Class Properties 7、讲几个runtime的应用场景hook系统方法进行方法交换。 了解一个类(闭源)的私有属性和方法。 关联对象,实现添加分类属性的功能。 修改isa指针,自定义KVO。Runloop 1、讲一下对Runloop的理解? Runloop就是一个运行循环,它保证了在没有任务的时候线程不退出,有任务的时候即使响应。Runloop跟线程,事件响应,手势识别,页面更新,定时器都有着紧密联系。 深入了解推荐ibireme的这篇深入理解RunLoop 2、可以用Runloop实现什么功能?检测卡顿 线程包活 性能优化,将一些耗时操作放到runloop wait的情况处理。性能优化 1、对TableView进行性能优化有哪些方式?缓存高度 异步渲染 减少离屏渲染2、Xcode的Instruments都有哪些调试的工具?Activity Monitor(活动监视器):监控进程的CPU、内存、磁盘、网络使用情况。是程序在手机 运行真正占用内存大小Allocations(内存分配):跟踪过程的匿名虚拟内存和堆的对象提供类名和可选保留/释放历史Core Animation(图形性能):显示程序显卡性能以及CPU使用情况Core Data:跟踪Core Data文件系统活动Energy Log:耗电量监控File Activity:检测文件创建、移动、变化、删除等Leaks(泄漏):一般的措施内存使用情况,检查泄漏的内存,并提供了所有活动的分配和泄漏模块的类对象分配统计信息以及内存地址历史记录Network:用链接工具分析你的程序如何使用TCP/IP和UDP/IP链接System Usage:记录关于文件读写,sockets,I/O系统活动,输入输出Time Profiler(时间探查):方法执行耗时分析Zombies:测量一般的内存使用,专注于检测过度释放的野指针对象。也提供对象分配统计以及主动分配的内存地址历史3、讲一下你做过的性能优化的事情。 这个根据自己情况来说吧。 4、如何检测卡顿,都有哪些方法?FPS,通过CADisplayLink计算1s内刷新次数,也可以利用Instruments里的Core Animation。 利用Runloop,实时计算 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 两个状态区域之间的耗时是否超过某个阀值 子线程检测,每次检测时设置标记位为YES,然后派发任务到主线程中将标记位设置为NO。接着子线程沉睡超时阙值时长,判断标志位是否成功设置成NO,如果没有说明主线程发生了卡顿。参考ANREye的实现5、缩小包体积有哪些方案?图片压缩,无用图片删除 一些大图可以动态下发 删除无用类,无用方法 减少三方库的依赖计算机相关 1、项目编译的流程是什么?手机上的应用程序自点击图标开始到首屏内容展示都经历了哪些步骤? 编译流程:预处理:处理宏定义,删除注释,展开头文件。词法分析:把代码切成一个个token,比如大小括号等于号还有字符串语法分析:验证语法是否正确,合成抽象语法树AST静态分析:查找代码错误类型检查:动态和静态目标代码的生成与优化,包括删除多余指令,选择合适的寻址方式,如果开启了bitcode,会做进一步的优化汇编:由汇编器生成汇编语言机器码:由汇编语言转成机器码,生成.o文件应用启动的流程: 启动的前提是完成编译,运行程序即运行编译过后的目标程序,它分为main函数前和main函数后: main前加载可执行文件(App的.o文件集合)加载动态链接库(系统和应用的动态链接库),进行rebase指针调整和bind符号绑定Objc运行时的初始处理,包括Objc相关类的注册,category注册,selector唯一性检查初始化,包括执行+load()、attribute(constructor)修饰的函数的调用、创建C++静态全局变量main后首页初始化所需要配置文件的读写操作首页界面渲染2、对于基本数据类型,一般是存储到栈中的,它有没有可能存在堆上,什么情况下会存储到堆上? 栈和堆都是同属一块内存,只不过一个是高地址往低地址存储,一个从低地址往高地址存储,他们并没有严格的界限说一个值只能放在堆上或者栈上。所以基本数据类型也是可以存储到堆上的。 至于什么情况会存储到堆上,我没想到,有知道的同学可以告知一下。 3、数据库中的事务是什么意思? 事务就是访问并操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行。如果其中一个步骤出错就要撤销整个操作,回滚到进入事务之前的状态。 4、使用过什么数据库(我回答的Sqlite,Realm),Realm在使用时有哪些注意事项,如何实现批量操作? 对于Realm感兴趣的同学可以看下其官方文档。 Realm需要注意的主要就是不能直接跨线程访问同一对象。 批量操作可以在一个单独的事务中执行多个数据库的修改。 5、LRU算法是否了解,如何实现一套LRU算法? LRU(Least recently used 最近最少使用)算法是一个缓存淘汰算法,其作用就是当缓存很多时,该淘汰哪些内容,见名知意,它的核心思想是淘汰最近使用最少的内容。实现它的关键步骤是:新数据插入到链表的头部每当缓存命中时,则将数据移动到链表头部链表满时,将尾部数据清除这个算法在SDWebImage和Kingfisher等需要处理缓存的库中都有实现。 6、知道哪些设计模式,怎么理解设计模式的作用? 工厂模式、观察者模式、中介者模式、单例模式。这个根据实际情况说吧。 7、如果有1000万个Int类型的数字,如何对他们排序? 这里的隐藏含义是,内存不够用时如何排序,还有一个隐藏含义是硬盘足够大。这是可以采用分而治之的方法,将数据分成若干块,使每一小块满足当前内容大小,然后对每块内容单独排序,最后采用归并排序对所有块进行排序,就得到了一个有序序列。 8、设计一套数据库方案,实现类似微信的搜索关键词能快速检索出包含该字符串的聊天信息,并展示对应数量(聊天记录的数据量较大) 可以对聊天记录的文本值加上索引。正常情况下数据库搜索都是全量检索的,加上索引之后只会检索满足条件的记录,大大降低检索量。 简历相关问题 1、Lottie实现动画效果的原理是什么? iOS里的动画基本都是基于CoreAnimation里的API实现的,Lottie也是如此。在AE上实现动画效果,通过插件导出对应的json文件,Lottie的库解析该json,转成对应的系统API方法。图片的引用可以使用Base64编到json里,也可以通过项目集成,通过路径引用。 2、OClint实现静态分析的原理是什么,它是如何做到的? 具体可以参考我之前写的如何通过静态分析提高iOS代码质量。 3、MVVM和MVC有什么区别? 对比架构时,可以从是否职责分离,可测试性,可易维护性三个维度对比。 更多对比可以参考我翻译的一篇文章:【译】iOS 架构模式--浅析MVC, MVP, MVVM 和 VIPER 4、静态库和动态库的区别是什么? 静态库:链接时被完整复制到可执行文件中,多次使用就多份拷贝。 动态库:链接时不复制,而是由系统动态加载到内存,内存中只会有一份该动态库。 5、了解Flutter吗?它有没有使用UIKit?它是如何渲染UI的?UIKit是基于CoreAnimation渲染的,而Flutter并没有用到它,而是自己基于C++实现了一套渲染框架。 6、二进制重排的核心依据是什么? 修改链接顺序,减少启动时的缺页中断。 实践步骤可以参考李斌同学的这篇iOS 优化篇 - 启动优化之Clang插桩实现二进制重排 7、如何设计一套切换主题的方案? 核心思路是观察者模式+协议(通知),当获取到主题切换时,通知各个实现了主题协议的类进行更新。 8、AVPlayer和IJKPlayer有什么区别?用IJKPlayer如何实现一个缓存视频列表每条视频前1s的内容? 因为对IJKPlayer和FFmpeg了解的不是很深,这个我也没有确切答案,如果有了解的小伙伴可以评论告知我。 9、类似微博的短视频列表,滑动停留播放,如何实现? 这个主要就是检测contentOffset和屏幕中间位置,设置一些边界条件,处理滑动过程中的切换行为。 10、使用python做过哪些事?如何理解脚本语言? 多语言管理,csv多语言文件读取,然后写入到项目Localizable.strings中;抓取项目中的多语言字符串。 脚本(script) 其实就是一系列指令,计算机看了指令就知道自己该做什么事情。像常见的Python,Shell,Ruby都是脚本语言,他们通常不需要编译,通过解释器运行。 数据结构与算法 1、什么是Hash表,什么是Hash碰撞,解决Hash碰撞有什么方法? 哈希表(Hash Table,也叫散列表),是根据关键码值 (Key-Value) 而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。我们常用的Dictionary就是一种Hash表。 那什么是Hash碰撞呢,我们知道Hash表的查找是通过键值进行定位的,当两个不同的输入对应一个输出时,即为Hash碰撞,也被称为Hash冲突。 如果使用字典的例子你可能联想不到冲突的情况,我们假设另一种情况:假设hash表的大小为9(即有9个槽),现在要把一串数据存到表里:5,28,19,15,20,33,12,17,10。我们使用的hash函数是对9取余。这样的话会出现hash(5)=5,hash(28)=1,hash(19)=1。28和19都对应一个地址,这就出现了Hash冲突。 解决Hash冲突的方式有开放定址法和链地址法。 2、如何遍历二叉树?二叉树的遍历有三种方式,对于上面这棵二叉树,他们的遍历结果为: 前序遍历:根节点 > 左子节点 > 右子节点。 10,6,4,8,14,12,16 中序遍历:左子节点 > 根节点 > 右子节点。 4,6,8,10,12,14,16 后序遍历:左子节点 > 右子节点 > 根节点。 4,8,6,12,16,14,10 3、简述下快速排序的过程,时间复杂度是多少? 快排的思想是通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行。 一个简单的Swift实现方式如下: func quicksort<T: Comparable>(_ a: [T]) -> [T] { guard a.count > 1 else { return a } let pivot = a[a.count/2] let less = a.filter { $0 < pivot } let equal = a.filter { $0 == pivot } let greater = a.filter { $0 > pivot } return quicksort(less) + equal + quicksort(greater) }快速排序是有好几种的,他们的区别在于如何实现filter和分区基准值的选取。 快排的时间复杂度是O(nlogn),空间复杂度是O(logn) 4、有一个整数数组,如何只遍历一遍就实现让该数组奇数都在前面,偶数都在后面? 这个是《剑指offer》里的一道题,leedcode也有对应题目:剑指offer 21 这个相对比较简单,因为不要求有序,可以采用收尾遍历的方式,进行交换,我这有个参考答案: func sorted( _ nums: inout [Int]) -> [Int] { guard !nums.isEmpty else { return [] } var start = 0 var end = nums.count - 1 while start < end { if nums[start] % 2 != 0 { start += 1 continue } if nums[end] % 2 == 0 { end -= 1 continue } (nums[start], nums[end]) = (nums[end], nums[start]) } return nums }5、假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? leetcode 20 6、给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转 leetcode 7 7、有红、黄、蓝三种颜色的气球。在牛客王国,1个红气球+1个黄气球+1个蓝气球可以兑换一张彩票 2个红气球+1个黄气球可以兑换1个蓝气球。 2个黄气球+1个蓝气球可以兑换1个红气球。 2个蓝气球+1个红气球可以兑换1个黄气球。 现在牛牛有a个红气球,b个黄气球, c个蓝气球,牛牛想知道自己最多可以兑换多少张彩票。 这个是牛客网里的一道算法题,这里有个题解可以参考。
iOS面试总结(2020年6月)
都说今年互联网行情很差,作为被大家喊了好几年“iOS开发没人要了”的iOS行情更差。那真实情况是什么样的呢,以我的经历给大家分析下。应某个朋友建议,去掉这一句啊,目前iOS岗位还是挺多的,你可以这么想只要苹果爸爸不倒,iOS开发就不会没人要。但另一方面,招聘方对iOS开发的要求是在不断提高的,我们不能固步自封,满足现状,只有不断学习,不断进步,才能保持自身竞争力。 我的面试的阶段基本都在6月份,准备的阶段则要再往前推个半个月吧。期间约到了不少一二线互联网公司面试机会,前期由于准备不足也错失了一些机会,在之后的面试中不断总结经验,越来越有信心了,最终选择了爱奇艺。整体来看求职情况还算可以,不是很好但也不是很差,其中会带有一定运气成分,所以要换工作的话一定不要裸辞。 这里总结下这段时间的面试经历和一些心得,后面会附上期间遇到的面试题,大家可以尝试作答一下。求职准备 如果确定了想要换工作就应该为求职做准备了。 知识准备 在确定了换工作的想法之后,我们就应该为面试做准备了。在回顾知识点的时候我建议分类去梳理:OC语法,Runtime,Runloop,多线程,性能优化等,这些是优先级高的内容,其次是网络知识,数据结构与算法等计算机通识知识。 有一本书非常推荐:《Objective-C高级编程》,建议精读。 开源库的话看Runtime(最新为可编译799.1版本)吧,把类的定义,Runloop,weak,Autoreleasepool相关的代码都看下。 网络的知识点可以参考我的那篇:iOS面试备战-网络篇。数据结构与算法,按照类别刷个几十题应该能应付大多数情况了,iOS面试一般不会有太难的算法题。 简历 简历是求职的第一步,也是你能否获得面试机会的敲门砖,我们一定要好好打磨下。下面是我在脉脉上看到的HR在筛选简历时主要关注的点:我在今年3月份的时候尝试投过几次简历,并没有太好的结果,后来进行了一些调整优化。6月份再投的时候相对好了些,陆续收到了些回应。本人之前并没有大厂经历,不是一流本科,但也能收到不少大厂的面试机会,所以我感觉自己的简历内容还是起到了一定的作用的。如果想参考我简历的话,可以关注公众号:「iOS成长之路」,回复:简历,进行下载。 上面有提到“高光时刻”,可以理解成亮点。怎么让自己的简历跟同能力水平的求职者不同,那就是找到属于我们的亮点。有一个建议,我们在写简历时,可以刻意夸大自己的能力,或者写我们想成为的样子,再之后我们就对着简历让这些内容一一实现,让它们变成自己的亮点。一定要注意不能只吹牛,不落实,因为被发现“造假”可是很严重的。 简历投递 以我的经历来说,相对靠谱的简历投递方式有:Boss直聘、脉脉、内推。 需要注意的是,Boss直聘和脉脉只有别人联系你,你再投递,反馈率才会高一些。如果是你主动联系的招聘方,那大概率是不会收到回应的。推测很多企业并没有很多的招聘岗位也会把招聘信息挂在上面,这种时候HR是不会关注投递的简历的。这也是为什么能看到很多人晒出投递上百个简历确一个回应的都没有的情况,不要气馁,这不一定代表你能力不行。 等招聘者联系是相对被动的,主动出击会更有效。那就是寻找内推,一般公司内推都有奖励的,所以公司内部人员都乐意去发布职位获取内推人选。脉脉,掘金,V2EX,一些知名公众号都能发现不少内推岗位,我们可以自己去挖掘。 面试流程 目前互联网公司大部分是2轮技术面+1轮HR,或三轮技术面+1轮HR。目前的面试形式多为视频面试,也有些是电话面试。视频面试的话,如果是通过Zoom,企业微信,钉钉等一般是不考察手写代码的。如果是通过牛客网,一般是会考察手写代码的。对于手写代码,仅有算法题会要求准确性,可运行,对于设计类题目,我们写出伪代码即可。 如果到了HR轮基本说明我们已经通过了面试,如果确定入职,接下来就是背调,薪资证明,学历证明,入职体检等一系列操作。 面试题 以下是我面试过程中遇到的面试题,其中网络和多线程问题已经分成两篇单独讲解了,这里就去除了这两部分。 Swift 因为我最近两年多一直在用Swift,面试开始的自我介绍环节,我也会着重提这一点。但是很不幸,我得到的答案基本都是:面试主要考察OC。这也说明了大部分公司对Swift态度还是非常保守的,所以除非招聘信息里写了要求Swift技能,否则我们是没有必要专门准备Swift相关面试的。 当然面试过程中也遇到了几个Swift问题: 1、Swift中struct和class有什么区别? 2、Swift中的方法调用有哪些形式? 3、Swift和OC有什么区别? 4、从OC向Swift迁移的时候遇到过什么问题? 5、怎么理解面向协议编程? OC语法 1、Block是如何实现的?Block对应的数据结构是什么样子的?__block的作用是什么?它对应的数据结构又是什么样子的? 2、GCD中的Block是在堆上还是栈上? 3、NSCoding协议是干什么用的? 4、KVO的实现原理 5、NSOperation有哪些特性比着GCD有哪些优点,它有哪些API? 6、NSNotificaiton是同步还是异步的,如果发通知时在子线程,接收在哪个线程? UI 1、事件响应链是如何传递的? 2、什么是异步渲染? 3、layoutsubviews是在什么时机调用的? 4、一张图片的展示经历了哪些步骤? 5、什么是离屏渲染,什么情况会导致离屏渲染? 6、CoreAnimation这个框架的作用什么,它跟UIKit的关系是什么? 引用计数 1、ARC方案的原理是什么?它是在什么时候做的隐式添加release操作? 2、循环引用有哪些场景,如何避免? 3、为什么当我们在使用block时外面是weak 声明一个weakSelf,还要在block内部使用strong再持有一下? 4、Autoreleasepool是实现机制是什么?它是什么时候释放内部的对象的?它内部的数据结构是什么样的?当我提到哨兵对象时,会继续问哨兵对象的作用是什么,为什么要设计它? 5、哪些对象会放入到Autoreleasepool中? 6、weak的实现原理是什么?当引用对象销毁是它是如何管理内部的Hash表的?(这里要参阅weak源码) Runtime 1、消息发送的流程是怎样的? 2、关联对象时什么情况下会导致内存泄露? 3、消息转发的流程是什么? 4、category能否添加属性,为什么?能否添加实例变量,为什么? 5、元类的作用是什么? 6、类方法是存储到什么地方的?类属性呢? 7、讲几个runtime的应用场景 Runloop 1、讲一下对Runloop的理解? 2、可以用Runloop实现什么功能? 性能优化 1、对TableView进行性能优化有哪些方式? 2、Xcode的Instruments都有哪些调试的工具? 3、讲一下你做过的性能优化的事情。 4、如何检测卡顿,都有哪些方法? 5、缩小包体积有哪些方案? 计算机相关 1、项目编译的流程是什么?手机上的应用程序自点击图标开始到首屏内容展示都经历了哪些步骤? 2、对于基本数据类型,一般是存储到栈中的,它有没有可能存在堆上,什么情况下会存储到堆上? 3、数据库中的事务是什么意思? 4、使用过什么数据库(我回答的Sqlite,Realm),Realm在使用时有哪些注意事项,如何实现批量操作? 5、LRU算法是否了解,如何实现一套LRU算法? 6、知道哪些设计模式,怎么理解设计模式的作用? 7、如果有1000万个Int类型的数字,如何对他们排序? 8、设计一套数据库方案,实现类似微信的搜索关键词能快速检索出包含该字符串的聊天信息,并展示对应数量(聊天记录的数据量较大)。 简历相关问题 1、Lottie实现动画效果的原理是什么? 2、OClint实现静态分析的原理是什么,它是如何做到的? 3、MVVM和MVC有什么区别? 4、静态库和动态库的区别是什么? 5、了解Flutter吗?它有没有使用UIKit?它是如何渲染UI的? 6、二进制重排的核心依据是什么? 7、如何设计一套切换主题的方案? 8、AVPlayer和IJKPlayer有什么区别?用IJKPlayer如何实现一个缓存视频列表每条视频前1s的内容? 9、类似微博的短视频列表,滑动停留播放,如何实现? 10、使用python做过哪些事?如何理解脚本语言? 数据结构与算法 1、什么是Hash表,什么是Hash碰撞,解决Hash碰撞有什么方法? 2、如何遍历二叉树? 3、简述下快速排序的过程,时间复杂度是多少? 4、有一个整数数组,如何只遍历一遍就实现让该数组奇数都在前面,偶数都在后面? 5、假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? 6、给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转。leetcode 7 7、有红、黄、蓝三种颜色的气球。在牛客王国,1个红气球+1个黄气球+1个蓝气球可以兑换一张彩票。 2个红气球+1个黄气球可以兑换1个蓝气球。 2个黄气球+1个蓝气球可以兑换1个红气球。 2个蓝气球+1个红气球可以兑换1个黄气球。 现在牛牛有a个红气球,b个黄气球, c个蓝气球,牛牛想知道自己最多可以兑换多少张彩票。 软技能 1、做过哪些工作职责之外的事情? 2、经历过最难的一次业务开发是什么样的,最终怎么解决的? 3、最近有学习什么新技术吗?有何收获? 4、你最擅长iOS哪方面的知识?怎么体现出来的? 5、常用哪些开源库,有没有研究过他们的原理? 6、如何保持个人成长? 流程型问题 流程性问题基本都会包含下面四个,最好提前准备好 1、请做下自我介绍。 2、你有什么问题要问我的吗? 3、为什么离职? 4、对下份工作的期望是什么样的? 这些问题看似不起眼,但其实还挺重要的,很有可能面试官就是通过这几个问题决定了要不要你通过面试。 自我介绍就不说了,简明扼要介绍自己近几年的经历和成绩就行,控制在一分钟以内。 第二个,最好不要直接说没有问题了,提问面试官是我们整个面试过程中少有的掌握主动权的时刻,它可以体现我们自主思考的能力。最好提前了解下公司和招聘需求,准备几个问题,或者面试过程中提出我们产生的一些疑问。 离职原因,这个如实回答即可,只要不说是因为钱或者跟领导同事不和基本都没有问题。 下份工作的期望,这个就看各自的需求吧。 总结 通过这些面试题,我们可以看出一些端倪。 1、面试官更喜欢“刨根问底”,对着一个概念不断的往深处延展,不断深入的问。这类问题会有很大的区分度,第一问第二问第三问难度逐次提高,用于筛选不同的面试者。这也提醒我们某些知识点不光要知道原理,还要知道为什么这么设计,这么设计的好处是什么。 2、问题范围更全面化,特别是二面时,问题不再局限于iOS端,而是更通用的计算机方向问题,这个需要我们平常多积累;还有就是开始重视个人软技能,学习能力和上进心。 3、围绕简历,还记得上面说过写简历时要吹牛逼吗。在面试的时候一定要把他们成为自己真正掌握的知识。 4、注重软技能,这个比前面几条作用稍微小些,但是如果被问到了,而我们也有很好的贴合点,那绝对就是加分项。我的一次经历是,当我向面试官说自己有写博客的习惯,他问我是否知道medium,我说知道,还翻译过几篇里面的文章,接着说了些我理解的国内外博客平台的现状分析。这种情况就属于加分项了。 另外面试是一次考察自己知识掌握程度的考核,考的好能提升自己自信心,考的不好可以帮助我们定位自身问题,不管怎么说都是不亏的。面试还可以帮助我们了解市场行情,薪资待遇,自身竞争力,流行技术栈等一系列情况。所以真的建议即使不考虑换工作,每年固定时间也可以出去面试几次。
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); });