iOS底层

深入理解OC中的对象

2020-09-09  本文已影响0人  番茄炒西红柿啊

大纲

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种:
方法一:

方法二:

方法三:

虽然找到了源码库,但是其实并不方便调试,如果我们能编译源码并直接通过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中的对象的种类有哪些:

实例对象

我们使用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指针关系

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和类对象地址一致。具体进一步了解参考链接

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

上一篇下一篇

猜你喜欢

热点阅读