1、崩溃日志如下
屏幕快照 2019-11-13 下午3.24.03.png由异常类型caused by:Application received signal 11,判断很可能是坏内存访问
注意到调用栈最上一行的崩溃信息_ZN3icu12RegexMatcher4findER10UErrorCode,字面意思是正则匹配造成的。
崩溃最后方法如下
- (BOOL)canHandleRequest:(NSURLRequest *)request{
NSURL *requestURL = request.URL;
static NSRegularExpression *videoRegex;
if (!videoRegex) {
NSError *error = nil; ——————— @1
videoRegex = [NSRegularExpression regularExpressionWithPattern:@"(?:&|^)ext=MOV(?:&|$)" options:NSRegularExpressionCaseInsensitive error:&error];
}
NSString *query = requestURL.query;
if (query != nil && [videoRegex firstMatchInString:query options:0 range:NSMakeRange(0, query.length)]) {
return NO;
}
for (id<RCTImageURLLoader> loader in _loaders) {
if (![loader conformsToProtocol:@protocol(RCTURLRequestHandler)] &&
[loader canLoadImageURL:requestURL]) {
return YES;
}
}
return NO;
}
推测报错的是[videoRegex firstMatchInString:query options:0 range:NSMakeRange(0, query.length)],字符串在正则匹配过程中崩溃。
上面代码认真分析,发现static变量存在多线程同时访问bug,多个线程同时访问该方法时,static变量会被多次付值,可能造成多线程同时修改变量异常。
并且,还会导致一个内存问题,例如
线程A: 进入@1处
线程B: 进入@1处
线程A: 初始化videoRegex,此时videoRegex指向堆上的内存M1,代码继续执行到firstMatchInString方法内部
线程B: 初始化videoRegex,修改videoRegex在堆上的指向为M2,
这就出问题了,M1已经没有指针指向会被销毁,而A线程内部正在使用,引发异常(这里是firstMatchInString正则匹配过程中字符串循环遍历而对象销毁导致出错)
上面是代码层面分析,下面复现bug
- (void)test{
for (int i = 0; i < 100000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static NSRegularExpression *videoRegex = nil;
if (i % 100 == 0) { ————————— @2
videoRegex = nil;
}
if (!videoRegex) {
NSError *error = nil;
videoRegex = [NSRegularExpression regularExpressionWithPattern:@"(?:&|^)ext=MOV(?:&|$)" options:NSRegularExpressionCaseInsensitive error:&error]; ———— @3
}
NSString *query = @"pretty=true&ext=MOV";
NSRegularExpression *newVideoRegex = videoRegex; ————- @4
[newVideoRegex firstMatchInString:query options:0 range:NSMakeRange(0, query.length)];
});
}
}
@2: 目的是多次模拟多线程初始化静态变量videoRegex的过程,增加bug复现概率
@4: 注释掉这行,则一般会在firstMatchInString崩溃。开启这里,则因为正则匹配firstMatchInString方法使用的是局部变量,不存在匹配过程中销毁现象,崩溃会出现在@3处
修复: dispatch_once保证static变量只初始化一次即可
2、日志如下
屏幕快照 2019-11-13 下午3.28.04.png无论是objc_msgSend + 16还是caused by:Application received signal 11都可以表明这也是一个坏内存访问问题
崩溃方法如下
- (NSURLSessionDataTask *)sendRequest:(NSURLRequest *)request
withDelegate:(id<RCTURLRequestDelegate>)delegate
{
if (!_session && [self isValid]) {
NSOperationQueue *callbackQueue = [NSOperationQueue new];
callbackQueue.maxConcurrentOperationCount = 1;
callbackQueue.underlyingQueue = [[_bridge networking] methodQueue];
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
[configuration setHTTPShouldSetCookies:YES];
[configuration setHTTPCookieAcceptPolicy:NSHTTPCookieAcceptPolicyAlways];
[configuration setHTTPCookieStorage:[NSHTTPCookieStorage sharedHTTPCookieStorage]];
_session = [NSURLSession sessionWithConfiguration:configuration
delegate:self
delegateQueue:callbackQueue];
std::lock_guard<std::mutex> lock(_mutex);
_delegates = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory
valueOptions:NSPointerFunctionsStrongMemory
capacity:0];
}
NSURLSessionDataTask *task = [_session dataTaskWithRequest:request]; ———— @5
{
std::lock_guard<std::mutex> lock(_mutex);
[_delegates setObject:delegate forKey:task];
}
[task resume];
return task;
}
很明显崩溃的地方在@5处,原因跟上面的类似,也是多线程导致_session变量指向堆内存释放问题。
这里因为_session变量可能会被代码置为nil,不能用dispatch_once,可以采用加锁方式避免多线程问题
3、某个版本发布后突然发现一个较多的数组越界崩溃,崩溃堆栈信息在一个已经稳定运行8个月的老方法中,照理来说这么长时间运行良好不应该在某个版本突然爆发。
崩溃堆栈结合代码分析,排除多线程,iOS系统版本号等,仔细检查代码逻辑也没有发现问题。
对比模块该版本和上一个版本的区别,没有发现有影响这个代码的更改
询问主工程AvoidCrash被告知没有更改
询问异常上报SDK发现有更新,询问SDK提供方也说没有啥更改
再次回归到代码处,考虑到代码执行时数组所有可能状态不好全量测试,另建工程抽取崩溃方法代码逻辑,尽最大可能还原原有逻辑,自定义各种数组长度 逐一 调用方法,发现崩溃
崩溃原因: 数组的count字段是NSUInteger类型,减去一个int类型的数值结果还是一个NSUInteger类型
例如
NSArray *arr = @[@1];
if (arr.count - 30 > 0) {
arr[10]; // 这里会崩溃
}
解决:使用count时强转下类型,例如(int)arr.count
这真是一个暗坑。 发现swift中数组的count属性变为int类型了,果然swift是更安全的语言
问题解决后返回去看为什么AvoidCrash没有避免数组越界问题。
AvoidCrash初始化如下
- (void)main{
isPrd = YES;
}
+ (void)load{
if (isPrd) {
[AvoidCrash makeAllEffective];
}
}
在main方法中设置是否是生产环境,在+load方法中使用该变量,因为+load方法先执行,isPrd尚未赋值为NO,生产环境AvoidCrash不能初始化,没生效。
查看git,就是这个版本有人改了这个,把AvoidCrash初始化方法从main中挪到+load方法中所致。改代码真是,一不留神就出bug
AvoidCrash避免常规崩溃还是很管用的,出现上述问题主要还是因为公司项目接入的是自己公司的异常捕获工具A,不具备自定义上传异常的功能,所以这类异常被AvoidCrash捕获处理,因为程序不会崩溃,A不能捕获到该异常,程序员就以为一切正常。
正确的操作应该是,使用AvoidCrash捕获异常避免崩溃,同时利用如Bugly上报给后台,然后每个版本查看这类异常并修复
网友评论