内存管理与NSRunLoop 笔记

一、内存布局

五大区

接下来我从内存中的低地址往高地址依次介绍五大区:

1.代码段(.text)

存放着程序代码,直接加载到内存中

2.初始化区域(.data)

存放着初始化的全局变量、静态变量

内存地址:一般以0x1开头

3.未初始化区域(.bss)

bss段存放着未初始化的全局变量、静态变量

内存地址:一般以0x1开头

4.堆区(heap)

堆区存放着通过alloc分配的对象、block copy后的对象

堆区速度比较慢

内存地址:一般以0x6开头

5.栈区(stack)

栈区存储着函数、方法以及局部变量

栈区比较小,但是速度比较快

内存地址:一般以0x7开头/

堆区访问对象的顺序是先拿到栈区的指针,再拿到指针指向的对象,才能获取到对象的isa、属性方法等

栈区访问对象的顺序是直接通过寄存器访问到对象的内存空间,因此访问速度快

二、内存管理方案

taggedPointer

0xa、0xb主要是用于判断是否是小对象taggedpointer

0xa转换成二进制为 1 010(64位为1,63~61后三位表示tagType类型-2),表示NSString类型

0xb转换为二进制为1 011(64位为1,63~61后三位表示tagType类型-3),表示NSNumber类型,这里需要注意一点,如果NSNumber的值是-1,其地址中的值是用补码表示的

{

    // 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_NSColor          =16,

    OBJC_TAG_UIColor          =17,

    OBJC_TAG_CGColor          =18,

    OBJC_TAG_NSIndexSet        =19,

    OBJC_TAG_NSMethodSignature =20,

    OBJC_TAG_UTTypeRecord      =21,

    // When using the split tagged pointer representation

    // (OBJC_SPLIT_TAGGED_POINTERS), this is the first tag where

    // the tag and payload are unobfuscated. All tags from here to

    // OBJC_TAG_Last52BitPayload are unobfuscated. The shared cache

    // builder is able to construct these as long as the low bit is

    // not set (i.e. even-numbered tags).

    OBJC_TAG_FirstUnobfuscatedSplitTag =136,// 128 + 8, first ext tag with high bit set

    OBJC_TAG_Constant_CFString =136,

    OBJC_TAG_First60BitPayload =0,

    OBJC_TAG_Last60BitPayload  =6,

    OBJC_TAG_First52BitPayload =8,

    OBJC_TAG_Last52BitPayload  =263,

    OBJC_TAG_RESERVED_264      =264

};

NSString的内存管理主要分为3种

__NSCFConstantString:字符串常量,是一种编译时常量,retainCount值很大,对其操作,不会引起引用计数变化,存储在字符串常量区

__NSCFString:是在运行时创建的NSString子类,创建后引用计数会加1,存储在堆上

NSTaggedPointerString:标签指针,是苹果在64位环境下对NSString、NSNumber等对象做的优化.对于NSString对象来说

当字符串是由数字、英文字母组合且长度小于等于9时,会自动成为NSTaggedPointerString类型,存储在常量区

当有中文或者其他特殊符号时,会直接成为__NSCFString类型,存储在堆区

taggedPointer总结

Tagged Pointer小对象类型(用于存储NSNumber、NSDate、小NSString),小对象指针不再是简单的地址,而是地址 + 值,即真正的值,所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已.所以可以直接进行读取.优点是占用空间小,节省内存

Tagged Pointer小对象,不会进入 retain 和 release,而是直接返回了,意味着不需要ARC进行管理,所以可以直接被系统自主的释放和回收

Tagged Pointer的内存并不存储在堆中,而是在常量区中,也不需要malloc和free,所以可以直接读取,相比存储在堆区的数据读取,效率上快了3倍左右.创建的效率相比堆区快了近100倍左右

taggedPointer的内存管理方案,比常规的内存管理,要快很多

Tagged Pointer的64位地址中,前4位代表类型,后4位主要适用于系统做一些处理,中间56位用于存储值

优化内存建议:对于NSString来说,当字符串较小时,建议直接通过@""初始化,因为存储在常量区,可以直接进行读取.会比WithFormat初始化方式更加快速

nonpointer_isa


nonpointer_isa


SideTable

散列表为什么在内存中有多张?最多能够多少张?

#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR

    enum{ StripeCount =8};

#else

    enum{ StripeCount =64};

#endif


ARC&MRC

MRC(手动内存管理)

在MRC时代,系统是通过对对象的引用计数来判断是否销毁,有以下规则

对象被创建时引用计数都为1

当对象被其他指针引用时,需要手动调用[objc retain],使对象的引用计数+1

当指针变量不再使用对象时,需要手动调用[objc release]来释放对象,使对象的引用计数-1

当一个对象的引用计数为0时,系统就会销毁这个对象

所以,在MRC模式下,必须遵守:谁创建,谁释放,谁引用,谁管理

ARC(自动内存管理)

ARC模式是在WWDC2011和iOS5引入的自动管理机制,即自动引用计数.是编译器的一种特性.其规则与MRC一致,区别在于

ARC中禁止手动调用retain/release/retainCount/dealloc

编译器会在适当的位置插入release和autorelease

ARC新加了weak、strong关键字

ARC是LLVM和Runtime配合的结果

retain 总结:

retain在底层首先会判断是否是Nonpointer isa,如果不是,则直接操作散列表 进行+1操作

如果是Nonpointer isa,还需要判断是否正在释放,如果正在释放,则执行dealloc流程,释放弱引用表和引用计数表,最后free释放对象内存

如果不是正在释放,则对Nonpointer isa进行常规的引用计数+1.这里需要注意一点的是,extra_rc在真机上只有8位用于存储引用计数的值,当存储满了时,需要借助散列表用于存储.需要将满了的extra_rc对半分,一半(即2^7)存储在散列表中.另一半还是存储在extra_rc中,用于常规的引用计数的+1或者-1操作,然后再返回

release

release与retain相似,会在底层调用objc_release

objc_release先判断是否为isTaggedPointer,是就直接返回不需要处理,不是在调用obj->release()

objc_object::release通过fastpath大概率调用rootRelease(),小概率通过消息发送调用对外提供的SEL_release

rootRelease调用rootRelease(true, false)

rootRelease内部实现也有个do-while循环

先判断是否为nonpointer_isa(小概率事件)不是的话则直接对散列表中的引用计数进行-1操作

如果是Nonpointer isa,则对extra_rc中的引用计数值进行-1操作,并存储此时的extra_rc状态到carry中

如果此时的状态carray为0,则走到underflow流程

判断散列表中是否存储了一半的引用计数

如果是,则从散列表中取出存储的一半引用计数,进行-1操作,然后存储到extra_rc中

如果此时extra_rc没有值,散列表中也是空的,则直接进行析构,即dealloc操作,属于自动触发

AutoReleasePool 自动释放池

从程序启动到加载完成,主线程对应的runloop会处于休眠状态,等待用户交互来唤醒runloop

用户的每一次交互都会启动一次runloop,用于处理用户的所有点击、触摸事件等

runloop在监听到交互事件后,就会创建自动释放池,并将所有延迟释放的对象添加到自动释放池中

在一次完整的runloop结束之前,会向自动释放池中的所有对象发送release消息,然后销毁自动释放池


1.autoreleasepool其本质是一个结构体对象,一个自动释放池对象就是页,是栈结构存储,符合先进后出的原则

2.页的栈底是一个56字节大小的空占位符,一页总大小为4096字节

3.只有第一页有哨兵对象,最多存储504个对象,从第二页开始最多存储505个对象

4.autoreleasepool在加入要释放的对象时,底层调用的是objc_autoreleasePoolPush方法(push操作)

5.autoreleasepool在调用析构函数释放时,内部的实现是调用objc_autoreleasePoolPop方法(pop操作)


3 自动释放池能否嵌套使用?。

1.可以嵌套使用,其目的是可以控制应用程序的内存峰值,使其不要太高

2.可以嵌套的原因是因为自动释放池是以栈为节点,通过双向链表的形式连接的,且是和线程一一对应的

3.自动释放池的多层嵌套其实就是不停的push哨兵对象,在pop时,会先释放里面的,在释放外面的

5 AutoreleasePool的释放时机是什么时候?

1.App启动后,苹果在主线程RunLoop里注册了两个Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()

2.第一个Observer监视的事件是Entry(即将进入 Loop),其回调内会调用_objc_autoreleasePoolPush()创建自动释放池.其order是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前

3.第二个Observer监视了两个事件:BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()释放旧的池并创建新池;Exit(即 将退出Loop)时调用_objc_autoreleasePoolPop()来释放自动释放池.这个Observer的order是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后

6 thread和AutoreleasePool的关系

每个线程都有与之关联的自动释放池堆栈结构,新的pool在创建时会被压栈到栈顶,pool销毁时,会被出栈,对于当前线程来说,释放对象会被压栈到栈顶,线程停止时,会自动释放与之关联的自动释放池.

7 RunLoop和AutoreleasePool的关系

1.主程序的RunLoop在每次事件循环之前,会自动创建一个autoreleasePool

2.并且会在事件循环结束时,执行drain操作,释放其中的对象


六、NSRunLoop

① RunLoop介绍

RunLoop是事件接收和分发机制的一个实现,是线程相关的基础框架的一部分,一个RunLoop就是一个事件处理的循环,用来不停的调度工作以及处理输入事件.

RunLoop本质是一个do-while循环,没事做就休息,来活了就干活.与普通的while循环是有区别的,普通的while循环会导致CPU进入忙等待状态,即一直消耗cpu,而RunLoop则不会,RunLoop是一种闲等待,即RunLoop具备休眠功能.

RunLoop的作用

保持程序的持续运行

处理App中的各种事件(触摸、定时器、performSelector)

节省cpu资源,提供程序的性能,该做事就做事,该休息就休息


run

⑦.1 当前有个子线程,子线程中有个timer。timer是否能够执行,并进行持续的打印?

不可以,因为子线程的runloop默认不启动, 需要runloop run手动启动.

⑦.2 RunLoop和线程的关系

1.每个线程都有一个与之对应的RunLoop,所以RunLoop与线程是一一对应的,其绑定关系通过一个全局的Dictionary存储,线程为key,runloop为value.2.线程中的RunLoop主要是用来管理线程的,当线程的RunLoop开启后,会在执行完任务后进行休眠状态,当有事件触发唤醒时,又开始工作,即有活时干活,没活就休息3.主线程的RunLoop是默认开启的,在程序启动之后,会一直运行,不会退出4.其他线程的RunLoop默认是不开启的,如果需要,则手动开启

⑦.3 NSRunLoop和CFRunLoopRef区别

1.NSRunLoop是基于CFRunLoopRef面向对象的API,是不安全的

2.CFRunLoopRef是基于C语言,是线程安全的

⑦.4 Runloop的mode作用是什么?

mode主要是用于指定RunLoop中事件优先级的

⑦.5 以+scheduledTimerWithTimeInterval:的方式触发的timer,在滑动页面上的列表时,timer会暂停回调,为什么?如何解决?

1.timer停止的原因是因为滑动scrollView时,主线程的RunLoop会从NSDefaultRunLoopMode切换到UITrackingRunLoopMode,而timer是添加在NSDefaultRunLoopMode。所以timer不会执行

2.将timer放入NSRunLoopCommonModes中执行.

你可能感兴趣的:(内存管理与NSRunLoop 笔记)