优化

iOS 内存管理相关

2021-04-14  本文已影响0人  学不来的凡人

内存管理的一些概念

1.1 为什么要使用内存管理?

严格的内存管理,能够是我们的应用程在性能上有很大的提高

如果忽略内存管理,可能导致应用占用内存过高,导致程序崩溃

1.2 OC的内存管理主要有三种方式:

ARC(自动内存计数)

手动内存计数

内存池

1.3 OC中内存管理的基本思想:

保证任何时候指向对象的指针个数和对象的引用计数相同,多一个指针指向这个对象这个对象的引用计数就加1,少一个指针指向这个对象这个对象的引用计数就减1。没有指针指向这个对象对象就被释放了。

每个对象都有一个引用计数器,每个新对象的计数器是1,当对象的计数器减为0时,就会被销毁
通过retain可以让对象的计数器+1、release可以让对象的计数器-1
还可以通过autorelease pool管理内存
如果用ARC,编译器会自动生成管理内存的代码
1.4 苹果官方基础内存管理规则:

你拥有你创建的任何对象
你可以使用retain获取一个对象的拥有权
当你不再需要它,你必须放弃你拥有的对象的拥有权
你一定不能释放不是你拥有的对象的拥有权
自动内存管理

谈谈你对 ARC 的认识和理解? ARC 是iOS 5推出的新功能。编译器在代码里适当的地方自动插入 retain / release 完成内存管理(引用计数)。
ARC机制中,系统判断对象是否被销毁的依据是什么?
指向对象的强指针是否被销毁
引用计数器

给对象发送一条retain消息,可以使引用计数器+1(retain方法返回对象本身)
给对象发送一条release消息,可以使引用计数器-1(注意release并不代表销毁/回收对象,仅仅是计数器-1)
给对象发送retainCount消息,可以获得当前的引用计数值
简单易用的标题

2.1 自动释放池底层怎么实现?

(以栈的方式实现的)(系统自动创建,系统自动释放)栈里面的(先进后出)
内存里面有栈,栈里面有自动释放池。
自动释放池以栈的形式实现:当你创建一个新的自动释放池时,它将被添加到栈顶。当一个对象收到发送autorelease消息时,它被添加到当前线程的处于栈顶的自动释放池中,当自动释放池被回收时,它们从栈中被删除,并且会给池子里面所有的对象都会做一次release操作。

2.2 什么是自动释放池?

答:自动释放池是用来存储多个对象类型的指针变量

2.3 自动释放池对池内对象的作用?

被存入到自动释放池内的对象,当自动释放池被销毁时,会对池内的对象全部做一次release操作

2.4 对象如何放入到自动释放池中?

当你确定要将对象放入到池中的时候,只需要调用对象的 autorelease 对象方法就可以把对象放入到自动释放池中

2.5 多次调用对象的autorelease方法会导致什么问题?
答:多次将地址存到自动释放池中,导致野指针异常

2.6 自动释放池作用
将对象与自动释放池建立关系,池子内调用 autorelease 方法,在自动释放池销毁时销毁对象,延迟 release 销毁时间

2.7 自动释放池,什么时候创建?

程序刚启动的时候,也会创建一个自动释放池

产生事件以后,运行循环开始处理事件,就会创建自动释放池

2.8 什么时候销毁的?

程序运行结束之前销毁

事件处理结束以后,会销毁自动释放池

还有在池子满的时候,也会销毁

2.9 自动释放池使用注意:
不要把大量循环操作放在释放池下,因为这会导致大量循环内的对象没有被回收,这种情况下应该手动写 release 代码。尽量避免对大内存对象使用 autorelease ,否则会延迟大内存的回收。

2.10 autorelease的对象是在什么时候被release的?

答:autorelease实际上只是把对release的调用延迟了,对于每一个Autorelease,系统只是把该Object放入了当前的 Autoreleasepool中,当该pool被释放时,该pool中的所有Object会被调用Release。对于每一个Runloop,系统会隐式创建一个Autoreleasepool,这样所有的releasepool会构成一个象CallStack一样的一个栈式结构,在每一个 Runloop结束时,当前栈顶的Autoreleasepool会被销毁,这样这个pool里的每个Object(就是autorelease的对象)会被release。那什么是一个Runloop呢?一个UI事件,Timer call,delegate call, 都会是一个新的Runloop。

2.11 If we don’t create any autorelease pool in our application then is there any autorelease pool already provided to us?
系统会默认会不定时地创建和销毁自动释放池

2.12 When you will create an autorelease pool in your application?
当不需要精确地控制对象的释放时间时,可以手动创建自动释放池

@property内存管理策略的选择

读写属性:readwrite 、readonly
setter语意:assign 、retain / copy
原子性(多线程管理):atomic 、 nonatomic
强弱引用:strong 、 weak

3.1 读写属性:

readwrite :同时生成 set 和 get 方法(默认)
readonly :只会生成 get 方法

3.2 控制set方法的内存管理:

retain:release 旧值,retain 新值。希望获得源对象的所有权时,对其他 NSObject 和其子类(用于 OC 对象)
copy :release 旧值,copy 新值。希望获得源对象的副本而不改变源对象内容时(一般用于 NSString ,block )
assign :直接赋值,不做任何内存管理(默认属性),控制需不需生成 set 方法。对基础数据类型 (NSInteger ,CGFloat )和C数据类型(int , float , double , char , 等等)

3.3 原子性(多线程管理):

atomic
默认属性,访问方法都为原子型事务访问。锁被加到所属对象实例级,性能低。原子性就是说一个操作不可以中途被 cpu 暂停然后调度, 即不能被中断, 要不就执行完, 要不就不执行. 如果一个操作是原子性的,那么在多线程环境下, 就不会出现变量被修改等奇怪的问题。原子操作就是不可再分的操作,在多线程程序中原子操作是一个非常重要的概念,它常常用来实现一些同步机制,同时也是一些常见的多线程 Bug 的源头。当然,原子性的变量在执行效率上要低些。
nonatomic
非原子性访问。不加同步,尽量避免多线程抢夺同一块资源。是直接从内存中取数值,因为它是从内存中取得数据,它并没有一个加锁的保护来用于cpu中的寄存器计算Value,它只是单纯的从内存地址中,当前的内存存储的数据结果来进行使用。多线程并发访问会提高性能,但无法保证数据同步。尽量避免多线程抢夺同一块资源,否则尽量将加锁资源抢夺的业务逻辑交给服务器处理,减少移动客户端的压力。
当有多个线程需要访问到同一个数据时,OC中,我们可以使用 @synchronized (变量)来对该变量进行加锁(加锁的目的常常是为了同步或保证原子操作)。
3.4 强指针(strong)、弱指针(weak)

strong
strong 系统一般不会自动释放,在 oc 中,对象默认为强指针。作用域销毁时销毁引用。在实际开放中一般属性对象一般 strong 来修饰(NSArray,NSDictionary),在使用懒加载定义控件的时候,一般也用strong。
weak
weak 所引用对象的计数器不会加一,当对象被释放时指针会被自动赋值为 nil,系统会立刻释放对象。
__unsafe_unretained 弱引用 当对象被释放时指针不会被自动赋值为 ni
在ARC时属性的修饰符是可以用 assign 的(相当于 __unsafe_unretained)
在ARC时属性的修饰符是可以用 retain 的 (相当于 __strong)
假定有N个指针指向同一个对象,如果至少有一个是强引用,这个对象只要还在作用域内就不会被释放。相反,如果这N个指针都是弱引用,这个对象马上就被释放
在使用 sb 或者 xib 给控件拖线的时候,为什么拖出来的先属性都是用 weak 修饰呢?
由于在向 xib 或者 sb 里面添加控件的时候,添加的子视图是添加到了跟视图 View 上面,而 控制器 Controller 对其根视图 View 默认是强引用的,当我们的子控件添加到 view 上面的时候,self.view addSubView: 这个方法会对添加的控件进行强引用,如果在用 strong 对添加的子控件进行修饰的话,相当于有两条强指针对子控件进行强引用, 为了避免这种情况,所以用 weak 修饰。
注意:
(1)addSubView 默认对其 subView 进行了强引用
(2)在纯手码实现界面布局时,如果通过懒加载处理界面控件,需要使用strong强指针
ARC管理内存是用 assign 还是用 weak ?
assign : 如果由于某些原因代理对象被释放了,代理指针就变成了野指针。
weak : 如果由于某些原因代理对象被释放了,代理指针就变成了空指针,更安全(weak 不能修饰基本数据类型,只能修饰对象)。
内存分析

静态分析(Analyze)
不运行程序, 直接检测代码中是否有潜在的内存问题(不一定百分百准确, 仅仅是提供建议)
结合实际情况来分析, 是否真的有内存问题
动态分析(Profile == Instruments)
运行程序, 通过使用app,查看内存的分配情况(Allocations):可以查看做出了某个操作后(比如点击了某个按钮\显示了某个控制器),内存是否有暴增的情况(突然变化)
运行程序, 通过使用app, 查看是否有内存泄漏(Leaks):红色区域代表内存泄漏出现的地方
什么情况下会发生内存泄漏和内存溢出?

内存泄漏:堆里不再使用的对象没有被销毁,依然占据着内存。
内存溢出:一次内存泄露危害可以忽略,但内存泄露多了,内存迟早会被占光,最终会导致内存溢出!当程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如数据长度比较小的数据类型 存储了数据长度比较大的数据。

关于图片占用内存管理

4.1 图片加载占用内存对比

使用 imageName: 加载图片:
加载到内存当中后,占据内存空间较大
相同的图片,图片不会重复加载
加载内存当中之后,会一直停留在内存当中,不会随着对象销毁而销毁
加载进去图片之后,占用的内存归系统管理,我们无法管理
使用 imageWithContentsOfFile: 加载图片
加载到内存当中后,占据内存空间较小
相同的图片会被重复加载内存当中
对象销毁的时候,加载到内存中图片会随着一起销毁
结论:
图片较小,并且使用频繁,使用 imageName: 来加载(按钮图标/主页里面图片)
图片较大,并且使用较少,使用 imageWithContentsOfFile: 来加载(版本新特性/相册)
4.2 图片在沙盒中的存在形式

部署版本在>=iOS8的时候,打包的资源包中的图片会被放到Assets.car。图片有被压缩;
部署版本在<iOS8的时候,打包的资源包中的图片会被放在MainBudnle里面。图片没有被压缩
没有放在Images.xcassets里面的所有图片会直接暴露在沙盒的资源包(main Bundle), 不会压缩到Assets.car文件,会被放到MainBudnle里面。图片没有被压缩
结论:
小图片\使用频率比较高的图片放在Images.xcassets里面
大图片\使用频率比较低的图片(一次性的图片, 比如版本新特性的图片)不要放在Images.xcassets里面
内存管理问题

5.1 单个对象内存管理的问题

关于内存我们主要研究的问题是什么? 野指针:对象的retainCount已经为0,保存了对象指针地址的变量就是野指针。使用野指针调用对象的方法,会导致野指针异常,导致程序直接崩溃
内存泄露:已经不在使用的对象,没有正确的释放掉,一直驻留在内存中,我们就说是内存泄漏
僵尸对象? retainCount = 0的对象被称之为僵尸对象,也就是不能够在访问的对象
是什么问题导致,访问僵尸对象,时而正确时而错误?
如何开始xcode的时时检测僵尸对象功能?
当对象的retainCount = 0 时 能否调用 retain方法使对象复活? 已经被释放的对象是无法在复活的
如何防止出现野指针操作? 通常在调用完release方法后,会把保存了对象指针地址的变量清空,赋值为nil 在oc中没有空指针异常,所以使用[nil retain]调用方法不会导致异常的发生
内存泄漏有几种情况?
没有配对释放,不符合内存管理原则
对象提前赋值为nil或者清空,导致release方法没有起作用
5.2 多个对象内存管理的问题

对象与对象之间存在几种关系?
继承关系
组合关系
对象作为方法参数传递
对象的组合关系中,如何确保作为成员变量的对象,不会被提前释放? 重写set方法,在set方法中,retain该对像,使其retainCount值增加 1
组合关系导致内存泄漏的原因是什么? 在set方法中,retain了该对象,但是并没有配对释放
作为成员变量的对象,应该在那里配对释放? 在dealloc函数中释放
内存相关的一些数据结构的对比

6.1 简述内存分区情况

代码区:存放函数二进制代码
数据区:系统运行时申请内存并初始化,系统退出时由系统释放。存放全局变量、静态变量、常量
堆区:通过malloc等函数或new等操作符动态申请得到,需程序员手动申请和释放
栈区:函数模块内申请,函数结束时由系统自动释放。存放局部变量、函数参数
6.2 手机的存储空间分为内存(RAM)和闪存(Flash)两种

内存一般较小:1G、2G、3G、4G。闪存空间相对较大16G、32G、64G;
内存的读写速度较快、闪存的读写速度相对较慢;
内存里的东西掉电后全部丢失、闪存里的东西掉电也不丢;
内存相当于电脑的内存条、闪存相当于电脑的硬盘;
6.3 堆和栈的区别?

管理方式:
堆释放工作由程序员控制,容易产生memory leak;
栈是由编译器自动管理,无需我们手工控制。
申请大小:
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 Windows下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
碎片问题:
堆:频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。
栈:则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出
分配方式:
堆都是动态分配的,没有静态分配的堆。
栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloc函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
分配效率:
栈:是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
堆:则是C/C++函数库提供的,它的机制是很复杂的。
每个App有个内存空间,假定是4G,分为堆和栈两大部分。一般来说每个进程有一个堆(这个进程的所有线程共用这个堆),进程中的线程有自己栈。
通过alloc、new或malloc获得的内存在堆中分配,堆中的内存需要写相应的代码释放。如果进程结束了在堆中分配的内存会自动释放。
局部变量、函数参数是在栈空间中分配,如果函数返回这个函数中的局部变量、参数所占的内存系统自动释放(回收)。
程序在编译期对变量和函数分配内存都在栈上进行,且程序运行过程中函数调用时参数的传递也在栈上进行。
队列和栈有什么区别:
队列和栈是两种不同的数据容器。从”数据结构”的角度看,它们都是线性结构,即数据元素之间的关系相同。
队列是一种先进先出的数据结构,它在两端进行操作,一端进行入队列操作,一端进行出列队操作。
栈是一种先进后出的数据结构,它只能在栈顶进行操作,入栈和出栈都在栈顶操作。
链表和数组的区别在哪里?
二者都属于一种数据结构。如果需要快速访问数据,很少或不插入和删除元素,就应该用数组;相反, 如果需要经常插入和删除元素就需要用链表数据结构。
从逻辑结构来看
数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况。当数据增加时,可能超出原先定义的元素个数;当数据减少时,造成内存浪费;数组可以根据下标直接存取。
链表动态地进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。(数组中插入、删除数据项时,需要移动其它数据项,非常繁琐)链表必须根据next指针找到下一个元素
从内存存储来看
数组从栈中分配空间,对于程序员方便快速,但是自由度小
链表从堆中分配空间, 自由度大但是申请管理比较麻烦
面试题

如何让程序尽量减少内存泄漏
非ARC
Foundation 对象( OC 对象) : 只要方法中包含了 alloc\new\copy\mutableCopy\retain 等关键字,那么这些方法产生的对象, 就必须在不再使用的时候调用1次 release 或者1次 autorelease。
CoreFoundation 对象( C 对象) : 只要函数中包含了 create\new\copy\retain 等关键字, 那么这些方法产生的对象, 就必须在不再使用的时候调用1次 CFRelease 或者其他 release 函数。
ARC(只自动管理OC对象, 不会自动管理C语言对象)
CoreFoundation 对象( C 对象) : 只要函数中包含了 create\new\copy\retain 等关键字, 那么这些方法产生的对象, 就必须在不再使用的时候调用1次 CFRelease 或者其他 release 函数。
block的注意
// block的内存默认在栈里面(系统自动管理)
void (^test)() = ^{

};
// 如果对block进行了Copy操作, block的内存会迁移到堆里面(需要通过代码管理内存)
Block_copy(test);
// 在不需要使用block的时候, 应该做1次release操作
Block_release(test);
[test release];
野指针举例
建了个视图控制器(ARC时)某个函数里写了如下代码。当这个函数返回时因为没有指针指向b所以b会被释放、但是b.view不会被释放。如果在b里有需要操作b的地方(比如代理的方法),就会产生野指针(提前释放)
B *b = [[B alloc]init];
[self.view addSubview:b.view];
set方法
在对象的组合关系中,导致内存泄漏有几种情况? 1.set方法中没有retain对象 2.没有release掉旧的对象 3.没有判断向set方法中传入的是否是同一个对象
该如何正确的重写set方法? 1.先判断是否是同一个对象 2.release一次旧的对象 3.retain新的对象
写一个setter方法用于完成@property (nonatomic,retain)NSString *name,
写一个setter方法用于完成@property(nonatomic,copy)NSString *name。
@property (nonatomic, retain) NSString *name;

@property(nonatomic, copy) NSString *name;

// [0 setAge:40];

    // message sent to deallocated instance 0x100201950
    // 给空指针发消息不会报错
    [p release];
}
return 0;

}
堆和栈

import <Foundation/Foundation.h>

import "Car.h"

int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10; // 栈

    int b = 20; // 栈
    
    // c : 栈
    // Car对象(计数器==1) : 堆
    Car *c = [[Car alloc] init];
}

// 当autoreleasepool执行完后后, 栈里面的变量a\b\c都会被回收
// 但是堆里面的Car对象还会留在内存中, 因为它是计数器依然是1

return 0;

}
看下面的程序,三次NSLog会输出什么?为什么?
结果:3、2、1
NSMutableArray* ary = [[NSMutableArray array] retain];
NSString *str = [NSString stringWithFormat:@"test"]; // 1
[str retain]; // 2
[ary addObject:str]; // 3
NSLog(@"%d", [str retainCount]);
[str retain]; // 4
[str release]; // 3
[str release]; // 2
NSLog(@"%d", [str retainCount]);
[ary removeAllObjects]; // 1
NSLog(@"%d", [str retainCount]);
[NSArray arrayWithobject:]后需要对这个数组做释放操作吗?
答:不需要,这个对象被放到自动释放池中
老版本的工程是可以转换成使用ARC的工程,转换规则包括:
去掉所有的retain,release,autorelease
把NSAutoRelease替换成@autoreleasepool{}块
把assign的属性变为weak使用ARC的一些强制规定
dealloc方法来管理一些资源,但不能用来释放实例变量,也不能在dealloc方法里面去掉[super dealloc]方法,在ARC下父类的dealloc同样由编译器来自动完成
Core Foundation类型的对象任然可以用CFRetain,CFRelease这些方法
不能在使用NSAllocateObject和NSDeallocateObject对象
不能在c结构体中使用对象指针,如果有类似功能可以创建一个Objective-c类来管理这些对象
在id和void *之间没有简便的转换方法,同样在Objective-c和core Foundation类型之间的转换都需要使用编译器制定的转换函数
不能使用内存存储区(不能再使用NSZone)
不能以new为开头给一个属性命名
声明outlet时一般应当使用weak,除了对StoryBoard,这样nib中间的顶层对象要用strong
weak 相当于老版本的assign,strong相当于retain

上一篇下一篇

猜你喜欢

热点阅读