
La entrega de contenido rápida y de alta calidad a los usuarios es la tarea más importante en la que estamos trabajando constantemente mientras trabajamos en la aplicación iFunny. La ausencia de elementos de espera incluso con una conexión deficiente: esto es lo que cualquier servicio para ver contenido multimedia busca hacer.
Tuvimos varias iteraciones para trabajar con la captación previa de contenido. En cada nueva versión principal, inventamos algo nuevo y observamos cómo funciona para los usuarios. En la siguiente iteración de trabajar con la captación previa, se decidió depurar primero las métricas que afecta en el stand local, y solo luego dar el resultado a los usuarios.
En este artículo hablaré sobre cómo se ve la captación previa en iFunny ahora y cómo se automatizó el proceso de investigación para ajustar aún más su configuración.
Captación previa estándar
En iOS 10, Apple proporcionó la capacidad de ejecutar la captación previa de fábrica. Para hacer esto, la clase UICollectionView tiene un campo:
@property (nonatomic, weak, nullable) id<UICollectionViewDataSourcePrefetching> prefetchDataSource; @property (nonatomic, getter=isPrefetchingEnabled) BOOL prefetchingEnabled;
Para habilitar la captación previa nativa, simplemente asigne al campo prefetchDataSource un objeto que implemente el protocolo UICollectionViewDatasourcePrefetching y configure el segundo campo en YES.
Para implementar el protocolo de captación previa, se deben describir dos métodos:
- (void)collectionView:(UICollectionView *)collectionView prefetchItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths; - (void)collectionView:(UICollectionView *)collectionView cancelPrefetchingForItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;
En el primer método, puede realizar cualquier trabajo útil en la preparación de contenido.
En el caso de iFunny, se veía así:
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]; }];
El segundo método es opcional, pero en el caso de la cinta iFunny no fue invocado por el sistema.
La captación previa funciona, pero solo llamamos al método para el contenido que sigue al contenido activo.
En general, el trabajo de captación previa estándar para UICollectionView depende en gran medida de cómo se implemente la vista de recopilación. Además, dado que no conocemos en absoluto la implementación de la captación previa estándar, es imposible garantizar su funcionamiento estable. Por lo tanto, implementamos nuestro mecanismo de captación previa, que siempre funcionó como lo necesitábamos.
Nuestro algoritmo de captación previa
Antes de desarrollar el algoritmo de captación previa, escribimos todas las características del feed de iFunny:
- Un feed puede constar de diferentes tipos de contenido: imágenes, videos, aplicaciones web, publicidad nativa.
- La cinta funciona con paginación.
- La mayoría de los usuarios voltean el feed solo hacia adelante.
- En iFunny, el 20% de las sesiones de usuario ocurren a través de LTE.
En base a estas condiciones, hemos obtenido un algoritmo simple:
- Hay 1 elemento activo en la cinta, todos los demás están inactivos.
- El elemento activo siempre necesita descargar contenido hasta el final.
- Cada elemento de contenido en el feed tiene su propio peso.
- En la conexión a Internet actual, puede cargar elementos en la cantidad de N.
- Cada vez que desplaza la cinta, cambiamos el elemento activo y calculamos qué elementos se cargan, y cancelamos el resto de la carga.
La arquitectura en el código de este algoritmo contiene varias clases base y un protocolo:
- IFPrefetchedCollectionProtocol
@protocol IFPrefetchedCollectionProtocol @property (nonatomic, readonly) NSUInteger prefetchItemsCount; - (NSObject<IFFeedItemProtocol> *)itemAtIndex:(NSInteger)index; @end
Este protocolo es necesario para obtener los parámetros de la colección y el contenido en los objetos de clase:
@interface IFContentPrefetcher : NSObject @property (nonatomic, weak) NSObject<IFPrefetchedCollectionProtocol> *collection; @property (nonatomic, assign) NSInteger activeIndex; @end
La clase implementa la lógica del algoritmo para la captación previa de contenido:
@interface IFPrefetchOperation : NSObject @property (nonatomic, readonly) NSUInteger cost; - (void)fetchMinumumBuffer; - (void)fetchEntireBuffer; - (void)pause; - (void)cancel; - (BOOL)isEqualOperation:(IFPrefetchOperation *)object; @end
Esta es la clase base de una operación atómica, que describe el trabajo útil de captar contenido específico e indica su parámetro - peso.
Para ejecutar el algoritmo, describimos dos operaciones:
- La foto Tiene un peso de 1. Siempre completamente cargado;
- Video Tiene un peso de 2. Se carga completamente solo cuando está activo. En el estado inactivo, se cargan los primeros 200 KB.
Como medida para evaluar el funcionamiento del algoritmo, elegimos el número de aciertos del elemento de la interfaz de usuario del cargador por cada 1000 elementos de contenido vistos.
En la captación previa estándar de esta métrica, teníamos aproximadamente 30 impresiones / 1000 elementos. Después de la introducción del nuevo algoritmo, esta métrica se redujo a 25 impresiones / 1000 elementos.
Por lo tanto, el número de impresiones del cargador disminuyó en un 20% y el número total de contenido visto por los usuarios aumentó ligeramente.
Luego pasamos a la selección de parámetros óptimos para Featured, la cinta más popular en iFunny.
Selección de parámetros para la captación previa
El algoritmo de captación previa desarrollado tiene parámetros de entrada:
- El costo total de la descarga.
- El costo de cargar cada artículo.
Continuaremos midiendo el número de cargadores.
Como herramientas auxiliares para simplificar la recopilación de datos, utilizaremos:
- Pruebas de Gray con un conjunto de marcos KIF, OHHTTPStubs.
- sh-scripts y xcodebuild para ejecutar pruebas con diferentes parámetros.
- Perfil de red 3G disponible en la configuración Desarrollador - Acondicionador de enlace de red
Veamos cómo nos ayudó cada una de estas herramientas.
Pruebas
Para emular cómo los usuarios ven el contenido, decidimos usar el marco KIF, familiar para los desarrolladores de iOS en Objective-C.
KIF funciona muy bien para Objective-C y Swift, después de algunas de las manipulaciones fáciles descritas en la documentación de KIF:
https://github.com/kif-framework/KIF#use-with-swiftPara probar la cinta, elegimos Objective-C, incluso para poder reemplazar los métodos que necesitamos en el servicio de análisis.
Echemos un vistazo al código de una prueba simple, que obtuvimos:
- (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; }]; }
En el método de configuración de prueba, debemos borrar el caché para que en cada lanzamiento el contenido se cargue desde la red y borrar completamente la carpeta Caches en la aplicación.
Para garantizar la estabilidad de los datos en cada una de las pruebas, utilizamos la biblioteca OHHTTPStubs, que facilita la sustitución de respuestas a solicitudes de red en unos pocos pasos simples:
- Definir parámetros de consulta. Para nosotros, esta es la URL de la solicitud de feed Destacado a la API: http://fun.co/rp/?feed=featured&limit=30
- Registre la respuesta necesaria y guárdela en un archivo, adjunte al objetivo con la prueba.
- Definir opciones de respuesta. En el código anterior, este es el encabezado Content-Type y el código de respuesta.
- Consulte las instrucciones para los OHHTTPStubs.
Puede leer más sobre cómo trabajar con OHHTTPStubs en la documentación:
http://cocoadocs.org/docsets/OHHTTPSPSs/La prueba en sí se ve así:
- (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]; }
Usando KIF, obtenemos un feed y luego nos desplazamos a través de 1000 elementos de contenido con una espera de 1 segundo.
El método setupCustomPrefetchParams se discutirá un poco más adelante.
Para determinar la cantidad de cargadores que se muestran, utilizaremos el tiempo de ejecución de Objective-C y reemplazaremos el método del servicio de análisis con el método de prueba:
+ (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]; } }
Ahora tenemos una prueba automática en la que la aplicación siempre recibe el mismo contenido y desplaza el mismo número de elementos. Y de acuerdo con sus resultados, escribe una línea con estadísticas de ejecución en el registro.
Dado que la conexión a Internet influye principalmente en la descarga de contenido, la prueba con un conjunto de parámetros debe repetirse más de una vez.
Automatización de inicio
Para automatizar y parametrizar las pruebas, decidimos usar el lanzamiento a través de xcodebuild con la transferencia de los parámetros necesarios.
Para pasar parámetros al código, necesitamos escribir el nombre del argumento en la configuración de destino para las pruebas en las macros de preprocesador:

Para acceder a un parámetro desde el código Objective-C, se deben declarar dos macros:
#define STRINGIZE(x) #x #define BUILD_PARAM(x) STRINGIZE(x)
Ahora, al comenzar desde la terminal usando xcodebuild:
xcodebuild test -workspace iFunny.xcworkspace -scheme iFunnyUITests -destination 'platform=iOS,id=DEVICE_ID' MAX_PREFETCH_COST="5" VIDEO_COST="2" IMAGE_COST="2"
En el código puedes leer los parámetros pasados:
- (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)]; }
Ahora todo está listo para ejecutar estas pruebas sin conexión utilizando scripts de shell.
Ejecutar xcodebuild con un conjunto de parámetros 10 veces seguidas:
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
También generamos un script con el lanzamiento de varios conjuntos de parámetros. Todas las pruebas duraron varios días. Los datos obtenidos se resumieron en una sola tabla, y los comparamos con la versión de trabajo actual.
Como resultado, la captación previa más simple de cinco elementos resultó ser la mejor para las cintas de iFunny destacadas, independientemente del formato de contenido (video o imagen).
De acuerdo con el resultado
El artículo describe el enfoque que le permitirá explorar y monitorear cualquier parte crítica de la aplicación, sin cambiar el código principal del proyecto.
Esto es lo que ayudará a realizar tales estudios:
- Uso de marcos de prueba para acciones monótonas.
- Automatización a través de xcodebuild para parametrizar startups.
- Runtime Objective-C para cambiar la lógica necesaria, donde sea posible.
Con base en este enfoque para probar la aplicación, comenzamos a agregar monitoreo de módulos importantes en el stand local y ya hemos preparado varias pruebas que realizamos periódicamente para verificar la calidad de la aplicación.
PD: Según los resultados de nuestras pruebas, la nueva configuración de captación previa relativa a la opción de producción gana aproximadamente un 8%, en realidad, recibió una disminución en la visualización de los cargadores en un 3%, lo que significa que comenzamos a entregar sonrisas a iFunny 3% más a menudo :)
PPS: no nos detendremos allí, continuaremos mejorando aún más la captación previa de contenido.