编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

为什么Android有65535限制,不仅仅是这些?

wxchong 2024-07-07 00:13:24 开源技术 11 ℃ 0 评论

一、什么是65535限制?

众所周知,随着Android平台持续增长,Android应用程序越来越大。在Android系统中,一个App的所有代码都在一个Dex文件里面。Dex是一个类似Jar的存储了多有Java编译字节码的归档文件。因为Android系统使用Dalvik虚拟机,所以需要把使用Java Compiler编译之后的class文件转换成Dalvik能够执行的class文件。这里需要强调的是,Dex和Jar一样是一个归档文件,里面仍然是Java代码对应的字节码文件。当Android系统启动一个应用的时候,有一步是对Dex进行优化,这个过程有一个专门的工具来处理,叫DexOpt。DexOpt的执行过程是在第一次加载Dex文件的时候执行的。这个过程会生成一个ODEX文件,即Optimised。DexOpt会把每一个类的方法id检索起来,存在一个链表结构里面。但是这个链表的长度是用一个short类型来保存的,导致了方法id的数目不能够超过65536个。



65536是什么样的数?2的16次方或者16进制的 0xFFFF

下边这个error是不是很熟悉

较高版本的Android构建系统下的提示(Android 7.0及以下):

Conversion to Dalvik format failed:
Unable to execute dex: method ID not in [0, 0xffff]: 65536

较高版本的Android构建系统的报错信息(Android 8.0)

trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.



二、为什么会出现64K的限制呢?

一般排查问题我们需要从问题本身入手,那么log是最重要的信息。

在构建流程中出现这种问题,根据提示我们大概明白方法数过大,而这些方法是存在于编译后的.class文件中的,而.class最后要存在于dex文件中。

那么如此分析的话,问题应该存在于dex的打包流程当中,这个需要以后深入了解一下。

根据前人的一些分析,我们来看看MemberIdsSection文件。 注意: 源码路径是 /dalvik/dx/src/com/android/dx/dex/file/MemberIdsSection.java 不是/dalvik/dexgen/src/com/android/dexgen/dex/file/MemberIdsSection.java 代码不多,如下:

/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dx.dex.file;

import com.android.dex.DexFormat;
import com.android.dex.DexIndexOverflowException;
import java.util.Formatter;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Member (field or method) refs list section of a {@code .dex} file.
 */
public abstract class MemberIdsSection extends UniformItemSection {

    /**
     * Constructs an instance. The file offset is initially unknown.
     *
     * @param name {@code null-ok;} the name of this instance, for annotation
     * purposes
     * @param file {@code non-null;} file that this instance is part of
     */
    public MemberIdsSection(String name, DexFile file) {
        super(name, file, 4);
    }

    /** {@inheritDoc} */
    @Override
    protected void orderItems() {
        int idx = 0;

        if (items().size() > DexFormat.MAX_MEMBER_IDX + 1) {
            throw new DexIndexOverflowException(getTooManyMembersMessage());
        }

        for (Object i : items()) {
            ((MemberIdItem) i).setIndex(idx);
            idx++;
        }
    }

    private String getTooManyMembersMessage() {
        Map<String, AtomicInteger> membersByPackage = new TreeMap<String, AtomicInteger>();
        for (Object member : items()) {
            String packageName = ((MemberIdItem) member).getDefiningClass().getPackageName();
            AtomicInteger count = membersByPackage.get(packageName);
            if (count == null) {
                count = new AtomicInteger();
                membersByPackage.put(packageName, count);
            }
            count.incrementAndGet();
        }

        Formatter formatter = new Formatter();
        try {
            String memberType = this instanceof MethodIdsSection ? "method" : "field";
            formatter.format("Too many %1$s references to fit in one dex file: %2$d; max is %3$d.%n" +
                            "You may try using multi-dex. If multi-dex is enabled then the list of " +
                            "classes for the main dex list is too large.%n" +
                    "References by package:",
                    memberType, items().size(), DexFormat.MAX_MEMBER_IDX + 1);
            for (Map.Entry<String, AtomicInteger> entry : membersByPackage.entrySet()) {
                formatter.format("%n%6d %s", entry.getValue().get(), entry.getKey());
            }
            return formatter.toString();
        } finally {
            formatter.close();
        }
    }

}

在48行到49中,我们看到如下可能抛出异常的情况

if (items().size() > DexFormat.MAX_MEMBER_IDX + 1) {
     throw new DexIndexOverflowException(getTooManyMembersMessage());
}

getTooManyMembersMessage()函数内(72行到77行)有如下异常信息字符串构造

String memberType = this instanceof MethodIdsSection ? "method" : "field";
           formatter.format("Too many %1$s references to fit in one dex file: %2$d; max is %3$d.%n" +
                            "You may try using multi-dex. If multi-dex is enabled then the list of " +
                            "classes for the main dex list is too large.%n" +
                    "References by package:",
                    memberType, items().size(), DexFormat.MAX_MEMBER_IDX + 1);

同时我们还要注意DexFormat类

 /**
     * Maximum addressable field or method index.
     * The largest addressable member is 0xffff, in the "instruction formats" spec as field@CCCC or
     * meth@CCCC.
     */
   public static final int MAX_MEMBER_IDX = 0xFFFF;

根据注释,我们来到Dalvik 字节码,根据表格中的解释如下图:

可以看到类型索引(16 位),由此可以知道,无论是方法数还是字段数都不能超过65536,这也就是为什么在构建流程中出现65536的报错信息。

由此可以得出结论:

invoke-kind (调用各类方法)指令中,方法引用索引数是 16 位的,也就是最多调用 2^16 = 65536 个方法,这就是 DexFormat 中 MAX_MEMBER_IDX 为 0xFFFF 的原因。 所以单个dex的方法或者字段数量不能超过65536



三、Android 官方是如何解决65536问题的?

官方提出了通过multidex包进行多dex编译的方法

1.导入multidex包,设置为支持多dex输出模式

android {
    compileSdkVersion 26
    buildToolsVersion "26.1.0"

    defaultConfig {
        ...
        minSdkVersion 14
        targetSdkVersion 26
        ...

        // Enabling multidex support.
        multiDexEnabled true
    }
    ...
}

afterEvaluate {
    tasks.matching {
        it.name.startsWith('dex')
    }.each { dx ->
        if (dx.additionalParameters == null) {
            dx.additionalParameters = []
        }
        dx.additionalParameters += '--multi-dex' // enable multidex

        // optional
        // dx.additionalParameters += "--main-dex-list=$projectDir/<filename>".toString() // enable the main-dex-list
    }
}

dependencies {
  compile 'com.android.support:multidex:1.0.1'
}

2.覆写Application类

如果你的工程中已经含有Application类,那么让它继承android.support.multidex.MultiDexApplication类,

如果你的Application已经继承了其他类并且不想做改动,那么还有另外一种使用方式,覆写attachBaseContext()方法。

import android.support.multidex.MultiDex;

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    MultiDex.install(this);
}

这样处理后,若代码函数超过65535, 就会生成多个dex文件。不会再报错。



四、MultiDex 对App的影响和解决方案


  • 使用 MultiDex 可能会造成什么问题?
  • MultiDex分包引起的app首次启动过慢:默认情况下,Dalvik会限制app只有一个class.dex字节码文件,multidex则可以取消这个限制。multidex会成为主dex文件的一部分,然后对其他分dex文件及其所访问的代码进行管理。在app首次启动的时候,主dex文件会进行复杂的计算来确定主dex文件所应该包含那些class文件,引起app首次启动过慢的原因就这里。

  • 使用 MultiDex 后首次启动 app 有什么优化方向吗?
  • 解决这个问题需要引入multidex.keep文件,来告诉主dex文件首次启动时不需要计算主dex包含的class文件,直接取multidex.keep列出来的文件即可。需要如下步骤,

    1、在build.gradle同目录中新建multidex.keep文件

    2、在build.gradle文件中指明主dex文件该包含的class文件应该从multidex.keep文件获取

    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"
                }
            }
        }



    3.在multidex.keep文件中添加主dex文件所需的class文件。主dex文件所需的class文件可以通过以下方法获取。调用MultiDexUtils的getLoadedExternalDexClasses方法即可获取所需的class文件的list。

    import android.content.Context;
    import android.content.SharedPreferences;
    import android.content.pm.ApplicationInfo;
    import android.content.pm.PackageManager;
    import android.os.Build;
    
    import java.io.File;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.Enumeration;
    import java.util.List;
    
    import dalvik.system.DexFile;
    
    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("#34;, ".class"));
                        }
                    }
                    return classList;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    }

    4.再把list数组写到手机sd卡的txt文件中,把txt文件的内容复制到multidex.keep即可。

    MultiDexUtils dexUtils = new MultiDexUtils();
            List<String> des = dexUtils.getLoadedExternalDexClasses(this);
    
            String sdCardDir = Environment.getExternalStorageDirectory().getAbsolutePath();
            File saveFile = new File(sdCardDir, "aaaa.txt");
            try {
                FileOutputStream outStream = new FileOutputStream(saveFile);
                outStream.write(listToString(des).getBytes());
                outStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
    public static String listToString(List<String> stringList){
            if(stringList==null) {
                return null;
            }
            StringBuilder result = new StringBuilder();
            boolean flag=false;
            for(String string : stringList) {
                if(flag) {
                    result.append("\n");
                }else{
                    flag=true;
                }
                result.append(string);
            }
            return result.toString();
        }


    以上是关于65535问题和解决方案的总结,欢迎大家转发留言进行交流。

    Tags:

    本文暂时没有评论,来添加一个吧(●'◡'●)

    欢迎 发表评论:

    最近发表
    标签列表