OC课程笔记总结10-内存管理1:内存管理基础

课程:内存管理


内存管理是OC中很重要的环节。一定要防止内存泄漏。

最新的ARC机制,可以自动管理内存。但是必须首先能够手动管理内存。如果程序占用的内存过多,系统会发出警告,并且可能会有闪退现象。

内存管理的管理范围是:任何继承了 NSObject 类型的对象都需要进行内存管理,而对于普通的基本数据类型,系统会自动回收,并不需要对基本数据类型占用的内存进行管理,而对象是不会进行自动回收的,

这是由于对象和基本数据类型在内存中存储的方式不同。对于局部变量,程序一执行完毕就会自动回收。OC 系统中共有五大存储空间,其中两大空间是栈,堆。栈中存放局部变量,而堆中存放的是动态产生的数据,

例如对象就是动态产生的。指向对象的指针是一种局部变量,指针存放在栈中。指针中的地址指向堆内存中的地址。当代码块执行完成后,当中的局部变量都会回收,即栈内存中的变量都会释放。但是堆内存中的数据是不会

自动释放的,而内存泄漏就是堆内存中不需要的数据没有被及时释放所引起的,所以对于堆内存中的数据需要程序员手动进行回收。java 中有垃圾自动回收机制。oc 中没有这样的机制,必须通过代码来进行回收,通过调用

对象的某个方法才能将对象数据回收释放。


引用计数器:

每个对象都有自己的引用计数器,这个计数器是一个整数,表示这个对象被引用了多少次。当引用计数器为0时,表示不再被引用了,那么这时候就可以对这个对象进行回收了。引用计数器的本质是一个整数,在内存中占

4个字节。引用计数器是放在对象的内部的,每个对象内部的存储空间中都有4个字节是用于存放引用计数器。对于新创建的对象,默认的引用计数器的值是 1 。通过操作引用计数器,来实现对于对象是否回收的判断,进而实现管理

内存的功能。当使用 alloc和 new 还有copy 方法时,产生的新对象的默认引用计数器值是1,判断一个对象是否需要被回收的唯一标准就是引用计数器是否为0,只要为0,就回收,如果不为0,就不应该回收。当给对象发送一条

retain 消息时,给对象发送消息即是指让对象调用某个方法,引用计数器就会+1,若发送的是 release,则计数器会 -1,而 retainCount 这个消息会让对象返回当前引用计数器的值,返回值类型是 NSUInterger,如果要输出打印这个值,

用%ld(长整形)接收。其实也相当于是 int 类型。release 操作是没有返回值的。当引用计数器为0,对象被销毁时,系统会自动想对象发送一条 dealloc 消息,让其释放所占的内存(alloc 消息是给对象在堆中分配内存),实际开发中都会重新 delloc 方法,

定义对象释放之前所要完成的相应的行为。可以理解为对象的遗言,在消失之前告诉程序要怎么怎么样。只要对象被回收,系统都会自动执行 dealloc 方法。如果重写了 dealloc 方法后,必须调用  [super dealloc] 这一父类方法,并且要将这一方法

放在重写的 dealloc 方法代码中的最后一行执行,原理类似于[super init ],先初始化父类属性再初始化子类自己的属性。在新版本的 xcode 中需要手动关闭 ARC 机制,可以按照下图来进行设置:

20140319231459687.jpg


对象被回收,有两种情况,一种是手动编写代码回收,另一种情况是程序退出时。而在实际的ios 中,main 函数是一个死循环函数,不手动退出的话这个程序会一直运行。而 main 函数不返回值给系统,那么这个程序就会一直执行。

调用 release 方法可以将对象的计数器值-1,而调用 retain 方法可以+1,并且retain 方法有返回值,哪个对象方法就返回哪个对象,return self。一个计数器为0 的对象被释放后,如果再进行计数器-1的操作,那么系统会报错的,

因为此时并不是因为程序执行完毕之后才释放这个对象内存的,而是手动释放的,这个时候,指向这个对象的指针变量还没有被释放,因为程序还没执行完毕,在通过这个指针指向的地址进行计数器-1操作是会产生错误的。

然而在计数器为0之后,这个对象会变成僵尸对象(不可用内存),指向僵尸对象的指针称为“野指针”。对僵尸对象进行操作系统回报错,EXC_BAD _ACCESS:坏访问错误,野称为野指针错误,表示访问了一个无法访问的内存。在实际手机

程序的表现为 闪退,点击屏幕几下就会出现程序闪退。

为了防止野指针错误,应该把指针清空 :把 nil 赋值给指针。在 oc 中不存在空指针错误,即对一个空指针发送消息是允许的,而在 java 中会有空指针错误。只要对象计数器为0后,这个对象就会变成僵尸对象,无法再对这个内存进行任何操

作,包括 retain 操作,本意是将这个对象的计数器值从0变为1,但实际是不起作用的,因为对于一个僵尸对象而言,不论什么操作都不会再起作用。一旦成为僵尸,就不可逆转。

计数器管理总结:

1.retain :计数器+1,并且返回当前对象;

2.release : 计数器-1.没有返回值;

3.retainCount :返回当前计数器的值,返回值类型是 NSUInteger,相当于 int;

4.dealloc :当对象将要被回收时,系统会自动调用这个方法,如果重写了默认的 dealloc 方法,一定要在这个方法代码中的最后一行写上【super dealloc】,不然也会发生内存错误。猜想:其实 dealloc 方法应该是删除了某些属性,类似于 init 会

初始化某些属性,先初始化父类属性,在初始化子类自己的属性,先删除子类自己 的属性,再删除父类的属性,而对象的方法都是存放在对象中的,因此不用考虑方法的删除。

5.僵尸对象:所占用的内存已近被回收的对象,对于僵尸内存不能再进行任何操作。

6.野指针:指向僵尸对象的指针,或说是指向不可用内存的指针,给野指针发送消息会报错,EXC_BAD_ACCESS.

7.空指针:指针所存储的地址值为 nil 的指针,在 oc 中不会报错,而在 java 中会报错。在 oc 中对空指针发送消息是不会报错的。


单个对象的内存管理是简单的,但是当涉及到多个对象,并且多个对象之间相互有联系的时候,内存管理会变得极为麻烦。一个 IOS 程序会有非常多的对象。并且一些对象的属性是另外的对象。对于对象是否被释放,记住只有一个标准,那就是

计数器的值是否为0,所以,在多对象间产生相互关系时,需要对计数器进行操作,在解除和对象的关系时,也要堆计数器进行操作,从而确保计数器的值是正确对应引用关系的。即:引用别的对象的对象(简称为1),被引用的对象(简称为2)

对象1要引用对象2时,对象1必须完成一个操作,就是让对象2的引用计数器加1,当对象1不再需要引用对象2时,对象1也必须完成另一个操作,就是将对象2的引用计数器减1,即对对象的计数器进行操作的,一般来说都应该是其他的对象。



谁创建了某个新对象,就由谁对这个对象进行 release 或者 autorelease 操作。并且谁 retain 了某个对象,也要由谁来对这个对象进行 release 操作。有始有终,有加有减,谁对对象进行引用,谁就要对这个引用行为负责。

谁让对象的引用计数器+1,谁就必须对这个计数器进行-1操作。只要出现了 alloc或者 new 或者 copy ,就必须有一个对应的 release,只要有 retain,野同样必须有对应的 release。 而所有的 release 都是指不再需要这个对象之后才 

进行的,并不是说 alloc 之后要马上 release,而是说要记得有一个release。同时还应该对指针进行处理,将 nil 值赋给相应的指针,即对指针进行清空操作。一套操作要记得,不要漏掉对指针的清零操作。  

对于定义方法而言,直接赋值可能也会造成内存泄露。因为如果传入的参数是对象,那么新建一个对象之后必须要对这个对象进行释放操作,为了更好的进行内存管理,在变写代码时如果需要传入一个对象参数,那么在

编写赋值操作时应该这样写 :_指针名 = 【传入对象 retain】,右边的方法会返回一个 self,然后赋值给指针变量,完成参数的传递,同时还完成了对象计数器加1的操作。对别的对象进行 release 的操作可以写在本类的 dealloc 方法中,只要

本类对象被回收了,那么本类对象都会对所引用过的其他对象进行一个 release 操作,来确保有始有终,有加有减。这也是为什么要把【super dealloc】放在最后一句执行的原因(alloc = allocate :分配,dealloc = 解除分配)。


而在对 set 方法的设计上时,也能做的更严谨一些,更利于内存的管理。主要的问题是在当将另一个对象赋值给参数时,即更新了参数的值,但是原来指向的对象并没有进行 release 处理,因为 release 是依附于另一个方法来执行的,

这样代码之间的耦合性较高,会出现问题,记住一定要降低代码的耦合性。改进方法就是对 set 方法增加一个对就对象的 release 操作,只要调用一次 set 方法,这意味着‘引用一个对象,那么不管这个对象是新对象还是旧对象,都认为是新引用了

一个对象,那么对于旧的对象,应该进行一次 release 操作,在 set 方法中加上这么一句:【-对象名 release】,表示对原来的引用的对象进行 release 操作,如果是第一次使用 set 方法,那么这时候的指针是初始化的默认值nil,没有指向,在 oc

中对一个指向空的指针发动消息是不会有影响的,因此,加上这一句后代码会更加严谨。但是,仍然不够严谨,如果两次都传入同一个对象作为参数的话,那么这个对象在第二次传入时就会计数器的值就会先变为0,然后再传递给参数,而这个时候,

这个对象就已经是僵尸对象了,如下p.name = str1 ;在传入一次,p.name = str1;这个时候就会发生错误,这时候需要堆 release 操作加上一个判断条件,如果传入的不是同一个对象,才对当前对象的引用计数器进行release 操作。即

 if (当前指针指向对象!=参数指针指向对象)

{

[对象 release]

对象属性 = [传入对象 retain]

}

加入这个代码表示如果传入的对象是同一个对象,那么计数器即不+1也不-1(如果没有这个语句,那么可能会多次+1,也可能多次-1,所以+1-1的操作都应该括起来,都应该预先判读),如果传入的不是同一个对象,那么就会让前一个对象计数器-1,

并且新传入的对象的计数器+1,这样的代码才是最严谨的。

之所以 新建一个对象是引用计数器值为1,是因为新建的对象必须由一个指针指向这个对象,即有第一个引用,当将这个对象传递给另外的对象作为成员变量时,传递的是地址的值,即有两个不同的指针,指向了同一个对象,这表示这个对象就有

两个引用,被引用了两次,计数器的值为2.


内存管理代码规范总结:

1.只要有引用,只要有 alloc,就必须有 release (或者 autorelease)

2.set 方法的代码规范:对于基本数据类型,不用进行内存管理,对于对象类型,首先要进行传入参数的判断,如果是新对象,那么要对旧对象-1操作,并且新对象+1操作,如果是同一个对象,则不进行任何堆引用计数器的操作。

3.dealloc 的代码规范:一定要调用[super dealloc]的方法,并且放在最后执行。还要对当前所引用的全部对象(所有的成员属性指向的对象)都进行-1操作。只要有成员变量的其他对象的,那么就必须重写 dealloc 方法,这样才能在这个对象

释放时对其所引用的全部对象进行-1操作,这样才能正确管理内存。

4.要注意的是在对象中有一个特例,就是 NSString 类的字符串对象@”XXXXXX”;对于OC 中字符串对象在创建时,并没有使用到 alloc 来进行创建,而是直接将字符串赋值给NSString 的指针,所以,如果是对于字符串对象,是不用再手动编写。其实在项目中,例如图片,手机号码等内容也可以看成是一种字符串类,将图片地址当成一个字符串传递就可以下载到这个图片,将手机号码当成字符串就可以存储更多位的数字,如果将手机号码当初 int 类型来存储的话,可能会超出 int 类型的范围。

一个字符串对象 release 的代码的,系统会自动的对字符串对象进行-1操作。注意,不用写 release 是对@”xxxx”而言的,不是对对象的属性而言的,也就是说,在首次新建了一个字符串对象时,对于这一步操作是不用手动释放的,但是,对于后续

如果需要把这个字符串对象当成一个对象传给其他参数,对于这一个传递的操作而言,还是要手动 release,因为这一操作增加了一次对字符串对象的引用,即对于字符串对象来说,首次引用不用手动-1,之后的任何再次引用都需要手动-1操作。

所以,在 OC 中,只要,并且只有是使用了 alloc 创建的对象,才需要对这个对象手动进行[xxx release]操作。


在以前对内存进行管理时,所写的代码会有很多重复,并且在使用中(main 函数中),还要自行编写 N 多的【xxxx release】,只要新建一个对象,就要手写一句,并且顺序不能出错,这样的过程非常痛苦。就像没用 property 之前,对于每个属性都要自己编写 set 和 get 方法,代码重复量大但是意义不大。类似于 property,新版的 xcode 中有一个自动编写的机制:arc 机制。这个机制可以自动生成相应的内存管理代码。

不论成员变量的类型是基本数据类型还是对象类型,都可以通过@property 来自动生成 set 个 get 方法,但是自动生成的方法是比较简陋的,对于不用内存管理的基本数据类型而言很方便,但是对于对象属性而言,@property 自动生成的方法

可以完成对对象的 set 和 get 操作,但是不会进行判断和内存管理,即+1-1操作。但是,如果给@property 加入参数,也是可以让 property完成更多功能的,例如,如果是在 property 中加入 retain 参数,那么 property 自动生成的方法就会加上对内存

管理的代码,格式如下:@property (retain) 对象;在小括号中加入 retain,property 生成的代码就会和我们自定义的 set 对象的代码一样,加入判断并且进行+1-1的操作,此外,property 中还可以加入多个指令,多个指令都写在小括号内,并且用逗号

隔开,可以实现 更多的功能。property 的参数可以分为4大类。格式如下:

@property (参数,参数,参数。。。。。。。。可有多个参数,顺序无关,但是功能不能冲突)  属性类型  属性名;


1.set 方法内存管理相关的参数:

* retain :release 旧对象的计数器值,retain 新对象的计数器值(这个参数只能用于属性类型是对象时)。

* assign:分派的意思,表示直接赋值,默认就是直接赋值,用于基本数据类型属性,包括枚举和结构体(OC 中的对象可以认为是一种特殊的结构体,但本质不同,要区分清楚)。在编写代码时,如果不是对象属性,尽管

系统会默认是 assign,但是一般都会写上 assign,这是也是良好的代码习惯。

* copy :release 旧对象的计数器值,copy 新值;


2.是否要生成 set 方法:

* readwrite : 可读可写,同时生成 set 和 get 方法,默认就是这个选项。

* readonly :只读,只生成 get 方法,不生成 set 方法,当对于某个属性只想要可以读取值不能够修改值时,使用这个参数。


3.多线程管理参数

* nonatomic  :使用这个参数性能高。(实际项目中通常用这个参数,所以记得要改写这个参数,不论是基本数据类型还是对象,都要写这个。因为默认是 atomic),一般 nonatomic 写在第一个参数的位置。

* atomic :默认参数,性能低。


4.set 和 get 方法名称定义参数:

* setter :用于修改 set 方法的方法名称,要注意的一点是 自定义的 set 方法名称后面一定要写 冒号 ,冒号也是方法名的一部分,如果不写就会报错,无法传递参数。一般实际中不会修改默认的 set 方法名称。

* getter :修改 get 方法的名称。在实际开发中,如果属性的类型是 BOOL 类型,那么通常会使用 getter 参数来对 get 方法的方法进行修改,因为如果一个类型如果是 BOOL 类型时,get 方法名通常以 ”is”开头,

这是良好的编写代码的习惯,便于代码阅读。is 表示是否的意思。因为返回的数据类型是 BOOL,表示要么是,要么否,因此在方法名开头加上 is 更利于阅读,一看就知道这个方法返回的是一个 BOOL 类型的数据。


内存管理中循环引用的问题:

循环引用就是指类与类之间的关系,就是我引用你,你也引用我,两个类之间相互引用,而在现实的生活中是存在这样相互引用的关系的,但是对于计算机而言,相互引用就会报错。在 oc 中解决类之间相互引用的问题通过

是使用关键字@class 类名。即使用@class 类名 ;(有分号,是一条语句) 来代替#import 类名。h(没有分号结束)。@class 类名 表示告诉编译器,有这么一个另外的类存在,但是当前的类并没有import 这个类,对于这个类属性以及方法是

不知道的,只是知道有这个类的存在而已。因为对类的详细 声明信息都在。h 文件中。一般来说,如果两个类之间相互引用,那么在这两个类中都用@class ,而#import 。h 文件则放在。m 文件中进行,因为。m 文件是不会包含在main 函数中的

,所以是不会产生冲突,而在。m 文件中对方法进行实现,只要包含了相关的。h 头文件就能够完成方法的调用。知道某个类的具体信息只需要在。m 文件中知道,正常情况下。h 文件会包含其他的。h 文件,而本身的。m 文件又会包含自己的。h 文件,

这样就完成了方法的引用,但是在类的相互引用关系中,如果把。h 文件按照正常的包含方法处理则会产生冲突,因此,把。h 文件放在。m 文件 中包含。

import 的意思是引用,引进。如果使用 import 来进行对其他头文件的引用,那么当这个头文件发生更改时,不仅是文件名更改,包括文件内容的更改,当发生更改是,编译器都会重新编译一次,如果某个头文件被上百个类引用时,当这个文件更改一次,

那么上百个类中每个类都会重新编译一次,这是非常浪费系统性能的,如果是使用了@class 来告诉编译器有这么一个类,而只有在需要真正使用到某个类的类方法时才在。m 文件中 import 相关的。h 文件,这样就能够减少不必要的引用,进而减少变异的次数

,提高系统的性能。头文件相互引用是循环引用所产生的问题之一,循环引用还有另一个问题就是两个类之间相互引用,都对计数器发送作用,导致对象内存不能正确释放。解决的方法是其中一个类的property 使用 retain 参数,另一个 类使用 assign 参数(直接赋值,不进行内存管理代码操作即不+1也不-1)。通过这样的方法来解决相互引用的问题。














内存管理总结:只要有一个 alloc,就要有一个 release,只要有一个 retain,就要有一个 release。以上的关系都要按顺序对应好。

如果是新建的对象,那么 release 写在 main 函数中,如果是属性的对象的引用,那么 release 写在 dealloc 方法中。不要忘记。


































































































评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值