Приступим.
Создаем фрагмент
Для начала создадим некий фрагмент Fragment0 и реализуем у него метод onCreateView():
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
//return inflater.inflate(R.layout.fragment1, container, false);
LinearLayout linearLayout = new LinearLayout(getActivity());
linearLayout.setOrientation(LinearLayout.VERTICAL);
linearLayout.setGravity(Gravity.CENTER);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
Button button = new Button(getActivity());
button.setText("Кнопка");
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showFragment("jatx.networkingclassloader.dx.Fragment1", null); // рассмотрим чуть позже
}
});
linearLayout.addView(button, lp);
return linearLayout;
}
Стандартный метод создания разметки из xml в нашем случае работать не будет, поэтому для первого фрагмента мы создаем ее программно.
Далее нам нужно на основе модуля, содержащего фрагмент, создать APK, распаковать его с помощью unzip, и выложить файл classes.dex на сервер.
Реализуем загрузку классов
В отдельном модуле создадим класс NetworkingActivity и реализуем в нем следующие методы:
@Override
protected void onCreate(Bundle savedInstanceState) {
// ......
dataDir = getApplicationInfo().dataDir;
frameLayout = (FrameLayout) findViewById(R.id.main_frame);
progressDialog = new ProgressDialog(this);
progressDialog.setIndeterminate(true);
progressDialog.setMessage("Загружаем классы из сети");
progressDialog.show();
// Загружаем classes.dex с сервера, подробно рассматривать не будем:
DownloadTask downloadTask = new DownloadTask(this, dataDir);
downloadTask.execute(null, null, null);
// receiver нужен для того, чтобы мы могли из фрагмента открывать другие фрагменты:
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String className = intent.getStringExtra("className");
Bundle args = intent.getBundleExtra("args");
showFragment(className, args);
}
};
IntentFilter filter = new IntentFilter("jatx.networkingclassloader.ShowFragment");
registerReceiver(receiver, filter);
}
// Вызывается, когда наш AsyncTask успешно загрузил c сервера classes.dex:
public void downloadReady() {
Toast.makeText(this, "Классы из сети загружены", Toast.LENGTH_SHORT).show();
progressDialog.dismiss();
showFragment("jatx.networkingclassloader.dx.Fragment0", null);
}
public void showFragment(String className, Bundle arguments) {
// Наш загруженный файл:
File dexFile = new File(dataDir, "classes.dex");
Log.e("Networking activity", "Loading from dex: " + dexFile.getAbsolutePath());
// Каталог кэша, нужен для DexClassLoader:
File codeCacheDir = new File(getCacheDir() + File.separator + "codeCache");
codeCacheDir.mkdirs();
// Создаем ClassLoader:
DexClassLoader dexClassLoader = new DexClassLoader(
dexFile.getAbsolutePath(), codeCacheDir.getAbsolutePath(), null, getClassLoader());
try {
// Загружаем класс фрагмента по имени:
Class clazz = dexClassLoader.loadClass(className);
// Создаем объект класса:
Fragment fragment = (Fragment) clazz.newInstance();
// Передаем фрагменту аргументы и отображаем его:
fragment.setArguments(arguments);
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.add(R.id.main_frame, fragment);
fragmentTransaction.commit();
} catch (Exception e) {
e.printStackTrace();
}
}
Открываем из фрагмента другие фрагменты
Для этого в классе LoadableFragment (суперкласс всех наших фрагментов) реализуем следующий метод:
public void showFragment(String className, Bundle args) {
Intent intent = new Intent("jatx.networkingclassloader.ShowFragment");
intent.putExtra("className", className);
intent.putExtra("args", args);
getActivity().sendBroadcast(intent);
}
Надеюсь, здесь все понятно.
Наш следующий фрагмент мы попробуем создать несколько иначе.
Подгружаем из сети xml-разметку
Для начала, создаем и выкладываем на сервер файл разметки. Я нашел на github библиотеку, которая умеет парсить xml layout из строки. Для корректной работы пришлось ее немного подпилить.
И так, добавим в наш класс LoadableFragment следующие методы:
protected void loadLayoutFromURL(FrameLayout container, String url) {
this.container = container;
// загружаем файл разметки:
LayoutDownloadTask layoutDownloadTask = new LayoutDownloadTask(this, url);
layoutDownloadTask.execute(null, null, null);
}
// Вызывается, если xml-разметка успешно загружена:
public void onLayoutDownloadSuccess(String xmlAsString) {}
Теперь с помощью этого всего создадим фрагмент Fragment1:
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
FrameLayout frameLayout = new FrameLayout(getActivity());
loadLayoutFromURL(frameLayout, "http://tabatsky.ru/testing/fragment1.xml");
return frameLayout;
}
@Override
public void onLayoutDownloadSuccess(String xmlAsString) {
LinearLayout linearLayout = (LinearLayout) DynamicLayoutInflator.inflate(getActivity(), xmlAsString, container);
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
linearLayout.setLayoutParams(lp);
final EditText editText = (EditText) DynamicLayoutInflator.findViewByIdString(linearLayout, "edit_text");
Button button = (Button) DynamicLayoutInflator.findViewByIdString(linearLayout, "button");
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Bundle args = new Bundle();
args.putString("userName", editText.getText().toString());
showFragment("jatx.networkingclassloader.dx.Fragment2", args);
}
});
}
Послесловие
Полностью исходный код проекта можно посмотреть на github. Готовый APK можно скачать здесь.
Ну и напоследок, хочу сказать пару слов о возможном применении подобной технологии: например, можно выдавать с сервера разные classes.dex в зависимости от типа аккаунта пользователя (платный/бесплатный), что должно несколько увеличить сложность реверс-инжиниринга приложения.
Комментарии (18)
ser-mk
18.06.2017 16:41Очень интересно, спасибо за статью!
По реализации возникло пару вопросов
Данный dex файл сохраняется в папке с приложением и его уже больше не требуется грузить повторно?
Есть еще такая штука AAR, в ней можно хранить и ресурсы и разметку, что бы не грузить с сервера.
Вам не доводилось, подобным образом, динамически загружать такой тип ресурсов?
jatx
19.06.2017 00:45+1dex-файл загружается каждый раз при запуске.
Смысл как раз в том, что мы можем изменить на сервере dex-файл, не обновляя само приложение.
В принципе, при желании не сложно добавить проверку на время обновления dex на сервере, и загружать его по новой, только если он изменился.
По поводу AAR — как я понял из описания, он используется на этапе компиляции и сборки приложения.
Лично мне этим пользоваться не приходилось.
kolipass
19.06.2017 05:53можно выдавать с сервера разные classes.dex в зависимости от типа аккаунта пользователя (платный/бесплатный)
Интересно виденье автора, как это реализовать, чтобы не было возможности перепаковать apk с обратной проверкой, или вообще включить платный classes.dex внутрь apk.
Я так полагаю, необходима комбинация нескольких разных подходов.jatx
19.06.2017 07:05Вопрос интересный.
Мое мнение — стопроцентную защиту приложения от взлома сделать невозможно в принципе.
Но этот самый процент защиты всегда можно увеличить )kolipass
20.06.2017 06:25И всё таки, можно какой-то абстрактный пример «на котиках», что можно вынести на сервер?
Я придумал только такое: в игрушках можно вынести автогенерируемые уровни, но смысла хранить уровни в classes.dex не придумал. Более-менее интересное решение пришло на ум такое: в клиентском apk есть интерфейс-протокол общения с _платным_ сервером, а в classes.dex нам сервер авторизации подсунет реализацию этого нитерфейса, которая будет динамически генерироваться.jatx
20.06.2017 09:31Мне еще такой пример пришел на ум. Только не с платным, а с корпоративным сервером.
Не знаю, может ли такое реально быть кому-нибудь нужно, но можно сделать так, чтобы classes.dex загружался только из корпоративной сети, а при смене сети самоудалялся.onepavel
21.06.2017 18:25Если не ошибаюсь класс лоадер внешний dex файл грузит со стораджа, куда предварительно выкачивается и сохраняется. И даже видел реализации, которые таскают с собой дополнительный dex в зашифрованном виде, но при подгрузке и расшифровке все-равно кладут на сторадж. Было бы интереснее если научить класс лоадер загружать dex, который лежит только в памяти в хипе. Снятие дампа может кидс хакеров отсеить.
jatx
22.06.2017 02:07Тоже думал о том, как хранить его в памяти. Пока не придумал.
Еще такая идея была — dex загружается, и первым делом проверяет на устройстве root. И дальше работает, только если устройство не рутованное.
ChPr
Обязательно стоит упомянуть, что такое поведение запрещено правилами Google Play Store.
jatx
Интересно, что в данном случае подразумевается под "сторонними разработчиками"? Подходит ли под это определение код, созданный тем же разработчиком, который публикует APK на Google Play?
Borz
учитывая что в начале написано про "разработчика", то имеется в виду всё-таки сторонний, т.е. не тот же самый разработчик
ChPr
Проверил английскую версию:
Borz
на территории РФ же будет действовать русская версия соглашения. Или я не прав?
jatx
Не уверен. Каждый раз при публикации там нужно ставить галочку, что-то вроде "приложение не нарушает экспортное законодательство США" (точную формулировку не помню), так что полагаю, что английская версия в данном случае первична, а в русской просто неточности перевода.
thelongrunsmoke
В корпоративном сегменте, класс лоадеры используются широко, дабы разгрузить тяжёлые приложения.
Но в Google Play за это забанят.
olegchir
что такое «внутренний код»? Если например, я использую свой простенький интерпретатор внутри приложения, а скрипты загружаю из интернетов, считается ли это кодом?
jatx
С той же страницы правил: