Mobile Development 22 min read

Exploring iOS A/B Testing Strategies and Implementation Techniques

This article examines the concept, benefits, and practical implementation of A/B testing in iOS development, covering design considerations, code organization patterns such as selector caching, strategy pattern, and protocol dispatchers, as well as related build‑time concerns like static‑library merging and CocoaPods subspecs.

Qunar Tech Salon
Qunar Tech Salon
Qunar Tech Salon
Exploring iOS A/B Testing Strategies and Implementation Techniques

Introduction

In late 2016 and early 2017, a travel‑product company faced a flood of A/B test requests from product managers, causing developers great anxiety. To cope with the growing number of A/B test requirements, the team needed a better code‑organization approach, which this article describes.

1. What Is A/B Testing?

A/B testing creates two variants (A and B) of the same feature, directs a portion of users to each variant, records their behavior, and determines which variant better meets the target metric. It is essentially an online version of a controlled experiment.

1.2 Benefits for Products

Gray‑release: Gradually roll out a new variant while keeping the old one available, allowing issues to be detected early.

Reversible plan: If the experiment causes problems, the system can instantly revert to the original version.

Data‑driven decisions: Enables product managers to justify changes with quantitative evidence rather than intuition.

2. What Developers Should Ask

Before starting an A/B test, developers should clarify six questions with the product manager:

What is the goal?

What are the A and B versions?

What is the required sample size?

How will users be split?

How long will the test run?

How will effectiveness be measured?

Questions 3 and 4 often affect server‑side implementation, while the client side must consider code duplication, bundle size, and maintenance overhead.

3. iOS A/B Test Implementation Exploration

3.1 Basic Function‑Level A/B

Using simple if statements for each method quickly becomes unmanageable as the number of variants grows.

3.2 Selector‑Caching Approach

Leveraging Objective‑C runtime, a method selector can be cached in a dictionary so that the A/B decision is made once and reused. This requires extending - (id)performSelector:(SEL)aSelector withObject:(id)object; to support multiple arguments:

- (id)fperformSelector:(SEL)selector withObjects:(NSArray *)objects {
    NSMethodSignature *methodSignature = [[self class] instanceMethodSignatureForSelector:selector];
    if (methodSignature == nil) {
        @throw [NSException exceptionWithName:@"抛异常错误" reason:@"没有这个方法,或者方法名字错误" userInfo:nil];
        return nil;
    } else {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
        [invocation setTarget:self];
        [invocation setSelector:selector];
        NSInteger signatureParamCount = methodSignature.numberOfArguments - 2;
        NSInteger requireParamCount = objects.count;
        NSInteger resultParamCount = MIN(signatureParamCount, requireParamCount);
        for (NSInteger i = 0; i < resultParamCount; i++) {
            id obj = objects[i];
            [invocation setArgument:&obj atIndex:i+2];
        }
        [invocation invoke];
        id callBackObject = nil;
        if (methodSignature.methodReturnLength) {
            [invocation getReturnValue:&callBackObject];
        }
        return callBackObject;
    }
}

Although this reduces the number of condition checks, it still leaves large view controllers and introduces runtime overhead.

3.3 Strategy Pattern

By defining a protocol for the variant‑specific behavior and implementing two concrete strategy classes (A and B), the controller can hold a reference to the protocol type and invoke the appropriate implementation without conditional logic, satisfying the open‑closed principle.

3.4 Protocol Dispatcher for A/B

A protocol dispatcher forwards protocol method calls to multiple implementors, allowing selective invocation of A or B implementations. The core idea is to intercept forwardInvocation: and route the selector to the chosen implementor.

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL aSelector = anInvocation.selector;
    if (!ProtocolContainSel(self.prococol, aSelector)) {
        [super forwardInvocation:anInvocation];
        return;
    }
    if (self.indexImplemertor) {
        for (NSInteger i = 0; i < self.implemertors.count; i++) {
            ImplemertorContext *ctx = self.implemertors[i];
            if (i == self.indexImplemertor.integerValue && [ctx.implemertor respondsToSelector:aSelector]) {
                [anInvocation invokeWithTarget:ctx.implemertor];
            }
        }
    } else {
        for (ImplemertorContext *ctx in self.implemertors) {
            if ([ctx.implemertor respondsToSelector:aSelector]) {
                [anInvocation invokeWithTarget:ctx.implemertor];
            }
        }
    }
}

Key runtime functions used to detect whether a selector belongs to a protocol:

objc_method_description MethodDescriptionForSELInProtocol(Protocol *protocol, SEL sel) {
    struct objc_method_description description = protocol_getMethodDescription(protocol, sel, YES, YES);
    if (description.types) return description;
    description = protocol_getMethodDescription(protocol, sel, NO, YES);
    if (description.types) return description;
    return (struct objc_method_description){NULL, NULL};
}
BOOL ProtocolContainSel(Protocol *protocol, SEL sel) {
    return MethodDescriptionForSELInProtocol(protocol, sel).types ? YES : NO;
}

Dispatcher instances are not singletons; they are created locally and attached to implementors via associated objects to ensure proper lifecycle.

- (instancetype)initWithProtocol:(Protocol *)protocol
               withIndexImplemertor:(NSNumber *)indexImplemertor
                     toImplemertors:(NSArray *)implemertors {
    if (self = [super init]) {
        self.prococol = protocol;
        self.indexImplemertor = indexImplemertor;
        NSMutableArray *contexts = [NSMutableArray arrayWithCapacity:implemertors.count];
        [implemertors enumerateObjectsUsingBlock:^(id impl, NSUInteger idx, BOOL *stop) {
            ImplemertorContext *ctx = [ImplemertorContext new];
            ctx.implemertor = impl;
            void *key = (__bridge void *)([NSString stringWithFormat:@"%p", self]);
            objc_setAssociatedObject(impl, key, self, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            [contexts addObject:ctx];
        }];
        self.implemertors = contexts;
    }
    return self;
}

3.5 Handling Return Values

When forwarding methods that return a value (e.g., - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section ), the last invoked implementation’s return value will overwrite previous ones because the result resides in register R0.

4. Build‑Time Concerns: Static Library Merging and CocoaPods

To reduce bundle size and simplify dependency management, the team explored merging static libraries of the same architecture using lipo and ar tools.

lipo -info libFlight.a
lipo -create libFlight.a libHotel.a -output libFlight_Hotel.a
ar -t libFlight.a
ar xv libFlight.a
ar rcs libFlight_Hotel.a *.o
ranlib libFlight_Hotel.a

CocoaPods subspecs were also discussed as a way to modularize components without creating separate Git repositories; subspecs are compiled separately but linked into a single pod.

Conclusion

A/B testing is a valuable, data‑driven technique that benefits both product and engineering teams. By adopting clean code‑organization patterns—selector caching, strategy pattern, and protocol dispatchers—developers can keep the codebase maintainable while supporting reversible, gray‑release experiments. The article also highlights practical tooling for static‑library handling and modularization in iOS projects.

design patternsmobile developmentiOSA/B testingObjective-Cstatic libraryProtocol Dispatcher
Qunar Tech Salon
Written by

Qunar Tech Salon

Qunar Tech Salon is a learning and exchange platform for Qunar engineers and industry peers. We share cutting-edge technology trends and topics, providing a free platform for mid-to-senior technical professionals to exchange and learn.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.