iOS A/B Test 方案探索

引子

公元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)理的:

  1. 目標(biāo)是什么竟闪?
  2. AB版本是什么?
  3. 樣本量有多大?
  4. 用戶(hù)如何分流杖狼?
  5. 測(cè)試時(shí)間多長(zhǎng)炼蛤?
  6. 如何衡量效果?

這其實(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

2017010534065A:BTest_functionA:B2_no.png
201701052172A:BTest_functionA:B_no.png

剛剛說(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

2017010548669A:BTest_Selector+dic1.png
201701052186A:BTest_Selector+dic2.png

由于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ì)模式之策略模式

2017010594760OTA_ABTestClass.png
201701058434ABstrategy_pattern1.png
2017010524744ABstrategy_pattern2.png

如圖所示戴差,通過(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.alibHotel.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 SORTEDFlight.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 SORTEDFlight.o這個(gè)兩個(gè)文件疮丛。
    同樣,在libHotel_o文件夾內(nèi)獲得__.SYMDEF SORTEDHotel.o

  • 合并辆它,重新打包誊薄。
    __.SYMDEF SORTEDFlight.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)越勇!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末马僻,一起剝皮案震驚了整個(gè)濱河市庄拇,隨后出現(xiàn)的幾起案子注服,更是在濱河造成了極大的恐慌韭邓,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件溶弟,死亡現(xiàn)場(chǎng)離奇詭異女淑,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)辜御,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)鸭你,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人擒权,你說(shuō)我怎么就攤上這事袱巨。” “怎么了碳抄?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵愉老,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我剖效,道長(zhǎng)嫉入,這世上最難降的妖魔是什么焰盗? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮咒林,結(jié)果婚禮上熬拒,老公的妹妹穿的比我還像新娘。我一直安慰自己垫竞,他們只是感情好澎粟,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著欢瞪,像睡著了一般捌议。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上引有,一...
    開(kāi)封第一講書(shū)人閱讀 51,718評(píng)論 1 305
  • 那天瓣颅,我揣著相機(jī)與錄音,去河邊找鬼譬正。 笑死宫补,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的曾我。 我是一名探鬼主播粉怕,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼抒巢!你這毒婦竟也來(lái)了贫贝?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤蛉谜,失蹤者是張志新(化名)和其女友劉穎稚晚,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體型诚,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡客燕,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了狰贯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片也搓。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖涵紊,靈堂內(nèi)的尸體忽然破棺而出傍妒,到底是詐尸還是另有隱情,我是刑警寧澤摸柄,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布颤练,位于F島的核電站,受9級(jí)特大地震影響塘幅,放射性物質(zhì)發(fā)生泄漏昔案。R本人自食惡果不足惜尿贫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望踏揣。 院中可真熱鬧庆亡,春花似錦、人聲如沸捞稿。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)娱局。三九已至彰亥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間衰齐,已是汗流浹背任斋。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留耻涛,地道東北人废酷。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像抹缕,于是被迫代替她去往敵國(guó)和親澈蟆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語(yǔ)法卓研,類(lèi)相關(guān)的語(yǔ)法趴俘,內(nèi)部類(lèi)的語(yǔ)法,繼承相關(guān)的語(yǔ)法奏赘,異常的語(yǔ)法寥闪,線(xiàn)程的語(yǔ)...
    子非魚(yú)_t_閱讀 31,643評(píng)論 18 399
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)志珍,斷路器橙垢,智...
    卡卡羅2017閱讀 134,665評(píng)論 18 139
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,180評(píng)論 25 707
  • “全世界只有一個(gè)我,你為什么不珍惜我嗽元?” “我為你付出了這么多敛纲,你為什么不珍惜我?” “別人都說(shuō)我好剂癌,你為什么不珍...
    酒紅甜言閱讀 1,320評(píng)論 2 5
  • 非洲的那種猴面包樹(shù)給人的震撼遠(yuǎn)比想象中的來(lái)得強(qiáng)烈淤翔。那些粗大的樹(shù)干可能需要十幾個(gè)人或著幾十個(gè)人手拉手才能?chē) ? 站...
    左小斜閱讀 395評(píng)論 0 3