Содержание статьи
Приложения с расширяемой функциональностью — обыденная вещь в настольных ОС. Большинство сложных, узкоспециализированных приложений позволяют подключать дополнения в виде плагинов или скриптов. Это удобная, важная и легко реализуемая функция, которая в простейшем случае может быть встроена в приложение с помощью нескольких строк, всего лишь загружающих внешнюю библиотеку. Но как с этим обстоят дела в Android, где каждое приложение замкнуто в свою собственную песочницу, а распространять «просто библиотеки» через маркет нельзя?
Введение
Есть как минимум три способа создать приложение с поддержкой плагинов для Android.
Первый способ заключается в том, чтобы встроить в приложение интерпретатор простого скриптового языка (Lua лучше всего годится на эту роль). Главная фишка этого способа — простота разработки плагинов. Но есть и ряд недостатков:
- Необходимо реализовать биндинги к функциям своего приложения и коллбэки в обратную сторону, что может оказаться совсем не простой задачей.
- Невозможно распространять плагины через маркет, придется либо просить пользователей самостоятельно копировать скрипты в память устройства, либо кодить свой собственный репозиторий и платить за хостинг.
- Производительность скриптов не самая высокая.
- Тебе, скорее всего, придется иметь дело с интерпретатором, написанным на C/C++, и самостоятельно собирать его для разных процессорных архитектур.
В общем, не самый удачный, но имеющий право на существование способ.
Второй способ основан на встроенном в среду исполнения механизме динамической загрузки классов. Он позволяет разбить приложение на множество модулей, которые будут подгружены прямо во время его работы. По сути этот способ очень похож на тот, что используется в приложениях для настольных ОС, и имеет те же недостатки:
- Модули не получится распространять через маркет.
- После загрузки модули становятся частью приложения, а значит, тебе придется самостоятельно ограничивать их права, вводить квоты на время исполнения и написать много другого кода, следящего за поведением модуля и ограничивающего его возможности.
- Необходимо будет не только тщательно продумать API модулей, но и следить за его соблюдением. Система подключит модуль, невзирая на любые несостыковки в API, а попытка вызвать несуществующую функцию или функцию, принимающую другой тип аргумента, приведет к падению всего приложения.
Тем не менее модули могут быть очень полезны при разработке разного рода бэкдоров и троянов (в образовательных целях, естественно), поэтому тему разработки модульных приложений, основанных на прямой загрузке классов, мы все-таки рассмотрим, но в следующий раз. А сегодня поговорим о третьем способе, самом удобном и полностью соответствующем идеологии Android. Это плагины, подключаемые с помощью RPC-механизма Binder.
Плагины и Binder
Binder — это механизм, с помощью которого в Android реализована система обмена сообщениями и вызова методов между приложениями и системными компонентами. Подробно о Binder я писал в статье, посвященной логированию звонков, однако рассказал только об обмене данными с его помощью. Если же мы хотим применить его для реализации плагинов, нам необходимо разобраться, как использовать возможность удаленного вызова процедур (RPC). Но сначала определимся с архитектурой нашего приложения.
Допустим, у нас есть гипотетическая софтина, к которой нужно прикрутить поддержку плагинов. Каждый плагин должен быть полноценным приложением для Android (с собственным графическим интерфейсом или без, на усмотрение разработчика), так что его тоже можно будет распространять через маркет. Плагины должны поддерживать определенный нами API, с помощью которого приложение сможет вызвать его функции. Приложение должно уметь само находить установленные плагины и добавлять их в список.
С учетом сказанного нам необходимо внести в код приложения следующие изменения:
- Определить API, с помощью которого приложение будет общаться с плагинами.
- Реализовать механизм поиска плагинов и вести их «учет».
API
Самый удобный и простой способ реализации плагинов — в виде сервисов, запускаемых по запросу. Наше приложение будет находить установленные в системе приложения-плагины, в нужные моменты запускать реализованные в них сервисы и вызывать их функции. При этом сервисы будут запущены только тогда, когда они действительно нужны, а система сама позаботится об их завершении и менеджменте ресурсов. Именно так, кстати, работает система плагинов виджета DashClock и многих других приложений.
Вызов функций будет происходить с помощью Binder, а это, как я уже сказал, RPC-механизм, и он требует описания API (интерфейса) с каждой из сторон (приложения и плагинов) с помощью языка AIDL (Android Interface Definition Language). Сам AIDL очень прост, и описание интерфейса на нем почти ничем не отличается от Java. Для нашего примера создадим простой интерфейс, определяющий две функции: run()
и name()
:
package com.example.plugin
interface IPlugin {
// Возвращает имя плагина
String name();
// Запускает плагин и возвращает результат работы
String run(int seconds);
}
Создай файл IPlugin.aidl
с помощью New → AIDL → AIDL file и помести в него эти строки. Затем выполни Build → Make Project, чтобы Android Studio преобразовал AIDL в обычный код на Java.
Простейший плагин
Теперь реализуем сам плагин. Для этого создаем новый проект (пусть его имя будет com.example.plugin1
), добавляем в него файл IPlugin.aidl
(обрати внимание, что он должен точно совпадать с аналогичным файлом из предыдущего раздела) и файл Plugin.java
со следующим содержимым:
public class Plugin extends Service {
@Override
public void onCreate() {
super.onCreate();
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
private final IPlugin.Stub mBinder = new IPlugin.Stub() {
public String name() {
return "ExamplePlugin";
}
public String run(int seconds) {
try {
Thread.sleep(seconds * 1000);
} catch (Exception e) {}
return "plugin1 done";
}
};
}
Это простейший плагин, который просто засыпает при запуске. Наиболее важная его часть — это метод onBind
, который возвращает объект класса Binder, реализующий наш интерфейс IPlugin, в момент подключения к сервису. Другими словами, при подключении к плагину наше приложение получит объект, с помощью которого сможет вызывать определенные в плагине функции name()
и run()
.
Поиск плагинов
Теперь нам необходимо реализовать систему поиска установленных плагинов. Проще (и правильнее) всего сделать это с помощью интентов, о которых можно прочитать в упомянутой выше статье. Для этого сначала внесем изменения в файл Manifest нашего плагина, добавив в него следующие строки (в раздел application):
<service android:name=".Plugin" android:exported="true">
<intent-filter>
<action android:name="com.example.action.PLUGIN" />
</intent-filter>
</service>
Данные строки означают, что сервис Plugin должен быть открыт для доступа извне и «отвечать» на интент com.example.action.PLUGIN
, однако нам этот интент нужен вовсе не для этого, а для того, чтобы найти плагин в системе среди сотен установленных приложений.
Сам механизм поиска плагинов реализовать довольно просто. Для этого достаточно обратиться к PackageManager с просьбой вернуть список всех приложений, отвечающих на интент com.example.action.PLUGIN
:
PackageManager packageManager = getPackageManager();
Intent intent = new Intent("com.example.action.PLUGIN");
List<ResolveInfo> list = packageManager.queryIntentServices(intent, 0);
Чтобы с плагинами было удобнее работать, создадим HashMap и поместим в него имена приложений-плагинов в качестве ключей, а имена их сервисов — в качестве значений:
HashMap<String, String> plugins = new HashMap<>();
if (list.size() > 0) {
for (ResolveInfo info : list) {
ServiceInfo serviceInfo = info.serviceInfo;
plugins.put(serviceInfo.applicationInfo.packageName, serviceInfo.name);
}
}
Запуск функций плагина
Теперь, когда у нас есть готовый плагин, а приложение умеет его находить, мы можем вызвать его функции. Для этого мы должны подключиться к сервису плагина с помощью bindService
, передав ему обработчик подключения, который будет вызван, когда соединение с сервисом установится. В коде все это будет выглядеть примерно так:
IPlugin plugin;
// Определяем наш «обработчик» подключения
class MyServiceConnection implements ServiceConnection {
// Коллбэк, который будет вызван при подключении к сервису
public void onServiceConnected(ComponentName className, IBinder boundService ) {
// Получаем объект для взаимодействия с сервисом
plugin = IPlugin.Stub.asInterface(boundService);
// Пробуем вызвать метод run() и логируем его вывод в консоль (это должна быть строка plugin1 done)
try {
String result = plugin.run(2);
Log.d(TAG, "result: " + result);
} catch (RemoteException e) {}
}
// Коллбэк, который будет вызван при потере связи с сервисом
public void onServiceDisconnected(ComponentName className) {
plugin = null;
Log.d(TAG, "onServiceDisconnected" );
}
};
// Создаем Intent для вызова сервиса, определенного в приложении com.example.plugin1
ComponentName name = new ComponentName("com.example.plugin1", plugins.get("com.example.plugin1"));
Intent i = new Intent();
i.setComponent(name);
// Подключаемся к сервису, запуская его в случае необходимости
MyServiceConnection myServiceConnection = new MyServiceConnection();
bindService(i, myServiceConnection, Context.BIND_AUTO_CREATE);
...
// Отключаемся от сервиса
unbindService(myServiceConnection);
Данный код, при всей своей громоздкости, делает очень простую вещь — подключается к сервису, реализованному в приложении com.example.plugin1
(это наш плагин, напомню), и вызывает функцию run()
, которую мы ранее определили в плагине и интерфейсе IPlugin.aidl
. Само собой разумеется, данный пример будет работать только в отношении одного плагина, имя которого заранее известно. В реальном приложении необходимо будет либо проходить по всему хешмепу plugins и запускать каждый плагин последовательно, либо реализовать графический интерфейс, который будет динамически создавать и выводить на экран список плагинов на основе хешмепа и позволит юзеру запускать каждый из них по отдельности. Можно использовать хешмеп plugins для создания кнопок интерфейса, при нажатии на которые будет запускаться тот или иной плагин.
В общем, вариантов масса, главное — запомнить, что перед запуском нового плагина необходимо отключаться от предыдущего. Все остальное Android сделает сам: при подключении к плагину запустит сервис, передаст ему управление, а затем завершит сервис при отключении. Никакого лишнего оверхеда на систему, никаких чрезмерных расходов оперативки, даже за поведением плагинов следить не надо, в случае если один или несколько из них начнут грузить систему или выжирать оперативку — система их прибьет.
И еще одна важная деталь. Функции плагина вызываются синхронно, то есть в нашем случае при выполнении plugin.run(2)
приложение будет заморожено на две секунды. По-хорошему тут нужно выполнять запуск функции в отдельном потоке, а затем отправлять результат исполнения в основной поток.
Выводы
Как видишь, реализовать приложение с поддержкой плагинов для Android не так уж и сложно. Более того, операционка даже поощряет создание модульных приложений, которые будут передавать управление друг другу, вместо громоздких софтин «все в одном». Главное — научиться пользоваться встроенными в ОС инструментами и понять ее философию.