Android Support Multidexの原理分析

22452 ワード

  • Android Support Multidex原理分析
  • 前言
  • ソースコード分析
  • まとめ

  • Android Support Multidexの原理分析
    前言
    公式紹介:Configure Apps with Over 64 K Methods
    ソース分析
    まず公式の使用紹介を見てみましょう.
    When writing instrumentation tests for multidex apps, no additional configuration is required. AndroidJUnitRunner supports multidex out of the box, as long as you use MultiDexApplication or override the attachBaseContext() method in your custom Application object and call MultiDex.install(this) to enable multidex.
    androidを使う必要があるならsupport_Multidexというjarパッケージは、jarパッケージのMultiDexApplicationまたはattachBaseContext()メソッドを書き換えるだけでいいです.
  • は、ManifestのApplicationノードの下で直接MultiDexApplicationを使用し、カスタムApplicationがあればMultiDexApplicationを継承すればよい.
  • 独自のアプリケーションがカスタマイズされ、他のアプリケーションが継承されている場合、attachBaseContextメソッドを書き換え、MultiDex.install(this)を呼び出すことができる.

  • 次に、この方法の具体的な実装を見てみましょう.
    public class MultiDexApplication
      extends Application
    {
      protected void attachBaseContext(Context base)
      {
        super.attachBaseContext(base);
        MultiDex.install(this); //      MultiDex.install(this)  
      }
    }
    

    この方法では、まずAndroidデバイスがMultiDexをサポートしているかどうかを判断し、実際にAndroid 4.4以上のバージョンの仮想マシンはART技術を導入し、すでにMultiDexをサポートしており、Dalvikとの違いは詳しくはART and Dalvikを参照してください.
    次に、現在のAndroidシステムのバージョンを判断します.android_support_multidex.jarが実際にサポートしている範囲はApi:4から20です.MultiDexExtractor.load(context, applicationInfo, dexDir, false);メソッドの呼び出しを続行
     public static void install(Context context)
      {
        Log.i("MultiDex", "install");
        if (IS_VM_MULTIDEX_CAPABLE) //          MultiDex,     return
        {
          Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
          return;
        }
        if (Build.VERSION.SDK_INT < 4) { //  Android       ,     Api 4(Android 1.6)
          throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT + 
            " is unsupported. Min SDK version is " + 4 + ".");
        }
        try
        {
          ApplicationInfo applicationInfo = getApplicationInfo(context);
          if (applicationInfo == null) { //  Application   null, null     install  。
            return;
          }
          synchronized (installedApk)
          {
            String apkPath = applicationInfo.sourceDir; //    apk   ,         ,           
            if (installedApk.contains(apkPath)) {
              return;
            }
            installedApk.add(apkPath);
            if (Build.VERSION.SDK_INT > 20) { //  Android       ,Api  20(Android 4.4W)    Multidex
              Log.w("MultiDex", "MultiDex is not guaranteed to work in SDK version " + 
                Build.VERSION.SDK_INT + ": SDK version higher than " + 
                20 + " should be backed by " + 
                "runtime with built-in multidex capabilty but it's not the " + 
                "case here: java.vm.version=\"" + 
                System.getProperty("java.vm.version") + "\"");
            }
            try
            {
              loader = context.getClassLoader(); //     ClassLoader
            }
            catch (RuntimeException e)
            {
              ClassLoader loader;
              Log.w("MultiDex", "Failure while trying to obtain Context class loader. Must be running in test mode. Skip patching.", 
                e); return;
            }
            ClassLoader loader;
            if (loader == null)
            {
              Log.e("MultiDex", 
                "Context class loader is null. Must be running in test mode. Skip patching.");
    
              return;
            }
            try
            {
              clearOldDexDir(context); //    Dex  
            }
            catch (Throwable t)
            {
              Log.w("MultiDex", "Something went wrong when trying to clear old MultiDex extraction, continuing without cleaning.", 
                t);
            }
            File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME); // Dex     : /data/app/  /code_cache/secondary-dexes
            List files = MultiDexExtractor.load(context, applicationInfo, dexDir, false); //   Dex     
            if (checkValidZipFiles(files)) //        zip  
            {
              installSecondaryDexes(loader, dexDir, files); //     Dex  ,  !
            }
            else
            {
              Log.w("MultiDex", "Files were not valid zip files.  Forcing a reload.");
    
              files = MultiDexExtractor.load(context, applicationInfo, dexDir, true); //zip      ,         
              if (checkValidZipFiles(files)) {
                installSecondaryDexes(loader, dexDir, files); //     ,      ,        
              } else {
                throw new RuntimeException("Zip files were not valid.");
              }
            }
          }
          Log.i("MultiDex", "install done");
        }
        catch (Exception e)
        {
          Log.e("MultiDex", "Multidex installation failure", e);
          throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ").");
        }
      }
    
    
    MultiDexExtractor.load(context, applicationInfo, dexDir, false);メソッド:次に、このメソッドの具体的な実装方法を見てみましょう.
      static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir, boolean forceReload)
        throws IOException
      {
        Log.i("MultiDex", "MultiDexExtractor.load(" + applicationInfo.sourceDir + ", " + forceReload + ")");
        File sourceApk = new File(applicationInfo.sourceDir); //    apk       /data/app/  -1.apk
    
        long currentCrc = getZipCrc(sourceApk); //  crc  
        List<File> files;
        if ((!forceReload) && (!isModified(context, sourceApk, currentCrc))) //         dex        
        {
          try
          {
            files = loadExistingExtractions(context, sourceApk, dexDir);
          }
          catch (IOException ioe)
          {
            List<File> files;
            Log.w("MultiDex", "Failed to reload existing extracted secondary dex files, falling back to fresh extraction", 
              ioe);
            List<File> files = performExtractions(sourceApk, dexDir); //        
            putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
          }
        }
        else
        {
          Log.i("MultiDex", "Detected that extraction must be performed.");
          files = performExtractions(sourceApk, dexDir);  //              
          putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
        }
        Log.i("MultiDex", "load found " + files.size() + " secondary dex files");
        return files;
      }
    

    loadExistingExtractions(context, sourceApk, dexDir); すでにロードされているDexファイルを含む圧縮ファイルを取得します.
    
    private static List loadExistingExtractions(Context context, File sourceApk, File dexDir)
        throws IOException
      {
        Log.i("MultiDex", "loading existing secondary dex files");
    
        String extractedFilePrefix = sourceApk.getName() + ".classes"; //  /data/app/  -1.classes
        int totalDexNumber = getMultiDexPreferences(context).getInt("dex.number", 1);
        List files = new ArrayList(totalDexNumber);
        for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++)
        {
          String fileName = extractedFilePrefix + secondaryNumber + ".zip"; // /data/app/  -1.apk.classes2.zip ...
          File extractedFile = new File(dexDir, fileName);                  // /data/app/  -1.apk.classesN.zip
          if (extractedFile.isFile())
          {
            files.add(extractedFile);  //       ,    List   
            if (!verifyZipFile(extractedFile)) //  zip      
            {
              Log.i("MultiDex", "Invalid zip file: " + extractedFile);
              throw new IOException("Invalid ZIP file.");
            }
          }
          else
          {
            throw new IOException("Missing extracted secondary dex file '" + 
              extractedFile.getPath() + "'");
          }
        }
        return files;
      }
    

    performExtractions(sourceApk, dexDir);
    
    
    private static final String DEX_PREFIX = "classes";
    private static final String DEX_SUFFIX = ".dex";
    
    private static final String EXTRACTED_NAME_EXT = ".classes";
    private static final String EXTRACTED_SUFFIX = ".zip";
    
    private static List performExtractions(File sourceApk, File dexDir)
                throws IOException {
    
            final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT; // /data/app/  .classes
    
            // Ensure that whatever deletions happen in prepareDexDir only happen if the zip that
            // contains a secondary dex file in there is not consistent with the latest apk.  Otherwise,
            // multi-process race conditions can cause a crash loop where one process deletes the zip
            // while another had created it.
            prepareDexDir(dexDir, extractedFilePrefix); //      classes{}.dex   
    
            List files = new ArrayList();
    
            final ZipFile apk = new ZipFile(sourceApk); //    zipFile,    apk: // /data/apk/  -1.apk
            try {
    
                int secondaryNumber = 2;
    
                ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX); //   zip  ,  classes2.dex ,classesN.dex....
                while (dexFile != null) {
                    String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX; 
                    File extractedFile = new File(dexDir, fileName);  //   /data/app/  -1.apk.classes2.zip
                    files.add(extractedFile); 
    
                    Log.i(TAG, "Extraction is needed for file " + extractedFile);
                    int numAttempts = 0;
                    boolean isExtractionSuccessful = false; //        
                    while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) { //    3 
                        numAttempts++;
    
                        // Create a zip file (extractedFile) containing only the secondary dex file
                        // (dexFile) from the apk.
                        extract(apk, dexFile, extractedFile, extractedFilePrefix);
    
                        // Verify that the extracted file is indeed a zip file.
                        isExtractionSuccessful = verifyZipFile(extractedFile); //  zip            
    
                        // Log the sha1 of the extracted zip file
                        Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") +
                                " - length " + extractedFile.getAbsolutePath() + ": " +
                                extractedFile.length());
                        if (!isExtractionSuccessful) {
                            // Delete the extracted file
                            extractedFile.delete(); //       
                            if (extractedFile.exists()) {
                                Log.w(TAG, "Failed to delete corrupted secondary dex '" +
                                        extractedFile.getPath() + "'");
                            }
                        }
                    }
                    if (!isExtractionSuccessful) {
                        throw new IOException("Could not create zip file " +
                                extractedFile.getAbsolutePath() + " for secondary dex (" +
                                secondaryNumber + ")");
                    }
                    secondaryNumber++;
                    dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
                }
            } finally {
                try {
                    apk.close();
                } catch (IOException e) {
                    Log.w(TAG, "Failed to close resource", e);
                }
            }
    
            return files;
        }
    

    最後に、現在のAndroidシステムのApiバージョンに従って対応するinstallメソッドを呼び出し、Dexファイルからロードします.
    private static void installSecondaryDexes(ClassLoader loader, File dexDir, List files)
                throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
                InvocationTargetException, NoSuchMethodException, IOException {
            if (!files.isEmpty()) {
                if (Build.VERSION.SDK_INT >= 19) {
                    V19.install(loader, files, dexDir); //files     List  ,// /data/app/  -1.apk.classes2.zip   
                } else if (Build.VERSION.SDK_INT >= 14) {                     // /data/app/  -1.apk.classesN.zip
                    V14.install(loader, files, dexDir);
                } else {
                    V4.install(loader, files);
                }
            }
        }
    

    まとめ
    上記の分析により、MultiDex全体の読み取りプロセスを簡単にまとめることができます.
  • apkをインストールするとき、現在のAndroidシステムAPIバージョンがMultiDex
  • をサポートしているかどうかを判断します.
  • このapkに複数のDexが含まれている場合、apkファイルを解凍し、その中のDexファイルからzipファイルに変換し、Dexセットから
  • に追加する.
  • DexコレクションからclassLoaderによってロードされたDexファイル