iOS优美的内存管理

前言

相关代码工程Demo

内存管理这篇文章主要会从内存布局,内存管理方案,ARC&MRC ,引用计数,弱引用,以及自动释放池这几个方面来进行详细的介绍;

内存布局,五大区

程序加载到内存中会被分开为几个区:内存地址从高到低分别是:内核区

  • 内核区:系统内核处理大小一般为1G 
  • stack(栈区):存储函数, 方法,常量等, 地址从高到低;(0x7,大概地址)
  • heap(堆区):通过alloc,New创建的对象和blockcopy都放在这里, 地址从低到高;( 0x6 大概地址)
  • bss:未初始化的全局变量,静态变量等;一般为:0x1开头
  • data:初始化的全局变量,静态变量;
  • text:程序中的代码段加载到内存中放在这里;

这些内存布局中,很多人会说stack 区访问速度比heap区速度要快,这是什么原因呢?

如图所示,我们在工程中创建一个对象objc ,我们可以看出po objc 可以看到对象是存储在堆区,但是objc 地址是在栈区,也就是说,我们一般在访问对象的时候是由寄存区-->访问栈区-->然后找到对象地址--->再访问堆区,才能获取到对象, 所以一般情况,栈区比堆区访问速度要快一些。

       面试题:全局变量和局部变量在内存中是否有区别?如果有,是什么区别?

答:有区别。全局变量保存在内存的全局存储区中,占用静态的存储单元局部变量保存在栈中,只有在所在函数被调用时动态地为变量分配存储单元;

       Block 是否可以直接修改全局变量 ?

//全局变量
static NSString *xzname = @"XZ";

 [UIView animateWithDuration:1 animations:^{
        xzname = @"XZ";
    }];
    
 NSLog(@"xzname = %@",xzname);

答:可以改变全局变量,因为全局变量作用域比较大,作用在全局;

看下面代码中定义的变量分布的区域:

@interface ViewController ()

@end

@implementation ViewController

int clA;
int clB = 10;

static int bssA;
static NSString *bssStr1;

static int bssB = 10;
static NSString *bssStr2 = @"Alan";
static NSString *xzname = @"XZ";

- (void)viewDidLoad {
    [super viewDidLoad];

    [self testStack];
    [self testHeap];
    [self testConst];

    // Do any additional setup after loading the view.
}
- (void)testStack{
    NSLog(@"************栈区************");
    // 栈区
    int a = 10;
    int b = 20;
    NSObject *object = [NSObject new];
    NSLog(@"a == \t%p",&a);
    NSLog(@"b == \t%p",&b);
    NSLog(@"object == \t%p",&object);
    NSLog(@"%lu",sizeof(&object));
    NSLog(@"%lu",sizeof(a));
}

- (void)testHeap{
    NSLog(@"************堆区************");
    // 堆区
    NSObject *object1 = [NSObject new];
    NSObject *object2 = [NSObject new];
    NSObject *object3 = [NSObject new];
    NSObject *object4 = [NSObject new];
    NSObject *object5 = [NSObject new];
    NSObject *object6 = [NSObject new];
    NSObject *object7 = [NSObject new];
    NSObject *object8 = [NSObject new];
    NSObject *object9 = [NSObject new];
    NSLog(@"object1 = %@",object1);
    NSLog(@"object2 = %@",object2);
    NSLog(@"object3 = %@",object3);
    NSLog(@"object4 = %@",object4);
    NSLog(@"object5 = %@",object5);
    NSLog(@"object6 = %@",object6);
    NSLog(@"object7 = %@",object7);
    NSLog(@"object8 = %@",object8);
    NSLog(@"object9 = %@",object9);
    // 访问---通过对象->堆区地址->存在栈区的指针
}

- (void)testConst{
    
    // 面试:全局变量和局部变量在内存中是否有区别?如果有,是什么区别?
   
    
//     面试:Block 是否可以直接修改全局变量 ?
//    [UIView animateWithDuration:1 animations:^{
//        xzname = @"XZ";
//    }];
//
//    NSLog(@"xzname = %@",xzname);
    
    
    NSLog(@"************静态区************");
    NSLog(@"clA == \t%p",&clA);
    NSLog(@"bssA == \t%p",&bssA);
    NSLog(@"bssStr1 == \t%p",&bssStr1);
    
    NSLog(@"************常量区************");
    NSLog(@"clB == \t%p",&clB);
    NSLog(@"bssB == \t%p",&bssB);
    NSLog(@"bssStr2 == \t%p",&bssStr2);
    
}

看下打印结果:

下面我们来看一道常见的静态区安全测试面试题:

看下面代码,说出执行结果:

在XZPerson中代码为:声明了一个全局静态变量,并在run 和eat方法中打印self,personNum地址和值,都进行++ 操作

#import <Foundation/Foundation.h>

static int personNum = 100;

NS_ASSUME_NONNULL_BEGIN

@interface XZPerson : NSObject
- (void)run;
+ (void)eat;

@end

NS_ASSUME_NONNULL_END
#import "XZPerson.h"

@implementation XZPerson
- (void)run{
    personNum ++;
    NSLog(@"XZPerson内部:%@-%p--%d",self,&personNum,personNum);
}

+ (void)eat{
    personNum ++;
    NSLog(@"XZPerson内部:%@-%p--%d",self,&personNum,personNum);
}

- (NSString *)description{
    return @"";
}
@end

XZPerson 分类中代码为:定义了一个方法,打印self,PersoNum 值和地址

#import "XZPerson.h"

NS_ASSUME_NONNULL_BEGIN

@interface XZPerson (XZ)
- (void)cate_method;

@end

NS_ASSUME_NONNULL_END
#import "XZPerson+XZ.h"



@implementation XZPerson (XZ)
- (void)cate_method{
    NSLog(@"XZPerson内部:%@-%p--%d",self,&personNum,personNum);
}

@end

然后在ViewController内写一个测试代码;

//personNum 默认值为100
- (void)testConstSafety
{
     NSLog(@"************静态区安全测试************");
     NSLog(@"vc:%p--%d",&personNum,personNum); 
     personNum = 10000;
     NSLog(@"vc:%p--%d",&personNum,personNum); 
     [[XZPerson new] run]; 
     NSLog(@"vc:%p--%d",&personNum,personNum); 
     [XZPerson eat]; 
     NSLog(@"vc:%p--%d",&personNum,personNum); 
    
     [[XZPerson alloc] cate_method];

}

看到博文的兄弟们,可以自己先考虑一下这个的输出结果,接下来我们来看一下输出结果:

可以看出在ViewController控制器中的personNum同一个地址,而且变化是相同的,XZPerson类中是同一个地址XZPerson分类中又是另一个地址,这些地址和别的类中地址都不相同这些值都不跟随别的文件中的值变化而变化,唯一一个就是这个值在没有赋值的情况下,默认值都是100;

这就说明:静态全局变量是全局可以使用,但是每个文件如果使用都会开辟出一个单独的空间进行赋值改变,而且别的文件中的值都不会改变,只针对文件有效。

内存管理方案

isa ,在之前的文章中有讲过.

TaggedPointer,我们先来看一到面试题:请问下面代码在运行中,点击屏幕会不会有问题?

- (void)viewDidLoad {
    [super viewDidLoad];
    [self taggedPointerDemo];

}
//MARK: - taggedPointer 面试题
- (void)taggedPointerDemo {
    
    self.queue = dispatch_queue_create("com.cooci.cn", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"alan"];
            NSLog(@"%@",self.nameStr);
        });
    }
    
 }
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    NSLog(@"来了");
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"内存管理--内存管理方案"];
            NSLog(@"%@",self.nameStr);
        });
    }

}

查看下运行结果:

运行程序可以看到确实崩溃了,而且是崩溃到touchBegan ,多次尝试,都崩溃到这个地方,我们就需要分析了:

   多线程+ setter getter 就会导致崩溃;

打开Debug->DebugWorkflow->Always Show Disassembly, 查看堆栈

可以看出是在objc_release 方法导致崩溃的

我们可以看一下setter 方法的源码:

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

可以看到核心就是: retian newvalue 和 realase oldvalue,

这个时候如果加入多线程后,就会导致可能会多次release 导致野指针,这就说明,多线程状态下,set方法和get方法是不安全的;

但是为什么不会崩溃到上面呢,我们来看一下这两个字符串:

可以看出在赋值为alan 的时候字符串类型为NSTaggedPointerString  类型 赋值为@"内存管理--内存管理方案" 时字符串类型为NSCFString 类型

我们来看一下release 源码:

void 
objc_release(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    return obj->release();
}

可以看出来,当对象是NSTaggedPointerString 类型时,不进行release ,这就解释了,为什么不会在上面崩溃了;

经过测试当英文字符为11个以内的时候字符串都会是NSTaggedPointerString 类型中文字符都不是这个类型

深入探索TaggedPointer

通常我们创建对象,对象存储在堆中,对象的指针存储在栈中,如果我们要找到这个对象,就需要先在栈中,找到指针地址,然后根据指针地址找到在堆中的对象。

这个过程比较繁琐,当存储的对象只是一个很小的东西,比如一个字符串,一个数字。去走这么一个繁琐的过程,无非是耗费性能的,所以苹果就搞出了TaggedPointer这么一个东西。

  1. TaggedPointer是苹果为了解决32位CPU到64位CPU的转变带来的内存占用和效率问题,针对NSNumberNSDate以及部分NSString的内存优化方案。

  2. TaggedPointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中也不需要malloc和free

  3. TaggedPointer指针中包含了当前对象的地址、类型、具体数值。因此TaggedPointer指针在内存读取上有着更高的效率,创建时比普通需要malloc跟free的类型快;

- (void)taggentPointer
{
    // 优化 - 编译读取的时候 更直接
    // retian release
    // 3倍
    // 创建 100
    // [8-10]
    
    NSString *str1 = [NSString stringWithFormat:@"a"];
    NSString *str2 = [NSString stringWithFormat:@"b"];
    
    NSLog(@"%@--%p-%@",object_getClass(str1),str1,str1);
    NSLog(@"%@--%p-%@",object_getClass(str2),str2,str2);
   
    NSNumber *number1 = @1;
    NSNumber *number2 = @1;
    NSNumber *number3 = @2.0;
    NSNumber *number4 = @3.2;
    NSLog(@"%@--%p---%@",object_getClass(number1),number1,number1);
    NSLog(@"%@--%p---%@",object_getClass(number2),number2,number2);
    NSLog(@"%@--%p---%@",object_getClass(number3),number3,number3);
    NSLog(@"%@--%p---%@",object_getClass(number4),number4,number4);
}

运行结果为:



看下这个的初始化:

static void
initializeTaggedPointerObfuscator(void)
{
    if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
        // Set the obfuscator to zero for apps linked against older SDKs,
        // in case they're relying on the tagged pointer representation.
        DisableTaggedPointerObfuscation) {
        objc_debug_taggedpointer_obfuscator = 0;
    } else {
        // Pull random data into the variable, then shift away all non-payload bits.
        arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                       sizeof(objc_debug_taggedpointer_obfuscator));
        objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
    }
}

可以看到在iOS 10_14h后会有一个objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;这个操作,一起看一下objc_debug_taggedpointer_obfuscator这个操作具体是干了什么?

extern uintptr_t objc_debug_taggedpointer_obfuscator;

static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}

可以看出对地址进行了一次异或操作,可以看出,在编码和解码过程中都是进行了异或objc_debug_taggedpointer_obfuscator这个值,这个应该都知道,一个数异或同一个数两次,结果还是那个数,常见的面试题,a,和b值进行变换,不加入第三方参数方式:

   a ^ a ^ b = b;
   a ^ b ^ a = b;
   b ^ a ^ a = b;

这种操作就可以将a,和b的值进行变换;

我们来对地址进行解码操作:

//因为外部没有这个值,所以我们将这个加入extern外部引用
extern uintptr_t objc_debug_taggedpointer_obfuscator;
//添加方法
uintptr_t
_objc_decodeTaggedPointer_(id ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
- (void)taggentPointer
{
    // 优化 - 编译读取的时候 更直接
    // retian release
    // 3倍
    // 创建 100
    // [8-10]
    
    
    NSString *str1 = [NSString stringWithFormat:@"a"];
    NSString *str2 = [NSString stringWithFormat:@"ab"];
    NSString *str3 = [NSString stringWithFormat:@"abc"];
    NSString *str4 = [NSString stringWithFormat:@"abcd"];
    NSString *str5 = [NSString stringWithFormat:@"abcde"];
    NSString *str6 = [NSString stringWithFormat:@"abcdef"];

    NSLog(@"%@--%p-%@--0x%lx",object_getClass(str1),str1,str1,_objc_decodeTaggedPointer_(str1));
    NSLog(@"%@--%p-%@--0x%lx",object_getClass(str2),str2,str2,_objc_decodeTaggedPointer_(str2));
    NSLog(@"%@--%p-%@--0x%lx",object_getClass(str3),str3,str3,_objc_decodeTaggedPointer_(str3));
    NSLog(@"%@--%p-%@--0x%lx",object_getClass(str4),str4,str4,_objc_decodeTaggedPointer_(str4));
    NSLog(@"%@--%p-%@--0x%lx",object_getClass(str5),str5,str5,_objc_decodeTaggedPointer_(str5));
    NSLog(@"%@--%p-%@--0x%lx",object_getClass(str6),str6,str6,_objc_decodeTaggedPointer_(str6));
  
    NSNumber *number1 = @1;
    NSNumber *number2 = @1;
    NSNumber *number3 = @2.0;
    NSNumber *number4 = @3.2;
   
    NSLog(@"%@-%p-%@ - 0x%lx",object_getClass(number1),number1,number1,_objc_decodeTaggedPointer_(number1));
    NSLog(@"%@-%p-%@ - 0x%lx",object_getClass(number2),number2,number2,_objc_decodeTaggedPointer_(number2));
    NSLog(@"%@-%p-%@ - 0x%lx",object_getClass(number3),number3,number3,_objc_decodeTaggedPointer_(number3));
    NSLog(@"%@-%p-%@ - 0x%lx",object_getClass(number4),number4,number4,_objc_decodeTaggedPointer_(number4));   
}

看一下运行结果:


首先我们来看一下字符串str1的地址

0xa000000000000611    其中 首位的a 表示为字符串   61  标示ASCII 中的a,最后面的1 标示字符串有几位

看一下number1 的地址:

0xb000000000000012    其中b标示是一个数值int 类型,  后面的  1 表示值, 最后面的2  标示是int 型;

根据测试可以得出number 类型最后一位

0表示char类型,1表示short类型,2表示整形,3表示长整型,4表示单精度类型,5表示双精度类型.

具体情况我们来看一下苹果大大对tagged point的介绍吧


/***********************************************************************
* Tagged pointer objects.
*
* Tagged pointer objects store the class and the object value in the 
* object pointer; the "pointer" does not actually point to anything.
* 
* Tagged pointer objects currently use this representation:
* (LSB)
*  1 bit   set if tagged, clear if ordinary object pointer
*  3 bits  tag index
* 60 bits  payload
* (MSB)
* The tag index defines the object's class. 
* The payload format is defined by the object's class.
*
* If the tag index is 0b111, the tagged pointer object uses an 
* "extended" representation, allowing more classes but with smaller payloads:
* (LSB)
*  1 bit   set if tagged, clear if ordinary object pointer
*  3 bits  0b111
*  8 bits  extended tag index
* 52 bits  payload
* (MSB)
*
* Some architectures reverse the MSB and LSB in these representations.
*
* This representation is subject to change. Representation-agnostic SPI is:
* objc-internal.h for class implementers.
* objc-gdb.h for debuggers.
**********************************************************************/

从apple给出的声明中,可以得到:

(1) 标签指针对象存储了类信息和对象实际的值,此时的指针不指向任何东西;

(2) 使用最低位作为标记位,如果是标签指针对象就标记为1,如果是普通对象类型就标记为0;

(3) 紧接着三位是标签索引位;

(4) 剩余的60位位有效的负载位,标签索引位定义了标签对象代表的对象的真实类型,负载的格式由实际的类定义;

(5) 如果标签位是0b111,表示该对象使用了是被扩展的标签对象,这种扩展的方式可以运训更多的类使用标签对象来表示,同时负载的有效位数变小。这时:

      5.1 最低位是标记位;

      5.2 紧接着三位位0b111;

      5.3 紧接着八位位扩展的标记位;

      5.4 剩余的52位才是真正的有效的负载位。

(6) 并不是所有的架构中都使用低位做标记位.在指令集框架中,除了64-bit的Mac操作系统之外,其余全是使用MSB。比如iOS就是使用高位作为标志位,而在接下来的讨论中,仅以iOS使用高位作为标记位的实现策略来讨论标签指针。

 在iOS中,使用最高位作为标签指针标志位,接下来的三位来作为类标记位,且当这三位为111的时候标签指针就变成了扩展的标签指针,所以普通的标签指针最多能表示7(000-110)种对象类型,那这些对象类型都是那些呢?

enum
#endif
{
    // 60-bit payloads
    OBJC_TAG_NSAtom            = 0, 
    OBJC_TAG_1                 = 1, 
    OBJC_TAG_NSString          = 2, 
    OBJC_TAG_NSNumber          = 3, 
    OBJC_TAG_NSIndexPath       = 4, 
    OBJC_TAG_NSManagedObjectID = 5, 
    OBJC_TAG_NSDate            = 6,

    // 60-bit reserved
    OBJC_TAG_RESERVED_7        = 7, 

    // 52-bit payloads
    OBJC_TAG_Photos_1          = 8,
    OBJC_TAG_Photos_2          = 9,
    OBJC_TAG_Photos_3          = 10,
    OBJC_TAG_Photos_4          = 11,
    OBJC_TAG_XPC_1             = 12,
    OBJC_TAG_XPC_2             = 13,
    OBJC_TAG_XPC_3             = 14,
    OBJC_TAG_XPC_4             = 15,

    OBJC_TAG_First60BitPayload = 0, 
    OBJC_TAG_Last60BitPayload  = 6, 
    OBJC_TAG_First52BitPayload = 8, 
    OBJC_TAG_Last52BitPayload  = 263, 

    OBJC_TAG_RESERVED_264      = 264
};

从上边的定义中,我们可以看到最开头的七种枚举就是最原始的标签指针对象所能代表的六个类原型。而我们比较常见的就是2(NSString), 3(NSNumber), 4(NSIndexPath)和6(NSDate)这四种类型;

因为TaggedPointer 有一部分代码并没有开源,所以不能在进行更深入的探索

MRC&ARC

ARC 为系统自动管理,但是我们还是需要了解MRC 中的一些东西的,MRC 中主要就是alloc  retain ,release , retainCount  autorelease  ,dealloc 主要就是这几个方面;

alloc

 我之前有一篇文章讲述了alloc 的流程iOS底层探索二(OC 中 alloc 方法 初探),有兴趣的可以去看看,这里需要注意的是,在alloc的过程中并没有对retainCount 进行处理

retain

我们直接看retain 源码

id 
objc_retain(id obj)
{
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->retain();
}

判读是不是TaggedPointer类型,是直接retain,前面文章也说了是TaggedPointer类型就不尽兴retain处理,然后直接进入retain方法;

inline id 
objc_object::retain()
{
    assert(!isTaggedPointer());

    if (fastpath(!ISA()->hasCustomRR())) {
        return rootRetain();
    }

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}
ALWAYS_INLINE id 
objc_object::rootRetain()
{
    return rootRetain(false, false);
}

判断是不是快速,一般情况都是快速并调用rootRetain 方法:

ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
//    容错处理
    if (isTaggedPointer()) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

//    处理引用计数
//    ISA 中有讲iretaincount 是存在ISA 中
    do {
        transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
//        散列表引用计数进行处理 这里对非nonpointer isa进行处理
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
/*
 散列表,散列表中是存储了多张表的,
 为了性能和安全所以会有锁,
 散列表内实际是哈希结构
 */
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();
        }
        // don't check newisa.fast_rr; we already called any RR overrides
//        判断是否正在进行析构m,是的话就不处理了
        if (slowpath(tryRetain && newisa.deallocating)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            return nil;
        }
        uintptr_t carry;
/*
 pointer isa 呢 处理extra_rc引用计数RC_ONE这个值模拟器和真机不一样
 uintptr_t extra_rc          : 8   模拟器8位
 uintptr_t extra_rc          : 7   真机7位
 extra_rc  当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc。
 */
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
//     如果超值了,就进行散列表中添加
        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
//            一半在散列表,一半在isa中的extra_rc
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

    if (slowpath(transcribeToSideTable)) {
        // Copy the other half of the retain counts to the side table.
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    return (id)this;
}

代码中加了详细的注释,这里大概总结一下retain:

    1. nopointer  会对 isa 进行引用计数表处理

    2. 然后对散列表处理,先lock  然后对引用计数表和弱引用表处理

    3. 非nopointer的isa 会对isa中的extraRC 进行处理addc

        3.1 如果 extraRC满了(carry)  将满数量的一半存入extraRC  (优先操作ISA ,因为快)

        3.2 另一半存入 引用计数表中 (操作这个表会慢一点)

release

先看下源码:

void 
objc_release(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    return obj->release();
}

inline void
objc_object::release()
{
    assert(!isTaggedPointer());

    if (fastpath(!ISA()->hasCustomRR())) {
        rootRelease();
        return;
    }

    ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_release);
}
ALWAYS_INLINE bool 
objc_object::rootRelease()
{
    return rootRelease(true, false);
}

可以看出前面和retain基本一样;

ALWAYS_INLINE bool 
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
    if (isTaggedPointer()) return false;

    bool sideTableLocked = false;

    isa_t oldisa;
    isa_t newisa;

 retry:
    do {
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
//nonpointer  -->散列表进行处理
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) sidetable_unlock();
            return sidetable_release(performDealloc);
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        uintptr_t carry;
//        进行减操作
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
//        如果isa 中的extra_rc减完了进行散列表操作
        if (slowpath(carry)) {
            // don't ClearExclusive()
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                             oldisa.bits, newisa.bits)));

    if (slowpath(sideTableLocked)) sidetable_unlock();
    return false;

 underflow:
    // newisa.extra_rc-- underflowed: borrow from side table or deallocate

    // abandon newisa to undo the decrement
    newisa = oldisa;
//  对三列表进行操作
    if (slowpath(newisa.has_sidetable_rc)) {
        if (!handleUnderflow) {
            ClearExclusive(&isa.bits);
            return rootRelease_underflow(performDealloc);
        }

        // Transfer retain count from side table to inline storage.

        if (!sideTableLocked) {
            ClearExclusive(&isa.bits);
            sidetable_lock();
            sideTableLocked = true;
            // Need to start over to avoid a race against 
            // the nonpointer -> raw pointer transition.
            goto retry;
        }

        // Try to remove some retain counts from the side table.        
        size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

        // To avoid races, has_sidetable_rc must remain set 
        // even if the side table count is now zero.

        if (borrowed > 0) {
            // Side table retain count decreased.
            // Try to add them to the inline count.
            newisa.extra_rc = borrowed - 1;  // redo the original decrement too
            bool stored = StoreReleaseExclusive(&isa.bits, 
                                                oldisa.bits, newisa.bits);
            if (!stored) {
                // Inline update failed. 
                // Try it again right now. This prevents livelock on LL/SC 
                // architectures where the side table access itself may have 
                // dropped the reservation.
                isa_t oldisa2 = LoadExclusive(&isa.bits);
                isa_t newisa2 = oldisa2;
                if (newisa2.nonpointer) {
                    uintptr_t overflow;
                    newisa2.bits = 
                        addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
                    if (!overflow) {
                        stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits, 
                                                       newisa2.bits);
                    }
                }
            }

            if (!stored) {
                // Inline update failed.
                // Put the retains back in the side table.
                sidetable_addExtraRC_nolock(borrowed);
                goto retry;
            }

            // Decrement successful after borrowing from side table.
            // This decrement cannot be the deallocating decrement - the side 
            // table lock and has_sidetable_rc bit ensure that if everyone 
            // else tried to -release while we worked, the last one would block.
            sidetable_unlock();
            return false;
        }
        else {
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }

    // Really deallocate.

    if (slowpath(newisa.deallocating)) {
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        return overrelease_error();
        // does not actually return
    }
    newisa.deallocating = true;
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;

    if (slowpath(sideTableLocked)) sidetable_unlock();

    __sync_synchronize();
    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return true;
}

可以看出release 和 retain操作方式基本一样,这里在进行引用计数变化的时候,优先会对isa 中的进行减操作,减完了之后才会查询散列表中有没有,

还有就是在release 最后会使用objc_msgSend 调用delloc 方法

retainCount :ios 中的对象引用计数

首先我们来看一下下面代码:

 NSObject *objc = [NSObject alloc];
 NSLog(@"objc------%ld",CFGetRetainCount((__bridge CFTypeRef)objc));

 运行结果的retaincount 是什么呢?


这就很奇怪了,对于alloc 的过程我们是很熟悉的:alloc的流程iOS底层探索二(OC 中 alloc 方法 初探),在这里面并没有对于retainCount 进行处理,但是这里为什么输出结果是1呢?

我们就需要看一下CFGetRetainCount 的源码了

uintptr_t
_objc_rootRetainCount(id obj)
{
    assert(obj);

    return obj->rootRetainCount();
}
inline uintptr_t 
objc_object::rootRetainCount()
{
//    容错处理
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    if (bits.nonpointer) {
//bits.extra_rc;  isa中存储retainCount
        uintptr_t rc = 1 + bits.extra_rc;
//        查看是否有散列表,有需要加上散列表中的值
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}

可以看到,因为我们知道 alloc 后isa中没有进行retainCount 处理所以 rc = 1+ bits.extra_rc (这个值为0)所以结果为0;所以默认给了一个值为1,

可以看到运行程序可以看到这个值确实为 空;

delloc

根据上面release 源码,我们可看出,在最后会调用delloc 方法,来看下delloc 源码:

void
_objc_rootDealloc(id obj)
{
    ASSERT(obj);

    obj->rootDealloc();
}
inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?
 //判断是否是nonpointer isa 直接释放
    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}
id 
object_dispose(id obj)
{
    if (!obj) return nil;
//   释放这个对象的关联对象和weak 处理,CXX 处理,散列表清空等
    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

这个底层还有对散列表,weak表,以及引用计数表等进行清空,下面是delloc的流程图

  总结

这篇文章主要讲述了内存管理的一部分知识,内存的分区,内存布局,内存管理方案,ARC&MRC ,引用计数下篇文章在堆弱引用,和自动释放池来进行详细介绍。

欢迎大家点赞,关注我的CSDN,我会定期做一些技术分享!未完待续。。。 

原创文章 88 获赞 21 访问量 2万+

猜你喜欢

转载自blog.csdn.net/ZhaiAlan/article/details/105071068
今日推荐