首页 > 解决方案 > 拍照时出现TransactionTooLargeException

问题描述

当我创建启动相机应用程序以拍照的意图时,我的应用程序出现问题,我的应用程序崩溃并收到以下错误:

2021-06-11 18:07:46.914 7506-7506/com.package.app E/JavaBinder: !!! FAILED BINDER TRANSACTION !!!  (parcel size = 14763232)

...

2021-06-11 18:07:49.567 7506-7506/com.package.app E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.package.app, PID: 7506
    java.lang.RuntimeException: android.os.TransactionTooLargeException: data parcel size 14763232 bytes
        at android.app.servertransaction.PendingTransactionActions$StopInfo.run(PendingTransactionActions.java:161)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
     Caused by: android.os.TransactionTooLargeException: data parcel size 14763232 bytes
        at android.os.BinderProxy.transactNative(Native Method)
        at android.os.BinderProxy.transact(BinderProxy.java:510)
        at android.app.IActivityTaskManager$Stub$Proxy.activityStopped(IActivityTaskManager.java:4524)
        at android.app.servertransaction.PendingTransactionActions$StopInfo.run(PendingTransactionActions.java:145)
        at android.os.Handler.handleCallback(Handler.java:883) 
        at android.os.Handler.dispatchMessage(Handler.java:100) 
        at android.os.Looper.loop(Looper.java:214) 
        at android.app.ActivityThread.main(ActivityThread.java:7356) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930) 

这些照片在相机应用程序中拍摄并返回到我的应用程序后,将保存在 Room 数据库中。有趣的是,只有当我试图添加/替换照片的数据库行中已经保存了一张照片时,才会出现问题。在创建新行或在没有图片的行中拍照时,我可以拍摄照片并将其保存到我的数据库中,没有任何问题。

我的 Room 数据库有一个 TypeConverter,它将位图转换为 base64 字符串以存储在数据库中,并在需要查看时转换回位图。在玩了一段时间的代码后,我尝试从数据库中删除转换器并将其功能实现到我的视图模型和片段中。无论是否有图片被替换,该应用程序现在都可以正常工作。

我现在怀疑我实现转换器的方式有问题,但我不确定它可能是什么。请看我下面的代码。

分段

lateinit var currentPhotoPath: String


@AndroidEntryPoint
class Fragment : Fragment(R.layout.fragment) {


    private val viewModel: ViewModel by viewModels()
    private var _binding: FragmentBinding? = null



    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.apply {
            ivPicture.setImageBitmap(viewModel.entryPictures)

            fab.setOnClickListener {
                viewModel.onSaveClick()
            }
        }


        viewLifecycleOwner.lifecycleScope.launchWhenStarted {
            viewModel.event.collect { event ->
                when (event) {
                    ViewModel.Event.NavigateToPhotoActivity -> {
                        dispatchTakePictureIntent()
                    }
                }
            }
        }

        setHasOptionsMenu(true)

    }

    private val REQUEST_IMAGE_CAPTURE = 23

    private fun dispatchTakePictureIntent() {

        Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
            val packageManager = requireContext().packageManager
            takePictureIntent.resolveActivity(packageManager)?.also {
                val photoFile: File? = try {
                    createImageFile()
                } catch (ex: IOException) {
                    Toast.makeText(activity, "Error Creating File", Toast.LENGTH_LONG).show()
                    null
                }
                photoFile?.also {
                    val photoURI: Uri = FileProvider.getUriForFile(
                        requireContext(),
                        "com.package.app.fileprovider",
                        it
                    )
                    takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
                    requireActivity().startActivityFromFragment(this, takePictureIntent, REQUEST_IMAGE_CAPTURE)
                }
            }
        }
    }

    @Throws(IOException::class)
    private fun createImageFile(): File {
        val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
        val storageDir: File? = context?.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
        return File.createTempFile(
            "JPEG_${timeStamp}_", /* prefix */
            ".jpg", /* suffix */
            storageDir /* directory */
        ).apply {
            // Save a file: path for use with ACTION_VIEW intents
            currentPhotoPath = absolutePath
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == Activity.RESULT_OK) {

            lifecycleScope.launch {

                val takenImage = BitmapFactory.decodeFile(currentPhotoPath)

                viewModel.onPhotoRetrieved(takenImage)

                binding.ivPicture.apply {
                    visibility = View.VISIBLE
                    setImageBitmap(takenImage)
                }
            }

        } else {
            Toast.makeText(activity, "Error Retrieving Image", Toast.LENGTH_LONG).show()
        }
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        inflater.inflate(R.menu.menu_fragment, menu)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.icon_photo -> {
                viewModel.onTakePhotoSelected()
                true
            }
            else -> super.onOptionsItemSelected(item)
        }
    }


    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

视图模型:

@HiltViewModel
class ViewModel @Inject constructor(
    private val dao: EntryDao,
    private val state: SavedStateHandle
) : ViewModel() {


    val entry = state.get<Entry>("entry")

    var entryPictures = entry?.pictures

    private val eventChannel = Channel<Event>()
    val event = eventChannel.receiveAsFlow()


    fun onSaveClick() {
        if (entry != null) {
            val updatedEntry = entry.copy(
                pictures = entryPictures
            )
            updatedEntry(updatedEntry)
        } else {
            val newEntry = Entry(
                pictures = entryPictures
            )

            createEntry(newEntry)
        }
    }

    private fun createEntry(entry: Entry) = viewModelScope.launch {
        dao.insert(entry)
    }

    private fun updatedEntry(entry: Entry) = viewModelScope.launch {
        dao.update(entry)
    }

    fun onTakePhotoSelected() = viewModelScope.launch {

        eventChannel.send(Event.NavigateToPhotoActivity)
    }

    fun onPhotoRetrieved(bitmap: Bitmap) = viewModelScope.launch {
        entryPictures = bitmap

    }


    sealed class Event {
        object NavigateToPhotoActivity : Event()
    }
}

数据库:

@Database(entities = [Entry::class], version = 1)
@TypeConverters(Converters::class)

abstract class Database : RoomDatabase() {

    abstract fun entryDao(): EntryDao

    class Callback @Inject constructor(
        private val database: Provider<com.mayuram.ascend.data.Database>,
        @ApplicationScope private val applicationScope: CoroutineScope
    ) : RoomDatabase.Callback() {

        override fun onCreate(db: SupportSQLiteDatabase) {
            super.onCreate(db)

            val dao = database.get().entryDao()

            applicationScope.launch {
                dao.insert(Entry(null))
                dao.insert(Entry(null))
                dao.insert(Entry(null))
                dao.insert(Entry(null))
            }
        }
    }
}

转换器


class Converters {

    @Suppress("DEPRECATION")
    @TypeConverter
    fun bitmapToString(bitmap: Bitmap?): String {

        val outputStream = ByteArrayOutputStream()

        if (android.os.Build.VERSION.SDK_INT >= 30) {
            bitmap?.compress(Bitmap.CompressFormat.WEBP_LOSSY, 50, outputStream)
        } else {
            bitmap?.compress(Bitmap.CompressFormat.WEBP, 50, outputStream)
        }
        val imageBytes: ByteArray = outputStream.toByteArray()

        return Base64.encodeToString(imageBytes, Base64.DEFAULT)
    }

    @TypeConverter
    fun stringToBitmap(string: String): Bitmap? {
        val imageBytes: ByteArray = Base64.decode(string, Base64.DEFAULT)

        return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)

    }
}

数据类:

@Entity(tableName = "entry_table")
@Parcelize
data class Entry(
    val pictures: Bitmap?,
    @PrimaryKey(autoGenerate = true) val id: Int = 0
) : Parcelable

我为使其正常工作所做的更改:

在 ViewModel 中,修改 onPhotoRetrieved 函数将图像转换为字符串

    fun onPhotoRetrieved(bitmap: Bitmap) = viewModelScope.launch {

        val outputStream = ByteArrayOutputStream()

        if (android.os.Build.VERSION.SDK_INT >= 30) {
            bitmap.compress(Bitmap.CompressFormat.WEBP_LOSSY, 50, outputStream)
        } else {
            bitmap.compress(Bitmap.CompressFormat.WEBP, 50, outputStream)
        }
        val imageBytes: ByteArray = outputStream.toByteArray()
        val result = Base64.encodeToString(imageBytes, Base64.DEFAULT)

        entryPictures = result

    }

在片段中,在 onViewCreated 中添加了将字符串转换为位图功能

val imageBytes: ByteArray = Base64.decode(viewModel.entryPictures.toString(), Base64.DEFAULT)
val result = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
ivPicture.setImageBitmap(result)

还将val图片的类型更改为String?而不是位图?在我的数据类中并在我的数据库中注释掉了@TypeConverters。

标签: androidkotlinandroid-roomtypeconverter

解决方案


使固定

简而言之,您需要重新考虑将图像保存在数据库中,并考虑存储图像的路径(或其合适的部分,以唯一标识图像) ,并将实际图像存储在合适的位置。

另一种选择,但在资源方面可能仍然相当昂贵。可以考虑存储可管理的图像块(也许考虑 100k 块)。例如,如何在Android SQLite 中使用大于CursorWindow 限制的图像?

另一种选择可能是将较小的图像(如果有的话认为它们是照片)存储在数据库中,但将较大的图像存储为路径。例如,如何在 sqlite 数据库中插入图像

问题

您遇到的是图像大小其他问题的前兆(双关语:))。

也就是说,您已经超出了TransactionTooLargeException解释的 Parcel 的 1Mb 限制,其中包括:-

Binder 事务缓冲区有一个有限的固定大小,目前为 1MB,由进程正在进行的所有事务共享。因此,当有许多事务正在进行时,即使大多数单个事务的大小适中,也可能会引发此异常。

您的包裹(图像)似乎是 14763232 即 14Mb。

即使您增加了包裹大小,您也可能会遇到 Android SQLite 实现,因此会遇到光标大小限制的 Room 问题,或者光标中行数减少的低效问题。

在创建新行或在没有图片的行中拍照时,我可以拍摄照片并将其保存到我的数据库中,没有任何问题。

插入时限制不存在,因为您单独插入。限制是当提取数据时,通常/经常在单个请求中提取数据组并使用中间缓冲区(即游标是缓冲区)。


推荐阅读