Effective-Objective-C Summary

<Effective Objective-C> 总结

Item 1 Familiarize Yourself with Objective-C’s Roots

  • 当你声明一个变量如 NSString *string = @“Hello world” 的时候 永远记住,在OC里 所有的对象( 有一些block 对象分配在栈??)都是分配在heap上的。 这个语句的作用是这样的 ,在stack 上有一个NSString型的指针 指向heap区的NSString object(即"Hello world")。这是很神奇的,对于比较熟悉c/cpp语言的程序员来说,因为我们知道任何在函数内部的局部变量 都是声明在stack上的,当程序执行离开函数的时候,stack会被释放,所有的局部变量都会失效。 而OC能够这么做的原因,是因为有ARC(Automatic Reference Count)也就是我们所常说的垃圾回收。

Item 2: Minimize Importing Headers in Headers

  • 尽可能少在.h文件里面引用别的.h文件,这会给compiling 带来极大的不必要的负担,因为当别的文件需要include 这个头文件的时候 会递归地#import 在这个头文件里面的内容。 一个好的做法是 尽可能在.m文件里引入必要的.h文件 和使用forward declaration。

Item 4: Prefer Typed Constants to Preprocessor

  • 尽可能用静态全局变量代替#define 来定义constant。这样做的好处是能够提供一定的type information,当被错误使用时 compiler会报错。并且声明在.m文件内的static const var 用k作为变量名前缀,如果需要被外界引用的const var则用class name 作为前缀
1
2
3
4
5
//In the header file
extern NSString *const EOCStringConstant

// In the implementation file
NSString *const EOCStringConstant = @"VALUE";

Item 5: Use Enumerations for States,Options, and Status Codes

  • 继承于cpp11的 enum 语法,是一种很实用的技巧。

Item 6: Understand Properties

  • Why not declare instance variable directly?
    • 如果直接声明实例变量的话,对象布局就会在编译器形成。我们如果在runtime对对象布局进行更改的话,就会导致访问实例变量发生错误。
    • 使用property的话,则会由class object 来持有offset,当runtime 添加新的实例变量的时候,就会更新class object持有的offset,因此我们就可以在runtime的时候动态添加新的property。
    • property的内存处理关键词
      • assign :setter 是简单的赋值操作,常被用于scalar value 例如CGFloat or NSInteger
      • strong :简单理解就是 当遇到持有操作的时候就会让 refcount ++;
      • weak : 简单理解就是 当遇到持有操作的时候 不会refcount ++; 但是一旦指向的对象被dealloc之后,它的值就会设置成nil。总所周知,向nil 发送消息并不会导致错误! 这是一个奇怪的设定
      • unsafe_unretained : 类似于assign 不过针对的类型是object type。 和weak一样 持有操作不会refcount++, 和weak不一样的是 当指向的对象被dealloc之后,它的值仍然指向那个地址。这意味着访问一个错误的地址 ,会导致出错!
      • copy : 和strong差不多,不过和strong不一样的地方在于当持有操作时,不是refcount ++,而是将整一个对象copy一遍, 你可以认为现在在堆里有两个一模一样的对象,一个是原来的对象持有,另一个被copy对象持有。

Item 7: Access Instance Variables Primarily Directly When Accessing Them Internally

  • 作者建议当你需要读一个Instance Variable的时候 最好使用_Ivar的方法(由于不用走方法转发 会快一点),而当你需要设置Instance Variable的时候 使用property的方法去访问。
    • 使用_Ivar的时候,不会触发内存管理,也就是说如果使用了copy关键字的话,也不会在heap上malloc 一个新的对象,而是在旧的对象上refcount++
    • 使用_Ivar的时候,不会触发KVO通知
    • 当使用lazy load的时候 只能使用property 来访问
    • 在initial和dealloc的时候都通过_Ivar来访问

Item 8: Understand Object Equality

  • 使用== 进行比较的时候,只会对pointer的值进行比较。

  • 一般来说 为自己的class声明类似于isEqual方法是一种好的code style,(例如NSString的isEqualString 方法),可以减少type checking的开销(对比继承于NSObject的isEqual)

  • 相等的object 一定返回相同的hash值,而hash值相同的object 不一定相等

  • set会根据object的hash value来确认该object是否在set里是唯一的。

  • 当你实现对象的比较方法之后,最好override 父类的isEqual方法。

  • 最好不要往set里面添加mutable 对象,不然会引发奇奇怪怪的bug

Item 9: Use the Class Cluster Pattern to Hide Implementation Detail

这就是常说的cluster pattern吧,就是对外只暴露基类的interface,然后根据你的需求创建不同的子类(一般来说是一个class method),是一种经常使用的设计模式。

Item 10: Use Associated Objects to Attach Custom Data to Existing Classes

Item 11: Understand the Role of objc_msgSend

    • 不同于静态语言的函数调用,在Objective-C 里面我们称(function call )为,给一个对象发送一条消息(Message Passing)。在C语言中,函数的调用是在编译阶段就已经确定下来的,而在Objective-C中 ,给对象发送一条消息则是在runtime的时候决定的(Dynamic Binding,Objective-C无法在编译期就确定给哪个receiver 发送哪个message, 也就是调用哪个函数),因为receiver很有可能无法响应你的消息,可能是它的superclass 来响应该消息/ 这条消息也有可能被转发给别的object 去处理(message forwarding)。

    • 1
      2
      3
      4
      
      //  in cpp
      object.method(para1,para2);
      // in Objective-C
      [receiver message];
      

上述描述可能听起来很绕口, 但确实是这样的。详细版解释是这样的,[receiver message] 本质上是一个函数调用(编译器会将这条消息传递翻译成objc_msgSend函数调用) 其函数原型是objc_msgSend(id self, SEL _cmd , …) ,但是问题就在于这里的self 和_cmd (分别对应的是receiver和message )可能和这里的[receiver message]不是同一个,(self 可能是receiver的superclass or super superclass) 在runtime中消息传递和转发的目的就是为了找到IMP

Item 12: Understand Message Forwarding

更新于 2.28 重新看了一遍 runtime 疯人院入院指南 感觉颇深,主要是对整个runtime的整个流程有了更加深刻的认识。 毕竟只学了一个月的Objective-C,所以刚开始的认识还是比较局限的。

首先疯人院day01 主要是对class/object的内存布局 进行了详细的解释,为第二天的消息传递(Message passing)和消息转发(Message forwarding)做准备。

Item 13: Consider Method Swizzling to Debug Opaque Methods

Item 29: Understand Reference Count

Item29主要讲述了MRC时代的一些关于内存的东西,现在我们普遍使用ARC,所以无需过于关心MRC的各种细节,有一点要注意的是,我之前一直对retain count 不太能够理解retain的意思,阅读了之后觉得翻译成“持有计数”就比较合适了,类似于reference count翻译成“引用计数”,讲道理在我看来没有啥区别。

还有一点就是了解了autorelease pool,在autorelease pool内的所有object 通常都会在下一次的event loop 中被释放掉。

Item 30: Use ARC to Make Reference Counting Easier

所谓ARC只不过是编译器帮你在适当的时候插入retain &&release 所以并没有什么特别高大上的操作,不过值得关心的是编译器是怎么知道(什么时候是一个合适的时机?)以及具体编译器是怎么去做这件事情的。

首先编译器在静态分析的时候(static analyzer)能够检测到内存存在泄漏的可能,在这个前提下,其实静态分析可以更进一步,比如说通过添加合适的retain 和releases 语句来解决这个问题,于是乎ARC就这么诞生了。就如刚才所说 ,ARC帮你在合适的地方添加内存管理的语句,因为如果这件事交给人来做的话 就是MRC,(程序员都喜欢偷懒,也有很大可能出错,所以这件事交给编译器来做 就很合适)所以现在的问题就是,“如何让编译器在合适的时机合适的地方插入合适的内存管理语句,并且你能完全确信编译器能做好这件事情”。

实际上ARC并不会直接调用 retain/release/autorelease/dealloc ,而是通过更低级C接口来实现内存的管理,这样做的原因是因为这些语句是被高频调用的,如果消息传递的方式去做的话,会有性能上的overhead (因为要走runtime的那一套消息流程),而直接用C接口来实现则简单许多,比如 对应retain的c接口为 objc_retain这个函数。 并且ARC还会适当优化不必要的retain和release,当它发现它这样做不影响程序的正确运行的前提下。感兴趣的可以看看objc_retainAutoreleasedReturnValue

  • 四种不同的qualifier

    • __strong : Default , the value is retained.
    • __unsafe_unretained : the value is not retained and potentially unsafe, as the object may have been deallocated already.
    • __weak : the value is not retained but is safe. it would automatically set to nil if it had been deallocated.
    • __autoreleasing : this qualifier is used when an object is passed by reference to a method.

ARC处理实例变量 :和普通的local var 不一样的是,ARC通过Objective-C++的一些feature(Deconstructor) 来对实例变量占用的内存来进行管理。简单一句话概括就是,ARC通过对对象的析构函数来对实例变量进行释放,而不需要有专门的dealloc函数。

ARC 只处理 Objective-C 的对象 对于非Objc对象(如CoreFunction的对象)内存管理是需要手动进行的。

  • Item 37 : Understand Blocks

    • 之前写过关于block底层的一篇文章深入理解block底层 ,简单的分析了block在底层到底是长什么样子的。block经常被用于一些需要异步处理的场景(也就是我们所说的call back),从这个的角度来看block不过就是一个函数指针,但是不同于函数指针在别的语言(c/c++)的实现,block有能力capture 他使用的context(这里的context指的是其传进去的参数和使用到的variable),而Objective-C 不同于c/cpp的地方在于其内存管理,所以导致block在capture context的时候,会对这些被captured的对象进行retain操作(refcount++),这就很容易导致了循环引用,造成内存泄漏。 还有一点就是block的语法比较怪异,需要一段时间去适应
1
2
3
4
5
6
// return_type (^block_name)(para1, para2)
int (^addBlock)(int a,int b) = ^(int a,int b){
    return a + b;
} // define a block variable 

int sum = addBlock(1,2);// Usage 
  • block可被用于遍历数组
1
2
3
4
5
6
7
8
NSArray *array = @[@0,@1,@2,@3,@4];
__block NSInteger count = 0;
[array 
    enumerateObjectsUsingBlock:^(NSNumber *number,NSUInteger idx,BOOL *stop){
        if([number compare:@2] == NSOrderedAscending) {
            count++;
        }
    }];

关于Block的内存布局 – (Need Update)

Item 38: Create typedefs for Common Block Types

其实对于很多刚学习Obective-C的新人来说,Block的语法都感觉让人看起来很是怪异,如

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/// 我们更常见的做法是声明一个 dispatch_block_t property;
@property (nonatomic,copy) dispatch_block_t block;

_block = ^{
    
};

/// 这是dispatch_block_t 被定义的地方
typedef void (^dispatch_block_t)(void);
/// 如果c语言基础不太好的同学可能不知道这个 typedef 干了什么
常见的typedef语法是 
typedef struct demo {
    int k;
} Demo;
 然后我们就可以直接使用 Demo *aDemo = malloc(sizeof(Demo));
 而不需要每次都 struct demo *aDemo = malloc(sizeof(struct demo));
 
 由于考虑到struct demo 基本上不会再使用 ,一个更简洁的写法是
 typedef struct {
    int k;
} Demo;

 如果有了解函数指针的话 
 我们知道函数指针的declaration是这样的int (*fp)(int a, int b);
typedef  int (*fp)(int a, int b);


int (^block_name)(BOOL para1,NSString *para2) = ^(BOOL para1, NSString *) {
    // do something nice
}

https://stackoverflow.com/questions/4295432/typedef-function-pointer 讲了如何typedef 一个函数指针

https://stackoverflow.com/questions/25716232/compare-blocks-and-functions-in-objective-c 讲了 block和函数指针的区别

Item 39 : Use Handler Blocks to Reduce Code Separation

Item 48 : Prefer Block Enumeration to for Loops

Item 49 : Use Toll-Free Bridging for collections with Custom Memory-Management Semantics

Item 50 : Use NSCache Instead of NSDictionary for Caches

Item 51 : Keep initialize and load Implementations Lean

Item 52: Remember that NSTimer Retains Its Target

血泪教训

0%