После прочтения и отладки некоторого кода AOSP мне наконец-то удалось динамически загружать ресурсы.Пошаговое руководство:
Создайте новый проект Android, я назвал его «Динамическая загрузка APK», указал whatever.dynamicapkloading
идентификатор приложения и оставил имя модуля «приложение».
Создайте еще один модуль приложения Android без каких-либо действий.Я сделал это в том же проекте, но это зависит от вас.Я назвал модуль «динамическим» и установил идентификатор приложения на whatever.dynamic
.
Удалите все ненужные вещи из «динамического» проекта.Я удалил элементы рисования в лаунчере вместе с любыми другими ресурсами, а также удалил зависимость AppCompat из build.gradle.Мое манифестное содержание выглядело минималистично, например: <application />
.
Добавить код в «динамический» проект.Например:
package whatever.dynamic;
import android.content.Context;
import android.widget.Toast;
public final class Code implements Runnable {
private final Context context;
public Code(Context context) {
this.context = context;
}
@Override
public void run() {
Toast.makeText(context, "Running dynamic code!",
Toast.LENGTH_SHORT).show();
}
}
Добавьте некоторые ресурсы в «динамический» проект.Я создал strings.xml
:
<resources>
<string name="someString">This is a dynamically loaded string.</string>
<string name="anotherString">Lol! That\'s it.</string>
</resources>
Добавьте его для запуска конфигураций.Установите параметры запуска / запуска ничего.Build -> Build APK!На этом этапе у меня есть файл размером 4,9 КБ, который называется dynamic-debug.apk
.
Переместите этот файл, dynamic/build/outputs/apk/dynamic-debug.apk
, в активы нашего основного проекта, то есть в app/src/main/assets/
.
. Создать / открыть класс, который будет загружать код и ресурсы.Скажем, это будет DynamicActivity.
Напишите код, который будет копировать APK из ресурсов в личный каталог приложения.Это скучно и тривиально, вы знаете:
File targetApk = new File(getDir("dex", Context.MODE_PRIVATE), "app.apk");
// copy APK from assets to private directory
// remove this condition in order to keep dynamic APK fresh
if (!targetApk.exists() || !targetApk.isFile()) {
try (BufferedInputStream bis = new BufferedInputStream(
getAssets().open("dynamic-debug.apk"));
OutputStream dexWriter = new BufferedOutputStream(
new FileOutputStream(targetApk))) {
byte[] buf = new byte[4096];
int len;
while((len = bis.read(buf, 0, 4096)) > 0) {
dexWriter.write(buf, 0, len);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
Напишите код, который загрузит необходимый класс и создаст его экземпляр.
PathClassLoader loader = new PathClassLoader(
targetApk.getAbsolutePath(), getClassLoader());
Class<?> dynamicClass = loader.loadClass("whatever.dynamic.Code");
Constructor<?> ctor = dynamicClass.getConstructor(Context.class);
dynamicInstance = (Runnable) ctor.newInstance(this);
Writeкод, который будет загружать ресурсы из указанного APK.Это был тяжелый!Все конструкторы и методы общедоступны, но они скрыты.Я написал это рефлексивно, но во избежание этого вы можете либо скомпилировать свой код с использованием полного фреймворка jar, либо написать часть кода на Smali.
AssetManager assets = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class
.getMethod("addAssetPath", String.class);
if (addAssetPath.invoke(assets, targetApk.getAbsolutePath()) ==
Integer.valueOf(0)) {
throw new RuntimeException();
}
Class<?> resourcesImpl = Class.forName("android.content.res.ResourcesImpl");
Class<?> daj = Class.forName("android.view.DisplayAdjustments");
Object impl = resourcesImpl
.getConstructor(AssetManager.class, DisplayMetrics.class,
Configuration.class, daj)
.newInstance(assets, getResources().getDisplayMetrics(),
getResources().getConfiguration(), daj.newInstance());
dynamicResources = Resources.class.getConstructor(ClassLoader.class)
.newInstance(loader);
Method setImpl = Resources.class.getMethod("setImpl",
Class.forName("android.content.res.ResourcesImpl"));
setImpl.invoke(dynamicResources, impl);
Используйте эти ресурсы!Есть два способа получения идентификаторов ресурсов.
int someStringId = dynamicResources.getIdentifier(
"someString", "string", "whatever.dynamic");
String someString = dynamicResources.getString(someStringId);
Class<?> rString = Class.forName("whatever.dynamic.R$string", true, loader);
anotherStringId = rString.getField("anotherString").getInt(null);
String anotherString = dynamicResources.getString(anotherStringId);
В реальных проектах вы должны подписывать APK при сборке и проверять его подпись при загрузке.И, конечно же, никогда не сохраняйте его на SD-карте, иначе ваш APK может быть подделан!
Вы также должны статически кэшировать идентификаторы ресурсов, но заново создавать Resources
и повторно запрашивать значения ресурсов при изменении конфигурации.