iOS Objective-C 语言基础1 —— @property必知必会

这篇文章讲了@property相关的面试题,有些结论我已经用实际运行过的代码验证了,可放心食用。(不放心也可以自己拿去跑一跑)。面试题来源为 sunnyxxx的博客招聘一个靠谱的 iOS

@property 后面可以有哪些修饰符?

原子性

  • atomic:

    @property使我们可以创建一个属性,系统会生成对于这个属性的 getter 和 setter函数。对这个属性的读写操作则是通过 setter 和 getter完成的。假如设置了atomic 属性,那就会对 setter 和 getter 进行加锁,在执行的时候保证只有当前函数能执行,也就是「互斥性」。这也保证了「原子性」,也就是其他线程不会读取到中间值。

    但是加锁有性能上的损耗,尽量不使用,它的使用场景是多线程环境下、对一个属性进行读取和写入操作,使用atomic 能够确保同一时间只有一个线程能够修改和访问。

    但是不能保证绝对的线程安全。比如在并发环境下,对一个属性进行+1 操作。A线程读取得到「100」,然后 B线程也读到「100」,A 线程写入「101」,B 线程也写入「101」。最后得到的值自然就小于 30000.

    而如果使用 NSLock 进行加锁,那self.intSource = self.intSource + 1; 这一行代码的执行就能保证互斥性。也就保证了最后数据的正确性(属性是不是 atomic 的已经无影响了了,因为此时锁的作用范围变大了)

  - (void)testAutomicProperty {
    self.intSource = 0;
    
    NSLock *myLock = [[NSLock alloc] init];
    
    dispatch_queue_t queue1 = dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //    dispatch_queue_t queue2 = dispatch_queue_create("555", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_group_t group = dispatch_group_create();
    // 异步的,都加到 group 中
    dispatch_group_async(group, queue1, ^{
        for (int i = 0; i < 10000; i++) {
            [myLock lock];
            self.intSource = self.intSource + 1;
            [myLock unlock];
        }
    });
    dispatch_group_async(group, queue1, ^{
        for (int i = 0; i < 10000; i++) {
            [myLock lock];
            self.intSource = self.intSource + 1;
            [myLock unlock];
        }
    });
    dispatch_group_async(group, queue1, ^{
        for (int i = 0; i < 10000; i++) {
            [myLock lock];
            self.intSource = self.intSource + 1;
            [myLock unlock];
        }
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 打印最后的结果
        NSLog(@"最后的结果是:%d", self.intSource);
    });
}

// 不加锁:最后的结果 <= 30000
// 加锁:结果 == 30000

nonatomic:setter 和 getter 不加锁。

内存管理

  • strong:强引用

  • copy:copy 修饰的属性,在给它赋值的时候,不是直接把指针赋值,而是深拷贝,把内容赋值给它。这样做,这个属性的值就有了独属于自己的一个值(一个对象),赋值给它的对象修改了,也不影响该属性的值。

    copy 常用于不可变对象(NSString、NSArray)。

    既然是深拷贝,那就跟原来的对象没关系,于是对原来对象的引用计数不会有影响。

    对于可变对象,比如 NSMutableString类型的对象,不能使用 copy 修饰 ta。因为 copy 出来的只能是不可变对象。如果我们给「可变对象」用了 copy,那实际上「可变对象指针」指向的是一个「不可变对象」,如果用它调用修改的方法,如appendString,就会报错,Attempt to mutate immutable object with xxx

  • assign:不执行 retain 操作,引用计数不会添加。用于修饰对象时,当引用对象被销毁后,如果再向 ta 发送消息,会报野指针错误。

  • weak:弱引用,引用对象的引用计数不会增加。当引用的对象被销毁时,weak 指针会自动置 nil,后续对 ta 发送消息,不会报野指针错误(跟 assign 属性的作用形成了明显的对比)。

    一个常见的面试问题是:Runtime是怎么实现weak 属性的自动置 nil 的?

    Runtime 维护了一个表格SideTables:

    struct SideTable {
    spinlock_t slock; // 确保原子性操作的锁,虽然名字还叫 spinlock_t ,其实本质已经是 mutex_t,具体可见 objc-os 源码( `using spinlock_t = mutex_tt<LOCKDEBUG>;` )
    RefcountMap refcnts; // 用来存储对象引用计数的 hash 表
    
    weak_table_t weak_table // 存储对象 weak 引用指针的 hash 表。自动置 nil 就靠这个属性
    //...
    };

    其中weak_table_t是一个哈希数组,创建 weak 指针引用的对象a 时,把 a 的内存地址作为 key,把 weak 指针作为 value 存入表中。所有 weak 指针都存进来。当销毁 a 对象时,会拿到 a的内存地址,去 weak_table_t 中找到对应 entry,把 value 逐一置 nil,然后将这个 entry 从表格中抹去。

  • retain:MRC环境下的 strong。修饰对象的引用计数+1。

  • unsafe_unretained:跟 weak 类似,但是不会自动置 nil,引用计数不 +1.

函数名字(setter 和 getter)

该修饰符可以指定 setter 和 getter 的名字。

这个修饰符的使用场景是:比如我们有一个 BOOL 类型的属性,叫 safe, 那么它默认的 getter 名字就是 safe,而用 getter 修饰符就能把 getter 的名字变成 isSafe,更加符合命名规范。

读写权限

  • readwrite
  • readonly

什么情况使用 weak 关键字,相比 assign 有什么不同?

  • 修饰对象,用 weak。因为 weak 可以用自动置 nil
  • 一般数据类型就用 assign。比如 NSInteger,NSUInteger,

怎么用 copy 关键字?

对于不可变对象,就用 copy 关键字,保证了该属性指向的值不会因为赋值对象的改变而改变。

这个写法会出什么问题: @property (copy) NSMutableArray *array;

如果向 array 对象发送修改的消息,比如addObject: 时,就会报错说Attempt to mutate immutable object with appendString:',因为 copy 存的只能是 不可变对象,也就是 NSArray.

如何让自己的类用 copy 修饰符?如何重写带 copy 关键字的 setter?

苹果文档Copying Collections

——有啥用呢?

能保证自己的类的对象是线程安全的。

需要实现NSCopying协议,该协议只有一个方法

- (id)copyWithZone:(NSZone *)zone;
举个例子
@interface MyObject : NSObject <NSCopying>

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSMutableArray *mutableArray;

@end

@implementation MyObject

- (id)copyWithZone:(NSZone *)zone {
    MyObject *copy = [[[self class] allocWithZone:zone] init];
    copy.name = [self.name copyWithZone:zone]; // 对字符串属性执行深拷贝
    copy.mutableArray = [self.mutableArray mutableCopyWithZone:zone]; // 对可变数组属性执行深拷贝
    return copy;
}

@end

@property 的本质是什么?ivar、getter、setter 是如何生成并添加到这个类中的 ?

本质是 ivar、getter、setter 。

ivar:Instance Variable,实例变量。

是这样添加的:自动合成auto synthesis。不仅创建 setter 和 getter,同时生成下划线开头的变量。

@protocol 和 category 中如何使用 @property ?

  • @protocol 定义的@property 只能是指示如何处理属性,而不包括 setter 和 getter 的实现。
  • category 中使用@property只能创建 setter 和 getter 的声明,具体的函数实现要自己借助关联对象实现,以下是一个例子
#import "NSObject+AssociatedObject.h"

@implementation NSObject (AssociatedObject)

// 使用关联对象实现属性的 getter 方法
- (NSString *)associatedProperty {
    return objc_getAssociatedObject(self, @selector(associatedProperty));
}

// 使用关联对象实现属性的 setter 方法
- (void)setAssociatedProperty:(NSString *)newValue {
    objc_setAssociatedObject(self, @selector(associatedProperty), newValue, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

参考资料


iOS Objective-C 语言基础1 —— @property必知必会
http://example.com/2023/09/19/property/
作者
发布于
2023年9月19日
许可协议