Apptimize \ Optimizely如何在iOS上工作?

我试图找出一些关于在幕后操作UI元素的实现,直接从Apptimize或Optimizely上的Web控制台。

更具体地说,我想了解以下内容:

1)客户端代码(iOS)如何将视图层次结构发送到Web服务器,以便在Web客户端上select任何UI元素时立即显示在iOS客户端上?

例如,我看到了FLEX,以及如何获得视图层次结构,但我不明白iPhone客户端如何“知道”在Web仪表板中select哪个视图。

2)此外,在Apptimize中,我可以从Web仪表板中select任何UI元素,更改其文本或颜色,并立即在应用程序中更改。 不仅如此,没有添加任何代码,只需要有SDK。

我所做的更改(文本,背景颜色等)将保留在应用程序的所有未来会话中。 这怎么能被执行?

我猜他们正在使用某种反思,但他们如何才能让它为所有用户和所有未来的会议工作? 客户端代码如何find正确的UI元素? 以及它如何在UITableViewCell上工作?

3)是否有可能检测到每次UIViewController加载? 即得到每个viewDidLoad的callback? 如果是的话,怎么样?

看到下面的一些截图:

在这里输入图像说明

在这里输入图像说明

我想知道同样的,也找不到一个明确的答案,所以这里是我(有希望)受过教育的猜测:

由于运行时环境,在Cocoa(-Touch)中使用Aspect-Orientated-Programming(AOP)实际上并不困难,其中规则被写入其他类的方法调用中。

如果你的谷歌为AOPObjective-C ,几个库popup,很好地包装运行时代码。

例如steinpete的方面库:

 [UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) { NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated); } error:NULL]; 

这个方法调用

 + (id<AspectToken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error { return aspect_add((id)self, selector, options, block, error); } 

调用aspect_add()

 static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) { NSCParameterAssert(self); NSCParameterAssert(selector); NSCParameterAssert(block); __block AspectIdentifier *identifier = nil; aspect_performLocked(^{ if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) { AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector); identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error]; if (identifier) { [aspectContainer addAspect:identifier withOptions:options]; // Modify the class to allow message interception. aspect_prepareClassAndHookSelector(self, selector, error); } } }); return identifier; } 

这再次调用了几个其他相当可怕的function,在运行时繁重的工作

 static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) { NSCParameterAssert(selector); Class klass = aspect_hookClass(self, error); Method targetMethod = class_getInstanceMethod(klass, selector); IMP targetMethodIMP = method_getImplementation(targetMethod); if (!aspect_isMsgForwardIMP(targetMethodIMP)) { // Make a method alias for the existing method implementation, it not already copied. const char *typeEncoding = method_getTypeEncoding(targetMethod); SEL aliasSelector = aspect_aliasForSelector(selector); if (![klass instancesRespondToSelector:aliasSelector]) { __unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding); NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass); } // We use forwardInvocation to hook in. class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding); AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector)); } } 

包括方法 – swizzling 。


很容易看到,在这里我们有一个工具,它将允许我们发送应用程序的当前状态,以在网页中重新构build该应用程序,而且也可以在现有代码中操作对象。
当然这只是一个起点。 您将需要一个组装应用程序并将其发送给用户的Web服务。


就我个人而言,我从来没有使用AOP来完成如此复杂的任务,但是我用它来教授所有的视图控制器跟踪function

 - (void)setupViewControllerTracking { NSError *error; @weakify(self); [UIViewController aspect_hookSelector:@selector(viewDidAppear:) withOptions:AspectPositionAfter usingBlock:^(id < AspectInfo > aspectInfo) { @strongify(self); UIViewController *viewController = [aspectInfo instance]; NSArray *breadCrumbs = [self breadCrumbsForViewController:viewController]; if (breadCrumbs.count) { NSString *pageName = [NSString stringWithFormat:@"/%@", [breadCrumbs componentsJoinedByString:@"/"]]; [ARAnalytics pageView:pageName]; } } error:&error]; } 

更新

我玩了一下,并能够创build一个原型。 如果添加到项目中,则将所有视图控制器的背景颜色更改为蓝色,5秒后所有视图控制器背景为橙色,通过使用AOP和dynamic方法添加。

源代码: https : //gist.github.com/vikingosegundo/0e4b30901b9498ae4b7b

5秒钟是由通知触发的,但显然这可能是一个networking事件。


更新2

我教我的原型打开一个networking接口,并接受rgb值的背景。
在模拟器中运行这将是

 http://127.0.0.1:8080/color/<r>/<g>/<b>/ http://127.0.0.1:8080/color/50/120/220/ 

在这里输入图像说明

我使用OCFWebServer

 // // ABController.m // ABTestPrototype // // Created by Manuel Meyer on 12.05.15. // Copyright (c) 2015 Manuel Meyer. All rights reserved. // #import "ABController.h" #import <Aspects/Aspects.h> #import <OCFWebServer/OCFWebServer.h> #import <OCFWebServer/OCFWebServerRequest.h> #import <OCFWebServer/OCFWebServerResponse.h> #import <objc/runtime.h> #import "UIViewController+Updating.h" #import "UIView+ABTesting.h" @import UIKit; @interface ABController () @property (nonatomic, strong) OCFWebServer *webserver; @end @implementation ABController void _ab_register_ab_notificaction(id self, SEL _cmd) { [[NSNotificationCenter defaultCenter] addObserver:self selector:NSSelectorFromString(@"ab_notifaction:") name:@"ABTestUpdate" object:nil]; } void _ab_notificaction(id self, SEL _cmd, id userObj) { NSLog(@"UPDATE %@", self); } +(instancetype)sharedABController{ static dispatch_once_t onceToken; static ABController *abController; dispatch_once(&onceToken, ^{ OCFWebServer *server = [OCFWebServer new]; [server addDefaultHandlerForMethod:@"GET" requestClass:[OCFWebServerRequest class] processBlock:^void(OCFWebServerRequest *request) { OCFWebServerResponse *response = [OCFWebServerDataResponse responseWithText:[[[UIApplication sharedApplication] keyWindow] listOfSubviews]]; [request respondWith:response]; }]; [server addHandlerForMethod:@"GET" pathRegex:@"/color/[0-9]{1,3}/[0-9]{1,3}/[0-9]{1,3}/" requestClass:[OCFWebServerRequest class] processBlock:^(OCFWebServerRequest *request) { NSArray *comps = request.URL.pathComponents; UIColor *c = [UIColor colorWithRed:^{ NSString *r = comps[2]; return [r integerValue] / 255.0;}() green:^{ NSString *g = comps[3]; return [g integerValue] / 255.0;}() blue:^{ NSString *b = comps[4]; return [b integerValue] / 255.0;}() alpha:1.0]; [[NSNotificationCenter defaultCenter] postNotificationName:@"ABTestUpdate" object:c]; OCFWebServerResponse *response = [OCFWebServerDataResponse responseWithText:[[[UIApplication sharedApplication] keyWindow] listOfSubviews]]; [request respondWith:response]; }]; dispatch_async(dispatch_queue_create(".", 0), ^{ [server runWithPort:8080]; }); abController = [[ABController alloc] initWithWebServer:server]; }); return abController; } -(instancetype)initWithWebServer:(OCFWebServer *)webserver { self = [super init]; if (self) { self.webserver = webserver; } return self; } +(void)load { class_addMethod([UIViewController class], NSSelectorFromString(@"ab_notifaction:"), (IMP)_ab_notificaction, "v@:@"); class_addMethod([UIViewController class], NSSelectorFromString(@"ab_register_ab_notificaction"), (IMP)_ab_register_ab_notificaction, "v@:"); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.00001 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self sharedABController]; }); [UIViewController aspect_hookSelector:@selector(viewDidLoad) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) { dispatch_async(dispatch_get_main_queue(), ^{ UIViewController *vc = aspectInfo.instance; SEL selector = NSSelectorFromString(@"ab_register_ab_notificaction"); IMP imp = [vc methodForSelector:selector]; void (*func)(id, SEL) = (void *)imp;func(vc, selector); }); } error:NULL]; [UIViewController aspect_hookSelector:NSSelectorFromString(@"ab_notifaction:") withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, NSNotification *noti) { dispatch_async(dispatch_get_main_queue(), ^{ UIViewController *vc = aspectInfo.instance; [vc updateViewWithAttributes:@{@"backgroundColor": noti.object}]; }); } error:NULL]; } @end 

 // // UIViewController+Updating.m // ABTestPrototype // // Created by Manuel Meyer on 12.05.15. // Copyright (c) 2015 Manuel Meyer. All rights reserved. // #import "UIViewController+Updating.h" @implementation UIViewController (Updating) -(void)updateViewWithAttributes:(NSDictionary *)attributes { [[attributes allKeys] enumerateObjectsUsingBlock:^(NSString *obj, NSUInteger idx, BOOL *stop) { if ([obj isEqualToString:@"backgroundColor"]) { [self.view setBackgroundColor:attributes[obj]]; } }]; } @end 

完整的代码: https : //github.com/vikingosegundo/ABTestPrototype

我叫Baraa,在Optimizely担任移动团队的一名软件工程实习生,所以我可以分享一些关于Optimizely SDK如何在Android和iOS上工作的高层次的见解。

在iOS上,Optimizely SDK使用了一种叫做swizzling的技术。 这使得我们可以根据我们的数据文件中当前正在进行的任何实验将视觉变化应用于应用程序。

在Android上,Optimizely使用reflection来将SDK附加为交互和生命周期事件的侦听器,以便根据数据文件中正在进行的任何实验来将可视化更改应用于应用程序。

有关我们在iOS上调用的方法和我们在Android上拦截的侦听器的完整列表,请参阅以下帮助文章: https : //help.optimizely.com/hc/en-us/articles/205014107-How-Optimizely- S-的SDK-工作-SDK-订单的执行中,实验-激活和目标执行#

Leanplum公司为iOS和Android提供了一个可视化界面编辑器:这不需要编码,Leanplum会自动检测元素并允许您更改它们。 没有工程师或应用程序商店需要重新提交。

关于你的问题:

  1. 通过在您的应用程序中安装iOS或Android SDK,您可以启用一个名为Visual Editor的function。 在开发模式下,在打开网站仪表板的情况下,SDK将实时向浏览器发送有关视图层次结构的信息。 视图层次结构以类似的方式在常规网站上构buildDOM。
  2. 您可以select应用程序中的任何UI元素,并实时更改其外观。 这通过识别视图树中的确切元素并将更改发送到SDK来工作。
  3. 这可以通过添加自定义钩子或称为“swizzling”的技术来实现。 看看这个博客文章 ,它是如何工作的。

要了解关于Leanplum Visual Interface Editor的更多信息,请查看leanplum.com 。 他们提供免费的30天试用。

(免责声明:我是Leanplum的工程师。)

Interesting Posts