Foundation
+[NSUserDefaults(NSUserDefaults_NSURLExtras) _web_defaultsDidChange]
현재 회사에서 서비스 중인 앱에서 오랜 기간 동안 리포트되고 있던 크래시가 있습니다.
Fatal Exception: NSInvalidArgumentException
*** -[NSTaggedPointerString stringByReplacingCharactersInRange:withString:]: nil argument
이름 그대로 예상치 못한 전달 인자로 인한 Exception 이며 NSTaggedPointerString 에 nil 이 인자로 전달되었다는 뜻입니다.
이 크래시의 특징은 iOS 13 운영체제에서만 발생한다는 것이고 기기 상태가 0% 백그라운드인 것으로 보아 유저의 특정 행동에 의해 앱이 강제 종료된다는 것을 추론해볼 수 있습니다.
그럼 유저가 어떤 행동을 하다가 크래시가 발생했는지 알기 위해 해당 유저가 앱을 사용하는 과정에서 Firebase 와 Amplitude 에 전달된 이벤트를 분석했습니다.
유저들의 행동 중 공통점은 본인 인증이 필요한 기능들을 이용하려는 과정에서 이벤트가 마지막으로 기록되었다는 점,
QA 과정에서 동일한 크래시가 발생했고, 테스트하던 화면은 WKWebview 를 통해서 휴대 전화를 이용해 본인 인증 절차를 하던 과정이라는 점을 보아 "본인 인증 단계에서 크래시가 발생했구나!" 라는 결론을 내릴 수 있었습니다.
크래쉬 재현 방법
일반적으로 본인 인증 단계에서는 휴대 전화로 오는 인증 번호를 입력하는 방법으로 진행합니다.
이 단계에서 OS 는 인증 번호를 입력하는 공간에 커서를 가져다 놓으면 문자 메시지로부터 인증 번호를 가져와서 자동 완성을 제공합니다.
인증 번호가 키패드의 자동 완성에 입력된 경우 해당 영역을 한 번 터치하고, 빈 영역을 한 번 더 터치하면 앱이 강제 종료됩니다.
원인은 빈칸을 터치했을 때 replacingCaracters(in: 인자1 with: 인자2) 에서 인자2에 빈 문자열이 전달되어야 하는데, nil 이 전달됨으로 인해서 크래시가 발생합니다. 해결 방법은 아래와 같습니다.
해결 방법
NSString+Extension.swift
extension NSString {
class func swizzleReplacingCharacters() {
let originalMethod = class_getInstanceMethod(
NSString.self, #selector(NSString.replacingCharacters(in:with:)))
let swizzledMethod = class_getInstanceMethod(
NSString.self, #selector(NSString.swizzledReplacingCharacters(in:with:)))
guard let original = originalMethod, let swizzled = swizzledMethod else {
return
}
method_exchangeImplementations(original, swizzled)
}
func swizzledReplacingCharacters(in range: NSRange, with replacement: String) -> String {
return self.swizzledReplacingCharacters(in: range, with: replacement)
}
}
AppDelegate.swift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if UIDevice.current.systemVersion.hasPrefix("13") {
NSString.swizzleReplacingCharacters()
}
return true
}
}
해결 방법은 간단합니다. 메서드 스위즐링을 이용해서 해당 다른 메서드를 대체합니다. 그리고 해당 메서드를 AppDelegate 의 didFinishLaunchingWithOptions 에서 호출합니다.
추가로 해당 크래시는 iOS 14 베타 에서 발생하지 않았습니다.사실 이 글을 작성하면서도 메서드 스위즐링을 통해 대체하는 메서드가 어떤 원리로 크래시를 방지하는지 이해를 못 했습니다.혹시나 원리에 대해서 아시는 분은 저에게 알려주시면 해당 내용에 대해서 포스팅에 반영하겠습니다.(소정의 스타벅스 아이스 아메리카노와 함께)해당 코드의 동작 원리는 이 글에서 다루고 있습니다.
- 레이더에 등록된 해당 이슈 : openradar.appspot.com/7428013
'iOS' 카테고리의 다른 글
Fatal Exception: NSInvalidArgumentException -[UILabel setLineBreakStrategy:]: unrecognized selector sent to instance (1) | 2021.08.26 |
---|---|
UIImage(contentsOfFile:), UIImage(named:) 차이 (0) | 2021.05.06 |