一. 背景

线上捕获到了关于_SFAppPasswordSavingViewController的相关溃散。具体溃散仓库如下:

Application tried to present  溃散管理

这个溃散呈现在钱包提现输入暗码页面。

二. 原因剖析

从溃散原因剖析:

Application Specific Information:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Application tried to present modally a view controller <_SFAppPasswordSavingViewController: 0x120d60030> that is already being presented by <UIKeyboardHiddenViewController_Save: 0x123849e30>.
UserInfo:(null)'

是因为Application测验去present存储暗码_SFAppPasswordSavingViewController页面,但这个_SFAppPasswordSavingViewController已经present过,导致呈现这个溃散。而从发生该溃散体系剖析来看,这个溃散根本发生在iOS16及相关体系,从现在来看应该是体系原因,首要呈现键盘弹起主动暗码填充界面。

Application tried to present  溃散管理

我们从苹果论坛上面也能看到关于这个crash的相关反馈:

developer.apple.com/forums/thre…

里面有人说到针对这个溃散的解决办法如下:

github.com/hackiftekha…

也就是hookUIViewControllerpresentViewController:animated:completion:办法,然后在hook办法里面增加判别当时的VCpresentedViewController是否有值,如果为nil,表示还没present过,直接调用“presentViewController:animated:completion:,如果有值,表示当时VC已经present过了,直接return`。

#import "UIViewController+PresentKBHook.h"
@implementation UIViewController (PresentKBHook)
+ (void)load {
    [UIViewController ff_swizzleInstanceMethodWithSrcClass:[UIViewController class] srcSel:@selector(presentViewController:animated:completion:) swizzledSel:@selector(cr_presentViewController:animated:completion:)];
}
- (void)cr_presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion {
    if (self.presentedViewController) {
        NSLog(@"[present devil]self=%@,toPresent=%@",self.description, viewControllerToPresent.description);
    }else{
        [self cr_presentViewController:viewControllerToPresent animated:flag completion:completion];
    }
}
/**
 Implementation of exchanging two object methods
 */
+ (void)ff_swizzleInstanceMethodWithSrcClass:(Class)srcClass
                                      srcSel:(SEL)srcSel
                                 swizzledSel:(SEL)swizzledSel{
    Method srcMethod = class_getInstanceMethod(srcClass, srcSel);
    Method swizzledMethod = class_getInstanceMethod(srcClass, swizzledSel);
    if (!srcClass || !srcMethod || !swizzledMethod) return;
    //Add a layer of protection measures. If the method is added successfully, it means that the method does not exist in this class, but exists in the parent class, and the method of the parent class cannot be exchanged, otherwise the method will crash when the parent class object calls the method; Adding failure indicates that the method exists in this class
    BOOL addMethod = class_addMethod(srcClass, srcSel, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if (addMethod){
        //After the IMP is successfully implemented by adding methods, the original implementation will be replaced with the swizzledMethod method to achieve method exchange without affecting the implementation of the parent method
        class_replaceMethod(srcClass, swizzledSel, method_getImplementation(srcMethod), method_getTypeEncoding(srcMethod));
    }else{
        //Adding failed, calling the implementation of the two interactive methods
        method_exchangeImplementations(srcMethod, swizzledMethod);
    }
}
@end

三. 解决方案

  • 增加降级开关、增加体系版本判别、只在iOS 16相关体系启用该溃散管理。
    /// 键盘溃散修正战略
    public struct FJFPresentCrashFixPolicy: HandyJSON {
        /// presnet事情 溃散修正
        public var viewControllerPresentCrashFixEnable: Bool = false
        /// 开端 修正 版本
        public var startFixVersion: String = "16.0"
        /// 结束 修正 版本
        public var endFixVersion: String = "17.0"
        public init() {}
    }
    /// 是否为需要修正的体系版本
     public static func isNeedToFixSystemVersionCrash(startVersion: String, endVersion: String) -> Bool {
        let curVersion = UIDevice.current.systemVersion
        if curVersion.compare(startVersion, options: .numeric) == .orderedDescending,
           curVersion.compare(endVersion, options: .numeric) == .orderedAscending {
            return true
        }
        return false
    }
}