插件化類加載

插件化框架實(shí)現(xiàn):基于kotlin的插件化框架

Java類加載

  • 我們知道Java代碼通過編譯成class文件后旋圆,需要通過類加載機(jī)制加載到虛擬機(jī)后才能運(yùn)行

類加載機(jī)制

ClassLife.png

加載階段

  • 通過類的全限定名獲取二進(jìn)制字節(jié)流(可以來自磁盤,網(wǎng)絡(luò)等)笔呀,將字節(jié)流裝換為方法區(qū)的數(shù)據(jù)結(jié)構(gòu)镜粤,生成Class對(duì)象作為該類訪問入口

連接階段

  • 驗(yàn)證Class的字節(jié)流符合虛擬機(jī)規(guī)范捏题,為類變量分配內(nèi)存初始化默認(rèn)值,將常量池符號(hào)引用轉(zhuǎn)化為直接引用

初始化階段

  • 執(zhí)行類構(gòu)造器:靜態(tài)語句塊和類變量賦值動(dòng)作
  • 初始化的觸發(fā)時(shí)機(jī)是在遇到new肉渴、invokestatic公荧、反射、父類還沒初始化等操作時(shí)進(jìn)行

Java 類加載器

  • 啟動(dòng)類加載器(Bootstrap ClassLoader)

    負(fù)責(zé)將<JAVA_HOME>/lib目錄下的類庫(kù)加載到虛擬機(jī)內(nèi)存中

  • 擴(kuò)展類加載器(Extension ClassLoader)

    負(fù)責(zé)加載<JAVA_HOME>\lib\ext目錄下的類庫(kù)

  • 應(yīng)用程序類加載器(Application ClassLoader):一般程序的默認(rèn)類加載器

    ? 負(fù)責(zé)加載用戶類路徑(ClassPath)的類庫(kù)

類加載器的雙親委托模型

  • 如果一個(gè)類加載器收到了類的加載的請(qǐng)求同规,它首先不會(huì)自己去加載這個(gè)類循狰,而是把這個(gè)請(qǐng)求委托給父類加載器去完成,每一層都是如此券勺;因此所有的加載請(qǐng)求最終都應(yīng)該傳遞到頂層的啟動(dòng)類加載器中绪钥,只有當(dāng)父類加載器反饋無法完成這個(gè)加載請(qǐng)求(它的搜索范圍內(nèi)沒有找到所需的類)時(shí),子加載器才會(huì)嘗試去加載
  • 同時(shí)類加載方式也分為隱式加載(new等方式)和顯示加載Class.forname(xxx)

Android類加載

  • Android不是基于jvm虛擬機(jī)关炼,不能直接加載class字節(jié)碼程腹,需要將class字節(jié)碼轉(zhuǎn)換為dex字節(jié)碼

Android 類加載器

Android類加載器主要是DexClassLoader和PathClassLoader,兩者的區(qū)別是:

  • PathClassLoader是系統(tǒng)類加載器儒拂,同時(shí)也是默認(rèn)類加載寸潦,只能加載系統(tǒng)中已經(jīng)安裝過的apk

  • DexClassLoader可以加載apk/dex缀去,可以加載未安裝的apk

DexClassLoader版本差異

  • Android在API 9-13 和API 14以上DexClassLoader內(nèi)部持有dex文件的數(shù)據(jù)結(jié)構(gòu)不同,如果需要設(shè)配API 9-13則需要做不同處理甸祭,先來看一下數(shù)據(jù)結(jié)構(gòu)不同的地方:

API 9 - 13

DexClassLoader

public class DexClassLoader extends ClassLoader {
    private static final boolean VERBOSE_DEBUG = false;
    /* constructor args, held for init */
    private final String mRawDexPath;
    private final String mRawLibPath;
    private final String mDexOutputPath;
    /*
     * Parallel arrays for jar/apk files.
     *
     * (could stuff these into an object and have a single array;
     * improves clarity but adds overhead)
     */
    private final File[] mFiles;         // source file Files, for rsrc URLs
    private final ZipFile[] mZips;       // source zip files, with resources
    private final DexFile[] mDexs;       // opened, prepped DEX files
 
    // ....
}
  • 這里可以看到DexFile是直接以數(shù)組結(jié)構(gòu)存放在DexClassLoader類中

API > 13

DexClassLoader相關(guān)類

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    }
}
public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;
    
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
    }
    
    // ...
}
final class DexPathList {
    /** class definition context */
    private final ClassLoader definingContext;
    /**
     * List of dex/resource (class path) elements.
     * Should be called pathElements, but the Facebook app uses reflection
     * to modify 'dexElements' (http://b/7726934).
     */
    private Element[] dexElements;

    // ...
  
    static class Element {
        private final File dir;
        private final boolean isDirectory;
        private final File zip;
        private final DexFile dexFile;
        
        // ...
    }
}
  • 這里可以看到DexFile被層層封裝存放在BaseDexClassLoader的DexPathList中

ODEX過程

  • android 虛擬機(jī)并不是直接讀取dex文件的缕碎,在安裝apk的時(shí)候會(huì)做一次優(yōu)化,在這一過程池户,由虛擬機(jī)控制的一個(gè)verify選項(xiàng)咏雌,如果開啟會(huì)進(jìn)行一次校驗(yàn),如果某個(gè)類沒有引用其他dex中的類校焦,這個(gè)類會(huì)被打上CLASS_ISPREVERIFIED 的標(biāo)志赊抖。一旦被打上這個(gè)標(biāo)志,就無法再?gòu)钠渌?dex 中加載這個(gè)類了
  • 這個(gè)問題的比較簡(jiǎn)單的解決辦法是引用其他dex的類

App ClassLoader Hook點(diǎn)

  • 我們知道App啟動(dòng)會(huì)初始化Application并且調(diào)用onCreate寨典,這其實(shí)是在接受AMS啟動(dòng)信息后調(diào)用ActivityThread的handleBindApplication氛雪,函數(shù)過長(zhǎng),下面截取關(guān)鍵代碼:
private void handleBindApplication(AppBindData data) {
  
  // ...
  data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
  // ...
  
  final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
  // 下面代碼中也創(chuàng)建ContextImpl耸成,這里應(yīng)該需要先用到ContextImpl的信息报亩,這里
  
  
  // 創(chuàng)建ApplicationInfo 
  ApplicationInfo instrApp = new ApplicationInfo();
  instrApp.packageName = ii.packageName;
  instrApp.sourceDir = ii.sourceDir;
  instrApp.publicSourceDir = ii.publicSourceDir;
  instrApp.splitSourceDirs = ii.splitSourceDirs;
  instrApp.splitPublicSourceDirs = ii.splitPublicSourceDirs;
  instrApp.dataDir = ii.dataDir;
  instrApp.nativeLibraryDir = ii.nativeLibraryDir;
  // 獲取或創(chuàng)建LoadedApk
  LoadedApk pi = getPackageInfo(instrApp, data.compatInfo,
          appContext.getClassLoader(), false, true, false);
  ContextImpl instrContext = ContextImpl.createAppContext(this, pi);
  
  try {
      java.lang.ClassLoader cl = instrContext.getClassLoader();
      mInstrumentation = (Instrumentation)
          cl.loadClass(data.instrumentationName.getClassName()).newInstance();
  } catch (Exception e) {
      throw new RuntimeException(
          "Unable to instantiate instrumentation "
          + data.instrumentationName + ": " + e.toString(), e);
  }

  mInstrumentation.init(this, instrContext, appContext,
         new ComponentName(ii.packageName, ii.name), data.instrumentationWatcher,
         data.instrumentationUiAutomationConnection);
  
  // ...
  
  // 創(chuàng)建Application
  Application app = data.info.makeApplication(data.restrictedBackupMode, null);
  mInitialApplication = app;
  
  // 調(diào)用Instrumentation的onCreate()方法
  mInstrumentation.onCreate(data.instrumentationArgs);
  
  // 調(diào)用Application的onCreate()方法
  mInstrumentation.callApplicationOnCreate(app);
}
  • 上面代碼有兩個(gè)LoadedApk對(duì)象,分別是data.infopi井氢,由下面獲取LoadedApk代碼可以知道都是同一個(gè)LoadedApk對(duì)象
public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,                                                CompatibilityInfo compatInfo) {
  return getPackageInfo(ai, compatInfo, null, false, true, false);
}

private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
        ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
        boolean registerPackage) {
    // 根據(jù)包名加載緩存中的LoadedApk 或 創(chuàng)建LoadedApk
}
  • 到這里我們知道ClassLoader來自appContext.getClassLoader()弦追,代碼跟蹤最終調(diào)用LoadedApk的getClassLoader()方法
public ClassLoader getClassLoader() {
  synchronized (this) {
      if (mClassLoader != null) {
          return mClassLoader;
      }

      if (mIncludeCode && !mPackageName.equals("android")) {
        // ...
        mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, lib,
                    mBaseClassLoader);
      } else {
        if (mBaseClassLoader == null) {
           mClassLoader = ClassLoader.getSystemClassLoader();
        } else {
           mClassLoader = mBaseClassLoader;
        }
      }
      return mClassLoader;
    }
}
  • 可以看到LoadedApk屬性mClassLoader就是整個(gè)App的使用的ClassLoader

ZenusPlugin 類加載

  • 通過替換系統(tǒng)LoadedApk的ClassLoader為ZeusClassLoader,利用ZeusClassLoader優(yōu)先查找補(bǔ)丁中的類花竞,若存在就返回劲件,不存在則再查找宿主中的類
  • 優(yōu)先查找補(bǔ)丁中的類是先通過反射宿主ClassLoader的parent來完成

ZeusClassLoader

  • 空ClassLoader,容器作用
  • ZeusPluginClassLoader[]约急,每個(gè)插件對(duì)應(yīng)一個(gè)ZeusPluginClassLoader

ZeusHotfixClassLoader

  • 補(bǔ)丁包類加載器零远,加載補(bǔ)丁包的時(shí)候會(huì)替換插件包的parent ClassLoader

加載插件代碼

/**
 * 啟動(dòng)插件
 *
 */
public void startPlugin() {
    PluginManager.loadLastVersionPlugin(MyApplication.PLUGIN_TEST);
    try {
        Class cl = PluginManager.mNowClassLoader.loadClass(PluginManager.getPlugin(MyApplication.PLUGIN_TEST).getPluginMeta().mainClass);
        Intent intent = new Intent(this, cl);
        //這種方式為通過在宿主AndroidManifest.xml中預(yù)埋activity實(shí)現(xiàn)
//            startActivity(intent);
        //這種方式為通過欺騙android系統(tǒng)的activity存在性校驗(yàn)的方式實(shí)現(xiàn)
        PluginManager.startActivity(this,intent);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

Small 類加載

  • 通過單ClassLoader模式,通過反射Application的ClassLoader將插件包的dex文件添加到DexClassLoader中厌蔽,通過添加到DexFile Array Head實(shí)現(xiàn)插件化

VirtualAPK

  • 具備單ClassLoader和多ClassLoader模式牵辣,具體是可配置的,默認(rèn)是單ClassLoader結(jié)合多ClassLoader
  • apk直接作為插件躺枕,在VirtualAPK中被封裝成LoadedPlugin服猪,LoadedPlugin具有自己的DexClassLoader,同時(shí)根據(jù)配置判斷是否將LoadedPlugin的DexClassLoader中DexPathList合并到宿主ClassLoader

單ClassLoader vs 多ClassLoader

  • 當(dāng)加載某個(gè)類的時(shí)候拐云,如果不知道在哪個(gè)插件罢猪,通過單ClassLoader直接查找比較方便,但是查找過程比多ClassLoader小范圍查找會(huì)比較慢
  • 多ClassLoader需要管理多個(gè)ClassLoader叉瘩,單新的補(bǔ)丁插件來時(shí)需要替換等操作膳帕,不像單ClassLoader模式,直接將新插件dex文件置于DexPathList頭部即可
  • VirtualAPK采用兩個(gè)模式,其實(shí)是一種中和危彩,當(dāng)不知道要啟動(dòng)的類在哪個(gè)插件則可以直接通過Class.forName查找攒磨,當(dāng)知道要啟動(dòng)的類在哪個(gè)插件則可以通過對(duì)應(yīng)LoadedPlugin的API查找
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市汤徽,隨后出現(xiàn)的幾起案子娩缰,更是在濱河造成了極大的恐慌,老刑警劉巖谒府,帶你破解...
    沈念sama閱讀 217,185評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拼坎,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡完疫,警方通過查閱死者的電腦和手機(jī)泰鸡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來壳鹤,“玉大人盛龄,你說我怎么就攤上這事》际模” “怎么了余舶?”我有些...
    開封第一講書人閱讀 163,524評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)兆沙。 經(jīng)常有香客問我欧芽,道長(zhǎng),這世上最難降的妖魔是什么葛圃? 我笑而不...
    開封第一講書人閱讀 58,339評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮憎妙,結(jié)果婚禮上库正,老公的妹妹穿的比我還像新娘。我一直安慰自己厘唾,他們只是感情好褥符,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評(píng)論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著抚垃,像睡著了一般喷楣。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鹤树,一...
    開封第一講書人閱讀 51,287評(píng)論 1 301
  • 那天铣焊,我揣著相機(jī)與錄音,去河邊找鬼罕伯。 笑死曲伊,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的追他。 我是一名探鬼主播坟募,決...
    沈念sama閱讀 40,130評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼岛蚤,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了懈糯?” 一聲冷哼從身側(cè)響起涤妒,我...
    開封第一講書人閱讀 38,985評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎赚哗,沒想到半個(gè)月后届腐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,420評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蜂奸,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評(píng)論 3 334
  • 正文 我和宋清朗相戀三年犁苏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片扩所。...
    茶點(diǎn)故事閱讀 39,779評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡围详,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出祖屏,到底是詐尸還是另有隱情助赞,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評(píng)論 5 345
  • 正文 年R本政府宣布袁勺,位于F島的核電站雹食,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏期丰。R本人自食惡果不足惜群叶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望钝荡。 院中可真熱鬧街立,春花似錦、人聲如沸埠通。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽端辱。三九已至梁剔,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間舞蔽,已是汗流浹背荣病。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留喷鸽,地道東北人众雷。 一個(gè)月前我還...
    沈念sama閱讀 47,876評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親砾省。 傳聞我的和親對(duì)象是個(gè)殘疾皇子鸡岗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評(píng)論 2 354

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

  • Android插件化基礎(chǔ)的主要內(nèi)容包括 Android插件化基礎(chǔ)1-----加載SD上APKAndroid插件化基...
    隔壁老李頭閱讀 5,410評(píng)論 5 36
  • 過去的一兩年android插件化,熱修復(fù)等技術(shù)發(fā)展迅速编兄,并且還在持續(xù)的探索中轩性,也許插件化技術(shù)最終會(huì)在android...
    jjlanbupt閱讀 4,493評(píng)論 16 15
  • 從去年下半年開始,熱修復(fù)技術(shù)在 Android 技術(shù)社區(qū)熱了一陣子狠鸳,這種不用發(fā)布新版本就可以修復(fù)線上 bug 的技...
    小小亭長(zhǎng)閱讀 6,330評(píng)論 6 19
  • 簡(jiǎn)書 編程之樂轉(zhuǎn)載請(qǐng)注明原創(chuàng)出處揣苏! JVM類加載機(jī)制 JVM類加載機(jī)制分為五個(gè)部分:加載,驗(yàn)證件舵,準(zhǔn)備卸察,解析,初始化...
    Alien的小窩閱讀 9,866評(píng)論 3 11
  • 我曾經(jīng)遇見過一個(gè)很好聽的名字铅祸,屬于朋友的朋友坑质,儲(chǔ)北辰。 今日回想临梗,特意去科普了不少涡扼。儲(chǔ)姓,無論在大陸還是臺(tái)灣盟庞,都沒...
    夜樨閱讀 1,575評(píng)論 0 4