How does Apptimize \ Optimizely work on iOS?

前端 未结 3 1235
醉话见心
醉话见心 2021-01-31 20:59

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

相关标签:
3条回答
  • 2021-01-31 21:27

    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

    0 讨论(0)
  • 2021-01-31 21:33

    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:

    1. 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.
    2. 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.
    3. 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.)

    0 讨论(0)
  • 2021-01-31 21:46

    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/
    

    enter image description here

    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

    0 讨论(0)
提交回复
热议问题