How does Apptimize \ Optimizely work on iOS?

前端 未结 3 1240
醉话见心
醉话见心 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: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, BOOL animated) {
        NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated);
    } error:NULL];
    

    This method call

    + (id)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////
    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 
    #import 
    #import 
    #import 
    
    
    #import 
    #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) {
    
                                       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, 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

提交回复
热议问题