内存
iOS 内存分区
高地址
栈区
临时变量,参数等
系统管理,可读可写
FILO 先进后出
连续的内存地址
地址 0x7开头
堆区
alloc,malloc 申请分配地址
手动管理内存,可读可写
不连续的内存地址
链表结构
先进先出 FIFO
内存空间分配灵活,不过也会导致内存碎片
用户管理内存,可能导致内存泄露
内存分配:动态, 速度比栈等都慢
地址 常以0x6 开头
全局/静态区
未分配全局变量区 .bss
已分配全局变量区 .data
以 0x1 开头
退出app时销毁,可读可写
运行中对象内存一直在
常量区
编译时加载,退出app时销毁
代码中的常量申请内存
只读
代码区
二进制代码数据
只读
编译时加载,退出app时销毁
保留区
低地址
二进制重排
启动优化中我们介绍了二进制重排,通过修改文件加载顺序,从而改变函数加载
顺序,从而减少虚拟内存映射真实内存地址缺页问题,最终达到优化加载速度效
果,减少启动时间
详细参考优化篇内容,本节不再介绍
下面要介绍的是字节对齐,本质上也是内存读取速度优化的一种策略
内存(字节)对齐
iOS中创建一个对象, 并且这个对象还有属性,比如 int age 占 4字节,
NSString *name 占 8 字节,等等,那么这个对象在编译分配 内存占用大小时,
会根据属性编写的先后顺序来计算
一个对象至少分配8字节,iOS 64位中至少是16字节,没有任何属性时,多出8
字节,这样保证至少以16字节作单位,方便计算指针位置,也方便扩展
如果属性本身 有嵌套对象,那么一小一大混合排序 过多的话,会产生内存“碎
片”,所以尽量定义属性时,把代码提前排好,这样能节省内存空间
iOS系统本身也是不断更新的,所以也会为 其进行优化
内存平移
简单理解就是内存地址上的 指针移动
一个实例对象调用对象方法,首先去对应的类对象(isa指向对应类对象 首地
址)缓存cache 和方法列表中寻找,通过指针移动来找对应的方法 函数地址,然
后进行调用
Person 类 :
@interface LGPerson : NSObject
- (void)sayHello;
@end
@implementation LGPerson
- (void) saySomething {
NSLog(@"%s: 你好", __func__);
}
@end
测试1:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 指针读取类地址,强转为对象,调用sayHello。
Class cls = [LGPerson class];
void * ht = &cls;
[(__bridge id)ht saySomething];
// 实例化对象,调用sayHello
LGPerson * person = [LGPerson new];
[person saySomething];
}
@end
打印结果:
打印如下:
-[LGPerson saySomething] : 你好
-[LGPerson saySomething] : 你好
通过类对象竟然成功调用 实例对象的 方法 !!!
测试1结果可知,通过类地址强转获得地址 和 实例对象 地址 应该是相同的,不
然怎么会调用成功呢
也就是说,测试1代码中两种实现方法都指向 Person 类对象的地址 isa
为什么首地址都是指向 isa ?
我们回顾下 class 的结构:
struct objc_class : objc_object { // objc_object 内包含 isa
Class superclass; // 父类,元类对象
cache_t cache; // 缓存表
class_data_bits_t bits; // class_rw_t内部用来获取基础信息的标记
class_rw_t *data() { // 可读可写结构:方法,属性,协议 ,对象信息 等
都整合到此处
return bits.data();
}
...
}
objc_object 结构里就是一个 isa ,继承过来的,其顺序在第一位。
也就是说,类对象起始地址就是指向 isa 的,或者说就是 isa 地址。 【isa 占8
字节】
另外,获取对象第一个属性时,肯定至少平移8字节,第二个属性的话就不一
定,可能中间隔断,具体还是以获取到对象属性的偏移位置为准
小结:
测试1 通过类直接强转后指向是类的首地址 isa 位置,刚好和实例对象 要去类对
象寻找方法指向isa 一致,这个巧合导致编译器未能判断前者不是真正的实例对
象,对于编译器来说,它只负责发送指令就行,管你是怎么来的呢
巧合往往也是不稳定的,测试1 中,person 我们没有设置过属性,假如我们设
置一个属性呢,会不会就有问题了,因为通过类直接强转的对象是不能设置属性
的
接下来我们设置一个属性,然后在方法里打印一下看看
测试2
修改结构:
给person 结构增加一个 NSString *kc_hobby;属性。
修改方法:
- (void) saySomething {
NSLog(@"%s - %@",__func__,self.kc_hobby); //kc_hobby是个字符串
}
接着再运行一遍,看结果:
第一个打印: ViewController
第二个打印:null
第二个实例对象因为还没设置属性值打印 null 正常,但是 第一个强转 对象打印
却打印到 控制器了 !!!!
【这里内存逻辑还没有搞明白,后面再补充】
经打印得知内存顺序为: self -> _cmd -> ViewController -> self -> cls ->
ht(TODO)
平移8字节后指向了self,导致打印到控制器了,如果指向未知地址有可能崩溃,
所以会存在问题,用来测试可以。
主要是理解内存平移概念
内存泄露
这个在 优化篇 介绍
主要是指因循环引用导致对象内存得不到释放
内存溢出
直接加载大图等资源文件,或内存中被大量占用,内存不足导致问题
循环创建对象,短时间没有得到释放,最终导致内存分配失败