Objective-C 内存管理与高级环境编程 阅读分享
常用的调试私有API
- uintptr_t objc_rootRetainCount(id obj)
- _objc_autoreleasePoolPrint();//查看自动释放池中的对象
- LLVM clang编译器 -rewrite-objc 转换为我们可读源代码:
- clang -rewrite-objc 文件名
序言
本书主要讲述了内存管理,block,GCD三种OC数据结构实现原理
手动内存管理
- 内存管理是由NSObject来负责的
- 生成并持有对象 alloc,new,copy,mutableCopy
- 持有对象retain,释放对象release
- 持有对象的方式只有:alloc,new,copy,mutableCopy,retain
- 自己持有的对象才能释放,没有持有,不能释放
- 方法生成的对象,要看方法名,如果是alloc,new,copy,mutableCopy等前缀开头的,那么接受者仍然可以持有对象,其他方法生成的对象是不能持有的,属性是autorelease的
- autorelease 可以取得对象,但是并不持有对象,所以在非持有对象前缀方法中将对象作为返回值使用,autorelease将对象注册到了自动释放池
- 释放非自己持有的对象就会导致崩溃,野指针错误
- 四句话总结:自己生成的对象,自己持有;非自己生成的对象,自己也能持有;不在需要自己持有的对象时释放;非自己持有的对象无法释放
- 内存管理实现:
- GNUStep开源实现:alloc 内部函数分配空间含有一个计数器,额外的内存空间,同时内部会调用allocWithZone:方法;dealloc函数内部会调用free方法进行调用,所以实际分配空间在C层面还有额外的分配空间,用来向free方法指明内存整体大小,以及是否可以再次使用,以及系统层面的一个数据结构表,来记录分配表,以及可用表;
- 苹果实现:使用一个引用hash表来管理内存:键值是一个内存地址,对象是一个对象的引用计数。alloc,dealloc,retainCount,retain都转化为了对引用计数表的操作
- GNUStep在对象头部添加一个变量来保存引用计数,苹果则是保存在一个引用计数表中:
- 保存在头部的好处:
- 少量代码就能完成
- 能够统一管理引用计数用内存块和对象用的内存块
- 通过引用计数表保存的好处:
- 对象用内存块的分配无需考虑内存块头部
- 引用计数表格中记录有内存地址,可以从各个记录追溯到个对象的内存块
- 保存在头部的好处:
- autorelease实现原理
- 类似于C语言中的自动变量,autorelease将对象添加到了自动释放池中,而自动释放池的作用域结束那么久开始释放对象。
- autorelease作用:生成并持有NSAutoreleasePool对象,调用已分配对象的autorelease实例方法,废弃NSAutoreleasePool对象,对池子中调用过autorelease方法的其他对象调用release方法
- 调用了大量的autorelease方法但是自动释放池如果没有释放,那么其实对象还是没有释放的,将会产生内存不足的现象。
- autorelease具体实现
- GNUStep 使用的是IMP caching 来实现的,速度是其他方法的两倍。
- 相当于将对象添加到NSAutoreleasePool池子中
- AutoReleasePool实现
- GNUStep 实现:去当前正在使用的pool添加对象到一个数组中,当池子被释放时候,先释放数组中保存的所有的对象,再释放自己
- 苹果实现: 使用一个AutoreleasePool栈来保存所有的autoreleasePool,autorelease一个对象,会将该对象添加到数组中
- autorelease一个autoreleasePool将会造成一个运行时错误,该方法已经被重载掉了
ARC 内存管理 ARC的内存管理是编译器的特性
-
内存的思考方式
- 自己生成的对象,自己持有
- 非自己生成的对象,自己也可持有
- 自己持有的对象在不需要的时候要进行释放
- 非自己持有的对象无法释放
-
所有权修饰符 增加了四中所有权修饰符:__strong,__weak,__unsafe_unretained,__autoreleasing修饰符,该修饰符修饰的变量将会自动将变量置为nil.
-
__strong
- __strong 是id类型和其他对象类型的默认的所有权修饰符,有两个作用:1. 选择性retain,持有赋值给它的对象,直接持有alloc,new,copy,mutableCopy系的对象,retain持有类名开头的方法返回的对象 2. 会在超出作用域的时候自动调用release方法
- 总之,有强引用指向的对象会被保存,随着强引用的失效,对象会随之释放。
- 类成员变量,方法参数上也可以使用附有__strong修饰符修饰的变量
- 作用:__strong方法实现了自己生成的对象自己持有,非自己持有的对象自己也可以持有,不再需要的时候需要进行释放,同时也对非自己持有的对象无法释放进行了满足,完全实现了四句话准则,而__strong为默认修饰符,使得ARC有效而简单的遵循了OC内存管理思考方式
-
__weak 修饰符的引入是因为__strong修饰符无法解决循环引用的问题
- 循环引用:是指两个对象互相引用对方。还有可能会出现自己引用自己的情况
- 内存泄漏:本应当废弃的对象在超出其生命周期后继续存在
- __weak 不持有对象,并且当该对象被废弃的时候,该弱引用将会自动失效并且处于nil被赋值的状态(空弱引用),__weak修饰符适用于iOS5以上,在以前的版本中用__unsafe_retained来代替
-
__unsafe_retained 对象类似于__weak对象,不持有对象,但是不同的是,不会当该对象被释放的时候,不会被自动置为nil
-
__autoreleasing 修饰符在ARC环境下代替了autorelease方法
- 使用__autoreleasing 修饰符修饰的对象自动注册到autoreleasepool中
- 取得非自己生成的对象的时候,编译器会自动检查方法名是否是一alloc/new/copy/mutableCopy等开头的,如果不是则自动将该对象注册到autoreleasepool中,此时没有使用__autoreleasing 修饰符也可以将对象注册到autoreleasepool中。
- 对象作为函数的返回值的时候,系统会自动将其注册到autoreleasepool中
- 对于访问__weak修饰符修饰的对象时候,会自动被注册到autoreleasepool中,防止在访问的过程中该对象被释放,造成数据不一致现象的出现
- id *obj或者NSError **error 默认的修饰符是__autoreleasing,id obj,NSError *error修饰符默认是__strong,举个例子就是NSError * __autoreleasing *error 作为参数传递的时候是默认修饰方式__autoreleasing,与非alloc/new/copy/mutableCopy外其他方法取得对象的情况是完全一样的
- 给对象赋值的时候,所有权修饰符必须保持一致,否则将会产生编译错误
- 对象指针赋值的时候,所有权修饰符也必须保持一致
- 显示指定__autoreleasing修饰符的对象必须是:自动变量,包含局部变量,函数,以及方法参数等
-
-
ARC规则
- 不能使用retain/release/retainCount/autorelease
- 不能使用NSAllcateObject/NSDeallocObject
- 必须遵守内存管理的方法命名规则
- 不要显示调用dealloc
- 使用@autoreleasepool块代替NSAutoreleasepool
- 不能使用NSZone
- 对象型的变量不能作为c语言结构体的成员
- id 与void *之间必须进行显示转换
__bridge
不改变对象的持有者的持有状况,使用__bridge替换__bridge_retained 将会造成野指针,使用__bridge替换__bridge_transfer将会造成内存的泄漏__bridge_retained
将要转换赋值给的变量也持有所赋值的对象,= 两边的两个变量同时持有该对象,类似于retain
,类似于CFRetain()
__bridge_transfer
将要转换赋值给的变量持有该赋值对象,但是原来的变量现在不持有该对象了,=左边的变量持有对象,右边的变量释放该对象,类似与release
,类似于CFRelease()
-
属性
- assign (__unsafe_retained)
- copy (__strong)
- retain (__strong)
- strong (__strong)
- unsafe_retained (__unsafe_retained) 不会自动初始化为nil
- weak (__weak)
-
ARC的实现 编译器和objc4 runtime配合实现,因为如果只是编译器特性,那么爱iOS4中也是可以使用weak属性修饰符的,但是实际却不能
- __strong实现
- objc_msgSend(obj,@selector),objc_release
- __autoreleasing 隐式实现
- objc_retainAutoreleasedReturnValue 与 objc_autoreleaseReturenValue结对优化
- objc_AutoreleasedReturnValue不仅仅将对象注册到autoreleasepool,还会检查方法的调用方在调用了该方法之后是否紧接着调用了objc_retainAutoreleaseReturnValue,如果调用了那么就不注册该对象到autoreleasepool中,从而实现不将对象注册到autoreleasepool方法中而是直接传递,从而实现了优化
- __weak实现 (weak表(是一张hash表)来实现)
- weak表来实现对象为nil的时候,自动置为nil id __weak obj1 = obj;
- objc_initWeak(&obj1,obj) => objc_storeWeak(&obj1,obj)//obj是weak表的键值
- objc_destroyWeak(&obj1) => obj1 = 0, objc_storeWeak(&obj1,obj) 从weak表中删除该记录
- 对象销毁过程
- objc_release
- dealloc
- _objc_rootDealloc
- object_dispose
- objc_destructInstance
- objc_clear_deallocing
- weak表中获取废弃对象地址为键值的记录
- 将包含记录中的所有附有__weak修饰符变量的地址,赋值为nil
- 从weak表中删除该对象
- 从引用计数表中删除废弃对象地址作为键值的记录
- 由于大量使用__weak表修饰的变量会消耗相应CPU资源,所以我们应该只需要在避免循环引用的时候才使用__weak修饰符
- 立即被释放的情况: id __weak obj = [[NSObject alloc]init];
- 访问还有strong引用指向的weak变量的时候,该变量指向的对象将会被注册到autoreleasepool中
- id tmp = objc_loadWeakRetained(&obj1);
- objc_autorelease(tmp);
- 从这里可以看出autorelease和retain也是配对使用的哦
- 重复访问weak变量,该对象将会被重复注册到autoreleasepool中,使用strong中间转换一下就不会多次丢入autoreleasepool中了
- 两个没有写入接口文档的的weak修饰符相关的方法
- (BOOL)allowRetainWeakReference; //是否允许weak引用,如果不,而引用了,会异常终止。
- (BOOL)retainWeakRefence; //是否允许加入到autoreleasepool中
- autoreleasing 修饰符
@autoreleasepool { id __autoreleasing obj = [[NSObject alloc] init]; } 等于 id pool = objc_autoreleasePoolPush(); id obj = objc_msgSend(NSObject, @selector(alloc)); objc_msgSend(obj, @selector(init)); objc_autorelease(obj); objc_autoreleasePoolPop(pool);
- __strong实现
Blocks
- blocks定义
- block 是带有自动变量(局部变量)的匿名函数,blocks提供了类似有C++和OC类生成实例或对象的方式来保持变量值得方法
- block 定义
- 语法格式
^ 返回值类型 参数列表 表达式
,如^int (int count){return count+1; }
, - 其中返回值类型是可以省略的,可以有return进行推断;参数列表为空的时候也是可以省略
-^(int count) { return count + 1; }
^{print("参数为空的显示形式")}
- 语法格式
- Block 类型声明
- 类似于函数指针,只是将*替换为^:
返回值类型(^blick变量名)参数类型
; - int (^blk)(int count) = ^(int count) {return count +1; };
- typedef int(^blk_type)(int count);
- 类似于函数指针,只是将*替换为^:
- block 截获自动变量值
- Block表达式所截获的自动变量的值是保存的该变量的瞬间值,该变量普通类型那么拷贝一份该变量,如果是对象类型,那么将会强引用该对象。(类似于函数传参咯)
- __block说明符 可以实现在block内部改变该变量的值,如果不加__bock,直接修改该变量会编译错误,但是可以调用该变量的方法
- block捕获变量不支持捕获c语言的数组,但是可以捕获c语言的指针变量
- block实现-内存结构
- block就是OC对象,该结构体中包含类型信息,保留的引用变量同类型的自动变量,函数指针等信息
- block 结构体中捕获的的成员变量的类型与捕获的自动变量的类型是一样的,没有使用的不会添加到block结构体中;
- 该结构体初始化的参数传递是普通的C函数传参
- 总的来说,所谓截获自动变量值,是指在执行block语法时,block语法变道时所使用的自动变量值被保存到Block结构体实例中。
- block不能捕获数组是因为,可能由于C语言规范不允许 char b[10] = a;这样的语法出现
- __block实现 由于block内部又生成了一份自动变量,并且按照函数的参数赋值进行保留,所以修改该自动变量拷贝,是无法修改原来的自动变量的,所以出现了__block说明符;
- 保存block内部的值,可以使用全局变量,静态全局变量,静态局部变量
- 如果在block中修改了静态局部变量,那么编译器将会改写该静态变量的声明为静态指针变量,在block内部就像以前一样进行生成针对静态指针变量的自动变量;
- 但是如果是普通的变量,block为什么不类似于静态局部block来处理呢,是因为原来的自动变量会在超出作用域后就会释放,如果此时block在用指针去访问,那么会造成野指针访问错误的。
- __block说明符是存储说明符,用来指定将变量存储到内存的哪个区域中
- __block说明符修饰的自动变量,将会将原来的自动变量变为一个 类型为blockByRef 的新的变量,用来保存自己的引用地址(_forwarding),以及当前的值
- _forwarding的出现是为了实现多个block引用同一个__block变量,不管该__block在堆区还是栈区都可以正确的引用__block变量
- block的内存位置及__block变量
- _NSConcreteStackBlock (栈区)
- 大多数是在栈区
- _NSConcreteGlobalBlock(全局数据区)
- 全局block
- block语法中没有使用截获的自动变量
- _NSConcreteMallocBlock(堆区)
- 调用了copy
- block 调用copy方法
- __NSConcreteStackBlock 栈 =》 堆区
- _NSConcreteMallocBlock 引用计数+1
- _NSConcreteGlobalBlock 什么都不做
- 哪些情况下系统自动复制block
- Cocoa框架的方法且方法名中含有usingBlock的时候不必手动复制block
- Grand Central Didspatch 的API
- 其余情况下根据需要手动复制
- block复制的同时__block变量也会被复制到malloc区(堆区),同时__block变量的_forwarding指针指向了堆区新复制的__block变量,这样,不管是在栈区的block还是在堆区的block都可以访问同一个__block变量
- _NSConcreteStackBlock (栈区)
- 截获对象的实现 block内部会生成一个与对象相同类型的变量(默认是__strong类型),从而捕获在block实现中引用的对象
- 截获对象与__block变量的不同
- 截获对象: block copy和dispose方法中都传递的是 BLOCK_FIELD_IS_OBJECT
- __block变量:以上两个方法传递的参数是BLOCK_FIELD_IS_BYREF
- 截获对象与__block变量的不同
- 什么时候栈上的block会被复制到堆中
- 调用block的copy实现方法的时候
- Block作为函数返回值返回的时候
- 将Block赋值给__strong修饰符id类型的类或者是Block类型成员变量的时候
- 在方法名中包含有usingBlock的cocoa框架或者GrandCentralDispatch的API中传递block时
- block循环引用的改正方法
- ARC 环境下:使用__weak修饰符修饰,4.0及之前的用__unsafe_unretained修饰符修饰,注意不会置空;当然也可以使用__block修饰符修饰,但是要在block内调用完毕,将weakSelf置为nil
- 非ARC环境下:使用__block变量来避免循环引用,此时用__block修饰的变量不会被block retain
- copy/release
- retain对于已经在堆上的block是有效的,但是对于在栈上block是无效的
- Block_copy = copy ;Block_release = release
GCD
- 基本概念
GCD
将普通应用程序的线程管理代码放到系统级中实现,开发者只要将定义任务追加到DispatchQueue中,GCD就能自动创建线程并且执行任务。多线程
- 优点是:同时执行多个任务,彼此独立,提高了CPU的吞吐率;提供响应用户的能力;
- 缺点是:容易产生数据同步,线程死锁等状况,同时大量的线程会造成CPU上下文切换等等额外的开销,同时也会消耗大量的内存空间
DispatchQueue
- Serial Dispatch Queue 同步线性队列,要等待现在执行中的任务处理结束
- Concurrent Dispatch Queue 异步并发队列,不等待现在执行中的处理结束
- 并行执行的任务数量取决于当前系统的状态,即iOS 和 OS X 基于Dispatch Queue中的任务数量,CPU核心数以及CPU负荷扥当前系统状态来决定Concurrent Dispatch Queue 中执行的任务数量
- iOS和OS X-XNU 内核决定应当使用的线程数,并只生成所需的线程执行处理。并且,当任务结束,应当执行的任务减少时,XNU 内核会结束不再需要使用的线程。内核通过ConcurrentDspatchQueue来管理并执行多个处理的线程
- 单个dispatch_queue_serial内的任务是串行执行的,但是多个dispatch_queue_serial 之间是并行执行的,同时一个dispatch_queue_serial代表一个线程,应该按需生成。而dispatch_queue_concurrent 则是XNU系统来管理生成多少个线程的
- 在iOS6.0 之前的系统,生成的dispatch_queue必须有程序员来负责释放,系统是没有自动回收的方式的,dispatch_queue_create生成的queue在不需要使用的时候应该通过dispatch_release函数进行释放
- 在iOS6.0 之前的部分,dispatch 相关的对象需要手动进行内存管理,使用dispach_createXX 创建但是并不持有,dispatch_retain()持有,dispath_release()进行释放。传入的block会持有queue,block执行完毕就会.
- 常用的函数
- dispatch_queue_create 生成串行或并行的队列,但是优先级与默认优先级的Global Dispatch Queue相同
dispatch_set_target_queue(<#dispatch_object_t object#>, <#dispatch_queue_t queue#>);
- Sets the target queue for the given object.An object's target queue is responsible for processing the object. The target queue determines the queue on which the object's finalizer is invoked.
- 用途是用来指定多个队列形成一个队列间的执行层次;可以用于同步任务执行顺序
- dispatch_after 在指定时间添加任务到Dispatch_Queue
- dispatch_group 用于设置所有任务执行完毕后执行的操作,设置任务之间的执行顺序关系
- dispatch_group_create()
- dispatch_group_async(group, queue, block1)
- dispatch_group_notify(group, queue, block)
- dispatch_group_wait(group, time) 指定时长后是否结束任务,在等待的这段时间,当前线程是暂停状态的
- dispatch_barrier_aysnc
-
用来控制读写同步
dispatch_async(queue,blk0_for_reading); dispatch_async(queue,blk0_for_reading); dispatch_barrier_async(queue,blk_for_writing); dispatch_async(queue,blk0_for_reading); dispatch_async(queue,blk0_for_reading);
-
- dispatch_async 直接将将任务添加到队列中,不等待添加的任务完成,直接返回
- dispatch_sync 添加任务到当前队列中,同时等待当前的任务完成后返回,所以当前的线程会因为等待而暂停,如果你添加的队列正好是当前队列,那么就会造成死锁。
- dispatch_apply 是dispatch_sync和dispatch_group的关联API。 该函数按照指定的次数将指定的Block追加到指定的Dispatc Queue中,并且等待全部处理执行完毕
- dispatch_apply(10, queue, ^(size_t index){})
- dispatch_suspend /dispatch_resume 是用来暂停和挂起整个队列的任务
- dispatch_semaphore 信号量机制,进行同步异步控制
- dispatch_semaphore_create(NSInteger)
- dispatch_semaphore_wait(semaphore, dispatch_time_t)
- dispatch_semaphore_signal(semaphore)
- dispatch_once 保证应用程序只执行一次API,多线程安全
- dispatch I/O 异步读取大文件,采用多个线程并发分割读取,提高文件读取的速度
- dispatch_io_set_low_water 设置一次读取的大小
- dispatch_io_read 使用Global Dispatch Queue 进行并列读取