复盘一次令人头大的上线

昨晚我负责的产品 1.6.0 正式版上线,距离正式版上线前已经上了两个灰度测试版,每次版本上线前都会遇到一些或大或小的阻断性 bug ,用我们从淘宝新来 android 老大哥的话来说:“看来 app 不论大小,上线前都是一样的难受”。

遇到的问题

第一个问题

昨天中午遇到的第一个情况是这样的,报表列表的置顶数据不正常,每当一个报表被置顶且成功后,再取消置顶,则会顺带把报表的收藏状态给取消了,而且“收藏”、”有权限“、”申请中“三个接口拿到的报表数据实体置顶字段居然不一致 = =。真是头大,做了确认后给相关 RD 发了接口错误信息,给的答复是跟着下周的例行迭代一起修复,也就是说,这个版本我们要带上错误的接口上线了,问题解决,接着上课。

第二个问题

第二个问题,刷新“有权限” tab 时因为线上数据量大,接口有点慢,这就导致部分用户(PM)等不了(一般 3s 后如果数据未展示用户量会流失 50% ),此时切回了“收藏” tab,最后再切回到“申请中中” tab 时,列表数据居然是“收藏”的 = =,改了改,解决。

第三个问题

第三个问题,在“报表搜索” vc 中的“有权限” tab 收藏报表时,侧滑 cell 后点击收藏,没有提示也不知道是收藏没收藏上,此时去“报表超市”查看 “收藏” tab 时,发现实际上并没有收藏,这个问题之前有遇到过,实际上是报错了的,因为收藏了重复的报表,但因为在“报表搜索”中 cell 没有收藏 icon ,错以为没有收藏导致重复收藏失败。最后发现其实判断报表是否收藏的字段用错了,改了一波,解决。

第四个问题

第四个问题一直搞到今天中午,Jenkins 上打出来的 release 包推到下载平台上,1.5.0 升级上来后居然是 debug 环境 = =,刚开始觉得不应该,但是又不想解决了,到了昨晚九点时已经不想做了,就直接把 #ifdef DEBUG 下替换的 hostName 改为了线上地址,重新打包,发现问题还在。leader 怀疑是 1.5.0 上的 cache 问题,而我怀疑是 DEBUG 宏的问题,我们一直围绕在 1.6.0 本身的问题上,因为当时的情况是,拿到升级包的下载地址后,我用 safari 打开后下载是正常的,但通过 1.5.0 升级下载的包居然还是 debug 环境,一直懵逼到夜里 12 点,因为 DEBUG 这个宏不可能有错啊,差点陷入了深深的怀疑当中。

第五个问题

一直到今天中午,无论在在 1.6.0 代码上做的任何操作,都无效,其它同事因为不是做 iOS 的,给的意见虽然也都还行,但是实际上我还是觉得 DEBUG 这个默认提供的宏没啥问题,但突然猛的发现,我应该去调 1.5.0 的代码啊,因为问题是出在 1.5.0 升级上来,而不是 1.6.0 本身,突然发现了有这么一段代码,通过 NSUserDefault set 了取的 hostName 宏,NSUserDefault 是用户偏好设置啊 = =,升级不会被清空,能够被备份,而我在 1.6.0 中同样的方法中做了类似的 set hostName 的替换 debug 与 release 环境域名的操作,但是因为原 NSUserDefaultfirstInitLocal key 对应的 value 已经备份了,且已被设置为了 YES,并没有因为 app 的升级而被清空,根本没能进入到重新 set hostName的代码, = =,最终的做法是在 1.6.0 上注释掉这个 set hostName 的方法,重新在预编译文件中设置唯一 hostName。

第六个问题

本以为能够高高兴兴的发了版,但却发现从 mdm(一个统一登录权限管理 app)唤起 app 时居然闪退了,重现的步骤是得先把 app 退出,然后通过 mdm 唤起 app,但是如果 app 还在后台,直接唤起没有问题,此时已经是中午 12 点多,脑子有点懵逼了(饿了),但是问题没解决这么能去吃饭呢?但是这个问题没法调试,因为 app 已经被杀掉了,看不到调试信息。想在我的手机上操作复现一下,发现 mdm 无法安装,用 leader 的手机进行调试发现该设备未在对应证书下注册,因为之前我们的企业证书下的注册设备量满了,曾经给了一段时间让大家把活跃设备重新注册,但当时 leader 的设备并没有进行注册,此时突然想到 PM 的手机注册过了,拿来一调,确实是能够重现这个问题,但却发现连上 Xcode 后居然没拿到对应的 crash log。

接着又发现就算设备不能使用 Xcode 进行调试,也能够拿到 log,还是通过 Xcode 获取,也可以通过设置-隐私-分析数据里看到所有 app 的 crash log,又把 leader 的设备拿过来连上了 Xcode 格式化了 log,变成了如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 DiDiData 0x0000000100ff9974 0x100f54000 + 678260
1 DiDiData 0x0000000100ff84d8 0x100f54000 + 672984
2 DiDiData 0x0000000100ff3fc8 0x100f54000 + 655304
3 DiDiData 0x0000000100ff57cc 0x100f54000 + 661452
4 UIKitCore 0x00000001e9bd1380 -[UIViewController loadViewIfRequired] + 1000
5 UIKitCore 0x00000001e9bd17b0 -[UIViewController view] + 28
6 UIKitCore 0x00000001e9b2a114 -[UINavigationController _startCustomTransition:] + 1136
7 UIKitCore 0x00000001e9b3ebd4 -[UINavigationController _startDeferredTransitionIfNeeded:] + 716
8 UIKitCore 0x00000001e9b400a8 -[UINavigationController __viewWillLayoutSubviews] + 164
9 UIKitCore 0x00000001e9b22298 -[UILayoutContainerView layoutSubviews] + 224
10 UIKitCore 0x00000001ea637f44 -[UIView+ 14184260 (CALayerDelegate) layoutSublayersOfLayer:] + 1380
11 QuartzCore 0x00000001c1971a34 -[CALayer layoutSublayers] + 184
12 QuartzCore 0x00000001c19769c4 CA::Layer::layout_if_needed+ 1317316 (CA::Transaction*) + 324
13 QuartzCore 0x00000001c18d59d4 CA::Context::commit_transaction+ 657876 (CA::Transaction*) + 340
14 QuartzCore 0x00000001c19042f4 CA::Transaction::commit+ 848628 () + 608
15 QuartzCore 0x00000001c190515c CA::Transaction::observer_callback+ 852316 (__CFRunLoopObserver*, unsigned long, void*) + 92
16 CoreFoundation 0x00000001bd31db94 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 32
17 CoreFoundation 0x00000001bd318828 __CFRunLoopDoObservers + 412
18 CoreFoundation 0x00000001bd318dc8 __CFRunLoopRun + 1264
19 CoreFoundation 0x00000001bd3185b8 CFRunLoopRunSpecific + 436
20 GraphicsServices 0x00000001bf58c584 GSEventRunModal + 100
21 UIKitCore 0x00000001ea194bc8 UIApplicationMain + 212
22 DiDiData 0x0000000100fc8778 0x100f54000 + 477048
23 libdyld.dylib 0x00000001bcdd8b94 start + 4

只需要看主线程报的 log 就好了,根据堆栈提示可以猜出是 UINavigationController 下的 UIViewController 初始化错误(如果你的 log 看不到格式化好的信息自行搜索一番如何格式化),而用到了的 UINavigationController 的地方是在 AppDelegate 中的 [self initTabBarController] 方法,刚开始我还真以为就是这个问题,然后把 tabBar 四大入口的 vc 都一行行代码注释掉查看,最后折腾了一圈发现其实跟这个问题没有关系。

又陷入了苦闷之中,在 mdm 对接群中请教了相关 RD,我的问题是:“iOS app 在前台的时候,mdm 唤起正常,app 被杀掉后,通过 mdm 唤起闪退”,他给我的回答是“应该是处理openURL回调和didFinishLaunchingWithOptions回调事务处理逻辑问题”,后来仔细一想确实是这两个方法中才能引发这个问题,首先确保了 mdm 唤起 app 时走的 openURL 回调方法逻辑正常,已经做了 scheme 判断,那问题就出在了 didFinishLaunchingWithOptions 方法中。

又采用了最原始的方法,把这部分的代码全部注释掉,一段一段代码块打开注释并执行,最后发现原来又是上一个实习同学挖的坑,每次 app 重新加载后都注册一遍友盟的消息推送的方法(这个方法是用来初始化消息推送 push 的),1.5.0 写的时候估计这个同学没有考虑到我们的 app 会存在被其它应用唤起的情况,只考虑到了点击消息通知栏内容唤起的情况,直接从键值对中取值,但是从 mdm 唤起 app 带上的数据并不符合他写的解析代码,也没有判空,最后对空值操作,引发了 crash

怎么解决的?

WechatIMG4.jpeg

第七个问题

最后一个问题,SSO 团队收回了一个字段,导致 app 的一个三方 H5 应用入口 ticket 验证失败,疯狂递归唤起 mdm 进行用户登录授权,允许授权后,更新的 ticket 又因为 SSO 的问题,导致 ticket 验证失败,又唤起 mdm 进行用户登录授权……,最后这个 H5 应用的团队把负责 SSO 团队的同学骂了一顿,重新上线

想骂人

OK,解决完了以上问题,除了睡觉的八个小时还有一些零零碎碎的时间,从昨天早上十点到今天下午四点,今天一来还被“关进”一间会议室五个人陪着做封闭式开发,嗯,这个版本迭代周期从七月份到现在,不但长还臭。