成员变量与属性
-
成员变量与属性 的声明
在 Objective-C 中,成员变量的声明如下(这里的 name 和 age 就称之为 成员变量):
#import <Foundation/Foundation.h> @interface animal : NSObject { @public NSString* name; @public int age; } @end
在 Objective-C 中,属性的声明如下(这里的 name 和 age 就称之为 属性):
#import <Foundation/Foundation.h> @interface animal : NSObject @property (nonatomic, strong) NSString* name; @property (nonatomic, assign) int age; @end
-
Objective-C 属性进化史
① GCC 编译器时代:声明属性时必须手动声明与之对应的成员变量
② LLVM 编译器第一版:不需要手动声明成员变量,关键字@synthesize
默认会去访问与属性同名的成员变量,如果找不到,则会自动生成一个(与属性同名的成员变量)
因此,从此版本开始,就不需要再手动声明与属性对应的成员变量了
(在这里,@synthesize name;
等价于@synthesize name = name;
)
③ LLVM 编译器第二版:考虑到编译器自动生成的成员变量名,与 getter 的方法名、setter 的参数名一样,容易让人误会,从而引起警告,所以从此版本开始,默认为(属性生成的同名成员变量)开头加下划线
④ LLVM 编译器第三版:从iOS4.5
开始,关键字@synthesize
也可以省略了。最终变成下面这样
如果把上面的隐藏代码全部显示出来的话,其本质如下:
-
同时重写属性的 getter 与 setter 的问题
如果同时重写属性对应的
getter
与setter
,那么编译器就会报错
这是因为,当同时重写属性对应的getter
与setter
时,编译器自动添加的代码@synthesize name = _name;
会失效
此时编译器不会再为属性自动生成相应的成员变量_name
了,getter
与setter
所操作的成员变量也就不存在了
所以此时要手动加上@synthesize name = _name;
,显式指定:属性 name 的getter
与setter
要操作的成员变量是_name
-
实例变量、成员变量、属性 三者的关系
#import <UIKit/UIKit.h> @interface ViewController : UIViewController { int count; // 成员变量(基本数据类型) NSInteger money; // 成员变量(基本数据类型) NSString* name; // 实例变量(类类型, 也是成员变量) id faith; // 实例变量(类类型, 也是成员变量) } @property (nonatomic, assign) int order; // 属性 @property (nonatomic, strong) NSString* tip; // 属性 @end
如上所示
① 在大括号{}
中声明的都是成员变量,成员变量的数据类型有 2 种:- 基本数据类型(比如,
count
是int
类型) - 类类型,也就是我们常说的实例变量(比如,
name
是NSString
类型)
即:成员变量 = 基本数据类型变量 + 实例变量(类类型变量)
② 通过
@property
声明的都是属性(编译器默认会生成属性所对应的成员变量)③ 属性是在成员变量的基础上扩充了存取方法
- 基本数据类型(比如,
点语法
-
getter 与 setter
- getter 与 setter 称之为 属性访问器
- 因为具体的成员变量和属性都是相对于对象而言的,所以 getter 与 setter 都是对象方法
- getter 方法用于访问成员变量,调用者通过 getter 方法获取对象内部的成员变量的值。在使用 getter 方法获取成员变量时,可以对成员变量进行加工(比如,懒加载)
- setter 方法用于设置成员变量,调用者通过 setter 方法设置对象内部的成员变量的值。在使用 setter 方法设置成员变量时,可以过滤掉不合理的值(比如,空值、负数 等),保证数据的安全性
-
Objective-C 点语法的本质
Objective-C 中的点语法,其本质是调用属性 getter 与 setter 方法的一种快捷方式
在编译时,编译器会自动将点语法转换为对应的 getter 与 setter 方法
如果点语法出现在等号(=)的左边,那么编译器会自动将其转换为 setter 方法
如果点语法出现在等号(=)的右边,或者没有等号(=),那么编译器会自动将其转换为 getter 方法// 以下两句代码是等价的 person.age = 10; [person setAge:10]; // 以下两句代码是等价的 int age = person.age; int age = [person age];
-
Objective-C 点语法的注意点
① 因为点语法的本质是调用属性的 getter 与 setter 方法,而不是直接访问成员变量
所以如果成员变量没有相应的 getter 与 setter 方法,则不能使用点语法如果在 .h 中声明的是属性,则在 .m 中,即可以通过点语法间接地访问成员变量,也可以通过
self->
直接地访问成员变量
如果在 .h 中声明的是成员变量,则在 .m 中,因为编译器没有自动生成对应的 getter 与 setter 方法,所以只能通过self->
直接地访问成员变量
② 因为点语法的本质是调用属性的 getter 与 setter 方法
所以不要在属性的 getter 与 setter 方法中使用本属性的点语法,否则会引起无限递归
在属性的 getter 与 setter 方法中应该使用属性对应的成员变量
③ 因为在 init 和 dealloc 时,对象存在与否不能确定,所以给对象发送消息可能会不成功
因此尽量不要在对象的- (instancetype)init
和- (void)dealloc
方法中,使用属性访问器(getter / setter)
成员变量的修饰符
-
@public、@protected、@private、@package
@public: 在有对象的前提下,任何地方都可以直接访问该成员变量 @protected:只能在当前类和子类的对象方法中访问该成员变量。成员变量默认的访问权限 @private: 只能在当前类的对象方法中才能访问该成员变量。如果其他地方要访问该成员变量(即使是子类),都需要调用当前对象提供的属性访问器(getter、setter) @package: 框架级的保护权限。对于 Framework 内部的类,该成员变量是 @public;对于 Framework 外部的类,该成员变量是 @private
-
类文件(.h 和 .m)中能声明成员变量的三个位置
① 类声明区域(Class Declaration)
② 类扩展区域(Class Extension)
③ 类实现区域(Class Implementation)
结论:
① 定义在类接口区域(Class Declaration)的成员变量,其访问权限默认为 @protected
② 定义在类扩展区域(Class Extension)的成员变量,无论是否显式指定访问权限,其访问权限均为 @private
③ 定义在类实现区域(Class Implementation)的成员变量,无论是否显式指定访问权限,其访问权限均为 @private
④ 在 Person 类的对象方法中,能访问 Person 对象的所有(访问权限的)成员变量
⑤ 在 Person 类的子类 Man 类的对象方法中,只能访问 Person 类接口区域(Class Declaration)中,标记为 @public 和 @protected 的成员变量
⑥ 在其他地方(非 Person 类和 Person 子类),通过 Person 对象(或者 Person 的子类对象),只能访问 Person 类接口区域(Class Declaration)中,标记为 @public 的成员变量注意:
只要子类继承了父类,那么子类就会拥有父类所有的:成员变量、属性、方法。即使父类的这些 成员变量、属性、方法 是私有的,子类也会统统继承过来,只是子类不能直接访问(子类可以通过 RunTime 间接地访问到从父类继承的私有:成员变量、属性、方法)
属性的修饰符
-
相关概念
野指针:对象在被释放之后,指针仍然指向原对象的地址。这时候如果继续通过指针访问原对象的话,会造成程序异常
MRC(Manual Reference Counting):手动引用计数
ARC(Automatic Reference Counting):自动引用计数
-
原子性
atomic
:原子性(默认)
编译器会自动对属性的 getter、setter 方法进行加锁(互斥锁),可以保证对属性的赋值和取值是线程安全的,但不包括操作和访问。例如:使用 atomic 修饰一个数组,那么对数组进行赋值和取值是可以保证线程安全的。但是如果对数组进行操作(比如给数组添加对象或者移除对象),是不在 atomic 的负责范围之内的,所以给被 atomic 修饰的数组添加对象或者移除对象,是没办法保证线程安全的nonatomic
:非原子性
因为 atomic 的锁机制非常耗时,所以一般属性都用 nonatomic 进行修饰,执行效率高,但是多线程并发访问不安全 -
读写权限
readwrite
:可读可写(默认)
编译器为属性同时生成 getter、setter 方法的声明和实现readonly
:只读
编译器只为属性生成 getter 方法的声明和实现getter
:可以指定为属性生成的 getter 方法名,如 getter = getName// 通常 bool 类型的属性的 getter 方法会以 is 开头 @property (nonatomic, assign, getter=isArrived) bool arrived;
setter
:可以指定为属性生成的 setter 方法名,如 setter = setName// 注意:setter 方法必须要有 : @property (nonatomic, strong, setter=named:) NSString* name;
-
内存管理(引用计数)
assign
:
① 既可以修饰基本数据类型,也可以修饰对象类型。MRC 模式和 ARC 模式都可以使用。属性 setter 方法的实现是直接赋值
② 一般用于修饰基本数据类型,如:NSInteger、CGFloat、int、float 等(基本数据类型的内存地址一般是分配在栈上,而栈中的内存是由系统自动管理的,不会造成野指针)
③ 修饰对象类型时,不增加对象的引用计数
④ 修饰的对象在被释放之后,不会自动将指针置为 nil,会产生野指针(所以 assign 一般不用于修饰对象类型)@property (nonatomic, assign) int age; -(void)setAge:(int)age { _age = age; }
weak
:
① 只能修饰对象类型。ARC 模式下才能使用
② 弱引用修饰符,不增加对象的引用计数,主要用于避免循环引用(Retain Cycles)
③ 修饰的对象在被释放之后,会自动将指针置为 nil,不会产生野指针
④ 常用于对 delegate 和 block 的修饰
⑤ Interface Builder 中 IBOutlet 修饰的控件一般也是用 weak__weak typeof(self) weakSelf = self;
unsafe_unretained
:
① 只能修饰对象类型。MRC 模式下经常使用,ARC 模式下基本不用
② 弱引用修饰符,不增加对象的引用计数,主要用于避免循环引用
③ 修饰的对象在被释放之后,不会自动将指针置为 nil,会产生野指针retain
:
① 只能修饰对象类型。MRC 模式下才能使用
② 强引用修饰符,将指针原来指向的旧对象释放掉,将新对象的引用计数加 1,然后指针指向新的对象@property (nonatomic, retain) NSString* name; -(void)setName:(NSString *)name { if (_name != name) { [_name release]; _name = [name retain]; } }
strong
:
① 只能修饰对象类型。ARC 模式下才能使用
② 强引用修饰符,将指针原来指向的旧对象释放掉,将新对象的引用计数加 1,然后指针指向新的对象@property (nonatomic, strong) NSString* name; -(void)setName:(NSString *)name { if (_name != name) { [_name release]; _name = [name retain]; } }
copy
:
① 只能修饰对象类型。MRC 模式和 ARC 模式都可以使用
② 属性 setter 方法的实现是先 release 旧值,再 copy 新值。用于对不可变集合对象(NSString、NSArray、NSDictionary)以及对 block 的修饰中@property (nonatomic, copy) NSString* name; -(void)setName:(NSString *)name { if (_name != name) { [_name release]; _name = [name copy]; } }
-
注意
① MRC 模式下,所有属性(内存管理)的默认修饰符都是 assign
② ARC 模式下,基本数据类型(内存管理)的默认修饰符是 assign,对象类型(内存管理)的默认修饰符是 strong
③ 内存管理相关的修饰符,只能修饰 OC 对象,不能修饰非 OC 对象。比如:CoreFoundation 是 C 语言框架,它的对象没有引用计数,所以不能用retain、strong 等进行修饰
所有权修饰符
-
__strong
强引用持有对象,编译器将为
strong
、retain
、copy
修饰的属性生成带__strong
所有权修饰符的实例变量 -
__weak
弱引用持有对象,编译器将为
weak
修饰的属性生成带__weak
所有权修饰符的实例变量 -
__unsafe_unretained
弱引用持有对象,编译器将为
unsafe_unretained
、assign
修饰的属性生成带__unsafe_unretained
所有权修饰符的实例变量 -
__autoreleasing
在 ARC 中可以使用 __autoreleasing 修饰符修饰对象,将对象注册到 autoreleasepool 中
在 MRC 中可以给对象发送 autorelease 消息,将对象注册到 autoreleasepool 中 -
__block
block 内部可以访问局部变量,但是无法修改局部变量的值
如果一个局部变需要在 block 内部被修改,则该局部变量需要使用 __block 修饰
注意:
__block 不能修饰全局变量、静态变量
Block 分为:全局 Block、堆 Block、栈 Block
可空性修饰符
-
简介
可空性(Nullability Annotations)是苹果在 Xcode 6.3 中新引入的一个 Objective-C 特性。可空性修饰符可以用于属性、方法返回值和方法参数中,来指定对象是否可以为空 ,这样编写代码的时候就会有智能提示
在 Swift 中可以使用
!
和?
来表示一个对象是可选的(optional)还是必选的(non-optional),如:button?
和button!
。而在 Objective-C 中则没有这一区分,button
即可表示这个对象是可选的(optional),也可表示这个对象是必选的(non-optional)。这就造成了一个问题:在 Swift 与 Objective-C 混编时,Swift 编译器并不知道一个 Objective-C 对象到底是可选的,还是必选的。为了解决这个问题,编译器会隐式地将 Objective-C 的对象当成是必选的(non-optional)。引入可空性修饰符,一方面是为了让开发者平滑地从 Objective-C 过渡到 Swift,另一方面也促使开发者在编写 Objective-C 代码时更加规范,减少同事之间的沟通成本可空性修饰符
__nullable
和__nonnull
是苹果在 Xcode 6.3 中发行的
由于可能与第三方库存在冲突,苹果在 Xcode 7 中将它们更改为_Nullable
和_Nonnull
。但是,为了与 Xcode 6.3 兼容,苹果预定义了宏__nullable
和__nonnull
来扩展为新的名称
同时苹果还支持没有下划线的写法nullable
和nonnull
这些可空性修饰符的区别在于在使用时在代码中的放置位置不同注意:
① 可空性修饰符仅仅是提供警告,并不会报编译错误
② 可空性修饰符,只能用于修饰对象类型,不能用于修饰基本数据类型可空性修饰符可以分为以下 4 类:
nullable
、_Nullable
、__nullable
:对象可以为空,区别在于放置位置不同nonnull
、_Nonnull
、__nonnull
:对象不能为空,区别在于放置位置不同null_unspecified
、_Null_unspecified
、__null_unspecified
:对象未指定是否可为空,区别在于放置位置不同null_resettable
:getter 方法不能返回为空,setter 方法可以为空。.必须重写 getter 或 setter 方法做非空处理。否则会报警告Synthesized setter 'setName:' for null_resettable property 'name' does not handle nil
-
使用示例
① 声明属性
@property (nonatomic, copy, nullable) NSString * param; @property (nonatomic, copy) NSString * _Nullable param; @property (nonatomic, copy) NSString * __nullable param;
② 修饰方法返回值
-(nullable NSString *)method; -(NSString * _Nullable)method; -(NSString * __nullable)method;
③ 修饰方法参数
-(void)methodWithParam:(nullable NSString *) param; -(void)methodWithParam:(NSString * _Nullable) param; -(void)methodWithParam:(NSString * __nullable) param;
④ 例外情况
对于双指针类型的对象 、Block 的返回值、Block 的参数等,不能用nonnull/nullable
修饰,只能用带下划线的__nonnull/__nullable
或者_Nonnull/_Nullable
修饰-(void)methodWithError:(NSError * _Nullable * _Nullable)error; -(void)methodWithError:(NSError * __nullable * __nullable)error; -(void)methodWithBlock:(nullable id __nonnull (^)(id __nullable params))block;
-
不可空区域(Audited Regions)
如果每个属性、方法返回值和方法参数,都去指定其可空性,将是一件非常繁琐的事。苹果为了减轻开发者的工作量,专门提供了两个宏:
NS_ASSUME_NONNULL_BEGIN
和NS_ASSUME_NONNULL_END
。在这两个宏之间的代码,所有简单指针类型都被假定为不可空(nonnull
),因此开发者只需要去指定那些可空(nullable
)的指针类型即可
-
使用规范
① 对于属性、方法返回值、方法参数的修饰,使用:
nonnull/nullable
② 对于 C 函数参数、Block 参数、Block 返回值的修饰,使用:_Nonnull/_Nullable
③ 建议弃用__nonnull/__nullable
④typedef
类型的可空性通常依赖于上下文,即使在不可空区域(Audited Regions)中也不能假定它为nonnull
⑤ 对于复杂的指针类型(如id *
)必须明确指定它的可空性。例如:指定一个指向可空(nullable)对象的不可空(nonnull)指针,可以使用_Nullable id * _Nonnull
⑥ 特殊类型NSError **
经常用于通过方法参数返回错误,因此始终假定它是指向可空(nullable)的 NSError 对象的可空(nullable )的指针
注意
-
在 Objective-C 的类中,如果只声明了成员变量,编译器不会自动生成与其对应的属性;
反之,如果声明了属性,则编译器会自动生成与其对应的成员变量Person 类如下所示:
通过 RunTime 机制获取 Person 对象的所有成员变量和属性:#import <objc/runtime.h> #import "Person.h" -(void)test { // 获取 Person 类的所有成员变量 unsigned variableCount = 0; Ivar* variables = class_copyIvarList([Person class], &variableCount); for (unsigned i = 0; i < variableCount; i++) { Ivar variable = variables[i]; const char * variableName = ivar_getName(variable); NSString* vName = [NSString stringWithUTF8String:variableName]; NSLog(@"variable[%d] = %@", i, vName); } // 获取 Person 类的所有属性 unsigned propertyCount = 0; objc_property_t* properties = class_copyPropertyList([Person class], &propertyCount); for (unsigned i = 0; i < propertyCount; i++) { objc_property_t property = properties[i]; const char * propertyName = property_getName(property); NSString* pName = [NSString stringWithUTF8String:propertyName]; NSLog(@"property[%d] = %@", i, pName); } } // 输出结果如下: 2021-01-31 21:37:28.678452+0800 PropertyDemo[19664:2182332] variable[0] = addr 2021-01-31 21:37:28.678872+0800 PropertyDemo[19664:2182332] variable[1] = height 2021-01-31 21:37:28.679574+0800 PropertyDemo[19664:2182332] variable[2] = _age 2021-01-31 21:37:28.679958+0800 PropertyDemo[19664:2182332] variable[3] = _name 2021-01-31 21:37:28.680478+0800 PropertyDemo[19664:2182332] property[0] = name 2021-01-31 21:37:28.682848+0800 PropertyDemo[19664:2182332] property[1] = age
-
在 Objective-C 中,成员变量和属性不能在定义的同时进行初始化
-
在 Objective-C 中,为属性自动生成的成员变量,其访问权限默认为
@private
(无论该属性是定义在 .h 文件还是定义在 .m 文件) -
@synthesize 和 @dynamic 的作用
@property
有两个与之对应的关键字:@synthesize
和@dynamic
如果没有显式声明@synthesize
和@dynamic
,那么默认就是@syntheszie var = _var;
@synthesize
的作用:如果没有手动实现属性的 getter 和 setter,则属性的 getter 和 setter 由编译器自动实现@dynamic
的作用:告诉编译器,属性的 getter 和 setter 由开发者自己实现,不自动生成(对于 readonly 的属性,开发者只需要提供 getter 即可)
假如一个属性被声明为 @dynamic,并且开发者没有提供相应的 getter 和 setter,在编译时编译器是不会发出任何警告的,因为编译器相信该属性对应的 getter 和 setter 可以在运行时动态地找到。但是当程序运行到 instance.var = someVar 时,会由于缺少 setter 方法而导致程序崩溃;或者当程序运行到 someVar = instance.var 时,会由于缺少 getter 方法而导致程序崩溃 -
浅拷贝 && 深拷贝
浅拷贝是指针复制,源对象指针和目标对象指针,指向同一片内存空间,源对象引用计数 +1
深拷贝是内容复制,源对象指针和目标对象指针,指向两片内容相同的内存空间,源对象引用计数保持不变,目标对象引用计数为 1源对象类型 拷贝方式 目标对象类型 拷贝类型(深/浅) 不可变对象 copy 不可变对象 浅拷贝 可变对象 copy 不可变对象 深拷贝 不可变对象 mutableCopy 可变对象 深拷贝 可变对象 mutableCopy 可变对象 深拷贝 总结:
① 可变对象的 copy 和 mutableCopy 都是深拷贝
② 不可变对象的 copy 是浅拷贝,mutableCopy 是深拷贝
③ copy 方法返回的都是不可变对象,mutableCopy 方法返回的是可变对象Question: 以下代码会出现什么问题? @property (nonatomic, copy) NSMutableArray* mArr; Answer: 因为属性 mArr 被声明为 NSMutableArray 类型,所以就不可避免地会去调用属性 mArr 的添加对象、移除对象等方法 又因为属性 mArr 被 copy 修饰,所以无论赋值过来的对象是 NSMutableArray 类型,还是 NSArray 类型,进行copy 操作后,属性 mArr 实际上都是 NSArray 类型 NSArray 是不可变数组,对 NSArray 类型的数组调用添加对象、移除对象等方法,会造成程序异常
-
NSString 类型的属性使用 strong 和 copy 修饰的区别
① 为属性赋值的变量为 NSString 类型
② 为属性赋值的变量为 NSMutableString 类型
-
weak 的实现原理
RunTime 维护了一个 weak 表,其本质是一个全局的哈希表,key 是所指对象的地址,value 是由 weak 指针的地址所组成的数组
① 对象初始化创建时,RunTime 会调用 objc_initWeak 函数,创建一个新的 weak 指针指向该对象的地址
② 对象添加 weak 引用时,objc_initWeak 函数会调用 objc_storeWeak 函数更新指针指向,创建对应的弱引用表
③ 对象释放时,RunTime 会调用 clearDeallocating 函数,通过对象地址(也就是 key 值),找到对应的 weak 指针地址数组(也就是 value 值),遍历 weak 指针地址数组,将其中的 weak 指针的数据置为 nil,并把这个 entry(key-value) 从 weak 表中删除,最后清理对象的记录