写码手滑,让我双十一失眠

thunder.jpeg

一个蛋疼的需求

双十一前夕的开发周期,我负责了一个有点麻烦的需求,简单来说就是“产品经理”希望在App的导航栏颜色上做文章,他需要h5活动页可配置白色导航栏。其实对于这种需求,在“产品经理”眼中就是一个小case,对于我来说,还是感觉头脑嗡嗡嗡,如闪电轰鸣啊,[参考配图],=_=!。

一种low逼的解决方案

读者也许会嘲笑我,不就是配置导航栏颜色嘛,有什么困难的呢?诸君且听我简单说一下背景,当时负责api开发的同事过两天就要离职了,说实话他的心思已经不在公司的任务,对于他而言,早点脱身,哪管身后洪水滔天。所以他想了一个最简单的实现方案,就是在url链接后面拼接参数,参数名叫做bgColor和textColor,分别表示导航栏颜色和文字颜色,举个例子来说吧,h5的url链接可能是这样的,http://hostname/activity?id=123321&bgColor=0x222222&textColor=0xfffeda

看见这样的解决方案时候,我内心感觉这样真实low逼,本身是一个简单的url,现在拼接了莫名其妙的参数,虽然不至于影响显示效果,可是说不定哪一天因为url长度太长,导致不能分享到第三方,例如微信、微博。可是low逼归low逼,还是得硬着头皮做啊。

在做这个需求的时候,我心想,服务端返回的url拼接了参数bgColor和textColor,那我在客户端解析参数就得了呗。当时为了省事也想直接判断url是否包含bgColor和textColor这样的字符串,如果有这些字符串,直接设置导航栏颜色得了,也没必要花多少力气去解析url的参数了。

可是呢,又觉得服务端的方案已经够low逼了,客户端再low逼一下,代码的质量就是这样下降的哦。然后脑子一冲动,心想使用炫酷的方式来对url进行解析吧。

定下了这样解决问题的基调,接下来就是实现url解析参数的工作了,其实呢,动脑子想想,解析url参数其实就是获取url中&=两边的数据,按照编写OC代码的习惯,很容易通过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的内容,原文链接parse nsurl query property,其实这段代码已经写的比较简洁明了,并且条件判断if ([elements count] < 2) continue;是很精髓的代码,至于为什么说这样精髓,稍后再作解释。

这样直接拿到url字符串在ViewController的viewDidLoad中解析,难免增加了ViewController的代码量,所以可以将上面的代码封装一下,作为NSURL的category,简单扩展一下,可以新建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

对NSURL添加一个category方法-queryValues,是一个好方法,而且上面的代码简单易读,熟悉编写OC代码的iOS开发者应该一看就懂了,而且该category方法是该stackoverflow上面点赞最多的回答。然而我却在想,咱项目都在转向swift开发,而且近期我也在研究了一番swift高阶函数的使用,何不尝试下swift的extension实现呢。后来在后面的回答中,看到了下面的代码,正和我意哦,swift的实现代码如下所示,

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 }
        }
    }
}

针对上述代码,我来以http://hostname/activity?id=123321&bgColor=0x222222&textColor=0xfffedaURL为例,做个简单的解释,

该extension为NSURL添加了queryValues属性,self.query就是一个URL从?之后的查询参数字符串,即为id=123321&bgColor=0x222222&textColor=0xfffeda;接下来的语法q.componentsSeparatedByString("&")表示将查询参数字符串以&作间隔切分为数组,切分过后数组为[id=123321, bgColor=0x222222, textColor=0xfffeda];然后呢,又对该数组做map映射操作,就是将数组中的每个元素以=为间隔来进行切分,得到了这样的结果,[[id, 123321], [bgColor, 0x222222], [textColor, 0xfffeda]];最后再使用reduce操作将所有的内容组合成字典,取每个子数组的第0个元素作为key、第1个元素作为value,则形成的temp字典为[id: 123321, bgColor: 0x222222, textColor:0xfffeda]。

我对上面的代码做了简单的解释之后,相信各位读者都明白解析参数的过程。这在正常的情况下,运行都OK;我也多次试验反复确认没问题之后就提交了代码,然后开始开发其他模块的内容。

一种意想不到的崩溃方式

但是,上面的代码存在一个问题,那就是处理不太标准或者说参数不完整的url,就有问题了,例如看看下面的url,string: "http://hostname/?&a=b&c=d&c1=d2&n1=&n2&,它有什么问题呢。首先,我们来回忆一下,比较符合我们思维中固有常识的url是什么样的标准格式,大概罗列一下,有如下几点,

  • &两边是key=value的参数形式
  • =两边包含key, value

但是上面的url格式,则不符合我们的常识,例如n1=只有key,而没有value;再比如n2只有key,连=都没有。然而这样的url也是合法的url,我却没有考虑这样比较异常的情况,这时,通过调用NSURL扩展的queryValues属性,则直接导致了崩溃,如下代码所示,

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

分解一下执行的步骤,分析崩溃在什么地方呢,如下过程所示,

  1. 第一步,q = self.query,q的值为&a=b&c=d&c1=d2&n1=&n2&,OK,没有什么问题,
  2. 第二步,map { $0.componentsSeparatedByString("&") },得到结果为数组 [nil, a=b, c=d, n1=, n2],OK,也没有什么问题,
  3. 第三步,map { $0.componentsSeparatedByString("="),该过程作用于数组 [nil, a=b, c=d, n1=, n2]中的每个元素,到最后一个元素n2时,直接调用componentsSeparatedByString("="),OK,没问题,它生成了数组[n2],继续,
  4. 第四步,temp[$1[0]] = $1[1].stringByRemovingPercentEncoding,回顾一下上一步的参数n2以及数组[n2],这时候$1[0]即为n2$1[1]nil,此时将nil作为字典temp的value,导致崩溃。

好了,分析了过程,我来说说带来的恶果 --- 这个坑直接带来双十一期间App崩溃率急剧上升,达到了0.8%。说真的,别人双十一愉快剁手,我却亚历山大,彻夜难眠。

之所以这个坑影响的范围如此之大,是因为上面的语句调用let params = url.queryValues直接发生在了一个通用的h5容器里面,在互联网+电商公司工作的人肯定知道,一般来说,稳定的业务比如商品详情、购物车、下单流程基本用原生代码居多;而促销活动或双十一推送,大多是通过h5来展现给用户的。而双十一当天,我公司的运营推送的内容,在h5页面就因为url参数出现了诸如http://hostname/?&a=b&c=d&c1=d2&n1=&n2&这样不符合我们常识但却合法的参数,导致App推送的内容崩溃。

后来我看了Bugly上面的崩溃日志,当晚大约有2000条左右的崩溃,在解决了运营端的bug之后,崩溃次数趋于减少;第二天崩溃又猛增了500次有做。如果以2500崩溃总数计算,用户购买转化率为5%,每个付费用户购买800元计算,则损失的交易额为tradeMoney = 2500 * 5% * 800 = 100 000,所以,差不多是10w元的损失。说多也不多,也不能说少,我估计会有很多潜在的损失,比如用户怒删App。

一下午心碎的热修复

双十一是在周五,当天下午崩溃数量又有所上升,我坐立不安,停下手中的任务,开始着手写热修复的JS代码,因为我怕项目经理让我修复的时候时间来不及,还不如及早进行。

热修复时候,想了多种方法,比如在h5容器对应的ViewController内部,当执行viewDidLoad时候,将可能错误的url拼接缺少的=,但是试了一会,发现url竟然定义成了private的,所以只能另寻其他门道。

后来想想,还是从修改NSURL的queryValues着手,如上所述,我为NSURL扩展了queryValues属性,其实可以把它想象成OC中的-queryValues方法,那么就是用JSPatch修复-queryValues的方法罢了,具体实现过程大体就是上述的OC的category NSURL+QueryParse代码,如下是修复该崩溃的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点
            for (var i = 0; i < temps.length; i++) {
                var param = temps[i]
            
                var paramStr = NSString.stringWithString(param) // 第2点
                var elements = paramStr.componentsSeparatedByString("=")
                if (elements.count < 2) {
                    continue
                }
                //console("element is " + elements)
                params.setObject_forKey(elements.lastObject(), elements.firstObject())
            }
            return params
            },
            })

我将OC的源码也写在了注释里面,上面的JSPatch热修复代码并不难理解,有3个地方着重说明一下,

1. 使用toJS()将OC数组转为JavaScript数组

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

这段代码使用toJS()将切片之后的数组转换为JavaScript的数组,所以在for循环中需要使用length获取数组长度。

2. 将JavaScript字符串转换为OC字符串,以便调用对应方法

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

这段代码,将param转换成OC中的NSString字符串,是因为如果不转换,则该字符串是JavaScript字符串,不能代用下面的componentsSeparatedByString方法。

3. 判断分解的参数是否小于2

if (elements.count < 2) {
  continue
}

这条判断语句处理了分解的数组是否小于2,以上面的n2参数举例,此时分解的数组为[n2],遇到此判断条件时候,直接忽略continue后面的语句,进行for的下一次循环,这也是文章前面所说的比较精髓的地方。

上述3点,我在写JS热修复的时候在前2点耽误了不少时间,也是对JSPatch文档阅读不够到位,希望读者可以避过这些坑。

备注:上面的热修复代码,翻译了NSURL+QueryParse的OC代码,这段代码还有点问题,就是没有处理urlencode的情况。

一点小小的总结

写代码还是以稳妥为主,保证不出现重大的问题,毕竟自己以为创新性的实现方式,说不定就要踩到坑里面了。特别是项目中比较底层、通用的模块,更加不能随便改动,比如我这次踩的大坑,就是改动了项目中多数h5页面使用的容器类。一把眼泪啊。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353

推荐阅读更多精彩内容

  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,397评论 0 17
  • Swift 介绍 简介 Swift 语言由苹果公司在 2014 年推出,用来撰写 OS X 和 iOS 应用程序 ...
    大L君阅读 3,209评论 3 25
  • iOS开发系列--网络开发 概览 大部分应用程序都或多或少会牵扯到网络开发,例如说新浪微博、微信等,这些应用本身可...
    lichengjin阅读 3,657评论 2 7
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,139评论 30 470
  • 你努力对他好,努力变成他想要的样子,其实你知道他不喜欢的只是你而已
    等晴天阅读 191评论 0 1