company_banner

Как мы боремся c динамическими библиотеками в Swift. Опыт Яндекса

    Честно говоря, когда мы приступили к работе над перезапуском Яндекс.Карт, я и представить себе не мог, сколько проблем нам в итоге доставит Swift. Если вы начали писать на Swift совсем недавно, то, поверьте, вы пропустили все самое интересное. Каких-то два года назад не было инкрементной компиляции в принципе (даже пробел в любом файле приводил к полной пересборке), сам компилятор постоянно вылетал с Segmentation Fault на вполне безобидных вещах вроде тройной вложенности типов или наследования от Generic, индексация проекта длилась невообразимо долго, автодополнение работало через раз и так далее и тому подобное, всех бед не счесть. Подобные моменты несомненно усложняют жизнь программистам, но постепенно решаются с каждым обновлением Xcode. Однако есть более существенные проблемы, влияющие не только на разработку, но и на качество приложения: запрет компиляции статических библиотек из свифтового кода, а также отсутствие поддержки Swift на уровне iOS.


    Изначально не было очевидно, что использование Swift и динамических библиотек приводит к росту времени запуска. Мы не сравнивали время запуска с предыдущей версией и воспринимали долгую загрузку как данность. Да и средств диагностики того, что же на самом деле происходит на этапе загрузки приложения, в общем-то не было. Но в один прекрасный день разработчики Apple добавили возможность профилирования работы системного загрузчика. Оказалось, что загрузка динамических библиотек занимает очень много времени по сравнению с другими этапами. Конечно, с нашим кодом тоже не все было идеально, но, пожалуй, это частные особенности отдельного приложения и не всем будет интересно о них читать. А вот борьба с динамическими библиотеками — общая тема для всех разработчиков, использующих Swift. Именно об этой проблеме и пойдет речь.

    Pre-main


    Загрузка приложения выполняется в два этапа. До запуска main системный загрузчик выполняет работу по подготовке образа приложения в памяти:

    1. загружает динамические библиотеки,
    2. проставляет адреса внешним указателям (bind) и базовые адреса внутренним указателям (rebase),
    3. создает контекст Objective-C,
    4. вызывает конструкторы глобальных переменных C++ и методы +load в классах Objective-C.

    Только после этого начинает выполняться код приложения.

    Замер pre-main — нетривиальная задача, поскольку этот этап выполняется системой и его нельзя залогировать как пользовательский код. К счастью, на WWDC 2016: Optimizing App Startup Time рассказали о переменной окружения DYLD_PRINT_STATISTICS, при включении которой в лог выводится статистика работы загрузчика по этапам. Например, для пустого приложения на Swift при запуске на iPhone 5 статистика следующая:
    Total pre-main time: 1.0 seconds (100.0%)
    dylib loading time: 975.17 milliseconds (95.8%)
    rebase/binding time: 14.39 milliseconds (1.4%)
    ObjC setup time: 12.46 milliseconds (1.2%)
    initializer time: 15.27 milliseconds (1.6%)
    Видно, что огромную часть pre-main занимает загрузка динамических библиотек. Их список можно получить, воспользовавшись переменной окружения DYLD_PRINT_LIBRARIES. Библиотеки делятся на системные и пользовательские, загружаемые из папки Frameworks бандла приложения.

    Загрузка системных библиотек оптимизирована — в этом просто убедиться, создав пустой проект на Objective-C и запустив его с DYLD_PRINT_LIBRARIES & DYLD_PRINT_STATISTICS:
    dyld: loaded: /var/containers/Bundle/Application/6232DEDA-1E38-44B9-8CE8-01E244711306/Test.app/Test
    ...
    dyld: loaded: /System/Library/Frameworks/JavaScriptCore.framework/JavaScriptCore
    dyld: loaded: /System/Library/Frameworks/AudioToolbox.framework/AudioToolbox
    dyld: loaded: /System/Library/PrivateFrameworks/TCC.framework/TCC

    Total pre-main time: 19.65 milliseconds (100.0%)
    dylib loading time: 1.32 milliseconds (6.7%)
    rebase/binding time: 1.30 milliseconds (6.6%)
    ObjC setup time: 5.11 milliseconds (26.0%)
    initializer time: 11.90 milliseconds (60.5%)
    Этап загрузки динамических библиотек выполняется практически мгновенно, хотя на самом деле их 147, и все — системные. Поэтому сфокусироваться нужно на пользовательских библиотеках.

    Минимальный набор динамических библиотек


    Прежде чем начинать работу над уменьшением числа динамических библиотек, нужно определить их минимальный набор в приложении, использующем Swift. Очевидно, что это будут динамические библиотеки, линкующиеся к пустому проекту. Чтобы их посмотреть, нужно после сборки перейти к собранному бандлу (через «Show in Finder» в контекстном меню) и зайти в папку Frameworks:



    Это так называемые Swift standard libraries (swift runtime). Если в проект добавлен хотя бы один файл *.swift, Xcode копирует их в бандл и линкует к бинарному файлу. Зачем они нужны? Все дело в молодости языка. Swift продолжает активно развиваться и не поддерживает бинарную совместимость. Если бы swift runtime сделали частью системы (как это уже давно сделано для Objective-C), то при очередном обновлении iOS старые программы не смогли бы работать на новой версии системы и наоборот. Поэтому приложения содержат копию swift runtime в папке Frameworks, причем система рассматривает их как пользовательские, отсюда и долгая загрузка. Такова плата за использование динамично развивающегося языка.

    Борьба с динамическими библиотеками


    Перейдем к более сложному примеру. Пусть некоторое приложение:
    — использует СocoaPods для подключения зависимостей, причем некоторые зависимости приходят готовыми динамическими библиотеками,
    — разбито на несколько таргетов,
    — использует CoreLocation, MapKit, AVFoundation.

    Внутри его бандла, в папке Frameworks, лежат следующие библиотеки:



    Статистика загрузки этого приложения на iPhone 5 выглядит так:
    Total pre-main time: 3.6 seconds (100.0%)
    dylib loading time: 3.5 seconds (95.3%)
    rebase/binding time: 50.04 milliseconds (1.3%)
    ObjC setup time: 59.78 milliseconds (1.6%)
    initializer time: 60.02 milliseconds (1.8%)

    Уменьшение числа Swift standard libraries


    Как видно, в указанном примере на пять библиотек swift runtime больше, чем в пустом проекте. Если в каком-либо файле *.swift есть import CoreLocation, или #import <CoreLocation/CoreLocation.h> стоит в bridging header, то Xcode добавляет в бандл libswiftCoreLocation.dylib. При этом использование #import <CoreLocation/CoreLocation.h> в коде на Objective-C не приводит к добавлению этой библиотеки. Напрашивается решение — сделать обертки Objective-C над нужными частями CoreLocation и использовать в приложении только их. Пример оберток можно посмотреть тут.

    К сожалению, этого может оказаться недостаточно из-за транзитивных зависимостей. Использование import MapKit в любом файле *.swift приводит к добавлению libswiftmapkit.dylib и libswiftCoreLocation.dylib, использование import AVFoundation — к добавлению libswiftAVFoundation.dylib, libswiftCoreAudio.dylib, libswiftCoreMedia.dylib. Поэтому нужные части MapKit и AVFoundation тоже приходится оборачивать. А еще libswiftCoreLocation.dylib добавляется, если есть #import <CoreLocation/CoreLocation.h> в каком-либо заголовочном файле, от которого транзитивно зависит bridging header. Если этот #import находится в какой-либо библиотеке, то ее тоже нужно будет обернуть. Все это звучит неприятно, но результат оправдан — можно достигнуть того же набора Swift standard libraries, что и в пустом приложении.

    Статическая линковка подов, поставляемых исходными файлами


    Следующий массовый источник динамических библиотек — поды, собираемые в динамические фреймворки при указании !use_frameworks в Podfile. Флаг !use_frameworks необходим для подключения зависимостей, написанных на Swift, поскольку Xcode не разрешает использование Swift в статических фреймворках — выкидывает ошибку «Swift is not supported for static libraries».

    На самом деле это не значит, что нельзя создавать и использовать статические библиотеки с кодом на Swift, так как статическая библиотека — это просто архив объектных файлов. Компилятор Swift для каждого исходного файла генерирует обычные объектники формата Mach-O. При помощи ar или libtool их можно заархивировать в статическую библиотеку и подставить результат в команду линковки:

    — Пусть модуль SomeLib состоит из двух файлов: SomeClass.swift и SomeOtherClass.swift. SomeLib можно собрать с Xcode 8.3.1 в статическую библиотеку и слинковать с main.swift следующими командами:
    DEVELOPER_DIR=/Applications/Xcode8.3.1.app/Contents/Developer/
    SWIFTC=$DEVELOPER_DIR/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftc
    SDK=$DEVELOPER_DIR/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.3.sdk

    # сгенерировать объектные файлы
    $SWIFTC -target armv7-apple-ios8.0 -sdk $SDK -module-name SomeLib \
    SomeClass.swift SomeOtherClass.swift -c

    # сгенерировать swiftmodule, необходимый для импорта в main.swift
    $SWIFTC -target armv7-apple-ios8.0 -sdk $SDK -module-name SomeLib \
    SomeClass.swift SomeOtherClass.swift -emit-module

    # создать статическую библиотеку из объектных файлов
    $libtool -static -o libSomeLib.a SomeClass.o SomeOtherClass.o

    # создать исполняемый файл из main.swift и libSomeLib.a
    $SWIFTC -target armv7-apple-ios8.0 -sdk $SDK -I . -L . main.swift -lSomeLib
    — Первые две команды можно объединить, используя OutputFileMap.json, как это делает Xcode. Конкретные параметры, с которыми драйвер компиляции swiftc вызывает компилятор swift, можно посмотреть, добавив опцию -v.

    К счастью, в Xcode 9 beta 4 запрет на использование Swift в статических фреймворках убран. Можно дождаться релиза Xcode и соответствующих правок в cocoapods (чтобы компилировались статические фреймворки), и проблема исчезнет сама собой. Но для тех, кто не планирует или не может перейти на Xcode 9, стоит упомянуть про имеющееся достаточно простое решение — cocoapods-amimono. Идея проста — после сборки каждого пода в отдельной билд-папке остаются билд-артефакты, в том числе объектные файлы. Вместо линковки с динамическими библиотеками можно слинковаться напрямую с объектными файлами, из которых они были собраны. Cocoapods-amimono:
    — добавляет билд-фазу, выполняющую скрипт, который составляет LinkFileList из объектных файлов, находящихся в build-папках подов,
    — линковку с фреймворками подов заменяет на линковку с LinkFileList,
    — удаляет встраивание фреймворков в бандл приложения.

    Решение работает: фреймворки подов исчезают из папки Frameworks, при этом можно использовать module import, то есть код приложения не меняется.

    Статическая линковка собственных таргетов


    Таким же образом можно избавиться и от динамических фреймворков, собираемых из пользовательских таргетов: либо дождаться Xcode 9 (и использовать статические фреймворки), либо линковать объектники напрямую в бинарный файл приложения, как это делает cocoapods-amimono. Для этого нужно:

    — оставить target в dependencies основного таргета,
    — не встраивать framework в бандл и не линковаться с ним,
    — добавить билд-фазу, составляющую LinkFileList, по аналогии с cocoapods-amimono:
    # таргеты, которые нужно статически влинковать
    DEPENDENCIES=('SomeTarget' 'SomeOtherTarget');
    ARCHS_LIST=($ARCHS)

    # итерация по архитектурам, для которых проводится сборка
    for ARCH in ${ARCHS[@]}; do
        DIR=$OBJECT_FILE_DIR_normal/$ARCH

        # абсолютный путь до создаваемого LinkFileList
        FILE_PATH=$DIR/$TARGET_NAME.Dependencies.LinkFileList
        FILE_LIST=""

        # итерация по таргетам
        for DEPENDENCY in "${DEPENDENCIES[@]}"; do
        
            # путь до папки, содержащей билд-артефакты таргета
            PATH=$CONFIGURATION_TEMP_DIR/${DEPENDENCY}.build/Objects-normal/$ARCH

            # паттерн объектных файлов
            SEARCH_EXP="$PATH/*.o"

            # итерация по всем файлам, удовлетворяющим SEARCH_EXP
            for OBJ_FILE in $SEARCH_EXP; do
                # добавить файл в FILE_LIST
                FILE_LIST+="${OBJ_FILE}\n"
            done
        done
        FILE_LIST=${FILE_LIST%$'\n'}

        # записать FILE_LIST на диск по пути FILE_PATH
        echo -n -e $FILE_LIST > $FILE_PATH
    done
    — линковать основной таргет с LinkFileList. Для этого в OTHER_LDFLAGS добавить:

    -filelist "${OBJECT_FILE_DIR_normal}/${CURRENT_ARCH}/${TARGET_NAME}.Dependencies.LinkFileList"

    Ленивая загрузка динамических библиотек


    С готовыми динамическими фреймворками сложнее, так как динамическую библиотеку нельзя преобразовать в статическую — это по сути исполняемый файл, допускающий только динамическую линковку. Если это core-framework приложения и его символы нужны сразу при запуске — ничего не сделать, его использование будет неизбежно увеличивать время запуска. Но если фреймворк не используется при старте программы, то можно загружать его лениво через dlopen. Причем лениво загружать через dlopen+dlsym можно только совместимую с Objective-C часть интерфейса, поскольку при module import в Swift библиотека линкуется автоматически. Если все необходимое доступно из Objective-C, то нужно:

    1. Убрать линковку библиотеки с основным таргетом. Если зависимость подключается через cocoapods, то убрать линковку можно через добавление фальшивого таргета (к которому будут привязаны проблемные поды) или через post_install в Podfile:
    post_install do | installer |

        # вызвать Amimono::Patcher.patch!(installer), если используется amimono

        # итерация по таргетам, агрегирующим зависимости основных таргетов
        installer.aggregate_targets.each do |aggregate_target|

            # xcconfig-и, с которыми собираются агрегируюшие и основные таргеты
            target_xcconfigs = aggregate_target.xcconfigs
            # у каждой конфигурации - свой xcconfig
            aggregate_target.user_build_configurations.each do |config_name,_|
                
                # путь до xcconfig для конкретной конфигурации
                path = aggregate_target.xcconfig_path(config_name)

                # взять текущее состояние
                xcconfig = Xcodeproj::Config.new(path)
                
                # удалить что нужно
                xcconfig.frameworks.delete("SomeFramework")
                
                # перезаписать
                xcconfig.save_as(path)
            end
        end
    end
    2. Написать на Objective-C обертку над framework-ом, реализующую ленивую загрузку библиотеки и нужных символов.

    — Загрузка библиотеки.
    #import <dlfcn.h>

    NSString *frameworksPath = [[NSBundle mainBundle] privateFrameworksPath];
    NSString *dyLib = @"DynamicLib.framework/DynamicLib";

    // абсолютный путь до файла библиотеки
    NSString *path = [NSString stringWithFormat:@"%@/%@", frameworksPath, dyLib];
    const char *pathPtr = [path cStringUsingEncoding:NSASCIIStringEncoding]

    // загрузка библиотеки
    void *handle = dlopen(pathPtr, RTLD_LAZY);
    — Получение имен символов в библиотеке DynamicLib, по которым их далее нужно загружать через dlsym.
    $nm -gU $BUNDLE_PATH/Frameworks/DynamicLib.framework/DynamicLib

    DynamicLib (for architecture armv7):
    00007ef0 S _DynamicLibVersionNumber
    00007ec8 S _DynamicLibVersionString
    0000837c S _OBJC_CLASS_$__TtC10DynamicLib16SomeClass
    00008408 D _OBJC_METACLASS_$__TtC10DynamicLib16SomeClass
    ...
    00004b98 T _someGlobalFunc
    000083f8 D _someGlobalStringVar
    000083f4 D _someGlobalVar
    ...
    — Загрузка и использование глобальных символов.
    // dlsym возвращает указатель на символ библиотеки.

    // получение указателя на функцию
    int (*someGlobalFuncPtr)(int) = dlsym(handle, "someGlobalFunc");

    // вызов функции по ее указателю
    someGlobalFuncPtr(5);

    // получение указателя на глобальную переменную
    int *someGlobalVarPtr = (int *)dlsym(handle, "someGlobalVar");
    NSLog(@"%@", *someGlobalVarPtr);

    // использование глобальной переменной через разыменование указателя
    NSString *__autoreleasing *someGlobalStringVarPtr =
    (NSString *__autoreleasing *)dlsym(handle, "someGlobalStringVar");

    NSLog(@"%@", *someGlobalStringVarPtr);
    *someGlobalStringVar = @"newValue";
    — Загрузка и использование классов. Objective-C позволяет вызвать у объекта типа id любой объявленный в каком-либо классе instance-метод, а у объекта типа Class — любой объявленный class-метод. Причем можно использовать заголовочные файлы с объявлением интерфейса нужного класса, это не вызывает автоматической загрузки библиотеки, как в случае со Swift.
    #import <DynamicLib/SomeClass.h>

    //dlsym возвращает сущность типа Class
    Class class = (__bridge Class)dlsym(handle,
    "OBJC_CLASS_$__TtC10DynamicLib16SomeClass")

    // вызов class-метода
    [class someClassFunc];

    // создание объекта
    SomeClass *obj = [(SomeСlass *)[class alloc] init];

    // использование
    NSLog(@"%@", obj.someVar)
    [obj someMethod];
    Пример целиком можно посмотреть тут. Стандартные действия по загрузке библиотеки и символов можно оформить в макросы, как это сделано в Facebook SDK.

    Результат оптимизаций


    В итоге остаются только библиотеки swift runtime и vendored-фреймворки, загружаемые по-возможности лениво. Причем набор библиотек swift runtime такой же, как у пустого приложения. Статистика pre-main теперь выглядит так:
    Total pre-main time: 1.0 seconds (100.0%)
    dylib loading time: 963.68 milliseconds (90.0%)
    rebase/binding time: 35.65 milliseconds (3.3%)
    ObjC setup time: 29.08 milliseconds (2.7%)
    initializer time: 41.35 milliseconds (4.0%)
    Время загрузки динамических библиотек сократилось c 3,5 до 1 секунды.

    Сохранение результата


    Есть пара простых предложений, как не испортить достигнутый результат с очередным обновлением. Первое — добавить в билд-фазы выполнение скрипта, проверяющего после сборки список библиотек и фреймворков в папке Frameworks бандла приложения — не появилось ли что-то новое.
    FRAMEWORKS_DIR="${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}"
    FRAMEWORKS_SEARCH_PATTERN="${FRAMEWORKS_DIR}/*"

    # возвращает все элементы на диске, удовлетворяющие FRAMEWORKS_SEARCH_PATTERN
    FRAMEWORKS=($FRAMEWORKS_SEARCH_PATTERN)

    # ожидаемый список файлов и папок в Frameworks
    ALLOWED_FRAMEWORKS=(libswiftFoundation.dylib SomeFramework.framework)

    for FRAMEWORK in ${ALLOWED_FRAMEWORKS[@]}
    do
        PATTERN="*${FRAMEWORK}"
        # удалить все элементы, удовлетворяющие PATTERN
        FRAMEWORKS=(${FRAMEWORKS[@]/${PATTERN}/})
    done

    echo ${FRAMEWORKS[@]}

    # вернуть число оставшихся элементов в FRAMEWORKS
    # любое ненулевое число будет интерпретировано как ошибка сборки
    exit ${#FRAMEWORKS[@]}
    Если появились какие-то новые файлы, это точно повод для разбирательств. Но могут быть библиотеки, которые должны загружаться лениво, и важно проверять, что они не начали загружаться на старте. Поэтому второе предложение — получать список загруженных библиотек через objc_copyImageNames и проверять список библиотек, загруженных из Frameworks:
    var count: UInt32 = 0

    // получение списка загруженных библиотек
    let imagesPathsPointer: UnsafeMutablePointer<UnsafePointer?>! =
                                                   objc_copyImageNames(&count)

    // ожидаемый список загруженных библиотек
    let expectedImages: Set = ["libswiftCore.dylib"]

    // путь до папки с библиотеками внутри бандла приложения
    let frameworksPath = Bundle.main.privateFrameworksPath ?? "none"

    for i in 0..<count {
        let pathPointer = imagesPathsPointer.advanced(by: Int(i)).pointee
        let path = pathPointer.flatMap { String(cString: $0) } ?? ""

        // системные библиотеки не учитываем
        guard path.contains(frameworksPath) else { continue }

        let name = (path as NSString).lastPathComponent
        assert(expectedImages.contains(name))
    }
    Список не должен меняться. Этих двух моментов вполне достаточно, чтобы увеличение времени pre-main за счет увеличения времени загрузки динамических библиотек не прошло незаметным.

    Заключение


    Перечисленные проблемы целиком и полностью порождены молодостью Swift. Часть из них исчезнет с выходом Xcode 9, в котором разрешены статические библиотеки на Swift, что позволит избавиться от костылей вроде cocoapods-amimono. Но окончательно проблема роста размера бандла и времени запуска приложения решится только тогда, когда swift runtime станет частью iOS. Причем еще какое-то время после этого приложениям придется таскать его с собой, чтобы поддерживать предыдущие версии системы. Разработка Swift 5 нацелена на стабилизацию бинарного интерфейса Swift standard library. Бинарный интерфейс планировалось стабилизировать в Swift 4, но Xcode 9 по-прежнему копирует swift runtime в бандл приложения с deployment target iOS 11, а значит, Swift все еще не является частью iOS.
    Метки:
    Яндекс 664,07
    Как мы делаем Яндекс
    Поделиться публикацией
    Комментарии 15
    • 0
      В итоге остаются только библиотеки swift runtime и vendored-фреймворки

      Возможно глупый вопрос, но я под iOS никогда не разрабатывал,
      а почему нельзя swift runtime пересобрать из исходников в статическую библиотеку?
      Вроде бы исходники доступны?

      • +1
        Действительно, исходники доступны — github.com/apple/swift/tree/master/stdlib
        Теоретически собрать их в статическую библиотеку можно. Более того, swiftc принимает флаг -static-stdlib, и он работает для Mac OS ($DEVELOPER_DIR/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift_static содержит статические либы только для macos)

        Но для iOS:
        1. Не могу сходу оценить какие переделки Xcode потребуются. Сейчас Xcode все, что касается swift runtime, делает автоматически.
        2. Нет гарантий, что стор примет такую сборку. В архиве, отправляемом в стор, лежит папка SwiftSupport, содержащая неподписанные библиотеки swift runtime, т.е. к ним особое отношение
        3. В Яндекс.Картах 4 исполняемых файла — основное приложение и три виджета. Если статически влинковать в них рантайм, то размер приложения вырастет на 60-70 мб
        • 0
          1. В Яндекс.Картах 4 исполняемых файла — основное приложение и три виджета. Если статически влинковать в них рантайм, то размер приложения вырастет на 60-70 мб

          А откуда такие цифры, проверяли? При статической линковке в отличие от динамической можно удалить ненужный код (флаг -dead_strip и его аналоги) и вполне возможно что за счет этого суммарный размер уменьшится, а не увеличится, т.к. каждый бинарник возьмет из runtime только малую часть и сумма будет меньше размер всего runtime в виде .so.


          Нет гарантий, что стор примет такую сборку. В архиве, отправляемом в стор, лежит папка SwiftSupport, содержащая неподписанные библиотеки swift runtime, т.е. к ним особое отношение

          А откуда будут какие-то папки, у вас же будет статическая линковка, кроме как дизасемблированием понять писали ли вы на swift или objective-c не должно быть возможным?

          • 0
            вполне возможно что за счет этого суммарный размер уменьшится, а не увеличится

            У нас огромное приложение. Вполне возможно, что стрип библиотеки не даст никакого эффекта.

            Кроме как дизасемблированием понять писали ли вы на swift или objective-c не должно быть возможным?

            В отличие от Objective-C, все символы Swift заманглены специальным образом и привязаны к модулю, см $nm -gU someBin | xcrun swift-demangle

            Есть еще момент — стор рекомпилирует приложение из биткода, и на этот процесс вообще никак нельзя повлиять, только биткод выключить. Но, например, для apple-watch его выключить нельзя.
          • +2
            Apple точно не пропустит такую сборку. Вот здесь явно указано, что в AppStore пропускаются приложения, собранные только с версией свифта, поставляемой с Xcode.
        • +3

          Мне одному кажется, что Свифт слегка недопилен? Особенно как для 3-й версии.

          • +1
            Конечно, впереди еще Swift 5 github.com/apple/swift-evolution
            • +2

              Я к тому, что эта цепочка версий больше похожа не на 1-2-3-4-5, а 0.2-0.4-0.6-0.8-1.0
              Любой другой язык за такие хронические breaking changes давно запинали бы ногами. Вон, Python 3 до сих пор вызывает сопротивление, уже который год.

              • 0
                По-настоящему breaking changes были при переходе с Swift 2* на Swift 3*, до сих пор не все перешли. С 3->4 полегче должно быть.
                • +1
                  У Питона беда в том, что он интерпретируемый, т.е. скрипты, написанные под 2.х не могут работать, если в системе стоит только 3.х. Свифт позволяет без проблем поддерживать и запускать уже собранные приложения, написанные на 2.0, т.к. рантайм поставляется вместе с самим приложением. Вот когда он станет частью iOS, тогда да, breaking changes не будет больше места. Все это взаимосвязано, если бы Apple интегрировали Swift 1.0 рантайм в iOS сразу, с бинарной совместимостью и прочим, мы бы так до сих пор и навешивали на него костыли. Но они этого не сделали, получив взамен возможность ломать язык практически как угодно, основываясь на опыте и предложениях реальных разработчиков, которые этим языком пользуются. Как только начало получаться то, что более-менее всех устраивает, начали думать о стабилизации. Так что в каком-то смысле да, это 0.1-0.2-0.3, но степень влияния сообщества на развитие языка получилась поразительная, и результат, как по мне, более чем хорош
            • +2
              я и представить себе не мог, сколько проблем нам в итоге доставит Swift

              А сколько еще доставит в будущем… Жаль, что ни одна компания, которая решает насильно внедрить Swift в продакшн-разработку, никогда не прислушивается к доводам «не надо, рано еще, подождите, проблем будет больше, чем пользы, остановитесь...». На моей памяти так. Кого ни убеждал — всегда не соглашаются. И лишь собственные грабли, на которые, такое ощущение, каждая компания должна наступить в обязательном порядке при iOS-разработке, «открывает глаза», да и то не всегда, к сожалению. Язык неплох, но рано ему в продакшн для серьезных проектов. Рано. Я сам разрабатываю для iOS еще с 4 ее версии, видел и кодил на всех версиях Swift, и пока еще ничего особо хорошего в нем не увидел, что перекрыло бы сопутствующие минусы.
              • +1
                Да. К сожалению swift на самом деле имеет проблемы. Мы столкнулись у себя и с динамическими библиотеками, и с крашами Xcode (особенно на 2.x версиях), и с долгой компиляцией, и с отваливающимся интелисенс (кстати на obj-c он тоже отваливается), но несмотря на это я считаю это лучше чем писать на obj-c.
                Сам язык при должном его понимании позволяет писать код быстрее, безопасней, короче и понятнее что в долгосрочной перспективе экономит уйму времени.
                Помимо этого все забывают про то, что код на swift исполняется быстрее в части алгоритмов, нежели чем на obj-c.

                На текущий момент не все так плохо:
                Xcode падает редко( у меня 6 с obj-c падал чаще)
                Интелисенс обычно после переключения файла оживает, но даже без него писать код становиться возможным, из-за более запоминаемого синтаксиса языка.
                Непонятные ошибки редкость, и ща частую уже пройденный этап.
                Инкрементальная сборка появилась, правда иногда перекомпилирует лишнее.
                Осталась 2 проблемы:
                Размер приложения, который разбухает на 20-25мб
                Долгая загрузка (pre-main), к сожалению у нас в компании это не решили, но статья заставила задуматься о том, что возможно надо было разобраться в этом сильнее.

                Кому что важнее то и выбирают. Если команда из 15 человек постоянна сталкивается с кодом любого другого человека, а обьем кода такой, что уже невозможно его весь помнить и приходится его читать, то я лично за swift. С другой стороны если вам важен размер и скорость загрузки, или отсутствуют разработчики способные разобраться в проблеме без google, то всеже лучше obj-c.
                • +1
                  Да вот конкретно на моем опыте как-то ни разу не получалось этой самой «долгосрочной перспективы» достаточной длительности, чтобы все сопутствующие разработке проблемы (в основном — потери времени на сборку, подлагивания, частые краши и отвал подсветки — на обжсях да, я изредка ее ловил, но это крайне редко) перекрылись плюсами от свифта. Это не говоря о переходе на новые версии, о потенциальных проблемах с поиском зависимостей, и так далее. Ну и размер, конечно, с загрузкой аппа и пересборкой никто не отменял, тоже верно. Особенно бесит адово долгая архивация приложения, когда приходилось делать проекты в CI — застрелиться можно. По скорости исполнения — вот имхо, на обжси написать приложение, работающее быстро, не составляет никаких проблем, были бы навыки. Да, в свифте, конечно, есть приятные конструкции, упрощающие (хотя иногда и нехило увеличивающие, не будем скромничать, пламенный привет составным генерикам в экстеншенах) код, но потери времени на все вышеперечисленное в моей практике обычно перекрывают экономию в скорости написания кода. Хотя соглашусь с вами, что каждый человек и каждая команда вольны выбирать то, что им больше нравится.
                  • 0
                    А чем плохи составные generic в extension?
                    Иногда просто необходимо для базовых протоколов описать функционал с включением или исключением конкретных типов.
              • 0
                Не планируете новый YandexMap Kit представить?

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.