内存管理(四)

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

打印如下:()

observers = (
    "<CFRunLoopObserver 0x600001e483c0 [0x7fff80617cb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), context = <CFArray 0x600002104600 [0x7fff80617cb0]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7f7efd801048>\n)}}",
    "<CFRunLoopObserver 0x600001e48140 [0x7fff80617cb0]>{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x7fff47c2f06a), context = <CFRunLoopObserver context 0x6000004481c0>}",
    "<CFRunLoopObserver 0x600001e48280 [0x7fff80617cb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _beforeCACommitHandler (0x7fff480bc2eb), context = <CFRunLoopObserver context 0x7f7efd400960>}",
    "<CFRunLoopObserver 0x600001e501e0 [0x7fff80617cb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x7fff2b0c046e), context = <CFRunLoopObserver context 0x0>}",
    "<CFRunLoopObserver 0x600001e48320 [0x7fff80617cb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2001000, callout = _afterCACommitHandler (0x7fff480bc354), context = <CFRunLoopObserver context 0x7f7efd400960>}",
    "<CFRunLoopObserver 0x600001e48460 [0x7fff80617cb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), context = <CFArray 0x600002104600 [0x7fff80617cb0]>{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方法检测。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343