81

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

 5 years ago
source link: http://www.cocoachina.com/ios/20190108/26067.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

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

NFFVZbM.jpg!web

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: 0x17001e450>,person_two地址: 0x17001e450
qM3umi6.png!web

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
FvyqyaE.png!web

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)
Nn2Y7vf.png!web

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

zeyU7rQ.png!web

循环引用示意图.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];这种是很低级的错误.

6ZjUBjU.png!web

Analyze_1.png

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

eqEvMzv.png!web

Analyze_2.png

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

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

JnI3euQ.png!web

Analyze_3_1.png

改完后就不再提示了

2I7R3yY.png!web

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对象(空指针)

JnQnmeb.png!web

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

IBVBBrJ.png!web

call_tree.png

YbERFrV.png!web

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
{
objc_property_t property = properties[i];
NSString * name = [[ NSString alloc]initWithCString:property_getName(property) encoding: NSUTF8StringEncoding ];

[infoarr addObject:name];
}
free(properties);
return infoarr;
}

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

iERbumE.png!web

retain_recycle_1.png

iEzI3mU.png!web

retain_recycle_2.png

EVFjQ3a.png!web

retain_recycle_3.png

总结

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

参考文献

作者:偶尔登南山

链接:https://www.jianshu.com/p/cedc278f90ad


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK