内存管理
内存管理是 iOS 一个经典的话题,每当面试时,你是否只会谈论 weak、assign、strong 关键字?只会说简单的内存泄漏(NSTimer、Block、delegate等引起的内存泄漏)?如果答案是 YES,那么这篇文章也许能帮到您。
本文会借鉴一些工具或者源代码,这里提前列举一下:
- Runtime。用于分析 Objective-C 对象在 C 语言层面的实现。版本选取了比较老的 706,而不是最新的,是因为相对来说更容易理解
对象地址
iOS 中 创建一个对象代码就会使用到内存,举例:
NSObject *obj = [[NSObject alloc] init];
分配内存的代码在 runtime 中可以看到:
/***********************************************************************
* class_createInstance
* fixme
* Locking: none
**********************************************************************/
static __attribute__((always_inline))
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
if (!cls) return nil;
assert(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) {
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
else {
if (zone) {
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (!obj) return nil;
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}
return obj;
}
对象地址、指针内存分布
要了解对象的内存分配,我们只需要了解一下方法 calloc
。其实现在 glibc 中,可以看到,其实就是 malloc 外面包一层皮。熟悉 C 语言开发的小伙伴对 malloc
应该不陌生了,他的原理可以在这篇文章中找到:Malloc tutorial。
因此 Objective-C 中创建的对象位于堆中,且在内存空间中不是连续的。
扩展阅读:您可以在https://www.geeksforgeeks.org/stack-vs-heap-memory-allocation/了解堆内存和栈内存的区别。
属性(成员变量)内存分布
属性/成员变量在内存中的位置是通过“内存平移”来获取的,我们可以通过如下方式来确认:
假设我们有个类 Kyson,其代码如下:
@interface Kyson : NSObject
@property (nonatomic, copy) NSString *sex;
@property (nonatomic) NSInteger age;
@property (nonatomic) NSInteger height;
@property (nonatomic, copy) NSString *tel;
@property (nonatomic, copy) NSString *IDNumber;
-(void) doSomething;
@end
@implementation Kyson
-(void) doSomething {
NSLog(@"%s==%@",__func__);
}
@end
我们使用 clang 的 rewrite-objc
方法来看一下,类 Kyson 在 C 语言层面的实现:
typedef struct objc_object Kyson;
struct NSObject_IMPL {
Class isa;
};
struct Kyson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString * _Nonnull _sex;
NSInteger _age;
NSInteger _height;
NSString * _Nonnull _tel;
NSString * _Nonnull _IDNumber;
};
extern "C" unsigned long int OBJC_IVAR_$_Kyson$_age __attribute__ \
((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct Kyson, _age);
static NSInteger _I_Kyson_age(Kyson * self, SEL _cmd) {
return (*(NSInteger *)((char *)self + OBJC_IVAR_$_Kyson$_age));
}
从以上代码可知:
- Objective-C 中的对象本质是结构体
- 结构体的第一个属性是 isa 属性
age
属性是通过内存 + offset的方式实现的(所谓的 内存平移)
测试时间(小试牛刀)
面试题
上面讲了这么多原理,下面来点干货,用于验证读者是否真正懂了对象、类等在内存的分布。
测试代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
Kyson *cls = [Kyson class];
void *ks = &cls;
Kyson *kyson = [[Kyson alloc] init];
[kyson doSomething];
[(__bridge id) ks doSomething];
}
请问,打印的内容是什么?
答案
-[Kyson doSomething]
-[Kyson doSomething]
相信代码 [(__bridge id) ks doSomething];
也能打印出 -[Kyson doSomething]
是令读者疑惑的地方。
解析
我们仍然用 clang
的 rewrite
命令看一下函数的实现逻辑,函数本质是在类的方法列表里的,而类又是通过对象指向 isa 来获取的,也就是指针平移了 8 个字节。
如此一来,这就很好理解了:对象 kyson
的 doSomething
方法,本质是对象 kyson
在内存中偏移 8 个字节,也就是对象 kyson 对应的类 Kyson
中的方法。
如果大家还不能理解,不妨和笔者一样,画一下图就一目了然了:
函数在内存中的分布
函数本身是在栈区的,这点可以通过打断点看出:
可以看到从上到下依次调用函数,这也就是我们所谓的函数“堆栈”的真正含义。说到函数在内存中的分布情况,我们顺便了解一下整个 app 内存分布情况。
测试时间(深入研究)
面试题
这次测试我们只在上次的代码基础上稍微改动一下,代码如下:
@interface Kyson : NSObject
@property (nonatomic, copy) NSString *sex;
@property (nonatomic) NSInteger age;
@property (nonatomic) NSInteger height;
@property (nonatomic, copy) NSString *tel;
@property (nonatomic, copy) NSString *IDNumber;
-(void) doSomething;
@end
@implementation Kyson
-(void) doSomething {
NSLog(@"%s==%@",__func__,self.sex);
}
@end
可以发现,改动的一行是 doSomething 函数体。
测试代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
Kyson *cls = [Kyson class];
void *ks = &cls;
Kyson *kyson = [[Kyson alloc] init];
kyson.age = 13;
kyson.tel = @"15265659598";
kyson.height = 177;
kyson.sex = @"male";
[kyson doSomething];
[(__bridge id) ks doSomething];
}
我们再次猜测一下, 命令行中输出的内容:
答案
笔者的 Xcode 输出如下:
-[Kyson doSomething]==male
-[Kyson doSomething]==<ViewController: 0x1040066f0>
相信大家会有一些疑惑,对象 kyson 调用 doSomething 方法输出 -[Kyson doSomething]==male
大家能理解,但 ks 调用 doSomething 方法输出 [Kyson doSomething]==<ViewController: 0x1040066f0>
就无法理解了。
答案分析
答案还是之前的知识点:内存平移。
ks 地址 = kyson 地址 + 8,
ks.sex = ks 地址 + male 的 offset(8) = ks 地址 + 8,也就是 viewcontroller 的地址了。
笔者这里再次展示一下函数、函数中的临时变量、堆、堆中的 kyson 对象以及 Kyson 类在内存中的位置:
由于 sex 在属性中排名第一,因此 ks.sex 就是 kyson 指针往后 offset 8 即可。
参考
Handling low memory conditions in iOS and Mavericks
深入理解iOS Jetsam机制,助力提升Flotsam召回率