Effective oc 2.0 第二章学习--对象、消息、运行期

发布时间:2024年01月21日

前言

在用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(可声明),那它就是原子的;

在并发编程中,如果某操作具备整体性,也就是说系统其它部分无法观测到其中步骤所生成的临时结果,那么该操作就是“原子的”,也可以说该操作具备原子性 ;

读写权限

  • readwrite 属性同时拥有获取方法和设置方法;
  • readonly 属性只用有获取方法,但我们也可以对外公开为只读属性,然后在class-continuation中将其重新定义为读写属性 ;

内存管理语义

这个特质只会影响“设置方法” ;

  • assign sette方法只会执行针对“纯量类型”的简单赋值操作,如CGFloat和NSInteger等
  • strong ,定义了一种拥有关系,设置方法会先保留新值释放旧值
  • weak 定义了一种非拥有关系,设置方法既不保留新值也不保留旧值,该特质和asssign类似,,但当属性所指对象遭到摧毁时,属性值不会自动清空 ;
  • unsafe——unretained 此特质的语义和assign相同,但适用于对象类型,表达非拥有关系,当把目标对象遭到摧毁时,属性值不会自动清空 ;
  • copy 和strong类似,但设置方法并不保留新值,但会将其拷贝,拷贝产生的对象是不可变的 ;

方法名

可以通过一下特质来指定方法的方法名 :

  • getter=< name> ;
  • setter = < name>
@property (nonatomic, getter=isOn) Bool on ;

注意的点

通过上面这些特质,可以微调编译器所合成的存取方法,但如果自己要来实现这些存取方法的时候也要保证能有这些特质的特性 ;

atomic和nonatomic的区别:具备atomic特质的获取方法会通过锁定机制来确保其操作的原子性。若不枷锁的话,当其中一个线程修改某属性值时,另一个线程也许会突然闯入,把尚未修改好的属性值读取出来。
在开发iOS程序时,其中所有属性的声明几乎都为nonatomic,因为iOS使用同步锁的开销较大且原子性也不能保证“线程安全” ;

要点

  • 可以用@property语法来定义对象中封装的数据
  • 通过“特质”来指定存储数据所需的正确语义
  • 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义 ;
  • 开发区iOS程序时应该使用nonatomic属性,因为atomic属性会严重影响性能 ;

在对象内部尽量直接访问实例变量

在对象之外总是通过属性来做,但除了几种特殊情况外,在对象内部还是尽量直接访问实例变量 ;

对于以下的类

@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] ;
}

这两种写法的区别 :

  • 不经过oc的“方法派发”,所以直接访问实例变量的速度要快 ,代码直接访问保存对象实例变量的·那块内存 ;
  • 直接访问实例变量时,不会调用其“设置方法”,绕开了为相关属性所定义的“内存管理语义”,也就是特质,
  • 如果直接访问实例变量,那么不会出发“键值观测(KVO)” 通知,这样做会不会有问题还取决于具体的对象行为 ;
  • 通过属性来访问可以在setter和getter方法中设置断点来监控该属性的调用者和访问时机 ;

注意的点

  • 在初始化方法中设置属性值,总是直接访问实例变量,因为如果用方法的话,子类往往会覆盖方法造成出错 ;
  • 在惰性初始化(和懒加载的思想类似吧),,必须通过getter方法来访问属性,负责永远不会发生初始化,往往用于要创建一个成本较高的类的对象 ;
- (EOCBrain*)brain {
    if (!_brain) {
        _brain = [Brain new] ;
    }
    return _brain ;
}

要点

  • 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应该通过属性来写
  • 在初始化方法及dealloc方法中,总
  • 是应该直接通过实例变量来读写数据
  • 有时会使用惰性初始化技术配置某份数据,在这种情况下,需要通过属性来读取数据 ;

理解“对象等同性”这一概念

等同性的比较方法

  • == 是指针性的比较,往往必须时相同的对象和内存
  • isequaltostring 是NSString类重写过的isEqualto,,虽然和isEqualto的结果相同,但isEqual要执行额外的步骤,所以要慢一些 ;
  • NSObject协议中判断等同性的两个关键方法:
 - (BOOL)isEqual : (id) object ;
 - (NSUInteger) hash ;
  • 这两个方法的默认实现是仅当指针值即内存地址相同时,两个对象才相等;当要注意,当isEqual相等时,hash判断一定相等,但hash相等时,isequal不一定相等 ;

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,这就叫做“深度等同性判断”

是否需要再等同性方法中检测全部字段取决于受测对象,只有类的编写者才可以确定两个对象实例在何种情况下应判定为相等 ;

理解objc_msgSend的作用

在对象上调用方法,用oc的属于来说叫做“传递消息”,消息有”名称“或”选择子“,可以接受参数,而且可能还有返回值

  • c语言使用的是”静态绑定“,在编译期就能决定运行时所应调用的函数。如下
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两个函数了,函数的地址实际上是硬编程在指令中的。

  • oc中采用的是动态绑定的方法,要调用的函数知道运行期才能确定。即带调用的函数无法硬编码在指令当中,要在运行期读取出来 ;
    下面是c语言的动态绑定:
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_masgSend_stret ,如果待发送的消息要返回结构体,可交由此函数处理 ;
  • objc_msgSend_fpret 如果消息返回的是浮点数,那么可交由此函数处理
  • objc_msgSendSuper 如果要给超类发消息,就交由这个函数处理,如[super message : parameter] ;

objc_msgSendd等函数一旦找到应该调用的方法实现后,就会跳转过去;用类和选择子来命名是为了解释其工作原理,每个类都有一个表格,而选择子的名称是查表时用的键;原型的样子和objc_msgSend函数很像,这是为了利用”尾调用优化“技术,令”跳至方法实现“这一操作变得简单些 ;

如果某函数的最后一项操作是调用另一个函数,那么就可以运用”尾调用优化“技术。即编译器会生成调转至另一函数的指令码,而且不会向跳用的堆栈中推入新的“栈帧”。只有当某函数的最后一个操作仅仅是调用其他函数而不会将其返回值另作他用时,才能执行“尾调用优化。这项优化对objc——msgSend非常关键,如果不这么做的话,那么每次调用oc方法之前,都需要为调用它而准备”栈帧“,若是不优化,还会过早的发生”栈溢出“ ;

要点

  • 消息由接收者,选择子及参数构成,给某对象”发送消息“也就相当于在该对象上”调用方法“
  • 发给某对象的全部消息都要由”动态消息派发系统“来处理,该系统会查出对应的方法,并执行其代码 ;

理解消息转发机制

当对象收到无法解读的消息之后,如我们调用的方法没有实现,在编译期向类发送了其无法解读的消息并不会报错,因为运行期可以继续向类中添加方法。当对象接受到无法解读的消息后,就会启动“消息转发”,程序员可经此过程告诉对象应该如何处理未知消息。

如书上的一个例子
在这里插入图片描述
这段异常提示时————NSCFNumber接收到了无法理解的lowercaseString方法,在这个例子中,消息转发过程会以程序崩溃告终,但开发者可以在编写自己的类时,于转发过程中设置挂钩,用以执行预定的逻辑,而不是程序崩溃 ;

消息转发分为两个阶段:

  • 先征询接收者所属的类,看是否能动态接收方法,以处理当前这个“未知的选择子”,这叫动态方法解析
  • 第二阶段涉及“完整的消息转发机制”,如果第一阶段未能解决问题,此时运行期系统会请求接收者以其他手段来处理和消息相关的方法调用,首先,请接收者看看有没有其他对象能处理这条信息,若有,运行期系统会吧消息转发给那个对象,消息转发过程结束;若没有成功,则启动完整的消息转发机制,运行期系统会把消息有关的全部细节都封装到NSInvocation对象中,再给接收者一次机会,令其设法解决当前的这条消息 ;

动态方法解析

对象在收到无法解读的消息后,会调用下面的类方法

+ (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)的哪一部分。

文章来源:https://blog.csdn.net/m0_74217598/article/details/135626954
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。