【iOS 底层原理】内存管理

一.定时器

1.CADisplayLink、NSTimer

CADisplayLink、NSTimer 会对 target 产生强引用,如果 target 又对它们产生强引用,那么就会引发循环引用。

解决方案
方式一:使用 block 的形式触发定时器


image.png

方式二:使用继承自 NSObject 的中间对象

@interface MJProxy1 : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end

@implementation MJProxy1
+ (instancetype)proxyWithTarget:(id)target {
    MJProxy1 *proxy = [[MJProxy1 alloc] init];
    proxy.target = target;
    return proxy;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
    return self.target;
}

@end

方式三:使用继承自 NSProxy 的中间对象

@interface MJProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
@implementation MJProxy
+ (instancetype)proxyWithTarget:(id)target {
    // NSProxy对象不需要调用init,因为它本来就没有init方法
    MJProxy *proxy = [MJProxy alloc];
    proxy.target = target;
    return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}
@end

使用中间对象解决循环引用的思路:


image.png
NSProxy 与 NSObject

NSProxy 做代理,比 NSObject 的优势

  • NSProxy 不会去父类搜索方法,会直接走消息转发流程,效率更高。
  • NSProxy 没有 - (id)forwardingTargetForSelector:(SEL)aSelector 方法

另外,NSObject 是普通的 OC 对象,而 NSProxy 调用 isKindOfClass 会匹配其代理类型,如下图


image.png

查看 NSProxy 的 isKindOfClass 如下:


image.png

内部也走的消息转发。

2.GCD Timer

NSTimer 依赖于 RunLoop,如果 RunLoop 的任务过于繁重,可能会导致 NSTimer 不准时。而GCD 的定时器会更加准时。

demo


image.png

注:GCD Timer 无需手动释放内存

Timer 封装

static NSMutableDictionary *timers_;
dispatch_semaphore_t semaphore_;
+ (void)initialize
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        timers_ = [NSMutableDictionary dictionary];
        semaphore_ = dispatch_semaphore_create(1);
    });
}

+ (NSString *)execTask:(void (^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
{
    if (!task || start < 0 || (interval <= 0 && repeats)) return nil;
    
    // 队列
    dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
    
    // 创建定时器
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
    // 设置时间
    dispatch_source_set_timer(timer,
                              dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
                              interval * NSEC_PER_SEC, 0);
    
    
    dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
    // 定时器的唯一标识
    NSString *name = [NSString stringWithFormat:@"%zd", timers_.count];
    // 存放到字典中
    timers_[name] = timer;
    dispatch_semaphore_signal(semaphore_);
    
    // 设置回调
    dispatch_source_set_event_handler(timer, ^{
        task();
        
        if (!repeats) { // 不重复的任务
            [self cancelTask:name];
        }
    });
    
    // 启动定时器
    dispatch_resume(timer);
    
    return name;
}

+ (NSString *)execTask:(id)target selector:(SEL)selector start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
{
    if (!target || !selector) return nil;
    
    return [self execTask:^{
        if ([target respondsToSelector:selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            [target performSelector:selector];
#pragma clang diagnostic pop
        }
    } start:start interval:interval repeats:repeats async:async];
}

+ (void)cancelTask:(NSString *)name
{
    if (name.length == 0) return;
    
    dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
    
    dispatch_source_t timer = timers_[name];
    if (timer) {
        dispatch_source_cancel(timer);
        [timers_ removeObjectForKey:name];
    }

    dispatch_semaphore_signal(semaphore_);
}

二.内存布局

1.内存布局

image.png

验证:高地址和低地址

int a = 10;
int b;

int main(int argc, char * argv[]) {
    @autoreleasepool {
        static int c = 20;
        
        static int d;
        
        int e;
        int f = 20;

        NSString *str = @"123";
        
        NSObject *obj = [[NSObject alloc] init];
        
        NSLog(@"\n&a=%p\n&b=%p\n&c=%p\n&d=%p\n&e=%p\n&f=%p\nstr=%p\nobj=%p\n",
              &a, &b, &c, &d, &e, &f, str, obj);
        
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

/*
 字符串常量
 str=0x10dfa0068
 
 已初始化的全局变量、静态变量
 &a =0x10dfa0db8
 &c =0x10dfa0dbc
 
 未初始化的全局变量、静态变量
 &d =0x10dfa0e80
 &b =0x10dfa0e84
 
 堆
 obj=0x608000012210
 
 栈
 &f =0x7ffee1c60fe0
 &e =0x7ffee1c60fe4
 */

三.OC 对象的内存管理

1. Tagged Pointer(小型对象存储技术)

从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储

在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值

使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中

当指针不够存储数据时,才会使用动态分配内存的方式来存储数据

objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销

如何判断一个指针是否为Tagged Pointer?
iOS平台,最高有效位是1(第64bit)
Mac平台,最低有效位是1

image.png
image.png

判断您是否为 Tagged Pointer


image.png

一个问题


image.png

打印两个字符串的内存地址,发现第一个长字符串在堆空间。第二个短字符串,直接将字符串的内容存在指针中,触发了 Tagged Pointer


image.png

打印类型也可以发现是 NSTaggedPointerString 类型


image.png

第一个直接奔溃,多线程异步对 name 赋值,会同时进入到 setter 方法,这时有可能多次调用 release 操作,造成坏内存访问。

第二个字符串"abc" 属于 Tagged Pointer,所以它不是一个 OC 对象,不会调用 setter 方法,会直接提取内存中的字符串值进行赋值。

小技巧:判断一个对象是否是 OC 对象,看二进制的最后一位,如果不是0,则不是 OC 对象。(堆空间地址内存对齐,最后一位一定是0)

2. MRC

在iOS中,使用引用计数来管理OC对象的内存

一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间

调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1

内存管理的经验总结

  • 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
  • 想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1

可以通过以下私有函数来查看自动释放池的情况

  • extern void _objc_autoreleasePoolPrint(void);
setter 方法的内存管理
  • 对象属性的 setter 方法中,要进行一次 retain 或 copy
  • 属性重新赋值时,要对之前的对象进行 release,在对新对象进行 retain
  • 属性重新赋值时,要判断赋值的对象和之前的对象是否一样,不一样时才会 release
  • 基本数据类型不需要进行内存管理

retain setter

- (MJDog *)dog
{
    return _dog;
}
- (void)setDog:(MJDog *)dog
{
    if (_dog != dog) {
        [_dog release];
        _dog = [dog retain];
    }
}

基本数据类型不进行内存管理(assign)

- (void)setAge:(int)age
{
    _age = age;
}

- (int)age
{
    return _age;
}

copy setter

- (void)setData:(NSArray *)data
{
    if (_data != data) {
        [_data release];
        _data = [data copy];
    }
}
dealloc 内存管理
  • dealloc 中对对象属性进行一次 release
  • dealloc 方法中要调用 super
  • MRC 下设置 @property 会自动生成 setter 和 getter,但 dealloc 中仍然需要进行内存管理
  • 保持计数器的平衡,有+1就要有-1
- (void)dealloc
{
    [_dog release];
    _dog = nil;
    // 父类的dealloc放到最后
    [super dealloc];
}
或者
- (void)dealloc
{
    self.dog = nil;
    // 父类的dealloc放到最后
    [super dealloc];
}
使用 autorelease
  • alloc 一个对象就需要进行一次 release,所以使用 autorelease 可以简化代码,让编译器自动去 release
  • 很多对象的类方法可以实现自动调用 autorelease

下面几种方式都等价

NSMutableArray *data = [[NSMutableArray alloc] init];
self.data = data;
[data release];
self.data = [[NSMutableArray alloc] init];
[self.data release];  self.data = [NSMutableArray array];
self.data = [[[NSMutableArray alloc] init] autorelease];
self.data = [NSMutableArray array];

3. copy 和 mutableCopy

拷贝的目的:产生一个副本对象,跟源对象互不影响

  • 修改了源对象,不会影响副本对象
  • 修改了副本对象,不会影响源对象

iOS提供了2个拷贝方法

  1. copy,不可变拷贝,产生不可变副本
  2. mutableCopy,可变拷贝,产生可变副本

深拷贝和浅拷贝

  1. 深拷贝:内容拷贝,产生新的对象
  2. 浅拷贝:指针拷贝,没有产生新的对象
NSString copy
void test2()
{
    NSString *str1 = [[NSString alloc] initWithFormat:@"test"];
    NSString *str2 = [str1 copy]; // 浅拷贝,指针拷贝,没有产生新对象,返回的是NSString
    NSMutableString *str3 = [str1 mutableCopy]; // 深拷贝,内容拷贝,有产生新对象,返回的是NSMutableString
    
    NSLog(@"%@ %@ %@", str1, str2, str3);
    NSLog(@"%p %p %p", str1, str2, str3);
}
image.png
NSMutableString *str1 = [[NSMutableString alloc] initWithFormat:@"test"];
NSString *str2 = [str1 copy]; // 深拷贝
NSMutableString *str3 = [str1 mutableCopy]; // 深拷贝
image.png
copy 和 mutablecopy 的内存管理
image.png
tagged pointer NSString 的内存管理

tagged pointer 的 NSString 不使用引用计数管理,retainCount 的值为 -1.

4. 引用计数的存储

在64bit中,引用计数可以直接存储在优化过的isa指针中

image.png

extra_rc

  • 里面存储的值是引用计数器减1

has_sidetable_rc

  • 引用计数器是否过大无法存储在isa中
  • 如果为1,那么引用计数会存储在一个叫SideTable的类的属性中

也可能存储在SideTable类中.


image.png

refcnts是一个存放着对象引用计数的散列表

retain 源码查看

image.png

SideTable 源码

image.png

5. dealloc 源码(weak 的原理)

当一个对象要释放时,会自动调用dealloc,接下的调用轨迹是

  • dealloc
  • _objc_rootDealloc
  • rootDealloc
  • object_dispose
  • objc_destructInstance、free
image.png
image.png

ARC是LLVM编译器和Runtime系统相互协作的一个结果,在编译器插入内存管理相关的代码,在运行时处理 weak 弱引用。

四.autorelease pool

1.介绍

调用 autorelease 方法的对象,会在 @autorelease {} 大括号结束时,自动调用 release 操作。

@autorelease {} 允许嵌套。

自动释放池的主要底层数据结构是:__AtAutoreleasePool、AutoreleasePoolPage

调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的

2. __AtAutoreleasePool 结构

clang重写@autoreleasepool,重写以下代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MJPerson *person = [[[MJPerson alloc] init] autorelease];
    }
    return 0;
}

得到 c++ 代码

 {
    __AtAutoreleasePool __autoreleasepool;
    MJPerson *person = ((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MJPerson"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
 }

__AtAutoreleasePool 是一个结构体,结构如下

 struct __AtAutoreleasePool {
    __AtAutoreleasePool() { // 构造函数,在创建结构体的时候调用
        atautoreleasepoolobj = objc_autoreleasePoolPush();
    }
 
    ~__AtAutoreleasePool() { // 析构函数,在结构体销毁的时候调用
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
 
    void * atautoreleasepoolobj;
 };

最终的代码相当于

    // __AtAutoreleasePool 构造
    atautoreleasepoolobj = objc_autoreleasePoolPush();
 
    MJPerson *person = [[[MJPerson alloc] init] autorelease];
 
    // __AtAutoreleasePool 析构
    objc_autoreleasePoolPop(atautoreleasepoolobj);

objc_autoreleasePoolPush 方法会将一个 POOL_BOUNDARY 入栈,并且返回其存放的内存地址,对象调用 autorelease 方法,会将改对象的地址入栈,当调用 objc_autoreleasePoolPop 时,会将所有对象进行一次 release,直到 POOL_BOUNDARY 标记处

3.AutoreleasePoolPage的结构

image.png

每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址。

所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起

image.png

调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址

调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY

id *next指向了下一个能存放autorelease对象地址的区域

另外可以通过以下私有函数来查看自动释放池的情况

extern void _objc_autoreleasePoolPrint(void);
image.png

PAGE hot 表示当前的 page,cold 表示已经存满对象的 page

4.autorelease 和 runloop

结论

iOS在主线程的Runloop中注册了2个Observer
第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
第2个Observer
监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
第3个监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()
由此可知,在每次 waiting 时,进行一次 autorelease 对象的清理。

挖掘过程

查看主线程的 runloop observer,注册了响应的 observer 进行 autorelease


image.png
image.png
举例说明

viewDidLoad 中创建的局部变量,会在 viewWillAppear 和 viewDidAppear 之间进行释放,也就是当前的那一次 runloop 循环的休眠之前至今 release

image.png
image.png

问题:方法中的局部变量会方法结束立即释放吗
如果是 autorelease 对象,不会立即释放,否则会立即释放
[[NSObject] alloc] init] // 立即释放
[NSObject objectWithCCC] // 不立即释放(类方法一般生成 autorelease 对象)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,326评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,228评论 1 304
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,979评论 0 252
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,489评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,894评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,900评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,075评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,803评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,565评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,778评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,255评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,582评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,254评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,151评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,952评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,035评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,839评论 2 277

推荐阅读更多精彩内容