NSBlockOperation内存泄漏致App崩溃求助(含NSExactBlockVariable泄漏)
嘿,我帮你梳理下这个内存泄漏问题的排查和解决方向,结合你贴的代码来看,大概率是循环引用在搞鬼,咱们一步步来拆解:
先分析你代码里的核心问题
先把你的代码贴出来方便对照:
- (EMUserInfoOperation*)loadingLocalModelOperationWithColor:(EMOutfitColor)outfitColor gender:(EMGender)gender { __block EMUserInfoOperation* operation = [EMUserInfoOperation blockOperationWithBlock:^{ NSURL* remoteURL = [NSURL URLWithString:self.settings[kEMRemoteUrlKey]]; EMOutfitModel* model = nil; if (remoteURL == nil) { model = [[EMDomainDataLoader sharedLoader] loadEmbededOutfitNamed:self.name gender:gender]; } else { model = [[EMDomainDataLoader sharedLoader] loadCachedOutfitNamed:self.name withVersion:self.version gender:gender]; } [model syncApplyTextureFromPath:[self texturePathForColor:outfitColor] textureSampler:EMTextureSamplerColor]; NSString *alphaPath = [self texturePathForAlpha]; if(alphaPath.length > 0) { [model syncApplyTextureFromPath:alphaPath textureSampler:EMTextureSamplerAlpha]; } operation.userInfo = model; }]; return operation; }
这里的关键问题是循环引用:你用__block修饰了operation,在ARC环境下,block会自动retain这个__block变量;同时EMUserInfoOperation作为NSBlockOperation的子类,会持有这个block。这样就形成了operation → block → operation的闭环,导致两者都无法被ARC自动释放,最终触发NSExactBlockVariable泄漏(这个类就是block用来捕获__block变量的容器)。
一步步排查验证
1. 用Xcode工具实锤循环引用
- Memory Graph Debugger:运行App后触发这个操作,点击调试栏里的内存图按钮(📊),找到
EMUserInfoOperation实例,查看它的引用链。如果看到它持有block,而block又持有它自己,那循环引用就坐实了。 - Instruments Leaks工具:启动Leaks模板,重复调用
loadingLocalModelOperationWithColor:gender:方法,Leaks会显示泄漏对象的完整引用路径,你能清晰看到NSExactBlockVariable是被block持有,而block又被operation持有的链条。
2. 修复循环引用的两种方案
方案一:用弱引用打破闭环(最直接)
把__block改成弱引用,再在block内部临时强引用避免提前释放,代码调整如下:
- (EMUserInfoOperation*)loadingLocalModelOperationWithColor:(EMOutfitColor)outfitColor gender:(EMGender)gender { __weak EMUserInfoOperation* weakOperation = nil; EMUserInfoOperation* operation = [EMUserInfoOperation blockOperationWithBlock:^{ // 临时强引用,防止block执行时operation被提前释放 __strong EMUserInfoOperation* strongOperation = weakOperation; if (!strongOperation) return; NSURL* remoteURL = [NSURL URLWithString:self.settings[kEMRemoteUrlKey]]; EMOutfitModel* model = nil; if (remoteURL == nil) { model = [[EMDomainDataLoader sharedLoader] loadEmbededOutfitNamed:self.name gender:gender]; } else { model = [[EMDomainDataLoader sharedLoader] loadCachedOutfitNamed:self.name withVersion:self.version gender:gender]; } [model syncApplyTextureFromPath:[self texturePathForColor:outfitColor] textureSampler:EMTextureSamplerColor]; NSString *alphaPath = [self texturePathForAlpha]; if(alphaPath.length > 0) { [model syncApplyTextureFromPath:alphaPath textureSampler:EMTextureSamplerAlpha]; } strongOperation.userInfo = model; }]; weakOperation = operation; return operation; }
解释:__weak修饰的weakOperation不会被block retain,打破了operation → block → operation的循环;block内部的strongOperation是为了确保在block执行期间,operation不会被意外释放,避免野指针问题。
方案二:避免在block内直接引用operation
如果你的业务允许,可以换一种方式传递model,比如给EMUserInfoOperation加一个completion block,把model通过completion传递出去,而不是直接给operation.userInfo赋值。不过这种方式需要修改EMUserInfoOperation的设计,成本相对高一些,适合不想改动当前引用逻辑的场景。
3. 排查其他潜在泄漏点
除了循环引用,还要检查其他对象的生命周期:
- EMOutfitModel的内存管理:查看
syncApplyTextureFromPath:方法内部是否持有了大的纹理资源没有释放,或者model是否和其他对象(比如self、EMDomainDataLoader)形成了新的循环引用。可以在Memory Graph里查看EMOutfitModel的引用者,看是否有不必要的长期持有。 - EMDomainDataLoader单例的缓存策略:单例的生命周期是整个App,如果它对加载的model做了强引用缓存,而你的operation又持有model,会导致model无法释放,间接拖垮operation的释放。可以检查
loadCachedOutfitNamed:方法的实现,看是否用了弱引用缓存,或者有没有定期清理缓存的逻辑。 - self的循环引用风险:你的block里引用了
self.settings、self.name等属性,ARC下block会自动retain self。如果self(当前类实例)又持有返回的operation,就会形成self → operation → block → self的循环。这种情况要给self也加弱引用:
__weak typeof(self) weakSelf = self; EMUserInfoOperation* operation = [EMUserInfoOperation blockOperationWithBlock:^{ __strong typeof(self) strongSelf = weakSelf; if (!strongSelf) return; // 后续用strongSelf代替self NSURL* remoteURL = [NSURL URLWithString:strongSelf.settings[kEMRemoteUrlKey]]; // ... 其他代码 }];
验证修复效果
修复后,按以下步骤确认问题解决:
- 用Memory Graph Debugger多次触发操作,确认
EMUserInfoOperation和block实例在操作完成后能被正常释放,没有残留引用。 - 用Leaks工具运行App,重复操作,确认
NSExactBlockVariable和其他泄漏类型全部消失。 - 长时间运行App,监控内存占用,看是否还会出现内存飙升导致崩溃的情况。
内容的提问来源于stack exchange,提问作者Atalyk

