“理解”OC的属性

1,536 阅读6分钟

在不使用@property来封装数据使用,Objective-C对象通常会把其所需要的数据保存为各种实例变量,我们先来看实例变量的用法:

OC中实例变量的定义:
实例变量可以定义在:类接口,匿名扩展。

注意:不能在类别中定义实例变量

//类接口中实例变量的定义
@interface MyClass : NSObject {
    @public
    NSString *publicString;
    @private
    NSString *_privateString;
}
@end
//匿名扩展中实例变量的定义
@interface MyClass() {
NSString *_categoryString;
}
@end

实例变量的访问:

- (instancetype)init {
    if (self = [super init]) {
        _categoryString = @"categoryString";
        _privateString = @"privateString";
        publicString = @"publicString";
    }
    return self;
}

在外部只能访问到"@public"定义的实例变量:

MyClass *obj = [MyClass new];
obj->publicString = @"modify publicString";

setter和getter方法

- (NSString *)implementString {
    return _implementString;
}

- (void)setImplementString:(NSString *)string {
    _implementString = [string copy];
}

读取和写入变量:

[self implementString];
[self setImplementString:@"publicString"];

属性
在实际的OC编码中我们很少像上面那样用实例变量,我们大多数时候用的是属性,之所以写上面的实例变量是为了更清楚的知道属性为我们做了什么。

引入《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》中第6条:理解”属性“这一概念:

  • 属性(property)是Objective-C的一项特性,用于封装对象中的数据。Objective-C对象通常会把其所需要的数据保存为实例变量
  • 可以令编译器自动编写与实例变量相关的存取方法。
  • 引入了一种新的"."语法,使开发者可以更容易的访问和存放数据。

实例变量定义为属性后:

@interface MyClass : NSObject
@property (nonatomic, copy) NSString *publicString;
@end

@interface MyClass()
@property (nonatomic, copy) NSString *privateString;
@property (nonatomic, copy) NSString *categoryString;
@property (nonatomic, copy) NSString *implementString;
@end

@implementation MyClass

- (instancetype)init {
    if (self = [super init]) {
        self.categoryString = @"categoryString";
        _privateString = @"privateString";
        _implementString = @"_implementString";
    }
    return self;
}

由上可以看出编译器自动向类中添加适当类型的实例变量,并且在属性名前面加"_",以此作为实例变量的名字。
我们也可以修改它创建的实例变量的名字,比如我想让属性“privateString”自动创建的实例变量名为"_myPrivateString",可以用以下方法实现:

@implementation MyClass
//更改实例变量名
@synthesize privateString = _myPrivateString;

- (instancetype)init {
    if (self = [super init]) {
        //可以用自定义的实例变量名来访问了
        _myPrivateString = @"myPrivateString";
        //但是不影响存取方法的名字
        self.privateString = @"privateString";
        _implementString = @"_implementString";
    }
    return self;
}

@end

我们也可以使用@dynamic关键字,告诉编译器不要自动创建实例变量,也不要为其创建存取方法。而且在编译访问属性的代码时,即使编译器没有发现存储方法也不会报错,因为它相信这些方法能在运行时找到。示例:

@implementation MyClass

@synthesize privateString = _myPrivateString;
@dynamic publicString;

- (instancetype)init {
    if (self = [super init]) {
        _categoryString = @"categoryString";
        _myPrivateString = @"privateString";
        _implementString = @"_implementString";

        self.publicString = @"publicString";
        NSLog(@"%@", self.publicString);
    }
    return self;
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selectorString = NSStringFromSelector(sel);
    if ([selectorString isEqualToString:@"setPublicString:"]) {
        class_addMethod(self, sel, (IMP)setPublicString, "v@:@");
        NSLog(@"add %@", selectorString);
        return YES;
    } else if([selectorString isEqualToString:@"publicString"]) {
        class_addMethod(self, sel, (IMP)publicString, "@@:");
        NSLog(@"add %@", selectorString);
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

static void *publicStringKey = "publicStringKey";
void setPublicString(id self, SEL _cmd, NSString *publicString) {
    MyClass *typedSelf = (MyClass *)self;
    objc_setAssociatedObject(typedSelf, publicStringKey, publicString, OBJC_ASSOCIATION_COPY);
}
id publicString(id self, SEL _cmd) {
    MyClass *typedSelf = (MyClass *)self;
    return objc_getAssociatedObject(typedSelf, publicStringKey);
}

@end

打印结果:

**2016-12-13 00:34:16.736 IOSAPILearn[41692:15613165] add setPublicString:**
**2016-12-13 00:34:16.739 IOSAPILearn[41692:15613165] add publicString**
**2016-12-13 00:34:16.740 IOSAPILearn[41692:15613165] publicString**

可以看出"."存取,分别访问了setPublicString:和publicString方法。

属性特性

  • 原子性:atomic或者nonatomic。在默认情况下, 由编译器所合成的方法会通过锁定机制确保其原子性。如果属性具有nonatomic(非原子)特性,则不使用同步锁。
  • 读/写权限:readwrite或者readonly。由@synthesize实现(非@dynamic实现)的readwrite属性,编译器自动生成getter和setter方法。由@synthesize实现的readonly属性,编译器只为其合成setter方法,可以对外公开为readonly,然后在匿名类别中声明为readwrite。
  • 内存管理语义:
属性声明的属性 所有权修饰符
assign __unsafe_unretained修饰符
copy __strong修饰符(但是赋值的是被复制的对象)
retain __strong修饰符
strong __strong修饰符
unsafe_unretained __unsafe_unretained修饰符
weak __weak修饰符
  • 方法名:getter=和setter=指定“获取方法(getter)”和“设置方法(setter)”的方法名。

示例:

@interface MyClass : NSObject
//设置getter和setter方法名,内存管理语义为copy,非原子特性
@property (nonatomic, copy, getter=mypublicString, setter=setMypublicString:) NSString *publicString;
//设置只读权限,非原子特性,内存管理语义为copy
@property (nonatomic, copy, readonly) NSString *implementString;
@end

@interface MyClass()
@property (nonatomic, copy, readonly) NSString *privateString;
@property (atomic, copy, readwrite) NSString *categoryString;
//在匿名类别中将该属性重新设置为可读写
@property (nonatomic, copy, readwrite) NSString *implementString;
@end

@implementation MyClass

@synthesize privateString = _myPrivateString;
@dynamic publicString;

- (instancetype)init {
    if (self = [super init]) {
        //此时mypublicString和publicString都可以访问,但调用的setter和getter方法是一样的,看下面的打印结果
        self.mypublicString = @"mypublicString";
        NSLog(@"%@", self.mypublicString);

        self.publicString = @"publicString";
        NSLog(@"%@", self.publicString);
    }
    return self;
}

//重写setter时,要遵循内存管理语义特性。
- (void)setImplementString:(NSString *)implementString  {
    _implementString = [implementString copy];
}

//@dynamic publicString 中的存取方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selectorString = NSStringFromSelector(sel);
    if ([selectorString isEqualToString:@"setMypublicString:"]) {
        class_addMethod(self, sel, (IMP)setPublicString, "v@:@");
        NSLog(@"add %@ method", selectorString);
        return YES;
    } else if([selectorString isEqualToString:@"mypublicString"]) {
        class_addMethod(self, sel, (IMP)publicString, "@@:");
        NSLog(@"add %@ method", selectorString);
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

static void *publicStringKey = "publicStringKey";
void setPublicString(id self, SEL _cmd, NSString *publicString) {
    MyClass *typedSelf = (MyClass *)self;
    objc_setAssociatedObject(typedSelf, publicStringKey, publicString, OBJC_ASSOCIATION_COPY);
    NSLog(@"use setter method: setMypublicString:");
}

id publicString(id self, SEL _cmd) {
    MyClass *typedSelf = (MyClass *)self;
    NSLog(@"use getter method: mypublicString");
    return objc_getAssociatedObject(typedSelf, publicStringKey);
}

@end

打印结果:

**2016-12-13 10:05:19.454 IOSAPILearn[43443:15723711] add setMypublicString: method**
**2016-12-13 10:05:19.460 IOSAPILearn[43443:15723711] use setter method: setMypublicString:**
**2016-12-13 10:05:19.461 IOSAPILearn[43443:15723711] add mypublicString method**
**2016-12-13 10:05:19.462 IOSAPILearn[43443:15723711] use getter method: mypublicString**
**2016-12-13 10:05:19.462 IOSAPILearn[43443:15723711] mypublicString**
**2016-12-13 10:05:19.464 IOSAPILearn[43443:15723711] use setter method: setMypublicString:**
**2016-12-13 10:05:19.465 IOSAPILearn[43443:15723711] use getter method: mypublicString**
**2016-12-13 10:05:19.465 IOSAPILearn[43443:15723711] publicString**

混合用法
属性是对实例变量的一种封装,属性内部自动生成了于之对应的实例变量,然后通过设置属性特性来封装这个实例变量:

  • 根据读写权限和getter/setter方法名自动生成相对应的存取方法
  • 通过内存管理语义影响设置方法(setter)的实现(比如是否要retain,是否要copy等)
  • 通过原子性来判断读写时,是否要加锁

这些属性自动封装的实现都是编译器做的。

其实我们可以把实例变量属性搭配着一起使用:
比如我在匿名类别中定义了一个属性:

@interface MyClass() {
    NSString *_categoryString;
}
@end

在没有写所有权修饰符时,默认为__strong修饰符

然后我希望编译器为这个实例变量自动生成存取方法,并可以通过"."来访问。那我只需要定义一个同名属性:

@interface MyClass() {
    NSString *_categoryString;
}
//属性会自动生成getter方法,通过copy语义生成setter方法
@property (copy) NSString *categoryString;
@end

注意:变量与属性声明中的内存管理语义中的所有权修饰符必须一致,否则会引起编译错误,例如属性如果声明为:@property (weak) NSString *categoryString,则会提示错误:Existing instance variable '_categoryString' for __weak property 'categoryString' must be __weak

在重写setter方法时,要遵循内存管理语义:

void setCategoryString:(NSString *)categoryString {
  _categoryString = [categoryString copy];
}

简书个人主页:www.jianshu.com/users/b92ab…