iOS摸鱼周报 第四十五期

iOS摸鱼周报 第四十五期

本期概要

  • 话题:苹果公司宣布暂停在俄销售产品并关闭部分功能
  • Tips:在 SPM 集成 SwiftLint
  • 面试模块:Swift 的 weak 是如何实现的?
  • 优秀博客:iOS项目中的脚手架/CLI介绍
  • 学习资料:Swift 实现的设计模式
  • 开发工具:nginxedit:Nginx 在线配置工具

本期话题

@zhangferry

苹果公司宣布暂停在俄销售产品并关闭部分功能

根据 CNBC 的报道,苹果公司在3月1号表示,已停止在俄罗斯的产品销售。与此同时,属于俄官方媒体的两款应用被下架,该地区 Apple Pay 等功能受限。以下是苹果发言人的原话:

We have taken a number of actions in response to the invasion. We have paused all product sales in Russia. Last week, we stopped all exports into our sales channel in the country. Apple Pay and other services have been limited. RT News and Sputnik News are no longer available for download from the App Store outside Russia. And we have disabled both traffic and live incidents in Apple Maps in Ukraine as a safety and precautionary measure for Ukrainian citizens.

有很多人说应该支持国产手机了,但国产也是魔改的安卓系统,这虽没有像苹果那样被牢牢掌控,也并非完全的可控。这几天不只是苹果,谷歌、推特、台积电、英特尔,甚至连开发社区 Github、开源库 React 都在抵制俄罗斯,「科技无国界」已经完全沦为谎言,这不禁令人惶恐。

现代战争是复杂的,它不只是枪炮还会伴随着各类舆论战、信息战,而信息战的主动权就掌握在拥有核心技术的一方。反观俄罗斯,类似的事情是不是也会发生在我们身上?由此事件引发的思考是,仅仅用言语冲了某个社区留言板是不够的,打破垄断,不断提高我们自己的核心技术能力才是王道。科技强国,吾辈当自强!

开发Tips

获取 Build Setting 对应的环境变量 Key

整理编辑:zhangferry

Xcode 的 build setting 里有很多配置项,这些配置项都有对应的环境变量,当我们要用脚本自定义的话就需要知道对应的环境变量 Key是哪个才好设置。比如下面这个 Header Search Paths

其对应的 Key 是 HEADER_SEARCH_PATHS。那如何或者这个 Key 呢,除了网上查相关资料我们还可以通过 Xcode 获取。

方法一(由@CodeStar提供)

选中该配置项,展开右部侧边栏,选中点击帮助按钮就能够看到这个配置的说明和对应的环境变量名称。

方法二

选中该配置,按住 Option 键,双击该配置,会出现一个描述该选项的帮助卡片,这个内容与上面的帮助侧边栏内容一致。

在 SPM 集成 SwiftLint

整理编辑:FBY展菲

SwiftLint 介绍

SwiftLint 是一个实用工具,用于实现 Swift 的风格。 在 Xcode 项目构建阶段,集成 SwiftLint 很简单,构建阶段会在编译项目时自动触发 SwiftLint。

遗憾的是,目前无法轻松地将 SwiftLintSwift Packages 集成,Swift Packages 没有构建阶段,也无法自动运行脚本。

下面介绍如何在 Xcode 中使用 post action 脚本在成功编译 Swift Package 后自动触发 SwiftLint。

SucceedsPostAction.sh 是一个 bash 脚本,用作 Xcode 中的 “Succeeds” 发布操作。当你编译一个 Swift 包时,这个脚本会自动触发 SwiftLint

SwiftLint 安装

  1. 在 Mac 上下载脚本 SucceedsPostAction.sh

  2. 确保脚本具有适当的权限,即运行 chmod 755 SucceedsPostAction.sh

  3. 如果要使用自定义 SwiftLint 规则,请将 .swiftlint.yml 文件添加到脚本旁边。

  4. 启动 Xcode 13.0 或更高版本

  5. 打开 Preferences > Locations 并确保 Command Line Tools 设置为 Xcode 版本

  6. 打开 Preferences > Behaviors > Succeeds

  7. 选择脚本 SucceedsPostAction.sh

就是这样:每次编译 Swift 包时,SucceedsPostAction.sh 都会运行 SwiftLint。

演示

存在一些问题

在 Xcode 中运行的 post action 脚本无法向 Xcode 构建结果添加日志、警告或错误。因此,SucceedsPostAction.sh 在 Xcode 中以新窗口的形式打开一个文本文件,其中包含 SwiftLint 报告列表。没有深度集成可以轻松跳转到 SwiftLint 警告。

Swift 5.6

请注意,由于SE-0303: Package Manager Extensible Build Tools,Swift 5.6(在撰写本文时尚不可用)可能会有所帮助。集成 SE-0303 后,不再需要此脚本。

参考:Swift 实用工具 — SwiftLint - Swift社区

面试解析

整理编辑:JY

Swift 的 weak 是如何实现的?

在 Swift 中,也是拥有 SideTable 的,SideTable 是针对有需要的对象而创建,系统会为目标对象分配一块新的内存来保存该对象额外的信息。

对象会有一个指向 SideTable 的指针,同时 SideTable 也有一个指回原对象的指针。在实现上为了不额外多占用内存,目前只有在创建弱引用时,会先把对象的引用计数放到新创建的 SideTable 去,再把空出来的空间存放 SideTable 的地址,会通过一个标志位来区分对象是否有 SideTable

1
2
3
4
5
6
7
class JYObject {
var age :Int = 18
var name:String = "JY"
}
var t = JYObject()
weak var t2 = t
print("----")

我们在print处打上断点,查看 t2 对象

1
2
3
4
5
6
7
8
9
10
(lldb) po t2
▿ Optional<JYObject>
▿ some : <JYObject: 0x6000001a9710>

(lldb) x/8gx 0x6000001a9710
0x6000001a9710: 0x0000000100491e18 0xc0000c00001f03dc
0x6000001a9720: 0x0000000000000012 0x000000000000594a
0x6000001a9730: 0xe200000000000000 0x0000000000000000
0x6000001a9740: 0x00007efd22b59740 0x000000000000009c
(lldb)

通过查看汇编,定义了一个weak变量,编译器自动调用了swift_weakInit函数,这个函数是由WeakReference调用的。说明weak字段在编译器声明的过程当中自动生成了WeakReference对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
WeakReference *swift::swift_weakInit(WeakReference *ref, HeapObject *value) {
ref->nativeInit(value);
return ref;
}

void nativeInit(HeapObject *object) {
auto side = object ? object->refCounts.formWeakReference() : nullptr;
nativeValue.store(WeakReferenceBits(side), std::memory_order_relaxed);
}

template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference() {
// 创建一个 Side Table
auto side = allocateSideTable(true);
if (side)
// 增加一个弱引用
return side->incrementWeak();
else
return nullptr;
}

我们来看一下allocateSideTable方法,是如何创建一个Side Table

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
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting) {
//1.拿到原有的引用计数
auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);

// 判断是否有SideTable,
if (oldbits.hasSideTable()) {
// Already have a side table. Return it.
return oldbits.getSideTable();
}
else if (failIfDeiniting && oldbits.getIsDeiniting()) {
// Already past the start of deinit. Do nothing.
return nullptr;
}

// Preflight passed. Allocate a side table.

// FIXME: custom side table allocator

//2.通过HeapObject创建了一个HeapObjectSideTableEntry实例对象
HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());

//3.将创建的实例对象地址给了InlineRefCountBits,也就是 RefCountBitsT
auto newbits = InlineRefCountBits(side);

do {
if (oldbits.hasSideTable()) {
// Already have a side table. Return it and delete ours.
// Read before delete to streamline barriers.
auto result = oldbits.getSideTable();
delete side;
return result;
}
else if (failIfDeiniting && oldbits.getIsDeiniting()) {
// Already past the start of deinit. Do nothing.
return nullptr;
}

// 将原有的引用计数存储
side->initRefCounts(oldbits);

} while (!refCounts.compare_exchange_weak(oldbits, newbits,
std::memory_order_release,
std::memory_order_relaxed));
return side;
}

总结一下上面所做的事情

1.拿到原有的引用计数
2.通过 HeapObject 创建了一个 HeapObjectSideTableEntry 实例对象
3.将创建的实例对象地址给了InlineRefCountBits,也就是 RefCountBitsT。

构造完 Side Table 以后,对象中的 RefCountBitsT 就不是原来的引用计数了,而是一个指向 Side Table 的指针,然而由于它们实际都是 uint64_t,因此需要一个方法来区分。区分的方法我们可以来看 InlineRefCountBits 的构造函数:

1
2
3
4
5
6
7
8
9
//弱引用
LLVM_ATTRIBUTE_ALWAYS_INLINE
RefCountBitsT(HeapObjectSideTableEntry* side)
: bits((reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits)
| (BitsType(1) << Offsets::UseSlowRCShift)
| (BitsType(1) << Offsets::SideTableMarkShift))
{
assert(refcountIsInline);
}

在弱引用方法中把创建出来的地址做了偏移操作然后存放到了内存当中。

SideTableUnusedLowBits = 3,所以,在这个过程中,传进来的side往右移了 3 位,下面的两个是 62 位和 63 位标记成 1

我们接着来看一下 HeapObjectSideTableEntry 的结构

1
2
3
4
5
6
7
8
9
class HeapObjectSideTableEntry {
// FIXME: does object need to be atomic?
std::atomic<HeapObject*> object;
SideTableRefCounts refCounts;

public:
HeapObjectSideTableEntry(HeapObject *newObject)
: object(newObject), refCounts()
{ }

我们来尝试还原一下拿到弱引用计数 :

0xc0000c00001f03dc62位和63位清0得到 HeapObjectSideTableEntry 实例对象的地址0xC00001F03DC

它既然是右移 3 位,那么我左移 3 位把它还原,HeapObjectSideTableEntry左移三位 得到0x10062AFE0

  • 0x6000001a9710 就是实例对象的地址
  • 0x0000000000000002就是弱引用计数
    这里弱引用为2的原因是因为SideTableRefCountBits初始化的时候从1开始

Side Table的生命周期与对象是分离的,当强引用计数为 0 时,只有 HeapObject 被释放了,并没有释放Side Table,只有所有的 weak 引用者都被释放了或相关变量被置 nil 后,Side Table 才能得以释放。

优秀博客

整理编辑:@我是熊大

本期优秀博客的主题为:脚手架/CLI。在项目最开始的时候,脚手架工具,就会帮你搭建好架子,并生成一些基本代码。脚手架的存在有利于团队统一架构风格,加速项目开发。

1、从0构建自己的脚手架/CLI知识体系 – 来自掘金:IT老班长

@我是熊大:如何生成搭建脚手架呢?本文作者使用 NodeJS,从0开始搭建了一个脚手架,每一步都很详细,介绍了热门脚手架工具库,没有 NodeJS 基础的也能看懂,非常适合作为新手篇入场学习。

2、iOS自动化工具Gckit CLI – 来自博客:SeongBrave

@我是熊大:在项目开发中,大家水平参差不齐,代码风格迥异,尤其是有新人加入团队时,适应期会比较长。那有没有可能让新同学也能像老同学一样,不仅快速进行开发,而且代码风格也近似呢?Gckit CLI 就是为此诞生的,大家在看完上篇文章后就可以对该库进行调整了,打造属于自己的自动化工具

3、Swift + RxSwift MVVM 模块化项目实践 – 来自掘金:SeongBrave

@我是熊大:本文是 Gckit 作者的实践总结,主要讲解通过 CocoaPods 结合 Gckit-CLI 实现开发效率的最大化的一些项目实践。

4、iOS自动化方案附脚本 – 来自掘金:我是熊大

@我是熊大:不同的电脑开发环境不同,多人协作下,因环境不同会导致各种问题,比如 CocoaPods 的版本不同,就会导致某些库无法下载,.lock文件频繁更新等。本文介绍了如何统一开发环境,以及自动化脚本的使用,可以把它放进你的脚手架工具中。文章最后提到了关于脚手架工具的遐想。

学习资料

整理编辑:Mimosa

Swift 实现的设计模式

地址https://oldbird.run/design-patterns/#/

一份由 Swift 语言实现的设计模式教程。其中设计模式的举例清晰明了,代码也简洁易懂,大部分例子中有 UML 图来帮助理解,其中也会有一些对于不同设计模式之间区别与联系的总结和归纳,是很不错的学习设计模式的资源。

工具推荐

整理编辑:CoderStar

nginxedit

地址https://www.nginxedit.cn/

软件状态:免费

软件介绍

Nginx在线配置生成工具,配置高性能,安全和稳定的Nginx服务器的最简单方法。

nginxedit

关于我们

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

往期推荐

iOS摸鱼周报 第四十四期

iOS摸鱼周报 第四十三期

iOS摸鱼周报 第四十二期

iOS摸鱼周报 第四十一期

iOS摸鱼周报 第四十四期

iOS摸鱼周报 第四十四期

本期概要

  • 话题:Apple 将推出 Tap to Pay 功能
  • Tips:解决 iOS 15 上 APP 莫名其妙地退出登录
  • 面试模块:Dealloc 使用注意事项及解析
  • 优秀博客:ARM64 汇编入门及应用
  • 学习资料:Github: How to Cook
  • 开发工具:文件搜索应用:EasyFind

本期话题

@zhangferry:Apple 将在 iPhone 上推出 Tap to Pay 功能,即可以通过简单的操作行为 – 轻触,完成在商户端的付款过程。该功能通过 NFC 实现,非常安全,支持 Apple Pay、非接触式信用卡、借记卡以及其他数字钱包,这意味着 iPhone 将具备类似 POS 的功能,客户可以直接在商户的 iPhone 上刷信用卡进行消费。该功能仅 iPhone XS 及之后的机型支持。

Stripe 将成为第一个在 iPhone 上向其商业客户提供 Tap to Pay 的支付平台。其他支付平台和应用程序将在今年晚些时候推出。

Apple empowers businesses to accept contactless payments through Tap to Pay on iPhone

开发Tips

整理编辑:FBY展菲

解决 iOS 15 上 APP 莫名其妙地退出登录

复现问题

在 iOS 15 正式版推出后, 我们开始收到用户的反馈:在打开我们的App (Cookpad) 时,用户莫名其妙地被强制退出帐号并返回到登录页。非常令人惊讶的是,我们在测试 iOS 15 beta 版的时候并没有发现这个问题。

我们没有视频,也没有具体的步骤来重现这个问题,所以我努力尝试以各种方式启动应用程序,希望能亲手重现它。我试着重新安装应用程序,我试着在有网络连接和没有网络连接的情况下启动,我试着强制退出,经过 30 分钟的努力,我放弃了,我开始回复用户说我没找到具体问题。

直到我再次解锁手机,没有做任何操作,就启动了 Cookpad,我发现 APP 就像我们的用户所反馈的那样,直接退出到了登录界面!

在那之后,我无法准确的复现该问题,但似乎与暂停使用手机一段时间后再次使用它有关。

缩小问题范围

我担心从 Xcode 重新安装应用程序可能会影响问题的复现,所以我首先检查代码并试图缩小问题的范围。根据我们的实现,我想出了三个怀疑的原因。

  • 1、UserDefaults 中的数据被清除。
  • 2、一个意外的 API 调用返回 HTTP 401 并触发退出登录。
  • 3、Keychain 抛出了一个错误。

我能够排除前两个潜在的原因,这要归功于我在自己重现该问题后观察到的一些微妙行为。

  • 登录界面没有要求我选择地区 —— 这表明 UserDefaults 中的数据没有问题,因为我们的 “已显示地区选择 “偏好设置仍然生效。
  • 主用户界面没有显示,即使是短暂的也没有 —— 这表明没有尝试进行网络请求,所以 API 是问题原因可能还为时过早。

这就把Keychain留给了我们,指引我进入下一个问题。是什么发生了改变以及为什么它如此难以复现?

寻找根本原因

我的调试界面很有用,但它缺少了一些有助于回答所有问题的重要信息:时间

我知道在 AppDelegate.application(_:didFinishLaunchingWithOptions:) 之前,“受保护的数据” 是不可用的,但它仍然没有意义,因为为了重现这个问题,我正在执行以下操作:

1、启动应用程序
2、简单使用
3、强制退出应用
4、锁定我的设备并将其放置约 30 分钟
5、解锁设备
6、再次启动应用

每当我在第 6 步中再次启动应用程序时,我 100% 确定设备已解锁,因此我坚信我应该能够从 AppDelegate.init() 中的 Keychain 读取数据。

直到我看了所有这些步骤的时间,事情才开始变得有点意义。

再次仔细查看时间戳:

  • main.swift — 11:38:47
  • AppDelegate.init() — 11:38:47
  • AppDelegate.application(_:didFinishLaunchingWithOptions:) — 12:03:04
  • ViewController.viewDidAppear(_:) — 12:03:04

在我真正解锁手机并点击应用图标之前的 25 分钟,应用程序本身就已经启动了!

现在,我实际上从未想过有这么大的延迟,实际上是 @_saagarjha 建议我检查时间戳,之后,他指给我看这条推特。

推特翻译:
有趣的 iOS 15 优化。Duet 现在试图先发制人地 “预热” 第三方应用程序,在你点击一个应用程序图标前几分钟,通过 dyld 和预主静态初始化器运行它们。然后,该应用程序被暂停,随后的 “启动” 似乎更快。

现在一切都说得通了。我们最初没有测试到它,因为我们很可能没有给 iOS 15 beta 版足够的时间来 “学习” 我们的使用习惯,所以这个问题只在现实世界的场景中再现,即设备认为我很快就要启动应用程序。我仍然不知道这种预测是如何形成的,但我只想把它归结为 “Siri 智能”,然后就到此为止了。

结论

从 iOS 15 开始,系统可能决定在用户实际尝试打开你的应用程序之前对其进行 “预热”,这可能会增加受保护的数据在你认为应该无法使用的时候的被访问概率。

通过等待 application(_:didFinishLaunchingWithOptions:) 委托回调来保护自己,如果可能的话,留意 UIApplication.isProtectedDataAvailable(或对应委托的回调/通知)并相应处理。

我们仍然发现了非常少的非致命问题,在 application(_:didFinishLaunchingWithOptions:) 中报告 isProtectedDataAvailablefalse,在我们可以推迟从钥匙串阅读的访问令牌之外,这将是一个大规模的任务,现在它不值得进行进一步调查。

参考:解决 iOS 15 上 APP 莫名其妙地退出登录 - Swift社区

面试解析

整理编辑:Hello World

Dealloc 使用注意事项及解析

关于 Dealloc 的相关面试题以及应用, 周报里已经有所提及。例如 三十八期:dealloc 在哪个线程执行四十二期:OOM 治理 FBAllocationTracker 实现原理,可以结合今天的使用注意事项一起学习。

避免在 dealloc 中使用属性访问

在很多资料中,都明确指出,应该尽量避免在 dealloc 中通过属性访问,而是用成员变量替代。

在初始化方法和 dealloc 方法中,总是应该直接通过实例变量来读写数据。- 《Effective Objective-C 2.0》第七条

Always use accessor methods. Except in initializer methods and dealloc. - WWDC 2012 Session 413 - Migrating to Modern Objective-C

The only places you shouldn’t use accessor methods to set an instance variable are in initializer methods and dealloc. - Practical Memory Management

除了可以提升访问效率,也可以防止发生 crash。有文章介绍 crash 的原因是:析构过程中,类结构不再完整,当使用 accessor 时,实际是向当前实例发送消息,此时可能会存在 crash。

笔者对这里也不是很理解,根据 debug 分析析构过程实际是优先调用了实例覆写的 dealloc 后,才依次处理 superclass 的 dealloccxx_destructAssociatedWeak ReferenceSide Table等结构的,最后执行 free,所以不应该发生结构破坏导致的 crash,希望有了解的同学指教一下

笔者个人的理解是:Apple 做这种要求的原因是不想让子类影响父类的构造和析构过程。例如以下代码,子类通过覆写了 Associated方 法, 会影响到父类的 dealloc 过程。

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
@interface HWObject : NSObject
@property(nonatomic) NSString* info;
@end

@implementation HWObject
- (void)dealloc
{
self.info = nil;
}
- (void)setInfo:(NSString *)info {
if (info)
{
_info = info;
NSLog(@"%@",[NSString stringWithString:info]);
}
}
@end

@interface HWSubObject : HWObject
@property (nonatomic) NSString* debugInfo;
@end

@implementation HWSubObject
- (void)setInfo:(NSString *)info {
NSLog(@"%@",[NSString stringWithString:self.debugInfo]);
}
- (void)dealloc {
_debugInfo = nil;
}
- (instancetype)init {
if (self = [super init]) {
_debugInfo = @"This is SubClass";
}
return self;
}
@end

造成 crash 的原因是 HWSubObject:dealloc() 中释放了变量 debugInfo,然后调用 HWObject:dealloc() ,该函数使用 Associated 设置 info ,由于子类覆写了 setInfo,所以执行子类 setInfo。该函数内使用了已经被释放的变量 debugInfo正如上面说的, 子类通过重写 Associated,最终影响到了父类的析构过程。

dealloc 是什么时候释放变量的

其实在 dealloc 中无需开发处理成员变量, 当系统调用 dealloc时会自动调用析构函数(.cxx_destruct)释放变量,参考源码调用链:[NSObject dealloc] => _objc_rootDealloc => rootDealloc => object_dispose => objc_destructInstance => object_cxxDestruct => object_cxxDestructFromClass

1
2
3
4
5
6
7
8
9
10
11
static void object_cxxDestructFromClass(id obj, Class cls)
{
// 遍历 self & superclass
// SEL_cxx_destruct 是在 map_images 时在 Sel_init 中赋值的, 其实就是 .cxx_destruct 函数
dtor = (void(*)(id))
lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
// 执行
(*dtor)(obj);
}
}
}

沿着 superClass 链通过 lookupMethodInClassAndLoadCache去查询 SEL_cxx_destruct函数,查找到调用。SEL_cxx_destructobjc 在初始化调用 map_images 时,在 Sel_init 中赋值的,值就是 .cxx_destruct

cxx_destruct 就是用于释放变量的,当类中新增了变量后,会自动插入该函数,这里可以通过 LLDB watchpoint 监听实例的属性值变化, 然后查看堆栈信息验证。

避免在 dealloc 中使用 __weak

1
2
3
- (void)dealloc {
__weak typeof(self) weakSelf = self;
}

当在 dealloc中使用了 __weak 后会直接 crash,报错信息为:Cannot form weak reference to instance (0x2813c4d90) of class xxx. It is possible that this object was over-released, or is in the process of deallocation. 报错原因是 runtime 在存储弱引用计数过程中判断了当前对象是否正在析构中, 如果正在析构则抛出异常

核心源码如下:

1
2
3
4
5
6
7
8
9
id  weak_register_no_lock(weak_table_t *weak_table, id referent_id,   id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions) {
// ... 省略
if (deallocating) {
if (deallocatingOptions == CrashIfDeallocating) {
_objc_fatal("Cannot form weak reference to instance (%p) of " "class %s. It is possible that this object was " "over-released, or is in the process of deallocation.", (void*)referent, object_getClassName((id)referent));
}
// ... 省略
}

避免在 dealloc 中使用 GCD

例如一个经常在子线程中使用的类,内部需要使用 NSTimer 定时器,定时器由于需要加到 NSRunloop 中,为了简单,这里加到了主线程, 而定时器有一个特殊性:定时器的释放和创建必须在同一个线程,所以释放也需要在主线程,示例代码如下(以上代码仅作为示例代码,并非实际开发使用):

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
- (void)dealloc {
[self invalidateTimer];
}

- (void)fireTimer {
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
if (!weakSelf.timer) {
weakSelf.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"TestDeallocModel timer:%p", timer);
}];
[[NSRunLoop currentRunLoop] addTimer:weakSelf.timer forMode:NSRunLoopCommonModes];
}
});
}

- (void)invalidateTimer {
dispatch_async(dispatch_get_main_queue(), ^{
// crash 位置
if (self.timer) {
NSLog(@"TestDeallocModel invalidateTimer:%p model:%p", self->_timer, self);
[self.timer invalidate];
self.timer = nil;
}
});
}
- (vodi)main {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
HWSubObject *obj = [[HWSubObject alloc] init];
[obj fireTimer];
});
}

代码会在invalidateTimer::if (self.timer) 处发生 crash, 报错为 EXC_BAD_ACCESS。原因很简单,因为 dealloc最终会调用 free()释放内存空间,而后 GCD再访问到 self 时已经是野指针,所以报错。

可以使用 performSelector代替 GCD实现, 确保线程操作先于 dealloc 完成。

总结:面试中对于内存管理和 dealloc 相关的考察应该不会很复杂,建议熟读一次源码,了解 dealloc 的调用时机以及整个释放流程,然后理解注意事项,基本可以一次性解决 dealloc 的相关面试题。

优秀博客

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

本期优秀博客的主题为:ARM64 汇编入门及应用。汇编代码对于我们大多数开发者来说是既熟悉又陌生的领域,在日常开发过程中我们经常会遇到汇编,所以很熟悉。但是我们遇到汇编后,大多数人可能并不了解汇编代码做了什么,也不知道能利用汇编代码解决什么问题而常常选择忽略,因此汇编代码又是陌生的。本期博客我搜集了 3 套汇编系列教程,跟大家一道进入 ARM64 的汇编世界。

阅读学习后我将获得什么?

完整阅读三套学习教程后,我们可以阅读一些逻辑简单的汇编代码,更重要的是多了一种针对疑难 bug 的排查手段。

需要基础吗?

我对汇编掌握的并不多,在阅读和学期过程期间发现那些需要思考和理解的东西作者们都介绍的很好。

1、[C in ASM(ARM64)] – 来自知乎:知兵

@皮拉夫大王:推荐先阅读此系列文章。作者从语法角度解释源码与汇编的关系,例如数组相关的汇编代码是什么样子?结构体相关的汇编代码又是什么样子。阅读后我们可以对栈有一定的理解,并且能够阅读不太复杂的汇编代码,并能结合指令集说明将一些人工源码翻译成汇编代码。

2、iOS汇编入门教程 – 来自掘金:Soulghost

@皮拉夫大王:页师傅出品经典教程。相对前一系列文章来说,更多地从 iOS 开发者的角度去看到和应用汇编,例如如何利用汇编代码分析 NSClassFromString 的实现。文章整体的深度也有所加深,如果读者有一定的汇编基础,可以从该系列文章开始阅读。

3、深入iOS系统底层系列文章目录 – 来自掘金:欧阳大哥2013

@皮拉夫大王:非常全面且深入的底层相关文章集合。有了前两篇文章的铺垫,可以阅读该系列文章做下拓展。另外作者还在 深入iOS系统底层之crash解决方法 文章中一步步带领我们利用汇编代码排查野指针问题。作为初学者我们可以快速感受到收益。

学习资料

整理编辑:Mimosa

程序员做饭指南

地址https://github.com/Anduin2017/HowToCook

一个由社区驱动和维护的做饭指南。在这里你可以学习到各色菜式是如何制作的,以及一些厨房的使用常识和知识。比较有意思的是,该仓库里的菜谱大都对制作过程中的细节和用量描述准确,比如菜谱中有 不允许使用不精准描述的词汇,例如:适量、少量、中量、适当。 等非常严格准确的要求,对几乎每个菜谱都做到了简洁准确,非常有意思,也非常欢迎大家贡献它~

工具推荐

整理编辑:CoderStar

EasyFind

地址https://easyfind.en.softonic.com/mac

软件状态:免费

软件介绍

小而强大的文件搜索应用,媲美 windows 下的 Everything

EasyFind

关于我们

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

往期推荐

iOS摸鱼周报 第四十三期

iOS摸鱼周报 第四十二期

iOS摸鱼周报 第四十一期

iOS摸鱼周报 第四十期

iOS摸鱼周报 第四十三期

iOS摸鱼周报 第四十三期

本期概要

  • 话题:dyld4 开源了。
  • Tips:Fix iOS12 libswift_Concurrency.dylib crash bug
  • 面试模块:Synchronized 源码解读
  • 优秀博客:Swift Protocol 进阶
  • 学习资料:南大软件分析课程,iOS 开发学习图谱
  • 开发工具:贝尔实验室开发的有向图/无向图自动布局应用,支持 dot 脚本绘制结构图,流程图等。

本期话题

@zhangferry:Apple 最近开源了 dyld4 的代码。通过阅读它的 Readme 文档,我们可以大致了解到 dyld4 相对 dyld3 做的改进有哪些。dyld3 出于对启动速度的优化,增加了启动闭包。应用首启和发生变化时将一些启动数据创建为闭包存到本地,下次启动将不再重新解析数据,而是直接读取闭包内容。这种方法的理想情况是应用程序和系统应很少发生变化,因为如果这两者经常变化,即意味着闭包可能面临失效。为了应对这类场景,dyld4 采用了 Prebuilt + JustInTime 的双解析模式,Prebuild 对应的就是 dyld3 中的启动闭包场景,JustInTime 大致对应 dyld2 中的实时解析,JustInTime 过程是可以利用 Prebuild 的缓存的,所以性能也还可控。应用首启、包体或系统版本更新、普通启动,dyld4 将根据缓存有效与否选择合适的模式进行解析。

dyld3 在不使用启动闭包的情况下会 fallback 到 dyld2,两套代码分别在两边,不利于行为的统一和维护,dyld4 做了逻辑统一(@鹅喵 补充)。所以 dyld4 的设计目标是更优的兼容性和逻辑统一。

还有一点,细心的开发者还在 dyld4 源码里发现了 realityOS 及 realityOS_Sim 相关的代码注释。很大可能苹果的 VR/AR 设备已经准备差不多了,静待今年的 WWDC 吧。

地址:apple-oss-distributions/dyld

开发Tips

整理编辑:Hello World

iOS12 libswift_Concurrency.dylib crash 问题修复

最近很多朋友都遇到了 iOS12 上 libswift_Concurrency 的 crash 问题,Xcode 13.2 release notes 中有提到是 Clang 编译器 bug,13.2.1 release notes 说明已经修复,但实际测试并没有。

crash 的具体原因是 Xcode 编译器在低版本 iOS12 上没有将 libswift_Concurrency.dylib 库剔除,反而是将该库嵌入到 ipa 的 Frameworks 路径下,导致动态链接时 libswift_Concurrency 被链接引发 crash。

问题分析

通过报错信息 Library not loaded: /usr/lib/swift/libswiftCore.dylib 分析是动态库没有加载,提示是 libswift_Concurrency.dylib 引用了该库。但是 libswift_Concurrency 只有在 iOS15 系统上才会存在, iOS12 本不该链接这个库,猜测是类似 swift 核心库嵌入的方式,内嵌在了 ipa 包中。校验方式也很简单,通过 iOS12 真机 run 一下,崩溃后通过 image list 查看加载的镜像文件会找到 libswift_Concurrency 的路径是 ipa/Frameworks 下的,通过解包 ipa 也证实了这一点。

问题定位

在按照 xcode 13.2 release notes 提供的方案,将 libswiftCore 设置为 weak 并指定 rpath 后,crash 信息变更,此时 error 原因是 ___chkstk_darwin 符号找不到;根据 error Referenced from 发现还是 libswift_Concurrency 引用的,通过:

1
$ nm -u xxxAppPath/Frameworks/libswift_Concurrency.dylib

查看所有未定义符号(类型为 U), 其中确实包含了 ___chkstk_darwin,13.2 release notes 中提供的解决方案只是设置了系统库弱引用,没有解决库版本差异导致的符号解析问题。

error 提示期望该符号应该在 libSystem.B.dylib 中,但是通过找到 libSystem.B.dylib 并打印导出符号:

1
$ nm -gAUj libSystem.B.dylib

发现即使是高版本的动态库中也并没有该符号,那么如何知道该符号在哪个库呢?这里用了一个取巧的方式,run iOS13 以上真机,并设置 symbol 符号 ___chkstk_darwin, xcode 会标记所有存在该符号的库,经过前面的思考,认为是在查找 libswiftCore 核心库时 crash 的可能性更大。

libSystem.B.dylib 路径在 ~/Library/Developer/Xcode/iOS DeviceSupport/xxversion/Symbols/usr/lib/ 目录下

如何校验呢,通过 xcode 上 iOS12 && iOS15 两个不同版本的 libswiftCore.dylib 查看导出符号,可以发现,iOS12 上的 Core 库不存在,对比组 iOS15 上是存在的,所以基本可以断定 symbol not found 是这个原因造成的;当然你也可以把其他几个库也采用相同的方式验证。

通过在 ~/Library/Developer/Xcode/iOS DeviceSupport/xxversion/Symbols/usr/lib/swift/libswiftCore.dylib 不同的 version 路径下找到不同系统对应的 libswiftCore.dylib 库,然后用 nm -gUAj libswiftCore.dylib 可以获取过滤后的全局符号验证。

库的路径,可以通过 linkmap 或者运行 demo 打个断点,通过LLDB的image list查看。

分析总结:无论是根据 xcode 提供的解决方案亦或是 error 分析流程,发现根源还是因为在 iOS12 上链接了 libswift_Concurrency 造成的,既然问题出在异步库,解决方案也很明了,移除项目中的 libswift_Concurrency.dylib 库即可。

解决方案

方案一:使用 xcode13.1 或者 xcode13.3 Beta 构建

使用 xcode13.1 或者 xcode13.3 Beta 构建,注意 beta 版构建的 ipa 无法上传到 App Store。
该方法比较麻烦,还要下载 xcode 版本,耗时较多,如果有多版本 xcode 的可以使用该方法。

方案二:添加 Post-actions 脚本移除

添加 Post-actions 脚本,每次构建完成后移除嵌入的libswift_Concurrency.dylib。添加流程: Edit Scheme -> Build -> Post-actions -> Click ‘+’ to add New Run Script。脚本内容为:

1
rm "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/libswift_Concurrency.dylib" || echo "libswift_Concurrency.dylib not exists"

方案三:降低或移除使用 libswift_Concurrency.dylib 的三方库

查找使用 concurrency 的三方库,降低到未引用 libSwiftConcurrency 前的版本,后续等 xcode 修复后再升级。如果是通过 cocoapods 管理三方库,只需要指定降级版本即可。但是需要解决一个问题,如何查找三方库中有哪些用到 concurrency 呢?

如果是源码,全局搜索相关的 await & async 关键字可以找到部分 SDK,但如果是二进制 SDK 或者是间接使用的,则只能通过符号查找。

查找思路:

  1. 首先明确动态库的链接是依赖导出符号的,即 xxx 库引用了 target_xxx 动态库时,xxx 是通过调用 target_xxx 的导出符号(全局符号)实现的,全局符号的标识是大写的类型,U 表示当前库中未定义的符号,即 xxx 需要链接其他库动态时的符号,符号操作可以使用 llvm nm 命令

  2. 如何查看是否引用了指定动态库 target_xxx 的符号?可以通过 linkmap 文件查找,但是由于 libswift_Concurrency 有可能是被间接依赖的,此时 linkmap 中不存在对这个库的符号记录,所以没办法进行匹配,换个思路,通过获取 libswift_Concurrency 的所有符号进行匹配,libswift_Concurrency 的路径可以通过上文提到的 image list 获取, 一般都是用的 /usr/lib/swift 下的。

  3. 遍历所有的库,查找里面用到的未定义符号( U ), 和 libswift_Concurrency 的导出符号进行匹配,重合则代表有调用关系。

为了节省校验工作量,提供 findsymbols.sh 脚本完成查找,构建前可以通过指定项目中 SDK 目录查找,或者也可以指定构建后 .app 包中的 Frameworks 查找。

使用方法:

  1. 下载后进行权限授权, chmod 777 findsymbols.sh
  2. 指定如下参数:
    • -f:指定单个二进制 framework/.a 库进行检查
    • -p:指定目录,检查目录下的所有 framework/.a 二进制 SDK
    • -o: 输出目录,默认是 ~/Desktop/iOS12 Crash Result

参考:

面试解析

整理编辑:Hello World

Synchronized 源码解读

Synchronized 作为 Apple 提供的同步锁机制中的一种,以其便捷的使用性广为人知,作为面试中经常被考察的知识点,我们可以带着几个面试题来解读源码:

  1. sychronized 是如何与传入的对象关联上的?
  2. 是否会对传入的对象有强引用关系?
  3. 如果 synchronized 传入 nil 会有什么问题?
  4. 当做key的对象在 synchronized 内部被释放会有什么问题?
  5. synchronized 是否是可重入的,即是否可以作为递归锁使用?

查看 synchronized 源码所在

通常查看底层调用有两种方式,通过 clang 查看编译后的 cpp 文件梳理,第二种是通过汇编断点梳理调用关系;这里采用第一种方式。命令为 xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.2 ViewController.m

核心代码就是 objc_sync_enterobjc_sync_exit ,拿到函数符号后可以通过 xcode 设置 symbol 符号断点获知该函数位于哪个系统库,这里直接说结论是在 libobjc 中,objc是开源的,全局搜索后定位到 objc/objc-sync 的文件中;

Synchronized 中重要的数据结构

核心数据结构有三个,SyncDataSyncList 以及 sDataLists;结构体成员变量注释如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData; // 指向下一个 SyncData 节点,作用类似链表
DisguisedPtr<objc_object> object; // 绑定的作为 key 的对象
int32_t threadCount; // number of THREADS using this block 使用当前 obj 作为 key 的线程数
recursive_mutex_t mutex; // 递归锁,根据源码继承链其实是 apple 自己封装了os_unfair_lock 实现的递归锁
} SyncData;

// SyncList 作为表中的首节点存在,存储着 SyncData 链表的头结点
struct SyncList {
SyncData *data; // 指向的 SyncData 对象
spinlock_t lock; // 操作 SyncList 时防止多线程资源竞争的锁,这里要和 SyncData 中的 mutex 区分开作用,SyncData 中的 mutex 才是实际代码块加锁使用的

constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};

// Use multiple parallel lists to decrease contention among unrelated objects.
/ 两个宏定义,方便调用
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data /
static StripedMap<SyncList> sDataLists; // 哈希表,以关联的 obj 内存地址作为 key,value是 SyncList 类型

StripedMap 本质是个泛型哈希表,是 objc 源码中经常使用的数据结构,例如 retain/release 中的 SideTables 结构等。

一般以内存地址值作为 key,返回声明类型的 value,iOS中 存储容量是 8 Mac中 容量是 64 ,可以通过源码查看

核心逻辑 id2data()

通过源码可以获知 objc_sync_enterobjc_sync_exit 核心逻辑都是 id2data(),入参为作为 key 的对象,以及状态枚举值。

代码流程如下:

  • 通过关联的对象地址获取 SyncList 中存储的的 SyncData 和 lock 锁对象;

  • 使用 fastCacheOccupied 标识,用来记录是否已经填充过快速缓存。

    • 首先判断是否命中 TLS 快速缓存,对应代码 SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    • 未命中则判断是否命中二级缓存 SyncCache, 对应代码 SyncCache *cache = fetch_cache(NO);
    • 命中逻辑处理类似,都是使用 switch 根据入参决定处理加锁还是解锁,如果匹配到,则使用 result 指针记录
      • 加锁,则将 lockCount ++,记录 key object 对应的 SyncData 变量 lock 的加锁次数,再次存储回对应的缓存。
      • 解锁,同样 lockCount–,如果 ==0,表示当前线程中 object 关联的锁不再使用了,对应缓存中 SyncData 的 threadCount 减1,当前线程中 object 作为 key 的加锁代码块完全释放
  • 如果两个缓存都没有命中,则会遍历全局表 SyncDataLists, 此时为了防止多线程影响查询,使用了 SyncList 结构中的 lock 加锁(注意区分和SyncData中lock的作用)。

    查找到则说明存在一个 SyncData 对象供其他线程在使用,当前线程使用需要设置 threadCount + 1 然后存储到上文的缓存中;对应的代码块为:

    1
    for (p = *listp; p != NULL; p = p->nextData) {goto done}
  • 如果以上查找都未找到,则会生成一个 SyncData 节点, 并通过 done 代码段填充到缓存中。

    • 如果存在未释放的 SyncData, 同时 theadCount == 0 则直接填充新的数据,减少创建对象,实现性能优化,对应代码:

      1
      if ( firstUnused != NULL ) {//...}
    • 如果不存在,则新建 SyncData 对象,并采用头插法插入到链表的头部,对应代码逻辑

      1
      2
      posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
      //....

最终的存储数据结构如下图所示:

当 id2data() 返回了 SyncData 对象后,objc_sync_try_enter 会调用 data->mutex.tryLock();尝试加锁,其他线程再次执行时如果判断已经加锁,则进行资源等待

以上是对源码的解读,需要对照着 libobjc 源码阅读会更好的理解。下面回到最初的几个问题:

  1. 锁是如何与你传入 @synchronized 的对象关联上的

    答: 由 SyncDataLits 可知是通过对象地址关联的,所以任何存在内存地址的对象都可以作为 synchronized 的 key 使用

  2. 是否会对关联的对象有强引用

    答:根据 StripedMap 里的代码可以没有强引用,只是将内存地址值进行位计算然后作为 key 使用,并没有指针指向传入的对象。

  3. 如果 synchronize 传入 nil 会有什么问题

    答:通过 objc_sync_enter 源码发现,传入 nil 会调用 objc_sync_nil, 而 BREAKPOINT_FUNCTION 对该函数的定义为 asm()"" 即空汇编指令。不执行加锁,所以该代码块并不是线程安全的。

  4. 假如你传入 @synchronized 的对象在 @synchronized 的 block 里面被释放或者被赋值为 nil 将会怎么样

    答:通过 objc_sync_exit 发现被释放后,不会做任何事,导致锁也没有被释放,即一直处于锁定状态,但是由于对象置为nil,导致其他异步线程执行 objc_sync_enter 时传入的为 nil,代码块不再线程安全。

  5. synchronized 是否是可重入的,即是否为递归锁

    答:是可递归的,因为 SyncData 内部是对 os_unfair_recursive_lock 的封装,os_unfair_recursive_lock 结构通过 os_unfair_lock 和 count 实现了可递归的功能,另外通过lockCount记录了重入次数

知识点总结:

  • id2data 函数使用拉链法解决了哈希冲突问题(更多哈希冲突方案查看 摸鱼周报39期 ),

  • 在查找缓存上支持了 TLS 快速缓存 以及 SyncCache 二级缓存和 SyncDataLists 全局查找三种方式:

  • Sychronized 使用注意事项,请参考 正确使用多线程同步锁@synchronized()

参考:

优秀博客

整理编辑:东坡肘子

1、在已实现协议要求方法的类型中如何调用协议中的默认实现 – 来自:Leonardo Maia Pugliese

@东坡肘子:能够提供默认实现是 Swift 协议功能的重要特性。本文介绍了在已实现协议要求方法的类型中继续调用协议的默认实现的三种方式。解决的思路可以给读者不小的启发。在每篇博文中附带介绍一副绘画作品也是该博客的特色之一。

2、通过 Swift 代码介绍 24 种设计模式 – 来自:oldbird

@东坡肘子:设计模式是程序员必备的基础知识,但是没有点年份,掌握也不是这么容易,所以例子就非常重要。概念是抽象的,例子是具象的。具象的东西,记忆和理解都会容易些。该项目提供了 24 种设计模式的 Swift 实现范例,对于想学习设计模式并加深理解的朋友十分有帮助。

3、Combining protocols in Swift – 来自:Sundell

@东坡肘子:组合和扩展均为 Swift 协议的核心优势。本文介绍了如何为组合后的协议添加具有约束的扩展。几种方式各有利弊,充分掌握后可以更好地理解和发挥 Swift 面向协议编程的优势。

4、Swift Protocol 背后的故事 – 来自: 赵雪峰

@东坡肘子:本文共分两篇。上篇中,以一个 Protocol 相关的编译错误为引,通过实例对 Type Erasure、Opaque Types 、Generics 以及 Phantom Types 做了较详细的讨论。下篇则主要讨论 Swift Protocol 实现机制,涉及 Type Metadata、Protocol 内存模型 Existential Container、Generics 的实现原理以及泛型特化等内容。

5、不透明类型 – 来自:Mzying

@东坡肘子:不透明类型是指我们被告知对象的功能而不知道对象具体是什么类型。作者通过三个篇章详细介绍了 Swift 的不透明类型功能,包括:不透明类型解决的问题(上)、返回不透明类型(中)、不透明类型和协议类型之间的区别 (下)。

学习资料

整理编辑:Mimosa

南大软件分析课程

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

南京大学《软件分析》课程系列,非常难得的高质量课程,可以通过这里获取所有课程的课件。

iOS 开发学习图谱

地址http://hdjc8.com/iOSRoadMap/

一份特别丰富的 iOS 开发学习图谱,其中包含了许多 iOS 开发的资源,编者认为这本图谱不适合作为学习的一个路线,适合作为一份让你了解 iOS 有哪些知识点的图谱,其中的许多的知识点很适合作为查漏补缺的一个工具。在我们做工作中常常会仅做某些领域内的工作,导致在不短的一段时间内接触的技术是比较窄的,假如你突然想了解一些别的知识点,你可以来这本图谱中闲逛一下,看看有什么知识点是你感兴趣的,也许有一些是你以前感兴趣但是由于种种原因没来及了解的内容!

工具推荐

整理编辑:CoderStar

Graphviz

地址http://www.graphviz.org/

软件状态:免费

软件介绍

贝尔实验室开发的有向图/无向图自动布局应用,支持 dot 脚本绘制结构图,流程图等。

Graphviz

对产物.gz文件进行解析查看的途径。

  • 在线网站:GraphvizOnline
  • vs 插件:Graphviz (dot) language support for Visual Studio Code

结合cocoapods-dependencies插件,我们可以解析podfile文件来分析项目的pod库依赖,生成.gz文件。

  • 生成.gz文件:pod dependencies --graphviz
  • 生成依赖图:pod dependencies --image
  • 生成.gz文件及依赖图:pod dependencies --graphviz --image

关于我们

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

往期推荐

iOS摸鱼周报 第四十二期

iOS摸鱼周报 第四十一期

iOS摸鱼周报 第四十期

iOS摸鱼周报 第三十九期

人物访谈 | 东坡肘子:健康&开发

人物访谈 | 东坡肘子:健康&开发

肘子是摸鱼周报的一位编辑,最早知道他还是通过他写的几篇关于 Swift 的博客。他的博客专注于 Swift 领域,每篇文章的质量都很高,是这个领域非常值得关注的博客之一。后来了解到他之前得过一场大病,现在的生活节奏基本平稳有序。又想到其他几位圈里同样受到疾病影响的开发者,就决定了这次访谈的主题:健康&开发。

简单介绍下自己吧

大家好,我是东坡肘子,70 后。小学时获得了我的第一台电脑(CPU 是 MC6800),几十年来更换过不少设备,算是亲身经历了信息时代的巨大变迁。尽管一直以来都对于硬件开发、软件编程十分喜爱,但并没有以此为职业。最近几年,由于疾病的原因,目前赋闲在家,通过博客 肘子的 Swift 记事本 记录一些关于 SwiftUI、Core Data、Combine 等方面的学习心得。

由于治疗的需要,我从一个不喜欢记笔记的的人变成了每天必须要记录大量数据(其实主要还是依赖我妻子)的人。前年通过手术,生活质量获得了较大的改善,本以为所需记录的数据能少一点,但事与愿违,数据量减少了,数据种类却大大的提升了,而且可以明确的是,这些数据将要在我有生之年一直记录下去。因为喜欢技术和记笔记的需求,于是就开发了一款 iOS app ——「健康笔记」。

最近也经常听到一些上班族特别是程序员群体相关的健康话题,很显然健康非常重要也很容易被大家忽视。方便讲一下你患病的原由吗,也提醒大家重视健康问题?

2013 年,我的身体状况出现了异样,每天不停的呕吐,几乎无法进食。当时工作很忙,休息时间无法得以保障,自认为或许是睡眠不足导致的肠胃问题。在家人的强烈要求下,去医院做了检查。简单的化验后,医生立刻就明确了问题所在——尿毒症。入院时的身体相当糟糕,肌酐达到 2100μmol/L(正常值通常低于 100)、血红蛋白 4.6(男性正常值通常高于 12)。

人是一种挺神奇的动物,在去医院的前一周,我还跑了三个城市。但在住院后,透支的身体立刻就无法继续坚持了,出现了数次的生死危机。经过几年的透析治疗,我在 2018 年接受了肾移植手术,目前各方面都恢复的不错。

都说久病成医,作为一个经历较大疾病的人,有没有什么忠告或建议给大家?

我很幸运,在关键时刻就医、有家人的悉心照顾、有不错的医疗资源。在身体逐渐恢复后,我迫切地希望有更多的人能够及早认识到健康的重要性,避免走到难以挽回的地步。

如实在无法做到早睡早起、按时吃饭、健康饮食、科学养生,希望至少重视以下两点:

  • 尊重你身体的求救信号。绝大多数的疾病,都不会毫无预警地凭空而来。不要将身体的一切不良反应都归于工作忙、压力大等因素。最起码要严肃的面对每一次的体检结果,及时完成需要进一步随检的内容。
  • 不要过度。在连续的熬夜加班后休息半天,休息后再工作或许可以取得意想不到的进展,熬夜加班中休息半天,或许可以取得意想不到的工作进展,更加关键地是,可以让你紧绷的身体获得适度的放松。身体是属于你自己的,也只有你自己可以对其负责。爱惜它、保养它,真正做到「活久见」,而不只是当成一个梗。

这几年我在知乎上从一个曾经的透析患者角度,对尿毒症、肾移植等方面的内容做了些介绍和解答。以下是其中的一篇,希望帮助到有需要的人——刚确诊尿毒症,患者和家属应该注意什么?。当然也衷心地希望大家都能身体健康。

@zhangferry:我的父亲也是尿毒症患者,于 19 年做了肾移植。他早期是痛风,痛风患者是需要严格控制喝啤酒的,他忌不住嘴,导致病情加重,以至于发展为慢性肾炎。后来看一则广告推荐的中医,治疗不当,最终发展为尿毒症。我曾随父亲多次往返医院的血液净化中心,也见过很多年轻的患者,对这个病感触非常深。现在回想过来正是由于早期很多应该做的措施没有做对,才造成了最终的后果。当前的教训就是:重视健康问题,及早正确的治疗。

从痛风这个点说起,它的前身一般是高尿酸血症,长期的高尿酸血症易发展为痛风。目前高尿酸血症的患病人数为 1.77 亿,痛风患病人数为 1466 万,这个比例已经很大了,以至于高尿酸血症被称为”第四高”。尿酸指标属于肾功能检查(非血常规),一般体检都会有,当尿酸数值超过 420μmol/L 即表示为高尿酸血症。如果是爱康国宾的体检的话,App 里体检报告的基本健康数据就会显示尿酸数值。

高尿酸血症及痛风的高发人群是:偏好海鲜等高嘌呤食物、过度饮酒不节制、作息不规律、纵容肥胖,不爱运动、吸烟。所以如果你的尿酸指标已经高了的话切记克服以上的生活习惯。

人身体的潜能和耐受力都是巨大的,特别是年轻的时候,但要知道这不是挥霍身体的理由。这副躯体我们是要用一辈子,而且没法随意更换的,我们一定要好好爱惜它。最后还是希望通过肘子跟我的一些经历,提醒到大家重视健康问题。

数据参考:第一财经商业数据中心:2021中国高尿酸及痛风趋势白皮书

你在开发的一款应用是「健康笔记」,能简单介绍下这款应用的功能和未来规划吗?

透析阶段,我采用的是腹膜透析方式。它的优势是可以在家中进行,无需每周多次往返透析中心。腹膜透析每天需要做多次的透析液更换,并且每个月都需要去医院做随诊和验血。数年间,我记录了大量的有关透析治疗和身体检验等方面的数据(数个笔记本)。移植手术后,因为需要终身服用免疫抑制剂,目前仍每个月进行一次验血,以跟踪某些指标。

尽管市面上已有一些针对特定疾病进行数据跟踪管理的 App,但随着治疗的发展,需要记录的内容也不断发生变化,到达某个阶段后,这些 App 便无法继续胜任了。 因此,我决定开发一款可记录各种数据类型的 App。

本质上讲,「健康笔记」是一款支持高度定制数据类型的记录工具。目前支持七种数据格式,并可为数据设置各种验证条件。除了健康数据外,使用者还可以使用「健康笔记」记录生活、工作中遇到的绝大多数可量化或不可量化的内容。

相较于纯记录型工具,「健康笔记」更注重对数据的分析和管理。提供了多维度的图表,并且使用者可以将 App 中的数据导出到其他的软件或工具中进行分析。

「健康笔记」基于 SwiftUI 和 Core Data with CloudKit 进行的开发。目前可用于 iOS 14 以上的设备。当前的版本为 2.x,3.0 版本目前仍在开发中。

健康笔记

现在的生活节奏怎么样?你说目前是处于一种赋闲在家的状态,对于没有外界约束的状态,保持规律的作息是比较难的一件事,你每天的时间是怎么安排的呢?

当前的生活状态可以用两个字来总结——规律。每天早上 6:00 点起床、晚上 10:00 睡觉,三餐准时,全年不变。生活的内容主要围绕着照顾猫狗、健身、学习、阅读。

我已无需强迫自己遵循以上的作息,相反,如果某天意外地违反我反倒有些不适应。

你是怎么考虑独立开发和远程工作的?

「健康笔记」算是我的独立作品,但我并不算是独立开发者。

以我的理解,严格意义上的独立开发者至少要满足两个条件:

  • 将商业的思维贯穿于开发行为之中,开发的是商品而不是作品
  • 要有以开发成果作为其主要收入来源的决心

当有了以上觉悟的情况下,结合自身的情况再决定是否踏足这个领域。

因为疫情的原因,远程工作得到了前所未有的发展。在某些领域,远程的的效率甚至高于传统的工作形式。但无论远程有多么的方便,仍应尽量保持一定量面对面的交流。摄像头、麦克风、文字所能传递的情感与信息实在有限。

对于技术,目前主要就是在研究 SwiftUI 和维护自己的应用吗?2022 年,有没有什么新的技术方向的规划?

SwiftUI 是一个比较新的框架,处于快速变化和发展中,今年仍会投入不少的精力对其跟踪和学习。「健康笔记」也会做一次彻底的更新,相对于功能上的增加,我更想在 app 架构上有所突破。今年会着重于夯实基础,逐渐从「知其然」向「知其所以然」转变。

看你每周都会固定输出 Swift 相关文章,而且质量都很高,相信肯定是花了不少时间整理的。也看到你最近发了一篇停更说明,说是遇到了一些瓶颈,计划用一段时间做一些系统性的充电。这种严谨的学习态度非常让人钦佩,但另一方面产出数量就会降低,能说下你对自己产出内容数量和质量上的一些想法吗?

创建「肘子的 Swift 记事本」的初衷很单纯,通过记录加深理解、梳理思路。我对内容的产出数量并没有具体要求,但希望做到言之有物,在满足自身学习需求的同时具备一点分享的价值。

「肘子的 Swift 记事本」和「健康笔记」之间是相互依存的关系。因为想写「健康笔记」,所以创建博客帮助学习;文章的方向基本围绕着「健康笔记」的需求展开;学习的结果又通过「健康笔记」来得以实践。

从去年年中开始,我便开始了「健康笔记」3.0 版本的开发。在已完成了 80%左右的情况下,我决定将之前的工作全部推翻。尽管相较于 2.x 版本来说,新版代码有了些提高,但对我本人来说并没有质的飞跃。「健康笔记」作为个人实践和检验学习结果的载体,我并不希望为了升级而升级(从功能和稳定性角度来看,2.x 版本目前仍可胜任)。

此次停更便是想用一段时间来系统改善开发过程中发现的不足。此阶段的学习重点集中于理论层面,大多与语言和平台无关。希望届时能够有所收获。

写文章非常容易遇到知识盲点,对于这种问题,你是如何快速梳理出正确脉络的,有没有什么可以借鉴的技巧分享下?

事实上,并非总能快速梳理出思路。当碰到一个盲点时,我喜欢采用拓展阅读的方式,可能仅仅因为某个没有使用过的 API 而借机学习了解一下整个框架;一个短时间就能找到解决方案的问题,我会将其扩张成几天才能学完的内容。前期这种做法会十分耽误时间,但在有了一定的储备后,对于之后遇到的问题,梳理起来就会方便很多。

另外,我会订阅大量优秀博客的 RSS 或 Newsletter。每天早上我会用 30-60 分钟,将最新的文章进行一遍通读,在将来遇到问题时,从记忆中找寻解决之道。

以下是我经常关注的英文Blog或Newsletter,中文内容还需更多地关注摸鱼周刊。

名称 地址 简介
Augmented Code https://augmentedcode.io/ 频谱查看应用 Signal Path 作者 Toomas Vahter 的博客。每个月 2-3 篇的更新量。内容主要涉及 SwiftUI、UIKit、XCTest 等。
Create with Swift https://www.createwithswift.com 由三名意大利人(Giovanni Monaco、Tiago Gomes Pereira、Moritz Philip Recke)创建的博客。内容以 Combine、SwiftUI 为主。
Donnywals.com https://www.donnywals.com Practical Combine 以及 Practical Core Data 两本书籍的作者作者 Donny Wals 的博客。主要聚焦于 Core Data、Combine、SwiftUI 等内容。
Dave Delong https://davedelong.com Dave Delong 的博客,最近一年更新的不太频繁。2020 年创建了一系列有关如何用 Swift 开发 HTTP Stack 的精彩内容。
Filip Němeček https://nemecek.be ImpressKit 作者 Filip Němeček 的博客。关于 UIKit 方面的内容较多。
FIVE STARS https://www.fivestars.blog/ Federico Zanetello 的博客。当前集中于 SwiftUI 方面的内容,每篇文章都很有价值。
Hacking with Swift https://www.hackingwithswift.com 畅销书籍作者 Paul Hudson 创建的网站,内容涉及 Swift 的各个方面。
Holy Swift https://holyswift.app Leonardo Pugliese 的博客。除了有关 Swift 的内容外,每篇文章都会介绍一副绘画作品。
iOS Dev Weekly https://iosdevweekly.com Dave Verwer 创建的 Newsletter 站点。少有的仅以 Newsletter 作为表述方式的作者。
Masilotti https://masilotti.com/ Joe Masilotti 的博客。有不少关于单元测试、UI 测试方面的内容。
Oleb https://oleb.net Ole Begemann 的博客。十多年间持续创作和 iOS 开发有关的内容。
onmyway133 https://onmyway133.com Khoa 的博客。非常高产,最近两年有关 SwiftUI 的内容居多。
Raywenderlich https://www.raywenderlich.com 知名的技术书籍出版商。尽管是商业机构,但仍提供了大量优秀的免费课程(课程基本上都会提供完整的项目代码)。
Sarunw https://sarunw.com 泰国开发者 Sarun W 的博客。创作了很多有关苹果生态开发的内容。他开发的 codeshot 可以方便的将代码转换成漂亮图片以利于交流。
Swift with Majia https://swiftwithmajid.com Majid Jabrayilov 的博客。他关于 Swift UI 数据架构方面的文章对我的影响很大。最近在做 Microapps 的专题。他的周刊 SwiftUI Weekly 已经提供了超过 90 期的内容。
SwiftLee https://www.avanderlee.com Antoine van der Lee 的博客。除了原创的文章外,每周通过 SwiftLee Weekly 介绍其他优秀的文章和工作机会。他开发的 RocketSim 对于 Xcode 的使用者帮助不小。
Swiftly Rush https://www.swiftlyrush.com Adam Rush 的博客。坚持周更,以 Tips 为主。也提供周报
Swift Rocks https://swiftrocks.com Bruno Rocha 的博客。更新不频繁,但不时会有相当有深度的内容出现。
Swift by Sundell https://www.swiftbysundell.com Swift 静态站点生成器 Publish 的作者 John Sundell 的博客(我的博客就是由 Publish 构建)。除了每周精彩的文章外同时还通过 Podcast 与很多业内专家交流最新的技术动态。
The SwiftUI Lab https://swiftui-lab.com Javier 的博客。他撰写的关于 SwiftUI 的文章对 SwiftUI 的开发者影响很大。他开发的的 A Companion for SwiftUI 是每个 SwiftUI 开发者都应购买的工具。
Trailing Closure https://trailingclosure.com 着重于 Swift UI。每篇文章都会介绍一个 SwiftUI 动效方面的具体实现。
Try Code https://trycombine.com Marin Todorov 的博客。作者参与了不少苹果官方的开源项目。最近正在打造一款轻量级的 Swift IDE。
Yet Another Swift Blog https://www.vadimbulavin.com Vadim Bulavin 的博客。内容主要涉及 Swift、SwiftUI、单元测试等。

现在很多开发者会因为程序员是青春饭而焦虑,而你作为一个技术领域的老兵却时刻保持着对技术的热情,能说下你能一直保持热情的原因吗?

学习使我快乐,能力提高让我获得满足。有点类似于打游戏,不断通关,坚持不 Game Over。

当前的职场环境好像给每个参与者都带来了不小的压力。与其为年龄而焦虑,我觉得更应该时刻关心自己是否保持了学习的热情和动力。技能往往是与职业绑定的,而学习能力与职业无关。人一生中处于不同的岗位或职业是十分正常的事情。相较于 IT 届,年龄因素在很多其他行业占据着更加重要的位置。无论身处什么行业,在职业技能提高的同时,也要做到个人综合能力的提升。尽管未必能减轻多少因年龄而产生的焦虑,但至少可以获得更多面对未来的信心。

iOS 摸鱼周报 第四十二期

iOS 摸鱼周报 第四十二期

本期概要

  • 话题:2022年1月31号之后提交的应用需提供账号删除功能。
  • Tips:openssh 8.8 默认禁用 ssh-rsa 加密算法导致 git 验证失效。
  • 面试模块:如何治理 OOM。
  • 优秀博客:一些优秀开发者的年终总结。
  • 学习资料:程序员考公指南;Vim 从入门到精通(中文)。
  • 开发工具:摸鱼单词,专注于利用碎片时间学习记忆英语单词。

本期话题

2022年1月31号之后提交的应用需提供账号删除功能

内容如题,该项要求是 2021 年 10 月 6 号提出的,主要目的是加强苹果生态的隐私保护。在 App Store 审核指南的 5.1.1 条款第 v 条更新了这句话:

If your app supports account creation, you must also offer account deletion within the app.

但对于如何设置该功能,苹果并没有明确的要求。如果删除用户账号,应用端可以根据相关法律继续保留用户信息,但这需要在隐私政策中进行说明所采集用户数据的内容和保留策略。

Account deletion within apps required starting January 31

开发 Tips

整理编辑:Hello World

openssh 8.8 默认禁用 ssh-rsa 加密算法导致 git 验证失效

问题源自于最近无意间在工作机上升级了 openssh 版本(后续才发现是版本问题),导致所有基于 ssh 方式的 git 操作全部失效;

git pull 一直提示请输入密码,在我输入了无数次个人 gitlab 密码仍然失败后,第一直觉是我的 ssh 密钥对出了问题,重新生成并上传了新的公钥,还是同样的提示;

使用 ssh -vT 命令查看了详细的日志信息,最终发现了问题所在。在解析日志之前,这里先了解一下简化的 ssh 密钥登录的原理:

我们都知道 ssh 是基于非对称加密的一种通信加密协议,常用于做登录校验,

一般支持两种方式:口令登录和公钥登录;由于篇幅问题这里只介绍公钥的简化流程,如果想了解探索过程以及 git 使用 ssh 的一些技巧,可以查看原文

登录分为两部分:

  • 生成会话密钥
    • 客户端和服务端互相发送 ssh 协议版本以及 openssh 版本,并约定协议版本
    • 客户端和服务端互相发送支持的加密算法并约定使用的算法类型
    • 服务端生成非对称密钥,并将公钥以及公钥指纹发送到客户端
    • 客户端和服务端分别使用 DH 算法计算出会话密钥,后续所有流程都会使用会话密钥加密传输
  • 验证阶段
    • 如果是公钥登录,则会将客户端将公钥指纹信息,使用上述的会话密钥加密发送到服务端
    • 服务端拿到后解密,并去 authorized_keys 中匹配对应的公钥,生成一个随机数,使用该客户端公钥加密后发送到客户端
    • 客户端使用自己的私钥解密,获取到随机数,使用会话密钥对随机数加密,并做 MD5 生成摘要发送给服务端
    • 服务端对原始随机数也使用会话密钥加密后计算 MD5,对比两个值是否相等决定是否登录

常说的 ssh 只是一种抽象的协议标准的,实际开发中我们使用的是开源 openssh 库,该库是对 ssh 这一抽象协议标准的实现

以上是 ssh 协议登录校验的流程概要,我们了解到在验证阶段会用到客户端的公钥,openssh 会判断公钥生成算法类型,由于不再支持ssh-rsa,,publickey 方式失败后会尝试使用口令登录方式,这也是一直提示我们输入密码的原因。具体可以通过日志查看:

这里针对日志做了简化,部分内容做了注释,你也可以对照自己的 log 日志查看更详细的过程

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
ssh -vT git@github.com

# 日志如下
# 版本信息
OpenSSH_8.8p1, OpenSSL 1.1.1m 14 Dec 2021
# 读取配置文件
debug1: Reading configuration data /Users/clownfish/.ssh/config
debug1: Reading configuration data /usr/local/etc/ssh/ssh_config
...
# 查找身份文件, 成功返回 0, 失败返回 -1, 由于本地只有默认的 id_ras 所以只有这一项返回 0
debug1: identity file /Users/clownfish/.ssh/id_rsa type 0
debug1: identity file /Users/clownfish/.ssh/id_rsa-cert type -1
...

# 版本号
debug1: Local version string SSH-2.0-OpenSSH_8.8
debug1: Remote protocol version 2.0, remote software version OpenSSH_6.6.1p1 Ubuntu-2ubuntu2.8
...
# 查找到host
debug1: Found key in /Users/clownfish/.ssh/known_hosts:5

debug1: Will attempt key: /Users/clownfish/.ssh/id_rsa RSA SHA256:7KTOiN2jUDgc5SJm22GnEk5TpshjTBk/lU9stwJYx48
... # Will attempt key 尝试其他类型密钥

# 认证支持两种方式, 公钥和口令
debug1: Authentications that can continue: publickey,password
debug1: Next authentication method: publickey
debug1: Offering public key: /Users/clownfish/.ssh/id_rsa RSA SHA256:7KTOiN2jUDgc5SJm22GnEk5TpshjTBk/lU9stwJYx48
# 针对上文找到的 public key 没有相互支持的签名算法
debug1: send_pubkey_test: no mutual signature algorithm
... # Trying private key 尝试其他私有 key
# 尝试口令登录
debug1: Next authentication method: password
... # 一直提示输入密码

找到关键字 no mutual signature supported。去查了一下,发现是 openssh 8.8 版本问题不再支持 ssh-rsa
openssh 8.8 release notes 中说明默认会自动转换,但是链接到版本较低的 server 时(从日志中可以看到我们 serve r的版本是 6.6),还是要手工处理。

那么解决办法也就有了,要么重新生成其他算法的秘钥对上传,要么修改配置再次开启支持,这里只针对第二种
在 config 中做如下配置:

1
2
3
Host * # 第一行说明对所有主机生效
PubkeyAcceptedKeyTypes=+ssh-rsa # 第二行是将 ssh-rsa 加会允许使用的范围,没配置会提示 no mutual signature supported.表示找不到匹配的签名算法
# HostKeyAlgorithms +ssh-rsa # 第三行是指定所有主机使用的都是 ssh-rsa 算法的 key,我个人测试可以不写,如果仍不生效可以打开测试

再次测试发现可以正常登录

另外开局提到的,提示输入的密码,其实应该是登录服务器 git 用户的密码,而不是指的 gitlab 中的个人账号密码;
因为 git 使用 ssh 目的仅仅是登录校验,而不用于访问数据,由于个人对 server 端了解的较少,所以在这里也坑了很久,希望了解的同学多多指教

面试解析

整理编辑:zhangferry

如何治理 OOM

OOM(Out Of Memory)指的是应用内存占用过高被系统强制杀死的情况,通常还会再分为 FOOM (前台 OOM) 和 BOOM (后台 OOM)两种。其中 FOOM 现象跟常规 Crash 一样,对用户体验影响比较大。

OOM 产生的原因是应用内存占用过高,治理方法就是降低内存占用,这可以分两部分进行:

1、现存代码:问题检测,查找内存占用较大的情况进行治理。

2、未来代码:防裂化,对内存使用进行合理的规范。

问题检测

OOM 与其他 Crash 不同的一点是它的触发是通过 SIGKILL 信号进行的,常规的 Crash 捕获方案无法捕获这类异常。那么该如何定位呢,线下我们可以通过 Schems 里的 Memory Management,生成 memgraph 文件进行内存分析,但这无法应用到线上环境。目前主流的线上检测 OOM 方案有以下几个:

FBAllocationTracker

由 Facebook 提出,它会 hook OC 中的 +alloc+ dealloc 方法,分别在分配和释放内存时增加和减少实例计数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@implementation NSObject (AllocationTracker)

+ (id)fb_newAlloc
{
id object = [self fb_originalAlloc];
AllocationTracker::tracker()->incrementInstanceCountForClass([object class]);
return object;
}

- (void)fb_newDealloc
{
AllocationTracker::tracker()->decrementInstanceCountForClass([object class]);
[self fb_originalDealloc];
}
@end

然后,当应用程序运行时,可以定期调用快照方法来记录当前活动的实例数。通过实例数量的异常变化来定位发生OOM的问题。

该方案的问题是无法检测非 OC 对象的内存占用,且没有堆栈信息。

参考:Reducing FOOMs in the Facebook iOS app

OOMDetector

这个是腾讯采用的方案。

通过 Hook iOS 系统底层内存分配的相关方法(包括 malloc_*zone相关的堆内存分配以及 vm*_allocate 对应的 VM 内存分配方法),跟踪并记录进程中每个对象内存的分配信息,包括分配堆栈、累计分配次数、累计分配内存等,这些信息也会被缓存到进程内存中。在内存触顶的时候,组件会定时 Dump 这些堆栈信息到本地磁盘,这样如果程序爆内存了,就可以将爆内存前 Dump 的堆栈数据上报到后台服务器进行分析。

参考:【腾讯开源】iOS爆内存问题解决方案-OOMDetector组件 - 掘金

Memory Graph

这个是字节采用的方案,基于内存快照生成内存分布情况。线上 Memory Graph 核心的原理是扫描进程中所有 Dirty 内存,通过内存节点中保存的其他内存节点的地址值建立起内存节点之间的引用关系的有向图,用于内存问题的分析定位,整个过程不使用任何私有 API。该方案实现细节未开源,目前已搭载在字节跳动火山引擎旗下应用性能管理平台(APMInsight)上,供开发者注册使用。

有一篇文章分析了这个方案的实现原理:通过 mach 内核的 vm_*region_recurse/vm_region_recurse64 函数遍历进程内所有 VM Region。这里包括二进制,动态库等内存,我们需要的是 Malloc Zone,然后通过 malloc*_get_all_zones 获取 libmalloc 内部所有的 zone,并遍历每个 zone 中管理的内存节点,获取 libmalloc 管理的存活的所有内存节点的指针和大小。再根据指针判断是 OC/Swift 对象,还是 C++ 对象,还是普通的 Buffer。

参考:iOS 性能优化实践:头条抖音如何实现 OOM 崩溃率下降50%+

防劣化

防劣化即防止出现 OOM 的一些手段,可以从以下方面入手:

  • 内存泄漏:关于内存泄漏的检测可以见上期内容

  • autoreleasepool:在循环里产生大量临时对象,内存峰值会猛涨,甚至出现 OOM。适当的添加 autoreleasepool 能及时释放内存,降低峰值。

  • 大图压缩:可以降低图片采样率。

  • 前后台切换:后台更容易发生 OOM,因为后台可利用的内存更小,我们可以在进入后台时考虑释放一些内存。

优秀博客

整理编辑:我是熊大

1、2021 年终总结 – 来自博客:王巍 (onevcat)

@我是熊大:王巍,拥有知名开源库 Kingfisher,创办了网站 ObjC CN,是 iOS 开发者重点关注对象。

2、大厂逃离后上岸人员的年终总结 – 来自掘金:东方赞

@我是熊大:工作不卷,生活要开心,收入稳中有升。

3、下一个五年计划起航 ! – 来自博客:halfrost

@我是熊大:霜神是前阿里巴巴资深后端工程师,iOS 开发届的大佬级别人物,这是 2020 的年终总结,来的更晚一些。

4、【年度总结】2021年度总结 – 来自博客:郑宇琦

@我是熊大:郑宇琦,LinkedIn 高级研发工程师,曾就职于百度,作者过去一年的经历十分丰富,生活不止有 coding。

5、2020年我阅读了87本书,推荐这12本好书给你 – 来自公众号: 千古壹号

@我是熊大:作者是京东的一位前端开发,读书爱好者,一年的读书清单有 87 本之多。

学习资料

整理编辑:Mimosa

程序员考公指南

地址https://github.com/coder2gwy/coder2gwy

互联网首份程序员考公指南,由 3 位已经进入体制内的前大厂程序员联合献上。程序员近几年内卷的程度有些加重了,不少人萌生了回家当公务员的想法,这个仓库主要分享了他们上岸的一些经历和一些最佳实践,也有他们上岸之后的一些感想和感悟。

Vim 从入门到精通(中文)

地址https://github.com/wsdjeg/vim-galore-zh_cn

许多程序员可能了解过一点点 Vim,但从没用过,也不知道具体是怎么用的以及有什么有点,为什么有这么多人用。该仓库会从 Vim 是什么开始,讲述 Vim 的哲学,并带你入门 Vim 的世界。同时仓库中也记录列举了大部分用法和规则,其作为一个速查表也是很好用的。

工具推荐

整理编辑:CoderStar

摸鱼单词

地址https://apps.apple.com/cn/app/id1488909953?mt=12

软件状态:免费

软件介绍

软件作者自述:电脑大部分使用场景是用来办公,如果在办公之余可以背背单词就很好啦,于是就有了摸鱼单词。专注于利用碎片时间学习记忆英语单词。

和《摸鱼周报》相得益彰,作者也是一直在维护这个软件。

摸鱼单词

关于我们

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

往期推荐

iOS摸鱼周报 第四十一期

iOS摸鱼周报 第四十期

iOS摸鱼周报 第三十九期

iOS摸鱼周报 第三十八期

iOS 摸鱼周报 第四十一期

iOS 摸鱼周报 第四十一期

本期概要

  • 话题:In-App Events 数据分析上线了;线上沙龙:抖音 iOS 基础技术大揭秘。
  • Tips:在 Objective-C 中标记构造器为指定构造器。
  • 面试模块:如何检测内存泄露。
  • 优秀博客:野指针的捕获与防护。
  • 学习资料:一份可视化的 Web 技能列表。
  • 开发工具:SwiftInfo 是一个 CLI 工具,用于提取、跟踪和分析对 Swift 应用程序有用的指标。

本期话题

In-App Events 数据分析功能上线了

In-App Events 的展示效果数据可以在 App Store Connect 中的 App 分析查看了。应用分析还包括事件的页面展示,提醒和通知数据,以及由你的 In-App Events 触发的下载和重新下载的数量。每个指标都可以根据区域、资源类型、设备等进行查看,这样你就可以了解 In-App Events 是如何影响应用的发展和成功的了。

Analytics now available for in-app events

线上直播沙龙 - 抖音 iOS 基础技术大揭秘

内容介绍:如何保证抖音 App 的稳定性?如何给用户带来如丝般柔滑的流畅体验?如何在用户弱感知甚至无感知的情况下,推进抖音 App 的架构演进?如何利用容器等技术推进自动化测试?字节自研的 iOS 构建系统 JOJO 又是如何实现超级 App 构建效能提升 40% 的?本期字节跳动技术沙龙将以《抖音 iOS 基础技术大揭秘》为主题,为你全面揭开抖音 iOS 基础技术背后的技术能力!

沙龙时间:2022 年 1 月 22 日 14:00-17:25

报名地址字节跳动技术 iOS 技术沙龙正式报名开启

开发 Tips

整理编辑:师大小海腾

在 Objective-C 中标记构造器为指定构造器

这是一个开发 tip,一个编码规范,也是快手的一道面试题。

指定构造器模式有助于确保继承的构造器正确地初始化所有实例变量。指定构造器通常为类中接收全部初始化参数的全能构造器,是类中最重要的构造器;便利构造器通常为接收部分初始化参数的构造器,它们调用当前类的其它构造器,并为一些参数赋默认值。便利构造器是类中比较次要的、辅助型的构造器。

Objective-C 类的指定构造器模式和 Swift 的略有不同。在 Objective-C 中,为了明确区分指定构造器和便利构造器,可以使用宏 NS_DESIGNATED_INITIALIZER 标记构造器为指定构造器,其它未添加该宏的构造器都成为了便利构造器。

1
- (instancetype)init NS_DESIGNATED_INITIALIZER;

使用这个宏会引入一些规则:

  1. 指定构造器的实现只能且必须向上代理到父类的一个指定构造器(with [super init...]);
  2. 便利构造器的实现只能且必须横向代理到当前类的另一个构造器(with [self init...]),最终需要在当前类的指定构造器处终止链;
  3. 如果一个类提供了一个或多个指定构造器,它必须覆写其父类的所有指定构造器作为(退化为)该类的便利构造器,并让其满足条件 2。这样才能保证子类新增的实例变量得到正确的初始化。

如果违反了以上任何规则,将会得到编译器的警告。

示例代码:

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
@interface MyClass : NSObject
- (instancetype)initWithTitle:(nullable NSString *)title subtitle:(nullable NSString *)subtitle NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithTitle:(nullable NSString *)title;
- (instancetype)init;
@end

@implementation MyClass

- (instancetype)initWithTitle:(nullable NSString *)title subtitle:(nullable NSString *)subtitle {
self = [super init]; // [规则1] 指定构造器只能向上代理到父类指定构造器,否则会得到编译器警告:Designated initializer should only invoke a designated initializer on 'super'
if (self) {
_title = [title copy];
_subtitle = [subtitle copy];
}
return self;
}

- (instancetype)initWithTitle:(nullable NSString *)title {
/*
return [super init];
[规则2] 当该类设定了指定构造器也就是使用了 NS_DESIGNATED_INITIALIZER 后,其它非指定构造器都变成了便利构造器。
便利构造器只能横向代理到该类的指定构造器,或者通过横向代理到其它便利构造器最后间接代理到该类的指定构造器。
这里调用 [super init] 的话会得到编译器警告:
- Convenience initializer missing a 'self' call to another initializer
- Convenience initializer should not invoke an initializer on 'super'
*/
return [self initWithTitle:title subtitle:nil];
}

// [规则3] 如果子类提供了指定构造器,那么需要重写所有父类的指定构造器为子类的便利构造器,保证子类新增的实例变量能够被正确初始化,以让构造过程更完整。
// 这里需要重写 -init,否则会得到编译器警告:Method override for the designated initializer of the superclass '-init' not found
- (instancetype)init {
return [self initWithTitle:nil];
}

@end

简单来说,指定构造器必须总是向上代理,便利构造器必须总是横向代理

另外,在 Objective-C 中,你还必须覆写父类的所有指定构造器退化为子类的便利构造器,并且要遵循便利构造器的实现规则;而 Swift 则不用,因为 Swift 中的子类默认情况下不会继承父类的构造器,仅会在安全和适当的某些情况下被继承。Swift 的这种机制可以防止一个父类的简单构造器被一个更精细的子类继承,而在用来创建子类时的新实例时没有完全或错误被初始化。

在 Objective-C 中,使用宏 NS_DESIGNATED_INITIALIZER 标记构造器为指定构造器,可以充分发挥编译器的特性帮我们找出初始化过程中可能存在的漏洞(通过警告),有助于确保继承的构造器正确地初始化所有实例变量,让构造过程更完整,增强代码的健壮性。

参考:

面试解析

整理编辑:zhangferry

如何检测内存泄露

内存泄漏指的是程序中已动态分配的堆内存(程序员自己管理的空间)由于某些原因未能释放或无法释放的现象。该现象会造成系统内存的浪费,导致程序运行速度变慢甚至系统崩溃。

在 ARC 模式下,导致内存泄露的主要原因是循环引用,其次是非 OC 对象的内存处理、野指针等。针对内存泄露的检测方案也基本从以上几种类型中入手,它们可以分为两类:工具类和代码类。

工具类

工具类比较多:

  • Instruments 里的 Leaks

  • Memory Graph Debugger

  • Schems 里的 Memory Management

  • XCTest 中的 XCTMemoryMetric

前两种方式比较常见,后两种内存泄露还需要借助于 Xcode 导出的 memgraph 文件,结合 leaksmalloc_history 等命令行工具进行分析。工具类检测方案都有一个缺点就是比较繁琐,开发阶段很容易遗漏,所以基于代码的自动化内存泄露检测方案更适合使用。

代码类

代码类检测泄露方式有三个典型的库。

MLeaksFinder

地址:https://github.com/Tencent/MLeaksFinder

它的基本原理是这样的,当一个 ViewController 被 pop 或 dismiss 之后,我们认为该 ViewController,包括它上面的子 ViewController,以及它的 View,View 的 subView 等等,都很快会被释放,如果某个 View 或者 ViewController 没释放,我们就认为该对象泄漏了。

它是基于 Method Swizzled 方式,需要 Hook ViewController 的 viewDidDisappearviewWillAppear 等方法。所以仅适用于 Objective-C 项目。

LifetimeTracker

地址:https://github.com/krzysztofzablocki/LifetimeTracker

LifetimeTracker 是使用 Swift 实现的,可以同时支持 OC 和 Swift 项目。它的原理是用一个协议表达监听泄露能力,我们提前设置监听入口和允许存在的对象个数。内部维护一个类似引用计数一样的数值,进入监听会进行一个 +1 操作,还会监听该对象的 deinit 方法,如果调用执行 -1。如果该「引用计数」大于我们设置的最大对象个数,就触发可视化的泄露警告。

简化一些流程之后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
internal func track(_ instance: Any, configuration: LifetimeConfiguration, file: String = #file) {
let instanceType = type(of: instance)
let configuration = configuration
configuration.instanceName = String(reflecting: instanceType)

func update(_ configuration: LifetimeConfiguration, with countDelta: Int) {
let groupName = configuration.groupName ?? Constants.Identifier.EntryGroup.none
let group = self.trackedGroups[groupName] ?? EntriesGroup(name: groupName)
group.updateEntry(configuration, with: countDelta)
// 检测当前计数是否大于最大引用数
if let entry = group.entries[configuration.instanceName], entry.count > entry.maxCount {
self.onLeakDetected?(entry, group)
}
self.trackedGroups[groupName] = group
}
// 开始检测,计数+1
update(configuration, with: +1)

onDealloc(of: instance) {
// 执行deinit,计数-1
update(configuration, with: -1)
}
}

FBRetainCycleDetector

地址:https://github.com/facebook/FBRetainCycleDetector

上面两种方案都是粗略的检测,是 ViewController 或者 View 级别的,要想知道更具体的信息,到底哪里导致的循环应用就无能为力了。而 FBRetainCycleDetector 就是用于解决这类问题,因为需要借助 OC 的动态特性,所以该库无法在 Swift 项目中发挥作用。

它的实现相对上面两个方案更复杂一些,大致原理是基于 DFS 算法,把整个对象之间的强引用关系当做图进行处理,查找其中的环,就找到了循环引用。

核心是寻找对象之间的强引用关系,在 OC 语言中,强引用关系主要发生在这三种场景里,针对这三种场景也有不同的处理方案:

类的成员变量

通过 runtimeclass_getIvarLayout 获取描述该类成员变量的布局信息,然后通过 ivar_getOffset 遍历获取成员变量在类结构中的偏移地址,然后获取强引用变量的集合。

关联对象

利用 fishhook hook objc_setAssociatedObjectobjc_removeAssociatedObjects 这两个方法,对通过 OBJC_ASSOCIATION_RETAINOBJC_ASSOCIATION_RETAIN_NONATOMIC 策略进行关联的对象进行保存。

block 持有

理解这个原理还需要再回顾下 block 的内存布局,FBRetainCycleDetector 对 block 结构体进行了等价的封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct BlockLiteral {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct BlockDescriptor *descriptor;
// imported variables
};

struct BlockDescriptor {
unsigned long int reserved; // NULL
unsigned long int size;
// optional helper functions
void (*copy_helper)(void *dst, void *src); // IFF (1<<25)
void (*dispose_helper)(void *src); // IFF (1<<25)
const char *signature; // IFF (1<<30)
};

BlockLiteral 结构体的 descriptor 字段之后的位置会存放 block 持有的对象,但是并非所有对象都是我们需要的,我们只需要处理强引用对象即可。而恰恰 block 的引用对象排列基于寻址长度对齐,较大地址放在前面,且强引用对象会排在弱引用之前,所以从 descriptor 之后的成员变量,可以按固定的指针长度依次取出对象。这之后的对象用 FBBlockStrongRelationDetector 封装,但这有可能会多取对象,比如 weak 类型的引用其实是不需要捕捉的。

该库的做法是重写 FBBlockStrongRelationDetector 对象的 release 方法,仅设置标记位,然后外部调用它的 dispose 方法,这样其强引用对象都会调用 release,被调用这部分都是强引用对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static NSIndexSet *_GetBlockStrongLayout(void *block) {
...
void (*dispose_helper)(void *src) = blockLiteral->descriptor->dispose_helper;
const size_t ptrSize = sizeof(void *);
const size_t elements = (blockLiteral->descriptor->size + ptrSize - 1) / ptrSize;

void *obj[elements];
void *detectors[elements];

for (size_t i = 0; i < elements; ++i) {
FBBlockStrongRelationDetector *detector = [FBBlockStrongRelationDetector new];
obj[i] = detectors[i] = detector;
}

@autoreleasepool {
dispose_helper(obj);
}
...
}

当拿到以上所有强引用关系时就可以利用 DFS 深度优先搜索遍历引用树,查找是否有环形引用了。

FBRetainCycleDetector 的检测方案明显更复杂、更耗时,所以几乎不可能针对所有对象都进行检测,所以更好的方案是配合 MLeaksFinder 或者 facebook 自己的 FBAllocationTracker,先找到潜在泄露对象,然后分析这些对象的强引用关系,查找是否存在循环引用。

其他方案

在资料查找过程中还发现了另一个库 BlockStrongReferenceObject ,它只检测 Block 导致的循环引用问题,跟 FBRetainCycleDetector 类似,也是要分析 block 内存布局。但不同的是,它可以完全根据内存布局,来定位到强引用对象。主要是依据 block 和 clang 源码进行分析得出,真的非常强👍🏻,如果对实现细节感兴趣可以阅读这篇文章:聊聊循环引用的检测

参考:

检测和诊断 App 内存问题

draveness的源码分析 - FBRetainCycleDetector

优秀博客

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

1、大白健康系统–iOS APP运行时Crash自动修复系统

@皮拉夫大王:整个文章是非常经典的,作者介绍通过 method swizzling 替换 NSObject 的 allocWithZone 方法和 dealloc方法实现野指针拦截。

2、JJException

@皮拉夫大王:这个库需要自己指定探测哪些类对应的野指针。换句话说,就是我们自己指定 10 个类,那么这 10 个类的对象发生野指针时我们才能发现。如果在此之外,野指针监控不到。

3、iOS 野指针定位:野指针嗅探器
@皮拉夫大王:文章介绍了 2 个方案:(1)在开发阶段破坏内存,使野指针必现崩溃(野指针可能由于内存释放但未被写入导致崩溃不必现)。在 free 时,并不释放内存,保留内存,判断是否为 objc 对象,如果是 objc 对象则将对象 setclass 为自定义类,借助消息转发得到堆栈和类信息。监听系统内存警告,收到警告后释放。(2)hook objc 的 dealloc 方法,在 dealloc 时判断是否需要开启野指针探测,如果不需要则直接释放,否则将对象修改 isa 后保留并加入到内存池中,再次调用对象时会触发消息转发拦截到堆栈及对象类名信息。

4、iOS野指针定位总结

@皮拉夫大王:文章介绍方案如下:分类覆盖 dealloc 函数,并在 dealloc 中重新设置 isa 并不释放 obj,其中重新指向的 isa 是动态创建的。也就是说 dealloc 是 10000 个类,也会同步动态创建 10000 个类。

5、浅谈 iOS 中的 Crash 捕获与防护

@皮拉夫大王:推荐阅读的文章,文章不仅仅介绍了野指针相关内容,还介绍了崩溃相关的基础知识。

6、xiejunyi’Blog

@皮拉夫大王:坦白讲我并没有看完的文章,在做技术调研时发现的博客,文章内容比较深入并且能看出作者是有大量实战经验的开发者,因此推荐给大家。

学习资料

整理编辑:Mimosa

Visual Web Skills

地址https://andreasbm.github.io/web-skills/

这是一份可视化的 Web 技能列表,它对刚开始学习 Web 或已经工作多年并想学习新东西的人都很有用,你可以从中了解 Web 开发的大概路径和图谱,按顺序或者选择自己感兴趣的部分来看。除此之外最吸引人的是这个列表可视化的非常棒,每个图标符号都很大方美观形象,快来看一下!

工具推荐

整理编辑:CoderStar

SwiftInfo

地址https://github.com/rockbruno/SwiftInfo

软件状态:开源、免费

软件介绍

SwiftInfo 是一个 CLI 工具,用于提取、跟踪和分析对 Swift 应用程序有用的指标。除了该工具附带的默认跟踪选项外,还支持自定义编写 .Swift 脚本来实现额外的功能。

默认支持的工具包括:

  • IPASizeProvider
  • WarningCountProvider
  • LinesOfCodeProvider

更多细节请直接前往 repo homepage 查看。

SwiftInfo

关于我们

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

往期推荐

iOS摸鱼周报 第四十期

iOS摸鱼周报 第三十九期

iOS摸鱼周报 第三十八期

iOS摸鱼周报 第三十七期

iOS摸鱼周报 第四十期

iOS摸鱼周报 第四十期

本期概要

  • 话题:启用新封面
  • Tips:Swift 中的预编译
  • 面试模块:dyld 2 和 dyld 3 的区别;编译流程
  • 优秀博客:包依赖管理工具
  • 学习资料:从头开发一个迷你 Go 语言编译器
  • 开发工具:git 资源库浏览工具 Tig

本期话题

@zhangferry:2022 年第一期摸鱼周报,从本期开始我们会使用新的封面,新封面由设计师朋友 Polaris 设计。这个场景表达的主题就是摸鱼,工作中的摸鱼不代表我们不尽职,而是我们对自由生活的向往。既要努力工作也要 Work Life Blance,2022 年,加油!

开发Tips

整理编辑:zhangferry

Swift 中的预编译

Clang 中有预编译宏的概念,在 Xcode 中其对应的是 Build Setting -> Apple Clang - Preprocessing 中的 Preprocessor Macros。这里可以根据不同的 Configuration 设置不同的预编译宏命令,其中 Debug 环境下的 DEBUG=1 就是内置的宏命令,我们通常使用的以下写法就是对应的这个配置:

1
2
3
#if DEBUG
// debug action
#end

如果需要新增 Configuration,比如 Stage,我们想要一个新的预编译宏比如 STAGE 表示它,如果这么做:

在 Objective-C 的代码中是可行的,对于 Swift 代码则无效。这是因为 Swift 使用的编译器是 swiftc,它无法识别 clang 里定义的预编译宏。

解决方案是利用 SWIFT_ACTIVE_COMPILATION_CONDITIONS 这个配置变量,它对应 Build Setting 里的 Active Compilation Conditions。做如下设置即可让 STAGE 宏供 Swift 代码使用:

面试解析

整理编辑:zhangferry

dyld 2 和 dyld 3 有哪些区别

dyld 是动态加载器,它主要用于动态库的链接和程序启动加载工作,它目前有两个主要版本:dyld 2 和 dyld 3。

dyld 2

dyld2 从 iOS 3.1 开始引入,一直到 iOS 12 被 dyld 3 全面代替。它经过了很多次版本迭代,我们现在常见的特性比如 ASLR,Code Sign,Shared Cache 等技术,都是在 dyld 2 中引入的。dyld 2 的执行流程是这样的:

  • 解析 mach-o 头文件,找到依赖库,依赖库又可能有别的依赖,这里会进行递归分析,直到获得所有 dylib 的完整图。这里数据庞大,需要进行大量的处理;
  • 映射所有 mach-o 文件,将它们放入地址空间;
  • 执行符号查找,若你的程序使用 printf 函数,将会查找 printf 是否在库系统中,然后我们找到它的地址,将它复制到你的程序中的函数指针上;
  • 进行 bind 和 rebase,修复内部和外部指针;
  • 运行一些初始化任务,像是加载 category、load 方法等;
  • 执行 main;

dyld 3

dyld 3 在 2017 年就被引入至 iOS 11,当时主要用来优化系统库。现在,在 iOS 13 中它也将用于启动第三方 APP,完全替代 dyld 2。

dyld 3 最大的特点就是引入了启动闭包,闭包里包含了启动所需要的缓存信息,而且这个闭包在进程外就完成了。在打开 APP 时,实际上已经有不少工作都完成了,这会使 dyld 的执行更快。

最重要的特性就是启动闭包,闭包里包含了启动所需要的缓存信息,从而提高启动速度。下图是 dyld 2 和 dyld 3 的执行步骤对比:

dyld 3 的执行步骤分两大步,以图中虚线隔开,虚线以上进程外执行,以下进程创建时执行:

  • 前 3 步查找依赖和符号相对耗时,且涉及一些安全问题,所以将这些信息做成缓存闭包写入磁盘里,对应地址:tmp/com.apple.dyld。闭包会在重启手机/更新/下载 App 的首启等时机创建。

  • 进程启动时,读取闭包并验证闭包有效性。

  • 后面步骤同 dyld 2

iOS 13中dyld 3的改进和优化

iOS dyld 前世今生

编译流程

一般的编译器架构,比如 LLVM 采用的都是三段式,也即从源码到机器码需要经过三个步骤:

前端 Frontend -> 优化器 Optimizer -> 后端 Backend

这么设计的好处就是将编译职责进行分离,当新增语言或者新增 CPU 架构时,只需修改前端和后端就行了。

其中前端受语言影响,Objective-C 和 Swift 对应的前端分别是 clang 和 swiftc。下图整理了两种语言的编译流程:

前端

编译前端做的工作主要是:

  1. 词法分析:将源码进行分割,生成一系列记号(token)。
  2. 语法分析:扫描上一步生成的记号生成语法树,该分析过程采用上下文无关的语法分析手段。
  3. 语义分析:语义分析分为静态语义分析和动态语义分析两种,编译期间确认的都是静态语义分析,动态语义需运行时期间才能确定。该步骤包括类型匹配和类型转换,会确认语法树中各表达式的类型。

之后导出 IR 中间件供优化器使用。这一步 Swift 会比 ObjC 多几个步骤,其中一个是 ClangImporter,这一步用于兼容 OC。它会导入 Clang Module,把 ObjC 或者 C 的 API 映射为 Swift API,导出结果能够被语义分析器使用。

另外一个不同是 Swift 会有几个 SIL 相关的步骤(蓝色标注),SIL 是 Swift Intermediate Language 的缩写,意为 Swift 中间语言,它不同于 IR,而是特定于 Swift 的中间语言,适合用于对 Swift 源码进行分析和优化。它这里又分三个步骤:

  1. 生成原始的 SIL
  2. 进行一些数据流诊断,转成标准 SIL
  3. 做一些特定于 Swift 的优化,包括 ARC、泛型等

优化器

编译前端会生成统一的 IR (Intermediate Representation) 文件传入到优化器,它是一种强类型的精简指令集,对目标指令进行了抽象。Xcode 中的 Optimization Level 的几个优化等级: -O0 , -O1 , -O2 , -O3 , -Os,即是这个步骤处理的。

如果开启了 Bitcode,还会转成 Bitcode 格式,它是 IR 的二进制形式。

后端

这个步骤相对简单,会根据不同的 CPU 架构生成汇编和目标文件。

链接

项目编译是以文件为单位的,跨文件调用方法是无法定位到调用地址的,链接的作用就是用于绑定这些符号。链接分为静态链接和动态链接两种:

  • 静态链接发生在编译期,在生成可执行程序之前会把各个 .o 文件和静态库进行一个链接。常用的静态链接器为 GNU 的 ld,LLVM4 里也有自己的链接器 lld

  • 动态链接发生在运行时,用于链接动态库,它会在启动时找到依赖的动态库然后进行符号决议和地址重定向。动态链接其为 dyld

Swift.org - Swift Compiler

优秀博客

整理编辑:东坡肘子

1、iOS包依赖管理工具 – 来自掘金:小小青叶

@东坡肘子:本系列一共六篇文章,不仅从原理、使用、创建自定义库等方面,对 CocoaPods 和 Swift Package Manager 进行了介绍,并且对两种包管理工具进行了比较。

2、CocoaPods Podspec 解析原理 – 来自楚权的世界:楚权

@东坡肘子:在 CocoaPods 中,podspec 文件主要用于描述一个 pod 库的基本信息,包括:名称、版本、源、依赖等等。本文介绍了如何通过 DSL 方法将配置的属性保存在一个对象的哈希表中,通过构建一棵保存所有配置信息的树从而建立相互之间的依赖关系。

3、关于 Swift Package Manager 的一些经验分享 – 来自:字节跳动技术团队

@东坡肘子:Swift Package Manager 是 Apple 为了弥补当前 iOS 开发中缺少官方组件库管理工具的产物。相较于其他组件管理控件,他的定义文件更加轻松易懂,使用起来也很 Magic,只需将源码放入对应的文件夹内,Xcode 就会自动生成工程文件,并生成编译目标产物所需要的相关配置。同时,SPM 与 Cocoapods 相互兼容,可以在特性上提供互补。本文除了介绍 Swift Package Manager 的现状、常见使用方法外,还阐述了作者对于 SPM 未来的一些思考。

4、解决swift package manager fetch慢的问题 – 来自简书:chocoford

@东坡肘子:由于网络的某些限制,在 Xcode 中直接 fetch Github 上的 SPM 库并不容易。本文中给出了几种提高 fetch 成功率的解决方案。(编辑特别提示:Xcode 程序包中内置了终端、命令行工具等应用,任何在系统终端下的代理设定对其都不会产生作用。使用 SS + Proxifier 的方式可以实现让 Xcode 中的网络数据从指定代理通过)

5、Swift Package Manager 添加资源文件 – 来自掘金:moxacist

@东坡肘子:从 swift-tool-version 5.3 版本开始,Swift Package Manager 提供了在包中添加资源文件的能力。本文是 WWDC 2020 —— 【Swift 软件包资源和本地化】 专题演讲的中文整理。

学习资料

整理编辑:Mimosa

《µGo语言实现——从头开发一个迷你Go语言编译器》

地址https://github.com/chai2010/ugo-compiler-book

µGo 是 Go 语言的真子集(不含标准库部分), 可以直接作为 Go 代码编译执行,作者尝试以实现 µGo 编译器为线索,以边学习边完善的自举方式实现一个玩具语言,目前还没写完,对编译器或者 Go 感兴趣的小伙伴可以关注一下。这里有一份作者写的 Go 编译器定制简介 供参考,同时作者还有《Go语法树入门(出版名:Go语言定制指南)》《Go语言高级编程》等开源图书作品。

工具推荐

整理编辑:CoderStar

Tig

地址https://jonas.github.io/tig/

软件状态:开源、免费

软件介绍

Tig 是一个 git 资源库浏览器,采用 ncurses 开发,很适合习惯使用命令行进行 git 操作的小伙伴们。

Tig

关于我们

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

往期推荐

iOS摸鱼周报 第三十九期

iOS摸鱼周报 第三十八期

iOS摸鱼周报 第三十七期

iOS摸鱼周报 第三十六期

2021年度总结

2021年度总结

今年还是疫情年,可以预见的 2022 仍然会被疫情所笼罩。因为疫情的缘故出行被限制,各个行业也都面临不景气的现状。曾几何时我们对努力奋斗都开始有些不懈,躺平成了主流。罗曼罗兰说过:世界上只有一种英雄主义,那就是认清生活真相之后依然热爱生活。虽无英雄主义,但面对当下的艰难,我依然愿意去寻找天空中的彩虹。简单回顾下即将过去的 2021,2022 要整装待发啦。

普通的生活

普通生活中也有几件值得记录的事情。

半马

今年的北京半马,因为疫情原因,参赛人数减半,我算是运气爆表才中的签。因为平常运动不多,且离比赛只有四周左右的准备时间,给自己制定了如下跑步计划:

第 1 周:早晨 2km,晚上 3km

第 2 - 3 周:早晨 2km,晚上 5km,

第 4 周:早晨 3km,晚上 4km。

配速都是在 6 分 30 秒左右,这里没有一次跑太多就是为了防止肌肉损伤。跑步计划虽然没有严格到一天不落的执行,但基本都做到了,期间均正常饮食,训练结束称体重发现自己还瘦了 4 斤,跑步对减肥真的有效。因为比赛前的训练不当,导致比赛期间小腿肌肉仍然酸痛,算是带点小伤坚持了全程。最后成绩是 2 小时 35 分,也挺满意了。

回顾整个过程:清晰的目标 -> 清晰的训练 -> 满意的结果,这是完整的正向循环机制,希望以后每件认真对待的事都能劳有所得。

拔牙

有智齿这件事被查出来已经有一定时间了,每年体检的时候都会被医生提醒应该拔了。但一直都没有下定决心,疼的时候没法拔,不疼了又害怕拔牙太疼,一直拖到今年。直到上个月,媳妇悄悄给我挂了北大口腔的号,说这个号可不好挂,不要浪费了机会,我才决心去拔。

我是四颗智齿都有问题,第一次拔了左边两颗。只有打麻药的时候感到有些疼,牙拔出的瞬间其实是无感的,听到牙齿落入铁盘的声音,我才意识到原来牙齿已经拔出来了,总共不到 5 分钟。后续恢复了一周,就基本无感了。第二次拔了右边两颗,稍微费劲些,牙龈缝了两针,术后第一天一直发烧,第二天才退烧。虽然过程不轻松,给自己折腾够呛,拔完之后心里却踏实很多。因为已经拔掉了,我知道即使再难受,过两天就会好了,这事已经过去了,而没拔的时候,却总是处在担心拔牙的不踏实状态。

这其实就是长痛不如短痛啊,勉强可以接受的状态容易削弱人的意志,如果奋起抵抗,即使经历短暂痛楚,获取的却是长远的舒适。这是非常划得来的,所以该迈的坎不要害怕尽早迈过它。

生活趣事

今年的动手能力多少提高了一些,不只是做饭,现在清理空调滤网,刷油烟机都开始尝试了。

说起做饭,比着去年被迫的状态,今年做多了已经开始享受做饭的过程了。油烧热,倒上葱花,滋滋啦啦的,葱花爆香的味道扑鼻而来,再加肉或者菜,调味品一放,这不就完事了吗。复杂点的还需要再加几道工序,注意顺序上的差别,所有菜都差不多这个定式。

后来吃饭的过程养成了看美食视频的习惯,做好饭撑开桌子第一件事就是把 iPad 支起来,看「盗月社食遇记」或「绵羊料理」。过程中不仅见识了各样特色美食,还观看了很多美食复杂的制作工序,我要收回做饭容易的话了,其实每一行都不是看起来的那样简单。

说到 B 站,也推荐几个今年发现的宝藏 up 主吧,「-LKs-」、「有山先生」、「oooooohmygosh」、「贪玩歌姬小宁子」。有不少人认为 B 站为了扩张导致用户质量下降,而怀念过去的 B 站。我是不认同的,维护 B 站质量的主要是那些 up 主而不是用户,B 站模式本身就适合更广阔的人群,在社区和谐的土壤里, UGC 内容的创造力可以趋近于无限。B 站的服务用户应该更广,生命力也应该更强才对,我非常看好 B 站的,小破站给我冲起来!

影视

看了两部经典动漫,《进击的巨人》、《一拳超人》,对于在这之前只了解火影和海贼的我来说,这两部的剧情和画面多少都有点让我惊掉下巴,动漫还可以这样?《巨人》的漫画在今年的 4 月 9 号完结,当我打开知乎想窥探一些新剧情时,发现了和以往完全不同的评论风格,这之前都是夸谏山创多么多么厉害的,而在这之后都是骂的。随着翻看评论,大概了解了烂尾的现实。同时这也是一个神奇的现象,当一步作品足够优秀,吸引甚至影响到很多人之后,这个作品本身就会被认为是大家共有的一个东西,如果最终作品变质,那当初有多喜欢这个作者,后面就有多讨厌他。

不说让人失望的《巨人》了,还是来看可爱的波吉吧。我理解《国王排名》受欢迎的原因是它展现了很多人类温柔的情感。不想让人看到自己脆弱的一面而偷偷哭泣的波吉,表面严厉实际一直保护波吉的皇后,这些真善美是人们本身就有的情感,但随着步入社会,我们对他人反而是越来越多的戒心,越来越不愿意轻易与人交心长谈,我们正在失去的这些东西被展示出来,从而击中自己内心,所以我们会更爱他。

今年还发现一个宝藏播客节目:《卖鱼桥分桥》,小宇宙和 Podcasts 都可以搜到。关注到这个节目是因为创作者是 iOS 圈里的一位开发:没故事的卓同学。一开始是想看看开发者的副业生活可以怎么样,没想到却被好几期节目圈粉了。特别是歌单那几期,有点超神了,顺道我也来点 BGM 吧。

昨日像那东流水

离我远去不可留

今日乱我心多烦忧

抽刀断水水更流

举杯消愁愁更愁

明朝清风四飘流

中间卓同学还有几次尝试邀请我参加节目,都被我拒绝了,这里澄清下,主要原因还是我不够自信,总担心自己说不好。卓同学是我非常佩服的一个人,希望后面我能收拾好自己的信心再一起合作。

缓慢成长

阅读

今年阅读不算多,到也遇到了几本非常好的书,这里列出来简单总结一下,大家如果有兴趣可以买来看一看。

《邓小平时代》:这是一本邓公的人物传记,写了很多那个历史背景下的很多故事,非常详实,让我对政治这个词有一些不一样的认识。因为内容太敏感,这里就不发表评价了,大家如果对那个年代,对邓公感兴趣的话,这本书可以作为首选资料阅读。

《经济学要义》:这本书比较通俗易懂的把多个经济学概念进行了串联和解释。在我看来,经济学最大的作用是对社会经济现象的解释,书中有几个比较重要的经济学概念。

边际效益:效益是收益和付出的比值,带上边际就是最后一个单位的收益和付出。这里有一个边际效用递减规律,就是当在一件事情上投入过多成本之后,其带来的收益会越来越低。以工作举例,重复的工作事项,仅有第一次是边际收益大的,后续的重复过程收益都是在不断递减的。

机会成本:鱼和熊掌不可兼得是对机会成本的最好诠释,每天我们都在面对诸多选择,凭借自己的阅历和经验做出选择,并得到了想要的结果。但从经济学上来看,事情并没有结束,每个选项的背后都意味着放弃了其他选项,那些放弃的选项中收益最高的就是机会成本。

以看视频为例,当你想看某部影片时发现,正版网站可以直接观看但需要付费,盗版网站免费但需要一定的检索时间。如果你认为收益更大的肯定是看免费的了,但就是忘了考虑时间导致的机会成本。天下没有免费的午餐,不收钱可不代表免费,时间和注意力也是稀缺资源,哪个能给自己带来最大化收益才应该选哪个。

《终身成长》:不要用固定型思维,而是成长型思维,相信人本身是可塑的,这个不光是对自己,还可以用于教育。文中列举了很多教育孩子正确和不正确的方法,各位宝爸宝妈可以看看。

《暗时间》:刘未鹏信仰的东西应该就是思维改变生活,这是他博客的标题,也是这本书探讨的核心观点。书中提到很多有趣的心理学现象,来帮助我们理解自己为什么会有那种行为。其中一个叫:自利归因。意思是人们总是习惯的把一件事情发生的原因归结为对自己有利的情况,通俗来说就是人们总是倾向于为自己辩解。比如我们因为晚起导致上班迟到,遇到了堵车,我们就会认为迟到是因为堵车导致的。即使没有堵车,我们也会找到电梯慢等原因,但真实原因其实是晚起,就因为我们不愿承认自己的错误所以才会就近找一些借口。

再有如果是我想做某件事,又感觉自己不太擅长,做不了,就会找各种接口推脱。到最后确实没做成,回顾时的自我归因会是,「我不想做」,心理还想了各种借口,那件事也没有那么重要,以后还有的是时间。因为不愿承认我不能,而改成了我不想,就因为这种解释会让自己心里更舒服,这能获得暂时的心理安慰,却让我们忽视了自己的弱点。这个理解深深击中了我,促使我在复盘各类事情的原因时不要给自己找借口。

文学类看了几本日本小说,太宰治的《人间失格》,东野圭吾的《幻夜》《嫌疑人x的现身》《疾风回旋曲》《白马庄杀人事件》《假面舞会》。之前有看过《白夜行》和《解忧杂货铺》,发现自己成了半个东野圭吾粉丝了。不过不得不说东野圭吾的叙事技巧确实厉害,情节跌宕起伏,伏笔一个接一个。相比近期各种让人失望的影视剧来说,小说一般都不会让人失望。凭借对东野圭吾的喜爱,我一连又买了好多本他的书,目前收集了这么多:

小说看多了技术类书看的就少了,完整看完的有《Head First 设计模式》《Swift异步与并发》《Objective-C编程全解》。喵神的书也是一如既往的好,由浅入深,虽然一行并发代码都还没写,但感觉好像对整个设计架构已经有了大概的了解。虽然系统提供了一些方法用于适配 Swift 并发并降低可接入版本至 iOS13,但迁移成本仍然是比较高,导致使用率还很低,希望明年 WWDC 苹果对这部分的过渡有更多平滑方案。

《深入解析 Mac OS X & iOS 操作系统》和《深入理解计算机系统》也看了一些,这两本对我来说更像工具书,对某个地方有疑惑时会拿来翻翻学习一下。

摸鱼周报

去年的一项 OKR 是摸鱼周报全年能够产出 15+ 期,当时是才刚发了第一期内容。今年的结果是一共产出了 38 期,除了早期几篇不稳定和节假日休刊之外,其余时间均是每周一篇,已经远超去年的规划了。这当然少不了小伙伴们的帮忙,因为摸鱼周报本身的故事也不少,所以决定单独用一篇文章来写,相关内容大家再等等吧。

工作

赶到年底裁员,多少有些突然,海外部门应该是最严重的,裁员比例 7 成以上。在爱奇艺待了将近 1 年半,有幸接触到一位非常优秀的领导,学到了很多东西。也非常感谢期间一起合作过的小伙伴,祝大家前程似锦哈。

前段时间看了极客时间里的一个专栏:《10x程序员工作法》,它由火币网首席架构师郑晔整理。发现有很多内容跟自己的工作心得比较类似,这里结合一下来整理今年工作上的感悟吧。

以终为始

以始为终是专栏里的一个重要主题,其来源是《高效能人士的七个习惯》里的第二个习惯。以始为终的含义是以目标为导向,网上流传亚马逊 CTO 介绍亚马逊如何开发一项产品的顺序:

1、写新闻稿

2、写 FAQ

3、写用户文档

4、写代码

事件真实性有待考究,但这件事本身是具有参考意义的,惯性思维我们很容易按照既定顺序去思考一件事,但有时候倒着思考会给我们带来更多启示。

我们还可以利用这个做一些提前演练,比如有一个较大重构模块需要上线,在未开始之前就进行构思,如何做如何做,甚至考虑到如果出了线上事故该如何处理,是否要使用 AB 测等等。这是第一次创造,我们会有一个清晰的目标,之后采取实际行动时,对照这个目标,一步步落实,这是第二次创造。

任务拆解

工作中的很多事情都可以借助于任务拆解来开展,它的一个最大好处就是打开了我们抉择的选择范围。0 和 1 对应一件事情做还是不做,通常选哪个都是艰难的,但如果选项更多呢,0,0.1,0.2, 0.3 一直到 1,我们再做选择时就会容易一些了,这就是拆分之后的一个好处。

明确边界

工作中很多事情都是在不确定中找确定,比如我们要确定是否能如期完成开发,但开发中会依赖后端进度、设计进度、测试进度等,他们能否按照我们预想的节奏完成都是不确定的。这时可以尝试明确一下边界,依赖后端,就告诉他们我们能够接受的最晚完成时间是什么时候,设计和测试环节一样,中间由项目统筹,即使出问题了也可以有依据确认哪个环节。

与之类似也可以给自己定边界,根据任务拆解内容制定计划,什么阶段应该完成到什么程度。

有效协同

因为公司里几乎所有的工作都是需要协同才能完成的,所以如何有效协同,你去明白别人的意思和让别人明白你的意思都非常关键。

如何明白别人的意思相对简单些,重点就是提前了解对方要表达的东西。提前的作用比较重要,一方面可以防止在需求评审时被产品的思路带着走而遗漏细节,还可以提前发掘一些疑问点在会上讨论。有时我还会对照 PRD 想象每个要点实现成代码应该是什么样子的,详细的预演通常也会发现一些问题。

如何让别人明白自己的意思这里引申一个小故事。之前在小组里做过一次技术分享,当时准备了很多东西,我还尝试去想大家看到幻灯片时会问哪些问题,我又该如何回答。但实际效果却并没有达到我理想的样子,有提问但感觉是比较浅显的问题,我认为大家会疑问的地方却没有人提,所以多少感觉有些受挫。后来跟老大交流了这个问题,他的回答让我释然不少:技术分享本身能有一半人认真听且跟上分享者的节奏就非常不错了,因为分享者提供的内容通常是他擅长的领域,让一个学习者去跟专业的人员对比这是不对等的,不应该过多强求。但是如果你分享的内容不只是扩展视野还是需要大家马上使用的,可以使用提问的方式,抓个人问他一个问题,来确认他的听讲效果,同时也起到强调重点的作用。

涉及团队协作不要抱太高要求,不要把每个人想的太理想化,及时沟通,多次确认,这些才是有效协同应该采取的方式。

其他

还有一些其他感悟,就简单列举了。

数字化衡量任务:要让自己的工作内容可量化,这里比较适用于做 OKR,量化的好处是便于分析成果,没法量化的情况很容易迷失。

数据分析非常重要:曾经解决一个困扰很久的 Bug,就是从一堆数据里分析出来的。这里对应两项能力,SQL 和 Excel。

自动化:把越来越多重复性的工作做成自动化,这像是驯服计算机的一种手段。

敬畏代码:很多时候容易过于自信,感觉做了一个东西肯定没问题,对繁杂的检查有些不屑一顾。但程序没 bug 才是不正常的,要代码起码的尊重,就是细心检查,严格验证测试 Case。

OKR

今年 OKR 完成情况

O1:个人成长

KR 完成进度 总结
时间规划能力再提升,完整记录20天以上的时间开销 50% 这个目标本意是为了提高时间利用率,期间有按小时的维度记录一天,持续有两周,但发现记录本身并没有产生期望的效果。
阅读20本书,选择其中5本写出读后感 80% 20 本达到了,读后感只有两篇,既然上面也写了部分读后感,就算完成80%了吧😄。
全年跑步里程400公里 34% 咕咚记录的全年里程是136km,这个差的有些多,我反思,是我太懒了。
研究3只头部基金,自己做一次有计划的尝试,最终收益能高于市场平均线 0% 没有研究,收益为负。我承认理财对我来说确实没有吸引力,之后不能在做计划了( ̄ε(# ̄)
提升代码阅读量,阅读3个苹果底层库,并写总结分享 10% 又打脸了
提升代码书写量,非工作内项目达到20万行。有一个长期维护的开源库,对2-3个经典计算机问题,手写代码实现 10% 连续打脸

代码阅读和手写是重要非紧急的事情,我反思问题出在没有以终为始。当时心满意足的列完目标就完事了,后续没有及时追踪目标,导致都忘了有这回事。

O2:输出更多优质内容

KR 完成进度 总结
公众号粉丝达到5000 60% 当时多少有些膨胀了
公众号收入能抵消博客服务器及域名的支出 100% 今年接了几单推广,不光是覆盖住了服务器的支出,我们还买了一些资料
输出30+篇博客 167% 有摸鱼周报加持,轻松达到了
摸鱼周报出15+期 253% 有队友加持,轻松超过了

这部分都完成比较好,给自己鼓掌👏🏻。

2022年计划

O1:技术成长

KR 总结
LeetCode 100题 在算法上栽过跟头了,不能再栽了
阅读 1 本英文技术书籍 算法和英语也多少受霜神影响吧,不过这本来也是程序员非常重要的两项技能,没啥说的,干吧
非工作内项目代码量达到 5 万行 未完成目标继续

O2:个人成长

KR 总结
阅读 20 本书 不求多,能安静看完有收获就够了
前年跑步里程 300km 300km合理一些,再不跑就变成胖子了
学会一项新技能 已经确定学什么并找了一位非常厉害的老师了,先不说是啥
面基 10 位技术圈的朋友 克服下自己的社恐多出来走走,同时向各位朋友学习

不再设置输出内容的目标,2022 的主题就是新一轮的成长,整装待发,加油ヾ(◍°∇°◍)ノ゙

iOS摸鱼周报 第三十九期

iOS摸鱼周报 第三十九期

本期概要

  • Tips:混编|为 Swift 改进 Objective-C API。
  • 面试模块:HTTPS 证书有效性的验证过程。
  • 优秀博客:Core Data、Realm、MMKV 这几个库相关的一些介绍。
  • 学习资料:一个学习正则表达式的网站。
  • 开发工具:一个安装 Xcode 的 CLI 工具 xcinfo,一款开源的 Markdown 编辑工具 Mark Text。

开发Tips

整理编辑:师大小海腾

混编|为 Swift 改进 Objective-C API

NS_REFINED_FOR_SWIFT 于 Xcode 7 引入,它可用于在 Swift 中隐藏 Objective-C API,以便在 Swift 中提供相同 API 的更好版本,同时仍然可以使用原始 Objective-C 实现。具体的应用场景有:

  • 你想在 Swift 中使用某个 Objective-C API 时,使用不同的方法声明,但要使用类似的底层实现。你还可以将 Objective-C 方法在 Swift 中变成属性,例如将 Objective-C 的 + (instancetype)sharedInstance; 方法在 Swift 中的变为 shared 属性。
  • 你想在 Swift 中使用某个 Objective-C API 时,采用一些 Swift 的特有类型,比如元组。例如,将 Objective-C 的 - (void)getRed:(nullable CGFloat *)red green:(nullable CGFloat *)green blue:(nullable CGFloat *)blue alpha:(nullable CGFloat *)alpha; 方法在 Swift 中变为一个只读计算属性,其类型是一个包含 rgba 四个元素的元组 var rgba: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat),以更方便使用。
  • 你想在 Swift 中使用某个 Objective-C API 时,重新排列、组合、重命名参数等等,以使该 API 与其它 Swift API 更匹配。
  • 利用 Swift 支持默认参数值的优势,来减少导入到 Swift 中的一组 Objective-C API 数量。例如,SDWebImage 的 UIImageView (WebCache) 分类中扩展的方法,在导入到 Swift 中时,方法数量从 9 个减少到 5 个。
  • 解决 Swift 调用 Objective-C 的 API 时可能由于数据类型等不一致导致无法达到预期的问题。例如,Objective-C 里的方法采用了 C 风格的多参数类型;或者 Objective-C 方法返回 NSNotFound,在 Swift 中期望返回 nil 等等。

NS_REFINED_FOR_SWIFT 可用于方法和属性。添加了 NS_REFINED_FOR_SWIFT 的 Objective-C API 在导入到 Swift 时,具体的 API 重命名规则如下:

  • 对于初始化方法,在其第一个参数标签前面加 “__”
  • 对于其它方法,在其基名前面加 “__”
  • 对于属性,在其名称前加上 “__”

注意:NS_REFINED_FOR_SWIFTNS_SWIFT_NAME 一起用的话,NS_REFINED_FOR_SWIFT 不生效,而是以 NS_SWIFT_NAME 指定的名称重命名 Objective-C API。

可以看看:

面试解析

整理编辑:zhangferry

HTTPS 建立的过程中客户端是如何保证证书的合法性的?

HTTPS 的建立流程大概是这样的:

1、Client -> Server: 支持的协议和加密算法,随机数 A

2、Server -> Client: 服务器证书,随机数 B

3、Client -> Server: 验证证书有效性,随机数 C

4、Server -> Client: 生成秘钥,SessionKey = f(A + B + C)

5、使用 SessionKey 进行对称加密沟通

其中第 3 步,就需要客户端验证证书的有效性。有效性的验证主要是利用证书的信任链和签名。

证书信任链

我们以 zhangferry.com这个网站的 HTTPS 证书为例进行分析:

zhangferry.com 的证书里有一个 Issuer Name 的分段,这里表示的是它的签发者信息。其签发者名称是 TrustAsia TLS RSA CA,而我们可以通过上面的链式结构发现,其上层就是TrustAsia TLS RSA CA。再往上一层是 *DigiCert Global Root CA,所以证书签发链就是:*DigiCert Global Root CA -> TrustAsia TLS RSA CA -> zhangferry.com

其中 DigiCert Global Root CA 是根证书,它的签发者是它自己。根证书由特定机构颁发,被认为是可信的。我们的电脑在安装的时候都会预装一些 CA 根证书,查看钥匙串能够找到刚才的根证书:

如果能够验证签发链是没有篡改的,那就可以说明当前证书有效。

签发有效

要验证 DigiCert Global Root CA(简称 A) 签发了 TrustAsia TLS RSA CA(简称 B) ,可以利用 RSA 的非对称性。这里分两步:签发、验证。

签发:A 对 B 签发时,由 B 的内容生成一个 Hash 值,然后 A 使用它的私钥对这个 Hash 值进行加密,生成签名,放到 B 证书里。

验证:使用 A 的公钥(操作系统内置在钥匙串中)对签名进行解密,得到签发时的 Hash 值 H1,然后单独对 B 内容进行 Hash 计算,得到 H2,如果 H1== H2,那么就说明证书没有被篡改过,验证通过。

这些过程中使用到的对称加密算法和 Hash 算法都会在证书里说明。同理逐级验证,直到最终的证书节点,都没问题就算是证书验证通过了。流程如下:

图片来源:https://cheapsslsecurity.com/blog/digital-signature-vs-digital-certificate-the-difference-explained/

Hash 冲突的解决方案

当两个不同的内容使用同一个 Hash 算法得到相同的结果,被称为发生了 Hash 冲突。Hash 冲突通常有两种解决方案:开放定址法、链地址法。

开放定址法

开放定址法的思路是当地址已经被占用时,就再重新计算,直到生成一个不被占用地址。对应公式为:

其中 di 为增量序列,m 为散列表长度, i 为已发生的冲突次数。根据 di 序列的内容不同又分为不同的处理方案:

di = 1, 2, 3…(m-1),为线性数列,就是线性探测法。

di = 1^2, 2^2, 3^2…k^2,为平方数列,就是平法探测法。

di = 伪随机数列,就是伪随机数列探测法。

链地址法

链地址法是用于解决开放定址法导致的数据聚集问题,它是采用一个链表将所有冲突的值一一记录下来。

其他方法

再哈希法:设置多个哈希算法,如果冲突就更换算法,重新计算。

建立公共溢出区:将哈希表和溢出数据分开存放,冲突内容填入溢出表中。

参考:wiki-散列表

优秀博客

整理编辑:我是熊大东坡肘子

1、数据库的设计:深入理解 Realm 的多线程处理机制 – 来自:Realm

@我是熊大:Realm 是一个跨平台的移动数据库引擎,性能优于 Core Data 和 FMDB;接口十分人性化,使用很方便。本文能快速加深你对 Realm 的理解,并学习到更多有用的技巧,篇幅较长,耐心读下来,定会有所收获。每当我在 Realm 遇到问题时,本文几乎都能为我解惑。

2、如何降低Realm数据库的崩溃 – 来自掘金:我是熊大

@我是熊大:Realm 的崩溃,猝不及防,不仅仅是 Realm,任何数据库导致的奔溃总是个难题,总有那么零星几个让人没有头绪的 bug,本文总结了我在实际工作中遇到的问题和解决办法。

3、MMKV–基于 mmap 的 iOS 高性能通用 key-value 组件 – 来自掘金:我是熊大

@我是熊大:在开发中是否真的需要沉重的数据库?还是需要一个好用的 NSUserDefaults。如果 app 中只是简单的存储,那么基于 mmap 内存映射的 MMKV 可能更适合你,他比 NSUserDefaults 快 100 倍。

4、iOS 数据库比较:SQLite vs. Core Data vs. Realm – 来自 OSCHINA:由 我是菜鸟我骄傲、theDoctor 翻译

@东坡肘子:在 iOS 中,除了官方提供的 Core Data 外,还有很多其他的持久化方案可供选择。每种方案都有其各自的特点及适用场景。本文对 Core Data、SQLite 以及 Realm 进行了横向比较,并讨论了从 SQLite 或 Core Data 转换到 Realm 的路径及注意事项。

5、Core Data with CloudKit – 来自:东坡肘子

@东坡肘子:Core Data with CloudKit 是苹果为 Core Data 推出的网络同步解决方案,通过将 Core Data 同 CloudKit 进行结合,仅需使用少量代码,便可实现在苹果生态内跨设备、跨平台的数据实时同步。本系列文章一共 6 篇,详细介绍了有关如何进行私有数据库同步、公共数据库同步以及在不同用户间共享数据等内容。

学习资料

整理编辑:Mimosa

学习正则表达式

地址https://regexlearn.com/zh-cn

这是一个学习正则表达式的网站。它从零开始,可以让不懂正则表达式的小白简单入门,比较特别的是,它采用答题的方式,一步一步的带你了解正则表达式的工作方式以及原理,每一个小关卡都有对应的知识点和实时匹配展示,当你边写表达式的时候你就能看到对应的结果。另外这种关卡的方式也让人很有成就感,让你闯关欲罢不能!相信对不是很熟悉了解正则表达式的朋友来说是很好的学习材料。

工具推荐

整理编辑:CoderStar

xcinfo

推荐来源faimin

地址https://github.com/xcodereleases/xcinfo

软件状态:开源、免费

推荐语

Xcodes 的另一种选择,方便我们直接从苹果官网下载 Xcode。 据称下载速度比 Xcodes 更快。

xcinfo

Mark Text

地址https://marktext.app/

软件状态:开源、免费

软件介绍

一个简单而优雅的开源 markdown 编辑器,专注于速度和可用性,适用于 Linux, macOSWindows

其和 Typora 一样,也是单窗的形式。

Mark Text

关于我们

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

往期推荐

iOS摸鱼周报 第三十八期

iOS摸鱼周报 第三十七期

iOS摸鱼周报 第三十六期

iOS摸鱼周报 第三十五期

人物访谈 | 老司机周报主编 Parsifal 专访

人物访谈 | 老司机周报主编 Parsifal 专访

Parsifal 是老司机周报的主编,今年他给摸鱼周报提供了很多帮助和建议,这里对他再次表达一下感谢。这次邀请他来做一期访谈,主题分为两部分,一部分是老司机周报的发展故事和他的行业观察,另一部分是他对开发者的一些建议。如果大家还有其他问题欢迎留言提问~

简单介绍下自己吧

哈喽,各位摸鱼周报的读者好,我是 Parsifal,是一位 iOS 开发多年的老司机了。17 年底开始加入「老司机技术周报」编辑组,到 18 年底正式接手至今,主要负责其运营工作(其实就是给我们编辑组的大佬们打打杂)。二次元一点说,我算目前的二代目,当然我们也很快会迎来三代目,感谢大家一直以来的支持~

当初创办老司机周报的目的是什么,能讲一些当时的背景吗?现在回想来看,对比当时的目标,达成度怎么样?

老司机技术周报自 17 年 12 月创刊以来,已经更新了整整 4 年时间。在我的认知里,4 年对于一个社区组织的生命周期来说,是很长很长了,长到足够让一个小萌新成长为优秀的成熟开发者,甚至长到我们读者群可以完成一轮新陈代谢。

现在我们的很多读者,谈到周报编辑组,可能更熟悉的是负责我们社群运营的老王「Damon」,或者时不时会在周报中插播公告的「Parsifal」,但如果要谈到周报创办的往事,就需要请出我们的创始人 - 「没故事的卓同学」。作为整个编辑组的一代目,老卓一直被我们认为是编辑组的灵魂人物,即使他 18 年底已从编辑组“退休”,但他创办周报的初衷我们还是延续了下来。

在经过 14、15 年的野蛮发展后,17 年我觉得是 iOS 开发的一个巅峰尾巴。那会儿很多优秀的开发者依然在社区内活跃着,比如老卓就是其中一位。他是非常擅长和乐于分享的,在周报创办之前也深受大家喜爱。在社区内容输出还很爆炸又良莠不齐的当时,老卓就有了想法要将自己平常看到的优质内容,定期整理出来分享给大家。当然这种事情是人多力量大的,所以老卓又和几个好友聊了下,大家都觉得很有意义,就水到渠成地组成了编辑组的第一批元老 Damon\MM\味精\EF\四娘和 BlackSun 等。随着这几年的不断成长和纳新,周报编辑组已经有 30 多人了,但我们的初心依然没变 - 为读者输送有价值的内容。

这么多年的积累,让我们有了一个非常高质的资源库,我个人是非常喜欢在周报仓库检索内容的;WWDC 内参活动越办越好,质量和数量都有了很大提高;今年编辑组也尝试了线下沙龙的方式,用周报的影响力帮助企业对外输送高质内容;我们一直以来做的事情,也逐渐被官方关注和认可。

现在回想来看,老卓的目标在他“退休”那天就已经实现了,周报每期都能够顺利发布,18 年的时候周报已经有了相当的影响力,当时主要的两个更新渠道 GitHub 和掘金都收获了较大的关注群体。而于我个人而言,18 年底从老卓手里接过来运营,算超预期了吧。按我原计划,接手后我负责 18 年的收尾工作,让周报能够有个相对完整的 ending。

但后面做着做着发现,编辑组的朋友依然还有着热情继续做这个事儿,Damon 和邦本等人也愿意加入平时的运营,并且得益于老卓之前的管理原则:周报的编辑周期不强依赖于某个人,我们还是坚持了下来。

18 至 20 上半年左右,周报的发布其实也是轮值的,而后由于调整了发布渠道和时间,并且开始重视公众号运营,才转为主要由我负责发布,我有事的情况下再找其他人代班。然后就是整个编辑组还是得有个人做下统筹,负责一些人员流转,对外合作,以及内参啊,沙龙啊等等其他的活动。轮值没有具体的多长时间,看上一任自己意愿吧。接着编辑组不断吸收优秀的开发者,团队越来越大,新老编辑交替也在很自然地进行。

我的周报编辑生涯可能也很快会结束,但老司机技术周报应该还没到停刊的时候。关于这些,接下来我们一二三代目会一起在老卓播客细聊。大家可以开始期待周报三代目了~

到目前为止老司机周报已经出了180多期了,分享和传播了非常多优秀的资源和内容,某种程度上也推进了整个 iOS 行业的发展。作为活跃在这一领域最前沿的一群人,在这4年期间里你感受到的变化是什么?

作为周报这类聚合资讯性质的内容创造者,我们可以说是最了解社区的那批人了。我们的内容源自于社区,所以对社区发展方向和热度是十分敏感的。这几年,从行业上看,如我上面所说的,17 年算是一个尾巴,而 18 年至今整体趋势还是相对稳定去泡沫化。不过这其实就是一个行业正常的生命周期,萌芽到野蛮生长,稀缺到泡沫化,然后会到稳定的成熟期,最终到衰退期。

社区产出的内容,也伴随着行业的稳定,没有了之前百花齐放资讯爆炸的光景。整体感觉就是新的东西越来越少,老的东西还在一遍又一遍地被新人挖出来继续写。而我们的读者群里,0-2 年新人也几乎成了凤毛麟角,早两年虽然也少(我们的内容更偏中高级开发者),但不至于看不太到。

整体社区的内容产出来看,17 还是 OC 内容占了大部分,然后是慢慢国内外出现了分歧,国外 18 年开始 Swift 的内容占比越来越大,这几年几乎都见不到国外有 OC 方面的产出了。18 年开始至今的国内,则对跨端(ReactNative\Flutter\Weex)方案爆发了热情,至于 Swift 依然没看到大范围的使用。

创办老司机周报的过程,都遇到过哪些困难?未来老司机的走向是什么样的,有没有什么规划呢?

17 年底起步到 18 年上半年,都算比较顺利,刚开始大家的热情也比较高,也比较有精力做这些事情,同时那会儿社区的优质内容也多,也有很多新的东西被不断挖掘出来。而后我们就遇到了第一个比较大的困难,每周被推荐的内容不够,从而需要主编来花费更多时间去收集。

未来的走向我现在不太好说,但目前的形势是很好的,今年做的几个尝试反响也很好,我们会争取更大的影响力,并继续尝试与苹果做一些进一步的合作。当然,更具体的未来规划,就等我们下一任主编官宣后再聊了。

随着移动互联网红利的消失,对 iOS 开发的要求可能不再仅限于能做出一个 App。如果需要进一步提升,学习的广度和深度,哪一个帮助可能更大?

其实我不太认为说以深度为发展方向的开发者和以广度为方向的开发者存在明显的孰优孰劣。为什么会有这两种特征明显的开发者?还是市场需求决定的。这几年大厂产品越来越成熟,那么自然对专精人才的需求就随之增大,比如各类的极致性能优化、基建建设和效能提高等,团队发展到一定阶段,遇到瓶颈了这些需求就出现了;但同样的,全面手,比如大前端全栈开发者,依然是很多企业喜欢的。既然路有很多条,那么主要还是看自己适合和擅长哪条了。

选择了哪条路,如何提升?我觉得理好知识图谱,按点攻破即可,方法论不难,关键还在于执行力。前几年看过成甲的一本讲关于如何学习的书,如果还没有形成一套自己学习技巧的朋友,建议快速过一遍,取其精华。

面对 Flutter、Uni-App 等跨端技术的崛起,对iOS行业未来的发展你怎么看?对当前的iOS从业者有什么想说的?

iOS 环境这几年确实被吐槽得厉害,事实上我也有认识的同事辞职回家种花当老板。未来几年,iPhone 还会是销量前几的产品,苹果还会是那个科技巨头。iOS 开发者接下来会相对平稳了吧,直到下一个颠覆性产品到来。别把自己局限于 iOS 开发者,我们是做最接近用户端这一侧的开发者,现在是 iPhone,Apple Watch,以后还可能是 Apple Glasses。元宇宙革命正在进行~

开发人员除了掌握本职的技术知识外,现在也越来越多提到「软技能」这个词,在你看来有哪些软技能是在职业发展中比较重要的。

提到软技能,应该有不少人都或多或少看过或听说过《软技能:代码之外的生存之道》一书。这本书也有提到「十步学习法」,同样适用上面那个问题。当然,这本书讲的方面很广,从社交、理财到健身什么的,作者都分享了自己的经历和想法。我这里基于自己的一些职场经验,简略谈谈职场开发者可能需要的一些软技能吧。

需要掌握优秀的检索能力,检索能力往往会被很多人忽视,但在工作中却很重要。虽然我们常常调侃面向搜索引擎编程,可这就是绝大多数人的编码常态。好好提高检索能力,会让你效率大增,并且学习起来也比别人更快一步。检索第一步是去哪里搜?Google 作为程序员第一搜索引擎,自然不是个坏去处,但某些场景,可能有更好的地方。比如,我现在想看某些方面的文章,我更优先检索的就是周报仓库。知道了去哪里搜之后,怎么组织关键词去搜就直接决定了你能搜到什么了。编码相关的问题,我更推荐是直接按英文关键词拼接空格来搜,这样搜到的东西往往更准确,而且资源也会更多。最后就是一些相对进阶点的搜索技巧,引号精确匹配,默认模糊匹配等等。

老生常谈的提问的能力,作为一个某领域的萌新怎么去请教别人也很值得重视。一个好的提问可以帮你更快得到答案,也省了对方很多时间。以寻求帮助去提问的场景下,比如问你的导师,或者平常的一些技术群求教,一个好的提问大体是需要包含这几点内容:谦虚的态度,细致的问题背景,目前你的进度和结论,你期望得到的帮助等。

完成事情的能力,有些人叫「拿结果」的能力。在日常团队管理中,我是比较强调办事能力的,这一个能体现综合水平的点。有些开发者编码能力很强,但他并不一定能把事情做好。在团队内把事情做好,需要足够的责任心,良好的沟通协作能力,过硬的业务水平,有时候还需要有很强的韧性。

开发的职业发展过程中大多数都会经历瓶颈期,你有没有经历过类似的阶段,后来是如何突破的?对于处于这种阶段的同学,你有什么好的建议吗?

可能是自己没有很高的追求,我的职业发展相对比较顺利。所以给不了很有价值的建议。但如果说我自己真的遇到了,我估计会索性放空自己一阵子,任性放纵自由一段时间,都放松下来后,再来思考下一个阶段需要怎么走。生活压力很大,并不是每个人都有这样的机会和勇气这么处理,总之挺过去就好。

人生就是无数习惯的总和。在你工作中遇到过哪些开发人员好的和不好的习惯,能给我们分享一下吗?能再分享一个你自身具备且感觉对自己帮助较大的好习惯吗?

有些小伙伴比较好的习惯是不定期的去归纳总结,将所做过的事情,解决好的问题,新掌握的知识,都去做一个阶段性的总结。为自己的总结再设立几个问题,通过这几个问题去判断总结的效果是否理想。

不好的习惯,有些小伙伴遇事先条件反射性地自我否定算一个吧。碰到一个困难,第一反应就先觉得这个不行,那个不可能,这不是我的问题等等,本能地去推脱,久而久之也就丧失了正确评估一个事情的能力。或许是出于自我保护意识,察觉到威胁先跑路,但这确实是很不好。于己,这样会让自己的上限不断被压缩,仅维持在一个相对安全的小舒适区;于人,不断地被否定,也会影响对你的观感。

我一直认为开发人员不应该扔掉代码,尤其是中低层管理,这很容易会让自己失去竞争力。平常写写代码,保持编码逻辑的触觉,也能让自己跟团队其他伙伴更能走到一块。

另外对于普通开发者也好,技术管理层也好,定期去关注业界动态和社区技术生态发展,也是很重要的。每周看老司机技术周报就是一个很好的途径,哈哈哈~

很多开发都知道学习的重要性,但又常常陷到工作中无法抽身学习,就学习和工作的平衡性应该如何维持呢?

对于很多人来说,这可能是一个普遍的问题。这里聊下我自己的看法吧。首先还是需要从工作中去学习,毕竟这占据了我们大部分精力,去和自己的直属 Leader 多沟通想法,将一部分想学习的能力与自己的工作内容结合起来,落到工作中去实践。

然后是必须注重学习效率,一个阶段做一个事情,贪多嚼不烂;最后,虽然我很不鼓励内卷,平常自己也不 996,但很多人拉开差距真的就是在工作之外的时间。去培养自己的兴趣,将能够给自己带来乐趣的学习内容安排在非工作时间。

讲一个最近的生活感悟吧?

杭州疫情在这几天又复发了,且有迅速扩张的态势。注意防护,没事少出门,听从指挥,少给别人添乱~