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、用户行为习惯,比如该时刻为上班高峰期,或者某个群体的特定习惯 如果有对网络方面比较熟悉的小伙伴也可以补充。
Swift进阶黄金之路(二)
- 01 Jun, 2020
Swift进阶黄金之路(一) 上期遗留一个问题:为什么 rethrows 一般用在参数中含有可以 throws 的方法的高阶函数中。 我们可以结合Swift的官方文档对rethrows再做一遍回顾:A function or method can be declared with the rethrows keyword to indicate that it throws an error only if one of its function parameters throws an error. These functions and methods are known as rethrowing functions and rethrowing methods. Rethrowing functions and methods must have at least one throwing function parameter.返回rethrows的函数要求至少有一个可抛出异常的函数式参数,而有以函数作为参数的函数就叫做高阶函数。 这期分两方面介绍Swift:特性修饰词和一些重要的Swift概念。特性修饰词 在Swift语法中有很多@符号,这些@符号在Swift4之前的版本大多是兼容OC的特性,Swift4及之后则出现越来越多搭配@符号的新特性。以@开头的修饰词,在官网中叫Attributes,在SwiftGG的翻译中叫特性,我并没有找到这一类被@修饰的符号的统称,就暂且叫他们特性修饰词吧,如果有清楚的小伙伴可以告知我。 从Swift5的发布来看(@dynamicCallable,@State),之后将会有更多的特性修饰词出现,在他们出来之前,我们有必要先了解下现有的一些特性修饰词以及它们的作用。 参考:Swift Attributes @available @available: 可用来标识计算属性、函数、类、协议、结构体、枚举等类型的生命周期。(依赖于特定的平台版本 或 Swift 版本)。它的后面一般跟至少两个参数,参数之间以逗号隔开。其中第一个参数是固定的,代表着平台和语言,可选值有以下这几个:iOS iOSApplicationExtension macOS macOSApplicationExtension watchOS watchOSApplicationExtension tvOS tvOSApplicationExtension swift可以使用*指代支持所有这些平台。 有一个我们常用的例子,当需要关闭ScrollView的自动调整inset功能时: // 指定该方法仅在iOS11及以上的系统设置 if #available(iOS 11.0, *) { scrollView.contentInsetAdjustmentBehavior = .never } else { automaticallyAdjustsScrollViewInsets = false }还有一种用法是放在函数、结构体、枚举、类或者协议的前面,表示当前类型仅适用于某一平台: @available(iOS 12.0, *) func adjustDarkMode() { /* code */ } @available(iOS 12.0, *) struct DarkModeConfig { /* code */ } @available(iOS 12.0, *) protocol DarkModeTheme { /* code */ }版本和平台的限定可以写多个: @available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *) public func applying(_ difference: CollectionDifference<Element>) -> ArraySlice<Element>?注意:作为条件语句的available前面是#,作为标记位时是@ 刚才说了,available后面参数至少要有两个,后面的可选参数这些:deprecated:从指定平台标记为过期,可以指定版本号obsoleted=版本号:从指定平台某个版本开始废弃(注意弃用的区别,deprecated是还可以继续使用,只不过是不推荐了,obsoleted是调用就会编译错误)该声明message=信息内容:给出一些附加信息unavailable:指定平台上是无效的renamed=新名字:重命名声明我们看几个例子,这个是Array里flatMap的函数说明: @available(swift, deprecated: 4.1, renamed: "compactMap(_:)", message: "Please use compactMap(_:) for the case where closure returns an optional value") public func flatMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult]它的含义是针对swift语言,该方式在swift4.1版本之后标记为过期,对应该函数的新名字为compactMap(_:),如果我们在4.1之上的版本使用该函数会收到编译器的警告,即⚠️Please use compactMap(_:) for the case where closure returns an optional value。 在Realm库里,有一个销毁NotificationToken的方法,被标记为unavailable: extension RLMNotificationToken { @available(*, unavailable, renamed: "invalidate()") @nonobjc public func stop() { fatalError() } }标记为unavailable就不会被编译器联想到。这个主要是为升级用户的迁移做准备,从可用stop()的版本升上了,会红色报错,提示该方法不可用。因为有renamed,编译器会推荐你用invalidate(),点击fix就直接切换了。所以这两个标记参数常一起出现。 @discardableResult 带返回的函数如果没有处理返回值会被编译器警告⚠️。但有时我们就是不需要返回值的,这个时候我们可以让编译器忽略警告,就是在方法名前用@discardableResult声明一下。可以参考Alamofire中request的写法: @discardableResult public func request( _ url: URLConvertible, method: HTTPMethod = .get, parameters: Parameters? = nil, encoding: ParameterEncoding = URLEncoding.default, headers: HTTPHeaders? = nil) -> DataRequest { return SessionManager.default.request( url, method: method, parameters: parameters, encoding: encoding, headers: headers ) }@inlinable 这个关键词是可内联的声明,它来源于C语言中的inline。C中一般用于函数前,做内联函数,它的目的是防止当某一函数多次调用造成函数栈溢出的情况。因为声明为内联函数,会在编译时将该段函数调用用具体实现代替,这么做可以省去函数调用的时间。 内联函数常出现在系统库中,OC中的runtim中就有大量的inline使用: static inline id autorelease(id obj) { ASSERT(obj); ASSERT(!obj->isTaggedPointer()); id *dest __unused = autoreleaseFast(obj); ASSERT(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj); return obj; }Swift中的@inlinable和C中的inline基本相同,它在标准库的定义中也广泛出现,可用于方法,计算属性,下标,便利构造方法或者deinit方法中。 例如Swift对Array中map函数的定义: @inlinable public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]其实Array中声明的大部分函数前面都加了@inlinable,当应用某一处调用该方法时,编译器会将调用处用具体实现代码替换。 需要注意内联声明不能用于标记为private或者fileprivate的地方。 这很好理解,对私有方法的内联是没有意义的。内联的好处是运行时更快,因为它省略了从标准库调用map实现的步骤。但这个快也是有代价的,因为是编译时做替换,这增加了编译的开销,会相应的延长编译时间。 内联更多的是用于系统库的特性,目前我了解的Swift三方库中仅有CocoaLumberjack使用了@inlinable这个特性。 @warn_unqualified_access 通过命名我们可以推断出其大概含义:对“不合规”的访问进行警告。这是为了解决对于相同名称的函数,不同访问对象可能产生歧义的问题。 比如说,Swift 标准库中Array和Sequence均实现了min()方法,而系统库中也定义了min(::),对于可能存在的二义性问题,我们可以借助于@warn_unqualified_access。 extension Array where Self.Element : Comparable { @warn_unqualified_access @inlinable public func min() -> Element? } extension Sequence where Self.Element : Comparable { @warn_unqualified_access @inlinable public func min() -> Self.Element? }这个特性声明会由编译器在可能存在二义性的场景中对我们发出警告。这里有一个场景可以便于理解它的含义,我们自定义一个求Array中最小值的函数: extension Array where Element: Comparable { func minValue() -> Element? { return min() } }我们会收到编译器的警告:Use of 'min' treated as a reference to instance method in protocol 'Sequence', Use 'self.' to silence this warning。它告诉我们编译器推断我们当前使用的是Sequence中的min(),这与我们的想法是违背的。因为有这个@warn_unqualified_access限定,我们能及时的发现问题,并解决问题:self.min()。 @objc 把这个特性用到任何可以在 Objective-C 中表示的声明上——例如,非内嵌类,协议,非泛型枚举(原始值类型只能是整数),类和协议的属性、方法(包括 setter 和 getter ),初始化器,反初始化器,下标。 objc 特性告诉编译器,这个声明在 Objective-C 代码中是可用的。 用 objc 特性标记的类必须继承自一个 Objective-C 中定义的类。如果你把 objc 用到类或协议中,它会隐式地应用于该类或协议中 Objective-C 兼容的成员上。如果一个类继承自另一个带 objc 特性标记或 Objective-C 中定义的类,编译器也会隐式地给这个类添加 objc 特性。标记为 objc 特性的协议不能继承自非 objc 特性的协议。 @objc还有一个用处是当你想在OC的代码中暴露一个不同的名字时,可以用这个特性,它可以用于类,函数,枚举,枚举成员,协议,getter,setter等。 // 当在OC代码中访问enabled的getter方法时,是通过isEnabled class ExampleClass: NSObject { @objc var enabled: Bool { @objc(isEnabled) get { // Return the appropriate value } } }这一特性还可以用于解决潜在的命名冲突问题,因为Swift有命名空间,常常不带前缀声明,而OC没有命名空间是需要带的,当在OC代码中引用Swift库,为了防止潜在的命名冲突,可以选择一个带前缀的名字供OC代码使用。 Charts作为一个在OC和Swift中都很常用的图标库,是需要较好的同时兼容两种语言的使用的,所以也可以看到里面有大量通过@objc标记对OC调用时的重命名代码: @objc(ChartAnimator) open class Animator: NSObject { }@objc(ChartComponentBase) open class ComponentBase: NSObject { }@objcMembers 因为Swift中定义的方法默认是不能被OC调用的,除非我们手动添加@objc标识。但如果一个类的方法属性较多,这样会很麻烦,于是有了这样一个标识符@objcMembers,它可以让整个类的属性方法都隐式添加@objc,不光如此对于类的子类、扩展、子类的扩展都也隐式的添加@objc,当然对于OC不支持的类型,仍然无法被OC调用: @objcMembers class MyClass : NSObject { func foo() { } // implicitly @objc func bar() -> (Int, Int) // not @objc, because tuple returns // aren't representable in Objective-C }extension MyClass { func baz() { } // implicitly @objc }class MySubClass : MyClass { func wibble() { } // implicitly @objc }extension MySubClass { func wobble() { } // implicitly @objc }参考:Swift3、4中的@objc、@objcMembers和dynamic @testable @testable是用于测试模块访问主target的一个关键词。 因为测试模块和主工程是两个不同的target,在swift中,每个target代表着不同的module,不同module之间访问代码需要public和open级别的关键词支撑。但是主工程并不是对外模块,为了测试修改访问权限是不应该的,所以有了@testable关键词。使用如下: import XCTest @testable import Projectclass ProjectTests: XCTestCase { /* code */ }这时测试模块就可以访问那些标记为internal或者public级别的类和成员了。 @frozen 和@unknown default frozen意为冻结,是为Swift5的ABI稳定准备的一个字段,意味向编译器保证之后不会做出改变。为什么需要这么做以及这么做有什么好处,他们和ABI稳定是息息相关的,内容有点多就不放这里了,之后会单独出一篇文章介绍,这里只介绍这两个字段的含义。 @frozen public enum ComparisonResult : Int { case orderedAscending = -1 case orderedSame = 0 case orderedDescending = 1 }@frozen public struct String {}extension AVPlayerItem { public enum Status : Int { case unknown = 0 case readyToPlay = 1 case failed = 2 } }ComparisonResult这个枚举值被标记为@frozen即使保证之后该枚举值不会再变。注意到String作为结构体也被标记为@frozen,意为String结构体的属性及属性顺序将不再变化。其实我们常用的类型像Int、Float、Array、Dictionary、Set等都已被“冻结”。需要说明的是冻结仅针对struct和enum这种值类型,因为他们在编译器就确定好了内存布局。对于class类型,不存在是否冻结的概念,可以想下为什么。 对于没有标记为frozen的枚举AVPlayerItem.Status,则认为该枚举值在之后的系统版本中可能变化。 对于可能变化的枚举,我们在列出所有case的时候还需要加上对@unknown default的判断,这一步会有编译器检查: switch currentItem.status { case .readyToPlay: /* code */ case .failed: /* code */ case .unknown: /* code */ @unknown default: fatalError("not supported") }@State、@Binding、@ObservedObject、@EnvironmentObject 这几个是SwiftUI中出现的特性修饰词,因为我对SwiftUI的了解不多,这里就不做解释了。附一篇文章供大家了解。 [译]理解 SwiftUI 里的属性装饰器@State, @Binding, @ObservedObject, @EnvironmentObject 几个重要关键词 lazy lazy是懒加载的关键词,当我们仅需要在使用时进行初始化操作就可以选用该关键词。举个例子: class Avatar { lazy var smallImage: UIImage = self.largeImage.resizedTo(Avatar.defaultSmallSize) var largeImage: UIImage init(largeImage: UIImage) { self.largeImage = largeImage } }对于smallImage,我们声明了lazy,如果我们不去调用它是不会走后面的图片缩放计算的。但是如果没有lazy,因为是初始化方法,它会直接计算出smallImage的值。所以lazy很好的避免的不必要的计算。 另一个常用lazy的地方是对于UI属性的定义: lazy var dayLabel: UILabel = { let label = UILabel() label.text = self.todayText() return label }()这里使用的是一个闭包,当调用该属性时,执行闭包里面的内容,返回具体的label,完成初始化。 使用lazy你可能会发现它只能通过var初始而不能通过let,这是由 lazy 的具体实现细节决定的:它在没有值的情况下以某种方式被初始化,然后在被访问时改变自己的值,这就要求该属性是可变的。 另外我们可以在Sequences中使用lazy,在讲解它之前我们先看一个例子: func increment(x: Int) -> Int { print("Computing next value of \(x)") return x+1 }let array = Array(0..<1000) let incArray = array.map(increment) print("Result:") print(incArray[0], incArray[4])在执行print("Result:")之前,Computing next value of ...会被执行1000次,但实际上我们只需要0和4这两个index对应的值。 上面说了序列也可以使用lazy,使用的方式是: let array = Array(0..<1000) let incArray = array.lazy.map(increment) print("Result:") print(incArray[0], incArray[4])// Result: // 1 5在执行print("Result:")之前,并不会打印任何东西,只打印了我们用到的1和5。就是说这里的lazy可以延迟到我们取值时才去计算map里的结果。 我们看下这个lazy的定义: @inlinable public var lazy: LazySequence<Array<Element>> { get }它返回一个LazySequence的结构体,这个结构体里面包含了Array,而map的计算在LazySequence里又重新定义了一下: /// Returns a `LazyMapSequence` over this `Sequence`. The elements of /// the result are computed lazily, each time they are read, by /// calling `transform` function on a base element. @inlinable public func map<U>(_ transform: @escaping (Base.Element) -> U) -> LazyMapSequence<Base, U>这里完成了lazy序列的实现。LazySequence类型的lazy只能被用于map、flatMap、compactMap这样的高阶函数中。 参考: “懒”点儿好 纠错:参考文章中说:"这些类型(LazySequence)只能被用在 map,flatMap,filter这样的高阶函数中" 其实是没有filter的,因为filter是过滤函数,它需要完整遍历一遍序列才能完成过滤操作,是无法懒加载的,而且我查了LazySequence的定义,确实是没有filter函数的。 unowned weak Swift开发过程中我们会经常跟闭包打交道,而用到闭包就不可避免的遇到循环引用问题。在Swift处理循环引用可以使用unowned和weak这两个关键词。看下面两个例子: class Dog { var name: String init (name: String ) { self.name = name } deinit { print("\(name) is deinitialized") } }class Bone { // weak 修饰词 weak var owner: Dog? init(owner: Dog?) { self.owner = owner } deinit { print("bone is deinitialized" ) } }var lucky: Dog? = Dog(name: "Lucky") var bone: Bone? = Bone(owner: lucky!) lucky = nil // Lucky is deinitialized这里Dog和Bone是相互引用的关系,如果没有weak var owner: Dog?这里的weak声明,将不会打印Lucky is deinitialized。还有一种解决循环应用的方式是把weak替换为unowned关键词。weak相当于oc里面的weak,弱引用,不会增加循环计数。主体对象释放时被weak修饰的属性也会被释放,所以weak修饰对象就是optional。 unowned相当于oc里面的unsafe_unretained,它不会增加引用计数,即使它的引用对象释放了,它仍然会保持对被已经释放了的对象的一个 "无效的" 引用,它不能是 Optional 值,也不会被指向 nil。如果此时为无效引用,再去尝试访问它就会crash。这两者还有一个更常用的地方是在闭包里面: lazy var someClosure: () -> Void = { [weak self] in // 被weak修饰后self为optional,这里是判断self非空的操作 guard let self = self else { retrun } self.doSomethings() }这里如果是unowned修饰self的话,就不需要用guard做解包操作了。但是我们不能为了省略解包的操作就用unowned,也不能为了安全起见全部weak,弄清楚两者的适用场景非常重要。 根据苹果的建议:Define a capture in a closure as an unowned reference when the closure and the instance it captures will always refer to each other, and will always be deallocated at the same time.当闭包和它捕获的实例总是相互引用,并且总是同时释放时,即相同的生命周期,我们应该用unowned,除此之外的场景就用weak。 img 参考:内存管理,WEAK 和 UNOWNED Unowned 还是 Weak?生命周期和性能对比 KeyPath KeyPath是键值路径,最开始是用于处理KVC和KVO问题,后来又做了更广泛的扩展。 // KVC问题,支持struct、class struct User { let name: String var age: Int }var user1 = User() user1.name = "ferry" user1.age = 18//使用KVC取值 let path: KeyPath = \User.name user1[keyPath: path] = "zhang" let name = user1[keyPath: path] print(name) //zhang// KVO的实现还是仅限于继承自NSObject的类型 // playItem为AVPlayerItem对象 playItem.observe(\.status, changeHandler: { (_, change) in /* code */ })这个KeyPath的定义是这样的: public class AnyKeyPath : Hashable, _AppendKeyPath {}/// A partially type-erased key path, from a concrete root type to any /// resulting value type. public class PartialKeyPath<Root> : AnyKeyPath {}/// A key path from a specific root type to a specific resulting value type. public class KeyPath<Root, Value> : PartialKeyPath<Root> {}定义一个KeyPath需要指定两个类型,根类型和对应的结果类型。对应上面示例中的path: let path: KeyPath<User, String> = \User.name根类型就是User,结果类型就是String。也可以不指定,因为编译器可以从\User.name推断出来。那为什么叫根类型的?可以注意到KeyPath遵循一个协议_AppendKeyPath,它里面定义了很多append的方法,KeyPath是多层可以追加的,就是如果属性是自定义的Address类型,形如: struct Address { var country: String = "" } let path: KeyPath<User, String> = \User.address.country这里根类型为User,次级类型是Address,结果类型是String。所以path的类型依然是KeyPath。 明白了这些我们可以用KeyPath做一些扩展: extension Sequence { func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] { return sorted { a, b in return a[keyPath: keyPath] < b[keyPath: keyPath] } } } // users is Array<User> let newUsers = users.sorted(by: \.age)这个自定义sorted函数实现了通过传入keyPath进行升序排列功能。 参考:The power of key paths in Swift some some是Swift5.1新增的特性。它的用法就是修饰在一个 protocol 前面,默认场景下 protocol 是没有具体类型信息的,但是用 some 修饰后,编译器会让 protocol 的实例类型对外透明。 可以通过一个例子理解这段话的含义,当我们尝试定义一个遵循Equatable协议的value时: // Protocol 'Equatable' can only be used as a generic constraint because it has Self or associated type requirements var value: Equatable { return 1 }var value: Int { return 1 }编译器提示我们Equatable只能被用来做泛型的约束,它不是一个具体的类型,这里我们需要使用一个遵循Equatable的具体类型(Int)进行定义。但有时我们并不想指定具体的类型,这时就可以在协议名前加上some,让编译器自己去推断value的类型: var value: some Equatable { return 1 }在SwiftUI里some随处可见: struct ContentView: View { var body: some View { Text("Hello World") } }这里使用some就是因为View是一个协议,而不是具体类型。 当我们尝试欺骗编译器,每次随机返回不同的Equatable类型: var value: some Equatable { if Bool.random() { return 1 } else { return "1" } }聪明的编译器是会发现的,并警告我们Function declares an opaque return type, but the return statements in its body do not have matching underlying types。 参考:SwiftUI 的一些初步探索 (一)
如何通过静态分析提高iOS代码质量
随着项目的扩大,依靠人工codereview来保证项目的质量,越来越不现实,这时就有必要借助于一种自动化的代码审查工具:程序静态分析。 程序静态分析(Program Static Analysis)是指在不运行代码的方式下,通过词法分析、语法分析、控制流、数据流分析等技术对程序代码进行扫描,验证代码是否满足规范性、安全性、可靠性、可维护性等指标的一种代码分析技术。(来自百度百科) 词法分析,语法分析等工作是由编译器进行的,所以对iOS项目为了完成静态分析,我们需要借助于编译器。对于OC语言的静态分析可以完全通过Clang,对于Swift的静态分析除了Clange还需要借助于SourceKit。 Swift语言对应的静态分析工具是SwiftLint,OC语言对应的静态分析工具有Infer和OCLitn。以下会是对各个静态分析工具的安装和使用做一个介绍。SwiftLint对于Swift项目的静态分析可以使用SwiftLint。SwiftLint 是一个用于强制检查 Swift 代码风格和规定的一个工具。它的实现是 Hook 了 Clang 和 SourceKit 从而能够使用 AST 来表示源代码文件的更多精确结果。Clange我们了解了,那SourceKit是干什么用的? SourceKit包含在Swift项目的主仓库,它是一套工具集,支持Swift的大多数源代码操作特性:源代码解析、语法突出显示、排版、自动完成、跨语言头生成等工作。 安装 安装有两种方式,任选其一: 方式一:通过Homebrew $ brew install swiftlint这种是全局安装,各个应用都可以使用。 方式二:通过CocoaPods pod 'SwiftLint', :configurations => ['Debug']这种方式相当于把SwiftLint作为一个三方库集成进了项目,因为它只是调试工具,所以我们应该将其指定为仅Debug环境下生效。 集成进Xcode 我们需要在项目中的Build Phases,添加一个Run Script Phase。如果是通过homebrew安装的,你的脚本应该是这样的。 if which swiftlint >/dev/null; then swiftlint else echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint" fi如果是通过cocoapods安装的,你得脚本应该是这样的: "${PODS_ROOT}/SwiftLint/swiftlint"运行SwiftLint 键入CMD + B编译项目,在编译完后会运行我们刚才加入的脚本,之后我们就能看到项目中大片的警告信息。有时候build信息并不能填入项目代码中,我们可以在编译的log日志里查看。定制 SwiftLint规则太多了,如果我们不想执行某一规则,或者想要滤掉对Pods库的分析,我们可以对SwfitLint进行配置。 在项目根目录新建一个.swiftlint.yml文件,然后填入如下内容: disabled_rules: # rule identifiers to exclude from running - colon - trailing_whitespace - vertical_whitespace - function_body_length opt_in_rules: # some rules are only opt-in - empty_count # Find all the available rules by running: # swiftlint rules included: # paths to include during linting. `--path` is ignored if present. - Source excluded: # paths to ignore during linting. Takes precedence over `included`. - Carthage - Pods - Source/ExcludedFolder - Source/ExcludedFile.swift - Source/*/ExcludedFile.swift # Exclude files with a wildcard analyzer_rules: # Rules run by `swiftlint analyze` (experimental) - explicit_self# configurable rules can be customized from this configuration file # binary rules can set their severity level force_cast: warning # implicitly force_try: severity: warning # explicitly # rules that have both warning and error levels, can set just the warning level # implicitly line_length: 110 # they can set both implicitly with an array type_body_length: - 300 # warning - 400 # error # or they can set both explicitly file_length: warning: 500 error: 1200 # naming rules can set warnings/errors for min_length and max_length # additionally they can set excluded names type_name: min_length: 4 # only warning max_length: # warning and error warning: 40 error: 50 excluded: iPhone # excluded via string allowed_symbols: ["_"] # these are allowed in type names identifier_name: min_length: # only min_length error: 4 # only error excluded: # excluded via string array - id - URL - GlobalAPIKey reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji, sonarqube, markdown)一条rules提示如下,其对应的rules名就是function_body_length。 ! Function Body Length Violation: Function body should span 40 lines or less excluding comments and whitespace: currently spans 43 lines (function_body_length)disabled_rules下填入我们不想遵循的规则。 excluded设置我们想跳过检查的目录,Carthage、Pod、SubModule这些一般可以过滤掉。 其他的一些像是文件长度(file_length),类型名长度(type_name),我们可以通过设置具体的数值来调节。 另外SwiftLint也支持自定义规则,我们可以根据自己的需求,定义自己的rule。 生成报告 如果我们想将此次分析生成一份报告,也是可以的(该命令是通过homebrew安装的swiftlint): # reporter type (xcode, json, csv, checkstyle, junit, html, emoji, sonarqube, markdown) $ swiftlint lint --reporter html > swiftlint.htmlxcodebuild xcodebuild是xcode内置的编译命令,我们可以用它来编译打包我们的iOS项目,接下来介绍的Infer和OCLint都是基于xcodebuild的编译产物进行分析的,所以有必要简单介绍一下它。 一般编译一个项目,我们需要指定项目名,configuration,scheme,sdk等信息以下是几个简单的命令及说明。 # 不带pod的项目,target名为TargetName,在Debug下,指定模拟器sdk环境进行编译 xcodebuild -target TargetName -configuration Debug -sdk iphonesimulator # 带pod的项目,workspace名为TargetName.xcworkspace,在Release下,scheme为TargetName,指定真机环境进行编译。不指定模拟器环境会验证证书 xcodebuild -workspace WorkspaceName.xcworkspace -scheme SchemeName Release # 清楚项目的编译产物 xcodebuild -workspace WorkspaceName.xcworkspace -scheme SchemeName Release clean之后对xcodebuild命令的使用都需要将这些参数替换为自己项目的参数。 InferInfer是Facebook开发的针对C、OC、Java语言的静态分析工具,它同时支持对iOS和Android应用的分析。对于Facebook内部的应用像是 Messenger、Instagram 和其他一些应用均是有它进行静态分析的。它主要检测隐含的问题,主要包括以下几条:资源泄露,内存泄露 变量和参数的非空检测 循环引用 过早的nil操作暂不支持自定义规则。 安装及使用 $ brew install infer运行infer $ cd projectDir # 跳过对Pods的分析 $ infer run --skip-analysis-in-path Pods -- xcodebuild -workspace "Project.xcworkspace" -scheme "Scheme" -configuration Debug -sdk iphonesimulator我们会得到一个infer-out的文件夹,里面是各种代码分析的文件,有txt,json等文件格式,当这样不方便查看,我们可以将其转成html格式: $ infer explore --html点击trace,我们会看到该问题代码的上下文。 因为Infer默认是增量编译,只会分析变动的代码,如果我们想整体编译的话,需要clean一下项目: $ xcodebuild -workspace "Project.xcworkspace" -scheme "Scheme" -configuration Debug -sdk iphonesimulator clean再次运行Infer去编译。 $ infer run --skip-analysis-in-path Pods -- xcodebuild -workspace "Project.xcworkspace" -scheme "Scheme" -configuration Debug -sdk iphonesimulatorInfer的大致原理 Infer的静态分析主要分两个阶段: 1、捕获阶段 Infer 捕获编译命令,将文件翻译成 Infer 内部的中间语言。 这种翻译和编译类似,Infer 从编译过程获取信息,并进行翻译。这就是我们调用 Infer 时带上一个编译命令的原因了,比如: infer -- clang -c file.c, infer -- javac File.java。结果就是文件照常编译,同时被 Infer 翻译成中间语言,留作第二阶段处理。特别注意的就是,如果没有文件被编译,那么也没有任何文件会被分析。 Infer 把中间文件存储在结果文件夹中,一般来说,这个文件夹会在运行 infer 的目录下创建,命名是 infer-out/。 2、分析阶段 在分析阶段,Infer 分析 infer-out/ 下的所有文件。分析时,会单独分析每个方法和函数。 在分析一个函数的时候,如果发现错误,将会停止分析,但这不影响其他函数的继续分析。 所以你在检查问题的时候,修复输出的错误之后,需要继续运行 Infer 进行检查,知道确认所有问题都已经修复。 错误除了会显示在标准输出之外,还会输出到文件 infer-out/bug.txt 中,我们过滤这些问题,仅显示最有可能存在的。 在结果文件夹中(infer-out),同时还有一个 csv 文件 report.csv,这里包含了所有 Infer 产生的信息,包括:错误,警告和信息。 OCLint OCLint是基于Clange Tooling编写的库,它支持扩展,检测的范围比Infer要大。不光是隐藏bug,一些代码规范性的问题,例如命名和函数复杂度也均在检测范围之内。 安装OCLint OCLint一般通过Homebrew安装 $ brew tap oclint/formulae $ brew install oclint通过Hombrew安装的版本为0.13。 $ oclint --version LLVM (http://llvm.org/): LLVM version 5.0.0svn-r313528 Optimized build. Default target: x86_64-apple-darwin19.0.0 Host CPU: skylakeOCLint (http://oclint.org/): OCLint version 0.13. Built Sep 18 2017 (08:58:40).我分别用Xcode11在两个项目上运行过OCLint,一个实例项目可以正常运行,另一个复杂的项目却运行失败,报如下错误: 1 error generated 1 error generated ... oclint: error: cannot open report output file ..../onlintReport.html我并不清楚原因,如果你想试试0.13能否使用的话,直接跳到安装xcpretty。如果你也遇到了这个问题,可以回来安装oclint0.15版本。 OCLint0.15 我在oclint issuse #547这里找到了这个问题和对应的解决方案。 我们需要更新oclint至0.15版本。brew上的最新版本是0.13,github上的最新版本是0.15。我下载github上的release0.15版本,但是这个包并不是编译过的,不清楚是不是官方自己搞错了,只能手动编译了。因为编译要下载llvm和clange,这两个包较大,所以我将编译过后的包直接传到了这里CodeChecker。 如果不关心编译过程,可以下载编译好的包,跳到设置环境变量那一步。 编译OCLint 1、安装CMake和Ninja这两个编译工具 $ brew install cmake ninja2、clone OCLint项目 $ git clone https://github.com/oclint/oclint3、进入oclint-scripts目录,执行make命令 $ ./make成功之后会出现build文件夹,里面有个oclint-release就是编译成功的oclint工具。 设置oclint工具的环境变量 设置环境变量的目的是为了我们能够快捷访问。然后我们需要配置PATH环境变量,注意OCLint_PATH的路径为你存放oclint-release的路径。将其添加到.zshrc,或者.bash_profile文件末尾: OCLint_PATH=/Users/zhangferry/oclint/build/oclint-release export PATH=$OCLint_PATH/bin:$PATH执行source .zshrc,刷新环境变量,然后验证oclint是否安装成功: $ oclint --version OCLint (http://oclint.org/): OCLint version 0.15. Built May 19 2020 (11:48:49).出现这个介绍就说明我们已经完成了安装。 安装xcpretty xcpretty是一个格式化xcodebuild输出内容的脚本工具,oclint的解析依赖于它的输出。它的安装方式为: $ gem install xcprettyOCLint的使用 在使用OCLint之前还需要一些准备工作,需要将编译项COMPILER_INDEX_STORE_ENABLE设置为NO。将 Project 和 Targets 中 Building Settings 下的 COMPILER_INDEX_STORE_ENABLE 设置为 NO 在 podfile 中 target 'target' do 前面添加下面的脚本,将各个pod的编译配置也改为此选项post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['COMPILER_INDEX_STORE_ENABLE'] = "NO" end end end使用方式 1、进入项目根目录,运行如下脚本: $ xcodebuild -workspace ProjectName.xcworkspace -scheme ProjectScheme -configuration Debug -sdk iphonesimulator | xcpretty -r json-compilation-database -o compile_commands.json会将xcodebuild编译过程中的一些信息记录成一个文件compile_commands.json,如果我们在项目根目录看到了该文件,且里面是有内容的,证明我们完成了第一步。 2、我们将这个json文件转成方便查看的html,过滤掉对Pods文件的分析,为了防止行数上限,我们加上行数的限制: $ oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html -rc LONG_LINE=9999 -max-priority-1=9999 -max-priority-2=9999 -max-priority-3=9999最终会产生一个oclintReport.html文件。OCLint支持自定义规则,因为其本身规则已经很丰富了,自定义规则的需求应该很小,也就没有尝试。 封装脚本 OCLint跟Infer一样都是通过运行几个脚本语言进行执行的,我们可以将这几个命令封装成一个脚本文件,以OCLint为例,Infer也类似: #!/bin/bash # mark sure you had install the oclint and xcpretty# You need to replace these values with your own project configuration workspace_name="WorkSpaceName.xcworkspace" scheme_name="SchemeName"# remove history rm compile_commands.json rm oclint_result.xml # clean project # -sdk iphonesimulator means run simulator xcodebuild -workspace $workspace_name -scheme $scheme_name -configuration Debug -sdk iphonesimulator clean || (echo "command failed"; exit 1);# export compile_commands.json xcodebuild -workspace $workspace_name -scheme $scheme_name -configuration Debug -sdk iphonesimulator \ | xcpretty -r json-compilation-database -o compile_commands.json \ || (echo "command failed"; exit 1);# export report html # you can run `oclint -help` to see all USAGE oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html \ -disable-rule ShortVariableName \ -rc LONG_LINE=1000 \ || (echo "command failed"; exit 1);open -a "/Applications/Safari.app" oclintReport.htmloclint-json-compilation-database命令的几个参数说明: -e 需要忽略分析的文件,这些文件的警告不会出现在报告中 -rc 需要覆盖的规则的阀值,这里可以自定义项目的阀值,默认阀值 -enable-rule 支持的规则,默认是oclint提供的都支持,可以组合-disable-rule来过滤掉一些规则 规则列表 -disable-rule 需要忽略的规则,根据项目需求设置 在Xcode中使用OCLint 因为OCLint提供了xcode格式的输出样式,所以我们可以将它作为一个脚本放在Xcode中。 1、在项目的 TARGETS 下面,点击下方的 "+" ,选择 cross-platform 下面的 Aggregate。输入名字,这里命名为 OCLint2、选中该Target,进入Build Phases,添加Run Script,写入下面脚本: # Type a script or drag a script file from your workspace to insert its path. # 内置变量 cd ${SRCROOT} xcodebuild clean xcodebuild | xcpretty -r json-compilation-database oclint-json-compilation-database -e Pods -- -report-type xcode可以看出该脚本跟上面的脚本一样,只不过 将oclint-json-compilation-database命令的-report-type由html改为了xcode。而OCLint作为一个target本身就运行在特定的环境下,所以xcodebuild可以省去配置参数。 3、通过CMD + B我们编译一下项目,执行脚本任务,会得到能够定位到代码的warning信息:总结 以下是对这几种静态分析方案的对比,我们可以根据需求选择适合自己的静态分析方案。SwiftLint Infer OCLint支持语言 Swift C、C++、OC、Java C、C++、OC易用性 简单 较简单 较简单能否集成进Xcode 可以 不能集成进xcode 可以自带规则丰富度 较多,包含代码规范 相对较少,主要检测潜在问题 较多,包含代码规范规则扩展性 可以 不可以 可以参考 OCLint 实现 Code Review - 给你的代码提提质量 Using OCLint in Xcode Infer 的工作机制 LLVM & Clang 入门
Swift进阶黄金之路
- 10 May, 2020
Swift进阶黄金之路(二) 这篇是对一文鉴定是Swift的王者,还是青铜文章中问题的解答。这些问题仅仅是表层概念,属于知识点,在我看来即使都很清楚也并不能代表上了王者,如果非要用段位类比的话,黄金还是合理的😄。 Swift是一门上手容易,但是精通较难的语言。即使下面这些内容都不清楚也不妨碍你开发业务需求,但是了解之后它能够帮助我们写出更加Swifty的代码。一、 协议 Protocol ExpressibleByDictionaryLiteral ExpressibleByDictionaryLiteral是字典的字面量协议,该协议的完整写法为: public protocol ExpressibleByDictionaryLiteral { /// The key type of a dictionary literal. associatedtype Key /// The value type of a dictionary literal. associatedtype Value /// Creates an instance initialized with the given key-value pairs. init(dictionaryLiteral elements: (Self.Key, Self.Value)...) }首先字面量(Literal)的意思是:用于表达源代码中一个固定值的表示法(notation)。 举个例子,构造字典我们可以通过以下两种方式进行: // 方法一: var countryCodes = Dictionary<String, Any>() countryCodes["BR"] = "Brazil" countryCodes["GH"] = "Ghana" // 方法二: let countryCodes = ["BR": "Brazil", "GH": "Ghana"]第二种构造方式就是通过字面量方式进行构造的。 其实基础类型基本都是通过字面量进行构造的: let num: Int = 10 let flag: Bool = true let str: String = "Brazil" let array: [String] = ["Brazil", "Ghana"]而这些都有对应的字面量协议: ExpressibleByNilLiteral // nil字面量协议 ExpressibleByIntegerLiteral // 整数字面量协议 ExpressibleByFloatLiteral // 浮点数字面量协议 ExpressibleByBooleanLiteral // 布尔值字面量协议 ExpressibleByStringLiteral // 字符串字面量协议 ExpressibleByArrayLiteral // 数组字面量协议Sequence Sequence翻译过来就是序列,该协议的目的是一系列相同类型的值的集合,并且提供对这些值的迭代能力,这里的迭代可以理解为遍历,也即for-in的能力。可以看下该协议的定义: protocol Sequence { associatedtype Iterator: IteratorProtocol func makeIterator() -> Iterator }Sequence又引入了另一个协议IteratorProtocol,该协议就是为了提供序列的迭代能力。 public protocol IteratorProtocol { associatedtype Element public mutating func next() -> Self.Element? }我们通常用for-in实现数组的迭代: let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"] for animal in animals { print(animal) }这里的for-in会被编译器翻译成: var animalIterator = animals.makeIterator() while let animal = animalIterator.next() { print(animal) }Collection Collection译为集合,其继承于Sequence。 public protocol Collection : Sequence { associatedtype Index : Comparable var startIndex: Index { get } var endIndex: Index { get } var isEmpty: Bool { get } var count: Int { get } subscript(position: Index) -> Element { get } subscript(bounds: Range<Index>) -> SubSequence { get } }是一个元素可以反复遍历并且可以通过索引的下标访问的有限集合,注意Sequence可以是无限的,Collection必须是有限的。 Collection在Sequence的基础上扩展了下标访问、元素个数能特性。我们常用的集合类型Array,Dictionary,Set都遵循该协议。 CustomStringConvertible 这个协议表示自定义类型输出的样式。先来看下它的定义: public protocol CustomStringConvertible { var description: String { get } }只有一个description的属性。它的使用很简单: struct Point: CustomStringConvertible { let x: Int, y: Int var description: String { return "(\(x), \(y))" } }let p = Point(x: 21, y: 30) print(p) // (21, 30) //String(describing: <#T##CustomStringConvertible#>) let s = String(describing: p) print(s) // (21, 30)如果不实现CustomStringConvertible,直接打印对象,系统会根据默认设置进行输出。我们可以通过CustomStringConvertible对这一输出行为进行设置,还有一个协议是CustomDebugStringConvertible: public protocol CustomDebugStringConvertible { var debugDescription: String { get } }跟CustomStringConvertible用法一样,对应debugPrint的输出。 Hashable 我们常用的Dictionary,Set均实现了Hashable协议。Hash的目的是为了将查找集合某一元素的时间复杂度降低到O(1),为了实现这一目的需要将集合元素与存储地址之间建议一种尽可能一一对应的关系。 我们再看Hashable`协议的定义: public protocol Hashable : Equatable { var hashValue: Int { get } func hash(into hasher: inout Hasher) }public protocol Equatable { static func == (lhs: Self, rhs: Self) -> Bool }注意到func hash(into hasher: inout Hasher),Swift 4.2 通过引入 Hasher 类型并采用新的通用哈希函数进一步优化 Hashable。 如果你要自定义类型实现 Hashable 的方式,可以重写 hash(into:) 方法而不是 hashValue。hash(into:) 通过传递了一个 Hasher 引用对象,然后通过这个对象调用 combine(_:) 来添加类型的必要状态信息。 // Swift >= 4.2 struct Color: Hashable { let red: UInt8 let green: UInt8 let blue: UInt8 // Synthesized by compiler func hash(into hasher: inout Hasher) { hasher.combine(self.red) hasher.combine(self.green) hasher.combine(self.blue) } // Default implementation from protocol extension var hashValue: Int { var hasher = Hasher() self.hash(into: &hasher) return hasher.finalize() } }参考:Hashable / Hasher Codable Codable是可Decodable和Encodable的类型别名。它能够将程序内部的数据结构序列化成可交换数据,也能够将通用数据格式反序列化为内部使用的数据结构,大大提升对象和其表示之间互相转换的体验。处理的问题就是我们经常遇到的JSON转模型,和模型转JSON。 public typealias Codable = Decodable & Encodablepublic protocol Decodable { init(from decoder: Decoder) throws } public protocol Encodable { func encode(to encoder: Encoder) throws }这里只举一个简单的解码过程: //json数据 { "id": "1283984", "name": "Mike", "age": 18 } // 定义对象 struct Person: Codable{ var id: String var name: String var age: Int } // json为网络接口返回的Data类型数据 let mike = try! JSONDecoder().decode(Person.self, from: json) print(mike) //输出:Student(id: "1283984", name: "Mike", age: 18)是不是非常简单,Codable还支持各种自定义解编码过程,完全可以取代SwiftyJSON,HandyJSON等编解码库。 Comparable 这个是用于实现比较功能的协议,它的定义如下: public protocol Comparable : Equatable { static func < (lhs: Self, rhs: Self) -> Bool static func <= (lhs: Self, rhs: Self) -> Bool static func >= (lhs: Self, rhs: Self) -> Bool static func > (lhs: Self, rhs: Self) -> Bool }其继承于Equatable,即判等的协议。可以很清楚的理解实现了各种比较的定义就具有了比较的功能。这个不做比较。 RangeReplaceableCollection RangeReplaceableCollection支持用另一个集合的元素替换元素的任意子范围的集合。 看下它的定义: public protocol RangeReplaceableCollection : Collection where Self.SubSequence : RangeReplaceableCollection { associatedtype SubSequence mutating func append(_ newElement: Self.Element) mutating func insert<S>(contentsOf newElements: S, at i: Self.Index) where S : Collection, Self.Element == S.Element /* 拼接、插入、删除、替换的方法,他们都具有对组元素的操作能力 */ override subscript(bounds: Self.Index) -> Self.Element { get } override subscript(bounds: Range<Self.Index>) -> Self.SubSequence { get } }举个例子,Array支持该协议,我们可以进行如下操作: var bugs = ["Aphid", "Damselfly"] bugs.append("Earwig") bugs.insert(contentsOf: ["Bumblebee", "Cicada"], at: 1) print(bugs) // Prints "["Aphid", "Bumblebee", "Cicada", "Damselfly", "Earwig"]"这里附一张Swift中Array遵循的协议关系图,有助于大家理解上面讲解的几个协议之间的关系:图像来源:https://swiftdoc.org/v3.1/type/array/hierarchy/ 二、@propertyWrapper阅读以下代码,print 输出什么@propertyWrapper struct Wrapper<T> { var wrappedValue: T var projectedValue: Wrapper<T> { return self } func foo() { print("Foo") } } struct HasWrapper { @Wrapper var x = 0 func foo() { print(x) // 0 print(_x) // Wrapper<Int>(wrappedValue: 0) print($x) // Wrapper<Int>(wrappedValue: 0) } }这段代码看似要考察对@propertyWrapper的理解,但是有很多无用内容,导致代码很奇怪。 @propertyWrapper的意思就是属性包装,它可以将一系列相似的属性方法进行统一处理。举个例子,如果我们需要在UserDefaults中加一个是否首次启动的值,正常可以这样处理: extension UserDefaults { enum Keys { static let isFirstLaunch = "isFirstLaunch" } var isFirstLaunch: Bool { get { return bool(forKey: Keys.isFirstLaunch) } set { set(newValue, forKey: Keys.isFirstLaunch) } } }如果我们需要加入很多这样属性的话,就需要写大量的get 、set方法。而@propertyWrapper的作用就是为属性的这种设置提供一个模板写法,以下是使用属性包装的写法。 @propertyWrapper struct UserDefaultWrapper<T> { private let key: String private let defaultValue: T init(key: String, defaultValue: T) { self.key = key self.defaultValue = defaultValue } var wrappedValue: T { get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue } set { UserDefaults.standard.set(newValue, forKey: key) } } }class UserDefaults { @UserDefaultWrapper(key: Keys.isFirstLaunch, defaultValue: false) var isFirstLaunch: Bool }@propertyWrapper约束的对象必须要定义wrappedValue属性,因为该对象包裹的属性会走到wrappedValue的实现。 回到实例代码,定义了wrappedValue却并没有添加任何实现,这是允许的。所以访问x的时候其实是访问Wrapper的wrappedValue,因为没有给出任何实现所以直接打印出0。而_x和$x对应的就是Wrapper自身。 参考:Swift Property Wrappers 三、关键字 public open public open为权限关键词。对于一个严格的项目来说,精确的最小化访问控制级别对于代码的维护来说相当重要的。完整的权限关键词,按权限大小排序如下: open > public > internal > fileprivate > privateopen权限最大,允许外部module访问,继承,重写。 public允许外部module访问,但不允许继承,重写。 internal为默认关键词,在同一个module内可以共用。 fileprivate表示代码可以在当前文件中被访问,而不做类型限定。 private表示代码只能在当前作用域或者同一文件中同一类型的作用域中被使用。这些权限关键词可以修饰,属性,方法和类型。需要注意:当一个类型的某一属性要用public修饰时,该类型至少要用public(或者open)权限的关键词修复。可以理解为数据访问是分层的,我们为了获取某一属性或方法需要先获取该类型,所以外层(类型)的访问权限要满足大于等于内层(类型、方法、属性)权限。 参考:Swift AccessControl static class final 原文中final跟权限关键词放在一起了,其实是不合理的,就将其放到这里来讨论。 static静态变量关键词,来源于C语言。 在Swift中常用语以下场景: // 仅用于类名前,表示该类不能被继承。仅支持class类型 final class Manager { // 单例的声明 static let shared = Manager() // 实例属性,可被重写 var name: String = "Ferry" // 实例属性,不可被重写 final var lastName: String = "Zhang" // 类属性,不可被重写 static var address: String = "Beijing" // 类属性,可被重写。注意只能作为计算属性,而不能作为存储属性 class var code: String { return "0122" } // 实例函数,可被重写 func download() { /* code... */ } // 实例函数,不可被重写 final func download() { /* code... */ } // 类函数,可被重写 class func removeCache() { /* code... */ } // 类函数,不可被重写 static func download() { /* code... */ } }struct Manager { // 单例的声明 static let shared = Manager() // 类属性 static var name: String = "Ferry" // 类函数 static func download() { /* code... */ } }struct和enum因为不能被继承,所以也就无法使用class和final关键词,仅能通过static关键词进行限定 mutating inout mutating用于修饰会改变该类型的函数之前,基本都用于struct对象的修改。看下面例子: struct Point { var x: CGFloat var y: CGFloat // 因为该方法改变了struct的属性值(x),所以必须要加上mutating mutating func moveRight(offset: CGFloat) { x += offset } func normalSwap(a: CGFloat, b: CGFloat) { let temp = a a = b b = temp } // 将两个值交换,需传入对象地址。注意inout需要加载类型名前 func inoutSwap(a: inout CGFloat, b: inout CGFloat) { let temp = a a = b b = temp } }var location1: CGFloat = 10 var location2: CGFloat = -10var point = Point.init(x: 0, y: 0) point.moveRight(offset: location1) print(point) //Point(x: 10.0, y: 0.0)point.normalSwap(a: location1, b: location2) print(location1) //10 print(location2) //-10 // 注意需带取址符& point.inoutSwap(a: &location1, b: &location2) print(location1) //-10 print(location2) //10inout需要传入取值符,所以它的改变会导致该对象跟着变动。可以再回看上面说的Hashable的一个协议实现: func hash(into hasher: inout Hasher) { hasher.combine(self.red) hasher.combine(self.green) hasher.combine(self.blue) }只有使用inout才能修改传入的hasher的值。 infix operator infix operator即为中缀操作符,还有prefix、postfix后缀操作符。 它的作用是自定义操作符。比如Python里可以用**进行幂运算,但是Swift里面,我们就可以利用自定义操作符来定义一个用**实现的幂运算。 // 定义中缀操作符 infix operator ** // 实现该操作符的逻辑,中缀需要两个参数 func ** (left: Double, right: Double) -> Double { return pow(left, right) } let number = 2 ** 3 print(value) //8同理我们还可以定义前缀和后缀操作符: //定义阶乘操作,后缀操作符 postfix operator ~! postfix func ~! (value: Int) -> Int { func factorial(_ value: Int) -> Int { if value <= 1 { return 1 } return value * factorial(value - 1) } return factorial(value) } //定义输出操作,前缀操作符 prefix operator << prefix func << (value: Any) { print(value) }let number1 = 4~! print(number1) // 24<<number1 // 24 <<"zhangferry" // zhangferry前缀和后缀仅需要一个操作数,所以只有一个参数即可。 关于操作符的更多内容可以查看这里:Swift Operators。 注意,因为该文章较早,其中对于操作符的一些定义已经改变。 @dynamicMemberLookup,@dynamicCallable 这两个关键词我确实没有用过,看到dynamic可以知道这两个特性是为了让Swift具有动态性。 @dynamicMemberLookup中文叫动态查找成员。在使用@dynamicMemberLookup标记了对象后(对象、结构体、枚举、protocol),实现了subscript(dynamicMember member: String)方法后我们就可以访问到对象不存在的属性。如果访问到的属性不存在,就会调用到实现的 subscript(dynamicMember member: String)方法,key 作为 member 传入这个方法。 举个例子: @dynamicMemberLookup struct Person { subscript(dynamicMember member: String) -> String { let properties = ["nickname": "Zhuo", "city": "Hangzhou"] return properties[member, default: "undefined"] } } //执行以下代码 let p = Person() print(p.city) //Hangzhou print(p.nickname) //Zhuo print(p.name) //undefined我们没有定义Person的city、nickname,name属性,却可以用点语法去尝试访问它。如果没有@dynamicMemberLookup这种写法会被编译器检查出来并报错,但是加了该关键词编译器就不会管它是不是存在都予以通过。 @dynamicCallable struct Person { // 实现方法一 func dynamicallyCall(withArguments: [String]) { for item in withArguments { print(item) } } // 实现方法二 func dynamicallyCall(withKeywordArguments: KeyValuePairs<String, String>){ for (key, value) in withKeywordArguments { print("\(key) --- \(value)") } } } let p = Person() p("zhangsan") // 等于 p.dynamicallyCall(withArguments: ["zhangsan"]) p("zhangsan", "20", "男") // 等于 p.dynamicallyCall(withArguments: ["zhangsan", "20", "男"]) p(name: "zhangsan") // 等于 p.dynamicallyCall(withKeywordArguments: ["name": "zhangsan"]) p(name: "zhangsan", age:"20", sex: "男") // 等于 p.dynamicallyCall(withKeywordArguments: ["name": "zhangsan", "age": "20", "sex": "男"])@dynamicCallable可以理解成动态调用,当为某一类型做此声明时,需要实现dynamicallyCall(withArguments:)或者dynamicallyCall(withKeywordArguments:)。编译器将允许你调用并为定义的方法。 一个动态查找成员变量,一个动态方法调用,带上这两个特性Swift就可以变成彻头彻尾的动态语言了。所以作为静态语言的Swift也是可以具有动态特性的。 更多关于这两个动态标记的讨论可以看卓同学的这篇:细说 Swift 4.2 新特性:Dynamic Member Lookup where where一般用作条件限定。它可以用在for-in、swith、do-catch中: let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9] for item in numbers where item % 2 == 1 { print("odd: \(item)") // 将输出1,3,5,7,9等数 }numbers.forEach { (item) in switch item { case let x where x % 2 == 0: print("even: \(x)") // 将输出2,4,6,8等数 default: break } }where也可以用于类型限定。 我们可以扩展一个字典的merge函数,它可以将两个字典进行合并,对于相同的Key值以要合并的字典为准。并且该方法我只想针对Key和Value都是String类型的字典使用,就可以这么做: // 这里的Key Value来自于Dictionary中定义的泛型 extension Dictionary where Key == String, Value == String { //同一个key操作覆盖旧值 func merge(other: Dictionary) -> Dictionary { return self.merging(other) { _, new in new } } }@autoclosure @autoclosure 是使用在闭包类型之前,做的事情就是把一句表达式自动地封装成一个闭包 (closure)。 比如我们有一个方法接受一个闭包,当闭包执行的结果为 true 的时候进行打印,分别使用普通闭包和加上autoclosure的闭包实现: func logIfTrueNormal(predicate: () -> Bool) { if predicate() { print("True") } } // 注意@autoclosure加到闭包的前面 func logIfTrueAutoclosure(predicate: @autoclosure () -> Bool) { if predicate() { print("True") } } // 调用方式 logIfTrueNormal(predicate: {3 > 1}) logIfTrueAutoclosure(predicate: 3 > 1)编译器会将logIfTrueAutoclosure函数参数中的3 > 1这个表达式转成{3 > 1}这种尾随闭包样式。 那这种写法有什么用处呢?我们可以从一个示例中体会一下,在Swift系统提供的几个短路运算符(即表达式左边如果已经确定结果,右边将不再运算)中均采用了@autoclosure标记的闭包。那??运算符举例,它的实现是这样的: public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T { switch optional { case .some(let value): return value case .none: return try defaultValue() } } // 使用 var name: String? = "ferry" let currentName = name ?? getDefaultName()因为使用了@autoclosure标记闭包,所以??的defaultValue参数我们可以使用表达式,又因为是闭包,所以当name非空时,直接返回了该值,不会调用getDefaultName()函数,减少计算。 参考:@AUTOCLOSURE 和 ??,注意因为Swift版本问题,实例代码无法运行。 @escaping @escaping也是闭包修饰词,用它标记的闭包被称为逃逸闭包,还有一个关键词是@noescape,用它修饰的闭包叫做非逃逸闭包。在Swift3及之后的版本,闭包默认为非逃逸闭包,在这之前默认闭包为逃逸闭包。 这两者的区别主要在于声明周期的不同,当闭包作为参数时,如果其声明周期与函数一致就是非逃逸闭包,如果声明周期大于函数就是逃逸闭包。结合示例来理解: // 非逃逸闭包 func logIfTrueNormal(predicate: () -> Bool) { if predicate() { print("True") } } // 逃逸闭包 func logIfTrueEscaping(predicate: @escaping () -> Bool) { DispatchQueue.main.async { if predicate() { print("True") } } }第二个函数的闭包为逃逸闭包是因为其是异步调用,在函数退出时,该闭包还存在,声明周期长于函数。 如果你无法判断出应该使用逃逸还是非逃逸闭包,也无需担心,因为编译器会帮你做出判断。第二个函数,如果我们不声明逃逸闭包编译器会报错,警告我们:Escaping closure captures non-escaping parameter 'predicate'。当然我们还是应该理解两者的区别。 四、高阶函数 Filter, Map, Reduce, flatmap, compactMap 这几个高阶函数都是对数组对象使用的,我们通过示例去了解他们吧: let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9] // filter 过滤 let odd = numbers.filter { (number) -> Bool in return number % 2 == 1 } print(odd) // [1, 3, 5, 7, 9]//map 转换 let maps = odd.map { (number) -> String in return "\(number)" } print(maps) // ["1", "3", "5", "7", "9"]// reduce 累计运算 let result = odd.reduce(0, +) print(result) // 25// flatMap 1.数组展开 let numberList = [[1, 2, 3], [4, 5], [[6]]] let flatMapNumber = numberList.flatMap { (value) in return value } print(flatMapNumber) // [1, 2, 3, 4, 5, [6]]// flatMap 2.过滤数组中的nil let country = ["cn", "us", nil, "en"] let flatMap = country.flatMap { (value) in return value } print(flatMap) //["cn", "us", "en"]// compactMap 过滤数组中的nil let compactMap = country.compactMap { (value) in return value } print(compactMap) // ["cn", "us", "en"]filter,reduce其实很好理解,map、flatMap、compactMap刚开始接触时确实容易搞混,这个需要多加使用和练习。 注意到flatMap有两种用法,一种是展开数组,将二维数组降为一维数组,一种是过滤数组中的nil。在Swift4.1版本已经将flatMap过滤数组中nil的函数标位deprecated,所以我们过滤数组中nil的操作应该使用compactMap函数。 参考:Swift 烧脑体操(四) - map 和 flatMap 五、几个Swift中的概念 柯里化什么意思 柯里化指的是从一个多参数函数变成一连串单参数函数的变换,这是实现函数式编程的重要手段,举个例子: // 该函数返回类型为(Int) -> Bool func greaterThan(_ comparer: Int) -> (Int) -> Bool { return { number in return number > comparer } } // 定义一个greaterThan10的函数 let greaterThan10 = greaterThan(10) greaterThan10(13) // => true greaterThan10(9) // => false所以柯里化也可以理解为批量生成一系列相似的函数。 参考:柯里化 (CURRYING) POP 与 OOP的区别 OOP(object-oriented programming)面向对象编程: 在面向对象编程世界里,一切皆为对象,它的核心思想是继承、封装、多态。 POP(protocol-oriented programming)面向协议编程: 面向协议编程则主要通过协议,又或叫做接口对一系列操作进行定义。面向协议也有继承封装多态,只不过这些不是针对对象建立的。 为什么Swift演变成了一门面向协议的编程语言。这是因为面向对象存在以下几个问题: 1、动态派发的安全性(这应该是OC的困境,在Swift中Xcode是不可能让这种问题编译通过的) 2、横切关注点(Cross-Cutting Concerns)问题。面向对象无法描述两个不同事物具有某个相同特性这一点。 3、菱形问题(比如C++中)。C++可以多继承,在多继承中,两个父类实现了相同的方法,子类无法确定继承哪个父类的此方法,由于多继承的拓扑结构是一个菱形,所以这个问题有被叫做菱形缺陷(Diamond Problem)。 参考文章: Swift 中的面向协议编程:是否优于面向对象编程? 面向协议编程与 Cocoa 的邂逅 (上) Any 与AnyObject 区别 AnyObject: 是一个协议,所有class都遵守该协议,常用语跟OC对象的数据转换。 Any:它可以代表任何型別的类(class)、结构体 (struct)、枚举 (enum),包括函式和可选型,基本上可以说是任何东西。 rethrows 和 throws 有什么区别呢? throws是处理错误用的,可以看一个往沙盒写入文件的例子: // 写入的方法定义 public func write(to url: URL, options: Data.WritingOptions = []) throws // 调用 do { let data = Data() try data.write(to: localUrl) } catch let error { print(error.localizedDescription) }将一个会有错误抛出的函数末尾加上throws,则该方法调用时需要使用try语句进行调用,用于提示当前函数是有抛错风险的,其中catch句柄是可以忽略的。 rethrows与throws并没有太多不同,它们都是标记了一个方法应该抛出错误。但是 rethrows 一般用在参数中含有可以 throws 的方法的高阶函数中(想一下为什么是高阶函数?下期给出答案)。 查看map的方法声明,我们能同时看到 throws,rethrows: @inlinable public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]不知道你们第一次见到map函数本体的时候会不会疑惑,为什么map里的闭包需要抛出错误?为什么我们调用的时候并没有用try语法也可以正常通过? 其实是这样的,transform是需要我们定义的闭包,它有可能抛出异常,也可能不抛出异常。Swift作为类型安全的语言就需要保证在有异常的时候需要使用try去调用,在没有异常的时候要正常调用,那怎么兼容这两种情况呢,这就是rethrows的作用了。 func squareOf(x: Int) -> Int {return x * x}func divideTenBy(x: Int) throws -> Double { guard x != 0 else { throw CalculationError.DivideByZero } return 10.0 / Double(x) }let theNumbers = [10, 20, 30] let squareResult = theNumbers.map(squareOf(x:)) // [100, 400, 9000]do { let divideResult = try theNumbers.map(divideTenBy(x:)) } catch let error { print(error) }当我们直接写let divideResult = theNumbers.map(divideTenBy(x:))时,编译器会报错:Call can throw but is not marked with 'try'。这样就实现了根据情况去决定是否需要用try-catch去捕获map里的异常了。 参考:错误和异常处理 break return continue fallthough 在语句中的含义(switch、while、for) 这个比较简单,只说相对特别的示例吧,在Swift的switch语句,会在每个case结束的时候自动退出该switch判断,如果我们想不退出,继续进行下一个case的判断,可以加上fallthough。
iOS开发图片格式选择
图片是如何显示的 在讲解如何选择图片格式之前,我感觉有必要先了解下,图片是如何展示的。如果我们要展示一张图片,一般步骤是这样的: /// Assets.xcassets中的图片,不需要后缀 let image = UIImage(named: "icon") let imageView = UIImageView(frame: rect) imageView.image = image view.addSubview(imageView)运行程序,我们就可以在指定位置看到这个icon。看似简单的代码背后隐藏了很多细节工作。一张图片的展示,从代码执行到展示出来大致经历了这些步骤:1. 加载图片从磁盘中加载一张图片;然后将生成的 UIImage 赋值给 UIImageView ;接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;分配内存缓冲区用于管理文件 IO 和解压缩操作,将文件数据从磁盘读到内存中;2. 图片解码(解压)将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作,默认在主线程进行;3. 图片渲染Core Animation 中CALayer使用解压(解码)的位图数据渲染 UIImageView 的图层;CPU计算好图片的Frame,对图片解压之后,就会交给GPU来做图片渲染渲染流程;GPU获取获取图片的坐标,将坐标交给顶点着色器(顶点计算),将图片光栅化(获取图片对应屏幕上的像素点),片元着色器计算(计算每个像素点的最终显示的颜色值);从帧缓存区中渲染到屏幕上;这其中有个关键步骤是图片解码。那为什么要解码呢,这是因为我们平常使用的图片一般为了节约空间都会经过一些压缩算法进行封装,而使用时屏幕要精确的渲染到每个像素点,这就需要把压缩的图片解码展开,便于系统处理。 名词解释 有损vs无损 有损压缩:指在压缩文件大小的过程中,损失了一部分图片的信息,也即降低了图片的质量,并且这种损失是不可逆的,我们不可能从有一个有损压缩过的图片中恢复出全来的图片。常见的有损压缩手段,是按照一定的算法将临近的像素点进行合并。 无损压缩:只在压缩文件大小的过程中,图片的质量没有任何损耗。我们任何时候都可以从无损压缩过的图片中恢复出原来的信息。 索引色vs直接色 索引色:用一个数字来代表(索引)一种颜色,在存储图片的时候,存储一个数字的组合,同时存储数字到图片颜色的映射。这种方式只能存储有限种颜色,通常是256种颜色,对应到计算机系统中,使用一个字节的数字来索引一种颜色。 直接色:使用四个数字来代表一种颜色,这四个数字分别代表这个颜色中红色、绿色、蓝色以及透明度。现在流行的显示设备可以在这四个维度分别支持256种变化,所以直接色可以表示2的32次方种颜色。当然并非所有的直接色都支持这么多种,为压缩空间使用,有可能只有表达红、绿、蓝的三个数字,每个数字也可能不支持256种变化之多。 点阵图vs矢量图 点阵图:也叫做位图,像素图。构成点阵图的最小单位是象素,位图就是由象素阵列的排列来实现其显示效果的,每个象素有自己的颜色信息,在对位图图像进行编辑操作的时候,可操作的对象是每个象素,我们可以改变图像的色相、饱和度、明度,从而改变图像的显示效果。点阵图缩放会失真,用最近非常流行的沙画来比喻最恰当不过,当你从远处看的时候,画面细腻多彩,但是当你靠的非常近的时候,你就能看到组成画面的每粒沙子以及每个沙粒的颜色。 矢量图:也叫做向量图。矢量图并不纪录画面上每一点的信息,而是纪录了元素形状及颜色的算法,当你打开一张矢量图的时候,软件对图形象对应的函数进行运算,将运算结果[图形的形状和颜色]显示给你看。无论显示画面是大还是小,画面上的对象对应的算法是不变的,所以,即使对画面进行倍数相当大的缩放,其显示效果仍然相同(不失真)。 几种格式的对比 一张图片,如果我们将它的每一个像素及其对应的颜色都存储起来(BMP格式),是会很大的。为了减小图片占用的存储空间,派生出了各种不同压缩算法所代表的图片格式。常见的图片格式有png、jpeg、heic、gif、webp,svg等。 PNG PNG有两种类型:PNG-8和PNG-24。PNG-8是无损的、索引色、点阵图。它支持透明度的调节。PNG-24是无损的、直接色、点阵图。因为使用直接色,颜色范围更大,也占用更大的空间。他的目标是替代JPEG,但一般而言,PNG-24的文件大小是JPEG的五倍之多,而显示效果则通常只能获得一点点提升。所以如果不是对图片质量要求特别高,不建议使用PNG-24PNG是苹果推荐的图片格式,而且这些图片会被一个叫pngcrush的开源工具优化,这样iOS设备就能在显示时更快的解压和渲染图片。该工具位于目录: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/binJPEG JPEG是有损的、采用直接色的、点阵图压缩方式。 JEPG目标是在不影响人类可分辨的图片质量的前提下,尽可能的压缩文件大小。一般都是用于相机图片的格式。但因为有损,会导致图片失真。iOS可以通过以下方式压缩图片: // 压缩比范围从0到1 func jpegData(compressionQuality: CGFloat) -> Data?题外话 一般来说,相同的图片采用JPEG的压缩方式会比png得到更小的尺寸,但也有例外。在网上查了资料说是JEPG更适合处理带有很多杂色的风景图,而对于使用数位板等电子绘制的纯色卡通系风格图片,JEPG的压缩方式会适得其反,导致体积更大。 HEIC HEIC是HEIF(High Efficiency Image Format 高效率图像文件格式)的一种。它并非苹果开发,而是由运动图像专家组(MPEG)开发。它同时支持有损压缩、无损压缩、透明度等特性。HEIF规范的完成是在2015年,是这几种图片格式中最新的一种了,目前除了苹果,还没有哪家大厂去拥抱这种格式。 在iOS 11更新后,iPhone 7及其后硬件,在拍摄照片时的默认图像存储格式。与JPG相比,它占用的空间更小,画质更加无损。HEIC的目的就是作为JPEG的继任者,以后或许会成为一种趋势。目前可以想到的在开发中的应用是,对于一些需要下载的大图可以转成HEIC格式,供客户端使用。但是当前却很少应用,大概率是考虑到图片兼容问题吧。 题外话 一个有趣的现象,我用相机(iPhoneXR)拍摄一张照片,通过AirDrop传到电脑,显示为HEIC格式。当我在拍照时选择系统自带的任意一种滤镜,图片格式就变成了JPEG。这是为什么? 有小伙伴解答: iOS拍照选择滤镜会“转”为JPEG,是因为拍照的格式还是HEIF,加滤镜和编辑图片都是相当于复制了一份再做操作的,点击复原又会“转”为HEIF。 经过测试确实是这样的,而且既然苹果提供复原的操作,说明原图(HEIF)并没有被覆盖。那为什么滤镜不能直接在HEIF格式下操作,猜测可能是跟滤镜的算法相关,该算法只能对JEPG格式编码的图片进行渲染,所以需要中间转成JEPG。 Live Photo Live图片的实质是:一张heic格式封面图 + mov格式视频。 对于Live Photo的展示,在原生应用中可以使用PHLivePhotoView,在Web应用中可以使用LivePhotosKit JS。 WebP WebP最初由Google发布于2010年,图片格式派生自VP8视频编码,也同时支持有损压缩、无损压缩、透明度等特性。2013年低,推出了Animated WebP,还可以支持动图。 WebP 集合了多种图片文件格式的特点。它像 JPEG 一样适合压缩照片和其他细节丰富的图片,像 GIF 一样可以显示动态图片,像 PNG 一样支持透明图像。根据 Google 的测试,WebP 无损压缩图片比 PNG 图片少了 45% 的文件体积,即使这些 PNG 图片在使用 pngcrush 和 PNGOUT 处理后,WebP 依旧可以减少 28% 的文件体积。可以在点击这里查看WebP对其它格式转换的效果。小是WebP的最大优点,小意味着更少的流量,这也是各大流量入口在意的地方。目前Google、Facebook、阿里等大厂已经在广泛使用WebP格式,国内的一些图床服务(七牛、又拍云)也支持将图片自动转成WebP格式。 诚然WebP非常优秀,独自完成了图片格式"大一统"的任务。但苹果对WebP的支持却很少,只Safari目前还不支持WebP显示就阻断了很多人应用WebP的决心。 如果我们需要在项目中显示WebP格式图片就不得不导入Google的libwebp解码库。当然WebP的解码任务在iOS端有些库已经封装好了,OC端可以用SDWebImageWebPCoder,Swift端可以用KingfisherWebP。以下是使用Kingfisher展示WebP图像的事例: // 全局配置对WebP图片的解码(仅针对WebP格式) KingfisherManager.shared.defaultOptions += [ .processor(WebPProcessor.default), .cacheSerializer(WebPSerializer.default) ] // 本地webp图片解码 let localUrl = Bundle.main.url(forResource: "sunset", withExtension: "webp") let dataProvider = LocalFileImageDataProvider.init(fileURL: localUrl!) imageView.kf.setImage(with: dataProvider)// 远程webp图片解码。一些图像服务器可能期望“Accept”标头包含“image/webp”,我们还需要加上 let modifier = AnyModifier { request in var req = request req.addValue("image/webp */*", forHTTPHeaderField: "Accept") return req } KingfisherManager.shared.defaultOptions += [ .requestModifier(modifier), // ... other options ]PDF pdf图片通常是矢量的,它的导入方式有些特殊。我们需要在Assets.xcassets文件,创建一个New Image Set,然后将该文件的Scales设置为Single Scale,拖入1x尺寸的pdf文件即可:使用时我们可以把它当做普通图片对待: let image = UIImage(named: "sunset")在运行期间Xcode会根据屏幕的比例因子生成对应尺寸的png图像。比如导入一张100x100的pdf图片,在2x和3x的机型里面会生成对应的200x200,300x300的png(可以在Assets.car中找到)。所以pdf只不过是Xcode处理图片的中间状态,下载到手机的应用包里面是没有这张pdf的。 这种处理方式有一个好处就是,当苹果以后发布一款4x屏幕的手机时,使用pdf处理的图片会自适应生成对应的4x资源,不需要再手动导入。但相比优点,pdf作为图片资源的缺点更多。 首先是尺寸上,因为是自动生成对应的png,并没有任何优化和压缩,而且我们也并不能在这中间做什么。对比相同尺寸经过ImageOptim压缩过的png,在大小上后者会是前者的1/2,甚至1/4。 另外pdf对阴影和渐变的处理会存在失真的情况:左边是png,右边是pdf。在一些渐变和光影的图像部分可以看出明显的失真。 更多关于pdf和png的差别,可以看这篇:Why I don't use PDFs for iOS assets: https://bjango.com/articles/idontusepdfs/ SVG/SF Symbol SVG是一种无损的矢量图,是众多矢量图中的一种,它的特点是使用XML来描述图片。使用XML的优点是,任何时候你都可以把它当做一个文本文件来对待,也就是说,你可以非常方便的修改SVG图片,你所需要的只需要一个文本编辑器。 在iOS13之前应用中直接使用SVG的场景非常少,但从iOS13开始,苹果推出了SF Symbol,一种svg格式的矢量符号集。而且苹果还提供了多于1500多种icon模板,我们可以在这里下载查看。我们可以从中选择适合自己的icon,选中之后,从File > Export Custom Symbol Templete中导出svg格式图片集,然后拖到Xcode的Assets.xcassets。SF Symbol有9种粗细的调节——从ultralight到black——每一种都相当于San Francisco系统字体的重量(weight)。(SF Symbol中的SF是San Francisco(旧金山)的缩写)。这种对应使您能够在符号和相邻文本之间实现精确的权重匹配,同时支持不同大小和上下文的灵活性。 当然如果这些图标都不能满足需求,我们还可以自定义SF图标,然后通过SF Symbol App进行验证和导出。操作细节可以看这里:Creating Custom Symbol Images for Your App。 SF Symbol使用起来也很简单: let configuration = UIImage.SymbolConfiguration.init(scale: .small) imageView.image = UIImage(systemName: "alarm", withConfiguration: configuration)SF Symbol可以一次性解决相同icon,不同尺寸,不同粗细的问题,它让我们处理图片像处理字体一样方便。可以想象这就是应用图标的未来。 当看到SF Symbol仅支持iOS13+,watchOS6+,我又不得不退回到现实,png也挺好的。 题外话 我在测试SF Symbol图标时,从生成的应用包中查看图片,会得到这样的结果:代码中的我将图片设置为100x100,仅有这一处地方使用。跟pdf类似我们找不到svg源文件,这好理解,svg只是中间状态,我们最终使用的还是png,但为什么会有多个小尺寸的png图像呢? 如何选择图片格式 我们平常开发时,使用最多的就是png了,甚至可能是不加考虑的全部使用png。其实这样是不好的,我们应该充分发挥不同格式图片的优点,从兼容性、空间占用、展示效果三方面考量选取最佳格式。 关于图片格式的选择,苹果的Human Interface Guidelines有以下说法:一般情况下,使用PNG图片,因为PNG支持透明性,而且是无损的,压缩工件不会模糊重要的细节或改变颜色。对于需要阴影、纹理和高光效果的复杂艺术品来说,这是一个不错的选择。使用8位的PNG图形,不需要完全24位的颜色。8位PNG可以在不降低图像质量的情况下减小文件大小。精致的应用图标最好使用png。 对于照片应该使用JPEG格式,它的压缩算法通常比无损格式产生更小的尺寸,而且很难在照片中辨别出来。应该尝试优化JPEG文件,在大小和质量之间找到平衡。大多数JPEG文件可以被压缩而不会导致图像明显的退化。即使是少量的压缩也可以节省大量的磁盘空间。 使用PDF处理字形和其他需要高分辨率缩放的平面矢量图形。最终可以做以下总结。图片格式 适用范围 注意事项png 应用icon,界面icon,卡通风格的背景图 导入项目前可以使用ImageOptim进行压缩jpeg 尺寸较大的风景图,照片 不支持透明度;因为可以调节压缩比,可以在大小和质量之间寻找最佳平衡。webp 支持有损、无损压缩、透明度、动图等特性,因为苹果本身不支持一般只应用于服务端返回来的图片 无法在xcode预览,不建议内置该类型图片pdf 字形,高分辨率的矢量图 存在展开尺寸较大,光效失真的情况svg(sf symbol) 指示性icon 仅支持iOS13及以上,系统sf符号是有版权的,使用时要注意应用范围和苹果要求引用 谈谈 iOS 中图片的解压缩 图片格式那么多,哪种更适合你 iOS图片格式选择 Why I don't use PDFs for iOS assets SF Symbols: The benefits and how to use them guide WebP 的前世今生
快手iOS面经
- 28 Mar, 2020
背景 过完年来北京之后,有准备看看机会,也是想了解下市场行情。简历没有投太多,只定向投了头条教育部门、抖音、快手、阿里,这些公司。头条和阿里的简历都没过,肯定是亮点太少吧。只有快手简历过了,快手是三轮技术面+一轮HR面,前两轮技术都比较顺利,到第三轮却栽了,很痛心o(╥﹏╥)o。目前就不考虑换工作了,等下半年再说了,接下来的时间再好好精炼一下。 快手是视频面试,不支持周末,但是可以选择晚上时间,我这几次都是定在了晚上九点。视频面试是通过牛客网进行的,以下是我还记得下来的各轮面试题,对于一些iOS基础知识就不做解答了。一面 1、用递归写一个算法,计算从1到100的和。 func sum(value: Int) -> Int { if value <= 0 { return 0 } var number = value return value + sum(value: number - 1) } // 计算过程 let result = sum(value: 100) print(result)写完算法之后又围绕着问了几个问题,都是算法基础:算法的时间复杂度是多少 递归会有什么缺点 不用递归能否实现,复杂度能否降到O(1)2、property的作用是什么,有哪些关键词,分别是什么含义? 3、父类的property是如何查找的? 4、NSArray、NSDictionary应该如何选关键词? 5、copy和muteCopy有什么区别,深复制和浅复制是什么意思,如何实现深复制? 6、用runtime做过什么事情?runtime中的方法交换是如何实现的? 7、讲一下对KVC合KVO的了解,KVC是否会调用setter方法? 8、__block有什么作用 9、说一下对GCD的了解,它有那些方法,分别是做什么用的? 10、对二叉树是否了解? 面试官是想接着问这方面的问题的。我当时说了不了解,然后就没有后续了。 二面 1、ARC和MRC的区别,iOS是如何管理引用计数的,什么情况下引用计数加1什么情况引用计数减一? 2、在MRC下执行[object autorelease]会发生什么,autorelease是如何实现的? 3、CoreAnimation是如何绘制图像的,动画过程中的frame能否获取到? 4、谈一下对Runlop的了解? 5、OC如何实现多继承? 这个当时没有答好。其实借助于消息转发,protocol和类别都可以间接实现多继承。 6、对设计模式有什么了解,讲一下其中一种是如何使用的。 7、有没有哪个开源库让你用的很舒服,讲一下让你舒服的地方。 我这里说了RxSwift中的观察者模式,和响应式编程。然后面试官问,如果要用OC实现一套RxSwift那样的逻辑应该怎么做。我回答的是结合KVO,将一些需要观察的属性,通过KVO进行监听,然后通过block回调出来。 8、一张100*100,RGBA的png图像解压之后占多大内存空间。 RGBA > FFFFFFFF > 4字节 所以会占用:(100 * 100 * 4) / 1024 = 39KB 9、算法题 题目:给定一个个数字arr,判断数组arr中是否所有的数字都只出现过一次。 这个并没有要求写出来,说是提供思路就行了。我当时给的方案是在便利数组的时候,用一个字典把便利的元素存起来,如果在后面的便利过程中新元素在字典中存在过就说明,有重复数字出现。时间复杂度是O(n)。 当时也问了有没有办法进行优化,我当时想到了将数组转成Set,然后和原数组比较,两个集合的数量是否变化。 10、因为我跟他介绍自己Swift用的多一些,然后问了些Swift跟OC的区别,各自的优缺点。 11、为什么离职,有什么职业规划。 三面 1、给定一个Int型数组,用里面的元素组成一个最大数,因为数字可能非常大,用字符串输出。 输入: [3,30,34,5,9] 输出: 9534330这个是leetcode的179题,难度中等。面试官让先说思路,再去做题。事先说一下这个题我没有做过。当时的思路是用冒泡法进行排序,排序的前提是将较少位数的数字进行循环补齐,例如3和30的比较,变成33和30的比较,34和4的比较变成34和44的比较,然后将结果从大到小整合成字符串输出。 但是做题是却发现没那么简单,位数的补齐对于2位和3位数的比较还需要求位数的最小公倍数,将他们都转成6位数才能比较。在挣扎了5分钟做了就做罢了。 后来再去做这道题,其实这就是一个排序而已,只不过他的规则是按高位优先级更高的原则,而这一点跟字符串的比较保持一致,如果再加一些Swift的高阶函数,就可以写成: func largestNumber(_ nums: [Int]) -> String { let sort = nums.map {"\($0)"}.sorted { (lStr, rStr) -> Bool in return lStr + rStr > rStr + lStr } let result = sort.joined() if result.prefix(1) == "0" { return "0" } else { return result } }2、项目中有这么一个方法func findfile(dir: String suffix: String) -> [String] ,可以通过输入文件夹目录,和后缀检索出所需的文件。 例如需要在某个文件中检索txt文件或者mp4文件,那就传入dir和suffix就行了。现在又有一些需求,例如需要检索utf8格式的txt或者h264编码的mp4,也会有一些例如查找最近一周更新过的文件这样的需求,你如何优化这个类,让它满足这些情况? 我首先想到的是这么多需求不可能一个方法就完成,需要根据不同场景拆出不同的方法,但是这些同属于文件操作,会有一个共同使用的方法就是检索文件。这个方法需要传入文件目录,然后递归的返回当前目录所有文件路径。外部不同场景的调用逻辑就用一个enum完成,不同值对应相同范围的不同种类。 面试官比较关注内部共用的文件检索怎么写,他说子文件如果过多怎么办,如何优化。我有点懵,查找文件至少是要遍历一遍的,子文件过多,这个应该是没法优化的啊。中间卡了一段时间,后来他给了提示说是不是可以用block实现,将文件路径返回出去,由外部决定当前文件是否可用,最终外部的调用类是这个样子。 //我的方案 //func findDir(_ dir: String) -> [String] //block方案 func findDir(_ dir: String, block: ((String) -> Bool))我想来确实没毛病,用block返回内容至少不会将该目录的所有文件都由一个对象持有,而前面一堆的铺垫其实也都是为验证block方案的好处。 其实事后想下这个问题没啥难的,这种写法自己也有写过,但当时就是没想起来,可能前面一圈的铺垫给我带偏了吧,说亏也不亏,以后多多努力吧。 总结 整体来看,快手的面试题跟我在别处看到的iOS面试题对比要简单些,一面主要是基础知识,二面考察更全面一些,更多让自己谈一些对技术的理解,三面则是更偏实践一些。 算法虽然三轮都有,但相对比较简单,即使写不出来,有思路也是可以的。当然写出来肯定是加分项,所以大家准备面试时,应该都看一下。算法相关的,排序,数组,二叉树,这几类是重点。
Runtime内存模型探究
- 23 Feb, 2020
Objective-C是一种通用、高级、面向对象的编程语言。它扩展了标准的ANSI C编程语言,将Smalltalk式的消息传递机制加入到ANSI C中。可以这么理解,Objective-C = C + Runtime,Runtime是将C语言具有面向对象功能的推动剂,是iOS开发中的核心概念。我们可以在苹果开源的 runtime(当前的最新版本objc4-779.1.tar.gz)中可以发现一些 Objective-C 对象模型的实现细节。NSObject的实现 OC中几乎所有的类都继承自NSObject,OC的动态性也是通过NSObject实现的,那就从NSObject开始探索。 在runtime源码中的NSObject.h中,我们可以找到NSObject的定义: @interface NSObject <NSObject> { Class isa OBJC_ISA_AVAILABILITY; }可以看出NSObject里有一个指向Class的isa,其中对于Class的定义在objc.h: /// An opaque type that represents an Objective-C class. typedef struct objc_class *Class;/// Represents an instance of a class. struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY; };objc_class代表类对象,objc_object代表实例对象,objc_object的isa指向objc_class。 这里可以得出一个结论,实例对象的isa是指向类(类对象)的。其实类(objc_class)也有一个isa属性,那它指向什么呢? Meta Class(元类) 这里runtime为了设计上的统一性,引入了元类(meta class)的概念。 对象的实例方法调用时,通过对象的 isa 在类中获取方法的实现。类对象的类方法调用时,通过类的 isa 在元类中获取方法的实现。 objc_class的isa指向meta class,甚至meta class也有isa指针,它指向根元类(root meta class)。实例对象,类对象,元类和根元类的关系如下图所示:类和元类形成了一个完整的闭环,其中有两条关系需要注意:元类的isa均指向根元类,根元类指向自己 根元类继承根类(NSObject)objective-c1.0数据模型 我们可以在runtime.h中查看objc_class的定义。 struct objc_class { Class _Nonnull isa OBJC_ISA_AVAILABILITY;#if !__OBJC2__ Class _Nullable super_class OBJC2_UNAVAILABLE; const char * _Nonnull name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; #endif} OBJC2_UNAVAILABLE; /* Use `Class` instead of `struct objc_class *` */注意这两个宏命令:!__OBJC2__和OBJC2_UNAVAILABLE,他们均是为了提示当前的objc_class结构是Objc2之前的结构设计,即Objc1.0的设计。 从这个objc_class的定义我们可以看出它包含了超类的指针(super_class),类名(name),实例大小(instance_size),objc_ivar_list成员变量列表的指针(ivars),指向objc_method_list指针的指针(methodLists)。 注意*methodLists是指向方法列表的指针,可以动态修改*methodLists的值来添加成员方法,这也是Category实现的原理,同样解释了Category不能添加属性的原因。 剩下的objc_cache代表函数的缓存列表,objc_protocol_list代表协议列表。 Objective语言历史 我在网上查资料的时候发现关于runtime的文章非常多,但提示数据模型在OC1.0和2.0之间区别的非常少,其实这一点很重要的。这也是为什么我将这段标题命名为Objective-C1.0数据模型的原因。 这里补一点Objective-C语言的发展历史(维基百科):Objective-C1.0 即Objective-C++ 由Stepstone 公司的布莱德·考克斯(Brad Cox)和 汤姆·洛夫(Tom Love) 在 1980 年代发明。它是GCC的一个前端,它可以编译混合C++与Objective-C语法的源文件。Objective-C++是C++的扩展,类似于Objective-C是C的扩展。 Objective-C2.0 在2006年7月苹果全球开发者会议中,Apple宣布了“Objective-C 2.0”的发布,其增加了“现代的垃圾收集,语法改进,运行时性能改进,以及64位支持”。Objective2.0数据模型 可以在objc-runtim-new.h文件找到新版对objc_class的数据模型定义: struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache; // formerly cache pointer and vtable class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags class_rw_t *data() const { return bits.data(); } }struct objc_object { private: isa_t isa; }union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { } Class cls; uintptr_t bits; }会发现objc_class不再是一个单独的结构体,而是继承于objc_object,objc_object内部的isa变成了isa_t的联合体。 class_data_bits_t 我们再回来看类中的其他属性,之前表示类的属性、方法、以及遵循的协议都放在了class_data_bits_t中,更准确的说是放在了class_rw_t。 struct class_data_bits_t { friend objc_class; // Values are the FAST_ flags above. uintptr_t bits; class_rw_t* data() const { return (class_rw_t *)(bits & FAST_DATA_MASK); } }struct class_rw_t { // Be warned that Symbolication knows the layout of this structure. uint32_t flags; uint16_t version; uint16_t witness; const class_ro_t *ro; method_array_t methods; property_array_t properties; protocol_array_t protocols; }struct class_ro_t { uint32_t flags; uint32_t instanceStart; uint32_t instanceSize; #ifdef __LP64__ uint32_t reserved; #endif const uint8_t * ivarLayout; const char * name; method_list_t * baseMethodList; protocol_list_t * baseProtocols; const ivar_list_t * ivars; const uint8_t * weakIvarLayout; property_list_t *baseProperties; }这里面引入了class_rw_t和class_ro_t(rw-readwrite,ro-readonly)两个结构体。可以看到class_rw_t是包含一个常量指针ro,结构体为class_ro_t。这里存储了当前类在编译期就已经确定的属性、方法以及遵循的协议。在 ObjC 运行时的时候会调用 realizeClass 方法,将class_ro_t传入class_rw_t,所以新版的动态性是通过这种方式实现的。 cache_t struct cache_t { static bucket_t *emptyBuckets(); struct bucket_t *buckets(); mask_t mask(); mask_t occupied(); }struct bucket_t { // IMP-first is better for arm64e ptrauth and no worse for arm64. // SEL-first is better for armv7* and i386 and x86_64. #if __arm64__ explicit_atomic<uintptr_t> _imp; explicit_atomic<SEL> _sel; #else explicit_atomic<SEL> _sel; explicit_atomic<uintptr_t> _imp; #endif }cache_t是objc_class中的缓存结构体,里面通过bucket_t结构体存储一些最近调用的函数。设置cache最大的原因就是OC为动态语言,函数的执行是通过消息调用实现的,消息调用会首先查找当前类中的方法列表,如果找不到会查找父类,直到检索至NSObject依然找不到函数实现,就会进入消息转发流程。而为了节省每次查找函数表的开销,发明了cache_t。我们从bucket_t的内联函数中可以看出,缓存的SEL和IMP都是在内存中进行加载的。 method_t struct method_t { SEL name; const char *types; MethodListIMP imp; struct SortBySELAddress : public std::binary_function<const method_t&, const method_t&, bool> { bool operator() (const method_t& lhs, const method_t& rhs) { return lhs.name < rhs.name; } }; };这是函数的结构体,里面包含3个成员变量。SEL是方法的名字name。types是类型编码,类型可参考Type Encoding。IMP是一个函数指针,指向的是函数的具体实现。在runtime中消息传递和转发的目的就是为了找到IMP,并执行函数。 数据模型对比 最后总结下,Objc1.0到2.0的对比:这两张图片引用自寒神博客。 参考链接深入解析 ObjC 中方法的结构 神经病院 Objective-C Runtime 入院第一天—— isa 和 Class
VSCode配置Python版本
- 01 Feb, 2020
刚学习Python没多久,对VSCode也不是很熟悉,在一次解问题的过程中,加深了对这两者的印象,于是记录一下。 环境:Mac OS 10.15,VSCode 1.41.1 问题定位 我在尝试用VSCode写python爬虫,遇到一个问题ImportError: No module named requests。 通过问题描述很容易知道问题,这是因为导入了requests库,但是VSCode却没有找到这个库。问题解决 知道了问题,那就好办,安装requests就行了。requests需要通过pip安装,所以要先安装pip。在安装pip之前还要清楚一件事,就是当前的python版本,因为pip是跟python版本对应的。 升级python 因为python2已经不再维护了,所以要保证当前为python3版本。查看当前python版本在命令行输入python: WARNING: Python 2.7 is not recommended. This version is included in macOS for compatibility with legacy software. Future versions of macOS will not include Python 2.7. Instead, it is recommended that you transition to using 'python3' from within Terminal.Python 2.7.16 (default, Aug 24 2019, 18:37:03) [GCC 4.2.1 Compatible Apple LLVM 11.0.0 (clang-1100.0.32.4) (-macos10.15-objc-s on darwin Type "help", "copyright", "credits" or "license" for more information.得知当前python版本为2.7,所以我们要进行升级。 小提示:命令行运行python就是打开了终端python环境,如果我们想退出该环境,运行exit()即可。 下载python安装包 下载地址当前最新版本为3.8.1,我们下载之后进行安装。 更改默认python版本 虽然我们安装了python3.8,但此时命令行运行python,还是显示为2.7,这是因为我们电脑中运行着两个版本的python,默认版本还是2.7。我们需要更改这项默认配置。 在此之前我们还需要确认终端的shell环境,常见的shell环境是bash和zsh。怎么当前是哪一个呢?在终端运行: $ echo $SHELL1、bash 使用vim打开.bash_profile,当然你也可以使用别的文本编辑器。 vi .bash_profile在最后一行添加: alias python="/Library/Frameworks/Python.framework/Versions/3.8/bin/python3.8"保存之后,更新配置: source .bash_profile2、zsh 编辑.zprofile文件 vi .zprofile在最后一行添加: alias python="/Library/Frameworks/Python.framework/Versions/3.8/bin/python3.8"保存之后,更新配置: source .zprofile然后再次运行python,会看到: Python 3.8.1 (v3.8.1:1b293b6006, Dec 18 2019, 14:08:53) [Clang 6.0 (clang-600.0.57)] on darwin Type "help", "copyright", "credits" or "license" for more information.说明我们已经成功将python默认版本改成了3.8版本。 安装pip 推荐使用pip安装脚本进行安装: $ curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py # 下载安装脚本 $ sudo python get-pip.py # 运行安装脚本因为我们默认的python版本就是3.x版本,所以安装的pip会自动关联到python3版本上。 查看pip版本和路径 $ pip --version会得到以下信息: pip 20.0.2 from /Library/Python/2.7/site-packages/pip (python 2.7)为什么关联的是python2.7版本?这是因为我电脑上之前通过python2.7安装过pip。那对应的python3.8版本的pip呢?它是由pip3表示的,可以验证下: $ pip3 --version会得到: pip 20.0.2 from /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/pip (python 3.8)安装requests 到这一步就很简单了,注意使用pip3执行安装命令。 $ pip3 install requestsVSCode的python环境和本机python环境 我们成功安装了requests,但是在vscode中运行脚本依旧报无法找到requests库的错误。这时我开始怀疑vscode和本机的python环境可能是不同的,而且我本机有两个python版本,vscode用的是哪一个呢?不管怎么样,我应该需要让vscode的python环境指向本机的python3.8,因为我的pip3和requests都是基于这个版本的python安装的。 vscode的python环境 在vscode的python编辑界面的最下面一行会显示当前的python环境:点击它可以看到当前支持python版本:发现并没有我们的python3.8,所以我们需要手动指定python的版本。 指定vscode的python版本 使用快捷键Command + ,可以唤出vscode的设计界面。因为配置项非常多,我们通过搜索命令查询,键入[python path](中括号表示整体搜索),可以找到python路径配置的设置项:默认值是python,我们将它改为/usr/local/bin/python3,然后重启vscode,我们就可以看到当前的python版本已经改为3.8.1了。导入requests的代码执行也没有问题了! 疑问? 这里还有几个疑问: 1、python2.7和3.7版本都是vscode在安装python时自己装进去的吗? 2、如果不通过指定为本机python版本,仅使用vscode中的python版本能否安装pip? 有能帮助解答的小伙伴将不胜感激!
2019年总结
- 02 Jan, 2020
好像随着年龄的增长,对时间的感知就变得越来越弱了。总感觉2019年还是一个很新鲜的年份,但它却已经走完了,2020年好像很遥远的样子,但它已经到来了。来个总结吧,2019年,这一年有太多的酸甜苦辣。从工作、生活和规划三个方面展开说吧。工作年初原项目组有一位很厉害的开发离职了,当时是我俩共同负责一个项目。他开发经验比我多,而且工作态度和习惯都很好,是我见过的未数不多让我称赞的程序员。曾经一起工作的将近一年多时间里,我都以他为学习榜样。对于他的离职,我纠结了很长一段时间,自认这是对我对公司的一大损失。 后来很长的时间里,一直都没有招到合适的人,我便一个人扛起了项目。很多之前应该是两个人一起商量的问题都需要自己一个人去思考解决方式,渐渐的我不断磨练自己的独立思考能力。大半年下来,突然感觉自己被逼着成长了许多,从一个崇拜别人,向别人学习的角色变成了一个不惧怕任何技术难题,能够帮助别人解决困难的角色。 2019年学会的第一个最重要的道理,很多时候有一个可以依赖的对象,是会减弱自己探索和尝试的动力,而开发很多时候对技能的理解和掌握,乃至个人进步,都是要依靠自己的探索获取的。写作 2019年共产出27篇博文,27是一个挺大的数量,但令我满意的文章到不是很多。博客内容质量的重要性应该是要远大于数量的,这一点后面注意到了。 对技术博客的写作今年做了一些不同的尝试。 首先对于月报的更新持续到今年7月份,共产出了一共13篇文章,就停更了。月报内容主要由三部分组成:最近一段时间的bug修复经历和开发tip 阅读过的优秀博文 优秀的github库第一部分,这个受开发经历的影响,就我个人来说,很多时候没有太好的经验可以写,到最后时感觉内容不好撑起这一个模块。 第二部分,出现过几次前期准备不够,最后一两天才去寻找素材,导致推荐内容很一般。 第三部分,优秀库的推荐,写到第十期时就感觉乏力的。当然我知道这里的内容是接近无限的,但是很多如果我都不了解,不使用,只读一个简介就去推荐,会有种不负责任的感觉。 还有一部分原因是,到后来关注的人越来越少,接到的反馈也越来越少,应该是跟质量下降有关。做这个东西也确实费时间,很多时候我就想如果这些时间去打磨一篇技术博客,可能效果会更好。而且月报给人感觉时效性更强,跟新闻类似,过了这个时间之后好像过时了一样,大家很少会特意翻看之前的内容,而普通的技术博客却不会有这种问题,解题思路,技术干货几乎不存在过时问题。 另外我也一直在观察给我启发的 “老司机iOS周报”,他们只有博文推荐和代码库推荐,由多位联合编辑共同完成,人员、时间、内容质量都不是问题。但就我观察得知,“老司机”的号召力和响应力也在不断减弱。技术类人员对内容聚合类产品的粘性是没有固定知识分享平台高的。一篇周报,十篇左右文章,大致看去,自己感兴趣的可能就两三篇,新窗口打开,导流到别的地方,有时间的话读一读,时间不够就放到收藏夹。一般在周报的停留时间不超过3分钟。 基于这些原因吧,我也就不再更新月报了,而是把时间分配到别的事情上去。 公众号 另外,开始尝试写公众号,名字叫“iOS成长之路”。文章更新不多,其中最主要的原因就是我习惯了写博客,而且博客有一套自己的workflow。一般我是先写博客,然后将文章复制到别的平台。像之前的简书和后来的掘金,都是可以全量复制的。 转到公众号时,虽然可以直接将Markdown转成公众号形式,但对图片却不识别,需要单独导入。我需要把github上的图片下载下来,放到公众号编辑界面。如果图多,就是项很繁琐的流程。而且公众号还限制了每天只能发一篇文章,所以后面更新了几次之后就不再更新了。如果有小伙伴知道解决方案的话,希望能够告知。之后的话,会想把公众号这个事情再推进下。 生活那些经历 今年基本没有什么活动。 清明节,回家看妈妈,不自觉拍了很多家里,地里的照片,感觉老家哪里都是美的。那个我曾经抵触,一心想逃离的家乡,在这个春天里一下变得可爱,温馨起来。走到路上我想起曾经走在这里发生过的一切,到了家里我回想这个房屋里曾经发生过的一切,那些欢声笑语。记忆一点点展开,如果此时放弃工作,放弃所有,跟爸爸就在老家生活下去,也未尝不可,我这样想着,对未来的规划和期许,已经都不重要了。但是也只能想想,现实还有很多问题需要考虑,需要处理,我已经被社会规则裹挟着由不得自己了,这可能就是长大的无奈吧。 4月份中旬,参加了《天火》的电影宣发,见到了很多明星,但也并没有太多兴奋,就感觉明星跟普通人其实也没有太多不同。12月12号,电影上映,因为宣发时收到了很多到场礼品,就买了票去支持下,女友和我观点一致:电影很烂,昆凌演的一般,剧情太散。配备了近乎完美的制作团队,外加周杰伦的宣传,电影竟然如此扑街,真的很让人匪夷所思。今年娱乐活动极少,一共就去电影院看了两场电影,其中一部还是这种感受。剧情,剧情,剧情,这是电影的灵魂,真的很重要啊。 4月底,参加我们609寝室第一个结婚的室友婚礼,分别两年多了再相聚,大家都基本没变。我们还是曾经的我们,各奔东西,又再次相聚,真好。 4月份报名了半马,但是很遗憾没有中签,直接导致了去年的一项目标没有完成。 5月份没有出门,跟女朋友在家探索了很多美食的做法,每次做好吃的,我都会用手机拍下来,相册里已经有几十张美食照片了。做饭其实是一项很有趣的事情,洗菜,切菜,食材下锅一瞬间的滋滋声,出锅时的香气四溢,都能不断激发自己的食欲,这可比塑料彩盒包装的外卖有灵魂多了。所以这一年不光点外卖次数少了很多,厨艺还增进了不少。 6月份重新拾起了跑步的习惯,每天下午下班之前的半个小时都陪菲姐去园区跑道跑步,这个习惯帮助她减掉了十几斤肉,也帮助我在年度跑步计划中增加了将近200公里的里程。 7月份准备园区的篮球比赛,虽然每次练习都挥汗如雨,各种制定战术,尽心准备,但还是改变不了比赛被别的公司虐的结局。虽然篮球比不过他们,但是我们敲代码比他们厉害啊,这么安慰自己。但老实说,为什么我感觉打球比敲代码还有意思啊。教练,我想打球。 8月份,配父亲去郑州等肾脏配型,当时想的是父亲留在这里,等有手术消息了,我再赶过去,但是医院要求等待期间必须要有家属陪同,那就回不了北京了。跟公司说明情况之后想申请远程办公,当时想的是如果公司不同意就直接离职。最后公司领导商量之后决定同意我的请求,之后就开始了长达45天的在郑州远程办公。 9月份,工作日期间,我将作息时间调整到跟公司一样的点,有时候去附近的咖啡店,有时候就在家办公。这期间最大的挑战就是自律和需求沟通。因为有时候配父亲做检查,或者送饭,耽误一些时间,这个就要想办法从别的地方补上来,还有就是家里的工作氛围比着公司要差一下,注意力的持续状态要短一些。所以这段时间基本周六都会根据情况抽出时间用来补进度。当然远程办公最爽的一点就是时间可以自由安排,可以是忙完一项任务或者通过时间挪移,你就可以在任意时间段做自己能做的事情。 当然因为效率没有在公司高,所以空闲时间相较而言还是更少了。这里的调整就是时间规划和时间管理的内容了,这个在未来我会重点优化下。 10月份,参加我们609第二个结婚的小伙伴婚礼,还顺道去了一趟大学校园。校园里多了好几栋教学楼和宿舍楼,当年的公教没变,宿舍楼变成了女寝?图书馆还是那么霸气,甚至想进去找个地方看会书。多好的环境啊,为什么当初没有好好珍惜,努力学习呢。好像在回忆过往时,我从来都是责备自己不够努力,不够优秀。 但是好像这就是真的,2019年也是,我并没有拼进全力,很多事情都没有做好,我应该更加努力,多逼一逼自己才是。 11月份,一直陪父亲等的肾脏配型终于有了结果,父亲接受手术,我来郑州陪护。这段经历大多数记在博客里面,这期间确实让我成长了不少。后来父亲出院,我回北京的第二天在V站看到一个帖子,一个程序员长时间加班导致身体不适,去医院检查被诊断为CKD4期(慢性肾病4期)。而CKD5期就是尿毒症,就是父亲的那个病。虽然我在医院见过很多病人了,但这个病的魔爪伸到了程序员群体,还是很让我惊讶和担忧的。 不只尿毒症,很多病都在向低龄化发展。健康!健康!健康!这个真的非常重要,作息,饮食,这两个最容易导致身体出问题的环节一定要重视啊。 12月份,在天桥看了一场话剧《黄金百万》,演员均非科班出身,当时还只是第三场演出,所以整体感觉水分很大,甚至一度有中途撤离的冲动。话剧演出结束,演员讲述舞台背后的故事时,却让我得到了些意外收获。他们是由互联网圈,投资圈,主持人圈的几个朋友组建起来的,其中就有为人熟知的张泉灵和于光东。他们都不缺钱,也不指望依靠话剧挣钱,就凭着爱好,组织话剧社,在本职工作之余,挤时间进行排练,很多人都不在北京居住,但他们还是圆满的完成了这次表演。 这让我看到了一种生活态度,工作不应该完全占据生活,生活也不应该只是吃喝玩乐,我们应该发觉自己的爱好,潜力,去做一些能带给自己满足感,更有意义的事情,这样才是精彩的活着。所以2020年,我也会往这方面做一些尝试。 关于理财 看到很多人晒今年收益,50%,甚至120%。因为今年经济整体都是上行,大多数都是赚的,只是赚多赚少而已。像我这种理财小白,只是定投指数基金不动都有将近30%的收益。所以大家对于很多人的收益不要眼红,不要过于乐观。今年大好,明年是好还是坏谁也说不准了。理财的一条金科玉律就是,收益越大风险越大,能承受风险,有一定的专业知识,那就努力达到收益最大化。如果都做不到,那就不要定过高的目标,像我目前定的目标就是跑赢通货膨胀,保证手里的钱不贬值就行。 理财这项技能,可以看到上限还是很高的,以后会花更多的时间在上面做一些研究,补足这方面的能力,努力提高收益。 关于阅读 阅读方面,近几年看的书越来越少了,今年只有: 《软技能:代码之外的生存指南》 《图解TCP/IP》 《重构》 《未来简史》 《老人与海》 《白夜行》 《半小时漫画中国史+世界史》 《如何高效读懂一本书》这里最推荐的就是《软技能》,它介绍了程序员在编程能力之外还应该具备哪些能力,涵盖职业规划,自我营销,学习方法,理财手段,健身,精神提升等方面。这本书影响了我对自己职业的看法,同时也让我对未来的规划更坚定,更清晰了一些。非常建议大家都读一读。 OKR总结2019年OKR总结 O1:精进技术栈KR 完成进度 总结刷50道LeetCode,输出解题思路 10% 时间问题逆向实践 0% 逆向理论没有持续深入研究,时间问题翻译5篇技术文章 60% 翻译水平有待提高,输出较慢博客主题优化 10% 学习前端要系统的学习,低估了这个工作量开发微习惯APP 5% 发现有别的APP做的很好了,另因产品设计能力短时间没有补全,及时间问题,后来做罢博客输出24篇文章 112% 因为多了几篇生活中的文章,如果只算技术文章也是不够的总结:实现度较低,很多是因为时间问题,和对制定任务的难度预估不准确导致的。这一年对技术层面的提升总综合性,没有在某一点有所突出,今年需要做一些调整。 O2:攒更多的钱KR 完成进度 总结工资外收入达到1000 200% 有段时间帮助一个团队处理了几个问题,获得了一些报酬总收入达到买房需要的首付一半 50% 收入是达到了,但是存款却没有总结:这几项定的目标都有些含糊了,工资外收入其实更像表达的是,写博客的打赏,和公众号的一些收入。总收入的目标其实没有意义,存款才有意义。 O3:运动健身KR 完成进度 总结半程马拉松跑到2小时以内 0% 因为没有中签,也就没有跑半马的经历了咕咚累积里程达到500公里 99% 依靠每天下午的跑步,基本追上了这个目标总结:跑步里程到是一个合理的目标,半马因为受外界因素影响,所以是一个不太好的KR。 2020年OKR 有些在2019年没有做好的事情,会在新的一年里持续的跟进优化。 O1:精进技术栈 KR1:刷20道经典LeetCode题目,输出2篇解题思路的文章 KR2:学习前端知识,优化博客小站 KR3:输出5篇对计算机知识总结的文章 KR4:维护一个Swift库,用于筛选项目中不用的文件 O2:个人成长 KR1:公众号粉丝达到500 KR2:学习基金知识,分析对比10种基金的数据表现 KR3:全年跑步里程400公里 KR4:阅读15本书 KR5:培养时间规划能力,总结并践行一份时间规划清单
OC项目转Swift指南
- 08 Dec, 2019
运行环境:Xcode 11.1 Swift5.0最近参与的一个项目需要从Objective-C(以下简称OC)转到Swift,期间遇到了一些坑,于是有了这篇总结性的文档。如果你也有将OC项目Swift化的需求,可以作为参考。 OC转Swift有一个大前提就是你要对Swift有一定的了解,熟悉Swift语法,最好是完整看过一遍官方的Language Guide。 转换的过程分自动化和手动转译,鉴于自动化工具的识别率不能让人满意,大部分情况都是需要手动转换的。自动化工具 有一个比较好的自动化工具Swiftify,可以将OC文件甚至OC工程整个转成Swift,号称准确率能达到90%。我试用了一些免费版中的功能,但感觉效果并不理想,因为没有使用过付费版,所以也不好评价它就是不好。 Swiftify还有一个Xcode的插件Swiftify for Xcode,可以实现对选中代码和单文件的转化。这个插件还挺不错,对纯系统代码转化还算精确,但部分代码还存在一些识别问题,需要手动再修改。手动Swift化 桥接文件 如果你是在项目中首次使用Swift代码,在添加Swift文件时,Xcode会提示你添加一个.h的桥接文件。如果不小心点了不添加还可以手动导入,就是自己手动生成一个.h文件,然后在Build Settings > Swift Compiler - General > Objective-C Bridging Header中填入该.h文件的路径。这个桥接文件的作用就是供Swift代码引用OC代码,或者OC的三方库。 #import "Utility.h" #import <Masonry/Masonry.h>在Bridging Header的下面还有一个配置项是Objective-C Generated Interface Header Name,对应的值是ProjectName-Swift.h。这是由Xcode自动生成的一个隐藏头文件,每次Build的过程会将Swift代码中声明为外接调用的部分转成OC代码,OC部分的文件会类似pch一样全局引用这个头文件。因为是Build过程中生成的,所以只有.m文件中可以直接引用,对于在.h文件中的引用下文有介绍。 Appdelegate(程序入口) Swift中没有main.m文件,取而代之的是@UIApplicationMain命令,该命令等效于原有的执行main.m。所以我们可以把main.m文件进行移除。 系统API 对于UIKit框架中的大部分代码转换可以直接查看系统API文档进行转换,这里就不过多介绍。 property(属性) Swift没有property,也没有copy,nonatomic等属性修饰词,只有表示属性是否可变的let和var。 注意点一 OC中一个类分.h和.m两个文件,分别表示用于暴露给外接的方法,变量和仅供内部使用的方法变量。迁移到Swift时,应该将.m中的property标为private,即外接无法直接访问,对于.h中的property不做处理,取默认的internal,即同模块可访问。 对于函数的迁移也是相同的。 注意点二 有一种特殊情况是在OC项目中,某些属性在内部(.m)可变,外部(.h)只读。这种情况可以这么处理: private(set) var value: String就是只对value的set方法就行private标记。 注意点三 Swift中针对空类型有个专门的符号?,对应OC中的nil。OC中没有这个符号,但是可以通过在nullable和nonnull表示该种属性,方法参数或者返回值是否可以空。 如果OC中没有声明一个属性是否可以为空,那就去默认值nonnull。 如果我们想让一个类的所有属性,函数返回值都是nonnull,除了手动一个个添加之外还有一个宏命令。 NS_ASSUME_NONNULL_BEGIN /* code */ NS_ASSUME_NONNULL_ENDenum(枚举) OC代码: typedef NS_ENUM(NSInteger, PlayerState) { PlayerStateNone = 0, PlayerStatePlaying, PlayerStatePause, PlayerStateBuffer, PlayerStateFailed, };typedef NS_OPTIONS(NSUInteger, XXViewAnimationOptions) { XXViewAnimationOptionNone = 1 << 0, XXViewAnimationOptionSelcted1 = 1 << 1, XXViewAnimationOptionSelcted2 = 1 << 2, }Swift代码 enum PlayerState: Int { case none = 0 case playing case pause case buffer case failed } struct ViewAnimationOptions: OptionSet { let rawValue: UInt static let None = ViewAnimationOptions(rawValue: 1<<0) static let Selected1 = ViewAnimationOptions(rawValue: 1<<0) static let Selected2 = ViewAnimationOptions(rawValue: 1 << 2) //... }Swift没有NS_OPTIONS的概念,取而代之的是为了满足OptionSet协议的struct类型。 懒加载 OC代码: - (MTObject *)object { if (!_object) { _object = [MTObject new]; } return _object; }Swift代码: lazy var object: MTObject = { let object = MTObject() return imagobjecteView }()闭包 OC代码: typedef void (^DownloadStateBlock)(BOOL isComplete);Swift代码: typealias DownloadStateBlock = ((_ isComplete: Bool) -> Void)单例 OC代码: + (XXManager *)shareInstance { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }); return instance; }Swift对单例的实现比较简单,有两种方式: 第一种 let shared = XXManager()// 声明在全局命名区(global namespace) Class XXManager { }你可能会疑惑,为什么没有dispatch_once,如何保证多线程下创建的唯一性?其实是这样的,Swift中全局变量是懒加载,在AppDelegate中被初始化,之后所有的调用都会使用该实例。而且全局变量的初始化是默认使用dispatch_once的,这保证了全局变量的构造器(initializer)只会被调用一次,保证了shard的原子性。 第二种 Class XXManager { static let shared = XXManager() private override init() { // do something } }Swift 2 开始增加了static关键字,用于限定变量的作用域。如果不使用static,那么每一个shared都会对应一个实例。而使用static之后,shared成为全局变量,就成了跟上面第一种方式原理一致。可以注意到,由于构造器使用了 private 关键字,所以也保证了单例的原子性。 初始化方法和析构函数 对于初始化方法OC先调用父类的初始化方法,然后初始自己的成员变量。Swift先初始化自己的成员变量,然后在调用父类的初始化方法。 OC代码: // 初始化方法 @interface MainView : UIView @property (nonatomic, strong) NSString *title; - (instancetype)initWithFrame:(CGRect)frame title:(NSString *)title NS_DESIGNATED_INITIALIZER; @end@implementation MainView - (instancetype)initWithFrame:(CGRect)frame title:(NSString *)title { if (self = [super initWithFrame:frame]) { self.title = title; } return self; } @end // 析构函数 - (void)dealloc { //dealloc }Swift代码: class MainViewSwift: UIView { let title: String init(frame: CGRect, title: String) { self.title = title super.init(frame: frame) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { //deinit } }函数调用 OC代码: // 实例函数(共有方法) - (void)configModelWith:(XXModel *)model {} // 实例函数(私有方法) - (void)calculateProgress {} // 类函数 + (void)configModelWith:(XXModel *)model {}Swift代码: // 实例函数(共有方法) func configModel(with model: XXModel) {} // 实例函数(私有方法) private func calculateProgress() {} // 类函数(不可以被子类重写) static func configModel(with model: XXModel) {} // 类函数(可以被子类重写) class func configModel(with model: XXModel) {} // 类函数(不可以被子类重写) class final func configModel(with model: XXModel) {}OC可以通过是否将方法声明在.h文件表明该方法是否为私有方法。Swift中没有了.h文件,对于方法的权限控制是通过权限关键词进行的,各关键词权限大小为: private < fileprivate < internal < public < open 其中internal为默认权限,可以在同一module下访问。 NSNotification(通知) OC代码: // add observer [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(method) name:@"NotificationName" object:nil]; // post [NSNotificationCenter.defaultCenter postNotificationName:@"NotificationName" object:nil];Swift代码: // add observer NotificationCenter.default.addObserver(self, selector: #selector(method), name: NSNotification.Name(rawValue: "NotificationName"), object: nil) // post NotificationCenter.default.post(name: NSNotification.Name(rawValue: "NotificationName"), object: self)可以注意到,Swift中通知中心NotificationCenter不带NS前缀,通知名由字符串变成了NSNotification.Name的结构体。 改成结构体的目的就是为了便于管理字符串,原本的字符串类型变成了指定的NSNotification.Name类型。上面的Swift代码可以修改为: extension NSNotification.Name { static let NotificationName = NSNotification.Name("NotificationName") } // add observer NotificationCenter.default.addObserver(self, selector: #selector(method), name: .NotificationName, object: nil) // post NotificationCenter.default.post(name: .NotificationName, object: self)protocol(协议/代理) OC代码: @protocol XXManagerDelegate <NSObject> - (void)downloadFileFailed:(NSError *)error; @optional - (void)downloadFileComplete; @end@interface XXManager: NSObject @property (nonatomic, weak) id<XXManagerDelegate> delegate; @endSwift中对protocol的使用拓宽了许多,不光是class对象,struct和enum也都可以实现协议。需要注意的是struct和enum为指引用类型,不能使用weak修饰。只有指定当前代理只支持类对象,才能使用weak。将上面的代码转成对应的Swift代码,就是: @objc protocol XXManagerDelegate { func downloadFailFailed(error: Error) @objc optional func downloadFileComplete() // 可选协议的实现 } class XXManager: NSObject { weak var delegate: XXManagerDelegate? }@objc是表明当前代码是针对NSObject对象,也就是class对象,就可以正常使用weak了。 如果不是针对NSObject对象的delegate,仅仅是普通的class对象可以这样设置代理: protocol XXManagerDelegate: class { func downloadFailFailed(error: Error) } class XXManager { weak var delegate: XXManagerDelegate? }值得注意的是,仅@objc标记的protocol可以使用@optional。 Swift和OC混编注意事项 函数名的变化 如果你在一个Swift类里定义了一个delegate方法: @objc protocol MarkButtonDelegate { func clickBtn(title: String) }如果你要在OC中实现这个协议,这时候方法名就变成了: - (void)clickBtnWithTitle:(NSString *)title { // code }这主要是因为Swift有指定参数标签,OC却没有,所以在由Swift方法名生成OC方法名时编译器会自动加一些修饰词,已使函数作为一个句子可以"通顺"。 在OC的头文件里调用Swift类 如果要在OC的头文件里引用Swift类,因为Swift没有头文件,而为了让在头文件能够识别该Swift类,需要通过@class的方法引入。 @class SwiftClass;@interface XXOCClass: NSObject @property (nonatomic, strong) SwiftClass *object; @end对OC类在Swift调用下重命名 因为Swift对不同的module都有命名空间,所以Swift类都不需要添加前缀。如果有一个带前缀的OC公共组件,在Swift环境下调用时不得不指定前缀是一件很不优雅的事情,所以苹果添加了一个宏命令NS_SWIFT_NAME,允许在OC类在Swift环境下的重命名: NS_SWIFT_NAME(LoginManager) @interface XXLoginManager: NSObject @end这样我们就将XXLoginManager在Swift环境下的类名改为了LoginManager。 引用类型和值类型 1、 struct 和 enum 是值类型,类 class 是引用类型。 2、String,Array和 Dictionary都是结构体,因此赋值直接是拷贝,而NSString, NSArray 和NSDictionary则是类,所以是使用引用的方式。 3、struct 比 class 更“轻量级”,struct 分配在栈中,class 分配在堆中。 id类型和AnyObject OC中id类型被Swift调用时会自动转成AnyObject,他们很相似,但却其实概念并不一致。 AnyObject可以代表任何class类型的实例; 其他语法区别及注意事项(待补充) 1、Swift语句中不需要加分号;。 2、关于Bool类型更加严格,Swift不再是OC中的非0就是真,真假只对应true和false。 3、Swift类内一般不需要写self,但是闭包内是需要写的。 4、Swift是强类型语言,必须要指定明确的类型。在Swift中Int和Float是不能直接做运算的,必须要将他们转成同一类型才可以运算。 5、Swift抛弃了传统的++,--运算,抛弃了传统的C语言式的for循环写法,而改为for-in。 6、Swift的switch操作,不需要在每个case语句结束的时候都添加break。 7、Swift对enum的使用做了很大的扩展,可以支持任意类型,而OC枚举仅支持Int类型,如果要写兼容代码,要选择Int型枚举。 8、Swift代码要想被OC调用,需要在属性和方法名前面加上@objc。 9、Swift独有的特性,如泛型,struct,非Int型的enum等被包含才函数参数中,即使添加@objc也不会被编译器通过。 10、Swift支持重载,OC不支持。 11、带默认值的Swift函数再被OC调用时会自动展开。 语法检查 对于OC转Swift之后的语法变化还有很多细节值得注意,特别是对于初次使用Swift这门语言的同学,很容易遗漏或者待着OC的思想去写代码。这里推荐一个语法检查的框架SwiftLint,可以自动化的检查我们的代码是否符合Swift规范。 可以通过cocoapods进行引入,配置好之后,每次Build的过程,Lint脚本都会执行一遍Swift代码的语法检查操作,Lint还会将代码规范进行分级,严重的代码错误会直接报错,导致程序无法启动,不太严重的会显示代码警告(⚠️)。 如果你感觉SwiftLint有点过于严格了,还可以通过修改.swiftlint.yml文件,自定义属于自己的语法规范。