본문 바로가기

iOS

[iOS] NSTaggedPointerString Crash

Foundation

+[NSUserDefaults(NSUserDefaults_NSURLExtras) _web_defaultsDidChange]


현재 회사에서 서비스 중인 앱에서 오랜 기간 동안 리포트되고 있던 크래시가 있습니다.

 

Fatal Exception: NSInvalidArgumentException
*** -[NSTaggedPointerString stringByReplacingCharactersInRange:withString:]: nil argument

 

이름 그대로 예상치 못한 전달 인자로 인한 Exception 이며 NSTaggedPointerString 에 nil 이 인자로 전달되었다는 뜻입니다.

 

이 크래시의 특징은 iOS 13 운영체제에서만 발생한다는 것이고 기기 상태가 0% 백그라운드인 것으로 보아 유저의 특정 행동에 의해 앱이 강제 종료된다는 것을 추론해볼 수 있습니다.

 

Firebase Crashlytics 를 사용하면 해당 크래쉬가 발생한 디바이스의 정보를 알 수 있다.

그럼 유저가 어떤 행동을 하다가 크래시가 발생했는지 알기 위해 해당 유저가 앱을 사용하는 과정에서 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 베타 에서 발생하지 않았습니다.
사실 이 글을 작성하면서도 메서드 스위즐링을 통해 대체하는 메서드가 어떤 원리로 크래시를 방지하는지 이해를 못 했습니다.

혹시나 원리에 대해서 아시는 분은 저에게 알려주시면 해당 내용에 대해서 포스팅에 반영하겠습니다.
(소정의 스타벅스 아이스 아메리카노와 함께)

해당 코드의 동작 원리는 이 글에서 다루고 있습니다.

 

rdar://7428013: Exception when the autofill SMS code is tapped multiple times in the predictive keyboard

Exception when the autofill SMS code is tapped multiple times in the predictive keyboard Originator:pablobart Number:rdar://7428013 Date Originated:5 Nov 2019 Status:Open Resolved: Product:UIKit Product Version:12.0+ Classification: Reproducible:Yes Steps

openradar.appspot.com

 

 

autofill SMS code webview crash https://openradar.appspot.com/7428013

autofill SMS code webview crash https://openradar.appspot.com/7428013 - autofill-sms-code-webview-crash.swift

gist.github.com