
Если вы думаете, что навигационная система в современном автомобиле — это просто красивое приложение, которое рисует синюю линию на карте и говорит «через 200 метров поверните направо», вы застряли в 2010 году.
В Android Automotive OS (AAOS) навигация часто становится одним из самых жирных и критически важных системных компонентов. Это «пространственный мозг» машины. Десятки других модулей постоянно дергают её за рукав с вопросами: «Где мы?», «Какое ограничение скорости?», «Сколько полос на дороге?», «В какой полосе мы сейчас едем?».
В этой статье я поделюсь опытом из своей практики разработки навигационной системы для крупного немецкого автопроизводителя. Мы разберем, как построить надежное межпроцессное взаимодействие (IPC) внутри автомобиля на примере передачи ADAS-атрибутов для японского модуля ETC 2.0.
Проблема: Японские дороги не прощают ошибок
Представьте ситуацию: машина едет по Токио. В Японии работает система ETC 2.0 (Electronic Toll Collection) — умные платные дороги. В отличие от старых систем, где вы останавливаетесь у шлагбаума, ETC 2.0 списывает деньги на лету, анализируя маршрут, пробки и даже конкретную полосу, по которой вы едете.
Аппаратный модуль ETC 2.0 вшит где-то под торпедой. Но сам по себе он «слеп». Ему нужна прецизионная гео-локация и данные ADAS (Advanced Driver Assistance Systems) — уклоны дороги, кривизна поворотов, точное количество полос.
Где взять эти данные? У нашей навигационной системы!
Но тут возникает проблема. Навигация — это приложение A (со своим процессом). Модуль ETC (или его сервис-адаптер в Android) — это приложение B (в другом процессе). Процессы в Android изолированы друг от друга как параноики в бункерах. Нам нужен надежный, быстрый и безопасный мост.
Добро пожаловать в мир AIDL (Android Interface Definition Language).
AIDL: Контракт, подписанный кровью
В мобильной разработке мы привыкли кидаться Intent-ами или использовать EventBus. В Automotive, когда речь идет о безопасности и деньгах (оплата дорог!), такие костыли не работают. Нужен жесткий контракт и строгая типизация.
С помощью AIDL мы описываем интерфейс, который навигационная система (Сервер) предоставляет наружу, а модуль ETC 2.0 (Клиент) может вызывать так, будто это локальный код.
Давайте напишем этот контракт.
Шаг 1: Описываем структуру данных (Parcelable)
Данные между процессами передаются через механизм Binder. Чтобы объект пролез через это «бутылочное горлышко», он должен реализовывать Parcelable.
Создадим файл AdasAttribute.aidl:
// AdasAttribute.aidl package com.germanauto.nav.api; // Объявляем, что у нас есть такой Parcelable класс parcelable AdasAttribute;
И сам класс в Kotlin (в реальном проекте там десятки полей, но для примера упростим):
package com.germanauto.nav.api import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize data class AdasAttribute( val latitude: Double, val longitude: Double, val currentLane: Int, val speedLimit: Int, val isTollRoad: Boolean ) : Parcelable
Шаг 2: Создаем интерфейс (AIDL)
Теперь опишем, как ETC-модуль будет получать эти данные. Мы используем паттерн "Подписка" (Observer), так как данные обновляются десятки раз в секунду.
Создаем IAdasCallback.aidl (коллбек от навигации к клиенту):
package com.germanauto.nav.api; import com.germanauto.nav.api.AdasAttribute; oneway interface IAdasCallback { // oneway означает, что вызов асинхронный и не блокирует поток void onAdasDataUpdated(in AdasAttribute data); }
И главный сервис IAdasProvider.aidl:
package com.germanauto.nav.api; import com.germanauto.nav.api.IAdasCallback; interface IAdasProvider { void registerCallback(IAdasCallback callback); void unregisterCallback(IAdasCallback callback); }
Когда проект скомпилируется, Android сгенерирует Java-классы (Stub и Proxy), которые возьмут на себя всю грязную работу по маршаллизации данных в байты и обратно.
Шаг 3: Реализация на стороне Навигации (Сервер)
В нашем навигационном приложении мы создаем Service, который реализует этот сгенерированный интерфейс (Stub).
class AdasProviderService : Service() { // Потокобезопасный список клиентов (ETC модуль и другие) private val callbacks = RemoteCallbackList<IAdasCallback>() private val binder = object : IAdasProvider.Stub() { override fun registerCallback(callback: IAdasCallback) { callbacks.register(callback) Log.d("AdasProvider", "Client registered") } override fun unregisterCallback(callback: IAdasCallback) { callbacks.unregister(callback) } } override fun onBind(intent: Intent?): IBinder { return binder } // Эту функцию вызывает внутреннее ядро навигации каждый раз, // когда позиция машины меняется fun broadcastAdasData(data: AdasAttribute) { val i = callbacks.beginBroadcast() for (index in 0 until i) { try { // Отправляем данные клиенту через IPC callbacks.getBroadcastItem(index).onAdasDataUpdated(data) } catch (e: RemoteException) { // Клиент умер в процессе передачи Log.e("AdasProvider", "Failed to send data", e) } } callbacks.finishBroadcast() } }
Боль опыта: Используйте RemoteCallbackList. Он автоматически отписывает клиента, если процесс клиента (ETC) внезапно упал (крашнулся). Иначе вы получите утечку памяти и исключение DeadObjectException.
Шаг 4: Подключение из модуля ETC (Клиент)
Теперь мы — японский модуль оплаты. Нам нужно приконнектиться к немецкой навигации.
class EtcTollManager(private val context: Context) { private var adasProvider: IAdasProvider? = null // Наш слушатель private val adasCallback = object : IAdasCallback.Stub() { override fun onAdasDataUpdated(data: AdasAttribute) { Log.d("ETC2.0", "Received ADAS. Lane: ${data.currentLane}, Toll: ${data.isTollRoad}") // Списываем иены со счета водителя... } } private val serviceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName, service: IBinder) { // Превращаем сырой IBinder в удобный интерфейс adasProvider = IAdasProvider.Stub.asInterface(service) try { adasProvider?.registerCallback(adasCallback) } catch (e: RemoteException) { Log.e("ETC2.0", "Nav service crashed!") } } override fun onServiceDisconnected(name: ComponentName) { adasProvider = null // Навигация упала или обновляется } } fun startListening() { val intent = Intent("com.germanauto.action.BIND_ADAS_PROVIDER") intent.setPackage("com.germanauto.nav") context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE) } }
Суровая реальность: Транзакционный лимит
Всё выглядит красиво, пока вы не запустите это в продакшене. В один прекрасный день ваша навигация упадет с ошибкой: TransactionTooLargeException.
Почему? Механизм Binder (на котором работает AIDL) имеет жесткий лимит буфера — 1 мегабайт на ВСЕ активные транзакции ВСЕХ процессов в системе.
Если ваша навигация попытается передать огромный массив с геометрией перекрестка (тысячи точек) через AIDL 10 раз в секунду, буфер переполнится, и система убьет ваш процесс.
Как лечить?
Передавать только изменения (дельту), а не весь объект целиком.
Для тяжелых гео-данных (например, кусков карты) использовать
MemoryFile(Shared Memory), а через AIDL передавать только файловый дескриптор (указатель). Но об этом трюке с ��азделяемой памятью мы поговорим в следующих статьях!
Итого
В Android Automotive ваше приложение редко живет в вакууме. Навигационная система — это яркий пример провайдера данных, от которого зависят критически важные системы автомобиля.
Грамотное проектирование AIDL-интерфейсов, понимание жизненного цикла процессов и умение работать с RemoteCallbackList — это база для любого, кто хочет писать надежный софт для автопрома.