从单体到子壳:一套「对标亿级 DAU App」的 iOS 架构实战 Demo

GitHub 源码

移动 App 做到一定规模以后,你会发现:

  • 加一个小需求,要改一堆地方;
  • 任何一个业务改动,都要整个 App 全量回归;
  • 编译时间、打包时间越来越长,CI 队列排队;
  • 想抽组件、做中台,结果越抽越乱,组件之间互相依赖;
  • 想给某个业务线做「独立壳工程」,结果带上半个世界的依赖。

所以,大厂的 App 架构演进路径几乎都很像:

单体工程 → 多模块 → 单仓多组件(subspec)→ 业务接口层 / 服务层

→ 中台化(Infra / 设计系统 / 基础服务)→ 子壳工程 → 自动化治理 / lint

这篇文章基于抖音的《抖音 iOS 工程架构演进》那篇文章,做了一套可跑的 Demo 工程,尽量向「阶段三 + 阶段四」靠拢,包括:

  • 单仓多组件(pod + subspec)
  • 业务接口层(ModuleInterface)
  • 服务层接口 + 多实现(ServiceInterface:Online / Mock)
  • 中台层(Infra / UIResources / UITheme)
  • 子壳工程(SearchShell / FeedShell)
  • 启动框架(LaunchKit + BootTask + ServiceRegistry + Router)

希望这套 Demo 能帮你把文章里的概念变成 真正能跑的工程,也让你在自己项目里更容易落地类似的架构。


1. 整体目标:我们到底要搭一套什么样的架构?

目标拆一下:

  1. 单仓多组件

    • 一个仓库,多个 Pod / 多个 subspec
    • 每个业务线仓库下有:ModuleInterface / Service / BizUI / Model / Basic / Impl
  2. 业务域拆分

    • 两个核心业务:Feed / Search
    • 支持跨业务调用(Feed 调 Search)
    • 调用通过 接口层 + 服务层 完成
  3. 两层接口

    • 业务接口层(ModuleInterface):对外暴露入口能力
    • 服务层接口(ServiceInterface):封装公共业务逻辑,可替换底层能力(Online/Mock)
  4. 中台层

    • Infra:用户服务 / 网络服务
    • UIResources:公共资源(icon / 图片 bundle)
    • UITheme:统一主题(颜色 / 字体 / BaseVC / BaseNav)
  5. 子壳工程

    • SearchShellApp:专门给 Search 业务线用的壳工程
    • FeedShellApp:专门给 Feed 业务线用的壳工程
    • 不同壳可以选择不同 Service 实现(Mock / Online),最小化依赖
  6. 启动框架

    • LaunchKit:BootPhase + BootTask
    • ServiceRegistry:服务总线
    • Router:字符串路由(douyin://searchdouyin://feed

2. 工程结构总览

目录结构大致如下(省略部分细节):

text 复制代码
DouyinArchDemo/
├── Podfile
├── LaunchKit/               # 启动框架 & ServiceRegistry & Router
│   ├── AWELaunchKit.podspec
│   └── Sources/
├── Infra/                   # 中台逻辑:用户 / 网络
│   ├── AWEInfra.podspec
│   └── Sources/
├── UIResources/             # 中台资源:通用 icon 等
│   ├── AWEUIResources.podspec
│   ├── Assets/
│   │   └── AWEUIIcons.xcassets
│   └── Sources/
├── UITheme/                 # 中台主题:颜色、字体、BaseVC、BaseNav
│   ├── AWEUITheme.podspec
│   └── Sources/
├── Modules/
│   ├── Search/              # 搜索业务域
│   │   ├── AWESearch.podspec
│   │   ├── ModuleInterface/
│   │   ├── Basic/
│   │   ├── Model/
│   │   ├── Service/
│   │   ├── BizUI/
│   │   └── Impl/
│   └── Feed/                # Feed 业务域
│       ├── AWEFeed.podspec
│       ├── ModuleInterface/
│       ├── Service/
│       ├── BizUI/
│       └── Impl/
└── Apps/
    ├── DouyinHostApp/       # 宿主 App
    ├── SearchShellApp/      # 搜索子壳
    └── FeedShellApp/        # Feed 子壳

3. 启动框架:LaunchKit + BootTask + ServiceRegistry + Router

3.1 BootPhase & BootTask:让启动过程可编排

swift 复制代码
public enum BootPhase: Int {
    case preInfra    = 0
    case infra       = 1   // 中台服务初始化(User / Network / Theme)
    case bizRegister = 2   // 各业务注册入口、路由、服务
    case postUI      = 3   // UI 之后的收尾
}

open class BootTask {
    public let phase: BootPhase
    public let priority: Int
    
    public init(phase: BootPhase, priority: Int = 0) {
        self.phase = phase
        self.priority = priority
    }
    
    open func execute(completion: @escaping () -> Void) {
        completion()
    }
}

3.2 LaunchKit:按 Phase + Priority 执行任务

swift 复制代码
public enum LaunchKit {
    private static var tasks: [BootTask] = []
    private static var hasRunPhases = Set<BootPhase>()
    
    public static func register(task: BootTask) {
        tasks.append(task)
    }
    
    public static func run(phase: BootPhase, completion: @escaping () -> Void) {
        guard !hasRunPhases.contains(phase) else {
            completion()
            return
        }
        hasRunPhases.insert(phase)
        
        let phaseTasks = tasks
            .filter { $0.phase == phase }
            .sorted { $0.priority > $1.priority }
        
        runTasksSequentially(phaseTasks, index: 0, completion: completion)
    }
    
    private static func runTasksSequentially(_ tasks: [BootTask],
                                             index: Int,
                                             completion: @escaping () -> Void) {
        guard index < tasks.count else {
            completion()
            return
        }
        let task = tasks[index]
        task.execute {
            runTasksSequentially(tasks, index: index + 1, completion: completion)
        }
    }
}

3.3 ServiceRegistry:统一管理服务实例

swift 复制代码
public final class ServiceRegistry {
    public static let shared = ServiceRegistry()
    
    private var store: [String: Any] = [:]
    private let lock = NSLock()
    
    private init() {}
    
    public func register<T>(_ type: T.Type, impl: T) {
        let key = String(reflecting: type)
        lock.lock()
        store[key] = impl
        lock.unlock()
    }
    
    public func resolve<T>(_ type: T.Type) -> T? {
        let key = String(reflecting: type)
        lock.lock()
        let value = store[key] as? T
        lock.unlock()
        return value
    }
}

3.4 Router:统一路由入口

swift 复制代码
public final class Router {
    public static let shared = Router()
    public typealias RouteHandler = (_ params: [String: Any]?) -> UIViewController?
    
    private var routes: [String: RouteHandler] = [:]
    
    private init() {}
    
    public func register(_ path: String, handler: @escaping RouteHandler) {
        routes[path] = handler
    }
    
    public func open(_ path: String,
                     from navigationController: UINavigationController?,
                     params: [String: Any]? = nil,
                     animated: Bool = true) {
        guard let vc = routes[path]?(params) else { return }
        navigationController?.pushViewController(vc, animated: animated)
    }
}

4. 中台层:Infra / UIResources / UITheme

4.1 Infra:用户 & 网络

swift 复制代码
public protocol UserService {
    var isLoggedIn: Bool { get }
    var userName: String? { get }
    func login(name: String, completion: @escaping (Bool) -> Void)
}

public final class DefaultUserService: UserService {
    public private(set) var isLoggedIn = false
    public private(set) var userName: String?
    
    public func login(name: String, completion: @escaping (Bool) -> Void) {
        isLoggedIn = true
        userName = name
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
            completion(true)
        }
    }
}
swift 复制代码
public protocol NetworkClient {
    func request(path: String,
                 query: [String: String],
                 completion: @escaping (Result<[String], Error>) -> Void)
}

public final class StubNetworkClient: NetworkClient {
    public func request(path: String,
                        query: [String : String],
                        completion: @escaping (Result<[String], Error>) -> Void) {
        let keyword = query["q"] ?? ""
        let results = (1...5).map { "Result \($0) for \(keyword)" }
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
            completion(.success(results))
        }
    }
}
swift 复制代码
public final class InfraBootTask: BootTask {
    public override func execute(completion: @escaping () -> Void) {
        ServiceRegistry.shared.register(UserService.self, impl: DefaultUserService())
        ServiceRegistry.shared.register(NetworkClient.self, impl: StubNetworkClient())
        completion()
    }
}

public func registerInfraBootTasks() {
    LaunchKit.register(task: InfraBootTask(phase: .infra, priority: 100))
}

4.2 UIResources:通用资源 Pod

swift 复制代码
public enum AWEUIResources {
    private static var bundle: Bundle = {
        let bundle = Bundle(for: BundleToken.self)
        if let url = bundle.url(forResource: "AWEUIResources", withExtension: "bundle"),
           let resourceBundle = Bundle(url: url) {
            return resourceBundle
        }
        return bundle
    }()
    
    public static var navBackIcon: UIImage? {
        UIImage(named: "ic_nav_back", in: bundle, compatibleWith: nil)
    }
}

private final class BundleToken {}

业务模块的私有资源也通过各自的封装访问,例如:

swift 复制代码
public enum SearchResources {
    private static var bundle: Bundle = {
        let bundle = Bundle(for: BundleToken.self)
        if let url = bundle.url(forResource: "AWESearchBizUIResources", withExtension: "bundle"),
           let resourceBundle = Bundle(url: url) {
            return resourceBundle
        }
        return bundle
    }()
    
    public static var searchTabIcon: UIImage? {
        UIImage(named: "ic_search_tab", in: bundle, compatibleWith: nil)
    }
}

private final class BundleToken {}

4.3 UITheme:统一样式 + BaseVC / BaseNav

swift 复制代码
public enum AWEColor {
    public static var navBackground: UIColor { .white }
    public static var navTitle: UIColor { .black }
    public static var navTint: UIColor { .systemBlue }
    public static var viewBackground: UIColor { .systemBackground }
}

public enum AWEFont {
    public static var navTitle: UIFont {
        .systemFont(ofSize: 17, weight: .semibold)
    }
}
swift 复制代码
public final class AWENavigationController: UINavigationController {
    public override func viewDidLoad() {
        super.viewDidLoad()
        
        let appearance = UINavigationBarAppearance()
        appearance.configureWithOpaqueBackground()
        appearance.backgroundColor = AWEColor.navBackground
        appearance.titleTextAttributes = [
            .foregroundColor: AWEColor.navTitle,
            .font: AWEFont.navTitle
        ]
        
        navigationBar.standardAppearance = appearance
        navigationBar.scrollEdgeAppearance = appearance
        navigationBar.compactAppearance = appearance
        navigationBar.tintColor = AWEColor.navTint
    }
}
swift 复制代码
open class AWEBaseViewController: UIViewController {
    open override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = AWEColor.viewBackground
        configureNavigationBar()
    }
    
    open func configureNavigationBar() {
        if let nav = navigationController,
           nav.viewControllers.first !== self {
            navigationItem.leftBarButtonItem = UIBarButtonItem(
                image: AWEUIResources.navBackIcon,
                style: .plain,
                target: self,
                action: #selector(onBack)
            )
        }
    }
    
    @objc open func onBack() {
        navigationController?.popViewController(animated: true)
    }
}

5.1 业务接口层(ModuleInterface)

swift 复制代码
public protocol SearchEntryProtocol: AnyObject {
    func makeSearchViewController() -> UIViewController
}

5.2 服务层接口(ServiceInterface)

swift 复制代码
public struct SearchResult {
    public let title: String
}

public protocol SearchServiceProtocol {
    func search(keyword: String,
                completion: @escaping (Result<[SearchResult], Error>) -> Void)
}

Online / Mock 两种实现

swift 复制代码
public final class OnlineSearchService: SearchServiceProtocol {
    private let network: NetworkClient
    
    public init(network: NetworkClient) {
        self.network = network
    }
    
    public func search(keyword: String,
                       completion: @escaping (Result<[SearchResult], Error>) -> Void) {
        network.request(path: "/search", query: ["q": keyword]) { result in
            switch result {
            case .success(let strings):
                completion(.success(strings.map { SearchResult(title: $0) }))
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
}

public final class MockSearchService: SearchServiceProtocol {
    public init() {}
    
    public func search(keyword: String,
                       completion: @escaping (Result<[SearchResult], Error>) -> Void) {
        let models = (1...5).map { SearchResult(title: "[MOCK] Result \($0) for \(keyword)") }
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            completion(.success(models))
        }
    }
}

5.3 BizUI:只依赖 SearchServiceProtocol

swift 复制代码
public final class SearchViewController: AWEBaseViewController {
    
    private let service: SearchServiceProtocol
    
    private let textField = UITextField()
    private let button = UIButton(type: .system)
    private let textView = UITextView()
    private let iconView = UIImageView()
    
    public init(service: SearchServiceProtocol) {
        self.service = service
        super.init(nibName: nil, bundle: nil)
        self.title = "Search"
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    public override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
    
    private func setupUI() {
        textField.borderStyle = .roundedRect
        textField.placeholder = "输入关键词,例如: douyin"
        
        button.setTitle("搜索", for: .normal)
        button.addTarget(self, action: #selector(onSearch), for: .touchUpInside)
        
        textView.isEditable = false
        textView.layer.borderColor = UIColor.lightGray.cgColor
        textView.layer.borderWidth = 1
        
        iconView.contentMode = .scaleAspectFit
        iconView.heightAnchor.constraint(equalToConstant: 40).isActive = true
        iconView.image = SearchResources.searchTabIcon
        
        let stack = UIStackView(arrangedSubviews: [iconView, textField, button, textView])
        stack.axis = .vertical
        stack.spacing = 12
        
        view.addSubview(stack)
        stack.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            stack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
            stack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
            stack.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
            textView.heightAnchor.constraint(equalToConstant: 250)
        ])
    }
    
    @objc private func onSearch() {
        let keyword = textField.text ?? ""
        guard !keyword.isEmpty else { return }
        
        textView.text = "搜索中..."
        
        service.search(keyword: keyword) { [weak self] result in
            switch result {
            case .success(let models):
                self?.textView.text = models.map { $0.title }.joined(separator: "\n")
            case .failure(let error):
                self?.textView.text = "Error: \(error)"
            }
        }
    }
}

5.4 Impl 层:按宿主选择 Service 实现

swift 复制代码
public final class SearchEntryImpl: SearchEntryProtocol {
    public init() {}
    
    public func makeSearchViewController() -> UIViewController {
        if let service = ServiceRegistry.shared.resolve(SearchServiceProtocol.self) {
            return SearchViewController(service: service)
        }
        if let network = ServiceRegistry.shared.resolve(NetworkClient.self) {
            return SearchViewController(service: OnlineSearchService(network: network))
        }
        let vc = UIViewController()
        vc.view.backgroundColor = .systemBackground
        vc.title = "Search (Service not available)"
        return vc
    }
}

public final class SearchServiceRegisterTask: BootTask {
    private let useMock: Bool
    
    public init(useMock: Bool) {
        self.useMock = useMock
        super.init(phase: .infra, priority: 60)
    }
    
    public override func execute(completion: @escaping () -> Void) {
        if useMock {
            let service = MockSearchService()
            ServiceRegistry.shared.register(SearchServiceProtocol.self, impl: service)
        } else if let network = ServiceRegistry.shared.resolve(NetworkClient.self) {
            let service = OnlineSearchService(network: network)
            ServiceRegistry.shared.register(SearchServiceProtocol.self, impl: service)
        }
        completion()
    }
}

public final class SearchBootTask: BootTask {
    public override func execute(completion: @escaping () -> Void) {
        let entry = SearchEntryImpl()
        ServiceRegistry.shared.register(SearchEntryProtocol.self, impl: entry)
        
        Router.shared.register("douyin://search") { _ in
            entry.makeSearchViewController()
        }
        
        completion()
    }
}

public func registerSearchBootTasks(useMockService: Bool = false) {
    LaunchKit.register(task: SearchServiceRegisterTask(useMock: useMockService))
    LaunchKit.register(task: SearchBootTask(phase: .bizRegister, priority: 50))
}

6. Feed 模块:FeedService 也做成接口 + 多实现

6.1 FeedServiceProtocol + Online / Mock 实现

swift 复制代码
import Foundation
import AWESearch
import AWELaunchKit
import AWEInfra

public protocol FeedServiceProtocol {
    func loadFeedItems() -> [String]
    func searchEntry() -> SearchEntryProtocol?
}

public final class OnlineFeedService: FeedServiceProtocol {
    private let userService: UserService?
    
    public init(userService: UserService?) {
        self.userService = userService
    }
    
    public func loadFeedItems() -> [String] {
        let userName = userService?.userName ?? "Guest"
        return (1...20).map { "Feed Item \($0) for \(userName)" }
    }
    
    public func searchEntry() -> SearchEntryProtocol? {
        ServiceRegistry.shared.resolve(SearchEntryProtocol.self)
    }
}

public final class MockFeedService: FeedServiceProtocol {
    public init() {}
    
    public func loadFeedItems() -> [String] {
        (1...10).map { "[MOCK] Feed Item \($0)" }
    }
    
    public func searchEntry() -> SearchEntryProtocol? {
        ServiceRegistry.shared.resolve(SearchEntryProtocol.self)
    }
}

6.2 Feed BizUI:只依赖 FeedServiceProtocol

swift 复制代码
public final class FeedViewController: AWEBaseViewController {
    
    private let tableView = UITableView()
    private let feedService: FeedServiceProtocol
    private var items: [String] = []
    
    public init(feedService: FeedServiceProtocol) {
        self.feedService = feedService
        super.init(nibName: nil, bundle: nil)
        self.title = "Feed"
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    public override func viewDidLoad() {
        super.viewDidLoad()
        setupTableView()
        setupSearchButton()
        loadData()
    }
    
    private func setupTableView() {
        tableView.dataSource = self
        view.addSubview(tableView)
        tableView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            tableView.topAnchor.constraint(equalTo: view.topAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }
    
    private func setupSearchButton() {
        let searchButton = UIBarButtonItem(title: "Search",
                                           style: .plain,
                                           target: self,
                                           action: #selector(onSearch))
        navigationItem.rightBarButtonItem = searchButton
    }
    
    private func loadData() {
        items = feedService.loadFeedItems()
        tableView.reloadData()
    }
    
    @objc private func onSearch() {
        if let nav = navigationController {
            Router.shared.open("douyin://search", from: nav)
            return
        }
        
        if let entry = feedService.searchEntry(),
           let nav = navigationController {
            let vc = entry.makeSearchViewController()
            nav.pushViewController(vc, animated: true)
        }
    }
}

extension FeedViewController: UITableViewDataSource {
    public func tableView(_ tableView: UITableView,
                          numberOfRowsInSection section: Int) -> Int {
        items.count
    }
    
    public func tableView(_ tableView: UITableView,
                          cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cellId = "cell"
        let cell = tableView.dequeueReusableCell(withIdentifier: cellId)
            ?? UITableViewCell(style: .default, reuseIdentifier: cellId)
        cell.textLabel?.text = items[indexPath.row]
        
        if let like = FeedResources.likeIcon {
            cell.accessoryView = UIImageView(image: like)
        }
        
        return cell
    }
}

6.3 Feed Impl:BootTask 注册 Online / Mock 实现

swift 复制代码
public final class FeedEntryImpl: FeedEntryProtocol {
    public init() {}
    
    public func makeRootViewController() -> UIViewController {
        if let service = ServiceRegistry.shared.resolve(FeedServiceProtocol.self) {
            return FeedViewController(feedService: service)
        }
        let userService = ServiceRegistry.shared.resolve(UserService.self)
        let service = OnlineFeedService(userService: userService)
        return FeedViewController(feedService: service)
    }
}

public final class FeedServiceRegisterTask: BootTask {
    private let useMock: Bool
    
    public init(useMock: Bool) {
        self.useMock = useMock
        super.init(phase: .infra, priority: 55)
    }
    
    public override func execute(completion: @escaping () -> Void) {
        if useMock {
            let service = MockFeedService()
            ServiceRegistry.shared.register(FeedServiceProtocol.self, impl: service)
        } else {
            let userService = ServiceRegistry.shared.resolve(UserService.self)
            let service = OnlineFeedService(userService: userService)
            ServiceRegistry.shared.register(FeedServiceProtocol.self, impl: service)
        }
        completion()
    }
}

public final class FeedBootTask: BootTask {
    public override func execute(completion: @escaping () -> Void) {
        let entry = FeedEntryImpl()
        ServiceRegistry.shared.register(FeedEntryProtocol.self, impl: entry)
        
        Router.shared.register("douyin://feed") { _ in
            entry.makeRootViewController()
        }
        
        completion()
    }
}

public func registerFeedBootTasks(useMockService: Bool = false) {
    LaunchKit.register(task: FeedServiceRegisterTask(useMock: useMockService))
    LaunchKit.register(task: FeedBootTask(phase: .bizRegister, priority: 40))
}

7. 子壳工程:SearchShell / FeedShell

7.1 SearchShell:专注搜索业务

swift 复制代码
@main
class SearchShellAppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    
    private func setupBootTasks() {
        registerUIThemeBootTasks()
        registerInfraBootTasks()
        registerSearchBootTasks(useMockService: true)   // 子壳用 MockSearchService
    }
    
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        setupBootTasks()
        
        LaunchKit.run(phase: .infra) {
            LaunchKit.run(phase: .bizRegister) {
                self.setupRootUI()
            }
        }
        return true
    }
    
    private func setupRootUI() {
        guard let entry = ServiceRegistry.shared.resolve(SearchEntryProtocol.self) else { return }
        let nav = AWENavigationController(rootViewController: entry.makeSearchViewController())
        let window = UIWindow(frame: UIScreen.main.bounds)
        window.rootViewController = nav
        window.makeKeyAndVisible()
        self.window = window
    }
}

7.2 FeedShell:专注 Feed 业务

swift 复制代码
@main
class FeedShellAppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    
    private func setupBootTasks() {
        registerUIThemeBootTasks()
        registerInfraBootTasks()
        registerSearchBootTasks(useMockService: true)
        registerFeedBootTasks(useMockService: true)     // 子壳用 MockFeedService
    }
    
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        setupBootTasks()
        
        LaunchKit.run(phase: .infra) {
            LaunchKit.run(phase: .bizRegister) {
                self.setupRootUI()
            }
        }
        return true
    }
    
    private func setupRootUI() {
        guard let entry = ServiceRegistry.shared.resolve(FeedEntryProtocol.self) else { return }
        let nav = AWENavigationController(rootViewController: entry.makeRootViewController())
        let window = UIWindow(frame: UIScreen.main.bounds)
        window.rootViewController = nav
        window.makeKeyAndVisible()
        self.window = window
    }
}

8. 宿主 App:编排所有业务和中台

swift 复制代码
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    
    var window: UIWindow?
    
    private func setupBootTasks() {
        registerUIThemeBootTasks()
        registerInfraBootTasks()
        registerSearchBootTasks(useMockService: false) // 宿主用线上实现
        registerFeedBootTasks(useMockService: false)
    }
    
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        setupBootTasks()
        
        LaunchKit.run(phase: .infra) {
            LaunchKit.run(phase: .bizRegister) {
                self.setupRootUI()
            }
        }
        return true
    }
    
    private func setupRootUI() {
        guard let entry = ServiceRegistry.shared.resolve(FeedEntryProtocol.self) else { return }
        let root = entry.makeRootViewController()
        let nav = AWENavigationController(rootViewController: root)
        
        let window = UIWindow(frame: UIScreen.main.bounds)
        window.rootViewController = nav
        window.makeKeyAndVisible()
        self.window = window
    }
}

9. 总结:这套 Demo 可以给你什么参考?

  • 架构层面

    • 单仓多组件(subspec 拆层)
    • 业务接口层(ModuleInterface)
    • 服务层接口(ServiceInterface),支持 Online / Mock 多实现
    • 中台层:Infra + UIResources + UITheme
  • 工程组织层面

    • Podfile 只依赖业务的 Impl 层,宿主不直接碰内部层级
    • 业务仓内部通过 subspec 定义 Basic / Model / Service / BizUI / Impl 分层
    • ServiceRegistry 作为轻量级服务总线
  • 研发流程层面

    • 子壳工程可独立运行某一业务线
    • 通过 useMockService 切换实现,支持离线、Mock、专项调试
    • 后续可以很自然地加上 lint 规则、防止分层依赖被破坏

你可以把这套 Demo 当成一个「骨架」:

  • 换成你们自己的业务:把 Feed / Search 换成 Home / Profile / Live 等;
  • 把 StubNetworkClient / MockService 替换成真实后端;
  • 在 ServiceInterface 上加更多能力(埋点、AB、灰度等);
  • 加上静态分析脚本,限制谁可以依赖谁。

项目地址GITHUB

相关推荐
n***i951 小时前
重新定义前端运行时:从浏览器脚本到分布式应用层的架构进化
前端·架构
Mintopia1 小时前
🏗️ 系统架构之:大模型 Token 计费方案
人工智能·架构·全栈
Eren7Y琳1 小时前
开箱即用构建应用环境:openEuler易获得性深度验证
redis·设计模式·架构
踏浪无痕2 小时前
从单体PHP到微服务:一个五年老项目的血泪重构史
后端·面试·架构
core5122 小时前
[硬核解析] 从感知到交互:InternVideo 1/2/2.5 全系列架构演进与原理解析
架构·大模型·交互·视频·video·intern
500842 小时前
鸿蒙 Flutter 接入鸿蒙系统能力:通知(本地 / 推送)与后台任务
java·flutter·华为·性能优化·架构
weixin_307779132 小时前
基于AWS安全组的两层架构访问控制设计与实现
运维·云原生·架构·云计算·aws
代码狂想家2 小时前
多架构兼容性与性能:openEuler的网络带宽与延迟报告
架构
李慕婉学姐3 小时前
【开题答辩过程】以《基于Hadoop架构的体育类短视频推荐系统设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
大数据·hadoop·架构