LY Corporation Tech Blog

Quảng bá công nghệ và văn hoá phát triển hỗ trợ cho các dịch vụ cung cấp bởi LY Corporation và LY Corporation Group (LINE Plus, LINE Taiwan, LINE Vietnam).

Một cách mới để quản lý dữ liệu lưu trữ trong chat trên phiên bản iOS của LINE

Trong khi dung lượng lưu trữ của thiết bị di động ngày càng lớn, dữ liệu của người dùng cũng ngày càng tăng theo. Người dùng có thể sở hữu hàng ngàn hình ảnh hoặc video chất lượng cực cao và điều này có thể nhanh chóng tiêu tốn dung lượng lưu trữ của họ. Hơn nữa, nội dung số có thể được chia sẻ dễ dàng giữa các người dùng trên nhiều ứng dụng mạng xã hội hoặc ứng dụng nhắn tin như ứng dụng LINE của chúng tôi hoặc Instagram, Messenger, ... Sự trùng lặp dữ liệu này thậm chí còn tạo thêm áp lực lên dung lượng lưu trữ thiết bị. Ứng dụng LINE của chúng tôi ngoài ra còn cho phép người dùng chia sẻ hình ảnh của họ với kích thước gốc trong cuộc trò chuyện, điều này có thể chiếm một lượng lưu trữ đáng kể. Điều quan trọng là dung lượng lưu trữ thiết bị có hạn và thường không thể thay đổi sau khi người dùng mua về. Người dùng không có lựa chọn nào khác ngoại trừ việc dọn dẹp dữ liệu của họ, mua thêm lưu trữ đám mây (ví dụ iCloud), hoặc chuyển sang thiết bị mới với dung lượng lớn hơn.

Nhiều người dùng LINE có thể gửi tin nhắn chứa dữ liệu quan trọng mà họ muốn giữ lại trên thiết bị của mình. Đây là một tính năng phổ biến vì không có nhiều dịch vụ nhắn tin có khả năng lưu trữ dữ liệu người dùng một cách liên tục và lâu dài trên trung tâm dữ liệu của họ. Bất cứ khi nào có một lượng lớn dữ liệu chiếm dụng một thiết bị, chúng tôi cần một cách tiện lợi để giúp người dùng quản lý dữ liệu đó một cách hiệu quả. Ví dụ, người dùng có thể muốn có cái nhìn tổng quan về cách cuộc trò chuyện của họ đang sử dụng phần lớn dung lượng lưu trữ để họ có thể xem xét việc xóa bớt một số để tiết kiệm dung lượng lưu trữ. Cài đặt lưu trữ của iPhone chỉ hiển thị dung lượng lưu trữ mà một ứng dụng đang sử dụng nhưng không có nhiều thông tin hay hành động mà người dùng có thể thực hiện, ngoại trừ tùy chọn "Xóa ứng dụng" mà đa số trường hợp chỉ dùng khi không còn cách nào khác.

Trong các phần tiếp theo, chúng tôi sẽ giải thích thêm về giải pháp trước đây của chúng tôi và giải pháp mới sẽ thay thế nó như thế nào.

Giải pháp đã dùng trước đây

Để giúp người dùng quản lý dữ liệu chat của họ một cách hiệu quả, các kỹ sư của chúng tôi đã cung cấp một tính năng gọi là "Xóa dữ liệu" vào năm 2016, và giao diện người dùng hiện tại đã tồn tại từ lâu. Trong quá trình bảo trì tính năng hiện tại, chúng tôi đã nhận được nhiều yêu cầu từ người dùng về những khó khăn mà họ gặp phải khi sử dụng tính năng này. Với sự giúp đỡ của đội ngũ Dịch vụ Khách hàng (CS), chúng tôi đã xác định được hai khó khăn chính:

  • "Chỉ số tiến trình" hiển thị trên màn hình không cho chúng ta biết nhiều về ý nghĩa của nó là gì, điều này đôi khi gây khó chịu cho người dùng khi họ phải chờ nó hoàn tất. Thực tế, chỉ số này hiển thị số lượng hiện tại của các cuộc trò chuyện đã được tính toán hoặc đồng bộ hóa trước khi hiển thị kích thước lưu trữ cuối cùng cho người dùng.
  • Việc tính toán và đồng bộ hóa dữ liệu cho một số người dùng có thể mất rất nhiều thời gian để hoàn tất. Điều này có thể dẫn đến một vấn đề khác: Người dùng không thể tương tác với ứng dụng cho đến khi toàn bộ quá trình này hoàn tất, ngay cả khi quá trình đó đã được thực hiện một phần.

Ngoài những điểm trên, người dùng không có quá nhiều lựa chọn để xóa dữ liệu của họ. Ví dụ, khi họ muốn dọn dẹp dữ liệu trò chuyện của mình, cách nhanh nhất là xóa theo danh mục (hình ảnh, âm thanh, tệp, hoặc tất cả dữ liệu trò chuyện). Điều này sẽ xóa toàn bộ dữ liệu từ tất cả các cuộc trò chuyện mà họ có. Nếu họ muốn giữ lại một số dữ liệu, họ không có lựa chọn nào khác ngoại trừ việc vào phần cài đặt của từng cuộc trò chuyện và xóa từng cái một, điều này có thể gây bất tiện và rất tốn thời gian.

Sau nhiều nghiên cứu và thảo luận cẩn thận, chúng tôi đã đưa ra những cải tiến cho UX/UI cùng với giải pháp giúp hiệu suất tính toán tốt hơn.

Một cách mới để quản lý dữ liệu chat

Những cải tiến trong giai đoạn đầu của chúng tôi bao gồm:

  • Giao diện người dùng mới để hiển thị dung lượng ứng dụng LINE giúp mô tả tốt hơn về việc sử dụng lưu trữ LINE và thông tin về dung lượng thiết bị. Trong trường hợp này, chúng tôi đã hiển thị một biểu đồ đơn giản, vì đây là cách dễ nhất để minh họa cho người dùng để họ có thể chỉ nhìn sơ qua là nắm bắt tổng quan về dung lượng lưu trữ của mình. Ngoài ra, chỉ số tiến trình trở nên ý nghĩa hơn và người dùng có thể tương tác với dữ liệu của họ ngay cả khi tác vụ tính toán đang diễn ra.
  • Giới thiệu một cách mới để lưu trữ dữ liệu trò chuyện và nhanh chóng tính toán kích thước lưu trữ.
  • Cải thiện UX bằng cách cung cấp cho họ một cách mới để quản lý dữ liệu trò chuyện của họ. Trong trường hợp này, chúng tôi sắp xếp các cuộc trò chuyện của họ vào một danh sách đã được sắp xếp cũng như hiển thị chi tiết về chúng một cách riêng biệt.

Chi tiết về những cải tiến này sẽ được liệt kê trong các phần sau.

Cải tiến tính toán dung lượng lưu trữ trò chuyện

Phiên bản iOS của ứng dụng LINE sử dụng CoreData để lưu trữ trò chuyện và tin nhắn; một framework phức tạp nhưng rất mạnh của Apple. Mô hình tin nhắn (Message Model) chứa nội dung và một số thông tin về dữ liệu trên bộ nhớ cục bộ của người dùng, chẳng hạn như loại tệp hoặc đường dẫn tệp. Những mô hình này đóng vai trò quan trọng nhất trong LINE chat và chúng đủ ổn định để chúng tôi sử dụng làm nguồn dữ liệu chính xác cho bất kỳ phép tính nào liên quan đến kích thước dữ liệu trò chuyện. Đối với yêu cầu của tính năng, chúng tôi muốn tính toán tổng kích thước dữ liệu trò chuyện (từ tất cả các tin nhắn trong trò chuyện) dựa trên mô hình hiện có. Chúng tôi có thể đơn giản thu gom tất cả các tin nhắn và thực hiện phép tính kích thước tệp (ví dụ, sử dụng API của FileManager ). Giải pháp này nghe có vẻ khá là đơn giản. 

Tuy nhiên, thực tế lại không đơn giản như vậy. Chúng tôi có nhiều người dùng mà các cuộc trò chuyện của họ có thể chứa một số lượng lớn tệp (chúng tôi đang nói về số lượng, không chỉ kích thước bản thân mỗi tệp). Điều này có thể làm giảm đáng kể hiệu suất và mất rất nhiều thời gian cho tác vụ tính toán do chi phí phát sinh từ các hoạt động đọc tệp. Như được nhắc tới trong tài liệu của Apple:

Relative to other operations, accessing files on disk is one of the slowest operations a computer can perform. Depending on the size and number of files, it can take anywhere from a few milliseconds to several minutes to read files from a disk-based hard drive. (So với các hoạt động khác, việc truy cập tệp trên đĩa là một trong những hoạt động chậm nhất mà máy tính có thể thực hiện. Tùy thuộc vào kích thước và số lượng tệp, việc đọc tệp từ ổ cứng dựa trên đĩa có thể mất từ vài mili giây đến vài phút.)

Ngay cả trên một số thiết bị hiện đại nhất ngày nay, vấn đề này vẫn còn đang tồn tại. Và nó còn tồi tệ hơn khi phải thực hiện cùng một tác vụ lặp đi lặp lại mỗi khi người dùng sử dụng tính năng đó. Để tránh điều này, chúng tôi đã sử dụng một trong những kỹ thuật quen thuộc được gọi là "lưu trong bộ nhớ cache". Chúng tôi muốn thực hiện ít phép tính nhất có thể trong khi tái sử dụng những thứ đã được tính toán.

Với ý tưởng trên, chúng tôi đã sử dụng một bộ lưu trữ để lưu thông tin về dung lượng trò chuyện. Trong trường hợp này, chúng là dữ liệu tin nhắn như hình ảnh, âm thanh, file và video. Chúng tôi đồng thời cũng muốn tránh việc phụ thuộc vào một giải pháp cụ thể nào trong việc lưu trữ ví dụ SQLite, Realm hay CoreData framework, đồng thời cũng tránh việc phụ thuộc vào các business model cụ thể nên chúng quyết định trừu tượng hóa chúng thành StorageManagementAbstractDatabase và sử dụng StorageItemModelable làm đơn vị lưu trữ dữ liệu. Model này chứa những loại thông tin như sau:

protocol StorageItemModelable {
    var id: String { get }
    var serviceType: StorageManagementDatabaseScheme.ServiceType { get }
    var groupId: String { get }
    var contentId: String { get }
    var contentSubId: String? { get }
    var contentType: String { get }
    var contentSizeInBytes: Int64 { get }
    var createdDate: Date { get }
    var fileName: String { get }
    var additionalData: String? { get }
}

Protocol này có thể được dùng bởi persistent store (trong trường hợp của chúng tôi, là SQLite) khi lưu dữ liệu vào backing store (File, Database). Để có thể lấy lại dữ liệu từ bộ lưu trữ, ta cần thêm một protocol mà sẽ được dùng khi khởi tạo lại model sau khi quá trình lấy lên từ bộ lưu trữ hoàn tất:

protocol StorageItemConstructible {
    init(from item: StorageItemModelable)
}

Một trong những sự tiện lợi của hướng xử lý này là nó có thể được dùng để lưu dữ liệu từ các dịch vụ (service) khác, không chỉ cho dịch vụ Chat. Bằng việc kết hợp các protocol ở trên, ta có thể tạo một kiểu tổng quát như sau:

typealias StorageItemConvertible = StorageItemModelable & StorageItemConstructible

Dưới đây là một ví dụ sử dụng nó trong dịch vụ Chat:

struct ChatStorageItemModel: StorageItemConvertible {
    let id: String
    let chatId: String
    let messageId: String
    ...
    var serviceType: StorageManagementDatabaseScheme.ServiceType {
        .chat
    }
 
    var groupId: String {
        chatId
    }
 
    var contentId: String {
        messageId
    }
}
extension ChatStorageItemModel { // for generating model after fetching from persistent store
    init(from item: StorageItemModelable) {
        id = item.id
        chatId = item.groupId
        messageId = item.contentId
        ...
    }
}

Trong ví dụ trên, chúng tôi định nghĩa một model mới chứa thông tin mà chúng tôi muốn lưu vào bộ lưu trữ và sau đó chỉ cần adapt với giao thức StorageItemConvertible. Giao thức ChatStorageItemModel ở trên là cái mà chúng tôi sử dụng cho việc cải tiến tính năng. Và sau khi lưu vào bộ lưu trữ, chúng tôi có thể nhanh chóng tính toán kích thước dữ liệu đã lưu trữ của chat (ví dụ, bảng SQLite sử dụng toán tử SUM aggregation với độ phức tạp thời gian ~O(n)).

Khi người dùng sử dụng tính năng này lần đầu, một công việc tuy nặng nhọc nhưng cần thiết nhất là tổng hợp dữ liệu hiện tại (lưu trữ dữ liệu hiện có trong thiết bị vào bộ lưu trữ), quá trình này được xử lý dần (các cuộc trò chuyện đã hoàn tất xử lý sẽ được hiển thị trước thay vì phải chờ cho đến khi tất cả đều hoàn tất). Quá trình này có thể được minh họa trong sơ đồ dưới đây:

Xử lý vấn đề không nhất quán trong dữ liệu

Phương pháp tính toán dung lượng lưu trữ trên có thể rất nhanh. Nhưng, nó cũng cần phải đáng tin cậy trong bất kỳ tình huống nào. Trong phương pháp của chúng tôi, chúng tôi cập nhật cửa hàng liên tục mỗi khi có bất kỳ sự kiện nào liên quan đến dữ liệu trò chuyện (ví dụ, nhận một tin nhắn hình ảnh, xóa một tin nhắn chứa tệp đính kèm). Để làm cho điều này đáng tin cậy hơn, chúng tôi cần đảm bảo tính chính xác của bộ lưu trữ một cách tốt nhất có thể. Bởi một số nguyên nhân nào đó có thể tồn tại gây ra sự không nhất quán dữ liệu giữa bộ lưu trữ và các tệp trên đĩa. Ví dụ, ứng dụng có thể bị lỗi vào thời điểm nó nhận bất kỳ sự kiện tin nhắn chèn/xóa nào trước khi nó cập nhật các thay đổi vào bộ lưu trữ. Chúng tôi gọi đây là bước đồng bộ hóa. Logic để kiểm tra có thể được mô tả như sau:

Để đảm bảo có trải nghiệm người dùng tốt hơn, việc kiểm tra như trên cần được thực hiện ngầm, tránh việc người dùng nhận ra và cố gắng ảnh hưởng tới hiệu suất hệ thống càng ít càng tốt. Ngoài ra, nhiệm vụ này có thể chạy độc lập với luồng sử dụng tính năng của người dùng nên chúng tôi có thể lên lịch cho nó chạy khi thiết bị đang ở trạng thái idle, ví dụ, trong khi thiết bị đang sạc.

Apple cũng đã giới thiệu một framework gọi là BackgroundTasks trong bài giới thiệu của họ ở WWDC19. Framework này có thể giúp chúng tôi lên lịch các tác vụ nặng để chạy vào thời điểm thích hợp ở chế độ nền (background). Điều này có nghĩa là hệ thống sẽ khởi chạy ứng dụng của chúng tôi vào một thời điểm nào đó và sau đó thực hiện công việc đã lên lịch ở chế độ nền, vì vậy mang lại hiệu năng tốt hơn đồng thời tránh gây ảnh hưởng tới trải nghiệm người dùng. Các tác vụ được lên lịch chỉ chạy khi thiết bị đang không được sử dụng và đang sạc sẽ ảnh hưởng ít hơn đến hiệu suất. Với cách tiếp cận này, chúng tôi có thể đảm bảo độ chính xác của dữ liệu người dùng mà không ảnh hưởng quá nhiều đến hiệu suất hệ thống và pin.

Hiển thị danh sách các hội thoại đang chiếm dung lượng lưu trữ

Một trong những lợi ích khi chúng tôi cấu trúc cơ sở dữ liệu như trên là việc chúng tôi có thể dễ dàng gom nhóm các mục theo các điều kiện cụ thể, trong trường hợp này là chatId. Khi nhóm các mục như vậy, chúng tôi có thể giúp người dùng quản lý dữ liệu của họ, vì nội dung của một số cuộc trò chuyện có thể quan trọng hơn với họ so với những cuộc trò chuyện khác. Điều này khiến việc loại bỏ những cuộc trò chuyện không quan trọng nhưng chiếm nhiều dung lượng lưu trữ trở nên hợp lý hơn.

Tại WWDC19, Apple đã giới thiệu một trong những cải tiến thú vị của UIKit ở iOS 13, cụ thể là UI Data Sources, và một vài cách sử dụng nâng cao ở WWDC20. Cách tiếp cận của DiffableDataSource (UICollectionViewDiffableDataSource & UITableViewDiffableDataSource) không quá mới mẻ, và đã được sử dụng từ lâu ở các Reactive frameworks như RxSwift. Ý tưởng ở đây là xem data sources như một dạng của bản chụp của trạng thái hiện tại (snapshot). Snapshot này sẽ được thay thể bởi cái cũ và từ đó tìm ra được sự khác nhau giữa hai trạng thái mới và cũ, từ đó giúp cho việc cập nhật thay đổi ở collection view (hoặc table view), nhờ vậy cải thiện hiệu suất và tránh những lỗi xảy ra đối với cách sử dụng truyền thống, đặc biệt là khi sử dụng một "tập hợp" các thay đổi trong data source.

Sau đây là một vài lợi ích được nhắc tới:

  • "Snapshot" có thể được xem là một nguồn tin cậy của trạng thái UI, có thể bao gồm định danh (id) cho các sections và items, từ đó tránh việc phải tương tác trực tiếp vơi IndexPath.
  • Ta có thể tránh được tình trạng crash, các tình huống rắc rối hay sự phức tạp khi sử dụng performBatchUpdates() như cách truyền thống. Ta chỉ việc gọi snapshot.apply() và mọi việc cập nhật sau đó sẽ được tự động hoàn thành.

Với việc hỗ trợ chính thức từ Apple và tính ổn định của nó từ iOS 13 trở đi, chúng tôi đã áp dụng vào để phát triển tính năng này.

Tính toán tổng lượng sử dụng dung lượng lưu trữ của LINE

Tương tự như mọi ứng dụng iOS, ứng dụng LINE cũng có sandbox ứng dụng riêng (app sandbox), chứa gói ứng dụng và dữ liệu ứng dụng. Thư mục dữ liệu ứng dụng, mà chúng tôi có thể lấy bằng cách sử dụng đường dẫn NSHomeDirectory(), là phần chủ yếu ảnh hưởng đến tổng kích thước của ứng dụng. Nó chứa nhiều thư mục có thể đang quen thuộc với chúng ta, chẳng hạn như Library/Cache, Library/Application Support, Document/, ...

Apple cung cấp thông tin kích thước của mỗi ứng dụng trong Cài đặt (Cài đặt > Chung > lưu trữ iOS). Tuy nhiên, không có API nào để lấy được kích thước của ứng dụng, và thậm chí cũng không có tài liệu nào cung cấp bởi Apple cho biết cách tính toán nó. Tại thời điểm viết bài này, chúng tôi phải tính bằng cách thủ công.


Trải qua quá trình thử nghiệm trong việc thu thập toàn bộ dữ liệu ứng dụng sử dụng và tính toán kích thước để tương ứng với thông số hiển thị trong Cài đặt của iOS, chúng tôi đi đến công thức gần đúng như sau:

Kích thước ứng dụng* = Kích thước gói ứng dụng + (kích thước thư mục Data - Kích thước thư mục Cache) + kích thước của App Groups

( * ) Công thức xấp xỉ kích thước ứng dụng

Chúng tôi sử dụng URLResourceValues để tính toán kích thước file và thư mục. Có một số điểm cần lưu ý như sau:

  • fileSize: Tổng kích thước file, đơn vị bytes
  • fileAllocatedSizeTổng kích thước đã được cấp phát để lưu trữ file trên thiết bị, đơn vị bytes. Được tính bằng số lượng block nhân với kích thước mỗi block (tài liệu APFS)
  • totalFileSize & totalFileAllocatedSize: Có thể bao gồm kích thước siêu dữ liệu (metadata) sử dụng.

Chúng tôi sử dụng totalFileAllocatedSize & fileAllocatedSize để tính toán công thức trên.

Có một vài điểm thú vị về Cài đặt dung lượng lưu trữ của các ứng dụng của Apple:

  • Có vẻ như Apple cũng đã tính kích thước của App Groups là một phần của kích thước ứng dụng. Tuy nhiên điều này không được nhắc đến trong tài liệu của họ.
  • Thư mục Library/Cache của ứng dụng không được tính vào kích thước ứng dụng. 
  • Cài đặt dung lượng lưu trữ của các ứng dụng của Apple thỉnh thoảng không cập nhật thông tin mới nhất để hiển thị, ngay cả khi khởi động lại thiết bị.

Kết luận

Sau một thời gian dài nghiên cứu, thảo luận và thử nghiệm, chúng tôi cuối cùng đã tìm ra một giải pháp để cải thiện việc quản lý dữ liệu lưu trữ trò chuyện và đã phát hành tính năng cho phiên bản 12.14.0 của ứng dụng LINE. Đó là nỗ lực xứng đáng của các kỹ sư, người lập kế hoạch, đội UX và QA của chúng tôi. Chúng tôi hy vọng đã đóng góp vào việc cải thiện trải nghiệm cho người dùng, cả về mức độ hài lòng khi trò chuyện cũng như sử dụng ứng dụng LINE nói chung.