第1章:熟悉Objective-C
通论该语言的核心概念
第1条:了解 Objective-C 语言的起源
- Objective-C从Smalltalk语言是从Smalltalk语言演化而来,
Smalltalk是消息语言的鼻祖 - Objective-C为C语言添加了面向对象特性,是其超集。Objective-C使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型。接收- -条消息之后,究竟应执行何种代码,由运行期环境而非编译器来决定。
- 消息结构与函数调用的关键区别在于:函数调用的语言,在编译阶段由编译器生成一些虚方法表,在运行时从这个表找到所要执行的方法去执行。而使用了动态绑定的消息结构在运行时接到一条消息,接下来要执行什么代码是运行期决定的,而不是编译器。
NSString *someString = @"Hello World";
NSString *anotherString = @"Hello World";
NSLog(@"someString:%p --- anotherString:%p", someString, anotherString);
印结果:
someString:0x1000102e8 --- anotherString:0x1000102e8
两个变量为指向同一块内存的相同指针。此时将 anotherString
赋值为 “Hello World!!!”
NSString *anotherString = @"Hello World!!!";
NSLog(@"someString:%p --- anotherString:%p", someString, anotherString);
打印结果:
someString:0x1000102e8 --- anotherString:0x100010308
此时,两者变为不同的内存地址。所以,对象的本质是指向某一块内存区域的指针,指针的存储位置取决于对象声明的区域和有无成员变量指向。若在方法内部声明的对象,内存会分配到栈中,随着栈帧弹出而被自动清理;若对象为成员变量,内存则分配在堆区,声明周期需要程序员管理。
第2条:在类的头文件中尽量少引入其他头文件
- 除非确有必要,否则不要引入头文件,一般来说,应该在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合。
- 有时无法使用向前声明,比如要声明某个类遵循一项协议,尽量把“该类遵循某协议” 的这条声明移至“class-continuation 分类中”。如果不行的话,就把协议单独放在某一个头文件中,然后将其引入。
//Student.h
@class Book; //向前引用,避免在 .h 里导入其他文件
@interface Student : NSObject
@property (nonatomic, strong) BOOK *book;
@end
//student.m
#import "Book.h"
@implementation Student
- (void)readBook {
NSLog(@"read the book name is %@",self.book);
}
@end
这样做有什么优点呢:
- 不在A的头文件中引入B的头文件,就不会一并引入B的全部内容,这样就减少了编译时间。
- 可以避免循环引用:因为如果两个类在自己的头文件中都引入了对方的头文件,那么就会导致其中一个类无法被正确编译。
但是个别的时候,必须在头文件中引入其他类的头文件:
- 该类继承于某个类,则应该引入父类的头文件。
- 该类遵从某个协议,则应该引入该协议的头文件。而且最好将协议单独放在一个头文件中
第3条:多用字面量语法,少用与之等价的方法
- 应该使用字面量语法来创建字符串、数值、数组、字典。与创建此类对象的常规方法相比,这么做更加简明扼要。
- 应该通过取下标操作来访问数组下标或字典中的键所对应的元素。
- 用字面量语法创建数组或字典,若值中有 nil ,则会抛出异常。因此,务必确保值里不含 nil。
- 声明时的字面量语法:
在声明NSNumber
,NSArray
,NSDictionary
时,应该尽量使用简洁字面量语法。
NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.5f;
NSArray *animals =[NSArray arrayWithObjects:@"cat", @"dog",@"mouse", @"badger", nil];
Dictionary *dict = @{@"animal":@"tiger",@"phone":@"iPhone 6"};
- 集合类取下标的字面量语法:
NSArray,NSDictionary,NSMutableArray,NSMutableDictionary 的取下标操作也应该尽量使用字面量语法。
NSString *cat = animals[0];
NSString *iphone = dict[@"phone"];
字面语法的局限性:
字面量语法所创建的对象必须属于 Foundation 框架,自定义类无法使用字面量语法创建。
使用字面量语法创建的对象只能是不可变的。若希望其变为可变类型,可将其深复制一份
NSMutableArray *arrayM = [@[@1,@"123",@"567"] mutableCopy];
第4条:多用类型常量,少用 #define 预处理指令
- 不要用预处理指令定义常量。这样定义的常量不含类型信息,编译器只是会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息⚠️,这将导致应用程序中的常量值不一致。
- 在实现文件中使用
static const
来定义“只在编译单元内可见的常量”。由于此类常量不在全局符号表中,所以无需为其名称加前缀。 - 在头文件中使用
extern
来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以名称应该加以区隔,通常用与之相关的类名做前缀。
在OC中,定义常量通常使用预处理命令,但是并不建议使用它,而是使用类型常量的方法。
两种方法的区别:
- 预处理命令:简单的文本替换,不包括类型信息,并且可被任意修改。
- 类型常量:包括类型信息,并且可以设置其使用范围,而且不可被修改。
我们可以看出来,使用预处理虽然能达到替换文本的目的,但是本身还是有局限性的:不具备类型 + 可以被任意修改,总之给人一种不安全的感觉。
知道了它们的长短处,我们再来简单看一下它们的具体使用方法:
预处理命令:
#define W_LABEL (W_SCREEN - 2*GAP)
这里,(W_SCREEN - 2*GAP)替换了W_LABEL,它不具备W_LABEL的类型信息。而且要注意一下:如果替换式中存在运算符号,最好用括号括起来。
类型常量:
static NSString* const kEnableGestureRecognizer = @"EnableGestureRecognizer";
这里:
const 将其设置为常量,不可更改。
static意味着该变量仅仅在定义此变量的编译单元中可见。如果不声明static,编译器会为它创建一个外部符号(external symbol)。我们来看一下对外公开的常量的声明方法:
注意:const修饰符在常量类型中的位置。常量定义应从右至左解读,所以在本例中,kEnableGestureRecognizer 就是“ 一个常量,而这个常量是指针,指向NSString对象”。这与需求相符:我们不希望有人改变此指针常量,使其指向另-个NSString对象。
对外公开某个常量:
如果我们需要发送通知,那么就需要在不同的地方拿到通知的“频道”字符串,那么显然这个字符串是不能被轻易更改,而且可以在不同的地方获取。这个时候就需要定义一个外界可见的字符串常量。
//header file
extern NSString *const NotificationString;
//implementation file
NSString *const NotificationString = @"Finish Download";
我们通常在头文件声明常量,在其实现文件里定义该常量。由实现文件生成目标文件时,编译器会在“数据段”为字符串分配存储空间。
最后注意一下公开和非公开的常量的命名规范:
- 公开的常量:常量的名字最好用与之相关的类名做前缀。
- 非公开的常量:局限于某个编译单元(tanslation unit,实现文件 implementation file)内,在签名加上字母k。
第5条:用枚举表示状态、选项、状态码
- 应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字。
- 如果把传递给某个方法的选项表示为枚举类型,而多个选项又可以同时使用,那么就将各选项定义为2的幂,以便通过按位或操作将其组合起来。
- 用 NS_ENUUM 与 NS_OPTIONS 宏来定义枚举类型,并指明其底层数据类型。这样做可以确保枚举是用开发者所选的底层数据类型实现出来的,而不会采用编译器所选类型。
- 在处理枚举类型的switch语句中不要实现 default分支。这样的话,加入新枚举之后,编译器就会提示开发者:switch 语句并未处理所以枚举。
/// 位移枚举
typedef NS_OPTIONS(NSUInteger, Direction) {
DirectionTop = 0,
DirectionBottom = 1 << 0,
DirectionLeft = 1 << 1,
DirectionRight = 1 << 2,
};
/// 常量枚举
typedef NS_ENUM(NSInteger,ShowType){
ShowTypeForce,
ShowTypeNormal
};
第2章:对象、消息、运行时
对象之间能够关联与交互,这是面向对象语言的重要特征。本章讲述这些特征,并深人研究代码在运行期的行为。
第6条:理解“属性”这一概念
- 可以用 @property 语法来定义对象中所封装的数据。
- 通过“特质”来指定存储数据所需的正确语义。
- 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
- 开发iOS程序时应该使用nonatomic属性,因为atomic属性会严重影响性能。
- 存取方法
在设置完属性后,编译器会自动写出一套存取方法,用于访问相应名称的变量:
@interface EOCPerson : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end
@interface EOCPerson : NSObject
- (NSString*)firstName;
- (void)setFirstName:(NSString*)firstName;
- (NSString*)lastName;
- (void)setLastName:(NSString*)lastName;
@end
访问属性,可以使用点语法。编译器会把点语法转换为对存取方法的调用:
aPerson.firstName = @"Bob"; // Same as:
[aPerson setFirstName:@"Bob"];
NSString *lastName = aPerson.lastName; // Same as:
NSString *lastName = [aPerson lastName];
如果我们不希望编译器自动生成存取方法的话,需要设置@dynamic 字段:
@interface EOCPerson : NSManagedObject
@property NSString *firstName;
@property NSString *lastName;
@end
@implementation EOCPerson
@dynamic firstName, lastName;
@end
- 属相特质
定义属性的时候,通常会赋予它一些特性,来满足一些对类保存数据所要遵循的需求。
原子性:
- nonatomic:不使用同步锁
- atomic:加同步锁,确保其原子性
读写
- readwrite:同时存在存取方法
- readonly:只有获取方法
内存管理
- assign:纯量类型(scalar type)的简单赋值操作
- strong:拥有关系保留新值,释放旧值,再设置新值
- weak:非拥有关系(nonowning relationship),属性所指的对象遭到摧毁时,属性也会清空
- unsafe_unretained :类似assign,适用于对象类型,非拥有关系,属性所指的对象遭到摧毁时,属性不会清空。
- copy:不保留新值,而是将其拷贝
注意:遵循属性定义
如果属性定义为copy,那么在非设置方法里设定属性的时候,也要遵循copy的语义
- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName
{
if ((self = [super init])) {
_firstName = [firstName copy];
_lastName = [lastName copy];
}
return self;
}
第7条:在对象内部尽量直接访问实例变量
关于实例变量的访问,可以直接访问,也可以通过属性的方式(点语法)来访问。书中作者建议在读取实例变量时采用直接访问的形式,而在设置实例变量的时候通过属性来做。
直接访问属性的特点:
绕过set,get语义,速度快;
通过属性访问属性的特点:
不会绕过属性定义的内存管理语义
有助于打断点排查错误
可以触发KVO
因此,有个关于折中的方案:
设置属性:通过属性
读取属性:直接访问
不过有两个特例:
- 初始化方法和dealloc方法中,需要直接访问实例变量来进行设置属性操作。因为如果在这里没有绕过set方法,就有可能触发其他不必要的操作。
- 惰性初始化(lazy initialization)的属性,必须通过属性来读取数据。因为惰性初始化是通过重写get方法来初始化实例变量的,如果不通过属性来读取该实例变量,那么这个实例变量就永远不会被初始化。
第8条:理解“对象等同性”这一概念
- 若想监测对象的等同性,请提供
isEqual:
与hash
方法。 - 相同对象必须具有相同的哈希码,但是两个哈希码相同的对象未必相同。
- 不要盲目地逐个监测每条属性,而是应该依照具体需求来制定检测方案。
- 编写 hash 方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。
1. 同等性判断
==操作符比较的是指针值,也就是内存地址。
然而有的时候我们只是想比较指针所指向的内容,在这个时候,就需要通过isEqual:方法来比较。
而且,如果已知两个对象是字符串,最好通过isEqualToString:
方法来比较。
对于数组和字典,也有isEqualToArray:
方法和isEqualToDictionary:
方法。
另外,如果比较的对象类型和当前对象类型相同,就可以采用自己编写的判定方法,否则调用父类的isEqual
方法:
- (BOOL)isEqualToPerson:(EOCPerson*)otherPerson {
//先比较对象类型,然后比较每个属性
if (self == object) return YES;
if (![_firstName isEqualToString:otherPerson.firstName])
return NO;
if (![_lastName isEqualToString:otherPerson.lastName])
return NO;
if (_age != otherPerson.age)
return NO;
return YES;
}
- (BOOL)isEqual:(id)object {
//如果对象所属类型相同,就调用自己编写的判定方法,如果不同,调用父类的isEqual:方法
if ([self class] == [object class]) {
return [self isEqualToPerson:(EOCPerson*)object];
} else {
return [super isEqual:object];
}
}
2. 深度等同性判定
比较两个数组是否相等的话可以使用深度同等性判断方法:
1.先比较数组的个数
2.再比较两个数组对应位置上的对象均相等。
第9条:以“类族模式”隐藏实现细节
- 类族模式可以把实现细节隐藏在一套简单的公共接口后面。
- 系统框架中经常使用类族。
- 从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。
在iOS开发中,我们也会使用“类族”(class cluster)这一设计模式,通过“抽象基类”来实例化不同的实体子类。例如
+ (UIButton *)buttonWithType:(UIButtonType)type;
该方法所返回的对象,其类型取决于传人的按钮类型( button type)。然而,不管返回什么类型的对象,它们都继承自同-一个基类: UIButton。 这么做的意义在于: UIButton 类的使用者无须关心创建出来的按钮具体属于哪个子类,也不用考虑按钮的绘制方式等实现细节。使用者只需明白如何创建按钮,如何设置像“标题”( title)这样的属性,如何增加触摸动作的目标对象等问题就好。
利用“工厂模式”(Factory pattern)是创建类族如下。
//EOCEmployee.h
typedef NS_ENUM(NSUInteger, EOCEmployeeType) {
EOCEmployeeTypeDeveloper,
EOCEmployeeTypeDesigner,
EOCEmployeeTypeFinance,
};
@interface EOCEmployee : NSObject
@property (copy) NSString *name;
@property NSUInteger salary;
// Helper for creating Employee objects
+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type;
// Make Employees do their respective day's work
- (void)doADaysWork;
@end
//EOCEmployee.m
@implementation EOCEmployee
+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type {
switch (type) {
case EOCEmployeeTypeDeveloper:
return [EOCEmployeeDeveloper new];
break;
case EOCEmployeeTypeDesigner:
return [EOCEmployeeDesigner new];
break;
case EOCEmployeeTypeFinance:
return [EOCEmployeeFinance new];
break;
}
}
- (void)doADaysWork {
// 需要子类来实现
}
@end
员工基类可以有多个子类,只需要根绝类型即可子类。使用了工厂模式的思想
第10条:在既有类中使用关联对象存放自定义数据
- 可以通过“关联对象”机制来把两个对象连起来。
- 定义关联对象时可指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”。
- 只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于查找的bug。
利用runtime里面的关联对象(Associated Object)为不方便修改的实体类添加属性
用法是
//此方法以给定的键和策略为某对象设置关联对象值。
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)
//此方法根据给定的键从某对象中获取相应的关联对象值。
id objc_ getAssociatedObject(id object, void*key)
//此方法移除指定对象的全部关联对象。
void objc_ removeAssociatedObjects(id object)
其中objc_AssociationPolicy
是策略模式,模式对应值如下
举个例子
#import
static void *EOCMyAlertViewKey = "EOCMyAlertViewKey";
- (void)askUserAQuestion {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question"
message:@"What do you want to do?"
delegate:self
cancelButtonTitle:@"Cancel"
otherButtonTitles:@"Continue", nil];
void (^block)(NSInteger) = ^(NSInteger buttonIndex){
if (buttonIndex == 0) {
[self doCancel];
} else {
[self doContinue];
}
};
//将alert和block关联在了一起
objc_setAssociatedObject(alert,EOCMyAlertViewKey,block, OBJC_ASSOCIATION_COPY);
[alert show];
}
// UIAlertViewDelegate protocol method
- (void)alertView:(UIAlertView*)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
//alert取出关联的block
void (^block)(NSInteger) = objc_getAssociatedObject(alertView, EOCMyAlertViewKey)
//给block传入index值
block(buttonIndex);
}
第11条:理解objc_msgSend的作用
- 消息由接收者、选择子及参数构成。给某对象“发送消息”(invoke a message)日也就相当于在该对象上“调用方法”(call a method)。
- 发给某对象的全部消息都要由“动态消息派发系统" (dynamic message dispatch system)来处理,该系统会查出对应的方法,并执行其代码。
在OC中,如果向某对象传递信息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的C语言函数.
然而对象收到 消息后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得OC成为一门真正的动态语言。
在OC中,给对象发送消息的语法是:
id returnValue = [someObject messageName:parameter];
这里,someObject叫做“接收者(receiver)”,messageName:叫做"选择子(selector)",选择子和参数合起来称为“消息”。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数叫做objc_msgSend,它的原型如下:
void objc_msgSend(id self, SEL cmd, ...)
第一个参数代表接收者,第二个参数代表选择子,后续参数就是消息中的那些参数,数量是可变的,所以这个函数就是参数个数可变的函数。
因此,上述以OC形式展现出来的函数就会转化成如下函数:
id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);
这个函数会在接收者所属的类中搜寻其“方法列表”,如果能找到与选择子名称相符的方法,就去实现代码,如果找不到就沿着继承体系继续向上查找。如果找到了就执行,如果最终还是找不到,就执行消息转发操作。
如果匹配成功的话,这种匹配的结果会缓存在“快速映射表”里面。每个类都有这样一块缓存。所以如果将来再次向该类发送形同的消息,执行速度就会更快了。
第12条:理解消息转发机制
- 若对象无法响应某个选择子(seletor),则进入消息转发流程
- 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中
- 对象可以把其无法解读的某些选择子转交给其他对象来处理
- 经过上述两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制
消息转发的全流程
类方法+(BOOL)resolveInstanceMethod:(SEL)selector:
查看这个类是否能新增一个实例方法用以处理此选择子;如果是类方法则为+ (BOOL)resolveClassMethod:(SEL)selector
实例方法- (id)forwardTargetForSelector:(SEL)selector;
询问是否能找到未知消息的备援接受者,如果能找到备援对象,就将其返回,如果不能,就返回nil。
实例方法- (void)forwardInvocation:(NSInvocation*)invocation:
创建NSInvocation对象,将尚未处理的那条消息 有关的全部细节都封于其中,在触发NSInvocation
对象时,“消息派发系统(message-dispatch system)”就会将消息派给目标对象。
一个完整例子
下面来看一个关于动态方法解析的例子:
#import
@interface EOCAutoDictionary : NSObject
@property (nonatomic, strong) NSString *string;
@property (nonatomic, strong) NSNumber *number;
@property (nonatomic, strong) NSDate *date;
@property (nonatomic, strong) id opaqueObject;
@end
#import "EOCAutoDictionary.h"
#import
@interface EOCAutoDictionary ()
@property (nonatomic, strong) NSMutableDictionary *backingStore;
@end
@implementation EOCAutoDictionary
@dynamic string, number, date, opaqueObject;
- (id)init {
if ((self = [super init])) {
_backingStore = [NSMutableDictionary new];
}
return self;
}
+ (BOOL)resolveInstanceMethod:(SEL)selector {
NSString *selectorString = NSStringFromSelector(selector);
if ([selectorString hasPrefix:@"set"]) {
class_addMethod(self,selector,(IMP)autoDictionarySetter, "v@:@");
} else {
class_addMethod(self,selector,(IMP)autoDictionaryGetter, "@@:");
}
return YES;
}
id autoDictionaryGetter(id self, SEL _cmd) {
// Get the backing store from the object
EOCAutoDictionary *typedSelf = (EOCAutoDictionary*)self;
NSMutableDictionary *backingStore = typedSelf.backingStore;
// The key is simply the selector name
NSString *key = NSStringFromSelector(_cmd);
// Return the value
return [backingStore objectForKey:key];
}
void autoDictionarySetter(id self, SEL _cmd, id value) {
// Get the backing store from the object
EOCAutoDictionary *typedSelf = (EOCAutoDictionary*)self;
NSMutableDictionary *backingStore = typedSelf.backingStore;
/** The selector will be for example, "setOpaqueObject:".
* We need to remove the "set", ":" and lowercase the first
* letter of the remainder.
*/
NSString *selectorString = NSStringFromSelector(_cmd);
NSMutableString *key = [selectorString mutableCopy];
// Remove the ':' at the end
[key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];
// Remove the 'set' prefix
[key deleteCharactersInRange:NSMakeRange(0, 3)];
// Lowercase the first character
NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
[key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
if (value) {
[backingStore setObject:value forKey:key];
} else {
[backingStore removeObjectForKey:key];
}
}
在本例中,EOCAutoDictionary类将属性设置为@dynamic,也就是说编译器无法自动为其属性生成set和get方法,因此我们需要动态给其添加set和get方法。
上面实例用到的编码含义地址为编码地址
更多消息转发请看看
消息转发与NSProxy
第13条:用“方法调配技术”调试“黑盒方法”
与选择子名称相对应的方法是可以在运行期被改变的,所以,我们可以不用通过继承类并覆写方法就能改变这个类本身的功能。
那么如何在运行期改变选择子对应的方法呢?
答:通过操纵类的方法列表的IMP指针
什么是类方法表?什么是IMP指针呢?
类的方法列表会把选择子的名称映射到相关的方法实现上,使得“动态消息派发系统”能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这些指针叫做IMP。例如NSString类的选择子列表:
OC的运行期系统提供的几个方法就能操纵它。开发者可以向其中增加选择子,也可以改变某选择子对应的方法实现,也可以交换两个选择子所映射到的指针以达到交换方法实现的目的。
举个 :交换lowercaseString
和uppercaseString
方法的实现:
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class],@selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
这样一来,类方法表的映射关系就变成了下图:
这时,如果我们调用lowercaseString方法就会实际调用uppercaseString的方法,反之亦然。
然而!
在实际应用中,只交换已经存在的两个方法是没有太大意义的。我们应该利用这个特性来给既有的方法添加新功能:
它的实现原理是:先通过分类增加一个新方法,然后将这个新方法和要增加功能的旧方法替换(旧方法名 对应新方法的实现),这样一来,如果我们调用了旧方法,就会实现新方法了。
不知道这么说是否抽象。还是举个 :
需求:我们要在原有的lowercaseString方法中添加一条输出语句。
步骤一:我们先将新方法写在NSString的分类里:
@interface NSString (EOCMyAdditions)
- (NSString*)eoc_myLowercaseString;
@end
@implementation NSString (EOCMyAdditions)
- (NSString*)eoc_myLowercaseString {
NSString *lowercase = [self eoc_myLowercaseString];//eoc_myLowercaseString方法会在将来方法调换后执行lowercaseString的方法
NSLog(@"%@ => %@", self, lowercase);//输出语句,便于调试
return lowercase;
}
@end
步骤二:交换两个方法的实现(操纵调换IMP指针)
Method originalMethod =
class_getInstanceMethod([NSString class],
@selector(lowercaseString));
Method swappedMethod =
class_getInstanceMethod([NSString class],
@selector(eoc_myLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
这样一来,我们如果交换了lowercaseString
和eoc_myLowercaseString
的方法实现,那么在调用原来的lowercaseString
方法后就可以输出新增的语句了。
“NSString *string = @"ThIs iS tHe StRiNg";
NSString *lowercaseString = [string lowercaseString];
// Output: ThIs iS tHe StRiNg => this is the string”
class_addMethod ,class_replaceMethod和method_exchangeImplementations区别
第14条:理解“类对象”的用意
在运行期程序库的头文件里定义了描述OC对象所用的数据结构:
typedef struct objc_class *Class;
struct objc_class {
Class isa;
Class super_class;
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list **methodLists;
struct objc_cache *cache;
struct objc_protocol_list *protocols;
};
在这里,isa指针指向了对象所属的类:元类(metaclass),它是整个结构体的第一个变量。super_class定义了本类的超类。
我们也可以向对象发送特定的方法来检视类的继承体系:自身属于哪一类;自身继承与哪一类。
- 我们使用
isMemberOfClass
:能够判断出对象是否为某个特定类的实例; - 而
isKindOfClass
:方法能够判断出对象是否为某类或其派生类的实例。
这两种方法都是利用了isa
指针获取对象所属的类,然后通过super_class
类在继承体系中查询。在OC语言中,必须使用这种查询类型信息的方法才能完全了解对象的真实类型。因为对象类型无法在编译期决定。
尤其注意在集合类里获取对象时,通常要查询类型信息因为这些对象不是强类型的(strongly typed),将它们从集合类中取出来的类型通常是id,也就是能响应任何消息(编译期)。
所以如果我们对这些对象的类型把握不好,那么就会有可能造成对象无法响应消息的情况。因此,在我们从集合里取出对象后,通常要进行类型判断:
- (NSString*)commaSeparatedStringFromObjects:(NSArray*)array {
NSMutableString *string = [NSMutableString new];
for (id object in array) {
if ([object isKindOfClass:[NSString class]]) {
[string appendFormat:@"%@,", object];
} else if ([object isKindOfClass:[NSNumber class]]) {
[string appendFormat:@"%d,", [object intValue]];
} else if ([object isKindOfClass:[NSData class]]) {
NSString *base64Encoded = /* base64 encoded data */;
[string appendFormat:@"%@,", base64Encoded];
} else {
// Type not supported
}
}
return string;
}