MST

星途 面试题库

面试题:Objective-C 中 block 的实现原理及应用场景

请简述 Objective-C 中 block 的实现原理,包括其数据结构以及内存管理机制。同时,举例说明 block 在实际开发中的三个常见应用场景,并阐述为什么在这些场景中使用 block 会是一个好的选择。
17.8万 热度难度
编程语言Objective-C

知识考点

AI 面试

面试题答案

一键面试

Objective-C 中 block 的实现原理

  1. 数据结构
    • Block本质上也是一个对象,它的结构体定义类似如下:
    struct Block_descriptor {
        unsigned long int reserved;
        unsigned long int size;
        // 指向 copy 函数的指针
        void (*copy)(void *dst, void *src);
        // 指向 dispose 函数的指针
        void (*dispose)(void *);
    };
    
    struct Block_layout {
        void *isa;
        int flags;
        int reserved;
        void (*invoke)(void *, ...);
        struct Block_descriptor *descriptor;
        // 捕获的变量
        /* 如果有捕获变量,这里会包含捕获的变量 */
    };
    
    • isa指针表明block是一个对象,invoke是执行block代码的函数指针,descriptor指向描述block属性(如大小、copydispose函数)的结构体。
  2. 内存管理机制
    • 栈上的 block:当block在函数内部定义时,它初始是在栈上的。栈上的block生命周期与函数栈帧相同,函数结束时,栈上的block会被销毁。
    • 堆上的 block:通过copy操作,可以将栈上的block复制到堆上。常见的触发block从栈到堆复制的情况有:将block作为函数返回值、将block赋值给__strong修饰的变量、将block作为Cocoa框架方法的参数且该方法对block有持有语义(如GCD相关函数)。堆上的block需要手动管理内存,当引用计数为0时会被释放。
    • 全局的 block:当block不捕获任何外部变量时,它会被存储在全局数据区,类似全局变量,生命周期贯穿程序始终,不需要进行额外的内存管理。

block 在实际开发中的常见应用场景

  1. 作为回调函数
    • 示例:在网络请求中,当请求完成时,通过block返回结果。
    typedef void(^CompletionBlock)(NSData *data, NSError *error);
    
    - (void)fetchDataWithCompletion:(CompletionBlock)completion {
        NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@"https://example.com"] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            if (completion) {
                completion(data, error);
            }
        }];
        [task resume];
    }
    
    • 优势:相比传统的委托模式(delegate),使用block代码更加紧凑,不需要额外定义协议和实现多个代理方法。可以将回调逻辑与请求逻辑写在同一处,增强了代码的可读性和维护性。
  2. 集合遍历
    • 示例:使用block遍历数组。
    NSArray *numbers = @[@1, @2, @3];
    [numbers enumerateObjectsUsingBlock:^(NSNumber * _Nonnull number, NSUInteger idx, BOOL * _Nonnull stop) {
        NSLog(@"%@", number);
        if (idx == 1) {
            *stop = YES;
        }
    }];
    
    • 优势:简洁明了,直接在遍历的地方定义处理逻辑,不需要单独定义一个遍历方法。而且block可以方便地捕获外部变量,方便在遍历过程中进行复杂的操作。
  3. GCD(Grand Central Dispatch)
    • 示例:在后台线程执行任务,并在主线程更新 UI。
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        // 耗时操作
        NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://example.com"]];
        dispatch_async(dispatch_get_main_queue(), ^{
            // 更新 UI
            UIImage *image = [UIImage imageWithData:data];
            self.imageView.image = image;
        });
    });
    
    • 优势blockGCD紧密结合,能够方便地将任务提交到不同的队列执行。通过block可以简洁地定义任务逻辑,并且易于管理不同任务之间的依赖关系。