×

iOS属性

96
齐滇大圣
2018.08.26 16:57* 字数 2576

什么是属性

属性是OC语言中的一个机制,我们在OC中用@property来声明一个属性,其实@property是一种语法糖,编译器会自动为你的实例变量生成settergetter方法。

所以你在获取这个属性值(NSLog(@"%@",self.name);)和设置属性值(self.name = @"dasheng")的时候其实是调用了gettersetter方法获取和设置实例值。

一般这个编译器帮你生成的实例变量就是你的属性名前面加个下划线。当然你也可直接@synthesize定义这个实例变量名,早期的时候其实这个变量名是需要你手动定义的,编译器并不会帮你定这个变量名,也就是你一定要写@synthesize,只是现在默认帮你把这一步也省了。只有你自己需要修改一下变量名的时候才需要调用@synthesize,下面是一个例子:

@interface ViewController ()

@property(nonatomic, copy)NSString *name;

@end

@implementation ViewController

//声明实例变量名
@synthesize name = _realName;

- (void)viewDidLoad {
    [super viewDidLoad];

    //通过setter方法给实例变量设置值
    self.name = @"dasheng";
    
    NSLog(@"%@",self.name);
    NSLog(@"%@",_realName);
}

输出值都是:dasheng

当然我们也可以自己定义settergetter方法,比如:

- (NSString *)name{
    return @"apple";
}

- (void)setName:(NSString *)name{
    
    _realName = @"banana";
}

那么我们根据self.name获取跟设置的值都不会变了

NSLog(@"%@",self.name);
NSLog(@"%@",_realName);
self.name = @"dasheng";
NSLog(@"%@",self.name);
NSLog(@"%@",_realName);

//输出
apple
(null)
apple
banana

属性的存储

Runtime下的实现

我们知道类在OC中是objc_class的结构体指针,这个结构体如下所示:

struct objc_class {

        Class isa  OBJC_ISA_AVAILABILITY;

        #if !__OBJC2__

        Class super_class                       OBJC2_UNAVAILABLE; 

        const char *name                        OBJC2_UNAVAILABLE; 

        long version                            OBJC2_UNAVAILABLE; 

        long info                               OBJC2_UNAVAILABLE; 

        long instance_size                      OBJC2_UNAVAILABLE; 

        struct objc_ivar_list *ivars            OBJC2_UNAVAILABLE; 

        struct objc_method_list **methodLists   OBJC2_UNAVAILABLE; 

        struct objc_cache *cache                OBJC2_UNAVAILABLE; 

        struct objc_protocol_list *protocols    OBJC2_UNAVAILABLE; 

        #endif

} OBJC2_UNAVAILABLE; 

我们主要关注其中的
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
这几部分。

objc_ivar_list是该类的成员变量链表,我们上面说过属性其实就是帮你生成一个gettersetter方法,最后方法里操作的那个成员变量其实也就是存储在这个成员变量链表里面的。而gettersetter也就是存储在objc_method_list里面。

下面是ivarsmethodLists存储的指针对应的结构体:

struct objc_ivar {
    char *ivar_name                     OBJC2_UNAVAILABLE;
    char *ivar_type                     OBJC2_UNAVAILABLE;
    int ivar_offset                     OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                           OBJC2_UNAVAILABLE;
#endif
}   
struct objc_method {
    SEL method_name;
    char *method_types;    /* a string representing argument/return types */
    IMP method_imp;
};

所以说整个属性的生成过程在runtime中分为以下几步:

  1. 创建该属性,设置其objc_ivar,通过偏移量和内存占用就可以方便获取。
  2. 生成其getter和setter。
  3. 将属性的ivar添加到类的ivar_list中,作为类的成员变量存在。
  4. 将getter和setter加入类的method_list中。之后可以通过直接调用或者点语法来使用。
  5. 将属性的描述添加到类的属性描述列表中。

获取成员变量和属性

当然C/C++中也提供了对应的函数可以取到成员变量和属性:

//获取整个成员变量链表
Ivar * class_copyIvarList ( Class cls, unsigned int *outCount );

//获取属性链表
objc_property_t * class_copyPropertyList ( Class cls, unsigned int *outCount );

class_copyIvarList函数,返回一个指向成员变量信息的数组,数组中每个元素是指向该成员变量信息的objc_ivar结构体的指针(只是class_copyPropertyList)。这个数组不包含在父类中声明的变量。outCount指针返回数组的大小。需要注意的是,我们必须使用free()来释放这个数组。

举个例子:

@interface Person: NSObject{
    NSString *name;
}

@property(nonatomic, copy)NSString *age;

@end


unsigned int count = 0;
Ivar *members = class_copyIvarList([Person class], &count);
for (int i = 0; i < count; i++) {
    Ivar ivar = members[i];
    const char *memberName = ivar_getName(ivar);
    NSLog(@"变量名 = %s",memberName);
}
free(members);

objc_property_t *properties =class_copyPropertyList([Person class], &count);
for (int i = 0; i<count; i++)
{
    objc_property_t property = properties[i];
    const char* char_f =property_getName(property);
    NSString *propertyName = [NSString stringWithUTF8String:char_f];
    NSLog(@"属性名 = %@",propertyName);
}
free(properties);
//输出

变量名 = name
变量名 = _age
属性名 = age

属性修饰符

我们在声明属性时都会同时声明它的一个修饰符,用来表明它的一个操作行为等。

修饰符 作用
readwrite 属性可读可写,生成getter+setter,默认属性
readonly 属性只读,只生成getter
nonatomic 非原子属性,提高性能但线程不安全
atomic 原子属性,线程安全但可能降低性能
assign 简单赋值,不更改引用计数,用于修饰基础类型的数据(NSInteger)和C语言类型数据(int,float,double,char,bool)
strong 强引用,持有对象,引用计数+1
weak 弱引用,不持有对象,不增加引用计数
copy 深拷贝

原子性修饰符

原子性操作符只有atomic和nonatomic两种修饰符,系统默认的修饰符是atomic。

  • nonatomic:非原子属性,线程不安全的,效率高

  • atomic:原子属性,线程安全的,效率相对低。

atomic属性是为了保证程序在多线程情况下,编译器会自动生成一些互斥加锁代码,避免该变量的读写不同步问题.

atomic属性内部的锁称为自旋锁
自旋锁表示如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。

所以说自旋锁相对于互斥锁是效率比较高,但是很耗资源。大部分情况下我们都会使用nonatomic,提高效率,减少资源的消耗。

我们给nonatomic写setter和getter已经很熟悉了,但是又要怎么给atomic写setter和getter方法呢,要点就是需要加锁,以下是一个对应的代码:

@property (copy, atomic) NSString *name;

- (NSString *)name {

    NSString *name;
    @synchronized (self) {
        name = _name;
    }
    return name;
}

- (void)setName:(NSString *)name {
    @synchronized(self) {      //加锁同步
        if (![_name isEqualToString:name]) {
            _name = name;
        }
    }
}

assign和weak

weakassign都表示了一种“非持有关系”(nonowning relationship),也称弱引用,在使用时不会增加被引用对象的引用计数。weak在引用的对象被销毁后会被指向nil。而assign不会被置nil。

assignunsafe_unretained其实差不多,如果用assign 来修饰对象,当assign指向的对象被释放时,assign就成了一个悬空指针,也就是说它会指向一块无效内存,这时你给这个assign修饰的属性发送消息时就会发生崩溃(也有可能不崩溃,取决于你发送消息的时候那块内存还是否有效)。

所以我们一般不用assign修饰对象,因为用assign修饰的指针会成为悬空指针导致错误。

那为什么用assign来修饰基本数据类型呢?因为基本数据类型是被分配到栈上的,栈的内存会由系统自己自动处理,不会造成悬空指针。

总结assign用于修饰基础类型的数据(NSInteger)和C语言类型数据(int,float,double,char,bool) ,而weak只能用于修饰对象。

strong和copy

使用strong和copy时都会使引用对象引用计数+1。但是使用copy修饰的属性在某些情况下赋值的时候会创建对象的副本,也就是深拷贝。

我上面说某些情况下,实际上不一定都会形成深拷贝,下面我会以字符串类型来说明。

关于copy的深入

为什么声明NSString属性要使用copy

我们在声明一个NSString属性时,其内存相关的特性,我们有两种选择:strong和copy。一般我们都会使用copy,但是为什么使用copy你知道吗?

稍微了解一点的人可能就会觉得这不就是深拷贝和浅拷贝嘛,使用copy就是深拷贝,使用strong就是浅拷贝。

然而真的是这样吗?下面我们来写一个例子:

@interface TestStringClass ()
@property (nonatomic, strong) NSString *strongString;
@property (nonatomic, copy)   NSString *copyedString;
@end
- (void)test {
    NSString *originString = [NSString stringWithFormat:@"abc"];
    self.strongString = originString;
    self.copyedString = originString;
    NSLog(@"origin string: %p, %p", originString, &originString);
    NSLog(@"strong string: %p, %p", _strongString, &_strongString);
    NSLog(@"copy string: %p, %p", _copyedString, &_copyedString);
}

你觉得输出会是什么呢?指针地址肯定是不一样的。普通的想法是认为strong是浅拷贝,copy是深拷贝。那么_strongStringoriginString的内存地址是一样的,_copyedString的内存地址是不一样的。

下面我们来看看实际输出是什么样的:

2015-08-30 14:37:50.573 test[19357:5912951] origin string: 0xa000000006362613, 0x7fff50bfbc48
2015-08-30 14:37:50.574 test[19357:5912951] strong string: 0xa000000006362613, 0x7fe44961d790
2015-08-30 14:37:50.574 test[19357:5912951] copy string: 0xa000000006362613, 0x7fe44961d798

好像跟我们想的不一样?内存地址都是一样的。


下面我们把NSString换成NSMutableString看看,将

NSString *originString = [NSString stringWithFormat:@"abc"];

改为:

NSMutableString *originString = [NSMutableString stringWithFormat:@"abc"];

输出结果:

2015-08-30 14:51:46.119 test[20229:5955951] origin string: 0x7fc27b47ff60, 0x7fff5c14cc48
2015-08-30 14:51:46.120 test[20229:5955951] strong string: 0x7fc27b47ff60, 0x7fc27b6433a0
2015-08-30 14:51:46.120 test[20229:5955951] copy string: 0xa000000006362613, 0x7fc27b6433a8

我们看到originString_strongString内存是一样的,_copyedString内存地址是不一样的。

我们现在来想一下原因,当我们使用NSString的时候其实是不希望他改变的,那么我们一般情况下是使用copy,希望他进行深拷贝,那源字符串修改就不会影响到_copyedString了。但是如果源字符串也是NSString不可变的呢,那其实就算是浅拷贝也不会有什么影响了。
所以系统可能就在当源字符串为不可变类型时,你属性的内存特性为copy其实也只进行浅拷贝。当源属性为可变类型时,才进行深拷贝。


所以我们建议在使用NSString属性时使用copy,避免可变字符串的修改导致的一些非预期问题。

上面这句话我们会常常看到,那么很多人问我了,这种情况什么情景下会出现呢?

我这里举一个最简单的例子,有个ViewController他刚进来的时候有个原价,这是一个原价那当然是不可变咯。

@interface GoodsViewController : UIViewController

@property(nonatomic, strong)NSString *orginPrice;

@end
NSMutableString *_mutablePrice = [NSMutableString stringWithFormat:@"100"];

GoodsViewController *goodsVC = [[GoodsViewController alloc] init];
goodsVC.orginPrice = _mutablePrice;
[self.navigationController pushViewController:goodsVC animated:YES];

我们先假设orginPricestrong
我们已经进入GoodsViewController,这个商品的原价就是100,我们不希望他发生改变。这时可能哪里发了个通知,_mutablePrice100变成了200
GoodsViewController也接收到了通知,准备把orginPrice100变为200。但是这时候因为是strong只是浅拷贝,orginPrice_mutablePrice变为200的那一刻已经改为200,这时如果你再加100,其实orginPrice就变成300了,这就不是我们想看到的了。

那如果orginPricecopy呢:
这时发生了深拷贝,_mutablePrice的改变跟orginPrice没有关系了,所以不用担心产生上面那样的问题。

为什么声明NSMutableString属性不能用copy

因为使用copy就是深拷贝了一个不可变的NSString对象。这时如果对这个对象进行可变操作,会产生崩溃。

@property(nonatomic, copy)NSMutableString *copyString;

//这句产生崩溃
[copyString appendString:@"齐滇大圣"];

等价于

NSMutableString *mutableString = [[NSMutableString alloc] initWithFormat:@"我是"];
    
NSMutableString *copyString = [mutableString copy];
  
//这句产生崩溃
[mystring appendString:@"齐滇大圣"];

如何让自己的类用 copy 修饰符

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

NSCopying协议中的声明的方法只有一个- (id)copyWithZone:(NSZone *)zone。当我们的类实现了NSCopying协议,通过类的对象调用copy方法时,copy方法就会去调用我们实现的- (id)copyWithZone:(NSZone *)zone方法,实现拷贝功能。实现代码如下所示:

@implementation PersonModel {
    NSString *_nickName;
}

- (id)copyWithZone:(NSZone *)zone{    
    PersonModel *model = [[[self class] allocWithZone:zone] init];
    model.firstName = self.firstName;
    model.lastName  = self.lastName;
    //未公开的成员
    model->_nickName = _nickName;
    return model;
}

NSMutableCopying中对于的声明方法为- (id)mutableCopyWithZone:(NSZone *)zone。跟NSCopying的区别就是返回的对象是否是可变类型。


下面我们来写个例子看看如何运用:

PersonModel *person1 = [[PersonModel alloc] init];
person1.firstName = @"郑";
   
PersonModel *person2 = person1;
person2.firstName = @"吴";
    
NSLog(@"%@",person1.firstName);

输出值:吴

因为这个person1对象根本没有被深拷贝,所有person2改变的时候,person1也被改变了。


我们修改代码如下:

PersonModel实现NSCopying协议

@interface PersonModel : NSObject<NSCopying>

@property(nonatomic, copy)NSString *firstName;

@end

@implementation PersonModel

- (id)copyWithZone:(NSZone *)zone{
    
    PersonModel *person = [[[self class] allocWithZone:zone] init];
    person.firstName = _firstName;
    return person;
}

@end

PersonModel *person1 = [[PersonModel alloc] init];
person1.firstName = @"郑";
   
PersonModel *person2 = [person1 copy];
person2.firstName = @"吴";
    
NSLog(@"%@",person1.firstName);

输出值:郑

iOS开发之路
Web note ad 1