洋仔的博客 洋仔的博客
首页
  • 个人心法总结

    • 价值心法
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • iOS基础知识
  • 前端
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 投资体系
  • 毛选
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

洋仔

奋斗的小青年
首页
  • 个人心法总结

    • 价值心法
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • iOS基础知识
  • 前端
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 投资体系
  • 毛选
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 技术文档

  • GitHub技巧

  • Nodejs

  • 博客搭建

  • iOS基础知识

    • iOS底层相关

      • OC基础之常见知识
      • OC基础之属性关键字
      • OC基础之底层系列
        • 内存对齐的原因
        • 内存对齐的原理
        • 内存对齐规则
        • OC对象的分类
        • instance对象 (实例对象)
        • Class对象 (类对象)
          • cache_t结构
        • meta-Class 元类对象
          • 为什么要设计metaclass?
        • isa指针
        • superClass 指针
          • 对像方法
          • 类方法
          • superclass走位
      • OC反射机制系列
      • 分类与扩展
      • load跟initialize
      • isa指针是什么
      • 引用计数管理
      • 字典系列
    • Runloop系列

    • Runtime系列

    • 内存管理系列

    • Block系列

    • 线程系列

    • KVC跟KVO系列以及通知中心

    • UI系列

    • 离屏渲染系列

    • 组件化系列跟架构

    • OC跟webview交互系列

    • 持久化系列

    • APP编译系列

    • APP性能优化系列

    • cocoapods系列

    • swift系列

    • Git系列

    • 网络相关

    • 三方库系列

    • 系统原理

    • 总结系列

    • 算法系列

    • 数据结构系列

  • 前端

  • 技术
  • iOS基础知识
  • iOS底层相关
yangyang.wen
2023-08-11
目录

OC基础之底层系列

# 内存对齐的原因

平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

假如没有内存对齐机制,数据可以任意存放,现在一个int变量存放在从地址1开始的联系四个字节地址中,该处理器去取数据时,要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),最后留下的两块数据合并放入寄存器。这需要做很多工作。 现在有了内存对齐的,int类型数据只能存放在按照对齐规则的内存中,比如说0地址开始的内存。那么现在该处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作,提高了效率。

# 内存对齐的原理

内存在进行IO的时候,一次操作取的就是64个bit。

所以,内存对齐最最底层的原因是内存的IO是以64bit为单位进行的。 对于64位数据宽度的内存,假如cpu也是64位的cpu(现在的计算机基本都是这样的),每次内存IO获取数据都是从同行同列的8个chip中各自读取一个字节拼起来的。从内存的0地址开始,0-63bit的数据可以一次IO读取出来,64-127bit的数据也可以一次读取出来。CPU和内存IO的硬件限制导致没办法一次跨在两个数据宽度中间进行IO。

假如对于一个c的程序员,如果把一个bigint(64位)地址写到的0x0001开始,而不是0x0000开始,那么数据并没有存在同一行列地址上。因此cpu必须得让内存工作两次才能取到完整的数据。效率自然就很低。这下你有没有彻底理解了内存对齐?

  • 扩展1:如果不强制对地址进行操作,仅仅只是简单用c定义一个结构体,编译和链接器会自动替开发者对齐内存的。尽量帮你保证一个变量不跨列寻址。

  • 扩展2:其实在内存硬件层上,还有操作系统层。操作系统还管理了CPU的一级、二级、三级缓存。实际中不一定每次IO都从内存出,如果你的数据局部性足够好,那么很有可能只需要少量的内存IO,大部分都是更为高效的高速缓存IO。但是高速缓存和内存一样,也是要考虑对齐的。

# 内存对齐规则

  • 基本类型的对齐值就是其sizeof值;
  • 数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行;
  • 结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行;
点击查看
//2020.05.12 公众号:C语言与CPP编程
#include<stdio.h>
struct
{
    int i;
    char c1;
    char c2;
}Test1;

struct{
    char c1;
    int i;
    char c2;
}Test2;

struct{
    char c1;
    char c2;
    int i;
}Test3;

int main()
{
    printf("%d\n",sizeof(Test1));  // 输出8
    printf("%d\n",sizeof(Test2));  // 输出12
    printf("%d\n",sizeof(Test3));  // 输出8
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

默认#pragma pack(4),且结构体中最长的数据类型为4个字节,所以有效对齐单位为4字节,下面根据上面所说的规则以第二个结构体来分析其内存布局:首先使用规则1,对成员变量进行对齐:

sizeof(c1) = 1 <= 4(有效对齐位),按照1字节对齐,占用第0单元; sizeof(i) = 4 <= 4(有效对齐位),相对于结构体首地址的偏移要为4的倍数,占用第4,5,6,7单元; sizeof(c2) = 1 <= 4(有效对齐位),相对于结构体首地址的偏移要为1的倍数,占用第8单元; 然后使用规则2,对结构体整体进行对齐:

第二个结构体中变量i占用内存最大占4字节,而有效对齐单位也为4字节,两者较小值就是4字节。因此整体也是按照4字节对齐。由规则1得到s2占9个字节,此处再按照规则2进行整体的4字节对齐,所以整个结构体占用12个字节。

根据上面的分析,不难得出上面例子三个结构体的内存布局如下:

## 一个OC对象占用多少内存

系统分配了16个字节给NSObject对象(通过malloc_size函数获得) 但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)

# OC对象的分类

OC对象 可以分为3种:

  1. instance对象 (实例对象)
  2. class对象 (类对象)
  3. meta-class对象 (元类对象)

# instance对象 (实例对象)

instance对象就是通过类alloc出来的对象,每次调用alloc都会产生新的instance对象

instance对象在内存中存储的信息包括 -isa指针 -其他成员变量

# Class对象 (类对象)

我们平时说的类,其实也是对象,称为类对象, 每个类在内存中有且只有一个class对象

# cache_t结构

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
    ...
};

struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    MethodCacheIMP _imp;
    cache_key_t _key;
#else
    cache_key_t _key;
    MethodCacheIMP _imp;
#endif

public:
    inline cache_key_t key() const { return _key; }
    inline IMP imp() const { return (IMP)_imp; }
    inline void setKey(cache_key_t newKey) { _key = newKey; }
    inline void setImp(IMP newImp) { _imp = newImp; }

    void set(cache_key_t newKey, IMP newImp);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

以上bucket_t的属性和方法中可以看出它应该与imp有联系——事实上bucket_t作为一个桶,里面是用来装imp方法实现以及它的key

LRU算法

也就是最近最少使用策略——这个策略的核心思想就是先淘汰最近最少使用的内容,在方法缓存中也用到了这种算法

# meta-Class 元类对象

每个类在内存中有且只有一个meta-class对象

// 将类对象当做参数传入,获得元类对象 Class objectMetaClass = object_getClass(objectClass5);

objectMetaClass是NSObject的meta-class对象(元类对象)

每个类在内存中有且只有一个meta-class对象

meta-class对象和class对象的内存结构是一样的,但是用途不一样,在内存中存储的信息主要包括

  • isa指针
  • superclass指针
  • 类的类方法信息(class method)

# 为什么要设计metaclass?

先说结论: 为了更好的复用传递消息.metaclass只是需要实现复用消息传递为目的工具.而Objective-C所有的类默认都是同一个MetaClass(通过isa指针最终指向metaclass). 因为Objective-C的特性基本上是照搬的Smalltalk,Smalltalk中的MetaClass的设计是Smalltalk-80加入的.所以Objective-C也就有了metaclass的设计.

继承关系

# isa指针

instance的isa指向class 当调用对象方法时,通过instance的isa找到class,最后找到对象方法的实现进行调用

class的isa指向meta-class 当调用类方法时,通过class的isa找到meta-class,最后找到类方法的实现进行调用

# superClass 指针

class对象的superclass指针

@interface Student: Person

@interfce Person: NSObject

当Student的instance对象要调用Person的对象方法时,会先通过isa找到Student的class, 然后通过superclass找到Person的class,最后找到对象方法的实现进行调用

meta-class对象的superclass指针

当Student的class要调用Person的类方法时,会先通过isa找到Student的meta-class, 然后通过superclass找到Person的meta-class,最后找到类方法的实现进行调用

提示

类对象存储实例方法列表等信息

元类对象存储类方法列表等信息

# 对像方法

  1. 实例对象(instance)要调用对象方法1.通过isa指针 -> 找到自己所属的类对象 -> 查找并调用方法

  2. 如果在自己的类对象没有找到方法,通过类对象的superclass指针 -> 找到父类的类对象 ->查找并调用方法

  3. 如果还没有找到,就通过superclass指针一直通过继承关系往上找,直到基类的类对象,如果还是没有找到,抛出异常

# 类方法

  1. 类对象通过isa指针 -> 找到自己的元类对象 -> 查找并调用方法

  2. 如果没有找到,通过元类对象的superclass指针 -> 找到父类的元类对象 -> 查找并且调用方法

  3. 如果还是没有找到,根据继承体系,通过元类对象的superclass指针一直找到基类的元类对象,查找并调用方法

  4. 如果基类的元类对象也找不到该类方法,会通过基类元类对象的superclass指针找到基类的类对象,查找有没有同名的对象方法,找到就调用,没有就抛出异常

isa的走向有以下几点说明:

实例对象(Instance of Subclass)的 isa 指向 类(class)

类对象(class) isa 指向 元类(Meta class)

元类(Meta class)的isa 指向 根元类(Root metal class)

根元类(Root metal class) 的isa 指向它自己本身,形成闭环,这里的根元类就是NSObject

# superclass走位

superclass(即继承关系)的走向也有以下几点说明:

类 之间 的继承关系:

类(subClass) 继承自 父类(superClass)

父类(superClass) 继承自 根类(RootClass),此时的根类是指NSObject

根类 继承自 nil,所以根类即NSObject可以理解为万物起源,即无中生有

元类也存在继承,元类之间的继承关系如下:

子类的元类(metal SubClass) 继承自 父类的元类(metal SuperClass)

父类的元类(metal SuperClass) 继承自 根元类(Root metal Class

根元类(Root metal Class) 继承于 根类(Root class),此时的根类是指NSObject

举例

NyanCat *cat = [[NyanCat alloc] init]; [cat nyan1];

向cat (instance) 发送消息nyan1时,运行时会通过isa指针查找到NyanCat(Class),这里保存着本类中定义的实例方法的指针。

[NyanCat nyan2];

向NyanCat(Class)发送消息nyan2时,运行时会通过isa查找到NyanCat(meta-class),这里保存着本类中定义的类方法的指针。

类的继承

在_class_t里面,第二个成员是superclass,很明显这个指针指向了它的父类。运行时可以通过isa和superclass获取一个类在继承树上的完整信息。

为了说明方便,这里把上面的例子稍微改一下:NyanCat : Cat : NSObject 这样一个继承树,画出图来就是这样子的

编辑 (opens new window)
上次更新: 2024/10/23, 23:26:17
OC基础之属性关键字
OC反射机制系列

← OC基础之属性关键字 OC反射机制系列→

最近更新
01
数组
10-25
02
数组双指针系列之对撞指针
10-25
03
数组双指针系列之快慢指针
10-25
更多文章>
Theme by Vdoing | Copyright © 2019-2024 Evan Xu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式