iOS 探究 OC对象、isa指针及KVO实现原理

3,313 阅读8分钟

目录

1. OC对象的本质

2. OC实例对象、类对象、元类对象分析

3. isa和superclass的分析

4. KVO实现原理

5. 用代码实现一个KVO


  • 一点感悟:最近复习一些之前学习过的内容,发现基本都忘记的差不多了,以前也没有写一些文章把学过的总结下来,现在回过头看,又得学习一遍,时间成本确实不小,趁最近业务不是很忙,多总结记录一下哈哈哈。进入正题:

OC对象的本质:

1. OC对象的底层数据结构

  • 我们都知道OC是面向对象的语言,底层都是基于C/C++代码实现的,所以OC的面向对象都是基C/C++的数据结构实现的:

截屏2021-03-12 下午3.22.21.png

  • 那么OC的对象、类主要是基于C\C++的什么数据结构实现的? 因为OC大多都有很多属性和方法,C\C++能满足其需求的只有结构体,所以OC的对象、类主要是基于结构体去实现的。 关于结构体更多知识这里篇文章总结的很棒:iOS中编写高效能结构体的7个要点

2. NSOject的底层实现

  • 说到OC对象,就不得不说NSOject了,因为所有OC对象都继承自NSObject,上面也说了对象、类主要是基于结构体去实现的,那么这个结构体里面到底有什么呢,我们将main.m文件转成.cpp文件(前面写的文章有说过iOS autoreleasePool原理总结)

在.cpp文件中发现NSObject的实现就是一个结构体,里面包含了一个结构体指针即isa(isa是Class类型,而Class又是一个结构体类型的指针,所以isa是一个结构体指针)

struct NSObject_IMPL {
	Class isa;
};
typedef struct objc_class *Class;
  • 继承自NSObject的对象结构大概是什么样的呢,如下我们创建一个继承NSObject的对象Student,添加name和age两个属性
@interface Student : NSObject
@property(nonatomic,assign)int age;
@property(nonatomic,strong)NSString * name;
@end

底层结构如下: 即第一个元素是isa指针,我们可以认为isa的地址即是Student的地址,后面的成员变量会遵循内存对齐的原则逐一分配内存地址。

struct Student_IMPL {
	struct NSObject_IMPL NSObject_IVARS;(Class isa;)
	int _age;
	NSString *_name;
};

总结:

  1. 因为OC对象都继承自NSObject,而NSObject又包含一个isa,所以我们后面对OC的底层学习,发现结构体内有isa即可判定为OC对象;
  2. isa为对象的第一个成员,它的地址即为对象的起始地址;

OC实例对象、类对象、元类对象分析

OC对象主要可以分成3种类型:

  1. instance对象(实例对象)
  2. class对象(类对象)
  3. meta-class对象(元类对象)

逐一看看他们到底有有什么作用和区别:

1.instance对象(实例对象):

  • 实例对象就是通过类alloc出来的对象,每次调用alloc都会产生新的instance对象,instance对象在内存中存储的信息包括
  1. isa指针
  2. 其他成员变量 如下创建一个student对象,它的内存信息如图:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface Student : NSObject
@property(nonatomic,strong)NSString * name;
@property(nonatomic,assign)int age;
@end

@implementation Student
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Student *student = [[Student alloc] init];
        student.age = 20;
        student.name = @"lanlin";  
    }
    return 0;
}

截屏2021-03-12 下午6.13.29.png

2.class对象(类对象):

  • 通过实例对象的class方法或者runtime的object_getClass方法可以获取到这个实例的类对象,每个类在内存中有且只有一个class对象,class对象在内存中存储的信息主要包括:
  1. isa指针
  2. superclass指针
  3. 类的属性信息(@property)
  4. 类的对象方法信息(instance method)
  5. 类的协议信息(protocol)
  6. 类的成员变量信息(ivar)

如下创建一个student对象,它的类对象内存信息如图:

     Student *student = [[Student alloc] init];
     Class stuClass1 = [student class];
     Class stuClass2 = object_getClass(student);//Runtime API

class的内存信息.png

2.meta-class对象(元类对象):

  • 通过RuntimeAPIobject_getClass([类名 class])获取,每个类在内存中有且只有一个meta-class对象,meta-class对象和class对象的内存结构是一样的,但是用途不一样,在内存中存储的信息主要包括:
  1. isa指针
  2. superclass指针
  3. 类的类方法信息(class method)
   ///获取元类对象
   Class objectMetaClass = object_getClass([NSObject class])
   ///判断是否为元类对象
   BOOL isMetaClass = class_isMetaClass(objectMetaClass);

截屏2021-03-12 下午7.02.33.png

以上三种类型对象的相关特点和作用。

  • class、meta-class对象的本质结构都是struct objc_class

通过源码分析:Objc源码下载地址 截屏2021-03-17 下午4.32.05.png


isa和superclass分析

  • 上面分析了OC对象的类型,有几个问题:
  1. 既然实例对象内存并没有包含对象方法,那么为什么实例对象可以调用到对象方法呢?
  2. 类对象也没有类方法的信息,但是却可以通过类名调用类方法
  3. 子类为什么可以调用父类的方法 要回答上面几个问题,就需要了解isa和superclass的作用了。
  • 上面也说了isa是结构体指针,只要是OC对象都有isa,作用如下:
  1. 实例对象的isa指向它的类对象。

当调用对象方法时,实例对象内存并没有包含对象方法,而是通过它内部的isa指针找到它的类对象,从而在类对象中找到对象方法的实现进行调用

  1. 类对象的isa指向元类对象

当调用类方法时,类对象并没有类方法的信息,而是通过类对象的isa找到元类对象,最后找到类方法的实现进行调用

截屏2021-03-15 上午10.51.09.png

  • superclass指针只会存在类对象和元类对象中,每个类(元类)对象的superclass指针都是指向它的父类,作用如下:

1.当实例对象调用对象方法会通过isa找到它的类对象,然后在类对象找方法实现,然而有时候这个类对象并没方法实现,那么就会通过superclass找到它的父类查找实现方法,一直找到基类。

截屏2021-03-15 上午11.54.16.png

2.当类调用类方法时会通过isa找到它的元类对象,然后在元类类对象找方法实现,然而有时候这个元类对象并没方法实现,那么就会通过superclass找到它的父元类查找实现方法,一直找到基类。

  • 总结:
  1. 实例对象(instance)的isa指向类对象(class)
  2. 类对象(class)的isa指向元类对象(meta-class)
  3. class的superclass指向父类的class,如果没有父类,superclass指针为nil
  4. meta-class的superclass指向父类的meta-class,基类的meta-class的superclass指向基类的class
  5. instance调用对象方法的轨迹:isa找到class,方法不存在,就通过superclass找父类
  6. class调用类方法的轨迹:isa找meta-class,方法不存在,就通过superclass找父类

截屏2021-03-15 上午11.59.40.png


KVO实现原理

  • 定义:KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。

  • 当一个对象的属性被监听时,本质实现如下步骤:

  1. 会利用Runtime API动态生成一个子类,继承实例对象isa原来指向的类
  2. 修改实例对象的isa指针,让其指向这个新的子类;
  3. 当修改实例对象属性时,本质就是调用了对象属性的set方法,会去isa指向的类找实现方法(上面讲过isa指向规则),这时候就是去新子类找set方法实现了;
  4. 全新的子类会重写set方法,会调用Foundation的_NSSetXXXValueAndNotify函数主要内容:
  1. 调用willChangeValueForKey: 取旧值(为了在回调中能够提供监听的属性的旧值)
  2. 调用super的set方法赋值
  3. 调用didChangeValueForKey: , 内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)
  1. 除了重写set方法,子类还重写了如下几个方法:
  1. class方法: 前面也说过调用对象[xxxx class],可以得到这个对象isa指向的class,这里重写class方法,当调用[xxxx class],返回动态生成的新class
  2. dealloc : 因为这个子类是用runtime生成的,系统并未暴露出来,所以释放的时候,要把相关信息还原和清除。
  3. _isKVOA : 这方法主要返回是否为kvo创建的类,如果是动态生成的子类:return Yes。

用代码实现一个KVO

  • 根据上面的实现步骤我们可以通过runtimeAPI实现一下, 创建一个NSObject的分类NSObject+CustomKVO .h文件如下
#import <Foundation/Foundation.h>
@interface NSObject (CustomKVO)
- (void)ll_addObserver:(NSObject *_Nullable)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end

.m文件

#import "NSObject+CustomKVO.h"
#import <objc/message.h>

@implementation NSObject (CustomKVO)

- (void)ll_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
{
    ///1.动态创建一个子类
    //获取对象名
    NSString * oldName = NSStringFromClass([self class]);
    //生成新的类名
    NSString * newName = [NSString stringWithFormat:@"CustomKVO_%@",oldName];
    //分配空间,创建类(仅在 创建之后,注册之前 能够添加成员变量)
    Class customClass = objc_allocateClassPair([self class], newName.UTF8String,0);
    //注册类(注册后方可使用该类创建对象)
    objc_registerClassPair(customClass);
    
    ///2.修改对象的isa指针
    object_setClass(self, customClass);
    
    ///3.重写对象的set方法
    NSString * methodName = [NSString stringWithFormat:@"set%@:",keyPath.capitalizedString];//capitalizedString:首字母大写
    SEL sel = NSSelectorFromString(methodName);
    class_addMethod(customClass, sel, (IMP)setterMethod, "v@:@");
    
    ///关联对象
    objc_setAssociatedObject(self, (__bridge  const void *)@"ObserverKey", observer, OBJC_ASSOCIATION_ASSIGN);
}

//set方法
void setterMethod(id self, SEL _cmd, NSString * name){
    id observer = objc_getAssociatedObject(self, (__bridge  const void *)@"ObserverKey");
    NSString *methodName = NSStringFromSelector(_cmd);
    NSString *getterName = getValueKey(methodName);
    //1.获取旧值
    NSString * oldValue = [self valueForKey:getterName];
    
    //2. 调用父类的set方法
    struct objc_super superClass = {
        self,
        class_getSuperclass([self class])
    };
    objc_msgSendSuper(&superClass ,_cmd , name);

    //3.发出通知
    NSDictionary<NSKeyValueChangeKey,id> *changeDict = oldValue ? @{NSKeyValueChangeNewKey : name, NSKeyValueChangeOldKey : oldValue} : @{NSKeyValueChangeNewKey : name};
    [observer observeValueForKeyPath:getterName ofObject:self change:changeDict context:NULL];

}

///通过方法名获取属性(字符串处理)
NSString * getValueKey(NSString * setter){
    NSRange range = NSMakeRange(3, setter.length - 4);
    NSString *key = [setter substringWithRange:range];
    NSString *letter = [[key substringToIndex:1] lowercaseString];
    key = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:letter];
    return  key;
}
@end

KVODemon 下载地址