浅谈技术探索与实践:在复杂业务中找到技术落地的节奏

代码里的烟火
2025-06-16 02:57
阅读 562

作为一名iOS工程师,我从业5年,经历了从初入职场的迷茫到如今对技术选型、架构设计和团队协作都有一定思考的成长过程。今天想借这篇文章聊聊我在实际项目中遇到的技术挑战以及我的应对思路和实践总结。


一、背景引入:一个需求不明确的“新功能”开发

一、背景引入:一个需求不明确的“新功能”开发

事情发生在两年前,我所在的公司接了一个大客户的需求,核心是要在APP内实现一套“实时协同浏览”的能力。简单说,就是用户可以邀请另一个用户共同浏览同一个页面,两人的操作(比如滑动、点击)要能同步反映在对方设备上。

听起来好像不算太难?但问题在于这个功能并不是完全独立的新模块,而需要嵌入到现有复杂的电商详情页结构里,涉及大量的UI交互控制和数据状态管理。更头疼的是——需求文档只有3页PPT,大部分内容还都是“类似某App的双人浏览”。

我们当时的团队有三位iOS同学,整体工期给了两周时间。当时我心里就隐隐觉得,“这怕是得踩坑了。”


二、问题浮现:看似简单的功能背后的技术挑战

二、问题浮现:看似简单的功能背后的技术挑战

真正开始编码后才发现,问题远比预估复杂得多:

1. 页面结构过于复杂,难以统一操控

电商详情页是一个高度聚合组件,融合了Banner、商品信息、参数、评价、底部工具条等多个子模块。每个模块都可能有自己的响应逻辑和交互方式,甚至有些页面还会根据网络环境加载不同的内容区块。

我们最初尝试用KVO来监听UIScrollView的位置变化,试图将滚动位置同步过去,但很快发现:不同页面的UIScrollView层级深度不一样,部分页面甚至使用UICollectionView或者UITableView嵌套。

2. 状态不同步导致操作混乱

当A端滑到了某个Tab时,B端如果还在上一个Tab,那点击事件或滚动行为就会出错。这种跨界面的状态不一致直接导致体验崩塌,用户根本无法正常使用。

3. 多设备间的通信延迟影响体验

由于初期选用的是WebSocket进行消息传递,但在弱网环境下经常出现延迟,导致操作同步感差,甚至有时会因为消息堆积造成视觉上的抖动。

这些问题叠加在一起,让原本以为两周就能搞定的功能变得遥遥无期。


三、我们的解决方案:重新定义“操作同步”的本质

三、我们的解决方案:重新定义“操作同步”的本质

面对这些挑战,我和团队决定先停下来,重新梳理整个场景的本质:所谓“操作同步”,其实是两个设备对同一个“抽象页面状态”的认知保持一致。

这给了我很大启发:与其硬性去监听所有交互动作,不如构建一个状态机模型,让两个客户端共享一个“当前状态描述”,并各自负责根据该状态渲染正确的UI和交互行为。

1. 构建统一的状态协议

我们设计了一种JSON格式的“页面状态协议”,它包括:

  • 当前展示的Tab索引
  • 列表滚动位置(contentOffset)
  • 某些特定按钮的选中状态(如加入购物车与否)
  • 自定义标记位(mark)
{
    "tabIndex": 1,
    "contentOffset": {
        "x": 0,
        "y": 450
    },
    "addToCartSelected": true,
    "marks": {
        "video_playing": true,
        "price_sheet_shown": false
    }
}

每次本地发生任何会影响页面状态的操作,我们都主动上报给服务端,并通过Socket广播给另一方。这样无论哪一方触发的交互,只要状态更新,另一侧就能立即感知并调整UI。

2. 使用Redux思想管理本地状态

为了更好地支持状态驱动渲染,我们在项目中引入了轻量级的状态管理方案——基于Swift的Combine框架实现了一个简易的Store机制。

我们定义了一个顶层状态对象:

struct PageState: Equatable {
    var tabIndex: Int
    var contentOffset: CGPoint
    var addToCartSelected: Bool
    var marks: [String: Bool]
}

并通过订阅的方式监听状态变更:

class ViewController: UIViewController {
    
    private var cancellables = Set<AnyCancellable>()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        AppState.shared.$pageState
            .receive(on: RunLoop.main)
            .sink { [weak self] state in
                self?.render(with: state)
            }
            .store(in: &cancellables)
    }
    
    private func render(with state: PageState) {
        // 更新 Tab
        tabController.select(index: state.tabIndex)
        // 设置滚动位置
        scrollView.setContentOffset(state.contentOffset, animated: false)
        // 更新按钮状态等
        cartButton.isSelected = state.addToCartSelected
    }
}

这种方式的好处在于,无论是本地操作还是远程同步过来的状态变更,最终都会收敛到同一个入口处理。


四、代码实践:关键实现细节

1. WebSocket连接的封装

我们采用了Starscream作为WebSocket库,做了封装以支持自动重连、心跳包检测等功能。

final class SocketManager: NSObject, ObservableObject {
    static let shared = SocketManager()
    
    private var socket: WebSocket?
    private var reconnectTimer: Timer?
    
    func connect(to urlString: String) {
        guard let url = URL(string: urlString) else { return }
        var request = URLRequest(url: url)
        request.timeoutInterval = 5
        
        socket = WebSocket(request: request)
        socket?.delegate = self
        socket?.connect()
    }
    
    func sendMessage(_ message: String) {
        socket?.write(string: message)
    }
}

extension SocketManager: WebSocketDelegate {
    func didReceive(event: WebSocketEvent, client: WebSocket) {
        switch event {
        case .text(let text):
            if let state = parseState(from: text) {
                AppState.shared.updateRemoteState(state)
            }
        case .connected:
            startHeartbeat()
        case .disconnected:
            restartReconnectTimer()
        default:
            break
        }
    }
    
    // ...
}

2. 本地状态更新与同步

每当用户有交互行为时,我们不是直接修改页面元素,而是先修改全局状态:

func userDidTapAddToCart() {
    let newState = AppState.shared.pageState.copyAndMutate {
        $0.addToCartSelected = !$0.addToCartSelected
    }
    AppState.shared.update(newState)
    
    sendStateToRemote(newState)
}

其中AppState.shared.update()方法内部会触发Combine的发布机制,从而引起UI刷新;而sendStateToRemote()则把最新状态发往后台做广播。


五、踩坑经验:那些差点让我崩溃的小事

坑点一:多个ScrollView导致offset不准

我们最初只监听主ScrollView的偏移量,后来发现当某个Tab是CollectionView的时候,里面的Cell也可能有自己的滚动区域。如果不做区分,会出现明明已经切换了Tab,却还在继续发送之前页面的offset值。

解决方案是在监听前判断当前是否为主控容器:

scrollView.addObserver(self, forKeyPath: #keyPath(UIScrollView.contentOffset), 
                      options: [.new, .old], context: nil)

override func observeValue(forKeyPath keyPath: String?, of object: Any?,
                            change: [NSKeyValueChangeKey : Any]?,
                            context: UnsafeMutableRawPointer?) {
    guard keyPath == #keyPath(UIScrollView.contentOffset),
          let scrollView = object as? UIScrollView else {
        return
    }
    
    // 判断当前是否属于主控ScrollView
    if currentContentView != .collectionView { return }
    
    AppState.shared.updateContentOffset(scrollView.contentOffset)
}

坑点二:心跳频率设置不合理导致频繁断连

起初我们设置心跳间隔为5秒,但在某些地区WiFi信号较弱时,服务器误认为客户端掉线,频繁触发重连。

我们后来改成了动态调节机制,根据RTT(往返时延)计算合理的超时时间和重试次数。


六、效果总结:上线后的收益与反思

最终我们用了整整三周才完成这一功能,比原计划多了一周。但这套方案上线后表现稳定,具体收益如下:

  • 实现了全平台两端同步,且状态一致性高;
  • 优化了弱网环境下的体验,消息丢失率下降90%以上;
  • 在后续迭代中,新增状态字段只需修改协议定义,无需重构整体结构;
  • 老功能复用也变得更方便,很多状态控制可以直接借鉴此机制。

最重要的是,这套方案让我们建立起了一套“状态优先”的思维方式,后续开发其他联动功能也更加得心应手。


七、一点心得:技术探索与实践之间的平衡

五年iOS开发下来,最大的感悟就是:“技术没有高低之分,能解决问题的就是好技术。”

刚入行时我很迷恋一些“高级架构”,比如VIPER、MVVM,总想着用它们去重构项目。但后来发现,很多时候过度设计反而拖慢了进度,甚至适得其反。

现在我更倾向于“实用主义+渐进式优化”的策略:

  • 从需求出发:理解清楚真实痛点再做技术选型;
  • 不做过度设计:尤其是面对不确定性的需求时,优先验证可行性;
  • 拥抱小改动:有时候一个小小的中间层封装就能带来很大的灵活性;
  • 持续重构:不要一开始就追求完美,先把问题解决,再逐步打磨结构;
  • 沟通永远是第一位的:跟产品聊清楚边界,和同事统一好节奏。

另外,我觉得现在的技术生态确实越来越复杂了,但归根结底还是要回归到“能落地”的东西。就像这次我们选择不用RxSwift而选择了原生Combine一样——虽然性能差不多,但上手成本更低,团队成员更快适应,这就是适合我们项目的选择。


八、写在最后:技术人的成长是一场长期修行

回望这几年的工作经历,其实每一次技术决策的背后,都是一个个具体问题推动的。正是这些看似微不足道的探索和实践,慢慢塑造了我的思维方式和技术判断力。

如果你也在做iOS开发,或许你也会遇到类似的困境:如何在有限时间内做出合理的技术选型?如何在复杂业务中找到清晰的技术路径?

我的建议是:不要急于下结论,多问几个“为什么”,多看几个“怎么做”,然后大胆试一试。

希望这篇文章能为你提供一些实战参考。如果有相关问题欢迎留言交流,一起进步!


作者:一只热爱编码的老程序员 🐱
坐标上海,iOS开发爱好者,致力于打造更优雅的交互体验

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝