Object-C中的内存管理

一起跳舞吧 等级 465 0 0

Object-C中的内存管理

原文链接地址:http://www.raywenderlich.com/2657/memory-management-in-objective-c-tutorial

免责申明(必读!):本博客提供的所有教程的翻译原稿均来自于互联网,仅供学习交流之用,切勿进行商业传播。同时,转载时不要移除本申明。如产生任何纠纷,均与本博客所有人、发表该翻译稿之人无任何关系。谢谢合作!

注:本教程由北方和我本人合作翻译。

教程截图:

Object-C中的内存管理

当我检查其他开发人员的代码时,似乎最常见的错误总是围绕在以Object-C中的内存管理为中心。如果您使用的语言是java或C#,它们会自动为您处理内存管理,但这也会使你对于手工内存管理工作更加迷惑。因此,在本教程中,您将通过一些实践来学习Object-C中的内存管理是如何工作的。我们将讨论引用计数如何工作,并通过学习内存管理的所有关键点来构建一个真实世界的例子——一个关于您喜爱的寿司类型的应用程序。

本教程是针对初学者的iOS开发人员或者时关注这个主题的中级开发人员。废话就少啰嗦了,开始编码。

开始

在xcode开发环境中,打开FileNew Project,选择iOSApplicationNavigation-based Application,并将新项目命名为ProMemFun,执行BuildBuild and Run, 在模拟器中你会看到一个如下空表视图:

Object-C中的内存管理

比方说,我们希望在这个列表中填入我们喜爱的寿司类型。最简单的方法是创建一个数组来容下每一种寿司类型的字符串名称,然后每次我们显示一行,从数组中放入合适的字符串到表格中。在rootViewController.h中为寿司类型声明一个实例变量,代码如下:

#import <UIKit/UIKit.h\> @interface RootViewController : UITableViewController {  
    NSArray \* \_sushiTypes;  
}  

@end 

通过这个声明,每个RootViewController实例对象将有空间来存储一个指向NSArray数组的指针,这是一个Object-C类,使用这个数组初始化后就不能改变它。如果你需要更改一个初始化后的数组(例如,添加一项后),你应该使用NSMutableArray替代。

也许你会奇怪,为什么我们在命名的变量前面添加一个下划线?这恰好是我喜欢做的事情,这样做有些事情会变得更容易。在后续的关于Objec-C教程中我将讨论我为什么喜欢这么做,但是现在请注意,到目前为止,我们所作的是仅仅添加了一个实例变量,没有做与属性相关的东东,我们把它命名为“以下划线开头”,这只是一个个人的喜好问题,其实它没有做特别的东西。

现在,打开RootViewController.m文件,注释viewDiaLoad,然后设置以下代码:

\- (void)viewDidLoad {  
    \[super viewDidLoad\];  

    \_sushiTypes \= \[\[NSArray alloc\] initWithObjects:@"California Roll", @"Tuna Roll", @"Salmon Roll", @"Unagi Roll", @"Philadelphia Roll", @"Rainbow Roll", @"Vegetable Roll", @"Spider Roll", @"Shrimp Tempura Roll", @"Cucumber Roll", @"Yellowtail Roll", @"Spicy Tuna Roll", @"Avocado Roll", @"Scallop Roll",  
                   nil\];  
} 

现在我们进入内存管理,Object-C中创建的对象使用的是引用计数。这就意味着每一个对象都跟踪有多少其他的对象引用它。一旦引用计数变为0,这个对象的内存就会安全的释放掉。

作为一个程序员,你要确保对象的引用计数总是准确的。当你在某个地方存储了一个对象的指针(比如是实例变量),你需要增加引用计数,有时候需要递减引用计数。

“我的天啊”,你可能会思考,“这听起来太复杂和混乱了”,不要担心,做起来要比听起来简单些。

初始化对象和释放对象的内存

不管什么时候你在Object-c中创建一个对象,首先你要调用alloc为这个对象去分配内存空间,然后调用init方法去初始化这个对象,当init方法不带任何参数时,有时候你会看到程序员用new方法替代(这类似于先调用alloc,然后调用init)。

  最重要的是一旦你这么做了,你会得到一个新的对象,并且它的引用计数置为1。因此,当完成所有的工作后,你需要递减引用计数。

好了,我们给出一个开头。仍然是在RootViewController.m中,去文件末尾,像下面一样设置viewDidUnload和dealloc方法:

\- (void)viewDidUnload {  
    \[\_sushiTypes release\];  
    \_sushiTypes \= nil;  
} \- (void)dealloc {  
    \[\_sushiTypes release\];  
    \_sushiTypes \= nil;  
    \[super dealloc\];  
} 

记住当你用alloc/init创建一个array时,它的引用计数已经为1了。因此当你完成与array相关的工作时,需要递减它的引用计数。在Object-C中,你可以通过对这个对象调用release方法。

但是你应该在什么地方release呢?哦,你一定要在dealloc方法中release这个array,显然易见,当这个viewController销毁后,你也不会再需要这个array了。所以,记住无论何时你在viewDidLoad中创建一个对象(这个对象的引用计数会初始化为1),你应该在viewDidUnload中释放这个对象。不要太担心,关于这儿主题我会专门写一篇教程。

注意,释放对象后,请将其设置为nil,如果你试图调用一个指向nil的指针,你的程序会崩溃。

好了,现在让我们使用新的array。首先,替换掉tableView:numberOfRowsInSection 里面的"return 0",替换成下面的语句:

// Replace "return 0;" in tableView:numberOfRowsInSection with this  
return \_sushiTypes.count; 

  这里意思是说,tableView里面的数据行数等于sushiTypes数组里面的记录个数。

  现在,我们需要告诉table view,每一行具体显示什么内容。找到tableView:cellForRowAtIndexPath函数,然后找到注释 “Configure the cell”,在后面添加下列代码:

NSString \* sushiName \= \[\_sushiTypes objectAtIndex:indexPath.row\]; // 1  
NSString \* sushiString \= \[\[NSString alloc\] initWithFormat:@"%d: %@",   
        indexPath.row, sushiName\]; // 2  
cell.textLabel.text \= sushiString; // 3  
\[sushiString release\]; // 4 

  让我们一行一行代码解释一下上面的程序:

  1. 根据当前行号查找sushiTypes数组里面对应的字符串
  2. 我们想这样显示字符串:“3: Unagi Roll“,3代表行号,而“Unagi Roll” 是那一行的sushi的名字。要构建一个具有这种格式的字符串的话,你可以用NSString的initWithFormat来轻松构建。记住,当你这样做完之后,返回的字符串的引用计数是1.
  3. 设置当前行的文本为刚刚得到的格式化字符串。当你这样设置之后,text label会把sushiString copy一下。(相应的,其引用计数会加1)
  4. 我们用完sushiString了,因此,调用release把它释放掉。如果你忘了这样做的话,那么这里就会导致一个内存泄漏。因为字符串的引用计数是1,永远也不会得到释放。(即使text label把sushiString释放了一次,也没用。因为刚开始创建的时候是1,赋值的时候为2,然后再label再释放一次,为1。而如果你不调用[sushiString release]的话,那么就会内存泄漏)

  编译并运行,如果一切OK的话,你将会看到sushi的列表。

Object-C中的内存管理

Autorelease Your Potential

  目前为止,你知道了,当你调用alloc/init的时候,引用计数是1,当你用完这个对象的时候,你需要调用release把引用计数变为0.

  接下来,让我们讨论一下另外一种方法----autorelease。

  当你给一个对象发送autorelease消息后,它的意思是说“嘿!我想让你在将来某个时刻被释放掉,比如当前run loop结束的时候。但是,现在我能够使用你”。

  最容易理解的方式就是看代码。修改 tableView:cellForRowAtIndexPath 方法,找到 “Configure the cell”注释,在后面添加下列代码:

NSString \* sushiName \= \[\_sushiTypes objectAtIndex:indexPath.row\]; // 1  
NSString \* sushiString \= \[\[\[NSString alloc\] initWithFormat:@"%d: %@",   
        indexPath.row, sushiName\] autorelease\]; // 2  
cell.textLabel.text \= sushiString; // 3 

  因此,和上一次相比,这里只改了两个地方。首先,你在第二行结尾的时候调用了autorelease。其次,你把最后一行release的调用代码移除掉了。

  接上来,我解释一下。在第2行代码结束的时候,sushiString的引用计数是1,但是,我们给它发送了一个autorelease消息。这意味着,你可以在这个函数里面使用sushiString,但是,一旦下一次run loop被调用的时候,它就会被发送release对象。然后引用计数改为0,那么内存也就被释放掉了。(关于autorelease到底是怎么工作的,我的理解是:每一个线程都有一个autoreleasePool的栈,里面放了很多autoreleasePool对象。当你向一个对象发送autorelease消息之后,就会把该对象加到当前栈顶的autoreleasePool中去。当当前runLoop结束的时候,就会把这个pool销毁,同时对它里面的所有的autorelease对象发送release消息。而autoreleasePool是在当前runLoop开始的时候创建的,并压入栈顶。那么什么是一个runLoop呢?一个UI事件,Timer call, delegate call, 都会是一个新的Runloop。)

  在这个例子中,上面的解决办法非常好,但是,后面我们不会使用它。然而,如果我们想要存储一个变量(但是不retain它),然后在某个地方使用这个变量(比如用户点击某一行的时候,选中那一行),那么我们就有大麻烦了。因为那样我们是在尝试访问一个已经销毁的对象,可想而知,程序肯定是crash拉!

  有时候,当你调用一些方法的时候,你得到的返回给你的对象的引用计数是1,但是,它是一个autorelease的对象。你修改一下tableView:cellForRowAtIndexPath方法,修改成下面的样子,然后你就知道我刚刚讲的是什么意思了:

NSString \* sushiName \= \[\_sushiTypes objectAtIndex:indexPath.row\]; // 1  
NSString \* sushiString \= \[NSString stringWithFormat:@"%d: %@",   
        indexPath.row, sushiName\]; // 2  
cell.textLabel.text \= sushiString; // 3 

  这里代码改变之处是第2行。你不是自己调用 alloc/init/autorelease,而是使用NSString的一个类方法stringWithFormat。这个方法会返回一个引用计数为1的字符串,并且它是一个autorelease的对象。因此,和上面的写法一样,你可以放心的使用这个字符串,但是,如果你不retain它,然后又在后面某个地方使用它的话,那么程序就会崩溃。

  你可能会奇怪,你怎么知道哪些对象返回给你的时候是autorelease的?好吧,让我教你一个简单的惯用法,具体如下:

  • 如果一个方法以init或者copy开头,那么返回给你的对象的引用计数是1,并且这不是一个autorelease的对象。换句话说,你调用这些方法的话,你就对返回的对象负责,你再用完之后必须手动调用release来释放内存。
  • 如果一个方法不是以init或者copy开头的话,那么返回的对象引用计数为1,但是,这是一个autorelease对象。换句话说,你现在可以放心使用此对象,用完之后它会自动释放内存。但是,如果你想在其它地方使用它(比如换个函数),那么,这时,你就需要手动retain它了。

Retain Your Wits

如果你现在有一个autorelease对象,并且像在后面继续使用它,那么该怎么办呢?其实很简单,你只需要对它发送retain消息就OK了。这样会把引用计数变为2,但是,只要出了当前runLoop,那么引用计数又会变为1,那么对象还是不会销毁(因为只有引用计数为0才能销毁)。

  让我们来看看具体怎么做。打开RootViewController.h ,然后在@interface里面添加一个实例变量:

NSString \* \_lastSushiSelected; 

  这里只是定义了一个新的实例变量,它将用来追踪选中的最后那一行的字符串。

  接下来,修改 tableView:didSelectRowAtIndexPath ,修改如下:

\- (void)tableView:(UITableView \*)tableView didSelectRowAtIndexPath:(NSIndexPath \*)indexPath {  

    NSString \* sushiName \= \[\_sushiTypes objectAtIndex:indexPath.row\]; // 1  
 NSString \* sushiString \= \[NSString stringWithFormat:@"%d: %@",   
        indexPath.row, sushiName\]; // 2  
 NSString \* message \= \[NSString stringWithFormat:@"Last sushi: %@.  Cur sushi: %@", \_lastSushiSelected, sushiString\]; // 3  
 UIAlertView \*alertView \= \[\[\[UIAlertView alloc\] initWithTitle:@"Sushi Power!" message:message delegate:nil   
                                               cancelButtonTitle:nil   
                                               otherButtonTitles:@"OK", nil\] autorelease\]; // 4  
 \[alertView show\]; // 5  
 \[\_lastSushiSelected release\]; // 6  
 \_lastSushiSelected \= \[sushiString retain\]; // 7  
 } 

  这里的代码比较多,让我们一行一行来看:

  1. 查找当前行对应的shshiTypes数组里面的字符串。
  2. 根据当前行号构建一个新的字符串。注意,这里使用的是stringWithFormat方法,它返回的是一个autorelease的字符串。因为这个方法并不是以init或者copy开头,所以你就知道。记住,这意味着,你可以在这个函数里面使用此字符串,但是出了这个函数的话,如果你还想继续使用之,那必须要对它发送一个retain消息。
  3. 构建一个消息,用来显示当前选中的sushi和最后选中的sushi。和上面一样,这里也是使用的stringWithFormat方法,它返回的是一个autorelease对象。因为我们只想在这个函数里面使用,所以没有retain。
  4. 创建一个alertView来显示刚刚构建的那个消息。这里是通过alloc/init方式创建的,所以我们需要在之后再发送一个autorelease消息,这样在出了这个函数以后,这个对象就会被释放掉了。
  5. 显示这个alert view。
  6. 再你设置lastSushiSelected实例变量之前,你需要先释放当前的lastSushiSelected实例变量,如果当前实例变是已经是nil的话,也没有关系,因上nil对象可以接收任何消息。
  7. 因为你想在这个函数之外再使用lastSushiSelected这个字符串,所以你需要retain它。

  还有一件事你不能忘记。为了保存不会有任何内存泄漏,你需要在RootViewController的dealloc方法里面调用下面方法来释放内存:

\[\_lastSushiSelected release\];  
\_lastSushiSelected \= nil; 

  基本上,在dealloc方法被里面,你需要对“你负责的对象”发送release消息,并且要把它赋值为nil。

  编译并运行,现在,当你选中一行,你就可以看到下面的屏幕输出了。

Object-C中的内存管理

引用计数相关参考资料

  让我们回顾一下所学的知识:

  • 当你调用alloc/init的时候,你得到一个引用计数是1的对象。
  • 当你用完这个对象之后,你要对它调用release消息,使其引用计数为0,这样它的内存才会被释放掉。
  • 当你调用一个方法,它不是以init或者copy开头的,这时,返回给你的对象是autorelease的,它是一种在将来某个时刻会自动被释放的对象。(这里我也要提醒大家一句,比如你在写一个函数,它的名字是xxx,没有以init或者copy开头,那么记得你返回的对象一定要是autorelease的,否则,别人在使用你这个函数的时候就会把它当前是autorelease的,那么他就不会release它,这样就会造成内存泄漏,千万要切记!!!)
  • 如果你想继续使用autorelease对象,那么你就要给它放送一个retain消息。
  • 如果你使用alloc/init方法创建了一个对象,但是你想让它自己在出了runLoop之后被自动释放的话,那么你可以在alloc/init之后再调用autorelease。这也是一种见得比较多的写法了。比如,cocos2d里面调用[xxx node]的时候,就等于[[[xxx alloc] init]autorelease].

  本教程只讲述了objc内存管理的很基本的部分,如果想获得更多的信息,请参考苹果的文档: Memory Management Programming Guide.

何去何从?

  这里有本教程的完整源代码

  不管你是一个多么优秀的开发者,或者你对内存管理的理解有多么的深入,你还是不可避免地要犯一些内存相关的错误。因此,在我的下一篇教程中,我将教大家如果使用XCode, Instruments, 和 Zombies来检测内存泄漏。因此,提前准备好跟我来吧!  

著作权声明:本文由**http://www.cnblogs.com/andyque翻译,欢迎转载分享。请尊重作者劳动,转载时保留该声明和作者博客链接,谢谢!**

收藏
评论区

相关推荐

前端开发中79条不可忽视的知识点汇总
过往一些不足的地方,通过博客,好好总结一下。 1.css禁用鼠标事件 css .disabled { pointerevents: none; cursor: default; opacity: 0.6; } 2.get/post的理解和他们之间的区别 http 超文本传输协议(HTTP)的设计目的是保证客户机
深度剖析github star数15.1k的开源项目redux-thunk
日益忙碌的一周又过去了,是时候开始每周一次的总结复盘了,今天笔者就来剖析一下github中star数15.1k的开源项目reduxthunk。 作为一名React方向的前端工程师,不管是被面试还是面试别人,大部分都会说起reduxthunk的实现原理,因为它非常经典且有用,而且代码量少的感人,只有短短12行代码,却能解决React开发中同一个函数支持多
Object-C中的内存管理
ObjectC中的内存管理 原文链接地址:http://www.raywenderlich.com/2657/memorymanagementinobjectivectutorial(http://
redhat linux 中用锐捷客服端实现上网
开学了我们学校用的是锐捷的客服端连接外网,window下安装锐捷客服端实现上网很随意,但linux下却不是那么如鱼得水。我们校的很多同学都想用linux系统,但都因为安装锐捷客服端问题,不能上网,望而却步。       如今linux系统越来越火,为了攻破以上问题,结合网络上和自身的知识,总结出下方法: 1.刚开始一直用xrgsu1.1.1.i386.
从中断机制看 React Fiber 技术
带你了解计算机的中断机制(操作系统心脏)是如何提在 React Fiber 中应用及提高了页面渲染性能和用户体验。 前言 React 16 开始,采用了 Fiber 机制替代了原有的同步渲染 VDOM 的方案,提高了页面渲染性能和用户体验。Fiber 究竟是什么,网上也有很多优秀的技术揭秘文章,本篇主要想从计算机的中断机制来聊聊 React Fiber 技术
前端 后端的区别
前台:眼睛看到的东西 关乎体验感。 后台:给管理人看的玩意 前端:程序员在进行编程的时候的代码编写。 后端:对应前端而言的,编写代码基本上是提供给前端调用,是不需要处理UI的内容.
【Flutter实战】单选框和复选框
3.6 单选开关和复选框Material 组件库中提供了Material风格的单选开关Switch和复选框Checkbox,虽然它们都是继承自StatefulWidget,但它们本身不会保存当前选中状态,选中状态都是由父组件来管理的。当Switch或Checkbox被点击时,会触发它们的onChanged回调,我们可以在此回调中处
使用jsp直接执行定时任务
使用jsp直接执行定时任务servicehtml<%@ page import"com.leasing.emogo.framework.util.ApplicationContextUtils" %<%@ page import"job.dsc.GetInfoByAssetPackageJob" %<%@ page contentType
在微前端中,antd icon createFormIconfontCN 的本地化问题
首发于 <a name"tCbT2"</a 前言用过 antd icon 的朋友们可能知道,对于 createFormIconfontCN,其中介绍的用法是: 使用的是外链的方式javascriptimport { createFromIconfontCN } from '@antdesign/icons';const IconFon
前端vue中常用的插件
一.moment时间库 1.安装 npm install moment save2.在main.js中引入 import moment from 'moment'; //导入模块moment.locale('zhcn'); //设置语言 或 moment.lang('zhcn'); Vue.prototype.$moment moment;//赋值使用3.
中高级前端-面试秘籍(一)
引言当下,正面临着近几年来的最严重的互联网寒冬,听得最多的一句话便是:相见于江湖🤣。缩减HC、裁员不绝于耳,大家都是人心惶惶,年前如此,年后想必肯定又是一场更为惨烈的江湖厮杀。但博主始终相信,寒冬之中,人才更是尤为珍贵。只要有过硬的操作和装备,在逆风局下,同样也能来一波收割翻盘。面试固然有技巧,但绝不是伪造与吹流弊,通过一段短时间沉下心来闭关修炼,出山收割,
中高级前端-面试秘籍(二)
引言大家知道,React 现在已经在前端开发中占据了主导的地位。优异的性能,强大的生态,让其无法阻挡。博主面的 5 家公司,全部是 React 技术栈。据我所知,大厂也大部分以 React 作为主技术栈。React 也成为了面试中并不可少的一环。 本来是计划只有上下两篇,可是写着写着越写越多,受限于篇幅,也为了有更好的阅读体验,只好拆分出中篇,希望各位童鞋别
中高级前端-面试秘籍(三)
引言本篇文章会继续沿着前面两篇的脚步,继续梳理前端领域一些比较主流的进阶知识点,力求能让大家在横向层面有个全面的概念。能在面试时有限的时间里,能够快速抓住重点与面试官交流。这些知识点属于加分项,如果能在面试时从容侃侃而谈,想必面试官会记忆深刻,为你折服的🤤另外有许多童鞋提到: 面试造火箭,实践全不会,对这种应试策略表达一些担忧。其实我是觉得面试或者这些知识点
JAVA回调机制(CallBack)之小红是怎样买到房子的??
JAVA回调机制CallBack 序言最近学习java,接触到了回调机制(CallBack)。初识时感觉比较混乱,而且在网上搜索到的相关的讲解,要么一言带过,要么说的比较单纯的像是给CallBack做了一个定义。当然了,我在理解了回调之后,再去看网上的各种讲解,确实没什么问题。但是,对于初学的我来说,缺了一个循序渐进的过程。此处,将我对回调机制的个人理解,按
【渗透测试】内网渗透中的端口转发
一、内网渗透中的端口转发需要知道,在渗透的整个流程中,根据web应用或者开放的端口进行渗透,获取只是渗透这门艺术的一小部分,在真实的错综复杂的企业环境中进行内网渗透,其实水很深,涉及内网穿透,端口转发,域渗透,提权等一系列的难题。所以这篇文章对内网渗透学习进行一些记录。在这过程中参考了很多前辈们的宝贵经验,我把这些链接进行了收集,放在参考文献部分。 二、转发