iOS App 稳定性指标及监测

前言

本文较长(5000字左右),建议阅读时间: 20min+html

一个iOS App的稳定性,主要决定于总体的系统架构设计,同时也不可忽略编程的细节,正所谓“千里之堤,溃于蚁穴”,一旦考虑不周,看似可有可无的代码片断可能会带来总体软件系统的崩溃。尤为由于苹果限制了热更新机制,App自己的稳定性及容错性就显的更加剧要,以前能够经过发布热补丁的方式解决线上代码问题,如今就须要在提交以前对App开发周期内的各个指标进行实时监测,尽可能让问题暴漏在开发阶段,而后及时修复,减小线上出问题的概率。针对一个App的开发周期,它的稳定性指标主要有如下几个环节构成,用一个脑图表示以下: 前端

稳定性指标

1 开发过程

开发过程当中,主要是经过监控内存使用及泄露,CPU使用率,FPS,启动时间等指标,以及常见的UI的主线程监测,NSAssert断言等,最好能在Debug模式下,实时显示在界面上,针对出现的问题及早解决。ios

内存问题

内存问题主要包括两个部分,一个是iOS中常见循环引用致使的内存泄露 ,另外就是大量数据加载及使用致使的内存警告。git

mmap

虽然苹果并无明确每一个App在运行期间可使用的内存最大值,可是有开发者进行了实验和统计,通常在占用系统内存超过20%的时候会有内存警告,而超过50%的时候,就很容易Crash了,因此内存使用率仍是尽可能要少,对于数据量比较大的应用,能够采用分步加载数据的方式,或者采用mmap方式。mmap 是使用逻辑内存对磁盘文件进行映射,中间只是进行映射没有任何拷贝操做,避免了写文件的数据拷贝。 操做内存就至关于在操做文件,避免了内核空间和用户空间的频繁切换。以前在开发输入法的时候 ,词库的加载也是使用mmap方式,能够有效下降App的内存占用率,具体使用能够参考连接第一篇文章。github

循环引用

循环引用是iOS开发中常常遇到的问题,尤为对于新手来讲是个头疼的问题。循环引用对App有潜在的危害,会使内存消耗太高,性能变差和Crash等,iOS常见的内存主要如下三种状况:objective-c

Delegate

代理协议是一个最典型的场景,须要你使用弱引用来避免循环引用。ARC时代,须要将代理声明为weak是一个即好又安全的作法:算法

@property (nonatomic, weak) id <MyCustomDelegate> delegate;
复制代码
NSTimer

NSTimer咱们开发中会用到不少,好比下面一段代码数据库

- (void)viewDidLoad {
    [super viewDidLoad];

    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self
                                            selector:@selector(doSomeThing)
                                            userInfo:nil
                                            repeats:YES];
}

- (void)doSomeThing {
}

- (void)dealloc {
     [self.timer invalidate];
     self.timer = nil;
}
复制代码

这是典型的循环引用,由于timer会强引用self,而self又持有了timer,全部就形成了循环引用。那有人可能会说,我使用一个weak指针,好比编程

__weak typeof(self) weakSelf = self;
self.mytimer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(doSomeThing) userInfo:nil repeats:YES];
复制代码

可是其实并无用,由于无论是weakSelf仍是strongSelf,最终在NSTimer内部都会从新生成一个新的指针指向self,这是一个强引用的指针,结果就会致使循环引用。那怎么解决呢?主要有以下三种方式:api

  • 使用类方法
  • 使用weakProxy
  • 使用GCD timer

具体如何使用,我就不作具体的介绍,网上有不少能够参考。

Block

Block的循环引用,主要是发生在ViewController中持有了block,好比:

@property (nonatomic, copy) LFCallbackBlock callbackBlock;
复制代码

同时在对callbackBlock进行赋值的时候又调用了ViewController的方法,好比:

self.callbackBlock = ^{
    [self doSomething];
}];
复制代码

就会发生循环引用,由于:ViewController->强引用了callback->强引用了ViewController,解决方法也很简单:

__weak __typeof(self) weakSelf = self;
self.callbackBlock = ^{
  [weakSelf doSomething];
}];
复制代码

缘由是使用MRC管理内存时,Block的内存管理须要区分是Global(全局)、Stack(栈)仍是Heap(堆),而在使用了ARC以后,苹果自动会将全部本来应该放在栈中的Block所有放到堆中。全局的Block比较简单,凡是没有引用到Block做用域外面的参数的Block都会放到全局内存块中,在全局内存块的Block不用考虑内存管理问题。(放在全局内存块是为了在以后再次调用该Block时能快速反应,固然没有调用外部参数的Block根本不会出现内存管理问题)。

因此Block的内存管理出现问题的,绝大部分都是在堆内存中的Block出现了问题。默认状况下,Block初始化都是在栈上的,但可能随时被收回,经过将Block类型声明为copy类型,这样对Block赋值的时候,会进行copy操做,copy到堆上,若是里面有对self的引用,则会有一个强引用的指针指向self,就会发生循环引用,若是采用weakSelf,内部不会有强类型的指针,因此能够解决循环引用问题。

那是否是全部的block都会发生循环引用呢?其实否则,好比UIView的类方法Block动画,NSArray等的类的遍历方法,也都不会发生循环引用,由于当前控制器通常不会强引用一个类。

其余内存问题

1 NSNotification addObserver以后,记得在dealloc里面添加remove;

2 动画的repeat count无限大,并且也不主动中止动画,基本就等于无限循环了;

3 forwardingTargetForSelector返回了self。

内存解决思路:

1 经过Instruments来查看leaks

2 集成Facebook开源的FBRetainCycleDetector

3 集成MLeaksFinder

具体原理及使用,能够参考连接。

CPU使用率

CPU的使用也能够经过两种方式来查看,一种是在调试的时候Xcode会有展现,具体详细信息能够进入Instruments内查看,经过查看Instruments的time profile来定位并解决问题。另外一种常见的方法是经过代码读取CPU使用率,而后显示在App的调试面板上,能够在Debug环境下显示信息,具体代码以下:

int result;
mib[0] = CTL_HW;
mib[1] = HW_CPU_FREQ;
length = sizeof(result);
if (sysctl(mib, 2, &result, &length, NULL, 0) < 0)
{
 	perror("getting cpu frequency");
}
printf("CPU Frequency = %u hz\n", result);
复制代码

FPS监控

目前主要使用CADisplayLink来监控FPS,CADisplayLink是一个能让咱们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器。咱们在应用中建立一个新的 CADisplayLink 对象,把它添加到一个runloop中,并给它提供一个 target 和selector 在屏幕刷新的时候调用,须要注意的是添加到runloop的common mode里面,代码以下:

- (void)setupDisplayLink {
    _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTicks:)];
    [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
 
- (void)linkTicks:(CADisplayLink *)link
{
    //执行次数
    _scheduleTimes ++;
 
    //当前时间戳
    if(_timestamp == 0){
        _timestamp = link.timestamp;
    }
    CFTimeInterval timePassed = link.timestamp - _timestamp;
 
    if(timePassed >= 1.f)
        //fps
        CGFloat fps = _scheduleTimes/timePassed; 
        printf("fps:%.1f, timePassed:%f\n", fps, timePassed);
    }
}

复制代码

启动时间

点评App里面自己就包含了不少复杂的业务,好比外卖、团购、到综和酒店等,同时还引入了不少第三方SDK好比微信、QQ、微博等,在App初始化的时候,不少SDK及业务也开始初始化,这就会拖慢应用的启动时间。 以下主要参考了今日头条iOS客户端启动速度优化

App的启动时间t(App总启动时间) = t1(main()以前的加载时间) + t2(main()以后的加载时间)。 

t1 = 系统dylib(动态连接库)和自身App可执行文件的加载; 

t2 = main方法执行以后到AppDelegate类中的- (BOOL)Application:(UIApplication *)Application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法执行结束前这段时间,主要是构建第一个界面,并完成渲染展现。
复制代码

针对t1的优化,优化主要有以下:

  • 减小没必要要的framework,由于动态连接比较耗时;
  • 检查framework应当设为optional和required,若是该framework在当前App支持的全部iOS系统版本都存在,那么就设为required,不然就设为optional,由于optional会有些额外的检查;
  • 合并或者删减一些OC类,这些我会在后续的静态检查中进行详解;

针对t2的时间优化,能够采用:

  • 异步初始化部分操做,好比网络,数据读取;
  • 采用延迟加载或者懒加载某些视图,图片等的初始化操做;
  • 对与图片展现类的App,能够将解码的图片保存到本地,下次启动时直接加载解码后的图片;
  • 对实现了+load()方法的类进行分析,尽可能将load里的代码延后调用。

UI的主线程监测

咱们都知道iOS的UI的操做必定是在主线程进行,该监测能够经过hook UIView的以下三个方法

-setNeedsLayout,
-setNeedsDisplay,
-setNeedsDisplayInRect 
复制代码

确保它们都是在主线程执行。子线程操做UI可能会引发什么问题,苹果说得并不清楚,可是在实际开发中,咱们常常会遇到整个App的动画丢失,很大缘由就是UI操做不是在主线程致使。

2 静态分析过程

静态分析在这里,我主要介绍两方面,一个是正常的code review机制,另一个就是代码静态检查工具

code review

组内的code review机制,能够参考团队以前的OpenDoc - 前端团队CodeReview制度,iOS客户端开发,会在此基础上进行一些常见手误及Crash状况的重点标记,好比:

1 咱们开发中首先都是在测试环境开发,开发时能够将测试环境的url写死到代码中,可是在提交代码的时候必定要将他改成线上环境的url,这个就能够经过gitlab中的重点比较部分字符串,给提交者一个强力的提示;

2 其余常见Crash的重点检查,好比NSMutableString/NSMutableArray/NSMutableDictionary/NSMutableSet 等类下标越界判断保护,或者 append/insert/add nil对象的保护;

3 ARC下的release操做,UITableViewCell返回nil,以及前面介绍的常见的循环引用等。

code review机制,一方面是依赖写代码者的代码习惯及质量,另外一名依赖审查者的经验和细心程度,即便让多人revew,也可能会漏过一些错误,因此咱们又添加了代码的静态检查。

代码静态检查

代码静态分析(Static Program Analysis)是指在不运行程序的条件下,由代码静态分析工具自动对程序进行分析的方法. iOS常见的静态扫描工具备Clang Static Analyzer、OCLint、Infer,这些主要是用来检查可能存在的问题,还有Deploymate用来检查api的兼容性。

Clang Static Analyzer

Clang Static Analyzer是一款静态代码扫描工具,专门用于针对C,C++和Objective-C的程序进行分析。已经被Xcode集成,能够直接使用Xcode进行静态代码扫描分析,Clang默认的配置主要是空指针检测,类型转换检测,空判断检测,内存泄漏检测这种等问题。若是须要更多的配置,可使用开源的Clang项目,而后集成到本身的CI上。

OCLint

OCLint是一个强大的静态代码分析工具,能够用来提升代码质量,查找潜在的bug,主要针对 C、C++和Objective-C的静态分析。功能很是强大,并且是出自国人之手。OCLint基于 Clang 输出的抽象语法树对代码进行静态分析,支持与现有的CI集成,部署以后基本不须要维护,简单方便。

OCLint能够发现这些问题

  • 可能的bug - 空的 if / else / try / catch / finally 语句
  • 未使用的代码 - 未使用的局部变量和参数
  • 复杂的代码 - 高圈复杂度, NPath复杂, 高NCSS
  • 冗余代码 - 多余的if语句和无用的括号
  • 坏味道的代码 - 过长的方法和过长的参数列表
  • 很差的使用 - 倒逻辑和入参从新赋值

对于OCLint的与原理和部署方法,这里不作细讲解,主要是每次提交代码后,能够在打包的过程当中进行代码检查,及早发现有问题的代码。固然也能够在合并代码以前执行对应的检查,若是检查不经过,不能合并代码,这样检查的力度更大。

Infer

Infer facebook开源的静态分析工具,Infer能够分析 Objective-C, Java 或者 C 代码,报告潜在的问题。Infer效率高,规模大,几分钟能扫描数千行代码; C/OC中捕捉的bug类型主要有:

1:Resource leak
2:Memory leak
3:Null dereference
4:Premature nil termination argument
复制代码

只在 OC中捕捉的bug类型

1:Retain cycle
2:Parameter not null checked
3:Ivar not null checked
复制代码

结论

Clang Static Analyzer和Xcode集成度更高、更好用,支持命令行形式,而且可以用于持续集成。OCLint有更多的检查规则和定制,和不少工具集成,也一样可用于持续集成。Infer效率高,规模大,几分钟能扫描数千行代码;支持增量及非增量分析;分解分析,整合输出结果。infer能将代码分解,小范围分析后再将结果整合在一块儿,兼顾分析的深度和速度,因此根据本身的项目特色,选择合适的检查工具对代码进行检查,减小人力review成本,保证代码质量,最大限度的避免运行错误。

3 测试过程

前面介绍了不少指标的监测,代码静态检查,这些都是性能相关的,真正决定一个App功能稳定是否的是测试环节。测试是发布以前的最后一道卡,若是bug不能在测试中发现,那么最终就会触达用户,因此一个App的稳定性,很大程度决定它的测试过程。iOS App的测试包括如下几个层次:单元测试,UI测试,功能测试,异常测试。

单元测试

XCTest是苹果官方提供的单元测试框架,与Xcode集成在一块儿,由此苹果提供了很详细的文档XCTest。

Xcode单元测试包含在一个XCTestCase的子类中。依据约束,每个 XCTestCase 子类封装一个特殊的有关联的集合,例如一个功能、用例或者一个程序流。同时还提供了XCTestExpectation来处理异步任务的测试,以及性能测试measureBlock(),还包括不少第三方测试框架好比:KiWi,Quick,Specta等,以及经常使用的mock框架OCMock。

单元测试的目的是将程序中全部的源代码,隔离成最小的可测试单元,以确保每一个单元的正确性,若是每一个单元都能保证正确,就能保证应用程序总体至关程度的正确性。可是在实际的操做过程当中,不少公司都很难完全执行单元测试,主要就是单元测试代码量甚至大于功能开发,比较难于维护。

对于测试用例覆盖度多少合适这个话题,也是仁者见仁智者见智,其实一个软件覆盖度在50%以上就能够称为一个健壮的软件了,要达到70,80这些已是很是难了,不过咱们常见的一些第三方开源框架的测试用例覆盖率仍是很是高的,让人咋舌。例如,AFNNetWorking的覆盖率高达87%,SDWebImage的覆盖率高达77%。

UI测试

Xcode7中新增了UI Test测试,UI测试是模拟用户操做,进而从业务处层面测试,经常使用第三方库有KIF,appium。关于XCTest的UI测试,建议看看WWDC 2015的视频UI Testing in Xcode。 UI测试还有一个核心功能是UI Recording。选中一个UI测试用例,而后点击图中的小红点既能够开始UI Recoding。你会发现:随着点击模拟器,自动合成了测试代码。(一般自动合成代码后,还须要手动的去调整)

UI测试

功能测试

功能测试跟上述的UT和UI测试有一些相通的地方,首先针对各个模块设计的功能,测试是否达到产品的目的,一般功能测试主要是测试及产品人员,而后还须要进行专项测试,好比咱们公司的云测平台,会对整个App的性能,稳定性,UI等都进行总体评测,看是否达到标准,对于大规模的活动,还须要进行服务端的压力测试,确保整个功能无异常。测试经过后,能够进行estFlight测试,到最后正式发布。

功能测试还包括以下场景:系统兼容性测试,屏幕分辨率兼容性测试,覆盖安装测试,UI是否符合设计,消息推送等,以及前面开发过程当中须要监控的内存、cpu、电量、网络流量、冷启动时间、热启动时间、存储、安装包的大小等测试。

异常测试

异常测试主要是针对一些不常规的操做

  • 使用过程当中的来电时及结束后,界面显示是否正常;
  • 状态栏为两倍高度时,界面是否显示正常;
  • 意外断电后,数据是否保存,数据是否有损害等;
  • 设备充电时,不一样电量时的App响应速度及操做流畅度等;
  • 其余App的相互切换,先后台转换时,是否正常;
  • 网络变化时的提示,弱网环境下的网络请求成功率等;
  • 各类monkey的随机点击,多点触摸测试等是否正常;
  • 更改系统时间,字体大小,语言等显示是否正常;
  • 设备存储不够时,是否能正常操做;
  • ...

异常测试有不少,App针对自身的特色,能够选择性的进行边界和异常测试,也是保证App稳定行的一个重要方面。

4 发布及监控

由于移动App的特色,即便咱们经过了各类测试,产品最终发布后,仍是会遇到不少问题,好比Crash,网络失败,数据损坏,帐号异常等等。针对已经发布的App,主要有一下方式保证稳定性:

热修复

目前比较流行的热修复方案都是基于JSPatch、React Native、Weex、lua+wax。

JSPatch能作到经过js调用和改写OC方法。最根本的缘由是 Objective-C 是动态语言,OC上全部方法的调用/类的生成都经过 objective-c Runtime 在运行时进行,咱们能够经过类名和方法名反射获得相应的类和方法,也能够替换某个类的方法为新的实现,还能够新注册一个类,为类添加方法。JSPatch 的原理就是:JS传递字符串给OC,OC经过 Runtime 接口调用和替换OC方法。

React Native 是从 Web 前端开发框架 React 延伸出来的解决方案,主要解决的问题是 Web 页面在移动端性能低的问题,React Native 让开发者能够像开发 Web 页面那样用 React 的方式开发功能,同时框架会经过 JavaScript 与 Objective-C 的通讯让界面使用原生组件渲染,让开发出来的功能拥有原生App的性能和体验。

Weex阿里开源的,基于Vue+Native的开发模式,跟RN的主要区别就在React和Vue的区别,同时在RN的基础上进行了部分性能优化,整体开发思路跟RN是比较像的。

可是在今年上半年,苹果以安全为理由,开始拒绝有热修复功能的应用,但其实苹果拒的不是热更新,拒的是从网络下载代码并修改应用行为,苹果禁止的是“基于反射的热更新“,而不是 “基于沙盒接口的热更新”。而大部分框架(如 React Native、weex)和游戏引擎(好比 Unity、Cocos2d-x等)都属于后者,因此不在被警告范围内。而JSPatch由于在国内大部分应用来作热更新修复bug的行为,因此才回被苹果禁止。

降级

用户使用App一段时间后,可能会遇到这样的状况:每次打开App时闪退,或者正常操做到某个界面时闪退,没法正常使用App。这样的用户体验十分糟糕,若是没有一个好的解决方案,很容易被用户删除App,致使用户量的流失。由于热更新基本不能使用,那就只能是App自身修复能力。目前经常使用的修复能力有:

  • 启动Crash的监控及修复

1 在应用起来的时候,记录flag并保存本地,启动一个定时器,好比5秒钟内,若是没有发生Crash,则认为用户操做正常,清空本地flag。

2 下次启动,发现有flag,则代表上次启动Crash,若是flag数组越大,则说明Crash的次数越多,这样就须要对整个App进行降级处理,好比登出帐号,清空Documents/Library/Caches目录下的文件。

  • 具体业务下的Crash及修复

针对某些具体业务Crash场景,若是是上线的前端页面引发的,能够先对前端功能进行回滚,或者隐藏入口,等修复完毕后再上线,若是是客户端的某些异常,好比数据库升迁问题,主要是进行业务数据库修复,缓存文件的删除,帐号退出等操做,尽可能只修复此业务的相关的数据。

  • 网络降级

好比点评App,自己有CIP(公司内部本身研发的)长链接,接入腾讯云的WNS长链接,UDP链接,HTTP短链接,若是CIP服务器发生问题,能够及时切换到WNS链接,或者降级到Http链接,保证网络链接的成功率。

线上监控

Crash监控

Crash是对用户来讲是最糟糕的体验,Crash日志可以记录用户闪退的崩溃日志及堆栈,进程线程信息,版本号,系统版本号,系统机型等有用信息,收集的信息越详细,越可以帮助解决崩溃,因此各大App都有本身崩溃日志收集系统,或者也可使用开源或者付费的第三方Crash收集平台。

端到端成功率监控

端到端监控是从客户端App发出请求时计时,到App收到数据数据的成功率,统计对象是:网络接口请求(包括H5页面加载)的成败和端到端延时状况。端到端监控SDK提供了监控上传接口,调用SDK提供的监控API能够将数据上报到监控服务器中。

整个端到端监控的能够在多个维度上作查询端到端成功率、响应时间、访问量的查询,维度包括:返回码、网络、版本、平台、地区、运营商等。

用户行为日志

用户行为日志,主要记录用户在使用App过程当中,点击元素的时间点,浏览时长,跳转流程等,而后基于此进行用户行为分析,大部分应用的推荐算法都是基于用户行为日志来统计的。某些状况下,Crash分析须要查询用户的行为日志,获取用户使用App的流程,帮助解决Crash等其余问题。

代码级日志

代码级别的日志,主要用来记录一个App的性能相关的数据,好比页面打开速度,内存使用率,CPU占用率,页面的帧率,网络流量,请求错误统计等,经过收集相关的上下文信息,优化App性能。

总结

虽然如今市面上第三方平台已经很成熟,可是各大互联公司都会本身开发线上监控系统,这样保证数据安全,同时更加灵活。由于移动用户的特色,在开发测试过程当中,很难彻底覆盖全部用户的所有场景,有些问题也只会在特定环境下才发生,因此经过线上监控平台,经过日志回捞等机制,及时获取特定场景的上下文环境,结合数据分析,可以及时发现问题,并后续修复,提升App的稳定性。

全文总结

本文主要从开发测试发布等流程来介绍了一个App稳定性指标及监测方法,开发阶段主要针对一些比较具体的指标,静态检查主要是扫描代码潜在问题,而后经过测试保证App功能的稳定性,线上降级主要是在尽可能不发版的状况下,进行自修复,配合线上监控,信息收集,用户行为记录,方便后续问题修复及优化。本文观点是做者从事iOS开发的一些经验,但愿能对你有所帮助,观点不一样欢迎讨论。

参考:

微信mars 的高性能日志模块 xlog 基于 CADisplayLink 的 FPS 指示器详解 今日头条iOS客户端启动速度优化 微信读书 iOS 性能优化总结 移动端监控体系之技术原理剖析 美团点评移动网络优化实践 iOS 启动连续闪退保护方案 微信 SQLite 数据库修复实践