Anúncios em banner no aplicativo iOS



Hoje estamos abrindo uma série de artigos sobre o que geralmente não é falado em conferências e reuniões técnicas. Esta e as postagens subsequentes mostrarão como o mecanismo de monetização funciona no aplicativo de entretenimento iFunny iOS, popular nos EUA, que estamos desenvolvendo.

A publicidade é uma das principais maneiras de monetizar aplicativos gratuitos. Mas agora, quais eram as opções em 2011 quando o iFunny apareceu? O serviço foi originalmente construído como um negócio forte e sustentável; portanto, desde o primeiro dia, a empresa decidiu não flertar com os usuários e não se envolver em jogos com capitalização condicional.

Naquela época, a principal opção para monetização era criar uma versão simplificada gratuita do serviço e tentar vender a funcionalidade principal. O consumidor era jovem, inexperiente e não estava pronto para participar de valores superiores a um dólar.

A matemática simples mostrou que, com uma conversão de 10%, obter um ARPU de mais de 10 centavos é uma tarefa quase impossível.

Então eu tive que pensar em como mais você pode monetizar o produto. O modelo de publicidade já funcionou muito bem na web e pode-se supor que em breve também florescerá nos telefones.
Em geral, o início do modelo de monetização de publicidade para celular pode ser considerado o aparecimento do AdWhirl - um serviço que permite integrar o SDK das redes de publicidade e alterná-las. Sua aparência nos permitiu aumentar o FillRate para uma média de 50% no mercado e tornar a receita do modelo de publicidade pelo menos comparável às vendas de um dólar. O próprio princípio da implementação de todas as fontes possíveis de demanda e a organização da concorrência entre elas se tornaram o principal motor do crescimento na indústria da publicidade e continuam sendo exploradas até hoje.

Porém, quanto mais complexo o sistema, menos estável ele se torna, o que é absolutamente inaceitável para grandes serviços do nível iFunny. Começando a avançar nessa direção em 2011, a empresa criou um dos mecanismos mais eficazes para trabalhar com banners móveis e publicidade nativa e aumentou sua receita por usuário em 40 vezes, o que permitiu desenvolver não apenas projetos internos, mas também investir em outras empresas.

MoPub e empresa


Desde 2012, passamos do AdWhirl para o MoPub.

O MoPub é uma plataforma de publicidade móvel com a capacidade de adicionar seus próprios módulos, que incluem várias ótimas ferramentas:

  • Mercado MoPub - troca de publicidade própria;
  • mediador de redes de publicidade para trabalhar com redes externas;
  • um mecanismo de pedidos que permite que você coloque banners independentemente em seu próprio aplicativo e personalize as exibições.

As principais vantagens do MoPub:

  • capaz de trabalhar com a maioria das redes de publicidade;
  • mecanismo claro para conectar novas redes de terceiros;
  • código aberto
  • um grande número de configurações básicas e segmentação;
  • uma grande comunidade em torno da rede, há até uma conferência própria.

O MoPub também tem desvantagens:

  • solicitações de pool no GitHub não são aceitas e não há nenhuma reação a elas;
  • o painel de controle é muito complexo e, para o desenvolvedor, durante a depuração, leva algum tempo para aprofundar sua estrutura.

O poder da verdade


Como disse o herói de um filme russo: "A força está na verdade". Nesta parte, falarei sobre as dificuldades que nós, como desenvolvedores de aplicativos, tivemos que enfrentar após o primeiro milhão de downloads do iFunny, o crescimento do público e o tráfego de publicidade de mais de 100 parceiros.

Conteúdo


O mercado de publicidade é uma “casta” muito fechada de empresas de tecnologia, mas, ao mesmo tempo, os agregadores têm uma grande rede de parceiros: de grandes empresas que trabalham com milhões de orçamentos a pequenas empresas adaptadas a públicos-alvo específicos.

Essa proximidade e fragmentação dos parceiros, apesar da pré-moderação do banner e das regras bastante rígidas sobre o conteúdo da publicidade, não permite que os vendedores de publicidade mais honestos publiquem criativos proibidos ou prejudicam a experiência do usuário no aplicativo.

Existem várias categorias principais de conteúdo "obsceno" em banners publicitários:

  • conteúdo pornô. Recentemente, parece cada vez menos, mas mesmo assim acontece. Não podemos publicar este conteúdo no artigo, então a imagem não estará aqui
  • alertas do sistema em banners, um exemplo pode ser visto em um dos usuários twitter.com/IfunnyStates/status/1029393804749668352
  • conteúdo com som. Os sons não são proibidos pelas redes de anúncios, nem pelas animações, mas se o som tocar sem interagir com a interface, isso será percebido pelos usuários como um bug do aplicativo e afetará negativamente a experiência do usuário.
  • atenção chamando. Um bom banner deve atrair a atenção do usuário, mas isso nem sempre acontece de maneira honesta: às vezes, vídeos tremeluzentes caem nos banners. Outra maneira desonesta de fazer com que o usuário toque no banner é simular a interface do aplicativo, por exemplo:


A propósito, na Rússia, um simples toque nesse banner pode emitir uma assinatura paga para algumas operadoras de celular, e você nem saberá disso até ver os detalhes. Essa também é uma maneira desonesta de trabalhar com publicidade, mas as operadoras nos Estados Unidos não têm essa oportunidade.

Cliques automáticos


Como mostra minha experiência, esse é um caso extremamente negativo para os usuários. Usando os recursos de JavaScript, WKWebView ou UIWebView, além de brechas na implementação de bibliotecas de publicidade, você pode criar anúncios que abrirão o próprio conteúdo do banner e levarão o usuário para fora do aplicativo.

Para repetir esse problema usando o exemplo do MoPub, basta adicionar o código javascript do seguinte conteúdo ao banner:

<a href="https://ifunny.co" id="testbutton">test</a> <script>document.getElementById('testbutton').click(); </script> 

Isso funcionou por muito tempo em muitas versões do MoPub, até a versão 4.13.

Ao explorar a implementação do MoPub, foi possível gerar links mais complexos que permitiriam não apenas abrir anúncios em tela cheia, mas também enviar o usuário à AppStore para um aplicativo específico e nem levar em consideração a exibição do banner.

A propósito, nas notas de lançamento da versão 4.13.0 do MoPub SDK para iOS, não há informações sobre essa correção, pois foi um buraco bastante sério no SDK, e os parceiros desonestos do MoPub a exploraram bastante ativamente. Como mostram os logs, que discutirei mais adiante, todos os dias eu tinha que bloquear até 2 milhões de tentativas de abrir o banner sem a interação do usuário.

No caso do MoPub, ficou fácil encontrar e repetir o problema, mas outras redes com as quais o iFunny trabalha têm um código fechado e você precisa lidar com os cliques automáticos emergentes bloqueando banners ou até desconectando as redes por um tempo.
O iFunny trabalha em estreita colaboração com todos os parceiros de publicidade e informa esses banners. Como o público jovem do iFunny é interessante para os anunciantes, os parceiros desejam conhecê-los e remover essa publicidade da rotação.

Crash


Bater sempre é ruim. Pior ainda, quando eles ocorrem devido a uma dependência de código fechado, e você pode influenciá-los apenas indiretamente. Ao longo dos anos de trabalho com publicidade no iFunnu, vários tipos de falhas foram identificados por eles mesmos, que podem ser divididos em vários grupos.

  • Sistema

Isso inclui exceções na biblioteca de rede, WKWebView (UIWebView), OpenGL.
É muito difícil afetar diretamente esse tipo de falha, mas ainda era possível afetar algumas delas, tendo estudado anteriormente a operação do componente WebView com o WebGL.

É assim que o stackrace de tais falhas se parece:

1 libGPUSupportMercury.dylib gpus_ReturnNotPermittedKillClient + 12
2 AGXGLDriver gldUpdateDispatch + 7132
3 libGPUSupportMercury.dylib gpusSubmitDataBuffers + 172
4 AGXGLDriver gldUpdateDispatch + 12700
5 WebCore WebCore::GraphicsContext3D::reshape(int, int) + 524
6 WebCore WebCore::WebGLRenderingContextBase::initializeNewContext() + 712
7 WebCore WebCore::WebGLRenderingContextBase::WebGLRenderingContextBase(WebCore::HTMLCanvasElement*, WTF::RefPtr<WebCore::GraphicsContext3D>&&, WebCore::GraphicsContext3D::Attributes) + 512
8 WebCore WebCore::WebGLRenderingContext::WebGLRenderingContext(WebCore::HTMLCanvasElement*, WTF::PassRefPtr<WebCore::GraphicsContext3D>, WebCore::GraphicsContext3D::Attributes) + 36
9 WebCore WebCore::WebGLRenderingContextBase::create(WebCore::HTMLCanvasElement*, WebCore::WebGLContextAttributes*, WTF::String const&) + 1272
10 WebCore WebCore::HTMLCanvasElement::getContext(WTF::String const&, WebCore::CanvasContextAttributes*) + 520
11 WebCore WebCore::JSHTMLCanvasElement::getContext(JSC::ExecState&) + 212
12 JavaScriptCore llint_entry + 27340
13 JavaScriptCore llint_entry + 24756
14 JavaScriptCore llint_entry + 24756
15 JavaScriptCore llint_entry + 24756
16 JavaScriptCore llint_entry + 25676
17 JavaScriptCore llint_entry + 24756
18 JavaScriptCore llint_entry + 24656
19 JavaScriptCore vmEntryToJavaScript + 260
20 JavaScriptCore JSC::JITCode::execute(JSC::VM*, JSC::ProtoCallFrame*) + 164
21 JavaScriptCore JSC::Interpreter::executeCall(JSC::ExecState*, JSC::JSObject*, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&) + 348
22 JavaScriptCore JSC::profiledCall(JSC::ExecState*, JSC::ProfilingReason, JSC::JSValue, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&, WTF::NakedPtr<JSC::Exception>&) + 160
23 WebCore WebCore::JSEventListener::handleEvent(WebCore::ScriptExecutionContext*, WebCore::Event*) + 980
24 WebCore WebCore::EventTarget::fireEventListeners(WebCore::Event&, WebCore::EventTargetData*, WTF::Vector<WebCore::RegisteredEventListener, 1ul, WTF::CrashOnOverflow, 16ul>&) + 616
25 WebCore WebCore::EventTarget::fireEventListeners(WebCore::Event&) + 324
26 WebCore WebCore::EventContext::handleLocalEvents(WebCore::Event&) const + 108
27 WebCore WebCore::EventDispatcher::dispatchEvent(WebCore::Node*, WebCore::Event&) + 876
28 WebCore non-virtual thunk to WebCore::HTMLScriptElement::dispatchLoadEvent() + 80
29 WebCore WebCore::ScriptElement::execute(WebCore::CachedScript*) + 360
30 WebCore WebCore::ScriptRunner::timerFired() + 456
31 WebCore WebCore::ThreadTimers::sharedTimerFiredInternal() + 144
32 WebCore WebCore::timerFired(__CFRunLoopTimer*, void*) + 24
33 CoreFoundation __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 24
34 CoreFoundation __CFRunLoopDoTimer + 868
35 CoreFoundation __CFRunLoopDoTimers + 240
36 CoreFoundation __CFRunLoopRun + 1568
37 CoreFoundation CFRunLoopRunSpecific + 440
38 WebCore RunWebThread(void*) + 452
39 libsystem_pthread.dylib _pthread_body + 236
40 libsystem_pthread.dylib _pthread_start + 280
41 libsystem_pthread.dylib thread_start + 0


Além disso, eles ocorrem exclusivamente ao sair em segundo plano. Isso ocorre porque o mecanismo OpenGL não deve funcionar quando o aplicativo está em segundo plano.

A correção aqui acabou sendo bastante simples:

Ao sair em segundo plano, é necessário tirar uma captura de tela do banner.

Remova a exibição de publicidade da tela para que o componente WebView pare de usar o OpenGL.
Quando você sair do plano de fundo, retorne tudo como estava.

No código Objective-C, fica assim:

 - (void)onWillResignActive { if (self.adView.superview) { UIGraphicsBeginImageContext(self.adView.bounds.size); [self.adView.layer renderInContext:UIGraphicsGetCurrentContext()]; UIImage *adViewScreenShot = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); adViewThumbView = [[UIImageView alloc] initWithImage:adViewScreenShot]; adViewThumbView.backgroundColor = [UIColor clearColor]; adViewThumbView.frame = self.adView.frame; NSInteger adIndex = [self.adView.superview.subviews indexOfObject:self.adView]; [self.adView.superview insertSubview:adViewThumbView atIndex:adIndex]; [self.adView removeFromSuperview]; } } - (void)onDidBecomeActive { if (self.adView && adViewThumbView) { NSInteger adIndex = [adViewThumbView.superview.subviews indexOfObject:adViewThumbView]; [adViewThumbView.superview insertSubview:self.adView atIndex:adIndex]; [adViewThumbView removeFromSuperview]; adViewThumbView = nil; } } 

  • Integração

Esses são os problemas que ocorrem na junção do iFunny, Mopub e o provedor de publicidade.
Como regra, eles surgem após a atualização da biblioteca do provedor e devido a novas maneiras de interagir com eles.

O último caso foi em junho deste ano, após a próxima atualização de uma das bibliotecas utilizadas. Uma nova maneira de inicializar a biblioteca sugerida usando o singleton para definir as configurações de rede.

Voltando a ele duas vezes, como aconteceu na implementação, periodicamente causou um friso no encadeamento principal, então tive que envolver a inicialização em dispatch_once.

O departamento de controle de qualidade do iFunny pode testar bem as bibliotecas de publicidade. Portanto, esse problema foi encontrado durante o teste da atualização.

  • Inesperado

Esse tipo de falha não pode ser controlado, pois ocorre sem nenhuma alteração no cliente.

Eles estão associados à atualização do back-end dos parceiros e à falta de compatibilidade com versões anteriores. Essas falhas geralmente ocorrem em grandes fornecedores de publicidade, mas são rapidamente corrigidas, pois afetam um grande número de aplicativos ao mesmo tempo.

Houve casos em que o iFunny sem falhas por dia caiu do padrão de 99,8% para 80%, e o número de comentários irritados na história ficou em dezenas.

Desempenho


A publicidade em banner, como regra, usa componentes do WebView para exibir publicidade; portanto, cada banner mostrado é uma inicialização de um novo WebView com todas as suas dependências.

Além disso, alguns parceiros também usam o WebView para se comunicar com seu próprio back-end, pois a publicidade em banner em dispositivos móveis é descendente de publicidade na web.

Acontece que após a atualização, há vazamentos de memória dentro da nova biblioteca. Após o aparecimento da ferramenta Memory Graph no Xcode, ficou muito mais fácil encontrar vazamentos em bibliotecas de terceiros, para que os parceiros possam ser rapidamente informados sobre eles.

Abaixo está o GIF do iFunny ocioso quando não há publicidade para o usuário:



Soluções


Mas, apesar de todos os problemas descritos acima, o iFunny é estável e todos os dias causa sorrisos entre milhões de usuários.

Ao longo dos anos de trabalho ativo com publicidade, a equipe de desenvolvimento possui várias ferramentas que podem monitorar com êxito os problemas de publicidade e responder a eles a tempo.

Sistema de registro


Agora, o sistema de registro de exceções no iFunny se espalhou por todo o aplicativo: para isso, usamos nosso próprio back-end com base no ClickHouse e exibimos no Grafana.

Mas a primeira tarefa para trabalhar com logs no aplicativo foi precisamente o log de situações excepcionais na publicidade.

Existem vários componentes relacionados para determinar se uma chamada é encaminhada para o iFunny. Vou lhe contar mais sobre cada um deles.

IFAdView


Esse é o descendente da classe MPAdView (é responsável por exibir anúncios no MoPub).

O método hitTest: withEvent é substituído nesta classe:

 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIView *hitView = [super hitTest:point withEvent:event]; if (hitView) { [[IFAdsExceptionManager instance] triggerTouchView]; } return hitView; } 

Assim, acionamos o fato de o usuário interagir com o anúncio.

IFURLProtocol


Herdamos de NSURLProtocol e descrevemos o método:

 + (BOOL)canInitWithRequest:(NSURLRequest *)request { __weak NSString *wRequestURL = request.URL.absoluteString; dispatch_async(dispatch_get_main_queue(), ^{ if (wRequestURL == nil) return; if ([wRequestURL hasPrefix:@"itms-appss://itunes.apple.com"] || [wRequestURL hasPrefix:@"itms-apps://itunes.apple.com"] || [wRequestURL hasPrefix:@"itmss://itunes.apple.com"] || [wRequestURL hasPrefix:@"http://itunes.apple.com"] || [wRequestURL hasPrefix:@"https://itunes.apple.com"]) { [[IFAdsExceptionManager instance] adsTriggerItunesURL:wRequestURL]; } }); return NO; } 

Este é um gatilho para abrir a AppStore a partir do aplicativo. Listamos todos os URLs disponíveis para isso.

IFAdsExceptionManager


Uma classe que coleta gatilhos e gera um registro de exceção no log.

Para deixar claro que tipo de gatilhos são, descreverei cada método da interface desta classe.

 - (void)triggerTouchView;       . <source lang="objectivec">- (void)triggerItunesURL:(NSString *)itunesURL; 

Um gatilho que determina se um redirecionamento ocorre no iTunes.

 - (void)triggerResignActive; 

Um gatilho para determinar a perda de atividade por um aplicativo. Ele compara os dois gatilhos anteriores.

 - (void)resetTriggers; 

Redefinir gatilhos. Nós o chamamos quando saímos de segundo plano ou quando abrimos a AppStore, por exemplo, quando enviamos o usuário para avaliar em versões mais antigas do iOS.

 @property (nonatomic, strong) FNAdConfigurationInfo *lastRequestedConfiguration; @property (nonatomic, strong) FNAdConfigurationInfo *lastLoadedConfiguration; @property (nonatomic, strong) FNAdConfigurationInfo *lastFailedConfiguration; 

Propriedades para registrar os últimos anúncios solicitados e baixados com ou sem êxito. Necessário para formar uma mensagem no log.

Pode-se ver que o algoritmo acabou sendo bastante simples, mas eficaz. Ele nos permite rastrear não apenas as descobertas automáticas do MoPub, mas também de outras redes.

Recentemente, os anúncios com abertura automática geralmente abrem o SKStoreProductViewController, então agora estamos trabalhando na definição de abertura automática desse controlador. O algoritmo para definir essa exceção será um pouco mais complicado, mas o Objective-C Runtime nos ajudará aqui.

Suporte local


Com base no sistema de registro, o iFunny também começou a desenvolver um estande local para receber e depurar anúncios que os usuários veem em tempo real.

O suporte consiste em:

  • agente de construção
  • dispositivos
  • suíte de teste para cada provedor

Uma das soluções interessantes que são usadas no estande é o IDFA de reclamações de usuários para publicidade real.

Desde cerca de 2016, paramos de receber anúncios reais segmentados para os EUA usando apenas VPNs, então precisamos substituir dispositivos IDFA por IDFAs para usuários reais.

Isso é feito com bastante facilidade usando o Objective-C Runtime e swizzling.
Você precisa substituir o método advertisingIdentifier da classe ASIdentifierManager.

Aqui fazemos isso através da categoria:

 @interface ASIdentifierManager (IDFARewrite) @end @implementation ASIdentifierManager (IDFARewrite) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ if (AdsMonitorTests.customIDFA != nil) { [self swizzleIDFA]; } }); } + (void)swizzleIDFA { Class class = [self class]; SEL originalSelector = @selector(advertisingIdentifier); SEL swizzledSelector = @selector(swizzled_advertisingIdentifier); Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } } #pragma mark - Method Swizzling - (NSUUID *)swizzled_advertisingIdentifier { NSUUID *result = AdsMonitorTests.customIDFA; return result; } @end 

O método descrito no artigo é usado para transferir o IDFA do usuário para a construção do agente de construção.

Concluindo, quero dizer que a publicidade em banner funciona muito bem nos Estados Unidos e, durante sete anos de seu uso ativo como o principal método de monetização, o iFunny aprendeu a trabalhar bem com ela.

Mas, apesar de os banners trazerem 75% da receita da empresa, ainda estão em andamento métodos alternativos de monetização e já foi adquirida alguma experiência em publicidade nativa e no uso de leilões de publicidade no mercado americano.

Em geral, há algo a dizer.

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


All Articles