Pull to refresh
105.36
Productivity Inside
Для старательного нет ничего невозможного

Уменьшаем размер приложения: проверенные способы

Reading time 8 min
Views 9.6K

Введение


Одним из немаловажных аспектов разработки мобильных приложений является оптимизация размера. Мы все по личному опыту знаем, что чем меньше весит приложение, тем охотнее его скачивают, особенно если под рукой нет точки доступа Wi-Fi, а скорость и/или трафик мобильного интернета оставляют желать лучшего. К тому же, нельзя забывать и о том, что некоторые маркеты ставят ограничение на размер выпускаемого приложения. Например, в App Store продукты размером до 100 МБ доступны для скачивания по мобильному интернету, если же вес приложения превышает этот порог, то скачать его можно только через Wi-Fi. На Play Market же приложение, которое вытягивает больше 100 МБ, нельзя загрузить в принципе. В данной статье мы опишем, к каким методам и хитростям прибегали наши разработчики нативных приложений на iOS для того, чтобы уменьшить вес продукта, и добавим к этому несколько дельных советов, найденных в сети.


Основные способы уменьшения размера приложения


Графический контент


Сейчас дизайн играет ключевую роль в любом хорошем приложении. Если интерфейс минималистичен или продукт имеет небольшой набор функций, то этот этап можно пропустить. Если же проект отличается богатым функционалом или поддерживает некоторое количество цветовых схем, то здесь уже не обойтись без большого количества изображений со всеми вытекающими последствиями для веса. Кроме того, зачастую в проекты по умолчанию добавляются наборы изображений под различные форм-факторы мобильных устройств, как например @1x, @2x, @3x для iOS приложений. Ниже мы приведем методы, которые использовали в своих приложениях, чтобы разрешить проблему с обилием графического контента. Возможно, какие-то из них вы применяете и сами.

Один из простейших путей — использовать вместо трех масштабов только 3x изображение. Этот способ не назовешь оптимальным, так как на устройствах, ориентированных под 1x и 2x масштабы, такие изображения не всегда будут смотреться приемлемо. Однако за неимением лучшего этим приемом можно неплохо уменьшить размер проекта при огромном количестве графики.

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

Теперь рассмотрим пример с приложением, имеющим несколько цветовых схем (в простонародье «скин»). Чем больше цветовых схем в приложении, тем сильнее возрастает количество необходимых изображений. Если в изображении используется более одного цвета, то приходится хранить несколько вариантов на каждый скин. Однако, в случае когда изображение однотонное, его можно сделать шаблонным и уже в самом коде менять цвет оттенка (tint color). На iOS создать подобный шаблон можно двумя способами:

  1. выставить Template Image в самом XCode (см. Рис.1);
  2. задать шаблонный режим программно



Рис.1. Выставление шаблонного режима изображения в XCode.

UIImage *templateImage = [[UIImage imageNamed:@«Back Chevron»] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; 
[backButton setTintColor:[UIColor blueColor]];

— где UIImageRenderingModeAlwaysTemplate и является шаблонным режимом изображения.

Замена анимационных изображений


Добавление анимации — обычное дело в приложениях. Она привлекает внимание пользователя к нужным объектам интерфейса и делает его менее статичным, обеспечивая более приятный опыт взаимодействия. Некоторые простые анимации, наподобие перемещения объекта из одной части экрана в другую или появления снизу нового окна, можно сделать программно. Другие же, более сложные, требуют отрисовки каждого кадра анимации. Когда мы впервые столкнулись с добавлением анимационного изображения в ходе разработки, то использовали для его реализации один из распространенных способов, а именно анимирование через массив изображений. Выглядело это так:

NSArray *gif=@[@"frame1",@"frame2",@"frame3",@"frame4",@"frame5", @"frame6",@"frame7",@"frame8",@"frame9",@"frame10"];
NSMutableArray <UIImage *> *images = [[NSMutableArray alloc] init];
for (int i = 0; i < gif.count; i++)
{
        	UIImage *image=[UIImage imageNamed:[gif objectAtIndex:i]];
        	[images addObject:image];
}
imageView.animationImages = images;
imageView.animationDuration = 0.3;
imageView.animationRepeatCount=1;
[imageView startAnimating];

Сначала создается массив с названиями изображений, затем — массив который поочередно пополняется изображениями из названий. Потом у переменной типа UIImageView задаются массив изображений для анимации, продолжительность анимации и количество повторений. После чего запускается сама анимация. Однако если кадров много и при этом на каждый из них приходится по три масштаба, то для размера приложения это не сулит ничего хорошего. Придя к такому печальному итогу, мы задались поиском способа добавления gif-файла вместо массива картинок. К счастью, на просторах интернета нам попалась категория UIImage+animatedGIF, которая все это уже умеет. Данная категория добавляет классу UIImage два метода:

+ (UIImage * _Nullable)animatedImageWithAnimatedGIFData:(NSData * _Nonnull)theData;
+ (UIImage * _Nullable)animatedImageWithAnimatedGIFURL:(NSURL * _Nonnull)theURL;

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

Однако gif-файл тоже занимает пространство, поэтому мы старались выполнить все анимации программно. В Audio Editor Tool на стартовом экране у нас проигрывается анимация появления логотипа AUDIO EDITOR побуквенно. Раньше данная анимация была реализована с помощью гифки, но из-за большого разрешения изображения весила она многовато. Поэтому мы решили реализовать ее с помощью CABasicAnimation.

CAGradientLayer *gradient=[CAGradientLayer layer];
gradient.frame=animationLabel.bounds;
gradient.colors = @[(id)[UIColor colorWithWhite:1 alpha:1.0].CGColor,
                    (id)[UIColor clearColor].CGColor];
gradient.startPoint = CGPointMake(0.0, 0.5);
gradient.endPoint = CGPointMake(0.1, 0.5);
animationLabel.layer.mask=gradient;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.99 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    gradient.colors = @[(id)[UIColor colorWithWhite:1 alpha:1.0].CGColor,
                        (id)[UIColor colorWithWhite:1 alpha:1.0].CGColor];
});
CABasicAnimation *startPoint=[CABasicAnimation animationWithKeyPath:@"startPoint"];
startPoint.fromValue= [NSValue valueWithCGPoint:CGPointMake(0.0, 0.5)];
startPoint.toValue= [NSValue valueWithCGPoint:CGPointMake(1.0, 0.5)];
startPoint.duration = 0.9;
[startPoint setBeginTime:0.1];
startPoint.removedOnCompletion=NO;
CABasicAnimation *endPoint=[CABasicAnimation animationWithKeyPath:@"endPoint"];
endPoint.fromValue= [NSValue valueWithCGPoint:CGPointMake(0.1, 0.5)];
endPoint.toValue= [NSValue valueWithCGPoint:CGPointMake(1.0, 0.5)];
endPoint.duration = 1.0;
[endPoint setBeginTime:0.0];
endPoint.removedOnCompletion=NO;
CAAnimationGroup *group = [CAAnimationGroup animation];
[group setDuration:1.2];
[group setAnimations:[NSArray arrayWithObjects:startPoint, endPoint, nil]];
[ gradient addAnimation:group forKey:nil];

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

Конвертирование аудио


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

+(void)convertAudio:(NSURL *)url toUrl:(NSURL *)convertedUrl{
    AVAudioFile *audioFile = [[AVAudioFile alloc] initForReading:url error:nil];
    AVAudioPCMBuffer *buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:audioFile.processingFormat frameCapacity:(uint32_t)audioFile.length];
    [audioFile readIntoBuffer:buffer error:nil];
    NSDictionary *recordSettings = @{
                                     AVFormatIDKey : @(kAudioFormatLinearPCM),
                                     AVSampleRateKey : @(audioFile.processingFormat.sampleRate),
                                     AVNumberOfChannelsKey : @(audioFile.processingFormat.channelCount),
                                     AVEncoderBitDepthHintKey : @16,
                                     AVEncoderAudioQualityKey : @(AVAudioQualityMedium),
                                     AVLinearPCMIsBigEndianKey: @0,
                                     AVLinearPCMIsFloatKey: @0,
                                     };
    AVAudioFile *writeAudioFile = [[AVAudioFile alloc] initForWriting:convertedUrl settings:recordSettings error:nil];
    [writeAudioFile writeFromBuffer:buffer error:nil];
}

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

Подгрузка файлов с сервера


Подгрузка файлов с сервера — это то что нужно для приложений со значительным объемом контента. Большое количество пресетов музыки, наборы изображений и многое другое, что сильно увеличивает размер приложения, можно загрузить и позднее. Конечно, загрузка каждого отдельного файла потребовала бы много времени и трафика, поэтому с сервера подгружаются архивы со всем необходимым, а уже в самом приложении они распаковываются и сохраняются в директории приложения. Для разархивирования используется библиотека SSZipArchive (репозиторий библиотеки вы найдете по ссылке). Эта библиотека способна как упаковывать файлы в архив, так и распаковывать архивы. Но нас интересует только один метод из основного класса библиотеки:

+ (BOOL)unzipFileAtPath:(NSString *)path toDestination:(NSString *)destination
    progressHandler:(void (^)(NSString *entry, unz_file_info zipInfo, long entryNumber, long total))progressHandler
    completionHandler:(void (^)(NSString *path, BOOL succeeded, NSError *error))completionHandler;

Данный метод распаковывает файл из пути path в путь destination, а пока он распаковывается в progressHandler можно совершать какие-либо действия (например, отображение прогресса распаковки), после чего в completionHandler показать, что распаковка благополучно завершилась, либо вывести ошибку при неудаче.

Заключение


В конечном счете, если судить по приложению Mix Wave, которое до установки весит ~41 МБ, а после загрузки всех пресетов — 281 МБ, то описанные методы смогли уменьшить размер приложения примерно в семь раз. Результат неплохой, хотя, возможно, существуют и более актуальные способы. Если вы знаете о таких, предлагаем поделиться в комментариях.

UPD: Спасибо Dim0v за дельные замечания о графическом контенте. Приводим их ниже:

Читать
«Во-первых, для устройств с iOS 9 и выше работает App slicing. iTunes Connect пересобирает загруженный архив в несколько вариантов для разных устройств. Таким образом, например, iPhone 6 при установке из апп стора будет тянуть только @2x ресурсы, а iPad mini 1 — только @1x. Поэтому если продукт поддерживает iOS 9+, то прислушивание к совету об оставлении только 3x ресурсов будет иметь строго обратный эффект — для айфонов+ ничего не изменится, а вот устройства с меньшим разрешением будут вынуждены тянуть себе 3x ресурсы, тогда как могли обойтись 2x или 1x.

Во-вторых — совет о переводе растровых изображений в вектор также не имеет смысла. Единственное, что вы таким образом можете сэкономить — это место на компьютере разработчиков. Xcode растеризирует векторные изображения при сборке билда, в чем несложно убедиться, к примеру, отмасштабировав «векторную» картинку на устройстве и увидев дико пикселизированное растровое изображение. Я не спорю, векторные ресурсы — это удобно: проще экспортировать дизайнерам, не нужно следить чтобы при изменении ресурса остались «синхронизированными» все его версии разных разрешений и т.п. Но перевод существующих растровых картинок в вектор именно с целью уменьшения размера билда не имеет никакого смысла».
Tags:
Hubs:
+2
Comments 4
Comments Comments 4

Articles

Information

Website
productivityinside.com
Registered
Founded
Employees
101–200 employees
Location
Россия