美文网首页
编写高质量的iOS代码(二)

编写高质量的iOS代码(二)

作者: Mustard_Buli | 来源:发表于2016-06-28 14:28 被阅读66次

    理解“属性”这一概念

    • 可以用@property来定义对象中所封装的数据。
    @interface MSTPerson : NSObject
    @property NSString *firstName;
    @property NSString *lastName;
    
    • 通过“特质”来制定存储数据所需的正确语义。
    @property (nonatomic, readwrite, copy) NSString *firstName;
    
    • 原子性:
      • atomic 通过锁定机制。
      • nonatomic 不加锁。
    • 读/写权限:
      • readwrite 拥有"getter"(获取方法)和"setter"(设置方法)。
      • readonly 只会生成"getter"。
    • 内存管理语义:
      • assign 只针对“纯量类型”(scalar type),就是C语言中的一些变量。
      • strong 定义了一种“拥有关系”(owning relationship),在ARC中会是retain count +1。
      • weak 定义了一种“非拥有关系”(nonowning relationship)。设置方法既不保留新值,并释放旧值,在属性所指的对象遭到摧毁时,属性值会清空。
      • unsafe_unretained 此特质的语义和assign相同,但是它是适用于“对象类型”(object type), 但当属性所指的对象遭到摧毁时,属性值不会清空。
      • copy 于strong类似设置方法不保留新值,而是拷贝。一般NSString经常用此特质。但是拷贝都是不可变的。
    • 方法名:
      • getter=<name> 指定“getter”方法名。
    @property (nonatomic, getter=isOn) BOOL on;
    
    • setter=<name> 指定“setter”方法名。但是这种用法不常见。
    • 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
    • 开发iOS程序是应该使用nonatomic属性,因为atomic属性会严重影响性能。

    在对象内部尽量直接访问实例变量

    • 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写。
      • 这两种写法有几个区别:
        • 由于不经过Objective-C的“方法派发”(method dispatch)步骤,所以直接访问实例变量的速度比较快。这种情况下,编译器所生成的代码会直接访问保存对象实例变量的那块内存。
        • 直接访问实例变量,不会调用其“设置方法”,这就绕过了为相关属性所定义的“内存管理语义”。比方说,如果在ARC下直接访问一个声明为copy的属性,那么并不会拷贝该属性,只会保留新值并释放旧值。
        • 如果直接访问实例变量,那么不会触发“键值观测”(KVO)通知。这样做是否会产生问题,还取决于具体的对象行为。
        • 通过属性来访问有助于排查与之相关的错误,因为可以给“获取方法”或“设置方法”中新增“断点”(breakpoint),监控该属性的调用者及其访问时机。
    • 在初始化访问及dealloc方法中,总是应该直接通过实例变量来读写数据。
    • 有时会使用惰性初始化(lazy initialization)技术配置某份数据,这种情况下,需要通过属性来读取数据。
     - (MSTCar *)myCar{
          if (!_myCar) {
              _myCar = [MSTCar alloc] init];
          }
           return _myCar;
     }
    

    理解“对象等同性”(equality)这一概念

    • 若想检测对象的等同性,请提供“isEqual:”与hash方法。
    NSString *strA = @"Mustard 123";
    NSString *strB = [NSString stringWithFormat:@"Mustard %i", 123];
    BOOL equalA = (strA == strB);    //NO
    BOOL equalB = [strA isEqual:strB];    //YES
    BOOL equalC = [strA isEqualToString:strB];    //YES
    

    NSString有一个独有的方法"isEqualToString:",同样的NSArray和NSDictionary也有类似的方法:"isEqualToArray"和"isEqualToDictionary"。调用本类的等同性判断方法相对于"isEqual:"更快一点,因为后者还需要判断受测对象的类型是否一致。

    • 相同对象必须具有相同的hash code(哈希码),但是两个hash code相同的对象未必是相同的。

    • 不要盲目地逐个检测每条属性,而是应该依照具体需求来制定监测方案。

    举个简单的例子,每个MSTPerson类都有四个属性:firstName、lastName、age、identifier。现在需要判断两个person是否相等,只需要判断identifier是否相等,而不需要进行“深度等同性判定”(deep equality,就是比较每个属性是否相等)。

    • 编写hash方法时,应该使用计算速度快而且hash code碰撞几率低的算法。

    以“类族模式”(class cluster pattern)隐藏实现细节

    • 类族模式可以把实现细节隐藏在一套简单的公共接口后面。

    比如UIKit框架中的UIButton类。想创建一个button,需要调用下面这个“类方法”(class method):

     + (UIButton *)buttonWithType:(UIButtonType)type;
    

    这个方法返回的对象取决于用户传入的button type。然而,不管返回的是什么类型的button,都是继承于同一个基类:UIButton。这么做的意义就是,使用者不需要关心创建出来的button具体是哪个子类,也不需要去考虑button绘制方式等实现细节。使用者这需要明白如何设置"title"这样的属性就可以了。
    但是回过头来说,也可以这样写:

     - (void)drawRect:(CGRect)rect {
          if (_type == TypeA) {
              //Draw TypeA button
          } else if (_type == TypeB) {
              //Draw TypeB button
          } /* ... */
     }
    

    这样看上去还算简单,但是需要依照button type来切换的绘制方法会有许多种,这样就变得特别的麻烦。所以应该将这种代码重构为多个子类,把各种button所用到的绘制方法放到相关的子类中去。但是这么做有一个小的弊端,就是使用者需要知道各种子类才行。虽然这样,但是这个小的弊端几乎可以说是忽略不计。

    这里有一点需要注意的是:在创建实例的时候,你可能觉得自己创建的是某个类的实例,然而实际上创建的确实其子类的实例。比如下面这段代码:

    typedef NS_ENUM(NSUInteger, MSTEmployeeType) {
          MSTEmployeeTypeDeveloper,
          MSTEmployeeTypeDesigner,
          MSTEmployeeTypeFinance
    };
    @inerface MSTEmployee : NSObject
    @property (copy, nonatomic) NSString *name;
    @property (assign, nonatomic) NSUInteger salary;
    //用来创建Employee实例
     + (MSTEmployee *)employeeWithType:(MSTEmployeeType)type;
    //Employee每天的工作
     - (void)doADayWork;
    @end
    
    @implementation MSTEmployee
     + (MSTEmployee *)employeeWithType:(MSTEmployeeType)type {
          switch (type) {
              case MSTEmployeeTypeDeveloper:
                  return [[MSTEmployeeDeveloper alloc] init];
                  break;
              case MSTEmployeeTypeDesigner:
                  return [[MSTEmployeeDesigner alloc] init];
                  break;
              case MSTEmployeeTypeFinance:
                  return [[MSTEmployeeFinance alloc] init];
                  break;
          }
     }
     - (void)doADayWork {
          //子类来实现具体的工作
     }
    @end
    

    在上面这个例子中,[employee isMemberOfClass:[MSTEmployee class]]似乎返回的是YES,但是实际上返回的趋势NO,因为employee并非Employee类的实例,而是其某个子类的实例。

    • 系统框架中经常会使用类族。

    像上面所说的UIButton就是一个例子。大部分collection类都是类族,例如NSArray与其可变版本NSMutableArray。
    在类族中实现子类是需要遵循的规范,一般都会定义于基类的文档中,编码前应该先看看。下面,列出了新增子类的时候需要遵守几条规则:

    • 子类应该继承自类族中的抽象基类。

    若要编写NSArray类族的子类,则需令其继承自不可变数组的基类或可变数组的基类。

    • 子类应该定义自己的数据储存方式。

    子类必须用一个实例变量来存放数组中的对象。NSArray本身只不过是包在其他隐藏对象外面的壳,它仅仅定义了所有数组都需具备的一些接口。对于这个自定义的数组子类来说,可以用NSArray来保存其实例。

    • 子类应当覆写超类文档中指明需要覆写的方法。

    每个抽象基类中,都有一些子类必须覆写的方法。🌰:想要编写NSArray的子类,就需要实现count及"objectAtIndex:"方法。


    在既有类中使用关联对象存放自定义数据

    • 可以通过“关联对象”机制来把两个对象连起来:
    //此方法以给定的键和策略为某对象设置关联对象值
     void objc_setAssociatedObject (id object, void *key, id value, objc_AssociationPolicy policy)
    //此方法以给定的键从对象中获取相应的关联对象值
    id objc_getAssociatedObject (id object, void *key)
    //此方法移除指定对象的全部关联对象
    void objc_removeAssociatedObjects (id object)
    

    我们可以把某对象想象成NSDictionary,把关联到该对象的值理解为字典中的条目。于是,存取关联对象的值就相当于在NSDictionary对象调用[object setObject:value forKey:key]与[object objectForKey:key]方法。但是,两者之间有个重要差别:设置关联对象是用的键(key)是个“不透明的指针”(opaque pointer)。在设置关联对象值时,通常使用静态全局变量做键。

    • 定义关联对象时可指定内存管理语义,用以模仿定义属性是所采用的“拥有关系”与“非拥有关系”。
    • 只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于查找的bug。

    相关文章

      网友评论

          本文标题:编写高质量的iOS代码(二)

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