
快速,高质量地向用户交付内容是我们在处理iFunny应用程序时不断努力的最重要任务。 即使连接不良也没有等待元素-这是任何用于观看媒体内容的服务都试图做到的。
我们使用了几次迭代来处理内容预取。 在每个新的主要版本中,我们都发明了一些新东西,并观察了它如何为用户服务。 在使用预取的下一个迭代中,决定首先调试会影响本地站点的度量,然后才将结果提供给用户。
在本文中,我将讨论iFunny现在的预取情况,以及如何自动进行研究过程以进一步调整其设置。
标准预取
在iOS 10中,Apple提供了开箱即用地运行预取的功能。 为此,UICollectionView类具有一个字段:
@property (nonatomic, weak, nullable) id<UICollectionViewDataSourcePrefetching> prefetchDataSource; @property (nonatomic, getter=isPrefetchingEnabled) BOOL prefetchingEnabled;
要启用本机预取,只需为prefetchDataSource字段分配一个实现UICollectionViewDatasourcePrefetching协议的对象,然后将第二个字段设置为YES。
要实现预取协议,必须描述两种方法:
- (void)collectionView:(UICollectionView *)collectionView prefetchItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths; - (void)collectionView:(UICollectionView *)collectionView cancelPrefetchingForItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;
在第一种方法中,您可以在准备内容时执行任何有用的工作。
对于iFunny,它看起来像这样:
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]; }];
第二种方法是可选的,但是对于iFunny磁带,系统根本不会调用它。
预取有效,但是我们仅在活动内容之后才对内容调用该方法。
通常,UICollectionView的标准预取工作在很大程度上取决于如何实现集合视图。 另外,由于我们完全不知道标准预取的实现,因此无法保证其稳定运行。 因此,我们实施了预取机制,该机制始终根据需要工作。
我们的预取算法
在开发预取算法之前,我们写出了iFunny feed的所有功能:
- Feed可以包含不同类型的内容:图片,视频,Web应用程序,本地广告。
- 该磁带可分页使用。
- 大多数用户只能将Feed向前翻转。
- 在iFunny中,20%的用户会话通过LTE发生。
基于这些条件,我们获得了一个简单的算法:
- 磁带中有1个活动元素,其余所有元素均处于非活动状态。
- 活动元素始终需要将内容下载到末尾。
- Feed中的每个内容项都有自己的权重。
- 在当前的Internet连接上,您可以加载N个项目。
- 每次滚动磁带时,我们都会更改活动元素并计算要加载的元素,然后取消其余的加载。
该算法代码中的体系结构包含几个基类和一个协议:
- IFPrefetchedCollectionProtocol
@protocol IFPrefetchedCollectionProtocol @property (nonatomic, readonly) NSUInteger prefetchItemsCount; - (NSObject<IFFeedItemProtocol> *)itemAtIndex:(NSInteger)index; @end
此协议对于在类对象中获取集合和内容的参数是必需的:
@interface IFContentPrefetcher : NSObject @property (nonatomic, weak) NSObject<IFPrefetchedCollectionProtocol> *collection; @property (nonatomic, assign) NSInteger activeIndex; @end
该类实现了内容预取算法的逻辑:
@interface IFPrefetchOperation : NSObject @property (nonatomic, readonly) NSUInteger cost; - (void)fetchMinumumBuffer; - (void)fetchEntireBuffer; - (void)pause; - (void)cancel; - (BOOL)isEqualOperation:(IFPrefetchOperation *)object; @end
这是原子操作的基类,它描述了预取特定内容的有用工作,并指示其参数-权重。
为了运行该算法,我们描述了两个操作:
- 图片。 它的重量为1。
- 录影带 权重为2。仅在激活时才完全加载。 在非活动状态下,将加载前200 KB。
作为评估算法运行情况的指标,我们选择了每查看1000个内容元素,加载程序UI元素的点击次数。
在标准预提取此指标时,我们大约有30次展示/ 1000个元素。 引入新算法后,该指标下降到25次展示/ 1000个元素。
因此,加载程序的印象数减少了20%,用户观看的内容总数略有增加。
然后,我们为iFunny中最流行的磁带Featured选择最佳参数。
选择预取参数
开发的预取算法具有输入参数:
- 下载的总费用。
- 加载每个项目的成本。
我们将继续测量装载机的数量。
作为简化数据收集的辅助工具,我们将使用:
- 使用一组KIF框架OHHTTPStubs进行灰色测试。
- sh-scripts和xcodebuild可以使用不同的参数运行测试。
- “开发人员-网络链接调节器”设置中提供了3G网络配置文件。
让我们看看这些工具如何对我们有所帮助。
测验
为了模拟用户如何查看内容,我们决定使用Objective-C上的iOS开发人员熟悉的KIF框架。
在KIF文档中介绍了一些简单的操作之后,KIF在Objective-C和Swift上非常有用:
https://github.com/kif-framework/KIF#use-with-swift为了测试磁带,我们选择了Objective-C,包括能够替换分析服务中所需的方法。
让我们看一下一个简单测试的代码,我们得到了:
- (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; }]; }
在测试设置方法中,我们必须清除缓存,以便在每次启动时从网络加载内容,并完全清除应用程序中的Caches文件夹。
为了确保每个测试中的数据稳定性,我们使用了OHHTTPStubs库,该库使您可以通过几个简单的步骤轻松将响应替换为网络请求:
- 定义查询参数。 对于我们来说,这是对API的精选供稿请求的URL- http://fun.co/rp/? feed= featured&limit=30
- 记录必要的答案并将其保存到文件中,并随测试附加到目标。
- 定义响应选项。 在上面的代码中,这是Content-Type标头和响应代码。
- 查看OHHTTPStubs的说明。
您可以在文档中阅读有关使用OHHTTPStubs的更多信息:
http://cocoadocs.org/docsets/OHHTTPSPSs/测试本身如下所示:
- (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]; }
使用KIF,我们将获得一个提要,然后在1秒钟的等待时间内滚动浏览1000个内容元素。
setupCustomPrefetchParams方法将在稍后讨论。
为了确定显示的装载程序数量,我们将使用Objective-C运行时并将分析服务中的方法替换为测试方法:
+ (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]; } }
现在,我们有了一个自动测试,其中应用程序始终接收相同的内容并滚动相同数量的元素。 然后根据其结果,将包含执行统计信息的行写入日志。
由于Internet连接主要影响内容的下载,因此使用一组参数进行的测试需要重复多次。
启动自动化
为了使测试自动化和参数化,我们决定使用通过xcodebuild进行的启动以及所需参数的传输。
要将参数传递给代码,我们需要在目标宏中的测试的目标设置中写入参数名称:

要从Objective-C代码访问参数,必须声明两个宏:
#define STRINGIZE(x) #x #define BUILD_PARAM(x) STRINGIZE(x)
现在,使用xcodebuild从终端启动时:
xcodebuild test -workspace iFunny.xcworkspace -scheme iFunnyUITests -destination 'platform=iOS,id=DEVICE_ID' MAX_PREFETCH_COST="5" VIDEO_COST="2" IMAGE_COST="2"
在代码中,您可以读取传递的参数:
- (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)]; }
现在一切就绪,可以使用Shell脚本脱机运行这些测试。
连续运行带有一组参数的xcodebuild 10次:
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
我们还通过启动各种参数集生成了一个脚本。 所有测试持续了几天。 获得的数据汇总在一个表中,我们将它们与当前的工作版本进行了比较。
结果,最简单的五个元素预取结果最适合“特色iFunny”功能区,而与内容格式(视频或图片)无关。
根据结果
本文介绍了一种方法,该方法将使您能够浏览和监视应用程序的任何关键部分,而无需更改主项目代码。
以下是有助于进行此类研究的内容:
- 使用测试框架进行单调操作。
- 通过xcodebuild自动化以参数化启动。
- 运行时Objective-C尽可能更改必要的逻辑。
基于这种测试应用程序的方法,我们开始在本地展台添加对重要模块的监视,并且已经准备了一些测试,我们会定期运行这些测试以检查应用程序的质量。
PS:根据我们的测试结果,相对于生产选项而言,新的预取设置赢得了大约8%,实际上,它们使装载机的显示减少了3%,这意味着我们开始向iFunny传递微笑的频率提高了3%:)
PPS:我们不会止步于此,我们将继续改善内容预取。