iOS应用中的横幅广告



今天,我们将打开一系列文章,介绍技术会议上通常不谈论的内容。 这篇文章和后续文章将告诉您获利机制如何在我们正在开发的美国流行的iFunny iOS娱乐应用程序中工作。

广告是通过免费应用获利的主要方法之一。 但是现在,2011年iFunny出现时有哪些选择? 该服务最初是作为强大的,可持续的业务而建立的,因此从第一天起,该公司就决定不与用户调情,也不从事有条件大写的游戏。

当时,获利的主要选择是创建免费的精简版服务,然后尝试出售主要功能。 消费者还很年轻,没有经验,还不愿意多花一美元。

简单的数学表明,转换率为10%时,ARPU超过10美分几乎是不可能的任务。

然后,我不得不考虑如何通过该产品获利。 广告模式在网络上已经非常有效,可以假设它也将很快在手机上盛行。
通常,可以将获利的移动广告模型的开始视为AdWhirl的出现,该服务可让您集成广告网络的SDK并对其进行轮换。 它的出现使FillRate在市场上的平均占有率提高到50%,并使广告模式的收入至少可与1美元的销售额相提并论。 实施所有可能的需求源以及它们之间的竞争组织的基本原则已成为广告行业增长的主要驱动力,并一直持续到今天。

但是系统越复杂,其稳定性就越差,这对于iFunny级的大型服务是绝对不能接受的。 从2011年开始朝这个方向发展,该公司创建了一种最有效的机制来处理移动横幅广告和原生广告,并将每位用户的收入提高了40倍,不仅可以开发内部项目,还可以投资其他公司。

MoPub和公司


自2012年以来,我们已从AdWhirl迁移到MoPub。

MoPub是一个移动广告平台,能够添加自己的模块,其中包括一些出色的工具:

  • MoPub市场-自己的广告交易平台;
  • 广告网络的调解员,用于与外部网络合作;
  • 一种排序机制,可让您在自己的应用程序中独立放置横幅并自定义其显示。

MoPub的主要优点:

  • 能够与大多数广告网络合作;
  • 建立新的第三方网络的清晰机制;
  • 开源
  • 大量的基本设置和定位;
  • 网络周围的大型社区,甚至都有自己的会议。

MoPub也有缺点:

  • GitHub上的池请求不被接受,并且对它们完全没有反应;
  • 控制面板非常复杂,对于开发人员而言,在调试时需要花一些时间来研究其结构。

真理的力量


正如一部俄罗斯电影的主人公所说:“实力就是真理。” 在这一部分中,我将讨论作为应用程序开发人员的iFunny的首百万次下载后所面临的困难,受众的增长以及来自100多个合作伙伴的广告流量。

内容内容


广告市场是技术公司非常封闭的“角色”,但同时,聚合商拥有庞大的合作伙伴网络:从处理数百万预算的大型公司到为特定目标受众量身定制的小型公司。

尽管标语预先审核并且对广告内容制定了相当严格的规定,但合作伙伴之间的这种亲密关系和支离破碎的关系,并不能使最诚实的广告销售商发布被禁止或破坏应用程序用户体验的广告素材。

广告横幅中有“淫秽”内容的几个主要类别:

  • 色情内容。 最近,它的出现越来越少,但还是如此。 我们无法在文章中发布此内容,因此图片将不在此处
  • 标语中的系统警报,可以在其中一个用户twitter.com/IfunnyStates/status/1029393804749668352上查看示例
  • 满足于声音。 广告网络和动画均未禁止声音,但是如果声音在播放时未与界面互动,则用户会认为这是应用程序错误,会对用户体验产生负面影响
  • 吸引注意力。 好的横幅广告应该引起用户的注意,但这并不总是以诚实的方式发生:有时,闪烁的视频会落入横幅广告中。 吸引用户点击横幅的另一种不诚实的方法是模拟应用程序界面,例如:


顺便说一句,在俄罗斯,只要轻按一下此横幅,就可以为一些移动运营商发出付费订阅,直到您看到详细信息,您甚至都不会知道。 这也是处理广告的不诚实方法,但是美国的运营商没有这种机会。

自动点击


据我的经验表明,这对用户来说是极为不利的情况。 使用JavaScript,WKWebView或UIWebView的功能,以及广告库实现内部的漏洞,您可以制作广告,这些广告将打开横幅内容本身,并将用户引出应用程序。

为了使用MoPub的示例重复此问题,只需将以下内容的javascript代码添加到标题中:

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

在MoPub的许多版本(直到版本4.13)中,此功能都已使用了很长时间。

通过探索MoPub的实现,可以生成更复杂的链接,这些链接不仅允许全屏打开广告,还可以将用户发送到特定应用程序的AppStore,甚至不考虑横幅显示。

顺便说一句,在iOS的MoPub SDK 4.13.0版本的发行说明中,没有有关此修复程序的信息,因为它是SDK中的一个相当严重的漏洞,并且MoPub的不诚实合作伙伴非常积极地利用了它。 正如我稍后将讨论的日志所显示的那样,每天我不得不阻止多达200万次尝试打开横幅,而无需用户与之交互。

对于MoPub来说,发现并重现问题很容易,但是与iFunny合作的其他网络的代码却是封闭的,您必须通过阻止横幅广告甚至断开网络一段时间来应对新兴的自动点击问题。
iFunny与所有广告合作伙伴紧密合作,并将此类标语告知他们。 由于iFunny的年轻受众对广告客户很感兴趣,因此合作伙伴愿意与他们见面并从轮播中删除此类广告。

崩溃


崩溃总是不好的。 更糟糕的是,当它们由于封闭源的依赖而发生时,您只能间接影响它们。 多年来,在iFunnu从事广告业务的工作,已经为自己确定了几种崩溃类型,这些崩溃可以分为几类。

  • 系统名称

这些包括网络库,WKWebView(UIWebView),OpenGL中的异常。
直接影响这类崩溃非常困难,但是在先前研究了WebGL对WebView组件的操作之后,仍然有可能影响其中一些崩溃。

这是此类崩溃的堆栈外观:

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


而且,它们仅在离开后台时发生。 这是由于以下事实:当应用程序在后台运行时,OpenGL引擎不起作用。

原来的解决方法非常简单:

离开背景时,您需要对横幅进行截图。

从屏幕上删除广告视图,以便WebView组件停止使用OpenGL。
退出背景时,返回所有内容。

在Objective-C代码中,它看起来像这样:

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

  • 整合性

这些是在iFunny,Mopub和广告提供商的交界处发生的问题。
通常,它们是在更新提供程序库之后出现的,并且是由于与它们进行交互的新方式所致。

上一次此类案件发生在今年6月,这是对所使用的其中一个图书馆的下一次更新之后。 建议使用单例配置网络设置的初始化库的新方法。

在实现过程中,两次进行该操作会定期引起主线程的混乱,因此我不得不将初始化包装在dispatch_once中。

iFunny质量检查部门能够很好地测试广告库,因此在测试更新期间发现了此问题。

  • 出乎意料

这类崩溃完全无法控制,因为它发生在客户端中而没有任何更改。

它们与更新合作伙伴的后端以及缺乏向后兼容性有关。 此类崩溃通常在大型广告提供商处发生,但由于会同时影响大量应用程序,因此很快得到修复。

在某些情况下,每天无崩溃的iFunny从标准的99.8%下降到80%,而故事中的愤怒评论数量却高达数十。

性能表现


通常,横幅广告使用WebView组件显示广告,因此显示的每个横幅都是新WebView及其所有依赖项的初始化。

此外,由于移动设备上的横幅广告是网络广告的后代,因此一些合作伙伴还使用WebView与自己的后端进行通信。

发生的情况是,升级后,新库中存在内存泄漏。 在Xcode中出现“内存图”工具后,在第三方库中查找漏洞变得更加容易,因此现在可以迅速将其告知合作伙伴。

下面是没有广告给用户时iFunny空闲的GIF:



解决方案


但是,尽管存在上述所有问题,iFunny还是稳定的,每天都会在数百万的用户中引起微笑。

多年来与广告积极合作,开发团队拥有多种工具,可以成功监控广告问题并及时做出响应。

测井系统


现在,iFunny中的异常日志记录系统已经扩展到整个应用程序:为此,我们将自己的后端与ClickHouse一起使用,并在Grafana中显示。

但是,在应用程序中使用日志的首要任务恰恰是在广告中记录特殊情况。

有几个相关的组件可以确定是否将呼叫转发到iFunny。 我会告诉您更多有关它们的信息。

IFAdView


这是MPAdView类的后代(负责在MoPub上展示广告)。

hitTest:withEvent方法在此类中被覆盖:

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

因此,我们根据用户与广告进行交互的事实来设置触发器。

IFURL协议


我们从NSURLProtocol继承并描述该方法:

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

这是从应用程序打开AppStore的触发器,我们列出了所有可用的URL。

IFAdsExceptionManager


收集触发器并在日志中生成异常记录的类。

为了清楚说明触发器是什么,我将介绍此类接口的每种方法。

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

确定iTunes中是否发生重定向的触发器。

 - (void)triggerResignActive; 

确定应用程序活动丢失的触发器。 它比较了之前的两个触发器。

 - (void)resetTriggers; 

重置触发器。 我们在离开后台或自己打开AppStore时(例如,当我们向用户发送旧版iOS的评分时)将其称为。

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

用于记录最后成功或未成功请求和下载的广告的属性。 需要向日志形成消息。

可以看出,该算法非常简单,但是有效。 它使我们不仅可以跟踪来自MoPub的自动发现,还可以跟踪其他网络的自动发现。

最近,具有自动打开功能的广告经常会打开SKStoreProductViewController,因此现在我们正在研究自动打开此控制器的定义。 定义此异常的算法将稍微复杂一些,但是Objective-C运行时将在这里为我们提供帮助。

当地摊位


基于日志记录系统,iFunny还开始开发本地摊位,以便接收和调试用户实时看到的广告。

展台包括:

  • 建立代理
  • 设备
  • 每个提供商的测试套件

展台上使用的一种有趣的解决方案是IDFA,它来自用户对真实广告的投诉。

从2016年左右开始,我们不再仅使用VPN接收针对美国的真实广告,因此我们必须将IDFA设备替换为真实用户的IDFA。

使用Objective-C运行时并轻松进行此操作很容易。
您需要替换ASIdentifierManager类的advertiseIdentifier方法。

在这里,我们通过类别进行操作:

 @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 

本文中描述的方法用于将用户IDFA从构建代理转移到构建。

最后,我想说的是,横幅广告在美国非常有效,并且在其连续七年作为主要货币化方法的积极应用中,iFunny学会了与之合作的良好方法。

但是,尽管横幅广告为公司带来了75%的收入,但有关替代货币化方法的工作仍在进行中,并且在本地广告和在美国市场上使用广告拍卖已获得了一些经验。

一般来说,有话要说。

Source: https://habr.com/ru/post/zh-CN426553/


All Articles