Быстрая работа с JSON в Swift

Работа с форматом 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!)

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

© Habrahabr.ru