Telegram

Mổ Xẻ Kiến Trúc Telegram — Phần 2: iOS

Bài viết phân tích kiến trúc kỹ thuật của Telegram iOS client dựa trên source code chính thức tại github.com/TelegramMessenger/Telegram-iOS. Đây là Phần 2 trong series 2 phần — đọc cùng Phần 1: Android để thấy bức tranh đầy đủ về cách Telegram giải quyết cùng một bài toán trên hai hệ điều hành khác nhau.


0. Telegram iOS có gì khác Telegram Android?

Nếu bạn đọc xong Phần 1, sẽ ngạc nhiên khi thấy số liệu này:

Android (DrKLO/Telegram)iOS (TelegramMessenger/Telegram-iOS)
Stars28.7k8.1k
Forks9.3k2.5k
Commits57329,753
Java/Swift %Java 43.3%Swift 45.5%
C++ %32.8%1.7%
C %14.3%42.0%
Obj-C / Obj-C++5.4% / 0.8%
Assembly %4.5%3.1%
Build systemGradle + CMakeBazel
Module count2 (lib + app)270+ submodules
Min OSAndroid 5.0 (API 21)iOS 12.0

Vài quan sát đáng chú ý ngay:

1. Repo iOS có nhiều commit gấp 50 lần Android. Android repo của DrKLO chỉ có 573 commits — vì DrKLO chỉ push public release snapshots, không phải dev branch. iOS repo có 29,753 commits — gần với truth của development thực. Đây là khác biệt về philosophy: team Android giấu đi development history, team iOS minh bạch hơn.

2. Repo iOS có 42% là C. Còn nhiều hơn cả Swift (45.5%). Đây là phần native shared với Android (MTProto, ffmpeg, opus, libwebp, sqlcipher…) — viết một lần dùng hai platform.

3. Repo iOS chỉ có 1.7% C++ vs Android 32.8%. Vì hầu hết logic phức tạp được viết bằng C (cross-compile dễ hơn) hoặc đẩy lên Swift. iOS có lợi thế Swift/Objective-C++ interop tốt nên không cần C++ làm cầu nối nhiều như Android JNI.

4. iOS có 270+ submodules. Android chỉ tách TMessagesProj core và 4 app variants. iOS thì mỗi UI screen, mỗi feature lớn đều là module riêng — ChatListUI, ChatMessageUI, GalleryUI, SettingsUI, v.v. Đây là khác biệt kiến trúc lớn nhất giữa hai platform, sẽ phân tích chi tiết bên dưới.

5. Bazel. iOS dùng Bazel của Google làm build system thay vì Xcode native. Lý do và trade-off sẽ được mổ xẻ ở mục 9.


1. Tổng quan tech stack

Thành phầniOS dùng gìAndroid (so sánh)
Build systemBazel + Xcode generationGradle + AGP
Min OSiOS 12.0Android 5.0 (API 21)
Ngôn ngữ chínhSwift, Obj-C, C, ARM64 AssemblyJava, C/C++, Assembly
UI frameworkAsyncDisplayKit (forked, ~35% gốc)Pure Android Views, custom Cells
ReactiveSSignalKit / SwiftSignalKit tự viếtNotificationCenter pattern
DIKhông dùng — singleton + manualKhông dùng — singleton
NetworkMTProtoKit (Obj-C)C++ tgnet qua JNI
DatabasePostbox (SQLite + SQLCipher + LMDB)SQLite raw qua JNI wrapper
Image loadingTransformImageNode + SignalImageReceiver tự viết
Animationrlottie + lottie-ios + POPrlottie native
ConcurrencyQueue (Obj-C wrapper GCD) + SignalDispatchQueue tự viết

Điểm chung Android: gần như không dùng thư viện iOS phổ biến nào. Không RxSwift, không Combine (chỉ dùng nội bộ một số chỗ), không Alamofire, không Kingfisher/SDWebImage, không Realm/CoreData/SwiftData, không SwiftUI cho main app.

Tuy nhiên, khác với Android (gần như tự viết hết từ đầu), iOS Telegram fork và customize một số thư viện open-source lớn:

  • AsyncDisplayKit (Texture) — fork từ Facebook/Pinterest, giữ ~35% code gốc, viết lại 65%
  • rlottie — share với Android
  • POP animation — fork từ Facebook
  • lottie-ios — fork từ airbnb

Quyết định fork thay vì tự viết phản ánh practicality: AsyncDisplayKit có 5+ năm tuning bởi Facebook/Pinterest — không có lý do reinvent.


2. Cấu trúc thư mục root

Telegram-iOS/
├── Telegram/                # Main app target
│   ├── Telegram-iOS/        # App entry, AppDelegate, Info.plist
│   ├── NotificationService/ # Push notification extension
│   ├── Share/               # Share extension
│   ├── SiriIntents/         # Siri Shortcuts integration
│   ├── WidgetKitWidget/     # Today widget (iOS 14+)
│   ├── BroadcastUpload/     # Screen broadcast (live streaming)
│   └── BUILD                # Bazel build file
│
├── submodules/              # 270+ Swift/Obj-C modules — TRÁI TIM của repo
│   ├── SSignalKit/          # Reactive (Obj-C version)
│   ├── SwiftSignalKit/      # Reactive (Swift version)
│   ├── Postbox/             # Database layer
│   ├── MtProtoKit/          # Network protocol
│   ├── TelegramApi/         # Generated TL API types
│   ├── TelegramCore/        # Business logic chung
│   ├── AsyncDisplayKit/     # UI framework (fork)
│   ├── Display/             # UI utilities, navigation
│   ├── TelegramUI/          # Main UI orchestration
│   ├── ChatListUI/          # Module riêng cho chat list
│   ├── ChatMessageUI/       # Module riêng cho message bubbles
│   ├── ChatHistoryUI/       # Module riêng cho lịch sử chat
│   ├── GalleryUI/           # Module riêng cho media viewer
│   ├── SettingsUI/          # Module riêng cho settings
│   ├── PeerInfoUI/          # Module riêng cho profile
│   ├── PremiumUI/           # Module riêng cho Telegram Premium
│   ├── StoryUI/             # Module riêng cho Stories
│   └── ... (260+ modules nữa)
│
├── third-party/             # External libraries (cùng nhiều phần với Android)
│   ├── openssl/             # Cùng phiên bản với Android
│   ├── ffmpeg/              # Same FFmpeg config
│   ├── sqlcipher/           # Encrypted SQLite
│   ├── webrtc/              # WebRTC cho calls
│   ├── boringssl/           # BoringSSL fork
│   └── opus/                # Voice codec
│
├── build-system/            # Bazel + Make.py orchestration
│   ├── Make/Make.py         # Master build script
│   ├── bazel-rules/         # Custom Bazel rules
│   └── template_*.json      # Build configuration templates
│
├── buildbox/                # CI infrastructure
├── docs/                    # Internal docs
├── tools/                   # Build tools
├── Tests/                   # Test targets
├── BUILD.bazel              # Root Bazel BUILD file
├── MODULE.bazel             # Bzlmod module definition
├── WORKSPACE                # Bazel workspace config
├── .bazelrc                 # Bazel runtime config
└── CLAUDE.md                # AI coding agent instructions (Telegram để sẵn cho Claude)

Một chi tiết hay: file CLAUDE.md ở root repo cho thấy Telegram team đã chính thức hóa việc dùng AI agent (Claude Code) để contribute. Trong repo có cả contributor @claude (avatar Anthropic).


3. SSignalKit / SwiftSignalKit — reactive framework tự viết

Đây là module foundation quan trọng nhất của Telegram iOS. Mọi thứ khác đều build trên đây.

3.1 Tại sao không dùng Combine/RxSwift?

Nếu bạn build app iOS hôm nay, lựa chọn rõ ràng là Combine (sau iOS 13) hoặc async/await (sau iOS 15). Tại sao Telegram không dùng?

Lý do thời gian: SSignalKit ra đời ~2014-2015, lúc RxSwift mới chập chững và Combine chưa ra đời. Khi Apple ra Combine năm 2019, Telegram đã có codebase 1M+ dòng dùng Signal — refactor ra Combine sẽ mất nhiều năm và không thêm value gì.

Lý do kỹ thuật:

  1. Cross-language. SSignalKit có cả Obj-C version (SSignalKit) và Swift version (SwiftSignalKit) tương thích bidirectional. Combine không có Obj-C interop. Mà Telegram có rất nhiều Obj-C code (đặc biệt MTProtoKit và AsyncDisplayKit fork).
  2. Custom queue model. Signal có concept deliverOnMainQueue, deliverOn(specificQueue) mà control được serial guarantee. Combine receive(on:) không serialize cùng cách.
  3. Pipe operator |>. Cú pháp đặc trưng của Telegram code:
let result = source
    |> map { ... }
    |> mapToSignal { ... }
    |> deliverOnMainQueue

Giống F#/OCaml, đọc top-down rất tự nhiên. Combine bắt buộc dot syntax .map { }.flatMap { }.

  1. Disposal model rõ ràng. Combine AnyCancellable không phân biệt được “cancel pending work” vs “release subscription”. Disposable của Signal được phân loại rõ: ActionDisposable, MetaDisposable, DisposableSet, DisposableDict.

3.2 Core type Signal<T, E>

public final class Signal<T, E> {
    public init(_ generator: @escaping (Subscriber<T, E>) -> Disposable)
    
    public func start(
        next: ((T) -> Void)? = nil,
        error: ((E) -> Void)? = nil,
        completed: (() -> Void)? = nil
    ) -> Disposable
}

public final class Subscriber<T, E> {
    public func putNext(_ value: T)
    public func putError(_ error: E)
    public func putCompletion()
}

Khác Combine Publisher<Output, Failure>, Signal có 3 sự khác biệt nhỏ nhưng quan trọng:

  • Generator-based. Closure tạo subscription chạy lại cho mỗi observer (lazy, cold).
  • Subscriber có 3 method. putNext cho values, putError để terminate với error, putCompletion để terminate normally. Sau error/completion, subscriber không nhận thêm event.
  • No backpressure. Signal không có demand như Combine. Đơn giản hơn, đủ dùng cho messaging.

3.3 Promise vs ValuePromise — analog của CurrentValueSubject

// Promise — set một lần, multiple observers nhận cùng value
let promise = Promise<User>()
promise.set(fetchUserSignal())

// ValuePromise — replay current value cho observer mới + push update
let valuePromise = ValuePromise<String>(initialValue: "Hello", ignoreRepeated: true)
valuePromise.set("World")

ValuePromise<T>CurrentValueSubject<T, Never> của Combine. Khác biệt: ignoreRepeated flag để skip emit khi value mới == value cũ — Combine cần removeDuplicates() operator.

3.4 Operators và |> pipe

Toàn bộ operators được implement như free functions, không phải method:

public func map<T, U, E>(_ f: @escaping (T) -> U) -> (Signal<T, E>) -> Signal<U, E> {
    return { signal in
        return Signal<U, E> { subscriber in
            return signal.start(
                next: { subscriber.putNext(f($0)) },
                error: { subscriber.putError($0) },
                completed: { subscriber.putCompletion() }
            )
        }
    }
}

Pipe operator |> được định nghĩa custom:

infix operator |> : DefaultPrecedence
public func |> <T, U>(value: T, function: (T) -> U) -> U {
    return function(value)
}

Cho phép viết:

account.postbox.transaction { ... }
    |> mapToSignal { messages -> Signal<[Message], NoError> in
        return enrichWithUsers(messages)
    }
    |> delay(0.3, queue: .mainQueue())
    |> deliverOnMainQueue

Điểm hay: operator là free function nên dễ viết operator mới, không cần extension Signal class. Nếu Apple bổ sung operator mới cho Combine bạn phải đợi SDK update — Telegram tự thêm trong vài dòng code.

3.5 Disposable hierarchy

// 4 kiểu disposable
class ActionDisposable: Disposable    // Chạy closure khi dispose
class MetaDisposable: Disposable      // Swap inner disposable (cancel previous + bind new)
class DisposableSet: Disposable       // Group nhiều disposable
class DisposableDict<Key>: Disposable // Dict — dispose by key

// Pattern dùng trong Controller
class ChatController: ViewController {
    private let disposables = DisposableSet()
    private let messagesDisposable = MetaDisposable()
    
    func loadMessages() {
        // MetaDisposable tự cancel request cũ khi set request mới
        messagesDisposable.set(
            account.postbox.messages(peerId)
                |> deliverOnMainQueue
                |> start(next: { [weak self] messages in
                    self?.update(messages)
                })
        )
    }
    
    deinit {
        disposables.dispose()
        messagesDisposable.dispose()
    }
}

MetaDisposable đặc biệt hữu ích — pattern “cancel previous, bind new” xuất hiện khắp app messaging (mỗi khi user gõ search query, cancel query cũ, gửi query mới).


4. Postbox — database layer reactive-first

Postbox là module database tự viết, không phải CoreData/Realm/SwiftData/GRDB. Build trên SQLite + SQLCipher (encryption) + LMDB (cho một số keyed store).

4.1 Tại sao không dùng CoreData?

  • CoreData chậm với insert hàng loạt (bulk message sync)
  • CoreData khó multi-thread (NSManagedObjectContext per thread)
  • CoreData khó share giữa app + extensions (NotificationService cần đọc DB)
  • CoreData không có FTS5 native
  • CoreData không cross-platform (Android dùng SQLite cùng schema)

Postbox giải quyết từng vấn đề:

  • Raw SQL → tốc độ insert tuyệt vời
  • Single writer queue → no contention
  • App Group container shared giữa main app, NotificationService, Share extension
  • FTS5 dùng trực tiếp
  • Schema tương đương Android Postbox (cross-platform sync logic shared)

4.2 Storage layout

~/Library/Group Containers/group.ph.telegra.Telegraph/   # App Group
└── telegram-data/
    ├── account-{id}/
    │   ├── postbox/
    │   │   └── db/
    │   │       ├── db_sqlite              # Main SQLite database
    │   │       ├── db_sqlite-shm          # Shared memory (WAL)
    │   │       ├── db_sqlite-wal          # Write-ahead log
    │   │       └── lmdb/                  # LMDB cho key-value cache nóng
    │   ├── cache/                         # Persistent media cache
    │   └── short-cache/                   # Auto-purged media
    └── accounts-metadata/
        └── metadata-{id}                  # Account list, active account

Điểm hay: account-{id} folder hoàn toàn isolated. Multi-account = nhiều folder song song. Switch account = đổi pointer trỏ đến folder khác. Logout = xóa folder.

4.3 Transaction API — chìa khóa của Postbox

// MỌI access database PHẢI qua transaction
account.postbox.transaction { transaction -> [Message] in
    return transaction.getMessages(peerId: peerId)
}
// Trả về Signal<[Message], NoError>

// Combine với operator
let messagesSignal: Signal<[MessageView], NoError> =
    account.postbox.aroundMessageHistoryViewForLocation(
        chatLocation: .peer(peerId),
        index: .upperBound,
        anchorIndex: .upperBound,
        count: 50
    )
    |> deliverOnMainQueue

Hai điểm thiết kế quan trọng:

1. Transaction là closure. Bạn không gọi db.beginTransaction() / db.commit() thủ công. Postbox quản lý lifetime — exception trong closure → rollback tự động.

2. Transaction trả về Signal. Không phải synchronous result. Vì:

  • Transaction có thể queue (nếu writer đang chạy)
  • Read transaction có thể chạy parallel (multiple reader)
  • Cancellation tự nhiên (dispose Signal = cancel transaction)

Đây là điểm khác biệt lớn so với Room/GRDB — Postbox reactive end-to-end từ database lên UI.

4.4 View — observable query

Concept quan trọng nhất của Postbox:

// Query trả về MessageHistoryView — KHÔNG phải snapshot, mà là LIVE view
let viewSignal = account.postbox.aroundMessageHistoryViewForLocation(...)

viewSignal.start(next: { view in
    // view tự update khi DB thay đổi
    // No need to re-query
})

Postbox track tất cả write transaction → tự diff với active views → emit update. Giống NSFetchedResultsController nhưng generic hơn và Signal-based. Giống Room LiveData nhưng cross-thread tốt hơn.

4.5 Schema highlights

-- Messages: composite key, ordered by timestamp DESC trong peer
CREATE TABLE messages (
    peerId INTEGER,
    namespace INTEGER,
    id INTEGER,
    timestamp INTEGER,
    flags INTEGER,
    tags INTEGER,
    globalTags INTEGER,
    forwardInfo BLOB,
    authorId INTEGER,
    data BLOB,                      -- Message body serialized (TL-style)
    PRIMARY KEY (peerId, namespace, id)
);

-- Chỉ có 1 index covering query phổ biến nhất
CREATE INDEX messages_peerId_timestamp_id 
    ON messages (peerId, namespace, timestamp DESC, id DESC);

-- FTS5 cho search
CREATE VIRTUAL TABLE messages_fts USING fts5(
    text,
    content='messages',
    content_rowid='rowid',
    tokenize='unicode61 remove_diacritics 2'  -- hỗ trợ Tiếng Việt và CJK
);

-- Hot keys lưu LMDB thay vì SQLite (faster)
-- VD: chat-list-state, peer-presence, sticker-pack-cache

Lưu ý tokenize='unicode61 remove_diacritics 2' — Telegram search Tiếng Việt OK ra ngay từ schema (search “phở” tìm thấy “pho”, search “anh” tìm thấy “ánh”). Nếu bạn build app Việt cần search, đây là setting đáng học.

4.6 LMDB cho hot keys

Postbox dùng LMDB (Lightning Memory-Mapped Database) cho một số dữ liệu truy cập rất nhanh:

  • Chat list state (pinned positions, unread counts)
  • Peer presence (online/offline)
  • Sticker pack cache
  • Wallpaper cache

LMDB nhanh hơn SQLite ~5-10x cho key-value lookup vì memory-mapped, no SQL parser overhead. Trade-off: schema-less, không có query language.


5. MTProtoKit — network layer (đối xứng Phần 1)

Phần network của iOS có cùng concept với Android, nhưng implementation chủ yếu Objective-C thay vì C++.

5.1 Tại sao Obj-C thay vì C++?

  • Obj-C native trên iOS, không cần JNI bridge
  • Obj-C có ARC (automatic reference counting) — đỡ memory leak
  • Obj-C interop tốt với Swift hơn C++
  • Existing codebase từ thời iOS app đầu tiên

5.2 Sơ đồ MTProto

┌──────────────────────────────────────────────────────────┐
│                       Swift Layer                         │
│  ┌────────────────────────────────────────────────────┐  │
│  │              TelegramCore.Network                   │  │
│  │  func request<T>(_ req: Api.Request<T>)             │  │
│  │       -> Signal<T, MTRpcError>                      │  │
│  └────────────────────────────────────────────────────┘  │
│                          │                                │
├──────────────────────────┼────────────────────────────────┤
│                  Objective-C Layer                        │
│  ┌────────────────────────────────────────────────────┐  │
│  │                   MTContext                         │  │
│  │  - apiEnvironment (App ID, lang, layer)            │  │
│  │  - datacenterAddresses[Int: [MTDatacenterAddress]] │  │
│  │  - authInfos[Int: MTDatacenterAuthInfo]            │  │
│  │  - serializer / parser                              │  │
│  └────────────────────────────────────────────────────┘  │
│                          │                                │
│  ┌────────────────────────────────────────────────────┐  │
│  │                    MTProto                          │  │
│  │  - per-DC connection orchestrator                   │  │
│  │  - request lifecycle (queue, retry, fail)           │  │
│  │  - sendRequest:                                     │  │
│  └────────────────────────────────────────────────────┘  │
│                          │                                │
│  ┌────────────────────────────────────────────────────┐  │
│  │              MTTcpTransport / MTHttpTransport       │  │
│  │  - GCDAsyncSocket wrap (Obj-C)                      │  │
│  │  - obfuscated2 transport encoding                   │  │
│  │  - AES-IGE encryption                               │  │
│  └────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────┘

So với Android C++ implementation, iOS Obj-C version có ít AbStraction hơn nhưng tận dụng được:

  • dispatch_queue_t cho serial execution (không cần epoll như Android)
  • ARC quản lý lifetime của request callback
  • NSData zero-copy với +[NSData dataWithBytesNoCopy:length:freeWhenDone:]

5.3 Datacenter routing

5 DC giống Android (DC1 Miami, DC2/DC4 Amsterdam, DC3 Miami, DC5 Singapore). Logic switch DC khi gặp *_MIGRATE_X error nằm trong MTContext.m:

- (void)performBatchUpdates:(void (^)(MTContextBlockChangeListener *))block {
    [_changeListenerQueue dispatch:^{
        // Apply DC migration changes atomically
        // Notify all observers
    }];
}

5.4 Endpoint discovery — đa fallback

Telegram iOS có nhiều fallback hơn Android cho việc tìm DC IP khi mạng bị chặn:

  1. DNS-over-HTTPS (Google dns.google.com, Cloudflare 1.1.1.1)
  2. CloudKit (Apple’s iCloud) — chỉ iOS mới có
  3. help.getConfig RPC (sau khi connect được DC nào đó)
  4. Push notification payload — server gửi DC config qua APNs
  5. Hardcoded fallback IPs trong app bundle
  6. Apple’s nw_endpoint_resolver với private DNS settings

CloudKit fallback rất khôn — Apple không thể block CloudKit, mà CloudKit sync giữa devices của user, nên Telegram có thể đẩy DC config qua iCloud private database.


6. AsyncDisplayKit — UI framework

Đây là phần khác biệt lớn nhất giữa Android và iOS Telegram. Android tự draw bằng Canvas trên View thuần. iOS dùng AsyncDisplayKit — framework ban đầu của Facebook (sau đó tách ra Pinterest/Texture).

6.1 ASDisplayNode là gì?

class ASDisplayNode {
    // Layout — có thể chạy background thread
    func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec
    
    // View — lazy-loaded trên main thread
    var view: UIView { get }
    
    // Layer — direct CALayer access
    var layer: CALayer { get }
}

Concept: node là abstraction over UIView/CALayer mà có thể create + measure + layout off-main-thread. Chỉ khi cần hiện lên screen, node mới materialize thành UIView trên main thread.

So sánh với UIKit thuần:

UIKitAsyncDisplayKit
initMain thread onlyBackground thread OK
sizeThatFitsMain threadBackground thread OK
layoutSubviewsMain threadBackground thread OK
Image decodeMain thread (blocking)Background thread
Text measureMain thread (CoreText)Background thread (TextNode)
Screen renderMain threadMain thread

Lợi ích thực: scroll chat list 200 message với rich text, image preview, link cards — main thread chỉ làm display. Mọi tính toán nặng (text layout, image decode) đã xong sẵn trong background.

6.2 Tại sao Telegram fork AsyncDisplayKit?

AsyncDisplayKit gốc (Texture) có nhiều thứ Telegram không cần và không match:

Giữ lại (~35% gốc):

  • Core node system (ASDisplayNode, ASControlNode)
  • Text nodes (ASTextNode, TextNode based CoreText)
  • Image nodes (ASImageNode, ASNetworkImageNode)
  • Layout specs (ASStackLayoutSpec, ASInsetLayoutSpec, …)
  • Basic list rendering primitives

Xóa bỏ:

  • ASCollectionNode, ASTableNode — thay bằng custom ListView của Telegram
  • ASViewController — Telegram có ViewController riêng tích hợp Signal
  • Yoga/Flexbox layout engine — quá phức tạp cho needs của Telegram
  • PINRemoteImage — thay bằng TransformImageNode + Signal
  • Debugging tools (ASDK debugger)
  • Pinterest/Facebook-specific utilities

Viết lại / thêm mới:

  • ListView — replace ASCollectionNode cho chat list/history (snap-to-bottom, pinned items, infinite scroll cả hai chiều)
  • TransformImageNode — image với Signal-based loading + transform pipeline (blur, round, crop)
  • ImmediateTextNode — text với inline link/mention/emoji, instant layout
  • NavigationController — custom replace UINavigationController cho iPad split view + interactive pop gesture toàn screen

6.3 ListView — heart of chat scrolling

ListView là module được tune nhất trong toàn iOS Telegram. Mỗi chat history với 100k+ messages chạy mượt 60fps trên iPhone X (2017). Cách họ làm:

1. Item layout độc lập với scroll position. Item ID là (peerId, namespace, messageId) — không phải index. ListView track items theo ID, scroll bằng cách insert/remove items khỏi viewport, không phải bằng cách shift offset.

2. Async layout pipeline. Khi nhận messages mới, ListView không apply ngay. Pipeline:

new messages → TransitionContext → 
    background-thread: create nodes + measure layout →
    main-thread: apply diff (insert/remove/move) với animation

3. Pre-loading 2 màn hình. Trên/dưới viewport luôn có sẵn ~2 screen height của items đã layout. Scroll nhanh không khựng.

4. Recycling — nhưng không như UICollectionView. ListView không reuse cell theo cách của UIKit. Mỗi message có node riêng tồn tại đúng lúc on-screen + buffer. Trade-off: tốn RAM hơn, đổi lại không có “flash of wrong content” khi scroll nhanh.

5. Snap-to-bottom logic. Khi user ở cuối chat và message mới đến, ListView tự scroll xuống. Nhưng nếu user đã scroll lên đọc lịch sử, KHÔNG scroll. Logic này thông qua FollowsBottom state — sai một chút là UX phá ngay.

6.4 TextNode dựa trên CoreText

Telegram không dùng TextKit/NSAttributedString render mà dùng CoreText trực tiếp:

class TextNode: ASDisplayNode {
    class func asyncLayout() -> (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode) {
        // Layout chạy background thread
        // CoreText: create CTFramesetter → get suggested size → CTFrame
        // Return immutable layout result + builder closure
    }
}

Lý do CoreText:

  • Async-safe. TextKit (UILabel/UITextView) có internal state thread-confined đến main thread. CoreText pure C API, an toàn từ bất kỳ thread nào.
  • Faster. TextKit có overhead UIKit. CoreText lower-level, ~2x faster cho complex layout.
  • Custom selection / link detection. Telegram tự implement long-press selection, link tap targets, quote highlight với CoreText API trực tiếp.

Cái giá: code phức tạp gấp 5-10 lần so với UILabel. TextNode.swift ~1500 dòng để render text mà UILabel làm bằng 1 dòng. Nhưng đổi lại scroll mượt + message render đúng cả với arabic, hebrew, emoji, mention, link.

6.5 TransformImageNode — image pipeline

Thay vì SDWebImage/Kingfisher, Telegram có:

class TransformImageNode: ASDisplayNode {
    func setSignal(_ signal: Signal<ImageDataTransformation, NoError>)
    
    // Custom transform pipeline
    func asyncLayout() -> (TransformImageArguments) -> (() -> TransformImageNode)
}

Pipeline khi load avatar tròn 40px:

Signal source (Postbox/Network)
    ↓ raw image bytes
Decode (background thread, ImageIO)
    ↓ UIImage
Apply transform: round corners + size 40x40
    ↓ pre-rendered UIImage
Cache transformed result trong memory
    ↓ result
Set on layer.contents trên main thread

Khác biệt với SDWebImage: transform là phần của pipeline, không phải post-processing. Vì:

  • 1000 chat list items mỗi item 40×40 round avatar
  • Render round corner bằng layer.cornerRadius thì GPU làm mỗi frame → tốn battery
  • Pre-render round avatar trong bitmap → GPU chỉ blit pixel → mượt + tiết kiệm pin

7. Module system với Bazel — 270+ submodules

Đây là phần kiến trúc gây sốc nhất với dev iOS quen Xcode native. Telegram có hơn 270 module riêng biệt, dùng Bazel build system thay vì Xcode workspace.

7.1 Tại sao 270 modules?

Mỗi UI screen lớn = 1 module. Mỗi feature lớn = 1 module. Mỗi util shared = 1 module:

ChatListUI/             # Module riêng cho chat list screen
├── BUILD               # Bazel build file
├── Sources/
│   ├── ChatListController.swift
│   ├── ChatListNode.swift
│   └── ...
└── README.md (tùy)

ChatMessageUI/          # Module riêng cho message bubbles
├── BUILD
├── Sources/
│   ├── ChatMessageBubbleNode.swift
│   ├── ChatMessageItem.swift
│   └── ... (~50 files)
└── ...

Lợi ích:

1. Incremental build siêu nhanh. Sửa 1 file trong ChatMessageUI chỉ rebuild module đó + dependent. Không phải build cả 1M dòng. Build incremental ~10-30 giây thay vì 5-10 phút.

2. Compile-time enforcement of boundaries. Module B dùng module A phải declare dependency trong BUILD file. Không có “vô tình import lung tung”.

3. Parallel build. Bazel build các module độc lập song song → tận dụng tối đa CPU cores.

4. Test isolation. Mỗi module có test target riêng. Test chạy nhanh hơn nhiều.

5. Code reuse cho extensions. NotificationService extension chỉ link Postbox, MtProtoKit, TelegramCore — không link UI modules. Extension memory limit 24MB (rất chặt) nên cần linking tối thiểu.

7.2 Bazel BUILD file ví dụ

# submodules/ChatListUI/BUILD
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")

swift_library(
    name = "ChatListUI",
    module_name = "ChatListUI",
    srcs = glob([
        "Sources/**/*.swift",
    ]),
    copts = [
        "-warnings-as-errors",
    ],
    deps = [
        "//submodules/AsyncDisplayKit:AsyncDisplayKit",
        "//submodules/Display:Display",
        "//submodules/SwiftSignalKit:SwiftSignalKit",
        "//submodules/Postbox:Postbox",
        "//submodules/TelegramCore:TelegramCore",
        "//submodules/TelegramPresentationData:TelegramPresentationData",
        "//submodules/TelegramUIPreferences:TelegramUIPreferences",
        "//submodules/AccountContext:AccountContext",
        # ... 30-40 deps khác
    ],
    visibility = [
        "//visibility:public",
    ],
)

So với SPM (Package.swift), Bazel verbose hơn nhiều, nhưng chính xác hơn — bạn thấy rõ module nào dùng cái gì.

7.3 Trade-off của Bazel

Không phải free lunch:

Khó setup. New developer mất 1-2 ngày để biết cách build (Make.py orchestrate Bazel + Xcode project generation).

Xcode integration không native. Phải chạy Make.py generateProject để có file .xcodeproj — file này là autogenerated từ Bazel. Sửa trong Xcode rồi chạy lại generate.

Apple SDK version pinned. versions.json lock chặt Xcode version. Apple ra Xcode mới → Telegram phải test compatibility trước.

Build cache cần local. --cacheDir="$HOME/telegram-bazel-cache" — folder ~50GB nếu build full architectures.

7.4 Khi nào nên dùng Bazel cho iOS app?

Nếu bạn đang viết app SwiftUI 50k dòng, đừng dùng Bazel. SPM/Xcode đủ.

Bazel hợp lý khi:

  • Codebase >500k dòng
  • Có >50 internal modules
  • Team >10 dev iOS
  • CI build time là pain point chính
  • Cần share code với Android (Bazel build cả hai)

Telegram match đủ 5 tiêu chí. JustCha/app SaaS Việt Nam thông thường không match điều nào.


8. Account isolation — multi-account architecture

Cũng giống Android, iOS hỗ trợ multi-account, nhưng implementation khác:

8.1 Account class

public final class Account {
    public let id: AccountRecordId
    public let postbox: Postbox        // DB instance riêng
    public let network: Network        // MTProto network riêng
    public let stateManager: AccountStateManager
    public let mediaReferenceRevalidationContext: MediaReferenceRevalidationContext
    public let pendingMessageManager: PendingMessageManager
    public let messageMediaPreuploadManager: MessageMediaPreuploadManager
    // ... ~20+ subsystems
}

Mỗi account là một instance Account đầy đủ với postbox + network + state riêng. Khác Android (singleton-per-account-int), iOS dùng instance object — Swift xử lý lifetime tự nhiên hơn.

8.2 SharedAccountContext

public final class SharedAccountContext {
    public let accountManager: AccountManager
    public var activeAccountsWithInfo: Signal<(primary: AccountRecordId?, accounts: [AccountWithInfo]), NoError>
    
    // Shared resources giữa các account
    public let mediaManager: MediaManager        // Audio/video player chia sẻ
    public let locationManager: LocationManager  // Location chia sẻ
}

Shared context giữ những resource toàn app: media playback (chỉ 1 audio playing tại 1 thời điểm bất kể account nào), location, contacts. Mỗi account giữ resource riêng cho dialog, message, profile.

8.3 Account switching

sharedAccountContext.switchToAccount(id: accountId, fromSettingsController: nil)

Switch account chỉ là đổi pointer, không tear down instance. Inactive accounts vẫn:

  • Nhận push notification
  • Sync state nếu app foreground
  • Có dữ liệu sẵn để switch instant

Trade-off: tốn RAM (~50-100MB per account active). iOS Premium cho 4 accounts → ~400MB chỉ cho account state.


9. Build system — Bazel + Make.py

9.1 Build pipeline

.bazelrc + WORKSPACE + MODULE.bazel
        ↓ (Bazel resolve deps)
build-system/Make/Make.py 
        ↓ orchestrate
Bazel build target (swift_library × 270)
        ↓ output
.framework binaries
        ↓ link
.app bundle
        ↓ codesign
.ipa

9.2 Reproducible builds — chìa khóa tin cậy

Telegram iOS public process verify rằng IPA trên App Store build từ chính source GitHub. Quy trình:

  1. Setup Bazel với --cacheDir đúng
  2. Lock Xcode version theo versions.json
  3. Build với python3 build-system/Make/Make.py build --configuration=release_arm64
  4. Tải .ipa của bản App Store về (qua App Store Connect)
  5. Diff binary

Khác biệt cho phép: codesign signature, provisioning profile, build timestamp. Không cho phép: bất kỳ byte nào trong code section khác nhau.

Telegram là một trong cực ít messenger thương mại publicly verifiable như vậy. Signal cũng làm được. WhatsApp/Messenger/Zalo không mở source.

9.3 ARM64 only cho production

iOS Telegram không build x86_64 cho production. Build x86_64 chỉ cho simulator. Lý do:

  • iOS 11+ đã drop 32-bit hoàn toàn
  • Apple Silicon iPad/iPhone đều ARM64
  • Bỏ x86_64 → giảm app size ~40%

Android phải ship arm64-v8a + armeabi-v7a + x86 + x86_64 cho compatibility. iOS gọn hơn nhiều.


10. Performance optimizations đáng học

10.1 Pre-rendered text layouts cached

Mỗi MessageItem có cached TextNodeLayout. Layout chạy 1 lần trên background → cache → onDraw chỉ gọi CTFrameDraw. Tương tự Android StaticLayout ở Phần 1 — cùng triết lý cross-platform.

10.2 Image transform cache

TransformImageNode cache theo (imageURL, transform_args) key. Avatar 40px tròn của user X chỉ render 1 lần trong session, mọi screen sau dùng cached bitmap.

10.3 Preloaded chat history

Khi user mở chat, Postbox load 40 messages quanh anchor + return view. Nhưng đồng thời, prefetch tiếp 80 messages trên + 80 messages dưới vào cache. Scroll lên = tức thời, không có “loading older messages…” spinner.

10.4 Lazy node materialization

Cell trong viewport buffer (chưa visible) layout xong nhưng chưa create UIView. Khi scroll vào viewport → materialize view. Khi scroll khỏi buffer → release view (giữ lại layout). Trade-off RAM vs scroll smoothness — tune theo device:

// On iPhone 6/7 (1GB RAM): buffer 1.5 screens
// On iPhone X+ (3+ GB RAM): buffer 3 screens

10.5 Background message decryption

Khi message mới đến qua APNs (NotificationService extension), decrypt + render preview ngay trong extension (24MB memory limit). Khi user mở app, message đã sẵn trong Postbox — không cần re-fetch. Lock screen notification show được nội dung thật, không phải “You have a new message”.

Đây là điều iOS làm tốt hơn Android — Android FCM payload bị đơn giản hóa hơn, không E2E giải mã trong service được vì memory + thread constraint khác.

10.6 Metal-based sticker rendering

Animated stickers (.tgs Lottie + .webm AnimatedSticker) render bằng Metal shader thay vì CoreAnimation. 30 sticker animation trên một màn hình 60fps không drop frame trên iPhone X. CoreAnimation sẽ stutter ngay.

Module AnimatedStickerNode link trực tiếp Metal framework. Phần này iOS-only, Android dùng Vulkan/OpenGL ES tương ứng.


11. Khác biệt thiết kế Android vs iOS — bảng so sánh

Đây là phần đặc biệt cho ai đọc cả 2 phần — bảng tổng hợp những quyết định khác nhau giữa hai platform với cùng chức năng:

Khía cạnhAndroidiOS
Build systemGradle + CMakeBazel + Make.py
Module count2 (lib + app variants)270+ submodules
ReactiveNotificationCenter patternSSignalKit / SwiftSignalKit
Database wrapperTự viết JNI wrapper SQLitePostbox (Signal-based, view-driven)
Network implC++ (tgnet/)Obj-C (MTProtoKit)
UI renderingView thuần + tự draw CanvasAsyncDisplayKit fork (node + spec)
List renderingRecyclerView + custom CellsCustom ListView (không phải UICollectionView)
Text renderingStaticLayout (Android API)CoreText (low-level)
Image loadingImageReceiver (tự viết)TransformImageNode (Signal-based)
NavigationCustom Fragment systemCustom NavigationController
Multi-accountSingleton-per-account-intAccount instance object
Push notification decryptLimited bởi FCM limitsFull E2E decrypt trong NSE
CryptoBoringSSL nativeBoringSSL native (shared)
Min OSAndroid 5.0 (API 21)iOS 12.0
ABI shippedarm64 + arm32 + x86 + x64arm64 only
Reproducible buildsapkdiff.pyBazel lock + binary diff

Một số quyết định khác nhau có lý do chính đáng:

  • NotificationCenter trên Android, Signal trên iOS: Android kế thừa từ codebase 2013 chưa có reactive ngon. iOS bắt đầu sau, có chỗ để thiết kế đẹp hơn.
  • 2 modules vs 270 modules: Android Gradle/AGP có overhead lớn cho mỗi module (10-30s configure time/module). 270 modules trên Android = build time 30-60 phút. iOS Bazel có overhead thấp hơn nhiều.
  • C++ vs Obj-C cho network: Android cần JNI bridge → C++ là natural choice. iOS có Obj-C → Swift bridge native → không cần đi qua C++.

Một số quyết định giống nhau hợp lý:

  • Tự viết network layer (cross-platform code reuse)
  • Không dùng Glide/SDWebImage (cần custom transform pipeline)
  • Không dùng Room/CoreData (need raw SQL performance)
  • BoringSSL chung
  • rlottie cho stickers

12. Bài học cho dev iOS Việt

Đặc biệt cho ai đang làm app messaging, social, marketplace, hay edtech với real-time UI mạnh:

1. Reactive matters, nhưng đừng tự viết.

SSignalKit của Telegram tốt — vì 2014. Năm 2026 bạn có Combine và Swift Concurrency. Đừng reinvent. Pattern Promise / ValuePromise của Telegram tương đương CurrentValueSubject của Combine. Pattern MetaDisposable (cancel previous, bind new) → dùng Task của async/await với handle cancel:

private var searchTask: Task<Void, Never>?

func search(_ query: String) {
    searchTask?.cancel()
    searchTask = Task {
        guard let results = try? await api.search(query) else { return }
        if Task.isCancelled { return }
        await MainActor.run { self.results = results }
    }
}

2. Database reactive là superpower thật.

Pattern Postbox.aroundMessageHistoryView (live observable view) — đây là khác biệt tạo ra UX của Telegram. Trên iOS hiện tại bạn có NSFetchedResultsController (CoreData), ValueObservation (GRDB), hoặc FetchRequest (SwiftData). Dùng đi. Đừng query một lần rồi tự refresh manual.

3. AsyncDisplayKit giờ đã già.

Năm 2026 bạn có 3 lựa chọn cho UI heavy:

  • SwiftUI — đủ cho 90% app, performance đã tốt từ iOS 16+
  • UIKit + UIHostingController — hybrid khi cần fine-tune
  • Compositional Layout + Diffable Data Source — nếu cần list cực nặng

Chỉ adopt AsyncDisplayKit/Texture nếu thực sự ship 1M+ DAU và chứng minh được UIKit/SwiftUI không đủ. Cái giá maintain framework fork đắt.

4. Module hóa khôn ngoan, đừng cực đoan.

270 modules của Telegram phù hợp khi bạn có 30 dev iOS làm việc song song. App 5 dev không cần thế. Nhưng đừng bỏ qua module hóa hoàn toàn — tách Core (data + business) khỏi App (UI) là minimum. Khi cần Widget Extension, NotificationService Extension, Share Extension, App Clip — bạn sẽ cảm ơn quyết định đó.

5. Background extension xứng đáng đầu tư.

iOS có NotificationService Extension cho push, ShareExtension cho share, WidgetKit cho widget. Mỗi cái có memory limit chặt (24MB/120MB/30MB) — nhưng dùng đúng cách = UX vượt trội. Pattern Telegram: shared App Group + Postbox shared, các extension đọc/ghi DB chung. Học làm.

6. Build system đầu tư xứng đáng.

Với codebase >100k dòng Swift, build time là pain. Đừng vội Bazel, nhưng:

  • Tách module SPM ngay từ đầu
  • Disable “Find Implicit Dependencies” trong Xcode
  • Dùng Build Active Architecture Only = YES cho debug
  • Cache DerivedData trên SSD external nếu dev MacBook RAM thấp

7. Reproducible build = trust với user.

Nếu app bạn xử lý dữ liệu nhạy cảm (banking, healthcare, fintech), cân nhắc làm reproducible build và publish process. User sẽ không tự verify, nhưng researcher / journalist / regulator sẽ. Telegram làm được vì Bazel deterministic — nếu bạn dùng SPM cũng có thể đạt mục đích tương tự với careful setup.


13. Tài nguyên tham khảo

Repository chính:

Documentation:

Source code walkthrough series (rất recommended):

Liên quan:


Kết series

Hai phần series này đã đi qua:

  • Phần 1 (Android): kiến trúc 2 module, native code C++ qua JNI, NotificationCenter event bus, Cells tự draw bằng Canvas, ImageReceiver, build với Gradle + CMake.
  • Phần 2 (iOS): kiến trúc 270 modules với Bazel, MTProtoKit Obj-C, SSignalKit reactive framework, Postbox database reactive-first, AsyncDisplayKit fork, ListView/TextNode/TransformImageNode, build reproducible với Make.py.

Điểm đáng kinh ngạc nhất: hai team Android và iOS Telegram không sync hằng ngày như team chung dự án. Họ ship feature theo quarter, share schema TL + native crypto/codec layer, còn lại tự do thiết kế platform-specific. Vậy mà UX trên hai platform giống nhau đến 95%. Đó là minh chứng: nếu bạn align trên data model + business invariants, UI/architecture có thể khác hoàn toàn vẫn đạt UX nhất quán.

Nếu bạn đang ở vị trí phải quyết định “có nên share code Android-iOS không?”, câu trả lời từ Telegram: share data layer (TL schema, MTProto core, crypto). Không share UI. Không share state management. Để mỗi team làm tốt nhất theo platform của họ.


Bài viết được biên soạn dựa trên source code công khai TelegramMessenger/Telegram-iOS, source walkthrough series của Bo Hu, và tài liệu kiến trúc thực hành.

Đọc Phần 1: Telegram Android trước để có context đầy đủ.

Code Toàn Bug

Code nhiều bug nhưng biết cách giấu!

Leave a Reply

Your email address will not be published. Required fields are marked *