Android 插件化开发(三):资源插件化

在前面的文章中我们成功的加载了外部的Dex(Apk)并执行了插件的Bean代码。这时我们会想,能不能加载并运行插件Apk的Activity。答案当然是能,否则后续我们的研究就没意义了,但是想实现Activity的插件化运行,我们必须要解决一个问题——如何使用插件中的资源

本文我们就讲一下插件的资源加载机制,并讲述一下如何实现资源的插件化。

一、资源的加载机制

Android的资源文件分为两类:

第一类是res目录下存放的可编辑的资源文件,这类文件在编译时系统会自动在R文件中生成资源文件的16进制值。

例如:

public final class R {
    public static final class anim {
      public static final int abc_fade_in=0x7f050000;
      public static final int abc_fade_in=0x7f050000;
      ... 
    } 
}

我们在平时的开发时,访问这类资源比较简单,使用Context的getResource方法即可得到res下的各种资源。如下面代码所示:

String content = mContext.getResource().getString(R.string.content);

第二类是assets目录下存放的原始资源文件。apk在编译时不会编译assets下的文件,我们不能使用R.的方式访问,只能使用AssetsManager类的open方法来获取assets目录下的文件资源。

而AssetsManager又源于Resources类的getAssets方法,如下面代码所示:

Resource resource = getResource();
AssetsManager am = getResource().getAssets();
InputStream is  = getResource().getAssets().open("filename");

通过上面的分析,我们可以初步做出一个结论:我们能使用Resources类是一个很重要的类,通过此类提供的相关API,我们能操作资源的加载。

二、资源插件化的解决方案

谈及资源插件化,我们不得不对AssetsManager的API多说一些。

AssetsManager中有一个addAssetsPath(String Path)方法,App启动的时候就会将当前的apk路径传进去,接下来AssetsManager和Resources就能访问当前apk的所有资源了。

AssetsManager的addAssetsPath方法不对外,但是我们可以通过反射的方式,把插件apk的路径传到这个方法,这样就把插件的资源添加到一个资源池中了。App有几个插件,我们就调用几次addAssetsPath方法,把插件的资源都塞到池子里。

这里我们以加载插件Apk里面的字符串资源为目标,实战一下资源插件化:

首先我们在插件app的string.xml里面定义字符串资源:

 <string name="myplugin1_hello_world">Hello Plugin</string>

然后我们在宿主app编写如下代码:

public class MainActivity extends Activity {

    private AssetManager mAssetManager;

    private Resources mResources;

    private Resources.Theme mTheme;

    private String dexPath = null;    //apk文件地址

    private File fileRelease = null;  //释放目录

    protected DexClassLoader classLoader = null;

    private String pluginName = "plugin1.apk";

    TextView mTextView;

    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);
        extractAssets(newBase, pluginName);
    }

    @SuppressLint("NewApi")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        File extractFile = this.getFileStreamPath(pluginName);
        dexPath = extractFile.getPath();

        fileRelease = getDir("dex", 0);

        classLoader = new DexClassLoader(dexPath, fileRelease.getAbsolutePath(), null, getClassLoader());

        mTextView = findViewById(R.id.tv);

        //带资源文件的调用
        findViewById(R.id.btn_6).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View arg0) {
                loadResources();
                try {
                    Class mLoadClassDynamic = classLoader.loadClass("com.plugin1.Dynamic");
                    Object dynamicObject = mLoadClassDynamic.newInstance();
                    IDynamic dynamic = (IDynamic) dynamicObject;
                    String content = dynamic.getStringForResId(MainActivity.this);
                    mTextView.setText(content);
                    Toast.makeText(getApplicationContext(), content + "", Toast.LENGTH_LONG).show();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    protected void loadResources() {
        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();
        superRes.getDisplayMetrics();
        superRes.getConfiguration();
        mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
        mTheme = mResources.newTheme();
        mTheme.setTo(super.getTheme());
    }

    @Override
    public AssetManager getAssets() {
        return mAssetManager == null ? super.getAssets() : mAssetManager;
    }

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

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


    /**
     * 把Assets里面得文件复制到 /data/data/files 目录下
     *
     * @param context
     * @param sourceName
     */
    public static void extractAssets(Context context, String sourceName) {
        AssetManager am = context.getAssets();
        InputStream is = null;
        FileOutputStream fos = null;
        try {
            is = am.open(sourceName);
            File extractFile = context.getFileStreamPath(sourceName);
            fos = new FileOutputStream(extractFile);
            byte[] buffer = new byte[1024];
            int count = 0;
            while ((count = is.read(buffer)) > 0) {
                fos.write(buffer, 0, count);
            }
            fos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            closeSilently(is);
            closeSilently(fos);
        }
    }

    private static void closeSilently(Closeable closeable) {
        if (closeable == null) {
            return;
        }
        try {
            closeable.close();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

以上代码分为四个逻辑部分:

1).  loadResources方法。通过反射创建AssetManager对象,调用addAssetPath方法,把插件Plugin1路径添加到这个AssetManager对象中。从此这个AssetManager就只为插件Plugin1服务了。在这个AssetManager对象的基础上,创建对应的Resource和Theme对象。

2). 重写Activity的getAsset,getResource和getTheme方法。重写逻辑见上面的代码。

3). 加载外部的插件,生成这个插件对应的ClassLoader。

4). 通过反射,获取插件中的类,构造出插件类的对象,然后就可以让插件类读取插件中的资源了。

 

上述内容代码仓库地址为:https://github.com/renhui/RHPluginProgramming/tree/master/DexResAccess-master

基于这个思路,我们可以尝试使用插件资源替换当前显示的内容,实现换肤效果,核心思想是一样的,这里就不过多赘述了。

 

posted @ 2019-11-25 17:28  灰色飘零  阅读(...)  评论(...编辑  收藏