《Effective Objective-C 2.0编写高质量i

作者: Mr_WangZz | 来源:发表于2019-01-22 17:24 被阅读0次

    用Objective-C等面对对象语言编程时,“对象”(object)就是“基本构造单元”(building block),开发者可以通过对象来存储并传递数据

    在对象之间传递数据并执行任务的过程,就叫做“消息传递”。

    当应用程序运行起来后,为其提供相关支持的代码叫做“Objective-C运行期环境”(Objective-C runtime)。它提供了一些使得对象之间能够传递消息的重要函数,并且包含创建类实例所用的全部逻辑。

    6. 理解“属性”这一概念

    //copy
    //  EOCPerson.h
    
    @interface EOCPerson : NSObject
    
    @property (copy) NSString *firstName;
    
    @property (copy) NSString *lastName;
    
    //与下面setter和getter方法等效
    //- (NSString *)firstName;
    //- (void)setFirstName:(NSString *)firstName;
    //- (NSString *)lastName;
    //- (void)setLastName:(NSString *)lastName;
    
    - (id)initWithFirstName:(NSString *)firstName
                   lastName:(NSString *)lastName;
    
    @end
    /************************/
    //  EOCPerson.m
    
    #import "EOCPerson.h"
    
    @implementation EOCPerson
    
    - (id)initWithFirstName:(NSString *)firstName
                   lastName:(NSString *)lastName {
        
        if ((self = [super init])) {
            _firstName = [firstName copy];
            _lastName = [lastName copy];
        }
        return self;
    }
    
    @end
    

    属性特质

    分为4类:通过这些特质,可以 微调 由编译器所合成的存取方法。

    1. 原子性
    .1 默认情况下,由编译器所合成的方法会通过**锁定机制**确保其原子性(atomicity)。
    .2 若属性具备 nonatomic 特性,则不适用同步锁。
    .3 若未标明具备 “atomic”或 “ nonatomic” 特性,那么,仍然是“原子的”的属性特质。
    
    2. 读/写权限
    .1 readwrite特质
        声明该特质的属性,拥有 getter (获取方法)和 setter (设置方法);
        但,当该属性由 @synthesize 实现时,编译器会自动为其合成读取方法。
    .2 readonly特质
        声明该特质的属性,仅拥有获取方法。
        但,当该属性由 @synthesize 实现时,编译器才会为其合成获取方法。
        特殊写法:可以利用此特质,可以把某个属性对外公开为只读属性,然后在.m文件中,将其重新定义为读写属性。详见27条
    
    3. 内存管理语义
    属性用于封装数据,而数据则要有“具体的所有权语义”。下面的这些特性仅会影响“设置方法”setter。
    .1 assign
        setter 只会执行针对“纯量类型”的简单赋值操作。如CGFloat、NSInteger等
    .2 strong 此特质表明该属性:
        定义了一种“拥有关系”(owning relationship)。
        为这种方法设置新值时,设置保留新值->释放旧值->设置为新值
    .3 weak 此特质表明该属性:
        定义了一种“非拥有关系”(nonowning relationship)。
        为这种方法设置新值时,设置方法既不保留新值,也不释放旧值。
        在属性所指对象遭到摧毁时,属性值也会清空(nil out)。
    .4 unsafe_unretained
        语义与 assign 比较,设置方法只会针对“对象类型”。
        该特质表达了一种“非拥有关系”(unretained),当目标对象遭到摧毁时,属性值不会自动清空(unsafe,不安全),与weak有区别。
    .5 copy
        设置方法并不保留新值,而是“拷贝”。
        当属性类型为NSString *时,经常用此特质保护其封装性。
        原因:新值可能是指向一个NSMutableString类的实例,是NSString的子类,那么设置完成后,字符串的值就可能在对象不知情的情况下遭人更改。所以要拷贝一份,确保对象中的字符串值不会无意间变动。
        案例:setter方法:_xxxx = [xxxx copy];
    
    4. 方法名
    可通过如下特质指定存取方法的方法名:
    .1 getter=<name>
        指定获取方法的方法名。
        当属性为BOOL或Boolean类型时,如果想在获取方法前面加上“is”前缀,就可以使用此方法。
        案例:@property (nonatomic, getter=isOn) BOOL on;
    .2 setter=<name>
        指定设置方法的方法名。
        用法不常见。
    
    要点
    • 可以用 @property 语法来定义对象中所封装的数据。
    • 通过 “特质” 来指定存储数据结构所需的正确语义。
    • 在设置属性所对应的实例变量时,一定要遵从该属性声明的语义。
    • 开发iOS程序时应该使用 nonatomic 属性,因为 atomic 属性会严重影响性能。

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

    使用“点语法” 和 直接访问实例变量 的写法区别

    .1 直接访问实例变量的 速度比较快。
        这种方式,不经过Objective-C的“方法派发”(method dispatch,见11条),编译器所生成的代码会 直接访问 保存对象实例变量的那块 内存。
    .2 直接访问实例变量时,不会调用其“设置方法”。
        绕过了为相关属性所定义的“内存管理语义”。
    .3 直接访问实例变量,不会触发“键值观测”(KVO)通知。
        是否因此产生问题,取决于具体的对象行为。
    .4 通过属性来访问,有助于排查与之相关的错误。
        因为,可以给“获取方法”或“设置方法”中新增断点(breakpoint),监控该属性的调用者及其访问时机。
    
    要点
    • 在 对象内部 取数据时,应该直接读取实例变量(_xxxx);而 入数据时,通过属性来写(set.xxx)。
    • 在 初始化方法 和 dealloc 方法中,总是应该 直接通过实例变量(_xxxx) 来读写数据。
    • 有时会使用 惰性初始化技术(懒加载) 配置某份数据,在这种情况下,需要通过属性来读数据。

    8.理解概念:“对象等同性”

    NSString *elem = @"hello 123";
    NSString *elem2 = [NSString stringWithFormat:@"hello %d", 123];
        
    NSLog(@"1 %@", (elem == elem2)?@"YES":@"NO");//NO
    NSLog(@"2 %@", [elem isEqual:elem2]?@"YES":@"NO");//YES
    NSLog(@"3 %@", [elem isEqualToString:elem2]?@"YES":@"NO");//YES。字符串比较时,用时优于isEqual
        
    NSLog(@"4 %lu_%lu_%@", (unsigned long)[elem hash], (unsigned long)[elem2 hash], ([elem hash] == [elem2 hash])?@"YES":@"NO");//比较哈希值:YES
    
        
    NSArray *arr1 = @[@"1", @"2"];
    NSArray *arr2 = [NSArray arrayWithObjects:@"1", @"2", nil];
    if ([arr1 isEqualToArray:arr2]) {
        NSLog(@"%@: YES", NSStringFromSelector(_cmd));
    }else {
        NSLog(@"%@: NO", NSStringFromSelector(_cmd));
    }
    
    

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

    #pragma mark - 8. 判断等同性
    
    - (BOOL)isEqualToEOCPerson:(EOCPerson *)anotherPerson {
        
        if (self == anotherPerson) {
            return YES;//判断两个指针是否相等
        }
        
        if (![_firstName isEqualToString:anotherPerson.firstName]) {
            return NO;
        }
        if (![_lastName isEqualToString:anotherPerson.lastName]) {
            return NO;
        }
        if (_age != anotherPerson.age) {
            return NO;
        }
        
        return YES;
    }
    
    - (BOOL)isEqual:(id)object {
        
        if ([[self class] isKindOfClass:[object class]]) {
            return [self isEqualToEOCPerson:(EOCPerson *)object];
        } else {
            return [super isEqual:object];
        }
        
    }
    

    容器中可变类的等同性: 向比较对象中插入可变元素,后面的行为将很难预料。

    NSMutableSet *setA = [NSMutableSet new];
    [setA addObject:arr1]; NSLog(@"setA: %@", setA);//{((1,2))}
        
    [setA addObject:arr2]; NSLog(@"setA: %@", setA);//{((1,2))}
        
    NSMutableArray *arr3 = [[NSMutableArray alloc] initWithObjects:@"1", nil];
    [setA addObject:arr3]; NSLog(@"setA: %@", setA);//{((1),(1,2))}
        
    [arr3 addObject:@"2"]; NSLog(@"setA: %@", setA);//{((1,2),(1,2))}
        
    NSSet *setB = [setA copy]; NSLog(@"setB: %@", setB);//{((1,2))}
    
    要点总结:

    "=="操作符比较的是两个指针本身,而不是所指的对象

    字符串比较时,isEqualToString用时优于isEqual

    数组NSArray和字典NSDictionary都有对应的比较方法:isEqualToArray、isEqualToDictionary

    “深度等同性判定”,指若对应位置上的对象均相等,那么两个就想等。比如若是从数据库读取数据,其中对象可能包含另外一个属性“唯一标识符”(主键,primary key)。那么,只需要根据它判断等同性。

    • 若想检测对象间的等同性,请提供 “isEqual:” 或 hash 方法。
    • 相同的对象具有相同的哈希码(hash值),但具有相同哈希码(hash值)的对象不一定相等。
    • 不要盲目的逐个检测每条属性,而应该按照具体情况来制定检测方案。如:两个对象是否具有“唯一标识符”。
    • 编写hash算法时,应使用 计算速度快 但 哈希值碰撞几率低 的算法。
    提问:
    1. 平时使用等同性判定有什么细节?

    2. 什么情况下需要使用自定义等同性判定?

    3. 有哪些等同性判定的方法,如何优化自定义?

       .1 isEqualToString等方法,注意OC语言特性,在编译期不做强类型检查(strong type checking)
       .2 等同判定频繁,且比较的属性(或对持有对象)等比较多
       .3 可以判断特征值(如唯一标识符),也可以比较生成hash值
      

    9. 以 “类族模式” 实现隐藏细节

    工厂模式 是 创建类族 的方法之一。如下:

    1). 抽象父类 abstract class

    .h

    //
    //  EOCEmployee.h
    //  Copyright © 2018 Mr. Wang. All rights reserved.
    //
    
    #import <Foundation/Foundation.h>
    
    typedef NS_ENUM(NSUInteger, EOCEmployeeType) {
        EOCEmployeeTypeDeveloper,
        EOCEmployeeTypeDesigner,
        EOCEmployeeTypeFinance
    };
    
    /** abstract class:抽象子类 */
    
    @interface EOCEmployee : NSObject
    
    @property (copy) NSString *name;
    
    @property (assign) NSUInteger salary;
    
    
    + (EOCEmployee *)employeeWithType:(EOCEmployeeType)employeeType;
    
    - (void)doADaysWork;
    
    @end
    

    .m

    //
    //  EOCEmployee.m
    //  Copyright © 2018 Mr. Wang. All rights reserved.
    //
    
    #import "EOCEmployee.h"
    
    #import "EOCEmployeeDeveloper.h"
    
    @implementation EOCEmployee
    
    + (EOCEmployee *)employeeWithType:(EOCEmployeeType)employeeType {
        
        switch (employeeType) {
            case EOCEmployeeTypeDeveloper:
                return [EOCEmployeeDeveloper new];
                break;
            case EOCEmployeeTypeDesigner:
                return [EOCEmployeeDesigner new];
                break;
            case EOCEmployeeTypeFinance:
                return [EOCEmployeeFinance new];
                break;
        }
        
    }
    
    - (void)doADaysWork {
        //sub method
    }
    
    

    2). 实现子类:concrete class

    .h

    //
    //  EOCEmployeeDeveloper.h
    //  Copyright © 2018 Mr. Wang. All rights reserved.
    //
    
    #import "EOCEmployee.h"
    
    /** concrete class:实体子类 */
    
    @interface EOCEmployeeDeveloper : EOCEmployee
    
    @end
    

    .m

    //
    //  EOCEmployeeDeveloper.m
    //  Copyright © 2018 Mr. Wang. All rights reserved.
    //
    
    #import "EOCEmployeeDeveloper.h"
    
    @implementation EOCEmployeeDeveloper
    
    - (void)doADaysWork {
        if (self.name && self.salary) {
            NSLog(@"%@做了一天的开发者,得到了%lu钱", self.name, (unsigned long)self.salary);
        }else {
            NSLog(@"我做了一天的开发者");
        }
        
    }
    
    @end
    

    3). 调用

    /*
     实现: 在公司有个搞开发的,叫Samara,工资时200元,输出他一天的工作绩效。
     */
    EOCEmployee *aEmployee = [EOCEmployee employeeWithType:EOCEmployeeTypeDeveloper];
    aEmployee.name = @"Samara";
    aEmployee.salary = 200;
    [aEmployee doADaysWork];
    
    为类族增加新的子类。需要遵守:
    1. 子类应该 继承自类族中的抽象基类。

    2. 子类应该 定义自己的存储格式。

       超类本身只不过是抱在其它隐藏对象外面的壳,仅仅定义了所有类都要具备的一些接口。
      
    3. 子类应该 覆写超类文档中指明需要覆写的方法。

       具体写法,根据文档
      
    要点总结:
    • 类族模式可以把 实现细节隐藏 在一套简单的 公用接口后面
    • 系统架构 中经常使用类族。
    • 从类族的公共抽象类中继承子类时要当心,若有开发文档,应先读。

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

    存储策略
    1. 对象关联类型
      |关联类型|等效的 @property 属性
      |:--|:--
      |OBJC_ASSOCIATION_ASSIGN| assign
      |OBJC_ASSOCIATION_RETAIN_NONATOMIC|nonatomic, retain
      |OBJC_ASSOCIATION_COPY_NONATOMIC| nonatomic, copy
      |OBJC_ASSOCIATION_RETAIN| retain
      |OBJC_ASSOCIATION_COPY|copy

    2. 管理关联对象

      1. void objc_setAssociatedObject** (id object, void *key, id value, objc_AssociationPolicy policy)

      此方法 以给定的键和策略 为某对象设置关联对象值。

      1. id objc_getAssociatedObject**(id object, void *key)

      此方法 根据给定的键从某对象中 获取相应的关联对象值。

      1. void objc_removeAssociatedObjects**(id object)

      此方法 移除指定对象的全部关联对象。

    使用场景

    在分类中使用runtime动态添加属性

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

    11. 理解 objc_msgSend 的作用(对象的消息传递机制)

    信息的调用过程

    • objc_msgSend
    //给对象发送消息 OC中
    id returnValue = [someObject messageName:parameter];
    
    //编译器看到此消息后,将其转换为一条标准的C语言函数调用,所用的函数乃是消息传递机制中的核心函数,叫做objc_msgSend,其“原形”(property)如下:
    void objc_msgSend(id self, SEL cmd, ...) //参数可变函数
    //即:
    id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);
    

    信息调用过程的应对其它“边界情况”的,Objective-C运行环境中的一些函数:

    • objc_msgSend_stret

      若,待发送的消息要返回 结构体,则可交由此函数处理。

    • objc_msgSend_fpret

      若,带发送的消息要返回 浮点数,则可交由此函数处理。

    • objc_msgSendSuper

      若,要给超类发送消息,则可交由此函数处理。如 [super message:parameter];

    要点
    • 消息由接收者选择子参数构成。给某对象发送消息(invoke a message) 也就相当于在该对象上“调用方法”(call a method)。
    • 发送给某对象的全部消息都由 “动态消息派发系统”(dynamic message dispatch system) 来处理:查出对应的方法,执行其代码。

    12. 理解消息转发机制

    对象在接受到消息后,可能可以解读,也可能无法解读。

    动态方法解析 第一次机会

    在无法解决时,调用

    + (BOOL)resolveInstanceMethod:(SEL)sel;//处理实例方法
    //或
    + (BOOL)resolveClassMethod:(SEL)sel;//处理类方法
    

    该方法的参数是那个未知的选择子,其返回值是Boolean类型,表示这个类能否新增一个实例方法 以处理此选择子。

    备援接收者 第二次机会
    - (id)forwardingTargetForSelector:(SEL)selector;
    
    完整的消息转发 第三次机会
    - (void)forwardInvocation:(NSInvocation *)invocation;
    

    首先创建NSInvocation对象,把尚未处理的那条消息有关的全部细节都封于其中。此对象包括选择子、目标、及参数。在触发NSInvocation对象是,“消息派发系统”将亲自出马,把消息指派给目标对象。

    最后

    在实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法。由此,继承体系里的每个类都有机会处理此调用请求,直至NSObject。若调用到了NSObject类的方法,该方法还会继而调用“doesNotRecognizeSelector:”以抛出异常,表明选择子最终未能得到处理。

    消息转发全流程图

    要点总结

    • 若对象无法响应某个选择子,则进入 消息转发流程
    • 通过运行期的动态方法解析功能,方法可以在 用到时再添加 到该类中。
    • 对象可以把无法解析的选择子 转交给其它对象处理
    • 若经过上述两步还 未解析成功 选择子,那么 启用完整的消息转发机制

    13. 用“方法调配技术”调试“黑盒方法”

    /*
     class_getClassMethod: 获取类方法
     class_getInstanceMethod: 获取对象方法
     */
    
    //获取要交换的两个目标方法
    Method methodA = class_getInstanceMethod([NSString class], @selector(lowercaseString));
    Method methodB = class_getInstanceMethod([NSString class], @selector(uppercaseString));
        
    //交换
    method_exchangeImplementations(methodA, methodB);
    
    /*** 测试 ***/
    NSString *string = @"Samara";
        NSLog(@"原小写%@_原大写:%@", [string lowercaseString], [string uppercaseString]);
    
    //添加新功能
    /** 只要用到了某类,就会调用该类的load 方法 */
    + (void)load {
        //获取要交换的两个目标方法
        Method methodA = class_getInstanceMethod([NSString class], @selector(lowercaseString));
        Method methodB = class_getInstanceMethod([NSString class], @selector(eoc_lowercaseString));
        
        //交换
        method_exchangeImplementations(methodA, methodB);
    }
    - (NSString *)eoc_lowercaseString {
        
        NSString *str = [self eoc_lowercaseString];
        
        return [NSString stringWithFormat:@"Ori:%@_rst:%@",self, str];
    }
    
    //调用
    NSString *string = @"Samara";
    NSLog(@"%@", [string lowercaseString]);
    //输出结果
    2018-11-07 13:39:17.768666+0800 EffectiveOC[7729:423305] Ori:Samara_rst:samara
    
    要点总结:
    • 在运行期,可以向类中 新增替换 选择子所对应的方法实现。
    • 使用另一份实现来替换原有的方法实现,这道工序叫做方法调配,开发者常用此技术向原有实现中添加新功能。
    • 一般来说,只有在调试程序的时候才需要在运行期修改方法,不宜滥用

    可用来开发微信抢红包、外挂等作弊功能

    14. 理解“类对象”的用意

    Class 对象在运行期头文件(objc/runtime.h)中的状态:

    struct objc_class {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    
    #if !__OBJC2__
        Class _Nullable super_class                              OBJC2_UNAVAILABLE;
        const char * _Nonnull name                               OBJC2_UNAVAILABLE;
        long version                                             OBJC2_UNAVAILABLE;
        long info                                                OBJC2_UNAVAILABLE;
        long instance_size                                       OBJC2_UNAVAILABLE;
        struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
        struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
        struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
        struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
    #endif
    
    } OBJC2_UNAVAILABLE;
    /* Use `Class` instead of `struct objc_class *` */
    
    要点总结
    • 每个实例都有一个指向 Class 对象的指针,用以表明其类型,而这些 Class 对象则构成了类的继承关系。
    • 如果对象类型无法在编译期确定,那么就应该使用 类型信息查询方法 查询。
    • 尽量使用 类型信息查询方法 确定对象类型,而不要直接比对对象类型,因为某些对象可能实现了 消息转发 功能。

    系列文章

    相关文章

      网友评论

        本文标题:《Effective Objective-C 2.0编写高质量i

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