1. 程式人生 > >Multidex記錄三:原始碼解析

Multidex記錄三:原始碼解析

記錄Multidex原始碼解析

為什麼要用記錄呢,因為我從開始接觸Android時我們的專案就在65535的邊緣。不久Google就出了multidex的解決方案。我們也已經接入multidex好多年,但我自己還沒有接入,知其然而不知其所以然。出了問題只能自己找原始碼來分析。前兩篇文章 Multidex記錄一:介紹和使用Multidex記錄二:缺陷&解決 分別講述了怎麼接入和接入時遇到的問題,本博文只是對multidex原始碼學習過程中的分析和理解的記錄。

關於Multidex的相關知識點前兩章已經講的差不多了,這篇文章只分析Multidex的安裝。

流程圖

multidex-flowchart.png

原始碼分析

我們先來看看MultiDex

的安裝日誌:

I/MultiDex: VM with version 1.6.0 does not have multidex support
    Installing application
    MultiDexExtractor.load(/data/app/com.xxx.xxx-1.apk, false, )
I/MultiDex: Blocking on lock /data/data/com.xxx.xxx/code_cache/secondary-dexes/MultiDex.lock
    /data/data/com.xxx.xxx/code_cache/secondary-dexes/MultiDex.
lock locked Detected that extraction must be performed. I/MultiDex: Extraction is needed for file /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes2.zip Extracting /data/data/com.xxx.xxx/code_cache/secondary-dexes/tmp-com.xxx.xxx-1.apk.classes1415547735.zip I/MultiDex: Renaming to /
data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes2.zip Extraction succeeded - length /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes2.zip: 4238720 - crc: 2971858359 Extraction is needed for file /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes3.zip Extracting /data/data/com.xxx.xxx/code_cache/secondary-dexes/tmp-com.xxx.xxx-1.apk.classes-1615165740.zip I/MultiDex: Renaming to /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes3.zip Extraction succeeded - length /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes3.zip: 3106018 - crc: 3138243730 Extraction is needed for file /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes4.zip Extracting /data/data/com.xxx.xxx/code_cache/secondary-dexes/tmp-com.xxx.xxx-1.apk.classes-469912688.zip I/MultiDex: Renaming to /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes4.zip Extraction succeeded - length /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx-1.apk.classes4.zip: 2163715 - crc: 1148318293 load found 3 secondary dex files I/MultiDex: install done

第二次啟動時的日誌:

I/MultiDex: VM with version 1.6.0 does not have multidex support
    Installing application
    MultiDexExtractor.load(/data/app/com.xxx.xxx-1.apk, false, )
    Blocking on lock /data/data/com.xxx.xxx/code_cache/secondary-dexes/MultiDex.lock
    /data/data/com.xxx.xxx/code_cache/secondary-dexes/MultiDex.lock locked
I/MultiDex: loading existing secondary dex files
    load found 3 secondary dex files
    install done

初始化資訊

public final class MultiDex {
    static final String TAG = "MultiDex";
    //老版本dex檔案存放路徑
    private static final String OLD_SECONDARY_FOLDER_NAME = "secondary-dexes";
    //dex檔案存放路徑 code_cache/secondary-dexes
    private static final String SECONDARY_FOLDER_NAME;
    //Multidex最高支援的版本,大於20Android系統已支援
    private static final int MAX_SUPPORTED_SDK_VERSION = 20;
    //Multidex最低支援的版本
    private static final int MIN_SDK_VERSION = 4;
    //vm的版本資訊
    private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2;
    private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1;
    //apk路徑
    private static final Set<String> installedApk;
    //是否支援Multidex
    private static final boolean IS_VM_MULTIDEX_CAPABLE;

    static {
    	//SECONDARY_FOLDER_NAME=code_cache/secondary-dexes
        SECONDARY_FOLDER_NAME = "code_cache" + File.separator + "secondary-dexes";
        installedApk = new HashSet();
        //VM是否已經支援自動Multidex
        IS_VM_MULTIDEX_CAPABLE = isVMMultidexCapable(System.getProperty("java.vm.version"));
    }

Multidex安裝

public static void install(Context context) {
    //VM是否已經支援自動Multidex
    if (IS_VM_MULTIDEX_CAPABLE) {
        Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
    } else if (VERSION.SDK_INT < 4) {
        //Multidex最低支援的版本
        throw new RuntimeException("Multi dex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
    } else {
        try {
            /***部分程式碼省略***/
            //多執行緒鎖
            synchronized(installedApk) {
                //apkPath = data/data/com.xxx.xxx/
                String apkPath = applicationInfo.sourceDir;
                if (installedApk.contains(apkPath)) {
                    return;
                }
                installedApk.add(apkPath);
                /***部分程式碼省略***/
                //清除 /data/data/com.xxx.xxx/files/secondary-dexes 目錄下的檔案
                try {
                    clearOldDexDir(context);
                } catch (Throwable var8) {
                    Log.w("MultiDex", "Something went wrong when trying to clear old MultiDex extraction, continuing without cleaning.", var8);
                }
                //data/data/com.xxx.xxx/code_cache/secondary-dexes
                File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
                //解壓apk,獲得dex的zip檔案列表
                List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
                //校驗zip檔案
                if (checkValidZipFiles(files)) {
                    //安裝dex檔案
                    installSecondaryDexes(loader, dexDir, files);
                } else {
                    //校驗失敗,重新執行解壓(解壓失敗直接丟擲異常)和安裝
                    Log.w("MultiDex", "Files were not valid zip files.  Forcing a reload.");
                    files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
                    if (!checkValidZipFiles(files)) {
                        throw new RuntimeException("Zip files were not valid.");
                    }

                    installSecondaryDexes(loader, dexDir, files);
                }
            }
        }
        /***部分程式碼省略***/
    }
}

Multidex獲取dex檔案

載入dex檔案

/**
 * @param context
 * @param applicationInfo
 * @param dexDir /data/data/com.xxx.xxx/code_cache/secondary-dexes/
 * @param forceReload 是否強制重新載入
 * @return 包含dex的zip檔案列表
 * @throws IOException
 *             if an error occurs when writing the file.
 */
static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir, boolean forceReload) throws IOException {
    //data/data/com.xxx.xxx/
    File sourceApk = new File(applicationInfo.sourceDir);
    //apk的迴圈冗餘校驗碼
    long currentCrc = getZipCrc(sourceApk);
    List files;
    //是否強制執行reload和是否已經解壓過apk
    if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
        try {
            //是否已存在zip檔案
            files = loadExistingExtractions(context, sourceApk, dexDir);
        } catch (IOException var9) {
            files = performExtractions(sourceApk, dexDir);
            putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
        }
    } else {
        //獲取apk中的classes2.dex並且壓縮為zip檔案
        files = performExtractions(sourceApk, dexDir);
        //儲存當前apk的資訊,作為下次有效快取的明證
        putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
    }
    return files;
}
/**
 * @param context
 * @param timeStamp apk的最後一次修改時間
 * @param crc apk的迴圈冗餘校驗碼
 * @param totalDexNumber apk中一共有幾個dex
 */
private static void putStoredApkInfo(Context context, long timeStamp, long crc, int totalDexNumber) {
    SharedPreferences prefs = getMultiDexPreferences(context);
    Editor edit = prefs.edit();
    edit.putLong("timestamp", timeStamp);
    edit.putLong("crc", crc);
    edit.putInt("dex.number", totalDexNumber);
    apply(edit);
}

獲取已經存在的dex的壓縮包

/**
 * @param context
 * @param dexDir /data/data/com.xxx.xxx/code_cache/secondary-dexes/
 * @param sourceApk data/app/com.xxx.xxx-1.apk
 * @return 包含dex的zip檔案列表
 * @throws IOException
 *             if an error occurs when writing the file.
 */
private static List<File> loadExistingExtractions(Context context, File sourceApk, File dexDir) throws IOException {
    //extractedFilePrefix = com.xxx.xxx.apk.classes
    String extractedFilePrefix = sourceApk.getName() + ".classes";
    //totalDexNumber=apk中dex的數量
    int totalDexNumber = getMultiDexPreferences(context).getInt("dex.number", 1);
    List<File> files = new ArrayList(totalDexNumber);
    //主dex已經載入過了,載入class2.dex,class3.dex......
    for(int secondaryNumber = 2; secondaryNumber <= totalDexNumber; ++secondaryNumber) {
        //fileName = com.xxx.xxx.apk.classes2.zip
        String fileName = extractedFilePrefix + secondaryNumber + ".zip";
        //extractedFile = data/data/com.xxx.xxx/code_cache/secondary-dexes/com.wuba.bangjob-1.apk.classes2.zip
        File extractedFile = new File(dexDir, fileName);
        if (!extractedFile.isFile()) {
            throw new IOException("Missing extracted secondary dex file '" + extractedFile.getPath() + "'");
        }

        files.add(extractedFile);
        //校驗zip檔案是否完整
        if (!verifyZipFile(extractedFile)) {
            Log.i("MultiDex", "Invalid zip file: " + extractedFile);
            throw new IOException("Invalid ZIP file.");
        }
    }

    return files;
}

生成dex的壓縮zip檔案

/**
 * @param sourceApk data/app/com.xxx.xxx-1.apk
 * @param dexDir /data/data/com.xxx.xxx/code_cache/secondary-dexes/
 * @return 包含dex的zip檔案列表
 * @throws IOException
 *             if an error occurs when writing the file.
 */
private static List<File> performExtractions(File sourceApk, File dexDir) throws IOException {
    //extractedFilePrefix = com.xxx.xxx.apk.classes
    String extractedFilePrefix = sourceApk.getName() + ".classes";
    //刪除data/data/com.xxx.xxx/code_cache/secondary-dexes/目錄下的檔案
    prepareDexDir(dexDir, extractedFilePrefix);
    List<File> files = new ArrayList();
    ZipFile apk = new ZipFile(sourceApk);
    try {
        //從class2.dex開始
        int secondaryNumber = 2;
        for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {
            //fileName = com.xxx.xxx.apk.classes2.zip
            String fileName = extractedFilePrefix + secondaryNumber + ".zip";
            File extractedFile = new File(dexDir, fileName);
            files.add(extractedFile);
            //最多三次嘗試生成dex的zip檔案
            int numAttempts = 0;
            //標識是否生成dex的zip檔案
            boolean isExtractionSuccessful = false;
            while(numAttempts < 3 && !isExtractionSuccessful) {
                ++numAttempts;
                extract(apk, dexFile, extractedFile, extractedFilePrefix);
                //校驗壓縮檔案是否完整,否則刪除重來
                isExtractionSuccessful = verifyZipFile(extractedFile);
                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;
        }
    }/***部分程式碼省略***/
    return files;
}

將classes2.dex放入zip檔案中

/**
 * @param apk apk的壓縮包
 * @param dexFile apk中的classes2.dex檔案
 * @param extractTo /data/data/com.xxx.xxx/code_cache/secondary-dexes/com.xxx.xxx.apk.classes2.zip
 * @param extractedFilePrefix  com.wuba.bangjob-1.apk.classes
 * @throws IOException
 *             if an error occurs when writing the file.
 */
private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, String extractedFilePrefix) throws IOException, FileNotFoundException {
    InputStream in = apk.getInputStream(dexFile);
    ZipOutputStream out = null;
    ///data/data/com.wuba.bangjob/code_cache/secondary-dexes/com.xxx.xxx.apk.classes1415547735.zip
    File tmp = File.createTempFile(extractedFilePrefix, ".zip", extractTo.getParentFile());
    try {
        out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
        try {
            //將classes2.dex進行壓縮內容名稱為classes.dex
            ZipEntry classesDex = new ZipEntry("classes.dex");
            classesDex.setTime(dexFile.getTime());
            out.putNextEntry(classesDex);
            byte[] buffer = new byte[16384];
            for(int length = in.read(buffer); length != -1; length = in.read(buffer)) {
                out.write(buffer, 0, length);
            }

            out.closeEntry();
        } finally {
            out.close();
        }
        //重新命名檔案為com.xxx.xxx.apk.classes2.zip
        if (!tmp.renameTo(extractTo)) {
            throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + "\" to \"" + extractTo.getAbsolutePath() + "\"");
        }
    }/***部分程式碼省略***/
}

dex檔案的裝載

/**
 * @param loader
 * @param dexDir /data/data/xxx.xxx.xxx/code_cache/secondary-dexes/
 * @param files dex的壓縮zip檔案
 */
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) {//KitKat(19)
            MultiDex.V19.install(loader, files, dexDir);
        } else if (VERSION.SDK_INT >= 14) {//IceCreamSandwich(14,15),JellyBean(16,17,18)
            MultiDex.V14.install(loader, files, dexDir);
        } else {
            MultiDex.V4.install(loader, files);
        }
    }
}
private static final class V19 {
    private V19() {
    }
    /**
     * @param loader PathClassLoader
     * @param additionalClassPathEntries dex壓縮包路徑
     * @param optimizedDirectory opt之後的dex檔案目錄
     */
    private static 
            
           

相關推薦

Multidex記錄原始碼解析

記錄Multidex原始碼解析 為什麼要用記錄呢,因為我從開始接觸Android時我們的專案就在65535的邊緣。不久Google就出了multidex的解決方案。我們也已經接入multidex好多年,但我自己還沒有接入,知其然而不知其所以然。出了問題只能自

自定義spring boot starter三部曲之原始碼分析spring.factories載入過程

本文是《自定義spring boot starter三部曲》系列的終篇,前文中我們開發了一個starter並做了驗證,發現關鍵點在於spring.factories的自動載入能力,讓應用只要依賴starter的jar包即可,今天我們來分析Spring和Spring boot原始碼,瞭解s

DelayQueue阻塞佇列第二章原始碼解析

DelayQueue阻塞佇列系列文章 介紹 DelayQueue是java併發包中提供的延遲阻塞佇列,業務場景一般是下單後多長時間過期,定時執行程式等 1-DelayQueue的組成結構 /** * DelayQueue佇列繼承了AbstractQueue,

python UI自動化實戰記錄pageobject-基類

指令碼思路:使用pageobject模式,寫一個basepage基類,所有頁面的通用方法封裝到基類中。 專案中的測試頁面page1和page2都繼承自basepage基類。可使用基類定義的方法。基類裡會將webdriver和page合二為一,既將webdriver的操作改寫成page的方法。  

Istio技術與實踐01 原始碼解析之Pilot多雲平臺服務發現機制

服務模型 首先,Istio作為一個(微)服務治理的平臺,和其他的微服務模型一樣也提供了Service,ServiceInstance這樣抽象服務模型。如Service的定義中所表達的,一個服務有一個全域名,可以有一個或多個偵聽埠。 type Service struct 

Istio技術與實踐02原始碼解析之Istio on Kubernetes 統一服務發現

前言 文章Istio技術與實踐01: 原始碼解析之Pilot多雲平臺服務發現機制結合Pilot的程式碼實現介紹了Istio的抽象服務模型和基於該模型的資料結構定義,瞭解到Istio上只是定義的服務發現的介面,並未實現服務發現的功能,而是通過Adapter機制以一種可擴充套件

Alink漫談(十八) 原始碼解析 之 多列字串編碼MultiStringIndexer

# Alink漫談(十八) :原始碼解析 之 多列字串編碼MultiStringIndexer [ToC] ## 0x00 摘要 Alink 是阿里巴巴基於實時計算引擎 Flink 研發的新一代機器學習演算法平臺,是業界首個同時支援批式演算法、流式演算法的機器學習平臺。本文將帶領大家來分析Alink中

Alink漫談(十九) 原始碼解析 之 分位點離散化Quantile

# Alink漫談(十九) :原始碼解析 之 分位點離散化Quantile [Toc] ## 0x00 摘要 Alink 是阿里巴巴基於實時計算引擎 Flink 研發的新一代機器學習演算法平臺,是業界首個同時支援批式演算法、流式演算法的機器學習平臺。本文將帶領大家來分析Alink中 Quantile 的

Java集合框架之HashMap原始碼解析 Java集合框架之HashMap原始碼解析

Java集合框架之三:HashMap原始碼解析     版權宣告:本文為博主原創文章,轉載請註明出處,歡迎交流學習!       HashMap在我們的工作中應用的非常廣泛,在工作面試中也經常會被問到,對於這樣一個重要的集合

Spring原始碼解析父子容器的概念

  相信大家現在在使用spring專案開發時可能不只是單單使用spring一個框架進行開發, 可能會用到現在主流的ssm,spring和springmvc一起使用。   而在一起使用的時候我就發現了一個問題,在web.xml配置spring容器初始化的時候存在一個問題。     一般我們在配置sprin

OKHttp 3.10原始碼解析快取機制

本篇我們來講解OKhttp的快取處理,在網路請求中合理地利用本地快取能有效減少網路開銷,提高響應速度。HTTP報頭也定義了很多控制快取策略的域,我們先來認識一下HTTP的快取策略。 一.HTTP快取策略 HTTP快取有多種規則,根據是否需要向伺服器發起請求來分類,我們將其分為兩大類:強制

Redis原始碼解析27叢集()主從複製、故障轉移

一:主從複製          在叢集中,為了保證叢集的健壯性,通常設定一部分叢集節點為主節點,另一部分叢集節點為這些主節點的從節點。一般情況下,需要保證每個主節點至少有一個從節點。          叢集初始化時,每個叢集節點都是以獨立的主節點角色而存在的,通過向叢集節點

ElasticSearch原始碼解析索引建立

我們先來看看索引建立的事例程式碼: Directory directory = FSDirectory.getDirectory("/tmp/testindex"); // Use standard analyzer Analyzer analyzer = new

Spark2.3.2原始碼解析 10. 排程系統 Task任務提交 () TaskScheduler : Executor 任務提交

架構圖:     程式碼提交時序圖 Standalone模式提交執行流程圖:     首先寫一個WordCount程式碼(這個程式碼,為了觀察多個stage操作,我寫了兩個reducebykey

ARouter解析URL跳轉本地頁面原始碼分析

在前面文章中對ARouter中頁面跳轉和原始碼進行了分析,今天我們來學習下通過URL跳轉本地頁面的使用和跳轉原始碼分析。在看這篇文章之前建議小夥伴們先看下ARouter解析一:基本使用及頁面註冊原始碼解析,先對ARouter有個整體的瞭解。通過URL跳轉也免不了前面的準備工作,比如ARouter例項的獲取,初

Spring系列(Spring IoC原始碼解析

一、Spring容器類繼承圖 二、容器前期準備   IoC原始碼解析入口: /** * @desc: ioc原理解析 啟動 * @author: toby * @date: 2019/7/22 22:20 */ public class PrincipleMain { public sta

Tomcat原始碼分析Tomcat啟動載入過程(一)的原始碼解析

Tomcat啟動載入過程(一)的原始碼解析 今天,我將分享用原始碼的方式講解Tomcat啟動的載入過程,關於Tomcat的架構請參閱《Tomcat原始碼分析二:先看看Tomcat的整體架構》一文。 先看看應用情況 在《Servlet與Tomcat執行示例》一文中,我詳細的記錄了Tomcat是如何啟動一個Ser

SpringBoot 原始碼解析)----- Spring Boot 精髓啟動時初始化資料

在我們用 springboot 搭建專案的時候,有時候會碰到在專案啟動時初始化一些操作的需求 ,針對這種需求 spring boot為我們提供了以下幾種方案供我們選擇: ApplicationRunner 與 CommandLineRu

mybatis原始碼配置檔案解析解析typeAliases標籤

在前邊的部落格在分析了mybatis解析settings標籤,《mybatis原始碼配置檔案解析之二:解析settings標籤》。下面來看解析typeAliases標籤的過程。 一、概述 在mybatis核心配置檔案(mybatis-config.xml)中有關typeAliases的配置如下, <t

Spring Boot系列(Spring Boot整合Mybatis原始碼解析

一、Mybatis回顧   1、MyBatis介紹   Mybatis是一個半ORM框架,它使用簡單的 XML 或註解用於配置和原始對映,將介面和Java的POJOs(普通的Java 物件)對映成資料庫中的記錄。   2、Mybatis整體架構  二、Spring Boot整合Mybatis +