本文僅為學(xué)習(xí)筆記很洋;不是原創(chuàng)文章;
動態(tài)加載的關(guān)鍵問題
ClassLoader機制
ClassLoader概念:Java代碼都是寫在Class里面的,程序運行在虛擬機上時褂策,虛擬機需要把需要的Class加載進(jìn)來才能創(chuàng)建實例對象并工作,而完成這一個加載工作的角色就是ClassLoader颓屑。
ClassLoader分類:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ClassLoader classLoader = getClassLoader();
if (classLoader != null){
Log.i(TAG, "[onCreate] classLoader " + i + " : " + classLoader.toString());
while (classLoader.getParent()!=null){
classLoader = classLoader.getParent();
Log.i(TAG,"[onCreate] classLoader " + i + " : " + classLoader.toString());
}
}
}
輸出結(jié)果為
[onCreate] classLoader 1 : dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/me.kaede.anroidclassloadersample-1/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]
[onCreate] classLoader 2 : java.lang.BootClassLoader@14af4e32
2個Classloader實例:
一個是BootClassLoader(系統(tǒng)啟動的時候創(chuàng)建的);
另一個是PathClassLoader (應(yīng)用啟動時創(chuàng)建的斤寂,用于加載“/data/app/me.kaede.anroidclassloadersample-1/base.apk”里面的類)。
創(chuàng)建ClassLoader: 需要使用一個現(xiàn)有的ClassLoader實例作為新創(chuàng)建的實例的Parent;復(fù)合ClassLoader的雙親委派機制(Parent-Delegation Model)的特點
/*
* constructor for the BootClassLoader which needs parent to be null.
*/
ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
if (parentLoader == null && !nullAllowed) {
throw new NullPointerException("parentLoader == null && !nullAllowed");
}
parent = parentLoader;
}
ClassLoader雙親代理模型加載類的特點和作用
JVM中ClassLoader通過defineClass方法加載jar里面的Class揪惦,而Android中這個方法被棄用了遍搞。
@Deprecated
protected final Class<?> defineClass(byte[] classRep, int offset, int length)
throws ClassFormatError {
throw new UnsupportedOperationException("can't load this type of class file");
}
取而代之的是loadClass方法
public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
1 會先查詢當(dāng)前ClassLoader實例是否加載過此類,有就返回器腋;
2 如果沒有溪猿。查詢Parent是否已經(jīng)加載過此類,如果已經(jīng)加載過纫塌,就直接返回Parent加載的類诊县;
3 如果繼承路線上的ClassLoader都沒有加載,才由Child執(zhí)行類的加載工作措左;
雙親委派模型優(yōu)勢:
共享功能 : 一些Framework層級的類一旦被頂層的ClassLoader加載過就緩存在內(nèi)存里面依痊,以后任何地方用到都不需要重新加載。
隔離功能:不同繼承路線上的ClassLoader加載的類肯定不是同一個類怎披,這樣的限制避免了用戶自己的代碼冒充核心類庫的類訪問核心類庫包可見成員的情況胸嘁;一些系統(tǒng)層級的類會在系統(tǒng)初始化的時候被加載,比如java.lang.String凉逛,如果在一個應(yīng)用里面能夠簡單地用自定義的String類把這個系統(tǒng)的String類給替換掉缴渊,那將會有嚴(yán)重的安全問題。
ClassLoader 隔離問題:
JVM 及 Dalvik 對類唯一的識別是 ClassLoader id + PackageName + ClassName鱼炒,所以一個運行程序中是有可能存在兩個包名和類名完全一致的類的衔沼。并且如果這兩個”類”不是由一個 ClassLoader 加載蝌借,是無法將一個類的示例強轉(zhuǎn)為另外一個類的,這就是 ClassLoader 隔離指蚁。 如 Android 中碰到如下異常菩佑;
同一個Class = 相同的 ClassName + PackageName + ClassLoader;
Java.lang.ClassCastException: android.support.v4.view.ViewPager can not be cast to android.support.v4.view.ViewPager
通過instance.getClass().getClassLoader()來判斷加載類的ClassLoader是否一致凝化;
DexClassLoader 和 PathClassLoader:(extends BaseDexClassLoader)
DexClassLoader :可以加載文件系統(tǒng)上的jar稍坯、dex、apk搓劫;可以從SD卡中加載未安裝的apk
PathClassLoader :可以加載/data/app目錄下的apk瞧哟,這也意味著,它只能加載已經(jīng)安裝的apk枪向;
URLClassLoader :可以加載java中的jar勤揩,但是由于dalvik不能直接識別jar,所以此方法在Android中無法使用秘蛔;
** DexClassLoader和PathClassLoader的區(qū)別在于PathClassLoader的optimizedDirectory指定為空**
// DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
// PathClassLoader.java
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
BaseDexClassLoader
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
DexPathList
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
……
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
}
private static Element[] makeDexElements(ArrayList<File> files,
File optimizedDirectory) {
ArrayList<Element> elements = new ArrayList<Element>();
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
dex = loadDexFile(file, optimizedDirectory);
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
zip = new ZipFile(file);
}
……
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}
/**
* Converts a dex/jar file path and an output directory to an
* output file path for an associated optimized dex file.
*/
private static String optimizedPathFor(File path,
File optimizedDirectory) {
String fileName = path.getName();
if (!fileName.endsWith(DEX_SUFFIX)) {
int lastDot = fileName.lastIndexOf(".");
if (lastDot < 0) {
fileName += DEX_SUFFIX;
} else {
StringBuilder sb = new StringBuilder(lastDot + 4);
sb.append(fileName, 0, lastDot);
sb.append(DEX_SUFFIX);
fileName = sb.toString();
}
}
File result = new File(optimizedDirectory, fileName);
return result.getPath();
}
optimizedDirectory是用來緩存我們需要加載的dex文件的陨亡,并創(chuàng)建一個DexFile對象,如果它為null深员,那么會直接使用dex文件原有的路徑來創(chuàng)建DexFile;
DexClassLoader可以指定自己的optimizedDirectory负蠕,所以它可以加載外部的dex,因為這個dex會被復(fù)制到內(nèi)部路徑的optimizedDirectory倦畅;而PathClassLoader沒有optimizedDirectory遮糖,所以它只能加載內(nèi)部的dex;
加載類的過程:
**第一步: **會先查詢當(dāng)前ClassLoader實例是否加載過此類叠赐,有就返回止吁;
**第二步: **如果沒有;查詢Parent是否已經(jīng)加載過此類燎悍,如果已經(jīng)加載過敬惦,就直接返回Parent加載的類;
public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
BaseDexClassLoader.findClass()
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
調(diào)用了DexPathList的findClass:遍歷了之前所有的DexFile實例谈山,其實也就是遍歷了所有加載過的dex文件俄删,再調(diào)用loadClassBinaryName方法一個個嘗試能不能加載想要的類
DexPathList.findClass()
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
DexFile.loadClassBinaryName()
public Class loadClassBinaryName(String name, ClassLoader loader) {
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);
動態(tài)加載的幾個關(guān)鍵問題
資源訪問:無法找到某某id所對應(yīng)的資源
因為將apk加載到宿主程序中去執(zhí)行,就無法通過宿主程序的Context去取到apk中的資源,比如圖片奏路、文本等畴椰,這是很好理解的,因為apk已經(jīng)不存在上下文了鸽粉,它執(zhí)行時所采用的上下文是宿主程序的上下文斜脂,用別人的Context是無法得到自己的資源的;
解決方案一:插件中的資源在宿主程序中也預(yù)置一份触机;
缺點:增加了宿主apk的大兄愦痢玷或;在這種模式下,每次發(fā)布一個插件都需要將資源復(fù)制到宿主程序中片任,這意味著每發(fā)布一個插件都要更新一下宿主程序偏友;
解決方案二:將插件中的資源解壓出來,然后通過文件流去讀取資源对供;
缺點:實際操作起來還是有很大難度的位他。首先不同資源有不同的文件流格式,比如圖片产场、XML等鹅髓,其次針對不同設(shè)備加載的資源可能是不一樣的,如何選擇合適的資源也是一個需要解決的問題京景;
實際解決方案:
Activity中有一個叫mBase的成員變量窿冯,它的類型就是ContextImpl。注意到Context中有如下兩個抽象方法醋粟,看起來是和資源有關(guān)的靡菇,實際上Context就是通過它們來獲取資源的重归。這兩個抽象方法的真正實現(xiàn)在ContextImpl中米愿;
/** Return an AssetManager instance for your application's package. */
public abstract AssetManager getAssets();
/** Return a Resources instance for your application's package. */
public abstract Resources getResources();
具體實現(xiàn)
protected void loadResources() {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, mDexPath);
mAssetManager = assetManager;
} catch (Exception e) {
e.printStackTrace();
}
Resources superRes = super.getResources();
mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),
superRes.getConfiguration());
mTheme = mResources.newTheme();
mTheme.setTo(super.getTheme());
}
加載資源的方法是通過反射,通過調(diào)用AssetManager中的addAssetPath方法鼻吮,我們可以將一個apk中的資源加載到Resources對象中育苟,由于addAssetPath是隱藏API我們無法直接調(diào)用,所以只能通過反射椎木。
addAssetPath();
@hide
public final int addAssetPath(String path) {
synchronized (this) {
int res = addAssetPathNative(path);
makeStringBlocks(mStringBlocks);
return res;
}
}
** Activity生命周期的管理:**
反射方式和接口方式违柏。
反射的方式很好理解,首先通過Java的反射去獲取Activity的各種生命周期方法香椎,比如onCreate漱竖、onStart、onResume等畜伐,然后在代理Activity中去調(diào)用插件Activity對應(yīng)的生命周期方法即可馍惹;
缺點:一方面是反射代碼寫起來比較復(fù)雜,另一方面是過多使用反射會有一定的性能開銷玛界。
反射方式
@Override
protected void onResume() {
super.onResume();
Method onResume = mActivityLifecircleMethods.get("onResume");
if (onResume != null) {
try {
onResume.invoke(mRemoteActivity, new Object[] { });
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
protected void onPause() {
Method onPause = mActivityLifecircleMethods.get("onPause");
if (onPause != null) {
try {
onPause.invoke(mRemoteActivity, new Object[] { });
} catch (Exception e) {
e.printStackTrace();
}
}
super.onPause();
}
接口方式
public interface DLPlugin {
public void onStart();
public void onRestart();
public void onActivityResult(int requestCode, int resultCode, Intent
data);
public void onResume();
public void onPause();
public void onStop();
public void onDestroy();
public void onCreate(Bundle savedInstanceState);
public void setProxy(Activity proxyActivity, String dexPath);
public void onSaveInstanceState(Bundle outState);
public void onNewIntent(Intent intent);
public void onRestoreInstanceState(Bundle savedInstanceState);
public boolean onTouchEvent(MotionEvent event);
public boolean onKeyUp(int keyCode, KeyEvent event);
public void onWindowAttributesChanged(LayoutParams params);
public void onWindowFocusChanged(boolean hasFocus);
public void onBackPressed();
…
}
代理Activity中只需要按如下方式即可調(diào)用插件Activity的生命周期方法万矾,這就完成了插件Activity的生命周期的管理;插件Activity需要實現(xiàn)DLPlugin接口;
@Override
protected void onStart() {
mRemoteActivity.onStart();
super.onStart();
}
@Override
protected void onRestart() {
mRemoteActivity.onRestart();
super.onRestart();
}
@Override
protected void onResume() {
mRemoteActivity.onResume();
super.onResume();
}
插件ClassLoader的管理
為了更好地對多插件進(jìn)行支持慎框,需要合理地去管理各個插件的DexClassLoader良狈,這樣同一個插件就可以采用同一個ClassLoader去加載類,從而避免了多個ClassLoader加載同一個類時所引發(fā)的類型轉(zhuǎn)換錯誤;通過將不同插件的ClassLoader存儲在一個HashMap中笨枯,這樣就可以保證不同插件中的類彼此互不干擾;
public class DLClassLoader extends DexClassLoader {
private static final String TAG = "DLClassLoader";
private static final HashMap<String, DLClassLoader> mPluginClassLoaders
= new HashMap<String, DLClassLoader>();
protected DLClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
super(dexPath, optimizedDirectory, libraryPath, parent);
}
/**
* return a available classloader which belongs to different apk
*/
public static DLClassLoader getClassLoader(String dexPath, Context
context, ClassLoader parentLoader) {
DLClassLoader dLClassLoader = mPluginClassLoaders.get(dexPath);
if (dLClassLoader != null)
return dLClassLoader;
File dexOutputDir = context.getDir("dex", Context.MODE_PRIVATE);
final String dexOutputPath = dexOutputDir.getAbsolutePath();
dLClassLoader = new DLClassLoader(dexPath, dexOutputPath, null,
parentLoader);
mPluginClassLoaders.put(dexPath, dLClassLoader);
return dLClassLoader;
}
}
DexClassLoader補充:
DexClassLoader構(gòu)造函數(shù)
DexClassLoader (String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)
dexPath: 包含資源和class文件的apk/jar;
optimizedDirectory: dex文件的存儲路徑薪丁;
libraryPath:native library的位置遇西;
parent: 父ClassLoader;