Skip to content

架构

面试点

图片缓存 阅读时长统计 复杂页面架构 客户端整体架构

架构&框架

  • 模块化
  • 分层
  • 解耦
  • 降低代码重合度

图片缓存

图片通过什么方式读写,过程是怎么样的?

  • 以图片的 URL 的单向 hash值作为 key.

内存设计,需要考虑哪些问题?

  • 存储 size
  • 淘汰策略

淘汰策略

  • 队列先进先出的方式淘汰
  • LRU 最近最久未使用的

#import <Foundation/Foundation.h>

@interface LRUCache : NSObject

@property (nonatomic, assign) NSInteger maxCacheSize;

- (instancetype)initWithMaxCacheSize:(NSInteger)maxCacheSize;
- (void)setObject:(id)object forKey:(id)key;
- (id)objectForKey:(id)key;
- (void)removeObjectForKey:(id)key;
- (void)removeAllObjects;

@end

@implementation LRUCache {
    NSMutableArray *_keys;
    NSMutableArray *_values;
    NSMutableDictionary *_cache;
    NSLock *_lock;
}

- (instancetype)initWithMaxCacheSize:(NSInteger)maxCacheSize {
    self = [super init];
    if (self) {
        _maxCacheSize = maxCacheSize;
        _keys = [NSMutableArray array];
        _values = [NSMutableArray array];
        _cache = [NSMutableDictionary dictionary];
        _lock = [[NSLock alloc] init];
    }
    return self;
}

- (void)setObject:(id)object forKey:(id)key {
    [_lock lock];
    NSInteger index = [_keys indexOfObject:key];

    if (index != NSNotFound) {
        [_keys removeObjectAtIndex:index];
        [_values removeObjectAtIndex:index];
    } else if (_keys.count >= _maxCacheSize) {
        id oldestKey = _keys.firstObject;
        [_keys removeObjectAtIndex:0];
        [_values removeObjectAtIndex:0];
        [_cache removeObjectForKey:oldestKey];
    }

    [_keys addObject:key];
    [_values addObject:object];
    [_cache setObject:object forKey:key];

    [_lock unlock];
}

- (id)objectForKey:(id)key {
    [_lock lock];
    NSInteger index = [_keys indexOfObject:key];
    id object = nil;

    if (index != NSNotFound) {
        object = _values[index];
        [_keys removeObjectAtIndex:index];
        [_values removeObjectAtIndex:index];

        [_keys addObject:key];
        [_values addObject:object];
    } else {
        object = [_cache objectForKey:key];
    }

    [_lock unlock];
    return object;
}

- (void)removeObjectForKey:(id)key {
    [_lock lock];
    NSInteger index = [_keys indexOfObject:key];

    if (index != NSNotFound) {
        [_keys removeObjectAtIndex:index];
        [_values removeObjectAtIndex:index];
    }

    [_cache removeObjectForKey:key];
    [_lock unlock];
}

- (void)removeAllObjects {
    [_lock lock];
    [_keys removeAllObjects];
    [_values removeAllObjects];
    [_cache removeAllObjects];
    [_lock unlock];
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LRUCache *cache = [[LRUCache alloc] initWithMaxCacheSize:3];

        // 添加缓存项
        [cache setObject:@1 forKey:@"one"];
        [cache setObject:@2 forKey:@"two"];
        [cache setObject:@3 forKey:@"three"];

        // 访问缓存项
        NSLog(@"Value for 'one': %@", [cache objectForKey:@"one"]); // 输出: 1

        // 添加新的缓存项(会触发移除最久未使用的项)
        [cache setObject:@4 forKey:@"four"];

        // 检查移除情况
        if (![cache objectForKey:@"two"]) {
            NSLog(@"'two' has been removed from cache"); // 输出: 'two' has been removed from cache
        }
    }
    return 0;
}

import Foundation

class LRUCache<Key: Hashable, Value> {
    private var keys: [Key] = []
    private var values: [Value] = []
    private var cache: [Key: Value] = [:]
    private var maxCacheSize: Int
    private let lock = NSLock()

    init(maxCacheSize: Int) {
        self.maxCacheSize = maxCacheSize
    }

    func set(value: Value, forKey key: Key) {
        lock.lock()
        defer { lock.unlock() }

        if let index = keys.firstIndex(of: key) {
            // If the key exists, update the value and move the key to the end
            values[index] = value
            keys.remove(at: index)
            values.remove(at: index)
        } else if keys.count >= maxCacheSize {
            // If the cache is full, remove the least recently used item
            let removedKey = keys.removeFirst()
            values.removeFirst()
            cache.removeValue(forKey: removedKey)
        }

        // Add the new key-value pair to the end
        keys.append(key)
        values.append(value)
        cache[key] = value
    }

    func value(forKey key: Key) -> Value? {
        lock.lock()
        defer { lock.unlock() }

        if let index = keys.firstIndex(of: key) {
            // Move the accessed key to the end to mark it as recently used
            let value = values[index]
            keys.remove(at: index)
            values.remove(at: index)
            keys.append(key)
            values.append(value)
            return value
        }
        return nil
    }

    func removeValue(forKey key: Key) {
        lock.lock()
        defer { lock.unlock() }

        if let index = keys.firstIndex(of: key) {
            keys.remove(at: index)
            values.remove(at: index)
            cache.removeValue(forKey: key)
        }
    }

    func removeAllValues() {
        lock.lock()
        defer { lock.unlock() }

        keys.removeAll()
        values.removeAll()
        cache.removeAll()
    }
}


  1. 初始化缓存:

    • 使用 NSMutableArray 来存储缓存的键(_keys)和对应的值(_values)。
    • 使用 NSMutableDictionary 来快速查找缓存中的对象。
    • 使用 NSLock 来确保线程安全。
  2. 设置缓存:

    • setObject:forKey: 方法将新对象插入缓存。
    • 如果键已经存在,更新其值并将其移动到数组末尾(表示最近使用)。
    • 如果缓存已满,移除最早的键值对(表示最久未使用)。
  3. 获取缓存值:

    • objectForKey: 方法从缓存中获取值。
    • 如果键存在,更新其位置以标记为最近使用。
  4. 删除缓存值:

    • removeObjectForKey: 方法从缓存中删除指定键的值。
  5. 清空缓存:

    • removeAllObjects 方法清空所有缓存。

阅读时长统计

为何要有不同类型的记录器,你的考虑是什么?

  • 基于不同分类场景提供的关于记录的封装、适配

记录的数据会由于某种原因丢失,你是怎样处理的?

  • 定时写磁盘
  • 限定内存缓存条数(如10条),超过该条数,即写磁盘

关于延时上传的具体场景有哪些

  • 前后台切换
  • 从无网到有网的变化
  • 通用轻量接口捎带

长传时机是怎样把控的?

  • 立刻上传
  • 延时上传
  • 定时上传

MVVM

MVVM(Model-View-ViewModel)是一种软件架构设计模式,尤其适用于构建用户界面的应用程序。它通过分离用户界面开发与业务逻辑来简化代码,使应用程序更容易测试和维护。MVVM 模式最常用于开发平台,如 iOS、Android、和 WPF(Windows Presentation Foundation)。

MVVM 架构的主要组成部分

  1. Model:

    • 数据和业务逻辑的表示层。
    • 负责处理数据和业务规则,但不涉及 UI 逻辑。
    • 例如,网络请求、数据库访问、数据处理等都在这一层进行。
  2. View:

    • 用户界面层。
    • 负责显示数据和处理用户交互。
    • 例如,按钮、标签、表格视图等 UI 元素。
  3. ViewModel:

    • 连接 Model 和 View 的桥梁。
    • 负责从 Model 中获取数据,并将数据转换为 View 可用的形式。
    • 处理 View 的交互,并将用户的操作传递给 Model。
    • 通过数据绑定(Data Binding)将 View 和 ViewModel 连接起来。

MVVM 在 iOS 中的实现示例

我们以一个简单的 iOS 应用为例,该应用展示一个用户列表,并在点击某个用户时显示详细信息。

Model

import Foundation

struct User {
    let id: Int
    let name: String
    let email: String
}

class UserService {
    func fetchUsers(completion: @escaping ([User]) -> Void) {
        // 模拟网络请求
        let users = [
            User(id: 1, name: "John Doe", email: "john@example.com"),
            User(id: 2, name: "Jane Smith", email: "jane@example.com")
        ]
        completion(users)
    }
}

ViewModel

import Foundation

class UserViewModel {
    private let userService: UserService
    var users: [User] = []
    var reloadTableView: (() -> Void)?

    init(userService: UserService) {
        self.userService = userService
        fetchUsers()
    }

    func fetchUsers() {
        userService.fetchUsers { [weak self] users in
            self?.users = users
            self?.reloadTableView?()
        }
    }

    func getUser(at indexPath: IndexPath) -> User {
        return users[indexPath.row]
    }
}

View (Controller)

import UIKit

class UserViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    private var viewModel: UserViewModel!

    private let tableView: UITableView = {
        let tableView = UITableView()
        tableView.translatesAutoresizingMaskIntoConstraints = false
        return tableView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(tableView)
        tableView.dataSource = self
        tableView.delegate = self
        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: self.view.topAnchor),
            tableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
            tableView.leftAnchor.constraint(equalTo: self.view.leftAnchor),
            tableView.rightAnchor.constraint(equalTo: self.view.rightAnchor)
        ])

        viewModel = UserViewModel(userService: UserService())
        viewModel.reloadTableView = { [weak self] in
            DispatchQueue.main.async {
                self?.tableView.reloadData()
            }
        }
    }

    // UITableViewDataSource
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.users.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell(style: .subtitle, reuseIdentifier: "cell")
        let user = viewModel.getUser(at: indexPath)
        cell.textLabel?.text = user.name
        cell.detailTextLabel?.text = user.email
        return cell
    }

    // UITableViewDelegate
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let user = viewModel.getUser(at: indexPath)
        let detailVC = UserDetailViewController(user: user)
        self.navigationController?.pushViewController(detailVC, animated: true)
    }
}

class UserDetailViewController: UIViewController {
    private let user: User

    init(user: User) {
        self.user = user
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        let nameLabel = UILabel()
        nameLabel.text = "Name: \(user.name)"
        nameLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(nameLabel)

        let emailLabel = UILabel()
        emailLabel.text = "Email: \(user.email)"
        emailLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(emailLabel)

        NSLayoutConstraint.activate([
            nameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            nameLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -20),
            emailLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            emailLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 20)
        ])
    }
}

解释

  1. Model

    • User 结构体表示用户数据。
    • UserService 类负责获取用户数据,这里模拟了一个网络请求。
  2. ViewModel

    • UserViewModel 类管理用户数据和与视图相关的逻辑。
    • fetchUsers 方法从 UserService 获取数据,并通知视图更新。
  3. View

    • UserViewController 类展示用户列表。
    • 它通过 UserViewModel 获取数据,并在用户点击时导航到详细信息页面。
    • UserDetailViewController 类展示用户的详细信息。

通过这种方式,数据逻辑和视图逻辑分离,方便测试和维护。如果数据源或数据逻辑发生变化,视图层的代码基本不需要修改。

客户端整体架构

  • 独立于 app 的通用层: 时长统计,崩溃统计,网络第三方库
  • 通用业务层: 自定义的 UI 控件封装
  • 中间层: 协调解耦
  • 业务 a, 业务 b, c, d

各个业务之间的解耦通信方式?

  • openURL
  • 依赖注入

理解你的需求后,我来为你设计一个更详细的客户端架构,包含独立的通用层、通用业务层、中间层以及具体业务模块。以下是该架构的层次设计:

客户端整体架构设计

  1. 通用层 (Common Layer)

    • 描述: 最底层,提供独立于具体应用的通用功能。
    • 职责:

      • 公共工具类和扩展(Utility classes, Extensions)
      • 网络请求库(Networking library)
      • 本地存储(Local Storage, Database Access)
      • 通用的UI组件(Common UI Components)
      • 日志记录(Logging)
      • 配置管理(Configuration Management)
  2. 通用业务层 (Common Business Layer)

    • 描述: 提供与应用业务相关的公共服务和逻辑。
    • 职责:
      • 用户管理(User Management)
      • 鉴权和授权(Authentication and Authorization)
      • 统一错误处理(Error Handling)
      • 通用数据模型(Common Data Models)
      • 通用服务(Common Services)
  3. 中间层 (Middleware Layer)

    • 描述: 连接通用业务层和具体业务模块,协调各个业务模块间的交互。
    • 职责:
      • 路由和导航(Routing and Navigation)
      • 状态管理(State Management)
      • 事件处理(Event Handling)
      • 中央数据存储(Central Data Store)
  4. 具体业务模块 (Feature Modules)

    • 描述: 具体的业务功能模块,每个模块独立开发,独立维护。
    • 职责:
      • 业务逻辑和数据处理(Business Logic and Data Processing)
      • 特定的UI和用户交互(Specific UI and User Interaction)
      • 模块内的状态管理(Module-Specific State Management)

架构图示

+---------------------------------------------------+
|                 具体业务模块 (Feature Modules)    |
|      +-------------+ +-------------+ +-----------+|
|      | 业务 A       | | 业务 B       | | 业务 C     ||
|      +-------------+ +-------------+ +-----------+|
+---------------------------------------------------+
|                      中间层 (Middleware Layer)   |
+---------------------------------------------------+
|               通用业务层 (Common Business Layer)  |
+---------------------------------------------------+
|                  通用层 (Common Layer)            |
+---------------------------------------------------+

各层具体实现示例(以 Swift 语言的 iOS 应用为例)

一. 通用层 (Common Layer)

网络请求库

import Foundation

class NetworkManager {
    static let shared = NetworkManager()

    private init() {}

    func requestData(url: URL, completion: @escaping (Data?, URLResponse?, Error?) -> Void) {
        let task = URLSession.shared.dataTask(with: url, completionHandler: completion)
        task.resume()
    }
}

日志记录

import Foundation

class Logger {
    static func log(_ message: String) {
        #if DEBUG
        print(message)
        #endif
    }
}

配置管理

import Foundation

class Configuration {
    static let shared = Configuration()

    private init() {}

    var apiEndpoint: String {
        return "https://api.example.com"
    }
}

二. 通用业务层 (Common Business Layer)

用户管理

import Foundation

class UserManager {
    static let shared = UserManager()

    private var currentUser: User?

    private init() {}

    func login(username: String, password: String, completion: @escaping (Bool) -> Void) {
        // 登录逻辑
        completion(true)
    }

    func logout() {
        currentUser = nil
    }

    func getCurrentUser() -> User? {
        return currentUser
    }
}

统一错误处理

import Foundation

class ErrorHandler {
    static func handleError(_ error: Error) {
        // 统一错误处理逻辑
        Logger.log("Error: \(error.localizedDescription)")
    }
}

三. 中间层 (Middleware Layer)

路由和导航

import UIKit

class Router {
    static func navigateTo(screen: Screen, from viewController: UIViewController) {
        // 导航逻辑
        let destinationVC = getViewController(for: screen)
        viewController.navigationController?.pushViewController(destinationVC, animated: true)
    }

    private static func getViewController(for screen: Screen) -> UIViewController {
        // 根据 Screen 枚举返回相应的 ViewController
        switch screen {
        case .home:
            return HomeViewController()
        case .profile:
            return ProfileViewController()
        }
    }
}

enum Screen {
    case home
    case profile
}

状态管理

import Foundation

class AppState {
    static let shared = AppState()

    var isLoggedIn: Bool = false
}

四. 具体业务模块 (Feature Modules)

业务 A - 用户资料模块

ViewModel

import Foundation

class UserProfileViewModel {
    private let userManager: UserManager

    var user: User? {
        return userManager.getCurrentUser()
    }

    init(userManager: UserManager = .shared) {
        self.userManager = userManager
    }

    func logout() {
        userManager.logout()
    }
}

View

import UIKit

class UserProfileViewController: UIViewController {
    private var viewModel: UserProfileViewModel!

    private let nameLabel = UILabel()
    private let logoutButton = UIButton()

    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel = UserProfileViewModel()

        view.backgroundColor = .white
        setupUI()

        if let user = viewModel.user {
            nameLabel.text = "\(user.firstName) \(user.lastName)"
        }
    }

    private func setupUI() {
        nameLabel.translatesAutoresizingMaskIntoConstraints = false
        logoutButton.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(nameLabel)
        view.addSubview(logoutButton)

        NSLayoutConstraint.activate([
            nameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            nameLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -20),
            logoutButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            logoutButton.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 20)
        ])

        logoutButton.setTitle("Logout", for: .normal)
        logoutButton.setTitleColor(.blue, for: .normal)
        logoutButton.addTarget(self, action: #selector(logout), for: .touchUpInside)
    }

    @objc private func logout() {
        viewModel.logout()
        // Navigate to login screen or handle logout
    }
}

总结

这个架构设计考虑了可扩展性、模块化和可维护性,通过将不同职责分离到各自的层中,可以实现更加清晰和可维护的代码结构。

  • 通用层 (Common Layer) 提供基础设施和工具支持,独立于具体业务。
  • 通用业务层 (Common Business Layer) 提供通用业务逻辑和服务,供各个业务模块共享。
  • 中间层 (Middleware Layer) 负责协调各个业务模块,管理应用的整体状态和导航。
  • 具体业务模块 (Feature Modules) 每个业务模块独立实现自己的业务逻辑和 UI,互不干扰。

这种架构不仅适用于移动客户端开发,还可以推广到其他客户端应用程序,如桌面应用、Web应用等。


依赖注入 (Dependency Inj

通过依赖注入,将模块所需的依赖通过构造函数或属性注入的方式提供,而不是在模块内部创建依赖。这可以通过手动注入或使用依赖注入框架来实现。

import Foundation

class UserService {
    func fetchUsers() -> [User] {
        // 获取用户数据
        return []
    }
}

class UserViewModel {
    private let userService: UserService

    init(userService: UserService) {
        self.userService = userService
    }

    func loadUsers() {
        let users = userService.fetchUsers()
        // 处理用户数据
    }
}

// 使用示例
let userService = UserService()
let userViewModel = UserViewModel(userService: userService)

路由和导航 (Routing and Navigation)

使用中间层的路由器或导航器来管理视图控制器之间的导航,使视图控制器之间的跳转逻辑集中在一处,避免直接相互引用。

import UIKit

class Router {
    static let shared = Router()

    private init() {}

    func navigate(to screen: Screen, from viewController: UIViewController) {
        let destinationVC = getViewController(for: screen)
        viewController.navigationController?.pushViewController(destinationVC, animated: true)
    }

    private func getViewController(for screen: Screen) -> UIViewController {
        switch screen {
        case .home:
            return HomeViewController()
        case .profile:
            return ProfileViewController()
        }
    }
}

enum Screen {
    case home
    case profile
}

// 使用示例
Router.shared.navigate(to: .profile, from: currentViewController)

协议和接口 (Protocols and Interfaces)

使用协议定义模块间的契约,各模块通过实现这些协议进行解耦,调用者只依赖协议,而不依赖具体实现。

import Foundation

protocol UserServiceProtocol {
    func fetchUsers() -> [User]
}

class UserService: UserServiceProtocol {
    func fetchUsers() -> [User] {
        // 获取用户数据
        return []
    }
}

class UserViewModel {
    private let userService: UserServiceProtocol

    init(userService: UserServiceProtocol) {
        self.userService = userService
    }

    func loadUsers() {
        let users = userService.fetchUsers()
        // 处理用户数据
    }
}

// 使用示例
let userService = UserService()
let userViewModel = UserViewModel(userService: userService)