Search code examples
iosswiftmemory-managementnsdata

Why does app crash at NSData getBytes?


NSData is extended to determine the file type:

extension NSData {
    var dataType: String? {

        // Ensure data length is at least 1 byte
        guard self.length > 0 else { return nil }

        // Get first byte
        var c = [UInt8](count: 1, repeatedValue: 0)
        self.getBytes(&c, length: 1)

        // Identify data type
        switch (c[0]) {
        case 0xFF:
            return "jpg"
        case 0x89:
            return "png"
        case 0x47:
            return "gif"
        case 0x49, 0x4D:
            return "tiff"
        default:
            return nil //unknown
        }
    }
}

The method above is called on a NSData object from image data that is fetched from a server.

dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {

    do {
        // Fetch image synchronously from server
        let query = PFQuery(className: <...>)
        let result = try query.getFirstObject()

        guard
            let imageObject = result.objectForKey(<...>) as? PFFile,
            let imageData = try? imageObject.getData(),
            let image = imageData.dataType == "gif" ? UIImage.animatedImageWithAnimatedGIFData(imageData) : UIImage(data: imageData)
        else {
            return
        }

        <...>

    } catch (let error as NSError) {
        <...>
    }
}

However the app very rarely crashes at line self.getBytes:

enter image description here

What is the reason for this?

The buffer of getBytes is &c, an UnsafeMutablePointer - do I have to take any special memory considerations because of that?

Update

The crashes still occur with the following variation of the code:

// Get first byte
var c: UInt8 = 0;
self.getBytes(&c, length: 1)

Update

The crashes still occur with the following variation of the code:

// Get first byte
var c = [UInt8](count: 1, repeatedValue: 0)
c.withUnsafeMutableBufferPointer {
    buffer in
    getBytes(buffer.baseAddress, length: 1)
}

guard c.indices.contains(0) else { return nil }

I got the following crash and included the whole thread, maybe someone can spot a hint:

Thread 18 Crashed:
0   libsystem_platform.dylib             0x21a8e198 _platform_memmove$VARIANT$CortexA9 + 92
1   Foundation                           0x22512923 __34-[_NSDispatchData getBytes:range:]_block_invoke + 176
2   libdispatch.dylib                    0x218d238d _dispatch_data_apply + 82
3   libdispatch.dylib                    0x218d4a51 dispatch_data_apply + 26
4   Foundation                           0x22512865 -[_NSDispatchData getBytes:range:] + 86
5   Foundation                           0x2267730b -[_NSDispatchData getBytes:length:] + 24
6   MyAppName                               0x00079ba0 partial apply forwarder for (extension in MyAppName):__ObjC.NSData.(dataType.getter : Swift.String?).(closure #1) (NSData+Extension.swift:54)
7   MyAppName                               0x00079c14 partial apply forwarder for reabstraction thunk helper from @callee_owned (@inout Swift.UnsafeMutableBufferPointer<Swift.UInt8>) -> (@unowned (), @error @owned Swift.ErrorType) to @callee_owned (@inout Swift.UnsafeMutableBufferPointer<Swift.UInt8>) -> (@out (), @error @owned Swift.ErrorType) (NSData+Extension.swift:0)
8   MyAppName                               0x00079cb8 generic specialization <Swift.UInt8, ()> of Swift.Array.withUnsafeMutableBufferPointer <A> ((inout Swift.UnsafeMutableBufferPointer<A>) throws -> A1) throws -> A1 (NSData+Extension.swift:0)
9   MyAppName                               0x00079a70 (extension in MyAppName):__ObjC.NSData.dataType.getter : Swift.String? (NSData+Extension.swift:55)
10  MyAppName                               0x00079948 @objc (extension in MyAppName):__ObjC.NSData.dataType.getter : Swift.String? (NSData+Extension.swift:0)
11  MyAppName                               0x000d2264 MyAppName.DataManager.(fetchImagesFromServer (MyAppName.ImageSet) -> ()).(closure #1) (DataManager.swift:1214)
12  libdispatch.dylib                    0x218cd823 _dispatch_call_block_and_release + 8
13  libdispatch.dylib                    0x218dc5e9 _dispatch_root_queue_drain + 1558
14  libdispatch.dylib                    0x218dbfcd _dispatch_worker_thread3 + 94
15  libsystem_pthread.dylib              0x21a91b29 _pthread_wqthread + 1022
16  libsystem_pthread.dylib              0x21a91718 start_wqthread + 6

Update

The crashes still occur with the following variation of the code:

// Get first byte
var c = UnsafeMutablePointer<UInt8>.alloc(1)
defer { c.dealloc(1) }
self.getBytes(c, length: 1)    

switch (c[0]) { ...

Solution

  • With the help of an Apple engineer (via a TSI ticket) the issue was finally identified. All code permutations above for reading the first byte are valid and working.

    The issue was that the NSData object was created when a file was fetched from a server using the Parse iOS SDK which stores the data in a temporary file with file protection key NSFileProtectionCompleteUntilFirstUserAuthentication.

    The file protection key allows reading data of the NSData object only after the user unlocks the device once after reboot. Although the data is not readable before unlocking, the NSData object can be created and even the NSData.length property is accessible. However, attempting to read the data would throw an exception.

    I changed the code and added a check if the protected data is available before attempting to read it with UIApplication.sharedApplication().protectedDataAvailable.

    You may wonder why a file was fetched by the app before the device was even unlocked. The app was started by a remote user notification. That explains why the crash happened so rarely.

    Learned 2 things:

    • Always check your file protection key
    • Apple technical support gave a super in-depth explanation and is worth the money