从其他APK动态加载资源

3
我正在考虑从其他APK中加载资源的方法。例如,我有一个APK(它只是APK,没有安装在手机内存中),保存在SD卡上,其中包含我想在已安装应用程序中使用的资源。是否可以从存储在SD卡上的APK的res/x中加载资源到我的已安装应用程序中(例如布局、图像、字符串等)?
谢谢。

看起来似乎没有人能够解决它。我个人认为这是不可能的,但你永远不知道... - Waypoint
可能是可能的,但不会起作用:ID 仍然会冲突。 - m0skit0
2个回答

5

在阅读和调试一些 AOSP 代码后,我终于成功地实现了动态加载资源。下面是详细步骤:

  1. Create a new Android project, I've called it 'Dynamic APK Loading', specified whatever.dynamicapkloading app ID, and left 'app' module name.

  2. Create another Android application module without any Activities. I've done this in the same project, but this is up to you. I've called module 'dynamic', and set application ID to whatever.dynamic.

  3. Remove all unnecessary things from 'dynamic' project. I've removed launcher drawables along with any other resources, and also removed AppCompat depencency from build.gradle. My manifest content looked minimalistic, like this: <application />.

  4. Add some code to 'dynamic' project. For example:

    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();
        }
    }
    
  5. Add some resources to 'dynamic' project. I've created strings.xml:

    <resources>
        <string name="someString">This is a dynamically loaded string.</string>
        <string name="anotherString">Lol! That\'s it.</string>
    </resources>
    
  6. Add it to Run configurations. Set Launch Options / Launch to Nothing. Build -> Build APK! On this step I've got 4.9 KB file called dynamic-debug.apk.

  7. Move this file, dynamic/build/outputs/apk/dynamic-debug.apk, into our main project's assets, i. e. to app/src/main/assets/.

  8. Create/open a class which will load code & resources. Say, this will be DynamicActivity.

  9. Write code which will copy APK from assets to private app directory. It's boring & trivial, you know:

    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);
        }
    }
    
  10. Write code which will load necessary class and instantiate it.

    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);
    
  11. Write code which will load resources from the specified APK. This was a hard one! All constructors and methods are public, but they are hidden. I've written it in reflective way, but to avoid this you can either compile your code against full framework jar or write a part of code in 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);
    
  12. Use these resources! There are two ways of getting resource IDs.

    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);
    

就这样了!这对我很有效。 完整的DynamicActivity.java代码

在实际项目中,您必须在构建时签名APK,并在加载时检查其签名。当然,永远不要将其存储在sdcard上,否则您的APK可能会被篡改!

您还应该静态缓存资源ID,但在配置更改时重新创建Resources并重新查询资源值。

有用的链接:


2

2
似乎这个 APK 必须被安装。 - Yevgeny Simkin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接