您的位置:首页 > 移动开发 > Objective-C

Objective-C 基础教程第九章,内存管理

2022-04-19 15:31 1356 查看

[toc]

Object-C 基础教程第九章,内存管理

前言:

最近事情比较多,很久没有来更新文章了。

刚好最近又空闲出来点时间,赶紧继续学习OC并且做笔记,这次要学习的是OC的内存管理。

对象生命周期

正如现实世界中的鸟类和蜜蜂一样,程序中你的对象也有生命周期。

对象的生命周期包括诞生(通过alloc或者new方法实现)、生存(接收消息并执行操作)、交友(通过复合以及方法传递参数)

以及最终死去(被释放掉)。

当生命周期结束时,它们的原材料(内存)将被回收以供新的对象使用。

引用计数

现在,对象何时诞生我们已经很清楚了,而且也讨论了如何使用对象,但是怎么知道对象生命周期结束了呢?Cocoa采用了一种叫做

引用计数(reference counting)
的技术,有也叫做
保留计数(retain counting)

每个对象都有一个关联的整数,当某段代码需求访问一个对象时候,计数器就+1,

反之当这段代码结束对象访问时,计数器-1,

当计数器为0的时候系统就回收该对象(😌可怜的对象)。

  • alloc
    new
    方法或者
    copy消息
    会创建一个对象,对象引用计数器被设置为1
  • -(id) retain;
    增加计数器
  • -(oneway void) release
    减少计数器
  • dealloc
    不要直接调用,系统会调用该方法
  • -(NSUInteger) retainCount
    返回当前引用计数器的值

RetainCount1项目例子

//声明
@interface RetainTracker: NSObject
@end

//实现
@implementation RetainTracker
-(id) init
{
if(self = [super init])
{
//当对象被创建的时候,调用retainCount来获取当前引用计数器的值.
NSLog(@"init: Retain count of %lu.",[self retainCount]);
return (self);
}
}

-(void) dealloc
{
//dealloc 方法无需我们自己调用,当计数器为0时候,系统自动调用dealloc回收对象。
NSLog(@"销毁方法被调用!");
[super dealloc];
}
@end
int main(int argc,const char *argv[])
{
//当通过new创建对象的时候,会将引用计数器设置为1,也会默认调用init方法。
RetainTracker *tracker = [RetainTracker new];

//增加引用计数器 count:2
[tracker retain];
NSLog(@"%d",[tracker retainCount]);
//增加引用计数器 count:3
[tracker retain];
NSLog(@"%d",[tracker retainCount]);

//减少引用计数器 count:2
[tracker release];
NSLog(@"%d",[tracker retainCount]);
//减少引用计数器 count:1
[tracker release];
NSLog(@"%d",[tracker retainCount]);

//增加引用计数器 count:2
[tracker retain];
NSLog(@"%d",[tracker retainCount]);
//减少引用计数器 count:1
[tracker release];
NSLog(@"%d",[tracker retainCount]);

//减少引用计数器 count:0
[tracker release];
NSLog(@"%d",[tracker retainCount]);

//当引用计数器为0的时候,系统将自动调用dealloc方法
//并且输出我们自定义dealloc方法里面的销毁方法被调用。
return(0);
}

但是当我们要编译的时候会报错,提示:

retainCount' is unavailable: not available in automatic reference counting mode

解决方案:https://blog.csdn.net/muy1030725645/article/details/109117668

-fno-objc-arc

最后输出如下图:

所以,当用

alloc
new
创建了一个对象的时候,通过用
release
对该对象进行释放就能销毁对象并且回收所占用的内存。

对象所有权

对象所有权(object ownership)
概念。

当我们说某个实体"拥有一个对象"时,就以为着该实体要负责确保对其他拥有的对象进行清理。

当对象里面有其他对象实例,

我们称为该对象拥有这些对象
。例如复合概念:
CarParts
类中,
car
对象拥有其指向的
engine
tire
对象。同样如果是一个函数创建了一个对象,则称该函数拥有这个对象。

当多个实例拥有某个特定的对象时,对象的所有权关系就更加复杂了,这也就是是保留计数器的值大于1的原因。

-(void) setEngine:(Engine*) newEngine;

int main()
{
Engine *engine = [Engine new];
[car setEngine: engine];//car设置新的引擎
}

现在我们参看如上代码,并且进行思考。

  • 现在哪个实体对象拥有engine对象?是
    main()
    函数还是
    Car
    类?
  • 哪个实体负责确保当engine对象不再被使用时能够收到release消息?

答:

1.Car类 因为Car类正在使用engine对象,所以不可能是main函数。
2.main()函数 因为main()函数随后可能还会用到engine对象,所以不可能是由Car类实体来收到release消息。

解决方案:
让Car类保留engine对象,将engine对象的保留计数器的值增加到2.
Car类应该在setEngine方法中保留engine对象
main()函数应该负责释放engine对象
当Car类完成其任务时再释放engine对象(在某dealloc方法中),最后engine对象占用的资源被回收。

访问方法中的保留和释放

编写

setEngine
方法的第一个内存管理版本。

-(void) setEngine:(Engine* )newEngine
{
engine = [newEngine retain];//增加引用计数器
}

int main()
{
Engine *engine1 = [Engine new];//new会创建一个对象,并且保留计数器会被设置为1
[car setEngine:engine1];//setEngine方法会调用retain,所以保留计数器+1 = 2
[engine1 release];//释放对象,保留计数器会被-1 = 1  ,这样main函数还能访问engine1对象

Engine *engine2 = [Engine new];//1
[car setEngine:engine2];//2
}

//如上代码有个bug,因为[engine1 release]的时候,保留计数器还是1,所以导致了内存泄露。

修改后

-(void) setEngine:(Engine*) newEngine
{
[newEngine retain];//保留计数器值+1
[engine release];
engine = newEngine;
}

自动释放

我们都知道,当我们不再使用对象的时候必须将其释放,但是在某些情况下需要弄清楚什么时候不再使用一个对象并不容易,比如:

-(NSString *)description
{
NSString *description;
description = [[NSString alloc] initWithFormat:@"hello"];//alloc 创建对象保留计数器值=1
return (description);
}
int main()
{
//可以用如下的代码进行释放,但是要写成这样看起来就很麻烦。
NSString *desc = [someObject description];
NSLog(@"%@",desc);
[desc release];
}

所有对象放入池中

Cocoa中有个自动释放池(

autorelease pool
)的概念。你可能已经在Xcode生成代码的时候见过
@autoreleasepool
NSAutoreleasePool
。那么对象池到底是个什么东西?从名字上看他大概应该是一个存放对象的池子(集合)。

-(id) autorelease;

该方法是NSObject类提供的,他预先设定了一条会在未来某个时间发送的

release
消息,其返回值是接收这条消息的对象。

当给一个对象发送

autorelease
消息的时候,实际上是将该对象添加到了自动释放池中。当自动释放池被销毁时,会向该池中的所有对象发送release消息。

改进后的之前description方法代码。

-(NSString*) description
{
NSString *description;
description = [[NSString alloc] initWithFormat:@"hello"];//保留计数器值= 1
return [description autorelease];//将description对象添加到自动释放池中,当自动释放池被销毁,对象也被销毁
}

//NSLog函数调用完毕后,自动释放池被销毁,所以对象也被销毁,内存被回收。
NSLog(@"%@",[someObject description]);

自动释放池的销毁时间

  1. 自动释放池什么时候才能会销毁,并向其包含的所有对象发送release消息?
  2. 还有自动释放池应该什么时候创建?

首先来回答第一个问题

我们看如下代码。自动释放池可以用下面两种方式来创建,那么第一种方法用的是OC的关键字,他会在结束后进行销毁并且发送release消息。第二种方法则是用

NSAutoreleasePool
类,来进行创建一个活的池。他会在release后回收并销毁池。

@autoreleasepool
{
//....Your Code
}

NSAutoreleasePool *pool = [NSAutoreleasePool new];
//....Your Code
[pool release];

回答第二个问题,我们需要先了解了解自动释放池的工作流程。

自动释放池的工作流程

如下代码展示了自动释放池的工作流程。

int main(int argc, char const *argv[])
{
//NSAutoReleasePool方式自动释放池.
NSAutoReleasePool *pool = [[NSAutoReleasePool alloc] init];
RetainTracker *tracker = [RetainTracker new];//Count = 1
[tracker retain];//Count = 2 (Count+1)
[tracker autorelease];//将tracker对象添加到自动释放池, Count = 2
[tracker release];//Count = 1 (Count-1)
NSLog(@"释放掉自动释放池(release pool)");
[pool release];

//@autorelease 关键字方式自动释放池
@autorelease
{
RetainTracker *tracker2 = [RetainTracker new];//count = 1
[tracker2 retain];//count = 2
[tracker2 autorelease];//count = 2 //将tracker2对象添加到自动释放池
[tracker2 release];//count = 1
NSLog(@"@autorelease关键字,自动释放池!");
}
return 0;
}

Cocoa的内存管理规则

  • 当你使用new、alloc、或copy方法创建一个对象时,该对象的保留计数器的值为1。当不再使用该对象时,你应该向对象发送一条release或autorelease消息。
  • 当你通过其他方法获得一个对象时,假设该对象的保留计数器的值为1,而且已经被设置为自动释放,那么你不需要执行任何操作来确保该对象得到清理。如果你打算在一段时间内拥有该对象,则需要保留它并确保在操作完成时释放它。
  • 如果你保留了某个对象,就需要(最终)释放会自动释放该对象。必须保持
    retain
    方法和
    release
    方法的使用次数相等。

临时对象

接下来我们通过代码来看看一些常用的内存管理生命周期例子。

//用new、alloc、copy创建的对象要自己来释放。
NSMutableArray *array;
array = [[NSMutalbleArray alloc] init];//调用alloc,保留计数器值=1
[array release];//发送release消息,保留计数器值=0
NSMutableArray *array = [NSMutableArray arrayWithCapacity:16];//count = 1,并且设置为了autorelease
//这个arrayWithCapacity创建的对象,不需要我们手动去release释放它,它会自动添加到releasepool,在自动释放池销毁掉的时候自动给array对象发送release消息,来进行释放。
NSColor *color;
color = [NSColor blueColor];
//blueColor方法也不属于alloc、new、copy这三个方法,所以也不需要进行手动释放,当它用blueColor创建对象的时候,会被添加到自动释放池,我们不需要手动来对他进行释放。

拥有对象

有时候,你可能希望在多段代码中一直拥有某个对象。典型的方法是把它们加入到诸如NSArray或者NSDicrionary等集合中,作为其他对象的实例变量来使用。

手动释放

-(void) doStuff
{
flonkArray = [NSMutableArray new];
}

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

自动释放

-(void) doStuff
{
//通过非alloc、new、copy函数创建的对象会添加到autorelease中。
flonkArray = [NSMutableArray arrayWithCapacity: 16];
[flonkArray retain];//count = 2
//autorelease后变成1
}

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

仔细观察这一段代码,指出哪里有问题?

int i;
for(i=0;i<1000000;i++)
{
id object = [someArray objectAtIndex:i];
NSString *desc = [object description];
}

首先,可以看出这段代码会循环1000000次,然后someArray类发送objectAtIndex消息创建了一个对象object。

object对象调用description消息会调用NSLog输出消息,接着也会创建一个对象desc。

所以说,这里两个对象都是通过非alloc、new、copy创建的,他们会添加到自动释放池。这就创建了1000000个自动释放池,大量的字符串占用了内存。自动释放池在for循环中并不会被销毁,所以这段期间电脑内存占用率会很高,从而影响用户体验。

改良后:

NSAutoreleasePool *pool;
pool = [[NSAutoreleasePool alloc] init];//创建自动释放池

int i;
for(i = 0;i<1000000;i++)
{
id object = [someArray objectAtIndex:i];
NSString *desc = [object description];

if(i % 1000 == 0)
{
[pool release];//当i=1000时候,销毁自动释放池。也就是当字符串超过1000就开始释放内存了!
pool = [[NSAutoreleasePool alloc] init];//再创建新的自动释放池
}
}
[pool release];

改见后的代码在循环1000次以后,就会释放自动释放池。这样就解决了字符串太多占用内存的问题。

垃圾回收

Object-C 2.0
后引入了自动内存管理机制,也就是垃圾回收。

熟悉

Java
Python
等语言的程序员应该非常熟悉垃圾回收的概念。对于已经创建和使用的对象,当你忘记清理时,系统会自动识别哪些对象仍在使用,哪些对象可以回收。

在Xcode13中,默认是开启垃圾回收功能的。注意!垃圾回收机制只能在macOS开发中用到,iOS开发暂不支持垃圾回收机制。

自动引用计数

iOS无法使用垃圾回收机制

在iOS开发中为什么无法使用垃圾回收机制,这是怎么回事?

主要的原因是因为你无法知道垃圾回收器什么时候回起作用。就像在现实生活中,你可能知道周一是垃圾回收日,但是不知道精确时间。假如你正要出门的时候,垃圾车到了该怎么办?垃圾回收机制会对移动设备的可用性产生非常不利的影响,因为移动设备比电脑更加私人化,资源更少。用户可不想再玩游戏或者打电话的时候因为系统突然进行内存清理而卡住。

ARC介绍

苹果公司的解决方案被称为

自动引用计数
(
automatic refernce countring
),简称:
ARC

顾名思义:

ARC
会追踪你的对象并确定哪一个仍会使用而哪一个不会再使用,就好像你有了一位专门负责内存管理的管家或私人助理。如果你启用了
ARC
,只管像平常那样按需分配并使用对象,编译器会帮你插入
retain
release
语言,无需你自己动手。

ARC
不是垃圾回收器。我们已经讨论过了,垃圾回收器在运行时工作,通过返回的代码来定期检查对象。

与此相反,

ARC
是在编译时进行工作的。它在代码中插入了合适的
retain
release
语句,就好像是你自己手动写好了所有的内存管理代码。不过编译器替你完成了内存管理的工作。

ARC条件

如果你想要在代码中使用

ARC
,必须满足以下三个条件:

  • 能够确定哪些对象需要进行内存管理;

  • 能够表明如何去管理对象;

  • 有可行的办法传递对象的所有权。

    第一个条件是最上层集合知道如何去管理他的子对象。

第一个条件例子:

这段代码创建了指向10个字符串的C型数组。因为C型数组是不可保留的对象,所以你无法在这个结构体里使ARC特性。

NSString **myString;
myString = malloc(10 * sizeof(NSString *));

第二个条件是你必须能够对某个对象的保留计数器的值进行加1或减1的操作。也就是说所有

NSObject
类的子类都能进行内存管理。这包括了大部分你需要管理的对象。

第三个条件是在传递对象的时候,你的程序需要能够在调用者和接收者(后面会详细介绍)之间传递所有权。

弱引用(Weak)、强引用

强引用:当用指针指向某个对象时,你可以管理他的内存(retain、release),如果你管理了那么你就拥有了这个对象的

强引用(strong refernce)
。如果你没有管理,那么你就拥有的是
弱引用(weak refernce)

当对象A创建出了对象B,然后对象B有一个指向对象A的强引用。

当对象A的拥有者不再需要需要它的时候,发送release消息,这时候对象A、B的值都还是1,引发了内存泄露!

解决方案:对象B通过

弱应用(weak refernce)
来指向对象A,并且记得清空弱引用对象。

__weak NSString *myString;
@proerty(weak) NSString* myString;

//如果有些比较老旧的系统不支持arc,就用如下方法
__unsafe_unretained

​ 使用ARC的时候两种命名规则需要注意:

  • 属性不能以new 开头,比如说@property NSString *newString;//是不被允许的? Why????
  • 属性不能只有一个read-only而没有内存管理特性。如果你没有启用ARC,可以使用@property(readonly) NSString *title,

拥有者权限

之前说过指针支持ARC的一个条件是必须是可保留对象指针(ROP)。

这意味着你不能简单的 将一个ROP表示成不可保留对象指针(non-ROP),因为指针的所有权会移交。

NSString *theString = @"Learn Objective-C";
CFStringRef cfString = (CFStringRef) theString;

theString指针是一个ROP,而另外一个cfString则不是。为了让ARC便于工作,需要告诉编译器哪个对象是指针的拥有者。

//(__bridge类型)操作符
//这种类型的转换会传递指针但不会传递它的所有权。
{
NSString *theString = @"Learn Objective-C";
CFStringRef cfString = (__bridge CFStringRef)theString;
}

//(__bridge_retained类型)操作符
//这种类型,所有权会转移到non-ROp上。
{
NSString *theString = @"Lean Objective-C";
CFStringRef cfString = (__bridge_retained CFStringRef)theString;
}

//(__bridge_transfer类型)操作符
//这种转换类型与上一个相反,它把所有权交给ROP
{
NSString *theString = @"Lean Objective-C";
CFStringRef cfString = (__bridge  CFStringRef)theString;
}

异常

与异常有关的关键字

  • @try:定义用来测试的代码块是否要抛出异常。
  • @catch():定义用来处理已抛出异常的代码块。
  • @finally:定义无论如何是否有抛出异常都会执行代码块。
  • @throw:抛出异常。

捕捉不同类型的异常

@try
{

}@catch(NSException *exception){

}@catch(MyCustomException *custom){

}@catch(id value){

}@finally
{

}

抛出异常

@try
{
NSException *e = @"error";
@throw e;
}@catch(NSException *e){
@throw;
}

异常也需要内存管理

-(void) mySimpleMethod
{
NSDictionary *dictionary = [[NSDictionary alloc] initWith....];
@try{
[self processDictionary:dictionary];
}
@finally{
[dictionary release];//finally中的代码会比trhow之前运行。
}
}

异常和自动释放池

-(void) myMethod
{
id savedException = nil;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSDictionary *myDictionary = [[NSDictionary alloc] initWith....];
@try{
[self processDictionary:myDictionary];
}@catch(NSException *e){
savedException = [e retain];
@throw;
}@finally{
[pool release];
[savedException autorelease];
}
}

通过使用retain方法,我们在当前池中保留了异常。当池被释放时,我们早已保存了一个异常指针,它会同当前池一同释放。

小结

本章介绍了

Cocoa
的内存管理方法:
retain
release
autorelease
,还讨论了垃圾回收和
自动应用技术
(
ARC
)。

Cocoa中有三个关于对象及其保留计数器的规则:

  • 如果使用new、alloc、copy操作获得了一个对象,则该对象的保留计数器的值为1.
  • 如果通过其他方法获得一个对象,则假设该对象的保留计数器的值为1,而且已经被设置为自动释放。
  • 如果保留了其对象,则必须保持
    retain
    方法和
    release
    方法的使用次数相等。

ARC技术会在编译过程中,编译器自动插入

retain
release
这些语句帮你完成内存释放和保留。

Pwn菜鸡学习小分队

欢迎来PWN菜鸡小分队闲聊:PWN、RE 或者摸鱼小技巧。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: