【iOS沉思录】深思Objective-C中的property属性

OC中的属性

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

回归传统C++类属性变量的定义形式

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

@interface Test : NSObject {

    @public    // 声明为共有变量
    NSString *_name;

    @private   // 限制为私有变量
    NSString *_major;

    @protected // 限制为子类访问变量,是这里头文件定义变量的默认类型,如果变量定义在.m实现文件中默认类型是@private
    NSString *_accupation;

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

}

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

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

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

【iOS沉思录】深思Objective-C中的property属性_第1张图片

插入后_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方法。

@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:
    直接简单赋值,主要用于修饰基础数据类型(例如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修饰符对应于上面的strongretain还有copy,强引用来持有对象,它和C++中的智能指针std::shared_ptr类似,也是通过引用计数来持有实例对象;
  • __weak修饰符对应于上面的weak,同样它和C++中的智能指针std::weak_ptr类似,也是用于防止循环引用问题;
  • __unsafe __unretained修饰符对应于上面的assignunsafe_unretained,创建但不持有对象,可能导致指针悬挂。

相关问题

问题: 区分‘assign’和‘retain’关键字的不同以及‘assign’和‘weak’的不同?
‘assign’和‘weak’的区别主要是‘weak’修饰的变量在对象释放时会自动将变量指针置nil,防止指针悬挂。


问题: 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主要是用来更改属性变量名的。

@dynamic 用来明确禁止属性存取方法的自动合成,由程序员自己手动编写存取方法。


问题:
类变量的@protected,@private,@public,@package声明各有什么含义?

前三个跟一般面向对象里面的继承封装概念相同:

@protected: 表示变量对子类可见,而对于其他类来说变量是私有的,不可访问;

@private: 表示变量完全私有化,只对本类可见,其子类也不可访问;

@public: 公开变量,表示变量对所有类都是开放可见的,都可以访问;

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


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

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

set方法里面又嵌套调用set方法导致死循环。


问题:
在一个对象的方法里面:self.name = @”object”;和 name = @”object”;有什么不同?

前者是调用set方法赋值,后者是变量直接赋值。第一种情况的代码等效于:[self setName:@"object];


问题:
__block 和 __weak 修饰符的区别?

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

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


问题:
定义属性时,什么情况使用copy、assign、retain?

assign用于简单数据类型,如NSInteger,double,bool等等。retain和copy用于对象,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,然后将指针变量指向新赋值对象,类似于‘浅拷贝’。

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

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

// copy
- (void)setName:(NSString *)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 {
    NSString *name;
}

- (NSString *)name; // 声明公有的getter函数

@end
#import "Person.h"

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

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

@end

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

/**
 * 公有的getter函数实现
 */
- (NSString *)name {
    return name;
}

/**
 * 私有的setter函数实现
 */
- (void) setName:(NSString *)newName {
    name = newName;
}

@end

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

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

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

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

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

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

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

@end

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

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

@end

你可能感兴趣的:(iOS沉思录)