1. 程式人生 > >Android動態載入APK外掛類

Android動態載入APK外掛類

前言

外掛化開發目前是非常熱門的Android技術,它主要通過將不同的業務物件封裝到外掛中,這樣不同的業務可以獨立開發和除錯,提高專案的開發效率。APK檔案就是常見的外掛檔案格式,它包含了Android應用常見的資源和程式碼,不過由於外掛沒有被安裝到系統中還需要開發者手動實現外掛類的動態載入,這裡就是用一個簡單的Demo來測是從APK外掛獲取類並執行裡面的程式碼邏輯。

準備

可以使用Android Studio建立APK專案,在專案中直接新增java程式碼,先增加一個簡單的實現類,我們需要在另外一個Android應用中獲取這個定義的類並且呼叫它實現的方法。

package com.example.apkresource;

public
class HelloWorld { // 返回一個字串 public String getMessage() { return "Hello World from APK Resouce"; } // 計算階乘 public int factorial(int x) { if (x == 0) { return 1; } if (x < 0) { return -1; } return x * factorial(x - 1
); } }

執行gradle assemble,由於沒有MainActivity會啟動失敗,不過apk檔案還是生成了,將生成的apk檔案拖到Android Studio編輯區域檢視內部的dex檔案裡有Hello這個類的實現。再把生成的apk檔案推送到Android手機的sdcard裡。
這裡寫圖片描述

adb push appres.apk /sdcard/
appres.apk: 1 file pushed. 4.1 MB/s (1830141 bytes in 0.431s)

載入類實現

Android中類載入器有都最終繼承自java.lang.ClassLoader,這裡僅介紹和動態載入相關的類載入器,主要有下面三種。

類載入器 註釋
BootClassLoader 和java虛擬機器中不同的是BootClassLoader是ClassLoader內部類,由java程式碼實現而不是c++實現,是Android平臺上所有ClassLoader的最終parent,這個內部類是包內可見
BaseDexClassLoader 負責從指定的路徑中載入類,載入類裡面的各種校驗、檢查和初始化工作都由它來完成
PathClassLoader 繼承自BaseDexClassLoader,只能載入已經安裝到Android系統的APK裡的類,主要邏輯由BaseDexClassLoader實現
DexClassLoader 繼承自BaseDexClassLoader,可以載入使用者自定義的其他路徑裡的類,主要邏輯都由BaseDexClassLoader實現

URLClassLoader只能用於載入jar檔案,由於Android裡的虛擬機器只識別dex檔案,因而在Android中無法使用這個載入器。

對於本Demo中從SDCard里加載類可以使用DexClassLoader來做載入器,它內部會首先從dexPath路徑出找dex檔案,再用odex優化dex檔案到optimizedDirectory位置,最後再載入優化之後的dex檔案裡的class物件。這個類主要有四個構造引數:

  • dexPath:要載入的類所在的jar或者apk檔案路徑,類裝載器將從該路徑中尋找指定的目標類,該類必須是APK或jar的全路徑
  • optimizedDirectory:odex優化之後的dex存放路徑,真正的資料是從這個位置的dex檔案載入的,由於ClassLoader只能載入內部儲存路徑中的dex檔案,所以這個路徑必須為內部路徑
  • libPath:目標類中所使用的C/C++庫存放的路徑
  • classloader:本裝載器的父裝載器,一般使用當前執行類的裝載器就可以了,在Android用context.getClassLoader()就可以了
private void loadClass() {
    // 獲取前面推送到SDCard中的外掛路勁
    String apkPath = Environment.getExternalStorageDirectory() + File.separator + "appres.apk";
    // 優化後的dex存放路徑
    String dexOutput = getCacheDir() + File.separator + "DEX";
    File file = new File(dexOutput);
    if (!file.exists()) file.mkdirs();
    DexClassLoader dexClassLoader = new DexClassLoader(apkPath, dexOutput, null, getClassLoader());
    try {
        // 從優化後的dex檔案中載入APK_HELLO_CLASS_PATH類
        clazz = dexClassLoader.loadClass(APK_HELLO_CLASS_PATH);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

呼叫了上面的載入動態類之後檢視內部快取下面的DEX資料夾包含了優化之後的dex檔案。
這裡寫圖片描述

測試Demo

測試Demo主要是兩個按鈕,一個按鈕負責呼叫getHello方法,一個負責呼叫factorial方法計算階乘,最後的結果會存放到介面裡的TextView檢視上。

if (v == getHello) {
    if (clazz == null) { // 如果還沒有載入類
        loadClass(); // 動態載入類
    }

    if (clazz != null) {
        try {
            Object object = clazz.newInstance(); // 建立類例項
            Method method = clazz.getMethod("getMessage"); // 後去getMessage方法
            String text = (String) method.invoke(object); // 呼叫返回結果
            mText.setText(text); // 將結果設定到text上
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
} else if (v == factorial) {
    if (clazz == null) {
        loadClass();
    }

    if (clazz != null) {
        try {
            Object object = clazz.newInstance(); // 建立類例項
            Method method = clazz.getMethod("factorial", int.class); // 獲取factorial(int)方法
            int value = (int) method.invoke(object, 6); // 呼叫factorial(6)
            mText.setText(String.valueOf(value));
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

上面的實現首先loadClass從APK中載入動態類,之後使用反射獲取要呼叫的方法,最後執行的結果如下。
這裡寫圖片描述