Работа с форматом JSON в Swift на первый взгляд не представляет особых сложностей, с одной стороны в стандартном наборе есть класс NSJSONSerialization который умеет парсить файлы, с другой стороны множество сторонних библиотек обещающих сделать этот процесс проще, а код нагляднее. В рамках же данной статьи я хотел бы рассмотреть как читать JSON файлы быстрее и почему очевидные подходы работают медленно.

И так, сначала стоит сформулировать решаемую задачу: есть некий достаточно объемный JSON (около 30 мегабайт) следующей структуры:

data -> [Node] -> [Item] -> id: String, pos: [Int], coo: [Double]

Требуется распарсить и получить массив объектов типа Item соответсвенно имеющих строковое поле id, поле pos — целочисленный массив и поле coo — массив чисел с плавающей точкой.

Вариант первый — использование сторонней библиотеки:

Надо сказать что все попавшиеся мне решения использовали в качестве парсера стандартный NSJSONSerialization, а свою задачу видели исключительно в добавлении “синтаксического сахара” и более строгой типизации. В качестве примера возьмем одну из наиболее популярных SwiftyJSON:

let json = JSON(data: data)     
if let nodes = json["data"].array
{
    for node in nodes
    {
        if let items = node.array
        {
            for item in items
            {
                if let id  = item["id"].string,
                   let pos = item["pos"].arrayObject as? [Int],
                   let coo = item["coo"].arrayObject as? [Double]
                {
                    Item(id: id, pos: pos, coo: coo)
                }
            }
        }
    }
}

На iPhone 6 выполнение данного кода заняло примерно 7.5 секунд, что непозволительно много для довольно быстрого устройства.
Для дальнейшего сравнение будем считать это время эталонным.
Теперь попробуем написать то же самое без использования SwiftyJSON.

Вариант второй — использование «чистого» swift:

let json = try! NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions())
if let nodes = (json as? [String: AnyObject])?["data"] as? [[[String: AnyObject]]]
{
    for node in nodes
    {
        for item in node
        {
            if let id  = item["id"] as? String,
               let pos = item["pos"] as? [Int],
               let coo = item["coo"] as? [Double]
            {
                Item(id: id, pos: pos, coo: coo)
            }
        }
    }
}

Наш «велосипед» справился за 6 секунд (80% от изначального), но все равно очень долго.

Попробуем разобраться, профайлер подсказывает что строка:

let nodes = (json as? [String: AnyObject])?["data"] as? [[[String: AnyObject]]]

выполняется неожиданно долго.

Поведение класса NSJSONSerialization в Swift'е полностью аналогично его поведению в Objective C, а значит результатом парсинга будет некая иерархия состоящая из объектов типа NSDictionary, NSArray, NSNumber, NSString и NSNull. Данная же команда преобразует объекты этих классов в структуры Swift'а Array и Dictionary, а значит копирует данные! (Массивы в Swift более сходны с массивами в C++ чем в Objective C)

Чтобы избежать подобного копирования попробуем не использовать красивые типизированные массивы Swift'a.

Вариант третий — без использования Array и Dictionary:

let json = try! NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions())
if let nodes = (json as? NSDictionary)?["data"] as? NSArray
{
    for node in nodes
    {
        if let node = node as? NSArray
        {
            for item in node
            {
                if  let item = item as? NSDictionary,
                    let id = item["id"] as? NSString,
                    let pos = item["pos"] as? NSArray,
                    let coo = item["coo"] as? NSArray
                {
                    var _pos = [Int](count: pos.count, repeatedValue: 0)
                    var _coo = [Double](count: coo.count, repeatedValue: 0)
                            
                    for var i = 0; i < pos.count; i++
                    {
                        if let p = pos[i] as? NSNumber {
                            _pos.append(p.integerValue)
                        }
                    }
                
                    for var i = 0; i < coo.count; i++
                    {
                        if let c = coo[i] as? NSNumber {
                           _coo.append(c.doubleValue)
                        }
                    }
                            
                    Item(id: String(id), pos: _pos, coo: _coo)
                }
            }
        }
    }
}

Выглядит, конечно, ужасно. Но нас интересует прежде всего скорость работы: 2 сек (почти в 4 раза быстрее SwiftyJSON!)

Таким образом исключив неявные преобразования можно добиться значительного увеличения в скорости.

Комментарии (6)


  1. Gorthauer87
    11.01.2016 13:12

    Работа со строками — извечная боль для любого программиста. Интересно, а как в swift'е обстоят дела со слайсами строк? По идеи если исходная строка не будет удаляться из памяти, то нам достаточно иерархию объекта сделать поверх слайсов, а не создавать новые строки и копировать данные туда, при этом и код не должен получится страшным.


  1. Shannon
    11.01.2016 14:36

    SwiftyJSON популярный, но очень не эффективный — habrahabr.ru/post/270063/#comment_8641347
    Вариант из той статьи работает значительно лучше. Используя его, в принципе в большинстве задач, можно вообще json не перегонять в другой формат, а работать с ним как есть:

    let obj = json?["workplan"]?["presets"]?[1]?["id"] as? Int 
    


    1. RedRover
      11.01.2016 14:43

      Можно, хотя в данном примере если в массиве presets не найдется элемента с индексом 1 то будет исключение.

      И вот так работает быстрее, хотя объяснений у меня этому нет:

      let obj = (json?["workplan"]?["presets"]?[1]?["id"] as? NSNumber)?.integerValue
      



      1. Shannon
        11.01.2016 15:38

        Вроде не должно, так как там optional chaining то будет nil

        Проверил
                let jsonData = "{\"workplan\":{\"presets\":[{\"id\":0}, {\"id\":1}, {\"id\":2}]}}".dataUsingEncoding(NSUTF8StringEncoding)
                let json = JSON(jsonData)
        
                var obj = json?["workplan"]?["presets"]?[0]?["id"] as? Int
                print(obj) // Optional(0)
                var obj = (json?["workplan"]?["presets"]?[1]?["id"] as? NSNumber)?.integerValue
                print(obj) // Optional(1)
                obj = json?["workplan"]?["presets"]?[5]?["id"] as? Int
                print(obj) // nil
                obj = json?["foo"]?["doo"]?[543]?["q"] as? Int
                print(obj) // nil
        


      1. EvilPartisan
        12.01.2016 09:44
        +1

        Как сказано ниже, исключения не будет, будет nil.
        А по поводу скорости, это скорее всего потому что объекты ObjC (NSDictionary, NSArray) это не тоже самое что объекты Swift (Dictionary<Key, Value>, Array) И класс NSJSONSerialization выдает результат тоже Objc формате, и класс разбора по ссылке внутри себя тоже делает множество неявных преобразований, поэтому казалось бы невинные операции могут быть весьма ресурсоёмкими.
        Пока не будет полностью переписана стандартная библиотека (NSFoundation) специально под Swift, быстрым он так и не станет.
        В этом плане использование просто сишных библиотек подключенных к Swift является более производительным решением, хоть и менее удобным. Впрочем и там можно наткнуться на грабли с преобрахованиями


  1. stuit
    11.01.2016 17:27

    А если проверить эти решения на Objective-C интересно какая будет разница.