《Effective Objective-C 2.0 编写高质量

2019-03-01  本文已影响3人  熊梓君

第一章:熟悉 Objective-C

  1. OC 语言是使用“消息转发机制”而不是“函数调用机制”。主要区别就是:消息结构语言的代码都是在运行环境决定,而函数调用是由编译器决定。
  2. OC 创建的引用对象保存在“堆空间(heap space)”中,其变量是存在“栈”(stack)中。栈中的每个变量在32位计算机上是4字节;在64位计算机上是8字节,其内存的管理由系统自动清理,而堆上的内存管理可以由编程人员自己管理也可以由"引用计数"管理。

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

  1. 当我们在某个类中需要使用到另外一个类的时候,我们尽量避免直接 #import 导入头文件,而是使用“前象引用声明(forward declaring)”,这样可以优化编译器的编辑时间,同时也可以避免循环引用头文件以及降低类之间的耦合。
  2. 如果遇到无法使用前向引用声明的时候,比如类的协议,这时可以考虑将部分将协议的声明移到“分类”或者一个新文件中去,然后引入“分类”文件或者新文件,这样可以避免引入整个类文件。

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

  1. 字面量使得代码看上起更加的简洁、易懂。
  2. 同样我们应该使用取下标的方式访问和修改数组与字典的元素。

第4条:多用类型常量,少用 #define 预处理命令

  1. 预处理指令定义常量不包含类型信息,并且是在编译前执行操作和替换操作,编译耗时,且容易出错。
  2. 使用 const 来定义常量,如果只在类中使用,则使用 static声明,并且在实现文件中定义,不要在头文件中定义。
  3. 常量的命名,参考系统命名,最好使用类名做前缀,比如:UIApplicationDidBecomeActiveNotification 等。

第 5 条:用枚举表示状态、选项、状态码。

  1. 如果枚举的值可以组合使用,则可以用移位操作定义为2的幂,以便通过位运算(位与、位或)进行组合。
  2. 如果使用 switch 语法判断枚举,则尽量不使用 default 操作,这样的话,如果加入新的枚举,编辑器就会提示。
  3. NSEnumNS_Options 定义枚举类型可以指明底层的数据类型,确保不是编译器给出来的数据类型,它们2个本质上是一样的,只是字面上来讲,NS_Options 表示可位移操作的枚举。

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

  1. 属性(@Property)声明之后,会自动生成一个带下划线的实例变量,以及根据设置的读写属性,生成对应的“存取方法(getter/setter)”。
  2. 使用属性的点语法获取属性时,会调用 getter 方法,所以如果我们在对象内部,尽量直接使用实例变量。
  3. @synthesize 关键字可以指明,属性所对应的实例变量名称;@dynamic 关键字告诉编译器,不要自动生成 getter/setter 方法。
  4. 属性声明的 copy 语义,在 setter 方法中,会拷贝属性值,这样做可以避免,属性值在其它地方被修改。同时需要注意,如果是可变类型的属性(mutable),比如 @property(nonatomic, copy) NSMutableArray *array;按这种方式声明,使用 copy 之后,属性会变成一个不可变类型,所以最好重写 setter 方法,使用 mutableCopy 进行设置。
  5. nonatomicatomic声明属性是否具备“原子性”,一般我们在 iOS 程序上使用 nonatomic。因为 atomic 严重影响性能,而且还不能完全保证原子性。因为其原理只是在 setter 方法时加上同步锁,但是如果我们在一个线程中连续请求这个属性,另外一个线程修改这个属性,那么还是会出现获取的值不是修改的值,所以还是不能保证“线程安全”,如果需要实现线程安全,还需要进一步写代码;如果在 Mac OS X 上使用 atomic 则就没有性能瓶颈。
  6. weakunsafe_unretained 这2个关键字都表示不会 retain 增加自动引用计数。但是区别就是,当对象被销毁时,weak 会自动指向 nil,而 unsafe_unretained 则还是会指向之前的内存地址,所以很容易变成野指针。

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


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

- (BOOL)isEqual:(id)object {
    if(self == object) {
        // 首先判断 指针地址是否相等
        return YES;
    }
    // 再判断 object 是不是 NSString 类型
    if(![object isKindOfClass:[NSString class]]) {
        return NO;
    }
    // 最后再调用 isEqualToString:    
    return [self isEqualToString: object];
}

第9章:以“类族模式”隐藏实现细节


第10章:在即有类中使用关联对象存放自定义数据

OBJC_ASSOCIATION_ASSGIN:asign
OBJC_ASSOCIATION_RETAIN_NONATOMIC:strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC:copy, nonatomic
OBJC_ASSOCIATION_RETAIN:strong,atomic
OBJC_ASSOCIATION_COPY:copy, atomic

第11条:理解objc_msgSend 的作用


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

如果是对象会调用:
+ (BOOL)resolveInstanceMethod:(SEL)sel {}
如果是类方法则会调用:
+ (BOOL)resolveClassMethod:(SEL)sel {}

此时我们就可以在这个方法中,
使用 class_addMethod() 动态添加方法到对象或者类中。

(2)如果在(1)中没有发现有动态方法添加其中,则会进入“备援接受者”阶段,即看能不能转给其它接受者来处理:

在 - (id)forwardingTargetForSelector:(SEL)aSelector {} 方法中
返回一个对象,然后交由该对象执行该消息。

(3)如果再第(2)不也还是没有找到接受者,则消息派发系统通过 NSInvocation 对象进入完成的消息转发阶段:

// 进入该阶段之后,我们首先需要获取方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if(aSelector == @selector(count)) {
// Type Encodings:https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100-SW1
        return [NSMethodSignature signatureWithObjCTypes:"i@:"];
// 也可以自动获取 Type Encodings
//        Method method = class_getInstanceMethod(Model.class, aSelector);
//        const char *des = method_getTypeEncoding(method);
    }
    return nil;
}

// 然后通过 NSInvocation 转发给其它对象去实现(该阶段和第(2)阶段的区别就是我们可以转发给多个对象去实现)
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if(anInvocation.selector == @selector(count)) {
        [anInvocation invokeWithTarget:[Model1 new]];
        [anInvocation invokeWithTarget:[Model2 new]];
        // 虽然此处是分发给多个对象执行的,但是返回值是最后一个 invokeWithTarget 的对象对应方法的返回值。
    }
}

(4)如果在第(3)步还找不到消息接受者,则直接调用doesNotRecognizeSelector抛出异常。

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


第14条:理解类对象的用意


第15条:用前缀避免命名空间冲突


第16条:提供“全能初始化方法”


第17条:实现 description 方法


第18条:尽量使用不可变对象

@property (nonatomic, strong, readonly) NSString *name;
@interface Model ()
@property (nonatomic, strong, readwrite) NSString *name;
@end

第19条:使用清晰而协调的命名方式


第20条:为私有方法名加前缀

- (void)p_privateMethod {
}

第21条:理解 Objective-C 的错误模型


第22条:理解 NSCopying 协议

- (id)copyWithZone:(NSZone *)zone {
}

第23条:通过委托与数据源协议进行对象通信

# 常规情况:
if([delegate respondsToSelector: SEL]) {
  // 判断委托对象是否执行了该方法:
  ...
}
缺点:这种方式,除了第一次检测的结果有用之外,后续的检测都是多余的。

# 优化:
通过 C 语音的位段方式将检测结果缓存起来。
(1) 设置位段
struct {
  // 通过一个位段来表示 delegate 是否可以调用
  unsigned int delegateVia : 1;
} flag;

(2) 在 setDelegate 里面判断位段值
- (void)setDelegate:(id)delegate {
  _delegate = delegate;
  _flag.delegateVia = [delegate respondsToSelector: SEL];
}
(3) 判断是否可以调用
if(_flag.delegateVia) {
  [_delegate 调用方法];  
}

第24条:将类的实现代码分散到便于管理的数个分类中


第25条:总是给第三方类的分类名称加前缀


第26条:勿在分类中声明属性

1. 将属性声明为只读属性,同时实现其读取方法:
@property (nonatomic, strong, readonly) NSString *name;

- (NSString *)name {
    return @"....";
}
如果是这种方式还是建议直接将属性变成一个方法,而不是直接声明为属性。

2. 将属性声明为 @dynamic 在运行期提供存取方法
static NSString *_name;
@dynamic name;
- (NSString *)name {
    return _name;
}
- (void)setName:(NSString *)name {
    _name = name;
}

3. 使用关联对象实现存取方法
- (NSString *)phone {
   return objc_getAssociatedObject(self, "name");
}

- (void)setPhone:(NSString *)phone {
    objc_setAssociatedObject(self, "name", phone, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}


第27条:使用“class-continuation 分类”


第28条:通过协议提供匿名对象

例如字典:
[dic setObject:<#(nonnull id)#> forKey:(nonnull id<NSCopying>)]
字典中由于 key 在底层会调用 copy 方法,所以在调用这个方法的时候,
就将 key 声明为了一个匿名对象,这个对象只要遵循 NSCopying 协议即可。


第29条:理解引用计数


第30条:以 ARC 简化引用计数


第31条:在 dealloc 方法中只释放引用并解除监听


第32条:编写“异常安全代码”时,留意内存管理问题


第33条:以弱引用避免保留环


第34条:以“自动释放池块”降低内存峰值


第35条:用“僵尸对象”调试内存管理问题


第36条:不要使用 retainCount


第37条:理解 Block

1. _NSConcreteStackBlock 保存在栈中的 block,我们创建 blcok 时默认就是在栈中。
但是在 ARC 下就不存在这种模式,但如果在 MRC 下如果我们创建好 block 之后,没有移到堆中,
很可能我们在使用的时候回崩溃(如下)。
- (NSArray *)blockArray {
    int num = 123;
    return @[^{ NSLog(@"this is block 0"); },
             ^{ NSLog(@"this is block 1:%i", num); },
              ^{ NSLog(@"this is block 2:%i", num); }];
}
- (void)test {
    NSArray *array = [self blockArray];
    void(^someblock)(void) = array.lastObject;
    someblock();
}
如果我们在 MRC 调用 test() 方法时,就会崩溃。
因为栈是由系统管理的,所以在我们调用 block 时,已经被销毁了。

2.  _NSConcreteMallocBlock 保存在堆中的 block,当引用计数为 0 时会被销毁。
(1)如果是在 ARC 下,如果 block 捕获了外部的局部变量则也会保存在堆中;
(2)在 MRC 和 ARC 下如果当在栈中的 block 执行了 `_Block_copy` 函数也会保存在堆中
    以下方式会被拷贝:
      a. 显示调用 copy 方法
      b. 赋值给一个用 copy 修饰的 blcok 属性时:
        关于这一点也就说明我们为什么在类里面声明一个 blcok 时要使用 @property (nonatomic, copy)。因为如果不使用 copy ,block 还保存在栈上的,这是很可能被系统回收了,但是在 ARC 下已经不存在,使用 @property (nonatomic, strong) 也是一样的效果。
      c. 在ARC下,向函数或者方法传递Block时(MRC下需要手动copy)
      d. 调用Coaca框架中方法名中含有usingBlock的方法时
      e. 调用GCD的API时

3. _NSConcreteGlobalBlock 全局的静态 block,不会访问任何外部变量
(1)在 MRC 和 ARC 下一个定义在外部的 block 就是一个全局的 block
(2)在 ARC 下如果是一个没有捕获任何外部变量的 block 也是一个全局 block

(2)使用关键字的时候,block 只是复制其引用地址来进行访问的


block-capture-2.jpg

第38条:为常用的块类型创建 typedef

typedef  return_type (^block_name)(parameters);


第39条:用 handle 块降低代码的分散程度


第40条:使用block时,不要出现保留环


书签:154页

上一篇 下一篇

猜你喜欢

热点阅读