插件化類加載

插件化框架實(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對象作為該類訪問入口

連接階段

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

初始化階段

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

Java 類加載器

  • 啟動類加載器(Bootstrap ClassLoader)

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

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

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

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

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

類加載器的雙親委托模型

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

Android類加載

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

Android 類加載器

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

  • PathClassLoader是系統(tǒng)類加載器,同時也是默認(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的時候會做一次優(yōu)化,在這一過程,由虛擬機(jī)控制的一個verify選項(xiàng),如果開啟會進(jìn)行一次校驗(yàn),如果某個類沒有引用其他dex中的類,這個類會被打上CLASS_ISPREVERIFIED 的標(biāo)志。一旦被打上這個標(biāo)志,就無法再從其他 dex 中加載這個類了
  • 這個問題的比較簡單的解決辦法是引用其他dex的類

App ClassLoader Hook點(diǎn)

  • 我們知道App啟動會初始化Application并且調(diào)用onCreate,這其實(shí)是在接受AMS啟動信息后調(diào)用ActivityThread的handleBindApplication,函數(shù)過長,下面截取關(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);
}
  • 上面代碼有兩個LoadedApk對象,分別是data.infopi,由下面獲取LoadedApk代碼可以知道都是同一個LoadedApk對象
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就是整個App的使用的ClassLoader

ZenusPlugin 類加載

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

ZeusClassLoader

  • 空ClassLoader,容器作用
  • ZeusPluginClassLoader[],每個插件對應(yīng)一個ZeusPluginClassLoader

ZeusHotfixClassLoader

  • 補(bǔ)丁包類加載器,加載補(bǔ)丁包的時候會替換插件包的parent ClassLoader

加載插件代碼

/**
 * 啟動插件
 *
 */
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,同時根據(jù)配置判斷是否將LoadedPlugin的DexClassLoader中DexPathList合并到宿主ClassLoader

單ClassLoader vs 多ClassLoader

  • 當(dāng)加載某個類的時候,如果不知道在哪個插件,通過單ClassLoader直接查找比較方便,但是查找過程比多ClassLoader小范圍查找會比較慢
  • 多ClassLoader需要管理多個ClassLoader,單新的補(bǔ)丁插件來時需要替換等操作,不像單ClassLoader模式,直接將新插件dex文件置于DexPathList頭部即可
  • VirtualAPK采用兩個模式,其實(shí)是一種中和,當(dāng)不知道要啟動的類在哪個插件則可以直接通過Class.forName查找,當(dāng)知道要啟動的類在哪個插件則可以通過對應(yīng)LoadedPlugin的API查找
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容