Effective Objective-C 2.0 读书笔记二(

作者: Miridescent | 来源:发表于2016-06-21 13:47 被阅读82次

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

    注意关键词“关联对象”,就是把两个对象关联起来,例如把对象B关联到对象A上面,这样只要我们知道对象A,就能通过关联方法拿到对象B,这是一个很有用的特性,可以帮助我们携带一些数据,以及一些信息。如果通俗一点理解的话可以把对象A理解成一个字典,对象B是存放在对象A中的一个对象,通过对应的key值就能拿到对应的对象B。
    下面是关联对象对应的三个方法(只有三个方法):
    1.通过给定的键值和关联策略对某对象设置关联对象

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

    第一个参数,被关联对象,对应上面的对象A。
    第二个参数,键值,通过参数形式我们知道,这是一个指针,一般我们在定义这个指针的时候使用静态全局变量,因为这是一个“不透明指针”(自行查找什么是“不透明指针”)。
    第三个参数,关联的对象,对应上面的对象B。
    第四个参数,关联策略,是一个枚举值,对应定义属性时候添加的属性特性,用于维护内存管理,下表列出对应关系:

    关联类型 等效的属性特性
    OBJC_ASSOCIATION_ASSIGN assign
    OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic, retain
    OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic, copy
    OBJC_ASSOCIATION_RETAIN retain
    OBJC_ASSOCIATION_COPY copy

    2.通过给定的键值取出相应的关联对象

    id objc_getAssociatedObject(id object, const void *key)
    

    第一个参数,被关联的对象,对应对象A。
    第二个参数,键值。
    返回值,关联对象,对应对象B。
    3.移除被关联对象的所有关联对象

    void objc_removeAssociatedObjects(id object)
    

    参数,被关联对象,对应对象B。
    上面就是关联对象的所有方法,但是在用的时候需要注意,关联对象应该被我们列在最后的选择方案,因为关联对象之间的关系没有正式的定义,其内存管理是在设置关联的时候才定义的,而不是在接口中预先设定好的,有时会出现一些不易查找的错误。
    PS:偶尔在代码中写点这样的代码,会增加代码的“气质”,你懂的。

    11. 理解objc_msgSend作用

    这一小节的内容和我们写代码没有什么关系,但是我们可以了解一下OC中方法的调用过程,对我们的程序调试很是很有用的。
    首先说一下C语言的函数调用方式,用以和OC做比较,C语言使用“静态绑定”,也就是说,在编译期就能决定运行时应该调用的函数,而大家都知道,OC是一门动态语言,与之差别的就是OC中有时候是使用“动态绑定”,就是在运行期调用对应的函数,甚至可以在程序运行时改变。
    写一个简单的方法调用的例子,解释一下方法的构成:

    id returnValue = [someObject messageName:parameter];
    

    在这句调用语句中,someObject就是类或类的实例,messageName就是方法名,parameter就是参数,编译器会把这条语句编译成一条标准的C语句,编译后的语句如下:

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

    objc_msgSend是一个可变参数的函数,对应OC中方法参数的增加,参数也会增加,相信大家都知道这个方法中参数的意思。
    objc_msgSend函数会根据参数,找到对应类的对应“方法列表”,然后找到对应实现代码,若找不到会沿着继承关系向上查找,如果还没找到,触发“消息转发”机制(后面会介绍这个机制)。
    这样下来调用一个方法大家可能感觉步骤太多,其实不会,objc_msgSend会将匹配结果放到一张“快速映射表”里,每个类都有一个这样的表,加快调用速度。另外还有一些特殊情况,OC运行环境中还有另外一些相关的处理函数,例如objc_msgSend_stretobjc_msgSend_fpretobjc_msgSendSuper就不在一一介绍。
    另外提一个点,OC对象的每一个方法当编译成C语言的时候可以看成是下面这种的形式的

    <returnType> Class_selector(id self, SEL _cmd, ...)
    

    其中的方法名是随意起的,大家发现这个函数和objc_msgSend的形式很想,这是为了利用“尾调用优化”,是调用函数更简单、高效。

    12. 理解消息转发机制

    这小节介绍一下上面提到的消息转发机制,大家都知道,触发了消息转发机制,是因为我们没有找到对应的方法,下面看消息转发机制怎么处理这个问题。
    介绍一下消息转发机制,大致分为三个阶段:
    1.第一阶段,动态方法解析
    对象在无法解读方法的时候,首先会调用所属类下面这个方法

    + (BOOL)resolveInstanceMethod:(SEL)sel
    

    sel就是方法名,返回值为Boolean类型,表示这个类是否能新增实例方法处理这个方法(如果是类方法会调用+ (BOOL)resolveClassMethod:(SEL)sel方法),我们需要自定义一些处理方法,用于动态添加到类中,用以解决问题(可以看后面的例子),如果这一步不能解决问题,转到第二阶段。
    2.第二阶段,备援接收者
    来到这一步,我们就要改变解决问题的思路,既然这个类不能处理这个方法,我们可不可以找别的类处理,这时候对应的处理方法:

    - (id)forwardingTargetForSelector:(SEL)aSelector
    

    aSelector是方法名,如果当前类能够找到一个类帮忙处理这个方法,就返回这个类,若找不到就放回nil(通过这个方法我们可以实现类似“多继承”)。
    3.第三阶段,完整的消息转发
    如果已经来到了这一步,我们就要做一个完整的消息转发。首先创建一个NSInvocation对象,把未处理方法的所有信息封装在里面,此对象包含方法名、目标、参数,这一步要调用下面的方法:

    - (void)forwardInvocation:(NSInvocation *)anInvocation
    

    这一步处理的方法很简单,就是在新的类上调用方法,如果这样做的话就和第二阶段没有什么差别了。通常在这一步的时候会做一些改进,会选择某种方式改变消息内容,例如追加参数,改变方法名等。
    对于消息的处理,越早越好。
    下面粘贴一个利用动态解析方法实现@dynamic属性的例子:
    这个例子实现一个类,类似字典的功能,只不过写入和读取信息的时候用属性,而不是像字典一样用关键字。
    .h文件中:

    #import <Foundation/Foundation.h>
    @interface EOCAutoDictionary : NSObject
    @property (nonatomic, strong) NSString *string;
    @property (nonatomic, strong) NSNumber *number;
    @property (nonatomic, strong) NSData *date;
    @property (nonatomic, strong) id opaqueObject;
    @end
    

    .m文件中:

    #import "EOCAutoDictionary.h"
    #import <objc/runtime.h> // 主要头文件的引用
    @interface EOCAutoDictionary ()
    @property (nonatomic, strong) NSMutableDictionary *backingStore;
    @end
    @implementation EOCAutoDictionary
    @dynamic string, number, date, opaqueObject;
    - (id)init{
        if ((self = [super init])) {
            _backingStore = [NSMutableDictionary new];
        }
        return self;
    }
    +(BOOL)resolveInstanceMethod:(SEL)sel{
        NSString *selectorString = NSStringFromSelector(sel);
        // 通过是否以“set”开头判断方法名
        if ([selectorString hasPrefix:@"set"]) {
            /**
             * 向类中添加一个方法
             * 参数一 指定类名.
             * 参数二 新添加的方法的方法名.
             * 参数三 函数指针,指向待添加方法.
             * 参数四 待添加方法的类型编码.
             */
            class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@");
        } else {
            class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");
        }
        return YES;
    }
    
    id autoDictionaryGetter(id self, SEL _cmd){
        // 拿到存储数据的字典
        EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
        NSMutableDictionary *backingStore = typedSelf.backingStore;
        // 拿到方法名
        NSString *key = NSStringFromSelector(_cmd);
        // 返回对应的值
        return [backingStore objectForKey:key];
    }
    void autoDictionarySetter(id self, SEL _cmd, id value){
        // 拿到存储数据的字典
        EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
        NSMutableDictionary *backingStore = typedSelf.backingStore;
        // 拿到方法名并对其进行处理
        NSString *selectorString = NSStringFromSelector(_cmd);
        NSMutableString *key = [selectorString mutableCopy];
        // 移除方法名中的“:”
        [key deleteCharactersInRange:NSMakeRange(key.length-1, 1)];
        // 移除方法名中的“set”
        [key deleteCharactersInRange:NSMakeRange(0, 3)];
        // 将方法名第一个字符转为小写
        NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
        [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
        // 如果有值,写入字典中
        if (value) {
            [backingStore setObject:value forKey:key];
        } else {
            [backingStore removeObjectForKey:key];
        }   
    }
    @end
    

    EOCAutoDictionary的用法也很简单,只要直接通过对应的属性名,就可以进行数据的存储。

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

    方法调配技术,简言之就是,将方法名和方法实现分割开来,任意组合。这样一来我们可以任意改变一个方法的实现,另外还可以通过这种办法给原有方法添加功能,对不知道内部实现的方法添加提示语句(黑盒调试)等等。
    之所以能这么做,主要是因为方法均以指针的形式来表示,这种指针叫IMP,我们在调用方法的时候,只要将指针指向改变,就能实现我们想要的效果,运用起来也很简单,通过下面的例子大家就会运用(注意运行时头文件的引用):

    Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
    Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString))
    method_exchangeImplementations(originalMethod, swappedMethod);
    

    通过上面的例子,我们就把NSString的lowercaseString方法和uppercaseString方法调换了,是不是很简单。
    其实这样做并没有什么意义,因为具体的方法实现已经都存在了,我们没必要改变一个方法实现,但是我们通过这种方法给已知的方法添加功能,例如下面的例子:
    .h文件:

    @interface NSString (EOCMyAdditions)
    - (NSString *)eoc_myLowercaseString; // 在分类中给NSString添加功能
    @end
    

    .m文件:

    @implementation NSString (EOCMyAdditions)
    - (NSString *)eoc_myLowercaseString{
        NSString *lowercase = [self eoc_myLowercaseString];
        NSLog(@"%@ => %@", self, lowercase);
        return lowercase;
    }
    @end
    

    然后我们使用方法调配技术,将上面的方法和lowercaseString方法进行调换:

    Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
    Method swappedMethod = class_getInstanceMethod([NSString class], @selector(eoc_myLowercaseString));
    method_exchangeImplementations(originalMethod, swappedMethod);
    

    这样执行完后,当我们再调用lowercaseString方法的时候会有下面的结果:

    NSString *string = @"This is tHe StRing";
    NSString *lowercaseString = [string lowercaseString];
    // Output:This is tHe StRing => this is the string
    

    通过这个方法我们发现,我们可以为那些不知道内部实现的黑盒方法添加日志记录功能。
    一般来说,我们很少用“方法调配”,只有在调试程序的时候才需要在运行期修改方法实现。

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

    首先我们要知道,OC的实例对象是指向某块内存数据的指针,所以在声明变量时,要用*号。同时我们知道OC中有一种通用对象类型“id”(id本身已是一个指针),所以我们在用“id”声明变量的时候可能和平常有点不同:

    NSString *aString = @"some string";
    id aString = @"some string";
    

    上面两种定义方式相比,语法意义相同,区别在于,指定具体类型后,当实例调用方法的时候,编辑器会给我们提示。
    下面看一下“id”类型的定义:

    typedef struct objc_object *id;
    

    id其实是objc_object类型的结构体,而objc_object定义如下:

    struct objc_object {
        Class isa  OBJC_ISA_AVAILABILITY;
    };
    

    结构体中是一个Class类型的变量,该变量定义对象所属的类。下面我们看一下Class类型是个什么东西:

    typedef struct objc_class *Class;
    struct objc_class {
        Class isa  OBJC_ISA_AVAILABILITY;
    #if !__OBJC2__
        Class super_class                                        OBJC2_UNAVAILABLE;
        const char *name                                         OBJC2_UNAVAILABLE;
        long version                                             OBJC2_UNAVAILABLE;
        long info                                                OBJC2_UNAVAILABLE;
        long instance_size                                       OBJC2_UNAVAILABLE;
        struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
        struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
        struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
        struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
    #endif
    } OBJC2_UNAVAILABLE;
    

    我们看到,这个结构体存放类的各种信息(元数据),例如类有多少个实力变量,类名等等信息。
    通过上面的关系,我们知道在objc的runtime中,类是用objc_class结构体表示的,对象是用objc_object结构体表示的, 对象的isa用来标示这个对象是哪个类的实例。
    这些源码是属于objc runtime的,objc runtime的源代码苹果已经开源了,你可以在这里下载到objc的runtime源代码。
    其实到这里大家可能会有一个疑问,为什么objc_class结构体里面也有一个isa,那么这个isa指向谁呢?我们往下看,[NSObject class],这里我们调用了+ (Class)class这个类方法,我们再开发中经常用到这个方法,它返回的是这个类所属的Class类型。+ (Class)class类方法的实现源码是这样的:

    + (Class)class { 
        return self; 
    } 
    

    为什么会返回self,self总是指的自身,而在这里没有实例啊!这时候看开发文档我们会发现,实际上函数的返回值是一个类对象class object,所以其本质上还是一个对象而已。既然是一个对象,它拥有一个self指针也就不奇怪了,所以对于像NSObject这样的类来说,它其实代表的是一个类对象,本质上还是一个普通的实例对象,那么又会问了,这个类对象是谁的实例呢?很遗憾,要找到这个问题的答案,我们在 objc runtime 这一层上已经没办法办到了,我们需要到更低层,也就是 objc 语言层去寻找答案了,但是 objc 语言层是不开源的,如果想继续学习,大家可以在网上找模仿OC低层的代码。
    以上了解一下就好,我们只要知道类的继承体系就行了,下面用一个例子:有一个类(暂且叫SomeClass)继承于NSObject,那么这些类和元类的继承关系是,SomeClass实例有一个isa指针指向SomeClass类,SomeClass类有一个isa指针指向SomeClass元类,NSObject类也有一个isa指针指向NSObject元类,SomeClass的父类是NSObject,SomeClass元类的父类是NSObject元类,通过这种关系,我们在类继承体系中查询类型信息,用isMenberOfClass:判断对象是否是某个特定类的实例,用isKindOfClass:判断对象是否为某类或其派生类的实例。因为OC是动态型语言的特性,上面两个方法非常有用。
    有时我们可以用比较类对象是否等同的办法来进行比较,这时要用==操作符,而不是用isEqual方法,因为类对象是单利,在应用程序中,每个类的类对象只有一个实例,也就是说另外一种判断对象是否为某类实例的办法是:

    id object = /*...*/
    if ([object class] == [SomeClass class]){
    }
    

    这一部分基本都是关于OC运行时的知识,可能我们平时写代码的时候涉及很少,但是了解这些,对于我们的开发是很有帮助的,OC运行时是一个很强大的东西,有兴趣的同学可以好好研究一下。

    相关文章

      网友评论

        本文标题:Effective Objective-C 2.0 读书笔记二(

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