零、简单概述访问属性和实例变量
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];
}
尽管我们可以这么做,但这种方式依然是不推荐的。