澳门新葡新京 > 关于我们 > 小心NSTimer中的内存泄漏,全面的理解和分析IOS的崩溃日志

小心NSTimer中的内存泄漏,全面的理解和分析IOS的崩溃日志
2020-05-05 19:38

  • 可扩展标记语言
  • XML的特点,出身名门,W3C制定,微软和IBM曾经共同大力推荐过的数据格式
  • XML 指可扩展标记语言(eXtensible Markup Language)
    • 被设计用来传输和存储数据
    • HTML 是设计用来表示页面的

写在前面:本文会在最开头将苹果官方的文档Understanding and Analyzing Application Crash Reports进行翻译,但这不仅仅是一篇翻译的文章,本文会让大家更加全面的了解ios的崩溃报告的获取、分析、用途。翻译的时候我会结合自己以往的使用经验来进行翻译。

随着手机屏幕的变大,原来右滑返回略显不够人性化,尤其是对手小的朋友,让我如何单手玩手机.对于app要全屏右滑或保持原生边缘触发,各有说辞,这里不讨论其好坏.

NSTimer大家都很熟悉,觉得用起来也很简单。然而,由NSTimer引起的内存泄漏,不经历过一次,一般很难察觉到。下面看一段代码:

SAX解析

SAX是iOS默认的解析XML的方式,simple API for XML . 是一种占用内存非常低,但是只能读取不能写入的解析方式.因为他是一行一行的解析的.

  • 准备一个模型以及两个属性接收解析出来的数据.
#import "ViewController.h"//导入模型类#import "VideoModel.h"//遵守代理协议@interface ViewController ()<NSXMLParserDelegate>//用于保存模型的数组@property(nonatomic,strong)NSMutableArray <VideoModel *>*modelArr;//用于临时保存解析出来的数据.@property(nonatomic,strong)NSMutableString *mStr;@end
  • NSXMLParse 类进行解析. 主要通过实现对象代理方法来解析.比较复杂.
@implementation ViewController- viewDidLoad { [super viewDidLoad]; //URL 加载本地Apache服务器的数据解析 NSURL *url = [NSURL URLWithString:@"http://127.0.0.1/videos.xml"]; //通过Session自动开启线程进行异步任务. [[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { //创建xml解析器 NSXMLParser *parser = [[NSXMLParser alloc]initWithData:data]; //设置代理 parser.delegate = self; //解析开始 [parser parse]; }] resume]; }//初始化可变数组- (NSMutableArray *)modelArr{ if (_modelArr == nil) { _modelArr = [NSMutableArray array]; } return _modelArr;}//初始化可变字符串- (NSMutableString *)mStr{ if (_mStr == nil) { _mStr = [NSMutableString string]; } return _mStr;}
  • 真正用于解析的代理方法 只用这5个,前后两对方法,加上中间一个获取数据的方法.
/** 开始解析 */- parserDidStartDocument:(NSXMLParser *)parser;{ //这里只是开始,貌似不用做什么}/** 开始一个新标签,这个时候应该创建对应的模型对象或者准备为模型的属性赋值. @param parser 解析器 @param elementName 标签元素名字 @param attributeDict 标签的属性 */- parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(nullable NSString *)namespaceURI qualifiedName:(nullable NSString *)qName attributes:(NSDictionary<NSString *, NSString *> *)attributeDict;{ //看一下xml的结构,决定这里是干什么.对应当前的xml应该是判断后创建模型对象 /* <videos> <video videoId="1"> <name>01.C语言-语法预览</name> <length>320</length> <videoURL>/itcast/videos/01.C语言-语法预览.mp4</videoURL> <imageURL>/itcast/images/head1.png</imageURL> <desc>C语言-语法预览</desc> <teacher>李雷</teacher> </video> */ if ([elementName isEqualToString:@"video"]) { //创建新的模型对象 VideoModel *model = [VideoModel new]; //取出属性,为videoId赋值 model.videoId = @(attributeDict[@"videoId"].intValue); //将模型保存到数组 [self.modelArr addObject:model]; } }/** 解析到标签中间的文字 标签中的文字不是一次性能读完的,可能会分几次调用这个方法,所以创建一个可变字符串保存起来. @param parser 解析器 @param string 文字 */- parser:(NSXMLParser *)parser foundCharacters:(NSString *)string;{ [self.mStr appendString:string];}/** 解析到一个元素结束的地方. @param parser 解析器 @param elementName 元素名字 */- parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(nullable NSString *)namespaceURI qualifiedName:(nullable NSString *)qName;{ //进行判断,外层标签不进行KVC,否则崩溃.如果标签过多也可以重写model的方法 里面什么都不做就可以避免KVC报错 //- setValue:value forUndefinedKey:(NSString *)key if (![elementName isEqualToString:@"video"] && ![elementName isEqualToString:@"videos"]) { //获取当前model VideoModel *model = self.modelArr.lastObject; //属性的值就是解析出来的string,key则是标签的名字 [model setValue:self.mStr forKey:elementName]; } //最后对mStr进行清空,准备进行下一个标签的解析 self.mStr.string = @"";}/** 结束解析 */- parserDidEndDocument:(NSXMLParser *)parser;{ //所有标签解析完毕,打印数组看看是否转换成功. NSLog(@"%@",self.modelArr);}@end
  • 当然写完以后一定记得封装到对应的模型中,创建模型方法.那么在控制器中一句代码就搞定了.
- (NSArray *)parserXML:(NSString *)URLString;{ //URL NSURL *url = [NSURL URLWithString:URLString]; //通过Session自动开启线程进行异步任务. [[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { //创建xml解析器 NSXMLParser *parser = [[NSXMLParser alloc]initWithData:data]; //设置代理 parser.delegate = self; //解析开始 [parser parse]; }] resume]; return self.modelArr;}

图片 1控制器一句话搞定加载

理解和分析应用程序崩溃报告

下面先看一下实现效果.

@interface ViewController()@property (nonatomic, strong) NSTimer *timer;@end@implementation ViewController- viewDidLoad { [super viewDidLoad]; self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(p_doSomeThing) userInfo:nil repeats:YES];}- p_doSomeThing { // doSomeThing}- p_stopDoSomeThing { [self.timer invalidate]; self.timer = nil;}- dealloc { [self.timer invalidate];}@end

DOM解析

因为iOS不能直接使用MAC的解析方式,所以DOM解析使用第三方框架.GDataXMLNode 它有增加删除等方法,头文件里面有对应的方法,这里我们仅使用它来进行XML的反序列化. 也就是解析

如果你不是通过控制台中pod加载的框架

  • pod init
  • pod GDataXML-HTML
  • pod install

那么你可能会碰到引入框架后#import <libxml/tree.h>报错的问题.

按照注释,在project->build Settings ->Header Search PathsOther Linker Flags 中分别添加两个地址

  1. /usr/include/libxml2
  2. -lxml2
  • 原文注释
// libxml includes require that the target Header Search Paths contain//// /usr/include/libxml2//// and Other Linker Flags contain//// -lxml2

图片 2使用pod install 导入框架会自动进行配置,非常方便

DOM的解析有点类似于字典转模型的过程.根据解析的XML的结构不同嵌套层次也不同.由于整个解析过程比较连贯,所以直接复制粘贴整段代码

  • 下面是全部代码,注释非常详细.
#import "VideoModel.h"#import <GDataXMLNode.h>@interface ViewController ()@end@implementation ViewController- viewDidLoad { [super viewDidLoad]; //1. 通过URL获取XML的Data数据. NSURL *url = [NSURL URLWithString:@"http://127.0.0.1/videos.xml"]; [[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { //2. 获取到Data数据,创建GData对象 这里创建方法接收的是一个XML的string,所以先转换Data成String NSString *xmlString = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]; //这个方法可以进去看看实现,默认是用UTF-8编码 GDataXMLDocument *GD = [[GDataXMLDocument alloc]initWithXMLString:xmlString error:nil]; //3. 创建对象以后,取出根元素的子元素.返回的数组中都是GDataXMLElement NSArray *rootArr = GD.rootElement.children; //4. 那么现在要做的就是遍历这个数组,对每一个元素进行操作,转换成模型了,这个过程类似于字典转模型. //创建一个可变数组保存转换好的模型 NSMutableArray *modelArrM = [NSMutableArray array]; for (GDataXMLElement *element in rootArr) { // 1. 取出数组中的每一个元素后,先将这一组的video标签的属性videoId取出来 /* 1. 属性返回的是一个数组,我们的属性只有一个,所以取出第一个.元素类型是GDataXMLNode 2. 通过这个结构我们不难发现,elment对应的是一个树枝节点,它包含attributes数组,`属性`一定是到头了,是叶子节点.所以这个数组中存的是多个GDataXMLNode类型的元素. 3. GDataXMLNode是GDataXMLElement的父类.node有对应的方法name,stringValue.返回键值对. */ GDataXMLNode *node = element.attributes.firstObject; //NSLog(@"name = %@ value = %@",node.name,node.stringValue); //2. 创建一个模型将video标签的属性保存 KVC VideoModel *model = [VideoModel new]; [model setValue:node.stringValue forKey:node.name]; //3. 取出video标签的子标签,数组,每个子元素依然是GDataXMLElement 代表着一个一个的标签. //NSLog(@"element.children = %@",[element.children.firstObject class]); //循环遍历数组 for (GDataXMLElement *elementTag in element.children) { //4. 这里的每一个tag就是最后的叶子借点了. name 是 key xml是value //NSLog(@"%@",elementTag); [model setValue:elementTag.XMLString forKey:elementTag.name]; } //5. 将添加完元素的模型保存到数组 [modelArrM addObject:model]; } //转换完毕,看看结果 NSLog(@"%@",modelArrM); }] resume]; }@end

图片 3转换完毕.看看结果

重要提示:当应用程序崩溃时,创建了一个崩溃报告,这是非常有用的了解什么造成的崩溃。本文件包含重要的信息,如何symbolicate,理解和解释的崩溃报告。

图片 4全屏pop

上面的代码主要是利用定时器重复执行p_doSomeThing方法,在合适的时候调用p_stopDoSomeThing方法使定时器失效。

简介

当应用程序崩溃时,将创建一个崩溃报告并存储在设备上。崩溃报告描述的情况下,应用程序终止,在大多数情况下,包括每个执行线程的一个完整的回溯,通常对于调试应用中的问题是非常有用。你应该看看这些崩溃报告,了解你的应用程序有什么崩溃,然后尝试修复它们。

有回溯的崩溃报告需要被符号化了才可以分析。符号化(symbolication)成人们可读的函数名称和行号来取代内存地址。如果你通过Xcode的设备窗口来获取设备的崩溃日志,它们将在几秒钟后自动被符号化。否则,你将需要自己将崩溃日志符号化,通过自己导入崩溃文件到Xcode设备窗口。看到符号化(symbolicating)后的崩溃报告。

低内存报告不同于其他的崩溃报告,没有回朔在这种类型的报告。当低内存崩溃发生时,你必须调查你的内存使用模式和对低内存警告的响应。此文档指向了您可能会发现有用的多个内存管理引用。

效果还不错吧.当然了,这里的所有效果都是系统实现的.或许你不信,一起看看实现吧.

能看出问题吗?在开始讨论上面代码问题之前,需要对NSTimer做一点说明。NSTimer的scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:方法的最后一个参数为YES时,NSTimer会一直保留目标对象,直到自身失效才释放目标对象。执行完任务后,一次性的定时器会自动失效;重复性的定时器,需要主动调用invalidate方法才会失效。

获取崩溃和低内存报告

调试部署iOS应用程序 讨论如何直接从iOS设备获取崩溃和内存不足的报道。

在应用程序分发指南中的分析崩溃报告 讨论如何从TestFlight beta测试者和那些已经从App Store下载应用程序的用户中收集崩溃报告。

#import "GLNavigationController.h"@interface GLNavigationController () <UIGestureRecognizerDelegate>@end@implementation GLNavigationController - viewDidLoad { [super viewDidLoad]; // 这句很核心 稍后讲解 id target = self.interactivePopGestureRecognizer.delegate; // 这句很核心 稍后讲解 SEL handler = NSSelectorFromString(@"handleNavigationTransition:"); // 获取添加系统边缘触发手势的View UIView *targetView = self.interactivePopGestureRecognizer.view; // 创建pan手势 作用范围是全屏 UIPanGestureRecognizer * fullScreenGes = [[UIPanGestureRecognizer alloc]initWithTarget:target action:handler]; fullScreenGes.delegate = self; [targetView addGestureRecognizer:fullScreenGes]; // 关闭边缘触发手势 防止和原有边缘手势冲突 [self.interactivePopGestureRecognizer setEnabled:NO];}// 防止导航控制器只有一个rootViewcontroller时触发手势- gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {//解决与左滑手势冲突 CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view]; if (translation.x <= 0) { return NO; } // 过滤执行过渡动画时的手势处理 if ([[self valueForKey:@"_isTransitioning"] boolValue]) { return NO; } return self.childViewControllers.count == 1 ? NO : YES;}@end

了解scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:最后一个参数含义之后,你发现何处出现问题了吗?

symbolicating崩溃报告

symbolication是解决回溯地址的源代码的方法或函数名的过程,称为符号化。没有经过符号化的崩溃报告,是不能了解具体的崩溃信息的。

注:低内存报告不需要symbolicated。

注:来自MacOS崩溃报告通常是已经符号化的,或部分符号化后的(symbolicated)。本节重点symbolicating从iOS,WatchOS,和TVOS的崩溃报告,但整体过程类似MacOS。

 图1概述了崩溃报告和symbolication过程。

图片 5获取崩溃报告以及符号化过程

1.当编译器将源代码翻译成机器代码时,它也会生成调试符号,它将编译后的二进制中的每一个机器指令映射到源代码的行源代码中。根据调试信息格式(debug_information_format)编译设置,这些调试符号存储在二进制或在同伴的调试符号文件。默认情况下,调试版本的应用程序的调试符号存储在编译后的二进制中,而发布版本的应用程序的调试符号存储在相应的dsym文中件以减少二进制大小。调试符号文件和应用程序二进制文件与每一个build生成的UUID捆绑在一起。一个新的UUID生成是由build一个应用产生的,它应用程序每次build的唯一标识。即使一个功能相同的可执行文件是从相同的源代码重构,具有相同的编译器设置,也会有不同的生成的UUID。调试符号文件的后续版本,甚至来自同一个源文件,不会与其他版本的二进制文件相混淆。

2.当你的归档要分发的应用程序,Xcode会收集应用程序二进制随着.dsym文件,并且存放在home文件夹里。你可以在Xcode的组织者在“归档”部分,找到所有你归档后的应用。有关创建存档的更多信息,可以参考应用程序分发指南。

重要:为了符号化(symbolicate)从测试人员,审查程序,和客户得到的崩溃报告,你必须保留每个存档您的应用程序。

3.如果你是通过App Store发布的应用程序,或使test flight进行测试,你将选择包括dsym文件到iTunes Connect。在提交对话框中,选择“您的应用程序的应用程序符号……”。为了接收来test flight收集的 以及 那些选择了分享的诊断数据的客户的崩溃报告,上传你的dsym文件是必要的 。有关崩溃报告服务的更多信息,可以参考应用程序分发指南。

重要:从app review收到的崩溃报告将是unsymbolicated,即使你有上传dsym文件到iTunes Connect。你需要使用Xcode 来symbolicate任何从app review得到的崩溃报告。看下午中"到symbolicating崩溃报告Xcode"。

4.当你的应用程序崩溃时,一个unsymbolicated崩溃报告会被创建并存储在设备上。

5.用户可以按照调试部署iOS应用程序的步骤,直接从他们的设备中检索崩溃报告。如果你的应用程序通过AdHoc or Enterprise distribution发布的,这是从你的用户获取崩溃报告的唯一途径。

6.从设备检索得到的未符号化的(unsymbolicated)崩溃报告需要使用Xcode来符号化(symbolicated)。Xcode使用您的应用程序的二进制在源代码中的源位置的回溯替换每个地址相关联的dsym文件。其结果是一个符号化的(symbolicated)崩溃报告。

7.如果用户选择了与苹果共享诊断数据,或者如果用户通过TestFlight安装了你的应用程序,崩溃报告会被上传到应用商店。

8.App Store 符号化崩溃报告 并且将类似的崩溃报告分组。类似的崩溃报告的集合被称为崩溃点。

9.符号化的崩溃报告是在Xcode's Crashes organizer提供给你的。

在实现之前,先推测一下苹果实现pop的大概思路.首先,需要在一个合适的view上添加边缘手势,其次,针对这个手势必然要实现一个方法响应该事件.当然,根据苹果一贯代码风格,处理该事件很可能交给另一个专门的类去处理.

创建定时器时,当前控制器(创建定时器的那个控制器,为了描述方便,简称当前控制器)引用了定时器(引用定时器是因为后续要用到这个定时器对象),在给定时器添加任务时,定时器保留了self,这里就出现了第一个内存泄漏问题循环引用

Bitcode

Bitcode是一个编译程序的中间表示。当你可以用bitcode来 archive an application,编译器产生的二进制文件包含bitcode而不是机器代码。一旦二进制已经上传到App Store,这可以被编译成机器码。App Store在将来,在没有任何行动的一部分的情况下,利用未来的改进的编译器,再次编译。

图片 6Bitcode compilation process

因为你的二进制最后的编译出现在App Store,Mac将不包含符号化从应用程序审查或用户发给你他们从设备中取得的崩溃报告所需要的调试符号文件。虽然dsym文件是您归档应用程序(archive your application)的时候产生的,它是为bitcode二进制并不能用来symbolicate崩溃报告。App Store在你从Xcode或从iTunes Connect网站,可以获得编译的bitcode并且可以下载的过程中,产生dsym文件。符号化从应用程序审查或用户发给你他们从设备中取得的崩溃报告,你必须下载这些dsym文件。崩溃报告获得通过崩溃报告服务将自动symbolicated。

重要:二进制应用程序商店会比最初二进制编译,有不同的UUID

假如以上推测成立,只要获得那个专门处理事件的类和方法,实现全屏pop效果就很简单了.

图片 7循环引用

从Xcode获取dSYMs文件

1.下载dsyms文件在归档管理中选择相应的归档并下载dsyms文件

图片 8QQ20161122-0@2x.png图片 9QQ20161122-1@2x.png

2.在归档出的文件中找到dSYMs文件

图片 10QQ20161122.png图片 11QQ20161122-3@2x.png图片 12QQ20161122-4@2x.png

下面是笔者在分析苹果实现pop的部分信息.看到这,是否若有所悟?

如果能在合适的时候打破循环引用,就不会有问题了。此时有两种选项: