Android dynamically loading resources

Resource file classification

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

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

To access this kind of resource, 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 category is the original resource files stored in the assets directory. When apk is compiled, the resource files under assets will not be compiled. We access them through the AssetManager object. AssetManager is derived 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 indirectly call the internal methods of AssetManager. AssetManager is responsible for requesting resources from the system.
Insert image description here

Principles of accessing external resources

For the principle of loading resources, it is recommended to check the Android resource (Resources) loading source code analysis
and Android resource dynamic loading and related principle analysis.

Here I just briefly talk about

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. Finally, the resource is accessed through AssetManager. Now we can draw a conclusion 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*/);
    }
}

This is very key and needs to be explained. First of all, AssetManager is an 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 add the apk/jar If you pass the path to it, it can read the resource data into the AssetManager and then access it.

But there is a problem. Although it is AssetManager that actually loads resources, the Resources object we access through the API is indeed the Resources object, so let’s take a 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 been analyzed before that resource access is completed through the mResourcesImpl.getAssets().getXXX() method. Now there is a way to solve the problem of loading external apk resources.

Creating ResourcesImpl requires 4 parameters:

  • Parameter 1: AssetManager specific resource management (important)

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

  • Parameter three: Configuration some configuration information

  • Parameter 4: Compatibility of DisplayAdjustments resources, etc.

Solution to loading external apk resources

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

File => Project Structure =>
Insert image description here

Plug-in project
  1. String resource definition
<string name="hello_message">Hello</string>
  1. Image resource definition:
    Put an image named ic_baseline_train_24.png in the drawable folder

Create a class to read the resource:

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 plug-in 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 when the application starts, copy the plug-in apk under assets to the path of /data/data/package name/files under the sd card, then load the apk file generated by the plug-in project, and display the plug-in H.

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文件地址
    private String fullReleaseFilePath; //释放目录
    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);

        genegatePluginInfo(plugin1name);
        genegatePluginInfo(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());
    }

/**
     * 加载外部的插件,生成插件对应的ClassLoader
     * @param pluginName
     */
    protected void genegatePluginInfo(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));
    }

 /**
     * 重要
     * 通过反射,创建AssetManager对象,调用addAssetPath方法,把插件Plugin的路径添加到这个AssetManager对象中
     * @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());
    }
 }
 
 /**
     * 重要
     * 重写Acitivity的getAsset,getResources和getTheme方法
     * mAssetManager是指向插件的,如果这个对象为空,就调用父类ContextImpl的getAssets方法,
     * 这个时候得到的AssetManager对象就指向宿主HostApp,读取的资源也就是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 make preparations before 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));
    }

/**
     * 通过反射,获取插件中的类,构造出插件类的对象uiUtils,再反射调用插件类对象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());
        }
    }

 /**
     * 直接反射获取插件类中的R文件R.java的内部类,获取内部类中资源文件对应生成的16进制的值,也就R.string.xxx
     * R.drawable.xxx对应的值,通过getResources方法的getxxx方法来获取资源文件
     * @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());
        }
    }

 /**
     * 通过反射,获取插件中的类,构造出插件类的对象dynamicObject,再直接调用插件类对象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

Guess you like

Origin blog.csdn.net/jxq1994/article/details/130825577