阅读 180

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

一、对象的本质

探究方式:编译还原

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

Clang与xcrun

什么是Clang

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

什么是xcrun

xcrun 是 Xcode 基本的命令行工具,在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指令编译

  • 模拟器:

xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o 
main-arm64.cpp
复制代码
  • 真机:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o 
main-arm64.cpp
复制代码

分析阶段:

main.m文件源码,声明了一个LGPerson的class类用于编译成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,而Class是objc_class的结构指针!在过程中发现id的类型是objc_object *,所以可以定义任何类型的变量。

关注点四getter

setter 隐藏参数:在.cpp文件中查找LGPerson的get set方法

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

复制代码

结论四:在Get与Set方法中看到了两组参数LGPserson * self、SEL _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字节空间,由于结构体成员变量本只占10bit,根据内存字节对齐原则可得到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";
        teacher1.age  = 18;
        
        NSLog(@"Hello, World!");
    }
    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";
        teacher2.age  = 18;
        NSLog(@"%ld",sizeof(teacher2));
       
    }
    return 0;
}
复制代码

对联合体teacher2对象的赋值过程teacher2.name、teacher2.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";
        teacher2.age  = 18;
        teacher2.teacher = teacher1;
        NSLog(@"%ld",sizeof(teacher2));
      
        NSLog(@"Hello, World!");
    }
    return 0;
}
复制代码

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

三、nonpointerIsa初探

如何找到isa?

  1. 在对象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部分无关代码......
}
复制代码
  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内代表LFPerson信息的33个bit位还原,将0x0002000101d00000向右移17个bit位

最后输出的结果为LGPerson

文章分类
iOS
文章标签