Application circumvented Objective-C runtime dealloc initiation for <%s> object

1,408 阅读3分钟

问题

升级到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中做了什么处理(非本文讨论重点,未分析),而开发同学的骚操作影响了苹果粑粑的某些流程,导致它不乐意了。

我们看下堆栈:

iShot_2022-09-22_20.14.58.png

通过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_METHODSYES帮我们打印出来。