在用oc等面向对象的语言编程时,对象就是“基本构造单元”,开发者可以通过对象来存储和传递数据。在对象间传递数据并执行任务的过程就叫做“消息传递”。
当应用程序运行起来以后,为其提供相关支持的代码叫做“objective-c运行环境”。
“属性”是oc的一项特性,用于封装对象中的数据。
oc对象通常会把所需要的数据保存为各种实例变量。
变量的访问通过sette方法设置,getter方法获取;
对于属性的实例变量可以通过点语法更加便捷的实现数据的存取;
如果直接声明使用实例变量,如下:
@interface EOCPerson : NSObject {
@public
NSString* _firstName ;
NSString* _lastName ;
@private
NSString* _someInternalData;
}
@end
但这种方法有个很大的问题,用这种方法添加的实例变量,对象布局在编译期就已经确定了,其原因在于这种方法的实现逻辑是每次遇到这个对象的_firstName实例变量的代码是会自动替换成“偏移量”,这个“偏移量”代表着该变量距离存放对象的内存地址有多远,假设在编译后,我们为这个对象再加一个实例变量,如下
@interface EOCPerson : NSObject {
@public
NSString* _dateOfBirth ;
NSString* _firstName ;
NSString* _lastName ;
@private
NSString* _someInternalData;
}
@end
那这个时候,_firstName在编译阶段获取的偏移量则指向了_dataOfBirth;
如果代码库中代码使用了旧的类定义,而与之链接的代码中还有新的类定义,那么运行时就会出现不兼容现象 ;
oc对这个问题的解决方法:把实例变量当作一种存储偏移量所用的“特殊变量”,交由“类对象”管理,这就是稳固的“应用程序二进制接口”ABI,ABI的实现也就意味着我们可以在class-continuation中或实现文件中定义实例变量,可以实现封装和保护;
另一种解决方法就是不直接访问实例变量,通过存取方法来实现数据的改变和存取。@property语法意味着编译器会自动写出一套存取方法和对应名称的实例变量;
@interface EOCPerson : NSObject
@property NSString* firstName ;
@property NSString* lastName ;
@end
等效于
@interface EOCPerson : NSObject
- (NSString*) firstName ;
- (void) setFirstNmae : (NSString*) firstName ;
- (NSString*) lastNmae ;
- (void)setLastName : (NSString*)lastName ;
@end
属性可以使用更加方便的点语法,且于直接调用存取方法等价,如果使用了属性,编译器会自动便携访问这些属性所需的方法,这个过程叫做“自动合成”,这个过程在编译期实现,源代码中是找不到的,自动合成的过程中还会适当的添加对应名称加下划线前缀的实例变量,实例变量的名称也可以在实现部分中通过@stnthesize方法来指定实例变量的名称:
@implementation EOCPerson
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
如果希望自己实现属性的存取方法,我们可以通过重写对应的存取方法,或者使用@dynamic关键字,让编译器不自动合成这个属性的方法和实例变量;
@interface EOCPerson : NSObject
@property NSString* firstName ;
@property NSString* lastName ;
@end
@implementation EOCPerson
@dynamic firstName, lastName ;
@end
属性的各种特质设定也会改变编译器生成的存取方法,属性的特质分为四类:
如果属性具备nonatomic,则不使用同步锁,反正则默认使用atomic(可声明),那它就是原子的;
在并发编程中,如果某操作具备整体性,也就是说系统其它部分无法观测到其中步骤所生成的临时结果,那么该操作就是“原子的”,也可以说该操作具备原子性 ;
这个特质只会影响“设置方法” ;
可以通过一下特质来指定方法的方法名 :
@property (nonatomic, getter=isOn) Bool on ;
通过上面这些特质,可以微调编译器所合成的存取方法,但如果自己要来实现这些存取方法的时候也要保证能有这些特质的特性 ;
atomic和nonatomic的区别:具备atomic特质的获取方法会通过锁定机制来确保其操作的原子性。若不枷锁的话,当其中一个线程修改某属性值时,另一个线程也许会突然闯入,把尚未修改好的属性值读取出来。
在开发iOS程序时,其中所有属性的声明几乎都为nonatomic,因为iOS使用同步锁的开销较大且原子性也不能保证“线程安全” ;
在对象之外总是通过属性来做,但除了几种特殊情况外,在对象内部还是尽量直接访问实例变量 ;
对于以下的类
@interface EOCPerson : NSObject
@property NSString* firstName ;
@property NSString* lastName ;
- (NSString*)fullName ;
- (void)setFullName : (NSString*) fullName ;
@end
其实现部分有以下两种实现
@implementation EOCPerson
- (NSString*) fullName {
return [NSString stringWithFormat:@"%@ %@",self.firstName,self.lastName];
}
- (void)setFullName:(NSString *)fullName {
NSArray* componentd = [fullName componentsSeparatedByString:@" "] ;
self.firstName = [componentd objectAtIndex:0] ;
self.lastName = [componentd objectAtIndex:1] ;
}
@end
- (NSString*) fullName {
return [NSString stringWithFormat:@"%@ %@",_firstName,_lastName];
}
- (void)setFullName:(NSString *)fullName {
NSArray* componentd = [fullName componentsSeparatedByString:@" "] ;
_firstName = [componentd objectAtIndex:0] ;
_lastName = [componentd objectAtIndex:1] ;
}
这两种写法的区别 :
- (EOCBrain*)brain {
if (!_brain) {
_brain = [Brain new] ;
}
return _brain ;
}
等同性的比较方法
- (BOOL)isEqual : (id) object ;
- (NSUInteger) hash ;
isEqual方法的实现 :
- (BOOL)isEqual:(id)object {
if (self == object) return YES;
if ([self class] != [object class]) return NO;
EOCPerson* otherPerson = (EOCPerson*) object ;
if (![_firstName isEqualToString:otherPerson.firstName])
return NO;
if (![_lastName isEqualToString:otherPerson.lastName])
return NO;
if (_age != otherPerson.age)
return NO;
return YES;
}
hash讲了三种实现方式,我就写一下最后一种哈希的计算
- (NSUInteger)hash {
NSUInteger firsthash = [_firstName hash] ;
NSUInteger lasthash = [_lastName hash] ;
NSUInteger ageHash = _age ;
return firsthash ^ lasthash ^ ageHash;
}
NSArray的isEqualTpArray以及NSDictionary的isEqualToDictionary ;如果相比较的对象不是数组或字典,那么两个方法各自抛出异常。但这两种方法和NSString的类似,不做强类型判断,可以大大提高检测速度 ;
对于NSArray的等同性判断方法来说,其检测时先看两个数组所含的对象个数是否相等,若相等再对应位置上的对象用isEqual,这就叫做“深度等同性判断”
是否需要再等同性方法中检测全部字段取决于受测对象,只有类的编写者才可以确定两个对象实例在何种情况下应判定为相等 ;
在对象上调用方法,用oc的属于来说叫做“传递消息”,消息有”名称“或”选择子“,可以接受参数,而且可能还有返回值
void printHello () {
printf("Hello,world! \n") ;
}
void printGoodbye () {
printf("Goodbye,world! \n") ;
}
void doTheThing (int type) {
if (type == 0) {
printHello() ;
} else {
printGoodbye() ;
}
return 0;
}
如果不考虑内联,那么编译期在编译代码是就已经知道程序中有printHello和printGoodbye两个函数了,函数的地址实际上是硬编程在指令中的。
void printHello () {
printf("Hello,world! \n") ;
}
void printGoodbye () {
printf("Goodbye,world! \n") ;
}
void doTheThing (int type) {
void (*fnc) () ;
if (type == 0) {
fnc = printHello() ;
} else {
fnc = printGoodbye() ;
}
fnc () ;
return 0 ;
}
在oc中,如果向某个对象传递消息,那就会使用动态绑定机制决定需要调用的方法,在底层,所有方法都是c语言函数,对象接受信息后,该如何调用那个方法则完全取决于运行期,甚至可以在程序运行时改变。
给对象发送信息
id returnValue = [someObject messageName:paramenter] ;
//someObject叫做接收者,messageName叫做选择子,选择子与参数和起来叫做消息。
编译期看到此消息后,将其转换为一条标准的c语言函数调用,所调用的函数叫做objc_msgSend,
void objc_msfSend (id self,SEL cmd,...)
id returnValue = objc_msfSend(someObject, @selector(messageName:),parameter) ;
objc_msfSend函数会依据接收者与选择子的类型来调用适当的方法,为了完成此操作,该方法需要在接收者所属的类中寻找其“方法列表”,如果能找到与选择子名称相符的方法,就跳至实现代码。若找不到,那就沿着继承体系继续向上查找,如果最终还是找不到相符的方法,那就执行“消息转发”操作 ;
虽然看起来想要调用一个方法似乎需要很多步骤,但objc_msfSend会将匹配到的结果缓存进“快速映射表”中,虽然不像静态绑定的函数调用那么迅速,但也不会慢多少。
上面这部分内容只描述了部分消息的调用,一些“边界情况”则要另一些函数来处理:
objc_msgSendd等函数一旦找到应该调用的方法实现后,就会跳转过去;用类和选择子来命名是为了解释其工作原理,每个类都有一个表格,而选择子的名称是查表时用的键;原型的样子和objc_msgSend函数很像,这是为了利用”尾调用优化“技术,令”跳至方法实现“这一操作变得简单些 ;
如果某函数的最后一项操作是调用另一个函数,那么就可以运用”尾调用优化“技术。即编译器会生成调转至另一函数的指令码,而且不会向跳用的堆栈中推入新的“栈帧”。只有当某函数的最后一个操作仅仅是调用其他函数而不会将其返回值另作他用时,才能执行“尾调用优化。这项优化对objc——msgSend非常关键,如果不这么做的话,那么每次调用oc方法之前,都需要为调用它而准备”栈帧“,若是不优化,还会过早的发生”栈溢出“ ;
当对象收到无法解读的消息之后,如我们调用的方法没有实现,在编译期向类发送了其无法解读的消息并不会报错,因为运行期可以继续向类中添加方法。当对象接受到无法解读的消息后,就会启动“消息转发”,程序员可经此过程告诉对象应该如何处理未知消息。
如书上的一个例子
这段异常提示时————NSCFNumber接收到了无法理解的lowercaseString方法,在这个例子中,消息转发过程会以程序崩溃告终,但开发者可以在编写自己的类时,于转发过程中设置挂钩,用以执行预定的逻辑,而不是程序崩溃 ;
消息转发分为两个阶段:
对象在收到无法解读的消息后,会调用下面的类方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
该方法的参数就是那个未知的选择子,返回值表示这个类是否能新增一个实例方法,假如实现的不是实例方法而是类方法 ,那么运行期系统就会调用resloveClassMethod ;
使用这种方法的前提:相关方法的实现代码已经写好,只等运行的时候动态插在类里面就可以了。
下面代码演示了如何用resloveInstanceMethod:来实现@dynamic属性:
id authDictionaryGetter (id self, SEL _cmd) ;
void authDictionarySetter (id self, SEL _cmd,id value) ;
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString* selectorString = NSStringFromSelector(selector) ;
if (/* selector is from a @dynamic property */) {
if ([selectorString hasPrefix:@"set"]) {
class_addMethod (self, selector,(IMP)authDictionarySetter,"v@ : @") ;
} else {
class_addMethod (self,selector,(IMP)authDictionaryGetter , "@ @ :") ;
}
return YES;
}
return [super resolveInstanceMethod:selecter];
}
当前接收者第二次处理未知的接收子,运行期系统会问它:能不能把这条信息转给其他接收者处理:
- (id)forwardingTargetForSelector:(SEL)aSelector ;
方法参数代表未知的接收子,若能找到备援对象,则将其返回,否则返回nil;通过这个可以用“组合”模拟出“多重继承”的某些特性。
前两步都没能解决的话,就执行完整的消息转发;首先创建NSInvocation对象,把尚未处理的那条消息有关的全部细节封装于其中。此对象包括选择子,目标及参数。“消息派发系统”会把消息指派给目标对象:
- (void)forwardInvocation : (NSInvocation*) invocation
实现此方法时,如果发现需要调用超类的方法,继承体系中的每个类都有机会处理此调用请求,直到NSObject。如果最后调用了NSObject类的方法,那么该方法还会继而调用“doesNotRecognizeSelector:”以抛出异常,此异常表明选择子最终未能得到处理。
步骤越往后,代价也越大,如果之后这个类的对象又接收到同名选择子,那么无需启动消息转发流程。
OC对象收到消息之后,究竟会调用何种方法需要在运行期才能解析出来。与给定的选择子名称相对应的方法也可以在运行期改变,不需要通过继承子类来覆写方法就能改变这个类本身的功能。这样一来,新功能将在本类的所有实例中生效,而并不仅限于覆写了相关方法的那些子类的实例。此方案经常被称为“方法调配”(method swizzling)。
类的方法列表会把选择子的名称映射到相关的方法实现之上,使得“动态消息派发系统”能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做IMP,其原型如下:
id (*IMP)(id, SEL, ...)
NSString类可以响应lowercaseString、uppercaseString、capitalizedString等选择子。这张映射表中的每个选择子都映射到了不同的IMP之上,OC运行期系统提供的几个方法都能够用来操作这张表,开发者可以向其中新增选择子,也可以改变某选择子所对应的方法实现,还可以交换两个选择子所映射到的指针
互换两个已经写好的方法实现:
可以使用下列函数:
void method_exchangeImplementations(Method m1, Method m2)
此函数的两个参数表示待交换的两个方法实现,而方法实现则可以通过下列函数获得:
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
像这样直接交换两个已经实现了的方法意义不大。但是,可以通过这一手段来为既有的方法实现增加新功能
对象类型并非在编译期就绑定好了,而是要在运行期查找。而且,还有个特殊的类型叫id,它能指代任意的OC对象类型。一般情况下,应指明消息接收者的具体类型,这样的话,如果向其发送了无法解读的消息,那么编译器就会产生警告信息。而对于id类型的对象,编译器假定它能响应所有消息。
“在运行期检视对象类型”这一操作也叫做“类型信息查询”(introspection,内省),这个强大而又有用的特性内置于Foundation框架的NSObject协议里,凡是由公共根类(common root class,即NSObject与NSProxy)继承而来的对象都要遵从此协议。在程序中不要直接比较对象所属的类,明智的做法是调用“类型信息查询方法”。
每个OC对象实例都是指向某块内存数据的指针。
NSString *pointerVariable = @"Some string";
id genericTypeString = @"Some string";
上面这种定义方法与用NSString *定义的唯一区别是:如果在定义时制定了具体类型,那么在该类实例上调用其所没有的方法时,编译器会发出警告信息。
typedef struct objc_object {
Class isa;
} *id;
由此可见,每个对象结构体的首个成员是Class类的变量。该变量定义了对象所属类,通常称为“isa”指针。例如,刚才的例子中所用的对象“是一个”(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;
};
此结构体存放类的“元数据”(metadata),例如类的实例实现了几个方法,具备多少个实例变量等信息。此结构体的首个变量也是isa指针,这说明了Class本身亦为OC对象。结构体里面还有个变量叫做super_class,它定义了本类的超类。类对象所属的类型(也就是isa指针所指向的类型)是另外一个类,叫做“元类”(metaclass)(初步了解元类),用来表述对象本身所具备的元数据。“类方法”就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”。
super_class指针确立了继承关系,而isa指针描述了实例所属的类。通过这张图即可执行“类型信息查询”。我们可以查出对象是否能响应某个选择子,是否遵从某项协议,并且能看出此对象位于“类继承体系”(class hierarchy)的哪一部分。