美文网首页iOS底层
深入理解OC中的对象

深入理解OC中的对象

作者: 番茄炒西红柿啊 | 来源:发表于2020-09-09 10:23 被阅读0次

大纲

  • oc中对象的本质是什么
  • 创建一个对象占用多少内存
  • alloc和init分别是如何工作的
    • 如何定位到官方源码库
    • alloc 的三部曲
  • 对象的种类有哪些
    • 实例对象(instance-class)
    • 类对象 (class)
    • 元类对象 (meta-class)
  • isa 指针
  • 后续有时间会继续补充内容

OC中对象的本质

开发中编写的oc代码会先编译成c/c++,然后成汇编最后转成机器语言,occ/c++汇编机器语言。我们用创建一个main.m文件定义一个类CWObject

interface CWObject: NSObject{}
@end
  
@implementation CWObject
@end

调用命令行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m,将其编译成c/c++代码main.cpp.

struct CWObject_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
};

struct NSObject_IMPL {
    Class isa;
};

可以看到OC中的CWObjectmain.cpp中变成了struct CWObject_IMPL,所以oc中的对象本质上是结构体指针。

创建一个对象占用多少内存

以NSObject为例

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>

NSObject *obj = [[NSObject alloc] init];
// 创建一个实例对象,需要的内存空间大小
NSLog(@"%zu", class_getInstanceSize([NSObject class])); // 8
// 创建一个实例对象,操作系统实际分配的内存空间大小
NSLog(@"%zu", malloc_size((__bridge const void *)obj)); // 16

可以看出,创建一个实例对象需要的内存大小系统实际分配的大小是有出入的。这里不一样的原因涉及到内存对齐问题。

alloc和init分别是如何工作的

NSObject *objc1 = [NSObject alloc];

如上代码,我们使用Xcode调试跟踪代码alloc,是定位不到源码的。因此通过这种方法我们并不能知道alloc内部做了些什么操作。所以我们需要一些方法来定位到源码。定位源码的方式有3种:
方法一:

  • 先在alloc这一行打上断点


  • 进入断点后,我们添加一个符号断点,操作如图:


    Jietu20200908-205759.gif
  • 如图可以看到红框部分libobjc.A.dylib,这即是源码库.

方法二:

  • 同方法一第一步类似,在alloc那一行打上断点,进入断点后按住control键后,点击下图红框部分
  • 直到出现如下信息:
  • 此时在打上一个符号断点,输入上图显示的objc_alloc.可以看到符号断点信息如图: 后面的libobjc就是源码所在的库。

方法三:

  • alloc打上断点后开启汇编调试


  • 后面的步骤同方法二类似按住control,点击 step into 键,执行到下图的callq ,对应 objc_alloc


  • 按住control,点击 step into 键进入,看到断点断在objc_alloc部分


  • 到了这一步就同方法二相同了,打上符号断点可知源码库在哪

    通过上面三种方式找到源码所在的库后,我们就可以在Apple源码库上搜索并下载对应的源码了。

虽然找到了源码库,但是其实并不方便调试,如果我们能编译源码并直接通过control+cmd+左键可以直接跳转到对应的源码块就实在太方便了。可以参考此文源码编译调试

做完上面的准备工作之后,我们终于可以愉快的调试了。我们一层一层的点进去看源码.



调用alloc大致整个流程如下:



alloc之后调用init又做了哪些操作呢?我们先看一段代码:
LGPerson *p = [LGPerson alloc];
LGPerson *p1 = [p init];
LGPerson *p2 = [p init];
NSLog(@"%@ - %p", p, p);
NSLog(@"%@ - %p", p1, p1);
NSLog(@"%@ - %p", p2, p2);

控制台输出如下:

 <LGPerson: 0x100656990> - 0x100656990
 <LGPerson: 0x100656990> - 0x100656990
 <LGPerson: 0x100656990> - 0x100656990

p, p1, p2这三个指针指向的都是同一个地址。查看源码可以看到init并没有做其他额外操作,而是直接返回了self。

+ (id)init {
    return (id)self;
}

- (id)init {
    return _objc_rootInit(self);
}

id
_objc_rootInit(id obj)
{
    // In practice, it will be hard to rely on this function.
    // Many classes do not properly chain -init calls.
    return obj;
}

竟然init没有做任何操作,那么其存在的意义是什么呢。这里涉及到了工厂设计模式的思想。单纯的alloc并不能满足开发者的需求,我们平时需要大量的使用自定义的类去自定义构造方法,此时init就是官方暴露出来的自定义接口。这也是为什么我们重写构造方法都是重写init方法。
创建一个对象还可以使用new方法LGPerson *p = [LGPerson new];点进去源码如下:

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}

new其实就是alloc和init的简写。但是new的弊端就是只会调用init方法,如果你自定义了类似initWithXXXX这样的构造方法是不会被new方法触发的。

OC中的对象的种类有哪些:

  • 实例对象(instance)
  • 类对象 (class)
  • 元类对象 (meta-class)
实例对象

我们使用alloc, new等方式创建出来的对象就是实例对象,他们各自有自己的内存空间。

NSObject *obj1 = [[NSObject alloc] init];
NSObject *obj2 = [[NSObject alloc] init];

obj1和obj2分别是两个不同的实例对象。一个实例对象在内存空间存储的信息有isa指针成员变量

实例对象
类对象

当我们执行如下代码时:

NSObject *obj1 = [[NSObject alloc] init];
NSObject *obj2 = [[NSObject alloc] init];
NSLog(@"%p %p, %p, %p, %p,"
        obj1.class,
        obj2.class,
        object_getClass(obj1),
        object_getClass(obj2),
    [NSObject class]);

// log: 0x7fff94a14118 0x7fff94a14118, 0x7fff94a14118, 0x7fff94a14118, 0x7fff94a14118

可以发现同一个类创建的实例对象调用class或者object_getClass得到的对象都是同一个地址的对象。而这个对象就是类对象类对象是唯一的

类对象里存储的信息有isa指针,superclass指针,该类的成员变量信息,属性信息,实例方法信息,协议信息。

类对象存储信息
元类对象

接下我们还是调用object_getClass,但是传入的不再是实例对象,而是类对象:

NSLog(@"%p", object_getClass([NSObject class]));// 这里传入类对象,看看得到了什么

// 控制台:0x7fff94a140f0

可以看到这次得到的是个新地址,0x7fff94a140f0和之前的类对象0x7fff94a14118是不同的地址。而这个新的对象就是0x7fff94a14118(meta-class)。

我们可以用class_isMetaClass来验证一下其是否是元对象:

Class meta = object_getClass([NSObject class]);
NSLog(@"%p", meta);
BOOL isMeta = class_isMetaClass(meta);
NSLog(@"is meta : %hhd", isMeta);

// 控制台log: 0x7fff94a140f0
// 控制台log: is meta: 1

元类对象类对象的内存结构其实是一样的。在代码中他们都是用Class关键字来接收的。不同的是他们各自存储的主要信息不同。类对象存储的信息在上文中已经提到。元类对象主要存储的信息:isa指针,superclass指针,类方法信息。

元对象

三种类型对象总览:


类对象,元类对象结构源码如下:

具体结构解析请看:class结构浅析

isa指针

实例对象,类对象,元类对象,他们都有一个isa指针,那么这个指针到底是指向哪里的呢。三种类的isa和superclass指针关系使用一张图可以概括:


isa指针关系
  • 实例对象可以通过isa指针找到对应的类对象
  • 类对象可以通过isa指针找到对应的元类对象
  • 元类对象可以通过isa指针找到基类的元类对象
  • 基类的元类对象的isa指向它自己。

superclass指针关系

  • 子类类对象的superclass指针指向父类的类对象
  • 基类的类对象的superclass指针指向nil,
  • 子类元类对象的superclass指针指向父类的元类对象
  • 基类的元类对象的superclass指针指向基类的类对象(这一点可能有点违反我们的思维逻辑,但底层实现的确如此,后文会有验证)。

当我们访问一个实例对象的属性或者调用实例方法时,流程如下:

  1. 通过isa找到对应的类对象,查看类对象中是否有这个属性(或实例方法)的信息。
  2. 如果类对象中没有找到相关信息,就会通过类对象的superclass指针找到父类的类对象。查看其中是否有相关信息
  3. 以此类推,直到找到为止,如果一直到基类的类对象都没有找到,会抛出异常,也就是我们常见的unrecognized selector sent to instance

当我们调用类方法时,流程如下:

  1. 类对象通过isa找到对应的元类对象,查找元类对象中是否有对应的方法信息。
  2. 如果没有找到,再通过superclass指针去父类的元类对象中查找,以此类推。
  3. 如果在基类的元类对象中也没有找到,上文中已经提到过了,基类的元类对象的superclass指向的是基类的类对象。所以这里会最后再去基类类对象中查找。如果没找到则会出现unrecognized selector sent to instance报错。如果找到了会调用该方法。但是值得注意的是,此时该方法就不再是类方法了(+号开头),而是实例方法(-号开头),因为我们知道类对象中存放的是实例方法信息。

关于第三点,我们可以做代码验证:

  1. 定义一个CWObject类, 并申明一个类方法,但是我们并不实现它。
@interface CWObject: NSObject{}
+ (void)testMethod;
@end

@implementation CWObject

@end
  1. 我们给基类NSObject写个扩展,定义一个同名的实例方法:
@interface NSObject (Test)
- (void)testMethod;
@end

@implementation NSObject (Test)
- (void)testMethod {
    NSLog(@"instance test method");
}
@end
  1. 执行以下代码:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [CWObject testMethod];
    }
    return 0;
}
  1. 控制台打印如下:
OCMain[44774:2212449] instance test method

流程图如下:



拓展:上文中一直描述的是通过isa找到XXX,而不是isa指向XXX。这是因为64bit之前,isa指针的确直接指向的就是对应内容的地址。而64bit之后,isa需要进行一次位运算才能得到真实的地址。



我们通过代码验证一下
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CWObject *obj = [CWObject new];
        Class classObjc = [obj class];
    }
    return 0;
}
  1. 我们打上断点调试,拿到类对象的地址
(lldb) p/x classObjc
(Class) $2 = 0x0000000100002158 CWObject
  1. 通过访问实例对象的isa,获取其指向的地址
(lldb) p/x (long)obj->isa
(long) $3 = 0x001d800100002159

可以发现isa指向地址0x001d800100002159和类对象地址0x0000000100002158并不相同。

  1. 通过位运算
(lldb) p/x 0x001d800100002159 & 0x00007ffffffffff8
(long) $4 = 0x0000000100002158

位运算之后的地址0x0000000100002158和类对象地址一致。具体进一步了解参考链接

都看到这里了,点个赞再走呗!赠人玫瑰,手有余香哟!^_^ !

相关文章

  • 深入理解OC中的对象

    大纲 oc中对象的本质是什么 创建一个对象占用多少内存 alloc和init分别是如何工作的如何定位到官方源码库a...

  • Objective-C KVO总结

    看本章之前建议先打好OC对象的基础:深入理解OC中的对象 大纲 什么是KVO 场景代码示例 (后面的分析都是基于示...

  • 我所理解的iOS runtime

    从一下方面来深入研究: 理解面向对象的类到面向过程的结构体 深入理解OC消息转发机制 理解OC的属性propert...

  • 深入理解OC面向对象

    目录 1.面向对象1.三要素2.属性 2.深拷贝与浅拷贝1.Foundation框架中的对象2.自定义对象 3.对...

  • OC 的类和对象

    这篇文章主要想深入介绍一下,对于 OC 中的类和对象的更深层理解。 分组导航标记 在 OC 中有一种特殊语法,可以...

  • 共用体和位域的运用

    前言 深入理解OC中的对象[https://www.jianshu.com/p/d7d8f94dc1e3]一文中有...

  • OC 与 Swift

    OC对象的本质(上):OC对象的底层实现原理OC对象的本质(中):OC对象的种类OC对象的本质(下):详解isa&...

  • OC对象的本质(中)—— OC对象的种类

    OC对象的本质(上):OC对象的底层实现原理OC对象的本质(中):OC对象的种类OC对象的本质(下):详解isa&...

  • OC对象的本质(下)—— 详解isa&supercl

    OC对象的本质(上):OC对象的底层实现原理OC对象的本质(中):OC对象的种类OC对象的本质(下):详解isa&...

  • 深入理解OC中的属性

    深入理解OC中的属性 传统C++类实例变量写法 传统C++实例变量的定义形式 可通过关键字 @public 、@p...

网友评论

    本文标题:深入理解OC中的对象

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