Android小知識-如何載入外部dex檔案中的類

公眾號_顧林海發表於2018-10-19

本平臺的文章更新會有延遲,大家可以關注微信公眾號-顧林海,包括年底前會更新kotlin由淺入深系列教程,目前計劃在微信公眾號進行首發,如果大家想獲取最新教程,請關注微信公眾

想要了解外掛化,首先得知道如何載入外部的dex檔案,這裡的外掛APK會存放在主APP的assets目錄中,用於模擬伺服器下載外掛。

第一步:建立主專案和外掛專案

先建立我們的主專案,並在專案中建立一個外掛依賴庫,取名為pluginlibrary,主專案依賴pluginlibrary。

主專案建立完畢後,接著建立外掛專案,將專案中的app模組複製到主專案並重新命名為plugin,同時也依賴pluginlibrary。

修改settings.gradle檔案,如下:

include ':app',':plugin'':pluginlibrary'
複製程式碼

重新編譯一下。

第二步:編譯外掛APK

將pluginlibrary依賴庫編譯成jar包,並放在外掛專案plugin的lib目錄下,不是libs目錄,通過compileOnly引用pluginlibrary的jar包,compileOnly只會在編譯時用到相應的jar,打包成APK後不會存在於APK中。

pluginlibrary編譯jar包,在pluginlibrary的build.gradle的配置如下:

apply plugin: 'com.android.library'

android {
    compileSdkVersion 28

    defaultConfig {
        minSdkVersion 17
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
}

task clearJar(type: Delete){
    delete 'build/outputs/pluginlibray.jar'
}

task makePluginLibraryJar(type: Copy){
    from ('build/intermediates/packaged-classes/release/')
    into ('build/outputs/')
    include ('classes.jar')
    rename ('classes.jar', 'pluginlibrary.jar')
}

makePluginLibraryJar.dependsOn(clearJar,build)
複製程式碼

編譯完成後可以從右側的Gradle皮膚的other分組中找到makePluginLibraryJar命令:

微信圖片_20181009100628.png

雙擊makePluginLibraryJar命令進行編譯,可以看到底部輸出編譯成功:

BUILD SUCCESSFUL in 4s
50 actionable tasks: 2 executed, 48 up-to-date
10:04:10: Task execution finished 'makePluginLibraryJar'.
複製程式碼

在pluginlibrary/build/outputs/下看到pluginlibrary.jar:

微信截圖_20181009100901.png

在plugin專案中建立lib資料夾並將pluginlibrary.jar複製到lib目錄下:

plugin專案的build.gradle修改如下:

    compileOnly files("lib/pluginlibrary.jar")
複製程式碼

第三步:載入外部dex

在編譯pluginlibrary.jar之前在專案中建立一個介面:

package com.plugin.administrator.pluginlibrary;

public interface IPluginBean {
    void setUserName(String name);
    String getUserName();
}

複製程式碼

在外掛plugin專案中就建立一個類:

package com.plugin.administrator.myapplication;

import com.plugin.administrator.pluginlibrary.IPluginBean;

public class UserInfo implements IPluginBean {

    private String name="billgu";

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

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

複製程式碼

編譯外掛plugin專案,將生成的apk複製到主專案的assets目錄下。

接下來就是主專案編寫載入外部DEX檔案了,需要把assets目錄下的plugin-debug.apk複製到/data/data/files目錄下,這步操作放在Activity的attachBaseContext方法中:

private String apkName = "plugin-debug.apk";    //apk名稱
    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);
        try {
            extractAssets(newBase, apkName);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
    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) {
        }
    }
複製程式碼

如何從APK中讀取dex檔案,需要藉助於DexClassLoader ,宣告如下:

   DexClassLoader (String dexPath,
              String optimizedDirectory,
             String libraryPath, 
              ClassLoader parent)
複製程式碼
  • dexPath:  指目標類所在的jar/apk檔案路徑, 多個路徑使用 File.pathSeparator分隔, Android裡面預設為 “:”

  • optimizedDirectory: 解壓出的dex檔案的存放路徑,以免被注入攻擊,不可存放在外接儲存。

  • libraryPath :目標類中的C/C++庫存放路徑。

  • parent: 父類裝載器

在onCreate方法中進行初始化DexClassLoader:

     private String mDexPath = null;    //apk檔案地址
    private File mFileRelease = null;  //釋放目錄
    private DexClassLoader mClassLoader = null;
    
    private void initDexClassLoader(){
        File extractFile = this.getFileStreamPath(apkName);
        mDexPath = extractFile.getPath();
        mFileRelease = getDir("dex", 0); //0 表示Context.MODE_PRIVATE
        mClassLoader = new DexClassLoader(mDexPath,
                mFileRelease.getAbsolutePath(), null, getClassLoader());
    }
複製程式碼

生成外掛APK的classLoader後就可以載入外掛plugin-debug.apk中的任何類了。

點選按鈕事件如下:

buttonGet.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View arg0) {
                Class mLoadClassBean;
                try {
                    mLoadClassBean = mClassLoader.loadClass("com.plugin.administrator.myapplication.UserInfo");
                    Object beanObject = mLoadClassBean.newInstance();

                    IPluginBean pluginBean= (IPluginBean) beanObject;
                    pluginBean.setUserName("顧林海");
                    Toast.makeText(getApplicationContext(), pluginBean.getUserName(), Toast.LENGTH_LONG).show();

                } catch (Exception e) {

                }
            }
        });
複製程式碼

載入外掛plugin中的UserInfo類,呼叫setUserName和getUserName方法,點選按鈕Toast顯示“顧林海”。至此載入外部dex檔案中的類就結束了。

838794-506ddad529df4cd4.webp.jpg

搜尋微信“顧林海”公眾號,定期推送優質文章。

相關文章