iOS性能调优之--内存管理

2019 年 1 月 10 日 CocoaChina

前言


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操作交给编译器去处理就行了.


MRC_ARC_示意图_来源_Apple_Document.jpg


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: 


strong所有权修饰.png


我们可以看到,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所有权修饰.png


 我们知道__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)


unsafe__unretained所有权修饰.png

 

这里我们在主线程中收到一条崩溃信息(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, 0sizeof(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,从而导致这些开辟出来的内存一直发挥着”占着茅坑不拉屎”的作用.这部分不容易检测,也容易背锅.不管新老司机遇到问题不假思索:循环引用的问题(所以遇到问题的时候,我们更多的是多思考,而不是在没有分析问题的情况下脱口而出,不仅误导别人,而且显得自己很水,多说了两句,见笑).


循环引用示意图.png


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];这种是很低级的错误.


Analyze_1.png


2)Value stored to 'xxxxx' is never read,声明的变量没有被用到


Analyze_2.png


3) API Misuse 接口应用错误,这里主要针对的是系统提供的接口


从下图中我们可以看到,_cachedStatements是一个字典,字典是不允许出现nil对象的,所以存数据之前我们要做容错判断.


Analyze_3_1.png


改完后就不再提示了


Analyze_3_2.png


4)Memory error,内存错误:nil returned from a method that is expected to return a non-null value,方法返回中需要一个对象(指针),你返回了一个空指针.例如,下图在UITableView的数据源回调方法返回cell的方法中,本应返回一个UITableViewCell对象,可是这里返回了一个nil对象(空指针)


Analyze_4.png


还存在其他潜在问题错误或者不规范的地方,大家可以照着这个自己去查找一下自己的项目.


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,也就是函数调用栈,顺藤摸瓜,找到内存泄露的地方


call_tree.png


memory_leak.png


不出意外,就可以看到具体内存泄露的代码了,我们这里是由于使用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;
}
</count; i++)


我们的学习任务中一个视频类型的任务,视频播放器估计是从网上找的别人封装好的,没有细致分析就用了.从下图中我们可以看到至少有三个环,我们需要打破这种环状,消除引用循环,这里不细说,大家可以根据需要去详细看看怎么处理引用循环.


retain_recycle_1.png


retain_recycle_2.png


retain_recycle_3.png


总结


文中简单介绍了iOS内存管理的相关内容,主要的还是ARC相关内容,这些大都是基于实际开发中的总结和平时学习的积累,里面不乏一些错误和不规范之处,希望没有没有大家没有被误导,更希望大家多给意见和建议.其实,基础知识扎牢了,对一些问题的理解,解决可能也会更加游刃有余,而不是天天纠结于一些"界面"上的问题.


参考文献


作者:偶尔登南山
链接:https://www.jianshu.com/p/cedc278f90ad


本公众号转载内容已尽可能注明出处,如未能核实来源或转发内容图片有权利瑕疵的,请及时联系本公众号进行修改或删除【联系方式QQ : 3442093904  邮箱:support@cocoachina.com】。文章内容为作者独立观点,不代表本公众号立场。版权归原作者所有,如申请授权请联系作者,因文章侵权本公众号不承担任何法律及连带责任。

---END---

登录查看更多
0

相关内容

编译器(Compiler),是一种计算机程序,它会将用某种编程语言写成的源代码(原始语言),转换成另一种编程语言(目标语言)。
【2020新书】实战R语言4,323页pdf
专知会员服务
97+阅读 · 2020年7月1日
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
56+阅读 · 2020年6月26日
【2020新书】C++20 特性 第二版,A Problem-Solution Approach
专知会员服务
56+阅读 · 2020年4月26日
【干货书】流畅Python,766页pdf,中英文版
专知会员服务
223+阅读 · 2020年3月22日
在K8S上运行Kafka合适吗?会遇到哪些陷阱?
DBAplus社群
9+阅读 · 2019年9月4日
浅谈 Kubernetes 在生产环境中的架构
DevOps时代
11+阅读 · 2019年5月8日
你真的会正确地调试 TensorFlow 代码吗?
数据库开发
7+阅读 · 2019年3月18日
iOS自定义带动画效果的模态框
CocoaChina
7+阅读 · 2019年3月3日
已删除
AI科技评论
4+阅读 · 2018年8月12日
浅显易懂的分布式TensorFlow入门教程
专知
7+阅读 · 2018年6月22日
优化哈希策略
ImportNew
5+阅读 · 2018年1月17日
TensorFlow seq2seq中的Attention机制(续)
深度学习每日摘要
15+阅读 · 2017年11月16日
前端高性能计算(4):GPU加速计算
前端大全
7+阅读 · 2017年10月26日
Arxiv
15+阅读 · 2018年2月4日
VIP会员
相关资讯
在K8S上运行Kafka合适吗?会遇到哪些陷阱?
DBAplus社群
9+阅读 · 2019年9月4日
浅谈 Kubernetes 在生产环境中的架构
DevOps时代
11+阅读 · 2019年5月8日
你真的会正确地调试 TensorFlow 代码吗?
数据库开发
7+阅读 · 2019年3月18日
iOS自定义带动画效果的模态框
CocoaChina
7+阅读 · 2019年3月3日
已删除
AI科技评论
4+阅读 · 2018年8月12日
浅显易懂的分布式TensorFlow入门教程
专知
7+阅读 · 2018年6月22日
优化哈希策略
ImportNew
5+阅读 · 2018年1月17日
TensorFlow seq2seq中的Attention机制(续)
深度学习每日摘要
15+阅读 · 2017年11月16日
前端高性能计算(4):GPU加速计算
前端大全
7+阅读 · 2017年10月26日
Top
微信扫码咨询专知VIP会员