第二章:对象、消息、运行期
“对象”就是“基本构造单元”,开发者可以通过对象来存储并传递数据。对象之间传递数据并执行任务的过程就是“消息传递”。程序运行起来后,为其提供相关支持的代码就是“Objective-C运行期环境”,它提供了一些使得对象之间能够传递消息的重要函数,并且包括创建类实例所用的全部逻辑。
第6条:理解“属性”这一概念
- ”属性“ (property) 是Objective-C的一项特性,用于封装对象中的数据。实例变量一般通过“存取方法”来访问,其中getter用于读取变量值,setter用于写入变量值。开发者可以令编译器自动编写与属性有关的存取方法。
- Objective-C语言很少将实例变量放在类接口的public区段内,更多的是使用
@property
的方式来声明。这是因为如果直接使用public的区段,当实例变量的偏移量改变(原有的实例变量里插入了新的实例变量)的时候,需要重新编译,假如代码库中某份代码使用了旧的类定义,就会出现不兼容的情况。
//public区段内声明实例变量
@interface EOCPerson : NSObject {
@public
NSData *_dataOfBirth;
NSString *_firstName;
NSString *_lastName;
}
//使用@property方式声明
@interface EOCPerson : NSObject
@property(nonatomic, copy) NSString *firstName;
@property(nonatomic, copy) NSString *lastName;
@end
//@property方式等效于
@interface EOCPerson : NSObject
- (NSString *)firstName;
- (void)setFirstName;
- (NSString *)lastName;
- (void)setLastName;
@end
如果要访问属性,可以使用“点语法”,编译器会把“点语法”转换成对存取方法的调用。
EOCPerson *aPerson = [EOCPerson new];
aPerson.firstName = @"GDGD";//same as
[aPerson setFirstName:@"GDGD"];
NSString *lastNameOfGD = aPerson.lastName;//same as
NSString *lastNameOfGD = [aPerson lastName];
使用属性的方式进行声明实例变量,编译器会自动生成存取方法,虽然这个时候在编译器上看不到生成的方法,这个方法默认的名字是:getter—就是属性的名字,setter—属性名字前面加set。
如果想要修改“合成方法”的名字,可以使用@synthesize语法来指定关键字的名字。
//在实现文件中
@implementation EOCPerson
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
如果不想要编译器自动生成的方法,可以使用@dynamic语法来阻止编译器的生成。
@implementation EOCPerson
@dynamic firstName, lastName;
@end
- 属性具有四种特质,不同的特质也会影响属性生成的存取方法。
(1)、原子性:默认情况下,编译器所合成的方法会通过锁定机制确保其原子性 (atomicity) 。如果属性具备nonatomic特质,则不使用同步锁。在iOS开发的程序里,由于同步锁开销较大,如果使用原子性的属性会导致性能问题,所以属性都是nonatomic的特质。
(2)、读写权限:
readwrite (读写) :拥有前面说的获取方法 (getter) 和设置方法 (setter),默认情况。
readonly (只读) :仅拥有获取方法。可以对外暴露为只读属性,然后在“class-continuation分类”中重新定义为读写属性。(27条有说)
(3)、内存管理语义:这是最难理解的部分了,虽然这个特质仅会影响“设置方法”,但是其中涉及到内存管理部分,所以还是非常重要。
assign:“设置方法”只会针对“纯量类型”(scalar type,例如CGFloat,NSInteger等)的简单赋值。一般在之前分配在栈里的数据类型就是使用assign修饰。
strong:此特质表示一种拥有关系,当给这种属性设置新值的时候,会先保留新值,再释放旧值,然后设置新值上去,一般Objective-C对象使用这个修饰词。
weak:表示非拥有关系,为这种属性设置新值的时候,不保留新值,也不释放旧值,当属性所指的对象被销毁的时候,属性值也会被清空。这就是一种弱持有,一般可以用来打破循环引用的情况(后面会介绍)。
unsafe_unretained :语义和assign相同,但是适用于对象类型(object type),表示一种非拥有关系,但是当目标对象被销毁的时候,属性值不会被清空。
copy:和strong类似,但是设置方法并不保留新值,而是将其拷贝。一般NSString *就使用这个特质来修饰。这是因为NSString *有可能指向一个可变的NSMutableString 实例,如果使用的不是copy的话,那么当可变字符串被篡改后,会影响到你不可变的字符串,所以要拷贝一个不可变的字符串。
(4)、方法名:可以用来指定存取方法的方法名。
getter=<name>:指定获取方法的方法名,如果某属性是Boolean,一般会用这种方式来在获取方法名上加上is
前缀。
setter=<name>:指定设置方法的方法名,较少使用。
第7条:在对象内部尽量直接访问实例变量
- 在对象之外访问实例变量是通过属性来实现,但是当在对象内部访问实例变量的话最好采用直接访问的形式,而在设置实例变量的时候才通过属性来做。
//使用属性访问
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}
- (void)setFullName {
NSArray *components = [[self fullName] componentsSeparatedByString:@" "];
self.firstName = components[0];
self.lastName = components[1];
}
//使用实例变量访问
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@", _firstName, _lastName];
}
- (void)setFullName {
NSArray *components = [[self fullName] componentsSeparatedByString:@" "];
_firstName = components[0];
_lastName = components[1];
}
-
使用实例变量和属性访问的区别如下:
- 使用实例变量直接访问由于不经过方法派发,直接访问保存实例变量的那块内存,所以速度更快。
- 直接访问实例变量可以绕过内存管理语义。
- 直接访问实例变量不会出发KVO (Key-Value Observing)。
- 通过属性可以帮助自己debug的时候排查与之相关的错误。
知道以上的区别了之后,一般采用一种折中的方案,设置属性使用“设置方法“,读取实例变量则直接访问。
-
在初始化的时候,如果需要设置实例变量,应当直接访问,因为子类可能会”覆写“设置方法,导致调用到子类的设置方法的时候,出现意想不到的结果。
-
如果使用了懒加载的方式,就需要使用属性的方式来访问实例变量。
//lazy initialization
- (NSString *)firstName {
if (!_firstName) {
_firstName = @"GDGD";
}
return _firstName;
}
第8条:理解“对象等同性”这一概念
- 由于
==
操作符比较出来的结果未必是我们想要的,因为该操作符是两个指针本身,而不是指针所指的对象。所以一般使用NSObject协议中声明的”isEqual“方法来判断两个对象的等同性。如果是自定义的类,则需要重写该方法以实现等同性判断。
NSString *foo = @"hhh123";
NSString *bar = [NSString stringWithFormat:@"hhh%i",123];
Boolean equalA = (foo == bar); // NO
Boolean equalB = [foo isEqual:bar];//YES
Boolean equalC = [foo isEqualToString:bar];//YES
- NSObject协议中判断对象等同性主要是两个方法:
- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;
NSObject对这两个方法的默认实现是:指针值完全相等的时候,两个对象才相等。如果isEqual方法为Yes,那么两个对象的hash值相等,反之不成立。理解这个意义是自定义实现isEqual:
的关键。
设计hash方法的时候,需要尽量减少对象的碰撞,防止出现运算复杂度过大的情况。
- 特定类所具有的等同性判定方法:由于Objective-C在编译期没有做强类型转换,为了增强代码的鲁棒性,应该保证所传对象的类型正确,且有异常处理逻辑。
- 等同性判定的执行深度:如果是对数组进行等同性判定,往往需要比较两个数组的对应位置的所有对象,这样叫做“深度等同性判定”,如果可以通过identifier来标示等同性,就能大大节省计算。
- 容器中可变类的等同性:在容器加入可变类对象的时候,把某个对象放入容器之后,就不应该改变其hash值了。由于容器会根据其hash值放在不同的“箱子数组”里,如果放入箱子之后hash值又改变了,那么证明容器放错了箱子,可能会出现隐患。所以需要保证hash值不是根据对象的可变部分来实现的,或者是在其放入容器后就不再改变hash值。
NSMutableSet *set = [NSMutableSet new];
NSMutableArray *arrayA = [@[@1, @2] mutableCopy];
[set addObject:arrayA];
//set = {((1,2))}
NSMutableArray *arrayB = [@[@1] mutableCopy];
[set addObject:arrayB];
//set = {((1),(1,2))}
[arrayB addObject:@2];
//set = {((1,2),(1,2))} 打破了set的语义
NSSet *copySet = [set copy];
//copySet = {((1,2))}
第9条:以”类族模式“隐藏实现细节
- 类族模式可以把实现细节隐藏在一套简单的公共接口后面。Objective-C的系统框架中普遍使用该模式,用户无需自己创建子类实例,只需要调用基类方法来创建即可。
- 一般采用工厂模式来创建类族。由于Objective-C这门语言没办法指明某个基类是”抽象“的,所以如果使用了类族模式需要写上相应的注释。
- Cocoa里的类族:大部分collection类都是类族,如果要子类化这些类族,需要遵守一些规则:
- 子类应到继承自类族中的抽象基类:如果要编写NSArray类族的子类,需要继承自不可变数组的基类或者可变数组的基类。
- 子类应该定义自己的数据存储方式。
- 子类应当覆写超类文档中指明需要覆写的方法。
第10条:在既有类中使用关联对象存放自定义数据
-
有时候类的实例可能是由某种机制所创建的,那么也就是无法创建出自己所写的子类实例,那么也就是如果你需要存放对象到类中,就不能通过创建子类的方式实现了。而Objective-C的“关联对象” (associated object) 可以解决这个问题。
-
可以给某个对象关联多个对象,通过“键”来区分,同时在创建关联对象的时候,也有相应的“内存管理语义”,由名为
objc_AssociationPolicy
的枚举所定义。关联类型 等效的@property属性 OBJC_ASSOCIATION_ASSIGN assign OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic、copy OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic、retain OBJC_ASSOCIATION_COPY copy OBJC_ASSOCIATION_RETAIN retain
-
-
管理关联对象的方法:
//此方法以给定的键和策略为某对象设置关联对象
- void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
//此方法根据给定的键从某对象中获取相应的关联对象值
id objc_getAssociatedObject(id object, const void *key);
//此方法移除指定对象的全部关联对象
void objc_removeAssociatedObjects(id object);
可以把对象想象成NSDictionary,把关联到该对象的值理解为字典的条目,本质的区别在于设置关联对象的key是个不透明的指针,在NSDictionary里如果两个键值相等那么isEqual:
方法的返回值就是YES,但是关联对象必须是两个指针相同才行,在设置关联对象的时候通常使用静态全局变量做键。
第11条:理解objc_msgSend的作用
- Objective-C的方法调用方式是消息结构,这种传递消息需要有“名称”或“选择子”,可以接受参数,而且可能还有返回值。在Objective-C中,如果向某对象传递消息,那么就会使用动态绑定机制来决定需要调用的方法,当对象收到消息之后,究竟该调用那个方法完全由运行期决定,同时可以在程序运行时改变。
- 给对象发送消息的例子和过程如下:
/**
someObject —— 接受者
messageName —— 选择子
parameter —— 参数
**/
id returnValue = [someObject messageName:parameter];
//编译器看到这个消息之后会转换成一条标准的C语言函数调用,调用的是消息中心的核心函数,objc_msgSend
void objc_msgSend(id self, SEL cmd, ...);
//所以上面那个函数调用经过编译器会转换成如下函数-->
id returnValue = objc_msgSend(someObject,
@selector(messageName:),
parameter);
//obj_msgSend函数会依据接受者和选择子的类型来调用适当的方法,为了完成此操作的方法需要在接受者所属类中搜寻方法列表,如果有就跳到其执行的代码,如果没找到就沿着体系继续往上查找,找到合适的方法再跳转。如果还没有,后面就会涉及到消息转发机制。
//obj_msgSend会将匹配结果换存在”快速映射表“里,这样每个类其实都有这样一块缓存,所以执行起来很快,但是还是不如”静态绑定的函数调用操作“那样迅速。
- 除了上述的部分消息的调用过程,还有一些”边界情况“,则需要交由Objective-C运行环境中的一些函数来处理:
- objc_msgSend_stret:如果待发送的消息要返回结构体,那么可以交由此函数处理。需要CPU的寄存器能够容纳这个消息返回的结构体,如果无法容纳就会由另一个函数进行派发,那个函数会通过分配在栈上的某个变量来处理返回的结构体。
- objc_msgSend_fpret:如果消息返回的是浮点数,那么需要交由此函数处理。这个函数是为了处理x86等架构CPU中某些奇怪状况。
- objc_msgSendSuper:如果要给超类发消息,就给这个函数处理。
- 如果某函数的最后一项操作是调用另外一个函数,就可以使用“尾调用优化”技术。编译器会生成跳转到另一函数所需要的指令码,而不用向调用堆栈里推入新的”栈帧“。当某函数的最后一个操作仅仅是调用其他函数而不会将其返回值另作他用的时候,才能执行”尾调用优化“。
第12条:理解消息转发机制
-
在编译期向类发送了其无法解读的消息并不会报错,因为运行期可以向类中添加方法,所以编译器在编译时无法确定类中到底会不会有某个方法实现。当对象接收到无法解读的消息后,就会启动”消息转发“机制。
-
消息转发分为两大阶段:
- 第一阶段先征询接受者所属的类,看其是否能动态添加方法,以处理当前这个”未知的选择子“,这部分叫”动态方法解析“。注意这是一个类方法,因为是向接收者所属的类进行请求。
- 第二阶段涉及”完整的消息转发机制“,这里细分为两小步:
- 当对象所属类不能动态添加方法后,
runtime
就会询问当前的接受者是否有其他对象可以处理这个未知的selector
。 - 当没有备援接收者时,就只剩下最后一次机会,那就是消息重定向。这个时候
runtime
会将未知消息的所有细节都封装为NSInvocation
对象,给接受者最后一次机会,令其设法解决当前还未处理的这条消息。
- 当对象所属类不能动态添加方法后,
-
动态方法解析:
- 对象收到无法解读的消息后,首先调用其所属类的类方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel;
这种方法的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就可以了。此方案常用来实现
@dynamic
属性。Example:
//假设这两个方法已经实现 id autoDictionaryGetter(id self,SEL _cmd); void autoDictionarySetter(id self, SEL _cmd, id value); //使用这个类方法进行消息转发 + (BOOL)resolveInstanceMethod:(SEL)sel { NSString *selectorString = NSStringFromSelector(sel); if (/* selector is from a @dynamic property */) { if ([selectorString hasPrefix:@"set"]) { class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@"); } else { class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:"); } return YES; } return [super resolveInstanceMethod:sel]; }
-
备援接收者:
- 当前接受者还有第二次机会能处理未知的选择子,在这一步中,系统会问该选择子能不能转发给其他接受者。相关的方法为:
- (id)forwardingTargetForSelector:(SEL)aSelector;
虽然Objective-C不支持多重继承,但是通过这个函数的组合我们可以模拟出多次继承的某些特性。
-
完整的消息转发:
- 首先创建NSInvocation对象,把尚未处理的那条消息的所有细节都封在其中,包括选择子、目标和参数,这个步骤会调用下列方法来转发消息:
- (void)forwardInvocation:(NSInvocation *)invocation;
实现此方法的时候,如果发现某调用操作不应由本类处理,则需调用超类的同名方法,这样继承体系中的每个类都有机会处理此调用请求,直到NSObject。
-
消息转发流程:
第13条:用“方法调配技术”调试“黑盒方法”
-
与给定的选择子名称相对应的方法也可以在运行期改变,不需要知道源代码,也不需要通过继承子类来覆写方法就能够改变这个类本身的功能,新功能会在本类的所有实例中生效,而不仅限于覆写了相关方法的子类实例,这个方案成为“方法调配” (method swizzling)。方法以函数指针的形式来表示,为IMP指针,原型如下:
id (*IMP)(id, SEL, ...);
-
为了互换2个已经写好的方法实现,可以使用下列函数:
//此函数的两个参数表示待交换的两个方法实现
void method_exchangeImplementations(Method m1, Method m2);
//方法实现可以通过下列函数获得。
Method class_getInstanceMethod(Class cls, SEL name);
//example
//将uppercaseString和lowercaseString两个方法利用方法调配进行调换
Method originalMethod = class_getClassMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getClassMethod([NSString class], @selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
除了上面说的两个系统方法的替换,还可以使用自定义的方法和系统方法进行替换,这样的话就能够为那些系统的黑盒方法增加日志记录功能,这个非常有助于程序调试。很少人会在调试程序之外的场合使用上述方法来永久改变某个类的功能。
第14条:理解“类对象”的用意
- Objective-C有个特殊的类型叫
id
,他能指代任意类型的Objective-C对象类型。编译器假定它能响应所有消息。id
类型本身的定义如下:
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
每个对象结构体的首个成员是Class
类的变量,该变量定义了对象所属的类,称为"is a"指针。
Class
的定义如下:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
此类结构体存放类的元数据,例如类的实例实现了几个方法,具备多少个实例变量等信息,首个变量也是isa
指针,说明Class
本身也是Objective-C对象。结构体有个叫做super_class
的变量,是本类的超类。类对象所属的类型是另一个类,叫做“元类”,用来表示类对象本身所具备的元数据。每个类仅有一个类对象,每个类对象仅有一个与之相关的元类。
example:
假设有个名为someClass的子类从NSObject继承而来,则既成体系如下所示:
super_class
指针确立了继承关系,而isa
指针描述了实例所属的类。
-
在类继承体系中查询类型信息:
- isMemberOfClass:能够判断出对象是否为某个特定类的实例。
- isKindOfClass:能够判断出对象是否为某类或者其派生类的实例。
NSMutableDictionary *dict = [NSMutableDictionary new]; [dict isMemberOfClass:[NSDictionary class]];// NO [dict isMemberOfClass:[NSMutableDictionary class]];// YES [dict isKindOfClass:[NSDictionary class]];// YES [dict isKindOfClass:[NSArray class]];// NO
如果要比较类对象是否等同的话需要使用
==
操作符,而不是isEqual:
,因为类对象是个单例,在应用程序范围内,每个类的Class
仅有一个实例。
网友评论