mkilmanas
mkilmanas

Reputation: 3485

iOS crash stack trace makes little sense

I have obtained and managed to symbolicate a crash report for my iOS app. However, looking at the stack trace I can't seem to make sense of it.

Thread 0 Crashed:
0   libswiftCore.dylib                  0x00000001005d330c specialized _assertionFailure(StaticString, String, file : StaticString, line : UInt, flags : UInt32) -> Never (__hidden#17347_:134)
1   libswiftCore.dylib                  0x00000001004d7d2c Error<A where ...>._code.getter (__hidden#18979_:221)
2   libswiftCore.dylib                  0x00000001004d7bf4 swift_errorInMain + 0
3   MyAppName                              0x00000001000c2190 specialized PersonRepository.hydrate(Row) -> Person (PersonRepository.swift:0)
4   MyAppName                              0x00000001000fbebc specialized Database.checkSchemaVersion(connection : Connection) -> () (Database.swift:0)
5   MyAppName                              0x00000001000fc254 _TTSf4d_g__TZFC6MyAppName8DatabaseP33_909B711B8156620EE1EFE30EC21C4C0C11getInstancefT_S0_ (Database.swift:0)
6   MyAppName                              0x00000001000fade8 static Database.getInstance() -> Database (Database.swift:0)
7   MyAppName                              0x00000001000a7a6c TaskRepository.init() -> TaskRepository (TaskRepository.swift:38)
(......)

What makes me scratch my head is how on Earth did it get from level 4 up to level 3. In my undertanding, it implies that Database.checkSchemaVersion(connection : Connection) -> () at some point calls PersonRepository.hydrate(Row) -> Person - which makes no sense whatsoever.

Here's my full Database class (it's not too large)

// Database.swift
import SQLite
import Foundation

class Database
{
    private var connection : Connection?
    private static var instance : Database?

    private static func getInstance() -> Database
    {
        if (instance == nil) {
            instance = Database()
            instance!.checkSchemaVersion(connection: instance!.connection!)
        }
        return instance!
    }

    private init()
    {
        let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
        connection = try! Connection("(path)/myappname.sqlite3");
    }

    deinit {
        connection = nil
    }

    static func getConnection() -> Connection
    {
        return getInstance().connection!
    }

    func checkSchemaVersion(connection : Connection)
    {
        guard let buildString = Bundle.main.infoDictionary?["CFBundleVersion"] as? String else {
            Application.debug("Database: could not convert Build to String")
            return
        }
        guard let build = Int64(buildString) else {
            Application.debug("Database: could not convert Build from String to Int64")
            return
        }
        let schemaVersion = try! connection.scalar("PRAGMA user_version") as? Int64 ?? 0
        Application.debug("Database: checking schema version - DB=(schemaVersion) vs app=(build)")
        if schemaVersion < build {
            Application.debug("Database: old schema version detected - updating now")
            PersonRepository.getInstance().updateSchema(from: schemaVersion, to: build)
            PictureRepository.getInstance().updateSchema(from: schemaVersion, to: build)
            TaskRepository.getInstance().updateSchema(from: schemaVersion, to: build)
            TaskCommentRepository.getInstance().updateSchema(from: schemaVersion, to: build)
            VenueRepository.getInstance().updateSchema(from: schemaVersion, to: build)
            try! connection.run("PRAGMA user_version = (build)")
        }
    }
}

Any ideas what that stack trace (isn't very stacky, huh?) supposed to mean, or how does it arrive at this situation? No wonder it crashes if things are actually going in such a way.

UPDATE Although I believe that stack trace says that somewhere in Database.checkSchemaVersion(connection : Connection) -> () there is a direct call to PersonRepository.hydrate(Row) -> Person and this addition is extraneous, here's the relevant section of PersonRepository

private init()
{
    db = Database.getConnection()
    table = Table("persons")

    pictureRepository = PictureRepository.getInstance()

    try! db.run(table.create(ifNotExists: true) {
        t in
        t.column(id, primaryKey: true)
        t.column(name)
        t.column(email)
        t.column(phone)
        t.column(company)
        t.column(pictureId)
    })
}

public func updateSchema(from: Int64, to: Int64)
{
    if from < 2016121201 {
        try! db.run(table.addColumn(active, defaultValue: 1))
    }
}

static func getInstance() -> PersonRepository
{
    if (instance == nil) {
        instance = PersonRepository()
    }
    return instance!
}

Upvotes: 2

Views: 3720

Answers (1)

Vytis
Vytis

Reputation: 1180

One tip - avoid ! as much as possible. Especially for exception handling with try. There should be a good reason why things can throw and I believe this is the reason of your crash.

The stack trace might not be completely obvious because some of the method calls can be inlined/optimised by the compiler for release builds. In your case it's probably happens this way:

  1. checkSchemaVersion(connection : Connection)
  2. PersonRepository.getInstance()
  3. PersonRepository.init()
  4. try! db.run()
  5. Exception is thrown because something is fishy happens in Database

If you cannot reproduce the crash my advice would be to rewrite this code without force-unwrap and thinking what to do in case something goes wrong. One solution could be to use failable initialisers and handle possible nil values upstream, i.e.

private init?()
{
    db = Database.getConnection()
    table = Table("persons")

    pictureRepository = PictureRepository.getInstance()

    do {
        try db.run(table.create(ifNotExists: true) {
            t in
            t.column(id, primaryKey: true)
            t.column(name)
            t.column(email)
            t.column(phone)
            t.column(company)
            t.column(pictureId)
        })
    } catch {
        return nil
    }
}

Upvotes: 1

Related Questions