공식 MultiDex 소스 분석

11937 단어
65535 문제를 해결하기 위해 지원되는 SDK는 4 이상, 낮으면 이상을 던진다, 안드로이드 5.0개 이상의 VM은 Dex 패키지 로딩을 지원합니다.
주요 원리: 응용DexClassLoader에 dex 파일을 동적으로 추가

프로세스 분석


기본 프로세스


1. 검사(Vm에서 21+와 같은 하도급을 지원했는지, 최저 SDK 버전은 4이고 하도급을 했는지 확인)
2、오래된 dex 하도급의 디렉터리에 있는 파일을 정리한다data/data/packageName/file/secondary-dexes3. Dex 패키지 읽기, 디렉터리 저장data/data/packageName/code_cache/secondary-dexes
  • 3.1은 주로 apk 압축 패키지 아래의classes2를 읽는다.dex、classesN.dex 순차적 쓰기 /data/data/pkgName/code_cache/secondary-dexes/base.apk.classesN.zip 아래
  • 4. 패키지의 dex 압축 패키지가 유효한지 확인하고 무효가 되면 다시 한 번 패키지를 진행합니다
    5.Dex 압축 패키지 파일 설치 로드, DexPathList#makeDexElements 방법으로 dex의 로드를 진행하고 되돌아오는 Element 수조로 원래ClassLoader 아래의 Elements를 확장하여 로드를 실현한다.
    public static void install(Context context) {
        if (IS_VM_MULTIDEX_CAPABLE) {
            Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
            return;
        }
        if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
            throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
        }
        try {
            ApplicationInfo applicationInfo = getApplicationInfo(context);
            if (applicationInfo == null) {
                // Looks like running on a test Context, so just return without patching.
                return;
            }
            synchronized (installedApk) {
                String apkPath = applicationInfo.sourceDir;
                if (installedApk.contains(apkPath)) {  // 
                    return;
                }
                installedApk.add(apkPath);
                if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) {
                    // : 20 dex 
                }
                /*
                 */
                ClassLoader loader;
                try {
                    loader = context.getClassLoader();
                } catch (RuntimeException e) {
                    //  MockContext
                    return;
                }
                if (loader == null) {
                    // Robolectric tests
                    return;
                }
                try {
                  clearOldDexDir(context);  // ( data/data/pkg-name/) secondary-dexes 
                } catch (Throwable t) {
                }
                // data/data/packageName/code_cache/secondary-dexes
                File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
                List files = MultiDexExtractor.load(context, applicationInfo, dexDir, false); // zip 
                if (checkValidZipFiles(files)) {  // zip 
                    installSecondaryDexes(loader, dexDir, files);
                } else {
                    // , 
                }
            }
    
        } catch (Exception e) {
        }
    }
    

    설치 방법

    DexClassLoader 구성할 때 지정한 디렉터리에 있는 zip, dex,jar 등 파일을 읽고 DexFile로 불러오며 Element 그룹을 구성한다. 구성원pathList에 기록된 다음에 클래스의 불러오는 것은 모두 이것DexFile에서 찾으려고 시도하지만 dex가 패키지를 나누면'새로운 dex의 파일 경로'를 스스로 알려줘야 한다DexClassLoader.여기에SDK19+를 예로 들면 (14,15,16,17and18의 차이점은 DexPathList#makeDexElements 방법의 서명 변화에 있다. 4에서 13의 변화는 약간 크지만 지금도 14이하의 것을 개발하지 않으면 자세히 보지 않는다)
    private static void installSecondaryDexes(ClassLoader loader, File dexDir, List files) {
        if (!files.isEmpty()) {
            if (Build.VERSION.SDK_INT >= 19) {
                V19.install(loader, files, dexDir);
            } else if (Build.VERSION.SDK_INT >= 14) {
                V14.install(loader, files, dexDir);
            } else {
                V4.install(loader, files);
            }
        }
    }
    

    주로DexPathList#makeDexElements의 방법으로dex를 불러오고 되돌아오는 Element수조로 원래ClassLoader하의Elements를 확충한다.
    private static final class V19 {
    
        private static void install(ClassLoader loader, List additionalClassPathEntries, File optimizedDirectory) {
    
            Field pathListField = findField(loader, "pathList");  //loader#pathList ,DexPathList 
            Object dexPathList = pathListField.get(loader);
            ArrayList suppressedExceptions = new ArrayList();
            expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
            if (suppressedExceptions.size() > 0) {
                for (IOException e : suppressedExceptions) {
                    Log.w(TAG, "Exception in makeDexElement", e);
                }
                //.....
            }
        }
    
        /**
         * {@code private static final dalvik.system.DexPathList#makeDexElements}.
         *  DexPathList#makeDexElements dex , `Element` 
         */
        private static Object[] makeDexElements(
                Object dexPathList, ArrayList files, File optimizedDirectory, ArrayList suppressedExceptions) {
            Method makeDexElements = findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,  ArrayList.class);
            return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,suppressedExceptions);
        }
    }
    

    Dex 읽기


    Dex의 읽기는 MultiDexExtractor#load 방법으로 수행됩니다.
    MultiDexExtractor.java
    
    static List load(Context context, ApplicationInfo applicationInfo, File dexDir, boolean forceReload) throws IOException {
        final File sourceApk = new File(applicationInfo.sourceDir); //data/app/packageName/base.apk
    
        long currentCrc = getZipCrc(sourceApk); // crc32 , MD5? 
    
        List files;
        // , 
        if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
                //...
                files = loadExistingExtractions(context, sourceApk, dexDir);
                //...
        } else {
            files = performExtractions(sourceApk, dexDir);
            putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1); //dex sp, 
        }
        return files;
    }
    
    private static boolean isModified(Context context, File archive, long currentCrc) {
        SharedPreferences prefs = getMultiDexPreferences(context);//multidex.version
        return (prefs.getLong(KEY_TIME_STAMP, NO_VALUE) != getTimeStamp(archive)) || (prefs.getLong(KEY_CRC, NO_VALUE) != currentCrc);
    }
    

    주로 DEX를 어떻게 읽고 apk 파일의 이름classesNdex을 얻는지ZipEntry, 파일에 쓰기data/data/packageName/code_cache/secondary-dexes/base.apk.classesN.zip, N은dex의 수량, 2로 시작합니다.안드로이드 시스템은 앱을 시작할 때 첫 번째Classes.dex만 불러오기 때문에 다른 DEX는 저희가 인공적으로 설치해야 합니다.
    private static List performExtractions(File sourceApk, File dexDir) throws IOException {
    
        final String extractedFilePrefix = sourceApk.getName() + "classes"; //base.apk.classes
    
        // Ensure that whatever deletions happen in prepareDexDir only happen if the zip that
        // contains a secondary dex file in there is not consistent with the latest apk.  Otherwise,
        // multi-process race conditions can cause a crash loop where one process deletes the zip
        // while another had created it.
        prepareDexDir(dexDir, extractedFilePrefix); // base.apk.classes 
    
        List files = new ArrayList();
    
        final ZipFile apk = new ZipFile(sourceApk); //data/app/packageName/base.apk
        try {
            int secondaryNumber = 2;
    
            ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + "dex"); // ZipEntry
            while (dexFile != null) {
                String fileName = extractedFilePrefix + secondaryNumber + "zip"; //base.classes2.zip, base.classes3.dex、base.classes4.dex、base.classesN.dex
                File extractedFile = new File(dexDir, fileName);   //data/data/packageName/code_cache/secondary-dexes/base.classes2.zip
                files.add(extractedFile);
    
                int numAttempts = 0;
                boolean isExtractionSuccessful = false;
                while (numAttempts < 3 && !isExtractionSuccessful) { // 3 
                    numAttempts++;
                    // Create a zip file (extractedFile) containing only the secondary dex file  (dexFile) from the apk.
                    extract(apk, dexFile, extractedFile, extractedFilePrefix);  //ZipEntry 
    
                    isExtractionSuccessful = verifyZipFile(extractedFile);  // zip 
    
                    // Log the sha1 of the extracted zip file
                    if (!isExtractionSuccessful) {
                        // Delete the extracted file
                        extractedFile.delete();
                        //...
                    }
                }
                if (!isExtractionSuccessful) {
                    //...
                }
                secondaryNumber++;
                dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
            } //end while
        } finally {
            //..
        }
    
        return files;
    }
    
    data/data/packageName/code_cache/secondary-dexes/ 디렉토리에서 시작되지 않은 모든 파일 삭제 base.apk.classes
    /**
     * This removes any files that do not have the correct prefix.
     */
    private static void prepareDexDir(File dexDir, final String extractedFilePrefix) throws IOException {
        /* mkdirs() has some bugs, especially before jb-mr1 and we have only a maximum of one parent
         * to create, lets stick to mkdir().
         */
        File cache = dexDir.getParentFile();
        mkdirChecked(cache);  //`data/data/packageName/code_cache/`
        mkdirChecked(dexDir); //`data/data/packageName/code_cache/secondary-dexes/`
    
        // Clean possible old files
        FileFilter filter = new FileFilter() {
    
            @Override
            public boolean accept(File pathname) {
                return !pathname.getName().startsWith(extractedFilePrefix); // base.apk.classes 
            }
        };
        File[] files = dexDir.listFiles(filter);
        if (files == null) {
            return;
        }
        for (File oldFile : files) {
            if (!oldFile.delete()) {
                Log.w(TAG, "Failed to delete old file " + oldFile.getPath());
            } else {
                Log.i(TAG, "Deleted old file " + oldFile.getPath());
            }
        }
    }
    
    ZipEntry를 파일, 구체적인 파일data/data/packageName/code_cache/secondary-dexes/base.apk.classesN.zip에 쓰기
    /**
    * apk : apk 
    * dexFile : Apk zip dex ,classes2.dex…classesN.dex
    * extractTo : data/data/packageName/code_cache/secondary-dexes/base.apk.classesN.zip
    * extractedFilePrefix : base.apk.classes
    */
    private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, String extractedFilePrefix) {
    
        InputStream in = apk.getInputStream(dexFile);
        ZipOutputStream out = null;
        File tmp = File.createTempFile(extractedFilePrefix, "zip", extractTo.getParentFile());
        try {
            out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
            try {
                ZipEntry classesDex = new ZipEntry("classes.dex");
                // keep zip entry time since it is the criteria used by Dalvik
                classesDex.setTime(dexFile.getTime());
                out.putNextEntry(classesDex);
    
                byte[] buffer = new byte[BUFFER_SIZE];
                int length = in.read(buffer);
                while (length != -1) {
                    out.write(buffer, 0, length);
                    length = in.read(buffer);
                }
                out.closeEntry();
            } finally {
                out.close();
            }
            if (!tmp.renameTo(extractTo)) {
                //...
            }
        } finally {
            closeQuietly(in);
            tmp.delete(); // return status ignored
        }
    }
    

    참고 자료

  • MultiDex 설치 프로세스 소스 분석
  • 안드로이드 컴파일 및 Dex 프로세스 소스 분석
  • 안드로이드 패키지 MultiDex 원리 상해
  • 아메리칸 그룹 안드로이드 DEX 자동 분해 및 동적 로드 프로필
  • 좋은 웹페이지 즐겨찾기