Xcode Debug

基于LLDB我们可以在运行期间对App进行Debug,除此之外Xcode也提供了一些设置,使得App在编译期间和运行期间独立LLDB之外方便发现问题。

问题

一个常见的错误就是,对一个指向已经释放过的对象的指针-即指向无效内存的野指针-进行操作。语义上讲,一个指针在其指向的对象无效时就应该标记为无效,比如指向nil,就算程序中不再使用该指针虽然不会出错,但还是存在着未来变化的风险。指向同一个对象地址的指针可以有很多个,怎么才能确保所有这些指针的安全呢?ARC的weak指针能够解决这一问题。同时,strong指针在试用期间就能确保指向的对象至少存在,所以也不会出错。而等效于MRC时代指针的unsafe_unretained指针就非常容易出错,因为它即不涉及内存管理,也不被安全置nil。那么,autoreleasing指针呢?

{
    __autoreleasing NSArray *test = nil;
    @autoreleasepool {
        test = @[@"A", @"B", @"C"];
    }
    [test count];
}

会发生EXC_BAD_ACCESS错误。test在离开{}后其注册在AutoreleasePool的对象被释放,但是test还是指向原地址没有被安全处理。所以,unsafe_unretained和autoreleasing指针都不是安全的。

为什么有时候已经释放过的对象,使用指向它的指针不会粗错呢?这是因为,Apple对回收对象内存这个行为不一定按照字面理解那样。最简单的就是更改heap的统计信息,而没有将内存全部置为0。如果在使用野指针时,其指向的内存还跟对象释放前一样的内容,那么这个时候是可以看起来正常运行的。

{
    NSView *view = [[NSView alloc] init];
    [view release];
    [[[NSWindow alloc] init] autorelease];
    [view description];
}

这是个不易发现但又确实存在的问题。

Static Analyze

静态分析指的是一种在不执行程序的情况下对程序代码进行分析的行为,对应着Scheme的Analyze(快捷键Shift+Command+B)。静态分析主要通过分析语法来发现这几种问题:

  • 逻辑错误:访问未初始化的变量等。
  • 声明错误:从未使用过的变量等。
  • 内存错误:内存泄漏或错误释放等。

声称变量并初始化是一个良好又安全的习惯,访问为初始化的变量将会带来出乎意料的行为。

对于没有使用的变量,虽然不会引起程序出错,编译优化时也可能直接省略掉,但是这些信息是可以在源代码中去掉的,这可以提高代码简洁度。

ARC不代表所有内存操作都没有问题,对于使用了不属于ARC管理范围的内存操作就可能出现问题。

MRC相比于ARC出错的可能性要多得多,所以MRC时使用Analyze比ARC时更有必要。像之前内存回收的问题,在Analyze的帮助下可以在运行之前就能发现。

这些问题在编译期间很难被发现,但是通过Analyze就变得容易。总的来说,虽然Analyze不能发现并解决所有的问题,但是从预防角度来讲,Analyze是成本最低的改成代码错误的方法。阶段性地在Run前Analyze一下是个好习惯。可以在Build Settings里也可以通过设置,让Xcode在Build时也执行Analyze及执行程度。

NSZombieEnabled

我在Runtime应用里介绍过NSZombieEnabled,这能够让对象伪释放,动态修改对象的类型信息,利用转发消息机制将发送给伪释放对象的消息传递到特定类型,打印出来以供追踪。Xcode的Scheme里面,可以在Argument的Environment Variable里设置:

也可以在Diagnostics里设置:

不过需要注意的是,因为Zombie对象并没有真正地释放内存,所以适合在发现问题后设置并重跑直面问题,否则长时间运行会产生大量不必要也消不掉的内存。

Memory Management-Malloc

对于语义上已经释放了、但是内存内容没有改变,且有时Zombie不适合的问题,可以在对象内存释放时在对应内存中写入无意义数据,如0×55(销毁时),0xAA(生成时);对于定位大内存越界访问的问题,可以在大内存分配之前和之后添加边界保护页;对于向内存缓冲区溢出和缓冲区释放后再用这样的常见内存问题,可以使用libgmalloc来追踪。Xcode都已经有了这些功能,分别是Malloc ScribbleMalloc Guard EdgesGuard Malloc,其实这些功能都是对malloc库(libsystem_malloc.dylib)自身调试库的调用。这些都能让相应的潜在的内存错误更容易显示地准确地暴露出来。

Logging-Malloc

有时候发现了野指针,通过Scribble确定了是意外释放,但是找不到释放的地方时,就可以使用Logging来追踪。Xcode提供了Malloc Stack功能来记录内存分配和释放的日志。在终端中使用malloc_history,App的PID、crash的地址,这里记录着该地址对应的所有分配释放日志。最终可以找到是谁对它意外释放了的。

Address Sanitization

EXC_BAD_ACCESS一直是很多开发者的噩梦,因为这个错误出现后难以实现跟踪。Apple在Xcode7中带来了提升,这样的错误会有更详细的信息,甚至会有内存使用情况的展示。启用该功能会在App下一次运行时重新编译,插入调试信息。

Environment Variable

通过添加能起到调试作用的环境变量能:

  • NSZombieEnabled
  • MallocLogFile
  • MallocGuardEdges
  • MallocDoNotProtectPrelude
  • MallocDoNotProtectPostlude
  • StackLogging
  • StackLoggingNoCompact
  • MallocCorruptionAbort
  • MallocNanoZone
  • MallocCheckHeap
  • CA_DEBUG_TRANSACTIONS

总结

总的来说,ARC要比MRC安全的多,在CoreFoundation的也支持ARC后更是如此,但是使用ARC也不是完全没事绝对安全的,比如block的循环引用。非Objc对象造成的内存错误可以有malloc相关的技术发现。这些工具都是帮助定位错误的地方,最根本的还是开发者自身编码习惯的培养和内存原则的执行。


延伸阅读


Xcode Debug
https://hllovesgithub.github.io/2015/12/25/2015-12-25-Xcode-Debug/
作者
Hu Liang
发布于
2015年12月25日
许可协议