OC对象原理探索(下)-对象的本质

2281973-fe6739676cb15c98.jpg

本文主要讨论两个方面 意识OC对象的本质 ,二是isa的结构及与类的关联

对象的本质

结构体,位域,联合体

首先我们来了解下 结构体,位域,联合体概念

    struct LMDirection{
        BOOL up;
        BOOL down;
        BOOL left;
        BOOL right;
    };
    struct LMDirection lmDirection;
    NSLog(@"LMDirection--------%lu",sizeof(lmDirection));
    //LMDirection--------4
    struct LMDirection1{
        BOOL up :1;
        BOOL down :1;
        BOOL left :1;
        BOOL right :1;
    };
    struct LMDirection1 lmDirection1;
    NSLog(@"LMDirection1--------%lu",sizeof(lmDirection1));
    //LMDirection1--------1
    union LMdirection2{
        BOOL up;
        BOOL down;
        BOOL left;
        BOOL right;
    };
    union LMdirection2 lmDirection2;
    NSLog(@"LMDirection2--------%lu",sizeof(lmDirection2));
    //LMDirection2--------1
  • lmDirection 是一个简单的结构体对象,占4个字节空间
  • lmDirection1是一个使用位域的结构体对象,占用1个字节空间
  • lmDirection2是一个联合体对象,占用1个字节空间
    下面我们通过对结构体和联合体成员变量赋值,看下,赋值过程中,成员变量的变化
    struct LMPerson{
        NSString *name;
        NSInteger age;
        CGFloat height;
    };
    struct LMPerson person;
    person.name = @"MC";
    person.age = 18;
    person.height = 190.0;
    
    union LMUnionPerson{
        char *name;
        int age;
        float height;
    };
    union LMUnionPerson person1;
    person1.name = "lilei";
    person1.age = 16;
    person1.height = 180.0;

我们从创建person对象开始打断点,一步一个输出,查看成员变量变化:

结构体和联合体成员变量变化

结论

  1. 结构体占用空间大小为其内部变量占用大小的总和
  2. lmDirection1 中每个变量占1位,总共占用4位一个字节
  3. 联合体占用空间大小为其内部成员变量占用空间最大的那个大小
  4. 结构体内成员变量的存储是互不影响的,联合体内是互斥的

Clang

我们知道Objective-c语言底层的实现是C语言和C++,因此我们可以通过clang轻量级编译器的代码还原,还原OC代码的底层实现,将.m文件编译成.cpp文件。

  • Clang是一个C++编写、基于LLVM、发布于LLVM BSD许可证下的C/C++/Objective-C/Objective-C++编译器
  • Clang主要功能是把.m文件编译为C++.mm文件等,便于我们探究底层代码逻辑
    Clang 终端编译命令
clang -rewrite-objc main.m -o main.cpp 
//UIKit报错 ---main.m:8:9: fatal error: 'UIKit/UIKit.h' file not found
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
// xcrun命令基于clang基础上进行了封装更好用
//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 

类的本质

#import 
#import "AppDelegate.h"
#import 

@interface LMPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
@implementation LMPerson


@end

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        LMPerson *person = [LMPerson alloc];
        person.name = @"MC";
        person.age = 18;
        
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

首先将main.m转化为main.cpp,打开main.cpp分析源码.
全局搜索LMPerson,找到如下代码:

typedef struct objc_object LMPerson;
typedef struct {} _objc_exc_LMPerson;
#endif

extern "C" unsigned long OBJC_IVAR_$_LMPerson$_name;
extern "C" unsigned long OBJC_IVAR_$_LMPerson$_age;
struct LMPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_name;
    NSInteger _age;
};

这里我们可以看到 LMPerson 类本质是一个objc_object 结构体

LMPerson_IMPL中包含一个结构体成员NSObject_IVARS,它是NSObject_IMPL类型,查看NSObject_IMPL的代码以下:

struct NSObject_IMPL {
    Class isa;
};

OC中的对象其实就是经过结构体来实现的。在NSObject_IMPL包含了一个Class类型的成员isa,这也说明上面代码中的NSObject_IVARS就是isa

我们接着找一下objc_object的具体实现,全局搜索objc_object {:

typedef struct objc_class *Class;


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


typedef struct objc_object *id;



typedef struct objc_selector *SEL;

我们发现 objc_object是一个结构体,内部有一个Class类型的isa,无意中我们又发现,Class 是一个objc_class的结构体指针 ,id是一个objc_object的结构体指针 ,SEL是一个objc_selector的结构体指针

Set&&Get

static NSString * _I_LMPerson_name(LMPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LMPerson$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_LMPerson_setName_(LMPerson * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LMPerson, _name), (id)name, 0, 1); }

static NSInteger _I_LMPerson_age(LMPerson * self, SEL _cmd) { return (*(NSInteger *)((char *)self + OBJC_IVAR_$_LMPerson$_age)); }
static void _I_LMPerson_setAge_(LMPerson * self, SEL _cmd, NSInteger age) { (*(NSInteger *)((char *)self + OBJC_IVAR_$_LMPerson$_age)) = age; }

这里是属性的setget方法,我们注意到LMPerson * self, SEL _cmd是隐藏参数
get方法是通过内存平移的方式获取对应的属性值
set方法也是通过内存平移的方式对属性进行赋值

这里我们注意到name属性赋值的时候有一个objc_setProperty,什么时候会有这个方法呢?我们来做个实验:
main.m修改为如下代码

@interface LMPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (atomic, copy) NSString *name1;
@property (nonatomic, strong) NSString *name2;
@property (atomic, strong) NSString *name3;
@property (nonatomic, assign) NSInteger age;
@property (atomic, assign) NSInteger age1;
@end
@implementation LMPerson


@end

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        LMPerson *person = [LMPerson alloc];
        person.name = @"MC";
        person.name1 = @"1";
        person.name2 = @"2";
        person.name3 = @"3";
        person.age = 18;
        person.age1 = 16;
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

然后通过Clang转化为main.cpp文件,打开main.cpp找到setget方法代码:

get&&set

结论: objc_setPropertycopy修饰有关系,与atomic,nonatomic,assign,strong无关
objc4源码查找objc_setProperty分析后续补

isa 结构 及与类的关联

我们上面查看main.cpp文件发现isaClass类型的,但是我们在objc4源码中查看objc_object结构发现:

struct objc_object {
private:
    isa_t isa;
public:
          ...

这是怎么回事呢? 下面一起来探究下
我们在探究alloc流程的时候OC对象原理探索(上)-alloc流程,最后一步_class_createInstanceFromZoneobj->initInstanceIsa(cls, hasCxxDtor)创建isa关联类,下面我们进入源码看下具体实现

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初始化

    if (!nonpointer) {
        newisa.setClass(cls, this); //如果纯指针 直接赋值
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());


        //不同平台执行对应的流程
#if SUPPORT_INDEXED_ISA
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE; //macOS 0x000001a000000001ULL
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
#   if ISA_HAS_CXX_DTOR_BIT
        newisa.has_cxx_dtor = hasCxxDtor;
#   endif
        newisa.setClass(cls, this);
#endif
        newisa.extra_rc = 1;
    }

    // This write must be performed in a single store in some cases
    // (for example when realizing a class because other threads
    // may simultaneously try to use the class).
    // fixme use atomics here to guarantee single-store and to
    // guarantee memory order w.r.t. the class index table
    // ...but not too atomic because we don't want to hurt instantiation
    isa = newisa;
}

查看源码发现,isa定义的是一个isa_t类型的对象,我们来看下isa_t的数据结构

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

分析源码:isa_t 是一个联合体, 有一个Class类型的cls变量,有一个uintptr_t类型的bits,如果定义了ISA_BITFIELD,还包含一个结构体,结构体内部是ISA_BITFIELD;
这些变量是互斥的,一个被赋值以后,其他的变量值为空或被覆盖
我们来看下ISA_BITFIELD具体是什么

ISA_BITFIELD

在macOS中的图示:
ISA_BITFIELD.png

各个变量的含义

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

那么为什么初始化initIsa的时候isa的类型是isa_t,而在NSObject定义中isa的类型是Class? 来看下objc_object中的源码函数ISA():

objc_object::ISA() 
{
    ASSERT(!isTaggedPointer()); 
#if SUPPORT_INDEXED_ISA
    if (isa.nonpointer) {
        uintptr_t slot = isa.indexcls;
        return classForIndex((unsigned)slot);
    }
    return (Class)isa.bits;
#else
    return (Class)(isa.bits & ISA_MASK);
#endif
}

我们发现在得到isa的时候做了一个类型强转,这是因为isa反应的都是类的信息,方便开发者的理解

isa与类的关联

clsisa 关联原理就是isa指针中的shiftcls位域中存储了类信息,下面我们通过几种方式来验证一下

  1. 通过initIsa方法中的newisa.shiftcls = (uintptr_t)cls >> 3
    打开可编译的源码objc4-818.2,创建一个LGPerson类,在main.m里创建类对象
LGPerson *p = [LGPerson alloc] ;

并打上断点
执行进入源码objc_object::initIsa,跳过纯指针的赋值后,在newisa.bits = ISA_INDEX_MAGIC_VALUE;前打印newisa

(lldb) p newisa
(isa_t) $4 = {
  bits = 0
  cls = nil
   = {
    nonpointer = 0
    has_assoc = 0
    has_cxx_dtor = 0
    shiftcls = 0
    magic = 0
    weakly_referenced = 0
    unused = 0
    has_sidetable_rc = 0
    extra_rc = 0
  }
}

newisa.bits赋值之后打印

(lldb) p newisa
(isa_t) $5 = {
  bits = 8303511812964353
  cls = 0x001d800000000001
   = {
    nonpointer = 1
    has_assoc = 0
    has_cxx_dtor = 0
    shiftcls = 0
    magic = 59
    weakly_referenced = 0
    unused = 0
    has_sidetable_rc = 0
    extra_rc = 0
  }
}

newisa.bits

bits赋值.png

其中magic是59是由于将isa指针地址转换为二进制,从47(因为前面有4个位域,共占用47位,地址是从0开始)位开始读取6位,再转换为十进制
magic.png

继续往下执行,走到newisa.setClass(cls, this);,进入isa_t::setClass方法:
我们看下关键代码:

shiftcls = (uintptr_t)newCls >> 3;

shiftcls

继续执行走到initIsaisa = newisa
我们输出一下这个newisa

Xnip2021-06-28_14-20-10.png

bits赋值结果的对比,bits的位域中有两处变化

  • cls 由默认值,变成了LGPerson,将isacls关联
  • shiftcls0变成了536875124
  1. 通过isa & MASK
    我们在alloc之后打上断点,在lldb里进行调试
LGPerson *p = [LGPerson alloc];

arm64中,ISA_MASK 宏定义的值为0x0000000ffffffff8ULL

x86_64中,ISA_MASK 宏定义的值为0x00007ffffffffff8ULL

(lldb) x/4gx p
0x10153a300: 0x011d8001000083a1 0x0000000000000000
0x10153a310: 0x0000000000000000 0x0000000000000000
(lldb) po 0x011d8001000083a1 & 0x00007ffffffffff8ULL
LGPerson

我们看到isa的地址&ISA_MASK 最后得到的就是类LGPerson

  1. 通过isa位运算验证
    类的信息存储在isashiftcls中,在macOS环境下,shiftcls44位,前面有3位,后面有17
isa图示.png
Xnip2021-07-06_10-56-56.png
  • isa初始地址0x011d8001000083a1
  • 右移3位得到十进制地址--10045138768236660--转16进制为--0x0023b00020001074
  • 左移20位得到十进制地址--562954370023424--转16进制为--0x0002000107400000
  • 右移17位得到十进制地址--4295000992--转16进制为0x00000001000083a0
  • 输出po--$3得到LGPerson
  • 输出 LGPerson.class的地址 0x00000001000083a0$3吻合,得到验证.

你可能感兴趣的:(OC对象原理探索(下)-对象的本质)