1. 程式人生 > >Android外掛化學習之路(二)之ClassLoader完全解析

Android外掛化學習之路(二)之ClassLoader完全解析

Java程式碼都是寫在Class裡面的,程式執行在虛擬機器上時,虛擬機器需要把需要的Class載入進來才能建立例項物件並工作,而完成這一個載入工作的角色就是ClassLoader。

類載入器ClassLoader介紹

Android的Dalvik/ART虛擬機器如同標準JAVA的JVM虛擬機器一樣,在執行程式時首先需要將對應的類載入到記憶體中。因此,我們可以利用這一點,在程式執行時手動載入Class,從而達到程式碼動態載入可執行檔案的目的。Android的Dalvik/ART虛擬機器雖然與標準Java的JVM虛擬機器不一樣,ClassLoader具體的載入細節不一樣,但是工作機制是類似的,也就是說在Android中同樣可以採用類似的動態載入外掛的功能,只是在Android應用中動態載入一個外掛的工作要比Eclipse載入一個外掛複雜許多(這點後面在解釋說明)。

ClassLoader例項有多個

動態載入的基礎是ClassLoader,從名字也可以看出,ClassLoader就是專門用來處理類載入工作的,所以這貨也叫類載入器,而且一個執行中的APP 不僅只有一個類載入器。

其實,在Android系統啟動的時候會建立一個Boot型別的ClassLoader例項,用於載入一些系統Framework層級需要的類,我們的Android應用裡也需要用到一些系統的類,所以APP啟動的時候也會把這個Boot型別的ClassLoader傳進來。

此外,APP也有自己的類,這些類儲存在APK的dex檔案裡面,所以APP啟動的時候,也會建立一個自己的ClassLoader例項,用於載入自己dex檔案中的類。

@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()); } } }
[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 : [email protected]14af4e32

可以看見有2個Classloader例項,一個是BootClassLoader(系統啟動的時候建立的),另一個是PathClassLoader(應用啟動時建立的,用於載入“/data/app/me.kaede.anroidclassloadersample-1/base.apk”裡面的類)。由此也可以看出,一個執行的Android應用至少有2個ClassLoader。

建立自己ClassLoader例項
動態載入外部的dex檔案的時候,我們也可以使用自己建立的ClassLoader例項來載入dex裡面的Class,不過ClassLoader的建立方式有點特殊,我們先看看它的構造方法

/*      * 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例項的時候,需要使用一個現有的ClassLoader例項作為新建立的例項的Parent。這樣一來,一個Android應用,甚至整個Android系統裡所有的ClassLoader例項都會被一棵樹關聯起來,這也是ClassLoader的 雙親代理模型(Parent-Delegation Model)的特點。

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;

從原始碼中我們也可以看出,loadClass方法在載入一個類的例項的時候
1) 會先查詢當前ClassLoader例項是否載入過此類,有就返回;

2) 如果沒有。查詢Parent是否已經載入過此類,如果已經載入過,就直接返回Parent載入的類;

3) 如果繼承路線上的ClassLoader都沒有載入,才由Child執行類的載入工作;

這樣做有個明顯的特點,如果一個類被位於樹根的ClassLoader載入過,那麼在以後整個系統的生命週期內,這個類永遠不會被重新載入。

使用ClassLoader需要注意的問題

如果你希望通過動態載入的方式,載入一個新版本的dex檔案,使用裡面的新類替換原有的舊類,從而修復原有類的BUG,那麼你必須保證在載入新類的時候,舊類還沒有被載入,因為如果已經載入過舊類,那麼ClassLoader會一直優先使用舊類。

如果舊類總是優先於新類被載入,我們也可以使用一個與載入舊類的ClassLoader沒有樹的繼承關係的另一個ClassLoader來載入新類,因為ClassLoader只會檢查其Parent有沒有載入過當前要載入的類,如果兩個ClassLoader沒有繼承關係,那麼舊類和新類都能被載入。
不過這樣一來又有另一個問題了,在Java中,只有當兩個例項的類名、包名以及載入其的ClassLoader都相同,才會被認為是同一種類型。上面分別載入的新類和舊類,雖然包名和類名都完全一樣,但是由於載入的ClassLoader不同,所以並不是同一種類型,在實際使用中可能會出現型別不符異常。

同一個Class = 相同的 ClassName + PackageName + ClassLoader

這個在採用動態載入功能的開發中容易出現,請注意。

DexClassLoader與PathClassLoader

平時開發的時候,使用DexClassLoader就夠用了,但是我們不妨挖一下這兩者具體細節上的區別。

class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
} 
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做了一下封裝,具體的實現還是在父類裡。不過這裡也可以看出,PathClassLoader的optimizedDirectory只能是null,進去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檔案的,並建立一個DexFile物件,如果它為null,那麼會直接使用dex檔案原有的路徑來建立DexFile物件。

optimizedDirectory必須是一個內部儲存路徑,還記得我們之前說過的,無論哪種動態載入,載入的可執行檔案一定要存放在內部儲存。DexClassLoader可以指定自己的optimizedDirectory,所以它可以載入外部的dex,因為這個dex會被複制到內部路徑的optimizedDirectory;而PathClassLoader沒有optimizedDirectory,所以它只能載入內部的dex,這些大都是存在系統中已經安裝過的apk裡面的。

載入類的過程

上面還只是建立了類載入器的例項,其中建立了一個DexFile例項,用來儲存dex檔案,我們猜想這個例項就是用來載入類的。
Android中,ClassLoader用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;
    }

loadClass方法呼叫了findClass方法,而BaseDexClassLoader過載了這個方法,得到BaseDexClassLoader看看

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    Class clazz = pathList.findClass(name);
    if (clazz == null) {
        throw new ClassNotFoundException(name);
    }
    return clazz;
}

結果還是呼叫了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例項,其實也就是遍歷了所有載入過的dex檔案,再呼叫loadClassBinaryName方法一個個嘗試能不能載入想要的類,真是簡單粗暴

public Class loadClassBinaryName(String name, ClassLoader loader) {
        return defineClass(name, loader, mCookie);
    }

    private native static Class defineClass(String name, ClassLoader loader, int cookie);

看到這裡想必大家都明白了,loadClassBinaryName中呼叫了Native方法defineClass載入類。

至此,ClassLoader的建立和載入類的過程的完成了。有趣的是,標準JVM中,ClassLoader是用defineClass載入類的,而Android中defineClass被棄用了,改用了loadClass方法,而且載入類的過程也挪到了DexFile中,在DexFile中載入類的具體方法也叫defineClass,不知道是Google故意寫成這樣的還是巧合。

**

Android程式比起使用動態載入時麻煩在哪裡

**
通過上面的分析,我們知道使用ClassLoader動態載入一個外部的類是非常容易的事情,所以很容易就能實現動態載入新的可執行程式碼的功能,但是比起一般的Java程式,在Android程式中使用動態載入主要有兩個麻煩的問題:

1) Android中許多元件類(如Activity、Service等)是需要在Manifest檔案裡面註冊後才能工作的(系統會檢查該元件有沒有註冊),所以即使動態載入了一個新的元件類進來,沒有註冊的話還是無法工作;

2) Res資源是Android開發中經常用到的,而Android是把這些資源用對應的R.id註冊好,執行時通過這些ID從Resource例項中獲取對應的資源。如果是執行時動態載入進來的新類,那類裡面用到R.id的地方將會丟擲找不到資源或者用錯資源的異常,因為新類的資源ID根本和現有的Resource例項中儲存的資源ID對不上;

相關推薦

Android外掛學習ClassLoader完全解析

Java程式碼都是寫在Class裡面的,程式執行在虛擬機器上時,虛擬機器需要把需要的Class載入進來才能建立例項物件並工作,而完成這一個載入工作的角色就是ClassLoader。 類載入器ClassLoader介紹 Android的Dalvik/ART虛擬

XLua 學習C#訪問Lua

       這裡指的是C#主動發起對Lua資料結構的訪問。從Lua支援的資料型別來講,C#獲取Lua元素主要分為:基本資料型別(number,bool,string),複雜的資料型別(table),函式(function)。        本次測試的待獲取的L

Android外掛學習使用外掛中的R資源

res裡的每一個資源都會在R.java裡生成一個對應的Integer型別的id,APP啟動時會先把R.java註冊到當前的上下文環境,我們在程式碼裡以R檔案的方式使用資源時正是通過使用這些id訪問res資源,然而外掛的R.java並沒有註冊到當前的上下文環境,所

Android外掛學習動態建立Activity

靜態代理Activity模式的限制 我們在代理Activity模式一文裡談到啟動外掛APK裡的Activity的兩個難題嗎,由於外掛裡的Activity沒在主專案的Manifest裡面註冊,所以無法經歷系統Framework層級的一系列初始化過程,最終導致獲得

Android外掛學習動態載入綜述

前段時間,公司專案完成了外掛化的開發,自己也因此學習了很多Android外掛化的知識,於是想把這些內容記錄下來,本次帶來Android外掛化的第一篇:動態載入綜述 背景知識 1.什麼是動態載入? 動態載入技術應由以下幾個部分組成: 1) 應用在執行

Android外掛學習代理Activity

簡單模式中,使用ClassLoader載入外部的Dex或Apk檔案,可以載入一些本地APP不存在的類,從而執行一些新的程式碼邏輯。但是使用這種方法卻不能直接啟動外掛裡的Activity。 啟動外掛中Activity的兩個主要問題 Activity等元件是

資料視覺神器matplotlib學習

之前學習了matplotlib的一些基本畫圖方法(檢視上一節),這次主要是學習在圖中加一些文字和其其它有趣的東西。 先來個最簡單的圖 from matplotlib import pyplot as plt import numpy as np x = np.linspace(-2, 2, 50)

Java學習流程控制語句

循環 cas 學習之路 將不 乘法表 length 跳出循環 spa int if、if…else…語句 if (true) { System.out.println("為真時執行");

python學習 -- 函數、JSON、終端樣式

blog ade def 數量 通過 等於 name tuple args 函數 函數構成 定義函數:使用def即可 def __getName(idCard): return user_info[idCard].Name 其中,__get

Python 學習

在外 封裝 過程 數列 == 3.6 開頭 res form Python 學習之路(二) 以下所用的是Python 3.6 一、條件語句 簡單判斷 1 if 判斷條件: 2 執行語句…… 3 else: 4 執行語句…… 復雜判斷 1 if 判斷

Hadoop學習Hadoop發展背景

chukwa 站點 avro azkaban das 可擴展性 對數 就是 pro Hadoop產生的背景 1. HADOOP最早起源於Nutch。Nutch的設計目標是構建一個大型的全網搜索引擎,包括網頁抓取、索引、查詢等功能,但隨著抓取網頁數量的增加,遇到了嚴重的可擴

C++再學習

iter pointer lin clas 數組元素 對數 表達 自增 條件操作符 1. 移位操作符“ << ”和“ >> ”擁有中等優先級:其優先級比算術操作符低,但比關系操作符、賦值操作符和條件操作符優先級高 2. *iter++   後自增操作

HBase學習 HBase集群安裝

star java_home 服務 blog usr mirrors logs 技術 ron 前提 1、HBase 依賴於 HDFS 做底層的數據存儲 2、HBase 依賴於 MapReduce 做數據計算 3、HBase 依賴於 ZooKeeper 做服務協調 4

Hive學習 Hive安裝

different 0.10 director lar blog cut cti mysql extend Hive的下載 下載地址http://mirrors.hust.edu.cn/apache/ 選擇合適的Hive版本進行下載,進到stable-2文件夾可以看到穩

Spark學習 Spark2.3 HA集群的分布式安裝

serve html 元數據 不安裝 rec ive cut 再次 apps 一、下載Spark安裝包 1、從官網下載 http://spark.apache.org/downloads.html 2、從微軟的鏡像站下載 http://mirrors.hust.

學習淺談:bash及其特性,命令歷史以及用戶管理及權限,shell的類型

bash 管理權限 過了一周了,進度似乎有點懈怠,不過過了周末重整旗鼓啦shell(外殼)GUI:Gnome,KDE,xfceCLI:sh,csh,ksh,bashbash(父進程)-----bash(子進程)他們相互獨立彼此不知命令歷史:historybash支持的引號:‘ ’命令替換(鍵盤~的按鍵

小強的Hadoop學習

com TE 區別 截斷 用戶 分開 路徑問題 登陸用戶 學習 接著第一遍。中間間隔了大約半年的時間了,話不多說,直接進入主題。 這篇是主要是應用篇。目前的環境是4臺機器 ,環境 centos 7.2 CDH5.10.2 網上很多安裝教程,這邊就不說明了。 Hive+

XML學習

req 方式 test 無符號 規範 內容 了解 實體 文本 DTD 一、什麽是DTD? DTD即Document Type Definition,文檔類型定義。 我們知道,XML的標簽可以自定義,不受任何約束。但有時侯,為了符合邏輯和業務需要,我們需要對XML文檔加以約束

Linux 學習:使用者及許可權詳解

作業: 1.ls 命令是否可以顯示某目錄的整體大小,即包括其內部的所有檔案的整體大小? 可以,使用ls -s xxx 2.通過幫助手冊,學習使用du命令: # du 估計檔案空間使用量 ​ -s 分割資料夾,不包括子目錄大小 ​ -h 以可讀格式展示

Tecnomatix Plant Simulation 14 學習

  本篇部落格主要介紹基礎遺傳演算法的實現,例子參考部落格一推薦的周金平老師教材第三章,這裡做簡單介紹   問題描述: 將n個裝置(M1,…Mn)放置到n個位置(A,B…)上,其中每個位置上能且僅能放置一臺裝置,已知n個裝置兩兩之間的物料搬運量大小W(i, j),以及