web-dev-qa-db-fra.com

Qu'est-ce qu'un "Classeur Android"?

Je reçois une TransactionTooLargeException lors de l'envoi de messages entre deux processus Android exécutés à partir d'un seul APK. Chaque message ne contient que de petites quantités de données, très inférieur au total de 1 Mo (comme spécifié dans la documentation) .

J'ai créé une application de test (code ci-dessous) pour jouer avec ce phénomène, et j'ai remarqué trois choses:

  1. J'ai reçu un Android.os.TransactionTooLargeException si chaque message dépassait 200 kb.

  2. J'ai reçu un Android.os.DeadObjectException si chaque message faisait moins de 200 Ko

  3. L'ajout d'une Thread.sleep(1) semble avoir résolu le problème. Je ne peux pas obtenir l'une ou l'autre exception avec un Thread.sleep

En regardant à travers le code Android C++ , il semble que le transaction échoue pour une raison inconnue et interprété comme l'une de ces exceptions

Des questions

  1. Qu'est-ce qu'une "transaction"?
  2. Qu'est-ce qui définit ce qui se passe dans une transaction? Est-ce un certain nombre d'événements dans un temps donné? Ou juste un nombre maximum/taille d'événements?
  3. Existe-t-il un moyen de "vider" une transaction ou d'attendre la fin d'une transaction?
  4. Quelle est la bonne façon d'éviter ces erreurs? (Remarque: le diviser en morceaux plus petits déclenchera simplement une exception différente)


Code

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'
}

Trace de la pile

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) 
12
Jason

1) Qu'est-ce qu'une "transaction"?

Lorsqu'un processus client appelle le processus serveur (dans notre cas, service?.send(message)), il transfère un code représentant la méthode à appeler avec des données marshallées (colis). Cet appel s'appelle une transaction. L'objet Binder client appelle transact() alors que l'objet Binder serveur reçoit cet appel dans la méthode onTransact(). Vérifiez Ceci et Ceci .

2) Qu'est-ce qui définit ce qui se passe dans une transaction? Est-ce un certain nombre d'événements dans un temps donné? Ou juste un nombre maximum/taille d'événements?

En général, il est décidé par le protocole Binder. Ils utilisent des proxies (par client) et des stubs (par service). Les mandataires prennent vos appels (demandes) de méthode Java/C++ de haut niveau et les convertissent en colis (Marshalling), puis envoient la transaction au gestionnaire de noyau du liant et le bloquent. Les stubs, d’autre part (dans le processus du service) écoutent le pilote du noyau Binder et annulent les parcelles lors de la réception d’un rappel, dans des types de données/objets riches que le service peut comprendre.

Sous Android Binder framwork send Les données transact () sont des Parcel (Cela signifie que nous pouvons envoyer tous les types de données supportées par un objet Parcel.), Stockées dans le tampon de transaction Binder. Le tampon de transaction Binder a une taille fixe limitée, actuellement de 1 Mo, partagée par toutes les transactions en cours du processus. Ainsi, si chaque message dépasse 200 kb, alors 5 transactions en cours ou moins entraîneront une limite de dépassement et de rejet de TransactionTooLargeException. Par conséquent, cette exception peut être levée lorsque plusieurs transactions sont en cours, même lorsque la plupart des transactions individuelles sont de taille modérée. Une activité verra une exception DeadObjectException si elle utilise un service exécuté dans un autre processus qui meurt au cours de l'exécution d'une requête. Il y a beaucoup de raisons pour un processus de tuer dans Android. Vérifiez ce blog pour plus d'informations.

3) Existe-t-il un moyen de "vider" une transaction ou d'attendre la fin d'une transaction?

Un appel à transact() bloque le thread client (Running in process1) par défaut jusqu'à ce que onTransact() soit terminé avec son exécution dans le thread distant (Running in process2). Par conséquent, l'API de transaction est de nature synchrone dans Android. Si vous ne souhaitez pas que l’appel transact () bloque, vous pouvez renvoyer immédiatement l’indicateur IBinder.FLAG_ONEWAY flag (Indicateur à transact (int, Colis, Colis, int) ) immédiatement sans attendre. Pour cela, vous devez implémenter votre implémentation IBinder personnalisée.

4) Quelle est la bonne façon d'éviter ces erreurs? (Remarque: le diviser en morceaux plus petits déclenchera simplement une exception différente)

  1. Limiter le nombre de transactions à la fois. Effectuez les transactions qui sont vraiment nécessaires (la taille du message de toutes les transactions en cours à la fois doit être inférieure à 1 Mo).
  2. Assurez-vous que le processus (autre que le processus d'application) dans lequel un autre composant Android en cours d'exécution doit être exécuté.

Remarque: - Android support Parcel pour envoyer des données entre différents processus. Un colis peut contenir à la fois des données aplaties qui seront sans aplatissement de l’autre côté de IPC (en utilisant les différentes méthodes ici pour écrire des types spécifiques, ou l’interface générale Parcelable), et des références à des objets IBinder en direct qui entraîneront l'autre côté recevant un proxy IBinder connecté avec le IBinder d’origine dans le colis.

Le moyen approprié d'associer un service à une activité est de lier le service sur Activity onStart () et de le dissocier dans onStop (), qui est le cycle de vie visible d'une activité.

Dans votre cas, ajoutez la méthode dans la classe MyServiceConnection: - 

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

Et dans votre classe d'activité: - 

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

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

J'espère que ceci vous aidera.

2
Pravin Divraniya