Я получаю TransactionTooLargeException
при отправке сообщений между двумя процессами Android, запущенными из одного APK. Каждое сообщение содержит только небольшие объемы данных, намного меньше, чем общий объем 1 МБ (как указано в документации).
Я создал тестовое приложение (код ниже), чтобы поиграть с этим явлением, и заметил три вещи:
-
Я получил
android.os.TransactionTooLargeException
если каждое сообщение было более 200 КБ. -
Я получил
android.os.DeadObjectException
если каждое сообщение было размером менее 200 КБ -
Добавление
Thread.sleep(1)
похоже, решило проблему. Я не могу получить ни одно исключение сThread.sleep
Просматривая код Android C++, кажется, что transaction
завершается неудачно по неизвестной причине и интерпретируется как одно из этих исключений
Вопросы
- Что такое "
transaction
"? - Что определяет, что происходит в транзакции? Это определенное количество событий в данный момент времени? Или просто максимальное количество/размер событий?
- Есть ли способ "сбросить" транзакцию или дождаться завершения транзакции?
- Какой правильный способ избежать этих ошибок? (Примечание: если разбить его на более мелкие части, просто возникнет другое исключение)
Код
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.example.boundservicestest"
xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<service android:name=".BoundService" android:process=":separate"/>
</application>
</manifest>
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var sendDataButton: Button
private val myServiceConnection: MyServiceConnection = MyServiceConnection(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
myServiceConnection.bind()
sendDataButton = findViewById(R.id.sendDataButton)
val maxTransactionSize = 1_000_000 // i.e. 1 mb ish
// Number of messages
val n = 10
// Size of each message
val bundleSize = maxTransactionSize / n
sendDataButton.setOnClickListener {
(1..n).forEach { i ->
val bundle = Bundle().apply {
putByteArray("array", ByteArray(bundleSize))
}
myServiceConnection.sendMessage(i, bundle)
// uncommenting this line stops the exception from being thrown
// Thread.sleep(1)
}
}
}
}
MyServiceConnection.kt
class MyServiceConnection(private val context: Context) : ServiceConnection {
private var service: Messenger? = null
fun bind() {
val intent = Intent(context, BoundService::class.java)
context.bindService(intent, this, Context.BIND_AUTO_CREATE)
}
override fun onServiceConnected(name: ComponentName, service: IBinder) {
val newService = Messenger(service)
this.service = newService
}
override fun onServiceDisconnected(name: ComponentName?) {
service = null
}
fun sendMessage(what: Int, extras: Bundle? = null) {
val message = Message.obtain(null, what)
message.data = extras
service?.send(message)
}
}
BoundService.kt
internal class BoundService : Service() {
private val serviceMessenger = Messenger(object : Handler() {
override fun handleMessage(message: Message) {
Log.i("BoundService", "New Message: ${message.what}")
}
})
override fun onBind(intent: Intent?): IBinder {
Log.i("BoundService", "On Bind")
return serviceMessenger.binder
}
}
build.gradle *
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 27
defaultConfig {
applicationId "com.example.boundservicestest"
minSdkVersion 19
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:27.1.1'
}
Трассировки стека
07-19 09:57:43.919 11492-11492/com.example.boundservicestest E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.boundservicestest, PID: 11492
java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:448)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
Caused by: android.os.DeadObjectException: Transaction failed on small parcel; remote process probably died
at android.os.BinderProxy.transactNative(Native Method)
at android.os.BinderProxy.transact(Binder.java:764)
at android.os.IMessenger$Stub$Proxy.send(IMessenger.java:89)
at android.os.Messenger.send(Messenger.java:57)
at com.example.boundservicestest.MyServiceConnection.sendMessage(MyServiceConnection.kt:32)
at com.example.boundservicestest.MainActivity$onCreate$1.onClick(MainActivity.kt:30)
at android.view.View.performClick(View.java:6294)
at android.view.View$PerformClick.run(View.java:24770)
at android.os.Handler.handleCallback(Handler.java:790)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6494)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)