什么是Android Binder的“交易”?

时间:2018-07-19 14:25:42

标签: android transactions android-binder

在通过单个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'
}

Stacktrace

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) 

2 个答案:

答案 0 :(得分:4)

1。 什么是“交易”?

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

2。什么定义了交易的内容?在给定时间内是否有一定数量的事件?还是仅仅是事件的最大数量/大小? 只有大小 Binder事务缓冲区的固定大小有限,目前为1Mb,该进程的所有正在进行的事务共享该大小。

3。是否可以“刷新”交易或等待交易完成?

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

据我了解,您的消息对象可能具有图像的字节数组或大小大于1mb的其他内容。不要在Bundle中发送字节数组。

选项1:对于图像,我认为您应该通过Bundle传递URI。使用毕加索(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);

答案 1 :(得分:3)

1)什么是“交易”?

当客户端进程调用服务器进程时(在我们的示例中为service?.send(message)),它会传输表示要调用的方法的代码以及封送数据(宗地)。此调用称为事务。客户端Binder对象调用transact(),而服务器Binder对象通过onTransact()方法接收此调用。选中ThisThis

2)什么定义了交易中的内容?在给定时间内是否有一定数量的事件?还是仅仅是事件的最大数量/大小?

通常由Binder协议决定,它们使用代理(由客户端)和存根(由服务)。代理接受您的高级Java / C ++方法调用(请求),并将其转换为包裹(编组),然后将事务提交给Binder内核驱动程序并进行阻止。另一方面(在服务过程中),存根会侦听Binder内核驱动程序,并在收到回调后将包裹解组为服务可以理解的丰富数据类型/对象。

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

3)是否可以“刷新”交易或等待交易完成?

transact()的调用默认情况下会阻止客户端线程(在process1中运行),直到onTransact()在远程线程(在process2中运行)执行完毕。因此事务API在Android中的自然。如果您不想阻止transact()调用,则可以传递IBinder.FLAG_ONEWAY标志(标记为transact(int, Parcel, Parcel, int))以立即返回,而无需等待任何返回值。您必须实现自定义IBinder实现。

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

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

注意:-Android支持Parcel在不同之间发送数据   流程。一个宗地可以同时包含将要   在IPC的另一端未展平(使用各种方法   此处用于编写特定类型或常规的Parcelable接口),   和对活动IBinder对象的引用,这将导致其他   接收与原始IBinder连接的代理IBinder   包裹。

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

在您的情况下,MyServiceConnection类中的Add on方法:-

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

在您的Activity类中:-

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

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

希望这会对您有所帮助。