iOS摸鱼周报 第二十六期

iOS摸鱼周报 第二十六期

本期概要

  • 话题:跟熊大聊一下独立开发和音视频开发。
  • Tips:对节流和防抖的再讨论;关于 TestFlight 外部测试的一些介绍。
  • 面试模块:本期解析 KVO 的实现原理。
  • 优秀博客:收录了一些 RxSwift 相关的文章。
  • 学习资料:靛青早期写的 RxSwift 学习教程。
  • 开发工具:公众号文章同步工具 Wechatsync。

本期话题

@zhangferry:本期交流对象是摸鱼周报的另一位编辑:🐻我是熊大。他在音视频方向有很多经验,自己也独立维护了两款应用,我们围绕这两个层面来和他交流一下。

zhangferry:简单介绍下自己吧。

大家好,我是熊大,野生iOS开发者,目前正在做一款海外社交 App,在团队中主要负责 iOS 的架构设计以及性能优化。

zhangferry:你有多款独立开发的应用,能简单介绍下当时独立开发的初衷和现状吗?

独立开发的产品是《今日计划》《imi我的小家》

当时做独立开发的目的有两个:一个是自己有些想法想要实现出来,二是希望能有睡后收入。之前认为独立开发可能需要更多时间投入,后来发现独立开发最首要的问题不是时间,而是运营和验证;如何找到产品定位人群,如何优化 ASO,关键词如何填写,产品留存达到多少才是及格?这些都是初次尝试独立开发容易忽略却不得不面对的挑战。也正因此,我做了个公众号独立开发者基地,分享独立开发遇到的问题。

zhangferry:在你看来要做独立开发需要具备哪些重要技能呢?

以下 5 个方面可能是我们需要的:

1、具备产品思维,能编写需求文档,分析产品数据,摸索产品方向。

2、具备使用 Sketch、Figma、蓝湖的能力。

3、运营推广能力,如何让app能被更多的人知道,如何召回用户

4、具备iOS、Android、小程序等一个或多个的开发能力

5、后端开发能力,其实这个前期可以使用第三方代替

这些都是硬件实例,最关键的还是执行力和创造力,如果你有想法,那就不要等待。

4、你的工作中多涉及音视频技能,能说下音视频开发和普通应用开发有什么区别吗?如果想往这个方面学习,需要注意和关注哪些东西。

在我的工作中,音视频开发主要涉及到AVFoundation、FFmpeg、OpenGL ES、MetalKit等框架。

音视频开发入门会更难一些,需要有图形图像学的基础知识;有时需要编写 C\C++ 代码,很多第三方的音视频库都是 C\C++ 写的,比如常用的 libjpeg-turbo、lame;同时要熟悉CMake工具进行编译等。

推荐学习路线:

1、数字图像的基本知识

2、开源库 GPUImage,AVFoundation + OpenGL ES,2016年时,很多第三方SDK图像处理框架都是基于这个开发的。

3、开源库 GPUImage3,这是AVFoundation + Metal。

4、李明杰老师今年有一个FFmpeg的课程。

我在小专栏开了一个介绍音视频技术的专栏:GPUImage By Metal,大家如果对这类知识感兴趣的话欢迎订阅。另外再送大家一些免费领取资格,名额有限只有十个,点击这里领取。(公众号外链无法跳转,专栏领取链接在下方参考资料中)

5、如何保持学习的热情,能否分享一些你的学习方法。

保持热情的最好办法就是热爱或追求。

1、学习要循序渐进,不要一下学太多,陌生的东西太多会打消积极性。

2、如果遇到几天都无法理解的东西,放一放,发酵几个月后再看。

3、要有目标和实践方案

6、有什么需要借助于摸鱼周报宣传的。

1、希望大家多多关注 SpeedySwift 这个开源项目:https://github.com/Tliens/SpeedySwift ,这是一个用于提效 Swift 开发的仓库,觉得 OK 的话给我个 star 鼓励一下吧。

2、北京连趣科技,寻找一起并肩作战的小伙伴,各个岗位都有需求,简历可以投递到 tliens.jp@gmail.com,我的微信:bear0000001

开发Tips

整理编辑:夏天zhangferry

节流、防抖再讨论

在之前的分享中,我们介绍了函数节流(Throttle)和防抖(Debounce)解析及其OC实现。部分读者会去纠结节流和防抖的概念以至于执拗于其中的区别,想在节流和防抖之间找到一个交接点,通过这个交接点进行区分,其究竟是节流(Throttle)还是防抖(Debounce)。

容易让人理解混乱的还有节流和防抖中的 LeadingTrailing 模式,这里我们试图通过更直白的语言来解释这两个概念的区别。

概念解释

以下是正常模式,当我们移动发生位移修改时,执行函数回调。

这个执行明显太频繁了,所以需要有一种方法来减少执行次数,节流和防抖就是这样的方法。

节流:在一定周期内,比如 200ms , 200ms 只会执行一次函数回调。

防抖:在一定周期内,比如 200ms,任意两个相邻事件间隔超过 200ms,才会执行一次函数调用。

注意上面两个方法都是把原本密集的行为进行了分段处理,但分段就分头和尾。比如每 200ms 触发一次,是第 0ms 还是第 200ms?相邻间隔超过 200ms,第一个事件算不算有效事件呢?这就引来了 Leading 和 Trailing,节流和防抖都有 Leading 和 Trailing 两种模式。

Leading:在时间段的开始触发。

Trailing:在时间段的结尾触发。

备注:leading 和 trailing 是更精确的概念区分,有些框架里并没有显性声明,只是固定为一个较通用的模式。比如 RxSwift, throttle 只有 leading 模式,debounce 只有 trailing 模式。

典型应用场景

通过对比文本输入校验和提供一个模糊查询功能来加深节流和防抖的理解。

在校验输入文本是否符合某种校验规则,我们可以在用户停止输入的 200ms 后进行校验,期间无论用户如果输入 是增加还是删减都不影响,这就是防抖。

而模糊查询则,用户在输入过程中我们每隔 200ms 进行一次模糊匹配避免用户输入过程中查询列表为空,这就是 节流。

拓展

如果你项目中有存在这样的高频调用,可以尝试使用该理念进行优化。

这些文章:彻底弄懂函数防抖和函数节流函数防抖与函数节流Objective-C Message Throttle and Debounce 都会对你理解有所帮助。

关于 TestFlight 外部测试

TestFlight 分为内部和外部测试两种。内部测试需要通过邮件邀请制,对方同意邀请才可以参与到内部测试流程,最多可邀请 100 人。每次上传应用到 AppStore Connect,内部测试人员就会自动收到测试邮件的通知。

外部测试可以通过邮件邀请也可以通过公开链接的形式直接参与测试,链接生成之后就固定不变了,其总是指向当前最新版本。外部测试最多可邀请 10000 人。

与内测不同的是,外测每个版本的首次提交都需要经过苹果的审核。比如应用新版本为 1.0.0,首次提交对应的 build 号为 100,这个 100 的版本无法直接发布到外部测试,需要等待 TestFlight 团队的审核通过。注意这个审核不同于上线审核,AppStore 和 TestFlight 也是两个不同的团队。外测审核条件较宽泛,一般24小时之内会通过。通过之后点击公开连接或者邮件通知就可以下载 100 版本包。后面同属 1.0.0 的其他 build 号版本,无需审核,但需要每次手动发布。(Apple 帮助文档里有提,后续版本还会有基本审核,但遇到的场景都是可以直接发布的。)

采用公开链接的形式是无法看到测试者的信息的,只能查看对应版本的安装次数和崩溃测试。

面试解析

整理编辑:师大小海腾

本期解析 KVO 的实现原理。

Apple 使用了 isa-swizzling 方案来实现 KVO。

注册:

当我们调用 addObserver:forKeyPath:options:context: 方法,为 被观察对象 a 添加 KVO 监听时,系统会在运行时动态创建 a 对象所属类 A 的子类 NSKVONotifying_A,(如果是在Swift工程中,因为命名空间的存在,生成的类名会是NSKVONotifying_ModuleName.A) 并且让 a 对象的 isa 指向这个子类,同时重写父类 A 的 被观察属性 的 setter 方法来达到可以通知所有 观察者对象 的目的。

这个子类的 isa 指针指向它自己的 meta-class 对象,而不是原类的 meta-class 对象。

重写的 setter 方法的 SEL 对应的 IMP 为 Foundation 中的 _NSSetXXXValueAndNotify 函数(XXX 为 Key 的数据类型)。因此,当 被观察对象 的属性发生改变时,会调用 _NSSetXXXValueAndNotify 函数,这个函数中会调用:

  • willChangeValueForKey: 方法
  • 父类 A 的 setter 方法
  • didChangeValueForKey: 方法

监听:

而 willChangeValueForKey: 和 didChangeValueForKey: 方法内部会触发 观察者对象 的监听方法:observeValueForKeyPath:ofObject:change:context:,以此完成 KVO 的监听。

willChangeValueForKey: 和 didChangeValueForKey: 触发监听方法的时机:

  • didChangeValueForKey: 方法会直接触发监听方法
  • NSKeyValueObservingOptionPrior 是分别在值改变前后触发监听方法,即一次修改有两次触发。而这两次触发分别在 willChangeValueForKey: 和 didChangeValueForKey: 的时候进行的。如果注册方法中 options 传入 NSKeyValueObservingOptionPrior,那么可以通过只调用 willChangeValueForKey: 来触发改变前的那次 KVO,可以用于在属性值即将更改前做一些操作。

移除:

在移除 KVO 监听后,被观察对象的 isa 会指回原类 A,但是 NSKVONotifying_A 类并没有销毁,还保存在内存中,不销毁的原因想必大家也很容易理解,其实就是一层缓存,避免动态类的频繁创建/销毁。

重写方法:

NSKVONotifying_A 除了重写 setter 方法外,还重写了 class、dealloc、_isKVOA 这三个方法(可以通过 class_copyMethodList 获得),其中:

  • class:返回父类的 class 对象,目的是为了不让外界知道 KVO 动态生成类的存在,隐藏 KVO 实现(通过此处我们可以知道获取对象所属类的方式最好是使用class方法,而不是isa指针);
  • dealloc:释放 KVO 使用过程中产生的东西;
  • _isKVOA:用来标志它是一个 KVO 的类。

参考:iOS - 关于 KVO 的一些总结

优秀博客

RxSwift

1、RxSwift 中文文档 – 来自RxSwift 中文文档

@我是熊大:其实 RxSwift 的中文文档完善度很高,其目的就是帮助 iOS 开发人员快速上手 RxSwift,其中不仅讲了核心成员使用,还附带了精选的 demo 以及生态架构的相关文章。

2、RxSwift 核心实现原理 – 来自博客:楚权的世界

@我是熊大:泛型和闭包,让 RxSwift 诞生,这篇文章带你还原 RxSwift 的设计现场,深入浅出,帮助你更深入的了解RxSwift 的原理。

3、初识RxSwift及使用教程 – 来自:韩俊强的博客

@皮拉夫大王:RxSwift 是 Swift 函数响应式编程的一个开源库。初次接触的同学可能会提问为什么要用 RxSwift。因此可以看看这篇文章。作为初学者,通过阅读这篇文章感觉 RxSwift 使逻辑离散的代码变的聚合,逻辑更加清晰。当然,RxSwift 不止于此,纸上得来终觉浅,更多的优势可能只有深入使用才会有所体会。

4、RxSwift使用教程大全 – 来自:韩俊强的博客

@皮拉夫大王:RxSwift 的教程大全,罗列了比较多的 RxSwift 使用方法。

5、使用 RxSwift 进行响应式编程 – 来自:AltConf

@zhangferry:这是 AltConf 2016 中的一期讲座内容,整理翻译成了中文。虽然是2016年的内容,但RxSwift的基本概念是不会改变的,这篇内容 Scott Gardner 将带大家走入到响应式编程的世界当中,并告诉大家一个简单的方法来开始学习 RxSwift。

6、RxSwift vs PromiseKit – 来自:靛青DKQing

@zhangferry:如果仅是为了处理回调地狱就选择引入 RxSwift,就有些大材小用了,处理回调地狱用 PromiseKit 就可以。RxSwift 里的回调处理只是附加功能,其真正的思想是响应式,PromiseKit 非响应式框架。响应式是一种面向数据流和变化传播的编程范式,不只是异步的网络请求,像是点击行为,文本框不同的输入都是数据流的一种形式,概念的理解在学习响应式编程中尤为重要。文中通过一个简单的例子,来说明 PromiseKit 不具备流的特性。

学习资料

整理编辑:zhangferry

RxSwift 学习教程

地址:http://t.swift.gg/t/rxswift-course

结合本期优秀博客的内容再推荐一个 RxSwift 学习教程。大家如果仔细看 AltConf 那篇译文的话会注意到里面的译者注:

国内最好的 RxSwift 教程推荐靛青DKQing所撰写的 RxSwift 教程系列,有兴趣的同学可以前往阅读。

由此可见靛青是当时公认的 RxSwift 代表人物,他是国内较早一批接触并深入理解 RxSwift 的人之一,对 RxSwift 在国内的推广起到了很大的帮助。

课程系列的顺序大致是这样:基本的使用 -> 基本的概念 -> 进阶的使用 -> 源码解读。

该课程写于2016年,至今有一段时间了,部分语法可能有变,但不影响我们对概念的理解,仍有一定的参考学习价值。

工具推荐

整理编辑:zhangferry

Wechatsync

地址https://www.wechatsync.com/

软件状态:免费,开源

软件介绍

作为号主通常会将文章发布到多个平台,每个平台都重复地登录、复制、粘贴是一件很麻烦的事。Wechatsync就是这样一款解脱重复工作的神器。它是一款 Chrome 浏览器插件,支持多个平台的文章发布,这需要我们提前登录各个平台获得授权。它会自动识别公众号文章,弹出「同步该文章」按钮,然后点击就可以同步文章到我们授权的平台。

关于我们

iOS 摸鱼周报,主要分享开发过程中遇到的经验教训、优质的博客、高质量的学习资料、实用的开发工具等。周报仓库在这里:https://github.com/zhangferry/iOSWeeklyLearning ,如果你有好的的内容推荐可以通过 issue 的方式进行提交。另外也可以申请成为我们的常驻编辑,一起维护这份周报。另可关注公众号:iOS成长之路,后台点击进群交流,联系我们,获取更多内容。

往期推荐

iOS摸鱼周报 第二十五期

iOS摸鱼周报 第二十四期

iOS摸鱼周报 第二十三期

iOS摸鱼周报 第二十二期

iOS摸鱼周报 第二十五期

iOS摸鱼周报 第二十五期

本期概要

  • 话题:本期跟竹立交流一下关于求职和学习方法的话题。
  • Tips:如何清除启动图的缓存;如何优化 SwiftLint 流程。
  • 面试模块:本期解析一道 GCD 死锁题。
  • 优秀博客:整理了 Swift 泛型相关的几篇文章。
  • 学习资料:Adobe 的调色板网站:Adobe Color Wheel;知识小集的 Tips 汇总:Awesome-tips。
  • 开发工具:管理 Github 项目 Star 的工具:OhMyStar。

本期话题

@zhangferry:本期交流的小伙伴是摸鱼周报的另一位编辑:竹立。这个名字大家可能比较陌生,但是他的 ID 应该有很多人都听过:皮拉夫大王。竹立是一位非常资深的 iOS 开发,本期围绕职场和学习主题,跟他进行了一些交流。大家如果还有其他问题想问竹立的,可以留言区告诉我们。

zhangferry:简单介绍下自己吧。

大家好,我是来自 58 同城的邓竹立,目前在团队中主要负责 iOS 的性能优化及稳定性建设。

zhangferry:你对二进制的研究很深入,还在社区做过一次玩转二进制的直播分享,为什么会对这一领域感兴趣?能讲一些想要学好这方面内容的几点建议吗?

我对二进制的研究还远远算不上深入,可能在个别细分的领域有一点探索,但是从整体来说二进制涉及的知识太庞大,因此还算不上深入。对二进制的探索主要是之前的技术项目所引导的。当时在做技术项目时遇到了一个问题:“如何动态调用项目中的 static C 函数?”,当时感觉研究的方向应该为 mach-o 文件,最终随着调研的慢慢深入,也就对二进制文件有了一定的了解。

想要学习二进制相关的内容其实我没有特别好的建议,因为我并没有成体系的去学习这方面内容,更多的是利用空闲时间凭借个人的兴趣去探索。有兴趣才会在探索过程中感受到有所收获,在其他技术方向上也是这样。如果大家对二进制文件解析感兴趣,58 同城近期会有一次线上技术沙龙(具体时间还未定,估计得 10 月后了),主要介绍 58 如何打造集团内 Swift 混编生态的。我会借助沙龙的机会介绍下 Swift 的二进制解析。

zhangferry:作为一位资深开发,能讲下你认为的高级开发和资深开发之间的区别吗?在你看来要达到资深开发需要具备哪些素质?

可能各家公司对高级和资深的定义不太一致。我理解资深开发相对于高级开发更具备触类旁通的能力,或者说是能根据以往的经验提取方法论应用到其他领域。因此从高级到资深可能需要在多个方向上有较好的理解。资深开发更应该从公司和产品的角度考虑问题,多问几个为什么,多关注事后的结果,而不是产品安排什么就做什么。另外,在沟通能力上的对资深开发的要求也会更高一些。

zhangferry:你具有多年面试官的经历,能简单总结下你认为的什么才是好的候选人以及他们需要具备哪些素质吗?

由于团队职责可能不同,因此每个团队招人的标准存在一些差异。比如有些团队可能对 RN/Flutter 等技术的应用比较看重,有些团队对底层技术比较看重,但是这些差异都是表象的差异。优秀的团队更注重候选人更深层的东西,这些东西可能在短期内无法突击弥补的,比如对技术探索的欲望、思维的灵活性、学习能力、抗压能力、责任心等等。就我所在的团队而言,在技术上,我们更关心的是候选人是否已经找到技术成长的第二曲线。(第二曲线摘自于《第二曲线:跨越“S 型曲线”的二次增长》,书我没看过,图与我脑海中的模型很像,所以在此引用)

或者通过面试能让我们看到即使候选人目前还处于成长期,但是经过培养是可以有更长远成长的。很多团队招人不外乎就这两种:要么候选人现在很厉害,要么候选人将来很厉害。

一些场景比较复杂的大型 APP,有些问题比较复杂,不是特别好定位,这需要一定的技术基础,灵活的思维,甚至要求心理素质过硬。大家可以看下我的2019年终总结中提到的 <工作篇>,我整理了日常工作中遇到的部分问题记录到总结中,这也是 58 T5级别工程师的日常工作内容和要求。

如果作为团队的老板,你肯定希望自己的属下能够具备打硬仗的实力,而不仅仅是写写需求做做任务,在关键的时刻能够攻坚克难才是团队价值的体现。以上几点要求如果只是看看面经,刷刷算法可能还不够。因此在日常工作中,我们就应该养成良好的习惯,多问几个为什么,多做探索调研和储备,不要放过一些细节。

回到正题,我们从简历和面试的实际情况来看下有哪些是被鼓励的:

(1)项目经历(这里不是指写了哪些 APP)比较匹配。比如团队目前的重点在与包大小治理,如果你的简历中有相关实践并且做的比较深入,那这就是加分项。

(2)令人耳目一新的技术。有些技术比较前沿或者并未广泛被大家所熟知,在这个领域候选人有较深入的研究。(这表明候选人已经找到了第二曲线)

(3)能证明自己探索和专研能力的经历。简历中有具体的事件能体现出候选人的这方面优势。(具备找到第二曲线的潜力)

(4)灵活的思维能力。算法或者临场方案设计比较完善,考虑的比较完备。

(5)良好的抗压能力。如果在高压的面试情况下,不烦躁不放弃,依旧能保持冷静思考。

(6)能体现出良好的学习习惯。高质量的博客文章、开源代码等都是加分项。临时凑数的可能起不到作用,我一般会留意内容质量和发布时间密度。

(7)业界视野。能关注业界的一些动态,对业界的一些热点技术比较熟悉。

(8)坦诚而良好的沟通。

(9)有足够的入职意愿。

(10)最后就是稳定性、工作背景、学历等条件。

zhangferry:如何保持学习热情,给我们分享一些你的学习方法吧。

学习主要还是需要制定大的方向,然后在具体实施时会对自己做一些鼓励。这些鼓励的行为包括:写文章、在团队内做技术分享、对外交流等等。在团队内我的文章数和分享数常年领先,因为写文章和分享会促使我重新审视和思考,对成长有极大的好处。对外交流获得的成就感会更大一些,但是需要更谨慎,尽量保证自己输出的内容是准确的,一旦内容有明显纰漏,丢自己脸事小。。。

zhangferry:个人有什么想法,可以借助于摸鱼周报进行宣传的。

(1)希望大家多多关注 WBBlades 开源项目:https://github.com/wuba/WBBlades ,觉得 OK 的话给我个 star 鼓励一下。

(2)58 主 APP、人人车、到家精选等团队正在招人,简历可以投递到 zhulideng@yeah.net。秋天到了,我想赚点内推费填几件衣服。

开发Tips

如何清除 iOS APP 的启动屏幕缓存

整理编辑:FBY展菲

每当我在我的 iOS 应用程序中修改了 LaunchScreen.storyboad 中的某些内容时,我都会遇到一个问题:

系统会缓存启动图像,即使删除了该应用程序,它实际上也很难清除原来的缓存。

有时我修改了 LaunchScreen.storyboad,删除应用程序并重新启动,它显示了新的 LaunchScreen.storyboad,但 LaunchScreen.storyboad 中引用的任何图片都不会显示,从而使启动屏显得不正常。

今天,我在应用程序的沙盒中进行了一些挖掘,发现该 Library 文件夹中有一个名为 SplashBoard 的文件夹,该文件夹是启动屏缓存的存储位置。

因此,要完全清除应用程序的启动屏幕缓存,您所需要做的就是在应用程序内部运行以下代码(已将该代码扩展到 UIApplication 的中):

1
2
3
4
5
6
7
8
9
10
11
12
13
import UIKit

public extension UIApplication {

func clearLaunchScreenCache() {
do {
try FileManager.default.removeItem(atPath: NSHomeDirectory()+"/Library/SplashBoard")
} catch {
print("Failed to delete launch screen cache: \(error)")
}
}

}

在启动屏开发过程中,您可以将其放在应用程序初始化代码中,然后在不修改启动屏时将其禁用。

这个技巧在启动屏出问题时为我节省了很多时间,希望也能为您节省一些时间。

使用介绍

1
UIApplication.shared.clearLaunchScreenCache()
  • 文章提到的缓存目录在沙盒下如下图所示:

  • OC 代码,创建一个 UIApplicationCategory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import <UIKit/UIKit.h>

@interface UIApplication (LaunchScreen)
- (void)clearLaunchScreenCache;
@end

#import "UIApplication+LaunchScreen.h"

@implementation UIApplication (LaunchScreen)
- (void)clearLaunchScreenCache {
NSError *error;
[NSFileManager.defaultManager removeItemAtPath:[NSString stringWithFormat:@"%@/Library/SplashBoard", NSHomeDirectory()] error:&error];
if (error) {
NSLog(@"Failed to delete launch screen cache: %@",error);
}
}
@end

OC 使用方法

1
2
3
#import "UIApplication+LaunchScreen.h"

[UIApplication.sharedApplication clearLaunchScreenCache];

参考:如何清除 iOS APP 的启动屏幕缓存

优化 SwiftLint 执行

整理编辑:zhangferry

很多 Swift 项目里都集成了 SwiftLint 用于代码检查。SwiftLint 的执行通常在编译的早期且全量检查的,目前我们项目的每次 lint 时间在 12s 左右。但细想一下,并没有改变的代码多次被 lint 是一种浪费。顺着这个思路在官方的 issues 里找到了可以过滤非修改文件的参考方法,其是通过 git diff 查找到变更的代码,仅对变更代码进行 lint 处理。使用该方案后,每次 lint 时长基本保持在 2s 以内。

下面附上该脚本,需要注意的是 SWIFT_LINT 要根据自己的集成方式进行替换,这里是 CocoaPods 的集成方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Run SwiftLint
START_DATE=$(date +"%s")

SWIFT_LINT="${PODS_ROOT}/SwiftLint/swiftlint"

# Run SwiftLint for given filename
run_swiftlint() {
local filename="${1}"
if [[ "${filename##*.}" == "swift" ]]; then
# ${SWIFT_LINT} autocorrect --path "${filename}"
${SWIFT_LINT} lint --path "${filename}"
fi
}

if [[ -e "${SWIFT_LINT}" ]]; then
echo "SwiftLint version: $(${SWIFT_LINT} version)"
# Run for both staged and unstaged files
git diff --name-only | while read filename; do run_swiftlint "${filename}"; done
git diff --cached --name-only | while read filename; do run_swiftlint "${filename}"; done
else
echo "${SWIFT_LINT} is not installed."
exit 0
fi

END_DATE=$(date +"%s")

DIFF=$(($END_DATE - $START_DATE))
echo "SwiftLint took $(($DIFF / 60)) minutes and $(($DIFF % 60)) seconds to complete."

面试解析

整理编辑:师大小海腾

本期解析一道 GCD 死锁题。

分别在 mainThread 执行 test1 和 test2 函数,问执行情况如何?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func test1() {
DispatchQueue.main.sync { // task1
print("1")
}
}

func test2() {
print("1")
let queue = DispatchQueue.init(label: "thread")
queue.async { // task1
print("2")
DispatchQueue.main.sync { // task3
print("3")
queue.sync { // task4
print("4")
}
}
print("5")
}
print("6")
queue.async { // task2
print("7")
}
print("8")
}
  1. 死锁。
    1. mainThread 当前正在执行 test1 函数。
    2. 这时候使用 sync 函数往 mainQueue 中提交 task1 以同步执行,需要 task1 执行完毕后才会返回。
    3. 由于队列 FIFO,要想从 mainQueue 取出 task1 放到 mainThread 执行,需要先等待上一个 task 也就是 test1 函数先执行完,而 test1 此时又被 sync 阻塞,需要 sync 函数先返回。因此 test1 与 task1 循环等待,产生死锁。
  2. 打印 1、6、8、2、3,然后死锁。
    1. 创建一个 serialQueue。使用 async 函数往指定队列提交 task 以异步执行会直接返回,不会阻塞,因此打印 1、6、8,并且 mainThread 执行完 test2。
    2. 从 serialQueue 中取出 task1 放到一条 childThread 执行,因为是 serialQueue 所以 task2 需要等待 task1 执行完毕才会执行。 执行 task1,打印 2;
    3. 使用 sync 函数往 mainQueue 提交 task3。此时 task1 被阻塞,需要等待 task3 执行完毕,才会接下去打印 5;
    4. mainThread 当前没有在执行 task,因此执行 task3,打印 3;
    5. 接着,使用 sync 往 serialQueue 中提交 task4,此时 task3 被阻塞,需要等待 task4 执行完毕
    6. 此时该 childThread 正在执行 task1,因此 task4 需要等待 task1 先执行完毕
    7. 此时,task1 在等待 task3,task3 在等待 task4,task4 在等待 task1。循环等待,产生死锁。

使用 GCD 的时候,我们一定要注意死锁问题,不要使用 sync 函数当前 serialQueue 中添加 task,否则会卡住当前 serialQueue,产生死锁。

优秀博客

Swift 泛型

1、Swift 进阶: 泛型 – 来自公众号:Swift社区

@FBY展菲:泛型是 Swift 最强大的特性之一,很多 Swift 标准库是基于泛型代码构建的。你可以创建一个容纳 Int 值的数组,或者容纳 String 值的数组,甚至容纳任何 Swift 可以创建的其他类型的数组。同样,你可以创建一个存储任何指定类型值的字典,而且类型没有限制。

2、Swift 泛型解析 – 来自掘金:我是熊大

@我是熊大:本文通俗易懂的解析了什么是泛型,泛型的应用场景,泛型和其他 Swift 特性摩擦出的火花,最后对泛型进行了总结。

3、WWDC2018 - Swift 泛型 Swift Generics – 来自掘金:西野圭吾

@我是熊大:本文回顾了 Swift 中对于泛型支持的历史变更,可以了解下泛型不同版本的历史以及特性。

4、Swift 性能优化(2)——协议与泛型的实现 – 来自博客:楚权的世界

@我是熊大:本文探讨了协议和泛型的底层实现原理,文中涉及到编译器对于 Swift 的性能优化,十分推荐阅读。

5、Swift 泛型底层实现原理 – 来自博客:楚权的世界

@皮拉夫大王在此:本文介绍了 Swift 相关的底层原理,干货较多。例如 VWT 的作用、什么是 Type Metadata、Metadata Pattern 等等。如果有些概念不是很清楚的话可以阅读文章下面的参考文献。

6、Generics Manifesto – 来自 Github:Apple/Swift

@zhangferry四娘对这篇官方说明进行了翻译,也可以直接阅读翻译版:【译】Swift 泛型宣言。这份文档介绍了如何建立一个完善的泛型系统,以及 Swift 语言在发展过程中,不断补充的那些泛型功能。

学习资料

整理编辑:Mimosa

Adobe Color Wheel

地址:https://color.adobe.com/zh/create/color-wheel

来自 Adobe 的调色盘网站,可以选择不同的色彩规则,例如:类比、分割辅色、三元色等等方案来生成配色,也可以通过导入照片来提取颜色,并且可以通过辅助工具例如对比度检查器来确认文字和图案在底色上的预览情况。另外,你也可以通过 Adobe 的「探索」和「趋势」社区来学习如何搭配颜色,或者是寻找配色灵感。

Awesome-tips

地址:https://awesome-tips.gitbook.io/ios/

来自知识小集整理的 iOS 开发 tips,已经整合成了 gitbook。虽然时间稍稍有点久了,但其中的文章都比较优质,在遇到的问题的时候可以翻阅一下,讲不定会有新的收获。

工具推荐

整理编辑:CoderStar

OhMyStar

地址https://ohmystarapp.com/

软件状态:普通版免费,Pro 版收费

软件介绍

直接引用官方自己介绍的话吧:

The best way to organise your GitHub Stars. Beautiful and efficient way to manage, explore and share your Github Stars.

其中 Pro 版增加的功能是设备间同步,不过软件本身也支持数据的导入导出,大家根据自己的情况进行选择。

关于我们

iOS 摸鱼周报,主要分享开发过程中遇到的经验教训、优质的博客、高质量的学习资料、实用的开发工具等。周报仓库在这里:https://github.com/zhangferry/iOSWeeklyLearning ,如果你有好的的内容推荐可以通过 issue 的方式进行提交。另外也可以申请成为我们的常驻编辑,一起维护这份周报。另可关注公众号:iOS成长之路,后台点击进群交流,联系我们,获取更多内容。

往期推荐

iOS摸鱼周报 第二十四期

iOS摸鱼周报 第二十三期

iOS摸鱼周报 第二十二期

iOS摸鱼周报 第二十一期

iOS摸鱼周报 第二十四期

iOS摸鱼周报 第二十四期

本期概要

  • 话题:跟一位同学聊一下最近的面试感受。
  • Tips:设计 OC 版本的 defer 功能,使用现有证书创建 Fastlane match 格式加密文件。
  • 面试模块:离屏渲染相关知识点。
  • 优秀博客:整理了Swift 闭包相关的文章。
  • 学习资料:介绍两个仓库,一个是大家容易读错的开发词汇汇总,一个是微软出品的物联网课程。
  • 开发工具:一款免费开源的跨平台密码管理工具:KeeWeb。

本期话题

@zhangferry:本期访谈对象是 @七里香蛋炒饭,他也是交流群里的小伙伴。了解到他最近刚换工作,从某小公司入职某一线大厂,就邀请他来聊一聊面试的一些感想。

zhangferry:你面试准备了多久,大概的面试经历是怎样的?

整个面试过程大概有一个半月时间,前期是断断续续在看一些东西,后面有 3 周左右时间是重点准备。接到面邀的比较多,有些不感兴趣的就没去,实际参与面试的有 10 家,也都是一二线互联网公司。这侧面也说明了 iOS 没人要仅仅是个调侃而已,目前对 iOS 开发的需求还是不少的。

zhangferry:结合这些面试经历,有哪些高频题?遇到的算法考察多吗?

高频题的话内存管理和多线程肯定算是了,基本上每家面试都会问的。

另一个就是项目经历,也是必问的。这个一般会结合简历来问,特别是项目重点和难点,所以大家准备简历的时候一定要保证对所写的内容是很清楚的。对于非常喜欢的公司还可以根据他们业务需求有针对性的优化下简历。

另外,架构设计能力,封装能力,有时也会考察,这个短时间无法快速提升,需要平常工作过程有意培养一下。

算法的考察整体来看不算多,大概有 30% 的概率吧。那些考算法的也都是考察比较简单的题目,也可能跟我面试的岗位有关,这个仅供参考,面试之前,算法方面多少还是要准备的。

zhangferry:现在经常有人说面试八股文,结合面试经历,你怎么看待八股文这个事?

首先存在即合理吧,八股文的现象体现的是面试官自身准备的不足。可能来源于早期,大家技术水平都一般,没有太多可问的东西,也没有特意研究过哪些方面,所以就网上扒一扒拿来问。目前的经历来看到还好,被问的问题还算多元,可能这种现象之后会随着面试官水平的提升越来越少。

同时这也算是一种双向选择,如果某次面试全是那种眼熟的问题,毫无新意,大概率可以说明这家公司对技术的重视和钻研程度不高,可以降低其优先级。

zhangferry:对待参加面试的小伙伴有没有什么建议?

投递简历没有回复或者面试感觉还可以最后却没过,出现这些现象是有多种原因,比如岗位正好招满了、岗位需求有变等等,不要首先否定自己。面试过程一定要放平心态,不要有心理压力。

最后祝所有准备找和正在找工作的小伙伴都能拿到满意的 Offer。

开发Tips

在 Objective-C 中实现 Swift 中的 defer 功能

整理编辑:RunsCodezhangferry

期望效果是下面这样,函数执行完出栈之前,要执行 defer 内定义的内容。

1
2
3
4
5
- (void)hello:(NSString *)str {
defer {
// do something
}
}
准备工作

实现 defer 的前提是需要有指令能够让函数在作用域出栈的时候触发 defer 里的闭包内容,这里需要用到两个东西:

__attribute__ :一个用于在声明时指定一些特性的编译器指令,它可以让我们进行更多的错误检查和高级优化工作。

想了解更多,参考: https://nshipster.cn/__attribute__/

cleanup(...):接受一个函数指针,在作用域结束的时候触发该函数指针。

简单实践

到这一步,我们已经了解了大概功能了,那我们实战一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdlib.h>
#include <stdio.h>

void free_buffer(char **buffer) { printf("3. free buffer\n"); }
void delete_file(int *value) { printf("2. delete file\n"); }
void close_file(FILE **fp) { printf("1. close file \n"); }

int main(int argc, char **argv) {
// 执行顺序与压栈顺序相反
char *buffer __attribute__ ((__cleanup__(free_buffer))) = malloc(20);
int res __attribute__ ((__cleanup__(delete_file)));
FILE *fp __attribute__ ((__cleanup__(close_file)));
printf("0. open file \n");
return 0;
}

输出结果:

1
2
3
4
5
0. open file 
1. close file
2. delete file
3. free buffer
[Finished in 683ms]

但是到这一步的话,我们使用不方便啊,何况我们还是 iOSer,这个不友好啊。那么继续改造成 Objective-C 独有版本。

实战优化

要做到上面那个理想方案,还需要什么呢?

  • 代码块,那就只能是 NSBlock
    1
    typedef void(^executeCleanupBlock)(void);
  • 宏函数 or 全局函数?想到 Objective-C 又没有尾随闭包这一说,那全局函数肯定不行,也就只能全局宏了
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #ifndef defer
    #define defer \
    __strong executeCleanupBlock blk __attribute__((cleanup(deferFunction), unused)) = ^
    #endif
    ...
    // .m 文件
    void deferFunction (__strong executeCleanupBlock *block) {
    (*block)();
    }
    OK 大功告成跑一下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    defer {
    NSLog(@"defer 1");
    };
    defer { // error: Redefinition of 'blk'
    NSLog(@"defer 2");
    };
    defer { // error: Redefinition of 'blk'
    NSLog(@"defer 3");
    };
    NSLog(@"beign defer");
    不好意思, 不行,报错 error: Redefinition of 'blk',为什么?(想一想)

上最终解决版本之前还得认识两个东西

  • __LINE__ :获取当前行号
  • ## :连接两个字符
    1
    2
    3
    4
    #define defer_concat_(A, B) A ## B
    #define defer_concat(A, B) defer_concat_(A, B)
    ...
    // 为什么要多一个下划线的宏, 这是因为每次只能展开一个宏, __LINE__ 的正确行号在第二层才能被解开

    最终方案

好了,差不多了, 是时候展示真功夫了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define defer_concat_(A, B) A ## B
#define defer_concat(A, B) defer_concat_(A, B)

typedef void(^executeCleanupBlock)(void);

#if defined(__cplusplus)
extern "C" {
#endif
void deferFunction (__strong executeCleanupBlock _Nonnull *_Nonnull block);
#if defined(__cplusplus)
}
#endif

#ifndef defer
#define defer \
__strong executeCleanupBlock defer_concat(blk, __LINE__) __attribute__((cleanup(deferFunction), unused)) = ^
#endif
// .m 文件
void deferFunction (__strong executeCleanupBlock *block) {
(*block)();
}

总共就这么多代码,实现 OC 版本的 defer

其实到了这里已经结束了, 但是还要讲一句:这里与 Justin Spahr-Summers 在 libextobj@onExit{})里的实现略有差异,当前实现更简单,libextobj 里的功能更丰富一些。

使用现有证书创建 Fastlane match 格式加密文件

简单说下 match 管理证书的工作流程,它将证书文件进行加密存放到 git 仓库,使用方 clone 这个仓库然后解密证书文件,再把证书安装到本机的 keychain 里。这样不同设备上就可以愉快的共享证书了。

match 创建证书有两种方式:

  • fastlane match nuke,对原证书 revoke 重新生成一份新的,这会导致原证书不可用,如果多 App 账号,不建议这样。
  • 通过已有证书导出为 match 格式加密文件,进行维护。

第二种方案不会影响原证书使用,比较推荐。但是看网上介绍这种方案的非常少,所以还是简单总结下:

1、导出文件

需要导出证书、p12 两个文件,将他们放到一个特定文件夹,假定他们的命名分别为:cert.cer、cert.p12。

2、使用 openssl 进行加密

需要一个预设密码,这个可以自定义,作为加密和解密的一个特定参数。

1
2
$ openssl enc -aes-256-cbc -k {password} -in "cert.cer" -out "cert.enc.cer" -a -e -salt
$ openssl enc -aes-256-cbc -k {password} -in "cert.p12" -out "cert.enc.p12" -a -e -salt

3、推送证书到 git 仓库

每个证书文件都有特定的 ID,推送之前我们还需要修改加密证书的文件名。该 ID 在开发者网站证书详情那一页的网址最后面展示。就是下面码糊住的那一块:

然后我们将那两个文件放到 git 仓库的 certs 目录对应的类型(development/distribution)下,然后进行推送。

4、使用

还记得我们上面设计的加密参数吗,在使用的时候也是需要用到的,我们将其放到 .env 这个文件中作为全局变量,它有一个特定的变量名 MATCH_PASSWROD。使用的时候用下面的语句就可以下载安装证书了:

1
2
$ fastlane match development
$ fastlane match adhoc

参考:https://docs.fastlane.tools/actions/match/

面试解析

整理编辑:FBY展菲

本期面试解析讲解的是离屏渲染的相关知识点。

为什么圆角和裁剪后 iOS 绘制会触发离屏渲染?

默认情况下每个视图都是完全独立绘制渲染的。
而当某个父视图设置了圆角和裁剪并且又有子视图时,父视图只会对自身进行裁剪绘制和渲染。

当子视图绘制时就要考虑被父视图裁剪部分的绘制渲染处理,因此需要反复递归回溯和拷贝父视图的渲染上下文和裁剪信息,再和子视图做合并处理,以便完成最终的裁剪效果。这样势必产生大量的时间和内存的开销。

解决的方法是当父视图被裁剪和有圆角并且有子视图时,就单独的开辟一块绘制上下文,把自身和所有子视图的内容都统一绘制在这个上下文中,这样子视图也不需要再单独绘制了,所有裁剪都会统一处理。当父视图绘制完成时再将开辟的缓冲上下文拷贝到屏幕上下文中去。这个过程就是离屏渲染!!

所以离屏渲染其实和我们先将内容绘制在位图内存上下文然后再统一拷贝到屏幕上下文中的双缓存技术是非常相似的。使用离屏渲染主要因为 iOS 内部的视图独立绘制技术所导致的一些缺陷而不得不才用的技术。

推荐阅读:关于iOS离屏渲染的深入研究

优秀博客

整理编辑:皮拉夫大王在此FBY展菲

本期主题:Swift 闭包

1、Swift 基于闭包的类型擦除 – 来自公众号:Swift社区

本文重点介绍在 Swift 中处理泛型时可能发生的一种情况,以及通常是如何使用基于闭包的类型擦除技术来解决这种情况。

2、swift 闭包(闭包表达式、尾随闭包、逃逸闭包、自动闭包) – 来自掘金:NewBoy

关于 Swift 闭包的初级文章,内容整合了几乎所有 Swift 闭包的概念和用法。比较适合 Swift 初学者或者是从 OC 转向 Swift 的同学。

3、Day6 - Swift 闭包详解 上 – 来自微信公众号: iOS成长指北

4、Day7 - Swift 闭包详解 下 – 来自微信公众号: iOS成长指北

Swift 闭包学习的两篇文章,也是包含了 Swift 的概念及用法,其中部分用法及概念更加细致。两篇文章是作者学习思考再输出的成果,因此在文章中有些作者的理解,这对我们学习是比较重要的,而且比较通俗易懂。

5、Closures – 来自:Swift Document

@zhangferry:对于概念的理解官方文档还是非常有必要看的。Swift 里的闭包跟 C/OC 中的 Block,其他语言中的 Lambda 含义是类似的。Swift 与 OC 混编时,闭包与 Block 是完全兼容的。但就含义来说两者仍有区别,Block 更多强调的是匿名代码块,闭包则是除这之外还有真正的一级对象的含义。

学习资料

整理编辑:Mimosa

中国程序员容易发音错误的单词

地址:https://github.com/shimohq/chinese-programmer-wrong-pronunciation

在担心和同事讨论代码的时候念的单词同事听不懂?开会 review 代码的时候突然遇到不会读的单词?如果你遇到过这些问题,那来看看这个 github 仓库吧。它是一个收录了在编程领域容易发音错误单词的仓库,目前已经有 14.4k stars 了,他标注出了易错的读音和正确的读音,且支持在线听读音。

IoT for Beginners

地址:https://github.com/microsoft/IoT-For-Beginners

这是来自微软 Azure 的物联网课程,是一个为期 12 周的 24 课时的课程,其中有所有关于物联网的基础知识,每节课都包括课前和课后测验、完成课程的书面说明、解决方案、作业等。其中每个项目都是适合学生或业余爱好者的、在真实世界可用的硬件,且每个项目都会提供相关的背景知识来研究具体的项目领域。

工具推荐

整理编辑:zhangferry

KeeWeb

地址https://keeweb.info/

软件状态:免费,开源

软件介绍

KeeWeb 是一个浏览器和桌面密码管理器,兼容 KeePass 数据库。它不需要任何服务器或额外的资源。该应用程序可以在浏览器中运行,也可以作为桌面应用程序运行。更重要的是它还可以利用 Dropbox、Google Drive 进行远程同步。

关于我们

iOS 摸鱼周报,主要分享开发过程中遇到的经验教训、优质的博客、高质量的学习资料、实用的开发工具等。周报仓库在这里:https://github.com/zhangferry/iOSWeeklyLearning ,如果你有好的的内容推荐可以通过 issue 的方式进行提交。另外也可以申请成为我们的常驻编辑,一起维护这份周报。另可关注公众号:iOS成长之路,后台点击进群交流,联系我们,获取更多内容。

往期推荐

iOS摸鱼周报 第二十三期

iOS摸鱼周报 第二十二期

iOS摸鱼周报 第二十一期

iOS摸鱼周报 第二十期

iOS摸鱼周报 第二十三期

iOS摸鱼周报 第二十三期

本期概要

  • 本期邀请 CoderStar 聊一下他的学习方法。
  • 简版 PromiseKit 的设计思路;如何通过配置文件区分 AdHoc/AppStore。
  • isMemberOfClassisKindOfClass 的含义与区别。
  • 博客部分整理了Swift 指针、Swift 属性包裹器的几篇文章。
  • 学习资料:
  • 一个帮助解析 Shell 脚本的网站:explainshell。

本期话题

@zhangferry:上周跟 @展菲 聊过之后,有了这个跟各位博主进行访谈的想法。博主+访谈,即可以帮大家介绍优秀的开发者,又能利用访谈的形式近距离了解博主,学习他们的思考和学习方法。本期所选问题是一个初步尝试,大家如果有更好的问题,欢迎留言告诉我们。

本期介绍的博主也是摸鱼周报的一位联合编辑:CoderStar

博主访谈

zhangferry:简单介绍下自己和自己的公众号吧

自己:CoderStar,坐标北京,目前主要工作与 iOS 相关,对大前端、后端都有一定涉猎,喜欢分享干货博文。

公众号:CoderStar,分享大前端相关的技术知识,只聊技术干货,目前分享的内容主要是 iOS 相关的,后续还会分享一些 Flutter、Vue 前端等相关技术知识。目前公众号文章内容均是自己原创,很欢迎大家投稿一些好文章,大家一块进步。

zhangferry:为什么有写公众号的打算?写公众号有带来什么好处吗?

最开始写公众号的原因其实比较简单,

1、因为过去积累了一些笔记,比较零散,想整理一下;

2、觉得工作经验已经到了一定的阶段,也是时候将知识梳理一遍,打造自己的知识体系了,融会贯通;

3、是想把自己积累的一些技术知识分享出来,大家一起来交流,创造一个好的技术圈子,一个好的技术圈子实在是太重要了。

写公众号的好处:

1、写文章不仅能让我对一个知识点理解的更透彻,也增强了我的写作能力,对于技术知识而言,自己理解是一个阶段,深入浅出的写出来又是一个更高的阶段;

2、可以认识很多小伙伴,同行的路上不会孤单,比如和飞哥就是这样认识的。

zhangferry:最近在研究什么有趣的东西?是否可以透露下未来几篇文章的规划?

最近在做优化方面的事情,未来几篇文章可能会偏向优化系列或者底层相关。

zhangferry:如何让自己每周都能抽出时间写博客呢?有没有什么好的学习方法可以分享?

我目前更新的频率是一周一篇文章,一般工作日晚上会去看一些本期文章涉及的资料以及做一些代码实践,然后积累一些笔记,在周末时候将笔记进行整理聚合,形成文章,其实这个过程中还是比较累的,毕竟有的时候工作会忙,但是这个事情一定要坚持,给自己一个目标,不能随随便便就断更,毕竟有第一次断更就有第二次。

学习方法:说一点吧,我自己对于技术的态度是实践型+更优解,当看到一些好的文章的时候,会自己将文章里面的原理或者实现自己动手实践一下,考虑这个方法有什么缺点,并围绕这个技术点去思考有没有更好的解决方案,不断地去寻找更优解。

开发Tips

整理编辑:RunsCodezhangferry

如何摊平复杂逻辑流程的设计

开发中我们通常会遇到以下问题:

  • 运营活动优先级问题
  • 后续慢慢在使用过程中逐渐衍生新的功能(延时,轮询,条件校验等)
  • 逐级递增的回调地狱

Talk is cheap, show code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 1
func function0() {
obj0.closure { _ in
// to do something
obj1.closure { _ in
// to do something
obj2.closure { _ in
...
objn.closure { _ in
...
}
}
}
}
}

or
// 2.
func function1 {
if 满足活动0条件 {
// to do something
} else if 满足活动1条件 {
// to do something
} else if 满足活动2条件 {
// to do something
}
...
else {
// to do something
}
}

分析上面那种代码我们可以得出以下几点结论:

  • 不管怎么看都是按流程或者条件设计的
  • 可读性还行,但可维护性较差,二次修改错误率较高
  • 无扩展性,只能不断增加代码的行数、条件分支以及更深层级的回调
  • 如果功能升级增加类似延迟、轮询,那完全不支持
  • 复用性可以说无

解决方案

  • 实现一个容器 Element 搭载所有外部实现逻辑
  • 容器 Element 以单向链表的方式链接,执行完就自动执行下一个
  • 容器内聚合一个抽象条件逻辑助手 Promise,可随意扩展增加行为,用来检查外部实现是否可以执行链表下一个 Element(可以形象理解为自来水管路的阀门,电路电气开关之类,当然会有更复杂的阀门与电气开关)
  • 自己管理自己的生命周期,无需外部强引用
  • 容器 Element 可以被继承实现,参考 NSOperation 设计

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private func head() -> PriorityElement<String, Int> {
return PriorityElement(id: "Head") { (promise: PriorityPromise<String, Int>) in
Println("head input : \(promise.input ?? "")")
self.delay(1) { promise.next(1) }
}.subscribe { i in
Println("head subscribe : \(i ?? -1)")
}.catch { err in
Println("head catch : \(String(describing: err))")
}.dispose {
Println("head dispose")
}
}
// This is a minimalist way to create element,
// using anonymous closure parameters and initializing default parameters
private func neck() -> PriorityElement<Int, String> {
return PriorityElement {
Println("neck input : \($0.input ?? -1)")
$0.output = "I am Neck"
$0.validated($0.input == 1)
}.subscribe { ... }.catch { err in ... }.dispose { ... }
}
// This is a recommended way to create element, providing an ID for debugging
private func lung() -> PriorityElement<String, String> {
return PriorityElement(id: "Lung") {
Println("lung input : \($0.input ?? "-1")")
self.count += 1
//
$0.output = "I am Lung"
$0.loop(validated: self.count >= 5, t: 1)
}.subscribe { ... }.catch { err in ... }.dispose { ... }
}
private func heart() -> PriorityElement<String, String> {}
private func liver() -> PriorityElement<String, String> {}
private func over() -> PriorityElement<String, String> {}
// ... ...
let head: PriorityElement<String, Int> = self.head()
head.then(neck())
.then(lung())
.then(heart())
.then(liver())
.then(over())
// nil also default value()
head.execute()

也许大家看到这里闻到一股熟悉的 Goolge/Promisesmxcl/PromiseKit 或者 RAC 等味道,那么为啥不用这些个大神的框架来解决实际问题呢?

主要有一点:框架功能过于丰富而复杂,而我呢,弱水三千我只要一瓢,越轻越好的原则!哈哈

这里可以看到详细的设计介绍,目前有 OC、Swift、Java 三个版本的具体实现。仓库地址:https://github.com/RunsCode/PromisePriorityChain 欢迎大家指正。

项目中区分 AppStore/Adhoc 包(二)

上期介绍了一种约定 Configuration,自定义预编译宏进行区分 AppStore/Adhoc 包的方法。后来尝试发现还可以通过应用内配置文件(embedded.mobileprovision)和 IAP 收据名区分包类型。

embedded.mobileprovison 仅在非 AppStore 环境存在,而且它里面还有一个参数 aps-environment 可以区分证书的类型是 development 还是 production,这两个值就对应了 Development 和 AdHoc 包。

另外 IAP 在非上架场景都是沙盒环境(上线 AppStoreConnect 的 TestFlight 包也是沙盒环境),是否为支付的沙盒环境我们可以用 Bundle.main.appStoreReceiptURL?.lastPathComponent 是否为 sandboxReceipt 进行判断。

所以使用上面两项内容我们可以区分:Development、AdHoc、TestFlight、AppStore。

读取 embedded.mobileprovision

在命令行中我们可以利用 security 读取:

1
$ security cms -D -i embedded.mobileprovision

在开发阶段读取方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
struct MobileProvision: Decodable {
var name: String
var appIDName: String
var platform: [String]
var isXcodeManaged: Bool? = false
var creationDate: Date
var expirationDate: Date
var entitlements: Entitlements

private enum CodingKeys : String, CodingKey {
case name = "Name"
case appIDName = "AppIDName"
case platform = "Platform"
case isXcodeManaged = "IsXcodeManaged"
case creationDate = "CreationDate"
case expirationDate = "ExpirationDate"
case entitlements = "Entitlements"
}

// Sublevel: decode entitlements informations
struct Entitlements: Decodable {
let keychainAccessGroups: [String]
let getTaskAllow: Bool
let apsEnvironment: Environment

private enum CodingKeys: String, CodingKey {
case keychainAccessGroups = "keychain-access-groups"
case getTaskAllow = "get-task-allow"
case apsEnvironment = "aps-environment"
}
// Occasionally there will be a disable
enum Environment: String, Decodable {
case development, production, disabled
}
}
}

class AppEnv {

enum AppCertEnv {
case devolopment
case adhoc
case testflight
case appstore
}

var isAppStoreReceiptSandbox: Bool {
return Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt"
}

var embeddedMobileProvisionFile: URL? {
return Bundle.main.url(forResource: "embedded", withExtension: "mobileprovision")
}

var appCerEnv: AppCertEnv!

init() {
// init or other time
assemblyEnv()
}

func assemblyEnv() {
if let provision = parseMobileProvision() {
switch provision.entitlements.apsEnvironment {
case .development, .disabled:
appCerEnv = .devolopment
case .production:
appCerEnv = .adhoc
}
} else {
if isAppStoreReceiptSandbox {
appCerEnv = .testflight
} else {
appCerEnv = .appstore
}
}
}

/// ref://gist.github.com/perlmunger/8318538a02166ab4c275789a9feb8992
func parseMobileProvision() -> MobileProvision? {
guard let file = embeddedMobileProvisionFile,
let string = try? String.init(contentsOf: file, encoding: .isoLatin1) else {
return nil
}

// Extract the relevant part of the data string from the start of the opening plist tag to the ending one.
let scanner = Scanner.init(string: string)
guard scanner.scanUpTo("<plist", into: nil) != false else {
return nil
}
var extractedPlist: NSString?
guard scanner.scanUpTo("</plist>", into: &extractedPlist) != false else {
return nil
}

guard let plist = extractedPlist?.appending("</plist>").data(using: .isoLatin1) else { return nil}

let decoder = PropertyListDecoder()
do {
let provision = try decoder.decode(MobileProvision.self, from: plist)
return provision
} catch let error {
// TODO: log / handle error
print(error.localizedDescription)
return nil
}
}
}

面试解析

整理编辑:师大小海腾

本期通过一个 demo 讲解 isMemberOfClass:isKindOfClass: 两个方法的相关知识点。

以下打印结果是什么?(严谨点就添加个说明吧:Person 类继承于 NSObject 类)

1
2
3
4
5
6
BOOL res1 = [[NSObject class] isKindOfClass:[NSObject class]];
BOOL res2 = [[NSObject class] isMemberOfClass:[NSObject class]];
BOOL res3 = [[Person class] isKindOfClass:[Person class]];
BOOL res4 = [[Person class] isMemberOfClass:[Person class]];

NSLog(@"%d, %d, %d, %d", res1, res2, res3, res4);

打印结果:1, 0, 0, 0

解释:

以下是 objc4-723 中 isMemberOfClass:isKindOfClass: 方法以及 object_getClass() 函数的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}

- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}

+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}

- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}

Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}

emmm 整理的时候发现后面的版本又做了小优化,具体就不展开了,不过原理不变,以下是 824 版本的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+ (BOOL)isMemberOfClass:(Class)cls {
return self->ISA() == cls;
}

- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}

+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}

- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}

由此我们可以得出结论:

  • isMemberOfClass: 方法是判断当前 instance/class 对象的 isa 指向是不是 class/meta-class 对象类型;
  • isKindOfClass: 方法是判断当前 instance/class 对象的 isa 指向是不是 class/meta-class 对象或者它的子类类型。

显然 isKindOfClass: 的范围更大。如果方法调用者是 instance 对象,传参就应该是 class 对象。如果方法调用着是 class 对象,传参就应该是 meta-class 对象。所以 res2-res4 都为 0。那为什么 res1 为 1 呢?

因为 NSObject 的 class 的对象的 isa 指向它的 meta-class 对象,而它的 meta-class 的 superclass 指向它的 class 对象,所以 [[NSObject class] isKindOfClass:[NSObject class]] 成立 。

总之,[instance/class isKindOfClass:[NSObject class]] 恒成立。(严谨点,需要是 NSObject 及其子类类型)

优秀博客

整理编辑:皮拉夫大王在此我是熊大

本期主题:Swift 指针Swift 属性包裹器

1、Swift 中的指针使用 – 来自:onevcat

Swift 中指针使用场景并不常见,但是有些时候我们又不得不尝试去使用指针,因此还是需要对 Swift 的指针运用有一定的了解。这篇文章是喵神 15 年写的,并在 2020 年做了更新。文章对 C 指针和 Swift 的指针应用做了映射,对于有一定 C 指针基础的同学阅读比较友好。

2、The 5-Minute Guide to C Pointers – 来自:Dennis Kubes

喵神文章中推荐的 C 语言指针教程,如果对 C 指针不了解的话,直接切入到 Swift 的指针还是有一定的困难的。

3、Swift5.1 - 指针Pointer – 来自简书:HChase

这篇文章根据 Swift 的类型给出了多种使用方法,查找用法非常方便。例如 malloc 之后如何填充字节、如何根据地址创建指针、如何进行类型转换等。如果在开发中需要使用 Swift 的指针,在不熟悉的情况下可以参考文中的小 demo。

4、使用 Property Wrapper 为 Codable 解码设定默认值 – 来自:onevcat

在 Swift 中,json 转 model 可以使用 Codable,但因为其无法指定可选值的默认属性,在开发的过程中需要更冗余的代码进行解可选操作;onevcat 的这篇文章就利用 Property Wrapper 为 Codable 解码设定了默认值。此外我将其总结成了一个文件 SSCodableDefault.swift,欢迎大家使用。

5、What is a Property Wrapper in Swift – 来自:sarunw

属性包装器是一种包装属性以添加额外逻辑的新类型。作者先抛出问题,分析属性包装器出现之前如何对属性进行包装以及他遇到的问题,然后来利用属性包装器对属性进行逻辑包装,比较了二种方式的区别,简述了属性包装器的好处。

6、Swift 5 属性包装器Property Wrappers完整指南 – 来自掘金:乐Coding

本文是使用属性包装器的一篇中文教程、可以结合 4、5 阅读。

学习资料

整理编辑:Mimosa

Machine Learning Crash Course from Google

地址:https://developers.google.com/machine-learning/crash-course

来自 Google 的机器学习教程资料,以 TensorFlow APIs 为基础,进行包括视频教学、真实的案例探究和动手实践练习。由于是谷歌支持的项目,你也可以在通过学习之后,去 Kaggle 竞赛获得真实竞赛经验,或者访问 Learn with Google AI,探索更完整的培训资源库。

ML-For-Beginners from Microsoft

地址:https://github.com/microsoft/ML-For-Beginners

来自 Microsoft 的机器学习教程资料。提供了一个为期 12 周、有 24 个课时的关于机器学习的课程。在这个课程中,你将学习经典的机器学习,主要使用 Scikit-learn 作为一个库来接触机器学习。这在我们即将推出的「AI初学者」课程中有所涉及。这些课程也可以与我们即将推出的「数据科学初学者」课程搭配使用。我们将这些经典技术应用于世界上许多地区的一些数据,请与我们一起到世界各地旅行,来边学习边旅行!在 Microsoft 的仓库中你也可以找到其他所有课程。

turicreate from Apple

地址:https://apple.github.io/turicreate/docs/userguide/

来自 Apple 的 turicreate 样例模型以及简化模型程序,他并不是学习机器学习的教程,而是给你提供解决任务的方案。你可以使用 turicreate 来训练推荐算法、对象检测、图像分类、图像相似性或活动分类等简单的机器学习模型。通过产生的 .mlmodel 模型,可以直接放到工程中使用 Core ML 来轻松使用。

工具推荐

整理编辑:zhangferry

explainshell

地址https://explainshell.com/

这个网站跟上期介绍的 regex101 很类似,一个用于解析正则表达式,一个用于解析 shell 指令。不常接触 shell 的小伙伴对于一个参数巨多,又巨长的指令可能会手足无措,没关系,这个网站来帮你😏。它会把主要命令和各个参数,传值进行详细的拆分讲解。比如这句列出所有包含 1a1b1c 这一 commit 的分支:

1
git branch -a -v --no-abbrev --contains 1a1b1c

解析结果:

关于我们

iOS 摸鱼周报,主要分享开发过程中遇到的经验教训、优质的博客、高质量的学习资料、实用的开发工具等。周报仓库在这里:https://github.com/zhangferry/iOSWeeklyLearning ,如果你有好的的内容推荐可以通过 issue 的方式进行提交。另外也可以申请成为我们的常驻编辑,一起维护这份周报。另可关注公众号:iOS成长之路,后台点击进群交流,联系我们,获取更多内容。

往期推荐

iOS摸鱼周报 第二十二期

iOS摸鱼周报 第二十一期

iOS摸鱼周报 第二十期

iOS摸鱼周报 第十九期

iOS摸鱼周报 第二十二期

iOS摸鱼周报 第二十二期

本期概要

  • 本期话题:聊聊 iOS 博客环境,公众号vs掘金。
  • Tips:Reachability 的使用建议,SQL 中 JOIN、UNION 的含义,如何在项目中区分 AdHoc 和 AppStore 包。
  • 面试解析:本期讲解 block 类型 的相关知识点。
  • 优秀博客:如何做电量方面的优化,关于 MetricKit 的使用。
  • 学习资料:布朗大学的学生制作的「看见统计」课程;一个 Github 仓库 Hacker Laws,总结各种定律和法则。
  • 一个帮助学习正则表达式的在线工具:regex101。

本期话题

@zhangferry:看一张最近掘金的作者排行榜图片

从上图发现,掘金排名靠前的前端和 Android 端有不少作者也有公众号,而 iOS 端却没有;另一个方面是,前端和 Android 靠前的都有不少是团队号,而 iOS 端基本都是个人创作者。因为这个排行是根据最近一段时间作者文章活跃度动态变化的,所以会存在一定的偶然性,但从中也能分析出一些端倪。以下是我总结出的几点(仅指掘金平台):

  • 资深作者不活跃:iOS 大号(等级高的号)创作活跃度不高,也可能偶然事件了,大号都在蛰伏下一篇。
  • 写作环境不过滤:iOS 写作环境不好,这个由来已久了,一直以来都是面试文章为主,一些培训机构甚至都将掘金作为了学员练手的平台,真正分享项目经验、实践的较少。
  • 创作环境难突围:掘金平台需要被推荐时才能更高的爆光量,同时,iOS 端作者写作公众号的较少。
  • @iHTCboy:iOS 系统封闭,所以苹果提供的 API 统一和完善,大多数的 API 网络上已经有很多文章积量,而前端和安卓,因为系统兼容性和开放性,手机厂商多等,需要更多的技术探索。

整个反应出的就是 iOS 社区不活跃,但真是这样吗?单就前段时间老司机周报组织的 WWDC21 内参来说,参与者就有 200+,社区肯定还是活跃的啊。所以我得出的结论是,就 iOS 博客环境来说,公众号要优于掘金。紧接着就有了这次的想法,介绍一些优质的由个人维护的 iOS 开发公众号、博客、独立应用等,同时还会邀请对应的作者讲一下做这些内容的初衷,目前的运营现状和一些未来规划。

这次是第一期,邀请的是摸鱼周报联合编辑:展菲。

博主访谈

个人介绍:展菲,目前就职于外企,从事人工智能、智能家居研发工作。

公众号:Swift社区。

FBY展菲:公众号是由 Swift 爱好者共同维护,我们会分享以 Swift 实战、SwiftUI、Swift 基础为核心的技术内容,也整理收集优秀的学习资料。

做最好的 Swift 社区,我们的使命是做一个最专业最权威的 Swift 中文社区,我们的愿景是希望更多的人学习和使用 Swift。我们会不断维护优化
,持续输出优质的原创内容。不忘初心,牢记使命。

如果您对 Swift 感兴趣欢迎关注我们,有好的建议欢迎联系我们。

开发Tips

整理编辑:夏天zhangferry

Reachability 类的实践准则

在网络请求实践中,常见的操作是监听 Reachability 的状态或变换来有选择的对网络的可达性进行判断,做出阻止或暂停请求的对应操作。

一直以来,受到监听网络状态这种手段的影响,当用户在执行某些操作时,会根据获取到的用户当前的网络状态,在网络不可达(NotReachable)的情况下会阻止用户发起网络请求

直到我看到了 AFNetworking Issues 中的 Docs on Reachability contradict Apple’s docs

我们不应该使用 Reachability 作为网络是否可用的判断,因为在某些情况下,其返回的状态可能不那么准确。

在 AFNetworking 的定义了关于 Reachability 的最佳实践:

Reachability can be used to determine background information about why a network operation failed, or to trigger a network operation retrying when a connection is established. It should not be used to prevent a user from initiating a network request, as it’s possible that an initial request may be required to establish reachability.

我们应该将其用于网络请求后失败的背景信息,或者在失败后用于判断是否应该重试。不应该用于阻止用户发起网络请求,因为可能需要初始化请求来建立可达性

我们在网络请求中集成的 Reachability 应该用在请求失败后,无论是作为请求失败的提示,还是利用可达性状态的更改,作为请求重试的条件。

当我们使用 AFNetworkReachabilityManager 或者功能相似的 Reachability 类时,我们可基于此来获取当前的网络类型,如 4G 还是 WiFi。但是当我们监听到其状态变为不可达时,不应该立即弹出 Toast 来告诉用户当前网络不可用,而应该是当请求失败以后判断该状态是否是不可达,如果是,再提示没有网络。并且,如果该接口需要一定的连贯性的话,可以保留当前的请求参数,提示用户等待网络可达时再主动去请求。

SQL 中的 JOIN 和 UNION

JOIN 作用是把多个表的行结合起来,各个表中对应的列有可能数据为空,就出现了多种结合关系:LEFT JOIN、RIGHT JOIN、INNER JOIN、OUTER JOIN。对应到集合的表示,它们会出现如下 7 种表示方法:

UNION 表示合并多个 SELECT 结果。UNION 默认会合并相同的值,如果想让结果逐条显示的话可以使用 UNION ALL。

有一个场景:三个表:table1、table2、table3,他们共用一个 id,table2 和 table3 为两个端的数据接口完全相同的数据,我现在要以table1 的某几个列为准,去查看对应到 table2 和 table3 与之关联的几个列的数据。SQL 语句如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
select 
t1.id
t1.column_name1 as name1,
t1.column_name2 as name2,
t2.column_name3 as name3,
t2.column_name4 as name4
from
(
select
id,
column_name1,
column_name2
from
table1_name
) t1
left join
(
select
union_table.*
from
(
select
id,
column_name3,
column_name4
from
table2_name
union all
select
id,
column_name3,
column_name4
from
table3_name
) union_table
) t2 on t1.id = t2.id

在项目中区分 AppStore/Adhoc 包

在解决这个问题前,我们先要了解开发环境这个概念,iOS 的开发环境通常涉及三个维度:项目,开发端服务器,AppStore 服务器。

项目

即我们的 Xcode 项目,它由 Project 里的 Configuration 管理,默认有两个开发环境:Debug、Release。而我们常用的控制开发环境的宏命令如 DEBUG 是 Xcode 帮我们预置的值,它的设置形式为 DEBUG=1,这里的内容都是可以修改的。

我们新增一个名为 AppStore 的 Configuration,然后给其设置一个宏 APPSTORE=1,然后将之前的 Release 设置为 ADHOC=1,即是为这两个项目环境指定了特定的宏。

开发端服务器

服务器环境由服务端管理,反应到项目里,我们通常将其跟项目环境保持一致。

AppStore 服务器

AppStore 的开发环境根据证书形式来定,其决定了 IAP 和推送的使用场景,在最后的封包环节决定,Xcode 将其分为以下四种场景:

可以看到 Configuration 设置和封包环节是相互独立的,如果本地有三个 Configuration 的话,我们可导出的包类型数量最多为:3 x 4 = 12 种。所以如果仅仅用开发包和生成环境包描述一个包的类型肯定是不够用的,但全描述又不现实,又因封包环节在编译之后,所以我们没法提前决定包类型,所以就有了约定成俗的一些习惯。

开发包通常指:Debug + Development,

生产环境包通常指:Release + Ad Hoc 或 Release + App Store Conenct

如题目所说,如果我们要区分 Ad Hoc 和 AppStore,就需要新增 Configuration:AppStore,这样的话:

Ad Hoc 包:Release + Ad Hoc

AppStore 包:AppStore + App Store Connect

这样代码里我们就可以使用我们上面定义的宏对 Ad Hoc 和 AppStore 进行单独配置了。

既然是约定所以他们之间是不存在强关联的,所以推荐使用脚本进行打包,以避免人为导致的错误。

备注:经@iHTCboy 建议,还可以通过非 Configuration 的形式区分包类型,部分内容还未实践,测试完毕之后会将方案放到下一期内容。

面试解析

整理编辑:师大小海腾

本期讲解 block 类型 的相关知识点。你是否遇到过这样的面试题:

  • block 都有什么类型?
  • 栈 block 存在什么问题?
  • block 每种类型调用 copy 的结果分别是怎样的?

希望以下的总结能帮助到你。如果你对内容有任何疑问,或者有更好的解答,都可以联系我们。

block 类型

block 有 3 种类型:栈块、堆块、全局块,最终都是继承自 NSBlock 类型。

block 类型 描述 环境
__NSGlobalBlock__
( _NSConcreteGlobalBlock )
全局 block,保存在数据段 没有访问自动局部变量
__NSStackBlock__
( _NSConcreteStackBlock )
栈 block,保存在栈区 访问了自动局部变量
__NSMallocBlock__
( _NSConcreteMallocBlock )
堆 block,保存在堆区 __NSStackBlock__ 调用了 copy

1. 栈块

定义块的时候,其所占的内存区域是分配在栈中的。块只在定义它的那个范围内有效。

1
2
3
4
5
6
7
8
9
10
11
void (^block)(void);
if ( /* some condition */ ) {
block = ^{
NSLog(@"Block A");
};
} else {
block = ^{
NSLog(@"Block B");
};
}
block();

上面的代码有危险,定义在 if 及 else 中的两个块都分配在栈内存中,当出了 if 及 else 的范围,栈块可能就会被销毁。如果编译器覆写了该块的内存,那么调用该块就会导致程序崩溃。或者数据可能会变成垃圾数据,尽管将来该块还能正常调用,但是它捕获的变量的值已经错乱了。

若是在 ARC 下,上面 block 会被自动 copy 到堆,所以不会有问题。但在 MRC 下我们要避免这样写。

2. 堆块

为了解决以上问题,可以给块对象发送 copy 消息将其从栈拷贝到堆区,堆块可以在定义它的那个范围之外使用。堆块是带引用计数的对象,所以在 MRC 下如果不再使用堆块需要调用 release 进行释放。

1
2
3
4
5
6
7
8
9
10
11
12
void (^block)(void);
if ( /* some condition */ ) {
block = [^{
NSLog(@"Block A");
} copy];
} else {
block = [^{
NSLog(@"Block B");
} copy];
}
block();
[block release];

3. 全局块

如果运行块所需的全部信息都能在编译期确定,包括没有访问自动局部变量等,那么该块就是全局块。全局块可以声明在全局内存中,而不需要在每次用到的时候于栈中创建。
全局块的 copy 操作是空操作,因为全局块决不可能被系统所回收,其实际上相当于单例。

1
2
3
void (^block)(void) = ^{
NSLog(@"This is a block");
};

每一种类型的 block 调用 copy 后的结果如下所示:

block 类型 副本源的配置存储区 复制效果
_NSConcreteGlobalBlock 程序的数据段区 什么也不做
_NSConcreteStackBlock 从栈复制到堆
_NSConcreteMallocBlock 引用计数增加

优秀博客

整理编辑:皮拉夫大王在此我是熊大

本期主题:电量优化

1、iOS性能优化之耗电检测 – 来自:杂货铺

文章介绍了耗电量检测的三种方式:Energy impact、Energy Log、sysdiagnose。 每种方案详细介绍了检测步骤。在 Energy Log 中提到了“当前台三分钟或后台一分钟 CPU 线程连续占用 80% 以上就判定为耗电,同时记录耗电线程堆栈供分析”,这对我们日常分析有一定的帮助。

2、Analyzing Your App’s Battery Use – 来自:Apple

苹果官方提供了一些性能监控的手段,通过 Xcode Organizer 可以查看 24 小时的性能数据,包括电量数据。

3、iOS 性能优化:使用 MetricKit 2.0 收集数据 – 来自老司机周报:Jerry4me

既然提到了官方的方案就不得不提到 MetricKit。本文介绍了什么是 MetricKit,如何使用以及 iOS 14 之后的新的数据指标。另外需要注意的是 MetricKit 是 iOS13 之后才支持的,并且并不能搜集全部用户的数据,只有共享 iPhone 分析的用户数据才能被收集。

4、iOS进阶–App功耗优化 – 来自cocoachina:yyuuzhu

直观上耗电大户主要包括:CPU、设备唤醒、网络、图像、动画、视频、动作传感器、定位、蓝牙。测试工具:Energy Impact、Energy Log,更加具体的信息查看本文。

5、iOS耗电量和性能优化的全新框架 – 来自博客:Punmy

在 Session 417 中,苹果推出了三项新的电量和性能监测工具,分别用于开发阶段、内测阶段、以及线上阶段。相信通过本文,你会对你的 App 接下去的耗电量和性能优化的方向,有更好的计划。

6、耗电优化的几点建议 – 来自博客:Catalog

关于耗电优化的几点实操性的建议。

学习资料

整理编辑:Mimosa

Seeing Theory

地址;https://seeing-theory.brown.edu/cn.html

由布朗大学的学生制作的「看见统计」课程,致力于用数据可视化的手段让数理统计概念更容易理解。其中的内容与国内本科的概率论与数理统计内容也大致相仿,且对于中文的本地化支持的非常好。

Hacker Laws

地址:https://github.com/dwmkerr/hacker-laws

我们常常会说「xx法则」、「xx定律」,如「摩尔定律」等。在 Hacker Laws 这个仓库中,我们可以找到在开发者群体比较有名或者是比较常见的法则和定律。但要注意:这个资源库包含了对一些法则、原则和模式的解释,但并不提倡任何一种。它们是否应该被应用在很大程度上取决于你正在做的事情,要根据情况来判断使用与否。

工具推荐

整理编辑:zhangferry

regex101

地址https://regex101.com

一个正则表达式测试和分析网站,不仅可以将匹配结果进行输出,还会逐个分析表达式的含义。我们以摸鱼周报关于我们的文案进行测试,我们想匹配出 “iOS 摸鱼周报”(中间有空格),“iOS成长之路”,这两个字符串。文案特征为:”iOS“开头,不能紧跟其他字母,以逗号结尾但不包括逗号。测试结果如下:

观察右侧结果分析,示例中使用的 *? 非贪婪模式和 (?=,) 零宽度正预测先行断言,都有很详细的讲解。特别是我们拿到别人写好的正则表达式的时候,通过这个网站可以很好的分析每个语句的作用。

关于我们

iOS 摸鱼周报,主要分享开发过程中遇到的经验教训、优质的博客、高质量的学习资料、实用的开发工具等。周报仓库在这里:https://github.com/zhangferry/iOSWeeklyLearning ,如果你有好的的内容推荐可以通过 issue 的方式进行提交。另外也可以申请成为我们的常驻编辑,一起维护这份周报。另可关注公众号:iOS成长之路,后台点击进群交流,联系我们,获取更多内容。

往期推荐

iOS摸鱼周报 第二十一期

iOS摸鱼周报 第二十期

iOS摸鱼周报 第十九期

iOS摸鱼周报 第十八期

iOS摸鱼周报 第二十一期

iOS摸鱼周报 第二十一期

本期概要

本期话题讲了一些身体的特殊机制,希望大家看完能少熬夜。

开发Tips本期讲的是 UserDefaults 的一些用法。

面试解析本期讲解 load 和 initialize 的一些知识点。

博客内容推荐了几篇不同方式的内存优化文章。

Open Source Society University:非常受欢迎的计算机科学自学教程,Swift Programming for macOS:用Swift开发Mac 引用的一些示例介绍。

Messier:一个 Objective-C 方法耗时监控的方案。

本期话题

@zhangferry:今天看到一个视频,来自北医三院的薄世宁医生,他是危重医学科副主任医师,讲人在危重的时候,身体会有什么反应。当时看完还是比较震撼的,这里转成如下文字:

我们经常被教育在危难的时候要努力,其实你完全不知道你身体里面的细胞比你还努力。人在大出血的时候血压会迅速下降,这么做的目的是为了减缓出血的速度,而且肾脏会没有尿,是为了把有限的血流供应给更关键的大脑和心脏。

我们每天,人体会有 3300 亿个细胞死亡,同时会新生出 3300 亿个新生的细胞。孕妇在即将生产的前几天,血液当中有一个凝血指标会升高几十倍,这么做的目的是为了防止生产过程有可能发生的大出血。你肯定还会想不到,心脏每一次收缩跳动,它的电活动是要从窦房结,然后再到房室结之后,还要传导到心脏。当危难病重的时候,窦房结失败了,不能跳动了房室结开始替它跳,房室结也不能够传导的时候,心脏、心室开始自主逸博跳动。这个时候的跳动,心电图已经面目全非,人的血流也会非常微弱,但是即便是这么微弱的血流,也保证了大脑和心脏自身的供血,等着援兵的到来,你看在危机时刻,所有这些细胞都在为你拼命,拼命到生命的最后一刻。

讲这么多其实就是要告诉你,你还有什么资格熬夜抽烟喝酒,虐待你身体里这些组织和细胞呢。在困难面前,你还有什么资格轻言放弃呢,

开发Tips

关于UserDefaults你应该这么用

整理编辑:CoderStar

构造器的选用

UserDefaults生成对象实例大概有以下三种方式:

1
2
3
4
5
6
open class var standard: UserDefaults { get }

public convenience init()

@available(iOS 7.0, *)
public init?(suiteName suitename: String?)

平时大家经常使用的应该是第一种方式,第二种方式和第一种方式产生的结果是一样的,实际上操作的都是 APP 沙箱中 Library/Preferences 目录下的以 bundle id 命名的 plist 文件,只不过第一种方式是获取到的是一个单例对象,而第二种方式每次获取到都是新的对象,从内存优化来看,很明显是第一种方式比较合适,其可以避免对象的生成和销毁。

如果一个 APP 使用了一些 SDK,这些 SDK 或多或少的会使用UserDefaults来存储信息,如果都使用前两种方式,这样就会带来一系列问题:

  • 各个 SDK 需要保证设置数据 KEY 的唯一性,以防止存取冲突;
  • plist 文件越来越大造成的读写效率问题;
  • 无法便捷的清除由某一个 SDK 创建的 UserDefaults 数据;

针对上述问题,我们可以使用第三种方式。

第三种方式根据传入的 suiteName的不同会产生四种情况:

  • 传入 nil:跟使用UserDefaults.standard效果相同;
  • 传入 bundle id:无效,返回 nil;
  • 传入 App Groups 配置中 Group ID:会操作 APP 的共享目录中创建的以Group ID命名的 plist 文件,方便宿主应用与扩展应用之间共享数据;
  • 传入其他值:操作的是沙箱中 Library/Preferences 目录下以 suiteName 命名的 `plist 文件。

UserDefaults的统一管理

经常会在一些项目中看到UserDefaults的数据存、取操作,key直接用的字符串魔法变量,搞到最后都不知道项目中UserDefaults到底用了哪些 key,对 key 的管理没有很好的重视起来。下面介绍两种UserDefaults使用管理的两种方式,一种是通过protocol及其默认实现的方式,另一种是通过@propertyWrapper的方式,因第一种方式涉及代码比较多,不便在周报中展示,这里就只介绍第二种方式。

Swift 5.1 推出了为 SwiftUI 量身定做的@propertyWrapper关键字,翻译过来就是属性包装器,有点类似 java 中的元注解,它的推出其实可以简化很多属性的存储操作,使用场景比较丰富,用来管理UserDefaults只是其使用场景中的一种而已。

先上代码,相关说明请看代码注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@propertyWrapper
public struct UserDefaultWrapper<T> {
let key: String
let defaultValue: T
let userDefaults: UserDefaults

/// 构造函数
/// - Parameters:
/// - key: 存储key值
/// - defaultValue: 当存储值不存在时返回的默认值
public init(_ key: String, defaultValue: T, userDefaults: UserDefaults = UserDefaults.standard) {
self.key = key
self.defaultValue = defaultValue
self.userDefaults = userDefaults
}

/// wrappedValue是@propertyWrapper必须需要实现的属性
/// 当操作我们要包裹的属性时,其具体的set、get方法实际上走的都是wrappedValue的get、set方法
public var wrappedValue: T {
get {
return userDefaults.object(forKey: key) as? T ?? defaultValue
}
set {
userDefaults.setValue(newValue, forKey: key)
}
}
}

// MARK: - 使用示例

enum UserDefaultsConfig {
/// 是否显示指引
@UserDefaultWrapper("hadShownGuideView", defaultValue: false)
static var hadShownGuideView: Bool

/// 用户名称
@UserDefaultWrapper("username", defaultValue: "")
static var username: String

/// 保存用户年龄
@UserDefaultWrapper("age", defaultValue: nil)
static var age: Int?
}

func test() {
/// 存
UserDefaultsConfig.hadShownGuideView = true
/// 取
let hadShownGuideView = UserDefaultsConfig.hadShownGuideView
}

UserDefaults的一些相关问题以及第一种利用protocol及其默认实现的管理方式的详细描述可以前往UserDefaults 浅析及其使用管理查看。

面试解析

整理编辑:师大小海腾

本期讲解 load 和 initialize 的相关知识点。

load 和 initialize 的区别

区别 load initialize
调用时刻 Runtime 加载类、分类时调用
(不管有没有用到这些类,在程序运行起来的时候都会加载进内存,并调用 +load 方法)。

每个类、分类的 +load,在程序运行过程中只调用一次(除非开发者手动调用)。
第一次接收到消息时调用。

如果子类没有实现 +initialize 方法,会调用父类的 +initialize,所以父类的 +initialize 方法可能会被调用多次,但不代表父类初始化多次,每个类只会初始化一次。
调用方式 ① 系统自动调用 +load 方式为直接通过函数地址调用;
② 开发者手动调用 +load 方式为消息机制 objc_msgSend 函数调用。
消息机制 objc_msgSend 函数调用。
调用顺序 ① 先调用类的 +load,按照编译先后顺序调用(先编译,先调用),调用子类的 +load 之前会先调用父类的 +load
② 再调用分类的 +load,按照编译先后顺序调用(先编译,先调用)(注意:通过消息机制调用分类方法是:后编译,优先调用)。
① 先调用父类的 +initialize
② 再调用子类的 +initialize
(先初始化父类,再初始化子类)。

手动调用子类的 load 方法,但是子类没有实现该方法,为什么会去调用父类的 load 方法,且是调用父类的分类的 load 方法呢?

因为 load 方法可以继承,手动调用 load 的方式为是消息机制的调用,会去类方法列表里找对应的方法,由于子类没有实现,就会去父类的方法列表中查找。因为分类方法会“覆盖”同名宿主类方法,所以如果父类的分类实现了 load 方法,那么会调用分类的。如果存在多个分类都实现 load 方法的话,那么会调用最后参与编译的分类的 load 方法。

优秀博客

整理编辑:皮拉夫大王在此我是熊大

内存优化可以从以下几点入手:

  • 工具分析,可以利用 Xcode 自带的 Instruments 中的 leak、allocation,也可以利用 MLeaksFinder 等开源工具。找到内存泄漏、内存激增、内存不释放的位置。

  • 利用 mmap,一种低内存的首选方案。

  • 图片优化,经过第一步之后,一定会发现内存激增极有可能与图片相关。

1、iOS的文件内存映射——mmap –来自简书:落影loyinglin

mmap 一定是低内存方案的首选。文件映射,用于将文件或设备映射到虚拟地址空间中,以使用户可以像操作内存地址一样操作文件或设备,作者介绍了 mmap 原理并根据官方代码,整理了一个简单的 Demo,有兴趣的人还可以阅读下微信的开源仓库:MMKV。

2、iOS图片加载速度极限优化—FastImageCache解析 – 来自博客:bang

在app中,图片在内存中占用比例相对较大,有没有办法优化缓存一些图片到磁盘中呢?答案是:FastImageCache。FastImageCache 是 Path 团队开发的一个开源库,用于提升图片的加载和渲染速度,让基于图片的列表滑动起来更顺畅,来看看它是怎么做的。

3、Instruments学习之Allocations – 来自简书:Thebloodelves

详细介绍 Allocations 的使用,为你分析 app 内存助力。

4、【基本功】深入剖析Swift性能优化 – 来自美团技术团队:亚男

Swift 已经是大势所趋,各个大厂都已经在做尝试和推广,所以内存优化也离不开 Swift。本文前半部分介绍了 Swift 的数据类型的内存分配情况,先了解 Swift 的内存基本原理才能在日常开发中提前避免问题的发生。

5、Swift内存泄漏详解([weak self]使用场景) – 来自简书:码农淏

本文通过代码的方式列举了 Swift 中造成内存泄漏的一些情况,比较适合 Swift 的初学者,文章较短但是比较实用。OC 转 Swift 的同学可以关注下。

学习资料

整理编辑:Mimosa

Open Source Society University

地址:https://github.com/ossu/computer-science

这是在 Github 有 92.7k Stars 的计算机科学自学教程。它是根据计算机科学专业本科生的学位要求设计的。这些课程本身是世界上最好的课程之一,通常来自哈佛、普林斯顿、麻省理工学院等。该课程不仅仅是为了职业培训或专业发展,它是为那些希望在所有计算机学科的基本概念方面有一个适当的、全面的基础的人而设的,也是为那些有纪律、意志和(最重要的是!)良好习惯的人而设的,可以使他们通过这种方式靠自己来获得这些知识。

Swift Programming for macOS

地址:https://gavinw.me/swift-macos/

尽管 iPhone 和 iPad 的 App 都需要 Mac 来进行代码开发,但关于实际创建原生 Mac App 的相关资料在网上很少见到。这个网站囊括了最新版本使用 Swift 和 SwiftUI 来开发 Mac App 的一些简单例子,给想要学习 Mac 开发的开发者提供一个小型的资源库。

工具推荐

整理编辑:zhangferry

Messier

地址https://messier-app.github.io/

软件状态:免费

软件介绍

Messier 是基于 AppleTrace 开发的 Objective-C 方法耗时测量应用,其相对于 AppleTrace 更易用,且能更方便的在越狱设备上 Trace 任意应用。它由三部分组成:Tweak 插件,动态库(Messier.framework),桌面端应用。非越狱场景,我们使用后两个部分可完成对自己应用的耗时监控,输出为 json 文件,再使用 chrome://tracing 将 json 文件绘制为火焰图,效果如下:

关于我们

iOS 摸鱼周报,主要分享开发过程中遇到的经验教训、优质的博客、高质量的学习资料、实用的开发工具等。周报仓库在这里:https://github.com/zhangferry/iOSWeeklyLearning ,如果你有好的的内容推荐可以通过 issue 的方式进行提交。另外也可以申请成为我们的常驻编辑,一起维护这份周报。另可关注公众号:iOS成长之路,后台点击进群交流,联系我们,获取更多内容。

往期推荐

iOS摸鱼周报 第二十期

iOS摸鱼周报 第十九期

iOS摸鱼周报 第十八期

iOS摸鱼周报 第十七期

iOS摸鱼周报 第二十期

iOS摸鱼周报 第二十期

本期概要

  • 小编整理了一些洪灾应对的指南,希望对大家有所帮助。
  • Tips 部分介绍了如何绘制一个高颜值的统计图。
  • 面试解析模块本期讲解深拷贝浅拷贝的知识点。
  • 优秀博客汇总了不少包体积优化的优秀文章。
  • 学习资料推荐了 Better Explaine 这个网站,其用于帮助大家理解那些复杂的数学概念。
  • 截图工具 Snipaste,无用图片搜索工具 LSUnusedResources。

本期话题

@zhangferry:近期的河南洪灾一直牵动人心,泄洪是治理洪水的有效措施,这次郑州泄洪的主要地方就有河南周口,下游是安徽的界首与阜阳。我是河南周口人,而我媳妇是安徽阜阳人,了解到各自家乡的情况之后,我们既感到心疼也感到自豪。

虽然天灾无情,但是有非常多感动人心的事情,一方有难八方支援,为每一个参与到河南抗洪救灾的人员致以最高的敬意。截止目前洪灾还没有完全退去,还不能掉以轻心。以下是我从多处官方新闻报道中总结的一些防洪应对指南,希望对大家有所帮助。

最后的最后,河南加油,安徽加油!

开发Tips

码一个高颜值统计图

整理编辑:FBY展菲

项目开发中有一些需求仅仅通过列表展示是不能满足的,如果通过图表的形式来展示,就可以更快捷的获取到数据变化情况。大部分情况我们都是使用第三方图表工具,现在我们介绍一个手动绘制的简易统计图,目前支持三种类型:折线统计图柱状图环形图

效果展示

折线统计图实现思路分析

观察发现折线图包含这几部分:x 轴、y 轴及其刻度,背景辅助线,代表走势的折线及圆圈拐点,折线下部的整体渐变。

1、x 轴、y 轴使用 UIBezierPath 描绘路径,使用 CAShapeLayer 设置颜色及虚线样式,标签使用 UILabel 表示,需要注意每个标点的间距。

2、背景辅助线及走势线绘制同坐标轴,区别仅在于线段走势和样式稍微不同。

3、渐变方案整体渐变,然后让折线图下部作为 mask 遮罩即可实现。

柱状图和圆饼图设计思路相似,大家可以自行思考,完整代码可查看这里:FBYDataDisplay-iOS。以下是折线走势的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#pragma mark 画折线图
- (void)drawChartLine {
UIBezierPath *pAxisPath = [[UIBezierPath alloc] init];

for (int i = 0; i < self.valueArray.count; i ++) {

CGFloat point_X = self.xScaleMarkLEN * i + self.startPoint.x;

CGFloat value = [self.valueArray[i] floatValue];
CGFloat percent = value / self.maxValue;
CGFloat point_Y = self.yAxis_L * (1 - percent) + self.startPoint.y;

CGPoint point = CGPointMake(point_X, point_Y);

// 记录各点的坐标方便后边添加 渐变阴影 和 点击层视图 等
[pointArray addObject:[NSValue valueWithCGPoint:point]];

if (i == 0) {
[pAxisPath moveToPoint:point];
}
else {
[pAxisPath addLineToPoint:point];
}
}

CAShapeLayer *pAxisLayer = [CAShapeLayer layer];
pAxisLayer.lineWidth = 1;
pAxisLayer.strokeColor = [UIColor colorWithRed:255/255.0 green:69/255.0 blue:0/255.0 alpha:1].CGColor;
pAxisLayer.fillColor = [UIColor clearColor].CGColor;
pAxisLayer.path = pAxisPath.CGPath;
[self.layer addSublayer:pAxisLayer];
}

遇到的问题(已解决)

reloadDatas 方法无效,title 没变,数据源没变,移除 layer 的时候还会闪退

解决方案:在 reloadData 时,需要将之前缓存的数组数据 pointArray 清空,不然数组中保存了上次的数据。

参考:码一个高颜值统计图 - 展菲

面试解析

整理编辑:师大小海腾

本期讲解深浅拷贝的知识点。文章将从深拷贝和浅拷贝的区别开始讲起,然后讲解在 iOS 中对 mutable 对象与 immutable 对象进行 copy 与 mutableCopy 的结果,以及如何对集合对象进行真正意义上的深拷贝,最后带你实现对自定义对象的深浅拷贝。

对深浅拷贝的理解

我们先要理解拷贝的目的:产生一个副本对象,跟源对象互不影响。

深拷贝和浅拷贝

拷贝类型 拷贝方式 特点
深拷贝 内存拷贝,让副本对象指针和源对象指针指向 两片 内容相同的内存空间。 1. 不会增加被拷贝对象的引用计数;
2. 产生了一个内存分配,出现了两块内存。
浅拷贝 指针拷贝,对内存地址的复制,让副本对象指针和源对象指针指向 同一片 内存空间。 1. 会增加被拷贝对象的引用计数;
2. 没有进行新的内存分配。
注意:如果是小对象如 NSString,可能通过 Tagged Pointer 来存储,没有引用计数。

简而言之:

  • 深拷贝:内存拷贝,产生新对象,不增加被拷贝对象引用计数
  • 浅拷贝:指针拷贝,不产生新对象,增加被拷贝对象引用计数,相当于执行了 retain
  • 区别:1. 是否影响了引用计数;2. 是否开辟了新的内存空间

在 iOS 中对 mutable 对象与 immutable 对象进行 copy 与 mutableCopy 的结果

iOS 提供了 2 个拷贝方法:

  • copy:不可变拷贝,产生不可变副本
  • mutableCopy:可变拷贝,产生可变副本

对 mutable 对象与 immutable 对象进行 copy 与 mutableCopy 的结果:

源对象类型 拷贝方式 副本对象类型 拷贝类型(深/浅)
mutable 对象 copy 不可变 深拷贝
mutable 对象 mutableCopy 可变 深拷贝
immutable 对象 copy 不可变 浅拷贝
immutable 对象 mutableCopy 可变 深拷贝

注:这里的 immutable 对象与 mutable 对象指的是系统类 NSArray、NSDictionary、NSSet、NSString、NSData 与它们的可变版本如 NSMutableArray 等。

一个记忆技巧就是:对 immutable 对象进行 copy 操作是 浅拷贝,其它情况都是 深拷贝

我们还可以根据拷贝的目的加深理解:

  • 对 immutable 对象进行 copy 操作,产生 immutable 对象,因为源对象和副本对象都不可变,所以进行指针拷贝即可,节省内存
  • 对 immutable 对象进行 mutableCopy 操作,产生 mutable 对象,对象类型不同,所以需要深拷贝
  • 对 mutable 对象进行 copy 操作,产生 immutable 对象,对象类型不同,所以需要深拷贝
  • 对 mutable 对象进行 mutableCopy 操作,产生 mutable 对象,为达到修改源对象或副本对象互不影响的目的,需要深拷贝

使用 copy、mutableCopy 对集合对象进行的深浅拷贝是针对集合对象本身的

使用 copy、mutableCopy 对集合对象(Array、Dictionary、Set)进行的深浅拷贝是针对集合对象本身的,对集合中的对象执行的默认都是浅拷贝。也就是说只拷贝集合对象本身,而不复制其中的数据。主要原因是,集合内的对象未必都能拷贝,而且调用者也未必想在拷贝集合时一并拷贝其中的每个对象。

如果想要深拷贝集合对象本身的同时,也对集合内容进行 copy 操作,可使用类似以下的方法,copyItems 传 YES。但需要注意的是集合中的对象必须都符合 NSCopying 协议,否则会导致 Crash。

1
NSArray *deepCopyArray = [[NSArray alloc]initWithArray:someArray copyItems:YES];

注:initWithArray:copyItems: 方法不是所有情况下都深拷贝集合对象本身的。如果执行 [[NSArray alloc]initWithArray:@[] copyItems:aBoolValue];,也就是源对象为不可变的空数组的话,对源对象本身执行的是浅拷贝,苹果对 @[] 使用了享元。

但是,如果集合中的对象的 copy 操作是浅拷贝,那么对于集合来说还不是真正意义上的深拷贝。比如,你需要对一个 NSArray<NSArray *> 对象进行真正的深拷贝,那么内层数组及其内容也应该执行深拷贝,可以对该集合对象进行 归档 然后 解档,只要集合中的对象都符合 NSCoding 协议。而且,使用这种方式,无论集合中存储的模型对象嵌套多少层,都可以实现深拷贝,但前提是嵌套的子模型也需要符合 NSCoding 协议才行,否则会导致 Crash。

1
NSArray *trueDeepCopyArray = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:oldArray]];

需要注意的是,使用 initWithArray:copyItems: 并将 copyItems 传 YES 时,生成的副本集合对象中的对象(下一个级别)是不可变的,所有更深的级别都具有它们以前的可变性。比如以下代码将 Crash。

1
2
3
4
>NSArray *oldArray = @[@[].mutableCopy];
>NSArray *deepCopyArray = [[NSArray alloc] initWithArray:oldArray copyItems:YES];
>NSMutableArray *mArray = deepCopyArray[0]; // deepCopyArray[0] 已经被深拷贝为 NSArray 对象
>[mArray addObject:@""]; // Crash

归档解档集合 的方式会保留所有级别的可变性,就像以前一样。

实现对自定义对象的拷贝

如果想要实现对自定义对象的拷贝,需要遵守 NSCopying 协议,并实现 copyWithZone: 方法。

  • 如果要浅拷贝,copyWithZone: 方法就返回当前对象:return self;
  • 如果要深拷贝,copyWithZone: 方法中就创建新对象,并给希望拷贝的属性赋值,然后将其返回。如果有嵌套的子模型也需要深拷贝,那么子模型也需符合 NSCopying 协议,且在属性赋值时调用子模型的 copy 方法,以此类推。

如果自定义对象支持可变拷贝和不可变拷贝,那么还需要遵守 NSMutableCopying 协议,并实现 mutableCopyWithZone: 方法,返回可变副本。而 copyWithZone: 方法返回不可变副本。使用方可根据需要调用该对象的 copy 或 mutableCopy 方法来进行不可变拷贝或可变拷贝。

以下代码会出现什么问题?

1
2
3
@interface Model : NSObject
@property (nonatomic, copy) NSMutableArray *array;
@end

不论赋值过来的是 NSMutableArray 还是 NSArray 对象,进行 copy 操作后都是 NSArray 对象(深拷贝)。由于属性被声明为 NSMutableArray 类型,就不可避免的会有调用方去调用它的添加对象、移除对象等一些方法,此时由于 copy 的结果是 NSArray 对象,所以就会导致 Crash。

参考:iOS 面试解析 - 对深浅拷贝的理解

优秀博客

整理编辑:皮拉夫大王在此

本期主题:包大小优化

1、今日头条 iOS 安装包大小优化—— 新阶段、新实践 – 来自微信公众号:字节跳动技术团队

在多个渠道多次推荐的老文章了,再次推荐还是希望能跟大家一块打开思路,尤其在二进制文件的控制上,目前还有很多比较深入的手段去优化,资源的优化可能并不是全部的手段。

2、今日头条优化实践: iOS 包大小二进制优化,一行代码减少 60 MB 下载大小 – 来自微信公众号:字节跳动技术团队

上篇文章的姊妹篇,也是大家比较熟悉的文章了。总而言之段迁移技术效果很明显,但是段迁移会带来一些其他的问题,比如文中提到的日志解析问题。我们在实践过程中也遇到了各种各样的小问题,一些二进制分析工具会失效,需要针对段迁移的 ipa 做适配。

3、基于mach-o+反汇编的无用类检测 – 来自简书:皮拉夫大王

很少在周报中推荐自己的文章,尤其是自己 2 年前的老文章。推荐这篇文章的原因是我在文中列举了 58 各个业务线的包大小占比分析。从数据中可以看出我们经过多年包大小治理后,资源的优化空间并不大,只能从二进制文件的瘦身上入手。可能很多公司的 APP 也有同样的问题,就是资源瘦身已经没有太大的空间了,这时我们就应该从二进制层面寻找突破口。文中工具地址:Github WBBlades

4、Flutter包大小治理上的探索与实践 – 来自美团技术团队:艳东 宗文 会超

谈点新鲜的内容。作者首先对 Flutter 的包大小进行了细致的分析,并在双平台选择了不同的优化方案。在 Android 平台选择动态下发,而 iOS 平台则将部分非指令数据进行动态下发。通过修改 Flutter 的编译后端将 data 重定向输入到文件,从而实现了数据段与文本段的拆分。使用 Flutter 的团队可以关注下这个方案。

5、iOS 优化 - 瘦身 – 来自微信公众号:CoderStar

文章详细介绍了 APP 瘦身的技巧与方案,包括资源和代码层面。对图片压缩与代码的编译选项有深入的解释。方案比较全面,可以通过此文章检查 APP 瘦身是否还有哪些方案没有应用。

6、科普:为什么iOS的APP比安卓大好几倍? – 来自简书:春暖花已开

前几篇文章已经将瘦身的技术介绍得比较完善了。接下来通过这篇文章回答下老板们经常会问到的问题:为什么 iOS 的包比安卓的大?是因为 iOS 的技术不如安卓吗?建议 iOS 程序员都看看这个问题,至少可以满足我们自己的好奇心。

学习资料

整理编辑:Mimosa

Better Explaine

地址:https://betterexplained.com/

Better Explaine 是一个帮助你真正理解数学概念、使数学概念变得有趣的网站,在这个网站你可以看到很多复杂的概念被分解成图形、公式和通俗易懂的解释。网站的指导思想是爱因斯坦的这句话:“如果你不能简单地解释它,你就没有充分地理解它”,这里没有装腔作势,没有古板老师,只是一个兴奋的朋友在分享究竟是什么让一个想法变成了现实!

程序员可能必读书单推荐

地址:https://draveness.me//books-1

来自 Draveness 的程序员书单,这是书单的系列一,应该还会有后续的推荐。这次的推荐中推荐了三本「大部头」:SICP、CTMCP 和 DDIA。即使你和小编一样感觉这些书晦涩难懂(苦笑),并不准备阅读,也可以从 Draveness 的这篇书单推荐中窥探一眼别人的编程世界是什么样的😉。

工具推荐

整理编辑:CoderStar

Snipaste

地址https://zh.snipaste.com/

软件状态: 普通版免费,专业版收费,有 Mac、Windows 两个版本

软件介绍

Snipaste 是一个简单但强大的截图工具,也可以让你将截图贴回到屏幕上!普通版的功能已经足够使用,笔者认为其是最好用的截图软件了!(下图是官方图)

Snipaste

LSUnusedResources

地址https://github.com/tinymind/LSUnusedResources

软件状态: 免费

软件介绍

一个 Mac 应用程序,用于在 Xcode 项目中查找未使用的图像和资源,可以辅助我们优化包体积大小。

LSUnusedResources

关于我们

iOS 摸鱼周报,主要分享开发过程中遇到的经验教训、优质的博客、高质量的学习资料、实用的开发工具等。周报仓库在这里:https://github.com/zhangferry/iOSWeeklyLearning ,如果你有好的的内容推荐可以通过 issue 的方式进行提交。另外也可以申请成为我们的常驻编辑,一起维护这份周报。另可关注公众号:iOS成长之路,后台点击进群交流,联系我们,获取更多内容。

往期推荐

iOS摸鱼周报 第十九期

iOS摸鱼周报 第十八期

iOS摸鱼周报 第十七期

iOS摸鱼周报 第十六期

iOS摸鱼周报 第十九期

iOS摸鱼周报 第十九期

本期概要

  • 本期话题讲了关于学习和记忆的一些方法。
  • 开发 Tips 讲了如何区分 minimumLineSpacing 和 minimumInteritemSpacing 这两个属性及本地化关于日期的注意事项。
  • 面试解析本期讲解了属性及属性关键字的几个知识点,由@师大小海腾@反向抽烟整理,内容非常之详细。
  • 优秀博客整理了几篇卡顿优化的优质文章。
  • 学习资料有两个内容,Combine Operators:帮助理解 Combine 操作符的手机端 App;还有 Stanford 最新的 SwiftUI 2.0 双语教程。
  • 开发工具带来了一个基于 linkmap 分析执行文件大小的工具:LinkMap。

本期话题

@zhangferry:本期讲下高效记忆这个话题,多数内容来源于《暗时间》。关于知识书中有句话是这样说的:

你所拥有的知识并不取决于你记得多少,而在于它们能否在恰当的时候被回忆起来。

这让我想起爱因斯坦的一句话:

教育就是忘记了在学校所学的一切之后剩下的东西。

两种说法很相似,都在强调为我所用才是知识的真正价值。而为我所用的前提就是记忆,记住了,才有可能在适当的时候被唤醒,记忆与学习也总是相辅相成的。关于记忆有一个被广泛认可的机制:我们在记忆的时候会将很多线索(例如当时的场景、语言环境等)一并编码进行记忆,事后能否快速提取出来主要就取决于这些线索有多丰富。

针对这一机制有以下方法可用于加深记忆并辅助学习:

  • 过段时间尝试再回忆。它的作用一方面是转换为长时记忆,还有一方面可以通过当前掌握的知识体系重新整合原有知识,这样有时还可以得到新的启发。
  • 用自己的语言表述,书写下来,甚至讲给他人听。这个就是费曼学习法了,它的作用是确保不是我以为我理解了,而是我用自己的方式理解了。
  • 气味,背景音乐,天气等这些外界因素,都可以作为线索进行编码记忆。有时我们偶然听一段以前的音乐,就能一下子回忆起当时的场景和感受,感觉尘封记忆被打开,DNA 动了一样,这些都是由于一个线索串连起来一系列回忆引起的。
  • 对于经验知识的学习,光听别人说或者看着别人做还不够,我们可以努力设想自己处于别人的境地,感受它们,将它们和你的情绪记忆挂钩。
  • 如果一件事情就是一件事情,那我们永远也无法学到“未来”的知识,所以我们还要剥去无关紧要的细节,抽象出那个关键点,这样才能进行知识的迁移与推广。

开发Tips

UICollectionView 的 scrollDirection 对 minimumLineSpacing 和 minimumInteritemSpacing 影响

整理编辑:人魔七七

minimumLineSpacingminimumInteritemSpacing 这两个值表示含义是受滚动方向影响的,不同滚动方向,行列的排列方式不同,我们仅需记住行间距为 lineSpace 即可。下图为可视化的描述:

本地化时一些需要注意的日期设置

整理编辑:夏天

不同地域会有不同的日期格式,一般而言,我们都默认使用 [NSLocale defaultLocale] 来获取存储在设备设置中 Regional Settings 的地域,而不是指定某个地域,该行为不需要显示设置。

默认的语言/区域设置会导致 NSCalendarNSDateFormatter 等跟区域关联的类上存在不同的展示

Calendar 的 firstWeekday

The firstWeekday property tells you what day of the week the week starts in your locale. In the US, which is the default locale, a week starts on Sun.

当我们使用 CalendarfirstWeekday 属性时,需要注意,这个世界上不是所有地域其 firstWeekday 值都是 1。比如,对莫斯科来说,其 firstWeekday 的值是 2

如果你的日历控件并没有考虑到这些,对于某一天具体排列在一周的哪天来说,其值是不同的。

笔者之前所做的日历头部是按照周一至周日固定展示的,然后用户在俄罗斯发现日期乱了,日期与周几错乱。

后续直接定死了firstWeekday = 1 来功能上解决了这个问题。

DateFormatter

目前部分地域(部分欧美国家)存在夏令时,其会在接近春季开始的时候,将时间调快一小时,并在秋季调回正常时间。

虽然目前现有的设备支持特定的夏令时的展示,但是存在某些历史原因,如俄罗斯:

1
2
3
4
5
6
7
let dFmt = DateFormatter()
dFmt.dateFormat = "yyyy-MM-dd"
dFmt.timeZone = TimeZone(identifier:"Europe/Moscow")
print(dFmt.date(from:"1981-04-01") as Any) // nil
print(dFmt.date(from:"1982-04-01") as Any) // nil
print(dFmt.date(from:"1983-04-01") as Any) // nil
print(dFmt.date(from:"1984-04-01") as Any) // nil

对于 1981 年 - 1984 年 4 个年度的俄罗斯来说,4 月 1 号当天没有零点,会导致转化出的 Date 为 nil。如果我们需要做类似转换,就需注意该特殊情况。

面试解析

整理编辑:反向抽烟师大小海腾

面试解析是新出的模块,我们会按照主题讲解一些高频面试题,本期主题是属性及属性关键字

谈属性及属性关键字

@property、@synthesize 和 @dynamic

@property

属性用于封装对象中数据,属性的本质是 ivar + setter + getter。

可以用 @property 语法来声明属性。@property 会帮我们自动生成属性的 setter 和 getter 方法的声明。

@synthesize

帮我们自动生成 setter 和 getter 方法的实现以及 _ivar。

你还可以通过 @synthesize 来指定实例变量名字,如果你不喜欢默认的以下划线开头来命名实例变量的话。但最好还是用默认的,否则影响可读性。

如果不想令编译器合成存取方法,则可以自己实现。如果你只实现了其中一个存取方法 setter or getter,那么另一个还是会由编译器来合成。但是需要注意的是,如果你实现了属性所需的全部方法(如果属性是 readwrite 则需实现 setter and getter,如果是 readonly 则只需实现 getter 方法),那么编译器就不会自动进行 @synthesize,这时候就不会生成该属性的实例变量,需要根据实际情况自己手动 @synthesize 一下。

1
@synthesize ivar = _ivar;
@dynamic

告诉编译器不用自动进行 @synthesize,你会在运行时再提供这些方法的实现,无需产生警告,但是它不会影响 @property 生成的 setter 和 getter 方法的声明。@dynamic 是 OC 为动态运行时语言的体现。动态运行时语言与编译时语言的区别:动态运行时语言将函数决议推迟到运行时,编译时语言在编译器进行函数决议。

1
@dynamic ivar;

以前我们需要手动对每个 @property 添加 @synthesize,而在 iOS 6 之后 LLVM 编译器引入了 property autosynthesis,即属性自动合成。换句话说,就是编译器会自动为每个 @property 添加 @synthesize。

那你可能就会问了,@synthesize 现在有什么用呢?

  1. 如果我们同时重写了 setter 和 getter 方法,则编译器就不会自动为这个 @property 添加 @synthesize,这时候就不存在 _ivar,所以我们需要手动添加 @synthesize。
  2. 如果该属性是 readonly,那么只要你重写了 getter 方法,property autosynthesis 就不会执行,同样的你需要手动添加 @synthesize 如果你需要的话,看你这个属性是要定义为存储属性还是计算属性吧。
  3. 实现协议中要求的属性。

此外需要注意的是,分类当中添加的属性,也不会 property autosynthesis 哦。因为类的内存布局在编译的时候会确定,但是分类是在运行时才加载并将数据合并到宿主类中的,所以分类当中不能添加成员变量,只能通过关联对象间接实现分类有成员变量的效果。如果你给分类添加了一个属性,但没有手动给它实现 getter、setter(如果属性是 readonly 则不需要实现)的话,编译器就会给你警告啦 Property 'ivar' requires method 'ivar'、'setIvar:' to be defined - use @dynamic or provide a method implementation in this category,编译器已经告诉我们了有两种解决方式来消除警告:

  1. 在这个分类当中提供该属性 getter、setter 方法的实现
  2. 使用 @dynamic 告诉编译器 getter、setter 方法的实现在运行时自然会有,您就不用操心了。当然在这里 @dynamic 只是消除了警告而已,如果你没有在运行时动态添加方法实现的话,那么调用该属性的存取方法还是会 Crash。

属性修饰符分类

分类 属性关键字
原子性 atomicnonatomic
读写权限 readwritereadonly
方法名 settergetter
内存管理 assignweakunsafe_unretainedretainstrongcopy
可空性 (nullable_Nullable__nullable)、
(nonnull_Nonnull__nonnull)、
(null_unspecified_Null_unspecified__null_unspecified)、
null_resettable
类属性 class
原子性
属性关键字 用法
atomic 原子性(默认),编译器会自动生成互斥锁(以前是自旋锁,后面改为了互斥锁),对 setter 和 getter 方法进行加锁,可以保证属性的赋值和取值的原子性操作是线程安全的,但不包括操作和访问。
比如说 atomic 修饰的是一个数组的话,那么我们对数组进行赋值和取值是可以保证线程安全的。但是如果我们对数组进行操作,比如说给数组添加对象或者移除对象,是不在 atomic 的负责范围之内的,所以给被 atomic 修饰的数组添加对象或者移除对象是没办法保证线程安全的。
nonatomic 非原子性,一般属性都用 nonatomic 进行修饰,因为 atomic 耗时。
读写权限
属性关键字 用法
readwrite 可读可写(默认),同时生成 setter 方法和 getter 方法的声明和实现。
readonly 只读,只生成 getter 方法的声明和实现。为了达到封装的目的,我们应该只在确有必要时才将属性对外暴露,并且尽量把对外暴露的属性设为 readonly。如果这时候想在对象内部通过 setter 修改属性,可以在类扩展中将属性重新声明为 readwrite;如果仅在对象内部通过 _ivar 修改,则不需要重新声明为 readwrite。
方法名
属性关键字 用法
setter 可以指定生成的 setter 方法名,如 setter = setName。这个关键字笔者在给分类添加属性的时候会用得比较多,为了避免分类方法“覆盖”同名的宿主类(或者其它分类)方法的问题,一般我们都会加前缀,比如 bbIvar,但是这样生成的 setter 方法名就不美观了(为 setBbIvar),于是就使用到了 setter 关键字 @property (nonatomic, strong, setter = bb_setIvar:) NSObject *bbIvar;
getter 可以指定生成的 getter 方法名,如 getter = getName。使用示例:@property (nonatomic, assign, getter = isEnabled) BOOL enabled;
内存管理
属性关键字 用法
assign 1. 既可以修饰基本数据类型,也可以修饰对象类型;
2. setter 方法的实现是直接赋值,一般用于基本数据类型 ;
3. 修饰基本数据类型,如 NSInteger、BOOL、int、float 等;
4. 修饰对象类型时,不增加其引用计数;
5. 会产生悬垂指针(悬垂指针:assign 修饰的对象在被释放之后,指针仍然指向原对象地址,该指针变为悬垂指针。这时候如果继续通过该指针访问原对象的话,就可能导致程序崩溃)。
weak 1. 只能修饰对象类型;
2. ARC 下才能使用;
3. 修饰弱引用,不增加对象引用计数,主要可以用于避免循环引用;
4. weak 修饰的对象在被释放之后,会自动将指针置为 nil,不会产生悬垂指针;
5. 对于视图,通常还是用在 xib 和 storyboard 上;代码中对于有必要进行 remove 的视图也可以使用 weak,这样 remove 之后会自动置为 nil。
unsafe_unretained 1. 既可以修饰基本数据类型,也可以修饰对象类型;
2. MRC 下经常使用,ARC 下基本不用;
3. 同 weak,区别就在于 unsafe_unretained 会产生悬垂指针;
4. weak 对性能会有一定的消耗,当一个对象 dealloc 时,需要遍历对象的 weak 表,把表里的所有 weak 指针变量值置为 nil,指向对象的 weak 指针越多,性能消耗就越多。所以 unsafe_unretained 比 weak 快。当明确知道对象的生命周期时,选择 unsafe_unretained 会有一些性能提升。比如 A 持有 B 对象,当 A 销毁时 B 也销毁。这样当 B 存在,A 就一定会存在。而 B 又要调用 A 的接口时,B 就可以存储 A 的 unsafe_unretained 指针。虽然这种性能上的提升是很微小的。但当你很清楚这种情况下,unsafe_unretained 也是安全的,自然可以快一点就是一点。而当情况不确定的时候,应该优先选用 weak。
retain 1. MRC 下使用,ARC 下基本使用 strong;
2. 修饰强引用,将指针原来指向的旧对象释放掉,然后指向新对象,同时将新对象的引用计数加 1;
3. setter 方法的实现是 release 旧值,retain 新值,用于 OC 对象类型。
strong 1. ARC 下才能使用;
2. 原理同 retain;
3. 但是在修饰 block 时,strong 相当于 copy,而 retain 相当于 assign。
copy setter 方法的实现是 release 旧值,copy 新值,一般用于 block、NSString、NSArray、NSDictionary 等类型。使用 copy 和 strong 修饰 block 其实都一样,用 copy 是为了和 MRC 下保持一致的写法;用于 NSString、NSArray、NSDictionary 是为了保证赋值后是一个不可变对象,以免遭外部修改而导致不可预期的结果。
可空性

Nullability and Objective-C

苹果在 Xcode 6.3 引入的一个 Objective-C 的新特性 nullability annotations。这些关键字可以用于属性、方法返回值和参数中,来指定对象的可空性,这样编写代码的时候就会智能提示。在 Swift 中可以使用 ?! 来表示一个对象是 optional 的还是 non-optional,如 UIView?UIView!。而在 Objective-C 中则没有这一区分,UIView 即可表示这个对象是 optional,也可表示是 non-optioanl。这样就会造成一个问题:在 Swift 与 Objective-C 混编时,Swift 编译器并不知道一个 Objective-C 对象到底是 optional 还是 non-optional,因此这种情况下编译器会隐式地将 Objective-C 的对象当成是 non-optional。引入 nullability annotations 一方面为了让 iOS 程序员平滑地从 Objective-C 过渡到 Swift,另一方面也促使开发者在编写 Objective-C 代码时更加规范,减少同事之间的沟通成本。

关键字 __nullable__nonnull 是苹果在 Xcode 6.3 中发行的。由于与第三方库的潜在冲突,苹果在 Xcode 7 中将它们更改为 _Nullable_Nonnull。但是,为了与 Xcode 6.3 兼容,苹果预定义了宏 __nullable__nonnull 来扩展为新名称。同时苹果同样还支持没有下划线的写法 nullablenonnull,它们的区别在于放置位置不同。

注意:此类关键词仅仅提供警告,并不会报编译错误。只能用于声明对象类型,不能声明基本数据类型。

属性关键字 用法
nullable、_Nullable 、__nullable 对象可以为空,区别在于放置位置不同
nonnull、_Nonnull、__nonnull 对象不能为空,区别在于放置位置不同
null_unspecified、_Null_unspecified 、__null_unspecified 未指定是否可为空,区别在于放置位置不同
null_resettable 1. getter 方法不能返回为空,setter 方法可以为空;
2. 必须重写 setter 或 getter 方法做非空处理。否则会报警告 Synthesized setter 'setName:' for null_resettable property 'name' does not handle nil
使用效果
1
2
3
4
5
6
7
8
9
10
@interface AAPLList : NSObject <NSCoding, NSCopying>
// ...
- (AAPLListItem * _Nullable)itemWithName:(NSString * _Nonnull)name;
@property (copy, readonly) NSArray * _Nonnull allItems;
// ...
@end

// --------------

[self.list itemWithName:nil]; // warning!
Audited Regions:Nonnull 区域设置

如果每个属性或每个方法都去指定 nonnullnullable,将是一件非常繁琐的事。苹果为了减轻我们的工作量,专门提供了两个宏: NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END。在这两个宏之间的代码,所有简单指针类型都被假定为 nonnull,因此我们只需要去指定那些 nullable 指针类型即可。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NS_ASSUME_NONNULL_BEGIN
@interface AAPLList : NSObject <NSCoding, NSCopying>
// ...
- (nullable AAPLListItem *)itemWithName:(NSString *)name;
- (NSInteger)indexOfItem:(AAPLListItem *)item;

@property (copy, nullable) NSString *name;
@property (copy, readonly) NSArray *allItems;
// ...
@end
NS_ASSUME_NONNULL_END

// --------------

self.list.name = nil; // okay

AAPLListItem *matchingItem = [self.list itemWithName:nil]; // warning!
笔者的一些经验总结
  • 使用好可空性关键字可以让 Objective-C 开发者平滑地过渡到 Swift,而不会被 Swift 可选类型绊倒。
  • 使用好可空性关键字可以让代码更加规范,比如你不应该将一个指定为 nonnull 的属性赋值为 nil。
  • NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END 只是苹果为了减轻我们的工作量而提供的宏,而不是允许我们忽略可空性关键字。
  • 如果你没有指定属性/方法参数为 nullable 的话,当给该属性赋值/传参 nil 的时候,会得到烦人的警告。
  • 进行混编的时候,如果你没有给一个可为空的属性指定 nullable,就无法进行可选链式调用,因为 Swift 会把它当作非可选类型来处理,而且你还不能强制解包,因为它可能为 nil,这时候你就得加一层保护。
类属性 class

属性可以分为实例属性和类属性:

  • 实例属性:每个实例都有一套属于自己的属性值,它们之前是相互独立的;
  • 类属性:可以为类本身定义属性,无论创建了多少个该类型的实例,这些属性都只有唯一一份,因为类是单例。

说白了就是实例属性与 instance 关联,类属性与 class 关联。

用处:类属性用于定义某个类型所有实例共享的数据,比如所有实例都能用的一个常量/变量(就像 C 语言中的静态常量/静态变量)。

通过给属性添加 class 关键字来定义类属性

1
@property (class, nonatimoc, strong) NSObject *object;

类属性是不会进行 property autosynthesis 的,那怎么关联值呢?

  • 如果是存储属性
    1. 在 .m 中定义一个 static 全局变量,然后在 setter 和 getter 方法中对此变量进行操作。
    2. 在 setter 和 getter 方法中使用关联对象来存储值。笔者之前遇到的一个使用场景就是,类是通过 Runtime 动态创建的,这样就没办法使用 static 全局变量存储值。于是笔者在父类中定义了一个类属性并使用关联对象来存储值,这样动态创建的子类就可以给它的类属性关联值了。
  • 如果是计算属性,就直接实现 setter 和 getter 方法就好。

其它补充

在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义:

1
2
3
4
5
6
7
8
@property (nonatomic, copy) NSString *name;

— (instancetype)initWithName:(NSString *)name {
if (self = [super init]) {
_name = [name copy];
}
return self;
}

若是自己来实现存取方法,也应该保证其具备相关属性所声明的性质。

参考:iOS - 再谈 OC 属性及属性关键字

优秀博客

整理编辑:皮拉夫大王在此我是熊大

本期主题:卡顿优化

1、iOS卡顿监测方案总结

文章总结了业界的很多卡顿监控技术。包括:FPS、runloop、子线程 Ping、CPU 占用率监测。文章中附带了作者参考和收集到的原文链接,以及部分相关上下游技术的文章。如果您想要做卡顿监控,阅读本文可以节省不少时间和精力。

2、iOS 渲染原理解析

文章细致的介绍了图像渲染的流程。包括一些细小有趣的知识点,比如 CALayer 的 contents 保存了 bitmap 信息等。文中当然少不了对离屏渲染的介绍,包括离屏渲染的场景、离屏渲染的原因以及如何避免离屏渲染。文后附有小题目,可以让大家带着问题回顾文章,加深对知识的理解。

3、UIView 动画降帧探究

本文首先介绍为了降帧的目的:降低 GPU 的使用率,并介绍了为什么动画渲染对 GPU 有较大的影响。正文中主要介绍了降帧的方案:UIView animation 指定 UIViewAnimationOptionPreferredFramesPerSecond30 进行降帧、CADisplayLink 逐帧动画降帧。

4、天罗地网? iOS卡顿监控实战 – 来自掘金:进击的蜗牛君

本文利用 ping 方案,即每隔一段时间就去目标线程中检测状态,如果目标线程”运行良好”,则标记为正常,当一段时间 ping 均不正常时,上报目标线程的堆栈,此时认为目标线程发生了卡顿,作者已经做出了开源工具,方便大家深入研究。

5、列表流畅度优化 – 来自掘金:Hello_Vincent

作者借鉴了 WWDC18 的相关 session,从实际角度出发,进行一次列表优化的旅程,从原因到解决办法,最后提出意见,称得上是一篇佳作。

6、WWDC2016 Session笔记 - iOS 10 UICollectionView新特性 – 来自掘金:一缕殇流化隐半边冰霜

早在 WWDC16,官方针对 UICollectionView 已经做过优化教程,如果你还不知道,可以看一看这篇文章。

学习资料

整理编辑:Mimosa

Combine Operators

地址:https://apps.apple.com/app/combine-operators/id1507756027

一个用来学习 Combine 的 App,他将一些 Combine 中的各种操作符用可视化的手段表达了出来,还附加了蠢萌蠢萌的动画效果,很适合刚接触 Combine 的朋友尝试一下。

Stanford CS193P 2021 SwiftUI 2.0 双语字幕

地址:https://www.bilibili.com/video/BV1q64y1d7x5

Stanford CS193P 2021 SwiftUI 2.0 课程,该课程的老师是 Paul Hegarty,在 Stanford 执教 10 年左右了。该课程创办了很多年,每当 Apple 推出了新技术,例如 Storyboard、SwiftUI,这个白胡子老爷爷就会迅速跟上,更新他的课程,实乃一 it 潮人。你可以去油管 Stanford 官方账号查看该课程,也可以看看 up 主转载的该课程,还上传了中文字幕、英文字幕、繁体字幕的双语版本。理论上来说,你只需要有面向对象编程及 Swift 语言的相关基础和了解,你就可以看懂该课程,适合想要学习 SwiftUI 入门的朋友。

工具推荐

整理编辑:brave723

地址https://github.com/huanxsd/LinkMap

软件状态: 免费

软件介绍

iOS 包的大小,是每个开发必须关注的问题,对于大型项目来说,只是代码段就有可能超过 100M,算上 armv7 和 arm64 架构,会超过 200M。 LinkMap 工具通过分析项目的 LinkMap 文件,能够计算出各个类、各个三方库占用的空间大小(代码段+数据段),方便开发者快速定位需要优化的文件。

关于我们

iOS 摸鱼周报,主要分享开发过程中遇到的经验教训、优质的博客、高质量的学习资料、实用的开发工具等。周报仓库在这里:https://github.com/zhangferry/iOSWeeklyLearning ,如果你有好的的内容推荐可以通过 issue 的方式进行提交。另外也可以申请成为我们的常驻编辑,一起维护这份周报。另可关注公众号:iOS成长之路,后台点击进群交流,联系我们,获取更多内容。

往期推荐

iOS摸鱼周报 第十八期

iOS摸鱼周报 第十七期

iOS摸鱼周报 第十六期

iOS摸鱼周报 第十五期

iOS摸鱼周报 第十八期

iOS摸鱼周报 第十八期

本期概要

  • 本期话题:什么是暗时间。
  • Tips 带来了多个内容:Fastlane 用法总结、minimumLineSpacing 与 minimumInteritemSpacing 的区别以及一个定位 RN 发热问题的过程。
  • 面试解析:本期围绕 block 的变量捕获机制展开说明。
  • 优秀博客带来了几篇编译优化的文章。
  • 学习资料带来了一个从 0 设计计算机的视频教程,还有 Git 和正则表达式的文字教程。
  • 开发工具介绍了两个代码片段整理的相关工具。

本期话题

@zhangferry:最近在看一本书:《暗时间》,初听书名可能有些不知所云,因为这个词是作者发明的,我们来看文中对“暗时间”的解释:

看书并记住书中的东西只是记忆,并没有涉及推理,只有靠推理才能深入理解一个事物,看到别人看不到的地方,这部分推理的过程就是你的思维时间,也是人一生中占据一个显著比例的“暗时间”。你走路、买菜、洗脸洗手、坐公交、逛街、出游、吃饭、睡觉,所有这些时间都可以成为暗时间,你可以充分利用这些时间进行思考,反刍和消化平时看和读的东西,这些时间看起来微不足道,但日积月累会产生巨大的效应。

这里对于暗时间的解释是思维时间,因为思维是人的”后台线程“,我们通常注意不到它,可它却实际存在且非常重要。但按思维时间来说其适用的范围就有点窄了,大多数情况我们并不会一直保持思考。我尝试把刘未鹏关于暗时间的概念进行扩展,除思维时间外,还包括那些零碎的,可以被利用但未被利用起来的时间。“明时间”,暗时间倘若都能利用起来,那定是极佳的。

目前我有两个关于暗时间应用的实践:

1、在上下班走路过程中是思考时间。我现在换了一条上下班路线,使得步行时间更长,一趟在 15 分钟左右。这段时间,我会尝试想下今天的工作内容,规划日常任务;或者回忆最近在看的某篇文章,脑海里进行推演然后尝试复述其过程;或者仅仅观察路过的行人,想象下如果我是他们,我在另一个视角观察到的自己是什么样子。总之,让大脑活跃起来。

2、等待的过程是运动时间。等人或者等红绿灯的时候,我会尝试让自己运动起来,比如小动作像垫垫脚,大一点的动作像跳一跳、跑一跑。运动是一项反人性的事情,所以它不能规划,一规划就要跟懒惰做斗争,所以干脆就随时有空就动两下。通常这种小型的运动体验,如果突然因为要开始干正事被打断了,还会有种意犹未尽的感觉。

当然还可以有别的尝试,重要的是我们要明白和感受到暗时间这个东西,然后再想办法怎么利用它。至少在我的一些尝试中会让一些本该枯燥的时间变得更有趣了些。

开发Tips

整理编辑:zhangferry

Fastlane 用法总结

图片来源:iOS-Tips

React Native 0.59.9 引发手机发烫问题解决思路

内容贡献:yyhinbeijing

问题出现的现象是:RN 页面放置久了,或者反复操作不同的 RN 页面,手机会变得很烫,并且不会自动降温,要杀掉进程才会降温,版本是 0.59.9,几乎不同手机不同手机系统版本均遇到了这个问题,可以确定是 RN 导致的,但具体哪里导致的呢,以下是通过代码注释定位问题的步骤,后面数值为 CPU 占用率:

1、原生:7.2%

2、无网络无 Flatlist:7.2%

3、网络 + FlatList :100%+

4、网络 + 无 FlatList:100%+

5、去掉 loading:2.6% — 30%,会降低

6、网络和 FlatList 全部放开,只关闭 loading 最低 7.2%,能降低,最高 63%

首先是发现网络导致 CPU 占用率很高,然后网络注释掉 RNLoading (我们自写的 loading 动画),发现内存占用不高了。就断定是 RNLoading 问题,查询发现:我们每次点击 tab 都会加载 loading,而 loading 又是动画,这样大量的动画引发内存问题。虽不是特例问题,但发现、定位、解决问题的过程仍然是有借鉴意义的,即确定范围,然后不断缩小范围。

面试解析

整理编辑:反向抽烟师大小海腾

面试解析会按照主题讲解一些高频面试题,本期面试题是 block 的变量捕获机制

block 的变量捕获机制

block 的变量捕获机制,是为了保证 block 内部能够正常访问外部的变量。

1、对于全局变量,不会捕获到 block 内部,访问方式为直接访问;作用域的原因,全局变量哪里都可以直接访问,所以不用捕获。

2、对于局部变量,外部不能直接访问,所以需要捕获。

  • auto 类型的局部变量(我们定义出来的变量,默认都是 auto 类型,只是省略了),block 内部会自动生成一个同类型成员变量,用来存储这个变量的值,访问方式为值传递auto 类型的局部变量可能会销毁,其内存会消失,block 将来执行代码的时候不可能再去访问那块内存,所以捕获其值。由于是值传递,我们修改 block 外部被捕获变量的值,不会影响到 block 内部捕获的变量值。
  • static 类型的局部变量,block 内部会自动生成一个同类型成员变量,用来存储这个变量的地址,访问方式为指针传递。static 变量会一直保存在内存中, 所以捕获其地址即可。相反,由于是指针传递,我们修改 block 外部被捕获变量的值,会影响到 block 内部捕获的变量值。
  • 对于对象类型的局部变量,block 会连同它的所有权修饰符一起捕获。
    • 如果 block 是在栈上,将不会对对象产生强引用
    • 如果 block 被拷贝到堆上,将会调用 block 内部的 copy(__funcName_block_copy_num)函数,copy 函数内部又会调用 assign(_Block_object_assign)函数,assign 函数将会根据变量的所有权修饰符做出相应的操作,形成强引用(retain)或者弱引用。
    • 如果 block 从堆上移除,也就是被释放的时候,会调用 block 内部的 dispose(_Block_object_dispose)函数,dispose 函数会自动释放引用的变量(release)。
  • 对于 __block(可用于解决 block 内部无法修改 auto 变量值的问题) 修饰的变量,编译器会将 __block 变量包装成一个 __Block_byref_varName_num 对象。它的内存管理几乎等同于访问对象类型的 auto 变量,但还是有差异。
    • 如果 block 是在栈上,将不会对 __block 变量产生强引用
    • 如果 block 被拷贝到堆上,将会调用 block 内部的 copy
      函数,copy 函数内部又会调用 assign 函数,assign 函数将会直接对 __block 变量形成强引用(retain)。
    • 如果 block 从堆上移除,也就是被释放的时候,会调用 block 内部的 dispose 函数,dispose 函数会自动释放引用的 __block 变量(release)。
  • __block修饰的对象类型的内存管理:
    • 如果 __block 变量是在栈上,将不会对指向的对象产生强引用
    • 如果 __block 变量被拷贝到堆上,将会调用 __block 变量内部的 copy(__Block_byref_id_object_copy)函数,copy 函数内部会调用 assign 函数,assign 函数又会根据变量的所有权修饰符做出相应的操作,形成强引用(retain)或者弱引用。(注意:这里仅限于 ARC 下会 retain,MRC 下不会 retain,所以在 MRC 下还可以通过 __block 解决循环引用的问题)
    • 如果 __block 变量从堆上移除,会调用 __block 变量内部的 dispose 函数,dispose 函数会自动释放指向的对象(release)。

掌握了 block 的变量捕获机制,我们就能更好的应对内存管理,避免因使用不当造成内存泄漏。

常见的 block 循环引用为:self(obj) -> block -> self(obj)。这里 block 强引用了 self 是因为对于对象类型的局部变量,block 会连同它的所有权修饰符一起捕获,而对象的默认所有权修饰符为 __strong。

1
2
3
self.block = ^{
NSLog(@"%@", self);
};

为什么这里说 self 是局部变量?因为 self 是 OC 方法的一个隐式参数。

为了避免循环引用,我们可以使用 __weak 解决,这里 block 将不再持有 self。

1
2
3
4
__weak typeof(self) weakSelf = self;
self.block = ^{
NSLog(@"%@", weakSelf);
};

为了避免在 block 调用过程中 self 提前释放,我们可以使用 __strong 在 block 执行过程中持有 self,这就是所谓的 Weak-Strong-Dance。

1
2
3
4
5
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(self) strongSelf = weakSelf;
NSLog(@"%@", strongSelf);
};

当然,我们平常用的比较多的还是 @weakify(self)@strongify(self) 啦。

1
2
3
4
5
@weakify(self);
self.block = ^{
@strongify(self);
NSLog(@"%@", self);
};

如果你使用的是 RAC 的 Weak-Strong-Dance,你还可以这样:

1
2
3
4
5
@weakify(self, obj1, obj2);
self.block = ^{
@strongify(self, obj1, obj2);
NSLog(@"%@", self);
};

如果是嵌套的 block:

1
2
3
4
5
6
7
8
@weakify(self);
self.block = ^{
@strongify(self);
self.block2 = ^{
@strongify(self);
NSLog(@"%@", self);
}
};

你是否会疑问,为什么内部不需要再写 @weakify(self) ?这个问题就留给你自己去思考和解决吧!

相比于简单的相互循环引用,block 造成的大环引用更需要你足够细心以及敏锐的洞察力,比如:

1
2
3
4
5
6
TYAlertView *alertView = [TYAlertView alertViewWithTitle:@"TYAlertView" message:@"This is a message, the alert view containt text and textfiled. "];
[alertView addAction:[TYAlertAction actionWithTitle:@"取消" style:TYAlertActionStyleCancle handler:^(TYAlertAction *action) {
NSLog(@"%@-%@", self, alertView);
}]];
self.alertController = [TYAlertController alertControllerWithAlertView:alertView preferredStyle:TYAlertControllerStyleAlert];
[self presentViewController:alertController animated:YES completion:nil];

这里循环引用有两处:

  1. self -> alertController -> alertView -> handlerBlock -> self
  2. alertView -> handlerBlock -> alertView

避免循环引用:

1
2
3
4
5
6
7
8
TYAlertView *alertView = [TYAlertView alertViewWithTitle:@"TYAlertView" message:@"This is a message, the alert view containt text and textfiled. "];
@weakify(self, alertView);
[alertView addAction:[TYAlertAction actionWithTitle:@"取消" style:TYAlertActionStyleCancle handler:^(TYAlertAction *action) {
@strongify(self, alertView);
NSLog(@"%@-%@", self, alertView);
}]];
self.alertController = [TYAlertController alertControllerWithAlertView:alertView preferredStyle:TYAlertControllerStyleAlert];
[self presentViewController:alertController animated:YES completion:nil];

另外再和你提一个小知识点,当我们在 block 内部直接使用 _variable 时,编译器会给我们警告:Block implicitly retains self; explicitly mention 'self' to indicate this is intended behavior

原因是 block 中直接使用 _variable 会导致 block 隐式的强引用 self。Xcode 认为这可能会隐式的导致循环引用,从而给开发者带来困扰,而且如果不仔细看的话真的不太好排查,笔者之前就因为这个循环引用找了半天,还拉上了我导师一起查找原因。所以警告我们要显式的在 block 中使用 self,以达到 block 显式 retain 住 self 的目的。改用 self->_variable 或者 self.variable

你可能会觉得这种困扰没什么,如果你使用 @weakify@strongify 那确实不会造成循环引用,因为 @strongify 声明的变量名就是 self。那如果你使用 weak typeof(self) weak_self = self;strong typeof(weak_self) strong_self = weak_self 呢?

优秀博客

整理编辑:皮拉夫大王在此我是熊大

本期主题:编译优化

1、iOS编译过程的原理和应用 – 来自 CSDN:黄文臣

做编译优化前,先了解下编译原理吧!该作者通过 iOS 的视角,白话了编译原理,通俗易懂。

2、Xcode编译疾如风系列 - 分析编译耗时 – 来自腾讯社区:小菜与老鸟

在进行编译速度优化前,一个合适的分析工具是必要的,它能告诉你哪部分编译时间较长,让你发现问题,从而解决问题,本文介绍了几种分析编译耗时的方式,助你分析构建时间。该作者还有其他相关姊妹篇,建议前往阅读。

3、iOS 微信编译速度优化分享 – 来自云+社区:微信终端开发团队

文章对编译优化由浅入深做了介绍。作者首先介绍了常见的现有方案,利用现有方案以及精简代码、将模板基类改为虚基类、使用 PCH 等方案做了部分优化。文章精彩的部分在于作者并没有止步于此,而是从编译原理入手,结合量化手段,分析出编译耗时的瓶颈。在找到问题的瓶颈后,作者尝试人工进行优化,但是效率较低。最终在 IWYU 基础上,增加了 ObjC 语言的支持,高效地处理了一部分多余的头文件。

4、iOS编译速度如何稳定提高10倍以上之一 – 来自掘金:Mr_Coder

美柚 iOS 的编译提效历程。作者对常见的优化做了分析,列举了各自的优缺点。有想做编译优化的可以参考这篇文章了解一下。对于业界的主流技术方案,别的技术文章往往只介绍优点,对方案的缺点谈的不够彻底。这篇文章从实践者的角度阐述了常见方案的优缺点,很有参考价值。文章介绍了双私有源二进制组件并与 ccache 做了对比,最后列出了方案支持的功能点。

5、iOS编译速度如何稳定提高10倍以上之二 – 来自掘金:Mr_Coder

作为上文的姊妹篇,本文详细介绍了双私有源二进制组件的方案细节以及使用方法。对该方案感兴趣的可以关注下。

6、一款可以让大型iOS工程编译速度提升50%的工具 – 来自美团技术团队:思琦 旭陶 霜叶

本文主要介绍了如何通过优化头文件搜索机制来实现编译提速,全源码编译效率提升 45%。文中涉及很多知识点,比如 hmap 文件的作用、Build Phases - Headers 中的 Public,Private,Project 各自是什么作用。文中详细分析了 podspec 创建头文件产物的逻辑以及 Use Header Map 失效的原因。干货比较多,可能得多读几遍。

学习资料

整理编辑:Mimosa

从 0 到 1 设计一台计算机

地址:https://www.bilibili.com/video/BV1wi4y157D3

来自 Ele实验室 的计算机组成原理课程,该系列视频主要目的是让大家对「计算机是如何工作的」有个较直观的认识,做为深入学习计算机科学的一个启蒙。观看该系列视频最好有一些数字电路和模拟电路的基础知识,Ele 实验室同时也有关于 数电模电 的基础知识介绍供大家参考。

Git Cheat Sheet 中文版

地址:https://github.com/flyhigher139/Git-Cheat-Sheet

Git Cheat Sheet 让你不用再去记所有的 git 命令!对新手友好,可以用于查阅简单的 git 命令。

正则表达式 30 分钟入门教程

地址:https://deerchao.cn/tutorials/regex/regex.htm

30 分钟内让你明白正则表达式是什么,并对它有一些基本的了解。别被那些复杂的表达式吓倒,只要跟着我一步一步来,你会发现正则表达式其实并没有想象中的那么困难。除了作为入门教程之外,本文还试图成为可以在日常工作中使用的正则表达式语法参考手册。

工具推荐

整理编辑:zhangferry

SnippetsLab

地址http://www.renfei.org/snippets-lab/

软件状态:$9.99

软件介绍

一款强大的代码片段管理工具,从此告别手动复制粘贴,SnippetsLab 的设计更符合 Apple 的交互习惯,支持导航栏快速操作。另外还可以同步 Github Gist 内容,使用 iCloud 备份。

CodeExpander

地址https://codeexpander.com/

软件状态:普通版免费,高级版付费

软件介绍

专为开发者开发的一个集输入增强、代码片段管理工具,支持跨平台,支持云同步(Github/码云)。免费版包含 90% 左右功能,相对 SnippetsLab 来说其适用范围更广泛,甚至包括一些日常文本的片段处理。

关于我们

iOS 摸鱼周报,主要分享开发过程中遇到的经验教训、优质的博客、高质量的学习资料、实用的开发工具等。周报仓库在这里:https://github.com/zhangferry/iOSWeeklyLearning ,如果你有好的的内容推荐可以通过 issue 的方式进行提交。另外也可以申请成为我们的常驻编辑,一起维护这份周报。另可关注公众号:iOS成长之路,后台点击进群交流,联系我们,获取更多内容。

往期内容

iOS摸鱼周报 第十七期

iOS摸鱼周报 第十六期

iOS摸鱼周报 第十五期

iOS摸鱼周报 第十四期

iOS摸鱼周报 第十七期

iOS摸鱼周报 第十七期

本期速览

  • 这期的 本期话题 从一个同学的问题入手,谈了些关于习惯的理解。
  • Tips 部分围绕一个问题展开:String.count 和 NSString.length 结果是一样的吗?他们之间有什么差别?后面的阅读可以帮你回答这个问题。
  • 面试专题带来网络部分的第二弹,关于 TCP 连接,关于三次握手,四次握手。
  • 博客部分整理了一些网络优化的文章,网络请求分为:请求前阶段,连接阶段,数据处理阶段,各个阶段都是可以进行优化的。
  • 如何用 Swift 实现常用的数据结构?来看 Swift Algorithm Club 吧。
  • 桌面版 Homebrew:Cakebrew,一个好用的剪切板工具:Paste - Clipboard Manager。

本期话题

@zhangferry:本期话题来源于一个同学的提问:普通人如何摆脱娱乐的诱惑,让自己可以每天,或者每周有固定的学习时间。

这个问题比较难回答,因为娱乐是人的本能,真正做到摆脱诱惑是非常难的。不光是学习,其实有很多事情我们都是知道应该要做什么的,但就是无法做到,那到底是为什么呢?《Synaptic Self》这本书从脑科学的角度解释了这个现象:

大脑中的新皮层(neocortex,所谓“理性”居住的地方,尤其是前额叶)在进化历史上是较为新近的年代才进化出来的,跟底层较原始的模块(如主管情绪的杏仁核)之间的神经网络沟通并不是合作无间,这就解释了为什么有些事情我们明明知道是对的,但就是不能说服自己,情绪还是在那里不依不挠的驱使你去做另一样事情。

显然来自大脑的本能我们无法克服,但我们可以换个角度考虑这个问题,不再是考虑如何摆脱诱惑,而是如何培养一个定期学习的习惯。它们能达到一样的效果,而且相对来说,习惯养成比对抗本能显得更容易一些。

关于习惯有很多书专门去讲了,像是《习惯的力量》、《微习惯》等,这里不展开太多,仅说下我个人认为有用的几个小技巧:

  • 拆分目标:即微习惯,把定时学习拆分成每天 5-10 分钟的固定学习,目标越小,我们在准备实施的时候遇到的阻力就会越小。比如我自己的学习习惯,每天在群里分享一点自己学习的知识,平均准备时长在 10 分钟左右,偶有断开连接,但毕竟负担不大,多数时间都在坚持输出,已经差不多有一年时间了。其实类似的还可以是每天弄懂一个开发概念,每天读两页书等。这一步是最关键,最重要的,因为大部分情况我们没能采取行动都是因为杏仁核的阻挠,微习惯的核心作用就是为了避开情绪的负担,先能够行动起来
  • 找到监督环境:即使习惯微小,要完全自觉可能还是有些困难的,可以附带施加一些外部监督力量。想象下在公司的一天和在家的一天,同样的时间,大部分人的工作效果都是在公司更好一些,因为公司相当于外部的监督力量,能提供更好的工作氛围。还是我那个例子,我是用交流群充当监督作用,因为知道有人在期待这个,所以我有不得不做的理由;类似的还有付费群,交钱入群,完成目标退钱,甚至还能挣钱;网络自习室,打开摄像头互相监督等。
  • 提供奖励:每完成一次目标都可以提供适当的奖励给自己,奖励的目的是让下次行动时能更容易一些,以此来形成一个良性循环。对我那个每日分享的微习惯来说,因为那些总结的内容,后期写文章都是有用到的,所以对我来说奖励就是提前启动准备工作了,后期写东西会更容易一些。当然大家可以根据自己的情况设置适当的奖励,比如增加一点娱乐的时间等。

这几点是按顺序来说的,也可以当做一个培养习惯的简单模型,实际场景大家可以根据自身情况适当调整。习惯的培养是一个持久战,不是坚持 21 天就算养成了,也不是中断了几次就是失败了,只管一点点持续下去,它总会反馈给我们好的结果。

如果对该话题有兴趣的小伙伴还可以评价区继续跟我们交流。

开发Tips

内容贡献:HansZhang,校验整理:夏天

关于 String.count 和 NSString.length 的探究

在开发过程中使用 Swift 的 String.count 创建 NSRange 时,发现在某些语言下(印度语言,韩语)对应位置的文字没有应用预期的显示效果。通过打印同一个字符串在 NSString 类型下的 length 和在 Swift 类型下的 count 发现二者的值并不相等,lengthcount 要大一些。也就是说,在创建 NSRange 时,Swift 的 String.count 并不可靠,我们可以使用 NSString.length 解决这个问题。

lengthcount 的不同

那么,为什么同一个字符串的 长度 在 String 与 NSString 中会得到不同的值呢?我们来看一下 String.countNSString.length 各自的官方定义:

  • String.count: The number of characters in a string.
  • NSString.length: The length property of an NSString returns the number of UTF-16 code units in an NSString

通过上述官方文字,我们隐约能察觉到一丝不同而继续发出疑问🤔️:

  • 这个 charactersUTF-16 code units 是一回事么?
  • 如果不是的话那各自的定义又是什么呢?

Swift doc 中对 Swift 中的 Character 有如下说明:

Every instance of Swift’s Character type represents a single extended grapheme cluster. An extended grapheme cluster is a sequence of one or more Unicode scalars that (when combined) produce a single human-readable character.

在 Swift 1.0 版本的 Swift String Design 中,也找到了相关描述:

Character, the element type of String, represents a grapheme cluster, as specified by a default or tailored Unicode segmentation algorithm. This term is precisely defined by the Unicode specification, but it roughly means what the user thinks of when she hears “character”. For example, the pair of code points “LATIN SMALL LETTER N, COMBINING TILDE” forms a single grapheme cluster, “ñ”.

所以我们可以粗略的理解为一个 Character 表示一个人类可读的字符,举个例子:

1
2
3
4
5
6
7
8
9
10
11
let eAcute: Character = "\u{E9}"                         // é
let combinedEAcute: Character = "\u{65}\u{301}" // e followed by ́
// eAcute is é, combinedEAcute is é

let eAcute: String = "\u{E9}"
let combinedEAcute: String = "\u{65}\u{301}"
// eAcute is é, combinedEAcute is é
print(eAcute.count) // 1
print(combinedEAcute.count) // 1
print((eAcute as NSString).length) // 1
print((combinedEAcute as NSString).length) // 2

é 在 unicode 中由一个标量(unicode scalar value)表示,也有由两个标量组合起来表示的,不论哪种在 Swift 的 String 中都表示为一个 Character。

那我们再返回来看 Swift String.count 的定义就好理解了,count 表示的是 Character 的数量,而 NSString 的 length 表示的是实际 unicode 标量(code point)的数量。所以在某些有很多组合标量字符的语言中(或者 emoji 表情)一个 Character 与一个 unicode 标量并不是一一对应的,也就造成了同一个字符 NSString.lengthString.count 值可能不相等的问题,其实这个问题在 Swift doc 中早有提示:

The count of the characters returned by the count property isn’t always the same as the length property of an NSString that contains the same characters. The length of an NSString is based on the number of 16-bit code units within the string’s UTF-16 representation and not the number of Unicode extended grapheme clusters within the string.

我们可以看到对于字符串 Character 这样 grapheme cluster 式的分割字符的方式,更符合我们人类看到文字时的预期的,可以很方便的遍历真实字符,且包容多种多样的语言。但在带来便利的同时也增加了实现上的复杂度。由于每个 Character 长度不尽相同,String.count 无法像 NSString.length 那样使用 O(1) 复杂度的情况简单计算固定长度的个数,而是需要遍历每一个字符,在确定每个 Character 的边界和长度后才能推算出总个数。所以当你使用 String.count 时,也许要注意一下这是一个 O(n) 的调用。

面试解析

整理编辑:反向抽烟师大小海腾

面试解析是新出的模块,我们会按照主题讲解一些高频面试题,本期主题是计算机网络,以下题目均来自真实面试场景。计算机网络是面试必考的知识点,最好比较系统的去学习了解,推荐书籍:《图解 TCP/IP》、《网络是怎样连接的》;推荐付费课程:计算机网络通关 29 讲,大家可以根据自己喜欢的学习方式进行选择。

什么是 TCP 的三次握手和四次挥手?

我们先来看一下 TCP 报文头部结构:

握手阶段主要依靠以下几个标志位:

  • SYN:在建立连接时使用,用来同步序号。SYN=1 代表这是一个请求建立连接或同意建立连接的报文,只有前两次握手中 SYN 才为 1,带 SYN 标志的 TCP 报文段称为同步报文段;
    • 当 SYN=1,ACK=0 时,表示这是一个请求建立连接的报文段
    • 当 SYN=1,ACK=1 时,表示对方同意建立连接
  • ACK:表示前面确认号字段是否有效。ACK=1 代表有效。带 ACK 标志的 TCP 报文段称为确认报文段;
  • FIN:表示通知对方本端数据已发送完毕,要关闭连接了。带 FIN 标志的 TCP 报文段称为终止报文段。

三次握手是指建立一个 TCP 连接时,需要客户端和服务端总共发送 3 个包,需要三次握手才能确认双方的接收与发送能力是否正常。

  1. 客户端向服务端发起连接请求,需要发送一个 SYN 报文到服务端。
  2. 当服务端收到客户端发过来的 SYN 报文后,返回给客户端 SYN、ACK 报文。这时候服务端可以确认客户端的发送能力和自己的接收能力正常
  3. 客户端收到该报文。这时候客户端可以确认双方的发送和接收能力都正常。然后客户端再回复 ACK 报文给服务端,服务端收到该报文。这时候服务端可以确认客户端的接收能力和自己的发送能力正常。所以这时候双方都可以确认自己和对方的接收与发送能力都正常。就这样客户端和服务端通过 TCP 建立了连接。

四次挥手的目的是关闭一个 TCP 连接。

  1. 客户端主动发起连接断开,发送一个 FIN 报文到服务端;
  2. 服务端返回给客户端 ACK 报文。此时服务端处于关闭等待状态,而不是立马给客户端发 FIN 报文,这个状态还要持续一段时间,因为服务端可能还有数据没发完。此时客户端到服务端的连接已经断开。但客户端和服务端之间所建立的 TCP 连接通道是全双工的,此时只是处于半关闭状态,所以服务端到客户端可能还会传递数据
  3. 当服务端的数据都发送完毕后,给客户端发送一个 FIN,ACK 报文;
  4. 客户端回应一个 ACK 报文。注意客户端发出 ACK 报文后不是立马释放 TCP 连接,而是要经过 2MSL(最长报文段寿命的 2 倍时长)后才释放 TCP 连接。而服务端一旦收到客户端发出的确认报文就会立马释放 TCP 连接,所以服务端结束 TCP 连接的时间要比客户端早一些。此时服务端到客户端的连接也已经断开,整个 TCP 连接关闭

为什么 TCP 连接是三次握手?两次不可以吗?

TCP 是一个全双工协议,它要保证双方都具有接收与发送的能力。

因为需要考虑连接时丢包的问题,如果只握手两次,第二次握手时如果服务端发给客户端的确认报文段丢失,此时服务端已经准备好了收发数据(可以理解为服务端已经连接成功),而客户端一直没收到服务端的确认报文,所以客户端就不知道服务端是否已经准备好了(可以理解为客户端未连接成功),这种情况下客户端不会给服务端发数据,也会忽略服务端发过来的数据。

如果是三次握手,即便发生丢包也不会有问题,比如如果第三次握手客户端发的确认报文丢失,服务端在一段时间内没有收到确认报文的话就会重新进行第二次握手,也就是服务端会重发 SYN 报文段,客户端收到重发的报文段后会再次给服务端发送确认报文。

为什么 TCP 连接是三次握手,关闭的时候却要四次挥手?

主要是建立连接时接收者的 SYN-ACK 一同发送了,而关闭是 FIN 和 ACK 却不能同时发送,因为断开连接要处理的情况比较多,比如服务器端可能还有发送出的消息没有得到 ACK,也可能服务器资源需要释放等。所以先发一个 ACK 表示已经收到了发送方的请求,等上述情况都有了确定的处理,再发 FIN 表示接收方已经完成了后续工作。

类比现实世界中,你收到了一个 Offer,出于礼貌你先回复一下,然后思考一段时间再回复 HR 最终的结果。

为什么客户端发出第四次挥手的确认报文后要等 2MSL 的时间才能释放 TCP 连接?

这里同样是要考虑丢包的问题,如果第四次挥手的报文丢失,服务端没收到确认报文就会重发第三次挥手的报文,这样报文一去一回最长时间就是 2MSL,所以需要等这么长时间来确认服务端确实已经收到了。

参考:https://zhuanlan.zhihu.com/p/141396896

优秀博客

整理编辑:皮拉夫大王在此我是熊大

本期主题:网络优化

网络优化大致可分为三个阶段:请求前阶段,连接阶段,数据处理阶段。

  • 请求前阶段:接口冷却,优先级调整、接口依赖、数据压缩、请求拦截
  • 连接阶段:IP 直连、HTTPDNS、重试、不同网络环境的超时处理
  • 数据处理阶段:数据解析、缓存

一、全面理解DNS及HTTPDNS – 来自掘金:iosmedia

你有没有遇到过,某些地区的连接成功率很低,有时连接成功,有时连接不成功呢?如果你遇到这种情况那可能是 DNS 解析出现了问题。本文全面解析了 DNS 是什么,为什么会被劫持,为什么 HTTPDNS 可以解决这种问题,如果你有类似困惑,建议阅读本文,相信一定能收获满满。

二、iOS IP 直连原理剖析 – 来自掘金:joy_xx

HTTPDNS 是自研还是使用第三方的?如果自研的话会不会成本比较高呢?IP 直连可能适合,遇到 DNS 问题,但又不希望花费大量时间精力的解决方案。其本质就是服务器有多个 IP,app 内置多个 IP,如果连接成功,每次启动就去请求更新新的 IP 列表。

三、网络请求优化之取消请求 – 来自掘金:阿南

本文介绍了,我们在开发中一定会遇到的场景:销毁页面时,取消网络请求;同一接口短时间请求多次,做忽略处理;请求重试,防止网络抖动造成连接失败。

四、iOS网络缓存扫盲篇 – 来自简书:iOS程序猿

iOS 系统会自动对 GET 请求进行缓存;同时提供了NSURLCache支持我们设置缓存路径和缓存大小,文中就如何控制缓存的有效性展开进行了讨论。

五、移动端IM开发者必读(二):史上最全移动弱网络优化方法总结 – 来自即时通讯网

作者针对 IM 场景下弱网进行的一些列总结,文中提到了很多理论基础,提出自动重试时导致后台雪崩的重要因素的观点。本文篇幅较长,适合有一定网络底层基础的人阅读。

六、iOS中的网络调试 – 来自掘金:即刻团队

“开发 iOS 的过程中,有一件非常令人头疼的事,那就是网络请求的调试,无论是后端接口的问题,或是参数结构问题,你总需要一个网络调试的工具来简化调试步骤。” 本文是即刻团队进行网络调试的解决方案。

学习资料

整理编辑:Mimosa

Swift Algorithm Club

地址:https://github.com/raywenderlich/swift-algorithm-club

raywenderlich 创立的 Swift 算法俱乐部,在这里会用 Swift 来解释和实现大部分常见的数据结构和算法,例如栈、队列、快速排序、BFS、KMP 等等,如果按照他的学习路线来学习的话,难度由浅入深,循序渐进,很适合入门选手。另外你也可以自己选择感兴趣的内容来查看,适合想要温习算法和数据结构或者温习 Swift 语法的朋友👍。

工具推荐

整理编辑:brave723zhangferry

Cakebrew

地址:https://www.cakebrew.com/

软件状态:免费,开源

Homebrew 是 Mac 端常用的包管理工具,但其仅能通过命令行操作,对那些不擅长使用命令行的开发来说会是一种苦恼,而且命令行确实不够直观。Cakebrew 是一款桌面端的 Homebrew 管理工具,它包含常用的 Homebrew 功能,并将其可视化,像是已安装工具,可升级工具以及工具库等功能。

Paste - Clipboard Manager

地址: https://apps.apple.com/us/app/paste-clipboard-manager/id967805235

软件状态: 收费 ¥98/年

软件介绍

Paste for Mac 是 Mac 平台上一款专业的剪切板记录增强工具,它能够为您储存您在设备上复制您的所有内容,并将其储存在 Paste for Mac 的历史记录中。是您日常生活工作中必不可少的一款软件。

关于我们

iOS 摸鱼周报,主要分享开发过程中遇到的经验教训、优质的博客、高质量的学习资料、实用的开发工具等。周报仓库在这里:https://github.com/zhangferry/iOSWeeklyLearning ,如果你有好的的内容推荐可以通过 issue 的方式进行提交。另外也可以申请成为我们的常驻编辑,一起维护这份周报。另可关注公众号:iOS成长之路,后台点击进群交流,联系我们,获取更多内容。

往期内容

iOS摸鱼周报 第十六期

iOS摸鱼周报 第十五期

iOS摸鱼周报 第十四期

iOS摸鱼周报 第十三期