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

2016-11-19  本文已影响98人  flionel
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是什么样的标准格式,大概罗列一下,有如下几点,

但是上面的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页面使用的容器类。一把眼泪啊。

上一篇 下一篇

猜你喜欢

热点阅读