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

基於這個思路,我們可以嘗試使用插件資源替換當前顯示的內容,實現換膚效果,核心思想是一樣的,這里就不過多贅述了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM