Русский
Русский
English
Статистика
Реклама

Objective c

Перевод Горячая четвёрка умирающих языков программирования

25.09.2020 16:06:58 | Автор: admin
Я занимался поиском лучших языков программирования 2020 года и наткнулся на страницы, на которых шла речь о языках, теряющих популярность. Я программист, и я понимаю, что любому программисту крайне важно знать о том, какие технологии являются актуальными, а какие нет.

Каждый программист это писатель.

Серкан Лейлек


Я, после того, как насмотрелся на отчёты о языках программирования, теряющих актуальность, выбрал 4 языка, которые, как я полагаю, уже не стоят того, чтобы их изучали. Я, ради подкрепления своих выводов, прибегну к некоторым показателям популярности языков. В частности, речь идёт об индексе PYPL (PopularitY of Programming Language Index, индекс популярности языков программирования), о данных Google Trends и о некоторых сведениях, которые можно найти на платформе YouTube.


Фрагмент рейтинга PYPL (источник)

Платформа YouTube использовалась мной в качестве источника данных о перспективности языков программирования через оценку популярности видеоуроков по соответствующим языкам.

Хочу отметить, что анализ рейтингов языков программирования способен оказать большую помощь тому, кто планирует построить карьеру в сфере информационных технологий. Дело в том, что эти рейтинги позволяют выбирать языки, которые стоит учить, а правильный выбор языка может оказать огромное влияние на профессиональный путь программиста.

Но хватит предисловий. Поговорим об умирающих языках программирования.

1. Perl


Интерес к языку программирования Perl стремительно падает. Хорошие показатели он демонстрировал в период с 2004 по 2009 годы, а после этого начался спад. Хотя этот язык пока и не мёртв, но он уже и не очень-то жив.

Информацию по нему не особенно активно ищут на YouTube и в Google. Например, есть видео по Perl, загруженное 4 года назад и набравшее всего 240 тысяч просмотров.


Видео по Perl

Кроме того, показатели языка идут вниз и в рейтинге PYPL.

Я решил сравнить Perl с каким-нибудь другим языком, с Python в данном случае, и обратился к Google Trends.


Сравнение Perl (красная линия) и Python (синяя линия), последние 12 месяцев

Как видно, красная линия, представляющая Perl, находится где-то на уровне нуля.

2. Haskell


Язык Haskell выглядит лучше, чем Perl. Он, к тому же, используется во многих крупных компаниях вроде Facebook и IBM. На YouTube есть видео по Haskell, загруженное 5 лет назад. Оно набрало 585 тысяч просмотров.


Видео по Haskell

Посмотрим теперь на показатели Google Trends, сравним Haskell и Python.


Сравнение Haskell (синяя линия) и Python (красная линия), последние 5 лет

Haskell, в сравнении с Python, выглядит более чем скромно. Синяя линия, представляющая Haskell, стелется по земле, а Python, представленный красной линией, показывает уверенный восходящий тренд.

3. Objective-C


Язык Objective-C, если ориентироваться на рейтинг PYPL, вырос в популярности на 0,2%. А что будет, если взглянуть на данные с YouTube?


Видео по Objective-C

Видео, загруженное 5 лет назад, набрало 250 тысяч просмотров.

Обратимся теперь к показателям Google Trends.


Сравнение Objective-C (синяя линия) и Python (красная линия), последние 5 лет

Конечно, многие всё ещё пользуются Objective-C. Но, хотя по этому языку есть вакансии, если вы строите планы на будущее и посматриваете на Objective-C, то вам стоит переключить внимание на Swift.

4. Visual Basic for Applications


Visual Basic for Applications, VBA, был у всех на слуху в 2004 году, а вот после 2009 интерес к нему начал падать. Я, например, изучал этот язык в школе.

Рейтинг PYPL указывает на то, что популярность VBA упала на 0,2%.

В моей ленте на YouTube видео по VBA мне видеть не доводилось. Возможно, это говорит о падении интереса к данному языку. Правда, когда я поискал подобные видео, мне попалось одно, загруженное 9 месяцев назад, набравшее 810 тысяч просмотров.


Видео по VBA

Если посмотреть на данные по VBA, которые имеются на Google Trends, то окажется, что интерес к VBA с 2004 года стабильно падает.


Сравнение VBA (красная линия) и Python (синяя линия), c 2004 года по настоящее время

В целом можно отметить, что VBA, несмотря на падение популярности, выглядит в сравнении с другими умирающими языками не так уж и плохо.

Python


Я занимаюсь серверной разработкой, используя Python. Я, кроме того, сделал несколько проектов, используя фреймворк Django. Что тут сказать мне нравится Python.

Это, если верить тому, что выдаёт Google, язык, который лучше других языков помогает в поиске работы в 2020 году.


Языки, знание которых помогает в поиске работы

Я, например, создал проект на Django. А именно, речь идёт о сайте с вопросами и ответами для разработчиков. Этот проект всё ещё в работе. Я расширяю его и занимаюсь его оптимизацией.

Python в рейтинге PYPL демонстрирует рост на 2,9%. Если поинтересоваться данными YouTube по просмотрам видео о Python, то окажется, что они, за короткие промежутки времени, набирают миллионы просмотров.


Видео по Python

Анализ исследования Stack Overflow


Выше я опирался на рейтинг PYPL, на данные с Google Trends и на анализ видео по интересующим меня языкам программирования на YouTube. Теперь же я обращусь к результатам опроса разработчиков, проведённого Stack Overflow в 2020 году. А именно, к данным по языкам программирования, на которых программисты пишут, но не хотят продолжать этим заниматься.


Данные опроса Stack Overflow (источник)

Но надо отметить, что есть много вакансий, требующих знания языков, занимающих высокие позиции в этом антирейтинге. Тем, кто пишет на этих языках, хорошо платят, вознаграждая за их опыт. Эти языки используются во многих компаниях, хотя некоторые компании меняют их на что-то другое.


Зарплаты разработчиков и их связь с языками программирования (источник)

Итоги


Я постоянно наблюдаю за ситуацией в сфере языков программирования. И я считаю, что подобные наблюдения способны принести немалую пользу. Ведь правильный выбор языка способен помочь карьерному росту программиста.

Кроме того, я изучал некоторые из языков, о которых сегодня рассказал. Правда, на практике я ими не пользуюсь. Собственно, надеюсь, что эта статья сможет уберечь других от изучения устаревших языков.

Если же вы пишете на этих языках, если у вас есть крупные проекты и интересная работа, то не думаю, что вам стоит бросаться на поиски чего-то нового. Просто занимайтесь тем, чем занимались, и не обращайте внимания на всякие индикаторы, указывающие на то, что популярность того или иного языка снизилась на 0,2%.

Какими языками программирования вы дополнили бы список умирающих технологий из этой статьи?



Подробнее..

Перевод Связанные не явные выражения в Swift 5.4

14.04.2021 00:14:01 | Автор: admin

В Swift 5.4: не явные выражения для членов классов (также известные как точечный синтаксис) теперь могут использоваться даже при обращении к свойству или методу в результате такого выражения, пока окончательный тип возвращаемого значения остается прежним.

Обратите внимание, что на момент написания статьи Swift 5.4 находится в стадии бета-тестирования в качестве части Xcode 12.5.

На практике это означает, что всякий раз, когда мы создаем объект или значение с помощью статического API, или при обращении обращении к перечисляемому типу, мы теперь можем напрямую вызвать метод или свойство в этом экземпляре класса, и компилятор по-прежнему сможет вывести тот тип, к которому мы обращаемся.

Например, при создании экземпляра UIColor с использованием одного из встроенных статических API-интерфейсов, предоставленных в качестве части системы, мы теперь можем легко изменить альфа-компонент такого цвета без необходимости явно ссылаться на сам UIColor в таких ситуациях, как:

// In Swift 5.3 and earlier, an explicit type reference is always// required when dealing with chained expressions:let view = UIView()view.backgroundColor = UIColor.blue.withAlphaComponent(0.5)...// In Swift 5.4, the type of our expression can now be inferred:let view = UIView()view.backgroundColor = .blue.withAlphaComponent(0.5)...

Конечно, вышеупомянутый подход также работает при использовании наших собственных статических API, например, любых пользовательских определений UIColor, которые мы добавили с помощью расширения:

extension UIColor {    static var chiliRed: UIColor {        UIColor(red: 0.89, green: 0.24, blue: 0.16, alpha: 1)    }}let view = UIView()view.backgroundColor = .chiliRed.withAlphaComponent(0.5)...

Возможно, даже более интересным является то, какие двери открывает эта новая возможность с точки зрения дизайна API. В качестве примера, в Легком дизайне API в Swift мы рассмотрели следующий стиль API, включающий расширение структуры с помощью статических методов и свойств, что позволяет нам использовать ее способом, подобным типу перечисления:

extension ImageFilter {    static var dramatic: Self {        ImageFilter(            name: "Dramatic",            icon: .drama,            transforms: [                .portrait(withZoomMultipler: 2.1),                .contrastBoost,                .grayScale(withBrightness: .dark)            ]        )    }}

При использовании Swift 5.4 (или более поздних версий в будущем) мы могли бы добавить что-то вроде такого, что позволяет нам легко объединить два экземпляра ImageFilter, путем объединения их .transforms:

extension ImageFilter {    func combined(with filter: Self) -> Self {        var newFilter = self        newFilter.transforms += filter.transforms        return newFilter    }}

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

let filtered = image.withFilter(.dramatic.combined(with: .invert))

Довольно круто! Я собираюсь продолжить изучение того, какие API-интерфейсы позволяет мне разрабатывать эта новая языковая функциональность, и, конечно же, продолжу делиться своими знаниями с вами в будущих статьях.

Подробнее..

Модуляризация iOS-приложения Badoo борьба с последствиями

21.01.2021 20:07:17 | Автор: admin

В предыдущей статье я рассказывал о том, как мы выделили модуль чата в нашем приложении. Всё прошло успешно, и мы собирались распространить этот опыт начать тотальную модуляризацию в iOS-разработке Badoo. Даже презентовали подход продуктовым командам, командам, занимающимся тестированием и непрерывной интеграцией, и постепенно стали внедрять модуляризацию в наши процессы.

Мы сразу понимали, что будут сложности, поэтому не торопились и раскатывали решение поэтапно. Это помогло нам вовремя понять проблемы, о которых сегодня пойдёт речь.

В этой статье я расскажу:

  • как мы не потерялись в сложном графе зависимостей;

  • как спасли CI от чрезмерной нагрузки;

  • что делать, если с каждым новым модулем приложение запускается всё медленнее;

  • мониторинг каких показателей стоит предусмотреть и почему это необходимо.

Сложный граф зависимостей

Только когда количество модулей стало сильно расти, мы поняли, что связывание и поддержка развесистого графа зависимостей это действительно сложно. Наши представления о 50 модулях, отображённых в красивой иерархии, разбились о суровую реальность.

Так выглядел граф зависимостей Badoo к моменту, когда у нас было около 50 модулей:

Было потрачено немало усилий в попытках сделать визуализацию удобной и читабельной, но безрезультатно. Сложный граф помог нам понять несколько вещей:

  1. Визуализация всех зависимостей одновременно сложная и бессмысленная задача. При работе с графом нужно понимать, какая проблема ищется или решается и каких модулей она касается. Этим инструментом можно пользоваться эффективно только в рамках графа вокруг одного, максимум нескольких модулей.

  2. Сложности визуализации порождают сложности отладки. Найти фундаментальные проблемы в сложном графе зависимостей крайне непросто.

  3. Добавление зависимостей, особенно промежуточных (о них мы говорили в первой части), ресурсоёмкая задача, влекущая за собой дополнительную нагрузку на разработчиков и отвлекающая от решения продуктовых задач.

Мы поняли, что простой визуализацией не обойтись, работу с графом зависимостей нужно автоматизировать. Так появилась наша внутренняя утилита deps. Она была призвана решать наши новые задачи: искать проблемы в графе зависимостей, указывать на них разработчикам и исправлять типовые ошибки линковки.

Основные характеристики утилиты:

  • это консольное Swift-приложение;

  • работает с xcodeproj-файлами с помощью фреймворка XcodeProj;

  • понимает сторонние зависимости (мы не очень активно и охотно принимаем их в проект, но некоторые всё же используем; загружаются и собираются они через Carthage);

  • включена в процессы непрерывной интеграции;

  • знает о требованиях к нашему графу зависимостей и работает в соответствии с ними.

Последний пункт объясняет, почему не стоит надеяться на появление подобных утилит с открытым исходным кодом. Сегодня не существует универсальной хорошо описанной структуры проектов и их настроек, которая подойдёт всем. В разных компаниях могут отличаться различные параметры графа:

  • статическая или динамическая линковка;

  • инструменты поддержки сторонних зависимостей (Carthage, CocoaPods, Swift Package Manager);

  • хранение ресурсов в отдельных фреймворках или на уровне приложения;

  • и другие.

Поэтому, если вы смотрите в сторону 100+ модулей, на каком-то этапе вам, скорее всего, придётся задуматься о написании подобной утилиты.

Итак, для автоматизации работы с графом зависимостей мы разработали несколько команд:

  1. Doctor. Команда проверяет, все ли зависимости корректно связаны и встроены в приложение. После исполнения мы либо получаем список ошибок в графе (например, отсутствие чего-либо в фазе Link with binaries или Embedded frameworks), либо скрипт говорит, что всё хорошо и можно двигаться дальше.

  2. Fix. Развитие команды doctor. Эта команда в автоматическом режиме исправляет проблемы, найденные командой doctor.

  3. Add. Добавляет зависимость между модулями. Пока у вас простое небольшое приложение, добавление зависимости между двумя фреймворками кажется простой задачей. Но когда граф сложный и многоуровневый, а вы работаете с включёнными явными зависимостями, добавление нужных зависимостей становится задачей, которую вы не захотите из раза в раз делать руками. Благодаря команде add разработчики могут просто указать два названия фреймворков (зависимый и зависящий) и все фазы сборки заполнятся необходимыми зависимостями в соответствии с графом.

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

  1. Автоматизированную поддержку графа. Мы находим ошибки прямо в pre-commit hook, сохраняя стабильность и правильность графа и давая возможность разработчику в автоматическом режиме эти ошибки исправлять.

  2. Упрощённое редактирование. Добавить новый модуль или связи между модулями можно одной командой, что сильно упрощает их разработку.

Непрерывная интеграция не справлялась

Перед переходом на модуляризацию у нас было семь приложений, и мы придерживались стратегии проверь всё. Вне зависимости от того, что именно было изменено в нашем монорепозитории, мы просто пересобирали все приложения и прогоняли все тесты. Если всё было хорошо, то мы позволяли изменениям попасть в основную ветку разработки.

Модуляризация быстро дала нам понять, что этот подход плохо масштабируется. Количество модулей и количество CI-агентов, необходимых для распараллеливания проверки изменений, линейно связаны. Когда количество модулей увеличилось, увеличились и очереди на CI. Конечно, до какого-то момента можно просто покупать новые билд-агенты, но мы пошли более рациональным путём.

Кстати, это была не единственная проблема CI. Инфраструктура также подвержена проблемам: симулятор может не запуститься, память закончиться, диск повредиться и т. д. Доля таких проблем небольшая, но с увеличением количества проверяемых модулей (запущенных работ на агентах в целом) абсолютное число инцидентов выросло, что не позволяло команде CI оперативно обрабатывать входящие обращения разработчиков.

Мы безуспешно попробовали несколько подходов. Расскажу о них подробнее, чтобы вы не повторяли наши ошибки.

Очевидным решением было перестать собирать и проверять всё и всегда. Нужно было, чтобы CI проверял только то, что нужно проверить. Что не сработало:

  1. Вычисление изменений по структуре директорий. Мы пробовали положить в репозиторий файл с маппингом директория модуль, чтобы понимать, какие модули нужно проверить, но буквально через неделю из-за увеличившегося количества падений приложения на проде обнаружили, что файл был перенесён в новый модуль и изменён одновременно, а файл маппинга при этом обновлён не был. Контролировать обновление файла с маппингом автоматически не представлялось возможным и мы перешли к поиску иных решений.

  2. Запуск интеграционных проверок на CI ночью. В целом это неплохая идея, но разработчики не сказали нам за это спасибо. Регулярными стали ситуации, когда ты уходишь домой в надежде, что всё хорошо, а утром в корпоративном мессенджере получаешь сообщение от CI, что 25 проверок не прошли, и первое, что нужно делать, разбираться с ночными проблемами, которые, вероятно, ещё кого-то блокируют. В общем, мы не захотели портить завтраки людям и продолжили поиски оптимального решения.

  3. Разработчик указывает, что проверять. Этот эксперимент завершился быстрее всех буквально за пару дней. Благодаря ему мы узнали, что разработчики бывают двух типов:

    1. Те, кто на всякий случай проверяет всё.

    2. Те, кто уверен, что не мог ничего сломать.

Как вы поняли, из-за первых очереди на CI почти не становились меньше, а из-за вторых у нас ломалась основная ветка разработки.

В итоге мы вернулись к идее автоматизации вычисления изменений, но немного с другой стороны. У нас была утилита deps, которая знала про граф зависимостей и файлы проекта. А Git позволяла получить список изменённых файлов. Мы расширили deps командой affected, с помощью которой можно было получить список затронутых модулей, исходя из изменений, отражаемых системой контроля версий. Ещё более важно то, что она учитывала зависимости между модулями (если от затронутого модуля зависят другие модули, их тоже необходимо проверить, чтобы, например, в случае изменения интерфейса более низкого модуля верхний не перестал собираться).

Пример: изменения в блоках Регистрация и Аналитика на нашей схеме указывают на необходимость проверить также модули Чат, Sign In with Apple, Видеостриминг и само приложение.

Это был инструмент для CI. Но любой разработчик тоже имел возможность локально посмотреть, что он потенциально мог затронуть своими изменениями, чтобы проверить работоспособность зависящих модулей вручную.

В результате такой модернизации мы получили ряд бонусов:

  1. Мы проверяем в CI только то, что действительно было затронуто прямо или косвенно.

  2. Продолжительность CI-проверок перестала линейно зависеть от количества модулей.

  3. Разработчик понимает, что его изменения могут затронуть и где нужно быть осторожным.

  4. Механизм впоследствии был адаптирован не только для проверки безошибочной компиляции модулей, но и для запуска юнит-тестов затронутых модулей, что также существенно снизило нагрузку на CI и ускорило попадание изменений в мастер.

Ждём завершения Е2Е-тестов

Для приложения Badoo у нас есть более 2000 сквозных (end-to-end) тестов, которые его запускают и проходят по сценариям использования для проверки ожидаемых результатов. Если запустить все эти тесты на одной машине, то прогон всех сценариев займёт около 60 часов. Поэтому на CI все тесты запускаются параллельно насколько это позволяет количество свободных агентов.

После успешного внедрения фильтрации по изменениям для юнит-тестов нам хотелось реализовать подобный механизм и для сквозных тестов. И тут мы столкнулись с очевидной проблемой: сквозные тесты не соотносятся напрямую с модулями. Например, сценарий отправки сообщений в чате проверяет и модуль чата, и модуль загрузки изображений, и модуль, являющийся точкой входа в чат. На деле один сценарий может косвенно проверять до семи модулей.

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

Для каждого нового модуля отдельный скрипт в CI проверяет наличие модуля в этом маппинге, чтобы разработчики не забывали синхронизироваться с командой тестирования для пометки модуля необходимыми группами тестов.

Подобное решение едва ли можно назвать оптимальным, но всё же преимущества от его внедрения были ощутимы:

  1. Нагрузка на CI существенно снизилась. Чтобы не быть голословным, привожу график времени, которое задача на прогон сквозных тестов провела в очереди:

  2. Уменьшился шум инфраструктурных проблем (меньше запусков тестов меньше падений из-за зависших агентов, сломавшихся симуляторов, недостатка места и т. д.).

  3. Карта модулей и их тестов стала точкой синхронизации отделов разработки и тестирования. В рамках разработки программист и тестировщик обсуждают, например, какие из имеющихся групп тестов могут подойти для проверки в том числе нового модуля.

Медленный запуск приложения

Apple открыто говорит о том, что динамическое связывание замедляет запуск приложения, однако именно оно является вариантом по умолчанию для любого нового проекта, созданного в Xcode. Так выглядит реальный график времени запуска одного из наших проектов:

В середине графика видно резкое снижение времени. Причина переход на статическую линковку. С чем это связано? Инструмент динамической загрузки модулей dyld от Apple выполняет трудоёмкие задачи не совсем оптимальными способами, время исполнения которых линейно зависит от количества модулей. В этом и была основная причина замедления запуска нашего приложения: мы добавляли новые модули dyld работал всё медленнее (на графике синяя линия отражает количество добавляемых модулей).

После перехода на статическое связывание количество модулей перестало влиять на скорость запуска приложения, а с появлением iOS 13 Apple перешла на использование dyld3 для запуска приложений, что тоже поспособствовало ускорению этого процесса.

Стоит сказать, что статическая линковка несёт с собой и ряд ограничений:

  1. Одно из наиболее неудобных невозможность хранения ресурсов в статических модулях. Эта проблема частично решается в относительно новых XCFrameworks, но технологию пока что нельзя считать проверенной временем и полноценно готовой к Enterprise. Мы решили эту проблему созданием рядом с модулем отдельных бандлов, которые уже упаковываются в готовое приложение или тестовый бандл для прогона тестов. Обеспечением целостности графа при работе с ресурсами также занимается утилита deps, которая получила несколько новых правил.

  2. После перехода на статическую линковку нужно хорошенько протестировать приложение на предмет рантайм-падений. Чтобы исправить многие из них, вам просто придётся использовать не самые оптимальные параметры оптимизаций. Например, почти для всех Objective-C-модулей придётся включить флаг -all-load. Отмечу ещё раз, что решение всех этих проблем с вынесенными xcconfigами (про xcconfig в первой части) не было таким мучительным, каким могло бы быть.

Итак, мы побороли две основные проблемы, вынесли ресурсы в отдельные бандлы, поправили конфигурации сборки. В результате:

  • количество модулей в приложении перестало влиять на скорость его запуска;

  • размер приложения уменьшился примерно на 30%;

  • количество крашей уменьшилось в три раза за счёт ускорения запуска. iOS убивает приложения, если они долго запускаются, а на устройствах старше трёх лет со слабенькими батареями, где процессор давно не работает на максимуме, это реальные случаи, которых стало значительно меньше.

Цифры подскажут, куда двигаться дальше

Мы рассмотрели глобальные изменения, которые нам пришлось сделать, чтобы жить в мире и согласии в процессе модуляризации:

  • автоматизация работы с графом зависимостей: следите за графом, сделайте так, чтобы было легко понять, что от чего зависит и где на графе узкие места;

  • уменьшение нагрузки на CI за счёт фильтрации проверяемых модулей и умных тестов: не попадайтесь в ловушку прямой зависимости продолжительности CI-проверок от количества модулей;

  • статическая линковка: скорее всего, вам придётся перейти на статическое связывание, так как уже к 50-60 модулям регресс в скорости запуска приложения станет заметен не только вам, но и вашим менеджерам.

Я не говорил об этом напрямую, но, как вы могли заметить, у нас есть графики времени, которое задачи проводят в очереди CI, скорости запуска приложения и т. д. Необходимость всех изменений, описанных ранее, была обусловлена значениями важных при разработке приложения метрик. Какие бы преобразования процессов вы ни задумали, необходимо иметь возможность измерить их результаты. Если такой возможности нет, скорее всего, изменения не имеют смысла.

В нашем случае мы понимали, что изменения сильно затронут разработчиков, поэтому основная метрика, за которой мы следили и следим, это время сборки проекта на железе разработчика. Да, есть показатели времени чистой сборки с CI, но разработчики редко делают чистую сборку; они используют другое оборудование и т. д., то есть и конфигурации сборки, и их окружение другие.

Поэтому у нас есть график среднего времени сборки наших приложений на компьютерах разработчиков:

Если на графике есть выбросы (резко замедлилась сборка всех приложений), значит, скорее всего, в конфигурациях сборки что-то изменилось не в лучшую для разработчиков сторону. Такие проблемы мы стараемся исследовать и решать.

Ещё один интересный вывод, к которому мы пришли, получив подобную аналитику: медленно собирающиеся модули не всегда повод для оптимизации.

Мы построили график с расположенными по осям средней продолжительностью сборки и количеством сборок в день. Стало ясно, что среди самых долго собирающихся модулей есть такие, которые собираются всего лишь два раза в день, и в целом их влияние на общий опыт работы с проектом крайне мало. Есть же, наоборот, модули, сборка которых занимает порядка 10 секунд, но в день их собирают более 1500 раз: за ними нужно внимательно следить. В общем, старайтесь смотреть не на один модуль, а на картину в целом.

Кроме того, мы стали понимать, какое оборудование ещё подходит для работы над нашим проектом, а с каким пришло время прощаться.

Например, видно, что iMac Pro 5K 2017 года выпуска не лучшее железо для сборки Badoo, в то время как MacBook Pro 15 2018 года ещё вполне неплох.

А главный вывод, который мы сделали, заключается в необходимости повышения качества жизни разработчиков. В процессе модуляризации есть вероятность уйти в технологии с головой, забыв о том, для чего это всё делается. Поэтому важно помнить основные мотивы и понимать, что последствия отразятся и на вас самих.

Измеряем время сборки

Чтобы получать данные о продолжительности сборки на компьютерах разработчиков, мы создали специальное macOS-приложение Zuck. Оно сидит в статус-баре и следит за всеми xcactivitylog-файлами в DerivedData. xcactivitylog файлы, которые содержат ту же информацию, которую мы видим в билд-логах Xcode в непростом для парсинга формате от Apple. По ним можно понять, когда началась и закончилась сборка отдельного модуля и в какой последовательности они собирались.

В утилите есть white- и black-листы, так что мы отслеживаем только рабочие проекты. Если разработчик скачал демо-проект какой-то библиотеки с GitHub, мы не будем отправлять данные о её сборке куда-либо.

Информацию о сборке наших проектов мы передаем во внутреннюю систему аналитики, где имеется широкий инструментарий для построения графиков и анализа данных. Например, у нас есть инструмент Anomaly Detection, который предсказывает аномалии в виде слишком сильных отклонений от прогнозируемых значений. Если время сборки резко изменяется по сравнению с предыдущим днём, Core-команда получает уведомление и начинает разбираться, где и что пошло не так.

P. S. Мы дорабатываем Zuck, чтобы выпустить его в open source.

В целом измерение локального времени сборки даёт важные результаты:

  • мы измеряем влияние изменений на разработчиков;

  • имеем возможность сравнивать чистые и инкрементальные сборки;

  • знаем, что надо улучшить;

  • быстро получаем результаты экспериментов. Например, мы изменили настройки оптимизаций и уже на следующий день видим на графике, как это отразилось на жизни разработчиков. Если эксперимент не удался, мы быстро откатываем изменения до лучших времён.

Далее я приведу краткую сводку метрик, за которыми нужно следить, если вы начали двигаться в сторону модуляризации:

  1. Время запуска приложения. Последние версии Xcode предоставляют эту информацию в разделе Organizer. Метрика быстро укажет на появившиеся проблемы.

  2. Размер артефактов. Эта метрика поможет быстро обнаружить проблемы линковки. Возможны случаи, когда линковщик не сообщает о том, что, например, какой-то модуль дублировался. Однако об этом скажет увеличившийся размер скомпилированного приложения. Обращайте внимание на этот график. Проще всего подобную информацию получить из CI.

  3. Инфраструктурные показатели (время сборки, время задач в очереди CI и т. п.). Модуляризация скажется на многих процессах компании, но инфраструктура особенно важна, потому что её придётся масштабировать. Не попадайтесь в ловушку, как мы, когда до мержа в мастер изменениям приходилось находиться в очереди по пять-шесть часов.

Конечно, нет предела совершенству, но это основные показатели, которые позволят вам отслеживать самые критичные проблемы и ошибки. Некоторые метрики можно собирать и локально, если нет возможности инвестировать в сложные системы мониторинга: заведите локальные скрипты и хотя бы раз в неделю смотрите на основные показатели. Это поможет понять, в правильном ли вообще направлении вы движетесь.

Если вам кажется, что я сейчас сделаю вот так и по-любому станет лучше, но вы не можете это лучше измерить, стоит подождать с таким экспериментом. У вас ведь наверняка тоже есть менеджер, которому нужно будет как-то презентовать результаты своей работы.

Заключение

После моего рассказа у вас могло сложиться впечатление, что требуется очень много сил и ресурсов, чтобы построить в компании процесс модуляризации. Отчасти это правда, но я приведу небольшую статистику: на самом деле мы не такие уж и большие.

  1. Всего сейчас у нас работают 43 iOS-разработчика.

  2. Четыре из них в Core-команде.

  3. Сейчас у нас два основных приложения и N экспериментальных.

  4. Около 2 миллионов строк кода.

  5. Около 78% из них находятся в модулях.

Последняя цифра постепенно растёт: старые фичи и переписанный legacy-код потихоньку перетекают из основного таргета приложений в модули.

В двух статьях я рекламировал вам модуляризацию, но, конечно, у неё есть свои минусы:

  • усложнение процессов: вам придётся решить ряд вопросов в процессах как вашего департамента и рядовых iOS-разработчиков, так и во взаимодействии с другими департаментами: QA, CI, менеджерами продуктов и т. д.;

  • в процессе перехода на модуляризацию вскроются проблемы, которых вы не ждали, всё не предусмотреть: возможно, потребуются дополнительные ресурсы и других команд;

  • всё, что вы построите, будет нуждаться в дополнительной поддержке: процессы, новые внутренние инструменты кто-то должен будет за это отвечать;

  • приложения, базирующиеся на модулях, сильнее зависят друг от друга. Например, при изменении чего-то в модуле работы с базой данных для приложения Badoo через несколько минут можно получить от CI сообщение о том, что в приложении Bumble не прошли тесты.

Но хорошая новость заключается в том, что все эти минусы управляемы. Их не стоит бояться, поскольку это не те проблемы, которые невозможно решить. Нужно просто быть к ним готовыми.

О плюсах модуляризации уже было сказано, но я повторюсь:

  • гибкое масштабирование iOS-разработки: новые команды начинают работать в своих модулях, подход к созданию и поддержке которых унифицирован;

  • явные зоны ответственности: с одной стороны, есть конкретные люди, ответственные за конкретные участки кода, с другой в рамках команд возможна ротация, так как подходы общие, отличается лишь начинка модулей;

  • развитие инфраструктуры и мониторинга: это неизбежные улучшения, без которых вам будет очень сложно создать и поддерживать процесс модуляризации.

Напоследок скажу, что совсем не обязательно брать крупную часть приложения как первый модуль на старте внедрения модуляризации. Берите кусочки поменьше и попроще, экспериментируйте. Не стоит слепо следовать любому из примеров других компаний у всех разные подходы к разработке, и вероятность того, что вам полностью подойдёт чей-то опыт, небольшая. Обязательно развивайте инфраструктуру и мониторьте результаты. Успехов!

Подробнее..

Apple убивает TeamCity, Bitrise, Appcenter, Fastlane, Firebase, Sentry и иже с ними. Краткий обзор Xcode Cloud

10.06.2021 14:04:32 | Автор: admin

Заголовок конечно громковат, может не убивает, но уменьшит им доходы точно. Давайте кратко посмотрим что представила Apple на WWDC 2021, что такое Xcode Cloud?

Xcode Cloud - это сервис CI/CD, встроенный в Xcode и разработанный специально для разработчиков Apple. Он ускоряет разработку и доставку приложений, объединяя облачные инструменты, которые помогают создавать приложения, параллельно запускать автоматические тесты, доставлять приложения тестировщикам, а также просматривать отзывы пользователей и управлять ими.

Цикл разработки по мнению Apple заключается в этапах 1) Написать код 2) протестировать его 3) Интегрировать (в текущий) 4) Доставить до пользователя 5) Улучшить, и по новой. На то он и цикл. В принципе похоже на правду, так оно и есть.

Если вы хоть раз настраивали CI/CD для iOS приложений, вы знаете примерно какие там шаги, ничего сложного, но это может включать в себя использование нескольких сервисов, генерации сертификатов и тд и тп.

Теперь же Apple предлагает нам сделать это все не выходя из Xcode, давайте взглянем на процесс.

Для начала нам нужно настроить первый workflow, а потом уже который будет пробегать при PR/MR (pull request/merge request) на main/develop ветку в системе контроля версий.

I CI/CD

1) Жмем в новом Xcode при подключенном сервисе Xcode Cloud кнопку "создать workflow" и видим настройки

Name - название воркфлоу, Start condition когда запускать воркфлоу (например при изменении в main ветке), Environment - можно выбрать стабильную версию Xcode или новую бета версию, Actions - что собственно надо сделать, обычно выполнить archive и опубликовать например в TestFlight, после прогонки тестов, Post-Actions - что сделать после того как воркфлоу пройден, например написать в slack/telegram канал об этом событии

2) Выбираем репозиторий где хранится наш код

3) Выбираем с какой ветки собрать билд (при первой настройке)

Собственно готово, теперь можем посмотреть как выглядит в Xcode место где можно управлять сборками, запускать их, пересобирать и тд

Давайте теперь посмотрим как выглядит управлени воркфлоу (выше показан путь настройки первой сборки)

1) Выбираем "управление воркфлоу"

2) Выбираем настройки (например при pull/merge request что-то выполнять)

3) Выбираем какие тесты мы хотим прогнать в воркфлоу (UI или Unit тесты), я так понимаю речь именно про нативные тесты, про Appium и тд, пока ничего не известно.

4) И выбираем отправить сообщение в Slack после того как воркфлоу пройден

5) Готово

II Тесты

1) Давайте посмотрим как выглядит интерфейс работы с тестами, мы видим тут тесты которые пройдены при сборке а также устройства на которых они прогонялись

2) Посмотрим какие конкретно тесты прогнались на iPad Air, видим что тест кейс с Light mode, портретный режим, с Английским языком, далее видим какие конкретно тесты пройдены

3) Ну и совсем чудеса, можно смотреть скриншоты пройденных тестов

4) Можно также посмотреть какой тест упал, можно также пометить тест как Flaky (Флаки тест или другими словами тест неактуальный, который надо либо удалить либо переписать), для этого используется XCTExpectFailure (что в переводе логично видно по названию метода ожидаемый фейл)

Удобно.

III - Работа с системой контроля версий (и переписка прямо в коде в Xcode)

1) Изменения теперь видно еще нагляднее (привет всем кто пользуется визуальными штуками, а не через консоль при работе с git). Сверху мы видим наши локальные изменения (которые мы накодили) а снизу "висящие" pr/mr реквесты, которые можно посмотреть, и дать свой комментарий или approve (одобрение на слияние кода)

2) Даже видно какой тест план для этой фичи, которая просится в главную ветку

3) Переписка,комменты прямо в Xcode при pr/mr (а не на веб мордах gitlab/github/bitbucket и тд)

В общем очень круто и удобно

IV - Улучшения (Crashes/Сбои/Ошибки)

1) Все краши/сбои теперь видно прямо в Xcode (а не в веб морде Firebase или Sentry), код приходит сразу символизированный (symbolized log), то есть человекопонятный с указанием что и как произошло

2) А тестер (возможно и пользователь) может оставить комментарий при краше который вы сможете прочитать (и даже ответить!)

3) Ну и самое интересное вы сможете кликнуть открыть место краша в проекте

4) И вас за ручку проведут к вашему куску кода который натворил зло

Послесловие

Плюс ко всему это обещает все генерить сертификаты которые нужно автоматически, обновлять провижн (provision) файл тоже автоматически, судя по всему даже UDID теперь не надо будет собирать с инженеров по тестированию и заинтересованных менеджеров которые хотят смотреть на билд.

В общем как по мне сервис выглядит очень интересно, но пока недоступен для всех. Понятно что видимо он еще в стадии разработки, но как все задумывается, это выглядит круто, и очень упростит разработку для iOS.

Можете подать заявку на бета-тест здесь https://developer.apple.com/xcode-cloud/

Сколько он будет стоить пока тоже неизвестно.

И пока непонятно что с Android потому что обычно сервисы CI/CD используют сразу для двух платформ, так как приложения обычно тоже для двух платформ разрабатывают. Но может быть когда нибудь приложения для Android можно будет писать и в Xcode))

Изображение и информацию брал из видеосессий WWDC 2021, кому интересно как это выглядит вот видео про Xcode Cloud https://developer.apple.com/videos/play/wwdc2021/102/

Подробнее..

Разделяй и властвуй. Модульное приложение из монолита на Objective-C и Swift

11.08.2020 14:11:07 | Автор: admin


Привет, Хабр! Меня зовут Василий Козлов, я iOS-техлид в Delivery Club, и застал проект в его монолитном виде. Признаюсь, что приложил руку к тому, борьбе с чем посвящена эта статья, но раскаялся и трансформировал своё сознание вместе с проектом.

Я хочу рассказать, как разбивал существующий проект на Objective-C и Swift на отдельные модули frameworkи. Согласно Apple, framework это директория определенной структуры.

Изначально мы поставили цель: обособить код, реализующий функцию чата для поддержки пользователей, и уменьшить длительность сборки. Это привело к полезным последствиям, которым сложно следовать, не имея привычки и существуя в монолитном мире одного проекта.

Неожиданно пресловутые принципы SOLID начали обретать очертания, а главное сама постановка задачи вынуждала организовывать код в соответствии с ними. Вынося какую-то сущность в отдельный модуль, вы автоматически сталкиваетесь со всеми её зависимостями, которые не должны находиться в этом модуле, а также дублироваться в главном проекте приложения. Поэтому назрел вопрос об организации дополнительного модуля с общей функциональностью. Это ли не принцип единой ответственности, когда одна сущность должна иметь одно предназначение?

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

В предварительно найденных статьях авторы обещали безоблачное будущее при выполнении простых и четких шагов, характерных для нового проекта. Но когда я перенёс первый базовый класс в модуль для общего кода, выявилось столько неочевидных зависимостей, столько строчек кода покрылось красным в Xcode, что продолжать дальше не хотелось.

Проект содержал много legacy-кода, перекрестных зависимостей от классов на Objective-C и Swift, разных targetов в терминах iOS-разработки, внушительный список CocoaPods. Любой шаг в сторону от этого монолита приводил к тому, что проект переставал собираться в Xcode, обнаруживая порой ошибки в самых неожиданных местах.

Поэтому я решил записать последовательность предпринятых мною действий, чтобы облегчить жизнь владельцам таких проектов.

Первые шаги


Они очевидны, о них написано много статей. Apple постаралась сделать их максимально удобными.

1. Создаем первый модуль: File New Project Cocoa Touch Framework

2. Добавляем модуль в workspace проекта





3. Создаем зависимость основного проекта от модуля, указав последний в разделе Embedded Binaries. Если в проекте несколько targetов, то модуль надо будет включить в раздел Embedded Binaries каждого зависящего от него targetа.

От себя добавлю только один комментарий: не спешите.

Знаете ли вы, что будет размещено в этом модуле, по какому признаку будут разделены модули? В моём варианте это должен был быть UIViewController для чата с таблицей и ячейками. К модулю должен был быть привязан Cocoapods с чатом. Но вышло всё немного по-другому. Реализацию чата мне пришлось отложить, потому что и UIViewController, и его presenter, и даже ячейка основывались на базовых классах и протоколах, о которых новый модуль ничего не знал.

Как выделить модуль? Наиболее логичный подход по фичам (features), то есть по какой-то пользовательской задаче. Например, чат с техподдержкой, экраны регистрации/авторизации, bottom sheet с настройками основного экрана. Кроме этого, скорее всего, понадобится какая-то базовая функциональность, которая представляет из себя не feature, а лишь набор UI-элементов, базовых классов и т.д. Эту функциональность следует вынести в общий модуль, аналогичный знаменитому файлу Utils. Не бойтесь раздробить и этот модуль. Чем меньше кубики, тем проще их вписать в основную постройку. Мне кажется, так можно сформулировать еще один из принципов SOLID.

Есть готовые советы по разделению на модули, которыми я не воспользовался, потому и сломал столько копий, и даже решил рассказать о наболевшем. Однако такой подход сначала действовать, потом думать как раз и открыл мне глаза на ужас зависимого кода в монолитном проекте. Когда вы в начале пути, вам сложно объять всё количество изменений, которые потребуются для устранения зависимостей.

Поэтому просто переместите класс из одного модуля в другой, посмотрите, что покраснело в Xcode, и постарайтесь разобраться с зависимостями. Xcode 10 хитёр: при перемещении ссылок на файлы из одного модуля в другой он оставляет файлы на прежнем месте. Потому следующий шаг будет таким

4. Перемещайте файлы в файловом менеджере, удаляйте старые ссылки в Xcode и заново добавляйте файлы в новый модуль. Если делать это по классу за раз, будет легче не запутаться в зависимостях.

Чтобы сделать все обособленные сущности доступными извне модуля, придётся принять во внимание особенности Swift и Objective-C.

5. В Swift все классы, перечисления и протоколы должны быть помечены модификатором доступа public, тогда к ним можно будет получить доступ снаружи модуля. Если в отдельный framework перемещается базовый класс, его следует пометить модификатором open, иначе не получится создать от него класс-потомок.

Сразу следует вспомнить (или впервые узнать), какие есть уровни доступа в Swift, и получить profit!



При изменении уровня доступа у перенесённого класса Xcode потребует изменить уровень доступа у всех переопределённых методов на идентичный.



Затем необходимо добавить импорт нового frameworkа в Swift-файл, где используется выделенная функциональность, наряду с каким-нибудь UIKit. После этого ошибок в Xcode должно стать меньше.

import UIKitimport FeatureOneimport FeatureTwoclass ViewController: UIViewController {//..}

С Objective-C последовательность действий немного сложнее. Кроме того, использование bridging headerа для импорта классов Objective-C в Swift не поддерживается во frameworkах.



Поэтому поле Objective-C Bridging Header должно быть пустым в настройках frameworkа.



Из сложившейся ситуации есть выход, и почему это так тема отдельного исследования.

6. У каждого frameworkа есть собственный заголовочный файл umbrella header, через который будут смотреть во внешний мир все публичные интерфейсы Objective-C.

Если в этом umbrella header указать импорт всех прочих заголовочных файлов, то они будут доступны в Swift.



import UIKitimport FeatureOneimport FeatureTwoclass ViewController: UIViewController {        var vc: Obj2ViewController?        override func viewDidLoad() {        super.viewDidLoad()        // Do any additional setup after loading the view, typically from a nib.    }

В Objective-C, чтобы получить доступ к классам снаружи модуля, нужно поиграться с его настройками: сделать заголовочные файлы публичными.



7. Когда все файлы поодиночке перенесены в отдельный модуль, нужно не забыть о Cocoapods. Файл Podfile требует реорганизации, если какая-то функциональность окажется в отдельном frameworkе. У меня так и было: pod с графическими индикаторами надлежало вынести в общий framework, а чат новый pod был включён в свой собственный отдельный framework.

Необходимо явно указать, что проект теперь не просто проект, а рабочее пространство с подпроектами:

workspace 'myFrameworkTest'

Общие для frameworkов зависимости следует вынести в отдельные переменные, например, networkPods и uiPods:

def networkPods     pod 'Alamofire'end def uiPods     pod 'GoogleMaps' end

Тогда зависимости основного проекта будут описаны следующим образом:

target 'myFrameworkTest' doproject 'myFrameworkTest'    networkPods    uiPods    target 'myFrameworkTestTests' do    endend 

Зависимости frameworkа с чатом таким образом:

target 'FeatureOne' do    project 'FeatureOne/FeatureOne'    uiPods    pod 'ChatThatMustNotBeNamed'end


Подводные камни


Наверное, на этом можно было бы закончить, но в дальнейшем я обнаружил несколько неявных проблем, о которых также хочется упомянуть.

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

Первая проблема скрывалась в реализации чата. На просторах сети проблема встречается и в других podах, достаточно загуглить Library not loaded: Reason: image not found. Именно с таким сообщением происходило падение.

Более элегантного решения я не нашёл и был вынужден продублировать подключение podа с чатом в основном приложении:

target 'myFrameworkTest' do    project 'myFrameworkTest'    pod 'ChatThatMustNotBeNamed'    networkPods    uiPods    target 'myFrameworkTestTests' do    endend

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

Другая проблема заключалась в ресурсах, про которые я благополучно забыл и нигде не встречал упоминания о том, что этот аспект надо держать в уме. Приложение падало при попытке зарегистрировать xib-файл ячейки: Could not load NIB in bundle.

Конструктор init(nibName:bundle:) класса UINib по умолчанию ищет ресурс в модуле главного приложения. Естественно, об этом ничего не знаешь, когда разработка ведется в монолитном проекте.

Решение указывать bundle, в котором определен класс ресурса, либо позволить компилятору сделать это самому, используя конструктор init(for:) класса Bundle. Ну и, конечно, впредь не забывать о том, что ресурсы теперь могут быть общими для всех модулей или специфичными для одного модуля.

Если в модуле используются xibы, то Xcode будет, как обычно, предлагать для кнопок и UIImageView выбирать графические ресурсы из всего проекта, но в run time все расположенные в других модулях ресурсы окажутся не загруженными. Я загружал изображения в коде, используя конструктор init(named:in:compatibleWith:) класса UIImage, где вторым параметром идёт Bundle, в котором расположен файл изображения.

Ячейки в UITableView и UICollectionView теперь также должны регистрироваться подобным образом. Причем надо помнить, что Swift-классы в строковом представлении включают в себя ещё и имя модуля, а метод NSClassFromString() из Objective-C возвращает nil, поэтому рекомендую регистрировать ячейки, указывая не строку, а класс. Для UITableView можно воспользоваться таким вспомогательным методом:

@objc public extension UITableView {    func registerClass(_ classType: AnyClass) {        let bundle = Bundle(for: classType)        let name = String(describing: classType)        register(UINib(nibName: name, bundle: bundle), forCellReuseIdentifier: name)    }}


Выводы


Теперь можно не переживать, если в одном pull request окажутся изменения в структуре проекта, сделанные в разных модулях, потому что у каждого модуля свой xcodeproj-файл. Можно распределять работу так, чтобы не приходилось тратить несколько часов на сведение файла проекта воедино. Полезно иметь модульную архитектуру в больших и распределенных командах. Как следствие, должна увеличиться скорость разработки, но верно и обратное. На свой самый первый модуль я потратил гораздо больше времени, чем если бы создавал чат внутри монолита.

Из очевидных плюсов, на которые также указывает Apple, возможность снова использовать код. Если в приложении имеются различные targetы (app extensions), то это самый доступный подход. Возможно, чат не самый лучший вариант для примера. Следовало начать с вынесения сетевого слоя, но давайте будем честными сами с собой, это очень длинная и опасная дорога, которую лучше разбить на небольшие отрезки. А так как за последние пару лет это было внедрение второго сервиса для организации технической поддержки, хотелось внедрить его не внедряя. Где гарантии, что скоро не появится третий?

Один неочевидный эффект при разработке модуля более продуманные, чистые интерфейсы. Разработчику приходится проектировать классы так, чтобы определенные свойства и методы были доступны извне. Поневоле приходится задумываться, что сокрыть и как сделать модуль таким, чтобы его можно было с легкостью использовать снова.
Подробнее..

Memory Management ARC vs MRC в iOS

05.01.2021 16:09:52 | Автор: admin

Как работает Automatic Reference Counter в iOS? На самом деле эту тему мне было легче понять после того, как я познакомился с Manual Reference Counter. Это очень простая статья, которая помогает базово понять как работает управление памятью в iOS.

Для управления памятью в iOS есть несколько инструментов:

MRC - Manual Reference Counter

MRC - это ручное управление ссылками через код. В самом начале и в доисторические времена разработчики сами управляли подсчетом ссылок через команды. Было это, мягко говоря, жестко:

  • alloc - создание объекта (создаем ссылку)

  • retain - обращение к нему (+1 к ссылке)

  • release - уменьшаем счетчик ссылок (-1)

  • dealloc - если счетчик ссылок равен 0 = выгрузка из памяти

По сути, вы выделяете объект, сохраняете его в какой-то момент, а затем отправляете один выпуск для каждого отправленного вами выделения / сохранения. Метод dealloc вызывается для объекта, когда он удаляется из памяти.

Проблемы:

  • Нужно постоянно считать retain, release

  • крэш при обращении из выгруженного из памяти

  • забыли поставить релиз - утечка памяти

ARC - Automatic reference counter

После того, как умные программисты поняли, что можно придумать механизм, который сам за программиста считает ссылки - мир в iOS поменял. Больше не нужно было считать ссылки и следить за ними. За нас это делает ARC автоматически. Он сам понимает куда и зачем что вставлять и когда удалять. Стоит понять, ЧТО ARC РАБОТАЕТ ПРИ КОМПИЛЯЦИИ, А ПОДСЧЕТ ССЛОК В РАНТАЙМЕ.

Что изменилось?

  • (release/retain - нельзя вызывать) dealloc - работает частично

  • properties change - weak/strong

У property появились модификаторы:

  • strong - аналог retain

  • weak - аналог assign. в проперти при освобождении ставится нил и не крэшит приложение при обращении

Но есть и минусы, с которыми не справляется ARC:

  • Retain cycle - это когда объем выделенного пространства в памяти не может быть освобожден из-за циклов сохранения. Поскольку Swift использует автоматический подсчет ссылок (ARC), цикл сохранения происходит, когда два или более объекта содержат сильные ссылки друг на друга. В результате эти объекты сохраняют друг друга в памяти, потому что их счетчик сохранения никогда не уменьшится до 0, что предотвратит вызов функции deinit и освобождение памяти

Решение банальное - сделать одну из ссылок слабой.

Эта статья является больше базовой и ознакомительной с основами memory management в iOS, для легкого концептуального понимания.

Подробнее..

MFS паттерн построения UI в iOS приложениях

26.01.2021 12:18:23 | Автор: admin

Логика развития мобильных приложений заключается в постепенном усложнении функциональной нагрузки на пользовательский интерфейс.
Что, в свою очередь, приводит к росту кодовой базы и затруднению ее обслуживания.

MFS - позволяет создавать современный дизайн приложений и при этом избежать такого явления как MassiveViewController.

Фото: 10 years of the App Store: The design evolution of the earliest apps - 9to5Mac

Причины создания паттерна

Времена когда весь интерфейс контроллера настраивался целиком в методеviewDidLoadдавно закончились.
Настройка графических компонентов и пользовательскогоlayoutстала занимать сотни строк кода.
Помимо массивной настройки и инициализации, встает второй немало важный вопрос поддержки и управления созданными активами.

О существующих трудностях знают в Купертино, на изменение требований рынка компания отреагировала выпуском перспективной технологиейSwiftUI.
Которая, к сожалению, имеет ряд серьезных ограничений, например поддержка отiOS 13 и выше.
Что на данный момент - абсолютно неприемлемо для большинства солидных приложений, которые стараются охватить максимально большую аудиторию пользователей.

MFS - же напротив ориентирован на поддержку и разгрузку уже существующих приложений.
А набор используемых технологий позволяет внедрять паттерн на самую большую аудиторию приложений.

Архитектурный паттернMFS(Managment-Frames-Styles) был разработан, для того чтобы соответствовать духу времени и его потребностям.

Кому может понадобиться MFS ?

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

Если же ваше приложение имеет крайне простой интерфейс, то возможно более оптимальным будет использование стандартногоStoryboard иAutolayout.

Отношение к Autolayout и другим подобным технологиям

Поскольку исторически паттерн создавался именно для реализации сложных дизайнов, поэтому в стандартной своей реализацииMFS- имеет ручное вычисление всехframe, без использования каких-либо сторонних библиотек.

Подобный подход позволяет крайне гибко реализовывать любые требования дизайнера, чего к примеру не позволяет делать или же серьезно ограничиваетAutolayout.
Однако, использование технологииAutolayout не запрещено, поскольку паттерн имеет высокую декомпозированность, пользуясь которой, вы можете заменить ручной расчет координат на созданиеconstraints.

Обзор паттерна

ПаттернMFS призван равномерно распределить обязанности между категориями класса, с целью избежания возникновенияMassiveViewController.

Название категории

Обязанности

+Managment

Содержит методы дополнительного жизненного цикла интерфейса, которые инициализируют, добавляют на экран, наполняют контентом и совершают прочие действия.

+Frames

Содержит методы вычисляющие размеры и координаты subviews.

+Styles

Содержит методы графической конфигурации subviews.
Например задает цвет,закругление,шрифт, и т.д.
Как правило, для каждого property существует отдельный метод настройки.

Как стало вам понятно, категория+Managment является главенствующей, она содержит дополнительные методы, которые полностью забирают на себя все хлопоты по созданию и обслуживанию интерфейса контроллера.
Остальные же категории содержат так называемыечистые функции, которые более широко известны под названиемpure function.

Функция является "чистой" если она мутирует только те значения, которые создала самостоятельно или же приняла из собственных параметров.


То есть, если функция изменяет некие глобальные переменные, которые не были переданы ей в параметры, то "чистой" она не является.

Обратите внимание, на то, что методы категории+Frames ВСЕГДА должны быть чистыми.
Методы же категории+Stylesмогут быть чистыми по усмотрению пользователя, поскольку это не так критично.

На главный файл имплементации контроллера (ViewController.m) ложится обязанность выполнять протоколы различных представлений (напр.:UITableViewDelegate,UITableViewDataSource), а также содержать методыIBAction.


Выше были перечисленны основные обязанности, но в случае острой необходимости, вы должны самостоятельно принимать решение - размещать некий функционал в файле имплементации или же вынести его в отдельную категорию.
Также стоит заметить, что вся бизнес-логика содержится во вьюМодели вашего контроллера или же во вьюМоделях егоsubviews.

Подобная целенаправленная политика позволяет умещать такие сложные контроллеры, как профиль пользователя и его стену, всего в около~300 строчек кода наObjC и вероятно еще меньше - наSwift.

В свою очередь, такая лаконичность полностью исключает побочный эффект в виде "бесконечного скроллинга" и поиска нужного метода.

Все методы распределены строго по категориям и если нам, например, потребуется изменить размер шрифта или поменятьlayout, мы с легкостью откроем соответствующий файл и не будем бесконечно скролить основной код контроллера.

Порядок вызова методов построения UI

На схеме ниже показаны методы и порядок их вызовов для построенияUI вUIViewController.
Как мы можем увидеть процесс построенияUI начинается из методаviewDidAppear, который вызывает методprepareUI.

Который в свою очередь, по цепочке, вызывает все остальные.
Также надо сказать, что иногда, при перевороте экрана или же при установке новой вьюМодели, нам требуется вызывать разный набор методов - все зависит от непосредственной сложности вашего интерфейса.

На самых простых представлениях, после переворота экрана, нам потребуется вызывать только методresizeSubviews, на более сложных, где нужно, например скрывать некоторые элементы для определенных ориентаций, там может понадобиться и вызовupdateStyles или жеbindDataFrom.

Обзор контроллера

Подобный обзор паттерна будет проводиться на примере демо приложения.
Данный контроллер входа в приложение имеет различную верстку для разных ориентаций.

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

Даже такой минималистичный дизайн является частным случаем труднореализуемого интерфейса с помощью применения стандартногоAutolayout.

Ниже представлены.h/.m контроллера.
Обратите внимание, что помимо стандартного набор проперти, наш контроллер имеет две достаточно необычных, в привычном понимании, переменных.

@interface LoginController : UIViewController// ViewModel@property (nonatomic, strong, nullable) LoginViewModel* viewModel;@property (nonatomic, strong, nullable) LoginViewModel* oldViewModel;// UI@property (nonatomic, strong, nullable) UIImageView* logoImgView;@property (nonatomic, strong, nullable) UIButton* signInButton;@property (nonatomic, strong, nullable) UIButton* signUpButton;@property (nonatomic, strong, nullable) CAGradientLayer *gradient;@property (nonatomic, assign) CGSize oldSize;#pragma mark - Actions- (void) signUpBtnAction:(UIButton*)sender;- (void) signInBtnAction:(UIButton*)sender;#pragma mark - Initialization+ (LoginController*) initWithViewModel:(nullable LoginViewModel*)viewModel;@end


Речь идет оoldViewModel иoldSize - эти проперти помогают избегать лишних перерисовок и вставок данных.
Подробней о них будет рассказано в разборах отдельных категорий.

@interface LoginController ()@end@implementation LoginController#pragma mark - Life cycle- (void) viewDidAppear:(BOOL)animated{    [super viewDidAppear:animated];    [self prepareUI];}- (void)viewWillTransitionToSize:(CGSize)size      withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator{    __weak LoginController* weak = self;    [coordinator animateAlongsideTransition:nil             completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {        [UIView animateWithDuration:0.3                               delay:0                            options:UIViewAnimationOptionCurveEaseOut  animations:^{            [weak resizeSubviews:weak.viewModel];        } completion:nil];    }];}#pragma mark - Action- (void) signUpBtnAction:(UIButton*)sender{    [self.viewModel signUpBtnAction];}- (void) signInBtnAction:(UIButton*)sender{    [self.viewModel signInBtnAction];}#pragma mark - Getters/Setters- (void)setViewModel:(LoginViewModel *)viewModel{    _viewModel = viewModel;      if ((!self.oldViewModel) &amp;&amp; (self.view)){         [self prepareUI];    } else if ((self.oldViewModel) &amp;&amp; (self.view)){        [self bindDataFrom:viewModel];        [self resizeSubviews:viewModel];    }}#pragma mark - Initialization+ (LoginController*) initWithViewModel:(nullable LoginViewModel*)viewModel{    LoginController* vc = [[LoginController alloc] init];    if (vc) {        vc.viewModel = (viewModel) ? viewModel : [LoginViewModel defaultMockup];    }    return vc;}@end

Просмотрев код расположенный выше, можно сказать, что в результате декомпозиции основной файл контроллера остается крайне лаконичным.

Обзор методов категории +Managment

Имя метода

Принимает ли вьюМодель

Предназначение

prepareUI

Главный метод построения UI, вызывает нужную последовательность методов.
Данную функцию рекомендуется вызывать из viewDidAppear.

removeSubviews

Удаляет все subviews с superView.
А также обнуляет все проперти на UI элементы.

initSubviews

Инициализирует нужные subviews.

updateStyles

Вызывает индивидуальные методы настройки для каждого subviews, куда также передает вьюМодель, на основании данных которой может быть принято решение относительно стилей.
Например, задает разный цвет для плашек сообщений в зависимости от пола пользователя.

bindDataFrom

Вставляет данные из вьюМодели в subviews.

resizeSubviews

Вызывает индивидуальные методы расчета размеров и координат для каждой subviews.

addSubviewsToSuperView

Добавляет subviews на superView если те были проинициализированы и не добавлены на родительское представление ранее.

postUIsetting

Здесь должна происходить настройкаsubviews для которых не было создано уникальных методов конфигурации по причине их ненадобности.
Например, тут будет настраиватьсяstatusBar,gestures,allowSelectation и тд.

Из выше перечисленных методов явно прослеживается виденье того, как должен строиться UI в приложении:

  1. Удаление всехsubviewsи обнуление всех проперти наUIэлементы.
    (Если того требует ситуация).

  2. Инициализация нужныхsubviews.

  3. Обновление стилейsubviews (цвет/размер шрифта итд).

  4. Вставка данных вsubviews.

  5. Расчет и установка корректныхframesдляsubviews.

  6. Добавление полностью готовыхsubviews на родительское представление.

Реализация методов категории +Managment

Для того чтобы внутриviewDidAppear не вызывать целый набор методов, был придуман метод-оберткаprepareUI,который вызывает методы категории в нужной последовательности.

Обратите внимание, на методыresizeSubviews иbindDataFrom, порядок их вызовов в некоторых ситуациях может быть прямо противоположенный.

Например, некоторые библиотеки, кэширующие изображения из интернета, возвращают картинку не в полном разрешении, а уже заранее подготовленную под размер вашегоUIImageView, тогда, если вы сначала попытаетесь вставить картинку в рамку размером0x0, у вас может произойти ошибка.

В классическом сценарии сначалаsubviews наполняются данными, а потом производится расчет размеров и координат.

/*----------------------------------------------------------------------  Основной метод построения интерфейса.   Вызывает нужную последовательность методов ----------------------------------------------------------------------*/- (void) prepareUI{    if (self.view){        [self removeSubviews];        [self initSubviews:self.viewModel];        [self updateStyles:self.viewModel];        [self bindDataFrom:self.viewModel];        [self resizeSubviews:self.viewModel];        [self addSubviewsToSuperView];        [self postUIsetting];    }}


Метод удаления всехsubviews с родительского представления.
При работе с контроллером нужен - только в исключительных случаях.

Например, если данные во viewModel могут динамически меняться и набор с расположениемsubviews зависит от вариативностиviewModel, то есть для одной вьюМодели у вас будет один наборsubviews, в определенном месте, а для вьюМодел с другим набором данных, будут иные subviews с прочимиUIэффектами.

Тогда при сменеviewModel имеет смысл вызывать неbindDataFrom иresizeSubviews, а полноценный методprepareUI, потому что он вызовет всю цепочку, которая прежде всего удалит все старые представления.

/*---------------------------------------------------------------------- Удаляем все `subviews` и обнуляем все проперти на UI элементы. ----------------------------------------------------------------------*/- (void) removeSubviews{    // removing subviews from superview    for (UIView* subview in self.view.subviews){        [subview removeFromSuperview];    }    // remove sublayers from superlayer    for (CALayer* sublayer in self.view.layer.sublayers) {        [sublayer removeFromSuperlayer];    }    self.logoImgView   = nil;    self.signInButton  = nil;    self.signUpButton  = nil;    self.gradient      = nil;}


Обратите внимание, что в этом методе происходит чистая инициализация, без каких-либо настроек.

/*---------------------------------------------------------------------- Инициализирует нужные subviews на основе данных из viewModel ----------------------------------------------------------------------*/- (void) initSubviews:(LoginViewModel*)viewModel{  if (self.view)  {   if (!self.logoImgView)  self.logoImgView  = [[UIImageView alloc] init];   if (!self.signInButton) self.signInButton = [UIButton buttonWithType:UIButtonTypeCustom];   if (!self.signUpButton) self.signUpButton = [UIButton buttonWithType:UIButtonTypeCustom];  }}


МетодupdateStyles вызывает индивидуальные методы для каждого изsubviews, с целью настроить их внешний вид.

/*----------------------------------------------------------------------  Задает стили для subviews. Цвета/размера шрифта/селекторы для кнопок ----------------------------------------------------------------------*/- (void) updateStyles:(LoginViewModel*)viewModel{    if (!viewModel) return;    if (self.logoImgView)  [self styleFor_logoImgView:self.logoImgView   vm:viewModel];    if (self.signInButton) [self styleFor_signInButton:self.signInButton vm:viewModel];    if (self.signUpButton) [self styleFor_signUpButton:self.signUpButton vm:viewModel];}


Во время декларации.hфайла контроллера, фигурировало пропертиoldViewModel.
В данном случае оно понадобилось нам для осуществления проверки на идентичность моделей.
Если вьюМодели идентичны, то биндинга данных не произойдет.

Традиционно подобная конструкция чаще используется при работе с ячейками, но в некоторых случаях может потребоваться и при работе с контроллером.

/*---------------------------------------------------------------------- Связывает данные из вьюМодели в subviews ----------------------------------------------------------------------*/- (void) bindDataFrom:(LoginViewModel*)viewModel{    // Если модели идентичны, то биндинга данных не происходит    if (([self.oldViewModel isEqualToModel:viewModel]) || (!viewModel)){        return;    }    [self.logoImgView setImage:[UIImage imageNamed:viewModel.imageName]];    [self.signInButton setTitle:viewModel.signInBtnTitle forState:UIControlStateNormal];    [self.signUpButton setTitle:viewModel.signUpBtnTitle forState:UIControlStateNormal];    self.oldViewModel = viewModel;}


МетодisEqualToModel в каждом отдельном случае имеет разную реализацию.
Например, может возникнуть ситуация, когда в ваш контроллер устанавливается новая вьюМодель, но основные данные, критически важные данные не отличаются, а были изменены только второстепенные проперти, которые не отображаются в вашемUI.

Тогда методisEqualToModel должен вернуть значениеNO, чтобы избежать повторного биндинга данных.

В нашем случае он имеет подобную реализацию:

/*---------------------------------------------------------------------- Сравнивает модели данных на индетичность. ----------------------------------------------------------------------*/- (BOOL) isEqualToModel:(LoginViewModel*)object{    BOOL isEqual = YES;    if (![object.imageName isEqualToString:self.imageName]){        return NO;    }    if (![object.signInBtnTitle isEqualToString:self.signInBtnTitle]){        return NO;    }    if (![object.signUpBtnTitle isEqualToString:self.signUpBtnTitle]){        return NO;    }    return isEqual;}

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

/*---------------------------------------------------------------------- Вызывает индивидуальные методы расчета размеров и координат для subviews.  После изменения ориентации или после первой инициализации. ----------------------------------------------------------------------*/- (void) resizeSubviews:(LoginViewModel*)viewModel{    // Выходим если модель данных и размеры одни и те же    if ((([self.oldViewModel isEqualToModel:self.viewModel]) &&        (CGSizeEqualToSize(self.oldSize, self.view.frame.size))) || (!viewModel)) {        return;    }    if (self.view){      if (self.logoImgView)  self.logoImgView.frame  = [LoginController rectFor_logoImgView:viewModel  parentFrame:self.view.frame];      if (self.signInButton) self.signInButton.frame = [LoginController rectFor_signInButton:viewModel parentFrame:self.view.frame];      if (self.signUpButton) self.signUpButton.frame = [LoginController rectFor_signUpButton:viewModel parentFrame:self.view.frame];      if (self.gradient)     self.gradient.frame     =  self.view.bounds;    }    self.oldSize = self.view.frame.size;}


Добавляемsubviews на родительскоеview.

/*---------------------------------------------------------------------- Добавляет subviews на superView ----------------------------------------------------------------------*/- (void) addSubviewsToSuperView{    if (self.view){        if ((self.logoImgView)  &amp;&amp; (!self.logoImgView.superview))   [self.view addSubview:self.logoImgView];        if ((self.signInButton) &amp;&amp; (!self.signInButton.superview))  [self.view addSubview:self.signInButton];        if ((self.signUpButton) &amp;&amp; (!self.signUpButton.superview))  [self.view addSubview:self.signUpButton];    }}


На этом этапе все методы категории+Managment были разобраны и остался единственный метод пост-настройки, который принадлежит категории+Styles.

В методеpostUIsetting мы настраиваемUI компоненты, для которых не создали индивидуальных методов.
Например, в нем можно добавлятьgestures, настраивать таблицу, устанавливать цвет статус бара и т.д.

- (void) postUIsetting{    UIColor* firstColor  =    [UIColor colorWithRed: 0.54 green: 0.36 blue: 0.79 alpha: 1.00];       UIColor* secondColor =    [UIColor colorWithRed: 0.41 green: 0.59 blue: 0.88 alpha: 1.00];;    self.gradient = [CAGradientLayer layer];    self.gradient.frame      = self.view.bounds;    self.gradient.startPoint = CGPointZero;    self.gradient.endPoint   = CGPointMake(1, 1);    self.gradient.colors     = [NSArray arrayWithObjects:(id)firstColor.CGColor,                           (id)secondColor.CGColor, nil];    [self.view.layer insertSublayer:self.gradient atIndex:0];}

Реализация методов категории +Styles

В отличии от категории+Managment,+Styles не имеет системных методов, а лишь содержит индивидуальные методы настройкиUI компонентов.
Ниже будет приведен один из методов.

- (void) styleFor_logoImgView:(UIImageView*)imgView   vm:(LoginViewModel*)viewModel{    if (!imgView.isStylized){        imgView.contentMode = UIViewContentModeScaleAspectFit;        imgView.backgroundColor = [UIColor clearColor];        imgView.opaque = YES;        imgView.clipsToBounds       = YES;        imgView.layer.masksToBounds = YES;        imgView.alpha      = 1.0f;        imgView.isStylized = YES;    }}


Обратите внимание на некое пропертиisStylized, оно было добавлено категорией к каждому наследнику классаUIView.

Традиция использовать данную переменную при настройкеUIэлементов пришла от опыта работы с ячейками в таблице.

Создана была для того, чтобы при переиспользовании однотипных ячеек не производилась повторная настройка (уже настроенных элементов), то есть, чтобы еще раз не добавлялись тени, блюры и т.д.

Реализация методов категории +Frames

Категория+Frames также как и+Styles не имеет системных методов, но может иметь словари класса, в которых могут быть расположены закэшированные размеры и координатыsubviews.

Подробный листинг данных методов публиковать не имеет смысла из-за их громоздкости, по сути, там не происходит ничего интересного, стандартное ручное вычисление координат и размеров.

Но стоит также обратить внимание, что по сравнению с другими категориями,+FramesсодержитИСКЛЮЧИТЕЛЬНОметоды класса (+).

Данная традиция также пришла из опыта работы с ячейками, первоначально методы класса были созданы для того, чтобы в фоне можно было заранее вычислить координаты и размеры для всехsubviewsне имея фактического объекта ячейки, а имея только лишь ее вьюМодель.

+ (CGRect) rectFor_signUpButton:(LoginViewModel*)viewModel                     parentFrame:(CGRect)parentFrame{    if (CGRectEqualToRect(CGRectZero, parentFrame)) return CGRectZero;    // Calculating...    return rect;}

Советы и рекомендации

По собственному опыту использования могу сказать, что имплементироватьMFSможно выборочно.

Например, как правило, имеет смысл создавать подобную архитектуру в тех случаях, когда мы имеем сложныйUIViewController(UIдля которого ввиду его сложности мы создаем кодом), или же когда имеем сложные ячейки таблицы.

То есть, для контроллера классаUITableViewController, за исключением особых случаев - смысла имплементировать данное решение нет.

Заключение

В данной статье вы имели возможность ознакомиться с паттерномMFS на примере работы с вьюКонтроллерами.

Во второй части статьи мы поговорим о примененииMFSпри работе с ячейками таблицы, как обеспечивать60FPSпри быстром скроллинге сложных таблиц на старых девайсах.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru