💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# Effective Objective-C 2.0 Tips 总结 Chapter 1 & Chapter 2 下面只是对读到的所有 Tips 结合我平时开发中遇到的问题进行总结,每一个 Tips 和书中的每一条对应,本文的目的是去掉书中的大部分讨论的内容,让人能够马上使用这些 Tips,建议阅读过原书后食用更佳。 ## Chapter 1 熟悉 Objective-C - Tips 1 Objective-C 的起源 - Objective-C 是从 C 语言演化而来,有 C 的一些基础会有很大帮助 - Tips 2 头文件中减少引用 - 减少在类的头文件中 import 其他头文件,如果使用其他类,那么使用`@class ClassName;`来进行**Forward Declaring** - 对于协议,每个协议放到对应的头文件,使用时候引用 - 对于委托协议(比如 `UITableView` 和 `UITableViewDelegate`)因为只有与委托类放在一起才有意义,所以就不用单独分离头文件,应该放到定义 `UITableView` 的头文件中 - Tips 3 使用字面量 - 对于 `NSString`,`NSNumber`,`NSDictionary` 和 `NSArray` 使用类似 `@"String"`,`@1`,`@[]` 和 `@{}` 不要使用等价方法 - Tips 4 使用类型常量 - 定义常量时,使用类型常量不要使用 `#define`,比如: ``` // 使用如下的方式定义 static const NSInteger kInteger = 1; // 而不是 #define SOME_INTEGER 1 ``` 这样可以给编译器类型信息,在编译时和开发时能够进行类型检查 - 每一个 m 文件都是一个编译单元 - 使用`static` 声明表示在本编译单元有效,若需要将变量放到全局有效,那么需要使用 `extern` - 使用 `const` 表示常量不会被修改 - Tips 5 使用枚举表示状态,选项,状态码 - 使用 `NS\_ENUM` 宏定义枚举,因为枚举是按顺序的,也就是枚举值是1,2,3…… 这样的 - 使用 `NS\_OPTION` 定义选项,因为选项是按位的,也就是选项是通过 `1 << 0`,`1 << 1` 这样来定义的,表示1右移 ## Chapter 2 对象,消息,运行时 - Tips 6 理解属性 - 使用属性,而不是实例变量,在代码中使用点(`.`)操作符访问属性 - 属性会生成对应的实例变量,一般是属性名前加下划线,也可以在类的实现代码中通过 `@synthesize` 来指定,例如:`@synthesize firstName = _firstName` - 使用 `@dynamic` 告诉编译器不需要生成对应的getter 和 setter - 属性的 attribute 会影响编译器生成的代码 - atomic / nonatomic,原子性,一般我们都使用 `nonatomic` 因为 iOS 的属性锁开销很大,另外 `atomic` 并不能保证线程安全 - readwrite / readonly,读写或是只读 - 内存管理要注意的 - assign,简单类型直接赋值 - strong,表示持有 - weak,不持有,在对象被释放时属性将会变成 `nil` - unsafe_unretained,不持有,在对象被释放时属性不会变成 `nil` - copy,设置属性时会调用对象的 `-copy` 方法获得新的对象,建议所有不可变的 `NSString`,`NSArray`,`NSDictionary` 都使用这个方法,可变的类型不可以使用这个方法 - getter=name / setter=name,指定 getter 和 setter 方法的名字 - Tips 7 对象内部直接访问实例变量,设置时通过属性方法 - 直接访问实例变量减少方法调用消耗 - 设置通过属性方法,调用实际的 setter,能够保证写入控制和 KVO 的触发 - 可以在 getter 和 setter 方法中加入断点方便调试 - 惰性初始化的变量,因为需要需要重写 getter 方法,所以不能使用直接访问实例变量来访问 - Tips 8 对象相等 - `==` 只是比较对象指针是否相等,深层的比较需要使用 `-isEqual:` 方法 - 如果 `-isEqual:` 返回返回真,那他们的 `-hash` 方法要返回同一个值,但是 `-hash` 方法返回同一个值的两个对象 `-isEqual:` 不一定为真 - `-hash` 方法用作在集合类型中计算索引,如果所有对象的这个方法都返回同一个值,那么在集合中检索对象性能会很差,这个方法应该使用计算速度快,并且不容易碰撞的方法实现 - 有特定相等判断方法的对象,优先使用特定相等判断方法,可以减少调用次数和对对象进行类型检查(例如 `NSString` 的 `-isEqualToString:` 方法) - 放入集合对象中的对象,需要保证 `-hash` 方法得到的值不会变,如果放入集合后,修改集合内对象导致 `-hash` 的值发生变化,那么集合对象是不会知道 `-hash` 值有变化,并且将来会出现奇怪的错误 - Tips 9 类簇 - 类簇很有用,可以把实现细节隐藏在抽象的基类中 - 对于使用到类簇的对象,需要使用 `-isKindOfClass:` 来判断,不可使用 `[object class] == [Class class]` 或者 `-isMemberOfClass:` 来判断 - 若要继承类簇中的类,那么需要根据文档实现对应的方法 - Tips 10 Associated Object - 可以为已有的对象创建新的属性 - 设置方法 - `void objc\_setAssociatedObject(id object, void *key, id value, objc\_AssociationPolicy policy)` - `id objc\_getAssociatedObject(id object, void *key)` - `id objc\_removeAssociatedObject(id object)` - 关联类型和属性的对应(上面 policy 的值) - OBJC\_ASSOCIATION\_ASSIGN:assign - OBJC\_ASSOCIATION\_RETAIN_NONATOMIC:nonatomic, retain - OBJC\_ASSOCIATION\_RETAIN:retain - OBJC\_ASSOCIATION\_COPY_NONATOMIC:nonatomic, copy - OBJC\_ASSOCIATION\_COPY:copy - key 的值,使用一个 opaque pointer,一般来说使用静态全局变量 - Tips 11 理解 `objc\_msgSend` 的作用 - Objective-C 中所有的方法,都是 C 函数 - 给某个对象的消息全部都是动态发送的,如下 - `id returnValue = [someObject messageName:parameter]` - 编译后,将会使用 `objc\_msgSend` 函数处理消息发送,得到 `id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter)` - `objc\_msgSend` 会去 `someObject` 的方法列表中对应的函数,如果找不到,那么沿着继承体系继续找,还是找不到,那么会进行消息转发操作(在 Tips 12 讲解) - Objective-C 的运行时已经做了很多保证让这套机制性能很好 - 其中一个优化就是尾调用优化,Objective-C 的每个对象的方法都是 C 方法,并且有和 `objc\_msgSend` 一样的原型,这样在 `objc\_msgSend` 从对象的方法列表中找到对应函数时,可以直接跳转过去,不需要重新在调用堆栈中插入新的栈帧 - Tips 12 理解消息转发 - 因为 Objective-C 使用运行时来决定具体调用的方法,所以在运行之前是不知道一个对象是否能响应特点方法的 - 消息转发是在一个对象收到无法解读的消息时触发的机制 - 消息转发的过程 - 第一步,进行动态方法解析——询问接收者,所属的类,看是否能动态添加方法,来处理未知的 selector - 第二步,第一步无法处理这个 selector 的话进行消息转发——首先,让接收者看看是否有对象能处理这个消息,如果有,那么丢给他处理;如果没有,那么运行时会把所有和消息相关的东西放到一个 `NSInvocation` 对象里面,最后再给接收者一次处理的机会 - 动态方法解析 - 过程,下面两个过程是渐进的,第一个失败了,那么执行第二个 - 调用 `+ (BOOL)resolveInstanceMethod:(SEL)selector` 询问类是否能新增一个实例方法处理这个消息 - 调用 `+ (BOOL)resolveClassMethod:(SEL)selector` 询问类是否能增加一个类方法来处理这个消息 - 用法,相关方法实现已经写好,只等着运行时去动态插入到类里面就行了 - 实现 `@dynamic` 属性 - 消息转发 - 备选的消息接收者 - 调用 `- (id)forwardingTargetForSelector:(SEL)selector` 询问接收者是否有另一个对象来处理这个消息 - 可以模拟多重继承,由对象内的其他对象来处理这个消息,但是从调用者看来,是被调用的对象处理的消息 - 这样转发的消息,我们是无法进行操作或是修改消息内容的 - 完整的消息转发 - 创建 `NSInvocation` 对象,把 selector,target 和参数都放进去 - 消息派发系统(message-dispatch system)调用 `- (void)forwardInvocation:(NSInvocation *)invocation` 方法吧消息指派给目标对象 - 这个方法可以简单的只修改接收者让另一个对象去接受处理这个方法,但是这样做法和使用备选的消息接收者做法是等效的,所以一般来说更多是先修改消息内容,再触发消息 - 这个方法的如果没有实现,那么就会调用超类的这个方法,类的继承体系中的所有类都有机会处理直到 `NSObject` - `NSObject` 的 `- (void)forwardInvocation:(NSInvocation *)invocation` 只是简单的调用 `- (void)doesNotRecognizeSelector:` 方法抛出异常,所以一般向对象发送他没有实现的方法都会通过这个方法抛出异常 - Tips 13 Method Swizzling(原书叫方法调配技术,看到这个名词,第一句话是什么鬼) - 很多时候,这个技术被大家称为黑魔法,但是其实他做的只是在运行时交换方法的实现而已 - 所有的方法都是在对象中是一个 `IMP` 指针,指针原型 `id (*IMP)(id, SEL, ...)` - 每个类通过一张映射表来映射可相应的 selector 和对应 `IMP` 指针的关系 - 可以做 AOP - 对方法的操作 - 使用 `void method_exchangeImplementations(Method m1, Method m2)` 来交换两个方法 - 使用 `Method class_getInstanceMethod(Class aClass, SEL aSelector)` 来获取类的实例方法 - 常规的 Method Swizzling 做法 ``` // 在 Category 的定义文件中增加我们将要用来替换的方法 @interface NSString (MethodSwizzling) - (NSString *)ms_myLowercaseString; @end // 在 Category 的实现文件中,进行交换 @implementation NSString (MethodSwizzling) - (NSString *)ms_myLowercaseString { // 在调用原方法前做点其他的事情 // 注意这里并不是递归调用,而是因为我们交换了 lowercaseString 和 ms_myLowercaseString 的实现,所以这里调用 ms_myLowercaseString 实际上是在调用 lowercaseString 方法 NSString *s = [self ms_myLowercaseString]; // 在调用完原方法后做点其他的事情 return s; } + (void)load { Method m1 = class_getInstanceMethod([NSString class], @selector(lowercaseString)); Method m2 = class_getInstanceMethod([NSString class], @selector(ms_myLowercaseString)); method_exchangeImplementations(m1, m2); } @end ``` - Tips 14 理解类对象 - 每个对象结构体的首个成员变量是 `Class` 类的变量 - `Class` 对象是一个 `objc_class` 的结构体,里面保存了类的元数据 - `Class` 类同样有元类(metaclass) - 某个类如果有超类(super class)那么他的 `Class` 对象的元类,也继承于该类超类的元类 - `-isMemberOfClass:` 可以判断某个对象是否是某个类的实例 - `-isKindOfClass:` 可以判断某个对象是否是某个类或其子类的实例 - 使用上面说到的两个方法类判断类型,不要直接比较类对象,因为某些对象可能实现了消息装发功能