瓴岳科技

瓴岳科技

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.二叉树的翻转

226. 翻转二叉树

参考答案

(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/图片资源

移动 H5 首屏秒开优化方案探讨

WebView性能、体验分析与优化

3.线上监控,卡顿、内存

4.Runloop有几个mode?

最常用的模式主要是以下几个:

  • NSDefaultRunLoopMode / kCFRunLoopDefaultMode
  • UITrackingRunLoopMode
  • NSRunLoopCommonModes / kCFRunLoopCommonModes

5.项目里有用到常驻线程吗?

6.KVO原理

KVO 基本概念

KVO 允许对象观察另一个对象的某个属性,当该属性发生变化时,观察者会收到通知。通常,KVO 的使用分为以下几个步骤:

  1. 注册观察者:使用 addObserver:forKeyPath:options:context: 方法注册观察者。
  2. 实现回调方法:实现 observeValueForKeyPath:ofObject:change:context: 方法来处理属性变化。
  3. 移除观察者:使用 removeObserver:forKeyPath: 方法移除观察者。

KVO 工作原理

KVO 的实现主要依赖于 Objective-C 的运行时机制,具体步骤如下:

  1. 动态创建子类
    当你为某个对象的某个属性注册观察者时,Objective-C 运行时会动态创建该对象的一个子类(通常以 NSKVONotifying_ 为前缀),并将该对象的类指针(isa 指针)指向这个新创建的子类。
  2. 重写属性的 setter 方法
    在这个新创建的子类中,Objective-C 运行时会重写被观察属性的 setter 方法。例如,如果你观察的是 name 属性,KVO 会重写 setName: 方法。
  3. 通知观察者
    在重写的 setter 方法中,KVO 会插入一些钩子代码,在属性值变更前后调用 willChangeValueForKey:didChangeValueForKey: 方法。这些方法用于通知观察者属性即将改变和已经改变。
  4. 调用观察者的回调方法
    didChangeValueForKey: 方法会触发 KVO 通知机制,最终调用观察者的 observeValueForKeyPath:ofObject:change:context: 方法。

注意事项

  1. 移除观察者:在对象释放之前,一定要移除所有的观察者,否则会导致崩溃。
  2. 线程安全:KVO 通知是同步的,意味着属性的 setter 方法会在观察者的回调方法完成之后才返回。确保观察者的回调方法是线程安全的非常重要。
  3. KVO-compliant:确保属性遵循 KVO 协议,即通过 setter 方法(而不是直接修改实例变量)来改变属性的值。
  4. 自动和手动通知:默认情况下,KVO 是自动通知的。你也可以通过重写 automaticallyNotifiesObserversForKey: 方法来禁用自动通知,并手动调用 willChangeValueForKey:didChangeValueForKey:

7.IMP、selector、SEL含义和区别

  1. SEL(selector)
    • SELselector 是方法的唯一标识符。
    • 在编译时,编译器会将方法名称映射为一个 SEL 类型的选择器。
    • 选择器用于在运行时查找方法的实现。
  2. IMP
    • IMP 是一个函数指针,指向方法的实际实现。
    • 当你发送消息给对象时,运行时系统会根据 SEL 找到对应的 IMP,然后调用它。
    • 直接调用 IMP 可以略过消息传递机制,提高性能。

8.消息转发机制的优劣,和直接调用相比

  1. 性能
    • 直接调用方法:最快,因为编译器在编译时已经确定了方法的实现。
    • 消息传递机制:稍慢,因为需要在运行时查找方法的实现。
    • 消息转发机制:最慢,因为涉及多个步骤来处理未知消息。
  2. 灵活性
    • 直接调用方法:灵活性最低,因为方法实现是静态绑定的。
    • 消息传递机制:灵活性较高,可以在运行时动态查找和调用方法。
    • 消息转发机制:灵活性最高,可以在运行时动态处理未知消息,甚至可以将消息转发给其他对象。
  3. 使用场景
    • 直接调用方法:适用于性能关键的代码,方法实现是确定的。
    • 消息传递机制:适用于需要一些动态特性的代码。
    • 消息转发机制:适用于需要高度动态性和灵活性的代码,例如代理模式、消息路由等。

9.点击界面上的一个按钮,发生了什么?

响应者链(Responder Chain)

响应者链是一个由 UIResponder 对象组成的链条,这些对象可以响应和处理事件。UIView, UIViewController, UIWindow, 以及 UIApplication 都是 UIResponder 的子类。

事件传递和响应者链

  1. 触摸事件的产生:当用户点击屏幕时,硬件会捕捉到这个触摸事件,并将其传递给 iOS 系统。
  2. 创建 UIEvent 对象:iOS 系统会将触摸数据封装成一个 UIEvent 对象。
  3. 事件传递给 UIWindow:系统会将这个 UIEvent 传递给应用的主 UIWindow
  4. 找到第一响应者UIWindow 会调用 hitTest:withEvent: 方法,从视图层次结构中找出最合适的视图来处理这个触摸事件。这个视图成为第一响应者。
  5. 事件传递给 UIView:触摸事件会被传递给找到的视图(通常是一个 UIButton),并调用其 touchesBegan:withEvent:, touchesMoved:withEvent:, touchesEnded:withEvent: 方法来处理具体的触摸事件。

手势识别器的优先级

手势识别器(Gesture Recognizer)在 iOS 的事件处理机制中具有较高的优先级。具体来说,手势识别器会先接收到触摸事件,并尝试识别是否是自己关心的手势。如果手势识别器识别成功,那么它会处理该事件并阻止事件继续传递给响应者链。

具体的事件传递顺序

  1. 触摸事件产生:用户触摸屏幕,触摸事件被硬件捕获并传递给 iOS 系统。
  2. 创建 UIEvent 对象:iOS 系统将触摸数据封装成一个 UIEvent 对象。
  3. 事件传递给 UIWindow:系统将此 UIEvent 对象传递给应用的主 UIWindow
  4. 手势识别器检测UIWindow 会先将触摸事件传递给视图层次结构中相关视图的手势识别器。如果有手势识别器检测到手势并识别成功,那么该手势识别器会处理该事件,并阻止事件继续传递。
  5. 事件传递给响应者链:如果手势识别器没有处理该事件,那么事件将按照响应者链的机制传递。响应者链会从最合适的视图开始处理触摸事件,调用相关的 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
2
3
4
void (^simpleBlock)(void) = ^{
NSLog(@"This is a simple block");
};
simpleBlock();

使用 -rewrite-objc 将其转换为 C++ 代码后,可以看到 Block 的底层实现。

Block 的结构体

在底层,Block 是一个结构体,通常包含以下几个部分:

  1. isa 指针:指向 Block 的类对象,用于实现 Objective-C 的动态特性。
  2. flags:标志位,指示 Block 的一些特性(如是否需要复制、是否包含捕获变量等)。
  3. reserved:保留字段,通常用于内存对齐。
  4. invoke 指针:指向实际执行 Block 代码的函数指针。
  5. descriptor 指针:指向 Block 的描述信息,包括 Block 的大小、拷贝和释放函数等。
  6. 捕获变量:如果 Block 捕获了外部变量,这些变量会被存储在结构体中。

具体的结构体定义可能如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct __block_impl {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct __block_descriptor *descriptor;
};

struct __block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *src);
};

Block 的内存管理

Block 的内存管理涉及到以下几个方面:

  1. 栈上的 Block:默认情况下,Block 是在栈上分配的。这意味着它的生命周期是有限的,当超出其作用域时,Block 会被销毁。如果试图在作用域外使用该 Block,会导致崩溃。
  2. 堆上的 Block:为了让 Block 在作用域外仍然有效,可以将其复制到堆上。可以使用 Block_copy 函数或 ^ 运算符来实现这一点。在 ARC 环境下,直接赋值给 __strong 类型的变量时,Block 会自动复制到堆上。
  3. 捕获变量的内存管理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
2
3
4
enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
}

使用 Result 类型

定义一个返回 Result 类型的函数

假设我们有一个函数,用于从服务器获取数据,该函数可以成功返回数据,也可能因为网络错误而失败:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
enum NetworkError: Error {
case badURL
case requestFailed
case unknown
}

func fetchData(from urlString: String, completion: (Result<Data, NetworkError>) -> Void) {
guard let url = URL(string: urlString) else {
completion(.failure(.badURL))
return
}

URLSession.shared.dataTask(with: url) { data, response, error in
if let _ = error {
completion(.failure(.requestFailed))
} else if let data = data {
completion(.success(data))
} else {
completion(.failure(.unknown))
}
}.resume()
}

调用并处理 Result

调用 fetchData 函数,并处理返回的 Result

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fetchData(from: "https://example.com") { result in
switch result {
case .success(let data):
// 处理成功情况
print("Data received: \(data)")
case .failure(let error):
// 处理失败情况
switch error {
case .badURL:
print("Bad URL")
case .requestFailed:
print("Request failed")
case .unknown:
print("Unknown error")
}
}
}

Result 类型的便利方法

Swift 提供了一些便利方法来处理 Result 类型,包括 mapflatMapmapErrorflatMapError

map 方法

map 方法可以将 Result 中的成功值转换为另一种类型:

1
2
3
4
5
let result: Result<Int, NetworkError> = .success(42)
let stringResult = result.map { value in
return "The answer is \(value)"
}
// stringResult 是 Result<String, NetworkError>.success("The answer is 42")

flatMap 方法

flatMap 方法用于将 Result 中的成功值转换为另一个 Result

1
2
3
4
5
let result: Result<Int, NetworkError> = .success(42)
let newResult = result.flatMap { value in
return Result<String, NetworkError>.success("The answer is \(value)")
}
// newResult 是 Result<String, NetworkError>.success("The answer is 42")

mapError 方法

mapError 方法用于将 Result 中的错误值转换为另一种错误类型:

1
2
3
4
5
let result: Result<Int, NetworkError> = .failure(.badURL)
let newResult = result.mapError { error in
return MyCustomError.networkError(error)
}
// newResult 是 Result<Int, MyCustomError>.failure(.networkError(.badURL))

flatMapError 方法

flatMapError 方法用于将 Result 中的错误值转换为另一个 Result

1
2
3
4
5
let result: Result<Int, NetworkError> = .failure(.badURL)
let newResult = result.flatMapError { error in
return Result<Int, MyCustomError>.failure(.networkError(error))
}
// newResult 是 Result<Int, MyCustomError>.failure(.networkError(.badURL))

总结

Result 类型在 Swift 中提供了一种优雅的方式来处理可能成功或失败的操作。通过明确地表达成功和失败的情况,Result 类型使得代码更加易读和可维护。借助 mapflatMapmapErrorflatMapError 等便利方法,可以更方便地处理 Result 类型的值。使用 Result 类型可以更好地管理错误处理,特别是在异步操作中。

15.二叉树的翻转

226. 翻转二叉树

递归:

1
2
3
4
5
6
7
8
9
10
var invertTree = function(root) {
if (root === null) {
return null;
}
const left = invertTree(root.left);
const right = invertTree(root.right);
root.left = right;
root.right = left;
return root;
};

迭代:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var invertTree = function(root) {
if (root == null) {
return null;
}
let stack = [];
stack.push(root);
while (stack.length) {
let node = stack.pop();
if (node.left != null) {
stack.push(node.left);
}
if (node.right != null) {
stack.push(node.right);
}
let temp = node.left;
node.left = node.right;
node.right = temp;
}
return root;
};
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×