阅读 118

iOS-内存管理(二、理论实践1)

前言

iOS开发中,内存管理是不可避免的。鉴于当下MRC已经远去多时,本篇学习笔记主要针对ARC下的内存管理进行实践。

内存理论篇:

内存理论实践篇:

实践哪些对象需要我们进行内存管理呢?

  • 项目中所有实例对象原则上都需要进行内存管;继承了 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内存管理

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-内存管理(三、理论实践2)