Objective-C 的 Property && Instance Variable

成员变量与属性

  • 成员变量与属性 的声明

    在 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 编译器时代:声明属性时必须手动声明与之对应的成员变量
    ① GCC 编译器时代
    ② LLVM 编译器第一版:不需要手动声明成员变量,关键字 @synthesize 默认会去访问与属性同名的成员变量,如果找不到,则会自动生成一个(与属性同名的成员变量)
    因此,从此版本开始,就不需要再手动声明与属性对应的成员变量了
    (在这里,@synthesize name; 等价于 @synthesize name = name;
    ② LLVM 编译器第一版
    ③ LLVM 编译器第二版:考虑到编译器自动生成的成员变量名,与 getter 的方法名、setter 的参数名一样,容易让人误会,从而引起警告,所以从此版本开始,默认为(属性生成的同名成员变量)开头加下划线
    ③  LLVM 编译器第二版
    ④ LLVM 编译器第三版:从 iOS4.5 开始,关键字 @synthesize 也可以省略了。最终变成下面这样
    ④  LLVM 编译器第三版
    如果把上面的隐藏代码全部显示出来的话,其本质如下:
    ④  LLVM 编译器第三版

  • 同时重写属性的 getter 与 setter 的问题

    如果同时重写属性对应的 gettersetter,那么编译器就会报错
    这是因为,当同时重写属性对应的 gettersetter 时,编译器自动添加的代码 @synthesize name = _name; 会失效
    此时编译器不会再为属性自动生成相应的成员变量 _name 了,gettersetter 所操作的成员变量也就不存在了
    所以此时要手动加上 @synthesize name = _name;,显式指定:属性 name 的 gettersetter 要操作的成员变量是 _name
    同时重写属性的 getter 与 setter

  • 实例变量、成员变量、属性 三者的关系

    #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 种:

    1. 基本数据类型(比如,countint 类型)
    2. 类类型,也就是我们常说的实例变量(比如,nameNSString 类型)

    即:成员变量 = 基本数据类型变量 + 实例变量(类类型变量)

    ② 通过 @property 声明的都是属性(编译器默认会生成属性所对应的成员变量)

    ③ 属性是在成员变量的基础上扩充了存取方法

点语法

  • getter 与 setter

    1. getter 与 setter 称之为 属性访问器
    2. 因为具体的成员变量和属性都是相对于对象而言的,所以 getter 与 setter 都是对象方法
    3. getter 方法用于访问成员变量,调用者通过 getter 方法获取对象内部的成员变量的值。在使用 getter 方法获取成员变量时,可以对成员变量进行加工(比如,懒加载)
    4. 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

    强引用持有对象,编译器将为 strongretaincopy 修饰的属性生成带 __strong 所有权修饰符的实例变量

  • __weak

    弱引用持有对象,编译器将为 weak 修饰的属性生成带 __weak 所有权修饰符的实例变量

  • __unsafe_unretained

    弱引用持有对象,编译器将为 unsafe_unretainedassign 修饰的属性生成带 __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 来扩展为新的名称
    同时苹果还支持没有下划线的写法 nullablenonnull
    这些可空性修饰符的区别在于在使用时在代码中的放置位置不同

    注意:
    ① 可空性修饰符仅仅是提供警告,并不会报编译错误
    ② 可空性修饰符,只能用于修饰对象类型,不能用于修饰基本数据类型

    可空性修饰符可以分为以下 4 类:

    1. nullable_Nullable__nullable:对象可以为空,区别在于放置位置不同
    2. nonnull_Nonnull__nonnull:对象不能为空,区别在于放置位置不同
    3. null_unspecified_Null_unspecified__null_unspecified:对象未指定是否可为空,区别在于放置位置不同
    4. 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_BEGINNS_ASSUME_NONNULL_END。在这两个宏之间的代码,所有简单指针类型都被假定为不可空(nonnull),因此开发者只需要去指定那些可空(nullable )的指针类型即可
    Audited Regions

  • 使用规范

    ① 对于属性、方法返回值、方法参数的修饰,使用: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 类如下所示:
    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 类型
    ① 为属性赋值的变量为 NSString 类型
    ② 为属性赋值的变量为 NSMutableString 类型
    ② 为属性赋值的变量为 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 表中删除,最后清理对象的记录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值