一、内存布局
五大区
接下来我从内存中的低地址往高地址依次介绍五大区:
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
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资源,提供程序的性能,该做事就做事,该休息就休息
⑦.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中执行.