iOS底层原理iOS开发你需要知道的夯实基础

小码哥底层原理笔记:内存管理

2020-07-15  本文已影响0人  chilim

iOS程序的内存布局

55B534F2-D918-47A7-86EE-D03A79E39FB6.png

注:只要是static修饰的变量就相当于是全局变量,整个项目就只有一份内存地址

Tagged Point技术

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

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

当使用了Tagged Point之后,NSNumber指针里面存储的数据变成了:Tag + Data,直接把数据存储在指针里面,比如NSNumber里面存储10:number = 0xb000a1最高位是标志位,如果为1表示为Tagged Point指针,如果是MAC平台则最低有效位为1
当指针不够存储数据的时候,才使用动态分配内存的方式来存储数据

objc_msgSend能识别Tagged Point,比如调用NSNumber的intValue方法,直接从指针提取数据,因为Tagged Point并不是一个OC对象,没有类对象,没有isa。所以如果按之前的方法调用流程的话会报找不到方法错误。

面试题:

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    for (int i = 0; i < 1000; i++) {
        dispatch_async(queue, ^{
            // 加锁
            self.name = [NSString stringWithFormat:@"abcdefghijk"];
            // 解锁
        });
    }

以上代码运行会出现闪退。因为给self.name赋值相当于调用setName方法。
在MRC中setName方法:

- (void)setName:(NSString *)name
{
    if (_name != name) {
        [_name release];
        _name = [name retain];
    }
}

有可能会出现多个子线程同时访问[_name release];,从而导致报错。解决办法可以将name设置为atmoic或者在name赋值的前后加锁。保证只有一个线程在赋值

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    for (int i = 0; i < 1000; i++) {
        dispatch_async(queue, ^{
            self.name = [NSString stringWithFormat:@"abc"];
        });
    }

以上代码并不会闪退。因为abc是小数据,根据Tagged Point技术,数据是直接存储在name的地址里面的,并不会动态生成对象存储,也就不会调用set方法了。

引用计数

在iOS中,使用引用计数来管理OC对象的内存
一个新建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁。
当调用alloc、new、copy、mutableCopy方法返回一个对象,在不需要使用时需要进行release或者autorelease。

ARC是LLVM编译的时候帮我们处理内存管理方面的代码,然后在RunTime运行时帮我们处理弱引用相关的操作

引用计数的存储
在64bit中,引用计数可以直接存储在有划过的isa指针中,也可能存储在SideTable类中:

struct SideTable {
        spinlock_t slock;
        RefcountMap refcnts;//引用计数存储,散列表,当前最新的指针为key,value为引用计数
        weak_table_t weak_table;//weak引用存储,散列表
    }

在isa指针中有一个字段19位存储对象的引用计数,如果不够存储则会存储在SideTable表中的refcnts;

weak指针的原理

将weak修饰的对象存放到SideTable里面的weak_table里面
当对象释放自动调用dealloc函数时会调用clearDeallocating()将指向当前对象的弱指针置为nil

自动释放池

autoreleasePool的底层代码如下:

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

接下来我们在使用的时候如下代码:

- (void)test{
    @autoreleasepool {
        NSObject *object = [[NSObject alloc] init];
    }
}

以上代码本质是

- (void)test{
atautoreleasepoolobj = objc_autoreleasePoolPush();
 
    MJPerson *person = [[[MJPerson alloc] init] autorelease];
 
    objc_autoreleasePoolPop(atautoreleasepoolobj);
}

即在autorelease前面调用了objc_autoreleasePoolPush方法,在销毁的时候调用了objc_autoreleasePoolPush方法。在这两个方法中都是通过AutoreleasePoolPage对象来管理的。

class AutoreleasePoolPage 
{
    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
}

每个AutoreleasePoolPage对象占用4096字节,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的内存地址。当我们一个对象调用了autorelease后会将这个对象的地址存放到AutoreleasePoolPage里面。
所有的AutoreleasePoolPage对象是通过双向链表的形式连接在一起。


AutoreleasePoolPage

autoreleasePool调用push方法:

static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) {
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

当我们调用push方法会将一个POOL_BOUNDARY入栈,并且返回其内存地址,然后再把对象的内存地址一次入栈。当调用pop方法的时候会传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY。比如当我们有多个@autoreleasepool 的时候:

int main(int argc, const char * argv[]) {
    @autoreleasepool {//r1 = push()
        MJPerson *p1 = [[MJPerson alloc] init];
        MJPerson *p2 = [[MJPerson alloc] init];
        @autoreleasepool {//r2 = push()
            MJPerson *p3 = [[MJPerson alloc] init];
            
            @autoreleasepool {//r3 = push()
                MJPerson *p4 = [[MJPerson alloc] init];
            }//pop(r3)
        }//pop(r2)
    }//pop(r1)
    return 0;
}

POOL_BOUNDARY相当于一个标志位,将多个autoreleasepool隔开r1、r2、r3分别是对应每个autoreleasepool的POOL_BOUNDARY的内存地址,释放的时候我们调用pop把这个POOL_BOUNDARY的内存地址传进去,然后一次释放直到遇到POOL_BOUNDARY的内存地址。


autoreleasepool释放流程

autorelease在什么时候释放?
如果是直接被autoreleasePool包住的话,那么释放的时机就是在autoreleasePool执行完之后就释放了。比如:object将在执行完@autoreleasepool的时候释放

- (void)test{
    @autoreleasepool {
        NSObject *object = [[NSObject alloc] init];
    }
}

如果不是在autoreleasePool里面的话,那是由runloop控制的,iOS在主线程的RunLoop中注册了2个Observer,第一个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()。第二个Observer监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush(),还监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()。

注意:在ARC中编译时插入的代码是release,而不是autorelease,所以在执行完当前函数的时候局部变量就会立即释放。

上一篇 下一篇

猜你喜欢

热点阅读