Реализация межпроцессного взаимодействия (IPC) для Android
Большинство Android-приложений работают в одном процессе и никогда не сталкиваются с IPC. Но как только появляется отдельный Service с android:process=":remote", сторонний SDK, работающий в своём процессе, или необходимость передавать данные между приложениями — начинается серьёзная работа с Binder-механизмом, AIDL и Messenger.
Механизмы IPC в Android
Android предоставляет несколько уровней абстракции над Binder:
| Механизм | Когда использовать | Сложность |
|---|---|---|
| Intent | Запустить Activity/Service, передать небольшие данные | Низкая |
| Messenger | Односторонние сообщения, не нужен параллелизм | Средняя |
| AIDL | Двустороннее взаимодействие, параллельные вызовы | Высокая |
| ContentProvider | Структурированные данные между приложениями | Средняя |
| BroadcastReceiver | События типа «уведомить всех» | Низкая |
Binder — транспортный слой для всего перечисленного. Ядро Linux передаёт данные между процессами через /dev/binder. Максимальный размер буфера транзакции — 1 МБ (делится между всеми активными транзакциями). Попытка передать большой Bitmap через Binder даёт TransactionTooLargeException.
AIDL: когда нужен настоящий IPC
AIDL (Android Interface Definition Language) генерирует Binder-прокси на обеих сторонах. Подходит для случаев, когда Service предоставляет API с несколькими методами и нужны синхронные ответы.
Определение интерфейса (IDataService.aidl):
// IDataService.aidl
package com.example.service;
import com.example.service.IDataCallback;
interface IDataService {
void getData(String key, IDataCallback callback);
boolean setData(String key, String value);
List<String> getKeys();
}
// IDataCallback.aidl
package com.example.service;
oneway interface IDataCallback {
void onResult(String key, String value);
void onError(int code, String message);
}
oneway на интерфейсе callback — асинхронный вызов, не блокирует вызывающий поток. Без oneway callback блокирует поток Service до завершения обработки на стороне клиента.
Реализация в Service:
class DataService : Service() {
private val binder = object : IDataService.Stub() {
override fun getData(key: String, callback: IDataCallback) {
// AIDL вызовы приходят в Binder thread pool, не в main thread
val value = dataStore.get(key)
if (value != null) {
callback.onResult(key, value)
} else {
callback.onError(404, "Key not found: $key")
}
}
override fun setData(key: String, value: String): Boolean {
return try {
dataStore.set(key, value)
true
} catch (e: Exception) {
false
}
}
override fun getKeys(): List<String> = dataStore.getAllKeys()
}
override fun onBind(intent: Intent): IBinder = binder
}
Подключение со стороны клиента:
class ClientActivity : AppCompatActivity() {
private var dataService: IDataService? = null
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
dataService = IDataService.Stub.asInterface(service)
// теперь можно вызывать методы
}
override fun onServiceDisconnected(name: ComponentName) {
dataService = null
// Service упал или был убит системой — переподключиться
}
}
override fun onStart() {
super.onStart()
val intent = Intent().apply {
component = ComponentName("com.example.service", "com.example.service.DataService")
}
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
override fun onStop() {
super.onStop()
unbindService(serviceConnection)
dataService = null
}
private fun fetchData(key: String) {
dataService?.getData(key, object : IDataCallback.Stub() {
override fun onResult(key: String, value: String) {
runOnUiThread { updateUI(key, value) }
}
override fun onError(code: Int, message: String) {
runOnUiThread { showError(message) }
}
})
}
}
Критический момент: callback IDataCallback.Stub вызывается в Binder thread pool на стороне клиента, не на main thread. runOnUiThread или lifecycleScope.launch(Dispatchers.Main) обязательны для обновления UI.
Безопасность: кто может подключиться
По умолчанию Service с exported="true" доступен любому приложению. Для ограничения доступа:
<service
android:name=".DataService"
android:exported="true"
android:permission="com.example.permission.DATA_SERVICE">
</service>
// Проверка внутри onBind или в каждом методе
override fun onBind(intent: Intent): IBinder? {
val callerUid = Binder.getCallingUid()
if (checkPermission("com.example.permission.DATA_SERVICE", callerUid) != PackageManager.PERMISSION_GRANTED) {
return null
}
return binder
}
Binder.getCallingUid() — UID процесса-клиента. Позволяет имплементировать whitelist по UIDs или проверить подпись APK через PackageManager.checkSignatures().
Messenger: проще, чем AIDL
Для простых сценариев (очередь команд от клиента к сервису) Messenger удобнее:
class MessengerService : Service() {
private val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
when (msg.what) {
MSG_DO_WORK -> {
val data = msg.data.getString("payload")
processData(data)
// ответ клиенту
msg.replyTo?.send(Message.obtain(null, MSG_RESULT, 0, 0).apply {
this.data = Bundle().apply { putString("result", "done") }
})
}
}
}
}
override fun onBind(intent: Intent): IBinder = Messenger(handler).binder
companion object {
const val MSG_DO_WORK = 1
const val MSG_RESULT = 2
}
}
Слабое место Messenger: все сообщения обрабатываются последовательно в Handler. Если обработка одного сообщения долгая — очередь встаёт. AIDL с Binder thread pool параллелен по умолчанию.
Передача больших данных: SharedMemory
Если нужно передать больше нескольких сотен КБ (изображение, аудиобуфер) — используется SharedMemory (API 27+) или MemoryFile (старые версии). Через Binder передаётся только дескриптор, данные — через общую память:
// Сторона Service
val sharedMemory = SharedMemory.create("image_buffer", bitmap.byteCount)
val buffer = sharedMemory.mapReadWrite()
bitmap.copyPixelsToBuffer(buffer)
SharedMemory.unmap(buffer)
// Передать ParcelFileDescriptor через Binder
val pfd = sharedMemory.fdDup // ParcelFileDescriptor для передачи через Binder
Это обходит лимит 1 МБ транзакции Binder и единственный правильный способ передавать медиаданные между процессами.
Типичные проблемы
DeadObjectException. Service убит системой, но клиент не знает об этом — следующий вызов метода бросает исключение. Обрабатывать в try/catch, в onServiceDisconnected повторно вызывать bindService.
Утечка ServiceConnection. Если bindService() вызван в onCreate(), а unbindService() забыт — Service держится в памяти до уничтожения процесса. Симметричность bind/unbind в onStart/onStop или onCreate/onDestroy — правило без исключений.
AIDL-интерфейс на стороне двух разных проектов. Если Service и клиент — разные APK, .aidl файлы должны быть одинаковыми (включая имя пакета). Несовпадение пакета даёт SecurityException или ClassCastException при asInterface().
Реализация IPC через AIDL: 3-7 дней — проектирование интерфейса, безопасность, обработка разрыва соединения, тестирование. Интеграция с существующим Service — от 1-2 дней. Стоимость рассчитывается индивидуально.







