人物访谈--一位研究生的iOS之路

人物访谈--一位研究生的iOS之路

本期访谈对象是摸鱼周报的主编之一:反向抽烟。他还在读研,因为名字里带个尧字,我们都叫他尧兄。尧兄有一个博客:https://blog.csdn.net/opooc ,分享自己的学习记录,大家有兴趣可以看下。

zhangferry:简单介绍下自己,再讲一下最近的状态吧。

大家好,首先感谢飞哥的采访。网名叫反向抽烟,是一名研二在读的大学生,也是 iOS成长之路群的早期用户。目前在做计算机视觉方向学习,一方面对这个领域比较感兴趣,另一方面也是为了毕业设计。在学校的状态一般,强度不大,还是比较随意的。

zhangferry:你之前在网易实习,后来又回到了校园,为啥结束了实习?对比校园和职场这两种不同的环境,能分享下你的感受吗?

导师给我安排了一些任务,要结束实习工作,所以才回来的。其实我自己不想结束实习,还是挺想上班的。大家一起共事,一起努力完成工作,那种氛围,我还是挺喜欢的。

时间和精力上的分配不太一样,实习更多的是输出吧,校园自由一些,输入会多一些。

zhangferry:你接触过的技术方向挺多的,算法、Java、iOS、计算机视觉等,你在选择或者更换技术方向时一般如何做权衡呢?

这些方向的本质是没有什么区别,都是为了解决问题,后端也好,前端也好,我觉得都是值得学一下的,毕竟打工嘛,还是得提高自身价值,。

zhangferry:尧兄啃过很多本书,能推荐一个对自己影响最大的一本书,简单介绍下吗

书的话没有看很多,飞哥既然给面了,那我就推荐一本课外读物《狼的智慧》,强者恒强,适者生存。

备注:尧兄真的是在啃书,有一张之前群里发的图片为证:

再附一张他推荐过的书目名单:

zhangferry:对于学习这件事,你是如何保持热情的?感觉你对学习是沉迷的。可以推荐一些你的学习方法吗?

对我而言应该就是兴趣吧,如果对一样技术的态度是为了完成任务而学习,我大概率完成不好。但如果是自主的学习过程中,我有了一些自己的想法,就会竭尽全力的去弄懂。所以主动而非被动,会让学习这件事更有趣味性。

学习方法的话,分两个角度吧,一是学习一定得是刻意的,要拿出时间来经常练习的,这个没有捷径;第二是对于新东西要先用起来,再去研究原理,边用边学,这样效率会高一些。

zhangferry:你经历过不少大厂的 iOS 面试,实习面试时考察侧重点是什么样的?对正在找实习工作的同学有什么建议吗?

实习面试重点还是在考察基础,有四个部分:算法基础 + 计算机基础 + iOS基础 + 项目/实习。算法是最基本的一项,一般每一面都会问到;计算机基础一般是在一面进行考察;项目/实习一般是在二面/三面进行考察。

准备的话:

1.算法最好有个 300 的题量,一定要在面试前保持题感。

2.计算机基础中考察有计算机网络、编译原理、操作系统、数据结构,网络和操作系统基本是必考,这一部分还是需要花时间理解的,建议平时多花时间钻研下,只背面经是经不住问的。

3.iOS基础准备起来还是有章可循的,推荐看 mj 老师的底层视频和慕课网的 iOS 大牛面试视频,最后把不理解的点做好笔记,及时请教。

4.项目/实习有一样就可以,要是准备项目的话,可以把自己的以前写过的项目进行一些优化,做好总结,把写了简历上的点,都一定要弄明白。

实习面试看公司或者部门,有的公司要实习生去干活,就考 iOS 多一些,有的是为了培养转正,就要求基础牢实一些。主要还是凡事提前准备吧,多往前看几步,早做准备。

zhangferry:最近有什么新的感想或心得跟我们分享吗?

程序员这个行业其实是与社会关系脱节的,它不是那种社会性质的工作,会经常和人打交道。技术肯定要有的,毕竟是门手艺活,得靠它吃饭,但技术只是一个最最基本的层面,政治书上说了,人的本质是一切社会关系的总和,所以大家还是要多多搞好社会关系,这样人生路才会更宽广,走的更顺畅。(仅为个人观点)

iOS摸鱼周报 第三十期

iOS摸鱼周报 第三十期

本期概要

  • Tips:分享 WKWebView 几个不常用的特性。
  • 面试模块:一道 Tagged Pointer 相关题目。
  • 优秀博客:本期博客整理了 Codable 在一些特殊场景的处理方式,Swift 处理 JSON 解析时的一些技术细节。
  • 学习资料:Xcode Build Settings 的参数说明网站;来自 Microsoft 的 Data Science 基础课程。
  • 开发工具:免费且开源的 Coding 时间追踪工具:wakapi。

开发Tips

WKWebView 几个不常用的特性

整理编辑:FBY展菲

1. 截获 Web URL

通过实现 WKNavigationDelegate 协议的 definePolicyFor 函数,我们可以在导航期间截获 URL。以下代码段显示了如何完成此操作:

1
2
3
4
5
6
7
8
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {

let urlString = navigationAction.request.url?.absoluteString ?? ""
let pattern = "interceptSomeUrlPattern"
if urlString.contains(pattern){
var splitPath = urlString.components(separatedBy: pattern)
}
}

2. 使用 WKWebView 进行身份验证

当 WKWebView 中的 URL 需要用户授权时,我们需要实现以下方法:

1
2
3
4
5
6
7
8
func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {

let authenticationMethod = challenge.protectionSpace.authenticationMethod
if authenticationMethod == NSURLAuthenticationMethodDefault || authenticationMethod == NSURLAuthenticationMethodHTTPBasic || authenticationMethod == NSURLAuthenticationMethodHTTPDigest {
//Do you stuff
}
completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential, credential)
}

收到身份验证质询后,我们可以确定所需的身份验证类型(用户凭据或证书),并相应地使用提示或预定义凭据来处理条件。

3. 多个 WKWebView 共享 Cookie

WKWebView 的每个实例都有其自己的 cookie 存储。为了在 WKWebView 的多个实例之间共享 cookie,我们需要使用 WKHTTPCookieStore,如下所示:

1
2
3
4
let cookies = HTTPCookieStorage.shared.cookies ?? []
for (cookie) in cookies {
webView.configuration.websiteDataStore.httpCookieStore.setCookie(cookie)
}

4. 获取加载进度

WKWebView 的其他功能非常普遍,例如显示正在加载的 URL 的进度更新。

可以通过监听以下方法的 estimatedProgress 的 keyPath 值来更新 ProgressViews:

1
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)

5. 配置 URL 操作

使用 decisionPolicyFor 函数,我们不仅可以通过电话,Facetime 和邮件等操作来控制外部导航,还可以选择限制某些 URL 的打开。以下代码展示了每种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {

guard let url = navigationAction.request.url else {
decisionHandler(.allow)
return
}

if ["tel", "sms", "mailto"].contains(url.scheme) && UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
decisionHandler(.cancel)
} else {
if let host = navigationAction.request.url?.host {
if host == "www.notsafeforwork.com" {
decisionHandler(.cancel)
} else{
decisionHandler(.allow)
}
}
}
}

参考:WKWebView 几个不常用的特性

面试解析

整理编辑:师大小海腾

Q:以下两段代码的执行情况分别如何?

1
2
3
4
5
6
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abcdefghij"];
});
}
1
2
3
4
5
6
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abcdefghi"];
});
}
  • 第一段代码,self.name 是 __NSCFString 类型,存储在堆,需要维护引用计数,其 setter 方法实现为先 release 旧值,再 retain/copy 新值。这里异步并发执行 setter 就可能会有多条线程同时 release 旧值,过度释放对象,导致 Crash。
  • 第二段代码,由于指针足够存储数据,字符串的值就直接通过 Tagged Pointer 存储在了指针上,self.name 是 NSTaggedPointerString 类型。在 objc_release 函数中会判断指针是不是 Tagged Pointer,是的话就不对对象进行 release 操作,更不会过度释放而导致 Crash 了。

这里是 release 的实现:

1
2
3
4
5
6
7
8
__attribute__((aligned(16), flatten, noinline))
void
objc_release(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
return obj->release();
}

优秀博客

整理编辑:皮拉夫大王在此我是熊大东坡肘子

1、或许你并不需要重写 init(from:) 方法 – 来自:kemchenj

@东坡肘子:Codable 作为 Swift 的特性之一是很注重安全,也很严谨的,这就导致了它在实际使用时总会有这样那样的磕磕绊绊,我们不得不重写 init 方法去让它跟外部环境融洽地共存。本文介绍了一种通过重载 decodeIfPresent 方法以实现应对特殊类型的思路。从某种程度上来说,作者认为这甚至是比 Objective-C 的消息机制更加灵活的一种函数声明机制,而且它的影响范围是有限的,不容易对外部模块造成破坏(别声明为 open 或者 public 就没问题)。

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

@东坡肘子:本文介绍了一个使用 Swift Codable 解码时难以设置默认值问题,并利用 Property Wrapper 给出了一种相对优雅的解决方式,来在 key 不存在时或者解码失败时,为某个属性设置默认值。这为编解码系统提供了更好的稳定性和可扩展性。最后,对 enum 类型在某些情况下是否胜任进行了简单讨论。

3、2021 年了,Swift 的 JSON-Model 转换还能有什么新花样 – 来自知乎:非著名程序员,作者 明林清

@皮拉夫大王:本文主要介绍 ExCodable 的特性和使用方法。在文章开头先介绍了常见的 JSON 转模型的几种方式,并对这些方式各自的优缺点进行了总结,随后引出 ExCodable 的特性及使用方法。

4、json 解析有什么可说道的 – 来自公众号:码农哈皮

@皮拉夫大王:文章开头先介绍了什么是 JSON。正文主要篇幅在介绍 SwiftyJSON 和 YYModel 的实现方案。文章最后引出了 HandyJSON,HandyJSON 是基于借助 metadata 结构来实现 JSON 转 Model 的。在这里额外提一句,如何推断 metadata 的结构,可以参考 GenMeta.cpp 中每个结构的 layout 函数。

5、Swift中Json转Model的便捷方式 – 来自掘金:我是熊大

@我是熊大:本文介绍 JSON、Model、Data、Dict 相互转换的小技巧和代码段,适合在实际工作中使用。

6、Swift 码了个 JSON 解析器(一) – 来自知乎:OldBirds

@我是熊大:正如作者所言,码了个 JSON 解析器,感兴趣的可以看一下。

学习资料

整理编辑:Mimosa

Xcode Build Settings

地址:https://xcodebuildsettings.com/

顾名思义,这个网站的作用是展示 Xcode 所有的 Build Settings。你可以在这里按分类查看所有的设置项,搜索你想要的设置项,或查询某个设置项的值类型及其默认值。对于常常要和 Build Settings 打交道的开发者来说,这个网站很实用。

Data-Science-For-Beginners

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

来自 Microsoft 的 Data Science 基础课程,为期 10 周,有 20 节课。这是一个基于项目的课程,配套 40 多个小测试,通过该课程你可以学习到关于数据科学的基础知识。每节课程还有精美的插画配图,有兴趣学习 Data Science 的朋友可以尝试一下。

工具推荐

整理编辑:CoderStar

wakapi

地址https://wakapi.dev/

软件状态:免费,开源

软件介绍

Wakapi 是一个开源工具,可帮助我们跟踪使用不同编程语言等在不同项目上编码所花费的时间,并使用图表等形式展现出来,支付 Xcode,值得一玩。

wakapi

关于我们

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

往期推荐

iOS摸鱼周报 第二十九期

iOS摸鱼周报 第二十八期

iOS摸鱼周报 第二十七期

iOS摸鱼周报 第二十六期

人物访谈--微软Offer之路

人物访谈--微软Offer之路

本期访谈人物是张安宇,他是 #Swift社区 公众号的负责人,最近换工作接了微软(苏州)的 Offer,11月底入职,目前正在爽爽的休假。咱们拉他过来聊聊关于面试微软的经历。

zhangferry:简单介绍下自己吧。

大家好,我是 #Swift社区 公众号的负责人,我叫张安宇,很高兴被摸鱼周报邀请来参加这期访谈,我是摸鱼周报的读者,经常看摸鱼周报的文章。

我是一个换工作非常频繁的人,平均每家公司工作 1-2 年甚至更短,比如搜狐和掌阅科技,兄弟们常常因为这个取笑我。关于这个问题我没有特别多的看法,找工作的过程中也没有因为这些受到伤害或者阻挠,相反每次换工作都还挺顺利,大部分情况下是能够受到用人单位的尊重的。

在饿了么工作的时间稍长一些,我在饿了么的蜂鸟团队和北京的前端组主导完成过饿了么入淘的第一个项目叫做蜂鸟商城,同时还负责给我们物流团队的几个 App 统一接入淘宝的浏览器框架 Windvane

我喜欢玩游戏、看书、看电影,偶尔运动,很喜欢交朋友。平时我跟展菲一起维护我们的这个公众号,还有一些技术群。我们会定期分享一些 Swift 社区动态和相关的资讯,欢迎大家的关注。

zhangferry:能简单说下你是如何准备微软的面试的吗,实际面试过程中有哪些比较重要的记忆点?跟国内其他大厂的 iOS 面试有什么区别?

微软的面试流程非常长,一共有 6 轮,每轮 1 个小时左右,都是技术面。我之前也没有接触过这种面试形式,所以主要依靠 HR 提供的面试考察点,以及自己去了解来的一些信息来准备这场面试。

这里有个小插曲,正常来说每轮面试间隔应该都是一周左右。因为当时还有另外几家公司在同时面试,跟 HR 说想加快进度,结果后面 2 天就安排完了剩余的 5 轮面试。

微软的 HR 会特别详细地向大家说明每一轮面试的主要步骤,我觉得最重要的是进行针对性地准备。微软面试的核心考察点就是 coding 环节,算法在每轮面试都有,如果代码功底不足,建议继续刷题再参加应聘。整体来说,是在能白板编程 + bug free 的前提下,再根据自己多年的工作经验,与面试官交流一些 iOS 方面的通用性技术。

实际面试中,我能够比较从容应对的主要原因是熟练 + 自信。我有 NOIP 与 ACM 的参赛经验,加上面试前认真刷了半个月 Leetcode,基本上遇到题目不会发慌。因为备战仓促的原因,我刷的题目数量也有限,实际面试过程中,也没有遇到最近刷过的原题。好在微软的题目不算太难,毕竟面试时间只有一小时左右,基本上是 mid 难度的题目。在现场编写时我都基本做到了编译通过、结果正确,并且能分析出算法的时间复杂度和空间复杂度。我觉得我这个表现对于微软来说,是最低要求了,如果 coding 不过关,应该也是会得到面试官的 no hire 的。

我想声明一下,微软的面试要求其实不低,并不是说我就凭刷了两周题目(60 题左右)就通过了面试,整个面试过程的考察还是很全面的,算法只是其中一部分。希望大家不要误解这一点,不要黑微软,谢谢大家。

zhangferry:你面过很多国内大厂,在你看来微软跟他们比起来有什么区别呢?

工作了这么多年,我几乎参加过所有公司的面试,确实微软这样的企业与国内很多互联网企业的面试要求、流程、侧重点都不一样。

据我观察,头部的互联网企业,面试大都是以面试题八股文为主,最后结束以前面试官可能会考一下算法题,也可能不会。像字节的话就比较看重算法题,相信这个大家都比我更了解。

微软对于算法能力更看重一些,整个面试过程对于候选人的考察点聚焦在算法题,coding 能力。coding 对应的是现场写代码,比如手写 GCD 代码,实现一个多线程的需求。还会有一些相对开放的题目,比如问是否关注过最近的 WWDC,苹果的一些最新特性是什么;如果实现一个词云功能有什么思路,中英文如何分词等等。

zhangferry:你应该通过了多家互联网公司的面试,为什么最终选择了微软,而且还要从北京搬到苏州?

这个并没有特别多,我是收到微软面试的邀请以后决定离职,因为刷题需要付出很多精力与时间,我曾戏谑说,我备战高考时也不曾如此认真,哈哈。

确实除了微软以外,还有其他公司也在接触,基本都聊的很愉快很顺利,但是不多。我没怎么主动投递自己的简历,甚至简历都没来得及更新,从离职到 Offer 大约两周时间,对于这么密集的面试流程来说,时间是非常仓促的,所以也不能说难度不大。

为了这 6 轮面试,我腾不开时间鸽掉了一些公司,有一家公司我鸽了两次……我怀疑我已经被这些公司屏蔽拉黑了,但我也没办法哈哈。

选择微软的原因,我觉得应该是目标职位的匹配程度比较高,还有微软给出的薪资待遇足够慷慨大方。入职的是 Edge 部门,我说过我喜欢前端技术,也喜欢浏览器内核研发相关的领域,我希望这份工作能够让我继续成长突破自己。

在接受 Offer 的过程中,我坦言过自己可以接受落差,因为我没去过微软,所以对微软所有的了解只能来自于想象与道听途说。希望我的苏州之旅,能够真正让我了解到这家公司,也很乐意在朋友圈与朋友们继续分享我的成长路程与心得体会。

为什么愿意去苏州,这个话题比较沉重。我是特别舍不得北京的,最开始我拒绝过一次微软的面试邀请,也是因为微软在北京没有 iOS 职位的原因。最后愿意去苏州,说实话也是对北京这个城市很多地方感到不太满意。

北京是好,但在北京的这些年,真的没有太多归属感,永远在内心里把自己当做低人一等的外地人。所以搬家去苏州对我来说也没那么难受,毕竟我在北京没有根,我自嘲我这种没有根的人,也是很洒脱的,我想去哪里就去哪里。值得一提的是,微软因为这个给了我搬家费,还不少,令我挺感动的。
我很庆幸微软的 HR 都特别大度,没有把我之前拒绝面试这件事放心里,后来很快就给我重新安排了面试,并且也很热心的提供了很多资料给我。

zhangferry:能否简单介绍下已知的微软工作环境、入职的部门?对于英语是否有特殊要求,有没有英文的面试?在你看来微软更喜欢招什么样的人?

据我了解,微软的工作环境非常的好,不管是苏州的研发中心,还是北京的总部。我入职的部门是 Edge 团队,我听说 Bing App 即将与 Edge App 合并,希望这件事不会对我的 Offer 有什么影响,哈哈哈。

Edge 是微软自研的浏览器,我相信很多人都知道,Edge 除了 Windows 客户端以外还有 Mac 和 iOS 端,当然也有 Android 端。据我了解,我应该可以去这个团队学习一些比较深入的浏览器内核技术,以及一些 C++ 技术。我希望我以后能成为这方面的大牛,哈哈。

英语的话,当然是要能够日常交流,起码阅读、发 email 能做到没有障碍吧我觉得。面试过程中,我没有遇到说英文的面试官,但是不排除别人不会遇到哈,毕竟微软是一家跨国企业。

我觉得微软喜欢招什么样的人?哈哈这个问题太大了,不过了解我的知道我这个人很狂我也没啥不敢讲的话,我觉得微软应该喜欢招天才进来,真正的天才。不是我自诩天才,是我觉得微软公司配吸引天才们加入这里,一起去实现改变世界的想法,真的。

zhangferry:对于正在面试和即将面试的人有什么好的建议?

据我了解我有个师弟也在面试微软,他比我优秀多了,我认为他没有问题,他应该刚从中科院读完硕士,在我眼里属于学霸,我认为他进入微软没有太大问题,加油。

对于即将面试的人有什么建议的话,我觉得真的比较简单,那就是调整好心态,认真刻苦复习算法与数据结构,这是一块特别有针对性的领域。

你面试其他公司可能还需要复习一下编译原理之类的,但是对于微软的面试流程,对于 coding 环节来说,我认为就可以不用再额外付出时间精力去复习这些。目标性非常强,所以很容易成功。祝愿和希望更多优秀的人才加入微软和我做同事,哈哈。

zhangferry:有什么想借助于摸鱼周报宣传的。

当然是替微软招人啦。虽然我目前还没入职,还是想借此机会,替微软的 HR 宣传一下,微软苏州研发中心,大量 HC 期待大家加入。前端、后端、全栈、算法、Data Scientist 都需要,感兴趣的可以投递简历到:a-huili@Microsoft.com

也希望没有关注过 #Swift社区 公众号的朋友,能点个关注,支持一下我的事业,谢谢大家。

zhangferry:你经常在朋友圈发不少长文,最近肯定也有不少生活或者工作的感悟吧,可以给我们分享一下。

当然有,最近有很多兄弟说被我的精神感动了,说我怎么说到做到,说刷题就刷题,说换工作就换工作呢,每天还刷到后半夜,太励志了。

我想了一下,可能也是有那么点励志吧,哈哈。人生需要目标,需要理想,需要榜样,看着别人奋斗你也能被感染与鼓舞的,我觉得这就是力量,让生命变得更加精彩的力量。

我在朋友圈编过一句鸡汤,我说“我喜欢光,我追寻光,同时我也想成为光”。我觉得大概就是这个意思吧,好的气质,互相吸引互相鼓舞,这是很难能可贵的事。不过鸡汤喝多了会腻,在身边找一个真正的榜样,向他看齐,脚踏实地的前进,时间久了你可能会发现已经与他并肩而行了。

我就有很多技术偶像和技术榜样,我很荣幸他们大部分都通过了我的好友申请,特别开心。希望大家都能找到自己的榜样。

如果大家不嫌弃想认识我的话,可以通过飞哥找到我,我这个人真的很爱交朋友,哈哈,兄弟们都说我有社交牛逼症……

再次感谢飞哥的邀请,参加这期摸鱼周报的访谈,谢谢。

iOS摸鱼周报 第二十九期

iOS摸鱼周报 第二十九期

本期概要

  • Tips:关于低电量模式的一些介绍。
  • 面试模块:Objective-C 的消息机制(下)。
  • 优秀博客:整理了几篇 Swift Tips 的文章。
  • 学习资料:Gitmoji:一个 GitHub 提交信息的 emoji 指南😎。
  • 开发工具:能够使用 Swift 开发安卓应用的工具:SCADE;可视化解析 .ndjson 文件的工具:Privacy-Insight。

本期话题

@zhangferry:本期访谈内容独立成篇了,大家可以查看本期公众号推送的次条。或者访问这个链接:

本期摸鱼周报迎来一位新伙伴:东坡肘子。肘子之前因为身体原因修养过一段时间,也因为身体的原因需要做健康记录,但并没有找到满意的记录方式,于是决定自己开发,由此结缘 iOS 做起了独立开发。之后我们还会对他进行一次访谈,带大家了解他的更多故事,你也可以关注他的博客:肘子的 Swift 记事本 https://www.fatbobman.com/

开发Tips

整理编辑:夏天

低电量模式

从 iOS 9 开始,Apple 为 iPhone 添加了低电量模式(Low Power Mode)。用户可以在 设置 -> 电池 启用低电量模式。在低电量模式下,iOS 通过制定某些节能措施来延长电池寿命,包括但不限于以下措施:

  • 降低 CPU 和 GPU 性能,降低屏幕刷新率
  • 包括联网在内的主动或后台活动都将被暂停
  • 降低屏幕亮度
  • 减少设备的自动锁定时间
  • 邮件无法自动获取,陀螺仪及指南针等动态效果将被减弱,动态屏保将会失效
  • 对于支持 5G 的 iPhone 设备来说,其 5G 能力将被限制,除非你在观看流媒体

上述节能措施是否会影响到你的应用程序,如果有的话,你可能需要针对低电量模式来适当采取某些措施。

lowPowerModeEnabled

我们可以通过 NSProcessInfo 来获取我们想要的进程信息。这个线程安全的单例类可以为开发人员提供与当前进程相关的各种信息。

一个值得注意的点是,NSProcessInfo 将尝试将环境变量和命令行参数解释为 Unicode,以 UTF-8 字符串返回。如果该进程无法成功转换为 Unicode 或随后的 C 字符串转换失败的话 —— 该进程将被忽略

当然,我们还是需要关注于低电量模式的标志,一个表示设备是否启用了低电量模式的布尔值 —— lowPowerModeEnabled

1
2
3
4
5
if NSProcessInfo.processInfo().lowPowerModeEnabled {
// 当前用户启用低电量模式
} else {
// 当前用户未启用低电量模式
}

NSProcessInfoPowerStateDidChangeNotification

为了更好的响应电量模式的切换——当电池充电到 80% 时将退出低电量模式,Apple 为我们提供了一个全局的通知NSProcessInfoPowerStateDidChangeNotification

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: "yourMethodName:",
name: NSProcessInfoPowerStateDidChangeNotification,
object: nil
)

func yourMethodName:(note:NSNotification) {
if (NSProcessInfo.processInfo().isLowPowerModeEnabled) {
// 当前用户启用低电量模式
// 在这里减少动画、降低帧频、停止位置更新、禁用同步和备份等
} else {
// 当前用户未启用低电量模式
// 在这里恢复被禁止的操作
}
}

总结

通过遵守 iOS 应用程序能效指南 推荐的方式,为平台的整体能效和用户体验做出改变。

参考

面试解析

整理编辑:师大小海腾

本期面试解析讲解的知识点是 Objective-C 的消息机制(下)。在上一期摸鱼周报中我们讲解了 objc_msgSend 执行流程的第一大阶段 消息发送,那么这一期我们就来聊聊后两大阶段 动态方法解析消息转发

动态方法解析

如果 消息发送 阶段未能处理未知消息,那么就会进行一次 动态方法解析。我们可以在该阶段通过动态添加方法实现,来处理未知消息。动态方法解析 后,会再次进入 消息发送 阶段,从 “去 receiverClass 的 method cache 中查找 IMP” 这一步开始执行。

具体来说,在该阶段,Runtime 会根据 receiverClass 的类型是 class/meta-class 来调用以下方法:

1
2
+ (BOOL)resolveInstanceMethod:(SEL)sel;
+ (BOOL)resolveClassMethod:(SEL)sel;

我们可以重写以上方法,并通过 class_addMethod 函数来动态添加方法实现。需要注意的一点是,实例方法存储在类对象中,类方法存储在元类对象中,因此这里要注意传参。

1
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)

如果我们在该阶段正确地处理了未知消息,那么再次进入到 消息发送 阶段肯定能找到 IMP 并调用,否则将进入 消息转发 阶段。

消息转发

消息转发 又分为 Fast 和 Normal 两个阶段,顾名思义 Fast 更快。

  1. Fast:找一个备用接收者,尝试将未知消息转发给备用接收者去处理。

具体来说,就是给 receiver 发送一条如下消息,注意有类方法和实例方法之分。

1
+/- (id)forwordingTargetForSelector:(SEL)selector;

如果我们重写了以上方法,并正确返回了一个 != receiver 的对象(备用接收者),那么 Runtime 就会通过 objc_msgSend 给备用接收者发送当前的未知消息,开启新的消息执行流程。

如果该阶段还是没能处理未知消息,就进入 Normal。需要注意,在 Fast 阶段无法修改未知消息的内容,如果需要,请在 Normal 阶段去处理。

  1. Normal:启动完整的消息转发,将消息有关的全部细节都封装到一个 NSInvocation 实例中,再给接收者最后一次机会去处理未知消息。

具体来说,Runtime 会先通过调用以下方法来获取适合未知消息的方法签名。

1
+/- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

然后根据这个方法签名,创建一个封装了未知消息的全部内容(target、selector、arguments)的 NSInvocation 实例,然后调用以下方法并将该 NSInvocation 实例作为参数传入。

1
+/- (void)forwardInvocation:(NSInvocation *)invocation;

我们可以重写以上方法来处理未知消息。在 forwardInvocation: 方法中,我们可以直接将未知消息转发给其它对象(代价太大,不如在 Fast 处理),或者改变未知消息的内容再转发给其它对象,甚至可以定义任何逻辑。

如果到了 Normal 还是没能处理未知消息,如果是没有返回方法签名,那么将调用 doesNotRecognizeSelector:;如果是没有重写 forwardInvocation:,将调用 NSObject 的 forwardInvocation: 的默认实现,而该方法的默认实现也是调用 doesNotRecognizeSelector:,表明未知消息最终未能得到处理,以 Crash 程序结束 objc_msgSend 的全部流程。

一些注意点

  • 重写以上方法时,不应由本类处理的未知消息,应该调用父类的实现,这样继承体系中的每个类都有机会处理未知消息,直至 NSObject。
  • 以上几个阶段均有机会处理消息,但处理消息的时间越早,性能就越高。
    • 最好在 动态方法解析 阶段就处理完,这样 Runtime 就可以将此方法缓存,稍后这个对象再接收到同一消息时就无须再启动 动态方法解析消息转发 流程。
    • 如果在 消息转发 阶段只是单纯想将消息转发给备用接收者,那么最好在 Fast 阶段就完成。否则还得创建并处理 NSInvocation 实例。
  • respondsToSelector: 会触发 动态方法解析,但不会触发 消息转发

优秀博客

整理编辑:皮拉夫大王在此我是熊大东坡肘子

1、【iOS】Swift Tips - (一) – 来自掘金:Layer

@皮拉夫大王:文章是作者的学习笔记,作者将 objccn.io/ 的内容整理出来,一共 6 篇,适合在地铁上阅读。在这篇文章中主要介绍了柯里化、多元组、操作符等写法和用途。

2、十个技巧让你成为更加优秀的 Swift 工程师 – 来自知乎:Summer

@皮拉夫大王:学习 Swift 不光要能写 Swift 代码,更要优雅地使用 Swift,这也是本期博客主题的目的。这篇文章介绍了巧用扩展、泛型、计算属性等优化代码,在初学者看来是比较有意思的。

3、写更好的 Swift 代码:技巧拾遗 – 来自掘金:OldBirds

@东坡肘子:作者在文章中介绍了几个很实用的 Swift 使用技巧,包括:通过前缀避免命名冲突、快速交换值、@discardableresult、访问控制等,对日常的开发很有帮助。

4、Swift:where关键词使用 – 来自掘金:season_zhu

@东坡肘子:本文介绍了 where 在 Swift 中的几个使用场景,除了应用于 for 循环外,还包括泛型约束、指明类型等。有助于更好的理解在不同上下文中的 where 用法。

5、Swift - 使用Color Literal实现代码中颜色的智能提示(Xcode自带功能) – 来自航歌

@我是熊大:Color Literal 让颜色赋值可视化。

6、【译】使用Swift自定义运算符重载 – 来自掘金:shankss

@我是熊大:有没有想过 “+”,“-”,“??” 底层是怎么实现的?想不想自己也实现一个特有的运算符,如:“–>”,这篇文章带你一起探究。

学习资料

整理编辑:Mimosa

gitmoji

地址:https://gitmoji.js.org/

gitmoji 是一个 GitHub 提交信息的 emoji 指南😎,致力于成为一个标准化的 emoji 备忘单📋,当你在提交信息时,使用 emoji 来描述成了一种简单的方式来识别提交的目的和意图🍰,因为维护者只需要看一眼所使用的 emoji 就能明白🧐。由于有很多的 emoji,所以这里创建了一份指南来让使用 emoji 变得轻松、易懂、一致🥳。

工具推荐

整理编辑:CoderStarzhangferry

SCADE

地址https://www.scade.io/

软件状态

  • SCADE Community:免费
  • SCADE Professional:$29 per month / user

软件介绍

利用 SCADE 我们可以使用 Swift 语言进行跨端原生开发。其描述特点如下:

  • 跨平台:使用相同的源代码为 iOS 和 Android 开发
  • 原生功能:不受限制地使用所有 iOS 和 Android 功能
  • 无与伦比的速度:Swift 被编译为本机二进制代码以获得无与伦比的应用程序性能
  • Swift 框架:在 iOS 和 Android 上使用流行的 Swift 框架,如 Swift Foundation,无需更改代码

SCADE

Privacy-Insight

地址https://github.com/Co2333/Privacy-Insight/releases

软件状态 :免费,开源

软件介绍

解析 iOS 15 下格式为 .ndjson 的系统隐私报告,用 SwiftUI 写成。

隐私日志的生成为设置 -> 隐私 -> 打开记录 App 活动,等待一段时间之后点击下面的存储 App 活动按钮,即可收集这一段时间的隐私日志。存储会生成一个 .ndjson 格式的文件,导出使用 Privacy-Insight 打开即可查看。

以下为我使用 1 天的隐私请求记录:

微信和今日头条的隐私权限获取频率均非常高,我是肯定没有那么频繁通过微信访问相册的。对于微信频繁获取相册权限的问题最近也在热议,希望不仅是微信,各个主流 App 都应该对于用户隐私问题予以重视。

作为使用者相对有效的保护隐私的方案是,关闭对应 App 的「后台刷新」,非必要情况下关闭蓝牙、定位等权限,并将相册调用权限改为「选中的照片」。

关于我们

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

往期推荐

iOS摸鱼周报 第二十八期

iOS摸鱼周报 第二十七期

iOS摸鱼周报 第二十六期

iOS摸鱼周报 第二十五期

MachO 代码签名剖析

验证代码的正确性是计算机科学中最难的问题之一,因为不存在普遍意义的正确的算法,所以这一验证通常使用数字签名处理。数字签名主要做两部分工作:

  • 验证代码的来源是否合法。
  • 代码是否被修改过。

代码签名并非苹果独有技术,Java 和 Android 的 Dalvik 都在使用,但苹果公司是最早开始使用的。大家可以通过阅读下文思考代码来源是否合法和代码是否被修改过的验证是如何实现的。

本篇文章主要参考自 Jonathan Levin 的《最强 iOS 和 macOS 安全宝典》代码签名一章。

  • 测试环境:macOS 11.2.3。
  • 测试项目:/bin/ls 在 x86_64 架构下的 MachO 文件。iOS 下的文件与之相差不大。

代码签名格式

在了解代码签名机制前,非常有必要了解代码签名的包含的内容。代码签名附着在 MachO 的尾部。加载命令为LC_CODE_SIGNATURE,它指向一个超级二进制块Code Signature,该二进制块又包含了多个其他的子二进制块。之前写过一篇文章,有讲解如何手动解析这个签名二进制块:深入理解MachO数据解析规则

下面是该二进制块的层级结构:

超级二进制块是一个目录性质的结构,用于指定子二进制块的位置,各个子二进制块才是代码签名的主要角色。

子二进制块类型

子二进制块类型通过不同值进行表示:

二进制块类型
0x0000 代码目录
0x0002 需求
0x0005 授权
0x10000 CMS 二进制块
0x10001 身份证明(未使用)

本篇主要就是对这几个子二进制块的功能和部分实现进行分析。

二进制块的提取

jtool 是 Jonathan Levin 开发的一款主要用于 MachO 分析的高效工具,可以使用 homebrew 进行安装。

1
$ brew install jtool

我们可以使用 jtool 单独提取代码签名部分:

1
2
3
4
5
6
7
8
9
10
$ jtool -arch x86_64 -e signature /bin/ls
Extracting Code Signature (5728 bytes) into ls.signature
$ od -t x1 -A x ls.signature #原始字节内容
0000000 fa de 0c c0 00 00 14 86 00 00 00 03 00 00 00 00
0000010 00 00 00 24 00 00 00 02 00 00 02 61 00 01 00 00
0000020 00 00 02 9d fa de 0c 02 00 00 02 3d 00 02 01 00
0000030 00 00 00 00 00 00 00 7d 00 00 00 30 00 00 00 02
0000040 00 00 00 0e 00 00 d2 30 20 02 0b 0c 00 00 00 00
0000050 00 00 00 00 63 6f 6d 2e 61 70 70 6c 65 2e 6c 73
# ...

也可以使用 MachO 找到 Code Signature 块进行查看。

代码签名的子二进制块

我们可以使用 jtool 查看代码签名内容的分析:

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
$ jtool -arch x86_64 --sig -v /bin/ls
Blob at offset: 53808 (5728 bytes) is an embedded signature of 5254 bytes, and 3 blobs
Blob 0: Type: 0 @36: Code Directory (573 bytes)
Version: 20100
Flags: none (0x0)
Platform Binary
CodeLimit: 0xd230
Identifier: com.apple.ls (0x30)
CDHash: 46cc1da7c874a5853984a286ffecb48daf2f65f023d10258a31118acfc8a3697 (computed)
# of Hashes: 14 code + 2 special
Hashes @125 size: 32 Type: SHA-256
Requirements blob: a8ccc60c2a5bff15805beb8687c6a899db386d964a5eb3cf3c895753f6879cea (OK)
Bound Info.plist: Not Bound
Slot 0 (File page @0x0000): e4a537939e00f4974e02b03d36e4dab75f7dc095d2214ba66bc53c73c145ceff (OK)
Slot 1 (File page @0x1000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK)
Slot 2 (File page @0x2000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK)
Slot 3 (File page @0x3000): 4a7cb3e6c1b3a6ac82e3575239ee53d4f0d3bed260fed63438fd21ce0d00392e (OK)
Slot 4 (File page @0x4000): 9ec9e4e02292dfda34ef3caa8317e8bfbcc41a46b18d994dba45febe31b8c660 (OK)
Slot 5 (File page @0x5000): 037285f744f366210cde48821261d4a5f5b739dcf0b82f94144613e92c4b7c07 (OK)
Slot 6 (File page @0x6000): be89c764e52382702918f2db62ff24d9df40410fe894b11d505a4abc1f854340 (OK)
Slot 7 (File page @0x7000): a6b322014743965656e796155c1e0bf22e19a3e8770a43f1111cfbc961037d26 (OK)
Slot 8 (File page @0x8000): a643fc9485d941019cbdeead1d5c47add9382417ebe4d15768221f3763553b84 (OK)
Slot 9 (File page @0x9000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK)
Slot 10 (File page @0xa000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK)
Slot 11 (File page @0xb000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK)
Slot 12 (File page @0xc000): 23304ae11c1ade4411cb63a0955eb644574b8af416e4e3818e382421272ae1b4 (OK)
Slot 13 (File page @0xd000): e0ca7b7000d04057e71c49365b1937711b3557f6b91e0fa144791c66de2a7a4d (OK)
Blob 1: Type: 2 @609: Requirement Set (60 bytes) with 1 requirement:
0: Designated Requirement (@20, 28 bytes): SIZE: 28
Ident: (com.apple.ls) AND Apple Anchor
Blob 2: Type: 10000 @669: Blob Wrapper (4585 bytes) (0x10000 is CMS (RFC3852) signature)
CA: Apple Certification Authority CN: Apple Root CA
CA: Apple Certification Authority CN: Apple Code Signing Certification Authority
CA: Apple Certification Authority CN: Apple Root CA
CA: Apple Certification Authority CN: Apple Root CA
CA: Apple Certification Authority CN: Apple Code Signing Certification Authority
CA: Apple Software CN: Software Signing
Time: 201222002625Zi

它有三个 Blob,即三个子二进制块,Blob 0 是代码签名 Blob 1是需求,Blob 2 是 CMS,下面是对这几个 Blob 的分析。

代码目录(Code Directory)

代码目录是签名块的主体,它提供了签名资源的散列值(哈希值)。代码签名并非对整个文件进行签名,因为有时二进制文件可能很大,计算全部内容占用资源较多;而且二进制的加载是按需加载,不会一开始就都全部映射到内存中。签名时会将整个 MachO 文件划分成多个页,每个页单独签名。

代码目录部分就是对签名信息的描述,其中包含了各个分页的签名值,签名算法和分页大小等内容。代码签名的数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* C form of a CodeDirectory.
*/
typedef struct __CodeDirectory {
uint32_t magic; /* magic number (CSMAGIC_CODEDIRECTORY) */
uint32_t length; /* total length of CodeDirectory blob */
uint32_t version; /* compatibility version */
uint32_t flags; /* setup and mode flags */
uint32_t hashOffset; /* offset of hash slot element at index zero */
uint32_t identOffset; /* offset of identifier string */
uint32_t nSpecialSlots; /* number of special hash slots */
uint32_t nCodeSlots; /* number of ordinary (code) hash slots */
uint32_t codeLimit; /* limit to main image signature range */
uint8_t hashSize; /* size of each hash in bytes */
uint8_t hashType; /* type of hash (cdHashType* constants) */
uint8_t platform; /* platform identifier; zero if not platform binary */
uint8_t pageSize; /* log2(page size in bytes); 0 => infinite */
uint32_t spare2; /* unused (must be zero) */
/* Version 0x20100 */
uint32_t scatterOffset; /* offset of optional scatter vector */
/* Version 0x20200 */
uint32_t teamOffset; /* offset of optional team identifier */
/* followed by dynamic content as located by offset fields above */
} CS_CodeDirectory;

结合 CodeDirectory 的偏移量,可以从 MachOView 里查看到这部分数据的内容:

找到对应数据结构中的含义,我们关注其中三个 uint8_t 类型的值:

参数 含义
hashSize 0x20 hash 值大小,为 0x20 字节。
hashType 0x02 表示签名算法,0x01 表示 SHA-1,0x02表示SHA-256。从 macOS10.12 和 iOS11开始,苹果转向使用 SHA-256。
pageSize 0x0C 这里是一个计算公式:log2(PageSize) = 0x0C

根据公式算出分页大小:PageSize = 2 ^ 0x0C = 4096 = 0x1000 = 4K。这跟系统的内存分页大小是一致的。

由此可知整个 MachO 文件会按照 0x1000 字节的大小进行分页,分页使用 SHA-256 算出散列值。这些计算出的散列值会记录在代码插槽(Code Slots)里。

代码插槽验证

上面Slot 从 0 到 13 的标记对应的都是代码插槽。

有了计算规则我们还可以手动验证代码签名的正确性,我们以前三个代码插槽为例,也即前 0x1000 字节的内容,尝试手动计算其散列值。

1
2
3
4
5
6
7
$ lipo /bin/ls -thin x86_64 -output /tmp/ls_x86_64
$ dd bs=0x1000 skip=0 count=1 if=/tmp/ls_x86_64 2>/dev/null | openssl sha256
SHA256(stdin)= e4a537939e00f4974e02b03d36e4dab75f7dc095d2214ba66bc53c73c145ceff
$ dd bs=0x1000 skip=1 count=1 if=/Users/zhangferry/ls_x86_64 2>/dev/null | openssl sha256
SHA256(stdin)= ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7
$ dd bs=0x1000 skip=2 count=1 if=/Users/zhangferry/ls_x86_64 2>/dev/null | openssl sha256
SHA256(stdin)= ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7

注意到后面两个插槽计算结果一样,这是因为这两部分数据为补齐位,它们全部为0。

跟前三个代码插槽的值进行对比:

1
2
3
Slot   0 (File page @0x0000):	e4a537939e00f4974e02b03d36e4dab75f7dc095d2214ba66bc53c73c145ceff (OK)
Slot 1 (File page @0x1000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK)
Slot 2 (File page @0x2000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK)

发现两边散列值一样,输出内容后面的 OK 是 jtool 验证的结果。

这里可以看到代码插槽有14个,File page 里的内容表示相对起始地址。另外注意到输出部分有一句注释:

1
# of Hashes: 14 code + 2 special

其表示有 14 个代码插槽和 2 个特殊插槽。

特殊插槽

特殊插槽的出现是因为App由多个内容组成,并非只有二进制文件,为了保证这些非二进制文件的完整性,对它们也会进行签名,它们的签名值就是特殊插槽。因为代码插槽的索引是从0开始的,而且其大小不固定,为了把特殊插槽也能排列进去,就选用负数来表示特殊插槽的含义。以下是特殊插槽的定义:

# 插槽目的
-1 绑定的info.plist
-2 需求(requirement):二进制块嵌入代码签名
-3 资源目录:CodeSignature/CodeResources文件的散列值
-4 具体应用:实际上未被使用
-5 授权(entitlement):嵌入在代码签名中的授权

我们可以在上方 jtool 的输出内容里找到特殊插槽的内容:

1
2
Requirements blob:	a8ccc60c2a5bff15805beb8687c6a899db386d964a5eb3cf3c895753f6879cea (OK)
Bound Info.plist: Not Bound

因为特殊插槽作用是固定的,也就没用序号表示。

代码签名需求(Requirements)

目前代码签名只是分块取散列值,保存起来,但好像还不够强大。苹果公司已经为代码签名增加了另外一个机制:需求(requirements)。它可以自定义规则以施加特定限制,比如允许哪些动态库加载。

需求有一套特殊的语法规则,其表达由操作数和操作码组成,丰富的操作码集使得构建任何数量的逻辑条件成为可能。可以在requirements.h 文件里查看都有哪些操作码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum ExprOp {
opFalse, // unconditionally false
opTrue, // unconditionally true
opIdent, // match canonical code [string]
opAppleAnchor, // signed by Apple as Apple's product
opAnchorHash, // match anchor [cert hash]
opInfoKeyValue, // *legacy* match Info.plist field [key; value]
opAnd, // binary prefix expr AND expr
opOr, // binary prefix expr OR expr
opCDHash, // match hash of CodeDirectory directly
opNot, // logical inverse
opInfoKeyField, // Info.plist key field [string; match suffix]
opCertField, // Certificate field [cert index; field name; match suffix]
opTrustedCert, // require trust settings to approve one particular cert [cert index]
opTrustedCerts, // require trust settings to approve the cert chain
opCertGeneric, // Certificate component by OID [cert index; oid; match suffix]
opAppleGenericAnchor, // signed by Apple in any capacity
opEntitlementField, // entitlement dictionary field [string; match suffix]
exprOpCount // (total opcode count in use)
};

对需求的编译是由 csreq 进行的,对需求的验证可以使用 codesign -v

我们这里来尝试解读下已有的需求内容。

大部分二进制文件的需求只是验证签名身份,即使用证书是否为苹果所颁发。在 App Store 里的应用则使用更严格的规则集。我们可以查看 Xcode 的代码签名需求:

1
2
3
$ codesign -d -r- /Applications/Xcode.app/Contents/MacOS/Xcode
Executable=/Applications/Xcode.app/Contents/MacOS/Xcode
designated => (anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = APPLECOMPUTER) and identifier "com.apple.dt.Xcode"

注意这里有不少 1.2.840.113635 开头的标识,它代表的是国际通用标准证书中苹果公司的分支(iso.member-body.us.appleOID)。其中100对应了安全相关的一些定义 appleDataSecurity,详细内容可以看这里oidref.com

对照Xcode的签名需求,我们可以大致推断出这些规则的含义:

  • 由苹果签名且证书节点包含 6.1.9 即 Mac App Store App。
  • 或由苹果签名且证书节点包含 6.2.6 即 “dev_program”。(推测是开发版本的应用)
  • 其证书节点包含 6.1.13 即 Developer ID Applications。
  • 证书的团队标识符(OU)为 APPLECOMPUTER 且 BundleId 为 com.apple.dt.Xcode

注意其中最后一项的内容,限定了团队标识符和BundleId,这样就能够解决应用被重签名的问题了。

CMS

CMS 是Cryptographic Message Syntax的缩写,是一种标准的签名格式,由RFC3852定义。书中并没有提这部分内容,但我认为这部分恰恰是代码签名最关键的步骤。

CMS 格式的签名中,除了包含证书之外,还承载了一些其他的信息,比如签名属性 signedAttrs

上面说了 CodeDirectory 里保存了 MachO 分页的 Hash 值,只要保证这个 CodeDirectory 不被修改就可以了。所以对代码目录进行 Hash 计算,获得 CDHash,然后对这个 CDHash 进行签名就可以了。

注意这一步才是真正的签名,其开始涉及加密,前面的代码插槽只是提供摘要信息。

注意到 jtool 的签名输出里有这样一句:

1
CDHash:	     46cc1da7c874a5853984a286ffecb48daf2f65f023d10258a31118acfc8a3697 (computed)

这就是外部计算的 CDHash 值,用于跟 signedAttrs 里的内容进行对比。而更关键的是对 signedAttrs 的加密验证,实际验证流程比较复杂,感兴趣的小伙伴可以阅读这篇细说iOS代码签名(三)

我结合文中签名校验内容和上面的代码插槽,画出了表示签名校验的整个流程:

这里有两处 Hash 对比,一个是对 signedAttrs 的解密,确保其是可信任的。另一处是 CDHash 的对比,确保代码未被修改。

signerInfo 里包含了 signedAttrs 、签名使用的 Hash 算法、加密算法、签名数据等信息。再结合证书里的公钥,我们就可以验证,signedAttrs 的有效性。

授权

除了确保代码的真实性和完整性,代码签名还为苹果公司及其强大的安全机制提供了授权(entitlement)功能。授权文件也被包含在签名里,其散列值放在索引为-5的插槽中。授权文件是一个 XML 格式的文件,我们可以使用 jtool --ent 查看其内容,因为 ls 没有授权文件,我们以 Mac 端微信为例进行查看:

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
$ jtool -arch x86_64 --ent /Applications/WeChat.app/Contents/MacOS/WeChat
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>5A4RE8SF68.com.tencent.xinWeChat</string>
</array>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.device.microphone</key>
<true/>
<key>com.apple.security.files.downloads.read-write</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.personal-information.location</key>
<true/>
</dict>
</plist>

这里我们可以看到其中包含了沙盒、application-groups、声音输入、摄像头等一系列权限。在应用访问特定 API 的时候苹果可以根据这些授权判定该行为是否合法。因为苹果公司是应用的终极签名者,所以签名过程中也可以很容易的修改授权,比如 com.apple.security.sandbox.container-required 这一表示沙盒权限的值就会被强制安置到授权文件中。

强制验证代码签名

为了使代码签名真正有效,非常重要的一步就是要保证验证过程顺利执行,没有遗漏。当前签名验证是发生在内核模式下,而非用户模式下。签名的验证发生在两个阶段:加载可执行文件时、实际访问二进制代码时(Page Fault)。分成两个阶段也是出于性能方面的考虑,因为二进制文件是动态加载的,对于还没加载的部分仅当其加载如内存时,也即发生 Page Fault 时再进行签名验证。

可执行文件的加载

可执行文件的加载出现在execve()/mac_execve()posix_spawn()系统调用被触发的时候。对于MachO,exec_mach_imgact()会被调用,在解析文件时它会找到LC_CODE_SIGNATURE的位置。代码签名二进制块会被加载到内核的统一高速缓存缓冲区中。

Page Fault时的处理

可以查看 osfmk/vm/vm_fault.c 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* CODE SIGNING:
* When soft faulting a page, we have to validate the page if:
* 1. the page is being mapped in user space
* 2. the page hasn't already been found to be "tainted"
* 3. the page belongs to a code-signed object
* 4. the page has not been validated yet or has been mapped for write.
*/
#define VM_FAULT_NEED_CS_VALIDATION(pmap, page, page_obj) \
((pmap) != kernel_pmap /*1*/ && \
!(page)->cs_tainted /*2*/ && \
(page_obj)->code_signed /*3*/ && \
(!(page)->cs_validated || (page)->wpmapped /*4*/))

当 Page Fault 满足以上条件时将触发签名验证过程:

1、该页面正在用户空间中映射

2、这个页面还没有被发现为 tainted

3、该页属于一个代码签名对象

4、页面还没有被验证,或者还没有被映射为可写状态

代码签名的漏洞

代码签名机制虽然强大,保护着应用的安全,但依然被攻破过,以下讲解几例曾经出现的漏洞。

JIT(即时生成代码)

该情况发生在 Page Fault 过程,如果该页内容是用于 JIT,将会被特殊标记,可以创建和执行任意代码,而无需代码签名。

从 iOS 10 开始,苹果公司开始在64位的设备上加固 JIT。采用专门的 memcpy() 将JIT映射到可执行但不可读的内存上,然后可执行的 JIT 映射为不可写,可写的 JIT 映射为不可执行状态。

Jekyll 应用

Jekyll 应用的含义是应用在提交至 App Store 时表现为无害,但其实它包含恶意功能,只不过这些功能处于休眠状态。过审之后和本地服务器进行合作,自愿公开其地址空间和符号,通过代码注入或者返回导向编程(Return Oriented Programming,ROP),触发预置的恶意程序。

目前还没有可靠的打击 ROP 的方法,但因为沙盒机制的缘故,恶意代码的影响范围是可控的。

苹果公司使用 LLVM BitCode 向 App Store 提交应用的方案,也会使恶意应用难以事先知晓其地址空间。

内存锁定

从上面我们知道发生Page Fault会触发签名验证的流程,那如果没有Page Fault就不会存在签名验证了。按照mmap -> mlock -> memcpy -> mprotect 的调用顺序,应用可以修改可执行内存,以任何看起来合适的方式修补内存。虽然XNU通常会阻止将曾经可写的内存设置为r-x,但当内存锁定时,会绕过该检测。

苹果在iOS 9.3中修复了这个漏洞。

总结

我们再来尝试回答开头上面遗留的问题:

1、如何验证代码的来源是否合法?

主要通过证书来验证来源是否合法,所有的开发者证书都由苹果颁发,且被 Root CA 认证。另外依托于需求(requirements),还可以再扩展一些其他验证方式。

2、如何确认代码是否被修改过。

主要通过代码插槽和 CDHash,再对 CDHash 进行签名,就可确认其是否被修改过。注意实际验证流程有两处关键的 Hash 比对,可以再结合上面的流程图加深理解。

iOS摸鱼周报 第二十八期

iOS摸鱼周报 第二十八期

本期概要

  • 话题:跟 yuriko 聊一下职业选择和如何保持学习热情。
  • Tips:介绍缓动函数相关的一些内容。
  • 面试模块:Objective-C 的消息机制(上)。
  • 优秀博客:整理了几篇 Swift 协议相关的优秀文章。
  • 学习资料:两个高质量的学习仓库,用涂鸦绘画的形式讲解编程知识和 raywenderlich 出品的 Swift 编码指南。
  • 开发工具:Xcode 下载管理工具 XcodesApp。

本期话题

@zhangferry:yuriko 是群里非常活跃也很有趣的一位小伙伴,工作时间还不是太长但学习热情很高,一起来了解下他是如何看待学习这件事的。

zhangferry:简单介绍下自己吧。

大家好,我就是我们成长之路群里自诩瓜皮的同学,为什么我自诩瓜皮呢,因为我 19 年毕业,就工作经验而言对于很多前辈而言还是稚嫩了些。刚毕业时去了一个 P2P 背景的公司,后来 P2P 在疫情前没几个月就倒了,急于找到一份工作,去了一家传统行业做开发,然后今年跳槽去了家互联网股票头部公司。

zhangferry:你之前在传统企业也工作过,传统企业跟互联网公司工作的感受有什么不同。

就拿我之前的公司而言,最大的不同就是对开发过程的重视,对技术的重视程度不同。

虽然我之前待的传统行业,就这家公司而言,员工也是不少,但是对于开发部门来说,完全就是个小作坊。没有合理的生产流程,只注重生产出产品(其实也不是很注重)。一个人负责的东西很杂,测试离职后就只能自测,甚至有一个星期被迫下店当了店员(一生黑)。其实我之前早就想要离开那里,可是碍于疫情只能多留了一年。

现在在这家公司,最大的感受就是流程规范了许多,公司重视技术,有定期的技术分享,生产流程也规范了许多,也有内部的自动化平台,目前也有机会参与公司的自动化流程构建优化(脚本自动化打包等),感觉在这里可以接触到学习到很多东西。

zhangferry:感觉你兴趣范围挺广的,逆向、算法,这些由兴趣推动的技术方向,你是如何保热情持续不断的学习的呢?

也可以说是一种有目标,也是为了成功的成就感。

在我看清了前司要离开却迫于疫情留下后,我就知道要为以后做打算了,于是我每天开始刷算法题,每个模块有目的性的做下来,然后参照题解,分析自己的时空复杂度是否有优化空间。在换工作前做了300 余道算法题,刷算法的同时感受到了算法思维的重要性。

逆向的学习也是机缘巧合,当时我的好基友有一款付费办公软件找我,希望我能破解。当时我就决定去学习这部分的知识。学了一段时间,也买了 Hopper 作为分析工具,帮基友破解掉了里面的内购付费功能。为了简化部分重复的工作,抽了一段时间学习写 Shell 脚本(也稍微了解了 Python,以后会详细学习),然后自写了一套重签名脚本。软件破解成功以后我真的是满满的成就感。

zhangferry:有了学习动力还需要一些学习方法,分享一些你的学习方法吧。

我认为最重要的是要有目的性,当初我决定要跳槽后,基本上能抽的空闲时间都抽出来了,地铁上刷 MJ 的视频,回去以后打开 Leetcode 刷题,每天制定学习的时长,时间不到不能进行娱乐活动。然后只玩休闲类益智类游戏,保证不会在游戏上花掉太多时间。像现在的话,我虽然已经没有跳槽的目的性,不过最近 *OS internals part3 译本已经出来了,我也购入了一本,当前目标就是先读完这本书。虽然里面有很多陌生的概念,也磕磕绊绊的看了一百余页。所以对我而言,目的性是我学习的最大动力。

zhangferry:说一下最近的思想感悟吧。

主要就是想以我自己的经历,向跟我差不多年龄的同学分享下,一定做好职业规划。之前我去那传统公司也是看人比较多,结果却大失所望,走了不少弯路。但是也是因为在那个公司,我才明白了只有提升自己才能进入好企业来摆脱它。

之前看群里的同学们,有分享过一些开发者视频(油管上的),都是英文的。感觉虽然工作了,英语也是相当重要,建议英文基础薄弱些的同学还是花些精力在这里,毕竟一手的资料比二手的译本更能代表原作者的意思是吧。

如果有什么想跟我聊的欢迎在群里骚扰我哈,基本秒回^_^

开发Tips

整理编辑:zhangferry

缓动函数

很多动画为了效果更加自然,通常都不是线性变化的,而是先慢后快,或者先慢后快再慢的速度进行的。在 iOS 开发里会用 UIView.AnimationOptions 这个枚举值进行描述,它有这几个值:

1
2
3
4
5
6
public struct AnimationOptions : OptionSet {
public static var curveEaseInOut: UIView.AnimationOptions { get } // default
public static var curveEaseIn: UIView.AnimationOptions { get }
public static var curveEaseOut: UIView.AnimationOptions { get }
public static var curveLinear: UIView.AnimationOptions { get }
}

ease 表示减缓,所以 easeInOut 表示,进入和完成都是减缓的,则中间就是快速的,就是表示先慢后快再慢。那这个先慢后快,或者先快后慢的过程具体是如何描述的呢?这里就引入了缓动函数,缓动函数就是描述这一快慢过程的函数,其对应三种状态:easeIn、easeOut、easeInOut。

缓动函数并非特定的某一个函数,它有不同的拟合方式,不同形式的拟合效果可以参看[下图](https://easings.net/ “easings.net”):

缓动函数名例如 easeInSine 后面的 Sine 就是拟合类型,其对应的就是三角函数拟合。常见的还有二次函数 Quad,三次函数 Cubic 等。以上函数有对应的 TypeScript 源码,有了具体的计算规则,我们就可以将缓动效果应用到颜色渐变等各个方面。以下是三角函数和二次函数拟合的 Swift 版本:

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
struct EasingsFunctions {
/// sine
static func easeInSine(_ x: CGFloat) -> CGFloat {
return 1 - cos((x * CGFloat.pi) / 2)
}
static func easeOutSine(_ x: CGFloat) -> CGFloat {
return sin((x * CGFloat.pi) / 2)
}
static func easeInOutSine(_ x: CGFloat) -> CGFloat {
return -(cos(CGFloat.pi * x) - 1) / 2
}
/// quad
static func easeInQuad(_ x: CGFloat) -> CGFloat {
return x * x
}
static func easeOutQuad(_ x: CGFloat) -> CGFloat {
return 1 - (1 - x) * (1 - x)
}
static func easeInOutQuad(_ x: CGFloat) -> CGFloat {
if x < 0.5 {
return 2 * x * x
} else {
return 1 - pow(-2 * x + 2, 2) / 2
}
}
}

面试解析

整理编辑:师大小海腾

本期面试解析讲解的知识点是 Objective-C 的消息机制(上)。为了避免篇幅过长这里不会展开太细,而且太细的笔者我也不会😅,网上相关的优秀文章数不胜数,如果大家看完还有疑惑🤔一定要去探个究竟🐛。

消息机制派发

“消息机制派发” 是 Objective-C 的消息派发方式,其 “动态绑定” 机制让所要调用的方法在运行时才确定,支持开发者使用 “method-swizzling”、“isa-swizzling” 等黑魔法来在运行时改变调用方法的行为。除此之外,还有 “直接派发”、“函数表派发” 等消息派发方式,这些方式在 Swift 中均有应用。

“消息” 这个词好像不常说,更多的是称之为 “方法”。其实,给某个对象 “发送消息” 就相当于在该对象上“ 调用方法”。完整的消息派发由 接收者选择子参数 构成。在 Objective-C 中,给对象发送消息的语法为:

1
id returnValue = [someObject message:parameter];

在这里,someObject 叫做 接收者,message 叫做 选择子选择子参数 合起来称为 消息。编译器看到此消息后,会将其转换为一条标准的 C 语言函数调用,所调用的函数为消息机制的核心函数 objc_msgSend

1
void objc_msgSend(id self, SEL _cmd, ...)

该函数参数个数可变,能接受两个或两个以上参数。前面两个参数 self 消息接收者_cmd 选择子 即为 Objective-C 方法的两个隐式参数,后续参数就是消息中的那些参数(也就是方法显式参数)。

Objective-C 中的方法调用在编译后会转换成该函数调用,比如以上方法调用会转换为:

1
id returnValue = objc_msgSend(someObject, @selector(message:), parameter);

除了 objc_msgSend,还有其它函数负责处理边界情况:

  • objc_msgSend_stret:待发送的消息返回的是结构体
  • objc_msgSend_fpret:待发送的消息返回的是浮点数
  • objc_msgSendSuper:给父类发消息
  • ……

在讲了一大段废话之后(废话居然占了这么大篇幅 wtm),该步入重点了,objc_msgSend 函数的执行流程是什么样的?

objc_msgSend 执行流程通常分为三大阶段:消息发送动态方法解析消息转发。而有些地方又将 动态方法解析 阶段归并到 消息转发 阶段中,从而将其分为了 消息发送消息转发 两大阶段,比如《Effective Objective-C 2.0》。好吧,其实我也不知道哪种是通常😅。

消息发送

  • 判断 receiver 是否为 nil,是的话直接 return,这就是为什么给 nil 发送消息却不会 Crash 的原因。
  • 去 receiverClass 以及逐级遍历的 superclass 中的 cache_t 和 class_rw_t 中查找 IMP,找到就调用。如果遍历到 rootClass 还没有找到的话,则进入 动态方法解析 阶段。
  • 该阶段还涉及到 initialize 消息的发送cache_t 缓存添加、扩容 等流程。

动态方法解析

消息转发

由于篇幅原因,剩下的内容我们下期再见吧👋。

优秀博客

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

1、Swift 协议 – 来自:Swift 编程语言中文教程

@我是熊大:在学习面向协议编程前,先了解 Swift 中的协议该如何使用。

2、面向协议编程与 Cocoa 的邂逅 (上) – 来自:OneV’s Den

@皮拉夫大王:文章先通过引入例子介绍 OOP 的核心思想:封装、继承。随后介绍 OOP 中 “Cross-Cutting Concerns”、多继承的菱形缺陷问题、动态派发的安全问题这三大困境。面向协议编程可以解决除菱形问题外的其他问题。

3、面向协议编程与 Cocoa 的邂逅 (下) – 来自:OneV’s Den

@我是熊大:作者使用协议演示了基于 Protocol 的网络请求,然后又回答了工作中的使用场景,正如作者所言:”通过面向协议的编程,我们可以从传统的继承上解放出来,用一种更灵活的方式,搭积木一样对程序进行组装”。

4、Swift Protocol 详解 - 协议&面向协议编程 – 来自掘金:RickeyBoy

@皮拉夫大王:文章概念性的东西较多,本文先介绍了协议的基本使用方法,主要介绍耦合相关的概念,例如耦合的 5 个级别、耦合带来的问题、依赖翻转和协议解耦等。

5、如果你还在用子类(Subclassing),那就不对了 – 来自简书:97c49dfd1f9f

@皮拉夫大王:本文主要介绍了面向协议、面向对象、函数式编程的优缺点。OC->Swift 不仅仅是语法上的变化,想想大家项目中的 xxxBasexxx.m,如果用 Swift开发需要避免再出现此类情况。

6、Swift 中的面向协议编程:是否优于面向对象编程? – 来自:SwiftGG

@我是熊大:引用作者的一句话:”30 年的开发经验,让我能够平心静气地说,你应该了解协议和 POP。开始设计并书写你自己的 POP 代码吧“。

学习资料

整理编辑:Mimosa

a-picture-is-worth-a-1000-words

地址:https://github.com/girliemac/a-picture-is-worth-a-1000-words

把复杂知识放进简单涂鸦!该仓库中用可爱的涂鸦绘制了包涵数据结构、算法、机器学习入门、web 基础开发的一些知识,画风可爱,简单易懂。但要下载的时候要注意一下,涂鸦图片很大。

raywenderlich/swift-style-guide

地址:https://github.com/raywenderlich/swift-style-guide

来自 raywenderlich 的 Swift 代码风格指南,其风格的重点在于印刷和网页版的可读性,这个风格指南是为了保持他们的书籍、教程和入门套件中的代码的优雅和一致性。可以供大家有特别需要时参考和借鉴。

工具推荐

整理编辑:brave723

XcodesApp

地址https://github.com/RobotsAndPencils/XcodesApp

软件状态: 免费,开源

软件介绍

AppStore 自带的升级功能经常因为某些奇怪的原因卡住而被吐槽,如果你也经历过这些事情可以试下 Xcodes。Xcodes 是一个 Xcode 下载管理器,支持下载不同版本的 Xcode,还可以切换默认版本。如果你喜欢命令行,还可以使用其命令行版本进行安装。

关于我们

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

往期推荐

iOS摸鱼周报 第二十七期

iOS摸鱼周报 第二十六期

iOS摸鱼周报 第二十五期

iOS摸鱼周报 第二十四期

iOS摸鱼周报 第二十七期

iOS摸鱼周报 第二十七期

本期概要

  • 话题:跟 RunsCode 聊聊编程和兴趣爱好,以及如何在 1min 之内复原魔方😏。

  • Tips:iOS 识别虚拟定位调研;使用 App Store Connect API Key 解决 Fastlane 双重验证问题。

  • 面试模块:KVC 取值和赋值过程的工作原理。

  • 优秀博客:关于 Combine 的相关文章推荐。

  • 学习资料:阮一峰最新发布的 C 语言入门教程,GitHub 漫游指南。

  • 开发工具:Xcode 工程管理工具 XcodeProjects。

本期话题

@zhangferry:这期参与访谈的对象是摸鱼周报的另一个编辑:RunsCode,RunsCode 在我看来是一个非常酷的人。这个酷来自于他很强的学习能力和个人要求,编程和兴趣爱好都能被他玩出花来。下面通过几个问题,一起了解下他。

zhangferry:简单介绍下自己吧。

Hello,大家好我是 RunsCode,目前就职于 Hello出行,坐标杭州。

小时候因为沉迷于电脑游戏,毕业后入坑做手游,后来机缘巧合之下结识 iOS,从此移情别恋了,跟 iOS 相爱相杀至今。

zhangferry:你的学习范围比较广,安卓、Ruby、Applescript 都有写过,你是在什么场景下接触一门新语言的?学习一门新的语言,首先应该关注哪部分内容?

因为之前做的 cpp 游戏要做移动端跨平台移植对接移动支付 SDK,被迫学了 Android 和 iOS,但是还是被 iOS 的纯粹给吸引了。后来在 15 年搞 iOS 的过程中来了一个 CSDN 当时前十的大神当我们领导,开拓视野学习 Swift 和 Ruby,因为他说 Swift 借鉴了很多 Ruby 的特性,再加上 CocoaPods 也是 Ruby 写的,也就稍稍学习了一下。AppleScript 也是这个 CSDN 大神提醒的搞 Mac 自动化,后来也是一发不可收拾。

这些语言对我而言他们共性是让我写起来很开心,就像那种小孩子看到好吃的那种感觉(主要说 Swift 和 Ruby)
其实学语言都是有针对的学习,不同的语言都是有自己擅长的领域,比如说我要用 Ruby 写一个 iOS App,这就有点过分了,相比 OC/Swift 这就不是很适合了,虽说有点过分,但是真的有人这么干了,可以了解下 RubyMotion,真是极客啊。

个人觉得学习一门新语言除了一开始掌握基本的词法语法之外,慢慢的你就会发现这个语言跟你有没有共鸣,你的思路跟它的处理逻辑是不是很契合,就像大家都喜欢脚本 Python 我却更喜欢 Ruby,这主要是Ruby 的处理逻辑跟我很契合,就是同样的展示你用德玛习惯,我却用吕布更加习惯。

zhangferry:听说你乒乓球和魔方都玩的很好,这些东西的学习跟编程有没有什么共性呢?我之前也着迷过一段时间魔方(三阶),但最快也要 2min 以上,听说你的记录是 15s。由不会到会再到熟练是相对简单的过程,一般人都能做到,但再往上突破就很难了。就魔方来说,1min 对我来说就是一个大的突破了,如果要达到这一步需要做哪些事情呢?

乒乓球会一点,请过教练专门训练过基础技能,不能说玩的很好,跟小区大爷打球有时候经常翻车的,尤其打长胶翻车不少,哎……

魔方这个吧,就是比一般复原的稍微快一点,三阶止步 CFOP。

跟编程的共性:兴趣第一, 然后就是坚持,用正确的方法刻意练习就是了(高手都是寂寞的)。

一分钟还原三阶魔方来说,学会七步还原法,然后在苦练一个礼拜,每天练习两个小时应该就差不多了。当然掌握正确手法的天赋型选手也许只要两三天。

然后最重要的事情说三遍,买个好魔方, 买个好魔方, 买个好魔方!!!
千万不要买路边摊十块钱那种啊,那种容易卡住或者 POP(就是爆炸开了),及其打击自信,过来人血的教训。

怎么的也得三十块起步,国甲,孤鸿,圣手这种吧。

zhangferry:学习很多时候并不是有趣的,该如何保持学习热情?

这个我不知道怎么回答,或许是好奇,或许是焦虑,或许是爱好兴趣。我只知道是这三点支配了我。还有就是我只做自己喜欢的事情,我从不跟风,跟风容易迷失自己,适合别人的不一定适合自己。

主要还是要坚持,坚持下去能让人更坚定。时间长了你就习惯学习了,如果突然一下子你不学习,你是不是都会感到空虚、焦虑和内疚吧,哈哈哈。

zhangferry:说一个最近的思想感悟吧。

提升生活幸福感还是要终身学习,不仅限于工作技能,其他一切都可以。

开发Tips

整理编辑:FBY展菲zhangferry

iOS 识别虚拟定位调研

前言

最近业务开发中,有遇到我们的项目 App 定位被篡改的情况,在 Android 端表现的尤为明显。为了防止这种黑产使用虚拟定位薅羊毛,iOS 也不得不进行虚拟定位的规避。

第一种:使用越狱手机

一般 App 用户存在使用越狱苹果手机的情况,一般可以推断用户的行为存在薅羊毛的嫌疑(也有 App 被竞品公司做逆向分析的可能),因为买一部越狱的手机比买一部正常的手机有难度,且在系统升级和 Appstore 的使用上,均不如正常手机,本人曾经浅浅的接触皮毛知识通过越狱 iPhone5s 进行的 App 逆向。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// 判断是否是越狱设备
/// - Returns: true 表示设备越狱
func isBrokenDevice() -> Bool {

var isBroken = false

let cydiaPath = "/Applications/Cydia.app"

let aptPath = "/private/var/lib/apt"

if FileManager.default.fileExists(atPath: cydiaPath) {
isBroken = true
}

if FileManager.default.fileExists(atPath: aptPath) {
isBroken = true
}

return isBroken
}

第二种:使用爱思助手

对于使用虚拟定位的场景,大多应该是司机或对接人员打卡了。而在这种场景下,就可能催生了一批专门以使用虚拟定位进行打卡薅羊毛的黑产。对于苹果手机,目前而言,能够很好的实现的,当数爱思助手的虚拟定位功能了。

使用步骤: 下载爱思助手 Mac 客户端,连接苹果手机,工具箱中点击虚拟定位,即可在地图上选定位,然后点击修改虚拟定位即可实现修改地图的定位信息。

原理: 在未越狱的设备上通过电脑和手机进行 USB 连接,电脑通过特殊协议向手机上的 DTSimulateLocation 服务发送模拟的坐标数据来实现虚假定位,目前 Xcode 上内置位置模拟就是借助这个技术来实现的。

识别方式

一、通过多次记录爱思助手的虚拟定位的数据发现,其虚拟的定位信息的经纬度的高度是为 0 且经纬度的数据位数也是值得考究的。

二、把定位后的数据的经纬度上传给后台,后台再根据收到的经纬度获取详细的经纬度信息,对司机的除经纬度以外的地理信息进行深度比较,优先比较 altitudehorizontalAccuracyverticalAccuracy 值,根据值是否相等进行权衡后确定。

三、具体识别流程

  • 通过获取公网 ip,大概再通过接口根据 ip 地址可获取大概的位置,但误差范围有点大。
  • 通过 Wi-Fi 热点来读取 App 位置
  • 利用 CLCircularRegion 设定区域中心的指定经纬度和可设定半径范围,进行监听。
  • 通过 IBeacon 技术,使用 CoreBluetooth 框架下的 CBPeripheralManager 建立一个蓝牙基站。这种定位直接是端对端的直接定位,省去了 GPS 的卫星和蜂窝数据的基站通信。

四、iOS 防黑产虚假定位检测技术

文章的末尾附的解法本人有尝试过,一层一层通过 KVC 读取 CLLocation 的 _internal 的 fLocation,只能读取到此。

参考:iOS 识别虚拟定位调研

Fastlane 使用 App Store Connect API Key 解决双重验证问题

现在申请的 AppleId 都是要求必须要有双重验证的,这在处理 CI 问题时通常会引来麻烦,之前的解决方案使用 FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORDFASTLANE_SESSION,但 FASTLANE_SESSION 具有时效性,每过一个月就需要更新一次,也不是长期方案。Fastlane 在 2.160.0 版本开始支持 Apple 的 App Store Connect API 功能。App Store Connect API 由苹果提供,需登录 App Store Connect 完成授权问题。使用方法如下:

1、在 这里 创建共享秘钥。

请求权限:

创建秘钥:

这里的 .p8 秘钥文件只能下载一次,注意保存。

2、fastfile 的配置。

可以直接用 app_store_connect_api_key 对象配置,也可以写成 json 供多个 lane 共享,这里推荐使用 json 形式管理,新建一个json文件,配置如下内容:

1
2
3
4
5
6
7
{
"key_id": "D383SF739",
"issuer_id": "6053b7fe-68a8-4acb-89be-165aa6465141",
"key": "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHknlhdlYdLu\n-----END PRIVATE KEY-----",
"duration": 1200,
"in_house": false
}

前面三项都是对秘钥文件的描述,可以根据自己的项目进行修改。这里需注意 key 的内容,原始 .p8 文件是带换行的,转成字符串时用 \n 表示换行。注意这里的值为 key,官网写法是 key_content,这是官网的错误,我开始也被坑了,已经有人提出了 issues 19341

基本所有需要登录 app conenct 的命令都包含 api_key_path 这个参数,传入 json 文件路径即可:

1
2
3
lane :release do
pilot(api_key_path: "fastlane/D383SF739.json" )
end

参考:fastlane app-store-connect-api documents

面试解析

整理编辑:师大小海腾

本期面试解析讲解的知识点是 KVC 取值和赋值过程的工作原理

Getter

以下是 valueForKey: 方法的默认实现,给定一个 key 作为输入参数,在消息接收者类中操作,执行以下过程。

  • ① 按照 get<Key><key>is<Key>_<key> 顺序查找方法。

    如果找到就调用取值并执行 ⑤,否则执行 ②;
  • ② 查找 countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes: 命名的方法。

    如果找到第一个和后面两个中的至少一个,则创建一个能够响应所有 NSArray 的方法的集合代理对象(类型为 NSKeyValueArray,继承自 NSArray),并返回该对象。否则执行 ③;
    • 代理对象随后将其接收到的任何 NSArray 消息转换为 countOf<Key>objectIn<Key>AtIndex:<Key>AtIndexes: 消息的组合,并将其发送给 KVC 调用方。如果原始对象还实现了一个名为 get<Key>:range: 的可选方法,则代理对象也会在适当时使用该方法。
  • ③ 查找 countOf<Key>enumeratorOf<Key>memberOf<Key>: 命名的方法。

    如果三个方法都找到,则创建一个能够响应所有 NSSet 的方法的集合代理对象(类型为 NSKeyValueSet,继承自 NSSet),并返回该对象。否则执行④;
    • 代理对象随后将其接收到的任何 NSSet 消息转换为 countOf<Key>enumeratorOf<Key>memberOf<Key>: 消息的组合,并将其发送给 KVC 调用方。
  • ④ 查看消息接收者类的 +accessInstanceVariablesDirectly 方法的返回值(默认返回 YES)。如果返回 YES,就按照 _<key>_is<Key><key>is<Key> 顺序查找成员变量。如果找到就直接取值并执行 ⑤,否则执行 ⑥。如果 +accessInstanceVariablesDirectly 方法返回 NO 也执行 ⑥。
  • ⑤ 如果取到的值是一个对象指针,即获取的是对象,则直接将对象返回。
    • 如果取到的值是一个 NSNumber 支持的数据类型,则将其存储在 NSNumber 实例并返回。
    • 如果取到的值不是一个 NSNumber 支持的数据类型,则转换为 NSValue 对象, 然后返回。
  • ⑥ 调用 valueForUndefinedKey: 方法,该方法抛出异常 NSUnknownKeyException,程序 Crash。这是默认实现,我们可以重写该方法对特定 key 做一些特殊处理。

Setter

以下是 setValue:forKey: 方法的默认实现,给定 keyvalue 作为输入参数,尝试将 KVC 调用方 key 的值设置为 value,执行以下过程。

  • ① 按照 set<Key>:_set<Key>: 顺序查找方法。

    如果找到就调用并将 value 传进去(根据需要进行数据类型转换),否则执行 ②。
  • ② 查看消息接收者类的 +accessInstanceVariablesDirectly 方法的返回值(默认返回 YES)。如果返回 YES,就按照 _<key>_is<Key><key>is<Key> 顺序查找成员变量(同 Getter)。如果找到就将 value 赋值给它(根据需要进行数据类型转换),否则执行 ③。如果 +accessInstanceVariablesDirectly 方法返回 NO 也执行 ③。
  • ③ 调用 setValue:forUndefinedKey: 方法,该方法抛出异常 NSUnknownKeyException,程序 Crash。这是默认实现,我们可以重写该方法对特定 key 做一些特殊处理。

优秀博客

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

1、深入浅出 Apple 响应式框架 Combine – 来自 InfoQ:青花瓷的平方

@我是熊大:本文是 Joseph Heck 编写的教程的中文版本,适合新手阅读,学习 Combine。

2、Combine debugging using operators in Swift – 来自博客:avanderlee

@我是熊大:使用 RxSwift 会产生大量的不可读堆栈信息,这也是开发人员放弃 RxSwift 的原因之一,在 Combine 中这一点依旧如此。但好在有一些提示和技巧可以改善,本文就介绍了几种方式。

3、Combine: Getting Started – 来自:raywenderlich

@我是熊大:Swift Combine 的硬核教程,作者利用 UnsplashAPI 带大家实现了一个简易的 App,让我们学习了解如何使用 Combine 的发布者和订阅者来处理事件流、合并多个发布者等。

4、Combine - 介绍、核心概念 – 来自知乎:Talaxy

@皮拉夫大王:提到响应式编程就不得不说 Combine。这篇文章介绍了Combine 的相关概念和用法。包括发布者-订阅者的生命周期、发布者订阅者操作者的概念等等。

5、Apple 官方异步编程框架:Swift Combine 应用 – 来自:Nemocdz’s Blog

@皮拉夫大王:本文通过例子和代码介绍了 Combine 的用法,适合了解 Combine 相关概念和基础的同学阅读。

6、RxSwift to Combine Cheatsheet – 来自 GitHub:CombineCommunity

@皮拉夫大王:RxSwift 与 Combine 的对照关系,如果你想从 RxSwift 过渡到 Combine,可以参考此文章。

学习资料

整理编辑:Mimosa

阮一峰的《C 语言入门教程》

地址:https://wangdoc.com/clang

阮一峰最近新写的《C 语言入门教程》,他对该教程做了一些介绍可以看 这里,这对想重拾 C 语言这一门手艺的读者来说一定是一个巨大的帮助。同时各位读者若发现错误和遗漏,欢迎大家到仓库提交补丁。

Github 漫游指南

地址:http://github.phodal.com/

如果你是一名 Github 新手的话,这本《Github 漫游指南》将会带你漫游 Github 的世界,带你了解 Github 到底是什么,他有什么用,该怎么去使用它。如果你是一名老手了,它也可以带你深入平时可能不会注意的细节,帮助你更加了解这个我们每天都在使用的工具。

工具推荐

整理编辑:brave723

XcodeProjects

地址https://github.com/DKalachniuk/XcodeProjects

软件状态: 免费,开源

软件介绍

日常开发过程中,经常在终端中执行 pod install、pod update、或者 clear derived data 等操作,XcodeProjects 工具执行这些操作,只需要点击两下就能完成,还能为自己的项目自定义 command,很大程度的简化我们的操作步骤,节省开发时间。

关于我们

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

往期推荐

iOS摸鱼周报 第二十六期

iOS摸鱼周报 第二十五期

iOS摸鱼周报 第二十四期

iOS摸鱼周报 第二十三期

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摸鱼周报 第二十期