当我创建启动相机应用程序来拍照的意图时,我的应用程序遇到问题,我的应用程序崩溃并收到以下错误:
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 数据库中。有趣的是,只有当我尝试添加/替换照片的数据库行中已保存照片时,才会出现此问题。当创建新行或在没有图片的行中拍摄照片时,我可以毫无问题地拍摄照片并将其保存到我的数据库中。
My 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?而不是位图?在我的数据类中,并在我的数据库中注释掉@TypeConverters。
修复
简而言之,您需要重新考虑将图像保存在数据库中,并考虑存储图像的路径(或其合适的部分,以唯一标识图像),并将实际图像存储在合适的位置。
一种替代方案,但在资源方面可能仍然相当昂贵。可以考虑存储可管理的图像块(也许考虑 100k 块)。 例如如何在 Android SQLite 中使用大于 CursorWindow 限制的图像?
另一种选择是将较小的图像(如果有的话认为它们是照片)存储在数据库中,但将较大的图像作为路径存储。 例如如何在sqlite数据库中插入图像
问题
您遇到的问题是其他图像大小问题的先兆(双关语:))。
也就是说,您已经超出了包裹的 1Mb 限制,如 TransactionTooLargeException 所解释,其中包括:-
Binder 事务缓冲区的固定大小有限,目前为 1MB,由进程中所有正在进行的事务共享。因此,当有许多事务正在进行时,即使大多数单个事务的大小适中,也可能会引发此异常。
您的包裹(图片)似乎是 14763232,即 14Mb。
即使您增加了地块大小,您也可能会遇到 Android SQLite 实现,因此 Room 会出现游标大小限制和/或游标中行数减少的低效率问题。
当创建新行或在没有图片的行中拍摄照片时,我可以毫无问题地拍摄照片并将其保存到我的数据库中。
插入时不存在限制,因为您是单独插入的。限制在于提取数据时,因为通常/经常在单个请求中提取数据组并使用中间缓冲区(即游标是缓冲区)。
2024 年更新,有一个库可以通过有效管理数据大小来帮助修复 TransactionTooLargeException。该库确保 Bundle 中保存的数据不超过事务缓冲区限制。
您可以在 GitHub 上找到 BundleSaver 库:https://github.com/kernel0x/bundlesaver