- PVSM.RU - https://www.pvsm.ru -
Быстрая и качественная доставка контента пользователям — важнейшая задача, которой мы постоянно занимаемся, работая над приложением iFunny. Отсутствие элементов ожидания даже при плохом соединении — к этому стремится любой сервис для просмотра медиа-контента.
У нас было несколько итераций по работе с префетчингом контента. В каждой новой мажорной версии мы изобретали что-то новое и смотрели, как это работает на пользователях. В очередной итерации по работе с префетчингом было решено сначала отладить метрики, на которые он влияет, на локальном стенде, а уже потом отдать результат пользователям.
В этой статье я расскажу про то, как выглядит префетчинг в iFunny сейчас и о том, как автоматизировали процесс исследования для дальнейшего тюнинга его настроек.
В iOS 10 Apple предоставила возможность запускать префетчинг из коробки. Для этого у класса UICollectionView появилось поле:
@property (nonatomic, weak, nullable) id<UICollectionViewDataSourcePrefetching> prefetchDataSource;
@property (nonatomic, getter=isPrefetchingEnabled) BOOL prefetchingEnabled;
Чтобы включить нативный префетчинг, достаточно присвоить полю prefetchDataSource объект, реализующий протокол UICollectionViewDatasourcePrefetching, и выставить в YES второе поле.
Для реализации протокола префетчинга нужно описать два его метода:
- (void)collectionView:(UICollectionView *)collectionView prefetchItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;
- (void)collectionView:(UICollectionView *)collectionView cancelPrefetchingForItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;
В первом методе можно выполнять любую полезную работу по подготовке контента.
В случае с iFunny это выглядело вот так:
NSMutableArray<NSURL *> *urls = [NSMutableArray new];
for (NSIndexPath *indexPath in indexPaths) {
NSObject<IFFeedItemProtocol> *item = [self.model itemAtIndex:indexPath.row];
NSURL *downloadURL = item.downloadURL;
if (downloadURL) {
[urls addObject:downloadURL];
}
}
[self.downloadManager updateActiveURLs:urls];
[urls enumerateObjectsUsingBlock:^(NSURL *_Nonnull url, NSUInteger idx, BOOL *_Nonnull stop) {
[self.downloadManager downloadContentWithURL:url.absoluteString forView:nil withOptions:0];
}];
Второй метод является опциональным, но в случае с лентой iFunny он не вызывался системой совсем.
Префетчинг работает, но у нас метод вызывался только для контента, следующего за активным.
В целом работа стандартного префетчинга для UICollectionView очень сильно зависит от того, как реализован вид коллекции. Кроме того, так как мы совсем не знаем реализацию работы стандартного префетчинга — невозможно гарантировать его стабильную работу. Поэтому мы реализовали свой механизм префетчинга, который всегда работал, так как нам нужно.
Перед тем, как разработать алгоритм префетчинга, мы выписали все особенности ленты iFunny:
Исходя из этих условий, у нас получился простой алгоритм:
Архитектура в коде этого алгоритма содержит несколько базовых классов и протокол:
@protocol IFPrefetchedCollectionProtocol
@property (nonatomic, readonly) NSUInteger prefetchItemsCount;
- (NSObject<IFFeedItemProtocol> *)itemAtIndex:(NSInteger)index;
@end
Этот протокол необходим для получения параметров коллекции и контента в объекты класса:
@interface IFContentPrefetcher : NSObject
@property (nonatomic, weak) NSObject<IFPrefetchedCollectionProtocol> *collection;
@property (nonatomic, assign) NSInteger activeIndex;
@end
Класс реализует логику алгоритма по префетчингу контента:
@interface IFPrefetchOperation : NSObject
@property (nonatomic, readonly) NSUInteger cost;
- (void)fetchMinumumBuffer;
- (void)fetchEntireBuffer;
- (void)pause;
- (void)cancel;
- (BOOL)isEqualOperation:(IFPrefetchOperation *)object;
@end
Это базовый класс атомарной операции, в которой описывается полезная работа по префетчингу конкретного контента и указывается его параметр — вес.
Для запуска алгоритма мы описали две операции:
В качестве метрики для оценки работы алгоритма мы выбрали количество показов UI-элемента лоадеров на 1000 просмотренных элементов контента.
На стандартном префетчинге эта метрика у нас была около 30 показов/1000 элементов. После внедрения нового алгоритма эта метрика снизилась до 25 показов/1000 элементов.
Таким образом, снизилось количество показов лоадера на 20% и немного повысилось общее количество просмотренного пользователями контента.
Далее приступили к подбору оптимальных параметров для Featured — самой популярной ленты в iFunny.
Разработанный алгоритм префетчинга имеет входные параметры:
Мы по-прежнему будем мерить количество лоадеров.
В качестве вспомогательных инструментов для упрощения сбора данных будем использовать:
Разберём, как нам помог каждый из этих инструментов.
Чтобы эмулировать то, как пользователи просматривают контент, мы решили использовать фреймворк KIF, знакомый разработчикам под iOS на Objective-C.
KIF отлично работает для Objective-C и Swift, после некоторых нетрудных манипуляций, описанных в документации KIF:
https://github.com/kif-framework/KIF#use-with-swift [1]
Для тестирования ленты мы выбрали Objective-C, в том числе и для того чтобы можно было подменить нужные нам методы в сервисе аналитики.
Разберём по частям код простого теста, который у нас получился:
- (void)setUp {
[super setUp];
[self clearCache];
[[NSURLCache sharedURLCache] removeAllCachedResponses];
[OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *_Nonnull request) {
return [request.URL.absoluteString isEqualToString:@"http://fun.co/rp/?feed=featured&limit=30"];
}
withStubResponse:^OHHTTPStubsResponse *_Nonnull(NSURLRequest *_Nonnull request) {
NSString *path = OHPathForFile(@"featured.json", self.classForCoder);
OHHTTPStubsResponse *response = [[OHHTTPStubsResponse alloc] initWithFileAtPath:path statusCode:200 headers:@{ @"Content-Type" : @"application/json" }];
return response;
}];
}
В методе настройки теста обязательно чистим кеш, чтобы при каждом запуске контент грузился из сети, и полностью очищаем папку Caches в приложении.
Для обеспечения стабильности данных в каждом из тестов мы воспользовались библиотекой OHHTTPStubs, которая позволяет легко подменять ответы на сетевые запросы в несколько простых шагов:
Более подробно о работе с OHHTTPStubs можно почитать в документации:
http://cocoadocs.org/docsets/OHHTTPStubs/ [3]
Сам тест выглядит так:
- (void)testFeed {
KIFUIViewTestActor *feed = [viewTester usingLabel:@"ScrolledFeed"];
[feed waitForView];
[self setupCustomPrefetchParams];
for (NSInteger i = 1; i <= 1000; i++) {
[feed waitForCellInCollectionViewAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
[viewTester waitForTimeInterval:1.0f];
}
[self appendStatisticLine];
}
С помощью KIF мы получаем ленту и далее скролим 1000 элементов контента с ожиданием в 1 секунду.
Метод setupCustomPrefetchParams мы разберём чуть позже.
Чтобы определить количество показанных лоадеров, мы воспользуемся Objective-C runtime и подменим метод из сервиса для аналитики на метод теста:
+ (void)load {
[self swizzleSelector:@selector(trackEventLoaderViewedVideo:)
ofClass:[IFAnalyticService class]];
}
+ (void)swizzleSelector:(SEL)originalSelector
ofClass:(Class) class {
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod([self class], originalSelector);
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
originalSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
}
else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
- (void)trackEventLoaderViewedVideo : (BOOL)onVideo {
if (onVideo) {
[IFTestFeed trackLoaderOnVideo];
}
else {
[IFTestFeed trackLoaderOnImage];
}
}
Теперь у нас есть автоматический тест, в котором приложение всегда получает одинаковый контент и скроллит одинаковое количество элементов. А по его результатам записывает в лог строчку со статистикой выполнения.
Так как на загрузку контента в основном влияет интернет соединение, то тест с одним набором параметров нужно повторить не один раз, а несколько.
Для автоматизации и параметризации тестов мы решили воспользоваться запуском через xcodebuild с передачей нужных параметров.
Для передачи параметров в код нам нужно прописать имя аргумента в настройках таргета для тестов в Prepocessor Macros:
Чтобы обращаться к параметру из Objective-C кода нужно объявить два макроса:
#define STRINGIZE(x) #x
#define BUILD_PARAM(x) STRINGIZE(x)
Теперь при запуске из терминала с помощью xcodebuild:
xcodebuild test -workspace iFunny.xcworkspace -scheme iFunnyUITests -destination 'platform=iOS,id=DEVICE_ID' MAX_PREFETCH_COST="5" VIDEO_COST="2" IMAGE_COST="2"
В коде можно прочитать передаваемые параметры:
- (void)setupCustomPrefetchParams {
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
formatter.numberStyle = NSNumberFormatterNoStyle;
[IFAppController instance].prefetchParams.goodNetMaxCost = [formatter numberFromString:@BUILD_PARAM(MAX_PREFETCH_COST)];
[IFAppController instance].prefetchParams.videoCost = [formatter numberFromString:@BUILD_PARAM(VIDEO_COST)];
[IFAppController instance].prefetchParams.imageCost = [formatter numberFromString:@BUILD_PARAM(IMAGE_COST)];
}
Теперь всё готово к тому, чтобы запускать эти тесты в автономном режиме с помощью shell скриптов.
Запуск xcodebuild с набором параметров 10 раз подряд:
max=10
for i in `seq 1 $max`
do
xcodebuild test -workspace iFunny.xcworkspace -scheme iFunnyUITests -destination 'platform=iOS,id=DEVICE_ID' MAX_PREFETCH_COST="$1" VIDEO_COST="$2" IMAGE_COST="$3"
done
Также мы сгенерировали скрипт с запуском различных наборов параметров. Всё тестирование длилось несколько дней. Полученные данные были сведены в единую таблицу, и мы сравнили их с текущим рабочим вариантом.
В итоге, самым лучшим для Featured ленты iFunny оказался простой префетчинг пяти элементов, без учёта формата контента (видео это или картинка).
В статье описан подход, которых позволит исследовать и мониторить любую критически важную часть приложения, без изменения основного кода проекта.
Вот что поможет проводить такие исследования:
На основе такого подхода к тестированию приложения мы стали добавлять мониторинг важных модулей на локальном стенде и уже подготовили несколько тестов, которые периодически запускаем для проверки качества приложения.
P. S.: По результатам наших тестов новые настройки префетчинга относительно продакшн-варианта выигрывают около 8%, в реальности же получили снижение показа лоадеров на 3%, а это значит, что мы стали доставлять улыбки в iFunny на 3% чаще :)
P. P. S.: Останавливаться на достигнутом не собираемся, продолжим совершенствовать префетчинг контента и дальше.
Автор: amyhametov
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/objective-c/285866
Ссылки в тексте:
[1] https://github.com/kif-framework/KIF#use-with-swift: https://github.com/kif-framework/KIF#use-with-swift
[2] http://fun.co/rp/?feed=featured&limit=30: http://fun.co/rp/?feed=featured&limit=30
[3] http://cocoadocs.org/docsets/OHHTTPStubs/: http://cocoadocs.org/docsets/OHHTTPStubs/
[4] Источник: https://habr.com/post/417059/?utm_campaign=417059
Нажмите здесь для печати.