记一些印象深刻的 Bug

目录

一、iOS 加载超大尺寸图片 Crash 的调研及解决方案

二、夏令时引起某些时间段转换为 NSDate 为 nil 问题的解决方案

三、日期格式 YYYY-MM-dd 和 yyyy-MM-dd 区别

四、iOS 局部 BOOL 变量随机值

五、iPhone 用户名包含特殊符号引起的后台 Crash

六、参考链接


一、iOS 加载超大尺寸图片 Crash 的调研及解决方案

1.1、问题描述

前段时间遇到一个工单,客户反馈,只要进入订单列表界面 1~2 秒,客户端就会 Crash,订单列表界面示意如下:

1.2、问题分析

由于是客户投诉的 Bug,没有 Debug 信息,先猜测各种情况,数组越界/后台传 nil 值/内存泄露/ KVO 赋未定义值等等;然而经过仔细分析模拟逐个排除了上述可能,仍查找不到 Crash 原因,百思不得其解。

排除了代码的问题,只有可能是数据问题了,猜测是异常的图片/数据解析出现的问题,于是抽取用户订单数据分析,发现有 2 张尺寸非常大的 JPEG 图片,尺寸达到了 15000*8000 的像素,瞬间想明白了怎么回事,像素总量达到了一亿两千万,猜测是图片解压缩到内存后占用内存过大,导致系统内存紧张,因此系统杀死了 App 进程。

1.3、问题验证

验证是否因大尺寸图片引起的错误。验证过程如下:

写一个类似上面订单列表的 Demo,点击 Cell 逐个加载大图图片,测试用的手机为 iPhone 7P,图片尺寸为(15000px*15000px),点击加载第二张图片就发生了 Crash,一般情况下,APP 占用系统内存 60% 左右就会被杀死进程。iPhone 7P 加载大图后的内存截图如下:

Tips: 不同手机由于内存和屏幕不一样,内存超限 App 发生 Crash 的条件不一样,其中 iPhone 6P 是最容易 Crash 的,因为它有 5.5 寸的屏幕,却只有 1G 内存,加载 Assets.xcassets 图片时会加载 3x 图片,同一张网络图片,UIImageView 布局一般会按照比例放大,大屏手机图片会放大,解码后占用内存也就更大。

1.4、解决方案

  • 约定大于配置,上传图片也要遵守一定的约定。 基于 SDWebImage/YYImage 等第三方库加载超大图引起的崩溃,可通过修改源码解决,但不建议这样做;修改源码可能会引起其他 Bug,而且大图毕竟是少数,没必要对所有图片都进行判断,个别大图单独处理即可。按照一定约定,通过管理平台限定上传图片尺寸大小,增加 APP 流畅度的同时,还能减少用户流量损失,此为最佳方案。
  • 缩放图片尺寸。 如果是展示整张图片,不需要展示图片细节,受限于屏幕分辨率,太大尺寸的图片是没有意义的;如果需要做类似于图片浏览器,可对图片进行放大缩小操作的需求,大图预览的时候可加载缩略图,展示的时候切片处理。

1.5、iOS 图片解码

我们常见的图片格式例如 PNG/JPG/GIF 等格式都属于图像压缩格式,解压为位图后占用的内存会非常大。

假设 iOS 系统从磁盘加载一张图片,首先将文件数据从磁盘读到内存中,此时在内存中仍旧是压缩格式,只有在需要的时机,才会把图片解码为无压缩的位图格式,最后 Core Animation 使用未压缩的位图数据渲染 UIImageView 的图层。

将压缩的图片数据解码成未压缩的位图形式,这是一个耗时的 CPU 操作,SDWebimage/YYImage 等第三方框架一般都会提前异步强制解码图片,保证了 UI 界面的流畅性。

1.6、图片解码占用内存计算

图片解码后会占用多少内存呢?其实这个很好计算,苹果手机采用 24 位真彩色显示图像,也就是 24bit(3 字节,RGB 红绿蓝三原色分别占用 8bit,每个颜色有 256 种状态),如果是不包含 Alpha 通道(透明度)的 RGB 图片,那每个像素占用的就是 3 字节,15000px*15000px*3Byte = 644MB,如果是包含透明度的 RGBA 图片,则为 15000px*15000px*4Byte = 858MB,如图2所示,加载一张长和宽 15000px 的图片,内存暴增 858MB。

1.7、图片缩放最优选择

最常用的图片缩放方法是使用 CGContextUIGraphicsGetImageFromCurrentImageContext 方法对图片进行裁剪缩放,能够满足大部分需求。但如果是处理多张大图,这时候就需要优化缩放速度了,可通过 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 为 nil 问题的解决方案

2.1、问题描述

开发中我们经常会处理用户的生日,例如下面的代码,将用户生日转换为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。

2.2、问题分析

通过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”中间的时间段。

2.3、解决方案

既然是时区引起的问题,那就把时区转换为 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 区别

3.1、问题描述

开发中遇到有人使用 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

3.2、问题分析

我们先来理解 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.

3.3、解决方案

使用正确的时间格式 yyyy-MM-dd 来处理日期时间。

四、iOS 局部 BOOL 变量随机值

4.1、问题描述

测试给一个小伙伴提了一个Bug,点击一个功能时会不定时出现问题,能够复现,但不是每次都出现。如下代码所示:

BOOL isSuccess;
if (isSuccess) {
NSLog(@"success");
}else{
NSLog(@"failed");
}

测试结果:在 Debug 环境下真机和模拟器都是 failed,但打包成出来安装后可能为 success 也可能是 failed 了。

4.2、问题分析

很明显是局部变量 isSuccess 出现了随机值导致的,虽然我个人平时的习惯是声明遍历一定会初始化,但 Debug 模式下正常,打包后就出现随机值的原因还是不清楚,于是调研了一下。

在 ARC 环境下,本地对象创建如果未初始化,指针会指向默认值 nil;但是类似 BOOL 的非对象类型的局部变量,未初始化时会指向最后一次写入该地址的内容,可能为任意值,也就是垃圾值,出现随机值也就不稀奇了。

4.3、解决方案

创建变量时要养成初始化的好习惯,尤其是基本数据类型,例如:

BOOL isSuccess = NO; 
int a = 0;

五、iPhone 用户名包含特殊符号引起的后台 Crash

5.1、问题描述

遇到一个工单,客户反馈无法正常进入 App,进入后就报错,还反馈了机型、系统版本,App 版本等信息。

排查代码逻辑没有问题,找到相同系统的机型,相同 App 版本测试没有问题。

期间也回复了用户软件没有问题,但这个用户锲而不舍,最终给这个用户发了一个 Debug 版本,报错时 Debug 日志展示在界面上,复制粘贴发过来。最终问题定位在了用户名上面,这位用户的用户名类似于这样的 &&###???###&&。

5.2、问题分析

这时候可能已经想明白怎么回事了,特殊符号转义引起的后台Bug。例如一些特殊的符号,例如 !#$&’()*+,/:;=?@[] 这些特殊符号,拼接在 URL 或者 Body 里面,传送到后台时都可能引起转义,不能正常解析,不同的后台表现逻辑不一致。

5.3、解决方案

既然是特殊字符引起的,在网络传输过程中,对特殊字符进 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**%###!!!

备注: 经历这次事件,在处理特殊字符问题上留下了深刻的印象,无论是处理用户输入,还是取值用户字符串,都会注意特殊字符的转义问题了。

六、参考链接

https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/DataFormatting/Articles/dfDateFormatting10_4.html

http://blog.leichunfeng.com/blog/2017/02/20/talking-about-the-decompression-of-the-image-in-ios/#jtss-tsina