iOS-内存管理-理论实践1
2020/6/29 23:27:25
本文主要是介绍iOS-内存管理-理论实践1,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
前言
iOS开发中,内存管理是不可避免的。鉴于当下MRC已经远去多时,本篇学习笔记主要针对ARC下的内存管理进行实践。
内存理论篇:
- iOS-内存管理-理论篇
内存理论实践篇:
- iOS-内存管理-理论实践1(本文)
- iOS-内存管理-理论实践2(待完成)
实践哪些对象需要我们进行内存管理呢?
- 项目中所有实例对象原则上都需要进行内存管;继承了 NSObject 的对象,可以通过ARC由系统进行管理;非NSObject对象,需要自己进行对应内存管理。
- 而其他非对象类型,如 int(NSInteger) 、 char 、 float(CGFloat) 、 double 、 struct 、 enum 等,不需要进行内存管理。
原因:
- 一般继承了 NSObject 的对象,存储在操作系统到 堆 里边。(PS:并非所有都是这样,创建的字符串有时候根据创建方式、位置,也会存储到 常量区)
- 操作系统的 堆:一般由程序员分配释放,若程序员不释放,结束时可能由系统回收,分配方式类似数据结构的链表。
- 操作系统的 栈:由操作系统自动分配释放内存,存放函数的参数值、局部变量值等。其操作方式类似数据结构中的 栈(先进后出)。
示例:
int main(int argc, const char *argv []) { @autoreleasepool { int a = 10; // 栈 int b = 20; // 栈 // p: 栈 // Person 对象(计数器 == 1):堆 Person *p = [[Person alloc] init]; } // 经过上面代码后,栈里的变量 a、b、p 都会被回收 // 但是堆里的 Person 对象仍会留在内存中,因为它的计数器依然是 1 return 0; } 复制代码
项目中需要关注的内存管理知识点,也是容易造成内存泄露的地方
- block内存管理
- weak等防止循环引用的内存管理
- autorelease内存管理
- 非OC对象,例如CF框架,C/C++语言混编等内存管理(待续)
- 计时器、通知等内存管理(待续)
- 属性的内存管理(待续)
一、block内存管理
1、block内存类型
block内存分为三种类型:
- 1、_NSConcreteGlobalBlock(全局)
当我们声明一个block时,如果这个block没有捕获外部的变量,那么这个block就位于全局区,此时对_NSConcreteGlobalBlock的retain、copy、release操作都无效。ARC和MRC环境下都是如此。
示例:声明并定义一个全局区block
void (^myBlock) (int x); myBlock = ^(int number) { int result = number + 100; NSLog(@"result: %d",result); }; myBlock(10); 复制代码
- 2、_NSConcreteStackBlock(栈)
栈区block我们平时编程基本不会遇到!因为在ARC环境下,当我们声明并且定义了一个block,并且没有为Block添加额外的修饰符(默认是__strong修饰符),如果该Block捕获了外部的变量,实质上是有一个从_NSConcreteStackBlock转变到_NSConcreteMallocBlock的过程,只不过是系统帮我们完成了copy操作,将栈区的block迁移到堆区,延长了Block的生命周期。对于栈区block而言,栈block在当函数退出的时候,该空间就会被回收。
那什么时候在ARC的环境下出现_NSConcreteStackBlock呢?如果我们在声明一个block的时候,使用了__weak或者__unsafe__unretained的修饰符,那么系统就不会为我们做copy的操作,不会将其迁移到堆区。下面我们实验一下:
__weak void (^myBlock1) (int n) = ^(int num) { int result = num + n; NSLog(@"result: %d",result); }; myBlock1(12); NSLog(@"myBlock1: %@",myBlock1); //打印结果 "myBlock1:<__NSStackBlock__:0x7ffff50726c0>" //结论:被__weak修饰的myBlock1捕获了外部变量n,成为一个栈区的block 复制代码void (^myBlock1) (int n) = ^(int num) { int result = num + n; NSLog(@"result: %d",result); }; myBlock1(12); NSLog(@"默认myBlock1: %@",myBlock1); //打印结果 "默认myBlock1:<__NSMallocBlock__:0x604000259020>" //结论:不使用__weak修饰,在默认修饰符环境下,捕获了外部变量的block位于堆区 复制代码我们可以手动地去执行copy方法,验证系统为我们做的隐式转换:
__weak void (^myBlock1) (int n) = ^(int num) { int result = num + n; NSLog(@"result: %d",result); }; myBlock1(12); NSLog(@"手动copy myBlock1: %@",[myBlock1 copy]); //打印结果 "手动copy myBlock1:<__NSMallocBlock__:0x60000025c020>" //结论:手动执行copy方法之后,block被迁移到了堆区 复制代码
- 3、_NSConcreteMallocBlock(堆)
在MRC环境下,我们需要手动调用copy方法才可以将block迁移到堆区,而在ARC环境下,__strong修饰的(默认)block只要捕获了外部变量就会位于堆区,NSMallocBlock支持retain、release,会对其引用计数+1或 -1。
2、block实际运用
- 1、当使用局部变量时,需要添加__block
__blockintnum =100; self.tBlock= ^(int n) { num = num + n; NSLog(@"%d",num); }; self.tBlock(100); 复制代码
- 2、当使用全局变量时,需要时使用__weak typeof(self)weakSelf = self修饰,否则会造成循环引用
__weak typeof(self)weakSelf = self; weakSelf.qBlock= ^(NSString*str) { NSLog(@"%@",weakSelf.nameStr); }; 复制代码
- 3、 这样循环问题是解决了,但是又会导致一个新的问题,假如在block有一个耗时操作,在这个过程self被销毁了,而weakself也会随着self的销毁而销毁,block又要对weakself进行某些操作,这是拿到的weakself就是nil了。(原因请参考iOS-内存管理-理论篇 __weak -内存理论)
__weak typeof(self)weakSelf = self; weakSelf.qBlock= ^(NSString*str) { _strong__typeof(self) strongSelf = weakSelf; NSLog(@"%@",strongSelf.nameStr); }; self.tBlock(100); 复制代码
block 小结:
block作为属性,使用copy修饰时(strong修饰符不会改变block内存类型),因此使用copy或strong修饰都可以。block中使用weak一般是为了防止循环引用,为了避免重复,在这里就不过多介绍weak的使用。
项目当中使用block尽量不要嵌套,如果实在嵌套也请控制在一层。不然很容易造成内存泄露或是地狱回调。特别是如果是用block进行数据传递,多层嵌套的block很容易造成数据缺失,app崩溃,而且项目复杂以后很难排查。
二、weak等防止循环引用的内存管理
1、weak的实现原理
Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针,对于 weak 对象会放入一个 hash 表中,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。 当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。
注:由于可能多个weak指针指向同一个对象,所以value为一个数组
weak 的实现原理可以概括以下三步:
- 1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。 示例代码:
{ id __weak obj1 = obj; } 复制代码当我们初始化一个weak变量时,runtime会调用objc_initWeak函数。这个函数在Clang中的声明如下:
id objc_initWeak(id *object, id value); 复制代码其具体实现如下:
id objc_initWeak(id *object, id value) { *object = 0; return objc_storeWeak(object, value); } 复制代码示例代码轮换成编译器的模拟代码如下:
id obj1; objc_initWeak(&obj1, obj); 复制代码因此,这里所做的事是先将obj1初始化为0(nil),然后将obj1的地址及obj作为参数传递给objc_storeWeak函数。objc_initWeak函数有一个前提条件:就是object必须是一个没有被注册为__weak对象的有效指针。而value则可以是null,也可以指向一个有效的对象。
- 2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数。
objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
- 3、释放时,调用clearDeallocating函数。
clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
2、释放时机
在dealloc的时候,会将weak属性的值会自动设置为nil
三、autorelease内存管理
1、autoreleasePool什么时候创建的,里面的对象又是什么时候释放的?
- 1、系统通过runloop创建的autoreleasePool
runloop 可以说是iOS 系统的灵魂。内存管理/UI 刷新/触摸事件这些功能都需要 runloop 去管理和实现。runloop是通过线程创建的,和线程保持一对一的关系,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。
runloop和autoreleasePool又是什么关系呢?对象又是什么时候释放的?
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
- 2、手动autoreleasePool
我们可以通过@autoreleasepool {}方式手动创建autoreleasepool对象,那么这个对象什么时候释放呢?答案是除了autoreleasepool的大括号就释放了。
- 3、子线程的autoreleasepool对象的管理?
线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。所以在我们创建子线程的时候,如果没有获取runloop,那么也就没用通过runloop来创建autoreleasepool,那么我们的autorelease对象是怎么管理的,会不会存在内存泄漏呢?答案是否定的,当子线程有autoreleasepool的时候,autorelease对象通过其来管理,如果没有autoreleasepool,会通过调用 autoreleaseNoPage 方法,将对象添加到 AutoreleasePoolPage 的栈中,也就是说你不进行手动的内存管理,也不会内存泄漏啦!这部分我们可以看下runtime中NSObject.mm的部分,有相关代码。
static inline id *autoreleaseFast(id obj) { AutoreleasePoolPage *page = hotPage(); if (page && !page->full()) { return page->add(obj); } else if (page) { return autoreleaseFullPage(obj, page); } else { //调用 autoreleaseNoPage 方法管理autorelease对象。 return autoreleaseNoPage(obj); } } 复制代码
2、autorelease在实际当中的应用?
- 1、使用autorelease有什么好处呢?
- 不在关心对象的释放时间
- 不在关心什么时候调用 release
- 2、autorelease 的创建方法
- 使用 NSAutoreleasePool 来创建:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 创建自动释放池 [pool release]; // [pool drain]; 销毁自动释放池 复制代码
- 使用 @autoreleasepool 创建
@autoreleasepool { //开始代表创建自动释放池 } //结束代表销毁自动释放池 复制代码
- 3、autorelease 的使用方法
- NSAutoreleasePool 用法:
NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init]; Person *p = [[[Person alloc] init] autorelease]; [autoreleasePool drain]; 复制代码
- @autoreleasepool 用法:
@autoreleasepool { // 创建一个自动释放池 Person *p = [[Person new] autorelease]; // 将代码写到这里就放入了自动释放池 } // 销毁自动释放池(会给池子中所有对象发送一条release消息) 复制代码
- 4、autorelease 的注意事项
- 并不是放到自动释放池代码中,就会自动加入自动释放池
错误案例1 @autoreleasepool { // 因为没有调用 autorelease 方法,所以对象没有加入到自动释放池 Person *p = [[Person alloc] init]; [p run]; } 复制代码
- 在自动释放池的外部调用 autorelease 不会被加入到自动释放池中。autorelease 是一个方法,只有在自动释放池中调用才有效
错误案例2 @autoreleasepool { } // 没有与之对应的自动释放池, 只有在自动释放池中调用autorelease才会放到释放池 Person *p = [[[Person alloc] init] autorelease]; [p run]; // 正确案例1 @autoreleasepool { Person *p = [[[Person alloc] init] autorelease]; } // 正确案例2 Person *p = [[Person alloc] init]; @autoreleasepool { [p autorelease]; } 复制代码
- 5、autorelease 经典错误案例实际当中容易犯的错误
自动释放池内不宜放占用内存比较大的对象
- 尽量避免对大内存使用该方法,对这种延迟释放机制,还是尽量少用。
- 不要把大量循环操作放到一个 autoreleasepool 之间,这样会造成内存峰值的上升
// 内存暴涨 @autoreleasepool { for (int i = 0; i < 99999; ++i) { //如果Person对象内存占用大这种写法在少量循环中就会造成严重内存泄露 Person *p = [[[Person alloc] init] autorelease]; } } // 内存不会暴涨 for (int i = 0; i < 99999; ++i) { @autoreleasepool { Person *p = [[[Person alloc] init] autorelease]; } } 复制代码
这篇关于iOS-内存管理-理论实践1的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-10-05Swift语法学习--基于协议进行网络请求
- 2022-08-17Apple开发_Swift语言地标注释
- 2022-07-24Swift 初见
- 2022-05-22SwiftUI App 支持多语种 All In One
- 2022-05-10SwiftUI 组件参数简写 All In One
- 2022-04-14SwiftUI 学习笔记
- 2022-02-23Swift 文件夹和文件操作
- 2022-02-17Swift中使用KVO
- 2022-02-08Swift 汇编 String array
- 2022-01-30SwiftUI3.0页面反向传值