性能一直是老生常谈的一个问题,这一块内容很大,最近看了《小专栏》中关于性能方面的文章,内容很多,可能会分成很多篇来总结,这一篇先来总结一下可以从哪些方面来入手。
在开发和测试阶段,我们可能不会过多的顾虑性能测试带来的性能损耗,这时候我们可以利用一些工具来进行针对性的测试:
内存泄漏检测
内存大图片检测
图片主线程解压缩检测
卡顿检测
帧率检测
线上的各种指标监控:
网络性能监控
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目前能自动检测 UIViewController
和UIView
对象的内存泄露,而且也可以扩展以检测其它类型的对象。
新版的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 <FBRetainCycleDetector/FBRetainCycleDetector.h>
@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 通常是结合其它工具一起使用,通过其它工具先找出候选的检测对象,然后进行有选择的检测。当 MLeaksFinder 与 FBRetainCycleDetector 结合使用时,正好能达到很好的效果。我们先通过 MLeaksFinder 找到内存泄漏的对象,然后再过 FBRetainCycleDetector 检测该对象有没有循环引用即可。
Demo地址:https://github.com/MichealMIX/FBRetainCycleDetectorTestDemo
FBAllocationTracker
主要用于快速检测潜在的内存泄漏对象,并提供给 FBRetainCycleDetector 进行检测
这是一个用来主动追踪所有 NSObject 的子类的内存分配和释放操作的工具。
FBAllocationTracker 用于检测应用在运行时所有实例的分配。它的原理其实就是用 method swizzling 替换原本的 alloc 方法。这样就可以记录下所有的实例分配了。
我们可以直接使用Pod来导入FBAllocationTracker
pod 'FBAllocationTracker'
注意FBAllocationTracker的开启是写在main.m
的文件中,而不是大多数写在appdelegate
中
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import <FBAllocationTracker/FBAllocationTrackerManager.h>
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实例(
"<NSObject: 0x600002c48520>",
"<NSObject: 0x600002c484e0>"
)
我们也可以查看其他类型的实例:
[[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实例(
"<UIView: 0x7f89f9709c80; frame = (0 0; 0 0); layer = <CALayer: 0x600002e71dc0>>"
)
有了这一款工具,我们就可以实时监测,查看对应的实例对象,获取到的实例对象将会提供给FBRetainCycleDetector
进行循环引用的检测。
Demo地址:https://github.com/MichealMIX/FBAllocationTrackerTestDemo.git
FBMemoryProfiler
可视化工具,直接嵌入到 App 中,可以起到在 App 中直接查看内存使用情况,并筛选潜在泄漏对象的作用。
当我们Pod导入FBMemoryProfiler时,FBRetainCycleDetector和FBAllocationTracker也会一同导入到项目中,它会使用FBAllocationTracker检测对象内存情况,通过FBRetainCycleDetector来查找内存泄漏。
步骤一:将FBMemoryProfiler使用Pod导入到工程中
pod 'FBMemoryProfiler'
步骤二:在main.m
文件中添加如下代码
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#if DEBUG
#import <FBAllocationTracker.h>
#import <FBAssociationManager.h>
#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
造成的内存泄漏问题,大家可以下载下来试一下,不过在检测Time
r时一定要去appdelegate
中,将是否检查计时器设置为YES。