【问题标题】:What is an Android Binder "Transaction?"什么是 Android Binder“事务”?
【发布时间】:2018-12-27 17:47:52
【问题描述】:

在通过单个 APK 运行的两个 Android 进程之间发送消息时,我收到了 TransactionTooLargeException。每条消息只包含少量数据,much smaller than the 1 mb total (as specified in the docs)

我创建了一个测试应用程序(下面的代码)来解决这种现象,并注意到三件事:

  1. 如果每条消息超过 200 kb,我会收到 android.os.TransactionTooLargeException

  2. 如果每条消息小于 200kb,我会收到 android.os.DeadObjectException

  3. 添加Thread.sleep(1) 似乎已经解决了这个问题。我无法使用Thread.sleep

  4. 获得任何异常

Looking through the Android C++ code,似乎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) 

【问题讨论】:

  • 我可以在 Google Pixel 模拟器 - 具有 64 位架构的 Android 8.1 中运行您的代码(没有任何错误),无需 Thread.sleep(1)。跨度>
  • @PravinDivraniya,您是否包括了android:process=":separate"?只有在单独的进程中运行服务时才会失败。你也有真正的手机可以测试吗?
  • 是的,我在不同的进程上运行我的服务。&lt;service android:name=".BoundService" android:process=":myprocess"/&gt;
  • 我还在真实设备 moto G5 plus 上进行了测试 - Android 7.0 及其工作。
  • @PravinDivraniya 你的 sdk、目标 sdk 和 minsdk 是什么?我刚刚添加了我的build.gradle。我每次在运行 Android P 的 Google Pixel XL Android 8.1 和 Google Pixel 2 XL 上都会崩溃

标签: android transactions android-service ipc android-binder


【解决方案1】:

其他人对情况进行了很多解释,但缺少一些重要的事情。

1) 什么是“交易”? 事务是预定义的消息,通过进程间屏障传递缓冲区(在内核的帮助下 - 实现在内核内部 - https://elixir.bootlin.com/linux/latest/source/drivers/android/binder.c - 不是 C++/Java 用户代码。)。 它使用名为 parcels 的序列化缓冲区,它通过内核传递给消费者。 事务适用于小型自包含数据,而不是巨大的内存缓冲区。

2) 什么定义了事务中的内容?在给定时间内是否有一定数量的事件?或者只是事件的最大数量/规模? 事务或多或少是写入内核以通过总线“传输”到连接的客户端。看来你有限制。

3) 有没有办法“刷新”交易或等待交易完成? 取决于,你不应该需要这个。

4) 避免这些错误的正确方法是什么? (注意:将其分解成更小的部分只会引发不同的异常)

更合理地设计您的应用程序: 如果你在同一个内存中,你可以共享对象。

但是,有一个被忽视的功能允许使用 binder 共享大量数据:您可以映射整个文件描述符。有一个类:https://developer.android.com/reference/android/os/ParcelFileDescriptor

你可以用来包装内存文件: https://developer.android.com/reference/android/os/MemoryFile

一般来说,传递的文件描述符可以通过检索进程来加载,因为 android 在后台将这些描述符映射为传递的进程可以访问的。这是真正的“修复”。文件描述符本身可以附加巨大的缓冲区。

【讨论】:

    【解决方案2】:

    1) 什么是“交易”?

    当客户端进程调用服务器进程(在我们的例子中为service?.send(message))时,它会传输一个表示要调用的方法的代码以及编组数据(包裹)。此调用称为事务。客户端 Binder 对象调用 transact() 而服务器 Binder 对象在 onTransact() 方法中接收此调用。检查ThisThis

    2) 什么定义了事务中的内容?在给定时间内是否有一定数量的事件?或者只是事件的最大数量/大小?

    一般来说,它是由 Binder 协议决定的。它们使用代理(由客户端)和存根(由服务)。代理接受您的高级 Java/C++ 方法调用(请求)并将它们转换为 Parcels(编组)并将事务提交给 Binder Kernel Driver 并阻止。另一方面,存根(在服务进程中)监听 Binder 内核驱动程序并在收到回调时将 Parcel 解组为服务可以理解的丰富数据类型/对象。

    如果Android Binder框架发送通过transact()的数据是一个Parcel(表示我们可以发送Parcel对象支持的所有类型的数据。),存储在Binder事务缓冲区中。Binder事务缓冲区有一个有限的固定大小,目前为 1Mb,由该进程正在进行的所有事务共享。因此,如果每条消息超过 200 kb,则 5 个或更少的正在运行的事务将导致超过限制并抛出TransactionTooLargeException。因此,当有许多事务正在进行时,即使大多数单个事务的大小适中,也可能会引发此异常。如果一个活动使用了在另一个进程中运行的服务,而该服务在执行请求的过程中死掉了,它将看到DeadObjectException 异常。在 Android 中杀死进程有很多原因。更多信息请查看this blog

    3) 有没有办法“刷新”交易或等待交易完成?

    默认情况下,对transact() 的调用会阻塞客户端线程(在进程1 中运行),直到onTransact() 在远程线程中完成其执行(在进程2 中运行)。因此事务API 在Android 中本质上是同步的。如果您不希望 transact() 调用阻塞,那么您可以传递 IBinder.FLAG_ONEWAY 标志(标志到 transact(int, Parcel, Parcel, int))以立即返回,而无需等待任何返回值。您必须为此实现自定义 IBinder 实现。

    4) 避免这些错误的正确方法是什么? (注意:将其分解成更小的部分只会引发不同的异常)

    1. 一次限制事务数。进行真正必要的事务(一次所有正在进行的事务的消息大小必须小于 1MB)。
    2. 确保运行其他 Android 组件的进程(应用进程除外)必须在其中运行。

    注意:- Android 支持 Parcel 在不同之间发送数据 过程。一个 Parcel 可以包含两个展平的数据 在 IPC 的另一侧未展平(使用各种方法 这里用于编写特定类型,或通用 Parcelable 接口), 以及对活动 IBinder 对象的引用,这将导致另一个 接收与原始 IBinder 连接的代理 IBinder 的一方 包裹。

    将服务与活动绑定的正确方法是在活动onStart()上绑定服务并在onStop()中取消绑定,这是活动的可见生命周期。

    在你的情况下,在MyServiceConnection 类中添加方法:-

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

    在你的 Activity 类中:-

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

    希望这会对你有所帮助。

    【讨论】:

      【解决方案3】:

      1. 什么是“交易”?

      在远程过程调用期间,调用的参数和返回值作为存储在 Binder 事务缓冲区中的 Parcel 对象进行传输。如果参数或返回值太大而无法放入事务缓冲区,则调用将失败并抛出TransactionTooLargeException

      2。什么定义了事务中的内容?在给定时间内是否有一定数量的事件?或者只是事件的最大数量/规模? 唯一且唯一的尺寸 Binder 事务缓冲区有一个有限的固定大小,目前为 1Mb,由该进程正在进行的所有事务共享。

      3.有没有办法“刷新”交易或等待交易完成?

      没有

      4.避免这些错误的正确方法是什么? (注意:将其分解成更小的部分只会引发不同的异常)

      据我了解,您的消息对象可能包含图像的字节数组或其他大小超过 1mb 的东西。不要在 Bundle 中发送 bytearray。

      选项 1: 对于图像,我认为您应该通过 Bundle 传递 URI。使用毕加索,因为它使用缓存不会多次下载图像。

      选项 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 [推荐]使用文件读/写并通过bundle传递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);
      

      【讨论】:

      • 如果你按顺序发送一堆小消息,它会失败。但是,如果您在每个消息之间发送一个小消息和Thread.sleep(1),它就会成功,那么其中一定有一些时间组件?
      • 查看原始代码。它占用 1,000,000 字节并除以包数。因此,如果您发送 10 个请求,每个请求 100,000 字节,它会以DeadObjectException 失败。如果您发送 4 个请求,每个请求 250,000 字节,如果失败并显示 TransactionTooLargeException
      • 你能推荐一个更好的方法吗?目标是在进程之间传输数据。我有 iBinder 对象,因此无法将其写入磁盘或共享内存。我试过了,但它抛出了一个关于捆绑中包含不可打包物品的异常。
      • 我添加了另一个使用 gson 和文件写入的选项,试试那个。我认为它会解决问题。
      猜你喜欢
      • 2012-01-30
      • 1970-01-01
      • 2022-11-28
      • 2017-10-02
      • 1970-01-01
      • 1970-01-01
      • 2014-10-20
      • 1970-01-01
      • 2011-07-09
      相关资源
      最近更新 更多