为了账号安全,请及时绑定邮箱和手机立即绑定

Android Multidex导致的App启动缓慢

标签:
Android

Android社区中多次说到了dex包的65536方法数限制,现在针对这个问题的解决方法是dex分包(Multidexing)。虽然这是google提出的一个很好的解决办法,但是我注意到了它对App的启动速度影响很严重(这个问题现在还没有被Android社区所重视)。所以我写下了这篇文章,写给那些想实现dex分包但是不知道它的这个缺点或者已经实现了dex分包但是想看看它性能的开发者。

背景

简单来说,构建Android应用时这样一个流程:Java代码=>.class文件(与依赖库)=>独立的.dex文件。这个.dex文件最后与资源文件一起打包成.apk文件,这就是你最后从应用商店下载下来的安装文件。具体可以参考这里

对编译过程的一个限制就是在dex文件中系统允许的方法总数最多为65536。早期的Android开发者通过混淆来减少不必要的代码,从而避免方法数超过限制的问题。然而混淆在这方面能做到的事情比较有限,而且它只是延缓了方法数量过限的时间,并没有根治。所以后来Google在support library里面出了一个解决方案:dex分包(Multidex),这个方案可以很方便地处理方法数超过限制的问题,但是就如同我之前所说,它会极大地延缓App的启动速度。

使用Multidex

Multidex现在是一个成熟的、文档丰富的工具。我强烈推荐通过官网流程来在工程中实现Multidex,你也可以在我的github

NoClassDefFoundError?!

当你在项目中使用了multidex的时候,你的app可能会产生java.lang.NoClassDefFoundError异常。这意味着你的app在启动的时候没有找到含有指定类的class文件。Android的Gradle插件首先需要SDK build tools 21.1及以上才支持multidex,它会在混淆工程之后列出一个主dex文件中包含的类的清单([buildDir]/intermediates/multi-dex/[buildType]/maindexlist.txt)。但这里面可能没有包含所有在App启动时需要加载的类,这时启动App就会抛出这个异常。

如何解决?

要解决这个问题,你要列出一份启动App时需要加载的类的清单,并告诉编译器这些类要保留在主dex文件中。你可以这么做:

·         在工程文件夹下创建一个multidex.keep文件

·         将java.lang.NoClassDefFoundError异常中报出的类列到multidex.keep中。(不要修改maindexlist.txt,这个文件每次都会重新生成,改动无效)

·         在使用混淆的模块的gradle脚本中天下如下代码,它会每次在编译的时候将multidex.keep文件中的内容添加到`maindexlist.txt"中。

[代码]java代码:

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

android.applicationVariants.all { variant ->

       task   "fix${variant.name.capitalize()}MainDexClassList" << {

           logger.info   "Fixing main dex keep file for $variant.name"

           File   keepFile = new File("$buildDir/intermediates/multi-dex/$variant.buildType.name/maindexlist.txt")

 

           keepFile.withWriterAppend   { w ->

               //   Get a reader for the input file

               w.append('\n')

               new   File("${projectDir}/multidex.keep").withReader { r ->

                   //   And write data from the input into the output

                   w   << r << '\n'

               }

               logger.info   "Updated main dex keep file for   ${keepFile.getAbsolutePath()}\n$keepFile.text"

           }

       }

   }

 

   tasks.whenTaskAdded { task ->

       android.applicationVariants.all   { variant ->

           if   (task.name == "create${variant.name.capitalize()}MainDexClassList")   {

               task.finalizedBy   "fix${variant.name.capitalize()}MainDexClassList"

           }

       }

   }

 

Multidex对应用启动速度造成的影响。

如果你使用了multidex,那么你需要注意它可能会加长App的启动速度。我们通过追踪App的启动时间(从点击icon到所有的图标被下载、显示的时间)。当使用multidex后,4.4及以下的系统启动时间会加长大约15%,你可以在Carlos Sessa的这篇文章中了解到更多信息。

由于5.0以上的Android系统采用了ART运行时,它本身就支持multidex的加载,所以5.0以上系统影响较小。但是5.0以下的系统将会在加载主dex之外的类时会有比较明显的延迟。

解决multidex带来的启动时间影响

在App启动到所有图片加载到屏幕上之间的这段时间内,有很多类既没有被混淆,也不在主dex文件中。我们要怎么知道哪些类已经被App加载了呢?

幸运的是,在ClassLoader中有一个findLoadedClass()方法,我们的解决办法就是在启动结束的时候查看有没有不在主dex文件中却依然在启动阶段被加载的类,将它们添加到之前的multidex.keep文件中,手动将其加入主dex文件:

·         将下面这个类添加到代码中,运行其中的getLoadedExternalDexClasses查看是否有一些副dex中的类在启动结束后被加载了;

·         将上一步检测到的类添加到我们的multidex.keep文件中,重新编译。

[代码]java代码:

?

001

002

003

004

005

006

007

008

009

010

011

012

013

014

015

016

017

018

019

020

021

022

023

024

025

026

027

028

029

030

031

032

033

034

035

036

037

038

039

040

041

042

043

044

045

046

047

048

049

050

051

052

053

054

055

056

057

058

059

060

061

062

063

064

065

066

067

068

069

070

071

072

073

074

075

076

077

078

079

080

081

082

083

084

085

086

087

088

089

090

091

092

093

094

095

096

097

098

099

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

public class MultiDexUtils {

        private   static final String EXTRACTED_NAME_EXT = ".classes";

        private   static final String EXTRACTED_SUFFIX = ".zip";

 

        private   static final String SECONDARY_FOLDER_NAME = "code_cache" +   File.separator +

                "secondary-dexes";

 

        private   static final String PREFS_FILE = "multidex.version";

        private   static final String KEY_DEX_NUMBER = "dex.number";

 

        private   SharedPreferences getMultiDexPreferences(Context context) {

            return   context.getSharedPreferences(PREFS_FILE,

                    Build.VERSION.SDK_INT   < Build.VERSION_CODES.HONEYCOMB

                            ?   Context.MODE_PRIVATE

                            :   Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);

        }

 

        /**

         -   get all the dex path

         *

         -   @param context the application context

         -   @return all the dex path

         -   @throws PackageManager.NameNotFoundException

         -   @throws IOException

         */

        public List<string>   getSourcePaths(Context context) throws PackageManager.NameNotFoundException,   IOException {

            final   ApplicationInfo applicationInfo =   context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);

            final   File sourceApk = new File(applicationInfo.sourceDir);

            final   File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);

 

            final   List<string> sourcePaths = new ArrayList<>();

            sourcePaths.add(applicationInfo.sourceDir);   //add the default apk path

 

            //the   prefix of extracted file, ie: test.classes

            final   String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;

            //the   total dex numbers

            final   int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER,   1);

 

            for   (int secondaryNumber = 2; secondaryNumber <= totalDexNumber;   secondaryNumber++) {

                //for   each dex file, ie: test.classes2.zip, test.classes3.zip...

                final   String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;

                final   File extractedFile = new File(dexDir, fileName);

                if   (extractedFile.isFile()) {

                    sourcePaths.add(extractedFile.getAbsolutePath());

                    //we   ignore the verify zip part

                }   else {

                    throw   new IOException("Missing extracted secondary dex file '" +

                            extractedFile.getPath()   + "'");

                }

            }

 

            return   sourcePaths;

        }

 

        /**

         -   get all the external classes name in "classes2.dex",   "classes3.dex" ....

         *

         -   @param context the application context

         -   @return all the classes name in the external dex

         -   @throws PackageManager.NameNotFoundException

         -   @throws IOException

         */

        public List<string>   getExternalDexClasses(Context context) throws PackageManager.NameNotFoundException,   IOException {

            final   List<string> paths = getSourcePaths(context);

            if(paths.size()   <= 1) {

                //   no external dex

                return   null;

            }

            //   the first element is the main dex, remove it.

            paths.remove(0);

            final   List<string> classNames = new ArrayList<>();

            for   (String path : paths) {

                try   {

                    DexFile   dexfile = null;

                    if   (path.endsWith(EXTRACTED_SUFFIX)) {

                        //NOT   use new DexFile(path), because it will throw "permission error in   /data/dalvik-cache"

                        dexfile   = DexFile.loadDex(path, path + ".tmp", 0);

                    }   else {

                        dexfile   = new DexFile(path);

                    }

                    final   Enumeration<string> dexEntries = dexfile.entries();

                    while   (dexEntries.hasMoreElements()) {

                        classNames.add(dexEntries.nextElement());

                    }

                }   catch (IOException e) {

                    throw   new IOException("Error at loading dex file '" +

                            path   + "'");

                }

            }

            return   classNames;

        }

 

        /**

         -   Get all loaded external classes name in "classes2.dex",   "classes3.dex" ....

         -   @param context

         -   @return get all loaded external classes

         */

        public List<string>   getLoadedExternalDexClasses(Context context) {

            try   {

                final   List<string> externalDexClasses = getExternalDexClasses(context);

                if   (externalDexClasses != null && !externalDexClasses.isEmpty()) {

                    final   ArrayList<string> classList = new ArrayList<>();

                    final   java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod("findLoadedClass",   new Class[]{String.class});

                    m.setAccessible(true);

                    final   ClassLoader cl = context.getClassLoader();

                    for   (String clazz : externalDexClasses) {

                        if   (m.invoke(cl, clazz) != null) {

                            classList.add(clazz.replaceAll("\\.",   "/").replaceAll("$", ".class"));

                        }

                    }

                    return   classList;

                }

            }   catch (Exception e) {

                e.printStackTrace();

            }

            return   null;

        }

    }</string></string></string></string></string></string></string></string></string>

 

实验结果

这里是我们得出的实验结果。蓝色柱是不使用multidex时的启动时间,红色柱是使用multidex时的启动时间,你可以看到两者之间的巨大差距,仅仅是因为我们使用了multidex而已。之后我们进行了上述优化改进,得出的启动时间是绿色柱,你可以看到它回到了原先的启动速度,甚至比原先更快。你可以尝试一下,它会为你的app性能带来提升。

https://img1.sycdn.imooc.com//5bd42fc90001dc5107100482.jpg

最后的话

你可以这么做不代表你必须这么做。

你应该将multidex认为是一种走投无路的解决办法,因为它会严重影响App的启动速度,并且还要维护额外的代码来解决奇怪的异常(java.lang.NoClassDefFoundError)。当达到了65536的方法数限制时,我们不应该先想到multidex因为它会带来性能影响。我们检查工程并发现了很多没有用处的代码,进行删除、重构。仅仅当没有别的办法时,才引入multidex,并且从此我们都会十分注意代码质量、标准。与其直接使用multidex,不如花时间让你的代码变得简洁、高效,重构你的代码使其不要超过65536的方法数限制。

 原文链接:http://www.apkbus.com/blog-705730-60513.html


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消