小蓝书学习
- 第二章:对象,消息,运行期
- 前言
- 第六条:理解“属性”这一概念
- 属性特质
- 原子性
- atomic
- nonatimic
- 读写权限
- 内存管理语义
- 方法名
- 第七条:在对象内部进来直接访问变量实例
- 第八条:理解“对象等同性”这一概念
- 特定类的等同性判断方法
- 容器中可变类的等同性
- 第九条:以“类族模式”隐藏实现细节
- COcoa里的类族
- 第十条:在既有类中使用关联对象存放自定义数据
- 第十一条:理解objc_masgSend的作用
- 第十二条:理解消息转发机制
- 动态方法解析
- 备援接收者
- 完整的消息转发
- 完整的动态方法解析例子
- 第十三条:用“方法调配技术”调试“黑盒方法”
- 第十四条:理解类对象
第二章:对象,消息,运行期
前言
在OC中,对象就是“基本构造单元”,用来储存并传递数据。
在对象之间传递数据并执行任务的过程就叫做“消息传递”
第六条:理解“属性”这一概念
属性是OC中的一项特性,用于封装对象中的数据。OC对象通常会把其所需要的数据保存为各种实例变量,通过“存取方法”(即setter
和getter
方法)来访问。
在对象的接口定义中,可以使用属性来访问封装在对象里的数据。因此可以把属性当作一种简称:编译器会自动写出一套存取方法,用以访问给定类型中具有给定名称的量。代码如下:
@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
当需要访问属性时,采用“点语法”即可调用。编译器会把“点语法”转换为对存取方法的调用
EOCPerson* aPerson = [EOCPerson new];aPerson.firstName = @"Luck"; // Same as
[aPerson setFirstName: @"Luck"];NSString* lastName = aPerson.lastName; // Same as
NSString* lastName = [aPerson lastName];
使用属性,编译器会自动编写访问这些属性所需的方法。这个过程叫作”自动合成“。由编译器在编译期执行,所以编辑器里看不到这些“合成方法”的源代码。除此之外,编译器还会自动向类中添加适当类型的实例变量,并且在属性名前面加下划线_
。如在上述代码中,会生成两个实例变量分别为_firstName
和_lastName
。可以使用@synthesize
语法来制定实例变量的名字。
@implementation EOCPerson@synthesize firstName = _bigName;
@synthesize lastName = _smallName;@end
使用@dynamic
关键字,它告诉编译器不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。而且即使编译器发现没有定义存取方法,也不会报错,它相信这些方法能在运行期找到。
属性特质
属性的各种特质设定也会影响编译器所生成的存取方法。下面这个属性指定了三项特质:
@property (nonatomic, readwrite, copy)NSString* firstName;
属性特质分为四类:
原子性
atomic
在默认情况下,编译器所合成的方法会通过锁定机制确保其原子性,即编译器自动生成同步锁。对setter和getter方法加锁,保证性的赋值和取值的原子性操作是线程安全的。
比如说atomic修饰的是一个数组的话,那么我们对数组进行赋值和取值是可以保证线程安全的。但是如果我们对数组进行操作,比如说给数组添加对象或者移除对象,是不在atomic的负责范围之内的,所以给被atomic修饰的数组添加对象或者移除对象是没办法保证线程安全的。
nonatimic
如果属性具有nonatimic特质,则不使用同步锁。性能比atomic更高,但不保证线程安全。
读写权限
- 具备readwrite(读写)特质的属性,由编译器自动生成setter和getter方法的声明和实现。
- 具备readonly(只读)特质的属性仅有getter获取方法。
内存管理语义
修饰符 | 特点 |
---|---|
assign | 用于纯量类型(如 CGFloat、NSInteger 等),执行简单的赋值操作,不涉及内存管理。 |
strong | 表示“拥有关系”,设置新值时保留新值并释放旧值,适用于对象类型,会增加对象的引用计数。 |
weak | 表示“非拥有关系”,设置新值时既不保留新值也不释放旧值。当对象被销毁时,属性值会自动置为 nil。 |
unsafe_unretained | 类似于 assign,但适用于对象类型。不保留对象,对象销毁时属性值不会自动置为 nil,可能导致野指针。 |
copy | 设置新值时拷贝对象,适用于不可变对象(如 NSString、NSArray 等),确保属性的不可变性。 |
对于copy这个关键词:
如果传入的是 NSString,copy 不会创建新对象,直接引用原对象。
如果传入的是 NSMutableString,copy 会创建一个不可变的 NSString 副本。
方法名
可通过如下特质来指定存取方法的方法名:
- getter=< name >,指定“获取方法”的方法名。
- setter=< name >,指定“设置方法”的方法名。
在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
我们还可以自定义初始化方法:
@interface EOCPerson : NSObject
@property (nonatomic, copy) NSString* firstName;
@property (nonatomic, copy) NSString* lastName;
-(id) initWithFitstName:(NSString*)firstName lastName:(NSString*)lastName;
@end
- (id)initWithName:(NSString *)firstName lastName:(NSString *)name {if (self = [super init]) {_firstName = [firstName copy];_lastName = [lastName copy];}return self;
}
第七条:在对象内部进来直接访问变量实例
在对象内部读取数据时,直接通过实例便利那个来读取。
在写入数据时,通过属性来写入。
直接访问实例变量和通过属性访问的区别:
- 由于不经过OC的“方法派发”步骤,直接访问实例变量的速度比较快,编译器所生成的代码就直接访问保存对象实例变量的那块内存。
- 直接访问实例变量,不会调用其setter方法,这就绕过了为相关属性所定义的“内存管理语义”。比如,如果在ARC下直接访问一个声明为copy的属性,那么并不会拷贝该属性,只会保留新对象释放旧对象。
- 直接访问实例变量不会触发“键值观测”(Key-Value
Observing,KVO)通知,因为KVO时通过在运行时生成派生类并重写setter方法以达到通知所有观察者的目的。这样做是否会产生问题,取决于具体的对象行为。 - 通过属性来访问有助于排查与之相关的错误,因为可以在setter和getter方法中设置断点来调试。
有一种合理的折中方案,那就是:在写入实例变量时,通过setter来做;在读取实例变量时,则直接访问。这样既可以提高读取操作的速度,又能控制对属性的写入操作。
需要注意的是:
在初始化中建议直接访问实例变量。因为子类可能会”覆写“设置方法。例如下列代码:
假设EOCPerson有一个子类,叫做EOCSmithPerson,这个子类专门表示那些姓“Smith”的人。该子类可能会覆写lastName属性所对应的setter方法:
- (void)setLastName: (NSString *)lastName {if (![lastName isEqualToString: @"Smith"]) {[NSException raise: NSInvalidArgumentException format: @"Last name must be Smith"];self.lastName = lastName;}
}
在基类EOCPerson的默认初始化方法中,可能将姓氏设会空字符串。如果此时,我们采用设置方法,则会调用子类的设置方法,从而抛出异常。
以下两种情况必须在初始化方法中调用设置方法:
- 待初始化的实例变量声明在父类,而我们又无法在子类中直接访问此实例变量。
- 懒加载时,必须通过getter来访问,否则实例变量就永远不会初始化。
- (EOCBrain *)brain {if (!_brain) {_brain = [Brain new];}return _brain;
}
上述代码就是一个懒加载,如果不调用getter方法,实例变量就永远不会初始化。
第八条:理解“对象等同性”这一概念
首先明白一点:==
是用来判断两个指针地址是否相同,并不是对象。判断对象的等同性,我们应该使用isEqual:
一般来说,两个类型不同的对象总是不相等的,因为某些对象提供了特殊的“等同性判断方法”,如果已经知道两个受测对象都属于同一个类,那么就可以使用此方法,以NSString为例:
NSString* foo = @"Badger 123";
NSString* bar = [NSString stringWithFormat: @"Badger %i", 123];BOOL equalA = (foo == bar); //NO
BOOL equalB = [foo isEqual: bar]; //YES
BOOL equalC = [foo isEqualToString: bar]; //YES
调用isEqualToString:方法比调用isEqual:方法要快,因为后者还要执行额外的步骤,因为它不知道受测对象的类型。
NSObject协议中定义了两个判断同等性的关键方法:
默认实现是当且仅当指针值(内存地址)完全相同的时候两个对象才相等。
如果想自主实现的话,我们的hash方法必须返回同一个值,同时在通过自定义的isEqual方法正确后后才能来判断其是否正确。
特定类的等同性判断方法
NSArray和NSDictionary都有自己的等同性判断方法。分别名为isEqualToArray isEqualToDictionary。
这里我们要注意如果和其比较的对象不是数组或字典,这两个方法会抛出异常。
有些时候需要我们自己写一个方法来判断等同性,在创建等同行判断方法时,需要决定是根据整个对象来判断等同性,还是仅根据其中几个字段来判断。
NSArray的检测方式为:
- 先看两个数组所含对象个数是否相同
- 若相同,则在每个对应位置的两个对象上调用其isEqual:方法
如果对应位置上的对象均相等,那么这两个数组就相等。
这就叫做“深度等同性判断”
有时我们可以为属性创建一个唯一标识符,根据标识符来判断等同性。
容器中可变类的等同性
在容器中放入某个对象时,就不应该再改变其hash值 ,否则会有隐患。
因为collection会把各个对象按照其hash分装到不同的“箱子数组”中,如果某对象在放入箱子后hash又变了,那么其现在所处的箱子对它来说就是“错误”的。
所以要确保添加到容器中对象的hash不是根据对象的“可变部分”计算出来的,或是保证之后不再改变对象内容。因此,我们最好不要往容器中添加NSMutableArray等可变对象。代码如下:
NSMutableSet* set = [NSMutableSet new];NSMutableArray* arrayA = [@[@1, @2] mutableCopy];
[set addObject: arrayA];
NSLog(@"set = %@", set);
//set = {((1, 2))}NSMutableArray* arrayB = [@[@1, @2] mutableCopy];;
[set addObject: arrayB];
NSLog(@"set = %@", set);
//set = {((1, 2))}
//待加入数组对象和set中已有的数组对象相等,所以set不会改变NSMutableArray* arrayC = [@[@1] mutableCopy];
[set addObject: arrayC];
NSLog(@"set = %@", set);
//set = {((1), (1, 2))}[arrayC addObject: @2];
NSLog(@"set = %@", set);
//set = {((1, 2), (1, 2))}//改变arrayC的内容,令其和最早加入set的那个数组相等//set中居然可以包含两个彼此相等的数组,根据set的语义是不允许出现这种情况的。然而现在却无法保证这一点了,因为我们修改了set中已有的对象//若是拷贝此set,就更糟了
NSSet* setB = [set copy];
NSLog(@"setB = %@", setB); // set = {((1, 2))};
因此,我们将都数据放入容器之后,最好不要对内部的可变数据进行操作。
第九条:以“类族模式”隐藏实现细节
“类族”模式是一种设计模式:定义一个抽象基类,使用”类族“把具体行为放在这个基类的子类中,将它们的实现细节隐藏在抽象基类后面,以保持接口简洁。
以在UIKit框架中创建UIButton为例,我们调用下面这个“类方法”:
+(UIButton*)buttonWithType:(UIButtonType)type;
该方法所返回的对象,其类型取决于传入的按钮类型。但是,不管返回什么类型的对象,它们都继承自同一个基类:UIButton。
即实现:使用者无须关心创建出来的实例具体是什么类型,也不用考虑其实现细节,只需调用基类方法来创建即可。
现在我们举例创建基类:
假设有一个处理雇员的类,每个雇员都有“名字”和“薪水”这两个属性,管理者可以命令其执行日常工作。
但是,各种雇员的工作内容却不同,经理在带领雇员做项目时,无须关心每个人如何完成其工作,仅需指示其开工即可。
定义抽象基类:
//头文件
typedef NS_ENUM(NSUInteger, EOCEmployeeType) {EOCEmployeeTypeDeveloper,EOCEmployeeTypeDesigner,EOCEmployeeTypeFinance
};@interface EOCEmployee : NSObject@property (copy) NSString* name;
@property NSUInteger salary;//创建EOCEmployee对象
+ (EOCEmployee *)employeeWithType: (EOCEmployeeType)type;//雇员们做各自的工作
- (void)doADaysWork;@end
//实现文件
@implementation EOCEmployee+ (EOCEmployee *)employeeWithType:(EOCEmployeeType)type {EOCEmployee* employee = nil;switch (type) {case EOCEmployeeTypeDeveloper:employee = [EOCEmployeeDeveloper new];break;case EOCEmployeeTypeDesigner:employee = [EOCEmployeeDesigner new];break;case EOCEmployeeTypeFinance:employee = [EOCEmployeeFinance new];break;}return employee;
}- (void)doADaysWork {//抽象方法由子类们实现
}@end
每个实体子类(concrete subclass,与“抽象基类”相对,意思是非抽象的、可实例化的),都从基类继承而(此处仅举一例):
//EOCEmployeeDeveloper.h
@interface EOCEmployeeDeveloper : EOCEmployee
@end//EOCEmployeeDeveloper.m
@implementation EOCEmployeeDeveloper
- (void)doADaysWork {NSLog(@"Developer Work!");
}
@end
在这个例子中,基类实现了一个“类方法”,该类方法根据待创建的雇员类型分配好对应的雇员类实例。
COcoa里的类族
大部分容器类都是类族。以NSArray和NSMutableArray为例,这两个尽管是两个不同的类,但是可以合起来算作一个类族。不可变的类定义了所有数组都通用的方法,而可变的类则定义了那些只适用于可变数组的方法。同时可以互相转化。
要明白NSArray这样的类背后其实是个类族,则下面的代码具有明显错误:
id maybeAnArray = /* */
if ([maybeAnArray class] == [NSArray class]) {//}
该代码的if语句永远为假。[maybeAnArray class]
所返回的决不可能是NSArray类本身,这是由于NSArray的初始化方法所返回的那个实例其类型是隐藏在类族公共接口后面的某个内部类型。
当我们向类族中新增实体子类时,需遵守以下规则:
- 子类要继承与抽象基类
- 子类要定义自己的数据存储方式
- 子类要重写超类文档中应该重写的方法
第十条:在既有类中使用关联对象存放自定义数据
当有的类的实例是由某种机制创建的,我们无法令这种机制创建一个子类实例,这时就需要用到“关联对象”。
可以给某对象关联许多其他对象,这些对象通过“键”来区分。
存储对象值的时候,可以指明“存储策略”(storage policy),用以维护相应的“内存管理语义”。
存储策略由名为objc_AssociationPolicy的枚举所定义,下表列出了该枚举的取值,同时还列出了与之等效的@property属性":
关联对象类型:
相关方法:
- 以给定的键和策略为某对象设置关联对象值:
void objc_setAssociatedObject(id object, void* key, id value, objc_AssociationPolicy policy)
- 根据给定的键从某对象中获取相应的关联对象值:
id objc_getAssociatedObject(id object, void* key)
- 移除指定对象的全部关联对象:
void objc_removeAssociatedObjects(id object)
可以把关联对象object想象成NSDictionary,于是存取关联对象值就相当于在NSDictionary对象上调用[object setObject: value forKey: key]与[object objectForKey: key]方法。
然而两者之间有个重要差别:设置关联对象时用的键(key)是个“不透明指针”如果在两个键上调用isEqual:方法的返回值是YES,那么NSDictionary就会认为二者相等;然而在设置关联对象值时,若想令两个键匹配到同一个值,则二者必须时完全相同的指针才行。
鉴于此,在设置关联对象时,通常使用静态全局变量做键。
使用举例:
#import <UIKit/UIKit.h>@interface UIView (TagString)// 定义 tagString 的 setter 和 getter 方法
@property (nonatomic, copy) NSString *tagString;@end
#import "UIView+TagString.h"
#import <objc/runtime.h> // 关联对象所需的头文件@implementation UIView (TagString)// 定义关联对象的 key
static const void *kTagStringKey = &kTagStringKey;- (void)setTagString:(NSString *)tagString {// 将 tagString 属性关联到当前实例 self 上objc_setAssociatedObject(self, kTagStringKey, tagString, OBJC_ASSOCIATION_COPY_NONATOMIC);
}- (NSString *)tagString {// 获取关联到 self 上的 tagString 属性值return objc_getAssociatedObject(self, kTagStringKey);
}@end
在 UIView 类 的 TagString 分类 中,通过关联对象动态添加了一个名为 tagString 的属性。
这种技术可以让我们在不修改原有类定义的情况下,给现有类添加属性。
第十一条:理解objc_masgSend的作用
C语言采用“静态绑定”,在编译器就能决定运行时所调用的函数。
#import <stdio.h>void printHello(void) {printf("Hello, world!\n");
}void printGoodbye(void) {printf("Goodbye, world!\n");
}void doTheThings(int type) {if (type == 0) {printHello();} else {printGoodbye();}return 0;
}
如果不考虑“内联”(inline),那么编译器在编译代码的时候就已经知道程序中有哪些函数了,于是会直接生成调用这些函数的指令,而函数地址实际上是硬编码在指令之中的。如果将上例代码写成下面这样,会如何呢?
#import <stdio.h>void printHello(void) {printf("Hello, world!\n");
}void printGoodbye(void) {printf("Goodbye, world!\n");
}void doTheThings(int type) {void (*func) (void);if (type == 0) {func = printHello;} else {func = printGoodbye;}func();
}
这时就得使用“动态绑定”了,因为所要调用的函数直到运行期才能确定。第一个例子中,if与else语句里都有函数调用指令,而在第二个例子中,只有一个函数调用指令,不过待调用函数地址无法硬编码在指令之中,而是要在运行期读取出来。
在OC中,给对象发送消息可以这样写:
id returnValue = [someObject messageName: parameter];
在这里,someObject叫做“接收者”,messageName叫做“名称”或“选择” ,可以接受参数,而且可能还有返回值,选择子与参数合起来称为“消息”。
编译器看到此消息后,会将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数objc_msgSend,其原型如下:
void objc_msgSend(id self, SEL cmd, ...);
该函数参数个数可变,能接受两个或两个以上参数。第一个参数代表“接受者”,第二个参数代表“选择字”,其余参数就是消息中原本的参数。OC中的方法调用在编译后会转换成该函数调用,比如以上方法调用后转换为:
id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);
边界情况处理:
“尾调用优化” 技术:编译器会生成调转至另一函数所需的指令码,而且不会向调用堆栈中推入新的“栈帧”。
当函数最后一个操作仅仅是调用其他函数而不会将其返回值另作他用时,才能执行此技术。
如果不这么做的话,那么每次调用OC方法前,都需要为调用objc_msgSend函数准备“栈帧”,在“栈踪迹”中,可以看到这种栈帧。此外,若是不优化,还会过早地发生“栈溢出”现象。
第十二条:理解消息转发机制
当对象在“消息发送”阶段无法处理消息(找不到方法实现)时,就会进入“消息转发”阶段,开发者可以在此阶段处理未知消息。
消息转发机制分为三个阶段:动态方法解析、备援接受者和完整的消息转发。
动态方法解析
对象在收到无法解读的消息后,首先会调用以下方法,尝试动态添加方法实现来处理未知消息:
+ (BOOL)resolveInstanceMethod:(SEL)sel;
参数:sel 是未知的选择子(方法名)。返回值表示是否成功动态添加了方法实现。
备援接收者
如果动态方法解析失败,对象会尝试寻找一个备援接受者来处理未知消息:
- (id)forwardingTargetForSelector:(SEL)aSelector;
参数:aSelector 是未知的选择子(方法名)。
将未知消息转发给其他对象处理,返回一个能够处理该选择子的对象(备援接受者),如果找不到则返回 nil。
完整的消息转发
如果备援接受者也无法处理未知消息,系统会启动完整的消息转发机制:
先调用如下方法:
- (void)forwardInvocation:(NSInvocation *)anInvocation;
为未知消息创建一个方法签名,返回一个适合该选择子的方法签名。如果返回 nil,则消息转发失败。
实现以上方法时,不应由本类处理的未知消息,应该调用父类同名方法的实现,这样继承体系中的每个类都有机会处理此调用请求,直至NSObject。
NSObject的默认实现时最终调用doesNotRecognizeSelector:
以抛出异常,表明未知消息最终未能得到处理。
消息转发流程图:
完整的动态方法解析例子
//头文件
#import <Foundation/Foundation.h>@interface EOCAutoDictionary : NSObject@property (nonatomic, copy)NSString* string;
@property (nonatomic, strong)NSNumber* number;
@property (nonatomic, strong)NSDate* date;
@property (nonatomic, strong)id opaqueObject;@end
//实现文件
#import "EOCAutoDictionary.h"
#import <objc/runtime.h> @interface EOCAutoDictionary ()@property (nonatomic, strong) NSMutableDictionary *backingStore;@end@implementation EOCAutoDictionary@dynamic string, number, date, opaqueObject;- (instancetype)init {self = [super init];if (self) {_backingStore = [NSMutableDictionary new];}return self;
}+ (BOOL)resolveInstanceMethod:(SEL)sel {// 选择字命名NSString *selectorString = NSStringFromSelector(sel);if ([selectorString hasPrefix:@"set"]) {// 动态添加 setter 方法class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@");} else {// 动态添加 getter 方法class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");}return YES;
}id autoDictionaryGetter(id self, SEL _cmd) {// 获取当前对象和后端存储字典EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;NSMutableDictionary *backingStore = typedSelf.backingStore;// 将选择子(方法名)作为键NSString *key = NSStringFromSelector(_cmd);return [backingStore objectForKey:key];
}void autoDictionarySetter(id self, SEL _cmd, id value) {// 获取当前对象和后端存储字典EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;NSMutableDictionary *backingStore = typedSelf.backingStore;// 将选择子(方法名)转换为字符串NSString *selectorString = NSStringFromSelector(_cmd);NSMutableString *key = [selectorString mutableCopy];// 删除":" 后缀[key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];// 删除"set" 前缀[key deleteCharactersInRange:NSMakeRange(0, 3)];// 将首字母转换为小写NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];[key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];if (value) {[backingStore setObject:value forKey:key];} else {[backingStore removeObjectForKey:key];}
}@end
这里设计一个功能性的类,思路是:由开发者来添加属性定义,并将其声明为@dynamic,而类会自动处理相关属性值的存放setter与获取getter操作。
第十三条:用“方法调配技术”调试“黑盒方法”
方法调配 是一种通过运行时动态交换两个方法实现的技术。它允许开发者在不通过继承子类重写方法的情况下,直接改变类的行为。新功能会在本类的所有实例中生效,而不仅限于重写了相关方法的子类实例。
类的方法列表会把选择子的名称映射到相关的方法实现之上,使得“动态消息派发系统”能够根据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做IMP,其实就是SEL到IMP的映射,其原型如下
id (*IMP)(id, SEL, ...)
NSString类可以响应lowercaseString、uppercaseString、capitalizedString等选择子,下面这张映射表中的每个选择子都映射到了不同的IMP之上:
在运行期我们可以操作这张表,为其新增或改变选择子。
上述修改均无须编写子类,只要修改了“方法表”的布局,就会反映到程序中所有的NSString实例之上。
#import <Foundation/Foundation.h>
#import <objc/runtime.h> @interface NSString (EOCSwizzling)
- (NSString *)eocLowercaseString; // 新方法,用于扩展功能
@end@implementation NSString (EOCSwizzling)// 新方法的实现
- (NSString *)eocLowercaseString {// 调用原始方法(实际上是交换后的 lowercaseString)NSString *lowercaseString = [self eocLowercaseString];// 添加自定义逻辑(如日志记录)NSLog(@"Original: %@ => Lowercase: %@", self, lowercaseString);// 返回结果return lowercaseString;
}@end
int main(int argc, const char * argv[]) {@autoreleasepool {// 交换 lowercaseString 和 eocLowercaseString 的实现Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));Method swizzledMethod = class_getInstanceMethod([NSString class], @selector(eocLowercaseString));method_exchangeImplementations(originalMethod, swizzledMethod);NSString *string = @"I lIke GogoGo"; // 调用 lowercaseString,实际上会执行 eocLowercaseStringNSString *lowercaseString = [string lowercaseString];NSLog(@"Lowercase: %@", lowercaseString);// 调用 uppercaseStringNSString *uppercaseString = [string uppercaseString];NSLog(@"Uppercase: %@", uppercaseString);}return 0;
}
通过方法交换,开发者可以为那些“完全不知道具体实现的”(completely opaque,“完全不透明的”)黑盒方法增加日志记录功能,这非常有助于程序调试。但是,很少有人在调试程序之外的场合用上述“方法调配技术”来永久改动某个类的功能,应该合理使用这个方案,若是滥用,反而会令代码变得不易读懂且难于维护。
第十四条:理解类对象
id类型可以指代任意的OC对象。编译器假定类型为id的对象可以响应所有消息。
每一个OC对象实例都是指向某块内存数据的指针。对于通用的对象类型id,其本身就已经是指针了,所以写法如下:
id genericTypedString = @"Some string";
id类型本身的定义:
typedef struct objc_object {Class isa;
}* id;
由此可见,每个对象结构体的首个成员是Class类的变量。该变量定义了对象所属的类,通常称为“is a”指针,该指针描述了实例所属的类。例如,所用的对象“是一个”(is a)NSString,所以其“is a”指针就指向NSString。Class对象也定义在运行期程序库的头文件中:
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;
};
- 此结构体存放类的 “元数据”(metaclass),例如类的实例实现了几个方法,具备多少个实例变量等信息。
- 此结构体的首个变量也是isa指针,这说明Class本身亦为Objective-C对象。
- 结构体里还有个变量叫做super_class,它定义了本类的超类,该指针确立了继承关系。
- 类对象所属的类型(isa指针指向的类型)是另外一个类,叫做 “元类”(metaclass),用来表述类对象本身所具备的元数据。
- “类方法” 就定义此处,因为这些方法可以理解成类对象的实例方法。
- 每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”。
假设有个名为SomeClass的子类从NSObject中继承而来,则其继承体系如下:
在继承体系中查询类型消息
两个方法:
-/+ (BOOL)isMemberOfClass:(Class)aClass;
:判断当前instance/class对象的isa指向是不是class/metaclass对象类型(也就是判断当前对象是否为某个类的实例)。
-/+ (BOOL)isKindOfClass:(Class)aClass
;:判断当前instance/class对象的isa指向是不是class/metaclass对象或者它的子类类型(也就是判断当前对象是否为某个类或其子类的实例)。
这两个方法查询的逻辑如图所示:
这里直接调用大佬文章:文章地址