你为什么要用ivar?

时间:2012-01-31 20:51:08

标签: objective-c ios memory-management key-value-observing ivar

我通常会以另一种方式看到这个问题,例如Must every ivar be a property?(我喜欢bbum对此问题的回答)。

我几乎只在我的代码中使用属性。然而,每隔一段时间,我就与一位长期在iOS上开发并且是传统游戏程序员的承包商合作。他编写的代码几乎没有声明任何属性,并且依赖于ivars。我认为他这样做是因为1.)他已经习惯了,因为在Objective C 2。0(2007年10月)和2)之前,属性并不总是存在,因为最小的性能增益是不通过getter / setter。

虽然他编写了不泄漏的代码,但我仍然希望他使用ivars上的属性。我们谈到了它,他或多或少看不到使用属性的理由,因为我们没有使用KVO,而且他在处理内存问题方面经验丰富。

我的问题更多......为什么你会想要使用伊瓦尔时期 - 经验丰富与否。使用ivar的性能差异真的很合理吗?

另外作为澄清点,我根据需要覆盖了setter和getter,并使用与getter / setter中的属性相关的ivar。但是,在getter / setter或init之外,我总是使用self.myProperty语法。


编辑1

我感谢所有好的回应。我想解决的一个看起来不正确的是,使用ivar你可以获得封装,而不是使用属性。只需在类继续中定义属性即可。这会将财产隐藏在外人之外。您还可以在接口中声明属性readonly,并在实现中将其重新定义为readwrite,如:

// readonly for outsiders
@property (nonatomic, copy, readonly) NSString * name;

并在课堂上继续:

// readwrite within this file
@property (nonatomic, copy) NSString * name;

让它完全“私有”只在类继续中声明它。

7 个答案:

答案 0 :(得分:96)

封装

如果ivar是私有的,程序的其他部分就不能轻易搞定。通过声明的属性,聪明的人可以通过访问器轻松访问和变异。

性能

是的,这在某些情况下会有所不同。某些程序存在约束,它们无法在程序的某些部分使用任何objc消息(实时思考)。在其他情况下,您可能希望直接访问它以提高速度。在其他情况下,这是因为objc消息传递充当优化防火墙。最后,它可以减少您的引用计数操作并最大限度地减少峰值内存使用(如果正确完成)。

非平凡类型

示例:如果您有C ++类型,有时直接访问就是更好的方法。该类型可能无法复制,或者复制可能并不容易。

多线程

你的许多ivars都是相互依赖的。您必须确保多线程上下文中的数据完整性。因此,您可能倾向于直接访问关键部分中的多个成员。如果您坚持使用访问器来获取相关数据,那么您的锁通常必须是可重入的,并且您通常最终会进行更多的采集(有时会更多)。

程序正确性

由于子类可以覆盖任何方法,因此最终可能会发现在写入接口与正确管理状态之间存在语义差异。程序正确性的直接访问在部分构造状态中尤为常见 - 在初始化程序和dealloc中,最好使用直接访问。您还可以在访问器,便捷构造函数,copymutableCopy以及归档/序列化实现的实现中找到这种常见。

当一个人从一个公共读写访问器思维模式转移到一个隐藏其实现细节/数据的思维模式时,它也会更频繁。有时你需要正确地绕过子类'覆盖可能引入的副作用才能做正确的事。

二进制大小

默认情况下声明所有读写操作通常会导致您在程序执行一会儿时不需要许多访问器方法。因此,它会为你的程序和加载时间增加一些脂肪。

最小化复杂性

在某些情况下,完全没必要添加+ type +为一个简单的变量维护所有额外的脚手架,例如用一个方法编写并在另一个方法中读取的私有bool。


根本不是说使用属性或访问器是不好的 - 每个都有重要的好处和限制。与许多OO语言和设计方法一样,您也应该倾向于在ObjC中具有适当可见性的访问者。有时你需要偏离。出于这个原因,我认为通常最好限制对声明ivar的实现的直接访问(例如声明它@private)。


重新编辑1:

我们大多数人都记得如何动态调用隐藏的访问器(只要我们知道名称......)。与此同时,我们大多数人都没有记住了如何正确访问不可见的ivars(超出KVC)。类继续帮助,但它确实引入了漏洞。

这种解决方法很明显:

if ([obj respondsToSelector:(@selector(setName:)])
  [(id)obj setName:@"Al Paca"];

现在只用ivar尝试,没有KVC。

答案 1 :(得分:72)

对我而言,通常是表演。访问对象的ivar与使用指向包含此类结构的内存的指针访问C中的struct成员一样快。实际上,Objective-C对象基本上是位于动态分配内存中的C结构。这通常和代码一样快,甚至手动优化的汇编代码也不会比这更快。

通过getter / setting访问一个ivar涉及一个Objective-C方法调用,它比一个" normal"慢得多(至少3-4倍)。 C函数调用甚至普通的C函数调用已经比访问struct成员慢几倍。根据属性的属性,编译器生成的setter / getter实现可能涉及对函数objc_getProperty / objc_setProperty的另一个C函数调用,因为它们必须retain / {根据需要{1}} / copy对象,并在必要时进一步对原子属性执行自旋锁定。这很容易变得非常昂贵,而且我并不是说要慢50%。

让我们试试这个:

autorelease

输出:

CFAbsoluteTime cft;
unsigned const kRuns = 1000 * 1000 * 1000;

cft = CFAbsoluteTimeGetCurrent();
for (unsigned i = 0; i < kRuns; i++) {
    testIVar = i;
}
cft = CFAbsoluteTimeGetCurrent() - cft;
NSLog(@"1: %.1f picoseconds/run", (cft * 10000000000.0) / kRuns);

cft = CFAbsoluteTimeGetCurrent();
for (unsigned i = 0; i < kRuns; i++) {
    [self setTestIVar:i];
}
cft = CFAbsoluteTimeGetCurrent() - cft;
NSLog(@"2: %.1f picoseconds/run", (cft * 10000000000.0) / kRuns);

慢了4.28倍,这是一个非原子的原始int,几乎是最好的情况;大多数其他情况甚至更糟(尝试原子1: 23.0 picoseconds/run 2: 98.4 picoseconds/run 属性!)。因此,如果您可以接受每个ivar访问速度比它可能慢4-5倍这一事实,那么使用属性很好(至少在性能方面),但是,有很多情况下这样的性能下降是完全不能接受。

更新2015-10-20

有些人认为,这不是一个现实世界的问题,上面的代码纯粹是合成的,你永远不会注意到它在真实的应用程序中。那么,让我们试试一个真实世界的样本吧。

下面的代码定义了NSString *个对象。帐户具有描述其所有者的名称(Account),性别(NSString *)和年龄(enum)以及余额(unsigned)的属性。帐户对象具有int64_t方法和init方法。 compare:方法定义为:男性订单在男性之前,名称按字母顺序排列,年轻订单在旧之前,平衡订单从低到高。

实际上有两个帐户类compare:AccountA。如果您查看其实现,您会注意到它们几乎完全相同,但有一个例外:AccountB方法。 compare:个对象通过方法(getter)访问他们自己的属性,而AccountA个对象通过ivar访问他们自己的属性。这真的是唯一的区别!它们都访问另一个对象的属性以通过getter进行比较(通过ivar访问它是不安全的!如果另一个对象是子类并覆盖了getter会怎样?)。另请注意,以ivars 访问您自己的属性不会破坏封装(ivars仍然不公开)。

测试设置非常简单:创建1个Mio随机帐户,将它们添加到数组并对该数组进行排序。就是这样。当然,有两个数组,一个用于AccountB个对象,另一个用于AccountA个对象,两个数组都填充相同的帐户(相同的数据源)。我们计算对数组进行排序所需的时间。

这是我昨天做过的几次跑步的输出:

AccountB

正如您所看到的,排序runTime 1: 4.827070, 5.002070, 5.014527, 5.019014, 5.123039 runTime 2: 3.835088, 3.804666, 3.792654, 3.796857, 3.871076 对象数组总是显着更快比排序AccountB对象数组。

无论谁声称运行时差异高达1.32秒没有任何区别,最好不要进行UI编程。例如,如果我想更改大表的排序顺序,那么这些时间差异会对用户产生巨大影响(可接受和缓慢的UI之间的差异)。

同样在这种情况下,示例代码是此处执行的唯一真实工作,但是您的代码多长时间只是复杂发条的一小部分?如果每个齿轮都像这样放慢整个过程,这对整个发条的速度到底意味着什么呢?特别是如果一个工作步骤取决于另一个工作步骤的输出,这意味着所有低效率将总结。大多数低效率本身并不是问题,它们的纯粹总和成为整个过程的问题。而这样的问题并不是简介者能够轻易展示的,因为剖析器是关于找到关键的热点,但这些低效率都不是他们自己的热点。 CPU时间在它们之间平均分布,但它们每个只有它的一小部分,似乎完全浪费时间来优化它。这是真的,优化其中一个绝对没有任何帮助,优化所有这些都可以大大提高。

即使你不考虑CPU时间,因为你认为浪费CPU时间是完全可以接受的,毕竟&#34;它是免费的&#34;,那么服务器怎么样?由功耗引起的托管成本?那么移动设备的电池运行时间呢?如果你要写两次相同的移动应用程序(例如一个自己的移动网络浏览器),一旦所有类只通过getter访问自己的属性的版本和一次所有类只通过ivars访问它们的版本,使用第一个经常会消耗电池比使用第二个电池要快得多,即使它们功能相同,对于用户来说,第二个甚至可能感觉更快一些。

现在这里是AccountA文件的代码(代码依赖于启用ARC并确保在编译时使用优化以查看完整效果):

main.m

答案 2 :(得分:8)

最重要的原因是信息隐藏的OOP概念:如果您通过属性公开所有内容,从而允许外部对象窥视另一个对象的内部,那么您将利用这些内部因此更改实施变得复杂。

“最低性能”增益可以快速总结,然后成为一个问题。我从经验中知道;我正在开发一个真正使iDevices达到极限的应用程序,因此我们需要避免不必要的方法调用(当然只有在合理的情况下)。为了实现这一目标,我们还避免使用点语法,因为它很难在第一眼看到方法调用的数量:例如,表达式self.image.size.width触发了多少个方法调用?相比之下,您可以立即告诉[[self image] size].width

此外,通过正确的ivar命名,KVO可能没有属性(IIRC,我不是KVO专家)。

答案 3 :(得分:8)

<强>语义

  • @property可以表达ivars不能:nonatomiccopy
  • 哪些ivars可以表达@property不能:
    • @protected:子类公开,外部私有。
    • @package:公开64位的框架,私有外部。与32位上的@public相同。请参阅Apple的64-bit Class and Instance Variable Access Control
    • 限定符。例如,强对象引用的数组:id __strong *_objs

<强>性能

简短的故事:ivars更快,但对大多数用途来说无关紧要。 nonatomic属性不使用锁,但直接ivar更快,因为它会跳过访问者调用。有关详细信息,请阅读lists.apple.com上的以下email

Subject: Re: when do you use properties vs. ivars?
From: John McCall <email@hidden>
Date: Sun, 17 Mar 2013 15:10:46 -0700
     

属性以多种方式影响性能:

     
      
  1. 如前所述,发送邮件进行加载/存储的速度比单独执行内联加载/存储更慢。

  2.   
  3. 发送消息以进行加载/存储也需要保存在i-cache中更多代码:即使getter / setter也是如此   除了加载/存储之外还添加了零额外的指令,有一个   在调用者中设置固定的半打额外指令   消息发送并处理结果。

  4.   
  5. 发送消息会强制该选择器的条目保留在方法缓存中,并且该内存通常会在   d高速缓存。这增加了启动时间,增加了静态内存   使用您的应用程序,并使上下文切换更痛苦。自从   方法缓存特定于对象的动态类,这个   问题越多,你就越多地使用KVO。

  6.   
  7. 发送消息强制函数中的所有值溢出到堆栈(或保存在被调用者保存寄存器中,这只是意味着   在不同的时间溢出。)

  8.   
  9. 发送消息可以有任意副作用,因此

         
        
    • 强制编译器重置有关非本地内存的所有假设
    •   
    • 不能悬挂,沉没,重新订购,合并或消除。

    •   
  10.   
  11. 在ARC中,消息发送的结果将始终被保留,无论是被调用者还是调用者,即使是+0返回:即使   方法不保留/自动释放其结果,调用者不知道   那并且必须尝试采取行动以防止结果得到   自动释放。这是永远无法消除的,因为消息发送是   不可静态分析。

  12.   
  13. 在ARC中,因为setter方法通常将其参数取为+0,所以无法“转移”该对象的保留(如   如上所述,ARC通常有)进入ivar,所以的价值   通常必须保留/释放两次

  14.         

    这些都不意味着它们总是坏的,当然 - 有一个   使用属性的很多好理由。请记住,就像   许多其他语言功能,他们不是免费的。

         


    约翰。

答案 4 :(得分:5)

属性将变量公开给其他类。如果您只需要一个仅与您正在创建的类相关的变量,请使用实例变量。这是一个小例子:用于解析RSS等的XML类循环通过一堆委托方法等。使用NSMutableString的实例来存储解析的每个不同传递的结果是切实可行的。没有理由为什么外部类需要访问或操纵该字符串。因此,您只需在标题中或私下声明它,并在整个类中访问它。为它设置属性可能仅对确保没有内存问题有用,使用self.mutableString来调用getter / setter。

答案 5 :(得分:5)

向后兼容性对我来说是一个因素。我无法使用任何Objective-C 2.0功能,因为我正在开发必须在Mac OS X 10.3上运行的软件和打印机驱动程序作为要求的一部分。我知道您的问题似乎针对iOS,但我认为我仍然会分享不使用属性的原因。

答案 6 :(得分:5)

属性与实例变量是一种权衡,最终选择归结为应用程序。

封装/信息隐藏从设计角度来看,这是一件好事(TM),狭窄的界面和最小的连接是使软件易于维护和理解的原因。 Obj-C很难隐藏任何东西,但实现中声明的实例变量尽可能接近。

性能虽然“过早优化”是Bad Thing(TM),但编写性能不佳的代码只是因为您至少可以做到这一点。很难反对方法调用比加载或存储更昂贵,而且在计算密集型代码中,成本很快就会增加。

在具有属性的静态语言(例如C#)中,对setter / getter的调用通常可以由编译器优化掉。然而,Obj-C是动态的,删除这样的调用要困难得多。

抽象 Obj-C中针对实例变量的参数传统上是内存管理。使用MRC实例变量需要调用保留/释放/自动释放以在整个代码中传播,属性(合成与否)将MRC代码保存在一个地方 - 抽象原理是Good Thing(TM)。然而,对于GC或ARC,这个论点消失了,因此内存管理的抽象不再是实例变量的参数。