Showing Posts From
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真的很强大,但是你要发现它的美,学会使用它。 最后就是要告诫自己:该做的事还得及时做出来, 不然就是午饭没吃 午休没睡。
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、字节码含义如何解析。
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签名。
一位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月)
都说今年互联网行情很差,作为被大家喊了好几年“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); });
iOS面试备战-网络
计算机网络是计算机科学与技术专业的必修课,也是移动端,前端,后端都会涉及到的知识点,同时它也是iOS面试中大概率会出现的问题。所以准备面试的话,网络相关的知识点一定不能错过。这里总结了一些我认为有用的和最近面试遇到的网络相关知识点。 去年写过一篇《图解TCP/IP》总结的文章,也可以对着看下。 计算机网络是如何分层的 网络有两种分层模型,一种是ISO(国际标准化组织)制定的OSI(Open System Interconnect)模型,它将网络分为七层。一种是TCP/IP的四层网络模型。OSI是一种学术上的国际标准,理想概念,TCP/IP是事实上的国际标准,被广泛应用于现实生活中。两者的关系可以看这个图:注:也有说五层模型的,它跟四层模型的区别就是,在OSI模型中的数据链路层和物理层,前者将其作为两层,后者将其合并为一层称为网络接口层。一般作为面试题的话都是需要讲出OSI七层模型的。 各个层的含义以及它们之间的关系可以看这张图:Http协议 http协议特性HTTP 协议构建于 TCP/IP 协议之上,是一个应用层协议,默认端口号是 80 灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。 无状态:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。 无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传。请求方法GET:请求获取Request-URI标识的资源,请求参数附加在url上,明文展示。POST:在Request-URI所标识的资源后附加新的数据,常用于修改服务器资源或者提交资源到服务器。POST请求体是放到body中的,可以指定编码方式,更加安全。HEAD:请求获取由Request-URI所标识的资源的响应消息报头。PUT:请求服务器存储一个资源,并用Request-URI作为其标识。DELETE:请求服务器删除Request-URI所标识的资源。TRACE:请求服务器回送收到的请求信息,主要用于测试或诊断。OPTIONS:请求查询服务器的性能,或者查询与资源相关的选项和需求。请求和响应报文 以该链接为例:https://zhangferry.com/2019/08/31/diagram_tcpip_concepts/ 在Chrome查看其请求的Headers信息。 General这里标记了请求的URL,请求方法为GET。状态码为304,代表文件未修改,可以直接使用缓存的文件。远程地址为185.199.111.153:443,此IP为Github 服务器地址,是因为我的博客是部署在GitHub上的。 除了304还有别的状态码,分别是:200 OK 客户端请求成功 301 Moved Permanently 请求永久重定向 302 Moved Temporarily 请求临时重定向 304 Not Modified 文件未修改,可以直接使用缓存的文件。 400 Bad Request 由于客户端请求有语法错误,不能被服务器所理解。 401 Unauthorized 请求未经授权。这个状态代码必须和WWW-Authenticate报头域一起使用 403 Forbidden 服务器收到请求,但是拒绝提供服务。服务器通常会在响应正文中给出不提供服务的原因 404 Not Found 请求的资源不存在,例如,输入了错误的URL 500 Internal Server Error 服务器发生不可预期的错误,导致无法完成客户端的请求。 503 Service Unavailable 服务器当前不能够处理客户端的请求,在一段时间之后,服务器可能会恢复正常。Response Headers:content-encoding:用于指定压缩算法 content-length:资源的大小,以十进制字节数表示。 content-type:指示资源的媒体类型。图中所示内容类型为html的文本类型,文字编码方式为utf-8 last-modified:上次内容修改的日期,为6月8号 status:304 文件未修改状态码 注:其中content-type在响应头中代表,需要解析的格式。在请求头中代表上传到服务器的内容格式。 Request Headers::method:GET请求 :path:url路径 :scheme:https请求 accept:通知服务器可以返回的数据类型。 accept-encoding:编码算法,通常是压缩算法,可用于发送回的资源 accept-language:通知服务器预期发送回的语言类型。这是一个提示,并不一定由用户完全控制:服务器应该始终注意不要覆盖用户的显式选择(比如从下拉列表中选择语言)。 cookie:浏览器cookie user-agent:用户代理,标记系统和浏览器内核 更多请求头的字段含义可以参考这里:HTTP headers TCP三次握手和四次挥手的过程以及为什么要有三次和四次 在了解TCP握手之前我们先看下TCP的报文样式:其中控制位(Control Flag)标记着握手阶段的各个状态。TCP三次握手 示意图如下:三次握手是指建立一个TCP连接时,需要客户端和服务器总共发送3个数据包。 1、第一次握手(SYN=1, seq=x) 客户端发送一个 TCP 的 SYN 标志位置1的包,指明客户端打算连接的服务器的端口,以及初始序号 X,保存在包头的序列号(Sequence Number)字段里。 发送完毕后,客户端进入 SYN_SEND 状态。 2、第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1) 服务器发回确认包(ACK)应答。即 SYN 标志位和 ACK 标志位均为1。服务器端选择自己 ISN 序列号,放到 Seq 域里,同时将确认序号(Acknowledgement Number)设置为客户的 ISN 加1,即X+1。 发送完毕后,服务器端进入 SYN_RCVD 状态。 3、第三次握手(ACK=1, ACKnum=y+1) 客户端再次发送确认包(ACK),SYN 标志位为0,ACK 标志位为1,并且把服务器发来 ACK 的序号字段+1,放在确定字段中发送给对方,并且在数据段放写ISN的+1 发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手结束。 问题一:为什么需要三次握手呢? 在谢希仁著的《计算机网络》里说,『为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误』。怎么理解呢,我们假设一种情况,有一个建立连接的第一次握手的报文段因为滞留到网络中过了较长时间才发送到服务端。这时服务器是要做ACK应答的,如果只有两次握手就代表连接建立,那服务器此时就要等待客户端发送建立连接之后的数据。而这只是一个因滞留而废弃的请求,是不是白白浪费了很多服务器资源。 从另一个角度看这个问题,TCP是全双工的通信模式,需要保证两端都已经建立可靠有效的连接。在三次握手过程中,我们可以确认的状态是: 第一次握手:服务器确认自己接收OK,服务端确认客户端发送OK。 第二次握手:客户端确认自己发送OK,客户端确认自己接收OK,客户端确认服务器发送OK,客户端确认服务器接收OK。 第三次握手:服务器确认自己发送OK,服务器确认客户端接收OK。 只有握手三次才能达到全双工的目的:确认自己和对方都能够接收和发送消息。 TCP四次挥手 示意图如下:四次挥手表示要发送四个包,挥手的目的是断开连接。 1、第一次挥手(FIN=1, seq=x) 假设客户端想要关闭连接,客户端发送一个 FIN 标志位置为1的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据。 发送完毕后,客户端进入 FIN_WAIT_1 状态。 2、第二次挥手(ACK=1,ACKnum=x+1) 服务器端确认客户端的 FIN 包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。 发送完毕后,服务器端进入 CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态,等待服务器端关闭连接。 3、第三次挥手(FIN=1,seq=y) 服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN 置为1。 发送完毕后,服务器端进入 LAST_ACK 状态,等待来自客户端的最后一个ACK。 4、第四次挥手(ACK=1,ACKnum=y+1) 客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待可能出现的要求重传的 ACK 包。 服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。 客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。 问题一:为什么挥手需要四次呢?为什么不能将ACK和FIN报文一起发送? 当服务器收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端『你发的FIN我收到了』。只有等到服务端所有的报文都发送完了,才能发FIN报文,所以要将ACK和FIN分开发送,这就导致需要四次挥手。 问题二:为什么TIMED_WAIT之后要等2MSL才进入CLOSED状态? MSL是TCP报文的最大生命周期,因为TIME_WAIT持续在2MSL就可以保证在两个传输方向上的尚未接收到或者迟到的报文段已经消失,同时也是在理论上保证最后一个报文可靠到达。假设最后一个ACK丢失,那么服务器会再重发一个FIN,这是虽然客户端的进程不在了,但是TCP连接还在,仍然可以重发LAST_ACK。 ###HTTPS的流程 HTTPS = HTTP + TLS/SSL,它的建立可以用以下示意图表示:1、客户端首次请求服务器,告诉服务器自己支持的协议版本,支持的加密算法及压缩算法,并生成一个随机数(client random)告知服务器。 2、服务器确认双方使用的加密方法,并返回给客户端证书以及一个服务器生成的随机数(server random) 3、客户端收到证书后,首先验证证书的有效性,然后生成一个新的随机数(premaster secret),并使用数字证书中的公钥,加密这个随机数,发送给服务器。 4、服务器接收到加密后的随机数后,使用私钥进行解密,获取这个随机数(premaster secret 5、服务器和客户端根据约定的加密方法,使用前面的三个随机数(client random, server random, premaster secret),生成『对话密钥』(session key),用来加密接下来的整个对话过程(对称加密)。 有一篇由浅入深介绍HTTPS的文章可以阅读一下:看图学HTTPS 问题一:为什么握手过程需要三个随机数,而且安全性只取决于第三个随机数? 前两个随机数是明文传输,存在被拦截的风险,第三个随机数是通过证书公钥加密的,只有它是经过加密的,所以它保证了整个流程的安全性。前两个随机数的目的是为了保证最终对话密钥的『更加随机性』。 问题二:Charles如何实现HTTPS的拦截? Charles要实现对https的拦截,需要在客户端安装Charles的证书并信任它,然后Charles扮演中间人,在客户端面前充当服务器,在服务器面前充当客户端。 问题三:为什么有些HTTPS请求(例如微信)抓包结果仍是加密的,如何实现的?我在聊天过程中并没有抓到会话的请求,在小程序启动的时候到是抓到了一个加密内容。我手动触发该链接会下载一个加密文件,我猜测这种加密是内容层面的加密,它的解密是由客户端完成的,而不是在HTTPS建立过程完成的。 另外在研究这个问题的过程中,又发现了一些有趣的问题:1、图中所示的三个https请求分别对应三个不同类型的图标,它们分别代表什么意思呢? 感谢iOS憨憨的回答。 第一个图标含义是HTTP/2.0,第二个图标含义是HTTP/1.1,第三个图标加锁是因为我用charles只抓取了443端口的请求,该请求端口为5228,所以不可访问。 2、第三个请求https://mtalk.google.com:5228图标和请求内容都加了锁,这个加锁是在https之上又加了一层锁吗? 这些问题暂时没有确切的答案,希望了解的小伙伴告知一下哈。 DNS解析流程 DNS(Domain name system)域名系统。DNS是因特网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户通过域名访问到对应的服务器(IP地址)。具体的解析流程是这样的: 1、浏览器中输入想要访问的网站域名,操作系统会检查本地hosts文件是否有这个网址的映射关系,如果有就调用这个IP地址映射,完成域名解析。没有的话就走第二步。 2、客户端回向本地DNS服务器发起查询,如果本地DNS服务器收到请求,并可以在本地配置区域资源中查到该域名,就将对应结果返回为给客户端。如果没有就走第三步。 3、根据本地DNS服务器的设置,采用递归或者迭代查询,直至解析完成。其中递归查询和迭代查询可以用如下两图表示。 递归查询 如图所示,递归查询是由DNS服务器一级一级查询传递的。迭代查询 如果所示,迭代查询是找到指定DNS服务器,由客户端发起查询。DNS劫持 DNS劫持发生在DNS服务器上,当客户端请求解析域名时将其导向错误的服务器(IP)地址。 常见的解决办法是使用自己的解析服务器或者是将域名以IP地址的方式发出去以绕过DNS解析。 Cookie和Session的区别 HTTP 是无状态协议,说明它不能以状态来区分和管理请求和响应。也就是说,服务器单从网络连接上无从知道客户身份。 可是怎么办呢?就给客户端们颁发一个通行证吧,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。这就是Cookie的工作原理。Cookie:Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,实际上Cookie是服务器在本地机器上存储的一小段文本,并随着每次请求发送到服务器。Cookie技术通过请求和响应报文中写入Cookie信息来控制客户端的状态。Session:Session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构来保存信息。当有用户请求创建一个session时,服务器会先检查这个客户端里是否已经包含了一个Session标识(session id),如果有就通过session id把session检索出来。如果没有就创建一个对应此Session的session id。这个session id会在本次响应中返回给客户端。两者有以下区别: 1、存储位置:Cookie存放在客户端上,Session数据存放在服务器上。 2、Session 的运行依赖 session id,而 session id 是存在 Cookie 中的,也就是说,如果浏览器禁用了 Cookie ,同时 Session 也会失效 3、安全性:Cookie存在浏览器中,可能会被一些程序复制,篡改;而Session存在服务器相对安全很多。 4、性能:Session会在一定时间内保存在服务器上,当访问增多,会对服务器造成一定的压力。考虑到减轻服务器压力,应当使用Cookie CDN CDN(Content Delivery Network),根本作用是将网站的内容发布到最接近用户的网络『边缘』,以提高用户访问速度。概括的来说:CDN = 镜像(Mirror) + 缓存(Cache) + 整体负载均衡(GSLB)。 目前CDN都以缓存网站中的静态数据为主,如CSS、JS、图片和静态网页等数据。用户在从主站服务器请求到动态内容后再从CDN上下载这些静态数据,从而加速网页数据内容的下载速度,如淘宝有90%以上的数据都是由CDN来提供的。 CDN工作流程 一个用户访问某个静态文件(如CSS),这个静态文件的域名假如是www.baidu.com,而这个域名最终会被指向CDN全局中CDN负载均衡服务器,再由这个负载均衡服务器来最终分配是哪个地方的访问用户,返回给离这个访问用户最近的CDN节点。之后用户就直接去这个CDN节点访问这个静态文件了,如果这个节点中请求的文件不存在,就会再回到源站去获取这个文件,然后再返回给用户。参考:深入理解Http请求、DNS劫持与解析 Socket socket位于应用层和传输层之间:它的作用是为了应用层能够更方便的将数据经由传输层来传输。所以它的本质就是对TCP/IP的封装,然后应用程序直接调用socket API即可进行通信。上文中说的三次握手和四次挥手即是通过socket完成的。 我们可以从iOS中网络库分层找到BSD Sockets,它是位于CFNetwork之下。在CFNetwork中还有一个CFSocket,推测是对BSD Sockets的封装。WebRTC WebRTC是一个可以用在视频聊天,音频聊天或P2P文件分享等Web App中的 API。借助WebRTC,你可以在基于开放标准的应用程序中添加实时通信功能。它支持在同级之间发送视频,语音和通用数据,从而使开发人员能够构建功能强大的语音和视频通信解决方案。该技术可在所有现代浏览器以及所有主要平台的本机客户端上使用。WebRTC项目是开源的,并得到Apple,Google,Microsoft和Mozilla等的支持。 如果某一请求只在某一地特定时刻失败率较高,会有哪些原因 这个是某公司二面时的问题,是一个开放性问题,我总结了以下几点可能: 1、该时刻请求量过大 2、该地的网络节点较不稳定 3、用户行为习惯,比如该时刻为上班高峰期,或者某个群体的特定习惯 如果有对网络方面比较熟悉的小伙伴也可以补充。
如何通过静态分析提高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 入门
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
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的界面,因为它原本就是工作的好好的,而且这两个架构模式是很容易兼容的。事情应该力求简单,不过不能过于简单 -- 阿尔伯特·爱因斯坦
【译】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)。你可以在网上找到很多相关的代码。
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
【译】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的更新版本。
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。 参考文献 视频文件格式 - 维基百科,自由的百科全书 音频编码格式的比较 - 维基百科,自由的百科全书
使用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时都是根据版本号进行查找的,可以检查下当前修改跟版本号是否对应。
可能被忽略的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、关闭触摸反馈 button.isUserInteractionEnabled = false //custome,system均会关闭触摸反馈 button.adjustsImageWhenHighlighted = false //custom:会关闭image,backgroundImage的反馈 //system:此设置无效5、showsTouchWhenHighlighted 这个属性是系统提供的一种highlighted样式,点击时按钮高亮。但是效果确实有点丑丑的,基本不用这种效果 其他特性 全局修改UIButton的样式可以: let gobalBtn = UIButton.appearance()//所有继承UIView的类都可以使用这个方法 gobalBtn.setTitle("Good", for: .normal)//会将所有button的title改为GoodsetAttributedTitle方法 这个方法可以将button的title以富文本的形式进行设置,支持对不同state的设置。需要注意它和setTitle的优先级 //用attributed方式设置button的title和titleColor let string = "Title" let attributed = NSMutableAttributedString(string: string) let range = NSMakeRange(0, string.count) attributed.addAttributes([NSAttributedStringKey.foregroundColor : UIColor.green], range: range) button.setAttributedTitle(attributed, for: .normal) //此时用setTitle重新设置title样式,不会生效,attributed优先级大于直接设置 button.setTitle("Next Button", for: .normal) button.setTitleColor(UIColor.blue, for: .normal)
AVPlayer详解系列(一)参数设置
最近工作内容基本都是围绕视频播放展开的,从AVPlayer到IJKPlayer,期间遇到挺多问题,趟了很多bug,也总结了一些心得。对AVPlayer了解的更多一些,因为涉及点比较多,所以打算做一个系列详尽的写一下这部分内容。希望大家多多支持,有问题的地方欢迎指正。思维导图 先来一张思维导图,作为这篇文章的目录索引:为什么使用AVPlayer: 首先在iOS平台使用播放视频,可用的选项一般有这四个,他们各自的作用和功能如下:使用环境 优点 缺点MPMoviePlayerController MediaPlayer 简单易用 不可定制AVPlayerViewController AVKit 简单易用 不可定制AVPlayer AVFoundation 可定制度高,功能强大 不支持流媒体IJKPlayer IJKMediaFramework 定制度高,支持流媒体播放 使用稍复杂由此可以看出,如果我们不做直播功能AVPlayer就是一个最优的选择。 另外AVPlayer是一个可以播放任何格式的全功能影音播放器 支持视频格式: WMV,AVI,MKV,RMVB,RM,XVID,MP4,3GP,MPG等。 支持音频格式:MP3,WMA,RM,ACC,OGG,APE,FLAC,FLV等。 支持视频格式: MP4,MOV,M4V,3GP,AVI等。 支持音频格式:MP3,AAC,WAV,AMR,M4A等。 详见AVPlayer支持的视频格式 ##如何使用 AVPlayer存在于AVFoundation框架,我们使用时需要导入: #import <AVFoundation/AVFoundation.h> 几个播放相关的参数 在创建一个播放器之前我们需要先了解一些播放器相关的类AVPlayer:控制播放器的播放,暂停,播放速度 AVURLAsset : AVAsset 的一个子类,使用 URL 进行实例化,实例化对象包换 URL 对应视频资源的所有信息。 AVPlayerItem:管理资源对象,提供播放数据源 AVPlayerLayer:负责显示视频,如果没有添加该类,只有声音没有画面我们这片文章就围绕这几个参数展开,光说这些你可能还有点不明白,那我们就围绕一个最简单的播放器做起,一点点扩展功能,在具体讲解这几个参数的作用。 最简单的播放器 根据上面描述,我们知道AVPlayer是播放的必要条件,所以我们可以构建的极简播放器就是: NSURL *playUrl = [NSURL URLWithString:@"http://baobab.wdjcdn.com/14573563182394.mp4"]; self.player = [[AVPlayer alloc] initWithURL:playUrl]; [self.player play];是不是很简单,只有三行代码! 但是它太简单了,仅可以完成音频的播放,连画面都没有。回看上面播放相关类的介绍,是因为缺少AVPlayerLayer;作为一个播放器,我不能只播放一条视频啊,我还想根据需要切换视频,那我们就得把AVPlayerItem也加上。 加上这两个属性之后的播放器是这样的: NSURL *playUrl = [NSURL URLWithString:@"http://baobab.wdjcdn.com/14573563182394.mp4"]; self.playerItem = [AVPlayerItem playerItemWithURL:playUrl]; //如果要切换视频需要调AVPlayer的replaceCurrentItemWithPlayerItem:方法 self.player = [AVPlayer playerWithPlayerItem:_playerItem]; self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player]; self.playerLayer.frame = _videoView.bounds; //放置播放器的视图 [self.videoView.layer addSublayer:self.playerLayer]; [_player play];现在的播放器稍微完整了一些,我们在自己创建的容器里可以看到画面了! 更多功能 但是它作为一个视频播放器,还是有很多不能让人满意的地方。例如:没有暂停、快进快退、倍速播放等,另外如果遇到url错误是不是还要有播放失败的提示,还有播放完成的相关提示。 为完成这些,我们需要对AVPlayerItem和AVPlayerLayer进一步了解一下。 一、AVPlayer的控制 前面讲过该类是控制视频播放行为的,他的使用比较简单。 播放视频: [self.player play];暂停视频: [self.player pause];更改速度: self.player.rate = 1.5;//注意更改播放速度要在视频开始播放之后才会生效还有一下其他的控制,我们可以调转到系统API进行查看 二、AVPlayerItem的控制 AVPlayerItem作为资源管理对象,它控制着视频从创建到销毁的诸多状态。 1、播放状态 status typedef NS_ENUM(NSInteger, AVPlayerItemStatus) { AVPlayerItemStatusUnknown,//未知 AVPlayerItemStatusReadyToPlay,//准备播放 AVPlayerItemStatusFailed//播放失败 };我们使用KVO监测playItem.status,可以获取播放状态的变化 [self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];在监听回调中: - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ if ([object isKindOfClass:[AVPlayerItem class]]) { if ([keyPath isEqualToString:@"status"]) { switch (_playerItem.status) { case AVPlayerItemStatusReadyToPlay: //推荐将视频播放放在这里 [self play]; break; case AVPlayerItemStatusUnknown: NSLog(@"AVPlayerItemStatusUnknown"); break; case AVPlayerItemStatusFailed: NSLog(@"AVPlayerItemStatusFailed") break; default: break; } } } }虽然设置完播放配置我们可以直接调用[self.player play];进行播放,但是更稳妥的方法是在回调收到AVPlayerItemStatusReadyToPlay时进行播放 2、视频的时间信息 在AVPlayer中时间的表示有一个专门的结构体CMTime typedef struct{ CMTimeValue value; // 帧数 CMTimeScale timescale; // 帧率(影片每秒有几帧) CMTimeFlags flags; CMTimeEpoch epoch; } CMTime;CMTime是以分数的形式表示时间,value表示分子,timescale表示分母,flags是位掩码,表示时间的指定状态。 获取当前播放时间,可以用value/timescale的方式: float currentTime = self.playItem.currentTime.value/item.currentTime.timescale;还有一种利用系统提供的方法,我们用它获取视频总时间: float totalTime = CMTimeGetSeconds(item.duration);如果我们想要添加一个计时的标签不断更新当前的播放进度,有一个系统的方法: - (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block;方法名如其意, “添加周期时间观察者” ,参数1 interal 为CMTime 类型的,参数2 queue为串行队列,如果传入NULL就是默认主线程,参数3 为CMTime 的block类型。 简而言之就是,每隔一段时间后执行 block。 比如:我们把interval设置成CMTimeMake(1, 10),在block里面刷新label,就是一秒钟刷新10次。 正常观察播放进度一秒钟一次就行了,所以可以这么写: [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:nil usingBlock:^(CMTime time) { AVPlayerItem *item = WeakSelf.playerItem; NSInteger currentTime = item.currentTime.value/item.currentTime.timescale; NSLog(@"当前播放时间:%ld",currentTime); }];3、loadedTimeRange 缓存时间 获取视频的缓存情况我们需要监听playerItem的loadedTimeRanges属性 [self.playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];在KVO的回调里: if ([keyPath isEqualToString:@"loadedTimeRanges"]){ NSArray *array = _playerItem.loadedTimeRanges; CMTimeRange timeRange = [array.firstObject CMTimeRangeValue];//本次缓冲时间范围 float startSeconds = CMTimeGetSeconds(timeRange.start); float durationSeconds = CMTimeGetSeconds(timeRange.duration); NSTimeInterval totalBuffer = startSeconds + durationSeconds;//缓冲总长度 NSLog(@"当前缓冲时间:%f",totalBuffer); }4、playbackBufferEmpty 监听playbackBufferEmpty我们可以获取当缓存不够,视频加载不出来的情况: [self.playerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];在KVO回调里: if ([keyPath isEqualToString:@"playbackBufferEmpty"]) { //some code show loading }5、playbackLikelyToKeepUp playbackLikelyToKeepUp和playbackBufferEmpty 是一对,用于监听缓存足够播放的状态 [self.playerItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil]; /* ... */ if([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) { //由于 AVPlayer 缓存不足就会自动暂停,所以缓存充足了需要手动播放,才能继续播放 [_player play]; }AVURLAsset 播放视频只需一个url就能进行这样太不安全了,别人可以轻易的抓包盗链,为此我们需要为视频链接做一个请求头的认证,这个功能可以借助AVURLAsset完成。 AVPlayerItem除了可以用URL初始化,还可以用AVAsset初始化,而AVAsset不能直接使用,我们看下AVURLAsset的一个初始化方法: /*! @param URL An instance of NSURL that references a media resource. @param options An instance of NSDictionary that contains keys for specifying options for the initialization of the AVURLAsset. See AVURLAssetPreferPreciseDurationAndTimingKey and AVURLAssetReferenceRestrictionsKey above. */ + (instancetype)URLAssetWithURL:(NSURL *)URL options:(nullable NSDictionary<NSString *, id> *)options;AVURLAssetPreferPreciseDurationAndTimingKey.这个key对应的value是一个布尔值, 用来表明资源是否需要为时长的精确展示,以及随机时间内容的读取进行提前准备。 除了这个苹果官方介绍的功能外,他还可以设置请求头,这个算是隐藏功能了,因为苹果并没有明说这个功能,我是费了很大劲找到的。 NSMutableDictionary * headers = [NSMutableDictionary dictionary]; [headers setObject:@"yourHeader"forKey:@"User-Agent"]; self.urlAsset = [AVURLAsset URLAssetWithURL:self.videoURL options:@{@"AVURLAssetHTTPHeaderFieldsKey" : headers}]; // 初始化playerItem self.playerItem = [AVPlayerItem playerItemWithAsset:self.urlAsset];补充:后来得知这个参数是非公开的API,但是经多人测试项目上线不受影响。 播放相关通知 1、声音类: //声音被打断的通知(电话打来) AVAudioSessionInterruptionNotification //耳机插入和拔出的通知 AVAudioSessionRouteChangeNotification根据userInfo判断具体状态 2、播放类 //播放完成 AVPlayerItemDidPlayToEndTimeNotification //播放失败 AVPlayerItemFailedToPlayToEndTimeNotification //异常中断 AVPlayerItemPlaybackStalledNotification对于播放完成的通知我们可以这么写: [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerMovieFinish:) name:AVPlayerItemDidPlayToEndTimeNotification object:[self.player currentItem]];3、系统状态 //进入后台 UIApplicationWillResignActiveNotification //返回前台 UIApplicationDidBecomeActiveNotification提示:所有通知和KVO的使用我们都要记得在不用时remove掉。 小结 视频播放相关的知识比较多,细节的方面需要一点一点去扣。暂且写这么多吧,以后有需要会及时补充。 参考: ZFPlayer AVPlayer那些坑 如果还有什么不理解的可以简书私信问我,或者查看我写的Demo,欢迎star- ( ゜- ゜)つロ乾杯~
iOS获取来电和短信发送状态
- 12 Dec, 2016
获取电话状态 在我想要了解iOS获取来电状态时,经常被这是不是允许的,是不是要调用私有库等问题困扰。费了好大劲终于解决了上面问题,你可以获取系统提供的电话相关状态,而且它不属于私有库。为了需要这方面资料的人查阅时少走弯路,我把这些东西写下来,废话少说,上代码。如何获取电话状态 首先要导入CoreTelephony框架: @import CoreTelephony; 然后声明一个CTCallCenter变量: @interface ViewController () { CTCallCenter *center_; //为了避免形成retain cycle而声明的一个变量,指向接收通话中心对象 } @end 然后监听电话状态: - (void) aboutCall{ //获取电话接入信息 callCenter.callEventHandler = ^(CTCall *call){ if ([call.callState isEqualToString:CTCallStateDisconnected]){ NSLog(@"Call has been disconnected");}else if ([call.callState isEqualToString:CTCallStateConnected]){ NSLog(@"Call has just been connected");}else if([call.callState isEqualToString:CTCallStateIncoming]){ NSLog(@"Call is incoming");}else if ([call.callState isEqualToString:CTCallStateDialing]){ NSLog(@"call is dialing");}else{ NSLog(@"Nothing is done"); } }; }还可以获取运营商信息: - (void)getCarrierInfo{ // 获取运营商信息 CTTelephonyNetworkInfo *info = [[CTTelephonyNetworkInfo alloc] init]; CTCarrier *carrier = info.subscriberCellularProvider; NSLog(@"carrier:%@", [carrier description]);// 如果运营商变化将更新运营商输出 info.subscriberCellularProviderDidUpdateNotifier = ^(CTCarrier *carrier) { NSLog(@"carrier:%@", [carrier description]); };// 输出手机的数据业务信息 NSLog(@"Radio Access Technology:%@", info.currentRadioAccessTechnology); } 当然这样在真机进行测试,以下为输出信息: 2015-12-29 16:34:14.525 RWBLEManagerDemo[1489:543655] carrier:CTCarrier (0x134e065c0) { Carrier name: [中国移动] Mobile Country Code: [460] Mobile Network Code:[07] ISO Country Code:[cn] Allows VOIP? [YES] } 2015-12-29 16:34:14.526 RWBLEManagerDemo[1489:543655] Radio Access Technology:CTRadioAccessTechnologyHSDPA CoreTelephony框架是不是私有库 私有框架的目录为: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/PrivateFrameworks/可以看出CoreTelephony框架是在frameworks内而不是PrivateFrameworks,所以它是可以放心使用的。网上之所以有说CoreTelephony是私有库,是因为在iOS6的时候是私有框架,后来苹果又给公开了。 获取短信状态 关于短信的状态获取,我直接看了 #import <MessageUI/MessageUI.h> 里面就两个头文件: #import <MessageUI/MFMailComposeViewController.h> #import <MessageUI/MFMessageComposeViewController.h> 一个是邮件相关的方法,一个短信相关的方法。进到MFMessageComposeViewController.h有一个枚举值: enum MessageComposeResult { MessageComposeResultCancelled, MessageComposeResultSent, MessageComposeResultFailed };typedef enum MessageComposeResult MessageComposeResult; // available in iPhone 4.0 这是表示短信发送状态的值。要使用这个框架发送自己编辑的内容还需要添加代理:MFMessageComposeViewControllerDelegate 代码如下: - (void)showMessageView { if( [MFMessageComposeViewController canSendText] )// 判断设备能不能发送短信 { MFMessageComposeViewController*picker = [[MFMessageComposeViewControlleralloc] init]; // 设置委托 picker.messageComposeDelegate= self; // 默认信息内容 picker.body = @"nihao"; // 默认收件人(可多个) picker.recipients = [NSArray arrayWithObject:@"12345678901", nil]; [self presentModalViewController:picker animated:YES]; [picker release]; } else { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"提示信息" message:@"该设备不支持短信功能" delegate:self cancelButtonTitle:nil otherButtonTitles:@"确定", nil]; [alert show]; [alert release]; } }- (void)messageComposeViewController:(MFMessageComposeViewController *)controller didFinishWithResult:(MessageComposeResult)result { switch (result){ case MessageComposeResultCancelled: NSLog(@"取消发送"); break; case MessageComposeResultFailed: NSLog(@"发送失败"); break; case MessageComposeResultSent: NSLog(@"发送成功"); break; default: break; } }对于来短信的通知没有找到,应该是不能获取的。 参考资料private framework使用 http://chenjohney.blog.51cto.com/4132124/1288551 CoreTelephony框架的简单使用 http://blog.csdn.net/jymn_chen/article/details/19240903 iOS关于系统短信和电话的调用 http://blog.csdn.net/frank_jb/article/details/49815883
iOS10本地通知UserNotifications快速入门
- 01 Oct, 2016
iOS10更新变动最大的就是通知这部分了,新版通知变得更加统一,使用更加方便,设计更加自由。以前本地通知和远程推送是分开的,虽然这些到了iOS10都合在一起了,但是为了便于理解,我们还是把他俩分开来进行学习。这节我们学习的是本地通知。以下的用语,如无特别表述,通知就代表本地通知,推送就代表远程服务器的推送。 ##快速添加一个通知 我们先举个完整的代码例子,大家了解下这个流程,然后分步介绍这几项: //第一步:注册通知 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; //请求获取通知权限(角标,声音,弹框) [center requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert) completionHandler:^(BOOL granted, NSError * _Nullable error) { if (granted) { //获取用户是否同意开启通知 NSLog(@"request authorization successed!"); } }]; } //第二步:新建通知内容对象 UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init] content.title = @"iOS10通知"; content.subtitle = @"新通知学习笔记"; content.body = @"新通知变化很大,之前本地通知和远程推送是两个类,现在合成一个了。这是一条测试通知,"; content.badge = @1; UNNotificationSound *sound = [UNNotificationSound soundNamed:@"caodi.m4a"]; content.sound = sound; //第三步:通知触发机制。(重复提醒,时间间隔要大于60s) UNTimeIntervalNotificationTrigger *trigger1 = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:5 repeats:NO]; //第四步:创建UNNotificationRequest通知请求对象 NSString *requertIdentifier = @"RequestIdentifier"; UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:requertIdentifier content:content trigger:trigger1]; //第五步:将通知加到通知中心 [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { NSLog(@"Error:%@",error); }]; }最终效果如下:通知内容UNMutableNotificationContent通知内容就是设定通知的一些展示信息,iOS10之后可以设置subtitle。 声音的设置需要借助一个新类UNNotificationSound ,通知文件要放到bundle里面。另外在实际的测试过程中发现,添加通知的声音有时候会无效。这应该是iOS10存在的一个bug,删除掉程序,再安装运行就好了。触发机制UNNotificationTriggerTrigger是新加入的一个功能,通过此类可设置本地通知触发条件。它一共有一下几种类型: 1、UNPushNotificaitonTrigger 推送服务的Trigger,由系统创建 2、UNTimeIntervalNotificaitonTrigger 时间触发器,可以设置多长时间以后触发,是否重复。如果设置重复,重复时长要大于60s 3、UNCalendarNotificaitonTrigger 日期触发器,可以设置某一日期触发。例如,提醒我每天早上七点起床: NSDateComponents *components = [[NSDateComponents alloc] init]; components.hour = 7; components.minute = 0; // components 日期 UNCalendarNotificationTrigger *calendarTrigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:components repeats:YES];4、UNLocationNotificaitonTrigger 位置触发器,用于到某一范围之后,触发通知。通过CLRegion设定具体范围。通知请求UNNotificationRequest通知请求的构造 + (instancetype)requestWithIdentifier:(NSString *)identifier content:(UNNotificationContent *)content trigger:(nullable UNNotificationTrigger *)trigger; 就是把上面三项连接起来。它有一个参数identifier,这相当于通知的一个身份。iOS10通知支持更新,就是基于此identifier再发一条通知。通知中心UNUserNotificationCenter获取通知[UNUserNotificationCenter currentNotificationCenter] 然后通过addNotificaitonRequest:就完成了一个通知的添加。 ##扩展通知的内容 通知我们已经添加上了,现在我们需要扩展一下通知的内容,给它加一些内容。扩展的内容需要支持3D-touch的手机(6s以上),重压之后全面显示添加附件iOS10之前通知的样式不能更改,在iOS10之后引入了UNNotificationationAttachment,可以在通知中添加图片,音频,视频。苹果对这些附件的大小和类型有一个限制:如果我想在通知里加一个图片,可以这样处理: NSString *imageFile = [[NSBundle mainBundle] pathForResource:@"sport" ofType:@"png"]; UNNotificationAttachment *imageAttachment = [UNNotificationAttachment attachmentWithIdentifier:@"iamgeAttachment" URL:[NSURL fileURLWithPath:imageFile] options:nil error:nil]; content.attachments = @[imageAttachment];//虽然是数组,但是添加多个只能显示第一个 /* add request and notificaiton code ... */效果如下:重压之后:添加交互//点击可以显示文本输入框 UNTextInputNotificationAction *action1 = [UNTextInputNotificationAction actionWithIdentifier:@"replyAction" title:@"文字回复" options:UNNotificationActionOptionNone]; //点击进入应用 UNNotificationAction *action2 = [UNNotificationAction actionWithIdentifier:@"enterAction" title:@"进入应用" options:UNNotificationActionOptionForeground]; //点击取消,没有任何操作 UNNotificationAction *action3 = [UNNotificationAction actionWithIdentifier:@"cancelAction" title:@"取消" options:UNNotificationActionOptionDestructive]; //通过UNNotificationCategory对象将这几个action行为添加到通知里去 UNNotificationCategory *categroy = [UNNotificationCategory categoryWithIdentifier:@"Categroy" actions:@[action1,action2,action3] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction]; //将categroy赋值到通知内容上 content.categoryIdentifier = @"Categroy"; //设置通知代理,用于检测点击方法 [[UNUserNotificationCenter currentNotificationCenter] setDelegate:self]; /* add request and notificaiton code ... */效果如下:获取通知交互内容: //识别通知交互处理的代理方法 - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler{ NSString *categoryIdentifier = response.notification.request.content.categoryIdentifier; if ([categoryIdentifier isEqualToString:@"Categroy"]) { //识别需要被处理的拓展 if ([response.actionIdentifier isEqualToString:@"replyAction"]){ //识别用户点击的是哪个 action UNTextInputNotificationResponse *textResponse = (UNTextInputNotificationResponse*)response; //获取输入内容 NSString *userText = textResponse.userText; //发送 userText 给需要接收的方法 NSLog(@"要发送的内容是:%@",userText); //[ClassName handleUserText: userText]; }else if([response.actionIdentifier isEqualToString:@"enterAction"]){ NSLog(@"点击了进入应用按钮"); }else{ NSLog(@"点击了取消"); } } completionHandler(); }由此我们可以知道action,categroy,request这些东西都是通过各自的identifier获取的。这样可以很方便的定位到某一个通知或者action上,为交互的处理提供了很大的便利。##自定义通知样式 在Xcode中File->New->Targe会出现下面的视图Notification Content对应的是通知,Notification Service Extension对应的是推送。我们这里要实现通知的自定义,选择左边那个。创建成功之后会在工程里多一个文件件NotificationViewController文件是自动生成的,里面有一个 - (void)didReceiveNotification:(UNNotification *)notification 可以在这里定义一些通知的显示。 MainInterface.storyboard文件是控制通知的storyboard文件,可以编辑需要的通知样式。我们设计一下文字的颜色和显示位置接下来你可能会问,怎么把这个自定义的通知样式应用到当前通知里呢?先别急,我们看下一个文件Info.flist里面的内容第一项UNNotificationExtensionCategory就是UNNotificationCategory的标示,我们把他换成我们通知里使用的标示"Category",系统就会自动匹配通知显示的样式。 第二项UNNotificationExtensionIntialContentSizeRation初始内容 Size 的比例。也可以在 viewDidLoad 中使用 self.preferredContentSize 直接设置 Size。 第三项UNNotificationExtensionDefaultContentHidden是否隐藏默认内容,如果设为YES,默认内容会被隐藏。 显示的效果:##总结 至此,iOS通知部分的内容就学完了,参考代码:Demo。 参考文档: iOS10 User Notificaitons学习笔记 活久见的重构-iOS10 UserNotificaiotns框架解析
iOS中实现JS和OC的交互(Hybrid App)
在项目开发中,我们常常遇到这种情况,一个功能性界面需要分享到其他平台,或者是一个较复杂,原生框架不易实现,需要经常变动的界面,处理这种功能,最优的处理办法就是交给H5来完成了。像这种介于web-app、native-app这两者之间的app,兼具“Native App良好用户交互体验的优势”和“Web App跨平台开发的优势”的应用就被成为Hybrid App。在iOS端涉及的知识就是JS和OC之间的交互,下面我们逐步学习如何处理这两者交互的问题。##目录使用JavaScriptCore与JS交互JavaScriptCore了解 获取JS点击事件(JS调用OC) OC调用JS方法 使用WebViewJavascriptBridge与JS交互 引入WebViewJavascriptBridge JS调用OC OC调用JS##使用JavaScriptCore与JS交互 ###JavaScriptCore了解 JavaScript和OC交互常用的框架就是这个了,首先我们了解几个概念:JSValue: 代表一个JavaScript实体,一个JSValue可以表示很多JavaScript原始类型例如boolean, integers, doubles,甚至包括对象和函数。 JSContext: 代表JavaScript的运行环境,你需要用JSContext来执行JavaScript代码。所有的JSValue都是捆绑在一个JSContext上的。 JSExport: 这是一个协议,可以用这个协议来将原生对象导出给JavaScript,这样原生对象的属性或方法就成为了JavaScript的属性或方法,非常神奇。 JavaScriptCore了解###网页内容 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> </head> <body> <div style="margin-top: 20px"> <h2>JavaSript与OC交互</h2> <input type="button" value="Native传值" onclick="Native.call('Native调用')"> </div> <div> <input type="button" value="传值测试" onclick="passValue('test')"> </div> <script> var alertShow = function(str) { alert(str); } </script> </body> </html>HTML运行结果:###获取JS点击事件 我们目的是获取点击方法 第一步 导入JavaScriptCore #import <JavaScriptCore/JavaScriptCore.h>第二步 加载HTML文件 NSString* path = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"]; NSURL* url = [NSURL fileURLWithPath:path]; NSURLRequest* request = [NSURLRequest requestWithURL:url] ; [self.webView loadRequest:request];第三步 获取当前页面JSContxt对象 - (void)webViewDidFinishLoad:(UIWebView *)webView { self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; //捕捉异常回调 self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) { context.exception = exceptionValue; NSLog(@"异常信息:%@", exceptionValue); }; }第四步 获取点击事件 有两种添加点击事件的方式直接调用的函数<input type="button" value="传值测试" onclick="passValue('test')">如果是第一种情况,比较简单。在代理- (void)webViewDidFinishLoad:(UIWebView *)webView 中加上 self.context[@"passValue"] = ^(NSString *str){ NSLog(@"%@",str); };如果点击传值按钮,就会调用此block,输出test第二种:通过native调用的方式<input type="button" value="Native传值" onclick="Native.call('Native调用')">这种方式需要声明一个JSExport的协议,协议中声明供JS使用的方法: #import <UIKit/UIKit.h> #import <JavaScriptCore/JavaScriptCore.h>@protocol JSObjcDelegate <JSExport> - (void)call:(NSString *)str; @end然后还要以JSExport协议关联Native方法, self.content[@"Native"] = self;在实现文件中实现这个方法 - (void)call:(NSString *)str{ NSLog(@"call"); }此时点击一下Native传值,就会输出Native调用 还有一个需要注意的地方是,在JS调用的方法里实现页面跳转时,要回到主线程执行。可以使用GCD方法 dispatch_async(dispatch_get_main_queue(), ^{ [self.navigationController pushViewController:vc animated:YES]; });至此,获取JS点击事件的功能就完成了。 ###OC调用JS方法 在上面那段HTML里有一个函数alertShow(),是调用弹框并将str值显示出来。那么我们如何调用这个方法呢? 第一步、获取这个函数,有两种方法 JSValue *Callback = self.context[@"alertShow"];//第一种方式 JSValue *Callback = [self.context objectForKeyedSubscript:@"alertShow"];//第二种方式第二步、向JS传参 [Callback callWithArguments:@[@"OC调用JS方法"]];//出现一个弹框,`OC调用JS方法`如果想直接使用JS的方法可以 NSString *str = @"alert('OC添加JS提示成功')"; [self.context evaluateScript:str];###加载新的URL WebView有一个代理方法,每当需要去加载一个request,就会回调这个方法,让上层决定是否加载。常常一个H5页面会进行不同url之间的跳转,这里截获,进行本地处理。 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{ NSString *url = request.URL.absoluteString; NSLog(@"____%@",url); if(![self isNetwork]){ //无网情况的一个处理 }else if([url hasSuffix:@"ios/test"]){ //do something you want return NO; } return YES; }##使用WebViewJavascriptBridge与JS交互 WebViewJavascriptBridge是一个轻量的用于OC与JS交互的第三方库,使用它可以用CocoaPods导入: pod 'WebViewJavascriptBridge', '~> 5.0' 也可以在Github上下载示例工程,将工程文件WebViewJavascriptBridge拖入自己项目中。本节主要介绍使用方法,关于实现原理网上介绍也比较多,感兴趣的同学可以自行了解。 引入WebViewJavascriptBridge //设置能够进行桥接 [WebViewJavascriptBridge enableLogging]; //注册handler在Object-C,如果有self.webView.delegate = self。应注释掉,否则注册方法不执行 self.bridge = [WebViewJavascriptBridge bridgeForWebView:self.webView];//如果想执行UIWebView的代理方法,需设置 [self.bridge setWebViewDelegate:self];JS调用OC JS调用原生方法getUsername,可以使用如下方式注册。JS需要返回值的可以用responseCallback将返回值传过去 [self.bridge registerHandler:@"getUsername" handler:^(id data, WVJBResponseCallback responseCallback) { NSLog(@"%@",data); responseCallback([self getUsername]); }];OC调用JS OC调用JS可以使用如下方法实现,如果需要传参,可以写到参数data:里,如果没有参数就传nil //OC调JS的方法 [self.bridge callHandler:@"testJavascriptHandler" data:nil responseCallback:^(id responseData) { NSLog(@"ObjC received response: %@", responseData); }];常见的处理JS与OC之间交互的问题,基本就这些了。希望对大家有所帮助,Demo在这里传送门