对于一个类,可以这样定义属性:
@interface EOCPerson : NSObject {
@public
NSString *_name;
NSString *_name1;
@private
NSString *_someInternalData;
}
@end
我们对其第一个位置在添加一个实例变量:
@interface EOCPerson: NSObject {
@public
NSString *_dateOfBirth;
NSString *_firstName;
NSString *_lastName;
@private
NSString *_someInternalData;
}
@end
我们新添加的实例变量就会代替原第一个位置实例变量的偏移量。
这样的话,如果代码使用了编译器计算出来的偏移量,那么在修改类定义之后必须重新编译,否则就会出错。例如:某个代码库中的代码使用了一份旧的类定义。如果和其相链接的代码使用了新的类定义,那么运行时就会出现不兼容现象。对此类问题,OC的解决方法是,把实例变量当做一种存储偏移量所用的“特殊变量”,交由“类对象”保管。偏移量会在运行期查找,如果类的定义变了,那么存储的偏移量也就变了,这样的话,无论何时访问实例变量,总能使用正确的偏移量。这就是稳固的“应用程序二进制接口”
这个问题还有一种解决方法,就是尽量不要直接访问实例变量,而应该通过存取方法来做。这时@property语法就派上用场了。这种规范的命名方法OC会自动创建出存取方法。
简单来说,以下两部分代码的效果是相同的:
@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
其中,编译器自动编写访问这些属性所需的方法的过程叫做:“自动合成”。这个过程由编译器在编译期执行,所有编译器例看不到这些“合成方法”的源代码。另外,编译器还会自动向类中添加适当类型的实例变量,并且在属性名前加下划线,以此作为实例变量的名字。还有,也可以在类的实现代码里通过@synthesize语法来指定实例变量的名字。
如果我们不想令编译器自动合成存取方法,那我们应该怎么做呢?那就是使用@dynamic关键字了,它会告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。然后我们就需要在运行期动态创建存取方法了。
如下代码所示:
@interface EOCPerson : NSManageObject
@property NSString *firstName;
@end
@implementation
@dynamic firstName;
@end
这样编译器就不会自动合成存取方法。
使用属性时还有一个问题要注意,就是各种特质设定也会影响编译器所生成的存取方法。比如下面这个属性就指定了三项特性 :
默认情况下,有编译器所合成的方法会通过锁定机制确保其原子性。如果属性具备nonatomic特质,则不使用同步锁。如果不具备,那它就是“原子的”,但是仍然可以在属性特质中写明这一点,编译器不会报错。如果是自己定义存取的方法,那么就应该遵从与属性特质相符的原子性。
属性用于封装数据,而数据则要有“具体的所有权语义”,如果自己编写存取方法,就必须同有关属性所具备的特质相符。
可通过如下特质来指定存取方法的方法名:
getter=指定“获取方法”的方法名。如果某属性时BOOL型,而你想为其获取方法加上“is”前缀,那么就可以用这个办法来指定。
(1)可以用@property语法来定义对象中所封装的数据。
(2)通过“特质”来指定存储数据所需的正确语义。
(3)在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
(4)开发iOS程序时应该使用nonatomic属性,因为atomic属性会严重影响性能。
笔者强烈建议大家在读取实例变量的时候,采用直接访问的形式,而在设置实例变量的时候通过属性来做。
对于上述的理解:给实例变量赋值的时候通过点语法来操作,读取实例成员的内容时以下划线的方式直接访问。
也叫做“延迟初始化”。在惰性初始化的情况下,必须通过“获取方法”来访问属性,否则,实例变量就永远不会初始化。一般用于:一个属性不常用,而且创建该属性的成本较高的情况。
- (EOCBrain *) brain {
if (!_brain) {
_brain = [Brain new];
}
return _brain;
}
若没有调用“获取方法” 就直接访问实例变量,则会看到尚末设置好的brain,所以说, 如果使用了“情性初始化” 技术,那么必领通过存取方法来访问brain属性。
== 是看地址来进行判断,地址不一致即返回false
isEqual:是专门用于判断的方法,不一定是看地址,也可以是其他的标准。
在NSObject类中, ==与isEqual:没有明显区别,但在NSString中,已经完成了重写,只要字符串字符序列相同,isEqual:方法就返回true。
等同性来比较对象是一个非常有用的功能。不过,按照==操作比较出来的结果未必是我们想要的,因为该操作比较的是两个指针本身,而不是其所指的对象。应该使用NSObject协议中声明的“isEqual”:方法来判断两个对象的等同性。一般来说,两个类型不同的对象总是不相等的。如果知道两个受测对象属于同一个类,那么就可以使用这种方法。
以下面代码为例:
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
可以看到== 与等同性判断方法之间的差别。NSString类实现了一个自己独有的等 同性判断方法,名叫 “isEqualToString: ” 。传递给该方法的对象必领是NSString,否则结果 末定义(u deined)。调用该方法比调用“ isEqual:〞方法快,后者还要执行额外的步骤,因为它不知道受测对象的类型。
NSObject 协议中有两个用于判断等同性的关键方法:
- (BOOL) isEqual: (id) object;
- (NSUInteger) hash;
NSObject 类对这两个方法的默认实现是: 当且仅当其“ 指针值” (pointervalue)日完全相等时,这两个对象才相等。若想在自定义的对象中正确後写这些方法,就必领先理解其约定 (contract)。如果“ isEqual:〞方法判定两个对象相等,那么其hash 方法也必须返回同 一个 值。但是,如果两个对象的hash 方法返回同一个值,那么“isEqual:〞方法末必会认为两者相等 。
当把某个对象放入collection后,就不应该再改变其哈希码了。因为collection会把各个对象按照其哈希码分装到不同的“箱子数组”中。如果某对象在放入“箱子”之后,哈希码又发生变化,那么其所处的这个箱子对他来说就是“错误”的。
“类族”是一种很有用的模式,可以隐藏“抽象基类”背后的实现细节。Objective-C的系统框架中普遍使用此模式。比如,ios 的用户 界面框架(user interface framework)UIKit中就有一个名为UIButton的类。想创建按钮,需要调用下面这个“ 类方法” ( class method )。
+ (UIButton*)buttonWithType:(UIButtonType)type;
该方法所返回的对象,其类型取决于传人的按钮类型(button type)。然而,不管返回什 么类型的对象,它们都继承自同 一个基类:UIButton。这么做的意义在于:UIButton 类的使 用者无领关心创建出来的按钮具体属于哪个 子类,也不用考虑按钮的绘制方式等实现细节。 使用者只需明白如何创建按钮,如何设置像“标题” (title)这样的属性,如何增加触摸动作 的目标对象等问题就好。
首先要定义抽象基类,也就是一个新的类,其中可以包括你的类型选取,使用枚举器和switch语句来完成,并且还的定义你的类的相关方法,再创建一个新的类,继承你之前的类,并且完成之前的定义方法,使用覆盖的原理,完成这些方法。这种“工厂模式”是创建类族的办法之一。
如果你想创建的类中没有init初始化的方法,那么这就是在暗示你该类的实例也许不应该由用户直接创建。总而言之,以后创建对象一定不要被其的表象迷惑住了,你可能觉得自己创建了某个类的实例,然而实际上创建的却是其子类的实例。
定义该类:
//定义员工类型
typedef NS_ENUM(NSUInteger, EOCEmployeeType) {
EOCEmployeeTypeDeveloper,
EOCEmployeeTypeDesiner,
EOCEmployeeTypeFinance
};
@interface EOCEmployee : NSObject
//定义属性
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger salary;
//定义方法
+ (EOCEmployee *)employeeWithType:(EOCEmployeeType)type;
- (void)doADaysWork;
@end
实现这个类:
@implementation EOCEmployee
+ (EOCEmployee *)employeeWithType:(EOCEmployeeType)type {
switch (type) {
case EOCEmployeeTypeDeveloper:
return [EOCEmployeeTypeDeveloper new];
break;
case EOCEmployeeTypeDesiner:
return [EOCEmployeeTypeDesiner new];
break;
case EOCEmployeeTypeFinance:
return [EOCEmployeeTypeFinance new];
break;
}
}
- (void)doADaysWork {
// Subclasses implement this.
}
@end
每个“实体子类”(concrete subclass)都是基类继承而来:
@interface EOCEmployeeTypeDeveloper : EOCEmployee
@end
@@implementation EOCEmployeeTypeDeveloper
- (void)doADaysWork {
[self writeCode];
}
@end
系统框架中有许多类族,就用我们经常使用的NSArray和NSMutableArray来说,这样来看,它是两个抽象基类,但是他们两个拥有相同的方法,这个方法可能就是他们共同类族中的方法,而可变数组的特殊方法就是只适用于可变数组的方法,其他的共同方法可能就是类族中的方法。
在使用NSArray的alloc方法来获取实例时,该方法首先会分配一个属于某个类的实例,此实例充当一个“占位数组”,也就是说,你把这个位置是先分配给其类族的,后来其类族才将这个位置分配给你创建的具体数据类型的。所以像这些类的背后其实是一个类族,在对一些if条件进行判断的时候一定要注意,例如:
id maybeAnArray = /* ... */;
if ([maybeAnArray class] == [NSArray class]) {
//Will never be hit
}
id maybeAnArray = /*...*/
if(maybeAnArray isKindOfClass:[NSArray class]) {
//will be hit
}
你若是想向NSArray这种已有类新增子类,那就得遵循以下规则:
可以通过关联对象为分类添加成员变量
//添加关联对象
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);
对象/属性/值/关联策略
对象/属性/值/关联策略
objc_setAssociatedObject(self,“str”,urlString,OBJC_ASSOCIATION_COPY);
objc_getAssociatedObject(self,“str”);
OBJC_ASSOCIATION_ASSIGN//等价于 @property(assign)。
OBJC_ASSOCIATION_RETAIN_NONATOMIC//等价于 @property(strong, nonatomic)。
OBJC_ASSOCIATION_COPY_NONATOMIC//等价于@property(copy, nonatomic)。
OBJC_ASSOCIATION_RETAIN//等价于@property(strong,atomic)。
OBJC_ASSOCIATION_COPY//等价于@property(copy, atomic)。
值得注意的是关联对象虽然好用,但是我们也是在没有办法继承类的情况下为了方便才使用的,本质上面关联对象的简便已经经过了层层筛选不得才使用关联对象。
在对象上调用方法是OC中经常使用的功能。用OC的术语来说,这叫做“传递消息”。消息有“名称”或“选择子”,可以接受参数,而且可能还有返回值。我们之前用C语言写出来的函数就是“静态绑定”的函数,就是说,他在编译期就能决定运行时所调用的函数。但是若是我们使用一个函数指针来实现函数调用的话,这时他就成为一个“动态绑定”了,因为所调用的函数直到运行期才能确定。
在OC中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有的方法都是普通的C语言函数,然而对象收到消息之后究竟该调用那个方法则完全取决于运行期决定,甚至可以在程序运行时改变,这些特性使得OC成为一门真正的动态语言。
来看下面一个对象发送消息:
id returnValue = [someObject messageName:parameter];
在这个例子中
someObject叫做“接收者”(receiver),messageName叫做 “选择子”(selector)。 选择子与参数合起来称为 “消息” (message)。编译器看到此消息后,将其转换为一条标准的 C语言两数调用,所调用的函数乃是消息传递机制中的核心两数,叫做objc_msgSend,其 “ 原型” ( prototype )如下:
void objc_msgsend(id self, SEL cmd, ...)
这是个“ 参数个数可变的两数” ( variadic function)。能接受两个或两个以上的参数。第一 个参数代表接收者,第二个参数代表选择子(SEL 是选择子的类型),后续参数就是消息中的 那些参数,其顺序不变。选择子指的就是方法的名字。“选择子〞与“方法” 这两个词经常 交替使用。编译器会把刚才那个例子中的消息转换为如下函数:
id returnValue = objc msgSend(someobject, @selector (messageName:), parameter);
objc_msgSend 两数会依据接收者与选择子的类型来调用适当的方法。为了完成此操作, 该方法需要在接收者所属的类中搜寻其“方法列表” (list of methods),如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等找 到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行 “消息转发”(mesage forwarding)操作。
前面讲的这部分内容只描述了部分消息的调用过程,其他“边界情况” (edge case )则 需要交由Objective-C 运行环境中的另 一些函数来处理:
在上一条中提到了消息转发,这一条主要讲解消息转发的过程:
当对象接收到无法解读的消息后,就会启动“消息转发”机制,程序员可经由此过程告诉对象应该如何处理未知消息。
上面这段异常信息是由NSObject 的“doesNotRecognizeSelector:” 方法所抛出的,**此异常表明 : 消息接收者的类型是__NSCFNumber, 而该接收者无法理解名为lowercaseString的选择子。 **本例所列举的这种情况并不奇怪,因为NSNumber 类里本来就没有名为 lowercaseString 的方法。控制台中看到的那个__NSFCNumber 是 为了实现 “ 无缝桥接” (toll- free bridging,第49 条将会详解此技术)而使用的内部类(intermal class) ,配置NSNumber 对象时 也会一 并创建此对象。在本例中, 消息转发过程以应用程序崩溃而告终。
消息转发分为两大阶段,第一阶段先征询接收者,所属的类,看其是否能动态添加方法,处理当前这个“未知的选择子”,这叫做“动态方法解析”。第二阶段涉及“完整的消息转发机制”。
对象在收到无法解读的消息后,首先将调用其所属类的下列类方法:
+ (BOOL)resolveInstanceMethod:(SEL)selector
==该方法的参数就是那个未知的选择子,其返回值为Boolean类型,表示这个类是否能新增一个实例方法用以处理此选择子。==在继续往下执行转发机制之前,本类有机会新增一个处理此选择子的方法,假如尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另一个方法,该方法与“resolveInstanceMethod:”类似,叫做“resolveClassMethod”。
使用这种办法的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面 就可以了。 此方案常用来实现@dynamic属性(参见第6条), 比如说,要访问CoreData框架中NSManagedObjects对象的属性时就可以这么做,因为实现这些属性所需的存取方法 在编译期就能确定。
==当接收者还有第二次机会能处理未知的选择子,在这一步中运行期系统会问它:能不能把这条消息转给其他接收者来处理。==与该步骤对应的处理方法如下:
- (id)forwardingTargetForSelector:(SEL)selector;
方法参数代表未知的选择子,若当前接收者能找到各授对象,则将其返回,若找不到, 就 返 回 nil。 通过此方案,我们可以用“组合”(composition) 来模拟出“多重继承”( multipleinheritance )的某些特性。在一个对象内部,可能还有一系列其他对象,该对象可经由此方法 将能够处理某选择 子的相关内部对象返回,这样的话,在外界看来,好像是该对象亲自处理了这些消息似的。
请注意,我们无法操作经由这 一步所转发的消息。若是想在发送给备援接收者之前先修改消息内容,那就得通过完整的消息转发机制来做了。
如果转发算法已经来到这一步的话,那么唯一能做的就是启用完整的消息转发机制 了。 首先创建NSInvocation 对象,把与尚未处理的那条消息有关的全部细节都封于其中。 此对象包含选择子、目标(target )及参数。在触发NSInvocation 对象时,“ 消息派发系统”
( message-dispatchsystem )将亲自出马,把消息指派给目标对象。
此步骤会调用 下列方法来转发消息:
- (void)forwardInvocation:(NSInvocation*)invocation
这个方法可以 实现得很简单: 只需改变调用目标,使消息在新目标 上得以调用即可。然 而这样实现出来的方法与“备援接收者〞方案所实现的方法等效, 所以很少有人采用这么简 单的实现方式。比较有用的实现方式为: 在触发消息前, 先以某种 方式改变消息内容, 比如 追加另外一个参数,或是改换选择子,等等。
实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法。这样的话, 继承体系中的每个类都有机会处理此调用请求, 直至NSObject。 如果最后调用了NSObject 类的方法,那么该方法还会继而调用“doesNotRecognizeSelector:” 以拋出异常, 此异常表明选择 子最终未能得到处理。
消息转发全过程:
黑盒机制,不少人都有理解,就是一个方法你知道怎么用,如何用,并且常常都能使用它但是对于该方法的内部机制却不知道是什么样子,这一条就介绍了如何利用现有的方法去调试黑盒方法变成我们所能用的。
对于OC对象收到消息之后使用何种方法在运行期进行解析,与给定的选择子名称相对应的方法是可以在运行期改变的! 这是OC语言强大的特性,我们就可以不知道源代码并且不需要通过继承子类复写方法来改变某个类本身的功能,这样一来新功能能够在本类实例化的所有实例里面生效,而不是仅限于覆写了相关方法的那些子类的实例。叫做方法调配。
类的方法列表会把选择子的名称映射到相关的方法实现上面,使得动态消息派发系统能够根据此找到应该调用的方法,这些方法均以函数指针的形式表示,这种指针叫做IMP,原型如下:
id (*IMP) (id, SEL, ...)
例如NSString类可以响应自己所带的选择子,它们的关系类似于key 和 Value .
OC运行期提供的几个方法都能操作这张表,开发者可以向其中新增选择子,也可以改变某选择子所对应的方法实现,还可以交选择子所映射到的指针,我们可以经过操作改变类的方法表。
也就是说我们无需修改子类覆写方法,只需要修改方法表的布局,就会反映到程序所有NSString实例上。
我们添加新功能的本质就是修改之前的方法实现,也就是重写一个方法实现然后实现交换。
方法交换实例
一般情况下,应该指明消息接收者的具体类型,这样的话,如果向其发送了无法解读的消息,那么编译器就会产生警告信息。而类型为id的对象则不然,编译器假定它能响应所有信息。
每个OC对象实例都是指向某块内存数据的指针,所以在声明变量时,类型后面要跟一个“”字符。
描述Objective-C对象所用的数据结构定义在运行期程序库的头文件里,id类型本身也在定义在这里:
typedef struct objc_object {
Class isa;
} *id;
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;
};
此结构体存放类的“元数据”。其中的super_class,它定义了本类的超类。类对象所属的类型(也就是isa指针所指向的类型)是另一个类,叫做“元类”(metaclass)。并且每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”。
“isMemberOfClass:”能够判断出对象是否为某个特定类的实例(只有与其出创建的类型相同时才返回YES),而“isKindOfClass:”则能够判断出对象是否为某类或其派生类的实例。