MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C 在 iOS 应用调试与错误排查中的技巧

2023-10-211.6k 阅读

一、NSLog 与输出调试信息

在 Objective-C 开发 iOS 应用时,NSLog 是最基础且常用的调试手段。它可以将信息输出到 Xcode 的控制台,方便开发者了解程序的执行流程和变量的值。

1.1 基本使用

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSString *message = @"Hello, Debugging!";
        NSLog(@"%@", message);
    }
    return 0;
}

在上述代码中,通过 NSLogmessage 字符串输出到控制台。NSLog 支持格式化输出,与 C 语言的 printf 类似。例如,可以输出变量的类型和值:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSInteger number = 42;
        NSLog(@"The number is of type %@ and value %ld", NSStringFromClass([number class]), (long)number);
    }
    return 0;
}

这里使用 NSStringFromClass 获取变量的类名,然后通过格式化输出展示变量的类型和值。

1.2 带时间戳和函数名的输出

为了更方便地追踪调试信息,我们可以在 NSLog 中添加时间戳和函数名。

#define DEBUG_LOG(format, ...) NSLog(@"[%@ %s] " format, [NSDate date], __func__, ##__VA_ARGS__)

然后在代码中使用 DEBUG_LOG 宏:

#import <Foundation/Foundation.h>

#define DEBUG_LOG(format, ...) NSLog(@"[%@ %s] " format, [NSDate date], __func__, ##__VA_ARGS__)

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        DEBUG_LOG(@"This is a debug log");
    }
    return 0;
}

这样输出的日志会带有当前时间和调用函数的名称,有助于定位问题所在的具体位置和时间点。

二、断点调试技巧

断点调试是深入了解程序执行流程和排查错误的重要手段。

2.1 普通断点

在 Xcode 中,只需在代码行号处单击即可设置普通断点。当程序运行到该断点时,会暂停执行,此时可以查看变量的值、调用栈等信息。

例如,有如下代码:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSInteger num1 = 10;
        NSInteger num2 = 20;
        NSInteger result = num1 + num2;
        NSLog(@"The result is %ld", (long)result);
    }
    return 0;
}

NSInteger result = num1 + num2; 这一行设置断点,运行程序后,程序会停在此处。此时在 Xcode 的调试区域,可以看到 num1num2 的值,并且可以单步执行代码,查看 result 的计算过程。

2.2 条件断点

条件断点允许在满足特定条件时才暂停程序。比如,我们有一个循环,只想在循环变量达到某个值时暂停。

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        for (NSInteger i = 0; i < 100; i++) {
            if (i % 10 == 0) {
                NSLog(@"The number is %ld", (long)i);
            }
        }
    }
    return 0;
}

要在 i 等于 50 时暂停,可以在设置断点后,右键点击断点,选择“Edit Breakpoint”。在弹出的窗口中,输入条件 i == 50。这样,程序运行到 i 等于 50 时才会暂停,避免在大量循环中逐个检查。

2.3 符号断点

符号断点用于在特定函数被调用时暂停程序。例如,我们想在 NSLog 函数被调用时暂停,以便查看所有的日志输出情况。在 Xcode 的断点导航器中,点击“+”号,选择“Symbolic Breakpoint”。在“Symbol”栏中输入 NSLog,这样每次调用 NSLog 时,程序都会暂停。

三、异常捕获与错误处理

在 Objective-C 中,异常捕获和错误处理是确保应用稳定运行的关键。

3.1 @try - @catch - @finally

@try - @catch - @finally 块用于捕获异常。例如,当我们进行数组越界访问时:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSArray *array = @[@"One", @"Two"];
        @try {
            NSString *element = array[2];
            NSLog(@"%@", element);
        } @catch (NSException *exception) {
            NSLog(@"Caught exception: %@", exception.reason);
        } @finally {
            NSLog(@"This will always be printed");
        }
    }
    return 0;
}

@try 块中进行可能会引发异常的操作,这里尝试访问 array 越界的元素。如果发生异常,@catch 块会捕获到 NSException 对象,通过 exception.reason 可以获取异常的原因。@finally 块中的代码无论是否发生异常都会执行。

3.2 NSError 与错误处理

在很多 Objective-C 方法中,通过 NSError 指针来传递错误信息。例如,读取文件时可能会遇到错误:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSString *filePath = @"/nonexistentfile.txt";
        NSError *error;
        NSString *content = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:&error];
        if (!content) {
            NSLog(@"Error reading file: %@", error.localizedDescription);
        }
    }
    return 0;
}

在调用 stringWithContentsOfFile:encoding:error: 方法时,将 NSError 指针作为参数传入。如果方法执行失败,error 会被填充相应的错误信息。通过 error.localizedDescription 可以获取本地化的错误描述,方便开发者了解错误详情。

四、内存调试

内存问题是 iOS 应用开发中常见的错误来源,如内存泄漏、悬空指针等。

4.1 Instruments 工具

Xcode 提供的 Instruments 工具是内存调试的强大助手。其中,Leaks 模板可以检测内存泄漏。

例如,假设有如下代码可能存在内存泄漏:

#import <Foundation/Foundation.h>

@interface MemoryLeakClass : NSObject
@end

@implementation MemoryLeakClass
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        while (1) {
            MemoryLeakClass *obj = [[MemoryLeakClass alloc] init];
        }
    }
    return 0;
}

运行程序并使用 Instruments 的 Leaks 模板,它会检测到 MemoryLeakClass 对象没有被正确释放,从而定位到内存泄漏的位置。

4.2 自动引用计数(ARC)与手动内存管理

ARC 极大地简化了 Objective-C 的内存管理。在 ARC 环境下,编译器会自动插入 retainreleaseautorelease 等内存管理方法。

例如,在非 ARC 环境下,需要手动管理对象的内存:

#import <Foundation/Foundation.h>

@interface ManualMemoryClass : NSObject
@end

@implementation ManualMemoryClass
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ManualMemoryClass *obj = [[ManualMemoryClass alloc] init];
        [obj release];
    }
    return 0;
}

而在 ARC 环境下,无需手动调用 release,编译器会自动处理:

#import <Foundation/Foundation.h>

@interface ARCMemoryClass : NSObject
@end

@implementation ARCMemoryClass
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ARCMemoryClass *obj = [[ARCMemoryClass alloc] init];
    }
    return 0;
}

但即使在 ARC 下,也可能存在循环引用导致的内存泄漏。比如两个对象相互持有:

#import <Foundation/Foundation.h>

@interface ClassA;
@interface ClassB;

@interface ClassA : NSObject
@property (nonatomic, strong) ClassB *classB;
@end

@interface ClassB : NSObject
@property (nonatomic, strong) ClassA *classA;
@end

@implementation ClassA
@end

@implementation ClassB
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ClassA *a = [[ClassA alloc] init];
        ClassB *b = [[ClassB alloc] init];
        a.classB = b;
        b.classA = a;
    }
    return 0;
}

在这种情况下,ab 相互持有,导致无法释放,形成内存泄漏。可以通过将其中一个属性改为 weak 来解决:

#import <Foundation/Foundation.h>

@interface ClassA;
@interface ClassB;

@interface ClassA : NSObject
@property (nonatomic, strong) ClassB *classB;
@end

@interface ClassB : NSObject
@property (nonatomic, weak) ClassA *classA;
@end

@implementation ClassA
@end

@implementation ClassB
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ClassA *a = [[ClassA alloc] init];
        ClassB *b = [[ClassB alloc] init];
        a.classB = b;
        b.classA = a;
    }
    return 0;
}

这样,当 a 释放时,ba 的引用不会阻止 a 的释放,从而避免了循环引用导致的内存泄漏。

五、网络调试

在 iOS 应用中,网络请求是常见功能,网络调试对于排查网络相关问题至关重要。

5.1 使用 NSURLSession 进行调试

NSURLSession 是 iOS 中用于网络请求的重要类。我们可以通过设置代理来获取详细的网络请求和响应信息。

#import <Foundation/Foundation.h>

@interface NetworkDebugger : NSObject <NSURLSessionDataDelegate>
@end

@implementation NetworkDebugger

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
    NSLog(@"Received response: %@", response);
    completionHandler(NSURLSessionResponseAllow);
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    NSString *responseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"Received data: %@", responseString);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NetworkDebugger *debugger = [[NetworkDebugger alloc] init];
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
        NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:debugger delegateQueue:nil];
        NSURL *url = [NSURL URLWithString:@"https://www.example.com"];
        NSURLSessionDataTask *task = [session dataTaskWithURL:url];
        [task resume];

        // 模拟程序运行一段时间
        [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];
    }
    return 0;
}

在上述代码中,NetworkDebugger 类实现了 NSURLSessionDataDelegate 协议的方法。didReceiveResponse: 方法在接收到服务器响应时被调用,didReceiveData: 方法在接收到数据时被调用。通过在这些方法中添加日志输出,可以详细了解网络请求的响应和数据接收情况。

5.2 Charles 代理工具

Charles 是一款强大的网络代理工具,可以拦截和分析 iOS 应用的网络请求。首先,需要在 iOS 设备上配置 Charles 的代理。然后,在 Objective-C 代码中,确保网络请求通过代理进行。

例如,使用 NSURLSession 时,可以设置代理:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
        config.connectionProxyDictionary = @{
            @"HTTPEnable" : @(1),
            @"HTTPProxy" : @"192.168.1.100", // Charles 代理服务器 IP
            @"HTTPPort" : @(8888) // Charles 代理服务器端口
        };
        NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
        NSURL *url = [NSURL URLWithString:@"https://www.example.com"];
        NSURLSessionDataTask *task = [session dataTaskWithURL:url];
        [task resume];

        // 模拟程序运行一段时间
        [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];
    }
    return 0;
}

这样,通过 Charles 可以查看应用发出的所有网络请求,包括请求头、请求体、响应头和响应体等信息,有助于排查网络请求参数错误、服务器响应异常等问题。

六、视图调试

在 iOS 应用开发中,视图相关的问题也较为常见,如视图布局错误、视图层级混乱等。

6.1 实时视图调试

Xcode 提供了实时视图调试功能。在应用运行时,点击 Xcode 调试栏中的“Debug View Hierarchy”按钮,即可查看应用当前的视图层级结构。

例如,有一个简单的视图控制器:

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    UIView *containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height)];
    containerView.backgroundColor = [UIColor whiteColor];
    [self.view addSubview:containerView];

    UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 50)];
    label.text = @"Hello, View Debugging";
    [containerView addSubview:label];
}

@end

通过实时视图调试,可以清晰地看到 containerViewlabel 的层级关系,以及它们的位置和大小等属性。如果布局出现问题,可以直观地发现并进行调整。

6.2 日志输出视图属性

除了实时视图调试,还可以通过日志输出视图的属性来排查问题。例如,查看视图的框架、透明度等:

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 200, 200)];
    view.backgroundColor = [UIColor redColor];
    view.alpha = 0.5;
    [self.view addSubview:view];

    NSLog(@"View frame: %@, Alpha: %f", NSStringFromCGRect(view.frame), view.alpha);
}

@end

通过日志输出的视图框架和透明度等信息,可以判断视图是否按照预期进行了设置。如果视图显示异常,可以通过这些信息排查是布局问题还是属性设置问题。

七、性能调试

性能问题会影响用户体验,在 Objective-C 开发 iOS 应用时,需要关注性能调试。

7.1 Time Profiler

Xcode 的 Instruments 中的 Time Profiler 模板可以分析应用的性能瓶颈。它可以展示每个函数的执行时间和调用次数。

例如,有一个包含复杂计算的函数:

#import <Foundation/Foundation.h>

long long complexCalculation() {
    long long result = 0;
    for (long long i = 0; i < 1000000000; i++) {
        result += i;
    }
    return result;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        for (int j = 0; j < 10; j++) {
            long long result = complexCalculation();
            NSLog(@"Result: %lld", result);
        }
    }
    return 0;
}

使用 Time Profiler 运行该程序,可以看到 complexCalculation 函数占用了大量的执行时间,从而定位到性能瓶颈,进而可以考虑优化算法,如使用等差数列求和公式来替代循环计算。

7.2 优化内存使用与性能

除了函数执行时间,内存使用也会影响性能。例如,避免在循环中频繁创建大量对象。

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 不好的做法,在循环中频繁创建对象
        for (NSInteger i = 0; i < 10000; i++) {
            NSString *tempString = [NSString stringWithFormat:@"%ld", (long)i];
        }

        // 优化做法,提前创建一个可变字符串
        NSMutableString *mutableString = [NSMutableString string];
        for (NSInteger i = 0; i < 10000; i++) {
            [mutableString appendFormat:@"%ld", (long)i];
        }
    }
    return 0;
}

通过合理优化内存使用,可以减少内存分配和释放的开销,提高应用的性能。同时,结合 Instruments 的 Memory Profiler 可以更好地分析内存使用情况,找出内存使用不合理的地方并进行优化。

八、多线程调试

在 iOS 应用中,多线程编程可以提高应用的响应性,但也带来了调试的复杂性,如线程同步问题、死锁等。

8.1 线程同步问题排查

当多个线程访问共享资源时,可能会出现数据竞争问题。例如,多个线程同时对一个共享变量进行读写:

#import <Foundation/Foundation.h>

@interface ThreadSafeClass : NSObject
@property (nonatomic, assign) NSInteger sharedValue;
@end

@implementation ThreadSafeClass
@end

void threadFunction(ThreadSafeClass *obj) {
    for (NSInteger i = 0; i < 1000; i++) {
        obj.sharedValue++;
    }
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ThreadSafeClass *obj = [[ThreadSafeClass alloc] init];
        NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(threadFunction:) object:obj];
        NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(threadFunction:) object:obj];

        [thread1 start];
        [thread2 start];

        // 等待线程执行完毕
        [thread1 join];
        [thread2 join];

        NSLog(@"Final value: %ld", (long)obj.sharedValue);
    }
    return 0;
}

由于没有进行线程同步,obj.sharedValue 的最终值可能并非预期的 2000。为了解决这个问题,可以使用锁机制,如 NSLock

#import <Foundation/Foundation.h>

@interface ThreadSafeClass : NSObject
@property (nonatomic, assign) NSInteger sharedValue;
@property (nonatomic, strong) NSLock *lock;
@end

@implementation ThreadSafeClass
@end

void threadFunction(ThreadSafeClass *obj) {
    for (NSInteger i = 0; i < 1000; i++) {
        [obj.lock lock];
        obj.sharedValue++;
        [obj.lock unlock];
    }
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ThreadSafeClass *obj = [[ThreadSafeClass alloc] init];
        obj.lock = [[NSLock alloc] init];
        NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(threadFunction:) object:obj];
        NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(threadFunction:) object:obj];

        [thread1 start];
        [thread2 start];

        // 等待线程执行完毕
        [thread1 join];
        [thread2 join];

        NSLog(@"Final value: %ld", (long)obj.sharedValue);
    }
    return 0;
}

通过在对共享资源访问前后加锁和解锁,确保了线程安全,obj.sharedValue 的最终值将是预期的 2000。

8.2 死锁调试

死锁是多线程编程中常见的严重问题。例如,两个线程相互等待对方释放锁:

#import <Foundation/Foundation.h>

@interface DeadlockClass : NSObject
@property (nonatomic, strong) NSLock *lock1;
@property (nonatomic, strong) NSLock *lock2;
@end

@implementation DeadlockClass
@end

void thread1Function(DeadlockClass *obj) {
    [obj.lock1 lock];
    NSLog(@"Thread 1 locked lock1");
    [NSThread sleepForTimeInterval:1];
    [obj.lock2 lock];
    NSLog(@"Thread 1 locked lock2");
    [obj.lock2 unlock];
    [obj.lock1 unlock];
}

void thread2Function(DeadlockClass *obj) {
    [obj.lock2 lock];
    NSLog(@"Thread 2 locked lock2");
    [NSThread sleepForTimeInterval:1];
    [obj.lock1 lock];
    NSLog(@"Thread 2 locked lock1");
    [obj.lock1 unlock];
    [obj.lock2 unlock];
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        DeadlockClass *obj = [[DeadlockClass alloc] init];
        obj.lock1 = [[NSLock alloc] init];
        obj.lock2 = [[NSLock alloc] init];

        NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(thread1Function:) object:obj];
        NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(thread2Function:) object:obj];

        [thread1 start];
        [thread2 start];

        // 等待线程执行完毕(但会陷入死锁)
        [thread1 join];
        [thread2 join];
    }
    return 0;
}

在这个例子中,thread1 先获取 lock1,然后尝试获取 lock2,而 thread2 先获取 lock2,再尝试获取 lock1,从而导致死锁。调试死锁时,可以使用 Instruments 的 Deadlocks 模板。运行应用并触发死锁场景,Instruments 会检测到死锁,并指出死锁发生的位置,即哪些线程持有哪些锁,以及等待获取哪些锁,帮助开发者找出死锁原因并进行修复。

通过以上多种调试技巧和方法,开发者可以更高效地排查 Objective - C 编写的 iOS 应用中的各种错误,提高应用的质量和稳定性。无论是从基础的日志输出到复杂的多线程调试,每个环节都需要开发者仔细对待,熟练掌握这些技巧将有助于打造优秀的 iOS 应用。