美文网首页
iOS常见内存泄漏及解决方案

iOS常见内存泄漏及解决方案

作者: MTSu1e丶 | 来源:发表于2023-03-28 10:11 被阅读0次

    iOS已经有了ARC(自动引用计数)来替代MRC(手动引用计数),申请的对象在没有被强引用时会自动释放。但在编码不规范的情况下,引用计数无法及时归零,还是会存在引入内存泄露的风险。熟悉内存泄漏场景、养成避免内存泄露的习惯是十分重要的。下面介绍一些iOS常见内存泄漏及解决方案。

    1. block引起的循环引用

    block引入的循环引用是最常见的一类内存泄露问题。常见的引用环是对象->block->对象,此时对象和block的引用计数均为1,无法被释放。

    [self.contentView setActionBlock:^{
        [self doSomething];
    }];
    

    例子代码中,self强引用成员变量contentView,contentView强引用actionBlock,actionBlock又强引用了self,引入内存泄露问题。


    image.png

    解除循环引用,就是解除强引用环,需要将某一强引用替换为弱引用。如:

    __weak typeof(self) weakSelf = self;
    [self.contentView setActionBlock:^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        [strongSelf doSomething];
    }];
    

    此时actionBlock弱引用self,循环引用被打破,可以正常释放。


    image.png

    或者使用RAC提供的更简便的写法:

    @weakify(self);
    [self setTaskActionBlock:^{
        @strongify(self);
        [self doSomething];
    }];
    

    需要注意的是,可能和block存在循环引用的不仅仅是self,所有实例对象都有可能存在这样的问题,而这也是开发过程中很容易忽略的。比如

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        Cell *cell = [tableView dequeueReusableCellWithIdentifier:@"identifer"];
        @weakify(self);
        cell.clickItemBlock = ^(CellModel * _Nonnull model) {
            @strongify(self);
            [self didSelectRowMehod:model tableView:tableView];
        };
        return cell;
    }
    

    这个例子中,self和block之间的循环引用被打破,self可以正常释放了,但是需要注意的是还存在一条循环引用链:tableView强引用cell,cell强引用block,block强引用tableView。这同样会导致tableView和cell无法释放。


    image.png

    正确的写法为:

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        Cell *cell = [tableView dequeueReusableCellWithIdentifier:@"identifer"];
        @weakify(self);
        @weakify(tableView);
        cell.clickItemBlock = ^(CellModel * _Nonnull model) {
            @strongify(self);
            @strongify(tableView);
            [self didSelectRowMehod:model tableView:tableView];
        };
        return cell;
    }
    
    image.png

    2. delegate引起的循环引用

    @protocol TestSubClassDelegate <NSObject>
    
    - (void)doSomething;
    
    @end
    
    @interface TestSubClass : NSObject
    
    @property (nonatomic, strong) id<TestSubClassDelegate> delegate;
    
    @end
    
    @interface TestClass : NSObject <TestSubClassDelegate>
    
    @property (nonatomic, strong) TestSubClass *subObj;
    
    @end
    

    上述例子中,TestSubClass对delegate使用了strong修饰符,导致设置代理后,TestClass实例和TestSubClass实例相互强引用,造成循环引用。大部分情况下,delegate都需要使用weak修饰符来避免循环引用。

    3. NSTimer强引用

    self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
    [NSRunLoop.currentRunLoop addTimer:self.timer forMode:NSRunLoopCommonModes];
    

    NSTimer实例会强引用传入的target,就会出现self和timer的相互强引用。此时必须手动维护timer的状态,在timer停止或view被移除时,主动销毁timer,打破循环引用。

    解决方案1:换用iOS10后提供的block方式,避免NSTimer强引用target。

    @weakify(self);
    self.timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        @strongify(self);
        [self doSomething];
    }];
    

    解决方案2:使用NSProxy解决强引用问题

    // WeakProxy
    @interface TestWeakProxy : NSProxy
    
    @property (nullable, nonatomic, weak, readonly) id target;
    
    - (instancetype)initWithTarget:(id)target;
    
    + (instancetype)proxyWithTarget:(id)target;
    
    @end
    
    @implementation TestWeakProxy
    
    - (instancetype)initWithTarget:(id)target {
        _target = target;
        return self;
    }
    
    + (instancetype)proxyWithTarget:(id)target {
        return [[TestWeakProxy alloc] initWithTarget:target];
    }
    
    - (void)forwardInvocation:(NSInvocation *)invocation {
        if ([self.target respondsToSelector:[invocation selector]]) {
            [invocation invokeWithTarget:self.target];
        }
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        return [self.target methodSignatureForSelector:aSelector];
    }
    
    - (BOOL)respondsToSelector:(SEL)aSelector {
        return [self.target respondsToSelector:aSelector];
    }
    
    @end
    
    // 调用
    self.timer = [NSTimer timerWithTimeInterval:1 target:[TestWeakProxy proxyWithTarget:self] selector:@selector(doSomething) userInfo:nil repeats:YES];
    

    4. 非引用类型内存泄漏

    ARC的自动释放是基于引用计数来实现的,只会维护oc对象。直接使用C语言malloc申请的内存,是不被ARC管理的,需要手动释放。常见的如使用CoreFoundation、CoreGraphics框架自定义绘图、读取文件等操作。
    如通过CVPixelBufferRef生成UIImage:

    CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    CIImage* bufferImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
    CIContext *context = [CIContext contextWithOptions:nil];
    CGImageRef frameCGImage = [context createCGImage:bufferImage fromRect:bufferImage.extent];
    UIImage *uiImage = [UIImage imageWithCGImage:frameCGImage];
    CGImageRelease(frameCGImage);
    CFRelease(sampleBuffer);
    

    5. 延迟释放问题

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(20 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self doSomething];
    });
    

    上述例子中,使用dispatch_after延迟20秒后执行doSomething方法。这并不会造成self对象的内存泄漏问题。但假设self是一个UIViewController,即使self已经从导航栈中移除,不需要再使用了,self也会在block执行后才会被释放,造成业务上出现类似内存泄露的现象。
    在这种长时间的延时执行中,最好也加入weakify-strongify对,避免强持有。

    @weakify(self);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(20 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        @strongify(self);
        [self doSomething];
    });
    

    相关文章

      网友评论

          本文标题:iOS常见内存泄漏及解决方案

          本文链接:https://www.haomeiwen.com/subject/fnsoqktx.html