Android打包系列

[TOC]

打包流程

前言

我們每一個(gè)產(chǎn)品中一般都是由一位同事來(lái)負(fù)責(zé)打包工作的,其他同學(xué)一般是不需要關(guān)心具體的流程的。然而掌握打包的知識(shí)對(duì)我們每一個(gè)人都是必要的,以備不時(shí)之需。

一般打包有兩種方式:

  • 通過(guò)開(kāi)發(fā)工具提供的build入口
Android Studio的編譯按鈕
  • 使用命令行打包 gradle tasksName

不論哪種方式,背后執(zhí)行的都是一整套Google提供的構(gòu)建系統(tǒng)。下面我們就來(lái)一起看一下這一具體流程。

構(gòu)建流程
構(gòu)建流程簡(jiǎn)略圖

上面這個(gè)圖是Android官方提供的打包簡(jiǎn)略流程圖。清晰地展示了一個(gè)Android Project經(jīng)過(guò)編譯和打包后生成apk文件,然后再經(jīng)過(guò)簽名,就可以安裝到設(shè)備上。

具體步驟如下:

  1. 編譯器將您的源代碼轉(zhuǎn)換成DEX(Dalvik Executable) 文件,將資源文件轉(zhuǎn)換成已編譯資源。
  2. APK打包器將DEX文件和已編譯資源合并成單個(gè)APK。不過(guò),必須先將APK簽名,才能將應(yīng)用安裝并部署到Android設(shè)備上。
  3. APK打包器使用密鑰簽署APK:
    a. 如果構(gòu)建的APK是debug版本,那么將使用調(diào)試密鑰簽名,Android會(huì)默認(rèn)提供一個(gè)debug的密鑰。
    b. 如果構(gòu)建的是release版本,會(huì)使用發(fā)布版本的密鑰簽名。要生成自己的簽名文件發(fā)布應(yīng)用可以參考Android的官方文檔。
  4. 在生成最終的APK文件之前還會(huì)使用zipalign工具來(lái)優(yōu)化文件。

生成的APK文件本質(zhì)還是一個(gè)zip文件,只不過(guò)被Google強(qiáng)行修改了一下后綴名稱(chēng)而已。所以我們將APK的后綴修改成.zip就可以查看其包含的內(nèi)容了。如圖所示:

APK包含的文件
  • AndroidManifest.xml
  • assets文件夾:這里面的資源是不經(jīng)過(guò)編譯原樣打包進(jìn)來(lái)的
  • classes.dex:這個(gè)是Java的字節(jié)碼文件,Android會(huì)將所有的class文件全部放到這一個(gè)文件里。
  • META-INFO:關(guān)于簽名的信息存放,應(yīng)用安裝驗(yàn)證簽名的時(shí)候會(huì)驗(yàn)證該文件里面的信息
    -res:資源文件,是被編譯過(guò)的。raw和圖片是保持原樣的,但是其他的文件會(huì)被編譯成二進(jìn)制文件。
  • resources.arsc:保存資源文件的索引,由aapt生成
詳細(xì)流程
詳細(xì)流程
1. 使用aapt進(jìn)行資源預(yù)編譯。

打包的第一步就是將資源進(jìn)行預(yù)編譯,資源文件包括res文件夾下面的所有文件。在編譯的這一步,使用的是aapt工具進(jìn)行的,編譯后將會(huì)生成我們常見(jiàn)的R文件。R文件中是資源對(duì)應(yīng)的ID。每一個(gè)ID都是根據(jù)特殊規(guī)則來(lái)生成的,由4個(gè)字節(jié)組成,用十六進(jìn)制表示:0x PPTTEEEE。其中PP(Package ID)代表的是資源命名空間,TT( Type ID)是資源類(lèi)型標(biāo)識(shí),資源類(lèi)型有animator、anim、color、drawable、layout、menu、raw、string和xml等等若干種。EEEE(Entry ID)則代表的是每一個(gè)資源所出現(xiàn)的順序,呈現(xiàn)自增長(zhǎng)態(tài)。

R文件
2. AIDL文件編譯

AIDL 全稱(chēng)Android Interface definition language。在設(shè)計(jì)進(jìn)程之間通信的時(shí)候需要用定義aidl文件,統(tǒng)一通信接口。Android打包時(shí)會(huì)將aidl文件轉(zhuǎn)化為一個(gè)同名的java接口。

3. Java文件編譯

這一步就是標(biāo)準(zhǔn)的Java編譯的過(guò)程,將.java編譯成.class文件。

4. 生成.dex文件

將所有的.class文件(包括第三方庫(kù)的.class文件)轉(zhuǎn)化成Dalvik文件,即.dex文件。

雖然Android使用Java語(yǔ)言編程。但是Android并不使用標(biāo)準(zhǔn)的Java虛擬機(jī)執(zhí)行java code。Android有自己的虛擬機(jī) Dalvik以及5.0以上的ART。 可以大致了解一下Dalvik虛擬機(jī)和傳統(tǒng)虛擬機(jī)的區(qū)別:

  • Dalvik虛擬機(jī)占用內(nèi)存更少
  • JVM是基于Stack的(Stack based),DVM是基于Register的(Register based). 更進(jìn)一步說(shuō),JVM將局部變量存在stack中,而DVM將變量保存在register中。因此標(biāo)準(zhǔn)java虛擬機(jī)需要更多的指令集,而DVM的指令集更少,另一方面DVM需要用寄存器編碼指令的source 和 destination ,因此Dalvik的一條指令更長(zhǎng)。
  • 在JVM運(yùn)行時(shí),會(huì)根據(jù)需要去load每個(gè)class文件。而Dalvik中所有的class都在一個(gè)dex文件中。
.class文件轉(zhuǎn)化成.dex文件

這里有一個(gè)潛在的危險(xiǎn),由于dex設(shè)計(jì)上的原因,單個(gè)dex內(nèi)最多能引用的方法數(shù)上限為65536,這個(gè)數(shù)字對(duì)于當(dāng)今很多APP來(lái)說(shuō)都已經(jīng)不夠用了。不過(guò)Google也早早就放出了解決方案。這里就不詳細(xì)討論了,有興趣了解可以戳這里。

5. 打包APK

這一步就是講所有的資源文件,dex文件一起打包,生成APK文件。

6. 簽名

第五步生成的APK文件是不能直接安裝到Android手機(jī)上面的,必須經(jīng)過(guò)簽名后才行。

$ keytool -genkey -v -keystore my-release-key.keystore-alias alias_name -keyalg RSA -keysize 2048 -validity 10000

生成keytool生成一個(gè)keyStore,有效期是10000天。

$ jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1-keystore my-release-key.keystore my_application.apk alias_name

通過(guò)jarsinger來(lái)對(duì)APK簽名

7. 對(duì)齊

這是打包的最后一步。對(duì)apk包進(jìn)行對(duì)齊處理,工具為zipalign。對(duì)齊處理即使得所有資源文件距離文件起始偏移為4字節(jié)的整數(shù)倍,這樣通過(guò)內(nèi)存映射訪(fǎng)問(wèn)apk文件時(shí)處理速度更快。

zipalign -c -v 4 application.apk

以上就是Android打包的完成過(guò)程。

多渠道打包

配置gradle實(shí)現(xiàn)多渠道打包

每當(dāng)應(yīng)用發(fā)布一個(gè)新的版本的時(shí)候,我們會(huì)分發(fā)到每一個(gè)應(yīng)用市場(chǎng)中去,比如,360手機(jī)助手,小米應(yīng)用市場(chǎng),華為應(yīng)用市場(chǎng)等。為了能夠統(tǒng)計(jì)每個(gè)應(yīng)用市場(chǎng)的下載量,活躍量我們必須用一個(gè)標(biāo)記來(lái)區(qū)分這些不同市場(chǎng)分發(fā)下去的應(yīng)用,渠道號(hào)也就應(yīng)運(yùn)而生。隨著渠道的不斷增加,需要生成的渠道包也就越來(lái)越多。
在打包的過(guò)程中,我們一般都是使用gradle來(lái)進(jìn)行的。gradle為我們的打包提高了很多的便利,多渠道打包也可以輕松實(shí)現(xiàn)。

  1. 首先在AndroidManifest.xml文件中定義一個(gè)meta-data
<meta-data
    android:name="CHANNEL"
    android:value="${CHANNEL_VALUE}" />
  1. 然后在gradle文件中設(shè)置一下productFlavors
android {  
    productFlavors {
        xiaomi {
            manifestPlaceholders = [CHANNEL_VALUE: "xiaomi"]
        }
        _360 {
            manifestPlaceholders = [CHANNEL_VALUE: "_360"]
        }
        baidu {
            manifestPlaceholders = [CHANNEL_VALUE: "baidu"]
        }
        wandoujia {
            manifestPlaceholders = [CHANNEL_VALUE: "wandoujia"]
        }
    }  
}

productFlavors是為了在同一個(gè)項(xiàng)目中創(chuàng)建應(yīng)用的不同版本。具體的配置信息可以看官方說(shuō)明。

  1. 執(zhí)行gradle aS就可以將所有的渠道包輸出了。
gradle實(shí)現(xiàn)多渠道打包的缺點(diǎn)

雖然gradle配置多渠道打包很簡(jiǎn)單,也很方便,但是這種方式存在一個(gè)致命的缺陷,那就是費(fèi)時(shí)間。因?yàn)?code>AndroidManifest.xml文件被修改過(guò)了,所以所有的包都必須重新編譯簽名。一般來(lái)說(shuō)100個(gè)渠道包就要至少一個(gè)小時(shí)的時(shí)間,這一個(gè)小時(shí)5杯咖啡都不夠等的。更要命的是萬(wàn)一哪里需要微調(diào)一下代碼或者文案,那么不好意思,一切又得重頭來(lái)。這就很麻煩了,所以有沒(méi)有什么方法可以快速完成打包呢?我們繼續(xù)往下看。

多渠道快速打包

快速打包方案Version_1.0

如上所說(shuō),我們?nèi)サ叫畔⒅皇切薷牧艘幌耺anifest文件里面的一個(gè)meta-data的值而已,有沒(méi)有什么辦法可以不需要重新構(gòu)建代碼呢?答案是肯定的。我們可以使用apktool,反編譯我們的APK文件。

apktool d yourApkName build

經(jīng)過(guò)解碼后,我們會(huì)得到如下文件:

解碼后的APK文件

我們發(fā)現(xiàn)我們需要修改的manifest文件就在里面,所以通過(guò)命令可以修改下他的內(nèi)容,然后重新打包,就可以生成一個(gè)全新的渠道包了,省去了重新編譯構(gòu)建代碼的過(guò)程。使用一下Python腳本,將manifest文件里面channel信息進(jìn)行替換。

import re

def replace_channel(channel, manifest):
    pattern = r'(<meta-data\s+android:name="channel"\s+android:value=")(\S+)("\s+/>)'
    replacement = r"\g<1>{channel}\g<3>".format(channel=channel)
    return re.sub(pattern, replacement, manifest)

然后通過(guò)apktool重新將文件夾打包生成APK。

apktool b build your_unsigned_apk

最后,使用jarsigner重新簽名apk:

jarsigner -sigalg MD5withRSA -digestalg SHA1 -keystore your_keystore_path -storepass your_storepass -signedjar your_signed_apk, your_unsigned_apk, your_alias

通過(guò)這上面的一系列過(guò)程,我們可以實(shí)現(xiàn)不重新編譯構(gòu)建項(xiàng)目就生成不同的渠道包。這會(huì)節(jié)省很多的時(shí)間。但是隨著渠道包增加,重新簽名也會(huì)占用很大一部分時(shí)間,那能不能不重新簽名呢?

分析簽名的算法后發(fā)現(xiàn),在打包過(guò)程后的META-INF文件夾下面添加空白文件是不會(huì)對(duì)簽名的結(jié)果產(chǎn)生影響的。

APK解壓文件

所以我們只要像META_INF文件夾里面寫(xiě)入空白的文件來(lái)標(biāo)識(shí)渠道號(hào)就可以了。

通過(guò)Python腳本像APK文件中寫(xiě)入渠道:

import zipfile
zipped = zipfile.ZipFile(your_apk, 'a', zipfile.ZIP_DEFLATED) 
empty_channel_file = "META-INF/mtchannel_{channel}".format(channel=your_channel)
zipped.write(your_empty_file, empty_channel_file)

執(zhí)行后會(huì)在META-INF文件夾下面生成一個(gè)空白文件:

生成空白文件

然后我們?cè)陧?xiàng)目中去讀取這個(gè)空白文件:

public static String getChannel(Context context) {
        ApplicationInfo appinfo = context.getApplicationInfo();
        String sourceDir = appinfo.sourceDir;
        String ret = "";
        ZipFile zipfile = null;
        try {
            zipfile = new ZipFile(sourceDir);
            Enumeration<?> entries = zipfile.entries();
            while (entries.hasMoreElements()) {
                ZipEntry entry = ((ZipEntry) entries.nextElement());
                String entryName = entry.getName();
                if (entryName.startsWith("mtchannel")) {
                    ret = entryName;
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (zipfile != null) {
                try {
                    zipfile.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        String[] split = ret.split("_");
        if (split != null && split.length >= 2) {
            return ret.substring(split[0].length() + 1);

        } else {
            return "";
        }
    }

這樣每生成一個(gè)渠道包只需要復(fù)制一下APK,然后添加一個(gè)空態(tài)的文件到META-INF下面就可以了,這樣100個(gè)渠道包一分鐘之內(nèi)就可以搞定了。

具體的原因可以看下這里。

快速打包方案Version_2.0

上面的方案基本上已經(jīng)比較完美的解決我們打包的問(wèn)題了,然而好景不長(zhǎng),Google在Android 7.0中更新了應(yīng)用的簽名算法-APK Signature Scheme v2,它是一個(gè)對(duì)全文件進(jìn)行簽名的方案,能提供更快的應(yīng)用安裝時(shí)間、對(duì)未授權(quán)APK文件的更改提供更多保護(hù),在默認(rèn)情況下,Android Gradle 2.2.0插件會(huì)使用APK Signature Scheme v2和傳統(tǒng)簽名方案來(lái)簽署你的應(yīng)用。因?yàn)槭菍?duì)全文件進(jìn)行簽名的,所以之前的添加空白文件的方案就沒(méi)有作用了。

不過(guò)目前這個(gè)方案還不是強(qiáng)制性的,我們可以選擇在gradle配置文件中將其關(guān)閉:

android {
    defaultConfig { ... }
    signingConfigs {
      release {
        storeFile file("myreleasekey.keystore")
        storePassword "password"
        keyAlias "MyReleaseKey"
        keyPassword "password"
        v2SigningEnabled false
      }
    }
  }

那么新的簽名方案對(duì)已有的渠道生成方案有什么影響呢?下圖是新的應(yīng)用簽名方案和舊的簽名方案的一個(gè)對(duì)比:

v1和v2版本簽名對(duì)比

新的簽名方案會(huì)在ZIP文件格式的 Central Directory 區(qū)塊所在文件位置的前面添加一個(gè)APK Signing Block區(qū)塊,下面按照Z(yǔ)IP文件的格式來(lái)分析新應(yīng)用簽名方案簽名后的APK包。
整個(gè)APK(ZIP文件格式)會(huì)被分為以下四個(gè)區(qū)塊:

  1. Contents of ZIP entries(from offset 0 until the start of APK Signing Block)
  2. APK Signing Block
  3. ZIP Central Directory
  4. ZIP End of Central Directory

新應(yīng)用簽名方案的簽名信息會(huì)被保存在區(qū)塊2(APK Signing Block)中, 而區(qū)塊1(Contents of ZIP entries)、區(qū)塊3(ZIP Central Directory)、區(qū)塊4(ZIP End of Central Directory)是受保護(hù)的,在簽名后任何對(duì)區(qū)塊1、3、4的修改都逃不過(guò)新的應(yīng)用簽名方案的檢查。

之前的渠道包生成方案是通過(guò)在META-INF目錄下添加空文件,用空文件的名稱(chēng)來(lái)作為渠道的唯一標(biāo)識(shí),之前在META-INF下添加文件是不需要重新簽名應(yīng)用的,這樣會(huì)節(jié)省不少打包的時(shí)間,從而提高打渠道包的速度。但在新的應(yīng)用簽名方案下META-INF已經(jīng)被列入了保護(hù)區(qū)了,向META-INF添加空文件的方案會(huì)對(duì)區(qū)塊1、3、4都會(huì)有影響,新應(yīng)用簽名方案簽署的應(yīng)用經(jīng)過(guò)我們舊的生成渠道包方案處理后,在安裝時(shí)會(huì)報(bào)以下錯(cuò)誤:

Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES: 
Failed to collect certificates from base.apk: META-INF/CERT.SF indicates base.apk is signed using APK Signature Scheme v2, 
but no such signature was found. Signature stripped?]

區(qū)塊1,3,4是受保護(hù)的,任何的修改都會(huì)引起簽名的不一致,但是區(qū)塊2是不受保護(hù)的,所以能不能在區(qū)塊2上面找到解決辦法呢?首先看一下區(qū)塊2的文件結(jié)構(gòu):

區(qū)塊2文件結(jié)構(gòu)

區(qū)塊2中APK Signing Block是由這幾部分組成:2個(gè)用來(lái)標(biāo)示這個(gè)區(qū)塊長(zhǎng)度的8字節(jié) + 這個(gè)區(qū)塊的魔數(shù)(APK Sig Block 42)+ 這個(gè)區(qū)塊所承載的數(shù)據(jù)(ID-value)。

我們重點(diǎn)來(lái)看一下這個(gè)ID-value,它由一個(gè)8字節(jié)的長(zhǎng)度標(biāo)示+4字節(jié)的ID+它的負(fù)載組成。V2的簽名信息是以ID(0x7109871a)的ID-value來(lái)保存在這個(gè)區(qū)塊中,不知大家有沒(méi)有注意這是一組ID-value,也就是說(shuō)它是可以有若干個(gè)這樣的ID-value來(lái)組成,那我們是不是可以在這里做一些文章呢?

對(duì)于簽名的認(rèn)證過(guò)程是這樣的:

  1. 尋找APK Signing Block,如果能夠找到,則進(jìn)行驗(yàn)證,驗(yàn)證成功則繼續(xù)進(jìn)行安裝,如果失敗了則終止安裝
  2. 如果未找到APK Signing Block,則執(zhí)行原來(lái)的簽名驗(yàn)證機(jī)制,也是驗(yàn)證成功則繼續(xù)進(jìn)行安裝,如果失敗了則終止安裝
簽名認(rèn)證過(guò)程

在校驗(yàn)的時(shí)候,檢驗(yàn)代碼如下:

 public static ByteBuffer findApkSignatureSchemeV2Block(
            ByteBuffer apkSigningBlock,
            Result result) throws SignatureNotFoundException {
        checkByteOrderLittleEndian(apkSigningBlock);
        // FORMAT:
        // OFFSET       DATA TYPE  DESCRIPTION
        // * @+0  bytes uint64:    size in bytes (excluding this field)
        // * @+8  bytes pairs
        // * @-24 bytes uint64:    size in bytes (same as the one above)
        // * @-16 bytes uint128:   magic
        ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);

        int entryCount = 0;
        while (pairs.hasRemaining()) {
            entryCount++;
            if (pairs.remaining() < 8) {
                throw new SignatureNotFoundException(
                        "Insufficient data to read size of APK Signing Block entry #" + entryCount);
            }
            long lenLong = pairs.getLong();
            if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
                throw new SignatureNotFoundException(
                        "APK Signing Block entry #" + entryCount
                                + " size out of range: " + lenLong);
            }
            int len = (int) lenLong;
            int nextEntryPos = pairs.position() + len;
            if (len > pairs.remaining()) {
                throw new SignatureNotFoundException(
                        "APK Signing Block entry #" + entryCount + " size out of range: " + len
                                + ", available: " + pairs.remaining());
            }
            int id = pairs.getInt();
            if (id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {
                return getByteBuffer(pairs, len - 4);
            }
            result.addWarning(Issue.APK_SIG_BLOCK_UNKNOWN_ENTRY_ID, id);
            pairs.position(nextEntryPos);
        }

        throw new SignatureNotFoundException(
                "No APK Signature Scheme v2 block in APK Signing Block");
    }

我們可以發(fā)現(xiàn),述代碼中關(guān)鍵的一個(gè)位置是 if (id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {return getByteBuffer(pairs, len - 4);},通過(guò)源代碼可以看出Android是通過(guò)查找ID為 APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a 的ID-value,來(lái)獲取APK Signature Scheme v2 Block,對(duì)這個(gè)區(qū)塊中其他的ID-value選擇了忽略。也就是說(shuō),在APK Signature Scheme v2中沒(méi)有看到對(duì)無(wú)法識(shí)別的ID,有相關(guān)處理的介紹。所以我們可以通過(guò)寫(xiě)入自定義的ID-Value來(lái)自定義渠道。

所以整理一下思路應(yīng)該是這樣的:

  1. 對(duì)新的應(yīng)用簽名方案生成的APK包中的ID-value進(jìn)行擴(kuò)展,提供自定義ID-value(渠道信息),并保存在APK中
  2. 在App運(yùn)行階段,可以通過(guò)ZIP的EOCD(End of central directory)、Central directory等結(jié)構(gòu)中的信息找到我們自己添加的ID-value,從而實(shí)現(xiàn)獲取渠道信息的功能

通過(guò)這個(gè)方案我們同樣可以實(shí)現(xiàn)不重新構(gòu)建不重新簽名就能生成渠道包。美團(tuán)提供了walle供我們使用,詳細(xì)使用方法點(diǎn)這里。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容