
Hoy estamos abriendo una serie de art铆culos sobre lo que generalmente no se habla en conferencias y reuniones t茅cnicas. Esta y las publicaciones posteriores le dir谩n c贸mo funciona el mecanismo de monetizaci贸n en la aplicaci贸n de entretenimiento iFunny iOS popular en los Estados Unidos, que estamos desarrollando.
La publicidad es una de las principales formas de monetizar aplicaciones gratuitas. Pero ahora, 驴qu茅 opciones hab铆a en 2011 cuando apareci贸 iFunny? El servicio se cre贸 originalmente como un negocio fuerte y sostenible, por lo que desde el primer d铆a la compa帽铆a decidi贸 no coquetear con los usuarios y no participar en juegos con capitalizaci贸n condicional.
En ese momento, la opci贸n principal para la monetizaci贸n era crear una versi贸n gratuita y simplificada del servicio, y luego tratar de vender la funcionalidad principal. El consumidor era joven, inexperto y no estaba listo para desprenderse de cantidades superiores a un d贸lar.
Las matem谩ticas simples mostraron que con una conversi贸n del 10%, obtener un ARPU de m谩s de 10 centavos es una tarea casi imposible.
Luego tuve que pensar en c贸mo m谩s puedes monetizar el producto. El modelo publicitario ya ha funcionado muy bien en la web, y se podr铆a suponer que pronto tambi茅n florecer谩 en los tel茅fonos.
En general, el comienzo del modelo de monetizaci贸n de publicidad m贸vil puede considerarse la aparici贸n de AdWhirl, un servicio que le permiti贸 integrar el SDK de las redes publicitarias y rotarlas. Su apariencia nos permiti贸 elevar FillRate a un promedio del 50% en el mercado y hacer que los ingresos del modelo publicitario sean al menos comparables a las ventas de un d贸lar. El principio mismo de la implementaci贸n de todas las posibles fuentes de demanda y la organizaci贸n de la competencia entre ellas se ha convertido en el principal impulsor del crecimiento en la industria publicitaria y contin煤a siendo explotado hasta nuestros d铆as.
Pero cuanto m谩s complejo es el sistema, menos estable se vuelve, lo que es absolutamente inaceptable para grandes servicios del nivel iFunny. Comenzando a moverse en esta direcci贸n en 2011, la compa帽铆a cre贸 uno de los mecanismos m谩s efectivos para trabajar con pancartas m贸viles y publicidad nativa y aument贸 sus ingresos por usuario en 40 veces, lo que permiti贸 desarrollar no solo proyectos internos, sino tambi茅n invertir en otras compa帽铆as.
MoPub y compa帽铆a
Desde 2012, nos hemos mudado de AdWhirl a MoPub.
MoPub es una plataforma de publicidad m贸vil con la capacidad de agregar sus propios m贸dulos, que incluye varias herramientas excelentes:
- Mercado de MoPub: intercambio publicitario propio;
- mediador de redes publicitarias para trabajar con redes externas;
- Un mecanismo de pedido que le permite colocar pancartas de forma independiente en su propia aplicaci贸n y personalizar sus pantallas.
Las principales ventajas de MoPub:
- capaz de trabajar con la mayor铆a de las redes publicitarias;
- mecanismo claro para conectar nuevas redes de terceros;
- c贸digo abierto
- una gran cantidad de configuraciones y objetivos b谩sicos;
- Una gran comunidad alrededor de la red, incluso hay una conferencia propia.
MoPub tambi茅n tiene desventajas:
- No se aceptan solicitudes de grupo en GitHub y no hay ninguna reacci贸n a ellas;
- El panel de control es muy complejo, y para el desarrollador, mientras se depura, lleva alg煤n tiempo profundizar en su estructura.
El poder de la verdad
Como dijo el h茅roe de una pel铆cula rusa: "La fuerza es la verdad". En esta parte, hablar茅 sobre las dificultades que nosotros, como desarrolladores de aplicaciones, tuvimos que enfrentar despu茅s del primer mill贸n de descargas de iFunny, el crecimiento de la audiencia y el tr谩fico publicitario de m谩s de 100 socios.
Contenido
El mercado publicitario es una "casta" muy cerrada de empresas tecnol贸gicas, pero al mismo tiempo, los agregadores tienen una gran red de socios: desde grandes empresas que trabajan con millones de presupuestos hasta peque帽as empresas adaptadas a p煤blicos espec铆ficos.
Esta cercan铆a y fragmentaci贸n de los socios, a pesar de la moderaci贸n previa del banner y las reglas bastante estrictas sobre el contenido publicitario, no permite que los vendedores de publicidad m谩s honestos publiquen creatividades que est茅n prohibidas o estropeen la experiencia del usuario en la aplicaci贸n.
Existen varias categor铆as principales de contenido "obsceno" en los banners publicitarios:
- contenido porno. Recientemente, parece cada vez menos, pero sin embargo tiene lugar para ser. No podemos publicar este contenido en el art铆culo, por lo que la imagen no estar谩 aqu铆
- alertas del sistema en pancartas, se puede ver un ejemplo en uno de los usuarios twitter.com/IfunnyStates/status/1029393804749668352
- contenido con sonido. Las redes publicitarias y las animaciones no proh铆ben los sonidos, pero si el sonido se reproduce sin interactuar con la interfaz, los usuarios lo perciben como un error de la aplicaci贸n y afecta negativamente la experiencia del usuario.
- Llama la atenci贸n. Un buen banner deber铆a atraer la atenci贸n del usuario, pero esto no siempre sucede de manera honesta: a veces los videos parpadeantes caen en los banners. Otra forma deshonesta de hacer que el usuario toque el banner es simular la interfaz de la aplicaci贸n, por ejemplo, as铆:

Por cierto, en Rusia, un toque ordinario en este banner puede emitir una suscripci贸n paga para algunos operadores m贸viles, y ni siquiera lo sabr谩 hasta que vea los detalles. Esta es tambi茅n una forma deshonesta de trabajar con publicidad, pero los operadores en los Estados Unidos no tienen esta oportunidad.
Clics autom谩ticos
Como muestra mi experiencia, este es un caso extremadamente negativo para los usuarios. Usando las capacidades de JavaScript, WKWebView o UIWebView, as铆 como los agujeros dentro de la implementaci贸n de las bibliotecas publicitarias, puede crear anuncios que abrir谩n el contenido del banner y sacar谩n al usuario de la aplicaci贸n.
Para repetir este problema usando el ejemplo de MoPub, simplemente agregue el c贸digo javascript del siguiente contenido al banner:
<a href="https://ifunny.co" id="testbutton">test</a> <script>document.getElementById('testbutton').click(); </script>
Esto funcion贸 durante mucho tiempo en muchas versiones de MoPub, hasta la versi贸n 4.13.
Al explorar la implementaci贸n de MoPub, fue posible generar enlaces m谩s complejos que permitir铆an no solo abrir anuncios en pantalla completa, sino tambi茅n enviar al usuario a la AppStore a una aplicaci贸n espec铆fica y ni siquiera tener en cuenta la visualizaci贸n del banner.
Por cierto, en las notas de la versi贸n 4.13.0 del SDK de MoPub para iOS no hay informaci贸n sobre esta soluci贸n, ya que era un agujero bastante serio en el SDK, y los socios deshonestos de MoPub lo explotaron de manera bastante activa. Como muestran los registros, que analizar茅 m谩s adelante, todos los d铆as ten铆a que bloquear hasta 2 millones de intentos para abrir el banner sin la interacci贸n del usuario con 茅l.
En el caso de MoPub, result贸 ser f谩cil de encontrar y repetir el problema, pero otras redes con las que trabaja iFunny tienen un c贸digo cerrado, y tienes que lidiar con los clics autom谩ticos emergentes bloqueando pancartas o incluso desconectando las redes por un tiempo.
iFunny trabaja en estrecha colaboraci贸n con todos los socios publicitarios y les informa de dichos banners. Dado que el p煤blico joven de iFunny es interesante para los anunciantes, los socios est谩n dispuestos a conocerlos y eliminar dicha publicidad de la rotaci贸n.
Choque
Chocar siempre es malo. Peor a煤n, cuando suceden debido a una dependencia con fuente cerrada, y puede influir en ellos solo indirectamente. A lo largo de los a帽os de trabajo con publicidad en iFunnu, se han identificado varios tipos de accidentes, que se pueden dividir en varios grupos.
Estos incluyen excepciones en la biblioteca de red, WKWebView (UIWebView), OpenGL.
Es muy dif铆cil afectar directamente este tipo de bloqueos, pero a煤n fue posible afectar algunos de ellos, ya que hab铆a estudiado previamente el funcionamiento del componente WebView con WebGL.
As铆 es como se ve el stackrace de tales accidentes:
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
Adem谩s, se producen exclusivamente cuando se dejan en segundo plano. Esto se debe al hecho de que el motor OpenGL no deber铆a funcionar cuando la aplicaci贸n est谩 en segundo plano.
La soluci贸n aqu铆 result贸 ser bastante simple:
Al salir en segundo plano, debe tomar una captura de pantalla del banner.
Elimine la Vista publicitaria de la pantalla para que el componente WebView deje de usar OpenGL.
Cuando salga del fondo, devuelva todo como estaba.
En el c贸digo Objective-C, se ve as铆:
- (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; } }
Estos son problemas que ocurren en la uni贸n de iFunny, Mopub y el proveedor de publicidad.
Como regla, surgen despu茅s de actualizar la biblioteca del proveedor y debido a nuevas formas de interactuar con ellos.
El 煤ltimo caso fue en junio de este a帽o, despu茅s de la pr贸xima actualizaci贸n de una de las bibliotecas utilizadas. Una nueva forma de inicializar la biblioteca sugiere usar singleton para configurar la red.
Volvi茅ndolo dos veces, como sucedi贸 en la implementaci贸n, peri贸dicamente causaba un friso del hilo principal, por lo que tuve que ajustar la inicializaci贸n en dispatch_once.
El departamento de control de calidad de iFunny puede probar bien las bibliotecas publicitarias, por lo que este problema se encontr贸 durante la prueba de la actualizaci贸n.
Este tipo de bloqueos no se puede controlar en absoluto, ya que ocurre sin ning煤n cambio en el cliente.
Est谩n asociados con la actualizaci贸n del backend de los socios y la falta de compatibilidad con versiones anteriores. Tales bloqueos a menudo ocurren en grandes proveedores de publicidad, pero se solucionan r谩pidamente, ya que afectan una gran cantidad de aplicaciones al mismo tiempo.
Hubo casos en los que iFunny sin accidentes por d铆a cay贸 del 99.8% est谩ndar al 80%, y la cantidad de comentarios enojados en la historia fue de diez.
Rendimiento
La publicidad de banner, por regla general, utiliza componentes de WebView para mostrar publicidad, por lo que cada banner que se muestra es una inicializaci贸n de un nuevo WebView con todas sus dependencias.
Adem谩s, algunos socios tambi茅n usan WebView para comunicarse con su propio backend, ya que la publicidad de banner en dispositivos m贸viles es un descendiente de la publicidad en la web.
Sucede que despu茅s de la actualizaci贸n hay p茅rdidas de memoria dentro de la nueva biblioteca. Despu茅s de la aparici贸n de la herramienta Memory Graph en Xcode, se hizo mucho m谩s f谩cil encontrar fugas en bibliotecas de terceros, por lo que ahora los socios pueden ser informados r谩pidamente sobre ellas.
A continuaci贸n se muestra el GIF de iFunny inactivo cuando no hay publicidad para el usuario:

Soluciones
Pero a pesar de todos los problemas descritos anteriormente, iFunny es estable y todos los d铆as sonr铆e entre millones de usuarios.
A lo largo de los a帽os de trabajo activo con la publicidad, el equipo de desarrollo tiene varias herramientas que pueden monitorear con 茅xito los problemas de publicidad y responder a tiempo.
Sistema de registro
Ahora el sistema de registro de excepciones en iFunny se ha extendido a toda la aplicaci贸n: para esto, utilizamos nuestro propio backend con una base en ClickHouse y lo mostramos en Grafana.
Pero la primera tarea para trabajar con registros en la aplicaci贸n fue precisamente el registro de situaciones excepcionales en publicidad.
Hay varios componentes relacionados para determinar si una llamada se reenv铆a a iFunny. Te contar茅 m谩s sobre cada uno de ellos.
IFAdView
Este es el descendiente de la clase MPAdView (es responsable de mostrar anuncios en MoPub).
El m茅todo hitTest: withEvent se reemplaza en esta clase:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIView *hitView = [super hitTest:point withEvent:event]; if (hitView) { [[IFAdsExceptionManager instance] triggerTouchView]; } return hitView; }
Por lo tanto, activamos el hecho de que el usuario interactu贸 con el anuncio.
IFURLProtocol
Heredamos de NSURLProtocol y describimos el 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 es un desencadenante para abrir la AppStore desde la aplicaci贸n, enumeramos todas las URL disponibles para esto.
IFAdsExceptionManager
Una clase que recopila desencadenantes y genera un registro de excepci贸n en el registro.
Para dejar en claro qu茅 tipo de disparadores son, describir茅 cada m茅todo de la interfaz de esta clase.
- (void)triggerTouchView; . <source lang="objectivec">- (void)triggerItunesURL:(NSString *)itunesURL;
Un disparador que determina si se produce una redirecci贸n en iTunes.
- (void)triggerResignActive;
Un disparador para determinar la p茅rdida de actividad de una aplicaci贸n. Compara los dos disparadores anteriores.
- (void)resetTriggers;
Restablecer disparadores. Lo llamamos al dejar el fondo o cuando abrimos la AppStore nosotros mismos, por ejemplo, cuando enviamos al usuario a calificar en versiones anteriores de iOS.
@property (nonatomic, strong) FNAdConfigurationInfo *lastRequestedConfiguration; @property (nonatomic, strong) FNAdConfigurationInfo *lastLoadedConfiguration; @property (nonatomic, strong) FNAdConfigurationInfo *lastFailedConfiguration;
Propiedades para registrar los 煤ltimos anuncios solicitados y descargados con 茅xito o sin 茅xito. Necesario para formar un mensaje en el registro.
Se puede ver que el algoritmo result贸 ser bastante simple, pero efectivo. Nos permite rastrear no solo los descubrimientos autom谩ticos de MoPub, sino tambi茅n de otras redes.
Recientemente, los anuncios con apertura autom谩tica a menudo abren SKStoreProductViewController, por lo que ahora estamos trabajando en la definici贸n de apertura autom谩tica de este controlador. El algoritmo para definir esta excepci贸n ser谩 un poco m谩s complicado, pero Objective-C Runtime nos ayudar谩 aqu铆.
Stand local
Basado en el sistema de registro, iFunny tambi茅n comenz贸 a desarrollar un stand local para recibir y depurar anuncios que los usuarios ven en tiempo real.
El stand consta de:
- agente de construcci贸n
- dispositivos
- conjunto de pruebas para cada proveedor
Una de las soluciones interesantes que se utiliza en el stand es IDFA a partir de las quejas de los usuarios por publicidad real.
Desde aproximadamente 2016, dejamos de recibir anuncios reales dirigidos a los EE. UU. Utilizando solo VPN, por lo que tenemos que reemplazar los dispositivos IDFA con IDFA para usuarios reales.
Esto se hace con bastante facilidad utilizando Objective-C Runtime y swizzling.
Debe reemplazar el m茅todo advertisingIdentifier de la clase ASIdentifierManager.
Aqu铆 lo hacemos a trav茅s de la categor铆a:
@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
El m茅todo descrito en el
art铆culo se utiliza para transferir el IDFA de usuario a la compilaci贸n desde el agente de compilaci贸n.
En conclusi贸n, quiero decir que la publicidad de banner funciona muy bien en los Estados Unidos, y durante siete a帽os de su uso activo como m茅todo principal de monetizaci贸n, iFunny ha aprendido a trabajar bien con ella.
Pero a pesar del hecho de que las pancartas aportan el 75% de los ingresos de la compa帽铆a, se est谩 trabajando en m茅todos alternativos de monetizaci贸n y ya se ha ganado algo de experiencia en publicidad nativa y el uso de subastas de publicidad en el mercado estadounidense.
En general, hay algo que contar.