前段时间遇到一个工单,客户反馈,只要进入订单列表界面 1~2 秒,客户端就会 Crash,订单列表界面示意以下:html
因为是客户投诉的 Bug,没有 Debug 信息,先猜想各类状况,数组越界/后台传 nil 值/内存泄露/ KVO 赋未定义值等等;然而通过仔细分析模拟逐个排除了上述可能,仍查找不到 Crash 缘由,百思不得其解。ios
排除了代码的问题,只有多是数据问题了,猜想是异常的图片/数据解析出现的问题,因而抽取用户订单数据分析,发现有 2 张尺寸很是大的 JPEG 图片,尺寸达到了 15000*8000 的像素,瞬间想明白了怎么回事,像素总量达到了一亿两千万,猜想是图片解压缩到内存后占用内存过大,致使系统内存紧张,所以系统杀死了 App 进程。数组
验证是否因大尺寸图片引发的错误。验证过程以下:浏览器
写一个相似上面订单列表的 Demo,点击 Cell 逐个加载大图图片,测试用的手机为 iPhone 7P,图片尺寸为(15000px*15000px),点击加载第二张图片就发生了 Crash,通常状况下,APP 占用系统内存 60% 左右就会被杀死进程。iPhone 7P 加载大图后的内存截图以下:服务器
Tips: 不一样手机因为内存和屏幕不同,内存超限 App 发生 Crash 的条件不同,其中 iPhone 6P 是最容易 Crash 的,由于它有 5.5 寸的屏幕,却只有 1G 内存,加载 Assets.xcassets 图片时会加载 3x 图片,同一张网络图片,UIImageView 布局通常会按照比例放大,大屏手机图片会放大,解码后占用内存也就更大。网络
咱们常见的图片格式例如 PNG/JPG/GIF 等格式都属于图像压缩格式,解压为位图后占用的内存会很是大。app
假设 iOS 系统从磁盘加载一张图片,首先将文件数据从磁盘读到内存中,此时在内存中仍旧是压缩格式,只有在须要的时机,才会把图片解码为无压缩的位图格式,最后 Core Animation 使用未压缩的位图数据渲染 UIImageView 的图层。框架
将压缩的图片数据解码成未压缩的位图形式,这是一个耗时的 CPU 操做,SDWebimage/YYImage 等第三方框架通常都会提早异步强制解码图片,保证了 UI 界面的流畅性。异步
图片解码后会占用多少内存呢?其实这个很好计算,苹果手机采用 24 位真彩色显示图像,也就是 24bit(3 字节,RGB 红绿蓝三原色分别占用 8bit,每一个颜色有 256 种状态),若是是不包含 Alpha 通道(透明度)的 RGB 图片,那每一个像素占用的就是 3 字节,15000px*15000px*3Byte = 644MB,若是是包含透明度的 RGBA 图片,则为 15000px*15000px*4Byte = 858MB,如图2所示,加载一张长和宽 15000px 的图片,内存暴增 858MB。布局
最经常使用的图片缩放方法是使用 CGContext 的 UIGraphicsGetImageFromCurrentImageContext 方法对图片进行裁剪缩放,可以知足大部分需求。但若是是处理多张大图,这时候就须要优化缩放速度了,可经过 Image I/O 框架对图片进行缩放,在工程中添加 Image I/O Framework,而后在须要使用的地方 #import <ImageIO/ImageIO.h> 便可,示例代码以下:
//maxPixelSize MUST BE a valid value.
+ (UIImage *)thumbImageFromLargeFile:(NSString *)filePath withConfirmedMaxPixelSize:(CGFloat)maxPixelSize
{
// Create the image source (from path)
CGImageSourceRef src = CGImageSourceCreateWithURL((__bridge CFURLRef) [NSURL fileURLWithPath:filePath], NULL);
// Create thumbnail options
CFDictionaryRef options = (__bridge CFDictionaryRef) @{
(id) kCGImageSourceCreateThumbnailWithTransform : @YES,
(id) kCGImageSourceCreateThumbnailFromImageAlways : @YES,
(id) kCGImageSourceThumbnailMaxPixelSize : @(maxPixelSize)
};
// Generate the thumbnail
CGImageRef thumbnail = CGImageSourceCreateThumbnailAtIndex(src, 0, options);
CFRelease(src);
UIImage *image = [[UIImage alloc] initWithCGImage:thumbnail];
CFRelease(thumbnail);
return image;
}
复制代码
开发中咱们常常会处理用户的生日,例以下面的代码,将用户生日转换为NSDate,例以下面的代码:
NSString *birthStr = @"1986-05-04";
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"yyyy-MM-dd"];
NSDate * birDate = [formatter dateFromString:birthStr];
NSLog(@"timeStr to date is %@ %@", birthStr, birDate);
复制代码
这时候咱们会惊奇的发现,birDate 为 nil ?嗯,nil。
经过Google搜索及测试,最终定位在了夏令时问题上。
我国解放前几年在部分地区也曾实行过夏令时。1986年4月,中央有关部门发出“在全国范围内实行夏时制的通知”,具体做法是:每一年从四月中旬第一个星期日的凌晨2时整(北京时间),将时钟拨快一小时,即将表针由2时拨至3时,夏令时开始;到九月中旬第一个星期日的凌晨2时整(北京夏令时),再将时钟拨回一小时,即将表针由2时拨至1时,夏令时结束。从1986年到1991年的六个年度,除1986年因是实行夏时制的第一年,从5月4日开始到9月14日结束外,其它年份均按规定的时段施行。1992年起,夏令时暂停实行。
看完这段描述应该就明白缘由了,在中国东八时区时区,某些时间段是不存在的,例如"1988-04-10 00-00-00"至"1988-04-10 01-00-00"中间的时间段。
既然是时区引发的问题,那就把时区转换为 UTC 或 GMT 的时区便可。
NSString *birthStr = @"1988-04-10 00-00-00";
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setTimeZone:[NSTimeZone timeZoneWithName:@"GMT"]];// 零时区
[formatter setDateFormat:@"yyyy-MM-dd HH-mm-ss"];
NSDate * birDate = [formatter dateFromString:birthStr];
NSLog(@"BirthStr convert to NSDate is %@", birDate);
复制代码
Tips: 不要用模拟器测试,模拟器测试结果不正确。
开发中遇到有人使用 YYYY-MM-dd 处理时间格式,以为不对又说不出为何,就调研了一下。
大多数状况下,设置时间格式 YYYY-MM-dd 和 yyyy-MM-dd 转换的日期是同样的,只有当到达一些特色的时间节点,跨年时使用 "YYYY-MM-dd" 可能会出现差一年的问题。以下代码所示:
// 原始的日期字符串
NSString *orginDateStr = @"2015-12-28";
// 转换为NSDate
NSDateFormatter *orginFormatter = [[NSDateFormatter alloc] init];
[orginFormatter setDateFormat:@"yyyy-MM-dd"];
NSDate * orginDate = [orginFormatter dateFromString:orginDateStr];
NSLog(@"orginFormatter: orginDate is %@", orginDate);
// 若是用YYYY将orginDate转换回字符串时就出现了问题
NSDateFormatter *weekFormatter = [[NSDateFormatter alloc] init];
[weekFormatter setDateFormat:@"YYYY-MM-dd"];
NSString *weekDateStr = [weekFormatter stringFromDate:orginDate];
NSLog(@"weekFormatter: weekDateStr is %@", weekDateStr);
复制代码
打印结果,相差一年:
orginFormatter: orginDate is Mon Dec 28 00:00:00 2015
weekFormatter: weekDateStr is 2016-12-28
咱们先来理解 YYYY 和 yyyy 的区别:
“YYYY format” 是 “ISO week numbering system”
“yyyy format” 是 “Gregorian Calendar(公历)”
“YYYY specifies the week of the year (ISO) while yyyy specifies the calendar year (Gregorian)”
yyyy specifies the calendar year whereas YYYY specifies the year (of “Week of Year”), used in the ISO year-week calendar.
也就是说转换为日期时,DateFormatter若是是YYYY格式的话,若是1月1日是星期一,星期二,星期三或星期四,它是在01周。若是一月1日是星期五,星期六或星期日,它在前一年的52周或53周。
苹果官方文档说使用YYYY是常见错误,正确的应该是使用yyyy格式,官方文档解释以下:
It uses yyyy to specify the year component. A common mistake is to use YYYY. yyyy specifies the calendar year whereas YYYY specifies the year (of “Week of Year”), used in the ISO year-week calendar. In most cases, yyyy and YYYY yield the same number, however they may be different. Typically you should use the calendar year.
The representation of the time may be 13:00. In iOS, however, if the user has switched 24-Hour Time to Off, the time may be 1:00 pm.
使用正确的时间格式 yyyy-MM-dd 来处理日期时间。
测试给一个小伙伴提了一个Bug,点击一个功能时会不定时出现问题,可以复现,但不是每次都出现。以下代码所示:
BOOL isSuccess;
if (isSuccess) {
NSLog(@"success");
}else{
NSLog(@"failed");
}
复制代码
测试结果:在 Debug 环境下真机和模拟器都是 failed,但打包成出来安装后可能为 success 也多是 failed 了。
很明显是局部变量 isSuccess 出现了随机值致使的,虽然我我的平时的习惯是声明遍历必定会初始化,但 Debug 模式下正常,打包后就出现随机值的缘由仍是不清楚,因而调研了一下。
在 ARC 环境下,本地对象建立若是未初始化,指针会指向默认值 nil;可是相似 BOOL 的非对象类型的局部变量,未初始化时会指向最后一次写入该地址的内容,可能为任意值,也就是垃圾值,出现随机值也就不稀奇了。
建立变量时要养成初始化的好习惯,尤为是基本数据类型,例如:
BOOL isSuccess = NO;
int a = 0;
复制代码
遇到一个工单,客户反馈没法正常进入 App,进入后就报错,还反馈了机型、系统版本,App 版本等信息。
排查代码逻辑没有问题,找到相同系统的机型,相同 App 版本测试没有问题。
期间也回复了用户软件没有问题,但这个用户持之以恒,最终给这个用户发了一个 Debug 版本,报错时 Debug 日志展现在界面上,复制粘贴发过来。最终问题定位在了用户名上面,这位用户的用户名相似于这样的 &&###???###&&。
这时候可能已经想明白怎么回事了,特殊符号转义引发的后台Bug。例如一些特殊的符号,例如 !#$&'()*+,/:;=?@[] 这些特殊符号,拼接在 URL 或者 Body 里面,传送到后台时均可能引发转义,不能正常解析,不一样的后台表现逻辑不一致。
既然是特殊字符引发的,在网络传输过程当中,对特殊字符进 URLEncode 便可,服务器接收到进行 URLDecode 便可。
// 用户手机设置的用户名
NSString *userPhoneName = @"abc&&&???dd**%###!!!";
// 设置须要转义的特殊字符,例如@"/+=\n"
NSString *characterSetStr = @"?!@#$^&%*+,:;='\"`<>()[]{}/\\| ";
NSCharacterSet *characterSet = [[NSCharacterSet characterSetWithCharactersInString:characterSetStr] invertedSet];
// 返回转义后的字符串
NSString *urlEncodeStr = [userPhoneName stringByAddingPercentEncodingWithAllowedCharacters:characterSet];
NSLog(@"UserPhoneName Encoding is %@",urlEncodeStr);
// 移除百分号转义
NSString *removeEncodingStr = urlEncodeStr.stringByRemovingPercentEncoding;
NSLog(@"UserPhoneName removeEncoding is %@",removeEncodingStr);
复制代码
打印结果:
UserPhoneName Encoding is abc%26%26%26%3F%3F%3Fdd%2A%2A%25%23%23%23%21%21%21
UserPhoneName removeEncoding is abc&&&???dd**%###!!!
备注: 经历此次事件,在处理特殊字符问题上留下了深入的印象,不管是处理用户输入,仍是取值用户字符串,都会注意特殊字符的转义问题了。