Подтвердить что ты не робот

Как работает Apptimize\Optimizely на iOS?

Я пытаюсь выяснить несколько вещей о том, что реализация происходит "за сценой" для управления элементами пользовательского интерфейса "на лету" прямо с веб-консоли на Apptimize или Optimizely.

В частности, я хочу понять следующее:

1) Как клиентский код (iOS) отправляет иерархию представлений на веб-сервер таким образом, что, когда вы выбираете какой-либо элемент пользовательского интерфейса на веб-панели, он сразу отображается на клиенте iOS?

Я видел, например, FLEX, и как ему удается получить иерархию представлений, но я не понимаю, как клиент iphone "знает", какое представление выбрано в веб-панели.

2) Кроме того, в Apptimize я могу выбрать любой элемент пользовательского интерфейса из веб-панели, изменить его текст или цвет, и он немедленно изменится в приложении. Не только это, не добавляя никакого кода, просто имея SDK.

Изменения, которые я делаю (текст, цвет фона и т.д.), останутся для всех будущих сеансов приложения. Как это можно реализовать?

Я предполагаю, что они используют какое-то отражение, но как они могут заставить его работать для всех пользователей и для всех будущих сеансов? как код клиента находит нужный элемент пользовательского интерфейса? и как это работает с UITableViewCell?

3) Можно ли обнаруживать каждый раз, когда загружается UIViewController? т.е. получить обратный вызов для каждого viewDidLoad? если да, то как?

Смотрите скриншоты ниже:

enter image description here

enter image description here

4b9b3361

Ответ 1

Интересно то же самое и не мог найти определенного ответа, так что вот мое (надеюсь) образованное предположение:

Благодаря среде выполнения на самом деле не так сложно использовать Aspect-Orientated-Programming (AOP) в Cocoa (- Touch), в котором правила записываются, чтобы подключаться к вызовам методов других классов.

Если вы google для AOP и Objective-C, появятся несколько библиотек, которые красиво завершают код выполнения.

Например, steinpete Aspect библиотека:

[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;
}

который снова вызывает несколько других довольно пугающе выглядящих функций, которые делают тяжелый подъем во время выполнения

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));
    }
}

включая method-swizzling.


Легко видеть, что здесь у нас есть инструмент, который позволит нам отправить текущее состояние приложения для его повторной сборки на веб-странице, а также для управления объектами в существующем коде.
Конечно, это только отправная точка. Вам понадобится веб-сервис, который собирает приложение и отправляет его пользователям.


Лично я никогда не использовал AOP для такой сложной задачи, но я использовал ее для обучения всем возможностям отслеживания контроллеров просмотров

- (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];
}

Обновление

Я немного поиграл и смог создать прототип. Если он добавлен в проект, он изменит цвет фона всех контроллеров представления на синий, и через 5 секунд все элементы управления живыми экранами будут оранжевыми, используя AOP и добавление динамического метода.

исходный код: https://gist.github.com/vikingosegundo/0e4b30901b9498ae4b7b

5 секунд инициируются уведомлением, но очевидно, что это может быть сетевое событие.


update 2

Я научил своего прототипа открывать сетевой интерфейс и принимать значения rgb для фона.
Запуск в симуляторе был бы

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

enter image description here

Я использую 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, "[email protected]:@");
    class_addMethod([UIViewController class], NSSelectorFromString(@"ab_register_ab_notificaction"), (IMP)_ab_register_ab_notificaction, "[email protected]:");

    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

Ответ 2

Меня зовут Baraa, и я - специалист по разработке программного обеспечения, работающий над мобильной командой Optimizely, поэтому я могу рассказать о высоком уровне понимания того, как Optimizely SDK работает как на Android, так и на iOS.

В iOS Optimizely SDK использует метод swizzling. Это позволяет нам применять визуальные изменения к приложению на основе любых экспериментов, которые в настоящее время активны в нашем файле данных.

В Android Optimisely использует отражение для присоединения SDK в качестве слушателя для событий взаимодействия и жизненного цикла для применения визуальных изменений к приложению на основе любых экспериментов, которые активны в файле данных.

Полный список методов, которые мы просматриваем на iOS и слушателях, которые мы перехватываем на Android, см. в этой статье справки: https://help.optimizely.com/hc/en-us/articles/205014107-How-Optimizely-s-SDKs-Work-SDK-Order-of-execution-experiment-activation-and-goals#execute

Ответ 3

Компания Leanplum предлагает редактор визуальных интерфейсов для iOS и Android: это не требует кодирования, и Leanplum автоматически обнаружит элементы и позволит вам их изменить. Не требуется повторная отправка инженеров или приложений.

Относительно ваших вопросов:

  • При установке iOS или Android SDK в вашем приложении вы включаете функцию, называемую Visual Editor. В режиме разработки и открытии панели управления сайтом SDK отправляет информацию о иерархии представлений в реальном времени в ваш браузер. Иерархия представления сканируется аналогичным образом, когда DOM создается на обычном веб-сайте.
  • Вы можете выбрать любой элемент пользовательского интерфейса в своем приложении и изменить его внешний вид в режиме реального времени. Это работает путем определения точного элемента в дереве просмотра и отправки изменений в SDK.
  • Это может быть достигнуто путем добавления пользовательских крючков или техники под названием "swizzling". Взгляните на это сообщение в блоге, как оно работает.

Чтобы узнать больше о редакторе визуальных интерфейсов Leanplum, просмотрите leanplum.com. Они предлагают бесплатную 30-дневную пробную версию.

(Отказ от ответственности: я инженер в Leanplum.)