iOS基础

iOS-内存管理 堆栈

2018-03-31  本文已影响111人  我是谁重要吗

为什么管理内存:

程序在运行的时候,要创建大量的对象,这些对象放在堆和栈上。(基本类型放在栈上,由系统自动管理。) 而放在堆上的对象如果得不到及时释放,就会占用大量内存。OC中没有垃圾回收机制,所以我们要手动管理内存(ARC之前)
任何继承了NSObject的对象需要进行内存管理

原理:

依赖对象引用计数器+1 -1:在ObjC中对象创建后内部都有一个与之对应的整数(retainCount),叫“引用计数器”,当一个对象在创建之后它的引用计数器为1,当调用这个对象的alloc、retain、new、copy方法之后引用计数器自动在原来的基础上加1(ObjC中调用一个对象的方法就是给这个对象发送一个消息),当调用这个对象的release方法之后它的引用计数器减1,如果一个对象的引用计数器为0,则系统会自动调用这个对象的dealloc方法来销毁这个对象。

内存管理机制:
程序运行过程中要在堆和栈中创建大量的对象,int bool等基本类型放在栈上,由系统自己管理。存储在堆中的对象,系统并不会自动释放堆中的内存。如果一个对象创建并使用后没有得到及时释放那么就会占用大量内存。
内存的管理是依赖对象引用计数器来进行的:在ObjC中每个对象内部都有一个与之对应的整数(retainCount),叫“引用计数器”,当一个对象在创建之后它的引用计数器为1,当调用这个对象的alloc、retain、new、copy方法之后引用计数器自动在原来的基础上加1(ObjC中调用一个对象的方法就是给这个对象发送一个消息),当调用这个对象的release方法之后它的引用计数器减1,如果一个对象的引用计数器为0,则系统会自动调用这个对象的dealloc方法来销毁这个对象。
对于int,bool等基本类型,这些直接在栈上开辟的内存,由系统自己管理,用assign;
对于在堆中开辟的内存,我们需要维护内存的计数器,首次创建内存后,内存计数器+1,当指针A付给指针B的时候,给内存计数器再+1,告诉其他指针,我B也引用了这块内存,这个时候,retain正是做这个工作的,只是oc封装了这个过程。其实copy更好理解,如果指针A和指针B不想相互牵扯,A管理A的内存,B管理B的内存,copy正是为这个而生。
在+和-之间保持平衡,不是所有的成员变量都要设置属性。释放带属性的成员变量的时候,手动设置为nil,用self.属性=nil。否则可能造成野指针错误,而且需要注意在ObjC中给空对象发送消息是不会引起错误的。

原则:

谁创建,谁释放,谁引用,谁管理。

动态内存管理的黄金法则,如果我们alloc/new/retain/copy/mutableCopy了一个对象,那么我们需要对这个对象进行release或autorelease!

自动释放池:

autorelease方法不会改变对象的引用计数器,只是将这个对象放到自动释放池中;

自动释放池(@autoreleaespool)实质是当自动释放池销毁后调用对象的release方法,不一定就能销毁对象(例如如果一个对象的引用计数器>1则此时就无法销毁);

由于自动释放池最后统一销毁对象,因此如果一个操作比较占用内存(对象比较多或者对象占用资源比较多),最好不要放到自动释放池或者考虑放到多个自动释放池;

自动释放池是什么,如何工作

当您向一个对象发送一个autorelease消息时,将该对象的一个引用放入到最新的自动释放池。它仍然是个正当的对象,因此自动释放池定义的作用域内的其它对象可以向它发送消息。当程序执行到作用域结束的位置时,自动释放池就会被释放,池中的所有对象也就被释放。

自动释放池:
如果不是万般无奈的情况下,我建议不使用autorelease构建对象。但是,在一个方法中返回一个对象的指针,这个时候,我们不方便管理方法返回的对象,这种情况,用autorelease是比较明智的选择。
如果不方便管理,那就要用autorelease了。不要轻易把autorelease对象付给retain属性,因为你很有可能忘记给属性设置nil。
向一个对象发送一个autorelease消息时,autorelease方法不会改变对象的引用计数器,只是将这个对象放到自动释放池中,它仍然是个正当的对象,因此自动释放池定义的作用域内的其它对象可以向它发送消息。
自动释放池(@autoreleaespool)实质是当自动释放池销毁后调用对象的release方法,不一定就能销毁对象(例如如果一个对象的引用计数器>1则此时就无法销毁);
由于自动释放池最后统一销毁对象,因此如果一个操作比较占用内存(对象比较多或者对象占用资源比较多),最好不要放到自动释放池或者考虑放到多个自动释放池;
请说出下面代码是否有问题,如果有问题请修改
@autoreleasepool {
for (int i=0; i<largeNumber; i++) {
Person *per = [[Person alloc] init];
[per autorelease];
}
}
内存管理的原则:如果对一个对象使用了alloc、copy、retain,那么你必须使用相应的release或者autorelease。咋一看,这道题目有alloc,也有autorelease,两者对应起来,应该没问题。但autorelease虽然会使引用计数减一,但是它并不是立即减一,它的本质功能只是把对象放到离他最近的自动释放池里。当自动释放池销毁了,才会向自动释放池中的每一个对象发送release消息。这道题的问题就在autorelease。因为largeNumber是一个很大的数,autorelease又不能使引用计数立即减一,所以在循环结束前会造成内存溢出的问题。
解决方案如下:
@autoreleasepool {
for (int i=0; i<largeNumber; i++) {
@autoreleasepool {
Person *per = [[Person alloc] init];
[per autorelease];
}
}
}

在循环内部再加一个自动释放池,这样就能保证每创建一个对象就能及时释放。

什么是ARC:

官方定义:“自动引用计数(ARC)是一个编译器级的功能,它能简化Cocoa应用中对象生命周期管理(内存管理)的流程。”

Automatic Reference Counting,自动引用计数,即ARC

ARC编译器有两部分,分别是前端编译器和优化器

当ARC开启时,编译器将自动在代码合适的地方插入retain, release和autorelease

ARC优化器---当代码中出现多个对 retain 和release的重复调用,ARC优化器负责移出多余的 retain 和release语句。确保生成的代码运行速度高于手动引用计数的代码。

如果涉及到较为底层的东西,比如Core Foundation中的malloc()或者free()等,ARC就鞭长莫及了,这时候还是需要自己手动进行内存管理

关键字:

@property的参数分为三类,也就是说参数最多可以有三个

程序会使用三类中的各个默认参数,默认参数:(atomic,readwrite,assign)

atomic 原子的 对属性加锁,多线程下线程安全,默认属性 加同步 是防止在写未完成的时候被另外一个线程读取,造成数据错误。而这种机制是耗费系统资源的。对于对象的默认属性,就是setter/getter生成的方法是一个原子操作。
如果有多个线程同时调用setter的话,不会出现某一个线程执行setter全部语句之前,另一个线程开始执行setter的情况,相关于方法头尾加了锁一样。

nonatomic 非原子的 对属性不加锁,多线程下不安全,但速度快 不加同步,多线程并发访问会提高性能。非原子操作,就是没有锁,所有的线程都可以一起访问。

readwrite 默认属性,将生成不带额外参数的getter和setter方法(setter方法只有一个参数)。

readonly 将只生成getter方法而不生成setter方法(getter方法没有get前缀)。

assign 基本的赋值、直接赋值,默认值 不更改索引计数(Reference Counting).使用assign: 对基础数据类型 (NSInteger)和C数据类型(int, float, double, char,等)

retain 让对象引用计数+1,表示拥有这个对象,先release原来的值,在retain新值

copy 先release原来的值,在copy新值 建立一个索引计数为1的对象,然后释放旧对象

retain是指针拷贝,copy是内容拷贝。(可以理解为retain是浅拷贝,copy是深拷贝。)

strong 强引用,strong关键字与retain关似,用了它,引用计数自动+1,有strong指向的对象不会被释放

weak 弱引用,声明为weak的指针,指针指向的地址一旦被释放,这些指针都将被赋值为nil。这样的好处能有效的防止野指针。声明了一个可以自动 nil 化的弱引用,当weak指向的内存释放掉后自动置为nil,防止野指针。

unsafe_unretained声明的指针,由于 self.string1=nil已将内存释放掉了,但是string2并不知道已被释放了,所以是野指针。然后访问野指针的内存就造成crash. 声明一个弱应用,但是不会自动nil化,也就是说,如果所指向的内存区域被释放了,这个指针就是一个野指针了。

assign,用于基本数据类型

retain,通常用于非字符串对象 对其他NSObject和其子类

copy,通常用于字符串对象、block、NSArray、NSDictionary
copy是创建一个新对象,retain是创建一个指针,引用对象计数加1

strong强引用也就是我们通常所讲的引用,其存亡直接决定了所指对象的存亡。如果不存在指向一个对象的引用,并且此对象不再显示列表中,则此对象会被从内存中释放。

weak弱引用除了不决定对象的存亡外,其他与强引用相同。即使一个对象被持有无数个若引用,只要没有强引用指向他,那么其还是会被清除。

什么情况下使用weak关键字,相比assign有什么不同?
1) 在ARC中,在有可能出现循环引用的时候,往往要通过让其中一端使用weak来解决。比如delegate代理
2) 自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用weak,自定义控件属性一般也使用weak。
不同点:
1)weak此特质表明该属性定义了一种“非拥有关系”。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特性与assign一样,不同在于它修饰的属性对象遭到推毁时,属性值也会清空。而assign的“设置方法”只会执行针对“纯量类型” (scalar type,例如 CGFloat 或 NSlnteger 等)的简单赋值操作。
2)assign可以用非OC对象,而weak必须用于OC对象。

static 关键字的作用:

static 一般情况下,只能用NSString或者基本类型 都是私有的,通常在单例中使用

函数体内 static 变量的作用范围为该函数体,不同于 auto 变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
在C语言中,
关键字static有三个明显的作用:作用域只限在本地范围内使用。

1). 在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。

2). 在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。

3). 在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。

关键字const有什么含意?修饰类呢?static的作用,用于类呢?还有extern c的作用

const 意味着"只读", 就是被修饰的不可以再修改!
下面的声明都是什么意思?

1 const int a;

2 int const a;

3 const int *a;

4 int * const a;

5 int const * a const;

前两个的作用是一样,a是一个常整型数。

第三个意味着a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。

第四个意思a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。

最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。

结论:

关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const的程序员很少会留下的垃圾让别人 来清理的。) 通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。
合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出 现。

(1)欲阻止一个变量被改变,可以使用 const 关键字。在定义该 const 变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;

(2)对指针来说,可以指定指针本身为 const,也可以指定指针所指的数据为 const,或二者同时指定为 const;

(3)在一个函数声明中,const 可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;

(4)对于类的成员函数,若指定其为 const 类型,则表明其是一个常函数,不能修改类的成员变量;

(5)对于类的成员函数,有时候必须指定其返回值为 const 类型,以使得其返回值不为“左值”。

volatile有什么含意?并给出三个不同的例子。

一个定义为 volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。

精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。

下面是volatile变量的几个例子:

并行设备的硬件寄存器(如:状态寄存器)

一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)

多线程应用中被几个任务共享的变量

ios的@property属性和@synthesize属性:

//当编译器遇到@property时,会自动展开成getter和setter的声明

@property int age;

@property int no;

//@synthesize 会自动生成getter和setter的实现

//@synthesize 默认会去访问age,no,height同名的变量,

//如果找不到同名的变量,会在内部自动生成一个私有同名变量age,no,height,,

//因此Student.h 中的这几个变量也可以省略不写。

@synthesize age,no;

1.在Xcode4.5及以后的版本中,可以省略@synthesize ,编译器会自动帮你加上getter 和 setter 方法的实现,并且默认会去访问_age这个成员变量,如果找不到_age这个成员变量,会自动生成一个叫做 _age的私有成员变量。

ios中的成员变量定义在@interface 和@implementation 中的区别是什么?

定义在@interface中是指定义在头文件里, 定义在@implementation中是指在实现文件中的类扩展(Class Extensions), 一般来说把要公开的信息(变量,属性,方法)定义在头文件里, 把要隐藏的信息定义在类扩展里,只是为了隐藏私有信息, 不需要被外界知道的就不要放在头文件里, 这样可以隔离接口和实现。

堆和栈?

对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来讲,释放工作有程序员控制,容易产生memory Leak。

1.申请方式:stack:由系统自动分配。heap:需要程序员自己申请。

2.申请后系统的响应

栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,

3.申请大小的限制:

栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,大小有限制。

堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址

4.申请效率的比较:

栈由系统自动分配,速度较快。

堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,

5.堆和栈中的存储内容

6.存取效率的比较:

栈的效率比较高

堆和栈的区别?

堆是程序员控制的,栈是编辑器自动管理,栈是一块连续的内存区域,是向低地址扩展的数据结构,堆是向高低直落站的数据结构。

管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。

申请大小:

栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

碎片问题:
对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出

分配方式:
堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

分配效率:
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的。

MRC下内存管理的缺点:
释放一个堆内存时,首先要确定指向这个堆空间的指针都被release了。(避免提前释放)
释放指针指向的堆空间,首先要确定哪些指向同一个堆,这些指针只能释放一次。(避免释放多次,造成内存泄露)
模块化操作时,对象可能被多个模块创建和使用,不能确定最后由谁释放
多线程操作时,不确定哪个线程最后使用完毕。

虽然ARC给我们编程带来的很多好多,但也可能出现内存泄露。如下面两种情况:
循环参照: A有个属性参照B,B有个属性参照A,如果都是strong参照的话,两个对象都无法释放。
死循环: 如果有个ViewController中有无限循环,也会导致即使ViewController对应的view消失了,ViewController也不能释放。

什么情况下会发生内存泄漏和内存溢出?
当程序在申请内存后,无法释放已申请的内存空间(例如一个对象或者变量使用完成后没有释放,这个对象一直占用着内存),一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。内存泄露会最终会导致内存溢出!
当程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个int,但给它存了long才能存下的数,那就是内存溢出。

runloop、autorelease pool以及线程之间的关系。
每个线程(包含主线程)都有一个Runloop。对于每一个Runloop,系统会隐式创建一个Autorelease pool,这样所有的release pool会构成一个像callstack一样的一个栈式结构,在每一个Runloop结束时,当前栈顶的Autorelease pool会被销毁,这样这个pool里的每个Object会被release。

上一篇下一篇

猜你喜欢

热点阅读