美文网首页
Effective Objective-C 2.0 学习笔记 第

Effective Objective-C 2.0 学习笔记 第

作者: TAsama | 来源:发表于2018-12-21 08:44 被阅读5次

下载地址:Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法

对象、消息、运行期

6. 理解“属性”这一概念

当我们在类中声明实例变量时,编译器会在编译期把实例变量距离该对象地址的距离确定下来,即实例变量在内存中的位置相对于该对象地址的偏移量。这样一来,如果类的定义发生了改变,但有没有经过重新编译,实例变量的地址所对应的内容可能就不正确了。也就是我们说的,当使用旧的代码库链接到了新的代码库时的不兼容现象。

在类中新增另一个实例变量前后的数据分布图
各种语言对于这种情况都采取了不同的解决方案,OC的解决方案是,将实例变量看做一种保存内存地址偏移量的“特殊变量”,交给类对象去保管。实例的偏移量会在运行期动态查找。这样一来,当类改变时,实例变量也改变了。基于这种机制,我们才能在分类和扩展中添加新的实例变量。
@dynamic关键字告诉@property不要自动创建属性和存取方法

7. 在对象内部尽量直接访问实力变量

!!在对象内部,读取尽量直接访问,设置则调用属性方法(懒加载除外)
!!在初始化方法“init”与“dealloc”中应该直接访问变量

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

OC中“NSObject”协议定义了了“isEqual”方法和“hash”方法,“isEqual”方法返回YES且“hash”方法返回的值也相同,系统才会认为两个对象是同一个值。
比如说在set集合中,系统便是根据hash值来判断对象是否相同,如果hash值相同,则认为是相同的对象,则不会重复添加。
我们可以自己编写hash码方法的返回值,但是要注意算法的简便性和低碰撞性

等同判定的执行深度

数组的isEqualToArray方法判断的流程是,先判断数据元素的个数是否相同,再对每一个元素调用isEqual方法来判断每个元素是否相同。

9. 以“类族模式”隐藏实现细节

通过继承的方式架空父类,在构造方法中返回子类,这种方式实现类抽象类的模式,比如UIbutton,当我们调用:

+ (UIButton *)buttonWithType:(UIButtonType)type;

方法时,系统内部就会返回我们对应Type的子类,隐藏了具体的实现细节。
这样的方式称之为“工厂模式”,是创建类族的办法之一。

Cocoa里的类族

大部分collection类都是类族
对Cocoa中的类族新增子类需要遵守以下规则:

  • 子类应该继承自类族中的抽象基类
  • 子类应该定义自己的数据存储方式
  • 子类应当重写父类文档中知名需要重写的方法

10. 在既有类中使用关联对象寻访自定义数据

使用关联对象的三个方法:

void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy);

id _Nullable objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key);

void objc_removeAssociatedObjects(id _Nonnull object);

在设置关联对象的值时,通常使用静态全局变量作为键。
使用关联对象需在万不得已时使用,因为这种做法通常会引起难以查找的bug。

11. 理解objc_msgSend的作用

在C语言中,函数调用使用“静态绑定”的方式,在编译期决定运行时所调用的函数。

void printHello() {
    
    printf("Hello, world!");
}

void printGoodBy() {
    
    printf("Goodbye, world!");
}

void doTheThing(int type) {
    
    void (*fnc)();
    if (type == 0) {
        
        fnc = printHello();
    } else {
        
        fnc = printGoodbye();
    }
    return 0;
}

而当代码如下:

void printHello() {
    
    printf("Hello, world!");
}

void printGoodBy() {
    
    printf("Goodbye, world!");
}

void doTheThing(int type) {
    
    void (*fnc)();
    if (type == 0) {
        
        fnc = printHello;
    } else {
        
        fnc = printGoodbye;
    }
    fnc();
    return 0;
}

这种情况时,就需要使用“动态绑定”了。这时的函数调用指令fnc无法将带调用函数的地址硬编码,只能在运行期读取出来。
当我们调用OC方法时:

id returnValue = [someObject messageName:parameter];

编译器会将他转化成:

id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);

objc_msgSend函数会一句接受者与选择子的类型来调用适当的方法,在接收者所属的类中搜索“方法列表”,若找不到同名方法,则向上在其父类中寻找,若还是找不到,就会执行“消息转发”(message forwarding)操作(见第七条)。
每个类中都会有一张函数表格,存放函数指针,当objc_msgSend方法找到对应的函数时,就会去调用该函数指针。
OC中所有消息都会由“动态消息派发系统”来处理,系统会查出对应的方法,并执行其代码。

12. 理解消息转发机制

在上一条中提到过“消息转发”,当对象在收到无法解读的消息之后,便会执行消息转发。
在OC中,我们向类发送了无法解读的消息并不会报错,因为在运行期是可以继续向类中添加方法的,所以编译时编译器并不能确定这条消息是否能被实现。
消息转发分为两大阶段:

  1. 征询接受者所属的类,看起能否动态添加方法(动态方法解析)
  2. 这个阶段所涉及“完整的消息转发机制”,若第一阶段已经执行完毕,则接收者就无法再通过动态新增的方法来响应消息了,此时,系统会请求接收者采取其他手段来处理消息相关的方法调用。
    2.1 请接收者寻找能够处理该消息的对象,如果有,则将消息转发给他。
    2.2 启动完整的消息转发机制,系统把与消息有关的全部细节都封装到NSInvocation对象中,令接收者解决当前未处理的消息,这是最后一次请求。
动态方法解析

对象在收到无法解读的消息后,将调用:

+ (BOOL)resolveInstanceMethod:(SEL)sel

其中selector解释我们的未知SEL,其返回值表示这个类能否新政一个实例方法用以处理次SEL
如果尚未实现的方法不是一个实例方法,而是一个类方法,则系统会调用

+ (BOOL)resolveClassMethod:(SEL)sel

实例:

#import "SelectorHandleManager.h"
#import <objc/runtime.h>
@implementation SelectorHandleManager

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    const char *type = method_getTypeEncoding(class_getInstanceMethod([self class], @selector(haha:)));
    NSLog(@"%s", type);
    class_addMethod([self class], sel, method_getImplementation(class_getInstanceMethod([self class], @selector(haha:))), type);
    
    return YES;
}

- (void)haha:(id)value {
    
    NSLog(@"%@", value);
}
备源接收者

当动态添加方法无法解决问题的时候,接收者还有第二次机会来处理未知SEL,运行期系统会询问是否转发消息给其他接收者,通过下面的方法。

- (id)forwardingTargetForSelector:(SEL)aSelector

通过返回改变的目标使消息在新的目标上得以实现。

消息转发全流程

执行到这一步,唯一能做的就是启用完整消息转发机制。

- (void)forwardInvocation:(NSInvocation *)anInvocation

NSInvocation对象:

/*  NSInvocation.h
    Copyright (c) 1994-2018, Apple Inc. All rights reserved.
*/

#import <Foundation/NSObject.h>
#include <stdbool.h>

@class NSMethodSignature;

NS_ASSUME_NONNULL_BEGIN

NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available")
@interface NSInvocation : NSObject {
@private
    void *_frame;
    void *_retdata;
    id _signature;
    id _container;
    uint8_t _retainedArgs;
    uint8_t _reserved[15];
}

+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;

@property (readonly, retain) NSMethodSignature *methodSignature;

- (void)retainArguments;
@property (readonly) BOOL argumentsRetained;

@property (nullable, assign) id target;
@property SEL selector;

- (void)getReturnValue:(void *)retLoc;
- (void)setReturnValue:(void *)retLoc;

- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;

- (void)invoke;
- (void)invokeWithTarget:(id)target;

@end


#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE))
#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_5

enum _NSObjCValueType {
    NSObjCNoType = 0,
    NSObjCVoidType = 'v',
    NSObjCCharType = 'c',
    NSObjCShortType = 's',
    NSObjCLongType = 'l',
    NSObjCLonglongType = 'q',
    NSObjCFloatType = 'f',
    NSObjCDoubleType = 'd',
    NSObjCBoolType = 'B',
    NSObjCSelectorType = ':',
    NSObjCObjectType = '@',
    NSObjCStructType = '{',
    NSObjCPointerType = '^',
    NSObjCStringType = '*',
    NSObjCArrayType = '[',
    NSObjCUnionType = '(',
    NSObjCBitfield = 'b'
} API_DEPRECATED("Not supported", macos(10.0,10.5), ios(2.0,2.0), watchos(2.0,2.0), tvos(9.0,9.0));

typedef struct {
    NSInteger type API_DEPRECATED("Not supported", macos(10.0,10.5), ios(2.0,2.0), watchos(2.0,2.0), tvos(9.0,9.0));
    union {
        char charValue;
    short shortValue;
    long longValue;
    long long longlongValue;
    float floatValue;
    double doubleValue;
    bool boolValue;
    SEL selectorValue;
    id objectValue;
    void *pointerValue;
    void *structLocation;
    char *cStringLocation;
    } value API_DEPRECATED("Not supported", macos(10.0,10.5), ios(2.0,2.0), watchos(2.0,2.0), tvos(9.0,9.0));
} NSObjCValue API_DEPRECATED("Not supported", macos(10.0,10.5), ios(2.0,2.0), watchos(2.0,2.0), tvos(9.0,9.0));

#endif
#endif

NS_ASSUME_NONNULL_END

在这里我们可以修改参数,返回值等,然后发送给新的接收者


消息转发

13. 用“方法调配技术”调试“黑盒方法”

Method isEqualM = class_getInstanceMethod([self class], @selector(my_isEqualToString:));
Method my_isEqualM = class_getInstanceMethod([self class], @selector(my_isEqualToString2:));
    
method_exchangeImplementations(isEqualM, my_isEqualM);

就是我们常说的方法交换。
这里有一个坑,当我们做方法交换时,取出来的实例方法可能并不是该类自己的,很有可能是从他的父类那里继承过来的,这样一来我们交换方法实际上是将父类方法与子类的自定义方法进行了交换。这会导致所有由该父类所派生的子类的该方法都被交换,同时,因为只有你所定义的这一个子类拥有交换后的方法,所以将导致其他子类找不到对应的交换后的方法而崩溃。做方法交换时,一定要去其父类进行交换,也就是在分类中进行这一操作。

14. 理解“类对象”的用意

isa指针:is a xxx的意思,表示对象是一个xxx。
可以看到Class中也有一个isa指针,类也是一个对象。


SomeClass实例所属的“类集成体系”

super_class 指针确立了继承关系,而isa指针描述了实例所属的类。通过这样的布局关系就可以执行“类型信息查询”,查出对象能否响应方法,是否遵从协议,看出对象出于集成体系的那一部分。

在类集成体系中查询类型信息
  1. isMemberOfClass: 判断对象是否是某个特定的实例
  2. isKindOfClass: 判断对象是否是某类或其派生类的实例

类对象是“单例”,因此每个类的class仅有一个实例,我们可以用“==”来精确判断对象时是否为某类的实例。

if ([object class] == [EOCSomeClass class]) {
    //do something
}

不过我们还是要尽量使用“isKindOfClass:”方法,因为当我们使用遵循代理的代理对象时,class指针往往指向的事代理对象本身,即:“NSProxy"的子类,但是改用isKindOfClass方法,代理对象会转发消息到执行的对象本身,就能够判断其真实身份。

其他章节:

Effective Objective-C 2.0 学习笔记 第一章
Effective Objective-C 2.0 学习笔记 第三章

相关文章

网友评论

      本文标题:Effective Objective-C 2.0 学习笔记 第

      本文链接:https://www.haomeiwen.com/subject/aymjkqtx.html