Впровадження міжпроцесної комунікації (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 в інтерфейсі зворотного виклику — це асинхронне—не блокує потік, що викликає. Без oneway зворотний виклик блокує потік 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) }
}
})
}
}
Критичний момент: зворотний виклик 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 процесу клієнта. Це дозволяє вам впровадити UID-вайтлист або перевірити підпис 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, знову викличте bindService в onServiceDisconnected.
Витік 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 дні. Вартість розраховується індивідуально.







