iOS底层探究——–类的原理分析(上)

前面从对象的本质中 对象的本质,就分析了isa指针的底层是什么,以及它的位运算。那么接下来,我们再去探寻下,isa中还需要我们注意和了解的地方。

1. 从ISA分析到元类

1.1.类的ISA指针指向一个未知的东西

老规矩,带入到代码里面去操作。创建一个TestPerson类,并在main.m文件里面进行初始化:
FFE2CB4A-598D-49EC-896F-38438EC38B51.png

通过断点调试,查看地址。通过p/x p 获取TestPerson的指针地址,再通过x/4gx 拿到当前对象的isa指针地址。拿到这个isa指针地址后,再&isa指针的掩码,就能获取对应类的信息(思路:根据objc源码得到:return (Class)(isa.bits & ISA_MASK);)。

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);
//这里与ISA_MASK这个掩码是将isa指针shiftcls的前面3位以及后面的位置0,这样得到的就是isa指针的类信息,这样就将isa指针和类关联起来了。
#endif
}
复制代码

掩码:(ULL是无符号长整型unlonglong标志)
F5158ECA-1C50-41C2-A742-C673597BEE45.png

下面根据刚刚说的步骤,通过lldb调试:
55C06ADA-3345-4ABA-85A0-1D13C593394D.png

通过这么一通操作,得到的是TestPerson类的地址,总感觉过程太平淡,得搞点事,激动的心,颤抖的手,喊起响亮的口号:搞事、搞事、搞事。。。?

那么如果直接用x/4gx 查看最后得到TestPerson地址(0x0000000100008260),是不是也能得到相应的内存结构,如果能拿到这个内存结构,再执行一次上面所进行的&操作,会是怎么样的一个结果了?
9D49093C-072C-4A93-B59C-47BFA98CDD07.png
对比两次&操作的结果:第一次:0x0000000100008260,第二次:0x0000000100008238,这两者是不一样的,但是所指向的类,都是TestPerson

那么我们是不是可以有这么一个猜想:当前的这个类,就和对象一样,可以无限开辟,也就是说,在内存中不止有一个类。

有了这么一个猜想之后,就进行验证下,创建了void verTestPersonNum(void)方法,通过打印结果:
5A767B16-0816-44D0-8D26-938439A56E5D.png
通过打印结果,发现,所得到的TestPerson类的指针地址都是0x100008260,和第一次&操作得到的指针地址0x0000000100008260是一样的。

到这里,就能得出一个结果:第二次&操作得出的指针地址:0x0000000100008238不是类。那么,它是什么了?是不是有可能是苹果系统所给出的一个新的结构?

因为苹果系统的设计是如下图所示的路线,那么接下来该是什么,就需要进一步探究了:
未命名文件-3.png

1.2.通过MachOView分析是元类

接下来的分析,就要用到MachOView这个工具了
AF72CD7F-2A9C-4729-9C6F-C02E4E69C7BA.png

把工程中的machO文件,拖入这个工具里面:
3739D1D0-BF40-4527-9D52-D5B18235AA91.png

通过这个工具,就能分析machO的符号表了,我们在symbol TabelSymbols里面,搜索class,就能找到TestPerson类的对应信息(如下图),在TestPerson类前面,还有一个metaClass的类。
B9F06CEF-412B-48FB-B4BB-6C38BF26FF79.png

我们只是创建TestPerson类,但是在编译的可执行文件里面,还多了一个metaclass,也就是元类这个是系统或者编译器自己创建的
未命名文件.png

2.ISA的走位图和类的继承链

2.1.根元类的探究

经过分析,对象的isa指针指向类,类的指针指向元类,那么,会不会有元类的isa指针指向其他的类了?
继续分析,通过lldb调试,寻找答案:
BD78D446-04C8-4751-8D39-F5BC737CAE49.png

那么,isa指针的走位图,应该是:
未命名文件-2.png

既然得到了根元类是NSObject,那么,是不是可以直接用NSObject类去找他的根元类了?
FA3F0FB0-3530-449D-96C0-7A7568BC947A.png
根据lldb调试,发现,根类NSObjectisa指针直接指向的是根元类的isa指针。
未命名文件.png

2.2.ISA指针的走位图

先通过代码,来获取类、元类、根元类、根根元类的地址,以TestPerson做个对比,来分析他们之间的关系:

#pragma mark - NSObject 元类链
void lgTestNSObject(void){
    // NSObject实例对象
    NSObject *object1 = [NSObject alloc];
    // NSObject类
    Class class = object_getClass(object1);
    // NSObject元类
    Class metaClass = object_getClass(class);
    // NSObject根元类
    Class rootMetaClass = object_getClass(metaClass);
    // NSObject根根元类
    Class rootRootMetaClass = object_getClass(rootMetaClass);
    NSLog(@"\n%p 实例对象\n%p 类\n%p 元类\n%p 根元类\n%p 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
    
    // TestPerson元类
    Class pMetaClass = object_getClass(TestPerson.class);
    Class psuperClass = class_getSuperclass(pMetaClass);//父类
    NSLog(@"%@ - %p",psuperClass,psuperClass);
复制代码

main函数里面,调用该方法,打印的结果:
668A804D-83C6-453B-964A-856DB0A614D1.png

按照我们正常的推断流程,TestPerson通过class_getSuperclass是找到的父类NSObject的类对象,但是,打印的结果,却是元类的地址。
那么我们是不是可以得出一个结论:任何对象,他的元类的父类,就是其根元类。就如下面这张示意图:
未命名文件-2.png

就比如:子类实例对象的isa指针指向子类,子类的isa指针指向子元类,子元类的isa指针指向根元类。

2.3.类的继承链

我们的任何类,都会满足一条继承关系。那么,我们在工程里面,再创建一个TestSon类,来继承于TestPerson
6CC233F2-1F62-498E-A789-6E1CDBF5A51B.png

那么在lgTestNSObject方法里,再增加几行代码:

#pragma mark - NSObject 元类链
void lgTestNSObject(void){
    // NSObject实例对象
    NSObject *object1 = [NSObject alloc];
    // NSObject类
    Class class = object_getClass(object1);
    // NSObject元类
    Class metaClass = object_getClass(class);
    // NSObject根元类
    Class rootMetaClass = object_getClass(metaClass);
    // NSObject根根元类
    Class rootRootMetaClass = object_getClass(rootMetaClass);
    NSLog(@"\n%p 实例对象\n%p 类\n%p 元类\n%p 根元类\n%p 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
    
    // TestPerson元类
    Class pMetaClass = object_getClass(TestPerson.class);
    Class psuperClass = class_getSuperclass(pMetaClass);
    NSLog(@"%@ - %p",psuperClass,psuperClass);
    
    Class sonMetaClass = object_getClass(TestSon.class);
    Class sonSuperClass = class_getSuperclass(sonMetaClass);
    NSLog(@"%@ - %p",sonSuperClass,sonSuperClass);
    
}
复制代码

再次运行,得到打印结果:
E49F69D9-4E51-457E-A4FA-63475C8DE157.png

发现,TestSon类的父类,是TestPerson类。那么,就验证了刚刚得出的那个结论(任何对象,他的元类的父类,就是其根元类)是错误的。
我们都知道,TestSon继承于TestPerson,而TestPerson继承于NSObject。根据我们前面的打印结果,TestPerson的元类,是指向NSObject的元类的,但是TestSon的父类指向的却是TestPerson,那么意味着,元类,也有一条继承链。就是:子元类继承于父元类,父元类继承于根元类。

然而,好像一到NSObject,总会有特殊情况,那么,我们再针对NSObject做一个探究:

#pragma mark - NSObject 元类链
void lgTestNSObject(void){
    // NSObject实例对象
    NSObject *object1 = [NSObject alloc];
    // NSObject类
    Class class = object_getClass(object1);
    // NSObject元类
    Class metaClass = object_getClass(class);
    // NSObject根元类
    Class rootMetaClass = object_getClass(metaClass);
    // NSObject根根元类
    Class rootRootMetaClass = object_getClass(rootMetaClass);
    NSLog(@"\n%p 实例对象\n%p 类\n%p 元类\n%p 根元类\n%p 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
    
    // TestPerson元类
    Class pMetaClass = object_getClass(TestPerson.class);
    Class psuperClass = class_getSuperclass(pMetaClass);
    NSLog(@"%@ - %p",psuperClass,psuperClass);
    
    //TestSon
    Class sonMetaClass = object_getClass(TestSon.class);
    Class sonSuperClass = class_getSuperclass(sonMetaClass);
    NSLog(@"%@ - %p",sonSuperClass,sonSuperClass);
    
    // NSObject 根类特殊情况
    Class nsuperClass = class_getSuperclass(NSObject.class);
    NSLog(@"%@ - %p",nsuperClass,nsuperClass);
    // 根元类 -> NSObject
    Class rnsuperClass = class_getSuperclass(metaClass);
    NSLog(@"%@ - %p",rnsuperClass,rnsuperClass);
    
}
复制代码

再次运行,得到结果:
8F38FF63-177C-49C4-80C4-262F3EC9795D.png
根据打印结果,发现NSObject的父类是nil,而根元类的父类,竟然是NSObject类,那么这就回到了原点,也可以得出结论:所有的类,都是来源于NSObject。那么就得到下面这个isa指针链和superclass继承链关系图:
未命名文件-3.png

这里再附上苹果官方文档提供的图:
isa流程图.png

3.通过源码分析类的结构

刚刚探究完类的isa指针链和类的继承链,那么类里面,到底是怎样组成的了?就比如:
60F688B1-2F0A-4641-9775-3F75206FCA45.png
我们直接查看TestPerson类的内存情况,发现,是有内容存在的。就好比对象,内存里面存放的是isa指针和成员变量,那么类了?内存里面存放的什么了?

FE5B6231-DF4B-4C34-B202-F601699379FA.png

那么就需要分析类的底层原理了。通过前面章节,我们知道,类的底层就是objc_class的结构体。那么我们直接在objc的源码中(iOS底层探究之alloc)全文索引struct objc_class 来找到其声明的地方。搜索到的结果,发现有两处声明,第处声明已经明确表示,只能在objc2环境下,才能使用,但是我们目前不是在这个环境下的,所以,这个声明不用管,所以,直接看另外一个就成。
0130AD7D-B20F-4FD2-8EF2-66B47264FFEC.png

那么我们看另外一个声明:
0C1867C5-5FBF-4017-89F0-F320A5B0AEA1.png
从底层,能够看出,类的底层是objc_class,是继承于objc_object,里面也包含好些内容,比如:cachebits。。。等等,那么像这类内容到底代表着什么了?那就需要进一步的探究了。

那么可以得出类的结构图:
未命名文件.png

4.类的结构内存计算

objc源码中,创建一个TestPerson类,在里面添加一个name的成员变量。然后在main.m文件里面初始化。首先通过x/4gxTestPerson(继承于NSObject)类的内存结构情况打印出来:
A3D22557-06B9-43E7-B7DC-08928EDB340C.png
我们刚刚通过类的源码分析,那么打印出来的内存结构里面,isa指针地址在最前面,isa0x0000000100008480,推断下,0x000000010036a140就应该是Class superclass了(根据源码,类的结构体里面的内容)
1F340CE8-F46D-4FC8-BF28-0E197607D83A.png
通过lldb调试打印下,果然是父类NSObject
C01AE3AB-E38B-40A3-BD0A-D40F149E514B.png

那么接下来,0x000000010161d470就应该是cache_t cache了,这个探究先放一放,先来探究第个地址0x0001801800000003,那么第个地址,应该就是class_data_bits_t bits了。想要获取第个地址所对应的值,就要把首地址指针平移到这个位置上来,平移的长度,就是isa8字节) + superclass (8字节) + cache,这三者的长度之和。现在已知isasuperclass的长度,但是cache的长度不知道。我们可以看下cache_t底层的类型。
C7930A52-5CA8-4F08-BD58-DA1987B72469.png

cache_t底层的类型是一个结构体,结构体的内存大小,是根据结构体里面的内容来决定的。因为,结构体里面的方法、函数的内存不存储在结构体的内存区域里面,还有全局变量的内存是在全局区域,也不占结构体的内存,通过查看源码(源码太长,不粘贴出来了),所以,最后只剩下:
2DF6B088-E184-407C-8716-2E13C2F52CFF.png
那么只剩下一个explicit_atomic<uintptr_t> _bucketsAndMaybeMask和一个联合体。而在explicit_atomic的声明,发现

struct explicit_atomic : public std::atomic<T> {***}
复制代码

是一个public类型,所以他的内存大小取决于<uintptr_t>的大小。那么uintptr_t是一个无符号的长整型,所以是8字节大小(64位)。

typedef unsigned long   uintptr_t;
复制代码

在联合体里面,同理,explicit_atomic<mask_t> _maybeMask;的大小取决于<mask_t>,而mask_t,是

typedef uint32_t mask_t; 
复制代码

所以explicit_atomic<mask_t> _maybeMask内存大小为4uint16_t _flags;和uint16_t _occupied;都是2,它们三者的总和是8
同理explicit_atomic<preopt_cache_t *> _originalPreoptCache;的大小取决于<preopt_cache_t *>,而preopt_cache_t *是一个指针,所以内存大小为8

由于联合体是互斥的,所以联合体的内存大小是8。所以,最终算得cache的内存大小是8 + 8 = 16.

所以,要看第四个地址的值,那么首地址就要平移 8 + 8 + 16 = 32字节大小的位置。转变成二进制,就是0x20。首地址是:0x1000084a8,接下来就通过lldb调试:
EA5A0E1E-87EA-4BF7-8495-F3662F86D664.png
之所以使用class_data_bits_t,原因是:当前是在进行取地址,而这个地址指向的空间是bits空间,所以就能强转成(class_data_bits_t *)这么一个指针地址

5.lldb分析类的结构

刚刚在源码分析中,知道了bits的计算流程,接着,就通过lldb来分析类的结构。通过源码,知道bits里面包含了class_rw_t,返回了class_rw_t的内容,那么猜想出:class_rw_t里面就有对外的接口。
9AEF4AB1-8A36-462F-AEE6-910A3D6BDC9B.png

根据源码里面的提示,可以先取出bits里面的data():
2837F67B-C334-480D-8013-17DABCAABDDD.png
这样子,就把bitsdata打印出来了。查看所打印的信息,发现,在TestPerson类里面的name成员变量根本就没出现在这里面。那么还需要接着找。

回想下,刚刚在打印data()的时候,在源码里面,这个dataclass_rw_t,那么就进入到class_rw_t的底层,去寻找答案。既然能在外面获取data,那么在class_rw_t的底层,就有提供对外的方法。刚好印证了之前的猜想。所以可以从这里入手。在class_rw_t的底层源码里面,有一段代码:

const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
        }
    }
复制代码

因为我们在声明TestPerson类的成员变量使用的方式是@property (nonatomic, copy) NSString *name;,那么,找property_array_t properties()应该是可以的。通过lldb调试:
B7709111-2176-4E92-8F43-CD799743FAD2.png

此时,我们目前得到的,还是property_array_t,还需要进入到property_array_t里面去继续跟踪。进入其底层源码:

class property_array_t : 
    public list_array_tt<property_t, property_list_t, RawPtr>
{
    typedef list_array_tt<property_t, property_list_t, RawPtr> Super;

 public:
    property_array_t() : Super() { }
    property_array_t(property_list_t *l) : Super(l) { }
};
复制代码

可以看到,是分两层拷贝的(就是最外层是一个数组,这个数组里面的元素还是数组),一个property_t,另外一个是property_list_t,再进入list_array_tt,查看其底层(源码太多,不粘出来了),这里面就有很多的迭代器(就如数组遍历,就是一个迭代器)。这里面就有我们要找的name信息。再进行lldb调试:
9E8006CF-3AC9-449A-AE4D-0417E27BD4E9.png
这么一层层的找下来,最后通过get方法,把name这个成员变量的信息找出来了。
s

6.类的bits数据分析

通过lldb分析,找到了需要找的name信息,在TestPerson类里面添加更多的成员变量,再添加一个实例方法和一个类方法:
9E8006CF-3AC9-449A-AE4D-0417E27BD4E9.png

然后再运行objc源码,到断点处,再进行lldb调试:
C8EEC0D0-7B68-49C0-9783-8883D5300BEC.png
最后,我们发现,我们只能找到namehobby这个两个成员变量,还有一个subject没发现。还有定义的两个方法。这是我们通过property_array_t,只找到namehobby这个两个成员变量。

那么接下来,再试试通过methods(),再去挖掘下,看能不能发现新的东西。
5FB8A06B-FC5B-48E8-BAC2-EE346E770FFD.png
找到了5个方法,但是方法都为空,这是为什么了?
那就要到底层源码中去寻找答案了。
class_rw_t底层中,有

const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
        }
    }
    
const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
        }
    }
复制代码

property_array_t的底层的property_list_t是没有实现的

class property_array_t : 
    public list_array_tt<property_t, property_list_t, RawPtr>
{
    typedef list_array_tt<property_t, property_list_t, RawPtr> Super;

 public:
    property_array_t() : Super() { }
    property_array_t(property_list_t *l) : Super(l) { }
};
复制代码

源码中,property_list_t底层没有实现

struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};

复制代码

而我们所需要的信息是在property_t里面,课以查看下property_t的声明,全文索引 property_t {,可以看到是结构体,我们之所以通过data(),逐级查询,能够获取namehobby的信息,就在此了。

再看看method_array_t的底层的method_list_t是有实现的

class method_array_t : 
    public list_array_tt<method_t, method_list_t, method_list_t_authed_ptr>
{
    typedef list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> Super;

 public:
    method_array_t() : Super() { }
    method_array_t(method_list_t *l) : Super(l) { }

    const method_list_t_authed_ptr<method_list_t> *beginCategoryMethodLists() const {
        return beginLists();
    }
    
    const method_list_t_authed_ptr<method_list_t> *endCategoryMethodLists(Class cls) const;
};
复制代码

method_list_t 的底层实现

struct method_list_t : entsize_list_tt<method_t, method_list_t, 0xffff0003, method_t::pointer_modifier> {
    bool isUniqued() const;
    bool isFixedUp() const;
    void setFixedUp();

    uint32_t indexOfMethod(const method_t *meth) const {
        uint32_t i = 
            (uint32_t)(((uintptr_t)meth - (uintptr_t)this) / entsize());
        ASSERT(i < count);
        return i;
    }

    bool isSmallList() const {
        return flags() & method_t::smallMethodListFlag;
    }

    bool isExpectedSize() const {
        if (isSmallList())
            return entsize() == method_t::smallSize;
        else
            return entsize() == method_t::bigSize;
    }

    method_list_t *duplicate() const {
        method_list_t *dup;
        if (isSmallList()) {
            dup = (method_list_t *)calloc(byteSize(method_t::bigSize, count), 1);
            dup->entsizeAndFlags = method_t::bigSize;
        } else {
            dup = (method_list_t *)calloc(this->byteSize(), 1);
            dup->entsizeAndFlags = this->entsizeAndFlags;
        }
        dup->count = this->count;
        std::copy(begin(), end(), dup->begin());
        return dup;
    }
};
复制代码

那么同理,我们所需要的信息是在method_t里面,课以查看下method_t的声明,全文索引 method_t {,看源码(部分)

struct method_t {
    static const uint32_t smallMethodListFlag = 0x80000000;

    method_t(const method_t &other) = delete;

    // The representation of a "big" method. This is the traditional
    // representation of three pointers storing the selector, types
    // and implementation.
    struct big {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };

private:
    bool isSmall() const {
        return ((uintptr_t)this & 1) == 1;
    }

    // The representation of a "small" method. This stores three
    // relative offsets to the name, types, and implementation.
    struct small {
        // The name field either refers to a selector (in the shared
        // cache) or a selref (everywhere else).
        RelativePointer<const void *> name;
复制代码

有个big()的对外输出,也就是可以通过方法.big()就能够打印出对应的方法信息

  big &big() const {
        ASSERT(!isSmall());
        return *(struct big *)this;
    }
复制代码

那么就能通过lldb调试
D41E1D12-9E89-42E1-AC5C-B732203E8A75.png
能够把部分的方法打印出来,但是还是缺少subject成员变量,和+ (void)say666;的类方法。这些将在后面的文章中,进行相应的补充。

补充内容—指针和内存平移

如何获取当前的内存结构—–之内存偏移

  • 普通指针

比如:新起一个工程,打印两个int变量,来查看其内存情况,数据和地址:

int a = 10; //
int b = 10; //
NSLog(@"%d -- %p",a,&a);
NSLog(@"%d -- %p",b,&b);
复制代码

15AA2245-3207-4D03-8A25-8989CF336BBD.png
根据这个打印的结果,看出,在内存某个区域里面,有两个指针,指向同一个的值区域。
未命名文件-4.png
有两个指针,同时指向某一个值为10的区域,意味着这个值区域能被任何指针访问。这一个现象有点类似于值拷贝(不是真的拷贝啊),把这个值区域的值,给到ab

  • 对象指针

接下来,创建一个TestPerson类,两次初始化,形成两个对象:

TestPerson *p1 = [TestPerson alloc];
TestPerson *p2 = [TestPerson alloc];
NSLog(@"%@ -- %p",p1,&p1);
NSLog(@"%@ -- %p",p2,&p2);
复制代码

E74B4640-66DD-4EC3-A444-6855107113B8.png
从打印的结果来看,他们两者的地址不同,并且他们所指向的空间也不同。
未命名文件.png

  • 数组指针
// 数组指针
int c[4] = {1,2,3,4};
int *d   = c;
NSLog(@"%p - %p - %p",&c,&c[0],&c[1]);
NSLog(@"%p - %p - %p",d,d+1,d+2);
复制代码

打印的结果:
5F02A314-91CA-4426-BA9E-4E8C01690C8D.png
从打印结果看出,&c&c[0]是同一个地址,那是因为&c[0]是首地址,数组中,地址的排列是顶针存储的,还是连续的存储。
未命名文件-2.png

d、d+1、d+2,其中d就是&c的指针地址,d+1就是按照现有的数据结构平移1个间隔大小为4的单位。同理,d+2就是平移2个间隔大小为4的单位。如果是对象,间隔单位大小就为8.

按照上面的分析结果,可以有个大胆的构想,通过for循环把数组c[4]的元素遍历出来,就可以把原本的int value = c[i],改成 int value = *(d+i);,那么同样能够遍历出来:

int c[4] = {1,2,3,4};
int *d   = c;

for (int i = 0; i<4; i++) {
   // int value = c[i];
   int value =  *(d+i);
      NSLog(@"%d",value);
}
复制代码

打印结果:
868D6B5C-46AF-4F77-B75E-1727BB8865AB.png
得出结论:内存是可以进行平移,平移后,取地址里面的值,就可以得到当前内存里面的值。

刚刚我们是获取了类的地址, 那么这个类的地址,就是当前内存结构里面的首地址。有了首地址,就可以按照刚刚内存平移的步骤,平移一些大小,就能得到里面的内容。

到了此处,欧耶,大功告成,类的原理的底层探究,就完成了,有木有点收获啊,(不许没有啊<( ̄▽ ̄)/)
感谢各位的光临71623057628_.pic.jpg

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享