开发者

Dynamic resource loading from other APK

I am thinking about some way of loading resources from other APKs. For example, I have an APK (it is just APK, it is not installed in phone memory) on sdcard, which contains resources I want to use in my installed application. Is it possible to load resources from res/x from APK stored on the sdcard to my installed application (e.g.开发者_运维知识库 layout, images, strings, ...).

Thanks


After reading and debugging some AOSP code, I've finally managed to load resources dynamically. Step-by-step guide:

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

That's it! This worked for me. Full DynamicActivity.java code

In real-world projects you must sign APK while building and check its signature while loading. And, of course, never store it on sdcard, otherwise your APK may be spoofed!

You should also cache resource IDs statically, but re-create Resources and re-query resource values when configuration gets changed.

Useful links:

  • My another answer about dynamic class loading
  • Grab'n Run library, designed to load code dynamically and perform some security checks. However, I haven't tested it myself.
  • Android – hidden and internal API’s revisited, an article about linking against full framework jar
  • Dynamically Retrieving Resources in Android — this post says that reflective way of getting resource IDs is faster. I've never checked it.


You can use PackageManager.getResourcesForApplication to access resources found in a different apk.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜