OC底层原理(三):objc对象本质与内存

643 阅读10分钟

一、对象的本质

探究方式:编译还原

在底层探索的过程中,由于Objective-c语言是C语言和C++的超集,那么可以通过clang轻量级编译器的代码还原,还原OC代码的基层实现,或者说用来查看OC代码的底层结构以及过程结构。clang可以将.m文件编译.cpp文件。

Clangxcrun

Clang是什么

  1. ClangCC++Objective-c语言的轻量编译器。源代码发布于BSD协议下。
  2. Clang将支持lambad表达式,返回类型的简单处理以及更好的处理constexpr关键字。
  3. ClangApple主导编写,基于LLVMC/C++/Objective-c编译器

xcrun是什么

  1. xcrunXcode 基本的命令行工具,在clang的基础上进行了封装,使用更加方便。

准备阶段:

  • 直接将main.m编译main.cpp文件,要将依赖的平台系统要求加入。

clang指令编译

clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m

xcrun指令编译

  1. 模拟器:
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o 
main-arm64.cpp
  1. 真机:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o 
main-arm64.cpp

分析阶段:

  • main.m文件源码,声明了一个LGPersonclass类用于编译成main.cpp

objc部分源码:

#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;
}

编译的c++部分源码:

关注点一、struct (LGPerson_IMPL):

main.cpp文件看到LGPerson对应的地方:

#ifndef _REWRITER_typedef_LGPerson
#define _REWRITER_typedef_LGPerson
typedef struct objc_object LGPerson;
typedef struct {} _objc_exc_LGPerson;
#endif

extern "C" unsigned long OBJC_IVAR_$_LGPerson$_KCName;
struct LGPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *__strong _KCName;
};
  • 结论一:针对上面声明与.cpp文件源码的对比,可以得出结论对象在底层的本质就是结构体

关注点二、isa结构体 (NSObject_IMPL):

LGPerson_IMPL结构体中看到了struct NSObject_IMPL NSObject_IVARS;在.cpp文件中查找NSObject_IMPL

struct NSObject_IMPL {
	__unsafe_unretained Class isa;
};
  • 结论二NSObject_IVARS成员变量isa

关注点三、objc_object

LGPerson继承的是NSObject,但是在.cpp文件中确是objc_object类型、搜索其结构体组成

typedef struct objc_object NSObject;

struct objc_object {
    Class _Nonnull isa __attribute__((deprecated));
};

typedef struct objc_class *Class;

typedef struct objc_object *id;

typedef struct objc_selector *SEL;

结论三NSObject对象在底层的对象是结构体objc_object,而objc_object的主要成员Class isa,而Classobjc_class结构体指针!在过程中发现id的类型是objc_object *,所以可以定义任何类型的变量。

关注点四、getter

setter 隐藏参数:在.cpp文件中查找LGPersongetset方法

extern "C" unsigned long OBJC_IVAR_$_LGPerson$_KCName;
struct LGPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *__strong _KCName;
};

// @property (nonatomic, strong) NSString *KCName;

/* @end */


// @implementation LGPerson

//方法:隐藏参数
static NSString * _I_LGPerson_KCName(LGPerson * self, SEL _cmd) { 
    return (*(NSString *__strong *)((char *)self + OBJC_IVAR_$_LGPerson$_KCName)); 
}
static void _I_LGPerson_setKCName_(LGPerson * self, SEL _cmd, NSString *KCName) { 
    (*(NSString *__strong *)((char *)self + OBJC_IVAR_$_LGPerson$_KCName)) = KCName; 
}

结论四:在GetSet方法中看到了两组参数LGPserson * selfSEL _cmd这是隐藏参数,我们通常创建的方法默认携带这两个参数,这也就是为什么我们在每一个方法里面都可以使用self的原因。set方法是就是获取到对象的地址然后移位查找变量内存

Screenshot 2021-06-20 at 4.56.19 PM.png

Screenshot 2021-06-20 at 4.59.53 PM.png

二、结构体、联合体、位域

案例一、布尔变量的struct

直接打印正常的结构体,不做位域处理

#import <Foundation/Foundation.h>

// 4 * 8 = 32  0000 0000 0000 0000 0000 0000 0000 1111
// 4 位
// 1 字节 3倍浪费
struct LGCar1 {
    BOOL front; // 0 1
    BOOL back;
    BOOL left;
    BOOL right;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        struct LGCar1 p1;
        NSLog(@"---%lu---",sizeof(p1));
    }
    return 0;
}
  • 打印sizeof结果为:p1占用4字节空间
2021-06-12 19:46:51.463279+0800 001-联合体位域[98512:2394618] ---4---

案例二、位域处理的struct

对结构体做位域处理,让每个布尔值只占用一个1bit (1byte = 8bit):

// 位域
// 互斥
// 0000 1111
struct LGCar2 {
    BOOL front: 1;
    BOOL back : 1;
    BOOL left : 1;
    BOOL right: 1;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        struct LGCar2 p2;
        NSLog(@"---%lu---",sizeof(p2));
    }
    return 0;
}
  • 打印sizeof结果为:p2占用1字节空间
2021-06-20 17:30:16.330232+0800 001-联合体位域[4793:5619332] 1

案例三、修改位域值的struct

改变位域值,改变结构体占用字节数

// 位域
// 互斥:只能单方向:先前占位,不可以同时向后占位
// 后面代表占用位数
struct LGCar2 {
    BOOL front: 1; // 0000 0000 0001
    BOOL back : 2; // 0000 0000 0110
    BOOL left : 6; // 0001 1111 1000
    BOOL right: 1; // 0010 0000 0000
};
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        struct LGCar2 p2;
        NSLog(@"---%lu---",sizeof(p2));
    }
    return 0;
}
  • 打印sizeof结果为:p2占用2字节空间,由于结构体成员变量本只占10bits,根据内存字节对齐原则可得到2byte.
2021-06-20 17:33:58.893876+0800 001-联合体位域[4906:5623242] 2

案例四、不同类型变量的struct

通过断点来分析对结构体的赋值时的内存变化

// 共存
struct LGTeacher1 {
    char        *name;
    int         age;
    double      height ;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        struct LGTeacher1   teacher1;
        teacher1.name = "Cooci";// 断点1
        teacher1.age  = 18;     // 断点2
        NSLog(@"Hello, World!");// 断点3
    }
    return 0;
}
  • teacher1的赋值过程teacher1.name = @"BBLv"teacher1.age设置了断点,分别打印了在当下状态下的teacher1的信息
(lldb) p teacher1
(LGTeacher1) $0 = (name = 0x0000000000000000, age = 0, height = 0)
(lldb) p teacher1
(LGTeacher1) $1 = (name = "Cooci", age = 0, height = 0)
(lldb) p teacher1
(LGTeacher1) $2 = (name = "Cooci", age = 18, height = 0)
(lldb) 
  • 图解补充:

Screenshot 2021-06-20 at 5.43.15 PM.png

案例五、不同类型变量的union

通过断点来分析对联合体的赋值时的内存变化

// 联合体 : 互斥
union LGTeacher2 {
    char        *name;
    int         age;
    double      height ;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
       
        union LGTeacher2    teacher2;
        teacher2.name = "Cooci";        // 断点1
        teacher2.age  = 18;             // 断点2
        NSLog(@"%ld",sizeof(teacher2)); // 断点3
       
    }
    return 0;
}
  • 对联合体teacher2对象的赋值过程teacher2.nameteacher2.age设置了断点,分别打印了在当下状态下的teacher2的信息。
(lldb) p teacher2
(LGTeacher2) $0 = (name = 0x0000000000000000, age = 0, height = 0)
(lldb) p teacher2
(LGTeacher2) $1 = (name = "Cooci", age = 15966, height = 2.1220036792173738E-314)
(lldb) p teacher2
(LGTeacher2) $2 = (name = "", age = 18, height = 2.1219957998584539E-314)
2021-06-20 17:49:48.308431+0800 001-联合体位域[5287:5637154] 8
  • 图解补充:

Screenshot 2021-06-20 at 5.50.54 PM.png

案例六、包含不同变量的struct的union

// 共存
struct LGTeacher1 {
    char        *name; // 8 [0 1 2 3 4 5 6 7]
    int         age;   // 4 [8 9 10 11 12]
    double      height ;//8 (13 14 15 [16 17 18 19 ... 23])在以最大变量字节倍数为基,刚好8*3=24
};

// 联合体 : 互斥
union LGTeacher2 {
    char        *name;
    int         age;
    double      height ;
    struct LGTeacher1 teacher;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        struct LGTeacher1   teacher1;
        teacher1.name = "Cooci";
        teacher1.age  = 18;

        union LGTeacher2    teacher2;
        teacher2.name = "Cooci";     // 断点1
        teacher2.age  = 18;          // 断点2
        teacher2.teacher = teacher1; // 断点3
        NSLog(@"%ld",sizeof(teacher2)); //断点4
      
        NSLog(@"Hello, World!");
    }
    return 0;
}
  • 对联合体teacher2对象的赋值过程teacher2.nameteacher2.ageteacher2.teacher设置了断点,分别打印了在当下状态下的teacher2的信息。发现即使有结构体变量,还是最大成员变量作为内存字节。
(lldb) p teacher2
(LGTeacher2) $0 = {
  name = 0x0000000100000000 "\317\372\355\376"
  age = 0
  height = 2.1219957909652723E-314
  teacher = (name = "\317\372\355\376", age = 0, height = 0)
}
(lldb) p teacher2
(LGTeacher2) $1 = {
  name = 0x0000000100003e5e "Cooci"
  age = 15966
  height = 2.1220036792173738E-314
  teacher = (name = "Cooci", age = 0, height = 0)
}
(lldb) p teacher2
(LGTeacher2) $2 = {
  name = 0x0000000100000012 ""
  age = 18
  height = 2.1219957998584539E-314
  teacher = (name = "", age = 0, height = 0)
}
(lldb) p teacher2
(LGTeacher2) $3 = {
  name = 0x0000000100003e5e "Cooci"
  age = 15966
  height = 2.1220036792173738E-314
  teacher = (name = "Cooci", age = 18, height = 0)
}
2021-06-20 17:54:59.714195+0800 001-联合体位域[5403:5641254] 24

Screenshot 2021-06-20 at 5.59.15 PM.png

结论:

  1. 通过案例一结构体内声明了4bool类型,占用4字节内存
  2. 通过案例二对结构体内声明的4bool类型进行指定位域,指定每一个bool类型的成员变量使用1bit的内存空间,那么4个bool类型最终占用4bit空间,即0.5字节的空间,最终占用了1字节内存,为对象指定位域是内存优化的方式
  3. 通过案例三与案例四,也就是structunion的区别,struct成员变量的存储互不影响union内的对象存储是互斥
  4. 结构体(struct)中所有的变量是共存的,优点是可以存储所有的对象的值,比较全面。缺点是struct内存空间分配是粗放的,不管是否被使用,全部分配
  5. 联合体(union)中所有的变量是互斥的,优点是内存使用更加精细灵活,也节省了内存空间,缺点也很明显,就是不够包容
  6. 即使有结构体(struct)变量的联合体(union),内存还是以最大占用字节的变量为主

三、nonpointerIsa初探

如何找到isa

  1. 在对象alloc过程中执行_class_createInstanceFromZone方法中,会执行initIsa方法将objclass进行绑定,这里删除了跟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部分无关代码......
}
  1. 然后可能进入initInstanceIsa函数或者initIsa函数,但是initInstanceIsa函数执行后依然会进入到initIsa
inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}
  1. 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;
}
  1. 可以看到源码内声明isa的类型是isa_t,而isa_t的类型是联合体(union)
// nonPinterIsa 无指向的指针:(地址指针|其他功能)
// 类 指真
// 8 * 8 = 64
// 其他功能:是否释放、引用计数、weak、关联对象、析构函数
// 要看:位域
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);
};
  1. 找到isa指针内存了什么
  • 通过宏定义ISA_BITFIELD找到了isa指针内都存放了什么,分为两种模式

arm64:

define ISA_MASK        0x0000000ffffffff8ULL
#     define ISA_MAGIC_MASK  0x000003f000000001ULL
#     define ISA_MAGIC_VALUE 0x000001a000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 1
#     define ISA_BITFIELD                                                      \
        uintptr_t nonpointer        : 1;       //是否为纯指针                                \
        uintptr_t has_assoc         : 1;       //关联对象                                \
        uintptr_t has_cxx_dtor      : 1;       //C++的析构函数                               \
        uintptr_t shiftcls          : 33; /     *MACH_VM_MAX_ADDRESS 0x1000000000*/ \           //类的指针地址
        uintptr_t magic             : 6;                                       \
        uintptr_t weakly_referenced : 1;        //弱引用                             \
        uintptr_t unused            : 1;        //是否引用                               \
        uintptr_t has_sidetable_rc  : 1;        //散列表                               \
        uintptr_t extra_rc          : 19        //引用计数
#     define RC_ONE   (1ULL<<45)
#     define RC_HALF  (1ULL<<18)

x86_64:

#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_HAS_CXX_DTOR_BIT 1
#   define ISA_BITFIELD                                                        \
      uintptr_t nonpointer        : 1;       //是否为纯指针                                    \
      uintptr_t has_assoc         : 1;       //关联对象                                  \
      uintptr_t has_cxx_dtor      : 1;       //C++的析构函数                                \
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \    //类的指着地址
      uintptr_t magic             : 6;                                         \
      uintptr_t weakly_referenced : 1;      //弱引用                                  \
      uintptr_t unused            : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;       //散列表                                  \
      uintptr_t extra_rc          : 8       //引用计数
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

图形分析:

Screenshot 2021-06-21 at 12.02.20 AM.png

Screenshot 2021-06-21 at 12.03.11 AM.png

Screenshot 2021-06-21 at 12.04.30 AM.png

Screenshot 2021-06-21 at 12.05.07 AM.png

四、isa的位运算,还原类信息

案例代码:

对象通过掩码->class

#import <Foundation/Foundation.h>
#import "LGPerson.h"

//1:关于对象的本质
//2:nonPointerIsa

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        LGPerson *p = [LGPerson alloc];
        NSLog(@"%@",p);
    
    }
    return 0;
}
  • 位运算测试结果:
(lldb) x/4gx p
0x10060eee0: 0x011d800100008275 0x0000000000000000
0x10060eef0: 0x0000000000000000 0x0000000000000000
0x10060eee0: 0x011d800100008275 0x0000000000000000
0x10060eef0: 0x0000000000000000 0x0000000000000000
(lldb) p/x LGPerson.class
(Class) $2 = 0x0000000100008270 LGPerson
(lldb) p/x 0x0000000100008270 & 0x00007ffffffffff8ULL
(unsigned long long) $3 = 0x0000000100008270
(lldb) p/x 0x011d800100008275 & 0x00007ffffffffff8ULL
(unsigned long long) $4 = 0x0000000100008270
(lldb) p 0x011d800100008275 >> 3
(long) $5 = 10045138768236622
(lldb) p/x 0x011d800100008275 >> 3
(long) $6 = 0x0023b0002000104e
(lldb) p/x 0x0023b0002000104e << 20
(long) $7 = 0x0002000104e00000
(lldb) p/x 0x0002000104e00000 >> 17
(long) $8 = 0x0000000100008270
(lldb) p/x LGPerson.class
(Class) $9 = 0x0000000100008270 LGPerson
(lldb) 
  • 图形分析:

Screenshot 2021-06-21 at 4.37.21 PM.png

  • 测试所用架构为x86_64的架构,初始化的对象的isa的bit位信息第3号标志位至47号标志位为LGPerson的信息,还原方式为:
  1. 通过x/4gx person,格式化输出person对象的内存地址,首地址为isa指针0x011d8001000080e9

  2. isa前三个bit位移除,即0x011d8001000080e9向右移3位,通过p/X 得到新的isa指针0x0023b0002000101d

  3. isa指针的后17个bit位移除,由于刚刚向右移了3个bit位,那么现在需要向左移20个bit位,即0x0002000101d00000向左移17+3位,通过p/x得到新的isa指针0x0002000101d00000

  4. 最后将isa内代表LGPerson信息的33个bit位还原,将0x0002000101d00000向右移17个bit位

  5. 最后输出的结果为LGPerson