iOS 深入理解“属性”及其关键字

时间:2025-01-30 07:01:24

用@property声明的NSString(或NSArray,NSDictionary)经常使用copy关键字,为什么?如果改用strong关键字,可能造成什么问题?

  1. 因为父类指针可以指向子类对象,使用copy的目的是为了让本对象的属性不受外界影响,使用copy无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本.
  2. 如果我们使用是strong,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性.

copy此特质所表达的所属关系与strong类似。然而设置方法并不保留新值,而是将其“拷贝” (copy)。 当属性类型为NSString时,经常用此特质来保护其封装性,因为传递给设置方法的新值有可能指向一个NSMutableString类的实例。这个类是NSString的子类,表示一种可修改其值的字符串,此时若是不拷贝字符串,那么设置完属性之后,字符串的值就可能会在对象不知情的情况下遭人更改。所以,这时就要拷贝一份“不可变” (immutable)的字符串,确保对象中的字符串值不会无意间变动。只要实现属性所用的对象是“可变的” (mutable),就应该在设置新属性值时拷贝一份,以防止之后“值”被可变。

为了理解这种做法,首先要知道,对非集合类对象的copy操作:

在非集合类对象中:对immutable对象进行copy操作,是指针复制,mutableCopy操作时内容复制;对mutable对象进行copy和mutableCopy都是内容复制。用代码简单表示如下:

  • [immutableObject copy] // 浅复制
  • [immutableObject mutableCopy] //深复制
  • [mutableObject copy] //深复制
  • [mutableObject mutableCopy] //深复制

比如以下代码:

NSMutableString *string = [NSMutableString stringWithString:@"origin"];//copy
NSString *stringCopy = [string copy]; 

查看内存,会发现 string、stringCopy 内存地址都不一样,说明此时都是做内容拷贝、深拷贝。即使你进行如下操作:

[string appendString:@"origion!"]

stringCopy的值也不会因此改变,但是如果不使用copy,stringCopy的值就会被改变。 集合类对象以此类推。 所以,

用@property声明 NSString、NSArray、NSDictionary 经常使用copy关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操作,为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。

参考链接:iOS 集合的深复制与浅复制



3.让自己的类用 copy 修饰符,重写带 copy 关键字的 setter

若想令自己所写的对象具有拷贝功能,则需实现NSCopying协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现NSCopyiog与NSMutableCopying协议。

具体步骤:

  1. 需声明该类遵从NSCopying协议
  2. 实现NSCopying协议。该协议只有一个方法:

    - (id)copyWithZone: (NSZone*) zone

    注意:一提到让自己的类用 copy 修饰符,我们总是想覆写copy方法,其实真正需要实现的却是“copyWithZone”方法。

以第一题的代码为例:

// .h文件
// /luohanchenyilong/
// /ChenYilong
// 修改完的代码

typedef NS_ENUM(NSInteger, CYLSex) {
    CYLSexMan,
    CYLSexWoman
};

@interface CYLUser : NSObject<NSCopying>

@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, assign, readonly) NSUInteger age;
@property (nonatomic, assign, readonly) CYLSex sex;

- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
+ (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;

@end

然后实现协议中规定的方法:

- (id)copyWithZone:(NSZone *)zone {
    CYLUser *copy = [[[self class] allocWithZone:zone] 
                     initWithName:_name
                                  age:_age
                                  sex:_sex];
    return copy;
}

但在实际的项目中,不可能这么简单,遇到更复杂一点,比如类对象中的数据结构可能并未在初始化方法中设置好,需要另行设置。举个例子,假如CYLUser中含有一个数组,与其他CYLUser对象建立或解除朋友关系的那些方法都需要操作这个数组。那么在这种情况下,你得把这个包含朋友对象的数组也一并拷贝过来。下面列出了实现此功能所需的全部代码:

// .h文件
// /luohanchenyilong/
// /ChenYilong
// 以第一题《风格纠错题》里的代码为例

typedef NS_ENUM(NSInteger, CYLSex) {
    CYLSexMan,
    CYLSexWoman
};

@interface CYLUser : NSObject<NSCopying>

@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, assign, readonly) NSUInteger age;
@property (nonatomic, assign, readonly) CYLSex sex;

- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
+ (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
- (void)addFriend:(CYLUser *)user;
- (void)removeFriend:(CYLUser *)user;

@end

// .m文件

// .m文件
// /luohanchenyilong/
// /ChenYilong
// 

@implementation CYLUser {
    NSMutableSet *_friends;
}

- (void)setName:(NSString *)name {
    _name = [name copy];
}

- (instancetype)initWithName:(NSString *)name 
                             age:(NSUInteger)age 
                             sex:(CYLSex)sex {
     if(self = [super init]) {
        _name = [name copy];
        _age = age;
        _sex = sex;
        _friends = [[NSMutableSet alloc] init];
     }
     return self;
}

- (void)addFriend:(CYLUser *)user {
    [_friends addObject:user];
}

- (void)removeFriend:(CYLUser *)user {
    [_friends removeObject:person];
}

- (id)copyWithZone:(NSZone *)zone {
    
CYLUser *copy = [[[self class] allocWithZone:zone] 
                     initWithName:_name
                                  age:_age
                                  sex:_sex];
    copy->_friends = [[NSMutableSet alloc] initWithSet:_friends 
                                             copyItems:YES];
    return copy;
}@end

4.@property 的本质:ivar、getter、setter 

@property 的本质是什么?

@property = ivar + getter + setter;

下面解释下:

“属性” (property)有两大概念:ivar(实例变量)、存取方法(access method = getter + setter)。

“属性” (property)作为 Objective-C 的一项特性,主要的作用就在于封装对象中的数据。 Objective-C 对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”(access method)来访问。其中,“获取方法” (getter)用于读取变量值,而“设置方法” (setter)用于写入变量值。这个概念已经定型,并且经由“属性”这一特性而成为Objective-C 2.0的一部分。 而在正规的 Objective-C 编码风格中,存取方法有着严格的命名规范。 正因为有了这种严格的命名规范,所以 Objective-C 这门语言才能根据名称自动创建出存取方法。其实也可以把属性当做一种关键字,其表示:

编译器会自动写出一套存取方法,用以访问给定类型中具有给定名称的变量。 所以你也可以这么说:

@property = getter + setter;

例如下面这个类:

@interface Person : NSObject 
@property NSString *firstName; 
@property NSString *lastName; 
@end 

上述代码写出来的类与下面这种写法等效:

@interface Person : NSObject 
- (NSString *)firstName; 
- (void)setFirstName:(NSString *)firstName; 
- (NSString *)lastName; 
- (void)setLastName:(NSString *)lastName; 
@end 

ivar、getter、setter 是如何生成并添加到这个类中的?

“自动合成”( autosynthesis)

完成属性定义后,编译器会自动编写访问这些属性所需的方法,此过程叫做“自动合成”( autosynthesis)。需要强调的是,这个过程由编译 器在编译期执行,所以编辑器里看不到这些“合成方法”(synthesized method)的源代码。除了生成方法代码 getter、setter 之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。在前例中,会生成两个实例变量,其名称分别为 _firstName_lastName。也可以在类的实现代码里通过 @synthesize语法来指定实例变量的名字.(关于@synthesize看第9点)

@implementation Person 
@synthesize firstName = _myFirstName; 
@synthesize lastName = myLastName; 
@end 

我为了搞清属性是怎么实现的,曾经反编译过相关的代码,他大致生成了五个东西

  1. OBJC_IVAR_$类名$属性名称 :该属性的“偏移量” (offset),这个偏移量是“硬编码” (hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。
  2. setter与getter方法对应的实现函数
  3. ivar_list :成员变量列表
  4. method_list :方法列表
  5. prop_list :属性列表

也就是说我们每次在增加一个属性,系统都会在ivar_list中添加一个成员变量的描述,在method_list中增加setter与getter方法的描述,在属性列表中增加一个属性的描述,然后计算该属性在对象中的偏移量,然后给出setter与getter方法对应的实现,在setter方法中从偏移量的位置开始赋值,在getter方法中从偏移量开始取值,为了能够读取正确字节数,系统对象偏移量的指针类型进行了类型强转.

5.@protocol 和 category 中使用 @property

  1. 在protocol中使用property只会生成setter和getter方法声明,我们使用属性的目的,是希望遵守我协议的对象能实现该属性
  2. category 使用 @property 也是只会生成setter和getter方法的声明,如果我们真的需要给category增加属性的实现,需要借助于运行时的两个函数:

    1. objc_setAssociatedObject
    2. objc_getAssociatedObject

如何实现 weak 属性

要实现weak属性,首先要搞清楚weak属性的特点:

weak 此特质表明该属性定义了一种“非拥有关系” (nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似, 然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。

那么runtime如何实现weak变量的自动置nil?

runtime 对注册的类, 会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。

(注:在下文的《使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?》里给出的“对象的内存销毁时间表”也提到__weak引用的解除时间。)

我们可以设计一个函数(伪代码)来表示上述机制:

objc_storeWeak(&a, b)函数:

objc_storeWeak函数把第二个参数--赋值对象(b)的内存地址作为键值key,将第一个参数--weak修饰的属性变量(a)的内存地址(&a)作为value,注册到 weak 表中。如果第二个参数(b)为0(nil),那么把变量(a)的内存地址(&a)从weak表中删除,

你可以把objc_storeWeak(&a, b)理解为:objc_storeWeak(value, key),并且当key变nil,将value置nil。

在b非nil时,a和b指向同一个内存地址,在b变nil时,a变nil。此时向a发送消息不会崩溃:在Objective-C中向nil发送消息是安全的。

而如果a是由assign修饰的,则: 在b非nil时,a和b指向同一个内存地址,在b变nil时,a还是指向该内存地址,变野指针。此时向a发送消息极易崩溃。

下面我们将基于objc_storeWeak(&a, b)函数,使用伪代码模拟“runtime如何实现weak属性”:

// 使用伪代码模拟:runtime如何实现weak属性
// /luohanchenyilong/
// /ChenYilong

 id obj1;
 objc_initWeak(&obj1, obj);
/*obj引用计数变为0,变量作用域结束*/
 objc_destroyWeak(&obj1);

下面对用到的两个方法objc_initWeakobjc_destroyWeak做下解释:

总体说来,作用是: 通过objc_initWeak函数初始化“附有weak修饰符的变量(obj1)”,在变量作用域结束时通过objc_destoryWeak函数释放该变量(obj1)。

下面分别介绍下方法的内部实现:

objc_initWeak函数的实现是这样的:在将“附有weak修饰符的变量(obj1)”初始化为0(nil)后,会将“赋值对象”(obj)作为参数,调用objc_storeWeak函数。

obj1 = 0obj_storeWeak(&obj1, obj);

也就是说:

weak 修饰的指针默认值是 nil (在Objective-C中向nil发送消息是安全的)

然后obj_destroyWeak函数将0(nil)作为参数,调用objc_storeWeak函数。

objc_storeWeak(&obj1, 0);

前面的源代码与下列源代码相同。

// 使用伪代码模拟:runtime如何实现weak属性
// /luohanchenyilong/
// /ChenYilong

id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
/* ... obj的引用计数变为0,被置nil ... */
objc_storeWeak(&obj1, 0);

objc_storeWeak函数把第二个参数--赋值对象(obj)的内存地址作为键值,将第一个参数--weak修饰的属性变量(obj1)的内存地址注册到 weak 表中。如果第二个参数(obj)为0(nil),那么把变量(obj1)的地址从weak表中删除,在后面的相关一题会详解。

使用伪代码是为了方便理解,下面我们“真枪实弹”地实现下:

如何让不使用weak修饰的@property,拥有weak的效果。

我们从setter方法入手:

- (void)setObject:(NSObject *)object
{
    objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
    [object cyl_runAtDealloc:^{
        _object = nil;
    }];
}

也就是有两个步骤:

  1. 在setter方法中做如下设置:

    objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
    
  2. 在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。做到这点,同样要借助runtime:

    //要销毁的目标对象
    id objectToBeDeallocated;
    //可以理解为一个“事件”:当上面的目标对象销毁时,同时要发生的“事件”。
    id objectWeWantToBeReleasedWhenThatHappens;
    objc_setAssociatedObject(objectToBeDeallocted,
                         someUniqueKey,
                         objectWeWantToBeReleasedWhenThatHappens,
                         OBJC_ASSOCIATION_RETAIN);

知道了思路,我们就开始实现cyl_runAtDealloc方法,实现过程分两部分:

第一部分:创建一个类,可以理解为一个“事件”:当目标对象销毁时,同时要发生的“事件”。借助block执行“事件”。

// .h文件
// /luohanchenyilong/
// /ChenYilong
// 这个类,可以理解为一个“事件”:当目标对象销毁时,同时要发生的“事件”。借助block执行“事件”。

typedef void (^voidBlock)(void);

@interface CYLBlockExecutor : NSObject 

- (id)initWithBlock:(voidBlock)block;

@end


// .m文件
// /luohanchenyilong/
// /ChenYilong
// 这个类,可以理解为一个“事件”:当目标对象销毁时,同时要发生的“事件”。借助block执行“事件”。

#import ""

@interface CYLBlockExecutor() {
    voidBlock _block;
}
@implementation CYLBlockExecutor

- (id)initWithBlock:(voidBlock)aBlock
{
    self = [super init];

    if (self) {
        _block = [aBlock copy];
    }

    return self;
}

- (void)dealloc
{
    _block ? _block() : nil;
}

@end

第二部分:核心代码:利用runtime实现cyl_runAtDealloc方法

// CYLNSObject+文件
// /luohanchenyilong/
// /ChenYilong
// 利用runtime实现cyl_runAtDealloc方法

#import ""

const void *runAtDeallocBlockKey = &runAtDeallocBlockKey;

@interface NSObject (CYLRunAtDealloc)

- (void)cyl_runAtDealloc:(voidBlock)block;

@end


// CYLNSObject+文件
// /luohanchenyilong/
// /ChenYilong
// 利用runtime实现cyl_runAtDealloc方法

#import "CYLNSObject+"
#import ""

@implementation NSObject (CYLRunAtDealloc)

- (void)cyl_runAtDealloc:(voidBlock)block
{
    if (block) {
        CYLBlockExecutor *executor = [[CYLBlockExecutor alloc] initWithBlock:block];

        objc_setAssociatedObject(self,
                                 runAtDeallocBlockKey,
                                 executor,
                                 OBJC_ASSOCIATION_RETAIN);
    }
}

@end

使用方法: 导入

#import "CYLNSObject+"

然后就可以使用了:

    NSObject *foo = [[NSObject alloc] init];

    [foo cyl_runAtDealloc:^{
        NSLog(@"正在释放foo!");
    }];

如果对cyl_runAtDealloc的实现原理有兴趣,可以看下这篇博文 Fun With the Objective-C Runtime: Run Code at Deallocation of Any Object