问题
升级到Xcoce 14 + iOS 16后,有的项目可能遇到了 NSException: Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Application circumvented Objective-C runtime dealloc initiation for <UIViewController> object.'这样的错误。
这个问题笔者最早是在 iOS-Weekly #3551 看到的
必现Demo:
#import "ViewController.h"
@implementation UIViewController (Test)
+ (void)initialize {
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
// 调用UIViewController
[[UIViewController alloc] init];
}
@end
分析
根据 iOS-Weekly #3551 中的描述是某些开发同学不走寻常路,在category中重写系统UI类的+ (void)initialize方法导致的,我们都知道initialize是通过消息发送调用的,会导致原始类中的initialize不执行,既然现在断言了,大概率是苹果粑粑在initialize中做了什么处理(非本文讨论重点,未分析),而开发同学的骚操作影响了苹果粑粑的某些流程,导致它不乐意了。
我们看下堆栈:
通过crash堆栈,很容易看出是由于dealloc中触发了断言导致的。既然是断言,那就简单了,解决断言问题就好了。
解决
子类化一个NSAssertionHandler类,覆写父类方法,然后让这个对象替换掉当前线程的assert处理,把断言吃掉。
@interface ZDAssertionHandler : NSAssertionHandler
@end
@implementation ZDAssertionHandler
+ (void)load {
// 核心实现,建议放到 `- application:didFinishLaunchingWithOptions:`中
ZDAssertionHandler *assertionHandler = [[ZDAssertionHandler alloc] init];
NSThread.currentThread.threadDictionary[NSAssertionHandlerKey] = assertionHandler;
}
- (void)handleFailureInMethod:(SEL)selector
object:(id)object
file:(NSString *)fileName
lineNumber:(NSInteger)line
description:(NSString *)format, ...
{
NSLog(@"NSAssert Failure: Method %@ for object %@ in %@#%li", NSStringFromSelector(selector), object, fileName, (long)line);
}
- (void)handleFailureInFunction:(NSString *)functionName
file:(NSString *)fileName
lineNumber:(NSInteger)line
description:(NSString *)format, ...
{
NSLog(@"NSCAssert Failure: Function (%@) in %@#%i", functionName, fileName, line);
}
弊端
上面的处理只是吃掉了主线程的assert(因为替换的是主线程的NSAssertionHandler),也就是说,这个问题只发生在UI层的类的话,那上面的处理方式完全可以覆盖到,因为UI类的dealloc最终都会切回主线程调用(哪怕这个UI对象最后一个引用计数是在子线程减掉的)。
但是如果其他系统类也存在类似校验,并且dealloc发生在子线程,那上面的方式就捉襟见肘了。如果真存在这种情况,那就临时靠 hook NSAssertionHandler来应急处理下。
总结
说了那么多,其实笔者并不认为这是正确的做法,这充其量只能算是临时救急处理。正统的做法应该是老老实实把项目中覆盖系统实现的代码改掉,不要给自己以及后面接手项目的同学留坑。抱着侥幸心理迟早要还的。
还有,笔者暂未测试其他非UI系统类是否也存在这种问题。。。
最后,以上内容,仅供参考,至于只处理断言,不从根本上解决问题是否还存在其他隐患,暂未可知,建议采用正统的处理方法。
附赠
小技巧:
检查分类覆盖原始类实现可以通过在Xcode开启环境变量OBJC_PRINT_REPLACED_METHODS为YES帮我们打印出来。