美文网首页
探究Objective-C属性关键字

探究Objective-C属性关键字

作者: Minecode | 来源:发表于2017-12-24 14:12 被阅读0次

    来自我的个人博客Minecode.link

    在使用Objective-C时,频繁用到属性关键字。我们应该理解每种属性的意义,并了解一些偏底层的实现,故在此对OC的属性关键字做个浅析。

    基础概念:ivar、getter、setter

    在C语言中,我们通常是直接操作成员变量。而在Objective-C中,使用了“属性”这一概念来封装对象中的数据,OC对象会把需要的数据保存为各种实例变量,同时通过“存取方法”(Access Method)来进行访问,也就是常说的getter和setter。

    所以,ivar是对象的各种实例变量,getter用于获取变量的值,setter用于写入变量的值。

    我们来看一个标准的ivar+getter+setter样板代码:

    @interface Person: NSObject
    {
    // ivar声明
    @private
    NSString *_myName;
    }
    // getter方法
    - (NSString *)myName {
    return _myName;
    }
    // setter方法
    - (void)setMyName:(NSString *)newName {
    _myName = newName;
    }
    

    可以看到,这样的组合方式造成了代码的臃肿,大大降低了开发效率和可读性,实际开发中使用ivar+getter+setter的情况并不常见,这就要引入@synthesize@property这个关键字。

    @synthesize

    这个属性已经很少见到了,它是属于MRC和32bit时代的产物。@synthesize属性用来合成一个属性,变量名如果没有显式声明则默认添加一个下划线的前缀(_变量名)。当然也可以手动声明变量名并建立与@property的关系。

    为了加深理解,我们看一下以下代码,它的逻辑为:手动声明ivar,使用property声明存取方法,使用@synthesize建立ivar和property的关系

    @interface SubClass ()
    {
    // 声明ivar
    NSString *_myName;
    }
    // 声明属性(并合成getter+setter)
    @property (nonatomic, copy) NSString* myName;
    @end
    
    @implementation SubClass
    // 建立myName属性与_myName成员变量的关系
    @synthesize myName = _myName;
    @end
    

    可以看出@synthesize和@property各自负责的工作,虽然这些工作已经由编译器帮我们做了,但是理解这一概念还是很重要的。

    现在我们知道了省略@synthesize声明实际上是因为LLVM的Clang为在ARC模式下会自动生成@synthesize声明,但是这仅限于64位OC运行时中,当使用32位系统时,我们必须要手动声明,否则会报错。我们可以设置NS_BUILD_32_LIKE_64宏来解决这个问题。

    @dynamic

    相对于@synthesize,@dynamic告诉编译器该属性的getter和setter由程序员自行实现,编译器不再自动生成。在运行时执行过程中如果找不到对应存取方法,则会报错。这便是Runtime中的动态绑定。

    同时,使用了@dynamic修饰则必须动态生成方法实现,没有@dynamic myName = _myName;的语法,也就是说我们没有办法静态的建立getter/setter并访问下划线前缀的ivar。对应的解决方法是消息转发和动态方法解析,本文不过多讨论。

    @property

    本质上来说,@property实际上是告知编译器为你的ivar生成getter和setter,并不生成ivar,要理解这一点。但是由于@synthesize无须再手动声明,所以我们使用@property后实际上是声明了ivar+getter+setter的标准模板。

    Runtime下的定义

    我们首先反编译为cpp代码,有关反编译的内容请见Objective-C开发中Clang的使用

    可以发现property在OC运行时中是objc_property_t类型的,定义如下:

    typedef struct objc_property *objc_property_t;
    
    struct property_t {
    const char *name;
    const char *attributes;
    };
    

    property结构体有name和attributes两个成员变量,而attributes则是property的属性定义,我们看一下它的定义:

    /// Defines a property attribute
    typedef struct {
    const char *name;           /**< The name of the attribute */
    const char *value;          /**< The value of the attribute (usually empty) */
    } objc_property_attribute_t;
    

    我们可以通过以下方法获取对应变量:

    // 获取所有属性列表
    class_copyPropertyList
    // 获取属性名
    property_getName
    // 获取属性描述字符串
    property_getAttributes
    // 获取所有属性列表
    property_copyAttributeList
    

    可以看到,每一个attribute对应一种属性修饰符,property所定义的属性就包含其中。对应关系如下

    属性修饰符类型 name value
    属性类型 T 属性类型名
    内存管理 C(copy) &(strong/retain) W(weak) R(readonly)
    自定义getter/setter G(getter) S(setter) 方法名
    原子/非原子类型 N(nonatomic) 空(atomic)
    ivar名称 V 变量名称

    比如我们分别定义一个对象类型、标量、以及id类型的属性来看一下

    属性定义 attributes描述
    @property char charDefault; Tc,V_charDefault
    @property (nonatomic, copy) NSString *myString; T@"NSString",C,N,V_myString
    @property(nonatomic, readonly, retain) id idVar; T@,R,&,V_idVar

    注意:注意描述符中的V_ivar名称,此描述符是基于64bit系统的,因为会自动合成ivar,如果是32bit系统则不会有下划线,前文已做解释。

    Runtime下的实现

    了解了属性在运行时系统下的定义,我们现在探究一下其的实现。
    运行时中有ivar、method、class、object等概念,其中@property就涉及到了ivar和method(get方法和set方法),具体如何实现呢,我们通过反编译来一探究竟。

    在OC中,所有对象都可以认为是id类型,id类型定义为下:

    typedef struct objc_object {
    Class isa;
    } *id;
    

    而id类型就是指向Class类型的指针,那么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;
    };
    

    现在我们大致了解了OC中对象的实现原理。OC中所有对象都可以认为是id类型,而id又是指向Class的指针,Class类型实际是objc_class结构体,其定义了OC对象的基本信息。

    更多Runtime的内容在此不再赘述,我们来看一下属性涉及到的类型:objc_ivar_listobjc_method_list

    struct objc_ivar {
    char *ivar_name;
    char *ivar_type;
    int ivar_offset;
    int space;
    };
    struct objc_ivar_list {
    int ivar_count;
    int space;
    struct objc_ivar ivar_list[1];
    }
    

    在此我们看到了ivar的真面目,它包含了名称、类型、基地址偏移、内存空间。
    同样,objc_method_list定义如下:

    struct objc_method_list {
    struct objc_method_list *obsolete;
    int method_count;
    #ifdef __LP64__
    int space;
    #endif
    /* variable length structure */
    struct objc_method method_list[1];
    };
    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。详情请查阅objc中方法的实现(SEL,IMP)。
    3. 将属性的ivar添加到类的ivar_list中,作为类的成员变量存在。
    4. 将getter和setter加入类的method_list中。之后可以通过直接调用或者点语法来使用。
    5. 将属性的描述添加到类的属性描述列表中。

    属性的获取

    为了记录属性,有以下几个变量:
    ivar_list: 记录成员变量的描述
    method_list: 记录该变量getter和setter的描述
    prop_list: 记录属性的描述
    OBJC_IVAR_$类名_$属性名称: 记录属性相对对象地址的偏移地址(重要)

    其中,记录变量的偏移地址很重要。我们来看一下实现:

    // 生成一个SubClass类型,包含一个属性
    @interface SubClass ()
    @property (nonatomic, strong) NSMutableArray* array;
    @end
    // 在该类的实现中创建一个方法
    @implementation SubClass
    - (void)testArrayMethod {
    self.array = [NSMutableArray array];
    }
    @end
    

    反编译代码,我们查看一下它是如何赋值的:

    // 属性的定义
    extern "C" unsigned long OBJC_IVAR_$_SubClass$_array;
    struct SubClass_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSMutableArray *_array;
    };
    // 赋值(已经去掉了复杂的类型转换代码)
    static void _I_SubClass_testArrayMethod(SubClass * self, SEL _cmd) {
    (objc_msgSend)(self, sel_registerName("setArray:"), (objc_getClass("NSMutableArray"), sel_registerName("array")));
    }
    // 属性的setter方法
    static void _I_SubClass_setArray_(SubClass * self, SEL _cmd, NSMutableArray *array) {
    *(self + OBJC_IVAR_$_SubClass$_array) = array;
    }
    

    我们可以看到,属性的偏移地址命名为OBJC_IVAR_$类名_$属性名称,点语法本质上是调用了setter,而setter中确定属性对应ivar的内存地址则是通过 对象地址+偏移量 来寻址,即*(self + OBJC_IVAR_$_SubClass$_array)

    @Property的属性修饰符

    谈完@property的底层实现,再看一下属性修饰符。此处仅讨论@property的属性修饰符,对于ARC的所有权修饰符(__strong,__weak,__unsafe_unretained,__autorealesing)会专门写一篇文章讨论。

    属性符作用及区别

    属性 内容
    readwrite 属性可读可写,生成getter+setter,默认属性
    readonly 属性只读,只生成getter
    nonatomic 非原子属性,提高性能但线程不安全
    atomic 原子属性,线程安全但可能降低性能
    MRC模式下
    assign 直接赋值,不增加引用计数
    retain 持有对象,引用计数+1
    copy 生成并持有一个新对象,并深拷贝对象的值
    ARC模式下
    strong 强引用,持有对象,引用计数+1,相当于MRC的retain
    weak 弱引用,不持有对象,不增加引用计数,相当于MRC的assign,但在对象销毁后会置为nil
    copy 深拷贝,同MRC的copy
    unsafe_unretained 无须内存管理的对象,相当于MRC的assign,对象销毁后不会置nil,可能造成野指针。(iOS 4之后基本废弃,使用assign替代)

    同时,根据LLVM文档所述,ARC模式下依旧可以使用MRC修饰符,编译器会自动转换。assign对应unsafe_unretainedretain对应strong

    原子属性atomic

    原子属性(atomic)通过加锁来实现访问/赋值的线程安全,但atomic只是保证了getter和setter的线程安全,并没有保证整个对象是线程安全的。比如线程A在读数据,而线程BCD在写数据,虽然BCD并不能同时写,但A读到的数据却是BCD某个时间写入的,无法保证线程安全。同样的,对于objectAtIndex:等非getter/setter方法,则不是线程安全的。

    weak的使用场景及与assign的区别

    首先,weak与assign都表示了一种“非持有关系”(nonowning relationship),也成弱引用,在使用时不会增加被引用变量的引用计数。而weak在引用的对象被销毁后会被指向nil,保证了安全,相反assign不会被置nil,成为野指针。
    其次,对于标量(基础数据类型:int,double,以及OC中使用宏定义的数据类型:CGFloat,NSInteger),只能使用assign。weak只能用于对象,assign可用于对象和标量

    copy的使用场景及注意事项

    使用copy修饰的对象在赋值的时候创建对象的副本,也成深拷贝。实际则是调用了copy方法。支持copy方法要遵守NSCopying协议,实现copyWithZone:方法来生成并持有对象的副本。同时,还有mutableCopy用于实现对于可变对象的深拷贝,如NSMutableArray。
    当我们想复制字符串的值而非直接引用该字符串时,我们就应该深拷贝一份,否则会出现修改原对象值的情况。NSArray、NSDictionary,以及我们自己的类同理。
    但是,对于@property的copy修饰符,只是调用了copy方法,所以只能生成不可变对象。对于如下代码:

    @property (nonatomic, copy) NSMutableArray *mutableArray;
    /* ... */
    NSMutableArray *anotherMutableArray = [NSMutableArray arrayWithObjects:@1,@2,nil];
    self.mutableArray = anotherMutableArray;
    [self.mutableArray removeObjectAtIndex:0];
    

    会发生崩溃。原因在于copy生成了不可变对象,导致removeObjectAtIndex:方法报错。
    所以,对于可变对象,不要使用copy属性修饰符,而是调用mutableCopy方法

    相关资料

    1. Objective-C Runtime Programming Guide - Declared Properties

    相关文章

      网友评论

          本文标题:探究Objective-C属性关键字

          本文链接:https://www.haomeiwen.com/subject/kachgxtx.html