公元2016年底,2017年初,某作旅行產品的互聯網公司內,產品經理瘋狂的提 A/BTest 需求,以致於該司程序猿談AB色變,邪惡的產品經理令程序猿們聞風喪膽,苦不堪言...咳咳,扯遠了。git
近期團隊作了不少 AB Test 的業務需求,在這種需求日益見多的狀況下,咱們不得不提高咱們的代碼組織方式,以適應或更好的在此類需求上維護咱們的代碼。因此有了本文,本文主要闡述了業務團隊在作 AB Test 的一些想法和思路,才疏學淺,不靈賜教。程序員
既然產品經理在 A/B Test 胯下瘋狂的輸出,那咱們就要弄清楚,什麼是 A/BTest?爲什麼產品經理如此癡情於 A/B Test ?github
A/B Test 就是爲了同一個目標制定兩個方案(好比兩個website,app的頁面),讓一部分用戶使用 A 方案,另外一部分用戶使用 B 方案,記錄下用戶的使用狀況,看哪一個方案更接近測試想要的結果,並確信該結論在推廣到所有流量可信。web
請注意上述那段話中的黑體字,這將是 AB Test 的核心價值所在。設計模式
其實 A/B Test 就是咱們中學上化學實驗課時常作的對照試驗,把這種對照試驗搬到了互聯網上,經過改變單一變量的實驗組和原來的對照組作對比,經過數據指標對比,看哪一種方案可以提升用戶體驗(轉化率);緩存
灰度發佈,是指在黑與白之間,可以平滑過渡的一種發佈方式。A/B Test就是一種灰度發佈方式,讓一部分用戶繼續用A,一部分用戶開始用B,若是用戶對B沒有什麼反對意見,那麼逐步擴大範圍,把全部用戶都遷移到B上面來。灰度發佈能夠保證總體系統的穩定,在初始灰度的時候就能夠發現、調整問題,以保證其影響度。bash
可逆方案,有點相似於以前的灰度發佈,只不過不灰度的控制力更強,當咱們發佈後發現實驗組方案出現了嚴重的故障,或者對比數據量相差懸殊,那麼就徹底能夠全量切換回原來的對照組,保證了線上環境的穩定,不影響用戶的正常使用。架構
這點,對產品而言就是多了試錯的可能,想一想在以前App動態化匱乏的時代,App的發佈就是嫁出去的女兒潑出去的水,一去不復返,發佈了的產品用戶更新完就不可能在回退到上一個版本。從這一點開始,產品經理就大愛A/B Test !app
數據驅動,這一點我想相當重要,在目前這種以用戶數據爲商業土壤的大數據時代,一個產品是以數據驅動,將可以更加鏗鏘有力的支持這個產品的全線發佈,也是產品經理對新方案推動的重要王牌。以前要發佈一個新產品,要麼美其名曰參考競品(不反對抄襲,抄襲是遇上競爭對手最快的手段,可是並非超越的手段),要麼腦洞打開,認爲某種新的方案或交互體驗能帶來更多的轉化率。這種方法都是沒有數聽說明的,只能經過項目上線後進行後評估才能肯定是否如產品經理所願真正到達了目標。框架
經過A/B Test,能在不全量影響線上的正常運轉的狀況下,經過對照度和試驗組的數據對比,在短期能肯定哪一種方案的優越,從而讓產品的轉化率在短期能得可信性提高。這也正是產品經理說服老闆,並彰顯其能力價值的精華之處!so,大愛!
在作 AB Test 以前,有幾個問題是要問產品經理的:
這其實就是咱們上面那段話中加粗文字的重點,固然,有些問題是服務端須要關心的,好比問題3和4。
那麼客戶端開發須要關心哪些個問題呢?
第一個問題,目標是什麼?目的是什麼,這是咱們須要問的,對客戶端而言,A/B Test 就須要客戶端維護兩套一樣業務的代碼,這種工做量簡單理解就是以前的double,既然會致使工做量翻倍,那就要問清楚,此次作 A/B Test 的目的是什麼?評估一下真的值得這樣作嗎?雖然有時候胳膊擰不過大腿,但或許在你的分析下,某些需求是不須要作 A/B Test 的。例如:競品已經作了好久方案(你不要告訴我抄都沒自信),或者很明顯的UI改動是優於以前的方案的,等等。
第二個問題,A/B Test 版本是什麼?測試時間多長?其實這兩個問題,就是在確認這個 A/B Test 方案何時上線,何時下線。上下線的時間咱們要清楚,由於在這段時間內,咱們都須要去維護兩套代碼,並且在 App Size 這麼緊張,你們都在搞瘦身的大環境下,你的安裝包的過大或需就是用戶從一開始就不選擇大家產品的理由!A/B Test 方案,代碼有寫就有刪,什麼時候刪代碼取決於這個 A/B Test 方案什麼時候下線,刪完代碼後有多久的時間給 QA 測試工程師去測試,這都是要安排的。
對於某些開發天天都要聲嘶力竭的說5次以上:「這個(需求)是要算(研發)成本的呀。」這樣用力扣研發成本,儘可能把價值低收益低的需求砍下去,把收益不明確的需求排到後面去,至關於在輸出幾乎不變的基礎上,節約了2-3個開發工程師。這也是長期維持團隊的訣竅,從源頭上精簡,而不是苛求超人般的程序員。
如何衡量效果,就是來判斷這種需求是不是價值低收益低或不明確的項目,咱們都想作有價值的東西,而不是隨隨便便隨時準備砍掉的功能,但願產品經理敢想,並且加以思考!
好了,扯完了產品篇,我們進入正題。 既然本來一套代碼有了兩種邏輯,或者兩種UI樣式,就須要從本來的邏輯中拆出來,其必然結果是多了一個if判斷語句,那若是判斷的地方多了,咱還這樣if、if、if、if、i....就太失水準了,常言道:寫業務代碼,搬得一手好磚是程序員的基本要求。接下來說下小生的 A/B Test 方案探索歷程。
先來大概介紹本次探索的業務背景:
咱們就以 iOS 中典型的 UITabelView 中的 Delegate 和 DataSource 的協議函數分 A/B 方案來講;
剛剛說了,A 方案是一個全量方案,因此這裏的switch會有一個默認方案。可是這種寫法實在是太low了,每個調用函數中都去判斷一次A/B,影響效率暫且不提,維護起來也是坑坑坑,看見第二張圖的函數列表頁以爲頭大,並且也致使了Controller過於龐大,若是再有一個C方案豈不是要炸?因此這種方案不可取。
因爲Objective-C 的Runtime 動態特性,咱們能夠把方法選擇子緩存在一個字典中,在須要肯定 A/B 方案的調用處判斷一次,獲得對應方案的方法緩存字典,在調用的時候,只須要去對應的緩存字典中調用就能夠了,固然這裏須要擴展NSObject類中的- (id)performSelector:(SEL)aSelector withObject:(id)object;
使其支持多個參數的傳遞。
- (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];
//簽名中方法參數的個數,內部包含了self和_cmd,因此參數從第3個開始
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;
}
}
複製代碼
這種方案僅僅比上個方案提升了一點,就是咱們並無在每一個函數中判斷 A/B ,只判斷了一次。但仍然解決不了Controller過於龐大,沒法優雅的擴展的問題。並且還引入了新的問題,就是在進行Runtime消息轉發時的額外開銷,和performSelector
返回值須要轉一下類型的尷尬。
如圖所示,經過策略模式,把須要分 A/B 的方法抽象到一個協議中,而後抽象出一個策略父類去遵循這個協議,其兩個A/B子類也遵循這個協議,這樣在Controller只須要在判斷A/B策略的調用處初始化對應的策略類,經過父類指針去調用子類的協議方法,達到A/B函數的執行。這樣採用了面向對象的繼承和多態的機制,完成了一次完美的 A/B 函數執行,AB策略能夠自由切換,避免了使用多重條件判斷,同時知足了開閉原則,對擴展開放(增長新的策略類),對修改關閉。
協議分發能夠簡單理解爲將協議代理交給多個對象實現,相似於多播委託。
Protocol協議代理在開發中應用頻繁,開發者常常會遇到一個問題——事件的連續傳遞。好比,爲了隔離封裝,開發者可能常常會把tableview的delegate或者datesource抽離出獨立的對象,而其它對象(好比VC)須要獲取某些delegate事件時,只能經過事件的二次傳遞。有沒有更簡單的方法了?協議分發器正好能夠派上用場。
既然能實現多播委託消息分發,那麼消息分發時,指定的分發的接收者,不就是 A/B Test 的消息分爲A/B分發嗎?
先給各位看官呈上乾貨,LJFABTestProtocolDispatcher是一個協議分發器,經過該工具可以輕易實現將協議事件分發給多個實現者,而且能指定調用哪些實現者。好比最多見的UITableViewDelegate和UITableViewDataSource協議,經過LJFABTestProtocolDispatcher可以很是容易發分發給多個對象,並且能夠指定A/B方案執行,具體可參考Demo。
原理並不複雜, 協議分發器Dispatcher並不實現Protocol協議,其只需將對應的Protocol事件分發給不一樣的實現者Implemertor。如何實現分發?
NSObject對象主要經過如下函數響應未實現的Selector函數調用
+ (BOOL)resolveInstanceMethod:(SEL)sel;
+ (BOOL)resolveClassMethod:(SEL)sel;
複製代碼
//返回實現了方法的消息轉發對象
- (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0,9.0, 1.0);
複製代碼
//函數簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
//函數調用
- (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");
複製代碼
所以,協議分發器Dispatcher能夠在該函數中將Protocol中Selector的調用傳遞給實現者Implemertor,由實現者Implemertor實現具體的Selector函數便可,而現實指定的A/B調用,須要傳入全部實現者組織的下標,來指定調用
/**
協議分發器Dispatcher能夠在該函數中將Protocol中Selector的調用傳遞給實現者Implemertor,由實現者Implemertor實現具體的Selector函數便可
*/
- (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 *implemertorContext = [self.implemertors objectAtIndex:i];
if (i == self.indexImplemertor.integerValue && [implemertorContext.implemertor respondsToSelector:aSelector])
{
[anInvocation invokeWithTarget:implemertorContext.implemertor];
}
}
}
else
{
for (ImplemertorContext *implemertorContext in self.implemertors)
{
if ([implemertorContext.implemertor respondsToSelector:aSelector])
{
[anInvocation invokeWithTarget:implemertorContext.implemertor];
}
}
}
}
複製代碼
如何作到只對Protocol中Selector函數的調用作分發是設計的關鍵,系統提供有函數
objc_method_description protocol_getMethodDescription(Protocol *p, SEL aSel, BOOL isRequiredMethod, BOOL isInstanceMethod)
複製代碼
經過如下方法便可判斷Selector是否屬於某一Protocol
struct 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;
}
複製代碼
還有一點,協議分發器並非一個單例,而是一個局部變量,那如何來防止一個局部變量延遲釋放呢?這裏使用了「自釋放」的一種思想,看源碼:
- (instancetype)initWithProtocol:(Protocol *)protocol
withIndexImplemertor:(NSNumber *)indexImplemertor
toImplemertors:(NSArray *)implemertors
{
if (self = [super init])
{
self.prococol = protocol;
self.indexImplemertor = indexImplemertor;
NSMutableArray *implemertorContexts = [NSMutableArray arrayWithCapacity:implemertors.count];
[implemertors enumerateObjectsUsingBlock:^(id implemertor, NSUInteger idx, BOOL * _Nonnull stop){
ImplemertorContext *implemertorContext = [ImplemertorContext new];
implemertorContext.implemertor = implemertor;
[implemertorContexts addObject:implemertorContext];
// 爲何關聯個 ProtocolDispatcher 屬性?
// "自釋放",ProtocolDispatcher 並非一個單例,而是一個局部變量,當implemertor釋放時就會觸發ProtocolDispatcher釋放。
// key 須要爲隨機,不然當有兩個分發器是,key 會被覆蓋,致使第一個分發器釋放。因此 key = _cmd 是不行的。
void *key = (__bridge void *)([NSString stringWithFormat:@"%p",self]);
objc_setAssociatedObject(implemertor, key, self, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}];
self.implemertors = implemertorContexts;
}
return self;
}
複製代碼
協議分發器使用須要瞭解如何處理帶有返回值的函數 ,好比
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
複製代碼
咱們知道,iOS中,函數執行返回的結果存在於寄存器R0中,後執行的會覆蓋先執行的結果。所以,當遇到有返回結果的函數時,返回結果之後執行的函數返回結果爲最終值。
Protocol協議分發器,本人並非獨創,也是看了這篇文章Protocol協議分發器獲得運用於 A/B Test 的靈感,在這裏感謝做者和開源社區。
隨着A/B Test 的代碼愈來愈多,業務模塊內的 A/B Test 組件化,無非是爲了更方便的上下業務的 A/B Test 代碼,提升工做效率,讓寫代碼和刪代碼變成一件快樂的事情。
關於 iOS 組件化,網上也有不少文章,這裏就不炒冷飯了,你們能夠搜索一下關於組件化的一些定義和經驗。
在整個客戶端已經被組件化的今天,不是架構組的業務程序員可不能夠嘗試來解決一下業務模塊內的 A/B Test 組件化呢?iOS 組件化大部分都是圍繞 Cocoapods 來展開的,因此在基於 Cocoapods iOS 高度組件化的的框架下, 咱們先來問幾個技術問題。
這個問題主要是基於目前整個客戶端架構,各個業務線向殼工程提供了本身的靜態庫, 咱們大部分時間(打包時)都會合並不一樣架構的相同靜態庫,相同架構的不一樣靜態庫是否可合併?
答案是,能夠的。
在合併不一樣架構的相同靜態庫時,用到如下命令:
lipo -info libname.a(或者libname.framework/libname)
複製代碼
lipo -create 靜態庫存放路徑1 靜態庫存放路徑2 ... -output 整合後存放的路徑
複製代碼
lipo 靜態庫源文件路徑 -thin CPU架構名稱 -output 拆分後文件存放路徑
複製代碼
那麼合併相同架構的不一樣靜態庫是怎麼作的?
靜態庫文件也稱爲「文檔文件」,它是一些.o文件的集合。在Linux(Unix)中使用工具「ar」對它進行維護管理。它所包含的成員(member)就是若干.o文件。除了.o文件,還有一個一個特殊的成員,它的名字是__.SYMDEF
。它包含了靜態庫中全部成員所定義的有效符號(函數名、變量名)。所以,當爲庫增長了一個成員時,相應的就須要更新成員__.SYMDEF
,不然所增長的成員中定義的全部的符號將沒法被鏈接程序定位。完成更新的命令是:
ranlib libname.a
複製代碼
舉個例子: 咱們有倆個靜態庫libFlight.a
和libHotel.a
,合併成一個libFlight_Hotel.a
。
取出相同架構下的Lib.a。 首先查看靜態庫Flight.a
的架構:
lipo -info Flight.a
複製代碼
能夠看到:
input file /Users/f.li/Desktop/相同架構的不一樣靜態庫合併/libFlight.a is not a fat file
Non-fat file: /Users/f.li/Desktop/相同架構的不一樣靜態庫合併/libFlight.a is architecture: x86_64
複製代碼
libFlight.a is not a fat file 和 libFlight.a is architecture: x86_64
fat file 那麼表明這個包是支持多平臺的,not a fat file 就是不支持多平臺的,架構是x86_64。
固然,若是是 fat file ,咱們就須要取出相同平臺架構的庫。
lipo libFlight.a -thin x86_64 -output libFlight.a
複製代碼
這樣,就會取出 x86_64 架構下的libFlight.a
。
查看庫中所包含的文件列表。
ar -t /Users/f.li/Desktop/相同架構的不一樣靜態庫合併/libFlight.a
__.SYMDEF SORTED
Flight.o
複製代碼
看到libFlight.a
有兩個文件,__.SYMDEF SORTED
和Flight.o
解壓出object file(即.o後綴文件)。
~libFlight_o ar xv /Users/f.li/Desktop/libFlight.a
x - __.SYMDEF SORTED
x - Flight.o
複製代碼
這樣,在libFlight_o
文件夾內,就有了__.SYMDEF SORTED
和Flight.o
這個兩個文件。 一樣,在libHotel_o
文件夾內得到__.SYMDEF SORTED
和Hotel.o
合併,從新打包。
把__.SYMDEF SORTED
和Flight.o
,還有Hotel.o
移動到libFlight_Hotel_o
文件夾內。把從新打包object file;
ar rcs libFlight_Hotel.a /Users/f.li/Desktop/libFlight_Hotel_o/*o
複製代碼
這樣就獲得了libFlight_Hotel.a
。
更新__.SYMDEF
文件。 其實,咱們是把Hotel.o
加入了LibFlight.a中,最後,須要更新__.SYMDEF
文件。
ranlib libFlight_Hotel.a
複製代碼
若是包含頭文件,那麼把頭文件也放到一個文件內在使用libFlight_Hotel.a的工程中引入就能夠了。
可是顯然這樣作太麻煩。
Xcode 子工程,實際上是幫助咱們在一個工程內配合git submodule 來進行分模塊開發。 整理下思路。
$(SRCROOT)/Flight_SubProject/Flight_SubProject
,其中$(SRCROOT)宏表明你的工程文件目錄。這樣其實回到了以前架構的一個狀態,沒法調用解耦,相互依賴嚴重。
答案是,subspec 不是獨立的代碼庫,只是編譯時候分開進行,最後會和pod造成一個產物。
爲何會問 Cocoapods subspecs?由於在基於Cocoapods架構組件化後,業務對外部提供的是靜態庫類型的pod。
源碼類型是subspec,在引入pod時,能夠選擇引入subspec目錄,也能夠設置podspec的默認subsepc,subspect之間也能夠有依賴關係。
業務線內部拆分能夠作成多個 pod,最後提供一個 pod 依賴全部業務內部的組件 pod,這樣不影響外部架構打包,業務線也能夠靈活修改。
最後這個依賴全部業務內部組件的pod對外提供的也是一個靜態庫,業務內部的組件pod不須要提供靜態庫,可是也會有獨立的Git。
固然這種業務內部的 A/B Test 組件化方案目前處於探索階段,由於目前咱們的 A/B Test 的代碼量並無達到須要咱們進行拆分的地步,全部這階段尚處於技術拓展調(yi)研(yin)階段。
關於 iOS A/B Test 的探索目前小生就這麼多,A/B Test 對於產品而言確實是一種比較好的方案,尤爲是可逆性和數據驅動,固然小生是站在開發的角度上來看待 A/B Test。既然是對產品有利的方案,咱們的代碼就應該時代潮流,畢竟技術是爲業務服務的。
前段時間在看 sunny 直播時,談到了 iOS 開發的進階速度
純平常開發 < 純看書、博客 < 本身試驗、Demo < 寫博客 < 系統性分享和討論 < 提供完整的開源方案
以前本身的進階速度僅僅到寫博客的分段,最近這半年在團隊中發起了技術分享了和團隊博客的浪潮,但願可以向系統性分享、討論和完整的開源方案這兩個高分段衝分,本次結合最近的業務和自身的一些想法和實踐,完成了一次衝分嘗試,但願在衝分的路上越戰越勇!