Подходы и технологии, используемые в разработке
iOS-клиента ViberКирилл Лашкевич
Yandex Mobile Camp
Viber• >20M зарегистрированных пользоватей в России
• >400M по миру
• 100M online
• 220 сотрудников в 9 странах
• 4.5 года проекту
Статистика iOS проекта• 206K SLOC под iOS
• +50K SLOC сторонних библиотек под iOS
• +140K SLOC общего кода Viber
• +300K SLOC медиа-движка WebRTC
• +120K SLOC аудио/видео кодеков
• ≈800K строк кода
Agenda
1. Управление исходным кодом
2. Сборка проекта
3. Несколько примеров кода
Работа с репозиториями
• 1 репозиторий для всего проекта
• Много репозиториев
• 12 git origins, >100 с форками
1 репозиторий
• Просто для начального этапа
• Поддерживается всеми инструментами
• Разграничения доступа
• Ветки
• Совместная работа
• Размер
• Скорость работы
+ -
CocoaPods
• База готовых pods для сторонних библиотек
• Возможность управления приватными репозиториями
• Только для проектов под iOS,для остальных платформ проблема остается
• Практически не используем сторонние библиотеки
+ -
Сторонние библиотеки• Только в исходных кодах
• 7 раз подумать стоит ли брать сторонний код либо написать самим
• Быть готовым исправлять баги, адаптировать под новые версии ОС и компилятора быстрее авторов
• Приватный форк в github (git-svn для svn)
Git submodules• Ссылка на конкретную ревизию внешнего репозитория в каталоге основного
• Что бы получить весь код для сборки: git clone … && git submodule update --init —update
• Подходит как для внутренних библиотек так и для стороннего кода
• Работает одинаково на всех платформах
Git subtree• Способ добавить внешний репозиторий, либо его часть в основной, с сохранением возможности обновления в обе стороны
• Подходит для случаев, когда внешний репозиторий активно не взаимодействует с upstream
• Проще в использовании, код из внешнего репозитория выглядит как часть основного
LibViber
Core
Media����������� ������������������ Engine
Codec Codec
Viber����������� ������������������ iOS
DSP
RAC Mantle
Сборка проекта
• make, cmake, qmake, scons
• Xcode, xcodebuild:
• Workspace
• Project
• Target
1 Проект 1 Target
1 Проект N Targets
1 Проект N подпроектов
1 Workspace N проектов
+ external build
+ external build
Недостатки Xcode• Переодически что-нибудь ломают (а иногда и чинят)
• Headermap который сложно отключить
• Сборка отдельных файлов под разные платформы
• Не все документировано
• Скорость сборки не максимальная
Немного кода
• Swift
• Main thread profiler
• ReactiveCocoa
• Mantle
Swift• Да, мы используем Swift!
• 15 строчке скрипта для генерации иконки с номером версии
• Для кода приложения не раньше Xcode 6.1
Main thread profiler• UI обрабатывается в главном потоке, он не должен блокироваться
• Каждые 0.1с запускать на выполнение блок в главном потоке
• Если выполнение блока задерживается — уведомление в UI и backtrace в лог
while (!NSThread.currentThread.isCancelled) { static bool pingTaskIsRunning; pingTaskIsRunning = YES; dispatch_async(dispatch_get_main_queue(), ^{ pingTaskIsRunning = NO; dispatch_semaphore_signal(semaphore); }); [NSThread sleepForTimeInterval:0.4]; if (pingTaskIsRunning) { // Уведомление о блокировке } dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); [NSThread sleepForTimeInterval:0.1]; }
ReactiveCocoa• Удобная замена KVO
• Функциональное программирование в ObjC
• Меньше состояния, меньше багов
• Возрастает порог входа
• Много блоков в коде
RAC(self, label.text) = RACObserve(self, name);
http://www.raywenderlich.com/62796/reactivecocoa-tutorial-pt1
[[[[[[[self requestAccessToTwitterSignal] then:^RACSignal *{ @strongify(self) return self.searchText.rac_textSignal; }] filter:^BOOL(NSString *text) { @strongify(self) return [self isValidSearchText:text]; }] throttle:0.5] flattenMap:^RACStream *(NSString *text) { @strongify(self) return [self signalForSearchWithText:text]; }] deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSDictionary *jsonSearchResult) { NSArray *tweets = [jsonSearchResult[@"statuses"] .rac_sequence map:^(id tweet) { return [RWTweet tweetWithStatus:tweet]; }].array; [self.resultsViewController displayTweets:tweets]; } error:^(NSError *error) { NSLog(@"An error occurred: %@", error); }];
self и циклические ссылки• self захватывается как сильная ссылка в блоке
• Доступ к ivar происходит через неявный self:^ { NSLog(@"%@", _ivar); }; ^ { NSLog(@"%@", self->_ivar); };
• Циклическая ссылка получается при сохранении блока в атрибуте объекта
@weakself• До вызова блока объект, на который указывает self, хранится по слабой ссылке, а во время вызова блока — по сильной
• self называется self
• Проверяется доступ к ivar внутри блоков [RACObserve(self, pttState) subscribeNext:@weakselfnotnil(^(NSNumber *state)) { self.isRecordingPTT = !!state.intValue;} @weakselfend];
https://gist.github.com/notorca/9192459
Mantle• Слой модели данных для приложения
• Замена NSDictionary
• Замена классов состоящих только из набора @property
• Автоматом добавляет основные методы: initWithDictionary:, description, debugDescription, initWithCoder:, encodeWithCoder:, copyWithZone:, isEqual:, hash:
NSDictinary *httpRequestSetup = @{ @"URL" : [NSURL URLWithString:@"http...."], @"HTTPMethod" : @"GET", @"HTTPHeaders" : @{}, @"HTTPBody" : [NSData dataWithString:@""], @"resumable" : @(YES), @"streamBoundary" : @"---123---" @"streamBody" : [NSData dataWithString:@""] };
• Нет проверки типов
• Только тонны тестов спасут когда проект разрастется
@interface VTMHTTPRequestSetup "@property (nonatomic) NSURL *URL; @property (nonatomic) NSString *HTTPMethod; @property (nonatomic) NSDictionary *HTTPHeaders; @property (nonatomic) NSData *HTTPBody; @property (nonatomic) BOOL resumable; @property (nonatomic) NSString *streamBoundary; @property (nonatomic) NSData *streamBody; "// initWithDictionary:, copyWithZone:, // description, debugDescription, // initWithCoder:, encodeWithCoder:, // isEqual:, hash: "@end
@interface VTMHTTPRequestSetup : MTLModel "@property (nonatomic) NSURL *URL; @property (nonatomic) NSString *HTTPMethod; @property (nonatomic) NSDictionary *HTTPHeaders; @property (nonatomic) NSData *HTTPBody; @property (nonatomic) BOOL resumable; @property (nonatomic) NSString *streamBoundary; @property (nonatomic) NSData *streamBody; "@end
• Конвертация из/в JSON
• Конвертация из/в NSManagedObject
• Вся работа с CoreData в фоновом потоке
• NSManagedObjects -> MTLModel
• FetchResultController для отслеживания изменений +логика получения конкретных обновившихся полей во время merge NSManagedContext
• ReactiveCocoa для связи всего, пересылки из потока в поток и throtlling
@notorca