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

前言:

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

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

对象生命周期

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

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

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

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

引用计数

现在,对象何时诞生我们已经很清楚了,而且也讨论了如何使用对象,但是怎么知道对象生命周期结束了呢?Cocoa采用了一种叫做引用计数(reference counting)的技术,有也叫做保留计数(retain counting)

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

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

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

  • allocnew方法或者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

最后输出如下图:

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

对象所有权

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

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

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

当多个实例拥有某个特定的对象时,对象的所有权关系就更加复杂了,这也就是是保留计数器的值大于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生成代码的时候见过@autoreleasepoolNSAutoreleasePool。那么对象池到底是个什么东西?从名字上看他大概应该是一个存放对象的池子(集合)。

-(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后引入了自动内存管理机制,也就是垃圾回收。

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

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

自动引用计数

iOS无法使用垃圾回收机制

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

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

ARC介绍

苹果公司的解决方案被称为自动引用计数(automatic refernce countring),简称:ARC

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

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

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

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的内存管理方法:retainreleaseautorelease,还讨论了垃圾回收和自动应用技术(ARC)。

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

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

ARC技术会在编译过程中,编译器自动插入retainrelease这些语句帮你完成内存释放和保留。

Pwn菜鸡学习小分队

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

Objective-C 基础教程第九章,内存管理的更多相关文章

  1. [学习笔记—Objective-C]《Objective-C-基础教程 第2版》第九章 内存管理

    内存管理: 确保在须要的时候分配内存,在程序运行结束时释放占用的内存 假设仅仅分配内存而不释放内存,则会发生内存泄漏(leak memory),程序的内存占用量不断添加.终于会被耗尽并导致程序崩溃. ...

  2. 村田噪声抑制基础教程-第一章 需要EMI静噪滤波器的原因

    1-1. 简介 EMI静噪滤波器 (EMIFIL®) 是为电子设备提供电磁噪声抑制的电子元件,配合屏蔽罩和其他保护装置一起使用.这种滤波器仅从通过连线传导的电流中提取并移除引起电磁噪声的元件.第1章说 ...

  3. Java基础教程:Java内存区域

    Java基础教程:Java内存区域 运行时数据区域 Java虚拟机在执行Java程序的过程种会把它所管理的内存划分为若干个不同的数据区域.这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟 ...

  4. 【翻译】《深入解析windows操作系统第6版下册》第10章:内存管理

    [翻译]<深入解析windows操作系统第6版下册>第10章:内存管理(第一部分) [翻译]<深入解析windows操作系统第6版下册>第10章:内存管理(第二部分) [翻译] ...

  5. 【CUDA 基础】4.2 内存管理

    title: [CUDA 基础]4.2 内存管理 categories: - CUDA - Freshman tags: - CUDA内存管理 - CUDA内存分配和释放 - CUDA内存传输 - 固 ...

  6. JZ2440 裸机驱动 第7章 内存管理单元MMU

    本章目标:     了解虚拟地址和物理地址的关系:     掌握如何通过设置MMU来控制虚拟地址到物理地址的转化:     了解MMU的内存访问权限机制:     了解TLB.Cache.Write ...

  7. c语言基础学习08_关于内存管理的复习

    =============================================================================对于c语言来讲,内存管理是一个很重要的内容,它 ...

  8. 重读金典------高质量C编程指南(林锐)-------第七章 内存管理

    2015/12/10补充: 当我们需要给一个数组返回所赋的值的时候,我们需要传入指针的指针.当我们只需要一个值的时候,传入指针即可,或者引用也可以. 结构大致如下: char* p = (char*) ...

  9. 【译】x86程序员手册13-第5章 内存管理

    Chapter 5 Memory Management 内存管理 The 80386 transforms logical addresses (i.e., addresses as viewed b ...

随机推荐

  1. 你应该知道的Redis事务

    前两篇 Redis 文章都大几千字,今天我们换个小清新点的 如果你也了解过关系型数据库事务的话,相信这篇文章对你来说是很容易理解的了.具体什么是事务我就不说不多了,直接讲 Redis 事务相关的部分. ...

  2. C++ cout 数字之间进制的转换

    转换一个数变成8进制,则为 cout << oct << x << endl; 转换一个数变为16进制,为 cout << hex << x ...

  3. 设置一段文字的大小为6px?

    谷歌最小12px, 其他浏览器可以更小 通过transform: scale实现

  4. 数据库连接(Database link)?

    在一个用户下,可以获取到另外的用户下的表的数据,通常在跨数据库时使用. create database link link93 connect to scott identified by tiger ...

  5. Springboot添加静态资源映射addResourceHandlers,可实现url访问

    @Configuration //public class WebMvcConfiger extends WebMvcConfigurerAdapter { public class WebMvcCo ...

  6. C与C++的区别之函数调用堆栈

    函数调用栈 1.函数参数带入(入调用方函数的栈,从右向左入栈) int fun(int a); int fun(int a, int b); int fun(int a, int b, int c); ...

  7. ECMAScript中有两种属性:数据属性和访问器属性。

    ECMA-262定义这些特性是为了实现JavaScript引擎用的,因此在JavaScript中不能直接访问它们.为了表示特性是内部值,该规范把它们放在了两对儿方括号中,例如 [[Enumerable ...

  8. 微信小程序登录鉴权流程图

  9. Linux 0.11源码阅读笔记-总览

    Linux 0.11源码阅读笔记-总览 阅读源码的目的 加深对Linux操作系统的了解,了解Linux操作系统基本架构,熟悉进程管理.内存管理等主要模块知识. 通过阅读教复杂的代码,锻炼自己复杂项目代 ...

  10. 【Android开发】Webview 和 JS 交互问题

    一,安卓原生调用JS代码 1,js代码: function handlePasteDataFromApp(pasteStr) { showInfo('pasteData: aaaaa' + JSON. ...