1.SDK 体积优化
2.webView 加载速度优化,H5 怎么加载本地 JS/图片资源
3.线上监控,卡顿、内存
4.Runloop 有几个 mode?
5.项目里有用到常驻线程吗?
6.KVO 原理
7.IMP、selector、SEL 含义和区别
8.消息转发机制的优劣,和直接调用相比
9.点击界面上的一个按钮,发生了什么?
10.SQLite3 和 Realm 的区别?
11.Category 和 Extension 的区别
12.Block 的本质,结构体里有哪些东西?内存管理
13.为什么要在主线程刷新 UI
14.Swift 中 Result 类型
15.二叉树的翻转
参考答案
(GPT)
1.SDK体积优化
优化iOS SDK体积对于提升应用性能、减少下载时间以及节省用户设备存储空间都非常重要。以下是一些有效的方法来优化iOS SDK的体积:
1. 移除不必要的架构
只保留所需的架构:在构建SDK时,移除不必要的架构,例如x86_64和i386(模拟器架构),只保留arm64(实际设备架构)。
1
lipo -remove x86_64 YourSDK.framework/YourSDK -o YourSDK.framework/YourSDK
2. 减少资源文件
- 移除未使用的资源:移除未使用的图片、音频、视频等资源文件。
- 压缩资源文件:使用工具(如ImageOptim)压缩图片文件,使用合适的编码格式压缩音频和视频文件。
3. 使用符号表压缩
压缩符号表:通过strip工具压缩符号表以减少二进制文件的体积。
1
strip -S YourSDK.framework/YourSDK
4. 代码优化
- 移除未使用代码:通过代码分析工具(如Xcode的静态分析功能)查找并移除未使用的代码。
- 模块化:将SDK拆分成多个模块,只在需要时引入特定模块。
5. 使用Swift Package Manager
- Swift Package Manager (SPM):使用SPM来管理和分发SDK,可以显著减少SDK的体积,因为SPM只会下载和编译所需的代码。
6. 动态库 vs 静态库
- 选择合适的库类型:在某些情况下,使用动态库(.dylib或.framework)可以减少整体应用的体积,因为它们可以被多个应用共享。
7. 编译优化
优化编译设置:在Xcode中,确保在Release配置下选择优化选项(如-Oz)以减少代码体积。
1
Build Settings -> Optimization Level -> Optimize for Size [-Oz]
8. 引用第三方库优化
- 移除未使用的第三方库:确保只包含那些在SDK中实际使用的第三方库。
- 使用轻量级的第三方库:选择功能相同但体积更小的第三方库。
9. 符号混淆
- 符号混淆:使用符号混淆工具(如llvm-obfuscator)混淆符号表以减少符号表大小和提高代码安全性。
通过以上方法,您可以显著减少iOS SDK的体积,从而提升用户体验和应用的整体性能。
2.webView加载速度优化,H5怎么加载本地JS/图片资源
3.线上监控,卡顿、内存
4.Runloop有几个mode?
最常用的模式主要是以下几个:
NSDefaultRunLoopMode
/kCFRunLoopDefaultMode
UITrackingRunLoopMode
NSRunLoopCommonModes
/kCFRunLoopCommonModes
5.项目里有用到常驻线程吗?
6.KVO原理
KVO 基本概念
KVO 允许对象观察另一个对象的某个属性,当该属性发生变化时,观察者会收到通知。通常,KVO 的使用分为以下几个步骤:
- 注册观察者:使用
addObserver:forKeyPath:options:context:
方法注册观察者。 - 实现回调方法:实现
observeValueForKeyPath:ofObject:change:context:
方法来处理属性变化。 - 移除观察者:使用
removeObserver:forKeyPath:
方法移除观察者。
KVO 工作原理
KVO 的实现主要依赖于 Objective-C 的运行时机制,具体步骤如下:
- 动态创建子类
当你为某个对象的某个属性注册观察者时,Objective-C 运行时会动态创建该对象的一个子类(通常以NSKVONotifying_
为前缀),并将该对象的类指针(isa
指针)指向这个新创建的子类。 - 重写属性的 setter 方法
在这个新创建的子类中,Objective-C 运行时会重写被观察属性的 setter 方法。例如,如果你观察的是name
属性,KVO 会重写setName:
方法。 - 通知观察者
在重写的 setter 方法中,KVO 会插入一些钩子代码,在属性值变更前后调用willChangeValueForKey:
和didChangeValueForKey:
方法。这些方法用于通知观察者属性即将改变和已经改变。 - 调用观察者的回调方法
didChangeValueForKey:
方法会触发 KVO 通知机制,最终调用观察者的observeValueForKeyPath:ofObject:change:context:
方法。
注意事项
- 移除观察者:在对象释放之前,一定要移除所有的观察者,否则会导致崩溃。
- 线程安全:KVO 通知是同步的,意味着属性的 setter 方法会在观察者的回调方法完成之后才返回。确保观察者的回调方法是线程安全的非常重要。
- KVO-compliant:确保属性遵循 KVO 协议,即通过 setter 方法(而不是直接修改实例变量)来改变属性的值。
- 自动和手动通知:默认情况下,KVO 是自动通知的。你也可以通过重写
automaticallyNotifiesObserversForKey:
方法来禁用自动通知,并手动调用willChangeValueForKey:
和didChangeValueForKey:
。
7.IMP、selector、SEL含义和区别
- SEL(selector)
SEL
或selector
是方法的唯一标识符。- 在编译时,编译器会将方法名称映射为一个
SEL
类型的选择器。 - 选择器用于在运行时查找方法的实现。
- IMP
IMP
是一个函数指针,指向方法的实际实现。- 当你发送消息给对象时,运行时系统会根据
SEL
找到对应的IMP
,然后调用它。 - 直接调用
IMP
可以略过消息传递机制,提高性能。
8.消息转发机制的优劣,和直接调用相比
- 性能
- 直接调用方法:最快,因为编译器在编译时已经确定了方法的实现。
- 消息传递机制:稍慢,因为需要在运行时查找方法的实现。
- 消息转发机制:最慢,因为涉及多个步骤来处理未知消息。
- 灵活性
- 直接调用方法:灵活性最低,因为方法实现是静态绑定的。
- 消息传递机制:灵活性较高,可以在运行时动态查找和调用方法。
- 消息转发机制:灵活性最高,可以在运行时动态处理未知消息,甚至可以将消息转发给其他对象。
- 使用场景
- 直接调用方法:适用于性能关键的代码,方法实现是确定的。
- 消息传递机制:适用于需要一些动态特性的代码。
- 消息转发机制:适用于需要高度动态性和灵活性的代码,例如代理模式、消息路由等。
9.点击界面上的一个按钮,发生了什么?
响应者链(Responder Chain)
响应者链是一个由 UIResponder
对象组成的链条,这些对象可以响应和处理事件。UIView
, UIViewController
, UIWindow
, 以及 UIApplication
都是 UIResponder
的子类。
事件传递和响应者链
- 触摸事件的产生:当用户点击屏幕时,硬件会捕捉到这个触摸事件,并将其传递给 iOS 系统。
- 创建 UIEvent 对象:iOS 系统会将触摸数据封装成一个
UIEvent
对象。 - 事件传递给 UIWindow:系统会将这个
UIEvent
传递给应用的主UIWindow
。 - 找到第一响应者:
UIWindow
会调用hitTest:withEvent:
方法,从视图层次结构中找出最合适的视图来处理这个触摸事件。这个视图成为第一响应者。 - 事件传递给 UIView:触摸事件会被传递给找到的视图(通常是一个
UIButton
),并调用其touchesBegan:withEvent:
,touchesMoved:withEvent:
,touchesEnded:withEvent:
方法来处理具体的触摸事件。
手势识别器的优先级
手势识别器(Gesture Recognizer)在 iOS 的事件处理机制中具有较高的优先级。具体来说,手势识别器会先接收到触摸事件,并尝试识别是否是自己关心的手势。如果手势识别器识别成功,那么它会处理该事件并阻止事件继续传递给响应者链。
具体的事件传递顺序
- 触摸事件产生:用户触摸屏幕,触摸事件被硬件捕获并传递给 iOS 系统。
- 创建 UIEvent 对象:iOS 系统将触摸数据封装成一个
UIEvent
对象。 - 事件传递给 UIWindow:系统将此
UIEvent
对象传递给应用的主UIWindow
。 - 手势识别器检测:
UIWindow
会先将触摸事件传递给视图层次结构中相关视图的手势识别器。如果有手势识别器检测到手势并识别成功,那么该手势识别器会处理该事件,并阻止事件继续传递。 - 事件传递给响应者链:如果手势识别器没有处理该事件,那么事件将按照响应者链的机制传递。响应者链会从最合适的视图开始处理触摸事件,调用相关的
touchesBegan:withEvent:
,touchesMoved:withEvent:
,touchesEnded:withEvent:
方法。
10.SQLite3和Realm的区别?
1. 数据库类型
- SQLite3: SQLite 是一个轻量级的关系型数据库管理系统,遵循 SQL 标准。它使用 SQL 语言进行数据操作,支持复杂的查询和事务处理。
- Realm: Realm 是一个面向对象的数据库,旨在简化数据存储和查询过程。它不使用 SQL,而是通过对象模型进行数据操作。
2. 数据模型
- SQLite3: 使用关系型数据模型,数据存储在表中,表包含行和列。数据操作通过 SQL 查询语句完成。
- Realm: 使用面向对象的数据模型,数据存储在对象中。你可以直接通过对象属性进行查询和操作,这使得代码更加直观和简洁。
3. 性能
- SQLite3: 通常在处理非常复杂的查询或需要高度自定义的查询时性能较好。SQLite3 的性能可能会受到 SQL 查询复杂性的影响。
- Realm: 由于其设计面向移动设备,通常在读写性能上表现优异,尤其是对于常见的、简单的查询操作。Realm 还提供了对多线程的支持,以提高并发性能。
4. 数据迁移
- SQLite3: 数据库迁移通常需要手动编写 SQL 脚本来修改表结构,这可能会比较繁琐。
- Realm: 提供了内置的数据迁移机制,可以通过代码来定义迁移步骤,这使得数据迁移过程更加方便和安全。
5. 易用性
- SQLite3: 由于使用 SQL 语言,开发者需要了解 SQL 语法和关系型数据库的基本概念。数据模型的改变通常需要手动修改表结构。
- Realm: 提供了面向对象的 API,使用起来更加直观。开发者不需要学习 SQL,只需操作对象模型即可。
6. 数据同步
- SQLite3: 本身不提供数据同步功能。如果需要在不同设备间同步数据,通常需要额外的服务器支持。
- Realm: 提供了 Realm Cloud 服务,可以实现数据的实时同步和离线访问,适合需要多设备数据同步的应用。
7. 文件大小和存储方式
- SQLite3: 数据存储在单个文件中,文件大小会随着数据量的增加而增加。SQLite3 支持多种数据类型和存储格式。
- Realm: 数据也存储在单个文件中,但由于其高效的存储格式和压缩机制,通常文件大小较小。Realm 对于对象模型的存储进行了优化,减少了数据的冗余。
8. 加密和安全性
- SQLite3: 支持加密,但需要使用第三方库(如 SQLCipher)来实现。
- Realm: 内置支持加密,可以方便地加密整个数据库文件,提高数据安全性。
9. 多平台支持
- SQLite3: 支持几乎所有的平台,包括 iOS、Android、Windows、Linux 等。因为其广泛的支持和成熟度,SQLite 是一个非常可靠的选择。
- Realm: 也支持多平台,包括 iOS、Android、React Native 等。Realm 提供的 API 在不同平台上保持一致性,便于跨平台开发。
总结
- SQLite3: 适合需要复杂查询、事务处理和高度自定义的应用,尤其是那些开发者已经熟悉 SQL 语言的情况。
- Realm: 适合需要高性能、简单易用和面向对象的数据存储解决方案,特别是移动应用开发。
11.Category和Extension的区别
特性 | Category | Extension |
---|---|---|
定义和用途 | 为现有类添加方法 | 在实现文件中添加私有方法和属性 |
实例变量 | 不能添加 | 可以添加 |
访问控制 | 方法是公开的 | 方法和属性是私有的 |
编译时机 | 动态加载,在运行时添加 | 编译时添加,类定义的一部分 |
使用场景 | 将类的方法划分到多个文件,为系统类添加方法 | 在实现文件中定义私有方法和属性,提高封装性 |
12.Block的本质,结构体里有哪些东西?内存管理
Block 的本质
在底层,Block
是一个封装了函数指针、捕获变量及其他相关信息的结构体。可以通过 Clang 提供的编译器选项 -rewrite-objc
将 Objective-C 代码转换为纯 C++ 代码,来查看 Block
的具体实现。
例如,以下是一个简单的 Block
声明和使用:
1 | void (^simpleBlock)(void) = ^{ |
使用 -rewrite-objc
将其转换为 C++ 代码后,可以看到 Block
的底层实现。
Block 的结构体
在底层,Block
是一个结构体,通常包含以下几个部分:
- isa 指针:指向
Block
的类对象,用于实现 Objective-C 的动态特性。 - flags:标志位,指示
Block
的一些特性(如是否需要复制、是否包含捕获变量等)。 - reserved:保留字段,通常用于内存对齐。
- invoke 指针:指向实际执行
Block
代码的函数指针。 - descriptor 指针:指向
Block
的描述信息,包括Block
的大小、拷贝和释放函数等。 - 捕获变量:如果
Block
捕获了外部变量,这些变量会被存储在结构体中。
具体的结构体定义可能如下所示:
1 | struct __block_impl { |
Block 的内存管理
Block
的内存管理涉及到以下几个方面:
- 栈上的 Block:默认情况下,
Block
是在栈上分配的。这意味着它的生命周期是有限的,当超出其作用域时,Block
会被销毁。如果试图在作用域外使用该Block
,会导致崩溃。 - 堆上的 Block:为了让
Block
在作用域外仍然有效,可以将其复制到堆上。可以使用Block_copy
函数或^
运算符来实现这一点。在 ARC 环境下,直接赋值给__strong
类型的变量时,Block
会自动复制到堆上。 - 捕获变量的内存管理:
Block
可以捕获其作用域中的变量,包括自动变量(局部变量)和静态变量。捕获变量的内存管理取决于变量的类型:- 对于自动变量,
Block
会将其值拷贝到Block
的结构体中。 - 对于对象类型的变量,
Block
会对其进行retain
操作(在 ARC 下),以确保Block
的生命周期内变量仍然有效。
- 对于自动变量,
13.为什么要在主线程刷新UI
在 iOS 和 macOS 开发中,所有的 UI 更新必须在主线程(也称为主队列)上执行。以下是为什么需要在主线程刷新 UI 的几个原因:
1. UIKit 和 AppKit 的线程安全性
- 单线程设计:
- UIKit(iOS 的 UI 框架)和 AppKit(macOS 的 UI 框架)是设计为非线程安全的。它们的大多数 API 都假定是在主线程上调用的。
- 这是因为 UI 操作通常涉及到大量复杂的内部状态管理和绘图操作,确保所有这些操作在一个单一线程上可以避免并发问题。
2. 数据一致性和线程同步
- 数据一致性:
- 如果多个线程同时操作 UI 组件,可能会导致不一致的 UI 状态。例如,一个线程正在修改视图的属性,而另一个线程正在试图渲染视图,这可能会导致崩溃或未定义行为。
- 线程同步:
- 多线程操作 UI 需要额外的同步机制来防止并发访问冲突。使用主线程可以简化这种同步需求,使代码更容易维护和理解。
3. 事件处理模型
- 事件循环:
- iOS 和 macOS 应用程序的主线程运行一个事件循环,处理用户交互、定时器、网络响应等各种事件。UI 更新也是事件循环的一部分。
- 通过将所有 UI 操作放在主线程上,确保了事件处理的顺序性和一致性。
4. 用户体验
- 平滑的 UI 动画和响应:
- 在主线程上更新 UI 可以确保动画的平滑性和用户交互的及时响应。如果在后台线程更新 UI,可能会导致动画卡顿或延迟响应,影响用户体验。
- 避免死锁:
- 主线程上的操作是顺序执行的,这降低了发生死锁的风险。如果尝试在后台线程进行复杂的 UI 操作,可能会导致线程间的资源争用和死锁。
总结
在主线程刷新 UI 是为了确保 UIKit 和 AppKit 的线程安全性、保持数据一致性和简化线程同步、遵循事件处理模型以及提供更好的用户体验。通过理解和遵循这些原则,可以避免许多潜在的并发问题和性能瓶颈,从而构建更稳定和高效的应用程序。
14.Swift中Result类型
在 Swift 中,Result
类型是一种用于表示操作结果的枚举,能够明确地表达成功和失败的情况。它的引入使得错误处理更加简洁和明确,特别是在异步操作和函数返回值中。
Result
类型的定义
Result
类型是一个泛型枚举,有两个可能的值:
.success(Value)
:表示操作成功,并包含成功时的返回值。.failure(Error)
:表示操作失败,并包含失败时的错误。
以下是 Result
类型的定义:
1 | enum Result<Success, Failure: Error> { |
使用 Result
类型
定义一个返回 Result
类型的函数
假设我们有一个函数,用于从服务器获取数据,该函数可以成功返回数据,也可能因为网络错误而失败:
1 | enum NetworkError: Error { |
调用并处理 Result
调用 fetchData
函数,并处理返回的 Result
:
1 | fetchData(from: "https://example.com") { result in |
Result
类型的便利方法
Swift 提供了一些便利方法来处理 Result
类型,包括 map
、flatMap
、mapError
和 flatMapError
。
map
方法
map
方法可以将 Result
中的成功值转换为另一种类型:
1 | let result: Result<Int, NetworkError> = .success(42) |
flatMap
方法
flatMap
方法用于将 Result
中的成功值转换为另一个 Result
:
1 | let result: Result<Int, NetworkError> = .success(42) |
mapError
方法
mapError
方法用于将 Result
中的错误值转换为另一种错误类型:
1 | let result: Result<Int, NetworkError> = .failure(.badURL) |
flatMapError
方法
flatMapError
方法用于将 Result
中的错误值转换为另一个 Result
:
1 | let result: Result<Int, NetworkError> = .failure(.badURL) |
总结
Result
类型在 Swift 中提供了一种优雅的方式来处理可能成功或失败的操作。通过明确地表达成功和失败的情况,Result
类型使得代码更加易读和可维护。借助 map
、flatMap
、mapError
和 flatMapError
等便利方法,可以更方便地处理 Result
类型的值。使用 Result
类型可以更好地管理错误处理,特别是在异步操作中。
15.二叉树的翻转
递归:
1 | var invertTree = function(root) { |
迭代:
1 | var invertTree = function(root) { |