当前位置: 首页 > 科技观察

浅谈iOSCrash(二)

时间:2023-03-17 14:24:13 科技观察

浅谈iOSCrash(一)一、ZombieObjects(僵尸对象)一、概述Zombieobjects:被释放的对象。通常,访问或向已释放的对象发送消息会导致错误。因为指针指向的内存块认为你无权访问或不能执行消息,此时内核会抛出异常(EXC),表示你不能访问该存储区(BADACCESS)。(EXC_BAD_ACCESS类型错误)调试解决这类问题,一般使用NSZombieEnabled(开启僵尸模式)。2、使用Xcode提供的NSZombieEnabled,通过生成僵尸对象来代替dealloc的实现。当对象引用计数为0时,将需要dealloc的对象转为僵尸对象。如果稍后向这个僵尸对象发送消息,则会抛出异常。首先选择Product->Scheme->EditScheme->Diagnostics->勾选ZombieObjects项,如下图:SetNSZombieEnabled.png然后在Product->Scheme->EditScheme->Arguments中设置NSZombieEnabled和MallocStackLoggingNoCompact这两个变量,and的值都是YES。显示如下:如果设置了NSZombieEnabled和MallocStackLoggingNoCompact.png,并且只设置了ZombieObjects,如果crash发生在当前调用栈,系统可以在具体代码中定位到crash的原因;但是如果当前调用栈没有发生crash,系统只会告知crash地址,所以我们需要添加变量MallocStackLoggingNoCompact,让Xcode记录下每次地址alloc的历史,然后通过命令恢复地址。Xcode6之前可以使用gdb,通过infomalloc-historyaddress命令可以将crash的地址还原到具体的代码行。Xcode7之后只能使用lldb,可以使用命令bt打印调用栈。下面是通过僵尸模式调试一个Crash,使用bt查看的效果。bteffect.png描述:在发布版本之前,僵尸对象检测的这些设置应该被删除,否则每次通过指针访问对象时,它都会检查指针指向的对象是否为僵尸对象,这将影响效率。3、代码中的注意事项在ARC时代,避免访问释放的内存。代码需要注意以下几点:检查代码1:不能使用assgin或unsafe_unretained来修改指向OC对象的指针。引用。如果指针所指向的对象被释放,就会变成野指针,很可能会发生Crash。建议1:assign只用于修饰NSInteger等OC基本类型,short、int、double、structure等C数据类型,不修饰对象指针;建议二:OC对象属性一般用strong关键字修饰(默认)。建议3:如果需要弱引用OC对象,建议使用weak关键字,因为弱指针引用的对象被回收后,弱指针会被赋值为nil(空指针),不会再有向nil发送任何消息时出现问题。使用weak修改代理对象属性就是一个很好的例子。检查代码2:CoreFoundation等底层操作CoreFoundation等底层操作不支持ARC,需要手动进行内存管理。建议:注意CF对象的创建和释放。二、野指针(Wildpointer)1.概述野指针是指向已删除对象或未申请访问受限内存区域的指针。这里的野指针主要是对象释放后指针没有清空导致的野指针。这种崩溃的发生比较随机,很难发现。一个普遍的做法是在开发阶段提高此类crash的复现率,尽可能的去发现并解决。向OC对象发送release消息只是标志该对象占用的内存可以释放,系统不会立即回收内存;如果此时向该对象发送其他消息,可能会发生Crash,也可能没有问题。下图是访问杂散指针(指向已删除对象的指针)时可能发生的情况。访问野指针可能出现的情况.png从上图我们可以看出,野指针导致crash的随机性比较大,但是当访问不到随机填充的数据时,crash是在所难免的。我们的想法是:想办法把不可访问的数据填充到野指针指向的内存中,让随机崩溃变成不可避免的崩溃。2.设置Xcode提供的MallocScribble,可以释放对象并填充内存上不可访问的数据,将随机出现的变成非随机出现的。选择Product->Scheme->EditScheme->Diagnostics->Diagnostics->勾选MallocScribble项,结果如下:设置MallocScribble.png并设置EnableScribble,对象后申请内存填0xaa申请内存,释放内存后在释放的内存上填0x55;如果内存没有初始化,就会被访问,或者释放后访问,就会出现Crash。注意:该方法必须连接Xcode才能运行代码,不适合测试人员使用。在fishhook的基础上,可以选择hook对象释放接口(C的自由函数),达到和设置EnableScribble一样的效果。详见如何定位Obj-C野指针随机崩溃(一):一、提高野指针崩溃率,如何定位Obj-C野指针随机崩溃(二):使非强制性崩溃Obj-CWildPointerRandomCrash的必然性与定位方法(三):加点黑科技让Crash自报3.查看代码中的注意事项使用assgin或unsafe_unretained修改OC对象和CoreFoundation等底层的指针操作。三、内存泄漏(MemoryLeak)一、概述内存泄漏是指未能释放不再引用对象的内存。尽管ARC帮我们解决了很多麻烦,但是还是有很多内存泄漏;一般在开发结束后,还要进行一些基本的内存泄漏排查。排查内存泄漏,一般使用Analyzer(静态分析)+Leaks+MLeaksFinder(第三方工具)2-1。用于故障排除的静态分析(Analyzer)Xcode提供的Analyzer可以在程序未运行时分析代码上下文的语法结构和内存情况,发现代码中潜在的错误,例如内存泄漏、未使用的函数和变量等。选择Product->Analyze(快捷键command+shift+B)即可使用。Analyzer主要分析四种问题:1)逻辑错误:访问空指针或未初始化的变量等;2)内存管理错误:如内存泄漏等;CoreFoundation不支持ARC3)声明错误:从未使用过的变量;4)API调用错误:不包括使用的库和框架。Analyzer执行后,常见的警告类型有:1)内存警告(Memory)eg:-(UIImage*)clipImageWithRect:(CGRect)rect{CGFloatscale=self.scale;CGImageRefclipImageRef=CGImageCreateWithImageInRect(self.CGImage,CGRectMake(rect.origin).x*scale,rect.origin.y*scale,rect.size.width*scale,rect.size.height*scale));CGRectsmallBounds=CGRectMake(0,0,CGImageGetWidth(clipImageRef)/scale,CGImageGetHeight(clipImageRef)/scale);UIGraphicsBeginImageContextWithOptions(smallBounds.size,YES,scale);CGContextRefcontext=UIGraphicsGetCurrentContext();CGContextTranslateCTM(context,0,smallBounds.size.height);CGContextScaleCTM(context,1.0,-1.0);CGContextDrawImage(context,CGRectMake(0,0,smallBounds.size.width,smallBounds.size.height),clipImageRef);UIImage*clipImage=UIGraphicsGetImageFromCurrentImageContext();UIGraphicsEndImageContext();CGImageRelease(clipImageRef);//不要添加,内存泄漏,会警告:Potentialleakofanobjectstoredinto'clipImageRef'returnclipImage;}分析:分析器检测内存leaks,比较常见的是CG和CF开头的内存泄漏,申请内存,忘记释放。还有一个就是C申请的内存没有配对newdelete,mallocfree。2)Invaliddatawarning(Deadstore)eg://错误的方式,Analyzer分析后会提示你:Valuestoredto'dataArray'duringgitsinitializationisneverreadNSMutableArray*dataArray=[[NSMutableArrayalloc]init];dataArray=_otherDataArray;//正确的方式NSMutableArray*dataArray=零;dataArray=_otherDataArray;分析:dataArray已经初始化分配内存,然后又被另一个变量数组赋值,导致一个数据源申请了两块内存,导致内存泄漏。3)逻辑错误监控(Logicerror)eg://wrongway,Analyzer分析后会提示:Propertyofmutabletype'NSMutableArray'has'copy'attribute,animmutableobjectwillbestoredinstead@property(nonatomic,copy)NSMutableArray*dataArr;//正确的方式@property(nonatomic,strong)NSMutableArray*dataArr;分析:NSMutableArray是可变数据类型,其对象要用strong修饰。注意:因为Analyzer是编译器根据代码做出的判断,做出的判断不一定准确,所以如果遇到提示,要结合上面的代码进行检查;还有一些循环引用导致内存泄漏,Analyzer分析不出来。2-2。排查内存泄漏的工具(Leaks)Xcode提供的Leak可以帮助查找运行程序的内存泄漏。通过选择Product->Profile(快捷键command+i,调出Instrument工具界面)->Leaks。切换到CallTree模式,选择SeparatebyThread(按线程单独分析)、InvertCallTree(反向输出调用树)、最下方的HideSystemLibraries(隐藏系统库文件)。最后点击红色按钮开始“记录”,效果如下:Leaksdebugginginterface.png在Leaks调试界面,1是Allocations模板,显示内存分配情况;2是Leaks模板,可以查看内存泄漏情况。如果出现红色X,则表示存在内存泄漏;主框架区域将显示泄漏的对象。CallTree选项介绍如下:CALLTREE选项说明SeparatebyCategory是按类型分类的,展开AllHeapAllocations设置可以显示不同方式下堆内存的分配SeparatebyThread是按线程单独分析的,这样更容易找出那些消耗资源的问题线程。特别是对于主线程来说,它要处理和渲染所有的界面数据。一旦被屏蔽,程序将不可避免地卡住或停止响应。InvertCallTree反转输出调用树。将调用层级最深的方法显示在顶部,可以更容易地找到最耗时的操作。HideSystemLibraries隐藏系统库文件。过滤掉各种系统调用,只显示自己的代码调用。FlatternRecursion扁平化递归。将同一个递归函数产生的多个栈(因为递归函数会调用自己)合并为一个2-3,MLeaksFinder供排查(强烈推荐)MLeaksFinder是微信读书团队推出的一款简化排查内存的第三方工具泄漏。我们当前项目中的内存泄漏工具之一。特点:集成简单,主要检查UI(UIView和UIViewController)的漏洞。原理:在不侵入开发代码的情况下,通过hookUIViewController和UINavigationController的pop和dismiss方法,在ViewController对象被关闭后的短时间内检查ViewController对象的视图,视图的子视图等是否仍然存在弹出或解雇。实现方式:在基类NSObject中添加一个方法-willDealloc方法,使用弱指针指向自身,过一小段时间(3秒)再次检查弱指针是否有效,如果有效,内存泄漏。集成:通过Cocoapods将代码引入或直接拖入项目中,非常方便。当发生内存泄漏时,会弹出一个警告框,提示内存泄漏的位置。注:具体请参考:MLeaksFinder:精准的iOS内存泄漏检测工具和MLeaksFinder新特性三、代码中的注释(ARC下的循环引用是内存泄漏的主要原因)查看代码1:CoreFoundation、CoreGraphics和其他操作CoreFoundation和CoreGraphics等操作不支持ARC,需要手动进行内存管理。建议:注意CF和CG对象的创建和释放。检查代码2:NSTimer/CADisplayLink的使用,因为NSTimer/CADisplayLink对象的target会强引用self,self会强引用NSTimer/CADisplayLink对象。建议:使用扩展方法,使用block或者target弱引用目标对象,打破保留环。具体实现请参考iOS记录8:解析NSTimer/CADisplayLink的循环引用检查代码3:block使用代码。建议:成对使用weakSelf和strongSelf来打破block循环引用(没有引用self的block不会造成循环引用,没必要使用weakSelf和strongSelf)原理:在block外定义弱引用(weakSelf),指向self对象;在block中捕获弱引用(weakSelf),保证self不会被block持有;当块中的方法执行时,会产生一个强引用(strongSelf),指向弱引用(weakSelf)所指向的对象(selfobject);self对象其实是保存在block内部的,但是这个强引用(strongSelf)的生命周期只在block执行的过程中,执行完block就立即释放。四、废弃内存(AbandonedMemory)1、概述废弃内存(AbandonedMemory)是指被引用对象所在的内存,但在程序逻辑中不能再使用了。排查这类问题,推荐使用Xcode提供的Allocation,可以跟踪应用程序的内存分配情况。2.使用Xcode提供的Allocation,因为它可以跟踪应用程序的内存分配。开发者反复运行App检查内存基线变化;他们甚至可以设置MarkGeneration来比较多代之间的内存增长。这部分增长是我们没有及时释放的内存。通过Product->Profile(快捷键command+i,调出Instrument工具界面)->Allocations。最后点击红色按钮开始“记录”,效果如下图:Allocation界面StatisticsDetail。,orCFobject,orOCobject,ororiginalblockmemoryPersistentBytesUnreleasedmemoryandsizePersistentNumberofunreleasedobjectsTransientNumberofreleasedobjectsTotalBytesTotalusedmemorysizeTotalTotalusednumberofobjectsTransient/TotalBytes释放内存大小/总usedmemorysizeALLOCATIONTYPEDescriptionAllHeap&AnonymousAllheapmemoryandothermemoryAllHeapAllocationsAllheapmemoryAllAnonymousVMAllothermemory下图是切换到CallTree下的界面显示CallTree下的Allocation界面.pngCALLTREE列名描述BytesUsedMemorysizeusedCountTotalnumbersusedsymbolsSymbolName符号名描述:这些术语的具体解释见Instrument-AllocationsIntervalTime(比如2分钟)然后点击“MarkGeneration”判断几代之间的内存增长,这些增长可能是没有及时释放的内存:根据内存使用比例,找到比例最高的部分,然后找到我们自己的代码,然后分析解决问题。Allocation界面MarkGeneration下显示的.png3,代码中的注意事项省略,与内存泄漏部分代码中的注意事项相同。