iOS疑难崩溃问题梳理
负责iOS版本质量这段时间碰到了不少疑难崩溃,它们的统一特点就是崩溃栈信息十分十分不明确,导致解决起来异常困难。这里梳理和记录一下那些曾经令人头秃的崩溃。(不定期更新)
1、多线程操作NSMutable类型数据
1、NSMutableData appendBytes崩溃
5 Foundation __NSMutableDataGrowBytes + 272
6 Foundation -[NSConcreteMutableData appendBytes:length:] + 372
7 Foundation ___49-[_NSDispatchData enumerateByteRangesUsingBlock:]_block_invoke + 44
8 libdispatch.dylib __dispatch_data_apply + 128
9 libdispatch.dylib dispatch_data_apply + 40
10 Foundation -[_NSDispatchData enumerateByteRangesUsingBlock:] + 64
11 Foundation -[NSConcreteMutableData appendData:]
2、使用BlocksKit子线程遍历__NSDictionaryM(NSMutableDictionary)崩溃
0 libobjc.A.dylib objc_retain + 8
1 MojiWeather __35-[NSDictionary(BlocksKit) bk_each:]_block_invoke (NSDictionary+BlocksKit.m:14)
2 CoreFoundation -[__NSDictionaryM enumerateKeysAndObjectsWithOptions:usingBlock:] + 232
3 MojiWeather -[NSDictionary(BlocksKit) bk_each:] (NSDictionary+BlocksKit.m:17)
4 MojiWeather -[AdvSystemNetworkManager p_handleNoAdResponse:] (AdvSystemNetworkManager.m:394)
5 MojiWeather __33-[GCDAsyncSocket closeWithError:]_block_invoke (GCDAsyncSocket.m:3143)
6 libdispatch.dylib __dispatch_call_block_and_release + 24
7 libdispatch.dylib __dispatch_client_callout + 16
8 libdispatch.dylib __dispatch_queue_serial_drain$VARIANT$mp + 528
9 libdispatch.dylib __dispatch_queue_invoke$VARIANT$mp + 340
10 libdispatch.dylib __dispatch_root_queue_drain_deferred_wlh$VARIANT$mp + 404
11 libdispatch.dylib __dispatch_workloop_worker_thread$VARIANT$mp + 644
12 libsystem_pthread.dylib _pthread_wqthread + 932
此类崩溃的共同点都很明显
1、崩溃栈中出现了NSMutable为前缀的类对象。
2、崩溃栈均在子线程中调用。
触发场景:
多个线程同时操作NSMutableData、NSMutableArray时即容易出现崩溃。
解决方案:
1、在串行队列中进行操作。
//在串行队列中异步执行
dispatch_async(serialQueue, ^{
//critical section,进行数据的增删改操作。
});
2、加锁。
//使用信号量加锁
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
//critical section,进行数据的增删改操作。
dispatch_semaphore_signal(self.semaphore);
备注:
2、CTTelephonyNetworkInfo崩溃
0 libobjc.A.dylib objc_msgSend + 16
1 CoreTelephony _ServerConnectionCallback(__CTServerConnection*, __CFString const*, __CFDictionary const*, void*) + 52
2 CoreTelephony invocation function for block in CTServerState::sendNotification_sync(CTEvent, __CFString const*, __CFDictionary const*) const + 32
3 libdispatch.dylib __dispatch_call_block_and_release + 24
4 libdispatch.dylib __dispatch_client_callout + 16
5 libdispatch.dylib __dispatch_queue_drain + 1216
6 libdispatch.dylib __dispatch_queue_invoke + 132
7 libdispatch.dylib __dispatch_root_queue_drain + 664
8 libdispatch.dylib __dispatch_worker_thread3 + 108
9 libsystem_pthread.dylib _pthread_wqthread + 816
这个崩溃仅在iOS9及以下系统版本上出现。乍一看很令人费解,但是仔细看能注意到崩溃栈中出现了CoreTelephony这个库,在iOS9以下版本上的CTTelephonyNetworkInfo对象有bug,已经释放的CTTelephonyNetworkInfo对象会接受通知并崩溃。继续排查项目,发现旧版本Reachability里的CTTelephonyNetworkInfo不是类变量,会被多次初始化和释放,存在崩溃风险。
触发场景
多处调用旧版本的
[[Reachability reachabilityForLocalWiFi] isReachableViaWiFi]等方法。
解决方案
修改CTTelephonyNetworkInfo为类变量,或者使用AFN里的AFNetworkReachabilityManager作为替代。
备注
CTTelephonyNetworkInfo should not be released
3、内存泄露崩溃
0 libobjc.A.dylib objc_object::release() + 16
1 libobjc.A.dylib (anonymous namespace)::AutoreleasePoolPage::pop(void*) + 844
2 libobjc.A.dylib (anonymous namespace)::AutoreleasePoolPage::pop(void*) + 844
3 libdispatch.dylib __dispatch_last_resort_autorelease_pool_pop + 36
4 libdispatch.dylib __dispatch_root_queue_drain + 1308
5 libdispatch.dylib __dispatch_worker_thread3 + 120
6 libsystem_pthread.dylib _pthread_wqthread + 1176
通过调用栈AutoreleasePoolPage::pop,可以得知此时系统正在调用自动释放池的释放操作(通过objc_object::release()也可以看出是对象内存释放时出现的崩溃),因此判定是内存泄露问题。
触发场景
通过instrument分析,发现以下代码有严重的内存泄露问题:
MTLModel.m line 45: [obj validateValue:&validatedValue forKey:key error:error]
解决方案
参照链接,给MTLModel增加分类方法。
https://github.com/Mantle/Mantle/issues/818
https://github.com/Mantle/Mantle/issues/787#issuecomment-286608719
备注
内存泄露这类问题不同的项目有不同的原因,建议多使用instrument的leaks排查,并通过崩溃趋势来大致确认出现问题的业务模块。
4、后台操作UIWindow问题
0 libobjc.A.dylib objc_retain + 16
1 UIKit ___39-[UIWindow _noteOverlayInsetsDidChange]_block_invoke + 132
2 UIKit __runAfterCACommitDeferredBlocks + 292
3 UIKit __cleanUpAfterCAFlushAndRunDeferredBlocks + 560
4 UIKit __afterCACommitHandler + 168
5 CoreFoundation ___CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 32
6 CoreFoundation ___CFRunLoopDoObservers + 372
7 CoreFoundation ___CFRunLoopRun + 1024
8 CoreFoundation CFRunLoopRunSpecific + 444
9 GraphicsServices GSEventRunModal + 180
10 UIKit -[UIApplication _run] + 684
11 UIKit UIApplicationMain + 208
12 MojiWeather main (main.m:16)
13 libdyld.dylib _start + 4
仅在iOS10以下设备出现。崩溃栈完全得不到有用信息,但是在Bugly上传的页面跟踪数据中可以看出每次崩溃的页面停留都是刚启动应用时在二级Splash展示时崩溃,我们项目中的二级Splash是通过设置RootViewController的方式展示的,因此基本判断是操作Splash的Window时出现的崩溃。继续排查log,发现几乎所有崩溃用户在application:didFinishLaunchingWithOptions:方法中的launchOption的key值都是UIApplicationLaunchOptionsLocationKey,因此判断该崩溃出现在用户后台定位发生改变时,并不是用户在前台操作产生。
触发场景
后台定位功能触发时,应用操作UIWindow导致。
解决方案
在application:didFinishLaunchingWithOptions:中判断launchOption,如果是UIApplicationLaunchOptionsLocationKey方式进入应用,则不触发二级Splash展示。即不操作UIWindow。
5、后台OpenGL绘制问题
由于项目接入了cocos2d-objc,因此陆陆续续出现过各类后台绘制问题。
这里统一梳理一下:只要崩溃栈中出现GLEngine、OpenGLES、gpusSubmitDataBuffers等相关字眼,即确认为绘制问题。
触发场景
这里的场景有两种:1、同问题4,launchOption为UIApplicationLaunchOptionsLocationKey时其实应用还在后台,此时触发OpenGL绘制即崩溃。
2、用户手动进入后台之后(即调用UIApplication DidEnterBackground),绘制没有停止。
解决方案
1、触发后台定位进入引用时,程序会走willFinishLaunchingWithOptions和didFinishLaunchingWithOptions,但是不会走didBecomeActive,因此可以在didBecomeActive方法中增加标志位,没有走过didBecomeActive时,不触发cocos2D的绘制。
2、在didEnterBackground增加停止绘制的操作。