你为什么要用ivar?

Sam*_*Sam 151 memory-management objective-c key-value-observing ios ivar

我通常会看到这个问题是另一种方式,例如每个伊娃都必须是财产吗?(我喜欢bbum对这个Q的回答).

我几乎只在我的代码中使用属性.然而,每隔一段时间,我就与一位长期在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;
Run Code Online (Sandbox Code Playgroud)

并在课堂上继续:

// readwrite within this file
@property (nonatomic, copy) NSString * name;
Run Code Online (Sandbox Code Playgroud)

让它完全"私有"只在类继续中声明它.

jus*_*tin 100

封装

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

性能

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

非平凡的类型

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

多线程

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

程序正确性

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

它也更频繁,因为从一切都有一个公共读写访问者心态到一个隐藏其实现细节/数据的思维模式.有时您需要正确地绕过子类'覆盖可能引入的副作用才能做正确的事情.

二进制大小

默认情况下声明所有读写操作通常会导致许多您永远不需要的访问器方法,当您考虑程序执行一会儿时.所以它会给你的程序和加载时间增加一些脂肪.

最大限度地减少复杂性

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


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


重编辑1:

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

这种解决方法显而易见:

if ([obj respondsToSelector:(@selector(setName:)])
  [(id)obj setName:@"Al Paca"];
Run Code Online (Sandbox Code Playgroud)

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


Mec*_*cki 76

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

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

我们试试这个:

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);
Run Code Online (Sandbox Code Playgroud)

输出:

1: 23.0 picoseconds/run
2: 98.4 picoseconds/run
Run Code Online (Sandbox Code Playgroud)

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

更新2015-10-20

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

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

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

测试设置非常简单:创建1个Mio随机帐户,将它们添加到数组并对该数组进行排序.而已.当然,有两个数组,一个用于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
Run Code Online (Sandbox Code Playgroud)

如您所见,对AccountB对象数组进行排序总是比排序AccountA对象数组快得多.

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

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

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

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

#import <Foundation/Foundation.h>

typedef NS_ENUM(int, Gender) {
    GenderMale,
    GenderFemale
};


@interface AccountA : NSObject
    @property (nonatomic) unsigned age;
    @property (nonatomic) Gender gender;
    @property (nonatomic) int64_t balance;
    @property (nonatomic,nonnull,copy) NSString * name;

    - (NSComparisonResult)compare:(nonnull AccountA *const)account;

    - (nonnull instancetype)initWithName:(nonnull NSString *const)name
        age:(const unsigned)age gender:(const Gender)gender
        balance:(const int64_t)balance;
@end


@interface AccountB : NSObject
    @property (nonatomic) unsigned age;
    @property (nonatomic) Gender gender;
    @property (nonatomic) int64_t balance;
    @property (nonatomic,nonnull,copy) NSString * name;

    - (NSComparisonResult)compare:(nonnull AccountB *const)account;

    - (nonnull instancetype)initWithName:(nonnull NSString *const)name
        age:(const unsigned)age gender:(const Gender)gender
        balance:(const int64_t)balance;
@end


static
NSMutableArray * allAcocuntsA;

static
NSMutableArray * allAccountsB;

static
int64_t getRandom ( const uint64_t min, const uint64_t max ) {
    assert(min <= max);
    uint64_t rnd = arc4random(); // arc4random() returns a 32 bit value only
    rnd = (rnd << 32) | arc4random();
    rnd = rnd % ((max + 1) - min); // Trim it to range
    return (rnd + min); // Lift it up to min value
}

static
void createAccounts ( const NSUInteger ammount ) {
    NSArray *const maleNames = @[
        @"Noah", @"Liam", @"Mason", @"Jacob", @"William",
        @"Ethan", @"Michael", @"Alexander", @"James", @"Daniel"
    ];
    NSArray *const femaleNames = @[
        @"Emma", @"Olivia", @"Sophia", @"Isabella", @"Ava",
        @"Mia", @"Emily", @"Abigail", @"Madison", @"Charlotte"
    ];
    const NSUInteger nameCount = maleNames.count;
    assert(maleNames.count == femaleNames.count); // Better be safe than sorry

    allAcocuntsA = [NSMutableArray arrayWithCapacity:ammount];
    allAccountsB = [NSMutableArray arrayWithCapacity:ammount];

    for (uint64_t i = 0; i < ammount; i++) {
        const Gender g = (getRandom(0, 1) == 0 ? GenderMale : GenderFemale);
        const unsigned age = (unsigned)getRandom(18, 120);
        const int64_t balance = (int64_t)getRandom(0, 200000000) - 100000000;

        NSArray *const nameArray = (g == GenderMale ? maleNames : femaleNames);
        const NSUInteger nameIndex = (NSUInteger)getRandom(0, nameCount - 1);
        NSString *const name = nameArray[nameIndex];

        AccountA *const accountA = [[AccountA alloc]
            initWithName:name age:age gender:g balance:balance
        ];
        AccountB *const accountB = [[AccountB alloc]
            initWithName:name age:age gender:g balance:balance
        ];

        [allAcocuntsA addObject:accountA];
        [allAccountsB addObject:accountB];
    }
}


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        @autoreleasepool {
            NSUInteger ammount = 1000000; // 1 Million;
            if (argc > 1) {
                unsigned long long temp = 0;
                if (1 == sscanf(argv[1], "%llu", &temp)) {
                    // NSUIntegerMax may just be UINT32_MAX!
                    ammount = (NSUInteger)MIN(temp, NSUIntegerMax);
                }
            }
            createAccounts(ammount);
        }

        // Sort A and take time
        const CFAbsoluteTime startTime1 = CFAbsoluteTimeGetCurrent();
        @autoreleasepool {
            [allAcocuntsA sortedArrayUsingSelector:@selector(compare:)];
        }
        const CFAbsoluteTime runTime1 = CFAbsoluteTimeGetCurrent() - startTime1;

        // Sort B and take time
        const CFAbsoluteTime startTime2 = CFAbsoluteTimeGetCurrent();
        @autoreleasepool {
            [allAccountsB sortedArrayUsingSelector:@selector(compare:)];
        }
        const CFAbsoluteTime runTime2 = CFAbsoluteTimeGetCurrent() - startTime2;

        NSLog(@"runTime 1: %f", runTime1);
        NSLog(@"runTime 2: %f", runTime2);
    }
    return 0;
}



@implementation AccountA
    - (NSComparisonResult)compare:(nonnull AccountA *const)account {
        // Sort by gender first! Females prior to males.
        if (self.gender != account.gender) {
            if (self.gender == GenderFemale) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // Otherwise sort by name
        if (![self.name isEqualToString:account.name]) {
            return [self.name compare:account.name];
        }

        // Otherwise sort by age, young to old
        if (self.age != account.age) {
            if (self.age < account.age) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // Last ressort, sort by balance, low to high
        if (self.balance != account.balance) {
            if (self.balance < account.balance) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // If we get here, the are really equal!
        return NSOrderedSame;
    }

    - (nonnull instancetype)initWithName:(nonnull NSString *const)name
        age:(const unsigned)age gender:(const Gender)gender
        balance:(const int64_t)balance
    {
        self = [super init];
        assert(self); // We promissed to never return nil!

        _age = age;
        _gender = gender;
        _balance = balance;
        _name = [name copy];

        return self;
    }
@end


@implementation AccountB
    - (NSComparisonResult)compare:(nonnull AccountA *const)account {
        // Sort by gender first! Females prior to males.
        if (_gender != account.gender) {
            if (_gender == GenderFemale) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // Otherwise sort by name
        if (![_name isEqualToString:account.name]) {
            return [_name compare:account.name];
        }

        // Otherwise sort by age, young to old
        if (_age != account.age) {
            if (_age < account.age) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // Last ressort, sort by balance, low to high
        if (_balance != account.balance) {
            if (_balance < account.balance) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // If we get here, the are really equal!
        return NSOrderedSame;
    }

    - (nonnull instancetype)initWithName:(nonnull NSString *const)name
        age:(const unsigned)age gender:(const Gender)gender
        balance:(const int64_t)balance
    {
        self = [super init];
        assert(self); // We promissed to never return nil!

        _age = age;
        _gender = gender;
        _balance = balance;
        _name = [name copy];

        return self;
    }
@end
Run Code Online (Sandbox Code Playgroud)

  • 非常丰富的信息和脚踏实地的解释.Upvote代码示例 (3认同)
  • @malhal标记为`copy`的属性将不会在每次访问时复制其值的副本。复制属性的获取者就像强/保留属性的获取者。它的代码基本上是“返回[[self-&gt; value keep] autorelease];”。只有设置器会复制该值,它的外观大致类似于此[[self-&gt; value autorelease]; self-&gt; value = [newValue copy];`,而`strong` /`retain`设置器看起来像这样:`[self-&gt; value autorelease]; 自我-&gt;值= [newValue保留];` (2认同)

Dar*_*ust 9

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

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

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

  • +1关于"最低性能"的良好响应增加并希望明确地查看所有方法调用.使用带有属性的点语法肯定会掩盖自定义getter/setter中的许多工作(特别是如果getter每次调用它时返回一个东西的副本). (3认同)
  • KVC可以访问ivars.KVO无法检测到对ivars的更改(而是依赖于调用访问器). (2认同)

Jan*_*ano 9

语义

  • 什么@property可以表达ivars不能:nonatomiccopy.
  • 什么伊娃可以表达@property不能:
    • @protected:public上的子类,私有的外部.
    • @package:公共框架64位,私有外部.与@public32位相同.请参阅Apple的64位类和实例变量访问控制.
    • 预选赛.例如,强对象引用的数组:id __strong *_objs.

性能

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

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

属性在很多方面影响性能:

  1. 如前所述,发送消息以执行加载/存储比仅执行内联加载/存储要.

  2. 发送消息来进行加载/存储也需要在i-cache中保存更多的代码:即使getter/setter在加载/存储之外添加了额外的指令,也会有一个稳定的一半 - 在调用者中打开额外的指令来设置消息发送和处理结果.

  3. 发送消息会强制将该选择器的条目保留在方法高速缓存中,并且该内存通常会在d-cache中保留.这会增加启动时间,增加应用程序的静态内存使用量,并使上下文切换更加痛苦.由于方法缓存特定于对象的动态类,因此此问题会增加您在其上使用KVO的次数.

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

  5. 发送一个消息可以具有任意的副作用,因此

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

  6. 在ARC中,消息发送的结果将始终由被调用者或调用者保留,即使+0返回:即使方法不保留/自动释放其结果,调用者也不知道并且已经尝试采取措施防止结果被自动释放.这永远不会被消除,因为消息发送不是静态可分析的.

  7. 在ARC中,因为setter方法通常将其参数设置为+0,所以无法将该对象的保留(如上所述,ARC通常具有的)"转移"到ivar中,因此该值通常必须得到保留/释放两次.

当然,这并不意味着它们总是很糟糕 - 使用属性有很多充分的理由.请记住,像许多其他语言功能一样,它们并不是免费的.


约翰.


CRD*_*CRD 6

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

封装/信息隐藏从设计的角度来看,这是一个Good Thing(TM),狭窄的接口和最小的链接是使软件易于维护和理解的原因.Obj-C很难隐藏任何东西,但实现中声明的实例变量就像你得到的那样接近.

性能虽然"过早优化"是Bad Thing(TM),但编写性能不佳的代码只是因为你可以至少同样糟糕.很难反对方法调用比加载或存储更昂贵,并且在计算密集型代码中,成本很快就会增加.

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

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


Jus*_*tin 5

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


dre*_*lax 5

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