【iOS】类与对象底层探索

文章目录


前言

这篇文章主要探索OC对象的本质

首先我们需要明白我们平时编写的OC代码,底层实现都是C\C++代码

一、编译源码

首先通过终端利用clangmain.m编译为main.cpp

bash 复制代码
//1、将 main.m 编译成 main.cpp
clang -rewrite-objc main.m -o main.cpp

//2、将 ViewController.m 编译成  ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m

//以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun
//3、模拟器文件编译
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 

//4、真机文件编译
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp 

二、探索对象本质

我们打开编译好的源文件后找到LGPerson,发现其在底层被编译为struct结构体

bash 复制代码
//NSObject的定义
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

//NSObject 的底层编译
struct NSObject_IMPL {
	Class isa;
};

//LGPerson的底层编译
struct LGPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS; // 等效于 Class isa;
	NSString *_name;
};

LGPerson_IMPL实现结构体中的第一个属性是isa,是继承自NSObject,是伪继承。意味着LGPerson拥有者NSObject中所有成员变量

LGPerson中的第一个属性 NSObject_IVARS 等效于 NSObject中的 isa

这里也许我们会产生一个疑问就是为什么isa的类型是class
根本原因是由于isa 对外反馈的是类信息

总结

因此我们可以得出:

OC对象本质就是结构体
LGPerson中的isa就是继承自NSObject中的isa

三、objc_setProperty 源码探索

除了LGPerson的底层定义,我们发现了属性name还有set与get方法,其中set方法依赖于runtime中的objc_setProperty

我们通过源码来查看一下objc_setProperty的底层实现

bash 复制代码
// 定义静态内联函数,用于设置对象的属性值。
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    // 如果偏移量为0,直接将newValue设置为对象的类(可能用于特殊的目的,如改变对象的动态类型)
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    // 定义用来保存旧值的变量
    id oldValue;
    // 计算属性值在内存中的实际位置
    id *slot = (id*) ((char*)self + offset);

    // 如果指定了copy标志,则对newValue执行不可变拷贝
    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } 
    // 如果指定了mutableCopy标志,则对newValue执行可变拷贝
    else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } 
    // 如果没有指定拷贝,检查newValue是否已经是当前值,如果是,则无需操作;否则,增加newValue的引用计数
    else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    // 如果不是原子操作,直接更新内存位置的值
    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } 
    // 如果是原子操作,使用锁来保证线程安全
    else {
        spinlock_t& slotlock = PropertyLocks[slot]; // 获取与属性位置相关联的锁
        slotlock.lock();                            // 锁定
        oldValue = *slot;                          // 取出旧值
        *slot = newValue;                          // 设置新值
        slotlock.unlock();                         // 解锁
    }

    // 释放旧值的引用,以防内存泄漏
    objc_release(oldValue);
}

其方法原理就是retain新值->设置新值->释放旧值

四、类 & 类结构分析

本篇章的主要目的是分析 类 & 类的结构,整篇都是围绕一个类展开的一些探索

isa指针是什么

OC是一门面向对象编程的语言,每个对象都是类的实例,同时也被称为实例对象,同时每个对象都有一个isa指针,指向对象所属的类

另外我们打开NSObject源码

bash 复制代码
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

由此可知类也是一个对象,简称为类对象

类的分析

首先我们定义两个类

继承自NSObject的类CJLPerson

bash 复制代码
@interface CJLPerson : NSObject
{
    NSString *hobby;
}
@property (nonatomic, copy) NSString *cjl_name;
- (void)sayHello;
+ (void)sayBye;
@end

@implementation CJLPerson
- (void)sayHello
{}
+ (void)sayBye
{}
@end

继承自CJLPerson的类CJLTeacher

bash 复制代码
@interface CJLTeacher : CJLPerson
@end

@implementation CJLTeacher
@end

在main中分别用两个定义两个对象:person & teacher

bash 复制代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //ISA_MASK  0x00007ffffffffff8ULL
        CJLPerson *person = [CJLPerson alloc];
        CJLTeacher *teacher = [CJLTeacher alloc];
        NSLog(@"Hello, World! %@ - %@",person,teacher);  
    }
    return 0;
}

元类

首先我们通过一张图片引入元类

根据调试过程,我们产生了一个疑问:为什么图中的p/x 0x001d8001000022dd & 0x00007ffffffffff8ULLp/x 0x00000001000022b0 & 0x00007ffffffffff8ULL 中的类信息打印出来都是CJLPerson

  • 0x001d8001000022ddperson对象的isa指针地址,其&后得到的结果是 创建person的类CJLPerson
  • 0x00000001000022b0isa中获取的类信息所指的类的isa的指针地址,即 CJLPerson类的类 的isa指针地址,在Apple中,我们简称CJLPerson类的类为 元类
    所以,两个打印都是CJLPerson的根本原因就是因为元类导致的

元类的说明

下面我们来解释一下什么是元类

首先我们知道对象的isa指向类,同时我们前文也说了类也是一个对象,那么类也有isa指针,类的isa指针指向的就是元类

元类是系统给的,当我们创建类时会自动创建元类,类的归属来自于元类

首先我们之前的博客有分析过元类【iOS】isKindOfClass & isMemberOfClass比较

我们这里引出一个问题,NSObject到底有几个?

从这张图中我们可以看出根元类NSObject只有一个,这个与我们日常开发的NSObject是同一个吗

我们通过代码来验证一下

可以看出打印出的地址为同一个,所以NSObject只有一个,即类对象与元类都只有一个,而实例对象可以有很多个

bash 复制代码
 NSObject *object1 = [[NSObject alloc] init];
 NSObject *object2 = [[NSObject alloc] init];
 // 打印两个实例的内存地址
 NSLog(@"object1: %p", object1);
 NSLog(@"object2: %p", object2);
 // 获取并打印NSObject的类对象
 Class objectClass1 = [object1 class];
 Class objectClass2 = [object2 class];
 NSLog(@"NSObject class: %p. %p", objectClass1, objectClass2);
 // 获取并打印NSObject的元类对象
 Class metaClass1 = object_getClass(objectClass1);
 Class metaClass2 = object_getClass(objectClass2);
 NSLog(@"NSObject meta-class: %p, %p", metaClass1, metaClass2);

[面试题]:类存在几份?

由于类的信息在内存中永远只存在一份,所以 类对象只有一份

五、著名的isa走位 & 继承关系图

这里的知识之前已经说过了,这里不再赘述

六、objc_class & objc_object

isa的走位我们清楚了,现在我们来讨论一个新问题,为什么对象和类会有isa属性,这里就引出了objc_class & objc_object

首先我们通过之前的源码知道:
NSObject的底层编译是NSObject_IMPL结构体

首先我们知道Classisa指针的类型,是由objc_class定义的类型
objc_class实际上就是Class

在iOS中所有的Class都是以objc_class为模版创建的

我们查看一下objc_class的源码

我们可以看到objc_class结构体是继承自objc_object

再搜寻objc_object的定义

【问题】objc_class 与 objc_object 有什么关系?

通过查看源码我们可以得出几点说明:

  • objc_class继承自objc_object类型,其中objc_object也是一个结构体,且有一个isa类型
  • 在cpp底层编译中,isa的类型是Class,class的底层编码来自objc_class,因此NSObject也有了isa属性
  • objc_object(结构体)是当前的根对象,所有的对象都有一个特性objc_object,即拥有isa属性

objc_object 与 对象的关系

  • 是一个继承关系,所有对象都是由objc_object继承过来的

  • 所有的对象都是来自NSObject,但最后到底层都是一个objc_object(C/C++)结构体类型

总结:

所有对象+类+元类 都有isa属性

所有对象都是由objc_object继承来的

objc_class结构

首先我们在源码中找到其结构

bash 复制代码
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}

isa:主要指向类对象或是元类对象
superclass:指向当前类的父类
cache:方法缓存,提高调用方法的性能
bits:封装了类的其他信息,例如成员变量,方法列表,协议,属性

元类对象结构也是这样,只不过元类对象里面存放的是类方法

superClass

这里需要注意superclass是objc_class特有的,实例对象是没有的

bits

类结构中还有一个bits

我们通过源码查看一下其数据结构class_data_bits_t

bash 复制代码
struct class_data_bits_t {
    friend objc_class;
    // Values are the FAST_ flags above.
    uintptr_t bits;
    public:
    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    // Get the class's ro data, even in the presence of concurrent realization.
    // fixme this isn't really safe without a compiler barrier at least
    // and probably a memory barrier when realizeClass changes the data field
    const class_ro_t *safe_ro() const {
        class_rw_t *maybe_rw = data();
        if (maybe_rw->flags & RW_REALIZED) {
            // maybe_rw is rw
            return maybe_rw->ro();
        } else {
            // maybe_rw is actually ro
            return (class_ro_t *)maybe_rw;
        }
    }
}

其中最重要的两个方法就是datasafe_ro,两个方法分别返回class_rw_tclass_ro_t

class_rw_t

首先可以看到三个方法,可以分别获取到类的方法,属性,协议

class_ro_t

可以看到有方法、属性、协议和成员变量。但方法、属性、协议的命名都是base开通的

ro与rw的区别

ro是编译阶段生成,rw是运行阶段生成,从存储的角度来说,ro中有方法,属性,协议与成员变量,而rw中没有成员变量,rw中的取值方法也是通过取ro或是rwe的值来获得的

class_rw_ext_t

在2020WWDC中有个视频对ro与rw进行了解释,由于rw是一块脏内存,但是rw总有许多用不到的数据,我们将rw中那些一般用不到的数据分离出来变为干净的内存,也就是rw_ext_t,这样就减轻了rw对内存的占用

rw_ext_t生成条件:

  • 使用分类的类

  • 使用Runtime API动态修改类的结构的时候

这两种情况时,由于类的结构发生改变,但是ro是只读的,因此需要重新生成可读可写的内存结构rw_ext(Dirty Memory, 比较贵),来存放新的类结构。

由此再次读取方法,属性,列表时如果有rw_ext,就会先从rw_ext中读取,如果没有再去读取ro

参考博客:
iOS八股文(四)类对象的结构(下)

思考:为什么类方法存储在元类中,而不把类方法存储在类对象中?或者说设置元类的目的是什么?

  1. 单一职责设计原理 :一个对象或是一个类只应该有一个职责,类对象负责实例对象的行为,例如实例方法,协议,成员变量,属性等,元类对象负责类对象的行为,负责存放类方法,各司其职互不影响
  2. 符合消息转发机制
  3. 继承模型的一致性 :在Objective-C中,类方法的继承与实例方法的继承遵循相同的模式。如果没有元类,类方法的继承将需要另外一套机制来处理。

cache_t结构

cache的作用是在objc_msgSend过程中会先在cache中根据方法名来hash查找方法实现,如果能查找到就直接掉用。如果查找不到然后再去rw_t中查找。然后再在cache中缓存。

总结

通过这篇文章我们得知

  1. 所有对象都有isa属性
  2. 所有对象都是由objc_object继承而来的
  3. objc_class中存放着对象的各种信息,实例对象则存放成员变量,类对象则存放实例方法与属性等,元类对象则存放类方法,符合单一职责原则
  4. isa 指针指向对象所属的类
  5. 可以从bits中的rw中查找方法属性,但不能查找到成员变量,成员变量存储在ro中,这也是为什么分类不能添加成员变量的原因
相关推荐
若水无华1 天前
fiddler 配置ios手机代理调试
ios·智能手机·fiddler
Aress"1 天前
【ios越狱包安装失败?uniapp导出ipa文件如何安装到苹果手机】苹果IOS直接安装IPA文件
ios·uni-app·ipa安装
Jouzzy2 天前
【iOS安全】Dopamine越狱 iPhone X iOS 16.6 (20G75) | 解决Jailbreak failed with error
安全·ios·iphone
瓜子三百克2 天前
采用sherpa-onnx 实现 ios语音唤起的调研
macos·ios·cocoa
左钦杨2 天前
IOS CSS3 right transformX 动画卡顿 回弹
前端·ios·css3
努力成为包租婆2 天前
SDK does not contain ‘libarclite‘ at the path
ios
安和昂3 天前
【iOS】Tagged Pointer
macos·ios·cocoa
I烟雨云渊T3 天前
iOS 阅后即焚功能的实现
macos·ios·cocoa
struggle20253 天前
适用于 iOS 的 开源Ultralytics YOLO:应用程序和 Swift 软件包,用于在您自己的 iOS 应用程序中运行 YOLO
yolo·ios·开源·app·swift
Unlimitedz3 天前
iOS视频编码详细步骤(视频编码器,基于 VideoToolbox,支持硬件编码 H264/H265)
ios·音视频