全面理解Objective-C中的Property属性

OC中的属性

属性(Property)是Objective-C语言的其中一个特性,它把类对象中的实例变量及其读写方法统一封装起来,是对传统C++中要重复为每个变量定义读写方法的一种封装优化,OC将这些实例变量封装为属性变量,系统可自动生成getter和setter读写方法,同时仍然允许开发者利用读写语义属性参数(readwrite等)、@synthesize和@dynamic关键词去选择性自定义读写方法或方法名。

回归传统C++类实例变量的定义形式

原本的类实例变量定义形式如下,类(Class)是实例变量和方法的集合,变量的定义可以通过public、private和protected等语义关键词来修饰限定变量的定义域,实现对变量的封装,OC中仍然保留这种定义方法,其中关键词改为:@public、@private、@protected以及@package等,在头文件中的变量默认是@protected,在.m文件中的变量默认是@private。

@interface Test : NSObject {

@public    // 声明为共有变量     NSString *_name;
    
@private   // 限制为私有变量,.m实现文件中定义变量的默认类型     NSString *_major;
    
@protected // 限制为子类访问变量,头文件中定义变量的默认类型     NSString *_accupation;

@package   // 包内变量,只能本框架内使用     NSString *_company;

}

这种传统定义形式的缺点:

1.每个变量都要手动编写getter和setter方法,当变量很多时类中会出现大量的这些读写方法的代码,同时这些读写方法的形式是相同的,因此会产生代码冗余。OC中属性变量的封装就是将这些方法的定义封装起来,减少大量形式重复的方法定义;

2.这种类变量定义的方式属于“硬编码”,即对象内部的变量定义和布局已经写死在了编译期,编译后不可再更改,否则会出错。因为上面所谓的硬编码,指的是类中的变量会被编译器定义为离对象初始指针地址的偏移量(offset),编译之后变量是通过地址偏移来寻找的,如果想要在类中插入新的变量,则必须要重新编译计算每个变量的偏移量。否则顺序被打乱会读取错误的变量。例如下面的例子,在编译好的对象中变量的前面插入新的变量:


90.png

插入后_occupation的偏移量变了,因为现在是第三个指针,这时候按照编译器的结果访问就会出错。

Property属性变量封装定义

存取方法和变量名的自动合成:
使用OC的property属性,编译器会自动按照OC的严格的存取函数命名规范自动生成对应的存取函数,通过存取函数就可以根据变量名访问对应的变量,通过“点语法”访问变量其实就是调用了变量的存取方法(编译器会将点语法转换成存取方法的调用),也就是说通过属性定义的变量名成了存取函数名。此外,还会自动生成对应的实例变量名,由于自定义的变量名跟获取函数名一样,为了区分,实际的变量名在前面加下划线,另外虽然默认是加下划线,但可以在实现文件中使用关键词@synthesize自定义实际的变量名。下面例子中使用property属性定义变量,编译器会自动生成对应的存取方法和加下划线的实例变量名,由于是编译期生成的方法所以编译之前看不到:

@interface Test : NSObject

    /* property属性声明变量,编译期到时会自动生成获取方法:name和设置方法:setName */
    @property NSString *name;

@end
    Test *test = [[Test alloc] init];
    /** 通过点语法访问变量,等效于调用自动生成的存取方法访问变量:**/
    /* 1.调用test的setter方法设置变量 */
    test.name = @"sam";
    /* 等效于: */
    [test setName:@"sam"];
    
    /* 2.调用test的getter获取方法访问变量 */
    NSString *s = test.name;
    /* 等效于: */
    NSString *s = [test name];
    
    /* 3.name成了存取方法的函数名,所以要想直接访问实例变量要使用自动生成的变量名,也就是_name */
    _name = @"albert";
    s = _name;

@synthesize自定义变量名(特殊使用场景):
如果在Test的实现文件中使用@synthesize关键字自定义实例变量名,那么就不可以通过_name默认变量名来直接访问变量了,而是要使用自定义的名字,但实际为了规范和约定,@synthesize自定义实例变量名的用法是不建议使用的(@synthesize的原始用法是和@property成对出现来自动合成指定属性变量的存取方法的):

@implementation Test

/* 自定义实例变量名 */
@synthesize name = theName;

@end

现在要直接访问实例变量(不使用存取函数)就要通过自定义的变量名了

/* 通过自定义的变量名访问,此时_name已经不存在了 */
theName = @"albert";
s = theName;

【注意:】较旧版本@synthesize和@property是成对出现的,也就是说要手动使用@synthesize来合成相应的存取方法,否则不会自动合成(现在编译器默认会自动添加@synthesize自动合成存取方法)。

@synthesize name; // 旧版本手动指定要合成存取方法的变量

此时set方法名为:setName, 变量名和get方法名都为name,即name作为方法调用就是方法名,作为变量直接取就是变量名:

name = @"Sam"; // name为变量名 NSString *oldName = [self name]; // name为get方法名

@dynamic禁止存取方法自动合成:
@dynamic关键字是用来明确告诉编译器禁止自动合成属性变量的存取方法和加下划线的默认变量名。默认情况如果不用@dynamic关键字,编译器就会在编译器自动合成那些没有定义的存取方法,而那些程序员已经定义了的存取方法则不会再去合成,即程序员定义的存取方法优先级高。例如,如果此时程序员自定义了setter方法,那么编译器就会只自动合成getter方法,而不会再去合成已经定义了的setter方法。

@implementation Test

/* 禁止编译器自动生成存取方法 */
@dynamic name;

@end

此时,如果代码中依旧使用点方法,或者通过存取函数调用来访问name,编译之前并没有异常,但编译之后由于在编译期编译器并没有自动合成存取方法,运行起来时会在存取方法调用的位置处程序崩溃,因为调用了不存在的方法。

不同属性特质修饰词的限制

通过在@property后的括号内添加属性特质参数,也可以影响存取方法的生成:

@interface Test : NSObject

/* 括号内添加属性特质进行限制 */
@property(nonatomic, readwrite, copy) NSString *name;

@end

属性参数主要可以分为三类:

原子性: atomic,nonatomic
读写语义:readwrite,readonly,getter,setter
内存管理语义:assign,weak,unsafe_unretained,retain,strong,copy

其中最重要的是内存管理语义,要理解内存管理语义的作用和用法,首先要理解内存管理中的引用计数原理,也就是要理解OC的内存管理机制,属性参数的内存管理语义是OC中协助管理内存的很重要一部分。各种属性参数的含义和区别如下:

atomic、nonatomic: 原子性和非原子性。原子性是数据库原理里面的一个概念,ACID中的第一个。在多线程中同一个变量可能被多个线程访问甚至更改造成数据污染,因此为了安全,OC中默认是atomic,会对setter方法加锁,相应的也会付出维护原子性(数据加锁解锁等)的系统资源代价。应用中如果不是特殊情况(多线程间的通讯编程),一般还是用nonatomic来修饰变量的,不会对setter方法加锁,以提高多线程并发访问时的性能。
readonly、readwrite: readonly表示变量只读,也就是它修饰的变量只有get方法没有set方法;readwrite就是既有get方法也有set方法了,可读亦可写;
getter = < gettername >, setter = < settername >: 可以选择性的在括号里直接指定存取方法的方法名,例如:

    /* 更改默认的获取方法name为getName */
    @property(nonatomic, getter=getName, copy) NSString *name;
    
    /* 之后要调用获取方法应使用上面指定的 */
    s = [test getName];

assign: 直接简单赋值,不会增加对象的引用计数,用于修饰非OC类型,主要指基础数据类型(例如NSInteger)和C数据类型(int, float, double, char等),或修饰对指针的弱引用;

weak: 修饰弱引用,不增加引用对象的引用计数,主要可以用于避免循环引用,和strong/retain对应,功能上和assign一样简单,但不同的是用weak修饰的对象消失后会自动将指针置nil,防止出现‘悬挂指针’;

unsafe_unretained:这种修饰方式不常用,通过名字看出它是不安全的,为什么这么说呢?首先它和weak类似都是自己创建并持有的对象之后却不会继续被自己持有(引用计数没有+1,引用计数为0的时候会被自动释放,尽管unsafe_unretained和weak修饰的指针还指向那个对象)。不同的是虽然在ARC中由编译器来自动管理内存,但unsafe_unretained修饰的变量并不会被编译器进行内存管理,也就是说既不是强引用也不是弱引用,生成的对象立刻就被释放掉了,也就是出现了所谓的‘悬挂指针’,所以不安全。

retain: 常用于引用类型,是为了持有对象,声明强引用,将指针本来指向的旧的引用对象释放掉,然后将指针指向新的引用对象,同时将新对象的索引计数加1;

strong: 原理和retain类似,只不过在使用ARC自动引用计数时,用strong代替retain;

copy: 建立一个和新对象内容相同且索引计数为1的对象,指针指向这个对象,然后释放指针之前指向的旧对象。NSString变量一般都用copy修饰,因为字符串常用于直接复制,而不是去引用某个字符串;

【补充:】除了在属性变量前面加修饰词,开发中还会用到一些所有权修饰符,例如: __strong和 __weak。所有权修饰符和上面的修饰符有着对应关系,使用的目的和原理是一样的,可结合理解记忆,他们的对应关系如下:

__strong修饰符对应于上面的strong和retain还有copy,强引用来持有对象,它和C++中的智能指针std::shared_ptr类似,也是通过引用计数来持有实例对象;
__weak修饰符对应于上面的weak,同样它和C++中的智能指针std::weak_ptr类似,也是用于防止循环引用问题;
__unsafe __unretained修饰符对应于上面的assign和unsafe_unretained,创建但不持有对象,可能导致指针悬挂。

相关问题

问题: OC中的属性和实例变量有哪些区别?

@interface Test : NSObject {
    /* 实例变量 */
    @private
    NSString *major;
    @public
    int age;
}

/* 属性变量 */
@property (nonatomic, copy) NSString *name;

@end

首先OC中的属性主要是对传统实例变量的封装,类对象有一个属性列表用来存放类的所有属性,属性和实例变量的区别主要有以下几个方面:

实例变量的存放采用硬编码,编译后写死,根据离起始地址的偏移量来访问变量,不可再插入新变量,而属性可以在运行时动态添加删除;
实例变量可以通过@private、@public和@protected等修饰词来定义变量的作用域,限制变量的访问权限,而属性不可以。从设计角度,属性主要是用来和外部类进行访问交互的,实例变量主要用于类内部使用;
属性可以通过三类属性特质分别来帮助内存管理、多线程管理和读写控制,可以让编译器自动合成存取方法,而不用重复为每一个实例变量手写存取方法造成代码臃肿;

问题: 什么时候使用‘weak’关键字以及‘assign’和‘weak’的不同?
使用‘weak’关键字的几种情况:
ARC中为了避免出现循环引用,会让相互引用的对象中其中一个使用‘weak’弱引用;
自定义的IBOutlet控件属性一般也是用weak;

‘assign’和‘weak’的区别主要是‘weak’修饰的指针变量在所指的对象释放时会自动将变量指针置nil,防止指针悬挂,而‘assign’不可以,‘assign’主要用于修饰简单纯量类型,进行简单赋值; ‘weak’只能用于修饰OC对象,而assign还可以用于修饰非OC对象。 ***

问题: atomic原子性属性和nonatomic非原子性属性有什么不同?默认的是哪一个?
atomic原子属性修饰的变量setter方法会加锁,防止多线程环境下被多个线程同时访问造成数据污染,但会浪费资源;而nonatomic非原子性属性修饰的变量setter方法不会被加锁。为了安全,默认的是atomic原子属性的。 ***

问题: ARC下,不显式指定任何属性关键字时,默认的关键字都有哪些?
默认的属性关键字分两种情况:一种是基本数据类型,一种是OC普通对象。不管哪种情况默认都有atomic原子属性和readwrite可读写属性,区别是基本数据类型默认是有个assign属性关键字,而OC对象对应的默认有个strong属性关键字。

基本数据类型的默认关键字有:atomic,readwrite,assign
普通OC对象的默认关键字有:atomic,readwrite,strong

问题: 什么是“强引用”和“弱引用”?为什么他们很重要以及它们是怎样帮助控制内存管理和避免内存泄漏的?

默认的指向对象的指针变量都是strong强引用,当两个或多个对象互相强引用的时候就可能出现循环引用的情况,也就是引用成了一个环状。例如在ARC自动引用计数机制下循环引用中的所有对象将永远得不到释放销毁而导致内存泄漏,因为引用循环使得里面的对象的引用计数至少为1(当应用中的所有其他对象都释放了对环内的这些对象的拥有权的时)。因此对象之间互相的强引用是要尽可能的避免的,使用weak修饰的弱引用就是为了打破循环引用从而避免内存泄漏的。

问题: @synthesize和@dynamic各表示什么,有什么不同?
@synthesize 修饰的属性默认情况下由系统自动合成setter和getter方法,除非开发者自己定义了这些方法;@synthesize经常用来更改属性的变量名,系统自动合成时默认变量名为_var,即在原变量名前加下划线。

@dynamic 用来明确禁止编译器自动合成属性存取方法和默认变量名_var,由程序员自己手动编写存取方法。
@synthesize和@dynamic,前者明确让编译器自动合成存取方法和默认变量名,而后者明确禁止编译器自动合成存取方法和默认变量名,因此两者语义冲突,不可同时使用。

问题: 类变量的@protected,@private,@public,@package声明各有什么含义?
前三个跟一般面向对象里面的继承封装概念相同:
@protected: 表示变量对子类可见,而对于其他类来说变量是私有的,不可访问;
@private: 表示变量完全私有化,只对本类可见,其子类也不可访问;
@public: 公开变量,表示变量对所有类都是开放可见的,都可以访问;

最后一个就是Objective-C中特有的一个修饰词了,一般在开发静态类库的时候会用到,意思是这个关键词修饰的变量对于framework包内部来说是@protected类型的,而对于包外来说是@priviate类型的,这样可以实现包内变量的封装,包内可以使用而包外不可用,防止使用该包的人看到这些变量。

问题: 这段代码有什么问题:

@implementation Person
- (void)setAge:(int)newAge {
self.age = newAge;
}
@end

self.age是调用self中变量age的setter方法,setter方法调用自身,即setter方法里面又嵌套调用set方法导致死循环。通过点语法访问变量时,变量为左值时调用的是setter方法,为右值时调用的是getter方法,例如下面点语法访问的变量作为右值时调用的是getter方法:

int age = self.age; // 调用了age变量的getter方法

问题: 在一个对象的方法里面:self.name = @”object”;和name = @”object”;有什么不
同?
前者是调用setter方法赋值,后者是变量直接赋值。第一种情况的代码等效于:[self setName:@"object];。另外利用属性让编译器自动合成存取方法时变量名默认加下划线,因此第二种情况直接访问变量时通常为:_name = @"object"; ***

问题: __block 和 __weak 修饰符的区别?
__block: 可以用在ARC和MRC中,可以在MRC中避免循环引用问题但在ARC中不可以,可以修饰对象和基本数据类型,在block中可以被重新赋值。

__weak: 只能在ARC中使用,可以用于避免循环引用问题,只能修饰对象,不能修饰基本数据类型,不能在block中被重新赋值。

问题: 定义属性时,什么情况使用copy、assign、retain?
assign用于简单数据类型,如NSInteger,double,bool等等。retain和copy用于修饰OC对象,copy用于当a指向一个对象,b也想指向同样内容的对象但实际不是同一个对象的时候,如果用assign,a如果释放,再调用b会crash,如果用copy的方式,a和b各自有自己的内存,就可以解决这个问题。retain会使计数器加一,也可以解决assign的问题。 ***

问题: 分别写一个setter方法用于完成非ARC下的@property(nonatomic,retain)NSString *name和@property(nonatomic,copy)NSString *name

第一种情况retain是指针变量name对新赋值对象的强引用,相当于ARC下的strong,因此对name指针变量set新值时要先将新赋值对象的引用计数加1,然后将指针变量指向新赋值对象,类似于‘浅拷贝’。

首先在实现文件中合成属性变量:@synthesisze name;,然后两种情况下自定义setter方法如下,自定义了setter方法后编译器就不会再在编译期重复合成setter方法了:

/* retain */
- (void)setName:(NSString *)newName {
    if (name != newName) {
        [newName retain];  // 新对象引用计数加1         [name release];    // 将指针变量原来的对象释放掉         name = newName;    // 指针变量指向新对象     }
}

第二种情况copy指的是对指针变量name赋值新对象时,是将新对象完全copy一份,将copy好的对象复制给指针变量,即指针指向的是临时copy出来的对象,而不是新赋值的那个对象,因此新赋值对象不需要引用计数加1,因为指针变量并没有指向持有它,类似于‘深拷贝’。

/* copy */
- (void)setName:(NSString *)newName {
    if (name != newName) {
        id temp = [newName copy];  // 将新对象原样克隆一份        [name release];            // 将指针变量原来的对象释放掉         name = temp;               // 指针变量指向新对象的克隆体     }
}

问题:如何仅仅通过属性特质的参数来实现公有的getter函数和私有的setter函数?
首先如果不考虑自动合成的功能,如果要手动写一个共有的getter函数那么我们先要在.h头文件中声明这个getter函数以暴露给外部调用,并在.m文件中进行实现,然后手动在.m文件中写一个私有的setter函数的实现即可,当然私有函数可以在.m的continue区域进行私有函数声明,但是没有必要,只要不在.h文件中声明暴露即可(C++中是要在.m文件最前面声明的,否则要考虑函数调用顺序,在函数实现之前无法调用)。这里以一个简单的Person类为例说明具体写法,手动实现的方法如下:

/* .h头文件区域 */
@interface Person : NSObject {
    @private
    NSString *name;
}

/* 声明公有的getter函数 */
- (NSString *)name;
@end
#import "Person.h" 
/* continue 私有声明区域 */
@interface Person()

/* 在.m文件的continue区域声明私有setter方法,通常私有函数不需要声明,可以省略 */
- (void)setName:(NSString *)newName;

@end

/* implementation实现区域 */
@implementation Person

/** * 公有的getter函数实现 */
- (NSString *)name {
    /* 注意这里直接返回实例变量,如果使用self.name相当于getter方法调用自身会造成死循环 */
    return name;
}

/** * 私有的setter函数实现 */
- (void) setName:(NSString *)newName {
    /* 注意这里直接给实例变量赋值,如果使用self.name相当于setter方法调用自身会造成死循环 */
    name = newName;
}

@end

这样在类外部是可以调用getter方法的,但setter方法只能在本类内部调用,外部无法找到setter方法。

现在题目要求我们使用属性的读写语义也就是readwrite和readonly来让编译器自动合成上面的效果,如何实现呢?

实现方法是要在.h头文件和.m实现文件中定义属性变量两次,第一次在.h头文件中使用readonly读写语义让编译器自动合成公有的getter函数,第二次在.m文件中使用readwrite读写语义再让编译器自动合成私有的setter方法。写法如下:

/* .h头文件区域 */
@interface Person : NSObject

/* 使用readonly,让编译器只合成公有getter方法 */
@property (nonatomic, readonly, copy) NSString *name;

@end
/* continue 私有声明区域 */
@interface Person()

/* 让编译器再合成私有setter方法,其中readwrite可以省略,因为默认就是readwrite */
@property (nonatomic, readwrite, copy) NSString *name;
@end

/* implementation实现区域 */
@implementation Person

/** * 测试 */
- (void)test {
    /* 下面两条语句等效,都是调用setter方法,但注意setter方法是私有的,只能在此处调用,在外部无法调用 */
    self.name = @"name";
    [self setName:@"name"];
}

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

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,082评论 1 32
  • 1. load 与 initialize load:是当类或分类被添加到 Objective-C runtime ...
    杰克道长阅读 1,354评论 0 3
  • 苹果官方文档翻译 《Objective-C语言编程》(Programming with Objective-C) ...
    fever105阅读 25,578评论 19 130
  • 电子银行我行手机银行现可提升转账额度为30万(单笔/日均),需要在usb key网银里设置定向转账收款人。手机具备...
    于成友南阅读 213评论 0 0
  • 昨日的天气一股阴沉沉的空气始终萦绕在街头巷尾,不时有雨滴散落在地上犹如蜻蜓点水似的,始终湿不透整个地面。 心里一直...
    陆学峰阅读 771评论 7 37