multidex_multidex原理 - CSDN
  • Android multidex 使用 与 实现原理

    千次阅读 2019-09-08 21:50:50
    Android multidex 使用 与 实现原理 在Android中一个Dex文件最多存储65536个方法,也就是一个short类型的范围。但随着应用方法数量的不断增加,当Dex文件突破65536方法数量时,打包时就会抛出异常。 为解决该问题,...

    Android multidex 使用 与 实现原理

    在Android中一个Dex文件最多存储65536个方法,也就是一个short类型的范围。但随着应用方法数量的不断增加,当Dex文件突破65536方法数量时,打包时就会抛出异常。

    为解决该问题,Android5.0时Google推出了官方解决方案:MultiDex。

    • 打包时,把一个应用分成多个dex,例:classes.dex、classes2.dex、classes3.dex…,加载的时候把这些dex都追加到DexPathList对应的数组中,这样就解决了方法数的限制。
    • Andorid 5.0之后,ART虚拟机天然支持MultiDex。
    • Andorid 5.0之前,系统只加载一个主dex,其它的dex采用MultiDex手段来加载。

    一、使用

    如何使用,最好参照google官方文档,写的很详细:

    配置方法数超过 64K 的应用

    这里做一下简要说明:

    1、minSdkVersion 为 21 或更高值

    如果是android 5.0以上的设备,只需要设置为multiDexEnabled true

    android {
        defaultConfig {
            ...
            minSdkVersion 21 
            targetSdkVersion 26
            multiDexEnabled true
        }
        ...
    }
    

    2、minSdkVersion 为 20 或更低值

    如果需要适配android 5.0以下的设备,需使用 Dalvik 可执行文件分包支持库

    android {
        defaultConfig {
            ...
            minSdkVersion 15 
            targetSdkVersion 26
            multiDexEnabled true
        }
        ...
    }
    
    dependencies {
      compile 'com.android.support:multidex:1.0.3'
    }
    

    Java代码方面,继承MultiDexApplication 或者 在Application中添加MultiDex.install(this);

    // 继承 MultiDexApplication
    public class MyApplication extends MultiDexApplication { ... }
    
    
    // 或者 在Application中添加 MultiDex.install(this);
    public class MyApplication extends Application {
      @Override
      protected void attachBaseContext(Context base) {
         super.attachBaseContext(base);
         MultiDex.install(this);
      }
    }
    
    

    二、android 5.0 以下 MultiDex 原理

    注:
    源码基于的版本 com.android.support:multidex:1.0.3

    通过 Dalvik可执行文件分包支持库配置方法数超过64K的应用 我们了解到:

    • android 5.0 以下Dalvik虚拟机 只能加载一个主class.dex
    • android.support.multidex.MultiDex.install(this)是对android 5.0 以下Dalvik虚拟机 的兼容;

    这里我们分两部分介绍,一部分是dex文件的加载;一部分是dex文件的抽取。

    2.1、Dex文件的加载

    下面通过跟踪 MultiDex.install(this); 源码,了解其实现原理。

    MultiDex.install(this);

    跟踪 MultiDex.install(this); 源码

    public static void install(Context context) {
        // 如果系统版本大于android 5.0 则天然支持MultiDex
        if (IS_VM_MULTIDEX_CAPABLE) {
            Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
        } 
        // 系统版本低于android 1.6 抛出异常
        else if (VERSION.SDK_INT < 4) {
            throw new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
        } 
        // android 1.6 < android < android 5.0
        else {
            try {
    	        // 获取当前应用信息 应用信息不存在,则返回
                ApplicationInfo applicationInfo = getApplicationInfo(context);
                if (applicationInfo == null) {
                    Log.i("MultiDex", "No ApplicationInfo available, i.e. running on a test Context: MultiDex support library is disabled.");
                    return;
                }
                // MultiDex
                // sourceDir: /data/app/com.xiaxl.demo-2/base.apk
                // dataDir:   /data/user/0/com.xiaxl.demo
                doInstallation(context, new File(applicationInfo.sourceDir), new File(applicationInfo.dataDir), "secondary-dexes", "", true);
            } catch (Exception var2) {
                Log.e("MultiDex", "MultiDex installation failure", var2);
                throw new RuntimeException("MultiDex installation failed (" + var2.getMessage() + ").");
            }
    
            Log.i("MultiDex", "install done");
        }
    }
    

    上边代码中,对1.6 < android < android 5.0 进行判断处理,低于1.6版本抛出异常;高于5.0版本,天然支持MultiDex,所以忽略

    • 如果系统版本大于android 5.0 ART虚拟机 天然支持MultiDex
    • 系统版本低于android 1.6 抛出异常
    • doInstallation MultiDex 处理

    跟踪 MultiDex.doInstallation

    跟踪 MultiDex.doInstallation,查看MultiDex的实现原理

    // 相关入口参数
    // sourceDir: /data/app/com.xiaxl.demo-2/base.apk
    // dataDir:   /data/user/0/com.xiaxl.demo
    // secondaryFolderName: "secondary-dexes"
    // prefsKeyPrefix: ""
    // reinstallOnPatchRecoverableException: true
    private static void doInstallation(Context mainContext, File sourceApk, File dataDir, String secondaryFolderName, String prefsKeyPrefix, boolean reinstallOnPatchRecoverableException) throws IOException, IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, SecurityException, ClassNotFoundException, InstantiationException {
    	// 已安装Apk
        Set var6 = installedApk;
        // 同步
        synchronized(installedApk) {
    	    // 如果 /data/app/com.xiaxl.demo-2/base.apk 未安装
            if (!installedApk.contains(sourceApk)) {
    	        // 添加到 installedApk 这个集合中
                installedApk.add(sourceApk);
                // Android 系统版本大约5.0("java.vm.version"的版本号错误),天然支持MultiDex
                if (VERSION.SDK_INT > 20) {
                    Log.w("MultiDex", "MultiDex is not guaranteed to work in SDK version " + 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") + "\"");
                }
                // 根据context 获取 ClassLoader
                ClassLoader loader;
                try {
    	            // 获取ClassLoader,实际上是PathClassLoader
                    loader = mainContext.getClassLoader();
                } catch (RuntimeException var25) {
                    Log.w("MultiDex", "Failure while trying to obtain Context class loader. Must be running in test mode. Skip patching.", var25);
                    return;
                }
                // ClassLoader 获取失败
                if (loader == null) {
                    Log.e("MultiDex", "Context class loader is null. Must be running in test mode. Skip patching.");
                }
                //  
    			else {
    				// 清除老的缓存的Dex目录,来源的缓存目录是"/data/user/0/${packageName}/files/secondary-dexes"
    				// 清空 /data/user/0/com.xiaxl.demo/files/secondary-dexes
                    try {
                        clearOldDexDir(mainContext);
                    } catch (Throwable var24) {
                        Log.w("MultiDex", "Something went wrong when trying to clear old MultiDex extraction, continuing without cleaning.", var24);
                    }
                    
                    // 新建一个存放dex的目录,路径是"/data/user/0/${packageName}/code_cache/secondary-dexes",用来存放优化后的dex文件
                    // 创建 /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes 目录
                    File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
                    
                    // 使用MultiDexExtractor这个工具类把APK中的dex抽取到dexDir目录中,返回的files集合有可能为空,表示没有secondaryDex
    	            // 不强制重新加载,也就是说如果已经抽取过了,可以直接从缓存目录中拿来使用,这么做速度比较快
                    // sourceApk: /data/app/com.xiaxl.demo-2/base.apk
                    // dexDir: /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
                    MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
                    IOException closeException = null;
    
                    try {
    		            // prefsKeyPrefix: ""
    		            // 返回dex文件列表
                        List files = extractor.load(mainContext, prefsKeyPrefix, false);
                        try {
    	                    // 安装secondaryDex
    	                    // /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
                            installSecondaryDexes(loader, dexDir, files);
                        } catch (IOException var26) {
                            if (!reinstallOnPatchRecoverableException) {
                                throw var26;
                            }
    
                            Log.w("MultiDex", "Failed to install extracted secondary dex files, retrying with forced extraction", var26);
                            files = extractor.load(mainContext, prefsKeyPrefix, true);
                            installSecondaryDexes(loader, dexDir, files);
                        }
                    } finally {
                        try {
                            extractor.close();
                        } catch (IOException var23) {
                            closeException = var23;
                        }
    
                    }
    
                    if (closeException != null) {
                        throw closeException;
                    }
                }
            }
        }
    }
    

    忽略dex文件抽取逻辑和校验逻辑,以上代码中主要做了以下三件事:

    • 清空缓存目录"/data/user/0/${packageName}/files/secondary-dexes"
    • 使用MultiDexExtractor这个工具把APK中的dex抽取到"/data/user/0/${packageName}/code_cache/secondary-dexes"目录
    • 加载"/data/user/0/${packageName}/code_cache/secondary-dexes"目录下的dex

    下边查看MultiDex.installSecondaryDexes方法,了解MultiDex的具体实现

    MultiDex.V4.install(loader, files);

    // dexDir:  /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
    private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<? extends File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException, SecurityException, ClassNotFoundException, InstantiationException {
        if (!files.isEmpty()) {
            if (VERSION.SDK_INT >= 19) {
                MultiDex.V19.install(loader, files, dexDir);
            } else if (VERSION.SDK_INT >= 14) {
                MultiDex.V14.install(loader, files);
            } else {
                MultiDex.V4.install(loader, files);
            }
        }
    }
    

    不同版本的Android系统,类加载机制有一些不同,所以分为了V19、V14和V4三种情况下的安装。

    这里我们看下一V19的源码

    private static final class V19 {
        private V19() {
        }
        // additionalClassPathEntries: dex列表
        // optimizedDirectory: /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
        static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
    	    // 传递的loader是PathClassLoader,findFidld()方法找到父类BaseClassLoader中pathList属性
    	    // 获取BaseDexClassLoader中pathList属性
    	    // this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
            Field pathListField = MultiDex.findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);
            // 将dex文件添加到DexPathList中的dexElements 数组的末尾
            ArrayList<IOException> suppressedExceptions = new ArrayList();
            MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
            // 后面就是添加一些IO异常信息,因为调用DexPathList的makeDexElements会有一些IO操作,相应的可能就会有一些异常情况
            if (suppressedExceptions.size() > 0) {
                Iterator var6 = suppressedExceptions.iterator();
    
                while(var6.hasNext()) {
                    IOException e = (IOException)var6.next();
                    Log.w("MultiDex", "Exception in makeDexElement", e);
                }
    
                Field suppressedExceptionsField = MultiDex.findField(dexPathList, "dexElementsSuppressedExceptions");
                IOException[] dexElementsSuppressedExceptions = (IOException[])((IOException[])suppressedExceptionsField.get(dexPathList));
                if (dexElementsSuppressedExceptions == null) {
                    dexElementsSuppressedExceptions = (IOException[])suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
                } else {
                    IOException[] combined = new IOException[suppressedExceptions.size() + dexElementsSuppressedExceptions.length];
                    suppressedExceptions.toArray(combined);
                    System.arraycopy(dexElementsSuppressedExceptions, 0, combined, suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
                    dexElementsSuppressedExceptions = combined;
                }
    
                suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);
                IOException exception = new IOException("I/O exception during makeDexElement");
                exception.initCause((Throwable)suppressedExceptions.get(0));
                throw exception;
            }
        }
    	
    	// 通过反射的方式调用DexPathList#makeDexElements()方法
    	// dexPathList: DexPathList
    	// files: dex文件列表
        private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
    	    // 通过DexPathList的makeDexElements方法加载 “dex文件”
            Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class);
            return (Object[])((Object[])makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions));
        }
    }
    

    通过V19的install()方法,关于MultiDex如何加载Dex文件的问题已经清晰:

    • 将APK文件中除主dex文件之外的dex文件追加到PathClassLoader(也就是BaseClassLoader)DexPathListde Element[]数组中。这样在加载一个类的时候就会遍历所有的dex文件,保证了打包的类都能够正常加载。

    Dex的加载到此完成,下边查看Dex的抽取逻辑~~~~~

    2.2、Dex文件的抽取

    前边说过:
    MultiDexExtractor这个工具类的作用是把APK中的dex文件抽取到/data/user/0/com.xiaxl.demo/code_cache/secondary-dexes目录中

    MultiDexExtractor 构造方法

    // sourceApk:  /data/app/com.xiaxl.demo-2/base.apk
    // dexDir:  /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
    MultiDexExtracto(File sourceApk, File dexDir) throws IOException {
        this.sourceApk = sourceApk;
        this.dexDir = dexDir;
        // 循环冗余校验码(CRC)
        this.sourceCrc = getZipCrc(sourceApk);
        // 创建 /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes/MultiDex.lock
        File lockFile = new File(dexDir, "MultiDex.lock");
        // 对文件内容的访问,既可以读文件也可以写文件,可以访问文件的任意位置适用于由大小已知的记录组成的文件
        // 对/data/user/0/com.xiaxl.demo/code_cache/secondary-dexes/MultiDex.lock 进行读写
        this.lockRaf = new RandomAccessFile(lockFile, "rw");
    
        try {
    	    // 返回文件通道
            this.lockChannel = this.lockRaf.getChannel();
    
            try {
                Log.i("MultiDex", "Blocking on lock " + lockFile.getPath());
                this.cacheLock = this.lockChannel.lock();
            } catch (RuntimeException | Error | IOException var5) {
                closeQuietly(this.lockChannel);
                throw var5;
            }
    
            Log.i("MultiDex", lockFile.getPath() + " locked");
        } catch (RuntimeException | Error | IOException var6) {
            closeQuietly(this.lockRaf);
            throw var6;
        }
    }
    

    MultiDexExtractor.load

    APK中的dex文件的抽取

    // 返回dex文件列表
    // prefsKeyPrefix: ""
    // forceReload: false
    List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload) throws IOException {
        // MultiDexExtractor 不可用
        if (!this.cacheLock.isValid()) {
            throw new IllegalStateException("MultiDexExtractor was closed");
        } else {
            List files;
            // forceReload ==false;
            // isModified == true;
            // 如果不需要重新加载并且文件没有被修改过
    	    // isModified()方法是根据SharedPreference中存放的APK文件上一次修改的时间戳和currentCrc来判断是否修改过文件
            if (!forceReload && !isModified(context, this.sourceApk, this.sourceCrc, prefsKeyPrefix)) {
                try {
    	            // 从缓存目录中加载已经抽取过的文件,并返回dex文件列表
                    files = this.loadExistingExtractions(context, prefsKeyPrefix);
                } catch (IOException var6) {
                    Log.w("MultiDex", "Failed to reload existing extracted secondary dex files, falling back to fresh extraction", var6);
                    files = this.performExtractions();
                    putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
                }
            } else {
                if (forceReload) {
                    Log.i("MultiDex", "Forced extraction must be performed.");
                } else {
                    Log.i("MultiDex", "Detected that extraction must be performed.");
                }
                // 如果强制加载或者APK文件已经修改过就重新抽取dex文件
                files = this.performExtractions();
                putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
            }
    
            Log.i("MultiDex", "load found " + files.size() + " secondary dex files");
            return files;
        }
    }
    

    MultiDexExtractor.performExtractions()

    private List<MultiDexExtractor.ExtractedDex> performExtractions() throws IOException {
    	//  抽取出的dex文件名前缀是"base.apk.classes"
        String extractedFilePrefix = this.sourceApk.getName() + ".classes";
        this.clearDexDir();
        // 返回的dex列表
        List<MultiDexExtractor.ExtractedDex> files = new ArrayList();
        // apk压缩包
        ZipFile apk = new ZipFile(this.sourceApk);
    
        try {
            int secondaryNumber = 2;
    
            for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {
    	        // base.apk.classes2.zip
                String fileName = extractedFilePrefix + secondaryNumber + ".zip";
                // 创建文件/data/app/com.xiaxl.demo-2/base.apk.classes2.zip
                MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(this.dexDir, fileName);
                // 添加到文件列表
                files.add(extractedFile);
                Log.i("MultiDex", "Extraction is needed for file " + extractedFile);
                int numAttempts = 0;
                boolean isExtractionSuccessful = false;
                // 抽取dex
                while(numAttempts < 3 && !isExtractionSuccessful) {
                    ++numAttempts;
                    extract(apk, dexFile, extractedFile, extractedFilePrefix);
    
                    try {
                        extractedFile.crc = getZipCrc(extractedFile);
                        isExtractionSuccessful = true;
                    } catch (IOException var18) {
                        isExtractionSuccessful = false;
                        Log.w("MultiDex", "Failed to read crc from " + extractedFile.getAbsolutePath(), var18);
                    }
    
                    Log.i("MultiDex", "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed") + " '" + extractedFile.getAbsolutePath() + "': length " + extractedFile.length() + " - crc: " + extractedFile.crc);
                    if (!isExtractionSuccessful) {
                        extractedFile.delete();
                        if (extractedFile.exists()) {
                            Log.w("MultiDex", "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;
            }
        } finally {
            try {
                apk.close();
            } catch (IOException var17) {
                Log.w("MultiDex", "Failed to close resource", var17);
            }
    
        }
    
        return files;
    }
    

    2.3、其他相关代码

    clearOldDexDir(Context context)

    private static void clearOldDexDir(Context context) throws Exception {
    	// /data/user/0/com.xiaxl.demo/files/secondary-dexes
        File dexDir = new File(context.getFilesDir(), "secondary-dexes");
        if (dexDir.isDirectory()) {
            Log.i("MultiDex", "Clearing old secondary dex dir (" + dexDir.getPath() + ").");
            // 获取文件列表
            File[] files = dexDir.listFiles();
            // 文件为空
            if (files == null) {
                Log.w("MultiDex", "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
                return;
            }
            // 文件不为空
            File[] var3 = files;
            int var4 = files.length;
            // 循环清空 /data/user/0/com.xiaxl.demo/files/secondary-dexes 下全部文件
            for(int var5 = 0; var5 < var4; ++var5) {
                File oldFile = var3[var5];
                Log.i("MultiDex", "Trying to delete old file " + oldFile.getPath() + " of size " + oldFile.length());
                if (!oldFile.delete()) {
                    Log.w("MultiDex", "Failed to delete old file " + oldFile.getPath());
                } else {
                    Log.i("MultiDex", "Deleted old file " + oldFile.getPath());
                }
            }
            // 删除 /data/user/0/com.xiaxl.demo/files/secondary-dexes 文件夹
            if (!dexDir.delete()) {
                Log.w("MultiDex", "Failed to delete secondary dex dir " + dexDir.getPath());
            } else {
                Log.i("MultiDex", "Deleted old secondary dex dir " + dexDir.getPath());
            }
        }
    }
    
    private static File getDexDir(Context context, File dataDir, String secondaryFolderName) throws IOException {
    	// 创建 /data/user/0/com.xiaxl.demo/code_cache 目录
        File cache = new File(dataDir, "code_cache");
        try {
            mkdirChecked(cache);
        } catch (IOException var5) {
            cache = new File(context.getFilesDir(), "code_cache");
            mkdirChecked(cache);
        }
        // 创建 /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes 目录
        File dexDir = new File(cache, secondaryFolderName);
        mkdirChecked(dexDir);
        return dexDir;
    }
    

    三、总结

    到这里,MultiDex安装多个dex的原理已经清楚了。

    • 通过一定的方式把dex文件抽取出来;
    • 把这些dex文件追加到DexPathList的Element[]数组的后面
      这个过程要尽可能的早,所以一般是在Application的attachBaseContext()方法中。

    另外,hotfix热修复技术,就是通过一定的方式把修复后的dex插入到DexPathList的Element[]数组前面实现修复后的class抢先加载。

    参考:

    Android源代码

    类加载机制系列3——MultiDex原理解析

    展开全文
  • MultiDex与热修复实现原理

    千次阅读 2018-09-26 11:10:32
    一、Android的ClassLoader体系 由上图可以看出,在叶子节点上,我们能使用到的是DexClassLoader和PathClassLoader,他们有如下使用场景: ...PathClassLoader是Android应用中的默认加载器,PathClassLoader只能...

    推荐书籍:
    深入探索Android热修复技术原理:
    https://pan.baidu.com/s/1lsDQdE1Z-_VDidtSGcpVrA
    提取码获取方式:扫描关注下面微信公众号,回复关键字: androidfix

    在这里插入图片描述

    一、Android的ClassLoader体系

    这里写图片描述

    由上图可以看出,在叶子节点上,我们能使用到的是DexClassLoader和PathClassLoader,他们有如下使用场景:

    1. PathClassLoader是Android应用中的默认加载器,PathClassLoader只能加载/data/app中的apk,也就是已经安装到手机中的apk。这个也是PathClassLoader作为默认的类加载器的原因,因为一般程序都是安装了,在打开,这时候PathClassLoader就去加载指定的apk(解压成dex,然后在优化成odex)就可以了。

    2. DexClassLoader可以加载任何路径的apk/dex/jar,PathClassLoader只能加载已安装到系统中(即/data/app目录下)的apk文件。

    从上面我们知道,DexClassLoader和PathClassLoader加载原理其实是一样的,就是使用场景不一样。

    二、DexClassLoader动态加载的实现

    第一步:创建DexClassLoader对象,加载对应的apk/dex/jar文件。

    is = getAssets().open("app.apk");
    file = new File(getFilesDir(), "plugin.apk");
    
    fos = new FileOutputStream(file);
    byte[] buffer = new byte[1024];
    int len;
    while ((len = is.read(buffer)) != -1) {
    	fos.write(buffer, 0, len);
    }
    fos.flush();
    String apkPath = file.getAbsolutePath();
    dexClassLoader = new DexClassLoader(apkPath, getFilesDir().getAbsolutePath(), null, getClassLoader());
    

    下面来看看DexClassLoader的构造函数

    public class DexClassLoader extends BaseDexClassLoader {
    	// dexPath:是加载apk/dex/jar的路径
    	// optimizedDirectory:是dex的输出路径(因为加载apk/jar的时候会解压除dex文件,这个路径就是保存dex文件的)
    	// libraryPath:是加载的时候需要用到的lib库,这个一般不用
    	// parent:给DexClassLoader指定父加载器
        public DexClassLoader(String dexPath, String optimizedDirectory,
                String libraryPath, ClassLoader parent) {
            super(dexPath, new File(optimizedDirectory), libraryPath, parent);
        }
    }
    

    可以看到它调用的是父类的构造函数,所以直接来看BaseDexClassLoader的构造函数。

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
    		String libraryPath, ClassLoader parent) {
    	super(parent);
    	this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
    

    可以看到,它创建了一个DexPathList实例,下面来看看构造函数。

    private final Element[] dexElements;
    
    // definingContext对应的就是当前classLoader
    // dexPath对应的就是上面传进来的apk/dex/jar的路径
    // libraryPath就是上面传进来的加载的时候需要用到的lib库的目录,这个一般不用
    // optimizedDirectory就是上面传进来的dex的输出路径
    public DexPathList(ClassLoader definingContext, String dexPath,
    		String libraryPath, File optimizedDirectory) {
    	ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
    	this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
    									   suppressedExceptions);
    }
    

    可以看到它调用的是makeDexElements方法,这个方法就是得到一个装有dex文件的数组Element[],每个Element对象里面包含一个DexFile对象成员,它对应的就是dex文件。

    static class Element {
    	private final File file; 
    	private final boolean isDirectory; 
    	private final File zip;
    	private final DexFile dexFile;
    	......
    }
    

    具体的我们后面再说,下面先看看makeDexElements方法。

    // files是一个ArrayList<File>列表,它对应的就是apk/dex/jar文件,因为我们可以指定多个文件。
    // optimizedDirectory是前面传入dex的输出路径
    // suppressedExceptions为一个异常列表
    private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
    										 ArrayList<IOException> suppressedExceptions) {
    	ArrayList<Element> elements = new ArrayList<Element>();
    	/*
    	 * Open all files and load the (direct or contained) dex files
    	 * up front.
    	 */
    	for (File file : files) {
    		File zip = null;
    		DexFile dex = null;
    		String name = file.getName();
    		
    		// 如果是一个dex文件
    		if (name.endsWith(DEX_SUFFIX)) {
    			// Raw dex file (not inside a zip/jar).
    			try {
    				dex = loadDexFile(file, optimizedDirectory);
    			} catch (IOException ex) {
    				System.logE("Unable to load dex file: " + file, ex);
    			}
    		// 如果是一个apk或者jar或者zip文件
    		} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
    				|| name.endsWith(ZIP_SUFFIX)) {
    			zip = file;
    
    			try {
    				dex = loadDexFile(file, optimizedDirectory);
    			} catch (IOException suppressed) {
    				/*
    				 * IOException might get thrown "legitimately" by the DexFile constructor if the
    				 * zip file turns out to be resource-only (that is, no classes.dex file in it).
    				 * Let dex == null and hang on to the exception to add to the tea-leaves for
    				 * when findClass returns null.
    				 */
    				suppressedExceptions.add(suppressed);
    			}
    		} else if (file.isDirectory()) {
    			// We support directories for looking up resources.
    			// This is only useful for running libcore tests.
    			elements.add(new Element(file, true, null, null));
    		} else {
    			System.logW("Unknown file type for: " + file);
    		}
    
    		if ((zip != null) || (dex != null)) {
    			elements.add(new Element(file, false, zip, dex));
    		}
    	}
    
    	return elements.toArray(new Element[elements.size()]);
    }
    

    前面我们提到过Element,它里面具体包含哪些元素,现在从上面代码我们就可以知道了。

    static class Element {
    	private final File file;  // 它对应的就是需要加载的apk/dex/jar文件
    	private final boolean isDirectory; // 第一个参数file是否为一个目录,一般为false,因为我们传入的是要加载的文件
    	private final File zip;  // 如果加载的是一个apk或者jar或者zip文件,该对象对应的就是该apk或者jar或者zip文件
    	private final DexFile dexFile; // 它是得到的dex文件
    	......
    }
    

    上面我们可以看到,它调用的是loadDexFile方法。

    // file为需要加载的apk/dex/jar文件
    // optimizedDirectorydex的输出路径
    private static DexFile loadDexFile(File file, File optimizedDirectory)
    		throws IOException {
    	if (optimizedDirectory == null) {
    		return new DexFile(file);
    	} else {
    		String optimizedPath = optimizedPathFor(file, optimizedDirectory);
    		return DexFile.loadDex(file.getPath(), optimizedPath, 0);
    	}
    }
    

    如果我们没有指定dex输出目录的话,就直接创建一个DexFile对象,如果我们指定了dex输出目录,我们就需要构造dex输出路径。

    optimizedPathFor方法用来得到输出文件dex路径,就是optimizedDirectory/filename.dex,optimizedDirectory是前面指定的输出目录,filename就是加载的文件名,后缀为.dex,最终构造得到一个输出dex文件路径.

    下面我们重点看看DexFile.loadDex方法。

    static public DexFile loadDex(String sourcePathName, String outputPathName,
    	int flags) throws IOException {
    	return new DexFile(sourcePathName, outputPathName, flags);
    }
    

    下面我们就不往下看了,我们这里可以进行总结。

    1、在DexClassLoader我们指定了加载的apk/dex/jar文件和dex输出路径optimizedDirectory,它最终会被解析得到DexFile文件。
    2、将DexFile文件对象放在Element对象里面,它对应的就是Element对象的dexFile成员变量。
    3、将这个Element对象放在一个Element[]数组中,然后将这个数组返回给DexPathList的dexElements成员变量。
    4、DexPathList是BaseDexClassLoader的一个成员变量。

    最终得到一个装有dex文件的数组Element[],每个Element对象里面包含一个DexFile对象成员,它对应的就是dex文件。

    这里写图片描述

    第二步:调用dexClassLoader的loadClass,得到加载的dex里面的指定的Class.

    clazz = dexClassLoader.loadClass("com.example.apkplugin.PluginTest");
    

    下面我们来分析一下loadClass方法。因为DexClassLoader和BaseDexClassLoader都没有实现loadClass方法,所以最终调用的是ClassLoader的loadClass方法。

    public Class<?> loadClass(String className) throws ClassNotFoundException {
    	return loadClass(className, false);
    }
    
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
    	Class<?> clazz = findLoadedClass(className);
    
    	if (clazz == null) {
    		ClassNotFoundException suppressed = null;
    		try {
    			clazz = parent.loadClass(className, false);
    		} catch (ClassNotFoundException e) {
    			suppressed = e;
    		}
    
    		if (clazz == null) {
    			try {
    				clazz = findClass(className);
    			} catch (ClassNotFoundException e) {
    				e.addSuppressed(suppressed);
    				throw e;
    			}
    		}
    	}
    
    	return clazz;
    }
    

    可以看到它调用的是findClass方法,由于DexClassLoader没有实现这个方法,所以我们看BaseDexClassLoader的findClass

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    	List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    	Class c = pathList.findClass(name, suppressedExceptions);
    	if (c == null) {
    		ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
    		for (Throwable t : suppressedExceptions) {
    			cnfe.addSuppressed(t);
    		}
    		throw cnfe;
    	}
    	return c;
    }
    

    pathList就是前面创建的DexPathList对象,从上面我们知道,我们加载的dex文件都存放在它的exElements成员变量上面,dexElements就是Element[]数组,所以可以看到BaseDexClassLoader的findClass方法调用的是pathList的findClass方法,我们具体来看看。

    可以看到BaseDexClassLoader的findClass方法调用的是DexPathList的findClass方法。

    public Class findClass(String name, List<Throwable> suppressed) {
    	for (Element element : dexElements) {
    		DexFile dex = element.dexFile;
    
    		if (dex != null) {
    			Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
    			if (clazz != null) {
    				return clazz;
    			}
    		}
    	}
    	if (dexElementsSuppressedExceptions != null) {
    		suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    	}
    	return null;
    }
    

    可以看到它就是遍历dexElements数组,从每个Element对象中拿到DexFile类型的dex文件,然后就是从dex去加载所需要的class文件,直到找到为止。

    **总结:**一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。

    三、MultiDex基本原理
    当一个app的功能越来越复杂,代码量越来越多,可以遇到下面两种情况:

    1. 生成的apk在2.3以前的机器无法安装,提示INSTALL_FAILED_DEXOPT
    2. 方法数量过多,编译时出错,提示:Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536

    原因:

    1. Android2.3及以前版本用来执行dexopt(用于优化dex文件)的内存只分配了5M
    2. 一个dex文件最多只支持65536个方法。

    解决方案:
    1、使用Multidex,将编译好的class文件拆分打包成两个dex,绕过dex方法数量的限制以及安装时的检查,在运行时再动态加载第二个dex文件中。
    2、使用插件化,将功能模块分离,减少宿主apk的大小和代码。

    插件化我们这里先不讨论,这里主要来说说Multidex的原理。

    基本原理:
    1、除了第一个dex文件(即正常apk包唯一包含的Dex文件),其它dex文件都以资源的方式放在安装包中。所以我们需要将其他dex文件并在Application的onCreate回调中注入到系统的ClassLoader。并且对于那些在注入之前已经引用到的类(以及它们所在的jar),必须放入第一个Dex文件中。

    2、PathClassLoader作为默认的类加载器,在打开应用程序的时候PathClassLoader就去加载指定的apk(解压成dex,然后在优化成odex),也就是第一个dex文件是PathClassLoader自动加载的。所以,我们需要做的就是将其他的dex文件注入到这个PathClassLoader中去。

    3、因为PathClassLoader和DexClassLoader的原理基本一致,从前面的分析来看,我们知道PathClassLoader里面的dex文件是放在一个Element数组里面,可以包含多个dex文件,每个dex文件是一个Element,所以我们只需要将其他的dex文件放到这个数组中去就可以了。

    实现:
    1、通过反射获取PathClassLoader中的DexPathList中的Element数组(已加载了第一个dex包,由系统加载)
    2、通过反射获取DexClassLoader中的DexPathList中的Element数组(将第二个dex包加载进去)
    3、将两个Element数组合并之后,再将其赋值给PathClassLoader的Element数组

    谷歌提供的MultiDex支持库就是按照这个思路来实现的,我们可以直接来看看源码。

    首先来看看使用:
    1、修改Gradle的配置,支持multidex:

    android {
        compileSdkVersion 21
        buildToolsVersion "21.1.0"
        defaultConfig {
            ...
            minSdkVersion 14
            targetSdkVersion 21
            ...
            // Enabling multidex support.
            multiDexEnabled true
        }
        ...
    }
    dependencies {
      compile 'com.android.support:multidex:1.0.0'
    }
    

    在manifest文件中,在application标签下添加MultidexApplication Class的引用,如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.android.multidex.myapplication">
        <application
            ...
            android:name="android.support.multidex.MultiDexApplication">
            ...
        </application>
    </manifest>
    	
    

    使用起来很简单,下面我们来看看源码,看是不是按照前面介绍的思路实现的。

    首先我们来看看MultiDexApplication类。

    public class MultiDexApplication extends Application {
        public MultiDexApplication() {
        }
    
        protected void attachBaseContext(Context base) {
            super.attachBaseContext(base);
            MultiDex.install(this);
        }
    }
    

    原来要求使用MultiDexApplication的原因就是它重写了Application,主要是为了将其他dex文件注入到系统的ClassLoader。

    进入MultiDex.install(this)方法。

    public static void install(Context context) {
    	if(IS_VM_MULTIDEX_CAPABLE) {
    		Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
    	// 可以看到,MultiDex不支持SDK版本小于4的系统
    	} else if(VERSION.SDK_INT < 4) {
    		throw new RuntimeException("Multi dex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
    	} else {
    		try {
    		    // 获取到应用信息
    			ApplicationInfo e = getApplicationInfo(context);
    			if(e == null) {
    				return;
    			}
    
    			Set var2 = installedApk;
    			synchronized(installedApk) {
    				// 得到我们这个应用的apk文件路径
    				// 拿到这个apk文件路径之后,后面就可以从中提取出其他的dex文件
    				// 并且加载dex放到一个Element数组中
    				String apkPath = e.sourceDir;
    				if(installedApk.contains(apkPath)) {
    					return;
    				}
    				// 将这个apk文件路径放到一个set中
    				installedApk.add(apkPath);
    				
    				// 得到classLoader,它就是PathClassLoader
    				// 后面就可以从这个PathClassLoader中拿到DexPathList中的Element数组
    				// 这个数组里面就包括由系统加载第一个dex包
    				ClassLoader loader;
    				try {
    					loader = context.getClassLoader();
    				} catch (RuntimeException var9) {
    					Log.w("MultiDex", "Failure while trying to obtain Context class loader. Must be running in test mode. Skip patching.", var9);
    					return;
    				}
    				
    				// 得到apk解压后得到的dex文件的存放目录,放到应用的data目录下
    				File dexDir = new File(e.dataDir, SECONDARY_FOLDER_NAME);
    				
    				// 这个方法就是从apk中提取dex文件,放到data目录下,就不展开了
    				List files = MultiDexExtractor.load(context, e, dexDir, false);
    				if(checkValidZipFiles(files)) {
    					// 这个方法就是将其他的dex文件注入到系统classloader中的具体操作
    					installSecondaryDexes(loader, dexDir, files);
    				} else {
    					files = MultiDexExtractor.load(context, e, dexDir, true);
    					installSecondaryDexes(loader, dexDir, files);
    				}
    			}
    		} catch (Exception var11) {
    			Log.e("MultiDex", "Multidex installation failure", var11);
    			throw new RuntimeException("Multi dex installation failed (" + var11.getMessage() + ").");
    		}
    	}
    }
    

    下面我们重点看看installSecondaryDexes方法。

    // loader对应的就是PathClassLoader
    // dexDir是dex的存放目录
    // files对应的就是dex文件
    private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
    	if(!files.isEmpty()) {
    		if(VERSION.SDK_INT >= 19) {
    			MultiDex.V19.install(loader, files, dexDir);
    		} else if(VERSION.SDK_INT >= 14) {
    			MultiDex.V14.install(loader, files, dexDir);
    		} else {
    			MultiDex.V4.install(loader, files);
    		}
    	}
    
    }
    

    可以看到不同的sdk版本实现是有差别的,因为它里面是使用反射实现的,所以会有不同,我们看看MultiDex.V14.install方法。

    private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
    	// 这个方法就是使用反射来得到loader的pathList字段
    	Field pathListField = MultiDex.findField(loader, "pathList");
    	// 得到loader的pathList字段后,我们就可以得到这个字段的值,也就是DexPathList对象
    	Object dexPathList = pathListField.get(loader);
    	// 这个方法就是将其他的dex文件Element数组和第一个dex的Element数组合并
    	// makeDexElements方法就是用来得到其他dex的Elements数组
    	MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory));
    }
    

    下面来看看合并的过程

    // instance对应的就是pathList对象
    // fieldName 对应的就是字段名,我们要得到的就是pathList对象里面的dexElements数组
    // extraElements对应的就是其他dex对应的Element数组
    private static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
    	// 得到Element数组字段
    	Field jlrField = findField(instance, fieldName);
    	// 得到pathList对象里面的dexElements数组
    	Object[] original = (Object[])((Object[])jlrField.get(instance));
    	// 创建一个新的数组用来存放合并之后的结果
    	Object[] combined = (Object[])((Object[])Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length));
    	// 将第一个dex的Elements数组复制到创建的数组中去
    	System.arraycopy(original, 0, combined, 0, original.length);
    	// 将其他dex的Elements数组复制到创建的数组中去
    	System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
    	// 将得到的这个合并的新数组的值设置到pathList对象的Element数组字段上
    	jlrField.set(instance, combined);
    }
    

    整体思路跟上面说的基本一致,理解思路,结合上面的注释基本还是比较清楚的。

    四、热修复的一种实现原理

    一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。

    理论上,如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类,如下图:
    这里写图片描述

    所以,如果某些类需要修复,我们可以把有问题的类打包到一个dex(patch.dex)中去,然后把这个dex插入到Elements的最前面,如下图:
    这里写图片描述

    具体的方案可以参看文章:安卓App热补丁动态修复技术介绍

    使用该原理的开源方案有:
    Nuwa
    https://github.com/jasonross/Nuwa

    HotFix
    https://github.com/dodola/HotFix

    DroidFix
    https://github.com/bunnyblue/DroidFix

    参考文章:
    Android中插件开发篇之----类加载器

    Android dex分包方案

    Android分包原理

    欢迎关注微信公众号:DroidMind
    精品内容独家发布平台
    20170619210700424
    呈现与博客不一样的技术干货
    展开全文
  • Android关于Dex拆分 MultiDex 技术详解

    千次阅读 2018-11-15 11:11:27
    Android关于Dex拆分 MultiDex 技术详解

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow

    也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!

                   

    一、前言

    关于Android中的分包技术,已经不是什么新的技术了,网上也有很多解析了,但是他们都是给了理论上的知道和原理解析,并没有详细的案例说明,所以这里我们就来详细讲解一下Android中dex拆分技术的解析。在讲解之前,我们还是先来看一下为什么有这个技术的出现?google为什么提供这样的技术。


    二、背景

    在开发应用时,随着业务规模发展到一定程度,不断地加入新功能、添加新的类库,代码在急剧的膨胀,相应的apk包的大小也急剧增加, 那么终有一天,你会不幸遇到这个错误:
    生成的apk在android 2.3或之前的机器上无法安装,提示:INSTALL_FAILED_DEXOPT
    方法数量过多,编译时出错,提示:
    Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536

    无法安装(Android 2.3 INSTALL_FAILED_DEXOPT)问题,是由dexopt的LinearAlloc限制引起的,在Android版本不同分别经历了4M/5M/8M/16M限制,目前主流4.2.x系统上可能都已到16M, 在Gingerbread或者以下系统LinearAllocHdr分配空间只有5M大小的, 高于Gingerbread的系统提升到了8M。Dalvik linearAlloc是一个固定大小的缓冲区。在应用的安装过程中,系统会运行一个名为dexopt的程序为该应用在当前机型中运行做准备。dexopt使用LinearAlloc来存储应用的方法信息。Android 2.2和2.3的缓冲区只有5MB,Android 4.x提高到了8MB或16MB。当方法数量过多导致超出缓冲区大小时,会造成dexopt崩溃。
    超过最大方法数限制的问题,是由于DEX文件格式限制,一个DEX文件中method个数采用使用原生类型short来索引文件中的方法,也就是4个字节共计最多表达65536个method,field/class的个数也均有此限制。对于DEX文件,则是将工程所需全部class文件合并且压缩到一个DEX文件期间,也就是Android打包的DEX过程中, 单个DEX文件可被引用的方法总数(自己开发的代码以及所引用的Android框架、类库的代码)被限制为65536。

    我们知道原因了,但是我们可以看到,google提供了一个方案:Multidex技术来解决这样的问题,为了兼容老版本SDK,但是我们想一下,是不是所有的项目都会用到这个技术呢?答案肯定不是的,这种问题不是所有的项目都会遇到的,只有当你的项目足够庞大,导致方法个数超出限制了,才会使用到这种技术。那么既然要用到,这里就还是要介绍一下,在介绍这篇文章之前,我们先要准备哪些知识点呢?

    1、了解如何使用Ant脚本编译出一个apk包

    2、了解编译一个apk包出来的整个流程和步骤

    3、了解Android中的dx命令和aapt命令的用法

    4、了解Android中动态加载机制

    关于这些资料,我在之前的文章中有说道,不了解的同学可以转战:

    Ant脚本编译一个apk包以及apk包打包的整个流程和步骤可以查看这篇文章:

    http://blog.csdn.net/jiangwei0910410003/article/details/50740026

    关于Android中的动态加载机制不了解的同学可以查看这篇文章:

    http://blog.csdn.net/jiangwei0910410003/article/details/48104581

    只有了解了这四个知识点,下面介绍的内容才能看的明白。所以这两篇文章希望能够仔细看一下。


    三、技术原理

    既然准备知识已经做完了,下面我们就来详细介绍一下拆包的技术原理,其实就是google提供个方案:

    第一步:使用dx进行拆包

    这里还有两个两类:一个是自动拆包,一个手动拆包

    1、自动拆包

    使用google在5.0+的SDK开始,dx命令就已经支持:--multi-dex 参数来直接自动分包


    关于具体命令如何进行拆包,后面讲到案例的时候在详细讲解

    2、手动拆包

    手动拆包其实就是为了解决低版本的SDK,不支持--mulit-dex参数这种情况,所以我们得想个办法,我们知道,android中有一个步骤就是使用dx命令将class文件变成dex,那么这里我们就可以这么做了,在dx命令执行前,先将javac编译之后的所有class文件进行分类,然后在对具体的分类使用dx来转化成dex文件,这样结果也是有多个dex了。这时候我们可能需要写一个额外的脚本去进行class分类了,具体分多少种dex,那就自己决定了。

    上面就说完了拆包的两种方式,其实这两种方式各有优势和缺点,当然我们提倡的还是使用第一种方式,因为现在SDK已经是5.0+了,而且这种方式也是google所倡导的,而且也方便。

    关于上面的两种分包技术,有一个共同需要注意的地方

    就是Android中在分包之后会有多个dex,但是系统默认会先找到classes.dex文件然后自动加载运行,所以这里就有一个问题,我们需要将一些初始化的重要类放到classes.dex中,不然运行就会报错或者闪退。

    那么这里就可以看到这两种分包技术的区别了:

    如果使用第一种分包技术的话,我们可以使用:--main-dex-list 参数来规定哪些class文件归到主dex中,也就是classes.dex中,剩余的dx会自动更具方法数的限制来进行分类成从dex,比如classes2.dex...classes3.dex...这里没有classes1.dex。需要注意这点。同时,subdex是不支持class的归类的,完全依靠dx自动分类,这个也是为什么叫做自动拆包的原因吧。


    但是如果我们要是使用第二种拆包技术的话,就是很随意的操作了,因为本身分类就是我们自己操作的,所以想怎么分就怎么分,而且这里支持分多少个dex,哪些class归到哪个dex,都是可以做到的。灵活性比较好,但是这种方式有一个不好就是需要自己写脚本去归类,这时候就要非常小心,因为可能会遗漏一个class没有归入到具体的dex的话,运行就会报错,找不到这个类。


    第二步:Dex的加载

    在第一步中我们讲解完了拆包技术,如何将class拆分成多个dex。那么问题也就来了,上面我们也说到了,Android中在运行的时候默认只会加载classes.dex这个dex文件,我们也叫作主dex.那么拆分之后还有其他的dex怎么加载到系统运行呢?这时候google就提供了一个方案,使用DexClassLoader来进行动态加载,关于动态加载dex的话,这里不想讲解的太多了,因为我之前的很多文章都介绍了,具体可以参考这篇文章:http://blog.csdn.net/jiangwei0910410003/article/details/48104581

    所以我们只要获取到所有的dex,除了classes.dex之外。我们然后一一的将各个dex加载到系统中即可。那么这里有两个问题需要解决的:

    1、如何获取所有的subdex呢?

    这里我们可以这么做,在Application的attachContext方法中(因为这个方法的时机最早),我们可以获取到程序的apk路径,然后使用ZipFile来解压apk文件,取到所有的subdex文件即可,具体的操作看后面的案例详解。

    2、我们使用DexClassLoader来加载subdex之后,然后让系统知道?

    这里我们使用反射的机制,来合并系统的PathClassLoader和DexClassLoader中的DexList变量值。这个技术也是google提供的。具体技术,也是到后面的案例中我们详细讲解。


    四、案例分析

    到这里我们就介绍完了,我们拆包的两个步骤:拆分dex和加载dex;下面来使用具体的案例来实践一下吧,这个也是网上现在很多介绍了dex拆分技术,但是就是没有具体案例的不好的地方,理论知识谁都知道,但是没有案例讲解的话,就不知道在实际的过程中会遇到什么问题,以及详细的操作步骤,所以这里必须用一个案例来详细介绍一下。

    我们的案例采用ant脚本来编译,其实现在google推荐的是gradle来编译,因为这个已经集成到AndroidStudio中了,说句实话,我是因为gradle的语法不太熟,所以就没用gradle来讲解了,当然gradle语法和ant语法很想,如果有同学会gradle的话,可以使用gradle来进行操作一次,这里我不会太多的介绍ant脚本语法,而是介绍详细的命令使用。其实所有的编译脚本理论上就是对编译命令的优化已经加上一些功能罢了。好了,不多说,看案例:


    这里的 main-dex-rule.txt 是我们后面需要用到的主dex包含的class文件清单,my.keystore是签名apk文件

    下面我们先来看一下编译脚本build.xml

    这里再次说明一下,这里不会全部解读,这个脚本是在我之前的一篇文章:

    http://blog.csdn.net/jiangwei0910410003/article/details/50740026

    中的用到的脚本基础上修改的,所以一定要先看这篇文章呀~~

    首先来看一下如何使用dx命令来自动拆分dex的:

    <target  name="multi-dex" depends="compile"> <echo>  Generate multi-dex... </echo> <exec  executable="${tools.dx}"  failonerror="true">   <arg value="--dex" />   <arg value="--multi-dex" />   <arg value="--set-max-idx-number=2000" />   <arg value="--main-dex-list" />   <arg value="${main-dex-rule}" />   <arg value="--minimal-main-dex" />   <arg value="--output=${bin}" />   <arg value="${bin}" /> </exec></target>
    这里的参数很简单:

    参数说明:
    --multi-dex:多 dex 打包的开关
    --main-dex-list=<file>:参数是一个类列表的文件,在该文件中的类会被打包在第一个 dex 中
    --minimal-main-dex:只有在--main-dex-list 文件中指定的类被打包在第一个 dex,其余的都在第二个 dex 文件中

    因为后两个参数是 optional 参数,所以理论上只需给 dx 加上“--multi-dex”参数即可生成出 classes.dex、classes2.dex、classes3.dex等。

    这里我们指定了--main-dex-list 参数,将指定的class合并到classes.dex中,看看main-dex-rule.txt的内容:


    这里我们将程序的MyApplication类和入口Activity以及需要加载dex的这三个类合并到主dex中,因为这三个是初始化时机最早的,而且也是加载后面subdex类的重要类,所以必须放到主dex中。不然程序都运行不起来的。当然也要注意内部类的情况,也要进行添加的。

    好了,我们运行build之后,发现有一个问题,就是我们只看到了一个dex,那就是classes.dex。为什么会这样呢?

    再看 dx 的参数,main-dex-list 和 minimal-main-dex 只会影响到主 dex 中包含的文件,不会影响到从 dex 是否生成,所以应该是其他原因造成的。
    查不到资料,分析源代码就是解决问题的不二法门。于是我把 dx.jar 反编译了一下,通过分析,找到了下面的几行关键代码:


    显然,dx 进行多 dex 打包时,默认每个 dex 中方法数最大为65536。而查看当前应用 dex 的方法数,一共只有51392(方法数没超标,主要是 LinearAlloc 超标),没有达到65536,所以打包的时候只有一个 dex。
    再继续分析代码,发现下面一段关键代码:


    这说明 dx 有一个隐藏的参数:--set-max-idx-number,这个参数可以用于设置 dx 打包时每个 dex 最大的方法数,但是此参数并未在 dx 的 Usage 中列出(坑爹啊!)。


    我们在 ant 脚本中把这个参数设置上,暂时设置每个 dex 的方法数最大为:2000,然后在编译:


    好吧,这下出来了两个dex了,这里我可以发现,没有classes1.dex的,下标是从2开始的,这个需要注意的。

    这里我们可以用IDA查看dex文件:

    classes.dex:


    classes2.dex:


    看来是成功了,那么下面我们在继续看,现在有了两个dex文件,下面如何编译成apk文件呢?

    我们之前知道这时候可以在使用apkbuilder命令将我们编译完的:resource.arsc,asset,dex文件打包成apk文件。

    但是这里遇到一个蛋疼问题来了:

    apkbuilder命令支持的参数中,只能包含一个dex文件,说白了就是只能打入一个dex文件。

    这下蛋疼了,该怎么办呢?这里当时急需验证结果,先简单弄了一下,也算是一个小技巧,直接使用WinRar软件直接把subdex导进去:


    然后在单独签名apk,运行,可以了,心情也是很好的,但是感觉这个不能自动化呀。太黑了,所以得想个办法,想到了ZipFile这个类,我们可以解压apk,然后将subdex添加进去,然后把这部分功能打包成一个可执行的jar,然后在build.xml中运行即可。这个也是个方法,但是还是感觉不好,有点繁琐。思前想后,最后找到了一个好办法,也是一个很重要的知识点,那就是使用aapt命令来进行操作,我们在之前可能知道aapt命令用来编译资源文件的,其实他还有一个重要的用途就是可以编辑apk文件:


    可以看到,我们可以删除apk中的一个文件,或者是添加一个文件,好的,方法终于找到了,下面就直接操作吧,首先我们单独用命令来操作一下看看效果:


    成功了,接下来我们在来看看apk文件:


    尼玛,发现还不对,这里有一个坑,就是我们没看到classes2.dex,而看到了一个目录结构,哦,想想应该明白了,aapt命令在添加或者删除的时候,文件的路径必须明确,不能是绝对路径,而是相对路径,这个相对就要相对apk的跟目录,我们现在想把classes2.dex放到apk的根目录下面,那么就应该直接是classes2.dex,我们可以将dex和apk放到一个目录下面来进行操作:


    这时候我们在来看一下apk文件:


    好吧,成功了,所以这里我们需要注意的是subdex的路径。

    下面我们继续来看build.xml脚本内容,看看在拆包成功之后,如何打包成apk中:

    <!-- 拷贝文件到项目的根目录下面,因为我们的脚本是在根目录下面,这样在运行aapt的时候,可以直接操作dex文件了 --><target name="copy_dex" depends="build-unsigned-apk"> <echo message="copy dex..." /> <copy todir="${project-dir}">  <fileset dir="${bin}" >   <include name="classes*.dex" />  </fileset> </copy></target><!-- 使用aapt命令添加dex文件--><target name="aapt-add-dex">  <echo message="${dir.name}" /> <!-- 使用正则表达式获取classes的文件名--> <propertyregex   property="dexfile"    input="${dir.name}"    regexp="classes(.*).dex"   select="\0"    casesensitive="false" />   <!-- 这里不需要添加classes.dex文件 --> <if>    <equals arg1="${dexfile}" arg2="classes.dex"/>    <then>    <echo >${dexfile} is not handle</echo>    </then>   <else>    <echo>${dexfile} is handle</echo> <exec  executable="${tools.aapt}"  failonerror="true" >  <arg value="add" />  <arg value="${unsigned-apk-path}" />  <arg value="${dexfile}" /> </exec> </else>   </if>  <delete file="${project-dir}\${dexfile}" /> </target>  <!-- 循环遍历bin目录下的所有dex文件  --><target name="add-subdex-toapk" depends="copy_dex"> <echo message="Add Subdex to Apk ..." /> <foreach target="aapt-add-dex" param="dir.name">   <path>    <fileset dir="${bin}" includes="classes*.dex"/>   </path>  </foreach> </target> 
    这里的大体流程是这样的:

    1、遍历bin目录下所有的dex文件

    2、将dex文件先拷贝到项目的根目录下面,因为我们的脚本文件是在根目录下面,运行脚本的时候也是在根目录下面,所以需要把dex文件拷贝到这里,不然会发生上面说到的问题,不能使用绝对路径,而是相对路径来添加dex文件。

    3、因为apk默认的话是包含了classes.dex文件,所以这里需要过滤classes.dex文件,对其他的dex文件进行aapt命令进行添加。

    但是这里当时还遇到了一个问题,就是在ant脚本中如何使用循环,正则表达式,条件判断等标签:

    具体标签不说了,主要说明一下如何使用这些标签?因为默认情况下,这些标签是不能用的,所以需要导入jar包?

    <taskdef resource="net/sf/antcontrib/antlib.xml" />
    网上光说导入这个定义,但是还是报错的:


    这时候我们还需要把:ant-contrib-1.0b3.jar 放到ant的lib目录下面,默认是没有这个jar的。关于这个jar,网上很多,自行下载即可

    这时候我们就可以运行build.xml脚本了。


    上面就介绍完了使用脚本拆包,以及如何将多个dex打包成apk.


    下面来看一下代码如何实现:

    代码中我们定义了四个Activity:


    MainActivity是我们的入口Activity,SecondaryDexEx是动态加载的核心类


    首先如何从apk中获取dex文件:

    ZipFile apkFile = null;try { apkFile = new ZipFile(appContext.getApplicationInfo().sourceDir);} catch (Exception e) { Log.i("multidex", "create zipfile error:"+Log.getStackTraceString(e)); return;}Log.i("multidex", "zipfile:"+apkFile.getName());File filesDir = appContext.getDir("odex", Context.MODE_PRIVATE);Log.i("multidex", "filedir:"+filesDir.getAbsolutePath());for(int i = 0 ; i < SUB_DEX_NUM; i ++){ String possibleDexName = buildDexFullName(i); Log.i("multidex", "proname:"+possibleDexName); ZipEntry zipEntry = apkFile.getEntry(possibleDexName); Log.i("multidex", "entry:"+zipEntry); if(zipEntry == null) {  break; } msLoadedDexList.add(new LoadedDex(filesDir,possibleDexName,zipEntry));}
    先得到应用apk的路径,使用ZipFile来解压apk,获取所有的dex文件,这里我们不需要处理classes.dex文件了,默认就会加载,所以这里进行过滤一下,还有就是subdex的下标都是从2开始的,没有classes1.dex。需要注意的。同时这里获取应用的apk文件是不需要任何权限限制的,类似于QQ中可以分享本地app给好友的功能一样。

    接下来我们获取到了所有的subdex之后,然后就可以动态加载了,需要把我们的dex加载到系统中,这里主要就是使用反射技术合并dexList即可。

    /** * 这里需要注入DexClassLoader * 因为GDT内部点击广告是去下载App,是启动一个DownloadService去下载App * 所以这里需要这么做,不然会报异常 * @param loader */private static void inject(DexClassLoader loader, Context ctx){ PathClassLoader pathLoader = (PathClassLoader) ctx.getClassLoader(); try {  Object dexElements = combineArray(    getDexElements(getPathList(pathLoader)),    getDexElements(getPathList(loader)));  Object pathList = getPathList(pathLoader);  setField(pathList, pathList.getClass(), "dexElements", dexElements); } catch (Exception e) {  Log.i("multidex", "inject dexclassloader error:" + Log.getStackTraceString(e)); }}private static Object getPathList(Object baseDexClassLoader)  throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");}private static Object getField(Object obj, Class<?> cl, String field)  throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { Field localField = cl.getDeclaredField(field); localField.setAccessible(true); return localField.get(obj);}private static Object getDexElements(Object paramObject)  throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException return getField(paramObject, paramObject.getClass(), "dexElements");}private static void setField(Object obj, Class<?> cl, String field,  Object value) throws NoSuchFieldException,  IllegalArgumentException, IllegalAccessException { Field localField = cl.getDeclaredField(field); localField.setAccessible(true); localField.set(obj, value);}private static Object combineArray(Object arrayLhs, Object arrayRhs) { Class<?> localClass = arrayLhs.getClass().getComponentType(); int i = Array.getLength(arrayLhs); int j = i + Array.getLength(arrayRhs); Object result = Array.newInstance(localClass, j); for (int k = 0; k < j; ++k) {  if (k < i) {   Array.set(result, k, Array.get(arrayLhs, k));  } else {   Array.set(result, k, Array.get(arrayRhs, k - i));  } } return result;}

    在MyApplication中的attachBaseContext方法中调用loadSecondaryDex方法即可:



    到这里我们介绍完了脚本和代码,下面就开跑一边脚本吧:ant release


    跑完之后,我们看一下bin目录下多了一个成功的apk文件:


    我们安装运行,结果如下:


    看到了,我们首先启动的是MainActivity入口,然后我们依次点击btn进入不同的Activity。结果运行正常,同时我们也可以查看一下日志,获取dex和加载dex是正常的。



    到这里我们的心情很激动,终于搞定了拆包,遇到一些问题,但是我们都解决了。哈哈~~


    项目下载:http://download.csdn.net/detail/jiangwei0910410003/9452599


    五、梳理流程

    下面我们来总结一下我们做的哪些工作:

    1、首先我们使用脚本进行自动拆包,得到两个dex文件。

    2、因为apkbuilder命令在打apk包的时候,只能包含一个dex文件,所以我们需要在使用aapt命令添加其他的subdex文件

    3、得到应用程序的apk文件,然后得到所有的dex文件,在使用DexClassLoader进行subdex的加载。

    但是关于我们之前说到的拆包的第二个技术,手动拆包这里就没有介绍了,其实很简单,就是在ant脚本中进行分类copy文件就好了,得到了多个dex之后,同样使用aapt命令添加到apk中就可以了。


    六、知识点概要

    学习到了哪些知识点:

    1、更加深入的了解了ant脚本的语法

    2、学习到了使用dx命令来进行自动拆包

    3、复习了DexClassLoader的用法


    七、遇到的错误

    我们在这个过程中可能会遇到一些错误,最多的就是:ClassNotFound和NotDefClass这两个错误,这个处理很简单的,我们查看是哪个类,然后用IDA查看每个dex文件是否包含了这个类,如果没有,那说明没有将这个类归类进去


    八、性能分析

    在冷启动时因为需要加载多个DEX文件,如果DEX文件过大时,处理时间过长,很容易引发ANR(Application Not Responding);
    采用MultiDex方案的应用可能不能在低于Android 4.0 (API level 14) 机器上启动,这个主要是因为Dalvik linearAlloc的一个bug (Issue 22586);采用MultiDex方案的应用因为需要申请一个很大的内存,在运行时可能导致程序的崩溃,这个主要是因为Dalvik linearAlloc 的一个限制(Issue 78035). 这个限制在 Android 4.0 (API level 14)已经增加了, 应用也有可能在低于 Android 5.0 (API level 21)版本的机器上触发这个限制。

    Dex分包后,如果是启动时同步加载,对应用的启动速度会有一定的影响,但是主要影响的是安装后首次启动。这是因为安装后首次启动时,Android系统会对加载的从dex做Dexopt并生成ODEX,而 Dexopt 是比较耗时的操作,所以对安装后首次启动速度影响较大。在非安装后首次启动时,应用只需加载 ODEX,这个过程速度很快,对启动速度影响不大。同时,从dex 的大小也直接影响启动速度,即从dex 越小则启动越快。


    九、总结

    关于Android中的拆包技术网上已经有很多案例和解释了,但是大部分都是理论知识,没有说到细节的,更没有什么demo。所以这篇文章就主要详细介绍了分包的技术点以及用一个demo来进行案例分析,更加深入的了解了拆包技术,本文中虽然用到了是ant脚本进行编译的,但是同样可以使用gradle来进行编译,只要理解了原理,其实都是命令在操作的,和脚本没有任何关系的。从此我们也不会在感觉拆包技术多不觉明历了。其实就是那么回事。


    《Android应用安全防护和逆向分析》

    点击立即购买:京东  天猫

    更多内容:点击这里

    关注微信公众号,最新Android技术实时推送

               

    给我老师的人工智能教程打call!http://blog.csdn.net/jiangjunshow

    这里写图片描述
    展开全文
  • 0x00 Who is Multidex 很多大厂的Android App因为业务量大,引用库多导致其apk包的中的类于方法剧增.这样就有可能出现因为方法数过多导致编译失败的情况.产生这个问题的主因是dex文件格式的限制.一个DEX文件中...

    0x00 Who is Multidex

    很多大厂的Android App因为业务量大,引用库多导致其apk包的中的类于方法剧增.这样就有可能出现因为方法数过多导致编译失败的情况.产生这个问题的主因是dex文件格式的限制.一个DEX文件中method个数采用使用原生类型short来索引文件中的方法,也就是4个字节共计最多表达65536个method,field/class的个数也均有此限制。对于DEX文件,则是将工程所需全部class文件合并且压缩到一个DEX文件期间,也就是Android打包的DEX过程中,单个DEX文件可被引用的方法总数被限制为65536.

    为解决这个问题,谷歌推出了Multidex技术,简单来说就是将一个apk中的dex文件拆分成多个分主次先后加载,当然在这之前业界已经开始使用插件化来弱化此类问题.现在市面上也有很多Multidex和插件化两种方案都使用的app.

    Multidex会给逆向工程师带来如下麻烦:

    1.常见工具静态分析的类和方法不全

    2.静态分析工具因为交叉引用的问题导致反编译失败或崩溃

    3.动态调试中无法下断点

    4.hook的时候找不到制定的类和方法

    0x01 merge multidex

    在逆向apk中经常会遇到一些类丢失,反编译崩溃的情况.如果这时候去观察apk压缩包会发现其中有超过一个dex,上图中就有两个dex.那么这个app分析起来就不会很轻松了,因为这个叫dex分包的技术直接意味着你要面对超过65536个java方法.而这篇文章主要就是讲解笔者在遇到这种情况的时候一些应对手法.

    如果你的dex2jar版本为v2.1之前的版本,那么dex2jar就会默认转化第一个dex文件而忽略其他dex文件. 52f26c6 2.1版本开始支持multidex,直接执行

    d2j-dex2jar.sh the-apk-witch-mutidex.apk

    就可以转化所有dex文件到一个jar包中.

    在dex2jar更新v2.1之前笔者是用的一个比较”耿直”的方法解决这个问题,因为dex的method数量有限但是jar包对method是没有特别限制的,所以我写了一个脚本将apk中多个dex先转化为jar文件之后解压缩最后合并打包压缩,同样可以得到完全的反编译文件.

    Android逆向中出镜率较高的jeb在早期版本v1.x也同样有类似尴尬的问题,只默认反编译第一个dex文件.但是到v2.2也开始支持multidex采用merge的方式解决多个dex的反编译问题.也就是在jeb2.2之后就可以无障碍的在multidex中使用交叉引用功能了.

    在很长一段时间使用jeb1.5的时候解决这个问题的办法同样也也merge多个dex的smali代码,因为要回编译至dex所以就无法突然65K方法数的限制,那么就要想办法对multidex进行瘦身.大数据情况下我们只关心其自有代码,对其依赖库往往是带过的(大多数情况是和配置文件中application或者入口Activity同路径的代码).这里就需要选择一个较小的dex去识别去除一些依赖库和第三方sdk比如android support / google gson,然后抽取另外的dex的主包smali代码合并到较小的dex中.最后回编译至dex拖入jeb1.5中就可以正常分析了.

    0x02 attach multidex 

    在multidex的测试过程中还会出现一种情况,就是使用xposed hook其方法的时候,如果方法位于默认dex中是可以的正常hook,但是如果方法位于dex分包中xposed就会报错提示所要hook的方法所在类无法找到.

    要分析这个问题的原因以及解决办法,就要先了解multidex的载入过程以及xposed的hook时机.

    dex分包加载大致流程如下,可以得出分包是滞后主包不少时间加载的:

    1.检测是否有分包需要安装,系统是否支持multidex

    2.从apk中解压出分包

    3.通过反射将分包注入到当前classloader

    而xposed为了能让module及时载入执行所以得尽快调用handleLoadPackage(),所以此时获取的context的classloader中只要默认dex主的包的类.

    因此我们得想法得到完整的上下文context,比较明显的获取完整context的hook时机有如下两处:

    MultiDex.install()

    MultiDexApplication.attachBaseContext()

    而xposed的作者建议是选择android.app.Application.attach(),因为attachBaseContext是有概率被重写的不稳定.所以选择方法内调用了attachBaseContext的Application.attach().

    示例代码如下

    分析到这里就可以想到一些加壳后的app无法正常hook也可能是类似原因可以用同样的方法解决(这里前提当然是能脱壳看到代码且壳没对hook做对抗,如果有对抗还是脱了之后回打包比较稳妥.).下图这个壳同样也佐证了attachBaseContext被重写的可能,直接hook被重写的attachBaseContext也是可行的.

    0x03refer

    https://developer.android.com/studio/build/multidex.html#about

    http://souly.cn/%E6%8A%80%E6%9C%AF%E5%8D%9A%E6%96%87/2016/02/25/android%E5%88%86%E5%8C%85%E5%8E%9F%E7%90%86/

    http://bbs.pediy.com/showthread.php?t=212332

    https://github.com/pxb1988/dex2jar

    https://github.com/rovo89/XposedBridge/issues/30#issuecomment-68488797

    http://tech.meituan.com/mt-android-auto-split-dex.html

    https://android.googlesource.com/platform/frameworks/multidex/+/master/library/src/android/support/multidex

     

     

    来自:http://drops.wiki/index.php/2016/10/26/android-multidex/

    展开全文
  • Google MultiDex可以解决method id超过65536的问题,但是使用MultiDex又无形之中可能引进了一些坑,比较常见的就是crash和ANR,比如Could not find class,NoClassDefFoundError, Could not find method,那么要如何...
  • Multidex记录一:介绍和使用

    千次阅读 2019-11-15 10:16:14
    Multidex记录一:介绍和使用 Multidex记录二:缺陷&amp;amp;解决 Multidex记录三:源码解析 记录Multidex介绍和使用 为什么要用记录呢,因为我从开始接触Android时我们的项目就在65535的边缘。不久Google就出了...
  • Multidex详解

    2020-04-18 18:41:22
    Multidex记录一:介绍和使用
  • 最终会导致方法超限这一问题,下面让大家了解下什么是MultiDex,讲讲它如何使用,从源码角度说说它的工作原理。 1.认识一下MultiDex 1.1 方法超限问题 当应用及其引用的库包含的方法数超过 65536 时,会遇到一个构建...
  • 问题描述在项目中引用了带so库的jar包之后,项目可以编译通过,但是在安装的时候报错:I/MultiDex: VM with version 2.1.0 has multidex support I/MultiDex: install I/MultiDex: VM has multidex support, ...
  • android studio中gradle版本问题,升级为2.1.0完美解决
  • 程序包android.support.multidex不存在

    千次阅读 2018-01-22 17:07:33
    程序包android.support.multidex不存在  添加:compile "com.android.support:multidex:1.0.0"
  • multidex文件

    2020-07-23 14:15:23
    multidex用于gradle打包时缺少的资源。
  • android.support.content 支持库类 AndroidX 类 android.support.content.ContentPager androidx.contentpager.content.ContentPager android.support.content.InMemoryCursor ...
  • android-support-multidex.jar

    热门讨论 2020-07-30 23:33:29
    android-support-multidex解决Android Dex 65536 65k问题,解决方案来自严振杰的博客:http://blog.csdn.net/yanzhenjie1003/article/details/51818269
  • 如题,我遇到这个问题是这样的,升级了as到2.3后,在6.0以上的系统打包就会遇到这个问题,但是在6.0以下的系统打包没问题,解决方案如下: 在build.gradle文件里... 'com.android.support:multidex:1.0.1'即可!!!
  • 出现java.lang.NoClassDefFoundError异常,排除掉classpath设置问题,发现是multidex后导致的。 为什么要用multidex?项目中因方法数超过了65536,所以用到了MultiDex。 经过筛查,发现这里异常是因为multidex落下...
  • android-support-multidex-instrumentation.jar android-support-multidex-instrumentation.jar
  • 在工程下build.gradle下添加如下内容:参考:https://stackoverflow.com/questions/45608362/android-studio-3-0-beta-1-failed-to-resolve-com-android-supportmultidex1-0-2
  • RN项目在5.0及以后项目都运行正常,5.0之前版本有问题。 com.facebook.react.CoreModulesPackage$1 ...   问题原因,是因为用了MultiDex分包 解决办法: import an...
  • 升级到Android Studio 3.2.1 ,引入...Could not find com.android.support:multidex:1.0.2. Searched in the following locations:  file:/Users/luminal/Library/Android/sdk/extras/m2repository/com/android...
1 2 3 4 5 ... 20
收藏数 6,556
精华内容 2,622
热门标签
关键字:

multidex