Today we are opening a series of articles about what is not usually talk at technical conferences and meetings. This and the following posts will tell you how the monetization mechanism works in iFunny, an entertainment iOS application popular in the US, which we are developing.
Advertising is one of the main ways to monetize free apps. But it is now, and what options were in 2011, when iFunny appeared? The service was originally built as a strong, sustainable business, so from the very first day the company decided not to flirt with users and not engage in games with conditional capitalization.
At that time, the main monetization option was to create a free cut-down version of the service, and then try to sell the main functionality. The consumer was young, inexperienced and was not ready to part with the sums of more than one dollar.
')
Simple mathematics showed that at a 10% conversion, getting ARPU more than 10 cents is almost impossible task.
Then I had to think about how else you can monetize the product. The advertising model already worked very well on the web, and it could be assumed that it would soon flourish on the phones.
In general, the beginning of the mobile advertising model of monetization can be considered the emergence of AdWhirl - a service that allowed to integrate the SDK ad networks and rotate them. Its appearance allowed FillRate to increase on average to 50% in the market and make the revenue from the advertising model at least comparable to the one-dollar sale. The principle of implementation of all possible sources of demand and the organization of competition between them has become the main driver of the growth of the advertising industry and continues to be exploited to this day.
But the more complex the system, the less stable it becomes, which is absolutely unacceptable for large services of the iFunny level. Starting to move in this direction in 2011, the company created one of the most effective mechanisms for working with mobile banner and native advertising and increased revenue per user 40 times, which allowed developing not only internal projects, but also investing in other companies.
MoPub and Company
Since 2012, we switched from AdWhirl to MoPub.
MoPub is a mobile advertising platform with the ability to add your own modules, which includes several large tools:
- MoPub marketplace - own advertising exchange;
- a mediator of ad networks for working with external networks;
- ordering mechanism that allows you to place banners in your own application and customize their displays.
The main advantages of MoPub:
- can work with most ad networks;
- clear mechanism for connecting new third-party networks;
- open source;
- a huge number of basic settings and targeting;
- there is a large community around the network, there is even its own conference.
There are MoPub and disadvantages:
- GitHub pull requests are not accepted and there is no reaction to them at all;
- The control panel is very complex, and it takes some time for the developer to debug into its structure.
Power is in the truth
As the hero of a Russian film said: “Strength in truth.” In this part, I will talk about the difficulties that we, as application developers, had to face after the first million downloads of iFunny, audience growth and advertising traffic from more than 100 partners.
Content
The advertising market is a very closed “caste” of technology companies, but at the same time aggregators have a large network of partners: from large companies that work with millions of budgets to small firms, tailored to specific target audiences.
This closeness and fragmentation of partners, despite the pre-moderation of banners and fairly strict rules on advertising content, allows not the most honest sellers of advertising to publish creatives that are prohibited or spoil the user experience in the application.
There are several main categories of "obscene" content in advertising banners:
- porn content Recently, it appears less and less, but nevertheless it has a place to be. We can not publish this content in the article, so the pictures will not be here
- system alerts in banners, you can see an example from one of the users on twitter.com/IfunnyStates/status/1029393804749668352
- content with sound. Sounds are not prohibited by ad networks, as well as animations, but if the sound is played without interacting with the interface, this is perceived by users as an application bug and negatively affects the user experience.
- to attract attention. A good banner should attract the attention of the user, but this is not always the case in an honest manner: sometimes flickering videos appear in the banners. Another dishonest way to force a user to tap on a banner is to imitate an application interface, like this:

By the way, in Russia, the usual tap on this banner can make a paid subscription from some cellular operators, and you will not even know about it until you see the details. This is also a dishonest way to work with advertising, but operators in the US have no such opportunity.
Autoclics
As my experience shows, this is a very negative case for users. Using the capabilities of JavaScript, WKWebView or UIWebView, as well as holes within the implementation of advertising libraries, you can make an advertisement that will open the banner content itself and lead the user out of the application.
In order to repeat this problem with the example from MoPub, it is enough to add the following javascript code to the banner:
<a href="https://ifunny.co" id="testbutton">test</a> <script>document.getElementById('testbutton').click(); </script>
This worked long in many versions of MoPub, up to version 4.13.
Exploring the implementation of MoPub, it was possible to generate more complex links that allowed not only to open ads on full screen, but also to send the user to the AppStore for a specific application and not even take into account the display of the banner.
By the way, in the release notes for version 4.13.0 of the MoPub SDK for iOS, there is no information about this fix, since it was a rather serious hole in the SDK, and MoPub's dishonest partners were exploiting it quite actively. As the logs show, which I will discuss next, I had to block up to 2 million attempts to open a banner every day without user interaction with it.
In the case of MoPub, it turned out to find and repeat the problem quite easily, but other networks with which iFunny works have a closed code, and it is necessary to deal with the resulting auto-clicks by blocking banners or even turning off networks for a while.
iFunny works closely with all advertising partners and informs them about such banners. Since the young audience of iFunny is interesting to advertisers, partners willingly go forward and remove such advertising from the rotation.
Crash
Crash is always bad. Even worse, when they happen due to a closed code dependency, and they can only be influenced indirectly. Over the years, working with advertising in iFunna has identified for itself several types of crashes that can be divided into several groups.
These include exceptions in the network library, WKWebView (UIWebView), OpenGL.
It is very difficult to directly affect this type of crash, but it was still possible to influence some of them by first studying the work of the WebView component with WebGL.
It looks like a beautiful scene such beautiful:
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
And they occur only when leaving in the background. This is due to the fact that the OpenGL engine should not work when the application is in the background.
The fix here turned out to be quite simple:
When leaving in the background you need to take a banner screenshot.
Remove the ad View from the screen so that the WebView component stops using OpenGL.
When you exit the background, return everything as it was.
In Objective-C code, it looks like this:
- (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; } }
These are problems that occur at the junction of iFunny, Mopub and the advertising provider.
As a rule, they arise after updating the providers library and because of new ways of interacting with them.
The last such case was in June of this year, after the next update of one of the used libraries. A new way to initialize the library suggested using singleton to configure network settings.
Appealing to it twice, as it happened in the implementation, periodically caused a frieze of the main thread, so I had to wrap the initialization in dispatch_once.
IFunny's QA department is able to test advertising libraries well, so this problem was found during the update testing.
This type of crashes cannot be controlled at all, as it happens without any changes in the client.
They are associated with updating the backend of the partners and the lack of backward compatibility. Such crashes often occur with large advertising providers, but are quickly corrected, since they act on a large number of applications at the same time.
There were cases when crash free iFunny per day dropped from the standard 99.8% to 80%, and the number of angry comments in the page was in the tens.
Performance
Banner ads typically use WebView components to display ads, so each banner shown is an initialization of a new WebView with all its dependencies.
In addition, some partners use WebView to communicate with their own backend, as banner advertising on mobile devices is a descendant of advertising on the web.
It happens that after the update there are memory leaks inside the new library. After the appearance of the Memory Graph tool in Xcode, it has become much easier to find leaks in third-party libraries, so now we are able to promptly report them to our partners.
Below is the iFunny gif in idle when there is no advertising for the user:
Solutions
But despite all the problems described above, iFunny works stably and every day causes smiles to millions of its users.
Over the years of active work with advertising, the development team has acquired several tools that allow you to successfully monitor advertising problems and respond to them in time.
Logging system
Now the system of exceptions logging in iFunny has spread to the entire application: it uses its own backend with a base on ClickHouse and a display in Grafana.
But the first task for working with logs in the application was exactly logging of exceptional situations in advertising.
There are several related components for determining whether redirects are in iFunny. I'll tell you more about each of them.
IFAdView
This is the heir from the MPAdView class (he is responsible for displaying ads on MoPub).
In this class, the hitTest: withEvent method is overridden:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIView *hitView = [super hitTest:point withEvent:event]; if (hitView) { [[IFAdsExceptionManager instance] triggerTouchView]; } return hitView; }
Thus, we set the trigger on what the user interacted with the advertisement.
IFURLProtocol
Inherit from NSURLProtocol and describe the method:
+ (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; }
This is a trigger to open the AppStore from the application, we list all available URLs for this.
IFAdsExceptionManager
A class that collects triggers and generates an exception record in the log.
To make it clear what are the triggers, I will describe each interface method of this class.
- (void)triggerTouchView; . <source lang="objectivec">- (void)triggerItunesURL:(NSString *)itunesURL;
A trigger that determines that a redirect occurs in iTunes.
- (void)triggerResignActive;
A trigger for determining the loss of activity by an application. It compares the two previous triggers.
- (void)resetTriggers;
Reset triggers. We call when we go to the background or when we open the AppStore ourselves, for example, when we send a user to rate in older versions of iOS.
@property (nonatomic, strong) FNAdConfigurationInfo *lastRequestedConfiguration; @property (nonatomic, strong) FNAdConfigurationInfo *lastLoadedConfiguration; @property (nonatomic, strong) FNAdConfigurationInfo *lastFailedConfiguration;
Properties for recording the last successfully or unsuccessfully requested and loaded advertising. Needed to form a message in the log.
It is seen that the algorithm turned out quite simple, but effective. It allows us to track not only auto discoveries from MoPub, but also from other networks.
Recently, auto-discovery advertising often opens with SKStoreProductViewController, so now we are working on defining auto-opening of this controller. The algorithm for determining this exception will be somewhat more complicated, but here the Objective-C Runtime will help us.
Local stand
Based on the logging system in iFunny, they also began to develop a local booth in order to receive and debug the ads that users see in real time.
The stand consists of:
- build agent
- devices
- test suite for each provider
One of the interesting solutions that is used at the stand is IDFA from user complaints for receiving real advertising.
Since about 2016, we have stopped receiving real ads targeted to the USA using only VPN, so we have to replace IDFA devices with IDFAs of real users.
This is done quite easily with Objective-C Runtime and Swizzling.
It is necessary to replace the advertisingIdentifier method of the ASIdentifierManager class.
Here we do it through the category:
@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
For transfer from the build-agent of the user IDFA to the build, the method described in the
article is used .
In conclusion, I would like to say that banner advertising works perfectly in the USA, and during seven years of its active use as the main method of monetization in iFunny, we learned how to work well with it.
But despite the fact that banners bring 75% of the company's revenues, work is constantly underway on alternative ways to monetize and some experience has already been gained in native advertising and the use of advertising auctions in the US market.
In general, there is something to tell.