精华内容
参与话题
问答
  • Android multidex 使用 与 实现原理

    千次阅读 2018-10-16 15:55:52
    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文件

    2017-03-13 11:44:43
    multidex用于gradle打包时缺少的资源。
  • 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

    这里写图片描述
    展开全文
  • 其实你不知道MultiDex到底有多坑

    千次阅读 2015-10-15 15:57:40
    遭遇MultiDex 愉快地写着Android代码的总悟君往工程里引入了一个默默无闻的jar然后Run了一下, 经过漫长的等待AndroidStudio构建失败了。 于是总悟君带着疑惑查看错误信息。 UNEXPECTED TOP-LEVEL EXCEPTION: ...
    
    

    愉快地写着Android代码的总悟君往工程里引入了一个默默无闻的jar然后Run了一下, 经过漫长的等待AndroidStudio构建失败了。

    于是总悟君带着疑惑查看错误信息。

    UNEXPECTED TOP-LEVEL EXCEPTION: java.lang.IllegalArgumentException: method ID not in [0, 0xffff]: 65536 
    2.    at com.android.dx.merge.DexMerger$6.updateIndex(DexMerger.java:501) 
    3.    at com.android.dx.merge.DexMerger$IdMerger.mergeSorted(DexMerger.java:276) 
    4.    at com.android.dx.merge.DexMerger.mergeMethodIds(DexMerger.java:490) 
    5.    at com.android.dx.merge.DexMerger.mergeDexes(DexMerger.java:167) 
    6.    at com.android.dx.merge.DexMerger.merge(DexMerger.java:188) 
    7.    at com.android.dx.command.dexer.Main.mergeLibraryDexBuffers(Main.java:439) 
    8.    at com.android.dx.command.dexer.Main.runMonoDex(Main.java:287) 
    9.    at com.android.dx.command.dexer.Main.run(Main.java:230) 
    10.    at com.android.dx.command.dexer.Main.main(Main.java:199) 
    11.    at com.android.dx.command.Main.main(Main.java:103):Derp:dexDerpDebug FAILED
    

    看起来是:在试图将 classes和jar塞进一个Dex文件的过程中产生了错误。
    早期的Dex文件保存所有classes的方法个数的范围在0~65535之间。业务一直在增长,总悟君写(copy)的代码越来越长引入的库越来越多,超过这个范围只是时间问题。
    怎么解??太阳底下木有新鲜事,淡定先google一发,找找已经踩过坑的小伙伴。
    StackOverflow 的网友们对该问题表示情绪稳定,谈笑间抛出multiDex。
    这是Android官网对当初的短视行为给出的补丁方案。文档说,Dalvik Executable (DEX)文件的总方法数限制在65536以内,其中包括Android framwork method, lib method (后来总悟君发现仅仅是Android 自己的框架的方法就已经占用了1w多),还有你的 code method ,所以请使用MultiDex。 对于5.0以下版本,请使用multidex support library (这个是我们的补丁包!build tools 请升级到21)。而5.0及以上版本,由于ART模式的存在,app第一次安装之后会进行一次预编译(pre-compilation) ,如果这时候发现了classes(..N).dex文件的存在就会将他们最终合成为一个.oat的文件,嗯看起来很厉害的样子。
    同时Google建议review代码的直接或者间接依赖,尽可能减少依赖库,设置proguard参数进一步优化去除无用的代码。嗯,这两个实施起来倒是很简单,但是治标不治本,躲得过初一躲不过十五。 在Google给出这个解决方案之前,他们的开发人员先给了一个简陋简易版本的multiDex具体参看这里。(怀疑后来的官方解决方案就有这家伙参与)。简单地说就是:1.先把你的app 的class 拆分成主次两个dex。2.你的程序运行起来后,自己把第二个dex给load进来。看就这么简单!而且这就是个动态加载模块的框架! 然而总悟君早已看穿Dalvik VM 这种动态加载dex 的能力归根结底还是因为java 的classloader类加载机制。沿着这条道走,Android模块动态化加载,包括dex级别和apk级别的动态化加载,各种玩法层出不穷。参见这里123456

    还是先解决打包问题,回头再研究那些高深的动态化加载技术。偷懒一下咯考虑到投入产出比,决定使用Google官方的multiDex解决。(Google的补丁方案啊,不会再有坑了吧?后面才发现还是太天真) 该方案有两步:
    1.修改gradle脚本来产生多dex。
    2.修改manifest 使用MulitDexApplication。
    步骤1.在gradle脚本里写上:

    android {
    2.    compileSdkVersion 21
    3.    buildToolsVersion "21.1.0"
    4.
    5.    defaultConfig {
    6.        ...
    7.        minSdkVersion 14
    8.        targetSdkVersion 21
    9.        ...
    10.
    11.        // Enabling multidex support.
    12.        multiDexEnabled true
    13.    }
    14.    ...
    15.}
    16.
    17.dependencies {
    18.compile 'com.android.support:multidex:1.0.0'
    19.}
    

    步骤2. manifest声明修改

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

    如果有自己的Application,继承MulitDexApplication。如果当前代码已经继承自其它Application没办法修改那也行,就重写 Application的attachBaseContext()这个方法。

    @Override
    2.protected void attachBaseContext(Context base) {
    3.    super.attachBaseContext(base);
    4.    MultiDex.install(this);     
    5.
    6.} 
    

    run一下,可以了!但是dex过程好像变慢了。。。
    文档还写明了multiDex support lib 的局限。瞄一下是什么:
    1.在应用安装到手机上的时候dex文件的安装是复杂的(complex)有可能会因为第二个dex文件太大导致ANR。请用proguard优化你的代码。呵呵
    2.使用了mulitDex的App有可能在4.0(api level 14)以前的机器上无法启动,因为Dalvik linearAlloc bug(Issue 22586) 。请多多测试自祈多福。用proguard优化你的代码将减少该bug几率。呵呵
    3.使用了mulitDex的App在runtime期间有可能因为Dalvik linearAlloc limit (Issue 78035) Crash。该内存分配限制在 4.0版本被增大,但是5.0以下的机器上的Apps依然会存在这个限制。
    4.主dex被dalvik虚拟机执行时候,哪些类必须在主dex文件里面这个问题比较复杂。build tools 可以搞定这个问题。但是如果你代码存在反射和native的调用也不保证100%正确。呵呵

    感觉这就是个坑啊。补丁方案又引入一些问题。但是插件化方案要求对现有代码有比较大的改动,代价太大,而且动态化加载框架意味着维护成本更高,会有更多潜在bug。所以先测试,遇到有问题的版本再解决。

    呵呵,部分低端2.3机型(话说2.3版本的android机有高端机型么)安装失败!INSTALL_FAILED_DEXOPT。这个就是前面说的Issue 22586问题。
    apk是一个zip压缩包,dalvik每次加载apk都要从中解压出class.dex文件,加载过程还涉及到dex的classes需要的杂七杂八的依赖库的加载,真耗时间。于是Android决定优化一下这个问题,在app安装到手机之后,系统运行dexopt程序对dex进行优化,将dex的依赖库文件和一些辅助数据打包成odex文件。存放在cache/dalvik_cache目录下。保存格式为apk路径 @ apk名 @ classes.dex。这样以空间换时间大大缩短读取/加载dex文件的过程。
    那刚才那个bug是啥问题呢,原来dexopt程序的dalvik分配一块内存来统计你的app的dex里面的classes的信息,由于classes太多方法太多超过这个linearAlloc 的限制 。那减小dex的大小就可以咯。
    gradle脚本如下:

    android.applicationVariants.all {
    2.    variant ->
    3.        dex.doFirst{
    4.            dex->
    5.            if (dex.additionalParameters == null) {
    6.                dex.additionalParameters = []
    7.            }
    8.                dex.additionalParameters += '--set-max-idx-number=48000'
    9.
    10.       }
    11.}
    

    --set-max-idx-number= 用于控制每一个dex的最大方法个数,写小一点可以产生好几个dex。 踩过更多坑的FB的工程师表示这个linearAlloc的限制不仅仅在安装时候的dexopt程序里7,还在你的app的dalvik rumtime里。(很显然啊dvk vm的宿主进程fork自于同一个母体啊)。为了表示对这个坑的不满以及对Google的产品表示遗憾,FB工程师Read The Fucking Source Code找到了一个hack方案。这个linearAlloc的size定义在c层而且是一个全局变量,他们通过对结构体的size的计算成功覆盖了该值的内容,这里要特别感谢C语言的指针和内存的设计。C的世界里,You Are The King of This World。当然实际情况是大部分用户用这把利刃割伤了自己。。。别问总悟君谁是世界上最好的语言。。。
    为FB的工程师的机智和务实精神点赞!然而总悟君不愿意花那么多精力实现FB的hack方法。(dvk虚拟机c层代码在2.x 4.x 版本里有变更,找到那个内存地址太难,未必搞得定啊)我们有偷懒的解决方案,为了避免2.3机型runtime 的linearAlloclimit ,最好保持每一个dex体积<4M ,刚才的的value<=48000
    好了 现在2.3的机器可以安装run起来了!

    问题又来了!这次不仅仅是2.3 的机型!还有一些中档配置的4.x系统的机型。问题现象是:第一次安装后,点击图标,1s,2s,3s... 程序没有任何反应就好像你没点图标一样。
    5s过去。。。程序ANR!
    其实不仅仅总悟君的App存在这个问题,其他很多App也存在首次安装运行后几秒都无任何响应的现象或者最后ANR了。唯一的例外是美团App,点击图标立马就出现界面。唉要不就算啦?反正就一次。。。不行,这可是产品给用户的第一印象啊太重要了,而且美团搞得定就说明这问题有解决方案。
    ANR了是不是局限1描述的现象??不过也不重要...因为Google只是告诉你说第二个dex太大了导致的。并没有进一步解释根本原因。怎么办?Google一发?搜索点击图标 然后ANR?怎么可能有解决方案嘛。ANR就意味着UI线程被阻塞了,老老实实查看log吧。
    adb logcat -v time > log.txt
    于是发现 是 install dex + dexopt 时间太长!
    梳理一下流程:
    安装完app点击图标之后,系统木有发现对应的process,于是从该apk抽取classes.dex(主dex) 加载,触发 一次dexopt。
    App 的laucherActivity准备启动 ,触发Application启动,
    Application的 onattach()方法调用,这时候MultiDex.install()调用,classes2.dex 被install,再次触发dexopt。
    然后Applicaition onCreate()执行。
    然后 launcher Activity真的起来了。
    这些必须在5s内完成不然就ANR给你看!
    有点棘手。首先主dex是无论如何都绕不过加载和dexopt的。如果主dex比较小的话可以节省时间。主dex小就意味着后面的dex大啊,MultiDex.install()是在主线程里做的,总时间又没有实质性改变。install() 能不能放到线程里做啊?貌似不行。。。如果异步化,什么时候install完成都不知道。这时候如果进程需要second.dex里的classes信息不就悲剧?主dex越小这个错误几率就越大。要悲剧啊总悟君。
    淡定,这次Google搜索MultiDex.install 。于是总悟君发现了美团多dex拆包方案。 读完之后感觉看到胜利曙光。美团的主要思路是:精简主dex+异步加载secondary.dex 。对异步化执行速度的不确定性,他们的解决方案是重写Instrumentation execStartActivity 方法,hook跳转Activity的总入口做判断,如果当前secondary.dex 还没有加载完成,就弹一个loading Activity等待加载完成,如果已经加载完成那最好不过了。不错,RTFSC果然是王道。 可以试一试。
    但是有几个问题需要解决:
    1.分析主dex需要的classes这个脚本比较难写。。。Google文档说过这个问题比较复杂, 而且buildTools 不是已经帮我们搞定了吗?去瞄一下主dex的大小:8M 以及secondary.dex 3M 。 它是如何工作的?文档说dx的时候,先依据manifest里注册的组件生成一个 main-list,然后把这list里的classes所依赖的classes找出来,把他们打成classes.dex就是主dex。剩下的classes都放clsses2.dex(如果使用参数限制dex大小的话可能会有classe3.ex 等等) 。主dex至少含有main-list 的classes + 直接依赖classes ,使用mini-main-list参数可以仅仅包含刚才说的classes。
    关于写分析脚本的思路是:直接使用mini-main-list参数获取build目录下的main-list文件,这样manifest声明的类和他们的直接依赖类搞定的了,那后者的直接依赖类怎么解?这些在dvk runtime也是必须的classes。一个思路是解析class文件获得该class的依赖类。还一个思路是自己使用Dexclassloader 加载dex,然后hook getClass()方法,调用一次就记录一个。都挺折腾的。
    2.由于历史原因,总悟君在维护的App的manifest注册的组件的那些类,承载业务太多,依赖很多三方jar,导致直接依赖类非常多,而且短时间内无法梳理精简,没办法mini化主dex。
    3.Application的启动入口太多。Appication初始化未必是由launcher Activity的启动触发,还有可能是因为Service ,Receiver ,ContentProvider 的启动。 靠拦截重写Instrumentation execStartActivity 解决不了问题。要为 Service ,Receiver ,ContentProvider 分别写基类,然后在oncreate()里判断是否要异步加载secondary.dex。如果需要,弹出Loading Acitvity?用户看到这个会感觉比较怪异。
    结合自身App的实际情况来看美团的拆包方案虽然很美好然但是不能照搬啊。果然不能愉快地回家看动漫了。

    考虑到刚才说的2,3原因,先不要急着动手写分析脚本。总悟君期望找到更好的方案。问题到现在变成了:既希望在Application的attachContext()方法里同步加载secondary.dex,又不希望卡住UI线程。如果思路限制在线程异步化上,确实不可能实现。于是发现了微信开发团队的这篇文章。该文章介绍了关于这一问题 FB/QQ/微信的解决方案。FB的解决思路特别赞,让Launcher Activity在另外一个进程启动!当然这个Launcher Activity就是用来load dex 的 ,load完成就启动Main Activity。
    微信这篇文章给出了一个非常重要的观点:安装完成之后第一次启动时,是secondary.dex的dexopt花费了更多的时间。认识到这点非常重要,使得问题又转化为:在不阻塞UI线程的前提下,完成dexopt,以后都不需要再次dexopt,所以可以在UI线程install dex 了!文章最后给了一个对FB方案的改进版。
    仔细读完感觉完全可行。
    1.对现有代码改动量最小。
    2.该方案不关注Application被哪个组件启动。Activity ,Service ,Receiver ,ContentProvider 都满足。
    3.该方案不限制 Application ,Activity ,Service ,Receiver ,ContentProvider 继续新增业务。
    于是总悟君实现了这篇文章最后介绍的改进版的方法,稍微有一点点扩充。

    流程图如下


    上最终解决问题版的代码!
    在Application里面(这里不要再继承自MultiApplication了,我们要手动加载Dex):

    public class App extends Application {
    2.    @Override
    3.    protected void attachBaseContext(Context base) {
    4.        super .attachBaseContext(base);
    5.        LogUtils.d( "loadDex", "App attachBaseContext ");
    6.        if (!quickStart()) {
    7.            if (needWait(base)){
    8.                waitForDexopt(base);
    9.            }
    10.            MultiDex.install (this );
    11.        } else {
    12.            return;
    13.        }
    14.    }
    15.
    16.    @Override
    17.    public void onCreate() {
    18.        super .onCreate();
    19.        if (quickStart()) {
    20.            return;
    21.        }
    22.        ...
    23.    }
    24.
    25.    public boolean quickStart() {
    26.        if (StringUtils.contains( getCurProcessName(this), ":mini")) {
    27.            LogUtils.d( "loadDex", ":mini start!");
    28.            return true;
    29.        }
    30.        return false ;
    31.    }
    32.    //neead wait for dexopt ?
    33.    private boolean needWait(Context context){
    34.        String flag = get2thDexSHA1(context);
    35.        LogUtils.d( "loadDex", "dex2-sha1 "+flag);
    36.        SharedPreferences sp = context.getSharedPreferences(
    37.                PackageUtil.getPackageInfo(context). versionName, MODE_MULTI_PROCESS);
    38.        String saveValue = sp.getString(KEY_DEX2_SHA1, "");
    39.        return !StringUtils.equals(flag,saveValue);
    40.    }
    41.    /**
    42.     * Get classes.dex file signature
    43.     * @param context
    44.     * @return
    45.     */
    46.    private String get2thDexSHA1(Context context) {
    47.        ApplicationInfo ai = context.getApplicationInfo();
    48.        String source = ai.sourceDir;
    49.        try {
    50.            JarFile jar = new JarFile(source);
    51.            Manifest mf = jar.getManifest();
    52.            Map<String, Attributes> map = mf.getEntries();
    53.            Attributes a = map.get("classes2.dex");
    54.            return a.getValue("SHA1-Digest");
    55.        } catch (Exception e) {
    56.            e.printStackTrace();
    57.        }
    58.        return null ;
    59.    }
    60.    // optDex finish 
    61.    public void installFinish(Context context){
    62.        SharedPreferences sp = context.getSharedPreferences(
    63.                PackageUtil.getPackageInfo(context).versionName, MODE_MULTI_PROCESS);
    64.        sp.edit().putString(KEY_DEX2_SHA1,get2thDexSHA1(context)).commit();
    65.    }
    66.
    67.
    68.    public static String getCurProcessName(Context context) {
    69.        try {
    70.            int pid = android.os.Process.myPid();
    71.            ActivityManager mActivityManager = (ActivityManager) context
    72.                    .getSystemService(Context. ACTIVITY_SERVICE);
    73.            for (ActivityManager.RunningAppProcessInfo appProcess : mActivityManager
    74.                    .getRunningAppProcesses()) {
    75.                if (appProcess.pid == pid) {
    76.                    return appProcess. processName;
    77.                }
    78.            }
    79.        } catch (Exception e) {
    80.            // ignore
    81.        }
    82.        return null ;
    83.    }
    84.    public void waitForDexopt(Context base) {
    85.        Intent intent = new Intent();
    86.        ComponentName componentName = new
    87.                ComponentName( "com.zongwu", LoadResActivity.class.getName());
    88.        intent.setComponent(componentName);
    89.        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    90.        base.startActivity(intent);
    91.        long startWait = System.currentTimeMillis ();
    92.        long waitTime = 10 * 1000 ;
    93.        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR1 ) {
    94.            waitTime = 20 * 1000 ;//实测发现某些场景下有些2.3版本有可能10s都不能完成optdex
    95.        }
    96.        while (needWait(base)) {
    97.            try {
    98.                long nowWait = System.currentTimeMillis() - startWait;
    99.                LogUtils.d("loadDex" , "wait ms :" + nowWait);
    100.                if (nowWait >= waitTime) {
    101.                    return;
    102.                }
    103.                Thread.sleep(200 );
    104.            } catch (InterruptedException e) {
    105.                e.printStackTrace();
    106.            }
    107.        }
    108.    }
    109.}
    

    这里使用了classes(N).dex的方式保存了后面的dex而不是像微信目前的做法放到assest文件夹。前面有说到ART模式会将多个dex优化合并成oat文件。如果放置在asset里面就没有这个好处了。
    Launcher Activity 依然是原来的代码里的WelcomeActivity。
    在Application启动的时候会检测dexopt是否已经完成过,(检测方式是查看sp文件是否有dex文件的SHA1-Digest记录,这里要两个进程读取该sp,读取模式是MODE_MULTI_PROCESS)。如果没有就启动LoadDexActivity(属于:mini进程) 。否则就直接install dex !对,直接install。通过日志发现,已经dexopt的dex文件再次install的时候 只耗费几十毫秒。
    LoadDexActivity 的逻辑比较简单,启动AsyncTask 来install dex 这时候会触发dexopt 。

    public class LoadResActivity extends Activity {
    3.    @Override
    4.    public void onCreate(Bundle savedInstanceState) {
    5.        requestWindowFeature(Window.FEATURE_NO_TITLE);
    6.        super .onCreate(savedInstanceState);
    7.        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN , WindowManager.LayoutParams.FLAG_FULLSCREEN );
    8.        overridePendingTransition(R.anim.null_anim, R.anim.null_anim);
    9.        setContentView(R.layout.layout_load);      
    10.        new LoadDexTask().execute();
    11.    }
    12.    class LoadDexTask extends AsyncTask {
    13.        @Override
    14.        protected Object doInBackground(Object[] params) {
    15.            try {
    16.                MultiDex.install(getApplication());
    17.                LogUtils.d("loadDex" , "install finish" );
    18.                ((App) getApplication()).installFinish(getApplication());
    19.            } catch (Exception e) {
    20.                LogUtils.e("loadDex" , e.getLocalizedMessage());
    21.            }
    22.            return null;
    23.        }
    24.        @Override
    25.        protected void onPostExecute(Object o) {
    26.            LogUtils.d( "loadDex", "get install finish");
    27.            finish();
    28.            System.exit( 0);
    29.        }
    30.    }
    31.    @Override
    32.    public void onBackPressed() {
    33.        //cannot backpress
    34.    }
    

    Manifest.xml 里面

    <activity
    2.    android:name= "com.zongwu.LoadResActivity"
    3.    android:launchMode= "singleTask"
    4.    android:process= ":mini"
    5.    android:alwaysRetainTaskState= "false"
    6.    android:excludeFromRecents= "true"
    7.    android:screenOrientation= "portrait" />
    8.
    9.<activity
    10.    android:name= "com.zongwu.WelcomeActivity"
    11.    android:launchMode= "singleTop"
    12.    android:screenOrientation= "portrait">
    13.    <intent-filter >
    14.        <action android:name="android.intent.action.MAIN"/>
    15.        <category android:name="android.intent.category.LAUNCHER"/>
    16.    </intent-filter >
    17.</activity>
    

    替换Activity默认的出现动画 R.anim.null_anim 文件的定义:

    <set xmlns:android="http://schemas.android.com/apk/res/android">
    2.    <alpha
    3.        android:fromAlpha="1.0"
    4.        android:toAlpha="1.0"
    5.        android:duration="550"/>
    6.</set>
    

    微信开发团队的这篇文章所说,application启动了LoadDexActivity之后,自身不再是前台进程所以怎么hold 线程都不会ANR。

    系统何时会对apk进行dexopt总悟君其实并没有十分明白。通过查看安装运行的日志发现,安装的时候packageManagerService会对classes.dex 进行dexopt 。在调用MultiDex.install()加载 secondary.dex的时候,也会进行一次dexopt 。 这背后的流程到底是怎样的?dexopt是如何在另外一个进程执行的?如果是另外一个进程执行为何会阻塞主app的UI进程? 官方文档并没有详细介绍这个,那就RTFSC一探究竟吧.

    源代码跟踪比较长,移步到这里看吧。

    • MultiDex的问题难点在:要持续解决好几个bug才能最终解决问题。进一步的,想要仔细分辨且解决这些bug,就必须持续探索一些关联性的概念和原理
    • 耗费了这么多时间来解决了Android系统的缺陷是不是有点略伤心。这不应该是Google给出一个比较彻底的解决方案吗?
    • FB的工程师们脑洞好大。思考问题的方式很值得借鉴。
    • 微信团队的文章提到逆向了不少App。哈!总悟君感觉增长知识拓宽视野的新技能加强
    • RTFSC是王道
    • 在查看log的过程中发现一个比较有趣的现象。在App的secondary.dex加载之前居然先加载了某数字公司的dex!(手机没有root但是安装了xx手机助手)再加上之前看到的错误堆栈里Android framework的调用堆栈之间也赫然有他们的代码。总悟君恶意猜测该app利用了某种手段进行了提权,hook了系统框架代码,将自己的代码注入到了每一个应用app的进程里。嗯。。。有趣。。。
    • 嗯今晚已经没有时间看动漫了。。。

    转自:http://zongwu233.github.io/the-touble-of-multidex/

    展开全文
  • 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/

    展开全文
  • Multidex详解

    2020-04-18 18:40:27
    Multidex记录一:介绍和使用
  • MultiDex与热修复实现原理

    千次阅读 2016-06-15 14:12:29
    一、Android的ClassLoader体系 由上图可以看出,在叶子节点上,我们能使用到的是DexClassLoader和PathClassLoader,他们有如下使用场景: ...PathClassLoader是Android应用中的默认加载器,PathClassLoader只能...
  • Multidex记录一:介绍和使用

    千次阅读 2018-10-22 10:53:59
    Multidex记录一:介绍和使用 Multidex记录二:缺陷&amp;amp;解决 Multidex记录三:源码解析 记录Multidex介绍和使用 为什么要用记录呢,因为我从开始接触Android时我们的项目就在65535的边缘。不久Google就出了...
  • dex分包方案概述与multidex包的配置使用

    万次阅读 多人点赞 2016-07-22 10:06:14
    Android分包MultiDex原理 《Android开发艺术探索》博客中间会涉及到dex文件的反编译,参考博文: dex文件的反编译-dex2jar和jd-gui1.dex分包的原因对于功能越来越复杂的app的两大问题 问题一:当项目越来越大,...
  • android MultiDex 原理下遇见的N个深坑(二) 自动化打包工具 multidex原理 这是在一个论坛看到的问题,其实你不知道MultiDex到底有多坑。 解决和遇到的其它问题,请见下一篇文章:android MultiDex 原理下超出方法...
  • android MultiDex multiDex原理(一)

    万次阅读 2016-04-15 15:25:19
    android MultiDex 原理(一) Android分包MultiDex原理详解 MultiDex的产生背景 当Android系统安装一个应用的时候,有一步是对Dex进行优化,这个过程有一个专门的工具来处理,叫DexOpt。DexOpt的执行过程是在第一次...
  • android MultiDex

    2020-09-29 10:46:46
    为什么要使用MultiDexMultiDex 主要是为了解决 “65535 方法数超标” 以及 “INSTALL_FAILED_DEXOPT” 问题。 #### 64K限制 当Android系统启动一个应用的时候,有一步是对Dex进行优化,这个过程有一个专门的...
  • Android-Multidex安装流程解析

    千次阅读 2018-02-01 14:00:40
    Android-Multidex安装流程解析 关于为什么需要引入Multidex支持以及如何配置Multidex可参考官网,本篇不做阐述,本篇着重分析Multidex1.0.2源码进行分析 大家都知道配置Multidex都需要在Application中的添加这样...
  • Android multiDex

    2020-06-22 15:12:32
    gradle 配置 android { ... implementation group: 'androidx.multidex', name: 'multidex', version: '2.0.1' 代码 public class App extends Application { @Override protected void attachBaseCo
  • 安卓app开发的童鞋多数我想都碰到过65535这个最大方法数的问题,如果时间回退到几年前,我想自己一定是一脸懵逼,不过好在google爸爸给我们提供了一个从打包 到加载的完整解决方案multidex,刚接触时并没有深入...
  • Android 应用 (APK) 文件包含 Dalvik Executable (DEX) 文件形式的可执行字节码文件,其中包含用来运行您的应用的已编译代码。Dalvik Executable 规范将可在单个 DEX 文件内可引用的方法总数限制在 65,536,其中包括...
  • Android Multidex导致的App启动缓慢

    千次阅读 2016-07-15 09:27:22
    Android Multidex导致的App启动缓慢 Android社区中多次说到了dex包的65536方法数限制,现在针对这个问题的解决方法是dex分包(Multidexing)。虽然这是google提出的一个很好的解决办法,但是我注意到了它对App的...
  • Android MultiDex初次启动APP优化

    千次阅读 2016-12-09 16:23:04
    安装完成并初次启动APP的时候,5.0以下某些低端机会出现ANR或者长时间卡顿不进入引导页,而罪魁祸首是MultiDex.install(Context context)的dexopt过程耗时过长。
  • Problem 日常开发中,一旦项目变的庞大起来,很容易遇到如下的编译错误: trouble writing output: Too many field references: 131000; max is 65536. You may try using --multi-dex option. ...
  • Android Multidex原理及实现

    千次阅读 2017-08-28 22:09:59
    Android Multidex原理及实现一、什么是分包,分包能解决什么问题? 正常apk打包后的文件目录是含有AndroidManifest.xml、R、resource.arcs(资源的索引)、assets、lib、classes.dex这几个模块,而分包后又是怎么样的...
  • android studio Multidex多分包问题: 我用android studio向导新建了一个android工程,里边只包含一个MainActivity页面,然后我在gradle里开启multidexEnable true, 也引入了 compile '...
  • Android使用Multidex突破64K方法数限制原理解析 其实你不知道MultiDex到底有多坑 简单来说,安装完成并初次启动APP的时候,5.0以下某些低端机会出现ANR或者长时间卡顿不进入引导页,而罪魁祸首是MultiDex.install...
  • build.gradle中加入 第一步 android { multiDexEnabled true ... compile 'com.android.support:multidex:1.0.1' 第三步 在你的Application中加入 @Override protected void attachBas...
  • android multidex异步加载

    千次阅读 2016-12-29 01:22:33
    文章写了很久很久,今天再次看到一个相关的项目的Android-Easy-MultiDex,那么我也把自己的方案分享一下,代码待放。。。技术交流可加 QQ : 1831594078Multidex背景官方文档已经对这个做了比较详述的说明。 简单...
  • Android multidex

    2018-04-27 23:57:11
    学习自经典文章:https://www.cnblogs.com/codingWarrior/p/5111957.html65535原因1.优化dex的内存有限制,只有几兆2.short存储method、field、class。所有最多65535方案除了第一个dex,其他dex都以资源的形式放在...
  • Android MultiDex问题

    2017-03-08 19:04:48
    今天用AS在加入一个library的时候,由于SDK版本不对,导入失败,结果原有的项目gradle又出现问题,编译不通过了,后来把top-level里的android{}去掉问题才解决,后来又出现个大问题 就是multiDex ,解决办法是 在 ...
  • Android 的classLoader在加载APK的时候限制了class.dex包含的Java方法总数不能超过65535,但是现在随便一个复杂一点的App,轻而易举就能超过65535。为了解决这个问题,google推出了官方的解决方案——Multidex 一、...
  • Android MultiDex使用

    2017-06-14 14:55:14
    一、MultiDex出现原因: 下段摘自http://www.cnblogs.com/CharlesGrant/p/5112597.html 当Android系统安装一个应用的时候,有一步是对Dex进行优化,这个过程有一个专门的工具来处理,叫DexOpt。DexOpt的执行...
  • android 解决multidex打包失败的坑

    千次阅读 2017-02-28 18:28:05
    这个问题要从android的65535的限制开始说起,由于公司项目需求越来越多,东西也越来越复杂, 再加上加入了rxjava和tinker热更新,在项目中引入了好多第三方库,这就是导致项目总体method超过了65535 百度了一番,...
  • 就在项目灰度测试前不久,爆出了在 Android 3.0以下手机上安装时出现 INSTALL _ FAILED_DEXOPT,导致安装失败。这一问题意味着项目将不能在 Android 3.0以下的手机上安装使用,对项目的发布有比较大的影响,所以...

空空如也

1 2 3 4 5 ... 20
收藏数 7,775
精华内容 3,110
关键字:

multidex