美文网首页
第四节—类的本质

第四节—类的本质

作者: L_Ares | 来源:发表于2020-10-20 01:01 被阅读0次

    本文为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

    相关文章

      网友评论

          本文标题:第四节—类的本质

          本文链接:https://www.haomeiwen.com/subject/rwglmktx.html