原生工程师亲历:创业APP三年盈利的7次技术跃迁
|
去年十二月,iOS 17.2 正式版凌晨三点推的,我们那个「小而美」的记账APP在App Store更新后两小时,崩溃率从千分之三跳到百分之七——不是热更新崩,是冷启动直接闪退。我盯着 Xcode Organizer 里那段堆栈,第七行是 `-[UIView _updateLegacyLayerWithSize:]`,第八行空着,第九行跳到了 `libsystem_kernel.dylib` 的 `mach_msg_trap`。我原以为是 Core Animation 层级的脏读,结果翻了三天符号表,发现是 WKWebView 初始化时调了次 `[[UIScreen mainScreen] bounds].size.width`,而新系统里某个未文档化的 display scale 变化触发了它底层一个废弃路径里的 `CGRectZero` 强转。这个我敢打包票,因为我就蹲在隔壁工位老张的 Mac mini 上连了台 iPad Air 5 做复现,他刚把星巴克杯子放在双屏中间那条缝里,杯底压住了我的右屏壁纸——是张旧版 Android Studio 启动页,像素糊得像被水泡过。 跟产品经理吵是在周四下午三点十七分,会议室玻璃门没关严,漏风。她说“用户需要一键导入微信账单”,我说“微信不给私有 API,你让我 hook 微信进程?”。她推过来一张 Figma 链接,上面写了三个「极致体验」,还用荧光笔圈了「零感知同步」。我点开 DevTools —— 切在 Network tab,但鼠标悬停在「Preserve log」上没点,手指悬着。旁边测试小林突然起身去倒水,路过我工位时把我键盘左下角的 Option 键碰歪了,现在每次按 Cmd+R 都带出 Spotlight。后来我写了个伪装成 iOS 系统相册插件的逆向工具链,偷偷跑在越狱机上抓包,结果发现微信根本没传账单明细,只传了个 `wx_XXXXX_YYYY.json` 加密 blob,密钥藏在他们自己的 `libWeChatWalletCore.so` 里——这玩意儿 iOS 根本打不开。所以最后上线的所谓「一键导入」,其实是让用户自己截十张图,OCR 后人工校对三遍。这个我说不准,因为 PM 后来把需求文档删了,只留个钉钉截图,说“以实际交付为准”。 改版上线翻车那天,安卓端更狠:华为鸿蒙 4.2 里 WebView 渲染完首页,突然整个 Activity 被 `ActivityThread$H` 把 `mH` handler 的 callback queue 塞爆了,不是 ANR,是主线程消息循环卡死,但日志里没报错,只看到 `Choreographer: Skipped 42 frames!`。我们查到第十八毫秒,发现是 `ViewRootImpl#performTraversals()` 里调 `dispatchDetachedFromWindow()` 时,有个自定义 ViewGroup 在 `onDetachedFromWindow()` 里又反向 `post()` 了一个 `Runnable`,正好卡进当前 frame 的 commit 队列尾巴——它不是并发问题,是鸿蒙改了 `Choreographer#postCallbackDelayedInternal()` 的入队顺序,把 `CALLBACK_ANIMATION` 和 `CALLBACK_TRAVERSAL` 的优先级调反了。后端老周当时在 Slack 上发了个 GIF,是只猫踩奶,配文“你们在忙,我在呼吸”。我盯着他那个 GIF,帧率不对,第一帧多停了三百七十二个节点的渲染时间,跟我们卡顿那一下完全同步。 说到节点……上周代码审查 PR #883,我批注了句“别用 `view.setTranslationX(0)` 来重置动画状态”,结果实习生小王回:“文档里写着推荐这么用啊”。我懒得截图源码,就直接翻了 AOSP master 分支,找到 `View.java` 第 11409 行——注释里明晃晃写着 `// FIXME: This does not account for pending animations with non-zero translation`. 我删掉回复,重新打字:“你看 FIXME。” 中间外卖汤洒了一滴,在我 Surface Pro 的 Type Cover 左上角,浸湿了 Esc 键边的塑料膜,现在按 Esc 会轻微粘滞。这种东西编不出来,对吧?
文章配图,仅供参考 其实真没那么玄乎。三年里七次跃迁?全是被逼的。第一次是放弃 Hybrid,把所有 H5 支付页全砍了,纯原生重写,省下三十五兆包体;第二次是干掉第三方推送 SDK,手搓 Firebase + HMS 推送融合层,推送到达率从百分之六十八拉到九十一;第三次是把 Realm 换成 SQLite+Room+自研 WAL 日志合并器,冷启速快了一秒二,但代价是 DB 迁移脚本写了二十三版;第四次最蠢,为保 Apple Watch 独立安装,硬搞了个 Watch-only Extension 用纯 Core Data 存账单摘要,结果 WatchOS 10.1 一出,本地 NSPersistentContainer 的 `persistentStoreDescriptions` 在后台初始化直接卡死——没人告诉你 watchOS 里 `.sqlite` 文件锁不是 POSIX 的。这事我没声张,自己用 `NSFileCoordinator` 绕开了,但到现在也不敢合进主干。第五次……算了,第五次跟 UI 动效师吵的架,他说“交互动画必须丝滑”,我甩给他一段 `CADisplayLink` 帧率采样数据,峰值抖动 ±18ms,他看了三秒,默默关掉了 Framer Live Preview。第六次是 Flutter Embedding V2 过渡期,我们混编项目里同时跑着三个 FlutterEngine 实例,内存扛不住,最后靠 Runtime Method Swizzling 黑掉 `_flutter_engine_init`,强行把 Engine 实例池降到一个——这事不能细说,反正线上跑着,没 crash,也没提 PR。第七次?就是去年十二月那次。崩溃率上来后,我让测试把崩溃设备列表导出 Excel,筛出所有 iPhone 15 Pro Max 用户,再过滤出屏幕亮度 > 75% 的样本——一共四百六十七台。我写了个 Python 脚本,把 CrashReporter 符号化后的时间戳、`kern.boottime` 和 `mach_absolute_time()` 对齐,发现它们崩溃前平均都在执行 `AVCaptureSession startRunning` 后的 2.3–2.7 秒之间。再一查,这些设备全部开了“实时语音转文字”系统级开关。原来我们的记账页里那个小话筒按钮,触发的是 `AVAudioSession setMode:AVAudioSessionModeMeasurement`,而 iOS 17.2 里 Measurement 模式和实时字幕存在共享音频管道资源竞争。解决方案?把话筒按钮从 AVFoundation 换成 Speech Framework 的 `SFSpeechRecognizer requestAuthorization`,授权失败时默认隐藏。现在它不崩溃了,但录音延迟多了四百毫秒。优胜劣汰?对。活下来的不是技术最强的方案,是刚好没撞上苹果半夜改内核调度策略的那一个。 哦对,昨天早上茶水间碰到测试小林,她问我:“你是不是偷偷改了灰度开关逻辑?” 我说没。她眨眨眼:“那为啥我拿 QA 证书打包的测试包,启动速度比正式版快?” 我愣了两秒才想起来——上周五下班前,我把 `DEBUG=1` 的构建标记忘删了,CI 流水线自动把 debug 版上传 TestFlight,结果一百三十七个内测用户用了半个月 debug build。我现在没动,就让它跑着。反正 Crashlytics 也报不到 debug symbol,他们以为这是新优化。键盘那个 Option 键我还没收正,有点卡手,但已经习惯抬手腕绕开了。 今晚得重看一遍 `WKWebViewConfiguration` 初始化流程。或者先去把桌上那盒没拆的燕麦奶扔了——放了四天,盖子鼓起来了。我不确定它还能不能喝,但我确定,如果明天早上的 CI 再爆 `No space left on device`,我就把 Jenkins slave 全切到 M2 Mac Mini 上,哪怕 build time 多四十秒。 (编辑:站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |


Go驱动日志智能分析,赋能站长技术跃迁
Go驱动日志智能分析,赋能站长技术跃迁
Go驱动日志智能分析,赋能站长技术跃迁
Go驱动日志智能分析,赋能站长技术跃迁
Go赋能站长:原生工程师的跨界技术启迪
Go赋能站长:原生工程师的跨界技术启迪
