
Die schnelle und qualitativ hochwertige Bereitstellung von Inhalten für Benutzer ist die wichtigste Aufgabe, an der wir während der Arbeit an der iFunny-Anwendung ständig arbeiten. Das Fehlen wartender Elemente auch bei einer schlechten Verbindung - das ist es, was jeder Dienst zum Anzeigen von Medieninhalten tun möchte.
Wir hatten mehrere Iterationen für die Arbeit mit dem Vorabrufen von Inhalten. In jeder neuen Hauptversion haben wir etwas Neues erfunden und beobachtet, wie es für Benutzer funktioniert. In der nächsten Iteration der Arbeit mit dem Prefetching wurde beschlossen, zuerst die Metriken zu debuggen, die sich auf den lokalen Stand auswirken, und erst dann das Ergebnis an die Benutzer weiterzugeben.
In diesem Artikel werde ich darüber sprechen, wie das Vorabrufen in iFunny jetzt aussieht und wie der Forschungsprozess automatisiert wurde, um seine Einstellungen weiter zu optimieren.
Standard-Prefetching
In iOS 10 bot Apple die Möglichkeit, das Prefetching sofort auszuführen. Zu diesem Zweck verfügt die UICollectionView-Klasse über ein Feld:
@property (nonatomic, weak, nullable) id<UICollectionViewDataSourcePrefetching> prefetchDataSource; @property (nonatomic, getter=isPrefetchingEnabled) BOOL prefetchingEnabled;
Um das native Prefetching zu aktivieren, weisen Sie dem Feld prefetchDataSource einfach ein Objekt zu, das das Protokoll UICollectionViewDatasourcePrefetching implementiert, und legen Sie das zweite Feld in YES fest.
Um das Prefetching-Protokoll zu implementieren, müssen zwei Methoden beschrieben werden:
- (void)collectionView:(UICollectionView *)collectionView prefetchItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths; - (void)collectionView:(UICollectionView *)collectionView cancelPrefetchingForItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;
In der ersten Methode können Sie nützliche Arbeiten zur Erstellung von Inhalten ausführen.
Im Fall von iFunny sah es so aus:
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]; }];
Die zweite Methode ist optional, wurde jedoch beim iFunny-Band vom System überhaupt nicht aufgerufen.
Das Vorabrufen funktioniert, aber wir haben die Methode für Inhalte nur nach aktiven Inhalten aufgerufen.
Im Allgemeinen hängt die Arbeit des Standard-Prefetching für UICollectionView stark davon ab, wie die Sammlungsansicht implementiert ist. Da wir die Implementierung des Standard-Prefetching überhaupt nicht kennen, ist es außerdem unmöglich, seinen stabilen Betrieb zu gewährleisten. Deshalb haben wir unseren Prefetching-Mechanismus implementiert, der immer nach Bedarf funktioniert hat.
Unser Prefetching-Algorithmus
Vor der Entwicklung des Prefetching-Algorithmus haben wir alle Funktionen des iFunny-Feeds beschrieben:
- Ein Feed kann aus verschiedenen Arten von Inhalten bestehen: Bilder, Videos, Webapps, native Werbung.
- Das Band arbeitet mit Paginierung.
- Die meisten Benutzer drehen den Feed nur vorwärts.
- In iFunny finden 20% der Benutzersitzungen über LTE statt.
Basierend auf diesen Bedingungen haben wir einen einfachen Algorithmus erhalten:
- Das Band enthält 1 aktives Element, alle anderen sind inaktiv.
- Das aktive Element muss immer Inhalte bis zum Ende herunterladen.
- Jedes Inhaltselement im Feed hat sein eigenes Gewicht.
- Über die aktuelle Internetverbindung können Sie Elemente in Höhe von N laden.
- Jedes Mal, wenn Sie durch das Band scrollen, ändern wir das aktive Element, berechnen, welche Elemente geladen werden, und brechen den Rest des Ladens ab.
Die Architektur im Code dieses Algorithmus enthält mehrere Basisklassen und ein Protokoll:
- IFPrefetchedCollectionProtocol
@protocol IFPrefetchedCollectionProtocol @property (nonatomic, readonly) NSUInteger prefetchItemsCount; - (NSObject<IFFeedItemProtocol> *)itemAtIndex:(NSInteger)index; @end
Dieses Protokoll ist erforderlich, um die Parameter der Sammlung und des Inhalts in Klassenobjekten abzurufen:
@interface IFContentPrefetcher : NSObject @property (nonatomic, weak) NSObject<IFPrefetchedCollectionProtocol> *collection; @property (nonatomic, assign) NSInteger activeIndex; @end
Die Klasse implementiert die Logik des Algorithmus für das Vorabrufen von Inhalten:
@interface IFPrefetchOperation : NSObject @property (nonatomic, readonly) NSUInteger cost; - (void)fetchMinumumBuffer; - (void)fetchEntireBuffer; - (void)pause; - (void)cancel; - (BOOL)isEqualOperation:(IFPrefetchOperation *)object; @end
Dies ist die Basisklasse einer atomaren Operation, die die nützliche Arbeit des Vorabrufens bestimmter Inhalte beschreibt und deren Parametergewicht angibt.
Um den Algorithmus auszuführen, haben wir zwei Operationen beschrieben:
- Das Bild. Es hat ein Gewicht von 1. Immer voll beladen;
- Video Es hat ein Gewicht von 2. Wird nur dann vollständig geladen, wenn es aktiv ist. Im inaktiven Zustand werden die ersten 200 KB geladen.
Als Metrik zur Bewertung der Funktionsweise des Algorithmus haben wir die Anzahl der Treffer des Loader-UI-Elements pro 1000 angezeigten Inhaltselementen ausgewählt.
Beim Standard-Prefetching dieser Metrik hatten wir ungefähr 30 Impressionen / 1000 Elemente. Nach der Einführung des neuen Algorithmus fiel diese Metrik auf 25 Impressionen / 1000 Elemente.
Dadurch verringerte sich die Anzahl der Loader-Impressionen um 20% und die Gesamtzahl der von den Benutzern angezeigten Inhalte nahm leicht zu.
Dann gingen wir zur Auswahl der optimalen Parameter für Featured - das beliebteste Band in iFunny.
Auswahl der Parameter für das Prefetching
Der entwickelte Prefetching-Algorithmus verfügt über Eingabeparameter:
- Die Gesamtkosten des Downloads.
- Die Kosten für das Laden jedes Artikels.
Wir werden weiterhin die Anzahl der Lader messen.
Als Hilfsmittel zur Vereinfachung der Datenerfassung verwenden wir:
- Grautests mit einer Reihe von KIF-Frameworks, OHHTTPStubs.
- sh-scripts und xcodebuild zum Ausführen von Tests mit unterschiedlichen Parametern.
- 3G-Netzwerkprofil in der Einstellung Developer - Network Link Conditioner verfügbar.
Mal sehen, wie uns jedes dieser Tools geholfen hat.
Tests
Um zu emulieren, wie Benutzer Inhalte anzeigen, haben wir uns für das KIF-Framework entschieden, das iOS-Entwicklern unter Objective-C vertraut ist.
KIF eignet sich hervorragend für Objective-C und Swift, nachdem einige der in der KIF-Dokumentation beschriebenen einfachen Manipulationen vorgenommen wurden:
https://github.com/kif-framework/KIF#use-with-swiftZum Testen des Bandes haben wir uns für Objective-C entschieden, auch um die Methoden zu ersetzen, die wir im Analysedienst benötigen.
Werfen wir einen Blick auf den Code eines einfachen Tests, den wir erhalten haben:
- (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; }]; }
Bei der Test-Setup-Methode müssen wir den Cache leeren, damit bei jedem Start der Inhalt aus dem Netzwerk geladen wird, und den Caches-Ordner in der Anwendung vollständig löschen.
Um die Datenstabilität in jedem der Tests sicherzustellen, haben wir die OHHTTPStubs-Bibliothek verwendet, mit der Antworten auf Netzwerkanforderungen in wenigen einfachen Schritten ersetzt werden können:
- Abfrageparameter definieren. Für uns ist dies die URL der empfohlenen Feed-Anforderung an die API - http://fun.co/rp/?feed=featured&limit=30
- Notieren Sie die erforderliche Antwort, speichern Sie sie in einer Datei und hängen Sie sie mit dem Test an das Ziel an.
- Antwortoptionen definieren. Im obigen Code ist dies der Content-Type-Header und der Antwortcode.
- Lesen Sie die Anweisungen für OHHTTPStubs.
Weitere Informationen zur Arbeit mit OHHTTPStubs finden Sie in der Dokumentation:
http://cocoadocs.org/docsets/OHHTTPSPSs/Der Test selbst sieht folgendermaßen aus:
- (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]; }
Mit KIF erhalten wir einen Feed und scrollen dann mit einer Wartezeit von 1 Sekunde durch 1000 Inhaltselemente.
Die Methode setupCustomPrefetchParams wird etwas später erläutert.
Um die Anzahl der angezeigten Lader zu bestimmen, verwenden wir die Objective-C-Laufzeit und ersetzen die Methode aus dem Analysedienst durch die Testmethode:
+ (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]; } }
Jetzt haben wir einen automatischen Test, bei dem die Anwendung immer den gleichen Inhalt erhält und die gleiche Anzahl von Elementen scrollt. Und entsprechend seinen Ergebnissen schreibt es eine Zeile mit Ausführungsstatistiken in das Protokoll.
Da die Internetverbindung hauptsächlich das Herunterladen von Inhalten beeinflusst, muss der Test mit einem Parametersatz mehrmals wiederholt werden.
Startautomatisierung
Um die Tests zu automatisieren und zu parametrisieren, haben wir uns entschieden, den Start über xcodebuild mit der Übertragung der erforderlichen Parameter zu verwenden.
Um Parameter an den Code zu übergeben, müssen Sie den Namen des Arguments in die Zieleinstellungen für die Tests in Prepocessor-Makros schreiben:

Um über Objective-C-Code auf einen Parameter zuzugreifen, müssen zwei Makros deklariert werden:
#define STRINGIZE(x) #x #define BUILD_PARAM(x) STRINGIZE(x)
Wenn Sie jetzt mit xcodebuild vom Terminal aus starten:
xcodebuild test -workspace iFunny.xcworkspace -scheme iFunnyUITests -destination 'platform=iOS,id=DEVICE_ID' MAX_PREFETCH_COST="5" VIDEO_COST="2" IMAGE_COST="2"
Im Code können Sie die übergebenen Parameter lesen:
- (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)]; }
Jetzt ist alles bereit, um diese Tests mithilfe von Shell-Skripten offline auszuführen.
Xcodebuild zehnmal hintereinander mit einer Reihe von Parametern ausführen:
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
Wir haben auch ein Skript mit dem Start verschiedener Parametersätze generiert. Alle Tests dauerten mehrere Tage. Die erhaltenen Daten wurden in einer einzigen Tabelle zusammengefasst und mit der aktuellen Arbeitsversion verglichen.
Infolgedessen erwies sich das einfachste Vorabrufen von fünf Elementen als das beste für Featured iFunny-Bänder, unabhängig vom Inhaltsformat (Video oder Bild).
Nach dem Ergebnis
Der Artikel beschreibt den Ansatz, mit dem Sie wichtige Teile der Anwendung untersuchen und überwachen können, ohne den Hauptprojektcode zu ändern.
Folgendes hilft bei der Durchführung solcher Studien:
- Verwendung von Test-Frameworks für monotone Aktionen.
- Automatisierung über xcodebuild zur Parametrisierung von Starts.
- Laufzeitziel-C, um die erforderliche Logik nach Möglichkeit zu ändern.
Basierend auf diesem Ansatz zum Testen der Anwendung haben wir begonnen, die Überwachung wichtiger Module am lokalen Stand hinzuzufügen, und haben bereits mehrere Tests vorbereitet, die wir regelmäßig durchführen, um die Qualität der Anwendung zu überprüfen.
PS: Nach den Ergebnissen unserer Tests haben die neuen Prefetching-Einstellungen in Bezug auf die Produktionsoption etwa 8% gewonnen. In Wirklichkeit wurde die Anzeige der Lader um 3% verringert, was bedeutet, dass wir iFunny 3% häufiger zum Lächeln gebracht haben :)
PPS: Wir werden hier nicht aufhören, wir werden das Prefetching von Inhalten weiter verbessern.