问题
I'm trying to figure out a few things about the implementation going on "behind the scene" for manipulating UI elements on the fly, straight from the web console on Apptimize or Optimizely.
More specifically, I want to understand the following:
1) How does the client code (iOS) send the view hierarchy to the web-server in such a way that when you choose any UI element on the web dashboard it immediately shown on the iOS client?
I saw FLEX for example, and how it manage to get the view hierarchy, but I don't understand how the iphone client "knows" which view is picked in the web dashboard.
2) Moreover, in Apptimize I can choose any UI element from the web dashboard, change its text or color and it will immediately change in the app. Not only that, without adding any code, just by having the SDK.
The changes I make (text, background color, etc) will remain for all the future sessions of the app. How can this be implemented?
I'm guessing they are using some sort of reflection, but how can they get it to work for all users and for all future sessions? how does the client code find the right UI element? and how does it work on UITableViewCell?
3) Is it possible to detect every time a UIViewController is loaded? i.e. get a callback on each viewDidLoad? if so, how?
See some screenshots below:
回答1:
I wonder the same and couldn't find a definite answer, so here is my (hopefully) educated guess:
Thanks to the runtime environment it is actually not that hard to use Aspect-Orientated-Programming (AOP) in Cocoa(-Touch), in which rules are written to hook in in other classes method calls.
If you google for AOP
and Objective-C
, several libraries pop up that wrap the runtime code nicely.
For example steinpete's Aspect library:
[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];
This method call
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error {
return aspect_add((id)self, selector, options, block, error);
}
calls 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;
}
which again calls several other quite frightening looking functions that do the heavy lifting in the runtime
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));
}
}
including method-swizzling.
It is easy to see that here we have a tool that will allow us to send the current state of an app to re-build this in a web-page but also to manipulate objects in an existing code.
Of course this is just a starting point. You will need a web service that assembles the app and sends it to the users.
Personally I never used AOP for such a complex task, but I used it for teaching all view controllers tracking capabilities
- (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];
}
update
I played a bit and was able to create a prototype. If added to a project, it will changes all view controllers background color to blue and after 5 seconds all living view controllers background to orange, by using AOP and dynamic method adding.
source code: https://gist.github.com/vikingosegundo/0e4b30901b9498ae4b7b
The 5 seconds are triggered by a notification, but it is obvious that this could be a network event.
update 2
I taught my prototype to open a network interface and accept rgb values for the background.
Running in simulator this would be
http://127.0.0.1:8080/color/<r>/<g>/<b>/
http://127.0.0.1:8080/color/50/120/220/
I use OCFWebServer for that
//
// 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
the full code: https://github.com/vikingosegundo/ABTestPrototype
回答2:
My name is Baraa and I'm a Software Engineering Intern working on the mobile team at Optimizely, so I can share some high-level insight into how the Optimizely SDK works on both Android and iOS.
On iOS, the Optimizely SDK uses a technique called swizzling. This allows us to apply visual changes to the application based on whatever experiments are currently active in our data file.
On Android, Optimizely uses reflection to attach the SDK as a listener for interaction and lifecycle events to apply visual changes to the application based on whatever experiments are active in the data file.
For the full list of methods that we swizzle on iOS and listeners that we intercept on Android, please check out this help article: https://help.optimizely.com/hc/en-us/articles/205014107-How-Optimizely-s-SDKs-Work-SDK-Order-of-execution-experiment-activation-and-goals#execute
回答3:
The company Leanplum offers a Visual Interface Editor for iOS and Android: This requires no coding, and Leanplum will automatically detect the elements and allow you to change them. No engineers or app store resubmissions required.
Regarding your questions:
- With installing the iOS or Android SDK in your app, you enable a feature called Visual Editor. While in development mode and with the Website Dashboard open, the SDK sends information about the view hierarchy in real-time to your Browser. The view hierarchy is scanned in a similar way a DOM is built on a regular website.
- You can choose any UI element on your app and change the appearance of it in real-time. This works by identifying the exact element in the view tree and sending the changes to the SDK.
- This can be achieved by adding custom hooks or a technique called "swizzling". Take a look at this blog post, how it works.
To learn more about the Leanplum Visual Interface Editor, check out leanplum.com. They offer a free 30-day trial.
(Disclaimer: I am an Engineer at Leanplum.)
来源:https://stackoverflow.com/questions/29879195/how-does-apptimize-optimizely-work-on-ios