第四节—类的本质

本文为L_Ares个人写作,包括图片皆为个人亲自操作,以任何形式转载请表明原文出处。

第一节中,在alloc的源码分析中,我们已经知道,alloc申请了内存空间,并且利用isa关联了objcls,而init只是提供了一个工厂模式,方便我们来重写或者自定义类,那么对象的alloc研究完了,就看看这个对象归属的类是怎么回事。

从本节开始,由alloc初始化出来的对象的探索,进步到类的探索。

OC对象的本质是什么?

OC对象的本质是结构体。

这个结论怎么得来的?

那就需要用到Clang来帮助我们验证,OC对象的本质到底是不是结构体。

一、关于Clang

什么是Clang?

Clang是由苹果基于LLVM编写的,用于C/C++/OC的轻量级编译器。

什么是LLVM?

LLVM是构架编译器(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。

为什么要用到Clang

因为Clang可以看到底层编译,例如我们平时用到的main.m经过clang编译后会变成main.cpp

通过Clang的编译,我们可以看到OC底层的结构。

二、OC对象的本质

我们在objc的部分开放源码可编译文件的main.m中创建一个继承于NSObjectJDPerson这个类。强调是在main.m文件中,不要去文件夹里面直接创建出来JDPerson.h.m,因为我们只是编译main.m,没必要自己建个类,不然还要再编译这个类,作为探索,有点麻烦。

  1. 随意给这个类添加一个属性,我添加了myHobby属性。
@interface JDPerson : NSObject

@property (nonatomic, copy) NSString *myHobby;

@end

@implementation JDPerson

@end
  1. 然后我们用终端Terminal进入你的可编译的objc-781的源码文件夹下。一直cd命令进入文件夹,直到你的main.m的文件夹下,然后执行一下命令,将main.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 

  1. 打开编译好的main.cpp文件,commond + F找你刚才自己创建的类名或属性名。
图片.png
图片.png

这里可以看到,JDMan的确是结构体,而且拥有的第一个属性是NSObject结构体,这其实就是isa只不过是一种伪继承,而这个NSObject_IMPLJDMan_IMPL里面,也就导致了JDMan_IMPL拥有着NSObject_IMPL的所有成员变量。

那这个Class又是个什么东西?怎么就是isa的类了?

上一节探索alloc的时候,是不是有一个callAlloc,如下:

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif

    // No shortcuts available.
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

我们在这里找到了fastpathclsisa没有自定义allocWithZone的时候,我们才开始了alloc申请内存,绑定isaobj的关系的,这里的ISA()就是isa指针的初始化。我们进去再看一下。

inline Class 
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的内存。这里在返回的时候,明显的做了强转,转成了Class类。为什么非要把它转成Class类呢?其实只是个备注,就是让大家知道,我isa是跟类有关系的指针,具体是什么,下一节会说明。

isa原来又是什么类呢?

直接commond点击isa,发现:

 struct objc_object {
private:
    isa_t isa;
}

就是objc_object私有变量,是一个isa_t,继续跟进isa_t:

#include "isa.h"

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一样遵循着类的本质——结构体。

到这里,我们可以确定,OC的类的本质就是结构体,而继承与NSObject的类所拥有的isa指针,也继承于NSObjectisa指针。

到这里本节的探索重点已经结束了。

下面的内容是顺带着来的,因为刚好利用Clang编译好了main.cpp文件,那么就多看两行。

附加

// @implementation JDPerson


static NSString * _I_JDPerson_myHobby(JDPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JDPerson$_myHobby)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_JDPerson_setMyHobby_(JDPerson * self, SEL _cmd, NSString *myHobby) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct JDPerson, _myHobby), (id)myHobby, 0, 1); }
// @end

这是紧跟着刚才的JDPerson_IMPL的内容,仔细一看,原来一个是属性中的get,一个是set,配上成员变量__myHobby,一个@property出来了。。。

那就看一下。

这里说一下,看到getset方法的参数了吗?突然多出来了两个参数,一个是JDPerson* self,还有一个SEL _cmd。这两个参数是编译的时候,自动加进来的,一个指定了当前方法的对象,一个指定了当前方法实现的名字。那个self也是为什么我们在setget方法里面可以使用self来调用属性或者函数的原因。

get没什么看的,直接就return了,但是那个set里面扩展了一个函数objc_setProperty

我们去objc781里面看一下objc_setProperty这是什么。

图片.png

不要管.h头文件,直奔.mm的实现。发现一个reallySetProperty,点进去看一下。

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        //设置isa的指向
        object_setClass(self, newValue);
        return;
    }

    //旧的value
    id oldValue;
    
    //插槽,自己的的位置前面还要有isa,就把isa的位置留出来
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        //传进来的新值进行retain+1
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        //老的值拿插槽里面的值
        oldValue = *slot;
        //新的值放入插槽
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    //老的值release-1
    objc_release(oldValue);
}

自己加了两句注释,不知道对不对,以我的理解来的。

那么从这里可以看出来:

  1. objc_setProperty主要是一个接口,主要处理了两个拷贝参数的真假,利用这个来把上层的set和下层的set的处理分开,处理set逻辑的主要函数是reallySetProperty
  2. 为什么要分开上下层的set?因为上层的set方法很多,直接使用底层的reallySetProperty会出现很多的临时变量,这就会导致你想找到一个方法sel的时候变得复杂。

因为(2)中的因素,苹果采用了适配器设计模式,把底层的接口适配成客户端需要的接口。

这样做的好处就是你上面随便的改,最后我用这个适配的接口把需要的东西传给底层的reallySetProperty,具体是谁的,通过SEL_cmd的区分。最后由reallySetProperty来处理。

上层处理你要处理的,随意的变,但是不影响我底层的处理逻辑,我底层的处理逻辑也不影响你上层的处理逻辑。相当于给老板找了个秘书。

放个图:

图片.png

你可能感兴趣的:(第四节—类的本质)