Android外掛化原理和實踐 (二) 之 載入外掛中的類程式碼
我們在上一篇文章《Android外掛化原理和實踐 (一)之 外掛化簡介和基本原理簡述》中介紹了外掛化一些基本知識和歷史,最後還列出了三個根本問題。接下來我們打算圍繞著這三個根本問題展開對外掛化的學習。首先本章將介紹第一個根本問題:宿主和外掛中如何相互呼叫程式碼。要實現它們相互呼叫,就得要宿主先將外掛載入起來。Android中要想從載入外部外掛就在於ClassLoader。
1 初識PathClassLoader和DexClassLoader
ClassLoader下有子類BaseDexClassLoader,BaseDexClassLoader又有兩個重要的子類:PathClassLoader和DexClassLoader。先來看看PathClassLoader和DexClassLoader的原始碼:
PathClassLoader.java
/** * Provides a simple {@link ClassLoader} implementation that operates on a list * of files and directories in the local file system, but does not attempt to * load classes from the network. Android uses this class for its system class * loader and for its application class loader(s). */ public class PathClassLoader extends BaseDexClassLoader { /** * Creates a {@code PathClassLoader} that operates on a given list of files * and directories. This method is equivalent to calling * {@link #PathClassLoader(String, String, ClassLoader)} with a * {@code null} value for the second argument (see description there). * * @param dexPath the list of jar/apk files containing classes and * resources, delimited by {@code File.pathSeparator}, which * defaults to {@code ":"} on Android * @param parent the parent class loader */ public PathClassLoader(String dexPath, ClassLoader parent) { super(dexPath, null, null, parent); } /** * Creates a {@code PathClassLoader} that operates on two given * lists of files and directories. The entries of the first list * should be one of the following: * * <ul> * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as * well as arbitrary resources. * <li>Raw ".dex" files (not inside a zip file). * </ul> * * The entries of the second list should be directories containing * native library files. * * @param dexPath the list of jar/apk files containing classes and * resources, delimited by {@code File.pathSeparator}, which * defaults to {@code ":"} on Android * @param libraryPath the list of directories containing native * libraries, delimited by {@code File.pathSeparator}; may be * {@code null} * @param parent the parent class loader */ public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) { super(dexPath, null, libraryPath, parent); } }
DexClassLoader.java
/** * A class loader that loads classes from {@code .jar} and {@code .apk} files * containing a {@code classes.dex} entry. This can be used to execute code not * installed as part of an application. * * <p>This class loader requires an application-private, writable directory to * cache optimized classes. Use {@code Context.getDir(String, int)} to create * such a directory: <pre> {@code * File dexOutputDir = context.getDir("dex", 0); * }</pre> * * <p><strong>Do not cache optimized classes on external storage.</strong> * External storage does not provide access controls necessary to protect your * application from code injection attacks. */ public class DexClassLoader extends BaseDexClassLoader { /** * Creates a {@code DexClassLoader} that finds interpreted and native * code. Interpreted classes are found in a set of DEX files contained * in Jar or APK files. * * <p>The path lists are separated using the character specified by the * {@code path.separator} system property, which defaults to {@code :}. * * @param dexPath the list of jar/apk files containing classes and * resources, delimited by {@code File.pathSeparator}, which * defaults to {@code ":"} on Android * @param optimizedDirectory directory where optimized dex files * should be written; must not be {@code null} * @param libraryPath the list of directories containing native * libraries, delimited by {@code File.pathSeparator}; may be * {@code null} * @param parent the parent class loader */ public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), libraryPath, parent); } }
PathClassLoader和DexClassLoader這兩個類很簡單,區別僅在於建構函式的第2個引數optimizedDirectory,其中PathClassLoader把這個引數設定為null。我們來翻譯一下PathClassLoader的頭部註釋,大概意思是:PathClassLoader用於載入本地檔案系統上的檔案和目錄,它不能從網路上載入,Android使用這個類來載入系統類及應用程式。再來翻譯一下DexClassLoader的頭部註釋,大概意思是:DexClassLoader用於載入來自外部jar、apk包含的dex目錄。
說到載入dex,是不是想起了為了解決65535問題,而使用了multidex來將一個apk檔案內的dex進行拆分。沒錯,其實在使用這個過程中,主的classes.dex是由App使用PathClassLoader進行載入的,而其餘的classes2.dex這些子dex都會以資源的形式在App啟動後使用DexClassLoader進行載入到ClassLoader中的。
接著再來翻譯一下DexClassLoader的建構函式中引數的意思:
dexPath |
接收一個jar或apk檔案列表,多個的話,由File.pathSeparator分隔,在Android中預設使用”:”分隔 |
optimizedDirectory |
dex檔案被載入後會被編譯器優化,優化之後的dex存放路徑,不能為空。要注意的是,它需要一個應用私有的可寫的一個路徑,以防止應用被注入攻擊,例子如: File dexOutputDir = context.getDir(“dex”, 0); |
libraryPath |
包含libraries的目錄列表,同樣用File.pathSeparator分割,可為空,傳入null |
parent |
父類的class loader |
2 載入外掛中的類
好了,我們已經清楚了DexClassLoader是用來載入外部apk的dex,而且也瞭解了它的建構函式的呼叫了,那麼現在就來通過一個Demo來看看它是否真的可以載入外部apk的dex吧。Demo工程目錄如下所示,存在兩個applicaton模組,一個是宿主Host,一個是外掛Plugin。
Plugin外掛中僅有一個TestBean.java類:
package com.zyx.plugin;
public class TestBean {
private String mName = "子云心";
public void setName(String name) {
mName = name;
}
public String getName() {
return mName;
}
}
Host的MainActivity中編寫所有的邏輯程式碼:
package com.zyx.plugindemo;
import android.app.Activity;
import android.content.Context;
import android.content.res.AssetManager;
import android.os.Bundle;
import android.widget.Toast;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import dalvik.system.DexClassLoader;
public class MainActivity extends Activity {
private final static String sApkName = "Plugin-debug.apk";
private DexClassLoader mClassLoader;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
simulationDownload(this, sApkName);
mClassLoader = loadPlugin(this, sApkName);
onDo();
}
/**
* 載入外掛
* @param context
* @param apkName
* @return
*/
private DexClassLoader loadPlugin(Context context, String apkName) {
File extractFile = context.getFileStreamPath(apkName);
String dexpath = extractFile.getPath();
File fileRelease = context.getDir("dex", 0);
String absolutePath = fileRelease.getAbsolutePath();
DexClassLoader classLoader = new DexClassLoader(dexpath, absolutePath, null, context.getClassLoader());
return classLoader;
}
/**
* 執行外掛程式碼
*/
private void onDo() {
try {
Class mLoadClassBean = mClassLoader.loadClass("com.zyx.plugin.TestBean");
Object testBeanObject = mLoadClassBean.newInstance();
Method getNameMethod = mLoadClassBean.getMethod("getName");
getNameMethod.setAccessible(true);
String name = (String) getNameMethod.invoke(testBeanObject);
Toast.makeText(getApplicationContext(), name, Toast.LENGTH_LONG).show();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 模擬下載,實際上是將Assets中的外掛apk檔案複製到/data/data/files 目錄下
* @param context
* @param sourceName
*/
private void simulationDownload(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 {
try {
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
上述程式碼中,simulationDownload是一個測試方法程式碼,為了方便開發驗證,我們將Plugin通過編譯後生成的Plugin-debug.apk檔案放在了Host的assets目錄下。然後再通過simulationDownload方法將Plugin-debug.apk檔案轉存到/data/data/files 目錄下來模擬真實開發時的下載邏輯。
我們將重點放回在loadPlugin和onDo兩個方法上,loadPlugin方法中通過獲取Plugin-debug.apk檔案的目錄資訊來構造了一個DexClassLoader物件,然後在onDo方法中,通過這個物件使用了反射來讀到Plugin外掛中的TestBean類。App執行後,會彈出一個Toast,內容就是TestBean中的mName值。
你以為外掛化中載入外掛程式碼就是這麼簡單嗎?你錯了,上面的Demo只能使宿主呼叫外掛中的普通類,而實際上並不能使宿主調起外掛中的四大元件。即使你在宿主中的AndroidMainifest.xml中聲明瞭元件資訊也是不行。可以嘗試在外掛中建立一個TestService服務,並在宿主中的AndroidMainifest.xml中宣告它,然後在宿主中通過startService來啟動服務,最後再次執行App後就會報出以下的異常:
至於為什麼?和解決方法是怎樣?我們留在下一篇文章來介紹。