美文网首页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中的对象

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