《Effective Objective-C 2.0编写高质量iOS与OS X代码的52个方法》

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

2019-01-22  本文已影响0人  Mr_WangZz

用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>
    指定设置方法的方法名。
    用法不常见。
要点

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

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

.1 直接访问实例变量的 速度比较快。
    这种方式,不经过Objective-C的“方法派发”(method dispatch,见11条),编译器所生成的代码会 直接访问 保存对象实例变量的那块 内存。
.2 直接访问实例变量时,不会调用其“设置方法”。
    绕过了为相关属性所定义的“内存管理语义”。
.3 直接访问实例变量,不会触发“键值观测”(KVO)通知。
    是否因此产生问题,取决于具体的对象行为。
.4 通过属性来访问,有助于排查与之相关的错误。
    因为,可以给“获取方法”或“设置方法”中新增断点(breakpoint),监控该属性的调用者及其访问时机。
要点

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)。那么,只需要根据它判断等同性。

提问:
  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动态添加属性

要点

11. 理解 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运行环境中的一些函数:

要点

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 *` */
要点总结

系列文章

上一篇下一篇

猜你喜欢

热点阅读