《Effective OC 2.0》读书笔记(二 - 1)对象、消息、运行期

215 阅读8分钟

引言

  • OC对象是基本构造单元,开发者使用它来存储并传递数据。
  • 在对象之间传递数据并执行任务的过程叫做“消息传递”。
  • “OC运行期环境”(Runtime) 提供了一些使对象之间能够传递消息的重要函数,并且包含创建类实例所用到的全部逻辑。

第六条:理解“属性”这一概念

属性

  • 属性是OC的一项特性,用于封装对象中的数据。

  • OC对象会把需要的数据保存为各种实例变量,通过“存取方法”来访问实例变量。

  • “获取方法”(getter) 用于读取变量值,“设置方法”(setter) 用于写入变量值。

  • 通过“点语法” 可以访问,编译器会把点语法转换为对存取方法的调用。

    @interface MRTestClass : NSObject
    {
        NSString *_firstName;
    }
    
    @property (nonatomic, copy) NSString *lastName;
    @property (nonatomic, assign) NSInteger *age;
    
    @end
    
    @implementation MRTestClass
    
    - (instancetype)init
    {
         self = [super init];
         if (self) {
             self->_firstName = @"miao";  //访问成员变量
             self.lastName = @"rui";      //使用点语法方法访问属性
         }
         return self;
     }
    
     @end
    
  • 如上代码所示,_firstName 为 成员变量 ,成员变量的对象布局在编译器就固定了。只要有访问该成员变量的代码,编译器就会把其替换为“偏移量”,这个偏移量是“硬编码”,表示该变量距离存放对象的内存区域的起始地址的偏移量。

  • 使用成员变量时,在修改了类定义之后需要重新编译,否则会出错。

  • 在OC中,实例变量被当做一种“特殊变量”,交由类对象保管,其偏移量在运行期查找。假如类的定义变了,偏移量也会变,保证何时访问实例变量时都能使用正确的偏移量。甚至可以在运行期向分类中新增实例变量。

  • 使用属性时,编译器会自动编写访问这些属性所需的方法。此过程叫做“自动合成”(autosynthesis),在编译期执行。

  • 可以在.m文件中通过@synthesize 语法指定实例变量的名字,一般情况下无需修改默认的实例变量名。

  • 使用@dynamic 关键字告知编译器不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。且编译期访问属性的代码时,即使编译器发现没有定义存取方法也不会报错。

     @implementation MRTestClass
     @synthesize lastName = _myLastName;
     @dynamic age;
     @end
    

属性特质

原子性(atomic / nonatomic)

  • 默认atomic ,即声明属性时没有指明原子性时默认atomic。
  • 具备atomic特质的getter、setter方法会通过锁定机制(@synchronized) 确保原子性。当有多个线程同时访问该属性时,确保同一时间只有一个线程在操作,保证了数据的完整性。但这并不能说使用atomic特质的属性是绝对线程安全的。
  • 在iOS中使用同步锁的开销较大,大量使用atomic特质会带来性能问题。

读写权限(readwrite / readonly)

  • 默认readwrite
  • readwrite 特质的属性有getter、setter存取方法。
  • readonly 特质的属性仅有getter方法。通常在.h文件中声明为只读属性,在分类或.m中重新将其定义为读写属性。

内存管理语义(assign / weak / strong / copy / unsafe_unretained)

  • 默认strong

  • assign,针对纯量类型(CGFloat、NSInteger等)的简单赋值操作。

    • 使用assign修饰OC对象时,表示一种弱引用关系,但是当属性所指对象被销毁时,编译器不会将该属性置为nil,此时向对象发送消息时会crash。
  • weak,表示一种“非拥有关系”,弱引用。

    • 为属性设置新值时,既不保留新值,也不释放旧值。
    • 当属性所指对象被销毁时,属性值也会清空,该属性会被置为nil。
  • strong,表示一种“拥有关系”,强引用。

    • 为该属性设置新值时,会先保留新值并释放旧值,再设置新值。
    - (void)setFirstName:(NSString *)firstName
    {
        [firstName retain];
        [_firstName release];
        _firstName = firstName;
    }
    
  • copy,强引用,与strong类型

    • 为属性设置新值时,不保留新值,而是将其拷贝。
    - (void)setFirstName:(NSString *)firstName
    {
        [_firstName release];
        _firstName = [firstName copy];
    }
    
    • 一般使用该特性来保护属性的封装性,因为传递给设置方法的新值可能是一个可变类型,确保对象中的值不会被随意改变。
    • 一般用于修饰NSString、block
  • unsafe_unretained

    • 与assign语义类似,修饰OC对象,表示“非拥有关系”。当目标对象被销毁时,属性值不会被自动清空。

方法名(getter=<name> / setter=<name>)

  • getter=<name>,指定getter方法的方法名。
@property (nonatomic, getter=isOn) BOOL on;

如上代码所示,假如该属性是BOOL类型,假如想为其getter方法加上前缀,就可以用该办法指定。

  • setter=<name>,指定setter方法的方法名。这种方法并不常见。

第七条:在对象内部要尽量直接访问实例变量

  • 在读取实例变量时采用直接访问的形式,设置实例变量时通过属性来做。

  • 直接访问和通过属性访问的区别:

    • 直接访问实例变量时,编译器会直接访问对象实例变量的内存,速度快。
    • 直接访问实例变量时,不会调用其setter方法,绕过了相关属性所定义的内存管理语义。
    • 直接访问实例变量时,不会触发KVO(键值观测)。
    • 通过属性访问,对于排查相关错误有帮助,可以在getter、setter方法中添加断点来调试,查看属性的调用者及访问时机。
  • 初始化方法中应直接访问实例变量,因为子类可能会重写setter方法。

  • 在dealloc方法中要直接通过实例变量来读取数据。

  • 使用懒加载的属性,要通过存取方法来读取数据。

第八条:理解“对象等同性”这一概念

  • ==操作符,该操作符比较的是两个指针,而不是指针所指的对象。
  • isEqual: 方法,NSObject协议中声明的方法,判断两个对象的等同性。
  • NSObject协议中用于判断等同性的关键方法:
     - (BOOL)isEqual:(id)object;
     @property (readonly) NSUInteger hash;
    
    • 判断规则:当且仅当指针值完全相等时,两个对象才相等。
    • isEqual: 方法判断两个对象相等时,其hash方法必须返回同一个值;
      两个对象的hash方法返回同一个值,isEqual: 方法未必认为两者相等。
    • 编写hash方法时,应使用计算速度快且哈希吗碰撞几率低的算法。

特定类所具有的等同性判定方法

  • isEqualToString:
    NSSTring类独有的等同性判断方法,所传对象必须为NSString类型。
  • isEqualToArray:
    NSArray类独有等同性判断方法,所传对象须为NSArray类型。
  • isEqualToDictionary:
    NSDictionary类独有等同性判断方法,所传对象须为NSDictionary类型。

!当使用isEqualToArray: / isEqualToDictionary: 方法比较两个对象是否相等时,如果所传对象不是对应类型,会产生crash。
!OC在编译器不做强类型检查,开发者应该保证所传对象的类型是正确的。
!在编写判定方法时,应该一并重写isEqual: 方法。当所传对象与接收消息的对象是同一个类,调用自定义的判定方法;否则就交由超类来判断。

- (BOOL)isEqualToTestClass:(MRTestClass *)otherTestClass
{
    ...
}

- (BOOL)isEqual:(id)object
{
    if ([self class] == [object class]) {
        return [self isEqualToTestClass:object];
    } else {
        return [super isEqual:object];
    }
}

等同性判定的执行深度

  • 创建等同性判定方法时,根据整个对象来判断还是仅根据其中几个字段来判断,应按照具体需求来指定检测方案,不要盲目检测每个属性。
    • 以NSArray为例,它的检测方式是先看两个数组所含对象的个数是否相同,若相同则再对每个对应位置的两个对象调用isEqual:方法,如果每一个对应位置上的对象均相等,则认为这两个数组相等,称为“深度等同性判定”。

    • 再以一个类为例,假设该类名为Person类,该类有很多属性,但是有一个必须且不能重复的属性名为name,那么该属性可以作为唯一标识符。在这种情况下,就可以根据标识符是否相同来判断两个对象是否为同一个,而不必逐一去比较该类的所有属性。

容器中可变类的等同性

还有一种情况需要注意的就是把可变对象加入容器中。

此处以NSMutableSet和NSMutableArray为例:

  1. 先将一个数组加入set,打印;
NSMutableSet *set = [NSMutableSet new];
NSMutableArray *arrA = @[@[@1, @2] mutableCopy];

[set addObject: arrA];
NSLog(@"set = %@", set);

//set = {((1,2))}

此时,set中有一个数组对象且该数组包含了两个对象。

  1. 再向set中加入一个与arrA数组所包含对象相同且顺序也相同的数组,打印;
NSMutableArray *arrB = @[@[@1, @2] mutableCopy];

[set addObject: arrB];
NSLog(@"set = %@", set);

//set = {((1,2))}

此时,因为arrB与arrA中的数组对象相等,所以set不会改变。

  1. 再向set中添加一个与arrA不同的数组C,打印;
NSMutableArray *arrC = @[@[@1] mutableCopy];

[set addObject: arrC];
NSLog(@"set = %@", set);

//set = {((1),(1,2))}

此时,set中成功添加了arrC数组。

  1. 修改arrC的内容与arrA一致,打印set;
[arrC addObject: @2];
NSLog(@"set = %@", set);

//set = {((1,2),(1,2))}

此时,打印的set居然有两个一样的内容,根据set的语义是不会允许这种情况出现的,但是现在却不能保证这一点了。

  1. 拷贝set,打印;
NSSet *setB = [set copy];
NSLog(@"setB = %@", setB);

//set = {((1,2))}

此时,setB中又只有一个对象了,这种情况看起来是对的,但是对某些开发者来讲可能不符合他的预期。

 summary:
 尽量保证放入容器中的对象是不可变的;
 如果将某个对象加入容器后再改变它的值时需要注意下可能存在的隐患问题。