Android 動態加載之DexClassLoader

Android提供動態加載機制,允許從SD卡中加載dex格式的文件,其中,DexClassLoader類起了關鍵作用。
首先看下Android Developer關於DexClassLoader的介紹,
    A class loader that loads classes from .jar and .apk files containing a classes.dex entry. This can be used to execute code not installed as part of an application.
This class loader requires an application-private, writable directory to cache optimized classes. Use Context.getCodeCacheDir() to create such a directory:
File dexOutputDir = context.getCodeCacheDir();
Do not cache optimized classes on external storage. External storage does not provide access controls necessary to protect your application from code injection attacks.
簡單翻譯一下,DexClassLoader是一個加載.jar或.apk文件的類加載器,其中該文件是dex格式。其可以用來執行程序之外的其他代碼。該加載器需要一個程序私有的文件,Google推薦使用context.getCodeChaceDir()目錄來使用。
    我們來看下其構造函數,   

public DexClassLoader (String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)

Added in API level 3

Creates a DexClassLoader that finds interpreted and native code. Interpreted classes are found in a set of DEX files contained in Jar or APK files.

The path lists are separated using the character specified by the path.separator system property, which defaults to :.

Parameters
dexPath the list of jar/apk files containing classes and resources, delimited by File.pathSeparator, which defaults to ":" on Android
optimizedDirectory directory where optimized dex files should be written; must not be null
libraryPath the list of directories containing native libraries, delimited by File.pathSeparator; may be null
parent the parent class loader

其中,dexPath指定了要加載的文件目錄;optimizedDirectory是優化文件存放的路徑,也就是上面提到的應該是程序私有的路徑(其他程序無法訪問的),libraryPath包含的native庫路徑,parent,指定父加載器。

該類除了構造器之外,沒有其他方法,我們看下其繼承關係。
java.lang.Object
    java.lang.ClassLoader
     ↳ dalvik.system.BaseDexClassLoader
       ↳ dalvik.system.DexClassLoader
看到,其繼承了BaseDexClassLoader類,BaseDexClassLoader繼承了ClassLoaer類,我們找到其源碼,進行分析,
1. DexClassLoader類只有一個構造函數,將4個構造參數傳給BaseDexClassLoader,接下來分析BaseDexClassLoader,
2. BaseDexClassLoader有兩個成員變量,String originalPath和DexPathList pathList;看其註釋,originalPath指的是其文件路徑,pathlist是路徑元素列表(原文是,structured lists of path elements,下文會分析),BaseDexClassLoader重載了ClassLoader父類的一些方法,我們注重分析findclass,其代碼如下,
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
 
if (clazz == null) {
throw new ClassNotFoundException(name);
}
 
return clazz;
}
然而其並沒有重載父類的loadClass方法(加載類時最主要用到的方法),我們接着分析ClassLoader類
3. ClassLoader
    其包含了很多方法,我們注重分析loadClass方法,
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
 
if (clazz == null) {
try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
// Don't want to see this.
}
 
if (clazz == null) {
clazz = findClass(className);
}
}
 
return clazz;
}
    我們可以看到,當loadClass時,會先判斷是否已加載過,若加載過,則不需要重新加載,若沒有加載過,則調用parent.loadClass方法,若其返回爲null(說明parent未加載成功),則調用findClass自己加載,上面的機制稱爲雙親委派機制。+
    好,接下來我們測試一下DexClassLoader能否成功加載外部代碼。

1. 生成外部加載代碼
    我們新建一個Android工程,其目錄如下,

其中,ILib定義了一個接口,關於定義接口的好處,下面會提到。
public interface ILib {
public void sayHello();
}
Lib實現了ILIb接口,在sayHello中輸出一句話
public class Lib implements ILib {
 
@Override
public void sayHello() {
// TODO Auto-generated method stub
System.out.println("Hello from Thrid Dex Lib");
}
 
}
然後,讓我們導出Lib類,選中Lib.java,右鍵export,選擇java-jar file導出,命名爲lib.jar,注意導出時,不要選擇接口文件(ILib),否則會出錯。
上面提到DexClassLoader加載的是Dex格式的文件,但是Eclipse導出的是jar格式,所以,我們得用Android的dx命令來將lib.jar編譯爲dex格式。dx命令位於Android sdk/build-tools/版本號/目錄,爲了方便可以將其路徑加入到系統變量中,然後在cmd中切換到lib.jar所在目錄,執行一下命令
dx --dex --output=mylib.jar lib.jar
得到mylib.jar文件

2. 動態加載
    得到test.jar文件後,我們將其放入到工程的aseet目錄,由於DexClassLoader是從SD卡中加載代碼的,所以我們在程序運行的時候,將test.jar從asset目錄copy到SD卡中,注意copy到的路徑必須是該程序私有的。
private void copy2SDCard(File dexInternalStoragePath) {
BufferedInputStream bis;
try {
bis = new BufferedInputStream(getAssets().open(LIB_JAR));
OutputStream dexWriter = new BufferedOutputStream(
new FileOutputStream(dexInternalStoragePath));
byte[] buf = new byte[BUF_SIZE];
int len;
while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
dexWriter.write(buf, 0, len);
}
dexWriter.close();
bis.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
用到的成員變量如下,
private String LIB_JAR = "mylib.jar";
static final int BUF_SIZE = 8 * 1024;
static final String TAG = "MCL";
重載DexClassLoader方法,
MyDexClassLoader.java
public class MyDexClassLoader extends DexClassLoader {
public final static String TAG = "MCL";
 
public MyDexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, optimizedDirectory, libraryPath, parent);
// TODO Auto-generated constructor stub
}
 
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
Log.i(TAG, "[CL] findCLass: " + name);
return super.findClass(name);
}
 
@Override
public String findLibrary(String name) {
// TODO Auto-generated method stub
Log.i(TAG, "[CL] findLibrary: " + name);
return super.findLibrary(name);
}
 
@Override
protected URL findResource(String name) {
// TODO Auto-generated method stub
Log.i(TAG, "[CL] findResource: " + name);
return super.findResource(name);
}
 
@Override
protected Enumeration<URL> findResources(String name) {
// TODO Auto-generated method stub
Log.i(TAG, "[CL] findResource: " + name);
return super.findResources(name);
}
 
@Override
protected synchronized Package getPackage(String name) {
// TODO Auto-generated method stub
Log.i(TAG, "[CL] getPackage: " + name);
return super.getPackage(name);
}
 
@Override
public String toString() {
// TODO Auto-generated method stub
return "MyDexClassLoader: - " + super.toString();
}
 
@Override
public URL getResource(String resName) {
// TODO Auto-generated method stub
Log.i(TAG, "[CL] getResource: " + resName);
return super.getResource(resName);
}
 
@Override
public Enumeration<URL> getResources(String resName) throws IOException {
// TODO Auto-generated method stub
Log.i(TAG, "[CL] getResource: " + resName);
return super.getResources(resName);
}
 
@Override
public InputStream getResourceAsStream(String resName) {
// TODO Auto-generated method stub
Log.i(TAG, "[CL] getResourceAsStream: " + resName);
return super.getResourceAsStream(resName);
}
 
@Override
public Class<?> loadClass(String className) throws ClassNotFoundException {
// TODO Auto-generated method stub
Log.i(TAG, "[CL] loadClass: " + className);
return super.loadClass(className);
}
 
@Override
protected Class<?> loadClass(String className, boolean resolve)
throws ClassNotFoundException {
// TODO Auto-generated method stub
// Log.i(TAG, String.format("[CL] loadClass: Name - %s, resolved - %b",
// className, resolve));
return super.loadClass(className, resolve);
}
 
@Override
protected Package[] getPackages() {
// TODO Auto-generated method stub
Log.i(TAG, "[CL] getPackages");
return super.getPackages();
}
 
@Override
protected Package definePackage(String name, String specTitle,
String specVersion, String specVendor, String implTitle,
String implVersion, String implVendor, URL sealBase)
throws IllegalArgumentException {
// TODO Auto-generated method stub
Log.i(TAG, "[CL] definePackage: " + name);
return super.definePackage(name, specTitle, specVersion, specVendor,
implTitle, implVersion, implVendor, sealBase);
}
 
@Override
public void setClassAssertionStatus(String cname, boolean enable) {
// TODO Auto-generated method stub
Log.i(TAG, String.format(
"[CL] setClassAssertionStatus: Name - %s, enable - %b", cname,
enable));
super.setClassAssertionStatus(cname, enable);
}
 
@Override
public void setPackageAssertionStatus(String pname, boolean enable) {
// TODO Auto-generated method stub
Log.i(TAG, String.format(
"[CL] setPackageAssertionStatus: Name - %s, enable - %b",
pname, enable));
super.setPackageAssertionStatus(pname, enable);
}
 
@Override
public void setDefaultAssertionStatus(boolean enable) {
// TODO Auto-generated method stub.
Log.i(TAG, "setDefaultAssertionStatus: " + enable);
super.setDefaultAssertionStatus(enable);
}
 
@Override
public void clearAssertionStatus() {
// TODO Auto-generated method stub
Log.i(TAG, "clearAssertionStatus");
super.clearAssertionStatus();
}
其只是簡單的重載了父類的方法,並輸出了log。
加載代碼如下,
private void testDynamicLoad() {
Log.i(TAG, "$$$$$$$$$$$$$$$This is my test$$$$$$$$$$$$$$$");
ClassLoader parentcl = getClassLoader();
File libFile = new File(getDir("dex", Context.MODE_PRIVATE), LIB_JAR);
copy2SDCard(libFile);
String dexLibPath = getDir("outdex", Context.MODE_PRIVATE)
.getAbsolutePath();
MyDexClassLoader cl = new MyDexClassLoader(libFile.getAbsolutePath(),
dexLibPath, null, parentcl);
try {
Class dtClz = cl.loadClass("paul.example.thirddex.Lib");
ILib lib = (ILib) dtClz.newInstance();
lib.sayHello();
 
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
 
}
MainActivity代碼如下,
public class MainActivity extends ActionBarActivity {
Button button;
private String LIB_JAR = "mylib.jar";
static final int BUF_SIZE = 8 * 1024;
static final String TAG = "MCL";
 
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = (Button) findViewById(R.id.button1);
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
testDynamicLoad();
}
});
}
 
@SuppressLint("NewApi")
private void testDynamicLoad() {
Log.i(TAG, "$$$$$$$$$$$$$$$This is my test$$$$$$$$$$$$$$$");
ClassLoader parentcl = getClassLoader();
File libFile = new File(getDir("dex", Context.MODE_PRIVATE), LIB_JAR);
copy2SDCard(libFile);
String dexLibPath = getDir("outdex", Context.MODE_PRIVATE)
.getAbsolutePath();
MyDexClassLoader cl = new MyDexClassLoader(libFile.getAbsolutePath(),
dexLibPath, null, parentcl);
try {
Class dtClz = cl.loadClass("paul.example.thirddex.Lib");
ILib lib = (ILib) dtClz.newInstance();
lib.sayHello();
 
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
 
}
 
private void copy2SDCard(File dexInternalStoragePath) {
BufferedInputStream bis;
try {
bis = new BufferedInputStream(getAssets().open(LIB_JAR));
OutputStream dexWriter = new BufferedOutputStream(
new FileOutputStream(dexInternalStoragePath));
byte[] buf = new byte[BUF_SIZE];
int len;
while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
dexWriter.write(buf, 0, len);
}
dexWriter.close();
bis.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
 
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
 
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
}

在運行程序之前,一定要先把Lib.java文件刪掉,然後運行,運行結果,

    
若工程中沒有刪除Lib.java造成的影響。則運行結果下圖,


分析:

若工程中沒有刪除Lib.java文件,則該應用的ClassLoader會包含該類,MyDexClassLoader加載Lib Class的時候,會先通過parent ClassLoader來加載該類,也就是該應用的ClassLoade加載,若Lib.java未刪除,則parent ClassLoader會加載成功。

若刪除了Lib.java文件,則parent ClassLoader未加載成功,會用MyDexClassLoader來加載。這也就解釋了上面兩個結果。

另外,若沒有定義接口,則在調用該類方法的時候,必須通過反射來調用,比較麻煩,而定義了接口,就可以轉化爲接口來調用。

有幾項需要注意,

1. 在打包java文件的時候,一定注意不要將接口文件打包進來

2. dexcalssloader的優化目錄一定要是程序私有目錄

3. 若該工程內包含Lib.java,則一定要刪除。也可以新建一個Android項目,包含Lib.java,然後在另一個項目中動態加載Lib。

工程下載地址

http://download.csdn.net/detail/u014088294/9251541

 
發佈了31 篇原創文章 · 獲贊 15 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章