寫碼手滑,讓我雙十一失眠

thunder.jpeg

一個(gè)蛋疼的需求

雙十一前夕的開發(fā)周期,我負(fù)責(zé)了一個(gè)有點(diǎn)麻煩的需求,簡(jiǎn)單來說就是“產(chǎn)品經(jīng)理”希望在App的導(dǎo)航欄顏色上做文章,他需要h5活動(dòng)頁可配置白色導(dǎo)航欄。其實(shí)對(duì)于這種需求,在“產(chǎn)品經(jīng)理”眼中就是一個(gè)小case,對(duì)于我來說,還是感覺頭腦嗡嗡嗡,如閃電轟鳴啊,[參考配圖],=_=!。

一種low逼的解決方案

讀者也許會(huì)嘲笑我,不就是配置導(dǎo)航欄顏色嘛,有什么困難的呢?諸君且聽我簡(jiǎn)單說一下背景,當(dāng)時(shí)負(fù)責(zé)api開發(fā)的同事過兩天就要離職了,說實(shí)話他的心思已經(jīng)不在公司的任務(wù),對(duì)于他而言,早點(diǎn)脫身,哪管身后洪水滔天。所以他想了一個(gè)最簡(jiǎn)單的實(shí)現(xiàn)方案,就是在url鏈接后面拼接參數(shù),參數(shù)名叫做bgColor和textColor,分別表示導(dǎo)航欄顏色和文字顏色,舉個(gè)例子來說吧,h5的url鏈接可能是這樣的,http://hostname/activity?id=123321&bgColor=0x222222&textColor=0xfffeda。

看見這樣的解決方案時(shí)候,我內(nèi)心感覺這樣真實(shí)low逼,本身是一個(gè)簡(jiǎn)單的url,現(xiàn)在拼接了莫名其妙的參數(shù),雖然不至于影響顯示效果,可是說不定哪一天因?yàn)閡rl長(zhǎng)度太長(zhǎng),導(dǎo)致不能分享到第三方,例如微信、微博。可是low逼歸low逼,還是得硬著頭皮做啊。

在做這個(gè)需求的時(shí)候,我心想,服務(wù)端返回的url拼接了參數(shù)bgColor和textColor,那我在客戶端解析參數(shù)就得了唄。當(dāng)時(shí)為了省事也想直接判斷url是否包含bgColor和textColor這樣的字符串,如果有這些字符串,直接設(shè)置導(dǎo)航欄顏色得了,也沒必要花多少力氣去解析url的參數(shù)了。

可是呢,又覺得服務(wù)端的方案已經(jīng)夠low逼了,客戶端再low逼一下,代碼的質(zhì)量就是這樣下降的哦。然后腦子一沖動(dòng),心想使用炫酷的方式來對(duì)url進(jìn)行解析吧。

定下了這樣解決問題的基調(diào),接下來就是實(shí)現(xiàn)url解析參數(shù)的工作了,其實(shí)呢,動(dòng)腦子想想,解析url參數(shù)其實(shí)就是獲取url中&=兩邊的數(shù)據(jù),按照編寫OC代碼的習(xí)慣,很容易通過OC來解決這樣的問題,如下代碼所示,

NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
for (NSString *param in [url componentsSeparatedByString:@"&"]) { 
  NSArray *elements = [param componentsSeparatedByString:@"="]; 
  if([elements count] < 2) 
    continue; 
  [params setObject:[elements lastObject] forKey:[elements firstObject]];
}

這段代碼參考了stackoverflow的內(nèi)容,原文鏈接parse nsurl query property,其實(shí)這段代碼已經(jīng)寫的比較簡(jiǎn)潔明了,并且條件判斷if ([elements count] < 2) continue;是很精髓的代碼,至于為什么說這樣精髓,稍后再作解釋。

這樣直接拿到url字符串在ViewController的viewDidLoad中解析,難免增加了ViewController的代碼量,所以可以將上面的代碼封裝一下,作為NSURL的category,簡(jiǎn)單擴(kuò)展一下,可以新建NSURL+QueryParse的category,如下代碼所示,

// NSURL+QueryParse.h
#import <Foundation/Foundation.h>
@interface NSURL (QueryParse)
@property (strong, nonatomic) NSDictionary *queryValues;
@end

// NSURL+QueryParse.m
#import "NSURL+QueryParse.h"
@implementation NSURL (QueryParse)
- (NSDictionary *)queryValues {
    NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
    for (NSString *param in [self.query componentsSeparatedByString:@"&"]) {
        NSArray *elements = [param componentsSeparatedByString:@"="];
        if([elements count] < 2) continue;
        [params setObject:[elements lastObject] forKey:[elements firstObject]];
    }
    return params;
}
@end

對(duì)NSURL添加一個(gè)category方法-queryValues,是一個(gè)好方法,而且上面的代碼簡(jiǎn)單易讀,熟悉編寫OC代碼的iOS開發(fā)者應(yīng)該一看就懂了,而且該category方法是該stackoverflow上面點(diǎn)贊最多的回答。然而我卻在想,咱項(xiàng)目都在轉(zhuǎn)向swift開發(fā),而且近期我也在研究了一番swift高階函數(shù)的使用,何不嘗試下swift的extension實(shí)現(xiàn)呢。后來在后面的回答中,看到了下面的代碼,正和我意哦,swift的實(shí)現(xiàn)代碼如下所示,

extension NSURL {
  public var queryValues : [String:String] {
     get {
        guard  let q = self.query else { return [:] }
        let dic = [String:String]()
        return q.componentsSeparatedByString("&")
            .map { $0.componentsSeparatedByString("=") }
            .reduce(dic) {
                var temp = $0
                temp[$1[0]] = $1[1].stringByRemovingPercentEncoding
                return temp }
        }
    }
}

針對(duì)上述代碼,我來以http://hostname/activity?id=123321&bgColor=0x222222&textColor=0xfffedaURL為例,做個(gè)簡(jiǎn)單的解釋,

該extension為NSURL添加了queryValues屬性,self.query就是一個(gè)URL從?之后的查詢參數(shù)字符串,即為id=123321&bgColor=0x222222&textColor=0xfffeda;接下來的語法q.componentsSeparatedByString("&")表示將查詢參數(shù)字符串以&作間隔切分為數(shù)組,切分過后數(shù)組為[id=123321, bgColor=0x222222, textColor=0xfffeda];然后呢,又對(duì)該數(shù)組做map映射操作,就是將數(shù)組中的每個(gè)元素以=為間隔來進(jìn)行切分,得到了這樣的結(jié)果,[[id, 123321], [bgColor, 0x222222], [textColor, 0xfffeda]];最后再使用reduce操作將所有的內(nèi)容組合成字典,取每個(gè)子數(shù)組的第0個(gè)元素作為key、第1個(gè)元素作為value,則形成的temp字典為[id: 123321, bgColor: 0x222222, textColor:0xfffeda]。

我對(duì)上面的代碼做了簡(jiǎn)單的解釋之后,相信各位讀者都明白解析參數(shù)的過程。這在正常的情況下,運(yùn)行都OK;我也多次試驗(yàn)反復(fù)確認(rèn)沒問題之后就提交了代碼,然后開始開發(fā)其他模塊的內(nèi)容。

一種意想不到的崩潰方式

但是,上面的代碼存在一個(gè)問題,那就是處理不太標(biāo)準(zhǔn)或者說參數(shù)不完整的url,就有問題了,例如看看下面的url,string: "http://hostname/?&a=b&c=d&c1=d2&n1=&n2&,它有什么問題呢。首先,我們來回憶一下,比較符合我們思維中固有常識(shí)的url是什么樣的標(biāo)準(zhǔn)格式,大概羅列一下,有如下幾點(diǎn),

  • &兩邊是key=value的參數(shù)形式
  • =兩邊包含key, value

但是上面的url格式,則不符合我們的常識(shí),例如n1=只有key,而沒有value;再比如n2只有key,連=都沒有。然而這樣的url也是合法的url,我卻沒有考慮這樣比較異常的情況,這時(shí),通過調(diào)用NSURL擴(kuò)展的queryValues屬性,則直接導(dǎo)致了崩潰,如下代碼所示,

let url = URL(string: "http://hostname/?&a=b&c=d&c1=d2&n1=&n2&")!
let params = url.queryValues

分解一下執(zhí)行的步驟,分析崩潰在什么地方呢,如下過程所示,

  1. 第一步,q = self.query,q的值為&a=b&c=d&c1=d2&n1=&n2&,OK,沒有什么問題,
  2. 第二步,map { $0.componentsSeparatedByString("&") },得到結(jié)果為數(shù)組 [nil, a=b, c=d, n1=, n2],OK,也沒有什么問題,
  3. 第三步,map { $0.componentsSeparatedByString("="),該過程作用于數(shù)組 [nil, a=b, c=d, n1=, n2]中的每個(gè)元素,到最后一個(gè)元素n2時(shí),直接調(diào)用componentsSeparatedByString("="),OK,沒問題,它生成了數(shù)組[n2],繼續(xù),
  4. 第四步,temp[$1[0]] = $1[1].stringByRemovingPercentEncoding,回顧一下上一步的參數(shù)n2以及數(shù)組[n2],這時(shí)候$1[0]即為n2,$1[1]nil,此時(shí)將nil作為字典temp的value,導(dǎo)致崩潰。

好了,分析了過程,我來說說帶來的惡果 --- 這個(gè)坑直接帶來雙十一期間App崩潰率急劇上升,達(dá)到了0.8%。說真的,別人雙十一愉快剁手,我卻亞歷山大,徹夜難眠。

之所以這個(gè)坑影響的范圍如此之大,是因?yàn)樯厦娴恼Z句調(diào)用let params = url.queryValues直接發(fā)生在了一個(gè)通用的h5容器里面,在互聯(lián)網(wǎng)+電商公司工作的人肯定知道,一般來說,穩(wěn)定的業(yè)務(wù)比如商品詳情、購物車、下單流程基本用原生代碼居多;而促銷活動(dòng)或雙十一推送,大多是通過h5來展現(xiàn)給用戶的。而雙十一當(dāng)天,我公司的運(yùn)營(yíng)推送的內(nèi)容,在h5頁面就因?yàn)閡rl參數(shù)出現(xiàn)了諸如http://hostname/?&a=b&c=d&c1=d2&n1=&n2&這樣不符合我們常識(shí)但卻合法的參數(shù),導(dǎo)致App推送的內(nèi)容崩潰。

后來我看了Bugly上面的崩潰日志,當(dāng)晚大約有2000條左右的崩潰,在解決了運(yùn)營(yíng)端的bug之后,崩潰次數(shù)趨于減少;第二天崩潰又猛增了500次有做。如果以2500崩潰總數(shù)計(jì)算,用戶購買轉(zhuǎn)化率為5%,每個(gè)付費(fèi)用戶購買800元計(jì)算,則損失的交易額為tradeMoney = 2500 * 5% * 800 = 100 000,所以,差不多是10w元的損失。說多也不多,也不能說少,我估計(jì)會(huì)有很多潛在的損失,比如用戶怒刪App。

一下午心碎的熱修復(fù)

雙十一是在周五,當(dāng)天下午崩潰數(shù)量又有所上升,我坐立不安,停下手中的任務(wù),開始著手寫熱修復(fù)的JS代碼,因?yàn)槲遗马?xiàng)目經(jīng)理讓我修復(fù)的時(shí)候時(shí)間來不及,還不如及早進(jìn)行。

熱修復(fù)時(shí)候,想了多種方法,比如在h5容器對(duì)應(yīng)的ViewController內(nèi)部,當(dāng)執(zhí)行viewDidLoad時(shí)候,將可能錯(cuò)誤的url拼接缺少的=,但是試了一會(huì),發(fā)現(xiàn)url竟然定義成了private的,所以只能另尋其他門道。

后來想想,還是從修改NSURL的queryValues著手,如上所述,我為NSURL擴(kuò)展了queryValues屬性,其實(shí)可以把它想象成OC中的-queryValues方法,那么就是用JSPatch修復(fù)-queryValues的方法罷了,具體實(shí)現(xiàn)過程大體就是上述的OC的category NSURL+QueryParse代碼,如下是修復(fù)該崩潰的JSPatch代碼,

defineClass('NSURL', {
            queryValues: function() {
            /*
             NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
             
             for (NSString *param in [self.query componentsSeparatedByString:@"&"]) {
             NSArray *elements = [param componentsSeparatedByString:@"="];
             if([elements count] < 2) continue;
             [params setObject:[elements lastObject] forKey:[elements firstObject]];
             }
             return params;
             */
            
            var params = NSMutableDictionary.alloc().init()
            
            
            var temps = self.query().componentsSeparatedByString("&").toJS() // 第1點(diǎn)
            for (var i = 0; i < temps.length; i++) {
                var param = temps[i]
            
                var paramStr = NSString.stringWithString(param) // 第2點(diǎn)
                var elements = paramStr.componentsSeparatedByString("=")
                if (elements.count < 2) {
                    continue
                }
                //console("element is " + elements)
                params.setObject_forKey(elements.lastObject(), elements.firstObject())
            }
            return params
            },
            })

我將OC的源碼也寫在了注釋里面,上面的JSPatch熱修復(fù)代碼并不難理解,有3個(gè)地方著重說明一下,

1. 使用toJS()將OC數(shù)組轉(zhuǎn)為JavaScript數(shù)組

var temps = self.query().componentsSeparatedByString("&").toJS()
for (var i = 0;i < temps.length; i++) {
  
}

這段代碼使用toJS()將切片之后的數(shù)組轉(zhuǎn)換為JavaScript的數(shù)組,所以在for循環(huán)中需要使用length獲取數(shù)組長(zhǎng)度。

2. 將JavaScript字符串轉(zhuǎn)換為OC字符串,以便調(diào)用對(duì)應(yīng)方法

var paramStr = NSString.stringWithString(param)
var elements = paramStr.componentsSeparatedByString("=")

這段代碼,將param轉(zhuǎn)換成OC中的NSString字符串,是因?yàn)槿绻晦D(zhuǎn)換,則該字符串是JavaScript字符串,不能代用下面的componentsSeparatedByString方法。

3. 判斷分解的參數(shù)是否小于2

if (elements.count < 2) {
  continue
}

這條判斷語句處理了分解的數(shù)組是否小于2,以上面的n2參數(shù)舉例,此時(shí)分解的數(shù)組為[n2],遇到此判斷條件時(shí)候,直接忽略continue后面的語句,進(jìn)行for的下一次循環(huán),這也是文章前面所說的比較精髓的地方。

上述3點(diǎn),我在寫JS熱修復(fù)的時(shí)候在前2點(diǎn)耽誤了不少時(shí)間,也是對(duì)JSPatch文檔閱讀不夠到位,希望讀者可以避過這些坑。

備注:上面的熱修復(fù)代碼,翻譯了NSURL+QueryParse的OC代碼,這段代碼還有點(diǎn)問題,就是沒有處理urlencode的情況。

一點(diǎn)小小的總結(jié)

寫代碼還是以穩(wěn)妥為主,保證不出現(xiàn)重大的問題,畢竟自己以為創(chuàng)新性的實(shí)現(xiàn)方式,說不定就要踩到坑里面了。特別是項(xiàng)目中比較底層、通用的模塊,更加不能隨便改動(dòng),比如我這次踩的大坑,就是改動(dòng)了項(xiàng)目中多數(shù)h5頁面使用的容器類。一把眼淚啊。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個(gè)線程,因...
    小菜c閱讀 7,321評(píng)論 0 17
  • Swift 介紹 簡(jiǎn)介 Swift 語言由蘋果公司在 2014 年推出,用來撰寫 OS X 和 iOS 應(yīng)用程序 ...
    大L君閱讀 3,425評(píng)論 3 25
  • iOS開發(fā)系列--網(wǎng)絡(luò)開發(fā) 概覽 大部分應(yīng)用程序都或多或少會(huì)牽扯到網(wǎng)絡(luò)開發(fā),例如說新浪微博、微信等,這些應(yīng)用本身可...
    lichengjin閱讀 4,034評(píng)論 2 7
  • *面試心聲:其實(shí)這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個(gè)offer,總結(jié)起來就是把...
    Dove_iOS閱讀 27,604評(píng)論 30 472
  • 你努力對(duì)他好,努力變成他想要的樣子,其實(shí)你知道他不喜歡的只是你而已
    等晴天閱讀 231評(píng)論 0 1

友情鏈接更多精彩內(nèi)容