block专递参数导致野指针引发crash
一、问题引入
近日开发中引入一个随机crash,Crash堆栈如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | Exception Type: SIGSEGV Exception Codes: SEGV_ACCERR at 0x0000000101850148 Crashed Thread: 0 Thread 0 Crashed: 0 libobjc.A.dylib 0x00000001802601a0 objc_retain + 16 1 CoreFoundation 0x0000000180f593a0 -[__NSDictionaryM enumerateKeysAndObjectsWithOptions:usingBlock:] + 232 2 LiveAssistant 0x0000000100213fe8 -[LAPKViewStatusObj notifyViewStateDidChangeForType:fromUser:] (LAPKViewStatusObj.m:313) 3 LiveAssistant 0x0000000100214958 -[LAPKViewStatusObj changePKStatusTo:changeMatchStatusTo:changeGuestMatchStatusTo:] (LAPKViewStatusObj.m:635) 4 LiveAssistant 0x0000000100213ed4 -[LAPKViewStatusObj updatePKState] (LAPKViewStatusObj.m:300) 5 CoreFoundation 0x000000018101cc3c ___CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 20 + 20 6 CoreFoundation 0x000000018101c1b8 __CFXRegistrationPost + 428 7 CoreFoundation 0x000000018101bf14 ____CFXNotificationPost_block_invoke + 216 8 CoreFoundation 0x000000018109984c -[_CFXNotificationRegistrar find:object:observer:enumerator:] + 1408 9 CoreFoundation 0x0000000180f52f38 _CFXNotificationPost + 376 10 Foundation 0x00000001819c3bbc -[ NSNotificationCenter postNotificationName:object:userInfo:] + 68 11 LiveAssistant 0x00000001003ae710 __44-[LAPKStatusManager notifyPKStatusDidChange]_block_invoke (LAPKStatusManager.m:107) 12 libdispatch.dylib 0x000000018097caa0 __dispatch_call_block_and_release + 24 13 libdispatch.dylib 0x000000018097ca60 __dispatch_client_callout + 16 14 libdispatch.dylib 0x000000018098965c __dispatch_main_queue_callback_4CF$VARIANT$mp + 1012 15 CoreFoundation 0x0000000181033070 ___CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 12 + 12 16 CoreFoundation 0x0000000181030bc8 ___CFRunLoopRun + 2272 17 CoreFoundation 0x0000000180f50da8 CFRunLoopRunSpecific + 544 18 GraphicsServices 0x0000000182f36020 GSEventRunModal + 100 19 UIKit 0x000000018af70758 UIApplicationMain + 228 20 LiveAssistant 0x000000010048b514 main (main.m:14) 21 libdyld.dylib 0x00000001809e1fc0 _start + 4 |
明显是对一个对象进行retain的时候产生的Crash。仔细回忆却没有发现突破点。直到看到自己写的下列代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | - ( void )xxxxxBlock:(someBlock)block yyyy:( NSString *)zzzzz { __block someblock copyUserBlock = [block copy ]; if (block) { __weak typeof( self ) wself = self ; someblock hook = ^( NSObject *statusObj, NSObject *model, int status, NSString *businessKey) { __strong typeof( self ) sself = wself; block(statusObj, model, status, businessKey); }; return ; } } - ( void )viewDidLoad { [ super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. int x = 2; NSString *str = [ NSString stringWithFormat:@ "fsfsfsdfsfsdf" ]; [ self addPKAnimationUpdateBlock:^( NSObject *statusObj, NSObject *model, int status, NSString *businessKey) { NSLog (@ "x = %d, str = %@" , x, str); } forBusiness:str]; } |
经过代码验证,stackBlock作为参数传递的时候,需要确保对其进行copy操作,否则stackBlock在函数返回之后会被释放,造成野指针。
上面的方法执行完毕之后,传入的block参数作为 stackBlock 类型依然没有发生改变;对其进行拷贝之后变成了mallocBlock。
二、问题总结分析
1)block分类
1 2 3 | 1、_NSConcreteGlobalBlock全局的静态 block,不会访问任何外部变量; 2、_NSConcreteStackBlock保存在栈中的 block,当函数返回时会被销毁; 3、_NSConcreteMallocBlock保存在堆中的 block,当引用计数为0时会被销毁; |
2)block对外部变量的引用
1 2 3 4 5 6 7 8 9 10 | 一、静态变量 和 全局变量 在加和不加 __block 都会直接引用变量地址。也就意味着 可以修改变量的值。在没有加__block 参数的情况下。 全局block 和 栈block 区别为 是否引用了外部变量,堆block 则是对栈block copy 得来。对全局block copy 不会有任何作用,返回的依然是全局block。<br> 二, 常量变量( NSString *a = @ "hello" ;a 为常量变量,@“hello”为常量。)-----不加__block类型 block 会引用常量的地址(浅拷贝)。加__block类型 block会去引用常量变量(如:a变量,a = @ "abc" .可以任意修改a 指向的内容。)的地址。 <br> 三、对象变量 如(MyClass * class 、Block block)。 这里block 也是”类“对象(类似对象,其包含isa指针,clang 反编译可以查看。因为它不像从 NSObject 继承下来的对象都支持 retain、 copy 、release)。 Block的 copy 、retain、release操作不同于 NSObjec 的 copy 、retain、release操作: 对Block不管是retain、 copy 、release都不会改变引用计数retainCount,retainCount始终是1; NSGlobalBlock :retain、 copy 、release操作都无效; NSStackBlock :retain、release操作无效,必须注意的是, NSStackBlock 在函数返回后,Block内存将被回收。即使retain也没用。容易犯的错误是[[mutableAarry addObject:stackBlock],在函数出栈后,从mutableAarry中取到的stackBlock已经被回收,变成了野指针。正确的做法是先将stackBlock copy 到堆上,然后加入数组:[mutableAarry addObject:[[stackBlock copy ] autorelease]]。支持 copy , copy 之后生成新的 NSMallocBlock 类型对象。 NSMallocBlock 支持retain、release,虽然retainCount始终是1,但内存管理器中仍然会增加、减少计数。 copy 之后不会生成新的对象,只是增加了一次引用,类似retain; 尽量不要对Block使用retain操作。 |
3)block作为外部变量的时候,确保其被copy的场景
1 | 在ARC环境,大多数情况下编译器会适当地进行判断,会自动生成将Block从栈上复制到堆上的代码。<br> 将<code>Block</code>作为函数返回值返回时,编译器会自动生成复制到堆上的代码。<br> 编译器不能判断“自动将<code>Block</code>从栈上复制到堆上”的情况:<em highlighted= "true" >向方法或函数的参数传递<code>Block</code>时</em><br> |
比如下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | -( id )getBlockArray { int val = 10; //Block变量类型可以直接调用copy方法。所以说Block其实也是Objective-C对象。 //不管Block配置在堆、栈或者数据区域,用copy方法复制都不会引起任何问题。 return [[ NSArray alloc] initWithObjects:[^{ NSLog (@ "blk0:%@" ,@(val));} copy ],[^{ NSLog (@ "blk1:%@" ,@(val));} copy ], nil ]; } - ( void )viewDidLoad { [ super viewDidLoad]; //正常执行。 id obj = [ self getBlockArray]; blk_t blk = (blk_t)[obj objectAtIndex:0]; blk(); } |
4)block什么时候会被copy
1 2 3 4 | 1. 调用Block的 copy 实例方法时; 2. Block作为函数返回值返回时; 3. 将Block赋值给附有__strong修饰符 id 类型的类或Block类型成员变量时; 4. 在方法名中含有usingBlock的Cocoa框架方法或Grand Central Dispatch的API中传递Block时。 |
三、关于block被嵌套的问题
看下面的代码:
在运行到56行的时候,str的引用计数是多少呢?
在运行到78行的时候,str的引用计数是多少呢?
在78行的时候,为什么是4
首先创建的时候是1,54行str指针引用加1 = 2; 56行第一次执行第一层block引用str加1 ; 第一层中的指针str引用加1 总共等于4
这也是嵌套block的时候内存管理的方式,嵌套的block执行的时候,运行时只能处理第一层block中引用的外部变量
此处以后要多加注意。
四、引用资料
1) ARC环境下Block的内存管理
作者:sxtra
链接:https://www.jianshu.com/p/0fad960d6795
2)https://www.cnblogs.com/DamonTang/p/4146728.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架