Sửa lỗi android.os.TransactionTooLargeException Khi Truyền Dữ Liệu Qua Intent Trên Android

intermediate📱 Android2026-05-10| Android 7.0+ (API 24+), mọi thiết bị — crash xảy ra khi chuyển đổi Activity/Fragment hoặc giao tiếp với Service qua Intent/Bundle

Error Message

android.os.TransactionTooLargeException: data parcel size X bytes
#android#intent#bundle#parcel#ipc

Crash Chỉ Xuất Hiện Trên Production

Lỗi này khiến tôi bất ngờ trên một tính năng đã vượt qua testing hoàn toàn. Người dùng nhấn vào một mục danh sách, app serialize một object và truyền sang màn hình chi tiết — boom, crash. Dữ liệu test của tôi rất nhỏ. Người dùng thực có dữ liệu thực: mô tả dài, mảng byte cho ảnh nhúng, danh sách lồng nhau phình to lên vài trăm kilobyte. Bộ đệm IPC transaction giới hạn ở 1 MB, dùng chung cho tất cả các transaction đang hoạt động trên thiết bị. Tôi đã vượt qua giới hạn đó mà không nhận được bất kỳ cảnh báo nào trong suốt quá trình QA.

Đây là stacktrace trông như thế nào:

android.os.TransactionTooLargeException: data parcel size 1,048,576 bytes
    at android.os.BinderProxy.transactNative(Native Method)
    at android.os.BinderProxy.transact(Binder.java:804)
    at android.app.IActivityManager$Stub$Proxy.startActivity(IActivityManager.java:3919)
    at android.app.Instrumentation.execStartActivity(Instrumentation.java:1609)
    at android.app.Activity.startActivityForResult(Activity.java:4586)

Con số sau data parcel size chính là manh mối. Bất kỳ dữ liệu nào tiệm cận hoặc vượt quá 1 MB khi truyền qua Bundle hoặc Intent đều sẽ kích hoạt exception này.

Chẩn Đoán Vấn Đề

Tìm hiểu bạn đang thực sự gửi gì

Thêm một đoạn kiểm tra kích thước nhanh trước khi gọi startActivity. Android không expose kích thước Bundle trực tiếp, nhưng bạn có thể đo bằng Parcel:

fun Bundle.sizeInBytes(): Int {
    val parcel = Parcel.obtain()
    parcel.writeBundle(this)
    val size = parcel.dataSize()
    parcel.recycle()
    return size
}

// Before startActivity
val bundle = intent.extras
Log.d("BundleSize", "Intent extras: ${bundle?.sizeInBytes()} bytes")

Hãy test với dữ liệu người dùng thực, không phải dữ liệu giả của bạn. Thấy con số trên ~500 KB? Bạn đã vào vùng nguy hiểm rồi — giới hạn 1 MB là dùng chung, không phải dành riêng cho transaction của bạn.

Tìm ra field nào gây ra vấn đề

Serialize từng field riêng lẻ để tìm thủ phạm:

val parcel = Parcel.obtain()
parcel.writeParcelable(myLargeObject, 0)
Log.d("ParcelSize", "myLargeObject: ${parcel.dataSize()} bytes")
parcel.recycle()

Trong hầu hết các trường hợp, thủ phạm là một bitmap nhét vào mảng byte, một danh sách các Parcelable object, hoặc một data class lồng nhau sâu mà khi serialize lại lớn hơn nhiều so với vẻ ngoài của nó. Một bitmap 1024×768 không nén nặng khoảng 3 MB dưới dạng mảng byte — đó là crash tức thì.

Giải Pháp

Phương án 1: Truyền ID, lấy dữ liệu ở phía bên kia

Đây là cách sửa gọn nhất. Thay vì chuyển toàn bộ object qua ranh giới IPC, hãy truyền định danh của nó và để màn hình đích tự tải dữ liệu.

// Sender
val intent = Intent(this, DetailActivity::class.java)
intent.putExtra("article_id", article.id)  // chỉ là Long hoặc String
startActivity(intent)

// Receiver (DetailActivity)
val articleId = intent.getLongExtra("article_id", -1)
viewModel.loadArticle(articleId)  // lấy từ Room/Repository

Hoạt động tốt khi dữ liệu của bạn nằm trong Room, cache Retrofit, hoặc bất kỳ kho lưu trữ local nào. ViewModel fetch bất đồng bộ trong khi UI hiển thị trạng thái loading. Đơn giản, dễ test, và tuân theo nguyên tắc single source of truth.

Phương án 2: Dùng shared ViewModel cho điều hướng Fragment-to-Fragment

Bỏ qua hoàn toàn Bundle argument cho các Fragment transition trong cùng một Activity. Một ViewModel có phạm vi Activity giữ object trong process memory — không có IPC, không có giới hạn kích thước.

// Shared ViewModel
class SharedViewModel : ViewModel() {
    val selectedItem = MutableLiveData<MyLargeObject>()
}

// Source Fragment
val sharedViewModel: SharedViewModel by activityViewModels()
sharedViewModel.selectedItem.value = largeObject
findNavController().navigate(R.id.action_to_detail)

// Destination Fragment
val sharedViewModel: SharedViewModel by activityViewModels()
sharedViewModel.selectedItem.observe(viewLifecycleOwner) { item ->
    // sử dụng item
}

Đây là pattern được khuyến nghị cho các ứng dụng single-Activity dùng Navigation Component. Không có serialization, không lo về kích thước.

Phương án 3: Cache tạm thời trong bộ nhớ

Cần vượt qua ranh giới Activity với dữ liệu phức tạp nhưng không thể dễ dàng persist? Một singleton cache nhẹ sẽ làm cầu nối:

object DataCache {
    private val cache = mutableMapOf<String, Any?>()

    fun put(key: String, value: Any?) { cache[key] = value }
    fun get(key: String): Any? = cache[key]
    fun remove(key: String) { cache.remove(key) }
}

// Sender
val cacheKey = UUID.randomUUID().toString()
DataCache.put(cacheKey, largeObject)
val intent = Intent(this, DetailActivity::class.java)
intent.putExtra("cache_key", cacheKey)
startActivity(intent)

// Receiver
val key = intent.getStringExtra("cache_key") ?: return
val largeObject = DataCache.get(key) as? MyObject
DataCache.remove(key)  // xóa ngay lập tức

Hãy xem đây là cầu nối tạm thời, không phải kho lưu trữ dữ liệu. Xóa mục cache ngay khi đã sử dụng xong để tránh memory leak.

Phương án 4: Ghi ra đĩa, truyền đường dẫn file

Bitmap và các blob nhị phân lớn thuộc về đĩa, không phải trong Bundle. Ghi file trước, sau đó truyền đường dẫn của nó:

// Sender — lưu bitmap ra file tạm
fun saveBitmapToCache(context: Context, bitmap: Bitmap): String {
    val file = File(context.cacheDir, "temp_image_${System.currentTimeMillis()}.jpg")
    file.outputStream().use { out ->
        bitmap.compress(Bitmap.CompressFormat.JPEG, 85, out)
    }
    return file.absolutePath
}

val path = saveBitmapToCache(this, largeBitmap)
val intent = Intent(this, ImageViewerActivity::class.java)
intent.putExtra("image_path", path)
startActivity(intent)

// Receiver
val path = intent.getStringExtra("image_path") ?: return
val bitmap = BitmapFactory.decodeFile(path)

Dọn dẹp các file tạm trong onDestroy hoặc chạy quét cache định kỳ. Để chúng tồn đọng sẽ lãng phí bộ nhớ và gây khó khăn cho các buổi debug sau này.

Phương án 5: Giảm bớt payload trước khi gửi

Thường bạn đang truyền toàn bộ model trong khi màn hình tiếp theo chỉ cần một vài field. Hãy tạo một projection nhẹ hơn:

data class ArticleFull(
    val id: Long,
    val title: String,
    val body: String,       // có thể là 50 KB
    val author: Author,
    val comments: List<Comment>,  // có thể là 500 KB
    val rawHtml: String     // đừng bao giờ đưa cái này vào Bundle
)

// Chỉ truyền những gì màn hình chi tiết thực sự cần
data class ArticlePreview(
    val id: Long,
    val title: String
) : Parcelable

val preview = ArticlePreview(article.id, article.title)
intent.putExtra("article", preview)

Danh sách 200 comment object, mỗi object chứa 10 String field, có thể âm thầm vượt quá 500 KB. Giảm bớt payload thường là cách sửa nhanh nhất khi không thực tế để thay đổi kiến trúc toàn diện.

Xác Minh Sau Khi Sửa

Sau khi áp dụng giải pháp, hãy đo lại:

val size = intent.extras?.sizeInBytes() ?: 0
Log.d("BundleSize", "After fix: $size bytes")
check(size < 500_000) { "Bundle still too large: $size bytes" }

Ngoài ra, hãy bật StrictMode trong các bản build debug. Nó phát hiện vi phạm trước khi chúng trở thành crash trên production:

if (BuildConfig.DEBUG) {
    StrictMode.setVmPolicy(
        StrictMode.VmPolicy.Builder()
            .detectAll()
            .penaltyLog()
            .build()
    )
}

StrictMode ghi log cảnh báo đủ sớm để bạn xử lý trong quá trình phát triển. Hãy coi những cảnh báo đó nghiêm túc — chúng đang cho bạn biết chính xác nơi ẩn chứa crash production tiếp theo.

Những Điểm Cần Ghi Nhớ

  • Truyền ID, không truyền object. Để màn hình đích tải dữ liệu từ một nguồn sự thật duy nhất — Room, ViewModel, Repository. Pattern này có thể mở rộng; truyền toàn bộ object thì không.
  • Giới hạn 1 MB được dùng chung cho tất cả transaction. Trên thiết bị đang bận, bundle 800 KB của bạn có thể đẩy transaction của ứng dụng khác vượt ngưỡng. Hoặc ngược lại. Cả hai trường hợp đều không hay ho gì.
  • Android 7.0 đã thay đổi hành vi. Trước Nougat, dữ liệu quá lớn sẽ bị bỏ qua âm thầm. Từ API 24 trở đi, nó ném exception. Nếu bạn đang nhắm đến API hiện đại và thấy crash mới trên các tính năng cũ, điều này đáng kiểm tra.
  • Bitmap thường là thủ phạm chính. Một ảnh 1024×768 không nén nặng khoảng 3 MB dưới dạng mảng byte. Đó là gấp ba lần toàn bộ bộ đệm IPC. Đừng bao giờ đưa bitmap vào Bundle.
  • Danh sách Parcelable lồng nhau tăng kích thước rất nhanh. Hai trăm mục danh sách, mỗi mục có mười String field, có thể âm thầm vượt quá 500 KB.

Related Error Notes