Android dynamically loads resources

Categories of resource files

1.Android resource files are divided into two categories:
The first type is the compilable resource file stored in the res directory. When compiling, the system will automatically generate the hexadecimal value of the resource file in R.java, as follows:

public final class R {
public static final class id {
public static final int action0 = 0x7f0b006d;
...
}
}

It is relatively simple to access this kind of resources. Use the getResources method of Context to get the Resource object, and then get various resources through the getXXX method of Resources:

Resources resources = getResources();
String appName = resources. getString(R. string. app_name);

The second type is the original resource files stored in the assets directory. The apk will not compile the resource files under the assets when compiling. We access them through the AssetManager object, and the AssetManager comes from the getAssets method of the Resources class:

Resources resources = getResources();
AssetManager am = getResources().getAssets();
InputStream is = getResources().getAssets().open("filename");

Resources is the focus of loading resources. Various internal methods of Resources actually call the internal methods of AssetManager indirectly, and AssetManager is responsible for requesting resources from the system.

The principle of accessing external resources

The principle of loading resources is recommended to view Android skinning resources (Resources) loading source code analysis
And Android resource dynamic loading and related principle analysis

Here is just a brief

context. getResources(). getText()
##Resources
@NonNull public CharSequence getText(@StringRes int id) throws NotFoundException {
        CharSequence res = mResourcesImpl.getAssets().getResourceText(id);
        if (res != null) {
            return res;
        }
        throw new NotFoundException("String resource ID #0x"
                 + Integer.toHexString(id));
    }

 ##ResourcesImpl
 public AssetManager getAssets() {
        return mAssets;
    }

Internally, mResourcesImpl is called to access, this object is of type ResourcesImpl, and finally resources are accessed through AssetManager. Now it can be concluded that AssetManager is the object that actually loads resources, and Resources is the class for API calls at the app level.

AssetManager
/**
 * Provides access to an application's raw asset files; see {@link Resources}
 * for the way most applications will want to retrieve their resource data.
 * This class presents a lower-level API that allows you to open and read raw
 * files that have been bundled with the application as a simple stream of
 * bytes.
 */
public final class AssetManager implements AutoCloseable {

   /**
     * Add an additional set of assets to the asset manager. This can be
     * either a directory or ZIP file. Not for use by applications. Returns
     * the cookie of the added asset, or 0 on failure.
     * @hide
     */
    @UnsupportedAppUsage
    public int addAssetPath(String path) {
        return addAssetPathInternal(path, false /*overlay*/, false /*appAsLib*/);
    }
}

It is very important here and needs to be explained. First of all, AssetManager is a resource manager, which is responsible for loading resources. It has a hidden method addAssetPath inside, which is used to load resource files under the specified path. That is to say, you put the apk/jar The path is passed to it, and it can read the resource data to AssetManager, and then it can be accessed.

But there is a problem. Although it is AssetManager that actually loads resources, it is indeed the Resources object that we access through the API, so look at the construction method of the Resources object.

Creation of ResourcesImpl
/**
     * Create a new Resources object on top of an existing set of assets in an
     * AssetManager.
     *
     * @param assets Previously created AssetManager.
     * @param metrics Current display metrics to consider when
     *selecting/computing resource values.
     * @param config Desired device configuration to consider when
     * selecting/computing resource values (optional).
     */
    public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        this(null);
        mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
    }

Seeing this construction method, I feel a little bit. The mResourcesImpl object can be constructed through the AssetManager object. It has also been analyzed that resource access is done through the mResourcesImpl.getAssets().getXXX() method. Now there is a way to solve the problem of loading external apk resources.

Creating a ResourcesImpl requires 4 parameters:

  • Parameter 1: AssetManager specific resource management (important)

  • Parameter 2: Some encapsulation of the DisplayMetrics screen
    Get the density of the screen through getResources().getDisplayMetrics().density
    Get the width of the screen through getResources().getDisplayMetrics().widthPixels, etc.

  • Parameter three: Configuration Some configuration information

  • Parameter 4: Compatibility of DisplayAdjustments resources, etc.

Solutions for loading external apk resources

First, we need to have 3 projects: one is the host project, which is used to load external resources; the other is a plug-in project, which is used to provide external resources. There is also a public library, which defines the interface methods for obtaining resources. Both the host project and the plug-in project import this public library. The method introduced:

File => Project Structure =>

Plug-in project

  1. String resource definition
<string name="hello_message">Hello</string>
  1. Image resource definition
    Put a picture named ic_baseline_train_24.png in the drawable folder

Create a class that reads resources:

public class UIUtils implements IDynamic {
    public String getTextString(Context context){
        return context.getResources().getString(R.string.hello_message);
    }

    public Drawable getImageDrawable(Context ctx){
        return ctx.getResources().getDrawable(R.drawable.ic_baseline_train_24);
    }

    public View getLayout(Context ctx){
        LayoutInflater layoutInflater = LayoutInflater. from(ctx);
        View view = layoutInflater.inflate(R.layout.activity_main,null);
        return view;
    }
}

After compiling the plugin project, we name the generated apk file plugin1.apk, and copy the apk file to the assets directory of the host file:

#build.gradle

assemble. doLast {
    android.applicationVariants.all { variant ->
        // Copy Release artifact to HostApp's assets and rename
        if (variant.name == "release") {
            variant. outputs. each { output ->
                File originFile = output. outputFile
                println originFile.absolutePath
                copy {
                    from originFile
                    into "$rootDir/app/src/main/assets"
                    rename(originFile.name, "plugin1.apk")
                }
            }
        }
    }
}

Host project

We create a host project, and copy the plug-in apk under assets to the path of /data/data/package name/files under the sd card when the application starts, and then load the apk generated by the plug-in project file and display the resources in the plugin.

public class BaseActivity extends Activity {
    private AssetManager mAssetManager;
    public Resources mResources;
    private Resources. Theme mTheme;

    protected HashMap<String, PluginInfo> plugins = new HashMap<String, PluginInfo>();

    private String dexPath1,dexPath2; //apk file address
    private String fullReleaseFilePath; //release directory
    private String plugin1name = "plugin1.apk";
    private String plugin2name = "plugin2.apk";

    public ClassLoader classLoader1, classLoader2;
    @Override
    protected void attachBaseContext(Context newBase) {
        super. attachBaseContext(newBase);
        Utils.extractAssets(newBase,plugin1name);
        Utils.extractAssets(newBase,plugin2name);
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        generatePluginInfo(plugin1name);
        generatePluginInfo(plugin2name);

        fullReleaseFilePath = getDir("dex",0).getAbsolutePath();
        dexPath1 = this.getFileStreamPath(plugin1name).getPath();
        dexPath2 = this.getFileStreamPath(plugin2name).getPath();

        classLoader1 = new DexClassLoader(dexPath1,
                fullReleaseFilePath, null, getClassLoader());
        classLoader2 = new DexClassLoader(dexPath2,
                fullReleaseFilePath, null, getClassLoader());
    }

/**
     * Load external plug-ins and generate the ClassLoader corresponding to the plug-ins
     * @param pluginName
     */
    protected void generatePluginInfo(String pluginName) {
        File extractFile = this. getFileStreamPath(pluginName);
        File fileRelease = getDir("dex", 0);
        String dexpath = extractFile. getPath();
        DexClassLoader classLoader = new DexClassLoader(dexpath, fileRelease. getAbsolutePath(), null, getClassLoader());

        plugins.put(pluginName, new PluginInfo(dexpath, classLoader));
    }

 /**
     * important
     * Through reflection, create an AssetManager object, call the addAssetPath method, and add the path of the plug-in Plugin to the AssetManager object
     * @param dexPath
     */
    protected void loadResources(String dexPath) {
        try {
            AssetManager assetManager = AssetManager. class. newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager, dexPath);
            mAssetManager = assetManager;
        } catch (Exception e) {
            e.printStackTrace();
        }
        Resources superRes = super. getResources();
        mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
        mTheme = mResources. newTheme();
        mTheme.setTo(super.getTheme());
    }
 }
 
 /**
     * important
     * Rewrite Acitivity's getAsset, getResources and getTheme methods
     * mAssetManager points to the plugin, if this object is empty, call the getAssets method of the parent class ContextImpl,
     * The AssetManager object obtained at this time points to the host HostApp, and the resource read is the resource in the HostApp
     * @return
     */
 @Override
    public AssetManager getAssets() {
        if(mAssetManager == null){
            return super. getAssets();
        }
        return mAssetManager;
    }

    @Override
    public Resources getResources() {
        if(mResources == null){
            return super. getResources();
        }
        return mResources;
    }

    @Override
    public Resources. Theme getTheme() {
        if(mTheme == null){
            return super. getTheme();
        }
        return mTheme;
    }

A base class BaseActivity is created here to prepare for loading APK resources. The actual loading of APK resources is in MainActivity:

public class MainActivity extends BaseActivity {

    private TextView textView;
    private ImageView imageView;
    private LinearLayout layout;
    private Button btn1, btn2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R. layout. activity_main);
        textView = findViewById(R.id.text);
        imageView = findViewById(R.id.imageview);
        layout = findViewById(R.id.layout);

        btn1 = findViewById(R.id.btn1);
        btn2 = findViewById(R.id.btn2);

        btn1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                PluginInfo pluginInfo = plugins. get("plugin1. apk");

                loadResources(pluginInfo. getDexPath());

// doSomething(pluginInfo.getClassLoader(),"com.chinatsp.plugin1");
                doSomethingOther(pluginInfo.getClassLoader(),"com.chinatsp.plugin1");
// doSomethingAnother(pluginInfo.getClassLoader(),"com.chinatsp.plugin1");
            }
        });

        btn2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                PluginInfo pluginInfo = plugins. get("plugin2. apk");

                loadResources(pluginInfo. getDexPath());

// doSomething(pluginInfo.getClassLoader(),"com.chinatsp.plugin2");
// doSomethingOther(pluginInfo.getClassLoader(),"com.chinatsp.plugin2");
                doSomethingAnother(pluginInfo.getClassLoader(),"com.chinatsp.plugin2");
            }
        });

        System.out.println(getString(R.string.hello));
    }

/**
     * Through reflection, get the class in the plug-in, construct the object uiUtils of the plug-in class, and then reflect and call the method in the plug-in class object UIUtils
     * @param cl
     * @param uiUtilsPkgName
     */
    private void doSomething(ClassLoader cl,String uiUtilsPkgName) {
        try {
            Class clazz = cl.loadClass(uiUtilsPkgName + ".UIUtils");
            Object uiUtils = RefInvoke. createObject(clazz);
            String str = (String) RefInvoke.invokeInstanceMethod(uiUtils, "getTextString", Context.class, this);
            textView.setText(str);

            Drawable drawable = (Drawable) RefInvoke.invokeInstanceMethod(uiUtils, "getImageDrawable", Context.class, this);
            imageView.setBackground(drawable);

            layout. removeAllViews();

            View view = (View) RefInvoke.invokeInstanceMethod(uiUtils, "getLayout",Context.class,this);
            layout. addView(view);

        } catch (Exception e) {
            Log.e("DEMO", "msg:" + e.getMessage());
        }
    }

 /**
     * Get the inner class of the R file R.java in the plug-in class by direct reflection, and get the hexadecimal value corresponding to the resource file in the inner class, that is, R.string.xxx
     * The value corresponding to R.drawable.xxx, get the resource file through the getxxx method of the getResources method
     * @param cl
     * @param uiUtilsPkgName
     */
    private void doSomethingOther(ClassLoader cl,String uiUtilsPkgName) {
        try {
            Class stringClass = cl.loadClass(uiUtilsPkgName + ".R$string");
            int resId1 = (int) RefInvoke.getStaticFieldObject(stringClass,"hello_message");
            textView.setText(getResources().getString(resId1));

            Class drawableClass = cl.loadClass(uiUtilsPkgName + ".R$drawable");
            int resId2 = (int) RefInvoke.getStaticFieldObject(drawableClass,"ic_baseline_train_24");
            imageView.setBackground(getResources().getDrawable(resId2));

            Class layoutClass = cl.loadClass(uiUtilsPkgName + ".R$layout");
            int resId3 = (int) RefInvoke.getStaticFieldObject(layoutClass,"activity_main");
            View view = LayoutInflater.from(this).inflate(resId3,null);

            layout. removeAllViews();
            layout. addView(view);
        } catch (Exception e) {
            Log.e("DEMO", "msg:" + e.getMessage());
        }
    }

 /**
     * Through reflection, get the class in the plug-in, construct the dynamicObject object of the plug-in class, and then directly call the method in the plug-in class object UIUtils
     * @param cl
     * @param uiUtilsPkgName
     */
    private void doSomethingAnother(ClassLoader cl,String uiUtilsPkgName) {
        Class mLoadClassDynamic = null;
        try {
            mLoadClassDynamic = cl.loadClass(uiUtilsPkgName + ".UIUtils");
            Object dynamicObject = mLoadClassDynamic. newInstance();
            IDynamic dynamic = (IDynamic) dynamicObject;
            String str = dynamic. getTextString(this);
            textView.setText(str);

            Drawable drawable = dynamic. getImageDrawable(this);
            imageView.setBackground(drawable);

            layout. removeAllViews();
            View view = dynamic. getLayout(this);
            layout. addView(view);

        } catch (Exception e) {
            Log.e("DEMO", "msg:" + e.getMessage());
        }
    }

}

Source Code