普通视图

发现新文章,点击刷新页面。
昨天 — 2025年4月7日掘金 iOS
昨天以前掘金 iOS

2.流程控制

作者 思考着亮
2025年4月6日 15:25
if-else 的特殊用法 if后面的条件可以省略小括号 条件后面的大括号不可以省略 3.if后面的条件只能是Bool类型的 上面的语句会报错,也就是只能显式的转化是一个Bool类型 while 语句

Tagged Pointer:苹果工程师的内存优化艺术

作者 布多
2025年4月2日 20:54

Tagged Pointer 的前世今生

在 iOS 开发的早期阶段,所有对象都采用传统的堆内存存储方式。每个对象指针都指向堆内存中的一块区域,这块区域不仅存储着对象的实际数据,还包含了类型信息、引用计数等元数据。这种存储方式虽然通用且灵活,但对于一些简单的数据类型(如 NSNumber 存储的小整数 10、200 等),却显得有些"大材小用"。试想,仅仅为了存储一个小整数,就需要在堆上分配内存、维护引用计数、进行内存回收,这不仅造成了内存空间的浪费,还会因为频繁的内存操作而影响系统性能。

为了解决这个问题,苹果的工程师们提出了 Tagged Pointer 技术。这项技术的核心思想是:对于一些小型数据,将其直接编码到指针中,而不是在堆上分配内存。这种巧妙的设计不仅节省了宝贵的内存空间,还通过减少内存分配和释放操作显著提升了性能。

这种创新的效果是立竿见影的。根据 WWDC2013 苹果官方发布的数据显示,采用 Tagged Pointer 技术后,相关操作获得了显著的性能提升:内存占用直接减少了 50%,数据访问速度提升了 3 倍,而对象的创建和销毁速度更是实现了惊人的 100 倍提升。这些数据充分证明了 Tagged Pointer 技术在内存优化和性能提升方面的巨大价值。

技术原理深度解析

虽然 Tagged Pointer 技术在 iOS 中被广泛应用于 NSNumber、NSDate、NSString 等多个对象类型,但 NSNumber 是最具代表性的例子。因此,本文将以 NSNumber 为例深入讲解其实现原理。其它对象的实现机制与 NSNumber 类似,读者可以举一反三。

值得注意的是,在现代 Xcode 版本中(具体从哪个版本开始不太确定),苹果为了增强数据安全性,对 Tagged Pointer 进行了数据混淆处理。这种混淆机制使得开发者无法通过直接打印指针来判断一个对象是否为 Tagged Pointer,也无法解析其中存储的具体数据。

为了便于调试和观察 Tagged Pointer 的底层实现,我们需要关闭这个数据混淆功能。只需要将环境变量 OBJC_DISABLE_TAG_OBFUSCATION 设置为 YES 即可。

operator.png

由于 NSNumber 的源码未开源,所以我们通过分析结果来了解 Tagged Pointer 的实现原理。以下是我的调试结果:

result1.png

通过分析打印结果,我们可以看到 Tagged Pointer 的精妙设计:它将数据直接编码在指针中,而不是在堆内存中分配空间。具体来说:

  1. 指针的第 6 位到第 60 位(从左往右数)用于存储实际数据。这意味着一个 NSNumber 对象最多可以存储 55 位的数据,即 0x7FFFFFFFFFFFFF,对应的十进制为 36028797018963967。

  2. 指针的其余位则用于存储元数据:

    • 第 1 位(最高位)作为 Tag 标记,用于标识这是一个 Tagged Pointer;
    • 第 2-5 位用于存储对象类型(如 NSNumber 为 3,NSString 为 2);
    • 最后 4 位用于存储数据类型(如 int 为 2,long 为 3)。

Tagged Pointer 的内存分布图如下所示:

TaggedPointer.png

需要注意的是,不同平台和系统版本下,Tagged Pointer 的实现细节可能有所不同。例如,在 iOS 模拟器和真机环境中,Tag 位和数据位的存储位置就存在差异:模拟器中 Tag 位在最低位,而真机中 Tag 位在最高位。这种差异主要是由于不同平台的内存对齐要求和处理器架构特性导致的。因此,在调试过程中如果发现结果与预期不符,可以看看是不是平台或系统版本的问题。

这种设计巧妙地利用了 64 位系统的特性。在现代操作系统中,由于虚拟内存地址空间的限制,64 位指针实际上只使用了最低的 47 位来寻址,这意味着在正常情况下,合法的内存地址最高位一定是 0。Tagged Pointer 正是巧妙利用了这一特点,通过将最高位设为 1 来标识这是一个特殊的指针。Tagged Pointer 还将这些 "空闲" 的位进行了重新规划利用:一部分用于存储类型标记,另一部分用于直接存储数据。这样,对于小型数据(如小整数、短字符串等)就不再需要额外分配堆内存,而是直接将数据编码在指针中。这种设计不仅完美地保证了数据的完整性和访问效率,还通过消除堆内存分配、引用计数管理等开销,实现了内存使用的极致优化。

虽然 NSNumber 的源码未开源,但我通过深入分析 objc_runtime 源码,还是找到了一些关键的入口函数。这些函数揭示了 Tagged Pointer 的底层运作机制,包括数据混淆(其实就是启动的时候生成一个随机数,然后进行异或 ^ 运算)以及 Tag 和 Data 的存储位置等核心实现细节。以下是几个比较重要的函数,碍于篇幅这里就不展开了,感兴趣的同学可以下载 runtime 自行研究,它们共同构成了 Tagged Pointer 的基础框架:

isTaggedPointer => 判断是否是 Tagged Pointer
_objc_makeTaggedPointer => 生成 Tagged Pointer
_objc_getTaggedPointerTag => 获取 Tag
_objc_getTaggedPointerValue => 获取 Value
initializeTaggedPointerObfuscator => runtime 启动时初始化 Tagged Pointer 混淆器

Tagged Pointer 的类型编码

在 Tagged Pointer 中,系统使用特定的位来编码对象类型和数据类型。以下是详细的编码对照表:

对象类型编码

对象类型编码存储在指针的第 2-5 位。

对象类型
2 NSString
3 NSNumber
4 NSIndexPath
5 NSManagedObjectID
6 NSDate

数据类型编码

数据类型编码存储在指针的最后4位。

数据类型
0 char
1 short
2 int
3 long
4 float
5 double
6 long long

判断 Tagged Pointer 的原理

系统是如何判断一个指针是否是 Tagged Pointer 的呢?其实,在 objc4-906 源码中,我们可以找到 isTaggedPointer 的实现细节。

bool isTaggedPointer() {
    return _objc_isTaggedPointer(this);
}

#define _OBJC_TAG_MASK (1UL<<63)
static bool _objc_isTaggedPointer(void *ptr) {
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

从源码中可以看到,系统通过位运算 & 操作获取指针的最高位,并与 _OBJC_TAG_MASK(1UL<<63,即最高位为1的掩码)进行比较。如果结果等于 _OBJC_TAG_MASK,就表示这个指针是一个 Tagged Pointer(简单的说就是判断指针的第一位是否为1)。这种判断方式简单高效,只需要一次位运算就能完成判断。

注意事项

虽然我们一般不需要关心 Tagged Pointer 的具体实现细节,但在使用过程中还是有一些需要注意的地方:

  • Tagged Pointer 不是传统意义上的 ObjC 对象,它没有常规对象所具有的 isa 指针。因此,不要尝试直接访问或操作其 isa 指针,应该始终通过系统提供的公开接口来操作这些对象。

  • 出于安全考虑,在现代 Xcode 版本中,苹果对 Tagged Pointer 实现了数据混淆机制。这意味着即使你通过某种方式获取到了指针的原始值,也无法直接解析出其中存储的数据。正确的做法是使用框架提供的标准方法来访问数据。

  • Tagged Pointer 的具体实现细节可能会随着系统版本的更新而改变。因此,不要在代码中依赖其当前的实现方式(如位编码规则),这可能会导致你的应用在未来的系统版本中出现兼容性问题。

  • 在进行内存调试或性能分析时,要注意 Tagged Pointer 对象的特殊性。它们不会出现在常规的内存分配统计中,因为它们实际上并不占用堆内存。

关于 Tagged Pointer 有一个面试题,碍于篇幅,这里就不展开了,感兴趣的可以看一下 老生常谈内存管理 中的 “相关题目” 章节。

参考资料

WASM I/O 2025 | MoonBit获Kotlin核心开发,Golem Cloud CEO高度评价

作者 MoonBit
2025年4月2日 18:48

图片

图片

2025 年 3 月 27 日至 28 日,为期 2 日的 WASM I/O 2025 大会在巴塞罗那圆满落幕。期间,众多 WASM 社区领袖和技术专家,共同带来了 30 余场精彩的演讲,共同探讨云原生和开源领域的前沿洞察、核心技术与最佳实践。作为一门专为 WebAssembly 优化的新兴编程语言,MoonBit 作为首个中国开发者平台受邀在本次大会上进行技术分享。

主题演讲

在本次活动中,MoonBit 负责人张宏波带来了《MoonBit & WebAssembly》主题演讲。作为一门专为 WebAssembly(Wasm)设计的编程语言,MoonBit 凭借其极致的性能优化、创新的语法设计及开发者友好的工具链,展示了在浏览器与服务器端的全栈潜力。以下是本次演讲的核心技术回顾。图片

张宏波开宗明义地指出,现有语言在 Wasm 生态中存在三大痛点:二进制体积臃肿、编译速度缓慢以及运行时性能不足。MoonBit从语言设计之初便围绕Wasm优化,实现了极简二进制体积、闪电编译速度、高性能运行时的突破。

图片

MoonBit在设计时非常重视开发者体验,在工具链设计上体现了 “开发者优先” 的理念。其云 IDE 无需本地安装,集成并行类型检查、实时错误提示及基于 ChatGPT 的代码补全功能。调试支持亦是一大亮点,开发者可直接通过 Chrome DevTools 对 Wasm 模块进行断点调试和性能分析,与传统 JavaScript 工作流无缝衔接。MoonBit 与 WasmGC 的深度整合,使其在浏览器端表现尤为亮眼。通过零成本 JS 互操作Unicode 安全算法,在浏览器端实现无缝集成与性能突破。图片

针对服务器端开发,MoonBit 借助 Wasm 组件模型(Component Model)实现了跨语言协作的模块化架构。通过标准化接口定义语言(WIT),开发者可生成跨语言绑定。在编写 HTTP 服务器的实践中,MoonBit 生成的二进制大小仅为 27KB(Rust 为 100KB,Python 为 17MB)。

在未来,MoonBit 将深度整合组件模型,将 wasm-tools 直接嵌入工具链,支持 WASI 异步流(Phase 5提案)等前沿特性。

图片

MoonBit 的潜力不仅限于 Wasm,其支持多后端编译的特性为开发者提供了灵活的选择:编译为 C 代码时可运行于嵌入式设备(MCU),而实验性 JavaScript 后端则支持 Elm 架构,用于构建全栈应用。

对于一门语言来说,生态系统和开发者体验是其获得广大社区认可的必备条件。在 2025 年,MoonBit 将达到 Beta 版本,并进一步完善语言工具链与生态,随着 Wasm 在边缘计算、微服务等领域的深化,MoonBit 或将成为开发者拥抱下一代 Web 标准的关键选择。

社区反馈

在本次演讲后,Kotlin/Wasm 作者 Zalim 在社交媒体上表示:"MoonBit 在 WebAssembly 平台实现了精彩的成果",对 MoonBit 在 WASM 方向的技术成果给予高度认可。

图片

ZivergeTech & Golem Cloud 公司 CEO John A De Goes 表示:“与张宏波在 WASM I/O 见面后,我对 MoonBit 未来更加充满期待!MoonBit 融合了 Rust 语言的精华特性,还增加了垃圾回收(GC)机制,并优先关注工具链、性能和稳定性。欢迎大家加入我,一起参加在 LambdaConf 举办的 GolemCloud 黑客松活动,届时我会使用MoonBit!” 

图片

MoonBit 在本次 WASM I/O 上的精彩亮相吸引了海外社区诸多关注,我们期待未来 MoonBit 在全球开发者社区的无限潜力!

iOS启动优化 - 1分钟让你的启动时间降低 1s

作者 yidahis
2025年4月1日 16:01

先回答标题

原理是通过减少启动时加载的动态库数量达到目的。那一分钟就可以?是的,因为 CocoaPods 默认是会加载所有的依赖库,可以在一分钟内通过删除 Other Linker Flags 中的链接标记,取消启动时自动加载的动态库。那 1s 是不是夸张了?没有一点点夸张,不信大家往下看。(文章结尾有惊喜!)

准备工作

如何测量启动时间?

咱们首选还是 instruments,它的 App Launch 工具 可将启动过程拆解为关键阶段(如 dyld 加载、Runtime 初始化、首帧渲染等),并量化各阶段的耗时。

image.png

如图,这里精确的量化了本次程序运行总的时长、加载可执行文件时长(包含主程序、动态连接器、动态库、各种初始化等)、加载三个动态库的运行时长。这里重点关注加载的动态库,它们的特点是描述以 Map Image + <动态库路径>

如何知道加载了哪些动态库?

如上图,其中以 Map Image 开头的描述表明这就是一个动态库。dlopen() 函数调用后也会生成一个同样的任务。 除了 instruments 还有其他手段。这里就给出一个笔者常用的一个工具。 新建一个类 LazyLoader,将以下代码拷贝到全局调用区。

// 打印动态库名称(兼容 M1/Mac 和 iOS)
void filterThirdPartyLibs(void) {
    uint32_t count = _dyld_image_count();
    for (uint32_t i = 0; i < count; i++) {
        const char* path = _dyld_get_image_name(i);
        if (!strstr(path, "/usr/lib/") &&
            !strstr(path, "/System/Library/")) {
            NSString* pathStr = [NSString stringWithCString:path encoding:NSUTF8StringEncoding];
            NSLog(@"Third-party: %@\n", pathStr.lastPathComponent);
        }
    }
}

// 在程序启动时自动调用
__attribute__((constructor)) static void runtime_init(void) {
    filterThirdPartyLibs();
}

见证奇迹的时刻即将来到,开始发车了。

首先通过 pod 新建一个私有 LoadAFDemo

pod lib create LoadAFDemo

接下来在 podfile 中添加以下依赖,并执行pod install

pod 'AFNetworking'

然后将工具 LazyLoader 拖入工程中并进行桥接。现在开始运行项目,可以得到如下结果:

image.png 可以看到 AFNetworking 在其中,这里过滤了系统的库,其他两个是主工程相关,因为开发环境被拆分为两个。 这个时候可以看到主工程的 Other Linker Flags 很简单,只有 AFNetworking

image.png

接下来打开App Launch 工具查看总的时长 242.06ms, 这个数字很重要,它代表了 pre-main 阶段的启动耗时。

image.png 如图,动态库的加载描述都是以 Map image 开头,其中第一个是笔者添加的依赖库 AFNetworking ,下面三个都是系统库的。

  1. 在 podfile 中添加一些依赖库。
pod 'SDWebImage'
pod 'Realm'
pod 'RxTheme','4.1.1'
pod 'RxGesture','3.0.2'
pod 'NSObject+Rx','5.1.0'
pod 'Moya','~> 14.0.0'
pod 'Alamofire', '~> 5.2.1'
pod 'SwiftyJSON','5.0.0'
pod 'R.swift','5.2.2'
pod 'CryptoSwift','1.4.2'
pod 'SnapKit','5.6.0'
pod 'FlexLayout','1.3.20'
pod 'PinLayout','1.9.2'
pod 'SwifterSwift/Foundation','5.2.0'
pod 'SwifterSwift/UIKit','5.2.0'
pod 'WMPageController','2.5.2'
pod 'CYLTabBarController'
pod 'KMNavigationBarTransition'
pod 'Firebase/AnalyticsWithoutAdIdSupport','9.6.0'
pod 'Firebase/Crashlytics','9.6.0'
pod 'FirebaseCoreDiagnostics','9.6.0'
pod 'FirebaseInstallations','9.6.0'
pod 'FirebasePerformance','9.6.0'
pod 'AppsFlyerFramework','6.5.2'
pod 'MoEngage-iOS-SDK','7.1.4'
pod 'MoEngageInApp', '2.1.2'
pod 'MORichNotification','5.2.0'
pod 'ZLPhotoBrowser', '4.2.5'
pod 'FCUUID','1.3.1'

此时运行pod install再观察,发现刚才添加的依赖库都被添加进来。

image.png

接下来运行项目,查看控制台输出:

image.png

总计44个三方库加载了,这里只截取部分。

接下来再次打开App Launch 工具查看总的时长为 1.47s, 比原来多出了近 1.2s。是不是很惊讶,这里的三方库加载耗时基本在 1ms 左右,共 44 个那也最多 100ms 才对。这里先卖个关子,请看接下来的表演。 image.png

接下来打开主工程,删除 Other Linker Flags 中的所有内容,再次运行项目。

image.png 发现三方库一个也没有加载。再次打开App Launch 工具查看总的时长为 197.07ms

image.png 看到这里的你,是不是惊讶,从 1.47s197.07ms,启动效能完全是飞升了。那这个做法可以直接运用到项目中去吗?接下来通过一些问题来解开谜题。

一些疑点

  1. 清空 Other Linker Flags 中的依赖后,代码能运行吗?
    答:能运行,不过相关的三方库会在 pre-main 阶段加载。请看下图, image.png 上图中首先 import AFNetworking 库,然后使用 AFHTTPSessionManager 获取其实例,且 callAF 方法没有任何地方调用,只是定义了。运行项目可以看到,控制台中输出了 AFNetworking,没有编译和运行报错。(如果只是 import 是不会触发链接操作的)。这里暂且把这种行为定义为 Real Import

  2. 那为什么会这样呢?
    其实答案很明显,获取 AFHTTPSessionManager 实例是编译期就确定的行为。而编译没有报错,那英爱是系统在 pre-main 阶段通过 LC_LOAD_DYLIB 指令加载 AFNetworking

  3. 在私有podspec中添加了 AFNetworking依赖, 还需要清除私有podspec中的 Other Linker Flags 吗?
    答:在私有库没有 Real Import 的前提下,不需要清除 Other Linker Flags 也可以实现依赖的三方库不被加载。就是说,私有podspec断开了和主工程链接关系,那么这个 AFNetworking 也就没有断开了和主工程的联系,所以即使 Other Linker Flags 添加了链接标记,也只是 AFNetworking 和当前私有库的。

  4. AFNetworking 换成 Alamofire 也是一样的吗?
    答:是的

  5. 没有加载AFNetworking的情况下,通过反射(NSClassFromString(@"AFHTTPSessionManager"))可以拿到实例吗? 答:当然不可以,需要时先动态加载 AFNetworking

  6. 说了这么多那 dyld 的加载动态库的原则是什么
    答:这个问题主要与 Mach-O 的文件结构有关。可以使用 otool -l <动态库路径> 查看文件中所以依赖的动态库。主要有两个命令LC_LOAD_DYLIBLC_LOAD_WEAK_DYLIB, 其实上面的问题中提到的Real Import实际上是执行 LC_LOAD_DYLIB 命令将动态库加载到强依赖库列表中,也就对应在Other Linker Flags中标记为-framework的动态库。LC_LOAD_WEAK_DYLIB命令将动态库加载到弱依赖库列表中,对应于Other Linker Flags中的-weak_framework标记的动态库。而无论是强依赖还是弱依赖,正常情况情况下都会加载。

  7. 你搞这么麻烦,直接用系统支持的 -weak_framework标记不就行了? 答:这个笔者经过实测,在目前的系统版本(iOS18)是不行的。经过查阅官方文档发现最后更新日期是2013年,应该是很久没有更新了。而且现在大部分使用的是dyld3

惊喜

为了更加直观的观察依赖关系,笔者专门开发了一个脚本工具。优点是:非常直观和方便。下面以咸鱼 iOS 为例。传送门

xianyu-outputs.html.png

都2025年,你竟然还敢买iOS的源码?

作者 iOS阿玮
2025年4月1日 10:22

App Store审核问题或3.2f咨询,请公众号留言。不免费,不闲聊。iOS研究院

序言

买源码这件事情在互联网公司初创是最常见的行为,除了节约了开发成本,更为重要的是节约了开发的时间

对于想做某方面业务,但是完全不懂技术的初创者来说,是再好不过的选择了。

尤其是在2015~2020年,口罩前期不少外包公司也是雨后春笋般。那个时候没有这么多较为知名的头部产品,也是人人都是产品经理的时代

做微商的想做自己的商城App,做化妆品、做珠宝、做鞋子的,等等...

转折点

在步入2022年之后,苹果先后提高了开发者的门槛。首先就是大陆区账号注册的难度提升,增加了人脸拍摄。提高了代码查重的算法。之前矩阵的打法也就是马甲包,不断沦为苹果重点照顾的重灾区

衍生的条款

在算法一步步提升的背景下,Guideline 4.3(a) - Design - Spam中拓展了3点内容:

  • We noticed your app still shares a similar binary, metadata, and/or concept as apps previously submitted by a terminated Apple Developer Program account.

译:我们注意到你的应用程序仍然与之前被终止的苹果开发者计划帐户提交的应用程序共享类似的二进制文件、元数据和/或概念。

  • Submitting an app with the same source code or assets as other apps already submitted to the App Store

译:提交与其他已提交到app Store的应用程序具有相同源代码或资产的应用程序

  • Purchasing an app template with problematic code from a third party

译:从第三方购买有问题代码的应用模板

加强版

最近有很多同行在咨询3.2f,其中最值得关注的是

Evidence of Dishonest or Fraudulent Activity


Your account is associated with terminated developer accounts and/or accounts 
engaged in fraudulent activities. These associations may include, but are not limited to, shared account information, 
submissions with similar or identical App Store metadata, app binaries with s
hared code, resources, or assets, or the transfer of apps engaged in fraudulent activities between accounts.

包括但不限于共享账户信息、具有相似或相同App Store元数据的提交、具有共享代码、资源或资产的应用程序二进制文件

也就是说在今年之前,仅仅局限于上架被拒4.3a。苹果在最近开始了新一轮的二进制比对,在未进行迭代,上新的账户中,触发了代码关联性的查重。

所以都2025年了,你还敢买iOS的源码?我建议是算了吧,你把握不住的~

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

Flutter 性能优化:实战指南

2025年4月1日 09:46

主要的优化方面,如首屏、列表、渲染、内存、数据加载、工具等。

一、首屏渲染优化

1. 骨架屏与占位符

  • 问题:首屏加载时白屏或长时间等待数据导致用户体验差。

  • 解决方案

    • 骨架屏:使用 SkeletonLoader 等组件模拟页面结构,替代空白屏4。
    • 占位符:数据未加载时返回轻量组件如 SizedBox.shrink(),降低首帧渲染时间(如减少 200ms)。
  • 代码示例

    Widget build(BuildContext context) {
      if (_isLoading) return const SizedBox.shrink(); // 轻量占位
      // 真实内容
    }
    

2. 资源预加载

  • 问题:首屏图片加载延迟。

  • 解决方案:在页面初始化前预加载关键资源(如轮播图、Banner)。

  • 代码示例

    Future<void> _precacheImages() async {
      await Future.wait([
        precacheImage(AssetImage('assets/banner.png'), context),
      ]);
    }
    

3. 数据预取与FFI优化

  • 问题:Flutter 通过 Channel 请求数据时线程切换和编解码耗时。

  • 解决方案

    • Native侧发起请求:在页面路由阶段由 Native 提前请求数据,避免 Flutter 线程切换。
    • FFI(Foreign Function Interface) :通过 FFI 直接读取 Native 缓存数据,减少序列化开销。
  • 效果:详情页启动时间优化 100ms 以上9。


二、长列表与滚动性能优化

1. 懒加载与按需构建

  • 问题:一次性渲染大量列表项导致卡顿。

  • 解决方案

    • ListView.builder/GridView.builder:仅构建可见项,避免内存溢出。
    • 分帧渲染(分帧加载) :将复杂子组件分帧渲染,避免单帧耗时过长。
  • 代码示例

    ListView.builder(
      itemCount: 1000,
      itemBuilder: (context, index) => ListItem(data[index]),
    )
    

2. 复用与缓存

  • 问题:频繁切换 Tab 导致重复加载数据。

  • 解决方案

    • 状态缓存:使用 PageStorageKey 或 AutomaticKeepAliveClientMixin 缓存页面状态。
    • 数据分级管理:通过 RxMap 维护不同分类的数据状态,减少重复请求。

三、UI渲染优化

1. 减少Widget重建

  • 问题:频繁调用 setState 导致不必要的 Widget 重建。

  • 解决方案

    • const Widget:使用 const 构造函数创建静态组件,避免重复构建。
    • Provider 选择性刷新:通过 Selector 或 Consumer 局部刷新,而非全量更新。
  • 代码示例

    const MyWidget(); // 编译时确定,避免重建
    

2. 动画优化

  • 问题:复杂动画导致帧率下降。

  • 解决方案

    • AnimatedBuilder:分离动画逻辑与静态组件,仅重绘动画部分。
    • 复用Child:将不变部分作为 child 参数传入,避免重复构建。
  • 代码示例

    AnimatedBuilder(
      animation: _controller,
      child: const StaticWidget(),
      builder: (context, child) => Transform.rotate(
        angle: _controller.value,
        child: child,
      ),
    )
    

四、内存与GPU优化

1. 内存泄漏检测

  • 问题:未释放资源导致内存持续增长。

  • 解决方案

    • 及时销毁监听:在 dispose 方法中取消订阅和释放资源。
    • 工具检测:使用 DevTools 的 Memory 视图分析内存泄漏。

2. GPU渲染优化

  • 问题:多图层叠加(如半透明蒙层)导致 GPU 负载高。

  • 解决方案

    • 避免 saveLayer:使用 ClipRRect 替代复杂蒙层,减少离屏渲染。
    • 缓存静态图像:通过 checkerboardRasterCacheImages 检测未缓存的图像。

五、工具与工程化实践

1. 性能分析工具

  • DevTools:通过 Timeline 视图分析帧耗时,定位构建、布局、绘制阶段的瓶颈。
  • Profile模式:打包 Profile 版本,获取接近真实环境的性能数据。

2. 模块级混合开发

  • 问题:全 Flutter 页面启动性能差(如动态库加载耗时)。

  • 解决方案

    • Native与Flutter混合:核心模块用 Native 实现,非核心模块保留 Flutter,降低启动时间。
    • FlutterBoost 扩展:支持模块级混合容器,解决生命周期和布局问题。

六、其他高级优化

1. Isolate 并行计算

  • 问题:Dart 单线程阻塞 UI。

  • 解决方案:将耗时计算(如 JSON 解析)放入 Isolate 执行。

  • 代码示例

    final result = await compute(heavyTask, data);
    

2. 资源压缩与预置

  • 问题:资源加载慢(如图片、字体)。

  • 解决方案

    • 图片压缩:使用 flutter_image_compress 优化图片体积。
    • 本地预置:关键资源(如 JSON 模板)预置到本地,减少网络依赖。

总结

  • 核心原则:减少 Widget 重建、按需加载、复用资源、分离耗时操作。

  • 实战案例

    • 携程酒店通过分帧渲染优化;
    • 淘宝特价版使用 FFI 和 Native 数据预取提升首屏速度;
    • 长列表懒加载和状态缓存避免卡顿。

iOS性能优化:OC和Swift实战指南

2025年4月1日 09:29

常见的iOS性能优化点,比如内存管理、UI优化、多线程处理、网络请求优化、启动优化、I/O操作、图像处理、算法优化、工具使用等。

一、内存优化

1. 循环引用处理

  • 原理:对象之间的强引用导致无法释放,内存泄漏。

  • Objective-C

    • 使用 __weak 或 __unsafe_unretained(需谨慎)打破循环:

      __weak typeof(self) weakSelf = self;
      self.block = ^{
          // 弱引用避免循环
          [weakSelf doSomething];
      };
      
    • 对 NSTimer 使用 NSProxy 或 weak 委托模式。

  • Swift

    • 使用 [weak self] 或 [unowned self] 捕获列表:

      self.block = { [weak self] in
          guard let self = self else { return }
          self.doSomething()
      }
      
    • unowned 适用于生命周期相同或更短的场景(如父子关系)。


2. 自动释放池(Autorelease Pool)

  • 原理:批量创建临时对象时,及时释放内存。

  • Objective-C

    @autoreleasepool {
        for (int i = 0; i < 100000; i++) {
            NSString *temp = [NSString stringWithFormat:@"%d", i];
            // 临时对象会被及时释放
        }
    }
    
  • Swift

    autoreleasepool {
        for i in 0..<100000 {
            let temp = "(i)"
            // 临时对象在块结束时释放
        }
    }
    
  • 适用场景:大量临时对象生成(如解析 JSON 数组、图像处理)。


二、UI 性能优化

1. 避免离屏渲染(Offscreen Rendering)

  • 原理:离屏渲染(如圆角、阴影)触发 GPU 额外绘制,导致卡顿。

  • 优化方法

    • 预渲染圆角

      • Objective-C

        // 使用 Core Graphics 提前绘制圆角
        UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 0);
        [[UIBezierPath bezierPathWithRoundedRect:view.bounds cornerRadius:10] addClip];
        [image drawInRect:view.bounds];
        UIImage *roundedImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        view.layer.contents = (id)roundedImage.CGImage;
        
      • Swift

        let renderer = UIGraphicsImageRenderer(size: view.bounds.size)
        let roundedImage = renderer.image { context in
            UIBezierPath(roundedRect: view.bounds, cornerRadius: 10).addClip()
            image.draw(in: view.bounds)
        }
        view.layer.contents = roundedImage.cgImage
        
    • 避免 shouldRasterize:除非复用图层,否则会触发离屏渲染。


2. Cell 复用与轻量化

  • 原理:避免频繁创建/销毁 Cell,减少 CPU 和内存压力。

  • Objective-C

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        // 复用 Cell
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CellID"];
        if (!cell) {
            cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"CellID"];
        }
        // 轻量配置(避免复杂计算)
        cell.textLabel.text = [self.dataArray[indexPath.row] title];
        return cell;
    }
    
  • Swift

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CellID", for: indexPath)
        cell.textLabel?.text = dataArray[indexPath.row].title
        return cell
    }
    
  • 优化点

    • 避免在 cellForRow 中执行耗时操作(如网络请求)。
    • 使用 prepareForReuse 清理旧数据。

三、多线程优化

1. 主线程任务最小化

  • 原理:主线程阻塞导致 UI 卡顿(16ms 内未完成一帧绘制)。

  • Objective-C

    // 将耗时操作放到后台线程
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSData *data = [NSData dataWithContentsOfURL:url];
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = [UIImage imageWithData:data];
        });
    });
    
  • Swift

    DispatchQueue.global(qos: .userInitiated).async {
        let data = try? Data(contentsOf: url)
        DispatchQueue.main.async {
            self.imageView.image = UIImage(data: data!)
        }
    }
    

2. 线程安全与锁优化

  • Objective-C

    • @synchronized 实现简单但性能较低:

      objc

      @synchronized(self) {
          // 临界区
      }
      
    • 高性能场景使用 os_unfair_lock(替代已废弃的 OSSpinLock):

      #include <os/lock.h>
      os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
      os_unfair_lock_lock(&lock);
      // 临界区
      os_unfair_lock_unlock(&lock);
      
  • Swift

    • 使用 NSLock 或 DispatchQueue 屏障:

      let queue = DispatchQueue(label: "com.example.threadSafe", attributes: .concurrent)
      queue.async(flags: .barrier) {
          // 写操作(独占访问)
      }
      

四、网络优化

1. 请求合并与缓存

  • Objective-C

    // 使用 NSURLSession 的缓存策略
    NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:10];
    
  • Swift

    let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 10)
    
  • 优化点

    • 减少重复请求(如短时间内多次刷新)。
    • 使用 HTTP/2 多路复用降低连接开销。

五、启动时间优化

1. 冷启动阶段优化

  • 原理:减少 main() 函数之前的加载时间(T1)和首帧渲染时间(T2)。

  • Objective-C

    • 减少动态库数量,合并 Category。
    • 避免在 +load 方法中执行代码。
  • Swift

    • 使用 @UIApplicationMain 减少启动代码。

    • 延迟非必要初始化:

      DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
          // 延迟初始化第三方 SDK
      }
      

六、I/O 优化

1. 文件读写异步化

  • Objective-C

    dispatch_io_t channel = dispatch_io_create_with_path(DISPATCH_IO_STREAM, [path UTF8String], O_RDONLY, 0, queue, ^(int error) {});
    dispatch_io_read(channel, 0, SIZE_MAX, queue, ^(bool done, dispatch_data_t data, int error) {});
    
  • Swift

    let queue = DispatchQueue.global(qos: .background)
    DispatchIO.read(fromFileDescriptor: fd, queue: queue) { data, _ in
        // 处理数据
    }
    

七、图像处理优化

1. 异步解码与降采样

  • Objective-C

    // 使用 ImageIO 进行降采样
    CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)url, NULL);
    CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, 0, (__bridge CFDictionaryRef)@{(id)kCGImageSourceThumbnailMaxPixelSize: @(300)});
    UIImage *image = [UIImage imageWithCGImage:imageRef];
    
  • Swift

    let source = CGImageSourceCreateWithURL(url as CFURL, nil)!
    let options: [CFString: Any] = [kCGImageSourceThumbnailMaxPixelSize: 300]
    let imageRef = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary)!
    let image = UIImage(cgImage: imageRef)
    

八、算法与数据结构

1. 高效遍历与查询

  • Objective-C

    • 使用 NSDictionary 替代 NSArray 快速查找(O(1) vs O(n))。
  • Swift

    • 使用 lazy 延迟计算集合:

      let filteredData = data.lazy.filter { $0.isValid }.map { $0.value }
      
    • 使用 ContiguousArray 提升性能(连续内存布局)。


九、工具使用

1. Instruments 分析

  • Time Profiler:定位 CPU 热点函数。
  • Allocations:分析内存分配类型和泄漏。
  • Core Animation:检测离屏渲染和帧率。

十、Swift 特有优化

1. 减少动态派发

  • 使用 final 和 private 修饰类或方法:

    final class NetworkManager {
        private func fetchData() { ... }
    }
    

2. 值类型优先

  • 使用结构体(struct)替代类(class)减少引用计数开销:

    struct Point {
        var x: Double
        var y: Double
    }
    

总结

  • Objective-C 优化重点:手动内存管理、@autoreleasepool、避免 performSelector 潜在泄漏。
  • Swift 优化重点:值类型、协议扩展、DispatchQueue 和现代语法(如 Result 类型)。
  • 通用原则:减少主线程阻塞、复用资源、异步化耗时操作、利用工具分析瓶颈。

为什么 Swift 的反射像个“玩具”?聊聊 Mirror 背后的设计哲学

作者 JQShan
2025年3月31日 21:03

引子:当程序员想“窥探”一个对象

如果你用过 Java 或 C#,大概对「反射」这个词不陌生——它能让你在运行时像变魔术一样动态调用方法、修改属性,甚至篡改私有字段。但在 Swift 中,如果你想“窥探”一个对象的内部,苹果只给了你一个看似简陋的工具:Mirror。它不能修改属性,无法获取方法,连私有变量都藏着掖着。这不禁让人想问:反射这么重要的能力,苹果为何只做了个“玩具”出来?

今天,我们就来聊聊 Mirror 的设计逻辑,以及它背后隐藏的 Swift 语言哲学。

一、Mirror 的“克制”:安全与性能的双重防线

想象一下,你正在造一辆车。Java 的反射像是给你一套万能扳手,能拆发动机、改刹车片,甚至把车门卸下来。而 Swift 的 Mirror 更像是一把车钥匙——它能让你打开车门,看看内饰,但别想乱改零件。这种“克制”背后,是苹果对 Swift 的两大核心坚持:

  1. 安全第一 Swift 从诞生起就带着「杜绝未定义行为」的执念。反射能绕过编译检查,就像给代码开了个后门。比如,Java 中你可以用反射强行修改 final 字段,但这可能导致不可预知的崩溃。而 Mirror 的只读设计,本质上是在说: “你可以看看,但别乱动。”
  2. 速度不能妥协 Swift 被用于 iOS 系统内核、高性能游戏引擎等场景。反射的动态类型检查(as?Any)会带来运行时开销,而 Mirror 的轻量化设计,让它在需要时足够快,甚至能被编译器优化掉部分成本。

二、为什么不需要“万能扳手”?Swift 的编译时魔法

苹果似乎对动态反射兴趣缺缺,但其实他们找到了一种更“Swift 风格”的解决方案:把问题消灭在编译时。举个例子:

  • 场景:JSON 解析 在 Java 中,你可能用反射遍历字段,匹配 JSON 的 key。而在 Swift 中,Codable 协议通过编译器自动生成代码,直接映射属性——无需运行时反射,且类型安全零开销。
// 编译器自动生成编解码逻辑!
struct User: Codable {
    var name: String
    var age: Int
}
  • 场景:依赖注入 Java 的 Spring 框架依赖反射创建对象,而 Swift 可以通过泛型 + 协议,在编译时完成类型绑定:
// 编译时就知道 Container 里存了什么类型
container.register(UserService()) 
let service: UserService = container.resolve()

苹果的逻辑很明确:能通过类型系统解决的问题,绝不留到运行时。

三、Mirror 的生存空间:优雅的妥协

当然,总有些场景需要运行时信息。比如调试工具、动态生成日志,或是教学demo中展示对象结构。这时 Mirror 就派上用场了:

// 打印对象的所有属性
func debugPrint(_ value: Any) {
    let mirror = Mirror(reflecting: value)
    for child in mirror.children {
        print("(child.label ?? "?"): (child.value)")
    }
}

但你会发现,Mirror 的设计处处透着“小心翼翼”:

  • 不支持方法反射(避免动态派发)
  • 不暴露内存布局(防止不安全访问)
  • 对枚举和结构体的支持有限(鼓励模式匹配)

它更像是一个“安全气囊”,只在必要时弹出,而非让开发者随时飙车。

四、从 Mirror 看 Swift 的“价值观”

  1. 开发者不是敌人 Java 的反射默认允许访问私有字段,而 Swift 的 Mirror 对私有属性的可见性取决于模块边界——它假设开发者是理性的,但依然用访问控制保护代码的封装性。
  2. 工具链即力量 Swift 更倾向于通过编译器(如自动生成 Codable 代码)、Xcode 工具链(如 LLDB 调试器)来辅助开发,而非依赖运行时动态能力。
  3. 生态的统一性 在 SwiftUI 中,属性包装器(@State)、函数式编程等特性,让开发者无需反射也能实现动态 UI 和数据绑定。反射不再是必需品,而是备胎。

五、如果你真的需要“万能扳手”……

虽然苹果不鼓励,但总有极客想突破限制。比如:

  • 用 @dynamic 修饰符兼容 Objective-C 的运行时
  • 通过指针黑魔法直接操作内存布局(危险!但刺激)
  • 第三方库如 Runtime 提供元编程能力

但当你走这条路时,苹果的设计师可能会在背后叹气: “何必呢?明明有更安全的方式啊。”

结语:Mirror 是一面镜子,照出 Swift 的灵魂

Mirror 的简陋,恰恰反映了 Swift 的野心——它不想成为另一个“什么都能做,但处处是坑”的动态语言,而是试图用类型安全、编译时优化和清晰的API,重新定义现代编程的边界。就像 Swift 之父 Chris Lattner 所说: “我们希望开发者写出明显正确的代码,而非依赖运行时的小聪明。”

所以,下次当你嫌弃 Mirror 功能弱时,不妨换个角度想:或许不是苹果吝啬,而是他们相信,最好的魔法,应该发生在编译时 ✨。

Swift运行时以及与OC混编

作者 jz_study
2025年3月30日 20:44

Swift运行时

  • 在swift中如果我们要定义一个表示错误的类型非常简单,只要遵循Error协议就可以了,我们通常用枚举或结构体来表示错误类型,枚举可能用的多些,因为它能更直观的表达当前错误类型的每种错误细节。

image.png

如何抛出错误

  • 函数、方法和初始化器都可以抛出错误。需要在参数列表后面,返回值前面加throws关键字

image.png

image.png

image.png

使用Do-Catch做错误处理

  • 在Swift中我们使用do-catch块对错误进行捕获,当我们调用一个throws声明的函数或方法时,我们必须把调用语句放在do语句块中,同时do语句块后面紧接着使用catch语句块。

image.png

try?

  • try?会将错误转换为可选值,当调用try?+函数或方法语句时候,如果函数或方法抛出错误,程序不会发生崩溃,而返回一个nil,如果没有抛出错误则返回可选值

image.png

try!

  • 如果你确信一个函数或者方法不会抛出错误,可以使用try!来中断错误的传播。但是如果错误真的发生了,你会得到一个运行时错误。
let photo = try! loadImage(atPath:"./Resources/John Appleseed.jpg")

指定退出的清理动作

  • defer关键字:defer block里的代码会在函数return之前执行,无论函数是从哪个分支return的,还是有throw,还是自动而然走到最后一行(类似于ts的finally)

image.png

权限控制(模块和源文件)

  • 模块指的是独立的代码分发单元,框架或应用程序会作为一个独立的模块来构建和发布。在Swift中,一个模块可以使用import关键字导入另外一个模块
  • 源文件就是Swift中的源代码文件,它通常属于一个模块,即一个应用程序或者框架。尽管我们一般会将不同的类型分别定义在不同的源文件中,但是同一个源文件也可以包含多个类型、函数之类的定义。

访问级别

image.png

潜规则1
  • 如果一个类的访问级别是fileprivate或private那么该类的所有成员都是fileprivate或private(此时成员无法修改访问级别),如果一个类的访问级别是open、internal或者public那么它的所有成员都是internal,类成员的访问级别不能高于类的访问级别(注意:嵌套类型的访问级别也符合此条规则)
潜规则2
  • 常量、变量、属性、下标脚本访问级别低于其所声明的类型级别,并且如果不是默认访问级别(internal)要明确声明访问级别(例如一个常量是一个private类型的类类型,那么此常量必须声明为private或fileprivate)
潜规则3
  • 在不违反1、2两条潜规则的情况下,setter的访问级别可以低于getter的访问级别(例如一个属性访问级别是internal),那么可以添加private(set)修饰符将setter权限设置为private,在当前模块中只有此源文件可以访问,对外部是只读的)
潜规则4
  • 必要构造方法(required修改)的访问级别必须和类访问级别相同,结构体的默认逐一构造函数的访问级别不高于其成员的访问级别(例如一个成员时private那么这个构造函数就是private,但是可以通过自定义来声明一个public的构造函数),其他方法(包括其他构造方法和普通方法)的访问级别遵循潜规则1
不透明类型(why)

image.png

  • 代码是可以编译通过的,但是makeTrapezoid的返回类型有凑有偿,被暴露了出去

image.png

  • 不能将其Container用作函数的返回类型,因为该协议具有关联类型。也不能将它用作返回类型的泛型约束,因为函数体外没有足够的信息来推断泛型类型需要什么

image.png

解决问题

image.png

返回不透明类型vs返回协议类型
  • 返回opaque类型看起来非常类似于使用协议类型作为函数的返回类型,但这两种返回类型的不同之处在于它们是否保留了类型标识。opaque类型是指一种特定类型,尽管函数的调用者不能看到是哪种类型;协议类型可以指代符合协议的任何类型。一般来说,协议类型为存储值的基础类型提供了更大的灵活性,而不透明类型可以对这些基础类型做出更强有力的保证。

image.png

ARC

  • Swift使用自动引用计数(ARC)来跟踪并管理应用使用的内存。大部分情况下,这意味着在Swift语言中,内存管理"仍然工作",不需要自己去考虑内存管理的事情。当实例不再被使用时,ARC会自动释放这些类的实例所占用的内存。
  • 引用计数只应用在类的实例。结构体(Structure)和枚举类型是值类型,并非引用类型,不是以引用的方式来存储和传递的

ARC如何工作

image.png

循环引用

  • 在两个类实例彼此保持对方的强引用,使得每个实例都使对方保持有效时会发生这种情况。我们称之为强引用环。
  • 通过用弱引用或者无主引用来取代强引用,我们可以解决强引用环问题

image.png

image.png

解决循环引用
  • 弱引用和无主引用允许引用环中的一个实例引用另外一个实例,但不是强引用。因此实例可以互相引用但是不会产生强引用环。
  • 对于生命周期中引用会变为nil的实例,使用弱引用;对于初始化时赋值之后引用再也不会赋值为nil的实例,使用无主引用

弱引用

  • 弱引用不会增加实例的引用计数,因此不会阻止ARC销毁引用的实例。这种特性使得引用不会变成强引用环。声明属性或者变量的时候,关键字weak表明引用弱引用。
  • 弱引用只能声明为变量类型,因为运行时它的值可能改变。弱引用绝对不能声明为常量。
  • 因为弱引用可以没有值,所以声明弱引用的时候必须是可选类型的。在Swift语言中,推荐用可选类型来作为可能没有值的引用的类型。

image.png

image.png

无主引用

  • 和弱引用相似,无主引用也不强持有实例。但是和弱引用不同的是,无主引用默认始终有值。因此,无主引用定义为非可选类型(non-optional-type).在属性、变量前添加unowned关键字,可以声明一个无主引用。
  • 因为是非可选类型,因此当使用无助引用的时候,不需要展开,可以直接访问。不过非可选类型变量不能赋值为nil,因此当实例被销毁的时候,ARC无法将引用赋值为nil。
  • 当实例被销毁后,试图访问该实例的无主引用会触发运行时错误。使用无主引用时请确保引用始终指向一个未销毁的实例。

image.png

闭包引用循环

  • 将一个闭包赋值给类实例的某个属性,并且这个闭包使用了实例,这样也会产生强引用环。这个闭包可能访问了实例的某个属性,例如self.someProperty,或者调用了实例的某个方法,例如self.someMethod。这两种情况导致了闭包使用self,从而产生了循环引用。
闭包引用循环解决
  • 定义占有列表-占有列表中的每个元素都是由weak或者unowned关键字和实例的引用(如self或someInstance)组成。每一对都在中括号中,通过逗号分开。
  • 当闭包和占有的实例总是互相引用时并且总是同时销毁时,将闭包内的占有定义为无主引用。
  • 相反的,当占有引用有时可能会是nil时,将闭包内的占有定义为弱引用。

image.png

内存安全

  • 默认情况下,Swift会克服代码层面上的一些不安全的行为,如:确保一个变量被初始化完后才能被访问、确保变量在销毁后不会被访问等等安全操作。
  • Swift也会确保在多路访问内存中同一区域时不会冲突(独占访问该区域)。通常情况下,我们完全无需考虑内存访问冲突的问题,因为Swift是自动管理内存的。然而,在码代码的时候,了解那些地方可能发生内存访问冲突是非常重要的。通常情况喜爱,如果你的代码有内存访问冲突,那么Xcode会提示编译错误或者运行时错误。
  • 内存访问分为两种
  1. 即时访问:即在访问开始至结束前都不可能有其他代码来访问同一区域
  2. 长期访问:即在访问开始至结束前可能有其他代码来访问同一区域。长期访问可能和其他即时访问或者长期访问重叠
inout参数访问冲突

image.png

image.png

image.png

解决访问冲突问题:

image.png

self访问冲突

image.png

image.png

有问题

image.png

三方库

简介

使用cocoapods

image.png

  • 网络请求:Alamofire
  • JSON解析:SwiftJSON
  • 资源管理:R.swift
  • 社交分享:MonkeyKing
  • 图片缓存与加载:Kingfisher
  • 自动布局:SnapKit
  • 标准款扩展:Dollar
网络请求:Alamofire
  • Alamofire是在平果URL Loading System基础上封装的网络库,简单易用并且可扩展
  • Alamofire
基本用法
  • AF命名空间,链式调用
AF.request("https://time.geekbang.org").response { response in
 debugPrint(response)
}
request方法详解

image.png

SwiftJSON

image.png

Dollar(是对标准库的扩展,类似于js的loadash)

image.png

image.png

image.png

Snapkit

image.png

  • make初始化约束还调用
  • update更新约束的使用
  • remark有冲突约束的时候使用
图片加载和缓存-Kingfisher(类似SDWebImage)

image.png

R.Swift

image.png

image.png

image.png

R.Swift之前

image.png

R.Swift的使用

image.png

MonkeyKing

image.png

OC和Swift运行时简介

Objective-C运行时

  • 动态类型(dynamic typing)
  • 动态绑定(dynamic binding)
  • 动态加载(dynamic loading)

image.png

image.png

派发方式

  • 直接派发(Direct Dispatch)
  • 函数表派发(Table Dispatch)
  • 消息机制派发(Message Dispatch)
直接派发
  • 直接派发是最快的,不止是因为需要调用的指令集会更少,并且编译器还能够有很大的优化空间,例如函数内联等,直接派发也有人称为静态调用
  • 然而,对于编程来说直接调用也是最大的局限,而且因为缺乏动态性所以没办法支持继承和多继承
函数表派发

image.png

image.png

  • 查表是一种简单,易实现,而且性能可预知的方式.然而,这种派发方式比起直接派发还是慢一点,从字节码角度来看,多了两次读和一次跳转,由此带来了性能的损耗,另一个慢的原因在于编译器可能会由于函数内执行的任务导致无法优化(如果函数带有副作用的话)
  • 这种基于数组的实现,缺陷在于函数表无法拓展.子类会在虚数函数表的最后插入新的函数,没有位置可以让extesion安全地插入函数
消息机制派发
  • 消息机制是调用函数最动态的方式.也是Cocoa的基石,这样的机制催生了KVO,UIAppearence和CoreData等功能.这种运作方式的关键在于开发者可以在运行时改变函数的行为.不止可以通过swizzling来改变,甚至可以用isa-swizzling修改对象的继承关系,可以在面向对象的基础上实现自定义派发

Swift运行时

  • 纯Swift类的函数调用已经不再是Objective-c的运行时发消息,而是类似C++的vtable,在编译时就确定了调用哪个函数,所以没法通过runtime获取方法、属性。
  • 而Swift为了兼容Objective-C,凡是继承自NSObject的类都会保留其动态性,所以我们能通过runtime拿到他的方法。这里有一点说明:老版本的Swift(如2.2)是编译期隐式的自动帮我加上了@objc,而4.0以后版本的Swift去掉了隐式特性,必须使用显式添加
  • 不管是纯Swift类还是继承自NSObject的类只要在属性和方法前面添加@objc关键字就可以使用runtime

image.png

  • 值类型总是会使用直接派发,简单易懂
  • 而协议和类的extension都会使用直接派发
  • NSObject的extension会使用消息机制进行派发
  • NSObject声明作用域里的函数都会使用函数表进行派发
  • 协议里声明的,并且带有默认实现的函数会使用函数表进行派发

image.png

Swift运行时-final @objc

  • 可以在标记为final的同时,也是用@objc来让函数可以使用消息机制派发。这么做的结果就是,调用函数的时候会使用直接派发,但也会在Objective-C的运行时里注册响应的selector.函数可以响应perfrom(selector:)以及别的Objective-C特性,但在直接调用时又可以有直接派发的性能
Swift运行时

image.png

image.png

image.png

Swift与OC的桥接

Swift调用OC

image.png

OC调用Swift

image.png

image.png

NS_SWIFT_NAME
  • 在Objective-C中,重新命名在swift中的名称
NS_SWIFT_UNAVAILABLE
  • 在Swift中不可见,不能使用
  • OC中可以调用Swift方法,要保证swift的类是继承自NSObject的

Subclass

  • 对于自定义的类而言,Objective-C的类,不能继承自Swift的类,即要混编的OC类不能是Swift类的子类。反过来,需要混编的Swift类可以继承自OC的类。

  • 定义一个常量值,后面可以方便使用;如#define TOOLBAR_HEIGHT 44;
  • 定义一个不变化的常用值,或者一个较长的对象属性;如#define SCREEN_WIDTH ([[UIScreen mainScreen] bounds].size.width)
  • 定义一个会变化的常用变量值,或者一个较长的对象属性;如: #define STATUS_BAR_HEIGHT ([UIApplication sharedApplication].statusBarFrame.size.height)
  • 定义一个带参数的宏,类似于一个函数;如#define RGB_COLOR(r,g,b) [UIColor colorWithRed:r/255.f gree:g/255.f blue:b/255.f alpha:1.0]

Swift独有特性

  • Swift中有许多OC没有的特性,比如,Swift元组、为一等公民的函数、还有特有的枚举类型。所以,要使用的混编文件要注意Swift独有属性问题。

NS_REFINED_FOR_SWIFT

  • Objective-C的API和Swift的风格相差比较大,Swift调用Objective-C的API时可能由于数据类型等不一致导致无法达到预期(比如,Objective-C里的方法采用了C语言风格的多参数类型;或者Objective-C方法返回NSNotFound,在Swift中期望返回nil)。这时候就要NS_REFINED_FOR_SWIFT

image.png

SwiftUI-国际化

作者 YungFan
2025年3月30日 20:13

介绍

  • 如果 App 需要提供给不同国家的用户使用,则需要进行国际化处理。
  • SwiftUI 项目的国际化主要包括:Info.plist 文件国际化、文本国际化等。

配置国际化语言

在进行国际化之前,必须要添加需要的国际化语言,选中国际化的项目 —> PROJECT —> Info —> Localizations,点击+添加需要的国际化语言(默认已经存在英文)。

Info.plist文件国际化

  1. 新建一个Strings File,必须命名为InfoPlist.strings
  2. 选中InfoPlist.strings,在 Xcode 的右侧文件检查器中找到Localization,点击Localize...,然后勾选配置的国际化语言。
  3. InfoPlist.strings左侧多了一个箭头,点击箭头展开后可以看见不同语言的Strings File,里面存放的是形如Key = Value的键值对。
  4. 在不同语言的Strings File中设置需要国际化的内容,如 App 名称等。
// 英文App名
"CFBundleName" = "I18N";
// 中文App名
"CFBundleName" = "国际化";

文本国际化

  1. 新建一个Strings File,必须命名为Localizable.strings
  2. 选中InfoPlist.strings,在 Xcode 的右侧文件检查器中找到Localization,点击Localize...,然后勾选配置的国际化语言。
  3. Localizable.strings左侧多了一个箭头,点击箭头展开后可以看见不同语言的Strings File
  4. 在不同语言的Strings File中设置需要国际化的文本键值对。
// 英文
"title" = "Reminder";
"message" = "Weather Information";
// 插值
"Weather is %@" = "Today is %@";
"Temperature is %lld" = "The temperature is %lld";
// 中文
"title" = "提示";
"message" = "今日天气";
// 插值
"Weather is %@" = "今天 %@";
"Temperature is %lld" = "气温 %lld 度";
  1. SwiftUI 文本国际化非常简单,开箱即用,因为大多数 View 与 Modifier 的构造方法中都将LocalizedStringKey作为参数类型,该参数的值为文本键值对中的键。
import SwiftUI

struct ContentView: View {
    let weather = "Sunny"
    let temperature = 10

    var body: some View {
        VStack {
            // 纯文本,有3种方式
            Text(title)
            
            Text(LocalizedStringKey("title"))

            Text("title", comment: "The title of the dialog.")
            
            // 自定义View
            MessageView("message")
            
            // 插值
            Text("Weather is \(weather)")
            
            Text("Temperature is \(temperature)")   
        }
    }
}

struct MessageView: View {
    var messaege: LocalizedStringKey

    init(_ messaege: LocalizedStringKey) {
        self.messaege = messaege
    }

    var body: some View {
        Text(messaege)
    }
}

注意:插值格式参考 String Format Specifiers

测试

默认情况下,App 的语言随着系统语言的变化而变化。但在开发阶段,如果才能快速测试 App 的国际化效果?主要有以下几种方式。

  1. 运行 App 之后在设备/模拟器通过设置(Settings)—> 通用(General)—> 语言与地区(Languages & Region) 切换系统语言以查看 App 的国际化效果。
  2. 通过 Xcode 菜单 —> Product —> Scheme —> Edit Scheme... —> Run —> Options —> App Language,选择需要测试的国际化语言之后再运行 App。
  3. 通过 Xcode 菜单 —> Product —> Scheme —> Manage Scheme... —> 选择需要复制的 Scheme —> 点击下方的圆形...图标 —> Duplicate —> 重命名 Scheme,然后将复制的 Scheme 按照方式 2 将 App Language 设置为需要测试国际化语言,最后运行时选择对应国际化语言的 Scheme。

效果

  • 英文。

英文.png

  • 中文。

中文.png

iOS 中的 RunLoop 详解

作者 90后晨仔
2025年3月30日 20:03

1. 什么是 RunLoop?

RunLoop(运行循环) 是 iOS/macOS 开发中用于管理线程事件和消息的核心机制。它通过一个循环不断监听和处理输入事件(如触摸、定时器、网络数据),并在没有任务时让线程休眠以节省资源。

  • 本质:基于 mach port 的事件循环机制。
  • 核心作用:让线程在有任务时执行任务,无任务时休眠,避免资源浪费。

2. RunLoop 与线程的关系

  • 主线程:默认自动创建并启动 RunLoop(UIApplicationMain 函数中启动)。
  • 子线程:默认不开启 RunLoop,需手动创建和启动。
  • 生命周期:线程与 RunLoop 一一对应,存储在全局字典中(线程销毁时 RunLoop 也释放)。

3. RunLoop 的核心组成

组件 作用
Modes(模式) 定义 RunLoop 在不同场景下监听的事件源(Source)和观察者(Observer)。
Sources(事件源) - Source0:处理 App 内部事件(如触摸、PerformSelector)。
- Source1:基于 mach port 的系统事件(如硬件事件)。
Timers(定时器) 基于 RunLoop 的定时器(如 NSTimer),受 RunLoop Mode 影响。
Observers(观察者) 监听 RunLoop 的状态变化(如即将处理事件、即将休眠等),用于性能监控。

4. RunLoop 的 Mode(模式)

模式 描述
Default(NSDefaultRunLoopMode 默认模式,处理大多数事件(如普通触摸、网络请求)。
Tracking(UITrackingRunLoopMode 界面滚动时的模式(如 UIScrollView 滑动),优先保证滑动流畅。
Common(NSRunLoopCommonModes 一个虚拟模式,包含多个模式的集合(Default + Tracking),用于通用事件监听。

5. RunLoop 的典型应用场景

(1) 保持线程存活

通过 RunLoop 让子线程持续处理任务而不退出:

class BackgroundThread {
    private var thread: Thread?
    private var shouldKeepRunning = true
    
    func start() {
        thread = Thread { [weak self] in
            // 创建并启动 RunLoop
            let runLoop = RunLoop.current
            runLoop.add(NSMachPort(), forMode: .default)
            while self?.shouldKeepRunning == true {
                runLoop.run(mode: .default, before: .distantFuture)
            }
        }
        thread?.start()
    }
    
    func stop() {
        shouldKeepRunning = false
        thread?.cancel()
    }
}

(2) 优化滚动性能

将耗时任务放在 Default 模式,避免滚动时执行(如 UIScrollView 滑动):

DispatchQueue.main.async {
    // 在 Default 模式下执行任务
    RunLoop.current.perform(inMode: .default) {
        // 处理耗时任务(如数据解析)
    }
}

(3) 监听 RunLoop 状态

通过 CFRunLoopObserver 监控主线程卡顿:

let observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true, 0) { observer, activity in
    switch activity {
    case .beforeWaiting:
        print("RunLoop 即将休眠")
    case .afterWaiting:
        print("RunLoop 被唤醒")
    default: break
    }
}
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .commonModes)

(4) 解决定时器失效问题

在滚动模式下(如 UIScrollView 滑动时),将 NSTimer 添加到 CommonModes

let timer = Timer(timeInterval: 1.0, repeats: true) { _ in
    print("Timer fired")
}
RunLoop.current.add(timer, forMode: .common)

6. RunLoop 的工作流程

  1. 接收事件:通过 mach port 接收系统事件(Source1)或 App 内部事件(Source0)。
  2. 通知 Observers:即将处理事件(kCFRunLoopBeforeTimerskCFRunLoopBeforeSources)。
  3. 处理事件:执行触发的 Source0、Source1 和 Timer 事件。
  4. 进入休眠:若没有事件,调用 mach_msg 让线程休眠,等待新事件唤醒。
  5. 唤醒处理:被唤醒后处理新到达的事件,循环往复。

7. 注意事项

  • 避免阻塞主线程:主线程 RunLoop 负责 UI 更新,长时间阻塞会导致卡顿。
  • 子线程 RunLoop 管理:手动创建的 RunLoop 需自行控制生命周期(如添加保活 Source)。
  • 性能优化:合理使用 Mode 隔离不同任务(如滑动时暂停后台任务)。

总结

场景 RunLoop 的作用 关键技术点
线程保活 让子线程持续处理任务 添加保活 Source(如 NSMachPort
滑动性能优化 隔离耗时任务与滚动事件 使用 CommonModes 或模式切换
定时器管理 确保定时器在滚动时仍有效 注册到 CommonModes
卡顿监控 监听主线程 RunLoop 状态变化 CFRunLoopObserver

RunLoop 是 iOS 系统高效管理线程和事件的核心机制,合理利用可优化性能、实现复杂任务调度。在大多数情况下,开发者无需直接操作 RunLoop,但理解其原理对解决卡顿、线程管理等问题至关重要。

Swift基础知识(三)

作者 90后晨仔
2025年3月29日 19:21

Swift 与 Objective-C 中的自省(Introspection)对比


1. 核心概念

  • 自省(Introspection):在运行时检查对象的类型或是否符合特定协议的能力。
  • Objective-C:基于 NSObject 的类体系,主要用于检查类继承关系。
  • Swift:支持所有类型(类、结构体、枚举),不依赖 NSObject,且涵盖协议检查。

2. 类型检查语法与行为

特性 Objective-C Swift
检查类继承关系 isKindOfClass:(判断类或子类)
isMemberOfClass:(仅判断自身类)
is 操作符(判断类型或子类型)
支持类型 NSObject 子类 所有类型(类、结构体、枚举)
协议一致性检查 conformsToProtocol: is(检查协议类型)
as?(转换并检查协议)
值类型支持 不支持(仅类) 支持(结构体、枚举)
泛型支持 支持(结合泛型类型检查)

3. 具体用法与示例

Objective-C
// 类继承检查
BOOL isView = [obj isKindOfClass:[UIView class]];       // YES(obj 是 UIView 或其子类实例)
BOOL isExactView = [obj isMemberOfClass:[UIView class]]; // NO(obj 必须是 UIView 实例)

// 协议检查
BOOL conforms = [obj conformsToProtocol:@protocol(NSCopying)];
Swift
// 类型检查
let obj: Any = "Hello"
if obj is String { // true
    print("是 String 类型")
}

// 协议检查
protocol Drawable { func draw() }
struct Circle: Drawable { func draw() { } }

let shape: Any = Circle()
if shape is Drawable { // true
    print("符合 Drawable 协议")
}

// 值类型检查
let value: Any = 100
if value is Int { // true
    print("是 Int 类型")
}

// 类型转换 + 检查
if let drawableShape = shape as? Drawable {
    drawableShape.draw()
}

4. 核心区别总结

维度 Objective-C Swift
类型体系依赖 必须继承自 NSObject 不依赖任何基类,支持所有类型
检查范围 仅类(包括子类) 类、结构体、枚举、协议
协议检查 需显式调用 conformsToProtocol: 直接通过 isas? 检查
语法简洁性 方法调用(冗长) 操作符(简洁直观)
泛型支持 支持泛型类型检查

5. 高级特性(Swift 独有)

  • 模式匹配:结合 switch 语句进行类型和协议匹配。
    func checkType(_ value: Any) {
        switch value {
        case is String: print("字符串类型")
        case let num as Int where num > 0: print("正整数")
        case is Drawable: print("可绘制对象")
        default: break
        }
    }
    
  • 元类型检查:通过 .Type 获取类型元信息。
    let type: Int.Type = Int.self
    let instance = type.init(10) // 创建 Int 实例
    

6. 使用建议

  • Objective-C:在维护旧项目或与 Cocoa 框架交互时使用,注意仅适用于 NSObject 子类。
  • Swift:在新项目中优先使用 isas?,充分利用其对值类型和协议的支持,提升代码灵活性和安全性。

三、Swift 闭包(Closures)与 Objective-C Block 的对比

1. 内存分配与结构

特性 Swift 闭包 Objective-C Block
底层数据结构 闭包是 捕获上下文的函数,本质是结构体 Block 是 封装函数指针的结构体对象
内存位置 默认栈分配,逃逸闭包自动提升到堆 默认栈分配,需显式 copy 到堆
结构体布局 闭包结构体包含函数指针和捕获的上下文数据 Block 结构体包含 isa 指针、函数指针、捕获变量等
生命周期管理 通过 引用计数(ARC) 管理堆内存 手动 copy/release 或 ARC 管理堆内存

2. 变量捕获机制

特性 Swift 闭包 Objective-C Block
值类型捕获 捕获值类型的副本(深拷贝) 默认捕获值类型变量的 原始值(需 __block 修饰允许修改)
引用类型捕获 捕获引用类型的强引用(需通过捕获列表弱化) 捕获对象的强引用(需 __weak 弱化)
可变性支持 通过 varinout 参数捕获可变变量 __block 修饰符实现变量可变性

3. 函数执行与上下文

特性 Swift 闭包 Objective-C Block
函数指针 闭包的函数指针直接指向编译生成的函数代码 Block 的函数指针通过 invoke 成员指向代码
上下文管理 闭包通过结构体存储捕获的变量(值或引用) Block 通过结构体的 descriptor 管理捕获变量引用计数
逃逸性处理 编译器自动检测逃逸闭包,并生成堆分配逻辑 需手动调用 copy 将 Block 从栈复制到堆

4. 底层实现细节

Swift 闭包
  1. 结构体表示

    // 伪代码表示闭包结构体
    struct Closure<T> {
        var function: (T) -> Void  // 函数指针
        var capturedValues: [Any]  // 捕获的上下文数据
    }
    
  2. 内存管理

    • 非逃逸闭包:栈分配,函数返回后销毁。
    • 逃逸闭包:自动复制到堆,由 ARC 管理生命周期。
  3. 捕获列表优化

    • 显式声明 [weak self][unowned self],避免隐式强引用。
    • 编译器生成代码时,捕获列表直接修改闭包结构体的成员引用类型。
Objective-C Block
  1. 结构体表示

    // 伪代码表示 Block 结构体
    struct Block_layout {
        void *isa;                  // 指向 Block 类型(栈/堆/全局)
        int flags;                  // 状态标记(是否被拷贝等)
        int reserved;               // 保留字段
        void (*invoke)(void *, ...); // 函数指针
        struct Block_descriptor *descriptor; // 描述符(引用计数、捕获变量等)
        // 捕获的变量数据...
    };
    
  2. 内存管理

    • 栈 Block:默认创建在栈上,函数返回后失效。
    • 堆 Block:通过 copy 操作复制到堆,由引用计数管理。
  3. 变量捕获

    • __block 修饰的变量会被包装为 Block_byref 结构体,允许跨 Block 修改。
    • 对象类型变量通过 retain/release 管理引用计数(ARC 下自动处理)。

5. 性能对比

维度 Swift 闭包 Objective-C Block
内存开销 更小(结构体直接存储捕获数据) 较大(包含 isaflags 等元数据)
执行速度 更快(直接函数指针调用) 稍慢(需通过 invoke 间接调用)
捕获变量修改 灵活(值类型副本独立修改) __block 包装,性能开销较大

6. 总结

  • Swift 闭包

    • 更轻量:基于结构体和值语义,减少内存开销。
    • 更安全:编译时检查捕获列表,避免循环引用。
    • 更高效:栈分配优化 + 直接函数调用。
  • Objective-C Block

    • 更底层:直接操作结构体和引用计数,灵活性高。
    • 兼容性:与 Cocoa 框架深度集成,适合传统代码维护。
    • 显式控制:需手动管理 copy__block 修饰符。

底层核心差异

  • Swift 闭包是 值类型结构体,通过编译优化实现高效捕获和内存管理。
  • Objective-C Block 是 对象,依赖运行时元数据(isadescriptor)管理生命周期和捕获变量。

Swift基础知识(二)

作者 90后晨仔
2025年3月29日 18:53

一、Swift 属性观察器(Property Observers)

1. 核心概念

属性观察器用于监听存储属性值的变化,在值被修改前后执行自定义逻辑。包含两种观察器:

  • willSet:在值即将被设置前调用,默认提供 newValue 参数(新值)。
  • didSet:在值已被设置后调用,默认提供 oldValue 参数(旧值)。

2. 使用条件

  • 适用对象:仅用于 lazyvar 存储属性
    var score: Int = 0 {  // 正确:非 lazy 的存储属性
        willSet { print("新值:\(newValue)") }
        didSet { print("旧值:\(oldValue)") }
    }
    
    // lazy var data: [Int] = loadData() { willSet { } } // 错误:lazy 属性不支持
    
  • 不适用场景
    • 常量(let 属性)。
    • 计算属性(需通过 set 方法监听)。
    • 延迟存储属性(lazy var)。

3. 注意事项

  • 初始化不触发:属性在初始化赋值时不会调用观察器。
    struct User {
        var name: String = "Guest" { // 初始化赋值不会触发观察器
            didSet { print("名字被修改") }
        }
    }
    
  • 自定义参数名:可显式命名参数(如 willSet(newScore))。
  • 避免循环修改:在 didSet 中修改属性本身会再次触发观察器。

4. 使用场景

  • 数据验证:确保值在合法范围内。
    var age: Int = 0 {
        didSet {
            age = min(max(age, 0), 120)  // 限制年龄在 0~120
        }
    }
    
  • UI 同步更新:值变化时刷新界面。
    var isLoggedIn: Bool = false {
        didSet {
            updateLoginUI()
        }
    }
    
  • 日志记录:跟踪属性变化历史。
    var temperature: Double = 25.0 {
        willSet { print("温度将从 \(temperature) 变为 \(newValue)") }
        didSet { print("温度已从 \(oldValue) 更新为 \(temperature)") }
    }
    

5. 与计算属性的区别

特性 属性观察器 计算属性
用途 监听存储属性变化 通过计算动态获取值
触发时机 属性值被修改前后 每次访问时计算
存储能力 需有存储空间(必须初始化) 无存储空间(依赖其他属性)
语法 willSet/didSet get + set(或只读 get

6. 总结

  • 核心作用:在属性值变化前后注入逻辑,提升代码灵活性和可维护性。
  • 适用场景:数据验证、日志记录、UI 同步等。
  • 限制:仅适用于非 lazyvar 存储属性,初始化不触发。

二、Swift 中的异常捕获

Swift 通过 Error 协议和 throws 关键字实现异常处理,提供多种方式捕获和处理错误。以下是主要的异常捕获方法:

1. do-catch 语句(详细错误处理)

  • 用法:捕获特定错误类型并处理。
  • 适用场景:需要根据不同错误类型执行不同逻辑。
  • 示例
    enum NetworkError: Error {
        case invalidURL
        case timeout(seconds: Int)
    }
    
    func fetchData() throws {
        throw NetworkError.timeout(seconds: 30)
    }
    
    do {
        try fetchData()
    } catch NetworkError.invalidURL {
        print("URL 无效")
    } catch NetworkError.timeout(let seconds) {
        print("请求超时:\(seconds) 秒")
    } catch {
        print("未知错误:\(error)")
    }
    

2. try?(转换为可选类型)

  • 用法:忽略具体错误,返回 nil
  • 适用场景:不关心错误细节,只需判断是否成功。
  • 示例
    let result = try? someThrowingFunction()
    if let data = result {
        // 成功
    } else {
        // 失败
    }
    

3. try!(强制解包,慎用)

  • 用法:假设函数不会抛出错误,若出错则触发运行时崩溃。
  • 适用场景:确信代码不会失败(如本地静态数据加载)。
  • 示例
    let data = try! loadLocalConfig() // 确定本地文件存在时使用
    

4. 向上传递错误(throws 关键字)

  • 用法:函数声明 throws,错误由调用者处理。
  • 适用场景:将错误传递给上层调用链。
  • 示例
    func processFile() throws {
        let content = try String(contentsOfFile: "path/to/file")
        // 处理内容
    }
    
    // 调用处需处理或继续传递
    do {
        try processFile()
    } catch {
        print("文件处理失败:\(error)")
    }
    

5. rethrows(传递闭包中的错误)

  • 用法:函数本身不产生错误,但可能传递闭包的异常。
  • 适用场景:高阶函数(如 mapfilter)中处理闭包可能抛出的错误。
  • 示例
    func customMap<T>(_ array: [T], _ transform: (T) throws -> T) rethrows -> [T] {
        var result = [T]()
        for item in array {
            try result.append(transform(item))
        }
        return result
    }
    
    let numbers = [1, 2, 3]
    let doubled = try? customMap(numbers) { num in
        if num == 2 { throw NetworkError.invalidURL }
        return num * 2
    }
    

6. 异步错误处理(async/await

  • 用法:在异步函数中使用 throwstry
  • 适用场景:异步操作中的错误处理。
  • 示例
    func downloadData() async throws -> Data {
        let url = URL(string: "https://example.com")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return data
    }
    
    Task {
        do {
            let data = try await downloadData()
        } catch {
            print("下载失败:\(error)")
        }
    }
    

总结

方法 关键字 特点 适用场景
详细错误处理 do-catch 精准捕获错误类型 需要区分不同错误逻辑
可选值简化 try? 忽略错误,返回 nil 不关心错误细节
强制解包(高危) try! 假设成功,崩溃风险高 确定不会失败的场景
向上传递错误 throws 将错误传递给调用者 多层调用链中的错误处理
闭包错误传递 rethrows 传递闭包中的错误 高阶函数中的闭包操作
异步错误处理 async throws 结合 async/await 处理异步错误 网络请求、文件读写等异步操作

最佳实践

  • 优先使用 do-catch 处理可预见的错误。
  • 谨慎使用 try!,确保代码绝对安全。
  • 异步操作中结合 async/await 提升可读性。

三、Swift 中 defer 关键字的详解

1. 基本概念

defer 用于定义一个代码块(延迟执行块),该代码块会在 当前作用域结束前 执行,无论作用域是通过正常返回、抛出错误还是其他控制流(如 returnbreak)结束的。
核心作用:确保清理或收尾逻辑(如资源释放)一定会执行,避免资源泄漏。


2. 使用场景

  • 资源管理:文件操作、网络请求、锁的获取与释放等。
  • 错误处理:在抛出错误前确保清理逻辑执行。
  • 代码可读性:将清理代码紧跟在初始化代码后,提高可维护性。

3. 基本用法

func readFile() {
    let file = openFile()
    defer {
        closeFile(file) // 作用域结束前执行
    }
    // 处理文件...
    if someCondition {
        return // 触发 defer
    }
    // 更多操作...
} // 函数结束,触发 defer

4. 执行规则

  • 逆序执行:同一作用域内的多个 defer定义顺序的逆序 执行(类似栈结构)。
    defer { print("1") }
    defer { print("2") }
    // 输出:2 → 1
    
  • 作用域限制defer 仅在 当前作用域 有效(如函数、循环、条件语句)。
    func example() {
        if condition {
            defer { print("if 块结束") }
            // ...
        } // 此处执行 defer
        // 函数后续代码...
    }
    

5. 常见应用示例

(1) 文件操作
func readFile(path: String) throws -> String {
    let file = try openFile(path)
    defer {
        closeFile(file) // 确保文件关闭
    }
    return try parseFile(file)
}
(2) 加锁与解锁
let lock = NSLock()
func criticalSection() {
    lock.lock()
    defer { lock.unlock() } // 确保锁释放
    // 执行关键代码...
}
(3) 循环中的资源清理
for url in urls {
    let data = try downloadData(from: url)
    defer { 
        cleanupTemporaryFiles() // 每次循环结束清理
    }
    process(data)
}

6. 注意事项

  • 禁止控制流操作defer 块中不能使用 breakreturn 或抛出错误。
  • 避免副作用:修改外部变量可能导致逻辑混乱。
    var value = 0
    func example() {
        defer { value += 1 } // 不推荐
        // ...
    }
    
  • 性能影响:频繁使用 defer 可能影响性能(极少数情况)。

7. 错误处理中的 defer

即使函数抛出错误,defer 仍会执行:

func riskyOperation() throws {
    let resource = allocateResource()
    defer { releaseResource(resource) } // 错误抛出前执行
    try mayThrowError()
}

8. 总结

场景 示例 作用
文件/网络资源释放 defer { file.close() } 防止资源泄漏
锁的获取与释放 defer { lock.unlock() } 避免死锁
错误处理中的清理 defer { cleanup() } 确保异常时资源释放
临时数据清理 defer { removeTempFiles() } 提升代码健壮性

最佳实践

  • defer 紧跟在资源获取代码后,提高可读性。
  • 避免在 defer 中执行耗时操作或修改外部状态。
  • 优先用于必须执行的清理逻辑,而非复杂业务代码。

通过合理使用 defer,可以显著提升代码的健壮性和可维护性,确保资源安全和逻辑清晰。


四、Swift 与 Objective-C 中的 Protocol 区别

1. 基本概念

  • Objective-C:Protocol 是一种定义方法列表的方式,用于声明接口,实现类必须遵守这些接口(除非标记为 @optional)。
  • Swift:Protocol 不仅定义接口,还支持属性、方法、关联类型、默认实现等,是面向协议编程(POP)的核心工具。

2. 核心区别

特性 Objective-C Protocol Swift Protocol
可选方法 使用 @optional 标记可选方法 默认无可选方法,可通过 @objc optional 兼容 OC,或通过协议扩展提供默认实现
默认实现 不支持 支持通过 协议扩展 提供默认实现
关联类型 不支持 支持 associatedtype,实现泛型协议
值类型支持 仅适用于类(Class) 适用于类、结构体(Struct)、枚举(Enum)
协议继承与组合 单继承(只能继承一个协议) 支持多继承(ProtocolA & ProtocolB
泛型支持 支持泛型约束(where 子句)
属性定义 只能定义方法 可定义属性(需指定 { get }{ get set }
类型检查 运行时检查(conformsToProtocol: 编译时检查 + 运行时检查(isas?

3. 使用场景与示例

Objective-C Protocol
@protocol DataSource <NSObject>
@required
- (NSInteger)numberOfItems;
@optional
- (NSString *)titleForItemAtIndex:(NSInteger)index;
@end

// 类遵循协议
@interface ViewController : UIViewController <DataSource>
@property(nonatomic,weak)id <DataSource> delegate;
@end
  • 特点:主要用于委托模式(Delegate)、数据源模式(DataSource)。
Swift Protocol
protocol Drawable {
    func draw()
}

// 协议扩展提供默认实现
extension Drawable {
    func draw() { print("默认绘制") }
}

struct Circle: Drawable {} // 结构体遵循协议
class Square: Drawable {}  // 类遵循协议

// 关联类型
protocol Container {
    associatedtype Item
    var items: [Item] { get }
    mutating func add(_ item: Item)
}

// 泛型约束
func process<T: Container>(container: T) where T.Item: Equatable {
    // ...
}
  • 特点:支持面向协议编程,适用于值类型和引用类型,灵活扩展功能。

4. 协议扩展(Swift 独有)

Swift 允许通过扩展为协议添加默认实现,而 Objective-C 无法实现:

protocol Loggable {
    func log()
}

extension Loggable {
    func log() { print("日志记录") }
}

struct User: Loggable {} // 自动获得默认 log() 实现

5. 可选方法的实现方式

  • Objective-C:显式标记 @optional,调用时需检查 respondsToSelector:

    @protocol NetworkDelegate <NSObject>
    @optional
    - (void)didReceiveData:(NSData *)data;
    @end
    
    // 调用前检查
    if ([delegate respondsToSelector:@selector(didReceiveData:)]) {
        [delegate didReceiveData:data];
    }
    
  • Swift:通过协议扩展或 @objc optional(仅限类)实现可选方法。

    @objc protocol NetworkDelegate {
        @objc optional func didReceiveData(_ data: Data)
    }
    
    class Handler: NetworkDelegate {} // 可选实现方法
    
    // 调用时检查
    delegate?.didReceiveData?(data)
    

6. 总结

场景 Objective-C Swift
接口定义 委托、数据源等简单场景 面向协议编程、泛型抽象、功能扩展
类型支持 仅类 类、结构体、枚举
灵活性 基础功能,依赖运行时检查 编译时安全,支持默认实现和关联类型
跨平台设计 主要用于 iOS/macOS 开发 跨平台(iOS/macOS/服务器/开源项目)

选择建议

  • Objective-C:在传统项目或需要与 OC 代码交互时使用,适合简单接口定义。
  • Swift:在新项目或需要高度抽象、复用和类型安全时使用,充分发挥面向协议编程的优势。

五、Swift 与 Objective-C 初始化方法(init)有什么不一样?

1. 核心设计理念

  • Swift:强调 安全性严格性,通过两段式初始化编译时检查确保对象完整初始化。
  • Objective-C灵活性优先,依赖开发者自觉管理初始化过程,缺少编译时强制约束。

2. 主要区别

特性 Swift Objective-C
初始化阶段 两段式初始化(属性初始化 → 自定义操作) 无明确阶段划分
安全检查 强制所有非可选(non-optional)存储属性初始化 无强制检查,未初始化的属性可能为 nil 或默认值
可选属性 必须显式初始化或声明为 Optional 默认允许 nil(引用类型)
初始化器类型 支持 指定初始化器(Designated)和 便利初始化器(Convenience) 无明确分类,但可通过 NS_DESIGNATED_INITIALIZER 标记指定初始化器
修饰符 convenience(便利初始化器)、required(强制子类实现)、override(重写父类初始化器) 无类似关键字
可失败初始化器 支持 init?init! 返回 nil 表示失败
继承与重写规则 子类必须重写父类的指定初始化器或自动继承,规则严格 子类可自由重写初始化器,无强制要求
值类型支持 结构体(struct)、枚举(enum)可定义初始化器 仅类(class)支持初始化器

3. 关键机制详解

(1) 两段式初始化(Swift 独有)
  • 阶段 1:确保所有存储属性被初始化。
  • 阶段 2:在属性初始化完成后,进一步自定义实例(如调用方法、访问 self)。
  • 优势:避免属性未初始化就被使用,提升安全性。
  • 示例
    class Person {
        var name: String
        var age: Int
        
        // 指定初始化器
        init(name: String, age: Int) {
            self.name = name // 阶段1:初始化属性
            self.age = age
            // 阶段2:可调用方法
            setup()
        }
        
        convenience init() {
            self.init(name: "Unknown", age: 0) // 必须调用指定初始化器
        }
        
        private func setup() { /*...*/ }
    }
    
(2) 强制属性初始化(Swift)
  • 所有非可选存储属性必须在初始化完成前赋值,否则编译器报错。
  • 示例
    class User {
        var id: Int    // 错误:未初始化
        var name: String?
        
        init(id: Int) {
            self.id = 1234 // 正确:非可选属性必须赋值
        }
    }
    
(3) 初始化器修饰符(Swift)
  • convenience:定义便利初始化器,必须调用同类中的指定初始化器。
    class Rectangle {
        var width: Double
        var height: Double
        
        init(width: Double, height: Double) {
            self.width = width
            self.height = height
        }
        
        convenience init(side: Double) {
            self.init(width: side, height: side) // 调用指定初始化器
        }
    }
    
  • required:强制子类实现该初始化器。
    class Vehicle {
        required init() { /*...*/ }
    }
    
    class Car: Vehicle {
        required init() { // 必须实现
            super.init()
        }
    }
    
(4) 可失败初始化器
  • Swift:通过 init? 返回可选实例,init! 返回隐式解包实例。
    struct Temperature {
        let celsius: Double
        init?(celsius: Double) {
            guard celsius >= -273.15 else { return nil }
            self.celsius = celsius
        }
    }
    
  • Objective-C:返回 nil 表示失败。
    @interface MyClass : NSObject
    - (instancetype)initWithValue:(NSInteger)value;
    @end
    
    @implementation MyClass
    - (instancetype)initWithValue:(NSInteger)value {
        if (value < 0) return nil;
        self = [super init];
        return self;
    }
    @end
    

4. 初始化器继承规则

  • Swift
    • 子类默认不继承父类初始化器。
    • 若子类未定义任何指定初始化器,则自动继承父类所有指定初始化器。
    • 若子类实现了父类所有指定初始化器,则自动继承父类便利初始化器。
  • Objective-C:子类自动继承父类所有初始化器,除非显式重写。

5. 总结

场景 Swift Objective-C
安全性 高(编译时强制检查) 低(依赖开发者自觉)
灵活性 较低(严格规则限制) 高(自由定义初始化逻辑)
代码复杂度 高(需遵循两段式、修饰符规则) 低(简单直接)
适用类型 类、结构体、枚举 仅类
错误处理 可失败初始化器、异常抛出 返回 nilNSError

选择建议

  • Swift:在需要高安全性、复杂类型设计的场景下使用,遵循严格初始化规则。
  • Objective-C:在维护旧项目或需要快速灵活初始化时使用,但需注意潜在风险。

iOS 中的 `@autoreleasepool` 详细解析

作者 90后晨仔
2025年3月29日 16:10

1. 什么是 @autoreleasepool

@autoreleasepoolObjective-CSwift 中用于管理 自动释放对象(Autoreleased Objects) 的机制。它的核心作用是 延迟对象的释放时机,避免内存峰值过高,尤其在需要频繁创建临时对象的场景下优化内存使用。


2. 为什么需要 @autoreleasepool

在 iOS 内存管理中:

  • MRC(手动引用计数):开发者需手动调用 retain/release 管理对象生命周期。
  • ARC(自动引用计数):编译器自动插入内存管理代码,但 自动释放池 依然用于管理某些场景下的临时对象。

核心问题
当一个对象被标记为 autorelease(如通过工厂方法 [NSString stringWithFormat:] 创建的对象),它会被添加到当前的 autoreleasepool 中,直到池子销毁时才释放。若未及时释放池子,大量临时对象可能导致内存峰值。


3. @autoreleasepool 的工作原理

  • 创建与销毁
    • 进入 @autoreleasepool 作用域时,系统会创建一个池子。
    • 退出作用域时,池子中的所有对象会收到 release 消息,若引用计数为 0 则释放。
  • 延迟释放
    对象不会立即释放,而是延迟到池子销毁时统一处理。
@autoreleasepool {
    // 临时对象会被添加到当前池子
    NSString *tempStr = [NSString stringWithFormat:@"Hello, %@", @"World"];
    // 池子销毁时,tempStr 会被释放(若引用计数为 0)
}

4. 使用场景

(1) 循环中创建大量临时对象

在循环中频繁生成临时对象时,使用 @autoreleasepool 及时释放内存:

for (int i = 0; i < 100000; i++) {
    @autoreleasepool {
        NSString *temp = [NSString stringWithFormat:@"Index: %d", i];
        // 每次循环结束后,temp 会被释放
    }
}
(2) 主线程 RunLoop 的自动释放池
  • 主线程 的 RunLoop 在每次事件循环(Event Loop)中会自动创建和销毁 autoreleasepool
  • 子线程 默认不自动创建池子,需手动添加:
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @autoreleasepool {
            // 子线程中的代码
        }
    });
    
(3) 优化内存敏感操作
  • 解析大型 JSON/XML 文件。
  • 处理图像或视频数据时生成临时缓冲对象。

5. 底层机制

(1) NSAutoreleasePool@autoreleasepool
  • MRC 时代:通过 NSAutoreleasePool 类手动管理池子:
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    // 创建对象
    [pool drain]; // 释放池子
    
  • ARC 时代:使用 @autoreleasepool 语法糖,编译器会将其转换为高效的底层代码。
(2) 自动释放池的栈结构
  • 自动释放池以 形式管理,后进先出(LIFO)。
  • 嵌套使用 @autoreleasepool 时,内层池子优先释放:
    @autoreleasepool {
        // 外层池子
        @autoreleasepool {
            // 内层池子(优先释放)
        }
    }
    
(3) 对象如何被添加到池子?
  • 通过 autorelease 方法标记对象(ARC 下由编译器自动插入):
    // MRC 下显式调用
    NSString *str = [[[NSString alloc] initWithFormat:@"Test"] autorelease];
    

6. ARC 下的注意事项

  • 无需手动调用 autorelease:ARC 会自动插入内存管理代码。
  • 推荐使用 @autoreleasepool 语法:比 NSAutoreleasePool 更高效,且可读性更好。
  • 避免过度嵌套:仅在必要时使用,频繁创建池子可能影响性能。

7. 性能优化建议

  • 减少临时对象数量:尽量复用对象,避免循环内频繁创建。
  • 合理控制池子作用域:在内存敏感代码段精确包裹 @autoreleasepool
  • 避免主线程阻塞:耗时操作放在子线程,并为其添加池子。

8. 总结

场景 解决方案
循环中创建大量临时对象 在循环内部使用 @autoreleasepool
子线程中操作 手动添加 @autoreleasepool
解析大型数据文件 分段处理数据并包裹池子
优化内存敏感代码 精确控制池子作用域,及时释放临时对象

通过合理使用 @autoreleasepool,开发者可以有效管理内存峰值,提升应用性能和稳定性。

__block 与 __weak的区别是什么?

作者 90后晨仔
2025年3月29日 15:57

两者在功能上的区别?

__block会持有该对象,即使超出了该对象的作用域,该对象还是会存在的,直到block对象从堆上销毁;

__weak仅仅是将该对象赋值给weak对象,当该对象销毁时,weak对象将指向nil;

__block可以让block修改局部变量,而__weak不能。
MRC中__block是不会引起retain;但在ARC中__block则会引起retain。所以ARC中应该使用__weak。

循环引用的问题?

block下循环引用的问题

__block本身并不能避免循环引用,避免循环引用需要在block内部把__block修饰的obj置为nil

__weak可以避免循环引用,但是其会导致外部对象释放了之后,block 内部也访问不到这个对象的问题,我们可以通过在 block 内部声明一个 __strong的变量来指向 weakObj,使外部对象既能在 block 内部保持住,又能避免循环引用的问题。

❌
❌