Objective-c 初阶——异常处理(try-catch)

一、@try/@catch/@throw/@finally 执行顺序

objectivec 复制代码
void doSomething() {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    @try {
        // 这一步抛异常
        [self riskyMethod]; 
    } @catch (NSException *e) {
        @throw;  // 把异常继续往上抛
    } @finally {
        // ❗ 注意:这里的 finally 会 **立刻执行**
        [pool release]; // 🔥🔥🔥 外层 pool 提前 pop 了
    }

    // ❌ 如果你此时还没处理完那个 exception,它里面的对象已经被释放了
}

1. riskyMethod 抛出异常
2. 进入 @catch 块
3. @catch 里调用了 @throw(重新抛出)
4. ❗ 根据编译器实现,@finally 会 **立刻执行**
    ⬅️ 在这个"再抛之前",@finally 就执行了
5. @finally 执行时,释放了 autorelease pool
6. ❌ 异常对象(NSException 实例)可能在 autorelease pool 中,它被提前释放了
7. 异常还没被真正交到上层 catch,就已经是 zombie 了

二、try-catch 内存管理(坑)

1、普通内存泄漏

1.1. 如何泄漏

objectivec 复制代码
- (void)doSomething {
    NSMutableArray *anArray = [[NSMutableArray alloc] initWithCapacity:0];
    @try {
        [self doSomethingElse:anArray]; // 假设一定会抛错  
    } @catch(...) {
        @throw;
    }
    [anArray release];
}

就像这种情况,我在 catch 里又重新抛异常,那么就会导致 [anArray release]; 就不会被执行了,所以 anArray 就会内存泄漏了。

1.2. 解决方案

objectivec 复制代码
- (void)doSomething {
    NSMutableArray *anArray = [[NSMutableArray alloc] initWithCapacity:0];
    @try {
        [self doSomethingElse:anArray]; // 假设一定会抛错  
    } @catch(...) {
        @throw;
    } @finally {
        [anArray release];
    }
}

2、提前释放 auto release pool 引起的内存泄漏

2.1. 如何泄漏

objectivec 复制代码
- (void)doSomething {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    NSException *ex = [NSException exceptionWithName:@"MyError" reason:nil userInfo:nil]; // 创建 NSException,retain 一次,retainCount + 1
    @throw ex; // 由 runtime retain 1 次,retainCount + 2

    [pool release]; // 不会执行
}

@try {
    [someObject doSomething]; // 抛出 ex
} @catch (NSException *e) {
    @throw; // 再次抛出 ex,但这是 rethrow, retainCount 不会加 1
} @finally {
    [outerPool release]; // 释放 ex 所在 autorelease pool,retainCount - 1;
                         // 因为 ex 是通过 unwind 找到当前栈帧的,且当前栈帧有 @finally,
                         // 所以会调一次 runtime cleanup 来清理对象,retainCount - 1;
                         // 此时 ex 的 retainCount 为 0,ex 被 dealloc
}

因为 doSomething: 函数内部创建了一个 auto release pool;而且外层又有一个 auto release pool,所以这两个 auto release pool 是这样的:

然后 ex 的引用计数看注释就可以了。顺带提一下,只要是被 unwind 到当前栈帧且当前栈帧有 @finally,系统就会在 finally 块里调 cleanup 函数,给当前所有对象的 retain count 减 1.

那什么时候会被 unwind 呢?其实就是当异常对象被 throw 或当前函数栈帧无法捕获异常时,系统就会调unwind 来一个个函数栈帧往上找。只要当前栈帧(没有 throw 的栈帧)的 catch 能捕获到异常且 catch 不再重新抛出异常,unwind 就不会继续被调。

2.2. 解决方案

objectivec 复制代码
- (void)doSomething {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    NSException *ex = [NSException exceptionWithName:@"MyError" reason:nil userInfo:nil]; 
    @throw ex; 

    [pool release]; // 不会执行
}

@try {
    [someObject doSomething]; // 抛出 ex
} @catch (NSException *e) {
    [e retain];
    @throw; 
} @finally {
    [outerPool release]; 
}

是的,就是给 e 的 retainCount 多加 1 就可以了。这样就可以给整个异常对象的引用计数加 1,不让他被 dealloc 掉。

三、性能对比:32 位的 try-catch VS 64 位的 try-catch

先提前下个结论:根据苹果开发者文档,其实 32 位跟 64 位的 try-catch 各有利弊。因为苹果针对 64 位情况重新实现了 objective-c 的异常处理机制,也就是说采用了 cpp 的 zero-cost 的异常处理机制;因此,32 位的 try-catch 在 try 的部份性能比 64 位的 try 部份差;而 64 位的 throw 部份会比 32 位的 throw 部份差。下面我会分别讲这两部分性能差异的原理。不过先叠个甲,里面的伪代码实现都是用于逻辑自洽的,语法上难免会有错误。

1、32 位下 objective-c 的异常处理机制

其实在 32 位下 objective-c 的异常处理机制是这样的:每进入一个 @try 块,都会先创建一个异常栈帧(不是函数的栈帧),把当前 @try 代码块的地址写到 jmp_buf env 里,然后再调 setjmp 函数来标记 longjmp() 应该跳到的地方。

对于每一个 throw 函数,都会调 longjmp() 函数,然后让程序的 pc 指针指向当前异常栈帧的 setjmp(frame.env) 处。因为 setjmp(frame.env) 此时返回值不是 0,所以跳到 catch 执行......然后一直沿着 try 的调用链往上找 catch。

cpp 复制代码
typedef struct ExceptionFrame {
    jmp_buf env;
    struct ExceptionFrame *prev;
    const char *handledType; // 这个 try 块能处理的异常类型
    Exception *caughtException; // 用于传值给 catch
} ExceptionFrame;

__thread ExceptionFrame *topFrame = NULL; // 每个线程一份
cpp 复制代码
// try 的伪代码实现
#define TRY(type) do { \
    ExceptionFrame frame; \
    frame.prev = topFrame; \
    frame.handledType = type; \
    frame.caughtException = NULL; \
    topFrame = &frame; \
    if (setjmp(frame.env) == 0)

#define CATCH(x) else for (Exception *x = frame.caughtException; x != NULL; x = NULL)

#define END_TRY \
    topFrame = frame.prev; \
} while (0)

// throw 的伪代码实现
#define THROW(e) do { \
    currentException = e; \
    ExceptionFrame *frame = topFrame; \
    while (frame) { \
        if (strcmp(frame->handledType, e->type) == 0 || strcmp(frame->handledType, "*") == 0) { \
            frame->caughtException = e; \
            topFrame = frame->prev; \
            longjmp(frame->env, 1); \
        } \
        frame = frame->prev; \
    } \
    fprintf(stderr, "Uncaught exception: %s\n", e->message); \
    abort(); \
} while (0)
cpp 复制代码
TRY {
    // 这里写 try 块的内容
    maybeDangerousFunction();
    THROW(&someException);
} CATCH(e) {

} END_TRY;

2、64 位下 objective-c 的异常处理机制

但是在 64 位下 objective-c 的异常处理用的是 zero-cost 那套,也就是每进入一个 try 块时并不会创建一个 exception frame。但一旦遇到 throw 时,就会从当前函数栈帧开始,找每个栈帧的匹配的 catch 块,找不到就再去上一个函数栈帧找。

cpp 复制代码
struct StackFrame {
    StackFrame *prev;          // 栈上的上一个帧
    FunctionMeta *functionMeta; // 指向 LSDA + handler 信息
    void *pc;                  // 当前执行地址 (程序计数器)
};

struct Exception {
    const char *type;
    const char *message;
};

typedef struct {
    char *type;                   // 异常类型
    void (*handler)(Exception*);  // catch handler 跳转landing pad
    void (*cleanup)(void);        // cleanup handler,例如 finally
    int hasCleanup;               // 是否有 cleanup
} CatchClause;

struct FunctionMeta {
    CatchClause *clauses; // 存 catch 代码块地址的数组,一个 catch 对应一个 clause
    int clauseCount;
    void (*personality)(StackFrame *, Exception *);
};
cpp 复制代码
// 默认的 personality 函数,带 cleanup 处理
void defaultPersonality(StackFrame *frame, Exception *e) {
    int handled = 0; // 该层栈帧的 catch 能否捕获异常,可以为 1,不可以为 0

    // 先查找 catch clause
    for (int i = 0; i < frame->functionMeta->clauseCount; ++i) {
        CatchClause *clause = &frame->functionMeta->clauses[i];
        
        if (strcmp(clause->type, e->type) == 0) {
            // 找到匹配的 catch
            clause->handler(e);  // 执行 catch handler(landing pad)
            handled = 1;
            break;
        }
    }

    // 调用 cleanup handler(无论 catch 是否找到都会调用 cleanup)
    for (int i = 0; i < frame->functionMeta->clauseCount; ++i) {
        CatchClause *clause = &frame->functionMeta->clauses[i];
        if (clause->hasCleanup && clause->cleanup != nullptr) {
            clause->cleanup(); // 执行 cleanup,比如 finally
        }
    }

    if (!handled) {
        // 当前栈帧不能处理异常,继续向上 unwind
        unwind(frame->prev, e);
    }
}

// throw 异常时当前栈帧直接调 unwind,可以看成 defaultPersonality 是私有的
void unwind(StackFrame *frame, Exception *e) {
    if (!frame) {
        printf("Uncaught exception: %s\n", e->message);
        abort();
    }
    frame->functionMeta->personality(frame, e);
}

unwind 执行什么呢?就是先看看当前栈帧有没有符合类型的 catch 可以捕获异常;然后在看看当前栈帧有没有 finally,有的话就调 cleanup();然后如果当前栈帧可以捕获异常,不继续调 unwind ,否则继续调上一个栈帧的 unwind.

3、性能差异原因

所以 32 位下 oc 的 try 比 64 的 try 性能消耗更大是因为 32 位下 oc 会给每一个 try 块创建一个叫 jmp_buf env 的东西,而且还会调一次 setjmp() ;但 64 位对每一个 try 块并不会做任何事,所以 32 位下 try 块会比 64 位需要更多的内存。

那 64 位的 throw 比 32 位的 throw 性能更差是因为 32 位下用的是 exception frame + longjmp() 来跳转,每一跳肯定可以跳到最近有 catch 的函数栈帧中,所以 32 位找对应 catch 所花的时间是比 64 位一个一个栈帧找要快的。

相关推荐
用户092 小时前
SwiftUI Charts 函数绘图完全指南
ios·swiftui·swift
YungFan2 小时前
iOS26适配指南之UIColor
ios·swift
权咚19 小时前
阿权的开发经验小集
git·ios·xcode
用户0919 小时前
TipKit与CloudKit同步完全指南
ios·swift
法的空间1 天前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
侃侃_天下1 天前
最终的信号类
开发语言·c++·算法
echoarts1 天前
Rayon Rust中的数据并行库入门教程
开发语言·其他·算法·rust
2501_915918411 天前
iOS 上架全流程指南 iOS 应用发布步骤、App Store 上架流程、uni-app 打包上传 ipa 与审核实战经验分享
android·ios·小程序·uni-app·cocoa·iphone·webview
Aomnitrix1 天前
知识管理新范式——cpolar+Wiki.js打造企业级分布式知识库
开发语言·javascript·分布式
每天回答3个问题1 天前
UE5C++编译遇到MSB3073
开发语言·c++·ue5