创建一个新的 Android 项目,我称之为“动态 APK 加载”,指定 whatever.dynamicapkloading 应用 ID,并留下“应用”模块名称。
创建另一个没有任何活动的 Android 应用程序模块。我在同一个项目中做过这个,但这取决于你。我将模块称为“动态”,并将应用程序 ID 设置为 whatever.dynamic。
从“动态”项目中删除所有不必要的东西。我已经删除了启动器可绘制对象以及任何其他资源,并且还从 build.gradle 中删除了 AppCompat 依赖项。我的清单内容看起来很简约,如下所示:<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>
将其添加到运行配置。将启动选项/启动设置为无。构建 -> 构建 APK!在这一步中,我有一个名为 dynamic-debug.apk 的 4.9 KB 文件。
将此文件 dynamic/build/outputs/apk/dynamic-debug.apk 移动到我们主要项目的资产中,即。 e.到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);
-
编写将从指定 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);
-
使用这些资源!有两种获取资源 ID 的方法。
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);