当一个应用崩溃时,会产生一个崩溃报告。这个报告对分析崩溃问题是非常有用的。这篇文章主要讲述了如何符号化、了解和分析应用报告。
简介
当一个应用崩溃时,会随之产生一个崩溃报告存储在设备上。崩溃报告描述了当时的崩溃情况,在绝大多数时候包含每个线程的堆栈回溯。你可以通过分析这些报告来了解应用的崩溃并且修复它们。
在分析一个报告之前,你需要先符号化它。符号化就是把原始的地址信息转换为刻度的函数名和对应的行数。如果你通过Xcode的devices窗口获得一个设备的崩溃日志,它会自动被符号化。
低内存报告与其他报告是不同的,它没有堆栈回溯信息。当由于低内存引发崩溃后,你必须研究一下你的内存使用方式和对内存警告的处理。
符号化崩溃报告
符号化是一种把堆栈地址转换为源码汇总函数名的过程。一个没有符号化的崩溃报告是无法分析出崩溃如何发生的。
注意:低内存报告不需要符号化。
随着编译器把源码转换为机器码,它同时产生了一堆debug符号,这写符号记录着源码和机器码之间的对应关系。根据Build Settings中的Debug Infomartion Format,这些符号会被存入一个二进制文件中或者DSYM中。默认的选项是存入DSYM文件中,这样会使文件的体积更小。
Debug符号文件通过UUID来和应用的二进制文件建立联系。每次build一个应用,都会产生一个新的UUID。即使源码一模一样,编译器设置也一样,build之后的UUID都是不同的。
当你archive一个应用准备发布时,Xcode会把应用的二进制文件和DYSM文件放在同一个目录下。
注意:为了能够符号化来自tester,App review和用户的崩溃报告,你必须保留每次发布的archive。
如果你通过App Store发布应用,或者通过Test Flight发布测试版本应用,你都将会收到一个选择是否上传DYSM文件的选择。对于TestFlight用户上传的崩溃报告,必须要上传DSYM文件。
注意:来自App review的崩溃报告不会被符号化,即使你上传了DSYM文件。你需要使用Xcode来符号化崩溃报告。
当你的应用崩溃了,一个没有符号化的崩溃报告会被存储在设备上。
取回用户的崩溃报告,一是可以通过Xcode直接获得设备的报告,二是用户的手机设置了允许上传诊断数据。
通过设备取回的崩溃报告是没有符号化的,需要通过Xcode来使用DSYM文件符号化它。
如果用户开启了与apple共享数据的选项,或者用户通过TestFlight安装了beta版应用,崩溃报告会上传到App Store。
App Store会符号化收到的崩溃报告。
可以通过Xcode的Crashes organizer来读取符号化的崩溃报告。
Bitcode
BitCode是编译后的程序的一种中间呈现代码。当你在bitcode选项打开时archive一个应用,编译器会产生包含bitcode的二进制,而不是机器码。一旦这个二进制被上传到App Store,bitcode会被编译成机器码。这样的话,如果编译器性能提高了就不需要再次编译二进制,而是通过App Store直接再次编译bitcode。
由于最终的编译时在App Store完成的,因此你的Mac电脑上存储的DSYM文件不能够符号化来自App review或者用户的崩溃报告。App Store在把bitcode转换为机器码时会产生一个DYSM文件,你可以通过Xcode或者iTunes Connect网站下载。你必须使用这个DYSM文件来符号化上述的崩溃报告。而通过崩溃报告服务上传的崩溃报告会被App Store自动符号化。
注意:原始提交的二进制映像的UUID和被App Store编译后的二进制映像的UUID是不同的。
通过Xcode下载DSYM文件
- 在Archives organizer中,选择提交到App Store的archive。
- 点击下载DSYMs按钮。
通过iTunes Connect网站下载DSYM
- 打开App详情页面。
- 点击Activity。
- 从一系列builds中选择一个版本。
- 点击下载DSYM链接。
转换隐藏符号名到原始符号名
当你上传带有bitcode的应用到App Store去时,你可以不勾选弹窗中的”Upload your app’s symbols to receive symbolicated reports from Apple” 选项。如果你不勾选,Xcode会混淆.dSYM
文件,再上传到iTunes Connect上。Xcode会在原始符号和隐藏符号之间建立一个mapping,然后把这个mapping存入.bcsymbolmap
文件中,这个文件会包含在archive中。
在符号化崩溃报告时,你需要解密从iTunes Connect下载的.dSYM
文件。如果你是从Xcode下载的.dSYM
文件,Xcode会自动解密。从iTunes Connect下载的.dSYM
文件可以通过如下命令行解密:
xcrun dsymutil -symbol-map ~/Library/Developer/Xcode/Archives/2017-11-23/MyGreatApp\ 11-23-17\,\ 12.00\ PM.xcarchive/BCSymbolMaps ~/Downloads/dSYMs/3B15C133-88AA-35B0-B8BA-84AF76826CE0.dSYM
是否符号化一个崩溃报告
一个崩溃报告可能是没被符号化的,或者全部符号化的。或者部分符号化的。没有符号化的崩溃报告不会在回溯信息中包含方法或者函数名。你得到的是16进制的地址。完全符号化的崩溃报告,每一行16进制地址都被转换为响应的符号。部分符号化的崩溃报告,只有部分地址被符号化。
对于分析崩溃报告,明显是需要一个完全符号化的报告。
通过Xcode来符号化崩溃报告
Xcode会尝试自动符号化一个崩溃报告。你需要做的只是把崩溃报告放入Xcode Organizer中。
注意:Xcode不会解析不以
.crash
为后缀的文件。如果你收到一个不正确后缀的报告,把后缀为.crash
。
- 连接iOS设备到Mac
- 从Window菜单中选择Devices
- 在左边的DEVICES列表中选择一个device
- 在右边的界面中点击 “View Device Logs” 按钮
- 把崩溃报告拖入
- Xcode自动符号化并展现结果
为了能够符号化崩溃报告,Xcode需要定位到以下几个内容:
- 崩溃应用的二进制映像和dSYM文件
- 应用链接的通用framework的二进制映像和dSYM需再次链接。对于源码打包的framework,其对应的dSYM文件会随着应用一同放入。对于第三方的framework,你需要问作者拿取dSYM。
- 应用崩溃时的操作系统符号。这些符号包含操作系统中framework的debug信息。Xcode会自动从设备拷贝操作系统的符号到Mac上。
如果缺少以上的任何一个信息,都将不能符号化崩溃报告,或者仅能部分符号化崩溃报告。
通过atos符号化崩溃报告
atos命令可以把地址转换为相应的符号。如果全部的debug符号信息都是可用的,那么atos的输出将包含文件名和行数信息。使用atos来符号化部分日志报告:
找到你想符号化的堆栈信息中某一行。二进制映像的的名字在第二列,地址是在第三列。
从崩溃报告中末尾的二进制映像列表中找寻相应的二进制映像。
通过UUID定位这个二进制映像的dSYM。
通过上述信息你能通过
atos
命令符号化相应的地址信息。
atos -arch <Binary Architecture> -o <Path to dSYM file>/Contents/Resources/DWARF/<binary image name> -l <load address> <address to symbolicate>
分析崩溃报告
Header
每个崩溃报告都有一个header。
Incident Identifier: B6FD1E8E-B39F-430B-ADDE-FC3A45ED368C
CrashReporter Key: f04e68ec62d3c66057628c9ba9839e30d55937dc
Hardware Model: iPad6,8
Process: TheElements [303]
Path: /private/var/containers/Bundle/Application/888C1FA2-3666-4AE2-9E8E-62E2F787DEC1/TheElements.app/TheElements
Version: 1.12
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: com.example.apple-samplecode.TheElements [402]
Date/Time: 2016-08-22 10:43:07.5806 -0700
Launch Time: 2016-08-22 10:43:01.0293 -0700
OS Version: iPhone OS 10.0 (14A5345a)
Report Version: 104
- Incident Identifier:报告的唯一标识符。不同的报告的标识符不同。
- CrashReporter Key:设备隐秘的identifier。同一设备不同的报告的值相同。
异常信息
崩溃可以有很多Mach异常类型导致。不是所有的异常类型都会在崩溃报告中。
由于uncaught Objective-C exception引起的崩溃
Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Triggered by Thread: 0
由于空指针引起的崩溃
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000000
Termination Signal: Segmentation fault: 11
Termination Reason: Namespace SIGNAL, Code 0xb
Terminating Process: exc handler [0]
Triggered by Thread: 0
- Exception Codes: 程序设置的关于异常的信息。这个信息是编码成16进制。一般,日志报告会解析这个信息成可读的描述,放入其它字段中。
- Exception Subtype:可读的exception code的名字。
- Exception Message:解析过后的exception code。
- Exception Note:没有被设置成异常类型的额外信息。如果这个字段包含SIMULATED,意味着这个程序没有崩溃,而是被系统杀死,比如watchdog。
- Exception Reason:崩溃原因。
- Triggered by Thread: 崩溃的线程。
接下去来介绍一下常见的异常类型:
Bad Memory Access [EXC_BAD_ACCESS // SIGSEGV // SIGBUS]
程序尝试进入无效的内存,或者尝试进入不被允许进入的内存。Exception Subtype字段包含kern_return_t
用来描述错误和错误进入的内存地址。
下面是一些错误内存进入崩溃的调试技巧:
- 如果
objc_msgSend
或者objc_release
在崩溃线程堆栈回溯的顶部附近,程序可能尝试向一个释放的对象发送消息。你应该设置Zombies instrument来进一步分析崩溃的状况。 - 如果
gpus_ReturnNotPermittedKillClient
在崩溃线程堆栈回溯的顶部附近,程序可能是由于用OpenGL ES或者Metal在后台进行渲染。 - 设置 Address Sanitizer来运行你的程序。
Abnormal Exit [EXC_CRASH // SIGABRT]
程序非正常退出。这种崩溃绝大多数由于uncaught Objective-C/C++异常和调用abort()
引起。
Trace Trap [EXC_BREAKPOINT // SIGTRAP]
跟非正常退出类型。这个异常会打算给一个附加的调试器,使有机会在设定的点来中止程序。你能通过__builtin_trap()
函数来触发这个异常。如果没有调试器被附加,程序会终止并产生崩溃报告。
底层库在遇到致命错误后会触发这个异常。关于这个错误的详细信息可以在下面的Additional Diagnostic Information中找到。
swifi代码在runtime时如果如下几种情况时也会产生这种异常:
- 非可选类型被赋予nil
- 强制进行错误的类型转换
Illegal Instruction [EXC_BAD_INSTRUCTION // SIGILL]
程序尝试执行一个非法的或者没有定义的指令。程序可能通过一个错误的函数指针进入一个无效的地址。
Quit [SIGQUIT]
程序被其他程序终止。在iOS中,键盘extension可能被宿主app杀死由于加载时间太长。
Killed [SIGKILL]
程序被系统终止。
Guarded Resource Violation [EXC_GUARD]
程序违反进入保护的资源。比如程序使用SQLite命令来操作Core Data,将会导致崩溃。
Resource Limit [EXC_RESOURCE]
程序超过了资源消耗的限制。
Other Exception Types
一些崩溃报告可能报告没有命名的异常类型,只会打印16进制数值。
0xbaaaaaad
表示日志是整个系统的一个堆栈快照(stackshot)而不是一个崩溃报告。通过按下Home键和任意音量键可以获得堆栈快照(stackshot)。通常这些日志是被用户意外创建的(不小心同时按下了Home键+音量键),而不是一个错误。0xbad22222
表示一个VoIP应用由于恢复过于频繁而被iOS结束进程。0x8badf00d
表示一个应用由于watchdog发生超时而被iOS结束进程。这表明该应用程序在启动、结束或者响应系统事件时花费太长时间。一个常见的例子是在主线程上实现同步的网络操作。任何在Thread 0(主线程)上的操作都应该放到后台线程上执行,或者使用不阻塞主线程的方式进行处理。0xc00010ff
表示一个应用由于响应一个热事件(thermal event)而被操作系统杀掉进程。这可能是由于在特定的设备上发生崩溃,或者是操作环境的问题。为了让你的程序获得更多有效的提示,请观看使用Instruments来提升iOS性能和电量优化的WWDC会议。0xdead10cc
表示一个应用由于在后台运行时还保留着系统的资源(如通讯录数据库)而被iOS结束进程。0x2bad45ec
表示应用程序违反安全规则被强制退出。"Process detected doing insecure drawing while in secure mode"
表示应用尝试draw在不被允许的时候,比如屏幕上锁时。用户可能不会注意到这个终止因为屏幕是被上锁了。
注意:终止一个挂起的程序不会产生崩溃报告。
额外的诊断信息 (Additional Diagnostic Information)
- Application Specific Information:在程序终止前被捕获到的framework错误信息
- Kernel Messages:代码签名问题详情
- Dyld Error Messages:动态链接器发出的错误信息
链接的framework无法被找到引起的崩溃
Dyld Error Message:
Dyld Message: Library not loaded: @rpath/MyCustomFramework.framework/MyCustomFramework
Referenced from: /private/var/containers/Bundle/Application/CD9DB546-A449-41A4-A08B-87E57EE11354/TheElements.app/TheElements
Reason: no suitable image found.
加载初始化view controller失败引起的崩溃
Application Specific Information:
com.example.apple-samplecode.TheElements failed to scene-create after 19.81s (launch took 0.19s of total time limit 20.00s)
Elapsed total CPU time (seconds): 7.690 (user 7.690, system 0.000), 19% CPU
Elapsed application CPU time (seconds): 0.697, 2% CPU
回溯
崩溃日志中最有趣的部分是程序终止时的每个线程回溯。
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 TheElements 0x000000010006bc20 -[AtomicElementViewController myTransitionDidStop:finished:context:] (AtomicElementViewController.m:203)
1 UIKit 0x0000000194cef0f0 -[UIViewAnimationState sendDelegateAnimationDidStop:finished:] + 312
2 UIKit 0x0000000194ceef30 -[UIViewAnimationState animationDidStop:finished:] + 160
3 QuartzCore 0x0000000192178404 CA::Layer::run_animation_callbacks(void*) + 260
4 libdispatch.dylib 0x000000018dd6d1c0 _dispatch_client_callout + 16
5 libdispatch.dylib 0x000000018dd71d6c _dispatch_main_queue_callback_4CF + 1000
6 CoreFoundation 0x000000018ee91f2c __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 12
7 CoreFoundation 0x000000018ee8fb18 __CFRunLoopRun + 1660
8 CoreFoundation 0x000000018edbe048 CFRunLoopRunSpecific + 444
9 GraphicsServices 0x000000019083f198 GSEventRunModal + 180
10 UIKit 0x0000000194d21bd0 -[UIApplication _run] + 684
11 UIKit 0x0000000194d1c908 UIApplicationMain + 208
12 TheElements 0x00000001000653c0 main (main.m:55)
13 libdyld.dylib 0x000000018dda05b8 start + 4
Thread 1:
0 libsystem_kernel.dylib 0x000000018deb2a88 __workq_kernreturn + 8
1 libsystem_pthread.dylib 0x000000018df75188 _pthread_wqthread + 968
2 libsystem_pthread.dylib 0x000000018df74db4 start_wqthread + 4
...
第一行列举了线程的序号和当前队列的标识。剩下的行表示回溯中的堆栈详情。从左到右依次为:
- 堆序号
- 执行这个函数的二进制映象名称
- 机器指令地址
- 符号化的方法名
异常(Exceptions)
Objective-C中的异常是用来指明程序运行时检测到的错误的,比如数组越界,转换不可变对象为可变对象,没有实现一个必要的方法或者协议,或者给接收者发送一个无法识别的message。
注意:给已经释放的对象发送消息可能会引发
NSInvalidArgumentException
,而不是违反内存进入崩溃。发生这种情况常常是被创建的新对象的内存覆盖了先前已经释放对象的内存。如果你的程序由于没有捕获NSInvalidArgumentException
引起崩溃,可以使用 Zombies instrument来解决引发这种错误内存管理的问题。
如果一个异常没有被捕获,它会被一个叫做uncaught exception handler
的函数拦截。uncaught exception handler
默认在设备的console打印异常信息,然后终止程序。这是崩溃报告中只会在Last Exception Backtrace
节中有这个异常的回溯。详细的异常信息在崩溃报告中被遗漏。如果你收到一个含有Last Exception Backtrace
的崩溃报告,你应该从原始射中中获取console日志来更好的了解当时引起异常的情况。
Last Exception Backtrace:
(0x18eee41c0 0x18d91c55c 0x18eee3e88 0x18f8ea1a0 0x195013fe4 0x1951acf20 0x18ee03dc4 0x1951ab8f4 0x195458128 0x19545fa20 0x19545fc7c 0x19545ff70 0x194de4594 0x194e94e8c 0x194f47d8c 0x194f39b40 0x194ca92ac 0x18ee917dc 0x18ee8f40c 0x18ee8f89c 0x18edbe048 0x19083f198 0x194d21bd0 0x194d1c908 0x1000ad45c 0x18dda05b8
一个含有Last Exception Backtrace
的崩溃报告只会有16进制地址,它必须被符号化才能变得可读。
下面是符号化的日志报告中的Last Exception Backtrace
节异常。这个异常是由于缺少IBOutlet在storyboard中引起的。
Last Exception Backtrace:
0 CoreFoundation 0x18eee41c0 __exceptionPreprocess + 124
1 libobjc.A.dylib 0x18d91c55c objc_exception_throw + 56
2 CoreFoundation 0x18eee3e88 -[NSException raise] + 12
3 Foundation 0x18f8ea1a0 -[NSObject(NSKeyValueCoding) setValue:forKey:] + 272
4 UIKit 0x195013fe4 -[UIViewController setValue:forKey:] + 104
5 UIKit 0x1951acf20 -[UIRuntimeOutletConnection connect] + 124
6 CoreFoundation 0x18ee03dc4 -[NSArray makeObjectsPerformSelector:] + 232
7 UIKit 0x1951ab8f4 -[UINib instantiateWithOwner:options:] + 1756
8 UIKit 0x195458128 -[UIStoryboard instantiateViewControllerWithIdentifier:] + 196
9 UIKit 0x19545fa20 -[UIStoryboardSegueTemplate instantiateOrFindDestinationViewControllerWithSender:] + 92
10 UIKit 0x19545fc7c -[UIStoryboardSegueTemplate _perform:] + 56
11 UIKit 0x19545ff70 -[UIStoryboardSegueTemplate perform:] + 160
12 UIKit 0x194de4594 -[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:] + 1352
13 UIKit 0x194e94e8c -[UITableView _userSelectRowAtPendingSelectionIndexPath:] + 268
14 UIKit 0x194f47d8c _runAfterCACommitDeferredBlocks + 292
15 UIKit 0x194f39b40 _cleanUpAfterCAFlushAndRunDeferredBlocks + 560
16 UIKit 0x194ca92ac _afterCACommitHandler + 168
17 CoreFoundation 0x18ee917dc __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 32
18 CoreFoundation 0x18ee8f40c __CFRunLoopDoObservers + 372
19 CoreFoundation 0x18ee8f89c __CFRunLoopRun + 1024
20 CoreFoundation 0x18edbe048 CFRunLoopRunSpecific + 444
21 GraphicsServices 0x19083f198 GSEventRunModal + 180
22 UIKit 0x194d21bd0 -[UIApplication _run] + 684
23 UIKit 0x194d1c908 UIApplicationMain + 208
24 TheElements 0x1000ad45c main (main.m:55)
25 libdyld.dylib 0x18dda05b8 start + 4
注意:如果你发现应用程序所指定的异常处理域的异常在抛出时没有被捕获,请检查你的应用在编译时是否设置了
-no_compact_unwind
标识。
64位的iOS系统使用了“零成本”异常实现。在“零成本”系统中,每个函数都有额外的数据来描述如何展开堆栈,在异常抛出时。如果抛出了一个没有展开数据的堆栈的异常,异常就无法被处理,然后程序被终止。异常处理程序可能能处理堆栈,但是如果没有展开数据的堆栈,那么就没有办法从异常中获取相应的堆栈。设置-no_compact_unwind
标识意味着你不需要展开的数据,那么你就不能从那些函数中抛出异常。
此外,如果你的程序或者库中包含C代码,你需要指定-funwind-tables
标识来让你的代码中的函数包含展开数据。
线程状态
这一节主要列举崩溃线程的线程状态。下面是程序终止时寄存器的值。在阅读崩溃报告时,不一定要了解线程状态,但是如果能够使用这些信息的话,就会更好的了解当时的崩溃状态。
Thread 0 crashed with ARM Thread State (64-bit):
x0: 0x0000000000000000 x1: 0x000000019ff776c8 x2: 0x0000000000000000 x3: 0x000000019ff776c8
x4: 0x0000000000000000 x5: 0x0000000000000001 x6: 0x0000000000000000 x7: 0x00000000000000d0
x8: 0x0000000100023920 x9: 0x0000000000000000 x10: 0x000000019ff7dff0 x11: 0x0000000c0000000f
x12: 0x000000013e63b4d0 x13: 0x000001a19ff75009 x14: 0x0000000000000000 x15: 0x0000000000000000
x16: 0x0000000187b3f1b9 x17: 0x0000000181ed488c x18: 0x0000000000000000 x19: 0x000000013e544780
x20: 0x000000013fa49560 x21: 0x0000000000000001 x22: 0x000000013fc05f90 x23: 0x000000010001e069
x24: 0x0000000000000000 x25: 0x000000019ff776c8 x26: 0xee009ec07c8c24c7 x27: 0x0000000000000020
x28: 0x0000000000000000 fp: 0x000000016fdf29e0 lr: 0x0000000100017cf8
sp: 0x000000016fdf2980 pc: 0x0000000100017d14 cpsr: 0x60000000
二进制映象
这一节列举了程序终止时加载的二进制映象。
Binary Images:
0x100060000 - 0x100073fff TheElements arm64 <2defdbea0c873a52afa458cf14cd169e> /var/containers/Bundle/Application/888C1FA2-3666-4AE2-9E8E-62E2F787DEC1/TheElements.app/TheElements
...
每一行列出二进制映象的一下细节:
- 二进制映象在进程中地址
- 二进制映象的名字或者包名(macOS only)
- 二进制映象的版本信息(macOS only)
- 二进制映象的架构(iOS only)
- 二进制映象的UUID
- 二进制映象在磁盘上的路径
了解低内存报告
当低内存被检测到时,iOS上的虚拟内存系统会协调应用来释放内存。低内存通知被发送到所有正在运行的应用来处理释放内存的请求,希望来降低使用的内存的总量。如果内存压力依旧存在,系统可能终止在后台的程序来降低内存压力。如果足够的内存被释放掉,你的程序将继续运行。如果没有的话,你的程序将会被终止,因为没有足够的内存来满足程序的需求,同时会在设备上生成一个低内存报告。
低内存报告的形式于其他崩溃报告不同,它没有程序线程的回溯信息。低内存报告的header和崩溃报告的相同。需要注意page size这个字段。每个程序的内存使用都会在低内存报告中内存页数量的形式展示。
低内存报告中最重要的时程序表。程序表包含所有正在运行的程序,包括系统守护进程。如果一个程序被杀死,原因会在 [reason]列中展示。一个程序可能被杀死的原因有以下这么几个:
- per-process-limit: 该进程跨越了系统施加的内存限制。系统为所有应用建立的常驻内存进行了限制。跨越该限制的进程将会被终止。
- vm-pageshortage/vm-thrashing/vm: 由于内存压力被杀死
- vnode-limit: 太多的文件被打开
- highwater: 系统守护进程到达了它的内存使用限制
- jettisoned: 其他原因
如果你没有看到原因中列出你的应用/扩展的进程,那么可能不是因为内存压力引起的崩溃。查看.crash文件(上一节讲述的)了解更多信息。
当你看到一个低内存崩溃时,与其关心那一部分代码在应用终止时正在执行,倒不如检查你对内存的使用方式和对低内存警告的响应处理。在你的应用中查找内存问题一文中详细地讲述了如何使用Instruments的Leaks分析来发现内存泄露,以及如何使用Allocations分析的Mark Heap功能来防止出现被遗弃的内存。《内存使用性能指南》论述了一种正确的方法来应对低内存通知,同时又提供了很多有效使用内存的技巧。同时也建议你看看WWDC2010年会议,使用Instruments进行高级内存分析。