[iOS] 内存相关
目录:
- 内存对齐
- Tagged Pointer乱七八糟的想法
- &取址
- 对象销毁干啥了
- 内存泄漏的一些情况
- 内存泄漏检测
1. 内存对齐
可参考:https://www.cnblogs.com/xylc/p/3780907.html 以及 https://www.jianshu.com/p/3294668e2d8c
之前面试的时候有个小哥哥问过我struct占用多少内存,我当时真的一脸懵逼,入职以后的小哥哥讲了一下swift的内存,于是我又想起了这个topic。
struct StructOne {
char a; //1字节
double b; //8字节
int c; //4字节
short d; //2字节
} MyStruct1;
struct StructTwo {
double b; //8字节
char a; //1字节
short d; //2字节
int c; //4字节
} MyStruct2;
NSLog(@"%lu---%lu--", sizeof(MyStruct1), sizeof(MyStruct2));
打印出来是24---16--
为什么同样的struct只是变量顺序不一样就会有不同的内存呢?这里就涉及了C语言中的内存对齐啦。
比如对于int x;(这里假设sizeof(int)==4),因为cpu对内存的读取操作是对齐的,如果x的地址不是4的倍数,那么读取这个x,需要读取两次共8个字节,然后还要将其拼接成一个int,这比存取对齐过的x要麻烦很多。
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。
#pragma pack(x)
//...
#pragma pack()
如果改成下面酱紫,打印出来的内存则为15---15--
:
#pragma pack(1)
struct StructOne {
char a; //1字节
double b; //8字节
int c; //4字节
short d; //2字节
} MyStruct1;
struct StructTwo {
double b; //8字节
char a; //1字节
short d; //2字节
int c; //4字节
} MyStruct2;
#pragma pack()
- 是不是很神奇,所以内存对齐到底是什么呢?
-
数据成员对齐规则:
每个数据成员的偏移应为(#pragma pack(指定的数n) 与该数据成员的自身长度中较小那个数的整数倍,不够整数倍的补齐。也就是相对于头部的偏移应该是这个数据的align(x) = min ( sizeof(x) , packalign)
的整数倍。(如果是数组的alignx的计算,以元素为准) -
数据成员为结构体:
如果结构体的数据成员还为结构体,则该数据成员的“自身长度”为其内部最大元素的大小。(struct a 里存有 struct b,b 里有char&int&double等元素,那 b的自身长度为 8) -
结构体的整体对齐规则:
在数据成员按照上述第一步完成各自对齐之后,结构体本身也要进行对齐。对齐会将结构体的大小调整为(#pragma pack(指定的数n) 与结构体中的最大长度的数据成员中较小那个的整数倍,不够的补齐。也就是整体的size为min(pack, max(成员长度))的整数倍。
所以当我们修改pack时,整体的内存布局都会被改变。但是如果修改pack其实会导致内存读取的时候的效率变低,仍旧会有可能读一个int需要读8个字节,所以这个又是空间还是时间的问题了。
struct与class的区别是神马呢?
首先在C里面认为数据和数据操作是分开的,所以其实struct只是一个数据结构,但是C++对struct进行了拓展:
- struct可以包括成员函数
- struct可以实现继承
- struct可以实现多态
那么在C++中,struct与class有神马区别呢?
-
默认的继承访问权
class默认的是private,struct默认的是public。 -
默认访问权限
struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的。 -
class这个关键字还用于定义模板参数,就像“typename”。但关键字struct不用于定义模板参数
虽然感觉struct是多余的,但考虑到“对c兼容”就将struct保留了下来,并做了一些扩展使其更适合面向对象,所以c++中的struct再也不是c中的那个了。
两者最大的区别就在于思想上,c语言编程单位是函数,语句是程序的基本单元。而C++语言的编程单位是类。从c到c++的设计有过程设计为中心向以数据组织为中心转移。
class的内存结构是神马呢?ARC指针存在哪里?

- has_assoc:对象含有或者曾经含有关联引用,没有关联引用的可以更快地释放内存
- has_cxx_dtor:表示该对象是否有 C++ 或者 Objc 的析构器
- shiftcls:类的指针。arm64架构中有33位可以存储类指针。
源码中isa.shiftcls = (uintptr_t)cls >> 3;
将当前地址右移三位的主要原因是用于将 Class 指针中无用的后三位清除减小内存的消耗,因为类的指针要按照字节(8 bits)对齐内存,其指针后三位都是没有意义的 0。具体可以看从 NSObject 的初始化了解 isa这篇文章里面的shiftcls分析。 - magic:判断对象是否初始化完成,在arm64中0x16是调试器判断当前对象是真的对象还是没有初始化的空间。
- weakly_referenced:对象被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放
- deallocating:对象是否正在释放内存
- has_sidetable_rc:判断该对象的引用计数是否过大,如果过大则需要其他散列表来进行存储。
- extra_rc:存放该对象的引用计数值减一后的结果。对象的引用计数超过 1,会存在这个这个里面,如果引用计数为 10,extra_rc的值就为 9。
也就是说其实class的struct里面是有用于保存引用计数的部分的哦。
Tagged Pointer
在2013年9月,苹果推出了iPhone5s,与此同时,iPhone5s配备了首个采用64位架构的A7双核处理器,为了节省内存和提高执行效率,苹果提出了Tagged Pointer的概念。
先看看原有的对象为什么会浪费内存。假设要存储一个NSNumber对象,其值是一个整数。正常情况下,如果这个整数只是一个NSInteger的普通变量,那么它所占用的内存是与CPU的位数有关,在32位CPU下占4个字节,在64位CPU下是占8个字节的。而指针类型的大小通常也是与CPU位数相关,一个指针所占用的内存在32位CPU下为4个字节,在64位CPU下也是8个字节。所以一个普通的iOS程序,如果没有Tagged Pointer对象,从32位机器迁移到64位机器中后,虽然逻辑没有任何变化,但这种NSNumber、NSDate一类的对象所占用的内存会翻倍。
为了存储和访问一个NSNumber对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失。
为了改进上面提到的内存占用和效率问题,苹果提出了Tagged Pointer对象。由于NSNumber、NSDate一类的变量本身的值需要占用的内存大小常常不需要8个字节,拿整数来说,4个字节所能表示的有符号整数就可以达到20多亿。所以我们可以将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。
简单来讲可以理解为把指针指向的内容直接放在了指针变量的内存地址中。
- Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate
- Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free。
- 在内存读取上有着 3 倍的效率,创建时比以前快 106 倍。
我们来尝试看下NSNumber的pointer:
NSNumber *number1 = @1;
NSNumber *number2 = @2;
NSNumber *number3 = @3;
NSLog(@"number1 pointer is %p", number1);
NSLog(@"number2 pointer is %p", number2);
NSLog(@"number3 pointer is %p", number3);
输出:
2019-11-24 15:09:59.728428+0800 Example1[27147:770431] number1 pointer is 0xfacae31dc706ba6f
2019-11-24 15:09:59.728551+0800 Example1[27147:770431] number2 pointer is 0xfacae31dc706ba5f
2019-11-24 15:09:59.728638+0800 Example1[27147:770431] number3 pointer is 0xfacae31dc706ba4f
一个比较神奇的事情是倒数第二位在数字依次增大,但是哦如果你把数字改为1&2&4会发现又不连续了。。所以其实这个还是蛮神奇的黑盒,anyway苹果据说是把值存到指针里面啦。
复习之前写的autorelease的时候惊讶的发现,当时觉得NSNumber和NSString无法被release其实是因为他们自己把值放到了指针里面了。
//autorelease方法
- (id)autorelease {
return ((id)self)->rootAutorelease();
}
//rootAutorelease 方法
inline id objc_object::rootAutorelease()
{
if (isTaggedPointer()) return (id)this;
//检查是否可以优化
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
//放到auto release pool中。
return rootAutorelease2();
2. Tagged Pointer乱七八糟的想法
上次提到了Tagged Pointer(64bit系统对 NSString、NSNumber 和 NSDate等对象进行优化的一种方式),昨天看之前写的autorelease的时候发现如果是Tagged Pointer是不会autorelease的,这也就解释了问什么NSNumber和NSString是不会被释放的有时候。
- 所以Tagged Pointer到底存在哪里呢?啥时候释放呢?
NSString
类名 | 存储区域 | 初始化的引用计数(retainCount) | 作用描述 |
---|---|---|---|
NSString | 堆区 | 1 | 开发者常用的不可变字符串类,编译期间会转换到其他类型 |
NSMutableString | 堆区 | 1 | 开发者常用的可变字符串类,编译期间会转换到其他类型 |
__NSCFString | 堆区 | 1 | 可变字符串 NSMutableString 类,编译期间会转换到该类型 |
__NSCFConstantString | 堆区 | 2^64-1 | 不可变字符串 NSString 类,编译期间会转换到该类型 |
NSTaggedPointerString | 栈区 | 2^64-1 | Tagged Pointer对象,并不是真的对象 |
在编译期间,已经会决定 NSString -> NSTaggedPointerString。值将存储在指针空间,也就是栈(Stack)区,并且 retainCount 为最大。不过要触发这样的类型转换,需要满足以下两个条件:
64位处理器
内容很少,栈区能够装得下
来尝试一下下面的代码看看各个变量的类型~
NSString *str = @"abc"; // __NSCFConstantString
NSString *str1 = @"abc"; //__NSCFConstantString
NSString *str2 = [NSString stringWithFormat:@"%@", str]; // NSTaggedPointerString
NSString *str3 = [str copy]; // __NSCFConstantString
NSString *str4 = [str mutableCopy]; // __NSCFString

[NSString alloc] initWithString:@"xxx"
这种方式初始化的字符串,xcode对这种方式做了处理,还包括[NSString stringWithString:@"xxx"]
这种方式,这两种初始化字符串都等同于@"xxx"了,和字面量一致,所以推荐字面量直接赋值,这种情况就是__NSCFConstantString。
可以看到和预测的一致,str2是Tagged Pointer的,但是如果将字符串改到栈区容不下,那么他就会被转为__NSCFString之类的。
还有一种是,当String的内容有中文或者特殊字符(非ASCII字符)时,那么就只能存储为__NSCFString指针。

Tagged Pointer是存在栈区的,并且retain count为最大值不会释放。
NSNumber
类名 | 存储区域 | 初始化的引用计数(retainCount) | 作用描述 |
---|---|---|---|
NSValue | 堆区 | 1 | 主要用于封装结构体 |
NSNumber | 堆区 | 1 | 开发者常用的数字类,编译期间会转换到其他类型 |
__NSCFNumber | 堆区、栈区 | 1、2^64-1 | 数字类 NSNumber 类,编译期间会转换到该类型,若是 Tagged Pointer 则在栈区,引用计数为 2^64-1 |
NSNumber就比较单调了看起来都是__NSCFNumber类型,木有tagged类型,虽然实际存储会通过Tagged Pointer方式存小数字,并且遵循栈区+max retain count的原则。

Tagged Pointer的线程安全
@property (nonatomic, copy) NSString *testStr;
{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for(int i=0;i<10000;i++) {
dispatch_async(queue, ^{
self.testStr = [NSString stringWithFormat:@"12666666666666666663"];
});
}
}
这段代码执行的话会出现什么事情呢?testStr是nonatomic的,在多个线程同时访问的时候会出现BAD_ACCESS问题,当然你可以把testStr改成atomic来解决这个问题或者用串行queue。
一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源。比如多个线程访问同一个对象、同一个变量、同一个文件,当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。
但是如果你将12666666666666666663
改成123
,那么你会发现不会crash了,这是为什么呢?
因为当string不长的时候,其实它是一个Tagged Pointer,那么它其实没有堆区对象,当我们赋值的时候就是改下指针(因为指针里存了值),所以其实一次读写就能完成,不用担心线程安全的问题。
无法一次读写完成的例子如:
比如32位的系统中:
一个Bool值占1字节,可以在一次读写操作中完成赋值或取值,因此是线程安全的。
一个指针占4字节,也可以在一次读写操作中完成赋值或取值,因此是线程安全的。
一个double占8字节,则需要两次读写操作才能完成赋值或取值,因此就会存在一个写操作(需两次写操作才能完成),之后就是读操作的可能,导致异常值的现象。
在进行方法调用时,objc_msgSend会进行识别,不会从isa查找cache、父类查找、消息转发的过程了,直接从指针中取出值进行操作。
可以节省内存,使用上也优化了,不需要经历消息发送这类的过程了。
需要注意,如果一个对象是TaggedPointer,就不存在isa指针了,不算真正的OC对象,只是看似对象的普通变量而已。要避免设计对isa的操作。
换成相应的方法调用如 isKindOfClass 和 object_getClass,只要避免在代码中直接访问对象的isa变量,即可避免这个问题。因为现在已经不允许直接用isa啦强制要求object_getClass,而object_getClass应该是对tagged pointer特殊处理了可以正常的返回所以其实木有关系的。。
3. &取址
可参考https://www.jianshu.com/p/1dc7c31fa06f 以及 https://www.jianshu.com/p/00c2c189f403
- int p1表示创建了一个指针变量p1,表示p1是一个指针变量,int表示该指针变量指向的对象的类型
- (*p1)表示寻址,找到p1指向的区域
- &表示取a的内存地址。
- ** 代表指向地址的指针
即使是基础变量也可以用指针指向哦,因为任何变量都有地址~
NSString *test = @"test";
NSLog(@"test:%p, test的地址为:%p", test, &test);
NSString *test2 = test;
NSLog(@"test2:%p, test2的地址为:%p", test2, &test2);
输出:
2019-12-21 09:13:23.423677+0800 Example1[2594:140413] test:0x100605cd0, test的地址为:0x7ffeef60e118
2019-12-21 09:13:23.423802+0800 Example1[2594:140413] test2:0x100605cd0, test2的地址为:0x7ffeef60e110
首先其实我们创建的类型 *变量名
形式的都是指针,什么是指针嘞?就是指向了实际存储对象的地址,类似下面酱紫:

当我们用NSString *test2 = test;
的时候,其实就是把test里面装的地址拷贝了一份也装入了test2,但是这俩地址存储位置是不一样的哦(&test != &test2)。
那为什么OC里面的对象都是直接使用的呢?并没有*变量名
这样取址嘞?
OC里面的对象其实都是结构体哦,对象其实是个指针变量,所有指针都是结构指针,结构指针是指向一种struct类型的指针变量,它是结构体在内存中的首地址。
结构指针说明的一般形式是:
struct (结构类型名称) * (结构指针变量名);
例如:struct date * pdate, today;
说明了两个变量,一个是指向结构date的结构指针pdate,today是一个date结构变量。
语句:
struct date{
int year;
int month;
int day;
};
pdate = &today;
通过结构变量today访问其成员的操作,也可以用等价的指针形式表示:
today.year = 2001; 等价于 (*pdate).year = 2001;
由于运算符""的优先级比运算符"."的优先级低,所以必须有"( )"将pdate括起来。若省去括号,则含义就变成了"*(pdate.year)"。
在C语言中,通过结构指针访问成员可以采用运算符"->"进行操作,对于指向结构的指针,为了访问其成员可以采用下列语句形式:
结构指针->成员名;
这样,上面通过结构指针pdate访问成员year的操作就可以写成:
pdate->year = 2001;
。如果结构指针p指向一个结构数组,那么对指针p的操作就等价于对数组下标的操作。
所以其实我们看到色NSString就类似下面的结构体:
typedef struct Object {
char *string;
} *Object;
当我们调用变量指针的时候内部转换为了结构指针的样子:
NSString *str = @"abc";
NSLog("str的值为:%@",str);
转为:
Object obj = malloc(sizeof(Object));
obj->string = "abc";
NSLog(@"%s", obj->string);
然后我们看下参数传递的时候如何处理指针~
NSString *test = @"test";
NSLog(@"test:%p, test的地址为:%p", test, &test);
[self testArgu:test];
- (void)testArgu:(NSString *)test {
NSLog(@"testArgu");
NSLog(@"test:%p, test的地址为:%p", test, &test);
}
输出:
2019-12-21 13:31:25.501729+0800 Example1[3100:228725] test:0x105393cd0, test的地址为:0x7ffeea880118
2019-12-21 13:31:25.501851+0800 Example1[3100:228725] testArgu
2019-12-21 13:31:25.501934+0800 Example1[3100:228725] test:0x105393cd0, test的地址为:0x7ffeea8800d8
方法内部会自行生成一个指针变量,来指向传入的指针变量指向的对象。(小扩展下:这个局部变量由系统自行管理,存放在栈区)
所以这里传入的参数和函数内的参数的内容是一样,但是参数自身的地址是不一样的。
- 那么如果我们传入的是指向地址的地址呢?即使方法内部copy了这个数据放入了新的地址,我们仍旧不会丢失参数指针的真实地址。
NSString *test = @"test";
NSLog(@"test:%p, test的地址为:%p", test, &test);
[self testArgu:&test];
- (void)testArgu:(NSString **)testPtr {
NSLog(@"testArgu");
NSLog(@"test:%p, test的地址为:%p", *testPtr, testPtr);
}
输出:
2019-12-21 13:43:15.367530+0800 Example1[3131:233734] test:0x108edfcd0, test的地址为:0x7ffee6d34118
2019-12-21 13:43:15.367649+0800 Example1[3131:233734] testArgu
2019-12-21 13:43:15.367746+0800 Example1[3131:233734] test:0x108edfcd0, test的地址为:0x7ffee6d34110
为什么test的地址还是不对呢?我们传入的就是NSString **了吖?其实是因为自动添加的autoreleasing~

编译器优化会默认对传入的对象加一个__autoreleasing修饰符。
__autoreleasing:将其修饰的对象加入autoreleasepool,也就会起到延缓释放的作用。
那么OC编译器为什么要添加这个呢?因为本着“谁创建谁释放”内存管理原则,在方法内部创建的对象(指向传入的&str),在方法结束后,就会自动释放
因此我们就可以推测:编译器的优化不只是在方法的参数前多加一个__autoreleasing,还使用了一个__autoreleasing修饰的临时变量来承接我们想要传入的变量。
类似酱紫:
NSString *test = @"test";
__autoreleasing NSString *tempStr = str;
[self testArgu:&tempStr];
既然是由于修饰符不一致的原因造成的隐式的优化,那么我们可不可以自己创建传入方法的对象时就使用满足要求的__autoreleasing修饰呢?
__autoreleasing NSString *str = @"test";
[self testArgu:&str];
这样就可以避免编译器再使用一个临时变量来转接我们自己的变量了。而且通过验证可以发现,这次的log地址也完全符合预期了。
再想想,可以让我们创建的对象“适应”编译器给方法自动生成的__autoreleasing,那么能不能让方法来“适应”我们创建的对象呢?
-(void) testArgu:(NSString * __strong *)str;
一般对付隐式的方式就是用显式来覆盖他。
酱紫试一下发现输出就符合预期啦:
- (void)testArgu:(NSString * __strong *)testPtr {
NSLog(@"testArgu");
NSLog(@"test:%p, test的地址为:%p", *testPtr, testPtr);
}
输出:
2019-12-21 13:46:08.288290+0800 Example1[3161:235411] test:0x10cedacd0, test的地址为:0x7ffee2d39118
2019-12-21 13:46:08.288413+0800 Example1[3161:235411] testArgu
2019-12-21 13:46:08.288501+0800 Example1[3161:235411] test:0x10cedacd0, test的地址为:0x7ffee2d39118
4. 对象销毁干啥了
runtime源码:https://opensource.apple.com/tarballs/objc4/
可参考:https://www.jianshu.com/p/30ba6d90cd37
// Replaced by NSZombies
- (void)dealloc {
_objc_rootDealloc(self);
}
void
_objc_rootDealloc(id obj)
{
assert(obj);
obj->rootDealloc();
}
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
看rootDealloc
方法卡一看到如果是isTaggedPointer
就return了不走dealloc,fastpath就是大概率object是没有weak引用、关联对象、析构函数等的,如果没有就直接free。
如果有的话就调用object_dispose
,里面调用了objc_destructInstance
,这个里面先判断了是不是有解构函数,再看有木有关联对象,最后再clear一下。
object_cxxDestruct 解构
强推这篇文章哦:http://blog.sunnyxx.com/2014/04/02/objc_dig_arc_dealloc/
MRC的时候需要在对象释放的时候手动释放ivar以及调用父类dealloc:
- (void)dealloc {
self.array = nil;
self.string = nil;
// ... //
// 非Objc对象内存的释放,如CFRelease(...)
// ... //
[super dealloc];
}
ARC只剩下了下面的代码:
- (void)dealloc
{
// ... //
// 非Objc对象内存的释放,如CFRelease(...)
// ... //
}
其实object_cxxDestruct就是帮我们release了实例变量以及调用父类解构函数~
void object_cxxDestruct(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
object_cxxDestructFromClass(obj, obj->ISA());
}
static void object_cxxDestructFromClass(id obj, Class cls)
{
void (*dtor)(id);
// Call cls's dtor first, then superclasses's dtors.
for ( ; cls; cls = cls->superclass) {
if (!cls->hasCxxDtor()) return;
dtor = (void(*)(id))
lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
if (dtor != (void(*)(id))_objc_msgForward_impcache) {
if (PrintCxxCtors) {
_objc_inform("CXX: calling C++ destructors for class %s",
cls->nameForLogging());
}
(*dtor)(obj);
}
}
}
可以看到其实就是从自己开始一直向上找解构函数并执行~ 沿着继承链逐层向上搜寻SEL_cxx_destruct这个selector,找到函数实现(void (*)(id)(函数指针)并执行。
那么SEL_cxx_destruct怎么生成的呢?
ARC actually creates a -.cxx_destruct method to handle freeing instance variables. This method was originally created for calling C++ destructors automatically when an object was destroyed.
When the compiler saw that an object contained C++ objects, it would generate a method called .cxx_destruct. ARC piggybacks on this method and emits the required cleanup code within it.
也就是.cxx_destruct方法原本是为了C++对象析构的,ARC hook这个方法插入代码实现了自动内存释放的工作。
什么时候会有.cxx_destruct方法呢:
- 只有在ARC下这个方法才会出现(试验代码的情况下)
- 只有当前类拥有实例变量时(不论是不是用property)这个方法才会出现,且父类的实例变量不会导致子类拥有这个方法
- 出现这个方法和变量是否被赋值,赋值成什么没有关系
那么cxx_destruct做了什么呢?
static void emitCXXDestructMethod(CodeGenFunction &CGF, ObjCImplementationDecl *impl)
{
CodeGenFunction::RunCleanupsScope scope(CGF);
llvm::Value *self = CGF.LoadObjCSelf();
const ObjCInterfaceDecl *iface = impl->getClassInterface();
for (const ObjCIvarDecl *ivar = iface->all_declared_ivar_begin(); ivar; ivar = ivar->getNextIvar())
{
QualType type = ivar->getType();
// Check whether the ivar is a destructible type.
QualType::DestructionKind dtorKind = type.isDestructedType();
if (!dtorKind) continue;
CodeGenFunction::Destroyer *destroyer = 0;
// Use a call to objc_storeStrong to destroy strong ivars, for the
// general benefit of the tools.
if (dtorKind == QualType::DK_objc_strong_lifetime) {
destroyer = destroyARCStrongWithStore;
// Otherwise use the default for the destruction kind.
} else {
destroyer = CGF.getDestroyer(dtorKind);
}
CleanupKind cleanupKind = CGF.getCleanupKind(dtorKind);
CGF.EHStack.pushCleanup<DestroyIvar>(cleanupKind, self, ivar, destroyer,
cleanupKind & EHCleanup);
}
assert(scope.requiresCleanups() && "nothing to do in .cxx_destruct?");
}
这里其实就是循环遍历了ivar然后执行了objc_storeStrong:
id objc_storeStrong(id *object, id value) {
value = [value retain];
id oldValue = *object;
*object = value;
[oldValue release];
return value;
}
release了实例变量并且置成nil啦~
其实调用super dealloc
也是类似的方式借用Codegen实现的,通过runtime在dealloc的时候插入了给父类dealloc的消息发送。
_object_remove_assocations 关联对象
关联对象其实就是一个key-value的dict,有的时候在实现category的property的时候会用到~
关于关联对象可以参考:https://www.jianshu.com/p/44289b5477f8
它的清理其实就是dealloc的时候自动做的~
void _object_remove_assocations(id object) {
vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
if (associations.size() == 0) return;
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// copy all of the associations that need to be removed.
ObjectAssociationMap *refs = i->second;
for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
elements.push_back(j->second);
}
// remove the secondary table.
delete refs;
associations.erase(i);
}
}
// the calls to releaseValue() happen outside of the lock.
for_each(elements.begin(), elements.end(), ReleaseValue());
}
也就是其实AssociationsManager里面维护了一个AssociationsHashMap,这个map的key是通过调用DISGUISE()函数获取对象地址的反码disguised_object,然后value是一个ObjectAssociationMap。
ObjectAssociationMap就类似我们存入的key-value的container dictionary~
所以销毁的时候先通过对象地址反码找到object的ObjectAssociationMap,然后循环内容执行elements.push_back(j->second);
先把关联key-value对放入elements,然后把ObjectAssociationMap delete掉,最后循环遍历elements ReleaseValue。
- 这里其实不太懂为啥不直接循环遍历的时候就release,需要先放入elements再最后统一release?
clearDeallocating 清weak表
inline void
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}
assert(!sidetable_present());
}
void
objc_object::sidetable_clearDeallocating()
{
SideTable& table = SideTables()[this];
// clear any weak table items
// clear extra retain count and deallocating bit
// (fixme warn or abort if extra retain count == 0 ?)
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
weak_clear_no_lock(&table.weak_table, (id)this);
}
table.refcnts.erase(it);
}
table.unlock();
}
// Slow path of clearDeallocating()
// for objects with nonpointer isa
// that were ever weakly referenced
// or whose retain count ever overflowed to the side table.
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
assert(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
SideTable& table = SideTables()[this];
table.lock();
if (isa.weakly_referenced) {
weak_clear_no_lock(&table.weak_table, (id)this);
}
if (isa.has_sidetable_rc) {
table.refcnts.erase(this);
}
table.unlock();
}
关于里面的sidetable等可以参考:https://www.jianshu.com/p/ef6d9bf8fe59
系统创建了一个全局的SideTables,虽然名字后面有个"s"不过他其实是一个全局的Hash表,里面的内容装的都是SideTable结构体而已。它使用对象的内存地址当它的key。管理引用计数和weak指针就靠它了。
所以上面代码通过SideTables()[this]
来拿到一个SideTable:
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
~SideTable() {
_objc_fatal("Do not delete SideTable.");
}
void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
void forceReset() { slock.forceReset(); }
// Address-ordered lock discipline for a pair of side tables.
template<HaveOld, HaveNew>
static void lockTwo(SideTable *lock1, SideTable *lock2);
template<HaveOld, HaveNew>
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
为了防止多线程问题,SideTable是有一个自旋锁的,所以在操作之前需要lock一下,并且还有一个weak_table。
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
weak_table_t其实就是一个包含entry以及size的结构体,第二个元素num_entries是用来维护保证数组始终有一个合适的size。比如数组中元素的数量超过3/4的时候将数组的大小乘以2。
weak_entry_t的结构:
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
}
- referent
被指对象的地址。前面循环遍历查找的时候就是判断目标地址是否和他相等。 - referrers
可变数组,里面保存着所有指向这个对象的弱引用的地址。当这个对象被释放的时候,referrers里的所有指针都会被设置成nil。 - inline_referrers
只有4个元素的数组,默认情况下用它来存储弱引用的指针。当大于4个的时候使用referrers来存储指针。
void
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
objc_object *referent = (objc_object *)referent_id;
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
/// XXX shouldn't happen, but does with mismatched CF/objc
//printf("XXX no entry for clear deallocating %p\n", referent);
return;
}
// zero out references
weak_referrer_t *referrers;
size_t count;
if (entry->out_of_line()) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}
for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
if (*referrer == referent) {
*referrer = nil;
}
else if (*referrer) {
_objc_inform("__weak variable at %p holds %p instead of %p. "
"This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
referrer, (void*)*referrer, (void*)referent);
objc_weak_error();
}
}
}
weak_entry_remove(weak_table, entry);
}
所以清除引用的时候就循环referrers这个保存了指向了指针的地址表,然后如果这个去看这个地址里面保存的地址和当前object(referent)的内存地址一样,就将*referrer里面的地址清掉,不让他保存着被dealloc的对象的内存地址啦。
总结一下,dealloc的流程是:
- 执行object_cxxDestruct调用析构函数
- 执行_object_remove_assocations删除关联对象
- 执行clearDeallocating清空引用计数表并清除弱引用表,将所有weak引用指nil(这也解释了为什么使用weak能自动置空)

5. 内存泄漏的一些情况
-
对象循环引用
@class ,Strong,weak -
block循环引用
__weak typeof(self) weakself = self; -
NSNotification的观察者忘记移除
[[NSNotificationCenter defaultCenter] removeObserver:self]; -
delegate循环引用问题
@property (nonatomic, weak) id delegate; -
NSTimer循环引用
使用GCD -
非OC对象内存处理
CGImageRef类型变量非OC对象,其需要手动执行释放操作CGImageRelease(ref),否则会造成大量的内存泄漏导致程序崩溃。其他的对于CoreFoundation框架下的某些对象或变量需要手动释放、C语言代码中的malloc等需要对应free等都需要注意 -
使用过多的UIWebView
参考:https://www.jianshu.com/p/9866294b39c6
改为WKWebView -
大次数循环内存暴涨问题
for (int i = 0; i < 100000; i++) {
NSString *string = @“Abc”;
string = [string lowercaseString];
string = [string stringByAppendingString:@“xyz”];
NSLog(@"%@", string);
}
改:
for (int i = 0; i < 100000; i++) {
@autoreleasepool {
NSString *string = @“Abc”;
string = [string lowercaseString];
string = [string stringByAppendingString:@“xyz”];
NSLog(@"%@", string);
}
}
-
加载大图片或者多个图片
[UIImage imageNamed:@""]
,次方法使用了系统缓存来缓存图像,会长时间占用内存,最好使用imageWithContentsOfFile
方法 -
ANF的AFHTTPSessionManager
参考:https://www.jianshu.com/p/922b043b244e -
地图类
6. 内存泄漏检测
可参考:https://cloud.tencent.com/developer/article/1337757
腾讯的那个MLeaksFinder
真的还挺好用的,引入以后会自动在VC pop的时候检测并弹出alert,但是它自带的其实主要是UI的内存检测,比如单例之类的也可能持有应该被回收的东东,这种时候就需要手动开启检测啦。
原理:MLeaksFinder一开始是从UIViewController入手的,UIViewController在POP或dismiss之后该控制器及其上的view,view的subviews都会被释放掉,MleaksFinder就是在控制器POP或dismiss之后去查看该控制器和其上的view是否都被释放掉。
具体的方法是,为基类 NSObject
添加一个方法 willDealloc
方法,该方法的作用是,先用一个弱指针指向 self
,并在一小段时间(3秒)后,通过这个弱指针调用 -assertNotDealloc
,而 -assertNotDealloc
主要作用是直接中断言。这样,当我们认为某个对象应该要被释放了,在释放前调用这个方法,如果3秒后它被释放成功,weakSelf
就指向 nil,不会调用到 -assertNotDealloc
方法,也就不会中断言,如果它没被释放(泄露了),-assertNotDealloc
就会被调用中断言。这样,当一个 UIViewController
被 pop 或 dismiss 时(我们认为它应该要被释放了),我们遍历该 UIViewController 上的所有 view,依次调 -willDealloc
,若3秒后没被释放,就会中断言。
简而言之就是当一个对象3秒之后还没释放,那么指向它的 weak 指针还是存在的,所以可以调用其 runtime 绑定的方法 willDealloc 从而提示内存泄漏。
所以如果你想特意看某个对象是不是释放,可以在它应该被释放的时候调用它的willDealloc
~