初探OC对象原理(三)

a5ccf31d7b7cb5ffc693af363e1aad41.jpeg

前言:

这是探究OC对象原理的第三章,也是按照对象的 的底层实现原理顺序来进行的。今天我们探究下对象的本质以及一些拓展内容。在这之前我先介绍一下clangxcrun,因为本文需要用到。请看下图(借鉴前人的总结)

clang.png

简单了解了clang后我们今天需要几个clangxcrun的命令,帮助我们把常用的.m文件转换成c++文件,也就是.cpp文件。

1,clang -rewrite-objc main.m -o main.cpp 把⽬标⽂件编译成c++⽂件

在用这条命令的时候可能会报错:UIKit找不到的问题。这个时候我们可以用以下命令来解决:

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

\color{#FFCB30}{ps:以上命令的环境部分(如:13.0.0、iPhoneSimulator、iPhoneSimulator13.0)需要留意根据自己的实际情况来进行修改。}

2,xcode安装的时候顺带安装了xcrun命令,xcrun命令在clang的基础上进⾏了⼀些封装,要更好⽤⼀些

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` (⼿机)

利用以上命令可以将我们OC的.m文件转成c++的.cpp文件,我们借助.cpp文件 来查看我们生成的对象在c++层面是以怎样的形式存在的。

初步探究

我们先创建一个项目在main.m文件里创建一个类ZYPerson,并且创建一个属性 zyName。如图:

#import 
#import 

@interface ZYPerson : NSObject
@property (nonatomic, copy) NSString *zyName;
@end

@implementation ZYPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert  code here...
        NSLog(@"Hello, World!");
    }
    return 0;
}

接下来我们执行上面的clang命令 将main.m文件转成main.cpp的文件并且双击打开。利用 command+F搜索ZYPerson

#ifndef _REWRITER_typedef_ZYPerson
#define _REWRITER_typedef_ZYPerson
typedef struct objc_object ZYPerson;
typedef struct {} _objc_exc_ZYPerson;
#endif

extern "C" unsigned long OBJC_IVAR_$_ZYPerson$_zyName;
struct ZYPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_zyName;
};

发现: ZYPerson 是以 struct 存在的

回到上面我们的ZYPerson_IMPL代码块,我们看到结构体内struct NSObject_IMPL NSObject_IVARS这句,我们根据这句代码全局搜索下NSObject_IMPL

struct NSObject_IMPL {
    Class isa;
};

发现:NSObject_IVARS其实是成员变量isa.

同时我们在ZYPerson_IMPL结构体中看到我们的属性zyName。我们不妨探究下属性在底层的setter和getter是如何实现的。在ZYPerson_IMPL结构体的正下方紧接着我们看到以下两句代码:

// @property (nonatomic, copy) NSString *zyName;
/* @end */
// @implementation ZYPerson

static NSString * _I_ZYPerson_zyName(ZYPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_ZYPerson$_zyName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_ZYPerson_setZyName_(ZYPerson * self, SEL _cmd, NSString *zyName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct ZYPerson, _zyName), (id)zyName, 0, 1); }
// @end

我们分析代码发现第一句代码return了一个值,第二句代码却是objc_setProperty设置了一个属性。并且两者都携带了我们经常提到的隐藏参数ZYPerson * selfSEL _cm,再次根据第一行代码返回值的形式,很明显是在拼接地址的方法。这很符合我们iOS在内存中查找值的规律。我们基本可以确定第一句代码就是我们属性zyNamegetter方法,相对的第二句则是它的setter方法。同时我们也明白了为什么我们可以在每个方法里都能调用到self的原因,因为每个方法属性都携带了selfSEL _cm这两个隐藏参数。

回到最初的代码我们还可以看到这样一句代码:typedef struct objc_object ZYPerson,这个objc_object让我很好奇,我们接着查看下它是什么东西,仍然command+F搜索

typedef struct objc_class *Class;

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

typedef struct objc_object *id;

发现:objc_object 是以 struct 存在的,而且我们的ZYPerson是继承的NSObject但是在底层其实是objc_object

在这段代码中,我们看到了熟悉的Classid。并且我们看到他们俩的声明跟ZYPerson的声明有点不一样他们都是以指针形式存在的,看对比:

发现:typedef struct objc_object ZYPerson;
typedef struct objc_class *Class;
typedef struct objc_object *id;
这也是为什么我们在使用id 声明对象的时候为什么不需要*的原因。因为他自己本身就是一个objc_object *结构体指针,他可以定义任何类型的变量。同时我们常用的Class他的类型也是objc_class *结构体指针。

结构体、位域、联合体

接下来我们插入一点预备知识,就是结构体、位域、联合体,分别来看看他们的区别。

实例一

struct ZYComputer1 {
    BOOL iMac; // 0 1
    BOOL macBookPro;
    BOOL airBook;
    BOOL iPad;
};
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        struct ZYComputer1   computer1;
        NSLog(@"ZYComputer1's size : %ld",sizeof(computer1));
        
    }
    return 0;
}

查看NSLog打印出来的结果:

2021-06-16 17:30:17.916212+0800 ZYProjectTree1[59630:1944971] ZYComputer1's size : 4

可以看到ZYComputer1的大小为 4 字节

示例二

struct ZYComputer2 {
    BOOL iMac: 1;
    BOOL macBookPro : 1;
    BOOL airBook : 1;
    BOOL ipad: 1;
};
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        struct ZYComputer2   computer2;
        NSLog(@"ZYComputer2's size : %ld",sizeof(computer2));
        
    }
    return 0;
}

查看NSLog打印出来的结果:

021-06-16 17:35:49.180143+0800 ZYProjectTree1[59712:1948611] ZYComputer2's size : 1

可以看到ZYComputer1的大小为 1 字节,这就是位域互斥

示例三

struct ZYPerson1 {
    double      height;
    char        *name;
    long        age;
};
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        struct ZYPerson1   person1;
        person1.height = 180.0;
        person1.name = "Wayne";
        person1.age = 20;
        
        NSLog(@"person1's height : %f",person1.height);
        NSLog(@"person1's name : %s",person1.name);
        NSLog(@"person1's age : %ld",person1.age);

    }
    return 0;
}

查看NSLog打印出来的结果:

2021-06-16 17:49:01.845235+0800 ZYProjectTree1[60074:1959304] person1's height : 180.000000
2021-06-16 17:49:01.845684+0800 ZYProjectTree1[60074:1959304] person1's name : Wayne
2021-06-16 17:49:01.845719+0800 ZYProjectTree1[60074:1959304] person1's age : 20

可以看到ZYPerson1的所有属性都已经被赋值成功了。所以结构体是共存的。

示例四

union ZYPerson2 {
    char        *name;
    double      height;
    long        age;
};
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        union ZYPerson2   person2;
        person2.name = "Wayne001";
        person2.height = 190.0;
        person2.age = 18;

        NSLog(@"person2's name : %s",person2.name);
        NSLog(@"person2's height : %f",person2.height);
        NSLog(@"person2's age : %ld",person2.age);
    }
    return 0;
}

直接查看NSLog打印出来的结果你会发现会崩溃(具体原因至今没找到),所以我们用断点在lldb下打印看看 (每一次p person2打印对应每一个断点)

(lldb) p person2
(ZYPerson2) $0 = (name = 0x0000000000000000, height = 0, age = 0)
(lldb) p person2
(ZYPerson2) $1 = (name = "Wayne001", height = 2.1220038126150982E-314, age = 4294983532)
(lldb) p person2
(ZYPerson2) $2 = (name = "", height = 190, age = 4640889047261118464)
(lldb) p person2
(ZYPerson2) $3 = (name = "", height = 8.8931816251424378E-323, age = 18)
(lldb) 

如图:

image.png

可以看到ZYPerson2 在几次赋值的过程中始终只保存了一个最后赋予的值。并不像实例三一样每一个属性都被赋值。这就是联合体 union结构体 struct直接的区别。联合体也是互斥的。

总结:

1,结构体 struct的大小是根据内部成员变量的类型和个数来决定的。可以根据实例一证明。
2,对结构体 struct内的成员变量指定位域(如示例二)可以优化结构体 struct的大小。但是最终的大小还是会以字节倍数来计算,如示例二中指定位域1 位,四个成员变量加起来应该是4 位也就是0.5字节,但是最终会分配1字节。因为内存中不存在0.5字节的单位。
3,结构体 struct中的成员变量是共存的,存储的值互不影响,可以通过示例三来证明,每个成员变量都成功赋值。优点是全⾯;缺点是struct内存空间的分配是粗放的,不管⽤不⽤,全分配。
4,联合体 union中的成员变量是互斥的,当某一个成员变量被赋值其他成员变量的地址将不被使用,此时其他成员变量存储的将是脏数据。联合体(union)的缺点就是不够包容;优点是内存使⽤更为精细灵活,也节省了内存空间

initIsa 探索

首先上一段代码 (ps:来源于苹果开源的objc底层源码):

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)
{
    /***删除了前段部分代码,删除的代码是前两章探索的alloc、内存分配的部分***/
   //以下判断就是我们要探索的 isa 部分 也就是 对象alloc 以及内存分配后 的步骤

    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);
    }
   /***删除了后段部分代码,删除的代码是关于return的内容***/
}

这段代码我们很熟悉,因为在前两章我们经常要跟踪alloc代码到这个方法里。这个方法就包含了对象初始化的一些过程,从alloc内存分配再到现在我们探索的 initIsaclass的绑定。前两步我们已经在前两章探索过了,这里紧接着第二章探索initIsa
我们根据这个if 判断首先看到initInstanceIsa这个方法。我们跟踪进去:

inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

可以看到这方法 进去后 最终还是调用了initIsa方法。所以我们着重探索initIsa,同样利用command+F可以跟踪到以下方法:

inline void 
objc_object::initIsa(Class cls)
{
    initIsa(cls, false, false);
}

NONPOINTER_ISA

继续上面的代码跟踪:

inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ 
    ASSERT(!isTaggedPointer()); 
    
    isa_t newisa(0);

    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;
        // 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_t newisa(0)这个方法,发现是个isa的初始化方法,我们看看这个isa_t到底是个什么东西,跟踪进去:

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

发现这是个联合体(union)。我们看到这个联合体里有构造方法isa_t() { }、有uintptr_t bits(ps:这个不是位域里的bits么)、还有我们的对象cls,而且我们发现还有一个ISA_BITFIELD的结构体成员变量。我们看看ISA_BITFIELD是什么

# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
#   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#     define ISA_MASK        0x007ffffffffffff8ULL
#     define ISA_MAGIC_MASK  0x0000000000000001ULL
#     define ISA_MAGIC_VALUE 0x0000000000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 0
#     define ISA_BITFIELD                                                      \
        uintptr_t nonpointer        : 1;                                       \
        uintptr_t has_assoc         : 1;                                       \
        uintptr_t weakly_referenced : 1;                                       \
        uintptr_t shiftcls_and_sig  : 52;                                      \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 8
#     define RC_ONE   (1ULL<<56)
#     define RC_HALF  (1ULL<<7)
#   else
#     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;                                       \
        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)
#   endif
# elif __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;                                         \
      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)

# else
#   error unknown architecture for packed isa
# endif

// SUPPORT_PACKED_ISA
#endif

在这里我们看到分别是 arm64x86_64 两个框架的定义,我们发现这个ISA_BITFIELD原来是位域。而且我们发现ISA_BITFIELD存的内容有nonpointer关联对象(has_assoc)析构函数(has_cxx_dtor)类的指针地址(shiftcls)(magic)弱引用(weakly_referenced)是否使用(unused)散列表(has_sidetable_rc)引用计数(extra_rc)等。这就证明isa_t并不是简单地isa指针和类的信息,而是存了很多其他东西,这就所谓的NONPOINTER_ISA

下面针对这个位域我们看看 arm64架构下的图解:

image.png

0号标志位——nonpointer表示是否对 isa指针开启指针优化
0:纯isa指针,1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等

1号标志位——has_assoc关联对象标志位,0没有,1存在

2号标志位——has_cxx_dtor该对象是否有 C++ 或者Objc 的析构器,如果有析构函数,则需要做析构,如果没有,则可以更快的释放对象

3-35号标志位——shiftcls: 存储类指针的值。开启指针优化的情况下,在 arm64架构中有 33 位⽤来存储类指针。

36-41号标志位——magic⽤于调试器判断当前对象是真的对象还是没有初始化的空间。

42号标志位——weakly_referenced志对象是否被指向或者曾经指向⼀个 ARC 的弱变量,没有弱引⽤的对象可以更快释放。

43号标志位——`unused:标志对象是否正在使用

44号标志位——has_sidetable_rc当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位

45-64号标志位——extra_rc当表示该对象的引⽤计数值,实际上是引⽤计数值减 1。
例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,
则需要使⽤到下⾯的 has_sidetable_rc。

isa关联对象

上面我们花了很长的时间去跟踪了isa_t一直到位域的分析。现在我们回到代码去看看
我们创建一个ZYPerson类,在main文件里导入头文件,代码如下:

#import 
#import 
#import "ZYPerson.h"

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

        ZYPerson *p = [ZYPerson alloc];
        NSLog(@"%@",p);
    }
    return 0;
}

我们在NSLog(@"%@",p);这句代码上打断点 利用lldbp一下 看看这个对象的地址 以及 类的地址:

image.png

我们看到这里的对象p 和 类ZYPerson 对应的指针地址 0x011d8001000082750x00000001000082700x 后的最高位都是0 这就证明这两个64位地址的内存根本没有用完。我们下面来看看对象和类到底是怎么关联的。我们回到objc源码 方法

inline void 
objc_object::initIsa(Class cls, bool nonpointer,UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor){ }

在这个方法里我们 接着从 isa_t newisa(0)这句代码往下走,可以看到一个 if else 的判断,如果!nonpointer 就直接newisa.setClass(cls, this); 设置了cls。如果不是则走else对上面我们分析的位域进行一些列的赋值 最后也对cls 进行了set 方法。我们跟踪下newisa.setClass(cls, this);

inline void
isa_t::setClass(Class newCls, UNUSED_WITHOUT_PTRAUTH objc_object *obj)
{
    // Match the conditional in isa.h.
#if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#   if ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_NONE
    // No signing, just use the raw pointer.
    uintptr_t signedCls = (uintptr_t)newCls;

#   elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ONLY_SWIFT
    // We're only signing Swift classes. Non-Swift classes just use
    // the raw pointer
    uintptr_t signedCls = (uintptr_t)newCls;
    if (newCls->isSwiftStable())
        signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));

#   elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ALL
    // We're signing everything
    uintptr_t signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));

#   else
#       error Unknown isa signing mode.
#   endif

    shiftcls_and_sig = signedCls >> 3;

#elif SUPPORT_INDEXED_ISA
    // Indexed isa only uses this method to set a raw pointer class.
    // Setting an indexed class is handled separately.
    cls = newCls;

#else // Nonpointer isa, no ptrauth
    shiftcls = (uintptr_t)newCls >> 3;
#endif
}

我们可以看到这方法里先获取到 newClass

uintptr_t signedCls = (uintptr_t)newCls;

然后获取到位域结构:

 uintptr_t signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));

然后进行一系列的位移

shiftcls_and_sig = signedCls >> 3;
shiftcls = (uintptr_t)newCls >> 3;

到这里我们不禁有些疑惑为什么要这样位移来处理呢?这里就涉及到另一个我们需要留意的点那就是 ISA_MASK

ISA_MASK (掩码)

在我们上面探究的位域部分,我们可以留意到以下代码

# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL

这个就是ISA掩码,他的作用你可以理解为对ISA的一个防护伪装,之前在对象和类绑定的 过程中利用这个ISA_MASK掩码进行了伪装(因为ISA不是单纯的nopointer里面还包含了很多上面我们分析的位域内容等),所以如果我们想要看到ISA对应类的地址就需要经过反掩码的操作来获取。

我们用代码来演示一下,我们知道对象pisa地址0x011d800100008275,也打印出来了其对应的类的地址0x0000000100008270。我们用掩码来验证一下:(也就是让对象的isa地址掩码ISA_MASK 进行 与(&) 操作)

image.png

其实我们想要通过对象得到类的信息,还可以通过ISA位运算直接算出来。下面我们来看看ISA位运算

ISA位运算

我们在之前分析isa_t的时候分析到了他包含的位域部分。在位域里有一个关于类的shiftcls ,上面我们分析的是arm64架构的,但是我们现在电脑跑的代码是在模拟器上所以我们看看shiftclsx86架构上它所占的位是 44位。所以我们下面计算的时候用 44位来计算:

image.png

图解:

ISA位运算图解.jpg

至此,本章内容就算是告一段落了,其中关于对象属性 的settergetter 方法的更详细解析,后面再补充。

遇事不决,可问春风。站在巨人的肩膀上学习,如有疏忽或者错误的地方还请多多指教。谢谢!

你可能感兴趣的:(初探OC对象原理(三))