APP的性能如何保证:内存泄漏

性能一直是老生常谈的一个问题,这一块内容很大,最近看了《小专栏》中关于性能方面的文章,内容很多,可能会分成很多篇来总结,这一篇先来总结一下可以从哪些方面来入手。

在开发和测试阶段,我们可能不会过多的顾虑性能测试带来的性能损耗,这时候我们可以利用一些工具来进行针对性的测试:

内存泄漏检测
内存大图片检测
图片主线程解压缩检测
卡顿检测
帧率检测

线上的各种指标监控:

网络性能监控
Crash监控
Abort 监控 (指由于 jetsam 机制杀死 APP,或者主线程长时间未反应被 watchdog 杀死等情况,这些情况不具有堆栈,定位困难)
基于页面级别的内存消耗监控
卡顿监控
DNS解析情况监控
启动时间监控

这一篇先从内存泄漏开始记录

在iOS中内存泄漏的场景无非就有那么几种,重型对象在该释放时没有被释放,依旧存在在内存中,我们在Xcode中有Instruments工具进行检测,但是如果已经发生内存泄漏了之后再去检测,就已经晚了,现在在iOS中有两个工具对内存泄漏的监控很高效,一个是腾讯的MLeaksFinder,另外的就是Facebook的三件套:

FBRetainCycleDetector(用于检测循环引用)
FBAllocationTracker(主要用于快速检测潜在的内存泄漏对象,并提供给 FBRetainCycleDetector 进行检测)
FBMemoryProfiler(可视化工具,直接嵌入到 App 中,可以起到在 App 中直接查看内存使用情况,并筛选潜在泄漏对象的作用)

我们依次看一下,先看看微信团队开发的MLeaksFinder

一个app的内存分为三类:

1.Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument)
泄漏内存:应用程序未引用的内存,不能再次使用或释放(也可以使用泄漏工具检测)

2.Abandoned memory: Memory still referenced by your application that has no useful purpose.
废弃内存:应用程序仍然引用的内存,没有任何用处。

3.Cached memory: Memory still referenced by your application that might be used again for better performance.
缓存内存:应用程序仍然引用的内存,可能会再次使用以获得更好的性能。

其中 Leaked memory 和 Abandoned memory 都属于应该释放而没释放的内存,都是内存泄露,而 Leaks 工具只负责检测 Leaked memory,而不管 Abandoned memory。在 MRC 时代 Leaked memory 很常见,因为很容易忘了调用 release,但在 ARC 时代更常见的内存泄露是循环引用导致的 Abandoned memory,Leaks 工具查不出这类内存泄露,应用有限

对于 Abandoned memory,可以用 Instrument 的 Allocations 检测出来。检测方法是用 Mark Generation 的方式,当你每次点击 Mark Generation 时,Allocations 会生成当前 App 的内存快照,而且 Allocations 会记录从上回内存快照到这次内存快照这个时间段内,新分配的内存信息。

我们可以不断重复 push 和 pop 同一个 UIViewController,理论上来说,push 之前跟 pop 之后,app 会回到相同的状态。因此,在 push 过程中新分配的内存,在 pop 之后应该被 dealloc 掉,除了前几次 push 可能有预热数据和 cache 数据的情况。如果在数次 push 跟 pop 之后,内存还不断增长,则有内存泄露。因此,我们在每回 push 之前跟 pop 之后,都 Mark Generation 一下,以此观察内存是不是无限制增长。

用这种方法来发现内存泄露还是很不方便的:
1.首先,你得打开 Allocations
2.其次,你得一个个场景去重复的操作
3.无法及时得知泄露,得专门做一遍上述操作,十分繁琐

MLeaksFinder提供了内存泄露检测更好的解决方案。只需要引入MLeaksFinder,就可以自动在 App 运行过程检测到内存泄露的对象并立即提醒,无需打开额外的工具,也无需为了检测内存泄露而一个个场景去重复地操作。MLeaksFinder目前能自动检测 UIViewControllerUIView 对象的内存泄露,而且也可以扩展以检测其它类型的对象。

新版的MLeaksFinder会弹窗提示是哪一个控制器出现了内存泄漏的问题,原来版本的则会直接使项目中断。

内存泄漏

从 MLeaksFinder 的使用方法可以看出,MLeaksFinder 具备以下优点:
1.使用简单,不侵入业务逻辑代码,不用打开 Instrument
2.不需要额外的操作,你只需开发你的业务逻辑,在你运行调试时就能帮你检测
3.内存泄露发现及时,更改完代码后一运行即能发现(这点很重要,你马上就能意识到哪里写错了)
4.精准,能准确地告诉你哪个对象没被释放

原理

MLeaksFinder 一开始从 UIViewController 入手。我们知道,当一个 UIViewController 被 pop 或 dismiss 后,该 UIViewController 包括它的 view,view 的 subviews 等等将很快被释放(除非你把它设计成单例,或者持有它的强引用,但一般很少这样做)。于是,我们只需在一个 ViewController 被 pop 或 dismiss 一小段时间后,看看该 UIViewController,它的 view,view 的 subviews 等等是否还存在。

具体的方法是,为基类 NSObject 添加一个方法 -willDealloc 方法,该方法的作用是,先用一个弱指针指向 self,并在一小段时间(3秒)后,通过这个弱指针调用 -assertNotDealloc,而 -assertNotDealloc 主要作用是直接中断言。

- (BOOL)willDealloc {
    __weak id weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [weakSelf assertNotDealloc];
    });
    return YES;
}
- (void)assertNotDealloc {
     NSAssert(NO, @“”);
}

这样,当我们认为某个对象应该要被释放了,在释放前调用这个方法,如果3秒后它被释放成功,weakSelf 就指向 nil,不会调用到 -assertNotDealloc 方法,也就不会中断言,如果它没被释放(泄露了),-assertNotDealloc 就会被调用中断言。这样,当一个 UIViewController 被 pop 或 dismiss 时(我们认为它应该要被释放了),我们遍历该 UIViewController 上的所有 view,依次调 -willDealloc,若3秒后没被释放,就会中断言。

Facebook三件套

FBRetainCycleDetector

FBRetainCycleDetector用以检测循环引用,可以检测NSObject的循环引用、关联对象(Associated Object)的循环引用、block的循环引用。

循环引用的三种常见情景:

1.两个对象的相互持有

ClassA *aObj = [ClassA new];
ClassB *bObj = [ClassB new];

aObj.b = bObj;
bObj.a = aObj;

2.block中的循环引用

@interface DetailViewController ()
{
    void (^_handlerBlock)();
}
@end

@implementation DetailViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    _handleBlock = ^{
        NSLog(@"%@", self);
    };
}
@end

3.通过成为正在运行的定时器的Target而被持有。这种情况可能许多初学者都不太会注意到,比如在一个页面启动定时器后,喜欢在dealloc中写上定时器的invalidate方法。其实只要定时器不停止,该页面就不会销毁,更不会执行dealloc方法

往往因为循环引用造成的内存泄漏会被我们忽略,Facebook发布了一个叫FBRetainCycleDetector的工具,专门用于检测对象是否存在引用循环。

这个工具能够检测指定对象的引用情况,并把所存在的引用循环中各对象和引用在终端进行打印。

#import "ViewController.h"
#import 

@interface ViewController ()

@property (nonatomic, strong) NSTimer *timer;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self blockRetainCycle];
    [self timerRetainCycle];
    
    [self testRetainCycle];
}

- (void)blockRetainCycle {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@", self);
    });
}

- (void)timerRetainCycle {
    _timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"timer");
    }];
}

- (void)testRetainCycle {
    FBRetainCycleDetector *detector = [[FBRetainCycleDetector alloc] init];
    [detector addCandidate:self];
    NSSet *retainCycles = [detector findRetainCycles];
    NSLog(@"%@", retainCycles);
}

@end
{(
        (
        "-> DetailViewController ",
        "-> _handlerBlock -> __NSMallocBlock__ "
    )
)}

很明显,DetailViewController通过_handlerBlock实例变量引用一个Block对象,而该Block又引用了DetailViewController对象。如果不存在引用循环的话,打印的结果将是空的。

很多循环引用是 block 的使用不当造成的。而 FBRetainCycleDetector 最大的技术亮点,正在于如何找出一个 block 的所有强引用对象。

但是FBRetainCycleDetector有两个缺点
1.需要找到候选的检测对象
2.检测循环引用比较耗时

正是由于这两个问题,FBRetainCycleDetector 通常是结合其它工具一起使用,通过其它工具先找出候选的检测对象,然后进行有选择的检测。当 MLeaksFinderFBRetainCycleDetector 结合使用时,正好能达到很好的效果。我们先通过 MLeaksFinder 找到内存泄漏的对象,然后再过 FBRetainCycleDetector 检测该对象有没有循环引用即可。

Demo地址:https://github.com/MichealMIX/FBRetainCycleDetectorTestDemo

FBAllocationTracker

主要用于快速检测潜在的内存泄漏对象,并提供给 FBRetainCycleDetector 进行检测
这是一个用来主动追踪所有 NSObject 的子类的内存分配和释放操作的工具。

FBAllocationTracker 用于检测应用在运行时所有实例的分配。它的原理其实就是用 method swizzling 替换原本的 alloc 方法。这样就可以记录下所有的实例分配了。

我们可以直接使用Pod来导入FBAllocationTracker

pod 'FBAllocationTracker'

注意FBAllocationTracker的开启是写在main.m的文件中,而不是大多数写在appdelegate

#import 
#import "AppDelegate.h"
#import 
int main(int argc, char * argv[]) {
    [[FBAllocationTrackerManager sharedManager] startTrackingAllocations];
    [[FBAllocationTrackerManager sharedManager] enableGenerations];
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

先来看一下使用代码

    // We are marking new generation
    [[FBAllocationTrackerManager sharedManager] markGeneration];
    
    // Objects b and c will be kept in second generation at index 1
    NSObject *b = [NSObject new];
    NSObject *c = [NSObject new];
    
    [[FBAllocationTrackerManager sharedManager] markGeneration];
    
    // Object d will be kept in third generation at index 2
    NSObject *d = [NSObject new];
    
    NSArray *instances =[[FBAllocationTrackerManager sharedManager] instancesForClass:[NSObject class]
                                                                         inGeneration:1];
    
    NSLog(@"第一代NSObject实例%@",instances);

当我们调用[[FBAllocationTrackerManager sharedManager] markGeneration]方法,FBAllocationTracker便会开始记录这一代之内所存在的内存,直到下一个[[FBAllocationTrackerManager sharedManager] markGeneration]方法调用,这个区间便为一代。

我们使用[[FBAllocationTrackerManager sharedManager] instancesForClass:[NSObject class] inGeneration:1]方法可以获取特定一代之内,特定对象类型的实例。

如代码所示我们将会得到第一代,类型为NSObject的实例数组:

2019-12-02 10:04:12.155317+0800 TrackDemo[75146:119147815] 第一代NSObject实例(
    "",
    ""
)

我们也可以查看其他类型的实例:

 [[FBAllocationTrackerManager sharedManager] markGeneration];
    
    NSObject *e = [NSObject new];
    
    UIView *f = [UIView new];
    
    NSArray *instances1 =[[FBAllocationTrackerManager sharedManager] instancesForClass:[UIView class]
                                                                         inGeneration:3];
    
    NSLog(@"第三代UIView实例%@",instances1);

结果如下:

2019-12-02 10:04:12.155610+0800 TrackDemo[75146:119147815] 第三代UIView实例(
    ">"
)

有了这一款工具,我们就可以实时监测,查看对应的实例对象,获取到的实例对象将会提供给FBRetainCycleDetector进行循环引用的检测。

Demo地址:https://github.com/MichealMIX/FBAllocationTrackerTestDemo.git

FBMemoryProfiler

可视化工具,直接嵌入到 App 中,可以起到在 App 中直接查看内存使用情况,并筛选潜在泄漏对象的作用。

当我们Pod导入FBMemoryProfiler时,FBRetainCycleDetectorFBAllocationTracker也会一同导入到项目中,它会使用FBAllocationTracker检测对象内存情况,通过FBRetainCycleDetector来查找内存泄漏。

步骤一:将FBMemoryProfiler使用Pod导入到工程中

pod 'FBMemoryProfiler'

步骤二:在main.m文件中添加如下代码

#import 
#import "AppDelegate.h"

#if DEBUG
#import 
#import 
#endif

int main(int argc, char * argv[]) {
    @autoreleasepool {
#if DEBUG
        //hook关联对象
        [FBAssociationManager hook];
        //跟踪内存实例
        [[FBAllocationTrackerManager sharedManager] startTrackingAllocations];
        //启用“代”
        [[FBAllocationTrackerManager sharedManager] enableGenerations];
#endif
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

步骤三:在AppDelegate.m添加如下代码

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    
#if DEBUG
    NSArray *filters = @[FBFilterBlockWithObjectIvarRelation([UIView class], @"_subviewCache"),
                         FBFilterBlockWithObjectIvarRelation([UIPanGestureRecognizer class], @"_internalActiveTouches")];
    //此项可以过滤指定的情况,例如subview缓存,手势相关可能引起内存的内存问题,可以选择[图片上传中...(20191203145046.jpg-60b31-1575355942010-0)]
是否检查定时器
    FBObjectGraphConfiguration *configuration =
    [[FBObjectGraphConfiguration alloc] initWithFilterBlocks:filters
                                         shouldInspectTimers:NO];
    
    memoryProfiler = [[FBMemoryProfiler alloc] initWithPlugins:@[[CacheCleanerPlugin new],
                                                                 [RetainCycleLoggerPlugin new]]
                              retainCycleDetectorConfiguration:configuration];
    [memoryProfiler enable];
#endif
    
    return YES;
}

使用演示:

首先我们打开demo,然后Mark Gen,点击第一行进入控制器,然后再退出,可以发现Gen2中仍旧存在这个控制器,并且未释放,我们可以点击这个控制器名称。

这个控件就会告诉我们,控制器中哪里存在内存泄漏问题,很显然是因为Block的循环引用造成了内存泄漏问题。

Demo中我还写了因Timer造成的内存泄漏问题,大家可以下载下来试一下,不过在检测Timer时一定要去appdelegate中,将是否检查计时器设置为YES

Demo地址:https://github.com/MichealMIX/FBProfilerDemo

你可能感兴趣的:(APP的性能如何保证:内存泄漏)