引子
公元2016年末侧甫,2017年初,某做旅行產(chǎn)品的互聯(lián)網(wǎng)公司內(nèi),產(chǎn)品經(jīng)理瘋狂的提 A/BTest 需求披粟,以至于該司程序猿談AB色變咒锻,邪惡的產(chǎn)品經(jīng)理令程序猿們聞風(fēng)喪膽,苦不堪言...咳咳守屉,扯遠(yuǎn)了惑艇。
近期團(tuán)隊(duì)做了很多 AB Test 的業(yè)務(wù)需求,在這種需求日益見(jiàn)多的情況下拇泛,我們不得不提升我們的代碼組織方式滨巴,以適應(yīng)或更好的在此類(lèi)需求上維護(hù)我們的代碼。所以有了本文俺叭,本文主要闡述了業(yè)務(wù)團(tuán)隊(duì)在做 AB Test 的一些想法和思路恭取,才疏學(xué)淺,不靈賜教绪颖。
A/B Test
A/B Test 是什么秽荤?
既然產(chǎn)品經(jīng)理在 A/B Test 胯下瘋狂的輸出,那我們就要弄清楚柠横,什么是 A/BTest窃款?為何產(chǎn)品經(jīng)理如此癡情于 A/B Test ?
A/B Test 就是為了同一個(gè)目標(biāo)制定兩個(gè)方案(比如兩個(gè)website牍氛,app的頁(yè)面)晨继,讓一部分用戶(hù)使用 A 方案,另一部分用戶(hù)使用 B 方案搬俊,記錄下用戶(hù)的使用情況紊扬,看哪個(gè)方案更接近測(cè)試想要的結(jié)果,并確信該結(jié)論在推廣到全部流量可信唉擂。
請(qǐng)注意上述那段話(huà)中的黑體字餐屎,這將是 AB Test 的核心價(jià)值所在。
其實(shí) A/B Test 就是我們中學(xué)上化學(xué)實(shí)驗(yàn)課時(shí)常做的對(duì)照試驗(yàn)玩祟,把這種對(duì)照試驗(yàn)搬到了互聯(lián)網(wǎng)上腹缩,通過(guò)改變單一變量的實(shí)驗(yàn)組和原來(lái)的對(duì)照組做對(duì)比,通過(guò)數(shù)據(jù)指標(biāo)對(duì)比空扎,看哪種方案能夠提高用戶(hù)體驗(yàn)(轉(zhuǎn)化率)藏鹊;
AB Test 的優(yōu)點(diǎn)有哪些(對(duì)產(chǎn)品而言)?
優(yōu)點(diǎn)1. 灰度發(fā)布
灰度發(fā)布转锈,是指在黑與白之間盘寡,能夠平滑過(guò)渡的一種發(fā)布方式。A/B Test就是一種灰度發(fā)布方式撮慨,讓一部分用戶(hù)繼續(xù)用A竿痰,一部分用戶(hù)開(kāi)始用B脆粥,如果用戶(hù)對(duì)B沒(méi)有什么反對(duì)意見(jiàn),那么逐步擴(kuò)大范圍影涉,把所有用戶(hù)都遷移到B上面來(lái)冠绢。灰度發(fā)布可以保證整體系統(tǒng)的穩(wěn)定常潮,在初始灰度的時(shí)候就可以發(fā)現(xiàn)弟胀、調(diào)整問(wèn)題,以保證其影響度喊式。
優(yōu)點(diǎn)2. 可逆方案
可逆方案孵户,有點(diǎn)類(lèi)似于之前的灰度發(fā)布,只不過(guò)不灰度的控制力更強(qiáng)岔留,當(dāng)我們發(fā)布后發(fā)現(xiàn)實(shí)驗(yàn)組方案出現(xiàn)了嚴(yán)重的故障夏哭,或者對(duì)比數(shù)據(jù)量相差懸殊,那么就完全可以全量切換回原來(lái)的對(duì)照組献联,保證了線(xiàn)上環(huán)境的穩(wěn)定竖配,不影響用戶(hù)的正常使用。
這點(diǎn)里逆,對(duì)產(chǎn)品而言就是多了試錯(cuò)的可能进胯,想想在之前App動(dòng)態(tài)化匱乏的時(shí)代,App的發(fā)布就是嫁出去的女兒潑出去的水原押,一去不復(fù)返胁镐,發(fā)布了的產(chǎn)品用戶(hù)更新完就不可能在回退到上一個(gè)版本。從這一點(diǎn)開(kāi)始诸衔,產(chǎn)品經(jīng)理就大愛(ài)A/B Test !
優(yōu)點(diǎn)3. 數(shù)據(jù)驅(qū)動(dòng)
數(shù)據(jù)驅(qū)動(dòng)盯漂,這一點(diǎn)我想至關(guān)重要,在目前這種以用戶(hù)數(shù)據(jù)為商業(yè)土壤的大數(shù)據(jù)時(shí)代笨农,一個(gè)產(chǎn)品是以數(shù)據(jù)驅(qū)動(dòng)就缆,將能夠更加鏗鏘有力的支持這個(gè)產(chǎn)品的全線(xiàn)發(fā)布,也是產(chǎn)品經(jīng)理對(duì)新方案推進(jìn)的重要王牌谒亦。之前要發(fā)布一個(gè)新產(chǎn)品竭宰,要么美其名曰參考競(jìng)品(不反對(duì)抄襲,抄襲是趕上競(jìng)爭(zhēng)對(duì)手最快的手段诊霹,但是并不是超越的手段)羞延,要么腦洞打開(kāi)渣淳,認(rèn)為某種新的方案或交互體驗(yàn)?zāi)軒?lái)更多的轉(zhuǎn)化率脾还。這種方法都是沒(méi)有數(shù)據(jù)說(shuō)明的,只能通過(guò)項(xiàng)目上線(xiàn)后進(jìn)行后評(píng)估才能確定是否如產(chǎn)品經(jīng)理所愿真正到達(dá)了目標(biāo)入愧。
通過(guò)A/B Test鄙漏,能在不全量影響線(xiàn)上的正常運(yùn)轉(zhuǎn)的情況下嗤谚,通過(guò)對(duì)照度和試驗(yàn)組的數(shù)據(jù)對(duì)比,在短時(shí)間能確定哪種方案的優(yōu)越怔蚌,從而讓產(chǎn)品的轉(zhuǎn)化率在短時(shí)間能得可信性提升巩步。這也正是產(chǎn)品經(jīng)理說(shuō)服老板,并彰顯其能力價(jià)值的精華之處桦踊!so椅野,大愛(ài)!
開(kāi)發(fā)工程師需要關(guān)注的事情
六問(wèn)產(chǎn)品經(jīng)理
在做 AB Test 之前籍胯,有幾個(gè)問(wèn)題是要問(wèn)產(chǎn)品經(jīng)理的:
- 目標(biāo)是什么竟闪?
- AB版本是什么?
- 樣本量有多大?
- 用戶(hù)如何分流杖狼?
- 測(cè)試時(shí)間多長(zhǎng)炼蛤?
- 如何衡量效果?
這其實(shí)就是我們上面那段話(huà)中加粗文字的重點(diǎn)蝶涩,當(dāng)然理朋,有些問(wèn)題是服務(wù)端需要關(guān)心的,比如問(wèn)題3和4绿聘。
那么客戶(hù)端開(kāi)發(fā)需要關(guān)心哪些個(gè)問(wèn)題呢嗽上?
目標(biāo)是什么?
第一個(gè)問(wèn)題熄攘,目標(biāo)是什么炸裆?目的是什么,這是我們需要問(wèn)的鲜屏,對(duì)客戶(hù)端而言烹看,A/B Test 就需要客戶(hù)端維護(hù)兩套同樣業(yè)務(wù)的代碼,這種工作量簡(jiǎn)單理解就是之前的double洛史,既然會(huì)導(dǎo)致工作量翻倍惯殊,那就要問(wèn)清楚,這次做 A/B Test 的目的是什么也殖?評(píng)估一下真的值得這樣做嗎土思?雖然有時(shí)候胳膊擰不過(guò)大腿,但或許在你的分析下忆嗜,某些需求是不需要做 A/B Test 的己儒。例如:競(jìng)品已經(jīng)做了很久方案(你不要告訴我抄都沒(méi)自信),或者很明顯的UI改動(dòng)是優(yōu)于之前的方案的捆毫,等等闪湾。
A/B Test 版本是什么?測(cè)試時(shí)間多長(zhǎng)绩卤?
第二個(gè)問(wèn)題途样,A/B Test 版本是什么江醇?測(cè)試時(shí)間多長(zhǎng)?其實(shí)這兩個(gè)問(wèn)題何暇,就是在確認(rèn)這個(gè) A/B Test 方案什么時(shí)候上線(xiàn)陶夜,什么時(shí)候下線(xiàn)。上下線(xiàn)的時(shí)間我們要清楚裆站,因?yàn)樵谶@段時(shí)間內(nèi)条辟,我們都需要去維護(hù)兩套代碼,而且在 App Size 這么緊張宏胯,大家都在搞瘦身的大環(huán)境下捂贿,你的安裝包的過(guò)大或需就是用戶(hù)從一開(kāi)始就不選擇你們產(chǎn)品的理由!A/B Test 方案胳嘲,代碼有寫(xiě)就有刪厂僧,何時(shí)刪代碼取決于這個(gè) A/B Test 方案何時(shí)下線(xiàn),刪完代碼后有多久的時(shí)間給 QA 測(cè)試工程師去測(cè)試了牛,這都是要安排的颜屠。
如何衡量效果?
對(duì)于某些開(kāi)發(fā)每天都要聲嘶力竭的說(shuō)5次以上:“這個(gè)(需求)是要算(研發(fā))成本的呀鹰祸「撸”這樣用力扣研發(fā)成本,盡量把價(jià)值低收益低的需求砍下去蛙婴,把收益不明確的需求排到后面去粗井,相當(dāng)于在輸出幾乎不變的基礎(chǔ)上,節(jié)約了2-3個(gè)開(kāi)發(fā)工程師街图。這也是長(zhǎng)期維持團(tuán)隊(duì)的訣竅浇衬,從源頭上精簡(jiǎn),而不是苛求超人般的程序員餐济。
如何衡量效果耘擂,就是來(lái)判斷這種需求是否是價(jià)值低收益低或不明確的項(xiàng)目,我們都想做有價(jià)值的東西絮姆,而不是隨隨便便隨時(shí)準(zhǔn)備砍掉的功能醉冤,希望產(chǎn)品經(jīng)理敢想,而且加以思考篙悯!
iOS A/B Test 方案探索
好了蚁阳,扯完了產(chǎn)品篇,咱們進(jìn)入正題鸽照。
既然原本一套代碼有了兩種邏輯螺捐,或者兩種UI樣式,就需要從原本的邏輯中拆出來(lái),其必然結(jié)果是多了一個(gè)if判斷語(yǔ)句归粉,那如果判斷的地方多了,咱還這樣if漏峰、if糠悼、if、if浅乔、i....就太失水準(zhǔn)了倔喂,常言道:寫(xiě)業(yè)務(wù)代碼,搬得一手好磚是程序員的基本要求靖苇。接下來(lái)講下小生的 A/B Test 方案探索歷程席噩。
方案探索歷程
先來(lái)大概介紹本次探索的業(yè)務(wù)背景:
A/B Test 方案背景介紹
- A 方案 線(xiàn)上方案,全量贤壁;
- B 方案悼枢,適用于 A 中的一種情況,是 A 方案的子集脾拆;
- 非標(biāo)準(zhǔn) A/B Test馒索,只是過(guò)渡,因?yàn)?A 方案為全量方案名船,無(wú)法被下掉绰上,B方案為部分A中的;
我們就以 iOS 中典型的 UITabelView 中的 Delegate 和 DataSource 的協(xié)議函數(shù)分 A/B 方案來(lái)說(shuō)渠驼;
最基本的函數(shù) A/B
剛剛說(shuō)了蜈块,A 方案是一個(gè)全量方案,所以這里的switch會(huì)有一個(gè)默認(rèn)方案迷扇。但是這種寫(xiě)法實(shí)在是太low了百揭,每一個(gè)調(diào)用函數(shù)中都去判斷一次A/B,影響效率暫且不提蜓席,維護(hù)起來(lái)也是坑坑坑信峻,看見(jiàn)第二張圖的函數(shù)列表頁(yè)覺(jué)得頭大,而且也導(dǎo)致了Controller過(guò)于龐大瓮床,如果再有一個(gè)C方案豈不是要炸盹舞?所以這種方案不可取。
方法選擇子 + 字典隘庄,緩存式 A/B
由于Objective-C 的Runtime 動(dòng)態(tài)特性踢步,我們可以把方法選擇子緩存在一個(gè)字典中,在需要確定 A/B 方案的調(diào)用處判斷一次丑掺,得到對(duì)應(yīng)方案的方法緩存字典获印,在調(diào)用的時(shí)候,只需要去對(duì)應(yīng)的緩存字典中調(diào)用就可以了街州,當(dāng)然這里需要擴(kuò)展NSObject類(lèi)中的- (id)performSelector:(SEL)aSelector withObject:(id)object;
使其支持多個(gè)參數(shù)的傳遞兼丰。
- (id)fperformSelector:(SEL)selector withObjects:(NSArray *)objects
{
NSMethodSignature *methodSignature = [[self class] instanceMethodSignatureForSelector:selector];
if(methodSignature == nil)
{
@throw [NSException exceptionWithName:@"拋異常錯(cuò)誤" reason:@"沒(méi)有這個(gè)方法玻孟,或者方法名字錯(cuò)誤" userInfo:nil];
return nil;
}
else
{
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
[invocation setTarget:self];
[invocation setSelector:selector];
//簽名中方法參數(shù)的個(gè)數(shù),內(nèi)部包含了self和_cmd鳍征,所以參數(shù)從第3個(gè)開(kāi)始
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;
}
}
這種方案僅僅比上個(gè)方案提高了一點(diǎn)黍翎,就是我們并沒(méi)有在每個(gè)函數(shù)中判斷 A/B ,只判斷了一次艳丛。但仍然解決不了Controller過(guò)于龐大匣掸,無(wú)法優(yōu)雅的擴(kuò)展的問(wèn)題。而且還引入了新的問(wèn)題氮双,就是在進(jìn)行Runtime消息轉(zhuǎn)發(fā)時(shí)的額外開(kāi)銷(xiāo)碰酝,和performSelector
返回值需要轉(zhuǎn)一下類(lèi)型的尷尬。
設(shè)計(jì)模式之策略模式
如圖所示戴差,通過(guò)策略模式送爸,把需要分 A/B 的方法抽象到一個(gè)協(xié)議中,然后抽象出一個(gè)策略父類(lèi)去遵循這個(gè)協(xié)議暖释,其兩個(gè)A/B子類(lèi)也遵循這個(gè)協(xié)議碱璃,這樣在Controller只需要在判斷A/B策略的調(diào)用處初始化對(duì)應(yīng)的策略類(lèi),通過(guò)父類(lèi)指針去調(diào)用子類(lèi)的協(xié)議方法饭入,達(dá)到A/B函數(shù)的執(zhí)行嵌器。這樣采用了面向?qū)ο蟮睦^承和多態(tài)的機(jī)制,完成了一次完美的 A/B 函數(shù)執(zhí)行谐丢,AB策略可以自由切換爽航,避免了使用多重條件判斷,同時(shí)滿(mǎn)足了開(kāi)閉原則乾忱,對(duì)擴(kuò)展開(kāi)放(增加新的策略類(lèi))讥珍,對(duì)修改關(guān)閉。
Protocol協(xié)議分發(fā)器窄瘟,運(yùn)用于 A/B Test 方案
協(xié)議分發(fā)可以簡(jiǎn)單理解為將協(xié)議代理交給多個(gè)對(duì)象實(shí)現(xiàn)衷佃,類(lèi)似于多播委托。
Protocol協(xié)議代理在開(kāi)發(fā)中應(yīng)用頻繁蹄葱,開(kāi)發(fā)者經(jīng)常會(huì)遇到一個(gè)問(wèn)題——事件的連續(xù)傳遞氏义。比如,為了隔離封裝图云,開(kāi)發(fā)者可能經(jīng)常會(huì)把tableview的delegate或者datesource抽離出獨(dú)立的對(duì)象惯悠,而其它對(duì)象(比如VC)需要獲取某些delegate事件時(shí),只能通過(guò)事件的二次傳遞竣况。有沒(méi)有更簡(jiǎn)單的方法了克婶?協(xié)議分發(fā)器正好可以派上用場(chǎng)。
既然能實(shí)現(xiàn)多播委托消息分發(fā),那么消息分發(fā)時(shí)情萤,指定的分發(fā)的接收者鸭蛙,不就是 A/B Test 的消息分為A/B分發(fā)嗎?
先給各位看官呈上干貨,LJFABTestProtocolDispatcher是一個(gè)協(xié)議分發(fā)器筋岛,通過(guò)該工具能夠輕易實(shí)現(xiàn)將協(xié)議事件分發(fā)給多個(gè)實(shí)現(xiàn)者娶视,并且能指定調(diào)用哪些實(shí)現(xiàn)者。比如最常見(jiàn)的UITableViewDelegate和UITableViewDataSource協(xié)議泉蝌,通過(guò)LJFABTestProtocolDispatcher能夠非常容易發(fā)分發(fā)給多個(gè)對(duì)象搂妻,而且可以指定A/B方案執(zhí)行汹胃,具體可參考Demo。
原理解析
原理并不復(fù)雜拱撵, 協(xié)議分發(fā)器Dispatcher并不實(shí)現(xiàn)Protocol協(xié)議硫兰,其只需將對(duì)應(yīng)的Protocol事件分發(fā)給不同的實(shí)現(xiàn)者Implemertor诅愚。如何實(shí)現(xiàn)分發(fā)?
NSObject對(duì)象主要通過(guò)以下函數(shù)響應(yīng)未實(shí)現(xiàn)的Selector函數(shù)調(diào)用
- 方案一:動(dòng)態(tài)解析
+ (BOOL)resolveInstanceMethod:(SEL)sel; + (BOOL)resolveClassMethod:(SEL)sel;
- 方案二:快速轉(zhuǎn)發(fā)
//返回實(shí)現(xiàn)了方法的消息轉(zhuǎn)發(fā)對(duì)象 - (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0,9.0, 1.0);
- 方案三:慢速轉(zhuǎn)發(fā)
//函數(shù)簽名 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector //函數(shù)調(diào)用 - (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");
因此劫映,協(xié)議分發(fā)器Dispatcher可以在該函數(shù)中將Protocol中Selector的調(diào)用傳遞給實(shí)現(xiàn)者Implemertor违孝,由實(shí)現(xiàn)者Implemertor實(shí)現(xiàn)具體的Selector函數(shù)即可,而現(xiàn)實(shí)指定的A/B調(diào)用泳赋,需要傳入所有實(shí)現(xiàn)者組織的下標(biāo)雌桑,來(lái)指定調(diào)用
/**
協(xié)議分發(fā)器Dispatcher可以在該函數(shù)中將Protocol中Selector的調(diào)用傳遞給實(shí)現(xiàn)者Implemertor,由實(shí)現(xiàn)者Implemertor實(shí)現(xiàn)具體的Selector函數(shù)即可
*/
- (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];
}
}
}
}
設(shè)計(jì)關(guān)鍵
如何做到只對(duì)Protocol中Selector函數(shù)的調(diào)用做分發(fā)是設(shè)計(jì)的關(guān)鍵祖今,系統(tǒng)提供有函數(shù)
objc_method_description protocol_getMethodDescription(Protocol *p, SEL aSel, BOOL isRequiredMethod, BOOL isInstanceMethod)
通過(guò)以下方法即可判斷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;
}
還有一點(diǎn)校坑,協(xié)議分發(fā)器并不是一個(gè)單例,而是一個(gè)局部變量千诬,那如何來(lái)防止一個(gè)局部變量延遲釋放呢耍目?這里使用了“自釋放”的一種思想,看源碼:
- (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];
// 為什么關(guān)聯(lián)個(gè) ProtocolDispatcher 屬性徐绑?
// "自釋放"邪驮,ProtocolDispatcher 并不是一個(gè)單例,而是一個(gè)局部變量傲茄,當(dāng)implemertor釋放時(shí)就會(huì)觸發(fā)ProtocolDispatcher釋放毅访。
// key 需要為隨機(jī),否則當(dāng)有兩個(gè)分發(fā)器是盘榨,key 會(huì)被覆蓋俺抽,導(dǎo)致第一個(gè)分發(fā)器釋放。所以 key = _cmd 是不行的较曼。
void *key = (__bridge void *)([NSString stringWithFormat:@"%p",self]);
objc_setAssociatedObject(implemertor, key, self, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}];
self.implemertors = implemertorContexts;
}
return self;
}
注意事項(xiàng)
協(xié)議分發(fā)器使用需要了解如何處理帶有返回值的函數(shù) 磷斧,比如
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
我們知道,iOS中,函數(shù)執(zhí)行返回的結(jié)果存在于寄存器R0中弛饭,后執(zhí)行的會(huì)覆蓋先執(zhí)行的結(jié)果冕末。因此,當(dāng)遇到有返回結(jié)果的函數(shù)時(shí)侣颂,返回結(jié)果以后執(zhí)行的函數(shù)返回結(jié)果為最終值档桃。
感謝
Protocol協(xié)議分發(fā)器,本人并不是首創(chuàng)憔晒,也是看了這篇文章Protocol協(xié)議分發(fā)器得到運(yùn)用于 A/B Test 的靈感藻肄,在這里感謝作者和開(kāi)源社區(qū)。
業(yè)務(wù)模塊內(nèi)的 A/B Test 組件探索
隨著A/B Test 的代碼越來(lái)越多拒担,業(yè)務(wù)模塊內(nèi)的 A/B Test 組件化嘹屯,無(wú)非是為了更方便的上下業(yè)務(wù)的 A/B Test 代碼,提高工作效率从撼,讓寫(xiě)代碼和刪代碼變成一件快樂(lè)的事情州弟。
關(guān)于 iOS 組件化,網(wǎng)上也有很多文章低零,這里就不炒冷飯了婆翔,大家可以搜索一下關(guān)于組件化的一些定義和經(jīng)驗(yàn)。
在整個(gè)客戶(hù)端已經(jīng)被組件化的今天掏婶,不是架構(gòu)組的業(yè)務(wù)程序員可不可以嘗試來(lái)解決一下業(yè)務(wù)模塊內(nèi)的 A/B Test 組件化呢啃奴?iOS 組件化大部分都是圍繞 Cocoapods 來(lái)展開(kāi)的,所以在基于 Cocoapods iOS 高度組件化的的框架下雄妥, 我們先來(lái)問(wèn)幾個(gè)技術(shù)問(wèn)題最蕾。
相同架構(gòu)的不同靜態(tài)庫(kù)是否可合并?
這個(gè)問(wèn)題主要是基于目前整個(gè)客戶(hù)端架構(gòu)茎芭,各個(gè)業(yè)務(wù)線(xiàn)向殼工程提供了自己的靜態(tài)庫(kù)揖膜,
我們大部分時(shí)間(打包時(shí))都會(huì)合并不同架構(gòu)的相同靜態(tài)庫(kù),相同架構(gòu)的不同靜態(tài)庫(kù)是否可合并梅桩?
答案是壹粟,可以的。
在合并不同架構(gòu)的相同靜態(tài)庫(kù)時(shí)宿百,用到以下命令:
- 查看靜態(tài)庫(kù)支持的CPU架構(gòu)
lipo -info libname.a(或者libname.framework/libname)
- 合并靜態(tài)庫(kù)
lipo -create 靜態(tài)庫(kù)存放路徑1 靜態(tài)庫(kù)存放路徑2 ... -output 整合后存放的路徑
- 靜態(tài)庫(kù)拆分
lipo 靜態(tài)庫(kù)源文件路徑 -thin CPU架構(gòu)名稱(chēng) -output 拆分后文件存放路徑
那么合并相同架構(gòu)的不同靜態(tài)庫(kù)是怎么做的趁仙?
靜態(tài)庫(kù)文件也稱(chēng)為“文檔文件”,它是一些.o文件的集合垦页。在Linux(Unix)中使用工具“ar”對(duì)它進(jìn)行維護(hù)管理雀费。它所包含的成員(member)就是若干.o文件。除了.o文件痊焊,還有一個(gè)一個(gè)特殊的成員盏袄,它的名字是__.SYMDEF
忿峻。它包含了靜態(tài)庫(kù)中所有成員所定義的有效符號(hào)(函數(shù)名、變量名)辕羽。因此逛尚,當(dāng)為庫(kù)增加了一個(gè)成員時(shí),相應(yīng)的就需要更新成員__.SYMDEF
刁愿,否則所增加的成員中定義的所有的符號(hào)將無(wú)法被連接程序定位绰寞。完成更新的命令是:
ranlib libname.a
舉個(gè)例子:
我們有倆個(gè)靜態(tài)庫(kù)libFlight.a
和libHotel.a
,合并成一個(gè)libFlight_Hotel.a
铣口。
-
取出相同架構(gòu)下的Lib.a滤钱。
首先查看靜態(tài)庫(kù)Flight.a
的架構(gòu):lipo -info Flight.a
可以看到:
input file /Users/f.li/Desktop/相同架構(gòu)的不同靜態(tài)庫(kù)合并/libFlight.a is not a fat file Non-fat file: /Users/f.li/Desktop/相同架構(gòu)的不同靜態(tài)庫(kù)合并/libFlight.a is architecture: x86_64
libFlight.a is not a fat file 和 libFlight.a is architecture: x86_64
fat file 那么代表這個(gè)包是支持多平臺(tái)的,not a fat file 就是不支持多平臺(tái)的脑题,架構(gòu)是x86_64件缸。
當(dāng)然,如果是 fat file 旭蠕,我們就需要取出相同平臺(tái)架構(gòu)的庫(kù)停团。
lipo libFlight.a -thin x86_64 -output libFlight.a
這樣旷坦,就會(huì)取出 x86_64 架構(gòu)下的
libFlight.a
掏熬。 -
查看庫(kù)中所包含的文件列表。
ar -t /Users/f.li/Desktop/相同架構(gòu)的不同靜態(tài)庫(kù)合并/libFlight.a __.SYMDEF SORTED Flight.o
看到
libFlight.a
有兩個(gè)文件秒梅,__.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
文件夾內(nèi)捆蜀,就有了__.SYMDEF SORTED
和Flight.o
這個(gè)兩個(gè)文件疮丛。
同樣,在libHotel_o
文件夾內(nèi)獲得__.SYMDEF SORTED
和Hotel.o
-
合并辆它,重新打包誊薄。
把__.SYMDEF SORTED
和Flight.o
,還有Hotel.o
移動(dòng)到libFlight_Hotel_o
文件夾內(nèi)锰茉。把重新打包object file呢蔫;ar rcs libFlight_Hotel.a /Users/f.li/Desktop/libFlight_Hotel_o/*o
這樣就得到了
libFlight_Hotel.a
。 -
更新
__.SYMDEF
文件飒筑。
其實(shí)片吊,我們是把Hotel.o
加入了LibFlight.a中,最后协屡,需要更新__.SYMDEF
文件俏脊。ranlib libFlight_Hotel.a
如果包含頭文件,那么把頭文件也放到一個(gè)文件內(nèi)在使用libFlight_Hotel.a的工程中引入就可以了肤晓。
但是顯然這樣做太麻煩爷贫。
Xcode 子工程认然?
Xcode 子工程,其實(shí)是幫助我們?cè)谝粋€(gè)工程內(nèi)配合git submodule 來(lái)進(jìn)行分模塊開(kāi)發(fā)漫萄。
整理下思路季眷。
- 創(chuàng)建一個(gè) target(Flight_Hotel_Project) 為 Application 的 Xcode 工程為父工程。并git化卷胯。
- 創(chuàng)建一個(gè) tagget(Flight_SubProject) 為 Static Library 的 Xcode 工程為子工程子刮,并git化。
- 為父工程添加git submodule窑睁。具體參照git挺峡。
- 將子工程文件夾拖入父工程。
- 在父工程的 link binary with library 加入Flight_SubProject.a
- 在父工程的 header search paths 中添加頭文件搜索路徑
$(SRCROOT)/Flight_SubProject/Flight_SubProject
担钮,其中$(SRCROOT)宏代表你的工程文件目錄橱赠。 - 編譯運(yùn)行。
這樣其實(shí)回到了之前架構(gòu)的一個(gè)狀態(tài)箫津,無(wú)法調(diào)用解耦狭姨,相互依賴(lài)嚴(yán)重。
Cocoapods 的 subspecs 是什么概念苏遥?subspec 有自己獨(dú)立的git倉(cāng)庫(kù)嗎饼拍?是可以理解成pod的子pod嗎?
答案是田炭,subspec 不是獨(dú)立的代碼庫(kù)师抄,只是編譯時(shí)候分開(kāi)進(jìn)行,最后會(huì)和pod形成一個(gè)產(chǎn)物教硫。
為什么會(huì)問(wèn) Cocoapods subspecs叨吮?因?yàn)樵诨贑ocoapods架構(gòu)組件化后,業(yè)務(wù)對(duì)外部提供的是靜態(tài)庫(kù)類(lèi)型的pod瞬矩。
源碼類(lèi)型是subspec茶鉴,在引入pod時(shí),可以選擇引入subspec目錄景用,也可以設(shè)置podspec的默認(rèn)subsepc涵叮,subspect之間也可以有依賴(lài)關(guān)系。
最終解決方案是什么丛肢?
業(yè)務(wù)線(xiàn)內(nèi)部拆分可以做成多個(gè) pod围肥,最后提供一個(gè) pod 依賴(lài)所有業(yè)務(wù)內(nèi)部的組件 pod,這樣不影響外部架構(gòu)打包蜂怎,業(yè)務(wù)線(xiàn)也可以靈活修改穆刻。
最后這個(gè)依賴(lài)所有業(yè)務(wù)內(nèi)部組件的pod對(duì)外提供的也是一個(gè)靜態(tài)庫(kù),業(yè)務(wù)內(nèi)部的組件pod不需要提供靜態(tài)庫(kù)杠步,但是也會(huì)有獨(dú)立的Git氢伟。
當(dāng)然這種業(yè)務(wù)內(nèi)部的 A/B Test 組件化方案目前處于探索階段榜轿,因?yàn)槟壳拔覀兊?A/B Test 的代碼量并沒(méi)有達(dá)到需要我們進(jìn)行拆分的地步,所有這階段尚處于技術(shù)拓展調(diào)(yi)研(yin)階段朵锣。
小結(jié)
關(guān)于 iOS A/B Test 的探索目前小生就這么多谬盐,A/B Test 對(duì)于產(chǎn)品而言確實(shí)是一種比較好的方案,尤其是可逆性和數(shù)據(jù)驅(qū)動(dòng)诚些,當(dāng)然小生是站在開(kāi)發(fā)的角度上來(lái)看待 A/B Test飞傀。既然是對(duì)產(chǎn)品有利的方案,我們的代碼就應(yīng)該時(shí)代潮流诬烹,畢竟技術(shù)是為業(yè)務(wù)服務(wù)的砸烦。
前段時(shí)間在看 sunny 直播時(shí),談到了 iOS 開(kāi)發(fā)的進(jìn)階速度
純?nèi)粘i_(kāi)發(fā) < 純看書(shū)绞吁、博客 < 自己試驗(yàn)幢痘、Demo < 寫(xiě)博客 < 系統(tǒng)性分享和討論 < 提供完整的開(kāi)源方案
之前自己的進(jìn)階速度僅僅到寫(xiě)博客的分段,最近這半年在團(tuán)隊(duì)中發(fā)起了技術(shù)分享了和團(tuán)隊(duì)博客的浪潮家破,希望能夠向系統(tǒng)性分享颜说、討論和完整的開(kāi)源方案這兩個(gè)高分段沖分,本次結(jié)合最近的業(yè)務(wù)和自身的一些想法和實(shí)踐汰聋,完成了一次沖分嘗試门粪,希望在沖分的路上越戰(zhàn)越勇!