群组
Flutter,iOS,Android,Python等,闲聊工作&技术&问题;
个人主页:https://waitwalker.cn
telegram群链接:https://t.me/joinchat/Ej0o0A1ntlq5ZFIMzzO5Pw
前言
iOS内存管理无论是早期的MRC还是现在的ARC本质都是通过引用计数(Reference Counting)机制管理内存,当一个对象被创建出来时,它的引用计数从0到1,当有外部对象对它进行强引用时,它的应用计数会+1,当该对象收到一条release消息时,它的引用计数会-1;当对象的引用计数为0时,对象将被释放,对象指向的内存被回收.
1. ARC内存管理的本质
MRC时代需要程序员手动管理对象的生命周期,也就是对象的引用计数有程序员来控制,什么时候retain,什么时候release,完全自己掌握.ARC(Automatic Reference Counting)自动引用计数是编译器的一个特性,能够自动管理OC对象内存生命周期.在ARC中你需要专注于写你的代码, retain ,release, autorelease操作交给编译器去处理就行了.
ARC 下编译器如何自动管理内存,其中,能想到的是在类的 dealloc 方法中,对该类的所持有的成员变量(strong)执行 release 操作,让所有成员变量的引用计数为0。对于局部变量,更可能是的对象在出作用域之前,编译器自动给对象加上一条 release消息.这些工作都是编译器为我们处理了.
// 作用域
{
NSString *str = [[NSString alloc]initWithFormat:@"%@",@"str"];
NSLog(@"%@",str);
// 在对象出作用域时,编译器自动给对象发一条release消息
[str release];
}
ARC,则无需我们自己显式持有(retain)和释放(release)对象,ARC通过对对像加上所有权修饰符(__strong等),编译器通过对象的所有权修饰符将会自动管理对象的引用计数.
2. 所有权修饰符
基础知识:指针是其实也是一个对象,它指向一个内存地址单元,内存单元里存着各种变量.这样指针就可以指向这样变量,当我们用的时候我们就可以从内存单元取出变量内容.
Objective-C对象的ARC是通过所有权修饰符来管理对象的持有和释放。所有权修饰符一共有4种:
2.1 __strong 修饰符
默认的修饰符,只要有一个强指针指向这个对象,这个对象就一直不会销毁,这个对象指向的指针也不会置为NULL.
//这里person_one 可以理解为一个指针 指向 Person创建的出来的对象(指针)的内存,可以读取内存上的内容
Person * __strong person_one = [[Person alloc]init];
Person * __strong person_two = person_one;
person_one = nil;
NSLog(@"person_one:%@,person_one地址:%p",person_one,person_one);
NSLog(@"person_two:%@,person_two地址:%p",person_two,person_two);
Log:
2018-03-19 16:19:09.822168 TestARC[16592:5864784] person_one:(null),person_one地址:0x0
2018-03-19 16:19:22.443524 TestARC[16592:5864784] person_two:<Person: 0x17001e450>,person_two地址:0x17001e450
我们可以看到,person_two是person_one的浅拷贝对象,也就是指针拷贝对象,而person_two是通过__strong修饰,相当于强指针,指向的是与person_one一块内存区域.而这块内存区域被retain了两次,引用计数为2,即使person_one = nil将引用计数-1了,person_two依然可以打印出内存地址.person_one的指针已经被置为NULL,所以打印出的地址是0x0.
2.2 __weak 修饰符
当没有强指针指向弱引用的对象时,弱引用的对象将被置为nil,对象的指针置为NULL.
//这里person_one 可以理解为一个指针 指向 Person创建的出来的对象(指针)的内存,可以读取内存上的内容
Person * __strong person_one = [[Person alloc]init];
Person * __weak person_two = person_one;
person_one = nil;
NSLog(@"person_one:%@,person_one地址:%p",person_one,person_one);
NSLog(@"person_two:%@,person_two地址:%p",person_two,person_two);
Log:
2018-03-19 16:28:21.453255 TestARC[16599:5866487] person_one:(null),person_one地址:0x0
2018-03-19 16:28:25.521762 TestARC[16599:5866487] person_two:(null),person_two地址:0x0
我们知道__weak修饰的对象不会对对象进行retain,所以person_two指向的内存区域对象引用计数还是1.这里只有person_one强引用那块内存区域,当person_one = nil时,引用计数为0,内存区域被释放,person_two指向的内存地址为:0x0.
2.3 __unsafe_unretained 修饰符
就像其表面意思一样:当没有强指针指向__unsafe_unretained修饰的对象时,这个对象会被置为nil,但是指向对象的指针不会被清空,苹果官方: the pointer is left dangling.
//这里person_one 可以理解为一个指针 指向 Person创建的出来的对象(指针)的内存,可以读取内存上的内容
Person * __strong person_one = [[Person alloc]init];
Person * __unsafe_unretained person_two = person_one;
person_one = nil;
NSLog(@"person_one:%@,person_one地址:%p",person_one,person_one);
NSLog(@"person_two:%@,person_two地址:%p",person_two,person_two);
Log:
2018-03-19 16:42:52.400375 TestARC[16608:5869804] person_one:(null),person_one地址:0x0
这里已经报错:Thread 1: EXC_BAD_ACCESS (code=1, address=0xb84d2beb8)
这里我们在主线程中收到一条崩溃信息(EXC_BAD_ACCESS),通过unsafe_unretained官方文档解释,我们可以猜出address=0xb84d2beb8应该是person_one没被置为nil之前的内存地址,而当person_one = nil时,这块内存已经被回收,而person_two因为被unsafe_unretained修饰,其指针还没有被销毁,还想指向这块内存地址,所以造成了野指针错误.
2.4 __autoreleasing 修饰符
autorelease 本质上就是延迟调用 release,这里不做细致的分析了,大家感兴趣的可以自己找相关资料查看.
到这里我们对ARC的引用计数管理应该有了大概的了解.
3. 源码分析
引用计数的实现,我们可以通过查看苹果的源码(https://opensource.apple.com/source/objc4/).我们下面主要来看看retain的实现源码,我们可以在OC的鼻祖类--NSObject中可以看到协议NSObject中定义的几个方法:
- (instancetype)retain OBJC_ARC_UNAVAILABLE;
- (oneway void)release OBJC_ARC_UNAVAILABLE;
- (instancetype)autorelease OBJC_ARC_UNAVAILABLE;
- (NSUInteger)retainCount OBJC_ARC_UNAVAILABLE;
以上方法,就是编译器在合适的时机给对象所要发送的消息.我们点进去retain方法,我们可以在NSObject.mm文件的2138行可以看到其实现:
// Replaced by ObjectAlloc
- (id)retain {
return ((id)self)->rootRetain();
}
沿着调用链,我们可以在objc-object.h文件中看到id rootRetain(bool tryRetain, bool handleOverflow)方法的实现:
LWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
assert(!UseGC);
if (isTaggedPointer()) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
isa_t oldisa;
isa_t newisa;
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (!newisa.indexed) goto unindexed;
// don't check newisa.fast_rr; we already called any RR overrides
if (tryRetain && newisa.deallocating) goto tryfail;
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
if (carry) {
// newisa.extra_rc++ overflowed
if (!handleOverflow) return rootRetain_overflow(tryRetain);
// Leave half of the retain counts inline and
// prepare to copy the other half to the side table.
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
}
} while (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits));
if (transcribeToSideTable) {
// Copy the other half of the retain counts to the side table.
sidetable_addExtraRC_nolock(RC_HALF);
}
if (!tryRetain && sideTableLocked) sidetable_unlock();
return (id)this;
tryfail:
if (!tryRetain && sideTableLocked) sidetable_unlock();
return nil;
unindexed:
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();
}
最后一行sidetable_retain(),这个也是retain方法的最终调用的方法.而sidetable_retain()的实现:
id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.indexed);
#endif
SideTable& table = SideTables()[this];
if (table.trylock()) {
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();
return (id)this;
}
return sidetable_retain_slow(table);
}
我们可以看到这个方法中SideTable这个结构体,
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
~SideTable() {
_objc_fatal("Do not delete SideTable.");
}
void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
bool trylock() { return slock.trylock(); }
// Address-ordered lock discipline for a pair of side tables.
template<bool HaveOld, bool HaveNew>
static void lockTwo(SideTable *lock1, SideTable *lock2);
template<bool HaveOld, bool HaveNew>
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
其中的 RefcountMap 应该就是引用计数哈希表,而weak_table_t则是弱引用表(weak table).
RefcountMap 则是一个简单的 map,其 key 为 object 内存地址,value 为引用计数值.通过SideTable源码,还可以得出如下结论:
存在全局的若干个SideTable实例,它们保存在 static 成员变量table_buf中;
程序运行过程中生成的所有对象都会通过其内存地址映射到table_buf中相应的 SideTable实例上.这里之所以会存在多个SideTable实例,object 映射到不同SideTable实例上,猜测是出于性能优化的目的,避免SideTable中的 reference table、weak table 过大.
回到上面的sidetable_retain方法,其首先通过 object 的地址找到对应的 sidetale,然后通过 RefcountMap将该 object 的引用计数加1.简单地说,Apple 通过全局的 map 来记录Reference Counting,其key 为 object 地址,value 为引用计数值。
release、retainCount等相关方法的代码在该开源代码中也能找到,这里不细说了.
4. ARC开发环境需要注意的管理内存:
4.1CoreFoundation,Runtime以及其他C语言库的使用
通过malloc,create,copy等创建对象,还需要手动释放.
4.2 循环引用
循环引用是两个或多个对象之间相互持有,形成环状,即使在没有外部对象指针指向这些对象内存区域(堆区)的时候,系统无法将每个对象的引用计数置为0,从而导致这些开辟出来的内存一直发挥着”占着茅坑不拉屎”的作用.这部分不容易检测,也容易背锅.不管新老司机遇到问题不假思索:循环引用的问题(所以遇到问题的时候,我们更多的是多思考,而不是在没有分析问题的情况下脱口而出,不仅误导别人,而且显得自己很水,多说了两句,见笑).
5. 内存管理检测
5.1 Analyze静态分析
静态内存分析, 指的是在程序没运行的时候, 通过预编译对代码进行预判断分析,分析代码的基本数据结构,语法等,编译器检查是否存在潜在的内存泄露及不规范的地方.常遇到问题:
1)The ‘viewWillDisappear:’ instance method in UIViewController subclass ‘xxxxx’ is missing a [super viewWillDisappear:] call;这个错误提示是:重写父类中的实例方法viewWillDisappear,没有在子类中调用,从下图我们可以看到确实是这样,-(void)viewWillDisappear:(BOOL)animated方法内部调用的是[super viewDidAppear:animated];这种是很低级的错误.
2)Value stored to ‘xxxxx’ is never read,声明的变量没有被用到
3) API Misuse 接口应用错误,这里主要针对的是系统提供的接口
从下图中我们可以看到,_cachedStatements是一个字典,字典是不允许出现nil对象的,所以存数据之前我们要做容错判断.
改完后就不再提示了
4)Memory error,内存错误:nil returned from a method that is expected to return a non-null value,方法返回中需要一个对象(指针),你返回了一个空指针.例如,下图在UITableView的数据源回调方法返回cell的方法中,本应返回一个UITableViewCell对象,可是这里返回了一个nil对象(空指针)
还存在其他潜在问题错误或者不规范的地方,大家可以照着这个自己去查找一下自己的项目.
5.2 Instruments内存泄露检测
Instruments内存分析你应用内存的使用情况,帮助你查找定位出现问题的代码区域.详细介绍可以参考apple developer documentation(https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/InstrumentsUserGuide/CommonMemoryProblems.html#//apple_ref/doc/uid/TP40004652-CH91-SW1)
从文档中我们大概可以看到,一个应用所使用的内存可能占三种:
Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument).
泄露的内存:应用无法再次应用或者释放的内存.
Abandoned memory: Memory still referenced by your application that has no useful purpose.
废弃的内存:你的应用还占据着这块内存,但是这块内存无法释放了,ARC中最有可能的是循环引用.
Cached memory: Memory still referenced by your application that might be used again for better performance.
缓存的内存:能够被你的应用正常释放回收利用的内存.
内存泄露:如果程序运行时一直分配内存而不及时释放无用的内存,程序占用的内存越来越大,直到把系统分配给该APP的内存消耗殚尽,程序因无内存可用导致崩溃,这样的情况我们称之为内存泄漏。可能引起的问题:
1)内存消耗殆尽的时候,程序会因没有内存被杀死,即crash。
2)当内存快要用完的时候,会非常的卡顿
3)如果是ViewController没有释放掉,引起的内存泄露,还会引起其他很多问题,尤其是和通知相关的。没有被释放掉的ViewController还能接收通知,还会执行相关的动作,所以会引起各种各样的异常情况的发生。
以我们现在开发的项目为例:这里打个广告,我们现在开发的应用叫做爱学.横版主要有我的班级,自学,消息,设置等模块,下面我们用Instruments来检查一下:
1)打开调试工具步骤:首先先将待检测的源码安装到你的真机设备上(Command + r 或者 直接Run运行);然后按着快捷键:Command + Control + i,打开Instruments,选择Leaks.
2)定位内存泄露区域
我们选择call_tree,也就是函数调用栈,顺藤摸瓜,找到内存泄露的地方
不出意外,就可以看到具体内存泄露的代码了,我们这里是由于使用Runtime了,调用了class_copyPropertyList方法.我们知道Runtime是OC的底层,是OC的幕后工作者,所写的OC代码最终都转换成Runtime的C代码执行.这里通过class_copyPropertyList方法来获取类的所有成员变量的时候,没有释放.所以在使用C语言相关库的时候,一定要做好释放工作(不然装B就装大了😄,玩笑).最终在使用遍历完类中的成员变量后,free(properties);就没问题了.
-(NSArray *)modelInfo:(Class)cls
{
unsigned int count = 0;
objc_property_t * properties= class_copyPropertyList(cls, &count);
NSMutableArray * infoarr = [NSMutableArray new];
for (int i = 0; i<count; i++)
{
objc_property_t property = properties[i];
NSString * name = [[NSString alloc]initWithCString:property_getName(property) encoding:NSUTF8StringEncoding ];
[infoarr addObject:name];
}
free(properties);
return infoarr;
}
我们的学习任务中一个视频类型的任务,视频播放器估计是从网上找的别人封装好的,没有细致分析就用了.从下图中我们可以看到至少有三个环,我们需要打破这种环状,消除引用循环,这里不细说,大家可以根据需要去详细看看怎么处理引用循环.
总结
文中简单介绍了iOS内存管理的相关内容,主要的还是ARC相关内容,这些大都是基于实际开发中的总结和平时学习的积累,里面不乏一些错误和不规范之处,希望没有没有大家没有被误导,更希望大家多给意见和建议.其实,基础知识扎牢了,对一些问题的理解,解决可能也会更加游刃有余,而不是天天纠结于一些”界面”上的问题.
参考文献
https://blog.devtang.com/2016/07/30/ios-memory-management/
https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/InstrumentsUserGuide/FindingLeakedMemory.html
http://clang.llvm.org/docs/AutomaticReferenceCounting.html