【性能优化】65535方法数超出

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/www1575066083/article/details/80938378

1、基本介绍:

  • 当Android系统启动一个应用的时候,有一步是对Dex进行优化,这个过程有一个专门的工具来处理,叫DexOpt。DexOpt的执行过程是在第一次加载Dex文件的时候执行的。这个过程会生成一个ODEX文件,即Optimised Dex。执行ODex的效率会比直接执行Dex文件的效率要高很多。
  • 但是在早期的Android系统中,DexOpt有一个问题,也就是这篇文章想要说明并解决的问题。DexOpt会把每一个类的方法id检索起来,存在一个链表结构里面。但是这个链表的长度是用一个short类型来保存的,导致了方法id的数目不能够超过65536个。当一个项目足够大的时候,显然这个方法数的上限是不够的。尽管在新版本的Android系统中,DexOpt修复了这个问题,但是我们仍然需要对老系统做兼容。
    • 这些 method 是指能够 索引 (reference) 到的,而不是 定义 (define) 的。或者说,如果你定义了一个方法,但这个方法并没有被调用,那么就不算在内。
    • 这些 method 不仅仅是开发人员自己写的,还包括所有第三方 library 里面的 method。

2、解决办法:

Multidex:
官方提供的解决方案,这篇文章 里有详细的使用方法,此不赘述。
ProGuard:
ProGuard可以把 code 里 unnecessary 的 method 移除,压缩 apk,当然还有 代码混淆 的奇效。
再创建一个 DEX File:
把 app 里可以独立的模块或 code 提取出来,放到一个独立的 dex 文件里,你可以使用Custom ClassLoader 来加载这些类,然后使用 接口 或反射 来调用这些方法。不过,这个过程还是比较麻烦的。

3、Multidex原理:

3.1、工作流程:MultiDex的工作流程具体分为两个部分

  • 一个部分是打包构建Apk的时候,将Dex文件拆分成若干个小的Dex文件,这个Android Studio已经帮我们做了(设置 “multiDexEnabled true”)。
  • 另一部分就是在启动Apk的时候,同时加载多个Dex文件(具体是加载Dex文件优化后的Odex文件,不过文件名还是.dex),这一部分工作从Android 5.0开始系统已经帮我们做了,但是在Android 5.0以前还是需要通过MultiDex Support库来支持(MultiDex.install(Context))。

3.2、MultiDex.install(Context):

   public static void install(Context context) {
          Log.i(TAG, "install");

          // 1. 判读是否需要执行MultiDex。
          if (IS_VM_MULTIDEX_CAPABLE) {
              Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
              return;
          }
          if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
              throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT
                      + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
          }
          try {
              ApplicationInfo applicationInfo = getApplicationInfo(context);
              if (applicationInfo == null) {
                  // Looks like running on a test Context, so just return without patching.
                  return;
              }

              // 2. 如果这个方法已经调用过一次,就不能再调用了。
              synchronized (installedApk) {
                  String apkPath = applicationInfo.sourceDir;
                  if (installedApk.contains(apkPath)) {
                      return;
                  }
                  installedApk.add(apkPath);

                  // 3. 如果当前Android版本已经自身支持了MultiDex,依然可以执行MultiDex操作,
                  // 但是会有警告。
                  if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) {
                      Log.w(TAG, "MultiDex is not guaranteed to work in SDK version "
                              + Build.VERSION.SDK_INT + ": SDK version higher than "
                              + MAX_SUPPORTED_SDK_VERSION + " 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") + "\"");
                  }

                  // 4. 获取当前的ClassLoader实例,后面要做的工作,就是把其他dex文件加载后,
                  // 把其DexFile对象添加到这个ClassLoader实例里就完事了。
                  ClassLoader loader;
                  try {
                      loader = context.getClassLoader();
                  } catch (RuntimeException e) {
                      Log.w(TAG, "Failure while trying to obtain Context class loader. " +
                              "Must be running in test mode. Skip patching.", e);
                      return;
                  }
                  if (loader == null) {
                      Log.e(TAG,
                              "Context class loader is null. Must be running in test mode. "
                              + "Skip patching.");
                      return;
                  }
                  try {
                    // 5. 清除旧的dex文件,注意这里不是清除上次加载的dex文件缓存。
                    // 获取dex缓存目录是,会优先获取/data/data/<package>/code-cache作为缓存目录。
                    // 如果获取失败,则使用/data/data/<package>/files/code-cache目录。
                    // 这里清除的是/data/data/<package>/files/code-cache目录。
                    clearOldDexDir(context);
                  } catch (Throwable t) {
                    Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
                        + "continuing without cleaning.", t);
                  }

                  // 6. 获取缓存目录(/data/data/<package>/code-cache)。
                  File dexDir = getDexDir(context, applicationInfo);

                  // 7. 加载缓存文件(如果有)。
                  List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);

                  // 8. 检查缓存的dex是否安全
                  if (checkValidZipFiles(files)) {
                      // 9. 安装缓存的dex
                      installSecondaryDexes(loader, dexDir, files);
                  } else {
                      // 9. 从apk压缩包里面提取dex文件
                      Log.w(TAG, "Files were not valid zip files.  Forcing a reload.");
                      files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
                      if (checkValidZipFiles(files)) {
                          // 10. 安装提取的dex
                          installSecondaryDexes(loader, dexDir, files);
                      } else {
                          throw new RuntimeException("Zip files were not valid.");
                      }
                  }
              }
          } catch (Exception e) {
              Log.e(TAG, "Multidex installation failure", e);
              throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ").");
          }
          Log.i(TAG, "install done");
      }
1、MultiDex#clearOldDexDir(Context)方法:
这个方法的作用是删除/data/data//files/code-cache,一开始我以为这个方法是删除上一次执行MultiDex后的缓存文件,不过这明显不对,不可能每次MultiDex都重新解压dex文件一边,这样每次启动会很耗时,只有第一次冷启动的时候才需要解压dex文件。后来我又想是不是以前旧版的MultiDex曾经把缓存文件放在这个目录里,现在新版本只是清除以前旧版的遗留文件?但是我找遍了整个MultiDex Repo的提交也没有见过类似的旧版本代码。后面我仔细看MultiDex#getDexDir这个方法才发现,原来MultiDex在获取dex缓存目录是,会优先获取/data/data//code-cache作为缓存目录,如果获取失败,则使用/data/data//files/code-cache目录,而后者的缓存文件会在每次App重新启动的时候被清除。
2、MultiDexExtractor#load()方法:
    这个过程主要是获取可以安装的dex文件列表,可以是上次解压出来的缓存文件,也可以是重新从Apk包里面提取出来的。需要注意的时,如果是重新解压,这里会有明显的耗时,而且解压出来的dex文件,会被压缩成.zip压缩包,压缩的过程也会有明显的耗时(这里压缩dex文件可能是问了节省空间)。 如果dex文件是重新解压出来的,则会保存dex文件的信息,包括解压的apk文件的crc值、修改时间以及dex文件的数目,以便下一次启动直接使用已经解压过的dex缓存文件,而不是每一次都重新解压。
static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir,
              boolean forceReload) throws IOException {
          Log.i(TAG, "MultiDexExtractor.load(" + applicationInfo.sourceDir + ", " + forceReload + ")");
          final File sourceApk = new File(applicationInfo.sourceDir);

          // 1. 获取当前Apk文件的crc值。
          long currentCrc = getZipCrc(sourceApk);
          // Validity check and extraction must be done only while the lock file has been taken.
          File lockFile = new File(dexDir, LOCK_FILENAME);
          RandomAccessFile lockRaf = new RandomAccessFile(lockFile, "rw");
          FileChannel lockChannel = null;
          FileLock cacheLock = null;
          List<File> files;
          IOException releaseLockException = null;
          try {
              lockChannel = lockRaf.getChannel();
              Log.i(TAG, "Blocking on lock " + lockFile.getPath());

              // 2. 加上文件锁,防止多进程冲突。
              cacheLock = lockChannel.lock();
              Log.i(TAG, lockFile.getPath() + " locked");

              // 3. 先判断是否强制重新解压,这里第一次会优先使用已解压过的dex文件,如果加载失败就强制重新解压。
              // 此外,通过crc和文件修改时间,判断如果Apk文件已经被修改(覆盖安装),就会跳过缓存重新解压dex文件。
              if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
                  try {

                      // 4. 加载缓存的dex文件
                      files = loadExistingExtractions(context, sourceApk, dexDir);
                  } catch (IOException ioe) {
                      Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
                              + " falling back to fresh extraction", ioe);

                      // 5. 加载失败的话重新解压,并保存解压出来的dex文件的信息。
                      files = performExtractions(sourceApk, dexDir);
                      putStoredApkInfo(context,
                              getTimeStamp(sourceApk), currentCrc, files.size() + 1);
                  }
              } else {
                  // 4. 重新解压,并保存解压出来的dex文件的信息。
                  Log.i(TAG, "Detected that extraction must be performed.");
                  files = performExtractions(sourceApk, dexDir);
                  putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
              }
          } finally {
              if (cacheLock != null) {
                  try {
                      cacheLock.release();
                  } catch (IOException e) {
                      Log.e(TAG, "Failed to release lock on " + lockFile.getPath());
                      // Exception while releasing the lock is bad, we want to report it, but not at
                      // the price of overriding any already pending exception.
                      releaseLockException = e;
                  }
              }
              if (lockChannel != null) {
                  closeQuietly(lockChannel);
              }
              closeQuietly(lockRaf);
          }
          if (releaseLockException != null) {
              throw releaseLockException;
          }
          Log.i(TAG, "load found " + files.size() + " secondary dex files");
          return files;
      }

3、MultiDex#installSecondaryDexes()方法:

  • 在不同的SDK版本上,ClassLoader(更准确来说是DexClassLoader)加载dex文件的方式有所不同。
  • DexClassLoader会使用一个DexPathList类来封装DexFile数组。通过调用DexPathList#makeDexElements方法,可以加载我们上面解压得到的dex文件,从代码也可以看出,DexPathList#makeDexElements其实也是通过调用DexFile#loadDex来加载dex文件并创建DexFile对象的。V14中,通过反射调用DexPathList#makeDexElements方法加载我们需要的dex文件,在把加载得到的数组扩展到ClassLoader实例的”pathList”字段,从而完成dex文件的安装。
    ClassLoader是支持直接加载.dex/.zip/.jar/.apk的dex文件包的。

  • 在创建DexFile对象的时候,都需要通过DexFile的Native方法openDexFile来打开dex文件,其具体细节暂不讨论(涉及到dex的文件结构,很烦,有兴趣请阅读dalvik_system_DexFile.cpp),这个过程的主要目的是给当前的dex文件做Optimize优化处理并生成相同文件名的odex文件,App实际加载类的时候,都是通过odex文件进行的。因为每个设备对odex格式的要求都不一样,所以这个优化的操作只能放在安装Apk的时候处理,主dex的优化我们已经在安装apk的时候搞定了,其余的dex就是在MultiDex#installSecondaryDexes里面优化的,而后者也是MultiDex过程中,另外一个耗时比较多的操作。(在MultiDex中,提取出来的dex文件被压缩成.zip文件,又优化后的odex文件则被保存为.dex文件。)

    private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files)
              throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
              InvocationTargetException, NoSuchMethodException, IOException {
          if (!files.isEmpty()) {
              if (Build.VERSION.SDK_INT >= 19) {
                  V19.install(loader, files, dexDir);
              } else if (Build.VERSION.SDK_INT >= 14) {
                  V14.install(loader, files, dexDir);
              } else {
                  V4.install(loader, files);
              }
          }
      }

猜你喜欢

转载自blog.csdn.net/www1575066083/article/details/80938378