effective-Objective-C 第二章阅读笔记

对象,消息,运行期

文章目录

  • 对象,消息,运行期
  • 前言
  • 理解“属性”这一概念
  • 属性修饰符
  • 原子性
  • nonatimic
  • atomic
  • 读/写权限
  • 内存管理语义
  • 方法名
  • 自定义初始化方法
  • 小结
  • 在对象内部尽量直接访问实例变量
  • 小结
  • 对象等同性
  • 特定类的isEqual
  • 执行深度
  • 容器类可变类的等同性
  • 类族
  • CoCo类族
  • 小结
  • 使用关联对象来存放自定义数据
  • 小结
  • 理解objc_msgSend
  • 小结
  • 理解消息转发机制
  • 动态方法解析
  • 备援接受者
  • 完整的消息转发
  • 全流程
  • 简单的例子
  • 小结
  • 用方法调配技术调试黑盒方法
  • 小结
  • 理解类对象
  • 继承体系中查询类型关系
  • 小结
  • 总结
  • 前言

    在对象之间传输数据并执行任务的过程叫做”消息传递“,这部分内容主要有关于运行期环境中各个部分的协同工作的内容

    理解“属性”这一概念

    属性是OC的一项特性,用于封装对象中的数据。一般我们采用getter来访问属性中的数据,通过setter来写入变量值,这里OC同样引入了一个点语法来进行一个访问存放在属性中的数据。

    这里我们先看这段代码:

    image-20250114171450926

    这种在C++中很常见,但是在OC中我们一般采用属性,至于原因是因为这种方式采用硬编码,我们访问实例变量的时候,编译器会把他替换成距离存放地址的起始地址的一个偏移量来直接访问对应的数据。

    image-20250114171806148

    假设这时候多了一个实例变量,这种情况会在访问不到我们原来的数据。

    image-20250114172333667

    所以我们呢在修改类定义之后一定要重新编译,否则就会出错,OC为了解决这个问题采用的是通过类变量来对他进行一个管理。这样子做我们就可以在分类或者实现文件中定义实例变量了。

    同时在OC中我们尽量不要直接访问实例变量,而是通过存取方法来获取对应的数据。这里我们之前也学习过他的语法:

    image-20250114173516404

    他和下面的方法是等效的:

    image-20250114173614698

    当我们使用了属性之后,编译器会自动编写访问这些属性所需的方法,这个过程在编译期执行,编译器会给类中添加对应类型的一个实例变量,他自动生成的实例变量是属性名前面加一个下划线,我们也可以使用@dynamic来让我们自动合成存取方法以及生成实例变量。

    属性修饰符

    @property (nonatomic, readwrite, copy) NSString *firstName;
    

    这里我们有三种不同的修饰符

    原子性

    nonatimic

    这个不会使用同步锁,也就是他由编译器所合成的方法会通过锁定机制确保其原子性。

    atomic

    这个是原子性的,会通过锁定机制保证他的一个原子性,他只能保证读写操作的一个安全性,不能保证他的其他操作(访问或者是添加)的一个安全。

    比如说atomic修饰的是一个数组的话,那么我们对数组进行赋值和取值是可以保证线程安全的。但是如果我们对数组进行操作,比如说给数组添加对象或者移除对象,是不在atomic的负责范围之内的,所以给被atomic修饰的数组添加对象或者移除对象是没办法保证线程安全的。

    读/写权限

    readwrite保证这个属性有getter和setter,只要采用@synthesize编译器就会自动生成这两个方法。

    readonly的属性仅仅拥有获取方法,仅仅生成getter方法。

    内存管理语义
    修饰符 解释
    strong 强引用,当一个对象被声明为strong属性,ARC会增加该对象的引用计数。设置方法会先保留新值释放旧值
    weak 只能修饰对象类型;2. ARC 下才能使用;3. 修饰弱引用,不增加对象引用计数,主要可以用于避免循环引用;4. weak 修饰的对象在被释放之后,会自动将指针置为 nil,不会产生悬垂指针。设置方法先不留新值,不释放旧值
    assign 一般用于修饰基本类型; setter 方法的实现是直接赋值,一般用于基本数据类型 ; 修饰基本数据类型,如 NSInteger、BOOL、int、float 等;
    copy 指定属性为拷贝引用,即属性会拷贝对象的值,而不是持有原始对象的引用
    unsafe_unretained 基本和weak相似,但是他不会在该指针所引用的对象被回收后将指针赋为nil
    方法名
  • getter=< name >指定对应的一个getter的方法名字。
  • @property (nonatomic, getter=isOn) BOOL on;
    
  • setter方法也是这样的
  • 自定义初始化方法

    @interface Person : NSObject
    @property (nonatomic, copy) NSString* name;
    @property (nonatomic, copy) NSString* firstName;
    -(id) initWithName:(NSString*)firstName lastName:(NSString*)name;
    @end
    - (id)initWithName:(NSString *)firstName lastName:(NSString *)name {
        if (self = [super init]) {
            _firstName = [firstName copy];
            _name = [name copy];
        }
        return self;
    }
    

    这里注意我们初始化的函数中注意一个点就是我们对应的属性的修饰符对应起来。copy就用copy方式赋值。

    小结

  • 用@property来进行一个修饰
  • 开发的时候尽量使用nonatomic、
  • 在对象内部尽量直接访问实例变量

    读取实例变量的时候采用直接访问的形式,在设置实例变量的时候通过属性来做。

    我们访问实例变量的时候采用点语法和直接访问有几个区别:

  • 直接访问不通过OC的方法派发,所以直接访问实例变量的速度比较快。
  • 直接访问的时候,会绕过相关属性设置的时候的内存管理语义的部分
  • 直接访问不会触发KVO
  • 通过属性访问更有利于排查与之相关的一些错误,因为可以在我们的一个存取方法中添加断点。
  • 所以有一种折中方案,可以在写入实例变量的时候,通过设置方法来做,在读取的时候,则直接访问。

    下面是几个要注意的地方:

  • 首先是我们的在初始化方法中应该直接访问实例变量,因为子类可能会复写设置方法。
  • - (void)setName:(NSString *)name {
        if (![name isEqualToString:@"nan"]) {
            [NSException raise:NSInvalidArgumentException format:@"must nan"];
        }
        self.name = name;
    }
    

    这里其实有一个循环调用set方法的问题,所以这里应该是直接访问实例变量。出了一种情况,如果将待初始化的实例变量放在超类中,我们无法在子类中直接访问此实例变量的话,这里才需要调用设置方法。

    还有一个懒加载的内容,我们使用懒加载的话,我们就必须要通过getter方法来获取对应的属性。

    小结

  • 在对象内部,通过实例变量读取,写入通过set
  • 在初始化中应该直接通过实例变量来读写数据
  • 如果使用懒加载,则一定要通过属性来读取数据。
  • 对象等同性

    这部分内容主要是在讲isEqual的内容,首先我们要明白==是用来判断两个的地址是否相同的。

    一般情况我们是要使用NSObject中的isEqual:来进行一个判断,这里简单介绍一下NSString的isEqual:isEqualToString:两个方法的一个差异,后者更快速。前者还要执行更多的一个步骤。

    NSObject中有两个用于判断等同性的方法:

    -(NSUInteger)hash;
    - (BOOL)isEqual:(id)object
    

    默认实现是地址完全相同的时候两个对象才相等。这里我们如果自主实现的话,我们是hash方法必须返回同一个值,同时在通过自定义的isEqual方法正确后后才能来判断其是否正确。

    我们自己设计一个类的isEqual的时候要合理。

    image-20250115183119802

    同时我们还要保证hash值也要相同,在编写hash方法的时候要注意减少hash碰撞,如果设置所有hash值一致,那么我们把他添加到集合中就会出现问题,因为我们其实还是通过hash值来寻找对应的一个数组的,然后在判断isEqual是否有相同的数据来进行一个判断,如果hash值一致,会导致每一次添加都会扫瞄所有的对象。

    特定类的isEqual

    NSArray和NSDictionary都有自己的isqueal。

    分别是isEqualToArray isEqualToDictionary,这里我们要注意下不可以穿入错误类型的一个数据,否则会抛出异常。

    有些时候我们需要自己写一个等同性方法,想之前提到的一个peson类,我们可以设置isEqualToPerson这个方法来进行一个判断比较

    在编写判断方法时,也应该一并复写isEqual方法,如果两者相同就掉用自己的判断方法,否则就交给父类来判断。

    执行深度

    创建等同性判定方法的时候,需要根据整个对象,在更具对象中的几个字段,就像NSArray的检测行为来判断两个数组中所含对象的个数是否相同,在比对内部的一个对象。

    容器类可变类的等同性

    在容器中加入可变类型的对象的时候,注意不要在修改他的一个值。

    否则就会出现一个set中出现两个重复的数据的内容。

    NSMutableSet *set = [NSMutableSet set];
        NSMutableArray* ary = [@[@1] mutableCopy];
        NSMutableArray* ary1 = [NSMutableArray array];
        [set addObject:ary];
        [set addObject:ary1];
        NSLog(@"%@", set);
        [ary1 addObject:@1];
        NSLog(@"%@", set);
        NSLog(@"%@", [set copy]);
    

    者段代码的结果是:

    image-20250115195224899

    这里经过了一个copy又出现了一个bug,发现数据又少了一个部分,所以我们这里的问题是尽量不要在set后对内部的可变数据进行一个操作。

  • 如果检验对象的等同性,提供isEqual:和hash方法
  • 相同的对象要有相同的哈希码,相同hash吗的对象却不一定相同
  • 选择哈希碰撞低的算法。
  • 类族

    类族的概念其实是一个工厂模式的实现,类族的概念是一种很有用的模式,可以隐藏抽象基类的实现

    比方说我们的一个UIButton的内容,我们创建UIButton的函数和别的创建不太一样,这里其实就是一种类族的方式来进行一个判断的。

    [UIButton buttonWithType:<#(UIButtonType)#>]
    

    这里我们可以吧各种按钮的绘制逻辑都放在一个类里面,根据不同的按钮类型来进行一个切换

    image-20250116152432499

    CoCo类族

    这里我觉得比较重要的是认识一下我们的一个CoCo框架中的类族,比方说我们的之前提到过的一个NSString的内容,NSString就是一个类族,我们还需要知道大部分集合类都是类族,基类提供一个接口,然后我们的子类有各自的一个底层实现。

    这里我们来看一段代码:

    NSArray* ary = @[@23];
    NSLog(@"%@, %@, %ld", [ary class], [NSArray class], [ary class] == [NSArray class]);
    

    输出结果如下:

    NSConstantArray, NSArray, 0
    
    Type: Notice | Timestamp: 2025-01-16 15:33:27.173111+08:00 | Process: fragment | Library: fragment | TID: 0x1fe410b
    

    所以我们判断类别的方式要采用isKindOfClass的方式,而不是[<name> class] == [<name> class]

    我们也是可以给Cocoa中的NSArray这样的类族来新增一些子类的,但是还是要遵循一些规则

  • 子类要继承与抽象基类
  • 子类要定义自己的数据存储方式
  • 子类要重写超类文档中应该重写的方法
  • 小结

  • 可以吧实现细节隐藏在一套简单的公共接口后面
  • 系统框架中常常使用类族
  • 使用关联对象来存放自定义数据

    关联对象用来解决在某些情况下,有的类的实例是由某种机制创建的,我们无法自己创建一个子类实例,这时候才会用到关联对象的一个内容。比方说:给一个分类添加属性,或者是不能修改类定义的时候,就会变得非常常用。

    这里先看一下有关于关联对象的一个类型:

    image-20250116155238538

    这里是如果关联对象成为了一个对应的属性就会具有关联对象同属性一样所对应的一个语义。

    下面有三个方法来管理关联对象:

    objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                             id _Nullable value, objc_AssociationPolicy policy);//以给定的值来给某对象设置关联对象值
    objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key);//获取相应的一个关联对象的值
    objc_removeAssociatedObjects(id _Nonnull object);//移除所有关联对象
    

    这里简单介绍一下有关于给分类添加属性的内容:

    //.h
    @interface UIView (Associated)
    @property (nonatomic, strong) NSString* string;
    @end
    //.m
    #import <objc/runtime.h>
    @implementation UIView (Associated)
    - (void)setString:(NSString *)string {
        objc_setAssociatedObject(self, @selector(string), string, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    - (NSString *)string {
        return objc_getAssociatedObject(self, @selector(string));
    }
    @end
    
    UIView* view = [[UIView alloc] init];
    view.string = @"2134";
    NSLog(@"%@", view.string);
    

    经过上述操作就可以完成一个添加的效果:

    2134
    Type: Notice | Timestamp: 2025-01-16 16:15:07.480281+08:00 | Process: fragment | Library: fragment | TID: 0x1fee74c
    

    这种做法效果很不错但是可能会带来循环引用的一些问题。

    小结

  • 通过关联对象来连接两个对象
  • 可以给分类添加属性
  • 这种做法可能会引入一些bug
  • 理解objc_msgSend

    传递消息是OC中非常重要的一个内容,消息有名称或选择子,可以接受参数还可能有返回值,在C语言中采用的是一个静态绑定,在编译期就可以决定运行的时候应该调用的一个函数,在OC中如果向某个对象传递消息,那么就会使用动态绑定机制来决定需要调用的一个方法。

    给对象发送消息可以写成这样:

    
    id objc_msgSend(id self, SEL op, ...);
    
    //self:消息的接收者(目标对象)。
    //op:消息对应的选择器(方法名,类型为 SEL)。
    //...:方法所需的参数(可选)。
    

    这里笔者直接引用一段书中的话:

    objc_msgSend 函数会依据接收者与选择子的类型来调用适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜寻其“方法列表”(list of methods),如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。

    同时OC还有一个缓存机制,这里的objc_msgSend会将匹配结果缓存在(hash map)里面,每个类都有一个缓存,如果之后查找相同的方法,就会直接从缓存中调用。

    这里还有部分边界情况的一个派发:

    image-20250116173048116

    这里的OC对象的每一个方法都可以理解为C语言函数:

    <return_type> Class_selector(id self, SEL _cmd, ...);
    

    每一个类都是一个表格,其中的指针会指向这些函数,选择子的名称则是查找的建。

    这里吧所有的函数的都和我们的一个objc_msgSend函数大致类似,这里可以实现一个尾递归优化。

    尾递归优化:是一种编译器或运行时对递归函数的优化技术。当递归调用是函数中的最后一个操作时,编译器可以优化递归调用,避免创建新的栈帧,从而节省内存并防止栈溢出。

    小结

  • 消息由接受者,选择子及其参数构成,给某个对象发送消息,也就相当于在该对象上调用方法
  • 发给某对象的所有消息都要由动态派发系统来进行一个处理,该系统会查出对应的方法,并且执行对应的代码
  • 理解消息转发机制

    当对象接收到无法解读的消息后,就会启动消息转发机制,程序猿可以由此过程告诉如何处理未知消息。

    动态方法解析

    对象在收到无法解读的消息后,会先调用这个类方法:

    + (BOOL)resolveInstanceMethod:(SEL)sel;
    

    该方法的参数就是那个未知的选择子,期返回值为Boolean类型,表示这里新增一个类来处理这个选择子。

    如果是失败的是一个类方法,那么就会调用这个类方法:

    + (BOOL)resolveClassMethod:(SEL)sel
    

    备援接受者

    前面那个方法失败后,当前接受者还有第二次机会来处理选择子,这一步是寻找别人帮忙处理这个选择子:

    - (id)forwardingTargetForSelector:(SEL)aSelector;
    

    如果找的到人帮助他,那么就将其返回,如果找不到就返回nil。

    这样的话我们可以通过继承的方式来解决这种问题,因为在一个对象内部可能还有很多别的对象,可以内部的其他对象来进行一个处理

    完整的消息转发

    也就是上一步也失败之后,我们就会进入下一个步骤:

    首先创建 NSInvocation 对象,把与尚未处理的那条消息有关的全部细节都封于其中。此对象包含选择子、目标(target)及参数。在触发 NSInvocation 对象时,“消息派发系统”(message-dispatch system) 将亲自出马,把消息指派给目标对象。

    这时候回调用:

    - (void)forwardInvocation:(NSInvocation *)anInvocation
    

    一般情况下,我们在触发这个消息的时候,会先以某种方式改变消息内容,比方说追加另一个参数,或者是改换一个选择子。如果发现不应该由这个类来处理,他会调用他的超类,继承连中的每一个类都有机会来处理这个请求,知道NSobject

    最后还没有找到的话,会调用

    - (void)doesNotRecognizeSelector:(SEL)aSelector
    

    全流程

    image-20250116184507158

    这里尽量让他在前面处理对应的一个消息,

    简单的例子

    //
    //  EOCDictionary.h
    
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface EOCDictionary : NSObject
    @property (nonatomic, strong) NSMutableDictionary* dictary;
    @property (nonatomic, copy) NSString* string;
    @property (nonatomic, strong) NSDate* date;
    @end
    
    NS_ASSUME_NONNULL_END
    
      
    //
    //  EOCDictionary.m
    
    
    #import "EOCDictionary.h"
    #import <objc/runtime.h>
    @implementation EOCDictionary
    @dynamic string, date;//这里用@dynamic来实现运行期的操作
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            self.dictary = [NSMutableDictionary dictionary];
        }
        return self;
    }
    id autoDictionaryGet(id self, SEL _cmd) {
        EOCDictionary* typdefSelf = (EOCDictionary*)self;
        NSMutableDictionary* backStore = typdefSelf.dictary;
        NSString* key = NSStringFromSelector(_cmd);
        return [backStore objectForKey:key];
    }
    void autoDictionarySet(id self, SEL _cmd, id value) {
        EOCDictionary* typdefSelf = (EOCDictionary*)self;
        NSMutableDictionary* backStore = typdefSelf.dictary;
        
        NSMutableString* key = [NSStringFromSelector(_cmd) mutableCopy];
        [key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];// 删除:
        [key deleteCharactersInRange:NSMakeRange(0, 3)];//删除set
        NSString* lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
        [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
        if (value) {
            [backStore setObject:value forKey:key];
        } else {
            [backStore removeObjectForKey:key];
        }
    }
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        NSString* selcetString = NSStringFromSelector(sel);
        if ([selcetString hasPrefix:@"set"]) {
            class_addMethod(self, sel, (IMP)autoDictionarySet, "v@:@"); //这个方法向类中动态添加方法,处理遮盖力给定的一个选择子
        } else {
            class_addMethod(self, sel, (IMP)autoDictionaryGet, "@@:");
        }
        return YES;
    }
    @end
    
    

    上面的代码实现了一个自动合成对应的一个字典,可以持有多个属性来分别访问,如果想添加新的属性只用用@property,再声明成@dynamic。其实笔者也不是很理解这部分内容的一个具体实现

    在iOS的CoreAnimation 框架中,CALayer 类就用了与本例相似的实现方式,这使得 CALayer 成为“兼容于键值编码的”(key-value-coding-compliant)。容器类,也就等于说,能够向里面随意添加属性,然后以键值对的形式来访问。于是,开发者就可以向其中新增自定义的属性了,这些属性值的存储工作由基类直接负责,我们只需在CALayer的子类中定义新属性即可。

    小结

  • 运行期无法响应某个消息子,则进入消息转发
  • 通过运行期的一个动态解析,可以在用到的时候把他添加进去
  • 还无法处理可以转交给其他对象
  • 最后启用完整的消息转发机制
  • 用方法调配技术调试黑盒方法

    方法调配,可以让我们不用通过继承子类来重写方法就能改变这个类的巨大的功能,新功能能在本类的所有实例中生效,而不是仅限于重写了相关方法的子类实例。

    类的方法列表会把选择子的名称映射到相关的方法实现之上,使得“动态消息派发系统”能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做 IMP,

    id (*IMP)(id, SEL, ...)
    

    下面是一个映射表:

    image-20250116192835706

    在运行期我们可以操作这个表,可以让他变成不同的样式:

    image-20250116192910373

    可以看到表上的内容不仅仅多了一个newSelector,还吧low和upper进行了一个交换。

    Method originalMahon = class_getInstanceMethod([NSString class], @selector(lowercaseString));
    Method swpperMahton = class_getInstanceMethod([NSString class], @selector(uppercaseString));
    method_exchangeImplementations(originalMahon, swpperMahton);
    NSString* str = @"ThIS iS tHE";
    NSString* low = [str lowercaseString];
    NSString* up = [str uppercaseString];
    NSLog(@"%@, %@", low, up);
    

    这段代码实现了一个交换upper和lower两个方法名

    THIS IS THE, this is the
    Type: Notice | Timestamp: 2025-01-16 19:36:58.911202+08:00 | Process: fragment | Library: fragment | TID: 0x2010234
    

    看上去没什么意义,但是这里我们可以通过这种手段来给原先的方法添加新功能

    //创建了一个分类,给NSString添加新方法
    - (NSString *)eocString {
        NSString* lowcase = [self eocString];
        NSLog(@"%@ => %@", self, lowcase);
        return lowcase;
    }	
    
    		NSString* string = @"This is low";
    
        Method originalMahon = class_getInstanceMethod([NSString class], @selector(lowercaseString));
        Method swpperMahton = class_getInstanceMethod([NSString class], @selector(eocString));
        method_exchangeImplementations(originalMahon, swpperMahton);
    
        NSLog(@"%@", [string lowercaseString]);
    

    输出结果:

    This is low => this is low
    Type: Notice | Timestamp: 2025-01-16 19:52:55.610695+08:00 | Process: fragment | Library: fragment | TID: 0x2014300
    this is low
    Type: Notice | Timestamp: 2025-01-16 19:52:55.610749+08:00 | Process: fragment | Library: fragment | TID: 0x2014300
    

    小结

  • 云溪区可以给类中添加新的选择子所对应的方法实现
  • 可以用一个新的实现来替换原来的一个方法实现
  • 这种方法只有在调试的时候才需要在运行期修改方法实现,这种做法不要滥用
  • 理解类对象

    “在运行期检视对象类型”这一操作也叫做“类型信息查询”(introspection,“内省”),这个强大而有用的特性内置于 Foundation 框架的NSObject 协议里,凡是由公共根类(commonroot class,即 NSObject 与NSProxy)继承而来的对象都要遵从此协议。这里我们首先要了解类对象,以及OC每个对象的一个底层实现。

    该变量定义了对象所属的类,通常称为’is a’指针

    image-20250116201034304

    此结构体存放类的“元数据”(metadata),例如类的实例实现了几个方法,具备多少个实例变量等信息。此结构体的首个变量也是isa 指针,这说明 Class 本身亦 Objective-C 对象。结构体里还有个变量叫做super_class,它定义了本类的超类。类对象所属的类型(也就是isa指针所指向的类型)是另外一个类,叫做“元类”(metaclass),用来表述类对象本身所具备的元数据。“类方法”就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”。

    image-20250116201551472

    继承体系中查询类型关系

    isMemberOfClass用于判断实例,isKindOfClass用于判断是否属于某个类,这里的内容笔者在OC的isa指针的简单理解讲过了,这里就不多赘述,我们想比较类对象是否等同的时候可以通过==来精确的判断出对象是否为某类实例

    小结

  • 每一个实例都有一个指向Class对象的指针,来表明类型
  • 对象类型无法在编译器确定,那么就应该用类型查询方法来探知
  • 尽量使用类型信息查询方法来确定对象类型,而不是直接比较类对象,因为有些对象实现了消息转发
  • 总结

    这一章内容比较多,还需要之后自己多看一下这部分内容。

    作者:安和昴

    物联沃分享整理
    物联沃-IOTWORD物联网 » effective-Objective-C 第二章阅读笔记

    发表回复