天天看點

Android 插件化開發——宿主APP加載APK插件

本篇部落格說一下我們的宿主APP怎樣加載别的APK檔案。

首先需要說一些知識點,我們的Java檔案要想在Android環境運作,需要将.java檔案通過轉為class檔案,然後為了能在DVM上面運作,再轉為dex檔案。同理反過來,我們在代碼中要操作的基本都是class檔案,但是class檔案怎麼來呢? 從DexClassLoader加載擷取。

DexClassLoader和PathClassLoader什麼差別呢?

  1. DexClassLoader可以加載jar/apk/dex,可以從SD卡中加載未安裝的apk
  2. PathClassLoader隻能加載系統中已經安裝過的apk

至于具體源碼差別:建議讀一下DVM源碼。本篇不再贅述,之後專門寫一篇部落格講述DexClassLoader和PathClassLoader的差別。

**

加載外部APK

**

其實這個場景是這樣的:

  1. 從伺服器下載下傳APK,儲存在我們的手機儲存卡内
  2. 讀取APK檔案,然後生成對應的DexClassLoader
  3. 通過DexClassLoader的loadClass方法讀取插件APK dex中的任何一個類。

說幹就幹,首先我們建立一個項目MyPluginProject,在這個項目中建立一個Java類:TestModel

/**
 * author: liumengqiang
 * Date : 2019/7/27
 * Description :
 */
public class TestModel {

    private String name;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}
           

然後打包此項目生成:app-debug.apk。

由于插件APK基本都是從伺服器下載下傳,為了模拟這個場景,我們需要在宿主項目中建立一個assets檔案,将插件APK複制進去,然後在複制到宿主APP的data/data/files檔案夾下。

注意:這裡之前鑽牛角尖了,就是為什麼我不直接手動将插件APK直接複制到data/data/files,檔案夾下呢? 說幹就幹,但是問題來了,我在檔案管理器的目前Android/data/<包名>下找不到此包名,也就是說沒有生成包路徑。 說實話卡了很長時間,我一直以為是不是版本問題,最後求助朋友,折騰了一番,最終手動調用:getExternalCacheDir即可解決。生成路徑是生成了,那麼接下來就是複制APK了吧,當我複制到裡面之後,我發現,尼瑪,死活擷取不到複制進去的插件APK,真的是活見鬼,最後猛然發現那個包路徑是系統路徑!!!這個需要系統簽名權限才能有權限通路! 而我們代碼中assets中的APK是複制到記憶體中。。。

然後在我們的宿主項目中,建立assets,然後将app-debug.apk複制進去。接下來就是将app-debug.apk加載到記憶體中。

/**
     * 把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);
        }

    }

           

然後在APP啟動的時候調用:

extractAssets(this, "app-debug.apk");
           

接下來建立DexClassLoader ,加載插件app-debug.apk中的dex:

try {
            File extractFile = this.getFileStreamPath("app-debug.apk");
            String dexpath = extractFile.getPath();
            File fileRelease = getDir("dex", 0); //0 表示Context.MODE_PRIVATE

            classLoader = new DexClassLoader(dexpath,
                    fileRelease.getAbsolutePath(), null, getClassLoader());
        } catch (Throwable e) {
            e.printStackTrace();
        }
           

然後就是我們通過反射擷取TestModel類,并且設定setName,擷取getName:

private void modelTestClick() {
        try {
            Class<?> loadClass = classLoader.loadClass("com.liumengqiang.mypluginproject.TestModel");
            Object newInstance = loadClass.newInstance();
            Class[] paramClass = new Class[]{String.class};
            Object[] valueObj = new Object[]{"Hello world"};
            Method setName = loadClass.getMethod("setName", paramClass);
            setName.setAccessible(true);
            setName.invoke(newInstance, valueObj);

            Method getName = loadClass.getMethod("getName");
            getName.setAccessible(true);
            String value = (String) getName.invoke(newInstance);
            Toast.makeText(this, "Value:" + value, Toast.LENGTH_SHORT).show();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
           

至此就調用到了插件中的類。

細看上面的代碼醜得一逼,每次都要通過反射擷取類裡面的方法,如果這個類很多方法,那代碼量可想而知。可以更新一下:面向接口程式設計

**

面向接口程式設計

**

面向接口程式設計是面向對象程式設計的一種實作方式,它的核心思想是将抽象與實作分離,從元件的級别來設計代碼,達到高内聚低耦合的目的。最簡單的面向接口程式設計方法是,先定義底層接口子產品,再定義高層實作子產品。具體的之後在單獨開部落格講述。

上述代碼優化的思路:

  1. 建立一個interfaceLibrary, 是個Library,裡面定義一個接口:IBaseInterface :
/**
 1. author: liumengqiang
 2. Date : 2019/8/20
 3. Description :
 */
public interface IBaseInterface {
    void setName(String name);

    String getName();
}
           
  1. 然後插件MyPluginProject以及宿主項目分别依賴此Library
  2. 對于插件MyPluginProject中TestModel類,實作IBaseInterface接口:
/**
 * author: liumengqiang
 * Date : 2019/7/27
 * Description :
 */
public class TestModel implements IBaseInterface {

    private String name;

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return name;
    }
}

           
  1. 然後打包APK,按照上面的流程。
  2. 最後在宿主反射擷取TestModel類的時候,就可以通過接口接收建立的TestModel類了。
try {
            Class<?> loadClass = classLoader.loadClass("com.liumengqiang.mypluginproject.TestModel");
            IBaseInterface newInstance = (IBaseInterface) loadClass.newInstance();
            newInstance.setName("hello world");
            Toast.makeText(this, "" + newInstance.getName(), Toast.LENGTH_SHORT).show();
        } catch (Throwable e) {
            e.printStackTrace();
        }
           

是不是精簡了看着。 這是簡單的面向接口程式設計,也就是先定義接口:然後再實作。

Android開發交流:

Android 插件化開發——宿主APP加載APK插件

繼續閱讀