拍照时出现TransactionTooLargeException

TransactionTooLargeException When Taking Photo

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

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 数据库中。有趣的是,只有在我尝试 add/replace 照片的数据库行中已经保存了照片时,才会出现此问题。当创建新行或连续拍摄没有图片的照片时,我可以毫无问题地拍摄照片并将其保存到我的数据库中。

我的 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

    }

在fragment中,在onViewCreated中添加了字符串转位图函数

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

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

修复

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

一个替代方案,但可能仍然相当昂贵的资源。可以考虑存储可管理的图像块(也许考虑 100k 块)。 例如

另一种选择是将较小的图像(如果有的话认为它们是照片)存储在数据库中,但将较大的图像作为路径。 例如How can I insert image in a sqlite database

问题

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

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

The Binder transaction buffer has a limited fixed size, currently 1MB, which is shared by all transactions in progress for the process. Consequently this exception can be thrown when there are many transactions in progress even when most of the individual transactions are of moderate size.

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

即使您增加了 parcel 大小,您也可以点击 Android SQLite 实现,因此 Room 存在游标大小限制问题和/或游标中行数减少导致的低效问题。

When creating a new row or taking a photo in a row which has no picture, I'm able to take the photo and save it into my database without any problem.

插入时没有限制,因为您是根据个人情况插入的。限制是在提取数据时,因为 typically/often 数据组是在单个请求中提取的,并且使用了中间缓冲区(即 Cursor 是缓冲区)。