iOS开发iOS移动开发社区上海快风信息科技有限公司

iOS 开发 -《Effective Objective-C 2

2018-01-16  本文已影响151人  Q以梦为马

《Effective Objective-C 2.0: 编写高质量 iOS 与 OS X 代码的 52 个有效方法》是一本非常经典的 OC 书籍。这本书从语法、接口与 API 设计、内存管理、框架等 7 大方面总结和探讨了 OC 编程中的 52 个特性与陷阱,很值得读。

文章共分为三篇:

第一篇:iOS 开发 -《Effective Objective-C 2.0:编写高质量 iOS 与 OS X 代码的 52 个有效方法》读书笔记(1)
第二篇:iOS 开发 -《Effective Objective-C 2.0:编写高质量 iOS 与 OS X 代码的 52 个有效方法》读书笔记(2)
第三篇:iOS 开发 -《Effective Objective-C 2.0:编写高质量 iOS 与 OS X 代码的 52 个有效方法》读书笔记(3)

文章目录:

第 1 章:熟悉 OC
第 2 章:对象、消息、运行期
第 3 章:接口与 API 设计
第 4 章:协议与分类
第 5 章:内存管理
第 6 章:块与大中枢派发(block 与 GCD)
第 7 章:系统框架

第 1 章:熟悉 OC

第 1 条 - 在类的头文件中尽量少引入其他头文件

OC 中应尽量避免在头文件中引用其他类,即比如我们有两个类,NNListViewController 以及 NNHomeViewController,应避免在 NNListViewController.h 中引用 NNHomeViewController,如果非要在 NNListViewController.h 中引入 NNHomeViewController,如下面代码:

#import <UIKit/UIKit.h>

@interface NNListViewController : UIViewController
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *age;
@property (nonatomic, strong) NNHomeViewController *homeVC;
@end

这时程序会报错,因为 NNHomeViewController 类并不可见,这时常见的做法是在 NNListViewController.h 中加入下面这行代码

#import "NNHomeViewController.h"

这种方法可行,但是不够优雅。在编译 NNListViewController 类时,不需要知道 NNHomeViewController 类的全部细节,只需要知道有一个类名叫 NNHomeViewController 就好,所以我们应该这样写:

@class NNHomeViewController;

这叫做“向前声明”该类。现在 NNListViewController.h 变成了这样:

#import <UIKit/UIKit.h>

@class NNHomeViewController;

@interface NNListViewController : UIViewController
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *age;
@property (nonatomic, strong) NNHomeViewController *homeVC;
@end

NNListViewController 的实现文件需要引入 NNHomeViewController.h,因为若要使用后者,就必须知道其所有接口细节。于是 NNListViewController.m 就是:

#import "NNListViewController.h"
#import "NNHomeViewController.h"

@implementation NNListViewController

@end

第 2 条 - 多用字面量语法,少用与之等价的方法

编写 OC 代码总会用到几个类,NSStringNSArrayNSNumberNSDictionary,从类名上即可看出各自所表达的数据结构。NSString 对象有一种简单的创建方式,叫做“字符串字面量”,其语法如下:

NSString string = @"Liu Zhong Ning";

如果不用这种语法,就要以常见的allocinit 方法来分配并初始化 NSString 对象了。同样也能用字面量语法声明 NSArrayNSNumberNSDictionary 类的实例。使用字面量语法可以缩减代码长度,使其更为易读。

    // NSNumber
    NSNumber *intNumber = [NSNumber numberWithInt:1];
    NSNumber *floatNumber = [NSNumber numberWithFloat:6.6f];
    NSNumber *charNumber = [NSNumber numberWithChar:'a'];

    // NSArray
    NSArray *animals = [NSArray arrayWithObjects:@"cat", @"dog", nil];
    // 取下标
    NSString *dog = [animals objectAtIndex:1];

    // NSDictionary
    NSDictionary *personDic = [NSDictionary dictionaryWithObjectsAndKeys:@"Liu Zhong Ning", @"name", 25, @"age", nil];
    // 访问字典
    NSString *name = [personDic objectForKey:@"name"];
    // NSNumber
    NSNumber *intNumber = @1;
    NSNumber *floatNumber = @6.6f;
    NSNumber *charNumber = @'a';

    // NSArray
    NSArray *animals = @[@"cat", @"dog"];
    // 取下标
    NSString *dog = animals[1];

    // NSDictionary
    NSDictionary *personDic = @{@"name" : @"Liu Zhong Ning",
                                @"age" : @25};
    // 访问字典
    NSString *name = personDic[@"name"];

第 3 条 - 多用类型常量,少用 #define 预处理指令

#define ANIMATION_DURATION 0.3

用预处理指令也可以达到我们想要的效果,但这样定义出来的常量 没有类型信息
另外,预处理过程会把碰到的所有 ANIMATION_DURATION 换成 0.3,这样的话,假设此指令声明在某头文件,那么 所有引入了这个头文件的代码,其ANIMATION_DURATION 都会被替换。有个办法比用预处理指令来定义常量更好:

static const NSTimeInterval kAnimationDuration = 0.3;

可以看出,上面方式定义的常量包含类型信息,可知该常量类型为 NSTimeInterval,这种方式能令阅读代码的人更易理解其意图。另外还应注意常量名称:若常量只用在实现文件,则在前面加字母 k;若常量在类之外可见,则通常以类名为前缀。

变量一定要同时用 staticconst 来声明。如果试图修改由 const 修饰符所声明的变量,那么编译器就会报错。而 static 修饰符意味着该变量仅在定义此变量的实现文件中可见。如果不加 static,则编译器会为它创建一个“外部符号”,此时若另一个类中也声明了同名变量,那么编译器就会抛出一条错误消息。

第 4 条 - 用枚举表示状态、选项、状态码

比如UITableView 中的 UITableViewStyle 就是一个枚举:

typedef NS_ENUM(NSInteger, UITableViewStyle) {
    UITableViewStylePlain,          // regular table view
    UITableViewStyleGrouped         // preferences style table view
};

由于每种状态都用一个便于理解的值来表示,所以这样写出来的代码更易读懂。编译器会为枚举分配一个独有的编号,从 0 开始,每个枚举递增 1

    // 定义
    typedef enum : NSUInteger {
        NNAccountStateInitial = 0,        // 初始
        NNAccountStateWaitReviewed,       // 待审核
        NNAccountStatePassed,             // 已通过
        NNAccountStateNoPassed,           // 未通过
        NNAccountStateDeactivated,        // 停用
        NNAccountStateFreeze,             // 冻结
    } NNAccountState;

    // 使用
    switch (self.accountState) {
        case NNAccountStateInitial:
            
            break;
        case NNAccountStateWaitReviewed:
            
            break;
        case NNAccountStatePassed:
            
            break;
        case NNAccountStateNoPassed:
            
            break;
        case NNAccountStateDeactivated:
            
            break;
        case NNAccountStateFreeze:
            
            break;
    }

第 2 章:对象、消息、运行期

第 5 条 - 在对象内部尽量直接访问实例变量

请看下边这个类。

.h 文件

#import <UIKit/UIKit.h>

@interface NNListViewController : UIViewController

@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, copy) NSString *firstName;

- (NSString *)fullName;
- (void)setFullName:(NSString *)fullName;

@end

.m 文件

@implementation NNListViewController

- (void)viewDidLoad {
    [super viewDidLoad];
}

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}

- (void)setFullName:(NSString *)fullName {
    NSArray *compoents = [fullName componentsSeparatedByString:@" "];
    self.firstName = [compoents objectAtIndex:0];
    self.firstName = [compoents objectAtIndex:1];
}

@end

在 fullName 的获取方法与设置方法中,我们使用点语法,通过存取方法来访问相关的实例变量。现在假设重写这两个方法,不经由存取方法,而是直接访问实例变量:

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@", _firstName, _lastName];
}

- (void)setFullName:(NSString *)fullName {
    NSArray *compoents = [fullName componentsSeparatedByString:@" "];
    _firstName = [compoents objectAtIndex:0];
    _firstName = [compoents objectAtIndex:1];
}

第 6 条 - 理解“对象等同性”这一概念

根据“等同性”来比较对象是一个非常有用的功能,请看下面这段代码,并思考会输出什么:

    NSString *foo = @"Badger 123";
    NSString *bar = [NSString stringWithFormat:@"Badger %d", 123];
    BOOL equalA = (foo == bar);
    BOOL equalB = [foo isEqual:bar];
    BOOL equalC = [foo isEqualToString:bar];
    NSLog(@"equalA = %d, equalB = %d, equalC = %d", equalA, equalB, equalC);

这里是输出结果:equalA = 0, equalB = 1, equalC = 1

大家可以看到 == 与等同性判断方法之间的差别(== 操作符只是比较了两个指针,而不是指针所指的对象),NSString 实现了一个自己独有的判断方法,isEqualToString。调用该方法比调用 isEqual 方法快,后者还要执行额外的步骤,因此它不知道受测对象的类型。

第 7 条 - 以“类族模式”隐藏实现细节

“类族”是一种很有用的模式,可以隐藏“抽象基类”背后的实现细节。OC 的系统框架中普遍使用此模式。比如创建 UIButton 时需要调用下面这个方法:

+ (instancetype)buttonWithType:(UIButtonType)buttonType;

该方法所返回的对象,其类型取决于传入的按钮类型,然而,不管返回什么类型的对象,它们都继承自同一个基类:UIButton。这么做的意义在于:UIButton 类的使用者无须关心创建出来的按钮具体属于哪个子类,也不用考虑按钮的绘制方式等实现细节。

.h 文件

#import <UIKit/UIKit.h>

typedef enum : NSUInteger {
    NNAccountStateInitial = 0,        // 初始
    NNAccountStateWaitReviewed,       // 待审核
    NNAccountStatePassed,             // 已通过
    NNAccountStateNoPassed,           // 未通过
    NNAccountStateDeactivated,        // 停用
    NNAccountStateFreeze,             // 冻结
} NNAccountState;

@interface NNHomeViewController : UIViewController

+ (NNHomeViewController *)accountWithState:(NNAccountState)state;

@end

.m 文件

+ (NNHomeViewController *)accountWithState:(NNAccountState)state {
    switch (state) {
        case NNAccountStateInitial:
            // 做操作
            return [[NNHomeViewController alloc] init];
            break;
        case NNAccountStateWaitReviewed:
            // 做操作
            return [[NNHomeViewController alloc] init];
            break;
        case NNAccountStatePassed:
            // 做操作
            return [[NNHomeViewController alloc] init];
            break;
        case NNAccountStateNoPassed:
            // 做操作
            return [[NNHomeViewController alloc] init];
            break;
        case NNAccountStateDeactivated:
            // 做操作
            return [[NNHomeViewController alloc] init];
            break;
        case NNAccountStateFreeze:
            // 做操作
            return [[NNHomeViewController alloc] init];
            break;
    }
}
    id maybeAnArray;
    // 可以对 maybeAnArray 赋任何值
    if ([maybeAnArray class] == [NSArray class]) { // 判断永远不会为真
        // 不会执行
    }

解析代码:NSArray 是个类族,[maybeAnArray class] 所返回的绝不可能是 NSArray 本身,因为由 NSArray 的初始化方法所返回的那个实例其类型是隐藏在类族公共接口后面的某个内部类型。
不过仍然有办法可以判断出某个实例所属的类是否位于类族之中。若想判断某对象是否位于类族中,不要直接检测两个类对象是否相同,而应该采用下列代码:

    id maybeAnArray;
    // 对 maybeAnArray 赋任何值
    if ([maybeAnArray isKindOfClass:[NSArray class]]) {
        // 会执行
    }

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

在 OC 中可以通过 Category 给一个现有的类添加属性,但却不能添加实例变量,这似乎成为了 OC 的一个短板。值得庆幸的是,OC 中有一项强大的特性可以解决此问题,这就是关联对象(Associated Object)

    // 需要导入头文件
    #import <objc/runtime.h>

    // 根据给定的键和策略为某对象设置关联对象值
    OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
    // 根据给定的键从某对象获取相应的关联对象值
    OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key);
    // 移除指定对象的全部关联对象
    OBJC_EXPORT void objc_removeAssociatedObjects(id object);
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

关联对象有很多细节很多坑需要注意,想了解更多关联对象的童鞋请看这篇文章:Objective-C Associated Objects 的实现原理。

第 9 条:理解 objc_msgSend 的作用

在 OC 中,如果向某个对象传递消息,那就会在运行时使用动态绑定(dynamic binding)机制来决定需要调用的方法。但是到了底层具体实现,却是普通的C语言函数实现的。这个实现的函数就是objc_msgSend,该函数定义如下:

void objc_msgSend(id self, SEL cmd, ...) 

第一个参数代表接收者,第二个参数代表选择子SEL是选择子的类型,选择子即方法名字),后续参数就是消息中的那些参数,其顺序不变。

id returnValue = [someObject messageName:parameter]; 

代码解析:objc_msgSend 函数会依据接收者与选择子的类型来调用适当的方法。函数首先会在接收者所属的类中搜寻其方法列表,如果能找到这个跟选择子名称相同的方法,就跳转至其实现代码。若是当前类没找到,那就沿着继承体系向上查找,等找到合适方法之后再跳转 ,如果最终还是找不到,那就进入消息转发的流程去进行处理了。

第 10 条:理解消息转发机制

当对象接收到无法解读的消息后,就会启动“消息转发”(message forwarding)机制,我们可以通过代码在消息转发的过程中告诉对象应该如何处理未知的消息,系统默认是抛出异常,控制台给出提示代码如下:

unrecognized selector sent to instance 0x7f8c8a70a380

消息转发机制所讲的就是在抛出异常之前也就是消息转发过程中经过的一些步骤。

消息转发机制分为两个阶段:

第 11 条:用“方法调配技术”调试“黑盒方法”

这条讲的主要内容就是方法调配( Method Swizzling),通过运行时用另外一种方法实现来替换掉原有的方法实现,往往被应用在向原有实现中添加新功能,打印信息等。

class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)

首先我们写一个分类,比如为NSString写一个“分类”(category)

#import "NSString+extension.h"
#import <objc/runtime.h>

@implementation NSString (extension)

+ (void)load {
    Method oldMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
    Method newMethod = class_getInstanceMethod([NSString class], @selector(new_lowercaseString));
    method_exchangeImplementations(oldMethod, newMethod);
}

- (NSString *)new_lowercaseString {
    NSString *lowerString = [self new_myLowercaseString];
    NSLog(@"%@ - %@", self, lowerString);
    return lowerString;
}

@end

解析代码:+ (void)load中会调换lowercaseString方法与new_lowercaseString方法;在方法- (NSString *)new_lowercaseString中,[self new_myLowercaseString],我们调用new_myLowercaseString方法,此时不会引起死循环,因为这个方法是与lowercaseString方法调换的。

此时我们在NSString实例中调用lowercaseString方法:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSString *string = @"Talk is cheap, Show me the code.";
    NSString *lowercaseString = [string lowercaseString];
    NSLog(@"%@", lowercaseString);
}

打印效果图如下:


打印效果图

通过此方案,开发者可以为那些“完全不知道其具体实现的”(completely opaque,“完全不透明的”)黑盒方法增加日志记录功能,这非常有助于程序调试。然而,此做法只在调试程序时有用。很少有人在调试程序之外的场合用上述“方法调配技术”来永久改动某个类的功能。不能仅仅因为OC语言里有这个特性就一定要用它。若是滥用,反而会令代码变得不易读懂且难于维护。

第 12 条:理解“类对象”的用意

类是一个对象,是Class 类型的对象,简称“类对象”。

typedef struct objc_class *Class; 

可以用类型信息查询方法来检视类继承体系。“isMemberOfClass:”能够判断出对象是否为某个特定类的实例,而“isKindOfClass:”则能够判断出对象是否为某类或其派生类的实例。例如:

NSMutableDictionary *dict = [NSMutableDictionary new];  
[dict isMemberOfClass:[NSDictionary class]]; ///< NO 
[dict isMemberOfClass:[NSMutableDictionary class]]; ///< YES 
[dict isKindOfClass:[NSDictionary class]]; ///< YES 
[dict isKindOfClass:[NSArray class]]; ///< NO 

比较类对象是否等同的办法来判断,使用==操作符,而不要使用比较OC对象时常用的“isEqual:”方法。原因在于,类对象是“单例”(singleton),在应用程序范围内,每个类的Class仅有一个实例。



下一篇:iOS 开发 -《Effective Objective-C 2.0:编写高质量 iOS 与 OS X 代码的 52 个有效方法》读书笔记(2)

上一篇下一篇

猜你喜欢

热点阅读