美文网首页ios-内存管理
iOS 内存管理-引用计数

iOS 内存管理-引用计数

作者: coder_my | 来源:发表于2019-10-11 22:44 被阅读0次

    参考:
    Objective-C高级编程 iOS与OS X多线程和内存管理

    程序的内存分配:
    1、栈区(stack)—由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。
    2、堆区(heap)—一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。
    3、全局区(静态区)(static)—全局变量和静态变量的存储是放在一块的,初始化的
    全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域; 程序结束后由系统释放。
    4、文字常量区,常量字符串就是放在这里的。程序结束后由系统释放。
    5、程序代码区—存放函数体的二进制代码。

    Objective-C使用的内存管理方式为引用计数。

    ARC是iOS 5推出的新功能,全称叫 ARC(Automatic Reference Counting)。简单地说,就是代码中自动加入了retain/release,原先需要手动添加的用来处理内存管理的引用计数的代码可以自动地由编译器完成了。

    内存管理的思考方式

    • 自己生成的对象自己持有,
    • 非自己生成的对象,自己也能持有
    • 不再需要自己持有的对象时释放
    • 非自己持有的对象无法释放
    对象操作与Objective-C方法的对应.jpg
    • 在OC对象存有引用计数这一整数值。
    • 在调用alloc或者retain后,引用计数值加一。
    • 调用release后,引用计数值减一。
    • 当引用计数值为1后,继续调用release,系统调用dealloc方法废弃对象。

    ARC自动管理引用计数

    ARC介绍
    ARC其实也是基于引用计数,只是编译器在编译时期自动在已有代码中插入合适的内存管理代码(包括 retain、release、copy、autorelease、autoreleasepool)以及在 Runtime 做一些优化。

    现在的iOS开发基本都是基于ARC的,所以开发人员大部分情况都是不需要考虑内存管理的,因为编译器已经帮你做了。为什么说是大部分呢,因为底层的 Core Foundation 对象由于不在 ARC 的管理下,所以需要自己维护这些对象的引用计数。

    还有就算循环引起情况就算由于互相之间强引用,引用计数永远不会减到0,所以需要自己主动断开循环引用,使引用计数能够减少。

    所有权修饰符

    Objective-C编程中为了处理对象,可将变量类型定义为id类型或各种对象类型。 ARC中id类型和对象类其类型必须附加所有权修饰符。

    其中有以下4种所有权修饰符:

    __strong
    __weak
    __unsafe_unretaied
    __autoreleasing

    所有权修饰符和属性的修饰符对应关系如下所示:

    assign 对应的所有权类型是 __unsafe_unretained
    copy 对应的所有权类型是 __strong
    retain 对应的所有权类型是 __strong
    strong 对应的所有权类型是 __strong
    unsafe_unretained对应的所有权类型是__unsafe_unretained
    weak 对应的所有权类型是 __weak

    __strong
    __strong 表示强引用,对应定义 property 时用到的 strong。当对象没有任何一个强引用指向它时,它才会被释放。如果在声明引用时不加修饰符,那么引用将默认是强引用。当需要释放强引用指向的对象时,需要保证所有指向对象强引用置为 nil。__strong 修饰符是 id 类型和对象类型默认的所有权修饰符。

    原理:

    {
        id __strong obj = [[NSObject alloc] init];
    }
    
    //编译器的模拟代码
    id obj = objc_msgSend(NSObject,@selector(alloc));
    objc_msgSend(obj,@selector(init));
    
    // 出作用域的时候调用
    objc_release(obj);
    

    虽然ARC有效时不能使用release方法,但由此可知编译器自动插入了release。

    对象是通过除alloc、new、copy、multyCopy外方法产生的情况

    {
        id __strong obj = [NSMutableArray array];
    }
    
    //编译器的模拟代码
    id obj = objc_msgSend(NSMutableArray,@selector(array));
    objc_retainAutoreleasedReturnValue(obj);
    objc_release(obj);
    

    objc_retainAutoreleasedReturnValue函数主要用于优化程序的运行。它是用于持有(retain)对象的函数,它持有的对象应为返回注册在autoreleasePool中对象的方法,或是函数的返回值。像该源码这样,在调用array类方法之后,由编译器插入该函数。

    而这种objc_retainAutoreleasedReturnValue函数是成对存在的,与之对应的函数是objc_autoreleaseReturnValue。它用于array类方法返回对象的实现上。下面看看NSMutableArray类的array方法通过编译器进行了怎样的转换:

    + (id)array
    {
        return [[NSMutableArray alloc] init];
    }
    
    //编译器模拟代码
    + (id)array
    {
        id obj = objc_msgSend(NSMutableArray,@selector(alloc));
        objc_msgSend(obj,@selector(init));
        
        // 代替我们调用了autorelease方法
        return objc_autoreleaseReturnValue(obj);
    }
    

    我们可以看见调用了objc_autoreleaseReturnValue函数且这个函数会返回注册到自动释放池的对象,但是,这个函数有个特点,它会查看调用方的命令执行列表,如果发现接下来会调用objc_retainAutoreleasedReturnValue则不会将返回的对象注册到autoreleasePool中而仅仅返回一个对象。达到了一种最优效果。如下图:


    省略autoreleasepool注册.jpg

    __weak

    __weak 表示弱引用,对应定义 property 时用到的 weak。弱引用不会影响对象的释放,而当对象被释放时,所有指向它的弱引用都会自定被置为 nil,这样可以防止野指针。使用__weak修饰的变量,即是使用注册到autoreleasePool中的对象。__weak 最常见的一个作用就是用来避免循环循环。需要注意的是,__weak 修饰符只能用于 iOS5 以上的版本,在 iOS4 及更低的版本中使用 __unsafe_unretained 修饰符来代替。

    __weak 的几个使用场景:

    在 Delegate 关系中防止循环引用。
    在 Block 中防止循环引用。
    用来修饰指向由 Interface Builder 创建的控件。比如:@property (weak, nonatomic) IBOutlet UIButton *testButton;。
    原理:

    {
        id __weak obj = [[NSObject alloc] init];
    }
    

    编译器转换后的代码如下:

        id obj;
        id tmp = objc_msgSend(NSObject,@selector(alloc));
        objc_msgSend(tmp,@selector(init));
        objc_initweak(&obj,tmp);
        objc_release(tmp);
        objc_destroyWeak(&object);
    

    对于__weak内存管理也借助了类似于引用计数表的散列表,它通过对象的内存地址做为key,而对应的__weak修饰符变量的地址作为value注册到weak表中,在上述代码中objc_initweak就是完成这部分操作,而objc_destroyWeak
    则是销毁该对象对应的value。当指向的对象被销毁时,会通过其内存地址,去weak表中查找对应的__weak修饰符变量,将其从weak表中删除。所以,weak在修饰只是让weak表增加了记录没有引起引用计数表的变化。

    对象通过objc_release释放对象内存的动作如下:

    • objc_release
    • 因为引用计数为0所以执行dealloc
    • _objc_rootDealloc
    • objc_dispose
    • objc_destructInstance
    • objc_clear_deallocating

    而在对象被废弃时最后调用了objc_clear_deallocating,该函数的动作如下:

    • 从weak表中获取已废弃对象内存地址对应的所有记录
    • 将已废弃对象内存地址对应的记录中所有以weak修饰的变量都置为nil
    • 从weak表删除已废弃对象内存地址对应的记录
    • 根据已废弃对象内存地址从引用计数表中找到对应记录删除
    • 据此可以解释为什么对象被销毁时对应的weak指针变量全部都置为nil,同时,也看出来销毁weak步骤较多,如果大量使用weak的话会增加CPU的负荷。

    还需要确认一点是:使用__weak修饰符的变量,即是使用注册到autoreleasePool中的对象。

    {
            id __weak obj1 = obj; 
            NSLog(@"obj2-%@",obj1);
        }
    

    编译器转换上述代码如下:

        id obj1; 
        objc_initweak(&obj1,obj);
        id tmp = objc_loadWeakRetained(&obj1);
        objc_autorelease(tmp);
        NSLog(@"%@",tmp);
        objc_destroyWeak(&obj1);
    

    objc_loadWeakRetained函数获取附有__weak修饰符变量所引用的对象并retain, objc_autorelease函数将对象放入autoreleasePool中,据此当我们访问weak修饰指针指向的对象时,实际上是访问注册到自动释放池的对象。因此,如果大量使用weak的话,在我们去访问weak修饰的对象时,会有大量对象注册到自动释放池,这会影响程序的性能。

    解决方案:要访问weak修饰的变量时,先将其赋给一个strong变量,然后进行访问

    为什么访问weak修饰的对象就会访问注册到自动释放池的对象呢?

    因为weak不会引起对象的引用计数器变化,因此,该对象在运行过程中很有可能会被释放。所以,需要将对象注册到自动释放池中并在autoreleasePool销毁时释放对象占用的内存。

    __unsafe_unretained

    ARC 是在 iOS5 引入的,而 __unsafe_unretained 这个修饰符主要是为了在ARC刚发布时兼容iOS4以及版本更低的系统,因为这些版本没有弱引用机制。这个修饰符在定义property时对应的是unsafe_unretained。__unsafe_unretained 修饰的指针纯粹只是指向对象,没有任何额外的操作,不会去持有对象使得对象的 retainCount +1。而在指向的对象被释放时依然原原本本地指向原来的对象地址,不会被自动置为 nil,所以成为了野指针,非常不安全。

    __unsafe_unretained的应用场景:

    在 ARC 环境下但是要兼容 iOS4.x 的版本,用__unsafe_unretained 替代 __weak 解决强循环循环的问题。

    __autoreleasing

    将对象赋值给附有__autoreleasing修饰符的变量等同于MRC时调用对象的autorelease方法。

    @autoeleasepool {
            // 如果看了上面__strong的原理,就知道实际上对象已经注册到自动释放池里面了 
            id __autoreleasing obj = [[NSObject alloc] init];
        }
    

    编译器转换上述代码如下:

        id pool = objc_autoreleasePoolPush(); 
        id obj = objc_msgSend(NSObject,@selector(alloc));
        objc_msgSend(obj,@selector(init));
        objc_autorelease(obj);
        objc_autoreleasePoolPop(pool);
    
    @autoreleasepool {
            id __autoreleasing obj = [NSMutableArray array];
        }
    

    编译器转换上述代码如下:

        id pool = objc_autoreleasePoolPush();
        id obj = objc_msgSend(NSMutableArray,@selector(array));
        objc_retainAutoreleasedReturnValue(obj);
        objc_autorelease(obj);
        objc_autoreleasePoolPop(pool);
    

    上面两种方式,虽然第二种持有对象的方法从alloc方法变为了objc_retainAutoreleasedReturnValue函数,都是通过objc_autorelease,注册到autoreleasePool中。

    autorelease苹果实现

    GNUstep实现

    - (id)autorelease 
    {
         [NSAutoreleasePool addObject:self];
    }
    

    objc4实现

    + (id)autorelease {
        return (id)self;
    }
    
    // Replaced by ObjectAlloc
    - (id)autorelease {
        return ((id)self)->rootAutorelease();
    }
    
    // Base autorelease implementation, ignoring overrides.
    inline id 
    objc_object::rootAutorelease()
    {
        if (isTaggedPointer()) return (id)this;
        if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
    
        return rootAutorelease2();
    }
    

    循环引用

    什么是循环引用?循环引用就是在两个对象互相之间强引用了,引用计数都加1了,我们前面说过,只有当引用计数减为0时对象才释放。但是这两个的引用计数都依赖于对方,所以也就导致了永远无法释放。

    最容易产生循环引用的两种情况就是Delegate和Block。所以我们就引入了弱引用这种概念,即弱引用虽然持有对象,但是并不增加引用计数,这样就避免了循环引用的产生。也就是我们上面所说的所有权修饰符__weak的作用。关于原理在__weak部分也有描述,简单的描述就是每一个拥有弱引用的对象都有一张表来保存弱引用的指针地址,但是这个弱引用并不会使对象引用计数加1,所以当这个对象的引用计数变为0时,系统就通过这张表,找到所有的弱引用指针把它们都置成nil。

    所以在ARC中做内存管理主要就是发现这些内存泄漏,关于内存泄漏Instrument为我们提供了 Allocations/Leaks 这样的工具用来检测。但是个人觉得还是很麻烦的,大部分时候内存泄漏并不会引起应用的崩溃或者报错之类的,所以我们也不会每次主动的去查看当前代码有没有内存泄漏之类的。

    这里有一个微信读书团队开源的工具MLeaksFinder,它可以在你程序运行期间,如果有内存泄漏就会弹出提示告诉你泄漏的地方。

    具体原理如下:

    我们知道,当一个 UIViewController 被 pop 或 dismiss 后,该 UIViewController 包括它的 view,view 的 subviews 等等将很快被释放(除非你把它设计成单例,或者持有它的强引用,但一般很少这样做)。于是,我们只需在一个 ViewController 被 pop 或 dismiss 一小段时间后,看看该 UIViewController,它的 view,view 的 subviews 等等是否还存在。

    具体的方法是,为基类 NSObject 添加一个方法 -willDealloc 方法,该方法的作用是,先用一个弱指针指向 self,并在一小段时间(3秒)后,通过这个弱指针调用 -assertNotDealloc,而 -assertNotDealloc 主要作用是直接中断言。

    - (BOOL)willDealloc {
        __weak id weakSelf = self;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [weakSelf assertNotDealloc];
        });
        return YES;
    }
    - (void)assertNotDealloc {
         NSAssert(NO, @“”);
    }
    

    这样,当我们认为某个对象应该要被释放了,在释放前调用这个方法,如果3秒后它被释放成功,weakSelf 就指向 nil,不会调用到 -assertNotDealloc 方法,也就不会中断言,如果它没被释放(泄露了),-assertNotDealloc 就会被调用中断言。这样,当一个 UIViewController 被 pop 或 dismiss 时(我们认为它应该要被释放了),我们遍历该 UIViewController 上的所有 view,依次调 -willDealloc,若3秒后没被释放,就会中断言。

    Core Foundation 对象的内存管理
    底层的 Core Foundation 对象,在创建时大多以 XxxCreateWithXxx 这样的方式创建,例如:

    // 创建一个 CFStringRef 对象
    CFStringRef str= CFStringCreateWithCString(kCFAllocatorDefault, “hello world", kCFStringEncodingUTF8);
    
    // 创建一个 CTFontRef 对象
    CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
    

    对于这些对象的引用计数的修改,要相应的使用 CFRetain 和 CFRelease 方法。如下所示:

    // 创建一个 CTFontRef 对象
    CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
    
    // 引用计数加 1
    CFRetain(fontRef);
    
    // 引用计数减 1
    CFRelease(fontRef);
    

    对于 CFRetain 和 CFRelease 两个方法,读者可以直观地认为,这与 Objective-C 对象的 retain 和 release 方法等价。

    所以对于底层 Core Foundation 对象,我们只需要延续以前手工管理引用计数的办法即可。

    除此之外,还有另外一个问题需要解决。在 ARC 下,我们有时需要将一个 Core Foundation 对象转换成一个 Objective-C 对象,这个时候我们需要告诉编译器,转换过程中的引用计数需要做如何的调整。这就引入了bridge相关的关键字,以下是这些关键字的说明:

    • __bridge: 只做类型转换,不修改相关对象的引用计数,原来的 Core Foundation 对象在不用时,需要调用 CFRelease 方法。

    • __bridge_retained:类型转换后,将相关对象的引用计数加 1,原来的 Core Foundation 对象在不用时,需要调用 CFRelease 方法。

    • __bridge_transfer:类型转换后,将该对象的引用计数交给 ARC 管理,Core Foundation 对象在不用时,不再需要调用 CFRelease 方法。

    相关文章

      网友评论

        本文标题:iOS 内存管理-引用计数

        本文链接:https://www.haomeiwen.com/subject/nzzqyctx.html