内存-③内存泄漏定位

内存-③内存泄漏定位

未整合的文章:

目录

1
2
3
4
5
6
7
8
1、常见的三种泄露情形
(1)、创建了一个对象,但是并没有使用。Xcode提示信息:Value Stored to 'number' is never read。翻译一下:存储在'number'里的值从未被读取过。
(2)、创建了一个(指针可变的)对象,且初始化了,但是初始化的值一直没读取过。Xcode提示信息:Value Stored to 'str' during its initialization is never read
(3)、调用了让某个对象引用计数加1的函数,但没有调用相应让其引用计数减1的函数。Xcode提示信息:Potential leak of an object stored into 'subImageRef'。 翻译一下:subImageRef对象的内存单元有潜在的泄露风险。ARC中常见于CGxxxRef未release。
>
2、.....
>
>
1
2
3
4
5
6
7
8
1、ARC下dealloc的使用
ARC下,系统可以帮我们释放该对象,及其包含的对象;但是却无法释放不属于该对象的一些东西,如:
(1)、通知的观察者,或KVO的观察者;
(2)、对象强委托/引用的解除;
(3)、做一些其他的注销之类的操作,如一个ViewController在销毁之前有可能需要和server打交道。
>
2、controller 不能释放,不走dealloc方法的几种可能
>

以下内容摘自:iOS性能优化之内存管理:Analyze、Leaks、Allocations的使用和案例代码

前言

内存空间的划分: 我们知道,一个进程占用的内存空间,包含5种不同的数据区:

1
2
3
4
5
(1)BSS段:通常是存放未初始化的全局变量;
(2)数据段:通常是存放已初始化的全局变量。
(3)代码段:通常是存放程序执行代码。
(4)堆:通常是用于存放进程运行中被动态分配的内存段,OC对象(所有继承自NSObject的对象)就存放在堆里。
(5)栈:由编译器自动分配释放,存放函数的参数值,局部变量等值。

栈内存是系统来管理的,因此我们常说的内存管理,指的是堆内存的管理,也就是所有OC对象的创建和销毁的管理。

伴随着iOS5的到来,苹果推出了ARC(自动引用计数)技术,此模式下编译器会自动在合适的地方插入retain、release、autorelease语句,也就是说编译器会自动生成内存管理的代码,解放了广大程序猿的双手,也基本上避免了内存泄露问题,但是呢…

内存泄露的定义是:用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。(其实说白了就是该内存空间使用完毕之后未回收)。

在iOS应用中的内存泄露,原因一般有循环引用、错用Strong/copy等。

重要文章

一、Analyze—静态分析

< 返回目录

顾名思义,静态分析不需要运行程序,就能检查到存在内存泄露的地方。

使用方法:打开Xcode,command + shift + B;或者Xcode - Product - Analyze;

1、常见的三种泄露情形:

(1)创建了一个对象,但是并没有使用。Xcode提示信息:Value Stored to ‘number’ is never read。翻译一下:存储在’number’里的值从未被读取过。

(2)创建了一个(指针可变的)对象,且初始化了,但是初始化的值一直没读取过。Xcode提示信息:Value Stored to ‘str’ during its initialization is never read

(3)调用了让某个对象引用计数加1的函数,但没有调用相应让其引用计数减1的函数。Xcode提示信息:Potential leak of an object stored into ‘subImageRef’。 翻译一下:subImageRef对象的内存单元有潜在的泄露风险。ARC中常见于CGxxxRef未release。

贴上三种常见情形的Demo代码,如下:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
 /**
* 情 形 一:创建了一个对象,但是并没有使用。
* 提示信息:Value Stored to 'number' is never read
* 翻译一下:存储在'number'里的值从未被读取过,
*/
- (void)leakOne {
NSString *str1 = [NSString string];
NSNumber *number;
number = @(str1.length);
/*
说我们没有读取过它,那就读取一下,比如打开下面这句代码,对它发送class消息,就不再会有这个提示了。
当然最好的方法还是将有关number的代码都删掉,因为,你只对number赋值,又不使用,那干嘛创建出来呢。
这是一个比较常见和典型的错误,也很容易检查出来
*/
// [number class];
}

/**
* 情 形 二:创建了一个(指针可变的)对象,且初始化了,但是初始化的值一直没读取过。
* 提示信息:Value Stored to 'str' during its initialization is never read
*/
- (void)leakTwo {
NSString *str = [NSString string]; // 创建并初始化str,此时已经有一个内存单元保存str初始化的值
// NSString *str; // 这样就内存不泄露,因为str是可变的,只需要先声明就行。
// printf("str前 = %p\n",str);
str = @"ceshi"; // str被改变了,指向了"ceshi"所在的地址,指针改变了,但之前保存初始化值的内存空间还未释放,保存str初始化值的内存单元泄露了。
// printf("str后 = %p\n",str); // 指针改变了
[str class];

// 再举两个例子,同理

NSArray *arr = [NSArray array];
// printf("arr前 = %p\n",arr);
// NSArray *arr; // 这样就内存不泄露
arr = @[@"1",@"2"];
// printf("arr后 = %p\n",arr); // 指针改变了
[arr class];

CGRect rect = self.view.frame;
// CGRect rect = CGRectZero; // 这样就内存不泄露
rect = CGRectMake(0, 0, 0, 0);
NSLog(@"rect = %@",NSStringFromCGRect(rect));
}

/**
* 情 形 三:调用了让某个对象引用计数加1的函数,但没有调用相应让其引用计数减1的函数。
* 提示信息:Potential leak of an object stored into 'subImageRef'
* 翻译一下:subImageRef对象的内存单元有潜在的泄露风险
*/
- (void)leakThree {
CGRect rect = CGRectMake(0, 0, 50, 50);
UIImage *image;
CGImageRef subImageRef = CGImageCreateWithImageInRect(image.CGImage, rect); // subImageRef 引用计数 + 1;

UIImage* smallImage = [UIImage imageWithCGImage:subImageRef];

// 应该调用对应的函数,让subImageRef的引用计数减1,就不会泄露了
// CGImageRelease(subImageRef);

[smallImage class];
UIGraphicsEndImageContext();
}

自己遇到的实例:

情形2:静态检测内存泄露Analyze--Value stored to ‘dataArr’ during its initialization is never read
即初始化的时候开辟了一块内存,却始终没用到,导致该块内存泄漏

1
2
3
4
5
6
NSMutableArray *tempMutArr = [NSMutableArray arrayWithCapacity:0];
if ([self.clickedButtonTpye isEqualToString:KClickedButtonTypeLast]) {
tempMutArr = self.lastDataSourceArr;
}else{
tempMutArr = self.hotDataSourceArr;
}

二、Leaks—内存泄露

< 返回目录

  • MLeaksFinder:精准 iOS 内存泄露检测工具

    项目 UIViewController+MemoryLeak.m NSObject+MemoryLeak.m

    原理:MLeaksFinder 一开始从 UIViewController 入手。我们知道,当一个 UIViewController 被 pop 或 dismiss 后,该 UIViewController 包括它的 view,view 的 subviews 等等将很快被释放(除非你把它设计成单例,或者持有它的强引用,但一般很少这样做)。于是,我们只需在一个 ViewController 被 pop 或 dismiss 一小段时间后,看看该 UIViewController,它的 view,view 的 subviews 等等是否还存在。

    即我们hook viewDidDisappear:dismissViewControllerAnimated: completion: 都去执行NSObject的willDealloc方法。

    在NSObject的willDealloc方法中,隔2秒后再去尝试调用另一个方法assertNotDealloc。如果UIViewController已经被成功释放,则肯定是 nil 执行 assertNotDealloc,即assertNotDealloc不会被执行,也就不会弹出内存泄露的弹窗。反之,若UIViewController没有释放,则会弹出内存泄露的弹窗。

    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
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    > // UIViewController+MemoryLeak.m
    > - (void)swizzled_viewDidDisappear:(BOOL)animated {
    > [self swizzled_viewDidDisappear:animated];
    >
    > if ([objc_getAssociatedObject(self, kHasBeenPoppedKey) boolValue]) {
    > [self willDealloc];
    > }
    > }
    >
    > - (void)swizzled_dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
    > [self swizzled_dismissViewControllerAnimated:flag completion:completion];
    > ......
    >
    > [dismissedViewController willDealloc];
    > }
    >
    > // NSObject+MemoryLeak.m
    > - (BOOL)willDealloc {
    > ......
    >
    > __weak id weakSelf = self;
    > dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    > __strong id strongSelf = weakSelf;
    > [strongSelf assertNotDealloc];
    > });
    >
    > return YES;
    > }
    >
    > - (void)assertNotDealloc {
    > if ([MLeakedObjectProxy isAnyObjectLeakedAtPtrs:[self parentPtrs]]) {
    > return;
    > }
    > [MLeakedObjectProxy addLeakedObject:self];
    >
    > NSString *className = NSStringFromClass([self class]);
    > NSLog(@"Possibly Memory Leak.\nIn case that %@ should not be dealloced, override -willDealloc in %@ by returning NO.\nView-ViewController stack: %@", className, className, [self viewStack]);
    > }
    >
  • 自动化内存泄漏检测 项目 https://github.com/liujiakuoyx/leak_detector/blob/main/lib/src/leak_navigator_observer.dart#L121

    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
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    > ///Such as WebSocket is delay close connect.
    > const int _defaultCheckLeakDelay = 500;
    >
    > typedef ShouldAddedRoute = bool Function(Route route);
    >
    > ///NavigatorObserver
    > class LeakNavigatorObserver extends NavigatorObserver {
    > final ShouldAddedRoute? shouldCheck;
    > final int checkLeakDelay;
    >
    > ///[callback] if 'null',the all route can added to LeakDetector.
    > ///if not 'null', returns ‘true’, then this route will be added to the LeakDetector.
    > LeakNavigatorObserver(
    > {this.checkLeakDelay = _defaultCheckLeakDelay, this.shouldCheck});
    >
    > @override
    > void didPop(Route route, Route? previousRoute) {
    > _remove(route);
    > }
    >
    > @override
    > void didPush(Route route, Route? previousRoute) {
    > _add(route);
    > }
    >
    > @override
    > void didRemove(Route route, Route? previousRoute) {
    > _remove(route);
    > }
    >
    > @override
    > void didReplace({Route? newRoute, Route? oldRoute}) {
    > if (newRoute != null) {
    > _add(newRoute);
    > }
    > if (oldRoute != null) {
    > _remove(oldRoute);
    > }
    > }
    > ......
    > }
    >
  • 快手 Flutter 上的内存泄漏监控

Leaks是动态的内存泄露检查工具,需要一边运行程序,一边检测。

内存泄漏Leak1

先不切换到Call Trees,先看看Statistics(统计数据)下的情况

内存泄漏Leak2
Allocation中我们主要关注的是Persistent和Persistent Bytes,分别表示当前时间段,申请了但是还没释放的内存数量和大小。

切换到Call Trees后世这样的

内存泄漏Leak3

项目中遇到过的内存泄漏

这里我们不是去查看Call Tree,而是查看Cycles & Roots

循环引用定位1

点击标记4处为黑色,即代表该处会发生内存泄漏,双击进入代码,如图:
循环引用定位2
果然存在内存泄漏

自己遇到的其他例子:

Leak_mine_1
开始不明白为什么这个变量会内存泄漏后面才能白,其实_priceDetailModel这个本身已经用OrderPriceDetailModel赋值过一次了,而这里你又赋值了一次,导致多了一个。代码情况如下两个图:
Leak_mine_2
又在set方法中生成了一个地址
Leak_mine_3

三、ARC下的dealloc

1、ARC下dealloc的使用

ARC下,系统可以帮我们释放该对象,及其包含的对象;但是却无法释放不属于该对象的一些东西,如:

1
2
3
(1)、通知的观察者,或KVO的观察者;
(2)、对象强委托/引用的解除;
(3)、做一些其他的注销之类的操作,如一个ViewController在销毁之前有可能需要和server打交道。
(1)、通知的观察者,或KVO的观察者;

由于通知中心是系统的一个单例,你在注册通知的观察者时,实际上是在通知中心注册的,

这时,即使ARC下系统帮我们释放了对象,但是在通知中心的观察还是没有移除,那么当有

该通知时,依然会尝试调用该对象的接受通知的方法,这可能会导致一些问题.

(2)、对象强委托/引用的解除;

对于其他的对象来把你当做委托 delegate时,并且是 强引用时,即时你自身被释放,但是引用你的对象依然还在,

这时需要在引用你的对象移除该delegate

(3)、做一些其他的注销之类的操作,如一个ViewController在销毁之前有可能需要和server打交道。

一个对象,如一个ViewController在销毁之前有可能需要和server打交道;

这时我们也可以在dealloc中写

2、controller 不能释放,不走dealloc方法的几种可能

主要原因还是循环引用,引起的内存泄漏。

详情参考:controller 不能释放,不走dealloc方法的4种可能

四、Time Profile

< 返回目录

详情参考:instrument Time Profiler总结

使用Time Profile前有两点需要注意的地方:

1
2
1、一定要使用真机调试
2、应用程序一定要使用发布配置

图标为黑色头像的就是Time Profiler给我们的提示,有可能存在性能瓶颈的地方

TimeProfile1

其他

其他参考材料:

问:Xcode 运行程序,左侧memory 不显示内存。。

答:运行程序后,xcode 不显示当前使用的内存情况,问题是打开了僵尸–enable zoombie Objects,关闭即可。
即打开 product—>SCheme–>EditSCheme –>enable zoombie Objects 取消选中 ok

就可以继续显示了

常见笔试/面试题

< 返回目录

END

< 返回目录