Android 動態(tài)加載機制基礎(chǔ)-ClassLoader

本文僅為學(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

DexClassLoader構(gòu)造函數(shù)

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

dexPath: 包含資源和class文件的apk/jar;
optimizedDirectory: dex文件的存儲路徑薪丁;
libraryPath:native library的位置遇西;
parent: 父ClassLoader;

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市窥突,隨后出現(xiàn)的幾起案子努溃,更是在濱河造成了極大的恐慌,老刑警劉巖阻问,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件梧税,死亡現(xiàn)場離奇詭異,居然都是意外死亡称近,警方通過查閱死者的電腦和手機第队,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來刨秆,“玉大人凳谦,你說我怎么就攤上這事『馕矗” “怎么了尸执?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長缓醋。 經(jīng)常有香客問我如失,道長,這世上最難降的妖魔是什么送粱? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任褪贵,我火速辦了婚禮,結(jié)果婚禮上抗俄,老公的妹妹穿的比我還像新娘脆丁。我一直安慰自己,他們只是感情好动雹,可當(dāng)我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布槽卫。 她就那樣靜靜地躺著,像睡著了一般胰蝠。 火紅的嫁衣襯著肌膚如雪歼培。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天姊氓,我揣著相機與錄音丐怯,去河邊找鬼。 笑死翔横,一個胖子當(dāng)著我的面吹牛读跷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播禾唁,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼效览,長吁一口氣:“原來是場噩夢啊……” “哼无切!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起丐枉,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤哆键,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后瘦锹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體籍嘹,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年弯院,在試婚紗的時候發(fā)現(xiàn)自己被綠了辱士。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡听绳,死狀恐怖颂碘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情椅挣,我是刑警寧澤头岔,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站鼠证,受9級特大地震影響峡竣,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜名惩,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一澎胡、第九天 我趴在偏房一處隱蔽的房頂上張望孕荠。 院中可真熱鬧娩鹉,春花似錦、人聲如沸稚伍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽个曙。三九已至锈嫩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間垦搬,已是汗流浹背呼寸。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留猴贰,地道東北人对雪。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像米绕,于是被迫代替她去往敵國和親瑟捣。 傳聞我的和親對象是個殘疾皇子馋艺,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內(nèi)容