您的位置:首页 > 移动开发 > IOS开发

iOS内存管理

2015-07-09 18:19 323 查看
iOS内存管理

iOS中存在两种类型的对象:

1) OC中的对象,NSObject的子类;

2) Core Foundation(C语言框架)对象;

对于不同类型的对象,其内存管理的方式也不同。在OC中,对象的内存管理分为手动管理(非ARC)和自动管理(ARC),也就是说ARC只能管理OC的对象,而不能管理Core Foundation对象。所以,在管理对象内存时,首先要做的就是区别对象的类型,即判断该对象的类型是OC对象还是CoreFoundation对象。

下面将从OC对象、CF对象、OC和CF对象转换、Block对象等方面介绍iOS的内存管理。

1、 OC对象内存管理

内存管理是关于如何管理对象生命周期的编程原则。OC中的内存管理只针对OC的引用类型,基本数据类型并不需要内存管理。

OC中采用了引用计数的方式来管理内存,所有的OC实例都有一个计数器,这个计数器称为引用计数。引用计数表示当前对象被引用多少次,在OC中只要对象的引用计数不为0,就不会释放对象,当引用计数器为0,该对象会被马上销毁,在销毁之前会调用对象的dealloc方法。

采用alloc创建对象,创建对象完成后,引用计数为1,只调用一次。retain操作会使引用计数加1,可以被多次调用,release使引用计数减1,也可以多次调用。如下所示;

Person* person=[[Personalloc]
init];
NSLog(@"%ld",[personretainCount]);
[person release];

1.1、 对象所有权

当一个所有者(owner,本身是一个OC对象)做了如下动作,它就拥有了一个对象的所有权(ownership),

l alloc

l retain

l [mutable]copy

使用如下方法,释放对象所有权,

l release

l autorelease

1.2、 黄金法则

如果一个对象使用了alloc、[mutable]copy、retain,那么你必须使用相应的release或者autorelease释放。

1.3、 再谈setter

当对象的某个属性为引用类型,那么setter方法怎么写呢?我们知道对象拥有了某个引用类型的属性,那么该对象就应该拥有属性的所有权,而且属性值不能被外界随意修改,如下的setter方法正确吗:

// Person.h
#import <Foundation/Foundation.h>
#import "Dog.h"
@class
Dog;
@interface Person :
NSObject
{
Dog * _dog;
}
-(void) setDog:(Dog *)dog;
-(Dog *) dog;
@end
// Person.m
#import "Person.h"
#import "Dog.h"
@implementation Person
-(void) setDog:(Dog *)dog
{
_dog=dog;
}
-(Dog *)dog
{
return
_dog;
}
- (void)dealloc
{
NSLog(@"person dead...");
}
@end
// main.m
#import <Foundation/Foundation.h>
#import "Person.h"
#import "Dog.h"
int main(int argc,const
char * argv[])
{
Person* person=[[Personalloc]
init];
Dog * dog=[[Dogalloc]
init];
dog.name=@"小黄";
person.dog=dog;
NSLog(@"%@",person.dog.name);
[dog release];
NSLog(@"%@",person.dog.name);
[person release];
return
0;
}
由上面的例子很容易看出,person对象并没有获取dog对象的所有权,外部dog对象的销毁将影响peroson内部dog对象。所以若实例对象的属性为引用类型,实例对象必须拥有该属性的所有权,现将setter方法改为如下:
-(void) setDog:(Dog *)dog
{
if (_dog!=dog)
{
[_dogrelease];
_dog=[dogretain];
}
}
注释:必须添加if (_dog!=dog)判断语句,若不添加该语句,若_dog对象和dog为同-对象,则将导致dog对象先释放,再赋值。
因为person对象拥有dog对象的所有权,所以在person对象销毁时必须释放dog对象的所有权,则person的dealloc方法如下:
- (void)dealloc
{
[superdealloc];
[_dogrelease];
NSLog(@"persondead...");
}
因为person对象拥有dog对象的所有权,所以在person对象销毁时必须释放dog对象的所有权,则person的dealloc方法如下:
对于属性为NSString类型的,setter方法采用copy原则。若属性为NUMutableString对象,为可变字符串,若采用assign或retain原则,则外部字符串的改变,将会影响对象内部字符串的值,就会破坏类的封装准侧。

1.4、 @property

在OC中属性的设置和访问要通过setter和getter方法,对于引用类型还要加上内存管理,这些逻辑其实都是相同的。为了提高开发人员的效率,OC提高了@property关键字,在编译时编译器可以自动加上setter和getter方法,并提供内存管理机制。

格式如下;

@property(nonatomic,retain,readwrite)Property * name

l 参数1

atomic:多线程环境下,存在线程保护,默认值;

nonatomic:多线程环境下,不存在线程保护;

l 参数2

assign:直接赋值,默认;

retain:保留对象;

copy:拷贝对象;

l 参数3

readwrite:生成getter、setter方法,默认;

readonly:只生产getter方法;

1.5、 容器的内存管理

如果一个对象被加入到容器中,那么这个对象的引用计数会变化吗?

对象在被加入到容器中计数器都会加1,而在容器销毁前会对容器中的每个对象做release操作。

1.6、 自动释放池

自动释放池是OC的一种内存自动管理机制。当自动释放池销毁时,它会对池子中的每一个对象调用一次release操作,如下

//创建自动释放池
NSAutoreleasePool *autoreleasepool=[[NSAutoreleasePoolalloc]
init];
Person* person=[[Personalloc]
init];
//将perosn对象加入自动释放池
[personautorelease];
//自动释放池销毁时会对池中的每个对象做一次release操作
[autoreleasepoolrelease];

1.7、 autorelease

autorelease的作用是向一个对象发送autorelease消息,当我们向一个对象发送autorelease消息时,这个对象就被放入了离它最近的自动释放池中(就近原则)。

@autoreleasepool
{
//创建自动释放池
Person* person=[[Personalloc]
init];
//将perosn对象加入自动释放池
[person autorelease];
}
//创建自动释放池

@autoreleasepool
{
//简写形式
Person* person=[[[Personalloc]
init]autorelease];
}
当自动释放池销毁时,它会对池子中的每一个对象做仅做一次release操作,所以为了防止内存泄露,必须遵守“黄金法则”-那里alloc、retain、copy,哪里就release。

1.8、 典型错误案例

1) [person setDog:[[Dogalloc]
init]];

创建dog对象,就应该释放。在自动释放池中,正确的做法如下:

[person setDog:[[[Dog alloc] init]autorelease ]];
2) 对象已销毁

- (NSString *)getInfo
{
NSString * info=[[NSStringalloc]
initWithString:@"www"];
[info release]; //返回的对象已销毁
return info;
}
NSString * info=[person
getInfo];
[info release];
//违背了黄金法则,并没有alloc、retain、copy操作

正确的做法为;
- (NSString *)getInfo
{
NSString * info=[[NSStringalloc]
initWithString:@"www"];
return [info
autorelease];
}

@autoreleasepool
{
NSString * info=[persongetInfo];
}

将返回的对象,放入到自动释放池中。
3) 一个例子

for (int i=0; i<1000000; i++)
{
NSMutableArray * mutableArr=[[NSMutableArrayalloc]
init];
[mutableArr autorelease];
}
如上代码,循环100万次创建100万个对象,这些对象在该循环体内不会自动销毁,只会加入到最近的自动释放池中。只有当自动释放池销毁了,这100万个对象才会收到release消息。这样的话,100万个对象同时存在内存中,将占用很大的内存,正确的做法如下:

NSAutoreleasePool *pool=[[NSAutoreleasePoolalloc]
init];
for (int i=0; i<1000000; i++)
{
if (i%1000==0)
{
[pool release];
pool=[[NSAutoreleasePoolalloc]
init];
}
NSMutableArray * mutableArr=[[NSMutableArrayalloc]
init];
[mutableArr autorelease];
}
[pool release];

该代码没执行1000次,就会把pool释放一次,同时再创建一个新的自动释放池,也就是内存中累计不会超过1000个对象。

4) 类方法创建的内存管理

+(Person *)person
{
Person * person=[[Personalloc]
init];
return [person
autorelease];
}

也要将返回的对象加入到自动释放池中,

//错误,违背了黄金法则,并没有alloc、retain、copy操作
Person * person=[Personperson];
[person release];

Foundation框架中使用类方法获取的对象都已主动加入到自动释放池中。

1.9、 ARC

ARC:自动引用计数,提供自动管理内存的功能,不需要手动管理引用计数,不需要也不允许retain、release、autorelease操作。

1.10、 循环引用

循环引用就是,对象A持有对象B的所有权,同时对象B也持有对象A的所有权。循环引用将会导致两个对象都无法销毁,如下所示;



// Person.h
#import <Foundation/Foundation.h>
#import "Dog.h"
@class
Dog;
@interface Person :
NSObject
{
Dog * _dog;
}
-(void) setDog:(Dog *)dog;
@end
//
// Person.m
#import "Person.h"
#import "Dog.h"
@implementation Person
- (void)setDog:(Dog *)dog
{
if (_dog!=dog)
{
[_dog
release];
_dog=[dog
retain];
}
}
- (void)dealloc
{
[super
dealloc];
[_dog
release];
NSLog(@"person dead...");
}
@end
//
// Dog.h
#import <Foundation/Foundation.h>
@class
Person;
@interface Dog :
NSObject
{
Person * _person;
}
-(void) setPerson:(Person *)person;
@end
//
// Dog.m
// oc
#import "Dog.h"
#import "Person.h"
@implementation Dog

- (void)setPerson:(Person *)person
{
if (_person!=person)
{
[_person
release];
_person=[person
retain];
}
}
- (void)dealloc
{
[super
dealloc];
[_person
release];
NSLog(@"dog dead...");
}
@end
//
// main.m
#import <Foundation/Foundation.h>
#import "Person.h"
#import "Dog.h"
int main(int argc,const
char * argv[])
{
Person * person=[[Personalloc]
init]; //p.retainCount=1
Dog * dog=[[Dogalloc]
init]; //d.retainCount=1
person.dog=dog; //d.retainCount=2
dog.person=person; //p.retainCount=2
[dog release]; //d.retainCount=1
[person release]; //p.retainCount=1
return
0;
}
针对循环引用问题,解决的方法是循环引用的双方或多方中要有一方主动放弃对象的持有权。
当采用代理设计模式,很可能会引起循环引用,所以代理对象一般设置为assign类型的,而不是strong类型的。

2、 CF对象内存管理

CoreFoundation框架是一组C语言接口,它们为iOS应用程序提供基本数据管理和服务功能。CoreFoundation的对象,其中也有对象引用的概念,其获取或释放的接口自身的CFRetain/CFRelease接口。

CoreFoundation框架和Foundation框架紧密相关,它们为相同功能提供接口,但Foundation框架提供OC接口。CoreFoundation对象在释放时采用CFRelease函数,如下:

CFStringRef cstring=CFSTR("aaaaaa");
NSLog(@"%@",cstring);
CFRelease(cstring);

3、 OC、CF对象转换内存管理

如果将Foundation对象和CoreFoundation类型混合使用,则可利用两个框架之间的“toll-free bridging”。所谓的Toll-freebridging是指在某个框架的方法或函数同时使用Foundation和CoreFoundation框架中的某些类型。

在应用中若混合使用Foundation和CoreFoundation框架的类型,要注意两者的内存管理方式的不同。实际上,Foundation类型和CoreFoundation类型是一一对应的,两者的类型可以相互转换,如NSString类型与CFStringRef类型可以相互转换。在Foundation类型和Core
Foundation类型转换之后,必须清楚那个类型拥有对象的所有权,以进行内存管理。

在非ARC下,

id obj=[[NSObjectalloc]
init];
void * p=obj;
[obj release];

代码中,对象的所有权归obj所有,当obj释放后,p将变为野指针。

在ARC下,上面的代码将会报错,为了更好的方便Foundation类型和Core Foundation类型之间的转换以及对象所有权的归属,iOS提供了桥接指令来转换,如下:

1) __bridge

__bridge只是类型转换,对象所有权不发生改变;下面的代码中,对象所有权仍归obj所有:

id obj=[[NSObjectalloc]
init];
void * p=(__bridgevoid*)obj;

2) __bridge_retained

__bridge_retained将Foundation类型转换为Core Foundation类型。在类型转换时,转换后的对象也获得对象的所有权,而obj作用域内也持有对象的所有权,需要主动释放strR,如下:

void *p =
0;

{
id obj = [[NSObjectalloc]
init];
p = (__bridge_retainedvoid *)obj;
}

NSLog(@"class=%@", [(__bridgeid)p
class]);
CFRelease(p);
3) __bridge_transfer

__bridge_transfer是将CoreFoundation类型转换为Foundation类型。在类型转换时,转换后的对象获得对象的所有权,而obj将不再持有对象的所有权,如下

id obj=nil;
{
void * p=(void *)CFSTR("hello world");
obj =(__bridge_transferid)p;
}
NSLog(@"%@",obj);
总结:
1) 明确类型,Foundation类型还是Core Foundation类型;
2) 确定对象所有权,若对象所有权为OC对象,则处于ARC管理下;否则,需要主动释放;

4、 Block对象转换内存管理

Block是对C语言的扩展,用来实现闭包的特性。闭包,简而言之就是可以读取其他函数内部变量的函数。如下所示:

int main(int argc,const
char * argv[])
{
@autoreleasepool
{
int age=10;
void (^block)()=^{
NSLog(@"%d",age);
};
block();

}
return
0;
}

4.1、访问外部变量

在Block内,可以访问Block之外的变量。那么在Block内部是如何能够访问外部的变量呢,实现原理是什么?

下面是将一段OC代码,利用clang转换为C++后的代码片段,从代码中我们可以得知,Block访问外部变量的实现机制。

/* OC代码 */
int main(int argc,const
char * argv[])
{
@autoreleasepool
{
int age=10;
void (^block)()=^{
NSLog(@"%d",age);
};
age=30;
block();
}
return
0;
}
/* C++代码 */
struct __block_impl
{
void *isa; /* block可以是一个NSObject*/
int Flags;
int Reserved;
void *FuncPtr;
/* block对应的函数指针 */
};

struct __main_block_impl_0
{
struct __block_impl impl;
struct __main_block_desc_0* Desc; /*描述信息*/
int age;
/* 构造函数*/

__main_block_impl_0(void *fp,struct __main_block_desc_0 *desc,
int _age, int flags=0) : age(_age)
{
/* NSConcreteStackBlock表示这个block位于栈中,还有_NSConcreteMacllocBlock、_NSConcreteGlobalBlock
*/
impl.isa =&_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
/* block对应的函数体 */
static
void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
int age = __cself->age;// bound by copy
NSLog((NSString*)&__NSConstantStringImpl__var_folders_gm_tyt8mbyd5nv_0dft7dpkfxr40000gn_T_main_287c33_mi_0,age);
}
/* 描述信息*/
static
struct __main_block_desc_0
{
size_t reserved;
size_t Block_size; /* block大小*/
} __main_block_desc_0_DATA = { 0,sizeof(struct __main_block_impl_0)};

int main(int argc,const
char * argv[])
{
/*@autoreleasepool */
{
__AtAutoreleasePool __autoreleasepool;
int age=10;
void (*block)()=(void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age);
age=30;
((void (*)(__block_impl *))((__block_impl*)block)->FuncPtr)((__block_impl *)block);
}
return
0;
}
从上面的代码中,我们可以得到如下结果:
1) 首先出现的结构体就是_main_block_impl_0,可以得知是根据所在函数以及出现的顺序进行命名的。如果是全局Block,就根据变量名和出现的次序命名;
2) 接着出现的是_main_block_func_0函数,即block对应的函数体,该函数接受一个_cself参数,即对应的block自身。
3) 再下面是_main_block_desc_0,主要是相关描述信息;
4) 最后就是main函数中对block的创建和调用,可以看出执行block就是调用一个以block自身作为参数的函数,这个函数对应着block的执行体。
5) 由于Block也是NSObject,我们可以对其进行retain操作;不过在将Block作为回调函数传递给底层框架时,底层框架需要对其copy一份。通常Block会存储在栈中,需要回调时,往往回调Block已经不在栈中,使用copy可以将block放在堆中。

4.2、更新外部变量

在4.1节中,我们知道在Block内可以访问外部的变量,那么可以更新变量的值吗?

答案是肯定的,可以在Block内部更新外部变量的值。外部变量有两种,一种是全局变量,另一种是局部变量。对于全局变量(存放在堆中),可以直接在Block内部对变量进行更新;而对于局部变量,需要在局部变量前加上__block修饰后,才能在Block内部更新变量值。

那么增加__block修饰的局部变量与一般变量有什么不同呢?下面代码段将会告诉我们原因。

/* OC代码 */
int main(int argc,const
char * argv[])
{
@autoreleasepool
{
__blockint age=10;
void (^block)()=^{
NSLog(@"%d",age);
};
age=30;
block();
}
return
0;
}

/* C++代码 */
struct __block_impl
{
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __Block_byref_age_0
{
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};

struct __main_block_impl_0
{
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age;
// by ref
__main_block_impl_0(void *fp,struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age,int flags=0)
: age(_age->__forwarding) {
impl.isa =&_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static
void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
__Block_byref_age_0 *age = __cself->age;// bound by ref

NSLog((NSString*)&__NSConstantStringImpl__var_folders_gm_tyt8mbyd5nv_0dft7dpkfxr40000gn_T_main_3b0128_mi_0,(age->__forwarding->age));
}
staticvoid __main_block_copy_0(struct
__main_block_impl_0*dst, struct __main_block_impl_0*src)
{
_Block_object_assign((void*)&dst->age, (void*)src->age,8/*BLOCK_FIELD_IS_BYREF*/);
}

staticvoid __main_block_dispose_0(struct
__main_block_impl_0*src)
{
_Block_object_dispose((void*)src->age,8
/*BLOCK_FIELD_IS_BYREF*/);
}

static
struct __main_block_desc_0
{
size_t reserved;
size_t Block_size;
void (*copy)(struct__main_block_impl_0*,struct__main_block_impl_0*);
void (*dispose)(struct__main_block_impl_0*);
} __main_block_desc_0_DATA = { 0,sizeof(struct __main_block_impl_0), __main_block_copy_0,__main_block_dispose_0};

int main(int argc,const
char * argv[])
{
/*@autoreleasepool */
{ __AtAutoreleasePool__autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_age_0 age= {(void*)0,(__Block_byref_age_0 *)&age,0,
sizeof(__Block_byref_age_0),10};
void (*block)()=(void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA,(__Block_byref_age_0 *)&age,570425344);
(age.__forwarding->age)=30;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl*)block);
}
return
0;
}
从上面的代码中,我们可以得到如下结果(来自网络):
1) __block修饰的局部变量与普通局部变量,转换后的C++代码发生了较大的变化。但总的来说,普通变量可以被Block访问是因为Block内保存了该变量的副本,该副本是通过定义Block是通过值传递的,而不是在调用时才发生值传递;而被__block修饰的局部变量,在Block内持有对该变量的指针,所以可以修改该变量;
2) 虽然,被__block修饰的局部变量可以被Block更新,但该变量存储在栈上,当Block被回调执行时,变量的栈已经被展开,那该怎么办呢?__main_block_desc_0其了重要作用,该结构体多了两个成员函数copy和dispose,分别指向__main_block_copy_0和__main_block_dispose_0。当Block从栈上被copy到堆上时,会调用_main_block_copy_0将Block类型的成员变量从栈上复制到堆上;而当Block被释放时,相应地会调用_main_block_dispose_0来释放Block的成员变量。Block可能一会在栈上,一会在堆上,那如果栈上和堆上同时修改变量,该怎么办?
此时,_forwarding的作用就体现出来了,当一个Block变量从栈上被复制到堆上时,栈上的那个_Block_byref_age_0中的_forwarding指针也会指向堆上的结构,从而保持一致。

4.3、Block内存

在iOS中,Block的内存类型可能为_NSStatckBlock、_NSMallocBlock、_NSGlobalBlock其中之一。

1) _NSGlobalBlock(适用于非ARC和ARC)

若Block体内并没有操作外部变量,也即Block体内都是Block内定义的局部变量,则该Block内存类型_NSGlobalBlock,该Block相当于静态变量。

若Block内有对全局变量的操作,该Block内存类型也为_NSGlobalBlock。

2) _NSMallocBlock

在ARC下,只要Block变量有强引用,且Block内没有操作全局变量,则该Block内存类型为_NSMallocBlock。如下

int main(int argc,const
char * argv[])
{
@autoreleasepool
{
int age=10;
void (^block)()=^{
NSLog(@"%d",age);
};
block();

NSLog(@"%@",block);
NSLog(@"%@",[blockcopy]);
NSLog(@"%@",^{NSLog(@"%d",age);});
}
return
0;
}
输出结果为:
2015-07-0917:59:43.736 Block[32311:1984325] 10
2015-07-0917:59:43.737 Block[32311:1984325] <__NSMallocBlock__: 0x100111c50>
2015-07-0917:59:43.738 Block[32311:1984325] <__NSMallocBlock__: 0x100111c50>
2015-07-0917:59:43.738 Block[32311:1984325] <__NSStackBlock__: 0x7fff5fbff7b0>
3) _NSStatckBlock

在非ARC下默然情况下(除_NSGlobalBlock外)定义的Block,其内存类型都为_NSStackBlock,只有经过copy方法生成的Block,内存类型才为_NSMallocBlock。

4.4、总结

1) 在非ARC下,Block变量的retain、copy、release行为

retain

copy

release

_NSStackBlock

不可用

将Block从栈移向堆中,retainCount=1

无用

_NSMallocBlock

+1

+1

-1

_NSGlobalBlock

无用

无用

无用

2) Block类型作为函数返回值

当一个Block对象在堆上,它的生命周期和一个普通的NSObject对象是一样的。

在非ARC环境下,copy了Block后一定要使用release,不然会有内存泄露,而且泄露点是在系统级。对于一个Block对象,当外传的时候要调用autorelease方法:(a)调用copy方法移到堆上;(b)调用autorelease方法防止内存泄露。

在ARC环境下,很多规则可以省略,因为在ARC下,只要Block变量有强引用,则都会放在堆上。若Block对象作为参数返回,也会被放在堆上。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: