码迷,mamicode.com
首页 > 其他好文 > 详细

底层一 : OC对象的本质

时间:2021-01-27 13:28:11      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:man   获取   结构   tar   nal   断点   ogr   nonatomic   dispatch   

OC对象本质是C++的结构体:因为对象涉及到不同类型,只有结构体能存储不同的结构体

OC对象

OC对象的本质

将OC代码转换成为C\C++代码

OC中的test方法会转化成c语言的方法

- (void)test {
    
}

其实是系统会传递两个参数过来

// self 方法调用者
// _cmd 方法名,等价于当前方法的selector,既@selector(test)
- (void)testId:(id)self _cmd:(SEL)_cmd {
    
}

//转化成CPP
void test(MJPerson* self, SEL _cmd) {
    
}

切换到文件目录,使用命令行
clang -rewrite-objc xxxx.m -o xxx.cpp
直接切换到iOS下64位系统可以使用的CPP代码
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc xxxx.m -o xxx.cpp

查看OC源码地址

OC源码 数字最大的是最新的

Core Foundation 硬性规定对象占用十六字节

面试题:

一个NSObject对象占用多少内存?

系统分配了16个字节NSObject对象(通过malloc_size函数获得)

NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)

查看过程:

 1. 将OC转换成C++代码,可以看到的NSObject的本质是一个结构体,其中包含了一个isa指针,占用了8个字节
// NSObject Implementation
struct NSObject_IMPL {
    Class isa; // 8个字节
};
 2. 查看源码
 size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

// Class‘s ivar size rounded up to a pointer-size boundary.
// 获取到类的实例对象的成员变量所占用的大小
    uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    }

查看alloc分配内存的过程

// 1.在源码中搜索allocWithZone,找到实现函数 

id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
    id obj;

#if __OBJC2__
    // allocWithZone under __OBJC2__ ignores the zone parameter
    (void)zone;
    obj = class_createInstance(cls, 0);
#else
    if (!zone) {
        obj = class_createInstance(cls, 0);
    }
    else {
        obj = class_createInstanceFromZone(cls, 0, zone);
    }
#endif

    if (slowpath(!obj)) obj = callBadAllocHandler(cls);
    return obj;
}

// 2. 查看class_createInstance函数
id 
class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}


id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    if (!cls) return nil;

    assert(cls->isRealized());

    // Read class‘s info bits all at once for performance
    bool hasCxxCtor = cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();

    size_t size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (!zone  &&  fast) {
        obj = (id)calloc(1, size);
        if (!obj) return nil;
        obj->initInstanceIsa(cls, hasCxxDtor);
    } 
    else {
        if (zone) {
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
        } else {
            obj = (id)calloc(1, size);
        }
        if (!obj) return nil;

        // Use raw pointer isa on the assumption that they might be 
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (cxxConstruct && hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls);
    }

    return obj;
}
// 3. 查看cls->instanceSize(extraBytes);

    size_t instanceSize(size_t extraBytes) {
        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;
        return size;
    }

可以看出Core Foundation框架要求对象至少占用16个字节

计算机中的正数用源码表示,负数用补码表示,而负数的补码是其反码+1 所以出现了-128
简单的说为了避免-0,负数的算法结果是每位都加了1

知识点

iOS是小端模式,在内存中从高位向低位读取数据
内存对其: 结构体的大小必须是最大成员大小的倍数
每个对象创建出来(通过alloc、init创建),方法都只有一份,因为方法是相同的,放到类的方法列表中

isa和superClass

instance对象、class对象、meta_class对象

一个类的类对象(class对象)是唯一的,在内存中只会开辟一份存储空间

isa指针向顺序

技术图片

class对象的superClass指针

技术图片

isa和superClass总结

技术图片

示例(self就是给谁发消息,不一定是当前类,谁接受消息,self就代表谁)

#import <Foundation/Foundation.h>

@interface NSObject (Test)

+ (void)test;

@end

//#import "NSObject+Test.h"

@implementation NSObject (Test)

//+ (void)test
//{
//    NSLog(@"+[NSObject test] - %p", self);
//}
//
- (void)test
{
    // self就是给谁发消息,不一定是当前类,谁接受消息,self就代表谁
    NSLog(@"-[NSObject test] - %p", self);
}

@end



@interface MJPerson : NSObject

+ (void)test;

@end

@implementation MJPerson

//+ (void)test
//{
//    NSLog(@"+[MJPerson test] - %p", self);
//}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"[MJPerson class] - %p", [MJPerson class]);
        NSLog(@"[NSObject class] - %p", [NSObject class]);
        // OC对象的调用方法,就是发送消息,发送过程中不会注意到是类方法还是对象方法
        [MJPerson test];
        // objc_msgSend([MJPerson class], @selector(test))
        // isa -> superclass -> suerpclass -> superclass -> .... superclass == nil
        
        [NSObject test];
        // objc_msgSend([NSObject class], @selector(test))
        
        /* 打印结果
         [MJPerson class] - 0x1000011e0
         [NSObject class] - 0x7fff8d775140
         // self就是给谁发消息,不一定是当前类,谁接受消息,self就代表谁,所以这里的打印虽然发生在NSObject的分类中,但是其实消息是发送给MJPerson的,所以打印出的self是MJPerson的类对象
         [NSObject test] - 0x1000011e0
         [NSObject test] - 0x7fff8d775140
         */

    }
    return 0;
}

isa指针细节

ISA_MASK 去源码里面获取
技术图片

检测方式

定义一个类MJPerson,创建一个instance对象和一个Class对象,断点打印对应的isa地址。

技术图片

因为类对象的isa指针无法通过程序直接打印,可以创建一个结构体模仿类的实现,然后进行打印

struct mj_objc_class {
    Class isa;
    Class superclass;
};

// 使用
struct mj_objc_class *personClass = (__bridge struct mj_objc_class *)([MJPerson class]);
从而得出
personClass->isa & ISA_MASK:0x00000001000014c8 = meta_class 

OC对象

KVO

未使用KVO监听的对象

技术图片

使用KVO监听的对象

技术图片

总结:使用了KVO,会产生一个MJPerson的子类(NSKVONotifying_MJPerson),会改变MJPersonisa指向新产生的子类,同时在新的子类里面会重写setAge:方法,以及新增三个方法

NSKVONotifying_MJPerson 类型 实现
setAge: 重写 实现Foundation_NSSetIntValueAndNotify方法
class 新增
dealloc 新增
_isKVO 新增

_NSSetIntValueAndNotify内部实现

1. [self willChangeValueForKey:@"age"]
2. 原来的setter实现
3. [self didChangeValueForKey:@"age"]

didChangeValueForKey:内部会调用observer的observeValueForKeyPath:ofObject:change:context:方法

_NSSet*ValueAndNotify 包括很多的基本数据类型
技术图片

KVC

技术图片

技术图片

category

最后面参与编译的分类,方法的实现在最前面

使用

1. 通过Runtime加载某个类的所有Category数据

2. 把所有Category的方法、属性、协议数据,合并到一个大数组中
后面参与编译的Category数据,会在数组的前面

3. 将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面

分类的加载思路

技术图片

使用msg_send发送消息的调用顺序1.分类 2.父类 (比对子类和父类没有意义)

+load方法

  • +load方法会在runtime加载类、分类写入内存时调用
  • 每个类、分类的+load,在程序运行过程中只调用一次
第一级调用顺序 第二级调用顺序
1.调用类的+load 按照编译先后顺序调用(先编译,先调用)
—— 调用子类的+load之前会先调用父类的+load
2.调用分类的+load 按照编译先后顺序调用(先编译,先调用)

+initialize方法

+initialize方法会在类第一次接收到消息时调用

调用顺序

  • 先调用父类的+initialize,再调用子类的+initialize
  • (先初始化父类,再初始化子类,每个类只会初始化1次)
  • 如果分类实现了+initialize,就覆盖类本身的+initialize调用 (父类的分类覆盖父类的,子类的分类覆盖子类的,相互之间不影响)
  • 如果子类实现了+initialize,多个子类只会调用一次父类的+initialize。如果子类没有实现,那么调用子类的+initialize会转化到调用父类的方法

+initialize+load的很大区别是,+initialize是通过objc_msgSend进行调用的,所以有以下特点

  • 如果子类没有实现+initialize,会调用父类的+initialize
  • 如果分类实现了+initialize,就覆盖类本身的+initialize调用

+initialize+load的区别

  • load方法是在冷启动运行时阶段,类加载进内存时候调用,initialize是在类第一次接收到消息时调用
  • initialize是通过objc_msgSend方法进行调用的,所以会首先加载分类的方法(依照objc_msgSend调用规律),load是直接找到方法地址,通过指针调取,会调用所有涉及到的load方法(包括分类的)

+initialize+load的联系
通常情况下两者都是只实现一次,不会重复实现。但是不排除重复实现的可能,譬如误操作直接调用这种,建议使用的时候加上dispatch_once 来使用

关联对象(给分类添加属性)

在类中声明一个属性,会做三件事情

1. 生成一个成员变量
2. 生成get和set方法的声明
3. 实现get和set方法的实现

在分类中写一个属性,只会自动生成getset方法的声明,不会生成方法实现,不会生成成员变量

默认情况下,因为分类底层结构的限制,不能添加成员变量到分类中。但可以通过关联对象来间接实现

关联对象提供了以下API

1. 添加关联对象
void objc_setAssociatedObject(id object, const void * key,
                                id value, objc_AssociationPolicy policy)

2. 获得关联对象
id objc_getAssociatedObject(id object, const void * key)

3. 移除所有的关联对象
void objc_removeAssociatedObjects(id object)

static意味着只有当前文件内部可以访问

key的常见用法

技术图片

objc_AssociationPolicy

objc_AssociationPolicy 对应的修饰符
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC copy, nonatomic
OBJC_ASSOCIATION_RETAIN strong, atomic
OBJC_ASSOCIATION_COPY copy, atomic

关联对象原理(底层实现)

技术图片

关联对象不会增加成员变量,不能通过person -> age 形式进行访问

底层一 : OC对象的本质

标签:man   获取   结构   tar   nal   断点   ogr   nonatomic   dispatch   

原文地址:https://www.cnblogs.com/CoderKJX/p/14329892.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!