【Android】動態載入實現的簡單demo
概念▪說明
動態載入:
此處的動態載入是指從服務端或者其他地方獲取jar包,並在執行時期,載入jar包,並與 jar包互相呼叫。
本例中,為了方便演示,將要動態載入的jar放到了assets目錄下,在程式執行時期,將其載入到/data/data/pkgname/files下,來模擬從服務端獲取
為什麼要動態載入:
1. 減少應用安裝包體積, 程式包很大時,將部分模組打成jar包,動態的從服務端獲取,然後載入
2. 方便升級維護,將主要功能放入動態jar包中,主應用(不包含動態jar包的部分)主要來維護動態jar,包括jar的載入,升級等。升級應用,可以更新動態jar包,使用者在不重新安裝的情況下,就能做到部分(強調部分,是因為它比較適用於部分功能升級)升級
3. 外掛化開發,動態jar包可以當做外掛來開發,在應用中,需要什麼功能,就下載什麼外掛,如一些面板主題類的功能,可以作為外掛功能來開發,使用者更換面板或者主題時,只需要下載和更新對應的外掛就行,如:桌面系統(不同的桌面主題),鎖屏(不同的鎖屏介面和風格)
4. 感覺還有好多好處,不一一列舉了........
以上概念就不親自去寫了,引用自Android動態載入,直接上乾貨,先從最簡單的DEMO開始。
DEMO實現:
首先要搞清楚原理和實現方法,眾所周知,Android程式實際是執行在 Dalvik/ART虛擬機器上的,而Dalvik/ART虛擬機器執行其特有的檔案格式——dex位元組碼。DEX檔案通常在打包APK的過程中會生成(我們把一個APK檔案解壓也可以看到DEX檔案,對經常進行反編譯的同學來說,DEX檔案是基礎中的基礎),在APK構建出來之後,通常我們是無法去修改裡面的DEX檔案的,但是,我們可以通過載入外部(SD卡或者此應用files目錄)的DEX檔案,通過DexClassLoader和PathClassLoader這兩個類載入器載入DEX檔案中的類,通過反射(invoke)的方法來呼叫類裡的方法。而由外部載入的DEX檔案我們可以在不解除安裝、不重新安裝APK的情況下進行替換的,通過替換外部載入的DEX檔案,實現動態載入(更新)我們APK的目的。
分析完了原理,咱們就動手開始實踐,從一個最簡單的例項開始,該例項把需要動態載入的jar包放在assets目錄下,再把該JAR包通過程式碼copy到/data/user/0/包名/files/目錄下,最後通過DexClassLoader去動態載入這個外部的DEX檔案。
首先我們要生成這個DEX檔案,生成步驟如下:
1. 隨便寫一個類並打包成JAR。在類裡寫一個方法,方便接下來展示效果。我這裡就寫了一個單例模式的toast方法,簡單粗暴,載入成功之後,效果是直接toast這一段話。
package com.example.dongtai; import android.content.Context; import android.widget.Toast; public class ToastUtil { private Context mContext; public ToastUtil(Context mContext){ this.mContext = mContext; } private static ToastUtil mInstance; public static ToastUtil getInstance(Context context){ if (mInstance == null) { synchronized (ToastUtil.class) { if (mInstance == null) { mInstance = new ToastUtil(context); } } } return mInstance; } public void showToast(){ Toast.makeText(mContext,"動態載入", 1000).show(); } }
2. 進入到SDK的 【build-tools\SDK版本號】 目錄下,(我的是C:\Users\zhhr\AppData\Local\Android\sdk\build-tools\25.0.1),輸入CMD進入到控制檯,再把我們需要動態載入的JAR包copy進去,輸入命令 【dx--dex --output=需要生成的DEX名 原始JAR包名】 (我的命令是(dx --dex--output=dongtai_dex.jar dongtai.jar)),之後,我們就拿到了由這個JAR包編譯來的DEX檔案。如下圖:
3. 新建一個工程,這個工程就是我們需要動態載入的工程,把我們上一步生成的DEX檔案copy進main目錄下的assets資料夾裡,如下圖:
4. 開始進行動態載入,程式碼如下:
package com.example.zhhr.dynamicloaddexdemo;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import dalvik.system.DexClassLoader;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.Log;
public class LoadUtil {
private DexClassLoader mDexClassLoader;
private Class<?> mToastClass;
private Object mInstanceObject;
private Method getInstanceMethod,showToastMethod;
private final String mOriginPath = "dongtai_dex.jar";//assets的原始檔案
private final String mOutPath = "/dongtai_dex.jar";//file目錄下的檔案
private static LoadUtil mInstance;
private Context mContext;
public LoadUtil(Context context){
mContext = context;
}
/**
* 獲取型別例項
* @return
*/
public static LoadUtil getInstance(Context context){
if (mInstance == null) {
synchronized (LoadUtil.class) {
if (mInstance == null) {
mInstance = new LoadUtil(context);
}
}
}
return mInstance;
}
/**
* 初始化
*/
@SuppressLint("NewApi") public boolean init() {
try {
descryptFile(mContext);
String destFilePath = mContext.getFilesDir().getAbsolutePath() + mOutPath;
File opFile = new File(destFilePath);
Log.d("zhhr", opFile.getAbsolutePath());
if (!opFile.exists()) {
return false;
}
//首先獲取例項
mDexClassLoader = new DexClassLoader(opFile.toString()
, mContext.getFilesDir().getAbsolutePath()
, null
, ClassLoader.getSystemClassLoader().getParent());
//載入其中的類
mToastClass = mDexClassLoader.loadClass("com.example.dongtai.ToastUtil");
//獲取到單例模式的方法
getInstanceMethod = mToastClass.getMethod("getInstance",Context.class);
showToastMethod = mToastClass.getMethod("showToast");
//獲取例項物件
mInstanceObject = getInstanceMethod.invoke(mToastClass,mContext);
//執行showTosat方法
showToastMethod.invoke(mInstanceObject);
} catch (Exception e) {
Log.d("zhhr", e.toString());
return false;
}
return true;
}
/**
* 將assets目錄下的資源拷貝到file目錄下
* @param context
* @throws IOException
*/
private boolean descryptFile(Context context) throws IOException{
File destFile = new File(context.getFilesDir().getAbsolutePath() + mOutPath);
if (destFile.exists()) {
long s = 0;
FileInputStream fis = null;
fis = new FileInputStream(destFile);
s = fis.available();
if (s > 20) {// 檔案大小,方式重複拷貝dex檔案,20是隨便給的
return true;
}
}
InputStream assetsFileInputStream = null;
try {
assetsFileInputStream = context.getAssets().open(mOriginPath);
} catch (Exception e) {
e.printStackTrace();
}
if (!destFile.getParentFile().exists()) {
destFile.getParentFile().mkdirs();
}
destFile.createNewFile();
FileOutputStream fos = new FileOutputStream(destFile);
int readNum = 0;
while((readNum = assetsFileInputStream.read()) != -1){
fos.write(readNum);
}
fos.close();
assetsFileInputStream.close();
return true;
}
}
4.1 我這裡用最精簡的程式碼實現了動態載入,首先是通過descryptFile這個方法把assets目錄下的檔案copy到/data/user/0/com.example.zhhr.dynamicloaddexdemo/files/目錄下,再通過DexClassLoader這個類去載入到該目錄下的DEX檔案。
DexClassLoader初始化時引數意思如下:
· 第一個引數指的是我們要載入的 dex 檔案的路徑,它有可能是多個 dex 路徑,取決於我們要載入的 dex 檔案的個數,多個路徑之間用 :隔開。
· 第二個引數指的是優化後的 dex 存放目錄。實際上,dex 其實還並不能被虛擬機器直接載入,它需要系統的優化工具優化後才能真正被利用。優化之後的 dex 檔案我們把它叫做 odex (optimizeddex,說明這是被優化後的 dex)檔案。其實從 class 到 dex 也算是經歷了一次優化,這種優化的是機器無關的優化,也就是說不管將來執行在什麼機器上,這種優化都是遵循固定模式的,因此這種優化發生在 apk 編譯。而從 dex 檔案到odex 檔案,是機器相關的優化,它使得 odex 適配於特定的硬體環境,不同機器這一步的優化可能有所不同,所以這一步需要在應用安裝等執行時期由機器來完成。需要注意的是,在較早版本的系統中,這個目錄可以指定為外部儲存中的目錄,較新版本的系統為了安全只允許其為應用程式私有儲存空間(/data/data/apk-package-name/)下的目錄,一般我們可以通過 Context#getDir(StringdirName)得到這個目錄。
· 第三個引數的意義是庫檔案的的搜尋路徑,一般來說是 .so庫檔案的路徑,也可以指明多個路徑。
· 第四個引數就是要傳入的父載入器,一般情況我們可以通過 Context#getClassLoader()得到應用程式的類載入器然後把它傳進去。
這裡只介紹類的使用方法,原始碼的研究可以參考文章:
4.2 得到DexClassLoader例項之後,再通過loadclass(類名)方法得到需要呼叫的類名。
4.3 再通過getMethod方法得到需要需要使用的方法
· 第一個引數指的是需要呼叫的方法名
· 第二個引數指定了需要反射呼叫的方法中的引數型別,我動態載入的類中,getInstance方法需要傳入一個Context型別的引數,所以第二個引數指定為 Context.class,如果有多個引數,則用陣列的形式,如需要傳入上下文和字串,則第二個引數為 new Class[]{Context.class,String.class})
4.4 獲取到需要使用的方法之後,再通過invoke方法反射呼叫。
· 第一個引數是呼叫該方法的物件
· 第二個至第N個引數是該反射方法需要傳入的引數,如不需要傳入引數,則可以不填寫
程式碼就介紹到這裡,通過先反射getinstance方法,拿到例項之後,再呼叫 showToast方法,既可以實現動態載入。
以上的程式碼比較簡單粗暴,已經上傳到GITHUB,是最簡單的動態載入的實現。接下來我會繼續更新動態載入的一些進階文章。