百融云创面试题

百融云创面试题

百融云创面试题

1.腾讯会议共享屏幕在线编程 https://leetcode.cn/problems/zhan-de-ya-ru-dan-chu-xu-lie-lcof/description/
2.KVO 原理
3.如何调用私有 API?三方库或系统未暴漏的方法
4.怎么改一个类的只读属性
5.消息机制
6.Runloop 流程,用 Runloop 做过什么?卡顿检测,发生卡顿后怎么获取堆栈信息?堆栈信息的原理
7.GCD 都用了哪些?网络请求 A 和 B 都回调后,请求 C,如何实现?除了 dispatch_group
8.可变数组 NSMutableArray,在头部插入元素,iOS 系统做了什么优化
9.Tagged Pointer
10.NSString 存储在哪里,引用计数存储哪里?
11.dealloc 流程
12.避免哈希碰撞的几种方法?
13.mach-o 了解吗?存储的方法信息可以无用代码检测?段迁移?
14.脚本检测未使用代码原理
15.render 渲染流程
16.UIView 动画和 CAAnimation 动画有什么联系?
17.项目亮点

参考答案

1.腾讯会议共享屏幕在线编程 https://leetcode.cn/problems/zhan-de-ya-ru-dan-chu-xu-lie-lcof/description/

2.KVO 原理工作原理

KVO 主要依赖于 Objective-C 的动态性和 Runtime 运行时机制。以下是 KVO 的几个核心步骤和原理:

1. 动态子类化

当你第一次给某个对象添加观察者时,Objective-C Runtime 会动态地创建该对象的一个子类。这个子类会重写被观察属性的 setter 方法。

例如,如果你观察对象 personname 属性:

1
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];

Runtime 会创建一个新的类,假设名为 NSKVONotifying_Person,这个类是 Person 类的子类。

2. 重写 setter 方法

在新的子类中,Runtime 会重写被观察属性的 setter 方法。例如,对于 name 属性,其 setter 方法可能如下:

1
2
3
4
5
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}
  • willChangeValueForKey::通知系统属性即将发生变化。
  • didChangeValueForKey::通知系统属性已经发生变化。

这些方法会触发 KVO 通知机制,向所有观察者发送通知。

3. 注册观察者

当你调用 addObserver:forKeyPath:options:context: 方法时,Runtime 会将观察者注册到一个内部的观察者列表中,这个列表通常是一个哈希表(Hash Table),键是被观察的属性,值是观察者对象。

4. 通知观察者

当属性值发生变化时,重写的 setter 方法会调用 willChangeValueForKey:didChangeValueForKey: 方法,这些方法会触发 KVO 通知机制,向注册的观察者发送通知。观察者会收到 observeValueForKeyPath:ofObject:change:context: 方法的回调。

3.如何调用私有 API?三方库或系统未暴漏的方法

1. 使用 performSelector: 方法

performSelector: 方法可以动态调用对象的方法,即使这些方法在编译时不可见。

1
2
3
4
SEL privateSelector = NSSelectorFromString(@"privateMethod");
if ([object respondsToSelector:privateSelector]) {
[object performSelector:privateSelector];
}

2. 使用 Runtime 动态调用

Objective-C 的 Runtime 库提供了一些函数,可以用来动态调用方法。

示例代码:调用私有方法

1
2
3
4
5
6
7
8
#import <objc/runtime.h>

SEL privateSelector = NSSelectorFromString(@"privateMethod");
if ([object respondsToSelector:privateSelector]) {
IMP imp = [object methodForSelector:privateSelector];
void (*func)(id, SEL) = (void *)imp;
func(object, privateSelector);
}

示例代码:访问私有属性

1
2
3
4
#import <objc/runtime.h>

Ivar ivar = class_getInstanceVariable([object class], "_privateProperty");
id privateValue = object_getIvar(object, ivar);

3. 使用 dlsym 函数

如果你知道私有 API 的符号名称,可以使用 dlsym 函数来获取函数指针,并调用它。

1
2
3
4
5
6
7
8
9
10
#import <dlfcn.h>

void *handle = dlopen(NULL, RTLD_LAZY);
if (handle) {
void (*privateFunction)(void) = dlsym(handle, "privateFunctionName");
if (privateFunction) {
privateFunction();
}
dlclose(handle);
}

4.怎么改一个类的只读属性

方法一:通过 KVC (Key-Value Coding)

Key-Value Coding (KVC) 是一种间接访问对象属性的方法。即使属性是只读的,也可以通过 KVC 修改它的值。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@interface MyClass : NSObject
@property (nonatomic, strong, readonly) NSString *readonlyProperty;
@end

@implementation MyClass {
NSString *_readonlyProperty;
}

- (instancetype)init {
self = [super init];
if (self) {
_readonlyProperty = @"Initial Value";
}
return self;
}
@end

// 修改只读属性
MyClass *myObject = [[MyClass alloc] init];
NSLog(@"Before: %@", myObject.readonlyProperty);

[myObject setValue:@"New Value" forKey:@"readonlyProperty"];
NSLog(@"After: %@", myObject.readonlyProperty);

方法二:使用运行时函数

在 Objective-C 中,可以使用运行时函数来修改属性的值。

示例:

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
#import <objc/runtime.h>

@interface MyClass : NSObject
@property (nonatomic, strong, readonly) NSString *readonlyProperty;
@end

@implementation MyClass {
NSString *_readonlyProperty;
}

- (instancetype)init {
self = [super init];
if (self) {
_readonlyProperty = @"Initial Value";
}
return self;
}
@end

// 修改只读属性
MyClass *myObject = [[MyClass alloc] init];
NSLog(@"Before: %@", myObject.readonlyProperty);

Ivar ivar = class_getInstanceVariable([MyClass class], "_readonlyProperty");
object_setIvar(myObject, ivar, @"New Value");

NSLog(@"After: %@", myObject.readonlyProperty);

方法三:子类化和重写 Getter 方法

通过创建一个子类并重写只读属性的 getter 方法,可以控制属性的值。

示例:

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
@interface MyClass : NSObject
@property (nonatomic, strong, readonly) NSString *readonlyProperty;
@end

@implementation MyClass {
NSString *_readonlyProperty;
}

- (instancetype)init {
self = [super init];
if (self) {
_readonlyProperty = @"Initial Value";
}
return self;
}
@end

@interface MyClassSubclass : MyClass
@end

@implementation MyClassSubclass {
NSString *_customValue;
}

- (NSString *)readonlyProperty {
return _customValue ? _customValue : [super readonlyProperty];
}

- (void)setCustomValue:(NSString *)value {
_customValue = value;
}
@end

// 修改只读属性
MyClassSubclass *myObject = [[MyClassSubclass alloc] init];
NSLog(@"Before: %@", myObject.readonlyProperty);

[myObject setCustomValue:@"New Value"];
NSLog(@"After: %@", myObject.readonlyProperty);

5.消息机制

1. 基本概念

  • 消息(Message):在 Objective-C 中,方法调用被称为“发送消息”。发送消息的语法是 [receiver message]
  • 接收者(Receiver):接收并处理消息的对象。
  • 选择器(Selector):表示方法名称的一个数据类型,类型为 SEL。选择器是方法的唯一标识符。

2. 消息发送流程

当向一个对象发送消息时,Objective-C 的运行时系统会进行一系列步骤来查找并调用相应的方法。以下是消息发送的基本流程:

  1. 消息发送:当代码中调用 [object message] 时,编译器将其转换为 objc_msgSend 函数调用。
  2. 查找方法objc_msgSend 函数会根据消息的选择器(SEL)在对象的类及其父类的方法列表中查找对应的方法实现。
  3. 调用方法:找到方法实现后,objc_msgSend 函数会调用该方法。如果没有找到方法实现,则会触发消息转发机制(Message Forwarding)。

3. objc_msgSend 函数

objc_msgSend 是 Objective-C 运行时系统的核心函数之一,负责消息的分发。其原型如下:

1
void objc_msgSend(id self, SEL _cmd, ...);
  • self:消息的接收者。
  • _cmd:消息的选择器。
  • 可变参数:方法的参数。

4. 消息转发机制

objc_msgSend 无法在类的方法列表中找到与选择器匹配的方法时,会触发消息转发机制。消息转发机制包括以下几个步骤:

4.1 动态方法解析

首先,运行时系统会尝试动态方法解析,通过调用类的 +resolveInstanceMethod:+resolveClassMethod: 方法来添加方法实现。如果方法实现被动态添加,消息发送会重新开始。

1
2
3
4
5
6
7
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(myMethod)) {
class_addMethod([self class], sel, (IMP)myMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}

4.2 快速转发

如果动态方法解析未能添加方法实现,运行时系统会调用 -forwardingTargetForSelector: 方法。这个方法允许将消息转发给另一个对象。

1
2
3
4
5
6
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(myMethod)) {
return someOtherObject;
}
return [super forwardingTargetForSelector:aSelector];
}

4.3 常规转发

如果快速转发也未能处理消息,运行时系统会调用 -methodSignatureForSelector:-forwardInvocation: 方法。首先,通过 -methodSignatureForSelector: 获取方法签名,然后通过 -forwardInvocation: 进行消息转发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(myMethod)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = [anInvocation selector];
if ([someOtherObject respondsToSelector:sel]) {
[anInvocation invokeWithTarget:someOtherObject];
} else {
[super forwardInvocation:anInvocation];
}
}

5. 消息缓存

为了提高消息发送的性能,Objective-C 运行时系统使用方法缓存(Method Caching)。当一个方法第一次被调用时,其实现会被缓存起来,下次调用相同的方法时,可以直接从缓存中获取,而不需要重新查找。

6.Runloop 流程,用 Runloop 做过什么?卡顿检测,发生卡顿后怎么获取堆栈信息?堆栈信息的原理

Runloop 基本概念

RunLoop 是一个事件处理循环,用来调度和处理任务。它可以在没有任务时使线程进入休眠状态,从而节省资源;有任务时则立即唤醒线程处理任务。RunLoop 实质上是一个对象,这个对象管理着其需要处理的事件和消息,并提供一个入口函数来执行这个事件处理循环。

Runloop 的基本流程

RunLoop 的基本流程可以概括为以下几个步骤:

  1. 进入循环:调用 CFRunLoopRun 或者 -[NSRunLoop run] 方法。
  2. 通知观察者:通知即将进入 RunLoop 事件处理循环。
  3. 检查 Timer:检查是否有定时器(Timer)需要处理。
  4. 处理输入源:处理输入源(Input Source),比如用户触摸事件,UI事件等。
  5. 通知观察者:通知即将进入休眠。
  6. 休眠:如果没有事件需要处理,线程进入休眠状态,等待事件发生。
  7. 唤醒:收到外部事件(如 Timer 到时间、输入源事件)唤醒线程。
  8. 通知观察者:通知即将处理事件。
  9. 处理事件:处理 Timer 或输入源事件。
  10. 通知观察者:通知事件已处理完成。
  11. 重复或退出:重复上述步骤,或根据特定条件退出循环。

RunLoop 在 iOS 中的应用

1. 保持线程活跃

RunLoop 的一个典型应用是保持线程活跃。默认情况下,子线程在任务完成后会立即退出。通过 RunLoop,可以让子线程在没有任务时处于休眠状态,但不会退出,等待新的任务到来。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)startThread {
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadEntryPoint) object:nil];
[thread start];
}

- (void)threadEntryPoint {
@autoreleasepool {
[[NSThread currentThread] setName:@"MyThread"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run]; // 进入 RunLoop
}
}

2. 处理定时任务

RunLoop 可以用来处理定时任务,比如定时器。通过将 NSTimer 添加到 RunLoop,可以在指定的时间间隔触发任务。

示例代码:

1
2
3
4
5
6
7
8
- (void)scheduleTimer {
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerFired:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}

- (void)timerFired:(NSTimer *)timer {
NSLog(@"Timer fired!");
}

3. 处理网络请求

在处理网络请求时,可以使用 RunLoop 来保持线程活跃,等待网络请求完成。NSURLConnectionNSURLSession 都使用 RunLoop 来处理网络事件。

4. 处理输入源事件

RunLoop 可以用来处理输入源事件,比如用户触摸事件、UI事件等。

7.GCD 都用了哪些?网络请求 A 和 B 都回调后,请求 C,如何实现?除了 dispatch_group

Grand Central Dispatch (GCD) 是苹果公司推出的一种用于并发编程的技术,旨在优化多核设备上的代码执行。GCD 提供了一种简单易用的 API 来管理并发任务,并且能够自动利用系统资源进行优化。以下是一些常见的 GCD 使用场景:

1. 异步任务

GCD 的一个主要用途就是在后台执行耗时任务,从而避免阻塞主线程,提高应用的响应速度。

示例代码:

1
2
3
4
5
6
7
8
9
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 执行耗时任务
NSData *data = [self fetchDataFromServer];

// 回到主线程更新 UI
dispatch_async(dispatch_get_main_queue(), ^{
[self updateUIWithData:data];
});
});

2. 并行任务

GCD 可以轻松地创建并行任务,从而充分利用多核处理器的计算能力。

示例代码:

1
2
3
4
5
6
7
8
9
10
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
[self performTask1];
});
dispatch_async(queue, ^{
[self performTask2];
});
dispatch_async(queue, ^{
[self performTask3];
});

3. 同步任务

尽管异步任务更常见,有时也需要同步任务来确保任务按顺序执行。

示例代码:

1
2
3
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self performSynchronousTask];
});

4. 延迟执行

GCD 提供了简洁的 API 来延迟执行任务。

示例代码:

1
2
3
4
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(delay, dispatch_get_main_queue(), ^{
[self performDelayedTask];
});

5. 一次性代码

有时我们需要确保某段代码只执行一次,这时候可以使用 dispatch_once

示例代码:

1
2
3
4
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self initializeOnce];
});

6. 调度组

调度组可以用来监控一组任务的完成状态,通常用于需要等待多项任务全部完成后才能继续下一步操作的场景。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_group_async(group, queue, ^{
[self performTask1];
});
dispatch_group_async(group, queue, ^{
[self performTask2];
});
dispatch_group_async(group, queue, ^{
[self performTask3];
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
[self allTasksCompleted];
});

7. 信号量

信号量可以用来控制并发访问资源的数量,适用于需要限制并发任务数量的场景。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2); // 允许同时执行两个并发任务

for (int i = 0; i < 5; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

// 执行任务
NSLog(@"Task %d started", i);
sleep(2); // 模拟耗时任务
NSLog(@"Task %d completed", i);

dispatch_semaphore_signal(semaphore);
});
}

8. Barrier Block

Barrier Block 用于在并发队列中插入一个障碍,确保在障碍前提交的所有任务执行完成后,才执行障碍后的任务。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dispatch_queue_t queue = dispatch_queue_create("com.example.myqueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{
[self performReadTask1];
});
dispatch_async(queue, ^{
[self performReadTask2];
});

dispatch_barrier_async(queue, ^{
[self performWriteTask];
});

dispatch_async(queue, ^{
[self performReadTask3];
});

9. Dispatch Source

Dispatch Source 是一种非常强大的工具,可以用来处理各种系统事件,如文件变化、定时器、信号等。

示例代码(定时器):

1
2
3
4
5
6
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
NSLog(@"Timer fired");
});
dispatch_resume(timer);

10. Dispatch Work Item

使用 dispatch_block_t 创建可取消的工作项。

示例代码:

1
2
3
4
5
6
7
8
9
10
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_block_t workItem = dispatch_block_create(0, ^{
// 执行任务
NSLog(@"Work item executed");
});

dispatch_async(queue, workItem);

// 取消工作项
dispatch_block_cancel(workItem);

在 iOS 开发中,有时候需要等待多个网络请求完成后再进行进一步操作,比如发起新的网络请求。为了实现这种需求,可以使用多种方法,包括 GCD(Grand Central Dispatch)、NSOperationQueue 和第三方库如 PromiseKitCombine

方法一:使用 GCD (Grand Central Dispatch)

GCD 提供了一种简单的方法来同步多个异步任务。可以使用 dispatch_group 来实现等待多个任务完成后执行下一步操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
dispatch_group_t group = dispatch_group_create();

// 开始请求A
dispatch_group_enter(group);
[self performRequestAWithCompletion:^(id response, NSError *error) {
// 处理请求A的响应
dispatch_group_leave(group);
}];

// 开始请求B
dispatch_group_enter(group);
[self performRequestBWithCompletion:^(id response, NSError *error) {
// 处理请求B的响应
dispatch_group_leave(group);
}];

// 当请求A和请求B都完成后,执行请求C
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
[self performRequestCWithCompletion:^(id response, NSError *error) {
// 处理请求C的响应
}];
});

方法二:使用 NSOperationQueue

NSOperationQueue 提供了一种更为面向对象的方法来管理异步任务的依赖关系。

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
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 2; // 设置最大并发数

NSBlockOperation *operationA = [NSBlockOperation blockOperationWithBlock:^{
[self performRequestAWithCompletion:^(id response, NSError *error) {
// 处理请求A的响应
}];
}];

NSBlockOperation *operationB = [NSBlockOperation blockOperationWithBlock:^{
[self performRequestBWithCompletion:^(id response, NSError *error) {
// 处理请求B的响应
}];
}];

NSBlockOperation *operationC = [NSBlockOperation blockOperationWithBlock:^{
[self performRequestCWithCompletion:^(id response, NSError *error) {
// 处理请求C的响应
}];
}];

// 设置依赖关系
[operationC addDependency:operationA];
[operationC addDependency:operationB];

// 将操作添加到队列
[queue addOperation:operationA];
[queue addOperation:operationB];
[queue addOperation:operationC];

方法三:使用第三方库 PromiseKit

PromiseKit 提供了一种现代的、链式的方式来处理异步操作。

首先,确保你已经安装了 PromiseKit

1
pod 'PromiseKit', '~> 6.0'

然后,你可以使用类似以下的代码来实现:

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
#import <PromiseKit/PromiseKit.h>

- (AnyPromise *)performRequestA {
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
[self performRequestAWithCompletion:^(id response, NSError *error) {
if (error) {
resolve(error);
} else {
resolve(response);
}
}];
}];
}

- (AnyPromise *)performRequestB {
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
[self performRequestBWithCompletion:^(id response, NSError *error) {
if (error) {
resolve(error);
} else {
resolve(response);
}
}];
}];
}

- (AnyPromise *)performRequestC {
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
[self performRequestCWithCompletion:^(id response, NSError *error) {
if (error) {
resolve(error);
} else {
resolve(response);
}
}];
}];
}

- (void)makeRequests {
[When(@[[self performRequestA], [self performRequestB]]) then:^id(id results) {
return [self performRequestC];
}].catch(^(NSError *error) {
// 处理错误
});
}

方法四:使用 Combine(适用于 iOS 13 及以上)

如果你在使用 iOS 13 及以上版本,可以使用 Combine 框架来处理异步任务。

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
47
48
49
50
51
52
53
54
55
import Combine

func performRequestA() -> Future<Data, Error> {
return Future { promise in
// 模拟网络请求A
self.performRequestA { (data, error) in
if let error = error {
promise(.failure(error))
} else {
promise(.success(data))
}
}
}
}

func performRequestB() -> Future<Data, Error> {
return Future { promise in
// 模拟网络请求B
self.performRequestB { (data, error) in
if let error = error {
promise(.failure(error))
} else {
promise(.success(data))
}
}
}
}

func performRequestC() -> Future<Data, Error> {
return Future { promise in
// 模拟网络请求C
self.performRequestC { (data, error) in
if let error = error {
promise(.failure(error))
} else {
promise(.success(data))
}
}
}
}

let cancellable = Publishers.Zip(performRequestA(), performRequestB())
.flatMap { _ in
return performRequestC()
}
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
print("Error: \(error)")
case .finished:
print("All requests finished successfully")
}
}, receiveValue: { data in
print("Received data: \(data)")
})

总结

  • GCD: 适用于需要轻量级并发管理的场景。
  • NSOperationQueue: 提供了更为面向对象的并发管理方式,适用于复杂任务依赖关系的场景。
  • PromiseKit: 提供了一种现代的链式异步处理方式,代码更为简洁。
  • Combine: 适用于 iOS 13 及以上版本,提供了响应式编程的方式来处理异步任务。

选择哪种方法取决于你的项目需求和个人偏好。希望这些示例能够帮助你实现网络请求 A 和 B 都回调后再请求 C 的需求。

8.可变数组 NSMutableArray,在头部插入元素,iOS 系统做了什么优化

在 iOS 系统中,NSMutableArray 是一个动态数组,它在插入和删除元素时会进行一些优化,但具体的优化细节是由底层实现决定的,这些实现细节通常是私有的,Apple 并未公开详细的内部实现。不过,我们可以推测和总结一些常见的优化策略,这些策略可能会在 NSMutableArray 中使用。

1. 内存预分配和扩展

NSMutableArray 会预先分配内存来容纳多个元素,以减少频繁的内存分配操作。当数组需要扩展容量时,它可能会一次性分配比当前需求更多的内存空间,以备将来使用。这种策略可以减少内存分配和释放的频率,提高性能。

2. 元素移动优化

在数组头部插入元素时,所有现有元素都需要向后移动一个位置。为了减少内存拷贝的开销,系统可能会使用高效的内存拷贝函数(如 memmove)来移动元素。memmove 函数在处理重叠内存区域时比 memcpy 更安全和高效。

3. 数据结构优化

尽管 NSMutableArray 主要使用动态数组来实现,但底层可能会结合使用其他数据结构(如链表)来优化特定操作的性能。例如,为了优化头部插入操作,系统可能会在特定情况下使用双向链表或者其他更适合插入操作的结构。

4. 缓存局部性

NSMutableArray 可能会利用缓存局部性来提高性能。通过将相邻的元素存储在接近的内存地址上,可以提高缓存命中率,从而提高内存访问速度。

5. 并发处理优化

虽然 NSMutableArray 本身不是线程安全的,但在一些多线程环境中,系统可能会使用锁、原子操作或者其他并发控制机制来优化并发访问的性能。

总的来说,虽然我们无法确切知道 NSMutableArray 的内部优化细节,但可以推测它会采用一些常见的优化策略来提升性能。了解这些优化策略有助于我们在开发过程中做出更好的性能优化决策。

9.Tagged Pointer

Tagged Pointer 的基本原理

Tagged Pointer 的基本原理是利用指针的高位位元来存储数据,而不是使用这些位元来存储内存地址。具体来说:

  • 在 64 位系统中,指针有 64 位,其中通常只有低 48 位用于存储内存地址,高 16 位未被使用。
  • Tagged Pointer 技术利用这些未使用的高位位元来存储数据。

如何识别 Tagged Pointer

在 Objective-C 中,Tagged Pointer 使用最高位(最高位为 1)来标识是否是 Tagged Pointer。例如:

  • 0x8000000000000000:最高位为 1,表示这是一个 Tagged Pointer
  • 0x0000000000000000:最高位为 0,表示这是一个普通的指针。

Tagged Pointer 的数据存储

Tagged Pointer 可以存储多种类型的数据,包括:

  • 小整数:直接在指针中存储整数值。
  • 小浮点数:直接在指针中存储浮点数值。
  • 小字符串:直接在指针中存储字符串数据。

10.NSString 存储在哪里,引用计数存储哪里?

1. 常量字符串

常量字符串是指在代码中直接使用的字符串字面量,例如:

1
NSString *str = @"Hello, World!";

这些字符串字面量通常会被存储在程序的只读数据段(Read-Only Data Segment)中。这意味着它们在编译时已经确定,并且在程序运行时不会被修改。它们的内存分配通常是由编译器在编译时完成的。

2. 动态字符串

动态字符串是指在程序运行时生成或拼接的字符串,例如:

1
2
NSString *str1 = [NSString stringWithFormat:@"Hello, %@", @"World"];
NSString *str2 = [str1 mutableCopy];

这些字符串是在程序运行时动态分配内存的。它们通常存储在堆(Heap)中,并由 NSStringNSMutableString 的内部实现来管理。

3. 内存管理

在 Objective-C 中,内存管理主要通过引用计数(Reference Counting)来完成。NSString 对象也遵循这一规则:

  • ARC(Automatic Reference Counting): 在使用 ARC 的情况下,编译器会自动插入内存管理代码,负责增加和减少引用计数。
  • 手动内存管理(MRC): 在不使用 ARC 的情况下,开发者需要手动管理内存,通过 retainreleaseautorelease 方法来管理引用计数。

4. 内部实现

NSString 是一个抽象类,其具体实现有多个子类,这些子类优化了不同类型的字符串存储和操作。常见的子类包括:

  • __NSCFConstantString: 用于存储常量字符串。
  • __NSCFString: 一般用于动态创建的不可变字符串。
  • NSMutableString: 用于可变字符串。

在实际运行时,NSString 对象的具体存储位置和方式可能会因为优化而有所不同。例如,短字符串可能会被存储在栈上,以提高性能。

5. 字符串缓存

为了提高性能,NSString 类可能会使用一些内部缓存机制。例如,常量字符串可能会被缓存,以避免重复分配内存。这种优化在底层是由 Core Foundation 框架实现的。

6. Foundation 框架与 Core Foundation

NSString 是 Foundation 框架的一部分,但其底层实现依赖于 Core Foundation 中的 CFStringCFString 提供了字符串操作的底层实现,而 NSString 则是在其基础上提供了面向对象的接口。

总结

  • 常量字符串: 存储在程序的只读数据段中。
  • 动态字符串: 在程序运行时存储在堆中。
  • 内存管理: 通过引用计数管理,无论是 ARC 还是 MRC。
  • 内部实现: 由多个子类优化不同类型的字符串,底层依赖于 Core Foundation 的 CFString

11.dealloc 流程

12.避免哈希碰撞的几种方法?

1. 更好的哈希函数

选择一个更好的哈希函数可以极大地减少哈希碰撞的可能性。一个好的哈希函数应具有以下特性:

  • 均匀性:能够将输入数据均匀地分布到哈希表的各个位置。
  • 确定性:相同的输入总是产生相同的哈希值。
  • 高效性:计算哈希值的时间复杂度应尽可能低。

常见的哈希函数包括:

  • MD5
  • SHA-1
  • SHA-256

在某些特定场景下,自定义的哈希函数也可能是一个有效的选择。

2. 增大哈希表的大小

通过增大哈希表的大小,可以减少哈希碰撞的概率。哈希表的大小通常是一个素数,这样可以更均匀地分布哈希值。

3. 链地址法(Separate Chaining)

链地址法是处理哈希碰撞的一种常见方法。每个哈希表的槽(bucket)都包含一个链表(或其他数据结构),用于存储具有相同哈希值的多个元素。

4. 开放定址法(Open Addressing)

开放定址法通过在发生碰撞时寻找下一个空闲槽来存储元素。常见的开放定址法包括线性探测、二次探测和双重散列。

线性探测

线性探测在发生碰撞时,按固定步长(通常为 1)依次探测下一个槽,直到找到空闲槽为止。

二次探测

二次探测在发生碰撞时,按二次方序列探测下一个槽。

1
index = (index + i^2) % self.size

双重散列

双重散列使用两个不同的哈希函数来计算探测序列。

1
index = (hash1(key) + i * hash2(key)) % self.size

5. 再哈希法(Rehashing)

再哈希法在发生碰撞时,使用另一个哈希函数重新计算哈希值,直到找到空闲槽为止。

6. 动态调整哈希表大小

在哈希表负载因子(Load Factor)达到一定阈值时,动态调整哈希表的大小。负载因子是已存储元素数量与哈希表大小的比值。当负载因子过高时,增大哈希表的大小并重新哈希所有元素。

13.mach-o 了解吗?存储的方法信息可以无用代码检测?段迁移?

Mach-O(Mach Object 文件格式)是 macOS 和 iOS 操作系统上使用的一种文件格式,用于可执行文件、目标代码、动态库、内核转储等。Mach-O 格式提供了一种灵活且强大的方式来描述程序的结构,支持多种架构(如 x86_64、arm64)和多种类型的文件(如可执行文件、动态库)。以下是关于 Mach-O 文件格式的详细介绍:

Mach-O 文件的基本结构

Mach-O 文件由多个部分组成,每个部分都有特定的用途和结构。主要部分包括:

  1. Header(头部):包含文件的基本信息,如文件类型、CPU 架构、加载命令数量等。
  2. Load Commands(加载命令):描述文件的各个部分如何加载到内存中。这些命令包括段的描述、动态库的引用、符号表的位置等。
  3. Segments(段):包含实际的代码和数据。每个段包含多个 section(节),每个节存储特定类型的数据,如代码节、数据节、符号表等。
  4. Sections(节):段的子部分,存储不同类型的数据,如可执行代码、只读数据、可写数据等。

详细结构

1. Header(头部)

头部是 Mach-O 文件的开始部分,包含文件的基本信息。头部的结构定义如下:

1
2
3
4
5
6
7
8
9
struct mach_header {
uint32_t magic; // 魔数,标识文件格式
cpu_type_t cputype; // CPU 类型
cpu_subtype_t cpusubtype;// CPU 子类型
uint32_t filetype; // 文件类型(如可执行文件、动态库)
uint32_t ncmds; // 加载命令的数量
uint32_t sizeofcmds; // 所有加载命令的总大小
uint32_t flags; // 标志位
};

2. Load Commands(加载命令)

加载命令描述文件的各个部分如何加载到内存中。常见的加载命令包括:

  • LC_SEGMENT:描述一个段。
  • LC_SYMTAB:描述符号表的位置。
  • LC_DYSYMTAB:描述动态符号表的位置。
  • LC_LOAD_DYLIB:描述需要加载的动态库。

每个加载命令的基本结构如下:

1
2
3
4
struct load_command {
uint32_t cmd; // 加载命令的类型
uint32_t cmdsize; // 加载命令的大小
};

3. Segments(段)

段是 Mach-O 文件中的主要数据部分,每个段包含多个节。常见的段包括:

  • __TEXT:包含可执行代码和只读数据。
  • __DATA:包含可写数据。
  • __LINKEDIT:包含符号表和其他链接信息。

段的基本结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct segment_command {
uint32_t cmd; // LC_SEGMENT
uint32_t cmdsize; // 加载命令的大小
char segname[16]; // 段的名称
uint32_t vmaddr; // 段在虚拟内存中的地址
uint32_t vmsize; // 段的大小
uint32_t fileoff; // 段在文件中的偏移
uint32_t filesize; // 段在文件中的大小
vm_prot_t maxprot; // 段的最大保护
vm_prot_t initprot; // 段的初始保护
uint32_t nsects; // 段中节的数量
uint32_t flags; // 段的标志
};

4. Sections(节)

节是段的子部分,存储具体类型的数据。每个节的基本结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct section {
char sectname[16]; // 节的名称
char segname[16]; // 段的名称
uint32_t addr; // 节在虚拟内存中的地址
uint32_t size; // 节的大小
uint32_t offset; // 节在文件中的偏移
uint32_t align; // 节的对齐
uint32_t reloff; // 重定位表的偏移
uint32_t nreloc; // 重定位表的数量
uint32_t flags; // 节的标志
uint32_t reserved1; // 保留字段
uint32_t reserved2; // 保留字段
};

Mach-O 文件类型

Mach-O 文件有多种类型,主要包括:

  • MH_EXECUTE:可执行文件。
  • MH_DYLIB:动态库。
  • MH_BUNDLE:可加载的代码包。
  • MH_OBJECT:目标文件。
  • MH_DYLINKER:动态链接器。

使用工具查看 Mach-O 文件

macOS 提供了一些工具来查看和分析 Mach-O 文件:

  • otool

    :用于显示 Mach-O 文件的头部、段、节、符号表等信息。

    1
    2
    3
    otool -hV MyApp.app/MyApp  # 显示头部信息
    otool -L MyApp.app/MyApp # 显示动态库依赖
    otool -tV MyApp.app/MyApp # 显示反汇编代码
  • nm

    :用于显示 Mach-O 文件中的符号表。

    1
    nm MyApp.app/MyApp
  • dyldinfo

    :用于显示动态链接相关信息。

    1
    dyldinfo -all MyApp.app/MyApp

总结

Mach-O 是 macOS 和 iOS 上使用的一种复杂而灵活的文件格式,用于描述可执行文件、动态库等。它通过头部、加载命令、段和节组织数据,支持多种文件类型和 CPU 架构。理解 Mach-O 文件格式有助于进行底层调试、性能优化和安全分析。macOS 提供了一些工具,如 otoolnm,可以用来查看和分析 Mach-O 文件的结构和内容。

14.脚本检测未使用代码原理

15.Render 渲染流程

iOS 渲染流程是一个复杂且高效的过程,它负责将应用的界面呈现到屏幕上。理解这个流程有助于优化应用的性能和用户体验。以下是 iOS 渲染流程的概要:

1. 应用层(Application Layer)

应用层是 iOS 应用的最上层,包括应用的逻辑、UI 视图控制器(UIViewController)、视图(UIView)等。开发者主要在这一层进行开发。

2. Core Animation

Core Animation 是 iOS 渲染的核心技术,它负责管理和执行所有的动画和视图层次结构(Layer Tree)。Core Animation 使用双缓冲技术来确保动画的流畅性。主要包括以下几个组成部分:

  • Layer Tree: 由 CALayer 对象组成的层次结构,每个 UIView 对应一个 CALayer
  • Display Tree: 由 Core Animation 生成的用于实际显示内容的树。
  • Render Tree: 用于渲染的最终树,由 Core Animation 发送给渲染服务器(Render Server)。

3. 渲染服务器(Render Server)

渲染服务器是一个独立的进程,负责接收来自 Core Animation 的 Render Tree,并将其转换为 GPU 可以理解的指令。渲染服务器会将这些指令发送给 GPU 进行实际的渲染。

4. GPU 渲染

GPU(图形处理单元)接收到渲染指令后,会执行以下操作:

  1. 顶点着色(Vertex Shading): 处理顶点数据,包括位置、颜色、纹理等信息。
  2. 图元组装(Primitive Assembly): 将顶点连接成图元(如三角形)。
  3. 光栅化(Rasterization): 将图元转换为片元(Pixel)。
  4. 片元着色(Fragment Shading): 计算每个片元的颜色和其他属性。
  5. 帧缓存操作(Frame Buffer Operations): 将最终的片元数据写入帧缓存(Frame Buffer)。

5. 显示层(Display Layer)

最终的帧缓存数据会被传递到显示层,由显示层将其显示在屏幕上。iOS 使用 VSync(垂直同步)信号来协调显示更新,以确保屏幕刷新和渲染同步,避免画面撕裂。

渲染流程的详细步骤

  1. 视图更新(View Update):
    • 应用程序修改视图或图层属性(如位置、大小、颜色等)。
    • UIView 的属性变化会触发 CALayer 的相应变化。
  2. 布局和显示(Layout and Display):
    • UIView 布局系统会重新计算视图的布局。
    • CALayer 会根据视图的变化更新自己的属性。
  3. Core Animation 动画处理(Core Animation Animation Handling):
    • Core Animation 会将动画应用到相关的 CALayer 上。
    • 根据动画时间轴生成 Display Tree。
  4. Render Tree 生成(Render Tree Generation):
    • Core Animation 从 Display Tree 生成 Render Tree。
    • Render Tree 包含了所有需要渲染的信息。
  5. Render Server 渲染(Render Server Rendering):
    • Core Animation 将 Render Tree 发送给渲染服务器。
    • 渲染服务器将 Render Tree 转换为 GPU 指令。
  6. GPU 渲染(GPU Rendering):
    • GPU 执行顶点着色、图元组装、光栅化、片元着色和帧缓存操作。
    • 渲染结果存储在帧缓存中。
  7. 显示更新(Display Update):
    • 帧缓存内容通过显示层显示在屏幕上。
    • VSync 信号确保显示更新和屏幕刷新同步。

性能优化

理解 iOS 渲染流程有助于优化应用性能,以下是一些常见的优化策略:

  1. 减少布局计算和视图层次结构的复杂度
  2. 使用合适的图层类型(如 CAShapeLayerCATextLayer
  3. 避免频繁的视图更新和动画
  4. 使用异步绘制技术(如 drawRect: 方法)
  5. 避免不必要的离屏渲染(Offscreen Rendering)

通过这些优化策略,可以减少 CPU 和 GPU 的负担,提高应用的流畅性和响应速度。

16.UIView 动画和 CAAnimation 动画有什么联系?

虽然 UIView 动画和 CAAnimation 动画在使用上有区别,但它们的底层机制是有联系的。UIView 动画实际上是对 CAAnimation 的封装和简化。当你使用 UIView 动画时,UIKit 会在内部创建相应的 CAAnimation 对象,并将其添加到视图的 CALayer 上。

示例:

1
2
3
UIView.animate(withDuration: 1.0, animations: {
myView.alpha = 0.0
})

上面的代码等价于:

1
2
3
4
5
6
let animation = CABasicAnimation(keyPath: "opacity")
animation.fromValue = 1.0
animation.toValue = 0.0
animation.duration = 1.0
myView.layer.add(animation, forKey: "opacityAnimation")
myView.layer.opacity = 0.0

结论

UIView 动画是对 CAAnimation 的封装,提供了更高层次的接口,使得简单动画的实现更加便捷,而 CAAnimation 提供了更强大的功能和更精细的控制,适合复杂的动画需求。在实际开发中,可以根据具体的动画需求选择合适的动画方式。

17.项目亮点

Your browser is out-of-date!

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

×