内存管理(四)

自动释放池和Runloop关系
po [NSRunLoop currentRunLoop];

打印如下:()

observers = (
    "{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), context = {type = mutable-small, count = 1, values = (\n\t0 : <0x7f7efd801048>\n)}}",
    "{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x7fff47c2f06a), context = }",
    "{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _beforeCACommitHandler (0x7fff480bc2eb), context = }",
    "{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x7fff2b0c046e), context = }",
    "{valid = Yes, activities = 0xa0, repeats = Yes, order = 2001000, callout = _afterCACommitHandler (0x7fff480bc354), context = }",
    "{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), context = {type = mutable-small, count = 1, values = (\n\t0 : <0x7f7efd801048>\n)}}"

分析:重点在Runloop开头和结尾有两个 callout 明为_wrapRunLoopWithAutoreleasePoolHandler。

打印主线程,main二进制代码:

004-自动释放池和runloop的关系`main:
    0x1056b95d0 <+0>:   pushq  %rbp
    0x1056b95d1 <+1>:   movq   %rsp, %rbp
    0x1056b95d4 <+4>:   subq   $0x30, %rsp
    0x1056b95d8 <+8>:   movl   $0x0, -0x4(%rbp)
    0x1056b95df <+15>:  movl   %edi, -0x8(%rbp)
    0x1056b95e2 <+18>:  movq   %rsi, -0x10(%rbp)
    0x1056b95e6 <+22>:  callq  0x1056b98dc               ; symbol stub for: objc_autoreleasePoolPush
    0x1056b95eb <+27>:  movl   -0x8(%rbp), %edi
    0x1056b95ee <+30>:  movq   -0x10(%rbp), %rsi
    0x1056b95f2 <+34>:  movq   0x273f(%rip), %rcx        ; (void *)0x00000001056bbdc8: AppDelegate
    0x1056b95f9 <+41>:  movq   0x2728(%rip), %rdx        ; "class"
    0x1056b9600 <+48>:  movl   %edi, -0x14(%rbp)
    0x1056b9603 <+51>:  movq   %rcx, %rdi
    0x1056b9606 <+54>:  movq   %rsi, -0x20(%rbp)
    0x1056b960a <+58>:  movq   %rdx, %rsi
    0x1056b960d <+61>:  movq   %rax, -0x28(%rbp)
    0x1056b9611 <+65>:  callq  *0x19f1(%rip)             ; (void *)0x00007fff513f7780: objc_msgSend
    0x1056b9617 <+71>:  movq   %rax, %rdi
    0x1056b961a <+74>:  callq  0x1056b98ca               ; symbol stub for: NSStringFromClass
    0x1056b961f <+79>:  movq   %rax, %rdi
    0x1056b9622 <+82>:  callq  0x1056b98e8               ; symbol stub for: objc_retainAutoreleasedReturnValue
    0x1056b9627 <+87>:  xorl   %r8d, %r8d
    0x1056b962a <+90>:  movl   %r8d, %edx
    0x1056b962d <+93>:  movl   -0x14(%rbp), %edi
    0x1056b9630 <+96>:  movq   -0x20(%rbp), %rsi
    0x1056b9634 <+100>: movq   %rax, %rcx
    0x1056b9637 <+103>: movq   %rax, -0x30(%rbp)
    0x1056b963b <+107>: callq  0x1056b98d0               ; symbol stub for: UIApplicationMain
->  0x1056b9640 <+112>: movl   %eax, -0x4(%rbp)
    0x1056b9643 <+115>: movq   -0x30(%rbp), %rcx
    0x1056b9647 <+119>: movq   %rcx, %rdi
    0x1056b964a <+122>: callq  *0x19c0(%rip)             ; (void *)0x00007fff51411000: objc_release
    0x1056b9650 <+128>: movq   -0x28(%rbp), %rdi
    0x1056b9654 <+132>: callq  0x1056b98d6               ; symbol stub for: objc_autoreleasePoolPop
    0x1056b9659 <+137>: movl   -0x4(%rbp), %eax
    0x1056b965c <+140>: addq   $0x30, %rsp
    0x1056b9660 <+144>: popq   %rbp
    0x1056b9661 <+145>: retq   

分析:这里可以看到源码分析里面的痕迹例如objc_autoreleasePoolPush,objc_release,objc_autoreleasePoolPop。

内存泄露:

  1. 野指针
  2. 循环引用
  3. 强引用
  4. 非oc对象的一些类型影响

首先第一个要看到就是循环引用:
首先看下正常的持有和释放对象图示如下:

A,B两个对象,A对B持有,A会给B发送一个retain信息,B retainCount+1,这时候如果A dealloc 会给B发release信号,B就会引用计数-1,然后判断retaincount==0,Deallock就会被调用图示如下:

再来看下循环引用:


A引用B的时候,B也会引用A,形成相互引用,同事彼此发送一个retain消息,当前A要进行析构,需要等待B给他发送信息,但是B发送不了,因为B对A也持有,就形成了相互等待发送release信息,A等待B发送release消息,B也等待A发送release消息,导致一直释放不掉,一直存在于内存中,这个只是一般情况,现实开发过程中,是例如下面这种循环引用圈情况。

下面用代码演示下:

- (void)retainCircle{
    self.nameStr = @"fanxing";
    self.block = ^{
        NSLog(@"%@", self.nameStr);
    };
    self.block();
}

分析:self -> block -> self 形成了一个小的circle圈,解决方式:

- (void)retainCircle{
    self.nameStr = @"fanxing";
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        NSLog(@"%@", weakSelf.nameStr);
    };
    self.block();
}

分析:使用weak变量是否就可以了呢?答案是否定的,在下面这种情况下就会有问题:

- (void)retainCircle{
    self.nameStr = @"fanxing";
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
             NSLog(@"%@", weakSelf.nameStr);
        });
    };
    self.block();
}
- (void)dealloc{
    NSLog(@"%s",__func__);
}

打印如下:


分析:
这里有一个问题,dealloc先调用,当前vc已经pop释放,但是我们的block还在异步执行,因为使用了弱引用对象,self.nameStr当前self已经为nil,给nil发送name消息,所以打印null。这里的解决方式如下:(简称强弱共舞,strong&weak dance)

- (void)retainCircle{
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        __strong typeof(self) strongSelf = weakSelf;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",strongSelf.name);
        });
    };
    self.block();
}

打印如下:

分析:这总用的最多,因为我们使用weakSelf不能保证在block持有范围的生命周期,所以使用strongSelf临时变量持有weak来保证在weakSelf在block作用域空间都是有效的,从而达到数据不会丢失的安全性。(其实就是strong对waekSelf的引用计数+1,weakSelf释放不掉,weakSelf持有self,所以self释放不掉,但是strongSelf出了作用域空间就会释放掉,从而weakSelf,self,都会释放掉,所以正好满足要求)
另一种写法:

__strong typeof(weakSelf) strongself = weakSelf

循环引用的另一种解法:

- (void)retainCircle2{
    self.nameStr = @"fanxing";
    __block RetainCircleVC *vc = self;
    self.block = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@", vc.nameStr);
            vc = nil;
        });
    };
    self.block();
}

分析:本来是有一个循环圈的 self->block->vc->self,在block没有使用完这个圈还是存在的,但是在使用完之后vc=nil,这个圈就不存在了。
注意这里有一个问题,那就是必须调用self.block,如果不调用就释放不掉,循环引用圈一直存在。

最后一种循环引用解决方式:

- (void)retainCircle3{
    self.nameStr = @"fanxing";
    self.blockVC = ^(RetainCircleVC *vc) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@", vc.nameStr);
        });
    };
    self.blockVC(self);
}

分析:这里只是把self当做一个参数传过来,这总方式性能是最高的。

强引用
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
}

- (void)fireHome{
    num++;
    NSLog(@"hello word - %d",num);
}

分析:这种情况下回发生强引用,原因是timer加到runloop里面,runloop只有timer,timer持有target也就是self对象,虽然没有循环引用,但是因为runloop一直不退出,所以也会导致timer和self释放不调,timer释放不掉,一直调用,一种解决方式如下:(就是在跳转到父页面的时候,吧timer置为nil)

- (void)didMoveToParentViewController:(UIViewController *)parent{
    if (parent == nil) {
        [self.timer invalidate];
        self.timer = nil;
    }
}

关于timer还有一种解决方案:
本来持有流程:

现在增加一个中间层:


这样的话self就可以比较灵活的释放。

源码:

#import "FXTimerWapper.h"

@interface FXTimerWapper()
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL aSelector;
@property (nonatomic, strong) NSTimer *timer;
@end
 
@implementation FXTimerWapper
- (instancetype)fx_initWIthTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo{
    if(self == [super init]){
        self.target = aTarget;
        self.aSelector = aSelector;
        self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:@selector(fireHome) userInfo:userInfo repeats:YES];
    }
    return self;
}
                      
                      
- (void)fireHome{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    //让编译器出栈,恢复状态,继续编译后续的代码
    if ([self.target respondsToSelector:self.aSelector]) {
        [self.target performSelector:self.aSelector];
    }
#pragma clang diagnostic pop
    NSLog(@"wapper_fireHome");
}

- (void)fx_invalidate {
    [self.timer invalidate];
    self.timer = nil;
}

-(void)dealloc{
    NSLog(@"%s", __func__);
}

@end

分析:这种情况不会导致vc释放不掉,需要手动调用fx_invalidate释放timer。
第三种解决方式:

- (void)testTimerBlock{
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"hello word");
    }];
}
-(void)dealloc{
    [self.timer invalidate];
    self.timer = nil;
}

分析:这种是系统做了优化,不会导致vc释放不了,需要在dealloc值置为nil。

第四种解决方式NSProxy:(基于消息转发)
NSProxy 是一个抽象基类,它为一些表现的像是其它对象替身或者并不存在的对象定义API。通常,发送给代理的消息被转发给一个真实的对象或者代理本身引起加载(或者将本身转换成)一个真实的对象。NSProxy的基类可以被用来单纯的转发消息或者耗费巨大的对象的轻量初始化。

 #import "FXProxy.h"

@interface FXProxy()
@property (nonatomic, weak) id object;
@end

@implementation FXProxy
+ (instancetype)proxyWithTransformObject:(id)object{
    FXProxy *proxy = [FXProxy alloc];
    proxy.object = object;
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    return [self.object methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation{
    [invocation invokeWithTarget:self.object];
}
@end

用法:

- (void)testProxy{
    self.proxy = [FXProxy proxyWithTransformObject:self];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];
}

分析:只是单纯的消息转发,没有其他东西。

内存泄露检测:静态和动态两种解决

首先静态分析:

或者长按运行建:


如果想在编译时就显示内存泄露信息,把这里设置为YES即可。(编译时间可能会变长,不建议打开)

通过静态分析可以得到一些内存泄露问题,例如shadowPath没有被操作,fp对象没有free,数组里面插入nil形成一些野指针和内存泄露问题等。

还有一些第三方库工具可以使用,例如MLeaksFinder,还有instrument工具:

点击右边代码可以直接定位到代码:

instrument有时候检测不到,推荐使用Mleakfind(有时间需要看下源码)。
最后还有最原始的dealloc方法检测。

你可能感兴趣的:(内存管理(四))