从“做出来”到“做得好”:技术探索与实践的必要性

半个架构师
2025-06-22 04:22
阅读 329

在iOS开发这条路上,我走过了五年。从最初面对Storyboard时的手忙脚乱,到现在参与架构设计、性能优化和项目拆分重构,这一路走来最大的感悟就是:技术的价值不在于你会不会写代码,而在于你是否愿意去“深挖”和“验证”。

今天我想分享一个真实项目的经历——我们是怎么从一次简单的功能迭代,最终演进成一场对架构和技术深度探索的旅程的。希望通过这个故事,能让你理解为什么我们需要不断进行技术探索与实践。


背景介绍:一场普通的模块重构

背景介绍:一场普通的模块重构

故事发生在我参与的一个中型社交类App改版项目上。我们团队负责用户发布内容的核心流程(俗称“发帖页”),这部分在整个App的DAU中占比非常大,但原有页面结构已经积攒了多年的技术债:大量冗余逻辑、强耦合的UI组件、复杂的生命周期管理,以及越来越多难以追踪的状态变化。

当时的需求其实很“简单”:

将原本只能上传一张图片的发帖页,升级为支持多张图片选择并可排序的功能,并接入新的图片上传链路以提升上传速度。

听起来是标准的UI/UX迭代?但我隐隐感觉这背后藏着更多技术挑战。


遇到的问题:复杂状态难维护

遇到的问题:复杂状态难维护

在接手后我迅速发现几个棘手问题:

  1. 数据状态难以统一管理
    • 原有代码中,图片数据在View层、ViewModel层之间来回传递,部分逻辑甚至直接暴露给ViewController处理。
  2. 交互细节繁杂
    • 新增图片拖动排序、实时缩略图预览、删除动画等,都让视图响应变得复杂。
  3. 上传流程不可控
    • 图片上传过程分散在多个地方,失败重试机制没有统一接口。
  4. 测试难度极高
    • 业务逻辑嵌套严重,单元测试几乎为零覆盖。

这些问题让我意识到:如果只是单纯加个UICollectionView完成功能,未来一定会埋下更大的隐患。

于是,我决定把这次重构当作一次全面的技术探索机会


探索之路:从架构开始下手

探索之路:从架构开始下手

系统架构设计-2

我花了一周时间梳理现有代码,画出了整个发帖页的数据流向图,发现了以下几个关键点:

  • ViewModel职责混乱
  • 多处重复的网络请求代码
  • UI事件流与数据流交织不清
  • 单一文件代码量超过5000行(你没看错)

于是我提出了几个核心目标:

  • 使用Redux-like状态管理方式,统一数据状态
  • 引入独立的ImageUploader模块,解耦上传逻辑
  • 抽离出ContentEditorManager作为状态中枢
  • 实现基础的单元测试覆盖率
  • 支持未来的可扩展性(比如加入视频、语音等内容类型)

这些想法起初遭到了一些质疑:“有必要为了这么一个页面搞这么多吗?”但当我展示出现有的代码质量报告之后,大家达成了共识:再不做技术改造,成本将越来越高。


我们做了什么:技术方案选型与实现思路

1. 状态管理 —— 自研轻量StateContainer

我们放弃了当时社区流行的ReSwift或Combine+Swinject这类重型框架(主要是担心学习成本),而是参考Redux思想搭建了一个简易的单向状态容器系统。

主要设计如下:

enum ContentAction {
    case addPhoto(UIImage)
    case removePhoto(index: Int)
    case reorderPhoto(from: Int, to: Int)
    case uploadStarted
    case uploadFailed(error: Error)
}

struct ContentState {
    var photos: [PhotoModel] = []
    var isUploading: Bool = false
    // 其他相关状态字段...
}

class ContentStore {
    private(set) var state: ContentState {
        didSet {
            dispatchDidChange()
        }
    }

    private let reducer: (ContentState, ContentAction) -> ContentState
    private var observers: [ObjectIdentifier: (ContentState) -> Void] = [:]

    init(initialState: ContentState, reducer: @escaping (ContentState, ContentAction) -> ContentState) {
        self.state = initialState
        self.reducer = reducer
    }

    func dispatch(_ action: ContentAction) {
        state = reducer(state, action)
    }

    func addObserver<O: AnyObject>(_ object: O, handler: @escaping (O, ContentState) -> Void) {
        let key = ObjectIdentifier(object)
        observers[key] = { [weak object] state in
            guard let obj = object else { return }
            handler(obj, state)
        }
    }

    func removeObserver<O: AnyObject>(_ object: O) {
        let key = ObjectIdentifier(object)
        observers.removeValue(forKey: key)
    }

    private func dispatchDidChange() {
        for handler in observers.values {
            handler(state)
        }
    }
}

这套系统虽然很简单,但我们通过清晰的Action定义,实现了所有图片相关的操作都被统一封装,并且状态变更具有可预测性和追溯能力。

2. 拖拽排序与性能优化

新需求包含图片拖动排序功能。原本打算用UICollectionViewDragDelegate + UICollectionViewDropDelegate,结果测试下来发现:

  • 在iPad多任务环境下拖拽性能下降明显
  • 多个CollectionView嵌套时容易触发冲突手势
  • 动画不流畅,特别是快速拖动时卡顿感严重

最后我们调研了社区库,在综合对比后使用了InteractMove(非广告,是基于UIViewPropertyAnimator封装的一个开源库),实现了更灵活的交互控制,同时保持帧率稳定在60FPS以上。

核心代码片段:

let interact = InteractMove()
interact.delegate = self
collectionView.addGestureRecognizer(interact)

// MARK: - InteractMoveDelegate 
func moveItemBegin(in view: UIView, at index: IndexPath?, touchLocation: CGPoint) {
    // 开始拖动
    if let index = index {
        store.dispatch(.reorderStart(index))
    }
}


![技术应用场景-1](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025062204/3078854b-cef5-4bc3-a382-7993dccef6f1.jpg)


func moveItemMoving(in view: UIView, movingIndex: IndexPath, to targetIndex: IndexPath, touchLocation: CGPoint) {
    collectionView.performBatchUpdates({
        let item = photoDataSource.remove(at: movingIndex.row)
        photoDataSource.insert(item, at: targetIndex.row)
        collectionView.moveItem(at: movingIndex, to: targetIndex)
    }, completion: nil)
}

func moveItemEnd(in view: UIView, from sourceIndex: IndexPath, to targetIndex: IndexPath) {
    store.dispatch(.reorderPhoto(from: sourceIndex.row, to: targetIndex.row))
}

3. 图片上传服务抽离

之前上传图片的逻辑散落在多个VC中,每次调整上传策略都要修改N个地方。这次我们封装了一个独立的ImageUploader模块:

protocol ImageUploadable {
    func upload(image: UIImage, forKey key: String, progress: ((Double) -> Void)?, completion: @escaping (Result<String, Error>) -> Void)
}

class CloudinaryUploader: ImageUploadable {
    func upload(...) {
        // 实现具体的上传逻辑
    }
}

class ImageUploader {
    private let uploader: ImageUploadable
    
    init(uploader: ImageUploadable) {
        self.uploader = uploader
    }

    func startUploads(images: [UIImage], keys: [String], completion: @escaping ([String]) -> Void) {
        let group = DispatchGroup()
        var uploadedURLs: [String] = []

        zip(images, keys).forEach { image, key in
            group.enter()
            uploader.upload(image: image, forKey: key) { result in
                switch result {
                case .success(let url):
                    uploadedURLs.append(url)
                case .failure:
                    break
                }
                group.leave()
            }
        }

        group.notify(queue: .main) {
            completion(uploadedURLs)
        }
    }
}

这样的封装不仅统一了上传接口,也为将来换用AWS S3或其他CDN留下了扩展空间。


踩坑经验:那些我们没想到的角落

内存暴涨 —— 因为Thumbnail缓存没控制住

原本我们在UICollectionViewCell里直接加载全尺寸图片用于预览,结果导致内存飙升。后来加上了异步压缩和缓存策略才解决。

解决方案:

  • 在图片添加时即异步生成thumbnail
  • 缓存在自定义的ImageCache类中,限制最大数量
  • 使用NSCache而非NSDictionary避免内存泄漏
let thumbnailSize = CGSize(width: 128, height: 128)
let scaledImage = originalImage.resize(to: thumbnailSize, scaleMode: .aspectFill)

图片方向翻转 —— EXIF信息导致显示异常

某些iPhone前置摄像头拍照的图片方向会因为EXIF被自动旋转,但在UIImageView中又不会自动处理。这个问题一度让产品以为是我们代码逻辑错误。

解决办法:

  • 在加载图片时统一修正方向
extension UIImage {
    func fixedOrientation() -> UIImage? {
        guard imageOrientation != .up else { return self }
        UIGraphicsBeginImageContextWithOptions(size, false, scale)
        draw(in: CGRect(origin: .zero, size: size))
        let normalizedImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return normalizedImage
    }
}

效果总结:不只是功能上线

重构完成后,我们做了以下评估:

指标 改造前 改造后
页面加载帧率 ~52 FPS ~60 FPS
上传成功率 92% 97.5%
新人上手时间 2周+ 3天以内
单元测试覆盖率 <10% >70%
文件行数 5132行 分为6个小文件,合计3156行

更重要的是:

  • 产品经理提出的“明天要加个视频贴纸”需求我们只用了半天完成接入;
  • 后续又有两个团队复用了我们的状态管理模块;
  • 发帖页成为新入职同学的“教学样板”,不再需要解释“这段代码是十年前写的”。

经验总结:为什么要做技术探索?

回顾这次经历,我特别想告诉刚入行的小伙伴们一句话:

“做出来的代码叫交付,做好的代码叫工程。”

技术探索从来不是“炫技”,而是为了解决真实世界中的问题。以下是我在这些年里积累的一些体会,希望能帮到你们:

✅ 不要怕“过度设计”

有时候我们总被“够用就行”这句话束缚。但实际上,“够用”的标准是要结合项目周期、人力配置和长期维护成本来看的。如果你知道某个功能会在未来频繁改动,提前做好抽象和封装是非常值得的。

✅ 工具服务于目的

在技术选型的时候不要盲目追求热门框架或最先进模式。适合当前团队能力和项目节奏的才是最好的。哪怕是自己动手写一个小工具,只要能解决问题、保证可控性,都是合理的做法。

✅ 架构是一种思维方式

真正的架构能力不在于用了多少协议或者依赖了多少外部库,而在于能否清晰地看到数据的流动路径和边界。哪怕是一个小功能,也可以具备良好的架构意识。

✅ 性能优化不是万能药

我们曾经陷入一种误区:一味追求帧率数字、代码效率。但实际使用场景下,用户感受远比测试数据重要。比如在图片上传时,即使后台稍微慢一点,但给出合理的反馈提示,用户也不会觉得卡顿。

✅ 文化比技术更重要

技术探索要能落地,需要有一个允许犯错、鼓励尝试的环境。我们组内有个不成文的规定:每次迭代后都要开个小复盘,不论成败都聊聊“哪些可以做得更好”。久而久之,团队成员都养成了主动思考的习惯。


最后的一点建议

如果你正处在这样的心态中:

“我就是个普通开发者,没必要研究太多底层原理。”
或者
“我现在做的项目太简单,学不到东西。”

我想告诉你:每一个伟大的工程,都始于平凡的一行代码。

技术探索并不是遥不可及的大词,它可能就藏在一个小小的函数封装中,也可能是一段被优化的内存占用日志里。

我始终相信一句话:

“写得好,不如问得深;走得快,不如看得远。”

希望这篇来自实战的真实分享,能给你带来一些启发。也欢迎大家留言交流,一起聊聊你在开发中遇到的那些“技术探索时刻”。

评论 0

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