1. 程式人生 > >【Android】動態載入實現的簡單demo

【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,是最簡單的動態載入的實現。接下來我會繼續更新動態載入的一些進階文章。