行业网站做不下去,义乌网红,长丰下塘新农村建设网站,陕西省建设网官网MRC与ARC再回顾
在前面#xff0c;我们简单学了MRC与ARC。MRC指手动内存管理#xff0c;需要开发者使用retain、release等手动管理对象的引用计数#xff0c;确保对象在必要时被释放。ARC指自动内存管理#xff0c;由编译器自动管理对象的引用计数#xff0c;开发者不需要…MRC与ARC再回顾
在前面我们简单学了MRC与ARC。MRC指手动内存管理需要开发者使用retain、release等手动管理对象的引用计数确保对象在必要时被释放。ARC指自动内存管理由编译器自动管理对象的引用计数开发者不需要手动管理内存。
哪些对象需要我们进行内存管理呢
任何继承了NSObject的对象需要进行内存管理而其他非对象类型(int、char、float、double、struct、enum等) 不需要进行内存管理
这是因为
继承了NSObject的对象的存储在操作系统的堆里边。而操作系统的堆一般由程序员分配释放。非OC对象一般放在操作系统的栈里面操作系统的栈由操作系统自动分配释放。
例
int main(int argc, const char * argv[])
{autoreleasepool {int a 10; // 栈int b 20; // 栈// Person对象(计数器1) : 堆Person *p [[Person alloc] init];// p局部指针变量: 栈}// 经过上面代码后, 栈里面的变量a、b、p 都会被回收// 但是堆里面的Person对象还会留在内存中,因为它是计数器依然是1return 0;
}MRC深入
首先先回顾空指针和野指针
空指针、野指针
空指针 空指针指的是没有指向存储空间的指针(里面存的是 nil, 也就是 0)。 给空指针发消息是没有任何反应的也不会报错 空指针比如
NSObject *a [[NSObject alloc] init]; //执行完引用计数为1
[a release]; //执行完引用计数为 0实例对象被释放。
a nil; //此时a变为了空指针。
[a release]; // 再给空指针a发送消息就不会报错了。
[a release]; // 这段代码不会报错野指针 只要一个对象被释放了我们就称这个对象为“僵尸对象(不能再使用的对象)”。 当一个指针指向一个僵尸对象我们就称这个指针为「野指针」。 只要给一个野指针发送消息就会报错(EXC_BAD_ACCESS 错误)。 MRC避免循环引用
定义两个了类Person和Dog Person类
#import Foundation/Foundation.h
NS_ASSUME_NONNULL_BEGIN
class Dog;
interface Person : NSObject
property (nonatomic, assign) Dog *dog;
endNS_ASSUME_NONNULL_ENDDog类
#import Foundation/Foundation.h
NS_ASSUME_NONNULL_BEGIN
class Person;
interface Dog : NSObject
property (nonatomic, retain) Person *person;
endNS_ASSUME_NONNULL_END主函数
Person *p [[Person alloc] init];
Dog *d [[Dog alloc] init];p.dog d;
d.person p;[p release];
[d release];我们看上面的代码会出现 A 对象要拥有 B 对象而 B 对应又要拥有 A 对象此时会形成循环 retain导致 A 对象和 B 对象永远无法释放。
要解决这个问题的话不要让 A retain BB retain A所以其中一方不要做retain方法。当两端互相引用时应该一端用 retain一端用 assign。
ARC深入
在之前的博客里已经学习了ARC许多知识包括修饰符、规则、属性和简单实现。可以看我的博客iOS——【自动引用计数】ARC规则及实现 现在我们更深入的了解它的实现。
在Objective C中有三种类型是ARC适用的 1. block 2. objective 对象id, Class, NSError*等 3. 由attribute((NSObject))标记的类型。
*像double ,CFStringRef等不是ARC适用的仍然需要手动管理内存。 Tips 以CF开头的Core Foundation的对象往往需要手动管理内存。
ARC在编译期和运行期分别做了什么
先说结论
1. 在编译期ARC会把互相抵消的retain、release、autorelease操作约简。 2. ARC包含有运行期组件可以在运行期检测到autorelease和retain这一对多余的操作。为了优化代码在方法中返回自动释放的对象时要执行一个特殊函数。
接下来我们来探究这个结论
ARC的实现
ARC的底层是怎么工作的
首先根据前面的学习我们知道ARC有一套命名规则若方法名以alloc、new、copy、mutableCopy开头则方法的调用者需要负责保留和释放使用该方法返回的对象在MRC中就需要我们手动保留和释放返回的对象。如果不以上面这些开头则不用调用者保留和释放因为方法内部会自动执行autorelease方法。
下面用实例说明 实例1 首先我们创建一个Person类这个类有两个类方法一个是用creat开头的一个是用new开头的现在调用这两个方法
#import Person.himplementation Person (instancetype)creatPerson {return [[self alloc] init];
}(instancetype)newPerson {return [[self alloc] init];
}end#import Foundation/Foundation.h
#import Person.hint main(int argc, const char * argv[]) {[Person creatPerson];[Person newPerson];
}接下来我们查看它的汇编代码
可以看见在newPerson方法下首先用objc_megSend向newPerson发送消息然后有一个objc_release的标志位这个标志位意味着系统在这个位置会调用objc_release函数。 而在creatPerson方法下creatPerson中ARC修改后的代码大致逻辑是
//Person.m(instancetype)creatPerson {id temp [self new]; return objc_autoreleaseReturnValue(temp);
}
//main.m
- (void)testForARC { objc_unsafeClaimAutoreleasedReturnValue([Person creatPerson]);
}在create中多了objc_autoreleaseReturnValue这是因为ARC规则无需手动释放的内部自动autorelease。 objc_autoreleaseReturnValue: 这个函数的作用相当于代替我们手动调用 autorelease, 创建了一个autorelease对象。编译器会检测之后的代码, 根据返回的对象是否执行 retain操作, 来设置全局数据结构中的一个标志位, 来决定是否会执行 autorelease操作。该标记有两个状态, ReturnAtPlus0代表执行 autorelease, 以及ReturnAtPlus1代表不执行 autorelease。 objc_unsafeClaimAutoreleasedReturnValue函数作用是对autorelease对象不做处理仅仅返回对非autorelease对象调用objc_release函数并返回。所以本情景中它创建时执行了 autorelease操作了就不会对其进行 release操作了。只是返回了对象在合适的实际autoreleasepool会对其进行释放的。
实例2 我们再给出下面的例子
id temp2 [Person newPerson];查看汇编代码
可以看出来与前面不同的是多了一个objc_storeStrong的标志位。因此可知编译器在这里调用了该函数。我们在objc库里看看这个函数的实现。在objc4的NSObject.mm中
void
objc_storeStrong(id *location, id obj)
{id prev *location;if (obj prev) {return;}objc_retain(obj);*location obj;objc_release(prev);
}说一下这个函数objc_storeStrong函数是用于管理强引用的一个函数主要用于更新一个指向对象的强引用并确保正确的内存管理。 在上面这段代码里首先获取旧对象的引用prev。然后进行比较如果新对象和旧对象相同则返回。否则保留新对象并将新对象的引用1。然后更新指针*location指向新对象。最后释放旧对象。使用这个函数是因为temp2是一个强引用变量。编译器会将上述代码转换为类似以下内容
id temp2 [Person newPerson];
objc_storeStrong(temp2, nil);而同样的在creatPerson方法中
id temp1 [Person creatPerson];它的汇编代码
这里同样使用了objc_storeStrong函数但是多了一个objc_retainAutoreleasedReturnValue函数这个函数将替代 MRC中的 retain方法, 此函数也会检测刚才提到的那个标志位, 如果为ReturnAtPlus0执行该对象的 retain操作否则直接返回对象本身。
在这个例子中, 由于代码中没有对对象进行保留, 所以创建时objc_autoreleaseReturnValue函数设置的标志位状态是应该是ReturnAtPlus0。所以, 该函数在此处是会进行 retain操作的。 实例3 有如下代码
#import strat.h
#import Person.hinterface strat ()property (nonatomic, strong) id temp1;
property (nonatomic, strong) id temp2;endimplementation strat- (void)gogo {self.temp1 [Person creatPerson];self.temp2 [Person newPerson];
}end我们先看temp2的汇编代码
可以看出这段代码相比于实例2的代码多了一个megSendobjc_msgSend$setTemp2这个是setter方法的函数所以其实这里相当于多了一个setter操作。因此这里ARC的补充的代码大概是
- (void)setTemp2:(id)newValue {objc_storeStrong(_temp2, newValue);
}temp1与temp2被补充的部分差不多。
自动释放池
自动释放池autorelease是 Objective-C 内存管理的重要特性之一。 简单来说它允许开发者推迟对象的释放时间通常在下一个事件循环中才执行释放操作。 自动释放池之前在博客 中学习过现在来深入学习一下。
MRC下的自动释放池
在MRC下使用自动释放池是通过NSAutoreleasePool实现的如果编译器版本是LLVM.3.0以上那么autorelease{…}也可以使用其生命周期相当于c语言变量的作用域。对于所有调用过 autorelease方法的对象在废弃 NSAutoreleasePool对象时都将调用 release实例方法。
int main(int argc, const char * argv[]) {autoreleasepool {//创建并使pool持有一个自动释放池NSAutoreleasePool *pool [[NSAutoreleasePool alloc] init];//创建Person实例对象pPerson *p [[Person alloc] init];//将实例对象p加入pool持有的自动释放池[p autorelease];//废弃NSAutoreleasePool对象;//并向pool池中的所有对象发送消息让它们调用release方法。这里相当于也[p release]了[pool drain];//这里会报错因为p已被释放NSLog(%, p);}return 0;
}报错内容Thread 1: EXC_BAD_ACCESS (code1, address0x370001a2b46808)
这里我当时有一个问题当调用[p autorelease]的时候是怎么判断当前的自动释放池是pool持有的那个的 这是因为运行时系统维护了一个全局的栈来跟踪所有的自动释放池。当一个新的 NSAutoreleasePool 对象被创建时它会被推入栈顶。随后当调用 -autorelease 方法时当前的栈顶的自动释放池会接收该对象。
理解 NSAutoreleasePool对象的生命周期如下图所示
ARC下使用自动释放池
ARC环境不能使用 NSAutoreleasePool类也不能调用autorelease方法代替它们实现对象自动释放的是 autoreleasepool块和__autoreleasing修饰符。比较两种环境下的代码差异如下图
ARC环境下使用自动释放池
autoreleasepool {id obj [[NSObject alloc] init];NSLog(%, obj);
}ARC的很多情况下即使是不显式的使用 __autoreleasing也能实现对象被注册到释放池中。主要包括以下几种情况
编译器会进行优化检查方法名是否以 alloc/new/copy/mutableCopy开始如果不是则自动将返回对象注册到 Autoreleasepool; 访问附有 __weak修饰符的变量时实际上必定要访问注册到 Autoreleasepool的对象即会自动加入 Autoreleasepool; id的指针或对象的指针(id*NSError **)在没有显式地指定修饰符时候会被默认附加上 __autoreleasing修饰符加入 Autoreleasepool。
AutoreleasePool的具体实现
有以下自动释放池的代码
int main(int argc, const char * argv[]) {autoreleasepool {NSLog(hello);}return 0;
}将其转化为cpp代码
int main(int argc, const char * argv[]) {/* autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; NSLog((NSString *)__NSConstantStringImpl__var_folders_mr_nr_xs_2x079d0zxp68ymmm4c0000gn_T_main_82a790_mi_0);}return 0;
}可以发现自动释放池在底层中对应的是一个__AtAutoreleasePool的结构体我们再在源码中可以找到这个结构体的定义
extern C __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern C __declspec(dllimport) void objc_autoreleasePoolPop(void *);struct __AtAutoreleasePool {__AtAutoreleasePool() {atautoreleasepoolobj objc_autoreleasePoolPush();}~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}// 边界对象(指针)用来标识自动释放池的开始和结束。这个对象在创建和销毁自动释放池时分别被推入和弹出。void * atautoreleasepoolobj;
};可以看见在这个结构体中有一个构造函数内部调用objc_autoreleasePoolPush()和一个析构函数内部调用objc_autoreleasePoolPop(atautoreleasepoolobj)以及一个atautoreleasepoolobj边界对象。
objc_autoreleasePoolPush()用于返回边界对象objc_autoreleasePoolPop(atautoreleasepoolobj)用于传入边界对象
边界对象其实就是 nil的别名而它的作用事实上也就是为了起到一个标识的作用。
AutoreleasePoolPage
我们继续深入对objc_autoreleasePoolPush()和objc_autoreleasePoolPop(atautoreleasepoolobj)进行学习发现它们实际上是对AutoreleasePoolPage对应的静态方法 push和 pop的封装。 在NSObject的源码中可以发现以下代码
void *
objc_autoreleasePoolPush(void)
{return AutoreleasePoolPage::push();
}NEVER_INLINE
void
objc_autoreleasePoolPop(void *ctxt)
{AutoreleasePoolPage::pop(ctxt);
}AutoreleasePoolPage是C的一个类在NSObject.mm中可以找到它的代码这是它的一部分核心代码
class AutoreleasePoolPage {
# define EMPTY_POOL_PLACEHOLDER ((id*)1) //空池占位
# define POOL_BOUNDARY nil //边界对象(即哨兵对象static pthread_key_t const key AUTORELEASE_POOL_KEY;static uint8_t const SCRIBBLE 0xA3; // 0xA3A3A3A3 after releasingstatic size_t const SIZE
#if PROTECT_AUTORELEASEPOOLPAGE_MAX_SIZE; // must be multiple of vm page size
#elsePAGE_MAX_SIZE; // size and alignment, power of 2
#endifstatic size_t const COUNT SIZE / sizeof(id);magic_t const magic; //校验AutoreleasePagePoolPage结构是否完整id *next; //指向新加入的autorelease对象的下一个位置初始化时指向begin()pthread_t const thread; //当前所在线程AutoreleasePool是和线程一一对应的AutoreleasePoolPage * const parent; //指向父节点page第一个结点的parent值为nilAutoreleasePoolPage *child; //指向子节点page最后一个结点的child值为niluint32_t const depth; //链表深度节点个数uint32_t hiwat; //数据容纳的一个上限//......
};每个自动释放池都是是由若干个 AutoreleasePoolPage组成的双向链表结构如下图所示:
AutoreleasePoolPage中拥有 parent和 child指针分别指向上一个和下一个 page当前一个 page的空间被占满(每个 AutorelePoolPage的大小为4096字节)时就会新建一个 AutorelePoolPage对象并连接到链表中后来的 Autorelease对象也会添加到新的 page中
另外当 nextbegin()时表示 AutoreleasePoolPage为空 当 next end()表示 AutoreleasePoolPage已满。
AutoreleasePool子线程上的释放时机
子线程默认不开启 RunLoop那么其中的延时对象该如何释放呢其实这依然要从 Thread和 AutoreleasePool的关系来考虑
Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects.也就是说每一个线程都会维护自己的 Autoreleasepool栈所以子线程虽然默认没有开启 RunLoop但是依然存在 AutoreleasePool在子线程退出的时候会去释放 autorelease对象。 前面讲到过ARC会根据一些情况进行优化添加 __autoreleasing修饰符其实这就相当于对需要延时释放的对象调用了 autorelease方法。从源码分析的角度来看如果子线程中没有创建 AutoreleasePool 而一旦产生了 Autorelease对象就会调用 autoreleaseNoPage方法自动创建 hotpage并将对象加入到其栈中。所以一般情况下子线程中即使我们不手动添加自动释放池也不会产生内存泄漏。
weak修饰符补充
在前面的博客中我们知道了weak指针的一个生命周期是objc_initWeak函数到objc_destroyWeak函数但其实这两个函数中都引用了另一个runtime的函数storeWeak这个函数和我们刚刚见到的storeStrong函数是对应的现在我们到NSObject.mm中看看他们的源码
id
objc_initWeak(id *location, id newObj)
{if (!newObj) {*location nil;return nil;}return storeWeakDontHaveOld, DoHaveNew, DoCrashIfDeallocating(location, (objc_object*)newObj);
}void
objc_destroyWeak(id *location)
{(void)storeWeakDoHaveOld, DontHaveNew, DontCrashIfDeallocating(location, nil);
}storeWeak:
static id
storeWeak(id *location, objc_object *newObj)
{ASSERT(haveOld || haveNew); // 断言必须有旧值或新值if (!haveNew) ASSERT(newObj nil); // 如果没有新值断言新对象为 nilClass previouslyInitializedClass nil; // 之前初始化的类id oldObj; // 旧对象SideTable *oldTable; // 旧对象的 SideTableSideTable *newTable; // 新对象的 SideTable// 获取旧值和新值的锁。按锁地址排序以防止锁排序问题。// 如果旧值在操作过程中发生变化重试。retry:if (haveOld) {oldObj *location; // 获取旧对象oldTable SideTables()[oldObj]; // 获取旧对象的 SideTable} else {oldTable nil; // 没有旧值}if (haveNew) {newTable SideTables()[newObj]; // 获取新对象的 SideTable} else {newTable nil; // 没有新值}SideTable::lockTwohaveOld, haveNew(oldTable, newTable); // 获取旧值和新值的锁if (haveOld *location ! oldObj) {SideTable::unlockTwohaveOld, haveNew(oldTable, newTable); // 如果旧值在操作过程中发生变化解锁并重试goto retry;}// 为了防止弱引用机制与 initialize 机制之间的死锁确保没有弱引用的对象具有未初始化的 isa。if (haveNew newObj) {Class cls newObj-getIsa(); // 获取新对象的类if (cls ! previouslyInitializedClass !((objc_class *)cls)-isInitialized()) // 如果类未初始化{SideTable::unlockTwohaveOld, haveNew(oldTable, newTable); // 解锁class_initialize(cls, (id)newObj); // 初始化类previouslyInitializedClass cls; // 更新之前初始化的类goto retry; // 重试}}// 清理旧值如果有。if (haveOld) {weak_unregister_no_lock(oldTable-weak_table, oldObj, location); // 从弱引用表中解除登记旧对象}// 登记新值如果有。if (haveNew) {newObj (objc_object *)weak_register_no_lock(newTable-weak_table, (id)newObj, location, crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating); // 在弱引用表中登记新对象// weak_register_no_lock 如果弱存储应被拒绝则返回 nil// 在引用计数表中设置 is-weakly-referenced 位。if (!_objc_isTaggedPointerOrNil(newObj)) {newObj-setWeaklyReferenced_nolock(); // 设置新对象为弱引用}// 不要在其他地方设置 *location。这会引入竞争条件。*location (id)newObj; // 更新位置指针为新对象}else {// 没有新值。存储没有变化。}SideTable::unlockTwohaveOld, haveNew(oldTable, newTable); // 解锁旧值和新值的 SideTable// 这个操作必须在解锁后调用因为它可能会调用任意代码。// 特别是即使 _setWeaklyReferenced 没有实现resolveInstanceMethod: 可能实现并可能回调到弱引用机制。callSetWeaklyReferenced((id)newObj); // 调用设置弱引用的方法return (id)newObj; // 返回新对象
}这段代码的主要思路是管理弱引用对象首先获取旧对象和新对象并锁定相关表以防止多线程冲突接着检查并确保新对象的类已初始化然后从弱引用表中解除旧对象的登记再将新对象登记到弱引用表并设置弱引用标志最后解锁并调用设置弱引用的方法从而安全地更新和管理弱引用对象。 弱引用表是Objective-C中用来管理和追踪弱引用对象的数据结构。在Objective-C的内存管理中弱引用是一种不会增加对象引用计数的引用类型这意味着当对象没有强引用时弱引用不会阻止对象被释放。 弱引用表的主要作用包括三个方面首先它能够跟踪记录被弱引用的对象并存储这些弱引用的位置其次当对象的所有强引用都被释放后系统会自动将这些对象从弱引用表中移除并将弱引用设置为nil从而防止悬挂指针问题的发生最后通过弱引用表可以有效地防止因循环引用而导致的内存泄漏因为弱引用不会增加对象的引用计数也不会导致对象无法释放。 ARC不会优化的场景。
ARC适用于绝大多数场景但并不是万能的例如performSelect系列有许多方法带有选择子编译器不知道选择子具体是什么必须到了运行期才能确定因此在编译时不知道其方法名没法利用ARC内存规则来判断是否释放。
如果这时我们使用selectornewTest就会造成内存泄漏因为ARC此时不会帮助我们添加release。