Android Multidex导致的App启动缓慢

发表于 4年以前  | 总阅读数:1217 次

Android Multidex导致的App启动缓慢

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"中。
    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文件中,重新编译。
    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;
        }
    }

实验结果

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

multidex

最后的话

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

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

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:8月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:8月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:8月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:8月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:8月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:8月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:8月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:8月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:8月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:8月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:8月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:8月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:8月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:8月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:8月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:8月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:8月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:8月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:8月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:8月以前  |  398次阅读  |  详细内容 »
 相关文章
简化Android的UI开发 4年以前  |  520761次阅读
Android 深色模式适配原理分析 3年以前  |  28743次阅读
Android阴影实现的几种方案 1年以前  |  10920次阅读
Android 样式系统 | 主题背景覆盖 3年以前  |  9672次阅读
 目录