外掛化知識梳理(7) 類的動態載入入門

澤毛發表於2017-12-21

一、前言

外掛化知識梳理(6) - Small 原始碼分析之 Hook 原理 這一章的學習完成之後,下一步我們將進入外掛化載入的精髓,動態載入類的學習,在此之前,我們需要先準備一些關於類載入的知識。

Android當中,支援動態載入的兩種方式為:DexClassLoaderPathClassLoader。這兩者之間的區別為:

  • DexClassLoader
  • 可以載入jar、apk、dex
  • 支援從SD卡目錄載入。
  • PathClassLoader
  • 在許多文章中都有提到,在Dalvik虛擬機器上,只能載入已經安裝到系統中的Apk檔案,也就是/data/app目錄下的apk檔案。之所以有這個限制是因為PathClassLoader會去讀取data/dalvik-cache目錄下經過優化後的dex檔案,如果檔案不存在,那麼就會報錯。由於手邊沒有機器,所以沒有版本驗證。
  • 而在ART虛擬機器上,通過原始碼當中的註釋,可以發現是支援的。
    外掛化知識梳理(7)   類的動態載入入門

二、具體例項

例項的工程目錄結構為:

外掛化知識梳理(7)   類的動態載入入門

  • app:宿主模組
  • library:外掛模組
  • libraryinterface:外掛介面模組

其中,applibrary模組分別依賴於libraryinterfacelibrarylibraryinterfaceAndroid Library型別的Module,下面,我們開始講解整個工程的構建過程。

2.1 介面模組 libraryinterface

介面模組相當於是宿主模組和外掛模組所定義的一套標準,宿主模組遵循固定的業務邏輯,而具體的實現則根據外掛模組的不同而不同。 在介面模組中,我們定義一個簡單的介面IPlugin.java

public interface IPlugin {
    public int getVersion();
}
複製程式碼

2.2 外掛模組 library

首先,我們在外掛模組的build.gradle檔案中,引入libraryinterface模組

dependencies {
    //引入介面模組
    compile project (':libraryinterface')
}
複製程式碼

接著,我們編寫一個實現類:

public class PluginImpl implements IPlugin {

    @Override
    public int getVersion() {
        return 1;
    }
}
複製程式碼

接下來需要做的就是將該外掛模組打包成一個jar檔案,同樣是在build.gradle檔案中,建立一個Task任務:

task makeJar(type: Copy) {
    delete 'build/libs/plugin.jar'
    from ('build/intermediates/bundles/release/')
    into ('../file/')
    include ('classes.jar')
    rename ('classes.jar','plugin.jar')
}

makeJar.dependsOn(build)
複製程式碼

首先點選make module

外掛化知識梳理(7)   類的動態載入入門
接下來,在專案的根目錄下執行命令:

./gradlew makeJar
複製程式碼

就會得到一個plugin.jar檔案,但是這個jar檔案是不能夠被動態載入的,因為它內部其實是.class檔案,我們通過解壓可以看出:

外掛化知識梳理(7)   類的動態載入入門
那麼我們就需要通過Android SDK自帶的dx工具進行轉換,把它轉換為.dex,轉換後的檔案為plugin_dex.jar

/Users/lizejun/Library/Android/sdk/build-tools/25.0.3/dx --dex --output=file/plugin_dex.jar file/plugin.jar
複製程式碼

plugin_dex.jar解壓之後,可以看到它已經被轉換成了.dex檔案:

外掛化知識梳理(7)   類的動態載入入門
最後,將該jarpush到手機中的/sdcard/Plugin目錄下:
外掛化知識梳理(7)   類的動態載入入門

2.3 宿主模組 app

首先,宿主模組同樣需要依賴於介面模組libraryinterface

dependencies {
    //引入介面模組
    compile project (':libraryinterface')
}
複製程式碼

在程式碼當中,我們通過DexClassLoader/PathClassLoader動態外部的外掛plugin_dex.jar,通過反射例項化PluginImpl類,並呼叫它的getVersion()方法進行驗證:

public class MainActivity extends AppCompatActivity {

    private TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView) findViewById(R.id.tv_plug_result);
        getPluginA();
    }

    private void getPluginA() {
        File dexOutputDir = getDir("dex1", 0);
        String dexPath = Environment.getExternalStorageDirectory().toString() + "/Plugin/plugin_dex.jar";
        DexClassLoader loader = new DexClassLoader(dexPath, dexOutputDir.getAbsolutePath(), null, getClassLoader());
        try {
            Class clz = loader.loadClass("com.demo.lizejun.library.PluginImpl");
            IPlugin impl = (IPlugin) clz.newInstance();
            int version = impl.getVersion();
            mTextView.setText("Version=" + version);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void getPluginB() {
        String dexPath = Environment.getExternalStorageDirectory().toString() + "/Plugin/plugin_dex.jar";
        PathClassLoader loader = new PathClassLoader(dexPath, getClassLoader());
        try {
            Class clz = loader.loadClass("com.demo.lizejun.library.PluginImpl");
            IPlugin impl = (IPlugin) clz.newInstance();
            int version = impl.getVersion();
            mTextView.setText("Version=" + version);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
複製程式碼

最終的結果為:

外掛化知識梳理(7)   類的動態載入入門


更多文章,歡迎訪問我的 Android 知識梳理系列:

相關文章