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

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,所以在执行完当前函数的时候局部变量就会立即释放。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,001评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,210评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,874评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,001评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,022评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,005评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,929评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,742评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,193评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,427评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,583评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,305评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,911评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,564评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,731评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,581评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,478评论 2 352

推荐阅读更多精彩内容