Что такое Android Binder "Транзакция?"

Я получаю TransactionTooLargeException при отправке сообщений между двумя процессами Android, запущенными из одного APK. Каждое сообщение содержит только небольшие объемы данных, намного меньше, чем общий объем 1 МБ (как указано в документации).

Я создал тестовое приложение (код ниже), чтобы поиграть с этим явлением, и заметил три вещи:

  1. Я получил android.os.TransactionTooLargeException если каждое сообщение было более 200 КБ.

  2. Я получил android.os.DeadObjectException если каждое сообщение было размером менее 200 КБ

  3. Добавление Thread.sleep(1) похоже, решило проблему. Я не могу получить ни одно исключение с Thread.sleep

Просматривая код Android C++, кажется, что transaction завершается неудачно по неизвестной причине и интерпретируется как одно из этих исключений

Вопросы

  1. Что такое " transaction "?
  2. Что определяет, что происходит в транзакции? Это определенное количество событий в данный момент времени? Или просто максимальное количество/размер событий?
  3. Есть ли способ "сбросить" транзакцию или дождаться завершения транзакции?
  4. Какой правильный способ избежать этих ошибок? (Примечание: если разбить его на более мелкие части, просто возникнет другое исключение)


Код

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) 

Ответы

Ответ 1

1) Что такое "транзакция"?

Когда клиентский процесс выполняет вызов серверного процесса (в нашем случае service?.send(message)), он передает код, представляющий метод для вызова вместе с сортированными данными (Parcels). Этот вызов называется транзакцией. Объект Binder клиента вызывает transact() тогда как объект Binder сервера получает этот вызов в onTransact(). Проверьте это и это.

2) Что определяет, что происходит в транзакции? Это определенное количество событий за определенное время? Или просто максимальное количество событий?

В целом это определяется протоколом Binder. Они используют прокси (по клиенту) и заглушки (по сервису). Прокси принимают ваши вызовы (запросы) на высоком уровне Java/C++ и конвертируют их в Parcels (Marshalling) и отправляют транзакцию драйверу Binder Kernel и блокируют его. С другой стороны, заглушки (в процессе обслуживания) прослушивают драйвер Binder Kernel и отключают посылки посылки после получения обратного вызова в богатые типы данных/объекты, которые Сервис может понять.

В случае отправки фреймворка Android Binder Данные через transact() являются Parcel (это означает, что мы можем отправлять все типы данных, поддерживаемые объектом Parcel.), Хранящиеся в буфере транзакций Binder. Буфер транзакции Binder имеет ограниченный фиксированный размер, в настоящее время 1Mb, который разделяется всеми транзакциями в процессе процесса. Так что, если каждое сообщение превышает 200 кб, то 5 или менее запущенных транзакций приведут к превышению лимита и выбросу TransactionTooLargeException. Следовательно, это исключение может быть брошено, когда происходит много транзакций, даже если большая часть отдельных транзакций имеет умеренный размер. В результате действия вы увидите исключение DeadObjectException если оно использует службу, запущенную в другом процессе, который умирает в середине выполнения запроса. Существует множество причин для убийства в Android. Проверьте этот блог для получения дополнительной информации.

3) Есть ли способ "Слить" транзакцию или дождаться завершения транзакции?

Вызов transact() блокирует поток клиента (выполняется в процессе 1) по умолчанию, пока onTransact() будет выполнен с его выполнением в удаленном потоке (выполняется в процессе2). Так что API транзакций синхронный по своей природе в Android. Если вы не хотите, чтобы вызов транзакции() блокировался, вы можете передать флаг IBinder.FLAG_ONEWAY (флаг для транзакции (int, Parcel, Parcel, int)) для немедленного возврата без ожидания каких-либо возвращаемых значений. Вы должны реализовать свой пользовательский интерфейс IBinder для этого.

4) Какой правильный способ избежать этих ошибок? (Примечание: разбить его на более мелкие кусочки просто бросить другое исключение)

  1. Ограничьте количество транзакций одновременно. Делать транзакции, которые действительно необходимы (с размером сообщений всех текущих транзакций за раз, должно быть меньше 1 МБ).
  2. Убедитесь, что процесс (кроме процесса приложения), в котором должен работать другой компонент Android, должен быть запущен.

Примечание. - Поддержка Android Parcel для отправки данных между различными процессами. Партия может содержать как сплющенные данные, которые будут отклеены на другой стороне IPC (с использованием различных методов для написания конкретных типов, или общего интерфейса Parcelable), так и ссылок на объекты live IBinder, которые приведут к тому, что другая сторона получит прокси-IBinder, связанный с оригинальным IBinder в Парцеле.

Правильный способ привязки службы с активностью - это привязывать службу к Activity onStart() и отвязать ее в onStop(), что является видимым жизненным циклом Activity.

В вашем случае добавьте метод в класс MyServiceConnection: -

fun unBind() { context.unbindService(this) }

И в вашем классе деятельности: -

override fun onStart() {
        super.onStart()
        myServiceConnection.bind()
    }

    override fun onStop() {
        super.onStop()
        myServiceConnection.unBind()
    }

Надеюсь, что это поможет вам.

Ответ 2

1. Что такое "транзакция"?

Во время удаленного вызова процедуры аргументы и возвращаемое значение вызова передаются как объекты Parcel, хранящиеся в буфере транзакции Binder. Если аргументы или возвращаемое значение слишком велики для размещения в буфере транзакций, тогда вызов завершится с ошибкой и будет выведено TransactionTooLargeException.

2. Что определяет, что происходит в транзакции? Это определенное количество событий за определенное время? Или просто максимальное количество событий? Только и только размер Буфер транзакций Binder имеет ограниченный фиксированный размер, в настоящее время 1 Мб, который используется всеми транзакциями в процессе.

3. Есть ли способ "Слить" транзакцию или дождаться завершения транзакции?

нет

4. Каков правильный способ избежать этих ошибок? (Примечание: разбить его на более мелкие кусочки просто бросить другое исключение)

По моему пониманию, ваш объект сообщения может иметь bytearray изображения или что-то еще, размер которого больше 1 Мб. Не отправляйте bytearray в Bundle.

Вариант 1: для изображения я думаю, что вы должны передать URI через Bundle. Используйте Picasso, так как он использует кеширование, не загружая изображение несколько раз.

Вариант 2 [не рекомендуется] Сжатие массива байтов, поскольку оно может не сжиматься до требуемого размера

//Convert to byte array
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bmp.compress(Bitmap.CompressFormat.PNG, 100, stream);
byte[] byteArr = stream.toByteArray();

Intent in1 = new Intent(this, Activity2.class);
in1.putExtra("image",byteArr);

Затем в Мероприятии 2:

byte[] byteArr = getIntent().getByteArrayExtra("image");
Bitmap bmp = BitmapFactory.decodeByteArray(byteArr, 0, byteArr.length);

Вариант 3 [Рекомендуется] использовать чтение/запись файла и передавать uri через комплект

Запись файла:

private void writeToFile(String data,Context context) {
    try {
        OutputStreamWriter outputStreamWriter = new OutputStreamWriter(context.openFileOutput("filename.txt", Context.MODE_PRIVATE));
        outputStreamWriter.write(data);
        outputStreamWriter.close();
    }
    catch (IOException e) {
        Log.e("Exception", "File write failed: " + e.toString());
    } 
}

Читать файл:

private String readFromFile(Context context) {

    String ret = "";

    try {
        InputStream inputStream = context.openFileInput("filename.txt");

        if ( inputStream != null ) {
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            String receiveString = "";
            StringBuilder stringBuilder = new StringBuilder();

            while ( (receiveString = bufferedReader.readLine()) != null ) {
                stringBuilder.append(receiveString);
            }

            inputStream.close();
            ret = stringBuilder.toString();
        }
    }
    catch (FileNotFoundException e) {
        Log.e("login activity", "File not found: " + e.toString());
    } catch (IOException e) {
        Log.e("login activity", "Can not read file: " + e.toString());
    }

    return ret;
}

Вариант 4 (Использование gson) Запись объекта

[YourObject] v = new [YourObject]();
Gson gson = new Gson();
String s = gson.toJson(v);

FileOutputStream outputStream;

try {
  outputStream = openFileOutput(filename, Context.MODE_PRIVATE);
  outputStream.write(s.getBytes());
  outputStream.close();
} catch (Exception e) {
  e.printStackTrace();
}

Как его прочитать:

 FileInputStream fis = context.openFileInput("myfile.txt", Context.MODE_PRIVATE);
 InputStreamReader isr = new InputStreamReader(fis);
 BufferedReader bufferedReader = new BufferedReader(isr);
 StringBuilder sb = new StringBuilder();
 String line;
 while ((line = bufferedReader.readLine()) != null) {
     sb.append(line);
 }

 String json = sb.toString();
 Gson gson = new Gson();
 [YourObject] v = gson.fromJson(json, [YourObject].class);