iOS基础之内存管理

内存管理的基本范围和概念.

程序运行过程中药创建大量的对象, 和其他高级语言类似,在ObjC中对象存储在堆区,程序不会自动释放内存. 如果对象创建使用后没有及时释放,那么就会占用大量内存. 其他高级语言都是通过垃圾回收机制来解决.在ObjC中并没有,所以需要手动维护.

目的:移动设备的内存非常有限,应对iOS系统对app内存限制.
iOS在应用程序使用超过20M,40M分别发送MemoryWaring消息警告一次并在120M时强制退出.

管理范围:只有对象.
因为对像存储在内存的堆区,而堆区内存是需要程序员手动管理,
而对象指针变量存在于栈区,会在作用域结束时自动释放,指针栈区和对象堆区的释放不同步也就造成野指针和内存泄露.

核心原则:引用计数器
对象所有权:任何对象都可能由一个或多个拥有者,只有所有权为0可以释放.
过早释放会造成野指针,僵尸对象. 不释放则造成没有指针指向,程序员不能操作,且占内存,内存泄漏

Cocoa所有权策略:任何自己创建的对象都归自己所有

Person *p=[[Person alloc]init]; //创建时会是引用计数器为1.所有权归p所有. 

对象使用一个计数器来保存当前对象被拥有者引用数.计数器8个字节NSUInterage;计数器为0,会触发系统自动调用的临终遗言dealloc.


内存管理的分类

MRC(手动管理,Xcode4,1之前).
ARC自动管理.现在默认使用.
"垃圾回收机制"
ARC是编译器特性,其他是运行时特性.

1.MRC手动内存管理

1.手动输入retain/release方法来增加/减少引用计数器.达到灵活内存释放.
2.retainCount方法可以返回计数器数值,不过不是一定准确,不建议使用.例:计数器为0时也显示1.
3.原则:

**1)只要对象还被使用,不回收.
2)只要你想使用,+1(发送retain消息)(防止他人使用完毕后release释放了.)
3)不使用了,-1(发送release消息). **
总结:谁创建,谁release;谁retain谁release.有始有终,有增有减.

ObjC中的内存管理机制跟C语言中指针的内容是同样重要的,要开发一个程序并不难,但是优秀的程序则更测重于内存管理,它们往往占用内存更少,运行更加流畅。虽然在新版Xcode引入了ARC,但是很多时候它并不能完全解决你的问题。在Xcode中关闭ARC:项目属性—Build Settings--搜索“garbage”找到Objective-C Automatic Reference Counting设置为No即可。


2.野指针

僵尸对象:已经释放的对象(不能再使用).
野指针:指向僵尸对象(不可用内存)的指针;
空指针,没有指向的指针(nil);
内存泄露:指向对象的指针被取消;

使用僵尸对象,使用野指针,依然能获取值,这个值可能是之前的,也可能是重新使用内存存储的值;格式不确定,就是垃圾值;(可以打开僵尸对象检测来报错)
死了不能活,不能0之后再retain
nil NIL NULL区别
nil:对象初始赋值,对象值
NIL: 类对象值
NULL:通用指针 a null point to an Object else


3.单个对象(野指针)僵尸对象和内存泄漏问题.

  1. 野指针:最后一次release后,并栓住野指针 => p=nil;(如果不设置,则p就是野指针,它的指向已经不属于这个程序,因此是很危险的.并且设置之后,再给p发送release消息不会报错.因为此时是nil)
    • 典型报错:Thread 1:EXC_BAD_ACCESS(code=EXC_I386_GPFLT. 访问了一块已经不属于你的内存.
  2. 内存泄漏:计数器没有到0,而指向对象的指针指向了别处或是作用域结束被释放.此时这个对象无法找到,无法操作.即造成内存泄漏. (无法解决,只能程序结束释放.所以尽力避免);

4.多个对象的内存管理.

总结而言就两个错误:1.释放了还要用---不用了还不释放*
多个对象主要是指针对关联关系的对象,此时一个对象拥有对象类型的实例变量.调用时相当于多了一个所有权(例set方法设置_car=car,对象型指针*_car指向了car,所以对象car需要retain一次).
为了解决这个问题可以重写Person类的set方法

-(void)setCar:(Car *)car{  
        _car=[car retain];
}//一般内存管理的方法返回值都是对象自己.

虽然方法中符合了原则,但是在主函数部分,如果调用一次set方法,就会使car对象计数器+1,而在主函数中不能release,不然主函数部分就违反了原则(谁创建,谁release;谁retain谁release.)而此时car对象引用计数器始终多一个,内存泄露

怎么办?--利用Person类释放时系统自动调用的dealloc方法在里面加上car.(创建对象p调用car);
//书写规范
//1,先释放子类自己的(关系)对象
//2,必须最后要写[super dealloc(父类有一部分占据了子类对象空间)

-(void)dealloc:{  
[_car release];        
[super dealloc];  
}

_car此时代表Person类的关联对象Car类的对象)目的:跟set的retain对称了;
同理,根据”想用+1.不用-1”原则:当Person有多个关系对象时 ,用多个set赋值,但是dealloc只能释放最后一次设置的对象和本身;所以==>在set中还需要释放上一次的设置的对象.还要加上[_car release]////如果第一次用set方法,_car一开始没指向时,_car等于nil,而[nil release]没有作用,所以可以放心使用;可是问题又来了.如果连续set同一个对象该如何.此时计数器由1->0->1;僵尸对象错误;所以需要价格判断;
因此最后set完整写法如下:

-(Void)setCar(car *)car{   
    if(_car != car){       //3)  
    [_car release];      //2)  
    _car = [car retain];   //1)  
}  

}1)1个对象—僵尸对象-2)防止两个不同对象—内存泄露-3)两个相同对象—僵尸对象;分别解决这三个问题;

ARC自动内存管理

1.移动设备内存及其有限,每个app所能占用的内存是有限制的
2.下列行为会增加内存:
创建一个oc对象 定义一个变量 调用一个函数或者方法
ARC是编译器特性(如@property,点语法),启用ARC之后,编译器就会自动在适当地方retain, release,autorelease(会在对象创建时自动添加) 语句.不是垃圾回收机制的运行时特性.

指针保持对象生命

ARC的原则:只要还有一个指针指向对象,对象就会保持在内存中.没有则会释放.与MRC原则区别:强指针指向.(避免了一般情况的僵尸对象和内存泄露)使对象寿命和指针变量一致了,不考虑引用计数器..
弱指针 _ _weak 修饰的
__weak Person * p; 弱指针指向的对象释放后,自动指向nil..

weak指针
_ _weak NSString *str =[[NSString alloc]initWithFormat:...];

NSLog(@"%@", str); // will output "(null)"

上例中String的对象没有拥有者,在创建之后就会被立即释放,会警告.其实一般不加前缀,编译器默认strong,不加也行.
weak 指针主要用于“父-子”关系,父亲拥有一个儿子的 strong 指针, 因此是儿子的所有者;但为了阻止所有权回环,儿子需要使用 weak 指针指向父亲.

有了ARC之后,可以不需要考虑retain或release对象,只需要考虑对象间的关系.虽然 ARC 管理了 retain 和 release,但并不表示你完全不需要处理 内存管理的问题。因为 strong 指针会保持对象的生命,某些情况下你 仍然需要手动设置这些指针为 nil(指正变量作用域结束释放,如果作用域过大),否则可能导致应用内存不足,无论何时你创建一个新对象时,都需要考虑谁拥有该对象,以及这个对象需
要存活多久。

Xcode 的 ARC 自动迁移 要启用一个项目的 ARC,你有以下几种选择:
  1. Xcode 带了一个自动转换工具,可以迁移源代码至 ARC
  2. 你可以手动转换源文件
  3. 你可以在 Xcode 中禁用某些文件使用 ARC,这点对于第三方库非常有用。
dealloc 方法

另外启用 ARC 之后,dealloc 方法在大部分时候都不再需要了,因为你不能 调用实例对象的 release 方法,也不能调用[super dealloc]。假如原先的 dealloc 方法只是释放这些对象,Xcode 就会把 dealloc 方法完全移除。你不再 需要手动释放任何实例变量。
如果你的 dealloc 方法处理了其它资源(非内存)的释放,如定时器、Core Foundation 对象,则你仍然需要在 dealloc 方法中进行手动释放,如 CFRelease(), free()等。这时 Xcode 会保留 dealloc 方法,但是移除所有的 release 和[super dealloc]调用。如下:

    -(void)dealloc{
AudioServiceDisposeSystemSoundID(soundID);
}

ARC下对象管理

  • 强弱指针都可以访问对象
  • ARC下set方法参数retain无用了.增加weak和strong参数.

  • 关联关系中.@property(nonatomic,weak)Dog*dog; 用弱指针预防内外不同步(外部强指针转向时,对象立即释放不收方法调用影响)

如果用strong,不同步,不过作用域结束后,mian中指针变量都会释放,之后调用对象会被释放,而被调对象因为调用对象的释放,其中的指针变量也释放了,最后调用对象也会被释放.
引申的:当ARC中循环引用出现时,(你拥有我我拥有你,只是一首太温柔的歌~)
1)此时防止编译错误还是先使用@Class,.m中再#import类头文件.
2)之后类似于MRC下改变一个@property 的set方法参数retain,把其中一个强指针替换成weak(其实不论是否循环推荐以后都使用weak)但是有一个要注意:__weak Person *p=[Person new];这种对象创建昙花一现,没有任何意义.


ARC和MRC混编

  1. MRC>ARC
    把MRC的代码转换成ARC的代码,删除内存管理操作(手动)
    xcode提供了自动将MRC转换成ARC的功能,操作菜单栏edit -> Refacotor(重构) -> Convert to Objective-C ARC
  2. ARC>MRC
    在ARC项目中继续使用MRC编译的类,在编译选项中标识MRC文件即可"-fno-objc-arc"
    在MRC项目中继续使用ARC编译的类在编译选项中标识MRC文件即可"-fobjc-arc”

5.属性参数

iOS基础之内存管理_第1张图片
261710009166151.png

atomic和nonatomic用来决定编译器生成的getter和setter是否为原子操作。

  • atomic
    • 设置成员变量的@property属性时,默认为atomic,提供多线程安全。
    • 在多线程环境下,原子操作是必要的,否则有可能引起错误的结果。加了atomic,

setter函数会变成下面这样:

@synchronized(self){
if(_car=!car){         
   [_car release];      
   _car=[car retain];
}
  • nonatomic
    • 禁止多线程,变量保护,提高性能。

atomic是Objc使用的一种线程保护技术,基本上来讲,是防止在写未完成的时候被另外一个线程读取,造成数据错误。而这种机制是耗费系统资源的,所以在iPhone这种小型设备上,如果没有使用多线程间的通讯编程,那么nonatomic是一个非常好的选择。

这也就是说,在多线程环境下,解析的访问器默认提供一个对属性的安全访问,从获取器得到的返回值或者通过设置器设置的值可以一次完成,即便是别的线程也正在对其进行访问。如果你不指定 nonatomic ,在自己管理内存的环境中,解析的访问器保留并自动释放返回的值,如果指定了 nonatomic ,那么访问器只是简单地返回这个值。

assign

  • 对基础数据类型 (NSInteger,CGFloat)和C数据类型(int, float, double, char)等等。
  • 此标记说明设置器直接进行赋值,这也是默认值。在使用垃圾收集的应用程序中,如果你要一个属性使用assign,且这个类符合NSCopying协议,你就要明确指出这个标记,而不是简单地使用默认值,否则的话,你将得到一个编译警告。这再次向编译器说明你确实需要赋值,即使它是 可拷贝的。

retain

  • 对其他NSObject和其子类对参数进行release旧值,再retain新值
  • 指定retain会在赋值时唤醒传入值的retain消息。此属性只能用于Objective-C对象类型,而不能用于Core Foundation对象。(原因很明显,retain会增加对象的引用计数,而基本数据类型或者Core Foundation对象都没有引用计数——译者注)。
    注意: 把对象添加到数组中时,引用计数将增加对象的引用次数+1。

copy

  • 对NSString 它指出,在赋值时使用传入值的一份拷贝。拷贝工作由copy方法执行,此属性只对那些实行了NSCopying协议的对象类型有效。更深入的讨论,请参考“复制”部分。

assign与retain:

  1. 接触过C,那么假设你用malloc分配了一块内存,并且把它的地址赋值给了指针a,后来你希望指针b也共享这块内存,于是你又把a赋值给(assign)了b。此时a和b指向同一块内存,请问当a不再需要这块内存,能否直接释放它?答案是否定的,因为a并不知道b是否还在使用这块内存,如果a释放了,那么b在使用这块内存的时候会引起程序crash掉。
  2. 了解到1中assign的问题,那么如何解决?最简单的一个方法就是使用引用计数(reference counting),还是上面的那个例子,我们给那块内存设一个引用计数,当内存被分配并且赋值给a时,引用计数是1。当把a赋值给b时引用计数增加到2。这时如果a不再使用这块内存,它只需要把引用计数减1,表明自己不再拥有这块内存。b不再使用这块内存时也把引用计数减1。当引用计数变为0的时候,代表该内存不再被任何指针所引用,系统可以把它直接释放掉。
    总结:上面两点其实就是assign和retain的区别,assign就是直接赋值,从而可能引起1中的问题,当数据为int, float等原生类型时,可以使用assign。retain就如2中所述,使用了引用计数,retain引起引用计数加1, release引起引用计数减1,当引用计数为0时,dealloc函数被调用,内存被回收。

循环retain问题;
循环retain的场景 互相拥有;

比如A对象retain了B对象,B对象retain了A对象 循环retain的弊端 这样会导致A对象和B对象永远无法释放
循环retain的解决方案 :
当两端互相引用时,应该一端用retain、一端用assign即可

@class的作用

在.h文件中声明,告诉编译器@Class 修饰的标示符是一个类,编译不会报错,这是为了防止如果有过多的头文件都#import同一个文件,或是这些依次#import,那么一旦最开始的头文件稍作改动,之后用到这个文件的所有类都要重新编译一边,效率非常低.但是,@Class并不会包含类的内容,所以具体实现还是需要在.m文件中调用类的头文件.
在MRC中,如果出现循环引用中,不仅头文件循环引入会编译报错,而且会出现循环retain问题(set方法)造成对象永远无法释放.
**第一个问题用@Class解决,第二个是需要声明两个对象类型实例变量时@property参数一个正常用retain,一个要换成assign.并且dealloc重写时不用release;
在ARC中只需@Class解决即可.

7.自动释放池

ObjC中的一种内存自动释放机制,与其他高级语言不同, 这是一种半自动的机制,有些操作还需要我们手动设置. 自动内存释放使用@autoreleasepool关键字声明一个代码块, 如果一个对象在初始化时调用了autorelease方法,那么当代码块执行完毕后,在块中调用过autorelease方法的对象都会自动调用一次release方法, 这样起到了自动释放的作用,同时对象的销毁也得到了延迟

  1. 在程序运行过程中,会创建无数个池子,这些池子都是以栈结构存在,(先进后出);
  2. 当一个对象(在池作用域内)调用autoreslease时,系统会登记之,并在池作用域结束后向对像发送且仅一条release消息.
  3. 由于OC内存管理原则:谁创建,谁释放.但是如果一个方法返回了一个新建的对象,该对象如何释放.而方法中不能写release,这样一创建就会释放,所以引入了释放池概念---在作用域内延迟释放对象.

一般管理内存方法的返回值都是对象本身:release,retain,autorelease

@autoreleasepool{
    Person *p=[Person new];
    [P autorelease];//把对象加入此释放池
    }//释放 

注意:
只有在池作用域中调用autorelease方法才会加入相应释放池,有多个嵌套时就近上一层的.大括号结束即结束. 引用释放池后OC内存管理规则可理解:一个申请配对一个autorelease.一个retain配对一个release;
应用场景: 1)在方法中创建了对象(注意在池域中调用);

-(void)run{
[[[Person alloc]init]autorelease];
}

2)快速创建对像

+(instancetype)per{
return [[Person new]autorelease];
}//Person换成self普适性更高;id换成instancetype稳定性更高
在main中调用就可快速创建对应对象,且不用release;

3)快速创建对象并初始化

-(instancetype)initWithAge:(int)age{
    if(self=[super init]){
    _age=age;
    }
+(instancetype)personWithAge:(int)age{
return [[[self alloc]initWithAge:age]autorelease];
}

OC的类库中静态方法一般已经同上调用了autorelease方法,所以不需要手动释放;

8.单例

作用:在开发中用于多个界面传值; 只创建一个对象,节省内存.
概念:单例模式的意图是类的对象成为系统中唯一的实例,提供一个访问点,共享;
使用场景:

  1. 类只能有一个实例,而且必须从一个为人熟知的访问点对其惊喜访问,比如:工厂方法;
  2. 这个唯一实例只能通过子类话进行扩展,而且扩展的对象不会破坏客户端代码.

特点:

  • 某个类只能有一个实例
  • 他必须自行创建这个对象
  • 必须自行向整个系统提供这个实例
  • 为了保证实例的唯一性,我们必须将....方法进行覆盖;

需要覆盖的方法:

-(id)copyWithZone:(NSZone*)zone{
return self;
}

+(id)allocWithZone:(NSZone*)zone{
//线程保护
@synchronized(self) {
    
    if (instances == nil) {
        //调用父类的alloc
        instances = [super allocWithZone:zone];           
        return instances;
    }        
}
 return instances;
}

-(id)retain{
return self;
}

-(NSUInteger)retainCount{
return NSUIntegerMax;
}

-(oneway void)release{
}

-(id)autorelease{
return self;
}

另外新定义一个类方法作为共享访问点:

先在.m中定义一个静态的对象类型的全局变量.并nil//静态区,程序结束释放.
static SingletonClass *instaces=nil;
+(instrancesType)shareXxxx{
  if(instaces==nil){
    instances=[self allocWithZone:null];
    return instances;
  }
  return instances;
}

你可能感兴趣的:(iOS基础之内存管理)