美文网首页
OC对象原理探究(下)

OC对象原理探究(下)

作者: 浅墨入画 | 来源:发表于2021-07-11 23:33 被阅读0次
介绍正文前,我们思考一个问题,什么是对象?或者说OC对象的本质是什么

对象本质以及拓展

在探索oc对象本质前,先了解一个编译器:clang

  • Clang是C语言、C++、Objective-c语言的轻量编译器。源代码发布于BSD协议下。
  • Clang将支持lambad表达式,返回类型的简单处理以及更好的处理constexpr关键字。
  • Clang是一个由Apple主导编写,基于LLVM的C/C++/Objective-c编译器
什么是xcrun
  • Xcode安装的时候一起安装了xcrun命令,xcrun在clang的基础上进行了封装,使用更加方便
探索对象本质
  • 在main中自定义一个类LGPerson,有一个属性name
<!-- main.m文件 -->
#import <Foundation/Foundation.h>
#import <objc/runtime.h>

// 对象在底层的本质就是结构体
@interface LGPerson : NSObject
@property (nonatomic, strong) NSString *KCName;
@end

@implementation LGPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
    }
    return 0;
}
  • 通过终端,利用clangmain.m编译成main.cpp,有以下几种编译命令,这里使用的是第一种
//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 
如果出现问题:
In file included from ViewController.m:9:
./ViewController.h:9:9: fatal error: 'UIKit/UIKit.h' file not found
#import <UIKit/UIKit.h>
        ^~~~~~~~~~~~~~~
1 error generated.
解决方案
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot /
Applications/Xcode.app/Contents/Developer/Platforms/
iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk main.m
  • 打开编译好的main.cpp,找到LGPerson的定义,发现LGPerson在底层会被编译成 struct 结构体
<!-- main.cpp文件 -->
struct LGPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_KCName;
};

// 查看结构体NSObject_IMPL
struct NSObject_IMPL {
    Class isa;
};

// LGPerson的本质类型是objc_object
typedef struct objc_object LGPerson;

// 通过类比LGPerson本质类型objc_object,在cpp文件中搜索objc_class
typedef struct objc_class *Class;

// 通常代表任何类型的id是什么
typedef struct objc_object *id;

// 在main.cpp文件中查找LGPerson的get set方法
static NSString * _I_LGPerson_KCName(LGPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_KCName)); }
static void _I_LGPerson_setKCName_(LGPerson * self, SEL _cmd, NSString *KCName) { (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_KCName)) = KCName; }
通过上述探索过程可以得出:
  • OC对象的本质其实就是结构体
  • LGPerson中的isa是继承自NSObject中的isa
  • NSObject对象在底层的对象是objc_object
  • 我们通常声明一个类时使用的Class的类型是objc_class *,是一个结构体指针
  • id的类型是objc_object *,所以可以定义任何类型的变量
  • 在Get与Set方法中看到了两组参数LGPserson * self、SEL _cmd这是隐藏参数,我们通常创建的方法默认携带这两个参数,这也就是为什么我们在每一个方法里面都可以使用self的原因

结构体 联合体 位域拓展

结构体一
#import <Foundation/Foundation.h>

struct LGCar1 {
    BOOL front; // 0 1
    BOOL back;
    BOOL left;
    BOOL right;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        struct LGCar1 car1;
        NSLog(@"---%lu---",sizeof(car1));
    }
    return 0;
}

// 打印结果是4
2021-07-11 21:35:43.363366+0800 001-联合体位域[18592:2045823] 4
  • 结构体内声明了4个BOOL类型,占用4字节内存
结构体二
// 位域
struct LGCar2 {
    BOOL front: 1;
    BOOL back : 1;
    BOOL left : 1;
    BOOL right: 1;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        struct LGCar2 car2;
        NSLog(@"---%lu---",sizeof(car2));
    }
    return 0;
}

// 打印结果是1
2021-07-11 21:43:40.195428+0800 001-联合体位域[18637:2051439] 1

// 如果LGCar2定义如下,就会占用2字节内存,2表示2bit
struct LGCar2 {
    BOOL front: 1;
    BOOL back : 2;
    BOOL left : 6;
    BOOL right: 1;
};
  • 对结构体内声明的4个BOOL类型进行指定位域,指定每一个bool类型的成员变量使用1bit的内存空间,那么4个bool类型最终占用4bit空间,即0.5字节的空间,最终占用了1字节内存为对象指定位域是内存优化的方式
结构体三
struct LGTeacher1 {
    char        *name;
    int         age;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        struct LGTeacher1   teacher1;
        teacher1.name = "kevin";
        teacher1.age  = 18;
    }
    return 0;
}

对teacher1的赋值过程teacher1.name = "kevin";、teacher1.age = 18;设置了断点,分别打印了在当前状态下的teacher1的信息

image.png
联合体一
// 联合体 : 互斥
union LGTeacher2 {
    char        *name;
    int         age;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        union LGTeacher2    teacher2;
        teacher2.name = "kevin";
        teacher2.age  = 18;
    }
    return 0;
}

对联合体teacher2对象的赋值过程teacher2.name、teacher2.age设置了断点,分别打印了在当前状态下的teacher2的信息

image.png
  • 通过结构体三与联合体一,也就是struct与union的区别,struct内成员变量的存储互不影响,union内的对象存储是互斥的
  • 结构体(struct)中所有的变量是共存的,优点是可以存储所有的对象的值,比较全面缺点是struct内存空间分配是粗放的,不管是否被使用,全部分配
  • 联合体(union)中所有的变量是互斥的,优点是内存使用更加精细灵活,也节省了内存空间缺点也很明显,就是不够包容

nonPointerisa分析

isa的类型 isa_t

以下是isa指针的类型isa_t的定义,从定义中可以看出是通过联合体(union)定义的。

union isa_t { //联合体
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    //提供了cls 和 bits ,两者是互斥关系
    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

isa_t类型使用联合体的原因也是基于内存优化的考虑,这里的内存优化是指在isa指针中通过char + 位域(即二进制中每一位均可表示不同的信息)的原理实现。通常来说,isa指针占用的内存大小是8字节即64位,已经足够存储很多的信息了,这样可以极大的节省内存,以提高性能

isa_t的定义中可以看出:提供了一个结构体定义的位域,用于存储类信息及其他信息。结构体的成员ISA_BITFIELD,这是一个宏定义,有两个版本 __arm64__(对应ios 移动端) 和__x86_64__(对应macOS),以下是它们的一些宏定义,如下图所示

image.png
  • nonpointer:表示是否对 isa 指针开启指针优化 0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等
  • has_assoc:关联对象标志位,0没有,1存在
  • has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
  • shiftcls:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
  • magic:用于调试器判断当前对象是真的对象还是没有初始化的空间
  • weakly_referenced:志对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放。
  • deallocating:标志对象是否正在释放内存
  • has_sidetable_rc:当对象引用技术大于 10 时,则需要借用该变量存储进位
  • extra_rc:当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc。

isa的位运算,还原类信息

  • main.m文件编写如下代码,并在LGPerson * person = [LGPerson alloc];添加断点,运行工程
<!-- main.m文件 -->
#import <Foundation/Foundation.h>
#import "LGPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson * person = [LGPerson alloc];
        NSLog(@"%@",person);    }
    return 0;
}

(lldb) x/4gx person
0x10064d420: 0x011d8001000080e9 0x0000000000000000
0x10064d430: 0x566265574b575b2d 0x74696e6920776569
(lldb) p 0x011d8001000080e9 >> 3
(long) $1 = 10045138768236573
(lldb) p/x 0x011d8001000080e9 >> 3
(long) $2 = 0x0023b0002000101d
(lldb) p/x 0x0023b0002000101d << 20
(long) $3 = 0x0002000101d00000
(lldb) p/x 0x0002000101d00000 >> 17
(long) $4 = 0x00000001000080e8
(lldb) p/x LGPerson.class
(class) $5 = 0x00000001000080e8 LGPerson
(lldb)

测试所用架构为x86_64的架构,初始化的对象的isa的bit位信息第3号标志位至47号标志位为LGPerson的信息,还原方式为:

  • 通过x/4gx person,格式化输出person对象的内存地址,首地址为isa指针0x011d8001000080e9
  • isa的前三个bit位移除,即0x011d8001000080e9向右移3位,通过p/X 得到新的isa指针0x0023b0002000101d
  • isa指针的后17个bit位移除,由于刚刚向右移了3个bit位,那么现在需要向左移20个bit位,即0x0002000101d00000向左移17+3位,通过p/x 得到新的isa指针0x0002000101d00000
  • 最后将isa内代表LGPerson信息的33个bit位还原,将0x0002000101d00000向右移17个bit位
  • 最后输出的结果为LGPerson
如何找到Isa
  • 在对象alloc过程中执行_class_createInstanceFromZone方法中,会执行initIsa方法将obj与class进行绑定,这里删除了跟isa部分无关的代码,只保留了isa相关的代码
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    //移除跟isa部分无关代码......
    
    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }
    
    //移除跟isa部分无关代码......
}
  • 然后可能进入initInstanceIsa函数或者initIsa函数,但是initInstanceIsa函数执行后依然会进入到initIsa
inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}
  • initIsa函数删除了与此次探索无关的代码
inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ 
    ASSERT(!isTaggedPointer()); 
    
    isa_t newisa(0);

    //删除了部分无关代码
    
    isa = newisa;
}
  • 可以看到源码内声明isa的类型是isa_t,而isa_t的类型是联合体(union)
union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    uintptr_t bits;

private:
    // Accessing the class requires custom ptrauth operations, so
    // force clients to go through setClass/getClass by making this
    // private.
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };

    bool isDeallocating() {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif

    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated);
    Class getDecodedClass(bool authenticated);
};

init与new的补充

<!-- LGPerson.h -->
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;
@end

NS_ASSUME_NONNULL_END

<!-- LGPerson.m -->
#import "LGPerson.h"

@implementation LGPerson
- (instancetype)init {
    if (self = [super init]) {
        self.name = @"kevin";
    }
    return self;
}
@end

<!-- main.m -->
#import <Foundation/Foundation.h>
#import "LGPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *p1 = [[LGPerson alloc] init];
        LGPerson *p2 = [LGPerson new];
    }
    return 0;
}

// 添加断点打印
(lldb) p p1.name
(__NSCFConstantString *) $0 = 0x0000000100004050 "kevin"
(lldb) p p2.name
(__NSCFConstantString *) $0 = 0x0000000100004050 "kevin"
得出结论
  • new 等价于 alloc init

相关文章

  • OC对象原理探究(下)

    介绍正文前,我们思考一个问题,什么是对象?或者说OC对象的本质是什么? 对象本质以及拓展 在探索oc对象本质前,先...

  • 初探OC对象原理(三)

    前言: 这是探究OC对象原理的第三章,也是按照对象的 的底层实现原理顺序来进行的。今天我们探究下对象的本质以及一...

  • iOS--OC底层原理文章汇总

    OC底层原理01—alloc + init + new原理OC底层原理02—内存对齐OC底层原理03— isa探究...

  • 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&...

  • 【1】OC对象原理探究

    1)了解OC运行底层入口 通常是直接进入main函数,通过插入断点,在工作台运行bt命令,可以得知线程调用状态,如...

  • OC对象底层原理探究

    1.runtime 是什么?回答:runtime是由C C++ 汇编为oc提供的一套运行时的api 2.以下代码输...

  • OC对象原理探究(上)

    前言:作为一名已经工作5年iOS开发人员,突然发现自己在底层方面的知识是如此的薄弱,甚至对一个APP的启动细节的认...

  • OC对象原理探究(上)

    APP启动流程探索 创建空工程代码如下,并且添加符号断点命名如下图 运行工程查看堆栈信息 红色为app启动过程: ...

网友评论

      本文标题:OC对象原理探究(下)

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