Aprimorando o download de conteúdo sem selos



A entrega rápida e de alta qualidade de conteúdo aos usuários é a tarefa mais importante na qual trabalhamos constantemente enquanto trabalhamos no aplicativo iFunny. A ausência de elementos de espera, mesmo com uma conexão ruim - é o que qualquer serviço para exibir conteúdo de mídia procura fazer.

Tivemos várias iterações para trabalhar com a pré-busca de conteúdo. Em cada nova versão principal, inventamos algo novo e observamos como ele funciona para os usuários. Na próxima iteração de trabalho com pré-busca, foi decidido primeiro depurar as métricas que elas afetam no suporte local e, somente então, fornecer o resultado aos usuários.

Neste artigo, falarei sobre como é a pré-busca no iFunny agora e como o processo de pesquisa foi automatizado para ajustar ainda mais suas configurações.

Pré-busca padrão


No iOS 10, a Apple forneceu a capacidade de executar a pré-busca imediata. Para fazer isso, a classe UICollectionView possui um campo:

@property (nonatomic, weak, nullable) id<UICollectionViewDataSourcePrefetching> prefetchDataSource; @property (nonatomic, getter=isPrefetchingEnabled) BOOL prefetchingEnabled; 

Para ativar a pré-busca nativa, apenas atribua ao campo prefetchDataSource um objeto que implemente o protocolo UICollectionViewDatasourcePrefetching e defina o segundo campo em YES.

Para implementar o protocolo de pré-busca, dois métodos devem ser descritos:

 - (void)collectionView:(UICollectionView *)collectionView prefetchItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths; - (void)collectionView:(UICollectionView *)collectionView cancelPrefetchingForItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths; 

No primeiro método, você pode executar qualquer trabalho útil na preparação do conteúdo.

No caso do iFunny, ficou assim:

 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]; }]; 

O segundo método é opcional, mas no caso da fita iFunny, ela não foi chamada pelo sistema.

A pré-busca funciona, mas apenas chamamos o método de conteúdo após o conteúdo ativo.
Em geral, o trabalho de pré-busca padrão para o UICollectionView depende muito de como a exibição da coleção é implementada. Além disso, como não conhecemos absolutamente a implementação da pré-busca padrão, é impossível garantir sua operação estável. Portanto, implementamos nosso mecanismo de pré-busca, que sempre funcionava conforme necessário.

Nosso algoritmo de pré-busca


Antes de desenvolver o algoritmo de pré-busca, escrevemos todos os recursos do feed iFunny:

  1. Um feed pode consistir em diferentes tipos de conteúdo: fotos, vídeos, aplicativos da web, publicidade nativa.
  2. A fita funciona com paginação.
  3. A maioria dos usuários vira o feed apenas para frente.
  4. No iFunny, 20% das sessões do usuário ocorrem através do LTE.

Com base nessas condições, obtivemos um algoritmo simples:

  1. Há 1 elemento ativo na fita, todo o resto está inativo.
  2. O elemento ativo sempre precisa baixar o conteúdo até o fim.
  3. Cada item de conteúdo no feed tem seu próprio peso.
  4. Na conexão atual com a Internet, você pode carregar itens no valor de N.
  5. Cada vez que você rola a fita, alteramos o elemento ativo, calculamos quais elementos estão carregados e cancelamos o restante do carregamento.

A arquitetura no código deste algoritmo contém várias classes base e um protocolo:

  • IFPrefetchedCollectionProtocol

 @protocol IFPrefetchedCollectionProtocol @property (nonatomic, readonly) NSUInteger prefetchItemsCount; - (NSObject<IFFeedItemProtocol> *)itemAtIndex:(NSInteger)index; @end 

Este protocolo é necessário para obter os parâmetros da coleção e do conteúdo nos objetos de classe:

  • IFContentPrefetcher

 @interface IFContentPrefetcher : NSObject @property (nonatomic, weak) NSObject<IFPrefetchedCollectionProtocol> *collection; @property (nonatomic, assign) NSInteger activeIndex; @end 

A classe implementa a lógica do algoritmo para pré-busca de conteúdo:

  • IFPrefetchOperation

 @interface IFPrefetchOperation : NSObject @property (nonatomic, readonly) NSUInteger cost; - (void)fetchMinumumBuffer; - (void)fetchEntireBuffer; - (void)pause; - (void)cancel; - (BOOL)isEqualOperation:(IFPrefetchOperation *)object; @end 

Esta é a classe base de uma operação atômica, que descreve o trabalho útil de pré-buscar conteúdo específico e indica seu parâmetro - peso.

Para executar o algoritmo, descrevemos duas operações:

  1. A foto. Tem um peso de 1. Sempre totalmente carregado;
  2. Vídeo Ele tem um peso de 2. Carrega totalmente somente quando ativo. No estado inativo, os primeiros 200 KB são carregados.

Como métrica para avaliar a operação do algoritmo, escolhemos o número de ocorrências do elemento de interface do usuário do carregador por 1000 elementos de conteúdo visualizados.

Na pré-busca padrão dessa métrica, tínhamos cerca de 30 impressões / 1.000 elementos. Após a introdução do novo algoritmo, essa métrica caiu para 25 impressões / 1000 elementos.

Assim, o número de impressões do carregador diminuiu 20% e o número total de conteúdo visualizado pelos usuários aumentou ligeiramente.

Em seguida, procedemos à seleção dos parâmetros ideais para o Featured - a fita mais popular do iFunny.

Seleção de parâmetros para pré-busca


O algoritmo de pré-busca desenvolvido possui parâmetros de entrada:

  1. O custo total do download.
  2. O custo de carregamento de cada item.

Continuaremos medindo o número de carregadeiras.

Como ferramentas auxiliares para simplificar a coleta de dados, usaremos:

  1. Testes de cinza com um conjunto de estruturas KIF, OHHTTPStubs.
  2. sh-scripts e xcodebuild para executar testes com parâmetros diferentes.
  3. Perfil de rede 3G disponível na configuração Desenvolvedor - Condicionador de link de rede.

Vamos ver como cada uma dessas ferramentas nos ajudou.

Testes


Para simular como os usuários veem o conteúdo, decidimos usar a estrutura KIF, familiar aos desenvolvedores do iOS no Objective-C.

O KIF funciona muito bem para Objective-C e Swift, depois de algumas das manipulações fáceis descritas na documentação do KIF:
https://github.com/kif-framework/KIF#use-with-swift

Para testar a fita, escolhemos Objective-C, inclusive para poder substituir os métodos necessários no serviço de análise.

Vamos dar uma olhada no código de um teste simples, que obtivemos:

  - (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; }]; } 

No método de configuração de teste, devemos limpar o cache para que a cada inicialização o conteúdo seja carregado da rede e limpe completamente a pasta Caches no aplicativo.

Para garantir a estabilidade dos dados em cada um dos testes, usamos a biblioteca OHHTTPStubs, que facilita a substituição de respostas a solicitações de rede em algumas etapas simples:

  1. Defina parâmetros de consulta. Para nós, esse é o URL da solicitação de feed em destaque para a API - http://fun.co/rp/?feed=featured&limit=30
  2. Registre a resposta necessária e salve-a em um arquivo, anexe-a ao alvo com o teste.
  3. Defina opções de resposta. No código acima, esse é o cabeçalho do tipo de conteúdo e o código de resposta.
  4. Confira as instruções para os OHHTTPStubs.

Você pode ler mais sobre como trabalhar com OHHTTPStubs na documentação:
http://cocoadocs.org/docsets/OHHTTPSPSs/

O teste em si é assim:

 - (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 o KIF, obtemos um feed e percorremos 1000 elementos de conteúdo com uma espera de 1 segundo.

O método setupCustomPrefetchParams será discutido um pouco mais tarde.

Para determinar o número de carregadores mostrados, usaremos o tempo de execução do Objective-C e substituiremos o método do serviço de análise pelo método de teste:

 + (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]; } } 

Agora, temos um teste automático no qual o aplicativo sempre recebe o mesmo conteúdo e rola o mesmo número de elementos. E de acordo com seus resultados, ele escreve uma linha com estatísticas de execução no log.

Como a conexão com a Internet influencia principalmente o download de conteúdo, o teste com um conjunto de parâmetros precisa ser repetido mais de uma vez.

Automação de inicialização


Para automatizar e parametrizar os testes, decidimos usar o lançamento através do xcodebuild com a transferência dos parâmetros necessários.

Para passar parâmetros para o código, precisamos escrever o nome do argumento nas configurações de destino dos testes nas macros do pré-processador:



Para acessar um parâmetro do código Objective-C, duas macros devem ser declaradas:

 #define STRINGIZE(x) #x #define BUILD_PARAM(x) STRINGIZE(x) 

Agora, ao iniciar a partir do terminal usando o xcodebuild:

 xcodebuild test -workspace iFunny.xcworkspace -scheme iFunnyUITests -destination 'platform=iOS,id=DEVICE_ID' MAX_PREFETCH_COST="5" VIDEO_COST="2" IMAGE_COST="2" 

No código, você pode ler os parâmetros passados:

 - (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)]; } 

Agora tudo está pronto para executar esses testes offline usando scripts de shell.

Executando o xcodebuild com um conjunto de parâmetros 10 vezes 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 

Também geramos um script com o lançamento de vários conjuntos de parâmetros. Todos os testes duraram vários dias. Os dados obtidos foram resumidos em uma única tabela e os comparamos com a versão de trabalho atual.

Como resultado, a pré-busca mais simples de cinco elementos acabou sendo a melhor para as fitas do iFunny em destaque, sem levar em consideração o formato do conteúdo (vídeo ou imagem).

De acordo com o resultado


O artigo descreve a abordagem que permitirá explorar e monitorar qualquer parte crítica do aplicativo, sem alterar o código principal do projeto.

Aqui está o que ajudará a conduzir esses estudos:

  • Uso de estruturas de teste para ações monótonas.
  • Automação via xcodebuild para parametrizar startups.
  • Tempo de execução Objective-C para alterar a lógica necessária, sempre que possível.

Com base nessa abordagem para testar o aplicativo, começamos a adicionar o monitoramento de módulos importantes no estande local e já preparamos vários testes que executamos periodicamente para verificar a qualidade do aplicativo.

PS: De acordo com os resultados de nossos testes, as novas configurações de pré-busca em relação à opção de produção ganham cerca de 8%, na realidade, elas receberam uma diminuição na exibição de carregadores em 3%, o que significa que começamos a entregar sorrisos ao iFunny 3% com mais frequência :)

PPS: não vamos parar por aí, continuaremos melhorando a pré-busca de conteúdo.

Source: https://habr.com/ru/post/pt417059/


All Articles