iOS底层原理探索 一 类结构分析

iOS底层原理探索篇 主要是围绕底层进行源码分析-LLDB调试-源码断点-汇编调试,让本身之后回顾复习的😀😀数组

目录以下:缓存

iOS底层原理探索 — 开篇bash

iOS底层原理探索 — alloc&init探索iphone

iOS底层原理探索 — 内存对齐&malloc源码分析ide

iOS底层原理探索 一 isa原理与对象的本质函数

iOS底层原理探索 一 类结构分析源码分析

1、章前复习

经过前面篇章的探索,咱们已成功的从对象过渡到类了.但在探索类以前,还须要补充一下咱们在前面篇章中没有细讲的一些小细节.post

1.1 alloc的一个小细节

咱们在iOS底层原理探索 — alloc&init探索一文中留下了一个细节没有细说,就是在分析alloc源码分析流程的时候,在调用callAlloc方法时,咱们只是简单的说了:此方法内部有一系列的判断条件,其中因为方法canAllocFast()的内部调用了bits.canAllocFast(),其返回值为固定值false,因此能够肯定以后建立对象只会走class_createInstance方法.即:callAllocif (fastpath(cls->canAllocFast()))方法不走直接走的else后面的代码.那么为何会这样呢?来看源码:测试

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (slowpath(checkNil && !cls)) return nil;

#if __OBJC2__ //这个表示object-c 2.0 版本才有的功能
    /*
        这里的hasDefaultAWZ()方法是用来判断当前class是否有默认的allocWithZone。
        if (fastpath(!cls->ISA()->hasCustomAWZ())):
        意思就是若是该类实现了allocWithZone方法,那么就不会走if里的逻辑,直接走如下逻辑
        if (allocWithZone) return [cls allocWithZone:nil];
     */
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        if (fastpath(cls->canAllocFast())) {
            // No ctors, raw isa, etc. Go straight to the metal.
            bool dtor = cls->hasCxxDtor();
            id obj = (id)calloc(1, cls->bits.fastInstanceSize());
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            obj->initInstanceIsa(cls, dtor);//initInstanceIsa 里面是初始化 isa 指针的操做。
            return obj;
        }
        else {
            id obj = class_createInstance(cls, 0);
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            return obj;
        }
    }
#endif

    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}
复制代码
  • 第一个判断fastpath(!cls->ISA()->hasCustomAWZ())的决定条件就是你是否有重写allocWithZone的方法:即if (fastpath(!cls->ISA()->hasCustomAWZ())):意思就是若是该类实现了allocWithZone方法,那么就不会走if里的逻辑,直接走if (allocWithZone) return [cls allocWithZone:nil];
  • 第二个判断fastpath(cls->canAllocFast())就是关于宏定义的设置:咱们沿着源码点进去能够看到:
    bool canAllocFast() {
          assert(!isFuture());
          return bits.canAllocFast();
      }
    复制代码
    顺着bits.canAllocFast();点进去能够看到:
    #if FAST_ALLOC
      size_t fastInstanceSize() 
      {
          assert(bits & FAST_ALLOC);
          return (bits >> FAST_SHIFTED_SIZE_SHIFT) * 16;
      }
      void setFastInstanceSize(size_t newSize) 
      {
          // Set during realization or construction only. No locking needed.
          assert(data()->flags & RW_REALIZING);
    
          // Round up to 16-byte boundary, then divide to get 16-byte units
          newSize = ((newSize + 15) & ~15) / 16;
          
          uintptr_t newBits = newSize << FAST_SHIFTED_SIZE_SHIFT;
          if ((newBits >> FAST_SHIFTED_SIZE_SHIFT) == newSize) {
              int shift = WORD_BITS - FAST_SHIFTED_SIZE_SHIFT;
              uintptr_t oldBits = (bits << shift) >> shift;
              if ((oldBits & FAST_ALLOC_MASK) == FAST_ALLOC_VALUE) {
                  newBits |= FAST_ALLOC;
              }
              bits = oldBits | newBits;
          }
      }
    
      bool canAllocFast() {
          return bits & FAST_ALLOC;
      }
      #else // 通常都会走这里
      size_t fastInstanceSize() {
          abort();
      }
      void setFastInstanceSize(size_t) {
          // nothing
      }
      // 通常流程都会走这个false的返回
      bool canAllocFast() {
          return false;
      }
      #endif
    复制代码
    通常都会走#else后面的代码,也就是bool canAllocFast(){return false}.为何会这样呢?,这就要去看条件控制:#if FAST_ALLOC这个宏定义的走向了. 在全局搜索宏定义FAST_ALLOC,发现#define FAST_ALLOC (1UL<<2)而这个宏定义外面还加了一层条件判断:
#if !__LP64__
   ...
   #elif 1
   ...
   #else
   ...
   #define FAST_ALLOC (1UL<<2)
   #endif
复制代码

由于咱们的环境都是在64位环境下,因此能够判断上面的判断只会走#elif 1里面的代码,而#define FAST_ALLOC的定义是在#else里面,即FAST_ALLOC永远都不会define了.即只会走bool canAllocFast(){return false},进而就有callAllocif (fastpath(cls->canAllocFast()))方法不走,直接走的else{}里面的代码.即走的下面红框里面的代码 优化

1.2 联合体互斥

咱们在iOS底层原理探索 一 isa原理与对象的本质一文中有分析到,isa的结构实际上是一个联合体,而联合体有一大特性,就是其内部属性是共享同一片内存的,也就是说属性之间都是互斥的.

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的时候,一个分支是赋值cls属性,一个分支是赋值bits属性了.

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    assert(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa.cls = cls;
    } else {
        assert(!DisableNonpointerIsa);
        assert(!cls->instancesRequireRawIsa());

        isa_t newisa(0);

#if SUPPORT_INDEXED_ISA
        assert(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
        isa = newisa;
    }
}
复制代码

2、类和元类的建立时机

咱们在探索类和元类的时候,对于其建立时机还不是很清楚,这里咱们先抛出结论:类和元类是在编译期建立的,即在进行alloc操做以前,类和元类就已经被编译器建立出来了. 那么如何来证实呢,咱们有两种方式来证实:

2.1 经过LLDB指令打印类和元类指针

咱们在 main函数开始以前打上断点,也就没有来到 TCJPerson *obj = [TCJPerson alloc];,可是咱们经过 LLDB能打印出 TCJPerson的类和元类.这就证实了,类和元类的建立时机是在编译期.

2.2 经过MachoView软件辅助证实:

MachoView 密码:kx8c 编译项目后,使用MachoView打开程序二进制可执行文件查看:

经过上面两种方式证实了:类和元类的建立时机是在编译期.

3、指针内存偏移

3.1 普通指针 - 值拷贝

咱们观察上面的代码,虽然整型变量 ab都是被赋值为10,可是 ab内存地址是不同的,这种方式被称为 值拷贝.

3.2 对象 - 指针拷贝或引用拷贝

经过运行结果,能够知道 obj1obj2对象不光自身内存地址不同,连指向的对象的内存地址也不同,这种方式被称为 指针拷贝引用拷贝.

咱们能够用一幅图来总结上面的两个例子:

3.3 用数组指针引出 - 内存偏移

经过运行结果能够看到:

  • &a&a[0]的地址是相同的.即首地址就表明数组的第一个元素的地址.
  • 第一个元素地址0x7ffeefbff400和第二个元素地址0x7ffeefbff404相差4个字节,也就是int的所占的4字节.
  • dd+1d+2这个地方的指针相加就是偏移地址.地址加1就是偏移,偏移一个位数所在元素的大小.
  • 能够经过地址,取出对应地址的值.

4、类的结构分析

OC中的类其实也是一种对象,怎么来证实呢,很简单,咱们只须要用clang命令重写咱们的OC代码将其转化为C++代码看其底层便可.

4.1 建立TCJPerson对象,并获取到TCJPerson的类,而后利用LLDB指令查看

经过上面结构能够得知:

  • 输出第二个内存地址获得NSObject,继续输出第三个发现输出不了.
  • 经过前面iOS底层原理探索 一 isa原理与对象的本质一文的分析,咱们知道第二个内存地址存储的是Class superclass,它表明的是继承关系,也即证实了TCJPerson是继承自NSObject的.

4.2 将OC代码转化为C++代码帮助分析

原文件main.c:

#import <Foundation/Foundation.h>
    #import <objc/runtime.h>

    @interface TCJPerson : NSObject

    @end

    @implementation TCJPerson

    @end

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

            TCJPerson *obj = [TCJPerson alloc];
            Class objClass = object_getClass(obj);
            NSLog(@"%@ - %p", obj, objClass); //0x00007ffffffffff8ULL
        }
        return 0;
    }
复制代码

在终端执行clang指令:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
复制代码

便可将OC原文件main.c转化为C++文件mian.cpp文件后可看到:

至此,咱们能够得出一个结论,Class类型在底层是一个结构体类型的指针,这个结构体类型为objc_classs. 咱们再在libObjc的源码中能够找到objc_classs的详细定义:

经过 objc_classs的定义,咱们能够知道, objc_classs是继承于 objc_object的.这就证实了 万物皆对象,也从本质上说明 类是一种对象,而且第一个属性是从 objc_object上继承而来的 isa. 除了 isa,类还包含了 superclass父类:表达继承关系; cache:方法缓存重要结构体; bits:存储数据的结构体.

至此咱们能够总结得出:

  • 类是一种对象,而且帮咱们定义了一些属性和方法.

  • OC是对C的底层封装,进而有下面的关系:

    C OC
    objc_object NSObject
    objc_class NSObject(Class)

最后咱们知道Class的基本结构类型为:

到这有一个疑问:为何在外面 isaClass?

  • 万物皆对象,isa是能够由Class接收的.
  • 早期调用isa是用来返回类的,后面是经过nonpointer区分纯净isa和优化的isa.
  • 用源码查看有:return (Class)(isa.bits & ISA_MASK),进行了Class类型强转.

5、类的属性存储探索

OC中的类都会有属性及成员变量,那么它们到底是怎么存在于类里面的呢?

5.1 类结构的isa、superclass、cache属性

这里咱们须要对类的内存结构有一个比较清晰的认识:

类的内存结构 大小(字节)
isa 8
superclass 8
cache 16

前面两个的大小很好理解,由于isasuperclass都是结构体指针,而在arm64环境下,一个结构体指针的内存占用大小为8字节.而第三个属性cache则须要咱们进行抽丝剥茧了. 来看源码:

从上面的代码咱们能够看出, cache属性实际上是 cache_t类型的结构体,其内部有一个 8字节的结构体指针,有2个各为4字节的 mask_t.因此加起来就是16个字节,也就是说前三个属性总共的内存偏移量为 8 + 8 + 16 = 32 个字节,32 是 10 进制的表示,在 16 进制下就是 20.

5.2 bits属性结合上文提到的内存偏移一块儿探索

利用LLDB命令来探索类结构的第四个属性bits.

咱们为了获得 bits的指针地址,就须要进行指针偏移,这里进行一下16进制下的地址偏移计算:

0x100001200 + 0x20 = 0x100001220
复制代码

咱们继续打印这个地址有:

经过输出结果,得知 bits并非一个对象,而是一个结构体,这里须要进行强转一下:
又由 objc_class源码可知,其内部有 data()方法:

因此接着调用 data()方法拿到 class_rw_t:

接着咱们继续查看 libObjc中关于 class_rw_t的源码:得知 class_rw_t也是一个结构体.

由源码推测出相关的属性应该存放在 properties里面,咱们在打印一下:

接着打印 properties:

咦,竟然为空.为何会这样呢?由于这里咱们漏掉了一个重要的线索就是 const class_ro_t *ro;.咱们来到其源码:
能够看到 ro的类型是 class_ro_t结构体,它包含了 baseMethodListbaseProtocolsivarsbaseProperties 等属性.咱们刚才在 class_rw_t 中没有找到咱们声明在 TCJPerson类中的实例变量 titleStr 和属性 helloName,那么但愿就在 class_ro_t身上了,咱们接着打印看看它的内容:
经过打印结果,咱们猜想,属性应该存在 baseProperties里面,咱们接着打印看看:
嗯哼,还有谁?咱们的属性 helloName被找到了,就存放在 class_ro_tbaseProperites 里面.咦,怎么没有看到咱们的实例变量 titleStr?咱们从 $10count 为 1 能够得知确定不在 baseProperites 里面根.据名称咱们猜想应该是在 $8ivars里面.那咱们接着打印:
嗯哼,实例变量 titleStr也找到了,那为何这里的 count是2呢?咱们接着打印第二个元素看看:
结果为 _helloName.这一结果证明了编译器会帮助咱们给属性 helloName 生成一个带下划线前缀的实例变量 _helloName. 至此,咱们能够处处一下结论:

  • class_rw_t 是能够在运行时来拓展类的一些属性、方法和协议等内容.
  • class_ro_t 是在编译时就已经肯定了的,存储的是类的成员变量、属性、方法和协议等内容.

6、类的方法存储探索

研究完了类的属性是怎么存储的,咱们再来看看类的方法又是怎么存储的. 在TCJPerson类里面增长一个readBook的实例方法和一个writeBook的类方法.

按照前面的思路,咱们直接读取 class_ro_t 中的 baseMethodList 的内容:
嗯哼, readBook方法被找出来了,这说明 baseMethodList就是存储实例方法的地方.咱们接着打印剩下的内容:
能够看到 baseMethodList中除了咱们的实例方法 readBook外,还有属性 helloNamegettersetter 方法以及一个 C++ 析构方法.而咱们的类方法 writeBook 并无被打印出来.那么类方法存储在哪呢?

7、类的类方法存储探索

咱们上面已经获得了属性,实例方法是怎么样存储的了,可是还留下了一个疑问点,就是类方法是怎么存储的,接下来咱们用 RuntimeAPI 来实际测试一下.

首先 testInstanceMethod_classToMetaclass 方法测试的是分别从类和元类去获取实例方法、类方法的结果.由打印结果咱们能够知道:

  • 对于类对象来讲,readBook 是实例方法,存储于类对象的内存中,不存在于元类对象中.而 writeBook 是类方法,存储于元类对象的内存中,不存在于类对象中.
  • 对于元类对象来讲,readBook 是类对象的实例方法,跟元类不要紧;writeBook 是元类对象的实例方法,因此存在元类中. 咱们再测试另外的一个方法:
    从结果咱们能够看出,对于类对象来讲,经过 class_getClassMethod 获取writeBook是有值的,而获取 readBook 是没有值的;对于元类对象来讲,经过 class_getClassMethod 获取writeBook也是有值的,而获取 readBook 是没有值的.这里第一点很好理解,可是第二点会有点让人糊涂,不是说类方法在元类中是体现为对象方法的吗?怎么经过 class_getClassMethod 从元类中也能拿到 writeBook,咱们进入到 class_getClassMethod 方法内部能够解开这个疑惑:

能够很清楚的看到, class_getClassMethod 方法底层其实调用的是 class_getInstanceMethod,而 cls->getMeta() 方法底层的判断逻辑是若是已是元类就返回,若是不是就返回类的 isa.这也就解释了上面的 writeBook 为何会出如今最后的打印中了. 除了上面的这种方式,咱们还能够经过 isa 的方式来验证类方法存放在元类中.

  • 经过 isa 在类对象中找到元类.

  • 打印元类的 baseMethodsList. 咱们也来验证一下: 首先咱们获取objClass的内存段:

    接着经过 & ISA_MASK拿到其元类,而且打印其内存段:
    接着按照上面类的属性存储探索的思路,进行指针偏移,获取bits属性:这里进行一下16进制下的地址偏移计算:

    0x100001280 + 0x20 = 0x1000012a0
    复制代码

    查找步骤都在图中标明了.这也验证了类方法存放在元类中.

章后总结

  • 类和元类建立于编译时,能够经过 LLDB 来打印类和元类的指针,或者用 MachOView软件查看二进制可执行文件
  • 万物皆对象:类的本质就是对象
  • 类在 class_ro_t 结构中存储了编译时肯定的属性、成员变量、方法和协议等内容,而且对于属性helloName:底层编译会生成相应的settergetter方法,且帮咱们转化为_helloName,对于成员变量titleStr:底层编译不会生成相应的settergetter方法,且没有转化为_titleStr
  • 实例方法存放在类中
  • 类方法存放在元类中

在这一章中咱们完成了对 iOS 中类的结构的探索,下一章咱们将对类的缓存进行探索,敬请期待~

相关文章
相关标签/搜索