iOS巩基之 不再纠结实例变量&属性

零、简单概述访问属性和实例变量

1.访问属性时,其实我们是在使用

objc语言:self.friend = person;
其访问方式是:
objc_msgSend((id)self, sel_registerName("setFriend:",...)

访问属性本质上通过消息机制在调用setFriend:方法。关于runtime运行时的东西这里就不多概述了。
2.访问实例变量的本质代码:

objc语言:_friend = person; (都为Person类,实例名为person和freind)
其访问方式是:
(*(Person **)((char *)self + OBJC_IVAR_$_Person$_friend)) = person;
...
 unsigned long int OBJC_IVAR_$_Person$_friend = __OFFSETOFIVAR__(struct Person, _friend);
...

从这段代码其实很容易看出访问实例变量就是在运行期查找成员的偏移量,这样就计算出了成员相应位置的内存地址,从而直接访问其内存。

一、属性 VS 实例变量的优缺点对比

1.在访问效率上的比较 ------实例变量完胜

概念示意图

2.在内存管理语义上的比较 ------属性完胜

概念示意图

3.KVO触发机制上的比较 ------- 属性略胜

直接访问实例变量,无法触发KVO机制,这一点需要根据具体业务来和对象具体的行为来决定。


概念示意图

4.在调试错误上的比较 ------ 属性胜
通过属性来访问可以来帮助排查与之相关的错误,因为我们有机会在set/get方法中增加断点,而实例变量无法做到。

二、根据具体场景来决定声明实例变量还是属性

1.争议最大的我想还是对象 内部 是声明实例变量成员还是属性成员呢?

Person.m
@interface Person : NSObject
@property (nonatomic,copy)NSString *firstName;
@end

又或者是:
Person.m
@interface Person : NSObject{
     NSString *firstName; 
}
@end

(1)在MRC下的折中方案:使用实例变量,但是为实例变量增加set方法,而不去实现get方法,就能实现在读取效率上的提高,和在设置方法中贯彻"内存管理语义",同时也能有助于排查错误。
在ARC下,我们无法再遵循上述的折中方案,我们无法在set方法中再手动简易的实现内存语义,但是因为ARC下默认都是strong。这是十分遗憾的,所以我们现在对内可以对大多数strong语义的类对象声明实例变量,但对需要特殊处理的内存语义的实例变量我们需要使用属性。

(2)如果对象内部需要实现KVO机制,我建议还是被观察对象建议还是选择属性。因为实例变量必须要执行KVC才能实现效果,对开发者来说有时候是个负担。

-(id)init{
    self = [super init];
    if (self) {
[self addObserver:self forKeyPath:@"firstName" options:NSKeyValueObservingOptionOld |NSKeyValueObservingOptionNew context:nil];
    }
    return self;
}
...
-(void)actionFor..{
       _firstName = @"Chen";   //错误例子:直接访问实例变量是不会触发KVO机制的。
       [self setValue:@"firstName" forKeyPath:@"Chen"];   //我们必须有意识的使用KVC才能实现效果
}

2.对外部访问,毫无疑问是使用属性来进行操作
(1).对于.h文件,属性是没有关键字来达到私有的,这也反应苹果想让属性来被外部访问。
(2)属性的get/set方法,也正是体现了面向对象的类的封装性。直接访问赤裸裸的实例变量肯定是不好的。我们可以通过内存语义合理管理外部访问内存的方式。

三.最重要--在初始化方法或者析构方法中应该大部分情况应该使用实例变量

我们应该使用直接访问实例变量访问实例变量而不是通过访问属性的操作实例变量:
这点问过不少人,大多数甚至某些培训机构的老师都说是无所谓的,这倒让我感到十分诧异~
No.1属性的Setter 方法可能会产生额外的副作用,它可能会触发 KVC 通知(尽管我们说KVO触发是优势,但在这里是初始化)或者你的自定义set方法带来的副作用,毕竟我们在初始化方法中主要向做的就是赋值。
No.2如果在初始化方法中使用属性,可能在当前类的初始化中没有问题,但是它的子类万一覆写了set方法,将会导致意向不到的后果。

@interface Person : NSObject
@property (nonatomic,copy)NSString *firstName;
@property (nonatomic,copy)NSString *lastName;
@end
-(id)init{
    self = [super init];
    if (self) {
        self.firstName = @"Chen";
        self.lastName = @"Ming";   
    }
    return self;
}
假如子类是这样的:
@interface SuperMan : Person
@end
-(id)init{
    self = [super init];
    if (self) {
    }
    return self;
}
-(void)setFirstName:(NSString *)firstName{
        ....//再做一些其他事(这样的话,就完全就会影响父类模块的初始化,
                           所以我们应当使用实例变量。)
}

No.3由于初始化方法中我们主要需要注重赋值,而不是语义上的管理。

这里再举一个例子
@interface Person : NSObject
@property (nonatomic,readonly)NSString *firstName;
@end
...
-(id)initWithName:(NSString*)name{
    self = [super init];
    if (self) {
         _firstName = name; //可以赋值
         self.firstName = name;//无法赋值
    }
    return self;
}
!!!针对语义,copy也不行,但是strong,weak,assign,retained这些实例变量
仍然能够享受语义。在编译过程中,我们可以发现它背后执行了
objc_storeStrong、objc_storeWeak函数来为它保持内存管理语义。


如果再查看背后的class结构,我们可以在编译后发现关于strong和weak语义的记录存放在Ivar Layout中:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif
    const uint8_t * ivarLayout; // <- 记录了哪些是 strong 的 ivar
    const char * name;
    const method_list_t * baseMethods;
    const protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;
    const uint8_t * weakIvarLayout; // <- 记录了哪些是 weak 的 ivar
    const property_list_t *baseProperties;
};

static struct _class_ro_t _OBJC_CLASS_RO_$_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    0, __OFFSETOFIVAR__(struct Person, _firstName2), sizeof(struct Person_IMPL), 
    (unsigned int)0, 
    0, 
    "Person",
    (const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_Person,
    0, 
    (const struct _ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_Person,
    0, 
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person,
};   //不难看出这里数值都是0、0,因为该数值需要运行时动态计算。

所以当程序运行时,它会动态地获取weak和strong的实例成员个数,然后来对相应位置的成员执行objc_storeStrong、objc_storeWeak函数。这也就是为什么实例变量能享受strong和weak语义的原因了。
其他语义的描述希望能再其他专门的专题详细概述,这里不多做解释了。

然后下面我们再来从另外一个角度看问题:

这里我再延伸出一个问题,我们可以这样考别人:
@property (nonatomic,copy)NSMutableArray *firstName;
//其实应该写成strong,比较经典的错误
-(id)init{
   ...
        NSMutableArray *array = [NSMutableArray arrayWithObject:@"1"];
        _firstName = array;
        NSLog(@"%p %p",array,_firstName);
   ...
}
你们猜猜[person.firstName addObject:@"123"];这样操作这个属性会报错吗?
答案是不会的,这个例子尽管不是那么的恰当,说明假如我们失误讲内存语义错误地写成了copy,
但如果使用实例变量,我们仍然可以绕过这个失误。

No.4应该总是从一个初始化方法中直接访问实例变量,因为当属性被设置好时,一个对象的其余部分可能还未初始化完全。

四.除了初始化和析构,在对象内部对于已经声明的属性,我们应该通过属性访问实例变量,还是直接访问实例变量?

这里只谈我个人的方法,如果已经声明为属性了,那么除了析构器和初始化方法,其他都可以使用属性来实现访问了。除了:

我见过有些诸如这样的面试题,我们在set/get接口不应该使用属性的访问方式,应该使用
实例变量的访问方式。这是一个特殊的情况,在set/get接口我们应该使用实例变量。
-(void)setFirstName:(NSMutableArray *)firstName{
             ...
            self.firstName = firstName; //错误
             ...
}

五.单独声明的实例变量 VS. 属性内部的实例变量

这个主题相对很小,它们之间的主要区别的是访问权限上的区别。
我们可以再看一个例子,如下所示:

@interface Person : NSObject
{
    NSString*_firstName2;
}
@property (nonatomic,strong)NSString *firstName;
@interface SuperMan : Person
@end
@implementation SuperMan
-(id)init{
    self = [super init];
    if (self) {
        _firstName2 = @"Chen";     //而单独声明的实例变量访问权限是protected
        _firstName = @"Chen";      //报错,属性内部的实例变量的访问权限是private
    }
    return self;
}

我们只需要这样来解决:

@interface Person : NSObject
{
          @protected NSString *_firstName;
}
@property (nonatomic,copy)NSMutableArray *firstName;

六、顺带提一个小误区,实例变量并非不可被外界访问。

1.从某一时代推出属性之后,大家都习惯用属性来进行外界访问,但这并不代表实例变量不行。
属性自带set/get方法,所以可以被外界访问,而实例变量没有自动的set/get,所以不可以被外界访问。但是我们只需要像下面这样:

Person.h
@interface Person : NSObject
{
   @public 
   __strong Person* friend;  //默认是@protected
}
@property (strong,nonatomic)Person* son;
@end
- (void)viewDidLoad {
    [super viewDidLoad];   
    Person* p1 = [Person new];
    p1->friend = [Person new]; 
    p1.son = [Person new];  
}
结果

尽管我们可以这么做,但这种方式依然是不推荐的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • 重点掌握 3 类对象和方法 对象就是一个物体 类的独特存在就是一个实例,对实例进行操作叫做方法。方法可以应用于类或...
    Coder大雄阅读 1,256评论 0 2
  • 哟哟哟
    钟成昊阅读 213评论 0 0
  • 昨天夜里,有人说我聒噪。我连问了几个群,“我聒噪吗?” 有闺蜜群,有朋友群,有亲戚群……没有人认为我聒噪。 夜里,...
    涉弋阅读 493评论 1 0
  • 梦湖之畔,有一棵 灼灼其华的桃树, 溪经过它的茎脉 溢出大地—— 清风狂饮后只剩下一朵 干瘪的云,被盗的花瓣 招惹...
    赖床的萤火虫阅读 213评论 0 0